diff --git a/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/METADATA b/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..83299c05e2e3fec725c891d7615a076292987010 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/METADATA @@ -0,0 +1,141 @@ +Metadata-Version: 2.4 +Name: setuptools +Version: 78.1.0 +Summary: Easily download, build, install, upgrade, and uninstall Python packages +Author-email: Python Packaging Authority +Project-URL: Source, https://github.com/pypa/setuptools +Project-URL: Documentation, https://setuptools.pypa.io/ +Project-URL: Changelog, https://setuptools.pypa.io/en/stable/history.html +Keywords: CPAN PyPI distutils eggs package management +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Archiving :: Packaging +Classifier: Topic :: System :: Systems Administration +Classifier: Topic :: Utilities +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +License-File: LICENSE +Provides-Extra: test +Requires-Dist: pytest!=8.1.*,>=6; extra == "test" +Requires-Dist: virtualenv>=13.0.0; extra == "test" +Requires-Dist: wheel>=0.44.0; extra == "test" +Requires-Dist: pip>=19.1; extra == "test" +Requires-Dist: packaging>=24.2; extra == "test" +Requires-Dist: jaraco.envs>=2.2; extra == "test" +Requires-Dist: pytest-xdist>=3; extra == "test" +Requires-Dist: jaraco.path>=3.7.2; extra == "test" +Requires-Dist: build[virtualenv]>=1.0.3; extra == "test" +Requires-Dist: filelock>=3.4.0; extra == "test" +Requires-Dist: ini2toml[lite]>=0.14; extra == "test" +Requires-Dist: tomli-w>=1.0.0; extra == "test" +Requires-Dist: pytest-timeout; extra == "test" +Requires-Dist: pytest-perf; sys_platform != "cygwin" and extra == "test" +Requires-Dist: jaraco.develop>=7.21; (python_version >= "3.9" and sys_platform != "cygwin") and extra == "test" +Requires-Dist: pytest-home>=0.5; extra == "test" +Requires-Dist: pytest-subprocess; extra == "test" +Requires-Dist: pyproject-hooks!=1.1; extra == "test" +Requires-Dist: jaraco.test>=5.5; extra == "test" +Provides-Extra: doc +Requires-Dist: sphinx>=3.5; extra == "doc" +Requires-Dist: jaraco.packaging>=9.3; extra == "doc" +Requires-Dist: rst.linker>=1.9; extra == "doc" +Requires-Dist: furo; extra == "doc" +Requires-Dist: sphinx-lint; extra == "doc" +Requires-Dist: jaraco.tidelift>=1.4; extra == "doc" +Requires-Dist: pygments-github-lexers==0.0.5; extra == "doc" +Requires-Dist: sphinx-favicon; extra == "doc" +Requires-Dist: sphinx-inline-tabs; extra == "doc" +Requires-Dist: sphinx-reredirects; extra == "doc" +Requires-Dist: sphinxcontrib-towncrier; extra == "doc" +Requires-Dist: sphinx-notfound-page<2,>=1; extra == "doc" +Requires-Dist: pyproject-hooks!=1.1; extra == "doc" +Requires-Dist: towncrier<24.7; extra == "doc" +Provides-Extra: ssl +Provides-Extra: certs +Provides-Extra: core +Requires-Dist: packaging>=24.2; extra == "core" +Requires-Dist: more_itertools>=8.8; extra == "core" +Requires-Dist: jaraco.text>=3.7; extra == "core" +Requires-Dist: importlib_metadata>=6; python_version < "3.10" and extra == "core" +Requires-Dist: tomli>=2.0.1; python_version < "3.11" and extra == "core" +Requires-Dist: wheel>=0.43.0; extra == "core" +Requires-Dist: platformdirs>=4.2.2; extra == "core" +Requires-Dist: jaraco.functools>=4; extra == "core" +Requires-Dist: more_itertools; extra == "core" +Provides-Extra: check +Requires-Dist: pytest-checkdocs>=2.4; extra == "check" +Requires-Dist: pytest-ruff>=0.2.1; sys_platform != "cygwin" and extra == "check" +Requires-Dist: ruff>=0.8.0; sys_platform != "cygwin" and extra == "check" +Provides-Extra: cover +Requires-Dist: pytest-cov; extra == "cover" +Provides-Extra: enabler +Requires-Dist: pytest-enabler>=2.2; extra == "enabler" +Provides-Extra: type +Requires-Dist: pytest-mypy; extra == "type" +Requires-Dist: mypy==1.14.*; extra == "type" +Requires-Dist: importlib_metadata>=7.0.2; python_version < "3.10" and extra == "type" +Requires-Dist: jaraco.develop>=7.21; sys_platform != "cygwin" and extra == "type" +Dynamic: license-file + +.. |pypi-version| image:: https://img.shields.io/pypi/v/setuptools.svg + :target: https://pypi.org/project/setuptools + +.. |py-version| image:: https://img.shields.io/pypi/pyversions/setuptools.svg + +.. |test-badge| image:: https://github.com/pypa/setuptools/actions/workflows/main.yml/badge.svg + :target: https://github.com/pypa/setuptools/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. |ruff-badge| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + +.. |docs-badge| image:: https://img.shields.io/readthedocs/setuptools/latest.svg + :target: https://setuptools.pypa.io + +.. |skeleton-badge| image:: https://img.shields.io/badge/skeleton-2025-informational + :target: https://blog.jaraco.com/skeleton + +.. |codecov-badge| image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white + :target: https://codecov.io/gh/pypa/setuptools + +.. |tidelift-badge| image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat + :target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme + +.. |discord-badge| image:: https://img.shields.io/discord/803025117553754132 + :target: https://discord.com/channels/803025117553754132/815945031150993468 + :alt: Discord + +|pypi-version| |py-version| |test-badge| |ruff-badge| |docs-badge| |skeleton-badge| |codecov-badge| |discord-badge| + +See the `Quickstart `_ +and the `User's Guide `_ for +instructions on how to use Setuptools. + +Questions and comments should be directed to `GitHub Discussions +`_. +Bug reports and especially tested patches may be +submitted directly to the `bug tracker +`_. + + +Code of Conduct +=============== + +Everyone interacting in the setuptools project's codebases, issue trackers, +chat rooms, and fora is expected to follow the +`PSF Code of Conduct `_. + + +For Enterprise +============== + +Available as part of the Tidelift Subscription. + +Setuptools and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. + +`Learn more `_. diff --git a/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/RECORD b/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..516ee39e6ac072ebd2ca9a08532a76cf3c684d7f --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/RECORD @@ -0,0 +1,872 @@ +_distutils_hack/__init__.py,sha256=34HmvLo07j45Uvd2VR-2aRQ7lJD91sTK6zJgn5fphbQ,6755 +_distutils_hack/__pycache__/__init__.cpython-311.pyc,, +_distutils_hack/__pycache__/override.cpython-311.pyc,, +_distutils_hack/override.py,sha256=Eu_s-NF6VIZ4Cqd0tbbA5wtWky2IZPNd8et6GLt1mzo,44 +distutils-precedence.pth,sha256=JjjOniUA5XKl4N5_rtZmHrVp0baW_LoHsN0iPaX10iQ,151 +pkg_resources/__init__.py,sha256=-rh7XOnTxdGuC-_9FAyu5D6s8BL1UsBehxUtj7a-IVo,126203 +pkg_resources/__pycache__/__init__.cpython-311.pyc,, +pkg_resources/api_tests.txt,sha256=XEdvy4igHHrq2qNHNMHnlfO6XSQKNqOyLHbl6QcpfAI,12595 +pkg_resources/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pkg_resources/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pkg_resources/tests/__pycache__/__init__.cpython-311.pyc,, +pkg_resources/tests/__pycache__/test_find_distributions.cpython-311.pyc,, +pkg_resources/tests/__pycache__/test_integration_zope_interface.cpython-311.pyc,, +pkg_resources/tests/__pycache__/test_markers.cpython-311.pyc,, +pkg_resources/tests/__pycache__/test_pkg_resources.cpython-311.pyc,, +pkg_resources/tests/__pycache__/test_resources.cpython-311.pyc,, +pkg_resources/tests/__pycache__/test_working_set.cpython-311.pyc,, +pkg_resources/tests/data/my-test-package-source/__pycache__/setup.cpython-311.pyc,, +pkg_resources/tests/data/my-test-package-source/setup.cfg,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pkg_resources/tests/data/my-test-package-source/setup.py,sha256=1VobhAZbMb7M9mfhb_NE8PwDsvukoWLs9aUAS0pYhe8,105 +pkg_resources/tests/data/my-test-package-zip/my-test-package.zip,sha256=AYRcQ39GVePPnMT8TknP1gdDHyJnXhthESmpAjnzSCI,1809 +pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/PKG-INFO,sha256=JvWv9Io2PAuYwEEw2fBW4Qc5YvdbkscpKX1kmLzsoHk,187 +pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/SOURCES.txt,sha256=4ClkH8eTovZrdVrJFsVuxdbMEF--lBVSuKonDAPE5Jc,208 +pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/dependency_links.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 +pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/top_level.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 +pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 +pkg_resources/tests/data/my-test-package_zipped-egg/my_test_package-1.0-py3.7.egg,sha256=ZTlMGxjRGiKDNkiA2c75jbQH2TWIteP00irF9gvczbo,843 +pkg_resources/tests/test_find_distributions.py,sha256=U91cov5L1COAIWLNq3Xy4plU7_MnOE1WtXMu6iV2waM,1972 +pkg_resources/tests/test_integration_zope_interface.py,sha256=nzVoK557KZQN0V3DIQ1sVeaCOgt4Kpl-CODAWsO7pmc,1652 +pkg_resources/tests/test_markers.py,sha256=0orKg7UMDf7fnuNQvRMOc-EF9EAP_JTQnk4mtGgbW50,241 +pkg_resources/tests/test_pkg_resources.py,sha256=5Mt4bJQhLCL8j8cC46Uv32Np2Xc1TTxLGawIfET55Fk,17111 +pkg_resources/tests/test_resources.py,sha256=K0LqMAUGpRQ9pUb9K0vyI7GesvtlQvTH074m-E2VQlo,31252 +pkg_resources/tests/test_working_set.py,sha256=lRtGJWIixSwSMSbjHgRxeJEQiLMRXcz3xzJL2qL7eXY,8602 +setuptools-78.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools-78.1.0.dist-info/METADATA,sha256=RESmtUmo67AuFzn0N1r3dWyEH5bwwxB3oYMnPHRivRA,6599 +setuptools-78.1.0.dist-info/RECORD,, +setuptools-78.1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91 +setuptools-78.1.0.dist-info/entry_points.txt,sha256=zkgthpf_Fa9NVE9p6FKT3Xk9DR1faAcRU4coggsV7jA,2449 +setuptools-78.1.0.dist-info/licenses/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +setuptools-78.1.0.dist-info/top_level.txt,sha256=d9yL39v_W7qmKDDSH6sT4bE0j_Ls1M3P161OGgdsm4g,41 +setuptools/__init__.py,sha256=AQsMeRFWz9CQ9aBtcSkbB4Dn8t2w86-GPriklpoAjew,10406 +setuptools/__pycache__/__init__.cpython-311.pyc,, +setuptools/__pycache__/_core_metadata.cpython-311.pyc,, +setuptools/__pycache__/_entry_points.cpython-311.pyc,, +setuptools/__pycache__/_imp.cpython-311.pyc,, +setuptools/__pycache__/_importlib.cpython-311.pyc,, +setuptools/__pycache__/_itertools.cpython-311.pyc,, +setuptools/__pycache__/_normalization.cpython-311.pyc,, +setuptools/__pycache__/_path.cpython-311.pyc,, +setuptools/__pycache__/_reqs.cpython-311.pyc,, +setuptools/__pycache__/_shutil.cpython-311.pyc,, +setuptools/__pycache__/_static.cpython-311.pyc,, +setuptools/__pycache__/archive_util.cpython-311.pyc,, +setuptools/__pycache__/build_meta.cpython-311.pyc,, +setuptools/__pycache__/depends.cpython-311.pyc,, +setuptools/__pycache__/discovery.cpython-311.pyc,, +setuptools/__pycache__/dist.cpython-311.pyc,, +setuptools/__pycache__/errors.cpython-311.pyc,, +setuptools/__pycache__/extension.cpython-311.pyc,, +setuptools/__pycache__/glob.cpython-311.pyc,, +setuptools/__pycache__/installer.cpython-311.pyc,, +setuptools/__pycache__/launch.cpython-311.pyc,, +setuptools/__pycache__/logging.cpython-311.pyc,, +setuptools/__pycache__/modified.cpython-311.pyc,, +setuptools/__pycache__/monkey.cpython-311.pyc,, +setuptools/__pycache__/msvc.cpython-311.pyc,, +setuptools/__pycache__/namespaces.cpython-311.pyc,, +setuptools/__pycache__/package_index.cpython-311.pyc,, +setuptools/__pycache__/sandbox.cpython-311.pyc,, +setuptools/__pycache__/unicode_utils.cpython-311.pyc,, +setuptools/__pycache__/version.cpython-311.pyc,, +setuptools/__pycache__/warnings.cpython-311.pyc,, +setuptools/__pycache__/wheel.cpython-311.pyc,, +setuptools/__pycache__/windows_support.cpython-311.pyc,, +setuptools/_core_metadata.py,sha256=T7Tjp-WSoN881adev3R1wzXCPnkDHqbC2MgylN1yjS8,11978 +setuptools/_distutils/__init__.py,sha256=xGYuhWwLG07J0Q49BVnEjPy6wyDcd6veJMDJX7ljlyM,359 +setuptools/_distutils/__pycache__/__init__.cpython-311.pyc,, +setuptools/_distutils/__pycache__/_log.cpython-311.pyc,, +setuptools/_distutils/__pycache__/_macos_compat.cpython-311.pyc,, +setuptools/_distutils/__pycache__/_modified.cpython-311.pyc,, +setuptools/_distutils/__pycache__/_msvccompiler.cpython-311.pyc,, +setuptools/_distutils/__pycache__/archive_util.cpython-311.pyc,, +setuptools/_distutils/__pycache__/ccompiler.cpython-311.pyc,, +setuptools/_distutils/__pycache__/cmd.cpython-311.pyc,, +setuptools/_distutils/__pycache__/core.cpython-311.pyc,, +setuptools/_distutils/__pycache__/cygwinccompiler.cpython-311.pyc,, +setuptools/_distutils/__pycache__/debug.cpython-311.pyc,, +setuptools/_distutils/__pycache__/dep_util.cpython-311.pyc,, +setuptools/_distutils/__pycache__/dir_util.cpython-311.pyc,, +setuptools/_distutils/__pycache__/dist.cpython-311.pyc,, +setuptools/_distutils/__pycache__/errors.cpython-311.pyc,, +setuptools/_distutils/__pycache__/extension.cpython-311.pyc,, +setuptools/_distutils/__pycache__/fancy_getopt.cpython-311.pyc,, +setuptools/_distutils/__pycache__/file_util.cpython-311.pyc,, +setuptools/_distutils/__pycache__/filelist.cpython-311.pyc,, +setuptools/_distutils/__pycache__/log.cpython-311.pyc,, +setuptools/_distutils/__pycache__/spawn.cpython-311.pyc,, +setuptools/_distutils/__pycache__/sysconfig.cpython-311.pyc,, +setuptools/_distutils/__pycache__/text_file.cpython-311.pyc,, +setuptools/_distutils/__pycache__/unixccompiler.cpython-311.pyc,, +setuptools/_distutils/__pycache__/util.cpython-311.pyc,, +setuptools/_distutils/__pycache__/version.cpython-311.pyc,, +setuptools/_distutils/__pycache__/versionpredicate.cpython-311.pyc,, +setuptools/_distutils/__pycache__/zosccompiler.cpython-311.pyc,, +setuptools/_distutils/_log.py,sha256=i-lNTTcXS8TmWITJ6DODGvtW5z5tMattJQ76h8rZxQU,42 +setuptools/_distutils/_macos_compat.py,sha256=JzUGhF4E5yIITHbUaPobZEWjGHdrrcNV63z86S4RjBc,239 +setuptools/_distutils/_modified.py,sha256=RF1n1CexyDYV3lvGbeXS0s-XCJVboDOIUbA8wEQqYTY,3211 +setuptools/_distutils/_msvccompiler.py,sha256=9PSfSHxvJnHnQL6Sqz4Xcz7iaBIT62p6BheQzGsSlwo,335 +setuptools/_distutils/archive_util.py,sha256=Qw2z-Pt-NV8lNUQrzjs3XDGWCWHMPnqHLyt8TiD2XEA,8884 +setuptools/_distutils/ccompiler.py,sha256=FKVjqzGJ7c-FtouNjhLiaMPm5LKMZHHAruXf8LU216c,524 +setuptools/_distutils/cmd.py,sha256=hXtaRaH7QBnfNOIqEvCt47iwZzD9MVvBdhhdQctHsxM,22186 +setuptools/_distutils/command/__init__.py,sha256=GfFAzbBqk1qxSH4BdaKioKS4hRRnD44BAmwEN85C4u8,386 +setuptools/_distutils/command/__pycache__/__init__.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/_framework_compat.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/bdist.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/bdist_dumb.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/bdist_rpm.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/build.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/build_clib.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/build_ext.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/build_py.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/build_scripts.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/check.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/clean.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/config.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/install.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/install_data.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/install_egg_info.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/install_headers.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/install_lib.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/install_scripts.cpython-311.pyc,, +setuptools/_distutils/command/__pycache__/sdist.cpython-311.pyc,, +setuptools/_distutils/command/_framework_compat.py,sha256=0iZdSJYzGRWCCvzRDKE-R0-_yaAYvFMd1ylXb2eYXug,1609 +setuptools/_distutils/command/bdist.py,sha256=jWtk61R7fWNUUNxJV0thTZzU5n80L3Ay1waSiP9kiLA,5854 +setuptools/_distutils/command/bdist_dumb.py,sha256=Hx1jAqoZNxYIy4N5TLzUp6J5fi8Ls18py7UlLNFhO2E,4631 +setuptools/_distutils/command/bdist_rpm.py,sha256=nxcXXv5a7B-1ntWu4DbGmCtES4EBINrJaBQcRNAYCJI,21785 +setuptools/_distutils/command/build.py,sha256=SpHlagf0iNaKVyIhxDfhPFZ8X1-LAWOCQACy-yt2K0w,5923 +setuptools/_distutils/command/build_clib.py,sha256=aMqZcUfCbOAu_xr-A9iW-Q9YZHzpDGLRTezOgMQJmSQ,7777 +setuptools/_distutils/command/build_ext.py,sha256=zrrsu9HXnzV6bXYbJuZCK4SwVZMjKnl4pG1o3bNcxtc,32710 +setuptools/_distutils/command/build_py.py,sha256=Vfq-INemoMbg6f003BTy_Ufp8bjOZhmFIhpKMcfXLgs,16696 +setuptools/_distutils/command/build_scripts.py,sha256=tUpEzwTsnrP8qrNory3ldPB240QDCFkMp-pdYPs2wTk,5118 +setuptools/_distutils/command/check.py,sha256=yoNe2MPY4JcTM7rwoIQdfZ75q5Ri058I2coi-Gq9CjM,4946 +setuptools/_distutils/command/clean.py,sha256=dQAacOabwBXU9JoZ-1GFusq3eFltDaeXJFSYncqGbvE,2644 +setuptools/_distutils/command/config.py,sha256=BuUdPu91Vo9w44xzD1RQfiyXfSfVcNpeX3aeoKcGAOw,12818 +setuptools/_distutils/command/install.py,sha256=-JenB-mua4hc2RI_-W8F9PnP_J-OaFO7E0PJGKxLo1o,30072 +setuptools/_distutils/command/install_data.py,sha256=GzBlUWWKubTYJlP-L0auUriq9cL-5RKOcoyHTttKj0Q,2875 +setuptools/_distutils/command/install_egg_info.py,sha256=ffiLoU1ivQJ8q2_WL7ZygZbUcOsgdFLKL7otEIJWWkI,2868 +setuptools/_distutils/command/install_headers.py,sha256=5ciKCj8c3XKsYNKdkdMvnypaUCKcoWCDeeZij3fD-Z4,1272 +setuptools/_distutils/command/install_lib.py,sha256=2s9-m5-b1qKm51F28lB5L39Z6vv_GHMlv9dNBSupok0,8588 +setuptools/_distutils/command/install_scripts.py,sha256=M0pPdiaqB7TGmqTMujpGGeiL0Iq_CTeGjMFtrmDmwzM,2002 +setuptools/_distutils/command/sdist.py,sha256=cRIF6Ht1hJ6ayOOFVycMFBUNxjo94e_rFYPx4Hi8Ahc,19151 +setuptools/_distutils/compat/__init__.py,sha256=J20aXGjJ86Rg41xFLIWlcWCgZ9edMdJ9vvdNEQ87vPQ,522 +setuptools/_distutils/compat/__pycache__/__init__.cpython-311.pyc,, +setuptools/_distutils/compat/__pycache__/numpy.cpython-311.pyc,, +setuptools/_distutils/compat/__pycache__/py39.cpython-311.pyc,, +setuptools/_distutils/compat/numpy.py,sha256=UFgneZw9w97g4c-yGoAIOyLxUOWQ-fPRIhhfMs7_Ouc,167 +setuptools/_distutils/compat/py39.py,sha256=hOsD6lwZLqZoMnacNJ3P6nUA-LJQhEpVtYTzVH0o96M,1964 +setuptools/_distutils/compilers/C/__pycache__/base.cpython-311.pyc,, +setuptools/_distutils/compilers/C/__pycache__/cygwin.cpython-311.pyc,, +setuptools/_distutils/compilers/C/__pycache__/errors.cpython-311.pyc,, +setuptools/_distutils/compilers/C/__pycache__/msvc.cpython-311.pyc,, +setuptools/_distutils/compilers/C/__pycache__/unix.cpython-311.pyc,, +setuptools/_distutils/compilers/C/__pycache__/zos.cpython-311.pyc,, +setuptools/_distutils/compilers/C/base.py,sha256=XR1rBCStCquqm7QOYXD41-LfvsFcPpGxrwxeXzJyn_w,54876 +setuptools/_distutils/compilers/C/cygwin.py,sha256=DUlwQSb55aj7OdcmcddrmCmVEjEaxIiJ5hHUO3GBPNs,11844 +setuptools/_distutils/compilers/C/errors.py,sha256=sKOVzJajMUmNdfywo9UM_QQGsKFcclDhtI5TlCiXMLc,573 +setuptools/_distutils/compilers/C/msvc.py,sha256=elzG8v9jN5QytLMwLCdUdSuZ3eZ3R98VUvnm9Y2PBCA,21404 +setuptools/_distutils/compilers/C/tests/__pycache__/test_base.cpython-311.pyc,, +setuptools/_distutils/compilers/C/tests/__pycache__/test_cygwin.cpython-311.pyc,, +setuptools/_distutils/compilers/C/tests/__pycache__/test_mingw.cpython-311.pyc,, +setuptools/_distutils/compilers/C/tests/__pycache__/test_msvc.cpython-311.pyc,, +setuptools/_distutils/compilers/C/tests/__pycache__/test_unix.cpython-311.pyc,, +setuptools/_distutils/compilers/C/tests/test_base.py,sha256=rdhHc56bhXtm5NnN9BSHwr6c69UqzMItZQzlw2AsdMc,2706 +setuptools/_distutils/compilers/C/tests/test_cygwin.py,sha256=UgV2VgUXj3VulcbDc0UBWfEyJDx42tgSwS4LzHix3mY,2701 +setuptools/_distutils/compilers/C/tests/test_mingw.py,sha256=hCmwyywISpRoyOySbFHBL4TprWRV0mUWDKmOLO8XBXE,1900 +setuptools/_distutils/compilers/C/tests/test_msvc.py,sha256=DlGjmZ1mBSMXIgmlu80BKc7V-EJOZuYucwJwFh5dn28,4151 +setuptools/_distutils/compilers/C/tests/test_unix.py,sha256=ErbYWio7ejY2ZqQmPk4AwOu1HFW4-_-aZdUvGa1W2Fw,11834 +setuptools/_distutils/compilers/C/unix.py,sha256=l7CxY4rDJAECJo-vcv6io0SBmmPJ9JmN5mSmZcin2VU,16502 +setuptools/_distutils/compilers/C/zos.py,sha256=vnNeWLRZkdIkdZ-YyBnL8idTUfcCOn0tLMW5OBJ0ScU,6586 +setuptools/_distutils/core.py,sha256=GEHKaFC48T3o-_SmH4864GvKyx1IgbVC6ISIPVlx7a4,9364 +setuptools/_distutils/cygwinccompiler.py,sha256=mG_cU8SVZ4amD_VtF5vH6BXP0-kghGsDPbDSXrQ963c,594 +setuptools/_distutils/debug.py,sha256=N6MrTAqK6l9SVk6tWweR108PM8Ol7qNlfyV-nHcLhsY,139 +setuptools/_distutils/dep_util.py,sha256=xN75p6ZpHhMiHEc-rpL2XilJQynHnDNiafHteaZ4tjU,349 +setuptools/_distutils/dir_util.py,sha256=DXPUlfVVGsg9B-Jgg4At_j9T7vM60OgwNXkQHqTo7-I,7236 +setuptools/_distutils/dist.py,sha256=gW598UE0WMkzXQQ31Nr-8L7MPw0oIOz5OSSRzYZlwrM,55794 +setuptools/_distutils/errors.py,sha256=PPE2oDRh5y9Q1beKK9rhdvDaCzQhi4HCXs4KcqfqgZY,3092 +setuptools/_distutils/extension.py,sha256=Foyu4gULcPqm1_U9zrYYHxNk4NqglXv1rbsOk_QrSds,11155 +setuptools/_distutils/fancy_getopt.py,sha256=PjdO-bWCW0imV_UN-MGEw9R2GP2OiE8pHjITgmTAY3Q,17895 +setuptools/_distutils/file_util.py,sha256=YFQL_pD3hLuER9II_H6-hDC_YIGEookdd4wedLuiTW0,7978 +setuptools/_distutils/filelist.py,sha256=MBeSRJmPcKmDv8ooZgSU4BiQPZ0Khwv8l_jhD50XycI,15337 +setuptools/_distutils/log.py,sha256=VyBs5j7z4-K6XTEEBThUc9HyMpoPLGtQpERqbz5ylww,1200 +setuptools/_distutils/spawn.py,sha256=zseCh9sEifyp0I5Vg719JNIASlROJ2ehXqQnHlpt89Q,4086 +setuptools/_distutils/sysconfig.py,sha256=KeI8OHbMuEzHJ8Q0cBez9KZny8iRy6Z6Y0AkMz1jlsU,19728 +setuptools/_distutils/tests/__init__.py,sha256=j-IoPZEtQv3EOPuqNTwalr6GLyRjzCC-OOaNvZzmHsI,1485 +setuptools/_distutils/tests/__pycache__/__init__.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/support.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_archive_util.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_bdist.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_bdist_dumb.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_bdist_rpm.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_build.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_build_clib.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_build_ext.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_build_py.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_build_scripts.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_check.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_clean.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_cmd.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_config_cmd.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_core.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_dir_util.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_dist.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_extension.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_file_util.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_filelist.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_install.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_install_data.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_install_headers.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_install_lib.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_install_scripts.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_log.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_modified.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_sdist.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_spawn.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_sysconfig.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_text_file.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_util.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_version.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/test_versionpredicate.cpython-311.pyc,, +setuptools/_distutils/tests/__pycache__/unix_compat.cpython-311.pyc,, +setuptools/_distutils/tests/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_distutils/tests/compat/__pycache__/__init__.cpython-311.pyc,, +setuptools/_distutils/tests/compat/__pycache__/py39.cpython-311.pyc,, +setuptools/_distutils/tests/compat/py39.py,sha256=t0GBTM-30jX-9zCfkwlNBFtzzabemx6065mJ0d9_VRw,1026 +setuptools/_distutils/tests/support.py,sha256=tjsYsyxvpTK4NrkCseh2ujvDIGV0Mf_b5SI5fP2T0yM,4099 +setuptools/_distutils/tests/test_archive_util.py,sha256=jozimSwPBF-JoJfN_vDaiVGZp66BNcWZGh34FlW57DQ,11787 +setuptools/_distutils/tests/test_bdist.py,sha256=xNHxUsLlHsZQRwkzLb_iSD24s-9Mk-NX2ffBWwOyPyc,1396 +setuptools/_distutils/tests/test_bdist_dumb.py,sha256=QF05MHNhPOdZyh88Xpw8KsO64s7pRFkl8KL-RoV4XK0,2247 +setuptools/_distutils/tests/test_bdist_rpm.py,sha256=Hdm-pwWgyaoGdGbEcGZa8cRhGU45y8gHK8umOanTjik,3932 +setuptools/_distutils/tests/test_build.py,sha256=JJY5XpOZco25ZY0pstxl-iI8mHsWP0ujf5o8aOtuZYY,1742 +setuptools/_distutils/tests/test_build_clib.py,sha256=Mo1ZFb4C1VXBYOGvnallwN7YCnTtr24akLDO8Zi4CsY,4331 +setuptools/_distutils/tests/test_build_ext.py,sha256=QFO9qYVhWWdJu17HXc4x9RMnLZlhk0lAHi9HVppbuX4,22545 +setuptools/_distutils/tests/test_build_py.py,sha256=NsfmRrojOHBXNMqWR_mp5g4PLTgjhD7iZFUffGZFIdw,6882 +setuptools/_distutils/tests/test_build_scripts.py,sha256=cD-FRy-oX55sXRX5Ez5xQCaeHrWajyKc4Xuwv2fe48w,2880 +setuptools/_distutils/tests/test_check.py,sha256=hHSV07qf7YoSxGsTbbsUQ9tssZz5RRNdbrY1s2SwaFI,6226 +setuptools/_distutils/tests/test_clean.py,sha256=hPH6jfIpGFUrvWbF1txkiNVSNaAxt2wq5XjV499zO4E,1240 +setuptools/_distutils/tests/test_cmd.py,sha256=bgRB79mitoOKR1OiyZHnCogvGxt3pWkxeTqIC04lQWQ,3254 +setuptools/_distutils/tests/test_config_cmd.py,sha256=Zs6WX0IfxDvmuC19XzuVNnYCnTr9Y-hl73TAmDSBN4Y,2664 +setuptools/_distutils/tests/test_core.py,sha256=L7XKVAxa-MGoAZeANopnuK9fRKneYhkSQpgw8XQvcF8,3829 +setuptools/_distutils/tests/test_dir_util.py,sha256=E84lC-k4riVUwURyWaQ0Jqx2ui2-io-0RuJa3M7qkJs,4500 +setuptools/_distutils/tests/test_dist.py,sha256=a6wlc5fQJd5qQ6HOndzcupNhjTxvj6-_JLtpuYvaP1M,18793 +setuptools/_distutils/tests/test_extension.py,sha256=-YejLgZCuycFrOBd64pVH0JvwMc9NwhzHvQxvvjXHqk,3670 +setuptools/_distutils/tests/test_file_util.py,sha256=livjnl3FkilQlrB2rFdFQq9nvjEVZHynNya0bfzv_b4,3522 +setuptools/_distutils/tests/test_filelist.py,sha256=rJwkqCUfkGDgWlD22TozsT8ycbupMHB8DXqThzwT1T4,10766 +setuptools/_distutils/tests/test_install.py,sha256=TfCB0ykhIxydIC2Q4SuTAZzSHvteMHgrBL9whoSgK9Q,8618 +setuptools/_distutils/tests/test_install_data.py,sha256=vKq3K97k0hBAnOg38nmwEdf7cEDVr9rTVyCeJolgb4A,2464 +setuptools/_distutils/tests/test_install_headers.py,sha256=PVAYpo_tYl980Qf64DPOmmSvyefIHdU06f7VsJeZykE,936 +setuptools/_distutils/tests/test_install_lib.py,sha256=qri6Rl-maNTQrNDV8DbeXNl0hjsfRIKiI4rfZLrmWBI,3612 +setuptools/_distutils/tests/test_install_scripts.py,sha256=KE3v0cDkFW-90IOID-OmZZGM2mhy-ZkEuuW7UXS2SHw,1600 +setuptools/_distutils/tests/test_log.py,sha256=isFtOufloCyEdZaQOV7cVUr46GwtdVMj43mGBB5XH7k,323 +setuptools/_distutils/tests/test_modified.py,sha256=h1--bOWmtJo1bpVV6uRhdnS9br71CBiNDM1MDwSGpug,4221 +setuptools/_distutils/tests/test_sdist.py,sha256=cfzUhlCA418-1vH9ta3IBs26c_jUBbkJoFOK5GnAyNk,15062 +setuptools/_distutils/tests/test_spawn.py,sha256=eS8w9D7bTxyFLSyRahJWeuh8Kc1F8RWWiY_dSG5B5Bc,4803 +setuptools/_distutils/tests/test_sysconfig.py,sha256=lxM8LsUi1TomjDV4HoYK8u5nUoBkeNL60Uq8PY1DcwU,11986 +setuptools/_distutils/tests/test_text_file.py,sha256=WQWSB5AfdBDZaMA8BFgipJPnsJb_2SKMfL90fSkRVtw,3460 +setuptools/_distutils/tests/test_util.py,sha256=H9zlZ4z4Vh4TfjNYDBsxP7wguQLpxCfJYyOcm1yZU3c,7988 +setuptools/_distutils/tests/test_version.py,sha256=ahfg_mP8wRy1sgwY-_Px5hrjgf6_upTIpnCgpR4yWRk,2750 +setuptools/_distutils/tests/test_versionpredicate.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_distutils/tests/unix_compat.py,sha256=z-op6C2iVdX1aq5BIBR7cqOxijKE97alNwJqHNdLpoI,386 +setuptools/_distutils/text_file.py,sha256=z4dkOJBr9Bo2LG0TNqm8sD63LEEaKSSP0J0bWBrFG3c,12101 +setuptools/_distutils/unixccompiler.py,sha256=1bXJWH4fiu_A2WfriHzf88xjllQTXnnjUkZdRKs9cWU,212 +setuptools/_distutils/util.py,sha256=Njfnqk60zMdkiAjRnGcTWX3t49-obHapOlbNvyIl02I,18094 +setuptools/_distutils/version.py,sha256=vImT5-ECXkQ21oKL0XYFiTqK6NyM09cpzBNoA_34CQU,12619 +setuptools/_distutils/versionpredicate.py,sha256=qBWQ6wTj12ODytoTmIydefIY2jb4uY1sdbgbuLn-IJM,5205 +setuptools/_distutils/zosccompiler.py,sha256=svdiXZ2kdcwKrJKfhUhib03y8gz7aGZKukXH3I7YkBc,58 +setuptools/_entry_points.py,sha256=Y3QUE9JKFW_YyquDnpffNWSs6f3jKEt1e-dnx--9-Kw,2310 +setuptools/_imp.py,sha256=YY1EjZEN-0zYci1cxO10B_adAEOr7i8eK8JoCc9Ierc,2435 +setuptools/_importlib.py,sha256=aKIjcK0HKXNz2D-XTrxaixGn_juTkONwmu3dcheMOF0,223 +setuptools/_itertools.py,sha256=jWRfsIrpC7myooz3hDURj9GtvpswZeKXg2HakmEhNjo,657 +setuptools/_normalization.py,sha256=kAmGfrwjF5djydEfLLyKgjkXCbL_0_ZxUPO-DlLlmIY,5824 +setuptools/_path.py,sha256=cPv41v03HD7uEYqCIo-E_cGRfpPVr4lywBCiK-HSrCg,2685 +setuptools/_reqs.py,sha256=QI3C9uOBSNRccu208qPnixHx51nxCry7_nPTIJaSYxM,1438 +setuptools/_shutil.py,sha256=cAOllcoyMTXs5JLoybQi29yI5gABk82hepJyOBv2bMw,1496 +setuptools/_static.py,sha256=GTR79gESF1_JaK4trLkpDrEuCeEtPlwQW0MRv7VNQX4,4855 +setuptools/_vendor/__pycache__/typing_extensions.cpython-311.pyc,, +setuptools/_vendor/autocommand-2.2.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/autocommand-2.2.2.dist-info/LICENSE,sha256=reeNBJgtaZctREqOFKlPh6IzTdOFXMgDSOqOJAqg3y0,7634 +setuptools/_vendor/autocommand-2.2.2.dist-info/METADATA,sha256=OADZuR3O6iBlpu1ieTgzYul6w4uOVrk0P0BO5TGGAJk,15006 +setuptools/_vendor/autocommand-2.2.2.dist-info/RECORD,sha256=giu6ZrQVJvpUcYa4AiH4XaUNZSvuVJPb_l0UCFES8MM,1308 +setuptools/_vendor/autocommand-2.2.2.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92 +setuptools/_vendor/autocommand-2.2.2.dist-info/top_level.txt,sha256=AzfhgKKS8EdAwWUTSF8mgeVQbXOY9kokHB6kSqwwqu0,12 +setuptools/_vendor/autocommand/__init__.py,sha256=zko5Rnvolvb-UXjCx_2ArPTGBWwUK5QY4LIQIKYR7As,1037 +setuptools/_vendor/autocommand/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/autocommand/__pycache__/autoasync.cpython-311.pyc,, +setuptools/_vendor/autocommand/__pycache__/autocommand.cpython-311.pyc,, +setuptools/_vendor/autocommand/__pycache__/automain.cpython-311.pyc,, +setuptools/_vendor/autocommand/__pycache__/autoparse.cpython-311.pyc,, +setuptools/_vendor/autocommand/__pycache__/errors.cpython-311.pyc,, +setuptools/_vendor/autocommand/autoasync.py,sha256=AMdyrxNS4pqWJfP_xuoOcImOHWD-qT7x06wmKN1Vp-U,5680 +setuptools/_vendor/autocommand/autocommand.py,sha256=hmkEmQ72HtL55gnURVjDOnsfYlGd5lLXbvT4KG496Qw,2505 +setuptools/_vendor/autocommand/automain.py,sha256=A2b8i754Mxc_DjU9WFr6vqYDWlhz0cn8miu8d8EsxV8,2076 +setuptools/_vendor/autocommand/autoparse.py,sha256=WVWmZJPcbzUKXP40raQw_0HD8qPJ2V9VG1eFFmmnFxw,11642 +setuptools/_vendor/autocommand/errors.py,sha256=7aa3roh9Herd6nIKpQHNWEslWE8oq7GiHYVUuRqORnA,886 +setuptools/_vendor/backports.tarfile-1.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/backports.tarfile-1.2.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +setuptools/_vendor/backports.tarfile-1.2.0.dist-info/METADATA,sha256=ghXFTq132dxaEIolxr3HK1mZqm9iyUmaRANZQSr6WlE,2020 +setuptools/_vendor/backports.tarfile-1.2.0.dist-info/RECORD,sha256=JYofHISeEXUGmlWl1s41ev3QTjTNXeJwk-Ss7HqdLOE,1360 +setuptools/_vendor/backports.tarfile-1.2.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/backports.tarfile-1.2.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92 +setuptools/_vendor/backports.tarfile-1.2.0.dist-info/top_level.txt,sha256=cGjaLMOoBR1FK0ApojtzWVmViTtJ7JGIK_HwXiEsvtU,10 +setuptools/_vendor/backports/__init__.py,sha256=iOEMwnlORWezdO8-2vxBIPSR37D7JGjluZ8f55vzxls,81 +setuptools/_vendor/backports/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/backports/tarfile/__init__.py,sha256=Pwf2qUIfB0SolJPCKcx3vz3UEu_aids4g4sAfxy94qg,108491 +setuptools/_vendor/backports/tarfile/__main__.py,sha256=Yw2oGT1afrz2eBskzdPYL8ReB_3liApmhFkN2EbDmc4,59 +setuptools/_vendor/backports/tarfile/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/backports/tarfile/__pycache__/__main__.cpython-311.pyc,, +setuptools/_vendor/backports/tarfile/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/backports/tarfile/compat/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/backports/tarfile/compat/__pycache__/py38.cpython-311.pyc,, +setuptools/_vendor/backports/tarfile/compat/py38.py,sha256=iYkyt_gvWjLzGUTJD9TuTfMMjOk-ersXZmRlvQYN2qE,568 +setuptools/_vendor/importlib_metadata-8.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/importlib_metadata-8.0.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358 +setuptools/_vendor/importlib_metadata-8.0.0.dist-info/METADATA,sha256=anuQ7_7h4J1bSEzfcjIBakPi2cyVQ7y7jklLHsBeH1k,4648 +setuptools/_vendor/importlib_metadata-8.0.0.dist-info/RECORD,sha256=DY08buueu-hsrH1ghhVSQzwynanqUSSLYdAr4uXmQDA,2518 +setuptools/_vendor/importlib_metadata-8.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/importlib_metadata-8.0.0.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91 +setuptools/_vendor/importlib_metadata-8.0.0.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19 +setuptools/_vendor/importlib_metadata/__init__.py,sha256=tZNB-23h8Bixi9uCrQqj9Yf0aeC--Josdy3IZRIQeB0,33798 +setuptools/_vendor/importlib_metadata/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/importlib_metadata/__pycache__/_adapters.cpython-311.pyc,, +setuptools/_vendor/importlib_metadata/__pycache__/_collections.cpython-311.pyc,, +setuptools/_vendor/importlib_metadata/__pycache__/_compat.cpython-311.pyc,, +setuptools/_vendor/importlib_metadata/__pycache__/_functools.cpython-311.pyc,, +setuptools/_vendor/importlib_metadata/__pycache__/_itertools.cpython-311.pyc,, +setuptools/_vendor/importlib_metadata/__pycache__/_meta.cpython-311.pyc,, +setuptools/_vendor/importlib_metadata/__pycache__/_text.cpython-311.pyc,, +setuptools/_vendor/importlib_metadata/__pycache__/diagnose.cpython-311.pyc,, +setuptools/_vendor/importlib_metadata/_adapters.py,sha256=rIhWTwBvYA1bV7i-5FfVX38qEXDTXFeS5cb5xJtP3ks,2317 +setuptools/_vendor/importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743 +setuptools/_vendor/importlib_metadata/_compat.py,sha256=73QKrN9KNoaZzhbX5yPCCZa-FaALwXe8TPlDR72JgBU,1314 +setuptools/_vendor/importlib_metadata/_functools.py,sha256=PsY2-4rrKX4RVeRC1oGp1lB1pmC9eKN88_f-bD9uOoA,2895 +setuptools/_vendor/importlib_metadata/_itertools.py,sha256=cvr_2v8BRbxcIl5x5ldfqdHjhI8Yi8s8yk50G_nm6jQ,2068 +setuptools/_vendor/importlib_metadata/_meta.py,sha256=nxZ7C8GVlcBFAKWyVOn_dn7ot_twBcbm1NmvjIetBHI,1801 +setuptools/_vendor/importlib_metadata/_text.py,sha256=HCsFksZpJLeTP3NEk_ngrAeXVRRtTrtyh9eOABoRP4A,2166 +setuptools/_vendor/importlib_metadata/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/importlib_metadata/compat/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/importlib_metadata/compat/__pycache__/py311.cpython-311.pyc,, +setuptools/_vendor/importlib_metadata/compat/__pycache__/py39.cpython-311.pyc,, +setuptools/_vendor/importlib_metadata/compat/py311.py,sha256=uqm-K-uohyj1042TH4a9Er_I5o7667DvulcD-gC_fSA,608 +setuptools/_vendor/importlib_metadata/compat/py39.py,sha256=cPkMv6-0ilK-0Jw_Tkn0xYbOKJZc4WJKQHow0c2T44w,1102 +setuptools/_vendor/importlib_metadata/diagnose.py,sha256=nkSRMiowlmkhLYhKhvCg9glmt_11Cox-EmLzEbqYTa8,379 +setuptools/_vendor/importlib_metadata/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/inflect-7.3.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/inflect-7.3.1.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +setuptools/_vendor/inflect-7.3.1.dist-info/METADATA,sha256=ZgMNY0WAZRs-U8wZiV2SMfjSKqBrMngXyDMs_CAwMwg,21079 +setuptools/_vendor/inflect-7.3.1.dist-info/RECORD,sha256=XXg0rBuiYSxoAQUP3lenuYsPNqz4jDwtTzdv2JEbMJE,943 +setuptools/_vendor/inflect-7.3.1.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91 +setuptools/_vendor/inflect-7.3.1.dist-info/top_level.txt,sha256=m52ujdp10CqT6jh1XQxZT6kEntcnv-7Tl7UiGNTzWZA,8 +setuptools/_vendor/inflect/__init__.py,sha256=Jxy1HJXZiZ85kHeLAhkmvz6EMTdFqBe-duvt34R6IOc,103796 +setuptools/_vendor/inflect/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/inflect/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/inflect/compat/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/inflect/compat/__pycache__/py38.cpython-311.pyc,, +setuptools/_vendor/inflect/compat/py38.py,sha256=oObVfVnWX9_OpnOuEJn1mFbJxVhwyR5epbiTNXDDaso,160 +setuptools/_vendor/inflect/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/jaraco.collections-5.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/jaraco.collections-5.1.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +setuptools/_vendor/jaraco.collections-5.1.0.dist-info/METADATA,sha256=IMUaliNsA5X1Ox9MXUWOagch5R4Wwb_3M7erp29dBtg,3933 +setuptools/_vendor/jaraco.collections-5.1.0.dist-info/RECORD,sha256=HptivXDkpfom6VlMu4CGD_7KPev-6Hc9rvp3TNJZygY,873 +setuptools/_vendor/jaraco.collections-5.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/jaraco.collections-5.1.0.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91 +setuptools/_vendor/jaraco.collections-5.1.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +setuptools/_vendor/jaraco.context-5.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/jaraco.context-5.3.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +setuptools/_vendor/jaraco.context-5.3.0.dist-info/METADATA,sha256=xDtguJej0tN9iEXCUvxEJh2a7xceIRVBEakBLSr__tY,4020 +setuptools/_vendor/jaraco.context-5.3.0.dist-info/RECORD,sha256=VRl7iKeEQyl7stgnp1uq50CzOJYlHYcoNdS0x17C9X4,641 +setuptools/_vendor/jaraco.context-5.3.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92 +setuptools/_vendor/jaraco.context-5.3.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +setuptools/_vendor/jaraco.functools-4.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/jaraco.functools-4.0.1.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +setuptools/_vendor/jaraco.functools-4.0.1.dist-info/METADATA,sha256=i4aUaQDX-jjdEQK5wevhegyx8JyLfin2HyvaSk3FHso,2891 +setuptools/_vendor/jaraco.functools-4.0.1.dist-info/RECORD,sha256=YyqnwE98S8wBwCevW5vHb-iVj0oYEDW5V6O9MBS6JIs,843 +setuptools/_vendor/jaraco.functools-4.0.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92 +setuptools/_vendor/jaraco.functools-4.0.1.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +setuptools/_vendor/jaraco.text-3.12.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/jaraco.text-3.12.1.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +setuptools/_vendor/jaraco.text-3.12.1.dist-info/METADATA,sha256=AzWdm6ViMfDOPoQMfLWn2zgBQSGJScyqeN29TcuWXVI,3658 +setuptools/_vendor/jaraco.text-3.12.1.dist-info/RECORD,sha256=gW2UV0HcokYJk4jKPu10_AZnrLqjb3C1WbJJTDl5sfY,1500 +setuptools/_vendor/jaraco.text-3.12.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/jaraco.text-3.12.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92 +setuptools/_vendor/jaraco.text-3.12.1.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +setuptools/_vendor/jaraco/__pycache__/context.cpython-311.pyc,, +setuptools/_vendor/jaraco/collections/__init__.py,sha256=Pc1-SqjWm81ad1P0-GttpkwO_LWlnaY6gUq8gcKh2v0,26640 +setuptools/_vendor/jaraco/collections/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/jaraco/collections/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/jaraco/context.py,sha256=REoLIxDkO5MfEYowt_WoupNCRoxBS5v7YX2PbW8lIcs,9552 +setuptools/_vendor/jaraco/functools/__init__.py,sha256=hEAJaS2uSZRuF_JY4CxCHIYh79ZpxaPp9OiHyr9EJ1w,16642 +setuptools/_vendor/jaraco/functools/__init__.pyi,sha256=gk3dsgHzo5F_U74HzAvpNivFAPCkPJ1b2-yCd62dfnw,3878 +setuptools/_vendor/jaraco/functools/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/jaraco/functools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/jaraco/text/Lorem ipsum.txt,sha256=N_7c_79zxOufBY9HZ3yzMgOkNv-TkOTTio4BydrSjgs,1335 +setuptools/_vendor/jaraco/text/__init__.py,sha256=Y2YUqXR_orUoDaY4SkPRe6ZZhb5HUHB_Ah9RCNsVyho,16250 +setuptools/_vendor/jaraco/text/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/jaraco/text/__pycache__/layouts.cpython-311.pyc,, +setuptools/_vendor/jaraco/text/__pycache__/show-newlines.cpython-311.pyc,, +setuptools/_vendor/jaraco/text/__pycache__/strip-prefix.cpython-311.pyc,, +setuptools/_vendor/jaraco/text/__pycache__/to-dvorak.cpython-311.pyc,, +setuptools/_vendor/jaraco/text/__pycache__/to-qwerty.cpython-311.pyc,, +setuptools/_vendor/jaraco/text/layouts.py,sha256=HTC8aSTLZ7uXipyOXapRMC158juecjK6RVwitfmZ9_w,643 +setuptools/_vendor/jaraco/text/show-newlines.py,sha256=WGQa65e8lyhb92LUOLqVn6KaCtoeVgVws6WtSRmLk6w,904 +setuptools/_vendor/jaraco/text/strip-prefix.py,sha256=NfVXV8JVNo6nqcuYASfMV7_y4Eo8zMQqlCOGvAnRIVw,412 +setuptools/_vendor/jaraco/text/to-dvorak.py,sha256=1SNcbSsvISpXXg-LnybIHHY-RUFOQr36zcHkY1pWFqw,119 +setuptools/_vendor/jaraco/text/to-qwerty.py,sha256=s4UMQUnPwFn_dB5uZC27BurHOQcYondBfzIpVL5pEzw,119 +setuptools/_vendor/more_itertools-10.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/more_itertools-10.3.0.dist-info/LICENSE,sha256=CfHIyelBrz5YTVlkHqm4fYPAyw_QB-te85Gn4mQ8GkY,1053 +setuptools/_vendor/more_itertools-10.3.0.dist-info/METADATA,sha256=BFO90O-fLNiVQMpj7oIS5ztzgJUUQZ3TA32P5HH3N-A,36293 +setuptools/_vendor/more_itertools-10.3.0.dist-info/RECORD,sha256=d8jnPgGNwP1-ntbICwWkQEVF9kH7CFIgzkKzaLWao9M,1259 +setuptools/_vendor/more_itertools-10.3.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/more_itertools-10.3.0.dist-info/WHEEL,sha256=rSgq_JpHF9fHR1lx53qwg_1-2LypZE_qmcuXbVUq948,81 +setuptools/_vendor/more_itertools/__init__.py,sha256=dtAbGjTDmn_ghiU5YXfhyDy0phAlXVdt5klZA5fUa-Q,149 +setuptools/_vendor/more_itertools/__init__.pyi,sha256=5B3eTzON1BBuOLob1vCflyEb2lSd6usXQQ-Cv-hXkeA,43 +setuptools/_vendor/more_itertools/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/more_itertools/__pycache__/more.cpython-311.pyc,, +setuptools/_vendor/more_itertools/__pycache__/recipes.cpython-311.pyc,, +setuptools/_vendor/more_itertools/more.py,sha256=1E5kzFncRKTDw0cYv1yRXMgDdunstLQd1QStcnL6U90,148370 +setuptools/_vendor/more_itertools/more.pyi,sha256=iXXeqt48Nxe8VGmIWpkVXuKpR2FYNuu2DU8nQLWCCu0,21484 +setuptools/_vendor/more_itertools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/more_itertools/recipes.py,sha256=WedhhfhGVgr6zii8fIbGJVmRTw0ZKRiLKnYBDGJv4nY,28591 +setuptools/_vendor/more_itertools/recipes.pyi,sha256=T_mdGpcFdfrP3JSWbwzYP9JyNV-Go-7RPfpxfftAWlA,4617 +setuptools/_vendor/packaging-24.2.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +setuptools/_vendor/packaging-24.2.dist-info/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197 +setuptools/_vendor/packaging-24.2.dist-info/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174 +setuptools/_vendor/packaging-24.2.dist-info/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344 +setuptools/_vendor/packaging-24.2.dist-info/METADATA,sha256=ohH86s6k5mIfQxY2TS0LcSfADeOFa4BiCC-bxZV-pNs,3204 +setuptools/_vendor/packaging-24.2.dist-info/RECORD,sha256=Y4DrXM0KY0ArfzhbAEa1LYFPwW3WEgEeL4iCqXe-A-M,2009 +setuptools/_vendor/packaging-24.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/packaging-24.2.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82 +setuptools/_vendor/packaging/__init__.py,sha256=dk4Ta_vmdVJxYHDcfyhvQNw8V3PgSBomKNXqg-D2JDY,494 +setuptools/_vendor/packaging/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/packaging/__pycache__/_elffile.cpython-311.pyc,, +setuptools/_vendor/packaging/__pycache__/_manylinux.cpython-311.pyc,, +setuptools/_vendor/packaging/__pycache__/_musllinux.cpython-311.pyc,, +setuptools/_vendor/packaging/__pycache__/_parser.cpython-311.pyc,, +setuptools/_vendor/packaging/__pycache__/_structures.cpython-311.pyc,, +setuptools/_vendor/packaging/__pycache__/_tokenizer.cpython-311.pyc,, +setuptools/_vendor/packaging/__pycache__/markers.cpython-311.pyc,, +setuptools/_vendor/packaging/__pycache__/metadata.cpython-311.pyc,, +setuptools/_vendor/packaging/__pycache__/requirements.cpython-311.pyc,, +setuptools/_vendor/packaging/__pycache__/specifiers.cpython-311.pyc,, +setuptools/_vendor/packaging/__pycache__/tags.cpython-311.pyc,, +setuptools/_vendor/packaging/__pycache__/utils.cpython-311.pyc,, +setuptools/_vendor/packaging/__pycache__/version.cpython-311.pyc,, +setuptools/_vendor/packaging/_elffile.py,sha256=cflAQAkE25tzhYmq_aCi72QfbT_tn891tPzfpbeHOwE,3306 +setuptools/_vendor/packaging/_manylinux.py,sha256=vl5OCoz4kx80H5rwXKeXWjl9WNISGmr4ZgTpTP9lU9c,9612 +setuptools/_vendor/packaging/_musllinux.py,sha256=p9ZqNYiOItGee8KcZFeHF_YcdhVwGHdK6r-8lgixvGQ,2694 +setuptools/_vendor/packaging/_parser.py,sha256=s_TvTvDNK0NrM2QB3VKThdWFM4Nc0P6JnkObkl3MjpM,10236 +setuptools/_vendor/packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431 +setuptools/_vendor/packaging/_tokenizer.py,sha256=J6v5H7Jzvb-g81xp_2QACKwO7LxHQA6ikryMU7zXwN8,5273 +setuptools/_vendor/packaging/licenses/__init__.py,sha256=1x5M1nEYjcgwEbLt0dXwz2ukjr18DiCzC0sraQqJ-Ww,5715 +setuptools/_vendor/packaging/licenses/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/packaging/licenses/__pycache__/_spdx.cpython-311.pyc,, +setuptools/_vendor/packaging/licenses/_spdx.py,sha256=oAm1ztPFwlsmCKe7lAAsv_OIOfS1cWDu9bNBkeu-2ns,48398 +setuptools/_vendor/packaging/markers.py,sha256=c89TNzB7ZdGYhkovm6PYmqGyHxXlYVaLW591PHUNKD8,10561 +setuptools/_vendor/packaging/metadata.py,sha256=YJibM7GYe4re8-0a3OlXmGS-XDgTEoO4tlBt2q25Bng,34762 +setuptools/_vendor/packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/packaging/requirements.py,sha256=gYyRSAdbrIyKDY66ugIDUQjRMvxkH2ALioTmX3tnL6o,2947 +setuptools/_vendor/packaging/specifiers.py,sha256=GG1wPNMcL0fMJO68vF53wKMdwnfehDcaI-r9NpTfilA,40074 +setuptools/_vendor/packaging/tags.py,sha256=CFqrJzAzc2XNGexerH__T-Y5Iwq7WbsYXsiLERLWxY0,21014 +setuptools/_vendor/packaging/utils.py,sha256=0F3Hh9OFuRgrhTgGZUl5K22Fv1YP2tZl1z_2gO6kJiA,5050 +setuptools/_vendor/packaging/version.py,sha256=olfyuk_DPbflNkJ4wBWetXQ17c74x3DB501degUv7DY,16676 +setuptools/_vendor/platformdirs-4.2.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/platformdirs-4.2.2.dist-info/METADATA,sha256=zmsie01G1MtXR0wgIv5XpVeTO7idr0WWvfmxKsKWuGk,11429 +setuptools/_vendor/platformdirs-4.2.2.dist-info/RECORD,sha256=TCEddtQu1A78Os_Mhm2JEqcYr7yit-UYSUQjZtbpn-g,1642 +setuptools/_vendor/platformdirs-4.2.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/platformdirs-4.2.2.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87 +setuptools/_vendor/platformdirs-4.2.2.dist-info/licenses/LICENSE,sha256=KeD9YukphQ6G6yjD_czwzv30-pSHkBHP-z0NS-1tTbY,1089 +setuptools/_vendor/platformdirs/__init__.py,sha256=EMGE8qeHRR9CzDFr8kL3tA8hdZZniYjXBVZd0UGTWK0,22225 +setuptools/_vendor/platformdirs/__main__.py,sha256=HnsUQHpiBaiTxwcmwVw-nFaPdVNZtQIdi1eWDtI-MzI,1493 +setuptools/_vendor/platformdirs/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/platformdirs/__pycache__/__main__.cpython-311.pyc,, +setuptools/_vendor/platformdirs/__pycache__/android.cpython-311.pyc,, +setuptools/_vendor/platformdirs/__pycache__/api.cpython-311.pyc,, +setuptools/_vendor/platformdirs/__pycache__/macos.cpython-311.pyc,, +setuptools/_vendor/platformdirs/__pycache__/unix.cpython-311.pyc,, +setuptools/_vendor/platformdirs/__pycache__/version.cpython-311.pyc,, +setuptools/_vendor/platformdirs/__pycache__/windows.cpython-311.pyc,, +setuptools/_vendor/platformdirs/android.py,sha256=xZXY9Jd46WOsxT2U6-5HsNtDZ-IQqxcEUrBLl3hYk4o,9016 +setuptools/_vendor/platformdirs/api.py,sha256=QBYdUac2eC521ek_y53uD1Dcq-lJX8IgSRVd4InC6uc,8996 +setuptools/_vendor/platformdirs/macos.py,sha256=wftsbsvq6nZ0WORXSiCrZNkRHz_WKuktl0a6mC7MFkI,5580 +setuptools/_vendor/platformdirs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/platformdirs/unix.py,sha256=Cci9Wqt35dAMsg6HT9nRGHSBW5obb0pR3AE1JJnsCXg,10643 +setuptools/_vendor/platformdirs/version.py,sha256=r7F76tZRjgQKzrpx_I0_ZMQOMU-PS7eGnHD7zEK3KB0,411 +setuptools/_vendor/platformdirs/windows.py,sha256=IFpiohUBwxPtCzlyKwNtxyW4Jk8haa6W8o59mfrDXVo,10125 +setuptools/_vendor/tomli-2.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/tomli-2.0.1.dist-info/LICENSE,sha256=uAgWsNUwuKzLTCIReDeQmEpuO2GSLCte6S8zcqsnQv4,1072 +setuptools/_vendor/tomli-2.0.1.dist-info/METADATA,sha256=zPDceKmPwJGLWtZykrHixL7WVXWmJGzZ1jyRT5lCoPI,8875 +setuptools/_vendor/tomli-2.0.1.dist-info/RECORD,sha256=DLn5pFGh42WsVLTIhmLh2gy1SnLRalJY-wq_-dPhwCI,999 +setuptools/_vendor/tomli-2.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/tomli-2.0.1.dist-info/WHEEL,sha256=jPMR_Dzkc4X4icQtmz81lnNY_kAsfog7ry7qoRvYLXw,81 +setuptools/_vendor/tomli/__init__.py,sha256=JhUwV66DB1g4Hvt1UQCVMdfCu-IgAV8FXmvDU9onxd4,396 +setuptools/_vendor/tomli/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/tomli/__pycache__/_parser.cpython-311.pyc,, +setuptools/_vendor/tomli/__pycache__/_re.cpython-311.pyc,, +setuptools/_vendor/tomli/__pycache__/_types.cpython-311.pyc,, +setuptools/_vendor/tomli/_parser.py,sha256=g9-ENaALS-B8dokYpCuzUFalWlog7T-SIYMjLZSWrtM,22633 +setuptools/_vendor/tomli/_re.py,sha256=dbjg5ChZT23Ka9z9DHOXfdtSpPwUfdgMXnj8NOoly-w,2943 +setuptools/_vendor/tomli/_types.py,sha256=-GTG2VUqkpxwMqzmVO4F7ybKddIbAnuAHXfmWQcTi3Q,254 +setuptools/_vendor/tomli/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26 +setuptools/_vendor/typeguard-4.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/typeguard-4.3.0.dist-info/LICENSE,sha256=YWP3mH37ONa8MgzitwsvArhivEESZRbVUu8c1DJH51g,1130 +setuptools/_vendor/typeguard-4.3.0.dist-info/METADATA,sha256=z2dcHAp0TwhYCFU5Deh8x31nazElgujUz9tbuP0pjSE,3717 +setuptools/_vendor/typeguard-4.3.0.dist-info/RECORD,sha256=SKUZWVgkeDUidUKM7s1473fXmsna55bjmi6vJUAoJVI,2402 +setuptools/_vendor/typeguard-4.3.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92 +setuptools/_vendor/typeguard-4.3.0.dist-info/entry_points.txt,sha256=qp7NQ1aLtiSgMQqo6gWlfGpy0IIXzoMJmeQTLpzqFZQ,48 +setuptools/_vendor/typeguard-4.3.0.dist-info/top_level.txt,sha256=4z28AhuDodwRS_c1J_l8H51t5QuwfTseskYzlxp6grs,10 +setuptools/_vendor/typeguard/__init__.py,sha256=Onh4w38elPCjtlcU3JY9k3h70NjsxXIkAflmQn-Z0FY,2071 +setuptools/_vendor/typeguard/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/typeguard/__pycache__/_checkers.cpython-311.pyc,, +setuptools/_vendor/typeguard/__pycache__/_config.cpython-311.pyc,, +setuptools/_vendor/typeguard/__pycache__/_decorators.cpython-311.pyc,, +setuptools/_vendor/typeguard/__pycache__/_exceptions.cpython-311.pyc,, +setuptools/_vendor/typeguard/__pycache__/_functions.cpython-311.pyc,, +setuptools/_vendor/typeguard/__pycache__/_importhook.cpython-311.pyc,, +setuptools/_vendor/typeguard/__pycache__/_memo.cpython-311.pyc,, +setuptools/_vendor/typeguard/__pycache__/_pytest_plugin.cpython-311.pyc,, +setuptools/_vendor/typeguard/__pycache__/_suppression.cpython-311.pyc,, +setuptools/_vendor/typeguard/__pycache__/_transformer.cpython-311.pyc,, +setuptools/_vendor/typeguard/__pycache__/_union_transformer.cpython-311.pyc,, +setuptools/_vendor/typeguard/__pycache__/_utils.cpython-311.pyc,, +setuptools/_vendor/typeguard/_checkers.py,sha256=JRrgKicdOEfIBoNEtegYCEIlhpad-a1u1Em7GCj0WCI,31360 +setuptools/_vendor/typeguard/_config.py,sha256=nIz8QwDa-oFO3L9O8_6srzlmd99pSby2wOM4Wb7F_B0,2846 +setuptools/_vendor/typeguard/_decorators.py,sha256=v6dsIeWvPhExGLP_wXF-RmDUyjZf_Ak28g7gBJ_v0-0,9033 +setuptools/_vendor/typeguard/_exceptions.py,sha256=ZIPeiV-FBd5Emw2EaWd2Fvlsrwi4ocwT2fVGBIAtHcQ,1121 +setuptools/_vendor/typeguard/_functions.py,sha256=ibgSAKa5ptIm1eR9ARG0BSozAFJPFNASZqhPVyQeqig,10393 +setuptools/_vendor/typeguard/_importhook.py,sha256=ugjCDvFcdWMU7UugqlJG91IpVNpEIxtRr-99s0h1k7M,6389 +setuptools/_vendor/typeguard/_memo.py,sha256=1juQV_vxnD2JYKbSrebiQuj4oKHz6n67v9pYA-CCISg,1303 +setuptools/_vendor/typeguard/_pytest_plugin.py,sha256=-fcSqkv54rIfIF8pDavY5YQPkj4OX8GMt_lL7CQSD4I,4416 +setuptools/_vendor/typeguard/_suppression.py,sha256=VQfzxcwIbu3if0f7VBkKM7hkYOA7tNFw9a7jMBsmMg4,2266 +setuptools/_vendor/typeguard/_transformer.py,sha256=9Ha7_QhdwoUni_6hvdY-hZbuEergowHrNL2vzHIakFY,44937 +setuptools/_vendor/typeguard/_union_transformer.py,sha256=v_42r7-6HuRX2SoFwnyJ-E5PlxXpVeUJPJR1-HU9qSo,1354 +setuptools/_vendor/typeguard/_utils.py,sha256=5HhO1rPn5f1M6ymkVAEv7Xmlz1cX-j0OnTMlyHqqrR8,5270 +setuptools/_vendor/typeguard/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/typing_extensions-4.12.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/typing_extensions-4.12.2.dist-info/LICENSE,sha256=Oy-B_iHRgcSZxZolbI4ZaEVdZonSaaqFNzv7avQdo78,13936 +setuptools/_vendor/typing_extensions-4.12.2.dist-info/METADATA,sha256=BeUQIa8cnYbrjWx-N8TOznM9UGW5Gm2DicVpDtRA8W0,3018 +setuptools/_vendor/typing_extensions-4.12.2.dist-info/RECORD,sha256=dxAALYGXHmMqpqL8M9xddKr118quIgQKZdPjFQOwXuk,571 +setuptools/_vendor/typing_extensions-4.12.2.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81 +setuptools/_vendor/typing_extensions.py,sha256=gwekpyG9DVG3lxWKX4ni8u7nk3We5slG98mA9F3DJQw,134451 +setuptools/_vendor/wheel-0.45.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/wheel-0.45.1.dist-info/LICENSE.txt,sha256=MMI2GGeRCPPo6h0qZYx8pBe9_IkcmO8aifpP8MmChlQ,1107 +setuptools/_vendor/wheel-0.45.1.dist-info/METADATA,sha256=mKz84H7m7jsxJyzeIcTVORiTb0NPMV39KvOIYhGgmjA,2313 +setuptools/_vendor/wheel-0.45.1.dist-info/RECORD,sha256=1jnxrHyZPDcVvULyfGFhiba4Z5L9_RsXr9dxcNbhaYQ,4900 +setuptools/_vendor/wheel-0.45.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/wheel-0.45.1.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82 +setuptools/_vendor/wheel-0.45.1.dist-info/entry_points.txt,sha256=rTY1BbkPHhkGMm4Q3F0pIzJBzW2kMxoG1oriffvGdA0,104 +setuptools/_vendor/wheel/__init__.py,sha256=mrxMnvdXACur_LWegbUfh5g5ysWZrd63UJn890wvGNk,59 +setuptools/_vendor/wheel/__main__.py,sha256=NkMUnuTCGcOkgY0IBLgBCVC_BGGcWORx2K8jYGS12UE,455 +setuptools/_vendor/wheel/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/wheel/__pycache__/__main__.cpython-311.pyc,, +setuptools/_vendor/wheel/__pycache__/_bdist_wheel.cpython-311.pyc,, +setuptools/_vendor/wheel/__pycache__/_setuptools_logging.cpython-311.pyc,, +setuptools/_vendor/wheel/__pycache__/bdist_wheel.cpython-311.pyc,, +setuptools/_vendor/wheel/__pycache__/macosx_libfile.cpython-311.pyc,, +setuptools/_vendor/wheel/__pycache__/metadata.cpython-311.pyc,, +setuptools/_vendor/wheel/__pycache__/util.cpython-311.pyc,, +setuptools/_vendor/wheel/__pycache__/wheelfile.cpython-311.pyc,, +setuptools/_vendor/wheel/_bdist_wheel.py,sha256=UghCQjSH_pVfcZh6oRjzSw_TQhcf3anSx1OkiLSL82M,21694 +setuptools/_vendor/wheel/_setuptools_logging.py,sha256=-5KC-lne0ilOUWIDfOkqapUWGMFZhuKYDIavIZiB5kM,781 +setuptools/_vendor/wheel/bdist_wheel.py,sha256=tpf9WufiSO1RuEMg5oPhIfSG8DMziCZ_4muCKF69Cqo,1107 +setuptools/_vendor/wheel/cli/__init__.py,sha256=Npq6_jKi03dhIcRnmbuFhwviVJxwO0tYEnEhWMv9cJo,4402 +setuptools/_vendor/wheel/cli/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/wheel/cli/__pycache__/convert.cpython-311.pyc,, +setuptools/_vendor/wheel/cli/__pycache__/pack.cpython-311.pyc,, +setuptools/_vendor/wheel/cli/__pycache__/tags.cpython-311.pyc,, +setuptools/_vendor/wheel/cli/__pycache__/unpack.cpython-311.pyc,, +setuptools/_vendor/wheel/cli/convert.py,sha256=Bi0ntEXb9nTllCxWeTRQ4j-nPs3szWSEKipG_GgnMkQ,12634 +setuptools/_vendor/wheel/cli/pack.py,sha256=CAFcHdBVulvsHYJlndKVO7KMI9JqBTZz5ii0PKxxCOs,3103 +setuptools/_vendor/wheel/cli/tags.py,sha256=lHw-LaWrkS5Jy_qWcw-6pSjeNM6yAjDnqKI3E5JTTCU,4760 +setuptools/_vendor/wheel/cli/unpack.py,sha256=Y_J7ynxPSoFFTT7H0fMgbBlVErwyDGcObgme5MBuz58,1021 +setuptools/_vendor/wheel/macosx_libfile.py,sha256=k1x7CE3LPtOVGqj6NXQ1nTGYVPaeRrhVzUG_KPq3zDs,16572 +setuptools/_vendor/wheel/metadata.py,sha256=JC4p7jlQZu2bUTAQ2fevkqLjg_X6gnNyRhLn6OUO1tc,6171 +setuptools/_vendor/wheel/util.py,sha256=aL7aibHwYUgfc8WlolL5tXdkV4DatbJxZHb1kwHFJAU,423 +setuptools/_vendor/wheel/vendored/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/wheel/vendored/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/wheel/vendored/packaging/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197 +setuptools/_vendor/wheel/vendored/packaging/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174 +setuptools/_vendor/wheel/vendored/packaging/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344 +setuptools/_vendor/wheel/vendored/packaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/wheel/vendored/packaging/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/wheel/vendored/packaging/__pycache__/_elffile.cpython-311.pyc,, +setuptools/_vendor/wheel/vendored/packaging/__pycache__/_manylinux.cpython-311.pyc,, +setuptools/_vendor/wheel/vendored/packaging/__pycache__/_musllinux.cpython-311.pyc,, +setuptools/_vendor/wheel/vendored/packaging/__pycache__/_parser.cpython-311.pyc,, +setuptools/_vendor/wheel/vendored/packaging/__pycache__/_structures.cpython-311.pyc,, +setuptools/_vendor/wheel/vendored/packaging/__pycache__/_tokenizer.cpython-311.pyc,, +setuptools/_vendor/wheel/vendored/packaging/__pycache__/markers.cpython-311.pyc,, +setuptools/_vendor/wheel/vendored/packaging/__pycache__/requirements.cpython-311.pyc,, +setuptools/_vendor/wheel/vendored/packaging/__pycache__/specifiers.cpython-311.pyc,, +setuptools/_vendor/wheel/vendored/packaging/__pycache__/tags.cpython-311.pyc,, +setuptools/_vendor/wheel/vendored/packaging/__pycache__/utils.cpython-311.pyc,, +setuptools/_vendor/wheel/vendored/packaging/__pycache__/version.cpython-311.pyc,, +setuptools/_vendor/wheel/vendored/packaging/_elffile.py,sha256=hbmK8OD6Z7fY6hwinHEUcD1by7czkGiNYu7ShnFEk2k,3266 +setuptools/_vendor/wheel/vendored/packaging/_manylinux.py,sha256=P7sdR5_7XBY09LVYYPhHmydMJIIwPXWsh4olk74Uuj4,9588 +setuptools/_vendor/wheel/vendored/packaging/_musllinux.py,sha256=z1s8To2hQ0vpn_d-O2i5qxGwEK8WmGlLt3d_26V7NeY,2674 +setuptools/_vendor/wheel/vendored/packaging/_parser.py,sha256=4tT4emSl2qTaU7VTQE1Xa9o1jMPCsBezsYBxyNMUN-s,10347 +setuptools/_vendor/wheel/vendored/packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431 +setuptools/_vendor/wheel/vendored/packaging/_tokenizer.py,sha256=alCtbwXhOFAmFGZ6BQ-wCTSFoRAJ2z-ysIf7__MTJ_k,5292 +setuptools/_vendor/wheel/vendored/packaging/markers.py,sha256=_TSPI1BhJYO7Bp9AzTmHQxIqHEVXaTjmDh9G-w8qzPA,8232 +setuptools/_vendor/wheel/vendored/packaging/requirements.py,sha256=dgoBeVprPu2YE6Q8nGfwOPTjATHbRa_ZGLyXhFEln6Q,2933 +setuptools/_vendor/wheel/vendored/packaging/specifiers.py,sha256=IWSt0SrLSP72heWhAC8UL0eGvas7XIQHjqiViVfmPKE,39778 +setuptools/_vendor/wheel/vendored/packaging/tags.py,sha256=fedHXiOHkBxNZTXotXv8uXPmMFU9ae-TKBujgYHigcA,18950 +setuptools/_vendor/wheel/vendored/packaging/utils.py,sha256=XgdmP3yx9-wQEFjO7OvMj9RjEf5JlR5HFFR69v7SQ9E,5268 +setuptools/_vendor/wheel/vendored/packaging/version.py,sha256=PFJaYZDxBgyxkfYhH3SQw4qfE9ICCWrTmitvq14y3bs,16234 +setuptools/_vendor/wheel/vendored/vendor.txt,sha256=Z2ENjB1i5prfez8CdM1Sdr3c6Zxv2rRRolMpLmBncAE,16 +setuptools/_vendor/wheel/wheelfile.py,sha256=USCttNlJwafxt51YYFFKG7jnxz8dfhbyqAZL6jMTA9s,8411 +setuptools/_vendor/zipp-3.19.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +setuptools/_vendor/zipp-3.19.2.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +setuptools/_vendor/zipp-3.19.2.dist-info/METADATA,sha256=UIrk_kMIHGSwsKKChYizqMw0MMZpPRZ2ZiVpQAsN_bE,3575 +setuptools/_vendor/zipp-3.19.2.dist-info/RECORD,sha256=8xby4D_ZrefrvAsVRwaEjiu4_VaLkJNRCfDY484rm_4,1039 +setuptools/_vendor/zipp-3.19.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/zipp-3.19.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92 +setuptools/_vendor/zipp-3.19.2.dist-info/top_level.txt,sha256=iAbdoSHfaGqBfVb2XuR9JqSQHCoOsOtG6y9C_LSpqFw,5 +setuptools/_vendor/zipp/__init__.py,sha256=QuI1g00G4fRAcGt-HqbV0oWIkmSgedCGGYsHHYzNa8A,13412 +setuptools/_vendor/zipp/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/zipp/__pycache__/glob.cpython-311.pyc,, +setuptools/_vendor/zipp/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/_vendor/zipp/compat/__pycache__/__init__.cpython-311.pyc,, +setuptools/_vendor/zipp/compat/__pycache__/py310.cpython-311.pyc,, +setuptools/_vendor/zipp/compat/py310.py,sha256=eZpkW0zRtunkhEh8jjX3gCGe22emoKCBJw72Zt4RkhA,219 +setuptools/_vendor/zipp/glob.py,sha256=etWpnfEoRyfUvrUsi6sTiGmErvPwe6HzY6pT8jg_lUI,3082 +setuptools/archive_util.py,sha256=Tl_64hSTtc4y8x7xa98rFVUbG24oArpjzLAYGYP2_sI,7356 +setuptools/build_meta.py,sha256=rry-LowqvWFsxG6Qm5QWetHJGeETzRdiQ5-bs4bOkjo,20446 +setuptools/cli-32.exe,sha256=MqzBvFQxFsviz_EMuGd3LfLyVP8mNMhwrvC0bEtpb9s,11776 +setuptools/cli-64.exe,sha256=u7PeVwdinmpgoMI4zUd7KPB_AGaYL9qVP6b87DkHOko,14336 +setuptools/cli-arm64.exe,sha256=uafQjaiA36yLz1SOuksG-1m28JsX0zFIoPZhgyiSbGE,13824 +setuptools/cli.exe,sha256=MqzBvFQxFsviz_EMuGd3LfLyVP8mNMhwrvC0bEtpb9s,11776 +setuptools/command/__init__.py,sha256=wdSrlNR0P6nCz9_oFtCAiAkeFJMsZa1jPcpXT53f0SM,803 +setuptools/command/__pycache__/__init__.cpython-311.pyc,, +setuptools/command/__pycache__/_requirestxt.cpython-311.pyc,, +setuptools/command/__pycache__/alias.cpython-311.pyc,, +setuptools/command/__pycache__/bdist_egg.cpython-311.pyc,, +setuptools/command/__pycache__/bdist_rpm.cpython-311.pyc,, +setuptools/command/__pycache__/bdist_wheel.cpython-311.pyc,, +setuptools/command/__pycache__/build.cpython-311.pyc,, +setuptools/command/__pycache__/build_clib.cpython-311.pyc,, +setuptools/command/__pycache__/build_ext.cpython-311.pyc,, +setuptools/command/__pycache__/build_py.cpython-311.pyc,, +setuptools/command/__pycache__/develop.cpython-311.pyc,, +setuptools/command/__pycache__/dist_info.cpython-311.pyc,, +setuptools/command/__pycache__/easy_install.cpython-311.pyc,, +setuptools/command/__pycache__/editable_wheel.cpython-311.pyc,, +setuptools/command/__pycache__/egg_info.cpython-311.pyc,, +setuptools/command/__pycache__/install.cpython-311.pyc,, +setuptools/command/__pycache__/install_egg_info.cpython-311.pyc,, +setuptools/command/__pycache__/install_lib.cpython-311.pyc,, +setuptools/command/__pycache__/install_scripts.cpython-311.pyc,, +setuptools/command/__pycache__/rotate.cpython-311.pyc,, +setuptools/command/__pycache__/saveopts.cpython-311.pyc,, +setuptools/command/__pycache__/sdist.cpython-311.pyc,, +setuptools/command/__pycache__/setopt.cpython-311.pyc,, +setuptools/command/__pycache__/test.cpython-311.pyc,, +setuptools/command/_requirestxt.py,sha256=ItYMTJGh_i5TlQstX_nFopqEhkC4PJFadBL2Zd3V670,4228 +setuptools/command/alias.py,sha256=rDdrMt32DS6qf3K7tjZZyHD_dMKrm77AXcAtx-nBQ0I,2380 +setuptools/command/bdist_egg.py,sha256=3eDucQ4fdeYMsLO9PhBfY1JkcMLhZXgnAI_9FdFNsEE,16972 +setuptools/command/bdist_rpm.py,sha256=LyqI49w48SKk0FmuHsE9MLzX1SuXjL7YMNbZMFZqFII,1435 +setuptools/command/bdist_wheel.py,sha256=_LfGHB7CV_uyncqlOTSETEi2gjVCoPKuAXcyRFoq7Cs,22246 +setuptools/command/build.py,sha256=eI7STMERGGZEpzk1tvJN8p9IOjAAXMcGLzljv2mwI3M,6052 +setuptools/command/build_clib.py,sha256=AbgpPIF_3qL8fZr3JIebI-WHTMTBiMfrFkVQz8K40G4,4528 +setuptools/command/build_ext.py,sha256=bUH4M0NizaJJrv10wK-ZD3uY0TxCSZlYQDhiwwzHslM,18377 +setuptools/command/build_py.py,sha256=DCbjvB18kkL-xUK5rvlzm0C6twTeOxNhyvJDxxa7fII,15539 +setuptools/command/develop.py,sha256=zX22119sI1G1gfJ1gNCE4hkg2zbLKx0uUwvNmC5bIu8,6886 +setuptools/command/dist_info.py,sha256=HU752iLLmmYMHbsDBgz2ubRjkgJobugOp8H71LzzDys,3450 +setuptools/command/easy_install.py,sha256=0Z4kFlE78Ae2AfHXYTxZFUa2t3qlNqXCtQu4J1Nx8iA,87870 +setuptools/command/editable_wheel.py,sha256=3bBipRZA3E4poQ8LEWhMYEjHjCzqU_pIdO86C3t7oNY,35624 +setuptools/command/egg_info.py,sha256=WWUozR3DZCrWsTQhHXOygMiEUcrjLWphET0-Zsocsm4,25982 +setuptools/command/install.py,sha256=MmTGb8m1R8fJ0cc5FTWCF6uq-s1ZJmvpYm-N_CtqEaI,7046 +setuptools/command/install_egg_info.py,sha256=3I9IPCH7D59Sh-6aVYz-h6wwyxq-wkxrKwKg3nDdJqs,2075 +setuptools/command/install_lib.py,sha256=9n1_U83eHcERL_a_rv_LhHCkhXlLdqyZ4SdBow-9qcE,4319 +setuptools/command/install_scripts.py,sha256=tVOCj3e8OTIrkoL_bGbT5pOksdxZfQblH_bdI4DtVV4,2637 +setuptools/command/launcher manifest.xml,sha256=xlLbjWrB01tKC0-hlVkOKkiSPbzMml2eOPtJ_ucCnbE,628 +setuptools/command/rotate.py,sha256=XNd_BEEOWAJHW1FcLTMUWWl4QB6zAuk7b8VWQg3FHos,2187 +setuptools/command/saveopts.py,sha256=Np0PVb7SD7oTbu9Z9sosS7D-CkkIkU7x4glu5Es1tjA,692 +setuptools/command/sdist.py,sha256=JaQm2-ebXI2kvyrBjJKP8yNLPa5eMbMeis88CXBMYlk,7374 +setuptools/command/setopt.py,sha256=xZF2RCc4ABvE9eHHAzF50-fkQg3au8fcRUVVGd58k3U,5100 +setuptools/command/test.py,sha256=k7xcq7D7bEehgxarbw-dW3AtmGZORqz8HjKR6FGJ3jk,1343 +setuptools/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/compat/__pycache__/__init__.cpython-311.pyc,, +setuptools/compat/__pycache__/py310.cpython-311.pyc,, +setuptools/compat/__pycache__/py311.cpython-311.pyc,, +setuptools/compat/__pycache__/py312.cpython-311.pyc,, +setuptools/compat/__pycache__/py39.cpython-311.pyc,, +setuptools/compat/py310.py,sha256=8sqwWczIcrkzeAbhaim4pKVd4tXZdcqmebgdvzji0rc,141 +setuptools/compat/py311.py,sha256=e6tJAFwZEP82hmMBl10HYeSypelo_Ti2wTjKZVKLwOE,790 +setuptools/compat/py312.py,sha256=vYKVtdrdOTsO_R90dJkEXsFwfMJFuIFJflhIgHrjJ-Y,366 +setuptools/compat/py39.py,sha256=BJMtnkfcqyTfccqjYQxfoRtU2nTnWaEESBVkshTiXqY,493 +setuptools/config/NOTICE,sha256=Ld3wiBgpejuJ1D2V_2WdjahXQRCMkTbfo6TYVsBiO9g,493 +setuptools/config/__init__.py,sha256=aiPnL9BJn1O6MfmuNXyn8W2Lp8u9qizRVqwPiOdPIjY,1499 +setuptools/config/__pycache__/__init__.cpython-311.pyc,, +setuptools/config/__pycache__/_apply_pyprojecttoml.cpython-311.pyc,, +setuptools/config/__pycache__/expand.cpython-311.pyc,, +setuptools/config/__pycache__/pyprojecttoml.cpython-311.pyc,, +setuptools/config/__pycache__/setupcfg.cpython-311.pyc,, +setuptools/config/_apply_pyprojecttoml.py,sha256=SUyTw7A2btZ1lBuWKN5o42-Diyv95eGTiYJ3rZOnGSc,19120 +setuptools/config/_validate_pyproject/NOTICE,sha256=XTANv6ZDE4sBO3WsnK7uWR-VG4sO4kKIw0zNkmxHgMg,18737 +setuptools/config/_validate_pyproject/__init__.py,sha256=dnp6T7ePP1R5z4OuC7Fd2dkFlIrtIfizUfvpGJP6nz0,1042 +setuptools/config/_validate_pyproject/__pycache__/__init__.cpython-311.pyc,, +setuptools/config/_validate_pyproject/__pycache__/error_reporting.cpython-311.pyc,, +setuptools/config/_validate_pyproject/__pycache__/extra_validations.cpython-311.pyc,, +setuptools/config/_validate_pyproject/__pycache__/fastjsonschema_exceptions.cpython-311.pyc,, +setuptools/config/_validate_pyproject/__pycache__/fastjsonschema_validations.cpython-311.pyc,, +setuptools/config/_validate_pyproject/__pycache__/formats.cpython-311.pyc,, +setuptools/config/_validate_pyproject/error_reporting.py,sha256=meldD7nBQdolQhvG-43r1Ue-gU1n7ORAJR86vh3Rrvk,11813 +setuptools/config/_validate_pyproject/extra_validations.py,sha256=-GUG5S--ijY8WfXbdXPoHl6ywGsyEF9dtDpenSoJPHg,2858 +setuptools/config/_validate_pyproject/fastjsonschema_exceptions.py,sha256=w749JgqKi8clBFcObdcbZVqsmF4oJ_QByhZ1SGbUFNw,1612 +setuptools/config/_validate_pyproject/fastjsonschema_validations.py,sha256=FihD5ZcM6p77BPZ04CGqh3BEwVNoPMKJZJAyuJpkAU0,354682 +setuptools/config/_validate_pyproject/formats.py,sha256=TETokJBK9hjl-cVg1olsojkJwLxfP7_chgJQNmzAB98,13564 +setuptools/config/distutils.schema.json,sha256=Tcp32kRnhwORGw_9p6GEi08lj2h15tQRzOYBbzGmcBU,972 +setuptools/config/expand.py,sha256=JNAktRCsyyRB-rQodbPnCucmLWqcYvzCDC8Ebn2Z7xM,16041 +setuptools/config/pyprojecttoml.py,sha256=YMu5PdbJJI5azp6kR_boM1mflf5nqOA-InF4s6LnLgw,18320 +setuptools/config/setupcfg.py,sha256=VZDkwE7DYv45SbadJD8CwKrDtiXvjgllL8PYSvoRCyg,26575 +setuptools/config/setuptools.schema.json,sha256=dZBRuSEnZkatoVlt1kVwG8ocTeRdO7BD0xvOWKH54PY,16071 +setuptools/depends.py,sha256=jKYfjmt_2ZQYVghb8L9bU7LJ6erHJ5ze-K_fKV1BMXk,5965 +setuptools/discovery.py,sha256=-42c3XhwzkfodDKKP50C2YBzr11fncAgmUzBdBRb0-Q,21258 +setuptools/dist.py,sha256=RZz7aj9RxqSYriqgFoZOu-7KIV82cmlaMFx9owZrApQ,44897 +setuptools/errors.py,sha256=gY2x2PIaIgy01yRANRC-zcCwxDCqCScgJoCOZFe0yio,3024 +setuptools/extension.py,sha256=KCnv9p3tgm0ZVqtgE451fyILsm4hCyvOiUtOu787D-4,6683 +setuptools/glob.py,sha256=AC_B33DY8g-CHELxDsJrtwFrpiucSAZsakPFdSOQzhc,6062 +setuptools/gui-32.exe,sha256=hdrh6V13hF8stZvKw9Sv50u-TJGpvMW_SnHNQxBNvnw,11776 +setuptools/gui-64.exe,sha256=NHG2FA6txkEid9u-_j_vjDRaDxpZd2CGuAo2GMOoPjs,14336 +setuptools/gui-arm64.exe,sha256=5pT0dDQFyLWSb_RX22_n8aEt7HwWqcOGR4TT9OB64Jc,13824 +setuptools/gui.exe,sha256=hdrh6V13hF8stZvKw9Sv50u-TJGpvMW_SnHNQxBNvnw,11776 +setuptools/installer.py,sha256=_4Wegx4r3L05sMo3-IlqFp-OuxnWyBqjyMZ7LWQXmh8,5110 +setuptools/launch.py,sha256=IBb5lEv69CyuZ9ewIrmKlXh154kdLmP29LKfTMkximE,820 +setuptools/logging.py,sha256=W16iHJ1HcCXYQ0RxyrEfJ83FT4175tCtoYg-E6uSpVI,1261 +setuptools/modified.py,sha256=ZwbfBfCFP88ltvbv_dJDz-t1LsQjnM-JUpgZnnQZjjM,568 +setuptools/monkey.py,sha256=FwMWl2n1v2bHbeqBy-o9g8yUNaAkYFbszCbXe9d5Za8,3717 +setuptools/msvc.py,sha256=vmM0qL4rIzrtD9pia9ZEwtqZ4LbbrgL0dU0EANVYRm8,41631 +setuptools/namespaces.py,sha256=2GGqYY1BNDEhMtBc1rHTv7klgmNVRdksJeW-L1f--ys,3171 +setuptools/package_index.py,sha256=yHXj4Z_QaUeh8W6rwq08I5LxnyHrSkNf01uXHwdE8K0,39095 +setuptools/sandbox.py,sha256=fMqtcOuipHO6RKPh1YB5o7d985dLKo76Whp3vrIei2E,14906 +setuptools/script (dev).tmpl,sha256=RUzQzCQUaXtwdLtYHWYbIQmOaES5Brqq1FvUA_tu-5I,218 +setuptools/script.tmpl,sha256=WGTt5piezO27c-Dbx6l5Q4T3Ff20A5z7872hv3aAhYY,138 +setuptools/tests/__init__.py,sha256=AnBfls2iJbTDQzmMKeLRt-9lxhaOHUVOZEgXv89Uwvs,335 +setuptools/tests/__pycache__/__init__.cpython-311.pyc,, +setuptools/tests/__pycache__/contexts.cpython-311.pyc,, +setuptools/tests/__pycache__/environment.cpython-311.pyc,, +setuptools/tests/__pycache__/fixtures.cpython-311.pyc,, +setuptools/tests/__pycache__/mod_with_constant.cpython-311.pyc,, +setuptools/tests/__pycache__/namespaces.cpython-311.pyc,, +setuptools/tests/__pycache__/script-with-bom.cpython-311.pyc,, +setuptools/tests/__pycache__/server.cpython-311.pyc,, +setuptools/tests/__pycache__/test_archive_util.cpython-311.pyc,, +setuptools/tests/__pycache__/test_bdist_deprecations.cpython-311.pyc,, +setuptools/tests/__pycache__/test_bdist_egg.cpython-311.pyc,, +setuptools/tests/__pycache__/test_bdist_wheel.cpython-311.pyc,, +setuptools/tests/__pycache__/test_build.cpython-311.pyc,, +setuptools/tests/__pycache__/test_build_clib.cpython-311.pyc,, +setuptools/tests/__pycache__/test_build_ext.cpython-311.pyc,, +setuptools/tests/__pycache__/test_build_meta.cpython-311.pyc,, +setuptools/tests/__pycache__/test_build_py.cpython-311.pyc,, +setuptools/tests/__pycache__/test_config_discovery.cpython-311.pyc,, +setuptools/tests/__pycache__/test_core_metadata.cpython-311.pyc,, +setuptools/tests/__pycache__/test_depends.cpython-311.pyc,, +setuptools/tests/__pycache__/test_develop.cpython-311.pyc,, +setuptools/tests/__pycache__/test_dist.cpython-311.pyc,, +setuptools/tests/__pycache__/test_dist_info.cpython-311.pyc,, +setuptools/tests/__pycache__/test_distutils_adoption.cpython-311.pyc,, +setuptools/tests/__pycache__/test_easy_install.cpython-311.pyc,, +setuptools/tests/__pycache__/test_editable_install.cpython-311.pyc,, +setuptools/tests/__pycache__/test_egg_info.cpython-311.pyc,, +setuptools/tests/__pycache__/test_extern.cpython-311.pyc,, +setuptools/tests/__pycache__/test_find_packages.cpython-311.pyc,, +setuptools/tests/__pycache__/test_find_py_modules.cpython-311.pyc,, +setuptools/tests/__pycache__/test_glob.cpython-311.pyc,, +setuptools/tests/__pycache__/test_install_scripts.cpython-311.pyc,, +setuptools/tests/__pycache__/test_logging.cpython-311.pyc,, +setuptools/tests/__pycache__/test_manifest.cpython-311.pyc,, +setuptools/tests/__pycache__/test_namespaces.cpython-311.pyc,, +setuptools/tests/__pycache__/test_packageindex.cpython-311.pyc,, +setuptools/tests/__pycache__/test_sandbox.cpython-311.pyc,, +setuptools/tests/__pycache__/test_sdist.cpython-311.pyc,, +setuptools/tests/__pycache__/test_setopt.cpython-311.pyc,, +setuptools/tests/__pycache__/test_setuptools.cpython-311.pyc,, +setuptools/tests/__pycache__/test_shutil_wrapper.cpython-311.pyc,, +setuptools/tests/__pycache__/test_unicode_utils.cpython-311.pyc,, +setuptools/tests/__pycache__/test_virtualenv.cpython-311.pyc,, +setuptools/tests/__pycache__/test_warnings.cpython-311.pyc,, +setuptools/tests/__pycache__/test_wheel.cpython-311.pyc,, +setuptools/tests/__pycache__/test_windows_wrappers.cpython-311.pyc,, +setuptools/tests/__pycache__/text.cpython-311.pyc,, +setuptools/tests/__pycache__/textwrap.cpython-311.pyc,, +setuptools/tests/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/tests/compat/__pycache__/__init__.cpython-311.pyc,, +setuptools/tests/compat/__pycache__/py39.cpython-311.pyc,, +setuptools/tests/compat/py39.py,sha256=eUy7_F-6KRTOIKl-veshUu6I0EdTSdBZMh0EV0lZ1-g,135 +setuptools/tests/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/tests/config/__pycache__/__init__.cpython-311.pyc,, +setuptools/tests/config/__pycache__/test_apply_pyprojecttoml.cpython-311.pyc,, +setuptools/tests/config/__pycache__/test_expand.cpython-311.pyc,, +setuptools/tests/config/__pycache__/test_pyprojecttoml.cpython-311.pyc,, +setuptools/tests/config/__pycache__/test_pyprojecttoml_dynamic_deps.cpython-311.pyc,, +setuptools/tests/config/__pycache__/test_setupcfg.cpython-311.pyc,, +setuptools/tests/config/downloads/__init__.py,sha256=9ixnDEdyL_arKbUzfuiJftAj9bGxKz8M9alOFZMjx9Y,1827 +setuptools/tests/config/downloads/__pycache__/__init__.cpython-311.pyc,, +setuptools/tests/config/downloads/__pycache__/preload.cpython-311.pyc,, +setuptools/tests/config/downloads/preload.py,sha256=sIGGZpY3cmMpMwiJYYYYHG2ifZJkvJgEotRFtiulV1I,450 +setuptools/tests/config/setupcfg_examples.txt,sha256=cAbVvCbkFZuTUL6xRRzRgqyB0rLvJTfvw3D30glo2OE,1912 +setuptools/tests/config/test_apply_pyprojecttoml.py,sha256=l6nE4d8WLU_eSWRic7VSoqeKv9Bi7CZGHcEuB2ehk2w,28807 +setuptools/tests/config/test_expand.py,sha256=S0oT6JvgA_oujR4YS4RUuf5gmOt1CTQV66RQDzV8xd4,8933 +setuptools/tests/config/test_pyprojecttoml.py,sha256=0LefSljUhA6MqtJ5AVzLhomqZcYiFKdu_1ckDeMT1LY,12406 +setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py,sha256=9W73-yLhZJmvCiO4rTiQoBpZT5wNA90Xbd5n2HCshd4,3271 +setuptools/tests/config/test_setupcfg.py,sha256=ZvN-O-2Dgon1adp6oM6il8JWdgT9y196fRvqESU5ELI,33427 +setuptools/tests/contexts.py,sha256=TAdZKxmmodx1ExMVo01o4QpRjpIpo4X3IWKq_BnjxpU,3480 +setuptools/tests/environment.py,sha256=95_UtTaRiuvwYC9eXKEHbn02kDtZysvZq3UZJmPUj1I,3102 +setuptools/tests/fixtures.py,sha256=-V7iD6BeE2E0Rw6dVvTOCm36JG8ZTTnrXhN0GISlgrg,5197 +setuptools/tests/indexes/test_links_priority/external.html,sha256=eL9euOuE93JKZdqlXxBOlHbKwIuNuIdq7GBRpsaPMcU,92 +setuptools/tests/indexes/test_links_priority/simple/foobar/index.html,sha256=DD-TKr7UU4zAjHHz4VexYDNSAzR27levSh1c-k3ZdLE,174 +setuptools/tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +setuptools/tests/integration/__pycache__/__init__.cpython-311.pyc,, +setuptools/tests/integration/__pycache__/helpers.cpython-311.pyc,, +setuptools/tests/integration/__pycache__/test_pip_install_sdist.cpython-311.pyc,, +setuptools/tests/integration/helpers.py,sha256=3PHcS9SCA-fwVJmUP2ad5NQOttJAETI5Nnoc_xroO5k,2522 +setuptools/tests/integration/test_pip_install_sdist.py,sha256=SFbvuYF_hDzt6OtsQ5GjFNnxmoJ_eElfvpYsiyyGJ-g,8256 +setuptools/tests/mod_with_constant.py,sha256=X_Kj80M55w1tmQ4f7uZY91ZTALo4hKVT6EHxgYocUMQ,22 +setuptools/tests/namespaces.py,sha256=HPcI3nR5MCFWXpaADIJ1fwKxymcQgBkuw87Ic5PUSAQ,2774 +setuptools/tests/script-with-bom.py,sha256=hRRgIizEULGiG_ZTNoMY46HhKhxpWfy5FGcD6Qbh5fc,18 +setuptools/tests/server.py,sha256=0FDZf0cSInCP5n1haK7AxHo3jD261RK7D3-RjP-F53k,2397 +setuptools/tests/test_archive_util.py,sha256=buuKdY8XkW26Pe3IKAoBRGHG0MDumnuNoPg2WsAQzIg,845 +setuptools/tests/test_bdist_deprecations.py,sha256=75Xq3gYn79LIIyusEltbHan0bEgAt2e_CaL7KLS8-KQ,775 +setuptools/tests/test_bdist_egg.py,sha256=6PaYN1F3JDbIh1uK0urv7yJFcx98z5dn9SOJ8Mv91l8,1957 +setuptools/tests/test_bdist_wheel.py,sha256=dZ9a7OT_UyRvLnoCi2KGEIbtzhEQjM3YutYMA6ZCezs,23083 +setuptools/tests/test_build.py,sha256=wJgMz2hwHADcLFg-nXrwRVhus7hjmAeEGgrpIQwCGnA,798 +setuptools/tests/test_build_clib.py,sha256=bX51XRAf4uO7IuHFpjePnoK8mE74N2gsoeEqF-ofgws,3123 +setuptools/tests/test_build_ext.py,sha256=e4ZSxsYPB5zq1KSqGEuATZ0t0PJQzMhjjkKJ-hIjcgc,10099 +setuptools/tests/test_build_meta.py,sha256=Iakpp9Micvhxi9_F2RPyY2NnCB1G90a38s4O5A3CuiE,34118 +setuptools/tests/test_build_py.py,sha256=gobME_Cvzf6Ugxq70iWfXekb_xyyT61khwjFq8zkwfw,14186 +setuptools/tests/test_config_discovery.py,sha256=FqV-lOtkqaI-ayzU2zocSdD5TaRAgCZnixNDilKA6FQ,22580 +setuptools/tests/test_core_metadata.py,sha256=vbVJ5_Lsx_hsO_GdB6nQEXJRjA2ydx6_qSbr5LpheAA,20881 +setuptools/tests/test_depends.py,sha256=yQBXoQbNQlJit6mbRVoz6Bb553f3sNrq02lZimNz5XY,424 +setuptools/tests/test_develop.py,sha256=CLzXZ8-b5-VFTuau4P4yXEdLx1UdyTFcOfrV0qyUIdE,5142 +setuptools/tests/test_dist.py,sha256=GFjyL2etAxvVM3q7NhFEGcXS5gyKj8VzbqcbKzpqbOk,8901 +setuptools/tests/test_dist_info.py,sha256=5kBRj9tuBsVreBsY22H2feMO_JQZsSoOZMU_MJfUevY,7077 +setuptools/tests/test_distutils_adoption.py,sha256=_eynrOfyEqXFEmjUJhzpe8GXPyTUPvNSObs4qAAmBy8,5987 +setuptools/tests/test_easy_install.py,sha256=jx4lpFyee0G432cdnwBow3AkL4ibw-0QILwldwv5SCI,53534 +setuptools/tests/test_editable_install.py,sha256=7eTEtpT0k7QeVyZg64eh3kZn-SjckuB9LcokOuV37DI,43383 +setuptools/tests/test_egg_info.py,sha256=QCzoUOkFocmbkwS6XU7F8WNzKE8CGEMRxYBqKLgfUrc,44866 +setuptools/tests/test_extern.py,sha256=rpKU6oCcksumLwf5TeKlDluFQ0TUfbPwTLQbpxcFrCU,296 +setuptools/tests/test_find_packages.py,sha256=CTLAcTzWGWBLCcd2aAsUVkvO3ibrlqexFBdDKOWPoq8,7819 +setuptools/tests/test_find_py_modules.py,sha256=zQjuhIG5TQN2SJPix9ARo4DL_w84Ln8QsHDUjjbrtAQ,2404 +setuptools/tests/test_glob.py,sha256=P3JvpH-kXQ4BZ3zvRF-zKxOgwyWzwIaQIz0WHdxS0kk,887 +setuptools/tests/test_install_scripts.py,sha256=scIrJ6a_ssKqg4vIBNaUjmAKHEYLUUZ9WKnPeKnE6gc,3433 +setuptools/tests/test_logging.py,sha256=zlE5DlldukC7Jc54FNvDV_7ux3ErAkrfrN5CSsnNOUQ,2099 +setuptools/tests/test_manifest.py,sha256=eMg65pIA52DizB6mpktSU-b8CjwaNCS5MSgL_V1LrFI,18562 +setuptools/tests/test_namespaces.py,sha256=Y6utoe5PHHqL_DlgawqB9F8XpsUDPvvw1sQMenK04e0,4515 +setuptools/tests/test_packageindex.py,sha256=qEjLHpSu2gAkegwEstzHQT-Om1uQIYjA8zeNzEX79uo,8775 +setuptools/tests/test_sandbox.py,sha256=shUWE7fLTWe7Jzdfi6NheBWauG3oUukbUV46cArD0u0,4330 +setuptools/tests/test_sdist.py,sha256=RYLvPa_nfyC1ZmoinzqMzJynTDG4RtPYC19_0LU6pvs,32872 +setuptools/tests/test_setopt.py,sha256=3VxxM4ATfP-P4AGnDjoWCnHr5-i9CSEQTFYU1-FTnvI,1365 +setuptools/tests/test_setuptools.py,sha256=_eIhqKf45-OtHqxRf20KndOZJlJdS0PuFLXBO3M-LN8,9008 +setuptools/tests/test_shutil_wrapper.py,sha256=g15E11PtZxG-InB2BWNFyH-svObXx2XcMhgMLJPuFnc,641 +setuptools/tests/test_unicode_utils.py,sha256=xWfEEl8jkQCt9othUTXJfFmdyATAFggJs2tTxjbumbw,316 +setuptools/tests/test_virtualenv.py,sha256=g-njC_9JTAs1YVx_1dGJ_Q6RlInO4qKVu9-XAgNb6TY,3730 +setuptools/tests/test_warnings.py,sha256=zwR2zcnCeCeDqILZlJOPAcuyPHoDvGu1OtOVYiLMk74,3347 +setuptools/tests/test_wheel.py,sha256=J-83W1KdXTgAjFZE3H-ytohhvDE1iqdbE5YF5jLQlGQ,19370 +setuptools/tests/test_windows_wrappers.py,sha256=aF6UTowN3yzCgdBh9nDQVvYIfSYogrTK776TEyXEBqg,7881 +setuptools/tests/text.py,sha256=a12197pMVTvB6FAWQ0ujT8fIQiLIWJlFAl1UCaDUDfg,123 +setuptools/tests/textwrap.py,sha256=FNNNq_MiaEJx88PnsbJQIRxmj1qmgcAOCXXRsODPJN4,98 +setuptools/unicode_utils.py,sha256=ukMGh8pEAw6F_Ezb-K5D3c-078RgA_GcF0oW6lg4lSs,3189 +setuptools/version.py,sha256=WJCeUuyq74Aok2TeK9-OexZOu8XrlQy7-y0BEuWNovQ,161 +setuptools/warnings.py,sha256=oY0Se5eOqje_FEyjTgonUc0XGwgsrI5cgm1kkwulz_w,3796 +setuptools/wheel.py,sha256=xkAtvgm7uPTyYV2zqVmQ0wA8kLwOyRT2Jes1zAy07Ks,8624 +setuptools/windows_support.py,sha256=wW4IYLM1Bv7Z1MaauP2xmPjyy-wkmQnXdyvXscAf9fw,726 diff --git a/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/WHEEL b/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..1eb3c49d99559863120cfb8433fc8738fba43ba9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (78.1.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/entry_points.txt b/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/entry_points.txt new file mode 100644 index 0000000000000000000000000000000000000000..0db0a6c8f1b8d9c0ad4a25db6892e29f8988fcf2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/entry_points.txt @@ -0,0 +1,51 @@ +[distutils.commands] +alias = setuptools.command.alias:alias +bdist_egg = setuptools.command.bdist_egg:bdist_egg +bdist_rpm = setuptools.command.bdist_rpm:bdist_rpm +bdist_wheel = setuptools.command.bdist_wheel:bdist_wheel +build = setuptools.command.build:build +build_clib = setuptools.command.build_clib:build_clib +build_ext = setuptools.command.build_ext:build_ext +build_py = setuptools.command.build_py:build_py +develop = setuptools.command.develop:develop +dist_info = setuptools.command.dist_info:dist_info +easy_install = setuptools.command.easy_install:easy_install +editable_wheel = setuptools.command.editable_wheel:editable_wheel +egg_info = setuptools.command.egg_info:egg_info +install = setuptools.command.install:install +install_egg_info = setuptools.command.install_egg_info:install_egg_info +install_lib = setuptools.command.install_lib:install_lib +install_scripts = setuptools.command.install_scripts:install_scripts +rotate = setuptools.command.rotate:rotate +saveopts = setuptools.command.saveopts:saveopts +sdist = setuptools.command.sdist:sdist +setopt = setuptools.command.setopt:setopt + +[distutils.setup_keywords] +dependency_links = setuptools.dist:assert_string_list +eager_resources = setuptools.dist:assert_string_list +entry_points = setuptools.dist:check_entry_points +exclude_package_data = setuptools.dist:check_package_data +extras_require = setuptools.dist:check_extras +include_package_data = setuptools.dist:assert_bool +install_requires = setuptools.dist:check_requirements +namespace_packages = setuptools.dist:check_nsp +package_data = setuptools.dist:check_package_data +packages = setuptools.dist:check_packages +python_requires = setuptools.dist:check_specifier +setup_requires = setuptools.dist:check_requirements +use_2to3 = setuptools.dist:invalid_unless_false +zip_safe = setuptools.dist:assert_bool + +[egg_info.writers] +PKG-INFO = setuptools.command.egg_info:write_pkg_info +dependency_links.txt = setuptools.command.egg_info:overwrite_arg +eager_resources.txt = setuptools.command.egg_info:overwrite_arg +entry_points.txt = setuptools.command.egg_info:write_entries +namespace_packages.txt = setuptools.command.egg_info:overwrite_arg +requires.txt = setuptools.command.egg_info:write_requirements +top_level.txt = setuptools.command.egg_info:write_toplevel_names + +[setuptools.finalize_distribution_options] +keywords = setuptools.dist:Distribution._finalize_setup_keywords +parent_finalize = setuptools.dist:_Distribution.finalize_options diff --git a/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..1bb5a44356f00884a71ceeefd24ded6caaba2418 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/licenses/LICENSE @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/top_level.txt b/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..b5ac1070294b478b7cc2ce677207ee08813bfa37 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools-78.1.0.dist-info/top_level.txt @@ -0,0 +1,3 @@ +_distutils_hack +pkg_resources +setuptools diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/config/__init__.py b/venv/lib/python3.11/site-packages/setuptools/tests/config/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/config/downloads/__init__.py b/venv/lib/python3.11/site-packages/setuptools/tests/config/downloads/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..00a16423f448e7773f5e6dc9b365efe80f40a778 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/config/downloads/__init__.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import re +import time +from pathlib import Path +from urllib.error import HTTPError +from urllib.request import urlopen + +__all__ = ["DOWNLOAD_DIR", "retrieve_file", "output_file", "urls_from_file"] + + +NAME_REMOVE = ("http://", "https://", "github.com/", "/raw/") +DOWNLOAD_DIR = Path(__file__).parent + + +# ---------------------------------------------------------------------- +# Please update ./preload.py accordingly when modifying this file +# ---------------------------------------------------------------------- + + +def output_file(url: str, download_dir: Path = DOWNLOAD_DIR) -> Path: + file_name = url.strip() + for part in NAME_REMOVE: + file_name = file_name.replace(part, '').strip().strip('/:').strip() + return Path(download_dir, re.sub(r"[^\-_\.\w\d]+", "_", file_name)) + + +def retrieve_file(url: str, download_dir: Path = DOWNLOAD_DIR, wait: float = 5) -> Path: + path = output_file(url, download_dir) + if path.exists(): + print(f"Skipping {url} (already exists: {path})") + else: + download_dir.mkdir(exist_ok=True, parents=True) + print(f"Downloading {url} to {path}") + try: + download(url, path) + except HTTPError: + time.sleep(wait) # wait a few seconds and try again. + download(url, path) + return path + + +def urls_from_file(list_file: Path) -> list[str]: + """``list_file`` should be a text file where each line corresponds to a URL to + download. + """ + print(f"file: {list_file}") + content = list_file.read_text(encoding="utf-8") + return [url for url in content.splitlines() if not url.startswith("#")] + + +def download(url: str, dest: Path): + with urlopen(url) as f: + data = f.read() + + with open(dest, "wb") as f: + f.write(data) + + assert Path(dest).exists() diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/config/downloads/preload.py b/venv/lib/python3.11/site-packages/setuptools/tests/config/downloads/preload.py new file mode 100644 index 0000000000000000000000000000000000000000..8eeb5dd75d3dcb375cee5acaf11ad385084bff5a --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/config/downloads/preload.py @@ -0,0 +1,18 @@ +"""This file can be used to preload files needed for testing. + +For example you can use:: + + cd setuptools/tests/config + python -m downloads.preload setupcfg_examples.txt + +to make sure the `setup.cfg` examples are downloaded before starting the tests. +""" + +import sys +from pathlib import Path + +from . import retrieve_file, urls_from_file + +if __name__ == "__main__": + urls = urls_from_file(Path(sys.argv[1])) + list(map(retrieve_file, urls)) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/config/setupcfg_examples.txt b/venv/lib/python3.11/site-packages/setuptools/tests/config/setupcfg_examples.txt new file mode 100644 index 0000000000000000000000000000000000000000..6aab887ff1fe631d97f1abea90a8448040746a12 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/config/setupcfg_examples.txt @@ -0,0 +1,22 @@ +# ==================================================================== +# Some popular packages that use setup.cfg (and others not so popular) +# Reference: https://hugovk.github.io/top-pypi-packages/ +# ==================================================================== +https://github.com/pypa/setuptools/raw/52c990172fec37766b3566679724aa8bf70ae06d/setup.cfg +https://github.com/pypa/wheel/raw/0acd203cd896afec7f715aa2ff5980a403459a3b/setup.cfg +https://github.com/python/importlib_metadata/raw/2f05392ca980952a6960d82b2f2d2ea10aa53239/setup.cfg +https://github.com/jaraco/skeleton/raw/d9008b5c510cd6969127a6a2ab6f832edddef296/setup.cfg +https://github.com/jaraco/zipp/raw/700d3a96390e970b6b962823bfea78b4f7e1c537/setup.cfg +https://github.com/pallets/jinja/raw/7d72eb7fefb7dce065193967f31f805180508448/setup.cfg +https://github.com/tkem/cachetools/raw/2fd87a94b8d3861d80e9e4236cd480bfdd21c90d/setup.cfg +https://github.com/aio-libs/aiohttp/raw/5e0e6b7080f2408d5f1dd544c0e1cf88378b7b10/setup.cfg +https://github.com/pallets/flask/raw/9486b6cf57bd6a8a261f67091aca8ca78eeec1e3/setup.cfg +https://github.com/pallets/click/raw/6411f425fae545f42795665af4162006b36c5e4a/setup.cfg +https://github.com/sqlalchemy/sqlalchemy/raw/533f5718904b620be8d63f2474229945d6f8ba5d/setup.cfg +https://github.com/pytest-dev/pluggy/raw/461ef63291d13589c4e21aa182cd1529257e9a0a/setup.cfg +https://github.com/pytest-dev/pytest/raw/c7be96dae487edbd2f55b561b31b68afac1dabe6/setup.cfg +https://github.com/platformdirs/platformdirs/raw/7b7852128dd6f07511b618d6edea35046bd0c6ff/setup.cfg +https://github.com/pandas-dev/pandas/raw/bc17343f934a33dc231c8c74be95d8365537c376/setup.cfg +https://github.com/django/django/raw/4e249d11a6e56ca8feb4b055b681cec457ef3a3d/setup.cfg +https://github.com/pyscaffold/pyscaffold/raw/de7aa5dc059fbd04307419c667cc4961bc9df4b8/setup.cfg +https://github.com/pypa/virtualenv/raw/f92eda6e3da26a4d28c2663ffb85c4960bdb990c/setup.cfg diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/config/test_apply_pyprojecttoml.py b/venv/lib/python3.11/site-packages/setuptools/tests/config/test_apply_pyprojecttoml.py new file mode 100644 index 0000000000000000000000000000000000000000..489fd98e26710e74d66204b6f51c2523586aaa04 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -0,0 +1,772 @@ +"""Make sure that applying the configuration from pyproject.toml is equivalent to +applying a similar configuration from setup.cfg + +To run these tests offline, please have a look on ``./downloads/preload.py`` +""" + +from __future__ import annotations + +import io +import re +import tarfile +from inspect import cleandoc +from pathlib import Path +from unittest.mock import Mock + +import pytest +from ini2toml.api import LiteTranslator +from packaging.metadata import Metadata + +import setuptools # noqa: F401 # ensure monkey patch to metadata +from setuptools._static import is_static +from setuptools.command.egg_info import write_requirements +from setuptools.config import expand, pyprojecttoml, setupcfg +from setuptools.config._apply_pyprojecttoml import _MissingDynamic, _some_attrgetter +from setuptools.dist import Distribution +from setuptools.errors import InvalidConfigError, RemovedConfigError +from setuptools.warnings import InformationOnly, SetuptoolsDeprecationWarning + +from .downloads import retrieve_file, urls_from_file + +HERE = Path(__file__).parent +EXAMPLES_FILE = "setupcfg_examples.txt" + + +def makedist(path, **attrs): + return Distribution({"src_root": path, **attrs}) + + +def _mock_expand_patterns(patterns, *_, **__): + """ + Allow comparing the given patterns for 2 dist objects. + We need to strip special chars to avoid errors when validating. + """ + return [re.sub("[^a-z0-9]+", "", p, flags=re.I) or "empty" for p in patterns] + + +@pytest.mark.parametrize("url", urls_from_file(HERE / EXAMPLES_FILE)) +@pytest.mark.filterwarnings("ignore") +@pytest.mark.uses_network +def test_apply_pyproject_equivalent_to_setupcfg(url, monkeypatch, tmp_path): + monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.0.1")) + monkeypatch.setattr( + Distribution, "_expand_patterns", Mock(side_effect=_mock_expand_patterns) + ) + setupcfg_example = retrieve_file(url) + pyproject_example = Path(tmp_path, "pyproject.toml") + setupcfg_text = setupcfg_example.read_text(encoding="utf-8") + toml_config = LiteTranslator().translate(setupcfg_text, "setup.cfg") + pyproject_example.write_text(toml_config, encoding="utf-8") + + dist_toml = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject_example) + dist_cfg = setupcfg.apply_configuration(makedist(tmp_path), setupcfg_example) + + pkg_info_toml = core_metadata(dist_toml) + pkg_info_cfg = core_metadata(dist_cfg) + assert pkg_info_toml == pkg_info_cfg + + if any(getattr(d, "license_files", None) for d in (dist_toml, dist_cfg)): + assert set(dist_toml.license_files) == set(dist_cfg.license_files) + + if any(getattr(d, "entry_points", None) for d in (dist_toml, dist_cfg)): + print(dist_cfg.entry_points) + ep_toml = { + (k, *sorted(i.replace(" ", "") for i in v)) + for k, v in dist_toml.entry_points.items() + } + ep_cfg = { + (k, *sorted(i.replace(" ", "") for i in v)) + for k, v in dist_cfg.entry_points.items() + } + assert ep_toml == ep_cfg + + if any(getattr(d, "package_data", None) for d in (dist_toml, dist_cfg)): + pkg_data_toml = {(k, *sorted(v)) for k, v in dist_toml.package_data.items()} + pkg_data_cfg = {(k, *sorted(v)) for k, v in dist_cfg.package_data.items()} + assert pkg_data_toml == pkg_data_cfg + + if any(getattr(d, "data_files", None) for d in (dist_toml, dist_cfg)): + data_files_toml = {(k, *sorted(v)) for k, v in dist_toml.data_files} + data_files_cfg = {(k, *sorted(v)) for k, v in dist_cfg.data_files} + assert data_files_toml == data_files_cfg + + assert set(dist_toml.install_requires) == set(dist_cfg.install_requires) + if any(getattr(d, "extras_require", None) for d in (dist_toml, dist_cfg)): + extra_req_toml = {(k, *sorted(v)) for k, v in dist_toml.extras_require.items()} + extra_req_cfg = {(k, *sorted(v)) for k, v in dist_cfg.extras_require.items()} + assert extra_req_toml == extra_req_cfg + + +PEP621_EXAMPLE = """\ +[project] +name = "spam" +version = "2020.0.0" +description = "Lovely Spam! Wonderful Spam!" +readme = "README.rst" +requires-python = ">=3.8" +license-files = ["LICENSE.txt"] # Updated to be PEP 639 compliant +keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"] +authors = [ + {email = "hi@pradyunsg.me"}, + {name = "Tzu-Ping Chung"} +] +maintainers = [ + {name = "Brett Cannon", email = "brett@python.org"}, + {name = "John X. Ãørçeč", email = "john@utf8.org"}, + {name = "Γαμα קּ 東", email = "gama@utf8.org"}, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python" +] + +dependencies = [ + "httpx", + "gidgethub[httpx]>4.0.0", + "django>2.1; os_name != 'nt'", + "django>2.0; os_name == 'nt'" +] + +[project.optional-dependencies] +test = [ + "pytest < 5.0.0", + "pytest-cov[all]" +] + +[project.urls] +homepage = "http://example.com" +documentation = "http://readthedocs.org" +repository = "http://github.com" +changelog = "http://github.com/me/spam/blob/master/CHANGELOG.md" + +[project.scripts] +spam-cli = "spam:main_cli" + +[project.gui-scripts] +spam-gui = "spam:main_gui" + +[project.entry-points."spam.magical"] +tomatoes = "spam:main_tomatoes" +""" + +PEP621_INTERNATIONAL_EMAIL_EXAMPLE = """\ +[project] +name = "spam" +version = "2020.0.0" +authors = [ + {email = "hi@pradyunsg.me"}, + {name = "Tzu-Ping Chung"} +] +maintainers = [ + {name = "Степан Бандера", email = "криївка@оун-упа.укр"}, +] +""" + +PEP621_EXAMPLE_SCRIPT = """ +def main_cli(): pass +def main_gui(): pass +def main_tomatoes(): pass +""" + +PEP639_LICENSE_TEXT = """\ +[project] +name = "spam" +version = "2020.0.0" +authors = [ + {email = "hi@pradyunsg.me"}, + {name = "Tzu-Ping Chung"} +] +license = {text = "MIT"} +""" + +PEP639_LICENSE_EXPRESSION = """\ +[project] +name = "spam" +version = "2020.0.0" +authors = [ + {email = "hi@pradyunsg.me"}, + {name = "Tzu-Ping Chung"} +] +license = "mit or apache-2.0" # should be normalized in metadata +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", +] +""" + + +def _pep621_example_project( + tmp_path, + readme="README.rst", + pyproject_text=PEP621_EXAMPLE, +): + pyproject = tmp_path / "pyproject.toml" + text = pyproject_text + replacements = {'readme = "README.rst"': f'readme = "{readme}"'} + for orig, subst in replacements.items(): + text = text.replace(orig, subst) + pyproject.write_text(text, encoding="utf-8") + + (tmp_path / readme).write_text("hello world", encoding="utf-8") + (tmp_path / "LICENSE.txt").write_text("--- LICENSE stub ---", encoding="utf-8") + (tmp_path / "spam.py").write_text(PEP621_EXAMPLE_SCRIPT, encoding="utf-8") + return pyproject + + +def test_pep621_example(tmp_path): + """Make sure the example in PEP 621 works""" + pyproject = _pep621_example_project(tmp_path) + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + assert set(dist.metadata.license_files) == {"LICENSE.txt"} + + +@pytest.mark.parametrize( + ("readme", "ctype"), + [ + ("Readme.txt", "text/plain"), + ("readme.md", "text/markdown"), + ("text.rst", "text/x-rst"), + ], +) +def test_readme_content_type(tmp_path, readme, ctype): + pyproject = _pep621_example_project(tmp_path, readme) + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + assert dist.metadata.long_description_content_type == ctype + + +def test_undefined_content_type(tmp_path): + pyproject = _pep621_example_project(tmp_path, "README.tex") + with pytest.raises(ValueError, match="Undefined content type for README.tex"): + pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + + +def test_no_explicit_content_type_for_missing_extension(tmp_path): + pyproject = _pep621_example_project(tmp_path, "README") + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + assert dist.metadata.long_description_content_type is None + + +@pytest.mark.parametrize( + ("pyproject_text", "expected_maintainers_meta_value"), + ( + pytest.param( + PEP621_EXAMPLE, + ( + 'Brett Cannon , "John X. Ãørçeč" , ' + 'Γαμα קּ 東 ' + ), + id='non-international-emails', + ), + pytest.param( + PEP621_INTERNATIONAL_EMAIL_EXAMPLE, + 'Степан Бандера <криївка@оун-упа.укр>', + marks=pytest.mark.xfail( + reason="CPython's `email.headerregistry.Address` only supports " + 'RFC 5322, as of Nov 10, 2022 and latest Python 3.11.0', + strict=True, + ), + id='international-email', + ), + ), +) +def test_utf8_maintainer_in_metadata( # issue-3663 + expected_maintainers_meta_value, + pyproject_text, + tmp_path, +): + pyproject = _pep621_example_project( + tmp_path, + "README", + pyproject_text=pyproject_text, + ) + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + assert dist.metadata.maintainer_email == expected_maintainers_meta_value + pkg_file = tmp_path / "PKG-FILE" + with open(pkg_file, "w", encoding="utf-8") as fh: + dist.metadata.write_pkg_file(fh) + content = pkg_file.read_text(encoding="utf-8") + assert f"Maintainer-email: {expected_maintainers_meta_value}" in content + + +@pytest.mark.parametrize( + ( + 'pyproject_text', + 'license', + 'license_expression', + 'content_str', + 'not_content_str', + ), + ( + pytest.param( + PEP639_LICENSE_TEXT, + 'MIT', + None, + 'License: MIT', + 'License-Expression: ', + id='license-text', + marks=[ + pytest.mark.filterwarnings( + "ignore:.project.license. as a TOML table is deprecated", + ) + ], + ), + pytest.param( + PEP639_LICENSE_EXPRESSION, + None, + 'MIT OR Apache-2.0', + 'License-Expression: MIT OR Apache-2.0', + 'License: ', + id='license-expression', + ), + ), +) +def test_license_in_metadata( + license, + license_expression, + content_str, + not_content_str, + pyproject_text, + tmp_path, +): + pyproject = _pep621_example_project( + tmp_path, + "README", + pyproject_text=pyproject_text, + ) + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + assert dist.metadata.license == license + assert dist.metadata.license_expression == license_expression + pkg_file = tmp_path / "PKG-FILE" + with open(pkg_file, "w", encoding="utf-8") as fh: + dist.metadata.write_pkg_file(fh) + content = pkg_file.read_text(encoding="utf-8") + assert "Metadata-Version: 2.4" in content + assert content_str in content + assert not_content_str not in content + + +def test_license_classifier_with_license_expression(tmp_path): + text = PEP639_LICENSE_EXPRESSION.rsplit("\n", 2)[0] + pyproject = _pep621_example_project( + tmp_path, + "README", + f"{text}\n \"License :: OSI Approved :: MIT License\"\n]", + ) + msg = "License classifiers have been superseded by license expressions" + with pytest.raises(InvalidConfigError, match=msg) as exc: + pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + + assert "License :: OSI Approved :: MIT License" in str(exc.value) + + +def test_license_classifier_without_license_expression(tmp_path): + text = """\ + [project] + name = "spam" + version = "2020.0.0" + license = {text = "mit or apache-2.0"} + classifiers = ["License :: OSI Approved :: MIT License"] + """ + pyproject = _pep621_example_project(tmp_path, "README", text) + + msg1 = "License classifiers are deprecated(?:.|\n)*MIT License" + msg2 = ".project.license. as a TOML table is deprecated" + with ( + pytest.warns(SetuptoolsDeprecationWarning, match=msg1), + pytest.warns(SetuptoolsDeprecationWarning, match=msg2), + ): + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + + # Check license classifier is still included + assert dist.metadata.get_classifiers() == ["License :: OSI Approved :: MIT License"] + + +class TestLicenseFiles: + def base_pyproject( + self, + tmp_path, + additional_text="", + license_toml='license = {file = "LICENSE.txt"}\n', + ): + text = PEP639_LICENSE_EXPRESSION + + # Sanity-check + assert 'license = "mit or apache-2.0"' in text + assert 'license-files' not in text + assert "[tool.setuptools]" not in text + + text = re.sub( + r"(license = .*)\n", + license_toml, + text, + count=1, + ) + assert license_toml in text # sanity check + text = f"{text}\n{additional_text}\n" + pyproject = _pep621_example_project(tmp_path, "README", pyproject_text=text) + return pyproject + + def base_pyproject_license_pep639(self, tmp_path, additional_text=""): + return self.base_pyproject( + tmp_path, + additional_text=additional_text, + license_toml='license = "licenseref-Proprietary"' + '\nlicense-files = ["_FILE*"]\n', + ) + + def test_both_license_and_license_files_defined(self, tmp_path): + setuptools_config = '[tool.setuptools]\nlicense-files = ["_FILE*"]' + pyproject = self.base_pyproject(tmp_path, setuptools_config) + + (tmp_path / "_FILE.txt").touch() + (tmp_path / "_FILE.rst").touch() + + # Would normally match the `license_files` patterns, but we want to exclude it + # by being explicit. On the other hand, contents should be added to `license` + license = tmp_path / "LICENSE.txt" + license.write_text("LicenseRef-Proprietary\n", encoding="utf-8") + + msg1 = "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'" + msg2 = ".project.license. as a TOML table is deprecated" + with ( + pytest.warns(SetuptoolsDeprecationWarning, match=msg1), + pytest.warns(SetuptoolsDeprecationWarning, match=msg2), + ): + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + assert set(dist.metadata.license_files) == {"_FILE.rst", "_FILE.txt"} + assert dist.metadata.license == "LicenseRef-Proprietary\n" + + def test_both_license_and_license_files_defined_pep639(self, tmp_path): + # Set license and license-files + pyproject = self.base_pyproject_license_pep639(tmp_path) + + (tmp_path / "_FILE.txt").touch() + (tmp_path / "_FILE.rst").touch() + + msg = "Normalizing.*LicenseRef" + with pytest.warns(InformationOnly, match=msg): + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + + assert set(dist.metadata.license_files) == {"_FILE.rst", "_FILE.txt"} + assert dist.metadata.license is None + assert dist.metadata.license_expression == "LicenseRef-Proprietary" + + def test_license_files_defined_twice(self, tmp_path): + # Set project.license-files and tools.setuptools.license-files + setuptools_config = '[tool.setuptools]\nlicense-files = ["_FILE*"]' + pyproject = self.base_pyproject_license_pep639(tmp_path, setuptools_config) + + msg = "'project.license-files' is defined already. Remove 'tool.setuptools.license-files'" + with pytest.raises(InvalidConfigError, match=msg): + pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + + def test_default_patterns(self, tmp_path): + setuptools_config = '[tool.setuptools]\nzip-safe = false' + # ^ used just to trigger section validation + pyproject = self.base_pyproject(tmp_path, setuptools_config, license_toml="") + + license_files = "LICENCE-a.html COPYING-abc.txt AUTHORS-xyz NOTICE,def".split() + + for fname in license_files: + (tmp_path / fname).write_text(f"{fname}\n", encoding="utf-8") + + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + + assert (tmp_path / "LICENSE.txt").exists() # from base example + assert set(dist.metadata.license_files) == {*license_files, "LICENSE.txt"} + + def test_missing_patterns(self, tmp_path): + pyproject = self.base_pyproject_license_pep639(tmp_path) + assert list(tmp_path.glob("_FILE*")) == [] # sanity check + + msg1 = "Cannot find any files for the given pattern.*" + msg2 = "Normalizing 'licenseref-Proprietary' to 'LicenseRef-Proprietary'" + with ( + pytest.warns(SetuptoolsDeprecationWarning, match=msg1), + pytest.warns(InformationOnly, match=msg2), + ): + pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + + def test_deprecated_file_expands_to_text(self, tmp_path): + """Make sure the old example with ``license = {text = ...}`` works""" + + assert 'license-files = ["LICENSE.txt"]' in PEP621_EXAMPLE # sanity check + text = PEP621_EXAMPLE.replace( + 'license-files = ["LICENSE.txt"]', + 'license = {file = "LICENSE.txt"}', + ) + pyproject = _pep621_example_project(tmp_path, pyproject_text=text) + + msg = ".project.license. as a TOML table is deprecated" + with pytest.warns(SetuptoolsDeprecationWarning, match=msg): + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + + assert dist.metadata.license == "--- LICENSE stub ---" + assert set(dist.metadata.license_files) == {"LICENSE.txt"} # auto-filled + + +class TestPyModules: + # https://github.com/pypa/setuptools/issues/4316 + + def dist(self, name): + toml_config = f""" + [project] + name = "test" + version = "42.0" + [tool.setuptools] + py-modules = [{name!r}] + """ + pyproject = Path("pyproject.toml") + pyproject.write_text(cleandoc(toml_config), encoding="utf-8") + return pyprojecttoml.apply_configuration(Distribution({}), pyproject) + + @pytest.mark.parametrize("module", ["pip-run", "abc-d.λ-xyz-e"]) + def test_valid_module_name(self, tmp_path, monkeypatch, module): + monkeypatch.chdir(tmp_path) + assert module in self.dist(module).py_modules + + @pytest.mark.parametrize("module", ["pip run", "-pip-run", "pip-run-stubs"]) + def test_invalid_module_name(self, tmp_path, monkeypatch, module): + monkeypatch.chdir(tmp_path) + with pytest.raises(ValueError, match="py-modules"): + self.dist(module).py_modules + + +class TestExtModules: + def test_pyproject_sets_attribute(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + pyproject = Path("pyproject.toml") + toml_config = """ + [project] + name = "test" + version = "42.0" + [tool.setuptools] + ext-modules = [ + {name = "my.ext", sources = ["hello.c", "world.c"]} + ] + """ + pyproject.write_text(cleandoc(toml_config), encoding="utf-8") + with pytest.warns(pyprojecttoml._ExperimentalConfiguration): + dist = pyprojecttoml.apply_configuration(Distribution({}), pyproject) + assert len(dist.ext_modules) == 1 + assert dist.ext_modules[0].name == "my.ext" + assert set(dist.ext_modules[0].sources) == {"hello.c", "world.c"} + + +class TestDeprecatedFields: + def test_namespace_packages(self, tmp_path): + pyproject = tmp_path / "pyproject.toml" + config = """ + [project] + name = "myproj" + version = "42" + [tool.setuptools] + namespace-packages = ["myproj.pkg"] + """ + pyproject.write_text(cleandoc(config), encoding="utf-8") + with pytest.raises(RemovedConfigError, match="namespace-packages"): + pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + + +class TestPresetField: + def pyproject(self, tmp_path, dynamic, extra_content=""): + content = f"[project]\nname = 'proj'\ndynamic = {dynamic!r}\n" + if "version" not in dynamic: + content += "version = '42'\n" + file = tmp_path / "pyproject.toml" + file.write_text(content + extra_content, encoding="utf-8") + return file + + @pytest.mark.parametrize( + ("attr", "field", "value"), + [ + ("license_expression", "license", "MIT"), + pytest.param( + *("license", "license", "Not SPDX"), + marks=[pytest.mark.filterwarnings("ignore:.*license. overwritten")], + ), + ("classifiers", "classifiers", ["Private :: Classifier"]), + ("entry_points", "scripts", {"console_scripts": ["foobar=foobar:main"]}), + ("entry_points", "gui-scripts", {"gui_scripts": ["bazquux=bazquux:main"]}), + pytest.param( + *("install_requires", "dependencies", ["six"]), + marks=[ + pytest.mark.filterwarnings("ignore:.*install_requires. overwritten") + ], + ), + ], + ) + def test_not_listed_in_dynamic(self, tmp_path, attr, field, value): + """Setuptools cannot set a field if not listed in ``dynamic``""" + pyproject = self.pyproject(tmp_path, []) + dist = makedist(tmp_path, **{attr: value}) + msg = re.compile(f"defined outside of `pyproject.toml`:.*{field}", re.S) + with pytest.warns(_MissingDynamic, match=msg): + dist = pyprojecttoml.apply_configuration(dist, pyproject) + + dist_value = _some_attrgetter(f"metadata.{attr}", attr)(dist) + assert not dist_value + + @pytest.mark.parametrize( + ("attr", "field", "value"), + [ + ("license_expression", "license", "MIT"), + ("install_requires", "dependencies", []), + ("extras_require", "optional-dependencies", {}), + ("install_requires", "dependencies", ["six"]), + ("classifiers", "classifiers", ["Private :: Classifier"]), + ], + ) + def test_listed_in_dynamic(self, tmp_path, attr, field, value): + pyproject = self.pyproject(tmp_path, [field]) + dist = makedist(tmp_path, **{attr: value}) + dist = pyprojecttoml.apply_configuration(dist, pyproject) + dist_value = _some_attrgetter(f"metadata.{attr}", attr)(dist) + assert dist_value == value + + def test_license_files_exempt_from_dynamic(self, monkeypatch, tmp_path): + """ + license-file is currently not considered in the context of dynamic. + As per 2025-02-19, https://packaging.python.org/en/latest/specifications/pyproject-toml/#license-files + allows setuptools to fill-in `license-files` the way it sees fit: + + > If the license-files key is not defined, tools can decide how to handle license files. + > For example they can choose not to include any files or use their own + > logic to discover the appropriate files in the distribution. + + Using license_files from setup.py to fill-in the value is in accordance + with this rule. + """ + monkeypatch.chdir(tmp_path) + pyproject = self.pyproject(tmp_path, []) + dist = makedist(tmp_path, license_files=["LIC*"]) + (tmp_path / "LIC1").write_text("42", encoding="utf-8") + dist = pyprojecttoml.apply_configuration(dist, pyproject) + assert dist.metadata.license_files == ["LIC1"] + + def test_warning_overwritten_dependencies(self, tmp_path): + src = "[project]\nname='pkg'\nversion='0.1'\ndependencies=['click']\n" + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text(src, encoding="utf-8") + dist = makedist(tmp_path, install_requires=["wheel"]) + with pytest.warns(match="`install_requires` overwritten"): + dist = pyprojecttoml.apply_configuration(dist, pyproject) + assert "wheel" not in dist.install_requires + + def test_optional_dependencies_dont_remove_env_markers(self, tmp_path): + """ + Internally setuptools converts dependencies with markers to "extras". + If ``install_requires`` is given by ``setup.py``, we have to ensure that + applying ``optional-dependencies`` does not overwrite the mandatory + dependencies with markers (see #3204). + """ + # If setuptools replace its internal mechanism that uses `requires.txt` + # this test has to be rewritten to adapt accordingly + extra = "\n[project.optional-dependencies]\nfoo = ['bar>1']\n" + pyproject = self.pyproject(tmp_path, ["dependencies"], extra) + install_req = ['importlib-resources (>=3.0.0) ; python_version < "3.7"'] + dist = makedist(tmp_path, install_requires=install_req) + dist = pyprojecttoml.apply_configuration(dist, pyproject) + assert "foo" in dist.extras_require + egg_info = dist.get_command_obj("egg_info") + write_requirements(egg_info, tmp_path, tmp_path / "requires.txt") + reqs = (tmp_path / "requires.txt").read_text(encoding="utf-8") + assert "importlib-resources" in reqs + assert "bar" in reqs + assert ':python_version < "3.7"' in reqs + + @pytest.mark.parametrize( + ("field", "group"), + [("scripts", "console_scripts"), ("gui-scripts", "gui_scripts")], + ) + @pytest.mark.filterwarnings("error") + def test_scripts_dont_require_dynamic_entry_points(self, tmp_path, field, group): + # Issue 3862 + pyproject = self.pyproject(tmp_path, [field]) + dist = makedist(tmp_path, entry_points={group: ["foobar=foobar:main"]}) + dist = pyprojecttoml.apply_configuration(dist, pyproject) + assert group in dist.entry_points + + +class TestMeta: + def test_example_file_in_sdist(self, setuptools_sdist): + """Meta test to ensure tests can run from sdist""" + with tarfile.open(setuptools_sdist) as tar: + assert any(name.endswith(EXAMPLES_FILE) for name in tar.getnames()) + + +class TestInteropCommandLineParsing: + def test_version(self, tmp_path, monkeypatch, capsys): + # See pypa/setuptools#4047 + # This test can be removed once the CLI interface of setup.py is removed + monkeypatch.chdir(tmp_path) + toml_config = """ + [project] + name = "test" + version = "42.0" + """ + pyproject = Path(tmp_path, "pyproject.toml") + pyproject.write_text(cleandoc(toml_config), encoding="utf-8") + opts = {"script_args": ["--version"]} + dist = pyprojecttoml.apply_configuration(Distribution(opts), pyproject) + dist.parse_command_line() # <-- there should be no exception here. + captured = capsys.readouterr() + assert "42.0" in captured.out + + +class TestStaticConfig: + def test_mark_static_fields(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + toml_config = """ + [project] + name = "test" + version = "42.0" + dependencies = ["hello"] + keywords = ["world"] + classifiers = ["private :: hello world"] + [tool.setuptools] + obsoletes = ["abcd"] + provides = ["abcd"] + platforms = ["abcd"] + """ + pyproject = Path(tmp_path, "pyproject.toml") + pyproject.write_text(cleandoc(toml_config), encoding="utf-8") + dist = pyprojecttoml.apply_configuration(Distribution({}), pyproject) + assert is_static(dist.install_requires) + assert is_static(dist.metadata.keywords) + assert is_static(dist.metadata.classifiers) + assert is_static(dist.metadata.obsoletes) + assert is_static(dist.metadata.provides) + assert is_static(dist.metadata.platforms) + + +# --- Auxiliary Functions --- + + +def core_metadata(dist) -> str: + with io.StringIO() as buffer: + dist.metadata.write_pkg_file(buffer) + pkg_file_txt = buffer.getvalue() + + # Make sure core metadata is valid + Metadata.from_email(pkg_file_txt, validate=True) # can raise exceptions + + skip_prefixes: tuple[str, ...] = () + skip_lines = set() + # ---- DIFF NORMALISATION ---- + # PEP 621 is very particular about author/maintainer metadata conversion, so skip + skip_prefixes += ("Author:", "Author-email:", "Maintainer:", "Maintainer-email:") + # May be redundant with Home-page + skip_prefixes += ("Project-URL: Homepage,", "Home-page:") + # May be missing in original (relying on default) but backfilled in the TOML + skip_prefixes += ("Description-Content-Type:",) + # Remove empty lines + skip_lines.add("") + + result = [] + for line in pkg_file_txt.splitlines(): + if line.startswith(skip_prefixes) or line in skip_lines: + continue + result.append(line + "\n") + + return "".join(result) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/config/test_expand.py b/venv/lib/python3.11/site-packages/setuptools/tests/config/test_expand.py new file mode 100644 index 0000000000000000000000000000000000000000..c5710ec63d7d9d4ed7b709203bb2fc4b512f2093 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/config/test_expand.py @@ -0,0 +1,247 @@ +import os +import sys +from pathlib import Path + +import pytest + +from setuptools._static import is_static +from setuptools.config import expand +from setuptools.discovery import find_package_path + +from distutils.errors import DistutilsOptionError + + +def write_files(files, root_dir): + for file, content in files.items(): + path = root_dir / file + path.parent.mkdir(exist_ok=True, parents=True) + path.write_text(content, encoding="utf-8") + + +def test_glob_relative(tmp_path, monkeypatch): + files = { + "dir1/dir2/dir3/file1.txt", + "dir1/dir2/file2.txt", + "dir1/file3.txt", + "a.ini", + "b.ini", + "dir1/c.ini", + "dir1/dir2/a.ini", + } + + write_files({k: "" for k in files}, tmp_path) + patterns = ["**/*.txt", "[ab].*", "**/[ac].ini"] + monkeypatch.chdir(tmp_path) + assert set(expand.glob_relative(patterns)) == files + # Make sure the same APIs work outside cwd + assert set(expand.glob_relative(patterns, tmp_path)) == files + + +def test_read_files(tmp_path, monkeypatch): + dir_ = tmp_path / "dir_" + (tmp_path / "_dir").mkdir(exist_ok=True) + (tmp_path / "a.txt").touch() + files = {"a.txt": "a", "dir1/b.txt": "b", "dir1/dir2/c.txt": "c"} + write_files(files, dir_) + + secrets = Path(str(dir_) + "secrets") + secrets.mkdir(exist_ok=True) + write_files({"secrets.txt": "secret keys"}, secrets) + + with monkeypatch.context() as m: + m.chdir(dir_) + assert expand.read_files(list(files)) == "a\nb\nc" + + cannot_access_msg = r"Cannot access '.*\.\..a\.txt'" + with pytest.raises(DistutilsOptionError, match=cannot_access_msg): + expand.read_files(["../a.txt"]) + + cannot_access_secrets_msg = r"Cannot access '.*secrets\.txt'" + with pytest.raises(DistutilsOptionError, match=cannot_access_secrets_msg): + expand.read_files(["../dir_secrets/secrets.txt"]) + + # Make sure the same APIs work outside cwd + assert expand.read_files(list(files), dir_) == "a\nb\nc" + with pytest.raises(DistutilsOptionError, match=cannot_access_msg): + expand.read_files(["../a.txt"], dir_) + + +class TestReadAttr: + @pytest.mark.parametrize( + "example", + [ + # No cookie means UTF-8: + b"__version__ = '\xc3\xa9'\nraise SystemExit(1)\n", + # If a cookie is present, honor it: + b"# -*- coding: utf-8 -*-\n__version__ = '\xc3\xa9'\nraise SystemExit(1)\n", + b"# -*- coding: latin1 -*-\n__version__ = '\xe9'\nraise SystemExit(1)\n", + ], + ) + def test_read_attr_encoding_cookie(self, example, tmp_path): + (tmp_path / "mod.py").write_bytes(example) + assert expand.read_attr('mod.__version__', root_dir=tmp_path) == 'é' + + def test_read_attr(self, tmp_path, monkeypatch): + files = { + "pkg/__init__.py": "", + "pkg/sub/__init__.py": "VERSION = '0.1.1'", + "pkg/sub/mod.py": ( + "VALUES = {'a': 0, 'b': {42}, 'c': (0, 1, 1)}\nraise SystemExit(1)" + ), + } + write_files(files, tmp_path) + + with monkeypatch.context() as m: + m.chdir(tmp_path) + # Make sure it can read the attr statically without evaluating the module + version = expand.read_attr('pkg.sub.VERSION') + values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'}) + + assert version == '0.1.1' + assert is_static(values) + + assert values['a'] == 0 + assert values['b'] == {42} + assert is_static(values) + + # Make sure the same APIs work outside cwd + assert expand.read_attr('pkg.sub.VERSION', root_dir=tmp_path) == '0.1.1' + values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'}, tmp_path) + assert values['c'] == (0, 1, 1) + + @pytest.mark.parametrize( + "example", + [ + "VERSION: str\nVERSION = '0.1.1'\nraise SystemExit(1)\n", + "VERSION: str = '0.1.1'\nraise SystemExit(1)\n", + ], + ) + def test_read_annotated_attr(self, tmp_path, example): + files = { + "pkg/__init__.py": "", + "pkg/sub/__init__.py": example, + } + write_files(files, tmp_path) + # Make sure this attribute can be read statically + version = expand.read_attr('pkg.sub.VERSION', root_dir=tmp_path) + assert version == '0.1.1' + assert is_static(version) + + @pytest.mark.parametrize( + "example", + [ + "VERSION = (lambda: '0.1.1')()\n", + "def fn(): return '0.1.1'\nVERSION = fn()\n", + "VERSION: str = (lambda: '0.1.1')()\n", + ], + ) + def test_read_dynamic_attr(self, tmp_path, monkeypatch, example): + files = { + "pkg/__init__.py": "", + "pkg/sub/__init__.py": example, + } + write_files(files, tmp_path) + monkeypatch.chdir(tmp_path) + version = expand.read_attr('pkg.sub.VERSION') + assert version == '0.1.1' + assert not is_static(version) + + def test_import_order(self, tmp_path): + """ + Sometimes the import machinery will import the parent package of a nested + module, which triggers side-effects and might create problems (see issue #3176) + + ``read_attr`` should bypass these limitations by resolving modules statically + (via ast.literal_eval). + """ + files = { + "src/pkg/__init__.py": "from .main import func\nfrom .about import version", + "src/pkg/main.py": "import super_complicated_dep\ndef func(): return 42", + "src/pkg/about.py": "version = '42'", + } + write_files(files, tmp_path) + attr_desc = "pkg.about.version" + package_dir = {"": "src"} + # `import super_complicated_dep` should not run, otherwise the build fails + assert expand.read_attr(attr_desc, package_dir, tmp_path) == "42" + + +@pytest.mark.parametrize( + ("package_dir", "file", "module", "return_value"), + [ + ({"": "src"}, "src/pkg/main.py", "pkg.main", 42), + ({"pkg": "lib"}, "lib/main.py", "pkg.main", 13), + ({}, "single_module.py", "single_module", 70), + ({}, "flat_layout/pkg.py", "flat_layout.pkg", 836), + ], +) +def test_resolve_class(monkeypatch, tmp_path, package_dir, file, module, return_value): + monkeypatch.setattr(sys, "modules", {}) # reproducibility + files = {file: f"class Custom:\n def testing(self): return {return_value}"} + write_files(files, tmp_path) + cls = expand.resolve_class(f"{module}.Custom", package_dir, tmp_path) + assert cls().testing() == return_value + + +@pytest.mark.parametrize( + ("args", "pkgs"), + [ + ({"where": ["."], "namespaces": False}, {"pkg", "other"}), + ({"where": [".", "dir1"], "namespaces": False}, {"pkg", "other", "dir2"}), + ({"namespaces": True}, {"pkg", "other", "dir1", "dir1.dir2"}), + ({}, {"pkg", "other", "dir1", "dir1.dir2"}), # default value for `namespaces` + ], +) +def test_find_packages(tmp_path, args, pkgs): + files = { + "pkg/__init__.py", + "other/__init__.py", + "dir1/dir2/__init__.py", + } + write_files({k: "" for k in files}, tmp_path) + + package_dir = {} + kwargs = {"root_dir": tmp_path, "fill_package_dir": package_dir, **args} + where = kwargs.get("where", ["."]) + assert set(expand.find_packages(**kwargs)) == pkgs + for pkg in pkgs: + pkg_path = find_package_path(pkg, package_dir, tmp_path) + assert os.path.exists(pkg_path) + + # Make sure the same APIs work outside cwd + where = [ + str((tmp_path / p).resolve()).replace(os.sep, "/") # ensure posix-style paths + for p in args.pop("where", ["."]) + ] + + assert set(expand.find_packages(where=where, **args)) == pkgs + + +@pytest.mark.parametrize( + ("files", "where", "expected_package_dir"), + [ + (["pkg1/__init__.py", "pkg1/other.py"], ["."], {}), + (["pkg1/__init__.py", "pkg2/__init__.py"], ["."], {}), + (["src/pkg1/__init__.py", "src/pkg1/other.py"], ["src"], {"": "src"}), + (["src/pkg1/__init__.py", "src/pkg2/__init__.py"], ["src"], {"": "src"}), + ( + ["src1/pkg1/__init__.py", "src2/pkg2/__init__.py"], + ["src1", "src2"], + {"pkg1": "src1/pkg1", "pkg2": "src2/pkg2"}, + ), + ( + ["src/pkg1/__init__.py", "pkg2/__init__.py"], + ["src", "."], + {"pkg1": "src/pkg1"}, + ), + ], +) +def test_fill_package_dir(tmp_path, files, where, expected_package_dir): + write_files({k: "" for k in files}, tmp_path) + pkg_dir = {} + kwargs = {"root_dir": tmp_path, "fill_package_dir": pkg_dir, "namespaces": False} + pkgs = expand.find_packages(where=where, **kwargs) + assert set(pkg_dir.items()) == set(expected_package_dir.items()) + for pkg in pkgs: + pkg_path = find_package_path(pkg, pkg_dir, tmp_path) + assert os.path.exists(pkg_path) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/config/test_pyprojecttoml.py b/venv/lib/python3.11/site-packages/setuptools/tests/config/test_pyprojecttoml.py new file mode 100644 index 0000000000000000000000000000000000000000..db40fcd23da137dfb2f4ddde7ffb722193ee83e2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/config/test_pyprojecttoml.py @@ -0,0 +1,396 @@ +import re +from configparser import ConfigParser +from inspect import cleandoc + +import jaraco.path +import pytest +import tomli_w +from path import Path + +import setuptools # noqa: F401 # force distutils.core to be patched +from setuptools.config.pyprojecttoml import ( + _ToolsTypoInMetadata, + apply_configuration, + expand_configuration, + read_configuration, + validate, +) +from setuptools.dist import Distribution +from setuptools.errors import OptionError + +import distutils.core + +EXAMPLE = """ +[project] +name = "myproj" +keywords = ["some", "key", "words"] +dynamic = ["version", "readme"] +requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +dependencies = [ + 'importlib-metadata>=0.12;python_version<"3.8"', + 'importlib-resources>=1.0;python_version<"3.7"', + 'pathlib2>=2.3.3,<3;python_version < "3.4" and sys.platform != "win32"', +] + +[project.optional-dependencies] +docs = [ + "sphinx>=3", + "sphinx-argparse>=0.2.5", + "sphinx-rtd-theme>=0.4.3", +] +testing = [ + "pytest>=1", + "coverage>=3,<5", +] + +[project.scripts] +exec = "pkg.__main__:exec" + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +package-dir = {"" = "src"} +zip-safe = true +platforms = ["any"] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.cmdclass] +sdist = "pkg.mod.CustomSdist" + +[tool.setuptools.dynamic.version] +attr = "pkg.__version__.VERSION" + +[tool.setuptools.dynamic.readme] +file = ["README.md"] +content-type = "text/markdown" + +[tool.setuptools.package-data] +"*" = ["*.txt"] + +[tool.setuptools.data-files] +"data" = ["_files/*.txt"] + +[tool.distutils.sdist] +formats = "gztar" + +[tool.distutils.bdist_wheel] +universal = true +""" + + +def create_example(path, pkg_root): + files = { + "pyproject.toml": EXAMPLE, + "README.md": "hello world", + "_files": { + "file.txt": "", + }, + } + packages = { + "pkg": { + "__init__.py": "", + "mod.py": "class CustomSdist: pass", + "__version__.py": "VERSION = (3, 10)", + "__main__.py": "def exec(): print('hello')", + }, + } + + assert pkg_root # Meta-test: cannot be empty string. + + if pkg_root == ".": + files = {**files, **packages} + # skip other files: flat-layout will raise error for multi-package dist + else: + # Use this opportunity to ensure namespaces are discovered + files[pkg_root] = {**packages, "other": {"nested": {"__init__.py": ""}}} + + jaraco.path.build(files, prefix=path) + + +def verify_example(config, path, pkg_root): + pyproject = path / "pyproject.toml" + pyproject.write_text(tomli_w.dumps(config), encoding="utf-8") + expanded = expand_configuration(config, path) + expanded_project = expanded["project"] + assert read_configuration(pyproject, expand=True) == expanded + assert expanded_project["version"] == "3.10" + assert expanded_project["readme"]["text"] == "hello world" + assert "packages" in expanded["tool"]["setuptools"] + if pkg_root == ".": + # Auto-discovery will raise error for multi-package dist + assert set(expanded["tool"]["setuptools"]["packages"]) == {"pkg"} + else: + assert set(expanded["tool"]["setuptools"]["packages"]) == { + "pkg", + "other", + "other.nested", + } + assert expanded["tool"]["setuptools"]["include-package-data"] is True + assert "" in expanded["tool"]["setuptools"]["package-data"] + assert "*" not in expanded["tool"]["setuptools"]["package-data"] + assert expanded["tool"]["setuptools"]["data-files"] == [ + ("data", ["_files/file.txt"]) + ] + + +def test_read_configuration(tmp_path): + create_example(tmp_path, "src") + pyproject = tmp_path / "pyproject.toml" + + config = read_configuration(pyproject, expand=False) + assert config["project"].get("version") is None + assert config["project"].get("readme") is None + + verify_example(config, tmp_path, "src") + + +@pytest.mark.parametrize( + ("pkg_root", "opts"), + [ + (".", {}), + ("src", {}), + ("lib", {"packages": {"find": {"where": ["lib"]}}}), + ], +) +def test_discovered_package_dir_with_attr_directive_in_config(tmp_path, pkg_root, opts): + create_example(tmp_path, pkg_root) + + pyproject = tmp_path / "pyproject.toml" + + config = read_configuration(pyproject, expand=False) + assert config["project"].get("version") is None + assert config["project"].get("readme") is None + config["tool"]["setuptools"].pop("packages", None) + config["tool"]["setuptools"].pop("package-dir", None) + + config["tool"]["setuptools"].update(opts) + verify_example(config, tmp_path, pkg_root) + + +ENTRY_POINTS = { + "console_scripts": {"a": "mod.a:func"}, + "gui_scripts": {"b": "mod.b:func"}, + "other": {"c": "mod.c:func [extra]"}, +} + + +class TestEntryPoints: + def write_entry_points(self, tmp_path): + entry_points = ConfigParser() + entry_points.read_dict(ENTRY_POINTS) + with open(tmp_path / "entry-points.txt", "w", encoding="utf-8") as f: + entry_points.write(f) + + def pyproject(self, dynamic=None): + project = {"dynamic": dynamic or ["scripts", "gui-scripts", "entry-points"]} + tool = {"dynamic": {"entry-points": {"file": "entry-points.txt"}}} + return {"project": project, "tool": {"setuptools": tool}} + + def test_all_listed_in_dynamic(self, tmp_path): + self.write_entry_points(tmp_path) + expanded = expand_configuration(self.pyproject(), tmp_path) + expanded_project = expanded["project"] + assert len(expanded_project["scripts"]) == 1 + assert expanded_project["scripts"]["a"] == "mod.a:func" + assert len(expanded_project["gui-scripts"]) == 1 + assert expanded_project["gui-scripts"]["b"] == "mod.b:func" + assert len(expanded_project["entry-points"]) == 1 + assert expanded_project["entry-points"]["other"]["c"] == "mod.c:func [extra]" + + @pytest.mark.parametrize("missing_dynamic", ("scripts", "gui-scripts")) + def test_scripts_not_listed_in_dynamic(self, tmp_path, missing_dynamic): + self.write_entry_points(tmp_path) + dynamic = {"scripts", "gui-scripts", "entry-points"} - {missing_dynamic} + + msg = f"defined outside of `pyproject.toml`:.*{missing_dynamic}" + with pytest.raises(OptionError, match=re.compile(msg, re.S)): + expand_configuration(self.pyproject(dynamic), tmp_path) + + +class TestClassifiers: + def test_dynamic(self, tmp_path): + # Let's create a project example that has dynamic classifiers + # coming from a txt file. + create_example(tmp_path, "src") + classifiers = cleandoc( + """ + Framework :: Flask + Programming Language :: Haskell + """ + ) + (tmp_path / "classifiers.txt").write_text(classifiers, encoding="utf-8") + + pyproject = tmp_path / "pyproject.toml" + config = read_configuration(pyproject, expand=False) + dynamic = config["project"]["dynamic"] + config["project"]["dynamic"] = list({*dynamic, "classifiers"}) + dynamic_config = config["tool"]["setuptools"]["dynamic"] + dynamic_config["classifiers"] = {"file": "classifiers.txt"} + + # When the configuration is expanded, + # each line of the file should be an different classifier. + validate(config, pyproject) + expanded = expand_configuration(config, tmp_path) + + assert set(expanded["project"]["classifiers"]) == { + "Framework :: Flask", + "Programming Language :: Haskell", + } + + def test_dynamic_without_config(self, tmp_path): + config = """ + [project] + name = "myproj" + version = '42' + dynamic = ["classifiers"] + """ + + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text(cleandoc(config), encoding="utf-8") + with pytest.raises(OptionError, match="No configuration .* .classifiers."): + read_configuration(pyproject) + + def test_dynamic_readme_from_setup_script_args(self, tmp_path): + config = """ + [project] + name = "myproj" + version = '42' + dynamic = ["readme"] + """ + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text(cleandoc(config), encoding="utf-8") + dist = Distribution(attrs={"long_description": "42"}) + # No error should occur because of missing `readme` + dist = apply_configuration(dist, pyproject) + assert dist.metadata.long_description == "42" + + def test_dynamic_without_file(self, tmp_path): + config = """ + [project] + name = "myproj" + version = '42' + dynamic = ["classifiers"] + + [tool.setuptools.dynamic] + classifiers = {file = ["classifiers.txt"]} + """ + + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text(cleandoc(config), encoding="utf-8") + with pytest.warns(UserWarning, match="File .*classifiers.txt. cannot be found"): + expanded = read_configuration(pyproject) + assert "classifiers" not in expanded["project"] + + +@pytest.mark.parametrize( + "example", + ( + """ + [project] + name = "myproj" + version = "1.2" + + [my-tool.that-disrespect.pep518] + value = 42 + """, + ), +) +def test_ignore_unrelated_config(tmp_path, example): + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text(cleandoc(example), encoding="utf-8") + + # Make sure no error is raised due to 3rd party configs in pyproject.toml + assert read_configuration(pyproject) is not None + + +@pytest.mark.parametrize( + ("example", "error_msg"), + [ + ( + """ + [project] + name = "myproj" + version = "1.2" + requires = ['pywin32; platform_system=="Windows"' ] + """, + "configuration error: .project. must not contain ..requires.. properties", + ), + ], +) +def test_invalid_example(tmp_path, example, error_msg): + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text(cleandoc(example), encoding="utf-8") + + pattern = re.compile(f"invalid pyproject.toml.*{error_msg}.*", re.M | re.S) + with pytest.raises(ValueError, match=pattern): + read_configuration(pyproject) + + +@pytest.mark.parametrize("config", ("", "[tool.something]\nvalue = 42")) +def test_empty(tmp_path, config): + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text(config, encoding="utf-8") + + # Make sure no error is raised + assert read_configuration(pyproject) == {} + + +@pytest.mark.parametrize("config", ("[project]\nname = 'myproj'\nversion='42'\n",)) +def test_include_package_data_by_default(tmp_path, config): + """Builds with ``pyproject.toml`` should consider ``include-package-data=True`` as + default. + """ + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text(config, encoding="utf-8") + + config = read_configuration(pyproject) + assert config["tool"]["setuptools"]["include-package-data"] is True + + +def test_include_package_data_in_setuppy(tmp_path): + """Builds with ``pyproject.toml`` should consider ``include_package_data`` set in + ``setup.py``. + + See https://github.com/pypa/setuptools/issues/3197#issuecomment-1079023889 + """ + files = { + "pyproject.toml": "[project]\nname = 'myproj'\nversion='42'\n", + "setup.py": "__import__('setuptools').setup(include_package_data=False)", + } + jaraco.path.build(files, prefix=tmp_path) + + with Path(tmp_path): + dist = distutils.core.run_setup("setup.py", {}, stop_after="config") + + assert dist.get_name() == "myproj" + assert dist.get_version() == "42" + assert dist.include_package_data is False + + +def test_warn_tools_typo(tmp_path): + """Test that the common ``tools.setuptools`` typo in ``pyproject.toml`` issues a warning + + See https://github.com/pypa/setuptools/issues/4150 + """ + config = """ + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + + [project] + name = "myproj" + version = '42' + + [tools.setuptools] + packages = ["package"] + """ + + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text(cleandoc(config), encoding="utf-8") + + with pytest.warns(_ToolsTypoInMetadata): + read_configuration(pyproject) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py b/venv/lib/python3.11/site-packages/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py new file mode 100644 index 0000000000000000000000000000000000000000..e42f28ffaaf5270a74fe9f03885b75bb91858030 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py @@ -0,0 +1,109 @@ +from inspect import cleandoc + +import pytest +from jaraco import path + +from setuptools.config.pyprojecttoml import apply_configuration +from setuptools.dist import Distribution +from setuptools.warnings import SetuptoolsWarning + + +def test_dynamic_dependencies(tmp_path): + files = { + "requirements.txt": "six\n # comment\n", + "pyproject.toml": cleandoc( + """ + [project] + name = "myproj" + version = "1.0" + dynamic = ["dependencies"] + + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" + + [tool.setuptools.dynamic.dependencies] + file = ["requirements.txt"] + """ + ), + } + path.build(files, prefix=tmp_path) + dist = Distribution() + dist = apply_configuration(dist, tmp_path / "pyproject.toml") + assert dist.install_requires == ["six"] + + +def test_dynamic_optional_dependencies(tmp_path): + files = { + "requirements-docs.txt": "sphinx\n # comment\n", + "pyproject.toml": cleandoc( + """ + [project] + name = "myproj" + version = "1.0" + dynamic = ["optional-dependencies"] + + [tool.setuptools.dynamic.optional-dependencies.docs] + file = ["requirements-docs.txt"] + + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" + """ + ), + } + path.build(files, prefix=tmp_path) + dist = Distribution() + dist = apply_configuration(dist, tmp_path / "pyproject.toml") + assert dist.extras_require == {"docs": ["sphinx"]} + + +def test_mixed_dynamic_optional_dependencies(tmp_path): + """ + Test that if PEP 621 was loosened to allow mixing of dynamic and static + configurations in the case of fields containing sub-fields (groups), + things would work out. + """ + files = { + "requirements-images.txt": "pillow~=42.0\n # comment\n", + "pyproject.toml": cleandoc( + """ + [project] + name = "myproj" + version = "1.0" + dynamic = ["optional-dependencies"] + + [project.optional-dependencies] + docs = ["sphinx"] + + [tool.setuptools.dynamic.optional-dependencies.images] + file = ["requirements-images.txt"] + """ + ), + } + + path.build(files, prefix=tmp_path) + pyproject = tmp_path / "pyproject.toml" + with pytest.raises(ValueError, match="project.optional-dependencies"): + apply_configuration(Distribution(), pyproject) + + +def test_mixed_extras_require_optional_dependencies(tmp_path): + files = { + "pyproject.toml": cleandoc( + """ + [project] + name = "myproj" + version = "1.0" + optional-dependencies.docs = ["sphinx"] + """ + ), + } + + path.build(files, prefix=tmp_path) + pyproject = tmp_path / "pyproject.toml" + + with pytest.warns(SetuptoolsWarning, match=".extras_require. overwritten"): + dist = Distribution({"extras_require": {"hello": ["world"]}}) + dist = apply_configuration(dist, pyproject) + assert dist.extras_require == {"docs": ["sphinx"]} diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/config/test_setupcfg.py b/venv/lib/python3.11/site-packages/setuptools/tests/config/test_setupcfg.py new file mode 100644 index 0000000000000000000000000000000000000000..61af99044742b46ec99ad1d38f34f576908f3e68 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/config/test_setupcfg.py @@ -0,0 +1,980 @@ +import configparser +import contextlib +import inspect +import re +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest +from packaging.requirements import InvalidRequirement + +from setuptools.config.setupcfg import ConfigHandler, Target, read_configuration +from setuptools.dist import Distribution, _Distribution +from setuptools.warnings import SetuptoolsDeprecationWarning + +from ..textwrap import DALS + +from distutils.errors import DistutilsFileError, DistutilsOptionError + + +class ErrConfigHandler(ConfigHandler[Target]): + """Erroneous handler. Fails to implement required methods.""" + + section_prefix = "**err**" + + +def make_package_dir(name, base_dir, ns=False): + dir_package = base_dir + for dir_name in name.split('/'): + dir_package = dir_package.mkdir(dir_name) + init_file = None + if not ns: + init_file = dir_package.join('__init__.py') + init_file.write('') + return dir_package, init_file + + +def fake_env( + tmpdir, setup_cfg, setup_py=None, encoding='ascii', package_path='fake_package' +): + if setup_py is None: + setup_py = 'from setuptools import setup\nsetup()\n' + + tmpdir.join('setup.py').write(setup_py) + config = tmpdir.join('setup.cfg') + config.write(setup_cfg.encode(encoding), mode='wb') + + package_dir, init_file = make_package_dir(package_path, tmpdir) + + init_file.write( + 'VERSION = (1, 2, 3)\n' + '\n' + 'VERSION_MAJOR = 1' + '\n' + 'def get_version():\n' + ' return [3, 4, 5, "dev"]\n' + '\n' + ) + + return package_dir, config + + +@contextlib.contextmanager +def get_dist(tmpdir, kwargs_initial=None, parse=True): + kwargs_initial = kwargs_initial or {} + + with tmpdir.as_cwd(): + dist = Distribution(kwargs_initial) + dist.script_name = 'setup.py' + parse and dist.parse_config_files() + + yield dist + + +def test_parsers_implemented(): + with pytest.raises(NotImplementedError): + handler = ErrConfigHandler(None, {}, False, Mock()) + handler.parsers + + +class TestConfigurationReader: + def test_basic(self, tmpdir): + _, config = fake_env( + tmpdir, + '[metadata]\n' + 'version = 10.1.1\n' + 'keywords = one, two\n' + '\n' + '[options]\n' + 'scripts = bin/a.py, bin/b.py\n', + ) + config_dict = read_configuration(str(config)) + assert config_dict['metadata']['version'] == '10.1.1' + assert config_dict['metadata']['keywords'] == ['one', 'two'] + assert config_dict['options']['scripts'] == ['bin/a.py', 'bin/b.py'] + + def test_no_config(self, tmpdir): + with pytest.raises(DistutilsFileError): + read_configuration(str(tmpdir.join('setup.cfg'))) + + def test_ignore_errors(self, tmpdir): + _, config = fake_env( + tmpdir, + '[metadata]\nversion = attr: none.VERSION\nkeywords = one, two\n', + ) + with pytest.raises(ImportError): + read_configuration(str(config)) + + config_dict = read_configuration(str(config), ignore_option_errors=True) + + assert config_dict['metadata']['keywords'] == ['one', 'two'] + assert 'version' not in config_dict['metadata'] + + config.remove() + + +class TestMetadata: + def test_basic(self, tmpdir): + fake_env( + tmpdir, + '[metadata]\n' + 'version = 10.1.1\n' + 'description = Some description\n' + 'long_description_content_type = text/something\n' + 'long_description = file: README\n' + 'name = fake_name\n' + 'keywords = one, two\n' + 'provides = package, package.sub\n' + 'license = otherlic\n' + 'download_url = http://test.test.com/test/\n' + 'maintainer_email = test@test.com\n', + ) + + tmpdir.join('README').write('readme contents\nline2') + + meta_initial = { + # This will be used so `otherlic` won't replace it. + 'license': 'BSD 3-Clause License', + } + + with get_dist(tmpdir, meta_initial) as dist: + metadata = dist.metadata + + assert metadata.version == '10.1.1' + assert metadata.description == 'Some description' + assert metadata.long_description_content_type == 'text/something' + assert metadata.long_description == 'readme contents\nline2' + assert metadata.provides == ['package', 'package.sub'] + assert metadata.license == 'BSD 3-Clause License' + assert metadata.name == 'fake_name' + assert metadata.keywords == ['one', 'two'] + assert metadata.download_url == 'http://test.test.com/test/' + assert metadata.maintainer_email == 'test@test.com' + + def test_license_cfg(self, tmpdir): + fake_env( + tmpdir, + DALS( + """ + [metadata] + name=foo + version=0.0.1 + license=Apache 2.0 + """ + ), + ) + + with get_dist(tmpdir) as dist: + metadata = dist.metadata + + assert metadata.name == "foo" + assert metadata.version == "0.0.1" + assert metadata.license == "Apache 2.0" + + def test_file_mixed(self, tmpdir): + fake_env( + tmpdir, + '[metadata]\nlong_description = file: README.rst, CHANGES.rst\n\n', + ) + + tmpdir.join('README.rst').write('readme contents\nline2') + tmpdir.join('CHANGES.rst').write('changelog contents\nand stuff') + + with get_dist(tmpdir) as dist: + assert dist.metadata.long_description == ( + 'readme contents\nline2\nchangelog contents\nand stuff' + ) + + def test_file_sandboxed(self, tmpdir): + tmpdir.ensure("README") + project = tmpdir.join('depth1', 'depth2') + project.ensure(dir=True) + fake_env(project, '[metadata]\nlong_description = file: ../../README\n') + + with get_dist(project, parse=False) as dist: + with pytest.raises(DistutilsOptionError): + dist.parse_config_files() # file: out of sandbox + + def test_aliases(self, tmpdir): + fake_env( + tmpdir, + '[metadata]\n' + 'author_email = test@test.com\n' + 'home_page = http://test.test.com/test/\n' + 'summary = Short summary\n' + 'platform = a, b\n' + 'classifier =\n' + ' Framework :: Django\n' + ' Programming Language :: Python :: 3.5\n', + ) + + with get_dist(tmpdir) as dist: + metadata = dist.metadata + assert metadata.author_email == 'test@test.com' + assert metadata.url == 'http://test.test.com/test/' + assert metadata.description == 'Short summary' + assert metadata.platforms == ['a', 'b'] + assert metadata.classifiers == [ + 'Framework :: Django', + 'Programming Language :: Python :: 3.5', + ] + + def test_multiline(self, tmpdir): + fake_env( + tmpdir, + '[metadata]\n' + 'name = fake_name\n' + 'keywords =\n' + ' one\n' + ' two\n' + 'classifiers =\n' + ' Framework :: Django\n' + ' Programming Language :: Python :: 3.5\n', + ) + with get_dist(tmpdir) as dist: + metadata = dist.metadata + assert metadata.keywords == ['one', 'two'] + assert metadata.classifiers == [ + 'Framework :: Django', + 'Programming Language :: Python :: 3.5', + ] + + def test_dict(self, tmpdir): + fake_env( + tmpdir, + '[metadata]\n' + 'project_urls =\n' + ' Link One = https://example.com/one/\n' + ' Link Two = https://example.com/two/\n', + ) + with get_dist(tmpdir) as dist: + metadata = dist.metadata + assert metadata.project_urls == { + 'Link One': 'https://example.com/one/', + 'Link Two': 'https://example.com/two/', + } + + def test_version(self, tmpdir): + package_dir, config = fake_env( + tmpdir, '[metadata]\nversion = attr: fake_package.VERSION\n' + ) + + sub_a = package_dir.mkdir('subpkg_a') + sub_a.join('__init__.py').write('') + sub_a.join('mod.py').write('VERSION = (2016, 11, 26)') + + sub_b = package_dir.mkdir('subpkg_b') + sub_b.join('__init__.py').write('') + sub_b.join('mod.py').write( + 'import third_party_module\nVERSION = (2016, 11, 26)' + ) + + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + + config.write('[metadata]\nversion = attr: fake_package.get_version\n') + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '3.4.5.dev' + + config.write('[metadata]\nversion = attr: fake_package.VERSION_MAJOR\n') + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1' + + config.write('[metadata]\nversion = attr: fake_package.subpkg_a.mod.VERSION\n') + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '2016.11.26' + + config.write('[metadata]\nversion = attr: fake_package.subpkg_b.mod.VERSION\n') + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '2016.11.26' + + def test_version_file(self, tmpdir): + fake_env(tmpdir, '[metadata]\nversion = file: fake_package/version.txt\n') + tmpdir.join('fake_package', 'version.txt').write('1.2.3\n') + + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + + tmpdir.join('fake_package', 'version.txt').write('1.2.3\n4.5.6\n') + with pytest.raises(DistutilsOptionError): + with get_dist(tmpdir) as dist: + dist.metadata.version + + def test_version_with_package_dir_simple(self, tmpdir): + fake_env( + tmpdir, + '[metadata]\n' + 'version = attr: fake_package_simple.VERSION\n' + '[options]\n' + 'package_dir =\n' + ' = src\n', + package_path='src/fake_package_simple', + ) + + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + + def test_version_with_package_dir_rename(self, tmpdir): + fake_env( + tmpdir, + '[metadata]\n' + 'version = attr: fake_package_rename.VERSION\n' + '[options]\n' + 'package_dir =\n' + ' fake_package_rename = fake_dir\n', + package_path='fake_dir', + ) + + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + + def test_version_with_package_dir_complex(self, tmpdir): + fake_env( + tmpdir, + '[metadata]\n' + 'version = attr: fake_package_complex.VERSION\n' + '[options]\n' + 'package_dir =\n' + ' fake_package_complex = src/fake_dir\n', + package_path='src/fake_dir', + ) + + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + + def test_unknown_meta_item(self, tmpdir): + fake_env(tmpdir, '[metadata]\nname = fake_name\nunknown = some\n') + with get_dist(tmpdir, parse=False) as dist: + dist.parse_config_files() # Skip unknown. + + def test_usupported_section(self, tmpdir): + fake_env(tmpdir, '[metadata.some]\nkey = val\n') + with get_dist(tmpdir, parse=False) as dist: + with pytest.raises(DistutilsOptionError): + dist.parse_config_files() + + def test_classifiers(self, tmpdir): + expected = set([ + 'Framework :: Django', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + ]) + + # From file. + _, config = fake_env(tmpdir, '[metadata]\nclassifiers = file: classifiers\n') + + tmpdir.join('classifiers').write( + 'Framework :: Django\n' + 'Programming Language :: Python :: 3\n' + 'Programming Language :: Python :: 3.5\n' + ) + + with get_dist(tmpdir) as dist: + assert set(dist.metadata.classifiers) == expected + + # From list notation + config.write( + '[metadata]\n' + 'classifiers =\n' + ' Framework :: Django\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3.5\n' + ) + with get_dist(tmpdir) as dist: + assert set(dist.metadata.classifiers) == expected + + def test_interpolation(self, tmpdir): + fake_env(tmpdir, '[metadata]\ndescription = %(message)s\n') + with pytest.raises(configparser.InterpolationMissingOptionError): + with get_dist(tmpdir): + pass + + def test_non_ascii_1(self, tmpdir): + fake_env(tmpdir, '[metadata]\ndescription = éàïôñ\n', encoding='utf-8') + with get_dist(tmpdir): + pass + + def test_non_ascii_3(self, tmpdir): + fake_env(tmpdir, '\n# -*- coding: invalid\n') + with get_dist(tmpdir): + pass + + def test_non_ascii_4(self, tmpdir): + fake_env( + tmpdir, + '# -*- coding: utf-8\n[metadata]\ndescription = éàïôñ\n', + encoding='utf-8', + ) + with get_dist(tmpdir) as dist: + assert dist.metadata.description == 'éàïôñ' + + def test_not_utf8(self, tmpdir): + """ + Config files encoded not in UTF-8 will fail + """ + fake_env( + tmpdir, + '# vim: set fileencoding=iso-8859-15 :\n[metadata]\ndescription = éàïôñ\n', + encoding='iso-8859-15', + ) + with pytest.raises(UnicodeDecodeError): + with get_dist(tmpdir): + pass + + @pytest.mark.parametrize( + ("error_msg", "config", "invalid"), + [ + ( + "Invalid dash-separated key 'author-email' in 'metadata' (setup.cfg)", + DALS( + """ + [metadata] + author-email = test@test.com + maintainer_email = foo@foo.com + """ + ), + {"author-email": "test@test.com"}, + ), + ( + "Invalid uppercase key 'Name' in 'metadata' (setup.cfg)", + DALS( + """ + [metadata] + Name = foo + description = Some description + """ + ), + {"Name": "foo"}, + ), + ], + ) + def test_invalid_options_previously_deprecated( + self, tmpdir, error_msg, config, invalid + ): + # This test and related methods can be removed when no longer needed. + # Deprecation postponed due to push-back from the community in + # https://github.com/pypa/setuptools/issues/4910 + fake_env(tmpdir, config) + with pytest.warns(SetuptoolsDeprecationWarning, match=re.escape(error_msg)): + dist = get_dist(tmpdir).__enter__() + + tmpdir.join('setup.cfg').remove() + + for field, value in invalid.items(): + attr = field.replace("-", "_").lower() + assert getattr(dist.metadata, attr) == value + + +class TestOptions: + def test_basic(self, tmpdir): + fake_env( + tmpdir, + '[options]\n' + 'zip_safe = True\n' + 'include_package_data = yes\n' + 'package_dir = b=c, =src\n' + 'packages = pack_a, pack_b.subpack\n' + 'namespace_packages = pack1, pack2\n' + 'scripts = bin/one.py, bin/two.py\n' + 'eager_resources = bin/one.py, bin/two.py\n' + 'install_requires = docutils>=0.3; pack ==1.1, ==1.3; hey\n' + 'setup_requires = docutils>=0.3; spack ==1.1, ==1.3; there\n' + 'dependency_links = http://some.com/here/1, ' + 'http://some.com/there/2\n' + 'python_requires = >=1.0, !=2.8\n' + 'py_modules = module1, module2\n', + ) + deprec = pytest.warns(SetuptoolsDeprecationWarning, match="namespace_packages") + with deprec, get_dist(tmpdir) as dist: + assert dist.zip_safe + assert dist.include_package_data + assert dist.package_dir == {'': 'src', 'b': 'c'} + assert dist.packages == ['pack_a', 'pack_b.subpack'] + assert dist.namespace_packages == ['pack1', 'pack2'] + assert dist.scripts == ['bin/one.py', 'bin/two.py'] + assert dist.dependency_links == ([ + 'http://some.com/here/1', + 'http://some.com/there/2', + ]) + assert dist.install_requires == ([ + 'docutils>=0.3', + 'pack==1.1,==1.3', + 'hey', + ]) + assert dist.setup_requires == ([ + 'docutils>=0.3', + 'spack ==1.1, ==1.3', + 'there', + ]) + assert dist.python_requires == '>=1.0, !=2.8' + assert dist.py_modules == ['module1', 'module2'] + + def test_multiline(self, tmpdir): + fake_env( + tmpdir, + '[options]\n' + 'package_dir = \n' + ' b=c\n' + ' =src\n' + 'packages = \n' + ' pack_a\n' + ' pack_b.subpack\n' + 'namespace_packages = \n' + ' pack1\n' + ' pack2\n' + 'scripts = \n' + ' bin/one.py\n' + ' bin/two.py\n' + 'eager_resources = \n' + ' bin/one.py\n' + ' bin/two.py\n' + 'install_requires = \n' + ' docutils>=0.3\n' + ' pack ==1.1, ==1.3\n' + ' hey\n' + 'setup_requires = \n' + ' docutils>=0.3\n' + ' spack ==1.1, ==1.3\n' + ' there\n' + 'dependency_links = \n' + ' http://some.com/here/1\n' + ' http://some.com/there/2\n', + ) + deprec = pytest.warns(SetuptoolsDeprecationWarning, match="namespace_packages") + with deprec, get_dist(tmpdir) as dist: + assert dist.package_dir == {'': 'src', 'b': 'c'} + assert dist.packages == ['pack_a', 'pack_b.subpack'] + assert dist.namespace_packages == ['pack1', 'pack2'] + assert dist.scripts == ['bin/one.py', 'bin/two.py'] + assert dist.dependency_links == ([ + 'http://some.com/here/1', + 'http://some.com/there/2', + ]) + assert dist.install_requires == ([ + 'docutils>=0.3', + 'pack==1.1,==1.3', + 'hey', + ]) + assert dist.setup_requires == ([ + 'docutils>=0.3', + 'spack ==1.1, ==1.3', + 'there', + ]) + + def test_package_dir_fail(self, tmpdir): + fake_env(tmpdir, '[options]\npackage_dir = a b\n') + with get_dist(tmpdir, parse=False) as dist: + with pytest.raises(DistutilsOptionError): + dist.parse_config_files() + + def test_package_data(self, tmpdir): + fake_env( + tmpdir, + '[options.package_data]\n' + '* = *.txt, *.rst\n' + 'hello = *.msg\n' + '\n' + '[options.exclude_package_data]\n' + '* = fake1.txt, fake2.txt\n' + 'hello = *.dat\n', + ) + + with get_dist(tmpdir) as dist: + assert dist.package_data == { + '': ['*.txt', '*.rst'], + 'hello': ['*.msg'], + } + assert dist.exclude_package_data == { + '': ['fake1.txt', 'fake2.txt'], + 'hello': ['*.dat'], + } + + def test_packages(self, tmpdir): + fake_env(tmpdir, '[options]\npackages = find:\n') + + with get_dist(tmpdir) as dist: + assert dist.packages == ['fake_package'] + + def test_find_directive(self, tmpdir): + dir_package, config = fake_env(tmpdir, '[options]\npackages = find:\n') + + make_package_dir('sub_one', dir_package) + make_package_dir('sub_two', dir_package) + + with get_dist(tmpdir) as dist: + assert set(dist.packages) == set([ + 'fake_package', + 'fake_package.sub_two', + 'fake_package.sub_one', + ]) + + config.write( + '[options]\n' + 'packages = find:\n' + '\n' + '[options.packages.find]\n' + 'where = .\n' + 'include =\n' + ' fake_package.sub_one\n' + ' two\n' + ) + with get_dist(tmpdir) as dist: + assert dist.packages == ['fake_package.sub_one'] + + config.write( + '[options]\n' + 'packages = find:\n' + '\n' + '[options.packages.find]\n' + 'exclude =\n' + ' fake_package.sub_one\n' + ) + with get_dist(tmpdir) as dist: + assert set(dist.packages) == set(['fake_package', 'fake_package.sub_two']) + + def test_find_namespace_directive(self, tmpdir): + dir_package, config = fake_env( + tmpdir, '[options]\npackages = find_namespace:\n' + ) + + make_package_dir('sub_one', dir_package) + make_package_dir('sub_two', dir_package, ns=True) + + with get_dist(tmpdir) as dist: + assert set(dist.packages) == { + 'fake_package', + 'fake_package.sub_two', + 'fake_package.sub_one', + } + + config.write( + '[options]\n' + 'packages = find_namespace:\n' + '\n' + '[options.packages.find]\n' + 'where = .\n' + 'include =\n' + ' fake_package.sub_one\n' + ' two\n' + ) + with get_dist(tmpdir) as dist: + assert dist.packages == ['fake_package.sub_one'] + + config.write( + '[options]\n' + 'packages = find_namespace:\n' + '\n' + '[options.packages.find]\n' + 'exclude =\n' + ' fake_package.sub_one\n' + ) + with get_dist(tmpdir) as dist: + assert set(dist.packages) == {'fake_package', 'fake_package.sub_two'} + + def test_extras_require(self, tmpdir): + fake_env( + tmpdir, + '[options.extras_require]\n' + 'pdf = ReportLab>=1.2; RXP\n' + 'rest = \n' + ' docutils>=0.3\n' + ' pack ==1.1, ==1.3\n', + ) + + with get_dist(tmpdir) as dist: + assert dist.extras_require == { + 'pdf': ['ReportLab>=1.2', 'RXP'], + 'rest': ['docutils>=0.3', 'pack==1.1,==1.3'], + } + assert set(dist.metadata.provides_extras) == {'pdf', 'rest'} + + @pytest.mark.parametrize( + "config", + [ + "[options.extras_require]\nfoo = bar;python_version<'3'", + "[options.extras_require]\nfoo = bar;os_name=='linux'", + "[options.extras_require]\nfoo = bar;python_version<'3'\n", + "[options.extras_require]\nfoo = bar;os_name=='linux'\n", + "[options]\ninstall_requires = bar;python_version<'3'", + "[options]\ninstall_requires = bar;os_name=='linux'", + "[options]\ninstall_requires = bar;python_version<'3'\n", + "[options]\ninstall_requires = bar;os_name=='linux'\n", + ], + ) + def test_raises_accidental_env_marker_misconfig(self, config, tmpdir): + fake_env(tmpdir, config) + match = ( + r"One of the parsed requirements in `(install_requires|extras_require.+)` " + "looks like a valid environment marker.*" + ) + with pytest.raises(InvalidRequirement, match=match): + with get_dist(tmpdir) as _: + pass + + @pytest.mark.parametrize( + "config", + [ + "[options.extras_require]\nfoo = bar;python_version<3", + "[options.extras_require]\nfoo = bar;python_version<3\n", + "[options]\ninstall_requires = bar;python_version<3", + "[options]\ninstall_requires = bar;python_version<3\n", + ], + ) + def test_warn_accidental_env_marker_misconfig(self, config, tmpdir): + fake_env(tmpdir, config) + match = ( + r"One of the parsed requirements in `(install_requires|extras_require.+)` " + "looks like a valid environment marker.*" + ) + with pytest.warns(SetuptoolsDeprecationWarning, match=match): + with get_dist(tmpdir) as _: + pass + + @pytest.mark.parametrize( + "config", + [ + "[options.extras_require]\nfoo =\n bar;python_version<'3'", + "[options.extras_require]\nfoo = bar;baz\nboo = xxx;yyy", + "[options.extras_require]\nfoo =\n bar;python_version<'3'\n", + "[options.extras_require]\nfoo = bar;baz\nboo = xxx;yyy\n", + "[options.extras_require]\nfoo =\n bar\n python_version<3\n", + "[options]\ninstall_requires =\n bar;python_version<'3'", + "[options]\ninstall_requires = bar;baz\nboo = xxx;yyy", + "[options]\ninstall_requires =\n bar;python_version<'3'\n", + "[options]\ninstall_requires = bar;baz\nboo = xxx;yyy\n", + "[options]\ninstall_requires =\n bar\n python_version<3\n", + ], + ) + @pytest.mark.filterwarnings("error::setuptools.SetuptoolsDeprecationWarning") + def test_nowarn_accidental_env_marker_misconfig(self, config, tmpdir, recwarn): + fake_env(tmpdir, config) + num_warnings = len(recwarn) + with get_dist(tmpdir) as _: + pass + # The examples are valid, no warnings shown + assert len(recwarn) == num_warnings + + def test_dash_preserved_extras_require(self, tmpdir): + fake_env(tmpdir, '[options.extras_require]\nfoo-a = foo\nfoo_b = test\n') + + with get_dist(tmpdir) as dist: + assert dist.extras_require == {'foo-a': ['foo'], 'foo_b': ['test']} + + def test_entry_points(self, tmpdir): + _, config = fake_env( + tmpdir, + '[options.entry_points]\n' + 'group1 = point1 = pack.module:func, ' + '.point2 = pack.module2:func_rest [rest]\n' + 'group2 = point3 = pack.module:func2\n', + ) + + with get_dist(tmpdir) as dist: + assert dist.entry_points == { + 'group1': [ + 'point1 = pack.module:func', + '.point2 = pack.module2:func_rest [rest]', + ], + 'group2': ['point3 = pack.module:func2'], + } + + expected = ( + '[blogtool.parsers]\n' + '.rst = some.nested.module:SomeClass.some_classmethod[reST]\n' + ) + + tmpdir.join('entry_points').write(expected) + + # From file. + config.write('[options]\nentry_points = file: entry_points\n') + + with get_dist(tmpdir) as dist: + assert dist.entry_points == expected + + def test_case_sensitive_entry_points(self, tmpdir): + fake_env( + tmpdir, + '[options.entry_points]\n' + 'GROUP1 = point1 = pack.module:func, ' + '.point2 = pack.module2:func_rest [rest]\n' + 'group2 = point3 = pack.module:func2\n', + ) + + with get_dist(tmpdir) as dist: + assert dist.entry_points == { + 'GROUP1': [ + 'point1 = pack.module:func', + '.point2 = pack.module2:func_rest [rest]', + ], + 'group2': ['point3 = pack.module:func2'], + } + + def test_data_files(self, tmpdir): + fake_env( + tmpdir, + '[options.data_files]\n' + 'cfg =\n' + ' a/b.conf\n' + ' c/d.conf\n' + 'data = e/f.dat, g/h.dat\n', + ) + + with get_dist(tmpdir) as dist: + expected = [ + ('cfg', ['a/b.conf', 'c/d.conf']), + ('data', ['e/f.dat', 'g/h.dat']), + ] + assert sorted(dist.data_files) == sorted(expected) + + def test_data_files_globby(self, tmpdir): + fake_env( + tmpdir, + '[options.data_files]\n' + 'cfg =\n' + ' a/b.conf\n' + ' c/d.conf\n' + 'data = *.dat\n' + 'icons = \n' + ' *.ico\n' + 'audio = \n' + ' *.wav\n' + ' sounds.db\n', + ) + + # Create dummy files for glob()'s sake: + tmpdir.join('a.dat').write('') + tmpdir.join('b.dat').write('') + tmpdir.join('c.dat').write('') + tmpdir.join('a.ico').write('') + tmpdir.join('b.ico').write('') + tmpdir.join('c.ico').write('') + tmpdir.join('beep.wav').write('') + tmpdir.join('boop.wav').write('') + tmpdir.join('sounds.db').write('') + + with get_dist(tmpdir) as dist: + expected = [ + ('cfg', ['a/b.conf', 'c/d.conf']), + ('data', ['a.dat', 'b.dat', 'c.dat']), + ('icons', ['a.ico', 'b.ico', 'c.ico']), + ('audio', ['beep.wav', 'boop.wav', 'sounds.db']), + ] + assert sorted(dist.data_files) == sorted(expected) + + def test_python_requires_simple(self, tmpdir): + fake_env( + tmpdir, + DALS( + """ + [options] + python_requires=>=2.7 + """ + ), + ) + with get_dist(tmpdir) as dist: + dist.parse_config_files() + + def test_python_requires_compound(self, tmpdir): + fake_env( + tmpdir, + DALS( + """ + [options] + python_requires=>=2.7,!=3.0.* + """ + ), + ) + with get_dist(tmpdir) as dist: + dist.parse_config_files() + + def test_python_requires_invalid(self, tmpdir): + fake_env( + tmpdir, + DALS( + """ + [options] + python_requires=invalid + """ + ), + ) + with pytest.raises(Exception): + with get_dist(tmpdir) as dist: + dist.parse_config_files() + + def test_cmdclass(self, tmpdir): + module_path = Path(tmpdir, "src/custom_build.py") # auto discovery for src + module_path.parent.mkdir(parents=True, exist_ok=True) + module_path.write_text( + "from distutils.core import Command\nclass CustomCmd(Command): pass\n", + encoding="utf-8", + ) + + setup_cfg = """ + [options] + cmdclass = + customcmd = custom_build.CustomCmd + """ + fake_env(tmpdir, inspect.cleandoc(setup_cfg)) + + with get_dist(tmpdir) as dist: + cmdclass = dist.cmdclass['customcmd'] + assert cmdclass.__name__ == "CustomCmd" + assert cmdclass.__module__ == "custom_build" + assert module_path.samefile(inspect.getfile(cmdclass)) + + def test_requirements_file(self, tmpdir): + fake_env( + tmpdir, + DALS( + """ + [options] + install_requires = file:requirements.txt + [options.extras_require] + colors = file:requirements-extra.txt + """ + ), + ) + + tmpdir.join('requirements.txt').write('\ndocutils>=0.3\n\n') + tmpdir.join('requirements-extra.txt').write('colorama') + + with get_dist(tmpdir) as dist: + assert dist.install_requires == ['docutils>=0.3'] + assert dist.extras_require == {'colors': ['colorama']} + + +saved_dist_init = _Distribution.__init__ + + +class TestExternalSetters: + # During creation of the setuptools Distribution() object, we call + # the init of the parent distutils Distribution object via + # _Distribution.__init__ (). + # + # It's possible distutils calls out to various keyword + # implementations (i.e. distutils.setup_keywords entry points) + # that may set a range of variables. + # + # This wraps distutil's Distribution.__init__ and simulates + # pbr or something else setting these values. + def _fake_distribution_init(self, dist, attrs): + saved_dist_init(dist, attrs) + # see self._DISTUTILS_UNSUPPORTED_METADATA + dist.metadata.long_description_content_type = 'text/something' + # Test overwrite setup() args + dist.metadata.project_urls = { + 'Link One': 'https://example.com/one/', + 'Link Two': 'https://example.com/two/', + } + + @patch.object(_Distribution, '__init__', autospec=True) + def test_external_setters(self, mock_parent_init, tmpdir): + mock_parent_init.side_effect = self._fake_distribution_init + + dist = Distribution(attrs={'project_urls': {'will_be': 'ignored'}}) + + assert dist.metadata.long_description_content_type == 'text/something' + assert dist.metadata.project_urls == { + 'Link One': 'https://example.com/one/', + 'Link Two': 'https://example.com/two/', + } diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/contexts.py b/venv/lib/python3.11/site-packages/setuptools/tests/contexts.py new file mode 100644 index 0000000000000000000000000000000000000000..97cceea0e7060c6e3568956e1ba20893301da387 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/contexts.py @@ -0,0 +1,145 @@ +import contextlib +import io +import os +import shutil +import site +import sys +import tempfile + +from filelock import FileLock + + +@contextlib.contextmanager +def tempdir(cd=lambda dir: None, **kwargs): + temp_dir = tempfile.mkdtemp(**kwargs) + orig_dir = os.getcwd() + try: + cd(temp_dir) + yield temp_dir + finally: + cd(orig_dir) + shutil.rmtree(temp_dir) + + +@contextlib.contextmanager +def environment(**replacements): + """ + In a context, patch the environment with replacements. Pass None values + to clear the values. + """ + saved = dict((key, os.environ[key]) for key in replacements if key in os.environ) + + # remove values that are null + remove = (key for (key, value) in replacements.items() if value is None) + for key in list(remove): + os.environ.pop(key, None) + replacements.pop(key) + + os.environ.update(replacements) + + try: + yield saved + finally: + for key in replacements: + os.environ.pop(key, None) + os.environ.update(saved) + + +@contextlib.contextmanager +def quiet(): + """ + Redirect stdout/stderr to StringIO objects to prevent console output from + distutils commands. + """ + + old_stdout = sys.stdout + old_stderr = sys.stderr + new_stdout = sys.stdout = io.StringIO() + new_stderr = sys.stderr = io.StringIO() + try: + yield new_stdout, new_stderr + finally: + new_stdout.seek(0) + new_stderr.seek(0) + sys.stdout = old_stdout + sys.stderr = old_stderr + + +@contextlib.contextmanager +def save_user_site_setting(): + saved = site.ENABLE_USER_SITE + try: + yield saved + finally: + site.ENABLE_USER_SITE = saved + + +@contextlib.contextmanager +def save_pkg_resources_state(): + import pkg_resources + + pr_state = pkg_resources.__getstate__() + # also save sys.path + sys_path = sys.path[:] + try: + yield pr_state, sys_path + finally: + sys.path[:] = sys_path + pkg_resources.__setstate__(pr_state) + + +@contextlib.contextmanager +def suppress_exceptions(*excs): + try: + yield + except excs: + pass + + +def multiproc(request): + """ + Return True if running under xdist and multiple + workers are used. + """ + try: + worker_id = request.getfixturevalue('worker_id') + except Exception: + return False + return worker_id != 'master' + + +@contextlib.contextmanager +def session_locked_tmp_dir(request, tmp_path_factory, name): + """Uses a file lock to guarantee only one worker can access a temp dir""" + # get the temp directory shared by all workers + base = tmp_path_factory.getbasetemp() + shared_dir = base.parent if multiproc(request) else base + + locked_dir = shared_dir / name + with FileLock(locked_dir.with_suffix(".lock")): + # ^-- prevent multiple workers to access the directory at once + locked_dir.mkdir(exist_ok=True, parents=True) + yield locked_dir + + +@contextlib.contextmanager +def save_paths(): + """Make sure ``sys.path``, ``sys.meta_path`` and ``sys.path_hooks`` are preserved""" + prev = sys.path[:], sys.meta_path[:], sys.path_hooks[:] + + try: + yield + finally: + sys.path, sys.meta_path, sys.path_hooks = prev + + +@contextlib.contextmanager +def save_sys_modules(): + """Make sure initial ``sys.modules`` is preserved""" + prev_modules = sys.modules + + try: + sys.modules = sys.modules.copy() + yield + finally: + sys.modules = prev_modules diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/environment.py b/venv/lib/python3.11/site-packages/setuptools/tests/environment.py new file mode 100644 index 0000000000000000000000000000000000000000..ed5499ef7d73762d033a4877bfe586d6c0b82235 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/environment.py @@ -0,0 +1,95 @@ +import os +import subprocess +import sys +import unicodedata +from subprocess import PIPE as _PIPE, Popen as _Popen + +import jaraco.envs + + +class VirtualEnv(jaraco.envs.VirtualEnv): + name = '.env' + # Some version of PyPy will import distutils on startup, implicitly + # importing setuptools, and thus leading to BackendInvalid errors + # when upgrading Setuptools. Bypass this behavior by avoiding the + # early availability and need to upgrade. + create_opts = ['--no-setuptools'] + + def run(self, cmd, *args, **kwargs): + cmd = [self.exe(cmd[0])] + cmd[1:] + kwargs = {"cwd": self.root, "encoding": "utf-8", **kwargs} # Allow overriding + # In some environments (eg. downstream distro packaging), where: + # - tox isn't used to run tests and + # - PYTHONPATH is set to point to a specific setuptools codebase and + # - no custom env is explicitly set by a test + # PYTHONPATH will leak into the spawned processes. + # In that case tests look for module in the wrong place (on PYTHONPATH). + # Unless the test sets its own special env, pass a copy of the existing + # environment with removed PYTHONPATH to the subprocesses. + if "env" not in kwargs: + env = dict(os.environ) + if "PYTHONPATH" in env: + del env["PYTHONPATH"] + kwargs["env"] = env + return subprocess.check_output(cmd, *args, **kwargs) + + +def _which_dirs(cmd): + result = set() + for path in os.environ.get('PATH', '').split(os.pathsep): + filename = os.path.join(path, cmd) + if os.access(filename, os.X_OK): + result.add(path) + return result + + +def run_setup_py(cmd, pypath=None, path=None, data_stream=0, env=None): + """ + Execution command for tests, separate from those used by the + code directly to prevent accidental behavior issues + """ + if env is None: + env = dict() + for envname in os.environ: + env[envname] = os.environ[envname] + + # override the python path if needed + if pypath is not None: + env["PYTHONPATH"] = pypath + + # override the execution path if needed + if path is not None: + env["PATH"] = path + if not env.get("PATH", ""): + env["PATH"] = _which_dirs("tar").union(_which_dirs("gzip")) + env["PATH"] = os.pathsep.join(env["PATH"]) + + cmd = [sys.executable, "setup.py"] + list(cmd) + + # https://bugs.python.org/issue8557 + shell = sys.platform == 'win32' + + try: + proc = _Popen( + cmd, + stdout=_PIPE, + stderr=_PIPE, + shell=shell, + env=env, + encoding="utf-8", + ) + + if isinstance(data_stream, tuple): + data_stream = slice(*data_stream) + data = proc.communicate()[data_stream] + except OSError: + return 1, '' + + # decode the console string if needed + if hasattr(data, "decode"): + # use the default encoding + data = data.decode() + data = unicodedata.normalize('NFC', data) + + # communicate calls wait() + return proc.returncode, data diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/fixtures.py b/venv/lib/python3.11/site-packages/setuptools/tests/fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..a5472984b5690572285a84575f6c2d598f06dd11 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/fixtures.py @@ -0,0 +1,157 @@ +import contextlib +import os +import subprocess +import sys +from pathlib import Path + +import path +import pytest + +from . import contexts, environment + + +@pytest.fixture +def user_override(monkeypatch): + """ + Override site.USER_BASE and site.USER_SITE with temporary directories in + a context. + """ + with contexts.tempdir() as user_base: + monkeypatch.setattr('site.USER_BASE', user_base) + with contexts.tempdir() as user_site: + monkeypatch.setattr('site.USER_SITE', user_site) + with contexts.save_user_site_setting(): + yield + + +@pytest.fixture +def tmpdir_cwd(tmpdir): + with tmpdir.as_cwd() as orig: + yield orig + + +@pytest.fixture(autouse=True, scope="session") +def workaround_xdist_376(request): + """ + Workaround pytest-dev/pytest-xdist#376 + + ``pytest-xdist`` tends to inject '' into ``sys.path``, + which may break certain isolation expectations. + Remove the entry so the import + machinery behaves the same irrespective of xdist. + """ + if not request.config.pluginmanager.has_plugin('xdist'): + return + + with contextlib.suppress(ValueError): + sys.path.remove('') + + +@pytest.fixture +def sample_project(tmp_path): + """ + Clone the 'sampleproject' and return a path to it. + """ + cmd = ['git', 'clone', 'https://github.com/pypa/sampleproject'] + try: + subprocess.check_call(cmd, cwd=str(tmp_path)) + except Exception: + pytest.skip("Unable to clone sampleproject") + return tmp_path / 'sampleproject' + + +# sdist and wheel artifacts should be stable across a round of tests +# so we can build them once per session and use the files as "readonly" + +# In the case of setuptools, building the wheel without sdist may cause +# it to contain the `build` directory, and therefore create situations with +# `setuptools/build/lib/build/lib/...`. To avoid that, build both artifacts at once. + + +def _build_distributions(tmp_path_factory, request): + with contexts.session_locked_tmp_dir( + request, tmp_path_factory, "dist_build" + ) as tmp: # pragma: no cover + sdist = next(tmp.glob("*.tar.gz"), None) + wheel = next(tmp.glob("*.whl"), None) + if sdist and wheel: + return (sdist, wheel) + + # Sanity check: should not create recursive setuptools/build/lib/build/lib/... + assert not Path(request.config.rootdir, "build/lib/build").exists() + + subprocess.check_output([ + sys.executable, + "-m", + "build", + "--outdir", + str(tmp), + str(request.config.rootdir), + ]) + + # Sanity check: should not create recursive setuptools/build/lib/build/lib/... + assert not Path(request.config.rootdir, "build/lib/build").exists() + + return next(tmp.glob("*.tar.gz")), next(tmp.glob("*.whl")) + + +@pytest.fixture(scope="session") +def setuptools_sdist(tmp_path_factory, request): + prebuilt = os.getenv("PRE_BUILT_SETUPTOOLS_SDIST") + if prebuilt and os.path.exists(prebuilt): # pragma: no cover + return Path(prebuilt).resolve() + + sdist, _ = _build_distributions(tmp_path_factory, request) + return sdist + + +@pytest.fixture(scope="session") +def setuptools_wheel(tmp_path_factory, request): + prebuilt = os.getenv("PRE_BUILT_SETUPTOOLS_WHEEL") + if prebuilt and os.path.exists(prebuilt): # pragma: no cover + return Path(prebuilt).resolve() + + _, wheel = _build_distributions(tmp_path_factory, request) + return wheel + + +@pytest.fixture +def venv(tmp_path, setuptools_wheel): + """Virtual env with the version of setuptools under test installed""" + env = environment.VirtualEnv() + env.root = path.Path(tmp_path / 'venv') + env.create_opts = ['--no-setuptools', '--wheel=bundle'] + # TODO: Use `--no-wheel` when setuptools implements its own bdist_wheel + env.req = str(setuptools_wheel) + # In some environments (eg. downstream distro packaging), + # where tox isn't used to run tests and PYTHONPATH is set to point to + # a specific setuptools codebase, PYTHONPATH will leak into the spawned + # processes. + # env.create() should install the just created setuptools + # wheel, but it doesn't if it finds another existing matching setuptools + # installation present on PYTHONPATH: + # `setuptools is already installed with the same version as the provided + # wheel. Use --force-reinstall to force an installation of the wheel.` + # This prevents leaking PYTHONPATH to the created environment. + with contexts.environment(PYTHONPATH=None): + return env.create() + + +@pytest.fixture +def venv_without_setuptools(tmp_path): + """Virtual env without any version of setuptools installed""" + env = environment.VirtualEnv() + env.root = path.Path(tmp_path / 'venv_without_setuptools') + env.create_opts = ['--no-setuptools', '--no-wheel'] + env.ensure_env() + return env + + +@pytest.fixture +def bare_venv(tmp_path): + """Virtual env without any common packages installed""" + env = environment.VirtualEnv() + env.root = path.Path(tmp_path / 'bare_venv') + env.create_opts = ['--no-setuptools', '--no-pip', '--no-wheel', '--no-seed'] + env.ensure_env() + return env diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/indexes/test_links_priority/external.html b/venv/lib/python3.11/site-packages/setuptools/tests/indexes/test_links_priority/external.html new file mode 100644 index 0000000000000000000000000000000000000000..92e4702f634dfb37a404bec3103b76f6afcaa917 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/indexes/test_links_priority/external.html @@ -0,0 +1,3 @@ + +bad old link + diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/indexes/test_links_priority/simple/foobar/index.html b/venv/lib/python3.11/site-packages/setuptools/tests/indexes/test_links_priority/simple/foobar/index.html new file mode 100644 index 0000000000000000000000000000000000000000..fefb028bd3ee7d45a414d6e96a7b2a21ffd7eda7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/indexes/test_links_priority/simple/foobar/index.html @@ -0,0 +1,4 @@ + +foobar-0.1.tar.gz
+external homepage
+ diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/integration/__init__.py b/venv/lib/python3.11/site-packages/setuptools/tests/integration/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/integration/helpers.py b/venv/lib/python3.11/site-packages/setuptools/tests/integration/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..77b196e029b6d2eeb25f0b84a4f7630f65201189 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/integration/helpers.py @@ -0,0 +1,77 @@ +"""Reusable functions and classes for different types of integration tests. + +For example ``Archive`` can be used to check the contents of distribution built +with setuptools, and ``run`` will always try to be as verbose as possible to +facilitate debugging. +""" + +import os +import subprocess +import tarfile +from pathlib import Path +from zipfile import ZipFile + + +def run(cmd, env=None): + r = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding="utf-8", + env={**os.environ, **(env or {})}, + # ^-- allow overwriting instead of discarding the current env + ) + + out = r.stdout + "\n" + r.stderr + # pytest omits stdout/err by default, if the test fails they help debugging + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + print(f"Command: {cmd}\nreturn code: {r.returncode}\n\n{out}") + + if r.returncode == 0: + return out + raise subprocess.CalledProcessError(r.returncode, cmd, r.stdout, r.stderr) + + +class Archive: + """Compatibility layer for ZipFile/Info and TarFile/Info""" + + def __init__(self, filename): + self._filename = filename + if filename.endswith("tar.gz"): + self._obj = tarfile.open(filename, "r:gz") + elif filename.endswith("zip"): + self._obj = ZipFile(filename) + else: + raise ValueError(f"{filename} doesn't seem to be a zip or tar.gz") + + def __iter__(self): + if hasattr(self._obj, "infolist"): + return iter(self._obj.infolist()) + return iter(self._obj) + + def get_name(self, zip_or_tar_info): + if hasattr(zip_or_tar_info, "filename"): + return zip_or_tar_info.filename + return zip_or_tar_info.name + + def get_content(self, zip_or_tar_info): + if hasattr(self._obj, "extractfile"): + content = self._obj.extractfile(zip_or_tar_info) + if content is None: + msg = f"Invalid {zip_or_tar_info.name} in {self._filename}" + raise ValueError(msg) + return str(content.read(), "utf-8") + return str(self._obj.read(zip_or_tar_info), "utf-8") + + +def get_sdist_members(sdist_path): + with tarfile.open(sdist_path, "r:gz") as tar: + files = [Path(f) for f in tar.getnames()] + # remove root folder + relative_files = ("/".join(f.parts[1:]) for f in files) + return {f for f in relative_files if f} + + +def get_wheel_members(wheel_path): + with ZipFile(wheel_path) as zipfile: + return set(zipfile.namelist()) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/integration/test_pip_install_sdist.py b/venv/lib/python3.11/site-packages/setuptools/tests/integration/test_pip_install_sdist.py new file mode 100644 index 0000000000000000000000000000000000000000..4e84f218323e6ad67adfbce07cea5a16e91a311c --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/integration/test_pip_install_sdist.py @@ -0,0 +1,223 @@ +# https://github.com/python/mypy/issues/16936 +# mypy: disable-error-code="has-type" +"""Integration tests for setuptools that focus on building packages via pip. + +The idea behind these tests is not to exhaustively check all the possible +combinations of packages, operating systems, supporting libraries, etc, but +rather check a limited number of popular packages and how they interact with +the exposed public API. This way if any change in API is introduced, we hope to +identify backward compatibility problems before publishing a release. + +The number of tested packages is purposefully kept small, to minimise duration +and the associated maintenance cost (changes in the way these packages define +their build process may require changes in the tests). +""" + +import json +import os +import shutil +import sys +from enum import Enum +from glob import glob +from hashlib import md5 +from urllib.request import urlopen + +import pytest +from packaging.requirements import Requirement + +from .helpers import Archive, run + +pytestmark = pytest.mark.integration + + +(LATEST,) = Enum("v", "LATEST") # type: ignore[misc] # https://github.com/python/mypy/issues/16936 +"""Default version to be checked""" +# There are positive and negative aspects of checking the latest version of the +# packages. +# The main positive aspect is that the latest version might have already +# removed the use of APIs deprecated in previous releases of setuptools. + + +# Packages to be tested: +# (Please notice the test environment cannot support EVERY library required for +# compiling binary extensions. In Ubuntu/Debian nomenclature, we only assume +# that `build-essential`, `gfortran` and `libopenblas-dev` are installed, +# due to their relevance to the numerical/scientific programming ecosystem) +EXAMPLES = [ + ("pip", LATEST), # just in case... + ("pytest", LATEST), # uses setuptools_scm + ("mypy", LATEST), # custom build_py + ext_modules + # --- Popular packages: https://hugovk.github.io/top-pypi-packages/ --- + ("botocore", LATEST), + ("kiwisolver", LATEST), # build_ext + ("brotli", LATEST), # not in the list but used by urllib3 + ("pyyaml", LATEST), # cython + custom build_ext + custom distclass + ("charset-normalizer", LATEST), # uses mypyc, used by aiohttp + ("protobuf", LATEST), + # ("requests", LATEST), # XXX: https://github.com/psf/requests/pull/6920 + ("celery", LATEST), + # When adding packages to this list, make sure they expose a `__version__` + # attribute, or modify the tests below +] + + +# Some packages have "optional" dependencies that modify their build behaviour +# and are not listed in pyproject.toml, others still use `setup_requires` +EXTRA_BUILD_DEPS = { + "pyyaml": ("Cython<3.0",), # constraint to avoid errors + "charset-normalizer": ("mypy>=1.4.1",), # no pyproject.toml available +} + +EXTRA_ENV_VARS = { + "pyyaml": {"PYYAML_FORCE_CYTHON": "1"}, + "charset-normalizer": {"CHARSET_NORMALIZER_USE_MYPYC": "1"}, +} + +IMPORT_NAME = { + "pyyaml": "yaml", + "protobuf": "google.protobuf", +} + + +VIRTUALENV = (sys.executable, "-m", "virtualenv") + + +# By default, pip will try to build packages in isolation (PEP 517), which +# means it will download the previous stable version of setuptools. +# `pip` flags can avoid that (the version of setuptools under test +# should be the one to be used) +INSTALL_OPTIONS = ( + "--ignore-installed", + "--no-build-isolation", + # Omit "--no-binary :all:" the sdist is supplied directly. + # Allows dependencies as wheels. +) +# The downside of `--no-build-isolation` is that pip will not download build +# dependencies. The test script will have to also handle that. + + +@pytest.fixture +def venv_python(tmp_path): + run([*VIRTUALENV, str(tmp_path / ".venv")]) + possible_path = (str(p.parent) for p in tmp_path.glob(".venv/*/python*")) + return shutil.which("python", path=os.pathsep.join(possible_path)) + + +@pytest.fixture(autouse=True) +def _prepare(tmp_path, venv_python, monkeypatch): + download_path = os.getenv("DOWNLOAD_PATH", str(tmp_path)) + os.makedirs(download_path, exist_ok=True) + + # Environment vars used for building some of the packages + monkeypatch.setenv("USE_MYPYC", "1") + + yield + + # Let's provide the maximum amount of information possible in the case + # it is necessary to debug the tests directly from the CI logs. + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + print("Temporary directory:") + map(print, tmp_path.glob("*")) + print("Virtual environment:") + run([venv_python, "-m", "pip", "freeze"]) + + +@pytest.mark.parametrize(("package", "version"), EXAMPLES) +@pytest.mark.uses_network +def test_install_sdist(package, version, tmp_path, venv_python, setuptools_wheel): + venv_pip = (venv_python, "-m", "pip") + sdist = retrieve_sdist(package, version, tmp_path) + deps = build_deps(package, sdist) + if deps: + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + print("Dependencies:", deps) + run([*venv_pip, "install", *deps]) + + # Use a virtualenv to simulate PEP 517 isolation + # but install fresh setuptools wheel to ensure the version under development + env = EXTRA_ENV_VARS.get(package, {}) + run([*venv_pip, "install", "--force-reinstall", setuptools_wheel]) + run([*venv_pip, "install", *INSTALL_OPTIONS, sdist], env) + + # Execute a simple script to make sure the package was installed correctly + pkg = IMPORT_NAME.get(package, package).replace("-", "_") + script = f"import {pkg}; print(getattr({pkg}, '__version__', 0))" + run([venv_python, "-c", script]) + + +# ---- Helper Functions ---- + + +def retrieve_sdist(package, version, tmp_path): + """Either use cached sdist file or download it from PyPI""" + # `pip download` cannot be used due to + # https://github.com/pypa/pip/issues/1884 + # https://discuss.python.org/t/pep-625-file-name-of-a-source-distribution/4686 + # We have to find the correct distribution file and download it + download_path = os.getenv("DOWNLOAD_PATH", str(tmp_path)) + dist = retrieve_pypi_sdist_metadata(package, version) + + # Remove old files to prevent cache to grow indefinitely + for file in glob(os.path.join(download_path, f"{package}*")): + if dist["filename"] != file: + os.unlink(file) + + dist_file = os.path.join(download_path, dist["filename"]) + if not os.path.exists(dist_file): + download(dist["url"], dist_file, dist["md5_digest"]) + return dist_file + + +def retrieve_pypi_sdist_metadata(package, version): + # https://warehouse.pypa.io/api-reference/json.html + id_ = package if version is LATEST else f"{package}/{version}" + with urlopen(f"https://pypi.org/pypi/{id_}/json") as f: + metadata = json.load(f) + + if metadata["info"]["yanked"]: + raise ValueError(f"Release for {package} {version} was yanked") + + version = metadata["info"]["version"] + release = metadata["releases"][version] if version is LATEST else metadata["urls"] + (sdist,) = filter(lambda d: d["packagetype"] == "sdist", release) + return sdist + + +def download(url, dest, md5_digest): + with urlopen(url) as f: + data = f.read() + + assert md5(data).hexdigest() == md5_digest + + with open(dest, "wb") as f: + f.write(data) + + assert os.path.exists(dest) + + +def build_deps(package, sdist_file): + """Find out what are the build dependencies for a package. + + "Manually" install them, since pip will not install build + deps with `--no-build-isolation`. + """ + # delay importing, since pytest discovery phase may hit this file from a + # testenv without tomli + from setuptools.compat.py310 import tomllib + + archive = Archive(sdist_file) + info = tomllib.loads(_read_pyproject(archive)) + deps = info.get("build-system", {}).get("requires", []) + deps += EXTRA_BUILD_DEPS.get(package, []) + # Remove setuptools from requirements (and deduplicate) + requirements = {Requirement(d).name: d for d in deps} + return [v for k, v in requirements.items() if k != "setuptools"] + + +def _read_pyproject(archive): + contents = ( + archive.get_content(member) + for member in archive + if os.path.basename(archive.get_name(member)) == "pyproject.toml" + ) + return next(contents, "") diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/mod_with_constant.py b/venv/lib/python3.11/site-packages/setuptools/tests/mod_with_constant.py new file mode 100644 index 0000000000000000000000000000000000000000..ef755dd1c7a8d1f116fe51f1b43315057f03379d --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/mod_with_constant.py @@ -0,0 +1 @@ +value = 'three, sir!' diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/namespaces.py b/venv/lib/python3.11/site-packages/setuptools/tests/namespaces.py new file mode 100644 index 0000000000000000000000000000000000000000..248db98f97951aeeee0222131417e73074cc72d2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/namespaces.py @@ -0,0 +1,90 @@ +import ast +import json +import textwrap +from pathlib import Path + + +def iter_namespace_pkgs(namespace): + parts = namespace.split(".") + for i in range(len(parts)): + yield ".".join(parts[: i + 1]) + + +def build_namespace_package(tmpdir, name, version="1.0", impl="pkg_resources"): + src_dir = tmpdir / name + src_dir.mkdir() + setup_py = src_dir / 'setup.py' + namespace, _, rest = name.rpartition('.') + namespaces = list(iter_namespace_pkgs(namespace)) + setup_args = { + "name": name, + "version": version, + "packages": namespaces, + } + + if impl == "pkg_resources": + tmpl = '__import__("pkg_resources").declare_namespace(__name__)' + setup_args["namespace_packages"] = namespaces + elif impl == "pkgutil": + tmpl = '__path__ = __import__("pkgutil").extend_path(__path__, __name__)' + else: + raise ValueError(f"Cannot recognise {impl=} when creating namespaces") + + args = json.dumps(setup_args, indent=4) + assert ast.literal_eval(args) # ensure it is valid Python + + script = textwrap.dedent( + """\ + import setuptools + args = {args} + setuptools.setup(**args) + """ + ).format(args=args) + setup_py.write_text(script, encoding='utf-8') + + ns_pkg_dir = Path(src_dir, namespace.replace(".", "/")) + ns_pkg_dir.mkdir(parents=True) + + for ns in namespaces: + pkg_init = src_dir / ns.replace(".", "/") / '__init__.py' + pkg_init.write_text(tmpl, encoding='utf-8') + + pkg_mod = ns_pkg_dir / (rest + '.py') + some_functionality = 'name = {rest!r}'.format(**locals()) + pkg_mod.write_text(some_functionality, encoding='utf-8') + return src_dir + + +def build_pep420_namespace_package(tmpdir, name): + src_dir = tmpdir / name + src_dir.mkdir() + pyproject = src_dir / "pyproject.toml" + namespace, _, rest = name.rpartition(".") + script = f"""\ + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + + [project] + name = "{name}" + version = "3.14159" + """ + pyproject.write_text(textwrap.dedent(script), encoding='utf-8') + ns_pkg_dir = Path(src_dir, namespace.replace(".", "/")) + ns_pkg_dir.mkdir(parents=True) + pkg_mod = ns_pkg_dir / (rest + ".py") + some_functionality = f"name = {rest!r}" + pkg_mod.write_text(some_functionality, encoding='utf-8') + return src_dir + + +def make_site_dir(target): + """ + Add a sitecustomize.py module in target to cause + target to be added to site dirs such that .pth files + are processed there. + """ + sc = target / 'sitecustomize.py' + target_str = str(target) + tmpl = '__import__("site").addsitedir({target_str!r})' + sc.write_text(tmpl.format(**locals()), encoding='utf-8') diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/script-with-bom.py b/venv/lib/python3.11/site-packages/setuptools/tests/script-with-bom.py new file mode 100644 index 0000000000000000000000000000000000000000..c074d263c45bcaebe32fdba328d975c73d1ad5ca --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/script-with-bom.py @@ -0,0 +1 @@ +result = 'passed' diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/server.py b/venv/lib/python3.11/site-packages/setuptools/tests/server.py new file mode 100644 index 0000000000000000000000000000000000000000..623a49a550f161f117d3af2173f91a2af260181d --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/server.py @@ -0,0 +1,86 @@ +"""Basic http server for tests to simulate PyPI or custom indexes""" + +import http.server +import os +import threading +import time +import urllib.parse +import urllib.request + + +class IndexServer(http.server.HTTPServer): + """Basic single-threaded http server simulating a package index + + You can use this server in unittest like this:: + s = IndexServer() + s.start() + index_url = s.base_url() + 'mytestindex' + # do some test requests to the index + # The index files should be located in setuptools/tests/indexes + s.stop() + """ + + def __init__( + self, + server_address=('', 0), + RequestHandlerClass=http.server.SimpleHTTPRequestHandler, + ): + http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass) + self._run = True + + def start(self): + self.thread = threading.Thread(target=self.serve_forever) + self.thread.start() + + def stop(self): + "Stop the server" + + # Let the server finish the last request and wait for a new one. + time.sleep(0.1) + + self.shutdown() + self.thread.join() + self.socket.close() + + def base_url(self): + port = self.server_port + return f'http://127.0.0.1:{port}/setuptools/tests/indexes/' + + +class RequestRecorder(http.server.BaseHTTPRequestHandler): + def do_GET(self): + requests = vars(self.server).setdefault('requests', []) + requests.append(self) + self.send_response(200, 'OK') + + +class MockServer(http.server.HTTPServer, threading.Thread): + """ + A simple HTTP Server that records the requests made to it. + """ + + def __init__(self, server_address=('', 0), RequestHandlerClass=RequestRecorder): + http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass) + threading.Thread.__init__(self) + self.daemon = True + self.requests = [] + + def run(self): + self.serve_forever() + + @property + def netloc(self): + return f'localhost:{self.server_port}' + + @property + def url(self): + return f'http://{self.netloc}/' + + +def path_to_url(path, authority=None): + """Convert a path to a file: URL.""" + path = os.path.normpath(os.path.abspath(path)) + base = 'file:' + if authority is not None: + base += '//' + authority + return urllib.parse.urljoin(base, urllib.request.pathname2url(path)) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_archive_util.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_archive_util.py new file mode 100644 index 0000000000000000000000000000000000000000..e3efc62889994fa68bc9170e8a0e403f48a204e1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_archive_util.py @@ -0,0 +1,36 @@ +import io +import tarfile + +import pytest + +from setuptools import archive_util + + +@pytest.fixture +def tarfile_with_unicode(tmpdir): + """ + Create a tarfile containing only a file whose name is + a zero byte file called testimäge.png. + """ + tarobj = io.BytesIO() + + with tarfile.open(fileobj=tarobj, mode="w:gz") as tgz: + data = b"" + + filename = "testimäge.png" + + t = tarfile.TarInfo(filename) + t.size = len(data) + + tgz.addfile(t, io.BytesIO(data)) + + target = tmpdir / 'unicode-pkg-1.0.tar.gz' + with open(str(target), mode='wb') as tf: + tf.write(tarobj.getvalue()) + return str(target) + + +@pytest.mark.xfail(reason="#710 and #712") +def test_unicode_files(tarfile_with_unicode, tmpdir): + target = tmpdir / 'out' + archive_util.unpack_archive(tarfile_with_unicode, str(target)) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_bdist_deprecations.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_bdist_deprecations.py new file mode 100644 index 0000000000000000000000000000000000000000..d9d67b06161a2b36e7b15fab09f797c5c15575c2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_bdist_deprecations.py @@ -0,0 +1,28 @@ +"""develop tests""" + +import sys +from unittest import mock + +import pytest + +from setuptools import SetuptoolsDeprecationWarning +from setuptools.dist import Distribution + + +@pytest.mark.skipif(sys.platform == 'win32', reason='non-Windows only') +@pytest.mark.xfail(reason="bdist_rpm is long deprecated, should we remove it? #1988") +@mock.patch('distutils.command.bdist_rpm.bdist_rpm') +def test_bdist_rpm_warning(distutils_cmd, tmpdir_cwd): + dist = Distribution( + dict( + script_name='setup.py', + script_args=['bdist_rpm'], + name='foo', + py_modules=['hi'], + ) + ) + dist.parse_command_line() + with pytest.warns(SetuptoolsDeprecationWarning): + dist.run_commands() + + distutils_cmd.run.assert_called_once() diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_bdist_egg.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_bdist_egg.py new file mode 100644 index 0000000000000000000000000000000000000000..036167dd951e70ad543775529d5ce3f6d6544c71 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_bdist_egg.py @@ -0,0 +1,73 @@ +"""develop tests""" + +import os +import re +import zipfile + +import pytest + +from setuptools.dist import Distribution + +from . import contexts + +SETUP_PY = """\ +from setuptools import setup + +setup(py_modules=['hi']) +""" + + +@pytest.fixture +def setup_context(tmpdir): + with (tmpdir / 'setup.py').open('w') as f: + f.write(SETUP_PY) + with (tmpdir / 'hi.py').open('w') as f: + f.write('1\n') + with tmpdir.as_cwd(): + yield tmpdir + + +class Test: + @pytest.mark.usefixtures("user_override") + @pytest.mark.usefixtures("setup_context") + def test_bdist_egg(self): + dist = Distribution( + dict( + script_name='setup.py', + script_args=['bdist_egg'], + name='foo', + py_modules=['hi'], + ) + ) + os.makedirs(os.path.join('build', 'src')) + with contexts.quiet(): + dist.parse_command_line() + dist.run_commands() + + # let's see if we got our egg link at the right place + [content] = os.listdir('dist') + assert re.match(r'foo-0.0.0-py[23].\d+.egg$', content) + + @pytest.mark.xfail( + os.environ.get('PYTHONDONTWRITEBYTECODE', False), + reason="Byte code disabled", + ) + @pytest.mark.usefixtures("user_override") + @pytest.mark.usefixtures("setup_context") + def test_exclude_source_files(self): + dist = Distribution( + dict( + script_name='setup.py', + script_args=['bdist_egg', '--exclude-source-files'], + py_modules=['hi'], + ) + ) + with contexts.quiet(): + dist.parse_command_line() + dist.run_commands() + [dist_name] = os.listdir('dist') + dist_filename = os.path.join('dist', dist_name) + zip = zipfile.ZipFile(dist_filename) + names = list(zi.filename for zi in zip.filelist) + assert 'hi.pyc' in names + assert 'hi.py' not in names diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_bdist_wheel.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_bdist_wheel.py new file mode 100644 index 0000000000000000000000000000000000000000..2ab4e9cfc6cf0d930b3ceeececcce10cfe91a687 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_bdist_wheel.py @@ -0,0 +1,708 @@ +from __future__ import annotations + +import builtins +import importlib +import os.path +import platform +import shutil +import stat +import struct +import sys +import sysconfig +from contextlib import suppress +from inspect import cleandoc +from zipfile import ZipFile + +import jaraco.path +import pytest +from packaging import tags + +import setuptools +from setuptools.command.bdist_wheel import bdist_wheel, get_abi_tag +from setuptools.dist import Distribution +from setuptools.warnings import SetuptoolsDeprecationWarning + +from distutils.core import run_setup + +DEFAULT_FILES = { + "dummy_dist-1.0.dist-info/top_level.txt", + "dummy_dist-1.0.dist-info/METADATA", + "dummy_dist-1.0.dist-info/WHEEL", + "dummy_dist-1.0.dist-info/RECORD", +} +DEFAULT_LICENSE_FILES = { + "LICENSE", + "LICENSE.txt", + "LICENCE", + "LICENCE.txt", + "COPYING", + "COPYING.md", + "NOTICE", + "NOTICE.rst", + "AUTHORS", + "AUTHORS.txt", +} +OTHER_IGNORED_FILES = { + "LICENSE~", + "AUTHORS~", +} +SETUPPY_EXAMPLE = """\ +from setuptools import setup + +setup( + name='dummy_dist', + version='1.0', +) +""" + + +EXAMPLES = { + "dummy-dist": { + "setup.py": SETUPPY_EXAMPLE, + "licenses_dir": {"DUMMYFILE": ""}, + **dict.fromkeys(DEFAULT_LICENSE_FILES | OTHER_IGNORED_FILES, ""), + }, + "simple-dist": { + "setup.py": cleandoc( + """ + from setuptools import setup + + setup( + name="simple.dist", + version="0.1", + description="A testing distribution \N{SNOWMAN}", + extras_require={"voting": ["beaglevote"]}, + ) + """ + ), + "simpledist": "", + }, + "complex-dist": { + "setup.py": cleandoc( + """ + from setuptools import setup + + setup( + name="complex-dist", + version="0.1", + description="Another testing distribution \N{SNOWMAN}", + long_description="Another testing distribution \N{SNOWMAN}", + author="Illustrious Author", + author_email="illustrious@example.org", + url="http://example.org/exemplary", + packages=["complexdist"], + setup_requires=["setuptools"], + install_requires=["quux", "splort"], + extras_require={"simple": ["simple.dist"]}, + entry_points={ + "console_scripts": [ + "complex-dist=complexdist:main", + "complex-dist2=complexdist:main", + ], + }, + ) + """ + ), + "complexdist": {"__init__.py": "def main(): return"}, + }, + "headers-dist": { + "setup.py": cleandoc( + """ + from setuptools import setup + + setup( + name="headers.dist", + version="0.1", + description="A distribution with headers", + headers=["header.h"], + ) + """ + ), + "headersdist.py": "", + "header.h": "", + }, + "commasinfilenames-dist": { + "setup.py": cleandoc( + """ + from setuptools import setup + + setup( + name="testrepo", + version="0.1", + packages=["mypackage"], + description="A test package with commas in file names", + include_package_data=True, + package_data={"mypackage.data": ["*"]}, + ) + """ + ), + "mypackage": { + "__init__.py": "", + "data": {"__init__.py": "", "1,2,3.txt": ""}, + }, + "testrepo-0.1.0": { + "mypackage": {"__init__.py": ""}, + }, + }, + "unicode-dist": { + "setup.py": cleandoc( + """ + from setuptools import setup + + setup( + name="unicode.dist", + version="0.1", + description="A testing distribution \N{SNOWMAN}", + packages=["unicodedist"], + zip_safe=True, + ) + """ + ), + "unicodedist": {"__init__.py": "", "åäö_日本語.py": ""}, + }, + "utf8-metadata-dist": { + "setup.cfg": cleandoc( + """ + [metadata] + name = utf8-metadata-dist + version = 42 + author_email = "John X. Ãørçeč" , Γαμα קּ 東 + long_description = file: README.rst + """ + ), + "README.rst": "UTF-8 描述 説明", + }, + "licenses-dist": { + "setup.cfg": cleandoc( + """ + [metadata] + name = licenses-dist + version = 1.0 + license_files = **/LICENSE + """ + ), + "LICENSE": "", + "src": { + "vendor": {"LICENSE": ""}, + }, + }, +} + + +if sys.platform != "win32": + # ABI3 extensions don't really work on Windows + EXAMPLES["abi3extension-dist"] = { + "setup.py": cleandoc( + """ + from setuptools import Extension, setup + + setup( + name="extension.dist", + version="0.1", + description="A testing distribution \N{SNOWMAN}", + ext_modules=[ + Extension( + name="extension", sources=["extension.c"], py_limited_api=True + ) + ], + ) + """ + ), + "setup.cfg": "[bdist_wheel]\npy_limited_api=cp32", + "extension.c": "#define Py_LIMITED_API 0x03020000\n#include ", + } + + +def bdist_wheel_cmd(**kwargs): + """Run command in the same process so that it is easier to collect coverage""" + dist_obj = ( + run_setup("setup.py", stop_after="init") + if os.path.exists("setup.py") + else Distribution({"script_name": "%%build_meta%%"}) + ) + dist_obj.parse_config_files() + cmd = bdist_wheel(dist_obj) + for attr, value in kwargs.items(): + setattr(cmd, attr, value) + cmd.finalize_options() + return cmd + + +def mkexample(tmp_path_factory, name): + basedir = tmp_path_factory.mktemp(name) + jaraco.path.build(EXAMPLES[name], prefix=str(basedir)) + return basedir + + +@pytest.fixture(scope="session") +def wheel_paths(tmp_path_factory): + build_base = tmp_path_factory.mktemp("build") + dist_dir = tmp_path_factory.mktemp("dist") + for name in EXAMPLES: + example_dir = mkexample(tmp_path_factory, name) + build_dir = build_base / name + with jaraco.path.DirectoryStack().context(example_dir): + bdist_wheel_cmd(bdist_dir=str(build_dir), dist_dir=str(dist_dir)).run() + + return sorted(str(fname) for fname in dist_dir.glob("*.whl")) + + +@pytest.fixture +def dummy_dist(tmp_path_factory): + return mkexample(tmp_path_factory, "dummy-dist") + + +@pytest.fixture +def licenses_dist(tmp_path_factory): + return mkexample(tmp_path_factory, "licenses-dist") + + +def test_no_scripts(wheel_paths): + """Make sure entry point scripts are not generated.""" + path = next(path for path in wheel_paths if "complex_dist" in path) + for entry in ZipFile(path).infolist(): + assert ".data/scripts/" not in entry.filename + + +def test_unicode_record(wheel_paths): + path = next(path for path in wheel_paths if "unicode_dist" in path) + with ZipFile(path) as zf: + record = zf.read("unicode_dist-0.1.dist-info/RECORD") + + assert "åäö_日本語.py".encode() in record + + +UTF8_PKG_INFO = """\ +Metadata-Version: 2.1 +Name: helloworld +Version: 42 +Author-email: "John X. Ãørçeč" , Γαμα קּ 東 + + +UTF-8 描述 説明 +""" + + +def test_preserve_unicode_metadata(monkeypatch, tmp_path): + monkeypatch.chdir(tmp_path) + egginfo = tmp_path / "dummy_dist.egg-info" + distinfo = tmp_path / "dummy_dist.dist-info" + + egginfo.mkdir() + (egginfo / "PKG-INFO").write_text(UTF8_PKG_INFO, encoding="utf-8") + (egginfo / "dependency_links.txt").touch() + + class simpler_bdist_wheel(bdist_wheel): + """Avoid messing with setuptools/distutils internals""" + + def __init__(self): + pass + + @property + def license_paths(self): + return [] + + cmd_obj = simpler_bdist_wheel() + cmd_obj.egg2dist(egginfo, distinfo) + + metadata = (distinfo / "METADATA").read_text(encoding="utf-8") + assert 'Author-email: "John X. Ãørçeč"' in metadata + assert "Γαμα קּ 東 " in metadata + assert "UTF-8 描述 説明" in metadata + + +def test_licenses_default(dummy_dist, monkeypatch, tmp_path): + monkeypatch.chdir(dummy_dist) + bdist_wheel_cmd(bdist_dir=str(tmp_path)).run() + with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf: + license_files = { + "dummy_dist-1.0.dist-info/licenses/" + fname + for fname in DEFAULT_LICENSE_FILES + } + assert set(wf.namelist()) == DEFAULT_FILES | license_files + + +def test_licenses_deprecated(dummy_dist, monkeypatch, tmp_path): + dummy_dist.joinpath("setup.cfg").write_text( + "[metadata]\nlicense_file=licenses_dir/DUMMYFILE", encoding="utf-8" + ) + monkeypatch.chdir(dummy_dist) + + bdist_wheel_cmd(bdist_dir=str(tmp_path)).run() + + with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf: + license_files = {"dummy_dist-1.0.dist-info/licenses/licenses_dir/DUMMYFILE"} + assert set(wf.namelist()) == DEFAULT_FILES | license_files + + +@pytest.mark.parametrize( + ("config_file", "config"), + [ + ("setup.cfg", "[metadata]\nlicense_files=licenses_dir/*\n LICENSE"), + ("setup.cfg", "[metadata]\nlicense_files=licenses_dir/*, LICENSE"), + ( + "setup.py", + SETUPPY_EXAMPLE.replace( + ")", " license_files=['licenses_dir/DUMMYFILE', 'LICENSE'])" + ), + ), + ], +) +def test_licenses_override(dummy_dist, monkeypatch, tmp_path, config_file, config): + dummy_dist.joinpath(config_file).write_text(config, encoding="utf-8") + monkeypatch.chdir(dummy_dist) + bdist_wheel_cmd(bdist_dir=str(tmp_path)).run() + with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf: + license_files = { + "dummy_dist-1.0.dist-info/licenses/" + fname + for fname in {"licenses_dir/DUMMYFILE", "LICENSE"} + } + assert set(wf.namelist()) == DEFAULT_FILES | license_files + metadata = wf.read("dummy_dist-1.0.dist-info/METADATA").decode("utf8") + assert "License-File: licenses_dir/DUMMYFILE" in metadata + assert "License-File: LICENSE" in metadata + + +def test_licenses_preserve_folder_structure(licenses_dist, monkeypatch, tmp_path): + monkeypatch.chdir(licenses_dist) + bdist_wheel_cmd(bdist_dir=str(tmp_path)).run() + print(os.listdir("dist")) + with ZipFile("dist/licenses_dist-1.0-py3-none-any.whl") as wf: + default_files = {name.replace("dummy_", "licenses_") for name in DEFAULT_FILES} + license_files = { + "licenses_dist-1.0.dist-info/licenses/LICENSE", + "licenses_dist-1.0.dist-info/licenses/src/vendor/LICENSE", + } + assert set(wf.namelist()) == default_files | license_files + metadata = wf.read("licenses_dist-1.0.dist-info/METADATA").decode("utf8") + assert "License-File: src/vendor/LICENSE" in metadata + assert "License-File: LICENSE" in metadata + + +def test_licenses_disabled(dummy_dist, monkeypatch, tmp_path): + dummy_dist.joinpath("setup.cfg").write_text( + "[metadata]\nlicense_files=\n", encoding="utf-8" + ) + monkeypatch.chdir(dummy_dist) + bdist_wheel_cmd(bdist_dir=str(tmp_path)).run() + with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf: + assert set(wf.namelist()) == DEFAULT_FILES + + +def test_build_number(dummy_dist, monkeypatch, tmp_path): + monkeypatch.chdir(dummy_dist) + bdist_wheel_cmd(bdist_dir=str(tmp_path), build_number="2").run() + with ZipFile("dist/dummy_dist-1.0-2-py3-none-any.whl") as wf: + filenames = set(wf.namelist()) + assert "dummy_dist-1.0.dist-info/RECORD" in filenames + assert "dummy_dist-1.0.dist-info/METADATA" in filenames + + +def test_universal_deprecated(dummy_dist, monkeypatch, tmp_path): + monkeypatch.chdir(dummy_dist) + with pytest.warns(SetuptoolsDeprecationWarning, match=".*universal is deprecated"): + bdist_wheel_cmd(bdist_dir=str(tmp_path), universal=True).run() + + # For now we still respect the option + assert os.path.exists("dist/dummy_dist-1.0-py2.py3-none-any.whl") + + +EXTENSION_EXAMPLE = """\ +#include + +static PyMethodDef methods[] = { + { NULL, NULL, 0, NULL } +}; + +static struct PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + "extension", + "Dummy extension module", + -1, + methods +}; + +PyMODINIT_FUNC PyInit_extension(void) { + return PyModule_Create(&module_def); +} +""" +EXTENSION_SETUPPY = """\ +from __future__ import annotations + +from setuptools import Extension, setup + +setup( + name="extension.dist", + version="0.1", + description="A testing distribution \N{SNOWMAN}", + ext_modules=[Extension(name="extension", sources=["extension.c"])], +) +""" + + +@pytest.mark.filterwarnings( + "once:Config variable '.*' is unset.*, Python ABI tag may be incorrect" +) +def test_limited_abi(monkeypatch, tmp_path, tmp_path_factory): + """Test that building a binary wheel with the limited ABI works.""" + source_dir = tmp_path_factory.mktemp("extension_dist") + (source_dir / "setup.py").write_text(EXTENSION_SETUPPY, encoding="utf-8") + (source_dir / "extension.c").write_text(EXTENSION_EXAMPLE, encoding="utf-8") + build_dir = tmp_path.joinpath("build") + dist_dir = tmp_path.joinpath("dist") + monkeypatch.chdir(source_dir) + bdist_wheel_cmd(bdist_dir=str(build_dir), dist_dir=str(dist_dir)).run() + + +def test_build_from_readonly_tree(dummy_dist, monkeypatch, tmp_path): + basedir = str(tmp_path.joinpath("dummy")) + shutil.copytree(str(dummy_dist), basedir) + monkeypatch.chdir(basedir) + + # Make the tree read-only + for root, _dirs, files in os.walk(basedir): + for fname in files: + os.chmod(os.path.join(root, fname), stat.S_IREAD) + + bdist_wheel_cmd().run() + + +@pytest.mark.parametrize( + ("option", "compress_type"), + list(bdist_wheel.supported_compressions.items()), + ids=list(bdist_wheel.supported_compressions), +) +def test_compression(dummy_dist, monkeypatch, tmp_path, option, compress_type): + monkeypatch.chdir(dummy_dist) + bdist_wheel_cmd(bdist_dir=str(tmp_path), compression=option).run() + with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf: + filenames = set(wf.namelist()) + assert "dummy_dist-1.0.dist-info/RECORD" in filenames + assert "dummy_dist-1.0.dist-info/METADATA" in filenames + for zinfo in wf.filelist: + assert zinfo.compress_type == compress_type + + +def test_wheelfile_line_endings(wheel_paths): + for path in wheel_paths: + with ZipFile(path) as wf: + wheelfile = next(fn for fn in wf.filelist if fn.filename.endswith("WHEEL")) + wheelfile_contents = wf.read(wheelfile) + assert b"\r" not in wheelfile_contents + + +def test_unix_epoch_timestamps(dummy_dist, monkeypatch, tmp_path): + monkeypatch.setenv("SOURCE_DATE_EPOCH", "0") + monkeypatch.chdir(dummy_dist) + bdist_wheel_cmd(bdist_dir=str(tmp_path), build_number="2a").run() + with ZipFile("dist/dummy_dist-1.0-2a-py3-none-any.whl") as wf: + for zinfo in wf.filelist: + assert zinfo.date_time >= (1980, 1, 1, 0, 0, 0) # min epoch is used + + +def test_get_abi_tag_windows(monkeypatch): + monkeypatch.setattr(tags, "interpreter_name", lambda: "cp") + monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "cp313-win_amd64") + assert get_abi_tag() == "cp313" + monkeypatch.setattr(sys, "gettotalrefcount", lambda: 1, False) + assert get_abi_tag() == "cp313d" + monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "cp313t-win_amd64") + assert get_abi_tag() == "cp313td" + monkeypatch.delattr(sys, "gettotalrefcount") + assert get_abi_tag() == "cp313t" + + +def test_get_abi_tag_pypy_old(monkeypatch): + monkeypatch.setattr(tags, "interpreter_name", lambda: "pp") + monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "pypy36-pp73") + assert get_abi_tag() == "pypy36_pp73" + + +def test_get_abi_tag_pypy_new(monkeypatch): + monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "pypy37-pp73-darwin") + monkeypatch.setattr(tags, "interpreter_name", lambda: "pp") + assert get_abi_tag() == "pypy37_pp73" + + +def test_get_abi_tag_graalpy(monkeypatch): + monkeypatch.setattr( + sysconfig, "get_config_var", lambda x: "graalpy231-310-native-x86_64-linux" + ) + monkeypatch.setattr(tags, "interpreter_name", lambda: "graalpy") + assert get_abi_tag() == "graalpy231_310_native" + + +def test_get_abi_tag_fallback(monkeypatch): + monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "unknown-python-310") + monkeypatch.setattr(tags, "interpreter_name", lambda: "unknown-python") + assert get_abi_tag() == "unknown_python_310" + + +def test_platform_with_space(dummy_dist, monkeypatch): + """Ensure building on platforms with a space in the name succeed.""" + monkeypatch.chdir(dummy_dist) + bdist_wheel_cmd(plat_name="isilon onefs").run() + + +def test_data_dir_with_tag_build(monkeypatch, tmp_path): + """ + Setuptools allow authors to set PEP 440's local version segments + using ``egg_info.tag_build``. This should be reflected not only in the + ``.whl`` file name, but also in the ``.dist-info`` and ``.data`` dirs. + See pypa/setuptools#3997. + """ + monkeypatch.chdir(tmp_path) + files = { + "setup.py": """ + from setuptools import setup + setup(headers=["hello.h"]) + """, + "setup.cfg": """ + [metadata] + name = test + version = 1.0 + + [options.data_files] + hello/world = file.txt + + [egg_info] + tag_build = +what + tag_date = 0 + """, + "file.txt": "", + "hello.h": "", + } + for file, content in files.items(): + with open(file, "w", encoding="utf-8") as fh: + fh.write(cleandoc(content)) + + bdist_wheel_cmd().run() + + # Ensure .whl, .dist-info and .data contain the local segment + wheel_path = "dist/test-1.0+what-py3-none-any.whl" + assert os.path.exists(wheel_path) + entries = set(ZipFile(wheel_path).namelist()) + for expected in ( + "test-1.0+what.data/headers/hello.h", + "test-1.0+what.data/data/hello/world/file.txt", + "test-1.0+what.dist-info/METADATA", + "test-1.0+what.dist-info/WHEEL", + ): + assert expected in entries + + for not_expected in ( + "test.data/headers/hello.h", + "test-1.0.data/data/hello/world/file.txt", + "test.dist-info/METADATA", + "test-1.0.dist-info/WHEEL", + ): + assert not_expected not in entries + + +@pytest.mark.parametrize( + ("reported", "expected"), + [("linux-x86_64", "linux_i686"), ("linux-aarch64", "linux_armv7l")], +) +@pytest.mark.skipif( + platform.system() != "Linux", reason="Only makes sense to test on Linux" +) +def test_platform_linux32(reported, expected, monkeypatch): + monkeypatch.setattr(struct, "calcsize", lambda x: 4) + dist = setuptools.Distribution() + cmd = bdist_wheel(dist) + cmd.plat_name = reported + cmd.root_is_pure = False + _, _, actual = cmd.get_tag() + assert actual == expected + + +def test_no_ctypes(monkeypatch) -> None: + def _fake_import(name: str, *args, **kwargs): + if name == "ctypes": + raise ModuleNotFoundError(f"No module named {name}") + + return importlib.__import__(name, *args, **kwargs) + + with suppress(KeyError): + monkeypatch.delitem(sys.modules, "wheel.macosx_libfile") + + # Install an importer shim that refuses to load ctypes + monkeypatch.setattr(builtins, "__import__", _fake_import) + with pytest.raises(ModuleNotFoundError, match="No module named ctypes"): + import wheel.macosx_libfile # noqa: F401 + + # Unload and reimport the bdist_wheel command module to make sure it won't try to + # import ctypes + monkeypatch.delitem(sys.modules, "setuptools.command.bdist_wheel") + + import setuptools.command.bdist_wheel # noqa: F401 + + +def test_dist_info_provided(dummy_dist, monkeypatch, tmp_path): + monkeypatch.chdir(dummy_dist) + distinfo = tmp_path / "dummy_dist.dist-info" + + distinfo.mkdir() + (distinfo / "METADATA").write_text("name: helloworld", encoding="utf-8") + + # We don't control the metadata. According to PEP-517, "The hook MAY also + # create other files inside this directory, and a build frontend MUST + # preserve". + (distinfo / "FOO").write_text("bar", encoding="utf-8") + + bdist_wheel_cmd(bdist_dir=str(tmp_path), dist_info_dir=str(distinfo)).run() + expected = { + "dummy_dist-1.0.dist-info/FOO", + "dummy_dist-1.0.dist-info/RECORD", + } + with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf: + files_found = set(wf.namelist()) + # Check that all expected files are there. + assert expected - files_found == set() + # Make sure there is no accidental egg-info bleeding into the wheel. + assert not [path for path in files_found if 'egg-info' in str(path)] + + +def test_allow_grace_period_parent_directory_license(monkeypatch, tmp_path): + # Motivation: https://github.com/pypa/setuptools/issues/4892 + # TODO: Remove this test after deprecation period is over + files = { + "LICENSE.txt": "parent license", # <---- the license files are outside + "NOTICE.txt": "parent notice", + "python": { + "pyproject.toml": cleandoc( + """ + [project] + name = "test-proj" + dynamic = ["version"] # <---- testing dynamic will not break + [tool.setuptools.dynamic] + version.file = "VERSION" + """ + ), + "setup.cfg": cleandoc( + """ + [metadata] + license_files = + ../LICENSE.txt + ../NOTICE.txt + """ + ), + "VERSION": "42", + }, + } + jaraco.path.build(files, prefix=str(tmp_path)) + monkeypatch.chdir(tmp_path / "python") + msg = "Pattern '../.*.txt' cannot contain '..'" + with pytest.warns(SetuptoolsDeprecationWarning, match=msg): + bdist_wheel_cmd().run() + with ZipFile("dist/test_proj-42-py3-none-any.whl") as wf: + files_found = set(wf.namelist()) + expected_files = { + "test_proj-42.dist-info/licenses/LICENSE.txt", + "test_proj-42.dist-info/licenses/NOTICE.txt", + } + assert expected_files <= files_found + + metadata = wf.read("test_proj-42.dist-info/METADATA").decode("utf8") + assert "License-File: LICENSE.txt" in metadata + assert "License-File: NOTICE.txt" in metadata diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_build.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_build.py new file mode 100644 index 0000000000000000000000000000000000000000..f0f1d9dcf21bafe9dc82a76d373b366ddeecfcec --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_build.py @@ -0,0 +1,33 @@ +from setuptools import Command +from setuptools.command.build import build +from setuptools.dist import Distribution + + +def test_distribution_gives_setuptools_build_obj(tmpdir_cwd): + """ + Check that the setuptools Distribution uses the + setuptools specific build object. + """ + + dist = Distribution( + dict( + script_name='setup.py', + script_args=['build'], + packages=[], + package_data={'': ['path/*']}, + ) + ) + assert isinstance(dist.get_command_obj("build"), build) + + +class Subcommand(Command): + """Dummy command to be used in tests""" + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + raise NotImplementedError("just to check if the command runs") diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_build_clib.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_build_clib.py new file mode 100644 index 0000000000000000000000000000000000000000..b5315df4f6599cd376e628c0d74cb14129cd89b8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_build_clib.py @@ -0,0 +1,84 @@ +import random +from unittest import mock + +import pytest + +from setuptools.command.build_clib import build_clib +from setuptools.dist import Distribution + +from distutils.errors import DistutilsSetupError + + +class TestBuildCLib: + @mock.patch('setuptools.command.build_clib.newer_pairwise_group') + def test_build_libraries(self, mock_newer): + dist = Distribution() + cmd = build_clib(dist) + + # this will be a long section, just making sure all + # exceptions are properly raised + libs = [('example', {'sources': 'broken.c'})] + with pytest.raises(DistutilsSetupError): + cmd.build_libraries(libs) + + obj_deps = 'some_string' + libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})] + with pytest.raises(DistutilsSetupError): + cmd.build_libraries(libs) + + obj_deps = {'': ''} + libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})] + with pytest.raises(DistutilsSetupError): + cmd.build_libraries(libs) + + obj_deps = {'source.c': ''} + libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})] + with pytest.raises(DistutilsSetupError): + cmd.build_libraries(libs) + + # with that out of the way, let's see if the crude dependency + # system works + cmd.compiler = mock.MagicMock(spec=cmd.compiler) + mock_newer.return_value = ([], []) + + obj_deps = {'': ('global.h',), 'example.c': ('example.h',)} + libs = [('example', {'sources': ['example.c'], 'obj_deps': obj_deps})] + + cmd.build_libraries(libs) + assert [['example.c', 'global.h', 'example.h']] in mock_newer.call_args[0] + assert not cmd.compiler.compile.called + assert cmd.compiler.create_static_lib.call_count == 1 + + # reset the call numbers so we can test again + cmd.compiler.reset_mock() + + mock_newer.return_value = '' # anything as long as it's not ([],[]) + cmd.build_libraries(libs) + assert cmd.compiler.compile.call_count == 1 + assert cmd.compiler.create_static_lib.call_count == 1 + + @mock.patch('setuptools.command.build_clib.newer_pairwise_group') + def test_build_libraries_reproducible(self, mock_newer): + dist = Distribution() + cmd = build_clib(dist) + + # with that out of the way, let's see if the crude dependency + # system works + cmd.compiler = mock.MagicMock(spec=cmd.compiler) + mock_newer.return_value = ([], []) + + original_sources = ['a-example.c', 'example.c'] + sources = original_sources + + obj_deps = {'': ('global.h',), 'example.c': ('example.h',)} + libs = [('example', {'sources': sources, 'obj_deps': obj_deps})] + + cmd.build_libraries(libs) + computed_call_args = mock_newer.call_args[0] + + while sources == original_sources: + sources = random.sample(original_sources, len(original_sources)) + libs = [('example', {'sources': sources, 'obj_deps': obj_deps})] + + cmd.build_libraries(libs) + assert computed_call_args == mock_newer.call_args[0] diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_build_ext.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_build_ext.py new file mode 100644 index 0000000000000000000000000000000000000000..c7b60ac32fcd6628cf96396a7e5d8fdb50f67c19 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_build_ext.py @@ -0,0 +1,293 @@ +from __future__ import annotations + +import os +import sys +from importlib.util import cache_from_source as _compiled_file_name + +import pytest +from jaraco import path + +from setuptools.command.build_ext import build_ext, get_abi3_suffix +from setuptools.dist import Distribution +from setuptools.errors import CompileError +from setuptools.extension import Extension + +from . import environment +from .textwrap import DALS + +import distutils.command.build_ext as orig +from distutils.sysconfig import get_config_var + +IS_PYPY = '__pypy__' in sys.builtin_module_names + + +class TestBuildExt: + def test_get_ext_filename(self): + """ + Setuptools needs to give back the same + result as distutils, even if the fullname + is not in ext_map. + """ + dist = Distribution() + cmd = build_ext(dist) + cmd.ext_map['foo/bar'] = '' + res = cmd.get_ext_filename('foo') + wanted = orig.build_ext.get_ext_filename(cmd, 'foo') + assert res == wanted + + def test_abi3_filename(self): + """ + Filename needs to be loadable by several versions + of Python 3 if 'is_abi3' is truthy on Extension() + """ + print(get_abi3_suffix()) + + extension = Extension('spam.eggs', ['eggs.c'], py_limited_api=True) + dist = Distribution(dict(ext_modules=[extension])) + cmd = build_ext(dist) + cmd.finalize_options() + assert 'spam.eggs' in cmd.ext_map + res = cmd.get_ext_filename('spam.eggs') + + if not get_abi3_suffix(): + assert res.endswith(get_config_var('EXT_SUFFIX')) + elif sys.platform == 'win32': + assert res.endswith('eggs.pyd') + else: + assert 'abi3' in res + + def test_ext_suffix_override(self): + """ + SETUPTOOLS_EXT_SUFFIX variable always overrides + default extension options. + """ + dist = Distribution() + cmd = build_ext(dist) + cmd.ext_map['for_abi3'] = ext = Extension( + 'for_abi3', + ['s.c'], + # Override shouldn't affect abi3 modules + py_limited_api=True, + ) + # Mock value needed to pass tests + ext._links_to_dynamic = False + + if not IS_PYPY: + expect = cmd.get_ext_filename('for_abi3') + else: + # PyPy builds do not use ABI3 tag, so they will + # also get the overridden suffix. + expect = 'for_abi3.test-suffix' + + try: + os.environ['SETUPTOOLS_EXT_SUFFIX'] = '.test-suffix' + res = cmd.get_ext_filename('normal') + assert 'normal.test-suffix' == res + res = cmd.get_ext_filename('for_abi3') + assert expect == res + finally: + del os.environ['SETUPTOOLS_EXT_SUFFIX'] + + def dist_with_example(self): + files = { + "src": {"mypkg": {"subpkg": {"ext2.c": ""}}}, + "c-extensions": {"ext1": {"main.c": ""}}, + } + + ext1 = Extension("mypkg.ext1", ["c-extensions/ext1/main.c"]) + ext2 = Extension("mypkg.subpkg.ext2", ["src/mypkg/subpkg/ext2.c"]) + ext3 = Extension("ext3", ["c-extension/ext3.c"]) + + path.build(files) + return Distribution({ + "script_name": "%test%", + "ext_modules": [ext1, ext2, ext3], + "package_dir": {"": "src"}, + }) + + def test_get_outputs(self, tmpdir_cwd, monkeypatch): + monkeypatch.setenv('SETUPTOOLS_EXT_SUFFIX', '.mp3') # make test OS-independent + monkeypatch.setattr('setuptools.command.build_ext.use_stubs', False) + dist = self.dist_with_example() + + # Regular build: get_outputs not empty, but get_output_mappings is empty + build_ext = dist.get_command_obj("build_ext") + build_ext.editable_mode = False + build_ext.ensure_finalized() + build_lib = build_ext.build_lib.replace(os.sep, "/") + outputs = [x.replace(os.sep, "/") for x in build_ext.get_outputs()] + assert outputs == [ + f"{build_lib}/ext3.mp3", + f"{build_lib}/mypkg/ext1.mp3", + f"{build_lib}/mypkg/subpkg/ext2.mp3", + ] + assert build_ext.get_output_mapping() == {} + + # Editable build: get_output_mappings should contain everything in get_outputs + dist.reinitialize_command("build_ext") + build_ext.editable_mode = True + build_ext.ensure_finalized() + mapping = { + k.replace(os.sep, "/"): v.replace(os.sep, "/") + for k, v in build_ext.get_output_mapping().items() + } + assert mapping == { + f"{build_lib}/ext3.mp3": "src/ext3.mp3", + f"{build_lib}/mypkg/ext1.mp3": "src/mypkg/ext1.mp3", + f"{build_lib}/mypkg/subpkg/ext2.mp3": "src/mypkg/subpkg/ext2.mp3", + } + + def test_get_output_mapping_with_stub(self, tmpdir_cwd, monkeypatch): + monkeypatch.setenv('SETUPTOOLS_EXT_SUFFIX', '.mp3') # make test OS-independent + monkeypatch.setattr('setuptools.command.build_ext.use_stubs', True) + dist = self.dist_with_example() + + # Editable build should create compiled stubs (.pyc files only, no .py) + build_ext = dist.get_command_obj("build_ext") + build_ext.editable_mode = True + build_ext.ensure_finalized() + for ext in build_ext.extensions: + monkeypatch.setattr(ext, "_needs_stub", True) + + build_lib = build_ext.build_lib.replace(os.sep, "/") + mapping = { + k.replace(os.sep, "/"): v.replace(os.sep, "/") + for k, v in build_ext.get_output_mapping().items() + } + + def C(file): + """Make it possible to do comparisons and tests in a OS-independent way""" + return _compiled_file_name(file).replace(os.sep, "/") + + assert mapping == { + C(f"{build_lib}/ext3.py"): C("src/ext3.py"), + f"{build_lib}/ext3.mp3": "src/ext3.mp3", + C(f"{build_lib}/mypkg/ext1.py"): C("src/mypkg/ext1.py"), + f"{build_lib}/mypkg/ext1.mp3": "src/mypkg/ext1.mp3", + C(f"{build_lib}/mypkg/subpkg/ext2.py"): C("src/mypkg/subpkg/ext2.py"), + f"{build_lib}/mypkg/subpkg/ext2.mp3": "src/mypkg/subpkg/ext2.mp3", + } + + # Ensure only the compiled stubs are present not the raw .py stub + assert f"{build_lib}/mypkg/ext1.py" not in mapping + assert f"{build_lib}/mypkg/subpkg/ext2.py" not in mapping + + # Visualize what the cached stub files look like + example_stub = C(f"{build_lib}/mypkg/ext1.py") + assert example_stub in mapping + assert example_stub.startswith(f"{build_lib}/mypkg/__pycache__/ext1") + assert example_stub.endswith(".pyc") + + +class TestBuildExtInplace: + def get_build_ext_cmd(self, optional: bool, **opts) -> build_ext: + files: dict[str, str | dict[str, dict[str, str]]] = { + "eggs.c": "#include missingheader.h\n", + ".build": {"lib": {}, "tmp": {}}, + } + path.build(files) + extension = Extension('spam.eggs', ['eggs.c'], optional=optional) + dist = Distribution(dict(ext_modules=[extension])) + dist.script_name = 'setup.py' + cmd = build_ext(dist) + vars(cmd).update(build_lib=".build/lib", build_temp=".build/tmp", **opts) + cmd.ensure_finalized() + return cmd + + def get_log_messages(self, caplog, capsys): + """ + Historically, distutils "logged" by printing to sys.std*. + Later versions adopted the logging framework. Grab + messages regardless of how they were captured. + """ + std = capsys.readouterr() + return std.out.splitlines() + std.err.splitlines() + caplog.messages + + def test_optional(self, tmpdir_cwd, caplog, capsys): + """ + If optional extensions fail to build, setuptools should show the error + in the logs but not fail to build + """ + cmd = self.get_build_ext_cmd(optional=True, inplace=True) + cmd.run() + assert any( + 'build_ext: building extension "spam.eggs" failed' + for msg in self.get_log_messages(caplog, capsys) + ) + # No compile error exception should be raised + + def test_non_optional(self, tmpdir_cwd): + # Non-optional extensions should raise an exception + cmd = self.get_build_ext_cmd(optional=False, inplace=True) + with pytest.raises(CompileError): + cmd.run() + + +def test_build_ext_config_handling(tmpdir_cwd): + files = { + 'setup.py': DALS( + """ + from setuptools import Extension, setup + setup( + name='foo', + version='0.0.0', + ext_modules=[Extension('foo', ['foo.c'])], + ) + """ + ), + 'foo.c': DALS( + """ + #include "Python.h" + + #if PY_MAJOR_VERSION >= 3 + + static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "foo", + NULL, + 0, + NULL, + NULL, + NULL, + NULL, + NULL + }; + + #define INITERROR return NULL + + PyMODINIT_FUNC PyInit_foo(void) + + #else + + #define INITERROR return + + void initfoo(void) + + #endif + { + #if PY_MAJOR_VERSION >= 3 + PyObject *module = PyModule_Create(&moduledef); + #else + PyObject *module = Py_InitModule("extension", NULL); + #endif + if (module == NULL) + INITERROR; + #if PY_MAJOR_VERSION >= 3 + return module; + #endif + } + """ + ), + 'setup.cfg': DALS( + """ + [build] + build_base = foo_build + """ + ), + } + path.build(files) + code, (stdout, stderr) = environment.run_setup_py( + cmd=['build'], + data_stream=(0, 2), + ) + assert code == 0, f'\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}' diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_build_meta.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_build_meta.py new file mode 100644 index 0000000000000000000000000000000000000000..624bba862eaed942802e68ee37427438c7d389ad --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_build_meta.py @@ -0,0 +1,983 @@ +import contextlib +import importlib +import os +import re +import shutil +import signal +import sys +import tarfile +import warnings +from concurrent import futures +from pathlib import Path +from typing import Any, Callable +from zipfile import ZipFile + +import pytest +from jaraco import path +from packaging.requirements import Requirement + +from setuptools.warnings import SetuptoolsDeprecationWarning + +from .textwrap import DALS + +SETUP_SCRIPT_STUB = "__import__('setuptools').setup()" + + +TIMEOUT = int(os.getenv("TIMEOUT_BACKEND_TEST", "180")) # in seconds +IS_PYPY = '__pypy__' in sys.builtin_module_names + + +pytestmark = pytest.mark.skipif( + sys.platform == "win32" and IS_PYPY, + reason="The combination of PyPy + Windows + pytest-xdist + ProcessPoolExecutor " + "is flaky and problematic", +) + + +class BuildBackendBase: + def __init__(self, cwd='.', env=None, backend_name='setuptools.build_meta'): + self.cwd = cwd + self.env = env or {} + self.backend_name = backend_name + + +class BuildBackend(BuildBackendBase): + """PEP 517 Build Backend""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.pool = futures.ProcessPoolExecutor(max_workers=1) + + def __getattr__(self, name: str) -> Callable[..., Any]: + """Handles arbitrary function invocations on the build backend.""" + + def method(*args, **kw): + root = os.path.abspath(self.cwd) + caller = BuildBackendCaller(root, self.env, self.backend_name) + pid = None + try: + pid = self.pool.submit(os.getpid).result(TIMEOUT) + return self.pool.submit(caller, name, *args, **kw).result(TIMEOUT) + except futures.TimeoutError: + self.pool.shutdown(wait=False) # doesn't stop already running processes + self._kill(pid) + pytest.xfail(f"Backend did not respond before timeout ({TIMEOUT} s)") + except (futures.process.BrokenProcessPool, MemoryError, OSError): + if IS_PYPY: + pytest.xfail("PyPy frequently fails tests with ProcessPoolExector") + raise + + return method + + def _kill(self, pid): + if pid is None: + return + with contextlib.suppress(ProcessLookupError, OSError): + os.kill(pid, signal.SIGTERM if os.name == "nt" else signal.SIGKILL) + + +class BuildBackendCaller(BuildBackendBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + (self.backend_name, _, self.backend_obj) = self.backend_name.partition(':') + + def __call__(self, name, *args, **kw): + """Handles arbitrary function invocations on the build backend.""" + os.chdir(self.cwd) + os.environ.update(self.env) + mod = importlib.import_module(self.backend_name) + + if self.backend_obj: + backend = getattr(mod, self.backend_obj) + else: + backend = mod + + return getattr(backend, name)(*args, **kw) + + +defns = [ + { # simple setup.py script + 'setup.py': DALS( + """ + __import__('setuptools').setup( + name='foo', + version='0.0.0', + py_modules=['hello'], + setup_requires=['six'], + ) + """ + ), + 'hello.py': DALS( + """ + def run(): + print('hello') + """ + ), + }, + { # setup.py that relies on __name__ + 'setup.py': DALS( + """ + assert __name__ == '__main__' + __import__('setuptools').setup( + name='foo', + version='0.0.0', + py_modules=['hello'], + setup_requires=['six'], + ) + """ + ), + 'hello.py': DALS( + """ + def run(): + print('hello') + """ + ), + }, + { # setup.py script that runs arbitrary code + 'setup.py': DALS( + """ + variable = True + def function(): + return variable + assert variable + __import__('setuptools').setup( + name='foo', + version='0.0.0', + py_modules=['hello'], + setup_requires=['six'], + ) + """ + ), + 'hello.py': DALS( + """ + def run(): + print('hello') + """ + ), + }, + { # setup.py script that constructs temp files to be included in the distribution + 'setup.py': DALS( + """ + # Some packages construct files on the fly, include them in the package, + # and immediately remove them after `setup()` (e.g. pybind11==2.9.1). + # Therefore, we cannot use `distutils.core.run_setup(..., stop_after=...)` + # to obtain a distribution object first, and then run the distutils + # commands later, because these files will be removed in the meantime. + + with open('world.py', 'w', encoding="utf-8") as f: + f.write('x = 42') + + try: + __import__('setuptools').setup( + name='foo', + version='0.0.0', + py_modules=['world'], + setup_requires=['six'], + ) + finally: + # Some packages will clean temporary files + __import__('os').unlink('world.py') + """ + ), + }, + { # setup.cfg only + 'setup.cfg': DALS( + """ + [metadata] + name = foo + version = 0.0.0 + + [options] + py_modules=hello + setup_requires=six + """ + ), + 'hello.py': DALS( + """ + def run(): + print('hello') + """ + ), + }, + { # setup.cfg and setup.py + 'setup.cfg': DALS( + """ + [metadata] + name = foo + version = 0.0.0 + + [options] + py_modules=hello + setup_requires=six + """ + ), + 'setup.py': "__import__('setuptools').setup()", + 'hello.py': DALS( + """ + def run(): + print('hello') + """ + ), + }, +] + + +class TestBuildMetaBackend: + backend_name = 'setuptools.build_meta' + + def get_build_backend(self): + return BuildBackend(backend_name=self.backend_name) + + @pytest.fixture(params=defns) + def build_backend(self, tmpdir, request): + path.build(request.param, prefix=str(tmpdir)) + with tmpdir.as_cwd(): + yield self.get_build_backend() + + def test_get_requires_for_build_wheel(self, build_backend): + actual = build_backend.get_requires_for_build_wheel() + expected = ['six'] + assert sorted(actual) == sorted(expected) + + def test_get_requires_for_build_sdist(self, build_backend): + actual = build_backend.get_requires_for_build_sdist() + expected = ['six'] + assert sorted(actual) == sorted(expected) + + def test_build_wheel(self, build_backend): + dist_dir = os.path.abspath('pip-wheel') + os.makedirs(dist_dir) + wheel_name = build_backend.build_wheel(dist_dir) + + wheel_file = os.path.join(dist_dir, wheel_name) + assert os.path.isfile(wheel_file) + + # Temporary files should be removed + assert not os.path.isfile('world.py') + + with ZipFile(wheel_file) as zipfile: + wheel_contents = set(zipfile.namelist()) + + # Each one of the examples have a single module + # that should be included in the distribution + python_scripts = (f for f in wheel_contents if f.endswith('.py')) + modules = [f for f in python_scripts if not f.endswith('setup.py')] + assert len(modules) == 1 + + @pytest.mark.parametrize('build_type', ('wheel', 'sdist')) + def test_build_with_existing_file_present(self, build_type, tmpdir_cwd): + # Building a sdist/wheel should still succeed if there's + # already a sdist/wheel in the destination directory. + files = { + 'setup.py': "from setuptools import setup\nsetup()", + 'VERSION': "0.0.1", + 'setup.cfg': DALS( + """ + [metadata] + name = foo + version = file: VERSION + """ + ), + 'pyproject.toml': DALS( + """ + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" + """ + ), + } + + path.build(files) + + dist_dir = os.path.abspath('preexisting-' + build_type) + + build_backend = self.get_build_backend() + build_method = getattr(build_backend, 'build_' + build_type) + + # Build a first sdist/wheel. + # Note: this also check the destination directory is + # successfully created if it does not exist already. + first_result = build_method(dist_dir) + + # Change version. + with open("VERSION", "wt", encoding="utf-8") as version_file: + version_file.write("0.0.2") + + # Build a *second* sdist/wheel. + second_result = build_method(dist_dir) + + assert os.path.isfile(os.path.join(dist_dir, first_result)) + assert first_result != second_result + + # And if rebuilding the exact same sdist/wheel? + open(os.path.join(dist_dir, second_result), 'wb').close() + third_result = build_method(dist_dir) + assert third_result == second_result + assert os.path.getsize(os.path.join(dist_dir, third_result)) > 0 + + @pytest.mark.parametrize("setup_script", [None, SETUP_SCRIPT_STUB]) + def test_build_with_pyproject_config(self, tmpdir, setup_script): + files = { + 'pyproject.toml': DALS( + """ + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" + + [project] + name = "foo" + license = {text = "MIT"} + description = "This is a Python package" + dynamic = ["version", "readme"] + classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers" + ] + urls = {Homepage = "http://github.com"} + dependencies = [ + "appdirs", + ] + + [project.optional-dependencies] + all = [ + "tomli>=1", + "pyscaffold>=4,<5", + 'importlib; python_version == "2.6"', + ] + + [project.scripts] + foo = "foo.cli:main" + + [tool.setuptools] + zip-safe = false + package-dir = {"" = "src"} + packages = {find = {where = ["src"]}} + license-files = ["LICENSE*"] + + [tool.setuptools.dynamic] + version = {attr = "foo.__version__"} + readme = {file = "README.rst"} + + [tool.distutils.sdist] + formats = "gztar" + """ + ), + "MANIFEST.in": DALS( + """ + global-include *.py *.txt + global-exclude *.py[cod] + """ + ), + "README.rst": "This is a ``README``", + "LICENSE.txt": "---- placeholder MIT license ----", + "src": { + "foo": { + "__init__.py": "__version__ = '0.1'", + "__init__.pyi": "__version__: str", + "cli.py": "def main(): print('hello world')", + "data.txt": "def main(): print('hello world')", + "py.typed": "", + } + }, + } + if setup_script: + files["setup.py"] = setup_script + + build_backend = self.get_build_backend() + with tmpdir.as_cwd(): + path.build(files) + msgs = [ + "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'", + "`project.license` as a TOML table is deprecated", + ] + with warnings.catch_warnings(): + for msg in msgs: + warnings.filterwarnings("ignore", msg, SetuptoolsDeprecationWarning) + sdist_path = build_backend.build_sdist("temp") + wheel_file = build_backend.build_wheel("temp") + + with tarfile.open(os.path.join(tmpdir, "temp", sdist_path)) as tar: + sdist_contents = set(tar.getnames()) + + with ZipFile(os.path.join(tmpdir, "temp", wheel_file)) as zipfile: + wheel_contents = set(zipfile.namelist()) + metadata = str(zipfile.read("foo-0.1.dist-info/METADATA"), "utf-8") + license = str( + zipfile.read("foo-0.1.dist-info/licenses/LICENSE.txt"), "utf-8" + ) + epoints = str(zipfile.read("foo-0.1.dist-info/entry_points.txt"), "utf-8") + + assert sdist_contents - {"foo-0.1/setup.py"} == { + 'foo-0.1', + 'foo-0.1/LICENSE.txt', + 'foo-0.1/MANIFEST.in', + 'foo-0.1/PKG-INFO', + 'foo-0.1/README.rst', + 'foo-0.1/pyproject.toml', + 'foo-0.1/setup.cfg', + 'foo-0.1/src', + 'foo-0.1/src/foo', + 'foo-0.1/src/foo/__init__.py', + 'foo-0.1/src/foo/__init__.pyi', + 'foo-0.1/src/foo/cli.py', + 'foo-0.1/src/foo/data.txt', + 'foo-0.1/src/foo/py.typed', + 'foo-0.1/src/foo.egg-info', + 'foo-0.1/src/foo.egg-info/PKG-INFO', + 'foo-0.1/src/foo.egg-info/SOURCES.txt', + 'foo-0.1/src/foo.egg-info/dependency_links.txt', + 'foo-0.1/src/foo.egg-info/entry_points.txt', + 'foo-0.1/src/foo.egg-info/requires.txt', + 'foo-0.1/src/foo.egg-info/top_level.txt', + 'foo-0.1/src/foo.egg-info/not-zip-safe', + } + assert wheel_contents == { + "foo/__init__.py", + "foo/__init__.pyi", # include type information by default + "foo/cli.py", + "foo/data.txt", # include_package_data defaults to True + "foo/py.typed", # include type information by default + "foo-0.1.dist-info/licenses/LICENSE.txt", + "foo-0.1.dist-info/METADATA", + "foo-0.1.dist-info/WHEEL", + "foo-0.1.dist-info/entry_points.txt", + "foo-0.1.dist-info/top_level.txt", + "foo-0.1.dist-info/RECORD", + } + assert license == "---- placeholder MIT license ----" + + for line in ( + "Summary: This is a Python package", + "License: MIT", + "License-File: LICENSE.txt", + "Classifier: Intended Audience :: Developers", + "Requires-Dist: appdirs", + "Requires-Dist: " + str(Requirement('tomli>=1 ; extra == "all"')), + "Requires-Dist: " + + str(Requirement('importlib; python_version=="2.6" and extra =="all"')), + ): + assert line in metadata, (line, metadata) + + assert metadata.strip().endswith("This is a ``README``") + assert epoints.strip() == "[console_scripts]\nfoo = foo.cli:main" + + def test_static_metadata_in_pyproject_config(self, tmpdir): + # Make sure static metadata in pyproject.toml is not overwritten by setup.py + # as required by PEP 621 + files = { + 'pyproject.toml': DALS( + """ + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" + + [project] + name = "foo" + description = "This is a Python package" + version = "42" + dependencies = ["six"] + """ + ), + 'hello.py': DALS( + """ + def run(): + print('hello') + """ + ), + 'setup.py': DALS( + """ + __import__('setuptools').setup( + name='bar', + version='13', + ) + """ + ), + } + build_backend = self.get_build_backend() + with tmpdir.as_cwd(): + path.build(files) + sdist_path = build_backend.build_sdist("temp") + wheel_file = build_backend.build_wheel("temp") + + assert (tmpdir / "temp/foo-42.tar.gz").exists() + assert (tmpdir / "temp/foo-42-py3-none-any.whl").exists() + assert not (tmpdir / "temp/bar-13.tar.gz").exists() + assert not (tmpdir / "temp/bar-42.tar.gz").exists() + assert not (tmpdir / "temp/foo-13.tar.gz").exists() + assert not (tmpdir / "temp/bar-13-py3-none-any.whl").exists() + assert not (tmpdir / "temp/bar-42-py3-none-any.whl").exists() + assert not (tmpdir / "temp/foo-13-py3-none-any.whl").exists() + + with tarfile.open(os.path.join(tmpdir, "temp", sdist_path)) as tar: + pkg_info = str(tar.extractfile('foo-42/PKG-INFO').read(), "utf-8") + members = tar.getnames() + assert "bar-13/PKG-INFO" not in members + + with ZipFile(os.path.join(tmpdir, "temp", wheel_file)) as zipfile: + metadata = str(zipfile.read("foo-42.dist-info/METADATA"), "utf-8") + members = zipfile.namelist() + assert "bar-13.dist-info/METADATA" not in members + + for file in pkg_info, metadata: + for line in ("Name: foo", "Version: 42"): + assert line in file + for line in ("Name: bar", "Version: 13"): + assert line not in file + + def test_build_sdist(self, build_backend): + dist_dir = os.path.abspath('pip-sdist') + os.makedirs(dist_dir) + sdist_name = build_backend.build_sdist(dist_dir) + + assert os.path.isfile(os.path.join(dist_dir, sdist_name)) + + def test_prepare_metadata_for_build_wheel(self, build_backend): + dist_dir = os.path.abspath('pip-dist-info') + os.makedirs(dist_dir) + + dist_info = build_backend.prepare_metadata_for_build_wheel(dist_dir) + + assert os.path.isfile(os.path.join(dist_dir, dist_info, 'METADATA')) + + def test_prepare_metadata_inplace(self, build_backend): + """ + Some users might pass metadata_directory pre-populated with `.tox` or `.venv`. + See issue #3523. + """ + for pre_existing in [ + ".tox/python/lib/python3.10/site-packages/attrs-22.1.0.dist-info", + ".tox/python/lib/python3.10/site-packages/autocommand-2.2.1.dist-info", + ".nox/python/lib/python3.10/site-packages/build-0.8.0.dist-info", + ".venv/python3.10/site-packages/click-8.1.3.dist-info", + "venv/python3.10/site-packages/distlib-0.3.5.dist-info", + "env/python3.10/site-packages/docutils-0.19.dist-info", + ]: + os.makedirs(pre_existing, exist_ok=True) + dist_info = build_backend.prepare_metadata_for_build_wheel(".") + assert os.path.isfile(os.path.join(dist_info, 'METADATA')) + + def test_build_sdist_explicit_dist(self, build_backend): + # explicitly specifying the dist folder should work + # the folder sdist_directory and the ``--dist-dir`` can be the same + dist_dir = os.path.abspath('dist') + sdist_name = build_backend.build_sdist(dist_dir) + assert os.path.isfile(os.path.join(dist_dir, sdist_name)) + + def test_build_sdist_version_change(self, build_backend): + sdist_into_directory = os.path.abspath("out_sdist") + os.makedirs(sdist_into_directory) + + sdist_name = build_backend.build_sdist(sdist_into_directory) + assert os.path.isfile(os.path.join(sdist_into_directory, sdist_name)) + + # if the setup.py changes subsequent call of the build meta + # should still succeed, given the + # sdist_directory the frontend specifies is empty + setup_loc = os.path.abspath("setup.py") + if not os.path.exists(setup_loc): + setup_loc = os.path.abspath("setup.cfg") + + with open(setup_loc, 'rt', encoding="utf-8") as file_handler: + content = file_handler.read() + with open(setup_loc, 'wt', encoding="utf-8") as file_handler: + file_handler.write(content.replace("version='0.0.0'", "version='0.0.1'")) + + shutil.rmtree(sdist_into_directory) + os.makedirs(sdist_into_directory) + + sdist_name = build_backend.build_sdist("out_sdist") + assert os.path.isfile(os.path.join(os.path.abspath("out_sdist"), sdist_name)) + + def test_build_sdist_pyproject_toml_exists(self, tmpdir_cwd): + files = { + 'setup.py': DALS( + """ + __import__('setuptools').setup( + name='foo', + version='0.0.0', + py_modules=['hello'] + )""" + ), + 'hello.py': '', + 'pyproject.toml': DALS( + """ + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" + """ + ), + } + path.build(files) + build_backend = self.get_build_backend() + targz_path = build_backend.build_sdist("temp") + with tarfile.open(os.path.join("temp", targz_path)) as tar: + assert any('pyproject.toml' in name for name in tar.getnames()) + + def test_build_sdist_setup_py_exists(self, tmpdir_cwd): + # If build_sdist is called from a script other than setup.py, + # ensure setup.py is included + path.build(defns[0]) + + build_backend = self.get_build_backend() + targz_path = build_backend.build_sdist("temp") + with tarfile.open(os.path.join("temp", targz_path)) as tar: + assert any('setup.py' in name for name in tar.getnames()) + + def test_build_sdist_setup_py_manifest_excluded(self, tmpdir_cwd): + # Ensure that MANIFEST.in can exclude setup.py + files = { + 'setup.py': DALS( + """ + __import__('setuptools').setup( + name='foo', + version='0.0.0', + py_modules=['hello'] + )""" + ), + 'hello.py': '', + 'MANIFEST.in': DALS( + """ + exclude setup.py + """ + ), + } + + path.build(files) + + build_backend = self.get_build_backend() + targz_path = build_backend.build_sdist("temp") + with tarfile.open(os.path.join("temp", targz_path)) as tar: + assert not any('setup.py' in name for name in tar.getnames()) + + def test_build_sdist_builds_targz_even_if_zip_indicated(self, tmpdir_cwd): + files = { + 'setup.py': DALS( + """ + __import__('setuptools').setup( + name='foo', + version='0.0.0', + py_modules=['hello'] + )""" + ), + 'hello.py': '', + 'setup.cfg': DALS( + """ + [sdist] + formats=zip + """ + ), + } + + path.build(files) + + build_backend = self.get_build_backend() + build_backend.build_sdist("temp") + + _relative_path_import_files = { + 'setup.py': DALS( + """ + __import__('setuptools').setup( + name='foo', + version=__import__('hello').__version__, + py_modules=['hello'] + )""" + ), + 'hello.py': '__version__ = "0.0.0"', + 'setup.cfg': DALS( + """ + [sdist] + formats=zip + """ + ), + } + + def test_build_sdist_relative_path_import(self, tmpdir_cwd): + path.build(self._relative_path_import_files) + build_backend = self.get_build_backend() + with pytest.raises(ImportError, match="^No module named 'hello'$"): + build_backend.build_sdist("temp") + + _simple_pyproject_example = { + "pyproject.toml": DALS( + """ + [project] + name = "proj" + version = "42" + """ + ), + "src": {"proj": {"__init__.py": ""}}, + } + + def _assert_link_tree(self, parent_dir): + """All files in the directory should be either links or hard links""" + files = list(Path(parent_dir).glob("**/*")) + assert files # Should not be empty + for file in files: + assert file.is_symlink() or os.stat(file).st_nlink > 0 + + def test_editable_without_config_settings(self, tmpdir_cwd): + """ + Sanity check to ensure tests with --mode=strict are different from the ones + without --mode. + + --mode=strict should create a local directory with a package tree. + The directory should not get created otherwise. + """ + path.build(self._simple_pyproject_example) + build_backend = self.get_build_backend() + assert not Path("build").exists() + build_backend.build_editable("temp") + assert not Path("build").exists() + + def test_build_wheel_inplace(self, tmpdir_cwd): + config_settings = {"--build-option": ["build_ext", "--inplace"]} + path.build(self._simple_pyproject_example) + build_backend = self.get_build_backend() + assert not Path("build").exists() + Path("build").mkdir() + build_backend.prepare_metadata_for_build_wheel("build", config_settings) + build_backend.build_wheel("build", config_settings) + assert Path("build/proj-42-py3-none-any.whl").exists() + + @pytest.mark.parametrize("config_settings", [{"editable-mode": "strict"}]) + def test_editable_with_config_settings(self, tmpdir_cwd, config_settings): + path.build({**self._simple_pyproject_example, '_meta': {}}) + assert not Path("build").exists() + build_backend = self.get_build_backend() + build_backend.prepare_metadata_for_build_editable("_meta", config_settings) + build_backend.build_editable("temp", config_settings, "_meta") + self._assert_link_tree(next(Path("build").glob("__editable__.*"))) + + @pytest.mark.parametrize( + ("setup_literal", "requirements"), + [ + ("'foo'", ['foo']), + ("['foo']", ['foo']), + (r"'foo\n'", ['foo']), + (r"'foo\n\n'", ['foo']), + ("['foo', 'bar']", ['foo', 'bar']), + (r"'# Has a comment line\nfoo'", ['foo']), + (r"'foo # Has an inline comment'", ['foo']), + (r"'foo \\\n >=3.0'", ['foo>=3.0']), + (r"'foo\nbar'", ['foo', 'bar']), + (r"'foo\nbar\n'", ['foo', 'bar']), + (r"['foo\n', 'bar\n']", ['foo', 'bar']), + ], + ) + @pytest.mark.parametrize('use_wheel', [True, False]) + def test_setup_requires(self, setup_literal, requirements, use_wheel, tmpdir_cwd): + files = { + 'setup.py': DALS( + """ + from setuptools import setup + + setup( + name="qux", + version="0.0.0", + py_modules=["hello"], + setup_requires={setup_literal}, + ) + """ + ).format(setup_literal=setup_literal), + 'hello.py': DALS( + """ + def run(): + print('hello') + """ + ), + } + + path.build(files) + + build_backend = self.get_build_backend() + + if use_wheel: + get_requires = build_backend.get_requires_for_build_wheel + else: + get_requires = build_backend.get_requires_for_build_sdist + + # Ensure that the build requirements are properly parsed + expected = sorted(requirements) + actual = get_requires() + + assert expected == sorted(actual) + + def test_setup_requires_with_auto_discovery(self, tmpdir_cwd): + # Make sure patches introduced to retrieve setup_requires don't accidentally + # activate auto-discovery and cause problems due to the incomplete set of + # attributes passed to MinimalDistribution + files = { + 'pyproject.toml': DALS( + """ + [project] + name = "proj" + version = "42" + """ + ), + "setup.py": DALS( + """ + __import__('setuptools').setup( + setup_requires=["foo"], + py_modules = ["hello", "world"] + ) + """ + ), + 'hello.py': "'hello'", + 'world.py': "'world'", + } + path.build(files) + build_backend = self.get_build_backend() + setup_requires = build_backend.get_requires_for_build_wheel() + assert setup_requires == ["foo"] + + def test_dont_install_setup_requires(self, tmpdir_cwd): + files = { + 'setup.py': DALS( + """ + from setuptools import setup + + setup( + name="qux", + version="0.0.0", + py_modules=["hello"], + setup_requires=["does-not-exist >99"], + ) + """ + ), + 'hello.py': DALS( + """ + def run(): + print('hello') + """ + ), + } + + path.build(files) + + build_backend = self.get_build_backend() + + dist_dir = os.path.abspath('pip-dist-info') + os.makedirs(dist_dir) + + # does-not-exist can't be satisfied, so if it attempts to install + # setup_requires, it will fail. + build_backend.prepare_metadata_for_build_wheel(dist_dir) + + _sys_argv_0_passthrough = { + 'setup.py': DALS( + """ + import os + import sys + + __import__('setuptools').setup( + name='foo', + version='0.0.0', + ) + + sys_argv = os.path.abspath(sys.argv[0]) + file_path = os.path.abspath('setup.py') + assert sys_argv == file_path + """ + ) + } + + def test_sys_argv_passthrough(self, tmpdir_cwd): + path.build(self._sys_argv_0_passthrough) + build_backend = self.get_build_backend() + with pytest.raises(AssertionError): + build_backend.build_sdist("temp") + + _setup_py_file_abspath = { + 'setup.py': DALS( + """ + import os + assert os.path.isabs(__file__) + __import__('setuptools').setup( + name='foo', + version='0.0.0', + py_modules=['hello'], + setup_requires=['six'], + ) + """ + ) + } + + def test_setup_py_file_abspath(self, tmpdir_cwd): + path.build(self._setup_py_file_abspath) + build_backend = self.get_build_backend() + build_backend.build_sdist("temp") + + @pytest.mark.parametrize('build_hook', ('build_sdist', 'build_wheel')) + def test_build_with_empty_setuppy(self, build_backend, build_hook): + files = {'setup.py': ''} + path.build(files) + + msg = re.escape('No distribution was found.') + with pytest.raises(ValueError, match=msg): + getattr(build_backend, build_hook)("temp") + + +class TestBuildMetaLegacyBackend(TestBuildMetaBackend): + backend_name = 'setuptools.build_meta:__legacy__' + + # build_meta_legacy-specific tests + def test_build_sdist_relative_path_import(self, tmpdir_cwd): + # This must fail in build_meta, but must pass in build_meta_legacy + path.build(self._relative_path_import_files) + + build_backend = self.get_build_backend() + build_backend.build_sdist("temp") + + def test_sys_argv_passthrough(self, tmpdir_cwd): + path.build(self._sys_argv_0_passthrough) + + build_backend = self.get_build_backend() + build_backend.build_sdist("temp") + + +def test_legacy_editable_install(venv, tmpdir, tmpdir_cwd): + pyproject = """ + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + [project] + name = "myproj" + version = "42" + """ + path.build({"pyproject.toml": DALS(pyproject), "mymod.py": ""}) + + # First: sanity check + cmd = ["pip", "install", "--no-build-isolation", "-e", "."] + output = venv.run(cmd, cwd=tmpdir).lower() + assert "running setup.py develop for myproj" not in output + assert "created wheel for myproj" in output + + # Then: real test + env = {**os.environ, "SETUPTOOLS_ENABLE_FEATURES": "legacy-editable"} + cmd = ["pip", "install", "--no-build-isolation", "-e", "."] + output = venv.run(cmd, cwd=tmpdir, env=env).lower() + assert "running setup.py develop for myproj" in output + + +@pytest.mark.filterwarnings("ignore::setuptools.SetuptoolsDeprecationWarning") +def test_sys_exit_0_in_setuppy(monkeypatch, tmp_path): + """Setuptools should be resilient to setup.py with ``sys.exit(0)`` (#3973).""" + monkeypatch.chdir(tmp_path) + setuppy = """ + import sys, setuptools + setuptools.setup(name='foo', version='0.0.0') + sys.exit(0) + """ + (tmp_path / "setup.py").write_text(DALS(setuppy), encoding="utf-8") + backend = BuildBackend(backend_name="setuptools.build_meta") + assert backend.get_requires_for_build_wheel() == [] + + +def test_system_exit_in_setuppy(monkeypatch, tmp_path): + monkeypatch.chdir(tmp_path) + setuppy = "import sys; sys.exit('some error')" + (tmp_path / "setup.py").write_text(setuppy, encoding="utf-8") + with pytest.raises(SystemExit, match="some error"): + backend = BuildBackend(backend_name="setuptools.build_meta") + backend.get_requires_for_build_wheel() diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_build_py.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_build_py.py new file mode 100644 index 0000000000000000000000000000000000000000..1e3a660833933717310de66978771c4e48b5b914 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_build_py.py @@ -0,0 +1,480 @@ +import os +import shutil +import stat +import warnings +from pathlib import Path +from unittest.mock import Mock + +import jaraco.path +import pytest + +from setuptools import SetuptoolsDeprecationWarning +from setuptools.dist import Distribution + +from .textwrap import DALS + + +def test_directories_in_package_data_glob(tmpdir_cwd): + """ + Directories matching the glob in package_data should + not be included in the package data. + + Regression test for #261. + """ + dist = Distribution( + dict( + script_name='setup.py', + script_args=['build_py'], + packages=[''], + package_data={'': ['path/*']}, + ) + ) + os.makedirs('path/subpath') + dist.parse_command_line() + dist.run_commands() + + +def test_recursive_in_package_data_glob(tmpdir_cwd): + """ + Files matching recursive globs (**) in package_data should + be included in the package data. + + #1806 + """ + dist = Distribution( + dict( + script_name='setup.py', + script_args=['build_py'], + packages=[''], + package_data={'': ['path/**/data']}, + ) + ) + os.makedirs('path/subpath/subsubpath') + open('path/subpath/subsubpath/data', 'wb').close() + + dist.parse_command_line() + dist.run_commands() + + assert stat.S_ISREG(os.stat('build/lib/path/subpath/subsubpath/data').st_mode), ( + "File is not included" + ) + + +def test_read_only(tmpdir_cwd): + """ + Ensure read-only flag is not preserved in copy + for package modules and package data, as that + causes problems with deleting read-only files on + Windows. + + #1451 + """ + dist = Distribution( + dict( + script_name='setup.py', + script_args=['build_py'], + packages=['pkg'], + package_data={'pkg': ['data.dat']}, + ) + ) + os.makedirs('pkg') + open('pkg/__init__.py', 'wb').close() + open('pkg/data.dat', 'wb').close() + os.chmod('pkg/__init__.py', stat.S_IREAD) + os.chmod('pkg/data.dat', stat.S_IREAD) + dist.parse_command_line() + dist.run_commands() + shutil.rmtree('build') + + +@pytest.mark.xfail( + 'platform.system() == "Windows"', + reason="On Windows, files do not have executable bits", + raises=AssertionError, + strict=True, +) +def test_executable_data(tmpdir_cwd): + """ + Ensure executable bit is preserved in copy for + package data, as users rely on it for scripts. + + #2041 + """ + dist = Distribution( + dict( + script_name='setup.py', + script_args=['build_py'], + packages=['pkg'], + package_data={'pkg': ['run-me']}, + ) + ) + os.makedirs('pkg') + open('pkg/__init__.py', 'wb').close() + open('pkg/run-me', 'wb').close() + os.chmod('pkg/run-me', 0o700) + + dist.parse_command_line() + dist.run_commands() + + assert os.stat('build/lib/pkg/run-me').st_mode & stat.S_IEXEC, ( + "Script is not executable" + ) + + +EXAMPLE_WITH_MANIFEST = { + "setup.cfg": DALS( + """ + [metadata] + name = mypkg + version = 42 + + [options] + include_package_data = True + packages = find: + + [options.packages.find] + exclude = *.tests* + """ + ), + "mypkg": { + "__init__.py": "", + "resource_file.txt": "", + "tests": { + "__init__.py": "", + "test_mypkg.py": "", + "test_file.txt": "", + }, + }, + "MANIFEST.in": DALS( + """ + global-include *.py *.txt + global-exclude *.py[cod] + prune dist + prune build + prune *.egg-info + """ + ), +} + + +def test_excluded_subpackages(tmpdir_cwd): + jaraco.path.build(EXAMPLE_WITH_MANIFEST) + dist = Distribution({"script_name": "%PEP 517%"}) + dist.parse_config_files() + + build_py = dist.get_command_obj("build_py") + + msg = r"Python recognizes 'mypkg\.tests' as an importable package" + with pytest.warns(SetuptoolsDeprecationWarning, match=msg): + # TODO: To fix #3260 we need some transition period to deprecate the + # existing behavior of `include_package_data`. After the transition, we + # should remove the warning and fix the behavior. + + if os.getenv("SETUPTOOLS_USE_DISTUTILS") == "stdlib": + # pytest.warns reset the warning filter temporarily + # https://github.com/pytest-dev/pytest/issues/4011#issuecomment-423494810 + warnings.filterwarnings( + "ignore", + "'encoding' argument not specified", + module="distutils.text_file", + # This warning is already fixed in pypa/distutils but not in stdlib + ) + + build_py.finalize_options() + build_py.run() + + build_dir = Path(dist.get_command_obj("build_py").build_lib) + assert (build_dir / "mypkg/__init__.py").exists() + assert (build_dir / "mypkg/resource_file.txt").exists() + + # Setuptools is configured to ignore `mypkg.tests`, therefore the following + # files/dirs should not be included in the distribution. + for f in [ + "mypkg/tests/__init__.py", + "mypkg/tests/test_mypkg.py", + "mypkg/tests/test_file.txt", + "mypkg/tests", + ]: + with pytest.raises(AssertionError): + # TODO: Enforce the following assertion once #3260 is fixed + # (remove context manager and the following xfail). + assert not (build_dir / f).exists() + + pytest.xfail("#3260") + + +@pytest.mark.filterwarnings("ignore::setuptools.SetuptoolsDeprecationWarning") +def test_existing_egg_info(tmpdir_cwd, monkeypatch): + """When provided with the ``existing_egg_info_dir`` attribute, build_py should not + attempt to run egg_info again. + """ + # == Pre-condition == + # Generate an egg-info dir + jaraco.path.build(EXAMPLE_WITH_MANIFEST) + dist = Distribution({"script_name": "%PEP 517%"}) + dist.parse_config_files() + assert dist.include_package_data + + egg_info = dist.get_command_obj("egg_info") + dist.run_command("egg_info") + egg_info_dir = next(Path(egg_info.egg_base).glob("*.egg-info")) + assert egg_info_dir.is_dir() + + # == Setup == + build_py = dist.get_command_obj("build_py") + build_py.finalize_options() + egg_info = dist.get_command_obj("egg_info") + egg_info_run = Mock(side_effect=egg_info.run) + monkeypatch.setattr(egg_info, "run", egg_info_run) + + # == Remove caches == + # egg_info is called when build_py looks for data_files, which gets cached. + # We need to ensure it is not cached yet, otherwise it may impact on the tests + build_py.__dict__.pop('data_files', None) + dist.reinitialize_command(egg_info) + + # == Sanity check == + # Ensure that if existing_egg_info is not given, build_py attempts to run egg_info + build_py.existing_egg_info_dir = None + build_py.run() + egg_info_run.assert_called() + + # == Remove caches == + egg_info_run.reset_mock() + build_py.__dict__.pop('data_files', None) + dist.reinitialize_command(egg_info) + + # == Actual test == + # Ensure that if existing_egg_info_dir is given, egg_info doesn't run + build_py.existing_egg_info_dir = egg_info_dir + build_py.run() + egg_info_run.assert_not_called() + assert build_py.data_files + + # Make sure the list of outputs is actually OK + outputs = map(lambda x: x.replace(os.sep, "/"), build_py.get_outputs()) + assert outputs + example = str(Path(build_py.build_lib, "mypkg/__init__.py")).replace(os.sep, "/") + assert example in outputs + + +EXAMPLE_ARBITRARY_MAPPING = { + "pyproject.toml": DALS( + """ + [project] + name = "mypkg" + version = "42" + + [tool.setuptools] + packages = ["mypkg", "mypkg.sub1", "mypkg.sub2", "mypkg.sub2.nested"] + + [tool.setuptools.package-dir] + "" = "src" + "mypkg.sub2" = "src/mypkg/_sub2" + "mypkg.sub2.nested" = "other" + """ + ), + "src": { + "mypkg": { + "__init__.py": "", + "resource_file.txt": "", + "sub1": { + "__init__.py": "", + "mod1.py": "", + }, + "_sub2": { + "mod2.py": "", + }, + }, + }, + "other": { + "__init__.py": "", + "mod3.py": "", + }, + "MANIFEST.in": DALS( + """ + global-include *.py *.txt + global-exclude *.py[cod] + """ + ), +} + + +def test_get_outputs(tmpdir_cwd): + jaraco.path.build(EXAMPLE_ARBITRARY_MAPPING) + dist = Distribution({"script_name": "%test%"}) + dist.parse_config_files() + + build_py = dist.get_command_obj("build_py") + build_py.editable_mode = True + build_py.ensure_finalized() + build_lib = build_py.build_lib.replace(os.sep, "/") + outputs = {x.replace(os.sep, "/") for x in build_py.get_outputs()} + assert outputs == { + f"{build_lib}/mypkg/__init__.py", + f"{build_lib}/mypkg/resource_file.txt", + f"{build_lib}/mypkg/sub1/__init__.py", + f"{build_lib}/mypkg/sub1/mod1.py", + f"{build_lib}/mypkg/sub2/mod2.py", + f"{build_lib}/mypkg/sub2/nested/__init__.py", + f"{build_lib}/mypkg/sub2/nested/mod3.py", + } + mapping = { + k.replace(os.sep, "/"): v.replace(os.sep, "/") + for k, v in build_py.get_output_mapping().items() + } + assert mapping == { + f"{build_lib}/mypkg/__init__.py": "src/mypkg/__init__.py", + f"{build_lib}/mypkg/resource_file.txt": "src/mypkg/resource_file.txt", + f"{build_lib}/mypkg/sub1/__init__.py": "src/mypkg/sub1/__init__.py", + f"{build_lib}/mypkg/sub1/mod1.py": "src/mypkg/sub1/mod1.py", + f"{build_lib}/mypkg/sub2/mod2.py": "src/mypkg/_sub2/mod2.py", + f"{build_lib}/mypkg/sub2/nested/__init__.py": "other/__init__.py", + f"{build_lib}/mypkg/sub2/nested/mod3.py": "other/mod3.py", + } + + +class TestTypeInfoFiles: + PYPROJECTS = { + "default_pyproject": DALS( + """ + [project] + name = "foo" + version = "1" + """ + ), + "dont_include_package_data": DALS( + """ + [project] + name = "foo" + version = "1" + + [tool.setuptools] + include-package-data = false + """ + ), + "exclude_type_info": DALS( + """ + [project] + name = "foo" + version = "1" + + [tool.setuptools] + include-package-data = false + + [tool.setuptools.exclude-package-data] + "*" = ["py.typed", "*.pyi"] + """ + ), + } + + EXAMPLES = { + "simple_namespace": { + "directory_structure": { + "foo": { + "bar.pyi": "", + "py.typed": "", + "__init__.py": "", + } + }, + "expected_type_files": {"foo/bar.pyi", "foo/py.typed"}, + }, + "nested_inside_namespace": { + "directory_structure": { + "foo": { + "bar": { + "py.typed": "", + "mod.pyi": "", + } + } + }, + "expected_type_files": {"foo/bar/mod.pyi", "foo/bar/py.typed"}, + }, + "namespace_nested_inside_regular": { + "directory_structure": { + "foo": { + "namespace": { + "foo.pyi": "", + }, + "__init__.pyi": "", + "py.typed": "", + } + }, + "expected_type_files": { + "foo/namespace/foo.pyi", + "foo/__init__.pyi", + "foo/py.typed", + }, + }, + } + + @pytest.mark.parametrize( + "pyproject", + [ + "default_pyproject", + pytest.param( + "dont_include_package_data", + marks=pytest.mark.xfail(reason="pypa/setuptools#4350"), + ), + ], + ) + @pytest.mark.parametrize("example", EXAMPLES.keys()) + def test_type_files_included_by_default(self, tmpdir_cwd, pyproject, example): + structure = { + **self.EXAMPLES[example]["directory_structure"], + "pyproject.toml": self.PYPROJECTS[pyproject], + } + expected_type_files = self.EXAMPLES[example]["expected_type_files"] + jaraco.path.build(structure) + + build_py = get_finalized_build_py() + outputs = get_outputs(build_py) + assert expected_type_files <= outputs + + @pytest.mark.parametrize("pyproject", ["exclude_type_info"]) + @pytest.mark.parametrize("example", EXAMPLES.keys()) + def test_type_files_can_be_excluded(self, tmpdir_cwd, pyproject, example): + structure = { + **self.EXAMPLES[example]["directory_structure"], + "pyproject.toml": self.PYPROJECTS[pyproject], + } + expected_type_files = self.EXAMPLES[example]["expected_type_files"] + jaraco.path.build(structure) + + build_py = get_finalized_build_py() + outputs = get_outputs(build_py) + assert expected_type_files.isdisjoint(outputs) + + def test_stub_only_package(self, tmpdir_cwd): + structure = { + "pyproject.toml": DALS( + """ + [project] + name = "foo-stubs" + version = "1" + """ + ), + "foo-stubs": {"__init__.pyi": "", "bar.pyi": ""}, + } + expected_type_files = {"foo-stubs/__init__.pyi", "foo-stubs/bar.pyi"} + jaraco.path.build(structure) + + build_py = get_finalized_build_py() + outputs = get_outputs(build_py) + assert expected_type_files <= outputs + + +def get_finalized_build_py(script_name="%build_py-test%"): + dist = Distribution({"script_name": script_name}) + dist.parse_config_files() + build_py = dist.get_command_obj("build_py") + build_py.finalize_options() + return build_py + + +def get_outputs(build_py): + build_dir = Path(build_py.build_lib) + return { + os.path.relpath(x, build_dir).replace(os.sep, "/") + for x in build_py.get_outputs() + } diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_config_discovery.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_config_discovery.py new file mode 100644 index 0000000000000000000000000000000000000000..b5df8203cdb6f9129a65d0c503c4f51e21315b1f --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_config_discovery.py @@ -0,0 +1,647 @@ +import os +import sys +from configparser import ConfigParser +from itertools import product +from typing import cast + +import jaraco.path +import pytest +from path import Path + +import setuptools # noqa: F401 # force distutils.core to be patched +from setuptools.command.sdist import sdist +from setuptools.discovery import find_package_path, find_parent_package +from setuptools.dist import Distribution +from setuptools.errors import PackageDiscoveryError + +from .contexts import quiet +from .integration.helpers import get_sdist_members, get_wheel_members, run +from .textwrap import DALS + +import distutils.core + + +class TestFindParentPackage: + def test_single_package(self, tmp_path): + # find_parent_package should find a non-namespace parent package + (tmp_path / "src/namespace/pkg/nested").mkdir(exist_ok=True, parents=True) + (tmp_path / "src/namespace/pkg/nested/__init__.py").touch() + (tmp_path / "src/namespace/pkg/__init__.py").touch() + packages = ["namespace", "namespace.pkg", "namespace.pkg.nested"] + assert find_parent_package(packages, {"": "src"}, tmp_path) == "namespace.pkg" + + def test_multiple_toplevel(self, tmp_path): + # find_parent_package should return null if the given list of packages does not + # have a single parent package + multiple = ["pkg", "pkg1", "pkg2"] + for name in multiple: + (tmp_path / f"src/{name}").mkdir(exist_ok=True, parents=True) + (tmp_path / f"src/{name}/__init__.py").touch() + assert find_parent_package(multiple, {"": "src"}, tmp_path) is None + + +class TestDiscoverPackagesAndPyModules: + """Make sure discovered values for ``packages`` and ``py_modules`` work + similarly to explicit configuration for the simple scenarios. + """ + + OPTIONS = { + # Different options according to the circumstance being tested + "explicit-src": {"package_dir": {"": "src"}, "packages": ["pkg"]}, + "variation-lib": { + "package_dir": {"": "lib"}, # variation of the source-layout + }, + "explicit-flat": {"packages": ["pkg"]}, + "explicit-single_module": {"py_modules": ["pkg"]}, + "explicit-namespace": {"packages": ["ns", "ns.pkg"]}, + "automatic-src": {}, + "automatic-flat": {}, + "automatic-single_module": {}, + "automatic-namespace": {}, + } + FILES = { + "src": ["src/pkg/__init__.py", "src/pkg/main.py"], + "lib": ["lib/pkg/__init__.py", "lib/pkg/main.py"], + "flat": ["pkg/__init__.py", "pkg/main.py"], + "single_module": ["pkg.py"], + "namespace": ["ns/pkg/__init__.py"], + } + + def _get_info(self, circumstance): + _, _, layout = circumstance.partition("-") + files = self.FILES[layout] + options = self.OPTIONS[circumstance] + return files, options + + @pytest.mark.parametrize("circumstance", OPTIONS.keys()) + def test_sdist_filelist(self, tmp_path, circumstance): + files, options = self._get_info(circumstance) + _populate_project_dir(tmp_path, files, options) + + _, cmd = _run_sdist_programatically(tmp_path, options) + + manifest = [f.replace(os.sep, "/") for f in cmd.filelist.files] + for file in files: + assert any(f.endswith(file) for f in manifest) + + @pytest.mark.parametrize("circumstance", OPTIONS.keys()) + def test_project(self, tmp_path, circumstance): + files, options = self._get_info(circumstance) + _populate_project_dir(tmp_path, files, options) + + # Simulate a pre-existing `build` directory + (tmp_path / "build").mkdir() + (tmp_path / "build/lib").mkdir() + (tmp_path / "build/bdist.linux-x86_64").mkdir() + (tmp_path / "build/bdist.linux-x86_64/file.py").touch() + (tmp_path / "build/lib/__init__.py").touch() + (tmp_path / "build/lib/file.py").touch() + (tmp_path / "dist").mkdir() + (tmp_path / "dist/file.py").touch() + + _run_build(tmp_path) + + sdist_files = get_sdist_members(next(tmp_path.glob("dist/*.tar.gz"))) + print("~~~~~ sdist_members ~~~~~") + print('\n'.join(sdist_files)) + assert sdist_files >= set(files) + + wheel_files = get_wheel_members(next(tmp_path.glob("dist/*.whl"))) + print("~~~~~ wheel_members ~~~~~") + print('\n'.join(wheel_files)) + orig_files = {f.replace("src/", "").replace("lib/", "") for f in files} + assert wheel_files >= orig_files + + # Make sure build files are not included by mistake + for file in wheel_files: + assert "build" not in files + assert "dist" not in files + + PURPOSEFULLY_EMPY = { + "setup.cfg": DALS( + """ + [metadata] + name = myproj + version = 0.0.0 + + [options] + {param} = + """ + ), + "setup.py": DALS( + """ + __import__('setuptools').setup( + name="myproj", + version="0.0.0", + {param}=[] + ) + """ + ), + "pyproject.toml": DALS( + """ + [build-system] + requires = [] + build-backend = 'setuptools.build_meta' + + [project] + name = "myproj" + version = "0.0.0" + + [tool.setuptools] + {param} = [] + """ + ), + "template-pyproject.toml": DALS( + """ + [build-system] + requires = [] + build-backend = 'setuptools.build_meta' + """ + ), + } + + @pytest.mark.parametrize( + ("config_file", "param", "circumstance"), + product( + ["setup.cfg", "setup.py", "pyproject.toml"], + ["packages", "py_modules"], + FILES.keys(), + ), + ) + def test_purposefully_empty(self, tmp_path, config_file, param, circumstance): + files = self.FILES[circumstance] + ["mod.py", "other.py", "src/pkg/__init__.py"] + _populate_project_dir(tmp_path, files, {}) + + if config_file == "pyproject.toml": + template_param = param.replace("_", "-") + else: + # Make sure build works with or without setup.cfg + pyproject = self.PURPOSEFULLY_EMPY["template-pyproject.toml"] + (tmp_path / "pyproject.toml").write_text(pyproject, encoding="utf-8") + template_param = param + + config = self.PURPOSEFULLY_EMPY[config_file].format(param=template_param) + (tmp_path / config_file).write_text(config, encoding="utf-8") + + dist = _get_dist(tmp_path, {}) + # When either parameter package or py_modules is an empty list, + # then there should be no discovery + assert getattr(dist, param) == [] + other = {"py_modules": "packages", "packages": "py_modules"}[param] + assert getattr(dist, other) is None + + @pytest.mark.parametrize( + ("extra_files", "pkgs"), + [ + (["venv/bin/simulate_venv"], {"pkg"}), + (["pkg-stubs/__init__.pyi"], {"pkg", "pkg-stubs"}), + (["other-stubs/__init__.pyi"], {"pkg", "other-stubs"}), + ( + # Type stubs can also be namespaced + ["namespace-stubs/pkg/__init__.pyi"], + {"pkg", "namespace-stubs", "namespace-stubs.pkg"}, + ), + ( + # Just the top-level package can have `-stubs`, ignore nested ones + ["namespace-stubs/pkg-stubs/__init__.pyi"], + {"pkg", "namespace-stubs"}, + ), + (["_hidden/file.py"], {"pkg"}), + (["news/finalize.py"], {"pkg"}), + ], + ) + def test_flat_layout_with_extra_files(self, tmp_path, extra_files, pkgs): + files = self.FILES["flat"] + extra_files + _populate_project_dir(tmp_path, files, {}) + dist = _get_dist(tmp_path, {}) + assert set(dist.packages) == pkgs + + @pytest.mark.parametrize( + "extra_files", + [ + ["other/__init__.py"], + ["other/finalize.py"], + ], + ) + def test_flat_layout_with_dangerous_extra_files(self, tmp_path, extra_files): + files = self.FILES["flat"] + extra_files + _populate_project_dir(tmp_path, files, {}) + with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"): + _get_dist(tmp_path, {}) + + def test_flat_layout_with_single_module(self, tmp_path): + files = self.FILES["single_module"] + ["invalid-module-name.py"] + _populate_project_dir(tmp_path, files, {}) + dist = _get_dist(tmp_path, {}) + assert set(dist.py_modules) == {"pkg"} + + def test_flat_layout_with_multiple_modules(self, tmp_path): + files = self.FILES["single_module"] + ["valid_module_name.py"] + _populate_project_dir(tmp_path, files, {}) + with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"): + _get_dist(tmp_path, {}) + + def test_py_modules_when_wheel_dir_is_cwd(self, tmp_path): + """Regression for issue 3692""" + from setuptools import build_meta + + pyproject = '[project]\nname = "test"\nversion = "1"' + (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8") + (tmp_path / "foo.py").touch() + with jaraco.path.DirectoryStack().context(tmp_path): + build_meta.build_wheel(".") + # Ensure py_modules are found + wheel_files = get_wheel_members(next(tmp_path.glob("*.whl"))) + assert "foo.py" in wheel_files + + +class TestNoConfig: + DEFAULT_VERSION = "0.0.0" # Default version given by setuptools + + EXAMPLES = { + "pkg1": ["src/pkg1.py"], + "pkg2": ["src/pkg2/__init__.py"], + "pkg3": ["src/pkg3/__init__.py", "src/pkg3-stubs/__init__.py"], + "pkg4": ["pkg4/__init__.py", "pkg4-stubs/__init__.py"], + "ns.nested.pkg1": ["src/ns/nested/pkg1/__init__.py"], + "ns.nested.pkg2": ["ns/nested/pkg2/__init__.py"], + } + + @pytest.mark.parametrize("example", EXAMPLES.keys()) + def test_discover_name(self, tmp_path, example): + _populate_project_dir(tmp_path, self.EXAMPLES[example], {}) + dist = _get_dist(tmp_path, {}) + assert dist.get_name() == example + + def test_build_with_discovered_name(self, tmp_path): + files = ["src/ns/nested/pkg/__init__.py"] + _populate_project_dir(tmp_path, files, {}) + _run_build(tmp_path, "--sdist") + # Expected distribution file + dist_file = tmp_path / f"dist/ns_nested_pkg-{self.DEFAULT_VERSION}.tar.gz" + assert dist_file.is_file() + + +class TestWithAttrDirective: + @pytest.mark.parametrize( + ("folder", "opts"), + [ + ("src", {}), + ("lib", {"packages": "find:", "packages.find": {"where": "lib"}}), + ], + ) + def test_setupcfg_metadata(self, tmp_path, folder, opts): + files = [f"{folder}/pkg/__init__.py", "setup.cfg"] + _populate_project_dir(tmp_path, files, opts) + + config = (tmp_path / "setup.cfg").read_text(encoding="utf-8") + overwrite = { + folder: {"pkg": {"__init__.py": "version = 42"}}, + "setup.cfg": "[metadata]\nversion = attr: pkg.version\n" + config, + } + jaraco.path.build(overwrite, prefix=tmp_path) + + dist = _get_dist(tmp_path, {}) + assert dist.get_name() == "pkg" + assert dist.get_version() == "42" + assert dist.package_dir + package_path = find_package_path("pkg", dist.package_dir, tmp_path) + assert os.path.exists(package_path) + assert folder in Path(package_path).parts() + + _run_build(tmp_path, "--sdist") + dist_file = tmp_path / "dist/pkg-42.tar.gz" + assert dist_file.is_file() + + def test_pyproject_metadata(self, tmp_path): + _populate_project_dir(tmp_path, ["src/pkg/__init__.py"], {}) + + overwrite = { + "src": {"pkg": {"__init__.py": "version = 42"}}, + "pyproject.toml": ( + "[project]\nname = 'pkg'\ndynamic = ['version']\n" + "[tool.setuptools.dynamic]\nversion = {attr = 'pkg.version'}\n" + ), + } + jaraco.path.build(overwrite, prefix=tmp_path) + + dist = _get_dist(tmp_path, {}) + assert dist.get_version() == "42" + assert dist.package_dir == {"": "src"} + + +class TestWithCExtension: + def _simulate_package_with_extension(self, tmp_path): + # This example is based on: https://github.com/nucleic/kiwi/tree/1.4.0 + files = [ + "benchmarks/file.py", + "docs/Makefile", + "docs/requirements.txt", + "docs/source/conf.py", + "proj/header.h", + "proj/file.py", + "py/proj.cpp", + "py/other.cpp", + "py/file.py", + "py/py.typed", + "py/tests/test_proj.py", + "README.rst", + ] + _populate_project_dir(tmp_path, files, {}) + + setup_script = """ + from setuptools import Extension, setup + + ext_modules = [ + Extension( + "proj", + ["py/proj.cpp", "py/other.cpp"], + include_dirs=["."], + language="c++", + ), + ] + setup(ext_modules=ext_modules) + """ + (tmp_path / "setup.py").write_text(DALS(setup_script), encoding="utf-8") + + def test_skip_discovery_with_setupcfg_metadata(self, tmp_path): + """Ensure that auto-discovery is not triggered when the project is based on + C-extensions only, for backward compatibility. + """ + self._simulate_package_with_extension(tmp_path) + + pyproject = """ + [build-system] + requires = [] + build-backend = 'setuptools.build_meta' + """ + (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8") + + setupcfg = """ + [metadata] + name = proj + version = 42 + """ + (tmp_path / "setup.cfg").write_text(DALS(setupcfg), encoding="utf-8") + + dist = _get_dist(tmp_path, {}) + assert dist.get_name() == "proj" + assert dist.get_version() == "42" + assert dist.py_modules is None + assert dist.packages is None + assert len(dist.ext_modules) == 1 + assert dist.ext_modules[0].name == "proj" + + def test_dont_skip_discovery_with_pyproject_metadata(self, tmp_path): + """When opting-in to pyproject.toml metadata, auto-discovery will be active if + the package lists C-extensions, but does not configure py-modules or packages. + + This way we ensure users with complex package layouts that would lead to the + discovery of multiple top-level modules/packages see errors and are forced to + explicitly set ``packages`` or ``py-modules``. + """ + self._simulate_package_with_extension(tmp_path) + + pyproject = """ + [project] + name = 'proj' + version = '42' + """ + (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8") + with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"): + _get_dist(tmp_path, {}) + + +class TestWithPackageData: + def _simulate_package_with_data_files(self, tmp_path, src_root): + files = [ + f"{src_root}/proj/__init__.py", + f"{src_root}/proj/file1.txt", + f"{src_root}/proj/nested/file2.txt", + ] + _populate_project_dir(tmp_path, files, {}) + + manifest = """ + global-include *.py *.txt + """ + (tmp_path / "MANIFEST.in").write_text(DALS(manifest), encoding="utf-8") + + EXAMPLE_SETUPCFG = """ + [metadata] + name = proj + version = 42 + + [options] + include_package_data = True + """ + EXAMPLE_PYPROJECT = """ + [project] + name = "proj" + version = "42" + """ + + PYPROJECT_PACKAGE_DIR = """ + [tool.setuptools] + package-dir = {"" = "src"} + """ + + @pytest.mark.parametrize( + ("src_root", "files"), + [ + (".", {"setup.cfg": DALS(EXAMPLE_SETUPCFG)}), + (".", {"pyproject.toml": DALS(EXAMPLE_PYPROJECT)}), + ("src", {"setup.cfg": DALS(EXAMPLE_SETUPCFG)}), + ("src", {"pyproject.toml": DALS(EXAMPLE_PYPROJECT)}), + ( + "src", + { + "setup.cfg": DALS(EXAMPLE_SETUPCFG) + + DALS( + """ + packages = find: + package_dir = + =src + + [options.packages.find] + where = src + """ + ) + }, + ), + ( + "src", + { + "pyproject.toml": DALS(EXAMPLE_PYPROJECT) + + DALS( + """ + [tool.setuptools] + package-dir = {"" = "src"} + """ + ) + }, + ), + ], + ) + def test_include_package_data(self, tmp_path, src_root, files): + """ + Make sure auto-discovery does not affect package include_package_data. + See issue #3196. + """ + jaraco.path.build(files, prefix=str(tmp_path)) + self._simulate_package_with_data_files(tmp_path, src_root) + + expected = { + os.path.normpath(f"{src_root}/proj/file1.txt").replace(os.sep, "/"), + os.path.normpath(f"{src_root}/proj/nested/file2.txt").replace(os.sep, "/"), + } + + _run_build(tmp_path) + + sdist_files = get_sdist_members(next(tmp_path.glob("dist/*.tar.gz"))) + print("~~~~~ sdist_members ~~~~~") + print('\n'.join(sdist_files)) + assert sdist_files >= expected + + wheel_files = get_wheel_members(next(tmp_path.glob("dist/*.whl"))) + print("~~~~~ wheel_members ~~~~~") + print('\n'.join(wheel_files)) + orig_files = {f.replace("src/", "").replace("lib/", "") for f in expected} + assert wheel_files >= orig_files + + +def test_compatible_with_numpy_configuration(tmp_path): + files = [ + "dir1/__init__.py", + "dir2/__init__.py", + "file.py", + ] + _populate_project_dir(tmp_path, files, {}) + dist = Distribution({}) + dist.configuration = object() + dist.set_defaults() + assert dist.py_modules is None + assert dist.packages is None + + +def test_name_discovery_doesnt_break_cli(tmpdir_cwd): + jaraco.path.build({"pkg.py": ""}) + dist = Distribution({}) + dist.script_args = ["--name"] + dist.set_defaults() + dist.parse_command_line() # <-- no exception should be raised here. + assert dist.get_name() == "pkg" + + +def test_preserve_explicit_name_with_dynamic_version(tmpdir_cwd, monkeypatch): + """According to #3545 it seems that ``name`` discovery is running, + even when the project already explicitly sets it. + This seems to be related to parsing of dynamic versions (via ``attr`` directive), + which requires the auto-discovery of ``package_dir``. + """ + files = { + "src": { + "pkg": {"__init__.py": "__version__ = 42\n"}, + }, + "pyproject.toml": DALS( + """ + [project] + name = "myproj" # purposefully different from package name + dynamic = ["version"] + [tool.setuptools.dynamic] + version = {"attr" = "pkg.__version__"} + """ + ), + } + jaraco.path.build(files) + dist = Distribution({}) + orig_analyse_name = dist.set_defaults.analyse_name + + def spy_analyse_name(): + # We can check if name discovery was triggered by ensuring the original + # name remains instead of the package name. + orig_analyse_name() + assert dist.get_name() == "myproj" + + monkeypatch.setattr(dist.set_defaults, "analyse_name", spy_analyse_name) + dist.parse_config_files() + assert dist.get_version() == "42" + assert set(dist.packages) == {"pkg"} + + +def _populate_project_dir(root, files, options): + # NOTE: Currently pypa/build will refuse to build the project if no + # `pyproject.toml` or `setup.py` is found. So it is impossible to do + # completely "config-less" projects. + basic = { + "setup.py": "import setuptools\nsetuptools.setup()", + "README.md": "# Example Package", + "LICENSE": "Copyright (c) 2018", + } + jaraco.path.build(basic, prefix=root) + _write_setupcfg(root, options) + paths = (root / f for f in files) + for path in paths: + path.parent.mkdir(exist_ok=True, parents=True) + path.touch() + + +def _write_setupcfg(root, options): + if not options: + print("~~~~~ **NO** setup.cfg ~~~~~") + return + setupcfg = ConfigParser() + setupcfg.add_section("options") + for key, value in options.items(): + if key == "packages.find": + setupcfg.add_section(f"options.{key}") + setupcfg[f"options.{key}"].update(value) + elif isinstance(value, list): + setupcfg["options"][key] = ", ".join(value) + elif isinstance(value, dict): + str_value = "\n".join(f"\t{k} = {v}" for k, v in value.items()) + setupcfg["options"][key] = "\n" + str_value + else: + setupcfg["options"][key] = str(value) + with open(root / "setup.cfg", "w", encoding="utf-8") as f: + setupcfg.write(f) + print("~~~~~ setup.cfg ~~~~~") + print((root / "setup.cfg").read_text(encoding="utf-8")) + + +def _run_build(path, *flags): + cmd = [sys.executable, "-m", "build", "--no-isolation", *flags, str(path)] + return run(cmd, env={'DISTUTILS_DEBUG': ''}) + + +def _get_dist(dist_path, attrs): + root = "/".join(os.path.split(dist_path)) # POSIX-style + + script = dist_path / 'setup.py' + if script.exists(): + with Path(dist_path): + dist = cast( + Distribution, + distutils.core.run_setup("setup.py", {}, stop_after="init"), + ) + else: + dist = Distribution(attrs) + + dist.src_root = root + dist.script_name = "setup.py" + with Path(dist_path): + dist.parse_config_files() + + dist.set_defaults() + return dist + + +def _run_sdist_programatically(dist_path, attrs): + dist = _get_dist(dist_path, attrs) + cmd = sdist(dist) + cmd.ensure_finalized() + assert cmd.distribution.packages or cmd.distribution.py_modules + + with quiet(), Path(dist_path): + cmd.run() + + return dist, cmd diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_core_metadata.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_core_metadata.py new file mode 100644 index 0000000000000000000000000000000000000000..0d925111fa6e611ae919df8bb30bcda0f248c5b6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_core_metadata.py @@ -0,0 +1,622 @@ +from __future__ import annotations + +import functools +import importlib +import io +from email import message_from_string +from email.generator import Generator +from email.message import EmailMessage, Message +from email.parser import Parser +from email.policy import EmailPolicy +from inspect import cleandoc +from pathlib import Path +from unittest.mock import Mock + +import jaraco.path +import pytest +from packaging.metadata import Metadata +from packaging.requirements import Requirement + +from setuptools import _reqs, sic +from setuptools._core_metadata import rfc822_escape, rfc822_unescape +from setuptools.command.egg_info import egg_info, write_requirements +from setuptools.config import expand, setupcfg +from setuptools.dist import Distribution + +from .config.downloads import retrieve_file, urls_from_file + +EXAMPLE_BASE_INFO = dict( + name="package", + version="0.0.1", + author="Foo Bar", + author_email="foo@bar.net", + long_description="Long\ndescription", + description="Short description", + keywords=["one", "two"], +) + + +@pytest.mark.parametrize( + ("content", "result"), + ( + pytest.param( + "Just a single line", + None, + id="single_line", + ), + pytest.param( + "Multiline\nText\nwithout\nextra indents\n", + None, + id="multiline", + ), + pytest.param( + "Multiline\n With\n\nadditional\n indentation", + None, + id="multiline_with_indentation", + ), + pytest.param( + " Leading whitespace", + "Leading whitespace", + id="remove_leading_whitespace", + ), + pytest.param( + " Leading whitespace\nIn\n Multiline comment", + "Leading whitespace\nIn\n Multiline comment", + id="remove_leading_whitespace_multiline", + ), + ), +) +def test_rfc822_unescape(content, result): + assert (result or content) == rfc822_unescape(rfc822_escape(content)) + + +def __read_test_cases(): + base = EXAMPLE_BASE_INFO + + params = functools.partial(dict, base) + + return [ + ('Metadata version 1.0', params()), + ( + 'Metadata Version 1.0: Short long description', + params( + long_description='Short long description', + ), + ), + ( + 'Metadata version 1.1: Classifiers', + params( + classifiers=[ + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'License :: OSI Approved :: MIT License', + ], + ), + ), + ( + 'Metadata version 1.1: Download URL', + params( + download_url='https://example.com', + ), + ), + ( + 'Metadata Version 1.2: Requires-Python', + params( + python_requires='>=3.7', + ), + ), + pytest.param( + 'Metadata Version 1.2: Project-Url', + params(project_urls=dict(Foo='https://example.bar')), + marks=pytest.mark.xfail( + reason="Issue #1578: project_urls not read", + ), + ), + ( + 'Metadata Version 2.1: Long Description Content Type', + params( + long_description_content_type='text/x-rst; charset=UTF-8', + ), + ), + ( + 'License', + params( + license='MIT', + ), + ), + ( + 'License multiline', + params( + license='This is a long license \nover multiple lines', + ), + ), + pytest.param( + 'Metadata Version 2.1: Provides Extra', + params(provides_extras=['foo', 'bar']), + marks=pytest.mark.xfail(reason="provides_extras not read"), + ), + ( + 'Missing author', + dict( + name='foo', + version='1.0.0', + author_email='snorri@sturluson.name', + ), + ), + ( + 'Missing author e-mail', + dict( + name='foo', + version='1.0.0', + author='Snorri Sturluson', + ), + ), + ( + 'Missing author and e-mail', + dict( + name='foo', + version='1.0.0', + ), + ), + ( + 'Bypass normalized version', + dict( + name='foo', + version=sic('1.0.0a'), + ), + ), + ] + + +@pytest.mark.parametrize(("name", "attrs"), __read_test_cases()) +def test_read_metadata(name, attrs): + dist = Distribution(attrs) + metadata_out = dist.metadata + dist_class = metadata_out.__class__ + + # Write to PKG_INFO and then load into a new metadata object + PKG_INFO = io.StringIO() + + metadata_out.write_pkg_file(PKG_INFO) + PKG_INFO.seek(0) + pkg_info = PKG_INFO.read() + assert _valid_metadata(pkg_info) + + PKG_INFO.seek(0) + metadata_in = dist_class() + metadata_in.read_pkg_file(PKG_INFO) + + tested_attrs = [ + ('name', dist_class.get_name), + ('version', dist_class.get_version), + ('author', dist_class.get_contact), + ('author_email', dist_class.get_contact_email), + ('metadata_version', dist_class.get_metadata_version), + ('provides', dist_class.get_provides), + ('description', dist_class.get_description), + ('long_description', dist_class.get_long_description), + ('download_url', dist_class.get_download_url), + ('keywords', dist_class.get_keywords), + ('platforms', dist_class.get_platforms), + ('obsoletes', dist_class.get_obsoletes), + ('requires', dist_class.get_requires), + ('classifiers', dist_class.get_classifiers), + ('project_urls', lambda s: getattr(s, 'project_urls', {})), + ('provides_extras', lambda s: getattr(s, 'provides_extras', {})), + ] + + for attr, getter in tested_attrs: + assert getter(metadata_in) == getter(metadata_out) + + +def __maintainer_test_cases(): + attrs = {"name": "package", "version": "1.0", "description": "xxx"} + + def merge_dicts(d1, d2): + d1 = d1.copy() + d1.update(d2) + + return d1 + + return [ + ('No author, no maintainer', attrs.copy()), + ( + 'Author (no e-mail), no maintainer', + merge_dicts(attrs, {'author': 'Author Name'}), + ), + ( + 'Author (e-mail), no maintainer', + merge_dicts( + attrs, {'author': 'Author Name', 'author_email': 'author@name.com'} + ), + ), + ( + 'No author, maintainer (no e-mail)', + merge_dicts(attrs, {'maintainer': 'Maintainer Name'}), + ), + ( + 'No author, maintainer (e-mail)', + merge_dicts( + attrs, + { + 'maintainer': 'Maintainer Name', + 'maintainer_email': 'maintainer@name.com', + }, + ), + ), + ( + 'Author (no e-mail), Maintainer (no-email)', + merge_dicts( + attrs, {'author': 'Author Name', 'maintainer': 'Maintainer Name'} + ), + ), + ( + 'Author (e-mail), Maintainer (e-mail)', + merge_dicts( + attrs, + { + 'author': 'Author Name', + 'author_email': 'author@name.com', + 'maintainer': 'Maintainer Name', + 'maintainer_email': 'maintainer@name.com', + }, + ), + ), + ( + 'No author (e-mail), no maintainer (e-mail)', + merge_dicts( + attrs, + { + 'author_email': 'author@name.com', + 'maintainer_email': 'maintainer@name.com', + }, + ), + ), + ('Author unicode', merge_dicts(attrs, {'author': '鉄沢寛'})), + ('Maintainer unicode', merge_dicts(attrs, {'maintainer': 'Jan Łukasiewicz'})), + ] + + +@pytest.mark.parametrize(("name", "attrs"), __maintainer_test_cases()) +def test_maintainer_author(name, attrs, tmpdir): + tested_keys = { + 'author': 'Author', + 'author_email': 'Author-email', + 'maintainer': 'Maintainer', + 'maintainer_email': 'Maintainer-email', + } + + # Generate a PKG-INFO file + dist = Distribution(attrs) + fn = tmpdir.mkdir('pkg_info') + fn_s = str(fn) + + dist.metadata.write_pkg_info(fn_s) + + with open(str(fn.join('PKG-INFO')), 'r', encoding='utf-8') as f: + pkg_info = f.read() + + assert _valid_metadata(pkg_info) + + # Drop blank lines and strip lines from default description + raw_pkg_lines = pkg_info.splitlines() + pkg_lines = list(filter(None, raw_pkg_lines[:-2])) + + pkg_lines_set = set(pkg_lines) + + # Duplicate lines should not be generated + assert len(pkg_lines) == len(pkg_lines_set) + + for fkey, dkey in tested_keys.items(): + val = attrs.get(dkey, None) + if val is None: + for line in pkg_lines: + assert not line.startswith(fkey + ':') + else: + line = f'{fkey}: {val}' + assert line in pkg_lines_set + + +class TestParityWithMetadataFromPyPaWheel: + def base_example(self): + attrs = dict( + **EXAMPLE_BASE_INFO, + # Example with complex requirement definition + python_requires=">=3.8", + install_requires=""" + packaging==23.2 + more-itertools==8.8.0; extra == "other" + jaraco.text==3.7.0 + importlib-resources==5.10.2; python_version<"3.8" + importlib-metadata==6.0.0 ; python_version<"3.8" + colorama>=0.4.4; sys_platform == "win32" + """, + extras_require={ + "testing": """ + pytest >= 6 + pytest-checkdocs >= 2.4 + tomli ; \\ + # Using stdlib when possible + python_version < "3.11" + ini2toml[lite]>=0.9 + """, + "other": [], + }, + ) + # Generate a PKG-INFO file using setuptools + return Distribution(attrs) + + def test_requires_dist(self, tmp_path): + dist = self.base_example() + pkg_info = _get_pkginfo(dist) + assert _valid_metadata(pkg_info) + + # Ensure Requires-Dist is present + expected = [ + 'Metadata-Version:', + 'Requires-Python: >=3.8', + 'Provides-Extra: other', + 'Provides-Extra: testing', + 'Requires-Dist: tomli; python_version < "3.11" and extra == "testing"', + 'Requires-Dist: more-itertools==8.8.0; extra == "other"', + 'Requires-Dist: ini2toml[lite]>=0.9; extra == "testing"', + ] + for line in expected: + assert line in pkg_info + + HERE = Path(__file__).parent + EXAMPLES_FILE = HERE / "config/setupcfg_examples.txt" + + @pytest.fixture(params=[None, *urls_from_file(EXAMPLES_FILE)]) + def dist(self, request, monkeypatch, tmp_path): + """Example of distribution with arbitrary configuration""" + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.42")) + monkeypatch.setattr(expand, "read_files", Mock(return_value="hello world")) + monkeypatch.setattr( + Distribution, "_finalize_license_files", Mock(return_value=None) + ) + if request.param is None: + yield self.base_example() + else: + # Real-world usage + config = retrieve_file(request.param) + yield setupcfg.apply_configuration(Distribution({}), config) + + @pytest.mark.uses_network + def test_equivalent_output(self, tmp_path, dist): + """Ensure output from setuptools is equivalent to the one from `pypa/wheel`""" + # Generate a METADATA file using pypa/wheel for comparison + wheel_metadata = importlib.import_module("wheel.metadata") + pkginfo_to_metadata = getattr(wheel_metadata, "pkginfo_to_metadata", None) + + if pkginfo_to_metadata is None: # pragma: nocover + pytest.xfail( + "wheel.metadata.pkginfo_to_metadata is undefined, " + "(this is likely to be caused by API changes in pypa/wheel" + ) + + # Generate an simplified "egg-info" dir for pypa/wheel to convert + pkg_info = _get_pkginfo(dist) + egg_info_dir = tmp_path / "pkg.egg-info" + egg_info_dir.mkdir(parents=True) + (egg_info_dir / "PKG-INFO").write_text(pkg_info, encoding="utf-8") + write_requirements(egg_info(dist), egg_info_dir, egg_info_dir / "requires.txt") + + # Get pypa/wheel generated METADATA but normalize requirements formatting + metadata_msg = pkginfo_to_metadata(egg_info_dir, egg_info_dir / "PKG-INFO") + metadata_str = _normalize_metadata(metadata_msg) + pkg_info_msg = message_from_string(pkg_info) + pkg_info_str = _normalize_metadata(pkg_info_msg) + + # Compare setuptools PKG-INFO x pypa/wheel METADATA + assert metadata_str == pkg_info_str + + # Make sure it parses/serializes well in pypa/wheel + _assert_roundtrip_message(pkg_info) + + +class TestPEP643: + STATIC_CONFIG = { + "setup.cfg": cleandoc( + """ + [metadata] + name = package + version = 0.0.1 + author = Foo Bar + author_email = foo@bar.net + long_description = Long + description + description = Short description + keywords = one, two + platforms = abcd + [options] + install_requires = requests + """ + ), + "pyproject.toml": cleandoc( + """ + [project] + name = "package" + version = "0.0.1" + authors = [ + {name = "Foo Bar", email = "foo@bar.net"} + ] + description = "Short description" + readme = {text = "Long\\ndescription", content-type = "text/plain"} + keywords = ["one", "two"] + dependencies = ["requests"] + license = "AGPL-3.0-or-later" + [tool.setuptools] + provides = ["abcd"] + obsoletes = ["abcd"] + """ + ), + } + + @pytest.mark.parametrize("file", STATIC_CONFIG.keys()) + def test_static_config_has_no_dynamic(self, file, tmpdir_cwd): + Path(file).write_text(self.STATIC_CONFIG[file], encoding="utf-8") + metadata = _get_metadata() + assert metadata.get_all("Dynamic") is None + assert metadata.get_all("dynamic") is None + + @pytest.mark.parametrize("file", STATIC_CONFIG.keys()) + @pytest.mark.parametrize( + "fields", + [ + # Single dynamic field + {"requires-python": ("python_requires", ">=3.12")}, + {"author-email": ("author_email", "snoopy@peanuts.com")}, + {"keywords": ("keywords", ["hello", "world"])}, + {"platform": ("platforms", ["abcd"])}, + # Multiple dynamic fields + { + "summary": ("description", "hello world"), + "description": ("long_description", "bla bla bla bla"), + "requires-dist": ("install_requires", ["hello-world"]), + }, + ], + ) + def test_modified_fields_marked_as_dynamic(self, file, fields, tmpdir_cwd): + # We start with a static config + Path(file).write_text(self.STATIC_CONFIG[file], encoding="utf-8") + dist = _makedist() + + # ... but then we simulate the effects of a plugin modifying the distribution + for attr, value in fields.values(): + # `dist` and `dist.metadata` are complicated... + # Some attributes work when set on `dist`, others on `dist.metadata`... + # Here we set in both just in case (this also avoids calling `_finalize_*`) + setattr(dist, attr, value) + setattr(dist.metadata, attr, value) + + # Then we should be able to list the modified fields as Dynamic + metadata = _get_metadata(dist) + assert set(metadata.get_all("Dynamic")) == set(fields) + + @pytest.mark.parametrize( + "extra_toml", + [ + "# Let setuptools autofill license-files", + "license-files = ['LICENSE*', 'AUTHORS*', 'NOTICE']", + ], + ) + def test_license_files_dynamic(self, extra_toml, tmpdir_cwd): + # For simplicity (and for the time being) setuptools is not making + # any special handling to guarantee `License-File` is considered static. + # Instead we rely in the fact that, although suboptimal, it is OK to have + # it as dynamics, as per: + # https://github.com/pypa/setuptools/issues/4629#issuecomment-2331233677 + files = { + "pyproject.toml": self.STATIC_CONFIG["pyproject.toml"].replace( + 'license = "AGPL-3.0-or-later"', + f"dynamic = ['license']\n{extra_toml}", + ), + "LICENSE.md": "--- mock license ---", + "NOTICE": "--- mock notice ---", + "AUTHORS.txt": "--- me ---", + } + # Sanity checks: + assert extra_toml in files["pyproject.toml"] + assert 'license = "AGPL-3.0-or-later"' not in extra_toml + + jaraco.path.build(files) + dist = _makedist(license_expression="AGPL-3.0-or-later") + metadata = _get_metadata(dist) + assert set(metadata.get_all("Dynamic")) == { + 'license-file', + 'license-expression', + } + assert metadata.get("License-Expression") == "AGPL-3.0-or-later" + assert set(metadata.get_all("License-File")) == { + "NOTICE", + "AUTHORS.txt", + "LICENSE.md", + } + + +def _makedist(**attrs): + dist = Distribution(attrs) + dist.parse_config_files() + return dist + + +def _assert_roundtrip_message(metadata: str) -> None: + """Emulate the way wheel.bdist_wheel parses and regenerates the message, + then ensures the metadata generated by setuptools is compatible. + """ + with io.StringIO(metadata) as buffer: + msg = Parser(EmailMessage).parse(buffer) + + serialization_policy = EmailPolicy( + utf8=True, + mangle_from_=False, + max_line_length=0, + ) + with io.BytesIO() as buffer: + out = io.TextIOWrapper(buffer, encoding="utf-8") + Generator(out, policy=serialization_policy).flatten(msg) + out.flush() + regenerated = buffer.getvalue() + + raw_metadata = bytes(metadata, "utf-8") + # Normalise newlines to avoid test errors on Windows: + raw_metadata = b"\n".join(raw_metadata.splitlines()) + regenerated = b"\n".join(regenerated.splitlines()) + assert regenerated == raw_metadata + + +def _normalize_metadata(msg: Message) -> str: + """Allow equivalent metadata to be compared directly""" + # The main challenge regards the requirements and extras. + # Both setuptools and wheel already apply some level of normalization + # but they differ regarding which character is chosen, according to the + # following spec it should be "-": + # https://packaging.python.org/en/latest/specifications/name-normalization/ + + # Related issues: + # https://github.com/pypa/packaging/issues/845 + # https://github.com/pypa/packaging/issues/644#issuecomment-2429813968 + + extras = {x.replace("_", "-"): x for x in msg.get_all("Provides-Extra", [])} + reqs = [ + _normalize_req(req, extras) + for req in _reqs.parse(msg.get_all("Requires-Dist", [])) + ] + del msg["Requires-Dist"] + del msg["Provides-Extra"] + + # Ensure consistent ord + for req in sorted(reqs): + msg["Requires-Dist"] = req + for extra in sorted(extras): + msg["Provides-Extra"] = extra + + # TODO: Handle lack of PEP 643 implementation in pypa/wheel? + del msg["Metadata-Version"] + + return msg.as_string() + + +def _normalize_req(req: Requirement, extras: dict[str, str]) -> str: + """Allow equivalent requirement objects to be compared directly""" + as_str = str(req).replace(req.name, req.name.replace("_", "-")) + for norm, orig in extras.items(): + as_str = as_str.replace(orig, norm) + return as_str + + +def _get_pkginfo(dist: Distribution): + with io.StringIO() as fp: + dist.metadata.write_pkg_file(fp) + return fp.getvalue() + + +def _get_metadata(dist: Distribution | None = None): + return message_from_string(_get_pkginfo(dist or _makedist())) + + +def _valid_metadata(text: str) -> bool: + metadata = Metadata.from_email(text, validate=True) # can raise exceptions + return metadata is not None diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_depends.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_depends.py new file mode 100644 index 0000000000000000000000000000000000000000..1714c041f7a23e1ecbfc3245bf964f75c13734ca --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_depends.py @@ -0,0 +1,15 @@ +import sys + +from setuptools import depends + + +class TestGetModuleConstant: + def test_basic(self): + """ + Invoke get_module_constant on a module in + the test package. + """ + mod_name = 'setuptools.tests.mod_with_constant' + val = depends.get_module_constant(mod_name, 'value') + assert val == 'three, sir!' + assert 'setuptools.tests.mod_with_constant' not in sys.modules diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_develop.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_develop.py new file mode 100644 index 0000000000000000000000000000000000000000..929fa9c285eb4d11e646dab2864be6d5fa023e2b --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_develop.py @@ -0,0 +1,175 @@ +"""develop tests""" + +import os +import pathlib +import platform +import subprocess +import sys + +import pytest + +from setuptools._path import paths_on_pythonpath +from setuptools.command.develop import develop +from setuptools.dist import Distribution + +from . import contexts, namespaces + +SETUP_PY = """\ +from setuptools import setup + +setup(name='foo', + packages=['foo'], +) +""" + +INIT_PY = """print "foo" +""" + + +@pytest.fixture +def temp_user(monkeypatch): + with contexts.tempdir() as user_base: + with contexts.tempdir() as user_site: + monkeypatch.setattr('site.USER_BASE', user_base) + monkeypatch.setattr('site.USER_SITE', user_site) + yield + + +@pytest.fixture +def test_env(tmpdir, temp_user): + target = tmpdir + foo = target.mkdir('foo') + setup = target / 'setup.py' + if setup.isfile(): + raise ValueError(dir(target)) + with setup.open('w') as f: + f.write(SETUP_PY) + init = foo / '__init__.py' + with init.open('w') as f: + f.write(INIT_PY) + with target.as_cwd(): + yield target + + +class TestDevelop: + in_virtualenv = hasattr(sys, 'real_prefix') + in_venv = hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix + + def test_console_scripts(self, tmpdir): + """ + Test that console scripts are installed and that they reference + only the project by name and not the current version. + """ + pytest.skip( + "TODO: needs a fixture to cause 'develop' " + "to be invoked without mutating environment." + ) + settings = dict( + name='foo', + packages=['foo'], + version='0.0', + entry_points={ + 'console_scripts': [ + 'foocmd = foo:foo', + ], + }, + ) + dist = Distribution(settings) + dist.script_name = 'setup.py' + cmd = develop(dist) + cmd.ensure_finalized() + cmd.install_dir = tmpdir + cmd.run() + # assert '0.0' not in foocmd_text + + @pytest.mark.xfail(reason="legacy behavior retained for compatibility #4167") + def test_egg_link_filename(self): + settings = dict( + name='Foo $$$ Bar_baz-bing', + ) + dist = Distribution(settings) + cmd = develop(dist) + cmd.ensure_finalized() + link = pathlib.Path(cmd.egg_link) + assert link.suffix == '.egg-link' + assert link.stem == 'Foo_Bar_baz_bing' + + +class TestResolver: + """ + TODO: These tests were written with a minimal understanding + of what _resolve_setup_path is intending to do. Come up with + more meaningful cases that look like real-world scenarios. + """ + + def test_resolve_setup_path_cwd(self): + assert develop._resolve_setup_path('.', '.', '.') == '.' + + def test_resolve_setup_path_one_dir(self): + assert develop._resolve_setup_path('pkgs', '.', 'pkgs') == '../' + + def test_resolve_setup_path_one_dir_trailing_slash(self): + assert develop._resolve_setup_path('pkgs/', '.', 'pkgs') == '../' + + +class TestNamespaces: + @staticmethod + def install_develop(src_dir, target): + develop_cmd = [ + sys.executable, + 'setup.py', + 'develop', + '--install-dir', + str(target), + ] + with src_dir.as_cwd(): + with paths_on_pythonpath([str(target)]): + subprocess.check_call(develop_cmd) + + @pytest.mark.skipif( + bool(os.environ.get("APPVEYOR")), + reason="https://github.com/pypa/setuptools/issues/851", + ) + @pytest.mark.skipif( + platform.python_implementation() == 'PyPy', + reason="https://github.com/pypa/setuptools/issues/1202", + ) + def test_namespace_package_importable(self, tmpdir): + """ + Installing two packages sharing the same namespace, one installed + naturally using pip or `--single-version-externally-managed` + and the other installed using `develop` should leave the namespace + in tact and both packages reachable by import. + """ + pkg_A = namespaces.build_namespace_package(tmpdir, 'myns.pkgA') + pkg_B = namespaces.build_namespace_package(tmpdir, 'myns.pkgB') + target = tmpdir / 'packages' + # use pip to install to the target directory + install_cmd = [ + sys.executable, + '-m', + 'pip', + 'install', + str(pkg_A), + '-t', + str(target), + ] + subprocess.check_call(install_cmd) + self.install_develop(pkg_B, target) + namespaces.make_site_dir(target) + try_import = [ + sys.executable, + '-c', + 'import myns.pkgA; import myns.pkgB', + ] + with paths_on_pythonpath([str(target)]): + subprocess.check_call(try_import) + + # additionally ensure that pkg_resources import works + pkg_resources_imp = [ + sys.executable, + '-c', + 'import pkg_resources', + ] + with paths_on_pythonpath([str(target)]): + subprocess.check_call(pkg_resources_imp) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_dist.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_dist.py new file mode 100644 index 0000000000000000000000000000000000000000..e65ab310e7f2ad98b591affb352c855a2f86eda1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_dist.py @@ -0,0 +1,278 @@ +import os +import re +import urllib.parse +import urllib.request + +import pytest + +from setuptools import Distribution +from setuptools.dist import check_package_data, check_specifier + +from .test_easy_install import make_trivial_sdist +from .test_find_packages import ensure_files +from .textwrap import DALS + +from distutils.errors import DistutilsSetupError + + +def test_dist_fetch_build_egg(tmpdir): + """ + Check multiple calls to `Distribution.fetch_build_egg` work as expected. + """ + index = tmpdir.mkdir('index') + index_url = urllib.parse.urljoin('file://', urllib.request.pathname2url(str(index))) + + def sdist_with_index(distname, version): + dist_dir = index.mkdir(distname) + dist_sdist = f'{distname}-{version}.tar.gz' + make_trivial_sdist(str(dist_dir.join(dist_sdist)), distname, version) + with dist_dir.join('index.html').open('w') as fp: + fp.write( + DALS( + """ + + {dist_sdist}
+ + """ + ).format(dist_sdist=dist_sdist) + ) + + sdist_with_index('barbazquux', '3.2.0') + sdist_with_index('barbazquux-runner', '2.11.1') + with tmpdir.join('setup.cfg').open('w') as fp: + fp.write( + DALS( + """ + [easy_install] + index_url = {index_url} + """ + ).format(index_url=index_url) + ) + reqs = """ + barbazquux-runner + barbazquux + """.split() + with tmpdir.as_cwd(): + dist = Distribution() + dist.parse_config_files() + resolved_dists = [dist.fetch_build_egg(r) for r in reqs] + assert [dist.key for dist in resolved_dists if dist] == reqs + + +EXAMPLE_BASE_INFO = dict( + name="package", + version="0.0.1", + author="Foo Bar", + author_email="foo@bar.net", + long_description="Long\ndescription", + description="Short description", + keywords=["one", "two"], +) + + +def test_provides_extras_deterministic_order(): + attrs = dict(extras_require=dict(a=['foo'], b=['bar'])) + dist = Distribution(attrs) + assert list(dist.metadata.provides_extras) == ['a', 'b'] + attrs['extras_require'] = dict(reversed(attrs['extras_require'].items())) + dist = Distribution(attrs) + assert list(dist.metadata.provides_extras) == ['b', 'a'] + + +CHECK_PACKAGE_DATA_TESTS = ( + # Valid. + ( + { + '': ['*.txt', '*.rst'], + 'hello': ['*.msg'], + }, + None, + ), + # Not a dictionary. + ( + ( + ('', ['*.txt', '*.rst']), + ('hello', ['*.msg']), + ), + ( + "'package_data' must be a dictionary mapping package" + " names to lists of string wildcard patterns" + ), + ), + # Invalid key type. + ( + { + 400: ['*.txt', '*.rst'], + }, + ("keys of 'package_data' dict must be strings (got 400)"), + ), + # Invalid value type. + ( + { + 'hello': '*.msg', + }, + ( + "\"values of 'package_data' dict\" must be of type " + " (got '*.msg')" + ), + ), + # Invalid value type (generators are single use) + ( + { + 'hello': (x for x in "generator"), + }, + ( + "\"values of 'package_data' dict\" must be of type " + " (got =3.0, !=3.1'} + dist = Distribution(attrs) + check_specifier(dist, attrs, attrs['python_requires']) + + attrs = {'name': 'foo', 'python_requires': ['>=3.0', '!=3.1']} + dist = Distribution(attrs) + check_specifier(dist, attrs, attrs['python_requires']) + + # invalid specifier value + attrs = {'name': 'foo', 'python_requires': '>=invalid-version'} + with pytest.raises(DistutilsSetupError): + dist = Distribution(attrs) + + +def test_metadata_name(): + with pytest.raises(DistutilsSetupError, match='missing.*name'): + Distribution()._validate_metadata() + + +@pytest.mark.parametrize( + ('dist_name', 'py_module'), + [ + ("my.pkg", "my_pkg"), + ("my-pkg", "my_pkg"), + ("my_pkg", "my_pkg"), + ("pkg", "pkg"), + ], +) +def test_dist_default_py_modules(tmp_path, dist_name, py_module): + (tmp_path / f"{py_module}.py").touch() + + (tmp_path / "setup.py").touch() + (tmp_path / "noxfile.py").touch() + # ^-- make sure common tool files are ignored + + attrs = {**EXAMPLE_BASE_INFO, "name": dist_name, "src_root": str(tmp_path)} + # Find `py_modules` corresponding to dist_name if not given + dist = Distribution(attrs) + dist.set_defaults() + assert dist.py_modules == [py_module] + # When `py_modules` is given, don't do anything + dist = Distribution({**attrs, "py_modules": ["explicity_py_module"]}) + dist.set_defaults() + assert dist.py_modules == ["explicity_py_module"] + # When `packages` is given, don't do anything + dist = Distribution({**attrs, "packages": ["explicity_package"]}) + dist.set_defaults() + assert not dist.py_modules + + +@pytest.mark.parametrize( + ('dist_name', 'package_dir', 'package_files', 'packages'), + [ + ("my.pkg", None, ["my_pkg/__init__.py", "my_pkg/mod.py"], ["my_pkg"]), + ("my-pkg", None, ["my_pkg/__init__.py", "my_pkg/mod.py"], ["my_pkg"]), + ("my_pkg", None, ["my_pkg/__init__.py", "my_pkg/mod.py"], ["my_pkg"]), + ("my.pkg", None, ["my/pkg/__init__.py"], ["my", "my.pkg"]), + ( + "my_pkg", + None, + ["src/my_pkg/__init__.py", "src/my_pkg2/__init__.py"], + ["my_pkg", "my_pkg2"], + ), + ( + "my_pkg", + {"pkg": "lib", "pkg2": "lib2"}, + ["lib/__init__.py", "lib/nested/__init__.pyt", "lib2/__init__.py"], + ["pkg", "pkg.nested", "pkg2"], + ), + ], +) +def test_dist_default_packages( + tmp_path, dist_name, package_dir, package_files, packages +): + ensure_files(tmp_path, package_files) + + (tmp_path / "setup.py").touch() + (tmp_path / "noxfile.py").touch() + # ^-- should not be included by default + + attrs = { + **EXAMPLE_BASE_INFO, + "name": dist_name, + "src_root": str(tmp_path), + "package_dir": package_dir, + } + # Find `packages` either corresponding to dist_name or inside src + dist = Distribution(attrs) + dist.set_defaults() + assert not dist.py_modules + assert not dist.py_modules + assert set(dist.packages) == set(packages) + # When `py_modules` is given, don't do anything + dist = Distribution({**attrs, "py_modules": ["explicit_py_module"]}) + dist.set_defaults() + assert not dist.packages + assert set(dist.py_modules) == {"explicit_py_module"} + # When `packages` is given, don't do anything + dist = Distribution({**attrs, "packages": ["explicit_package"]}) + dist.set_defaults() + assert not dist.py_modules + assert set(dist.packages) == {"explicit_package"} + + +@pytest.mark.parametrize( + ('dist_name', 'package_dir', 'package_files'), + [ + ("my.pkg.nested", None, ["my/pkg/nested/__init__.py"]), + ("my.pkg", None, ["my/pkg/__init__.py", "my/pkg/file.py"]), + ("my_pkg", None, ["my_pkg.py"]), + ("my_pkg", None, ["my_pkg/__init__.py", "my_pkg/nested/__init__.py"]), + ("my_pkg", None, ["src/my_pkg/__init__.py", "src/my_pkg/nested/__init__.py"]), + ( + "my_pkg", + {"my_pkg": "lib", "my_pkg.lib2": "lib2"}, + ["lib/__init__.py", "lib/nested/__init__.pyt", "lib2/__init__.py"], + ), + # Should not try to guess a name from multiple py_modules/packages + ("UNKNOWN", None, ["src/mod1.py", "src/mod2.py"]), + ("UNKNOWN", None, ["src/pkg1/__ini__.py", "src/pkg2/__init__.py"]), + ], +) +def test_dist_default_name(tmp_path, dist_name, package_dir, package_files): + """Make sure dist.name is discovered from packages/py_modules""" + ensure_files(tmp_path, package_files) + attrs = { + **EXAMPLE_BASE_INFO, + "src_root": "/".join(os.path.split(tmp_path)), # POSIX-style + "package_dir": package_dir, + } + del attrs["name"] + + dist = Distribution(attrs) + dist.set_defaults() + assert dist.py_modules or dist.packages + assert dist.get_name() == dist_name diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_dist_info.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_dist_info.py new file mode 100644 index 0000000000000000000000000000000000000000..426694e0198fdbe6ee7466822bc7c15634e2b648 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_dist_info.py @@ -0,0 +1,210 @@ +"""Test .dist-info style distributions.""" + +import pathlib +import re +import shutil +import subprocess +import sys +from functools import partial + +import pytest + +import pkg_resources +from setuptools.archive_util import unpack_archive + +from .textwrap import DALS + +read = partial(pathlib.Path.read_text, encoding="utf-8") + + +class TestDistInfo: + metadata_base = DALS( + """ + Metadata-Version: 1.2 + Requires-Dist: splort (==4) + Provides-Extra: baz + Requires-Dist: quux (>=1.1); extra == 'baz' + """ + ) + + @classmethod + def build_metadata(cls, **kwargs): + lines = ('{key}: {value}\n'.format(**locals()) for key, value in kwargs.items()) + return cls.metadata_base + ''.join(lines) + + @pytest.fixture + def metadata(self, tmpdir): + dist_info_name = 'VersionedDistribution-2.718.dist-info' + versioned = tmpdir / dist_info_name + versioned.mkdir() + filename = versioned / 'METADATA' + content = self.build_metadata( + Name='VersionedDistribution', + ) + filename.write_text(content, encoding='utf-8') + + dist_info_name = 'UnversionedDistribution.dist-info' + unversioned = tmpdir / dist_info_name + unversioned.mkdir() + filename = unversioned / 'METADATA' + content = self.build_metadata( + Name='UnversionedDistribution', + Version='0.3', + ) + filename.write_text(content, encoding='utf-8') + + return str(tmpdir) + + def test_distinfo(self, metadata): + dists = dict( + (d.project_name, d) for d in pkg_resources.find_distributions(metadata) + ) + + assert len(dists) == 2, dists + + unversioned = dists['UnversionedDistribution'] + versioned = dists['VersionedDistribution'] + + assert versioned.version == '2.718' # from filename + assert unversioned.version == '0.3' # from METADATA + + def test_conditional_dependencies(self, metadata): + specs = 'splort==4', 'quux>=1.1' + requires = list(map(pkg_resources.Requirement.parse, specs)) + + for d in pkg_resources.find_distributions(metadata): + assert d.requires() == requires[:1] + assert d.requires(extras=('baz',)) == [ + requires[0], + pkg_resources.Requirement.parse('quux>=1.1;extra=="baz"'), + ] + assert d.extras == ['baz'] + + def test_invalid_version(self, tmp_path): + """ + Supplying an invalid version crashes dist_info. + """ + config = "[metadata]\nname=proj\nversion=42\n[egg_info]\ntag_build=invalid!!!\n" + (tmp_path / "setup.cfg").write_text(config, encoding="utf-8") + msg = re.compile("invalid version", re.M | re.I) + proc = run_command_inner("dist_info", cwd=tmp_path, check=False) + assert proc.returncode + assert msg.search(proc.stdout) + assert not list(tmp_path.glob("*.dist-info")) + + def test_tag_arguments(self, tmp_path): + config = """ + [metadata] + name=proj + version=42 + [egg_info] + tag_date=1 + tag_build=.post + """ + (tmp_path / "setup.cfg").write_text(config, encoding="utf-8") + + print(run_command("dist_info", "--no-date", cwd=tmp_path)) + dist_info = next(tmp_path.glob("*.dist-info")) + assert dist_info.name.startswith("proj-42") + shutil.rmtree(dist_info) + + print(run_command("dist_info", "--tag-build", ".a", cwd=tmp_path)) + dist_info = next(tmp_path.glob("*.dist-info")) + assert dist_info.name.startswith("proj-42a") + + @pytest.mark.parametrize("keep_egg_info", (False, True)) + def test_output_dir(self, tmp_path, keep_egg_info): + config = "[metadata]\nname=proj\nversion=42\n" + (tmp_path / "setup.cfg").write_text(config, encoding="utf-8") + out = tmp_path / "__out" + out.mkdir() + opts = ["--keep-egg-info"] if keep_egg_info else [] + run_command("dist_info", "--output-dir", out, *opts, cwd=tmp_path) + assert len(list(out.glob("*.dist-info"))) == 1 + assert len(list(tmp_path.glob("*.dist-info"))) == 0 + expected_egg_info = int(keep_egg_info) + assert len(list(out.glob("*.egg-info"))) == expected_egg_info + assert len(list(tmp_path.glob("*.egg-info"))) == 0 + assert len(list(out.glob("*.__bkp__"))) == 0 + assert len(list(tmp_path.glob("*.__bkp__"))) == 0 + + +class TestWheelCompatibility: + """Make sure the .dist-info directory produced with the ``dist_info`` command + is the same as the one produced by ``bdist_wheel``. + """ + + SETUPCFG = DALS( + """ + [metadata] + name = {name} + version = {version} + + [options] + install_requires = + foo>=12; sys_platform != "linux" + + [options.extras_require] + test = pytest + + [options.entry_points] + console_scripts = + executable-name = my_package.module:function + discover = + myproj = my_package.other_module:function + """ + ) + + EGG_INFO_OPTS = [ + # Related: #3088 #2872 + ("", ""), + (".post", "[egg_info]\ntag_build = post\n"), + (".post", "[egg_info]\ntag_build = .post\n"), + (".post", "[egg_info]\ntag_build = post\ntag_date = 1\n"), + (".dev", "[egg_info]\ntag_build = .dev\n"), + (".dev", "[egg_info]\ntag_build = .dev\ntag_date = 1\n"), + ("a1", "[egg_info]\ntag_build = .a1\n"), + ("+local", "[egg_info]\ntag_build = +local\n"), + ] + + @pytest.mark.parametrize("name", "my-proj my_proj my.proj My.Proj".split()) + @pytest.mark.parametrize("version", ["0.42.13"]) + @pytest.mark.parametrize(("suffix", "cfg"), EGG_INFO_OPTS) + def test_dist_info_is_the_same_as_in_wheel( + self, name, version, tmp_path, suffix, cfg + ): + config = self.SETUPCFG.format(name=name, version=version) + cfg + + for i in "dir_wheel", "dir_dist": + (tmp_path / i).mkdir() + (tmp_path / i / "setup.cfg").write_text(config, encoding="utf-8") + + run_command("bdist_wheel", cwd=tmp_path / "dir_wheel") + wheel = next(tmp_path.glob("dir_wheel/dist/*.whl")) + unpack_archive(wheel, tmp_path / "unpack") + wheel_dist_info = next(tmp_path.glob("unpack/*.dist-info")) + + run_command("dist_info", cwd=tmp_path / "dir_dist") + dist_info = next(tmp_path.glob("dir_dist/*.dist-info")) + + assert dist_info.name == wheel_dist_info.name + assert dist_info.name.startswith(f"my_proj-{version}{suffix}") + for file in "METADATA", "entry_points.txt": + assert read(dist_info / file) == read(wheel_dist_info / file) + + +def run_command_inner(*cmd, **kwargs): + opts = { + "stderr": subprocess.STDOUT, + "stdout": subprocess.PIPE, + "text": True, + "encoding": "utf-8", + "check": True, + **kwargs, + } + cmd = [sys.executable, "-c", "__import__('setuptools').setup()", *map(str, cmd)] + return subprocess.run(cmd, **opts) + + +def run_command(*args, **kwargs): + return run_command_inner(*args, **kwargs).stdout diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_distutils_adoption.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_distutils_adoption.py new file mode 100644 index 0000000000000000000000000000000000000000..f99a58849950029f53322c19cde1c171fe26622c --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_distutils_adoption.py @@ -0,0 +1,198 @@ +import os +import platform +import sys +import textwrap + +import pytest + +IS_PYPY = '__pypy__' in sys.builtin_module_names + +_TEXT_KWARGS = {"text": True, "encoding": "utf-8"} # For subprocess.run + + +def win_sr(env): + """ + On Windows, SYSTEMROOT must be present to avoid + + > Fatal Python error: _Py_HashRandomization_Init: failed to + > get random numbers to initialize Python + """ + if env and platform.system() == 'Windows': + env['SYSTEMROOT'] = os.environ['SYSTEMROOT'] + return env + + +def find_distutils(venv, imports='distutils', env=None, **kwargs): + py_cmd = 'import {imports}; print(distutils.__file__)'.format(**locals()) + cmd = ['python', '-c', py_cmd] + return venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS, **kwargs) + + +def count_meta_path(venv, env=None): + py_cmd = textwrap.dedent( + """ + import sys + is_distutils = lambda finder: finder.__class__.__name__ == "DistutilsMetaFinder" + print(len(list(filter(is_distutils, sys.meta_path)))) + """ + ) + cmd = ['python', '-c', py_cmd] + return int(venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS)) + + +skip_without_stdlib_distutils = pytest.mark.skipif( + sys.version_info >= (3, 12), + reason='stdlib distutils is removed from Python 3.12+', +) + + +@skip_without_stdlib_distutils +def test_distutils_stdlib(venv): + """ + Ensure stdlib distutils is used when appropriate. + """ + env = dict(SETUPTOOLS_USE_DISTUTILS='stdlib') + assert venv.name not in find_distutils(venv, env=env).split(os.sep) + assert count_meta_path(venv, env=env) == 0 + + +def test_distutils_local_with_setuptools(venv): + """ + Ensure local distutils is used when appropriate. + """ + env = dict(SETUPTOOLS_USE_DISTUTILS='local') + loc = find_distutils(venv, imports='setuptools, distutils', env=env) + assert venv.name in loc.split(os.sep) + assert count_meta_path(venv, env=env) <= 1 + + +@pytest.mark.xfail('IS_PYPY', reason='pypy imports distutils on startup') +def test_distutils_local(venv): + """ + Even without importing, the setuptools-local copy of distutils is + preferred. + """ + env = dict(SETUPTOOLS_USE_DISTUTILS='local') + assert venv.name in find_distutils(venv, env=env).split(os.sep) + assert count_meta_path(venv, env=env) <= 1 + + +def test_pip_import(venv): + """ + Ensure pip can be imported. + Regression test for #3002. + """ + cmd = ['python', '-c', 'import pip'] + venv.run(cmd, **_TEXT_KWARGS) + + +def test_distutils_has_origin(): + """ + Distutils module spec should have an origin. #2990. + """ + assert __import__('distutils').__spec__.origin + + +ENSURE_IMPORTS_ARE_NOT_DUPLICATED = r""" +# Depending on the importlib machinery and _distutils_hack, some imports are +# duplicated resulting in different module objects being loaded, which prevents +# patches as shown in #3042. +# This script provides a way of verifying if this duplication is happening. + +from distutils import cmd +import distutils.command.sdist as sdist + +# import last to prevent caching +from distutils import {imported_module} + +for mod in (cmd, sdist): + assert mod.{imported_module} == {imported_module}, ( + f"\n{{mod.dir_util}}\n!=\n{{{imported_module}}}" + ) + +print("success") +""" + + +@pytest.mark.usefixtures("tmpdir_cwd") +@pytest.mark.parametrize( + ('distutils_version', 'imported_module'), + [ + pytest.param("stdlib", "dir_util", marks=skip_without_stdlib_distutils), + pytest.param("stdlib", "file_util", marks=skip_without_stdlib_distutils), + pytest.param("stdlib", "archive_util", marks=skip_without_stdlib_distutils), + ("local", "dir_util"), + ("local", "file_util"), + ("local", "archive_util"), + ], +) +def test_modules_are_not_duplicated_on_import(distutils_version, imported_module, venv): + env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version) + script = ENSURE_IMPORTS_ARE_NOT_DUPLICATED.format(imported_module=imported_module) + cmd = ['python', '-c', script] + output = venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS).strip() + assert output == "success" + + +ENSURE_LOG_IMPORT_IS_NOT_DUPLICATED = r""" +import types +import distutils.dist as dist +from distutils import log +if isinstance(dist.log, types.ModuleType): + assert dist.log == log, f"\n{dist.log}\n!=\n{log}" +print("success") +""" + + +@pytest.mark.usefixtures("tmpdir_cwd") +@pytest.mark.parametrize( + "distutils_version", + [ + "local", + pytest.param("stdlib", marks=skip_without_stdlib_distutils), + ], +) +def test_log_module_is_not_duplicated_on_import(distutils_version, venv): + env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version) + cmd = ['python', '-c', ENSURE_LOG_IMPORT_IS_NOT_DUPLICATED] + output = venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS).strip() + assert output == "success" + + +ENSURE_CONSISTENT_ERROR_FROM_MODIFIED_PY = r""" +from setuptools.modified import newer +from {imported_module}.errors import DistutilsError + +# Can't use pytest.raises in this context +try: + newer("", "") +except DistutilsError: + print("success") +else: + raise AssertionError("Expected to raise") +""" + + +@pytest.mark.usefixtures("tmpdir_cwd") +@pytest.mark.parametrize( + ('distutils_version', 'imported_module'), + [ + ("local", "distutils"), + # Unfortunately we still get ._distutils.errors.DistutilsError with SETUPTOOLS_USE_DISTUTILS=stdlib + # But that's a deprecated use-case we don't mind not fully supporting in newer code + pytest.param( + "stdlib", "setuptools._distutils", marks=skip_without_stdlib_distutils + ), + ], +) +def test_consistent_error_from_modified_py(distutils_version, imported_module, venv): + env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version) + cmd = [ + 'python', + '-c', + ENSURE_CONSISTENT_ERROR_FROM_MODIFIED_PY.format( + imported_module=imported_module + ), + ] + output = venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS).strip() + assert output == "success" diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_easy_install.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_easy_install.py new file mode 100644 index 0000000000000000000000000000000000000000..b58b0b66663e4bfb6e26bd730e62bff8267eeff5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_easy_install.py @@ -0,0 +1,1476 @@ +"""Easy install Tests""" + +import contextlib +import io +import itertools +import logging +import os +import pathlib +import re +import site +import subprocess +import sys +import tarfile +import tempfile +import time +import warnings +import zipfile +from pathlib import Path +from typing import NamedTuple +from unittest import mock + +import pytest +from jaraco import path + +import pkg_resources +import setuptools.command.easy_install as ei +from pkg_resources import Distribution as PRDistribution, normalize_path, working_set +from setuptools import sandbox +from setuptools._normalization import safer_name +from setuptools.command.easy_install import PthDistributions +from setuptools.dist import Distribution +from setuptools.sandbox import run_setup +from setuptools.tests import fail_on_ascii +from setuptools.tests.server import MockServer, path_to_url + +from . import contexts +from .textwrap import DALS + +import distutils.errors + + +@pytest.fixture(autouse=True) +def pip_disable_index(monkeypatch): + """ + Important: Disable the default index for pip to avoid + querying packages in the index and potentially resolving + and installing packages there. + """ + monkeypatch.setenv('PIP_NO_INDEX', 'true') + + +class FakeDist: + def get_entry_map(self, group): + if group != 'console_scripts': + return {} + return {'name': 'ep'} + + def as_requirement(self): + return 'spec' + + +SETUP_PY = DALS( + """ + from setuptools import setup + + setup() + """ +) + + +class TestEasyInstallTest: + def test_get_script_args(self): + header = ei.CommandSpec.best().from_environment().as_header() + dist = FakeDist() + args = next(ei.ScriptWriter.get_args(dist)) + _name, script = itertools.islice(args, 2) + assert script.startswith(header) + assert "'spec'" in script + assert "'console_scripts'" in script + assert "'name'" in script + assert re.search('^# EASY-INSTALL-ENTRY-SCRIPT', script, flags=re.MULTILINE) + + def test_no_find_links(self): + # new option '--no-find-links', that blocks find-links added at + # the project level + dist = Distribution() + cmd = ei.easy_install(dist) + cmd.check_pth_processing = lambda: True + cmd.no_find_links = True + cmd.find_links = ['link1', 'link2'] + cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok') + cmd.args = ['ok'] + cmd.ensure_finalized() + assert cmd.package_index.scanned_urls == {} + + # let's try without it (default behavior) + cmd = ei.easy_install(dist) + cmd.check_pth_processing = lambda: True + cmd.find_links = ['link1', 'link2'] + cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok') + cmd.args = ['ok'] + cmd.ensure_finalized() + keys = sorted(cmd.package_index.scanned_urls.keys()) + assert keys == ['link1', 'link2'] + + def test_write_exception(self): + """ + Test that `cant_write_to_target` is rendered as a DistutilsError. + """ + dist = Distribution() + cmd = ei.easy_install(dist) + cmd.install_dir = os.getcwd() + with pytest.raises(distutils.errors.DistutilsError): + cmd.cant_write_to_target() + + def test_all_site_dirs(self, monkeypatch): + """ + get_site_dirs should always return site dirs reported by + site.getsitepackages. + """ + path = normalize_path('/setuptools/test/site-packages') + + def mock_gsp(): + return [path] + + monkeypatch.setattr(site, 'getsitepackages', mock_gsp, raising=False) + assert path in ei.get_site_dirs() + + def test_all_site_dirs_works_without_getsitepackages(self, monkeypatch): + monkeypatch.delattr(site, 'getsitepackages', raising=False) + assert ei.get_site_dirs() + + @pytest.fixture + def sdist_unicode(self, tmpdir): + files = [ + ( + 'setup.py', + DALS( + """ + import setuptools + setuptools.setup( + name="setuptools-test-unicode", + version="1.0", + packages=["mypkg"], + include_package_data=True, + ) + """ + ), + ), + ( + 'mypkg/__init__.py', + "", + ), + ( + 'mypkg/☃.txt', + "", + ), + ] + sdist_name = 'setuptools-test-unicode-1.0.zip' + sdist = tmpdir / sdist_name + # can't use make_sdist, because the issue only occurs + # with zip sdists. + sdist_zip = zipfile.ZipFile(str(sdist), 'w') + for filename, content in files: + sdist_zip.writestr(filename, content) + sdist_zip.close() + return str(sdist) + + @fail_on_ascii + def test_unicode_filename_in_sdist(self, sdist_unicode, tmpdir, monkeypatch): + """ + The install command should execute correctly even if + the package has unicode filenames. + """ + dist = Distribution({'script_args': ['easy_install']}) + target = (tmpdir / 'target').ensure_dir() + cmd = ei.easy_install( + dist, + install_dir=str(target), + args=['x'], + ) + monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target)) + cmd.ensure_finalized() + cmd.easy_install(sdist_unicode) + + @pytest.fixture + def sdist_unicode_in_script(self, tmpdir): + files = [ + ( + "setup.py", + DALS( + """ + import setuptools + setuptools.setup( + name="setuptools-test-unicode", + version="1.0", + packages=["mypkg"], + include_package_data=True, + scripts=['mypkg/unicode_in_script'], + ) + """ + ), + ), + ("mypkg/__init__.py", ""), + ( + "mypkg/unicode_in_script", + DALS( + """ + #!/bin/sh + # á + + non_python_fn() { + } + """ + ), + ), + ] + sdist_name = "setuptools-test-unicode-script-1.0.zip" + sdist = tmpdir / sdist_name + # can't use make_sdist, because the issue only occurs + # with zip sdists. + sdist_zip = zipfile.ZipFile(str(sdist), "w") + for filename, content in files: + sdist_zip.writestr(filename, content.encode('utf-8')) + sdist_zip.close() + return str(sdist) + + @fail_on_ascii + def test_unicode_content_in_sdist( + self, sdist_unicode_in_script, tmpdir, monkeypatch + ): + """ + The install command should execute correctly even if + the package has unicode in scripts. + """ + dist = Distribution({"script_args": ["easy_install"]}) + target = (tmpdir / "target").ensure_dir() + cmd = ei.easy_install(dist, install_dir=str(target), args=["x"]) + monkeypatch.setitem(os.environ, "PYTHONPATH", str(target)) + cmd.ensure_finalized() + cmd.easy_install(sdist_unicode_in_script) + + @pytest.fixture + def sdist_script(self, tmpdir): + files = [ + ( + 'setup.py', + DALS( + """ + import setuptools + setuptools.setup( + name="setuptools-test-script", + version="1.0", + scripts=["mypkg_script"], + ) + """ + ), + ), + ( + 'mypkg_script', + DALS( + """ + #/usr/bin/python + print('mypkg_script') + """ + ), + ), + ] + sdist_name = 'setuptools-test-script-1.0.zip' + sdist = str(tmpdir / sdist_name) + make_sdist(sdist, files) + return sdist + + @pytest.mark.skipif( + not sys.platform.startswith('linux'), reason="Test can only be run on Linux" + ) + def test_script_install(self, sdist_script, tmpdir, monkeypatch): + """ + Check scripts are installed. + """ + dist = Distribution({'script_args': ['easy_install']}) + target = (tmpdir / 'target').ensure_dir() + cmd = ei.easy_install( + dist, + install_dir=str(target), + args=['x'], + ) + monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target)) + cmd.ensure_finalized() + cmd.easy_install(sdist_script) + assert (target / 'mypkg_script').exists() + + +@pytest.mark.filterwarnings('ignore:Unbuilt egg') +class TestPTHFileWriter: + def test_add_from_cwd_site_sets_dirty(self): + """a pth file manager should set dirty + if a distribution is in site but also the cwd + """ + pth = PthDistributions('does-not_exist', [os.getcwd()]) + assert not pth.dirty + pth.add(PRDistribution(os.getcwd())) + assert pth.dirty + + def test_add_from_site_is_ignored(self): + location = '/test/location/does-not-have-to-exist' + # PthDistributions expects all locations to be normalized + location = pkg_resources.normalize_path(location) + pth = PthDistributions( + 'does-not_exist', + [ + location, + ], + ) + assert not pth.dirty + pth.add(PRDistribution(location)) + assert not pth.dirty + + def test_many_pth_distributions_merge_together(self, tmpdir): + """ + If the pth file is modified under the hood, then PthDistribution + will refresh its content before saving, merging contents when + necessary. + """ + # putting the pth file in a dedicated sub-folder, + pth_subdir = tmpdir.join("pth_subdir") + pth_subdir.mkdir() + pth_path = str(pth_subdir.join("file1.pth")) + pth1 = PthDistributions(pth_path) + pth2 = PthDistributions(pth_path) + assert pth1.paths == pth2.paths == [], ( + "unless there would be some default added at some point" + ) + # and so putting the src_subdir in folder distinct than the pth one, + # so to keep it absolute by PthDistributions + new_src_path = tmpdir.join("src_subdir") + new_src_path.mkdir() # must exist to be accounted + new_src_path_str = str(new_src_path) + pth1.paths.append(new_src_path_str) + pth1.save() + assert pth1.paths, ( + "the new_src_path added must still be present/valid in pth1 after save" + ) + # now, + assert new_src_path_str not in pth2.paths, ( + "right before we save the entry should still not be present" + ) + pth2.save() + assert new_src_path_str in pth2.paths, ( + "the new_src_path entry should have been added by pth2 with its save() call" + ) + assert pth2.paths[-1] == new_src_path, ( + "and it should match exactly on the last entry actually " + "given we append to it in save()" + ) + # finally, + assert PthDistributions(pth_path).paths == pth2.paths, ( + "and we should have the exact same list at the end " + "with a fresh PthDistributions instance" + ) + + +@pytest.fixture +def setup_context(tmpdir): + with (tmpdir / 'setup.py').open('w', encoding="utf-8") as f: + f.write(SETUP_PY) + with tmpdir.as_cwd(): + yield tmpdir + + +@pytest.mark.usefixtures("user_override") +@pytest.mark.usefixtures("setup_context") +class TestUserInstallTest: + # prevent check that site-packages is writable. easy_install + # shouldn't be writing to system site-packages during finalize + # options, but while it does, bypass the behavior. + prev_sp_write = mock.patch( + 'setuptools.command.easy_install.easy_install.check_site_dir', + mock.Mock(), + ) + + # simulate setuptools installed in user site packages + @mock.patch('setuptools.command.easy_install.__file__', site.USER_SITE) + @mock.patch('site.ENABLE_USER_SITE', True) + @prev_sp_write + def test_user_install_not_implied_user_site_enabled(self): + self.assert_not_user_site() + + @mock.patch('site.ENABLE_USER_SITE', False) + @prev_sp_write + def test_user_install_not_implied_user_site_disabled(self): + self.assert_not_user_site() + + @staticmethod + def assert_not_user_site(): + # create a finalized easy_install command + dist = Distribution() + dist.script_name = 'setup.py' + cmd = ei.easy_install(dist) + cmd.args = ['py'] + cmd.ensure_finalized() + assert not cmd.user, 'user should not be implied' + + def test_multiproc_atexit(self): + pytest.importorskip('multiprocessing') + + log = logging.getLogger('test_easy_install') + logging.basicConfig(level=logging.INFO, stream=sys.stderr) + log.info('this should not break') + + @pytest.fixture + def foo_package(self, tmpdir): + egg_file = tmpdir / 'foo-1.0.egg-info' + with egg_file.open('w') as f: + f.write('Name: foo\n') + return str(tmpdir) + + @pytest.fixture + def install_target(self, tmpdir): + target = str(tmpdir) + with mock.patch('sys.path', sys.path + [target]): + python_path = os.path.pathsep.join(sys.path) + with mock.patch.dict(os.environ, PYTHONPATH=python_path): + yield target + + def test_local_index(self, foo_package, install_target): + """ + The local index must be used when easy_install locates installed + packages. + """ + dist = Distribution() + dist.script_name = 'setup.py' + cmd = ei.easy_install(dist) + cmd.install_dir = install_target + cmd.args = ['foo'] + cmd.ensure_finalized() + cmd.local_index.scan([foo_package]) + res = cmd.easy_install('foo') + actual = os.path.normcase(os.path.realpath(res.location)) + expected = os.path.normcase(os.path.realpath(foo_package)) + assert actual == expected + + @contextlib.contextmanager + def user_install_setup_context(self, *args, **kwargs): + """ + Wrap sandbox.setup_context to patch easy_install in that context to + appear as user-installed. + """ + with self.orig_context(*args, **kwargs): + import setuptools.command.easy_install as ei + + ei.__file__ = site.USER_SITE + yield + + def patched_setup_context(self): + self.orig_context = sandbox.setup_context + + return mock.patch( + 'setuptools.sandbox.setup_context', + self.user_install_setup_context, + ) + + +@pytest.fixture +def distutils_package(): + distutils_setup_py = SETUP_PY.replace( + 'from setuptools import setup', + 'from distutils.core import setup', + ) + with contexts.tempdir(cd=os.chdir): + with open('setup.py', 'w', encoding="utf-8") as f: + f.write(distutils_setup_py) + yield + + +@pytest.mark.usefixtures("distutils_package") +class TestDistutilsPackage: + def test_bdist_egg_available_on_distutils_pkg(self): + run_setup('setup.py', ['bdist_egg']) + + +@pytest.fixture +def mock_index(): + # set up a server which will simulate an alternate package index. + p_index = MockServer() + if p_index.server_port == 0: + # Some platforms (Jython) don't find a port to which to bind, + # so skip test for them. + pytest.skip("could not find a valid port") + p_index.start() + return p_index + + +class TestInstallRequires: + def test_setup_install_includes_dependencies(self, tmp_path, mock_index): + """ + When ``python setup.py install`` is called directly, it will use easy_install + to fetch dependencies. + """ + # TODO: Remove these tests once `setup.py install` is completely removed + project_root = tmp_path / "project" + project_root.mkdir(exist_ok=True) + install_root = tmp_path / "install" + install_root.mkdir(exist_ok=True) + + self.create_project(project_root) + cmd = [ + sys.executable, + '-c', + '__import__("setuptools").setup()', + 'install', + '--install-base', + str(install_root), + '--install-lib', + str(install_root), + '--install-headers', + str(install_root), + '--install-scripts', + str(install_root), + '--install-data', + str(install_root), + '--install-purelib', + str(install_root), + '--install-platlib', + str(install_root), + ] + env = {**os.environ, "__EASYINSTALL_INDEX": mock_index.url} + cp = subprocess.run( + cmd, + cwd=str(project_root), + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding="utf-8", + ) + assert cp.returncode != 0 + try: + assert '/does-not-exist/' in {r.path for r in mock_index.requests} + assert next( + line + for line in cp.stdout.splitlines() + if "not find suitable distribution for" in line + and "does-not-exist" in line + ) + except Exception: + if "failed to get random numbers" in cp.stdout: + pytest.xfail(f"{sys.platform} failure - {cp.stdout}") + raise + + def create_project(self, root): + config = """ + [metadata] + name = project + version = 42 + + [options] + install_requires = does-not-exist + py_modules = mod + """ + (root / 'setup.cfg').write_text(DALS(config), encoding="utf-8") + (root / 'mod.py').touch() + + +class TestSetupRequires: + def test_setup_requires_honors_fetch_params(self, mock_index, monkeypatch): + """ + When easy_install installs a source distribution which specifies + setup_requires, it should honor the fetch parameters (such as + index-url, and find-links). + """ + monkeypatch.setenv('PIP_RETRIES', '0') + monkeypatch.setenv('PIP_TIMEOUT', '0') + monkeypatch.setenv('PIP_NO_INDEX', 'false') + with contexts.quiet(): + # create an sdist that has a build-time dependency. + with TestSetupRequires.create_sdist() as dist_file: + with contexts.tempdir() as temp_install_dir: + with contexts.environment(PYTHONPATH=temp_install_dir): + cmd = [ + sys.executable, + '-c', + '__import__("setuptools").setup()', + 'easy_install', + '--index-url', + mock_index.url, + '--exclude-scripts', + '--install-dir', + temp_install_dir, + dist_file, + ] + subprocess.Popen(cmd).wait() + # there should have been one requests to the server + assert [r.path for r in mock_index.requests] == ['/does-not-exist/'] + + @staticmethod + @contextlib.contextmanager + def create_sdist(): + """ + Return an sdist with a setup_requires dependency (of something that + doesn't exist) + """ + with contexts.tempdir() as dir: + dist_path = os.path.join(dir, 'setuptools-test-fetcher-1.0.tar.gz') + make_sdist( + dist_path, + [ + ( + 'setup.py', + DALS( + """ + import setuptools + setuptools.setup( + name="setuptools-test-fetcher", + version="1.0", + setup_requires = ['does-not-exist'], + ) + """ + ), + ), + ('setup.cfg', ''), + ], + ) + yield dist_path + + use_setup_cfg = ( + (), + ('dependency_links',), + ('setup_requires',), + ('dependency_links', 'setup_requires'), + ) + + @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg) + def test_setup_requires_overrides_version_conflict(self, use_setup_cfg): + """ + Regression test for distribution issue 323: + https://bitbucket.org/tarek/distribute/issues/323 + + Ensures that a distribution's setup_requires requirements can still be + installed and used locally even if a conflicting version of that + requirement is already on the path. + """ + + fake_dist = PRDistribution( + 'does-not-matter', project_name='foobar', version='0.0' + ) + working_set.add(fake_dist) + + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + test_pkg = create_setup_requires_package( + temp_dir, use_setup_cfg=use_setup_cfg + ) + test_setup_py = os.path.join(test_pkg, 'setup.py') + with contexts.quiet() as (stdout, _stderr): + # Don't even need to install the package, just + # running the setup.py at all is sufficient + run_setup(test_setup_py, ['--name']) + + lines = stdout.readlines() + assert len(lines) > 0 + assert lines[-1].strip() == 'test_pkg' + + @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg) + def test_setup_requires_override_nspkg(self, use_setup_cfg): + """ + Like ``test_setup_requires_overrides_version_conflict`` but where the + ``setup_requires`` package is part of a namespace package that has + *already* been imported. + """ + + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + foobar_1_archive = os.path.join(temp_dir, 'foo_bar-0.1.tar.gz') + make_nspkg_sdist(foobar_1_archive, 'foo.bar', '0.1') + # Now actually go ahead an extract to the temp dir and add the + # extracted path to sys.path so foo.bar v0.1 is importable + foobar_1_dir = os.path.join(temp_dir, 'foo_bar-0.1') + os.mkdir(foobar_1_dir) + with tarfile.open(foobar_1_archive) as tf: + tf.extraction_filter = lambda member, path: member + tf.extractall(foobar_1_dir) + sys.path.insert(1, foobar_1_dir) + + dist = PRDistribution( + foobar_1_dir, project_name='foo.bar', version='0.1' + ) + working_set.add(dist) + + template = DALS( + """\ + import foo # Even with foo imported first the + # setup_requires package should override + import setuptools + setuptools.setup(**%r) + + if not (hasattr(foo, '__path__') and + len(foo.__path__) == 2): + print('FAIL') + + if 'foo_bar-0.2' not in foo.__path__[0]: + print('FAIL') + """ + ) + + test_pkg = create_setup_requires_package( + temp_dir, + 'foo.bar', + '0.2', + make_nspkg_sdist, + template, + use_setup_cfg=use_setup_cfg, + ) + + test_setup_py = os.path.join(test_pkg, 'setup.py') + + with contexts.quiet() as (stdout, _stderr): + try: + # Don't even need to install the package, just + # running the setup.py at all is sufficient + run_setup(test_setup_py, ['--name']) + except pkg_resources.VersionConflict: # pragma: nocover + pytest.fail( + 'Installing setup.py requirements caused a VersionConflict' + ) + + assert 'FAIL' not in stdout.getvalue() + lines = stdout.readlines() + assert len(lines) > 0 + assert lines[-1].strip() == 'test_pkg' + + @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg) + def test_setup_requires_with_attr_version(self, use_setup_cfg): + def make_dependency_sdist(dist_path, distname, version): + files = [ + ( + 'setup.py', + DALS( + f""" + import setuptools + setuptools.setup( + name={distname!r}, + version={version!r}, + py_modules=[{distname!r}], + ) + """ + ), + ), + ( + distname + '.py', + DALS( + """ + version = 42 + """ + ), + ), + ] + make_sdist(dist_path, files) + + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + test_pkg = create_setup_requires_package( + temp_dir, + setup_attrs=dict(version='attr: foobar.version'), + make_package=make_dependency_sdist, + use_setup_cfg=use_setup_cfg + ('version',), + ) + test_setup_py = os.path.join(test_pkg, 'setup.py') + with contexts.quiet() as (stdout, _stderr): + run_setup(test_setup_py, ['--version']) + lines = stdout.readlines() + assert len(lines) > 0 + assert lines[-1].strip() == '42' + + def test_setup_requires_honors_pip_env(self, mock_index, monkeypatch): + monkeypatch.setenv('PIP_RETRIES', '0') + monkeypatch.setenv('PIP_TIMEOUT', '0') + monkeypatch.setenv('PIP_NO_INDEX', 'false') + monkeypatch.setenv('PIP_INDEX_URL', mock_index.url) + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + test_pkg = create_setup_requires_package( + temp_dir, + 'python-xlib', + '0.19', + setup_attrs=dict(dependency_links=[]), + ) + test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') + with open(test_setup_cfg, 'w', encoding="utf-8") as fp: + fp.write( + DALS( + """ + [easy_install] + index_url = https://pypi.org/legacy/ + """ + ) + ) + test_setup_py = os.path.join(test_pkg, 'setup.py') + with pytest.raises(distutils.errors.DistutilsError): + run_setup(test_setup_py, ['--version']) + assert len(mock_index.requests) == 1 + assert mock_index.requests[0].path == '/python-xlib/' + + def test_setup_requires_with_pep508_url(self, mock_index, monkeypatch): + monkeypatch.setenv('PIP_RETRIES', '0') + monkeypatch.setenv('PIP_TIMEOUT', '0') + monkeypatch.setenv('PIP_INDEX_URL', mock_index.url) + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + dep_sdist = os.path.join(temp_dir, 'dep.tar.gz') + make_trivial_sdist(dep_sdist, 'dependency', '42') + dep_url = path_to_url(dep_sdist, authority='localhost') + test_pkg = create_setup_requires_package( + temp_dir, + # Ignored (overridden by setup_attrs) + 'python-xlib', + '0.19', + setup_attrs=dict(setup_requires=f'dependency @ {dep_url}'), + ) + test_setup_py = os.path.join(test_pkg, 'setup.py') + run_setup(test_setup_py, ['--version']) + assert len(mock_index.requests) == 0 + + def test_setup_requires_with_allow_hosts(self, mock_index): + """The `allow-hosts` option in not supported anymore.""" + files = { + 'test_pkg': { + 'setup.py': DALS( + """ + from setuptools import setup + setup(setup_requires='python-xlib') + """ + ), + 'setup.cfg': DALS( + """ + [easy_install] + allow_hosts = * + """ + ), + } + } + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + path.build(files, prefix=temp_dir) + setup_py = str(pathlib.Path(temp_dir, 'test_pkg', 'setup.py')) + with pytest.raises(distutils.errors.DistutilsError): + run_setup(setup_py, ['--version']) + assert len(mock_index.requests) == 0 + + def test_setup_requires_with_python_requires(self, monkeypatch, tmpdir): + """Check `python_requires` is honored.""" + monkeypatch.setenv('PIP_RETRIES', '0') + monkeypatch.setenv('PIP_TIMEOUT', '0') + monkeypatch.setenv('PIP_NO_INDEX', '1') + monkeypatch.setenv('PIP_VERBOSE', '1') + dep_1_0_sdist = 'dep-1.0.tar.gz' + dep_1_0_url = path_to_url(str(tmpdir / dep_1_0_sdist)) + dep_1_0_python_requires = '>=2.7' + make_python_requires_sdist( + str(tmpdir / dep_1_0_sdist), 'dep', '1.0', dep_1_0_python_requires + ) + dep_2_0_sdist = 'dep-2.0.tar.gz' + dep_2_0_url = path_to_url(str(tmpdir / dep_2_0_sdist)) + dep_2_0_python_requires = ( + f'!={sys.version_info.major}.{sys.version_info.minor}.*' + ) + make_python_requires_sdist( + str(tmpdir / dep_2_0_sdist), 'dep', '2.0', dep_2_0_python_requires + ) + index = tmpdir / 'index.html' + index.write_text( + DALS( + """ + + Links for dep + +

Links for dep

+ {dep_1_0_sdist}
+ {dep_2_0_sdist}
+ + + """ + ).format( + dep_1_0_url=dep_1_0_url, + dep_1_0_sdist=dep_1_0_sdist, + dep_1_0_python_requires=dep_1_0_python_requires, + dep_2_0_url=dep_2_0_url, + dep_2_0_sdist=dep_2_0_sdist, + dep_2_0_python_requires=dep_2_0_python_requires, + ), + 'utf-8', + ) + index_url = path_to_url(str(index)) + with contexts.save_pkg_resources_state(): + test_pkg = create_setup_requires_package( + str(tmpdir), + 'python-xlib', + '0.19', # Ignored (overridden by setup_attrs). + setup_attrs=dict(setup_requires='dep', dependency_links=[index_url]), + ) + test_setup_py = os.path.join(test_pkg, 'setup.py') + run_setup(test_setup_py, ['--version']) + eggs = list( + map(str, pkg_resources.find_distributions(os.path.join(test_pkg, '.eggs'))) + ) + assert eggs == ['dep 1.0'] + + @pytest.mark.parametrize('with_dependency_links_in_setup_py', (False, True)) + def test_setup_requires_with_find_links_in_setup_cfg( + self, monkeypatch, with_dependency_links_in_setup_py + ): + monkeypatch.setenv('PIP_RETRIES', '0') + monkeypatch.setenv('PIP_TIMEOUT', '0') + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + make_trivial_sdist( + os.path.join(temp_dir, 'python-xlib-42.tar.gz'), 'python-xlib', '42' + ) + test_pkg = os.path.join(temp_dir, 'test_pkg') + test_setup_py = os.path.join(test_pkg, 'setup.py') + test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') + os.mkdir(test_pkg) + with open(test_setup_py, 'w', encoding="utf-8") as fp: + if with_dependency_links_in_setup_py: + dependency_links = [os.path.join(temp_dir, 'links')] + else: + dependency_links = [] + fp.write( + DALS( + """ + from setuptools import installer, setup + setup(setup_requires='python-xlib==42', + dependency_links={dependency_links!r}) + """ + ).format(dependency_links=dependency_links) + ) + with open(test_setup_cfg, 'w', encoding="utf-8") as fp: + fp.write( + DALS( + """ + [easy_install] + index_url = {index_url} + find_links = {find_links} + """ + ).format( + index_url=os.path.join(temp_dir, 'index'), + find_links=temp_dir, + ) + ) + run_setup(test_setup_py, ['--version']) + + def test_setup_requires_with_transitive_extra_dependency(self, monkeypatch): + """ + Use case: installing a package with a build dependency on + an already installed `dep[extra]`, which in turn depends + on `extra_dep` (whose is not already installed). + """ + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + # Create source distribution for `extra_dep`. + make_trivial_sdist( + os.path.join(temp_dir, 'extra_dep-1.0.tar.gz'), 'extra_dep', '1.0' + ) + # Create source tree for `dep`. + dep_pkg = os.path.join(temp_dir, 'dep') + os.mkdir(dep_pkg) + path.build( + { + 'setup.py': DALS( + """ + import setuptools + setuptools.setup( + name='dep', version='2.0', + extras_require={'extra': ['extra_dep']}, + ) + """ + ), + 'setup.cfg': '', + }, + prefix=dep_pkg, + ) + # "Install" dep. + run_setup(os.path.join(dep_pkg, 'setup.py'), ['dist_info']) + working_set.add_entry(dep_pkg) + # Create source tree for test package. + test_pkg = os.path.join(temp_dir, 'test_pkg') + test_setup_py = os.path.join(test_pkg, 'setup.py') + os.mkdir(test_pkg) + with open(test_setup_py, 'w', encoding="utf-8") as fp: + fp.write( + DALS( + """ + from setuptools import installer, setup + setup(setup_requires='dep[extra]') + """ + ) + ) + # Check... + monkeypatch.setenv('PIP_FIND_LINKS', str(temp_dir)) + monkeypatch.setenv('PIP_NO_INDEX', '1') + monkeypatch.setenv('PIP_RETRIES', '0') + monkeypatch.setenv('PIP_TIMEOUT', '0') + run_setup(test_setup_py, ['--version']) + + def test_setup_requires_with_distutils_command_dep(self, monkeypatch): + """ + Use case: ensure build requirements' extras + are properly installed and activated. + """ + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + # Create source distribution for `extra_dep`. + make_sdist( + os.path.join(temp_dir, 'extra_dep-1.0.tar.gz'), + [ + ( + 'setup.py', + DALS( + """ + import setuptools + setuptools.setup( + name='extra_dep', + version='1.0', + py_modules=['extra_dep'], + ) + """ + ), + ), + ('setup.cfg', ''), + ('extra_dep.py', ''), + ], + ) + # Create source tree for `epdep`. + dep_pkg = os.path.join(temp_dir, 'epdep') + os.mkdir(dep_pkg) + path.build( + { + 'setup.py': DALS( + """ + import setuptools + setuptools.setup( + name='dep', version='2.0', + py_modules=['epcmd'], + extras_require={'extra': ['extra_dep']}, + entry_points=''' + [distutils.commands] + epcmd = epcmd:epcmd [extra] + ''', + ) + """ + ), + 'setup.cfg': '', + 'epcmd.py': DALS( + """ + from distutils.command.build_py import build_py + + import extra_dep + + class epcmd(build_py): + pass + """ + ), + }, + prefix=dep_pkg, + ) + # "Install" dep. + run_setup(os.path.join(dep_pkg, 'setup.py'), ['dist_info']) + working_set.add_entry(dep_pkg) + # Create source tree for test package. + test_pkg = os.path.join(temp_dir, 'test_pkg') + test_setup_py = os.path.join(test_pkg, 'setup.py') + os.mkdir(test_pkg) + with open(test_setup_py, 'w', encoding="utf-8") as fp: + fp.write( + DALS( + """ + from setuptools import installer, setup + setup(setup_requires='dep[extra]') + """ + ) + ) + # Check... + monkeypatch.setenv('PIP_FIND_LINKS', str(temp_dir)) + monkeypatch.setenv('PIP_NO_INDEX', '1') + monkeypatch.setenv('PIP_RETRIES', '0') + monkeypatch.setenv('PIP_TIMEOUT', '0') + run_setup(test_setup_py, ['epcmd']) + + +def make_trivial_sdist(dist_path, distname, version): + """ + Create a simple sdist tarball at dist_path, containing just a simple + setup.py. + """ + + make_sdist( + dist_path, + [ + ( + 'setup.py', + DALS( + f"""\ + import setuptools + setuptools.setup( + name={distname!r}, + version={version!r} + ) + """ + ), + ), + ('setup.cfg', ''), + ], + ) + + +def make_nspkg_sdist(dist_path, distname, version): + """ + Make an sdist tarball with distname and version which also contains one + package with the same name as distname. The top-level package is + designated a namespace package). + """ + # Assert that the distname contains at least one period + assert '.' in distname + + parts = distname.split('.') + nspackage = parts[0] + + packages = ['.'.join(parts[:idx]) for idx in range(1, len(parts) + 1)] + + setup_py = DALS( + f"""\ + import setuptools + setuptools.setup( + name={distname!r}, + version={version!r}, + packages={packages!r}, + namespace_packages=[{nspackage!r}] + ) + """ + ) + + init = "__import__('pkg_resources').declare_namespace(__name__)" + + files = [('setup.py', setup_py), (os.path.join(nspackage, '__init__.py'), init)] + for package in packages[1:]: + filename = os.path.join(*(package.split('.') + ['__init__.py'])) + files.append((filename, '')) + + make_sdist(dist_path, files) + + +def make_python_requires_sdist(dist_path, distname, version, python_requires): + make_sdist( + dist_path, + [ + ( + 'setup.py', + DALS( + """\ + import setuptools + setuptools.setup( + name={name!r}, + version={version!r}, + python_requires={python_requires!r}, + ) + """ + ).format( + name=distname, version=version, python_requires=python_requires + ), + ), + ('setup.cfg', ''), + ], + ) + + +def make_sdist(dist_path, files): + """ + Create a simple sdist tarball at dist_path, containing the files + listed in ``files`` as ``(filename, content)`` tuples. + """ + + # Distributions with only one file don't play well with pip. + assert len(files) > 1 + with tarfile.open(dist_path, 'w:gz') as dist: + for filename, content in files: + file_bytes = io.BytesIO(content.encode('utf-8')) + file_info = tarfile.TarInfo(name=filename) + file_info.size = len(file_bytes.getvalue()) + file_info.mtime = int(time.time()) + dist.addfile(file_info, fileobj=file_bytes) + + +def create_setup_requires_package( + path, + distname='foobar', + version='0.1', + make_package=make_trivial_sdist, + setup_py_template=None, + setup_attrs=None, + use_setup_cfg=(), +): + """Creates a source tree under path for a trivial test package that has a + single requirement in setup_requires--a tarball for that requirement is + also created and added to the dependency_links argument. + + ``distname`` and ``version`` refer to the name/version of the package that + the test package requires via ``setup_requires``. The name of the test + package itself is just 'test_pkg'. + """ + + normalized_distname = safer_name(distname) + test_setup_attrs = { + 'name': 'test_pkg', + 'version': '0.0', + 'setup_requires': [f'{normalized_distname}=={version}'], + 'dependency_links': [os.path.abspath(path)], + } + if setup_attrs: + test_setup_attrs.update(setup_attrs) + + test_pkg = os.path.join(path, 'test_pkg') + os.mkdir(test_pkg) + + # setup.cfg + if use_setup_cfg: + options = [] + metadata = [] + for name in use_setup_cfg: + value = test_setup_attrs.pop(name) + if name in 'name version'.split(): + section = metadata + else: + section = options + if isinstance(value, (tuple, list)): + value = ';'.join(value) + section.append(f'{name}: {value}') + test_setup_cfg_contents = DALS( + """ + [metadata] + {metadata} + [options] + {options} + """ + ).format( + options='\n'.join(options), + metadata='\n'.join(metadata), + ) + else: + test_setup_cfg_contents = '' + with open(os.path.join(test_pkg, 'setup.cfg'), 'w', encoding="utf-8") as f: + f.write(test_setup_cfg_contents) + + # setup.py + if setup_py_template is None: + setup_py_template = DALS( + """\ + import setuptools + setuptools.setup(**%r) + """ + ) + with open(os.path.join(test_pkg, 'setup.py'), 'w', encoding="utf-8") as f: + f.write(setup_py_template % test_setup_attrs) + + foobar_path = os.path.join(path, f'{normalized_distname}-{version}.tar.gz') + make_package(foobar_path, distname, version) + + return test_pkg + + +@pytest.mark.skipif( + sys.platform.startswith('java') and ei.is_sh(sys.executable), + reason="Test cannot run under java when executable is sh", +) +class TestScriptHeader: + non_ascii_exe = '/Users/José/bin/python' + exe_with_spaces = r'C:\Program Files\Python36\python.exe' + + def test_get_script_header(self): + expected = f'#!{ei.nt_quote_arg(os.path.normpath(sys.executable))}\n' + actual = ei.ScriptWriter.get_header('#!/usr/local/bin/python') + assert actual == expected + + def test_get_script_header_args(self): + expected = f'#!{ei.nt_quote_arg(os.path.normpath(sys.executable))} -x\n' + actual = ei.ScriptWriter.get_header('#!/usr/bin/python -x') + assert actual == expected + + def test_get_script_header_non_ascii_exe(self): + actual = ei.ScriptWriter.get_header( + '#!/usr/bin/python', executable=self.non_ascii_exe + ) + expected = f'#!{self.non_ascii_exe} -x\n' + assert actual == expected + + def test_get_script_header_exe_with_spaces(self): + actual = ei.ScriptWriter.get_header( + '#!/usr/bin/python', executable='"' + self.exe_with_spaces + '"' + ) + expected = f'#!"{self.exe_with_spaces}"\n' + assert actual == expected + + +class TestCommandSpec: + def test_custom_launch_command(self): + """ + Show how a custom CommandSpec could be used to specify a #! executable + which takes parameters. + """ + cmd = ei.CommandSpec(['/usr/bin/env', 'python3']) + assert cmd.as_header() == '#!/usr/bin/env python3\n' + + def test_from_param_for_CommandSpec_is_passthrough(self): + """ + from_param should return an instance of a CommandSpec + """ + cmd = ei.CommandSpec(['python']) + cmd_new = ei.CommandSpec.from_param(cmd) + assert cmd is cmd_new + + @mock.patch('sys.executable', TestScriptHeader.exe_with_spaces) + @mock.patch.dict(os.environ) + def test_from_environment_with_spaces_in_executable(self): + os.environ.pop('__PYVENV_LAUNCHER__', None) + cmd = ei.CommandSpec.from_environment() + assert len(cmd) == 1 + assert cmd.as_header().startswith('#!"') + + def test_from_simple_string_uses_shlex(self): + """ + In order to support `executable = /usr/bin/env my-python`, make sure + from_param invokes shlex on that input. + """ + cmd = ei.CommandSpec.from_param('/usr/bin/env my-python') + assert len(cmd) == 2 + assert '"' not in cmd.as_header() + + def test_from_param_raises_expected_error(self) -> None: + """ + from_param should raise its own TypeError when the argument's type is unsupported + """ + with pytest.raises(TypeError) as exc_info: + ei.CommandSpec.from_param(object()) # type: ignore[arg-type] # We want a type error here + assert ( + str(exc_info.value) == "Argument has an unsupported type " + ), exc_info.value + + +class TestWindowsScriptWriter: + def test_header(self): + hdr = ei.WindowsScriptWriter.get_header('') + assert hdr.startswith('#!') + assert hdr.endswith('\n') + hdr = hdr.lstrip('#!') + hdr = hdr.rstrip('\n') + # header should not start with an escaped quote + assert not hdr.startswith('\\"') + + +class VersionStub(NamedTuple): + major: int + minor: int + micro: int + releaselevel: str + serial: int + + +def test_use_correct_python_version_string(tmpdir, tmpdir_cwd, monkeypatch): + # In issue #3001, easy_install wrongly uses the `python3.1` directory + # when the interpreter is `python3.10` and the `--user` option is given. + # See pypa/setuptools#3001. + dist = Distribution() + cmd = dist.get_command_obj('easy_install') + cmd.args = ['ok'] + cmd.optimize = 0 + cmd.user = True + cmd.install_userbase = str(tmpdir) + cmd.install_usersite = None + install_cmd = dist.get_command_obj('install') + install_cmd.install_userbase = str(tmpdir) + install_cmd.install_usersite = None + + with monkeypatch.context() as patch, warnings.catch_warnings(): + warnings.simplefilter("ignore") + version = '3.10.1 (main, Dec 21 2021, 09:17:12) [GCC 10.2.1 20210110]' + info = VersionStub(3, 10, 1, "final", 0) + patch.setattr('site.ENABLE_USER_SITE', True) + patch.setattr('sys.version', version) + patch.setattr('sys.version_info', info) + patch.setattr(cmd, 'create_home_path', mock.Mock()) + cmd.finalize_options() + + name = "pypy" if hasattr(sys, 'pypy_version_info') else "python" + install_dir = cmd.install_dir.lower() + + # In some platforms (e.g. Windows), install_dir is mostly determined + # via `sysconfig`, which define constants eagerly at module creation. + # This means that monkeypatching `sys.version` to emulate 3.10 for testing + # may have no effect. + # The safest test here is to rely on the fact that 3.1 is no longer + # supported/tested, and make sure that if 'python3.1' ever appears in the string + # it is followed by another digit (e.g. 'python3.10'). + if re.search(name + r'3\.?1', install_dir): + assert re.search(name + r'3\.?1\d', install_dir) + + # The following "variables" are used for interpolation in distutils + # installation schemes, so it should be fair to treat them as "semi-public", + # or at least public enough so we can have a test to make sure they are correct + assert cmd.config_vars['py_version'] == '3.10.1' + assert cmd.config_vars['py_version_short'] == '3.10' + assert cmd.config_vars['py_version_nodot'] == '310' + + +@pytest.mark.xfail( + sys.platform == "darwin", + reason="https://github.com/pypa/setuptools/pull/4716#issuecomment-2447624418", +) +def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmp_path): + """`setup.py develop` should honor `--user` even under build isolation""" + + # == Arrange == + # Pretend that build isolation was enabled + # e.g pip sets the environment variable PYTHONNOUSERSITE=1 + monkeypatch.setattr('site.ENABLE_USER_SITE', False) + + # Patching $HOME for 2 reasons: + # 1. setuptools/command/easy_install.py:create_home_path + # tries creating directories in $HOME. + # Given:: + # self.config_vars['DESTDIRS'] = ( + # "/home/user/.pyenv/versions/3.9.10 " + # "/home/user/.pyenv/versions/3.9.10/lib " + # "/home/user/.pyenv/versions/3.9.10/lib/python3.9 " + # "/home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload") + # `create_home_path` will:: + # makedirs( + # "/home/user/.pyenv/versions/3.9.10 " + # "/home/user/.pyenv/versions/3.9.10/lib " + # "/home/user/.pyenv/versions/3.9.10/lib/python3.9 " + # "/home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload") + # + # 2. We are going to force `site` to update site.USER_BASE and site.USER_SITE + # To point inside our new home + monkeypatch.setenv('HOME', str(tmp_path / '.home')) + monkeypatch.setenv('USERPROFILE', str(tmp_path / '.home')) + monkeypatch.setenv('APPDATA', str(tmp_path / '.home')) + monkeypatch.setattr('site.USER_BASE', None) + monkeypatch.setattr('site.USER_SITE', None) + user_site = Path(site.getusersitepackages()) + user_site.mkdir(parents=True, exist_ok=True) + + sys_prefix = tmp_path / '.sys_prefix' + sys_prefix.mkdir(parents=True, exist_ok=True) + monkeypatch.setattr('sys.prefix', str(sys_prefix)) + + setup_script = ( + "__import__('setuptools').setup(name='aproj', version=42, packages=[])\n" + ) + (tmp_path / "setup.py").write_text(setup_script, encoding="utf-8") + + # == Sanity check == + assert list(sys_prefix.glob("*")) == [] + assert list(user_site.glob("*")) == [] + + # == Act == + run_setup('setup.py', ['develop', '--user']) + + # == Assert == + # Should not install to sys.prefix + assert list(sys_prefix.glob("*")) == [] + # Should install to user site + installed = {f.name for f in user_site.glob("*")} + # sometimes easy-install.pth is created and sometimes not + installed = installed - {"easy-install.pth"} + assert installed == {'aproj.egg-link'} diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_editable_install.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_editable_install.py new file mode 100644 index 0000000000000000000000000000000000000000..038dcadf934af20dbe11dcb13ba324ceb30bf90d --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_editable_install.py @@ -0,0 +1,1289 @@ +from __future__ import annotations + +import os +import platform +import stat +import subprocess +import sys +from copy import deepcopy +from importlib import import_module +from importlib.machinery import EXTENSION_SUFFIXES +from pathlib import Path +from textwrap import dedent +from typing import Any +from unittest.mock import Mock +from uuid import uuid4 + +import jaraco.envs +import jaraco.path +import pytest +from path import Path as _Path + +from setuptools._importlib import resources as importlib_resources +from setuptools.command.editable_wheel import ( + _DebuggingTips, + _encode_pth, + _find_namespaces, + _find_package_roots, + _find_virtual_namespaces, + _finder_template, + _LinkTree, + _TopLevelFinder, + editable_wheel, +) +from setuptools.dist import Distribution +from setuptools.extension import Extension +from setuptools.warnings import SetuptoolsDeprecationWarning + +from . import contexts, namespaces + +from distutils.core import run_setup + + +@pytest.fixture(params=["strict", "lenient"]) +def editable_opts(request): + if request.param == "strict": + return ["--config-settings", "editable-mode=strict"] + return [] + + +EXAMPLE = { + 'pyproject.toml': dedent( + """\ + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + + [project] + name = "mypkg" + version = "3.14159" + license = {text = "MIT"} + description = "This is a Python package" + dynamic = ["readme"] + classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers" + ] + urls = {Homepage = "https://github.com"} + + [tool.setuptools] + package-dir = {"" = "src"} + packages = {find = {where = ["src"]}} + license-files = ["LICENSE*"] + + [tool.setuptools.dynamic] + readme = {file = "README.rst"} + + [tool.distutils.egg_info] + tag-build = ".post0" + """ + ), + "MANIFEST.in": dedent( + """\ + global-include *.py *.txt + global-exclude *.py[cod] + prune dist + prune build + """ + ).strip(), + "README.rst": "This is a ``README``", + "LICENSE.txt": "---- placeholder MIT license ----", + "src": { + "mypkg": { + "__init__.py": dedent( + """\ + import sys + from importlib.metadata import PackageNotFoundError, version + + try: + __version__ = version(__name__) + except PackageNotFoundError: + __version__ = "unknown" + """ + ), + "__main__.py": dedent( + """\ + from importlib.resources import read_text + from . import __version__, __name__ as parent + from .mod import x + + data = read_text(parent, "data.txt") + print(__version__, data, x) + """ + ), + "mod.py": "x = ''", + "data.txt": "Hello World", + } + }, +} + + +SETUP_SCRIPT_STUB = "__import__('setuptools').setup()" + + +@pytest.mark.xfail(sys.platform == "darwin", reason="pypa/setuptools#4328") +@pytest.mark.parametrize( + "files", + [ + {**EXAMPLE, "setup.py": SETUP_SCRIPT_STUB}, + EXAMPLE, # No setup.py script + ], +) +def test_editable_with_pyproject(tmp_path, venv, files, editable_opts): + project = tmp_path / "mypkg" + project.mkdir() + jaraco.path.build(files, prefix=project) + + cmd = [ + "python", + "-m", + "pip", + "install", + "--no-build-isolation", # required to force current version of setuptools + "-e", + str(project), + *editable_opts, + ] + print(venv.run(cmd)) + + cmd = ["python", "-m", "mypkg"] + assert venv.run(cmd).strip() == "3.14159.post0 Hello World" + + (project / "src/mypkg/data.txt").write_text("foobar", encoding="utf-8") + (project / "src/mypkg/mod.py").write_text("x = 42", encoding="utf-8") + assert venv.run(cmd).strip() == "3.14159.post0 foobar 42" + + +def test_editable_with_flat_layout(tmp_path, venv, editable_opts): + files = { + "mypkg": { + "pyproject.toml": dedent( + """\ + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" + + [project] + name = "mypkg" + version = "3.14159" + + [tool.setuptools] + packages = ["pkg"] + py-modules = ["mod"] + """ + ), + "pkg": {"__init__.py": "a = 4"}, + "mod.py": "b = 2", + }, + } + jaraco.path.build(files, prefix=tmp_path) + project = tmp_path / "mypkg" + + cmd = [ + "python", + "-m", + "pip", + "install", + "--no-build-isolation", # required to force current version of setuptools + "-e", + str(project), + *editable_opts, + ] + print(venv.run(cmd)) + cmd = ["python", "-c", "import pkg, mod; print(pkg.a, mod.b)"] + assert venv.run(cmd).strip() == "4 2" + + +def test_editable_with_single_module(tmp_path, venv, editable_opts): + files = { + "mypkg": { + "pyproject.toml": dedent( + """\ + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" + + [project] + name = "mod" + version = "3.14159" + + [tool.setuptools] + py-modules = ["mod"] + """ + ), + "mod.py": "b = 2", + }, + } + jaraco.path.build(files, prefix=tmp_path) + project = tmp_path / "mypkg" + + cmd = [ + "python", + "-m", + "pip", + "install", + "--no-build-isolation", # required to force current version of setuptools + "-e", + str(project), + *editable_opts, + ] + print(venv.run(cmd)) + cmd = ["python", "-c", "import mod; print(mod.b)"] + assert venv.run(cmd).strip() == "2" + + +class TestLegacyNamespaces: + # legacy => pkg_resources.declare_namespace(...) + setup(namespace_packages=...) + + def test_nspkg_file_is_unique(self, tmp_path, monkeypatch): + deprecation = pytest.warns( + SetuptoolsDeprecationWarning, match=".*namespace_packages parameter.*" + ) + installation_dir = tmp_path / ".installation_dir" + installation_dir.mkdir() + examples = ( + "myns.pkgA", + "myns.pkgB", + "myns.n.pkgA", + "myns.n.pkgB", + ) + + for name in examples: + pkg = namespaces.build_namespace_package(tmp_path, name, version="42") + with deprecation, monkeypatch.context() as ctx: + ctx.chdir(pkg) + dist = run_setup("setup.py", stop_after="config") + cmd = editable_wheel(dist) + cmd.finalize_options() + editable_name = cmd.get_finalized_command("dist_info").name + cmd._install_namespaces(installation_dir, editable_name) + + files = list(installation_dir.glob("*-nspkg.pth")) + assert len(files) == len(examples) + + @pytest.mark.parametrize( + "impl", + ( + "pkg_resources", + # "pkgutil", => does not work + ), + ) + @pytest.mark.parametrize("ns", ("myns.n",)) + def test_namespace_package_importable( + self, venv, tmp_path, ns, impl, editable_opts + ): + """ + Installing two packages sharing the same namespace, one installed + naturally using pip or `--single-version-externally-managed` + and the other installed in editable mode should leave the namespace + intact and both packages reachable by import. + (Ported from test_develop). + """ + build_system = """\ + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + """ + pkg_A = namespaces.build_namespace_package(tmp_path, f"{ns}.pkgA", impl=impl) + pkg_B = namespaces.build_namespace_package(tmp_path, f"{ns}.pkgB", impl=impl) + (pkg_A / "pyproject.toml").write_text(build_system, encoding="utf-8") + (pkg_B / "pyproject.toml").write_text(build_system, encoding="utf-8") + # use pip to install to the target directory + opts = editable_opts[:] + opts.append("--no-build-isolation") # force current version of setuptools + venv.run(["python", "-m", "pip", "install", str(pkg_A), *opts]) + venv.run(["python", "-m", "pip", "install", "-e", str(pkg_B), *opts]) + venv.run(["python", "-c", f"import {ns}.pkgA; import {ns}.pkgB"]) + # additionally ensure that pkg_resources import works + venv.run(["python", "-c", "import pkg_resources"]) + + +class TestPep420Namespaces: + def test_namespace_package_importable(self, venv, tmp_path, editable_opts): + """ + Installing two packages sharing the same namespace, one installed + normally using pip and the other installed in editable mode + should allow importing both packages. + """ + pkg_A = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgA') + pkg_B = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgB') + # use pip to install to the target directory + opts = editable_opts[:] + opts.append("--no-build-isolation") # force current version of setuptools + venv.run(["python", "-m", "pip", "install", str(pkg_A), *opts]) + venv.run(["python", "-m", "pip", "install", "-e", str(pkg_B), *opts]) + venv.run(["python", "-c", "import myns.n.pkgA; import myns.n.pkgB"]) + + def test_namespace_created_via_package_dir(self, venv, tmp_path, editable_opts): + """Currently users can create a namespace by tweaking `package_dir`""" + files = { + "pkgA": { + "pyproject.toml": dedent( + """\ + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" + + [project] + name = "pkgA" + version = "3.14159" + + [tool.setuptools] + package-dir = {"myns.n.pkgA" = "src"} + """ + ), + "src": {"__init__.py": "a = 1"}, + }, + } + jaraco.path.build(files, prefix=tmp_path) + pkg_A = tmp_path / "pkgA" + pkg_B = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgB') + pkg_C = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgC') + + # use pip to install to the target directory + opts = editable_opts[:] + opts.append("--no-build-isolation") # force current version of setuptools + venv.run(["python", "-m", "pip", "install", str(pkg_A), *opts]) + venv.run(["python", "-m", "pip", "install", "-e", str(pkg_B), *opts]) + venv.run(["python", "-m", "pip", "install", "-e", str(pkg_C), *opts]) + venv.run(["python", "-c", "from myns.n import pkgA, pkgB, pkgC"]) + + def test_namespace_accidental_config_in_lenient_mode(self, venv, tmp_path): + """Sometimes users might specify an ``include`` pattern that ignores parent + packages. In a normal installation this would ignore all modules inside the + parent packages, and make them namespaces (reported in issue #3504), + so the editable mode should preserve this behaviour. + """ + files = { + "pkgA": { + "pyproject.toml": dedent( + """\ + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" + + [project] + name = "pkgA" + version = "3.14159" + + [tool.setuptools] + packages.find.include = ["mypkg.*"] + """ + ), + "mypkg": { + "__init__.py": "", + "other.py": "b = 1", + "n": { + "__init__.py": "", + "pkgA.py": "a = 1", + }, + }, + "MANIFEST.in": EXAMPLE["MANIFEST.in"], + }, + } + jaraco.path.build(files, prefix=tmp_path) + pkg_A = tmp_path / "pkgA" + + # use pip to install to the target directory + opts = ["--no-build-isolation"] # force current version of setuptools + venv.run(["python", "-m", "pip", "-v", "install", "-e", str(pkg_A), *opts]) + out = venv.run(["python", "-c", "from mypkg.n import pkgA; print(pkgA.a)"]) + assert out.strip() == "1" + cmd = """\ + try: + import mypkg.other + except ImportError: + print("mypkg.other not defined") + """ + out = venv.run(["python", "-c", dedent(cmd)]) + assert "mypkg.other not defined" in out + + +def test_editable_with_prefix(tmp_path, sample_project, editable_opts): + """ + Editable install to a prefix should be discoverable. + """ + prefix = tmp_path / 'prefix' + + # figure out where pip will likely install the package + site_packages_all = [ + prefix / Path(path).relative_to(sys.prefix) + for path in sys.path + if 'site-packages' in path and path.startswith(sys.prefix) + ] + + for sp in site_packages_all: + sp.mkdir(parents=True) + + # install workaround + _addsitedirs(site_packages_all) + + env = dict(os.environ, PYTHONPATH=os.pathsep.join(map(str, site_packages_all))) + cmd = [ + sys.executable, + '-m', + 'pip', + 'install', + '--editable', + str(sample_project), + '--prefix', + str(prefix), + '--no-build-isolation', + *editable_opts, + ] + subprocess.check_call(cmd, env=env) + + # now run 'sample' with the prefix on the PYTHONPATH + bin = 'Scripts' if platform.system() == 'Windows' else 'bin' + exe = prefix / bin / 'sample' + subprocess.check_call([exe], env=env) + + +class TestFinderTemplate: + """This test focus in getting a particular implementation detail right. + If at some point in time the implementation is changed for something different, + this test can be modified or even excluded. + """ + + def install_finder(self, finder): + loc = {} + exec(finder, loc, loc) + loc["install"]() + + def test_packages(self, tmp_path): + files = { + "src1": { + "pkg1": { + "__init__.py": "", + "subpkg": {"mod1.py": "a = 42"}, + }, + }, + "src2": {"mod2.py": "a = 43"}, + } + jaraco.path.build(files, prefix=tmp_path) + + mapping = { + "pkg1": str(tmp_path / "src1/pkg1"), + "mod2": str(tmp_path / "src2/mod2"), + } + template = _finder_template(str(uuid4()), mapping, {}) + + with contexts.save_paths(), contexts.save_sys_modules(): + for mod in ("pkg1", "pkg1.subpkg", "pkg1.subpkg.mod1", "mod2"): + sys.modules.pop(mod, None) + + self.install_finder(template) + mod1 = import_module("pkg1.subpkg.mod1") + mod2 = import_module("mod2") + subpkg = import_module("pkg1.subpkg") + + assert mod1.a == 42 + assert mod2.a == 43 + expected = str((tmp_path / "src1/pkg1/subpkg").resolve()) + assert_path(subpkg, expected) + + def test_namespace(self, tmp_path): + files = {"pkg": {"__init__.py": "a = 13", "text.txt": "abc"}} + jaraco.path.build(files, prefix=tmp_path) + + mapping = {"ns.othername": str(tmp_path / "pkg")} + namespaces = {"ns": []} + + template = _finder_template(str(uuid4()), mapping, namespaces) + with contexts.save_paths(), contexts.save_sys_modules(): + for mod in ("ns", "ns.othername"): + sys.modules.pop(mod, None) + + self.install_finder(template) + pkg = import_module("ns.othername") + text = importlib_resources.files(pkg) / "text.txt" + + expected = str((tmp_path / "pkg").resolve()) + assert_path(pkg, expected) + assert pkg.a == 13 + + # Make sure resources can also be found + assert text.read_text(encoding="utf-8") == "abc" + + def test_combine_namespaces(self, tmp_path): + files = { + "src1": {"ns": {"pkg1": {"__init__.py": "a = 13"}}}, + "src2": {"ns": {"mod2.py": "b = 37"}}, + } + jaraco.path.build(files, prefix=tmp_path) + + mapping = { + "ns.pkgA": str(tmp_path / "src1/ns/pkg1"), + "ns": str(tmp_path / "src2/ns"), + } + namespaces_ = {"ns": [str(tmp_path / "src1"), str(tmp_path / "src2")]} + template = _finder_template(str(uuid4()), mapping, namespaces_) + + with contexts.save_paths(), contexts.save_sys_modules(): + for mod in ("ns", "ns.pkgA", "ns.mod2"): + sys.modules.pop(mod, None) + + self.install_finder(template) + pkgA = import_module("ns.pkgA") + mod2 = import_module("ns.mod2") + + expected = str((tmp_path / "src1/ns/pkg1").resolve()) + assert_path(pkgA, expected) + assert pkgA.a == 13 + assert mod2.b == 37 + + def test_combine_namespaces_nested(self, tmp_path): + """ + Users may attempt to combine namespace packages in a nested way via + ``package_dir`` as shown in pypa/setuptools#4248. + """ + + files = { + "src": {"my_package": {"my_module.py": "a = 13"}}, + "src2": {"my_package2": {"my_module2.py": "b = 37"}}, + } + + stack = jaraco.path.DirectoryStack() + with stack.context(tmp_path): + jaraco.path.build(files) + attrs = { + "script_name": "%PEP 517%", + "package_dir": { + "different_name": "src/my_package", + "different_name.subpkg": "src2/my_package2", + }, + "packages": ["different_name", "different_name.subpkg"], + } + dist = Distribution(attrs) + finder = _TopLevelFinder(dist, str(uuid4())) + code = next(v for k, v in finder.get_implementation() if k.endswith(".py")) + + with contexts.save_paths(), contexts.save_sys_modules(): + for mod in attrs["packages"]: + sys.modules.pop(mod, None) + + self.install_finder(code) + mod1 = import_module("different_name.my_module") + mod2 = import_module("different_name.subpkg.my_module2") + + expected = str((tmp_path / "src/my_package/my_module.py").resolve()) + assert str(Path(mod1.__file__).resolve()) == expected + + expected = str((tmp_path / "src2/my_package2/my_module2.py").resolve()) + assert str(Path(mod2.__file__).resolve()) == expected + + assert mod1.a == 13 + assert mod2.b == 37 + + def test_dynamic_path_computation(self, tmp_path): + # Follows the example in PEP 420 + files = { + "project1": {"parent": {"child": {"one.py": "x = 1"}}}, + "project2": {"parent": {"child": {"two.py": "x = 2"}}}, + "project3": {"parent": {"child": {"three.py": "x = 3"}}}, + } + jaraco.path.build(files, prefix=tmp_path) + mapping = {} + namespaces_ = {"parent": [str(tmp_path / "project1/parent")]} + template = _finder_template(str(uuid4()), mapping, namespaces_) + + mods = (f"parent.child.{name}" for name in ("one", "two", "three")) + with contexts.save_paths(), contexts.save_sys_modules(): + for mod in ("parent", "parent.child", "parent.child", *mods): + sys.modules.pop(mod, None) + + self.install_finder(template) + + one = import_module("parent.child.one") + assert one.x == 1 + + with pytest.raises(ImportError): + import_module("parent.child.two") + + sys.path.append(str(tmp_path / "project2")) + two = import_module("parent.child.two") + assert two.x == 2 + + with pytest.raises(ImportError): + import_module("parent.child.three") + + sys.path.append(str(tmp_path / "project3")) + three = import_module("parent.child.three") + assert three.x == 3 + + def test_no_recursion(self, tmp_path): + # See issue #3550 + files = { + "pkg": { + "__init__.py": "from . import pkg", + }, + } + jaraco.path.build(files, prefix=tmp_path) + + mapping = { + "pkg": str(tmp_path / "pkg"), + } + template = _finder_template(str(uuid4()), mapping, {}) + + with contexts.save_paths(), contexts.save_sys_modules(): + sys.modules.pop("pkg", None) + + self.install_finder(template) + with pytest.raises(ImportError, match="pkg"): + import_module("pkg") + + def test_similar_name(self, tmp_path): + files = { + "foo": { + "__init__.py": "", + "bar": { + "__init__.py": "", + }, + }, + } + jaraco.path.build(files, prefix=tmp_path) + + mapping = { + "foo": str(tmp_path / "foo"), + } + template = _finder_template(str(uuid4()), mapping, {}) + + with contexts.save_paths(), contexts.save_sys_modules(): + sys.modules.pop("foo", None) + sys.modules.pop("foo.bar", None) + + self.install_finder(template) + with pytest.raises(ImportError, match="foobar"): + import_module("foobar") + + def test_case_sensitivity(self, tmp_path): + files = { + "foo": { + "__init__.py": "", + "lowercase.py": "x = 1", + "bar": { + "__init__.py": "", + "lowercase.py": "x = 2", + }, + }, + } + jaraco.path.build(files, prefix=tmp_path) + mapping = { + "foo": str(tmp_path / "foo"), + } + template = _finder_template(str(uuid4()), mapping, {}) + with contexts.save_paths(), contexts.save_sys_modules(): + sys.modules.pop("foo", None) + + self.install_finder(template) + with pytest.raises(ImportError, match="'FOO'"): + import_module("FOO") + + with pytest.raises(ImportError, match="'foo\\.LOWERCASE'"): + import_module("foo.LOWERCASE") + + with pytest.raises(ImportError, match="'foo\\.bar\\.Lowercase'"): + import_module("foo.bar.Lowercase") + + with pytest.raises(ImportError, match="'foo\\.BAR'"): + import_module("foo.BAR.lowercase") + + with pytest.raises(ImportError, match="'FOO'"): + import_module("FOO.bar.lowercase") + + mod = import_module("foo.lowercase") + assert mod.x == 1 + + mod = import_module("foo.bar.lowercase") + assert mod.x == 2 + + def test_namespace_case_sensitivity(self, tmp_path): + files = { + "pkg": { + "__init__.py": "a = 13", + "foo": { + "__init__.py": "b = 37", + "bar.py": "c = 42", + }, + }, + } + jaraco.path.build(files, prefix=tmp_path) + + mapping = {"ns.othername": str(tmp_path / "pkg")} + namespaces = {"ns": []} + + template = _finder_template(str(uuid4()), mapping, namespaces) + with contexts.save_paths(), contexts.save_sys_modules(): + for mod in ("ns", "ns.othername"): + sys.modules.pop(mod, None) + + self.install_finder(template) + pkg = import_module("ns.othername") + expected = str((tmp_path / "pkg").resolve()) + assert_path(pkg, expected) + assert pkg.a == 13 + + foo = import_module("ns.othername.foo") + assert foo.b == 37 + + bar = import_module("ns.othername.foo.bar") + assert bar.c == 42 + + with pytest.raises(ImportError, match="'NS'"): + import_module("NS.othername.foo") + + with pytest.raises(ImportError, match="'ns\\.othername\\.FOO\\'"): + import_module("ns.othername.FOO") + + with pytest.raises(ImportError, match="'ns\\.othername\\.foo\\.BAR\\'"): + import_module("ns.othername.foo.BAR") + + def test_intermediate_packages(self, tmp_path): + """ + The finder should not import ``fullname`` if the intermediate segments + don't exist (see pypa/setuptools#4019). + """ + files = { + "src": { + "mypkg": { + "__init__.py": "", + "config.py": "a = 13", + "helloworld.py": "b = 13", + "components": { + "config.py": "a = 37", + }, + }, + } + } + jaraco.path.build(files, prefix=tmp_path) + + mapping = {"mypkg": str(tmp_path / "src/mypkg")} + template = _finder_template(str(uuid4()), mapping, {}) + + with contexts.save_paths(), contexts.save_sys_modules(): + for mod in ( + "mypkg", + "mypkg.config", + "mypkg.helloworld", + "mypkg.components", + "mypkg.components.config", + "mypkg.components.helloworld", + ): + sys.modules.pop(mod, None) + + self.install_finder(template) + + config = import_module("mypkg.components.config") + assert config.a == 37 + + helloworld = import_module("mypkg.helloworld") + assert helloworld.b == 13 + + with pytest.raises(ImportError): + import_module("mypkg.components.helloworld") + + +def test_pkg_roots(tmp_path): + """This test focus in getting a particular implementation detail right. + If at some point in time the implementation is changed for something different, + this test can be modified or even excluded. + """ + files = { + "a": {"b": {"__init__.py": "ab = 1"}, "__init__.py": "a = 1"}, + "d": {"__init__.py": "d = 1", "e": {"__init__.py": "de = 1"}}, + "f": {"g": {"h": {"__init__.py": "fgh = 1"}}}, + "other": {"__init__.py": "abc = 1"}, + "another": {"__init__.py": "abcxyz = 1"}, + "yet_another": {"__init__.py": "mnopq = 1"}, + } + jaraco.path.build(files, prefix=tmp_path) + package_dir = { + "a.b.c": "other", + "a.b.c.x.y.z": "another", + "m.n.o.p.q": "yet_another", + } + packages = [ + "a", + "a.b", + "a.b.c", + "a.b.c.x.y", + "a.b.c.x.y.z", + "d", + "d.e", + "f", + "f.g", + "f.g.h", + "m.n.o.p.q", + ] + roots = _find_package_roots(packages, package_dir, tmp_path) + assert roots == { + "a": str(tmp_path / "a"), + "a.b.c": str(tmp_path / "other"), + "a.b.c.x.y.z": str(tmp_path / "another"), + "d": str(tmp_path / "d"), + "f": str(tmp_path / "f"), + "m.n.o.p.q": str(tmp_path / "yet_another"), + } + + ns = set(dict(_find_namespaces(packages, roots))) + assert ns == {"f", "f.g"} + + ns = set(_find_virtual_namespaces(roots)) + assert ns == {"a.b", "a.b.c.x", "a.b.c.x.y", "m", "m.n", "m.n.o", "m.n.o.p"} + + +class TestOverallBehaviour: + PYPROJECT = """\ + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + + [project] + name = "mypkg" + version = "3.14159" + """ + + # Any: Would need a TypedDict. Keep it simple for tests + FLAT_LAYOUT: dict[str, Any] = { + "pyproject.toml": dedent(PYPROJECT), + "MANIFEST.in": EXAMPLE["MANIFEST.in"], + "otherfile.py": "", + "mypkg": { + "__init__.py": "", + "mod1.py": "var = 42", + "subpackage": { + "__init__.py": "", + "mod2.py": "var = 13", + "resource_file.txt": "resource 39", + }, + }, + } + + EXAMPLES = { + "flat-layout": FLAT_LAYOUT, + "src-layout": { + "pyproject.toml": dedent(PYPROJECT), + "MANIFEST.in": EXAMPLE["MANIFEST.in"], + "otherfile.py": "", + "src": {"mypkg": FLAT_LAYOUT["mypkg"]}, + }, + "custom-layout": { + "pyproject.toml": dedent(PYPROJECT) + + dedent( + """\ + [tool.setuptools] + packages = ["mypkg", "mypkg.subpackage"] + + [tool.setuptools.package-dir] + "mypkg.subpackage" = "other" + """ + ), + "MANIFEST.in": EXAMPLE["MANIFEST.in"], + "otherfile.py": "", + "mypkg": { + "__init__.py": "", + "mod1.py": FLAT_LAYOUT["mypkg"]["mod1.py"], + }, + "other": FLAT_LAYOUT["mypkg"]["subpackage"], + }, + "namespace": { + "pyproject.toml": dedent(PYPROJECT), + "MANIFEST.in": EXAMPLE["MANIFEST.in"], + "otherfile.py": "", + "src": { + "mypkg": { + "mod1.py": FLAT_LAYOUT["mypkg"]["mod1.py"], + "subpackage": FLAT_LAYOUT["mypkg"]["subpackage"], + }, + }, + }, + } + + @pytest.mark.xfail(sys.platform == "darwin", reason="pypa/setuptools#4328") + @pytest.mark.parametrize("layout", EXAMPLES.keys()) + def test_editable_install(self, tmp_path, venv, layout, editable_opts): + project, _ = install_project( + "mypkg", venv, tmp_path, self.EXAMPLES[layout], *editable_opts + ) + + # Ensure stray files are not importable + cmd_import_error = """\ + try: + import otherfile + except ImportError as ex: + print(ex) + """ + out = venv.run(["python", "-c", dedent(cmd_import_error)]) + assert "No module named 'otherfile'" in out + + # Ensure the modules are importable + cmd_get_vars = """\ + import mypkg, mypkg.mod1, mypkg.subpackage.mod2 + print(mypkg.mod1.var, mypkg.subpackage.mod2.var) + """ + out = venv.run(["python", "-c", dedent(cmd_get_vars)]) + assert "42 13" in out + + # Ensure resources are reachable + cmd_get_resource = """\ + import mypkg.subpackage + from setuptools._importlib import resources as importlib_resources + text = importlib_resources.files(mypkg.subpackage) / "resource_file.txt" + print(text.read_text(encoding="utf-8")) + """ + out = venv.run(["python", "-c", dedent(cmd_get_resource)]) + assert "resource 39" in out + + # Ensure files are editable + mod1 = next(project.glob("**/mod1.py")) + mod2 = next(project.glob("**/mod2.py")) + resource_file = next(project.glob("**/resource_file.txt")) + + mod1.write_text("var = 17", encoding="utf-8") + mod2.write_text("var = 781", encoding="utf-8") + resource_file.write_text("resource 374", encoding="utf-8") + + out = venv.run(["python", "-c", dedent(cmd_get_vars)]) + assert "42 13" not in out + assert "17 781" in out + + out = venv.run(["python", "-c", dedent(cmd_get_resource)]) + assert "resource 39" not in out + assert "resource 374" in out + + +class TestLinkTree: + FILES = deepcopy(TestOverallBehaviour.EXAMPLES["src-layout"]) + FILES["pyproject.toml"] += dedent( + """\ + [tool.setuptools] + # Temporary workaround: both `include-package-data` and `package-data` configs + # can be removed after #3260 is fixed. + include-package-data = false + package-data = {"*" = ["*.txt"]} + + [tool.setuptools.packages.find] + where = ["src"] + exclude = ["*.subpackage*"] + """ + ) + FILES["src"]["mypkg"]["resource.not_in_manifest"] = "abc" + + def test_generated_tree(self, tmp_path): + jaraco.path.build(self.FILES, prefix=tmp_path) + + with _Path(tmp_path): + name = "mypkg-3.14159" + dist = Distribution({"script_name": "%PEP 517%"}) + dist.parse_config_files() + + wheel = Mock() + aux = tmp_path / ".aux" + build = tmp_path / ".build" + aux.mkdir() + build.mkdir() + + build_py = dist.get_command_obj("build_py") + build_py.editable_mode = True + build_py.build_lib = str(build) + build_py.ensure_finalized() + outputs = build_py.get_outputs() + output_mapping = build_py.get_output_mapping() + + make_tree = _LinkTree(dist, name, aux, build) + make_tree(wheel, outputs, output_mapping) + + mod1 = next(aux.glob("**/mod1.py")) + expected = tmp_path / "src/mypkg/mod1.py" + assert_link_to(mod1, expected) + + assert next(aux.glob("**/subpackage"), None) is None + assert next(aux.glob("**/mod2.py"), None) is None + assert next(aux.glob("**/resource_file.txt"), None) is None + + assert next(aux.glob("**/resource.not_in_manifest"), None) is None + + def test_strict_install(self, tmp_path, venv): + opts = ["--config-settings", "editable-mode=strict"] + install_project("mypkg", venv, tmp_path, self.FILES, *opts) + + out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"]) + assert "42" in out + + # Ensure packages excluded from distribution are not importable + cmd_import_error = """\ + try: + from mypkg import subpackage + except ImportError as ex: + print(ex) + """ + out = venv.run(["python", "-c", dedent(cmd_import_error)]) + assert "cannot import name 'subpackage'" in out + + # Ensure resource files excluded from distribution are not reachable + cmd_get_resource = """\ + import mypkg + from setuptools._importlib import resources as importlib_resources + try: + text = importlib_resources.files(mypkg) / "resource.not_in_manifest" + print(text.read_text(encoding="utf-8")) + except FileNotFoundError as ex: + print(ex) + """ + out = venv.run(["python", "-c", dedent(cmd_get_resource)]) + assert "No such file or directory" in out + assert "resource.not_in_manifest" in out + + +@pytest.mark.filterwarnings("ignore:.*compat.*:setuptools.SetuptoolsDeprecationWarning") +def test_compat_install(tmp_path, venv): + # TODO: Remove `compat` after Dec/2022. + opts = ["--config-settings", "editable-mode=compat"] + files = TestOverallBehaviour.EXAMPLES["custom-layout"] + install_project("mypkg", venv, tmp_path, files, *opts) + + out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"]) + assert "42" in out + + expected_path = comparable_path(str(tmp_path)) + + # Compatible behaviour will make spurious modules and excluded + # files importable directly from the original path + for cmd in ( + "import otherfile; print(otherfile)", + "import other; print(other)", + "import mypkg; print(mypkg)", + ): + out = comparable_path(venv.run(["python", "-c", cmd])) + assert expected_path in out + + # Compatible behaviour will not consider custom mappings + cmd = """\ + try: + from mypkg import subpackage; + except ImportError as ex: + print(ex) + """ + out = venv.run(["python", "-c", dedent(cmd)]) + assert "cannot import name 'subpackage'" in out + + +def test_pbr_integration(tmp_path, venv, editable_opts): + """Ensure editable installs work with pbr, issue #3500""" + files = { + "pyproject.toml": dedent( + """\ + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + """ + ), + "setup.py": dedent( + """\ + __import__('setuptools').setup( + pbr=True, + setup_requires=["pbr"], + ) + """ + ), + "setup.cfg": dedent( + """\ + [metadata] + name = mypkg + + [files] + packages = + mypkg + """ + ), + "mypkg": { + "__init__.py": "", + "hello.py": "print('Hello world!')", + }, + "other": {"test.txt": "Another file in here."}, + } + venv.run(["python", "-m", "pip", "install", "pbr"]) + + with contexts.environment(PBR_VERSION="0.42"): + install_project("mypkg", venv, tmp_path, files, *editable_opts) + + out = venv.run(["python", "-c", "import mypkg.hello"]) + assert "Hello world!" in out + + +class TestCustomBuildPy: + """ + Issue #3501 indicates that some plugins/customizations might rely on: + + 1. ``build_py`` not running + 2. ``build_py`` always copying files to ``build_lib`` + + During the transition period setuptools should prevent potential errors from + happening due to those assumptions. + """ + + # TODO: Remove tests after _run_build_steps is removed. + + FILES = { + **TestOverallBehaviour.EXAMPLES["flat-layout"], + "setup.py": dedent( + """\ + import pathlib + from setuptools import setup + from setuptools.command.build_py import build_py as orig + + class my_build_py(orig): + def run(self): + super().run() + raise ValueError("TEST_RAISE") + + setup(cmdclass={"build_py": my_build_py}) + """ + ), + } + + def test_safeguarded_from_errors(self, tmp_path, venv): + """Ensure that errors in custom build_py are reported as warnings""" + # Warnings should show up + _, out = install_project("mypkg", venv, tmp_path, self.FILES) + assert "SetuptoolsDeprecationWarning" in out + assert "ValueError: TEST_RAISE" in out + # but installation should be successful + out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"]) + assert "42" in out + + +class TestCustomBuildWheel: + def install_custom_build_wheel(self, dist): + bdist_wheel_cls = dist.get_command_class("bdist_wheel") + + class MyBdistWheel(bdist_wheel_cls): + def get_tag(self): + # In issue #3513, we can see that some extensions may try to access + # the `plat_name` property in bdist_wheel + if self.plat_name.startswith("macosx-"): + _ = "macOS platform" + return super().get_tag() + + dist.cmdclass["bdist_wheel"] = MyBdistWheel + + def test_access_plat_name(self, tmpdir_cwd): + # Even when a custom bdist_wheel tries to access plat_name the build should + # be successful + jaraco.path.build({"module.py": "x = 42"}) + dist = Distribution() + dist.script_name = "setup.py" + dist.set_defaults() + self.install_custom_build_wheel(dist) + cmd = editable_wheel(dist) + cmd.ensure_finalized() + cmd.run() + wheel_file = str(next(Path().glob('dist/*.whl'))) + assert "editable" in wheel_file + + +class TestCustomBuildExt: + def install_custom_build_ext_distutils(self, dist): + from distutils.command.build_ext import build_ext as build_ext_cls + + class MyBuildExt(build_ext_cls): + pass + + dist.cmdclass["build_ext"] = MyBuildExt + + @pytest.mark.skipif( + sys.platform != "linux", reason="compilers may fail without correct setup" + ) + def test_distutils_leave_inplace_files(self, tmpdir_cwd): + jaraco.path.build({"module.c": ""}) + attrs = { + "ext_modules": [Extension("module", ["module.c"])], + } + dist = Distribution(attrs) + dist.script_name = "setup.py" + dist.set_defaults() + self.install_custom_build_ext_distutils(dist) + cmd = editable_wheel(dist) + cmd.ensure_finalized() + cmd.run() + wheel_file = str(next(Path().glob('dist/*.whl'))) + assert "editable" in wheel_file + files = [p for p in Path().glob("module.*") if p.suffix != ".c"] + assert len(files) == 1 + name = files[0].name + assert any(name.endswith(ext) for ext in EXTENSION_SUFFIXES) + + +def test_debugging_tips(tmpdir_cwd, monkeypatch): + """Make sure to display useful debugging tips to the user.""" + jaraco.path.build({"module.py": "x = 42"}) + dist = Distribution() + dist.script_name = "setup.py" + dist.set_defaults() + cmd = editable_wheel(dist) + cmd.ensure_finalized() + + SimulatedErr = type("SimulatedErr", (Exception,), {}) + simulated_failure = Mock(side_effect=SimulatedErr()) + monkeypatch.setattr(cmd, "get_finalized_command", simulated_failure) + + expected_msg = "following steps are recommended to help debug" + with pytest.raises(SimulatedErr), pytest.warns(_DebuggingTips, match=expected_msg): + cmd.run() + + +@pytest.mark.filterwarnings("error") +def test_encode_pth(): + """Ensure _encode_pth function does not produce encoding warnings""" + content = _encode_pth("tkmilan_ç_utf8") # no warnings (would be turned into errors) + assert isinstance(content, bytes) + + +def install_project(name, venv, tmp_path, files, *opts): + project = tmp_path / name + project.mkdir() + jaraco.path.build(files, prefix=project) + opts = [*opts, "--no-build-isolation"] # force current version of setuptools + out = venv.run( + ["python", "-m", "pip", "-v", "install", "-e", str(project), *opts], + stderr=subprocess.STDOUT, + ) + return project, out + + +def _addsitedirs(new_dirs): + """To use this function, it is necessary to insert new_dir in front of sys.path. + The Python process will try to import a ``sitecustomize`` module on startup. + If we manipulate sys.path/PYTHONPATH, we can force it to run our code, + which invokes ``addsitedir`` and ensure ``.pth`` files are loaded. + """ + content = '\n'.join( + ("import site",) + + tuple(f"site.addsitedir({os.fspath(new_dir)!r})" for new_dir in new_dirs) + ) + (new_dirs[0] / "sitecustomize.py").write_text(content, encoding="utf-8") + + +# ---- Assertion Helpers ---- + + +def assert_path(pkg, expected): + # __path__ is not guaranteed to exist, so we have to account for that + if pkg.__path__: + path = next(iter(pkg.__path__), None) + if path: + assert str(Path(path).resolve()) == expected + + +def assert_link_to(file: Path, other: Path) -> None: + if file.is_symlink(): + assert str(file.resolve()) == str(other.resolve()) + else: + file_stat = file.stat() + other_stat = other.stat() + assert file_stat[stat.ST_INO] == other_stat[stat.ST_INO] + assert file_stat[stat.ST_DEV] == other_stat[stat.ST_DEV] + + +def comparable_path(str_with_path: str) -> str: + return str_with_path.lower().replace(os.sep, "/").replace("//", "/") diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_egg_info.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_egg_info.py new file mode 100644 index 0000000000000000000000000000000000000000..528e2c13d8513727794cf65272aa20ce33e87d9d --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_egg_info.py @@ -0,0 +1,1305 @@ +from __future__ import annotations + +import ast +import glob +import os +import re +import stat +import sys +import time +from pathlib import Path +from unittest import mock + +import pytest +from jaraco import path + +from setuptools import errors +from setuptools.command.egg_info import egg_info, manifest_maker, write_entries +from setuptools.dist import Distribution + +from . import contexts, environment +from .textwrap import DALS + + +class Environment(str): + pass + + +@pytest.fixture +def env(): + with contexts.tempdir(prefix='setuptools-test.') as env_dir: + env = Environment(env_dir) + os.chmod(env_dir, stat.S_IRWXU) + subs = 'home', 'lib', 'scripts', 'data', 'egg-base' + env.paths = dict((dirname, os.path.join(env_dir, dirname)) for dirname in subs) + list(map(os.mkdir, env.paths.values())) + path.build({ + env.paths['home']: { + '.pydistutils.cfg': DALS( + """ + [egg_info] + egg-base = {egg-base} + """.format(**env.paths) + ) + } + }) + yield env + + +class TestEggInfo: + setup_script = DALS( + """ + from setuptools import setup + + setup( + name='foo', + py_modules=['hello'], + entry_points={'console_scripts': ['hi = hello.run']}, + zip_safe=False, + ) + """ + ) + + def _create_project(self): + path.build({ + 'setup.py': self.setup_script, + 'hello.py': DALS( + """ + def run(): + print('hello') + """ + ), + }) + + @staticmethod + def _extract_mv_version(pkg_info_lines: list[str]) -> tuple[int, int]: + version_str = pkg_info_lines[0].split(' ')[1] + major, minor = map(int, version_str.split('.')[:2]) + return major, minor + + def test_egg_info_save_version_info_setup_empty(self, tmpdir_cwd, env): + """ + When the egg_info section is empty or not present, running + save_version_info should add the settings to the setup.cfg + in a deterministic order. + """ + setup_cfg = os.path.join(env.paths['home'], 'setup.cfg') + dist = Distribution() + ei = egg_info(dist) + ei.initialize_options() + ei.save_version_info(setup_cfg) + + with open(setup_cfg, 'r', encoding="utf-8") as f: + content = f.read() + + assert '[egg_info]' in content + assert 'tag_build =' in content + assert 'tag_date = 0' in content + + expected_order = ( + 'tag_build', + 'tag_date', + ) + + self._validate_content_order(content, expected_order) + + @staticmethod + def _validate_content_order(content, expected): + """ + Assert that the strings in expected appear in content + in order. + """ + pattern = '.*'.join(expected) + flags = re.MULTILINE | re.DOTALL + assert re.search(pattern, content, flags) + + def test_egg_info_save_version_info_setup_defaults(self, tmpdir_cwd, env): + """ + When running save_version_info on an existing setup.cfg + with the 'default' values present from a previous run, + the file should remain unchanged. + """ + setup_cfg = os.path.join(env.paths['home'], 'setup.cfg') + path.build({ + setup_cfg: DALS( + """ + [egg_info] + tag_build = + tag_date = 0 + """ + ), + }) + dist = Distribution() + ei = egg_info(dist) + ei.initialize_options() + ei.save_version_info(setup_cfg) + + with open(setup_cfg, 'r', encoding="utf-8") as f: + content = f.read() + + assert '[egg_info]' in content + assert 'tag_build =' in content + assert 'tag_date = 0' in content + + expected_order = ( + 'tag_build', + 'tag_date', + ) + + self._validate_content_order(content, expected_order) + + def test_expected_files_produced(self, tmpdir_cwd, env): + self._create_project() + + self._run_egg_info_command(tmpdir_cwd, env) + actual = os.listdir('foo.egg-info') + + expected = [ + 'PKG-INFO', + 'SOURCES.txt', + 'dependency_links.txt', + 'entry_points.txt', + 'not-zip-safe', + 'top_level.txt', + ] + assert sorted(actual) == expected + + def test_handling_utime_error(self, tmpdir_cwd, env): + dist = Distribution() + ei = egg_info(dist) + utime_patch = mock.patch('os.utime', side_effect=OSError("TEST")) + mkpath_patch = mock.patch( + 'setuptools.command.egg_info.egg_info.mkpath', return_val=None + ) + + with utime_patch, mkpath_patch: + import distutils.errors + + msg = r"Cannot update time stamp of directory 'None'" + with pytest.raises(distutils.errors.DistutilsFileError, match=msg): + ei.run() + + def test_license_is_a_string(self, tmpdir_cwd, env): + setup_config = DALS( + """ + [metadata] + name=foo + version=0.0.1 + license=file:MIT + """ + ) + + setup_script = DALS( + """ + from setuptools import setup + + setup() + """ + ) + + path.build({ + 'setup.py': setup_script, + 'setup.cfg': setup_config, + }) + + # This command should fail with a ValueError, but because it's + # currently configured to use a subprocess, the actual traceback + # object is lost and we need to parse it from stderr + with pytest.raises(AssertionError) as exc: + self._run_egg_info_command(tmpdir_cwd, env) + + # The only argument to the assertion error should be a traceback + # containing a ValueError + assert 'ValueError' in exc.value.args[0] + + def test_rebuilt(self, tmpdir_cwd, env): + """Ensure timestamps are updated when the command is re-run.""" + self._create_project() + + self._run_egg_info_command(tmpdir_cwd, env) + timestamp_a = os.path.getmtime('foo.egg-info') + + # arbitrary sleep just to handle *really* fast systems + time.sleep(0.001) + + self._run_egg_info_command(tmpdir_cwd, env) + timestamp_b = os.path.getmtime('foo.egg-info') + + assert timestamp_a != timestamp_b + + def test_manifest_template_is_read(self, tmpdir_cwd, env): + self._create_project() + path.build({ + 'MANIFEST.in': DALS( + """ + recursive-include docs *.rst + """ + ), + 'docs': { + 'usage.rst': "Run 'hi'", + }, + }) + self._run_egg_info_command(tmpdir_cwd, env) + egg_info_dir = os.path.join('.', 'foo.egg-info') + sources_txt = os.path.join(egg_info_dir, 'SOURCES.txt') + with open(sources_txt, encoding="utf-8") as f: + assert 'docs/usage.rst' in f.read().split('\n') + + def _setup_script_with_requires(self, requires, use_setup_cfg=False): + setup_script = DALS( + """ + from setuptools import setup + + setup(name='foo', zip_safe=False, %s) + """ + ) % ('' if use_setup_cfg else requires) + setup_config = requires if use_setup_cfg else '' + path.build({ + 'setup.py': setup_script, + 'setup.cfg': setup_config, + }) + + mismatch_marker = f"python_version<'{sys.version_info[0]}'" + # Alternate equivalent syntax. + mismatch_marker_alternate = f'python_version < "{sys.version_info[0]}"' + invalid_marker = "<=>++" + + class RequiresTestHelper: + @staticmethod + def parametrize(*test_list, **format_dict): + idlist = [] + argvalues = [] + for test in test_list: + test_params = test.lstrip().split('\n\n', 3) + name_kwargs = test_params.pop(0).split('\n') + if len(name_kwargs) > 1: + val = name_kwargs[1].strip() + install_cmd_kwargs = ast.literal_eval(val) + else: + install_cmd_kwargs = {} + name = name_kwargs[0].strip() + setup_py_requires, setup_cfg_requires, expected_requires = [ + DALS(a).format(**format_dict) for a in test_params + ] + for id_, requires, use_cfg in ( + (name, setup_py_requires, False), + (name + '_in_setup_cfg', setup_cfg_requires, True), + ): + idlist.append(id_) + marks = () + if requires.startswith('@xfail\n'): + requires = requires[7:] + marks = pytest.mark.xfail + argvalues.append( + pytest.param( + requires, + use_cfg, + expected_requires, + install_cmd_kwargs, + marks=marks, + ) + ) + return pytest.mark.parametrize( + ( + "requires", + "use_setup_cfg", + "expected_requires", + "install_cmd_kwargs", + ), + argvalues, + ids=idlist, + ) + + @RequiresTestHelper.parametrize( + # Format of a test: + # + # id + # install_cmd_kwargs [optional] + # + # requires block (when used in setup.py) + # + # requires block (when used in setup.cfg) + # + # expected contents of requires.txt + """ + install_requires_deterministic + + install_requires=["wheel>=0.5", "pytest"] + + [options] + install_requires = + wheel>=0.5 + pytest + + wheel>=0.5 + pytest + """, + """ + install_requires_ordered + + install_requires=["pytest>=3.0.2,!=10.9999"] + + [options] + install_requires = + pytest>=3.0.2,!=10.9999 + + pytest!=10.9999,>=3.0.2 + """, + """ + install_requires_with_marker + + install_requires=["barbazquux;{mismatch_marker}"], + + [options] + install_requires = + barbazquux; {mismatch_marker} + + [:{mismatch_marker_alternate}] + barbazquux + """, + """ + install_requires_with_extra + {'cmd': ['egg_info']} + + install_requires=["barbazquux [test]"], + + [options] + install_requires = + barbazquux [test] + + barbazquux[test] + """, + """ + install_requires_with_extra_and_marker + + install_requires=["barbazquux [test]; {mismatch_marker}"], + + [options] + install_requires = + barbazquux [test]; {mismatch_marker} + + [:{mismatch_marker_alternate}] + barbazquux[test] + """, + """ + setup_requires_with_markers + + setup_requires=["barbazquux;{mismatch_marker}"], + + [options] + setup_requires = + barbazquux; {mismatch_marker} + + """, + """ + extras_require_with_extra + {'cmd': ['egg_info']} + + extras_require={{"extra": ["barbazquux [test]"]}}, + + [options.extras_require] + extra = barbazquux [test] + + [extra] + barbazquux[test] + """, + """ + extras_require_with_extra_and_marker_in_req + + extras_require={{"extra": ["barbazquux [test]; {mismatch_marker}"]}}, + + [options.extras_require] + extra = + barbazquux [test]; {mismatch_marker} + + [extra] + + [extra:{mismatch_marker_alternate}] + barbazquux[test] + """, + # FIXME: ConfigParser does not allow : in key names! + """ + extras_require_with_marker + + extras_require={{":{mismatch_marker}": ["barbazquux"]}}, + + @xfail + [options.extras_require] + :{mismatch_marker} = barbazquux + + [:{mismatch_marker}] + barbazquux + """, + """ + extras_require_with_marker_in_req + + extras_require={{"extra": ["barbazquux; {mismatch_marker}"]}}, + + [options.extras_require] + extra = + barbazquux; {mismatch_marker} + + [extra] + + [extra:{mismatch_marker_alternate}] + barbazquux + """, + """ + extras_require_with_empty_section + + extras_require={{"empty": []}}, + + [options.extras_require] + empty = + + [empty] + """, + # Format arguments. + invalid_marker=invalid_marker, + mismatch_marker=mismatch_marker, + mismatch_marker_alternate=mismatch_marker_alternate, + ) + def test_requires( + self, + tmpdir_cwd, + env, + requires, + use_setup_cfg, + expected_requires, + install_cmd_kwargs, + ): + self._setup_script_with_requires(requires, use_setup_cfg) + self._run_egg_info_command(tmpdir_cwd, env, **install_cmd_kwargs) + egg_info_dir = os.path.join('.', 'foo.egg-info') + requires_txt = os.path.join(egg_info_dir, 'requires.txt') + if os.path.exists(requires_txt): + with open(requires_txt, encoding="utf-8") as fp: + install_requires = fp.read() + else: + install_requires = '' + assert install_requires.lstrip() == expected_requires + assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == [] + + def test_install_requires_unordered_disallowed(self, tmpdir_cwd, env): + """ + Packages that pass unordered install_requires sequences + should be rejected as they produce non-deterministic + builds. See #458. + """ + req = 'install_requires={"fake-factory==0.5.2", "pytz"}' + self._setup_script_with_requires(req) + with pytest.raises(AssertionError): + self._run_egg_info_command(tmpdir_cwd, env) + + def test_extras_require_with_invalid_marker(self, tmpdir_cwd, env): + tmpl = 'extras_require={{":{marker}": ["barbazquux"]}},' + req = tmpl.format(marker=self.invalid_marker) + self._setup_script_with_requires(req) + with pytest.raises(AssertionError): + self._run_egg_info_command(tmpdir_cwd, env) + assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == [] + + def test_extras_require_with_invalid_marker_in_req(self, tmpdir_cwd, env): + tmpl = 'extras_require={{"extra": ["barbazquux; {marker}"]}},' + req = tmpl.format(marker=self.invalid_marker) + self._setup_script_with_requires(req) + with pytest.raises(AssertionError): + self._run_egg_info_command(tmpdir_cwd, env) + assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == [] + + def test_provides_extra(self, tmpdir_cwd, env): + self._setup_script_with_requires('extras_require={"foobar": ["barbazquux"]},') + environ = os.environ.copy().update( + HOME=env.paths['home'], + ) + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + env=environ, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') + assert 'Provides-Extra: foobar' in pkg_info_lines + assert 'Metadata-Version: 2.4' in pkg_info_lines + + def test_doesnt_provides_extra(self, tmpdir_cwd, env): + self._setup_script_with_requires( + """install_requires=["spam ; python_version<'3.6'"]""" + ) + environ = os.environ.copy().update( + HOME=env.paths['home'], + ) + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + env=environ, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_text = fp.read() + assert 'Provides-Extra:' not in pkg_info_text + + @pytest.mark.parametrize( + ('files', 'license_in_sources'), + [ + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_file = LICENSE + """ + ), + 'LICENSE': "Test license", + }, + True, + ), # with license + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_file = INVALID_LICENSE + """ + ), + 'LICENSE': "Test license", + }, + False, + ), # with an invalid license + ( + { + 'setup.cfg': DALS( + """ + """ + ), + 'LICENSE': "Test license", + }, + True, + ), # no license_file attribute, LICENSE auto-included + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_file = LICENSE + """ + ), + 'MANIFEST.in': "exclude LICENSE", + 'LICENSE': "Test license", + }, + True, + ), # manifest is overwritten by license_file + pytest.param( + { + 'setup.cfg': DALS( + """ + [metadata] + license_file = LICEN[CS]E* + """ + ), + 'LICENSE': "Test license", + }, + True, + id="glob_pattern", + ), + ], + ) + def test_setup_cfg_license_file(self, tmpdir_cwd, env, files, license_in_sources): + self._create_project() + path.build(files) + + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + + sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8") + + if license_in_sources: + assert 'LICENSE' in sources_text + else: + assert 'LICENSE' not in sources_text + # for invalid license test + assert 'INVALID_LICENSE' not in sources_text + + @pytest.mark.parametrize( + ('files', 'incl_licenses', 'excl_licenses'), + [ + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_files = + LICENSE-ABC + LICENSE-XYZ + """ + ), + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license", + }, + ['LICENSE-ABC', 'LICENSE-XYZ'], + [], + ), # with licenses + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_files = LICENSE-ABC, LICENSE-XYZ + """ + ), + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license", + }, + ['LICENSE-ABC', 'LICENSE-XYZ'], + [], + ), # with commas + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_files = + LICENSE-ABC + """ + ), + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license", + }, + ['LICENSE-ABC'], + ['LICENSE-XYZ'], + ), # with one license + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_files = + """ + ), + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license", + }, + [], + ['LICENSE-ABC', 'LICENSE-XYZ'], + ), # empty + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_files = LICENSE-XYZ + """ + ), + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license", + }, + ['LICENSE-XYZ'], + ['LICENSE-ABC'], + ), # on same line + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_files = + LICENSE-ABC + INVALID_LICENSE + """ + ), + 'LICENSE-ABC': "Test license", + }, + ['LICENSE-ABC'], + ['INVALID_LICENSE'], + ), # with an invalid license + ( + { + 'setup.cfg': DALS( + """ + """ + ), + 'LICENSE': "Test license", + }, + ['LICENSE'], + [], + ), # no license_files attribute, LICENSE auto-included + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_files = LICENSE + """ + ), + 'MANIFEST.in': "exclude LICENSE", + 'LICENSE': "Test license", + }, + ['LICENSE'], + [], + ), # manifest is overwritten by license_files + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_files = + LICENSE-ABC + LICENSE-XYZ + """ + ), + 'MANIFEST.in': "exclude LICENSE-XYZ", + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license", + # manifest is overwritten by license_files + }, + ['LICENSE-ABC', 'LICENSE-XYZ'], + [], + ), + pytest.param( + { + 'setup.cfg': "", + 'LICENSE-ABC': "ABC license", + 'COPYING-ABC': "ABC copying", + 'NOTICE-ABC': "ABC notice", + 'AUTHORS-ABC': "ABC authors", + 'LICENCE-XYZ': "XYZ license", + 'LICENSE': "License", + 'INVALID-LICENSE': "Invalid license", + }, + [ + 'LICENSE-ABC', + 'COPYING-ABC', + 'NOTICE-ABC', + 'AUTHORS-ABC', + 'LICENCE-XYZ', + 'LICENSE', + ], + ['INVALID-LICENSE'], + # ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') + id="default_glob_patterns", + ), + pytest.param( + { + 'setup.cfg': DALS( + """ + [metadata] + license_files = + LICENSE* + """ + ), + 'LICENSE-ABC': "ABC license", + 'NOTICE-XYZ': "XYZ notice", + }, + ['LICENSE-ABC'], + ['NOTICE-XYZ'], + id="no_default_glob_patterns", + ), + pytest.param( + { + 'setup.cfg': DALS( + """ + [metadata] + license_files = + LICENSE-ABC + LICENSE* + """ + ), + 'LICENSE-ABC': "ABC license", + }, + ['LICENSE-ABC'], + [], + id="files_only_added_once", + ), + pytest.param( + { + 'setup.cfg': DALS( + """ + [metadata] + license_files = **/LICENSE + """ + ), + 'LICENSE': "ABC license", + 'LICENSE-OTHER': "Don't include", + 'vendor': {'LICENSE': "Vendor license"}, + }, + ['LICENSE', 'vendor/LICENSE'], + ['LICENSE-OTHER'], + id="recursive_glob", + ), + ], + ) + def test_setup_cfg_license_files( + self, tmpdir_cwd, env, files, incl_licenses, excl_licenses + ): + self._create_project() + path.build(files) + + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + + sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8") + sources_lines = [line.strip() for line in sources_text.splitlines()] + + for lf in incl_licenses: + assert sources_lines.count(lf) == 1 + + for lf in excl_licenses: + assert sources_lines.count(lf) == 0 + + @pytest.mark.parametrize( + ('files', 'incl_licenses', 'excl_licenses'), + [ + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_file = + license_files = + """ + ), + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license", + }, + [], + ['LICENSE-ABC', 'LICENSE-XYZ'], + ), # both empty + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_file = + LICENSE-ABC + LICENSE-XYZ + """ + ), + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license", + # license_file is still singular + }, + [], + ['LICENSE-ABC', 'LICENSE-XYZ'], + ), + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-XYZ + LICENSE-PQR + """ + ), + 'LICENSE-ABC': "ABC license", + 'LICENSE-PQR': "PQR license", + 'LICENSE-XYZ': "XYZ license", + }, + ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], + [], + ), # combined + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-ABC + LICENSE-XYZ + LICENSE-PQR + """ + ), + 'LICENSE-ABC': "ABC license", + 'LICENSE-PQR': "PQR license", + 'LICENSE-XYZ': "XYZ license", + # duplicate license + }, + ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], + [], + ), + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-XYZ + """ + ), + 'LICENSE-ABC': "ABC license", + 'LICENSE-PQR': "PQR license", + 'LICENSE-XYZ': "XYZ license", + # combined subset + }, + ['LICENSE-ABC', 'LICENSE-XYZ'], + ['LICENSE-PQR'], + ), + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-XYZ + LICENSE-PQR + """ + ), + 'LICENSE-PQR': "Test license", + # with invalid licenses + }, + ['LICENSE-PQR'], + ['LICENSE-ABC', 'LICENSE-XYZ'], + ), + ( + { + 'setup.cfg': DALS( + """ + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-PQR + LICENSE-XYZ + """ + ), + 'MANIFEST.in': "exclude LICENSE-ABC\nexclude LICENSE-PQR", + 'LICENSE-ABC': "ABC license", + 'LICENSE-PQR': "PQR license", + 'LICENSE-XYZ': "XYZ license", + # manifest is overwritten + }, + ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], + [], + ), + pytest.param( + { + 'setup.cfg': DALS( + """ + [metadata] + license_file = LICENSE* + """ + ), + 'LICENSE-ABC': "ABC license", + 'NOTICE-XYZ': "XYZ notice", + }, + ['LICENSE-ABC'], + ['NOTICE-XYZ'], + id="no_default_glob_patterns", + ), + pytest.param( + { + 'setup.cfg': DALS( + """ + [metadata] + license_file = LICENSE* + license_files = + NOTICE* + """ + ), + 'LICENSE-ABC': "ABC license", + 'NOTICE-ABC': "ABC notice", + 'AUTHORS-ABC': "ABC authors", + }, + ['LICENSE-ABC', 'NOTICE-ABC'], + ['AUTHORS-ABC'], + id="combined_glob_patterrns", + ), + ], + ) + def test_setup_cfg_license_file_license_files( + self, tmpdir_cwd, env, files, incl_licenses, excl_licenses + ): + self._create_project() + path.build(files) + + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + + sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8") + sources_lines = [line.strip() for line in sources_text.splitlines()] + + for lf in incl_licenses: + assert sources_lines.count(lf) == 1 + + for lf in excl_licenses: + assert sources_lines.count(lf) == 0 + + def test_license_file_attr_pkg_info(self, tmpdir_cwd, env): + """All matched license files should have a corresponding License-File.""" + self._create_project() + path.build({ + "setup.cfg": DALS( + """ + [metadata] + license_files = + NOTICE* + LICENSE* + **/LICENSE + """ + ), + "LICENSE-ABC": "ABC license", + "LICENSE-XYZ": "XYZ license", + "NOTICE": "included", + "IGNORE": "not include", + "vendor": {'LICENSE': "Vendor license"}, + }) + + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') + license_file_lines = [ + line for line in pkg_info_lines if line.startswith('License-File:') + ] + + # Only 'NOTICE', LICENSE-ABC', and 'LICENSE-XYZ' should have been matched + # Also assert that order from license_files is keeped + assert len(license_file_lines) == 4 + assert "License-File: NOTICE" == license_file_lines[0] + assert "License-File: LICENSE-ABC" in license_file_lines[1:] + assert "License-File: LICENSE-XYZ" in license_file_lines[1:] + assert "License-File: vendor/LICENSE" in license_file_lines[3] + + def test_metadata_version(self, tmpdir_cwd, env): + """Make sure latest metadata version is used by default.""" + self._setup_script_with_requires("") + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') + # Update metadata version if changed + assert self._extract_mv_version(pkg_info_lines) == (2, 4) + + def test_long_description_content_type(self, tmpdir_cwd, env): + # Test that specifying a `long_description_content_type` keyword arg to + # the `setup` function results in writing a `Description-Content-Type` + # line to the `PKG-INFO` file in the `.egg-info` + # directory. + # `Description-Content-Type` is described at + # https://github.com/pypa/python-packaging-user-guide/pull/258 + + self._setup_script_with_requires( + """long_description_content_type='text/markdown',""" + ) + environ = os.environ.copy().update( + HOME=env.paths['home'], + ) + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + env=environ, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') + expected_line = 'Description-Content-Type: text/markdown' + assert expected_line in pkg_info_lines + assert 'Metadata-Version: 2.4' in pkg_info_lines + + def test_long_description(self, tmpdir_cwd, env): + # Test that specifying `long_description` and `long_description_content_type` + # keyword args to the `setup` function results in writing + # the description in the message payload of the `PKG-INFO` file + # in the `.egg-info` directory. + self._setup_script_with_requires( + "long_description='This is a long description\\nover multiple lines'," + "long_description_content_type='text/markdown'," + ) + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') + assert 'Metadata-Version: 2.4' in pkg_info_lines + assert '' == pkg_info_lines[-1] # last line should be empty + long_desc_lines = pkg_info_lines[pkg_info_lines.index('') :] + assert 'This is a long description' in long_desc_lines + assert 'over multiple lines' in long_desc_lines + + def test_project_urls(self, tmpdir_cwd, env): + # Test that specifying a `project_urls` dict to the `setup` + # function results in writing multiple `Project-URL` lines to + # the `PKG-INFO` file in the `.egg-info` + # directory. + # `Project-URL` is described at https://packaging.python.org + # /specifications/core-metadata/#project-url-multiple-use + + self._setup_script_with_requires( + """project_urls={ + 'Link One': 'https://example.com/one/', + 'Link Two': 'https://example.com/two/', + },""" + ) + environ = os.environ.copy().update( + HOME=env.paths['home'], + ) + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + env=environ, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') + expected_line = 'Project-URL: Link One, https://example.com/one/' + assert expected_line in pkg_info_lines + expected_line = 'Project-URL: Link Two, https://example.com/two/' + assert expected_line in pkg_info_lines + assert self._extract_mv_version(pkg_info_lines) >= (1, 2) + + def test_license(self, tmpdir_cwd, env): + """Test single line license.""" + self._setup_script_with_requires("license='MIT',") + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') + assert 'License: MIT' in pkg_info_lines + + def test_license_escape(self, tmpdir_cwd, env): + """Test license is escaped correctly if longer than one line.""" + self._setup_script_with_requires( + "license='This is a long license text \\nover multiple lines'," + ) + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') + + assert 'License: This is a long license text ' in pkg_info_lines + assert ' over multiple lines' in pkg_info_lines + assert 'text \n over multiple' in '\n'.join(pkg_info_lines) + + def test_python_requires_egg_info(self, tmpdir_cwd, env): + self._setup_script_with_requires("""python_requires='>=2.7.12',""") + environ = os.environ.copy().update( + HOME=env.paths['home'], + ) + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + env=environ, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') + assert 'Requires-Python: >=2.7.12' in pkg_info_lines + assert self._extract_mv_version(pkg_info_lines) >= (1, 2) + + def test_manifest_maker_warning_suppression(self): + fixtures = [ + "standard file not found: should have one of foo.py, bar.py", + "standard file 'setup.py' not found", + ] + + for msg in fixtures: + assert manifest_maker._should_suppress_warning(msg) + + def test_egg_info_includes_setup_py(self, tmpdir_cwd): + self._create_project() + dist = Distribution({"name": "foo", "version": "0.0.1"}) + dist.script_name = "non_setup.py" + egg_info_instance = egg_info(dist) + egg_info_instance.finalize_options() + egg_info_instance.run() + + assert 'setup.py' in egg_info_instance.filelist.files + + with open(egg_info_instance.egg_info + "/SOURCES.txt", encoding="utf-8") as f: + sources = f.read().split('\n') + assert 'setup.py' in sources + + def _run_egg_info_command(self, tmpdir_cwd, env, cmd=None, output=None): + environ = os.environ.copy().update( + HOME=env.paths['home'], + ) + if cmd is None: + cmd = [ + 'egg_info', + ] + code, data = environment.run_setup_py( + cmd=cmd, + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + env=environ, + ) + assert not code, data + + if output: + assert output in data + + def test_egg_info_tag_only_once(self, tmpdir_cwd, env): + self._create_project() + path.build({ + 'setup.cfg': DALS( + """ + [egg_info] + tag_build = dev + tag_date = 0 + tag_svn_revision = 0 + """ + ), + }) + self._run_egg_info_command(tmpdir_cwd, env) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') + assert 'Version: 0.0.0.dev0' in pkg_info_lines + + +class TestWriteEntries: + def test_invalid_entry_point(self, tmpdir_cwd, env): + dist = Distribution({"name": "foo", "version": "0.0.1"}) + dist.entry_points = {"foo": "foo = invalid-identifier:foo"} + cmd = dist.get_command_obj("egg_info") + expected_msg = r"Problems to parse .*invalid-identifier.*" + with pytest.raises(errors.OptionError, match=expected_msg) as ex: + write_entries(cmd, "entry_points", "entry_points.txt") + assert "ensure entry-point follows the spec" in ex.value.args[0] + + def test_valid_entry_point(self, tmpdir_cwd, env): + dist = Distribution({"name": "foo", "version": "0.0.1"}) + dist.entry_points = { + "abc": "foo = bar:baz", + "def": ["faa = bor:boz"], + } + cmd = dist.get_command_obj("egg_info") + write_entries(cmd, "entry_points", "entry_points.txt") + content = Path("entry_points.txt").read_text(encoding="utf-8") + assert "[abc]\nfoo = bar:baz\n" in content + assert "[def]\nfaa = bor:boz\n" in content diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_extern.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_extern.py new file mode 100644 index 0000000000000000000000000000000000000000..d7eb3c62c190dacdd4a054d2934962be2f4ee860 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_extern.py @@ -0,0 +1,15 @@ +import importlib +import pickle + +import packaging + +from setuptools import Distribution + + +def test_reimport_extern(): + packaging2 = importlib.import_module(packaging.__name__) + assert packaging is packaging2 + + +def test_distribution_picklable(): + pickle.loads(pickle.dumps(Distribution())) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_find_packages.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_find_packages.py new file mode 100644 index 0000000000000000000000000000000000000000..9fd9f8f6637d13cb898fcd446a6b090323d02014 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_find_packages.py @@ -0,0 +1,218 @@ +"""Tests for automatic package discovery""" + +import os +import shutil +import tempfile + +import pytest + +from setuptools import find_namespace_packages, find_packages +from setuptools.discovery import FlatLayoutPackageFinder + +from .compat.py39 import os_helper + + +class TestFindPackages: + def setup_method(self, method): + self.dist_dir = tempfile.mkdtemp() + self._make_pkg_structure() + + def teardown_method(self, method): + shutil.rmtree(self.dist_dir) + + def _make_pkg_structure(self): + """Make basic package structure. + + dist/ + docs/ + conf.py + pkg/ + __pycache__/ + nspkg/ + mod.py + subpkg/ + assets/ + asset + __init__.py + setup.py + + """ + self.docs_dir = self._mkdir('docs', self.dist_dir) + self._touch('conf.py', self.docs_dir) + self.pkg_dir = self._mkdir('pkg', self.dist_dir) + self._mkdir('__pycache__', self.pkg_dir) + self.ns_pkg_dir = self._mkdir('nspkg', self.pkg_dir) + self._touch('mod.py', self.ns_pkg_dir) + self.sub_pkg_dir = self._mkdir('subpkg', self.pkg_dir) + self.asset_dir = self._mkdir('assets', self.sub_pkg_dir) + self._touch('asset', self.asset_dir) + self._touch('__init__.py', self.sub_pkg_dir) + self._touch('setup.py', self.dist_dir) + + def _mkdir(self, path, parent_dir=None): + if parent_dir: + path = os.path.join(parent_dir, path) + os.mkdir(path) + return path + + def _touch(self, path, dir_=None): + if dir_: + path = os.path.join(dir_, path) + open(path, 'wb').close() + return path + + def test_regular_package(self): + self._touch('__init__.py', self.pkg_dir) + packages = find_packages(self.dist_dir) + assert packages == ['pkg', 'pkg.subpkg'] + + def test_exclude(self): + self._touch('__init__.py', self.pkg_dir) + packages = find_packages(self.dist_dir, exclude=('pkg.*',)) + assert packages == ['pkg'] + + def test_exclude_recursive(self): + """ + Excluding a parent package should not exclude child packages as well. + """ + self._touch('__init__.py', self.pkg_dir) + self._touch('__init__.py', self.sub_pkg_dir) + packages = find_packages(self.dist_dir, exclude=('pkg',)) + assert packages == ['pkg.subpkg'] + + def test_include_excludes_other(self): + """ + If include is specified, other packages should be excluded. + """ + self._touch('__init__.py', self.pkg_dir) + alt_dir = self._mkdir('other_pkg', self.dist_dir) + self._touch('__init__.py', alt_dir) + packages = find_packages(self.dist_dir, include=['other_pkg']) + assert packages == ['other_pkg'] + + def test_dir_with_dot_is_skipped(self): + shutil.rmtree(os.path.join(self.dist_dir, 'pkg/subpkg/assets')) + data_dir = self._mkdir('some.data', self.pkg_dir) + self._touch('__init__.py', data_dir) + self._touch('file.dat', data_dir) + packages = find_packages(self.dist_dir) + assert 'pkg.some.data' not in packages + + def test_dir_with_packages_in_subdir_is_excluded(self): + """ + Ensure that a package in a non-package such as build/pkg/__init__.py + is excluded. + """ + build_dir = self._mkdir('build', self.dist_dir) + build_pkg_dir = self._mkdir('pkg', build_dir) + self._touch('__init__.py', build_pkg_dir) + packages = find_packages(self.dist_dir) + assert 'build.pkg' not in packages + + @pytest.mark.skipif(not os_helper.can_symlink(), reason='Symlink support required') + def test_symlinked_packages_are_included(self): + """ + A symbolically-linked directory should be treated like any other + directory when matched as a package. + + Create a link from lpkg -> pkg. + """ + self._touch('__init__.py', self.pkg_dir) + linked_pkg = os.path.join(self.dist_dir, 'lpkg') + os.symlink('pkg', linked_pkg) + assert os.path.isdir(linked_pkg) + packages = find_packages(self.dist_dir) + assert 'lpkg' in packages + + def _assert_packages(self, actual, expected): + assert set(actual) == set(expected) + + def test_pep420_ns_package(self): + packages = find_namespace_packages( + self.dist_dir, include=['pkg*'], exclude=['pkg.subpkg.assets'] + ) + self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg']) + + def test_pep420_ns_package_no_includes(self): + packages = find_namespace_packages(self.dist_dir, exclude=['pkg.subpkg.assets']) + self._assert_packages(packages, ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg']) + + def test_pep420_ns_package_no_includes_or_excludes(self): + packages = find_namespace_packages(self.dist_dir) + expected = ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg', 'pkg.subpkg.assets'] + self._assert_packages(packages, expected) + + def test_regular_package_with_nested_pep420_ns_packages(self): + self._touch('__init__.py', self.pkg_dir) + packages = find_namespace_packages( + self.dist_dir, exclude=['docs', 'pkg.subpkg.assets'] + ) + self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg']) + + def test_pep420_ns_package_no_non_package_dirs(self): + shutil.rmtree(self.docs_dir) + shutil.rmtree(os.path.join(self.dist_dir, 'pkg/subpkg/assets')) + packages = find_namespace_packages(self.dist_dir) + self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg']) + + +class TestFlatLayoutPackageFinder: + EXAMPLES = { + "hidden-folders": ( + [".pkg/__init__.py", "pkg/__init__.py", "pkg/nested/file.txt"], + ["pkg", "pkg.nested"], + ), + "private-packages": ( + ["_pkg/__init__.py", "pkg/_private/__init__.py"], + ["pkg", "pkg._private"], + ), + "invalid-name": ( + ["invalid-pkg/__init__.py", "other.pkg/__init__.py", "yet,another/file.py"], + [], + ), + "docs": (["pkg/__init__.py", "docs/conf.py", "docs/readme.rst"], ["pkg"]), + "tests": ( + ["pkg/__init__.py", "tests/test_pkg.py", "tests/__init__.py"], + ["pkg"], + ), + "examples": ( + [ + "pkg/__init__.py", + "examples/__init__.py", + "examples/file.py", + "example/other_file.py", + # Sub-packages should always be fine + "pkg/example/__init__.py", + "pkg/examples/__init__.py", + ], + ["pkg", "pkg.examples", "pkg.example"], + ), + "tool-specific": ( + [ + "htmlcov/index.html", + "pkg/__init__.py", + "tasks/__init__.py", + "tasks/subpackage/__init__.py", + "fabfile/__init__.py", + "fabfile/subpackage/__init__.py", + # Sub-packages should always be fine + "pkg/tasks/__init__.py", + "pkg/fabfile/__init__.py", + ], + ["pkg", "pkg.tasks", "pkg.fabfile"], + ), + } + + @pytest.mark.parametrize("example", EXAMPLES.keys()) + def test_unwanted_directories_not_included(self, tmp_path, example): + files, expected_packages = self.EXAMPLES[example] + ensure_files(tmp_path, files) + found_packages = FlatLayoutPackageFinder.find(str(tmp_path)) + assert set(found_packages) == set(expected_packages) + + +def ensure_files(root_path, files): + for file in files: + path = root_path / file + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_find_py_modules.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_find_py_modules.py new file mode 100644 index 0000000000000000000000000000000000000000..8034b544294e5d30274bac82f24f93613120a0d4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_find_py_modules.py @@ -0,0 +1,73 @@ +"""Tests for automatic discovery of modules""" + +import os + +import pytest + +from setuptools.discovery import FlatLayoutModuleFinder, ModuleFinder + +from .compat.py39 import os_helper +from .test_find_packages import ensure_files + + +class TestModuleFinder: + def find(self, path, *args, **kwargs): + return set(ModuleFinder.find(str(path), *args, **kwargs)) + + EXAMPLES = { + # circumstance: (files, kwargs, expected_modules) + "simple_folder": ( + ["file.py", "other.py"], + {}, # kwargs + ["file", "other"], + ), + "exclude": ( + ["file.py", "other.py"], + {"exclude": ["f*"]}, + ["other"], + ), + "include": ( + ["file.py", "fole.py", "other.py"], + {"include": ["f*"], "exclude": ["fo*"]}, + ["file"], + ), + "invalid-name": (["my-file.py", "other.file.py"], {}, []), + } + + @pytest.mark.parametrize("example", EXAMPLES.keys()) + def test_finder(self, tmp_path, example): + files, kwargs, expected_modules = self.EXAMPLES[example] + ensure_files(tmp_path, files) + assert self.find(tmp_path, **kwargs) == set(expected_modules) + + @pytest.mark.skipif(not os_helper.can_symlink(), reason='Symlink support required') + def test_symlinked_packages_are_included(self, tmp_path): + src = "_myfiles/file.py" + ensure_files(tmp_path, [src]) + os.symlink(tmp_path / src, tmp_path / "link.py") + assert self.find(tmp_path) == {"link"} + + +class TestFlatLayoutModuleFinder: + def find(self, path, *args, **kwargs): + return set(FlatLayoutModuleFinder.find(str(path))) + + EXAMPLES = { + # circumstance: (files, expected_modules) + "hidden-files": ([".module.py"], []), + "private-modules": (["_module.py"], []), + "common-names": ( + ["setup.py", "conftest.py", "test.py", "tests.py", "example.py", "mod.py"], + ["mod"], + ), + "tool-specific": ( + ["tasks.py", "fabfile.py", "noxfile.py", "dodo.py", "manage.py", "mod.py"], + ["mod"], + ), + } + + @pytest.mark.parametrize("example", EXAMPLES.keys()) + def test_unwanted_files_not_included(self, tmp_path, example): + files, expected_modules = self.EXAMPLES[example] + ensure_files(tmp_path, files) + assert self.find(tmp_path) == set(expected_modules) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_glob.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_glob.py new file mode 100644 index 0000000000000000000000000000000000000000..8d225a44610163c7d56d65b07c06f0f598ccfe84 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_glob.py @@ -0,0 +1,45 @@ +import pytest +from jaraco import path + +from setuptools.glob import glob + + +@pytest.mark.parametrize( + ('tree', 'pattern', 'matches'), + ( + ('', b'', []), + ('', '', []), + ( + """ + appveyor.yml + CHANGES.rst + LICENSE + MANIFEST.in + pyproject.toml + README.rst + setup.cfg + setup.py + """, + '*.rst', + ('CHANGES.rst', 'README.rst'), + ), + ( + """ + appveyor.yml + CHANGES.rst + LICENSE + MANIFEST.in + pyproject.toml + README.rst + setup.cfg + setup.py + """, + b'*.rst', + (b'CHANGES.rst', b'README.rst'), + ), + ), +) +def test_glob(monkeypatch, tmpdir, tree, pattern, matches): + monkeypatch.chdir(tmpdir) + path.build({name: '' for name in tree.split()}) + assert list(sorted(glob(pattern))) == list(sorted(matches)) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_install_scripts.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_install_scripts.py new file mode 100644 index 0000000000000000000000000000000000000000..e62a6b7f318df2da0cf29e53c41f74e5525e78ac --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_install_scripts.py @@ -0,0 +1,89 @@ +"""install_scripts tests""" + +import sys + +import pytest + +from setuptools.command.install_scripts import install_scripts +from setuptools.dist import Distribution + +from . import contexts + + +class TestInstallScripts: + settings = dict( + name='foo', + entry_points={'console_scripts': ['foo=foo:foo']}, + version='0.0', + ) + unix_exe = '/usr/dummy-test-path/local/bin/python' + unix_spaces_exe = '/usr/bin/env dummy-test-python' + win32_exe = 'C:\\Dummy Test Path\\Program Files\\Python 3.6\\python.exe' + + def _run_install_scripts(self, install_dir, executable=None): + dist = Distribution(self.settings) + dist.script_name = 'setup.py' + cmd = install_scripts(dist) + cmd.install_dir = install_dir + if executable is not None: + bs = cmd.get_finalized_command('build_scripts') + bs.executable = executable + cmd.ensure_finalized() + with contexts.quiet(): + cmd.run() + + @pytest.mark.skipif(sys.platform == 'win32', reason='non-Windows only') + def test_sys_executable_escaping_unix(self, tmpdir, monkeypatch): + """ + Ensure that shebang is not quoted on Unix when getting the Python exe + from sys.executable. + """ + expected = f'#!{self.unix_exe}\n' + monkeypatch.setattr('sys.executable', self.unix_exe) + with tmpdir.as_cwd(): + self._run_install_scripts(str(tmpdir)) + with open(str(tmpdir.join('foo')), 'r', encoding="utf-8") as f: + actual = f.readline() + assert actual == expected + + @pytest.mark.skipif(sys.platform != 'win32', reason='Windows only') + def test_sys_executable_escaping_win32(self, tmpdir, monkeypatch): + """ + Ensure that shebang is quoted on Windows when getting the Python exe + from sys.executable and it contains a space. + """ + expected = f'#!"{self.win32_exe}"\n' + monkeypatch.setattr('sys.executable', self.win32_exe) + with tmpdir.as_cwd(): + self._run_install_scripts(str(tmpdir)) + with open(str(tmpdir.join('foo-script.py')), 'r', encoding="utf-8") as f: + actual = f.readline() + assert actual == expected + + @pytest.mark.skipif(sys.platform == 'win32', reason='non-Windows only') + def test_executable_with_spaces_escaping_unix(self, tmpdir): + """ + Ensure that shebang on Unix is not quoted, even when + a value with spaces + is specified using --executable. + """ + expected = f'#!{self.unix_spaces_exe}\n' + with tmpdir.as_cwd(): + self._run_install_scripts(str(tmpdir), self.unix_spaces_exe) + with open(str(tmpdir.join('foo')), 'r', encoding="utf-8") as f: + actual = f.readline() + assert actual == expected + + @pytest.mark.skipif(sys.platform != 'win32', reason='Windows only') + def test_executable_arg_escaping_win32(self, tmpdir): + """ + Ensure that shebang on Windows is quoted when + getting a path with spaces + from --executable, that is itself properly quoted. + """ + expected = f'#!"{self.win32_exe}"\n' + with tmpdir.as_cwd(): + self._run_install_scripts(str(tmpdir), '"' + self.win32_exe + '"') + with open(str(tmpdir.join('foo-script.py')), 'r', encoding="utf-8") as f: + actual = f.readline() + assert actual == expected diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_logging.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_logging.py new file mode 100644 index 0000000000000000000000000000000000000000..ea58001e93d8e4bcbd50bfd49303324d71858d16 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_logging.py @@ -0,0 +1,76 @@ +import functools +import inspect +import logging +import sys + +import pytest + +IS_PYPY = '__pypy__' in sys.builtin_module_names + + +setup_py = """\ +from setuptools import setup + +setup( + name="test_logging", + version="0.0" +) +""" + + +@pytest.mark.parametrize( + ('flag', 'expected_level'), [("--dry-run", "INFO"), ("--verbose", "DEBUG")] +) +def test_verbosity_level(tmp_path, monkeypatch, flag, expected_level): + """Make sure the correct verbosity level is set (issue #3038)""" + import setuptools # noqa: F401 # import setuptools to monkeypatch distutils + + import distutils # <- load distutils after all the patches take place + + logger = logging.Logger(__name__) + monkeypatch.setattr(logging, "root", logger) + unset_log_level = logger.getEffectiveLevel() + assert logging.getLevelName(unset_log_level) == "NOTSET" + + setup_script = tmp_path / "setup.py" + setup_script.write_text(setup_py, encoding="utf-8") + dist = distutils.core.run_setup(setup_script, stop_after="init") + dist.script_args = [flag, "sdist"] + dist.parse_command_line() # <- where the log level is set + log_level = logger.getEffectiveLevel() + log_level_name = logging.getLevelName(log_level) + assert log_level_name == expected_level + + +def flaky_on_pypy(func): + @functools.wraps(func) + def _func(): + try: + func() + except AssertionError: # pragma: no cover + if IS_PYPY: + msg = "Flaky monkeypatch on PyPy (#4124)" + pytest.xfail(f"{msg}. Original discussion in #3707, #3709.") + raise + + return _func + + +@flaky_on_pypy +def test_patching_does_not_cause_problems(): + # Ensure `dist.log` is only patched if necessary + + import _distutils_hack + + import setuptools.logging + + from distutils import dist + + setuptools.logging.configure() + + if _distutils_hack.enabled(): + # Modern logging infra, no problematic patching. + assert dist.__file__ is None or "setuptools" in dist.__file__ + assert isinstance(dist.log, logging.Logger) + else: + assert inspect.ismodule(dist.log) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_manifest.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_manifest.py new file mode 100644 index 0000000000000000000000000000000000000000..903a528db0cc2bba27bbcef24aa1c59dc4156528 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_manifest.py @@ -0,0 +1,622 @@ +"""sdist tests""" + +from __future__ import annotations + +import contextlib +import io +import itertools +import logging +import os +import shutil +import sys +import tempfile + +import pytest + +from setuptools.command.egg_info import FileList, egg_info, translate_pattern +from setuptools.dist import Distribution +from setuptools.tests.textwrap import DALS + +from distutils import log +from distutils.errors import DistutilsTemplateError + +IS_PYPY = '__pypy__' in sys.builtin_module_names + + +def make_local_path(s): + """Converts '/' in a string to os.sep""" + return s.replace('/', os.sep) + + +SETUP_ATTRS = { + 'name': 'app', + 'version': '0.0', + 'packages': ['app'], +} + +SETUP_PY = f"""\ +from setuptools import setup + +setup(**{SETUP_ATTRS!r}) +""" + + +@contextlib.contextmanager +def quiet(): + old_stdout, old_stderr = sys.stdout, sys.stderr + sys.stdout, sys.stderr = io.StringIO(), io.StringIO() + try: + yield + finally: + sys.stdout, sys.stderr = old_stdout, old_stderr + + +def touch(filename): + open(filename, 'wb').close() + + +# The set of files always in the manifest, including all files in the +# .egg-info directory +default_files = frozenset( + map( + make_local_path, + [ + 'README.rst', + 'MANIFEST.in', + 'setup.py', + 'app.egg-info/PKG-INFO', + 'app.egg-info/SOURCES.txt', + 'app.egg-info/dependency_links.txt', + 'app.egg-info/top_level.txt', + 'app/__init__.py', + ], + ) +) + + +translate_specs: list[tuple[str, list[str], list[str]]] = [ + ('foo', ['foo'], ['bar', 'foobar']), + ('foo/bar', ['foo/bar'], ['foo/bar/baz', './foo/bar', 'foo']), + # Glob matching + ('*.txt', ['foo.txt', 'bar.txt'], ['foo/foo.txt']), + ('dir/*.txt', ['dir/foo.txt', 'dir/bar.txt', 'dir/.txt'], ['notdir/foo.txt']), + ('*/*.py', ['bin/start.py'], []), + ('docs/page-?.txt', ['docs/page-9.txt'], ['docs/page-10.txt']), + # Globstars change what they mean depending upon where they are + ( + 'foo/**/bar', + ['foo/bing/bar', 'foo/bing/bang/bar', 'foo/bar'], + ['foo/abar'], + ), + ( + 'foo/**', + ['foo/bar/bing.py', 'foo/x'], + ['/foo/x'], + ), + ( + '**', + ['x', 'abc/xyz', '@nything'], + [], + ), + # Character classes + ( + 'pre[one]post', + ['preopost', 'prenpost', 'preepost'], + ['prepost', 'preonepost'], + ), + ( + 'hello[!one]world', + ['helloxworld', 'helloyworld'], + ['hellooworld', 'helloworld', 'hellooneworld'], + ), + ( + '[]one].txt', + ['o.txt', '].txt', 'e.txt'], + ['one].txt'], + ), + ( + 'foo[!]one]bar', + ['fooybar'], + ['foo]bar', 'fooobar', 'fooebar'], + ), +] +""" +A spec of inputs for 'translate_pattern' and matches and mismatches +for that input. +""" + +match_params = itertools.chain.from_iterable( + zip(itertools.repeat(pattern), matches) + for pattern, matches, mismatches in translate_specs +) + + +@pytest.fixture(params=match_params) +def pattern_match(request): + return map(make_local_path, request.param) + + +mismatch_params = itertools.chain.from_iterable( + zip(itertools.repeat(pattern), mismatches) + for pattern, matches, mismatches in translate_specs +) + + +@pytest.fixture(params=mismatch_params) +def pattern_mismatch(request): + return map(make_local_path, request.param) + + +def test_translated_pattern_match(pattern_match): + pattern, target = pattern_match + assert translate_pattern(pattern).match(target) + + +def test_translated_pattern_mismatch(pattern_mismatch): + pattern, target = pattern_mismatch + assert not translate_pattern(pattern).match(target) + + +class TempDirTestCase: + def setup_method(self, method): + self.temp_dir = tempfile.mkdtemp() + self.old_cwd = os.getcwd() + os.chdir(self.temp_dir) + + def teardown_method(self, method): + os.chdir(self.old_cwd) + shutil.rmtree(self.temp_dir) + + +class TestManifestTest(TempDirTestCase): + def setup_method(self, method): + super().setup_method(method) + + f = open(os.path.join(self.temp_dir, 'setup.py'), 'w', encoding="utf-8") + f.write(SETUP_PY) + f.close() + """ + Create a file tree like: + - LICENSE + - README.rst + - testing.rst + - .hidden.rst + - app/ + - __init__.py + - a.txt + - b.txt + - c.rst + - static/ + - app.js + - app.js.map + - app.css + - app.css.map + """ + + for fname in ['README.rst', '.hidden.rst', 'testing.rst', 'LICENSE']: + touch(os.path.join(self.temp_dir, fname)) + + # Set up the rest of the test package + test_pkg = os.path.join(self.temp_dir, 'app') + os.mkdir(test_pkg) + for fname in ['__init__.py', 'a.txt', 'b.txt', 'c.rst']: + touch(os.path.join(test_pkg, fname)) + + # Some compiled front-end assets to include + static = os.path.join(test_pkg, 'static') + os.mkdir(static) + for fname in ['app.js', 'app.js.map', 'app.css', 'app.css.map']: + touch(os.path.join(static, fname)) + + def make_manifest(self, contents): + """Write a MANIFEST.in.""" + manifest = os.path.join(self.temp_dir, 'MANIFEST.in') + with open(manifest, 'w', encoding="utf-8") as f: + f.write(DALS(contents)) + + def get_files(self): + """Run egg_info and get all the files to include, as a set""" + dist = Distribution(SETUP_ATTRS) + dist.script_name = 'setup.py' + cmd = egg_info(dist) + cmd.ensure_finalized() + + cmd.run() + + return set(cmd.filelist.files) + + def test_no_manifest(self): + """Check a missing MANIFEST.in includes only the standard files.""" + assert (default_files - set(['MANIFEST.in'])) == self.get_files() + + def test_empty_files(self): + """Check an empty MANIFEST.in includes only the standard files.""" + self.make_manifest("") + assert default_files == self.get_files() + + def test_include(self): + """Include extra rst files in the project root.""" + self.make_manifest("include *.rst") + files = default_files | set(['testing.rst', '.hidden.rst']) + assert files == self.get_files() + + def test_exclude(self): + """Include everything in app/ except the text files""" + ml = make_local_path + self.make_manifest( + """ + include app/* + exclude app/*.txt + """ + ) + files = default_files | set([ml('app/c.rst')]) + assert files == self.get_files() + + def test_include_multiple(self): + """Include with multiple patterns.""" + ml = make_local_path + self.make_manifest("include app/*.txt app/static/*") + files = default_files | set([ + ml('app/a.txt'), + ml('app/b.txt'), + ml('app/static/app.js'), + ml('app/static/app.js.map'), + ml('app/static/app.css'), + ml('app/static/app.css.map'), + ]) + assert files == self.get_files() + + def test_graft(self): + """Include the whole app/static/ directory.""" + ml = make_local_path + self.make_manifest("graft app/static") + files = default_files | set([ + ml('app/static/app.js'), + ml('app/static/app.js.map'), + ml('app/static/app.css'), + ml('app/static/app.css.map'), + ]) + assert files == self.get_files() + + def test_graft_glob_syntax(self): + """Include the whole app/static/ directory.""" + ml = make_local_path + self.make_manifest("graft */static") + files = default_files | set([ + ml('app/static/app.js'), + ml('app/static/app.js.map'), + ml('app/static/app.css'), + ml('app/static/app.css.map'), + ]) + assert files == self.get_files() + + def test_graft_global_exclude(self): + """Exclude all *.map files in the project.""" + ml = make_local_path + self.make_manifest( + """ + graft app/static + global-exclude *.map + """ + ) + files = default_files | set([ml('app/static/app.js'), ml('app/static/app.css')]) + assert files == self.get_files() + + def test_global_include(self): + """Include all *.rst, *.js, and *.css files in the whole tree.""" + ml = make_local_path + self.make_manifest( + """ + global-include *.rst *.js *.css + """ + ) + files = default_files | set([ + '.hidden.rst', + 'testing.rst', + ml('app/c.rst'), + ml('app/static/app.js'), + ml('app/static/app.css'), + ]) + assert files == self.get_files() + + def test_graft_prune(self): + """Include all files in app/, except for the whole app/static/ dir.""" + ml = make_local_path + self.make_manifest( + """ + graft app + prune app/static + """ + ) + files = default_files | set([ml('app/a.txt'), ml('app/b.txt'), ml('app/c.rst')]) + assert files == self.get_files() + + +class TestFileListTest(TempDirTestCase): + """ + A copy of the relevant bits of distutils/tests/test_filelist.py, + to ensure setuptools' version of FileList keeps parity with distutils. + """ + + @pytest.fixture(autouse=os.getenv("SETUPTOOLS_USE_DISTUTILS") == "stdlib") + def _compat_record_logs(self, monkeypatch, caplog): + """Account for stdlib compatibility""" + + def _log(_logger, level, msg, args): + exc = sys.exc_info() + rec = logging.LogRecord("distutils", level, "", 0, msg, args, exc) + caplog.records.append(rec) + + monkeypatch.setattr(log.Log, "_log", _log) + + def get_records(self, caplog, *levels): + return [r for r in caplog.records if r.levelno in levels] + + def assertNoWarnings(self, caplog): + assert self.get_records(caplog, log.WARN) == [] + caplog.clear() + + def assertWarnings(self, caplog): + if IS_PYPY and not caplog.records: + pytest.xfail("caplog checks may not work well in PyPy") + else: + assert len(self.get_records(caplog, log.WARN)) > 0 + caplog.clear() + + def make_files(self, files): + for file in files: + file = os.path.join(self.temp_dir, file) + dirname, _basename = os.path.split(file) + os.makedirs(dirname, exist_ok=True) + touch(file) + + def test_process_template_line(self): + # testing all MANIFEST.in template patterns + file_list = FileList() + ml = make_local_path + + # simulated file list + self.make_files([ + 'foo.tmp', + 'ok', + 'xo', + 'four.txt', + 'buildout.cfg', + # filelist does not filter out VCS directories, + # it's sdist that does + ml('.hg/last-message.txt'), + ml('global/one.txt'), + ml('global/two.txt'), + ml('global/files.x'), + ml('global/here.tmp'), + ml('f/o/f.oo'), + ml('dir/graft-one'), + ml('dir/dir2/graft2'), + ml('dir3/ok'), + ml('dir3/sub/ok.txt'), + ]) + + MANIFEST_IN = DALS( + """\ + include ok + include xo + exclude xo + include foo.tmp + include buildout.cfg + global-include *.x + global-include *.txt + global-exclude *.tmp + recursive-include f *.oo + recursive-exclude global *.x + graft dir + prune dir3 + """ + ) + + for line in MANIFEST_IN.split('\n'): + if not line: + continue + file_list.process_template_line(line) + + wanted = [ + 'buildout.cfg', + 'four.txt', + 'ok', + ml('.hg/last-message.txt'), + ml('dir/graft-one'), + ml('dir/dir2/graft2'), + ml('f/o/f.oo'), + ml('global/one.txt'), + ml('global/two.txt'), + ] + + file_list.sort() + assert file_list.files == wanted + + def test_exclude_pattern(self): + # return False if no match + file_list = FileList() + assert not file_list.exclude_pattern('*.py') + + # return True if files match + file_list = FileList() + file_list.files = ['a.py', 'b.py'] + assert file_list.exclude_pattern('*.py') + + # test excludes + file_list = FileList() + file_list.files = ['a.py', 'a.txt'] + file_list.exclude_pattern('*.py') + file_list.sort() + assert file_list.files == ['a.txt'] + + def test_include_pattern(self): + # return False if no match + file_list = FileList() + self.make_files([]) + assert not file_list.include_pattern('*.py') + + # return True if files match + file_list = FileList() + self.make_files(['a.py', 'b.txt']) + assert file_list.include_pattern('*.py') + + # test * matches all files + file_list = FileList() + self.make_files(['a.py', 'b.txt']) + file_list.include_pattern('*') + file_list.sort() + assert file_list.files == ['a.py', 'b.txt'] + + def test_process_template_line_invalid(self): + # invalid lines + file_list = FileList() + for action in ( + 'include', + 'exclude', + 'global-include', + 'global-exclude', + 'recursive-include', + 'recursive-exclude', + 'graft', + 'prune', + 'blarg', + ): + with pytest.raises(DistutilsTemplateError): + file_list.process_template_line(action) + + def test_include(self, caplog): + caplog.set_level(logging.DEBUG) + ml = make_local_path + # include + file_list = FileList() + self.make_files(['a.py', 'b.txt', ml('d/c.py')]) + + file_list.process_template_line('include *.py') + file_list.sort() + assert file_list.files == ['a.py'] + self.assertNoWarnings(caplog) + + file_list.process_template_line('include *.rb') + file_list.sort() + assert file_list.files == ['a.py'] + self.assertWarnings(caplog) + + def test_exclude(self, caplog): + caplog.set_level(logging.DEBUG) + ml = make_local_path + # exclude + file_list = FileList() + file_list.files = ['a.py', 'b.txt', ml('d/c.py')] + + file_list.process_template_line('exclude *.py') + file_list.sort() + assert file_list.files == ['b.txt', ml('d/c.py')] + self.assertNoWarnings(caplog) + + file_list.process_template_line('exclude *.rb') + file_list.sort() + assert file_list.files == ['b.txt', ml('d/c.py')] + self.assertWarnings(caplog) + + def test_global_include(self, caplog): + caplog.set_level(logging.DEBUG) + ml = make_local_path + # global-include + file_list = FileList() + self.make_files(['a.py', 'b.txt', ml('d/c.py')]) + + file_list.process_template_line('global-include *.py') + file_list.sort() + assert file_list.files == ['a.py', ml('d/c.py')] + self.assertNoWarnings(caplog) + + file_list.process_template_line('global-include *.rb') + file_list.sort() + assert file_list.files == ['a.py', ml('d/c.py')] + self.assertWarnings(caplog) + + def test_global_exclude(self, caplog): + caplog.set_level(logging.DEBUG) + ml = make_local_path + # global-exclude + file_list = FileList() + file_list.files = ['a.py', 'b.txt', ml('d/c.py')] + + file_list.process_template_line('global-exclude *.py') + file_list.sort() + assert file_list.files == ['b.txt'] + self.assertNoWarnings(caplog) + + file_list.process_template_line('global-exclude *.rb') + file_list.sort() + assert file_list.files == ['b.txt'] + self.assertWarnings(caplog) + + def test_recursive_include(self, caplog): + caplog.set_level(logging.DEBUG) + ml = make_local_path + # recursive-include + file_list = FileList() + self.make_files(['a.py', ml('d/b.py'), ml('d/c.txt'), ml('d/d/e.py')]) + + file_list.process_template_line('recursive-include d *.py') + file_list.sort() + assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')] + self.assertNoWarnings(caplog) + + file_list.process_template_line('recursive-include e *.py') + file_list.sort() + assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')] + self.assertWarnings(caplog) + + def test_recursive_exclude(self, caplog): + caplog.set_level(logging.DEBUG) + ml = make_local_path + # recursive-exclude + file_list = FileList() + file_list.files = ['a.py', ml('d/b.py'), ml('d/c.txt'), ml('d/d/e.py')] + + file_list.process_template_line('recursive-exclude d *.py') + file_list.sort() + assert file_list.files == ['a.py', ml('d/c.txt')] + self.assertNoWarnings(caplog) + + file_list.process_template_line('recursive-exclude e *.py') + file_list.sort() + assert file_list.files == ['a.py', ml('d/c.txt')] + self.assertWarnings(caplog) + + def test_graft(self, caplog): + caplog.set_level(logging.DEBUG) + ml = make_local_path + # graft + file_list = FileList() + self.make_files(['a.py', ml('d/b.py'), ml('d/d/e.py'), ml('f/f.py')]) + + file_list.process_template_line('graft d') + file_list.sort() + assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')] + self.assertNoWarnings(caplog) + + file_list.process_template_line('graft e') + file_list.sort() + assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')] + self.assertWarnings(caplog) + + def test_prune(self, caplog): + caplog.set_level(logging.DEBUG) + ml = make_local_path + # prune + file_list = FileList() + file_list.files = ['a.py', ml('d/b.py'), ml('d/d/e.py'), ml('f/f.py')] + + file_list.process_template_line('prune d') + file_list.sort() + assert file_list.files == ['a.py', ml('f/f.py')] + self.assertNoWarnings(caplog) + + file_list.process_template_line('prune e') + file_list.sort() + assert file_list.files == ['a.py', ml('f/f.py')] + self.assertWarnings(caplog) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_namespaces.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_namespaces.py new file mode 100644 index 0000000000000000000000000000000000000000..a0f4120bf7900b2118cc066034e036ab7af1798b --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_namespaces.py @@ -0,0 +1,138 @@ +import subprocess +import sys + +from setuptools._path import paths_on_pythonpath + +from . import namespaces + + +class TestNamespaces: + def test_mixed_site_and_non_site(self, tmpdir): + """ + Installing two packages sharing the same namespace, one installed + to a site dir and the other installed just to a path on PYTHONPATH + should leave the namespace in tact and both packages reachable by + import. + """ + pkg_A = namespaces.build_namespace_package(tmpdir, 'myns.pkgA') + pkg_B = namespaces.build_namespace_package(tmpdir, 'myns.pkgB') + site_packages = tmpdir / 'site-packages' + path_packages = tmpdir / 'path-packages' + targets = site_packages, path_packages + # use pip to install to the target directory + install_cmd = [ + sys.executable, + '-m', + 'pip.__main__', + 'install', + str(pkg_A), + '-t', + str(site_packages), + ] + subprocess.check_call(install_cmd) + namespaces.make_site_dir(site_packages) + install_cmd = [ + sys.executable, + '-m', + 'pip.__main__', + 'install', + str(pkg_B), + '-t', + str(path_packages), + ] + subprocess.check_call(install_cmd) + try_import = [ + sys.executable, + '-c', + 'import myns.pkgA; import myns.pkgB', + ] + with paths_on_pythonpath(map(str, targets)): + subprocess.check_call(try_import) + + def test_pkg_resources_import(self, tmpdir): + """ + Ensure that a namespace package doesn't break on import + of pkg_resources. + """ + pkg = namespaces.build_namespace_package(tmpdir, 'myns.pkgA') + target = tmpdir / 'packages' + target.mkdir() + install_cmd = [ + sys.executable, + '-m', + 'pip', + 'install', + '-t', + str(target), + str(pkg), + ] + with paths_on_pythonpath([str(target)]): + subprocess.check_call(install_cmd) + namespaces.make_site_dir(target) + try_import = [ + sys.executable, + '-c', + 'import pkg_resources', + ] + with paths_on_pythonpath([str(target)]): + subprocess.check_call(try_import) + + def test_namespace_package_installed_and_cwd(self, tmpdir): + """ + Installing a namespace packages but also having it in the current + working directory, only one version should take precedence. + """ + pkg_A = namespaces.build_namespace_package(tmpdir, 'myns.pkgA') + target = tmpdir / 'packages' + # use pip to install to the target directory + install_cmd = [ + sys.executable, + '-m', + 'pip.__main__', + 'install', + str(pkg_A), + '-t', + str(target), + ] + subprocess.check_call(install_cmd) + namespaces.make_site_dir(target) + + # ensure that package imports and pkg_resources imports + pkg_resources_imp = [ + sys.executable, + '-c', + 'import pkg_resources; import myns.pkgA', + ] + with paths_on_pythonpath([str(target)]): + subprocess.check_call(pkg_resources_imp, cwd=str(pkg_A)) + + def test_packages_in_the_same_namespace_installed_and_cwd(self, tmpdir): + """ + Installing one namespace package and also have another in the same + namespace in the current working directory, both of them must be + importable. + """ + pkg_A = namespaces.build_namespace_package(tmpdir, 'myns.pkgA') + pkg_B = namespaces.build_namespace_package(tmpdir, 'myns.pkgB') + target = tmpdir / 'packages' + # use pip to install to the target directory + install_cmd = [ + sys.executable, + '-m', + 'pip.__main__', + 'install', + str(pkg_A), + '-t', + str(target), + ] + subprocess.check_call(install_cmd) + namespaces.make_site_dir(target) + + # ensure that all packages import and pkg_resources imports + pkg_resources_imp = [ + sys.executable, + '-c', + 'import pkg_resources; import myns.pkgA; import myns.pkgB', + ] + with paths_on_pythonpath([str(target)]): + subprocess.check_call(pkg_resources_imp, cwd=str(pkg_B)) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_packageindex.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_packageindex.py new file mode 100644 index 0000000000000000000000000000000000000000..2a6e5917a8733c71a5d9b29d4bde6fc12cc1e183 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_packageindex.py @@ -0,0 +1,267 @@ +import http.client +import re +import urllib.error +import urllib.request +from inspect import cleandoc + +import pytest + +import setuptools.package_index + +import distutils.errors + + +class TestPackageIndex: + def test_regex(self): + hash_url = 'http://other_url?:action=show_md5&' + hash_url += 'digest=0123456789abcdef0123456789abcdef' + doc = """ + Name + (md5) + """.lstrip().format(**locals()) + assert setuptools.package_index.PYPI_MD5.match(doc) + + def test_bad_url_bad_port(self): + index = setuptools.package_index.PackageIndex() + url = 'http://127.0.0.1:0/nonesuch/test_package_index' + with pytest.raises(Exception, match=re.escape(url)): + v = index.open_url(url) + assert isinstance(v, urllib.error.HTTPError) + + def test_bad_url_typo(self): + # issue 16 + # easy_install inquant.contentmirror.plone breaks because of a typo + # in its home URL + index = setuptools.package_index.PackageIndex(hosts=('www.example.com',)) + + url = 'url:%20https://svn.plone.org/svn/collective/inquant.contentmirror.plone/trunk' + + with pytest.raises(Exception, match=re.escape(url)): + v = index.open_url(url) + assert isinstance(v, urllib.error.HTTPError) + + def test_bad_url_bad_status_line(self): + index = setuptools.package_index.PackageIndex(hosts=('www.example.com',)) + + def _urlopen(*args): + raise http.client.BadStatusLine('line') + + index.opener = _urlopen + url = 'http://example.com' + with pytest.raises(Exception, match=r'line'): + index.open_url(url) + + def test_bad_url_double_scheme(self): + """ + A bad URL with a double scheme should raise a DistutilsError. + """ + index = setuptools.package_index.PackageIndex(hosts=('www.example.com',)) + + # issue 20 + url = 'http://http://svn.pythonpaste.org/Paste/wphp/trunk' + try: + index.open_url(url) + except distutils.errors.DistutilsError as error: + msg = str(error) + assert ( + 'nonnumeric port' in msg + or 'getaddrinfo failed' in msg + or 'Name or service not known' in msg + ) + return + raise RuntimeError("Did not raise") + + def test_url_ok(self): + index = setuptools.package_index.PackageIndex(hosts=('www.example.com',)) + url = 'file:///tmp/test_package_index' + assert index.url_ok(url, True) + + def test_parse_bdist_wininst(self): + parse = setuptools.package_index.parse_bdist_wininst + + actual = parse('reportlab-2.5.win32-py2.4.exe') + expected = 'reportlab-2.5', '2.4', 'win32' + assert actual == expected + + actual = parse('reportlab-2.5.win32.exe') + expected = 'reportlab-2.5', None, 'win32' + assert actual == expected + + actual = parse('reportlab-2.5.win-amd64-py2.7.exe') + expected = 'reportlab-2.5', '2.7', 'win-amd64' + assert actual == expected + + actual = parse('reportlab-2.5.win-amd64.exe') + expected = 'reportlab-2.5', None, 'win-amd64' + assert actual == expected + + def test__vcs_split_rev_from_url(self): + """ + Test the basic usage of _vcs_split_rev_from_url + """ + vsrfu = setuptools.package_index.PackageIndex._vcs_split_rev_from_url + url, rev = vsrfu('https://example.com/bar@2995') + assert url == 'https://example.com/bar' + assert rev == '2995' + + def test_local_index(self, tmpdir): + """ + local_open should be able to read an index from the file system. + """ + index_file = tmpdir / 'index.html' + with index_file.open('w') as f: + f.write('
content
') + url = 'file:' + urllib.request.pathname2url(str(tmpdir)) + '/' + res = setuptools.package_index.local_open(url) + assert 'content' in res.read() + + def test_egg_fragment(self): + """ + EGG fragments must comply to PEP 440 + """ + epoch = [ + '', + '1!', + ] + releases = [ + '0', + '0.0', + '0.0.0', + ] + pre = [ + 'a0', + 'b0', + 'rc0', + ] + post = ['.post0'] + dev = [ + '.dev0', + ] + local = [ + ('', ''), + ('+ubuntu.0', '+ubuntu.0'), + ('+ubuntu-0', '+ubuntu.0'), + ('+ubuntu_0', '+ubuntu.0'), + ] + versions = [ + [''.join([e, r, p, loc]) for loc in locs] + for e in epoch + for r in releases + for p in sum([pre, post, dev], ['']) + for locs in local + ] + for v, vc in versions: + dists = list( + setuptools.package_index.distros_for_url( + 'http://example.com/example-foo.zip#egg=example-foo-' + v + ) + ) + assert dists[0].version == '' + assert dists[1].version == vc + + def test_download_git_with_rev(self, tmp_path, fp): + url = 'git+https://github.example/group/project@master#egg=foo' + index = setuptools.package_index.PackageIndex() + + expected_dir = tmp_path / 'project@master' + fp.register([ + 'git', + 'clone', + '--quiet', + 'https://github.example/group/project', + expected_dir, + ]) + fp.register(['git', '-C', expected_dir, 'checkout', '--quiet', 'master']) + + result = index.download(url, tmp_path) + + assert result == str(expected_dir) + assert len(fp.calls) == 2 + + def test_download_git_no_rev(self, tmp_path, fp): + url = 'git+https://github.example/group/project#egg=foo' + index = setuptools.package_index.PackageIndex() + + expected_dir = tmp_path / 'project' + fp.register([ + 'git', + 'clone', + '--quiet', + 'https://github.example/group/project', + expected_dir, + ]) + index.download(url, tmp_path) + + def test_download_svn(self, tmp_path): + url = 'svn+https://svn.example/project#egg=foo' + index = setuptools.package_index.PackageIndex() + + msg = r".*SVN download is not supported.*" + with pytest.raises(distutils.errors.DistutilsError, match=msg): + index.download(url, tmp_path) + + +class TestContentCheckers: + def test_md5(self): + checker = setuptools.package_index.HashChecker.from_url( + 'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478' + ) + checker.feed('You should probably not be using MD5'.encode('ascii')) + assert checker.hash.hexdigest() == 'f12895fdffbd45007040d2e44df98478' + assert checker.is_valid() + + def test_other_fragment(self): + "Content checks should succeed silently if no hash is present" + checker = setuptools.package_index.HashChecker.from_url( + 'http://foo/bar#something%20completely%20different' + ) + checker.feed('anything'.encode('ascii')) + assert checker.is_valid() + + def test_blank_md5(self): + "Content checks should succeed if a hash is empty" + checker = setuptools.package_index.HashChecker.from_url('http://foo/bar#md5=') + checker.feed('anything'.encode('ascii')) + assert checker.is_valid() + + def test_get_hash_name_md5(self): + checker = setuptools.package_index.HashChecker.from_url( + 'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478' + ) + assert checker.hash_name == 'md5' + + def test_report(self): + checker = setuptools.package_index.HashChecker.from_url( + 'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478' + ) + rep = checker.report(lambda x: x, 'My message about %s') + assert rep == 'My message about md5' + + +class TestPyPIConfig: + def test_percent_in_password(self, tmp_home_dir): + pypirc = tmp_home_dir / '.pypirc' + pypirc.write_text( + cleandoc( + """ + [pypi] + repository=https://pypi.org + username=jaraco + password=pity% + """ + ), + encoding="utf-8", + ) + cfg = setuptools.package_index.PyPIConfig() + cred = cfg.creds_by_repository['https://pypi.org'] + assert cred.username == 'jaraco' + assert cred.password == 'pity%' + + +@pytest.mark.timeout(1) +def test_REL_DoS(): + """ + REL should not hang on a contrived attack string. + """ + setuptools.package_index.REL.search('< rel=' + ' ' * 2**12) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_sandbox.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_sandbox.py new file mode 100644 index 0000000000000000000000000000000000000000..a476b7c93d7441834f415223cb386859581724f8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_sandbox.py @@ -0,0 +1,134 @@ +"""develop tests""" + +import os +import types + +import pytest + +import pkg_resources +import setuptools.sandbox + + +class TestSandbox: + def test_devnull(self, tmpdir): + with setuptools.sandbox.DirectorySandbox(str(tmpdir)): + self._file_writer(os.devnull) + + @staticmethod + def _file_writer(path): + def do_write(): + with open(path, 'w', encoding="utf-8") as f: + f.write('xxx') + + return do_write + + def test_setup_py_with_BOM(self): + """ + It should be possible to execute a setup.py with a Byte Order Mark + """ + target = pkg_resources.resource_filename(__name__, 'script-with-bom.py') + namespace = types.ModuleType('namespace') + setuptools.sandbox._execfile(target, vars(namespace)) + assert namespace.result == 'passed' + + def test_setup_py_with_CRLF(self, tmpdir): + setup_py = tmpdir / 'setup.py' + with setup_py.open('wb') as stream: + stream.write(b'"degenerate script"\r\n') + setuptools.sandbox._execfile(str(setup_py), globals()) + + +class TestExceptionSaver: + def test_exception_trapped(self): + with setuptools.sandbox.ExceptionSaver(): + raise ValueError("details") + + def test_exception_resumed(self): + with setuptools.sandbox.ExceptionSaver() as saved_exc: + raise ValueError("details") + + with pytest.raises(ValueError) as caught: + saved_exc.resume() + + assert isinstance(caught.value, ValueError) + assert str(caught.value) == 'details' + + def test_exception_reconstructed(self): + orig_exc = ValueError("details") + + with setuptools.sandbox.ExceptionSaver() as saved_exc: + raise orig_exc + + with pytest.raises(ValueError) as caught: + saved_exc.resume() + + assert isinstance(caught.value, ValueError) + assert caught.value is not orig_exc + + def test_no_exception_passes_quietly(self): + with setuptools.sandbox.ExceptionSaver() as saved_exc: + pass + + saved_exc.resume() + + def test_unpickleable_exception(self): + class CantPickleThis(Exception): + "This Exception is unpickleable because it's not in globals" + + def __repr__(self) -> str: + return f'CantPickleThis{self.args!r}' + + with setuptools.sandbox.ExceptionSaver() as saved_exc: + raise CantPickleThis('detail') + + with pytest.raises(setuptools.sandbox.UnpickleableException) as caught: + saved_exc.resume() + + assert str(caught.value) == "CantPickleThis('detail',)" + + def test_unpickleable_exception_when_hiding_setuptools(self): + """ + As revealed in #440, an infinite recursion can occur if an unpickleable + exception while setuptools is hidden. Ensure this doesn't happen. + """ + + class ExceptionUnderTest(Exception): + """ + An unpickleable exception (not in globals). + """ + + with pytest.raises(setuptools.sandbox.UnpickleableException) as caught: + with setuptools.sandbox.save_modules(): + setuptools.sandbox.hide_setuptools() + raise ExceptionUnderTest + + (msg,) = caught.value.args + assert msg == 'ExceptionUnderTest()' + + def test_sandbox_violation_raised_hiding_setuptools(self, tmpdir): + """ + When in a sandbox with setuptools hidden, a SandboxViolation + should reflect a proper exception and not be wrapped in + an UnpickleableException. + """ + + def write_file(): + "Trigger a SandboxViolation by writing outside the sandbox" + with open('/etc/foo', 'w', encoding="utf-8"): + pass + + with pytest.raises(setuptools.sandbox.SandboxViolation) as caught: + with setuptools.sandbox.save_modules(): + setuptools.sandbox.hide_setuptools() + with setuptools.sandbox.DirectorySandbox(str(tmpdir)): + write_file() + + cmd, args, kwargs = caught.value.args + assert cmd == 'open' + assert args == ('/etc/foo', 'w') + assert kwargs == {"encoding": "utf-8"} + + msg = str(caught.value) + assert 'open' in msg + assert "('/etc/foo', 'w')" in msg + assert "{'encoding': 'utf-8'}" in msg diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_sdist.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_sdist.py new file mode 100644 index 0000000000000000000000000000000000000000..19d8ddf6dafb0cacc044a6021f283d3e9faaf800 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_sdist.py @@ -0,0 +1,984 @@ +"""sdist tests""" + +import contextlib +import io +import logging +import os +import pathlib +import sys +import tarfile +import tempfile +import unicodedata +from inspect import cleandoc +from pathlib import Path +from unittest import mock + +import jaraco.path +import pytest + +from setuptools import Command, SetuptoolsDeprecationWarning +from setuptools._importlib import metadata +from setuptools.command.egg_info import manifest_maker +from setuptools.command.sdist import sdist +from setuptools.dist import Distribution +from setuptools.extension import Extension +from setuptools.tests import fail_on_ascii + +from .text import Filenames + +import distutils +from distutils.core import run_setup + +SETUP_ATTRS = { + 'name': 'sdist_test', + 'version': '0.0', + 'packages': ['sdist_test'], + 'package_data': {'sdist_test': ['*.txt']}, + 'data_files': [("data", [os.path.join("d", "e.dat")])], +} + +SETUP_PY = f"""\ +from setuptools import setup + +setup(**{SETUP_ATTRS!r}) +""" + +EXTENSION = Extension( + name="sdist_test.f", + sources=[os.path.join("sdist_test", "f.c")], + depends=[os.path.join("sdist_test", "f.h")], +) +EXTENSION_SOURCES = EXTENSION.sources + EXTENSION.depends + + +@contextlib.contextmanager +def quiet(): + old_stdout, old_stderr = sys.stdout, sys.stderr + sys.stdout, sys.stderr = io.StringIO(), io.StringIO() + try: + yield + finally: + sys.stdout, sys.stderr = old_stdout, old_stderr + + +# Convert to POSIX path +def posix(path): + if not isinstance(path, str): + return path.replace(os.sep.encode('ascii'), b'/') + else: + return path.replace(os.sep, '/') + + +# HFS Plus uses decomposed UTF-8 +def decompose(path): + if isinstance(path, str): + return unicodedata.normalize('NFD', path) + try: + path = path.decode('utf-8') + path = unicodedata.normalize('NFD', path) + path = path.encode('utf-8') + except UnicodeError: + pass # Not UTF-8 + return path + + +def read_all_bytes(filename): + with open(filename, 'rb') as fp: + return fp.read() + + +def latin1_fail(): + try: + desc, filename = tempfile.mkstemp(suffix=Filenames.latin_1) + os.close(desc) + os.remove(filename) + except Exception: + return True + + +fail_on_latin1_encoded_filenames = pytest.mark.xfail( + latin1_fail(), + reason="System does not support latin-1 filenames", +) + + +skip_under_xdist = pytest.mark.skipif( + "os.environ.get('PYTEST_XDIST_WORKER')", + reason="pytest-dev/pytest-xdist#843", +) +skip_under_stdlib_distutils = pytest.mark.skipif( + not distutils.__package__.startswith('setuptools'), + reason="the test is not supported with stdlib distutils", +) + + +def touch(path): + open(path, 'wb').close() + return path + + +def symlink_or_skip_test(src, dst): + try: + os.symlink(src, dst) + except (OSError, NotImplementedError): + pytest.skip("symlink not supported in OS") + return None + return dst + + +class TestSdistTest: + @pytest.fixture(autouse=True) + def source_dir(self, tmpdir): + tmpdir = tmpdir / "project_root" + tmpdir.mkdir() + + (tmpdir / 'setup.py').write_text(SETUP_PY, encoding='utf-8') + + # Set up the rest of the test package + test_pkg = tmpdir / 'sdist_test' + test_pkg.mkdir() + data_folder = tmpdir / 'd' + data_folder.mkdir() + # *.rst was not included in package_data, so c.rst should not be + # automatically added to the manifest when not under version control + for fname in ['__init__.py', 'a.txt', 'b.txt', 'c.rst']: + touch(test_pkg / fname) + touch(data_folder / 'e.dat') + # C sources are not included by default, but they will be, + # if an extension module uses them as sources or depends + for fname in EXTENSION_SOURCES: + touch(tmpdir / fname) + + with tmpdir.as_cwd(): + yield tmpdir + + def assert_package_data_in_manifest(self, cmd): + manifest = cmd.filelist.files + assert os.path.join('sdist_test', 'a.txt') in manifest + assert os.path.join('sdist_test', 'b.txt') in manifest + assert os.path.join('sdist_test', 'c.rst') not in manifest + assert os.path.join('d', 'e.dat') in manifest + + def setup_with_extension(self): + setup_attrs = {**SETUP_ATTRS, 'ext_modules': [EXTENSION]} + + dist = Distribution(setup_attrs) + dist.script_name = 'setup.py' + cmd = sdist(dist) + cmd.ensure_finalized() + + with quiet(): + cmd.run() + + return cmd + + def test_package_data_in_sdist(self): + """Regression test for pull request #4: ensures that files listed in + package_data are included in the manifest even if they're not added to + version control. + """ + + dist = Distribution(SETUP_ATTRS) + dist.script_name = 'setup.py' + cmd = sdist(dist) + cmd.ensure_finalized() + + with quiet(): + cmd.run() + + self.assert_package_data_in_manifest(cmd) + + def test_package_data_and_include_package_data_in_sdist(self): + """ + Ensure package_data and include_package_data work + together. + """ + setup_attrs = {**SETUP_ATTRS, 'include_package_data': True} + assert setup_attrs['package_data'] + + dist = Distribution(setup_attrs) + dist.script_name = 'setup.py' + cmd = sdist(dist) + cmd.ensure_finalized() + + with quiet(): + cmd.run() + + self.assert_package_data_in_manifest(cmd) + + def test_extension_sources_in_sdist(self): + """ + Ensure that the files listed in Extension.sources and Extension.depends + are automatically included in the manifest. + """ + cmd = self.setup_with_extension() + self.assert_package_data_in_manifest(cmd) + manifest = cmd.filelist.files + for path in EXTENSION_SOURCES: + assert path in manifest + + def test_missing_extension_sources(self): + """ + Similar to test_extension_sources_in_sdist but the referenced files don't exist. + Missing files should not be included in distribution (with no error raised). + """ + for path in EXTENSION_SOURCES: + os.remove(path) + + cmd = self.setup_with_extension() + self.assert_package_data_in_manifest(cmd) + manifest = cmd.filelist.files + for path in EXTENSION_SOURCES: + assert path not in manifest + + def test_symlinked_extension_sources(self): + """ + Similar to test_extension_sources_in_sdist but the referenced files are + instead symbolic links to project-local files. Referenced file paths + should be included. Symlink targets themselves should NOT be included. + """ + symlinked = [] + for path in EXTENSION_SOURCES: + base, ext = os.path.splitext(path) + target = base + "_target." + ext + + os.rename(path, target) + symlink_or_skip_test(os.path.basename(target), path) + symlinked.append(target) + + cmd = self.setup_with_extension() + self.assert_package_data_in_manifest(cmd) + manifest = cmd.filelist.files + for path in EXTENSION_SOURCES: + assert path in manifest + for path in symlinked: + assert path not in manifest + + _INVALID_PATHS = { + "must be relative": lambda: ( + os.path.abspath(os.path.join("sdist_test", "f.h")) + ), + "can't have `..` segments": lambda: ( + os.path.join("sdist_test", "..", "sdist_test", "f.h") + ), + "doesn't exist": lambda: ( + os.path.join("sdist_test", "this_file_does_not_exist.h") + ), + "must be inside the project root": lambda: ( + symlink_or_skip_test( + touch(os.path.join("..", "outside_of_project_root.h")), + "symlink.h", + ) + ), + } + + @skip_under_stdlib_distutils + @pytest.mark.parametrize("reason", _INVALID_PATHS.keys()) + def test_invalid_extension_depends(self, reason, caplog): + """ + Due to backwards compatibility reasons, `Extension.depends` should accept + invalid/weird paths, but then ignore them when building a sdist. + + This test verifies that the source distribution is still built + successfully with such paths, but that instead of adding these paths to + the manifest, we emit an informational message, notifying the user that + the invalid path won't be automatically included. + """ + invalid_path = self._INVALID_PATHS[reason]() + extension = Extension( + name="sdist_test.f", + sources=[], + depends=[invalid_path], + ) + setup_attrs = {**SETUP_ATTRS, 'ext_modules': [extension]} + + dist = Distribution(setup_attrs) + dist.script_name = 'setup.py' + cmd = sdist(dist) + cmd.ensure_finalized() + + with quiet(), caplog.at_level(logging.INFO): + cmd.run() + + self.assert_package_data_in_manifest(cmd) + manifest = cmd.filelist.files + assert invalid_path not in manifest + + expected_message = [ + message + for (logger, level, message) in caplog.record_tuples + if ( + logger == "root" # + and level == logging.INFO # + and invalid_path in message # + ) + ] + assert len(expected_message) == 1 + (expected_message,) = expected_message + assert reason in expected_message + + def test_custom_build_py(self): + """ + Ensure projects defining custom build_py don't break + when creating sdists (issue #2849) + """ + from distutils.command.build_py import build_py as OrigBuildPy + + using_custom_command_guard = mock.Mock() + + class CustomBuildPy(OrigBuildPy): + """ + Some projects have custom commands inheriting from `distutils` + """ + + def get_data_files(self): + using_custom_command_guard() + return super().get_data_files() + + setup_attrs = {**SETUP_ATTRS, 'include_package_data': True} + assert setup_attrs['package_data'] + + dist = Distribution(setup_attrs) + dist.script_name = 'setup.py' + cmd = sdist(dist) + cmd.ensure_finalized() + + # Make sure we use the custom command + cmd.cmdclass = {'build_py': CustomBuildPy} + cmd.distribution.cmdclass = {'build_py': CustomBuildPy} + assert cmd.distribution.get_command_class('build_py') == CustomBuildPy + + msg = "setuptools instead of distutils" + with quiet(), pytest.warns(SetuptoolsDeprecationWarning, match=msg): + cmd.run() + + using_custom_command_guard.assert_called() + self.assert_package_data_in_manifest(cmd) + + def test_setup_py_exists(self): + dist = Distribution(SETUP_ATTRS) + dist.script_name = 'foo.py' + cmd = sdist(dist) + cmd.ensure_finalized() + + with quiet(): + cmd.run() + + manifest = cmd.filelist.files + assert 'setup.py' in manifest + + def test_setup_py_missing(self): + dist = Distribution(SETUP_ATTRS) + dist.script_name = 'foo.py' + cmd = sdist(dist) + cmd.ensure_finalized() + + if os.path.exists("setup.py"): + os.remove("setup.py") + with quiet(): + cmd.run() + + manifest = cmd.filelist.files + assert 'setup.py' not in manifest + + def test_setup_py_excluded(self): + with open("MANIFEST.in", "w", encoding="utf-8") as manifest_file: + manifest_file.write("exclude setup.py") + + dist = Distribution(SETUP_ATTRS) + dist.script_name = 'foo.py' + cmd = sdist(dist) + cmd.ensure_finalized() + + with quiet(): + cmd.run() + + manifest = cmd.filelist.files + assert 'setup.py' not in manifest + + def test_defaults_case_sensitivity(self, source_dir): + """ + Make sure default files (README.*, etc.) are added in a case-sensitive + way to avoid problems with packages built on Windows. + """ + + touch(source_dir / 'readme.rst') + touch(source_dir / 'SETUP.cfg') + + dist = Distribution(SETUP_ATTRS) + # the extension deliberately capitalized for this test + # to make sure the actual filename (not capitalized) gets added + # to the manifest + dist.script_name = 'setup.PY' + cmd = sdist(dist) + cmd.ensure_finalized() + + with quiet(): + cmd.run() + + # lowercase all names so we can test in a + # case-insensitive way to make sure the files + # are not included. + manifest = map(lambda x: x.lower(), cmd.filelist.files) + assert 'readme.rst' not in manifest, manifest + assert 'setup.py' not in manifest, manifest + assert 'setup.cfg' not in manifest, manifest + + def test_exclude_dev_only_cache_folders(self, source_dir): + included = { + # Emulate problem in https://github.com/pypa/setuptools/issues/4601 + "MANIFEST.in": ( + "global-include LICEN[CS]E* COPYING* NOTICE* AUTHORS*\n" + "global-include *.txt\n" + ), + # For the sake of being conservative and limiting unforeseen side-effects + # we just exclude dev-only cache folders at the root of the repository: + "test/.venv/lib/python3.9/site-packages/bar-2.dist-info/AUTHORS.rst": "", + "src/.nox/py/lib/python3.12/site-packages/bar-2.dist-info/COPYING.txt": "", + "doc/.tox/default/lib/python3.11/site-packages/foo-4.dist-info/LICENSE": "", + # Let's test against false positives with similarly named files: + ".venv-requirements.txt": "", + ".tox-coveragerc.txt": "", + ".noxy/coveragerc.txt": "", + } + + excluded = { + # .tox/.nox/.venv are well-know folders present at the root of Python repos + # and therefore should be excluded + ".tox/release/lib/python3.11/site-packages/foo-4.dist-info/LICENSE": "", + ".nox/py/lib/python3.12/site-packages/bar-2.dist-info/COPYING.txt": "", + ".venv/lib/python3.9/site-packages/bar-2.dist-info/AUTHORS.rst": "", + } + + for file, content in {**excluded, **included}.items(): + Path(source_dir, file).parent.mkdir(parents=True, exist_ok=True) + Path(source_dir, file).write_text(content, encoding="utf-8") + + cmd = self.setup_with_extension() + self.assert_package_data_in_manifest(cmd) + manifest = {f.replace(os.sep, '/') for f in cmd.filelist.files} + for path in excluded: + assert os.path.exists(path) + assert path not in manifest, (path, manifest) + for path in included: + assert os.path.exists(path) + assert path in manifest, (path, manifest) + + @fail_on_ascii + def test_manifest_is_written_with_utf8_encoding(self): + # Test for #303. + dist = Distribution(SETUP_ATTRS) + dist.script_name = 'setup.py' + mm = manifest_maker(dist) + mm.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt') + os.mkdir('sdist_test.egg-info') + + # UTF-8 filename + filename = os.path.join('sdist_test', 'smörbröd.py') + + # Must create the file or it will get stripped. + touch(filename) + + # Add UTF-8 filename and write manifest + with quiet(): + mm.run() + mm.filelist.append(filename) + mm.write_manifest() + + contents = read_all_bytes(mm.manifest) + + # The manifest should be UTF-8 encoded + u_contents = contents.decode('UTF-8') + + # The manifest should contain the UTF-8 filename + assert posix(filename) in u_contents + + @fail_on_ascii + def test_write_manifest_allows_utf8_filenames(self): + # Test for #303. + dist = Distribution(SETUP_ATTRS) + dist.script_name = 'setup.py' + mm = manifest_maker(dist) + mm.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt') + os.mkdir('sdist_test.egg-info') + + filename = os.path.join(b'sdist_test', Filenames.utf_8) + + # Must touch the file or risk removal + touch(filename) + + # Add filename and write manifest + with quiet(): + mm.run() + u_filename = filename.decode('utf-8') + mm.filelist.files.append(u_filename) + # Re-write manifest + mm.write_manifest() + + contents = read_all_bytes(mm.manifest) + + # The manifest should be UTF-8 encoded + contents.decode('UTF-8') + + # The manifest should contain the UTF-8 filename + assert posix(filename) in contents + + # The filelist should have been updated as well + assert u_filename in mm.filelist.files + + @skip_under_xdist + def test_write_manifest_skips_non_utf8_filenames(self): + """ + Files that cannot be encoded to UTF-8 (specifically, those that + weren't originally successfully decoded and have surrogate + escapes) should be omitted from the manifest. + See https://bitbucket.org/tarek/distribute/issue/303 for history. + """ + dist = Distribution(SETUP_ATTRS) + dist.script_name = 'setup.py' + mm = manifest_maker(dist) + mm.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt') + os.mkdir('sdist_test.egg-info') + + # Latin-1 filename + filename = os.path.join(b'sdist_test', Filenames.latin_1) + + # Add filename with surrogates and write manifest + with quiet(): + mm.run() + u_filename = filename.decode('utf-8', 'surrogateescape') + mm.filelist.append(u_filename) + # Re-write manifest + mm.write_manifest() + + contents = read_all_bytes(mm.manifest) + + # The manifest should be UTF-8 encoded + contents.decode('UTF-8') + + # The Latin-1 filename should have been skipped + assert posix(filename) not in contents + + # The filelist should have been updated as well + assert u_filename not in mm.filelist.files + + @fail_on_ascii + def test_manifest_is_read_with_utf8_encoding(self): + # Test for #303. + dist = Distribution(SETUP_ATTRS) + dist.script_name = 'setup.py' + cmd = sdist(dist) + cmd.ensure_finalized() + + # Create manifest + with quiet(): + cmd.run() + + # Add UTF-8 filename to manifest + filename = os.path.join(b'sdist_test', Filenames.utf_8) + cmd.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt') + manifest = open(cmd.manifest, 'ab') + manifest.write(b'\n' + filename) + manifest.close() + + # The file must exist to be included in the filelist + touch(filename) + + # Re-read manifest + cmd.filelist.files = [] + with quiet(): + cmd.read_manifest() + + # The filelist should contain the UTF-8 filename + filename = filename.decode('utf-8') + assert filename in cmd.filelist.files + + @fail_on_latin1_encoded_filenames + def test_read_manifest_skips_non_utf8_filenames(self): + # Test for #303. + dist = Distribution(SETUP_ATTRS) + dist.script_name = 'setup.py' + cmd = sdist(dist) + cmd.ensure_finalized() + + # Create manifest + with quiet(): + cmd.run() + + # Add Latin-1 filename to manifest + filename = os.path.join(b'sdist_test', Filenames.latin_1) + cmd.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt') + manifest = open(cmd.manifest, 'ab') + manifest.write(b'\n' + filename) + manifest.close() + + # The file must exist to be included in the filelist + touch(filename) + + # Re-read manifest + cmd.filelist.files = [] + with quiet(): + cmd.read_manifest() + + # The Latin-1 filename should have been skipped + filename = filename.decode('latin-1') + assert filename not in cmd.filelist.files + + @fail_on_ascii + @fail_on_latin1_encoded_filenames + def test_sdist_with_utf8_encoded_filename(self): + # Test for #303. + dist = Distribution(self.make_strings(SETUP_ATTRS)) + dist.script_name = 'setup.py' + cmd = sdist(dist) + cmd.ensure_finalized() + + filename = os.path.join(b'sdist_test', Filenames.utf_8) + touch(filename) + + with quiet(): + cmd.run() + + if sys.platform == 'darwin': + filename = decompose(filename) + + fs_enc = sys.getfilesystemencoding() + + if sys.platform == 'win32': + if fs_enc == 'cp1252': + # Python mangles the UTF-8 filename + filename = filename.decode('cp1252') + assert filename in cmd.filelist.files + else: + filename = filename.decode('mbcs') + assert filename in cmd.filelist.files + else: + filename = filename.decode('utf-8') + assert filename in cmd.filelist.files + + @classmethod + def make_strings(cls, item): + if isinstance(item, dict): + return {key: cls.make_strings(value) for key, value in item.items()} + if isinstance(item, list): + return list(map(cls.make_strings, item)) + return str(item) + + @fail_on_latin1_encoded_filenames + @skip_under_xdist + def test_sdist_with_latin1_encoded_filename(self): + # Test for #303. + dist = Distribution(self.make_strings(SETUP_ATTRS)) + dist.script_name = 'setup.py' + cmd = sdist(dist) + cmd.ensure_finalized() + + # Latin-1 filename + filename = os.path.join(b'sdist_test', Filenames.latin_1) + touch(filename) + assert os.path.isfile(filename) + + with quiet(): + cmd.run() + + # not all windows systems have a default FS encoding of cp1252 + if sys.platform == 'win32': + # Latin-1 is similar to Windows-1252 however + # on mbcs filesys it is not in latin-1 encoding + fs_enc = sys.getfilesystemencoding() + if fs_enc != 'mbcs': + fs_enc = 'latin-1' + filename = filename.decode(fs_enc) + + assert filename in cmd.filelist.files + else: + # The Latin-1 filename should have been skipped + filename = filename.decode('latin-1') + assert filename not in cmd.filelist.files + + _EXAMPLE_DIRECTIVES = { + "setup.cfg - long_description and version": """ + [metadata] + name = testing + version = file: src/VERSION.txt + license_files = DOWHATYOUWANT + long_description = file: README.rst, USAGE.rst + """, + "pyproject.toml - static readme/license files and dynamic version": """ + [project] + name = "testing" + readme = "USAGE.rst" + license-files = ["DOWHATYOUWANT"] + dynamic = ["version"] + [tool.setuptools.dynamic] + version = {file = ["src/VERSION.txt"]} + """, + "pyproject.toml - directive with str instead of list": """ + [project] + name = "testing" + readme = "USAGE.rst" + license-files = ["DOWHATYOUWANT"] + dynamic = ["version"] + [tool.setuptools.dynamic] + version = {file = "src/VERSION.txt"} + """, + "pyproject.toml - deprecated license table with file entry": """ + [project] + name = "testing" + readme = "USAGE.rst" + license = {file = "DOWHATYOUWANT"} + dynamic = ["version"] + [tool.setuptools.dynamic] + version = {file = "src/VERSION.txt"} + """, + } + + @pytest.mark.parametrize("config", _EXAMPLE_DIRECTIVES.keys()) + @pytest.mark.filterwarnings( + "ignore:.project.license. as a TOML table is deprecated" + ) + def test_add_files_referenced_by_config_directives(self, source_dir, config): + config_file, _, _ = config.partition(" - ") + config_text = self._EXAMPLE_DIRECTIVES[config] + (source_dir / 'src').mkdir() + (source_dir / 'src/VERSION.txt').write_text("0.42", encoding="utf-8") + (source_dir / 'README.rst').write_text("hello world!", encoding="utf-8") + (source_dir / 'USAGE.rst').write_text("hello world!", encoding="utf-8") + (source_dir / 'DOWHATYOUWANT').write_text("hello world!", encoding="utf-8") + (source_dir / config_file).write_text(config_text, encoding="utf-8") + + dist = Distribution({"packages": []}) + dist.script_name = 'setup.py' + dist.parse_config_files() + + cmd = sdist(dist) + cmd.ensure_finalized() + with quiet(): + cmd.run() + + assert ( + 'src/VERSION.txt' in cmd.filelist.files + or 'src\\VERSION.txt' in cmd.filelist.files + ) + assert 'USAGE.rst' in cmd.filelist.files + assert 'DOWHATYOUWANT' in cmd.filelist.files + assert '/' not in cmd.filelist.files + assert '\\' not in cmd.filelist.files + + def test_pyproject_toml_in_sdist(self, source_dir): + """ + Check if pyproject.toml is included in source distribution if present + """ + touch(source_dir / 'pyproject.toml') + dist = Distribution(SETUP_ATTRS) + dist.script_name = 'setup.py' + cmd = sdist(dist) + cmd.ensure_finalized() + with quiet(): + cmd.run() + manifest = cmd.filelist.files + assert 'pyproject.toml' in manifest + + def test_pyproject_toml_excluded(self, source_dir): + """ + Check that pyproject.toml can excluded even if present + """ + touch(source_dir / 'pyproject.toml') + with open('MANIFEST.in', 'w', encoding="utf-8") as mts: + print('exclude pyproject.toml', file=mts) + dist = Distribution(SETUP_ATTRS) + dist.script_name = 'setup.py' + cmd = sdist(dist) + cmd.ensure_finalized() + with quiet(): + cmd.run() + manifest = cmd.filelist.files + assert 'pyproject.toml' not in manifest + + def test_build_subcommand_source_files(self, source_dir): + touch(source_dir / '.myfile~') + + # Sanity check: without custom commands file list should not be affected + dist = Distribution({**SETUP_ATTRS, "script_name": "setup.py"}) + cmd = sdist(dist) + cmd.ensure_finalized() + with quiet(): + cmd.run() + manifest = cmd.filelist.files + assert '.myfile~' not in manifest + + # Test: custom command should be able to augment file list + dist = Distribution({**SETUP_ATTRS, "script_name": "setup.py"}) + build = dist.get_command_obj("build") + build.sub_commands = [*build.sub_commands, ("build_custom", None)] + + class build_custom(Command): + def initialize_options(self): ... + + def finalize_options(self): ... + + def run(self): ... + + def get_source_files(self): + return ['.myfile~'] + + dist.cmdclass.update(build_custom=build_custom) + + cmd = sdist(dist) + cmd.use_defaults = True + cmd.ensure_finalized() + with quiet(): + cmd.run() + manifest = cmd.filelist.files + assert '.myfile~' in manifest + + @pytest.mark.skipif("os.environ.get('SETUPTOOLS_USE_DISTUTILS') == 'stdlib'") + def test_build_base_pathlib(self, source_dir): + """ + Ensure if build_base is a pathlib.Path, the build still succeeds. + """ + dist = Distribution({ + **SETUP_ATTRS, + "script_name": "setup.py", + "options": {"build": {"build_base": pathlib.Path('build')}}, + }) + cmd = sdist(dist) + cmd.ensure_finalized() + with quiet(): + cmd.run() + + +def test_default_revctrl(): + """ + When _default_revctrl was removed from the `setuptools.command.sdist` + module in 10.0, it broke some systems which keep an old install of + setuptools (Distribute) around. Those old versions require that the + setuptools package continue to implement that interface, so this + function provides that interface, stubbed. See #320 for details. + + This interface must be maintained until Ubuntu 12.04 is no longer + supported (by Setuptools). + """ + (ep,) = metadata.EntryPoints._from_text( + """ + [setuptools.file_finders] + svn_cvs = setuptools.command.sdist:_default_revctrl + """ + ) + res = ep.load() + assert hasattr(res, '__iter__') + + +class TestRegressions: + """ + Can be removed/changed if the project decides to change how it handles symlinks + or external files. + """ + + @staticmethod + def files_for_symlink_in_extension_depends(tmp_path, dep_path): + return { + "external": { + "dir": {"file.h": ""}, + }, + "project": { + "setup.py": cleandoc( + f""" + from setuptools import Extension, setup + setup( + name="myproj", + version="42", + ext_modules=[ + Extension( + "hello", sources=["hello.pyx"], + depends=[{dep_path!r}] + ) + ], + ) + """ + ), + "hello.pyx": "", + "MANIFEST.in": "global-include *.h", + }, + } + + @pytest.mark.parametrize( + "dep_path", ("myheaders/dir/file.h", "myheaders/dir/../dir/file.h") + ) + def test_symlink_in_extension_depends(self, monkeypatch, tmp_path, dep_path): + # Given a project with a symlinked dir and a "depends" targeting that dir + files = self.files_for_symlink_in_extension_depends(tmp_path, dep_path) + jaraco.path.build(files, prefix=str(tmp_path)) + symlink_or_skip_test(tmp_path / "external", tmp_path / "project/myheaders") + + # When `sdist` runs, there should be no error + members = run_sdist(monkeypatch, tmp_path / "project") + # and the sdist should contain the symlinked files + for expected in ( + "myproj-42/hello.pyx", + "myproj-42/myheaders/dir/file.h", + ): + assert expected in members + + @staticmethod + def files_for_external_path_in_extension_depends(tmp_path, dep_path): + head, _, tail = dep_path.partition("$tmp_path$/") + dep_path = tmp_path / tail if tail else head + + return { + "external": { + "dir": {"file.h": ""}, + }, + "project": { + "setup.py": cleandoc( + f""" + from setuptools import Extension, setup + setup( + name="myproj", + version="42", + ext_modules=[ + Extension( + "hello", sources=["hello.pyx"], + depends=[{str(dep_path)!r}] + ) + ], + ) + """ + ), + "hello.pyx": "", + "MANIFEST.in": "global-include *.h", + }, + } + + @pytest.mark.parametrize( + "dep_path", ("$tmp_path$/external/dir/file.h", "../external/dir/file.h") + ) + def test_external_path_in_extension_depends(self, monkeypatch, tmp_path, dep_path): + # Given a project with a "depends" targeting an external dir + files = self.files_for_external_path_in_extension_depends(tmp_path, dep_path) + jaraco.path.build(files, prefix=str(tmp_path)) + # When `sdist` runs, there should be no error + members = run_sdist(monkeypatch, tmp_path / "project") + # and the sdist should not contain the external file + for name in members: + assert "file.h" not in name + + +def run_sdist(monkeypatch, project): + """Given a project directory, run the sdist and return its contents""" + monkeypatch.chdir(project) + with quiet(): + run_setup("setup.py", ["sdist"]) + + archive = next((project / "dist").glob("*.tar.gz")) + with tarfile.open(str(archive)) as tar: + return set(tar.getnames()) + + +def test_sanity_check_setuptools_own_sdist(setuptools_sdist): + with tarfile.open(setuptools_sdist) as tar: + files = tar.getnames() + + # setuptools sdist should not include the .tox folder + tox_files = [name for name in files if ".tox" in name] + assert len(tox_files) == 0, f"not empty {tox_files}" diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_setopt.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_setopt.py new file mode 100644 index 0000000000000000000000000000000000000000..ccf25618a5d6e255ad0be0fbed51fc29c179dcfe --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_setopt.py @@ -0,0 +1,40 @@ +import configparser + +from setuptools.command import setopt + + +class TestEdit: + @staticmethod + def parse_config(filename): + parser = configparser.ConfigParser() + with open(filename, encoding='utf-8') as reader: + parser.read_file(reader) + return parser + + @staticmethod + def write_text(file, content): + with open(file, 'wb') as strm: + strm.write(content.encode('utf-8')) + + def test_utf8_encoding_retained(self, tmpdir): + """ + When editing a file, non-ASCII characters encoded in + UTF-8 should be retained. + """ + config = tmpdir.join('setup.cfg') + self.write_text(str(config), '[names]\njaraco=джарако') + setopt.edit_config(str(config), dict(names=dict(other='yes'))) + parser = self.parse_config(str(config)) + assert parser.get('names', 'jaraco') == 'джарако' + assert parser.get('names', 'other') == 'yes' + + def test_case_retained(self, tmpdir): + """ + When editing a file, case of keys should be retained. + """ + config = tmpdir.join('setup.cfg') + self.write_text(str(config), '[names]\nFoO=bAr') + setopt.edit_config(str(config), dict(names=dict(oTher='yes'))) + actual = config.read_text(encoding='ascii') + assert 'FoO' in actual + assert 'oTher' in actual diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_setuptools.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_setuptools.py new file mode 100644 index 0000000000000000000000000000000000000000..1d56e1a8a4ebc5c7aaeb9902ef9972f7de97ecbf --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_setuptools.py @@ -0,0 +1,290 @@ +"""Tests for the 'setuptools' package""" + +import os +import re +import sys +from zipfile import ZipFile + +import pytest +from packaging.version import Version + +import setuptools +import setuptools.depends as dep +import setuptools.dist +from setuptools.depends import Require + +import distutils.cmd +import distutils.core +from distutils.core import Extension +from distutils.errors import DistutilsSetupError + + +@pytest.fixture(autouse=True) +def isolated_dir(tmpdir_cwd): + return + + +def makeSetup(**args): + """Return distribution from 'setup(**args)', without executing commands""" + + distutils.core._setup_stop_after = "commandline" + + # Don't let system command line leak into tests! + args.setdefault('script_args', ['install']) + + try: + return setuptools.setup(**args) + finally: + distutils.core._setup_stop_after = None + + +needs_bytecode = pytest.mark.skipif( + not hasattr(dep, 'get_module_constant'), + reason="bytecode support not available", +) + + +class TestDepends: + def testExtractConst(self): + if not hasattr(dep, 'extract_constant'): + # skip on non-bytecode platforms + return + + def f1(): + global x, y, z + x = "test" + y = z # pyright: ignore[reportUnboundVariable] # Explicitly testing for this runtime issue + + fc = f1.__code__ + + # unrecognized name + assert dep.extract_constant(fc, 'q', -1) is None + + # constant assigned + assert dep.extract_constant(fc, 'x', -1) == "test" + + # expression assigned + assert dep.extract_constant(fc, 'y', -1) == -1 + + # recognized name, not assigned + assert dep.extract_constant(fc, 'z', -1) is None + + def testFindModule(self): + with pytest.raises(ImportError): + dep.find_module('no-such.-thing') + with pytest.raises(ImportError): + dep.find_module('setuptools.non-existent') + f, _p, _i = dep.find_module('setuptools.tests') + f.close() + + @needs_bytecode + def testModuleExtract(self): + from json import __version__ + + assert dep.get_module_constant('json', '__version__') == __version__ + assert dep.get_module_constant('sys', 'version') == sys.version + assert ( + dep.get_module_constant('setuptools.tests.test_setuptools', '__doc__') + == __doc__ + ) + + @needs_bytecode + def testRequire(self): + req = Require('Json', '1.0.3', 'json') + + assert req.name == 'Json' + assert req.module == 'json' + assert req.requested_version == Version('1.0.3') + assert req.attribute == '__version__' + assert req.full_name() == 'Json-1.0.3' + + from json import __version__ + + assert str(req.get_version()) == __version__ + assert req.version_ok('1.0.9') + assert not req.version_ok('0.9.1') + assert not req.version_ok('unknown') + + assert req.is_present() + assert req.is_current() + + req = Require('Do-what-I-mean', '1.0', 'd-w-i-m') + assert not req.is_present() + assert not req.is_current() + + @needs_bytecode + def test_require_present(self): + # In #1896, this test was failing for months with the only + # complaint coming from test runners (not end users). + # TODO: Evaluate if this code is needed at all. + req = Require('Tests', None, 'tests', homepage="http://example.com") + assert req.format is None + assert req.attribute is None + assert req.requested_version is None + assert req.full_name() == 'Tests' + assert req.homepage == 'http://example.com' + + from setuptools.tests import __path__ + + paths = [os.path.dirname(p) for p in __path__] + assert req.is_present(paths) + assert req.is_current(paths) + + +class TestDistro: + def setup_method(self, method): + self.e1 = Extension('bar.ext', ['bar.c']) + self.e2 = Extension('c.y', ['y.c']) + + self.dist = makeSetup( + packages=['a', 'a.b', 'a.b.c', 'b', 'c'], + py_modules=['b.d', 'x'], + ext_modules=(self.e1, self.e2), + package_dir={}, + ) + + def testDistroType(self): + assert isinstance(self.dist, setuptools.dist.Distribution) + + def testExcludePackage(self): + self.dist.exclude_package('a') + assert self.dist.packages == ['b', 'c'] + + self.dist.exclude_package('b') + assert self.dist.packages == ['c'] + assert self.dist.py_modules == ['x'] + assert self.dist.ext_modules == [self.e1, self.e2] + + self.dist.exclude_package('c') + assert self.dist.packages == [] + assert self.dist.py_modules == ['x'] + assert self.dist.ext_modules == [self.e1] + + # test removals from unspecified options + makeSetup().exclude_package('x') + + def testIncludeExclude(self): + # remove an extension + self.dist.exclude(ext_modules=[self.e1]) + assert self.dist.ext_modules == [self.e2] + + # add it back in + self.dist.include(ext_modules=[self.e1]) + assert self.dist.ext_modules == [self.e2, self.e1] + + # should not add duplicate + self.dist.include(ext_modules=[self.e1]) + assert self.dist.ext_modules == [self.e2, self.e1] + + def testExcludePackages(self): + self.dist.exclude(packages=['c', 'b', 'a']) + assert self.dist.packages == [] + assert self.dist.py_modules == ['x'] + assert self.dist.ext_modules == [self.e1] + + def testEmpty(self): + dist = makeSetup() + dist.include(packages=['a'], py_modules=['b'], ext_modules=[self.e2]) + dist = makeSetup() + dist.exclude(packages=['a'], py_modules=['b'], ext_modules=[self.e2]) + + def testContents(self): + assert self.dist.has_contents_for('a') + self.dist.exclude_package('a') + assert not self.dist.has_contents_for('a') + + assert self.dist.has_contents_for('b') + self.dist.exclude_package('b') + assert not self.dist.has_contents_for('b') + + assert self.dist.has_contents_for('c') + self.dist.exclude_package('c') + assert not self.dist.has_contents_for('c') + + def testInvalidIncludeExclude(self): + with pytest.raises(DistutilsSetupError): + self.dist.include(nonexistent_option='x') + with pytest.raises(DistutilsSetupError): + self.dist.exclude(nonexistent_option='x') + with pytest.raises(DistutilsSetupError): + self.dist.include(packages={'x': 'y'}) + with pytest.raises(DistutilsSetupError): + self.dist.exclude(packages={'x': 'y'}) + with pytest.raises(DistutilsSetupError): + self.dist.include(ext_modules={'x': 'y'}) + with pytest.raises(DistutilsSetupError): + self.dist.exclude(ext_modules={'x': 'y'}) + + with pytest.raises(DistutilsSetupError): + self.dist.include(package_dir=['q']) + with pytest.raises(DistutilsSetupError): + self.dist.exclude(package_dir=['q']) + + +@pytest.fixture +def example_source(tmpdir): + tmpdir.mkdir('foo') + (tmpdir / 'foo/bar.py').write('') + (tmpdir / 'readme.txt').write('') + return tmpdir + + +def test_findall(example_source): + found = list(setuptools.findall(str(example_source))) + expected = ['readme.txt', 'foo/bar.py'] + expected = [example_source.join(fn) for fn in expected] + assert found == expected + + +def test_findall_curdir(example_source): + with example_source.as_cwd(): + found = list(setuptools.findall()) + expected = ['readme.txt', os.path.join('foo', 'bar.py')] + assert found == expected + + +@pytest.fixture +def can_symlink(tmpdir): + """ + Skip if cannot create a symbolic link + """ + link_fn = 'link' + target_fn = 'target' + try: + os.symlink(target_fn, link_fn) + except (OSError, NotImplementedError, AttributeError): + pytest.skip("Cannot create symbolic links") + os.remove(link_fn) + + +@pytest.mark.usefixtures("can_symlink") +def test_findall_missing_symlink(tmpdir): + with tmpdir.as_cwd(): + os.symlink('foo', 'bar') + found = list(setuptools.findall()) + assert found == [] + + +@pytest.mark.xfail(reason="unable to exclude tests; #4475 #3260") +def test_its_own_wheel_does_not_contain_tests(setuptools_wheel): + with ZipFile(setuptools_wheel) as zipfile: + contents = [f.replace(os.sep, '/') for f in zipfile.namelist()] + + for member in contents: + assert '/tests/' not in member + + +def test_wheel_includes_cli_scripts(setuptools_wheel): + with ZipFile(setuptools_wheel) as zipfile: + contents = [f.replace(os.sep, '/') for f in zipfile.namelist()] + + assert any('cli-64.exe' in member for member in contents) + + +def test_wheel_includes_vendored_metadata(setuptools_wheel): + with ZipFile(setuptools_wheel) as zipfile: + contents = [f.replace(os.sep, '/') for f in zipfile.namelist()] + + assert any( + re.search(r'_vendor/.*\.dist-info/METADATA', member) for member in contents + ) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_shutil_wrapper.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_shutil_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..74ff7e9a896328a3d57ca3639658e3b9d538585f --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_shutil_wrapper.py @@ -0,0 +1,23 @@ +import stat +import sys +from unittest.mock import Mock + +from setuptools import _shutil + + +def test_rmtree_readonly(monkeypatch, tmp_path): + """Verify onerr works as expected""" + + tmp_dir = tmp_path / "with_readonly" + tmp_dir.mkdir() + some_file = tmp_dir.joinpath("file.txt") + some_file.touch() + some_file.chmod(stat.S_IREAD) + + expected_count = 1 if sys.platform.startswith("win") else 0 + chmod_fn = Mock(wraps=_shutil.attempt_chmod_verbose) + monkeypatch.setattr(_shutil, "attempt_chmod_verbose", chmod_fn) + + _shutil.rmtree(tmp_dir) + assert chmod_fn.call_count == expected_count + assert not tmp_dir.is_dir() diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_unicode_utils.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_unicode_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a24a9bd5305d1de7c1c925466bb7d222c85864a7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_unicode_utils.py @@ -0,0 +1,10 @@ +from setuptools import unicode_utils + + +def test_filesys_decode_fs_encoding_is_None(monkeypatch): + """ + Test filesys_decode does not raise TypeError when + getfilesystemencoding returns None. + """ + monkeypatch.setattr('sys.getfilesystemencoding', lambda: None) + unicode_utils.filesys_decode(b'test') diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_virtualenv.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_virtualenv.py new file mode 100644 index 0000000000000000000000000000000000000000..b02949baf9cef8eb4df9c697add28f79a93b64d4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_virtualenv.py @@ -0,0 +1,113 @@ +import os +import subprocess +import sys +from urllib.error import URLError +from urllib.request import urlopen + +import pytest + + +@pytest.fixture(autouse=True) +def pytest_virtualenv_works(venv): + """ + pytest_virtualenv may not work. if it doesn't, skip these + tests. See #1284. + """ + venv_prefix = venv.run(["python", "-c", "import sys; print(sys.prefix)"]).strip() + if venv_prefix == sys.prefix: + pytest.skip("virtualenv is broken (see pypa/setuptools#1284)") + + +def test_clean_env_install(venv_without_setuptools, setuptools_wheel): + """ + Check setuptools can be installed in a clean environment. + """ + cmd = ["python", "-m", "pip", "install", str(setuptools_wheel)] + venv_without_setuptools.run(cmd) + + +def access_pypi(): + # Detect if tests are being run without connectivity + if not os.environ.get('NETWORK_REQUIRED', False): # pragma: nocover + try: + urlopen('https://pypi.org', timeout=1) + except URLError: + # No network, disable most of these tests + return False + + return True + + +@pytest.mark.skipif( + 'platform.python_implementation() == "PyPy"', + reason="https://github.com/pypa/setuptools/pull/2865#issuecomment-965834995", +) +@pytest.mark.skipif(not access_pypi(), reason="no network") +# ^-- Even when it is not necessary to install a different version of `pip` +# the build process will still try to download `wheel`, see #3147 and #2986. +@pytest.mark.parametrize( + 'pip_version', + [ + None, + pytest.param( + 'pip<20.1', + marks=pytest.mark.xfail( + 'sys.version_info >= (3, 12)', + reason="pip 23.1.2 required for Python 3.12 and later", + ), + ), + pytest.param( + 'pip<21', + marks=pytest.mark.xfail( + 'sys.version_info >= (3, 12)', + reason="pip 23.1.2 required for Python 3.12 and later", + ), + ), + pytest.param( + 'pip<22', + marks=pytest.mark.xfail( + 'sys.version_info >= (3, 12)', + reason="pip 23.1.2 required for Python 3.12 and later", + ), + ), + pytest.param( + 'pip<23', + marks=pytest.mark.xfail( + 'sys.version_info >= (3, 12)', + reason="pip 23.1.2 required for Python 3.12 and later", + ), + ), + pytest.param( + 'https://github.com/pypa/pip/archive/main.zip', + marks=pytest.mark.xfail(reason='#2975'), + ), + ], +) +def test_pip_upgrade_from_source( + pip_version, venv_without_setuptools, setuptools_wheel, setuptools_sdist +): + """ + Check pip can upgrade setuptools from source. + """ + # Install pip/wheel, in a venv without setuptools (as it + # should not be needed for bootstrapping from source) + venv = venv_without_setuptools + venv.run(["pip", "install", "-U", "wheel"]) + if pip_version is not None: + venv.run(["python", "-m", "pip", "install", "-U", pip_version, "--retries=1"]) + with pytest.raises(subprocess.CalledProcessError): + # Meta-test to make sure setuptools is not installed + venv.run(["python", "-c", "import setuptools"]) + + # Then install from wheel. + venv.run(["pip", "install", str(setuptools_wheel)]) + # And finally try to upgrade from source. + venv.run(["pip", "install", "--no-cache-dir", "--upgrade", str(setuptools_sdist)]) + + +def test_no_missing_dependencies(bare_venv, request): + """ + Quick and dirty test to ensure all external dependencies are vendored. + """ + setuptools_dir = request.config.rootdir + bare_venv.run(['python', 'setup.py', '--help'], cwd=setuptools_dir) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_warnings.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_warnings.py new file mode 100644 index 0000000000000000000000000000000000000000..41193d4f717344546f8c70ad18b268a04740129b --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_warnings.py @@ -0,0 +1,106 @@ +from inspect import cleandoc + +import pytest + +from setuptools.warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning + +_EXAMPLES = { + "default": dict( + args=("Hello {x}", "\n\t{target} {v:.1f}"), + kwargs={"x": 5, "v": 3, "target": "World"}, + expected=""" + Hello 5 + !! + + ******************************************************************************** + World 3.0 + ******************************************************************************** + + !! + """, + ), + "futue_due_date": dict( + args=("Summary", "Lorem ipsum"), + kwargs={"due_date": (9999, 11, 22)}, + expected=""" + Summary + !! + + ******************************************************************************** + Lorem ipsum + + By 9999-Nov-22, you need to update your project and remove deprecated calls + or your builds will no longer be supported. + ******************************************************************************** + + !! + """, + ), + "past_due_date_with_docs": dict( + args=("Summary", "Lorem ipsum"), + kwargs={"due_date": (2000, 11, 22), "see_docs": "some_page.html"}, + expected=""" + Summary + !! + + ******************************************************************************** + Lorem ipsum + + This deprecation is overdue, please update your project and remove deprecated + calls to avoid build errors in the future. + + See https://setuptools.pypa.io/en/latest/some_page.html for details. + ******************************************************************************** + + !! + """, + ), +} + + +@pytest.mark.parametrize("example_name", _EXAMPLES.keys()) +def test_formatting(monkeypatch, example_name): + """ + It should automatically handle indentation, interpolation and things like due date. + """ + args = _EXAMPLES[example_name]["args"] + kwargs = _EXAMPLES[example_name]["kwargs"] + expected = _EXAMPLES[example_name]["expected"] + + monkeypatch.setenv("SETUPTOOLS_ENFORCE_DEPRECATION", "false") + with pytest.warns(SetuptoolsWarning) as warn_info: + SetuptoolsWarning.emit(*args, **kwargs) + assert _get_message(warn_info) == cleandoc(expected) + + +def test_due_date_enforcement(monkeypatch): + class _MyDeprecation(SetuptoolsDeprecationWarning): + _SUMMARY = "Summary" + _DETAILS = "Lorem ipsum" + _DUE_DATE = (2000, 11, 22) + _SEE_DOCS = "some_page.html" + + monkeypatch.setenv("SETUPTOOLS_ENFORCE_DEPRECATION", "true") + with pytest.raises(SetuptoolsDeprecationWarning) as exc_info: + _MyDeprecation.emit() + + expected = """ + Summary + !! + + ******************************************************************************** + Lorem ipsum + + This deprecation is overdue, please update your project and remove deprecated + calls to avoid build errors in the future. + + See https://setuptools.pypa.io/en/latest/some_page.html for details. + ******************************************************************************** + + !! + """ + assert str(exc_info.value) == cleandoc(expected) + + +def _get_message(warn_info): + return next(warn.message.args[0] for warn in warn_info) diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_wheel.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_wheel.py new file mode 100644 index 0000000000000000000000000000000000000000..70165c608b60bc70a908a8b56e1f1105feeb4712 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_wheel.py @@ -0,0 +1,714 @@ +"""wheel tests""" + +from __future__ import annotations + +import contextlib +import glob +import inspect +import os +import pathlib +import shutil +import stat +import subprocess +import sys +import zipfile +from typing import Any + +import pytest +from jaraco import path +from packaging.tags import parse_tag +from packaging.utils import canonicalize_name + +from pkg_resources import PY_MAJOR, Distribution, PathMetadata +from setuptools.wheel import Wheel + +from .contexts import tempdir +from .textwrap import DALS + +from distutils.sysconfig import get_config_var +from distutils.util import get_platform + +WHEEL_INFO_TESTS = ( + ('invalid.whl', ValueError), + ( + 'simplewheel-2.0-1-py2.py3-none-any.whl', + { + 'project_name': 'simplewheel', + 'version': '2.0', + 'build': '1', + 'py_version': 'py2.py3', + 'abi': 'none', + 'platform': 'any', + }, + ), + ( + 'simple.dist-0.1-py2.py3-none-any.whl', + { + 'project_name': 'simple.dist', + 'version': '0.1', + 'build': None, + 'py_version': 'py2.py3', + 'abi': 'none', + 'platform': 'any', + }, + ), + ( + 'example_pkg_a-1-py3-none-any.whl', + { + 'project_name': 'example_pkg_a', + 'version': '1', + 'build': None, + 'py_version': 'py3', + 'abi': 'none', + 'platform': 'any', + }, + ), + ( + 'PyQt5-5.9-5.9.1-cp35.cp36.cp37-abi3-manylinux1_x86_64.whl', + { + 'project_name': 'PyQt5', + 'version': '5.9', + 'build': '5.9.1', + 'py_version': 'cp35.cp36.cp37', + 'abi': 'abi3', + 'platform': 'manylinux1_x86_64', + }, + ), +) + + +@pytest.mark.parametrize( + ('filename', 'info'), WHEEL_INFO_TESTS, ids=[t[0] for t in WHEEL_INFO_TESTS] +) +def test_wheel_info(filename, info): + if inspect.isclass(info): + with pytest.raises(info): + Wheel(filename) + return + w = Wheel(filename) + assert {k: getattr(w, k) for k in info.keys()} == info + + +@contextlib.contextmanager +def build_wheel(extra_file_defs=None, **kwargs): + file_defs = { + 'setup.py': ( + DALS( + """ + # -*- coding: utf-8 -*- + from setuptools import setup + import setuptools + setup(**%r) + """ + ) + % kwargs + ).encode('utf-8'), + } + if extra_file_defs: + file_defs.update(extra_file_defs) + with tempdir() as source_dir: + path.build(file_defs, source_dir) + subprocess.check_call( + (sys.executable, 'setup.py', '-q', 'bdist_wheel'), cwd=source_dir + ) + yield glob.glob(os.path.join(source_dir, 'dist', '*.whl'))[0] + + +def tree_set(root): + contents = set() + for dirpath, dirnames, filenames in os.walk(root): + for filename in filenames: + contents.add(os.path.join(os.path.relpath(dirpath, root), filename)) + return contents + + +def flatten_tree(tree): + """Flatten nested dicts and lists into a full list of paths""" + output = set() + for node, contents in tree.items(): + if isinstance(contents, dict): + contents = flatten_tree(contents) + + for elem in contents: + if isinstance(elem, dict): + output |= {os.path.join(node, val) for val in flatten_tree(elem)} + else: + output.add(os.path.join(node, elem)) + return output + + +def format_install_tree(tree): + return { + x.format( + py_version=PY_MAJOR, + platform=get_platform(), + shlib_ext=get_config_var('EXT_SUFFIX') or get_config_var('SO'), + ) + for x in tree + } + + +def _check_wheel_install( + filename, install_dir, install_tree_includes, project_name, version, requires_txt +): + w = Wheel(filename) + egg_path = os.path.join(install_dir, w.egg_name()) + w.install_as_egg(egg_path) + if install_tree_includes is not None: + install_tree = format_install_tree(install_tree_includes) + exp = tree_set(install_dir) + assert install_tree.issubset(exp), install_tree - exp + + metadata = PathMetadata(egg_path, os.path.join(egg_path, 'EGG-INFO')) + dist = Distribution.from_filename(egg_path, metadata=metadata) + assert dist.project_name == project_name + assert dist.version == version + if requires_txt is None: + assert not dist.has_metadata('requires.txt') + else: + # Order must match to ensure reproducibility. + assert requires_txt == dist.get_metadata('requires.txt').lstrip() + + +class Record: + def __init__(self, id, **kwargs): + self._id = id + self._fields = kwargs + + def __repr__(self) -> str: + return f'{self._id}(**{self._fields!r})' + + +# Using Any to avoid possible type union issues later in test +# making a TypedDict is not worth in a test and anonymous/inline TypedDict are experimental +# https://github.com/python/mypy/issues/9884 +WHEEL_INSTALL_TESTS: tuple[dict[str, Any], ...] = ( + dict( + id='basic', + file_defs={'foo': {'__init__.py': ''}}, + setup_kwargs=dict( + packages=['foo'], + ), + install_tree=flatten_tree({ + 'foo-1.0-py{py_version}.egg': { + 'EGG-INFO': ['PKG-INFO', 'RECORD', 'WHEEL', 'top_level.txt'], + 'foo': ['__init__.py'], + } + }), + ), + dict( + id='utf-8', + setup_kwargs=dict( + description='Description accentuée', + ), + ), + dict( + id='data', + file_defs={ + 'data.txt': DALS( + """ + Some data... + """ + ), + }, + setup_kwargs=dict( + data_files=[('data_dir', ['data.txt'])], + ), + install_tree=flatten_tree({ + 'foo-1.0-py{py_version}.egg': { + 'EGG-INFO': ['PKG-INFO', 'RECORD', 'WHEEL', 'top_level.txt'], + 'data_dir': ['data.txt'], + } + }), + ), + dict( + id='extension', + file_defs={ + 'extension.c': DALS( + """ + #include "Python.h" + + #if PY_MAJOR_VERSION >= 3 + + static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "extension", + NULL, + 0, + NULL, + NULL, + NULL, + NULL, + NULL + }; + + #define INITERROR return NULL + + PyMODINIT_FUNC PyInit_extension(void) + + #else + + #define INITERROR return + + void initextension(void) + + #endif + { + #if PY_MAJOR_VERSION >= 3 + PyObject *module = PyModule_Create(&moduledef); + #else + PyObject *module = Py_InitModule("extension", NULL); + #endif + if (module == NULL) + INITERROR; + #if PY_MAJOR_VERSION >= 3 + return module; + #endif + } + """ + ), + }, + setup_kwargs=dict( + ext_modules=[ + Record( + 'setuptools.Extension', name='extension', sources=['extension.c'] + ) + ], + ), + install_tree=flatten_tree({ + 'foo-1.0-py{py_version}-{platform}.egg': [ + 'extension{shlib_ext}', + { + 'EGG-INFO': [ + 'PKG-INFO', + 'RECORD', + 'WHEEL', + 'top_level.txt', + ] + }, + ] + }), + ), + dict( + id='header', + file_defs={ + 'header.h': DALS( + """ + """ + ), + }, + setup_kwargs=dict( + headers=['header.h'], + ), + install_tree=flatten_tree({ + 'foo-1.0-py{py_version}.egg': [ + 'header.h', + { + 'EGG-INFO': [ + 'PKG-INFO', + 'RECORD', + 'WHEEL', + 'top_level.txt', + ] + }, + ] + }), + ), + dict( + id='script', + file_defs={ + 'script.py': DALS( + """ + #/usr/bin/python + print('hello world!') + """ + ), + 'script.sh': DALS( + """ + #/bin/sh + echo 'hello world!' + """ + ), + }, + setup_kwargs=dict( + scripts=['script.py', 'script.sh'], + ), + install_tree=flatten_tree({ + 'foo-1.0-py{py_version}.egg': { + 'EGG-INFO': [ + 'PKG-INFO', + 'RECORD', + 'WHEEL', + 'top_level.txt', + {'scripts': ['script.py', 'script.sh']}, + ] + } + }), + ), + dict( + id='requires1', + install_requires='foobar==2.0', + install_tree=flatten_tree({ + 'foo-1.0-py{py_version}.egg': { + 'EGG-INFO': [ + 'PKG-INFO', + 'RECORD', + 'WHEEL', + 'requires.txt', + 'top_level.txt', + ] + } + }), + requires_txt=DALS( + """ + foobar==2.0 + """ + ), + ), + dict( + id='requires2', + install_requires=f""" + bar + foo<=2.0; {sys.platform!r} in sys_platform + """, + requires_txt=DALS( + """ + bar + foo<=2.0 + """ + ), + ), + dict( + id='requires3', + install_requires=f""" + bar; {sys.platform!r} != sys_platform + """, + ), + dict( + id='requires4', + install_requires=""" + foo + """, + extras_require={ + 'extra': 'foobar>3', + }, + requires_txt=DALS( + """ + foo + + [extra] + foobar>3 + """ + ), + ), + dict( + id='requires5', + extras_require={ + 'extra': f'foobar; {sys.platform!r} != sys_platform', + }, + requires_txt=DALS( + """ + [extra] + """ + ), + ), + dict( + id='requires_ensure_order', + install_requires=""" + foo + bar + baz + qux + """, + extras_require={ + 'extra': """ + foobar>3 + barbaz>4 + bazqux>5 + quxzap>6 + """, + }, + requires_txt=DALS( + """ + foo + bar + baz + qux + + [extra] + foobar>3 + barbaz>4 + bazqux>5 + quxzap>6 + """ + ), + ), + dict( + id='namespace_package', + file_defs={ + 'foo': { + 'bar': {'__init__.py': ''}, + }, + }, + setup_kwargs=dict( + namespace_packages=['foo'], + packages=['foo.bar'], + ), + install_tree=flatten_tree({ + 'foo-1.0-py{py_version}.egg': [ + 'foo-1.0-py{py_version}-nspkg.pth', + { + 'EGG-INFO': [ + 'PKG-INFO', + 'RECORD', + 'WHEEL', + 'namespace_packages.txt', + 'top_level.txt', + ] + }, + { + 'foo': [ + '__init__.py', + {'bar': ['__init__.py']}, + ] + }, + ] + }), + ), + dict( + id='empty_namespace_package', + file_defs={ + 'foobar': { + '__init__.py': ( + "__import__('pkg_resources').declare_namespace(__name__)" + ) + }, + }, + setup_kwargs=dict( + namespace_packages=['foobar'], + packages=['foobar'], + ), + install_tree=flatten_tree({ + 'foo-1.0-py{py_version}.egg': [ + 'foo-1.0-py{py_version}-nspkg.pth', + { + 'EGG-INFO': [ + 'PKG-INFO', + 'RECORD', + 'WHEEL', + 'namespace_packages.txt', + 'top_level.txt', + ] + }, + { + 'foobar': [ + '__init__.py', + ] + }, + ] + }), + ), + dict( + id='data_in_package', + file_defs={ + 'foo': { + '__init__.py': '', + 'data_dir': { + 'data.txt': DALS( + """ + Some data... + """ + ), + }, + } + }, + setup_kwargs=dict( + packages=['foo'], + data_files=[('foo/data_dir', ['foo/data_dir/data.txt'])], + ), + install_tree=flatten_tree({ + 'foo-1.0-py{py_version}.egg': { + 'EGG-INFO': [ + 'PKG-INFO', + 'RECORD', + 'WHEEL', + 'top_level.txt', + ], + 'foo': [ + '__init__.py', + { + 'data_dir': [ + 'data.txt', + ] + }, + ], + } + }), + ), +) + + +@pytest.mark.parametrize( + 'params', + WHEEL_INSTALL_TESTS, + ids=[params['id'] for params in WHEEL_INSTALL_TESTS], +) +def test_wheel_install(params): + project_name = params.get('name', 'foo') + version = params.get('version', '1.0') + install_requires = params.get('install_requires', []) + extras_require = params.get('extras_require', {}) + requires_txt = params.get('requires_txt', None) + install_tree = params.get('install_tree') + file_defs = params.get('file_defs', {}) + setup_kwargs = params.get('setup_kwargs', {}) + with ( + build_wheel( + name=project_name, + version=version, + install_requires=install_requires, + extras_require=extras_require, + extra_file_defs=file_defs, + **setup_kwargs, + ) as filename, + tempdir() as install_dir, + ): + _check_wheel_install( + filename, install_dir, install_tree, project_name, version, requires_txt + ) + + +def test_wheel_install_pep_503(): + project_name = 'Foo_Bar' # PEP 503 canonicalized name is "foo-bar" + version = '1.0' + with ( + build_wheel( + name=project_name, + version=version, + ) as filename, + tempdir() as install_dir, + ): + new_filename = filename.replace(project_name, canonicalize_name(project_name)) + shutil.move(filename, new_filename) + _check_wheel_install( + new_filename, + install_dir, + None, + canonicalize_name(project_name), + version, + None, + ) + + +def test_wheel_no_dist_dir(): + project_name = 'nodistinfo' + version = '1.0' + wheel_name = f'{project_name}-{version}-py2.py3-none-any.whl' + with tempdir() as source_dir: + wheel_path = os.path.join(source_dir, wheel_name) + # create an empty zip file + zipfile.ZipFile(wheel_path, 'w').close() + with tempdir() as install_dir: + with pytest.raises(ValueError): + _check_wheel_install( + wheel_path, install_dir, None, project_name, version, None + ) + + +def test_wheel_is_compatible(monkeypatch): + def sys_tags(): + return { + (t.interpreter, t.abi, t.platform) + for t in parse_tag('cp36-cp36m-manylinux1_x86_64') + } + + monkeypatch.setattr('setuptools.wheel._get_supported_tags', sys_tags) + assert Wheel('onnxruntime-0.1.2-cp36-cp36m-manylinux1_x86_64.whl').is_compatible() + + +def test_wheel_mode(): + @contextlib.contextmanager + def build_wheel(extra_file_defs=None, **kwargs): + file_defs = { + 'setup.py': ( + DALS( + """ + # -*- coding: utf-8 -*- + from setuptools import setup + import setuptools + setup(**%r) + """ + ) + % kwargs + ).encode('utf-8'), + } + if extra_file_defs: + file_defs.update(extra_file_defs) + with tempdir() as source_dir: + path.build(file_defs, source_dir) + runsh = pathlib.Path(source_dir) / "script.sh" + os.chmod(runsh, 0o777) + subprocess.check_call( + (sys.executable, 'setup.py', '-q', 'bdist_wheel'), cwd=source_dir + ) + yield glob.glob(os.path.join(source_dir, 'dist', '*.whl'))[0] + + params = dict( + id='script', + file_defs={ + 'script.py': DALS( + """ + #/usr/bin/python + print('hello world!') + """ + ), + 'script.sh': DALS( + """ + #/bin/sh + echo 'hello world!' + """ + ), + }, + setup_kwargs=dict( + scripts=['script.py', 'script.sh'], + ), + install_tree=flatten_tree({ + 'foo-1.0-py{py_version}.egg': { + 'EGG-INFO': [ + 'PKG-INFO', + 'RECORD', + 'WHEEL', + 'top_level.txt', + {'scripts': ['script.py', 'script.sh']}, + ] + } + }), + ) + + project_name = params.get('name', 'foo') + version = params.get('version', '1.0') + install_tree = params.get('install_tree') + file_defs = params.get('file_defs', {}) + setup_kwargs = params.get('setup_kwargs', {}) + + with ( + build_wheel( + name=project_name, + version=version, + install_requires=[], + extras_require={}, + extra_file_defs=file_defs, + **setup_kwargs, + ) as filename, + tempdir() as install_dir, + ): + _check_wheel_install( + filename, install_dir, install_tree, project_name, version, None + ) + w = Wheel(filename) + base = pathlib.Path(install_dir) / w.egg_name() + script_sh = base / "EGG-INFO" / "scripts" / "script.sh" + assert script_sh.exists() + if sys.platform != 'win32': + # Editable file mode has no effect on Windows + assert oct(stat.S_IMODE(script_sh.stat().st_mode)) == "0o777" diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/test_windows_wrappers.py b/venv/lib/python3.11/site-packages/setuptools/tests/test_windows_wrappers.py new file mode 100644 index 0000000000000000000000000000000000000000..f895485387f2f9f673f96f7114629297a381e66c --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/test_windows_wrappers.py @@ -0,0 +1,259 @@ +""" +Python Script Wrapper for Windows +================================= + +setuptools includes wrappers for Python scripts that allows them to be +executed like regular windows programs. There are 2 wrappers, one +for command-line programs, cli.exe, and one for graphical programs, +gui.exe. These programs are almost identical, function pretty much +the same way, and are generated from the same source file. The +wrapper programs are used by copying them to the directory containing +the script they are to wrap and with the same name as the script they +are to wrap. +""" + +import pathlib +import platform +import subprocess +import sys +import textwrap + +import pytest + +import pkg_resources +from setuptools.command.easy_install import nt_quote_arg + +pytestmark = pytest.mark.skipif(sys.platform != 'win32', reason="Windows only") + + +class WrapperTester: + @classmethod + def prep_script(cls, template): + python_exe = nt_quote_arg(sys.executable) + return template % locals() + + @classmethod + def create_script(cls, tmpdir): + """ + Create a simple script, foo-script.py + + Note that the script starts with a Unix-style '#!' line saying which + Python executable to run. The wrapper will use this line to find the + correct Python executable. + """ + + script = cls.prep_script(cls.script_tmpl) + + with (tmpdir / cls.script_name).open('w') as f: + f.write(script) + + # also copy cli.exe to the sample directory + with (tmpdir / cls.wrapper_name).open('wb') as f: + w = pkg_resources.resource_string('setuptools', cls.wrapper_source) + f.write(w) + + +def win_launcher_exe(prefix): + """A simple routine to select launcher script based on platform.""" + assert prefix in ('cli', 'gui') + if platform.machine() == "ARM64": + return f"{prefix}-arm64.exe" + else: + return f"{prefix}-32.exe" + + +class TestCLI(WrapperTester): + script_name = 'foo-script.py' + wrapper_name = 'foo.exe' + wrapper_source = win_launcher_exe('cli') + + script_tmpl = textwrap.dedent( + """ + #!%(python_exe)s + import sys + input = repr(sys.stdin.read()) + print(sys.argv[0][-14:]) + print(sys.argv[1:]) + print(input) + if __debug__: + print('non-optimized') + """ + ).lstrip() + + def test_basic(self, tmpdir): + """ + When the copy of cli.exe, foo.exe in this example, runs, it examines + the path name it was run with and computes a Python script path name + by removing the '.exe' suffix and adding the '-script.py' suffix. (For + GUI programs, the suffix '-script.pyw' is added.) This is why we + named out script the way we did. Now we can run out script by running + the wrapper: + + This example was a little pathological in that it exercised windows + (MS C runtime) quoting rules: + + - Strings containing spaces are surrounded by double quotes. + + - Double quotes in strings need to be escaped by preceding them with + back slashes. + + - One or more backslashes preceding double quotes need to be escaped + by preceding each of them with back slashes. + """ + self.create_script(tmpdir) + cmd = [ + str(tmpdir / 'foo.exe'), + 'arg1', + 'arg 2', + 'arg "2\\"', + 'arg 4\\', + 'arg5 a\\\\b', + ] + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + text=True, + encoding="utf-8", + ) + stdout, _stderr = proc.communicate('hello\nworld\n') + actual = stdout.replace('\r\n', '\n') + expected = textwrap.dedent( + r""" + \foo-script.py + ['arg1', 'arg 2', 'arg "2\\"', 'arg 4\\', 'arg5 a\\\\b'] + 'hello\nworld\n' + non-optimized + """ + ).lstrip() + assert actual == expected + + def test_symlink(self, tmpdir): + """ + Ensure that symlink for the foo.exe is working correctly. + """ + script_dir = tmpdir / "script_dir" + script_dir.mkdir() + self.create_script(script_dir) + symlink = pathlib.Path(tmpdir / "foo.exe") + symlink.symlink_to(script_dir / "foo.exe") + + cmd = [ + str(tmpdir / 'foo.exe'), + 'arg1', + 'arg 2', + 'arg "2\\"', + 'arg 4\\', + 'arg5 a\\\\b', + ] + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + text=True, + encoding="utf-8", + ) + stdout, _stderr = proc.communicate('hello\nworld\n') + actual = stdout.replace('\r\n', '\n') + expected = textwrap.dedent( + r""" + \foo-script.py + ['arg1', 'arg 2', 'arg "2\\"', 'arg 4\\', 'arg5 a\\\\b'] + 'hello\nworld\n' + non-optimized + """ + ).lstrip() + assert actual == expected + + def test_with_options(self, tmpdir): + """ + Specifying Python Command-line Options + -------------------------------------- + + You can specify a single argument on the '#!' line. This can be used + to specify Python options like -O, to run in optimized mode or -i + to start the interactive interpreter. You can combine multiple + options as usual. For example, to run in optimized mode and + enter the interpreter after running the script, you could use -Oi: + """ + self.create_script(tmpdir) + tmpl = textwrap.dedent( + """ + #!%(python_exe)s -Oi + import sys + input = repr(sys.stdin.read()) + print(sys.argv[0][-14:]) + print(sys.argv[1:]) + print(input) + if __debug__: + print('non-optimized') + sys.ps1 = '---' + """ + ).lstrip() + with (tmpdir / 'foo-script.py').open('w') as f: + f.write(self.prep_script(tmpl)) + cmd = [str(tmpdir / 'foo.exe')] + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding="utf-8", + ) + stdout, _stderr = proc.communicate() + actual = stdout.replace('\r\n', '\n') + expected = textwrap.dedent( + r""" + \foo-script.py + [] + '' + --- + """ + ).lstrip() + assert actual == expected + + +class TestGUI(WrapperTester): + """ + Testing the GUI Version + ----------------------- + """ + + script_name = 'bar-script.pyw' + wrapper_source = win_launcher_exe('gui') + wrapper_name = 'bar.exe' + + script_tmpl = textwrap.dedent( + """ + #!%(python_exe)s + import sys + f = open(sys.argv[1], 'wb') + bytes_written = f.write(repr(sys.argv[2]).encode('utf-8')) + f.close() + """ + ).strip() + + def test_basic(self, tmpdir): + """Test the GUI version with the simple script, bar-script.py""" + self.create_script(tmpdir) + + cmd = [ + str(tmpdir / 'bar.exe'), + str(tmpdir / 'test_output.txt'), + 'Test Argument', + ] + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding="utf-8", + ) + stdout, stderr = proc.communicate() + assert not stdout + assert not stderr + with (tmpdir / 'test_output.txt').open('rb') as f_out: + actual = f_out.read().decode('ascii') + assert actual == repr('Test Argument') diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/text.py b/venv/lib/python3.11/site-packages/setuptools/tests/text.py new file mode 100644 index 0000000000000000000000000000000000000000..e05cc633ede9e5ce4f74b66a7bf76327c2000caa --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/text.py @@ -0,0 +1,4 @@ +class Filenames: + unicode = 'smörbröd.py' + latin_1 = unicode.encode('latin-1') + utf_8 = unicode.encode('utf-8') diff --git a/venv/lib/python3.11/site-packages/setuptools/tests/textwrap.py b/venv/lib/python3.11/site-packages/setuptools/tests/textwrap.py new file mode 100644 index 0000000000000000000000000000000000000000..5e39618dca4ad6c3f0d4c8cb20af59ab85fb0eba --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/tests/textwrap.py @@ -0,0 +1,6 @@ +import textwrap + + +def DALS(s): + "dedent and left-strip" + return textwrap.dedent(s).lstrip() diff --git a/venv/lib/python3.11/site-packages/setuptools/unicode_utils.py b/venv/lib/python3.11/site-packages/setuptools/unicode_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f502f5b089619eafd28e2c7a61967e34e16920e5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/unicode_utils.py @@ -0,0 +1,102 @@ +import sys +import unicodedata +from configparser import RawConfigParser + +from .compat import py39 +from .warnings import SetuptoolsDeprecationWarning + + +# HFS Plus uses decomposed UTF-8 +def decompose(path): + if isinstance(path, str): + return unicodedata.normalize('NFD', path) + try: + path = path.decode('utf-8') + path = unicodedata.normalize('NFD', path) + path = path.encode('utf-8') + except UnicodeError: + pass # Not UTF-8 + return path + + +def filesys_decode(path): + """ + Ensure that the given path is decoded, + ``None`` when no expected encoding works + """ + + if isinstance(path, str): + return path + + fs_enc = sys.getfilesystemencoding() or 'utf-8' + candidates = fs_enc, 'utf-8' + + for enc in candidates: + try: + return path.decode(enc) + except UnicodeDecodeError: + continue + + return None + + +def try_encode(string, enc): + "turn unicode encoding into a functional routine" + try: + return string.encode(enc) + except UnicodeEncodeError: + return None + + +def _read_utf8_with_fallback(file: str, fallback_encoding=py39.LOCALE_ENCODING) -> str: + """ + First try to read the file with UTF-8, if there is an error fallback to a + different encoding ("locale" by default). Returns the content of the file. + Also useful when reading files that might have been produced by an older version of + setuptools. + """ + try: + with open(file, "r", encoding="utf-8") as f: + return f.read() + except UnicodeDecodeError: # pragma: no cover + _Utf8EncodingNeeded.emit(file=file, fallback_encoding=fallback_encoding) + with open(file, "r", encoding=fallback_encoding) as f: + return f.read() + + +def _cfg_read_utf8_with_fallback( + cfg: RawConfigParser, file: str, fallback_encoding=py39.LOCALE_ENCODING +) -> None: + """Same idea as :func:`_read_utf8_with_fallback`, but for the + :meth:`RawConfigParser.read` method. + + This method may call ``cfg.clear()``. + """ + try: + cfg.read(file, encoding="utf-8") + except UnicodeDecodeError: # pragma: no cover + _Utf8EncodingNeeded.emit(file=file, fallback_encoding=fallback_encoding) + cfg.clear() + cfg.read(file, encoding=fallback_encoding) + + +class _Utf8EncodingNeeded(SetuptoolsDeprecationWarning): + _SUMMARY = """ + `encoding="utf-8"` fails with {file!r}, trying `encoding={fallback_encoding!r}`. + """ + + _DETAILS = """ + Fallback behavior for UTF-8 is considered **deprecated** and future versions of + `setuptools` may not implement it. + + Please encode {file!r} with "utf-8" to ensure future builds will succeed. + + If this file was produced by `setuptools` itself, cleaning up the cached files + and re-building/re-installing the package with a newer version of `setuptools` + (e.g. by updating `build-system.requires` in its `pyproject.toml`) + might solve the problem. + """ + # TODO: Add a deadline? + # Will we be able to remove this? + # The question comes to mind mainly because of sdists that have been produced + # by old versions of setuptools and published to PyPI... diff --git a/venv/lib/python3.11/site-packages/setuptools/version.py b/venv/lib/python3.11/site-packages/setuptools/version.py new file mode 100644 index 0000000000000000000000000000000000000000..ec253c414474677d3a5977511cfe901bfb786740 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/version.py @@ -0,0 +1,6 @@ +from ._importlib import metadata + +try: + __version__ = metadata.version('setuptools') or '0.dev0+unknown' +except Exception: + __version__ = '0.dev0+unknown' diff --git a/venv/lib/python3.11/site-packages/setuptools/warnings.py b/venv/lib/python3.11/site-packages/setuptools/warnings.py new file mode 100644 index 0000000000000000000000000000000000000000..96467787c237846bfbacf2d44eb833be0a88b633 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/warnings.py @@ -0,0 +1,110 @@ +"""Provide basic warnings used by setuptools modules. + +Using custom classes (other than ``UserWarning``) allow users to set +``PYTHONWARNINGS`` filters to run tests and prepare for upcoming changes in +setuptools. +""" + +from __future__ import annotations + +import os +import warnings +from datetime import date +from inspect import cleandoc +from textwrap import indent +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + +_DueDate: TypeAlias = tuple[int, int, int] # time tuple +_INDENT = 8 * " " +_TEMPLATE = f"""{80 * '*'}\n{{details}}\n{80 * '*'}""" + + +class SetuptoolsWarning(UserWarning): + """Base class in ``setuptools`` warning hierarchy.""" + + @classmethod + def emit( + cls, + summary: str | None = None, + details: str | None = None, + due_date: _DueDate | None = None, + see_docs: str | None = None, + see_url: str | None = None, + stacklevel: int = 2, + **kwargs, + ) -> None: + """Private: reserved for ``setuptools`` internal use only""" + # Default values: + summary_ = summary or getattr(cls, "_SUMMARY", None) or "" + details_ = details or getattr(cls, "_DETAILS", None) or "" + due_date = due_date or getattr(cls, "_DUE_DATE", None) + docs_ref = see_docs or getattr(cls, "_SEE_DOCS", None) + docs_url = docs_ref and f"https://setuptools.pypa.io/en/latest/{docs_ref}" + see_url = see_url or getattr(cls, "_SEE_URL", None) + due = date(*due_date) if due_date else None + + text = cls._format(summary_, details_, due, see_url or docs_url, kwargs) + if due and due < date.today() and _should_enforce(): + raise cls(text) + warnings.warn(text, cls, stacklevel=stacklevel + 1) + + @classmethod + def _format( + cls, + summary: str, + details: str, + due_date: date | None = None, + see_url: str | None = None, + format_args: dict | None = None, + ) -> str: + """Private: reserved for ``setuptools`` internal use only""" + today = date.today() + summary = cleandoc(summary).format_map(format_args or {}) + possible_parts = [ + cleandoc(details).format_map(format_args or {}), + ( + f"\nBy {due_date:%Y-%b-%d}, you need to update your project and remove " + "deprecated calls\nor your builds will no longer be supported." + if due_date and due_date > today + else None + ), + ( + "\nThis deprecation is overdue, please update your project and remove " + "deprecated\ncalls to avoid build errors in the future." + if due_date and due_date < today + else None + ), + (f"\nSee {see_url} for details." if see_url else None), + ] + parts = [x for x in possible_parts if x] + if parts: + body = indent(_TEMPLATE.format(details="\n".join(parts)), _INDENT) + return "\n".join([summary, "!!\n", body, "\n!!"]) + return summary + + +class InformationOnly(SetuptoolsWarning): + """Currently there is no clear way of displaying messages to the users + that use the setuptools backend directly via ``pip``. + The only thing that might work is a warning, although it is not the + most appropriate tool for the job... + + See pypa/packaging-problems#558. + """ + + +class SetuptoolsDeprecationWarning(SetuptoolsWarning): + """ + Base class for warning deprecations in ``setuptools`` + + This class is not derived from ``DeprecationWarning``, and as such is + visible by default. + """ + + +def _should_enforce(): + enforce = os.getenv("SETUPTOOLS_ENFORCE_DEPRECATION", "false").lower() + return enforce in ("true", "on", "ok", "1") diff --git a/venv/lib/python3.11/site-packages/setuptools/wheel.py b/venv/lib/python3.11/site-packages/setuptools/wheel.py new file mode 100644 index 0000000000000000000000000000000000000000..c7ca43b5cfb2aff8d6983bbba4b7e6fdc9d01f83 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/wheel.py @@ -0,0 +1,236 @@ +"""Wheels support.""" + +import contextlib +import email +import functools +import itertools +import os +import posixpath +import re +import zipfile + +from packaging.tags import sys_tags +from packaging.utils import canonicalize_name +from packaging.version import Version as parse_version + +import setuptools +from setuptools.archive_util import _unpack_zipfile_obj +from setuptools.command.egg_info import _egg_basename, write_requirements + +from .unicode_utils import _read_utf8_with_fallback + +from distutils.util import get_platform + +WHEEL_NAME = re.compile( + r"""^(?P.+?)-(?P\d.*?) + ((-(?P\d.*?))?-(?P.+?)-(?P.+?)-(?P.+?) + )\.whl$""", + re.VERBOSE, +).match + +NAMESPACE_PACKAGE_INIT = "__import__('pkg_resources').declare_namespace(__name__)\n" + + +@functools.cache +def _get_supported_tags(): + # We calculate the supported tags only once, otherwise calling + # this method on thousands of wheels takes seconds instead of + # milliseconds. + return {(t.interpreter, t.abi, t.platform) for t in sys_tags()} + + +def unpack(src_dir, dst_dir) -> None: + """Move everything under `src_dir` to `dst_dir`, and delete the former.""" + for dirpath, dirnames, filenames in os.walk(src_dir): + subdir = os.path.relpath(dirpath, src_dir) + for f in filenames: + src = os.path.join(dirpath, f) + dst = os.path.join(dst_dir, subdir, f) + os.renames(src, dst) + for n, d in reversed(list(enumerate(dirnames))): + src = os.path.join(dirpath, d) + dst = os.path.join(dst_dir, subdir, d) + if not os.path.exists(dst): + # Directory does not exist in destination, + # rename it and prune it from os.walk list. + os.renames(src, dst) + del dirnames[n] + # Cleanup. + for dirpath, dirnames, filenames in os.walk(src_dir, topdown=True): + assert not filenames + os.rmdir(dirpath) + + +@contextlib.contextmanager +def disable_info_traces(): + """ + Temporarily disable info traces. + """ + from distutils import log + + saved = log.set_threshold(log.WARN) + try: + yield + finally: + log.set_threshold(saved) + + +class Wheel: + def __init__(self, filename) -> None: + match = WHEEL_NAME(os.path.basename(filename)) + if match is None: + raise ValueError(f'invalid wheel name: {filename!r}') + self.filename = filename + for k, v in match.groupdict().items(): + setattr(self, k, v) + + def tags(self): + """List tags (py_version, abi, platform) supported by this wheel.""" + return itertools.product( + self.py_version.split('.'), + self.abi.split('.'), + self.platform.split('.'), + ) + + def is_compatible(self): + """Is the wheel compatible with the current platform?""" + return next((True for t in self.tags() if t in _get_supported_tags()), False) + + def egg_name(self): + return ( + _egg_basename( + self.project_name, + self.version, + platform=(None if self.platform == 'any' else get_platform()), + ) + + ".egg" + ) + + def get_dist_info(self, zf): + # find the correct name of the .dist-info dir in the wheel file + for member in zf.namelist(): + dirname = posixpath.dirname(member) + if dirname.endswith('.dist-info') and canonicalize_name(dirname).startswith( + canonicalize_name(self.project_name) + ): + return dirname + raise ValueError("unsupported wheel format. .dist-info not found") + + def install_as_egg(self, destination_eggdir) -> None: + """Install wheel as an egg directory.""" + with zipfile.ZipFile(self.filename) as zf: + self._install_as_egg(destination_eggdir, zf) + + def _install_as_egg(self, destination_eggdir, zf): + dist_basename = f'{self.project_name}-{self.version}' + dist_info = self.get_dist_info(zf) + dist_data = f'{dist_basename}.data' + egg_info = os.path.join(destination_eggdir, 'EGG-INFO') + + self._convert_metadata(zf, destination_eggdir, dist_info, egg_info) + self._move_data_entries(destination_eggdir, dist_data) + self._fix_namespace_packages(egg_info, destination_eggdir) + + @staticmethod + def _convert_metadata(zf, destination_eggdir, dist_info, egg_info): + import pkg_resources + + def get_metadata(name): + with zf.open(posixpath.join(dist_info, name)) as fp: + value = fp.read().decode('utf-8') + return email.parser.Parser().parsestr(value) + + wheel_metadata = get_metadata('WHEEL') + # Check wheel format version is supported. + wheel_version = parse_version(wheel_metadata.get('Wheel-Version')) + wheel_v1 = parse_version('1.0') <= wheel_version < parse_version('2.0dev0') + if not wheel_v1: + raise ValueError(f'unsupported wheel format version: {wheel_version}') + # Extract to target directory. + _unpack_zipfile_obj(zf, destination_eggdir) + # Convert metadata. + dist_info = os.path.join(destination_eggdir, dist_info) + dist = pkg_resources.Distribution.from_location( + destination_eggdir, + dist_info, + metadata=pkg_resources.PathMetadata(destination_eggdir, dist_info), + ) + + # Note: Evaluate and strip markers now, + # as it's difficult to convert back from the syntax: + # foobar; "linux" in sys_platform and extra == 'test' + def raw_req(req): + req.marker = None + return str(req) + + install_requires = list(map(raw_req, dist.requires())) + extras_require = { + extra: [ + req + for req in map(raw_req, dist.requires((extra,))) + if req not in install_requires + ] + for extra in dist.extras + } + os.rename(dist_info, egg_info) + os.rename( + os.path.join(egg_info, 'METADATA'), + os.path.join(egg_info, 'PKG-INFO'), + ) + setup_dist = setuptools.Distribution( + attrs=dict( + install_requires=install_requires, + extras_require=extras_require, + ), + ) + with disable_info_traces(): + write_requirements( + setup_dist.get_command_obj('egg_info'), + None, + os.path.join(egg_info, 'requires.txt'), + ) + + @staticmethod + def _move_data_entries(destination_eggdir, dist_data): + """Move data entries to their correct location.""" + dist_data = os.path.join(destination_eggdir, dist_data) + dist_data_scripts = os.path.join(dist_data, 'scripts') + if os.path.exists(dist_data_scripts): + egg_info_scripts = os.path.join(destination_eggdir, 'EGG-INFO', 'scripts') + os.mkdir(egg_info_scripts) + for entry in os.listdir(dist_data_scripts): + # Remove bytecode, as it's not properly handled + # during easy_install scripts install phase. + if entry.endswith('.pyc'): + os.unlink(os.path.join(dist_data_scripts, entry)) + else: + os.rename( + os.path.join(dist_data_scripts, entry), + os.path.join(egg_info_scripts, entry), + ) + os.rmdir(dist_data_scripts) + for subdir in filter( + os.path.exists, + ( + os.path.join(dist_data, d) + for d in ('data', 'headers', 'purelib', 'platlib') + ), + ): + unpack(subdir, destination_eggdir) + if os.path.exists(dist_data): + os.rmdir(dist_data) + + @staticmethod + def _fix_namespace_packages(egg_info, destination_eggdir): + namespace_packages = os.path.join(egg_info, 'namespace_packages.txt') + if os.path.exists(namespace_packages): + namespace_packages = _read_utf8_with_fallback(namespace_packages).split() + + for mod in namespace_packages: + mod_dir = os.path.join(destination_eggdir, *mod.split('.')) + mod_init = os.path.join(mod_dir, '__init__.py') + if not os.path.exists(mod_dir): + os.mkdir(mod_dir) + if not os.path.exists(mod_init): + with open(mod_init, 'w', encoding="utf-8") as fp: + fp.write(NAMESPACE_PACKAGE_INIT) diff --git a/venv/lib/python3.11/site-packages/setuptools/windows_support.py b/venv/lib/python3.11/site-packages/setuptools/windows_support.py new file mode 100644 index 0000000000000000000000000000000000000000..7a2b53a291409c66851961a559eb4d69be0f4acc --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/windows_support.py @@ -0,0 +1,30 @@ +import platform + + +def windows_only(func): + if platform.system() != 'Windows': + return lambda *args, **kwargs: None + return func + + +@windows_only +def hide_file(path: str) -> None: + """ + Set the hidden attribute on a file or directory. + + From https://stackoverflow.com/questions/19622133/ + + `path` must be text. + """ + import ctypes + import ctypes.wintypes + + SetFileAttributes = ctypes.windll.kernel32.SetFileAttributesW + SetFileAttributes.argtypes = ctypes.wintypes.LPWSTR, ctypes.wintypes.DWORD + SetFileAttributes.restype = ctypes.wintypes.BOOL + + FILE_ATTRIBUTE_HIDDEN = 0x02 + + ret = SetFileAttributes(path, FILE_ATTRIBUTE_HIDDEN) + if not ret: + raise ctypes.WinError() diff --git a/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/LICENSE b/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..b9077766e9b9bdcae49ea5c8fced750ed13ec8f7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2018, Tzu-ping Chung + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/METADATA b/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..52118f1e5c83bd7ef39196a749651fc87d176812 --- /dev/null +++ b/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/METADATA @@ -0,0 +1,106 @@ +Metadata-Version: 2.1 +Name: shellingham +Version: 1.5.4 +Summary: Tool to Detect Surrounding Shell +Home-page: https://github.com/sarugaku/shellingham +Author: Tzu-ping Chung +Author-email: uranusjr@gmail.com +License: ISC License +Keywords: shell +Classifier: Development Status :: 3 - Alpha +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: ISC License (ISCL) +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.7 +Description-Content-Type: text/x-rst +License-File: LICENSE + +============================================= +Shellingham: Tool to Detect Surrounding Shell +============================================= + +.. image:: https://img.shields.io/pypi/v/shellingham.svg + :target: https://pypi.org/project/shellingham/ + +Shellingham detects what shell the current Python executable is running in. + + +Usage +===== + +.. code-block:: python + + >>> import shellingham + >>> shellingham.detect_shell() + ('bash', '/bin/bash') + +``detect_shell`` pokes around the process's running environment to determine +what shell it is run in. It returns a 2-tuple: + +* The shell name, always lowercased. +* The command used to run the shell. + +``ShellDetectionFailure`` is raised if ``detect_shell`` fails to detect the +surrounding shell. + + +Notes +===== + +* The shell name is always lowercased. +* On Windows, the shell name is the name of the executable, minus the file + extension. + + +Notes for Application Developers +================================ + +Remember, your application's user is not necessarily using a shell. +Shellingham raises ``ShellDetectionFailure`` if there is no shell to detect, +but *your application should almost never do this to your user*. + +A practical approach to this is to wrap ``detect_shell`` in a try block, and +provide a sane default on failure + +.. code-block:: python + + try: + shell = shellingham.detect_shell() + except shellingham.ShellDetectionFailure: + shell = provide_default() + + +There are a few choices for you to choose from. + +* The POSIX standard mandates the environment variable ``SHELL`` to refer to + "the user's preferred command language interpreter". This is always available + (even if the user is not in an interactive session), and likely the correct + choice to launch an interactive sub-shell with. +* A command ``sh`` is almost guaranteed to exist, likely at ``/bin/sh``, since + several POSIX tools rely on it. This should be suitable if you want to run a + (possibly non-interactive) script. +* All versions of DOS and Windows have an environment variable ``COMSPEC``. + This can always be used to launch a usable command prompt (e.g. `cmd.exe` on + Windows). + +Here's a simple implementation to provide a default shell + +.. code-block:: python + + import os + + def provide_default(): + if os.name == 'posix': + return os.environ['SHELL'] + elif os.name == 'nt': + return os.environ['COMSPEC'] + raise NotImplementedError(f'OS {os.name!r} support not available') diff --git a/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/RECORD b/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..d3ba8380cb85c0f0347a35010e93735be83431ed --- /dev/null +++ b/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/RECORD @@ -0,0 +1,21 @@ +shellingham-1.5.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +shellingham-1.5.4.dist-info/LICENSE,sha256=84j9OMrRMRLB3A9mm76A5_hFQe26-3LzAw0sp2QsPJ0,751 +shellingham-1.5.4.dist-info/METADATA,sha256=GD2AIgo3STJieVc53TV8xbs_Sb05DMkZjVGA5UUaB_o,3461 +shellingham-1.5.4.dist-info/RECORD,, +shellingham-1.5.4.dist-info/WHEEL,sha256=iYlv5fX357PQyRT2o6tw1bN-YcKFFHKqB_LwHO5wP-g,110 +shellingham-1.5.4.dist-info/top_level.txt,sha256=uKMQL5AKxPi4O9_Rbd838QeEs4ImpGQKNbEDZYqgBgk,12 +shellingham-1.5.4.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 +shellingham/__init__.py,sha256=pAKXUPKUdwyErC0ZjS-5w-fRdSbmdcfvnpt_x1yWqtA,635 +shellingham/__pycache__/__init__.cpython-311.pyc,, +shellingham/__pycache__/_core.cpython-311.pyc,, +shellingham/__pycache__/nt.cpython-311.pyc,, +shellingham/_core.py,sha256=v-CTr_7F7cJAtNnzpa1N_Hl8afkY5yiDA4joGmsUBu0,300 +shellingham/nt.py,sha256=m6J6SuwyqVVlxXT9Bc-9F_1x-T5u0gCFFrRAF2LIkeg,4516 +shellingham/posix/__init__.py,sha256=pB69qtvZJ_yIf48nl4-ZfS3wLwwuXuknXOZhBnC2T1o,3129 +shellingham/posix/__pycache__/__init__.cpython-311.pyc,, +shellingham/posix/__pycache__/_core.cpython-311.pyc,, +shellingham/posix/__pycache__/proc.cpython-311.pyc,, +shellingham/posix/__pycache__/ps.cpython-311.pyc,, +shellingham/posix/_core.py,sha256=_v18UaXbzr4muNhr3-mH1FdSdjZ_dOXQrtUyomIbKYQ,81 +shellingham/posix/proc.py,sha256=nSUxIuQSotvaDW76i0oTQAM9aZ9PXBLFAEktWljSKCo,2659 +shellingham/posix/ps.py,sha256=NGmDKCukhNp0lahwYCaMXphBYaVbhbiR9BtE0OkT8qU,1770 diff --git a/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/WHEEL b/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..c34f1162ef9a50c355df1261ef6194ffc1b39975 --- /dev/null +++ b/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.2) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/top_level.txt b/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..d4e44ce0299bb38463f8491ec8850910235c2709 --- /dev/null +++ b/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/top_level.txt @@ -0,0 +1 @@ +shellingham diff --git a/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/zip-safe b/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/zip-safe new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/venv/lib/python3.11/site-packages/shellingham-1.5.4.dist-info/zip-safe @@ -0,0 +1 @@ + diff --git a/venv/lib/python3.11/site-packages/shellingham/__init__.py b/venv/lib/python3.11/site-packages/shellingham/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..15f7a90cbd02e5c2cc933cf6aa0374cca68035f1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/shellingham/__init__.py @@ -0,0 +1,23 @@ +import importlib +import os + +from ._core import ShellDetectionFailure + +__version__ = "1.5.4" + + +def detect_shell(pid=None, max_depth=10): + name = os.name + try: + impl = importlib.import_module(".{}".format(name), __name__) + except ImportError: + message = "Shell detection not implemented for {0!r}".format(name) + raise RuntimeError(message) + try: + get_shell = impl.get_shell + except AttributeError: + raise RuntimeError("get_shell not implemented for {0!r}".format(name)) + shell = get_shell(pid, max_depth=max_depth) + if shell: + return shell + raise ShellDetectionFailure() diff --git a/venv/lib/python3.11/site-packages/shellingham/_core.py b/venv/lib/python3.11/site-packages/shellingham/_core.py new file mode 100644 index 0000000000000000000000000000000000000000..13b65417c733b54e48b120e37f573c2baa6ef72b --- /dev/null +++ b/venv/lib/python3.11/site-packages/shellingham/_core.py @@ -0,0 +1,11 @@ +SHELL_NAMES = ( + {"sh", "bash", "dash", "ash"} # Bourne. + | {"csh", "tcsh"} # C. + | {"ksh", "zsh", "fish"} # Common alternatives. + | {"cmd", "powershell", "pwsh"} # Microsoft. + | {"elvish", "xonsh", "nu"} # More exotic. +) + + +class ShellDetectionFailure(EnvironmentError): + pass diff --git a/venv/lib/python3.11/site-packages/shellingham/nt.py b/venv/lib/python3.11/site-packages/shellingham/nt.py new file mode 100644 index 0000000000000000000000000000000000000000..389551b223a761fa2f97e929b60bf3ca5baed94c --- /dev/null +++ b/venv/lib/python3.11/site-packages/shellingham/nt.py @@ -0,0 +1,163 @@ +import contextlib +import ctypes +import os + +from ctypes.wintypes import ( + BOOL, + CHAR, + DWORD, + HANDLE, + LONG, + LPWSTR, + MAX_PATH, + PDWORD, + ULONG, +) + +from shellingham._core import SHELL_NAMES + + +INVALID_HANDLE_VALUE = HANDLE(-1).value +ERROR_NO_MORE_FILES = 18 +ERROR_INSUFFICIENT_BUFFER = 122 +TH32CS_SNAPPROCESS = 2 +PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + + +kernel32 = ctypes.windll.kernel32 + + +def _check_handle(error_val=0): + def check(ret, func, args): + if ret == error_val: + raise ctypes.WinError() + return ret + + return check + + +def _check_expected(expected): + def check(ret, func, args): + if ret: + return True + code = ctypes.GetLastError() + if code == expected: + return False + raise ctypes.WinError(code) + + return check + + +class ProcessEntry32(ctypes.Structure): + _fields_ = ( + ("dwSize", DWORD), + ("cntUsage", DWORD), + ("th32ProcessID", DWORD), + ("th32DefaultHeapID", ctypes.POINTER(ULONG)), + ("th32ModuleID", DWORD), + ("cntThreads", DWORD), + ("th32ParentProcessID", DWORD), + ("pcPriClassBase", LONG), + ("dwFlags", DWORD), + ("szExeFile", CHAR * MAX_PATH), + ) + + +kernel32.CloseHandle.argtypes = [HANDLE] +kernel32.CloseHandle.restype = BOOL + +kernel32.CreateToolhelp32Snapshot.argtypes = [DWORD, DWORD] +kernel32.CreateToolhelp32Snapshot.restype = HANDLE +kernel32.CreateToolhelp32Snapshot.errcheck = _check_handle( # type: ignore + INVALID_HANDLE_VALUE, +) + +kernel32.Process32First.argtypes = [HANDLE, ctypes.POINTER(ProcessEntry32)] +kernel32.Process32First.restype = BOOL +kernel32.Process32First.errcheck = _check_expected( # type: ignore + ERROR_NO_MORE_FILES, +) + +kernel32.Process32Next.argtypes = [HANDLE, ctypes.POINTER(ProcessEntry32)] +kernel32.Process32Next.restype = BOOL +kernel32.Process32Next.errcheck = _check_expected( # type: ignore + ERROR_NO_MORE_FILES, +) + +kernel32.GetCurrentProcessId.argtypes = [] +kernel32.GetCurrentProcessId.restype = DWORD + +kernel32.OpenProcess.argtypes = [DWORD, BOOL, DWORD] +kernel32.OpenProcess.restype = HANDLE +kernel32.OpenProcess.errcheck = _check_handle( # type: ignore + INVALID_HANDLE_VALUE, +) + +kernel32.QueryFullProcessImageNameW.argtypes = [HANDLE, DWORD, LPWSTR, PDWORD] +kernel32.QueryFullProcessImageNameW.restype = BOOL +kernel32.QueryFullProcessImageNameW.errcheck = _check_expected( # type: ignore + ERROR_INSUFFICIENT_BUFFER, +) + + +@contextlib.contextmanager +def _handle(f, *args, **kwargs): + handle = f(*args, **kwargs) + try: + yield handle + finally: + kernel32.CloseHandle(handle) + + +def _iter_processes(): + f = kernel32.CreateToolhelp32Snapshot + with _handle(f, TH32CS_SNAPPROCESS, 0) as snap: + entry = ProcessEntry32() + entry.dwSize = ctypes.sizeof(entry) + ret = kernel32.Process32First(snap, entry) + while ret: + yield entry + ret = kernel32.Process32Next(snap, entry) + + +def _get_full_path(proch): + size = DWORD(MAX_PATH) + while True: + path_buff = ctypes.create_unicode_buffer("", size.value) + if kernel32.QueryFullProcessImageNameW(proch, 0, path_buff, size): + return path_buff.value + size.value *= 2 + + +def get_shell(pid=None, max_depth=10): + proc_map = { + proc.th32ProcessID: (proc.th32ParentProcessID, proc.szExeFile) + for proc in _iter_processes() + } + pid = pid or os.getpid() + + for _ in range(0, max_depth + 1): + try: + ppid, executable = proc_map[pid] + except KeyError: # No such process? Give up. + break + + # The executable name would be encoded with the current code page if + # we're in ANSI mode (usually). Try to decode it into str/unicode, + # replacing invalid characters to be safe (not thoeratically necessary, + # I think). Note that we need to use 'mbcs' instead of encoding + # settings from sys because this is from the Windows API, not Python + # internals (which those settings reflect). (pypa/pipenv#3382) + if isinstance(executable, bytes): + executable = executable.decode("mbcs", "replace") + + name = executable.rpartition(".")[0].lower() + if name not in SHELL_NAMES: + pid = ppid + continue + + key = PROCESS_QUERY_LIMITED_INFORMATION + with _handle(kernel32.OpenProcess, key, 0, pid) as proch: + return (name, _get_full_path(proch)) + + return None diff --git a/venv/lib/python3.11/site-packages/shellingham/posix/__init__.py b/venv/lib/python3.11/site-packages/shellingham/posix/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5bd2070db27189e62a1867e4de49f16f8c8841ff --- /dev/null +++ b/venv/lib/python3.11/site-packages/shellingham/posix/__init__.py @@ -0,0 +1,112 @@ +import os +import re + +from .._core import SHELL_NAMES, ShellDetectionFailure +from . import proc, ps + +# Based on QEMU docs: https://www.qemu.org/docs/master/user/main.html +QEMU_BIN_REGEX = re.compile( + r"""qemu- + (alpha + |armeb + |arm + |m68k + |cris + |i386 + |x86_64 + |microblaze + |mips + |mipsel + |mips64 + |mips64el + |mipsn32 + |mipsn32el + |nios2 + |ppc64 + |ppc + |sh4eb + |sh4 + |sparc + |sparc32plus + |sparc64 + )""", + re.VERBOSE, +) + + +def _iter_process_parents(pid, max_depth=10): + """Select a way to obtain process information from the system. + + * `/proc` is used if supported. + * The system `ps` utility is used as a fallback option. + """ + for impl in (proc, ps): + try: + iterator = impl.iter_process_parents(pid, max_depth) + except EnvironmentError: + continue + return iterator + raise ShellDetectionFailure("compatible proc fs or ps utility is required") + + +def _get_login_shell(proc_cmd): + """Form shell information from SHELL environ if possible.""" + login_shell = os.environ.get("SHELL", "") + if login_shell: + proc_cmd = login_shell + else: + proc_cmd = proc_cmd[1:] + return (os.path.basename(proc_cmd).lower(), proc_cmd) + + +_INTERPRETER_SHELL_NAMES = [ + (re.compile(r"^python(\d+(\.\d+)?)?$"), {"xonsh"}), +] + + +def _get_interpreter_shell(proc_name, proc_args): + """Get shell invoked via an interpreter. + + Some shells are implemented on, and invoked with an interpreter, e.g. xonsh + is commonly executed with an executable Python script. This detects what + script the interpreter is actually running, and check whether that looks + like a shell. + + See sarugaku/shellingham#26 for rational. + """ + for pattern, shell_names in _INTERPRETER_SHELL_NAMES: + if not pattern.match(proc_name): + continue + for arg in proc_args: + name = os.path.basename(arg).lower() + if os.path.isfile(arg) and name in shell_names: + return (name, arg) + return None + + +def _get_shell(cmd, *args): + if cmd.startswith("-"): # Login shell! Let's use this. + return _get_login_shell(cmd) + name = os.path.basename(cmd).lower() + if name == "rosetta" or QEMU_BIN_REGEX.fullmatch(name): + # If the current process is Rosetta or QEMU, this likely is a + # containerized process. Parse out the actual command instead. + cmd = args[0] + args = args[1:] + name = os.path.basename(cmd).lower() + if name in SHELL_NAMES: # Command looks like a shell. + return (name, cmd) + shell = _get_interpreter_shell(name, args) + if shell: + return shell + return None + + +def get_shell(pid=None, max_depth=10): + """Get the shell that the supplied pid or os.getpid() is running in.""" + pid = str(pid or os.getpid()) + for proc_args, _, _ in _iter_process_parents(pid, max_depth): + shell = _get_shell(*proc_args) + if shell: + return shell + return None diff --git a/venv/lib/python3.11/site-packages/shellingham/posix/_core.py b/venv/lib/python3.11/site-packages/shellingham/posix/_core.py new file mode 100644 index 0000000000000000000000000000000000000000..adc49e6e7a9d3edf062c55e0078136899f78d30d --- /dev/null +++ b/venv/lib/python3.11/site-packages/shellingham/posix/_core.py @@ -0,0 +1,3 @@ +import collections + +Process = collections.namedtuple("Process", "args pid ppid") diff --git a/venv/lib/python3.11/site-packages/shellingham/posix/proc.py b/venv/lib/python3.11/site-packages/shellingham/posix/proc.py new file mode 100644 index 0000000000000000000000000000000000000000..950f63228e5b328f82b70da8851ec60c6a2ff029 --- /dev/null +++ b/venv/lib/python3.11/site-packages/shellingham/posix/proc.py @@ -0,0 +1,83 @@ +import io +import os +import re +import sys + +from ._core import Process + +# FreeBSD: https://www.freebsd.org/cgi/man.cgi?query=procfs +# NetBSD: https://man.netbsd.org/NetBSD-9.3-STABLE/mount_procfs.8 +# DragonFlyBSD: https://www.dragonflybsd.org/cgi/web-man?command=procfs +BSD_STAT_PPID = 2 + +# See https://docs.kernel.org/filesystems/proc.html +LINUX_STAT_PPID = 3 + +STAT_PATTERN = re.compile(r"\(.+\)|\S+") + + +def detect_proc(): + """Detect /proc filesystem style. + + This checks the /proc/{pid} directory for possible formats. Returns one of + the following as str: + + * `stat`: Linux-style, i.e. ``/proc/{pid}/stat``. + * `status`: BSD-style, i.e. ``/proc/{pid}/status``. + """ + pid = os.getpid() + for name in ("stat", "status"): + if os.path.exists(os.path.join("/proc", str(pid), name)): + return name + raise ProcFormatError("unsupported proc format") + + +def _use_bsd_stat_format(): + try: + return os.uname().sysname.lower() in ("freebsd", "netbsd", "dragonfly") + except Exception: + return False + + +def _get_ppid(pid, name): + path = os.path.join("/proc", str(pid), name) + with io.open(path, encoding="ascii", errors="replace") as f: + parts = STAT_PATTERN.findall(f.read()) + # We only care about TTY and PPID -- both are numbers. + if _use_bsd_stat_format(): + return parts[BSD_STAT_PPID] + return parts[LINUX_STAT_PPID] + + +def _get_cmdline(pid): + path = os.path.join("/proc", str(pid), "cmdline") + encoding = sys.getfilesystemencoding() or "utf-8" + with io.open(path, encoding=encoding, errors="replace") as f: + # XXX: Command line arguments can be arbitrary byte sequences, not + # necessarily decodable. For Shellingham's purpose, however, we don't + # care. (pypa/pipenv#2820) + # cmdline appends an extra NULL at the end, hence the [:-1]. + return tuple(f.read().split("\0")[:-1]) + + +class ProcFormatError(EnvironmentError): + pass + + +def iter_process_parents(pid, max_depth=10): + """Try to look up the process tree via the /proc interface.""" + stat_name = detect_proc() + + # Inner generator function so we correctly throw an error eagerly if proc + # is not supported, rather than on the first call to the iterator. This + # allows the call site detects the correct implementation. + def _iter_process_parents(pid, max_depth): + for _ in range(max_depth): + ppid = _get_ppid(pid, stat_name) + args = _get_cmdline(pid) + yield Process(args=args, pid=pid, ppid=ppid) + if ppid == "0": + break + pid = ppid + + return _iter_process_parents(pid, max_depth) diff --git a/venv/lib/python3.11/site-packages/shellingham/posix/ps.py b/venv/lib/python3.11/site-packages/shellingham/posix/ps.py new file mode 100644 index 0000000000000000000000000000000000000000..3bc39a74a56390c263e63bfead028f6bce4df3cb --- /dev/null +++ b/venv/lib/python3.11/site-packages/shellingham/posix/ps.py @@ -0,0 +1,51 @@ +import errno +import subprocess +import sys + +from ._core import Process + + +class PsNotAvailable(EnvironmentError): + pass + + +def iter_process_parents(pid, max_depth=10): + """Try to look up the process tree via the output of `ps`.""" + try: + cmd = ["ps", "-ww", "-o", "pid=", "-o", "ppid=", "-o", "args="] + output = subprocess.check_output(cmd) + except OSError as e: # Python 2-compatible FileNotFoundError. + if e.errno != errno.ENOENT: + raise + raise PsNotAvailable("ps not found") + except subprocess.CalledProcessError as e: + # `ps` can return 1 if the process list is completely empty. + # (sarugaku/shellingham#15) + if not e.output.strip(): + return + raise + if not isinstance(output, str): + encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() + output = output.decode(encoding) + + processes_mapping = {} + for line in output.split("\n"): + try: + _pid, ppid, args = line.strip().split(None, 2) + # XXX: This is not right, but we are really out of options. + # ps does not offer a sane way to decode the argument display, + # and this is "Good Enough" for obtaining shell names. Hopefully + # people don't name their shell with a space, or have something + # like "/usr/bin/xonsh is uber". (sarugaku/shellingham#14) + args = tuple(a.strip() for a in args.split(" ")) + except ValueError: + continue + processes_mapping[_pid] = Process(args=args, pid=_pid, ppid=ppid) + + for _ in range(max_depth): + try: + process = processes_mapping[pid] + except KeyError: + return + yield process + pid = process.ppid diff --git a/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/LICENSE b/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..1cc22a5aa7679ebaa10934212f356823931bdc3e --- /dev/null +++ b/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/LICENSE @@ -0,0 +1,18 @@ +Copyright (c) 2010-2024 Benjamin Peterson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/METADATA b/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..cfde03c2631c5c1d5cdc0949d0ee3379e7110f0e --- /dev/null +++ b/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/METADATA @@ -0,0 +1,43 @@ +Metadata-Version: 2.1 +Name: six +Version: 1.17.0 +Summary: Python 2 and 3 compatibility utilities +Home-page: https://github.com/benjaminp/six +Author: Benjamin Peterson +Author-email: benjamin@python.org +License: MIT +Classifier: Development Status :: 5 - Production/Stable +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Utilities +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.* +License-File: LICENSE + +.. image:: https://img.shields.io/pypi/v/six.svg + :target: https://pypi.org/project/six/ + :alt: six on PyPI + +.. image:: https://readthedocs.org/projects/six/badge/?version=latest + :target: https://six.readthedocs.io/ + :alt: six's documentation on Read the Docs + +.. image:: https://img.shields.io/badge/license-MIT-green.svg + :target: https://github.com/benjaminp/six/blob/master/LICENSE + :alt: MIT License badge + +Six is a Python 2 and 3 compatibility library. It provides utility functions +for smoothing over the differences between the Python versions with the goal of +writing Python code that is compatible on both Python versions. See the +documentation for more information on what is provided. + +Six supports Python 2.7 and 3.3+. It is contained in only one Python +file, so it can be easily copied into your project. (The copyright and license +notice must be retained.) + +Online documentation is at https://six.readthedocs.io/. + +Bugs can be reported to https://github.com/benjaminp/six. The code can also +be found there. diff --git a/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/RECORD b/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..bcd83a187f36ed78dee3d4a7238b5767978aa473 --- /dev/null +++ b/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/RECORD @@ -0,0 +1,8 @@ +__pycache__/six.cpython-311.pyc,, +six-1.17.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +six-1.17.0.dist-info/LICENSE,sha256=Q3W6IOK5xsTnytKUCmKP2Q6VzD1Q7pKq51VxXYuh-9A,1066 +six-1.17.0.dist-info/METADATA,sha256=ViBCB4wnUlSfbYp8htvF3XCAiKe-bYBnLsewcQC3JGg,1658 +six-1.17.0.dist-info/RECORD,, +six-1.17.0.dist-info/WHEEL,sha256=pxeNX5JdtCe58PUSYP9upmc7jdRPgvT0Gm9kb1SHlVw,109 +six-1.17.0.dist-info/top_level.txt,sha256=_iVH_iYEtEXnD8nYGQYpYFUvkUW9sEO1GYbkeKSAais,4 +six.py,sha256=xRyR9wPT1LNpbJI8tf7CE-BeddkhU5O--sfy-mo5BN8,34703 diff --git a/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/WHEEL b/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..104f3874635f24f0d2918dfeaf6a59652274460c --- /dev/null +++ b/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: setuptools (75.6.0) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/top_level.txt b/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..ffe2fce498955b628014618b28c6bcf152466a4a --- /dev/null +++ b/venv/lib/python3.11/site-packages/six-1.17.0.dist-info/top_level.txt @@ -0,0 +1 @@ +six diff --git a/venv/lib/python3.11/site-packages/six.py b/venv/lib/python3.11/site-packages/six.py new file mode 100644 index 0000000000000000000000000000000000000000..3de5969b1ad3b973342e5e88ee1770fa7c798152 --- /dev/null +++ b/venv/lib/python3.11/site-packages/six.py @@ -0,0 +1,1003 @@ +# Copyright (c) 2010-2024 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Utilities for writing code that runs on Python 2 and 3""" + +from __future__ import absolute_import + +import functools +import itertools +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.17.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 +PY34 = sys.version_info[0:2] >= (3, 4) + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + +if PY34: + from importlib.util import spec_from_loader +else: + spec_from_loader = None + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) # Invokes __set__. + try: + # This is a bit ugly, but it avoids running this again by + # removing this descriptor. + delattr(obj.__class__, self.name) + except AttributeError: + pass + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + def __getattr__(self, attr): + _module = self._resolve() + value = getattr(_module, attr) + setattr(self, attr, value) + return value + + +class _LazyModule(types.ModuleType): + + def __init__(self, name): + super(_LazyModule, self).__init__(name) + self.__doc__ = self.__class__.__doc__ + + def __dir__(self): + attrs = ["__doc__", "__name__"] + attrs += [attr.name for attr in self._moved_attributes] + return attrs + + # Subclasses should override this + _moved_attributes = [] + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + +class _SixMetaPathImporter(object): + + """ + A meta path importer to import six.moves and its submodules. + + This class implements a PEP302 finder and loader. It should be compatible + with Python 2.5 and all existing versions of Python3 + """ + + def __init__(self, six_module_name): + self.name = six_module_name + self.known_modules = {} + + def _add_module(self, mod, *fullnames): + for fullname in fullnames: + self.known_modules[self.name + "." + fullname] = mod + + def _get_module(self, fullname): + return self.known_modules[self.name + "." + fullname] + + def find_module(self, fullname, path=None): + if fullname in self.known_modules: + return self + return None + + def find_spec(self, fullname, path, target=None): + if fullname in self.known_modules: + return spec_from_loader(fullname, self) + return None + + def __get_module(self, fullname): + try: + return self.known_modules[fullname] + except KeyError: + raise ImportError("This loader does not know module " + fullname) + + def load_module(self, fullname): + try: + # in case of a reload + return sys.modules[fullname] + except KeyError: + pass + mod = self.__get_module(fullname) + if isinstance(mod, MovedModule): + mod = mod._resolve() + else: + mod.__loader__ = self + sys.modules[fullname] = mod + return mod + + def is_package(self, fullname): + """ + Return true, if the named module is a package. + + We need this method to get correct spec objects with + Python 3.4 (see PEP451) + """ + return hasattr(self.__get_module(fullname), "__path__") + + def get_code(self, fullname): + """Return None + + Required, if is_package is implemented""" + self.__get_module(fullname) # eventually raises ImportError + return None + get_source = get_code # same as get_code + + def create_module(self, spec): + return self.load_module(spec.name) + + def exec_module(self, module): + pass + +_importer = _SixMetaPathImporter(__name__) + + +class _MovedItems(_LazyModule): + + """Lazy loading of moved objects""" + __path__ = [] # mark as package + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("intern", "__builtin__", "sys"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), + MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), + MovedAttribute("getoutput", "commands", "subprocess"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserDict", "UserDict", "collections", "IterableUserDict", "UserDict"), + MovedAttribute("UserList", "UserList", "collections"), + MovedAttribute("UserString", "UserString", "collections"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("collections_abc", "collections", "collections.abc" if sys.version_info >= (3, 3) else "collections"), + MovedModule("copyreg", "copy_reg"), + MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("dbm_ndbm", "dbm", "dbm.ndbm"), + MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread" if sys.version_info < (3, 9) else "_thread"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("_thread", "thread", "_thread"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), + MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), +] +# Add windows specific modules. +if sys.platform == "win32": + _moved_attributes += [ + MovedModule("winreg", "_winreg"), + ] + +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) + if isinstance(attr, MovedModule): + _importer._add_module(attr, "moves." + attr.name) +del attr + +_MovedItems._moved_attributes = _moved_attributes + +moves = _MovedItems(__name__ + ".moves") +_importer._add_module(moves, "moves") + + +class Module_six_moves_urllib_parse(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_parse""" + + +_urllib_parse_moved_attributes = [ + MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("SplitResult", "urlparse", "urllib.parse"), + MovedAttribute("parse_qs", "urlparse", "urllib.parse"), + MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), + MovedAttribute("urldefrag", "urlparse", "urllib.parse"), + MovedAttribute("urljoin", "urlparse", "urllib.parse"), + MovedAttribute("urlparse", "urlparse", "urllib.parse"), + MovedAttribute("urlsplit", "urlparse", "urllib.parse"), + MovedAttribute("urlunparse", "urlparse", "urllib.parse"), + MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), + MovedAttribute("quote", "urllib", "urllib.parse"), + MovedAttribute("quote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote", "urllib", "urllib.parse"), + MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes"), + MovedAttribute("urlencode", "urllib", "urllib.parse"), + MovedAttribute("splitquery", "urllib", "urllib.parse"), + MovedAttribute("splittag", "urllib", "urllib.parse"), + MovedAttribute("splituser", "urllib", "urllib.parse"), + MovedAttribute("splitvalue", "urllib", "urllib.parse"), + MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), + MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), + MovedAttribute("uses_params", "urlparse", "urllib.parse"), + MovedAttribute("uses_query", "urlparse", "urllib.parse"), + MovedAttribute("uses_relative", "urlparse", "urllib.parse"), +] +for attr in _urllib_parse_moved_attributes: + setattr(Module_six_moves_urllib_parse, attr.name, attr) +del attr + +Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes + +_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), + "moves.urllib_parse", "moves.urllib.parse") + + +class Module_six_moves_urllib_error(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_error""" + + +_urllib_error_moved_attributes = [ + MovedAttribute("URLError", "urllib2", "urllib.error"), + MovedAttribute("HTTPError", "urllib2", "urllib.error"), + MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +] +for attr in _urllib_error_moved_attributes: + setattr(Module_six_moves_urllib_error, attr.name, attr) +del attr + +Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes + +_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), + "moves.urllib_error", "moves.urllib.error") + + +class Module_six_moves_urllib_request(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_request""" + + +_urllib_request_moved_attributes = [ + MovedAttribute("urlopen", "urllib2", "urllib.request"), + MovedAttribute("install_opener", "urllib2", "urllib.request"), + MovedAttribute("build_opener", "urllib2", "urllib.request"), + MovedAttribute("pathname2url", "urllib", "urllib.request"), + MovedAttribute("url2pathname", "urllib", "urllib.request"), + MovedAttribute("getproxies", "urllib", "urllib.request"), + MovedAttribute("Request", "urllib2", "urllib.request"), + MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), + MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), + MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), + MovedAttribute("BaseHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), + MovedAttribute("FileHandler", "urllib2", "urllib.request"), + MovedAttribute("FTPHandler", "urllib2", "urllib.request"), + MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), + MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), + MovedAttribute("urlretrieve", "urllib", "urllib.request"), + MovedAttribute("urlcleanup", "urllib", "urllib.request"), + MovedAttribute("proxy_bypass", "urllib", "urllib.request"), + MovedAttribute("parse_http_list", "urllib2", "urllib.request"), + MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"), +] +if sys.version_info[:2] < (3, 14): + _urllib_request_moved_attributes.extend( + [ + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), + ] + ) +for attr in _urllib_request_moved_attributes: + setattr(Module_six_moves_urllib_request, attr.name, attr) +del attr + +Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes + +_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), + "moves.urllib_request", "moves.urllib.request") + + +class Module_six_moves_urllib_response(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_response""" + + +_urllib_response_moved_attributes = [ + MovedAttribute("addbase", "urllib", "urllib.response"), + MovedAttribute("addclosehook", "urllib", "urllib.response"), + MovedAttribute("addinfo", "urllib", "urllib.response"), + MovedAttribute("addinfourl", "urllib", "urllib.response"), +] +for attr in _urllib_response_moved_attributes: + setattr(Module_six_moves_urllib_response, attr.name, attr) +del attr + +Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes + +_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), + "moves.urllib_response", "moves.urllib.response") + + +class Module_six_moves_urllib_robotparser(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_robotparser""" + + +_urllib_robotparser_moved_attributes = [ + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +] +for attr in _urllib_robotparser_moved_attributes: + setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +del attr + +Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes + +_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), + "moves.urllib_robotparser", "moves.urllib.robotparser") + + +class Module_six_moves_urllib(types.ModuleType): + + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + __path__ = [] # mark as package + parse = _importer._get_module("moves.urllib_parse") + error = _importer._get_module("moves.urllib_error") + request = _importer._get_module("moves.urllib_request") + response = _importer._get_module("moves.urllib_response") + robotparser = _importer._get_module("moves.urllib_robotparser") + + def __dir__(self): + return ['parse', 'error', 'request', 'response', 'robotparser'] + +_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), + "moves.urllib") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + def create_unbound_method(func, cls): + return func + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + def create_unbound_method(func, cls): + return types.MethodType(func, None, cls) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +if PY3: + def iterkeys(d, **kw): + return iter(d.keys(**kw)) + + def itervalues(d, **kw): + return iter(d.values(**kw)) + + def iteritems(d, **kw): + return iter(d.items(**kw)) + + def iterlists(d, **kw): + return iter(d.lists(**kw)) + + viewkeys = operator.methodcaller("keys") + + viewvalues = operator.methodcaller("values") + + viewitems = operator.methodcaller("items") +else: + def iterkeys(d, **kw): + return d.iterkeys(**kw) + + def itervalues(d, **kw): + return d.itervalues(**kw) + + def iteritems(d, **kw): + return d.iteritems(**kw) + + def iterlists(d, **kw): + return d.iterlists(**kw) + + viewkeys = operator.methodcaller("viewkeys") + + viewvalues = operator.methodcaller("viewvalues") + + viewitems = operator.methodcaller("viewitems") + +_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") +_add_doc(itervalues, "Return an iterator over the values of a dictionary.") +_add_doc(iteritems, + "Return an iterator over the (key, value) pairs of a dictionary.") +_add_doc(iterlists, + "Return an iterator over the (key, [values]) pairs of a dictionary.") + + +if PY3: + def b(s): + return s.encode("latin-1") + + def u(s): + return s + unichr = chr + import struct + int2byte = struct.Struct(">B").pack + del struct + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO + del io + _assertCountEqual = "assertCountEqual" + if sys.version_info[1] <= 1: + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + _assertNotRegex = "assertNotRegexpMatches" + else: + _assertRaisesRegex = "assertRaisesRegex" + _assertRegex = "assertRegex" + _assertNotRegex = "assertNotRegex" +else: + def b(s): + return s + # Workaround for standalone backslash + + def u(s): + return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + unichr = unichr + int2byte = chr + + def byte2int(bs): + return ord(bs[0]) + + def indexbytes(buf, i): + return ord(buf[i]) + iterbytes = functools.partial(itertools.imap, ord) + import StringIO + StringIO = BytesIO = StringIO.StringIO + _assertCountEqual = "assertItemsEqual" + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + _assertNotRegex = "assertNotRegexpMatches" +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +def assertCountEqual(self, *args, **kwargs): + return getattr(self, _assertCountEqual)(*args, **kwargs) + + +def assertRaisesRegex(self, *args, **kwargs): + return getattr(self, _assertRaisesRegex)(*args, **kwargs) + + +def assertRegex(self, *args, **kwargs): + return getattr(self, _assertRegex)(*args, **kwargs) + + +def assertNotRegex(self, *args, **kwargs): + return getattr(self, _assertNotRegex)(*args, **kwargs) + + +if PY3: + exec_ = getattr(moves.builtins, "exec") + + def reraise(tp, value, tb=None): + try: + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + finally: + value = None + tb = None + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + exec_("""def reraise(tp, value, tb=None): + try: + raise tp, value, tb + finally: + tb = None +""") + + +if sys.version_info[:2] > (3,): + exec_("""def raise_from(value, from_value): + try: + raise value from from_value + finally: + value = None +""") +else: + def raise_from(value, from_value): + raise value + + +print_ = getattr(moves.builtins, "print", None) +if print_ is None: + def print_(*args, **kwargs): + """The new-style print function for Python 2.4 and 2.5.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + + def write(data): + if not isinstance(data, basestring): + data = str(data) + # If the file has an encoding, encode unicode with it. + if (isinstance(fp, file) and + isinstance(data, unicode) and + fp.encoding is not None): + errors = getattr(fp, "errors", None) + if errors is None: + errors = "strict" + data = data.encode(fp.encoding, errors) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) +if sys.version_info[:2] < (3, 3): + _print = print_ + + def print_(*args, **kwargs): + fp = kwargs.get("file", sys.stdout) + flush = kwargs.pop("flush", False) + _print(*args, **kwargs) + if flush and fp is not None: + fp.flush() + +_add_doc(reraise, """Reraise an exception.""") + +if sys.version_info[0:2] < (3, 4): + # This does exactly the same what the :func:`py3:functools.update_wrapper` + # function does on Python versions after 3.2. It sets the ``__wrapped__`` + # attribute on ``wrapper`` object and it doesn't raise an error if any of + # the attributes mentioned in ``assigned`` and ``updated`` are missing on + # ``wrapped`` object. + def _update_wrapper(wrapper, wrapped, + assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + for attr in assigned: + try: + value = getattr(wrapped, attr) + except AttributeError: + continue + else: + setattr(wrapper, attr, value) + for attr in updated: + getattr(wrapper, attr).update(getattr(wrapped, attr, {})) + wrapper.__wrapped__ = wrapped + return wrapper + _update_wrapper.__doc__ = functools.update_wrapper.__doc__ + + def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + return functools.partial(_update_wrapper, wrapped=wrapped, + assigned=assigned, updated=updated) + wraps.__doc__ = functools.wraps.__doc__ + +else: + wraps = functools.wraps + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(type): + + def __new__(cls, name, this_bases, d): + if sys.version_info[:2] >= (3, 7): + # This version introduced PEP 560 that requires a bit + # of extra care (we mimic what is done by __build_class__). + resolved_bases = types.resolve_bases(bases) + if resolved_bases is not bases: + d['__orig_bases__'] = bases + else: + resolved_bases = bases + return meta(name, resolved_bases, d) + + @classmethod + def __prepare__(cls, name, this_bases): + return meta.__prepare__(name, bases) + return type.__new__(metaclass, 'temporary_class', (), {}) + + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + if hasattr(cls, '__qualname__'): + orig_vars['__qualname__'] = cls.__qualname__ + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + + +def ensure_binary(s, encoding='utf-8', errors='strict'): + """Coerce **s** to six.binary_type. + + For Python 2: + - `unicode` -> encoded to `str` + - `str` -> `str` + + For Python 3: + - `str` -> encoded to `bytes` + - `bytes` -> `bytes` + """ + if isinstance(s, binary_type): + return s + if isinstance(s, text_type): + return s.encode(encoding, errors) + raise TypeError("not expecting type '%s'" % type(s)) + + +def ensure_str(s, encoding='utf-8', errors='strict'): + """Coerce *s* to `str`. + + For Python 2: + - `unicode` -> encoded to `str` + - `str` -> `str` + + For Python 3: + - `str` -> `str` + - `bytes` -> decoded to `str` + """ + # Optimization: Fast return for the common case. + if type(s) is str: + return s + if PY2 and isinstance(s, text_type): + return s.encode(encoding, errors) + elif PY3 and isinstance(s, binary_type): + return s.decode(encoding, errors) + elif not isinstance(s, (text_type, binary_type)): + raise TypeError("not expecting type '%s'" % type(s)) + return s + + +def ensure_text(s, encoding='utf-8', errors='strict'): + """Coerce *s* to six.text_type. + + For Python 2: + - `unicode` -> `unicode` + - `str` -> `unicode` + + For Python 3: + - `str` -> `str` + - `bytes` -> decoded to `str` + """ + if isinstance(s, binary_type): + return s.decode(encoding, errors) + elif isinstance(s, text_type): + return s + else: + raise TypeError("not expecting type '%s'" % type(s)) + + +def python_2_unicode_compatible(klass): + """ + A class decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if PY2: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + + +# Complete the moves implementation. +# This code is at the end of this module to speed up module loading. +# Turn this module into a package. +__path__ = [] # required for PEP 302 and PEP 451 +__package__ = __name__ # see PEP 366 @ReservedAssignment +if globals().get("__spec__") is not None: + __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable +# Remove other six meta path importers, since they cause problems. This can +# happen if six is removed from sys.modules and then reloaded. (Setuptools does +# this for some reason.) +if sys.meta_path: + for i, importer in enumerate(sys.meta_path): + # Here's some real nastiness: Another "instance" of the six module might + # be floating around. Therefore, we can't use isinstance() to check for + # the six meta path importer, since the other six instance will have + # inserted an importer with different class. + if (type(importer).__name__ == "_SixMetaPathImporter" and + importer.name == __name__): + del sys.meta_path[i] + break + del i, importer +# Finally, add the importer to the meta path import hook. +sys.meta_path.append(_importer) diff --git a/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/METADATA b/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..142a852ddfa283d419425eb9280cce1683755c7a --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/METADATA @@ -0,0 +1,178 @@ +Metadata-Version: 2.4 +Name: starlette +Version: 1.3.1 +Summary: The little ASGI library that shines. +Project-URL: Homepage, https://github.com/Kludex/starlette +Project-URL: Documentation, https://starlette.dev/ +Project-URL: Changelog, https://starlette.dev/release-notes/ +Project-URL: Funding, https://github.com/sponsors/Kludex +Project-URL: Source, https://github.com/Kludex/starlette +Author-email: Tom Christie +Maintainer-email: Marcelo Trylesinski +License-Expression: BSD-3-Clause +License-File: LICENSE.md +Classifier: Development Status :: 3 - Alpha +Classifier: Environment :: Web Environment +Classifier: Framework :: AnyIO +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Topic :: Internet :: WWW/HTTP +Requires-Python: >=3.10 +Requires-Dist: anyio<5,>=3.6.2 +Requires-Dist: typing-extensions>=4.10.0; python_version < '3.13' +Provides-Extra: full +Requires-Dist: httpx2>=2.0.0; extra == 'full' +Requires-Dist: httpx<0.29.0,>=0.27.0; extra == 'full' +Requires-Dist: itsdangerous; extra == 'full' +Requires-Dist: jinja2; extra == 'full' +Requires-Dist: python-multipart>=0.0.18; extra == 'full' +Requires-Dist: pyyaml; extra == 'full' +Description-Content-Type: text/markdown + +

+ + + + starlette-logo + +

+ +

+ ✨ The little ASGI framework that shines. ✨ +

+ +--- + +[![Build Status](https://github.com/Kludex/starlette/workflows/Test%20Suite/badge.svg)](https://github.com/Kludex/starlette/actions) +[![Package version](https://badge.fury.io/py/starlette.svg)](https://pypi.python.org/pypi/starlette) +[![Supported Python Version](https://img.shields.io/pypi/pyversions/starlette.svg?color=%2334D058)](https://pypi.org/project/starlette) +[![Discord](https://img.shields.io/discord/1051468649518616576?logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/RxKUF5JuHs) + +--- + +**Documentation**: https://starlette.dev + +**Source Code**: https://github.com/Kludex/starlette + +--- + +# Starlette + +Starlette is a lightweight [ASGI][asgi] framework/toolkit, +which is ideal for building async web services in Python. + +It is production-ready, and gives you the following: + +* A lightweight, low-complexity HTTP web framework. +* WebSocket support. +* In-process background tasks. +* Startup and shutdown events. +* Test client built on `httpx`. +* CORS, GZip, Static Files, Streaming responses. +* Session and Cookie support. +* 100% test coverage. +* 100% type annotated codebase. +* Few hard dependencies. +* Compatible with `asyncio` and `trio` backends. +* Great overall performance [against independent benchmarks][techempower]. + +## Installation + +```shell +$ pip install starlette +``` + +You'll also want to install an ASGI server, such as [uvicorn](https://uvicorn.dev) or any of the [other ASGI server implementations](https://asgi.readthedocs.io/en/latest/implementations.html#servers). + +```shell +$ pip install uvicorn +``` + +## Example + +```python title="main.py" +from starlette.applications import Starlette +from starlette.responses import JSONResponse +from starlette.routing import Route + + +async def homepage(request): + return JSONResponse({'hello': 'world'}) + +routes = [ + Route("/", endpoint=homepage) +] + +app = Starlette(debug=True, routes=routes) +``` + +Then run the application using Uvicorn: + +```shell +$ uvicorn main:app +``` + +## Dependencies + +Starlette only requires `anyio`, and the following are optional: + +* [`httpx2`][httpx2] - Required if you want to use the `TestClient`. +* [`jinja2`][jinja2] - Required if you want to use `Jinja2Templates`. +* [`python-multipart`][python-multipart] - Required if you want to support form parsing, with `request.form()`. +* [`itsdangerous`][itsdangerous] - Required for `SessionMiddleware` support. +* [`pyyaml`][pyyaml] - Required for `SchemaGenerator` support. + +You can install all of these with `pip install starlette[full]`. + +## Framework or Toolkit + +Starlette is designed to be used either as a complete framework, or as +an ASGI toolkit. You can use any of its components independently. + +```python +from starlette.responses import PlainTextResponse + + +async def app(scope, receive, send): + assert scope['type'] == 'http' + response = PlainTextResponse('Hello, world!') + await response(scope, receive, send) +``` + +Run the `app` application in `example.py`: + +```shell +$ uvicorn example:app +INFO: Started server process [11509] +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +Run uvicorn with `--reload` to enable auto-reloading on code changes. + +## Modularity + +The modularity that Starlette is designed on promotes building reusable +components that can be shared between any ASGI framework. This should enable +an ecosystem of shared middleware and mountable applications. + +The clean API separation also means it's easier to understand each component +in isolation. + +--- + +

Starlette is BSD licensed code.
Designed & crafted with care.

— ⭐️ —

+ +[asgi]: https://asgi.readthedocs.io/en/latest/ +[httpx2]: https://pypi.org/project/httpx2/ +[jinja2]: https://jinja.palletsprojects.com/ +[python-multipart]: https://multipart.fastapiexpert.com/ +[itsdangerous]: https://itsdangerous.palletsprojects.com/ +[sqlalchemy]: https://www.sqlalchemy.org +[pyyaml]: https://pyyaml.org/wiki/PyYAMLDocumentation +[techempower]: https://www.techempower.com/benchmarks/#hw=ph&test=fortune&l=zijzen-sf diff --git a/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/RECORD b/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..79b48e4b5a362278333c92ce77d28dc8870a68d9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/RECORD @@ -0,0 +1,74 @@ +starlette-1.3.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +starlette-1.3.1.dist-info/METADATA,sha256=2DzNlmXrBdH5OTx14gnLNTmQcxpiFaYJoPHFwKYobJo,6370 +starlette-1.3.1.dist-info/RECORD,, +starlette-1.3.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87 +starlette-1.3.1.dist-info/licenses/LICENSE.md,sha256=3LlWd6AiQCQxh-lk-UGEfRmxeCHPmeWvrmhPqzKMGb8,1518 +starlette/__init__.py,sha256=-ypEJktJToAL9by62JJKWEzDo_KPCQtmE5kwFgX24z4,22 +starlette/__pycache__/__init__.cpython-311.pyc,, +starlette/__pycache__/_exception_handler.cpython-311.pyc,, +starlette/__pycache__/_utils.cpython-311.pyc,, +starlette/__pycache__/applications.cpython-311.pyc,, +starlette/__pycache__/authentication.cpython-311.pyc,, +starlette/__pycache__/background.cpython-311.pyc,, +starlette/__pycache__/concurrency.cpython-311.pyc,, +starlette/__pycache__/config.cpython-311.pyc,, +starlette/__pycache__/convertors.cpython-311.pyc,, +starlette/__pycache__/datastructures.cpython-311.pyc,, +starlette/__pycache__/endpoints.cpython-311.pyc,, +starlette/__pycache__/exceptions.cpython-311.pyc,, +starlette/__pycache__/formparsers.cpython-311.pyc,, +starlette/__pycache__/requests.cpython-311.pyc,, +starlette/__pycache__/responses.cpython-311.pyc,, +starlette/__pycache__/routing.cpython-311.pyc,, +starlette/__pycache__/schemas.cpython-311.pyc,, +starlette/__pycache__/staticfiles.cpython-311.pyc,, +starlette/__pycache__/status.cpython-311.pyc,, +starlette/__pycache__/templating.cpython-311.pyc,, +starlette/__pycache__/testclient.cpython-311.pyc,, +starlette/__pycache__/types.cpython-311.pyc,, +starlette/__pycache__/websockets.cpython-311.pyc,, +starlette/_exception_handler.py,sha256=8Vm915fWYe9xLo_za5iQ5WyrAvFBu2kEEJc7XreTyOk,2257 +starlette/_utils.py,sha256=fLZ_pRlcp-wP4zEiyeXOIl7foyl_zIL5Am6E-cVlg8s,2970 +starlette/applications.py,sha256=H4R7Jvt8tWxIj7QoRRt8_gEVj2xrYnG2AfA6d-2en9I,5291 +starlette/authentication.py,sha256=bydUX4PKnTFJPSUj8B5k8HxYqrJ4eyyOma8AtgqwlD0,5202 +starlette/background.py,sha256=D-XpUpFRgUBnZeA1tHgzMOgube6ICVZFlOA4ML0q1T8,1122 +starlette/concurrency.py,sha256=Pp4kXQYO85cWxY7XVNt5lcYvhYHUquyLguCec7hKKoM,1701 +starlette/config.py,sha256=GVwko0pGSusxuQHO35tL_Nt_ApL0fmVYL3IiHHYpLW4,4426 +starlette/convertors.py,sha256=F1rse3AacN9rsfJnTeuDnjbN51r_ouHc3WLyYkjkX_o,2304 +starlette/datastructures.py,sha256=-eMQA2KZ1qQnRDypa6EY5YSG5ZJ81MO6wP-4FWJuUs0,23213 +starlette/endpoints.py,sha256=s5jPdHUT7VzUUyY-O5qfXDKYim-zbhWUCE-U5TsaFKQ,5271 +starlette/exceptions.py,sha256=45cuOyqdOjJJVZ2fRupfcSYvPDKqhkGZejjXw1cPGXE,1478 +starlette/formparsers.py,sha256=dItaict6sqVLmUs82dltsLUlDzPASBoZPIB8xyIlfsY,12079 +starlette/middleware/__init__.py,sha256=BORnOj0n0mL1OWUUs1PLTu1SASmvT8kEEDA6cbuhuPo,1349 +starlette/middleware/__pycache__/__init__.cpython-311.pyc,, +starlette/middleware/__pycache__/authentication.cpython-311.pyc,, +starlette/middleware/__pycache__/base.cpython-311.pyc,, +starlette/middleware/__pycache__/cors.cpython-311.pyc,, +starlette/middleware/__pycache__/errors.cpython-311.pyc,, +starlette/middleware/__pycache__/exceptions.cpython-311.pyc,, +starlette/middleware/__pycache__/gzip.cpython-311.pyc,, +starlette/middleware/__pycache__/httpsredirect.cpython-311.pyc,, +starlette/middleware/__pycache__/sessions.cpython-311.pyc,, +starlette/middleware/__pycache__/trustedhost.cpython-311.pyc,, +starlette/middleware/__pycache__/wsgi.cpython-311.pyc,, +starlette/middleware/authentication.py,sha256=-RSwwJ0pIkrM0v-2Ip8HXVB24xtSp7ObzR3OfTC8wSA,1800 +starlette/middleware/base.py,sha256=q90T3sHQjhr5kSIJyM4HNXJi9AWstlSNmozqT2_KUc0,10326 +starlette/middleware/cors.py,sha256=iBOpWmK43ENnyR71t4vH2WwfCWzaJNJdE7th3x5Cg80,7509 +starlette/middleware/errors.py,sha256=UZttzAUUejJtNZpPts0PXnYgV9C4h8o3sWDHRkN9ECg,8101 +starlette/middleware/exceptions.py,sha256=7OgSUiBgwHS4VMmpaWlw21uDKNDmOMzLVZrWDLDUqWo,2784 +starlette/middleware/gzip.py,sha256=_thpCRctguw0tMM6J2iDlAj5vZlol9T673IHtfvfxQE,5899 +starlette/middleware/httpsredirect.py,sha256=SNTleaYALGoITV7xwbic4gB6VYdM8Ylea_ykciUz31g,848 +starlette/middleware/sessions.py,sha256=kbSzZhMN8R2ofdrvMQEMz4ex4xEhW-sTSfsTKQ9SSGY,4778 +starlette/middleware/trustedhost.py,sha256=byKCUyPge54Z4MznyunD_2DsMfJc2UsfV4b2Du-WYTc,2219 +starlette/middleware/wsgi.py,sha256=BqXJpFjy3hbMuMa0_kjLAKskNPO0s7k-TPSRP-zcgLU,5501 +starlette/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +starlette/requests.py,sha256=hpGwGuSez6CF_mQZNDZ49zt3yS_GOkERBqd9jpvwrEQ,12708 +starlette/responses.py,sha256=XVKrAI732c5MUU8TuOxi4Voep48poRbw6Mvub07qkRI,21731 +starlette/routing.py,sha256=uV5qR75s8QqJvNGgrOc7qN6MbvW3NA_eSy3iRTsvWEQ,29579 +starlette/schemas.py,sha256=fBDlI1YDKF_rkQIalqBVQ0PQ9-3gNGh7T_39OyE7rV0,5168 +starlette/staticfiles.py,sha256=8GP3DKNN8W3VGZIRPZFPQBIDiqN-YaKcNN2EvaMMQYw,8786 +starlette/status.py,sha256=XYcVn-aU522TYJ8BJxl0WUnf6J_3yBTQz7Tqgye8KO4,6430 +starlette/templating.py,sha256=y-ATlQrNDoLMZs5hrHzQBTapECBDr9wAl-bs8Blk5jw,5447 +starlette/testclient.py,sha256=UqnjIiKkkrNoM5yr8DVxY94u1-gJWdfz6IKZxbjE60w,28356 +starlette/types.py,sha256=87wTuwF1036QxRse3mL5qOuS9i4a9xaIZMqTjY-lvvo,1041 +starlette/websockets.py,sha256=m64W1F4JdLw-7TRGVHqSqgreamlHbVJSOeIQ17EdHyg,8352 diff --git a/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/WHEEL b/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..bd842267444de3fff37e8c9981cd8662490531b4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.30.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/licenses/LICENSE.md b/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/licenses/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..d16a60ec5b9963ef86b35a52ac92227014618e6c --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette-1.3.1.dist-info/licenses/LICENSE.md @@ -0,0 +1,27 @@ +Copyright © 2018, [Encode OSS Ltd](https://www.encode.io/). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/venv/lib/python3.11/site-packages/starlette/__init__.py b/venv/lib/python3.11/site-packages/starlette/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9c73af26be70465839a5f43818dbab3f5c35571f --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/__init__.py @@ -0,0 +1 @@ +__version__ = "1.3.1" diff --git a/venv/lib/python3.11/site-packages/starlette/_exception_handler.py b/venv/lib/python3.11/site-packages/starlette/_exception_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..9491f8bf0e66c2f0ee7ea3635d741397ea57bc61 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/_exception_handler.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from typing import Any + +from starlette._utils import is_async_callable +from starlette.concurrency import run_in_threadpool +from starlette.exceptions import HTTPException +from starlette.requests import Request +from starlette.types import ASGIApp, ExceptionHandler, Message, Receive, Scope, Send +from starlette.websockets import WebSocket + +ExceptionHandlers = dict[Any, ExceptionHandler] +StatusHandlers = dict[int, ExceptionHandler] + + +def _lookup_exception_handler(exc_handlers: ExceptionHandlers, exc: Exception) -> ExceptionHandler | None: + for cls in type(exc).__mro__: + if cls in exc_handlers: + return exc_handlers[cls] + return None + + +def wrap_app_handling_exceptions(app: ASGIApp, conn: Request | WebSocket) -> ASGIApp: + exception_handlers: ExceptionHandlers + status_handlers: StatusHandlers + try: + exception_handlers, status_handlers = conn.scope["starlette.exception_handlers"] + except KeyError: + exception_handlers, status_handlers = {}, {} + + async def wrapped_app(scope: Scope, receive: Receive, send: Send) -> None: + response_started = False + + async def sender(message: Message) -> None: + nonlocal response_started + + if message["type"] == "http.response.start": + response_started = True + await send(message) + + try: + await app(scope, receive, sender) + except Exception as exc: + handler = None + + if isinstance(exc, HTTPException): + handler = status_handlers.get(exc.status_code) + + if handler is None: + handler = _lookup_exception_handler(exception_handlers, exc) + + if handler is None: + raise exc + + if response_started: + raise RuntimeError("Caught handled exception, but response already started.") from exc + + if is_async_callable(handler): + response = await handler(conn, exc) # type: ignore[arg-type] + else: + response = await run_in_threadpool(handler, conn, exc) # type: ignore[arg-type] + if response is not None: + await response(scope, receive, sender) + + return wrapped_app diff --git a/venv/lib/python3.11/site-packages/starlette/_utils.py b/venv/lib/python3.11/site-packages/starlette/_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..b9b8639b2106fd552db7526c3f12aa429b430252 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/_utils.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import functools +import sys +from collections.abc import AsyncGenerator, Awaitable, Callable, Generator +from contextlib import AbstractAsyncContextManager, asynccontextmanager +from typing import Any, Generic, Protocol, TypeVar, overload + +import anyio.abc + +from starlette.types import Scope + +if sys.version_info >= (3, 13): # pragma: no cover + from inspect import iscoroutinefunction + from typing import TypeIs +else: # pragma: no cover + from asyncio import iscoroutinefunction + + from typing_extensions import TypeIs + +if sys.version_info < (3, 11): # pragma: no cover + try: + from exceptiongroup import BaseExceptionGroup + except ImportError: + + class BaseExceptionGroup(BaseException): # type: ignore[no-redef] + pass + + +T = TypeVar("T") +AwaitableCallable = Callable[..., Awaitable[T]] + + +@overload +def is_async_callable(obj: AwaitableCallable[T]) -> TypeIs[AwaitableCallable[T]]: ... + + +@overload +def is_async_callable(obj: Any) -> TypeIs[AwaitableCallable[Any]]: ... + + +def is_async_callable(obj: Any) -> Any: + while isinstance(obj, functools.partial): + obj = obj.func + + return iscoroutinefunction(obj) or (callable(obj) and iscoroutinefunction(obj.__call__)) + + +T_co = TypeVar("T_co", covariant=True) + + +class AwaitableOrContextManager( + Awaitable[T_co], AbstractAsyncContextManager[T_co], Protocol[T_co] +): ... # pragma: no branch + + +class SupportsAsyncClose(Protocol): + async def close(self) -> None: ... # pragma: no cover + + +SupportsAsyncCloseType = TypeVar("SupportsAsyncCloseType", bound=SupportsAsyncClose, covariant=False) + + +class AwaitableOrContextManagerWrapper(Generic[SupportsAsyncCloseType]): + __slots__ = ("aw", "entered") + + def __init__(self, aw: Awaitable[SupportsAsyncCloseType]) -> None: + self.aw = aw + + def __await__(self) -> Generator[Any, None, SupportsAsyncCloseType]: + return self.aw.__await__() + + async def __aenter__(self) -> SupportsAsyncCloseType: + self.entered = await self.aw + return self.entered + + async def __aexit__(self, *args: Any) -> None | bool: + await self.entered.close() + return None + + +@asynccontextmanager +async def create_collapsing_task_group() -> AsyncGenerator[anyio.abc.TaskGroup, None]: + try: + async with anyio.create_task_group() as tg: + yield tg + except BaseExceptionGroup as excs: + if len(excs.exceptions) != 1: + raise + + exc = excs.exceptions[0] + context = None if exc.__suppress_context__ else exc.__context__ + raise exc from exc.__cause__ or context + + +def get_route_path(scope: Scope) -> str: + path: str = scope["path"] + root_path = scope.get("root_path", "") + if not root_path: + return path + + if not path.startswith(root_path): + return path + + if path == root_path: + return "" + + if path[len(root_path)] == "/": + return path[len(root_path) :] + + return path diff --git a/venv/lib/python3.11/site-packages/starlette/applications.py b/venv/lib/python3.11/site-packages/starlette/applications.py new file mode 100644 index 0000000000000000000000000000000000000000..3e1a7816a808d2a31e6161d847a1919feffd7cbf --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/applications.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Mapping, Sequence +from typing import Any, ParamSpec, TypeVar + +from starlette.datastructures import State, URLPath +from starlette.middleware import Middleware, _MiddlewareFactory +from starlette.middleware.errors import ServerErrorMiddleware +from starlette.middleware.exceptions import ExceptionMiddleware +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import BaseRoute, Router +from starlette.types import ASGIApp, ExceptionHandler, Lifespan, Receive, Scope, Send + +AppType = TypeVar("AppType", bound="Starlette") +P = ParamSpec("P") + + +class Starlette: + """Creates an Starlette application.""" + + def __init__( + self: AppType, + debug: bool = False, + routes: Sequence[BaseRoute] | None = None, + middleware: Sequence[Middleware] | None = None, + exception_handlers: Mapping[Any, ExceptionHandler] | None = None, + lifespan: Lifespan[AppType] | None = None, + ) -> None: + """Initializes the application. + + Parameters: + debug: Boolean indicating if debug tracebacks should be returned on errors. + routes: A list of routes to serve incoming HTTP and WebSocket requests. + middleware: A list of middleware to run for every request. A starlette + application will always automatically include two middleware classes. + `ServerErrorMiddleware` is added as the very outermost middleware, to handle + any uncaught errors occurring anywhere in the entire stack. + `ExceptionMiddleware` is added as the very innermost middleware, to deal + with handled exception cases occurring in the routing or endpoints. + exception_handlers: A mapping of either integer status codes, + or exception class types onto callables which handle the exceptions. + Exception handler callables should be of the form + `handler(request, exc) -> response` and may be either standard functions, or + async functions. + lifespan: A lifespan context function, which can be used to perform + startup and shutdown tasks. This is a newer style that replaces the + `on_startup` and `on_shutdown` handlers. Use one or the other, not both. + """ + self.debug = debug + self.state = State() + self.router = Router(routes, lifespan=lifespan) + self.exception_handlers = {} if exception_handlers is None else dict(exception_handlers) + self.user_middleware = [] if middleware is None else list(middleware) + self.middleware_stack: ASGIApp | None = None + + def build_middleware_stack(self) -> ASGIApp: + debug = self.debug + error_handler = None + exception_handlers: dict[Any, ExceptionHandler] = {} + + for key, value in self.exception_handlers.items(): + if key in (500, Exception): + error_handler = value + else: + exception_handlers[key] = value + + middleware = ( + [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)] + + self.user_middleware + + [Middleware(ExceptionMiddleware, handlers=exception_handlers, debug=debug)] + ) + + app = self.router + for cls, args, kwargs in reversed(middleware): + app = cls(app, *args, **kwargs) + return app + + @property + def routes(self) -> list[BaseRoute]: + return self.router.routes + + def url_path_for(self, name: str, /, **path_params: Any) -> URLPath: + return self.router.url_path_for(name, **path_params) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + scope["app"] = self + if self.middleware_stack is None: + self.middleware_stack = self.build_middleware_stack() + await self.middleware_stack(scope, receive, send) + + def mount(self, path: str, app: ASGIApp, name: str | None = None) -> None: + self.router.mount(path, app=app, name=name) # pragma: no cover + + def host(self, host: str, app: ASGIApp, name: str | None = None) -> None: + self.router.host(host, app=app, name=name) # pragma: no cover + + def add_middleware(self, middleware_class: _MiddlewareFactory[P], *args: P.args, **kwargs: P.kwargs) -> None: + if self.middleware_stack is not None: # pragma: no cover + raise RuntimeError("Cannot add middleware after an application has started") + self.user_middleware.insert(0, Middleware(middleware_class, *args, **kwargs)) + + def add_exception_handler( + self, + exc_class_or_status_code: int | type[Exception], + handler: ExceptionHandler, + ) -> None: # pragma: no cover + self.exception_handlers[exc_class_or_status_code] = handler + + def add_route( + self, + path: str, + route: Callable[[Request], Awaitable[Response] | Response], + methods: list[str] | None = None, + name: str | None = None, + include_in_schema: bool = True, + ) -> None: # pragma: no cover + self.router.add_route(path, route, methods=methods, name=name, include_in_schema=include_in_schema) diff --git a/venv/lib/python3.11/site-packages/starlette/authentication.py b/venv/lib/python3.11/site-packages/starlette/authentication.py new file mode 100644 index 0000000000000000000000000000000000000000..63c4da00493808479dbaba62438fc93ceb63be2a --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/authentication.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import functools +import inspect +from collections.abc import Callable, Sequence +from typing import Any, ParamSpec +from urllib.parse import urlencode + +from starlette._utils import is_async_callable +from starlette.exceptions import HTTPException +from starlette.requests import HTTPConnection, Request +from starlette.responses import RedirectResponse +from starlette.websockets import WebSocket + +_P = ParamSpec("_P") + + +def has_required_scope(conn: HTTPConnection, scopes: Sequence[str]) -> bool: + for scope in scopes: + if scope not in conn.auth.scopes: + return False + return True + + +def requires( + scopes: str | Sequence[str], + status_code: int = 403, + redirect: str | None = None, +) -> Callable[[Callable[_P, Any]], Callable[_P, Any]]: + scopes_list = [scopes] if isinstance(scopes, str) else list(scopes) + + def decorator( + func: Callable[_P, Any], + ) -> Callable[_P, Any]: + sig = inspect.signature(func) + for idx, parameter in enumerate(sig.parameters.values()): + if parameter.name == "request" or parameter.name == "websocket": + type_ = parameter.name + break + else: + raise Exception(f'No "request" or "websocket" argument on function "{func}"') + + if type_ == "websocket": + # Handle websocket functions. (Always async) + @functools.wraps(func) + async def websocket_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> None: + websocket = kwargs.get("websocket", args[idx] if idx < len(args) else None) + assert isinstance(websocket, WebSocket), ( + "Parameter with name 'websocket' is required to be of type 'WebSocket'" + f" not '{type(websocket).__name__}'" + ) + + if not has_required_scope(websocket, scopes_list): + await websocket.close() + else: + await func(*args, **kwargs) + + return websocket_wrapper + + elif is_async_callable(func): + # Handle async request/response functions. + @functools.wraps(func) + async def async_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> Any: + request = kwargs.get("request", args[idx] if idx < len(args) else None) + assert isinstance(request, Request), ( + f"Parameter with name 'request' is required to be of type 'Request' not '{type(request).__name__}'" + ) + + if not has_required_scope(request, scopes_list): + if redirect is not None: + orig_request_qparam = urlencode({"next": str(request.url)}) + next_url = f"{request.url_for(redirect)}?{orig_request_qparam}" + return RedirectResponse(url=next_url, status_code=303) + raise HTTPException(status_code=status_code) + return await func(*args, **kwargs) + + return async_wrapper + + else: + # Handle sync request/response functions. + @functools.wraps(func) + def sync_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> Any: + request = kwargs.get("request", args[idx] if idx < len(args) else None) + assert isinstance(request, Request), ( + f"Parameter with name 'request' is required to be of type 'Request' not '{type(request).__name__}'" + ) + + if not has_required_scope(request, scopes_list): + if redirect is not None: + orig_request_qparam = urlencode({"next": str(request.url)}) + next_url = f"{request.url_for(redirect)}?{orig_request_qparam}" + return RedirectResponse(url=next_url, status_code=303) + raise HTTPException(status_code=status_code) + return func(*args, **kwargs) + + return sync_wrapper + + return decorator + + +class AuthenticationError(Exception): + pass + + +class AuthenticationBackend: + async def authenticate(self, conn: HTTPConnection) -> tuple[AuthCredentials, BaseUser] | None: + raise NotImplementedError() # pragma: no cover + + +class AuthCredentials: + def __init__(self, scopes: Sequence[str] | None = None): + self.scopes = [] if scopes is None else list(scopes) + + +class BaseUser: + @property + def is_authenticated(self) -> bool: + raise NotImplementedError() # pragma: no cover + + @property + def display_name(self) -> str: + raise NotImplementedError() # pragma: no cover + + @property + def identity(self) -> str: + raise NotImplementedError() # pragma: no cover + + +class SimpleUser(BaseUser): + def __init__(self, username: str) -> None: + self.username = username + + @property + def is_authenticated(self) -> bool: + return True + + @property + def display_name(self) -> str: + return self.username + + +class UnauthenticatedUser(BaseUser): + @property + def is_authenticated(self) -> bool: + return False + + @property + def display_name(self) -> str: + return "" diff --git a/venv/lib/python3.11/site-packages/starlette/background.py b/venv/lib/python3.11/site-packages/starlette/background.py new file mode 100644 index 0000000000000000000000000000000000000000..55135eed37a42945480b29271cf87dc5920dcb10 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/background.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from collections.abc import Callable, Sequence +from typing import Any, ParamSpec + +from starlette._utils import is_async_callable +from starlette.concurrency import run_in_threadpool + +P = ParamSpec("P") + + +class BackgroundTask: + def __init__(self, func: Callable[P, Any], *args: P.args, **kwargs: P.kwargs) -> None: + self.func = func + self.args = args + self.kwargs = kwargs + self.is_async = is_async_callable(func) + + async def __call__(self) -> None: + if self.is_async: + await self.func(*self.args, **self.kwargs) + else: + await run_in_threadpool(self.func, *self.args, **self.kwargs) + + +class BackgroundTasks(BackgroundTask): + def __init__(self, tasks: Sequence[BackgroundTask] | None = None): + self.tasks = list(tasks) if tasks else [] + + def add_task(self, func: Callable[P, Any], *args: P.args, **kwargs: P.kwargs) -> None: + task = BackgroundTask(func, *args, **kwargs) + self.tasks.append(task) + + async def __call__(self) -> None: + for task in self.tasks: + await task() diff --git a/venv/lib/python3.11/site-packages/starlette/concurrency.py b/venv/lib/python3.11/site-packages/starlette/concurrency.py new file mode 100644 index 0000000000000000000000000000000000000000..a2c55be39b617650ee3fb50a5031959e0b49b121 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/concurrency.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import functools +import warnings +from collections.abc import AsyncIterator, Callable, Coroutine, Iterable, Iterator +from typing import ParamSpec, TypeVar + +import anyio.to_thread + +from starlette.exceptions import StarletteDeprecationWarning + +P = ParamSpec("P") +T = TypeVar("T") + + +async def run_until_first_complete(*args: tuple[Callable, dict]) -> None: # type: ignore[type-arg] + warnings.warn( + "run_until_first_complete is deprecated and will be removed in a future version.", + StarletteDeprecationWarning, + ) + + async with anyio.create_task_group() as task_group: + + async def run(func: Callable[[], Coroutine]) -> None: # type: ignore[type-arg] + await func() + task_group.cancel_scope.cancel() + + for func, kwargs in args: + task_group.start_soon(run, functools.partial(func, **kwargs)) + + +async def run_in_threadpool(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: + func = functools.partial(func, *args, **kwargs) + return await anyio.to_thread.run_sync(func) + + +class _StopIteration(Exception): + pass + + +def _next(iterator: Iterator[T]) -> T: + # We can't raise `StopIteration` from within the threadpool iterator + # and catch it outside that context, so we coerce them into a different + # exception type. + try: + return next(iterator) + except StopIteration: + raise _StopIteration + + +async def iterate_in_threadpool( + iterator: Iterable[T], +) -> AsyncIterator[T]: + as_iterator = iter(iterator) + while True: + try: + yield await anyio.to_thread.run_sync(_next, as_iterator) + except _StopIteration: + break diff --git a/venv/lib/python3.11/site-packages/starlette/config.py b/venv/lib/python3.11/site-packages/starlette/config.py new file mode 100644 index 0000000000000000000000000000000000000000..56c25ca3b9286d20a5ee722cf4ff4ba04267668b --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/config.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +import os +import warnings +from collections.abc import Callable, Iterator, Mapping, MutableMapping +from pathlib import Path +from typing import Any, TypeVar, overload + + +class undefined: + pass + + +class EnvironError(Exception): + pass + + +class Environ(MutableMapping[str, str]): + def __init__(self, environ: MutableMapping[str, str] = os.environ): + self._environ = environ + self._has_been_read: set[str] = set() + + def __getitem__(self, key: str) -> str: + self._has_been_read.add(key) + return self._environ.__getitem__(key) + + def __setitem__(self, key: str, value: str) -> None: + if key in self._has_been_read: + raise EnvironError(f"Attempting to set environ['{key}'], but the value has already been read.") + self._environ.__setitem__(key, value) + + def __delitem__(self, key: str) -> None: + if key in self._has_been_read: + raise EnvironError(f"Attempting to delete environ['{key}'], but the value has already been read.") + self._environ.__delitem__(key) + + def __iter__(self) -> Iterator[str]: + return iter(self._environ) + + def __len__(self) -> int: + return len(self._environ) + + +environ = Environ() + +T = TypeVar("T") + + +class Config: + def __init__( + self, + env_file: str | Path | None = None, + environ: Mapping[str, str] = environ, + env_prefix: str = "", + encoding: str = "utf-8", + ) -> None: + self.environ = environ + self.env_prefix = env_prefix + self.file_values: dict[str, str] = {} + if env_file is not None: + if not os.path.isfile(env_file): + warnings.warn(f"Config file '{env_file}' not found.") + else: + self.file_values = self._read_file(env_file, encoding) + + @overload + def __call__(self, key: str, *, default: None) -> str | None: ... + + @overload + def __call__(self, key: str, cast: type[T], default: T = ...) -> T: ... + + @overload + def __call__(self, key: str, cast: type[str] = ..., default: str = ...) -> str: ... + + @overload + def __call__( + self, + key: str, + cast: Callable[[Any], T] = ..., + default: Any = ..., + ) -> T: ... + + @overload + def __call__(self, key: str, cast: type[str] = ..., default: T = ...) -> T | str: ... + + def __call__( + self, + key: str, + cast: Callable[[Any], Any] | None = None, + default: Any = undefined, + ) -> Any: + return self.get(key, cast, default) + + def get( + self, + key: str, + cast: Callable[[Any], Any] | None = None, + default: Any = undefined, + ) -> Any: + key = self.env_prefix + key + if key in self.environ: + value = self.environ[key] + return self._perform_cast(key, value, cast) + if key in self.file_values: + value = self.file_values[key] + return self._perform_cast(key, value, cast) + if default is not undefined: + return self._perform_cast(key, default, cast) + raise KeyError(f"Config '{key}' is missing, and has no default.") + + def _read_file(self, file_name: str | Path, encoding: str) -> dict[str, str]: + file_values: dict[str, str] = {} + with open(file_name, encoding=encoding) as input_file: + for line in input_file.readlines(): + line = line.strip() + if "=" in line and not line.startswith("#"): + key, value = line.split("=", 1) + key = key.strip() + value = value.strip().strip("\"'") + file_values[key] = value + return file_values + + def _perform_cast( + self, + key: str, + value: Any, + cast: Callable[[Any], Any] | None = None, + ) -> Any: + if cast is None or value is None: + return value + elif cast is bool and isinstance(value, str): + mapping = {"true": True, "1": True, "false": False, "0": False} + value = value.lower() + if value not in mapping: + raise ValueError(f"Config '{key}' has value '{value}'. Not a valid bool.") + return mapping[value] + try: + return cast(value) + except (TypeError, ValueError): + raise ValueError(f"Config '{key}' has value '{value}'. Not a valid {cast.__name__}.") diff --git a/venv/lib/python3.11/site-packages/starlette/convertors.py b/venv/lib/python3.11/site-packages/starlette/convertors.py new file mode 100644 index 0000000000000000000000000000000000000000..72b1cf9fdfb6d7f5be61f648a9ae016deb45012b --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/convertors.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import math +import uuid +from typing import Any, ClassVar, Generic, TypeVar + +T = TypeVar("T") + + +class Convertor(Generic[T]): + regex: ClassVar[str] = "" + + def convert(self, value: str) -> T: + raise NotImplementedError() # pragma: no cover + + def to_string(self, value: T) -> str: + raise NotImplementedError() # pragma: no cover + + +class StringConvertor(Convertor[str]): + regex = "[^/]+" + + def convert(self, value: str) -> str: + return value + + def to_string(self, value: str) -> str: + value = str(value) + assert "/" not in value, "May not contain path separators" + assert value, "Must not be empty" + return value + + +class PathConvertor(Convertor[str]): + regex = ".*" + + def convert(self, value: str) -> str: + return str(value) + + def to_string(self, value: str) -> str: + return str(value) + + +class IntegerConvertor(Convertor[int]): + regex = "[0-9]+" + + def convert(self, value: str) -> int: + return int(value) + + def to_string(self, value: int) -> str: + value = int(value) + assert value >= 0, "Negative integers are not supported" + return str(value) + + +class FloatConvertor(Convertor[float]): + regex = r"[0-9]+(\.[0-9]+)?" + + def convert(self, value: str) -> float: + return float(value) + + def to_string(self, value: float) -> str: + value = float(value) + assert value >= 0.0, "Negative floats are not supported" + assert not math.isnan(value), "NaN values are not supported" + assert not math.isinf(value), "Infinite values are not supported" + return ("%0.20f" % value).rstrip("0").rstrip(".") + + +class UUIDConvertor(Convertor[uuid.UUID]): + regex = "[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}" + + def convert(self, value: str) -> uuid.UUID: + return uuid.UUID(value) + + def to_string(self, value: uuid.UUID) -> str: + return str(value) + + +CONVERTOR_TYPES: dict[str, Convertor[Any]] = { + "str": StringConvertor(), + "path": PathConvertor(), + "int": IntegerConvertor(), + "float": FloatConvertor(), + "uuid": UUIDConvertor(), +} + + +def register_url_convertor(key: str, convertor: Convertor[Any]) -> None: + CONVERTOR_TYPES[key] = convertor diff --git a/venv/lib/python3.11/site-packages/starlette/datastructures.py b/venv/lib/python3.11/site-packages/starlette/datastructures.py new file mode 100644 index 0000000000000000000000000000000000000000..33ecb1b1236b516daac0ba27f5c7d82c1e463363 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/datastructures.py @@ -0,0 +1,704 @@ +from __future__ import annotations + +import re +from collections.abc import ItemsView, Iterable, Iterator, KeysView, Mapping, MutableMapping, Sequence, ValuesView +from shlex import shlex +from typing import Any, BinaryIO, Literal, NamedTuple, TypeVar, cast +from urllib.parse import SplitResult, parse_qsl, urlencode, urlsplit + +from starlette.concurrency import run_in_threadpool +from starlette.types import Scope + + +class Address(NamedTuple): + host: str + port: int + + +_KeyType = TypeVar("_KeyType") +# Mapping keys are invariant but their values are covariant since +# you can only read them +# that is, you can't do `Mapping[str, Animal]()["fido"] = Dog()` +_CovariantValueType = TypeVar("_CovariantValueType", covariant=True) + +# Rejects Host header chars (/, ?, #, @, ...) that would let urlsplit produce a path differing from scope["path"]. +_HOST_RE = re.compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9.:]+\])(?::[0-9]+)?$", re.IGNORECASE) + + +class URL: + def __init__( + self, + url: str = "", + scope: Scope | None = None, + **components: Any, + ) -> None: + if scope is not None: + assert not url, 'Cannot set both "url" and "scope".' + assert not components, 'Cannot set both "scope" and "**components".' + scheme = scope.get("scheme", "http") + server = scope.get("server", None) + path = scope["path"] + query_string = scope.get("query_string", b"") + + host_header = None + for key, value in scope["headers"]: + if key == b"host": + host_header = value.decode("latin-1") + break + + if host_header is not None and _HOST_RE.fullmatch(host_header): + netloc = host_header + elif server is not None: + host, port = server + default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme] + netloc = host if port == default_port else f"{host}:{port}" + else: + netloc = None + + query = query_string.decode() + if netloc is not None: + url = SplitResult(scheme=scheme, netloc=netloc, path=path, query=query, fragment="").geturl() + else: + url = f"{path}?{query}" if query else path + elif components: + assert not url, 'Cannot set both "url" and "**components".' + url = URL("").replace(**components).components.geturl() + + self._url = url + + @property + def components(self) -> SplitResult: + if not hasattr(self, "_components"): + self._components = urlsplit(self._url) + return self._components + + @property + def scheme(self) -> str: + return self.components.scheme + + @property + def netloc(self) -> str: + return self.components.netloc + + @property + def path(self) -> str: + return self.components.path + + @property + def query(self) -> str: + return self.components.query + + @property + def fragment(self) -> str: + return self.components.fragment + + @property + def username(self) -> None | str: + return self.components.username + + @property + def password(self) -> None | str: + return self.components.password + + @property + def hostname(self) -> None | str: + return self.components.hostname + + @property + def port(self) -> int | None: + return self.components.port + + @property + def is_secure(self) -> bool: + return self.scheme in ("https", "wss") + + def replace(self, **kwargs: Any) -> URL: + if "username" in kwargs or "password" in kwargs or "hostname" in kwargs or "port" in kwargs: + hostname = kwargs.pop("hostname", None) + port = kwargs.pop("port", self.port) + username = kwargs.pop("username", self.username) + password = kwargs.pop("password", self.password) + + if hostname is None: + netloc = self.netloc + _, _, hostname = netloc.rpartition("@") + + if hostname and hostname[-1] != "]": + hostname = hostname.rsplit(":", 1)[0] + + netloc = hostname + if port is not None: + netloc += f":{port}" + if username is not None: + userpass = username + if password is not None: + userpass += f":{password}" + netloc = f"{userpass}@{netloc}" + + kwargs["netloc"] = netloc + + components = self.components._replace(**kwargs) + return self.__class__(components.geturl()) + + def include_query_params(self, **kwargs: Any) -> URL: + params = MultiDict(parse_qsl(self.query, keep_blank_values=True)) + params.update({str(key): str(value) for key, value in kwargs.items()}) + query = urlencode(params.multi_items()) + return self.replace(query=query) + + def replace_query_params(self, **kwargs: Any) -> URL: + query = urlencode([(str(key), str(value)) for key, value in kwargs.items()]) + return self.replace(query=query) + + def remove_query_params(self, keys: str | Sequence[str]) -> URL: + if isinstance(keys, str): + keys = [keys] + params = MultiDict(parse_qsl(self.query, keep_blank_values=True)) + for key in keys: + params.pop(key, None) + query = urlencode(params.multi_items()) + return self.replace(query=query) + + def __eq__(self, other: Any) -> bool: + return str(self) == str(other) + + def __str__(self) -> str: + return self._url + + def __repr__(self) -> str: + url = str(self) + if self.password: + url = str(self.replace(password="********")) + return f"{self.__class__.__name__}({repr(url)})" + + +class URLPath(str): + """ + A URL path string that may also hold an associated protocol and/or host. + Used by the routing to return `url_path_for` matches. + """ + + def __new__(cls, path: str, protocol: Literal["http", "websocket", ""] = "", host: str = "") -> URLPath: + assert protocol in ("http", "websocket", "") + return str.__new__(cls, path) + + def __init__(self, path: str, protocol: Literal["http", "websocket", ""] = "", host: str = "") -> None: + self.protocol = protocol + self.host = host + + def make_absolute_url(self, base_url: str | URL) -> URL: + if isinstance(base_url, str): + base_url = URL(base_url) + if self.protocol: + scheme = { + "http": {True: "https", False: "http"}, + "websocket": {True: "wss", False: "ws"}, + }[self.protocol][base_url.is_secure] + else: + scheme = base_url.scheme + + netloc = self.host or base_url.netloc + path = base_url.path.rstrip("/") + str(self) + return URL(scheme=scheme, netloc=netloc, path=path) + + +class Secret: + """ + Holds a string value that should not be revealed in tracebacks etc. + You should cast the value to `str` at the point it is required. + """ + + def __init__(self, value: str): + self._value = value + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + return f"{class_name}('**********')" + + def __str__(self) -> str: + return self._value + + def __bool__(self) -> bool: + return bool(self._value) + + +class CommaSeparatedStrings(Sequence[str]): + def __init__(self, value: str | Sequence[str]): + if isinstance(value, str): + splitter = shlex(value, posix=True) + splitter.whitespace = "," + splitter.whitespace_split = True + self._items = [item.strip() for item in splitter] + else: + self._items = list(value) + + def __len__(self) -> int: + return len(self._items) + + def __getitem__(self, index: int | slice) -> Any: + return self._items[index] + + def __iter__(self) -> Iterator[str]: + return iter(self._items) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + items = [item for item in self] + return f"{class_name}({items!r})" + + def __str__(self) -> str: + return ", ".join(repr(item) for item in self) + + +class ImmutableMultiDict(Mapping[_KeyType, _CovariantValueType]): + _dict: dict[_KeyType, _CovariantValueType] + + def __init__( + self, + *args: ImmutableMultiDict[_KeyType, _CovariantValueType] + | Mapping[_KeyType, _CovariantValueType] + | Iterable[tuple[_KeyType, _CovariantValueType]], + **kwargs: Any, + ) -> None: + assert len(args) < 2, "Too many arguments." + + value: Any = args[0] if args else [] + if kwargs: + value = ImmutableMultiDict(value).multi_items() + ImmutableMultiDict(kwargs).multi_items() + + if not value: + _items: list[tuple[Any, Any]] = [] + elif hasattr(value, "multi_items"): + value = cast(ImmutableMultiDict[_KeyType, _CovariantValueType], value) + _items = list(value.multi_items()) + elif hasattr(value, "items"): + value = cast(Mapping[_KeyType, _CovariantValueType], value) + _items = list(value.items()) + else: + value = cast("list[tuple[Any, Any]]", value) + _items = list(value) + + self._dict = {k: v for k, v in _items} + self._list = _items + + def getlist(self, key: Any) -> list[_CovariantValueType]: + return [item_value for item_key, item_value in self._list if item_key == key] + + def keys(self) -> KeysView[_KeyType]: + return self._dict.keys() + + def values(self) -> ValuesView[_CovariantValueType]: + return self._dict.values() + + def items(self) -> ItemsView[_KeyType, _CovariantValueType]: + return self._dict.items() + + def multi_items(self) -> list[tuple[_KeyType, _CovariantValueType]]: + return list(self._list) + + def __getitem__(self, key: _KeyType) -> _CovariantValueType: + return self._dict[key] + + def __contains__(self, key: Any) -> bool: + return key in self._dict + + def __iter__(self) -> Iterator[_KeyType]: + return iter(self.keys()) + + def __len__(self) -> int: + return len(self._dict) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, self.__class__): + return False + return sorted(self._list) == sorted(other._list) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + items = self.multi_items() + return f"{class_name}({items!r})" + + +class MultiDict(ImmutableMultiDict[Any, Any]): + def __setitem__(self, key: Any, value: Any) -> None: + self.setlist(key, [value]) + + def __delitem__(self, key: Any) -> None: + self._list = [(k, v) for k, v in self._list if k != key] + del self._dict[key] + + def pop(self, key: Any, default: Any = None) -> Any: + self._list = [(k, v) for k, v in self._list if k != key] + return self._dict.pop(key, default) + + def popitem(self) -> tuple[Any, Any]: + key, value = self._dict.popitem() + self._list = [(k, v) for k, v in self._list if k != key] + return key, value + + def poplist(self, key: Any) -> list[Any]: + values = [v for k, v in self._list if k == key] + self.pop(key) + return values + + def clear(self) -> None: + self._dict.clear() + self._list.clear() + + def setdefault(self, key: Any, default: Any = None) -> Any: + if key not in self: + self._dict[key] = default + self._list.append((key, default)) + + return self[key] + + def setlist(self, key: Any, values: list[Any]) -> None: + if not values: + self.pop(key, None) + else: + existing_items = [(k, v) for (k, v) in self._list if k != key] + self._list = existing_items + [(key, value) for value in values] + self._dict[key] = values[-1] + + def append(self, key: Any, value: Any) -> None: + self._list.append((key, value)) + self._dict[key] = value + + def update( + self, + *args: MultiDict | Mapping[Any, Any] | list[tuple[Any, Any]], + **kwargs: Any, + ) -> None: + value = MultiDict(*args, **kwargs) + existing_items = [(k, v) for (k, v) in self._list if k not in value.keys()] + self._list = existing_items + value.multi_items() + self._dict.update(value) + + +class QueryParams(ImmutableMultiDict[str, str]): + """ + An immutable multidict. + """ + + def __init__( + self, + *args: ImmutableMultiDict[Any, Any] | Mapping[Any, Any] | list[tuple[Any, Any]] | str | bytes, + **kwargs: Any, + ) -> None: + assert len(args) < 2, "Too many arguments." + + value = args[0] if args else [] + + if isinstance(value, str): + super().__init__(parse_qsl(value, keep_blank_values=True), **kwargs) + elif isinstance(value, bytes): + super().__init__(parse_qsl(value.decode("latin-1"), keep_blank_values=True), **kwargs) + else: + super().__init__(*args, **kwargs) # type: ignore[arg-type] + self._list = [(str(k), str(v)) for k, v in self._list] + self._dict = {str(k): str(v) for k, v in self._dict.items()} + + def __str__(self) -> str: + return urlencode(self._list) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + query_string = str(self) + return f"{class_name}({query_string!r})" + + +class UploadFile: + """ + An uploaded file included as part of the request data. + """ + + def __init__( + self, + file: BinaryIO, + *, + size: int | None = None, + filename: str | None = None, + headers: Headers | None = None, + ) -> None: + self.filename = filename + self.file = file + self.size = size + self.headers = headers or Headers() + + # Capture max size from SpooledTemporaryFile if one is provided. This slightly speeds up future checks. + # Note 0 means unlimited mirroring SpooledTemporaryFile's __init__ + self._max_mem_size = getattr(self.file, "_max_size", 0) + + @property + def content_type(self) -> str | None: + return self.headers.get("content-type", None) + + @property + def _in_memory(self) -> bool: + # check for SpooledTemporaryFile._rolled + rolled_to_disk = getattr(self.file, "_rolled", True) + return not rolled_to_disk + + def _will_roll(self, size_to_add: int) -> bool: + # If we're not in_memory then we will always roll + if not self._in_memory: + return True + + # Check for SpooledTemporaryFile._max_size + future_size = self.file.tell() + size_to_add + return bool(future_size > self._max_mem_size) if self._max_mem_size else False + + async def write(self, data: bytes) -> None: + new_data_len = len(data) + if self.size is not None: + self.size += new_data_len + + if self._will_roll(new_data_len): + await run_in_threadpool(self.file.write, data) + else: + self.file.write(data) + + async def read(self, size: int = -1) -> bytes: + if self._in_memory: + return self.file.read(size) + return await run_in_threadpool(self.file.read, size) + + async def seek(self, offset: int) -> None: + if self._in_memory: + self.file.seek(offset) + else: + await run_in_threadpool(self.file.seek, offset) + + async def close(self) -> None: + if self._in_memory: + self.file.close() + else: + await run_in_threadpool(self.file.close) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(filename={self.filename!r}, size={self.size!r}, headers={self.headers!r})" + + +class FormData(ImmutableMultiDict[str, UploadFile | str]): + """ + An immutable multidict, containing both file uploads and text input. + """ + + def __init__( + self, + *args: FormData | Mapping[str, str | UploadFile] | list[tuple[str, str | UploadFile]], + **kwargs: str | UploadFile, + ) -> None: + super().__init__(*args, **kwargs) + + async def close(self) -> None: + for key, value in self.multi_items(): + if isinstance(value, UploadFile): + await value.close() + + +class Headers(Mapping[str, str]): + """ + An immutable, case-insensitive multidict. + """ + + def __init__( + self, + headers: Mapping[str, str] | None = None, + raw: list[tuple[bytes, bytes]] | None = None, + scope: MutableMapping[str, Any] | None = None, + ) -> None: + self._list: list[tuple[bytes, bytes]] = [] + if headers is not None: + assert raw is None, 'Cannot set both "headers" and "raw".' + assert scope is None, 'Cannot set both "headers" and "scope".' + self._list = [(key.lower().encode("latin-1"), value.encode("latin-1")) for key, value in headers.items()] + elif raw is not None: + assert scope is None, 'Cannot set both "raw" and "scope".' + self._list = raw + elif scope is not None: + # scope["headers"] isn't necessarily a list + # it might be a tuple or other iterable + self._list = scope["headers"] = list(scope["headers"]) + + @property + def raw(self) -> list[tuple[bytes, bytes]]: + return list(self._list) + + def keys(self) -> list[str]: # type: ignore[override] + return [key.decode("latin-1") for key, value in self._list] + + def values(self) -> list[str]: # type: ignore[override] + return [value.decode("latin-1") for key, value in self._list] + + def items(self) -> list[tuple[str, str]]: # type: ignore[override] + return [(key.decode("latin-1"), value.decode("latin-1")) for key, value in self._list] + + def getlist(self, key: str) -> list[str]: + get_header_key = key.lower().encode("latin-1") + return [item_value.decode("latin-1") for item_key, item_value in self._list if item_key == get_header_key] + + def mutablecopy(self) -> MutableHeaders: + return MutableHeaders(raw=self._list[:]) + + def __getitem__(self, key: str) -> str: + get_header_key = key.lower().encode("latin-1") + for header_key, header_value in self._list: + if header_key == get_header_key: + return header_value.decode("latin-1") + raise KeyError(key) + + def __contains__(self, key: Any) -> bool: + get_header_key = key.lower().encode("latin-1") + for header_key, header_value in self._list: + if header_key == get_header_key: + return True + return False + + def __iter__(self) -> Iterator[Any]: + return iter(self.keys()) + + def __len__(self) -> int: + return len(self._list) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Headers): + return False + return sorted(self._list) == sorted(other._list) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + as_dict = dict(self.items()) + if len(as_dict) == len(self): + return f"{class_name}({as_dict!r})" + return f"{class_name}(raw={self.raw!r})" + + +class MutableHeaders(Headers): + def __setitem__(self, key: str, value: str) -> None: + """ + Set the header `key` to `value`, removing any duplicate entries. + Retains insertion order. + """ + set_key = key.lower().encode("latin-1") + set_value = value.encode("latin-1") + + found_indexes: list[int] = [] + for idx, (item_key, item_value) in enumerate(self._list): + if item_key == set_key: + found_indexes.append(idx) + + for idx in reversed(found_indexes[1:]): + del self._list[idx] + + if found_indexes: + idx = found_indexes[0] + self._list[idx] = (set_key, set_value) + else: + self._list.append((set_key, set_value)) + + def __delitem__(self, key: str) -> None: + """ + Remove the header `key`. + """ + del_key = key.lower().encode("latin-1") + + pop_indexes: list[int] = [] + for idx, (item_key, item_value) in enumerate(self._list): + if item_key == del_key: + pop_indexes.append(idx) + + for idx in reversed(pop_indexes): + del self._list[idx] + + def __ior__(self, other: Mapping[str, str]) -> MutableHeaders: + if not isinstance(other, Mapping): + raise TypeError(f"Expected a mapping but got {other.__class__.__name__}") + self.update(other) + return self + + def __or__(self, other: Mapping[str, str]) -> MutableHeaders: + if not isinstance(other, Mapping): + raise TypeError(f"Expected a mapping but got {other.__class__.__name__}") + new = self.mutablecopy() + new.update(other) + return new + + @property + def raw(self) -> list[tuple[bytes, bytes]]: + return self._list + + def setdefault(self, key: str, value: str) -> str: + """ + If the header `key` does not exist, then set it to `value`. + Returns the header value. + """ + set_key = key.lower().encode("latin-1") + set_value = value.encode("latin-1") + + for idx, (item_key, item_value) in enumerate(self._list): + if item_key == set_key: + return item_value.decode("latin-1") + self._list.append((set_key, set_value)) + return value + + def update(self, other: Mapping[str, str]) -> None: + for key, val in other.items(): + self[key] = val + + def append(self, key: str, value: str) -> None: + """ + Append a header, preserving any duplicate entries. + """ + append_key = key.lower().encode("latin-1") + append_value = value.encode("latin-1") + self._list.append((append_key, append_value)) + + def add_vary_header(self, vary: str) -> None: + existing = self.get("vary") + if existing is not None: + vary = ", ".join([existing, vary]) + self["vary"] = vary + + +class State: + """ + An object that can be used to store arbitrary state. + + Used for `request.state` and `app.state`. + """ + + _state: dict[str, Any] + + def __init__(self, state: dict[str, Any] | None = None): + if state is None: + state = {} + super().__setattr__("_state", state) + + def __setattr__(self, key: Any, value: Any) -> None: + self._state[key] = value + + def __getattr__(self, key: Any) -> Any: + try: + return self._state[key] + except KeyError: + message = "'{}' object has no attribute '{}'" + raise AttributeError(message.format(self.__class__.__name__, key)) + + def __delattr__(self, key: Any) -> None: + del self._state[key] + + def __getitem__(self, key: str) -> Any: + return self._state[key] + + def __setitem__(self, key: str, value: Any) -> None: + self._state[key] = value + + def __delitem__(self, key: str) -> None: + del self._state[key] + + def __iter__(self) -> Iterator[str]: + return iter(self._state) + + def __len__(self) -> int: + return len(self._state) diff --git a/venv/lib/python3.11/site-packages/starlette/endpoints.py b/venv/lib/python3.11/site-packages/starlette/endpoints.py new file mode 100644 index 0000000000000000000000000000000000000000..bdd51dce9583a64b5d3d0d26fd0e3191078d17ef --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/endpoints.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import json +from collections.abc import Callable, Generator +from typing import Any, Literal + +from starlette import status +from starlette._utils import is_async_callable +from starlette.concurrency import run_in_threadpool +from starlette.exceptions import HTTPException +from starlette.requests import Request +from starlette.responses import PlainTextResponse, Response +from starlette.types import Message, Receive, Scope, Send +from starlette.websockets import WebSocket + + +class HTTPEndpoint: + def __init__(self, scope: Scope, receive: Receive, send: Send) -> None: + assert scope["type"] == "http" + self.scope = scope + self.receive = receive + self.send = send + self._allowed_methods = [ + method + for method in ("GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") + if getattr(self, method.lower(), None) is not None + ] + + def __await__(self) -> Generator[Any, None, None]: + return self.dispatch().__await__() + + async def dispatch(self) -> None: + request = Request(self.scope, receive=self.receive) + handler_name = "get" if request.method == "HEAD" and not hasattr(self, "head") else request.method.lower() + + handler: Callable[[Request], Any] + if request.method in self._allowed_methods or (request.method == "HEAD" and "GET" in self._allowed_methods): + handler = getattr(self, handler_name) + else: + handler = self.method_not_allowed + is_async = is_async_callable(handler) + if is_async: + response = await handler(request) + else: + response = await run_in_threadpool(handler, request) + await response(self.scope, self.receive, self.send) + + async def method_not_allowed(self, request: Request) -> Response: + # If we're running inside a starlette application then raise an + # exception, so that the configurable exception handler can deal with + # returning the response. For plain ASGI apps, just return the response. + headers = {"Allow": ", ".join(self._allowed_methods)} + if "app" in self.scope: + raise HTTPException(status_code=405, headers=headers) + return PlainTextResponse("Method Not Allowed", status_code=405, headers=headers) + + +class WebSocketEndpoint: + encoding: Literal["text", "bytes", "json"] | None = None + + def __init__(self, scope: Scope, receive: Receive, send: Send) -> None: + assert scope["type"] == "websocket" + self.scope = scope + self.receive = receive + self.send = send + + def __await__(self) -> Generator[Any, None, None]: + return self.dispatch().__await__() + + async def dispatch(self) -> None: + websocket = WebSocket(self.scope, receive=self.receive, send=self.send) + await self.on_connect(websocket) + + close_code = status.WS_1000_NORMAL_CLOSURE + + try: + while True: + message = await websocket.receive() + if message["type"] == "websocket.receive": + data = await self.decode(websocket, message) + await self.on_receive(websocket, data) + elif message["type"] == "websocket.disconnect": # pragma: no branch + close_code = int(message.get("code") or status.WS_1000_NORMAL_CLOSURE) + break + except Exception as exc: + close_code = status.WS_1011_INTERNAL_ERROR + raise exc + finally: + await self.on_disconnect(websocket, close_code) + + async def decode(self, websocket: WebSocket, message: Message) -> Any: + if self.encoding == "text": + if "text" not in message: + await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA) + raise RuntimeError("Expected text websocket messages, but got bytes") + return message["text"] + + elif self.encoding == "bytes": + if "bytes" not in message: + await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA) + raise RuntimeError("Expected bytes websocket messages, but got text") + return message["bytes"] + + elif self.encoding == "json": + if message.get("text") is not None: + text = message["text"] + else: + text = message["bytes"].decode("utf-8") + + try: + return json.loads(text) + except json.decoder.JSONDecodeError: + await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA) + raise RuntimeError("Malformed JSON data received.") + + assert self.encoding is None, f"Unsupported 'encoding' attribute {self.encoding}" + return message["text"] if message.get("text") else message["bytes"] + + async def on_connect(self, websocket: WebSocket) -> None: + """Override to handle an incoming websocket connection""" + await websocket.accept() + + async def on_receive(self, websocket: WebSocket, data: Any) -> None: + """Override to handle an incoming websocket message""" + + async def on_disconnect(self, websocket: WebSocket, close_code: int) -> None: + """Override to handle a disconnecting websocket""" diff --git a/venv/lib/python3.11/site-packages/starlette/exceptions.py b/venv/lib/python3.11/site-packages/starlette/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..3cab4a4793a18fb38c0b69a6fc8efb9e8136d724 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/exceptions.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import http +from collections.abc import Mapping + + +class HTTPException(Exception): + def __init__(self, status_code: int, detail: str | None = None, headers: Mapping[str, str] | None = None) -> None: + if detail is None: + detail = http.HTTPStatus(status_code).phrase + self.status_code = status_code + self.detail = detail + self.headers = headers + + def __str__(self) -> str: + return f"{self.status_code}: {self.detail}" + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + return f"{class_name}(status_code={self.status_code!r}, detail={self.detail!r})" + + +class WebSocketException(Exception): + def __init__(self, code: int, reason: str | None = None) -> None: + self.code = code + self.reason = reason or "" + + def __str__(self) -> str: + return f"{self.code}: {self.reason}" + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + return f"{class_name}(code={self.code!r}, reason={self.reason!r})" + + +class StarletteDeprecationWarning(UserWarning): + """A custom deprecation warning for Starlette. + + Unlike the built-in DeprecationWarning, this inherits from UserWarning to ensure it is visible by default, helping + users discover deprecated features without needing to enable warnings explicitly. + + Reference: https://sethmlarson.dev/deprecations-via-warnings-dont-work-for-python-libraries + """ diff --git a/venv/lib/python3.11/site-packages/starlette/formparsers.py b/venv/lib/python3.11/site-packages/starlette/formparsers.py new file mode 100644 index 0000000000000000000000000000000000000000..3f3c598f45428436e518ea70555525fe41a0942c --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/formparsers.py @@ -0,0 +1,297 @@ +from __future__ import annotations + +from collections.abc import AsyncGenerator +from dataclasses import dataclass, field +from enum import Enum +from tempfile import SpooledTemporaryFile +from typing import TYPE_CHECKING +from urllib.parse import unquote_plus + +from starlette.datastructures import FormData, Headers, UploadFile + +if TYPE_CHECKING: + import python_multipart as multipart + from python_multipart.multipart import MultipartCallbacks, QuerystringCallbacks, parse_options_header +else: + try: + try: + import python_multipart as multipart + from python_multipart.multipart import parse_options_header + except ModuleNotFoundError: # pragma: no cover + import multipart + from multipart.multipart import parse_options_header + except ModuleNotFoundError: # pragma: no cover + multipart = None + parse_options_header = None + + +class FormMessage(Enum): + FIELD_START = 1 + FIELD_NAME = 2 + FIELD_DATA = 3 + FIELD_END = 4 + END = 5 + + +@dataclass +class MultipartPart: + content_disposition: bytes | None = None + field_name: str = "" + data: bytearray = field(default_factory=bytearray) + file: UploadFile | None = None + item_headers: list[tuple[bytes, bytes]] = field(default_factory=list) + + +def _user_safe_decode(src: bytes | bytearray, codec: str) -> str: + try: + return src.decode(codec) + except (UnicodeDecodeError, LookupError): + return src.decode("latin-1") + + +class MultiPartException(Exception): + def __init__(self, message: str) -> None: + self.message = message + + +class FormParser: + def __init__( + self, + headers: Headers, + stream: AsyncGenerator[bytes, None], + *, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, # 1MB + ) -> None: + assert multipart is not None, "The `python-multipart` library must be installed to use form parsing." + self.headers = headers + self.stream = stream + self.max_fields = max_fields + self.max_part_size = max_part_size + self.messages: list[tuple[FormMessage, bytes]] = [] + self._current_field_size = 0 + self._current_fields = 0 + + def on_field_start(self) -> None: + self._current_field_size = 0 + message = (FormMessage.FIELD_START, b"") + self.messages.append(message) + + def on_field_name(self, data: bytes, start: int, end: int) -> None: + self._current_field_size += end - start + if self._current_field_size > self.max_part_size: + raise MultiPartException(f"Field exceeded maximum size of {int(self.max_part_size / 1024)}KB.") + message = (FormMessage.FIELD_NAME, data[start:end]) + self.messages.append(message) + + def on_field_data(self, data: bytes, start: int, end: int) -> None: + self._current_field_size += end - start + if self._current_field_size > self.max_part_size: + raise MultiPartException(f"Field exceeded maximum size of {int(self.max_part_size / 1024)}KB.") + message = (FormMessage.FIELD_DATA, data[start:end]) + self.messages.append(message) + + def on_field_end(self) -> None: + self._current_fields += 1 + if self._current_fields > self.max_fields: + raise MultiPartException(f"Too many fields. Maximum number of fields is {self.max_fields}.") + message = (FormMessage.FIELD_END, b"") + self.messages.append(message) + + def on_end(self) -> None: + message = (FormMessage.END, b"") + self.messages.append(message) + + async def parse(self) -> FormData: + # Callbacks dictionary. + callbacks: QuerystringCallbacks = { + "on_field_start": self.on_field_start, + "on_field_name": self.on_field_name, + "on_field_data": self.on_field_data, + "on_field_end": self.on_field_end, + "on_end": self.on_end, + } + + # Create the parser. + parser = multipart.QuerystringParser(callbacks) + field_name = bytearray() + field_value = bytearray() + + items: list[tuple[str, str | UploadFile]] = [] + + # Feed the parser with data from the request. + async for chunk in self.stream: + if chunk: + parser.write(chunk) + else: + parser.finalize() + messages = list(self.messages) + self.messages.clear() + for message_type, message_bytes in messages: + if message_type == FormMessage.FIELD_START: + field_name = bytearray() + field_value = bytearray() + elif message_type == FormMessage.FIELD_NAME: + field_name.extend(message_bytes) + elif message_type == FormMessage.FIELD_DATA: + field_value.extend(message_bytes) + elif message_type == FormMessage.FIELD_END: + name = unquote_plus(field_name.decode("latin-1")) + value = unquote_plus(field_value.decode("latin-1")) + items.append((name, value)) + + return FormData(items) + + +class MultiPartParser: + spool_max_size = 1024 * 1024 # 1MB + """The maximum size of the spooled temporary file used to store file data.""" + max_part_size = 1024 * 1024 # 1MB + """The maximum size of a part in the multipart request.""" + + def __init__( + self, + headers: Headers, + stream: AsyncGenerator[bytes, None], + *, + max_files: int | float = 1000, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, # 1MB + ) -> None: + assert multipart is not None, "The `python-multipart` library must be installed to use form parsing." + self.headers = headers + self.stream = stream + self.max_files = max_files + self.max_fields = max_fields + self.items: list[tuple[str, str | UploadFile]] = [] + self._current_files = 0 + self._current_fields = 0 + self._current_partial_header_name: bytes = b"" + self._current_partial_header_value: bytes = b"" + self._current_part = MultipartPart() + self._charset = "" + self._file_parts_to_write: list[tuple[MultipartPart, bytes]] = [] + self._file_parts_to_finish: list[MultipartPart] = [] + self._files_to_close_on_error: list[SpooledTemporaryFile[bytes]] = [] + self.max_part_size = max_part_size + + def on_part_begin(self) -> None: + self._current_part = MultipartPart() + + def on_part_data(self, data: bytes, start: int, end: int) -> None: + message_bytes = data[start:end] + if self._current_part.file is None: + if len(self._current_part.data) + len(message_bytes) > self.max_part_size: + raise MultiPartException(f"Part exceeded maximum size of {int(self.max_part_size / 1024)}KB.") + self._current_part.data.extend(message_bytes) + else: + self._file_parts_to_write.append((self._current_part, message_bytes)) + + def on_part_end(self) -> None: + if self._current_part.file is None: + self.items.append( + ( + self._current_part.field_name, + _user_safe_decode(self._current_part.data, self._charset), + ) + ) + else: + self._file_parts_to_finish.append(self._current_part) + # The file can be added to the items right now even though it's not + # finished yet, because it will be finished in the `parse()` method, before + # self.items is used in the return value. + self.items.append((self._current_part.field_name, self._current_part.file)) + + def on_header_field(self, data: bytes, start: int, end: int) -> None: + self._current_partial_header_name += data[start:end] + + def on_header_value(self, data: bytes, start: int, end: int) -> None: + self._current_partial_header_value += data[start:end] + + def on_header_end(self) -> None: + field = self._current_partial_header_name.lower() + if field == b"content-disposition": + self._current_part.content_disposition = self._current_partial_header_value + self._current_part.item_headers.append((field, self._current_partial_header_value)) + self._current_partial_header_name = b"" + self._current_partial_header_value = b"" + + def on_headers_finished(self) -> None: + disposition, options = parse_options_header(self._current_part.content_disposition) + try: + self._current_part.field_name = _user_safe_decode(options[b"name"], self._charset) + except KeyError: + raise MultiPartException('The Content-Disposition header field "name" must be provided.') + if b"filename" in options: + self._current_files += 1 + if self._current_files > self.max_files: + raise MultiPartException(f"Too many files. Maximum number of files is {self.max_files}.") + filename = _user_safe_decode(options[b"filename"], self._charset) + tempfile = SpooledTemporaryFile(max_size=self.spool_max_size) + self._files_to_close_on_error.append(tempfile) + self._current_part.file = UploadFile( + file=tempfile, # type: ignore[arg-type] + size=0, + filename=filename, + headers=Headers(raw=self._current_part.item_headers), + ) + else: + self._current_fields += 1 + if self._current_fields > self.max_fields: + raise MultiPartException(f"Too many fields. Maximum number of fields is {self.max_fields}.") + self._current_part.file = None + + def on_end(self) -> None: + pass + + async def parse(self) -> FormData: + # Parse the Content-Type header to get the multipart boundary. + _, params = parse_options_header(self.headers["Content-Type"]) + charset = params.get(b"charset", "utf-8") + if isinstance(charset, bytes): + charset = charset.decode("latin-1") + self._charset = charset + try: + boundary = params[b"boundary"] + except KeyError: + raise MultiPartException("Missing boundary in multipart.") + + # Callbacks dictionary. + callbacks: MultipartCallbacks = { + "on_part_begin": self.on_part_begin, + "on_part_data": self.on_part_data, + "on_part_end": self.on_part_end, + "on_header_field": self.on_header_field, + "on_header_value": self.on_header_value, + "on_header_end": self.on_header_end, + "on_headers_finished": self.on_headers_finished, + "on_end": self.on_end, + } + + # Create the parser. + parser = multipart.MultipartParser(boundary, callbacks) + try: + # Feed the parser with data from the request. + async for chunk in self.stream: + parser.write(chunk) + # Write file data, it needs to use await with the UploadFile methods + # that call the corresponding file methods *in a threadpool*, + # otherwise, if they were called directly in the callback methods above + # (regular, non-async functions), that would block the event loop in + # the main thread. + for part, data in self._file_parts_to_write: + assert part.file # for type checkers + await part.file.write(data) + for part in self._file_parts_to_finish: + assert part.file # for type checkers + await part.file.seek(0) + self._file_parts_to_write.clear() + self._file_parts_to_finish.clear() + parser.finalize() + except (MultiPartException, OSError) as exc: + # Close all the files if there was an error. + for file in self._files_to_close_on_error: + file.close() + raise exc + + return FormData(self.items) diff --git a/venv/lib/python3.11/site-packages/starlette/middleware/__init__.py b/venv/lib/python3.11/site-packages/starlette/middleware/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f8d47d7a0877155ca16a0e489d68c514499b4231 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/middleware/__init__.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Iterator +from typing import Any, ParamSpec, Protocol + +P = ParamSpec("P") + + +_Scope = Any +_Receive = Callable[[], Awaitable[Any]] +_Send = Callable[[Any], Awaitable[None]] +# Since `starlette.types.ASGIApp` type differs from `ASGIApplication` from `asgiref` +# we need to define a more permissive version of ASGIApp that doesn't cause type errors. +_ASGIApp = Callable[[_Scope, _Receive, _Send], Awaitable[None]] + + +class _MiddlewareFactory(Protocol[P]): + def __call__(self, app: _ASGIApp, /, *args: P.args, **kwargs: P.kwargs) -> _ASGIApp: ... # pragma: no cover + + +class Middleware: + def __init__(self, cls: _MiddlewareFactory[P], *args: P.args, **kwargs: P.kwargs) -> None: + self.cls = cls + self.args = args + self.kwargs = kwargs + + def __iter__(self) -> Iterator[Any]: + as_tuple = (self.cls, self.args, self.kwargs) + return iter(as_tuple) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + args_strings = [f"{value!r}" for value in self.args] + option_strings = [f"{key}={value!r}" for key, value in self.kwargs.items()] + name = getattr(self.cls, "__name__", "") + args_repr = ", ".join([name] + args_strings + option_strings) + return f"{class_name}({args_repr})" diff --git a/venv/lib/python3.11/site-packages/starlette/middleware/authentication.py b/venv/lib/python3.11/site-packages/starlette/middleware/authentication.py new file mode 100644 index 0000000000000000000000000000000000000000..935385fd37f3461d3d05b3cc6d32f52c66e1a9b6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/middleware/authentication.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from collections.abc import Callable + +from starlette.authentication import ( + AuthCredentials, + AuthenticationBackend, + AuthenticationError, + UnauthenticatedUser, +) +from starlette.requests import HTTPConnection +from starlette.responses import PlainTextResponse, Response +from starlette.types import ASGIApp, Receive, Scope, Send + + +class AuthenticationMiddleware: + def __init__( + self, + app: ASGIApp, + backend: AuthenticationBackend, + on_error: Callable[[HTTPConnection, AuthenticationError], Response] | None = None, + ) -> None: + self.app = app + self.backend = backend + self.on_error: Callable[[HTTPConnection, AuthenticationError], Response] = ( + on_error if on_error is not None else self.default_on_error + ) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] not in ["http", "websocket"]: + await self.app(scope, receive, send) + return + + conn = HTTPConnection(scope) + try: + auth_result = await self.backend.authenticate(conn) + except AuthenticationError as exc: + response = self.on_error(conn, exc) + if scope["type"] == "websocket": + await send({"type": "websocket.close", "code": 1000}) + else: + await response(scope, receive, send) + return + + if auth_result is None: + auth_result = AuthCredentials(), UnauthenticatedUser() + scope["auth"], scope["user"] = auth_result + await self.app(scope, receive, send) + + @staticmethod + def default_on_error(conn: HTTPConnection, exc: Exception) -> Response: + return PlainTextResponse(str(exc), status_code=400) diff --git a/venv/lib/python3.11/site-packages/starlette/middleware/base.py b/venv/lib/python3.11/site-packages/starlette/middleware/base.py new file mode 100644 index 0000000000000000000000000000000000000000..2579c031b2b8c12f1e3e18b8398320257fc36c64 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/middleware/base.py @@ -0,0 +1,244 @@ +from __future__ import annotations + +from collections.abc import AsyncGenerator, AsyncIterable, Awaitable, Callable, Mapping, MutableMapping +from typing import Any, TypeVar + +import anyio + +from starlette._utils import create_collapsing_task_group +from starlette.requests import ClientDisconnect, Request +from starlette.responses import Response +from starlette.types import ASGIApp, Message, Receive, Scope, Send + +RequestResponseEndpoint = Callable[[Request], Awaitable[Response]] +DispatchFunction = Callable[[Request, RequestResponseEndpoint], Awaitable[Response]] +BodyStreamGenerator = AsyncGenerator[bytes | MutableMapping[str, Any], None] +AsyncContentStream = AsyncIterable[str | bytes | memoryview | MutableMapping[str, Any]] +T = TypeVar("T") + + +class _CachedRequest(Request): + """ + If the user calls Request.body() from their dispatch function + we cache the entire request body in memory and pass that to downstream middlewares, + but if they call Request.stream() then all we do is send an + empty body so that downstream things don't hang forever. + """ + + def __init__(self, scope: Scope, receive: Receive): + super().__init__(scope, receive) + self._wrapped_rcv_disconnected = False + self._wrapped_rcv_consumed = False + self._wrapped_rc_stream = self.stream() + + async def wrapped_receive(self) -> Message: + # wrapped_rcv state 1: disconnected + if self._wrapped_rcv_disconnected: + # we've already sent a disconnect to the downstream app + # we don't need to wait to get another one + # (although most ASGI servers will just keep sending it) + return {"type": "http.disconnect"} + # wrapped_rcv state 1: consumed but not yet disconnected + if self._wrapped_rcv_consumed: + # since the downstream app has consumed us all that is left + # is to send it a disconnect + if self._is_disconnected: + # the middleware has already seen the disconnect + # since we know the client is disconnected no need to wait + # for the message + self._wrapped_rcv_disconnected = True + return {"type": "http.disconnect"} + # we don't know yet if the client is disconnected or not + # so we'll wait until we get that message + msg = await self.receive() + if msg["type"] != "http.disconnect": # pragma: no cover + # at this point a disconnect is all that we should be receiving + # if we get something else, things went wrong somewhere + raise RuntimeError(f"Unexpected message received: {msg['type']}") + self._wrapped_rcv_disconnected = True + return msg + + # wrapped_rcv state 3: not yet consumed + if getattr(self, "_body", None) is not None: + # body() was called, we return it even if the client disconnected + self._wrapped_rcv_consumed = True + return { + "type": "http.request", + "body": self._body, + "more_body": False, + } + elif self._stream_consumed: + # stream() was called to completion + # return an empty body so that downstream apps don't hang + # waiting for a disconnect + self._wrapped_rcv_consumed = True + return { + "type": "http.request", + "body": b"", + "more_body": False, + } + else: + # body() was never called and stream() wasn't consumed + try: + stream = self.stream() + chunk = await stream.__anext__() + self._wrapped_rcv_consumed = self._stream_consumed + return { + "type": "http.request", + "body": chunk, + "more_body": not self._stream_consumed, + } + except ClientDisconnect: + self._wrapped_rcv_disconnected = True + return {"type": "http.disconnect"} + + +class BaseHTTPMiddleware: + def __init__(self, app: ASGIApp, dispatch: DispatchFunction | None = None) -> None: + self.app = app + self.dispatch_func = self.dispatch if dispatch is None else dispatch + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + request = _CachedRequest(scope, receive) + wrapped_receive = request.wrapped_receive + response_sent = anyio.Event() + app_exc: Exception | None = None + exception_already_raised = False + + async def call_next(request: Request) -> Response: + async def receive_or_disconnect() -> Message: + if response_sent.is_set(): + return {"type": "http.disconnect"} + + async with anyio.create_task_group() as task_group: + + async def wrap(func: Callable[[], Awaitable[T]]) -> T: + result = await func() + task_group.cancel_scope.cancel() + return result + + task_group.start_soon(wrap, response_sent.wait) + message = await wrap(wrapped_receive) + + if response_sent.is_set(): + return {"type": "http.disconnect"} + + return message + + async def send_no_error(message: Message) -> None: + try: + await send_stream.send(message) + except anyio.BrokenResourceError: + # recv_stream has been closed, i.e. response_sent has been set. + return + + async def coro() -> None: + nonlocal app_exc + + with send_stream: + try: + await self.app(scope, receive_or_disconnect, send_no_error) + except Exception as exc: + app_exc = exc + + task_group.start_soon(coro) + + try: + message = await recv_stream.receive() + info = message.get("info", None) + if message["type"] == "http.response.debug" and info is not None: + message = await recv_stream.receive() + except anyio.EndOfStream: + if app_exc is not None: + nonlocal exception_already_raised + exception_already_raised = True + # Prevent `anyio.EndOfStream` from polluting app exception context. + # If both cause and context are None then the context is suppressed + # and `anyio.EndOfStream` is not present in the exception traceback. + # If exception cause is not None then it is propagated with + # reraising here. + # If exception has no cause but has context set then the context is + # propagated as a cause with the reraise. This is necessary in order + # to prevent `anyio.EndOfStream` from polluting the exception + # context. + raise app_exc from app_exc.__cause__ or app_exc.__context__ + raise RuntimeError("No response returned.") + + assert message["type"] == "http.response.start" + + async def body_stream() -> BodyStreamGenerator: + async for message in recv_stream: + if message["type"] == "http.response.pathsend": + yield message + break + assert message["type"] == "http.response.body", f"Unexpected message: {message}" + body = message.get("body", b"") + if body: + yield body + if not message.get("more_body", False): + break + + response = _StreamingResponse(status_code=message["status"], content=body_stream(), info=info) + response.raw_headers = message["headers"] + return response + + streams: anyio.create_memory_object_stream[Message] = anyio.create_memory_object_stream() + send_stream, recv_stream = streams + with recv_stream, send_stream: + async with create_collapsing_task_group() as task_group: + response = await self.dispatch_func(request, call_next) + await response(scope, wrapped_receive, send) + response_sent.set() + recv_stream.close() + if app_exc is not None and not exception_already_raised: + raise app_exc + + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: + raise NotImplementedError() # pragma: no cover + + +class _StreamingResponse(Response): + def __init__( + self, + content: AsyncContentStream, + status_code: int = 200, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + info: Mapping[str, Any] | None = None, + ) -> None: + self.info = info + self.body_iterator = content + self.status_code = status_code + self.media_type = media_type + self.init_headers(headers) + self.background = None + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if self.info is not None: + await send({"type": "http.response.debug", "info": self.info}) + await send( + { + "type": "http.response.start", + "status": self.status_code, + "headers": self.raw_headers, + } + ) + + should_close_body = True + async for chunk in self.body_iterator: + if isinstance(chunk, dict): + # We got an ASGI message which is not response body (eg: pathsend) + should_close_body = False + await send(chunk) + continue + await send({"type": "http.response.body", "body": chunk, "more_body": True}) + + if should_close_body: + await send({"type": "http.response.body", "body": b"", "more_body": False}) + + if self.background: + await self.background() diff --git a/venv/lib/python3.11/site-packages/starlette/middleware/cors.py b/venv/lib/python3.11/site-packages/starlette/middleware/cors.py new file mode 100644 index 0000000000000000000000000000000000000000..b65fc15a02b0fe1d367b8f4190126c7ce1caae3d --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/middleware/cors.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +import functools +import re +from collections.abc import Sequence + +from starlette.datastructures import Headers, MutableHeaders +from starlette.responses import PlainTextResponse, Response +from starlette.types import ASGIApp, Message, Receive, Scope, Send + +ALL_METHODS = ("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT") +SAFELISTED_HEADERS = {"Accept", "Accept-Language", "Content-Language", "Content-Type"} + + +class CORSMiddleware: + def __init__( + self, + app: ASGIApp, + allow_origins: Sequence[str] = (), + allow_methods: Sequence[str] = ("GET",), + allow_headers: Sequence[str] = (), + allow_credentials: bool = False, + allow_origin_regex: str | None = None, + allow_private_network: bool = False, + expose_headers: Sequence[str] = (), + max_age: int = 600, + ) -> None: + if "*" in allow_methods: + allow_methods = ALL_METHODS + + compiled_allow_origin_regex = None + if allow_origin_regex is not None: + compiled_allow_origin_regex = re.compile(allow_origin_regex) + + allow_all_origins = "*" in allow_origins + allow_all_headers = "*" in allow_headers + preflight_explicit_allow_origin = not allow_all_origins or allow_credentials + + simple_headers: dict[str, str] = {} + if allow_all_origins: + simple_headers["Access-Control-Allow-Origin"] = "*" + if allow_credentials: + simple_headers["Access-Control-Allow-Credentials"] = "true" + if expose_headers: + simple_headers["Access-Control-Expose-Headers"] = ", ".join(expose_headers) + + preflight_headers: dict[str, str] = {} + if preflight_explicit_allow_origin: + # The origin value will be set in preflight_response() if it is allowed. + preflight_headers["Vary"] = "Origin" + else: + preflight_headers["Access-Control-Allow-Origin"] = "*" + preflight_headers.update( + { + "Access-Control-Allow-Methods": ", ".join(allow_methods), + "Access-Control-Max-Age": str(max_age), + } + ) + allow_headers = sorted(SAFELISTED_HEADERS | set(allow_headers)) + if allow_headers and not allow_all_headers: + preflight_headers["Access-Control-Allow-Headers"] = ", ".join(allow_headers) + if allow_credentials: + preflight_headers["Access-Control-Allow-Credentials"] = "true" + + self.app = app + self.allow_origins = allow_origins + self.allow_methods = allow_methods + self.allow_headers = [h.lower() for h in allow_headers] + self.allow_all_origins = allow_all_origins + self.allow_all_headers = allow_all_headers + self.allow_credentials = allow_credentials + self.preflight_explicit_allow_origin = preflight_explicit_allow_origin + self.allow_origin_regex = compiled_allow_origin_regex + self.allow_private_network = allow_private_network + self.simple_headers = simple_headers + self.preflight_headers = preflight_headers + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": # pragma: no cover + await self.app(scope, receive, send) + return + + method = scope["method"] + headers = Headers(scope=scope) + origin = headers.get("origin") + + if origin is None: + await self.app(scope, receive, send) + return + + if method == "OPTIONS" and "access-control-request-method" in headers: + response = self.preflight_response(request_headers=headers) + await response(scope, receive, send) + return + + await self.simple_response(scope, receive, send, request_headers=headers) + + def is_allowed_origin(self, origin: str) -> bool: + if self.allow_all_origins: + return True + + if self.allow_origin_regex is not None and self.allow_origin_regex.fullmatch(origin): + return True + + return origin in self.allow_origins + + def preflight_response(self, request_headers: Headers) -> Response: + requested_origin = request_headers["origin"] + requested_method = request_headers["access-control-request-method"] + requested_headers = request_headers.get("access-control-request-headers") + requested_private_network = request_headers.get("access-control-request-private-network") + + headers = dict(self.preflight_headers) + failures: list[str] = [] + + if self.is_allowed_origin(origin=requested_origin): + if self.preflight_explicit_allow_origin: + # The "else" case is already accounted for in self.preflight_headers + # and the value would be "*". + headers["Access-Control-Allow-Origin"] = requested_origin + else: + failures.append("origin") + + if requested_method not in self.allow_methods: + failures.append("method") + + # If we allow all headers, then we have to mirror back any requested + # headers in the response. + if self.allow_all_headers and requested_headers is not None: + headers["Access-Control-Allow-Headers"] = requested_headers + elif requested_headers is not None: + for header in [h.lower() for h in requested_headers.split(",")]: + if header.strip() not in self.allow_headers: + failures.append("headers") + break + + if requested_private_network is not None: + if self.allow_private_network: + headers["Access-Control-Allow-Private-Network"] = "true" + else: + failures.append("private-network") + + # We don't strictly need to use 400 responses here, since its up to + # the browser to enforce the CORS policy, but its more informative + # if we do. + if failures: + failure_text = "Disallowed CORS " + ", ".join(failures) + return PlainTextResponse(failure_text, status_code=400, headers=headers) + + return PlainTextResponse("OK", status_code=200, headers=headers) + + async def simple_response(self, scope: Scope, receive: Receive, send: Send, request_headers: Headers) -> None: + send = functools.partial(self.send, send=send, request_headers=request_headers) + await self.app(scope, receive, send) + + async def send(self, message: Message, send: Send, request_headers: Headers) -> None: + if message["type"] != "http.response.start": + await send(message) + return + + message.setdefault("headers", []) + headers = MutableHeaders(scope=message) + headers.update(self.simple_headers) + origin = request_headers["Origin"] + + # If credentials are allowed, then we must respond with the specific origin instead of '*'. + if self.allow_all_origins and self.allow_credentials: + self.allow_explicit_origin(headers, origin) + + # If we only allow specific origins, then we have to mirror back the Origin header in the response. + elif not self.allow_all_origins and self.is_allowed_origin(origin=origin): + self.allow_explicit_origin(headers, origin) + + await send(message) + + @staticmethod + def allow_explicit_origin(headers: MutableHeaders, origin: str) -> None: + headers["Access-Control-Allow-Origin"] = origin + headers.add_vary_header("Origin") diff --git a/venv/lib/python3.11/site-packages/starlette/middleware/errors.py b/venv/lib/python3.11/site-packages/starlette/middleware/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..5a302a83652d81e992f649875a7cea25c2861d3e --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/middleware/errors.py @@ -0,0 +1,259 @@ +from __future__ import annotations + +import html +import inspect +import sys +import traceback + +from starlette._utils import is_async_callable +from starlette.concurrency import run_in_threadpool +from starlette.requests import Request +from starlette.responses import HTMLResponse, PlainTextResponse, Response +from starlette.types import ASGIApp, ExceptionHandler, Message, Receive, Scope, Send + +STYLES = """ +p { + color: #211c1c; +} +.traceback-container { + border: 1px solid #038BB8; +} +.traceback-title { + background-color: #038BB8; + color: lemonchiffon; + padding: 12px; + font-size: 20px; + margin-top: 0px; +} +.frame-line { + padding-left: 10px; + font-family: monospace; +} +.frame-filename { + font-family: monospace; +} +.center-line { + background-color: #038BB8; + color: #f9f6e1; + padding: 5px 0px 5px 5px; +} +.lineno { + margin-right: 5px; +} +.frame-title { + font-weight: unset; + padding: 10px 10px 10px 10px; + background-color: #E4F4FD; + margin-right: 10px; + color: #191f21; + font-size: 17px; + border: 1px solid #c7dce8; +} +.collapse-btn { + float: right; + padding: 0px 5px 1px 5px; + border: solid 1px #96aebb; + cursor: pointer; +} +.collapsed { + display: none; +} +.source-code { + font-family: courier; + font-size: small; + padding-bottom: 10px; +} +""" + +JS = """ + +""" + +TEMPLATE = """ + + + + Starlette Debugger + + +

500 Server Error

+

{error}

+
+

Traceback

+
{exc_html}
+
+ {js} + + +""" + +FRAME_TEMPLATE = """ +
+

File {frame_filename}, + line {frame_lineno}, + in {frame_name} + {collapse_button} +

+
{code_context}
+
+""" # noqa: E501 + +LINE = """ +

+{lineno}. {line}

+""" + +CENTER_LINE = """ +

+{lineno}. {line}

+""" + + +class ServerErrorMiddleware: + """ + Handles returning 500 responses when a server error occurs. + + If 'debug' is set, then traceback responses will be returned, + otherwise the designated 'handler' will be called. + + This middleware class should generally be used to wrap *everything* + else up, so that unhandled exceptions anywhere in the stack + always result in an appropriate 500 response. + """ + + def __init__( + self, + app: ASGIApp, + handler: ExceptionHandler | None = None, + debug: bool = False, + ) -> None: + self.app = app + self.handler = handler + self.debug = debug + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + response_started = False + + async def _send(message: Message) -> None: + nonlocal response_started, send + + if message["type"] == "http.response.start": + response_started = True + await send(message) + + try: + await self.app(scope, receive, _send) + except Exception as exc: + request = Request(scope) + if self.debug: + # In debug mode, return traceback responses. + response = self.debug_response(request, exc) + elif self.handler is None: + # Use our default 500 error handler. + response = self.error_response(request, exc) + else: + # Use an installed 500 error handler. + if is_async_callable(self.handler): + response = await self.handler(request, exc) # type: ignore[assignment, arg-type] + else: + response = await run_in_threadpool(self.handler, request, exc) # type: ignore[arg-type] + + if not response_started: + await response(scope, receive, send) + + # We always continue to raise the exception. + # This allows servers to log the error, or allows test clients + # to optionally raise the error within the test case. + raise exc + + def format_line(self, index: int, line: str, frame_lineno: int, frame_index: int) -> str: + values = { + # HTML escape - line could contain < or > + "line": html.escape(line).replace(" ", " "), + "lineno": (frame_lineno - frame_index) + index, + } + + if index != frame_index: + return LINE.format(**values) + return CENTER_LINE.format(**values) + + def generate_frame_html(self, frame: inspect.FrameInfo, is_collapsed: bool) -> str: + code_context = "".join( + self.format_line( + index, + line, + frame.lineno, + frame.index, # type: ignore[arg-type] + ) + for index, line in enumerate(frame.code_context or []) + ) + + values = { + # HTML escape - filename could contain < or >, especially if it's a virtual + # file e.g. in the REPL + "frame_filename": html.escape(frame.filename), + "frame_lineno": frame.lineno, + # HTML escape - if you try very hard it's possible to name a function with < + # or > + "frame_name": html.escape(frame.function), + "code_context": code_context, + "collapsed": "collapsed" if is_collapsed else "", + "collapse_button": "+" if is_collapsed else "‒", + } + return FRAME_TEMPLATE.format(**values) + + def generate_html(self, exc: Exception, limit: int = 7) -> str: + traceback_obj = traceback.TracebackException.from_exception(exc, capture_locals=True) + + exc_html = "" + is_collapsed = False + exc_traceback = exc.__traceback__ + if exc_traceback is not None: + frames = inspect.getinnerframes(exc_traceback, limit) + for frame in reversed(frames): + exc_html += self.generate_frame_html(frame, is_collapsed) + is_collapsed = True + + if sys.version_info >= (3, 13): # pragma: no cover + exc_type_str = traceback_obj.exc_type_str + else: # pragma: no cover + exc_type_str = traceback_obj.exc_type.__name__ + + # escape error class and text + error = f"{html.escape(exc_type_str)}: {html.escape(str(traceback_obj))}" + + return TEMPLATE.format(styles=STYLES, js=JS, error=error, exc_html=exc_html) + + def generate_plain_text(self, exc: Exception) -> str: + return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) + + def debug_response(self, request: Request, exc: Exception) -> Response: + accept = request.headers.get("accept", "") + + if "text/html" in accept: + content = self.generate_html(exc) + return HTMLResponse(content, status_code=500) + content = self.generate_plain_text(exc) + return PlainTextResponse(content, status_code=500) + + def error_response(self, request: Request, exc: Exception) -> Response: + return PlainTextResponse("Internal Server Error", status_code=500) diff --git a/venv/lib/python3.11/site-packages/starlette/middleware/exceptions.py b/venv/lib/python3.11/site-packages/starlette/middleware/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..6c98558f4db50007dd3ddee1892454a5a73f6abd --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/middleware/exceptions.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from starlette._exception_handler import ( + ExceptionHandlers, + StatusHandlers, + wrap_app_handling_exceptions, +) +from starlette.exceptions import HTTPException, WebSocketException +from starlette.requests import Request +from starlette.responses import PlainTextResponse, Response +from starlette.types import ASGIApp, ExceptionHandler, Receive, Scope, Send +from starlette.websockets import WebSocket + + +class ExceptionMiddleware: + def __init__( + self, + app: ASGIApp, + handlers: Mapping[Any, ExceptionHandler] | None = None, + debug: bool = False, + ) -> None: + self.app = app + self.debug = debug # TODO: We ought to handle 404 cases if debug is set. + self._status_handlers: StatusHandlers = {} + self._exception_handlers: ExceptionHandlers = { + HTTPException: self.http_exception, + WebSocketException: self.websocket_exception, + } + if handlers is not None: # pragma: no branch + for key, value in handlers.items(): + self.add_exception_handler(key, value) + + def add_exception_handler( + self, + exc_class_or_status_code: int | type[Exception], + handler: ExceptionHandler, + ) -> None: + if isinstance(exc_class_or_status_code, int): + self._status_handlers[exc_class_or_status_code] = handler + else: + assert issubclass(exc_class_or_status_code, Exception) + self._exception_handlers[exc_class_or_status_code] = handler + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] not in ("http", "websocket"): + await self.app(scope, receive, send) + return + + scope["starlette.exception_handlers"] = ( + self._exception_handlers, + self._status_handlers, + ) + + conn: Request | WebSocket + if scope["type"] == "http": + conn = Request(scope, receive, send) + else: + conn = WebSocket(scope, receive, send) + + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) + + async def http_exception(self, request: Request, exc: Exception) -> Response: + assert isinstance(exc, HTTPException) + if exc.status_code in {204, 304}: + return Response(status_code=exc.status_code, headers=exc.headers) + return PlainTextResponse(exc.detail, status_code=exc.status_code, headers=exc.headers) + + async def websocket_exception(self, websocket: WebSocket, exc: Exception) -> None: + assert isinstance(exc, WebSocketException) + await websocket.close(code=exc.code, reason=exc.reason) # pragma: no cover diff --git a/venv/lib/python3.11/site-packages/starlette/middleware/gzip.py b/venv/lib/python3.11/site-packages/starlette/middleware/gzip.py new file mode 100644 index 0000000000000000000000000000000000000000..abd898b2daffb4c1cc2b47b5931060ce34f2abfd --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/middleware/gzip.py @@ -0,0 +1,145 @@ +import gzip +import io +from typing import NoReturn + +from starlette.datastructures import Headers, MutableHeaders +from starlette.types import ASGIApp, Message, Receive, Scope, Send + +DEFAULT_EXCLUDED_CONTENT_TYPES = ("text/event-stream",) + + +class GZipMiddleware: + def __init__(self, app: ASGIApp, minimum_size: int = 500, compresslevel: int = 9) -> None: + self.app = app + self.minimum_size = minimum_size + self.compresslevel = compresslevel + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": # pragma: no cover + await self.app(scope, receive, send) + return + + headers = Headers(scope=scope) + responder: ASGIApp + if "gzip" in headers.get("Accept-Encoding", ""): + responder = GZipResponder(self.app, self.minimum_size, compresslevel=self.compresslevel) + else: + responder = IdentityResponder(self.app, self.minimum_size) + + await responder(scope, receive, send) + + +class IdentityResponder: + content_encoding: str + + def __init__(self, app: ASGIApp, minimum_size: int) -> None: + self.app = app + self.minimum_size = minimum_size + self.send: Send = unattached_send + self.initial_message: Message = {} + self.started = False + self.content_encoding_set = False + self.content_type_is_excluded = False + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + self.send = send + await self.app(scope, receive, self.send_with_compression) + + async def send_with_compression(self, message: Message) -> None: + message_type = message["type"] + if message_type == "http.response.start": + # Don't send the initial message until we've determined how to + # modify the outgoing headers correctly. + self.initial_message = message + headers = Headers(raw=self.initial_message["headers"]) + self.content_encoding_set = "content-encoding" in headers + self.content_type_is_excluded = headers.get("content-type", "").startswith(DEFAULT_EXCLUDED_CONTENT_TYPES) + elif message_type == "http.response.body" and (self.content_encoding_set or self.content_type_is_excluded): + if not self.started: + self.started = True + await self.send(self.initial_message) + await self.send(message) + elif message_type == "http.response.body" and not self.started: + self.started = True + body = message.get("body", b"") + more_body = message.get("more_body", False) + if len(body) < self.minimum_size and not more_body: + # Don't apply compression to small outgoing responses. + await self.send(self.initial_message) + await self.send(message) + elif not more_body: + # Standard response. + body = self.apply_compression(body, more_body=False) + + headers = MutableHeaders(raw=self.initial_message["headers"]) + headers.add_vary_header("Accept-Encoding") + if body != message["body"]: + headers["Content-Encoding"] = self.content_encoding + headers["Content-Length"] = str(len(body)) + message["body"] = body + + await self.send(self.initial_message) + await self.send(message) + else: + # Initial body in streaming response. + body = self.apply_compression(body, more_body=True) + + headers = MutableHeaders(raw=self.initial_message["headers"]) + headers.add_vary_header("Accept-Encoding") + if body != message["body"]: + headers["Content-Encoding"] = self.content_encoding + del headers["Content-Length"] + message["body"] = body + + await self.send(self.initial_message) + await self.send(message) + elif message_type == "http.response.body": + # Remaining body in streaming response. + body = message.get("body", b"") + more_body = message.get("more_body", False) + + message["body"] = self.apply_compression(body, more_body=more_body) + + await self.send(message) + elif message_type == "http.response.pathsend": # pragma: no branch + # Don't apply GZip to pathsend responses + await self.send(self.initial_message) + await self.send(message) + + def apply_compression(self, body: bytes, *, more_body: bool) -> bytes: + """Apply compression on the response body. + + If more_body is False, any compression file should be closed. If it + isn't, it won't be closed automatically until all background tasks + complete. + """ + return body + + +class GZipResponder(IdentityResponder): + content_encoding = "gzip" + + def __init__(self, app: ASGIApp, minimum_size: int, compresslevel: int = 9) -> None: + super().__init__(app, minimum_size) + + self.gzip_buffer = io.BytesIO() + self.gzip_file = gzip.GzipFile(mode="wb", fileobj=self.gzip_buffer, compresslevel=compresslevel) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + with self.gzip_buffer, self.gzip_file: + await super().__call__(scope, receive, send) + + def apply_compression(self, body: bytes, *, more_body: bool) -> bytes: + self.gzip_file.write(body) + if not more_body: + self.gzip_file.close() + + body = self.gzip_buffer.getvalue() + self.gzip_buffer.seek(0) + self.gzip_buffer.truncate() + + return body + + +async def unattached_send(message: Message) -> NoReturn: + raise RuntimeError("send awaitable not set") # pragma: no cover diff --git a/venv/lib/python3.11/site-packages/starlette/middleware/httpsredirect.py b/venv/lib/python3.11/site-packages/starlette/middleware/httpsredirect.py new file mode 100644 index 0000000000000000000000000000000000000000..a8359067ff7afb80e979042077d5fa0fff119ddf --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/middleware/httpsredirect.py @@ -0,0 +1,19 @@ +from starlette.datastructures import URL +from starlette.responses import RedirectResponse +from starlette.types import ASGIApp, Receive, Scope, Send + + +class HTTPSRedirectMiddleware: + def __init__(self, app: ASGIApp) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] in ("http", "websocket") and scope["scheme"] in ("http", "ws"): + url = URL(scope=scope) + redirect_scheme = {"http": "https", "ws": "wss"}[url.scheme] + netloc = url.hostname if url.port in (80, 443) else url.netloc + url = url.replace(scheme=redirect_scheme, netloc=netloc) + response = RedirectResponse(url, status_code=307) + await response(scope, receive, send) + else: + await self.app(scope, receive, send) diff --git a/venv/lib/python3.11/site-packages/starlette/middleware/sessions.py b/venv/lib/python3.11/site-packages/starlette/middleware/sessions.py new file mode 100644 index 0000000000000000000000000000000000000000..aa0383f98a61903e89e1b63e4ae8483d597f538f --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/middleware/sessions.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +import json +import typing +from base64 import b64decode, b64encode +from typing import Literal + +import itsdangerous +from itsdangerous.exc import BadSignature + +from starlette.datastructures import MutableHeaders, Secret +from starlette.requests import HTTPConnection +from starlette.types import ASGIApp, Message, Receive, Scope, Send + + +class SessionMiddleware: + def __init__( + self, + app: ASGIApp, + secret_key: str | Secret, + session_cookie: str = "session", + max_age: int | None = 14 * 24 * 60 * 60, # 14 days, in seconds + path: str = "/", + same_site: Literal["lax", "strict", "none"] = "lax", + https_only: bool = False, + domain: str | None = None, + ) -> None: + self.app = app + self.signer = itsdangerous.TimestampSigner(str(secret_key)) + self.session_cookie = session_cookie + self.max_age = max_age + self.path = path + self.security_flags = "httponly; samesite=" + same_site + if https_only: # Secure flag can be used with HTTPS only + self.security_flags += "; secure" + if domain is not None: + self.security_flags += f"; domain={domain}" + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] not in ("http", "websocket"): # pragma: no cover + await self.app(scope, receive, send) + return + + connection = HTTPConnection(scope) + initial_session_was_empty = True + + if self.session_cookie in connection.cookies: + data = connection.cookies[self.session_cookie].encode("utf-8") + try: + data = self.signer.unsign(data, max_age=self.max_age) + scope["session"] = Session(json.loads(b64decode(data))) + initial_session_was_empty = False + except BadSignature: + scope["session"] = Session() + else: + scope["session"] = Session() + + async def send_wrapper(message: Message) -> None: + if message["type"] == "http.response.start": + session: Session = scope["session"] + headers = MutableHeaders(scope=message) + if session.accessed: + headers.add_vary_header("Cookie") + if session.modified and session: + # We have session data to persist. + data = b64encode(json.dumps(session).encode("utf-8")) + data = self.signer.sign(data) + header_value = "{session_cookie}={data}; path={path}; {max_age}{security_flags}".format( + session_cookie=self.session_cookie, + data=data.decode("utf-8"), + path=self.path, + max_age=f"Max-Age={self.max_age}; " if self.max_age else "", + security_flags=self.security_flags, + ) + headers.append("Set-Cookie", header_value) + elif session.modified and not initial_session_was_empty: + # The session has been cleared. + header_value = "{session_cookie}={data}; path={path}; {expires}{security_flags}".format( + session_cookie=self.session_cookie, + data="null", + path=self.path, + expires="expires=Thu, 01 Jan 1970 00:00:00 GMT; ", + security_flags=self.security_flags, + ) + headers.append("Set-Cookie", header_value) + await send(message) + + await self.app(scope, receive, send_wrapper) + + +class Session(dict[str, typing.Any]): + accessed: bool = False + modified: bool = False + + def mark_accessed(self) -> None: + self.accessed = True + + def mark_modified(self) -> None: + self.accessed = True + self.modified = True + + def __setitem__(self, key: str, value: typing.Any) -> None: + self.mark_modified() + super().__setitem__(key, value) + + def __delitem__(self, key: str) -> None: + self.mark_modified() + super().__delitem__(key) + + def clear(self) -> None: + self.mark_modified() + super().clear() + + def pop(self, key: str, *args: typing.Any) -> typing.Any: + self.modified = self.modified or key in self + return super().pop(key, *args) + + def setdefault(self, key: str, default: typing.Any = None) -> typing.Any: + if key not in self: + self.mark_modified() + return super().setdefault(key, default) + + def update(self, *args: typing.Any, **kwargs: typing.Any) -> None: + self.mark_modified() + super().update(*args, **kwargs) diff --git a/venv/lib/python3.11/site-packages/starlette/middleware/trustedhost.py b/venv/lib/python3.11/site-packages/starlette/middleware/trustedhost.py new file mode 100644 index 0000000000000000000000000000000000000000..98451e29fef8a22379b5c1ef22c0f7f077eca83a --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/middleware/trustedhost.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from collections.abc import Sequence + +from starlette.datastructures import URL, Headers +from starlette.responses import PlainTextResponse, RedirectResponse, Response +from starlette.types import ASGIApp, Receive, Scope, Send + +ENFORCE_DOMAIN_WILDCARD = "Domain wildcard patterns must be like '*.example.com'." + + +class TrustedHostMiddleware: + def __init__( + self, + app: ASGIApp, + allowed_hosts: Sequence[str] | None = None, + www_redirect: bool = True, + ) -> None: + if allowed_hosts is None: + allowed_hosts = ["*"] + + for pattern in allowed_hosts: + assert "*" not in pattern[1:], ENFORCE_DOMAIN_WILDCARD + if pattern.startswith("*") and pattern != "*": + assert pattern.startswith("*."), ENFORCE_DOMAIN_WILDCARD + self.app = app + self.allowed_hosts = list(allowed_hosts) + self.allow_any = "*" in allowed_hosts + self.www_redirect = www_redirect + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if self.allow_any or scope["type"] not in ( + "http", + "websocket", + ): # pragma: no cover + await self.app(scope, receive, send) + return + + headers = Headers(scope=scope) + host = headers.get("host", "").split(":")[0] + is_valid_host = False + found_www_redirect = False + for pattern in self.allowed_hosts: + if host == pattern or (pattern.startswith("*") and host.endswith(pattern[1:])): + is_valid_host = True + break + elif "www." + host == pattern: + found_www_redirect = True + + if is_valid_host: + await self.app(scope, receive, send) + else: + response: Response + if found_www_redirect and self.www_redirect: + url = URL(scope=scope) + redirect_url = url.replace(netloc="www." + url.netloc) + response = RedirectResponse(url=str(redirect_url)) + else: + response = PlainTextResponse("Invalid host header", status_code=400) + await response(scope, receive, send) diff --git a/venv/lib/python3.11/site-packages/starlette/middleware/wsgi.py b/venv/lib/python3.11/site-packages/starlette/middleware/wsgi.py new file mode 100644 index 0000000000000000000000000000000000000000..f456fc413d4ee15ce9e82e6d06676b924ce7237e --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/middleware/wsgi.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +import io +import math +import sys +import warnings +from collections.abc import Callable, MutableMapping +from typing import Any + +import anyio +from anyio.abc import ObjectReceiveStream, ObjectSendStream + +from starlette._utils import create_collapsing_task_group +from starlette.exceptions import StarletteDeprecationWarning +from starlette.types import Receive, Scope, Send + +warnings.warn( + "starlette.middleware.wsgi is deprecated and will be removed in a future release. " + "Please refer to https://github.com/abersheeran/a2wsgi as a replacement.", + StarletteDeprecationWarning, + stacklevel=2, +) + + +def build_environ(scope: Scope, body: bytes) -> dict[str, Any]: + """ + Builds a scope and request body into a WSGI environ object. + """ + + script_name = scope.get("root_path", "").encode("utf8").decode("latin1") + path_info = scope["path"].encode("utf8").decode("latin1") + if path_info.startswith(script_name): + path_info = path_info[len(script_name) :] + + environ = { + "REQUEST_METHOD": scope["method"], + "SCRIPT_NAME": script_name, + "PATH_INFO": path_info, + "QUERY_STRING": scope["query_string"].decode("ascii"), + "SERVER_PROTOCOL": f"HTTP/{scope['http_version']}", + "wsgi.version": (1, 0), + "wsgi.url_scheme": scope.get("scheme", "http"), + "wsgi.input": io.BytesIO(body), + "wsgi.errors": sys.stdout, + "wsgi.multithread": True, + "wsgi.multiprocess": True, + "wsgi.run_once": False, + } + + # Get server name and port - required in WSGI, not in ASGI + server = scope.get("server") or ("localhost", 80) + environ["SERVER_NAME"] = server[0] + environ["SERVER_PORT"] = server[1] + + # Get client IP address + if scope.get("client"): + environ["REMOTE_ADDR"] = scope["client"][0] + + # Go through headers and make them into environ entries + for name, value in scope.get("headers", []): + name = name.decode("latin1") + if name == "content-length": + corrected_name = "CONTENT_LENGTH" + elif name == "content-type": + corrected_name = "CONTENT_TYPE" + else: + corrected_name = f"HTTP_{name}".upper().replace("-", "_") + # HTTPbis say only ASCII chars are allowed in headers, but we latin1 just in + # case + value = value.decode("latin1") + if corrected_name in environ: + value = environ[corrected_name] + "," + value + environ[corrected_name] = value + return environ + + +class WSGIMiddleware: + def __init__(self, app: Callable[..., Any]) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + assert scope["type"] == "http" + responder = WSGIResponder(self.app, scope) + await responder(receive, send) + + +class WSGIResponder: + stream_send: ObjectSendStream[MutableMapping[str, Any]] + stream_receive: ObjectReceiveStream[MutableMapping[str, Any]] + + def __init__(self, app: Callable[..., Any], scope: Scope) -> None: + self.app = app + self.scope = scope + self.status = None + self.response_headers = None + self.stream_send, self.stream_receive = anyio.create_memory_object_stream(math.inf) + self.response_started = False + self.exc_info: Any = None + + async def __call__(self, receive: Receive, send: Send) -> None: + body = b"" + more_body = True + while more_body: + message = await receive() + body += message.get("body", b"") + more_body = message.get("more_body", False) + environ = build_environ(self.scope, body) + + async with create_collapsing_task_group() as task_group: + task_group.start_soon(self.sender, send) + async with self.stream_send: + await anyio.to_thread.run_sync(self.wsgi, environ, self.start_response) + if self.exc_info is not None: + raise self.exc_info[0].with_traceback(self.exc_info[1], self.exc_info[2]) + + async def sender(self, send: Send) -> None: + async with self.stream_receive: + async for message in self.stream_receive: + await send(message) + + def start_response( + self, + status: str, + response_headers: list[tuple[str, str]], + exc_info: Any = None, + ) -> None: + self.exc_info = exc_info + if not self.response_started: # pragma: no branch + self.response_started = True + status_code_string, _ = status.split(" ", 1) + status_code = int(status_code_string) + headers = [ + (name.strip().encode("ascii").lower(), value.strip().encode("ascii")) + for name, value in response_headers + ] + anyio.from_thread.run( + self.stream_send.send, + { + "type": "http.response.start", + "status": status_code, + "headers": headers, + }, + ) + + def wsgi( + self, + environ: dict[str, Any], + start_response: Callable[..., Any], + ) -> None: + for chunk in self.app(environ, start_response): + anyio.from_thread.run( + self.stream_send.send, + {"type": "http.response.body", "body": chunk, "more_body": True}, + ) + + anyio.from_thread.run(self.stream_send.send, {"type": "http.response.body", "body": b""}) diff --git a/venv/lib/python3.11/site-packages/starlette/py.typed b/venv/lib/python3.11/site-packages/starlette/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/starlette/requests.py b/venv/lib/python3.11/site-packages/starlette/requests.py new file mode 100644 index 0000000000000000000000000000000000000000..d4f3f4be6d2159b0c2a5838f13ea2ce7e683398e --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/requests.py @@ -0,0 +1,348 @@ +from __future__ import annotations + +import json +import sys +from collections.abc import AsyncGenerator, Iterator, Mapping +from http import cookies as http_cookies +from typing import TYPE_CHECKING, Any, Generic, NoReturn, cast + +import anyio + +from starlette._utils import AwaitableOrContextManager, AwaitableOrContextManagerWrapper +from starlette.datastructures import URL, Address, FormData, Headers, QueryParams, State +from starlette.exceptions import HTTPException +from starlette.formparsers import FormParser, MultiPartException, MultiPartParser +from starlette.types import Message, Receive, Scope, Send + +if TYPE_CHECKING: + from python_multipart.multipart import parse_options_header + + from starlette.applications import Starlette + from starlette.middleware.sessions import Session + from starlette.routing import Router +else: + try: + try: + from python_multipart.multipart import parse_options_header + except ModuleNotFoundError: # pragma: no cover + from multipart.multipart import parse_options_header + except ModuleNotFoundError: # pragma: no cover + parse_options_header = None + +if sys.version_info >= (3, 13): # pragma: no cover + from typing import TypeVar +else: # pragma: no cover + from typing_extensions import TypeVar + +SERVER_PUSH_HEADERS_TO_COPY = { + "accept", + "accept-encoding", + "accept-language", + "cache-control", + "user-agent", +} + + +def cookie_parser(cookie_string: str) -> dict[str, str]: + """ + This function parses a ``Cookie`` HTTP header into a dict of key/value pairs. + + It attempts to mimic browser cookie parsing behavior: browsers and web servers + frequently disregard the spec (RFC 6265) when setting and reading cookies, + so we attempt to suit the common scenarios here. + + This function has been adapted from Django 3.1.0. + Note: we are explicitly _NOT_ using `SimpleCookie.load` because it is based + on an outdated spec and will fail on lots of input we want to support + """ + cookie_dict: dict[str, str] = {} + for chunk in cookie_string.split(";"): + if "=" in chunk: + key, val = chunk.split("=", 1) + else: + # Assume an empty name per + # https://bugzilla.mozilla.org/show_bug.cgi?id=169091 + key, val = "", chunk + key, val = key.strip(), val.strip() + if key or val: + # unquote using Python's algorithm. + cookie_dict[key] = http_cookies._unquote(val) + return cookie_dict + + +class ClientDisconnect(Exception): + pass + + +StateT = TypeVar("StateT", bound=Mapping[str, Any] | State, default=State) + + +class HTTPConnection(Mapping[str, Any], Generic[StateT]): + """ + A base class for incoming HTTP connections, that is used to provide + any functionality that is common to both `Request` and `WebSocket`. + """ + + def __init__(self, scope: Scope, receive: Receive | None = None) -> None: + assert scope["type"] in ("http", "websocket") + self.scope = scope + + def __getitem__(self, key: str) -> Any: + return self.scope[key] + + def __iter__(self) -> Iterator[str]: + return iter(self.scope) + + def __len__(self) -> int: + return len(self.scope) + + # Don't use the `abc.Mapping.__eq__` implementation. + # Connection instances should never be considered equal + # unless `self is other`. + __eq__ = object.__eq__ + __hash__ = object.__hash__ + + @property + def app(self) -> Any: + return self.scope["app"] + + @property + def url(self) -> URL: + if not hasattr(self, "_url"): # pragma: no branch + self._url = URL(scope=self.scope) + return self._url + + @property + def base_url(self) -> URL: + if not hasattr(self, "_base_url"): + base_url_scope = dict(self.scope) + # This is used by request.url_for, it might be used inside a Mount which + # would have its own child scope with its own root_path, but the base URL + # for url_for should still be the top level app root path. + app_root_path = base_url_scope.get("app_root_path", base_url_scope.get("root_path", "")) + path = app_root_path + if not path.endswith("/"): + path += "/" + base_url_scope["path"] = path + base_url_scope["query_string"] = b"" + base_url_scope["root_path"] = app_root_path + self._base_url = URL(scope=base_url_scope) + return self._base_url + + @property + def headers(self) -> Headers: + if not hasattr(self, "_headers"): + self._headers = Headers(scope=self.scope) + return self._headers + + @property + def query_params(self) -> QueryParams: + if not hasattr(self, "_query_params"): # pragma: no branch + self._query_params = QueryParams(self.scope["query_string"]) + return self._query_params + + @property + def path_params(self) -> dict[str, Any]: + path_params: dict[str, Any] = self.scope.get("path_params", {}) + return path_params + + @property + def cookies(self) -> dict[str, str]: + if not hasattr(self, "_cookies"): + cookies: dict[str, str] = {} + cookie_headers = self.headers.getlist("cookie") + + for header in cookie_headers: + cookies.update(cookie_parser(header)) + + self._cookies = cookies + return self._cookies + + @property + def client(self) -> Address | None: + # client is a 2 item tuple of (host, port), None if missing + host_port = self.scope.get("client") + if host_port is not None: + return Address(*host_port) + return None + + @property + def session(self) -> dict[str, Any]: + assert "session" in self.scope, "SessionMiddleware must be installed to access request.session" + session: Session = self.scope["session"] + # We keep the hasattr in case people actually use their own `SessionMiddleware` implementation. + if hasattr(session, "mark_accessed"): # pragma: no branch + session.mark_accessed() + return session + + @property + def auth(self) -> Any: + assert "auth" in self.scope, "AuthenticationMiddleware must be installed to access request.auth" + return self.scope["auth"] + + @property + def user(self) -> Any: + assert "user" in self.scope, "AuthenticationMiddleware must be installed to access request.user" + return self.scope["user"] + + @property + def state(self) -> StateT: + if not hasattr(self, "_state"): + # Ensure 'state' has an empty dict if it's not already populated. + self.scope.setdefault("state", {}) + # Create a state instance with a reference to the dict in which it should + # store info + self._state = State(self.scope["state"]) + return cast(StateT, self._state) + + def url_for(self, name: str, /, **path_params: Any) -> URL: + url_path_provider: Router | Starlette | None = self.scope.get("router") or self.scope.get("app") + if url_path_provider is None: + raise RuntimeError("The `url_for` method can only be used inside a Starlette application or with a router.") + url_path = url_path_provider.url_path_for(name, **path_params) + return url_path.make_absolute_url(base_url=self.base_url) + + +async def empty_receive() -> NoReturn: + raise RuntimeError("Receive channel has not been made available") + + +async def empty_send(message: Message) -> NoReturn: + raise RuntimeError("Send channel has not been made available") + + +class Request(HTTPConnection[StateT]): + _form: FormData | None + + def __init__(self, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send): + super().__init__(scope) + assert scope["type"] == "http" + self._receive = receive + self._send = send + self._stream_consumed = False + self._is_disconnected = False + self._form = None + + @property + def method(self) -> str: + return cast(str, self.scope["method"]) + + @property + def receive(self) -> Receive: + return self._receive + + async def stream(self) -> AsyncGenerator[bytes, None]: + if hasattr(self, "_body"): + yield self._body + yield b"" + return + if self._stream_consumed: + raise RuntimeError("Stream consumed") + while not self._stream_consumed: + message = await self._receive() + if message["type"] == "http.request": + body = message.get("body", b"") + if not message.get("more_body", False): + self._stream_consumed = True + if body: + yield body + elif message["type"] == "http.disconnect": # pragma: no branch + self._is_disconnected = True + raise ClientDisconnect() + yield b"" + + async def body(self) -> bytes: + if not hasattr(self, "_body"): + chunks: list[bytes] = [] + async for chunk in self.stream(): + chunks.append(chunk) + self._body = b"".join(chunks) + return self._body + + async def json(self) -> Any: + if not hasattr(self, "_json"): # pragma: no branch + body = await self.body() + self._json = json.loads(body) + return self._json + + async def _get_form( + self, + *, + max_files: int | float = 1000, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, + ) -> FormData: + if self._form is None: # pragma: no branch + assert parse_options_header is not None, ( + "The `python-multipart` library must be installed to use form parsing." + ) + content_type_header = self.headers.get("Content-Type") + content_type: bytes + content_type, _ = parse_options_header(content_type_header) + if content_type == b"multipart/form-data": + try: + multipart_parser = MultiPartParser( + self.headers, + self.stream(), + max_files=max_files, + max_fields=max_fields, + max_part_size=max_part_size, + ) + self._form = await multipart_parser.parse() + except MultiPartException as exc: + if "app" in self.scope: + raise HTTPException(status_code=400, detail=exc.message) + raise exc + elif content_type == b"application/x-www-form-urlencoded": + try: + form_parser = FormParser( + self.headers, + self.stream(), + max_fields=max_fields, + max_part_size=max_part_size, + ) + self._form = await form_parser.parse() + except MultiPartException as exc: + if "app" in self.scope: + raise HTTPException(status_code=400, detail=exc.message) + raise exc + else: + self._form = FormData() + return self._form + + def form( + self, + *, + max_files: int | float = 1000, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, + ) -> AwaitableOrContextManager[FormData]: + return AwaitableOrContextManagerWrapper( + self._get_form(max_files=max_files, max_fields=max_fields, max_part_size=max_part_size) + ) + + async def close(self) -> None: + if self._form is not None: # pragma: no branch + await self._form.close() + + async def is_disconnected(self) -> bool: + if not self._is_disconnected: + message: Message = {} + + # If message isn't immediately available, move on + with anyio.CancelScope() as cs: + cs.cancel() + message = await self._receive() + + if message.get("type") == "http.disconnect": + self._is_disconnected = True + + return self._is_disconnected + + async def send_push_promise(self, path: str) -> None: + if "http.response.push" in self.scope.get("extensions", {}): + raw_headers: list[tuple[bytes, bytes]] = [] + for name in SERVER_PUSH_HEADERS_TO_COPY: + for value in self.headers.getlist(name): + raw_headers.append((name.encode("latin-1"), value.encode("latin-1"))) + await self._send({"type": "http.response.push", "path": path, "headers": raw_headers}) diff --git a/venv/lib/python3.11/site-packages/starlette/responses.py b/venv/lib/python3.11/site-packages/starlette/responses.py new file mode 100644 index 0000000000000000000000000000000000000000..63b49ecf4d8c55dc3588faf0b6c503de14afd933 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/responses.py @@ -0,0 +1,567 @@ +from __future__ import annotations + +import hashlib +import http.cookies +import json +import os +import stat +import sys +from collections.abc import AsyncIterable, Awaitable, Callable, Iterable, Mapping, Sequence +from datetime import datetime +from email.utils import format_datetime, formatdate +from functools import partial +from mimetypes import guess_type +from secrets import token_hex +from typing import Any, Literal +from urllib.parse import quote + +import anyio +import anyio.to_thread + +from starlette._utils import create_collapsing_task_group +from starlette.background import BackgroundTask +from starlette.concurrency import iterate_in_threadpool +from starlette.datastructures import URL, Headers, MutableHeaders +from starlette.requests import ClientDisconnect +from starlette.types import Message, Receive, Scope, Send + + +class Response: + media_type = None + charset = "utf-8" + + def __init__( + self, + content: Any = None, + status_code: int = 200, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + background: BackgroundTask | None = None, + ) -> None: + self.status_code = status_code + if media_type is not None: + self.media_type = media_type + self.background = background + self.body = self.render(content) + self.init_headers(headers) + + def render(self, content: Any) -> bytes | memoryview: + if content is None: + return b"" + if isinstance(content, bytes | memoryview): + return content + return content.encode(self.charset) # type: ignore + + def init_headers(self, headers: Mapping[str, str] | None = None) -> None: + if headers is None: + raw_headers: list[tuple[bytes, bytes]] = [] + populate_content_length = True + populate_content_type = True + else: + raw_headers = [(k.lower().encode("latin-1"), v.encode("latin-1")) for k, v in headers.items()] + keys = [h[0] for h in raw_headers] + populate_content_length = b"content-length" not in keys + populate_content_type = b"content-type" not in keys + + body = getattr(self, "body", None) + if ( + body is not None + and populate_content_length + and not (self.status_code < 200 or self.status_code in (204, 304)) + ): + content_length = str(len(body)) + raw_headers.append((b"content-length", content_length.encode("latin-1"))) + + content_type = self.media_type + if content_type is not None and populate_content_type: + if content_type.startswith("text/") and "charset=" not in content_type.lower(): + content_type += "; charset=" + self.charset + raw_headers.append((b"content-type", content_type.encode("latin-1"))) + + self.raw_headers = raw_headers + + @property + def headers(self) -> MutableHeaders: + if not hasattr(self, "_headers"): + self._headers = MutableHeaders(raw=self.raw_headers) + return self._headers + + def set_cookie( + self, + key: str, + value: str = "", + max_age: int | None = None, + expires: datetime | str | int | None = None, + path: str | None = "/", + domain: str | None = None, + secure: bool = False, + httponly: bool = False, + samesite: Literal["lax", "strict", "none"] | None = "lax", + partitioned: bool = False, + ) -> None: + cookie: http.cookies.BaseCookie[str] = http.cookies.SimpleCookie() + cookie[key] = value + if max_age is not None: + cookie[key]["max-age"] = max_age + if expires is not None: + if isinstance(expires, datetime): + cookie[key]["expires"] = format_datetime(expires, usegmt=True) + else: + cookie[key]["expires"] = expires + if path is not None: + cookie[key]["path"] = path + if domain is not None: + cookie[key]["domain"] = domain + if secure: + cookie[key]["secure"] = True + if httponly: + cookie[key]["httponly"] = True + if samesite is not None: + assert samesite.lower() in [ + "strict", + "lax", + "none", + ], "samesite must be either 'strict', 'lax' or 'none'" + cookie[key]["samesite"] = samesite + if partitioned: + if sys.version_info < (3, 14): + raise ValueError("Partitioned cookies are only supported in Python 3.14 and above.") # pragma: no cover + cookie[key]["partitioned"] = True # pragma: no cover + + cookie_val = cookie.output(header="").strip() + self.raw_headers.append((b"set-cookie", cookie_val.encode("latin-1"))) + + def delete_cookie( + self, + key: str, + path: str = "/", + domain: str | None = None, + secure: bool = False, + httponly: bool = False, + samesite: Literal["lax", "strict", "none"] | None = "lax", + ) -> None: + self.set_cookie( + key, + max_age=0, + expires=0, + path=path, + domain=domain, + secure=secure, + httponly=httponly, + samesite=samesite, + ) + + def _wrap_websocket_denial_send(self, send: Send) -> Send: + async def wrapped(message: Message) -> None: + message_type = message["type"] + if message_type in {"http.response.start", "http.response.body"}: # pragma: no branch + message = {**message, "type": "websocket." + message_type} + await send(message) + + return wrapped + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] == "websocket": + send = self._wrap_websocket_denial_send(send) + await send({"type": "http.response.start", "status": self.status_code, "headers": self.raw_headers}) + await send({"type": "http.response.body", "body": self.body}) + + if self.background is not None: + await self.background() + + +class HTMLResponse(Response): + media_type = "text/html" + + +class PlainTextResponse(Response): + media_type = "text/plain" + + +class JSONResponse(Response): + media_type = "application/json" + + def __init__( + self, + content: Any, + status_code: int = 200, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + background: BackgroundTask | None = None, + ) -> None: + super().__init__(content, status_code, headers, media_type, background) + + def render(self, content: Any) -> bytes: + return json.dumps( + content, + ensure_ascii=False, + allow_nan=False, + indent=None, + separators=(",", ":"), + ).encode("utf-8") + + +class RedirectResponse(Response): + def __init__( + self, + url: str | URL, + status_code: int = 307, + headers: Mapping[str, str] | None = None, + background: BackgroundTask | None = None, + ) -> None: + super().__init__(content=b"", status_code=status_code, headers=headers, background=background) + self.headers["location"] = quote(str(url), safe=":/%#?=@[]!$&'()*+,;") + + +Content = str | bytes | memoryview +SyncContentStream = Iterable[Content] +AsyncContentStream = AsyncIterable[Content] +ContentStream = AsyncContentStream | SyncContentStream + + +class StreamingResponse(Response): + body_iterator: AsyncContentStream + + def __init__( + self, + content: ContentStream, + status_code: int = 200, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + background: BackgroundTask | None = None, + ) -> None: + if isinstance(content, AsyncIterable): + self.body_iterator = content + else: + self.body_iterator = iterate_in_threadpool(content) + self.status_code = status_code + self.media_type = self.media_type if media_type is None else media_type + self.background = background + self.init_headers(headers) + + async def listen_for_disconnect(self, receive: Receive) -> None: + while True: + message = await receive() + if message["type"] == "http.disconnect": + break + + async def stream_response(self, send: Send) -> None: + await send({"type": "http.response.start", "status": self.status_code, "headers": self.raw_headers}) + async for chunk in self.body_iterator: + if not isinstance(chunk, bytes | memoryview): + chunk = chunk.encode(self.charset) + await send({"type": "http.response.body", "body": chunk, "more_body": True}) + + await send({"type": "http.response.body", "body": b"", "more_body": False}) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] == "websocket": + send = self._wrap_websocket_denial_send(send) + await self.stream_response(send) + if self.background is not None: + await self.background() + return + + spec_version = tuple(map(int, scope.get("asgi", {}).get("spec_version", "2.0").split("."))) + + if spec_version >= (2, 4): + try: + await self.stream_response(send) + except OSError: + raise ClientDisconnect() + else: + async with create_collapsing_task_group() as task_group: + + async def wrap(func: Callable[[], Awaitable[None]]) -> None: + await func() + task_group.cancel_scope.cancel() + + task_group.start_soon(wrap, partial(self.stream_response, send)) + await wrap(partial(self.listen_for_disconnect, receive)) + + if self.background is not None: + await self.background() + + +class MalformedRangeHeader(Exception): + def __init__(self, content: str = "Malformed range header.") -> None: + self.content = content + + +class RangeNotSatisfiable(Exception): + def __init__(self, max_size: int) -> None: + self.max_size = max_size + + +class FileResponse(Response): + chunk_size = 64 * 1024 + + def __init__( + self, + path: str | os.PathLike[str], + status_code: int = 200, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + background: BackgroundTask | None = None, + filename: str | None = None, + stat_result: os.stat_result | None = None, + content_disposition_type: str = "attachment", + ) -> None: + self.path = path + self.status_code = status_code + self.filename = filename + if media_type is None: + media_type = guess_type(filename or path)[0] or "application/octet-stream" + self.media_type = media_type + self.background = background + self.init_headers(headers) + self.headers.setdefault("accept-ranges", "bytes") + if self.filename is not None: + content_disposition_filename = quote(self.filename) + if content_disposition_filename != self.filename: + content_disposition = f"{content_disposition_type}; filename*=utf-8''{content_disposition_filename}" + else: + content_disposition = f'{content_disposition_type}; filename="{self.filename}"' + self.headers.setdefault("content-disposition", content_disposition) + self.stat_result = stat_result + if stat_result is not None: + self.set_stat_headers(stat_result) + + def set_stat_headers(self, stat_result: os.stat_result) -> None: + content_length = str(stat_result.st_size) + last_modified = formatdate(stat_result.st_mtime, usegmt=True) + etag_base = str(stat_result.st_mtime) + "-" + str(stat_result.st_size) + etag = f'"{hashlib.md5(etag_base.encode(), usedforsecurity=False).hexdigest()}"' + + self.headers.setdefault("content-length", content_length) + self.headers.setdefault("last-modified", last_modified) + self.headers.setdefault("etag", etag) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + scope_type = scope["type"] + send_header_only = scope_type == "http" and scope["method"].upper() == "HEAD" + send_pathsend = scope_type == "http" and "http.response.pathsend" in scope.get("extensions", {}) + if scope_type == "websocket": + send = self._wrap_websocket_denial_send(send) + + if self.stat_result is None: + try: + stat_result = await anyio.to_thread.run_sync(os.stat, self.path) + self.set_stat_headers(stat_result) + except FileNotFoundError: + raise RuntimeError(f"File at path {self.path} does not exist.") + else: + mode = stat_result.st_mode + if not stat.S_ISREG(mode): + raise RuntimeError(f"File at path {self.path} is not a file.") + else: + stat_result = self.stat_result + + headers = Headers(scope=scope) + http_range = headers.get("range") + http_if_range = headers.get("if-range") + + if http_range is None or (http_if_range is not None and not self._should_use_range(http_if_range)): + await self._handle_simple(send, send_header_only, send_pathsend) + else: + try: + ranges = self._parse_range_header(http_range, stat_result.st_size) + except MalformedRangeHeader as exc: + return await PlainTextResponse(exc.content, status_code=400)(scope, receive, send) + except RangeNotSatisfiable as exc: + response = PlainTextResponse(status_code=416, headers={"Content-Range": f"bytes */{exc.max_size}"}) + return await response(scope, receive, send) + + if len(ranges) == 1: + start, end = ranges[0] + await self._handle_single_range(send, start, end, stat_result.st_size, send_header_only) + else: + await self._handle_multiple_ranges(send, ranges, stat_result.st_size, send_header_only) + + if self.background is not None: + await self.background() + + async def _handle_simple(self, send: Send, send_header_only: bool, send_pathsend: bool) -> None: + await send({"type": "http.response.start", "status": self.status_code, "headers": self.raw_headers}) + if send_header_only: + await send({"type": "http.response.body", "body": b"", "more_body": False}) + elif send_pathsend: + await send({"type": "http.response.pathsend", "path": str(self.path)}) + else: + async with await anyio.open_file(self.path, mode="rb") as file: + more_body = True + while more_body: + chunk = await file.read(self.chunk_size) + more_body = len(chunk) == self.chunk_size + await send({"type": "http.response.body", "body": chunk, "more_body": more_body}) + + async def _handle_single_range( + self, send: Send, start: int, end: int, file_size: int, send_header_only: bool + ) -> None: + headers = MutableHeaders(raw=list(self.raw_headers)) + headers["content-range"] = f"bytes {start}-{end - 1}/{file_size}" + headers["content-length"] = str(end - start) + await send({"type": "http.response.start", "status": 206, "headers": headers.raw}) + if send_header_only: + await send({"type": "http.response.body", "body": b"", "more_body": False}) + else: + async with await anyio.open_file(self.path, mode="rb") as file: + await file.seek(start) + more_body = True + while more_body: + chunk = await file.read(min(self.chunk_size, end - start)) + start += len(chunk) + more_body = len(chunk) == self.chunk_size and start < end + await send({"type": "http.response.body", "body": chunk, "more_body": more_body}) + + async def _handle_multiple_ranges( + self, + send: Send, + ranges: list[tuple[int, int]], + file_size: int, + send_header_only: bool, + ) -> None: + # In firefox and chrome, they use boundary with 95-96 bits entropy (that's roughly 13 bytes). + boundary = token_hex(13) + content_length, header_generator = self.generate_multipart( + ranges, boundary, file_size, self.headers["content-type"] + ) + headers = MutableHeaders(raw=list(self.raw_headers)) + headers["content-type"] = f"multipart/byteranges; boundary={boundary}" + headers["content-length"] = str(content_length) + await send({"type": "http.response.start", "status": 206, "headers": headers.raw}) + if send_header_only: + await send({"type": "http.response.body", "body": b"", "more_body": False}) + else: + async with await anyio.open_file(self.path, mode="rb") as file: + for start, end in ranges: + await send({"type": "http.response.body", "body": header_generator(start, end), "more_body": True}) + await file.seek(start) + while start < end: + chunk = await file.read(min(self.chunk_size, end - start)) + start += len(chunk) + await send({"type": "http.response.body", "body": chunk, "more_body": True}) + await send({"type": "http.response.body", "body": b"\r\n", "more_body": True}) + await send( + { + "type": "http.response.body", + "body": f"--{boundary}--".encode("latin-1"), + "more_body": False, + } + ) + + def _should_use_range(self, http_if_range: str) -> bool: + return http_if_range == self.headers["last-modified"] or http_if_range == self.headers["etag"] + + @classmethod + def _parse_range_header(cls, http_range: str, file_size: int) -> list[tuple[int, int]]: + ranges: list[tuple[int, int]] = [] + try: + units, range_ = http_range.split("=", 1) + except ValueError: + raise MalformedRangeHeader() + + units = units.strip().lower() + + if units != "bytes": + raise MalformedRangeHeader("Only support bytes range") + + ranges = cls._parse_ranges(range_, file_size) + + if len(ranges) == 0: + raise MalformedRangeHeader("Range header: range must be requested") + + if any(not (0 <= start < file_size) for start, _ in ranges): + raise RangeNotSatisfiable(file_size) + + if any(start > end for start, end in ranges): + raise MalformedRangeHeader("Range header: start must be less than end") + + if len(ranges) == 1: + return ranges + + # Merge overlapping ranges + ranges.sort() + result: list[tuple[int, int]] = [ranges[0]] + for start, end in ranges[1:]: + last_start, last_end = result[-1] + if start <= last_end: + result[-1] = (last_start, max(last_end, end)) + else: + result.append((start, end)) + + return result + + @classmethod + def _parse_ranges(cls, range_: str, file_size: int) -> list[tuple[int, int]]: + ranges: list[tuple[int, int]] = [] + + for part in range_.split(","): + part = part.strip() + + # If the range is empty or a single dash, we ignore it. + if not part or part == "-": + continue + + # If the range is not in the format "start-end", we ignore it. + if "-" not in part: + continue + + start_str, end_str = part.split("-", 1) + start_str = start_str.strip() + end_str = end_str.strip() + + try: + start = int(start_str) if start_str else max(file_size - int(end_str), 0) + end = int(end_str) + 1 if start_str and end_str and int(end_str) < file_size else file_size + ranges.append((start, end)) + except ValueError: + # If the range is not numeric, we ignore it. + continue + + return ranges + + def generate_multipart( + self, + ranges: Sequence[tuple[int, int]], + boundary: str, + max_size: int, + content_type: str, + ) -> tuple[int, Callable[[int, int], bytes]]: + r""" + Multipart response headers generator. + + ``` + --{boundary}\r\n + Content-Type: {content_type}\r\n + Content-Range: bytes {start}-{end-1}/{max_size}\r\n + \r\n + ..........content...........\r\n + --{boundary}\r\n + Content-Type: {content_type}\r\n + Content-Range: bytes {start}-{end-1}/{max_size}\r\n + \r\n + ..........content...........\r\n + --{boundary}-- + ``` + """ + boundary_len = len(boundary) + static_header_part_len = 49 + boundary_len + len(content_type) + len(str(max_size)) + content_length = sum( + (len(str(start)) + len(str(end - 1)) + static_header_part_len) # Headers + + (end - start) # Content + for start, end in ranges + ) + ( + 4 + boundary_len # --boundary-- + ) + return ( + content_length, + lambda start, end: ( + f"""\ +--{boundary}\r +Content-Type: {content_type}\r +Content-Range: bytes {start}-{end - 1}/{max_size}\r +\r +""" + ).encode("latin-1"), + ) diff --git a/venv/lib/python3.11/site-packages/starlette/routing.py b/venv/lib/python3.11/site-packages/starlette/routing.py new file mode 100644 index 0000000000000000000000000000000000000000..a035d18cd8b64483efc0254dab506f83d5c84ab7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/routing.py @@ -0,0 +1,747 @@ +from __future__ import annotations + +import contextlib +import functools +import inspect +import re +import traceback +import types +import warnings +from collections.abc import Awaitable, Callable, Collection, Generator, Sequence +from contextlib import AbstractAsyncContextManager, AbstractContextManager, asynccontextmanager +from enum import Enum +from re import Pattern +from typing import Any, TypeVar + +from starlette._exception_handler import wrap_app_handling_exceptions +from starlette._utils import get_route_path, is_async_callable +from starlette.concurrency import run_in_threadpool +from starlette.convertors import CONVERTOR_TYPES, Convertor +from starlette.datastructures import URL, Headers, URLPath +from starlette.exceptions import HTTPException, StarletteDeprecationWarning +from starlette.middleware import Middleware +from starlette.requests import Request +from starlette.responses import PlainTextResponse, RedirectResponse, Response +from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send +from starlette.websockets import WebSocket, WebSocketClose + + +class NoMatchFound(Exception): + """ + Raised by `.url_for(name, **path_params)` and `.url_path_for(name, **path_params)` + if no matching route exists. + """ + + def __init__(self, name: str, path_params: dict[str, Any]) -> None: + params = ", ".join(list(path_params.keys())) + super().__init__(f'No route exists for name "{name}" and params "{params}".') + + +class Match(Enum): + NONE = 0 + PARTIAL = 1 + FULL = 2 + + +def request_response( + func: Callable[[Request], Awaitable[Response] | Response], +) -> ASGIApp: + """ + Takes a function or coroutine `func(request) -> response`, + and returns an ASGI application. + """ + f: Callable[[Request], Awaitable[Response]] = ( + func if is_async_callable(func) else functools.partial(run_in_threadpool, func) # type: ignore[assignment, call-arg] + ) + + async def app(scope: Scope, receive: Receive, send: Send) -> None: + request = Request(scope, receive, send) + + async def app(scope: Scope, receive: Receive, send: Send) -> None: + response = await f(request) + await response(scope, receive, send) + + await wrap_app_handling_exceptions(app, request)(scope, receive, send) + + return app + + +def websocket_session( + func: Callable[[WebSocket], Awaitable[None]], +) -> ASGIApp: + """ + Takes a coroutine `func(session)`, and returns an ASGI application. + """ + # assert asyncio.iscoroutinefunction(func), "WebSocket endpoints must be async" + + async def app(scope: Scope, receive: Receive, send: Send) -> None: + session = WebSocket(scope, receive=receive, send=send) + + async def app(scope: Scope, receive: Receive, send: Send) -> None: + await func(session) + + await wrap_app_handling_exceptions(app, session)(scope, receive, send) + + return app + + +def get_name(endpoint: Callable[..., Any]) -> str: + return getattr(endpoint, "__name__", endpoint.__class__.__name__) + + +def replace_params( + path: str, + param_convertors: dict[str, Convertor[Any]], + path_params: dict[str, str], +) -> tuple[str, dict[str, str]]: + for key, value in list(path_params.items()): + if "{" + key + "}" in path: + convertor = param_convertors[key] + value = convertor.to_string(value) + path = path.replace("{" + key + "}", value) + path_params.pop(key) + return path, path_params + + +# Match parameters in URL paths, eg. '{param}', and '{param:int}' +PARAM_REGEX = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)(:[a-zA-Z_][a-zA-Z0-9_]*)?}") + + +def compile_path( + path: str, +) -> tuple[Pattern[str], str, dict[str, Convertor[Any]]]: + """ + Given a path string, like: "/{username:str}", + or a host string, like: "{subdomain}.mydomain.org", return a three-tuple + of (regex, format, {param_name:convertor}). + + regex: "/(?P[^/]+)" + format: "/{username}" + convertors: {"username": StringConvertor()} + """ + is_host = not path.startswith("/") + + path_regex = "^" + path_format = "" + duplicated_params: set[str] = set() + + idx = 0 + param_convertors = {} + for match in PARAM_REGEX.finditer(path): + param_name, convertor_type = match.groups("str") + convertor_type = convertor_type.lstrip(":") + assert convertor_type in CONVERTOR_TYPES, f"Unknown path convertor '{convertor_type}'" + convertor = CONVERTOR_TYPES[convertor_type] + + path_regex += re.escape(path[idx : match.start()]) + path_regex += f"(?P<{param_name}>{convertor.regex})" + + path_format += path[idx : match.start()] + path_format += "{%s}" % param_name + + if param_name in param_convertors: + duplicated_params.add(param_name) + + param_convertors[param_name] = convertor + + idx = match.end() + + if duplicated_params: + names = ", ".join(sorted(duplicated_params)) + ending = "s" if len(duplicated_params) > 1 else "" + raise ValueError(f"Duplicated param name{ending} {names} at path {path}") + + if is_host: + # Align with `Host.matches()` behavior, which ignores port. + hostname = path[idx:].split(":")[0] + path_regex += re.escape(hostname) + "$" + else: + path_regex += re.escape(path[idx:]) + "$" + + path_format += path[idx:] + + return re.compile(path_regex), path_format, param_convertors + + +class BaseRoute: + def matches(self, scope: Scope) -> tuple[Match, Scope]: + raise NotImplementedError() # pragma: no cover + + def url_path_for(self, name: str, /, **path_params: Any) -> URLPath: + raise NotImplementedError() # pragma: no cover + + async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: + raise NotImplementedError() # pragma: no cover + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """ + A route may be used in isolation as a stand-alone ASGI app. + This is a somewhat contrived case, as they'll almost always be used + within a Router, but could be useful for some tooling and minimal apps. + """ + match, child_scope = self.matches(scope) + if match == Match.NONE: + if scope["type"] == "http": + response = PlainTextResponse("Not Found", status_code=404) + await response(scope, receive, send) + elif scope["type"] == "websocket": # pragma: no branch + websocket_close = WebSocketClose() + await websocket_close(scope, receive, send) + return + + scope.update(child_scope) + await self.handle(scope, receive, send) + + +class Route(BaseRoute): + def __init__( + self, + path: str, + endpoint: Callable[..., Any], + *, + methods: Collection[str] | None = None, + name: str | None = None, + include_in_schema: bool = True, + middleware: Sequence[Middleware] | None = None, + ) -> None: + assert path.startswith("/"), "Routed paths must start with '/'" + self.path = path + self.endpoint = endpoint + self.name = get_name(endpoint) if name is None else name + self.include_in_schema = include_in_schema + + endpoint_handler = endpoint + while isinstance(endpoint_handler, functools.partial): + endpoint_handler = endpoint_handler.func + if inspect.isfunction(endpoint_handler) or inspect.ismethod(endpoint_handler): + # Endpoint is function or method. Treat it as `func(request) -> response`. + self.app = request_response(endpoint) + if methods is None: + methods = ["GET"] + else: + # Endpoint is a class. Treat it as ASGI. + self.app = endpoint + + if middleware is not None: + for cls, args, kwargs in reversed(middleware): + self.app = cls(self.app, *args, **kwargs) + + if methods is None: + self.methods = None + else: + self.methods = {method.upper() for method in methods} + if "GET" in self.methods: + self.methods.add("HEAD") + + self.path_regex, self.path_format, self.param_convertors = compile_path(path) + + def matches(self, scope: Scope) -> tuple[Match, Scope]: + path_params: dict[str, Any] + if scope["type"] == "http": + route_path = get_route_path(scope) + match = self.path_regex.match(route_path) + if match: + matched_params = match.groupdict() + for key, value in matched_params.items(): + matched_params[key] = self.param_convertors[key].convert(value) + path_params = dict(scope.get("path_params", {})) + path_params.update(matched_params) + child_scope = {"endpoint": self.endpoint, "path_params": path_params} + if self.methods and scope["method"] not in self.methods: + return Match.PARTIAL, child_scope + else: + return Match.FULL, child_scope + return Match.NONE, {} + + def url_path_for(self, name: str, /, **path_params: Any) -> URLPath: + seen_params = set(path_params.keys()) + expected_params = set(self.param_convertors.keys()) + + if name != self.name or seen_params != expected_params: + raise NoMatchFound(name, path_params) + + path, remaining_params = replace_params(self.path_format, self.param_convertors, path_params) + assert not remaining_params + return URLPath(path=path, protocol="http") + + async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: + if self.methods and scope["method"] not in self.methods: + headers = {"Allow": ", ".join(self.methods)} + if "app" in scope: + raise HTTPException(status_code=405, headers=headers) + else: + response = PlainTextResponse("Method Not Allowed", status_code=405, headers=headers) + await response(scope, receive, send) + else: + await self.app(scope, receive, send) + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, Route) + and self.path == other.path + and self.endpoint == other.endpoint + and self.methods == other.methods + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + methods = sorted(self.methods or []) + path, name = self.path, self.name + return f"{class_name}(path={path!r}, name={name!r}, methods={methods!r})" + + +class WebSocketRoute(BaseRoute): + def __init__( + self, + path: str, + endpoint: Callable[..., Any], + *, + name: str | None = None, + middleware: Sequence[Middleware] | None = None, + ) -> None: + assert path.startswith("/"), "Routed paths must start with '/'" + self.path = path + self.endpoint = endpoint + self.name = get_name(endpoint) if name is None else name + + endpoint_handler = endpoint + while isinstance(endpoint_handler, functools.partial): + endpoint_handler = endpoint_handler.func + if inspect.isfunction(endpoint_handler) or inspect.ismethod(endpoint_handler): + # Endpoint is function or method. Treat it as `func(websocket)`. + self.app = websocket_session(endpoint) + else: + # Endpoint is a class. Treat it as ASGI. + self.app = endpoint + + if middleware is not None: + for cls, args, kwargs in reversed(middleware): + self.app = cls(self.app, *args, **kwargs) + + self.path_regex, self.path_format, self.param_convertors = compile_path(path) + + def matches(self, scope: Scope) -> tuple[Match, Scope]: + path_params: dict[str, Any] + if scope["type"] == "websocket": + route_path = get_route_path(scope) + match = self.path_regex.match(route_path) + if match: + matched_params = match.groupdict() + for key, value in matched_params.items(): + matched_params[key] = self.param_convertors[key].convert(value) + path_params = dict(scope.get("path_params", {})) + path_params.update(matched_params) + child_scope = {"endpoint": self.endpoint, "path_params": path_params} + return Match.FULL, child_scope + return Match.NONE, {} + + def url_path_for(self, name: str, /, **path_params: Any) -> URLPath: + seen_params = set(path_params.keys()) + expected_params = set(self.param_convertors.keys()) + + if name != self.name or seen_params != expected_params: + raise NoMatchFound(name, path_params) + + path, remaining_params = replace_params(self.path_format, self.param_convertors, path_params) + assert not remaining_params + return URLPath(path=path, protocol="websocket") + + async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: + await self.app(scope, receive, send) + + def __eq__(self, other: Any) -> bool: + return isinstance(other, WebSocketRoute) and self.path == other.path and self.endpoint == other.endpoint + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(path={self.path!r}, name={self.name!r})" + + +class Mount(BaseRoute): + def __init__( + self, + path: str, + app: ASGIApp | None = None, + routes: Sequence[BaseRoute] | None = None, + name: str | None = None, + *, + middleware: Sequence[Middleware] | None = None, + ) -> None: + assert path == "" or path.startswith("/"), "Routed paths must start with '/'" + assert app is not None or routes is not None, "Either 'app=...', or 'routes=' must be specified" + self.path = path.rstrip("/") + if app is not None: + self._base_app: ASGIApp = app + else: + self._base_app = Router(routes=routes) + self.app = self._base_app + if middleware is not None: + for cls, args, kwargs in reversed(middleware): + self.app = cls(self.app, *args, **kwargs) + self.name = name + self.path_regex, self.path_format, self.param_convertors = compile_path(self.path + "/{path:path}") + + @property + def routes(self) -> list[BaseRoute]: + return getattr(self._base_app, "routes", []) + + def matches(self, scope: Scope) -> tuple[Match, Scope]: + path_params: dict[str, Any] + if scope["type"] in ("http", "websocket"): # pragma: no branch + root_path = scope.get("root_path", "") + route_path = get_route_path(scope) + match = self.path_regex.match(route_path) + if match: + matched_params = match.groupdict() + for key, value in matched_params.items(): + matched_params[key] = self.param_convertors[key].convert(value) + remaining_path = "/" + matched_params.pop("path") + matched_path = route_path[: -len(remaining_path)] + path_params = dict(scope.get("path_params", {})) + path_params.update(matched_params) + child_scope = { + "path_params": path_params, + # app_root_path will only be set at the top level scope, + # initialized with the (optional) value of a root_path + # set above/before Starlette. And even though any + # mount will have its own child scope with its own respective + # root_path, the app_root_path will always be available in all + # the child scopes with the same top level value because it's + # set only once here with a default, any other child scope will + # just inherit that app_root_path default value stored in the + # scope. All this is needed to support Request.url_for(), as it + # uses the app_root_path to build the URL path. + "app_root_path": scope.get("app_root_path", root_path), + "root_path": root_path + matched_path, + "endpoint": self.app, + } + return Match.FULL, child_scope + return Match.NONE, {} + + def url_path_for(self, name: str, /, **path_params: Any) -> URLPath: + if self.name is not None and name == self.name and "path" in path_params: + # 'name' matches "". + path_params["path"] = path_params["path"].lstrip("/") + path, remaining_params = replace_params(self.path_format, self.param_convertors, path_params) + if not remaining_params: + return URLPath(path=path) + elif self.name is None or name.startswith(self.name + ":"): + if self.name is None: + # No mount name. + remaining_name = name + else: + # 'name' matches ":". + remaining_name = name[len(self.name) + 1 :] + path_kwarg = path_params.get("path") + path_params["path"] = "" + path_prefix, remaining_params = replace_params(self.path_format, self.param_convertors, path_params) + if path_kwarg is not None: + remaining_params["path"] = path_kwarg + for route in self.routes or []: + try: + url = route.url_path_for(remaining_name, **remaining_params) + return URLPath(path=path_prefix.rstrip("/") + str(url), protocol=url.protocol) + except NoMatchFound: + pass + raise NoMatchFound(name, path_params) + + async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: + await self.app(scope, receive, send) + + def __eq__(self, other: Any) -> bool: + return isinstance(other, Mount) and self.path == other.path and self.app == other.app + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + name = self.name or "" + return f"{class_name}(path={self.path!r}, name={name!r}, app={self.app!r})" + + +class Host(BaseRoute): + def __init__(self, host: str, app: ASGIApp, name: str | None = None) -> None: + assert not host.startswith("/"), "Host must not start with '/'" + self.host = host + self.app = app + self.name = name + self.host_regex, self.host_format, self.param_convertors = compile_path(host) + + @property + def routes(self) -> list[BaseRoute]: + return getattr(self.app, "routes", []) + + def matches(self, scope: Scope) -> tuple[Match, Scope]: + if scope["type"] in ("http", "websocket"): # pragma:no branch + headers = Headers(scope=scope) + host = headers.get("host", "").split(":")[0] + match = self.host_regex.match(host) + if match: + matched_params = match.groupdict() + for key, value in matched_params.items(): + matched_params[key] = self.param_convertors[key].convert(value) + path_params = dict(scope.get("path_params", {})) + path_params.update(matched_params) + child_scope = {"path_params": path_params, "endpoint": self.app} + return Match.FULL, child_scope + return Match.NONE, {} + + def url_path_for(self, name: str, /, **path_params: Any) -> URLPath: + if self.name is not None and name == self.name and "path" in path_params: + # 'name' matches "". + path = path_params.pop("path") + host, remaining_params = replace_params(self.host_format, self.param_convertors, path_params) + if not remaining_params: + return URLPath(path=path, host=host) + elif self.name is None or name.startswith(self.name + ":"): + if self.name is None: + # No mount name. + remaining_name = name + else: + # 'name' matches ":". + remaining_name = name[len(self.name) + 1 :] + host, remaining_params = replace_params(self.host_format, self.param_convertors, path_params) + for route in self.routes or []: + try: + url = route.url_path_for(remaining_name, **remaining_params) + return URLPath(path=str(url), protocol=url.protocol, host=host) + except NoMatchFound: + pass + raise NoMatchFound(name, path_params) + + async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: + await self.app(scope, receive, send) + + def __eq__(self, other: Any) -> bool: + return isinstance(other, Host) and self.host == other.host and self.app == other.app + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + name = self.name or "" + return f"{class_name}(host={self.host!r}, name={name!r}, app={self.app!r})" + + +_T = TypeVar("_T") + + +class _AsyncLiftContextManager(AbstractAsyncContextManager[_T]): + def __init__(self, cm: AbstractContextManager[_T]): + self._cm = cm + + async def __aenter__(self) -> _T: + return self._cm.__enter__() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: types.TracebackType | None, + ) -> bool | None: + return self._cm.__exit__(exc_type, exc_value, traceback) + + +def _wrap_gen_lifespan_context( + lifespan_context: Callable[[Any], Generator[Any, Any, Any]], +) -> Callable[[Any], AbstractAsyncContextManager[Any]]: + cmgr = contextlib.contextmanager(lifespan_context) + + @functools.wraps(cmgr) + def wrapper(app: Any) -> _AsyncLiftContextManager[Any]: + return _AsyncLiftContextManager(cmgr(app)) + + return wrapper + + +class _DefaultLifespan: + def __init__(self, router: Router): + self._router = router + + async def __aenter__(self) -> None: + pass + + async def __aexit__(self, *exc_info: object) -> None: + pass + + def __call__(self: _T, app: object) -> _T: + return self + + +class Router: + def __init__( + self, + routes: Sequence[BaseRoute] | None = None, + redirect_slashes: bool = True, + default: ASGIApp | None = None, + # the generic to Lifespan[AppType] is the type of the top level application + # which the router cannot know statically, so we use Any + lifespan: Lifespan[Any] | None = None, + *, + middleware: Sequence[Middleware] | None = None, + ) -> None: + self.routes = [] if routes is None else list(routes) + self.redirect_slashes = redirect_slashes + self.default = self.not_found if default is None else default + + if lifespan is None: + self.lifespan_context: Lifespan[Any] = _DefaultLifespan(self) + + elif inspect.isasyncgenfunction(lifespan): + warnings.warn( + "async generator function lifespans are deprecated, " + "use an @contextlib.asynccontextmanager function instead", + StarletteDeprecationWarning, + ) + self.lifespan_context = asynccontextmanager(lifespan) + elif inspect.isgeneratorfunction(lifespan): + warnings.warn( + "generator function lifespans are deprecated, use an @contextlib.asynccontextmanager function instead", + StarletteDeprecationWarning, + ) + self.lifespan_context = _wrap_gen_lifespan_context(lifespan) + else: + self.lifespan_context = lifespan + + self.middleware_stack = self.app + if middleware: + for cls, args, kwargs in reversed(middleware): + self.middleware_stack = cls(self.middleware_stack, *args, **kwargs) + + async def not_found(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] == "websocket": + websocket_close = WebSocketClose() + await websocket_close(scope, receive, send) + return + + # If we're running inside a starlette application then raise an + # exception, so that the configurable exception handler can deal with + # returning the response. For plain ASGI apps, just return the response. + if "app" in scope: + raise HTTPException(status_code=404) + else: + response = PlainTextResponse("Not Found", status_code=404) + await response(scope, receive, send) + + def url_path_for(self, name: str, /, **path_params: Any) -> URLPath: + for route in self.routes: + try: + return route.url_path_for(name, **path_params) + except NoMatchFound: + pass + raise NoMatchFound(name, path_params) + + async def lifespan(self, scope: Scope, receive: Receive, send: Send) -> None: + """ + Handle ASGI lifespan messages, which allows us to manage application + startup and shutdown events. + """ + started = False + app: Any = scope.get("app") + await receive() + try: + async with self.lifespan_context(app) as maybe_state: + if maybe_state is not None: + if "state" not in scope: + raise RuntimeError('The server does not support "state" in the lifespan scope.') + scope["state"].update(maybe_state) + await send({"type": "lifespan.startup.complete"}) + started = True + await receive() + except BaseException: + exc_text = traceback.format_exc() + if started: + await send({"type": "lifespan.shutdown.failed", "message": exc_text}) + else: + await send({"type": "lifespan.startup.failed", "message": exc_text}) + raise + else: + await send({"type": "lifespan.shutdown.complete"}) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """ + The main entry point to the Router class. + """ + await self.middleware_stack(scope, receive, send) + + async def app(self, scope: Scope, receive: Receive, send: Send) -> None: + assert scope["type"] in ("http", "websocket", "lifespan") + + if "router" not in scope: + scope["router"] = self + + if scope["type"] == "lifespan": + await self.lifespan(scope, receive, send) + return + + partial = None + + for route in self.routes: + # Determine if any route matches the incoming scope, + # and hand over to the matching route if found. + match, child_scope = route.matches(scope) + if match == Match.FULL: + scope.update(child_scope) + await route.handle(scope, receive, send) + return + elif match == Match.PARTIAL and partial is None: + partial = route + partial_scope = child_scope + + if partial is not None: + #  Handle partial matches. These are cases where an endpoint is + # able to handle the request, but is not a preferred option. + # We use this in particular to deal with "405 Method Not Allowed". + scope.update(partial_scope) + await partial.handle(scope, receive, send) + return + + route_path = get_route_path(scope) + if scope["type"] == "http" and self.redirect_slashes and route_path != "/": + redirect_scope = dict(scope) + if route_path.endswith("/"): + redirect_scope["path"] = redirect_scope["path"].rstrip("/") + else: + redirect_scope["path"] = redirect_scope["path"] + "/" + + for route in self.routes: + match, child_scope = route.matches(redirect_scope) + if match != Match.NONE: + redirect_url = URL(scope=redirect_scope) + response = RedirectResponse(url=str(redirect_url)) + await response(scope, receive, send) + return + + await self.default(scope, receive, send) + + def __eq__(self, other: Any) -> bool: + return isinstance(other, Router) and self.routes == other.routes + + def mount(self, path: str, app: ASGIApp, name: str | None = None) -> None: # pragma: no cover + route = Mount(path, app=app, name=name) + self.routes.append(route) + + def host(self, host: str, app: ASGIApp, name: str | None = None) -> None: # pragma: no cover + route = Host(host, app=app, name=name) + self.routes.append(route) + + def add_route( + self, + path: str, + endpoint: Callable[[Request], Awaitable[Response] | Response], + methods: Collection[str] | None = None, + name: str | None = None, + include_in_schema: bool = True, + ) -> None: # pragma: no cover + route = Route( + path, + endpoint=endpoint, + methods=methods, + name=name, + include_in_schema=include_in_schema, + ) + self.routes.append(route) + + def add_websocket_route( + self, + path: str, + endpoint: Callable[[WebSocket], Awaitable[None]], + name: str | None = None, + ) -> None: # pragma: no cover + route = WebSocketRoute(path, endpoint=endpoint, name=name) + self.routes.append(route) diff --git a/venv/lib/python3.11/site-packages/starlette/schemas.py b/venv/lib/python3.11/site-packages/starlette/schemas.py new file mode 100644 index 0000000000000000000000000000000000000000..cf7d439e3ab255663c72aa65e3ac5fb17a8db819 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/schemas.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +import inspect +import re +from collections.abc import Callable +from typing import Any, NamedTuple + +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import BaseRoute, Host, Mount, Route + +try: + import yaml +except ModuleNotFoundError: # pragma: no cover + yaml = None # type: ignore[assignment] + + +class OpenAPIResponse(Response): + media_type = "application/vnd.oai.openapi" + + def render(self, content: Any) -> bytes: + assert yaml is not None, "`pyyaml` must be installed to use OpenAPIResponse." + assert isinstance(content, dict), "The schema passed to OpenAPIResponse should be a dictionary." + return yaml.dump(content, default_flow_style=False).encode("utf-8") + + +class EndpointInfo(NamedTuple): + path: str + http_method: str + func: Callable[..., Any] + + +_remove_converter_pattern = re.compile(r":\w+}") + + +class BaseSchemaGenerator: + def get_schema(self, routes: list[BaseRoute]) -> dict[str, Any]: + raise NotImplementedError() # pragma: no cover + + def get_endpoints(self, routes: list[BaseRoute]) -> list[EndpointInfo]: + """ + Given the routes, yields the following information: + + - path + eg: /users/ + - http_method + one of 'get', 'post', 'put', 'patch', 'delete', 'options' + - func + method ready to extract the docstring + """ + endpoints_info: list[EndpointInfo] = [] + + for route in routes: + if isinstance(route, Mount | Host): + routes = route.routes or [] + if isinstance(route, Mount): + path = self._remove_converter(route.path) + else: + path = "" + sub_endpoints = [ + EndpointInfo( + path="".join((path, sub_endpoint.path)), + http_method=sub_endpoint.http_method, + func=sub_endpoint.func, + ) + for sub_endpoint in self.get_endpoints(routes) + ] + endpoints_info.extend(sub_endpoints) + + elif not isinstance(route, Route) or not route.include_in_schema: + continue + + elif inspect.isfunction(route.endpoint) or inspect.ismethod(route.endpoint): + path = self._remove_converter(route.path) + for method in route.methods or ["GET"]: + if method == "HEAD": + continue + endpoints_info.append(EndpointInfo(path, method.lower(), route.endpoint)) + else: + path = self._remove_converter(route.path) + for method in ["get", "post", "put", "patch", "delete", "options"]: + if not hasattr(route.endpoint, method): + continue + func = getattr(route.endpoint, method) + endpoints_info.append(EndpointInfo(path, method.lower(), func)) + + return endpoints_info + + def _remove_converter(self, path: str) -> str: + """ + Remove the converter from the path. + For example, a route like this: + Route("/users/{id:int}", endpoint=get_user, methods=["GET"]) + Should be represented as `/users/{id}` in the OpenAPI schema. + """ + return _remove_converter_pattern.sub("}", path) + + def parse_docstring(self, func_or_method: Callable[..., Any]) -> dict[str, Any]: + """ + Given a function, parse the docstring as YAML and return a dictionary of info. + """ + docstring = func_or_method.__doc__ + if not docstring: + return {} + + assert yaml is not None, "`pyyaml` must be installed to use parse_docstring." + + # We support having regular docstrings before the schema + # definition. Here we return just the schema part from + # the docstring. + docstring = docstring.split("---")[-1] + + parsed = yaml.safe_load(docstring) + + if not isinstance(parsed, dict): + # A regular docstring (not yaml formatted) can return + # a simple string here, which wouldn't follow the schema. + return {} + + return parsed + + def OpenAPIResponse(self, request: Request) -> Response: + routes = request.app.routes + schema = self.get_schema(routes=routes) + return OpenAPIResponse(schema) + + +class SchemaGenerator(BaseSchemaGenerator): + def __init__(self, base_schema: dict[str, Any]) -> None: + self.base_schema = base_schema + + def get_schema(self, routes: list[BaseRoute]) -> dict[str, Any]: + schema = dict(self.base_schema) + schema.setdefault("paths", {}) + endpoints_info = self.get_endpoints(routes) + + for endpoint in endpoints_info: + parsed = self.parse_docstring(endpoint.func) + + if not parsed: + continue + + if endpoint.path not in schema["paths"]: + schema["paths"][endpoint.path] = {} + + schema["paths"][endpoint.path][endpoint.http_method] = parsed + + return schema diff --git a/venv/lib/python3.11/site-packages/starlette/staticfiles.py b/venv/lib/python3.11/site-packages/starlette/staticfiles.py new file mode 100644 index 0000000000000000000000000000000000000000..ac39acc436a4118d99ffa7e1e4b38e21edb2837a --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/staticfiles.py @@ -0,0 +1,223 @@ +from __future__ import annotations + +import errno +import importlib.util +import os +import stat +from email.utils import parsedate +from typing import Union + +import anyio +import anyio.to_thread + +from starlette._utils import get_route_path +from starlette.datastructures import URL, Headers +from starlette.exceptions import HTTPException +from starlette.responses import FileResponse, RedirectResponse, Response +from starlette.types import Receive, Scope, Send + +PathLike = Union[str, "os.PathLike[str]"] + + +class NotModifiedResponse(Response): + NOT_MODIFIED_HEADERS = ( + "cache-control", + "content-location", + "date", + "etag", + "expires", + "vary", + ) + + def __init__(self, headers: Headers): + super().__init__( + status_code=304, + headers={name: value for name, value in headers.items() if name in self.NOT_MODIFIED_HEADERS}, + ) + + +class StaticFiles: + def __init__( + self, + *, + directory: PathLike | None = None, + packages: list[str | tuple[str, str]] | None = None, + html: bool = False, + check_dir: bool = True, + follow_symlink: bool = False, + ) -> None: + self.directory = directory + self.packages = packages + self.all_directories = self.get_directories(directory, packages) + self.html = html + self.config_checked = False + self.follow_symlink = follow_symlink + if check_dir and directory is not None and not os.path.isdir(directory): + raise RuntimeError(f"Directory '{directory}' does not exist") + + def get_directories( + self, + directory: PathLike | None = None, + packages: list[str | tuple[str, str]] | None = None, + ) -> list[PathLike]: + """ + Given `directory` and `packages` arguments, return a list of all the + directories that should be used for serving static files from. + """ + directories = [] + if directory is not None: + directories.append(directory) + + for package in packages or []: + if isinstance(package, tuple): + package, statics_dir = package + else: + statics_dir = "statics" + spec = importlib.util.find_spec(package) + assert spec is not None, f"Package {package!r} could not be found." + assert spec.origin is not None, f"Package {package!r} could not be found." + package_directory = os.path.normpath(os.path.join(spec.origin, "..", statics_dir)) + assert os.path.isdir(package_directory), ( + f"Directory '{statics_dir!r}' in package {package!r} could not be found." + ) + directories.append(package_directory) + + return directories + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """ + The ASGI entry point. + """ + assert scope["type"] == "http" + + if not self.config_checked: + await self.check_config() + self.config_checked = True + + path = self.get_path(scope) + response = await self.get_response(path, scope) + await response(scope, receive, send) + + def get_path(self, scope: Scope) -> str: + """ + Given the ASGI scope, return the `path` string to serve up, + with OS specific path separators, and any '..', '.' components removed. + """ + route_path = get_route_path(scope) + return os.path.normpath(os.path.join(*route_path.split("/"))) + + async def get_response(self, path: str, scope: Scope) -> Response: + """ + Returns an HTTP response, given the incoming path, method and request headers. + """ + if scope["method"] not in ("GET", "HEAD"): + raise HTTPException(status_code=405) + + try: + full_path, stat_result = await anyio.to_thread.run_sync(self.lookup_path, path) + except PermissionError: + raise HTTPException(status_code=401) + except OSError as exc: + # Filename is too long, so it can't be a valid static file. + if exc.errno == errno.ENAMETOOLONG: + raise HTTPException(status_code=404) + + raise exc + except ValueError: + # Null bytes or other invalid characters in the path. + raise HTTPException(status_code=404) + + if stat_result and stat.S_ISREG(stat_result.st_mode): + # We have a static file to serve. + return self.file_response(full_path, stat_result, scope) + + elif stat_result and stat.S_ISDIR(stat_result.st_mode) and self.html: + # We're in HTML mode, and have got a directory URL. + # Check if we have 'index.html' file to serve. + index_path = os.path.join(path, "index.html") + full_path, stat_result = await anyio.to_thread.run_sync(self.lookup_path, index_path) + if stat_result is not None and stat.S_ISREG(stat_result.st_mode): + if not scope["path"].endswith("/"): + # Directory URLs should redirect to always end in "/". + url = URL(scope=scope) + url = url.replace(path=url.path + "/") + return RedirectResponse(url=url) + return self.file_response(full_path, stat_result, scope) + + if self.html: + # Check for '404.html' if we're in HTML mode. + full_path, stat_result = await anyio.to_thread.run_sync(self.lookup_path, "404.html") + if stat_result and stat.S_ISREG(stat_result.st_mode): + return FileResponse(full_path, stat_result=stat_result, status_code=404) + raise HTTPException(status_code=404) + + def lookup_path(self, path: str) -> tuple[str, os.stat_result | None]: + # Reject absolute paths so they cannot escape the served directory. + if path.startswith(("/", "\\")): + return "", None + for directory in self.all_directories: + joined_path = os.path.join(directory, path) + if self.follow_symlink: + full_path = os.path.abspath(joined_path) + directory = os.path.abspath(directory) + else: + full_path = os.path.realpath(joined_path) + directory = os.path.realpath(directory) + if os.path.commonpath([full_path, directory]) != str(directory): + # Don't allow misbehaving clients to break out of the static files directory. + continue + try: + return full_path, os.stat(full_path) + except (FileNotFoundError, NotADirectoryError): + continue + return "", None + + def file_response( + self, + full_path: PathLike, + stat_result: os.stat_result, + scope: Scope, + status_code: int = 200, + ) -> Response: + request_headers = Headers(scope=scope) + + response = FileResponse(full_path, status_code=status_code, stat_result=stat_result) + if self.is_not_modified(response.headers, request_headers): + return NotModifiedResponse(response.headers) + return response + + async def check_config(self) -> None: + """ + Perform a one-off configuration check that StaticFiles is actually + pointed at a directory, so that we can raise loud errors rather than + just returning 404 responses. + """ + if self.directory is None: + return + + try: + stat_result = await anyio.to_thread.run_sync(os.stat, self.directory) + except FileNotFoundError: + raise RuntimeError(f"StaticFiles directory '{self.directory}' does not exist.") + if not (stat.S_ISDIR(stat_result.st_mode) or stat.S_ISLNK(stat_result.st_mode)): + raise RuntimeError(f"StaticFiles path '{self.directory}' is not a directory.") + + def is_not_modified(self, response_headers: Headers, request_headers: Headers) -> bool: + """ + Given the request and response headers, return `True` if an HTTP + "Not Modified" response could be returned instead. + """ + if if_none_match := request_headers.get("if-none-match"): + # The "etag" header is added by FileResponse, so it's always present. + etag = response_headers["etag"] + return etag in [tag.strip().removeprefix("W/") for tag in if_none_match.split(",")] + + try: + if_modified_since = parsedate(request_headers["if-modified-since"]) + last_modified = parsedate(response_headers["last-modified"]) + if if_modified_since is not None and last_modified is not None and if_modified_since >= last_modified: + return True + except KeyError: + pass + + return False diff --git a/venv/lib/python3.11/site-packages/starlette/status.py b/venv/lib/python3.11/site-packages/starlette/status.py new file mode 100644 index 0000000000000000000000000000000000000000..202b6b52c6feab5eacba13f7312b2c13a740ae69 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/status.py @@ -0,0 +1,211 @@ +""" +HTTP codes +See HTTP Status Code Registry: +https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + +And RFC 9110 - https://www.rfc-editor.org/rfc/rfc9110 +""" + +from __future__ import annotations + +import warnings + +from starlette.exceptions import StarletteDeprecationWarning + +__all__ = [ + "HTTP_100_CONTINUE", + "HTTP_101_SWITCHING_PROTOCOLS", + "HTTP_102_PROCESSING", + "HTTP_103_EARLY_HINTS", + "HTTP_200_OK", + "HTTP_201_CREATED", + "HTTP_202_ACCEPTED", + "HTTP_203_NON_AUTHORITATIVE_INFORMATION", + "HTTP_204_NO_CONTENT", + "HTTP_205_RESET_CONTENT", + "HTTP_206_PARTIAL_CONTENT", + "HTTP_207_MULTI_STATUS", + "HTTP_208_ALREADY_REPORTED", + "HTTP_226_IM_USED", + "HTTP_300_MULTIPLE_CHOICES", + "HTTP_301_MOVED_PERMANENTLY", + "HTTP_302_FOUND", + "HTTP_303_SEE_OTHER", + "HTTP_304_NOT_MODIFIED", + "HTTP_305_USE_PROXY", + "HTTP_306_RESERVED", + "HTTP_307_TEMPORARY_REDIRECT", + "HTTP_308_PERMANENT_REDIRECT", + "HTTP_400_BAD_REQUEST", + "HTTP_401_UNAUTHORIZED", + "HTTP_402_PAYMENT_REQUIRED", + "HTTP_403_FORBIDDEN", + "HTTP_404_NOT_FOUND", + "HTTP_405_METHOD_NOT_ALLOWED", + "HTTP_406_NOT_ACCEPTABLE", + "HTTP_407_PROXY_AUTHENTICATION_REQUIRED", + "HTTP_408_REQUEST_TIMEOUT", + "HTTP_409_CONFLICT", + "HTTP_410_GONE", + "HTTP_411_LENGTH_REQUIRED", + "HTTP_412_PRECONDITION_FAILED", + "HTTP_413_CONTENT_TOO_LARGE", + "HTTP_414_URI_TOO_LONG", + "HTTP_415_UNSUPPORTED_MEDIA_TYPE", + "HTTP_416_RANGE_NOT_SATISFIABLE", + "HTTP_417_EXPECTATION_FAILED", + "HTTP_418_IM_A_TEAPOT", + "HTTP_421_MISDIRECTED_REQUEST", + "HTTP_422_UNPROCESSABLE_CONTENT", + "HTTP_423_LOCKED", + "HTTP_424_FAILED_DEPENDENCY", + "HTTP_425_TOO_EARLY", + "HTTP_426_UPGRADE_REQUIRED", + "HTTP_428_PRECONDITION_REQUIRED", + "HTTP_429_TOO_MANY_REQUESTS", + "HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE", + "HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS", + "HTTP_500_INTERNAL_SERVER_ERROR", + "HTTP_501_NOT_IMPLEMENTED", + "HTTP_502_BAD_GATEWAY", + "HTTP_503_SERVICE_UNAVAILABLE", + "HTTP_504_GATEWAY_TIMEOUT", + "HTTP_505_HTTP_VERSION_NOT_SUPPORTED", + "HTTP_506_VARIANT_ALSO_NEGOTIATES", + "HTTP_507_INSUFFICIENT_STORAGE", + "HTTP_508_LOOP_DETECTED", + "HTTP_510_NOT_EXTENDED", + "HTTP_511_NETWORK_AUTHENTICATION_REQUIRED", + "WS_1000_NORMAL_CLOSURE", + "WS_1001_GOING_AWAY", + "WS_1002_PROTOCOL_ERROR", + "WS_1003_UNSUPPORTED_DATA", + "WS_1005_NO_STATUS_RCVD", + "WS_1006_ABNORMAL_CLOSURE", + "WS_1007_INVALID_FRAME_PAYLOAD_DATA", + "WS_1008_POLICY_VIOLATION", + "WS_1009_MESSAGE_TOO_BIG", + "WS_1010_MANDATORY_EXT", + "WS_1011_INTERNAL_ERROR", + "WS_1012_SERVICE_RESTART", + "WS_1013_TRY_AGAIN_LATER", + "WS_1014_BAD_GATEWAY", + "WS_1015_TLS_HANDSHAKE", +] + +HTTP_100_CONTINUE = 100 +HTTP_101_SWITCHING_PROTOCOLS = 101 +HTTP_102_PROCESSING = 102 +HTTP_103_EARLY_HINTS = 103 +HTTP_200_OK = 200 +HTTP_201_CREATED = 201 +HTTP_202_ACCEPTED = 202 +HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203 +HTTP_204_NO_CONTENT = 204 +HTTP_205_RESET_CONTENT = 205 +HTTP_206_PARTIAL_CONTENT = 206 +HTTP_207_MULTI_STATUS = 207 +HTTP_208_ALREADY_REPORTED = 208 +HTTP_226_IM_USED = 226 +HTTP_300_MULTIPLE_CHOICES = 300 +HTTP_301_MOVED_PERMANENTLY = 301 +HTTP_302_FOUND = 302 +HTTP_303_SEE_OTHER = 303 +HTTP_304_NOT_MODIFIED = 304 +HTTP_305_USE_PROXY = 305 +HTTP_306_RESERVED = 306 +HTTP_307_TEMPORARY_REDIRECT = 307 +HTTP_308_PERMANENT_REDIRECT = 308 +HTTP_400_BAD_REQUEST = 400 +HTTP_401_UNAUTHORIZED = 401 +HTTP_402_PAYMENT_REQUIRED = 402 +HTTP_403_FORBIDDEN = 403 +HTTP_404_NOT_FOUND = 404 +HTTP_405_METHOD_NOT_ALLOWED = 405 +HTTP_406_NOT_ACCEPTABLE = 406 +HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407 +HTTP_408_REQUEST_TIMEOUT = 408 +HTTP_409_CONFLICT = 409 +HTTP_410_GONE = 410 +HTTP_411_LENGTH_REQUIRED = 411 +HTTP_412_PRECONDITION_FAILED = 412 +HTTP_413_CONTENT_TOO_LARGE = 413 +HTTP_414_URI_TOO_LONG = 414 +HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415 +HTTP_416_RANGE_NOT_SATISFIABLE = 416 +HTTP_417_EXPECTATION_FAILED = 417 +HTTP_418_IM_A_TEAPOT = 418 +HTTP_421_MISDIRECTED_REQUEST = 421 +HTTP_422_UNPROCESSABLE_CONTENT = 422 +HTTP_423_LOCKED = 423 +HTTP_424_FAILED_DEPENDENCY = 424 +HTTP_425_TOO_EARLY = 425 +HTTP_426_UPGRADE_REQUIRED = 426 +HTTP_428_PRECONDITION_REQUIRED = 428 +HTTP_429_TOO_MANY_REQUESTS = 429 +HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 +HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451 +HTTP_500_INTERNAL_SERVER_ERROR = 500 +HTTP_501_NOT_IMPLEMENTED = 501 +HTTP_502_BAD_GATEWAY = 502 +HTTP_503_SERVICE_UNAVAILABLE = 503 +HTTP_504_GATEWAY_TIMEOUT = 504 +HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505 +HTTP_506_VARIANT_ALSO_NEGOTIATES = 506 +HTTP_507_INSUFFICIENT_STORAGE = 507 +HTTP_508_LOOP_DETECTED = 508 +HTTP_510_NOT_EXTENDED = 510 +HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511 + + +""" +WebSocket codes +https://www.iana.org/assignments/websocket/websocket.xml#close-code-number +https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent +""" +WS_1000_NORMAL_CLOSURE = 1000 +WS_1001_GOING_AWAY = 1001 +WS_1002_PROTOCOL_ERROR = 1002 +WS_1003_UNSUPPORTED_DATA = 1003 +WS_1005_NO_STATUS_RCVD = 1005 +WS_1006_ABNORMAL_CLOSURE = 1006 +WS_1007_INVALID_FRAME_PAYLOAD_DATA = 1007 +WS_1008_POLICY_VIOLATION = 1008 +WS_1009_MESSAGE_TOO_BIG = 1009 +WS_1010_MANDATORY_EXT = 1010 +WS_1011_INTERNAL_ERROR = 1011 +WS_1012_SERVICE_RESTART = 1012 +WS_1013_TRY_AGAIN_LATER = 1013 +WS_1014_BAD_GATEWAY = 1014 +WS_1015_TLS_HANDSHAKE = 1015 + +__deprecated__ = { + "HTTP_413_REQUEST_ENTITY_TOO_LARGE": 413, + "HTTP_414_REQUEST_URI_TOO_LONG": 414, + "HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE": 416, + "HTTP_422_UNPROCESSABLE_ENTITY": 422, +} + + +def __getattr__(name: str) -> int: + deprecation_changes = { + "HTTP_413_REQUEST_ENTITY_TOO_LARGE": "HTTP_413_CONTENT_TOO_LARGE", + "HTTP_414_REQUEST_URI_TOO_LONG": "HTTP_414_URI_TOO_LONG", + "HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE": "HTTP_416_RANGE_NOT_SATISFIABLE", + "HTTP_422_UNPROCESSABLE_ENTITY": "HTTP_422_UNPROCESSABLE_CONTENT", + } + + deprecated = __deprecated__.get(name) + if deprecated: + warnings.warn( + f"'{name}' is deprecated. Use '{deprecation_changes[name]}' instead.", + category=StarletteDeprecationWarning, + stacklevel=3, + ) + return deprecated + + raise AttributeError(f"module 'starlette.status' has no attribute '{name}'") + + +def __dir__() -> list[str]: + return sorted(list(__all__) + list(__deprecated__.keys())) # pragma: no cover diff --git a/venv/lib/python3.11/site-packages/starlette/templating.py b/venv/lib/python3.11/site-packages/starlette/templating.py new file mode 100644 index 0000000000000000000000000000000000000000..67872b37739db6594c37f17af51d248f8ab815a5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/templating.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from collections.abc import Callable, Mapping, Sequence +from os import PathLike +from typing import TYPE_CHECKING, Any, overload + +from starlette.background import BackgroundTask +from starlette.datastructures import URL +from starlette.requests import Request +from starlette.responses import HTMLResponse +from starlette.types import Receive, Scope, Send + +try: + import jinja2 + + # @contextfunction was renamed to @pass_context in Jinja 3.0, and was removed in 3.1 + # hence we try to get pass_context (most installs will be >=3.1) + # and fall back to contextfunction, + # adding a type ignore for mypy to let us access an attribute that may not exist + if TYPE_CHECKING: + pass_context = jinja2.pass_context + else: + if hasattr(jinja2, "pass_context"): + pass_context = jinja2.pass_context + else: # pragma: no cover + pass_context = jinja2.contextfunction # type: ignore[attr-defined] +except ImportError as _import_error: # pragma: no cover + raise ImportError("jinja2 must be installed to use Jinja2Templates") from _import_error + + +class _TemplateResponse(HTMLResponse): + def __init__( + self, + template: Any, + context: dict[str, Any], + status_code: int = 200, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + background: BackgroundTask | None = None, + ): + self.template = template + self.context = context + content = template.render(context) + super().__init__(content, status_code, headers, media_type, background) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + request = self.context.get("request", {}) + extensions = request.get("extensions", {}) + if "http.response.debug" in extensions: # pragma: no branch + await send({"type": "http.response.debug", "info": {"template": self.template, "context": self.context}}) + await super().__call__(scope, receive, send) + + +class Jinja2Templates: + """Jinja2 template renderer. + + Example: + ```python + from starlette.templating import Jinja2Templates + + templates = Jinja2Templates(directory="templates") + + async def homepage(request: Request) -> Response: + return templates.TemplateResponse(request, "index.html") + ``` + """ + + @overload + def __init__( + self, + directory: str | PathLike[str] | Sequence[str | PathLike[str]], + *, + context_processors: list[Callable[[Request], dict[str, Any]]] | None = None, + ) -> None: ... + + @overload + def __init__( + self, + *, + env: jinja2.Environment, + context_processors: list[Callable[[Request], dict[str, Any]]] | None = None, + ) -> None: ... + + def __init__( + self, + directory: str | PathLike[str] | Sequence[str | PathLike[str]] | None = None, + *, + context_processors: list[Callable[[Request], dict[str, Any]]] | None = None, + env: jinja2.Environment | None = None, + ) -> None: + assert bool(directory) ^ bool(env), "either 'directory' or 'env' arguments must be passed" + self.context_processors = context_processors or [] + if directory is not None: + loader = jinja2.FileSystemLoader(directory) + self.env = jinja2.Environment(loader=loader, autoescape=jinja2.select_autoescape()) + elif env is not None: # pragma: no branch + self.env = env + + self._setup_env_defaults(self.env) + + def _setup_env_defaults(self, env: jinja2.Environment) -> None: + @pass_context + def url_for( + context: dict[str, Any], + name: str, + /, + **path_params: Any, + ) -> URL: + request: Request = context["request"] + return request.url_for(name, **path_params) + + env.globals.setdefault("url_for", url_for) + + def get_template(self, name: str) -> jinja2.Template: + return self.env.get_template(name) + + def TemplateResponse( + self, + request: Request, + name: str, + context: dict[str, Any] | None = None, + status_code: int = 200, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + background: BackgroundTask | None = None, + ) -> _TemplateResponse: + """ + Render a template and return an HTML response. + + Args: + request: The incoming request instance. + name: The template file name to render. + context: Variables to pass to the template. + status_code: HTTP status code for the response. + headers: Additional headers to include in the response. + media_type: Media type for the response. + background: Background task to run after response is sent. + + Returns: + An HTML response with the rendered template content. + """ + context = context or {} + + context.setdefault("request", request) + for context_processor in self.context_processors: + context.update(context_processor(request)) + + template = self.get_template(name) + return _TemplateResponse( + template, + context, + status_code=status_code, + headers=headers, + media_type=media_type, + background=background, + ) diff --git a/venv/lib/python3.11/site-packages/starlette/testclient.py b/venv/lib/python3.11/site-packages/starlette/testclient.py new file mode 100644 index 0000000000000000000000000000000000000000..63d879098041c60ea076e1aac279f1869a99fda1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/testclient.py @@ -0,0 +1,748 @@ +from __future__ import annotations + +import contextlib +import inspect +import io +import json +import math +import sys +import warnings +from collections.abc import Awaitable, Callable, Generator, Iterable, Mapping, MutableMapping, Sequence +from concurrent.futures import Future +from contextlib import AbstractContextManager +from types import GeneratorType +from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeGuard, cast +from urllib.parse import unquote, urljoin + +import anyio +import anyio.abc +import anyio.from_thread +from anyio.streams.stapled import StapledObjectStream + +from starlette._utils import is_async_callable +from starlette.exceptions import StarletteDeprecationWarning +from starlette.types import ASGIApp, Message, Receive, Scope, Send +from starlette.websockets import WebSocketDisconnect + +if sys.version_info >= (3, 11): # pragma: no cover + from typing import Self +else: # pragma: no cover + from typing_extensions import Self + +if TYPE_CHECKING: + import httpx2 as httpx +else: + try: + import httpx2 as httpx + except ModuleNotFoundError: # pragma: no cover + try: + import httpx + except ModuleNotFoundError: + raise RuntimeError( + "The starlette.testclient module requires the httpx2 package to be installed.\n" + "You can install this with:\n" + " $ pip install httpx2\n" + ) from None + else: + warnings.warn( + "Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.", + StarletteDeprecationWarning, + stacklevel=2, + ) + +_PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]] + +ASGIInstance = Callable[[Receive, Send], Awaitable[None]] +ASGI2App = Callable[[Scope], ASGIInstance] +ASGI3App = Callable[[Scope, Receive, Send], Awaitable[None]] + + +_RequestData = Mapping[str, str | Iterable[str] | bytes] + + +def _is_asgi3(app: ASGI2App | ASGI3App) -> TypeGuard[ASGI3App]: + if inspect.isclass(app): + return hasattr(app, "__await__") + return is_async_callable(app) + + +class _WrapASGI2: + """ + Provide an ASGI3 interface onto an ASGI2 app. + """ + + def __init__(self, app: ASGI2App) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + instance = self.app(scope) + await instance(receive, send) + + +class _AsyncBackend(TypedDict): + backend: str + backend_options: dict[str, Any] + + +class _Upgrade(Exception): + def __init__(self, session: WebSocketTestSession) -> None: + self.session = session + + +class WebSocketDenialResponse( # type: ignore[misc] + httpx.Response, + WebSocketDisconnect, +): + """ + A special case of `WebSocketDisconnect`, raised in the `TestClient` if the + `WebSocket` is closed before being accepted with a `send_denial_response()`. + """ + + +class WebSocketTestSession: + def __init__( + self, + app: ASGI3App, + scope: Scope, + portal_factory: _PortalFactoryType, + ) -> None: + self.app = app + self.scope = scope + self.accepted_subprotocol = None + self.portal_factory = portal_factory + self.extra_headers = None + + def __enter__(self) -> Self: + with contextlib.ExitStack() as stack: + self.portal = portal = stack.enter_context(self.portal_factory()) + fut, cs = portal.start_task(self._run) + stack.callback(fut.result) + stack.callback(portal.call, cs.cancel) + self.send({"type": "websocket.connect"}) + message = self.receive() + self._raise_on_close(message) + self.accepted_subprotocol = message.get("subprotocol", None) + self.extra_headers = message.get("headers", None) + stack.callback(self.close, 1000) + self.exit_stack = stack.pop_all() + return self + + def __exit__(self, *args: Any) -> bool | None: + return self.exit_stack.__exit__(*args) + + async def _run(self, *, task_status: anyio.abc.TaskStatus[anyio.CancelScope]) -> None: + """ + The sub-thread in which the websocket session runs. + """ + send: anyio.create_memory_object_stream[Message] = anyio.create_memory_object_stream(math.inf) + send_tx, send_rx = send + receive: anyio.create_memory_object_stream[Message] = anyio.create_memory_object_stream(math.inf) + receive_tx, receive_rx = receive + with send_tx, send_rx, receive_tx, receive_rx, anyio.CancelScope() as cs: + self._receive_tx = receive_tx + self._send_rx = send_rx + task_status.started(cs) + await self.app(self.scope, receive_rx.receive, send_tx.send) + + # wait for cs.cancel to be called before closing streams + await anyio.sleep_forever() + + def _raise_on_close(self, message: Message) -> None: + if message["type"] == "websocket.close": + raise WebSocketDisconnect(code=message.get("code", 1000), reason=message.get("reason", "")) + elif message["type"] == "websocket.http.response.start": + status_code: int = message["status"] + headers: list[tuple[bytes, bytes]] = message["headers"] + body: list[bytes] = [] + while True: + message = self.receive() + assert message["type"] == "websocket.http.response.body" + body.append(message["body"]) + if not message.get("more_body", False): + break + raise WebSocketDenialResponse(status_code=status_code, headers=headers, content=b"".join(body)) + + def send(self, message: Message) -> None: + self.portal.call(self._receive_tx.send, message) + + def send_text(self, data: str) -> None: + self.send({"type": "websocket.receive", "text": data}) + + def send_bytes(self, data: bytes) -> None: + self.send({"type": "websocket.receive", "bytes": data}) + + def send_json(self, data: Any, mode: Literal["text", "binary"] = "text") -> None: + text = json.dumps(data, separators=(",", ":"), ensure_ascii=False) + if mode == "text": + self.send({"type": "websocket.receive", "text": text}) + else: + self.send({"type": "websocket.receive", "bytes": text.encode("utf-8")}) + + def close(self, code: int = 1000, reason: str | None = None) -> None: + self.send({"type": "websocket.disconnect", "code": code, "reason": reason}) + + def receive(self) -> Message: + return self.portal.call(self._send_rx.receive) + + def receive_text(self) -> str: + message = self.receive() + self._raise_on_close(message) + return cast(str, message["text"]) + + def receive_bytes(self) -> bytes: + message = self.receive() + self._raise_on_close(message) + return cast(bytes, message["bytes"]) + + def receive_json(self, mode: Literal["text", "binary"] = "text") -> Any: + message = self.receive() + self._raise_on_close(message) + if mode == "text": + text = message["text"] + else: + text = message["bytes"].decode("utf-8") + return json.loads(text) + + +class _TestClientTransport(httpx.BaseTransport): + def __init__( + self, + app: ASGI3App, + portal_factory: _PortalFactoryType, + raise_server_exceptions: bool = True, + root_path: str = "", + *, + client: tuple[str, int], + app_state: dict[str, Any], + ) -> None: + self.app = app + self.raise_server_exceptions = raise_server_exceptions + self.root_path = root_path + self.portal_factory = portal_factory + self.app_state = app_state + self.client = client + + def handle_request(self, request: httpx.Request) -> httpx.Response: + scheme = request.url.scheme + netloc = request.url.netloc.decode(encoding="ascii") + path = request.url.path + raw_path = request.url.raw_path + query = request.url.query.decode(encoding="ascii") + + default_port = {"http": 80, "ws": 80, "https": 443, "wss": 443}[scheme] + + if ":" in netloc: + host, port_string = netloc.split(":", 1) + port = int(port_string) + else: + host = netloc + port = default_port + + # Include the 'host' header. + if "host" in request.headers: + headers: list[tuple[bytes, bytes]] = [] + elif port == default_port: # pragma: no cover + headers = [(b"host", host.encode())] + else: # pragma: no cover + headers = [(b"host", (f"{host}:{port}").encode())] + + # Include other request headers. + headers += [(key.lower().encode(), value.encode()) for key, value in request.headers.multi_items()] + + scope: dict[str, Any] + + if scheme in {"ws", "wss"}: + subprotocol = request.headers.get("sec-websocket-protocol", None) + if subprotocol is None: + subprotocols: Sequence[str] = [] + else: + subprotocols = [value.strip() for value in subprotocol.split(",")] + scope = { + "type": "websocket", + "path": unquote(path), + "raw_path": raw_path.split(b"?", 1)[0], + "root_path": self.root_path, + "scheme": scheme, + "query_string": query.encode(), + "headers": headers, + "client": self.client, + "server": [host, port], + "subprotocols": subprotocols, + "state": self.app_state.copy(), + "extensions": {"websocket.http.response": {}}, + } + session = WebSocketTestSession(self.app, scope, self.portal_factory) + raise _Upgrade(session) + + scope = { + "type": "http", + "http_version": "1.1", + "method": request.method, + "path": unquote(path), + "raw_path": raw_path.split(b"?", 1)[0], + "root_path": self.root_path, + "scheme": scheme, + "query_string": query.encode(), + "headers": headers, + "client": self.client, + "server": [host, port], + "extensions": {"http.response.debug": {}}, + "state": self.app_state.copy(), + } + + request_complete = False + response_started = False + response_complete: anyio.Event + raw_kwargs: dict[str, Any] = {"stream": io.BytesIO()} + template = None + context = None + + async def receive() -> Message: + nonlocal request_complete + + if request_complete: + if not response_complete.is_set(): + await response_complete.wait() + return {"type": "http.disconnect"} + + body = request.read() + if isinstance(body, str): + body_bytes: bytes = body.encode("utf-8") # pragma: no cover + elif body is None: + body_bytes = b"" # pragma: no cover + elif isinstance(body, GeneratorType): + try: # pragma: no cover + chunk = body.send(None) + if isinstance(chunk, str): + chunk = chunk.encode("utf-8") + return {"type": "http.request", "body": chunk, "more_body": True} + except StopIteration: # pragma: no cover + request_complete = True + return {"type": "http.request", "body": b""} + else: + body_bytes = body + + request_complete = True + return {"type": "http.request", "body": body_bytes} + + async def send(message: Message) -> None: + nonlocal raw_kwargs, response_started, template, context + + if message["type"] == "http.response.start": + assert not response_started, 'Received multiple "http.response.start" messages.' + raw_kwargs["status_code"] = message["status"] + raw_kwargs["headers"] = [(key.decode(), value.decode()) for key, value in message.get("headers", [])] + response_started = True + elif message["type"] == "http.response.body": + assert response_started, 'Received "http.response.body" without "http.response.start".' + assert not response_complete.is_set(), 'Received "http.response.body" after response completed.' + body = message.get("body", b"") + more_body = message.get("more_body", False) + if request.method != "HEAD": + raw_kwargs["stream"].write(body) + if not more_body: + raw_kwargs["stream"].seek(0) + response_complete.set() + elif message["type"] == "http.response.debug": + template = message["info"]["template"] + context = message["info"]["context"] + + try: + with self.portal_factory() as portal: + response_complete = portal.call(anyio.Event) + portal.call(self.app, scope, receive, send) + except BaseException as exc: + if self.raise_server_exceptions: + raise exc + + if self.raise_server_exceptions: + assert response_started, "TestClient did not receive any response." + elif not response_started: + raw_kwargs = { + "status_code": 500, + "headers": [], + "stream": io.BytesIO(), + } + + raw_kwargs["stream"] = httpx.ByteStream(raw_kwargs["stream"].read()) + + response = httpx.Response(**raw_kwargs, request=request) + if template is not None: + response.template = template # type: ignore[attr-defined] + response.context = context # type: ignore[attr-defined] + return response + + +class TestClient(httpx.Client): + __test__ = False + task: Future[None] + portal: anyio.abc.BlockingPortal | None = None + + def __init__( + self, + app: ASGIApp, + base_url: str = "http://testserver", + raise_server_exceptions: bool = True, + root_path: str = "", + backend: Literal["asyncio", "trio"] = "asyncio", + backend_options: dict[str, Any] | None = None, + cookies: httpx._types.CookieTypes | None = None, + headers: dict[str, str] | None = None, + follow_redirects: bool = True, + client: tuple[str, int] = ("testclient", 50000), + ) -> None: + self.async_backend = _AsyncBackend(backend=backend, backend_options=backend_options or {}) + if _is_asgi3(app): + asgi_app = app + else: + app = cast(ASGI2App, app) # type: ignore[assignment] + asgi_app = _WrapASGI2(app) # type: ignore[arg-type] + self.app = asgi_app + self.app_state: dict[str, Any] = {} + transport = _TestClientTransport( + self.app, + portal_factory=self._portal_factory, + raise_server_exceptions=raise_server_exceptions, + root_path=root_path, + app_state=self.app_state, + client=client, + ) + if headers is None: + headers = {} + headers.setdefault("user-agent", "testclient") + super().__init__( + base_url=base_url, + headers=headers, + transport=transport, + follow_redirects=follow_redirects, + cookies=cookies, + ) + + @contextlib.contextmanager + def _portal_factory(self) -> Generator[anyio.abc.BlockingPortal, None, None]: + if self.portal is not None: + yield self.portal + else: + with anyio.from_thread.start_blocking_portal(**self.async_backend) as portal: + yield portal + + def request( # type: ignore[override] + self, + method: str, + url: httpx._types.URLTypes, + *, + content: httpx._types.RequestContent | None = None, + data: _RequestData | None = None, + files: httpx._types.RequestFiles | None = None, + json: Any = None, + params: httpx._types.QueryParamTypes | None = None, + headers: httpx._types.HeaderTypes | None = None, + cookies: httpx._types.CookieTypes | None = None, + auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + extensions: dict[str, Any] | None = None, + ) -> httpx.Response: + if timeout is not httpx.USE_CLIENT_DEFAULT: + warnings.warn( + "You should not use the 'timeout' argument with the TestClient. " + "See https://github.com/Kludex/starlette/issues/1108 for more information.", + StarletteDeprecationWarning, + stacklevel=2, + ) + url = self._merge_url(url) + return super().request( + method, + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def get( # type: ignore[override] + self, + url: httpx._types.URLTypes, + *, + params: httpx._types.QueryParamTypes | None = None, + headers: httpx._types.HeaderTypes | None = None, + cookies: httpx._types.CookieTypes | None = None, + auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + extensions: dict[str, Any] | None = None, + ) -> httpx.Response: + return super().get( + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def options( # type: ignore[override] + self, + url: httpx._types.URLTypes, + *, + params: httpx._types.QueryParamTypes | None = None, + headers: httpx._types.HeaderTypes | None = None, + cookies: httpx._types.CookieTypes | None = None, + auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + extensions: dict[str, Any] | None = None, + ) -> httpx.Response: + return super().options( + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def head( # type: ignore[override] + self, + url: httpx._types.URLTypes, + *, + params: httpx._types.QueryParamTypes | None = None, + headers: httpx._types.HeaderTypes | None = None, + cookies: httpx._types.CookieTypes | None = None, + auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + extensions: dict[str, Any] | None = None, + ) -> httpx.Response: + return super().head( + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def post( # type: ignore[override] + self, + url: httpx._types.URLTypes, + *, + content: httpx._types.RequestContent | None = None, + data: _RequestData | None = None, + files: httpx._types.RequestFiles | None = None, + json: Any = None, + params: httpx._types.QueryParamTypes | None = None, + headers: httpx._types.HeaderTypes | None = None, + cookies: httpx._types.CookieTypes | None = None, + auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + extensions: dict[str, Any] | None = None, + ) -> httpx.Response: + return super().post( + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def put( # type: ignore[override] + self, + url: httpx._types.URLTypes, + *, + content: httpx._types.RequestContent | None = None, + data: _RequestData | None = None, + files: httpx._types.RequestFiles | None = None, + json: Any = None, + params: httpx._types.QueryParamTypes | None = None, + headers: httpx._types.HeaderTypes | None = None, + cookies: httpx._types.CookieTypes | None = None, + auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + extensions: dict[str, Any] | None = None, + ) -> httpx.Response: + return super().put( + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def patch( # type: ignore[override] + self, + url: httpx._types.URLTypes, + *, + content: httpx._types.RequestContent | None = None, + data: _RequestData | None = None, + files: httpx._types.RequestFiles | None = None, + json: Any = None, + params: httpx._types.QueryParamTypes | None = None, + headers: httpx._types.HeaderTypes | None = None, + cookies: httpx._types.CookieTypes | None = None, + auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + extensions: dict[str, Any] | None = None, + ) -> httpx.Response: + return super().patch( + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def delete( # type: ignore[override] + self, + url: httpx._types.URLTypes, + *, + params: httpx._types.QueryParamTypes | None = None, + headers: httpx._types.HeaderTypes | None = None, + cookies: httpx._types.CookieTypes | None = None, + auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + extensions: dict[str, Any] | None = None, + ) -> httpx.Response: + return super().delete( + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def websocket_connect( + self, + url: str, + subprotocols: Sequence[str] | None = None, + **kwargs: Any, + ) -> WebSocketTestSession: + url = urljoin("ws://testserver", url) + headers = kwargs.get("headers", {}) + headers.setdefault("connection", "upgrade") + headers.setdefault("sec-websocket-key", "testserver==") + headers.setdefault("sec-websocket-version", "13") + if subprotocols is not None: + headers.setdefault("sec-websocket-protocol", ", ".join(subprotocols)) + kwargs["headers"] = headers + try: + super().request("GET", url, **kwargs) + except _Upgrade as exc: + session = exc.session + else: + raise RuntimeError("Expected WebSocket upgrade") # pragma: no cover + + return session + + def __enter__(self) -> Self: + with contextlib.ExitStack() as stack: + self.portal = portal = stack.enter_context(anyio.from_thread.start_blocking_portal(**self.async_backend)) + + @stack.callback + def reset_portal() -> None: + self.portal = None + + send: anyio.create_memory_object_stream[MutableMapping[str, Any] | None] = ( + anyio.create_memory_object_stream(math.inf) + ) + receive: anyio.create_memory_object_stream[MutableMapping[str, Any]] = anyio.create_memory_object_stream( + math.inf + ) + for channel in (*send, *receive): + stack.callback(channel.close) + self.stream_send = StapledObjectStream(*send) + self.stream_receive = StapledObjectStream(*receive) + self.task = portal.start_task_soon(self.lifespan) + portal.call(self.wait_startup) + + @stack.callback + def wait_shutdown() -> None: + portal.call(self.wait_shutdown) + + self.exit_stack = stack.pop_all() + + return self + + def __exit__(self, *args: Any) -> None: + self.exit_stack.close() + + async def lifespan(self) -> None: + scope = {"type": "lifespan", "state": self.app_state} + try: + await self.app(scope, self.stream_receive.receive, self.stream_send.send) + finally: + await self.stream_send.send(None) + + async def wait_startup(self) -> None: + await self.stream_receive.send({"type": "lifespan.startup"}) + + async def receive() -> Any: + message = await self.stream_send.receive() + if message is None: + self.task.result() + return message + + message = await receive() + assert message["type"] in ( + "lifespan.startup.complete", + "lifespan.startup.failed", + ) + if message["type"] == "lifespan.startup.failed": + await receive() + + async def wait_shutdown(self) -> None: + async def receive() -> Any: + message = await self.stream_send.receive() + if message is None: + self.task.result() + return message + + await self.stream_receive.send({"type": "lifespan.shutdown"}) + message = await receive() + assert message["type"] in ( + "lifespan.shutdown.complete", + "lifespan.shutdown.failed", + ) + if message["type"] == "lifespan.shutdown.failed": + await receive() diff --git a/venv/lib/python3.11/site-packages/starlette/types.py b/venv/lib/python3.11/site-packages/starlette/types.py new file mode 100644 index 0000000000000000000000000000000000000000..b767ae1599a781d8544e605faaabcfdcca194ac8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/types.py @@ -0,0 +1,26 @@ +from collections.abc import Awaitable, Callable, Mapping, MutableMapping +from contextlib import AbstractAsyncContextManager +from typing import TYPE_CHECKING, Any, TypeVar + +if TYPE_CHECKING: + from starlette.requests import Request + from starlette.responses import Response + from starlette.websockets import WebSocket + +AppType = TypeVar("AppType") + +Scope = MutableMapping[str, Any] +Message = MutableMapping[str, Any] + +Receive = Callable[[], Awaitable[Message]] +Send = Callable[[Message], Awaitable[None]] + +ASGIApp = Callable[[Scope, Receive, Send], Awaitable[None]] + +StatelessLifespan = Callable[[AppType], AbstractAsyncContextManager[None]] +StatefulLifespan = Callable[[AppType], AbstractAsyncContextManager[Mapping[str, Any]]] +Lifespan = StatelessLifespan[AppType] | StatefulLifespan[AppType] + +HTTPExceptionHandler = Callable[["Request", Exception], "Response | Awaitable[Response]"] +WebSocketExceptionHandler = Callable[["WebSocket", Exception], Awaitable[None]] +ExceptionHandler = HTTPExceptionHandler | WebSocketExceptionHandler diff --git a/venv/lib/python3.11/site-packages/starlette/websockets.py b/venv/lib/python3.11/site-packages/starlette/websockets.py new file mode 100644 index 0000000000000000000000000000000000000000..3ab709a308fdb65b675f3e5044c1733f2ddcbf4d --- /dev/null +++ b/venv/lib/python3.11/site-packages/starlette/websockets.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +import enum +import json +from collections.abc import AsyncIterator, Iterable +from typing import Any, cast + +from starlette.requests import HTTPConnection, StateT +from starlette.responses import Response +from starlette.types import Message, Receive, Scope, Send + + +class WebSocketState(enum.Enum): + CONNECTING = 0 + CONNECTED = 1 + DISCONNECTED = 2 + RESPONSE = 3 + + +class WebSocketDisconnect(Exception): + def __init__(self, code: int = 1000, reason: str | None = None) -> None: + self.code = code + self.reason = reason or "" + + +class WebSocket(HTTPConnection[StateT]): + def __init__(self, scope: Scope, receive: Receive, send: Send) -> None: + super().__init__(scope) + assert scope["type"] == "websocket" + self._receive = receive + self._send = send + self.client_state = WebSocketState.CONNECTING + self.application_state = WebSocketState.CONNECTING + + async def receive(self) -> Message: + """ + Receive ASGI websocket messages, ensuring valid state transitions. + """ + if self.client_state == WebSocketState.CONNECTING: + message = await self._receive() + message_type = message["type"] + if message_type != "websocket.connect": + raise RuntimeError(f'Expected ASGI message "websocket.connect", but got {message_type!r}') + self.client_state = WebSocketState.CONNECTED + return message + elif self.client_state == WebSocketState.CONNECTED: + message = await self._receive() + message_type = message["type"] + if message_type not in {"websocket.receive", "websocket.disconnect"}: + raise RuntimeError( + f'Expected ASGI message "websocket.receive" or "websocket.disconnect", but got {message_type!r}' + ) + if message_type == "websocket.disconnect": + self.client_state = WebSocketState.DISCONNECTED + return message + else: + raise RuntimeError('Cannot call "receive" once a disconnect message has been received.') + + async def send(self, message: Message) -> None: + """ + Send ASGI websocket messages, ensuring valid state transitions. + """ + if self.application_state == WebSocketState.CONNECTING: + message_type = message["type"] + if message_type not in {"websocket.accept", "websocket.close", "websocket.http.response.start"}: + raise RuntimeError( + 'Expected ASGI message "websocket.accept", "websocket.close" or "websocket.http.response.start", ' + f"but got {message_type!r}" + ) + if message_type == "websocket.close": + self.application_state = WebSocketState.DISCONNECTED + elif message_type == "websocket.http.response.start": + self.application_state = WebSocketState.RESPONSE + else: + self.application_state = WebSocketState.CONNECTED + await self._send(message) + elif self.application_state == WebSocketState.CONNECTED: + message_type = message["type"] + if message_type not in {"websocket.send", "websocket.close"}: + raise RuntimeError( + f'Expected ASGI message "websocket.send" or "websocket.close", but got {message_type!r}' + ) + if message_type == "websocket.close": + self.application_state = WebSocketState.DISCONNECTED + try: + await self._send(message) + except OSError: + self.application_state = WebSocketState.DISCONNECTED + raise WebSocketDisconnect(code=1006) + elif self.application_state == WebSocketState.RESPONSE: + message_type = message["type"] + if message_type != "websocket.http.response.body": + raise RuntimeError(f'Expected ASGI message "websocket.http.response.body", but got {message_type!r}') + if not message.get("more_body", False): + self.application_state = WebSocketState.DISCONNECTED + await self._send(message) + else: + raise RuntimeError('Cannot call "send" once a close message has been sent.') + + async def accept( + self, + subprotocol: str | None = None, + headers: Iterable[tuple[bytes, bytes]] | None = None, + ) -> None: + headers = headers or [] + + if self.client_state == WebSocketState.CONNECTING: # pragma: no branch + # If we haven't yet seen the 'connect' message, then wait for it first. + await self.receive() + await self.send({"type": "websocket.accept", "subprotocol": subprotocol, "headers": headers}) + + def _raise_on_disconnect(self, message: Message) -> None: + if message["type"] == "websocket.disconnect": + raise WebSocketDisconnect(message["code"], message.get("reason")) + + async def receive_text(self) -> str: + if self.application_state != WebSocketState.CONNECTED: + raise RuntimeError('WebSocket is not connected. Need to call "accept" first.') + message = await self.receive() + self._raise_on_disconnect(message) + return cast(str, message["text"]) + + async def receive_bytes(self) -> bytes: + if self.application_state != WebSocketState.CONNECTED: + raise RuntimeError('WebSocket is not connected. Need to call "accept" first.') + message = await self.receive() + self._raise_on_disconnect(message) + return cast(bytes, message["bytes"]) + + async def receive_json(self, mode: str = "text") -> Any: + if mode not in {"text", "binary"}: + raise RuntimeError('The "mode" argument should be "text" or "binary".') + if self.application_state != WebSocketState.CONNECTED: + raise RuntimeError('WebSocket is not connected. Need to call "accept" first.') + message = await self.receive() + self._raise_on_disconnect(message) + + if mode == "text": + text = message["text"] + else: + text = message["bytes"].decode("utf-8") + return json.loads(text) + + async def iter_text(self) -> AsyncIterator[str]: + try: + while True: + yield await self.receive_text() + except WebSocketDisconnect: + pass + + async def iter_bytes(self) -> AsyncIterator[bytes]: + try: + while True: + yield await self.receive_bytes() + except WebSocketDisconnect: + pass + + async def iter_json(self) -> AsyncIterator[Any]: + try: + while True: + yield await self.receive_json() + except WebSocketDisconnect: + pass + + async def send_text(self, data: str) -> None: + await self.send({"type": "websocket.send", "text": data}) + + async def send_bytes(self, data: bytes) -> None: + await self.send({"type": "websocket.send", "bytes": data}) + + async def send_json(self, data: Any, mode: str = "text") -> None: + if mode not in {"text", "binary"}: + raise RuntimeError('The "mode" argument should be "text" or "binary".') + text = json.dumps(data, separators=(",", ":"), ensure_ascii=False) + if mode == "text": + await self.send({"type": "websocket.send", "text": text}) + else: + await self.send({"type": "websocket.send", "bytes": text.encode("utf-8")}) + + async def close(self, code: int = 1000, reason: str | None = None) -> None: + await self.send({"type": "websocket.close", "code": code, "reason": reason or ""}) + + async def send_denial_response(self, response: Response) -> None: + if "websocket.http.response" in self.scope.get("extensions", {}): + await response(self.scope, self.receive, self.send) + else: + raise RuntimeError("The server doesn't support the Websocket Denial Response extension.") + + +class WebSocketClose: + def __init__(self, code: int = 1000, reason: str | None = None) -> None: + self.code = code + self.reason = reason or "" + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + await send({"type": "websocket.close", "code": self.code, "reason": self.reason}) diff --git a/venv/lib/python3.11/site-packages/sympy/__init__.py b/venv/lib/python3.11/site-packages/sympy/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c67ba49409169c49ac5d34f2be365def3c30369b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/__init__.py @@ -0,0 +1,545 @@ +""" +SymPy is a Python library for symbolic mathematics. It aims to become a +full-featured computer algebra system (CAS) while keeping the code as simple +as possible in order to be comprehensible and easily extensible. SymPy is +written entirely in Python. It depends on mpmath, and other external libraries +may be optionally for things like plotting support. + +See the webpage for more information and documentation: + + https://sympy.org + +""" + + +# Keep this in sync with setup.py/pyproject.toml +import sys +if sys.version_info < (3, 9): + raise ImportError("Python version 3.9 or above is required for SymPy.") +del sys + + +try: + import mpmath +except ImportError: + raise ImportError("SymPy now depends on mpmath as an external library. " + "See https://docs.sympy.org/latest/install.html#mpmath for more information.") + +del mpmath + +from sympy.release import __version__ +from sympy.core.cache import lazy_function + +if 'dev' in __version__: + def enable_warnings(): + import warnings + warnings.filterwarnings('default', '.*', DeprecationWarning, module='sympy.*') + del warnings + enable_warnings() + del enable_warnings + + +def __sympy_debug(): + # helper function so we don't import os globally + import os + debug_str = os.getenv('SYMPY_DEBUG', 'False') + if debug_str in ('True', 'False'): + return eval(debug_str) + else: + raise RuntimeError("unrecognized value for SYMPY_DEBUG: %s" % + debug_str) +# Fails py2 test if using type hinting +SYMPY_DEBUG = __sympy_debug() # type: bool + + +from .core import (sympify, SympifyError, cacheit, Basic, Atom, + preorder_traversal, S, Expr, AtomicExpr, UnevaluatedExpr, Symbol, + Wild, Dummy, symbols, var, Number, Float, Rational, Integer, + NumberSymbol, RealNumber, igcd, ilcm, seterr, E, I, nan, oo, pi, zoo, + AlgebraicNumber, comp, mod_inverse, Pow, integer_nthroot, integer_log, + trailing, Mul, prod, Add, Mod, Rel, Eq, Ne, Lt, Le, Gt, Ge, Equality, + GreaterThan, LessThan, Unequality, StrictGreaterThan, StrictLessThan, + vectorize, Lambda, WildFunction, Derivative, diff, FunctionClass, + Function, Subs, expand, PoleError, count_ops, expand_mul, expand_log, + expand_func, expand_trig, expand_complex, expand_multinomial, nfloat, + expand_power_base, expand_power_exp, arity, PrecisionExhausted, N, + evalf, Tuple, Dict, gcd_terms, factor_terms, factor_nc, evaluate, + Catalan, EulerGamma, GoldenRatio, TribonacciConstant, bottom_up, use, + postorder_traversal, default_sort_key, ordered, num_digits) + +from .logic import (to_cnf, to_dnf, to_nnf, And, Or, Not, Xor, Nand, Nor, + Implies, Equivalent, ITE, POSform, SOPform, simplify_logic, bool_map, + true, false, satisfiable) + +from .assumptions import (AppliedPredicate, Predicate, AssumptionsContext, + assuming, Q, ask, register_handler, remove_handler, refine) + +from .polys import (Poly, PurePoly, poly_from_expr, parallel_poly_from_expr, + degree, total_degree, degree_list, LC, LM, LT, pdiv, prem, pquo, + pexquo, div, rem, quo, exquo, half_gcdex, gcdex, invert, + subresultants, resultant, discriminant, cofactors, gcd_list, gcd, + lcm_list, lcm, terms_gcd, trunc, monic, content, primitive, compose, + decompose, sturm, gff_list, gff, sqf_norm, sqf_part, sqf_list, sqf, + factor_list, factor, intervals, refine_root, count_roots, all_roots, + real_roots, nroots, ground_roots, nth_power_roots_poly, cancel, + reduced, groebner, is_zero_dimensional, GroebnerBasis, poly, + symmetrize, horner, interpolate, rational_interpolate, viete, together, + BasePolynomialError, ExactQuotientFailed, PolynomialDivisionFailed, + OperationNotSupported, HeuristicGCDFailed, HomomorphismFailed, + IsomorphismFailed, ExtraneousFactors, EvaluationFailed, + RefinementFailed, CoercionFailed, NotInvertible, NotReversible, + NotAlgebraic, DomainError, PolynomialError, UnificationFailed, + GeneratorsError, GeneratorsNeeded, ComputationFailed, + UnivariatePolynomialError, MultivariatePolynomialError, + PolificationFailed, OptionError, FlagError, minpoly, + minimal_polynomial, primitive_element, field_isomorphism, + to_number_field, isolate, round_two, prime_decomp, prime_valuation, + galois_group, itermonomials, Monomial, lex, grlex, + grevlex, ilex, igrlex, igrevlex, CRootOf, rootof, RootOf, + ComplexRootOf, RootSum, roots, Domain, FiniteField, IntegerRing, + RationalField, RealField, ComplexField, PythonFiniteField, + GMPYFiniteField, PythonIntegerRing, GMPYIntegerRing, PythonRational, + GMPYRationalField, AlgebraicField, PolynomialRing, FractionField, + ExpressionDomain, FF_python, FF_gmpy, ZZ_python, ZZ_gmpy, QQ_python, + QQ_gmpy, GF, FF, ZZ, QQ, ZZ_I, QQ_I, RR, CC, EX, EXRAW, + construct_domain, swinnerton_dyer_poly, cyclotomic_poly, + symmetric_poly, random_poly, interpolating_poly, jacobi_poly, + chebyshevt_poly, chebyshevu_poly, hermite_poly, hermite_prob_poly, + legendre_poly, laguerre_poly, apart, apart_list, assemble_partfrac_list, + Options, ring, xring, vring, sring, field, xfield, vfield, sfield) + +from .series import (Order, O, limit, Limit, gruntz, series, approximants, + residue, EmptySequence, SeqPer, SeqFormula, sequence, SeqAdd, SeqMul, + fourier_series, fps, difference_delta, limit_seq) + +from .functions import (factorial, factorial2, rf, ff, binomial, + RisingFactorial, FallingFactorial, subfactorial, carmichael, + fibonacci, lucas, motzkin, tribonacci, harmonic, bernoulli, bell, euler, + catalan, genocchi, andre, partition, divisor_sigma, legendre_symbol, + jacobi_symbol, kronecker_symbol, mobius, primenu, primeomega, + totient, reduced_totient, primepi, sqrt, root, Min, Max, Id, + real_root, Rem, cbrt, re, im, sign, Abs, conjugate, arg, polar_lift, + periodic_argument, unbranched_argument, principal_branch, transpose, + adjoint, polarify, unpolarify, sin, cos, tan, sec, csc, cot, sinc, + asin, acos, atan, asec, acsc, acot, atan2, exp_polar, exp, ln, log, + LambertW, sinh, cosh, tanh, coth, sech, csch, asinh, acosh, atanh, + acoth, asech, acsch, floor, ceiling, frac, Piecewise, piecewise_fold, + piecewise_exclusive, erf, erfc, erfi, erf2, erfinv, erfcinv, erf2inv, + Ei, expint, E1, li, Li, Si, Ci, Shi, Chi, fresnels, fresnelc, gamma, + lowergamma, uppergamma, polygamma, loggamma, digamma, trigamma, + multigamma, dirichlet_eta, zeta, lerchphi, polylog, stieltjes, Eijk, + LeviCivita, KroneckerDelta, SingularityFunction, DiracDelta, Heaviside, + bspline_basis, bspline_basis_set, interpolating_spline, besselj, + bessely, besseli, besselk, hankel1, hankel2, jn, yn, jn_zeros, hn1, + hn2, airyai, airybi, airyaiprime, airybiprime, marcumq, hyper, + meijerg, appellf1, legendre, assoc_legendre, hermite, hermite_prob, + chebyshevt, chebyshevu, chebyshevu_root, chebyshevt_root, laguerre, + assoc_laguerre, gegenbauer, jacobi, jacobi_normalized, Ynm, Ynm_c, + Znm, elliptic_k, elliptic_f, elliptic_e, elliptic_pi, beta, mathieus, + mathieuc, mathieusprime, mathieucprime, riemann_xi, betainc, betainc_regularized) + +from .ntheory import (nextprime, prevprime, prime, primerange, + randprime, Sieve, sieve, primorial, cycle_length, composite, + compositepi, isprime, divisors, proper_divisors, factorint, + multiplicity, perfect_power, factor_cache, pollard_pm1, pollard_rho, primefactors, + divisor_count, proper_divisor_count, + factorrat, + mersenne_prime_exponent, is_perfect, is_mersenne_prime, is_abundant, + is_deficient, is_amicable, is_carmichael, abundance, npartitions, is_primitive_root, + is_quad_residue, n_order, sqrt_mod, + quadratic_residues, primitive_root, nthroot_mod, is_nthpow_residue, + sqrt_mod_iter, discrete_log, quadratic_congruence, + binomial_coefficients, binomial_coefficients_list, + multinomial_coefficients, continued_fraction_periodic, + continued_fraction_iterator, continued_fraction_reduce, + continued_fraction_convergents, continued_fraction, egyptian_fraction) + +from .concrete import product, Product, summation, Sum + +from .discrete import (fft, ifft, ntt, intt, fwht, ifwht, mobius_transform, + inverse_mobius_transform, convolution, covering_product, + intersecting_product) + +from .simplify import (simplify, hypersimp, hypersimilar, logcombine, + separatevars, posify, besselsimp, kroneckersimp, signsimp, + nsimplify, FU, fu, sqrtdenest, cse, epath, EPath, hyperexpand, + collect, rcollect, radsimp, collect_const, fraction, numer, denom, + trigsimp, exptrigsimp, powsimp, powdenest, combsimp, gammasimp, + ratsimp, ratsimpmodprime) + +from .sets import (Set, Interval, Union, EmptySet, FiniteSet, ProductSet, + Intersection, DisjointUnion, imageset, Complement, SymmetricDifference, ImageSet, + Range, ComplexRegion, Complexes, Reals, Contains, ConditionSet, Ordinal, + OmegaPower, ord0, PowerSet, Naturals, Naturals0, UniversalSet, + Integers, Rationals) + +from .solvers import (solve, solve_linear_system, solve_linear_system_LU, + solve_undetermined_coeffs, nsolve, solve_linear, checksol, det_quick, + inv_quick, check_assumptions, failing_assumptions, diophantine, + rsolve, rsolve_poly, rsolve_ratio, rsolve_hyper, checkodesol, + classify_ode, dsolve, homogeneous_order, solve_poly_system, factor_system, + solve_triangulated, pde_separate, pde_separate_add, pde_separate_mul, + pdsolve, classify_pde, checkpdesol, ode_order, reduce_inequalities, + reduce_abs_inequality, reduce_abs_inequalities, solve_poly_inequality, + solve_rational_inequalities, solve_univariate_inequality, decompogen, + solveset, linsolve, linear_eq_to_matrix, nonlinsolve, substitution) + +from .matrices import (ShapeError, NonSquareMatrixError, GramSchmidt, + casoratian, diag, eye, hessian, jordan_cell, list2numpy, matrix2numpy, + matrix_multiply_elementwise, ones, randMatrix, rot_axis1, rot_axis2, + rot_axis3, symarray, wronskian, zeros, MutableDenseMatrix, + DeferredVector, MatrixBase, Matrix, MutableMatrix, + MutableSparseMatrix, banded, ImmutableDenseMatrix, + ImmutableSparseMatrix, ImmutableMatrix, SparseMatrix, MatrixSlice, + BlockDiagMatrix, BlockMatrix, FunctionMatrix, Identity, Inverse, + MatAdd, MatMul, MatPow, MatrixExpr, MatrixSymbol, Trace, Transpose, + ZeroMatrix, OneMatrix, blockcut, block_collapse, matrix_symbols, + Adjoint, hadamard_product, HadamardProduct, HadamardPower, + Determinant, det, diagonalize_vector, DiagMatrix, DiagonalMatrix, + DiagonalOf, trace, DotProduct, kronecker_product, KroneckerProduct, + PermutationMatrix, MatrixPermute, Permanent, per, rot_ccw_axis1, + rot_ccw_axis2, rot_ccw_axis3, rot_givens) + +from .geometry import (Point, Point2D, Point3D, Line, Ray, Segment, Line2D, + Segment2D, Ray2D, Line3D, Segment3D, Ray3D, Plane, Ellipse, Circle, + Polygon, RegularPolygon, Triangle, rad, deg, are_similar, centroid, + convex_hull, idiff, intersection, closest_points, farthest_points, + GeometryError, Curve, Parabola) + +from .utilities import (flatten, group, take, subsets, variations, + numbered_symbols, cartes, capture, dict_merge, prefixes, postfixes, + sift, topological_sort, unflatten, has_dups, has_variety, reshape, + rotations, filldedent, lambdify, + threaded, xthreaded, public, memoize_property, timed) + +from .integrals import (integrate, Integral, line_integrate, mellin_transform, + inverse_mellin_transform, MellinTransform, InverseMellinTransform, + laplace_transform, laplace_correspondence, laplace_initial_conds, + inverse_laplace_transform, LaplaceTransform, + InverseLaplaceTransform, fourier_transform, inverse_fourier_transform, + FourierTransform, InverseFourierTransform, sine_transform, + inverse_sine_transform, SineTransform, InverseSineTransform, + cosine_transform, inverse_cosine_transform, CosineTransform, + InverseCosineTransform, hankel_transform, inverse_hankel_transform, + HankelTransform, InverseHankelTransform, singularityintegrate) + +from .tensor import (IndexedBase, Idx, Indexed, get_contraction_structure, + get_indices, shape, MutableDenseNDimArray, ImmutableDenseNDimArray, + MutableSparseNDimArray, ImmutableSparseNDimArray, NDimArray, + tensorproduct, tensorcontraction, tensordiagonal, derive_by_array, + permutedims, Array, DenseNDimArray, SparseNDimArray) + +from .parsing import parse_expr + +from .calculus import (euler_equations, singularities, is_increasing, + is_strictly_increasing, is_decreasing, is_strictly_decreasing, + is_monotonic, finite_diff_weights, apply_finite_diff, + differentiate_finite, periodicity, not_empty_in, AccumBounds, + is_convex, stationary_points, minimum, maximum) + +from .algebras import Quaternion + +from .printing import (pager_print, pretty, pretty_print, pprint, + pprint_use_unicode, pprint_try_use_unicode, latex, print_latex, + multiline_latex, mathml, print_mathml, python, print_python, pycode, + ccode, print_ccode, smtlib_code, glsl_code, print_glsl, cxxcode, fcode, + print_fcode, rcode, print_rcode, jscode, print_jscode, julia_code, + mathematica_code, octave_code, rust_code, print_gtk, preview, srepr, + print_tree, StrPrinter, sstr, sstrrepr, TableForm, dotprint, + maple_code, print_maple_code) + +test = lazy_function('sympy.testing.runtests_pytest', 'test') +doctest = lazy_function('sympy.testing.runtests', 'doctest') + +# This module causes conflicts with other modules: +# from .stats import * +# Adds about .04-.05 seconds of import time +# from combinatorics import * +# This module is slow to import: +#from physics import units +from .plotting import plot, textplot, plot_backends, plot_implicit, plot_parametric +from .interactive import init_session, init_printing, interactive_traversal + +evalf._create_evalf_table() + +__all__ = [ + '__version__', + + # sympy.core + 'sympify', 'SympifyError', 'cacheit', 'Basic', 'Atom', + 'preorder_traversal', 'S', 'Expr', 'AtomicExpr', 'UnevaluatedExpr', + 'Symbol', 'Wild', 'Dummy', 'symbols', 'var', 'Number', 'Float', + 'Rational', 'Integer', 'NumberSymbol', 'RealNumber', 'igcd', 'ilcm', + 'seterr', 'E', 'I', 'nan', 'oo', 'pi', 'zoo', 'AlgebraicNumber', 'comp', + 'mod_inverse', 'Pow', 'integer_nthroot', 'integer_log', 'trailing', 'Mul', 'prod', + 'Add', 'Mod', 'Rel', 'Eq', 'Ne', 'Lt', 'Le', 'Gt', 'Ge', 'Equality', + 'GreaterThan', 'LessThan', 'Unequality', 'StrictGreaterThan', + 'StrictLessThan', 'vectorize', 'Lambda', 'WildFunction', 'Derivative', + 'diff', 'FunctionClass', 'Function', 'Subs', 'expand', 'PoleError', + 'count_ops', 'expand_mul', 'expand_log', 'expand_func', 'expand_trig', + 'expand_complex', 'expand_multinomial', 'nfloat', 'expand_power_base', + 'expand_power_exp', 'arity', 'PrecisionExhausted', 'N', 'evalf', 'Tuple', + 'Dict', 'gcd_terms', 'factor_terms', 'factor_nc', 'evaluate', 'Catalan', + 'EulerGamma', 'GoldenRatio', 'TribonacciConstant', 'bottom_up', 'use', + 'postorder_traversal', 'default_sort_key', 'ordered', 'num_digits', + + # sympy.logic + 'to_cnf', 'to_dnf', 'to_nnf', 'And', 'Or', 'Not', 'Xor', 'Nand', 'Nor', + 'Implies', 'Equivalent', 'ITE', 'POSform', 'SOPform', 'simplify_logic', + 'bool_map', 'true', 'false', 'satisfiable', + + # sympy.assumptions + 'AppliedPredicate', 'Predicate', 'AssumptionsContext', 'assuming', 'Q', + 'ask', 'register_handler', 'remove_handler', 'refine', + + # sympy.polys + 'Poly', 'PurePoly', 'poly_from_expr', 'parallel_poly_from_expr', 'degree', + 'total_degree', 'degree_list', 'LC', 'LM', 'LT', 'pdiv', 'prem', 'pquo', + 'pexquo', 'div', 'rem', 'quo', 'exquo', 'half_gcdex', 'gcdex', 'invert', + 'subresultants', 'resultant', 'discriminant', 'cofactors', 'gcd_list', + 'gcd', 'lcm_list', 'lcm', 'terms_gcd', 'trunc', 'monic', 'content', + 'primitive', 'compose', 'decompose', 'sturm', 'gff_list', 'gff', + 'sqf_norm', 'sqf_part', 'sqf_list', 'sqf', 'factor_list', 'factor', + 'intervals', 'refine_root', 'count_roots', 'all_roots', 'real_roots', + 'nroots', 'ground_roots', 'nth_power_roots_poly', 'cancel', 'reduced', + 'groebner', 'is_zero_dimensional', 'GroebnerBasis', 'poly', 'symmetrize', + 'horner', 'interpolate', 'rational_interpolate', 'viete', 'together', + 'BasePolynomialError', 'ExactQuotientFailed', 'PolynomialDivisionFailed', + 'OperationNotSupported', 'HeuristicGCDFailed', 'HomomorphismFailed', + 'IsomorphismFailed', 'ExtraneousFactors', 'EvaluationFailed', + 'RefinementFailed', 'CoercionFailed', 'NotInvertible', 'NotReversible', + 'NotAlgebraic', 'DomainError', 'PolynomialError', 'UnificationFailed', + 'GeneratorsError', 'GeneratorsNeeded', 'ComputationFailed', + 'UnivariatePolynomialError', 'MultivariatePolynomialError', + 'PolificationFailed', 'OptionError', 'FlagError', 'minpoly', + 'minimal_polynomial', 'primitive_element', 'field_isomorphism', + 'to_number_field', 'isolate', 'round_two', 'prime_decomp', + 'prime_valuation', 'galois_group', 'itermonomials', 'Monomial', 'lex', 'grlex', + 'grevlex', 'ilex', 'igrlex', 'igrevlex', 'CRootOf', 'rootof', 'RootOf', + 'ComplexRootOf', 'RootSum', 'roots', 'Domain', 'FiniteField', + 'IntegerRing', 'RationalField', 'RealField', 'ComplexField', + 'PythonFiniteField', 'GMPYFiniteField', 'PythonIntegerRing', + 'GMPYIntegerRing', 'PythonRational', 'GMPYRationalField', + 'AlgebraicField', 'PolynomialRing', 'FractionField', 'ExpressionDomain', + 'FF_python', 'FF_gmpy', 'ZZ_python', 'ZZ_gmpy', 'QQ_python', 'QQ_gmpy', + 'GF', 'FF', 'ZZ', 'QQ', 'ZZ_I', 'QQ_I', 'RR', 'CC', 'EX', 'EXRAW', + 'construct_domain', 'swinnerton_dyer_poly', 'cyclotomic_poly', + 'symmetric_poly', 'random_poly', 'interpolating_poly', 'jacobi_poly', + 'chebyshevt_poly', 'chebyshevu_poly', 'hermite_poly', 'hermite_prob_poly', + 'legendre_poly', 'laguerre_poly', 'apart', 'apart_list', 'assemble_partfrac_list', + 'Options', 'ring', 'xring', 'vring', 'sring', 'field', 'xfield', 'vfield', + 'sfield', + + # sympy.series + 'Order', 'O', 'limit', 'Limit', 'gruntz', 'series', 'approximants', + 'residue', 'EmptySequence', 'SeqPer', 'SeqFormula', 'sequence', 'SeqAdd', + 'SeqMul', 'fourier_series', 'fps', 'difference_delta', 'limit_seq', + + # sympy.functions + 'factorial', 'factorial2', 'rf', 'ff', 'binomial', 'RisingFactorial', + 'FallingFactorial', 'subfactorial', 'carmichael', 'fibonacci', 'lucas', + 'motzkin', 'tribonacci', 'harmonic', 'bernoulli', 'bell', 'euler', 'catalan', + 'genocchi', 'andre', 'partition', 'divisor_sigma', 'legendre_symbol', 'jacobi_symbol', + 'kronecker_symbol', 'mobius', 'primenu', 'primeomega', 'totient', 'primepi', + 'reduced_totient', 'sqrt', 'root', 'Min', 'Max', 'Id', 'real_root', + 'Rem', 'cbrt', 're', 'im', 'sign', 'Abs', 'conjugate', 'arg', 'polar_lift', + 'periodic_argument', 'unbranched_argument', 'principal_branch', + 'transpose', 'adjoint', 'polarify', 'unpolarify', 'sin', 'cos', 'tan', + 'sec', 'csc', 'cot', 'sinc', 'asin', 'acos', 'atan', 'asec', 'acsc', + 'acot', 'atan2', 'exp_polar', 'exp', 'ln', 'log', 'LambertW', 'sinh', + 'cosh', 'tanh', 'coth', 'sech', 'csch', 'asinh', 'acosh', 'atanh', + 'acoth', 'asech', 'acsch', 'floor', 'ceiling', 'frac', 'Piecewise', + 'piecewise_fold', 'piecewise_exclusive', 'erf', 'erfc', 'erfi', 'erf2', + 'erfinv', 'erfcinv', 'erf2inv', 'Ei', 'expint', 'E1', 'li', 'Li', 'Si', + 'Ci', 'Shi', 'Chi', 'fresnels', 'fresnelc', 'gamma', 'lowergamma', + 'uppergamma', 'polygamma', 'loggamma', 'digamma', 'trigamma', 'multigamma', + 'dirichlet_eta', 'zeta', 'lerchphi', 'polylog', 'stieltjes', 'Eijk', 'LeviCivita', + 'KroneckerDelta', 'SingularityFunction', 'DiracDelta', 'Heaviside', + 'bspline_basis', 'bspline_basis_set', 'interpolating_spline', 'besselj', + 'bessely', 'besseli', 'besselk', 'hankel1', 'hankel2', 'jn', 'yn', + 'jn_zeros', 'hn1', 'hn2', 'airyai', 'airybi', 'airyaiprime', + 'airybiprime', 'marcumq', 'hyper', 'meijerg', 'appellf1', 'legendre', + 'assoc_legendre', 'hermite', 'hermite_prob', 'chebyshevt', 'chebyshevu', + 'chebyshevu_root', 'chebyshevt_root', 'laguerre', 'assoc_laguerre', + 'gegenbauer', 'jacobi', 'jacobi_normalized', 'Ynm', 'Ynm_c', 'Znm', + 'elliptic_k', 'elliptic_f', 'elliptic_e', 'elliptic_pi', 'beta', + 'mathieus', 'mathieuc', 'mathieusprime', 'mathieucprime', 'riemann_xi','betainc', + 'betainc_regularized', + + # sympy.ntheory + 'nextprime', 'prevprime', 'prime', 'primerange', 'randprime', + 'Sieve', 'sieve', 'primorial', 'cycle_length', 'composite', 'compositepi', + 'isprime', 'divisors', 'proper_divisors', 'factorint', 'multiplicity', + 'perfect_power', 'pollard_pm1', 'factor_cache', 'pollard_rho', 'primefactors', + 'divisor_count', 'proper_divisor_count', + 'factorrat', + 'mersenne_prime_exponent', 'is_perfect', 'is_mersenne_prime', + 'is_abundant', 'is_deficient', 'is_amicable', 'is_carmichael', 'abundance', + 'npartitions', + 'is_primitive_root', 'is_quad_residue', + 'n_order', 'sqrt_mod', 'quadratic_residues', + 'primitive_root', 'nthroot_mod', 'is_nthpow_residue', 'sqrt_mod_iter', + 'discrete_log', 'quadratic_congruence', 'binomial_coefficients', + 'binomial_coefficients_list', 'multinomial_coefficients', + 'continued_fraction_periodic', 'continued_fraction_iterator', + 'continued_fraction_reduce', 'continued_fraction_convergents', + 'continued_fraction', 'egyptian_fraction', + + # sympy.concrete + 'product', 'Product', 'summation', 'Sum', + + # sympy.discrete + 'fft', 'ifft', 'ntt', 'intt', 'fwht', 'ifwht', 'mobius_transform', + 'inverse_mobius_transform', 'convolution', 'covering_product', + 'intersecting_product', + + # sympy.simplify + 'simplify', 'hypersimp', 'hypersimilar', 'logcombine', 'separatevars', + 'posify', 'besselsimp', 'kroneckersimp', 'signsimp', + 'nsimplify', 'FU', 'fu', 'sqrtdenest', 'cse', 'epath', 'EPath', + 'hyperexpand', 'collect', 'rcollect', 'radsimp', 'collect_const', + 'fraction', 'numer', 'denom', 'trigsimp', 'exptrigsimp', 'powsimp', + 'powdenest', 'combsimp', 'gammasimp', 'ratsimp', 'ratsimpmodprime', + + # sympy.sets + 'Set', 'Interval', 'Union', 'EmptySet', 'FiniteSet', 'ProductSet', + 'Intersection', 'imageset', 'DisjointUnion', 'Complement', 'SymmetricDifference', + 'ImageSet', 'Range', 'ComplexRegion', 'Reals', 'Contains', 'ConditionSet', + 'Ordinal', 'OmegaPower', 'ord0', 'PowerSet', 'Naturals', + 'Naturals0', 'UniversalSet', 'Integers', 'Rationals', 'Complexes', + + # sympy.solvers + 'solve', 'solve_linear_system', 'solve_linear_system_LU', + 'solve_undetermined_coeffs', 'nsolve', 'solve_linear', 'checksol', + 'det_quick', 'inv_quick', 'check_assumptions', 'failing_assumptions', + 'diophantine', 'rsolve', 'rsolve_poly', 'rsolve_ratio', 'rsolve_hyper', + 'checkodesol', 'classify_ode', 'dsolve', 'homogeneous_order', + 'solve_poly_system', 'factor_system', 'solve_triangulated', 'pde_separate', + 'pde_separate_add', 'pde_separate_mul', 'pdsolve', 'classify_pde', + 'checkpdesol', 'ode_order', 'reduce_inequalities', + 'reduce_abs_inequality', 'reduce_abs_inequalities', + 'solve_poly_inequality', 'solve_rational_inequalities', + 'solve_univariate_inequality', 'decompogen', 'solveset', 'linsolve', + 'linear_eq_to_matrix', 'nonlinsolve', 'substitution', + + # sympy.matrices + 'ShapeError', 'NonSquareMatrixError', 'GramSchmidt', 'casoratian', 'diag', + 'eye', 'hessian', 'jordan_cell', 'list2numpy', 'matrix2numpy', + 'matrix_multiply_elementwise', 'ones', 'randMatrix', 'rot_axis1', + 'rot_axis2', 'rot_axis3', 'symarray', 'wronskian', 'zeros', + 'MutableDenseMatrix', 'DeferredVector', 'MatrixBase', 'Matrix', + 'MutableMatrix', 'MutableSparseMatrix', 'banded', 'ImmutableDenseMatrix', + 'ImmutableSparseMatrix', 'ImmutableMatrix', 'SparseMatrix', 'MatrixSlice', + 'BlockDiagMatrix', 'BlockMatrix', 'FunctionMatrix', 'Identity', 'Inverse', + 'MatAdd', 'MatMul', 'MatPow', 'MatrixExpr', 'MatrixSymbol', 'Trace', + 'Transpose', 'ZeroMatrix', 'OneMatrix', 'blockcut', 'block_collapse', + 'matrix_symbols', 'Adjoint', 'hadamard_product', 'HadamardProduct', + 'HadamardPower', 'Determinant', 'det', 'diagonalize_vector', 'DiagMatrix', + 'DiagonalMatrix', 'DiagonalOf', 'trace', 'DotProduct', + 'kronecker_product', 'KroneckerProduct', 'PermutationMatrix', + 'MatrixPermute', 'Permanent', 'per', 'rot_ccw_axis1', 'rot_ccw_axis2', + 'rot_ccw_axis3', 'rot_givens', + + # sympy.geometry + 'Point', 'Point2D', 'Point3D', 'Line', 'Ray', 'Segment', 'Line2D', + 'Segment2D', 'Ray2D', 'Line3D', 'Segment3D', 'Ray3D', 'Plane', 'Ellipse', + 'Circle', 'Polygon', 'RegularPolygon', 'Triangle', 'rad', 'deg', + 'are_similar', 'centroid', 'convex_hull', 'idiff', 'intersection', + 'closest_points', 'farthest_points', 'GeometryError', 'Curve', 'Parabola', + + # sympy.utilities + 'flatten', 'group', 'take', 'subsets', 'variations', 'numbered_symbols', + 'cartes', 'capture', 'dict_merge', 'prefixes', 'postfixes', 'sift', + 'topological_sort', 'unflatten', 'has_dups', 'has_variety', 'reshape', + 'rotations', 'filldedent', 'lambdify', 'threaded', 'xthreaded', + 'public', 'memoize_property', 'timed', + + # sympy.integrals + 'integrate', 'Integral', 'line_integrate', 'mellin_transform', + 'inverse_mellin_transform', 'MellinTransform', 'InverseMellinTransform', + 'laplace_transform', 'inverse_laplace_transform', 'LaplaceTransform', + 'laplace_correspondence', 'laplace_initial_conds', + 'InverseLaplaceTransform', 'fourier_transform', + 'inverse_fourier_transform', 'FourierTransform', + 'InverseFourierTransform', 'sine_transform', 'inverse_sine_transform', + 'SineTransform', 'InverseSineTransform', 'cosine_transform', + 'inverse_cosine_transform', 'CosineTransform', 'InverseCosineTransform', + 'hankel_transform', 'inverse_hankel_transform', 'HankelTransform', + 'InverseHankelTransform', 'singularityintegrate', + + # sympy.tensor + 'IndexedBase', 'Idx', 'Indexed', 'get_contraction_structure', + 'get_indices', 'shape', 'MutableDenseNDimArray', 'ImmutableDenseNDimArray', + 'MutableSparseNDimArray', 'ImmutableSparseNDimArray', 'NDimArray', + 'tensorproduct', 'tensorcontraction', 'tensordiagonal', 'derive_by_array', + 'permutedims', 'Array', 'DenseNDimArray', 'SparseNDimArray', + + # sympy.parsing + 'parse_expr', + + # sympy.calculus + 'euler_equations', 'singularities', 'is_increasing', + 'is_strictly_increasing', 'is_decreasing', 'is_strictly_decreasing', + 'is_monotonic', 'finite_diff_weights', 'apply_finite_diff', + 'differentiate_finite', 'periodicity', 'not_empty_in', + 'AccumBounds', 'is_convex', 'stationary_points', 'minimum', 'maximum', + + # sympy.algebras + 'Quaternion', + + # sympy.printing + 'pager_print', 'pretty', 'pretty_print', 'pprint', 'pprint_use_unicode', + 'pprint_try_use_unicode', 'latex', 'print_latex', 'multiline_latex', + 'mathml', 'print_mathml', 'python', 'print_python', 'pycode', 'ccode', + 'print_ccode', 'smtlib_code', 'glsl_code', 'print_glsl', 'cxxcode', 'fcode', + 'print_fcode', 'rcode', 'print_rcode', 'jscode', 'print_jscode', + 'julia_code', 'mathematica_code', 'octave_code', 'rust_code', 'print_gtk', + 'preview', 'srepr', 'print_tree', 'StrPrinter', 'sstr', 'sstrrepr', + 'TableForm', 'dotprint', 'maple_code', 'print_maple_code', + + # sympy.plotting + 'plot', 'textplot', 'plot_backends', 'plot_implicit', 'plot_parametric', + + # sympy.interactive + 'init_session', 'init_printing', 'interactive_traversal', + + # sympy.testing + 'test', 'doctest', +] + + +#===========================================================================# +# # +# XXX: The names below were importable before SymPy 1.6 using # +# # +# from sympy import * # +# # +# This happened implicitly because there was no __all__ defined in this # +# __init__.py file. Not every package is imported. The list matches what # +# would have been imported before. It is possible that these packages will # +# not be imported by a star-import from sympy in future. # +# # +#===========================================================================# + + +__all__.extend(( + 'algebras', + 'assumptions', + 'calculus', + 'concrete', + 'discrete', + 'external', + 'functions', + 'geometry', + 'interactive', + 'multipledispatch', + 'ntheory', + 'parsing', + 'plotting', + 'polys', + 'printing', + 'release', + 'strategies', + 'tensor', + 'utilities', +)) diff --git a/venv/lib/python3.11/site-packages/sympy/abc.py b/venv/lib/python3.11/site-packages/sympy/abc.py new file mode 100644 index 0000000000000000000000000000000000000000..a6f7a4056e60b0d651becc0b9909bd0ab3c1723f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/abc.py @@ -0,0 +1,111 @@ +""" +This module exports all latin and greek letters as Symbols, so you can +conveniently do + + >>> from sympy.abc import x, y + +instead of the slightly more clunky-looking + + >>> from sympy import symbols + >>> x, y = symbols('x y') + +Caveats +======= + +1. As of the time of writing this, the names ``O``, ``S``, ``I``, ``N``, +``E``, and ``Q`` are colliding with names defined in SymPy. If you import them +from both ``sympy.abc`` and ``sympy``, the second import will "win". +This is an issue only for * imports, which should only be used for short-lived +code such as interactive sessions and throwaway scripts that do not survive +until the next SymPy upgrade, where ``sympy`` may contain a different set of +names. + +2. This module does not define symbol names on demand, i.e. +``from sympy.abc import foo`` will be reported as an error because +``sympy.abc`` does not contain the name ``foo``. To get a symbol named ``foo``, +you still need to use ``Symbol('foo')`` or ``symbols('foo')``. +You can freely mix usage of ``sympy.abc`` and ``Symbol``/``symbols``, though +sticking with one and only one way to get the symbols does tend to make the code +more readable. + +The module also defines some special names to help detect which names clash +with the default SymPy namespace. + +``_clash1`` defines all the single letter variables that clash with +SymPy objects; ``_clash2`` defines the multi-letter clashing symbols; +and ``_clash`` is the union of both. These can be passed for ``locals`` +during sympification if one desires Symbols rather than the non-Symbol +objects for those names. + +Examples +======== + +>>> from sympy import S +>>> from sympy.abc import _clash1, _clash2, _clash +>>> S("Q & C", locals=_clash1) +C & Q +>>> S('pi(x)', locals=_clash2) +pi(x) +>>> S('pi(C, Q)', locals=_clash) +pi(C, Q) + +""" +from __future__ import annotations +from typing import Any + +import string + +from .core import Symbol, symbols +from .core.alphabets import greeks +from sympy.parsing.sympy_parser import null + +##### Symbol definitions ##### + +# Implementation note: The easiest way to avoid typos in the symbols() +# parameter is to copy it from the left-hand side of the assignment. + +a, b, c, d, e, f, g, h, i, j = symbols('a, b, c, d, e, f, g, h, i, j') +k, l, m, n, o, p, q, r, s, t = symbols('k, l, m, n, o, p, q, r, s, t') +u, v, w, x, y, z = symbols('u, v, w, x, y, z') + +A, B, C, D, E, F, G, H, I, J = symbols('A, B, C, D, E, F, G, H, I, J') +K, L, M, N, O, P, Q, R, S, T = symbols('K, L, M, N, O, P, Q, R, S, T') +U, V, W, X, Y, Z = symbols('U, V, W, X, Y, Z') + +alpha, beta, gamma, delta = symbols('alpha, beta, gamma, delta') +epsilon, zeta, eta, theta = symbols('epsilon, zeta, eta, theta') +iota, kappa, lamda, mu = symbols('iota, kappa, lamda, mu') +nu, xi, omicron, pi = symbols('nu, xi, omicron, pi') +rho, sigma, tau, upsilon = symbols('rho, sigma, tau, upsilon') +phi, chi, psi, omega = symbols('phi, chi, psi, omega') + + +##### Clashing-symbols diagnostics ##### + +# We want to know which names in SymPy collide with those in here. +# This is mostly for diagnosing SymPy's namespace during SymPy development. + +_latin = list(string.ascii_letters) +# QOSINE should not be imported as they clash; gamma, pi and zeta clash, too +_greek = list(greeks) # make a copy, so we can mutate it +# Note: We import lamda since lambda is a reserved keyword in Python +_greek.remove("lambda") +_greek.append("lamda") + +ns: dict[str, Any] = {} +exec('from sympy import *', ns) +_clash1: dict[str, Any] = {} +_clash2: dict[str, Any] = {} +while ns: + _k, _ = ns.popitem() + if _k in _greek: + _clash2[_k] = null + _greek.remove(_k) + elif _k in _latin: + _clash1[_k] = null + _latin.remove(_k) +_clash = {} +_clash.update(_clash1) +_clash.update(_clash2) + +del _latin, _greek, Symbol, _k, null diff --git a/venv/lib/python3.11/site-packages/sympy/algebras/__init__.py b/venv/lib/python3.11/site-packages/sympy/algebras/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..58013d2e0377a016d1a21fbf21c344ee76765189 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/algebras/__init__.py @@ -0,0 +1,3 @@ +from .quaternion import Quaternion + +__all__ = ["Quaternion",] diff --git a/venv/lib/python3.11/site-packages/sympy/algebras/quaternion.py b/venv/lib/python3.11/site-packages/sympy/algebras/quaternion.py new file mode 100644 index 0000000000000000000000000000000000000000..1bf4b363545128e50294e825461106ef9b41990d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/algebras/quaternion.py @@ -0,0 +1,1666 @@ +from sympy.core.numbers import Rational +from sympy.core.singleton import S +from sympy.core.relational import is_eq +from sympy.functions.elementary.complexes import (conjugate, im, re, sign) +from sympy.functions.elementary.exponential import (exp, log as ln) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (acos, asin, atan2) +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.simplify.trigsimp import trigsimp +from sympy.integrals.integrals import integrate +from sympy.matrices.dense import MutableDenseMatrix as Matrix +from sympy.core.sympify import sympify, _sympify +from sympy.core.expr import Expr +from sympy.core.logic import fuzzy_not, fuzzy_or +from sympy.utilities.misc import as_int + +from mpmath.libmp.libmpf import prec_to_dps + + +def _check_norm(elements, norm): + """validate if input norm is consistent""" + if norm is not None and norm.is_number: + if norm.is_positive is False: + raise ValueError("Input norm must be positive.") + + numerical = all(i.is_number and i.is_real is True for i in elements) + if numerical and is_eq(norm**2, sum(i**2 for i in elements)) is False: + raise ValueError("Incompatible value for norm.") + + +def _is_extrinsic(seq): + """validate seq and return True if seq is lowercase and False if uppercase""" + if type(seq) != str: + raise ValueError('Expected seq to be a string.') + if len(seq) != 3: + raise ValueError("Expected 3 axes, got `{}`.".format(seq)) + + intrinsic = seq.isupper() + extrinsic = seq.islower() + if not (intrinsic or extrinsic): + raise ValueError("seq must either be fully uppercase (for extrinsic " + "rotations), or fully lowercase, for intrinsic " + "rotations).") + + i, j, k = seq.lower() + if (i == j) or (j == k): + raise ValueError("Consecutive axes must be different") + + bad = set(seq) - set('xyzXYZ') + if bad: + raise ValueError("Expected axes from `seq` to be from " + "['x', 'y', 'z'] or ['X', 'Y', 'Z'], " + "got {}".format(''.join(bad))) + + return extrinsic + + +class Quaternion(Expr): + """Provides basic quaternion operations. + Quaternion objects can be instantiated as ``Quaternion(a, b, c, d)`` + as in $q = a + bi + cj + dk$. + + Parameters + ========== + + norm : None or number + Pre-defined quaternion norm. If a value is given, Quaternion.norm + returns this pre-defined value instead of calculating the norm + + Examples + ======== + + >>> from sympy import Quaternion + >>> q = Quaternion(1, 2, 3, 4) + >>> q + 1 + 2*i + 3*j + 4*k + + Quaternions over complex fields can be defined as: + + >>> from sympy import Quaternion + >>> from sympy import symbols, I + >>> x = symbols('x') + >>> q1 = Quaternion(x, x**3, x, x**2, real_field = False) + >>> q2 = Quaternion(3 + 4*I, 2 + 5*I, 0, 7 + 8*I, real_field = False) + >>> q1 + x + x**3*i + x*j + x**2*k + >>> q2 + (3 + 4*I) + (2 + 5*I)*i + 0*j + (7 + 8*I)*k + + Defining symbolic unit quaternions: + + >>> from sympy import Quaternion + >>> from sympy.abc import w, x, y, z + >>> q = Quaternion(w, x, y, z, norm=1) + >>> q + w + x*i + y*j + z*k + >>> q.norm() + 1 + + References + ========== + + .. [1] https://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/ + .. [2] https://en.wikipedia.org/wiki/Quaternion + + """ + _op_priority = 11.0 + + is_commutative = False + + def __new__(cls, a=0, b=0, c=0, d=0, real_field=True, norm=None): + a, b, c, d = map(sympify, (a, b, c, d)) + + if any(i.is_commutative is False for i in [a, b, c, d]): + raise ValueError("arguments have to be commutative") + obj = super().__new__(cls, a, b, c, d) + obj._real_field = real_field + obj.set_norm(norm) + return obj + + def set_norm(self, norm): + """Sets norm of an already instantiated quaternion. + + Parameters + ========== + + norm : None or number + Pre-defined quaternion norm. If a value is given, Quaternion.norm + returns this pre-defined value instead of calculating the norm + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy.abc import a, b, c, d + >>> q = Quaternion(a, b, c, d) + >>> q.norm() + sqrt(a**2 + b**2 + c**2 + d**2) + + Setting the norm: + + >>> q.set_norm(1) + >>> q.norm() + 1 + + Removing set norm: + + >>> q.set_norm(None) + >>> q.norm() + sqrt(a**2 + b**2 + c**2 + d**2) + + """ + norm = sympify(norm) + _check_norm(self.args, norm) + self._norm = norm + + @property + def a(self): + return self.args[0] + + @property + def b(self): + return self.args[1] + + @property + def c(self): + return self.args[2] + + @property + def d(self): + return self.args[3] + + @property + def real_field(self): + return self._real_field + + @property + def product_matrix_left(self): + r"""Returns 4 x 4 Matrix equivalent to a Hamilton product from the + left. This can be useful when treating quaternion elements as column + vectors. Given a quaternion $q = a + bi + cj + dk$ where a, b, c and d + are real numbers, the product matrix from the left is: + + .. math:: + + M = \begin{bmatrix} a &-b &-c &-d \\ + b & a &-d & c \\ + c & d & a &-b \\ + d &-c & b & a \end{bmatrix} + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy.abc import a, b, c, d + >>> q1 = Quaternion(1, 0, 0, 1) + >>> q2 = Quaternion(a, b, c, d) + >>> q1.product_matrix_left + Matrix([ + [1, 0, 0, -1], + [0, 1, -1, 0], + [0, 1, 1, 0], + [1, 0, 0, 1]]) + + >>> q1.product_matrix_left * q2.to_Matrix() + Matrix([ + [a - d], + [b - c], + [b + c], + [a + d]]) + + This is equivalent to: + + >>> (q1 * q2).to_Matrix() + Matrix([ + [a - d], + [b - c], + [b + c], + [a + d]]) + """ + return Matrix([ + [self.a, -self.b, -self.c, -self.d], + [self.b, self.a, -self.d, self.c], + [self.c, self.d, self.a, -self.b], + [self.d, -self.c, self.b, self.a]]) + + @property + def product_matrix_right(self): + r"""Returns 4 x 4 Matrix equivalent to a Hamilton product from the + right. This can be useful when treating quaternion elements as column + vectors. Given a quaternion $q = a + bi + cj + dk$ where a, b, c and d + are real numbers, the product matrix from the left is: + + .. math:: + + M = \begin{bmatrix} a &-b &-c &-d \\ + b & a & d &-c \\ + c &-d & a & b \\ + d & c &-b & a \end{bmatrix} + + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy.abc import a, b, c, d + >>> q1 = Quaternion(a, b, c, d) + >>> q2 = Quaternion(1, 0, 0, 1) + >>> q2.product_matrix_right + Matrix([ + [1, 0, 0, -1], + [0, 1, 1, 0], + [0, -1, 1, 0], + [1, 0, 0, 1]]) + + Note the switched arguments: the matrix represents the quaternion on + the right, but is still considered as a matrix multiplication from the + left. + + >>> q2.product_matrix_right * q1.to_Matrix() + Matrix([ + [ a - d], + [ b + c], + [-b + c], + [ a + d]]) + + This is equivalent to: + + >>> (q1 * q2).to_Matrix() + Matrix([ + [ a - d], + [ b + c], + [-b + c], + [ a + d]]) + """ + return Matrix([ + [self.a, -self.b, -self.c, -self.d], + [self.b, self.a, self.d, -self.c], + [self.c, -self.d, self.a, self.b], + [self.d, self.c, -self.b, self.a]]) + + def to_Matrix(self, vector_only=False): + """Returns elements of quaternion as a column vector. + By default, a ``Matrix`` of length 4 is returned, with the real part as the + first element. + If ``vector_only`` is ``True``, returns only imaginary part as a Matrix of + length 3. + + Parameters + ========== + + vector_only : bool + If True, only imaginary part is returned. + Default value: False + + Returns + ======= + + Matrix + A column vector constructed by the elements of the quaternion. + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy.abc import a, b, c, d + >>> q = Quaternion(a, b, c, d) + >>> q + a + b*i + c*j + d*k + + >>> q.to_Matrix() + Matrix([ + [a], + [b], + [c], + [d]]) + + + >>> q.to_Matrix(vector_only=True) + Matrix([ + [b], + [c], + [d]]) + + """ + if vector_only: + return Matrix(self.args[1:]) + else: + return Matrix(self.args) + + @classmethod + def from_Matrix(cls, elements): + """Returns quaternion from elements of a column vector`. + If vector_only is True, returns only imaginary part as a Matrix of + length 3. + + Parameters + ========== + + elements : Matrix, list or tuple of length 3 or 4. If length is 3, + assume real part is zero. + Default value: False + + Returns + ======= + + Quaternion + A quaternion created from the input elements. + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy.abc import a, b, c, d + >>> q = Quaternion.from_Matrix([a, b, c, d]) + >>> q + a + b*i + c*j + d*k + + >>> q = Quaternion.from_Matrix([b, c, d]) + >>> q + 0 + b*i + c*j + d*k + + """ + length = len(elements) + if length != 3 and length != 4: + raise ValueError("Input elements must have length 3 or 4, got {} " + "elements".format(length)) + + if length == 3: + return Quaternion(0, *elements) + else: + return Quaternion(*elements) + + @classmethod + def from_euler(cls, angles, seq): + """Returns quaternion equivalent to rotation represented by the Euler + angles, in the sequence defined by ``seq``. + + Parameters + ========== + + angles : list, tuple or Matrix of 3 numbers + The Euler angles (in radians). + seq : string of length 3 + Represents the sequence of rotations. + For extrinsic rotations, seq must be all lowercase and its elements + must be from the set ``{'x', 'y', 'z'}`` + For intrinsic rotations, seq must be all uppercase and its elements + must be from the set ``{'X', 'Y', 'Z'}`` + + Returns + ======= + + Quaternion + The normalized rotation quaternion calculated from the Euler angles + in the given sequence. + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy import pi + >>> q = Quaternion.from_euler([pi/2, 0, 0], 'xyz') + >>> q + sqrt(2)/2 + sqrt(2)/2*i + 0*j + 0*k + + >>> q = Quaternion.from_euler([0, pi/2, pi] , 'zyz') + >>> q + 0 + (-sqrt(2)/2)*i + 0*j + sqrt(2)/2*k + + >>> q = Quaternion.from_euler([0, pi/2, pi] , 'ZYZ') + >>> q + 0 + sqrt(2)/2*i + 0*j + sqrt(2)/2*k + + """ + + if len(angles) != 3: + raise ValueError("3 angles must be given.") + + extrinsic = _is_extrinsic(seq) + i, j, k = seq.lower() + + # get elementary basis vectors + ei = [1 if n == i else 0 for n in 'xyz'] + ej = [1 if n == j else 0 for n in 'xyz'] + ek = [1 if n == k else 0 for n in 'xyz'] + + # calculate distinct quaternions + qi = cls.from_axis_angle(ei, angles[0]) + qj = cls.from_axis_angle(ej, angles[1]) + qk = cls.from_axis_angle(ek, angles[2]) + + if extrinsic: + return trigsimp(qk * qj * qi) + else: + return trigsimp(qi * qj * qk) + + def to_euler(self, seq, angle_addition=True, avoid_square_root=False): + r"""Returns Euler angles representing same rotation as the quaternion, + in the sequence given by ``seq``. This implements the method described + in [1]_. + + For degenerate cases (gymbal lock cases), the third angle is + set to zero. + + Parameters + ========== + + seq : string of length 3 + Represents the sequence of rotations. + For extrinsic rotations, seq must be all lowercase and its elements + must be from the set ``{'x', 'y', 'z'}`` + For intrinsic rotations, seq must be all uppercase and its elements + must be from the set ``{'X', 'Y', 'Z'}`` + + angle_addition : bool + When True, first and third angles are given as an addition and + subtraction of two simpler ``atan2`` expressions. When False, the + first and third angles are each given by a single more complicated + ``atan2`` expression. This equivalent expression is given by: + + .. math:: + + \operatorname{atan_2} (b,a) \pm \operatorname{atan_2} (d,c) = + \operatorname{atan_2} (bc\pm ad, ac\mp bd) + + Default value: True + + avoid_square_root : bool + When True, the second angle is calculated with an expression based + on ``acos``, which is slightly more complicated but avoids a square + root. When False, second angle is calculated with ``atan2``, which + is simpler and can be better for numerical reasons (some + numerical implementations of ``acos`` have problems near zero). + Default value: False + + + Returns + ======= + + Tuple + The Euler angles calculated from the quaternion + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy.abc import a, b, c, d + >>> euler = Quaternion(a, b, c, d).to_euler('zyz') + >>> euler + (-atan2(-b, c) + atan2(d, a), + 2*atan2(sqrt(b**2 + c**2), sqrt(a**2 + d**2)), + atan2(-b, c) + atan2(d, a)) + + + References + ========== + + .. [1] https://doi.org/10.1371/journal.pone.0276302 + + """ + if self.is_zero_quaternion(): + raise ValueError('Cannot convert a quaternion with norm 0.') + + angles = [0, 0, 0] + + extrinsic = _is_extrinsic(seq) + i, j, k = seq.lower() + + # get index corresponding to elementary basis vectors + i = 'xyz'.index(i) + 1 + j = 'xyz'.index(j) + 1 + k = 'xyz'.index(k) + 1 + + if not extrinsic: + i, k = k, i + + # check if sequence is symmetric + symmetric = i == k + if symmetric: + k = 6 - i - j + + # parity of the permutation + sign = (i - j) * (j - k) * (k - i) // 2 + + # permutate elements + elements = [self.a, self.b, self.c, self.d] + a = elements[0] + b = elements[i] + c = elements[j] + d = elements[k] * sign + + if not symmetric: + a, b, c, d = a - c, b + d, c + a, d - b + + if avoid_square_root: + if symmetric: + n2 = self.norm()**2 + angles[1] = acos((a * a + b * b - c * c - d * d) / n2) + else: + n2 = 2 * self.norm()**2 + angles[1] = asin((c * c + d * d - a * a - b * b) / n2) + else: + angles[1] = 2 * atan2(sqrt(c * c + d * d), sqrt(a * a + b * b)) + if not symmetric: + angles[1] -= S.Pi / 2 + + # Check for singularities in numerical cases + case = 0 + if is_eq(c, S.Zero) and is_eq(d, S.Zero): + case = 1 + if is_eq(a, S.Zero) and is_eq(b, S.Zero): + case = 2 + + if case == 0: + if angle_addition: + angles[0] = atan2(b, a) + atan2(d, c) + angles[2] = atan2(b, a) - atan2(d, c) + else: + angles[0] = atan2(b*c + a*d, a*c - b*d) + angles[2] = atan2(b*c - a*d, a*c + b*d) + + else: # any degenerate case + angles[2 * (not extrinsic)] = S.Zero + if case == 1: + angles[2 * extrinsic] = 2 * atan2(b, a) + else: + angles[2 * extrinsic] = 2 * atan2(d, c) + angles[2 * extrinsic] *= (-1 if extrinsic else 1) + + # for Tait-Bryan angles + if not symmetric: + angles[0] *= sign + + if extrinsic: + return tuple(angles[::-1]) + else: + return tuple(angles) + + @classmethod + def from_axis_angle(cls, vector, angle): + """Returns a rotation quaternion given the axis and the angle of rotation. + + Parameters + ========== + + vector : tuple of three numbers + The vector representation of the given axis. + angle : number + The angle by which axis is rotated (in radians). + + Returns + ======= + + Quaternion + The normalized rotation quaternion calculated from the given axis and the angle of rotation. + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy import pi, sqrt + >>> q = Quaternion.from_axis_angle((sqrt(3)/3, sqrt(3)/3, sqrt(3)/3), 2*pi/3) + >>> q + 1/2 + 1/2*i + 1/2*j + 1/2*k + + """ + (x, y, z) = vector + norm = sqrt(x**2 + y**2 + z**2) + (x, y, z) = (x / norm, y / norm, z / norm) + s = sin(angle * S.Half) + a = cos(angle * S.Half) + b = x * s + c = y * s + d = z * s + + # note that this quaternion is already normalized by construction: + # c^2 + (s*x)^2 + (s*y)^2 + (s*z)^2 = c^2 + s^2*(x^2 + y^2 + z^2) = c^2 + s^2 * 1 = c^2 + s^2 = 1 + # so, what we return is a normalized quaternion + + return cls(a, b, c, d) + + @classmethod + def from_rotation_matrix(cls, M): + """Returns the equivalent quaternion of a matrix. The quaternion will be normalized + only if the matrix is special orthogonal (orthogonal and det(M) = 1). + + Parameters + ========== + + M : Matrix + Input matrix to be converted to equivalent quaternion. M must be special + orthogonal (orthogonal and det(M) = 1) for the quaternion to be normalized. + + Returns + ======= + + Quaternion + The quaternion equivalent to given matrix. + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy import Matrix, symbols, cos, sin, trigsimp + >>> x = symbols('x') + >>> M = Matrix([[cos(x), -sin(x), 0], [sin(x), cos(x), 0], [0, 0, 1]]) + >>> q = trigsimp(Quaternion.from_rotation_matrix(M)) + >>> q + sqrt(2)*sqrt(cos(x) + 1)/2 + 0*i + 0*j + sqrt(2 - 2*cos(x))*sign(sin(x))/2*k + + """ + + absQ = M.det()**Rational(1, 3) + + a = sqrt(absQ + M[0, 0] + M[1, 1] + M[2, 2]) / 2 + b = sqrt(absQ + M[0, 0] - M[1, 1] - M[2, 2]) / 2 + c = sqrt(absQ - M[0, 0] + M[1, 1] - M[2, 2]) / 2 + d = sqrt(absQ - M[0, 0] - M[1, 1] + M[2, 2]) / 2 + + b = b * sign(M[2, 1] - M[1, 2]) + c = c * sign(M[0, 2] - M[2, 0]) + d = d * sign(M[1, 0] - M[0, 1]) + + return Quaternion(a, b, c, d) + + def __add__(self, other): + return self.add(other) + + def __radd__(self, other): + return self.add(other) + + def __sub__(self, other): + return self.add(other*-1) + + def __mul__(self, other): + return self._generic_mul(self, _sympify(other)) + + def __rmul__(self, other): + return self._generic_mul(_sympify(other), self) + + def __pow__(self, p): + return self.pow(p) + + def __neg__(self): + return Quaternion(-self.a, -self.b, -self.c, -self.d) + + def __truediv__(self, other): + return self * sympify(other)**-1 + + def __rtruediv__(self, other): + return sympify(other) * self**-1 + + def _eval_Integral(self, *args): + return self.integrate(*args) + + def diff(self, *symbols, **kwargs): + kwargs.setdefault('evaluate', True) + return self.func(*[a.diff(*symbols, **kwargs) for a in self.args]) + + def add(self, other): + """Adds quaternions. + + Parameters + ========== + + other : Quaternion + The quaternion to add to current (self) quaternion. + + Returns + ======= + + Quaternion + The resultant quaternion after adding self to other + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy import symbols + >>> q1 = Quaternion(1, 2, 3, 4) + >>> q2 = Quaternion(5, 6, 7, 8) + >>> q1.add(q2) + 6 + 8*i + 10*j + 12*k + >>> q1 + 5 + 6 + 2*i + 3*j + 4*k + >>> x = symbols('x', real = True) + >>> q1.add(x) + (x + 1) + 2*i + 3*j + 4*k + + Quaternions over complex fields : + + >>> from sympy import Quaternion + >>> from sympy import I + >>> q3 = Quaternion(3 + 4*I, 2 + 5*I, 0, 7 + 8*I, real_field = False) + >>> q3.add(2 + 3*I) + (5 + 7*I) + (2 + 5*I)*i + 0*j + (7 + 8*I)*k + + """ + q1 = self + q2 = sympify(other) + + # If q2 is a number or a SymPy expression instead of a quaternion + if not isinstance(q2, Quaternion): + if q1.real_field and q2.is_complex: + return Quaternion(re(q2) + q1.a, im(q2) + q1.b, q1.c, q1.d) + elif q2.is_commutative: + return Quaternion(q1.a + q2, q1.b, q1.c, q1.d) + else: + raise ValueError("Only commutative expressions can be added with a Quaternion.") + + return Quaternion(q1.a + q2.a, q1.b + q2.b, q1.c + q2.c, q1.d + + q2.d) + + def mul(self, other): + """Multiplies quaternions. + + Parameters + ========== + + other : Quaternion or symbol + The quaternion to multiply to current (self) quaternion. + + Returns + ======= + + Quaternion + The resultant quaternion after multiplying self with other + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy import symbols + >>> q1 = Quaternion(1, 2, 3, 4) + >>> q2 = Quaternion(5, 6, 7, 8) + >>> q1.mul(q2) + (-60) + 12*i + 30*j + 24*k + >>> q1.mul(2) + 2 + 4*i + 6*j + 8*k + >>> x = symbols('x', real = True) + >>> q1.mul(x) + x + 2*x*i + 3*x*j + 4*x*k + + Quaternions over complex fields : + + >>> from sympy import Quaternion + >>> from sympy import I + >>> q3 = Quaternion(3 + 4*I, 2 + 5*I, 0, 7 + 8*I, real_field = False) + >>> q3.mul(2 + 3*I) + (2 + 3*I)*(3 + 4*I) + (2 + 3*I)*(2 + 5*I)*i + 0*j + (2 + 3*I)*(7 + 8*I)*k + + """ + return self._generic_mul(self, _sympify(other)) + + @staticmethod + def _generic_mul(q1, q2): + """Generic multiplication. + + Parameters + ========== + + q1 : Quaternion or symbol + q2 : Quaternion or symbol + + It is important to note that if neither q1 nor q2 is a Quaternion, + this function simply returns q1 * q2. + + Returns + ======= + + Quaternion + The resultant quaternion after multiplying q1 and q2 + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy import Symbol, S + >>> q1 = Quaternion(1, 2, 3, 4) + >>> q2 = Quaternion(5, 6, 7, 8) + >>> Quaternion._generic_mul(q1, q2) + (-60) + 12*i + 30*j + 24*k + >>> Quaternion._generic_mul(q1, S(2)) + 2 + 4*i + 6*j + 8*k + >>> x = Symbol('x', real = True) + >>> Quaternion._generic_mul(q1, x) + x + 2*x*i + 3*x*j + 4*x*k + + Quaternions over complex fields : + + >>> from sympy import I + >>> q3 = Quaternion(3 + 4*I, 2 + 5*I, 0, 7 + 8*I, real_field = False) + >>> Quaternion._generic_mul(q3, 2 + 3*I) + (2 + 3*I)*(3 + 4*I) + (2 + 3*I)*(2 + 5*I)*i + 0*j + (2 + 3*I)*(7 + 8*I)*k + + """ + # None is a Quaternion: + if not isinstance(q1, Quaternion) and not isinstance(q2, Quaternion): + return q1 * q2 + + # If q1 is a number or a SymPy expression instead of a quaternion + if not isinstance(q1, Quaternion): + if q2.real_field and q1.is_complex: + return Quaternion(re(q1), im(q1), 0, 0) * q2 + elif q1.is_commutative: + return Quaternion(q1 * q2.a, q1 * q2.b, q1 * q2.c, q1 * q2.d) + else: + raise ValueError("Only commutative expressions can be multiplied with a Quaternion.") + + # If q2 is a number or a SymPy expression instead of a quaternion + if not isinstance(q2, Quaternion): + if q1.real_field and q2.is_complex: + return q1 * Quaternion(re(q2), im(q2), 0, 0) + elif q2.is_commutative: + return Quaternion(q2 * q1.a, q2 * q1.b, q2 * q1.c, q2 * q1.d) + else: + raise ValueError("Only commutative expressions can be multiplied with a Quaternion.") + + # If any of the quaternions has a fixed norm, pre-compute norm + if q1._norm is None and q2._norm is None: + norm = None + else: + norm = q1.norm() * q2.norm() + + return Quaternion(-q1.b*q2.b - q1.c*q2.c - q1.d*q2.d + q1.a*q2.a, + q1.b*q2.a + q1.c*q2.d - q1.d*q2.c + q1.a*q2.b, + -q1.b*q2.d + q1.c*q2.a + q1.d*q2.b + q1.a*q2.c, + q1.b*q2.c - q1.c*q2.b + q1.d*q2.a + q1.a * q2.d, + norm=norm) + + def _eval_conjugate(self): + """Returns the conjugate of the quaternion.""" + q = self + return Quaternion(q.a, -q.b, -q.c, -q.d, norm=q._norm) + + def norm(self): + """Returns the norm of the quaternion.""" + if self._norm is None: # check if norm is pre-defined + q = self + # trigsimp is used to simplify sin(x)^2 + cos(x)^2 (these terms + # arise when from_axis_angle is used). + return sqrt(trigsimp(q.a**2 + q.b**2 + q.c**2 + q.d**2)) + + return self._norm + + def normalize(self): + """Returns the normalized form of the quaternion.""" + q = self + return q * (1/q.norm()) + + def inverse(self): + """Returns the inverse of the quaternion.""" + q = self + if not q.norm(): + raise ValueError("Cannot compute inverse for a quaternion with zero norm") + return conjugate(q) * (1/q.norm()**2) + + def pow(self, p): + """Finds the pth power of the quaternion. + + Parameters + ========== + + p : int + Power to be applied on quaternion. + + Returns + ======= + + Quaternion + Returns the p-th power of the current quaternion. + Returns the inverse if p = -1. + + Examples + ======== + + >>> from sympy import Quaternion + >>> q = Quaternion(1, 2, 3, 4) + >>> q.pow(4) + 668 + (-224)*i + (-336)*j + (-448)*k + + """ + try: + q, p = self, as_int(p) + except ValueError: + return NotImplemented + + if p < 0: + q, p = q.inverse(), -p + + if p == 1: + return q + + res = Quaternion(1, 0, 0, 0) + while p > 0: + if p & 1: + res *= q + q *= q + p >>= 1 + + return res + + def exp(self): + """Returns the exponential of $q$, given by $e^q$. + + Returns + ======= + + Quaternion + The exponential of the quaternion. + + Examples + ======== + + >>> from sympy import Quaternion + >>> q = Quaternion(1, 2, 3, 4) + >>> q.exp() + E*cos(sqrt(29)) + + 2*sqrt(29)*E*sin(sqrt(29))/29*i + + 3*sqrt(29)*E*sin(sqrt(29))/29*j + + 4*sqrt(29)*E*sin(sqrt(29))/29*k + + """ + # exp(q) = e^a(cos||v|| + v/||v||*sin||v||) + q = self + vector_norm = sqrt(q.b**2 + q.c**2 + q.d**2) + a = exp(q.a) * cos(vector_norm) + b = exp(q.a) * sin(vector_norm) * q.b / vector_norm + c = exp(q.a) * sin(vector_norm) * q.c / vector_norm + d = exp(q.a) * sin(vector_norm) * q.d / vector_norm + + return Quaternion(a, b, c, d) + + def log(self): + r"""Returns the logarithm of the quaternion, given by $\log q$. + + Examples + ======== + + >>> from sympy import Quaternion + >>> q = Quaternion(1, 2, 3, 4) + >>> q.log() + log(sqrt(30)) + + 2*sqrt(29)*acos(sqrt(30)/30)/29*i + + 3*sqrt(29)*acos(sqrt(30)/30)/29*j + + 4*sqrt(29)*acos(sqrt(30)/30)/29*k + + """ + # log(q) = log||q|| + v/||v||*arccos(a/||q||) + q = self + vector_norm = sqrt(q.b**2 + q.c**2 + q.d**2) + q_norm = q.norm() + a = ln(q_norm) + b = q.b * acos(q.a / q_norm) / vector_norm + c = q.c * acos(q.a / q_norm) / vector_norm + d = q.d * acos(q.a / q_norm) / vector_norm + + return Quaternion(a, b, c, d) + + def _eval_subs(self, *args): + elements = [i.subs(*args) for i in self.args] + norm = self._norm + if norm is not None: + norm = norm.subs(*args) + _check_norm(elements, norm) + return Quaternion(*elements, norm=norm) + + def _eval_evalf(self, prec): + """Returns the floating point approximations (decimal numbers) of the quaternion. + + Returns + ======= + + Quaternion + Floating point approximations of quaternion(self) + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy import sqrt + >>> q = Quaternion(1/sqrt(1), 1/sqrt(2), 1/sqrt(3), 1/sqrt(4)) + >>> q.evalf() + 1.00000000000000 + + 0.707106781186547*i + + 0.577350269189626*j + + 0.500000000000000*k + + """ + nprec = prec_to_dps(prec) + return Quaternion(*[arg.evalf(n=nprec) for arg in self.args]) + + def pow_cos_sin(self, p): + """Computes the pth power in the cos-sin form. + + Parameters + ========== + + p : int + Power to be applied on quaternion. + + Returns + ======= + + Quaternion + The p-th power in the cos-sin form. + + Examples + ======== + + >>> from sympy import Quaternion + >>> q = Quaternion(1, 2, 3, 4) + >>> q.pow_cos_sin(4) + 900*cos(4*acos(sqrt(30)/30)) + + 1800*sqrt(29)*sin(4*acos(sqrt(30)/30))/29*i + + 2700*sqrt(29)*sin(4*acos(sqrt(30)/30))/29*j + + 3600*sqrt(29)*sin(4*acos(sqrt(30)/30))/29*k + + """ + # q = ||q||*(cos(a) + u*sin(a)) + # q^p = ||q||^p * (cos(p*a) + u*sin(p*a)) + + q = self + (v, angle) = q.to_axis_angle() + q2 = Quaternion.from_axis_angle(v, p * angle) + return q2 * (q.norm()**p) + + def integrate(self, *args): + """Computes integration of quaternion. + + Returns + ======= + + Quaternion + Integration of the quaternion(self) with the given variable. + + Examples + ======== + + Indefinite Integral of quaternion : + + >>> from sympy import Quaternion + >>> from sympy.abc import x + >>> q = Quaternion(1, 2, 3, 4) + >>> q.integrate(x) + x + 2*x*i + 3*x*j + 4*x*k + + Definite integral of quaternion : + + >>> from sympy import Quaternion + >>> from sympy.abc import x + >>> q = Quaternion(1, 2, 3, 4) + >>> q.integrate((x, 1, 5)) + 4 + 8*i + 12*j + 16*k + + """ + return Quaternion(integrate(self.a, *args), integrate(self.b, *args), + integrate(self.c, *args), integrate(self.d, *args)) + + @staticmethod + def rotate_point(pin, r): + """Returns the coordinates of the point pin (a 3 tuple) after rotation. + + Parameters + ========== + + pin : tuple + A 3-element tuple of coordinates of a point which needs to be + rotated. + r : Quaternion or tuple + Axis and angle of rotation. + + It's important to note that when r is a tuple, it must be of the form + (axis, angle) + + Returns + ======= + + tuple + The coordinates of the point after rotation. + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy import symbols, trigsimp, cos, sin + >>> x = symbols('x') + >>> q = Quaternion(cos(x/2), 0, 0, sin(x/2)) + >>> trigsimp(Quaternion.rotate_point((1, 1, 1), q)) + (sqrt(2)*cos(x + pi/4), sqrt(2)*sin(x + pi/4), 1) + >>> (axis, angle) = q.to_axis_angle() + >>> trigsimp(Quaternion.rotate_point((1, 1, 1), (axis, angle))) + (sqrt(2)*cos(x + pi/4), sqrt(2)*sin(x + pi/4), 1) + + """ + if isinstance(r, tuple): + # if r is of the form (vector, angle) + q = Quaternion.from_axis_angle(r[0], r[1]) + else: + # if r is a quaternion + q = r.normalize() + pout = q * Quaternion(0, pin[0], pin[1], pin[2]) * conjugate(q) + return (pout.b, pout.c, pout.d) + + def to_axis_angle(self): + """Returns the axis and angle of rotation of a quaternion. + + Returns + ======= + + tuple + Tuple of (axis, angle) + + Examples + ======== + + >>> from sympy import Quaternion + >>> q = Quaternion(1, 1, 1, 1) + >>> (axis, angle) = q.to_axis_angle() + >>> axis + (sqrt(3)/3, sqrt(3)/3, sqrt(3)/3) + >>> angle + 2*pi/3 + + """ + q = self + if q.a.is_negative: + q = q * -1 + + q = q.normalize() + angle = trigsimp(2 * acos(q.a)) + + # Since quaternion is normalised, q.a is less than 1. + s = sqrt(1 - q.a*q.a) + + x = trigsimp(q.b / s) + y = trigsimp(q.c / s) + z = trigsimp(q.d / s) + + v = (x, y, z) + t = (v, angle) + + return t + + def to_rotation_matrix(self, v=None, homogeneous=True): + """Returns the equivalent rotation transformation matrix of the quaternion + which represents rotation about the origin if ``v`` is not passed. + + Parameters + ========== + + v : tuple or None + Default value: None + homogeneous : bool + When True, gives an expression that may be more efficient for + symbolic calculations but less so for direct evaluation. Both + formulas are mathematically equivalent. + Default value: True + + Returns + ======= + + tuple + Returns the equivalent rotation transformation matrix of the quaternion + which represents rotation about the origin if v is not passed. + + Examples + ======== + + >>> from sympy import Quaternion + >>> from sympy import symbols, trigsimp, cos, sin + >>> x = symbols('x') + >>> q = Quaternion(cos(x/2), 0, 0, sin(x/2)) + >>> trigsimp(q.to_rotation_matrix()) + Matrix([ + [cos(x), -sin(x), 0], + [sin(x), cos(x), 0], + [ 0, 0, 1]]) + + Generates a 4x4 transformation matrix (used for rotation about a point + other than the origin) if the point(v) is passed as an argument. + """ + + q = self + s = q.norm()**-2 + + # diagonal elements are different according to parameter normal + if homogeneous: + m00 = s*(q.a**2 + q.b**2 - q.c**2 - q.d**2) + m11 = s*(q.a**2 - q.b**2 + q.c**2 - q.d**2) + m22 = s*(q.a**2 - q.b**2 - q.c**2 + q.d**2) + else: + m00 = 1 - 2*s*(q.c**2 + q.d**2) + m11 = 1 - 2*s*(q.b**2 + q.d**2) + m22 = 1 - 2*s*(q.b**2 + q.c**2) + + m01 = 2*s*(q.b*q.c - q.d*q.a) + m02 = 2*s*(q.b*q.d + q.c*q.a) + + m10 = 2*s*(q.b*q.c + q.d*q.a) + m12 = 2*s*(q.c*q.d - q.b*q.a) + + m20 = 2*s*(q.b*q.d - q.c*q.a) + m21 = 2*s*(q.c*q.d + q.b*q.a) + + if not v: + return Matrix([[m00, m01, m02], [m10, m11, m12], [m20, m21, m22]]) + + else: + (x, y, z) = v + + m03 = x - x*m00 - y*m01 - z*m02 + m13 = y - x*m10 - y*m11 - z*m12 + m23 = z - x*m20 - y*m21 - z*m22 + m30 = m31 = m32 = 0 + m33 = 1 + + return Matrix([[m00, m01, m02, m03], [m10, m11, m12, m13], + [m20, m21, m22, m23], [m30, m31, m32, m33]]) + + def scalar_part(self): + r"""Returns scalar part($\mathbf{S}(q)$) of the quaternion q. + + Explanation + =========== + + Given a quaternion $q = a + bi + cj + dk$, returns $\mathbf{S}(q) = a$. + + Examples + ======== + + >>> from sympy.algebras.quaternion import Quaternion + >>> q = Quaternion(4, 8, 13, 12) + >>> q.scalar_part() + 4 + + """ + + return self.a + + def vector_part(self): + r""" + Returns $\mathbf{V}(q)$, the vector part of the quaternion $q$. + + Explanation + =========== + + Given a quaternion $q = a + bi + cj + dk$, returns $\mathbf{V}(q) = bi + cj + dk$. + + Examples + ======== + + >>> from sympy.algebras.quaternion import Quaternion + >>> q = Quaternion(1, 1, 1, 1) + >>> q.vector_part() + 0 + 1*i + 1*j + 1*k + + >>> q = Quaternion(4, 8, 13, 12) + >>> q.vector_part() + 0 + 8*i + 13*j + 12*k + + """ + + return Quaternion(0, self.b, self.c, self.d) + + def axis(self): + r""" + Returns $\mathbf{Ax}(q)$, the axis of the quaternion $q$. + + Explanation + =========== + + Given a quaternion $q = a + bi + cj + dk$, returns $\mathbf{Ax}(q)$ i.e., the versor of the vector part of that quaternion + equal to $\mathbf{U}[\mathbf{V}(q)]$. + The axis is always an imaginary unit with square equal to $-1 + 0i + 0j + 0k$. + + Examples + ======== + + >>> from sympy.algebras.quaternion import Quaternion + >>> q = Quaternion(1, 1, 1, 1) + >>> q.axis() + 0 + sqrt(3)/3*i + sqrt(3)/3*j + sqrt(3)/3*k + + See Also + ======== + + vector_part + + """ + axis = self.vector_part().normalize() + + return Quaternion(0, axis.b, axis.c, axis.d) + + def is_pure(self): + """ + Returns true if the quaternion is pure, false if the quaternion is not pure + or returns none if it is unknown. + + Explanation + =========== + + A pure quaternion (also a vector quaternion) is a quaternion with scalar + part equal to 0. + + Examples + ======== + + >>> from sympy.algebras.quaternion import Quaternion + >>> q = Quaternion(0, 8, 13, 12) + >>> q.is_pure() + True + + See Also + ======== + scalar_part + + """ + + return self.a.is_zero + + def is_zero_quaternion(self): + """ + Returns true if the quaternion is a zero quaternion or false if it is not a zero quaternion + and None if the value is unknown. + + Explanation + =========== + + A zero quaternion is a quaternion with both scalar part and + vector part equal to 0. + + Examples + ======== + + >>> from sympy.algebras.quaternion import Quaternion + >>> q = Quaternion(1, 0, 0, 0) + >>> q.is_zero_quaternion() + False + + >>> q = Quaternion(0, 0, 0, 0) + >>> q.is_zero_quaternion() + True + + See Also + ======== + scalar_part + vector_part + + """ + + return self.norm().is_zero + + def angle(self): + r""" + Returns the angle of the quaternion measured in the real-axis plane. + + Explanation + =========== + + Given a quaternion $q = a + bi + cj + dk$ where $a$, $b$, $c$ and $d$ + are real numbers, returns the angle of the quaternion given by + + .. math:: + \theta := 2 \operatorname{atan_2}\left(\sqrt{b^2 + c^2 + d^2}, {a}\right) + + Examples + ======== + + >>> from sympy.algebras.quaternion import Quaternion + >>> q = Quaternion(1, 4, 4, 4) + >>> q.angle() + 2*atan(4*sqrt(3)) + + """ + + return 2 * atan2(self.vector_part().norm(), self.scalar_part()) + + + def arc_coplanar(self, other): + """ + Returns True if the transformation arcs represented by the input quaternions happen in the same plane. + + Explanation + =========== + + Two quaternions are said to be coplanar (in this arc sense) when their axes are parallel. + The plane of a quaternion is the one normal to its axis. + + Parameters + ========== + + other : a Quaternion + + Returns + ======= + + True : if the planes of the two quaternions are the same, apart from its orientation/sign. + False : if the planes of the two quaternions are not the same, apart from its orientation/sign. + None : if plane of either of the quaternion is unknown. + + Examples + ======== + + >>> from sympy.algebras.quaternion import Quaternion + >>> q1 = Quaternion(1, 4, 4, 4) + >>> q2 = Quaternion(3, 8, 8, 8) + >>> Quaternion.arc_coplanar(q1, q2) + True + + >>> q1 = Quaternion(2, 8, 13, 12) + >>> Quaternion.arc_coplanar(q1, q2) + False + + See Also + ======== + + vector_coplanar + is_pure + + """ + if (self.is_zero_quaternion()) or (other.is_zero_quaternion()): + raise ValueError('Neither of the given quaternions can be 0') + + return fuzzy_or([(self.axis() - other.axis()).is_zero_quaternion(), (self.axis() + other.axis()).is_zero_quaternion()]) + + @classmethod + def vector_coplanar(cls, q1, q2, q3): + r""" + Returns True if the axis of the pure quaternions seen as 3D vectors + ``q1``, ``q2``, and ``q3`` are coplanar. + + Explanation + =========== + + Three pure quaternions are vector coplanar if the quaternions seen as 3D vectors are coplanar. + + Parameters + ========== + + q1 + A pure Quaternion. + q2 + A pure Quaternion. + q3 + A pure Quaternion. + + Returns + ======= + + True : if the axis of the pure quaternions seen as 3D vectors + q1, q2, and q3 are coplanar. + False : if the axis of the pure quaternions seen as 3D vectors + q1, q2, and q3 are not coplanar. + None : if the axis of the pure quaternions seen as 3D vectors + q1, q2, and q3 are coplanar is unknown. + + Examples + ======== + + >>> from sympy.algebras.quaternion import Quaternion + >>> q1 = Quaternion(0, 4, 4, 4) + >>> q2 = Quaternion(0, 8, 8, 8) + >>> q3 = Quaternion(0, 24, 24, 24) + >>> Quaternion.vector_coplanar(q1, q2, q3) + True + + >>> q1 = Quaternion(0, 8, 16, 8) + >>> q2 = Quaternion(0, 8, 3, 12) + >>> Quaternion.vector_coplanar(q1, q2, q3) + False + + See Also + ======== + + axis + is_pure + + """ + + if fuzzy_not(q1.is_pure()) or fuzzy_not(q2.is_pure()) or fuzzy_not(q3.is_pure()): + raise ValueError('The given quaternions must be pure') + + M = Matrix([[q1.b, q1.c, q1.d], [q2.b, q2.c, q2.d], [q3.b, q3.c, q3.d]]).det() + return M.is_zero + + def parallel(self, other): + """ + Returns True if the two pure quaternions seen as 3D vectors are parallel. + + Explanation + =========== + + Two pure quaternions are called parallel when their vector product is commutative which + implies that the quaternions seen as 3D vectors have same direction. + + Parameters + ========== + + other : a Quaternion + + Returns + ======= + + True : if the two pure quaternions seen as 3D vectors are parallel. + False : if the two pure quaternions seen as 3D vectors are not parallel. + None : if the two pure quaternions seen as 3D vectors are parallel is unknown. + + Examples + ======== + + >>> from sympy.algebras.quaternion import Quaternion + >>> q = Quaternion(0, 4, 4, 4) + >>> q1 = Quaternion(0, 8, 8, 8) + >>> q.parallel(q1) + True + + >>> q1 = Quaternion(0, 8, 13, 12) + >>> q.parallel(q1) + False + + """ + + if fuzzy_not(self.is_pure()) or fuzzy_not(other.is_pure()): + raise ValueError('The provided quaternions must be pure') + + return (self*other - other*self).is_zero_quaternion() + + def orthogonal(self, other): + """ + Returns the orthogonality of two quaternions. + + Explanation + =========== + + Two pure quaternions are called orthogonal when their product is anti-commutative. + + Parameters + ========== + + other : a Quaternion + + Returns + ======= + + True : if the two pure quaternions seen as 3D vectors are orthogonal. + False : if the two pure quaternions seen as 3D vectors are not orthogonal. + None : if the two pure quaternions seen as 3D vectors are orthogonal is unknown. + + Examples + ======== + + >>> from sympy.algebras.quaternion import Quaternion + >>> q = Quaternion(0, 4, 4, 4) + >>> q1 = Quaternion(0, 8, 8, 8) + >>> q.orthogonal(q1) + False + + >>> q1 = Quaternion(0, 2, 2, 0) + >>> q = Quaternion(0, 2, -2, 0) + >>> q.orthogonal(q1) + True + + """ + + if fuzzy_not(self.is_pure()) or fuzzy_not(other.is_pure()): + raise ValueError('The given quaternions must be pure') + + return (self*other + other*self).is_zero_quaternion() + + def index_vector(self): + r""" + Returns the index vector of the quaternion. + + Explanation + =========== + + The index vector is given by $\mathbf{T}(q)$, the norm (or magnitude) of + the quaternion $q$, multiplied by $\mathbf{Ax}(q)$, the axis of $q$. + + Returns + ======= + + Quaternion: representing index vector of the provided quaternion. + + Examples + ======== + + >>> from sympy.algebras.quaternion import Quaternion + >>> q = Quaternion(2, 4, 2, 4) + >>> q.index_vector() + 0 + 4*sqrt(10)/3*i + 2*sqrt(10)/3*j + 4*sqrt(10)/3*k + + See Also + ======== + + axis + norm + + """ + + return self.norm() * self.axis() + + def mensor(self): + """ + Returns the natural logarithm of the norm(magnitude) of the quaternion. + + Examples + ======== + + >>> from sympy.algebras.quaternion import Quaternion + >>> q = Quaternion(2, 4, 2, 4) + >>> q.mensor() + log(2*sqrt(10)) + >>> q.norm() + 2*sqrt(10) + + See Also + ======== + + norm + + """ + + return ln(self.norm()) diff --git a/venv/lib/python3.11/site-packages/sympy/algebras/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/algebras/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/algebras/tests/test_quaternion.py b/venv/lib/python3.11/site-packages/sympy/algebras/tests/test_quaternion.py new file mode 100644 index 0000000000000000000000000000000000000000..a4331cd6afa05c96e8e11d59df4b7520e4810930 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/algebras/tests/test_quaternion.py @@ -0,0 +1,437 @@ +from sympy.testing.pytest import slow +from sympy.core.function import diff +from sympy.core.function import expand +from sympy.core.numbers import (E, I, Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import (Abs, conjugate, im, re, sign) +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (acos, asin, cos, sin, atan2, atan) +from sympy.integrals.integrals import integrate +from sympy.matrices.dense import Matrix +from sympy.simplify import simplify +from sympy.simplify.trigsimp import trigsimp +from sympy.algebras.quaternion import Quaternion +from sympy.testing.pytest import raises +import math +from itertools import permutations, product + +w, x, y, z = symbols('w:z') +phi = symbols('phi') + +def test_quaternion_construction(): + q = Quaternion(w, x, y, z) + assert q + q == Quaternion(2*w, 2*x, 2*y, 2*z) + + q2 = Quaternion.from_axis_angle((sqrt(3)/3, sqrt(3)/3, sqrt(3)/3), + pi*Rational(2, 3)) + assert q2 == Quaternion(S.Half, S.Half, + S.Half, S.Half) + + M = Matrix([[cos(phi), -sin(phi), 0], [sin(phi), cos(phi), 0], [0, 0, 1]]) + q3 = trigsimp(Quaternion.from_rotation_matrix(M)) + assert q3 == Quaternion( + sqrt(2)*sqrt(cos(phi) + 1)/2, 0, 0, sqrt(2 - 2*cos(phi))*sign(sin(phi))/2) + + nc = Symbol('nc', commutative=False) + raises(ValueError, lambda: Quaternion(w, x, nc, z)) + + +def test_quaternion_construction_norm(): + q1 = Quaternion(*symbols('a:d')) + + q2 = Quaternion(w, x, y, z) + assert expand((q1*q2).norm()**2 - (q1.norm()**2 * q2.norm()**2)) == 0 + + q3 = Quaternion(w, x, y, z, norm=1) + assert (q1 * q3).norm() == q1.norm() + + +def test_issue_25254(): + # calculating the inverse cached the norm which caused problems + # when multiplying + p = Quaternion(1, 0, 0, 0) + q = Quaternion.from_axis_angle((1, 1, 1), 3 * math.pi/4) + qi = q.inverse() # this operation cached the norm + test = q * p * qi + assert ((test - p).norm() < 1E-10) + + +def test_to_and_from_Matrix(): + q = Quaternion(w, x, y, z) + q_full = Quaternion.from_Matrix(q.to_Matrix()) + q_vect = Quaternion.from_Matrix(q.to_Matrix(True)) + assert (q - q_full).is_zero_quaternion() + assert (q.vector_part() - q_vect).is_zero_quaternion() + + +def test_product_matrices(): + q1 = Quaternion(w, x, y, z) + q2 = Quaternion(*(symbols("a:d"))) + assert (q1 * q2).to_Matrix() == q1.product_matrix_left * q2.to_Matrix() + assert (q1 * q2).to_Matrix() == q2.product_matrix_right * q1.to_Matrix() + + R1 = (q1.product_matrix_left * q1.product_matrix_right.T)[1:, 1:] + R2 = simplify(q1.to_rotation_matrix()*q1.norm()**2) + assert R1 == R2 + + +def test_quaternion_axis_angle(): + + test_data = [ # axis, angle, expected_quaternion + ((1, 0, 0), 0, (1, 0, 0, 0)), + ((1, 0, 0), pi/2, (sqrt(2)/2, sqrt(2)/2, 0, 0)), + ((0, 1, 0), pi/2, (sqrt(2)/2, 0, sqrt(2)/2, 0)), + ((0, 0, 1), pi/2, (sqrt(2)/2, 0, 0, sqrt(2)/2)), + ((1, 0, 0), pi, (0, 1, 0, 0)), + ((0, 1, 0), pi, (0, 0, 1, 0)), + ((0, 0, 1), pi, (0, 0, 0, 1)), + ((1, 1, 1), pi, (0, 1/sqrt(3),1/sqrt(3),1/sqrt(3))), + ((sqrt(3)/3, sqrt(3)/3, sqrt(3)/3), pi*2/3, (S.Half, S.Half, S.Half, S.Half)) + ] + + for axis, angle, expected in test_data: + assert Quaternion.from_axis_angle(axis, angle) == Quaternion(*expected) + + +def test_quaternion_axis_angle_simplification(): + result = Quaternion.from_axis_angle((1, 2, 3), asin(4)) + assert result.a == cos(asin(4)/2) + assert result.b == sqrt(14)*sin(asin(4)/2)/14 + assert result.c == sqrt(14)*sin(asin(4)/2)/7 + assert result.d == 3*sqrt(14)*sin(asin(4)/2)/14 + +def test_quaternion_complex_real_addition(): + a = symbols("a", complex=True) + b = symbols("b", real=True) + # This symbol is not complex: + c = symbols("c", commutative=False) + + q = Quaternion(w, x, y, z) + assert a + q == Quaternion(w + re(a), x + im(a), y, z) + assert 1 + q == Quaternion(1 + w, x, y, z) + assert I + q == Quaternion(w, 1 + x, y, z) + assert b + q == Quaternion(w + b, x, y, z) + raises(ValueError, lambda: c + q) + raises(ValueError, lambda: q * c) + raises(ValueError, lambda: c * q) + + assert -q == Quaternion(-w, -x, -y, -z) + + q1 = Quaternion(3 + 4*I, 2 + 5*I, 0, 7 + 8*I, real_field = False) + q2 = Quaternion(1, 4, 7, 8) + + assert q1 + (2 + 3*I) == Quaternion(5 + 7*I, 2 + 5*I, 0, 7 + 8*I) + assert q2 + (2 + 3*I) == Quaternion(3, 7, 7, 8) + assert q1 * (2 + 3*I) == \ + Quaternion((2 + 3*I)*(3 + 4*I), (2 + 3*I)*(2 + 5*I), 0, (2 + 3*I)*(7 + 8*I)) + assert q2 * (2 + 3*I) == Quaternion(-10, 11, 38, -5) + + q1 = Quaternion(1, 2, 3, 4) + q0 = Quaternion(0, 0, 0, 0) + assert q1 + q0 == q1 + assert q1 - q0 == q1 + assert q1 - q1 == q0 + + +def test_quaternion_subs(): + q = Quaternion.from_axis_angle((0, 0, 1), phi) + assert q.subs(phi, 0) == Quaternion(1, 0, 0, 0) + + +def test_quaternion_evalf(): + assert (Quaternion(sqrt(2), 0, 0, sqrt(3)).evalf() == + Quaternion(sqrt(2).evalf(), 0, 0, sqrt(3).evalf())) + assert (Quaternion(1/sqrt(2), 0, 0, 1/sqrt(2)).evalf() == + Quaternion((1/sqrt(2)).evalf(), 0, 0, (1/sqrt(2)).evalf())) + + +def test_quaternion_functions(): + q = Quaternion(w, x, y, z) + q1 = Quaternion(1, 2, 3, 4) + q0 = Quaternion(0, 0, 0, 0) + + assert conjugate(q) == Quaternion(w, -x, -y, -z) + assert q.norm() == sqrt(w**2 + x**2 + y**2 + z**2) + assert q.normalize() == Quaternion(w, x, y, z) / sqrt(w**2 + x**2 + y**2 + z**2) + assert q.inverse() == Quaternion(w, -x, -y, -z) / (w**2 + x**2 + y**2 + z**2) + assert q.inverse() == q.pow(-1) + raises(ValueError, lambda: q0.inverse()) + assert q.pow(2) == Quaternion(w**2 - x**2 - y**2 - z**2, 2*w*x, 2*w*y, 2*w*z) + assert q**(2) == Quaternion(w**2 - x**2 - y**2 - z**2, 2*w*x, 2*w*y, 2*w*z) + assert q1.pow(-2) == Quaternion( + Rational(-7, 225), Rational(-1, 225), Rational(-1, 150), Rational(-2, 225)) + assert q1**(-2) == Quaternion( + Rational(-7, 225), Rational(-1, 225), Rational(-1, 150), Rational(-2, 225)) + assert q1.pow(-0.5) == NotImplemented + raises(TypeError, lambda: q1**(-0.5)) + + assert q1.exp() == \ + Quaternion(E * cos(sqrt(29)), + 2 * sqrt(29) * E * sin(sqrt(29)) / 29, + 3 * sqrt(29) * E * sin(sqrt(29)) / 29, + 4 * sqrt(29) * E * sin(sqrt(29)) / 29) + assert q1.log() == \ + Quaternion(log(sqrt(30)), + 2 * sqrt(29) * acos(sqrt(30)/30) / 29, + 3 * sqrt(29) * acos(sqrt(30)/30) / 29, + 4 * sqrt(29) * acos(sqrt(30)/30) / 29) + + assert q1.pow_cos_sin(2) == \ + Quaternion(30 * cos(2 * acos(sqrt(30)/30)), + 60 * sqrt(29) * sin(2 * acos(sqrt(30)/30)) / 29, + 90 * sqrt(29) * sin(2 * acos(sqrt(30)/30)) / 29, + 120 * sqrt(29) * sin(2 * acos(sqrt(30)/30)) / 29) + + assert diff(Quaternion(x, x, x, x), x) == Quaternion(1, 1, 1, 1) + + assert integrate(Quaternion(x, x, x, x), x) == \ + Quaternion(x**2 / 2, x**2 / 2, x**2 / 2, x**2 / 2) + + assert Quaternion(1, x, x**2, x**3).integrate(x) == \ + Quaternion(x, x**2/2, x**3/3, x**4/4) + + assert Quaternion(sin(x), cos(x), sin(2*x), cos(2*x)).integrate(x) == \ + Quaternion(-cos(x), sin(x), -cos(2*x)/2, sin(2*x)/2) + + assert Quaternion(x**2, y**2, z**2, x*y*z).integrate(x, y) == \ + Quaternion(x**3*y/3, x*y**3/3, x*y*z**2, x**2*y**2*z/4) + + assert Quaternion.rotate_point((1, 1, 1), q1) == (S.One / 5, 1, S(7) / 5) + n = Symbol('n') + raises(TypeError, lambda: q1**n) + n = Symbol('n', integer=True) + raises(TypeError, lambda: q1**n) + + assert Quaternion(22, 23, 55, 8).scalar_part() == 22 + assert Quaternion(w, x, y, z).scalar_part() == w + + assert Quaternion(22, 23, 55, 8).vector_part() == Quaternion(0, 23, 55, 8) + assert Quaternion(w, x, y, z).vector_part() == Quaternion(0, x, y, z) + + assert q1.axis() == Quaternion(0, 2*sqrt(29)/29, 3*sqrt(29)/29, 4*sqrt(29)/29) + assert q1.axis().pow(2) == Quaternion(-1, 0, 0, 0) + assert q0.axis().scalar_part() == 0 + assert (q.axis() == Quaternion(0, + x/sqrt(x**2 + y**2 + z**2), + y/sqrt(x**2 + y**2 + z**2), + z/sqrt(x**2 + y**2 + z**2))) + + assert q0.is_pure() is True + assert q1.is_pure() is False + assert Quaternion(0, 0, 0, 3).is_pure() is True + assert Quaternion(0, 2, 10, 3).is_pure() is True + assert Quaternion(w, 2, 10, 3).is_pure() is None + + assert q1.angle() == 2*atan(sqrt(29)) + assert q.angle() == 2*atan2(sqrt(x**2 + y**2 + z**2), w) + + assert Quaternion.arc_coplanar(q1, Quaternion(2, 4, 6, 8)) is True + assert Quaternion.arc_coplanar(q1, Quaternion(1, -2, -3, -4)) is True + assert Quaternion.arc_coplanar(q1, Quaternion(1, 8, 12, 16)) is True + assert Quaternion.arc_coplanar(q1, Quaternion(1, 2, 3, 4)) is True + assert Quaternion.arc_coplanar(q1, Quaternion(w, 4, 6, 8)) is True + assert Quaternion.arc_coplanar(q1, Quaternion(2, 7, 4, 1)) is False + assert Quaternion.arc_coplanar(q1, Quaternion(w, x, y, z)) is None + raises(ValueError, lambda: Quaternion.arc_coplanar(q1, q0)) + + assert Quaternion.vector_coplanar( + Quaternion(0, 8, 12, 16), + Quaternion(0, 4, 6, 8), + Quaternion(0, 2, 3, 4)) is True + assert Quaternion.vector_coplanar( + Quaternion(0, 0, 0, 0), Quaternion(0, 4, 6, 8), Quaternion(0, 2, 3, 4)) is True + assert Quaternion.vector_coplanar( + Quaternion(0, 8, 2, 6), Quaternion(0, 1, 6, 6), Quaternion(0, 0, 3, 4)) is False + assert Quaternion.vector_coplanar( + Quaternion(0, 1, 3, 4), + Quaternion(0, 4, w, 6), + Quaternion(0, 6, 8, 1)) is None + raises(ValueError, lambda: + Quaternion.vector_coplanar(q0, Quaternion(0, 4, 6, 8), q1)) + + assert Quaternion(0, 1, 2, 3).parallel(Quaternion(0, 2, 4, 6)) is True + assert Quaternion(0, 1, 2, 3).parallel(Quaternion(0, 2, 2, 6)) is False + assert Quaternion(0, 1, 2, 3).parallel(Quaternion(w, x, y, 6)) is None + raises(ValueError, lambda: q0.parallel(q1)) + + assert Quaternion(0, 1, 2, 3).orthogonal(Quaternion(0, -2, 1, 0)) is True + assert Quaternion(0, 2, 4, 7).orthogonal(Quaternion(0, 2, 2, 6)) is False + assert Quaternion(0, 2, 4, 7).orthogonal(Quaternion(w, x, y, 6)) is None + raises(ValueError, lambda: q0.orthogonal(q1)) + + assert q1.index_vector() == Quaternion( + 0, 2*sqrt(870)/29, + 3*sqrt(870)/29, + 4*sqrt(870)/29) + assert Quaternion(0, 3, 9, 4).index_vector() == Quaternion(0, 3, 9, 4) + + assert Quaternion(4, 3, 9, 4).mensor() == log(sqrt(122)) + assert Quaternion(3, 3, 0, 2).mensor() == log(sqrt(22)) + + assert q0.is_zero_quaternion() is True + assert q1.is_zero_quaternion() is False + assert Quaternion(w, 0, 0, 0).is_zero_quaternion() is None + +def test_quaternion_conversions(): + q1 = Quaternion(1, 2, 3, 4) + + assert q1.to_axis_angle() == ((2 * sqrt(29)/29, + 3 * sqrt(29)/29, + 4 * sqrt(29)/29), + 2 * acos(sqrt(30)/30)) + + assert (q1.to_rotation_matrix() == + Matrix([[Rational(-2, 3), Rational(2, 15), Rational(11, 15)], + [Rational(2, 3), Rational(-1, 3), Rational(2, 3)], + [Rational(1, 3), Rational(14, 15), Rational(2, 15)]])) + + assert (q1.to_rotation_matrix((1, 1, 1)) == + Matrix([ + [Rational(-2, 3), Rational(2, 15), Rational(11, 15), Rational(4, 5)], + [Rational(2, 3), Rational(-1, 3), Rational(2, 3), S.Zero], + [Rational(1, 3), Rational(14, 15), Rational(2, 15), Rational(-2, 5)], + [S.Zero, S.Zero, S.Zero, S.One]])) + + theta = symbols("theta", real=True) + q2 = Quaternion(cos(theta/2), 0, 0, sin(theta/2)) + + assert trigsimp(q2.to_rotation_matrix()) == Matrix([ + [cos(theta), -sin(theta), 0], + [sin(theta), cos(theta), 0], + [0, 0, 1]]) + + assert q2.to_axis_angle() == ((0, 0, sin(theta/2)/Abs(sin(theta/2))), + 2*acos(cos(theta/2))) + + assert trigsimp(q2.to_rotation_matrix((1, 1, 1))) == Matrix([ + [cos(theta), -sin(theta), 0, sin(theta) - cos(theta) + 1], + [sin(theta), cos(theta), 0, -sin(theta) - cos(theta) + 1], + [0, 0, 1, 0], + [0, 0, 0, 1]]) + + +def test_rotation_matrix_homogeneous(): + q = Quaternion(w, x, y, z) + R1 = q.to_rotation_matrix(homogeneous=True) * q.norm()**2 + R2 = simplify(q.to_rotation_matrix(homogeneous=False) * q.norm()**2) + assert R1 == R2 + + +def test_quaternion_rotation_iss1593(): + """ + There was a sign mistake in the definition, + of the rotation matrix. This tests that particular sign mistake. + See issue 1593 for reference. + See wikipedia + https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix + for the correct definition + """ + q = Quaternion(cos(phi/2), sin(phi/2), 0, 0) + assert(trigsimp(q.to_rotation_matrix()) == Matrix([ + [1, 0, 0], + [0, cos(phi), -sin(phi)], + [0, sin(phi), cos(phi)]])) + + +def test_quaternion_multiplication(): + q1 = Quaternion(3 + 4*I, 2 + 5*I, 0, 7 + 8*I, real_field = False) + q2 = Quaternion(1, 2, 3, 5) + q3 = Quaternion(1, 1, 1, y) + + assert Quaternion._generic_mul(S(4), S.One) == 4 + assert (Quaternion._generic_mul(S(4), q1) == + Quaternion(12 + 16*I, 8 + 20*I, 0, 28 + 32*I)) + assert q2.mul(2) == Quaternion(2, 4, 6, 10) + assert q2.mul(q3) == Quaternion(-5*y - 4, 3*y - 2, 9 - 2*y, y + 4) + assert q2.mul(q3) == q2*q3 + + z = symbols('z', complex=True) + z_quat = Quaternion(re(z), im(z), 0, 0) + q = Quaternion(*symbols('q:4', real=True)) + + assert z * q == z_quat * q + assert q * z == q * z_quat + + +def test_issue_16318(): + #for rtruediv + q0 = Quaternion(0, 0, 0, 0) + raises(ValueError, lambda: 1/q0) + #for rotate_point + q = Quaternion(1, 2, 3, 4) + (axis, angle) = q.to_axis_angle() + assert Quaternion.rotate_point((1, 1, 1), (axis, angle)) == (S.One / 5, 1, S(7) / 5) + #test for to_axis_angle + q = Quaternion(-1, 1, 1, 1) + axis = (-sqrt(3)/3, -sqrt(3)/3, -sqrt(3)/3) + angle = 2*pi/3 + assert (axis, angle) == q.to_axis_angle() + + +@slow +def test_to_euler(): + q = Quaternion(w, x, y, z) + q_normalized = q.normalize() + + seqs = ['zxy', 'zyx', 'zyz', 'zxz'] + seqs += [seq.upper() for seq in seqs] + + for seq in seqs: + euler_from_q = q.to_euler(seq) + q_back = simplify(Quaternion.from_euler(euler_from_q, seq)) + assert q_back == q_normalized + + +def test_to_euler_iss24504(): + """ + There was a mistake in the degenerate case testing + See issue 24504 for reference. + """ + q = Quaternion.from_euler((phi, 0, 0), 'zyz') + assert trigsimp(q.to_euler('zyz'), inverse=True) == (phi, 0, 0) + + +def test_to_euler_numerical_singilarities(): + + def test_one_case(angles, seq): + q = Quaternion.from_euler(angles, seq) + assert q.to_euler(seq) == angles + + # symmetric + test_one_case((pi/2, 0, 0), 'zyz') + test_one_case((pi/2, 0, 0), 'ZYZ') + test_one_case((pi/2, pi, 0), 'zyz') + test_one_case((pi/2, pi, 0), 'ZYZ') + + # asymmetric + test_one_case((pi/2, pi/2, 0), 'zyx') + test_one_case((pi/2, -pi/2, 0), 'zyx') + test_one_case((pi/2, pi/2, 0), 'ZYX') + test_one_case((pi/2, -pi/2, 0), 'ZYX') + + +@slow +def test_to_euler_options(): + def test_one_case(q): + angles1 = Matrix(q.to_euler(seq, True, True)) + angles2 = Matrix(q.to_euler(seq, False, False)) + angle_errors = simplify(angles1-angles2).evalf() + for angle_error in angle_errors: + # forcing angles to set {-pi, pi} + angle_error = (angle_error + pi) % (2 * pi) - pi + assert angle_error < 10e-7 + + for xyz in ('xyz', 'XYZ'): + for seq_tuple in permutations(xyz): + for symmetric in (True, False): + if symmetric: + seq = ''.join([seq_tuple[0], seq_tuple[1], seq_tuple[0]]) + else: + seq = ''.join(seq_tuple) + + for elements in product([-1, 0, 1], repeat=4): + q = Quaternion(*elements) + if not q.is_zero_quaternion(): + test_one_case(q) diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/__init__.py b/venv/lib/python3.11/site-packages/sympy/assumptions/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..af862653f3ce0eeb67f7764e16c32f3466e87024 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/__init__.py @@ -0,0 +1,18 @@ +""" +A module to implement logical predicates and assumption system. +""" + +from .assume import ( + AppliedPredicate, Predicate, AssumptionsContext, assuming, + global_assumptions +) +from .ask import Q, ask, register_handler, remove_handler +from .refine import refine +from .relation import BinaryRelation, AppliedBinaryRelation + +__all__ = [ + 'AppliedPredicate', 'Predicate', 'AssumptionsContext', 'assuming', + 'global_assumptions', 'Q', 'ask', 'register_handler', 'remove_handler', + 'refine', + 'BinaryRelation', 'AppliedBinaryRelation' +] diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/ask.py b/venv/lib/python3.11/site-packages/sympy/assumptions/ask.py new file mode 100644 index 0000000000000000000000000000000000000000..ec81ec8ecce245c2a798cf9e71af1e9373292bc1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/ask.py @@ -0,0 +1,651 @@ +"""Module for querying SymPy objects about assumptions.""" + +from sympy.assumptions.assume import (global_assumptions, Predicate, + AppliedPredicate) +from sympy.assumptions.cnf import CNF, EncodedCNF, Literal +from sympy.core import sympify +from sympy.core.kind import BooleanKind +from sympy.core.relational import Eq, Ne, Gt, Lt, Ge, Le +from sympy.logic.inference import satisfiable +from sympy.utilities.decorator import memoize_property +from sympy.utilities.exceptions import (sympy_deprecation_warning, + SymPyDeprecationWarning, + ignore_warnings) + + +# Memoization is necessary for the properties of AssumptionKeys to +# ensure that only one object of Predicate objects are created. +# This is because assumption handlers are registered on those objects. + + +class AssumptionKeys: + """ + This class contains all the supported keys by ``ask``. + It should be accessed via the instance ``sympy.Q``. + + """ + + # DO NOT add methods or properties other than predicate keys. + # SAT solver checks the properties of Q and use them to compute the + # fact system. Non-predicate attributes will break this. + + @memoize_property + def hermitian(self): + from .handlers.sets import HermitianPredicate + return HermitianPredicate() + + @memoize_property + def antihermitian(self): + from .handlers.sets import AntihermitianPredicate + return AntihermitianPredicate() + + @memoize_property + def real(self): + from .handlers.sets import RealPredicate + return RealPredicate() + + @memoize_property + def extended_real(self): + from .handlers.sets import ExtendedRealPredicate + return ExtendedRealPredicate() + + @memoize_property + def imaginary(self): + from .handlers.sets import ImaginaryPredicate + return ImaginaryPredicate() + + @memoize_property + def complex(self): + from .handlers.sets import ComplexPredicate + return ComplexPredicate() + + @memoize_property + def algebraic(self): + from .handlers.sets import AlgebraicPredicate + return AlgebraicPredicate() + + @memoize_property + def transcendental(self): + from .predicates.sets import TranscendentalPredicate + return TranscendentalPredicate() + + @memoize_property + def integer(self): + from .handlers.sets import IntegerPredicate + return IntegerPredicate() + + @memoize_property + def noninteger(self): + from .predicates.sets import NonIntegerPredicate + return NonIntegerPredicate() + + @memoize_property + def rational(self): + from .handlers.sets import RationalPredicate + return RationalPredicate() + + @memoize_property + def irrational(self): + from .handlers.sets import IrrationalPredicate + return IrrationalPredicate() + + @memoize_property + def finite(self): + from .handlers.calculus import FinitePredicate + return FinitePredicate() + + @memoize_property + def infinite(self): + from .handlers.calculus import InfinitePredicate + return InfinitePredicate() + + @memoize_property + def positive_infinite(self): + from .handlers.calculus import PositiveInfinitePredicate + return PositiveInfinitePredicate() + + @memoize_property + def negative_infinite(self): + from .handlers.calculus import NegativeInfinitePredicate + return NegativeInfinitePredicate() + + @memoize_property + def positive(self): + from .handlers.order import PositivePredicate + return PositivePredicate() + + @memoize_property + def negative(self): + from .handlers.order import NegativePredicate + return NegativePredicate() + + @memoize_property + def zero(self): + from .handlers.order import ZeroPredicate + return ZeroPredicate() + + @memoize_property + def extended_positive(self): + from .handlers.order import ExtendedPositivePredicate + return ExtendedPositivePredicate() + + @memoize_property + def extended_negative(self): + from .handlers.order import ExtendedNegativePredicate + return ExtendedNegativePredicate() + + @memoize_property + def nonzero(self): + from .handlers.order import NonZeroPredicate + return NonZeroPredicate() + + @memoize_property + def nonpositive(self): + from .handlers.order import NonPositivePredicate + return NonPositivePredicate() + + @memoize_property + def nonnegative(self): + from .handlers.order import NonNegativePredicate + return NonNegativePredicate() + + @memoize_property + def extended_nonzero(self): + from .handlers.order import ExtendedNonZeroPredicate + return ExtendedNonZeroPredicate() + + @memoize_property + def extended_nonpositive(self): + from .handlers.order import ExtendedNonPositivePredicate + return ExtendedNonPositivePredicate() + + @memoize_property + def extended_nonnegative(self): + from .handlers.order import ExtendedNonNegativePredicate + return ExtendedNonNegativePredicate() + + @memoize_property + def even(self): + from .handlers.ntheory import EvenPredicate + return EvenPredicate() + + @memoize_property + def odd(self): + from .handlers.ntheory import OddPredicate + return OddPredicate() + + @memoize_property + def prime(self): + from .handlers.ntheory import PrimePredicate + return PrimePredicate() + + @memoize_property + def composite(self): + from .handlers.ntheory import CompositePredicate + return CompositePredicate() + + @memoize_property + def commutative(self): + from .handlers.common import CommutativePredicate + return CommutativePredicate() + + @memoize_property + def is_true(self): + from .handlers.common import IsTruePredicate + return IsTruePredicate() + + @memoize_property + def symmetric(self): + from .handlers.matrices import SymmetricPredicate + return SymmetricPredicate() + + @memoize_property + def invertible(self): + from .handlers.matrices import InvertiblePredicate + return InvertiblePredicate() + + @memoize_property + def orthogonal(self): + from .handlers.matrices import OrthogonalPredicate + return OrthogonalPredicate() + + @memoize_property + def unitary(self): + from .handlers.matrices import UnitaryPredicate + return UnitaryPredicate() + + @memoize_property + def positive_definite(self): + from .handlers.matrices import PositiveDefinitePredicate + return PositiveDefinitePredicate() + + @memoize_property + def upper_triangular(self): + from .handlers.matrices import UpperTriangularPredicate + return UpperTriangularPredicate() + + @memoize_property + def lower_triangular(self): + from .handlers.matrices import LowerTriangularPredicate + return LowerTriangularPredicate() + + @memoize_property + def diagonal(self): + from .handlers.matrices import DiagonalPredicate + return DiagonalPredicate() + + @memoize_property + def fullrank(self): + from .handlers.matrices import FullRankPredicate + return FullRankPredicate() + + @memoize_property + def square(self): + from .handlers.matrices import SquarePredicate + return SquarePredicate() + + @memoize_property + def integer_elements(self): + from .handlers.matrices import IntegerElementsPredicate + return IntegerElementsPredicate() + + @memoize_property + def real_elements(self): + from .handlers.matrices import RealElementsPredicate + return RealElementsPredicate() + + @memoize_property + def complex_elements(self): + from .handlers.matrices import ComplexElementsPredicate + return ComplexElementsPredicate() + + @memoize_property + def singular(self): + from .predicates.matrices import SingularPredicate + return SingularPredicate() + + @memoize_property + def normal(self): + from .predicates.matrices import NormalPredicate + return NormalPredicate() + + @memoize_property + def triangular(self): + from .predicates.matrices import TriangularPredicate + return TriangularPredicate() + + @memoize_property + def unit_triangular(self): + from .predicates.matrices import UnitTriangularPredicate + return UnitTriangularPredicate() + + @memoize_property + def eq(self): + from .relation.equality import EqualityPredicate + return EqualityPredicate() + + @memoize_property + def ne(self): + from .relation.equality import UnequalityPredicate + return UnequalityPredicate() + + @memoize_property + def gt(self): + from .relation.equality import StrictGreaterThanPredicate + return StrictGreaterThanPredicate() + + @memoize_property + def ge(self): + from .relation.equality import GreaterThanPredicate + return GreaterThanPredicate() + + @memoize_property + def lt(self): + from .relation.equality import StrictLessThanPredicate + return StrictLessThanPredicate() + + @memoize_property + def le(self): + from .relation.equality import LessThanPredicate + return LessThanPredicate() + + +Q = AssumptionKeys() + +def _extract_all_facts(assump, exprs): + """ + Extract all relevant assumptions from *assump* with respect to given *exprs*. + + Parameters + ========== + + assump : sympy.assumptions.cnf.CNF + + exprs : tuple of expressions + + Returns + ======= + + sympy.assumptions.cnf.CNF + + Examples + ======== + + >>> from sympy import Q + >>> from sympy.assumptions.cnf import CNF + >>> from sympy.assumptions.ask import _extract_all_facts + >>> from sympy.abc import x, y + >>> assump = CNF.from_prop(Q.positive(x) & Q.integer(y)) + >>> exprs = (x,) + >>> cnf = _extract_all_facts(assump, exprs) + >>> cnf.clauses + {frozenset({Literal(Q.positive, False)})} + + """ + facts = set() + + for clause in assump.clauses: + args = [] + for literal in clause: + if isinstance(literal.lit, AppliedPredicate) and len(literal.lit.arguments) == 1: + if literal.lit.arg in exprs: + # Add literal if it has matching in it + args.append(Literal(literal.lit.function, literal.is_Not)) + else: + # If any of the literals doesn't have matching expr don't add the whole clause. + break + else: + # If any of the literals aren't unary predicate don't add the whole clause. + break + + else: + if args: + facts.add(frozenset(args)) + return CNF(facts) + + +def ask(proposition, assumptions=True, context=global_assumptions): + """ + Function to evaluate the proposition with assumptions. + + Explanation + =========== + + This function evaluates the proposition to ``True`` or ``False`` if + the truth value can be determined. If not, it returns ``None``. + + It should be discerned from :func:`~.refine` which, when applied to a + proposition, simplifies the argument to symbolic ``Boolean`` instead of + Python built-in ``True``, ``False`` or ``None``. + + **Syntax** + + * ask(proposition) + Evaluate the *proposition* in global assumption context. + + * ask(proposition, assumptions) + Evaluate the *proposition* with respect to *assumptions* in + global assumption context. + + Parameters + ========== + + proposition : Boolean + Proposition which will be evaluated to boolean value. If this is + not ``AppliedPredicate``, it will be wrapped by ``Q.is_true``. + + assumptions : Boolean, optional + Local assumptions to evaluate the *proposition*. + + context : AssumptionsContext, optional + Default assumptions to evaluate the *proposition*. By default, + this is ``sympy.assumptions.global_assumptions`` variable. + + Returns + ======= + + ``True``, ``False``, or ``None`` + + Raises + ====== + + TypeError : *proposition* or *assumptions* is not valid logical expression. + + ValueError : assumptions are inconsistent. + + Examples + ======== + + >>> from sympy import ask, Q, pi + >>> from sympy.abc import x, y + >>> ask(Q.rational(pi)) + False + >>> ask(Q.even(x*y), Q.even(x) & Q.integer(y)) + True + >>> ask(Q.prime(4*x), Q.integer(x)) + False + + If the truth value cannot be determined, ``None`` will be returned. + + >>> print(ask(Q.odd(3*x))) # cannot determine unless we know x + None + + ``ValueError`` is raised if assumptions are inconsistent. + + >>> ask(Q.integer(x), Q.even(x) & Q.odd(x)) + Traceback (most recent call last): + ... + ValueError: inconsistent assumptions Q.even(x) & Q.odd(x) + + Notes + ===== + + Relations in assumptions are not implemented (yet), so the following + will not give a meaningful result. + + >>> ask(Q.positive(x), x > 0) + + It is however a work in progress. + + See Also + ======== + + sympy.assumptions.refine.refine : Simplification using assumptions. + Proposition is not reduced to ``None`` if the truth value cannot + be determined. + """ + from sympy.assumptions.satask import satask + from sympy.assumptions.lra_satask import lra_satask + from sympy.logic.algorithms.lra_theory import UnhandledInput + + proposition = sympify(proposition) + assumptions = sympify(assumptions) + + if isinstance(proposition, Predicate) or proposition.kind is not BooleanKind: + raise TypeError("proposition must be a valid logical expression") + + if isinstance(assumptions, Predicate) or assumptions.kind is not BooleanKind: + raise TypeError("assumptions must be a valid logical expression") + + binrelpreds = {Eq: Q.eq, Ne: Q.ne, Gt: Q.gt, Lt: Q.lt, Ge: Q.ge, Le: Q.le} + if isinstance(proposition, AppliedPredicate): + key, args = proposition.function, proposition.arguments + elif proposition.func in binrelpreds: + key, args = binrelpreds[type(proposition)], proposition.args + else: + key, args = Q.is_true, (proposition,) + + # convert local and global assumptions to CNF + assump_cnf = CNF.from_prop(assumptions) + assump_cnf.extend(context) + + # extract the relevant facts from assumptions with respect to args + local_facts = _extract_all_facts(assump_cnf, args) + + # convert default facts and assumed facts to encoded CNF + known_facts_cnf = get_all_known_facts() + enc_cnf = EncodedCNF() + enc_cnf.from_cnf(CNF(known_facts_cnf)) + enc_cnf.add_from_cnf(local_facts) + + # check the satisfiability of given assumptions + if local_facts.clauses and satisfiable(enc_cnf) is False: + raise ValueError("inconsistent assumptions %s" % assumptions) + + # quick computation for single fact + res = _ask_single_fact(key, local_facts) + if res is not None: + return res + + # direct resolution method, no logic + res = key(*args)._eval_ask(assumptions) + if res is not None: + return bool(res) + + # using satask (still costly) + res = satask(proposition, assumptions=assumptions, context=context) + if res is not None: + return res + + try: + res = lra_satask(proposition, assumptions=assumptions, context=context) + except UnhandledInput: + return None + + return res + + +def _ask_single_fact(key, local_facts): + """ + Compute the truth value of single predicate using assumptions. + + Parameters + ========== + + key : sympy.assumptions.assume.Predicate + Proposition predicate. + + local_facts : sympy.assumptions.cnf.CNF + Local assumption in CNF form. + + Returns + ======= + + ``True``, ``False`` or ``None`` + + Examples + ======== + + >>> from sympy import Q + >>> from sympy.assumptions.cnf import CNF + >>> from sympy.assumptions.ask import _ask_single_fact + + If prerequisite of proposition is rejected by the assumption, + return ``False``. + + >>> key, assump = Q.zero, ~Q.zero + >>> local_facts = CNF.from_prop(assump) + >>> _ask_single_fact(key, local_facts) + False + >>> key, assump = Q.zero, ~Q.even + >>> local_facts = CNF.from_prop(assump) + >>> _ask_single_fact(key, local_facts) + False + + If assumption implies the proposition, return ``True``. + + >>> key, assump = Q.even, Q.zero + >>> local_facts = CNF.from_prop(assump) + >>> _ask_single_fact(key, local_facts) + True + + If proposition rejects the assumption, return ``False``. + + >>> key, assump = Q.even, Q.odd + >>> local_facts = CNF.from_prop(assump) + >>> _ask_single_fact(key, local_facts) + False + """ + if local_facts.clauses: + + known_facts_dict = get_known_facts_dict() + + if len(local_facts.clauses) == 1: + cl, = local_facts.clauses + if len(cl) == 1: + f, = cl + prop_facts = known_facts_dict.get(key, None) + prop_req = prop_facts[0] if prop_facts is not None else set() + if f.is_Not and f.arg in prop_req: + # the prerequisite of proposition is rejected + return False + + for clause in local_facts.clauses: + if len(clause) == 1: + f, = clause + prop_facts = known_facts_dict.get(f.arg, None) if not f.is_Not else None + if prop_facts is None: + continue + + prop_req, prop_rej = prop_facts + if key in prop_req: + # assumption implies the proposition + return True + elif key in prop_rej: + # proposition rejects the assumption + return False + + return None + + +def register_handler(key, handler): + """ + Register a handler in the ask system. key must be a string and handler a + class inheriting from AskHandler. + + .. deprecated:: 1.8. + Use multipledispatch handler instead. See :obj:`~.Predicate`. + + """ + sympy_deprecation_warning( + """ + The AskHandler system is deprecated. The register_handler() function + should be replaced with the multipledispatch handler of Predicate. + """, + deprecated_since_version="1.8", + active_deprecations_target='deprecated-askhandler', + ) + if isinstance(key, Predicate): + key = key.name.name + Qkey = getattr(Q, key, None) + if Qkey is not None: + Qkey.add_handler(handler) + else: + setattr(Q, key, Predicate(key, handlers=[handler])) + + +def remove_handler(key, handler): + """ + Removes a handler from the ask system. + + .. deprecated:: 1.8. + Use multipledispatch handler instead. See :obj:`~.Predicate`. + + """ + sympy_deprecation_warning( + """ + The AskHandler system is deprecated. The remove_handler() function + should be replaced with the multipledispatch handler of Predicate. + """, + deprecated_since_version="1.8", + active_deprecations_target='deprecated-askhandler', + ) + if isinstance(key, Predicate): + key = key.name.name + # Don't show the same warning again recursively + with ignore_warnings(SymPyDeprecationWarning): + getattr(Q, key).remove_handler(handler) + + +from sympy.assumptions.ask_generated import (get_all_known_facts, + get_known_facts_dict) diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/ask_generated.py b/venv/lib/python3.11/site-packages/sympy/assumptions/ask_generated.py new file mode 100644 index 0000000000000000000000000000000000000000..d90cdffc1e127d78e18f70cda13d8d5e0530d41b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/ask_generated.py @@ -0,0 +1,352 @@ +""" +Do NOT manually edit this file. +Instead, run ./bin/ask_update.py. +""" + +from sympy.assumptions.ask import Q +from sympy.assumptions.cnf import Literal +from sympy.core.cache import cacheit + +@cacheit +def get_all_known_facts(): + """ + Known facts between unary predicates as CNF clauses. + """ + return { + frozenset((Literal(Q.algebraic, False), Literal(Q.imaginary, True), Literal(Q.transcendental, False))), + frozenset((Literal(Q.algebraic, False), Literal(Q.negative, True), Literal(Q.transcendental, False))), + frozenset((Literal(Q.algebraic, False), Literal(Q.positive, True), Literal(Q.transcendental, False))), + frozenset((Literal(Q.algebraic, False), Literal(Q.rational, True))), + frozenset((Literal(Q.algebraic, False), Literal(Q.transcendental, False), Literal(Q.zero, True))), + frozenset((Literal(Q.algebraic, True), Literal(Q.finite, False))), + frozenset((Literal(Q.algebraic, True), Literal(Q.transcendental, True))), + frozenset((Literal(Q.antihermitian, False), Literal(Q.hermitian, False), Literal(Q.zero, True))), + frozenset((Literal(Q.antihermitian, False), Literal(Q.imaginary, True))), + frozenset((Literal(Q.commutative, False), Literal(Q.finite, True))), + frozenset((Literal(Q.commutative, False), Literal(Q.infinite, True))), + frozenset((Literal(Q.complex_elements, False), Literal(Q.real_elements, True))), + frozenset((Literal(Q.composite, False), Literal(Q.even, True), Literal(Q.positive, True), Literal(Q.prime, False))), + frozenset((Literal(Q.composite, True), Literal(Q.even, False), Literal(Q.odd, False))), + frozenset((Literal(Q.composite, True), Literal(Q.positive, False))), + frozenset((Literal(Q.composite, True), Literal(Q.prime, True))), + frozenset((Literal(Q.diagonal, False), Literal(Q.lower_triangular, True), Literal(Q.upper_triangular, True))), + frozenset((Literal(Q.diagonal, True), Literal(Q.lower_triangular, False))), + frozenset((Literal(Q.diagonal, True), Literal(Q.normal, False))), + frozenset((Literal(Q.diagonal, True), Literal(Q.symmetric, False))), + frozenset((Literal(Q.diagonal, True), Literal(Q.upper_triangular, False))), + frozenset((Literal(Q.even, False), Literal(Q.odd, False), Literal(Q.prime, True))), + frozenset((Literal(Q.even, False), Literal(Q.zero, True))), + frozenset((Literal(Q.even, True), Literal(Q.odd, True))), + frozenset((Literal(Q.even, True), Literal(Q.rational, False))), + frozenset((Literal(Q.finite, False), Literal(Q.transcendental, True))), + frozenset((Literal(Q.finite, True), Literal(Q.infinite, True))), + frozenset((Literal(Q.fullrank, False), Literal(Q.invertible, True))), + frozenset((Literal(Q.fullrank, True), Literal(Q.invertible, False), Literal(Q.square, True))), + frozenset((Literal(Q.hermitian, False), Literal(Q.negative, True))), + frozenset((Literal(Q.hermitian, False), Literal(Q.positive, True))), + frozenset((Literal(Q.hermitian, False), Literal(Q.zero, True))), + frozenset((Literal(Q.imaginary, True), Literal(Q.negative, True))), + frozenset((Literal(Q.imaginary, True), Literal(Q.positive, True))), + frozenset((Literal(Q.imaginary, True), Literal(Q.zero, True))), + frozenset((Literal(Q.infinite, False), Literal(Q.negative_infinite, True))), + frozenset((Literal(Q.infinite, False), Literal(Q.positive_infinite, True))), + frozenset((Literal(Q.integer_elements, True), Literal(Q.real_elements, False))), + frozenset((Literal(Q.invertible, False), Literal(Q.positive_definite, True))), + frozenset((Literal(Q.invertible, False), Literal(Q.singular, False))), + frozenset((Literal(Q.invertible, False), Literal(Q.unitary, True))), + frozenset((Literal(Q.invertible, True), Literal(Q.singular, True))), + frozenset((Literal(Q.invertible, True), Literal(Q.square, False))), + frozenset((Literal(Q.irrational, False), Literal(Q.negative, True), Literal(Q.rational, False))), + frozenset((Literal(Q.irrational, False), Literal(Q.positive, True), Literal(Q.rational, False))), + frozenset((Literal(Q.irrational, False), Literal(Q.rational, False), Literal(Q.zero, True))), + frozenset((Literal(Q.irrational, True), Literal(Q.negative, False), Literal(Q.positive, False), Literal(Q.zero, False))), + frozenset((Literal(Q.irrational, True), Literal(Q.rational, True))), + frozenset((Literal(Q.lower_triangular, False), Literal(Q.triangular, True), Literal(Q.upper_triangular, False))), + frozenset((Literal(Q.lower_triangular, True), Literal(Q.triangular, False))), + frozenset((Literal(Q.negative, False), Literal(Q.positive, False), Literal(Q.rational, True), Literal(Q.zero, False))), + frozenset((Literal(Q.negative, True), Literal(Q.negative_infinite, True))), + frozenset((Literal(Q.negative, True), Literal(Q.positive, True))), + frozenset((Literal(Q.negative, True), Literal(Q.positive_infinite, True))), + frozenset((Literal(Q.negative, True), Literal(Q.zero, True))), + frozenset((Literal(Q.negative_infinite, True), Literal(Q.positive, True))), + frozenset((Literal(Q.negative_infinite, True), Literal(Q.positive_infinite, True))), + frozenset((Literal(Q.negative_infinite, True), Literal(Q.zero, True))), + frozenset((Literal(Q.normal, False), Literal(Q.unitary, True))), + frozenset((Literal(Q.normal, True), Literal(Q.square, False))), + frozenset((Literal(Q.odd, True), Literal(Q.rational, False))), + frozenset((Literal(Q.orthogonal, False), Literal(Q.real_elements, True), Literal(Q.unitary, True))), + frozenset((Literal(Q.orthogonal, True), Literal(Q.positive_definite, False))), + frozenset((Literal(Q.orthogonal, True), Literal(Q.unitary, False))), + frozenset((Literal(Q.positive, False), Literal(Q.prime, True))), + frozenset((Literal(Q.positive, True), Literal(Q.positive_infinite, True))), + frozenset((Literal(Q.positive, True), Literal(Q.zero, True))), + frozenset((Literal(Q.positive_infinite, True), Literal(Q.zero, True))), + frozenset((Literal(Q.square, False), Literal(Q.symmetric, True))), + frozenset((Literal(Q.triangular, False), Literal(Q.unit_triangular, True))), + frozenset((Literal(Q.triangular, False), Literal(Q.upper_triangular, True))) + } + +@cacheit +def get_all_known_matrix_facts(): + """ + Known facts between unary predicates for matrices as CNF clauses. + """ + return { + frozenset((Literal(Q.complex_elements, False), Literal(Q.real_elements, True))), + frozenset((Literal(Q.diagonal, False), Literal(Q.lower_triangular, True), Literal(Q.upper_triangular, True))), + frozenset((Literal(Q.diagonal, True), Literal(Q.lower_triangular, False))), + frozenset((Literal(Q.diagonal, True), Literal(Q.normal, False))), + frozenset((Literal(Q.diagonal, True), Literal(Q.symmetric, False))), + frozenset((Literal(Q.diagonal, True), Literal(Q.upper_triangular, False))), + frozenset((Literal(Q.fullrank, False), Literal(Q.invertible, True))), + frozenset((Literal(Q.fullrank, True), Literal(Q.invertible, False), Literal(Q.square, True))), + frozenset((Literal(Q.integer_elements, True), Literal(Q.real_elements, False))), + frozenset((Literal(Q.invertible, False), Literal(Q.positive_definite, True))), + frozenset((Literal(Q.invertible, False), Literal(Q.singular, False))), + frozenset((Literal(Q.invertible, False), Literal(Q.unitary, True))), + frozenset((Literal(Q.invertible, True), Literal(Q.singular, True))), + frozenset((Literal(Q.invertible, True), Literal(Q.square, False))), + frozenset((Literal(Q.lower_triangular, False), Literal(Q.triangular, True), Literal(Q.upper_triangular, False))), + frozenset((Literal(Q.lower_triangular, True), Literal(Q.triangular, False))), + frozenset((Literal(Q.normal, False), Literal(Q.unitary, True))), + frozenset((Literal(Q.normal, True), Literal(Q.square, False))), + frozenset((Literal(Q.orthogonal, False), Literal(Q.real_elements, True), Literal(Q.unitary, True))), + frozenset((Literal(Q.orthogonal, True), Literal(Q.positive_definite, False))), + frozenset((Literal(Q.orthogonal, True), Literal(Q.unitary, False))), + frozenset((Literal(Q.square, False), Literal(Q.symmetric, True))), + frozenset((Literal(Q.triangular, False), Literal(Q.unit_triangular, True))), + frozenset((Literal(Q.triangular, False), Literal(Q.upper_triangular, True))) + } + +@cacheit +def get_all_known_number_facts(): + """ + Known facts between unary predicates for numbers as CNF clauses. + """ + return { + frozenset((Literal(Q.algebraic, False), Literal(Q.imaginary, True), Literal(Q.transcendental, False))), + frozenset((Literal(Q.algebraic, False), Literal(Q.negative, True), Literal(Q.transcendental, False))), + frozenset((Literal(Q.algebraic, False), Literal(Q.positive, True), Literal(Q.transcendental, False))), + frozenset((Literal(Q.algebraic, False), Literal(Q.rational, True))), + frozenset((Literal(Q.algebraic, False), Literal(Q.transcendental, False), Literal(Q.zero, True))), + frozenset((Literal(Q.algebraic, True), Literal(Q.finite, False))), + frozenset((Literal(Q.algebraic, True), Literal(Q.transcendental, True))), + frozenset((Literal(Q.antihermitian, False), Literal(Q.hermitian, False), Literal(Q.zero, True))), + frozenset((Literal(Q.antihermitian, False), Literal(Q.imaginary, True))), + frozenset((Literal(Q.commutative, False), Literal(Q.finite, True))), + frozenset((Literal(Q.commutative, False), Literal(Q.infinite, True))), + frozenset((Literal(Q.composite, False), Literal(Q.even, True), Literal(Q.positive, True), Literal(Q.prime, False))), + frozenset((Literal(Q.composite, True), Literal(Q.even, False), Literal(Q.odd, False))), + frozenset((Literal(Q.composite, True), Literal(Q.positive, False))), + frozenset((Literal(Q.composite, True), Literal(Q.prime, True))), + frozenset((Literal(Q.even, False), Literal(Q.odd, False), Literal(Q.prime, True))), + frozenset((Literal(Q.even, False), Literal(Q.zero, True))), + frozenset((Literal(Q.even, True), Literal(Q.odd, True))), + frozenset((Literal(Q.even, True), Literal(Q.rational, False))), + frozenset((Literal(Q.finite, False), Literal(Q.transcendental, True))), + frozenset((Literal(Q.finite, True), Literal(Q.infinite, True))), + frozenset((Literal(Q.hermitian, False), Literal(Q.negative, True))), + frozenset((Literal(Q.hermitian, False), Literal(Q.positive, True))), + frozenset((Literal(Q.hermitian, False), Literal(Q.zero, True))), + frozenset((Literal(Q.imaginary, True), Literal(Q.negative, True))), + frozenset((Literal(Q.imaginary, True), Literal(Q.positive, True))), + frozenset((Literal(Q.imaginary, True), Literal(Q.zero, True))), + frozenset((Literal(Q.infinite, False), Literal(Q.negative_infinite, True))), + frozenset((Literal(Q.infinite, False), Literal(Q.positive_infinite, True))), + frozenset((Literal(Q.irrational, False), Literal(Q.negative, True), Literal(Q.rational, False))), + frozenset((Literal(Q.irrational, False), Literal(Q.positive, True), Literal(Q.rational, False))), + frozenset((Literal(Q.irrational, False), Literal(Q.rational, False), Literal(Q.zero, True))), + frozenset((Literal(Q.irrational, True), Literal(Q.negative, False), Literal(Q.positive, False), Literal(Q.zero, False))), + frozenset((Literal(Q.irrational, True), Literal(Q.rational, True))), + frozenset((Literal(Q.negative, False), Literal(Q.positive, False), Literal(Q.rational, True), Literal(Q.zero, False))), + frozenset((Literal(Q.negative, True), Literal(Q.negative_infinite, True))), + frozenset((Literal(Q.negative, True), Literal(Q.positive, True))), + frozenset((Literal(Q.negative, True), Literal(Q.positive_infinite, True))), + frozenset((Literal(Q.negative, True), Literal(Q.zero, True))), + frozenset((Literal(Q.negative_infinite, True), Literal(Q.positive, True))), + frozenset((Literal(Q.negative_infinite, True), Literal(Q.positive_infinite, True))), + frozenset((Literal(Q.negative_infinite, True), Literal(Q.zero, True))), + frozenset((Literal(Q.odd, True), Literal(Q.rational, False))), + frozenset((Literal(Q.positive, False), Literal(Q.prime, True))), + frozenset((Literal(Q.positive, True), Literal(Q.positive_infinite, True))), + frozenset((Literal(Q.positive, True), Literal(Q.zero, True))), + frozenset((Literal(Q.positive_infinite, True), Literal(Q.zero, True))) + } + +@cacheit +def get_known_facts_dict(): + """ + Logical relations between unary predicates as dictionary. + + Each key is a predicate, and item is two groups of predicates. + First group contains the predicates which are implied by the key, and + second group contains the predicates which are rejected by the key. + + """ + return { + Q.algebraic: (set([Q.algebraic, Q.commutative, Q.complex, Q.finite]), + set([Q.infinite, Q.negative_infinite, Q.positive_infinite, + Q.transcendental])), + Q.antihermitian: (set([Q.antihermitian]), set([])), + Q.commutative: (set([Q.commutative]), set([])), + Q.complex: (set([Q.commutative, Q.complex, Q.finite]), + set([Q.infinite, Q.negative_infinite, Q.positive_infinite])), + Q.complex_elements: (set([Q.complex_elements]), set([])), + Q.composite: (set([Q.algebraic, Q.commutative, Q.complex, Q.composite, + Q.extended_nonnegative, Q.extended_nonzero, + Q.extended_positive, Q.extended_real, Q.finite, Q.hermitian, + Q.integer, Q.nonnegative, Q.nonzero, Q.positive, Q.rational, + Q.real]), set([Q.extended_negative, Q.extended_nonpositive, + Q.imaginary, Q.infinite, Q.irrational, Q.negative, + Q.negative_infinite, Q.nonpositive, Q.positive_infinite, + Q.prime, Q.transcendental, Q.zero])), + Q.diagonal: (set([Q.diagonal, Q.lower_triangular, Q.normal, Q.square, + Q.symmetric, Q.triangular, Q.upper_triangular]), set([])), + Q.even: (set([Q.algebraic, Q.commutative, Q.complex, Q.even, + Q.extended_real, Q.finite, Q.hermitian, Q.integer, Q.rational, + Q.real]), set([Q.imaginary, Q.infinite, Q.irrational, + Q.negative_infinite, Q.odd, Q.positive_infinite, + Q.transcendental])), + Q.extended_negative: (set([Q.commutative, Q.extended_negative, + Q.extended_nonpositive, Q.extended_nonzero, Q.extended_real]), + set([Q.composite, Q.extended_nonnegative, Q.extended_positive, + Q.imaginary, Q.nonnegative, Q.positive, Q.positive_infinite, + Q.prime, Q.zero])), + Q.extended_nonnegative: (set([Q.commutative, Q.extended_nonnegative, + Q.extended_real]), set([Q.extended_negative, Q.imaginary, + Q.negative, Q.negative_infinite])), + Q.extended_nonpositive: (set([Q.commutative, Q.extended_nonpositive, + Q.extended_real]), set([Q.composite, Q.extended_positive, + Q.imaginary, Q.positive, Q.positive_infinite, Q.prime])), + Q.extended_nonzero: (set([Q.commutative, Q.extended_nonzero, + Q.extended_real]), set([Q.imaginary, Q.zero])), + Q.extended_positive: (set([Q.commutative, Q.extended_nonnegative, + Q.extended_nonzero, Q.extended_positive, Q.extended_real]), + set([Q.extended_negative, Q.extended_nonpositive, Q.imaginary, + Q.negative, Q.negative_infinite, Q.nonpositive, Q.zero])), + Q.extended_real: (set([Q.commutative, Q.extended_real]), + set([Q.imaginary])), + Q.finite: (set([Q.commutative, Q.finite]), set([Q.infinite, + Q.negative_infinite, Q.positive_infinite])), + Q.fullrank: (set([Q.fullrank]), set([])), + Q.hermitian: (set([Q.hermitian]), set([])), + Q.imaginary: (set([Q.antihermitian, Q.commutative, Q.complex, + Q.finite, Q.imaginary]), set([Q.composite, Q.even, + Q.extended_negative, Q.extended_nonnegative, + Q.extended_nonpositive, Q.extended_nonzero, + Q.extended_positive, Q.extended_real, Q.infinite, Q.integer, + Q.irrational, Q.negative, Q.negative_infinite, Q.nonnegative, + Q.nonpositive, Q.nonzero, Q.odd, Q.positive, + Q.positive_infinite, Q.prime, Q.rational, Q.real, Q.zero])), + Q.infinite: (set([Q.commutative, Q.infinite]), set([Q.algebraic, + Q.complex, Q.composite, Q.even, Q.finite, Q.imaginary, + Q.integer, Q.irrational, Q.negative, Q.nonnegative, + Q.nonpositive, Q.nonzero, Q.odd, Q.positive, Q.prime, + Q.rational, Q.real, Q.transcendental, Q.zero])), + Q.integer: (set([Q.algebraic, Q.commutative, Q.complex, + Q.extended_real, Q.finite, Q.hermitian, Q.integer, Q.rational, + Q.real]), set([Q.imaginary, Q.infinite, Q.irrational, + Q.negative_infinite, Q.positive_infinite, Q.transcendental])), + Q.integer_elements: (set([Q.complex_elements, Q.integer_elements, + Q.real_elements]), set([])), + Q.invertible: (set([Q.fullrank, Q.invertible, Q.square]), + set([Q.singular])), + Q.irrational: (set([Q.commutative, Q.complex, Q.extended_nonzero, + Q.extended_real, Q.finite, Q.hermitian, Q.irrational, + Q.nonzero, Q.real]), set([Q.composite, Q.even, Q.imaginary, + Q.infinite, Q.integer, Q.negative_infinite, Q.odd, + Q.positive_infinite, Q.prime, Q.rational, Q.zero])), + Q.is_true: (set([Q.is_true]), set([])), + Q.lower_triangular: (set([Q.lower_triangular, Q.triangular]), set([])), + Q.negative: (set([Q.commutative, Q.complex, Q.extended_negative, + Q.extended_nonpositive, Q.extended_nonzero, Q.extended_real, + Q.finite, Q.hermitian, Q.negative, Q.nonpositive, Q.nonzero, + Q.real]), set([Q.composite, Q.extended_nonnegative, + Q.extended_positive, Q.imaginary, Q.infinite, + Q.negative_infinite, Q.nonnegative, Q.positive, + Q.positive_infinite, Q.prime, Q.zero])), + Q.negative_infinite: (set([Q.commutative, Q.extended_negative, + Q.extended_nonpositive, Q.extended_nonzero, Q.extended_real, + Q.infinite, Q.negative_infinite]), set([Q.algebraic, + Q.complex, Q.composite, Q.even, Q.extended_nonnegative, + Q.extended_positive, Q.finite, Q.imaginary, Q.integer, + Q.irrational, Q.negative, Q.nonnegative, Q.nonpositive, + Q.nonzero, Q.odd, Q.positive, Q.positive_infinite, Q.prime, + Q.rational, Q.real, Q.transcendental, Q.zero])), + Q.noninteger: (set([Q.noninteger]), set([])), + Q.nonnegative: (set([Q.commutative, Q.complex, Q.extended_nonnegative, + Q.extended_real, Q.finite, Q.hermitian, Q.nonnegative, + Q.real]), set([Q.extended_negative, Q.imaginary, Q.infinite, + Q.negative, Q.negative_infinite, Q.positive_infinite])), + Q.nonpositive: (set([Q.commutative, Q.complex, Q.extended_nonpositive, + Q.extended_real, Q.finite, Q.hermitian, Q.nonpositive, + Q.real]), set([Q.composite, Q.extended_positive, Q.imaginary, + Q.infinite, Q.negative_infinite, Q.positive, + Q.positive_infinite, Q.prime])), + Q.nonzero: (set([Q.commutative, Q.complex, Q.extended_nonzero, + Q.extended_real, Q.finite, Q.hermitian, Q.nonzero, Q.real]), + set([Q.imaginary, Q.infinite, Q.negative_infinite, + Q.positive_infinite, Q.zero])), + Q.normal: (set([Q.normal, Q.square]), set([])), + Q.odd: (set([Q.algebraic, Q.commutative, Q.complex, + Q.extended_nonzero, Q.extended_real, Q.finite, Q.hermitian, + Q.integer, Q.nonzero, Q.odd, Q.rational, Q.real]), + set([Q.even, Q.imaginary, Q.infinite, Q.irrational, + Q.negative_infinite, Q.positive_infinite, Q.transcendental, + Q.zero])), + Q.orthogonal: (set([Q.fullrank, Q.invertible, Q.normal, Q.orthogonal, + Q.positive_definite, Q.square, Q.unitary]), set([Q.singular])), + Q.positive: (set([Q.commutative, Q.complex, Q.extended_nonnegative, + Q.extended_nonzero, Q.extended_positive, Q.extended_real, + Q.finite, Q.hermitian, Q.nonnegative, Q.nonzero, Q.positive, + Q.real]), set([Q.extended_negative, Q.extended_nonpositive, + Q.imaginary, Q.infinite, Q.negative, Q.negative_infinite, + Q.nonpositive, Q.positive_infinite, Q.zero])), + Q.positive_definite: (set([Q.fullrank, Q.invertible, + Q.positive_definite, Q.square]), set([Q.singular])), + Q.positive_infinite: (set([Q.commutative, Q.extended_nonnegative, + Q.extended_nonzero, Q.extended_positive, Q.extended_real, + Q.infinite, Q.positive_infinite]), set([Q.algebraic, + Q.complex, Q.composite, Q.even, Q.extended_negative, + Q.extended_nonpositive, Q.finite, Q.imaginary, Q.integer, + Q.irrational, Q.negative, Q.negative_infinite, Q.nonnegative, + Q.nonpositive, Q.nonzero, Q.odd, Q.positive, Q.prime, + Q.rational, Q.real, Q.transcendental, Q.zero])), + Q.prime: (set([Q.algebraic, Q.commutative, Q.complex, + Q.extended_nonnegative, Q.extended_nonzero, + Q.extended_positive, Q.extended_real, Q.finite, Q.hermitian, + Q.integer, Q.nonnegative, Q.nonzero, Q.positive, Q.prime, + Q.rational, Q.real]), set([Q.composite, Q.extended_negative, + Q.extended_nonpositive, Q.imaginary, Q.infinite, Q.irrational, + Q.negative, Q.negative_infinite, Q.nonpositive, + Q.positive_infinite, Q.transcendental, Q.zero])), + Q.rational: (set([Q.algebraic, Q.commutative, Q.complex, + Q.extended_real, Q.finite, Q.hermitian, Q.rational, Q.real]), + set([Q.imaginary, Q.infinite, Q.irrational, + Q.negative_infinite, Q.positive_infinite, Q.transcendental])), + Q.real: (set([Q.commutative, Q.complex, Q.extended_real, Q.finite, + Q.hermitian, Q.real]), set([Q.imaginary, Q.infinite, + Q.negative_infinite, Q.positive_infinite])), + Q.real_elements: (set([Q.complex_elements, Q.real_elements]), set([])), + Q.singular: (set([Q.singular]), set([Q.invertible, Q.orthogonal, + Q.positive_definite, Q.unitary])), + Q.square: (set([Q.square]), set([])), + Q.symmetric: (set([Q.square, Q.symmetric]), set([])), + Q.transcendental: (set([Q.commutative, Q.complex, Q.finite, + Q.transcendental]), set([Q.algebraic, Q.composite, Q.even, + Q.infinite, Q.integer, Q.negative_infinite, Q.odd, + Q.positive_infinite, Q.prime, Q.rational, Q.zero])), + Q.triangular: (set([Q.triangular]), set([])), + Q.unit_triangular: (set([Q.triangular, Q.unit_triangular]), set([])), + Q.unitary: (set([Q.fullrank, Q.invertible, Q.normal, Q.square, + Q.unitary]), set([Q.singular])), + Q.upper_triangular: (set([Q.triangular, Q.upper_triangular]), set([])), + Q.zero: (set([Q.algebraic, Q.commutative, Q.complex, Q.even, + Q.extended_nonnegative, Q.extended_nonpositive, + Q.extended_real, Q.finite, Q.hermitian, Q.integer, + Q.nonnegative, Q.nonpositive, Q.rational, Q.real, Q.zero]), + set([Q.composite, Q.extended_negative, Q.extended_nonzero, + Q.extended_positive, Q.imaginary, Q.infinite, Q.irrational, + Q.negative, Q.negative_infinite, Q.nonzero, Q.odd, Q.positive, + Q.positive_infinite, Q.prime, Q.transcendental])), + } diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/assume.py b/venv/lib/python3.11/site-packages/sympy/assumptions/assume.py new file mode 100644 index 0000000000000000000000000000000000000000..743195a865a1d39389d471b95728ca79834ed019 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/assume.py @@ -0,0 +1,485 @@ +"""A module which implements predicates and assumption context.""" + +from contextlib import contextmanager +import inspect +from sympy.core.symbol import Str +from sympy.core.sympify import _sympify +from sympy.logic.boolalg import Boolean, false, true +from sympy.multipledispatch.dispatcher import Dispatcher, str_signature +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.utilities.iterables import is_sequence +from sympy.utilities.source import get_class + + +class AssumptionsContext(set): + """ + Set containing default assumptions which are applied to the ``ask()`` + function. + + Explanation + =========== + + This is used to represent global assumptions, but you can also use this + class to create your own local assumptions contexts. It is basically a thin + wrapper to Python's set, so see its documentation for advanced usage. + + Examples + ======== + + The default assumption context is ``global_assumptions``, which is initially empty: + + >>> from sympy import ask, Q + >>> from sympy.assumptions import global_assumptions + >>> global_assumptions + AssumptionsContext() + + You can add default assumptions: + + >>> from sympy.abc import x + >>> global_assumptions.add(Q.real(x)) + >>> global_assumptions + AssumptionsContext({Q.real(x)}) + >>> ask(Q.real(x)) + True + + And remove them: + + >>> global_assumptions.remove(Q.real(x)) + >>> print(ask(Q.real(x))) + None + + The ``clear()`` method removes every assumption: + + >>> global_assumptions.add(Q.positive(x)) + >>> global_assumptions + AssumptionsContext({Q.positive(x)}) + >>> global_assumptions.clear() + >>> global_assumptions + AssumptionsContext() + + See Also + ======== + + assuming + + """ + + def add(self, *assumptions): + """Add assumptions.""" + for a in assumptions: + super().add(a) + + def _sympystr(self, printer): + if not self: + return "%s()" % self.__class__.__name__ + return "{}({})".format(self.__class__.__name__, printer._print_set(self)) + +global_assumptions = AssumptionsContext() + + +class AppliedPredicate(Boolean): + """ + The class of expressions resulting from applying ``Predicate`` to + the arguments. ``AppliedPredicate`` merely wraps its argument and + remain unevaluated. To evaluate it, use the ``ask()`` function. + + Examples + ======== + + >>> from sympy import Q, ask + >>> Q.integer(1) + Q.integer(1) + + The ``function`` attribute returns the predicate, and the ``arguments`` + attribute returns the tuple of arguments. + + >>> type(Q.integer(1)) + + >>> Q.integer(1).function + Q.integer + >>> Q.integer(1).arguments + (1,) + + Applied predicates can be evaluated to a boolean value with ``ask``: + + >>> ask(Q.integer(1)) + True + + """ + __slots__ = () + + def __new__(cls, predicate, *args): + if not isinstance(predicate, Predicate): + raise TypeError("%s is not a Predicate." % predicate) + args = map(_sympify, args) + return super().__new__(cls, predicate, *args) + + @property + def arg(self): + """ + Return the expression used by this assumption. + + Examples + ======== + + >>> from sympy import Q, Symbol + >>> x = Symbol('x') + >>> a = Q.integer(x + 1) + >>> a.arg + x + 1 + + """ + # Will be deprecated + args = self._args + if len(args) == 2: + # backwards compatibility + return args[1] + raise TypeError("'arg' property is allowed only for unary predicates.") + + @property + def function(self): + """ + Return the predicate. + """ + # Will be changed to self.args[0] after args overriding is removed + return self._args[0] + + @property + def arguments(self): + """ + Return the arguments which are applied to the predicate. + """ + # Will be changed to self.args[1:] after args overriding is removed + return self._args[1:] + + def _eval_ask(self, assumptions): + return self.function.eval(self.arguments, assumptions) + + @property + def binary_symbols(self): + from .ask import Q + if self.function == Q.is_true: + i = self.arguments[0] + if i.is_Boolean or i.is_Symbol: + return i.binary_symbols + if self.function in (Q.eq, Q.ne): + if true in self.arguments or false in self.arguments: + if self.arguments[0].is_Symbol: + return {self.arguments[0]} + elif self.arguments[1].is_Symbol: + return {self.arguments[1]} + return set() + + +class PredicateMeta(type): + def __new__(cls, clsname, bases, dct): + # If handler is not defined, assign empty dispatcher. + if "handler" not in dct: + name = f"Ask{clsname.capitalize()}Handler" + handler = Dispatcher(name, doc="Handler for key %s" % name) + dct["handler"] = handler + + dct["_orig_doc"] = dct.get("__doc__", "") + + return super().__new__(cls, clsname, bases, dct) + + @property + def __doc__(cls): + handler = cls.handler + doc = cls._orig_doc + if cls is not Predicate and handler is not None: + doc += "Handler\n" + doc += " =======\n\n" + + # Append the handler's doc without breaking sphinx documentation. + docs = [" Multiply dispatched method: %s" % handler.name] + if handler.doc: + for line in handler.doc.splitlines(): + if not line: + continue + docs.append(" %s" % line) + other = [] + for sig in handler.ordering[::-1]: + func = handler.funcs[sig] + if func.__doc__: + s = ' Inputs: <%s>' % str_signature(sig) + lines = [] + for line in func.__doc__.splitlines(): + lines.append(" %s" % line) + s += "\n".join(lines) + docs.append(s) + else: + other.append(str_signature(sig)) + if other: + othersig = " Other signatures:" + for line in other: + othersig += "\n * %s" % line + docs.append(othersig) + + doc += '\n\n'.join(docs) + + return doc + + +class Predicate(Boolean, metaclass=PredicateMeta): + """ + Base class for mathematical predicates. It also serves as a + constructor for undefined predicate objects. + + Explanation + =========== + + Predicate is a function that returns a boolean value [1]. + + Predicate function is object, and it is instance of predicate class. + When a predicate is applied to arguments, ``AppliedPredicate`` + instance is returned. This merely wraps the argument and remain + unevaluated. To obtain the truth value of applied predicate, use the + function ``ask``. + + Evaluation of predicate is done by multiple dispatching. You can + register new handler to the predicate to support new types. + + Every predicate in SymPy can be accessed via the property of ``Q``. + For example, ``Q.even`` returns the predicate which checks if the + argument is even number. + + To define a predicate which can be evaluated, you must subclass this + class, make an instance of it, and register it to ``Q``. After then, + dispatch the handler by argument types. + + If you directly construct predicate using this class, you will get + ``UndefinedPredicate`` which cannot be dispatched. This is useful + when you are building boolean expressions which do not need to be + evaluated. + + Examples + ======== + + Applying and evaluating to boolean value: + + >>> from sympy import Q, ask + >>> ask(Q.prime(7)) + True + + You can define a new predicate by subclassing and dispatching. Here, + we define a predicate for sexy primes [2] as an example. + + >>> from sympy import Predicate, Integer + >>> class SexyPrimePredicate(Predicate): + ... name = "sexyprime" + >>> Q.sexyprime = SexyPrimePredicate() + >>> @Q.sexyprime.register(Integer, Integer) + ... def _(int1, int2, assumptions): + ... args = sorted([int1, int2]) + ... if not all(ask(Q.prime(a), assumptions) for a in args): + ... return False + ... return args[1] - args[0] == 6 + >>> ask(Q.sexyprime(5, 11)) + True + + Direct constructing returns ``UndefinedPredicate``, which can be + applied but cannot be dispatched. + + >>> from sympy import Predicate, Integer + >>> Q.P = Predicate("P") + >>> type(Q.P) + + >>> Q.P(1) + Q.P(1) + >>> Q.P.register(Integer)(lambda expr, assump: True) + Traceback (most recent call last): + ... + TypeError: cannot be dispatched. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Predicate_%28mathematical_logic%29 + .. [2] https://en.wikipedia.org/wiki/Sexy_prime + + """ + + is_Atom = True + + def __new__(cls, *args, **kwargs): + if cls is Predicate: + return UndefinedPredicate(*args, **kwargs) + obj = super().__new__(cls, *args) + return obj + + @property + def name(self): + # May be overridden + return type(self).__name__ + + @classmethod + def register(cls, *types, **kwargs): + """ + Register the signature to the handler. + """ + if cls.handler is None: + raise TypeError("%s cannot be dispatched." % type(cls)) + return cls.handler.register(*types, **kwargs) + + @classmethod + def register_many(cls, *types, **kwargs): + """ + Register multiple signatures to same handler. + """ + def _(func): + for t in types: + if not is_sequence(t): + t = (t,) # for convenience, allow passing `type` to mean `(type,)` + cls.register(*t, **kwargs)(func) + return _ + + def __call__(self, *args): + return AppliedPredicate(self, *args) + + def eval(self, args, assumptions=True): + """ + Evaluate ``self(*args)`` under the given assumptions. + + This uses only direct resolution methods, not logical inference. + """ + result = None + try: + result = self.handler(*args, assumptions=assumptions) + except NotImplementedError: + pass + return result + + def _eval_refine(self, assumptions): + # When Predicate is no longer Boolean, delete this method + return self + + +class UndefinedPredicate(Predicate): + """ + Predicate without handler. + + Explanation + =========== + + This predicate is generated by using ``Predicate`` directly for + construction. It does not have a handler, and evaluating this with + arguments is done by SAT solver. + + Examples + ======== + + >>> from sympy import Predicate, Q + >>> Q.P = Predicate('P') + >>> Q.P.func + + >>> Q.P.name + Str('P') + + """ + + handler = None + + def __new__(cls, name, handlers=None): + # "handlers" parameter supports old design + if not isinstance(name, Str): + name = Str(name) + obj = super(Boolean, cls).__new__(cls, name) + obj.handlers = handlers or [] + return obj + + @property + def name(self): + return self.args[0] + + def _hashable_content(self): + return (self.name,) + + def __getnewargs__(self): + return (self.name,) + + def __call__(self, expr): + return AppliedPredicate(self, expr) + + def add_handler(self, handler): + sympy_deprecation_warning( + """ + The AskHandler system is deprecated. Predicate.add_handler() + should be replaced with the multipledispatch handler of Predicate. + """, + deprecated_since_version="1.8", + active_deprecations_target='deprecated-askhandler', + ) + self.handlers.append(handler) + + def remove_handler(self, handler): + sympy_deprecation_warning( + """ + The AskHandler system is deprecated. Predicate.remove_handler() + should be replaced with the multipledispatch handler of Predicate. + """, + deprecated_since_version="1.8", + active_deprecations_target='deprecated-askhandler', + ) + self.handlers.remove(handler) + + def eval(self, args, assumptions=True): + # Support for deprecated design + # When old design is removed, this will always return None + sympy_deprecation_warning( + """ + The AskHandler system is deprecated. Evaluating UndefinedPredicate + objects should be replaced with the multipledispatch handler of + Predicate. + """, + deprecated_since_version="1.8", + active_deprecations_target='deprecated-askhandler', + stacklevel=5, + ) + expr, = args + res, _res = None, None + mro = inspect.getmro(type(expr)) + for handler in self.handlers: + cls = get_class(handler) + for subclass in mro: + eval_ = getattr(cls, subclass.__name__, None) + if eval_ is None: + continue + res = eval_(expr, assumptions) + # Do not stop if value returned is None + # Try to check for higher classes + if res is None: + continue + if _res is None: + _res = res + else: + # only check consistency if both resolutors have concluded + if _res != res: + raise ValueError('incompatible resolutors') + break + return res + + +@contextmanager +def assuming(*assumptions): + """ + Context manager for assumptions. + + Examples + ======== + + >>> from sympy import assuming, Q, ask + >>> from sympy.abc import x, y + >>> print(ask(Q.integer(x + y))) + None + >>> with assuming(Q.integer(x), Q.integer(y)): + ... print(ask(Q.integer(x + y))) + True + """ + old_global_assumptions = global_assumptions.copy() + global_assumptions.update(assumptions) + try: + yield + finally: + global_assumptions.clear() + global_assumptions.update(old_global_assumptions) diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/cnf.py b/venv/lib/python3.11/site-packages/sympy/assumptions/cnf.py new file mode 100644 index 0000000000000000000000000000000000000000..a95d27bed6eeb64c42f4edd9d49bd8e5753069e5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/cnf.py @@ -0,0 +1,445 @@ +""" +The classes used here are for the internal use of assumptions system +only and should not be used anywhere else as these do not possess the +signatures common to SymPy objects. For general use of logic constructs +please refer to sympy.logic classes And, Or, Not, etc. +""" +from itertools import combinations, product, zip_longest +from sympy.assumptions.assume import AppliedPredicate, Predicate +from sympy.core.relational import Eq, Ne, Gt, Lt, Ge, Le +from sympy.core.singleton import S +from sympy.logic.boolalg import Or, And, Not, Xnor +from sympy.logic.boolalg import (Equivalent, ITE, Implies, Nand, Nor, Xor) + + +class Literal: + """ + The smallest element of a CNF object. + + Parameters + ========== + + lit : Boolean expression + + is_Not : bool + + Examples + ======== + + >>> from sympy import Q + >>> from sympy.assumptions.cnf import Literal + >>> from sympy.abc import x + >>> Literal(Q.even(x)) + Literal(Q.even(x), False) + >>> Literal(~Q.even(x)) + Literal(Q.even(x), True) + """ + + def __new__(cls, lit, is_Not=False): + if isinstance(lit, Not): + lit = lit.args[0] + is_Not = True + elif isinstance(lit, (AND, OR, Literal)): + return ~lit if is_Not else lit + obj = super().__new__(cls) + obj.lit = lit + obj.is_Not = is_Not + return obj + + @property + def arg(self): + return self.lit + + def rcall(self, expr): + if callable(self.lit): + lit = self.lit(expr) + else: + lit = self.lit.apply(expr) + return type(self)(lit, self.is_Not) + + def __invert__(self): + is_Not = not self.is_Not + return Literal(self.lit, is_Not) + + def __str__(self): + return '{}({}, {})'.format(type(self).__name__, self.lit, self.is_Not) + + __repr__ = __str__ + + def __eq__(self, other): + return self.arg == other.arg and self.is_Not == other.is_Not + + def __hash__(self): + h = hash((type(self).__name__, self.arg, self.is_Not)) + return h + + +class OR: + """ + A low-level implementation for Or + """ + def __init__(self, *args): + self._args = args + + @property + def args(self): + return sorted(self._args, key=str) + + def rcall(self, expr): + return type(self)(*[arg.rcall(expr) + for arg in self._args + ]) + + def __invert__(self): + return AND(*[~arg for arg in self._args]) + + def __hash__(self): + return hash((type(self).__name__,) + tuple(self.args)) + + def __eq__(self, other): + return self.args == other.args + + def __str__(self): + s = '(' + ' | '.join([str(arg) for arg in self.args]) + ')' + return s + + __repr__ = __str__ + + +class AND: + """ + A low-level implementation for And + """ + def __init__(self, *args): + self._args = args + + def __invert__(self): + return OR(*[~arg for arg in self._args]) + + @property + def args(self): + return sorted(self._args, key=str) + + def rcall(self, expr): + return type(self)(*[arg.rcall(expr) + for arg in self._args + ]) + + def __hash__(self): + return hash((type(self).__name__,) + tuple(self.args)) + + def __eq__(self, other): + return self.args == other.args + + def __str__(self): + s = '('+' & '.join([str(arg) for arg in self.args])+')' + return s + + __repr__ = __str__ + + +def to_NNF(expr, composite_map=None): + """ + Generates the Negation Normal Form of any boolean expression in terms + of AND, OR, and Literal objects. + + Examples + ======== + + >>> from sympy import Q, Eq + >>> from sympy.assumptions.cnf import to_NNF + >>> from sympy.abc import x, y + >>> expr = Q.even(x) & ~Q.positive(x) + >>> to_NNF(expr) + (Literal(Q.even(x), False) & Literal(Q.positive(x), True)) + + Supported boolean objects are converted to corresponding predicates. + + >>> to_NNF(Eq(x, y)) + Literal(Q.eq(x, y), False) + + If ``composite_map`` argument is given, ``to_NNF`` decomposes the + specified predicate into a combination of primitive predicates. + + >>> cmap = {Q.nonpositive: Q.negative | Q.zero} + >>> to_NNF(Q.nonpositive, cmap) + (Literal(Q.negative, False) | Literal(Q.zero, False)) + >>> to_NNF(Q.nonpositive(x), cmap) + (Literal(Q.negative(x), False) | Literal(Q.zero(x), False)) + """ + from sympy.assumptions.ask import Q + + if composite_map is None: + composite_map = {} + + + binrelpreds = {Eq: Q.eq, Ne: Q.ne, Gt: Q.gt, Lt: Q.lt, Ge: Q.ge, Le: Q.le} + if type(expr) in binrelpreds: + pred = binrelpreds[type(expr)] + expr = pred(*expr.args) + + if isinstance(expr, Not): + arg = expr.args[0] + tmp = to_NNF(arg, composite_map) # Strategy: negate the NNF of expr + return ~tmp + + if isinstance(expr, Or): + return OR(*[to_NNF(x, composite_map) for x in Or.make_args(expr)]) + + if isinstance(expr, And): + return AND(*[to_NNF(x, composite_map) for x in And.make_args(expr)]) + + if isinstance(expr, Nand): + tmp = AND(*[to_NNF(x, composite_map) for x in expr.args]) + return ~tmp + + if isinstance(expr, Nor): + tmp = OR(*[to_NNF(x, composite_map) for x in expr.args]) + return ~tmp + + if isinstance(expr, Xor): + cnfs = [] + for i in range(0, len(expr.args) + 1, 2): + for neg in combinations(expr.args, i): + clause = [~to_NNF(s, composite_map) if s in neg else to_NNF(s, composite_map) + for s in expr.args] + cnfs.append(OR(*clause)) + return AND(*cnfs) + + if isinstance(expr, Xnor): + cnfs = [] + for i in range(0, len(expr.args) + 1, 2): + for neg in combinations(expr.args, i): + clause = [~to_NNF(s, composite_map) if s in neg else to_NNF(s, composite_map) + for s in expr.args] + cnfs.append(OR(*clause)) + return ~AND(*cnfs) + + if isinstance(expr, Implies): + L, R = to_NNF(expr.args[0], composite_map), to_NNF(expr.args[1], composite_map) + return OR(~L, R) + + if isinstance(expr, Equivalent): + cnfs = [] + for a, b in zip_longest(expr.args, expr.args[1:], fillvalue=expr.args[0]): + a = to_NNF(a, composite_map) + b = to_NNF(b, composite_map) + cnfs.append(OR(~a, b)) + return AND(*cnfs) + + if isinstance(expr, ITE): + L = to_NNF(expr.args[0], composite_map) + M = to_NNF(expr.args[1], composite_map) + R = to_NNF(expr.args[2], composite_map) + return AND(OR(~L, M), OR(L, R)) + + if isinstance(expr, AppliedPredicate): + pred, args = expr.function, expr.arguments + newpred = composite_map.get(pred, None) + if newpred is not None: + return to_NNF(newpred.rcall(*args), composite_map) + + if isinstance(expr, Predicate): + newpred = composite_map.get(expr, None) + if newpred is not None: + return to_NNF(newpred, composite_map) + + return Literal(expr) + + +def distribute_AND_over_OR(expr): + """ + Distributes AND over OR in the NNF expression. + Returns the result( Conjunctive Normal Form of expression) + as a CNF object. + """ + if not isinstance(expr, (AND, OR)): + tmp = set() + tmp.add(frozenset((expr,))) + return CNF(tmp) + + if isinstance(expr, OR): + return CNF.all_or(*[distribute_AND_over_OR(arg) + for arg in expr._args]) + + if isinstance(expr, AND): + return CNF.all_and(*[distribute_AND_over_OR(arg) + for arg in expr._args]) + + +class CNF: + """ + Class to represent CNF of a Boolean expression. + Consists of set of clauses, which themselves are stored as + frozenset of Literal objects. + + Examples + ======== + + >>> from sympy import Q + >>> from sympy.assumptions.cnf import CNF + >>> from sympy.abc import x + >>> cnf = CNF.from_prop(Q.real(x) & ~Q.zero(x)) + >>> cnf.clauses + {frozenset({Literal(Q.zero(x), True)}), + frozenset({Literal(Q.negative(x), False), + Literal(Q.positive(x), False), Literal(Q.zero(x), False)})} + """ + def __init__(self, clauses=None): + if not clauses: + clauses = set() + self.clauses = clauses + + def add(self, prop): + clauses = CNF.to_CNF(prop).clauses + self.add_clauses(clauses) + + def __str__(self): + s = ' & '.join( + ['(' + ' | '.join([str(lit) for lit in clause]) +')' + for clause in self.clauses] + ) + return s + + def extend(self, props): + for p in props: + self.add(p) + return self + + def copy(self): + return CNF(set(self.clauses)) + + def add_clauses(self, clauses): + self.clauses |= clauses + + @classmethod + def from_prop(cls, prop): + res = cls() + res.add(prop) + return res + + def __iand__(self, other): + self.add_clauses(other.clauses) + return self + + def all_predicates(self): + predicates = set() + for c in self.clauses: + predicates |= {arg.lit for arg in c} + return predicates + + def _or(self, cnf): + clauses = set() + for a, b in product(self.clauses, cnf.clauses): + tmp = set(a) + tmp.update(b) + clauses.add(frozenset(tmp)) + return CNF(clauses) + + def _and(self, cnf): + clauses = self.clauses.union(cnf.clauses) + return CNF(clauses) + + def _not(self): + clss = list(self.clauses) + ll = {frozenset((~x,)) for x in clss[-1]} + ll = CNF(ll) + + for rest in clss[:-1]: + p = {frozenset((~x,)) for x in rest} + ll = ll._or(CNF(p)) + return ll + + def rcall(self, expr): + clause_list = [] + for clause in self.clauses: + lits = [arg.rcall(expr) for arg in clause] + clause_list.append(OR(*lits)) + expr = AND(*clause_list) + return distribute_AND_over_OR(expr) + + @classmethod + def all_or(cls, *cnfs): + b = cnfs[0].copy() + for rest in cnfs[1:]: + b = b._or(rest) + return b + + @classmethod + def all_and(cls, *cnfs): + b = cnfs[0].copy() + for rest in cnfs[1:]: + b = b._and(rest) + return b + + @classmethod + def to_CNF(cls, expr): + from sympy.assumptions.facts import get_composite_predicates + expr = to_NNF(expr, get_composite_predicates()) + expr = distribute_AND_over_OR(expr) + return expr + + @classmethod + def CNF_to_cnf(cls, cnf): + """ + Converts CNF object to SymPy's boolean expression + retaining the form of expression. + """ + def remove_literal(arg): + return Not(arg.lit) if arg.is_Not else arg.lit + + return And(*(Or(*(remove_literal(arg) for arg in clause)) for clause in cnf.clauses)) + + +class EncodedCNF: + """ + Class for encoding the CNF expression. + """ + def __init__(self, data=None, encoding=None): + if not data and not encoding: + data = [] + encoding = {} + self.data = data + self.encoding = encoding + self._symbols = list(encoding.keys()) + + def from_cnf(self, cnf): + self._symbols = list(cnf.all_predicates()) + n = len(self._symbols) + self.encoding = dict(zip(self._symbols, range(1, n + 1))) + self.data = [self.encode(clause) for clause in cnf.clauses] + + @property + def symbols(self): + return self._symbols + + @property + def variables(self): + return range(1, len(self._symbols) + 1) + + def copy(self): + new_data = [set(clause) for clause in self.data] + return EncodedCNF(new_data, dict(self.encoding)) + + def add_prop(self, prop): + cnf = CNF.from_prop(prop) + self.add_from_cnf(cnf) + + def add_from_cnf(self, cnf): + clauses = [self.encode(clause) for clause in cnf.clauses] + self.data += clauses + + def encode_arg(self, arg): + literal = arg.lit + value = self.encoding.get(literal, None) + if value is None: + n = len(self._symbols) + self._symbols.append(literal) + value = self.encoding[literal] = n + 1 + if arg.is_Not: + return -value + else: + return value + + def encode(self, clause): + return {self.encode_arg(arg) if not arg.lit == S.false else 0 for arg in clause} diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/facts.py b/venv/lib/python3.11/site-packages/sympy/assumptions/facts.py new file mode 100644 index 0000000000000000000000000000000000000000..2ff268677cf74e252ac6c3bc3eecbea08b9414d0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/facts.py @@ -0,0 +1,270 @@ +""" +Known facts in assumptions module. + +This module defines the facts between unary predicates in ``get_known_facts()``, +and supports functions to generate the contents in +``sympy.assumptions.ask_generated`` file. +""" + +from sympy.assumptions.ask import Q +from sympy.assumptions.assume import AppliedPredicate +from sympy.core.cache import cacheit +from sympy.core.symbol import Symbol +from sympy.logic.boolalg import (to_cnf, And, Not, Implies, Equivalent, + Exclusive,) +from sympy.logic.inference import satisfiable + + +@cacheit +def get_composite_predicates(): + # To reduce the complexity of sat solver, these predicates are + # transformed into the combination of primitive predicates. + return { + Q.real : Q.negative | Q.zero | Q.positive, + Q.integer : Q.even | Q.odd, + Q.nonpositive : Q.negative | Q.zero, + Q.nonzero : Q.negative | Q.positive, + Q.nonnegative : Q.zero | Q.positive, + Q.extended_real : Q.negative_infinite | Q.negative | Q.zero | Q.positive | Q.positive_infinite, + Q.extended_positive: Q.positive | Q.positive_infinite, + Q.extended_negative: Q.negative | Q.negative_infinite, + Q.extended_nonzero: Q.negative_infinite | Q.negative | Q.positive | Q.positive_infinite, + Q.extended_nonpositive: Q.negative_infinite | Q.negative | Q.zero, + Q.extended_nonnegative: Q.zero | Q.positive | Q.positive_infinite, + Q.complex : Q.algebraic | Q.transcendental + } + + +@cacheit +def get_known_facts(x=None): + """ + Facts between unary predicates. + + Parameters + ========== + + x : Symbol, optional + Placeholder symbol for unary facts. Default is ``Symbol('x')``. + + Returns + ======= + + fact : Known facts in conjugated normal form. + + """ + if x is None: + x = Symbol('x') + + fact = And( + get_number_facts(x), + get_matrix_facts(x) + ) + return fact + + +@cacheit +def get_number_facts(x = None): + """ + Facts between unary number predicates. + + Parameters + ========== + + x : Symbol, optional + Placeholder symbol for unary facts. Default is ``Symbol('x')``. + + Returns + ======= + + fact : Known facts in conjugated normal form. + + """ + if x is None: + x = Symbol('x') + + fact = And( + # primitive predicates for extended real exclude each other. + Exclusive(Q.negative_infinite(x), Q.negative(x), Q.zero(x), + Q.positive(x), Q.positive_infinite(x)), + + # build complex plane + Exclusive(Q.real(x), Q.imaginary(x)), + Implies(Q.real(x) | Q.imaginary(x), Q.complex(x)), + + # other subsets of complex + Exclusive(Q.transcendental(x), Q.algebraic(x)), + Equivalent(Q.real(x), Q.rational(x) | Q.irrational(x)), + Exclusive(Q.irrational(x), Q.rational(x)), + Implies(Q.rational(x), Q.algebraic(x)), + + # integers + Exclusive(Q.even(x), Q.odd(x)), + Implies(Q.integer(x), Q.rational(x)), + Implies(Q.zero(x), Q.even(x)), + Exclusive(Q.composite(x), Q.prime(x)), + Implies(Q.composite(x) | Q.prime(x), Q.integer(x) & Q.positive(x)), + Implies(Q.even(x) & Q.positive(x) & ~Q.prime(x), Q.composite(x)), + + # hermitian and antihermitian + Implies(Q.real(x), Q.hermitian(x)), + Implies(Q.imaginary(x), Q.antihermitian(x)), + Implies(Q.zero(x), Q.hermitian(x) | Q.antihermitian(x)), + + # define finity and infinity, and build extended real line + Exclusive(Q.infinite(x), Q.finite(x)), + Implies(Q.complex(x), Q.finite(x)), + Implies(Q.negative_infinite(x) | Q.positive_infinite(x), Q.infinite(x)), + + # commutativity + Implies(Q.finite(x) | Q.infinite(x), Q.commutative(x)), + ) + return fact + + +@cacheit +def get_matrix_facts(x = None): + """ + Facts between unary matrix predicates. + + Parameters + ========== + + x : Symbol, optional + Placeholder symbol for unary facts. Default is ``Symbol('x')``. + + Returns + ======= + + fact : Known facts in conjugated normal form. + + """ + if x is None: + x = Symbol('x') + + fact = And( + # matrices + Implies(Q.orthogonal(x), Q.positive_definite(x)), + Implies(Q.orthogonal(x), Q.unitary(x)), + Implies(Q.unitary(x) & Q.real_elements(x), Q.orthogonal(x)), + Implies(Q.unitary(x), Q.normal(x)), + Implies(Q.unitary(x), Q.invertible(x)), + Implies(Q.normal(x), Q.square(x)), + Implies(Q.diagonal(x), Q.normal(x)), + Implies(Q.positive_definite(x), Q.invertible(x)), + Implies(Q.diagonal(x), Q.upper_triangular(x)), + Implies(Q.diagonal(x), Q.lower_triangular(x)), + Implies(Q.lower_triangular(x), Q.triangular(x)), + Implies(Q.upper_triangular(x), Q.triangular(x)), + Implies(Q.triangular(x), Q.upper_triangular(x) | Q.lower_triangular(x)), + Implies(Q.upper_triangular(x) & Q.lower_triangular(x), Q.diagonal(x)), + Implies(Q.diagonal(x), Q.symmetric(x)), + Implies(Q.unit_triangular(x), Q.triangular(x)), + Implies(Q.invertible(x), Q.fullrank(x)), + Implies(Q.invertible(x), Q.square(x)), + Implies(Q.symmetric(x), Q.square(x)), + Implies(Q.fullrank(x) & Q.square(x), Q.invertible(x)), + Equivalent(Q.invertible(x), ~Q.singular(x)), + Implies(Q.integer_elements(x), Q.real_elements(x)), + Implies(Q.real_elements(x), Q.complex_elements(x)), + ) + return fact + + + +def generate_known_facts_dict(keys, fact): + """ + Computes and returns a dictionary which contains the relations between + unary predicates. + + Each key is a predicate, and item is two groups of predicates. + First group contains the predicates which are implied by the key, and + second group contains the predicates which are rejected by the key. + + All predicates in *keys* and *fact* must be unary and have same placeholder + symbol. + + Parameters + ========== + + keys : list of AppliedPredicate instances. + + fact : Fact between predicates in conjugated normal form. + + Examples + ======== + + >>> from sympy import Q, And, Implies + >>> from sympy.assumptions.facts import generate_known_facts_dict + >>> from sympy.abc import x + >>> keys = [Q.even(x), Q.odd(x), Q.zero(x)] + >>> fact = And(Implies(Q.even(x), ~Q.odd(x)), + ... Implies(Q.zero(x), Q.even(x))) + >>> generate_known_facts_dict(keys, fact) + {Q.even: ({Q.even}, {Q.odd}), + Q.odd: ({Q.odd}, {Q.even, Q.zero}), + Q.zero: ({Q.even, Q.zero}, {Q.odd})} + """ + fact_cnf = to_cnf(fact) + mapping = single_fact_lookup(keys, fact_cnf) + + ret = {} + for key, value in mapping.items(): + implied = set() + rejected = set() + for expr in value: + if isinstance(expr, AppliedPredicate): + implied.add(expr.function) + elif isinstance(expr, Not): + pred = expr.args[0] + rejected.add(pred.function) + ret[key.function] = (implied, rejected) + return ret + + +@cacheit +def get_known_facts_keys(): + """ + Return every unary predicates registered to ``Q``. + + This function is used to generate the keys for + ``generate_known_facts_dict``. + + """ + # exclude polyadic predicates + exclude = {Q.eq, Q.ne, Q.gt, Q.lt, Q.ge, Q.le} + + result = [] + for attr in Q.__class__.__dict__: + if attr.startswith('__'): + continue + pred = getattr(Q, attr) + if pred in exclude: + continue + result.append(pred) + return result + + +def single_fact_lookup(known_facts_keys, known_facts_cnf): + # Return the dictionary for quick lookup of single fact + mapping = {} + for key in known_facts_keys: + mapping[key] = {key} + for other_key in known_facts_keys: + if other_key != key: + if ask_full_inference(other_key, key, known_facts_cnf): + mapping[key].add(other_key) + if ask_full_inference(~other_key, key, known_facts_cnf): + mapping[key].add(~other_key) + return mapping + + +def ask_full_inference(proposition, assumptions, known_facts_cnf): + """ + Method for inferring properties about objects. + + """ + if not satisfiable(And(known_facts_cnf, assumptions, proposition)): + return False + if not satisfiable(And(known_facts_cnf, assumptions, Not(proposition))): + return True + return None diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/__init__.py b/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0fbe618eb8b43e252ac8fb0baf1eeee22bf347cc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/__init__.py @@ -0,0 +1,13 @@ +""" +Multipledispatch handlers for ``Predicate`` are implemented here. +Handlers in this module are not directly imported to other modules in +order to avoid circular import problem. +""" + +from .common import (AskHandler, CommonHandler, + test_closed_group) + +__all__ = [ + 'AskHandler', 'CommonHandler', + 'test_closed_group' +] diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/calculus.py b/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/calculus.py new file mode 100644 index 0000000000000000000000000000000000000000..e2b9c43ccea216988a25aa671ea23bc81d2209ff --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/calculus.py @@ -0,0 +1,273 @@ +""" +This module contains query handlers responsible for calculus queries: +infinitesimal, finite, etc. +""" + +from sympy.assumptions import Q, ask +from sympy.core import Expr, Add, Mul, Pow, Symbol +from sympy.core.numbers import (NegativeInfinity, GoldenRatio, + Infinity, Exp1, ComplexInfinity, ImaginaryUnit, NaN, Number, Pi, E, + TribonacciConstant) +from sympy.functions import cos, exp, log, sign, sin +from sympy.logic.boolalg import conjuncts + +from ..predicates.calculus import (FinitePredicate, InfinitePredicate, + PositiveInfinitePredicate, NegativeInfinitePredicate) + + +# FinitePredicate + + +@FinitePredicate.register(Symbol) +def _(expr, assumptions): + """ + Handles Symbol. + """ + if expr.is_finite is not None: + return expr.is_finite + if Q.finite(expr) in conjuncts(assumptions): + return True + return None + +@FinitePredicate.register(Add) +def _(expr, assumptions): + """ + Return True if expr is bounded, False if not and None if unknown. + + Truth Table: + + +-------+-----+-----------+-----------+ + | | | | | + | | B | U | ? | + | | | | | + +-------+-----+---+---+---+---+---+---+ + | | | | | | | | | + | | |'+'|'-'|'x'|'+'|'-'|'x'| + | | | | | | | | | + +-------+-----+---+---+---+---+---+---+ + | | | | | + | B | B | U | ? | + | | | | | + +---+---+-----+---+---+---+---+---+---+ + | | | | | | | | | | + | |'+'| | U | ? | ? | U | ? | ? | + | | | | | | | | | | + | +---+-----+---+---+---+---+---+---+ + | | | | | | | | | | + | U |'-'| | ? | U | ? | ? | U | ? | + | | | | | | | | | | + | +---+-----+---+---+---+---+---+---+ + | | | | | | + | |'x'| | ? | ? | + | | | | | | + +---+---+-----+---+---+---+---+---+---+ + | | | | | + | ? | | | ? | + | | | | | + +-------+-----+-----------+---+---+---+ + + * 'B' = Bounded + + * 'U' = Unbounded + + * '?' = unknown boundedness + + * '+' = positive sign + + * '-' = negative sign + + * 'x' = sign unknown + + * All Bounded -> True + + * 1 Unbounded and the rest Bounded -> False + + * >1 Unbounded, all with same known sign -> False + + * Any Unknown and unknown sign -> None + + * Else -> None + + When the signs are not the same you can have an undefined + result as in oo - oo, hence 'bounded' is also undefined. + """ + sign = -1 # sign of unknown or infinite + result = True + for arg in expr.args: + _bounded = ask(Q.finite(arg), assumptions) + if _bounded: + continue + s = ask(Q.extended_positive(arg), assumptions) + # if there has been more than one sign or if the sign of this arg + # is None and Bounded is None or there was already + # an unknown sign, return None + if sign != -1 and s != sign or \ + s is None and None in (_bounded, sign): + return None + else: + sign = s + # once False, do not change + if result is not False: + result = _bounded + return result + +@FinitePredicate.register(Mul) +def _(expr, assumptions): + """ + Return True if expr is bounded, False if not and None if unknown. + + Truth Table: + + +---+---+---+--------+ + | | | | | + | | B | U | ? | + | | | | | + +---+---+---+---+----+ + | | | | | | + | | | | s | /s | + | | | | | | + +---+---+---+---+----+ + | | | | | + | B | B | U | ? | + | | | | | + +---+---+---+---+----+ + | | | | | | + | U | | U | U | ? | + | | | | | | + +---+---+---+---+----+ + | | | | | + | ? | | | ? | + | | | | | + +---+---+---+---+----+ + + * B = Bounded + + * U = Unbounded + + * ? = unknown boundedness + + * s = signed (hence nonzero) + + * /s = not signed + """ + result = True + possible_zero = False + for arg in expr.args: + _bounded = ask(Q.finite(arg), assumptions) + if _bounded: + if ask(Q.zero(arg), assumptions) is not False: + if result is False: + return None + possible_zero = True + elif _bounded is None: + if result is None: + return None + if ask(Q.extended_nonzero(arg), assumptions) is None: + return None + if result is not False: + result = None + else: + if possible_zero: + return None + result = False + return result + +@FinitePredicate.register(Pow) +def _(expr, assumptions): + """ + * Unbounded ** NonZero -> Unbounded + + * Bounded ** Bounded -> Bounded + + * Abs()<=1 ** Positive -> Bounded + + * Abs()>=1 ** Negative -> Bounded + + * Otherwise unknown + """ + if expr.base == E: + return ask(Q.finite(expr.exp), assumptions) + + base_bounded = ask(Q.finite(expr.base), assumptions) + exp_bounded = ask(Q.finite(expr.exp), assumptions) + if base_bounded is None and exp_bounded is None: # Common Case + return None + if base_bounded is False and ask(Q.extended_nonzero(expr.exp), assumptions): + return False + if base_bounded and exp_bounded: + is_base_zero = ask(Q.zero(expr.base),assumptions) + is_exp_negative = ask(Q.negative(expr.exp),assumptions) + if is_base_zero is True and is_exp_negative is True: + return False + if is_base_zero is not False and is_exp_negative is not False: + return None + return True + if (abs(expr.base) <= 1) == True and ask(Q.extended_positive(expr.exp), assumptions): + return True + if (abs(expr.base) >= 1) == True and ask(Q.extended_negative(expr.exp), assumptions): + return True + if (abs(expr.base) >= 1) == True and exp_bounded is False: + return False + return None + +@FinitePredicate.register(exp) +def _(expr, assumptions): + return ask(Q.finite(expr.exp), assumptions) + +@FinitePredicate.register(log) +def _(expr, assumptions): + # After complex -> finite fact is registered to new assumption system, + # querying Q.infinite may be removed. + if ask(Q.infinite(expr.args[0]), assumptions): + return False + return ask(~Q.zero(expr.args[0]), assumptions) + +@FinitePredicate.register_many(cos, sin, Number, Pi, Exp1, GoldenRatio, + TribonacciConstant, ImaginaryUnit, sign) +def _(expr, assumptions): + return True + +@FinitePredicate.register_many(ComplexInfinity, Infinity, NegativeInfinity) +def _(expr, assumptions): + return False + +@FinitePredicate.register(NaN) +def _(expr, assumptions): + return None + + +# InfinitePredicate + + +@InfinitePredicate.register(Expr) +def _(expr, assumptions): + is_finite = Q.finite(expr)._eval_ask(assumptions) + if is_finite is None: + return None + return not is_finite + + +# PositiveInfinitePredicate + + +@PositiveInfinitePredicate.register(Infinity) +def _(expr, assumptions): + return True + + +@PositiveInfinitePredicate.register_many(NegativeInfinity, ComplexInfinity) +def _(expr, assumptions): + return False + + +# NegativeInfinitePredicate + + +@NegativeInfinitePredicate.register(NegativeInfinity) +def _(expr, assumptions): + return True + + +@NegativeInfinitePredicate.register_many(Infinity, ComplexInfinity) +def _(expr, assumptions): + return False diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/common.py b/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/common.py new file mode 100644 index 0000000000000000000000000000000000000000..f6e9f6f321be461c09b16c03b9cec5708404d21a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/common.py @@ -0,0 +1,164 @@ +""" +This module defines base class for handlers and some core handlers: +``Q.commutative`` and ``Q.is_true``. +""" + +from sympy.assumptions import Q, ask, AppliedPredicate +from sympy.core import Basic, Symbol +from sympy.core.logic import _fuzzy_group, fuzzy_and, fuzzy_or +from sympy.core.numbers import NaN, Number +from sympy.logic.boolalg import (And, BooleanTrue, BooleanFalse, conjuncts, + Equivalent, Implies, Not, Or) +from sympy.utilities.exceptions import sympy_deprecation_warning + +from ..predicates.common import CommutativePredicate, IsTruePredicate + + +class AskHandler: + """Base class that all Ask Handlers must inherit.""" + def __new__(cls, *args, **kwargs): + sympy_deprecation_warning( + """ + The AskHandler system is deprecated. The AskHandler class should + be replaced with the multipledispatch handler of Predicate + """, + deprecated_since_version="1.8", + active_deprecations_target='deprecated-askhandler', + ) + return super().__new__(cls, *args, **kwargs) + + +class CommonHandler(AskHandler): + # Deprecated + """Defines some useful methods common to most Handlers. """ + + @staticmethod + def AlwaysTrue(expr, assumptions): + return True + + @staticmethod + def AlwaysFalse(expr, assumptions): + return False + + @staticmethod + def AlwaysNone(expr, assumptions): + return None + + NaN = AlwaysFalse + + +# CommutativePredicate + +@CommutativePredicate.register(Symbol) +def _(expr, assumptions): + """Objects are expected to be commutative unless otherwise stated""" + assumps = conjuncts(assumptions) + if expr.is_commutative is not None: + return expr.is_commutative and not ~Q.commutative(expr) in assumps + if Q.commutative(expr) in assumps: + return True + elif ~Q.commutative(expr) in assumps: + return False + return True + +@CommutativePredicate.register(Basic) +def _(expr, assumptions): + for arg in expr.args: + if not ask(Q.commutative(arg), assumptions): + return False + return True + +@CommutativePredicate.register(Number) +def _(expr, assumptions): + return True + +@CommutativePredicate.register(NaN) +def _(expr, assumptions): + return True + + +# IsTruePredicate + +@IsTruePredicate.register(bool) +def _(expr, assumptions): + return expr + +@IsTruePredicate.register(BooleanTrue) +def _(expr, assumptions): + return True + +@IsTruePredicate.register(BooleanFalse) +def _(expr, assumptions): + return False + +@IsTruePredicate.register(AppliedPredicate) +def _(expr, assumptions): + return ask(expr, assumptions) + +@IsTruePredicate.register(Not) +def _(expr, assumptions): + arg = expr.args[0] + if arg.is_Symbol: + # symbol used as abstract boolean object + return None + value = ask(arg, assumptions=assumptions) + if value in (True, False): + return not value + else: + return None + +@IsTruePredicate.register(Or) +def _(expr, assumptions): + result = False + for arg in expr.args: + p = ask(arg, assumptions=assumptions) + if p is True: + return True + if p is None: + result = None + return result + +@IsTruePredicate.register(And) +def _(expr, assumptions): + result = True + for arg in expr.args: + p = ask(arg, assumptions=assumptions) + if p is False: + return False + if p is None: + result = None + return result + +@IsTruePredicate.register(Implies) +def _(expr, assumptions): + p, q = expr.args + return ask(~p | q, assumptions=assumptions) + +@IsTruePredicate.register(Equivalent) +def _(expr, assumptions): + p, q = expr.args + pt = ask(p, assumptions=assumptions) + if pt is None: + return None + qt = ask(q, assumptions=assumptions) + if qt is None: + return None + return pt == qt + + +#### Helper methods +def test_closed_group(expr, assumptions, key): + """ + Test for membership in a group with respect + to the current operation. + """ + return _fuzzy_group( + (ask(key(a), assumptions) for a in expr.args), quick_exit=True) + +def ask_all(*queries, assumptions): + return fuzzy_and( + (ask(query, assumptions) for query in queries)) + +def ask_any(*queries, assumptions): + return fuzzy_or( + (ask(query, assumptions) for query in queries)) diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/matrices.py b/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/matrices.py new file mode 100644 index 0000000000000000000000000000000000000000..3b20385360136629ea037eb7238c45b70ba57fd2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/matrices.py @@ -0,0 +1,716 @@ +""" +This module contains query handlers responsible for Matrices queries: +Square, Symmetric, Invertible etc. +""" + +from sympy.logic.boolalg import conjuncts +from sympy.assumptions import Q, ask +from sympy.assumptions.handlers import test_closed_group +from sympy.matrices import MatrixBase +from sympy.matrices.expressions import (BlockMatrix, BlockDiagMatrix, Determinant, + DiagMatrix, DiagonalMatrix, HadamardProduct, Identity, Inverse, MatAdd, MatMul, + MatPow, MatrixExpr, MatrixSlice, MatrixSymbol, OneMatrix, Trace, Transpose, + ZeroMatrix) +from sympy.matrices.expressions.blockmatrix import reblock_2x2 +from sympy.matrices.expressions.factorizations import Factorization +from sympy.matrices.expressions.fourier import DFT +from sympy.core.logic import fuzzy_and +from sympy.utilities.iterables import sift +from sympy.core import Basic + +from ..predicates.matrices import (SquarePredicate, SymmetricPredicate, + InvertiblePredicate, OrthogonalPredicate, UnitaryPredicate, + FullRankPredicate, PositiveDefinitePredicate, UpperTriangularPredicate, + LowerTriangularPredicate, DiagonalPredicate, IntegerElementsPredicate, + RealElementsPredicate, ComplexElementsPredicate) + + +def _Factorization(predicate, expr, assumptions): + if predicate in expr.predicates: + return True + + +# SquarePredicate + +@SquarePredicate.register(MatrixExpr) +def _(expr, assumptions): + return expr.shape[0] == expr.shape[1] + + +# SymmetricPredicate + +@SymmetricPredicate.register(MatMul) +def _(expr, assumptions): + factor, mmul = expr.as_coeff_mmul() + if all(ask(Q.symmetric(arg), assumptions) for arg in mmul.args): + return True + # TODO: implement sathandlers system for the matrices. + # Now it duplicates the general fact: Implies(Q.diagonal, Q.symmetric). + if ask(Q.diagonal(expr), assumptions): + return True + if len(mmul.args) >= 2 and mmul.args[0] == mmul.args[-1].T: + if len(mmul.args) == 2: + return True + return ask(Q.symmetric(MatMul(*mmul.args[1:-1])), assumptions) + +@SymmetricPredicate.register(MatPow) +def _(expr, assumptions): + # only for integer powers + base, exp = expr.args + int_exp = ask(Q.integer(exp), assumptions) + if not int_exp: + return None + non_negative = ask(~Q.negative(exp), assumptions) + if (non_negative or non_negative == False + and ask(Q.invertible(base), assumptions)): + return ask(Q.symmetric(base), assumptions) + return None + +@SymmetricPredicate.register(MatAdd) +def _(expr, assumptions): + return all(ask(Q.symmetric(arg), assumptions) for arg in expr.args) + +@SymmetricPredicate.register(MatrixSymbol) +def _(expr, assumptions): + if not expr.is_square: + return False + # TODO: implement sathandlers system for the matrices. + # Now it duplicates the general fact: Implies(Q.diagonal, Q.symmetric). + if ask(Q.diagonal(expr), assumptions): + return True + if Q.symmetric(expr) in conjuncts(assumptions): + return True + +@SymmetricPredicate.register_many(OneMatrix, ZeroMatrix) +def _(expr, assumptions): + return ask(Q.square(expr), assumptions) + +@SymmetricPredicate.register_many(Inverse, Transpose) +def _(expr, assumptions): + return ask(Q.symmetric(expr.arg), assumptions) + +@SymmetricPredicate.register(MatrixSlice) +def _(expr, assumptions): + # TODO: implement sathandlers system for the matrices. + # Now it duplicates the general fact: Implies(Q.diagonal, Q.symmetric). + if ask(Q.diagonal(expr), assumptions): + return True + if not expr.on_diag: + return None + else: + return ask(Q.symmetric(expr.parent), assumptions) + +@SymmetricPredicate.register(Identity) +def _(expr, assumptions): + return True + + +# InvertiblePredicate + +@InvertiblePredicate.register(MatMul) +def _(expr, assumptions): + factor, mmul = expr.as_coeff_mmul() + if all(ask(Q.invertible(arg), assumptions) for arg in mmul.args): + return True + if any(ask(Q.invertible(arg), assumptions) is False + for arg in mmul.args): + return False + +@InvertiblePredicate.register(MatPow) +def _(expr, assumptions): + # only for integer powers + base, exp = expr.args + int_exp = ask(Q.integer(exp), assumptions) + if not int_exp: + return None + if exp.is_negative == False: + return ask(Q.invertible(base), assumptions) + return None + +@InvertiblePredicate.register(MatAdd) +def _(expr, assumptions): + return None + +@InvertiblePredicate.register(MatrixSymbol) +def _(expr, assumptions): + if not expr.is_square: + return False + if Q.invertible(expr) in conjuncts(assumptions): + return True + +@InvertiblePredicate.register_many(Identity, Inverse) +def _(expr, assumptions): + return True + +@InvertiblePredicate.register(ZeroMatrix) +def _(expr, assumptions): + return False + +@InvertiblePredicate.register(OneMatrix) +def _(expr, assumptions): + return expr.shape[0] == 1 and expr.shape[1] == 1 + +@InvertiblePredicate.register(Transpose) +def _(expr, assumptions): + return ask(Q.invertible(expr.arg), assumptions) + +@InvertiblePredicate.register(MatrixSlice) +def _(expr, assumptions): + if not expr.on_diag: + return None + else: + return ask(Q.invertible(expr.parent), assumptions) + +@InvertiblePredicate.register(MatrixBase) +def _(expr, assumptions): + if not expr.is_square: + return False + return expr.rank() == expr.rows + +@InvertiblePredicate.register(MatrixExpr) +def _(expr, assumptions): + if not expr.is_square: + return False + return None + +@InvertiblePredicate.register(BlockMatrix) +def _(expr, assumptions): + if not expr.is_square: + return False + if expr.blockshape == (1, 1): + return ask(Q.invertible(expr.blocks[0, 0]), assumptions) + expr = reblock_2x2(expr) + if expr.blockshape == (2, 2): + [[A, B], [C, D]] = expr.blocks.tolist() + if ask(Q.invertible(A), assumptions) == True: + invertible = ask(Q.invertible(D - C * A.I * B), assumptions) + if invertible is not None: + return invertible + if ask(Q.invertible(B), assumptions) == True: + invertible = ask(Q.invertible(C - D * B.I * A), assumptions) + if invertible is not None: + return invertible + if ask(Q.invertible(C), assumptions) == True: + invertible = ask(Q.invertible(B - A * C.I * D), assumptions) + if invertible is not None: + return invertible + if ask(Q.invertible(D), assumptions) == True: + invertible = ask(Q.invertible(A - B * D.I * C), assumptions) + if invertible is not None: + return invertible + return None + +@InvertiblePredicate.register(BlockDiagMatrix) +def _(expr, assumptions): + if expr.rowblocksizes != expr.colblocksizes: + return None + return fuzzy_and([ask(Q.invertible(a), assumptions) for a in expr.diag]) + + +# OrthogonalPredicate + +@OrthogonalPredicate.register(MatMul) +def _(expr, assumptions): + factor, mmul = expr.as_coeff_mmul() + if (all(ask(Q.orthogonal(arg), assumptions) for arg in mmul.args) and + factor == 1): + return True + if any(ask(Q.invertible(arg), assumptions) is False + for arg in mmul.args): + return False + +@OrthogonalPredicate.register(MatPow) +def _(expr, assumptions): + # only for integer powers + base, exp = expr.args + int_exp = ask(Q.integer(exp), assumptions) + if int_exp: + return ask(Q.orthogonal(base), assumptions) + return None + +@OrthogonalPredicate.register(MatAdd) +def _(expr, assumptions): + if (len(expr.args) == 1 and + ask(Q.orthogonal(expr.args[0]), assumptions)): + return True + +@OrthogonalPredicate.register(MatrixSymbol) +def _(expr, assumptions): + if (not expr.is_square or + ask(Q.invertible(expr), assumptions) is False): + return False + if Q.orthogonal(expr) in conjuncts(assumptions): + return True + +@OrthogonalPredicate.register(Identity) +def _(expr, assumptions): + return True + +@OrthogonalPredicate.register(ZeroMatrix) +def _(expr, assumptions): + return False + +@OrthogonalPredicate.register_many(Inverse, Transpose) +def _(expr, assumptions): + return ask(Q.orthogonal(expr.arg), assumptions) + +@OrthogonalPredicate.register(MatrixSlice) +def _(expr, assumptions): + if not expr.on_diag: + return None + else: + return ask(Q.orthogonal(expr.parent), assumptions) + +@OrthogonalPredicate.register(Factorization) +def _(expr, assumptions): + return _Factorization(Q.orthogonal, expr, assumptions) + + +# UnitaryPredicate + +@UnitaryPredicate.register(MatMul) +def _(expr, assumptions): + factor, mmul = expr.as_coeff_mmul() + if (all(ask(Q.unitary(arg), assumptions) for arg in mmul.args) and + abs(factor) == 1): + return True + if any(ask(Q.invertible(arg), assumptions) is False + for arg in mmul.args): + return False + +@UnitaryPredicate.register(MatPow) +def _(expr, assumptions): + # only for integer powers + base, exp = expr.args + int_exp = ask(Q.integer(exp), assumptions) + if int_exp: + return ask(Q.unitary(base), assumptions) + return None + +@UnitaryPredicate.register(MatrixSymbol) +def _(expr, assumptions): + if (not expr.is_square or + ask(Q.invertible(expr), assumptions) is False): + return False + if Q.unitary(expr) in conjuncts(assumptions): + return True + +@UnitaryPredicate.register_many(Inverse, Transpose) +def _(expr, assumptions): + return ask(Q.unitary(expr.arg), assumptions) + +@UnitaryPredicate.register(MatrixSlice) +def _(expr, assumptions): + if not expr.on_diag: + return None + else: + return ask(Q.unitary(expr.parent), assumptions) + +@UnitaryPredicate.register_many(DFT, Identity) +def _(expr, assumptions): + return True + +@UnitaryPredicate.register(ZeroMatrix) +def _(expr, assumptions): + return False + +@UnitaryPredicate.register(Factorization) +def _(expr, assumptions): + return _Factorization(Q.unitary, expr, assumptions) + + +# FullRankPredicate + +@FullRankPredicate.register(MatMul) +def _(expr, assumptions): + if all(ask(Q.fullrank(arg), assumptions) for arg in expr.args): + return True + +@FullRankPredicate.register(MatPow) +def _(expr, assumptions): + # only for integer powers + base, exp = expr.args + int_exp = ask(Q.integer(exp), assumptions) + if int_exp and ask(~Q.negative(exp), assumptions): + return ask(Q.fullrank(base), assumptions) + return None + +@FullRankPredicate.register(Identity) +def _(expr, assumptions): + return True + +@FullRankPredicate.register(ZeroMatrix) +def _(expr, assumptions): + return False + +@FullRankPredicate.register(OneMatrix) +def _(expr, assumptions): + return expr.shape[0] == 1 and expr.shape[1] == 1 + +@FullRankPredicate.register_many(Inverse, Transpose) +def _(expr, assumptions): + return ask(Q.fullrank(expr.arg), assumptions) + +@FullRankPredicate.register(MatrixSlice) +def _(expr, assumptions): + if ask(Q.orthogonal(expr.parent), assumptions): + return True + + +# PositiveDefinitePredicate + +@PositiveDefinitePredicate.register(MatMul) +def _(expr, assumptions): + factor, mmul = expr.as_coeff_mmul() + if (all(ask(Q.positive_definite(arg), assumptions) + for arg in mmul.args) and factor > 0): + return True + if (len(mmul.args) >= 2 + and mmul.args[0] == mmul.args[-1].T + and ask(Q.fullrank(mmul.args[0]), assumptions)): + return ask(Q.positive_definite( + MatMul(*mmul.args[1:-1])), assumptions) + +@PositiveDefinitePredicate.register(MatPow) +def _(expr, assumptions): + # a power of a positive definite matrix is positive definite + if ask(Q.positive_definite(expr.args[0]), assumptions): + return True + +@PositiveDefinitePredicate.register(MatAdd) +def _(expr, assumptions): + if all(ask(Q.positive_definite(arg), assumptions) + for arg in expr.args): + return True + +@PositiveDefinitePredicate.register(MatrixSymbol) +def _(expr, assumptions): + if not expr.is_square: + return False + if Q.positive_definite(expr) in conjuncts(assumptions): + return True + +@PositiveDefinitePredicate.register(Identity) +def _(expr, assumptions): + return True + +@PositiveDefinitePredicate.register(ZeroMatrix) +def _(expr, assumptions): + return False + +@PositiveDefinitePredicate.register(OneMatrix) +def _(expr, assumptions): + return expr.shape[0] == 1 and expr.shape[1] == 1 + +@PositiveDefinitePredicate.register_many(Inverse, Transpose) +def _(expr, assumptions): + return ask(Q.positive_definite(expr.arg), assumptions) + +@PositiveDefinitePredicate.register(MatrixSlice) +def _(expr, assumptions): + if not expr.on_diag: + return None + else: + return ask(Q.positive_definite(expr.parent), assumptions) + + +# UpperTriangularPredicate + +@UpperTriangularPredicate.register(MatMul) +def _(expr, assumptions): + factor, matrices = expr.as_coeff_matrices() + if all(ask(Q.upper_triangular(m), assumptions) for m in matrices): + return True + +@UpperTriangularPredicate.register(MatAdd) +def _(expr, assumptions): + if all(ask(Q.upper_triangular(arg), assumptions) for arg in expr.args): + return True + +@UpperTriangularPredicate.register(MatPow) +def _(expr, assumptions): + # only for integer powers + base, exp = expr.args + int_exp = ask(Q.integer(exp), assumptions) + if not int_exp: + return None + non_negative = ask(~Q.negative(exp), assumptions) + if (non_negative or non_negative == False + and ask(Q.invertible(base), assumptions)): + return ask(Q.upper_triangular(base), assumptions) + return None + +@UpperTriangularPredicate.register(MatrixSymbol) +def _(expr, assumptions): + if Q.upper_triangular(expr) in conjuncts(assumptions): + return True + +@UpperTriangularPredicate.register_many(Identity, ZeroMatrix) +def _(expr, assumptions): + return True + +@UpperTriangularPredicate.register(OneMatrix) +def _(expr, assumptions): + return expr.shape[0] == 1 and expr.shape[1] == 1 + +@UpperTriangularPredicate.register(Transpose) +def _(expr, assumptions): + return ask(Q.lower_triangular(expr.arg), assumptions) + +@UpperTriangularPredicate.register(Inverse) +def _(expr, assumptions): + return ask(Q.upper_triangular(expr.arg), assumptions) + +@UpperTriangularPredicate.register(MatrixSlice) +def _(expr, assumptions): + if not expr.on_diag: + return None + else: + return ask(Q.upper_triangular(expr.parent), assumptions) + +@UpperTriangularPredicate.register(Factorization) +def _(expr, assumptions): + return _Factorization(Q.upper_triangular, expr, assumptions) + +# LowerTriangularPredicate + +@LowerTriangularPredicate.register(MatMul) +def _(expr, assumptions): + factor, matrices = expr.as_coeff_matrices() + if all(ask(Q.lower_triangular(m), assumptions) for m in matrices): + return True + +@LowerTriangularPredicate.register(MatAdd) +def _(expr, assumptions): + if all(ask(Q.lower_triangular(arg), assumptions) for arg in expr.args): + return True + +@LowerTriangularPredicate.register(MatPow) +def _(expr, assumptions): + # only for integer powers + base, exp = expr.args + int_exp = ask(Q.integer(exp), assumptions) + if not int_exp: + return None + non_negative = ask(~Q.negative(exp), assumptions) + if (non_negative or non_negative == False + and ask(Q.invertible(base), assumptions)): + return ask(Q.lower_triangular(base), assumptions) + return None + +@LowerTriangularPredicate.register(MatrixSymbol) +def _(expr, assumptions): + if Q.lower_triangular(expr) in conjuncts(assumptions): + return True + +@LowerTriangularPredicate.register_many(Identity, ZeroMatrix) +def _(expr, assumptions): + return True + +@LowerTriangularPredicate.register(OneMatrix) +def _(expr, assumptions): + return expr.shape[0] == 1 and expr.shape[1] == 1 + +@LowerTriangularPredicate.register(Transpose) +def _(expr, assumptions): + return ask(Q.upper_triangular(expr.arg), assumptions) + +@LowerTriangularPredicate.register(Inverse) +def _(expr, assumptions): + return ask(Q.lower_triangular(expr.arg), assumptions) + +@LowerTriangularPredicate.register(MatrixSlice) +def _(expr, assumptions): + if not expr.on_diag: + return None + else: + return ask(Q.lower_triangular(expr.parent), assumptions) + +@LowerTriangularPredicate.register(Factorization) +def _(expr, assumptions): + return _Factorization(Q.lower_triangular, expr, assumptions) + + +# DiagonalPredicate + +def _is_empty_or_1x1(expr): + return expr.shape in ((0, 0), (1, 1)) + +@DiagonalPredicate.register(MatMul) +def _(expr, assumptions): + if _is_empty_or_1x1(expr): + return True + factor, matrices = expr.as_coeff_matrices() + if all(ask(Q.diagonal(m), assumptions) for m in matrices): + return True + +@DiagonalPredicate.register(MatPow) +def _(expr, assumptions): + # only for integer powers + base, exp = expr.args + int_exp = ask(Q.integer(exp), assumptions) + if not int_exp: + return None + non_negative = ask(~Q.negative(exp), assumptions) + if (non_negative or non_negative == False + and ask(Q.invertible(base), assumptions)): + return ask(Q.diagonal(base), assumptions) + return None + +@DiagonalPredicate.register(MatAdd) +def _(expr, assumptions): + if all(ask(Q.diagonal(arg), assumptions) for arg in expr.args): + return True + +@DiagonalPredicate.register(MatrixSymbol) +def _(expr, assumptions): + if _is_empty_or_1x1(expr): + return True + if Q.diagonal(expr) in conjuncts(assumptions): + return True + +@DiagonalPredicate.register(OneMatrix) +def _(expr, assumptions): + return expr.shape[0] == 1 and expr.shape[1] == 1 + +@DiagonalPredicate.register_many(Inverse, Transpose) +def _(expr, assumptions): + return ask(Q.diagonal(expr.arg), assumptions) + +@DiagonalPredicate.register(MatrixSlice) +def _(expr, assumptions): + if _is_empty_or_1x1(expr): + return True + if not expr.on_diag: + return None + else: + return ask(Q.diagonal(expr.parent), assumptions) + +@DiagonalPredicate.register_many(DiagonalMatrix, DiagMatrix, Identity, ZeroMatrix) +def _(expr, assumptions): + return True + +@DiagonalPredicate.register(Factorization) +def _(expr, assumptions): + return _Factorization(Q.diagonal, expr, assumptions) + + +# IntegerElementsPredicate + +def BM_elements(predicate, expr, assumptions): + """ Block Matrix elements. """ + return all(ask(predicate(b), assumptions) for b in expr.blocks) + +def MS_elements(predicate, expr, assumptions): + """ Matrix Slice elements. """ + return ask(predicate(expr.parent), assumptions) + +def MatMul_elements(matrix_predicate, scalar_predicate, expr, assumptions): + d = sift(expr.args, lambda x: isinstance(x, MatrixExpr)) + factors, matrices = d[False], d[True] + return fuzzy_and([ + test_closed_group(Basic(*factors), assumptions, scalar_predicate), + test_closed_group(Basic(*matrices), assumptions, matrix_predicate)]) + + +@IntegerElementsPredicate.register_many(Determinant, HadamardProduct, MatAdd, + Trace, Transpose) +def _(expr, assumptions): + return test_closed_group(expr, assumptions, Q.integer_elements) + +@IntegerElementsPredicate.register(MatPow) +def _(expr, assumptions): + # only for integer powers + base, exp = expr.args + int_exp = ask(Q.integer(exp), assumptions) + if not int_exp: + return None + if exp.is_negative == False: + return ask(Q.integer_elements(base), assumptions) + return None + +@IntegerElementsPredicate.register_many(Identity, OneMatrix, ZeroMatrix) +def _(expr, assumptions): + return True + +@IntegerElementsPredicate.register(MatMul) +def _(expr, assumptions): + return MatMul_elements(Q.integer_elements, Q.integer, expr, assumptions) + +@IntegerElementsPredicate.register(MatrixSlice) +def _(expr, assumptions): + return MS_elements(Q.integer_elements, expr, assumptions) + +@IntegerElementsPredicate.register(BlockMatrix) +def _(expr, assumptions): + return BM_elements(Q.integer_elements, expr, assumptions) + + +# RealElementsPredicate + +@RealElementsPredicate.register_many(Determinant, Factorization, HadamardProduct, + MatAdd, Trace, Transpose) +def _(expr, assumptions): + return test_closed_group(expr, assumptions, Q.real_elements) + +@RealElementsPredicate.register(MatPow) +def _(expr, assumptions): + # only for integer powers + base, exp = expr.args + int_exp = ask(Q.integer(exp), assumptions) + if not int_exp: + return None + non_negative = ask(~Q.negative(exp), assumptions) + if (non_negative or non_negative == False + and ask(Q.invertible(base), assumptions)): + return ask(Q.real_elements(base), assumptions) + return None + +@RealElementsPredicate.register(MatMul) +def _(expr, assumptions): + return MatMul_elements(Q.real_elements, Q.real, expr, assumptions) + +@RealElementsPredicate.register(MatrixSlice) +def _(expr, assumptions): + return MS_elements(Q.real_elements, expr, assumptions) + +@RealElementsPredicate.register(BlockMatrix) +def _(expr, assumptions): + return BM_elements(Q.real_elements, expr, assumptions) + + +# ComplexElementsPredicate + +@ComplexElementsPredicate.register_many(Determinant, Factorization, HadamardProduct, + Inverse, MatAdd, Trace, Transpose) +def _(expr, assumptions): + return test_closed_group(expr, assumptions, Q.complex_elements) + +@ComplexElementsPredicate.register(MatPow) +def _(expr, assumptions): + # only for integer powers + base, exp = expr.args + int_exp = ask(Q.integer(exp), assumptions) + if not int_exp: + return None + non_negative = ask(~Q.negative(exp), assumptions) + if (non_negative or non_negative == False + and ask(Q.invertible(base), assumptions)): + return ask(Q.complex_elements(base), assumptions) + return None + +@ComplexElementsPredicate.register(MatMul) +def _(expr, assumptions): + return MatMul_elements(Q.complex_elements, Q.complex, expr, assumptions) + +@ComplexElementsPredicate.register(MatrixSlice) +def _(expr, assumptions): + return MS_elements(Q.complex_elements, expr, assumptions) + +@ComplexElementsPredicate.register(BlockMatrix) +def _(expr, assumptions): + return BM_elements(Q.complex_elements, expr, assumptions) + +@ComplexElementsPredicate.register(DFT) +def _(expr, assumptions): + return True diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/ntheory.py b/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/ntheory.py new file mode 100644 index 0000000000000000000000000000000000000000..cfe63ba6467ea6863c6112c5e35bb3a78191a23e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/ntheory.py @@ -0,0 +1,279 @@ +""" +Handlers for keys related to number theory: prime, even, odd, etc. +""" + +from sympy.assumptions import Q, ask +from sympy.core import Add, Basic, Expr, Float, Mul, Pow, S +from sympy.core.numbers import (ImaginaryUnit, Infinity, Integer, NaN, + NegativeInfinity, NumberSymbol, Rational, int_valued) +from sympy.functions import Abs, im, re +from sympy.ntheory import isprime + +from sympy.multipledispatch import MDNotImplementedError + +from ..predicates.ntheory import (PrimePredicate, CompositePredicate, + EvenPredicate, OddPredicate) + + +# PrimePredicate + +def _PrimePredicate_number(expr, assumptions): + # helper method + exact = not expr.atoms(Float) + try: + i = int(expr.round()) + if (expr - i).equals(0) is False: + raise TypeError + except TypeError: + return False + if exact: + return isprime(i) + # when not exact, we won't give a True or False + # since the number represents an approximate value + +@PrimePredicate.register(Expr) +def _(expr, assumptions): + ret = expr.is_prime + if ret is None: + raise MDNotImplementedError + return ret + +@PrimePredicate.register(Basic) +def _(expr, assumptions): + if expr.is_number: + return _PrimePredicate_number(expr, assumptions) + +@PrimePredicate.register(Mul) +def _(expr, assumptions): + if expr.is_number: + return _PrimePredicate_number(expr, assumptions) + for arg in expr.args: + if not ask(Q.integer(arg), assumptions): + return None + for arg in expr.args: + if arg.is_number and arg.is_composite: + return False + +@PrimePredicate.register(Pow) +def _(expr, assumptions): + """ + Integer**Integer -> !Prime + """ + if expr.is_number: + return _PrimePredicate_number(expr, assumptions) + if ask(Q.integer(expr.exp), assumptions) and \ + ask(Q.integer(expr.base), assumptions): + prime_base = ask(Q.prime(expr.base), assumptions) + if prime_base is False: + return False + is_exp_one = ask(Q.eq(expr.exp, 1), assumptions) + if is_exp_one is False: + return False + if prime_base is True and is_exp_one is True: + return True + +@PrimePredicate.register(Integer) +def _(expr, assumptions): + return isprime(expr) + +@PrimePredicate.register_many(Rational, Infinity, NegativeInfinity, ImaginaryUnit) +def _(expr, assumptions): + return False + +@PrimePredicate.register(Float) +def _(expr, assumptions): + return _PrimePredicate_number(expr, assumptions) + +@PrimePredicate.register(NumberSymbol) +def _(expr, assumptions): + return _PrimePredicate_number(expr, assumptions) + +@PrimePredicate.register(NaN) +def _(expr, assumptions): + return None + + +# CompositePredicate + +@CompositePredicate.register(Expr) +def _(expr, assumptions): + ret = expr.is_composite + if ret is None: + raise MDNotImplementedError + return ret + +@CompositePredicate.register(Basic) +def _(expr, assumptions): + _positive = ask(Q.positive(expr), assumptions) + if _positive: + _integer = ask(Q.integer(expr), assumptions) + if _integer: + _prime = ask(Q.prime(expr), assumptions) + if _prime is None: + return + # Positive integer which is not prime is not + # necessarily composite + _is_one = ask(Q.eq(expr, 1), assumptions) + if _is_one: + return False + if _is_one is None: + return None + return not _prime + else: + return _integer + else: + return _positive + + +# EvenPredicate + +def _EvenPredicate_number(expr, assumptions): + # helper method + if isinstance(expr, (float, Float)): + if int_valued(expr): + return None + return False + try: + i = int(expr.round()) + except TypeError: + return False + if not (expr - i).equals(0): + return False + return i % 2 == 0 + +@EvenPredicate.register(Expr) +def _(expr, assumptions): + ret = expr.is_even + if ret is None: + raise MDNotImplementedError + return ret + +@EvenPredicate.register(Basic) +def _(expr, assumptions): + if expr.is_number: + return _EvenPredicate_number(expr, assumptions) + +@EvenPredicate.register(Mul) +def _(expr, assumptions): + """ + Even * Integer -> Even + Even * Odd -> Even + Integer * Odd -> ? + Odd * Odd -> Odd + Even * Even -> Even + Integer * Integer -> Even if Integer + Integer = Odd + otherwise -> ? + """ + if expr.is_number: + return _EvenPredicate_number(expr, assumptions) + even, odd, irrational, acc = False, 0, False, 1 + for arg in expr.args: + # check for all integers and at least one even + if ask(Q.integer(arg), assumptions): + if ask(Q.even(arg), assumptions): + even = True + elif ask(Q.odd(arg), assumptions): + odd += 1 + elif not even and acc != 1: + if ask(Q.odd(acc + arg), assumptions): + even = True + elif ask(Q.irrational(arg), assumptions): + # one irrational makes the result False + # two makes it undefined + if irrational: + break + irrational = True + else: + break + acc = arg + else: + if irrational: + return False + if even: + return True + if odd == len(expr.args): + return False + +@EvenPredicate.register(Add) +def _(expr, assumptions): + """ + Even + Odd -> Odd + Even + Even -> Even + Odd + Odd -> Even + + """ + if expr.is_number: + return _EvenPredicate_number(expr, assumptions) + _result = True + for arg in expr.args: + if ask(Q.even(arg), assumptions): + pass + elif ask(Q.odd(arg), assumptions): + _result = not _result + else: + break + else: + return _result + +@EvenPredicate.register(Pow) +def _(expr, assumptions): + if expr.is_number: + return _EvenPredicate_number(expr, assumptions) + if ask(Q.integer(expr.exp), assumptions): + if ask(Q.positive(expr.exp), assumptions): + return ask(Q.even(expr.base), assumptions) + elif ask(~Q.negative(expr.exp) & Q.odd(expr.base), assumptions): + return False + elif expr.base is S.NegativeOne: + return False + +@EvenPredicate.register(Integer) +def _(expr, assumptions): + return not bool(expr.p & 1) + +@EvenPredicate.register_many(Rational, Infinity, NegativeInfinity, ImaginaryUnit) +def _(expr, assumptions): + return False + +@EvenPredicate.register(NumberSymbol) +def _(expr, assumptions): + return _EvenPredicate_number(expr, assumptions) + +@EvenPredicate.register(Abs) +def _(expr, assumptions): + if ask(Q.real(expr.args[0]), assumptions): + return ask(Q.even(expr.args[0]), assumptions) + +@EvenPredicate.register(re) +def _(expr, assumptions): + if ask(Q.real(expr.args[0]), assumptions): + return ask(Q.even(expr.args[0]), assumptions) + +@EvenPredicate.register(im) +def _(expr, assumptions): + if ask(Q.real(expr.args[0]), assumptions): + return True + +@EvenPredicate.register(NaN) +def _(expr, assumptions): + return None + + +# OddPredicate + +@OddPredicate.register(Expr) +def _(expr, assumptions): + ret = expr.is_odd + if ret is None: + raise MDNotImplementedError + return ret + +@OddPredicate.register(Basic) +def _(expr, assumptions): + _integer = ask(Q.integer(expr), assumptions) + if _integer: + _even = ask(Q.even(expr), assumptions) + if _even is None: + return None + return not _even + return _integer diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/order.py b/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/order.py new file mode 100644 index 0000000000000000000000000000000000000000..24a8bae7f30777f62a1bec0579d58b9875143679 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/order.py @@ -0,0 +1,440 @@ +""" +Handlers related to order relations: positive, negative, etc. +""" + +from sympy.assumptions import Q, ask +from sympy.core import Add, Basic, Expr, Mul, Pow, S +from sympy.core.logic import fuzzy_not, fuzzy_and, fuzzy_or +from sympy.core.numbers import E, ImaginaryUnit, NaN, I, pi +from sympy.functions import Abs, acos, acot, asin, atan, exp, factorial, log +from sympy.matrices import Determinant, Trace +from sympy.matrices.expressions.matexpr import MatrixElement + +from sympy.multipledispatch import MDNotImplementedError + +from ..predicates.order import (NegativePredicate, NonNegativePredicate, + NonZeroPredicate, ZeroPredicate, NonPositivePredicate, PositivePredicate, + ExtendedNegativePredicate, ExtendedNonNegativePredicate, + ExtendedNonPositivePredicate, ExtendedNonZeroPredicate, + ExtendedPositivePredicate,) + + +# NegativePredicate + +def _NegativePredicate_number(expr, assumptions): + r, i = expr.as_real_imag() + + if r == S.NaN or i == S.NaN: + return None + + # If the imaginary part can symbolically be shown to be zero then + # we just evaluate the real part; otherwise we evaluate the imaginary + # part to see if it actually evaluates to zero and if it does then + # we make the comparison between the real part and zero. + if not i: + r = r.evalf(2) + if r._prec != 1: + return r < 0 + else: + i = i.evalf(2) + if i._prec != 1: + if i != 0: + return False + r = r.evalf(2) + if r._prec != 1: + return r < 0 + +@NegativePredicate.register(Basic) +def _(expr, assumptions): + if expr.is_number: + return _NegativePredicate_number(expr, assumptions) + +@NegativePredicate.register(Expr) +def _(expr, assumptions): + ret = expr.is_negative + if ret is None: + raise MDNotImplementedError + return ret + +@NegativePredicate.register(Add) +def _(expr, assumptions): + """ + Positive + Positive -> Positive, + Negative + Negative -> Negative + """ + if expr.is_number: + return _NegativePredicate_number(expr, assumptions) + + r = ask(Q.real(expr), assumptions) + if r is not True: + return r + + nonpos = 0 + for arg in expr.args: + if ask(Q.negative(arg), assumptions) is not True: + if ask(Q.positive(arg), assumptions) is False: + nonpos += 1 + else: + break + else: + if nonpos < len(expr.args): + return True + +@NegativePredicate.register(Mul) +def _(expr, assumptions): + if expr.is_number: + return _NegativePredicate_number(expr, assumptions) + result = None + for arg in expr.args: + if result is None: + result = False + if ask(Q.negative(arg), assumptions): + result = not result + elif ask(Q.positive(arg), assumptions): + pass + else: + return + return result + +@NegativePredicate.register(Pow) +def _(expr, assumptions): + """ + Real ** Even -> NonNegative + Real ** Odd -> same_as_base + NonNegative ** Positive -> NonNegative + """ + if expr.base == E: + # Exponential is always positive: + if ask(Q.real(expr.exp), assumptions): + return False + return + + if expr.is_number: + return _NegativePredicate_number(expr, assumptions) + if ask(Q.real(expr.base), assumptions): + if ask(Q.positive(expr.base), assumptions): + if ask(Q.real(expr.exp), assumptions): + return False + if ask(Q.even(expr.exp), assumptions): + return False + if ask(Q.odd(expr.exp), assumptions): + return ask(Q.negative(expr.base), assumptions) + +@NegativePredicate.register_many(Abs, ImaginaryUnit) +def _(expr, assumptions): + return False + +@NegativePredicate.register(exp) +def _(expr, assumptions): + if ask(Q.real(expr.exp), assumptions): + return False + raise MDNotImplementedError + + +# NonNegativePredicate + +@NonNegativePredicate.register(Basic) +def _(expr, assumptions): + if expr.is_number: + notnegative = fuzzy_not(_NegativePredicate_number(expr, assumptions)) + if notnegative: + return ask(Q.real(expr), assumptions) + else: + return notnegative + +@NonNegativePredicate.register(Expr) +def _(expr, assumptions): + ret = expr.is_nonnegative + if ret is None: + raise MDNotImplementedError + return ret + + +# NonZeroPredicate + +@NonZeroPredicate.register(Expr) +def _(expr, assumptions): + ret = expr.is_nonzero + if ret is None: + raise MDNotImplementedError + return ret + +@NonZeroPredicate.register(Basic) +def _(expr, assumptions): + if ask(Q.real(expr)) is False: + return False + if expr.is_number: + # if there are no symbols just evalf + i = expr.evalf(2) + def nonz(i): + if i._prec != 1: + return i != 0 + return fuzzy_or(nonz(i) for i in i.as_real_imag()) + +@NonZeroPredicate.register(Add) +def _(expr, assumptions): + if all(ask(Q.positive(x), assumptions) for x in expr.args) \ + or all(ask(Q.negative(x), assumptions) for x in expr.args): + return True + +@NonZeroPredicate.register(Mul) +def _(expr, assumptions): + for arg in expr.args: + result = ask(Q.nonzero(arg), assumptions) + if result: + continue + return result + return True + +@NonZeroPredicate.register(Pow) +def _(expr, assumptions): + return ask(Q.nonzero(expr.base), assumptions) + +@NonZeroPredicate.register(Abs) +def _(expr, assumptions): + return ask(Q.nonzero(expr.args[0]), assumptions) + +@NonZeroPredicate.register(NaN) +def _(expr, assumptions): + return None + + +# ZeroPredicate + +@ZeroPredicate.register(Expr) +def _(expr, assumptions): + ret = expr.is_zero + if ret is None: + raise MDNotImplementedError + return ret + +@ZeroPredicate.register(Basic) +def _(expr, assumptions): + return fuzzy_and([fuzzy_not(ask(Q.nonzero(expr), assumptions)), + ask(Q.real(expr), assumptions)]) + +@ZeroPredicate.register(Mul) +def _(expr, assumptions): + # TODO: This should be deducible from the nonzero handler + return fuzzy_or(ask(Q.zero(arg), assumptions) for arg in expr.args) + + +# NonPositivePredicate + +@NonPositivePredicate.register(Expr) +def _(expr, assumptions): + ret = expr.is_nonpositive + if ret is None: + raise MDNotImplementedError + return ret + +@NonPositivePredicate.register(Basic) +def _(expr, assumptions): + if expr.is_number: + notpositive = fuzzy_not(_PositivePredicate_number(expr, assumptions)) + if notpositive: + return ask(Q.real(expr), assumptions) + else: + return notpositive + + +# PositivePredicate + +def _PositivePredicate_number(expr, assumptions): + r, i = expr.as_real_imag() + # If the imaginary part can symbolically be shown to be zero then + # we just evaluate the real part; otherwise we evaluate the imaginary + # part to see if it actually evaluates to zero and if it does then + # we make the comparison between the real part and zero. + if not i: + r = r.evalf(2) + if r._prec != 1: + return r > 0 + else: + i = i.evalf(2) + if i._prec != 1: + if i != 0: + return False + r = r.evalf(2) + if r._prec != 1: + return r > 0 + +@PositivePredicate.register(Expr) +def _(expr, assumptions): + ret = expr.is_positive + if ret is None: + raise MDNotImplementedError + return ret + +@PositivePredicate.register(Basic) +def _(expr, assumptions): + if expr.is_number: + return _PositivePredicate_number(expr, assumptions) + +@PositivePredicate.register(Mul) +def _(expr, assumptions): + if expr.is_number: + return _PositivePredicate_number(expr, assumptions) + result = True + for arg in expr.args: + if ask(Q.positive(arg), assumptions): + continue + elif ask(Q.negative(arg), assumptions): + result = result ^ True + else: + return + return result + +@PositivePredicate.register(Add) +def _(expr, assumptions): + if expr.is_number: + return _PositivePredicate_number(expr, assumptions) + + r = ask(Q.real(expr), assumptions) + if r is not True: + return r + + nonneg = 0 + for arg in expr.args: + if ask(Q.positive(arg), assumptions) is not True: + if ask(Q.negative(arg), assumptions) is False: + nonneg += 1 + else: + break + else: + if nonneg < len(expr.args): + return True + +@PositivePredicate.register(Pow) +def _(expr, assumptions): + if expr.base == E: + if ask(Q.real(expr.exp), assumptions): + return True + if ask(Q.imaginary(expr.exp), assumptions): + return ask(Q.even(expr.exp/(I*pi)), assumptions) + return + + if expr.is_number: + return _PositivePredicate_number(expr, assumptions) + if ask(Q.positive(expr.base), assumptions): + if ask(Q.real(expr.exp), assumptions): + return True + if ask(Q.negative(expr.base), assumptions): + if ask(Q.even(expr.exp), assumptions): + return True + if ask(Q.odd(expr.exp), assumptions): + return False + +@PositivePredicate.register(exp) +def _(expr, assumptions): + if ask(Q.real(expr.exp), assumptions): + return True + if ask(Q.imaginary(expr.exp), assumptions): + return ask(Q.even(expr.exp/(I*pi)), assumptions) + +@PositivePredicate.register(log) +def _(expr, assumptions): + r = ask(Q.real(expr.args[0]), assumptions) + if r is not True: + return r + if ask(Q.positive(expr.args[0] - 1), assumptions): + return True + if ask(Q.negative(expr.args[0] - 1), assumptions): + return False + +@PositivePredicate.register(factorial) +def _(expr, assumptions): + x = expr.args[0] + if ask(Q.integer(x) & Q.positive(x), assumptions): + return True + +@PositivePredicate.register(ImaginaryUnit) +def _(expr, assumptions): + return False + +@PositivePredicate.register(Abs) +def _(expr, assumptions): + return ask(Q.nonzero(expr), assumptions) + +@PositivePredicate.register(Trace) +def _(expr, assumptions): + if ask(Q.positive_definite(expr.arg), assumptions): + return True + +@PositivePredicate.register(Determinant) +def _(expr, assumptions): + if ask(Q.positive_definite(expr.arg), assumptions): + return True + +@PositivePredicate.register(MatrixElement) +def _(expr, assumptions): + if (expr.i == expr.j + and ask(Q.positive_definite(expr.parent), assumptions)): + return True + +@PositivePredicate.register(atan) +def _(expr, assumptions): + return ask(Q.positive(expr.args[0]), assumptions) + +@PositivePredicate.register(asin) +def _(expr, assumptions): + x = expr.args[0] + if ask(Q.positive(x) & Q.nonpositive(x - 1), assumptions): + return True + if ask(Q.negative(x) & Q.nonnegative(x + 1), assumptions): + return False + +@PositivePredicate.register(acos) +def _(expr, assumptions): + x = expr.args[0] + if ask(Q.nonpositive(x - 1) & Q.nonnegative(x + 1), assumptions): + return True + +@PositivePredicate.register(acot) +def _(expr, assumptions): + return ask(Q.real(expr.args[0]), assumptions) + +@PositivePredicate.register(NaN) +def _(expr, assumptions): + return None + + +# ExtendedNegativePredicate + +@ExtendedNegativePredicate.register(object) +def _(expr, assumptions): + return ask(Q.negative(expr) | Q.negative_infinite(expr), assumptions) + + +# ExtendedPositivePredicate + +@ExtendedPositivePredicate.register(object) +def _(expr, assumptions): + return ask(Q.positive(expr) | Q.positive_infinite(expr), assumptions) + + +# ExtendedNonZeroPredicate + +@ExtendedNonZeroPredicate.register(object) +def _(expr, assumptions): + return ask( + Q.negative_infinite(expr) | Q.negative(expr) | Q.positive(expr) | Q.positive_infinite(expr), + assumptions) + + +# ExtendedNonPositivePredicate + +@ExtendedNonPositivePredicate.register(object) +def _(expr, assumptions): + return ask( + Q.negative_infinite(expr) | Q.negative(expr) | Q.zero(expr), + assumptions) + + +# ExtendedNonNegativePredicate + +@ExtendedNonNegativePredicate.register(object) +def _(expr, assumptions): + return ask( + Q.zero(expr) | Q.positive(expr) | Q.positive_infinite(expr), + assumptions) diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/sets.py b/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/sets.py new file mode 100644 index 0000000000000000000000000000000000000000..7a13ed9bf99c5b0ffc4f32fd55cb60c2c15ab836 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/handlers/sets.py @@ -0,0 +1,816 @@ +""" +Handlers for predicates related to set membership: integer, rational, etc. +""" + +from sympy.assumptions import Q, ask +from sympy.core import Add, Basic, Expr, Mul, Pow, S +from sympy.core.numbers import (AlgebraicNumber, ComplexInfinity, Exp1, Float, + GoldenRatio, ImaginaryUnit, Infinity, Integer, NaN, NegativeInfinity, + Number, NumberSymbol, Pi, pi, Rational, TribonacciConstant, E) +from sympy.core.logic import fuzzy_bool +from sympy.functions import (Abs, acos, acot, asin, atan, cos, cot, exp, im, + log, re, sin, tan) +from sympy.core.numbers import I +from sympy.core.relational import Eq +from sympy.functions.elementary.complexes import conjugate +from sympy.matrices import Determinant, MatrixBase, Trace +from sympy.matrices.expressions.matexpr import MatrixElement + +from sympy.multipledispatch import MDNotImplementedError + +from .common import test_closed_group, ask_all, ask_any +from ..predicates.sets import (IntegerPredicate, RationalPredicate, + IrrationalPredicate, RealPredicate, ExtendedRealPredicate, + HermitianPredicate, ComplexPredicate, ImaginaryPredicate, + AntihermitianPredicate, AlgebraicPredicate) + + +# IntegerPredicate + +def _IntegerPredicate_number(expr, assumptions): + # helper function + try: + i = int(expr.round()) + if not (expr - i).equals(0): + raise TypeError + return True + except TypeError: + return False + +@IntegerPredicate.register_many(int, Integer) # type:ignore +def _(expr, assumptions): + return True + +@IntegerPredicate.register_many(Exp1, GoldenRatio, ImaginaryUnit, Infinity, + NegativeInfinity, Pi, Rational, TribonacciConstant) +def _(expr, assumptions): + return False + +@IntegerPredicate.register(Expr) +def _(expr, assumptions): + ret = expr.is_integer + if ret is None: + raise MDNotImplementedError + return ret + +@IntegerPredicate.register(Add) +def _(expr, assumptions): + """ + * Integer + Integer -> Integer + * Integer + !Integer -> !Integer + * !Integer + !Integer -> ? + """ + if expr.is_number: + return _IntegerPredicate_number(expr, assumptions) + return test_closed_group(expr, assumptions, Q.integer) + +@IntegerPredicate.register(Pow) +def _(expr,assumptions): + if expr.is_number: + return _IntegerPredicate_number(expr, assumptions) + if ask_all(~Q.zero(expr.base), Q.finite(expr.base), Q.zero(expr.exp), assumptions=assumptions): + return True + if ask_all(Q.integer(expr.base), Q.integer(expr.exp), assumptions=assumptions): + if ask_any(Q.positive(expr.exp), Q.nonnegative(expr.exp) & ~Q.zero(expr.base), Q.zero(expr.base-1), Q.zero(expr.base+1), assumptions=assumptions): + return True + +@IntegerPredicate.register(Mul) +def _(expr, assumptions): + """ + * Integer*Integer -> Integer + * Integer*Irrational -> !Integer + * Odd/Even -> !Integer + * Integer*Rational -> ? + """ + if expr.is_number: + return _IntegerPredicate_number(expr, assumptions) + _output = True + for arg in expr.args: + if not ask(Q.integer(arg), assumptions): + if arg.is_Rational: + if arg.q == 2: + return ask(Q.even(2*expr), assumptions) + if ~(arg.q & 1): + return None + elif ask(Q.irrational(arg), assumptions): + if _output: + _output = False + else: + return + else: + return + + return _output + +@IntegerPredicate.register(Abs) +def _(expr, assumptions): + if ask(Q.integer(expr.args[0]), assumptions): + return True + +@IntegerPredicate.register_many(Determinant, MatrixElement, Trace) +def _(expr, assumptions): + return ask(Q.integer_elements(expr.args[0]), assumptions) + + +# RationalPredicate + +@RationalPredicate.register(Rational) +def _(expr, assumptions): + return True + +@RationalPredicate.register(Float) +def _(expr, assumptions): + return None + +@RationalPredicate.register_many(Exp1, GoldenRatio, ImaginaryUnit, Infinity, + NegativeInfinity, Pi, TribonacciConstant) +def _(expr, assumptions): + return False + +@RationalPredicate.register(Expr) +def _(expr, assumptions): + ret = expr.is_rational + if ret is None: + raise MDNotImplementedError + return ret + +@RationalPredicate.register_many(Add, Mul) +def _(expr, assumptions): + """ + * Rational + Rational -> Rational + * Rational + !Rational -> !Rational + * !Rational + !Rational -> ? + """ + if expr.is_number: + if expr.as_real_imag()[1]: + return False + return test_closed_group(expr, assumptions, Q.rational) + +@RationalPredicate.register(Pow) +def _(expr, assumptions): + """ + * Rational ** Integer -> Rational + * Irrational ** Rational -> Irrational + * Rational ** Irrational -> ? + """ + if expr.base == E: + x = expr.exp + if ask(Q.rational(x), assumptions): + return ask(Q.zero(x), assumptions) + return + + is_exp_integer = ask(Q.integer(expr.exp), assumptions) + if is_exp_integer: + is_base_rational = ask(Q.rational(expr.base),assumptions) + if is_base_rational: + is_base_zero = ask(Q.zero(expr.base),assumptions) + if is_base_zero is False: + return True + if is_base_zero and ask(Q.positive(expr.exp)): + return True + if ask(Q.algebraic(expr.base),assumptions) is False: + return ask(Q.zero(expr.exp), assumptions) + if ask(Q.irrational(expr.base),assumptions) and ask(Q.eq(expr.exp,-1)): + return False + return + elif ask(Q.rational(expr.exp), assumptions): + if ask(Q.prime(expr.base), assumptions) and is_exp_integer is False: + return False + if ask(Q.zero(expr.base)) and ask(Q.positive(expr.exp)): + return True + if ask(Q.eq(expr.base,1)): + return True + +@RationalPredicate.register_many(asin, atan, cos, sin, tan) +def _(expr, assumptions): + x = expr.args[0] + if ask(Q.rational(x), assumptions): + return ask(~Q.nonzero(x), assumptions) + +@RationalPredicate.register(exp) +def _(expr, assumptions): + x = expr.exp + if ask(Q.rational(x), assumptions): + return ask(~Q.nonzero(x), assumptions) + +@RationalPredicate.register_many(acot, cot) +def _(expr, assumptions): + x = expr.args[0] + if ask(Q.rational(x), assumptions): + return False + +@RationalPredicate.register_many(acos, log) +def _(expr, assumptions): + x = expr.args[0] + if ask(Q.rational(x), assumptions): + return ask(~Q.nonzero(x - 1), assumptions) + + +# IrrationalPredicate + +@IrrationalPredicate.register(Expr) +def _(expr, assumptions): + ret = expr.is_irrational + if ret is None: + raise MDNotImplementedError + return ret + +@IrrationalPredicate.register(Basic) +def _(expr, assumptions): + _real = ask(Q.real(expr), assumptions) + if _real: + _rational = ask(Q.rational(expr), assumptions) + if _rational is None: + return None + return not _rational + else: + return _real + + +# RealPredicate + +def _RealPredicate_number(expr, assumptions): + # let as_real_imag() work first since the expression may + # be simpler to evaluate + i = expr.as_real_imag()[1].evalf(2) + if i._prec != 1: + return not i + # allow None to be returned if we couldn't show for sure + # that i was 0 + +@RealPredicate.register_many(Abs, Exp1, Float, GoldenRatio, im, Pi, Rational, + re, TribonacciConstant) +def _(expr, assumptions): + return True + +@RealPredicate.register_many(ImaginaryUnit, Infinity, NegativeInfinity) +def _(expr, assumptions): + return False + +@RealPredicate.register(Expr) +def _(expr, assumptions): + ret = expr.is_real + if ret is None: + raise MDNotImplementedError + return ret + +@RealPredicate.register(Add) +def _(expr, assumptions): + """ + * Real + Real -> Real + * Real + (Complex & !Real) -> !Real + """ + if expr.is_number: + return _RealPredicate_number(expr, assumptions) + return test_closed_group(expr, assumptions, Q.real) + +@RealPredicate.register(Mul) +def _(expr, assumptions): + """ + * Real*Real -> Real + * Real*Imaginary -> !Real + * Imaginary*Imaginary -> Real + """ + if expr.is_number: + return _RealPredicate_number(expr, assumptions) + result = True + for arg in expr.args: + if ask(Q.real(arg), assumptions): + pass + elif ask(Q.imaginary(arg), assumptions): + result = result ^ True + else: + break + else: + return result + +@RealPredicate.register(Pow) +def _(expr, assumptions): + """ + * Real**Integer -> Real + * Positive**Real -> Real + * Negative**Real -> ? + * Real**(Integer/Even) -> Real if base is nonnegative + * Real**(Integer/Odd) -> Real + * Imaginary**(Integer/Even) -> Real + * Imaginary**(Integer/Odd) -> not Real + * Imaginary**Real -> ? since Real could be 0 (giving real) + or 1 (giving imaginary) + * b**Imaginary -> Real if log(b) is imaginary and b != 0 + and exponent != integer multiple of + I*pi/log(b) + * Real**Real -> ? e.g. sqrt(-1) is imaginary and + sqrt(2) is not + """ + if expr.is_number: + return _RealPredicate_number(expr, assumptions) + + if expr.base == E: + return ask( + Q.integer(expr.exp/I/pi) | Q.real(expr.exp), assumptions + ) + + if expr.base.func == exp or (expr.base.is_Pow and expr.base.base == E): + if ask(Q.imaginary(expr.base.exp), assumptions): + if ask(Q.imaginary(expr.exp), assumptions): + return True + # If the i = (exp's arg)/(I*pi) is an integer or half-integer + # multiple of I*pi then 2*i will be an integer. In addition, + # exp(i*I*pi) = (-1)**i so the overall realness of the expr + # can be determined by replacing exp(i*I*pi) with (-1)**i. + i = expr.base.exp/I/pi + if ask(Q.integer(2*i), assumptions): + return ask(Q.real((S.NegativeOne**i)**expr.exp), assumptions) + return + + if ask(Q.imaginary(expr.base), assumptions): + if ask(Q.integer(expr.exp), assumptions): + odd = ask(Q.odd(expr.exp), assumptions) + if odd is not None: + return not odd + return + + if ask(Q.imaginary(expr.exp), assumptions): + imlog = ask(Q.imaginary(log(expr.base)), assumptions) + if imlog is not None: + # I**i -> real, log(I) is imag; + # (2*I)**i -> complex, log(2*I) is not imag + return imlog + + if ask(Q.real(expr.base), assumptions): + if ask(Q.real(expr.exp), assumptions): + if ask(Q.zero(expr.base), assumptions) is not False: + if ask(Q.positive(expr.exp), assumptions): + return True + return + if expr.exp.is_Rational and \ + ask(Q.even(expr.exp.q), assumptions): + return ask(Q.positive(expr.base), assumptions) + elif ask(Q.integer(expr.exp), assumptions): + return True + elif ask(Q.positive(expr.base), assumptions): + return True + +@RealPredicate.register_many(cos, sin) +def _(expr, assumptions): + if ask(Q.real(expr.args[0]), assumptions): + return True + +@RealPredicate.register(exp) +def _(expr, assumptions): + return ask( + Q.integer(expr.exp/I/pi) | Q.real(expr.exp), assumptions + ) + +@RealPredicate.register(log) +def _(expr, assumptions): + return ask(Q.positive(expr.args[0]), assumptions) + +@RealPredicate.register_many(Determinant, MatrixElement, Trace) +def _(expr, assumptions): + return ask(Q.real_elements(expr.args[0]), assumptions) + + +# ExtendedRealPredicate + +@ExtendedRealPredicate.register(object) +def _(expr, assumptions): + return ask(Q.negative_infinite(expr) + | Q.negative(expr) + | Q.zero(expr) + | Q.positive(expr) + | Q.positive_infinite(expr), + assumptions) + +@ExtendedRealPredicate.register_many(Infinity, NegativeInfinity) +def _(expr, assumptions): + return True + +@ExtendedRealPredicate.register_many(Add, Mul, Pow) # type:ignore +def _(expr, assumptions): + return test_closed_group(expr, assumptions, Q.extended_real) + + +# HermitianPredicate + +@HermitianPredicate.register(object) # type:ignore +def _(expr, assumptions): + if isinstance(expr, MatrixBase): + return None + return ask(Q.real(expr), assumptions) + +@HermitianPredicate.register(Add) # type:ignore +def _(expr, assumptions): + """ + * Hermitian + Hermitian -> Hermitian + * Hermitian + !Hermitian -> !Hermitian + """ + if expr.is_number: + raise MDNotImplementedError + return test_closed_group(expr, assumptions, Q.hermitian) + +@HermitianPredicate.register(Mul) # type:ignore +def _(expr, assumptions): + """ + As long as there is at most only one noncommutative term: + + * Hermitian*Hermitian -> Hermitian + * Hermitian*Antihermitian -> !Hermitian + * Antihermitian*Antihermitian -> Hermitian + """ + if expr.is_number: + raise MDNotImplementedError + nccount = 0 + result = True + for arg in expr.args: + if ask(Q.antihermitian(arg), assumptions): + result = result ^ True + elif not ask(Q.hermitian(arg), assumptions): + break + if ask(~Q.commutative(arg), assumptions): + nccount += 1 + if nccount > 1: + break + else: + return result + +@HermitianPredicate.register(Pow) # type:ignore +def _(expr, assumptions): + """ + * Hermitian**Integer -> Hermitian + """ + if expr.is_number: + raise MDNotImplementedError + if expr.base == E: + if ask(Q.hermitian(expr.exp), assumptions): + return True + raise MDNotImplementedError + if ask(Q.hermitian(expr.base), assumptions): + if ask(Q.integer(expr.exp), assumptions): + return True + raise MDNotImplementedError + +@HermitianPredicate.register_many(cos, sin) # type:ignore +def _(expr, assumptions): + if ask(Q.hermitian(expr.args[0]), assumptions): + return True + raise MDNotImplementedError + +@HermitianPredicate.register(exp) # type:ignore +def _(expr, assumptions): + if ask(Q.hermitian(expr.exp), assumptions): + return True + raise MDNotImplementedError + +@HermitianPredicate.register(MatrixBase) # type:ignore +def _(mat, assumptions): + rows, cols = mat.shape + ret_val = True + for i in range(rows): + for j in range(i, cols): + cond = fuzzy_bool(Eq(mat[i, j], conjugate(mat[j, i]))) + if cond is None: + ret_val = None + if cond == False: + return False + if ret_val is None: + raise MDNotImplementedError + return ret_val + + +# ComplexPredicate + +@ComplexPredicate.register_many(Abs, cos, exp, im, ImaginaryUnit, log, Number, # type:ignore + NumberSymbol, re, sin) +def _(expr, assumptions): + return True + +@ComplexPredicate.register_many(Infinity, NegativeInfinity) # type:ignore +def _(expr, assumptions): + return False + +@ComplexPredicate.register(Expr) # type:ignore +def _(expr, assumptions): + ret = expr.is_complex + if ret is None: + raise MDNotImplementedError + return ret + +@ComplexPredicate.register_many(Add, Mul) # type:ignore +def _(expr, assumptions): + return test_closed_group(expr, assumptions, Q.complex) + +@ComplexPredicate.register(Pow) # type:ignore +def _(expr, assumptions): + if expr.base == E: + return True + return test_closed_group(expr, assumptions, Q.complex) + +@ComplexPredicate.register_many(Determinant, MatrixElement, Trace) # type:ignore +def _(expr, assumptions): + return ask(Q.complex_elements(expr.args[0]), assumptions) + +@ComplexPredicate.register(NaN) # type:ignore +def _(expr, assumptions): + return None + + +# ImaginaryPredicate + +def _Imaginary_number(expr, assumptions): + # let as_real_imag() work first since the expression may + # be simpler to evaluate + r = expr.as_real_imag()[0].evalf(2) + if r._prec != 1: + return not r + # allow None to be returned if we couldn't show for sure + # that r was 0 + +@ImaginaryPredicate.register(ImaginaryUnit) # type:ignore +def _(expr, assumptions): + return True + +@ImaginaryPredicate.register(Expr) # type:ignore +def _(expr, assumptions): + ret = expr.is_imaginary + if ret is None: + raise MDNotImplementedError + return ret + +@ImaginaryPredicate.register(Add) # type:ignore +def _(expr, assumptions): + """ + * Imaginary + Imaginary -> Imaginary + * Imaginary + Complex -> ? + * Imaginary + Real -> !Imaginary + """ + if expr.is_number: + return _Imaginary_number(expr, assumptions) + + reals = 0 + for arg in expr.args: + if ask(Q.imaginary(arg), assumptions): + pass + elif ask(Q.real(arg), assumptions): + reals += 1 + else: + break + else: + if reals == 0: + return True + if reals in (1, len(expr.args)): + # two reals could sum 0 thus giving an imaginary + return False + +@ImaginaryPredicate.register(Mul) # type:ignore +def _(expr, assumptions): + """ + * Real*Imaginary -> Imaginary + * Imaginary*Imaginary -> Real + """ + if expr.is_number: + return _Imaginary_number(expr, assumptions) + result = False + reals = 0 + for arg in expr.args: + if ask(Q.imaginary(arg), assumptions): + result = result ^ True + elif not ask(Q.real(arg), assumptions): + break + else: + if reals == len(expr.args): + return False + return result + +@ImaginaryPredicate.register(Pow) # type:ignore +def _(expr, assumptions): + """ + * Imaginary**Odd -> Imaginary + * Imaginary**Even -> Real + * b**Imaginary -> !Imaginary if exponent is an integer + multiple of I*pi/log(b) + * Imaginary**Real -> ? + * Positive**Real -> Real + * Negative**Integer -> Real + * Negative**(Integer/2) -> Imaginary + * Negative**Real -> not Imaginary if exponent is not Rational + """ + if expr.is_number: + return _Imaginary_number(expr, assumptions) + + if expr.base == E: + a = expr.exp/I/pi + return ask(Q.integer(2*a) & ~Q.integer(a), assumptions) + + if expr.base.func == exp or (expr.base.is_Pow and expr.base.base == E): + if ask(Q.imaginary(expr.base.exp), assumptions): + if ask(Q.imaginary(expr.exp), assumptions): + return False + i = expr.base.exp/I/pi + if ask(Q.integer(2*i), assumptions): + return ask(Q.imaginary((S.NegativeOne**i)**expr.exp), assumptions) + + if ask(Q.imaginary(expr.base), assumptions): + if ask(Q.integer(expr.exp), assumptions): + odd = ask(Q.odd(expr.exp), assumptions) + if odd is not None: + return odd + return + + if ask(Q.imaginary(expr.exp), assumptions): + imlog = ask(Q.imaginary(log(expr.base)), assumptions) + if imlog is not None: + # I**i -> real; (2*I)**i -> complex ==> not imaginary + return False + + if ask(Q.real(expr.base) & Q.real(expr.exp), assumptions): + if ask(Q.positive(expr.base), assumptions): + return False + else: + rat = ask(Q.rational(expr.exp), assumptions) + if not rat: + return rat + if ask(Q.integer(expr.exp), assumptions): + return False + else: + half = ask(Q.integer(2*expr.exp), assumptions) + if half: + return ask(Q.negative(expr.base), assumptions) + return half + +@ImaginaryPredicate.register(log) # type:ignore +def _(expr, assumptions): + if ask(Q.real(expr.args[0]), assumptions): + if ask(Q.positive(expr.args[0]), assumptions): + return False + return + # XXX it should be enough to do + # return ask(Q.nonpositive(expr.args[0]), assumptions) + # but ask(Q.nonpositive(exp(x)), Q.imaginary(x)) -> None; + # it should return True since exp(x) will be either 0 or complex + if expr.args[0].func == exp or (expr.args[0].is_Pow and expr.args[0].base == E): + if expr.args[0].exp in [I, -I]: + return True + im = ask(Q.imaginary(expr.args[0]), assumptions) + if im is False: + return False + +@ImaginaryPredicate.register(exp) # type:ignore +def _(expr, assumptions): + a = expr.exp/I/pi + return ask(Q.integer(2*a) & ~Q.integer(a), assumptions) + +@ImaginaryPredicate.register_many(Number, NumberSymbol) # type:ignore +def _(expr, assumptions): + return not (expr.as_real_imag()[1] == 0) + +@ImaginaryPredicate.register(NaN) # type:ignore +def _(expr, assumptions): + return None + + +# AntihermitianPredicate + +@AntihermitianPredicate.register(object) # type:ignore +def _(expr, assumptions): + if isinstance(expr, MatrixBase): + return None + if ask(Q.zero(expr), assumptions): + return True + return ask(Q.imaginary(expr), assumptions) + +@AntihermitianPredicate.register(Add) # type:ignore +def _(expr, assumptions): + """ + * Antihermitian + Antihermitian -> Antihermitian + * Antihermitian + !Antihermitian -> !Antihermitian + """ + if expr.is_number: + raise MDNotImplementedError + return test_closed_group(expr, assumptions, Q.antihermitian) + +@AntihermitianPredicate.register(Mul) # type:ignore +def _(expr, assumptions): + """ + As long as there is at most only one noncommutative term: + + * Hermitian*Hermitian -> !Antihermitian + * Hermitian*Antihermitian -> Antihermitian + * Antihermitian*Antihermitian -> !Antihermitian + """ + if expr.is_number: + raise MDNotImplementedError + nccount = 0 + result = False + for arg in expr.args: + if ask(Q.antihermitian(arg), assumptions): + result = result ^ True + elif not ask(Q.hermitian(arg), assumptions): + break + if ask(~Q.commutative(arg), assumptions): + nccount += 1 + if nccount > 1: + break + else: + return result + +@AntihermitianPredicate.register(Pow) # type:ignore +def _(expr, assumptions): + """ + * Hermitian**Integer -> !Antihermitian + * Antihermitian**Even -> !Antihermitian + * Antihermitian**Odd -> Antihermitian + """ + if expr.is_number: + raise MDNotImplementedError + if ask(Q.hermitian(expr.base), assumptions): + if ask(Q.integer(expr.exp), assumptions): + return False + elif ask(Q.antihermitian(expr.base), assumptions): + if ask(Q.even(expr.exp), assumptions): + return False + elif ask(Q.odd(expr.exp), assumptions): + return True + raise MDNotImplementedError + +@AntihermitianPredicate.register(MatrixBase) # type:ignore +def _(mat, assumptions): + rows, cols = mat.shape + ret_val = True + for i in range(rows): + for j in range(i, cols): + cond = fuzzy_bool(Eq(mat[i, j], -conjugate(mat[j, i]))) + if cond is None: + ret_val = None + if cond == False: + return False + if ret_val is None: + raise MDNotImplementedError + return ret_val + + +# AlgebraicPredicate + +@AlgebraicPredicate.register_many(AlgebraicNumber, Float, GoldenRatio, # type:ignore + ImaginaryUnit, TribonacciConstant) +def _(expr, assumptions): + return True + +@AlgebraicPredicate.register_many(ComplexInfinity, Exp1, Infinity, # type:ignore + NegativeInfinity, Pi) +def _(expr, assumptions): + return False + +@AlgebraicPredicate.register_many(Add, Mul) # type:ignore +def _(expr, assumptions): + return test_closed_group(expr, assumptions, Q.algebraic) + +@AlgebraicPredicate.register(Pow) # type:ignore +def _(expr, assumptions): + if expr.base == E: + if ask(Q.algebraic(expr.exp), assumptions): + return ask(~Q.nonzero(expr.exp), assumptions) + return + if expr.base == pi: + if ask(Q.integer(expr.exp), assumptions) and ask(Q.positive(expr.exp), assumptions): + return False + return + exp_rational = ask(Q.rational(expr.exp), assumptions) + base_algebraic = ask(Q.algebraic(expr.base), assumptions) + exp_algebraic = ask(Q.algebraic(expr.exp),assumptions) + if base_algebraic and exp_algebraic: + if exp_rational: + return True + # Check based on the Gelfond-Schneider theorem: + # If the base is algebraic and not equal to 0 or 1, and the exponent + # is irrational,then the result is transcendental. + if ask(Q.ne(expr.base,0) & Q.ne(expr.base,1)) and exp_rational is False: + return False + +@AlgebraicPredicate.register(Rational) # type:ignore +def _(expr, assumptions): + return expr.q != 0 + +@AlgebraicPredicate.register_many(asin, atan, cos, sin, tan) # type:ignore +def _(expr, assumptions): + x = expr.args[0] + if ask(Q.algebraic(x), assumptions): + return ask(~Q.nonzero(x), assumptions) + +@AlgebraicPredicate.register(exp) # type:ignore +def _(expr, assumptions): + x = expr.exp + if ask(Q.algebraic(x), assumptions): + return ask(~Q.nonzero(x), assumptions) + +@AlgebraicPredicate.register_many(acot, cot) # type:ignore +def _(expr, assumptions): + x = expr.args[0] + if ask(Q.algebraic(x), assumptions): + return False + +@AlgebraicPredicate.register_many(acos, log) # type:ignore +def _(expr, assumptions): + x = expr.args[0] + if ask(Q.algebraic(x), assumptions): + return ask(~Q.nonzero(x - 1), assumptions) diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/lra_satask.py b/venv/lib/python3.11/site-packages/sympy/assumptions/lra_satask.py new file mode 100644 index 0000000000000000000000000000000000000000..53afe3e5abe99109ec01a47f19f1a8a4c99c5628 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/lra_satask.py @@ -0,0 +1,286 @@ +from sympy.assumptions.assume import global_assumptions +from sympy.assumptions.cnf import CNF, EncodedCNF +from sympy.assumptions.ask import Q +from sympy.logic.inference import satisfiable +from sympy.logic.algorithms.lra_theory import UnhandledInput, ALLOWED_PRED +from sympy.matrices.kind import MatrixKind +from sympy.core.kind import NumberKind +from sympy.assumptions.assume import AppliedPredicate +from sympy.core.mul import Mul +from sympy.core.singleton import S + + +def lra_satask(proposition, assumptions=True, context=global_assumptions): + """ + Function to evaluate the proposition with assumptions using SAT algorithm + in conjunction with an Linear Real Arithmetic theory solver. + + Used to handle inequalities. Should eventually be depreciated and combined + into satask, but infinity handling and other things need to be implemented + before that can happen. + """ + props = CNF.from_prop(proposition) + _props = CNF.from_prop(~proposition) + + cnf = CNF.from_prop(assumptions) + assumptions = EncodedCNF() + assumptions.from_cnf(cnf) + + context_cnf = CNF() + if context: + context_cnf = context_cnf.extend(context) + + assumptions.add_from_cnf(context_cnf) + + return check_satisfiability(props, _props, assumptions) + +# Some predicates such as Q.prime can't be handled by lra_satask. +# For example, (x > 0) & (x < 1) & Q.prime(x) is unsat but lra_satask would think it was sat. +# WHITE_LIST is a list of predicates that can always be handled. +WHITE_LIST = ALLOWED_PRED | {Q.positive, Q.negative, Q.zero, Q.nonzero, Q.nonpositive, Q.nonnegative, + Q.extended_positive, Q.extended_negative, Q.extended_nonpositive, + Q.extended_negative, Q.extended_nonzero, Q.negative_infinite, + Q.positive_infinite} + + +def check_satisfiability(prop, _prop, factbase): + sat_true = factbase.copy() + sat_false = factbase.copy() + sat_true.add_from_cnf(prop) + sat_false.add_from_cnf(_prop) + + all_pred, all_exprs = get_all_pred_and_expr_from_enc_cnf(sat_true) + + for pred in all_pred: + if pred.function not in WHITE_LIST and pred.function != Q.ne: + raise UnhandledInput(f"LRASolver: {pred} is an unhandled predicate") + for expr in all_exprs: + if expr.kind == MatrixKind(NumberKind): + raise UnhandledInput(f"LRASolver: {expr} is of MatrixKind") + if expr == S.NaN: + raise UnhandledInput("LRASolver: nan") + + # convert old assumptions into predicates and add them to sat_true and sat_false + # also check for unhandled predicates + for assm in extract_pred_from_old_assum(all_exprs): + n = len(sat_true.encoding) + if assm not in sat_true.encoding: + sat_true.encoding[assm] = n+1 + sat_true.data.append([sat_true.encoding[assm]]) + + n = len(sat_false.encoding) + if assm not in sat_false.encoding: + sat_false.encoding[assm] = n+1 + sat_false.data.append([sat_false.encoding[assm]]) + + + sat_true = _preprocess(sat_true) + sat_false = _preprocess(sat_false) + + can_be_true = satisfiable(sat_true, use_lra_theory=True) is not False + can_be_false = satisfiable(sat_false, use_lra_theory=True) is not False + + if can_be_true and can_be_false: + return None + + if can_be_true and not can_be_false: + return True + + if not can_be_true and can_be_false: + return False + + if not can_be_true and not can_be_false: + raise ValueError("Inconsistent assumptions") + + +def _preprocess(enc_cnf): + """ + Returns an encoded cnf with only Q.eq, Q.gt, Q.lt, + Q.ge, and Q.le predicate. + + Converts every unequality into a disjunction of strict + inequalities. For example, x != 3 would become + x < 3 OR x > 3. + + Also converts all negated Q.ne predicates into + equalities. + """ + + # loops through each literal in each clause + # to construct a new, preprocessed encodedCNF + + enc_cnf = enc_cnf.copy() + cur_enc = 1 + rev_encoding = {value: key for key, value in enc_cnf.encoding.items()} + + new_encoding = {} + new_data = [] + for clause in enc_cnf.data: + new_clause = [] + for lit in clause: + if lit == 0: + new_clause.append(lit) + new_encoding[lit] = False + continue + prop = rev_encoding[abs(lit)] + negated = lit < 0 + sign = (lit > 0) - (lit < 0) + + prop = _pred_to_binrel(prop) + + if not isinstance(prop, AppliedPredicate): + if prop not in new_encoding: + new_encoding[prop] = cur_enc + cur_enc += 1 + lit = new_encoding[prop] + new_clause.append(sign*lit) + continue + + + if negated and prop.function == Q.eq: + negated = False + prop = Q.ne(*prop.arguments) + + if prop.function == Q.ne: + arg1, arg2 = prop.arguments + if negated: + new_prop = Q.eq(arg1, arg2) + if new_prop not in new_encoding: + new_encoding[new_prop] = cur_enc + cur_enc += 1 + + new_enc = new_encoding[new_prop] + new_clause.append(new_enc) + continue + else: + new_props = (Q.gt(arg1, arg2), Q.lt(arg1, arg2)) + for new_prop in new_props: + if new_prop not in new_encoding: + new_encoding[new_prop] = cur_enc + cur_enc += 1 + + new_enc = new_encoding[new_prop] + new_clause.append(new_enc) + continue + + if prop.function == Q.eq and negated: + assert False + + if prop not in new_encoding: + new_encoding[prop] = cur_enc + cur_enc += 1 + new_clause.append(new_encoding[prop]*sign) + new_data.append(new_clause) + + assert len(new_encoding) >= cur_enc - 1 + + enc_cnf = EncodedCNF(new_data, new_encoding) + return enc_cnf + + +def _pred_to_binrel(pred): + if not isinstance(pred, AppliedPredicate): + return pred + + if pred.function in pred_to_pos_neg_zero: + f = pred_to_pos_neg_zero[pred.function] + if f is False: + return False + pred = f(pred.arguments[0]) + + if pred.function == Q.positive: + pred = Q.gt(pred.arguments[0], 0) + elif pred.function == Q.negative: + pred = Q.lt(pred.arguments[0], 0) + elif pred.function == Q.zero: + pred = Q.eq(pred.arguments[0], 0) + elif pred.function == Q.nonpositive: + pred = Q.le(pred.arguments[0], 0) + elif pred.function == Q.nonnegative: + pred = Q.ge(pred.arguments[0], 0) + elif pred.function == Q.nonzero: + pred = Q.ne(pred.arguments[0], 0) + + return pred + +pred_to_pos_neg_zero = { + Q.extended_positive: Q.positive, + Q.extended_negative: Q.negative, + Q.extended_nonpositive: Q.nonpositive, + Q.extended_negative: Q.negative, + Q.extended_nonzero: Q.nonzero, + Q.negative_infinite: False, + Q.positive_infinite: False +} + +def get_all_pred_and_expr_from_enc_cnf(enc_cnf): + all_exprs = set() + all_pred = set() + for pred in enc_cnf.encoding.keys(): + if isinstance(pred, AppliedPredicate): + all_pred.add(pred) + all_exprs.update(pred.arguments) + + return all_pred, all_exprs + +def extract_pred_from_old_assum(all_exprs): + """ + Returns a list of relevant new assumption predicate + based on any old assumptions. + + Raises an UnhandledInput exception if any of the assumptions are + unhandled. + + Ignored predicate: + - commutative + - complex + - algebraic + - transcendental + - extended_real + - real + - all matrix predicate + - rational + - irrational + + Example + ======= + >>> from sympy.assumptions.lra_satask import extract_pred_from_old_assum + >>> from sympy import symbols + >>> x, y = symbols("x y", positive=True) + >>> extract_pred_from_old_assum([x, y, 2]) + [Q.positive(x), Q.positive(y)] + """ + ret = [] + for expr in all_exprs: + if not hasattr(expr, "free_symbols"): + continue + if len(expr.free_symbols) == 0: + continue + + if expr.is_real is not True: + raise UnhandledInput(f"LRASolver: {expr} must be real") + # test for I times imaginary variable; such expressions are considered real + if isinstance(expr, Mul) and any(arg.is_real is not True for arg in expr.args): + raise UnhandledInput(f"LRASolver: {expr} must be real") + + if expr.is_integer == True and expr.is_zero != True: + raise UnhandledInput(f"LRASolver: {expr} is an integer") + if expr.is_integer == False: + raise UnhandledInput(f"LRASolver: {expr} can't be an integer") + if expr.is_rational == False: + raise UnhandledInput(f"LRASolver: {expr} is irational") + + if expr.is_zero: + ret.append(Q.zero(expr)) + elif expr.is_positive: + ret.append(Q.positive(expr)) + elif expr.is_negative: + ret.append(Q.negative(expr)) + elif expr.is_nonzero: + ret.append(Q.nonzero(expr)) + elif expr.is_nonpositive: + ret.append(Q.nonpositive(expr)) + elif expr.is_nonnegative: + ret.append(Q.nonnegative(expr)) + + return ret diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/__init__.py b/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8e294544bfdce13633ecff762ff42861aa12719f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/__init__.py @@ -0,0 +1,5 @@ +""" +Module to implement predicate classes. + +Class of every predicate registered to ``Q`` is defined here. +""" diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/calculus.py b/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/calculus.py new file mode 100644 index 0000000000000000000000000000000000000000..f300703788683c07649ee3a0afd6e9d4eabd4567 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/calculus.py @@ -0,0 +1,82 @@ +from sympy.assumptions import Predicate +from sympy.multipledispatch import Dispatcher + +class FinitePredicate(Predicate): + """ + Finite number predicate. + + Explanation + =========== + + ``Q.finite(x)`` is true if ``x`` is a number but neither an infinity + nor a ``NaN``. In other words, ``ask(Q.finite(x))`` is true for all + numerical ``x`` having a bounded absolute value. + + Examples + ======== + + >>> from sympy import Q, ask, S, oo, I, zoo + >>> from sympy.abc import x + >>> ask(Q.finite(oo)) + False + >>> ask(Q.finite(-oo)) + False + >>> ask(Q.finite(zoo)) + False + >>> ask(Q.finite(1)) + True + >>> ask(Q.finite(2 + 3*I)) + True + >>> ask(Q.finite(x), Q.positive(x)) + True + >>> print(ask(Q.finite(S.NaN))) + None + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Finite + + """ + name = 'finite' + handler = Dispatcher( + "FiniteHandler", + doc=("Handler for Q.finite. Test that an expression is bounded respect" + " to all its variables.") + ) + + +class InfinitePredicate(Predicate): + """ + Infinite number predicate. + + ``Q.infinite(x)`` is true iff the absolute value of ``x`` is + infinity. + + """ + # TODO: Add examples + name = 'infinite' + handler = Dispatcher( + "InfiniteHandler", + doc="""Handler for Q.infinite key.""" + ) + + +class PositiveInfinitePredicate(Predicate): + """ + Positive infinity predicate. + + ``Q.positive_infinite(x)`` is true iff ``x`` is positive infinity ``oo``. + """ + name = 'positive_infinite' + handler = Dispatcher("PositiveInfiniteHandler") + + +class NegativeInfinitePredicate(Predicate): + """ + Negative infinity predicate. + + ``Q.negative_infinite(x)`` is true iff ``x`` is negative infinity ``-oo``. + """ + name = 'negative_infinite' + handler = Dispatcher("NegativeInfiniteHandler") diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/common.py b/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/common.py new file mode 100644 index 0000000000000000000000000000000000000000..a53892747131b03636abeb8f563c4f76cf3e281e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/common.py @@ -0,0 +1,81 @@ +from sympy.assumptions import Predicate, AppliedPredicate, Q +from sympy.core.relational import Eq, Ne, Gt, Lt, Ge, Le +from sympy.multipledispatch import Dispatcher + + +class CommutativePredicate(Predicate): + """ + Commutative predicate. + + Explanation + =========== + + ``ask(Q.commutative(x))`` is true iff ``x`` commutes with any other + object with respect to multiplication operation. + + """ + # TODO: Add examples + name = 'commutative' + handler = Dispatcher("CommutativeHandler", doc="Handler for key 'commutative'.") + + +binrelpreds = {Eq: Q.eq, Ne: Q.ne, Gt: Q.gt, Lt: Q.lt, Ge: Q.ge, Le: Q.le} + +class IsTruePredicate(Predicate): + """ + Generic predicate. + + Explanation + =========== + + ``ask(Q.is_true(x))`` is true iff ``x`` is true. This only makes + sense if ``x`` is a boolean object. + + Examples + ======== + + >>> from sympy import ask, Q + >>> from sympy.abc import x, y + >>> ask(Q.is_true(True)) + True + + Wrapping another applied predicate just returns the applied predicate. + + >>> Q.is_true(Q.even(x)) + Q.even(x) + + Wrapping binary relation classes in SymPy core returns applied binary + relational predicates. + + >>> from sympy import Eq, Gt + >>> Q.is_true(Eq(x, y)) + Q.eq(x, y) + >>> Q.is_true(Gt(x, y)) + Q.gt(x, y) + + Notes + ===== + + This class is designed to wrap the boolean objects so that they can + behave as if they are applied predicates. Consequently, wrapping another + applied predicate is unnecessary and thus it just returns the argument. + Also, binary relation classes in SymPy core have binary predicates to + represent themselves and thus wrapping them with ``Q.is_true`` converts them + to these applied predicates. + + """ + name = 'is_true' + handler = Dispatcher( + "IsTrueHandler", + doc="Wrapper allowing to query the truth value of a boolean expression." + ) + + def __call__(self, arg): + # No need to wrap another predicate + if isinstance(arg, AppliedPredicate): + return arg + # Convert relational predicates instead of wrapping them + if getattr(arg, "is_Relational", False): + pred = binrelpreds[type(arg)] + return pred(*arg.args) + return super().__call__(arg) diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/matrices.py b/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/matrices.py new file mode 100644 index 0000000000000000000000000000000000000000..151e78c4ff345800e1d2f17973fb0591b8d379d2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/matrices.py @@ -0,0 +1,511 @@ +from sympy.assumptions import Predicate +from sympy.multipledispatch import Dispatcher + +class SquarePredicate(Predicate): + """ + Square matrix predicate. + + Explanation + =========== + + ``Q.square(x)`` is true iff ``x`` is a square matrix. A square matrix + is a matrix with the same number of rows and columns. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol, ZeroMatrix, Identity + >>> X = MatrixSymbol('X', 2, 2) + >>> Y = MatrixSymbol('X', 2, 3) + >>> ask(Q.square(X)) + True + >>> ask(Q.square(Y)) + False + >>> ask(Q.square(ZeroMatrix(3, 3))) + True + >>> ask(Q.square(Identity(3))) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Square_matrix + + """ + name = 'square' + handler = Dispatcher("SquareHandler", doc="Handler for Q.square.") + + +class SymmetricPredicate(Predicate): + """ + Symmetric matrix predicate. + + Explanation + =========== + + ``Q.symmetric(x)`` is true iff ``x`` is a square matrix and is equal to + its transpose. Every square diagonal matrix is a symmetric matrix. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol + >>> X = MatrixSymbol('X', 2, 2) + >>> Y = MatrixSymbol('Y', 2, 3) + >>> Z = MatrixSymbol('Z', 2, 2) + >>> ask(Q.symmetric(X*Z), Q.symmetric(X) & Q.symmetric(Z)) + True + >>> ask(Q.symmetric(X + Z), Q.symmetric(X) & Q.symmetric(Z)) + True + >>> ask(Q.symmetric(Y)) + False + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Symmetric_matrix + + """ + # TODO: Add handlers to make these keys work with + # actual matrices and add more examples in the docstring. + name = 'symmetric' + handler = Dispatcher("SymmetricHandler", doc="Handler for Q.symmetric.") + + +class InvertiblePredicate(Predicate): + """ + Invertible matrix predicate. + + Explanation + =========== + + ``Q.invertible(x)`` is true iff ``x`` is an invertible matrix. + A square matrix is called invertible only if its determinant is 0. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol + >>> X = MatrixSymbol('X', 2, 2) + >>> Y = MatrixSymbol('Y', 2, 3) + >>> Z = MatrixSymbol('Z', 2, 2) + >>> ask(Q.invertible(X*Y), Q.invertible(X)) + False + >>> ask(Q.invertible(X*Z), Q.invertible(X) & Q.invertible(Z)) + True + >>> ask(Q.invertible(X), Q.fullrank(X) & Q.square(X)) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Invertible_matrix + + """ + name = 'invertible' + handler = Dispatcher("InvertibleHandler", doc="Handler for Q.invertible.") + + +class OrthogonalPredicate(Predicate): + """ + Orthogonal matrix predicate. + + Explanation + =========== + + ``Q.orthogonal(x)`` is true iff ``x`` is an orthogonal matrix. + A square matrix ``M`` is an orthogonal matrix if it satisfies + ``M^TM = MM^T = I`` where ``M^T`` is the transpose matrix of + ``M`` and ``I`` is an identity matrix. Note that an orthogonal + matrix is necessarily invertible. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol, Identity + >>> X = MatrixSymbol('X', 2, 2) + >>> Y = MatrixSymbol('Y', 2, 3) + >>> Z = MatrixSymbol('Z', 2, 2) + >>> ask(Q.orthogonal(Y)) + False + >>> ask(Q.orthogonal(X*Z*X), Q.orthogonal(X) & Q.orthogonal(Z)) + True + >>> ask(Q.orthogonal(Identity(3))) + True + >>> ask(Q.invertible(X), Q.orthogonal(X)) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Orthogonal_matrix + + """ + name = 'orthogonal' + handler = Dispatcher("OrthogonalHandler", doc="Handler for key 'orthogonal'.") + + +class UnitaryPredicate(Predicate): + """ + Unitary matrix predicate. + + Explanation + =========== + + ``Q.unitary(x)`` is true iff ``x`` is a unitary matrix. + Unitary matrix is an analogue to orthogonal matrix. A square + matrix ``M`` with complex elements is unitary if :math:``M^TM = MM^T= I`` + where :math:``M^T`` is the conjugate transpose matrix of ``M``. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol, Identity + >>> X = MatrixSymbol('X', 2, 2) + >>> Y = MatrixSymbol('Y', 2, 3) + >>> Z = MatrixSymbol('Z', 2, 2) + >>> ask(Q.unitary(Y)) + False + >>> ask(Q.unitary(X*Z*X), Q.unitary(X) & Q.unitary(Z)) + True + >>> ask(Q.unitary(Identity(3))) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Unitary_matrix + + """ + name = 'unitary' + handler = Dispatcher("UnitaryHandler", doc="Handler for key 'unitary'.") + + +class FullRankPredicate(Predicate): + """ + Fullrank matrix predicate. + + Explanation + =========== + + ``Q.fullrank(x)`` is true iff ``x`` is a full rank matrix. + A matrix is full rank if all rows and columns of the matrix + are linearly independent. A square matrix is full rank iff + its determinant is nonzero. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol, ZeroMatrix, Identity + >>> X = MatrixSymbol('X', 2, 2) + >>> ask(Q.fullrank(X.T), Q.fullrank(X)) + True + >>> ask(Q.fullrank(ZeroMatrix(3, 3))) + False + >>> ask(Q.fullrank(Identity(3))) + True + + """ + name = 'fullrank' + handler = Dispatcher("FullRankHandler", doc="Handler for key 'fullrank'.") + + +class PositiveDefinitePredicate(Predicate): + r""" + Positive definite matrix predicate. + + Explanation + =========== + + If $M$ is a :math:`n \times n` symmetric real matrix, it is said + to be positive definite if :math:`Z^TMZ` is positive for + every non-zero column vector $Z$ of $n$ real numbers. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol, Identity + >>> X = MatrixSymbol('X', 2, 2) + >>> Y = MatrixSymbol('Y', 2, 3) + >>> Z = MatrixSymbol('Z', 2, 2) + >>> ask(Q.positive_definite(Y)) + False + >>> ask(Q.positive_definite(Identity(3))) + True + >>> ask(Q.positive_definite(X + Z), Q.positive_definite(X) & + ... Q.positive_definite(Z)) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Positive-definite_matrix + + """ + name = "positive_definite" + handler = Dispatcher("PositiveDefiniteHandler", doc="Handler for key 'positive_definite'.") + + +class UpperTriangularPredicate(Predicate): + """ + Upper triangular matrix predicate. + + Explanation + =========== + + A matrix $M$ is called upper triangular matrix if :math:`M_{ij}=0` + for :math:`i>> from sympy import Q, ask, ZeroMatrix, Identity + >>> ask(Q.upper_triangular(Identity(3))) + True + >>> ask(Q.upper_triangular(ZeroMatrix(3, 3))) + True + + References + ========== + + .. [1] https://mathworld.wolfram.com/UpperTriangularMatrix.html + + """ + name = "upper_triangular" + handler = Dispatcher("UpperTriangularHandler", doc="Handler for key 'upper_triangular'.") + + +class LowerTriangularPredicate(Predicate): + """ + Lower triangular matrix predicate. + + Explanation + =========== + + A matrix $M$ is called lower triangular matrix if :math:`M_{ij}=0` + for :math:`i>j`. + + Examples + ======== + + >>> from sympy import Q, ask, ZeroMatrix, Identity + >>> ask(Q.lower_triangular(Identity(3))) + True + >>> ask(Q.lower_triangular(ZeroMatrix(3, 3))) + True + + References + ========== + + .. [1] https://mathworld.wolfram.com/LowerTriangularMatrix.html + + """ + name = "lower_triangular" + handler = Dispatcher("LowerTriangularHandler", doc="Handler for key 'lower_triangular'.") + + +class DiagonalPredicate(Predicate): + """ + Diagonal matrix predicate. + + Explanation + =========== + + ``Q.diagonal(x)`` is true iff ``x`` is a diagonal matrix. A diagonal + matrix is a matrix in which the entries outside the main diagonal + are all zero. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol, ZeroMatrix + >>> X = MatrixSymbol('X', 2, 2) + >>> ask(Q.diagonal(ZeroMatrix(3, 3))) + True + >>> ask(Q.diagonal(X), Q.lower_triangular(X) & + ... Q.upper_triangular(X)) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Diagonal_matrix + + """ + name = "diagonal" + handler = Dispatcher("DiagonalHandler", doc="Handler for key 'diagonal'.") + + +class IntegerElementsPredicate(Predicate): + """ + Integer elements matrix predicate. + + Explanation + =========== + + ``Q.integer_elements(x)`` is true iff all the elements of ``x`` + are integers. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol + >>> X = MatrixSymbol('X', 4, 4) + >>> ask(Q.integer(X[1, 2]), Q.integer_elements(X)) + True + + """ + name = "integer_elements" + handler = Dispatcher("IntegerElementsHandler", doc="Handler for key 'integer_elements'.") + + +class RealElementsPredicate(Predicate): + """ + Real elements matrix predicate. + + Explanation + =========== + + ``Q.real_elements(x)`` is true iff all the elements of ``x`` + are real numbers. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol + >>> X = MatrixSymbol('X', 4, 4) + >>> ask(Q.real(X[1, 2]), Q.real_elements(X)) + True + + """ + name = "real_elements" + handler = Dispatcher("RealElementsHandler", doc="Handler for key 'real_elements'.") + + +class ComplexElementsPredicate(Predicate): + """ + Complex elements matrix predicate. + + Explanation + =========== + + ``Q.complex_elements(x)`` is true iff all the elements of ``x`` + are complex numbers. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol + >>> X = MatrixSymbol('X', 4, 4) + >>> ask(Q.complex(X[1, 2]), Q.complex_elements(X)) + True + >>> ask(Q.complex_elements(X), Q.integer_elements(X)) + True + + """ + name = "complex_elements" + handler = Dispatcher("ComplexElementsHandler", doc="Handler for key 'complex_elements'.") + + +class SingularPredicate(Predicate): + """ + Singular matrix predicate. + + A matrix is singular iff the value of its determinant is 0. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol + >>> X = MatrixSymbol('X', 4, 4) + >>> ask(Q.singular(X), Q.invertible(X)) + False + >>> ask(Q.singular(X), ~Q.invertible(X)) + True + + References + ========== + + .. [1] https://mathworld.wolfram.com/SingularMatrix.html + + """ + name = "singular" + handler = Dispatcher("SingularHandler", doc="Predicate fore key 'singular'.") + + +class NormalPredicate(Predicate): + """ + Normal matrix predicate. + + A matrix is normal if it commutes with its conjugate transpose. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol + >>> X = MatrixSymbol('X', 4, 4) + >>> ask(Q.normal(X), Q.unitary(X)) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Normal_matrix + + """ + name = "normal" + handler = Dispatcher("NormalHandler", doc="Predicate fore key 'normal'.") + + +class TriangularPredicate(Predicate): + """ + Triangular matrix predicate. + + Explanation + =========== + + ``Q.triangular(X)`` is true if ``X`` is one that is either lower + triangular or upper triangular. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol + >>> X = MatrixSymbol('X', 4, 4) + >>> ask(Q.triangular(X), Q.upper_triangular(X)) + True + >>> ask(Q.triangular(X), Q.lower_triangular(X)) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Triangular_matrix + + """ + name = "triangular" + handler = Dispatcher("TriangularHandler", doc="Predicate fore key 'triangular'.") + + +class UnitTriangularPredicate(Predicate): + """ + Unit triangular matrix predicate. + + Explanation + =========== + + A unit triangular matrix is a triangular matrix with 1s + on the diagonal. + + Examples + ======== + + >>> from sympy import Q, ask, MatrixSymbol + >>> X = MatrixSymbol('X', 4, 4) + >>> ask(Q.triangular(X), Q.unit_triangular(X)) + True + + """ + name = "unit_triangular" + handler = Dispatcher("UnitTriangularHandler", doc="Predicate fore key 'unit_triangular'.") diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/ntheory.py b/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/ntheory.py new file mode 100644 index 0000000000000000000000000000000000000000..6c598e0ed1bd4a1170aa28044f9ae6de2fa1a1e0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/ntheory.py @@ -0,0 +1,126 @@ +from sympy.assumptions import Predicate +from sympy.multipledispatch import Dispatcher + + +class PrimePredicate(Predicate): + """ + Prime number predicate. + + Explanation + =========== + + ``ask(Q.prime(x))`` is true iff ``x`` is a natural number greater + than 1 that has no positive divisors other than ``1`` and the + number itself. + + Examples + ======== + + >>> from sympy import Q, ask + >>> ask(Q.prime(0)) + False + >>> ask(Q.prime(1)) + False + >>> ask(Q.prime(2)) + True + >>> ask(Q.prime(20)) + False + >>> ask(Q.prime(-3)) + False + + """ + name = 'prime' + handler = Dispatcher( + "PrimeHandler", + doc=("Handler for key 'prime'. Test that an expression represents a prime" + " number. When the expression is an exact number, the result (when True)" + " is subject to the limitations of isprime() which is used to return the " + "result.") + ) + + +class CompositePredicate(Predicate): + """ + Composite number predicate. + + Explanation + =========== + + ``ask(Q.composite(x))`` is true iff ``x`` is a positive integer and has + at least one positive divisor other than ``1`` and the number itself. + + Examples + ======== + + >>> from sympy import Q, ask + >>> ask(Q.composite(0)) + False + >>> ask(Q.composite(1)) + False + >>> ask(Q.composite(2)) + False + >>> ask(Q.composite(20)) + True + + """ + name = 'composite' + handler = Dispatcher("CompositeHandler", doc="Handler for key 'composite'.") + + +class EvenPredicate(Predicate): + """ + Even number predicate. + + Explanation + =========== + + ``ask(Q.even(x))`` is true iff ``x`` belongs to the set of even + integers. + + Examples + ======== + + >>> from sympy import Q, ask, pi + >>> ask(Q.even(0)) + True + >>> ask(Q.even(2)) + True + >>> ask(Q.even(3)) + False + >>> ask(Q.even(pi)) + False + + """ + name = 'even' + handler = Dispatcher("EvenHandler", doc="Handler for key 'even'.") + + +class OddPredicate(Predicate): + """ + Odd number predicate. + + Explanation + =========== + + ``ask(Q.odd(x))`` is true iff ``x`` belongs to the set of odd numbers. + + Examples + ======== + + >>> from sympy import Q, ask, pi + >>> ask(Q.odd(0)) + False + >>> ask(Q.odd(2)) + False + >>> ask(Q.odd(3)) + True + >>> ask(Q.odd(pi)) + False + + """ + name = 'odd' + handler = Dispatcher( + "OddHandler", + doc=("Handler for key 'odd'. Test that an expression represents an odd" + " number.") + ) diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/order.py b/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/order.py new file mode 100644 index 0000000000000000000000000000000000000000..86bfb2ae49789efd5b0df99e2cfc63984e956dd0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/order.py @@ -0,0 +1,390 @@ +from sympy.assumptions import Predicate +from sympy.multipledispatch import Dispatcher + + +class NegativePredicate(Predicate): + r""" + Negative number predicate. + + Explanation + =========== + + ``Q.negative(x)`` is true iff ``x`` is a real number and :math:`x < 0`, that is, + it is in the interval :math:`(-\infty, 0)`. Note in particular that negative + infinity is not negative. + + A few important facts about negative numbers: + + - Note that ``Q.nonnegative`` and ``~Q.negative`` are *not* the same + thing. ``~Q.negative(x)`` simply means that ``x`` is not negative, + whereas ``Q.nonnegative(x)`` means that ``x`` is real and not + negative, i.e., ``Q.nonnegative(x)`` is logically equivalent to + ``Q.zero(x) | Q.positive(x)``. So for example, ``~Q.negative(I)`` is + true, whereas ``Q.nonnegative(I)`` is false. + + - See the documentation of ``Q.real`` for more information about + related facts. + + Examples + ======== + + >>> from sympy import Q, ask, symbols, I + >>> x = symbols('x') + >>> ask(Q.negative(x), Q.real(x) & ~Q.positive(x) & ~Q.zero(x)) + True + >>> ask(Q.negative(-1)) + True + >>> ask(Q.nonnegative(I)) + False + >>> ask(~Q.negative(I)) + True + + """ + name = 'negative' + handler = Dispatcher( + "NegativeHandler", + doc=("Handler for Q.negative. Test that an expression is strictly less" + " than zero.") + ) + + +class NonNegativePredicate(Predicate): + """ + Nonnegative real number predicate. + + Explanation + =========== + + ``ask(Q.nonnegative(x))`` is true iff ``x`` belongs to the set of + positive numbers including zero. + + - Note that ``Q.nonnegative`` and ``~Q.negative`` are *not* the same + thing. ``~Q.negative(x)`` simply means that ``x`` is not negative, + whereas ``Q.nonnegative(x)`` means that ``x`` is real and not + negative, i.e., ``Q.nonnegative(x)`` is logically equivalent to + ``Q.zero(x) | Q.positive(x)``. So for example, ``~Q.negative(I)`` is + true, whereas ``Q.nonnegative(I)`` is false. + + Examples + ======== + + >>> from sympy import Q, ask, I + >>> ask(Q.nonnegative(1)) + True + >>> ask(Q.nonnegative(0)) + True + >>> ask(Q.nonnegative(-1)) + False + >>> ask(Q.nonnegative(I)) + False + >>> ask(Q.nonnegative(-I)) + False + + """ + name = 'nonnegative' + handler = Dispatcher( + "NonNegativeHandler", + doc=("Handler for Q.nonnegative.") + ) + + +class NonZeroPredicate(Predicate): + """ + Nonzero real number predicate. + + Explanation + =========== + + ``ask(Q.nonzero(x))`` is true iff ``x`` is real and ``x`` is not zero. Note in + particular that ``Q.nonzero(x)`` is false if ``x`` is not real. Use + ``~Q.zero(x)`` if you want the negation of being zero without any real + assumptions. + + A few important facts about nonzero numbers: + + - ``Q.nonzero`` is logically equivalent to ``Q.positive | Q.negative``. + + - See the documentation of ``Q.real`` for more information about + related facts. + + Examples + ======== + + >>> from sympy import Q, ask, symbols, I, oo + >>> x = symbols('x') + >>> print(ask(Q.nonzero(x), ~Q.zero(x))) + None + >>> ask(Q.nonzero(x), Q.positive(x)) + True + >>> ask(Q.nonzero(x), Q.zero(x)) + False + >>> ask(Q.nonzero(0)) + False + >>> ask(Q.nonzero(I)) + False + >>> ask(~Q.zero(I)) + True + >>> ask(Q.nonzero(oo)) + False + + """ + name = 'nonzero' + handler = Dispatcher( + "NonZeroHandler", + doc=("Handler for key 'nonzero'. Test that an expression is not identically" + " zero.") + ) + + +class ZeroPredicate(Predicate): + """ + Zero number predicate. + + Explanation + =========== + + ``ask(Q.zero(x))`` is true iff the value of ``x`` is zero. + + Examples + ======== + + >>> from sympy import ask, Q, oo, symbols + >>> x, y = symbols('x, y') + >>> ask(Q.zero(0)) + True + >>> ask(Q.zero(1/oo)) + True + >>> print(ask(Q.zero(0*oo))) + None + >>> ask(Q.zero(1)) + False + >>> ask(Q.zero(x*y), Q.zero(x) | Q.zero(y)) + True + + """ + name = 'zero' + handler = Dispatcher( + "ZeroHandler", + doc="Handler for key 'zero'." + ) + + +class NonPositivePredicate(Predicate): + """ + Nonpositive real number predicate. + + Explanation + =========== + + ``ask(Q.nonpositive(x))`` is true iff ``x`` belongs to the set of + negative numbers including zero. + + - Note that ``Q.nonpositive`` and ``~Q.positive`` are *not* the same + thing. ``~Q.positive(x)`` simply means that ``x`` is not positive, + whereas ``Q.nonpositive(x)`` means that ``x`` is real and not + positive, i.e., ``Q.nonpositive(x)`` is logically equivalent to + `Q.negative(x) | Q.zero(x)``. So for example, ``~Q.positive(I)`` is + true, whereas ``Q.nonpositive(I)`` is false. + + Examples + ======== + + >>> from sympy import Q, ask, I + + >>> ask(Q.nonpositive(-1)) + True + >>> ask(Q.nonpositive(0)) + True + >>> ask(Q.nonpositive(1)) + False + >>> ask(Q.nonpositive(I)) + False + >>> ask(Q.nonpositive(-I)) + False + + """ + name = 'nonpositive' + handler = Dispatcher( + "NonPositiveHandler", + doc="Handler for key 'nonpositive'." + ) + + +class PositivePredicate(Predicate): + r""" + Positive real number predicate. + + Explanation + =========== + + ``Q.positive(x)`` is true iff ``x`` is real and `x > 0`, that is if ``x`` + is in the interval `(0, \infty)`. In particular, infinity is not + positive. + + A few important facts about positive numbers: + + - Note that ``Q.nonpositive`` and ``~Q.positive`` are *not* the same + thing. ``~Q.positive(x)`` simply means that ``x`` is not positive, + whereas ``Q.nonpositive(x)`` means that ``x`` is real and not + positive, i.e., ``Q.nonpositive(x)`` is logically equivalent to + `Q.negative(x) | Q.zero(x)``. So for example, ``~Q.positive(I)`` is + true, whereas ``Q.nonpositive(I)`` is false. + + - See the documentation of ``Q.real`` for more information about + related facts. + + Examples + ======== + + >>> from sympy import Q, ask, symbols, I + >>> x = symbols('x') + >>> ask(Q.positive(x), Q.real(x) & ~Q.negative(x) & ~Q.zero(x)) + True + >>> ask(Q.positive(1)) + True + >>> ask(Q.nonpositive(I)) + False + >>> ask(~Q.positive(I)) + True + + """ + name = 'positive' + handler = Dispatcher( + "PositiveHandler", + doc=("Handler for key 'positive'. Test that an expression is strictly" + " greater than zero.") + ) + + +class ExtendedPositivePredicate(Predicate): + r""" + Positive extended real number predicate. + + Explanation + =========== + + ``Q.extended_positive(x)`` is true iff ``x`` is extended real and + `x > 0`, that is if ``x`` is in the interval `(0, \infty]`. + + Examples + ======== + + >>> from sympy import ask, I, oo, Q + >>> ask(Q.extended_positive(1)) + True + >>> ask(Q.extended_positive(oo)) + True + >>> ask(Q.extended_positive(I)) + False + + """ + name = 'extended_positive' + handler = Dispatcher("ExtendedPositiveHandler") + + +class ExtendedNegativePredicate(Predicate): + r""" + Negative extended real number predicate. + + Explanation + =========== + + ``Q.extended_negative(x)`` is true iff ``x`` is extended real and + `x < 0`, that is if ``x`` is in the interval `[-\infty, 0)`. + + Examples + ======== + + >>> from sympy import ask, I, oo, Q + >>> ask(Q.extended_negative(-1)) + True + >>> ask(Q.extended_negative(-oo)) + True + >>> ask(Q.extended_negative(-I)) + False + + """ + name = 'extended_negative' + handler = Dispatcher("ExtendedNegativeHandler") + + +class ExtendedNonZeroPredicate(Predicate): + """ + Nonzero extended real number predicate. + + Explanation + =========== + + ``ask(Q.extended_nonzero(x))`` is true iff ``x`` is extended real and + ``x`` is not zero. + + Examples + ======== + + >>> from sympy import ask, I, oo, Q + >>> ask(Q.extended_nonzero(-1)) + True + >>> ask(Q.extended_nonzero(oo)) + True + >>> ask(Q.extended_nonzero(I)) + False + + """ + name = 'extended_nonzero' + handler = Dispatcher("ExtendedNonZeroHandler") + + +class ExtendedNonPositivePredicate(Predicate): + """ + Nonpositive extended real number predicate. + + Explanation + =========== + + ``ask(Q.extended_nonpositive(x))`` is true iff ``x`` is extended real and + ``x`` is not positive. + + Examples + ======== + + >>> from sympy import ask, I, oo, Q + >>> ask(Q.extended_nonpositive(-1)) + True + >>> ask(Q.extended_nonpositive(oo)) + False + >>> ask(Q.extended_nonpositive(0)) + True + >>> ask(Q.extended_nonpositive(I)) + False + + """ + name = 'extended_nonpositive' + handler = Dispatcher("ExtendedNonPositiveHandler") + + +class ExtendedNonNegativePredicate(Predicate): + """ + Nonnegative extended real number predicate. + + Explanation + =========== + + ``ask(Q.extended_nonnegative(x))`` is true iff ``x`` is extended real and + ``x`` is not negative. + + Examples + ======== + + >>> from sympy import ask, I, oo, Q + >>> ask(Q.extended_nonnegative(-1)) + False + >>> ask(Q.extended_nonnegative(oo)) + True + >>> ask(Q.extended_nonnegative(0)) + True + >>> ask(Q.extended_nonnegative(I)) + False + + """ + name = 'extended_nonnegative' + handler = Dispatcher("ExtendedNonNegativeHandler") diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/sets.py b/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/sets.py new file mode 100644 index 0000000000000000000000000000000000000000..18261cee2d9de65df14a31a56b2cd22328328ed0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/predicates/sets.py @@ -0,0 +1,399 @@ +from sympy.assumptions import Predicate +from sympy.multipledispatch import Dispatcher + + +class IntegerPredicate(Predicate): + """ + Integer predicate. + + Explanation + =========== + + ``Q.integer(x)`` is true iff ``x`` belongs to the set of integer + numbers. + + Examples + ======== + + >>> from sympy import Q, ask, S + >>> ask(Q.integer(5)) + True + >>> ask(Q.integer(S(1)/2)) + False + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Integer + + """ + name = 'integer' + handler = Dispatcher( + "IntegerHandler", + doc=("Handler for Q.integer.\n\n" + "Test that an expression belongs to the field of integer numbers.") + ) + + +class NonIntegerPredicate(Predicate): + """ + Non-integer extended real predicate. + """ + name = 'noninteger' + handler = Dispatcher( + "NonIntegerHandler", + doc=("Handler for Q.noninteger.\n\n" + "Test that an expression is a non-integer extended real number.") + ) + + +class RationalPredicate(Predicate): + """ + Rational number predicate. + + Explanation + =========== + + ``Q.rational(x)`` is true iff ``x`` belongs to the set of + rational numbers. + + Examples + ======== + + >>> from sympy import ask, Q, pi, S + >>> ask(Q.rational(0)) + True + >>> ask(Q.rational(S(1)/2)) + True + >>> ask(Q.rational(pi)) + False + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Rational_number + + """ + name = 'rational' + handler = Dispatcher( + "RationalHandler", + doc=("Handler for Q.rational.\n\n" + "Test that an expression belongs to the field of rational numbers.") + ) + + +class IrrationalPredicate(Predicate): + """ + Irrational number predicate. + + Explanation + =========== + + ``Q.irrational(x)`` is true iff ``x`` is any real number that + cannot be expressed as a ratio of integers. + + Examples + ======== + + >>> from sympy import ask, Q, pi, S, I + >>> ask(Q.irrational(0)) + False + >>> ask(Q.irrational(S(1)/2)) + False + >>> ask(Q.irrational(pi)) + True + >>> ask(Q.irrational(I)) + False + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Irrational_number + + """ + name = 'irrational' + handler = Dispatcher( + "IrrationalHandler", + doc=("Handler for Q.irrational.\n\n" + "Test that an expression is irrational numbers.") + ) + + +class RealPredicate(Predicate): + r""" + Real number predicate. + + Explanation + =========== + + ``Q.real(x)`` is true iff ``x`` is a real number, i.e., it is in the + interval `(-\infty, \infty)`. Note that, in particular the + infinities are not real. Use ``Q.extended_real`` if you want to + consider those as well. + + A few important facts about reals: + + - Every real number is positive, negative, or zero. Furthermore, + because these sets are pairwise disjoint, each real number is + exactly one of those three. + + - Every real number is also complex. + + - Every real number is finite. + + - Every real number is either rational or irrational. + + - Every real number is either algebraic or transcendental. + + - The facts ``Q.negative``, ``Q.zero``, ``Q.positive``, + ``Q.nonnegative``, ``Q.nonpositive``, ``Q.nonzero``, + ``Q.integer``, ``Q.rational``, and ``Q.irrational`` all imply + ``Q.real``, as do all facts that imply those facts. + + - The facts ``Q.algebraic``, and ``Q.transcendental`` do not imply + ``Q.real``; they imply ``Q.complex``. An algebraic or + transcendental number may or may not be real. + + - The "non" facts (i.e., ``Q.nonnegative``, ``Q.nonzero``, + ``Q.nonpositive`` and ``Q.noninteger``) are not equivalent to + not the fact, but rather, not the fact *and* ``Q.real``. + For example, ``Q.nonnegative`` means ``~Q.negative & Q.real``. + So for example, ``I`` is not nonnegative, nonzero, or + nonpositive. + + Examples + ======== + + >>> from sympy import Q, ask, symbols + >>> x = symbols('x') + >>> ask(Q.real(x), Q.positive(x)) + True + >>> ask(Q.real(0)) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Real_number + + """ + name = 'real' + handler = Dispatcher( + "RealHandler", + doc=("Handler for Q.real.\n\n" + "Test that an expression belongs to the field of real numbers.") + ) + + +class ExtendedRealPredicate(Predicate): + r""" + Extended real predicate. + + Explanation + =========== + + ``Q.extended_real(x)`` is true iff ``x`` is a real number or + `\{-\infty, \infty\}`. + + See documentation of ``Q.real`` for more information about related + facts. + + Examples + ======== + + >>> from sympy import ask, Q, oo, I + >>> ask(Q.extended_real(1)) + True + >>> ask(Q.extended_real(I)) + False + >>> ask(Q.extended_real(oo)) + True + + """ + name = 'extended_real' + handler = Dispatcher( + "ExtendedRealHandler", + doc=("Handler for Q.extended_real.\n\n" + "Test that an expression belongs to the field of extended real\n" + "numbers, that is real numbers union {Infinity, -Infinity}.") + ) + + +class HermitianPredicate(Predicate): + """ + Hermitian predicate. + + Explanation + =========== + + ``ask(Q.hermitian(x))`` is true iff ``x`` belongs to the set of + Hermitian operators. + + References + ========== + + .. [1] https://mathworld.wolfram.com/HermitianOperator.html + + """ + # TODO: Add examples + name = 'hermitian' + handler = Dispatcher( + "HermitianHandler", + doc=("Handler for Q.hermitian.\n\n" + "Test that an expression belongs to the field of Hermitian operators.") + ) + + +class ComplexPredicate(Predicate): + """ + Complex number predicate. + + Explanation + =========== + + ``Q.complex(x)`` is true iff ``x`` belongs to the set of complex + numbers. Note that every complex number is finite. + + Examples + ======== + + >>> from sympy import Q, Symbol, ask, I, oo + >>> x = Symbol('x') + >>> ask(Q.complex(0)) + True + >>> ask(Q.complex(2 + 3*I)) + True + >>> ask(Q.complex(oo)) + False + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Complex_number + + """ + name = 'complex' + handler = Dispatcher( + "ComplexHandler", + doc=("Handler for Q.complex.\n\n" + "Test that an expression belongs to the field of complex numbers.") + ) + + +class ImaginaryPredicate(Predicate): + """ + Imaginary number predicate. + + Explanation + =========== + + ``Q.imaginary(x)`` is true iff ``x`` can be written as a real + number multiplied by the imaginary unit ``I``. Please note that ``0`` + is not considered to be an imaginary number. + + Examples + ======== + + >>> from sympy import Q, ask, I + >>> ask(Q.imaginary(3*I)) + True + >>> ask(Q.imaginary(2 + 3*I)) + False + >>> ask(Q.imaginary(0)) + False + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Imaginary_number + + """ + name = 'imaginary' + handler = Dispatcher( + "ImaginaryHandler", + doc=("Handler for Q.imaginary.\n\n" + "Test that an expression belongs to the field of imaginary numbers,\n" + "that is, numbers in the form x*I, where x is real.") + ) + + +class AntihermitianPredicate(Predicate): + """ + Antihermitian predicate. + + Explanation + =========== + + ``Q.antihermitian(x)`` is true iff ``x`` belongs to the field of + antihermitian operators, i.e., operators in the form ``x*I``, where + ``x`` is Hermitian. + + References + ========== + + .. [1] https://mathworld.wolfram.com/HermitianOperator.html + + """ + # TODO: Add examples + name = 'antihermitian' + handler = Dispatcher( + "AntiHermitianHandler", + doc=("Handler for Q.antihermitian.\n\n" + "Test that an expression belongs to the field of anti-Hermitian\n" + "operators, that is, operators in the form x*I, where x is Hermitian.") + ) + + +class AlgebraicPredicate(Predicate): + r""" + Algebraic number predicate. + + Explanation + =========== + + ``Q.algebraic(x)`` is true iff ``x`` belongs to the set of + algebraic numbers. ``x`` is algebraic if there is some polynomial + in ``p(x)\in \mathbb\{Q\}[x]`` such that ``p(x) = 0``. + + Examples + ======== + + >>> from sympy import ask, Q, sqrt, I, pi + >>> ask(Q.algebraic(sqrt(2))) + True + >>> ask(Q.algebraic(I)) + True + >>> ask(Q.algebraic(pi)) + False + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Algebraic_number + + """ + name = 'algebraic' + AlgebraicHandler = Dispatcher( + "AlgebraicHandler", + doc="""Handler for Q.algebraic key.""" + ) + + +class TranscendentalPredicate(Predicate): + """ + Transcedental number predicate. + + Explanation + =========== + + ``Q.transcendental(x)`` is true iff ``x`` belongs to the set of + transcendental numbers. A transcendental number is a real + or complex number that is not algebraic. + + """ + # TODO: Add examples + name = 'transcendental' + handler = Dispatcher( + "Transcendental", + doc="""Handler for Q.transcendental key.""" + ) diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/refine.py b/venv/lib/python3.11/site-packages/sympy/assumptions/refine.py new file mode 100644 index 0000000000000000000000000000000000000000..c36a4e1cdb40f1b59a96f60a3b36182b587920fa --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/refine.py @@ -0,0 +1,405 @@ +from __future__ import annotations +from typing import Callable + +from sympy.core import S, Add, Expr, Basic, Mul, Pow, Rational +from sympy.core.logic import fuzzy_not +from sympy.logic.boolalg import Boolean + +from sympy.assumptions import ask, Q # type: ignore + + +def refine(expr, assumptions=True): + """ + Simplify an expression using assumptions. + + Explanation + =========== + + Unlike :func:`~.simplify` which performs structural simplification + without any assumption, this function transforms the expression into + the form which is only valid under certain assumptions. Note that + ``simplify()`` is generally not done in refining process. + + Refining boolean expression involves reducing it to ``S.true`` or + ``S.false``. Unlike :func:`~.ask`, the expression will not be reduced + if the truth value cannot be determined. + + Examples + ======== + + >>> from sympy import refine, sqrt, Q + >>> from sympy.abc import x + >>> refine(sqrt(x**2), Q.real(x)) + Abs(x) + >>> refine(sqrt(x**2), Q.positive(x)) + x + + >>> refine(Q.real(x), Q.positive(x)) + True + >>> refine(Q.positive(x), Q.real(x)) + Q.positive(x) + + See Also + ======== + + sympy.simplify.simplify.simplify : Structural simplification without assumptions. + sympy.assumptions.ask.ask : Query for boolean expressions using assumptions. + """ + if not isinstance(expr, Basic): + return expr + + if not expr.is_Atom: + args = [refine(arg, assumptions) for arg in expr.args] + # TODO: this will probably not work with Integral or Polynomial + expr = expr.func(*args) + if hasattr(expr, '_eval_refine'): + ref_expr = expr._eval_refine(assumptions) + if ref_expr is not None: + return ref_expr + name = expr.__class__.__name__ + handler = handlers_dict.get(name, None) + if handler is None: + return expr + new_expr = handler(expr, assumptions) + if (new_expr is None) or (expr == new_expr): + return expr + if not isinstance(new_expr, Expr): + return new_expr + return refine(new_expr, assumptions) + + +def refine_abs(expr, assumptions): + """ + Handler for the absolute value. + + Examples + ======== + + >>> from sympy import Q, Abs + >>> from sympy.assumptions.refine import refine_abs + >>> from sympy.abc import x + >>> refine_abs(Abs(x), Q.real(x)) + >>> refine_abs(Abs(x), Q.positive(x)) + x + >>> refine_abs(Abs(x), Q.negative(x)) + -x + + """ + from sympy.functions.elementary.complexes import Abs + arg = expr.args[0] + if ask(Q.real(arg), assumptions) and \ + fuzzy_not(ask(Q.negative(arg), assumptions)): + # if it's nonnegative + return arg + if ask(Q.negative(arg), assumptions): + return -arg + # arg is Mul + if isinstance(arg, Mul): + r = [refine(abs(a), assumptions) for a in arg.args] + non_abs = [] + in_abs = [] + for i in r: + if isinstance(i, Abs): + in_abs.append(i.args[0]) + else: + non_abs.append(i) + return Mul(*non_abs) * Abs(Mul(*in_abs)) + + +def refine_Pow(expr, assumptions): + """ + Handler for instances of Pow. + + Examples + ======== + + >>> from sympy import Q + >>> from sympy.assumptions.refine import refine_Pow + >>> from sympy.abc import x,y,z + >>> refine_Pow((-1)**x, Q.real(x)) + >>> refine_Pow((-1)**x, Q.even(x)) + 1 + >>> refine_Pow((-1)**x, Q.odd(x)) + -1 + + For powers of -1, even parts of the exponent can be simplified: + + >>> refine_Pow((-1)**(x+y), Q.even(x)) + (-1)**y + >>> refine_Pow((-1)**(x+y+z), Q.odd(x) & Q.odd(z)) + (-1)**y + >>> refine_Pow((-1)**(x+y+2), Q.odd(x)) + (-1)**(y + 1) + >>> refine_Pow((-1)**(x+3), True) + (-1)**(x + 1) + + """ + from sympy.functions.elementary.complexes import Abs + from sympy.functions import sign + if isinstance(expr.base, Abs): + if ask(Q.real(expr.base.args[0]), assumptions) and \ + ask(Q.even(expr.exp), assumptions): + return expr.base.args[0] ** expr.exp + if ask(Q.real(expr.base), assumptions): + if expr.base.is_number: + if ask(Q.even(expr.exp), assumptions): + return abs(expr.base) ** expr.exp + if ask(Q.odd(expr.exp), assumptions): + return sign(expr.base) * abs(expr.base) ** expr.exp + if isinstance(expr.exp, Rational): + if isinstance(expr.base, Pow): + return abs(expr.base.base) ** (expr.base.exp * expr.exp) + + if expr.base is S.NegativeOne: + if expr.exp.is_Add: + + old = expr + + # For powers of (-1) we can remove + # - even terms + # - pairs of odd terms + # - a single odd term + 1 + # - A numerical constant N can be replaced with mod(N,2) + + coeff, terms = expr.exp.as_coeff_add() + terms = set(terms) + even_terms = set() + odd_terms = set() + initial_number_of_terms = len(terms) + + for t in terms: + if ask(Q.even(t), assumptions): + even_terms.add(t) + elif ask(Q.odd(t), assumptions): + odd_terms.add(t) + + terms -= even_terms + if len(odd_terms) % 2: + terms -= odd_terms + new_coeff = (coeff + S.One) % 2 + else: + terms -= odd_terms + new_coeff = coeff % 2 + + if new_coeff != coeff or len(terms) < initial_number_of_terms: + terms.add(new_coeff) + expr = expr.base**(Add(*terms)) + + # Handle (-1)**((-1)**n/2 + m/2) + e2 = 2*expr.exp + if ask(Q.even(e2), assumptions): + if e2.could_extract_minus_sign(): + e2 *= expr.base + if e2.is_Add: + i, p = e2.as_two_terms() + if p.is_Pow and p.base is S.NegativeOne: + if ask(Q.integer(p.exp), assumptions): + i = (i + 1)/2 + if ask(Q.even(i), assumptions): + return expr.base**p.exp + elif ask(Q.odd(i), assumptions): + return expr.base**(p.exp + 1) + else: + return expr.base**(p.exp + i) + + if old != expr: + return expr + + +def refine_atan2(expr, assumptions): + """ + Handler for the atan2 function. + + Examples + ======== + + >>> from sympy import Q, atan2 + >>> from sympy.assumptions.refine import refine_atan2 + >>> from sympy.abc import x, y + >>> refine_atan2(atan2(y,x), Q.real(y) & Q.positive(x)) + atan(y/x) + >>> refine_atan2(atan2(y,x), Q.negative(y) & Q.negative(x)) + atan(y/x) - pi + >>> refine_atan2(atan2(y,x), Q.positive(y) & Q.negative(x)) + atan(y/x) + pi + >>> refine_atan2(atan2(y,x), Q.zero(y) & Q.negative(x)) + pi + >>> refine_atan2(atan2(y,x), Q.positive(y) & Q.zero(x)) + pi/2 + >>> refine_atan2(atan2(y,x), Q.negative(y) & Q.zero(x)) + -pi/2 + >>> refine_atan2(atan2(y,x), Q.zero(y) & Q.zero(x)) + nan + """ + from sympy.functions.elementary.trigonometric import atan + y, x = expr.args + if ask(Q.real(y) & Q.positive(x), assumptions): + return atan(y / x) + elif ask(Q.negative(y) & Q.negative(x), assumptions): + return atan(y / x) - S.Pi + elif ask(Q.positive(y) & Q.negative(x), assumptions): + return atan(y / x) + S.Pi + elif ask(Q.zero(y) & Q.negative(x), assumptions): + return S.Pi + elif ask(Q.positive(y) & Q.zero(x), assumptions): + return S.Pi/2 + elif ask(Q.negative(y) & Q.zero(x), assumptions): + return -S.Pi/2 + elif ask(Q.zero(y) & Q.zero(x), assumptions): + return S.NaN + else: + return expr + + +def refine_re(expr, assumptions): + """ + Handler for real part. + + Examples + ======== + + >>> from sympy.assumptions.refine import refine_re + >>> from sympy import Q, re + >>> from sympy.abc import x + >>> refine_re(re(x), Q.real(x)) + x + >>> refine_re(re(x), Q.imaginary(x)) + 0 + """ + arg = expr.args[0] + if ask(Q.real(arg), assumptions): + return arg + if ask(Q.imaginary(arg), assumptions): + return S.Zero + return _refine_reim(expr, assumptions) + + +def refine_im(expr, assumptions): + """ + Handler for imaginary part. + + Explanation + =========== + + >>> from sympy.assumptions.refine import refine_im + >>> from sympy import Q, im + >>> from sympy.abc import x + >>> refine_im(im(x), Q.real(x)) + 0 + >>> refine_im(im(x), Q.imaginary(x)) + -I*x + """ + arg = expr.args[0] + if ask(Q.real(arg), assumptions): + return S.Zero + if ask(Q.imaginary(arg), assumptions): + return - S.ImaginaryUnit * arg + return _refine_reim(expr, assumptions) + +def refine_arg(expr, assumptions): + """ + Handler for complex argument + + Explanation + =========== + + >>> from sympy.assumptions.refine import refine_arg + >>> from sympy import Q, arg + >>> from sympy.abc import x + >>> refine_arg(arg(x), Q.positive(x)) + 0 + >>> refine_arg(arg(x), Q.negative(x)) + pi + """ + rg = expr.args[0] + if ask(Q.positive(rg), assumptions): + return S.Zero + if ask(Q.negative(rg), assumptions): + return S.Pi + return None + + +def _refine_reim(expr, assumptions): + # Helper function for refine_re & refine_im + expanded = expr.expand(complex = True) + if expanded != expr: + refined = refine(expanded, assumptions) + if refined != expanded: + return refined + # Best to leave the expression as is + return None + + +def refine_sign(expr, assumptions): + """ + Handler for sign. + + Examples + ======== + + >>> from sympy.assumptions.refine import refine_sign + >>> from sympy import Symbol, Q, sign, im + >>> x = Symbol('x', real = True) + >>> expr = sign(x) + >>> refine_sign(expr, Q.positive(x) & Q.nonzero(x)) + 1 + >>> refine_sign(expr, Q.negative(x) & Q.nonzero(x)) + -1 + >>> refine_sign(expr, Q.zero(x)) + 0 + >>> y = Symbol('y', imaginary = True) + >>> expr = sign(y) + >>> refine_sign(expr, Q.positive(im(y))) + I + >>> refine_sign(expr, Q.negative(im(y))) + -I + """ + arg = expr.args[0] + if ask(Q.zero(arg), assumptions): + return S.Zero + if ask(Q.real(arg)): + if ask(Q.positive(arg), assumptions): + return S.One + if ask(Q.negative(arg), assumptions): + return S.NegativeOne + if ask(Q.imaginary(arg)): + arg_re, arg_im = arg.as_real_imag() + if ask(Q.positive(arg_im), assumptions): + return S.ImaginaryUnit + if ask(Q.negative(arg_im), assumptions): + return -S.ImaginaryUnit + return expr + + +def refine_matrixelement(expr, assumptions): + """ + Handler for symmetric part. + + Examples + ======== + + >>> from sympy.assumptions.refine import refine_matrixelement + >>> from sympy import MatrixSymbol, Q + >>> X = MatrixSymbol('X', 3, 3) + >>> refine_matrixelement(X[0, 1], Q.symmetric(X)) + X[0, 1] + >>> refine_matrixelement(X[1, 0], Q.symmetric(X)) + X[0, 1] + """ + from sympy.matrices.expressions.matexpr import MatrixElement + matrix, i, j = expr.args + if ask(Q.symmetric(matrix), assumptions): + if (i - j).could_extract_minus_sign(): + return expr + return MatrixElement(matrix, j, i) + +handlers_dict: dict[str, Callable[[Expr, Boolean], Expr]] = { + 'Abs': refine_abs, + 'Pow': refine_Pow, + 'atan2': refine_atan2, + 're': refine_re, + 'im': refine_im, + 'arg': refine_arg, + 'sign': refine_sign, + 'MatrixElement': refine_matrixelement +} diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/relation/__init__.py b/venv/lib/python3.11/site-packages/sympy/assumptions/relation/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..04f5ed37893766feec941614691a9177f14e4027 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/relation/__init__.py @@ -0,0 +1,13 @@ +""" +A module to implement finitary relations [1] as predicate. + +References +========== + +.. [1] https://en.wikipedia.org/wiki/Finitary_relation + +""" + +__all__ = ['BinaryRelation', 'AppliedBinaryRelation'] + +from .binrel import BinaryRelation, AppliedBinaryRelation diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/relation/binrel.py b/venv/lib/python3.11/site-packages/sympy/assumptions/relation/binrel.py new file mode 100644 index 0000000000000000000000000000000000000000..4b4eba05bcce40f1a05483a30136b6ccd891c42f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/relation/binrel.py @@ -0,0 +1,212 @@ +""" +General binary relations. +""" +from typing import Optional + +from sympy.core.singleton import S +from sympy.assumptions import AppliedPredicate, ask, Predicate, Q # type: ignore +from sympy.core.kind import BooleanKind +from sympy.core.relational import Eq, Ne, Gt, Lt, Ge, Le +from sympy.logic.boolalg import conjuncts, Not + +__all__ = ["BinaryRelation", "AppliedBinaryRelation"] + + +class BinaryRelation(Predicate): + """ + Base class for all binary relational predicates. + + Explanation + =========== + + Binary relation takes two arguments and returns ``AppliedBinaryRelation`` + instance. To evaluate it to boolean value, use :obj:`~.ask()` or + :obj:`~.refine()` function. + + You can add support for new types by registering the handler to dispatcher. + See :obj:`~.Predicate()` for more information about predicate dispatching. + + Examples + ======== + + Applying and evaluating to boolean value: + + >>> from sympy import Q, ask, sin, cos + >>> from sympy.abc import x + >>> Q.eq(sin(x)**2+cos(x)**2, 1) + Q.eq(sin(x)**2 + cos(x)**2, 1) + >>> ask(_) + True + + You can define a new binary relation by subclassing and dispatching. + Here, we define a relation $R$ such that $x R y$ returns true if + $x = y + 1$. + + >>> from sympy import ask, Number, Q + >>> from sympy.assumptions import BinaryRelation + >>> class MyRel(BinaryRelation): + ... name = "R" + ... is_reflexive = False + >>> Q.R = MyRel() + >>> @Q.R.register(Number, Number) + ... def _(n1, n2, assumptions): + ... return ask(Q.zero(n1 - n2 - 1), assumptions) + >>> Q.R(2, 1) + Q.R(2, 1) + + Now, we can use ``ask()`` to evaluate it to boolean value. + + >>> ask(Q.R(2, 1)) + True + >>> ask(Q.R(1, 2)) + False + + ``Q.R`` returns ``False`` with minimum cost if two arguments have same + structure because it is antireflexive relation [1] by + ``is_reflexive = False``. + + >>> ask(Q.R(x, x)) + False + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Reflexive_relation + """ + + is_reflexive: Optional[bool] = None + is_symmetric: Optional[bool] = None + + def __call__(self, *args): + if not len(args) == 2: + raise ValueError("Binary relation takes two arguments, but got %s." % len(args)) + return AppliedBinaryRelation(self, *args) + + @property + def reversed(self): + if self.is_symmetric: + return self + return None + + @property + def negated(self): + return None + + def _compare_reflexive(self, lhs, rhs): + # quick exit for structurally same arguments + # do not check != here because it cannot catch the + # equivalent arguments with different structures. + + # reflexivity does not hold to NaN + if lhs is S.NaN or rhs is S.NaN: + return None + + reflexive = self.is_reflexive + if reflexive is None: + pass + elif reflexive and (lhs == rhs): + return True + elif not reflexive and (lhs == rhs): + return False + return None + + def eval(self, args, assumptions=True): + # quick exit for structurally same arguments + ret = self._compare_reflexive(*args) + if ret is not None: + return ret + + # don't perform simplify on args here. (done by AppliedBinaryRelation._eval_ask) + # evaluate by multipledispatch + lhs, rhs = args + ret = self.handler(lhs, rhs, assumptions=assumptions) + if ret is not None: + return ret + + # check reversed order if the relation is reflexive + if self.is_reflexive: + types = (type(lhs), type(rhs)) + if self.handler.dispatch(*types) is not self.handler.dispatch(*reversed(types)): + ret = self.handler(rhs, lhs, assumptions=assumptions) + + return ret + + +class AppliedBinaryRelation(AppliedPredicate): + """ + The class of expressions resulting from applying ``BinaryRelation`` + to the arguments. + + """ + + @property + def lhs(self): + """The left-hand side of the relation.""" + return self.arguments[0] + + @property + def rhs(self): + """The right-hand side of the relation.""" + return self.arguments[1] + + @property + def reversed(self): + """ + Try to return the relationship with sides reversed. + """ + revfunc = self.function.reversed + if revfunc is None: + return self + return revfunc(self.rhs, self.lhs) + + @property + def reversedsign(self): + """ + Try to return the relationship with signs reversed. + """ + revfunc = self.function.reversed + if revfunc is None: + return self + if not any(side.kind is BooleanKind for side in self.arguments): + return revfunc(-self.lhs, -self.rhs) + return self + + @property + def negated(self): + neg_rel = self.function.negated + if neg_rel is None: + return Not(self, evaluate=False) + return neg_rel(*self.arguments) + + def _eval_ask(self, assumptions): + conj_assumps = set() + binrelpreds = {Eq: Q.eq, Ne: Q.ne, Gt: Q.gt, Lt: Q.lt, Ge: Q.ge, Le: Q.le} + for a in conjuncts(assumptions): + if a.func in binrelpreds: + conj_assumps.add(binrelpreds[type(a)](*a.args)) + else: + conj_assumps.add(a) + + # After CNF in assumptions module is modified to take polyadic + # predicate, this will be removed + if any(rel in conj_assumps for rel in (self, self.reversed)): + return True + neg_rels = (self.negated, self.reversed.negated, Not(self, evaluate=False), + Not(self.reversed, evaluate=False)) + if any(rel in conj_assumps for rel in neg_rels): + return False + + # evaluation using multipledispatching + ret = self.function.eval(self.arguments, assumptions) + if ret is not None: + return ret + + # simplify the args and try again + args = tuple(a.simplify() for a in self.arguments) + return self.function.eval(args, assumptions) + + def __bool__(self): + ret = ask(self) + if ret is None: + raise TypeError("Cannot determine truth value of %s" % self) + return ret diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/relation/equality.py b/venv/lib/python3.11/site-packages/sympy/assumptions/relation/equality.py new file mode 100644 index 0000000000000000000000000000000000000000..d467cea2da706de2cbbc9875f93c7f8e324a9088 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/relation/equality.py @@ -0,0 +1,302 @@ +""" +Module for mathematical equality [1] and inequalities [2]. + +The purpose of this module is to provide the instances which represent the +binary predicates in order to combine the relationals into logical inference +system. Objects such as ``Q.eq``, ``Q.lt`` should remain internal to +assumptions module, and user must use the classes such as :obj:`~.Eq()`, +:obj:`~.Lt()` instead to construct the relational expressions. + +References +========== + +.. [1] https://en.wikipedia.org/wiki/Equality_(mathematics) +.. [2] https://en.wikipedia.org/wiki/Inequality_(mathematics) +""" +from sympy.assumptions import Q +from sympy.core.relational import is_eq, is_neq, is_gt, is_ge, is_lt, is_le + +from .binrel import BinaryRelation + +__all__ = ['EqualityPredicate', 'UnequalityPredicate', 'StrictGreaterThanPredicate', + 'GreaterThanPredicate', 'StrictLessThanPredicate', 'LessThanPredicate'] + + +class EqualityPredicate(BinaryRelation): + """ + Binary predicate for $=$. + + The purpose of this class is to provide the instance which represent + the equality predicate in order to allow the logical inference. + This class must remain internal to assumptions module and user must + use :obj:`~.Eq()` instead to construct the equality expression. + + Evaluating this predicate to ``True`` or ``False`` is done by + :func:`~.core.relational.is_eq` + + Examples + ======== + + >>> from sympy import ask, Q + >>> Q.eq(0, 0) + Q.eq(0, 0) + >>> ask(_) + True + + See Also + ======== + + sympy.core.relational.Eq + + """ + is_reflexive = True + is_symmetric = True + + name = 'eq' + handler = None # Do not allow dispatching by this predicate + + @property + def negated(self): + return Q.ne + + def eval(self, args, assumptions=True): + if assumptions == True: + # default assumptions for is_eq is None + assumptions = None + return is_eq(*args, assumptions) + + +class UnequalityPredicate(BinaryRelation): + r""" + Binary predicate for $\neq$. + + The purpose of this class is to provide the instance which represent + the inequation predicate in order to allow the logical inference. + This class must remain internal to assumptions module and user must + use :obj:`~.Ne()` instead to construct the inequation expression. + + Evaluating this predicate to ``True`` or ``False`` is done by + :func:`~.core.relational.is_neq` + + Examples + ======== + + >>> from sympy import ask, Q + >>> Q.ne(0, 0) + Q.ne(0, 0) + >>> ask(_) + False + + See Also + ======== + + sympy.core.relational.Ne + + """ + is_reflexive = False + is_symmetric = True + + name = 'ne' + handler = None + + @property + def negated(self): + return Q.eq + + def eval(self, args, assumptions=True): + if assumptions == True: + # default assumptions for is_neq is None + assumptions = None + return is_neq(*args, assumptions) + + +class StrictGreaterThanPredicate(BinaryRelation): + """ + Binary predicate for $>$. + + The purpose of this class is to provide the instance which represent + the ">" predicate in order to allow the logical inference. + This class must remain internal to assumptions module and user must + use :obj:`~.Gt()` instead to construct the equality expression. + + Evaluating this predicate to ``True`` or ``False`` is done by + :func:`~.core.relational.is_gt` + + Examples + ======== + + >>> from sympy import ask, Q + >>> Q.gt(0, 0) + Q.gt(0, 0) + >>> ask(_) + False + + See Also + ======== + + sympy.core.relational.Gt + + """ + is_reflexive = False + is_symmetric = False + + name = 'gt' + handler = None + + @property + def reversed(self): + return Q.lt + + @property + def negated(self): + return Q.le + + def eval(self, args, assumptions=True): + if assumptions == True: + # default assumptions for is_gt is None + assumptions = None + return is_gt(*args, assumptions) + + +class GreaterThanPredicate(BinaryRelation): + """ + Binary predicate for $>=$. + + The purpose of this class is to provide the instance which represent + the ">=" predicate in order to allow the logical inference. + This class must remain internal to assumptions module and user must + use :obj:`~.Ge()` instead to construct the equality expression. + + Evaluating this predicate to ``True`` or ``False`` is done by + :func:`~.core.relational.is_ge` + + Examples + ======== + + >>> from sympy import ask, Q + >>> Q.ge(0, 0) + Q.ge(0, 0) + >>> ask(_) + True + + See Also + ======== + + sympy.core.relational.Ge + + """ + is_reflexive = True + is_symmetric = False + + name = 'ge' + handler = None + + @property + def reversed(self): + return Q.le + + @property + def negated(self): + return Q.lt + + def eval(self, args, assumptions=True): + if assumptions == True: + # default assumptions for is_ge is None + assumptions = None + return is_ge(*args, assumptions) + + +class StrictLessThanPredicate(BinaryRelation): + """ + Binary predicate for $<$. + + The purpose of this class is to provide the instance which represent + the "<" predicate in order to allow the logical inference. + This class must remain internal to assumptions module and user must + use :obj:`~.Lt()` instead to construct the equality expression. + + Evaluating this predicate to ``True`` or ``False`` is done by + :func:`~.core.relational.is_lt` + + Examples + ======== + + >>> from sympy import ask, Q + >>> Q.lt(0, 0) + Q.lt(0, 0) + >>> ask(_) + False + + See Also + ======== + + sympy.core.relational.Lt + + """ + is_reflexive = False + is_symmetric = False + + name = 'lt' + handler = None + + @property + def reversed(self): + return Q.gt + + @property + def negated(self): + return Q.ge + + def eval(self, args, assumptions=True): + if assumptions == True: + # default assumptions for is_lt is None + assumptions = None + return is_lt(*args, assumptions) + + +class LessThanPredicate(BinaryRelation): + """ + Binary predicate for $<=$. + + The purpose of this class is to provide the instance which represent + the "<=" predicate in order to allow the logical inference. + This class must remain internal to assumptions module and user must + use :obj:`~.Le()` instead to construct the equality expression. + + Evaluating this predicate to ``True`` or ``False`` is done by + :func:`~.core.relational.is_le` + + Examples + ======== + + >>> from sympy import ask, Q + >>> Q.le(0, 0) + Q.le(0, 0) + >>> ask(_) + True + + See Also + ======== + + sympy.core.relational.Le + + """ + is_reflexive = True + is_symmetric = False + + name = 'le' + handler = None + + @property + def reversed(self): + return Q.ge + + @property + def negated(self): + return Q.gt + + def eval(self, args, assumptions=True): + if assumptions == True: + # default assumptions for is_le is None + assumptions = None + return is_le(*args, assumptions) diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/satask.py b/venv/lib/python3.11/site-packages/sympy/assumptions/satask.py new file mode 100644 index 0000000000000000000000000000000000000000..ffc13f6d3bc3fb7f573c8d5d0564b780440c1a8c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/satask.py @@ -0,0 +1,369 @@ +""" +Module to evaluate the proposition with assumptions using SAT algorithm. +""" + +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.core.kind import NumberKind, UndefinedKind +from sympy.assumptions.ask_generated import get_all_known_matrix_facts, get_all_known_number_facts +from sympy.assumptions.assume import global_assumptions, AppliedPredicate +from sympy.assumptions.sathandlers import class_fact_registry +from sympy.core import oo +from sympy.logic.inference import satisfiable +from sympy.assumptions.cnf import CNF, EncodedCNF +from sympy.matrices.kind import MatrixKind + + +def satask(proposition, assumptions=True, context=global_assumptions, + use_known_facts=True, iterations=oo): + """ + Function to evaluate the proposition with assumptions using SAT algorithm. + + This function extracts every fact relevant to the expressions composing + proposition and assumptions. For example, if a predicate containing + ``Abs(x)`` is proposed, then ``Q.zero(Abs(x)) | Q.positive(Abs(x))`` + will be found and passed to SAT solver because ``Q.nonnegative`` is + registered as a fact for ``Abs``. + + Proposition is evaluated to ``True`` or ``False`` if the truth value can be + determined. If not, ``None`` is returned. + + Parameters + ========== + + proposition : Any boolean expression. + Proposition which will be evaluated to boolean value. + + assumptions : Any boolean expression, optional. + Local assumptions to evaluate the *proposition*. + + context : AssumptionsContext, optional. + Default assumptions to evaluate the *proposition*. By default, + this is ``sympy.assumptions.global_assumptions`` variable. + + use_known_facts : bool, optional. + If ``True``, facts from ``sympy.assumptions.ask_generated`` + module are passed to SAT solver as well. + + iterations : int, optional. + Number of times that relevant facts are recursively extracted. + Default is infinite times until no new fact is found. + + Returns + ======= + + ``True``, ``False``, or ``None`` + + Examples + ======== + + >>> from sympy import Abs, Q + >>> from sympy.assumptions.satask import satask + >>> from sympy.abc import x + >>> satask(Q.zero(Abs(x)), Q.zero(x)) + True + + """ + props = CNF.from_prop(proposition) + _props = CNF.from_prop(~proposition) + + assumptions = CNF.from_prop(assumptions) + + context_cnf = CNF() + if context: + context_cnf = context_cnf.extend(context) + + sat = get_all_relevant_facts(props, assumptions, context_cnf, + use_known_facts=use_known_facts, iterations=iterations) + sat.add_from_cnf(assumptions) + if context: + sat.add_from_cnf(context_cnf) + + return check_satisfiability(props, _props, sat) + + +def check_satisfiability(prop, _prop, factbase): + sat_true = factbase.copy() + sat_false = factbase.copy() + sat_true.add_from_cnf(prop) + sat_false.add_from_cnf(_prop) + can_be_true = satisfiable(sat_true) + can_be_false = satisfiable(sat_false) + + if can_be_true and can_be_false: + return None + + if can_be_true and not can_be_false: + return True + + if not can_be_true and can_be_false: + return False + + if not can_be_true and not can_be_false: + # TODO: Run additional checks to see which combination of the + # assumptions, global_assumptions, and relevant_facts are + # inconsistent. + raise ValueError("Inconsistent assumptions") + + +def extract_predargs(proposition, assumptions=None, context=None): + """ + Extract every expression in the argument of predicates from *proposition*, + *assumptions* and *context*. + + Parameters + ========== + + proposition : sympy.assumptions.cnf.CNF + + assumptions : sympy.assumptions.cnf.CNF, optional. + + context : sympy.assumptions.cnf.CNF, optional. + CNF generated from assumptions context. + + Examples + ======== + + >>> from sympy import Q, Abs + >>> from sympy.assumptions.cnf import CNF + >>> from sympy.assumptions.satask import extract_predargs + >>> from sympy.abc import x, y + >>> props = CNF.from_prop(Q.zero(Abs(x*y))) + >>> assump = CNF.from_prop(Q.zero(x) & Q.zero(y)) + >>> extract_predargs(props, assump) + {x, y, Abs(x*y)} + + """ + req_keys = find_symbols(proposition) + keys = proposition.all_predicates() + # XXX: We need this since True/False are not Basic + lkeys = set() + if assumptions: + lkeys |= assumptions.all_predicates() + if context: + lkeys |= context.all_predicates() + + lkeys = lkeys - {S.true, S.false} + tmp_keys = None + while tmp_keys != set(): + tmp = set() + for l in lkeys: + syms = find_symbols(l) + if (syms & req_keys) != set(): + tmp |= syms + tmp_keys = tmp - req_keys + req_keys |= tmp_keys + keys |= {l for l in lkeys if find_symbols(l) & req_keys != set()} + + exprs = set() + for key in keys: + if isinstance(key, AppliedPredicate): + exprs |= set(key.arguments) + else: + exprs.add(key) + return exprs + +def find_symbols(pred): + """ + Find every :obj:`~.Symbol` in *pred*. + + Parameters + ========== + + pred : sympy.assumptions.cnf.CNF, or any Expr. + + """ + if isinstance(pred, CNF): + symbols = set() + for a in pred.all_predicates(): + symbols |= find_symbols(a) + return symbols + return pred.atoms(Symbol) + + +def get_relevant_clsfacts(exprs, relevant_facts=None): + """ + Extract relevant facts from the items in *exprs*. Facts are defined in + ``assumptions.sathandlers`` module. + + This function is recursively called by ``get_all_relevant_facts()``. + + Parameters + ========== + + exprs : set + Expressions whose relevant facts are searched. + + relevant_facts : sympy.assumptions.cnf.CNF, optional. + Pre-discovered relevant facts. + + Returns + ======= + + exprs : set + Candidates for next relevant fact searching. + + relevant_facts : sympy.assumptions.cnf.CNF + Updated relevant facts. + + Examples + ======== + + Here, we will see how facts relevant to ``Abs(x*y)`` are recursively + extracted. On the first run, set containing the expression is passed + without pre-discovered relevant facts. The result is a set containing + candidates for next run, and ``CNF()`` instance containing facts + which are relevant to ``Abs`` and its argument. + + >>> from sympy import Abs + >>> from sympy.assumptions.satask import get_relevant_clsfacts + >>> from sympy.abc import x, y + >>> exprs = {Abs(x*y)} + >>> exprs, facts = get_relevant_clsfacts(exprs) + >>> exprs + {x*y} + >>> facts.clauses #doctest: +SKIP + {frozenset({Literal(Q.odd(Abs(x*y)), False), Literal(Q.odd(x*y), True)}), + frozenset({Literal(Q.zero(Abs(x*y)), False), Literal(Q.zero(x*y), True)}), + frozenset({Literal(Q.even(Abs(x*y)), False), Literal(Q.even(x*y), True)}), + frozenset({Literal(Q.zero(Abs(x*y)), True), Literal(Q.zero(x*y), False)}), + frozenset({Literal(Q.even(Abs(x*y)), False), + Literal(Q.odd(Abs(x*y)), False), + Literal(Q.odd(x*y), True)}), + frozenset({Literal(Q.even(Abs(x*y)), False), + Literal(Q.even(x*y), True), + Literal(Q.odd(Abs(x*y)), False)}), + frozenset({Literal(Q.positive(Abs(x*y)), False), + Literal(Q.zero(Abs(x*y)), False)})} + + We pass the first run's results to the second run, and get the expressions + for next run and updated facts. + + >>> exprs, facts = get_relevant_clsfacts(exprs, relevant_facts=facts) + >>> exprs + {x, y} + + On final run, no more candidate is returned thus we know that all + relevant facts are successfully retrieved. + + >>> exprs, facts = get_relevant_clsfacts(exprs, relevant_facts=facts) + >>> exprs + set() + + """ + if not relevant_facts: + relevant_facts = CNF() + + newexprs = set() + for expr in exprs: + for fact in class_fact_registry(expr): + newfact = CNF.to_CNF(fact) + relevant_facts = relevant_facts._and(newfact) + for key in newfact.all_predicates(): + if isinstance(key, AppliedPredicate): + newexprs |= set(key.arguments) + + return newexprs - exprs, relevant_facts + + +def get_all_relevant_facts(proposition, assumptions, context, + use_known_facts=True, iterations=oo): + """ + Extract all relevant facts from *proposition* and *assumptions*. + + This function extracts the facts by recursively calling + ``get_relevant_clsfacts()``. Extracted facts are converted to + ``EncodedCNF`` and returned. + + Parameters + ========== + + proposition : sympy.assumptions.cnf.CNF + CNF generated from proposition expression. + + assumptions : sympy.assumptions.cnf.CNF + CNF generated from assumption expression. + + context : sympy.assumptions.cnf.CNF + CNF generated from assumptions context. + + use_known_facts : bool, optional. + If ``True``, facts from ``sympy.assumptions.ask_generated`` + module are encoded as well. + + iterations : int, optional. + Number of times that relevant facts are recursively extracted. + Default is infinite times until no new fact is found. + + Returns + ======= + + sympy.assumptions.cnf.EncodedCNF + + Examples + ======== + + >>> from sympy import Q + >>> from sympy.assumptions.cnf import CNF + >>> from sympy.assumptions.satask import get_all_relevant_facts + >>> from sympy.abc import x, y + >>> props = CNF.from_prop(Q.nonzero(x*y)) + >>> assump = CNF.from_prop(Q.nonzero(x)) + >>> context = CNF.from_prop(Q.nonzero(y)) + >>> get_all_relevant_facts(props, assump, context) #doctest: +SKIP + + + """ + # The relevant facts might introduce new keys, e.g., Q.zero(x*y) will + # introduce the keys Q.zero(x) and Q.zero(y), so we need to run it until + # we stop getting new things. Hopefully this strategy won't lead to an + # infinite loop in the future. + i = 0 + relevant_facts = CNF() + all_exprs = set() + while True: + if i == 0: + exprs = extract_predargs(proposition, assumptions, context) + all_exprs |= exprs + exprs, relevant_facts = get_relevant_clsfacts(exprs, relevant_facts) + i += 1 + if i >= iterations: + break + if not exprs: + break + + if use_known_facts: + known_facts_CNF = CNF() + + if any(expr.kind == MatrixKind(NumberKind) for expr in all_exprs): + known_facts_CNF.add_clauses(get_all_known_matrix_facts()) + # check for undefinedKind since kind system isn't fully implemented + if any(((expr.kind == NumberKind) or (expr.kind == UndefinedKind)) for expr in all_exprs): + known_facts_CNF.add_clauses(get_all_known_number_facts()) + + kf_encoded = EncodedCNF() + kf_encoded.from_cnf(known_facts_CNF) + + def translate_literal(lit, delta): + if lit > 0: + return lit + delta + else: + return lit - delta + + def translate_data(data, delta): + return [{translate_literal(i, delta) for i in clause} for clause in data] + data = [] + symbols = [] + n_lit = len(kf_encoded.symbols) + for i, expr in enumerate(all_exprs): + symbols += [pred(expr) for pred in kf_encoded.symbols] + data += translate_data(kf_encoded.data, i * n_lit) + + encoding = dict(list(zip(symbols, range(1, len(symbols)+1)))) + ctx = EncodedCNF(data, encoding) + else: + ctx = EncodedCNF() + + ctx.add_from_cnf(relevant_facts) + + return ctx diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/sathandlers.py b/venv/lib/python3.11/site-packages/sympy/assumptions/sathandlers.py new file mode 100644 index 0000000000000000000000000000000000000000..a11199eb0e547187ab280c18196c0259c178e004 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/sathandlers.py @@ -0,0 +1,322 @@ +from collections import defaultdict + +from sympy.assumptions.ask import Q +from sympy.core import (Add, Mul, Pow, Number, NumberSymbol, Symbol) +from sympy.core.numbers import ImaginaryUnit +from sympy.functions.elementary.complexes import Abs +from sympy.logic.boolalg import (Equivalent, And, Or, Implies) +from sympy.matrices.expressions import MatMul + +# APIs here may be subject to change + + +### Helper functions ### + +def allargs(symbol, fact, expr): + """ + Apply all arguments of the expression to the fact structure. + + Parameters + ========== + + symbol : Symbol + A placeholder symbol. + + fact : Boolean + Resulting ``Boolean`` expression. + + expr : Expr + + Examples + ======== + + >>> from sympy import Q + >>> from sympy.assumptions.sathandlers import allargs + >>> from sympy.abc import x, y + >>> allargs(x, Q.negative(x) | Q.positive(x), x*y) + (Q.negative(x) | Q.positive(x)) & (Q.negative(y) | Q.positive(y)) + + """ + return And(*[fact.subs(symbol, arg) for arg in expr.args]) + + +def anyarg(symbol, fact, expr): + """ + Apply any argument of the expression to the fact structure. + + Parameters + ========== + + symbol : Symbol + A placeholder symbol. + + fact : Boolean + Resulting ``Boolean`` expression. + + expr : Expr + + Examples + ======== + + >>> from sympy import Q + >>> from sympy.assumptions.sathandlers import anyarg + >>> from sympy.abc import x, y + >>> anyarg(x, Q.negative(x) & Q.positive(x), x*y) + (Q.negative(x) & Q.positive(x)) | (Q.negative(y) & Q.positive(y)) + + """ + return Or(*[fact.subs(symbol, arg) for arg in expr.args]) + + +def exactlyonearg(symbol, fact, expr): + """ + Apply exactly one argument of the expression to the fact structure. + + Parameters + ========== + + symbol : Symbol + A placeholder symbol. + + fact : Boolean + Resulting ``Boolean`` expression. + + expr : Expr + + Examples + ======== + + >>> from sympy import Q + >>> from sympy.assumptions.sathandlers import exactlyonearg + >>> from sympy.abc import x, y + >>> exactlyonearg(x, Q.positive(x), x*y) + (Q.positive(x) & ~Q.positive(y)) | (Q.positive(y) & ~Q.positive(x)) + + """ + pred_args = [fact.subs(symbol, arg) for arg in expr.args] + res = Or(*[And(pred_args[i], *[~lit for lit in pred_args[:i] + + pred_args[i+1:]]) for i in range(len(pred_args))]) + return res + + +### Fact registry ### + +class ClassFactRegistry: + """ + Register handlers against classes. + + Explanation + =========== + + ``register`` method registers the handler function for a class. Here, + handler function should return a single fact. ``multiregister`` method + registers the handler function for multiple classes. Here, handler function + should return a container of multiple facts. + + ``registry(expr)`` returns a set of facts for *expr*. + + Examples + ======== + + Here, we register the facts for ``Abs``. + + >>> from sympy import Abs, Equivalent, Q + >>> from sympy.assumptions.sathandlers import ClassFactRegistry + >>> reg = ClassFactRegistry() + >>> @reg.register(Abs) + ... def f1(expr): + ... return Q.nonnegative(expr) + >>> @reg.register(Abs) + ... def f2(expr): + ... arg = expr.args[0] + ... return Equivalent(~Q.zero(arg), ~Q.zero(expr)) + + Calling the registry with expression returns the defined facts for the + expression. + + >>> from sympy.abc import x + >>> reg(Abs(x)) + {Q.nonnegative(Abs(x)), Equivalent(~Q.zero(x), ~Q.zero(Abs(x)))} + + Multiple facts can be registered at once by ``multiregister`` method. + + >>> reg2 = ClassFactRegistry() + >>> @reg2.multiregister(Abs) + ... def _(expr): + ... arg = expr.args[0] + ... return [Q.even(arg) >> Q.even(expr), Q.odd(arg) >> Q.odd(expr)] + >>> reg2(Abs(x)) + {Implies(Q.even(x), Q.even(Abs(x))), Implies(Q.odd(x), Q.odd(Abs(x)))} + + """ + def __init__(self): + self.singlefacts = defaultdict(frozenset) + self.multifacts = defaultdict(frozenset) + + def register(self, cls): + def _(func): + self.singlefacts[cls] |= {func} + return func + return _ + + def multiregister(self, *classes): + def _(func): + for cls in classes: + self.multifacts[cls] |= {func} + return func + return _ + + def __getitem__(self, key): + ret1 = self.singlefacts[key] + for k in self.singlefacts: + if issubclass(key, k): + ret1 |= self.singlefacts[k] + + ret2 = self.multifacts[key] + for k in self.multifacts: + if issubclass(key, k): + ret2 |= self.multifacts[k] + + return ret1, ret2 + + def __call__(self, expr): + ret = set() + + handlers1, handlers2 = self[type(expr)] + + ret.update(h(expr) for h in handlers1) + for h in handlers2: + ret.update(h(expr)) + return ret + +class_fact_registry = ClassFactRegistry() + + + +### Class fact registration ### + +x = Symbol('x') + +## Abs ## + +@class_fact_registry.multiregister(Abs) +def _(expr): + arg = expr.args[0] + return [Q.nonnegative(expr), + Equivalent(~Q.zero(arg), ~Q.zero(expr)), + Q.even(arg) >> Q.even(expr), + Q.odd(arg) >> Q.odd(expr), + Q.integer(arg) >> Q.integer(expr), + ] + + +### Add ## + +@class_fact_registry.multiregister(Add) +def _(expr): + return [allargs(x, Q.positive(x), expr) >> Q.positive(expr), + allargs(x, Q.negative(x), expr) >> Q.negative(expr), + allargs(x, Q.real(x), expr) >> Q.real(expr), + allargs(x, Q.rational(x), expr) >> Q.rational(expr), + allargs(x, Q.integer(x), expr) >> Q.integer(expr), + exactlyonearg(x, ~Q.integer(x), expr) >> ~Q.integer(expr), + ] + +@class_fact_registry.register(Add) +def _(expr): + allargs_real = allargs(x, Q.real(x), expr) + onearg_irrational = exactlyonearg(x, Q.irrational(x), expr) + return Implies(allargs_real, Implies(onearg_irrational, Q.irrational(expr))) + + +### Mul ### + +@class_fact_registry.multiregister(Mul) +def _(expr): + return [Equivalent(Q.zero(expr), anyarg(x, Q.zero(x), expr)), + allargs(x, Q.positive(x), expr) >> Q.positive(expr), + allargs(x, Q.real(x), expr) >> Q.real(expr), + allargs(x, Q.rational(x), expr) >> Q.rational(expr), + allargs(x, Q.integer(x), expr) >> Q.integer(expr), + exactlyonearg(x, ~Q.rational(x), expr) >> ~Q.integer(expr), + allargs(x, Q.commutative(x), expr) >> Q.commutative(expr), + ] + +@class_fact_registry.register(Mul) +def _(expr): + # Implicitly assumes Mul has more than one arg + # Would be allargs(x, Q.prime(x) | Q.composite(x)) except 1 is composite + # More advanced prime assumptions will require inequalities, as 1 provides + # a corner case. + allargs_prime = allargs(x, Q.prime(x), expr) + return Implies(allargs_prime, ~Q.prime(expr)) + +@class_fact_registry.register(Mul) +def _(expr): + # General Case: Odd number of imaginary args implies mul is imaginary(To be implemented) + allargs_imag_or_real = allargs(x, Q.imaginary(x) | Q.real(x), expr) + onearg_imaginary = exactlyonearg(x, Q.imaginary(x), expr) + return Implies(allargs_imag_or_real, Implies(onearg_imaginary, Q.imaginary(expr))) + +@class_fact_registry.register(Mul) +def _(expr): + allargs_real = allargs(x, Q.real(x), expr) + onearg_irrational = exactlyonearg(x, Q.irrational(x), expr) + return Implies(allargs_real, Implies(onearg_irrational, Q.irrational(expr))) + +@class_fact_registry.register(Mul) +def _(expr): + # Including the integer qualification means we don't need to add any facts + # for odd, since the assumptions already know that every integer is + # exactly one of even or odd. + allargs_integer = allargs(x, Q.integer(x), expr) + anyarg_even = anyarg(x, Q.even(x), expr) + return Implies(allargs_integer, Equivalent(anyarg_even, Q.even(expr))) + + +### MatMul ### + +@class_fact_registry.register(MatMul) +def _(expr): + allargs_square = allargs(x, Q.square(x), expr) + allargs_invertible = allargs(x, Q.invertible(x), expr) + return Implies(allargs_square, Equivalent(Q.invertible(expr), allargs_invertible)) + + +### Pow ### + +@class_fact_registry.multiregister(Pow) +def _(expr): + base, exp = expr.base, expr.exp + return [ + (Q.real(base) & Q.even(exp) & Q.nonnegative(exp)) >> Q.nonnegative(expr), + (Q.nonnegative(base) & Q.odd(exp) & Q.nonnegative(exp)) >> Q.nonnegative(expr), + (Q.nonpositive(base) & Q.odd(exp) & Q.nonnegative(exp)) >> Q.nonpositive(expr), + Equivalent(Q.zero(expr), Q.zero(base) & Q.positive(exp)) + ] + + +### Numbers ### + +_old_assump_getters = { + Q.positive: lambda o: o.is_positive, + Q.zero: lambda o: o.is_zero, + Q.negative: lambda o: o.is_negative, + Q.rational: lambda o: o.is_rational, + Q.irrational: lambda o: o.is_irrational, + Q.even: lambda o: o.is_even, + Q.odd: lambda o: o.is_odd, + Q.imaginary: lambda o: o.is_imaginary, + Q.prime: lambda o: o.is_prime, + Q.composite: lambda o: o.is_composite, +} + +@class_fact_registry.multiregister(Number, NumberSymbol, ImaginaryUnit) +def _(expr): + ret = [] + for p, getter in _old_assump_getters.items(): + pred = p(expr) + prop = getter(expr) + if prop is not None: + ret.append(Equivalent(pred, prop)) + return ret diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_assumptions_2.py b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_assumptions_2.py new file mode 100644 index 0000000000000000000000000000000000000000..493fe4a7ed70301754ad2cfe181c5acf30433768 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_assumptions_2.py @@ -0,0 +1,35 @@ +""" +rename this to test_assumptions.py when the old assumptions system is deleted +""" +from sympy.abc import x, y +from sympy.assumptions.assume import global_assumptions +from sympy.assumptions.ask import Q +from sympy.printing import pretty + + +def test_equal(): + """Test for equality""" + assert Q.positive(x) == Q.positive(x) + assert Q.positive(x) != ~Q.positive(x) + assert ~Q.positive(x) == ~Q.positive(x) + + +def test_pretty(): + assert pretty(Q.positive(x)) == "Q.positive(x)" + assert pretty( + {Q.positive, Q.integer}) == "{Q.integer, Q.positive}" + + +def test_global(): + """Test for global assumptions""" + global_assumptions.add(x > 0) + assert (x > 0) in global_assumptions + global_assumptions.remove(x > 0) + assert not (x > 0) in global_assumptions + # same with multiple of assumptions + global_assumptions.add(x > 0, y > 0) + assert (x > 0) in global_assumptions + assert (y > 0) in global_assumptions + global_assumptions.clear() + assert not (x > 0) in global_assumptions + assert not (y > 0) in global_assumptions diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_context.py b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_context.py new file mode 100644 index 0000000000000000000000000000000000000000..be162f1c69492218ff90ea69492925d7779567a4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_context.py @@ -0,0 +1,39 @@ +from sympy.assumptions import ask, Q +from sympy.assumptions.assume import assuming, global_assumptions +from sympy.abc import x, y + +def test_assuming(): + with assuming(Q.integer(x)): + assert ask(Q.integer(x)) + assert not ask(Q.integer(x)) + +def test_assuming_nested(): + assert not ask(Q.integer(x)) + assert not ask(Q.integer(y)) + with assuming(Q.integer(x)): + assert ask(Q.integer(x)) + assert not ask(Q.integer(y)) + with assuming(Q.integer(y)): + assert ask(Q.integer(x)) + assert ask(Q.integer(y)) + assert ask(Q.integer(x)) + assert not ask(Q.integer(y)) + assert not ask(Q.integer(x)) + assert not ask(Q.integer(y)) + +def test_finally(): + try: + with assuming(Q.integer(x)): + 1/0 + except ZeroDivisionError: + pass + assert not ask(Q.integer(x)) + +def test_remove_safe(): + global_assumptions.add(Q.integer(x)) + with assuming(): + assert ask(Q.integer(x)) + global_assumptions.remove(Q.integer(x)) + assert not ask(Q.integer(x)) + assert ask(Q.integer(x)) + global_assumptions.clear() # for the benefit of other tests diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_matrices.py b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_matrices.py new file mode 100644 index 0000000000000000000000000000000000000000..8bfa990f080eebe4d6dd5bfdd733ce1a19adf329 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_matrices.py @@ -0,0 +1,283 @@ +from sympy.assumptions.ask import (Q, ask) +from sympy.core.symbol import Symbol +from sympy.matrices.expressions.diagonal import (DiagMatrix, DiagonalMatrix) +from sympy.matrices.dense import Matrix +from sympy.matrices.expressions import (MatrixSymbol, Identity, ZeroMatrix, + OneMatrix, Trace, MatrixSlice, Determinant, BlockMatrix, BlockDiagMatrix) +from sympy.matrices.expressions.factorizations import LofLU +from sympy.testing.pytest import XFAIL + +X = MatrixSymbol('X', 2, 2) +Y = MatrixSymbol('Y', 2, 3) +Z = MatrixSymbol('Z', 2, 2) +A1x1 = MatrixSymbol('A1x1', 1, 1) +B1x1 = MatrixSymbol('B1x1', 1, 1) +C0x0 = MatrixSymbol('C0x0', 0, 0) +V1 = MatrixSymbol('V1', 2, 1) +V2 = MatrixSymbol('V2', 2, 1) + +def test_square(): + assert ask(Q.square(X)) + assert not ask(Q.square(Y)) + assert ask(Q.square(Y*Y.T)) + +def test_invertible(): + assert ask(Q.invertible(X), Q.invertible(X)) + assert ask(Q.invertible(Y)) is False + assert ask(Q.invertible(X*Y), Q.invertible(X)) is False + assert ask(Q.invertible(X*Z), Q.invertible(X)) is None + assert ask(Q.invertible(X*Z), Q.invertible(X) & Q.invertible(Z)) is True + assert ask(Q.invertible(X.T)) is None + assert ask(Q.invertible(X.T), Q.invertible(X)) is True + assert ask(Q.invertible(X.I)) is True + assert ask(Q.invertible(Identity(3))) is True + assert ask(Q.invertible(ZeroMatrix(3, 3))) is False + assert ask(Q.invertible(OneMatrix(1, 1))) is True + assert ask(Q.invertible(OneMatrix(3, 3))) is False + assert ask(Q.invertible(X), Q.fullrank(X) & Q.square(X)) + +def test_singular(): + assert ask(Q.singular(X)) is None + assert ask(Q.singular(X), Q.invertible(X)) is False + assert ask(Q.singular(X), ~Q.invertible(X)) is True + +@XFAIL +def test_invertible_fullrank(): + assert ask(Q.invertible(X), Q.fullrank(X)) is True + + +def test_invertible_BlockMatrix(): + assert ask(Q.invertible(BlockMatrix([Identity(3)]))) == True + assert ask(Q.invertible(BlockMatrix([ZeroMatrix(3, 3)]))) == False + + X = Matrix([[1, 2, 3], [3, 5, 4]]) + Y = Matrix([[4, 2, 7], [2, 3, 5]]) + # non-invertible A block + assert ask(Q.invertible(BlockMatrix([ + [Matrix.ones(3, 3), Y.T], + [X, Matrix.eye(2)], + ]))) == True + # non-invertible B block + assert ask(Q.invertible(BlockMatrix([ + [Y.T, Matrix.ones(3, 3)], + [Matrix.eye(2), X], + ]))) == True + # non-invertible C block + assert ask(Q.invertible(BlockMatrix([ + [X, Matrix.eye(2)], + [Matrix.ones(3, 3), Y.T], + ]))) == True + # non-invertible D block + assert ask(Q.invertible(BlockMatrix([ + [Matrix.eye(2), X], + [Y.T, Matrix.ones(3, 3)], + ]))) == True + + +def test_invertible_BlockDiagMatrix(): + assert ask(Q.invertible(BlockDiagMatrix(Identity(3), Identity(5)))) == True + assert ask(Q.invertible(BlockDiagMatrix(ZeroMatrix(3, 3), Identity(5)))) == False + assert ask(Q.invertible(BlockDiagMatrix(Identity(3), OneMatrix(5, 5)))) == False + + +def test_symmetric(): + assert ask(Q.symmetric(X), Q.symmetric(X)) + assert ask(Q.symmetric(X*Z), Q.symmetric(X)) is None + assert ask(Q.symmetric(X*Z), Q.symmetric(X) & Q.symmetric(Z)) is True + assert ask(Q.symmetric(X + Z), Q.symmetric(X) & Q.symmetric(Z)) is True + assert ask(Q.symmetric(Y)) is False + assert ask(Q.symmetric(Y*Y.T)) is True + assert ask(Q.symmetric(Y.T*X*Y)) is None + assert ask(Q.symmetric(Y.T*X*Y), Q.symmetric(X)) is True + assert ask(Q.symmetric(X**10), Q.symmetric(X)) is True + assert ask(Q.symmetric(A1x1)) is True + assert ask(Q.symmetric(A1x1 + B1x1)) is True + assert ask(Q.symmetric(A1x1 * B1x1)) is True + assert ask(Q.symmetric(V1.T*V1)) is True + assert ask(Q.symmetric(V1.T*(V1 + V2))) is True + assert ask(Q.symmetric(V1.T*(V1 + V2) + A1x1)) is True + assert ask(Q.symmetric(MatrixSlice(Y, (0, 1), (1, 2)))) is True + assert ask(Q.symmetric(Identity(3))) is True + assert ask(Q.symmetric(ZeroMatrix(3, 3))) is True + assert ask(Q.symmetric(OneMatrix(3, 3))) is True + +def _test_orthogonal_unitary(predicate): + assert ask(predicate(X), predicate(X)) + assert ask(predicate(X.T), predicate(X)) is True + assert ask(predicate(X.I), predicate(X)) is True + assert ask(predicate(X**2), predicate(X)) + assert ask(predicate(Y)) is False + assert ask(predicate(X)) is None + assert ask(predicate(X), ~Q.invertible(X)) is False + assert ask(predicate(X*Z*X), predicate(X) & predicate(Z)) is True + assert ask(predicate(Identity(3))) is True + assert ask(predicate(ZeroMatrix(3, 3))) is False + assert ask(Q.invertible(X), predicate(X)) + assert not ask(predicate(X + Z), predicate(X) & predicate(Z)) + +def test_orthogonal(): + _test_orthogonal_unitary(Q.orthogonal) + +def test_unitary(): + _test_orthogonal_unitary(Q.unitary) + assert ask(Q.unitary(X), Q.orthogonal(X)) + +def test_fullrank(): + assert ask(Q.fullrank(X), Q.fullrank(X)) + assert ask(Q.fullrank(X**2), Q.fullrank(X)) + assert ask(Q.fullrank(X.T), Q.fullrank(X)) is True + assert ask(Q.fullrank(X)) is None + assert ask(Q.fullrank(Y)) is None + assert ask(Q.fullrank(X*Z), Q.fullrank(X) & Q.fullrank(Z)) is True + assert ask(Q.fullrank(Identity(3))) is True + assert ask(Q.fullrank(ZeroMatrix(3, 3))) is False + assert ask(Q.fullrank(OneMatrix(1, 1))) is True + assert ask(Q.fullrank(OneMatrix(3, 3))) is False + assert ask(Q.invertible(X), ~Q.fullrank(X)) == False + + +def test_positive_definite(): + assert ask(Q.positive_definite(X), Q.positive_definite(X)) + assert ask(Q.positive_definite(X.T), Q.positive_definite(X)) is True + assert ask(Q.positive_definite(X.I), Q.positive_definite(X)) is True + assert ask(Q.positive_definite(Y)) is False + assert ask(Q.positive_definite(X)) is None + assert ask(Q.positive_definite(X**3), Q.positive_definite(X)) + assert ask(Q.positive_definite(X*Z*X), + Q.positive_definite(X) & Q.positive_definite(Z)) is True + assert ask(Q.positive_definite(X), Q.orthogonal(X)) + assert ask(Q.positive_definite(Y.T*X*Y), + Q.positive_definite(X) & Q.fullrank(Y)) is True + assert not ask(Q.positive_definite(Y.T*X*Y), Q.positive_definite(X)) + assert ask(Q.positive_definite(Identity(3))) is True + assert ask(Q.positive_definite(ZeroMatrix(3, 3))) is False + assert ask(Q.positive_definite(OneMatrix(1, 1))) is True + assert ask(Q.positive_definite(OneMatrix(3, 3))) is False + assert ask(Q.positive_definite(X + Z), Q.positive_definite(X) & + Q.positive_definite(Z)) is True + assert not ask(Q.positive_definite(-X), Q.positive_definite(X)) + assert ask(Q.positive(X[1, 1]), Q.positive_definite(X)) + +def test_triangular(): + assert ask(Q.upper_triangular(X + Z.T + Identity(2)), Q.upper_triangular(X) & + Q.lower_triangular(Z)) is True + assert ask(Q.upper_triangular(X*Z.T), Q.upper_triangular(X) & + Q.lower_triangular(Z)) is True + assert ask(Q.lower_triangular(Identity(3))) is True + assert ask(Q.lower_triangular(ZeroMatrix(3, 3))) is True + assert ask(Q.upper_triangular(ZeroMatrix(3, 3))) is True + assert ask(Q.lower_triangular(OneMatrix(1, 1))) is True + assert ask(Q.upper_triangular(OneMatrix(1, 1))) is True + assert ask(Q.lower_triangular(OneMatrix(3, 3))) is False + assert ask(Q.upper_triangular(OneMatrix(3, 3))) is False + assert ask(Q.triangular(X), Q.unit_triangular(X)) + assert ask(Q.upper_triangular(X**3), Q.upper_triangular(X)) + assert ask(Q.lower_triangular(X**3), Q.lower_triangular(X)) + + +def test_diagonal(): + assert ask(Q.diagonal(X + Z.T + Identity(2)), Q.diagonal(X) & + Q.diagonal(Z)) is True + assert ask(Q.diagonal(ZeroMatrix(3, 3))) + assert ask(Q.diagonal(OneMatrix(1, 1))) is True + assert ask(Q.diagonal(OneMatrix(3, 3))) is False + assert ask(Q.lower_triangular(X) & Q.upper_triangular(X), Q.diagonal(X)) + assert ask(Q.diagonal(X), Q.lower_triangular(X) & Q.upper_triangular(X)) + assert ask(Q.symmetric(X), Q.diagonal(X)) + assert ask(Q.triangular(X), Q.diagonal(X)) + assert ask(Q.diagonal(C0x0)) + assert ask(Q.diagonal(A1x1)) + assert ask(Q.diagonal(A1x1 + B1x1)) + assert ask(Q.diagonal(A1x1*B1x1)) + assert ask(Q.diagonal(V1.T*V2)) + assert ask(Q.diagonal(V1.T*(X + Z)*V1)) + assert ask(Q.diagonal(MatrixSlice(Y, (0, 1), (1, 2)))) is True + assert ask(Q.diagonal(V1.T*(V1 + V2))) is True + assert ask(Q.diagonal(X**3), Q.diagonal(X)) + assert ask(Q.diagonal(Identity(3))) + assert ask(Q.diagonal(DiagMatrix(V1))) + assert ask(Q.diagonal(DiagonalMatrix(X))) + + +def test_non_atoms(): + assert ask(Q.real(Trace(X)), Q.positive(Trace(X))) + +@XFAIL +def test_non_trivial_implies(): + X = MatrixSymbol('X', 3, 3) + Y = MatrixSymbol('Y', 3, 3) + assert ask(Q.lower_triangular(X+Y), Q.lower_triangular(X) & + Q.lower_triangular(Y)) is True + assert ask(Q.triangular(X), Q.lower_triangular(X)) is True + assert ask(Q.triangular(X+Y), Q.lower_triangular(X) & + Q.lower_triangular(Y)) is True + +def test_MatrixSlice(): + X = MatrixSymbol('X', 4, 4) + B = MatrixSlice(X, (1, 3), (1, 3)) + C = MatrixSlice(X, (0, 3), (1, 3)) + assert ask(Q.symmetric(B), Q.symmetric(X)) + assert ask(Q.invertible(B), Q.invertible(X)) + assert ask(Q.diagonal(B), Q.diagonal(X)) + assert ask(Q.orthogonal(B), Q.orthogonal(X)) + assert ask(Q.upper_triangular(B), Q.upper_triangular(X)) + + assert not ask(Q.symmetric(C), Q.symmetric(X)) + assert not ask(Q.invertible(C), Q.invertible(X)) + assert not ask(Q.diagonal(C), Q.diagonal(X)) + assert not ask(Q.orthogonal(C), Q.orthogonal(X)) + assert not ask(Q.upper_triangular(C), Q.upper_triangular(X)) + +def test_det_trace_positive(): + X = MatrixSymbol('X', 4, 4) + assert ask(Q.positive(Trace(X)), Q.positive_definite(X)) + assert ask(Q.positive(Determinant(X)), Q.positive_definite(X)) + +def test_field_assumptions(): + X = MatrixSymbol('X', 4, 4) + Y = MatrixSymbol('Y', 4, 4) + assert ask(Q.real_elements(X), Q.real_elements(X)) + assert not ask(Q.integer_elements(X), Q.real_elements(X)) + assert ask(Q.complex_elements(X), Q.real_elements(X)) + assert ask(Q.complex_elements(X**2), Q.real_elements(X)) + assert ask(Q.real_elements(X**2), Q.integer_elements(X)) + assert ask(Q.real_elements(X+Y), Q.real_elements(X)) is None + assert ask(Q.real_elements(X+Y), Q.real_elements(X) & Q.real_elements(Y)) + from sympy.matrices.expressions.hadamard import HadamardProduct + assert ask(Q.real_elements(HadamardProduct(X, Y)), + Q.real_elements(X) & Q.real_elements(Y)) + assert ask(Q.complex_elements(X+Y), Q.real_elements(X) & Q.complex_elements(Y)) + + assert ask(Q.real_elements(X.T), Q.real_elements(X)) + assert ask(Q.real_elements(X.I), Q.real_elements(X) & Q.invertible(X)) + assert ask(Q.real_elements(Trace(X)), Q.real_elements(X)) + assert ask(Q.integer_elements(Determinant(X)), Q.integer_elements(X)) + assert not ask(Q.integer_elements(X.I), Q.integer_elements(X)) + alpha = Symbol('alpha') + assert ask(Q.real_elements(alpha*X), Q.real_elements(X) & Q.real(alpha)) + assert ask(Q.real_elements(LofLU(X)), Q.real_elements(X)) + e = Symbol('e', integer=True, negative=True) + assert ask(Q.real_elements(X**e), Q.real_elements(X) & Q.invertible(X)) + assert ask(Q.real_elements(X**e), Q.real_elements(X)) is None + +def test_matrix_element_sets(): + X = MatrixSymbol('X', 4, 4) + assert ask(Q.real(X[1, 2]), Q.real_elements(X)) + assert ask(Q.integer(X[1, 2]), Q.integer_elements(X)) + assert ask(Q.complex(X[1, 2]), Q.complex_elements(X)) + assert ask(Q.integer_elements(Identity(3))) + assert ask(Q.integer_elements(ZeroMatrix(3, 3))) + assert ask(Q.integer_elements(OneMatrix(3, 3))) + from sympy.matrices.expressions.fourier import DFT + assert ask(Q.complex_elements(DFT(3))) + + +def test_matrix_element_sets_slices_blocks(): + X = MatrixSymbol('X', 4, 4) + assert ask(Q.integer_elements(X[:, 3]), Q.integer_elements(X)) + assert ask(Q.integer_elements(BlockMatrix([[X], [X]])), + Q.integer_elements(X)) + +def test_matrix_element_sets_determinant_trace(): + assert ask(Q.integer(Determinant(X)), Q.integer_elements(X)) + assert ask(Q.integer(Trace(X)), Q.integer_elements(X)) diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_query.py b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_query.py new file mode 100644 index 0000000000000000000000000000000000000000..e1ae1f1e482e5c9d19e3dcce3f37ad46b6821817 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_query.py @@ -0,0 +1,2541 @@ +from sympy.abc import t, w, x, y, z, n, k, m, p, i +from sympy.assumptions import (ask, AssumptionsContext, Q, register_handler, + remove_handler) +from sympy.assumptions.assume import assuming, global_assumptions, Predicate +from sympy.assumptions.cnf import CNF, Literal +from sympy.assumptions.facts import (single_fact_lookup, + get_known_facts, generate_known_facts_dict, get_known_facts_keys) +from sympy.assumptions.handlers import AskHandler +from sympy.assumptions.ask_generated import (get_all_known_facts, + get_known_facts_dict) +from sympy.core.add import Add +from sympy.core.numbers import (I, Integer, Rational, oo, zoo, pi) +from sympy.core.singleton import S +from sympy.core.power import Pow +from sympy.core.symbol import Str, symbols, Symbol +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.complexes import (Abs, im, re, sign) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import ( + acos, acot, asin, atan, cos, cot, sin, tan) +from sympy.logic.boolalg import Equivalent, Implies, Xor, And, to_cnf +from sympy.matrices import Matrix, SparseMatrix +from sympy.testing.pytest import (XFAIL, slow, raises, warns_deprecated_sympy, + _both_exp_pow) +import math + + +def test_int_1(): + z = 1 + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is True + assert ask(Q.rational(z)) is True + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is False + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is True + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is True + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + +def test_int_11(): + z = 11 + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is True + assert ask(Q.rational(z)) is True + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is False + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is True + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is True + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is True + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + +def test_int_12(): + z = 12 + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is True + assert ask(Q.rational(z)) is True + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is False + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is True + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is True + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is True + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + +def test_float_1(): + z = 1.0 + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is None + assert ask(Q.rational(z)) is None + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is None + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is True + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is None + assert ask(Q.odd(z)) is None + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is None + assert ask(Q.composite(z)) is None + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + z = 7.2123 + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is False + assert ask(Q.rational(z)) is None + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is None + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is True + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + # test for issue #12168 + assert ask(Q.rational(math.pi)) is None + + +def test_zero_0(): + z = Integer(0) + assert ask(Q.nonzero(z)) is False + assert ask(Q.zero(z)) is True + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is True + assert ask(Q.rational(z)) is True + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is False + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is True + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is True + + +def test_negativeone(): + z = Integer(-1) + assert ask(Q.nonzero(z)) is True + assert ask(Q.zero(z)) is False + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is True + assert ask(Q.rational(z)) is True + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is False + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is False + assert ask(Q.negative(z)) is True + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is True + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + +def test_infinity(): + assert ask(Q.commutative(oo)) is True + assert ask(Q.integer(oo)) is False + assert ask(Q.rational(oo)) is False + assert ask(Q.algebraic(oo)) is False + assert ask(Q.real(oo)) is False + assert ask(Q.extended_real(oo)) is True + assert ask(Q.complex(oo)) is False + assert ask(Q.irrational(oo)) is False + assert ask(Q.imaginary(oo)) is False + assert ask(Q.positive(oo)) is False + assert ask(Q.extended_positive(oo)) is True + assert ask(Q.negative(oo)) is False + assert ask(Q.even(oo)) is False + assert ask(Q.odd(oo)) is False + assert ask(Q.finite(oo)) is False + assert ask(Q.infinite(oo)) is True + assert ask(Q.prime(oo)) is False + assert ask(Q.composite(oo)) is False + assert ask(Q.hermitian(oo)) is False + assert ask(Q.antihermitian(oo)) is False + assert ask(Q.positive_infinite(oo)) is True + assert ask(Q.negative_infinite(oo)) is False + + +def test_neg_infinity(): + mm = S.NegativeInfinity + assert ask(Q.commutative(mm)) is True + assert ask(Q.integer(mm)) is False + assert ask(Q.rational(mm)) is False + assert ask(Q.algebraic(mm)) is False + assert ask(Q.real(mm)) is False + assert ask(Q.extended_real(mm)) is True + assert ask(Q.complex(mm)) is False + assert ask(Q.irrational(mm)) is False + assert ask(Q.imaginary(mm)) is False + assert ask(Q.positive(mm)) is False + assert ask(Q.negative(mm)) is False + assert ask(Q.extended_negative(mm)) is True + assert ask(Q.even(mm)) is False + assert ask(Q.odd(mm)) is False + assert ask(Q.finite(mm)) is False + assert ask(Q.infinite(oo)) is True + assert ask(Q.prime(mm)) is False + assert ask(Q.composite(mm)) is False + assert ask(Q.hermitian(mm)) is False + assert ask(Q.antihermitian(mm)) is False + assert ask(Q.positive_infinite(-oo)) is False + assert ask(Q.negative_infinite(-oo)) is True + + +def test_complex_infinity(): + assert ask(Q.commutative(zoo)) is True + assert ask(Q.integer(zoo)) is False + assert ask(Q.rational(zoo)) is False + assert ask(Q.algebraic(zoo)) is False + assert ask(Q.real(zoo)) is False + assert ask(Q.extended_real(zoo)) is False + assert ask(Q.complex(zoo)) is False + assert ask(Q.irrational(zoo)) is False + assert ask(Q.imaginary(zoo)) is False + assert ask(Q.positive(zoo)) is False + assert ask(Q.negative(zoo)) is False + assert ask(Q.zero(zoo)) is False + assert ask(Q.nonzero(zoo)) is False + assert ask(Q.even(zoo)) is False + assert ask(Q.odd(zoo)) is False + assert ask(Q.finite(zoo)) is False + assert ask(Q.infinite(zoo)) is True + assert ask(Q.prime(zoo)) is False + assert ask(Q.composite(zoo)) is False + assert ask(Q.hermitian(zoo)) is False + assert ask(Q.antihermitian(zoo)) is False + assert ask(Q.positive_infinite(zoo)) is False + assert ask(Q.negative_infinite(zoo)) is False + + +def test_nan(): + nan = S.NaN + assert ask(Q.commutative(nan)) is True + assert ask(Q.integer(nan)) is None + assert ask(Q.rational(nan)) is None + assert ask(Q.algebraic(nan)) is None + assert ask(Q.real(nan)) is None + assert ask(Q.extended_real(nan)) is None + assert ask(Q.complex(nan)) is None + assert ask(Q.irrational(nan)) is None + assert ask(Q.imaginary(nan)) is None + assert ask(Q.positive(nan)) is None + assert ask(Q.nonzero(nan)) is None + assert ask(Q.zero(nan)) is None + assert ask(Q.even(nan)) is None + assert ask(Q.odd(nan)) is None + assert ask(Q.finite(nan)) is None + assert ask(Q.infinite(nan)) is None + assert ask(Q.prime(nan)) is None + assert ask(Q.composite(nan)) is None + assert ask(Q.hermitian(nan)) is None + assert ask(Q.antihermitian(nan)) is None + + +def test_Rational_number(): + r = Rational(3, 4) + assert ask(Q.commutative(r)) is True + assert ask(Q.integer(r)) is False + assert ask(Q.rational(r)) is True + assert ask(Q.real(r)) is True + assert ask(Q.complex(r)) is True + assert ask(Q.irrational(r)) is False + assert ask(Q.imaginary(r)) is False + assert ask(Q.positive(r)) is True + assert ask(Q.negative(r)) is False + assert ask(Q.even(r)) is False + assert ask(Q.odd(r)) is False + assert ask(Q.finite(r)) is True + assert ask(Q.prime(r)) is False + assert ask(Q.composite(r)) is False + assert ask(Q.hermitian(r)) is True + assert ask(Q.antihermitian(r)) is False + + r = Rational(1, 4) + assert ask(Q.positive(r)) is True + assert ask(Q.negative(r)) is False + + r = Rational(5, 4) + assert ask(Q.negative(r)) is False + assert ask(Q.positive(r)) is True + + r = Rational(5, 3) + assert ask(Q.positive(r)) is True + assert ask(Q.negative(r)) is False + + r = Rational(-3, 4) + assert ask(Q.positive(r)) is False + assert ask(Q.negative(r)) is True + + r = Rational(-1, 4) + assert ask(Q.positive(r)) is False + assert ask(Q.negative(r)) is True + + r = Rational(-5, 4) + assert ask(Q.negative(r)) is True + assert ask(Q.positive(r)) is False + + r = Rational(-5, 3) + assert ask(Q.positive(r)) is False + assert ask(Q.negative(r)) is True + + +def test_sqrt_2(): + z = sqrt(2) + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is False + assert ask(Q.rational(z)) is False + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is True + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is True + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + +def test_pi(): + z = S.Pi + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is False + assert ask(Q.rational(z)) is False + assert ask(Q.algebraic(z)) is False + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is True + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is True + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + z = S.Pi + 1 + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is False + assert ask(Q.rational(z)) is False + assert ask(Q.algebraic(z)) is False + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is True + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is True + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + z = 2*S.Pi + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is False + assert ask(Q.rational(z)) is False + assert ask(Q.algebraic(z)) is False + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is True + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is True + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + z = S.Pi ** 2 + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is False + assert ask(Q.rational(z)) is False + assert ask(Q.algebraic(z)) is False + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is True + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is True + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + z = (1 + S.Pi) ** 2 + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is False + assert ask(Q.rational(z)) is False + assert ask(Q.algebraic(z)) is None + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is True + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is True + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + +def test_E(): + z = S.Exp1 + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is False + assert ask(Q.rational(z)) is False + assert ask(Q.algebraic(z)) is False + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is True + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is True + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + +def test_GoldenRatio(): + z = S.GoldenRatio + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is False + assert ask(Q.rational(z)) is False + assert ask(Q.algebraic(z)) is True + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is True + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is True + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + +def test_TribonacciConstant(): + z = S.TribonacciConstant + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is False + assert ask(Q.rational(z)) is False + assert ask(Q.algebraic(z)) is True + assert ask(Q.real(z)) is True + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is True + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is True + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is True + assert ask(Q.antihermitian(z)) is False + + +def test_I(): + z = I + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is False + assert ask(Q.rational(z)) is False + assert ask(Q.algebraic(z)) is True + assert ask(Q.real(z)) is False + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is False + assert ask(Q.imaginary(z)) is True + assert ask(Q.positive(z)) is False + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is False + assert ask(Q.antihermitian(z)) is True + + z = 1 + I + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is False + assert ask(Q.rational(z)) is False + assert ask(Q.algebraic(z)) is True + assert ask(Q.real(z)) is False + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is False + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is False + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is False + assert ask(Q.antihermitian(z)) is False + + z = I*(1 + I) + assert ask(Q.commutative(z)) is True + assert ask(Q.integer(z)) is False + assert ask(Q.rational(z)) is False + assert ask(Q.algebraic(z)) is True + assert ask(Q.real(z)) is False + assert ask(Q.complex(z)) is True + assert ask(Q.irrational(z)) is False + assert ask(Q.imaginary(z)) is False + assert ask(Q.positive(z)) is False + assert ask(Q.negative(z)) is False + assert ask(Q.even(z)) is False + assert ask(Q.odd(z)) is False + assert ask(Q.finite(z)) is True + assert ask(Q.prime(z)) is False + assert ask(Q.composite(z)) is False + assert ask(Q.hermitian(z)) is False + assert ask(Q.antihermitian(z)) is False + + z = I**(I) + assert ask(Q.imaginary(z)) is False + assert ask(Q.real(z)) is True + + z = (-I)**(I) + assert ask(Q.imaginary(z)) is False + assert ask(Q.real(z)) is True + + z = (3*I)**(I) + assert ask(Q.imaginary(z)) is False + assert ask(Q.real(z)) is False + + z = (1)**(I) + assert ask(Q.imaginary(z)) is False + assert ask(Q.real(z)) is True + + z = (-1)**(I) + assert ask(Q.imaginary(z)) is False + assert ask(Q.real(z)) is True + + z = (1+I)**(I) + assert ask(Q.imaginary(z)) is False + assert ask(Q.real(z)) is False + + z = (I)**(I+3) + assert ask(Q.imaginary(z)) is True + assert ask(Q.real(z)) is False + + z = (I)**(I+2) + assert ask(Q.imaginary(z)) is False + assert ask(Q.real(z)) is True + + z = (I)**(2) + assert ask(Q.imaginary(z)) is False + assert ask(Q.real(z)) is True + + z = (I)**(3) + assert ask(Q.imaginary(z)) is True + assert ask(Q.real(z)) is False + + z = (3)**(I) + assert ask(Q.imaginary(z)) is False + assert ask(Q.real(z)) is False + + z = (I)**(0) + assert ask(Q.imaginary(z)) is False + assert ask(Q.real(z)) is True + +def test_bounded(): + x, y, z = symbols('x,y,z') + a = x + y + x, y = a.args + assert ask(Q.finite(a), Q.positive_infinite(y)) is None + assert ask(Q.finite(x)) is None + assert ask(Q.finite(x), Q.finite(x)) is True + assert ask(Q.finite(x), Q.finite(y)) is None + assert ask(Q.finite(x), Q.complex(x)) is True + assert ask(Q.finite(x), Q.extended_real(x)) is None + + assert ask(Q.finite(x + 1)) is None + assert ask(Q.finite(x + 1), Q.finite(x)) is True + a = x + y + x, y = a.args + # B + B + assert ask(Q.finite(a), Q.finite(x) & Q.finite(y)) is True + assert ask(Q.finite(a), Q.positive(x) & Q.finite(y)) is True + assert ask(Q.finite(a), Q.finite(x) & Q.positive(y)) is True + assert ask(Q.finite(a), Q.positive(x) & Q.positive(y)) is True + assert ask(Q.finite(a), Q.positive(x) & Q.finite(y) + & ~Q.positive(y)) is True + assert ask(Q.finite(a), Q.finite(x) & ~Q.positive(x) + & Q.positive(y)) is True + assert ask(Q.finite(a), Q.finite(x) & Q.finite(y) & ~Q.positive(x) + & ~Q.positive(y)) is True + # B + U + assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y)) is False + assert ask(Q.finite(a), Q.positive(x) & ~Q.finite(y)) is False + assert ask(Q.finite(a), Q.finite(x) + & Q.positive_infinite(y)) is False + assert ask(Q.finite(a), Q.positive(x) + & Q.positive_infinite(y)) is False + assert ask(Q.finite(a), Q.positive(x) & ~Q.finite(y) + & ~Q.positive(y)) is False + assert ask(Q.finite(a), Q.finite(x) & ~Q.positive(x) + & Q.positive_infinite(y)) is False + assert ask(Q.finite(a), Q.finite(x) & ~Q.positive(x) & ~Q.finite(y) + & ~Q.positive(y)) is False + # B + ? + assert ask(Q.finite(a), Q.finite(x)) is None + assert ask(Q.finite(a), Q.positive(x)) is None + assert ask(Q.finite(a), Q.finite(x) + & Q.extended_positive(y)) is None + assert ask(Q.finite(a), Q.positive(x) + & Q.extended_positive(y)) is None + assert ask(Q.finite(a), Q.positive(x) & ~Q.positive(y)) is None + assert ask(Q.finite(a), Q.finite(x) & ~Q.positive(x) + & Q.extended_positive(y)) is None + assert ask(Q.finite(a), Q.finite(x) & ~Q.positive(x) + & ~Q.positive(y)) is None + # U + U + assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y)) is None + assert ask(Q.finite(a), Q.positive_infinite(x) + & ~Q.finite(y)) is None + assert ask(Q.finite(a), ~Q.finite(x) + & Q.positive_infinite(y)) is None + assert ask(Q.finite(a), Q.positive_infinite(x) + & Q.positive_infinite(y)) is False + assert ask(Q.finite(a), Q.positive_infinite(x) & ~Q.finite(y) + & ~Q.extended_positive(y)) is None + assert ask(Q.finite(a), ~Q.finite(x) & ~Q.extended_positive(x) + & Q.positive_infinite(y)) is None + assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y) + & ~Q.extended_positive(x) & ~Q.extended_positive(y)) is False + # U + ? + assert ask(Q.finite(a), ~Q.finite(y)) is None + assert ask(Q.finite(a), Q.extended_positive(x) + & ~Q.finite(y)) is None + assert ask(Q.finite(a), Q.positive_infinite(y)) is None + assert ask(Q.finite(a), Q.extended_positive(x) + & Q.positive_infinite(y)) is False + assert ask(Q.finite(a), Q.extended_positive(x) + & ~Q.finite(y) & ~Q.extended_positive(y)) is None + assert ask(Q.finite(a), ~Q.extended_positive(x) + & Q.positive_infinite(y)) is None + assert ask(Q.finite(a), ~Q.extended_positive(x) & ~Q.finite(y) + & ~Q.extended_positive(y)) is False + # ? + ? + assert ask(Q.finite(a)) is None + assert ask(Q.finite(a), Q.extended_positive(x)) is None + assert ask(Q.finite(a), Q.extended_positive(y)) is None + assert ask(Q.finite(a), Q.extended_positive(x) + & Q.extended_positive(y)) is None + assert ask(Q.finite(a), Q.extended_positive(x) + & ~Q.extended_positive(y)) is None + assert ask(Q.finite(a), ~Q.extended_positive(x) + & Q.extended_positive(y)) is None + assert ask(Q.finite(a), ~Q.extended_positive(x) + & ~Q.extended_positive(y)) is None + + x, y, z = symbols('x,y,z') + a = x + y + z + x, y, z = a.args + assert ask(Q.finite(a), Q.negative(x) & Q.negative(y) + & Q.negative(z)) is True + assert ask(Q.finite(a), Q.negative(x) & Q.negative(y) + & Q.finite(z)) is True + assert ask(Q.finite(a), Q.negative(x) & Q.negative(y) + & Q.positive(z)) is True + assert ask(Q.finite(a), Q.negative(x) & Q.negative(y) + & Q.negative_infinite(z)) is False + assert ask(Q.finite(a), Q.negative(x) & Q.negative(y) + & ~Q.finite(z)) is False + assert ask(Q.finite(a), Q.negative(x) & Q.negative(y) + & Q.positive_infinite(z)) is False + assert ask(Q.finite(a), Q.negative(x) & Q.negative(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.negative(y)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.negative(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.finite(y) + & Q.finite(z)) is True + assert ask(Q.finite(a), Q.negative(x) & Q.finite(y) + & Q.positive(z)) is True + assert ask(Q.finite(a), Q.negative(x) & Q.finite(y) + & Q.negative_infinite(z)) is False + assert ask(Q.finite(a), Q.negative(x) & Q.finite(y) + & ~Q.finite(z)) is False + assert ask(Q.finite(a), Q.negative(x) & Q.finite(y) + & Q.positive_infinite(z)) is False + assert ask(Q.finite(a), Q.negative(x) & Q.finite(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.finite(y)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.finite(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.positive(y) + & Q.positive(z)) is True + assert ask(Q.finite(a), Q.negative(x) & Q.positive(y) + & Q.negative_infinite(z)) is False + assert ask(Q.finite(a), Q.negative(x) & Q.positive(y) + & ~Q.finite(z)) is False + assert ask(Q.finite(a), Q.negative(x) & Q.positive(y) + & Q.positive_infinite(z)) is False + assert ask(Q.finite(a), Q.negative(x) & Q.positive(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.extended_positive(y) + & Q.finite(y)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.positive(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.negative_infinite(y) + & Q.negative_infinite(z)) is False + assert ask(Q.finite(a), Q.negative(x) & Q.negative_infinite(y) + & ~Q.finite(z)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.negative_infinite(y) + & Q.positive_infinite(z)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.negative_infinite(y) + & Q.extended_negative(z)) is False + assert ask(Q.finite(a), Q.negative(x) + & Q.negative_infinite(y)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.negative_infinite(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.negative(x) & ~Q.finite(y) + & ~Q.finite(z)) is None + assert ask(Q.finite(a), Q.negative(x) & ~Q.finite(y) + & Q.positive_infinite(z)) is None + assert ask(Q.finite(a), Q.negative(x) & ~Q.finite(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.negative(x) & ~Q.finite(y)) is None + assert ask(Q.finite(a), Q.negative(x) & ~Q.finite(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.positive_infinite(y) + & Q.positive_infinite(z)) is False + assert ask(Q.finite(a), Q.negative(x) & Q.positive_infinite(y) + & Q.negative_infinite(z)) is None + assert ask(Q.finite(a), Q.negative(x) & + Q.positive_infinite(y)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.positive_infinite(y) + & Q.extended_positive(z)) is False + assert ask(Q.finite(a), Q.negative(x) & Q.extended_negative(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.negative(x) + & Q.extended_negative(y)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.extended_negative(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.negative(x)) is None + assert ask(Q.finite(a), Q.negative(x) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.negative(x) & Q.extended_positive(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.finite(y) + & Q.finite(z)) is True + assert ask(Q.finite(a), Q.finite(x) & Q.finite(y) + & Q.positive(z)) is True + assert ask(Q.finite(a), Q.finite(x) & Q.finite(y) + & Q.negative_infinite(z)) is False + assert ask(Q.finite(a), Q.finite(x) & Q.finite(y) + & ~Q.finite(z)) is False + assert ask(Q.finite(a), Q.finite(x) & Q.finite(y) + & Q.positive_infinite(z)) is False + assert ask(Q.finite(a), Q.finite(x) & Q.finite(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.finite(y)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.finite(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.positive(y) + & Q.positive(z)) is True + assert ask(Q.finite(a), Q.finite(x) & Q.positive(y) + & Q.negative_infinite(z)) is False + assert ask(Q.finite(a), Q.finite(x) & Q.positive(y) + & ~Q.finite(z)) is False + assert ask(Q.finite(a), Q.finite(x) & Q.positive(y) + & Q.positive_infinite(z)) is False + assert ask(Q.finite(a), Q.finite(x) & Q.positive(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.positive(y)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.positive(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.negative_infinite(y) + & Q.negative_infinite(z)) is False + assert ask(Q.finite(a), Q.finite(x) & Q.negative_infinite(y) + & ~Q.finite(z)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.negative_infinite(y) + & Q.positive_infinite(z)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.negative_infinite(y) + & Q.extended_negative(z)) is False + assert ask(Q.finite(a), Q.finite(x) + & Q.negative_infinite(y)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.negative_infinite(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y) + & ~Q.finite(z)) is None + assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y) + & Q.positive_infinite(z)) is None + assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y)) is None + assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.positive_infinite(y) + & Q.positive_infinite(z)) is False + assert ask(Q.finite(a), Q.finite(x) & Q.positive_infinite(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.finite(x) + & Q.positive_infinite(y)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.positive_infinite(y) + & Q.extended_positive(z)) is False + assert ask(Q.finite(a), Q.finite(x) & Q.extended_negative(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.finite(x) + & Q.extended_negative(y)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.extended_negative(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.finite(x)) is None + assert ask(Q.finite(a), Q.finite(x) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.extended_positive(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.positive(x) & Q.positive(y) + & Q.positive(z)) is True + assert ask(Q.finite(a), Q.positive(x) & Q.positive(y) + & Q.negative_infinite(z)) is False + assert ask(Q.finite(a), Q.positive(x) & Q.positive(y) + & ~Q.finite(z)) is False + assert ask(Q.finite(a), Q.positive(x) & Q.positive(y) + & Q.positive_infinite(z)) is False + assert ask(Q.finite(a), Q.positive(x) & Q.positive(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.positive(x) & Q.positive(y)) is None + assert ask(Q.finite(a), Q.positive(x) & Q.positive(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.positive(x) & Q.negative_infinite(y) + & Q.negative_infinite(z)) is False + assert ask(Q.finite(a), Q.positive(x) & Q.negative_infinite(y) + & ~Q.finite(z)) is None + assert ask(Q.finite(a), Q.positive(x) & Q.negative_infinite(y) + & Q.positive_infinite(z)) is None + assert ask(Q.finite(a), Q.positive(x) & Q.negative_infinite(y) + & Q.extended_negative(z)) is False + assert ask(Q.finite(a), Q.positive(x) + & Q.negative_infinite(y)) is None + assert ask(Q.finite(a), Q.positive(x) & Q.negative_infinite(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.positive(x) & ~Q.finite(y) + & ~Q.finite(z)) is None + assert ask(Q.finite(a), Q.positive(x) & ~Q.finite(y) + & Q.positive_infinite(z)) is None + assert ask(Q.finite(a), Q.positive(x) & ~Q.finite(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.positive(x) & ~Q.finite(y)) is None + assert ask(Q.finite(a), Q.positive(x) & ~Q.finite(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.positive(x) & Q.positive_infinite(y) + & Q.positive_infinite(z)) is False + assert ask(Q.finite(a), Q.positive(x) & Q.positive_infinite(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.positive(x) + & Q.positive_infinite(y)) is None + assert ask(Q.finite(a), Q.positive(x) & Q.positive_infinite(y) + & Q.extended_positive(z)) is False + assert ask(Q.finite(a), Q.positive(x) & Q.extended_negative(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.positive(x) + & Q.extended_negative(y)) is None + assert ask(Q.finite(a), Q.positive(x) & Q.extended_negative(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.positive(x)) is None + assert ask(Q.finite(a), Q.positive(x) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.positive(x) & Q.extended_positive(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.negative_infinite(y) & Q.negative_infinite(z)) is False + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.negative_infinite(y) & ~Q.finite(z)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.negative_infinite(y)& Q.positive_infinite(z)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.negative_infinite(y) & Q.extended_negative(z)) is False + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.negative_infinite(y)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.negative_infinite(y) & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & ~Q.finite(y) & ~Q.finite(z)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & ~Q.finite(y) & Q.positive_infinite(z)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & ~Q.finite(y) & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & ~Q.finite(y)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & ~Q.finite(y) & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.positive_infinite(y) & Q.positive_infinite(z)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.positive_infinite(y) & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.positive_infinite(y)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.positive_infinite(y) & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.extended_negative(y) & Q.extended_negative(z)) is False + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.extended_negative(y)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.extended_negative(y) & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.negative_infinite(x)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.negative_infinite(x) + & Q.extended_positive(y) & Q.extended_positive(z)) is None + assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y) + & ~Q.finite(z)) is None + assert ask(Q.finite(a), ~Q.finite(x) & Q.positive_infinite(z) + & ~Q.finite(z)) is None + assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y)) is None + assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), ~Q.finite(x) & Q.positive_infinite(y) + & Q.positive_infinite(z)) is None + assert ask(Q.finite(a), ~Q.finite(x) & Q.positive_infinite(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), ~Q.finite(x) + & Q.positive_infinite(y)) is None + assert ask(Q.finite(a), ~Q.finite(x) & Q.positive_infinite(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), ~Q.finite(x) & Q.extended_negative(y) + & Q.extended_negative(z)) is None + assert ask(Q.finite(a), ~Q.finite(x) + & Q.extended_negative(y)) is None + assert ask(Q.finite(a), ~Q.finite(x) & Q.extended_negative(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), ~Q.finite(x)) is None + assert ask(Q.finite(a), ~Q.finite(x) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), ~Q.finite(x) & Q.extended_positive(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.positive_infinite(x) + & Q.positive_infinite(y) & Q.positive_infinite(z)) is False + assert ask(Q.finite(a), Q.positive_infinite(x) + & Q.positive_infinite(y) & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.positive_infinite(x) + & Q.positive_infinite(y)) is None + assert ask(Q.finite(a), Q.positive_infinite(x) + & Q.positive_infinite(y) & Q.extended_positive(z)) is False + assert ask(Q.finite(a), Q.positive_infinite(x) + & Q.extended_negative(y) & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.positive_infinite(x) + & Q.extended_negative(y)) is None + assert ask(Q.finite(a), Q.positive_infinite(x) + & Q.extended_negative(y) & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.positive_infinite(x)) is None + assert ask(Q.finite(a), Q.positive_infinite(x) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.positive_infinite(x) + & Q.extended_positive(y) & Q.extended_positive(z)) is False + assert ask(Q.finite(a), Q.extended_negative(x) + & Q.extended_negative(y) & Q.extended_negative(z)) is None + assert ask(Q.finite(a), Q.extended_negative(x) + & Q.extended_negative(y)) is None + assert ask(Q.finite(a), Q.extended_negative(x) + & Q.extended_negative(y) & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.extended_negative(x)) is None + assert ask(Q.finite(a), Q.extended_negative(x) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.extended_negative(x) + & Q.extended_positive(y) & Q.extended_positive(z)) is None + assert ask(Q.finite(a)) is None + assert ask(Q.finite(a), Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.extended_positive(y) + & Q.extended_positive(z)) is None + assert ask(Q.finite(a), Q.extended_positive(x) + & Q.extended_positive(y) & Q.extended_positive(z)) is None + + assert ask(Q.finite(2*x)) is None + assert ask(Q.finite(2*x), Q.finite(x)) is True + + x, y, z = symbols('x,y,z') + a = x*y + x, y = a.args + assert ask(Q.finite(a), Q.finite(x) & Q.finite(y)) is True + assert ask(Q.finite(a), Q.finite(x) & ~Q.zero(x) & ~Q.finite(y)) is False + assert ask(Q.finite(a), Q.finite(x)) is None + assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(y) &~Q.zero(y)) is False + assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y)) is False + assert ask(Q.finite(a), ~Q.finite(x)) is None + assert ask(Q.finite(a), Q.finite(y)) is None + assert ask(Q.finite(a), ~Q.finite(y)) is None + assert ask(Q.finite(a)) is None + a = x*y*z + x, y, z = a.args + assert ask(Q.finite(a), Q.finite(x) & Q.finite(y) + & Q.finite(z)) is True + assert ask(Q.finite(a), Q.finite(x) & ~Q.zero(x) & Q.finite(y) + & ~Q.zero(y) & ~Q.finite(z)) is False + assert ask(Q.finite(a), Q.finite(x) & Q.finite(y)) is None + assert ask(Q.finite(a), Q.finite(x) & ~Q.zero(x) & ~Q.finite(y) + & Q.finite(z) & ~Q.zero(z)) is False + assert ask(Q.finite(a), Q.finite(x) & ~Q.zero(x) & ~Q.finite(y) + & ~Q.finite(z)) is False + assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y)) is None + assert ask(Q.finite(a), Q.finite(x) & Q.finite(z)) is None + assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(z)) is None + assert ask(Q.finite(a), Q.finite(x)) is None + assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(y) & ~Q.zero(y) + & Q.finite(z) & ~Q.zero(z)) is False + assert ask(Q.finite(a), ~Q.finite(x) & ~Q.zero(x) & Q.finite(y) + & ~Q.zero(y) & ~Q.finite(z)) is False + assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(y)) is None + assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y) + & Q.finite(z) & ~Q.zero(z)) is False + assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y) + & ~Q.finite(z)) is False + assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y)) is None + assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(z)) is None + assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(z)) is None + assert ask(Q.finite(a), ~Q.finite(x)) is None + assert ask(Q.finite(a), Q.finite(y) & Q.finite(z)) is None + assert ask(Q.finite(a), Q.finite(y) & ~Q.finite(z)) is None + assert ask(Q.finite(a), Q.finite(y)) is None + assert ask(Q.finite(a), ~Q.finite(y) & Q.finite(z)) is None + assert ask(Q.finite(a), ~Q.finite(y) & ~Q.finite(z)) is None + assert ask(Q.finite(a), ~Q.finite(y)) is None + assert ask(Q.finite(a), Q.finite(z)) is None + assert ask(Q.finite(a), ~Q.finite(z)) is None + assert ask(Q.finite(a), ~Q.finite(z) & Q.extended_nonzero(x) + & Q.extended_nonzero(y) & Q.extended_nonzero(z)) is None + assert ask(Q.finite(a), Q.extended_nonzero(x) & ~Q.finite(y) + & Q.extended_nonzero(y) & ~Q.finite(z) + & Q.extended_nonzero(z)) is False + + x, y, z = symbols('x,y,z') + assert ask(Q.finite(x**2)) is None + assert ask(Q.finite(2**x)) is None + assert ask(Q.finite(2**x), Q.finite(x)) is True + assert ask(Q.finite(x**x)) is None + assert ask(Q.finite(S.Half ** x)) is None + assert ask(Q.finite(S.Half ** x), Q.extended_positive(x)) is True + assert ask(Q.finite(S.Half ** x), Q.extended_negative(x)) is None + assert ask(Q.finite(2**x), Q.extended_negative(x)) is True + assert ask(Q.finite(sqrt(x))) is None + assert ask(Q.finite(2**x), ~Q.finite(x)) is False + assert ask(Q.finite(x**2), ~Q.finite(x)) is False + + # https://github.com/sympy/sympy/issues/27707 + assert ask(Q.finite(x**y), Q.real(x) & Q.real(y)) is None + assert ask(Q.finite(x**y), Q.real(x) & Q.negative(y)) is None + assert ask(Q.finite(x**y), Q.zero(x) & Q.negative(y)) is False + assert ask(Q.finite(x**y), Q.real(x) & Q.positive(y)) is True + assert ask(Q.finite(x**y), Q.nonzero(x) & Q.real(y)) is True + assert ask(Q.finite(x**y), Q.nonzero(x) & Q.negative(y)) is True + assert ask(Q.finite(x**y), Q.zero(x) & Q.positive(y)) is True + + # sign function + assert ask(Q.finite(sign(x))) is True + assert ask(Q.finite(sign(x)), ~Q.finite(x)) is True + + # exponential functions + assert ask(Q.finite(log(x))) is None + assert ask(Q.finite(log(x)), Q.finite(x)) is None + assert ask(Q.finite(log(x)), ~Q.zero(x)) is True + assert ask(Q.finite(log(x)), Q.infinite(x)) is False + assert ask(Q.finite(log(x)), Q.zero(x)) is False + assert ask(Q.finite(exp(x))) is None + assert ask(Q.finite(exp(x)), Q.finite(x)) is True + assert ask(Q.finite(exp(2))) is True + + # trigonometric functions + assert ask(Q.finite(sin(x))) is True + assert ask(Q.finite(sin(x)), ~Q.finite(x)) is True + assert ask(Q.finite(cos(x))) is True + assert ask(Q.finite(cos(x)), ~Q.finite(x)) is True + assert ask(Q.finite(2*sin(x))) is True + assert ask(Q.finite(sin(x)**2)) is True + assert ask(Q.finite(cos(x)**2)) is True + assert ask(Q.finite(cos(x) + sin(x))) is True + + +def test_unbounded(): + assert ask(Q.infinite(I * oo)) is True + assert ask(Q.infinite(1 + I*oo)) is True + assert ask(Q.infinite(3 * (I * oo))) is True + assert ask(Q.infinite(-I * oo)) is True + assert ask(Q.infinite(1 + zoo)) is True + assert ask(Q.infinite(I * zoo)) is True + assert ask(Q.infinite(x / y), Q.infinite(x) & Q.finite(y) & ~Q.zero(y)) is True + assert ask(Q.infinite(I * oo - I * oo)) is None + assert ask(Q.infinite(x * I * oo)) is None + assert ask(Q.infinite(1 / x), Q.finite(x) & ~Q.zero(x)) is False + assert ask(Q.infinite(1 / (I * oo))) is False + + +def test_issue_27441(): + # https://github.com/sympy/sympy/issues/27441 + assert ask(Q.composite(y), Q.integer(y) & Q.positive(y) & ~Q.prime(y)) is None + + +def test_issue_27447(): + x,y,z = symbols('x y z') + a = x*y + assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y)) is None + assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(y)) is None + + a = x*y*z + assert ask(Q.finite(a), Q.finite(x) & Q.finite(y) + & ~Q.finite(z)) is None + assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y) + & Q.finite(z) ) is None + assert ask(Q.finite(a), Q.finite(x) & ~Q.finite(y) + & ~Q.finite(z)) is None + assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(y) + & Q.finite(z)) is None + assert ask(Q.finite(a), ~Q.finite(x) & Q.finite(y) + & ~Q.finite(z)) is None + assert ask(Q.finite(a), ~Q.finite(x) & ~Q.finite(y) + & Q.finite(z)) is None + + +@XFAIL +def test_issue_27662_xfail(): + assert ask(Q.finite(x*y), ~Q.finite(x) + & Q.zero(y)) is None + + +@XFAIL +def test_bounded_xfail(): + """We need to support relations in ask for this to work""" + assert ask(Q.finite(sin(x)**x)) is True + assert ask(Q.finite(cos(x)**x)) is True + + +def test_commutative(): + """By default objects are Q.commutative that is why it returns True + for both key=True and key=False""" + assert ask(Q.commutative(x)) is True + assert ask(Q.commutative(x), ~Q.commutative(x)) is False + assert ask(Q.commutative(x), Q.complex(x)) is True + assert ask(Q.commutative(x), Q.imaginary(x)) is True + assert ask(Q.commutative(x), Q.real(x)) is True + assert ask(Q.commutative(x), Q.positive(x)) is True + assert ask(Q.commutative(x), ~Q.commutative(y)) is True + + assert ask(Q.commutative(2*x)) is True + assert ask(Q.commutative(2*x), ~Q.commutative(x)) is False + + assert ask(Q.commutative(x + 1)) is True + assert ask(Q.commutative(x + 1), ~Q.commutative(x)) is False + + assert ask(Q.commutative(x**2)) is True + assert ask(Q.commutative(x**2), ~Q.commutative(x)) is False + + assert ask(Q.commutative(log(x))) is True + + +@_both_exp_pow +def test_complex(): + assert ask(Q.complex(x)) is None + assert ask(Q.complex(x), Q.complex(x)) is True + assert ask(Q.complex(x), Q.complex(y)) is None + assert ask(Q.complex(x), ~Q.complex(x)) is False + assert ask(Q.complex(x), Q.real(x)) is True + assert ask(Q.complex(x), ~Q.real(x)) is None + assert ask(Q.complex(x), Q.rational(x)) is True + assert ask(Q.complex(x), Q.irrational(x)) is True + assert ask(Q.complex(x), Q.positive(x)) is True + assert ask(Q.complex(x), Q.imaginary(x)) is True + assert ask(Q.complex(x), Q.algebraic(x)) is True + + # a+b + assert ask(Q.complex(x + 1), Q.complex(x)) is True + assert ask(Q.complex(x + 1), Q.real(x)) is True + assert ask(Q.complex(x + 1), Q.rational(x)) is True + assert ask(Q.complex(x + 1), Q.irrational(x)) is True + assert ask(Q.complex(x + 1), Q.imaginary(x)) is True + assert ask(Q.complex(x + 1), Q.integer(x)) is True + assert ask(Q.complex(x + 1), Q.even(x)) is True + assert ask(Q.complex(x + 1), Q.odd(x)) is True + assert ask(Q.complex(x + y), Q.complex(x) & Q.complex(y)) is True + assert ask(Q.complex(x + y), Q.real(x) & Q.imaginary(y)) is True + + # a*x +b + assert ask(Q.complex(2*x + 1), Q.complex(x)) is True + assert ask(Q.complex(2*x + 1), Q.real(x)) is True + assert ask(Q.complex(2*x + 1), Q.positive(x)) is True + assert ask(Q.complex(2*x + 1), Q.rational(x)) is True + assert ask(Q.complex(2*x + 1), Q.irrational(x)) is True + assert ask(Q.complex(2*x + 1), Q.imaginary(x)) is True + assert ask(Q.complex(2*x + 1), Q.integer(x)) is True + assert ask(Q.complex(2*x + 1), Q.even(x)) is True + assert ask(Q.complex(2*x + 1), Q.odd(x)) is True + + # x**2 + assert ask(Q.complex(x**2), Q.complex(x)) is True + assert ask(Q.complex(x**2), Q.real(x)) is True + assert ask(Q.complex(x**2), Q.positive(x)) is True + assert ask(Q.complex(x**2), Q.rational(x)) is True + assert ask(Q.complex(x**2), Q.irrational(x)) is True + assert ask(Q.complex(x**2), Q.imaginary(x)) is True + assert ask(Q.complex(x**2), Q.integer(x)) is True + assert ask(Q.complex(x**2), Q.even(x)) is True + assert ask(Q.complex(x**2), Q.odd(x)) is True + + # 2**x + assert ask(Q.complex(2**x), Q.complex(x)) is True + assert ask(Q.complex(2**x), Q.real(x)) is True + assert ask(Q.complex(2**x), Q.positive(x)) is True + assert ask(Q.complex(2**x), Q.rational(x)) is True + assert ask(Q.complex(2**x), Q.irrational(x)) is True + assert ask(Q.complex(2**x), Q.imaginary(x)) is True + assert ask(Q.complex(2**x), Q.integer(x)) is True + assert ask(Q.complex(2**x), Q.even(x)) is True + assert ask(Q.complex(2**x), Q.odd(x)) is True + assert ask(Q.complex(x**y), Q.complex(x) & Q.complex(y)) is True + + # trigonometric expressions + assert ask(Q.complex(sin(x))) is True + assert ask(Q.complex(sin(2*x + 1))) is True + assert ask(Q.complex(cos(x))) is True + assert ask(Q.complex(cos(2*x + 1))) is True + + # exponential + assert ask(Q.complex(exp(x))) is True + assert ask(Q.complex(exp(x))) is True + + # Q.complexes + assert ask(Q.complex(Abs(x))) is True + assert ask(Q.complex(re(x))) is True + assert ask(Q.complex(im(x))) is True + + +def test_even_query(): + assert ask(Q.even(x)) is None + assert ask(Q.even(x), Q.integer(x)) is None + assert ask(Q.even(x), ~Q.integer(x)) is False + assert ask(Q.even(x), Q.rational(x)) is None + assert ask(Q.even(x), Q.positive(x)) is None + + assert ask(Q.even(2*x)) is None + assert ask(Q.even(2*x), Q.integer(x)) is True + assert ask(Q.even(2*x), Q.even(x)) is True + assert ask(Q.even(2*x), Q.irrational(x)) is False + assert ask(Q.even(2*x), Q.odd(x)) is True + assert ask(Q.even(2*x), ~Q.integer(x)) is None + assert ask(Q.even(3*x), Q.integer(x)) is None + assert ask(Q.even(3*x), Q.even(x)) is True + assert ask(Q.even(3*x), Q.odd(x)) is False + + assert ask(Q.even(x + 1), Q.odd(x)) is True + assert ask(Q.even(x + 1), Q.even(x)) is False + assert ask(Q.even(x + 2), Q.odd(x)) is False + assert ask(Q.even(x + 2), Q.even(x)) is True + assert ask(Q.even(7 - x), Q.odd(x)) is True + assert ask(Q.even(7 + x), Q.odd(x)) is True + assert ask(Q.even(x + y), Q.odd(x) & Q.odd(y)) is True + assert ask(Q.even(x + y), Q.odd(x) & Q.even(y)) is False + assert ask(Q.even(x + y), Q.even(x) & Q.even(y)) is True + + assert ask(Q.even(2*x + 1), Q.integer(x)) is False + assert ask(Q.even(2*x*y), Q.rational(x) & Q.rational(x)) is None + assert ask(Q.even(2*x*y), Q.irrational(x) & Q.irrational(x)) is None + + assert ask(Q.even(x + y + z), Q.odd(x) & Q.odd(y) & Q.even(z)) is True + assert ask(Q.even(x + y + z + t), + Q.odd(x) & Q.odd(y) & Q.even(z) & Q.integer(t)) is None + + assert ask(Q.even(Abs(x)), Q.even(x)) is True + assert ask(Q.even(Abs(x)), ~Q.even(x)) is None + assert ask(Q.even(re(x)), Q.even(x)) is True + assert ask(Q.even(re(x)), ~Q.even(x)) is None + assert ask(Q.even(im(x)), Q.even(x)) is True + assert ask(Q.even(im(x)), Q.real(x)) is True + + assert ask(Q.even((-1)**n), Q.integer(n)) is False + + assert ask(Q.even(k**2), Q.even(k)) is True + assert ask(Q.even(n**2), Q.odd(n)) is False + assert ask(Q.even(2**k), Q.even(k)) is None + assert ask(Q.even(x**2)) is None + + assert ask(Q.even(k**m), Q.even(k) & Q.integer(m) & ~Q.negative(m)) is None + assert ask(Q.even(n**m), Q.odd(n) & Q.integer(m) & ~Q.negative(m)) is False + + assert ask(Q.even(k**p), Q.even(k) & Q.integer(p) & Q.positive(p)) is True + assert ask(Q.even(n**p), Q.odd(n) & Q.integer(p) & Q.positive(p)) is False + + assert ask(Q.even(m**k), Q.even(k) & Q.integer(m) & ~Q.negative(m)) is None + assert ask(Q.even(p**k), Q.even(k) & Q.integer(p) & Q.positive(p)) is None + + assert ask(Q.even(m**n), Q.odd(n) & Q.integer(m) & ~Q.negative(m)) is None + assert ask(Q.even(p**n), Q.odd(n) & Q.integer(p) & Q.positive(p)) is None + + assert ask(Q.even(k**x), Q.even(k)) is None + assert ask(Q.even(n**x), Q.odd(n)) is None + + assert ask(Q.even(x*y), Q.integer(x) & Q.integer(y)) is None + assert ask(Q.even(x*x), Q.integer(x)) is None + assert ask(Q.even(x*(x + y)), Q.integer(x) & Q.odd(y)) is True + assert ask(Q.even(x*(x + y)), Q.integer(x) & Q.even(y)) is None + + +@XFAIL +def test_evenness_in_ternary_integer_product_with_odd(): + # Tests that oddness inference is independent of term ordering. + # Term ordering at the point of testing depends on SymPy's symbol order, so + # we try to force a different order by modifying symbol names. + assert ask(Q.even(x*y*(y + z)), Q.integer(x) & Q.integer(y) & Q.odd(z)) is True + assert ask(Q.even(y*x*(x + z)), Q.integer(x) & Q.integer(y) & Q.odd(z)) is True + + +def test_evenness_in_ternary_integer_product_with_even(): + assert ask(Q.even(x*y*(y + z)), Q.integer(x) & Q.integer(y) & Q.even(z)) is None + + +def test_extended_real(): + assert ask(Q.extended_real(x), Q.positive_infinite(x)) is True + assert ask(Q.extended_real(x), Q.positive(x)) is True + assert ask(Q.extended_real(x), Q.zero(x)) is True + assert ask(Q.extended_real(x), Q.negative(x)) is True + assert ask(Q.extended_real(x), Q.negative_infinite(x)) is True + + assert ask(Q.extended_real(-x), Q.positive(x)) is True + assert ask(Q.extended_real(-x), Q.negative(x)) is True + + assert ask(Q.extended_real(x + S.Infinity), Q.real(x)) is True + + assert ask(Q.extended_real(x), Q.infinite(x)) is None + + +@_both_exp_pow +def test_rational(): + assert ask(Q.rational(x), Q.integer(x)) is True + assert ask(Q.rational(x), Q.irrational(x)) is False + assert ask(Q.rational(x), Q.real(x)) is None + assert ask(Q.rational(x), Q.positive(x)) is None + assert ask(Q.rational(x), Q.negative(x)) is None + assert ask(Q.rational(x), Q.nonzero(x)) is None + assert ask(Q.rational(x), ~Q.algebraic(x)) is False + + assert ask(Q.rational(2*x), Q.rational(x)) is True + assert ask(Q.rational(2*x), Q.integer(x)) is True + assert ask(Q.rational(2*x), Q.even(x)) is True + assert ask(Q.rational(2*x), Q.odd(x)) is True + assert ask(Q.rational(2*x), Q.irrational(x)) is False + + assert ask(Q.rational(x/2), Q.rational(x)) is True + assert ask(Q.rational(x/2), Q.integer(x)) is True + assert ask(Q.rational(x/2), Q.even(x)) is True + assert ask(Q.rational(x/2), Q.odd(x)) is True + assert ask(Q.rational(x/2), Q.irrational(x)) is False + + assert ask(Q.rational(1/x), Q.rational(x) & Q.nonzero(x)) is True + assert ask(Q.rational(1/x), Q.integer(x) & Q.nonzero(x)) is True + assert ask(Q.rational(1/x), Q.even(x) & Q.nonzero(x)) is True + assert ask(Q.rational(1/x), Q.odd(x)) is True + assert ask(Q.rational(1/x), Q.irrational(x)) is False + + assert ask(Q.rational(2/x), Q.rational(x) & Q.nonzero(x)) is True + assert ask(Q.rational(2/x), Q.integer(x) & Q.nonzero(x)) is True + assert ask(Q.rational(2/x), Q.even(x) & Q.nonzero(x)) is True + assert ask(Q.rational(2/x), Q.odd(x)) is True + assert ask(Q.rational(2/x), Q.irrational(x)) is False + + assert ask(Q.rational(x), ~Q.algebraic(x)) is False + + # with multiple symbols + assert ask(Q.rational(x*y), Q.irrational(x) & Q.irrational(y)) is None + assert ask(Q.rational(y/x), Q.rational(x) & Q.rational(y) & Q.nonzero(x)) is True + assert ask(Q.rational(y/x), Q.integer(x) & Q.rational(y) & Q.nonzero(x)) is True + assert ask(Q.rational(y/x), Q.even(x) & Q.rational(y) & Q.nonzero(x)) is True + assert ask(Q.rational(y/x), Q.odd(x) & Q.rational(y)) is True + assert ask(Q.rational(y/x), Q.irrational(x) & Q.rational(y) & Q.nonzero(y)) is False + + for f in [exp, sin, tan, asin, atan, cos]: + assert ask(Q.rational(f(7))) is False + assert ask(Q.rational(f(7, evaluate=False))) is False + assert ask(Q.rational(f(0, evaluate=False))) is True + assert ask(Q.rational(f(x)), Q.rational(x)) is None + assert ask(Q.rational(f(x)), Q.rational(x) & Q.nonzero(x)) is False + + for g in [log, acos]: + assert ask(Q.rational(g(7))) is False + assert ask(Q.rational(g(7, evaluate=False))) is False + assert ask(Q.rational(g(1, evaluate=False))) is True + assert ask(Q.rational(g(x)), Q.rational(x)) is None + assert ask(Q.rational(g(x)), Q.rational(x) & Q.nonzero(x - 1)) is False + + for h in [cot, acot]: + assert ask(Q.rational(h(7))) is False + assert ask(Q.rational(h(7, evaluate=False))) is False + assert ask(Q.rational(h(x)), Q.rational(x)) is False + + # https://github.com/sympy/sympy/issues/27442 + assert ask(Q.rational(x**y),Q.irrational(x) & Q.rational(y)) is None + assert ask(Q.rational(x**y),Q.integer(x) & Q.prime(x) & Q.rational(y)) is None + assert ask(Q.rational(x**y),Q.integer(x) & Q.integer(y)) is None + assert ask(Q.rational(x**y),Q.integer(x) & Q.eq(x,0) & Q.integer(y)) is None + assert ask(Q.rational(x**y),Q.eq(x,1) & Q.rational(y)) is None + assert ask(Q.rational(x**y),Q.eq(x,-1) & Q.rational(y)) is None + assert ask(Q.rational(x**y), Q.prime(x) & Q.rational(y)) is None + assert ask(Q.rational(x**y), ~Q.rational(x) & Q.integer(y) ) is None + assert ask(Q.rational(Pow(-1, x, evaluate=False), Q.rational(x))) is None + assert ask(Q.rational(x**y), Q.integer(y) & ~Q. algebraic(x)) is None + assert ask(Q.rational(x**y), Q.integer(y) & ~Q. algebraic(x) & ~Q.zero(x)) is None + assert ask(Q.rational(x**y), Q.integer(y) & ~Q.algebraic(x) & Q.complex(x) & ~Q.real(x)) is None + assert ask(Q.rational(x**y), Q.integer(y) & ~Q.algebraic(x) & Q.complex(x)) is None + + +def test_hermitian(): + assert ask(Q.hermitian(x)) is None + assert ask(Q.hermitian(x), Q.antihermitian(x)) is None + assert ask(Q.hermitian(x), Q.imaginary(x)) is False + assert ask(Q.hermitian(x), Q.prime(x)) is True + assert ask(Q.hermitian(x), Q.real(x)) is True + assert ask(Q.hermitian(x), Q.zero(x)) is True + + assert ask(Q.hermitian(x + 1), Q.antihermitian(x)) is None + assert ask(Q.hermitian(x + 1), Q.complex(x)) is None + assert ask(Q.hermitian(x + 1), Q.hermitian(x)) is True + assert ask(Q.hermitian(x + 1), Q.imaginary(x)) is False + assert ask(Q.hermitian(x + 1), Q.real(x)) is True + assert ask(Q.hermitian(x + I), Q.antihermitian(x)) is None + assert ask(Q.hermitian(x + I), Q.complex(x)) is None + assert ask(Q.hermitian(x + I), Q.hermitian(x)) is False + assert ask(Q.hermitian(x + I), Q.imaginary(x)) is None + assert ask(Q.hermitian(x + I), Q.real(x)) is False + assert ask( + Q.hermitian(x + y), Q.antihermitian(x) & Q.antihermitian(y)) is None + assert ask(Q.hermitian(x + y), Q.antihermitian(x) & Q.complex(y)) is None + assert ask( + Q.hermitian(x + y), Q.antihermitian(x) & Q.hermitian(y)) is None + assert ask(Q.hermitian(x + y), Q.antihermitian(x) & Q.imaginary(y)) is None + assert ask(Q.hermitian(x + y), Q.antihermitian(x) & Q.real(y)) is None + assert ask(Q.hermitian(x + y), Q.hermitian(x) & Q.complex(y)) is None + assert ask(Q.hermitian(x + y), Q.hermitian(x) & Q.hermitian(y)) is True + assert ask(Q.hermitian(x + y), Q.hermitian(x) & Q.imaginary(y)) is False + assert ask(Q.hermitian(x + y), Q.hermitian(x) & Q.real(y)) is True + assert ask(Q.hermitian(x + y), Q.imaginary(x) & Q.complex(y)) is None + assert ask(Q.hermitian(x + y), Q.imaginary(x) & Q.imaginary(y)) is None + assert ask(Q.hermitian(x + y), Q.imaginary(x) & Q.real(y)) is False + assert ask(Q.hermitian(x + y), Q.real(x) & Q.complex(y)) is None + assert ask(Q.hermitian(x + y), Q.real(x) & Q.real(y)) is True + + assert ask(Q.hermitian(I*x), Q.antihermitian(x)) is True + assert ask(Q.hermitian(I*x), Q.complex(x)) is None + assert ask(Q.hermitian(I*x), Q.hermitian(x)) is False + assert ask(Q.hermitian(I*x), Q.imaginary(x)) is True + assert ask(Q.hermitian(I*x), Q.real(x)) is False + assert ask(Q.hermitian(x*y), Q.hermitian(x) & Q.real(y)) is True + + assert ask( + Q.hermitian(x + y + z), Q.real(x) & Q.real(y) & Q.real(z)) is True + assert ask(Q.hermitian(x + y + z), + Q.real(x) & Q.real(y) & Q.imaginary(z)) is False + assert ask(Q.hermitian(x + y + z), + Q.real(x) & Q.imaginary(y) & Q.imaginary(z)) is None + assert ask(Q.hermitian(x + y + z), + Q.imaginary(x) & Q.imaginary(y) & Q.imaginary(z)) is None + + assert ask(Q.antihermitian(x)) is None + assert ask(Q.antihermitian(x), Q.real(x)) is False + assert ask(Q.antihermitian(x), Q.prime(x)) is False + + assert ask(Q.antihermitian(x + 1), Q.antihermitian(x)) is False + assert ask(Q.antihermitian(x + 1), Q.complex(x)) is None + assert ask(Q.antihermitian(x + 1), Q.hermitian(x)) is None + assert ask(Q.antihermitian(x + 1), Q.imaginary(x)) is False + assert ask(Q.antihermitian(x + 1), Q.real(x)) is None + assert ask(Q.antihermitian(x + I), Q.antihermitian(x)) is True + assert ask(Q.antihermitian(x + I), Q.complex(x)) is None + assert ask(Q.antihermitian(x + I), Q.hermitian(x)) is None + assert ask(Q.antihermitian(x + I), Q.imaginary(x)) is True + assert ask(Q.antihermitian(x + I), Q.real(x)) is False + assert ask(Q.antihermitian(x), Q.zero(x)) is True + + assert ask( + Q.antihermitian(x + y), Q.antihermitian(x) & Q.antihermitian(y) + ) is True + assert ask( + Q.antihermitian(x + y), Q.antihermitian(x) & Q.complex(y)) is None + assert ask( + Q.antihermitian(x + y), Q.antihermitian(x) & Q.hermitian(y)) is None + assert ask( + Q.antihermitian(x + y), Q.antihermitian(x) & Q.imaginary(y)) is True + assert ask(Q.antihermitian(x + y), Q.antihermitian(x) & Q.real(y) + ) is False + assert ask(Q.antihermitian(x + y), Q.hermitian(x) & Q.complex(y)) is None + assert ask(Q.antihermitian(x + y), Q.hermitian(x) & Q.hermitian(y) + ) is None + assert ask( + Q.antihermitian(x + y), Q.hermitian(x) & Q.imaginary(y)) is None + assert ask(Q.antihermitian(x + y), Q.hermitian(x) & Q.real(y)) is None + assert ask(Q.antihermitian(x + y), Q.imaginary(x) & Q.complex(y)) is None + assert ask(Q.antihermitian(x + y), Q.imaginary(x) & Q.imaginary(y)) is True + assert ask(Q.antihermitian(x + y), Q.imaginary(x) & Q.real(y)) is False + assert ask(Q.antihermitian(x + y), Q.real(x) & Q.complex(y)) is None + assert ask(Q.antihermitian(x + y), Q.real(x) & Q.real(y)) is None + + assert ask(Q.antihermitian(I*x), Q.real(x)) is True + assert ask(Q.antihermitian(I*x), Q.antihermitian(x)) is False + assert ask(Q.antihermitian(I*x), Q.complex(x)) is None + assert ask(Q.antihermitian(x*y), Q.antihermitian(x) & Q.real(y)) is True + + assert ask(Q.antihermitian(x + y + z), + Q.real(x) & Q.real(y) & Q.real(z)) is None + assert ask(Q.antihermitian(x + y + z), + Q.real(x) & Q.real(y) & Q.imaginary(z)) is None + assert ask(Q.antihermitian(x + y + z), + Q.real(x) & Q.imaginary(y) & Q.imaginary(z)) is False + assert ask(Q.antihermitian(x + y + z), + Q.imaginary(x) & Q.imaginary(y) & Q.imaginary(z)) is True + + +@_both_exp_pow +def test_imaginary(): + assert ask(Q.imaginary(x)) is None + assert ask(Q.imaginary(x), Q.real(x)) is False + assert ask(Q.imaginary(x), Q.prime(x)) is False + + assert ask(Q.imaginary(x + 1), Q.real(x)) is False + assert ask(Q.imaginary(x + 1), Q.imaginary(x)) is False + assert ask(Q.imaginary(x + I), Q.real(x)) is False + assert ask(Q.imaginary(x + I), Q.imaginary(x)) is True + assert ask(Q.imaginary(x + y), Q.imaginary(x) & Q.imaginary(y)) is True + assert ask(Q.imaginary(x + y), Q.real(x) & Q.real(y)) is False + assert ask(Q.imaginary(x + y), Q.imaginary(x) & Q.real(y)) is False + assert ask(Q.imaginary(x + y), Q.complex(x) & Q.real(y)) is None + assert ask( + Q.imaginary(x + y + z), Q.real(x) & Q.real(y) & Q.real(z)) is False + assert ask(Q.imaginary(x + y + z), + Q.real(x) & Q.real(y) & Q.imaginary(z)) is None + assert ask(Q.imaginary(x + y + z), + Q.real(x) & Q.imaginary(y) & Q.imaginary(z)) is False + + assert ask(Q.imaginary(I*x), Q.real(x)) is True + assert ask(Q.imaginary(I*x), Q.imaginary(x)) is False + assert ask(Q.imaginary(I*x), Q.complex(x)) is None + assert ask(Q.imaginary(x*y), Q.imaginary(x) & Q.real(y)) is True + assert ask(Q.imaginary(x*y), Q.real(x) & Q.real(y)) is False + + assert ask(Q.imaginary(I**x), Q.negative(x)) is None + assert ask(Q.imaginary(I**x), Q.positive(x)) is None + assert ask(Q.imaginary(I**x), Q.even(x)) is False + assert ask(Q.imaginary(I**x), Q.odd(x)) is True + assert ask(Q.imaginary(I**x), Q.imaginary(x)) is False + assert ask(Q.imaginary((2*I)**x), Q.imaginary(x)) is False + assert ask(Q.imaginary(x**0), Q.imaginary(x)) is False + assert ask(Q.imaginary(x**y), Q.imaginary(x) & Q.imaginary(y)) is None + assert ask(Q.imaginary(x**y), Q.imaginary(x) & Q.real(y)) is None + assert ask(Q.imaginary(x**y), Q.real(x) & Q.imaginary(y)) is None + assert ask(Q.imaginary(x**y), Q.real(x) & Q.real(y)) is None + assert ask(Q.imaginary(x**y), Q.imaginary(x) & Q.integer(y)) is None + assert ask(Q.imaginary(x**y), Q.imaginary(y) & Q.integer(x)) is None + assert ask(Q.imaginary(x**y), Q.imaginary(x) & Q.odd(y)) is True + assert ask(Q.imaginary(x**y), Q.imaginary(x) & Q.rational(y)) is None + assert ask(Q.imaginary(x**y), Q.imaginary(x) & Q.even(y)) is False + + assert ask(Q.imaginary(x**y), Q.real(x) & Q.integer(y)) is False + assert ask(Q.imaginary(x**y), Q.positive(x) & Q.real(y)) is False + assert ask(Q.imaginary(x**y), Q.negative(x) & Q.real(y)) is None + assert ask(Q.imaginary(x**y), Q.negative(x) & Q.real(y) & ~Q.rational(y)) is False + assert ask(Q.imaginary(x**y), Q.integer(x) & Q.imaginary(y)) is None + assert ask(Q.imaginary(x**y), Q.negative(x) & Q.rational(y) & Q.integer(2*y)) is True + assert ask(Q.imaginary(x**y), Q.negative(x) & Q.rational(y) & ~Q.integer(2*y)) is False + assert ask(Q.imaginary(x**y), Q.negative(x) & Q.rational(y)) is None + assert ask(Q.imaginary(x**y), Q.real(x) & Q.rational(y) & ~Q.integer(2*y)) is False + assert ask(Q.imaginary(x**y), Q.real(x) & Q.rational(y) & Q.integer(2*y)) is None + + # logarithm + assert ask(Q.imaginary(log(I))) is True + assert ask(Q.imaginary(log(2*I))) is False + assert ask(Q.imaginary(log(I + 1))) is False + assert ask(Q.imaginary(log(x)), Q.complex(x)) is None + assert ask(Q.imaginary(log(x)), Q.imaginary(x)) is None + assert ask(Q.imaginary(log(x)), Q.positive(x)) is False + assert ask(Q.imaginary(log(exp(x))), Q.complex(x)) is None + assert ask(Q.imaginary(log(exp(x))), Q.imaginary(x)) is None # zoo/I/a+I*b + assert ask(Q.imaginary(log(exp(I)))) is True + + # exponential + assert ask(Q.imaginary(exp(x)**x), Q.imaginary(x)) is False + eq = Pow(exp(pi*I*x, evaluate=False), x, evaluate=False) + assert ask(Q.imaginary(eq), Q.even(x)) is False + eq = Pow(exp(pi*I*x/2, evaluate=False), x, evaluate=False) + assert ask(Q.imaginary(eq), Q.odd(x)) is True + assert ask(Q.imaginary(exp(3*I*pi*x)**x), Q.integer(x)) is False + assert ask(Q.imaginary(exp(2*pi*I, evaluate=False))) is False + assert ask(Q.imaginary(exp(pi*I/2, evaluate=False))) is True + + # issue 7886 + assert ask(Q.imaginary(Pow(x, Rational(1, 4))), Q.real(x) & Q.negative(x)) is False + + +def test_integer(): + assert ask(Q.integer(x)) is None + assert ask(Q.integer(x), Q.integer(x)) is True + assert ask(Q.integer(x), ~Q.integer(x)) is False + assert ask(Q.integer(x), ~Q.real(x)) is False + assert ask(Q.integer(x), ~Q.positive(x)) is None + assert ask(Q.integer(x), Q.even(x) | Q.odd(x)) is True + + assert ask(Q.integer(2*x), Q.integer(x)) is True + assert ask(Q.integer(2*x), Q.even(x)) is True + assert ask(Q.integer(2*x), Q.prime(x)) is True + assert ask(Q.integer(2*x), Q.rational(x)) is None + assert ask(Q.integer(2*x), Q.real(x)) is None + assert ask(Q.integer(sqrt(2)*x), Q.integer(x)) is False + assert ask(Q.integer(sqrt(2)*x), Q.irrational(x)) is None + + assert ask(Q.integer(x/2), Q.odd(x)) is False + assert ask(Q.integer(x/2), Q.even(x)) is True + assert ask(Q.integer(x/3), Q.odd(x)) is None + assert ask(Q.integer(x/3), Q.even(x)) is None + + # https://github.com/sympy/sympy/issues/7286 + assert ask(Q.integer(Abs(x)),Q.integer(x)) is True + assert ask(Q.integer(Abs(-x)),Q.integer(x)) is True + assert ask(Q.integer(Abs(x)), ~Q.integer(x)) is None + assert ask(Q.integer(Abs(x)),Q.complex(x)) is None + assert ask(Q.integer(Abs(x+I*y)),Q.real(x) & Q.real(y)) is None + + # https://github.com/sympy/sympy/issues/27739 + assert ask(Q.integer(x/y), Q.integer(x) & Q.integer(y)) is None + assert ask(Q.integer(1/x), Q.integer(x)) is None + assert ask(Q.integer(x**y), Q.integer(x) & Q.integer(y)) is None + assert ask(Q.integer(sqrt(5))) is False + assert ask(Q.integer(x**y), Q.nonzero(x) & Q.zero(y)) is True + assert ask(Q.integer(x**y), Q.integer(x) & Q.integer(y) & Q.positive(y)) is True + assert ask(Q.integer(-1**x), Q.integer(x)) is True + assert ask(Q.integer(x**y), Q.integer(x) & Q.integer(y) & Q.positive(y)) is True + assert ask(Q.integer(x**y), Q.zero(x) & Q.integer(y) & Q.positive(y)) is True + assert ask(Q.integer(pi**x), Q.zero(x)) is True + assert ask(Q.integer(x**y), Q.imaginary(x) & Q.zero(y)) is True + + +def test_negative(): + assert ask(Q.negative(x), Q.negative(x)) is True + assert ask(Q.negative(x), Q.positive(x)) is False + assert ask(Q.negative(x), ~Q.real(x)) is False + assert ask(Q.negative(x), Q.prime(x)) is False + assert ask(Q.negative(x), ~Q.prime(x)) is None + + assert ask(Q.negative(-x), Q.positive(x)) is True + assert ask(Q.negative(-x), ~Q.positive(x)) is None + assert ask(Q.negative(-x), Q.negative(x)) is False + assert ask(Q.negative(-x), Q.positive(x)) is True + + assert ask(Q.negative(x - 1), Q.negative(x)) is True + assert ask(Q.negative(x + y)) is None + assert ask(Q.negative(x + y), Q.negative(x)) is None + assert ask(Q.negative(x + y), Q.negative(x) & Q.negative(y)) is True + assert ask(Q.negative(x + y), Q.negative(x) & Q.nonpositive(y)) is True + assert ask(Q.negative(2 + I)) is False + # although this could be False, it is representative of expressions + # that don't evaluate to a zero with precision + assert ask(Q.negative(cos(I)**2 + sin(I)**2 - 1)) is None + assert ask(Q.negative(-I + I*(cos(2)**2 + sin(2)**2))) is None + + assert ask(Q.negative(x**2)) is None + assert ask(Q.negative(x**2), Q.real(x)) is False + assert ask(Q.negative(x**1.4), Q.real(x)) is None + + assert ask(Q.negative(x**I), Q.positive(x)) is None + + assert ask(Q.negative(x*y)) is None + assert ask(Q.negative(x*y), Q.positive(x) & Q.positive(y)) is False + assert ask(Q.negative(x*y), Q.positive(x) & Q.negative(y)) is True + assert ask(Q.negative(x*y), Q.complex(x) & Q.complex(y)) is None + + assert ask(Q.negative(x**y)) is None + assert ask(Q.negative(x**y), Q.negative(x) & Q.even(y)) is False + assert ask(Q.negative(x**y), Q.negative(x) & Q.odd(y)) is True + assert ask(Q.negative(x**y), Q.positive(x) & Q.integer(y)) is False + + assert ask(Q.negative(Abs(x))) is False + + +def test_nonzero(): + assert ask(Q.nonzero(x)) is None + assert ask(Q.nonzero(x), Q.real(x)) is None + assert ask(Q.nonzero(x), Q.positive(x)) is True + assert ask(Q.nonzero(x), Q.negative(x)) is True + assert ask(Q.nonzero(x), Q.negative(x) | Q.positive(x)) is True + + assert ask(Q.nonzero(x + y)) is None + assert ask(Q.nonzero(x + y), Q.positive(x) & Q.positive(y)) is True + assert ask(Q.nonzero(x + y), Q.positive(x) & Q.negative(y)) is None + assert ask(Q.nonzero(x + y), Q.negative(x) & Q.negative(y)) is True + + assert ask(Q.nonzero(2*x)) is None + assert ask(Q.nonzero(2*x), Q.positive(x)) is True + assert ask(Q.nonzero(2*x), Q.negative(x)) is True + assert ask(Q.nonzero(x*y), Q.nonzero(x)) is None + assert ask(Q.nonzero(x*y), Q.nonzero(x) & Q.nonzero(y)) is True + + assert ask(Q.nonzero(x**y), Q.nonzero(x)) is True + + assert ask(Q.nonzero(Abs(x))) is None + assert ask(Q.nonzero(Abs(x)), Q.nonzero(x)) is True + + assert ask(Q.nonzero(log(exp(2*I)))) is False + # although this could be False, it is representative of expressions + # that don't evaluate to a zero with precision + assert ask(Q.nonzero(cos(1)**2 + sin(1)**2 - 1)) is None + + +def test_zero(): + assert ask(Q.zero(x)) is None + assert ask(Q.zero(x), Q.real(x)) is None + assert ask(Q.zero(x), Q.positive(x)) is False + assert ask(Q.zero(x), Q.negative(x)) is False + assert ask(Q.zero(x), Q.negative(x) | Q.positive(x)) is False + + assert ask(Q.zero(x), Q.nonnegative(x) & Q.nonpositive(x)) is True + + assert ask(Q.zero(x + y)) is None + assert ask(Q.zero(x + y), Q.positive(x) & Q.positive(y)) is False + assert ask(Q.zero(x + y), Q.positive(x) & Q.negative(y)) is None + assert ask(Q.zero(x + y), Q.negative(x) & Q.negative(y)) is False + + assert ask(Q.zero(2*x)) is None + assert ask(Q.zero(2*x), Q.positive(x)) is False + assert ask(Q.zero(2*x), Q.negative(x)) is False + assert ask(Q.zero(x*y), Q.nonzero(x)) is None + + assert ask(Q.zero(Abs(x))) is None + assert ask(Q.zero(Abs(x)), Q.zero(x)) is True + + assert ask(Q.integer(x), Q.zero(x)) is True + assert ask(Q.even(x), Q.zero(x)) is True + assert ask(Q.odd(x), Q.zero(x)) is False + assert ask(Q.zero(x), Q.even(x)) is None + assert ask(Q.zero(x), Q.odd(x)) is False + assert ask(Q.zero(x) | Q.zero(y), Q.zero(x*y)) is True + + +def test_odd_query(): + assert ask(Q.odd(x)) is None + assert ask(Q.odd(x), Q.odd(x)) is True + assert ask(Q.odd(x), Q.integer(x)) is None + assert ask(Q.odd(x), ~Q.integer(x)) is False + assert ask(Q.odd(x), Q.rational(x)) is None + assert ask(Q.odd(x), Q.positive(x)) is None + + assert ask(Q.odd(-x), Q.odd(x)) is True + + assert ask(Q.odd(2*x)) is None + assert ask(Q.odd(2*x), Q.integer(x)) is False + assert ask(Q.odd(2*x), Q.odd(x)) is False + assert ask(Q.odd(2*x), Q.irrational(x)) is False + assert ask(Q.odd(2*x), ~Q.integer(x)) is None + assert ask(Q.odd(3*x), Q.integer(x)) is None + + assert ask(Q.odd(x/3), Q.odd(x)) is None + assert ask(Q.odd(x/3), Q.even(x)) is None + + assert ask(Q.odd(x + 1), Q.even(x)) is True + assert ask(Q.odd(x + 2), Q.even(x)) is False + assert ask(Q.odd(x + 2), Q.odd(x)) is True + assert ask(Q.odd(3 - x), Q.odd(x)) is False + assert ask(Q.odd(3 - x), Q.even(x)) is True + assert ask(Q.odd(3 + x), Q.odd(x)) is False + assert ask(Q.odd(3 + x), Q.even(x)) is True + assert ask(Q.odd(x + y), Q.odd(x) & Q.odd(y)) is False + assert ask(Q.odd(x + y), Q.odd(x) & Q.even(y)) is True + assert ask(Q.odd(x - y), Q.even(x) & Q.odd(y)) is True + assert ask(Q.odd(x - y), Q.odd(x) & Q.odd(y)) is False + + assert ask(Q.odd(x + y + z), Q.odd(x) & Q.odd(y) & Q.even(z)) is False + assert ask(Q.odd(x + y + z + t), + Q.odd(x) & Q.odd(y) & Q.even(z) & Q.integer(t)) is None + + assert ask(Q.odd(2*x + 1), Q.integer(x)) is True + assert ask(Q.odd(2*x + y), Q.integer(x) & Q.odd(y)) is True + assert ask(Q.odd(2*x + y), Q.integer(x) & Q.even(y)) is False + assert ask(Q.odd(2*x + y), Q.integer(x) & Q.integer(y)) is None + assert ask(Q.odd(x*y), Q.odd(x) & Q.even(y)) is False + assert ask(Q.odd(x*y), Q.odd(x) & Q.odd(y)) is True + assert ask(Q.odd(2*x*y), Q.rational(x) & Q.rational(x)) is None + assert ask(Q.odd(2*x*y), Q.irrational(x) & Q.irrational(x)) is None + + assert ask(Q.odd(Abs(x)), Q.odd(x)) is True + + assert ask(Q.odd((-1)**n), Q.integer(n)) is True + + assert ask(Q.odd(k**2), Q.even(k)) is False + assert ask(Q.odd(n**2), Q.odd(n)) is True + assert ask(Q.odd(3**k), Q.even(k)) is None + + assert ask(Q.odd(k**m), Q.even(k) & Q.integer(m) & ~Q.negative(m)) is None + assert ask(Q.odd(n**m), Q.odd(n) & Q.integer(m) & ~Q.negative(m)) is True + + assert ask(Q.odd(k**p), Q.even(k) & Q.integer(p) & Q.positive(p)) is False + assert ask(Q.odd(n**p), Q.odd(n) & Q.integer(p) & Q.positive(p)) is True + + assert ask(Q.odd(m**k), Q.even(k) & Q.integer(m) & ~Q.negative(m)) is None + assert ask(Q.odd(p**k), Q.even(k) & Q.integer(p) & Q.positive(p)) is None + + assert ask(Q.odd(m**n), Q.odd(n) & Q.integer(m) & ~Q.negative(m)) is None + assert ask(Q.odd(p**n), Q.odd(n) & Q.integer(p) & Q.positive(p)) is None + + assert ask(Q.odd(k**x), Q.even(k)) is None + assert ask(Q.odd(n**x), Q.odd(n)) is None + + assert ask(Q.odd(x*y), Q.integer(x) & Q.integer(y)) is None + assert ask(Q.odd(x*x), Q.integer(x)) is None + assert ask(Q.odd(x*(x + y)), Q.integer(x) & Q.odd(y)) is False + assert ask(Q.odd(x*(x + y)), Q.integer(x) & Q.even(y)) is None + + +@XFAIL +def test_oddness_in_ternary_integer_product_with_odd(): + # Tests that oddness inference is independent of term ordering. + # Term ordering at the point of testing depends on SymPy's symbol order, so + # we try to force a different order by modifying symbol names. + assert ask(Q.odd(x*y*(y + z)), Q.integer(x) & Q.integer(y) & Q.odd(z)) is False + assert ask(Q.odd(y*x*(x + z)), Q.integer(x) & Q.integer(y) & Q.odd(z)) is False + + +def test_oddness_in_ternary_integer_product_with_even(): + assert ask(Q.odd(x*y*(y + z)), Q.integer(x) & Q.integer(y) & Q.even(z)) is None + + +def test_prime(): + assert ask(Q.prime(x), Q.prime(x)) is True + assert ask(Q.prime(x), ~Q.prime(x)) is False + assert ask(Q.prime(x), Q.integer(x)) is None + assert ask(Q.prime(x), ~Q.integer(x)) is False + + assert ask(Q.prime(2*x), Q.integer(x)) is None + assert ask(Q.prime(x*y)) is None + assert ask(Q.prime(x*y), Q.prime(x)) is None + assert ask(Q.prime(x*y), Q.integer(x) & Q.integer(y)) is None + assert ask(Q.prime(4*x), Q.integer(x)) is False + assert ask(Q.prime(4*x)) is None + + assert ask(Q.prime(x**2), Q.integer(x)) is False + assert ask(Q.prime(x**2), Q.prime(x)) is False + + # https://github.com/sympy/sympy/issues/27446 + assert ask(Q.prime(4**x), Q.integer(x)) is False + assert ask(Q.prime(p**x), Q.prime(p) & Q.integer(x) & Q.ne(x, 1)) is False + assert ask(Q.prime(n**x), Q.integer(x) & Q.composite(n)) is False + assert ask(Q.prime(x**y), Q.integer(x) & Q.integer(y)) is None + assert ask(Q.prime(2**x), Q.integer(x)) is None + assert ask(Q.prime(p**x), Q.prime(p) & Q.integer(x)) is None + + # Ideally, these should return True since the base is prime and the exponent is one, + # but currently, they return None. + assert ask(Q.prime(x**y), Q.prime(x) & Q.eq(y,1)) is None + assert ask(Q.prime(x**y), Q.prime(x) & Q.integer(y) & Q.gt(y,0) & Q.lt(y,2)) is None + + assert ask(Q.prime(Pow(x,1, evaluate=False)), Q.prime(x)) is True + + +@_both_exp_pow +def test_positive(): + assert ask(Q.positive(cos(I) ** 2 + sin(I) ** 2 - 1)) is None + assert ask(Q.positive(x), Q.positive(x)) is True + assert ask(Q.positive(x), Q.negative(x)) is False + assert ask(Q.positive(x), Q.nonzero(x)) is None + + assert ask(Q.positive(-x), Q.positive(x)) is False + assert ask(Q.positive(-x), Q.negative(x)) is True + + assert ask(Q.positive(x + y), Q.positive(x) & Q.positive(y)) is True + assert ask(Q.positive(x + y), Q.positive(x) & Q.nonnegative(y)) is True + assert ask(Q.positive(x + y), Q.positive(x) & Q.negative(y)) is None + assert ask(Q.positive(x + y), Q.positive(x) & Q.imaginary(y)) is False + + assert ask(Q.positive(2*x), Q.positive(x)) is True + assumptions = Q.positive(x) & Q.negative(y) & Q.negative(z) & Q.positive(w) + assert ask(Q.positive(x*y*z)) is None + assert ask(Q.positive(x*y*z), assumptions) is True + assert ask(Q.positive(-x*y*z), assumptions) is False + + assert ask(Q.positive(x**I), Q.positive(x)) is None + + assert ask(Q.positive(x**2), Q.positive(x)) is True + assert ask(Q.positive(x**2), Q.negative(x)) is True + assert ask(Q.positive(x**3), Q.negative(x)) is False + assert ask(Q.positive(1/(1 + x**2)), Q.real(x)) is True + assert ask(Q.positive(2**I)) is False + assert ask(Q.positive(2 + I)) is False + # although this could be False, it is representative of expressions + # that don't evaluate to a zero with precision + assert ask(Q.positive(cos(I)**2 + sin(I)**2 - 1)) is None + assert ask(Q.positive(-I + I*(cos(2)**2 + sin(2)**2))) is None + + #exponential + assert ask(Q.positive(exp(x)), Q.real(x)) is True + assert ask(~Q.negative(exp(x)), Q.real(x)) is True + assert ask(Q.positive(x + exp(x)), Q.real(x)) is None + assert ask(Q.positive(exp(x)), Q.imaginary(x)) is None + assert ask(Q.positive(exp(2*pi*I, evaluate=False)), Q.imaginary(x)) is True + assert ask(Q.negative(exp(pi*I, evaluate=False)), Q.imaginary(x)) is True + assert ask(Q.positive(exp(x*pi*I)), Q.even(x)) is True + assert ask(Q.positive(exp(x*pi*I)), Q.odd(x)) is False + assert ask(Q.positive(exp(x*pi*I)), Q.real(x)) is None + + # logarithm + assert ask(Q.positive(log(x)), Q.imaginary(x)) is False + assert ask(Q.positive(log(x)), Q.negative(x)) is False + assert ask(Q.positive(log(x)), Q.positive(x)) is None + assert ask(Q.positive(log(x + 2)), Q.positive(x)) is True + + # factorial + assert ask(Q.positive(factorial(x)), Q.integer(x) & Q.positive(x)) + assert ask(Q.positive(factorial(x)), Q.integer(x)) is None + + #absolute value + assert ask(Q.positive(Abs(x))) is None # Abs(0) = 0 + assert ask(Q.positive(Abs(x)), Q.positive(x)) is True + + +def test_nonpositive(): + assert ask(Q.nonpositive(-1)) + assert ask(Q.nonpositive(0)) + assert ask(Q.nonpositive(1)) is False + assert ask(~Q.positive(x), Q.nonpositive(x)) + assert ask(Q.nonpositive(x), Q.positive(x)) is False + assert ask(Q.nonpositive(sqrt(-1))) is False + assert ask(Q.nonpositive(x), Q.imaginary(x)) is False + + +def test_nonnegative(): + assert ask(Q.nonnegative(-1)) is False + assert ask(Q.nonnegative(0)) + assert ask(Q.nonnegative(1)) + assert ask(~Q.negative(x), Q.nonnegative(x)) + assert ask(Q.nonnegative(x), Q.negative(x)) is False + assert ask(Q.nonnegative(sqrt(-1))) is False + assert ask(Q.nonnegative(x), Q.imaginary(x)) is False + +def test_real_basic(): + assert ask(Q.real(x)) is None + assert ask(Q.real(x), Q.real(x)) is True + assert ask(Q.real(x), Q.nonzero(x)) is True + assert ask(Q.real(x), Q.positive(x)) is True + assert ask(Q.real(x), Q.negative(x)) is True + assert ask(Q.real(x), Q.integer(x)) is True + assert ask(Q.real(x), Q.even(x)) is True + assert ask(Q.real(x), Q.prime(x)) is True + + assert ask(Q.real(x/sqrt(2)), Q.real(x)) is True + assert ask(Q.real(x/sqrt(-2)), Q.real(x)) is False + + assert ask(Q.real(x + 1), Q.real(x)) is True + assert ask(Q.real(x + I), Q.real(x)) is False + assert ask(Q.real(x + I), Q.complex(x)) is None + + assert ask(Q.real(2*x), Q.real(x)) is True + assert ask(Q.real(I*x), Q.real(x)) is False + assert ask(Q.real(I*x), Q.imaginary(x)) is True + assert ask(Q.real(I*x), Q.complex(x)) is None + + +def test_real_pow(): + assert ask(Q.real(x**2), Q.real(x)) is True + assert ask(Q.real(sqrt(x)), Q.negative(x)) is False + assert ask(Q.real(x**y), Q.real(x) & Q.integer(y)) is None + assert ask(Q.real(x**y), Q.real(x) & Q.real(y)) is None + assert ask(Q.real(x**y), Q.positive(x) & Q.real(y)) is True + assert ask(Q.real(x**y), Q.imaginary(x) & Q.imaginary(y)) is None # I**I or (2*I)**I + assert ask(Q.real(x**y), Q.imaginary(x) & Q.real(y)) is None # I**1 or I**0 + assert ask(Q.real(x**y), Q.real(x) & Q.imaginary(y)) is None # could be exp(2*pi*I) or 2**I + assert ask(Q.real(x**0), Q.imaginary(x)) is True + assert ask(Q.real(x**y), Q.positive(x) & Q.real(y)) is True + assert ask(Q.real(x**y), Q.real(x) & Q.rational(y)) is None + assert ask(Q.real(x**y), Q.imaginary(x) & Q.integer(y)) is None + assert ask(Q.real(x**y), Q.imaginary(x) & Q.odd(y)) is False + assert ask(Q.real(x**y), Q.imaginary(x) & Q.even(y)) is True + assert ask(Q.real(x**(y/z)), Q.real(x) & Q.real(y/z) & Q.rational(y/z) & Q.even(z) & Q.positive(x)) is True + assert ask(Q.real(x**(y/z)), Q.real(x) & Q.rational(y/z) & Q.even(z) & Q.negative(x)) is None + assert ask(Q.real(x**(y/z)), Q.real(x) & Q.integer(y/z)) is None + assert ask(Q.real(x**(y/z)), Q.real(x) & Q.real(y/z) & Q.positive(x)) is True + assert ask(Q.real(x**(y/z)), Q.real(x) & Q.real(y/z) & Q.negative(x)) is None + assert ask(Q.real((-I)**i), Q.imaginary(i)) is True + assert ask(Q.real(I**i), Q.imaginary(i)) is True + assert ask(Q.real(i**i), Q.imaginary(i)) is None # i might be 2*I + assert ask(Q.real(x**i), Q.imaginary(i)) is None # x could be 0 + assert ask(Q.real(x**(I*pi/log(x))), Q.real(x)) is True + + # https://github.com/sympy/sympy/issues/27485 + assert ask(Q.real(n**p), Q.negative(n) & Q.positive(p)) is None + + # https://github.com/sympy/sympy/issues/16530 + assert ask(Q.real(1/Abs(x))) is None + assert ask(Q.real(x**y), Q.zero(x) & Q.real(y)) is None + assert ask(Q.real(x**y), Q.zero(x) & Q.positive(y)) is True + + +@_both_exp_pow +def test_real_functions(): + # trigonometric functions + assert ask(Q.real(sin(x))) is None + assert ask(Q.real(cos(x))) is None + assert ask(Q.real(sin(x)), Q.real(x)) is True + assert ask(Q.real(cos(x)), Q.real(x)) is True + + # exponential function + assert ask(Q.real(exp(x))) is None + assert ask(Q.real(exp(x)), Q.real(x)) is True + assert ask(Q.real(x + exp(x)), Q.real(x)) is True + assert ask(Q.real(exp(2*pi*I, evaluate=False))) is True + assert ask(Q.real(exp(pi*I, evaluate=False))) is True + assert ask(Q.real(exp(pi*I/2, evaluate=False))) is False + + # logarithm + assert ask(Q.real(log(I))) is False + assert ask(Q.real(log(2*I))) is False + assert ask(Q.real(log(I + 1))) is False + assert ask(Q.real(log(x)), Q.complex(x)) is None + assert ask(Q.real(log(x)), Q.imaginary(x)) is False + assert ask(Q.real(log(exp(x))), Q.imaginary(x)) is None # exp(2*pi*I) is 1, log(exp(pi*I)) is pi*I (disregarding periodicity) + assert ask(Q.real(log(exp(x))), Q.complex(x)) is None + eq = Pow(exp(2*pi*I*x, evaluate=False), x, evaluate=False) + assert ask(Q.real(eq), Q.integer(x)) is True + assert ask(Q.real(exp(x)**x), Q.imaginary(x)) is True + assert ask(Q.real(exp(x)**x), Q.complex(x)) is None + + # Q.complexes + assert ask(Q.real(re(x))) is True + assert ask(Q.real(im(x))) is True + + +def test_matrix(): + + # hermitian + assert ask(Q.hermitian(Matrix([[2, 2 + I, 4], [2 - I, 3, I], [4, -I, 1]]))) == True + assert ask(Q.hermitian(Matrix([[2, 2 + I, 4], [2 + I, 3, I], [4, -I, 1]]))) == False + z = symbols('z', complex=True) + assert ask(Q.hermitian(Matrix([[2, 2 + I, z], [2 - I, 3, I], [4, -I, 1]]))) == None + assert ask(Q.hermitian(SparseMatrix(((25, 15, -5), (15, 18, 0), (-5, 0, 11))))) == True + assert ask(Q.hermitian(SparseMatrix(((25, 15, -5), (15, I, 0), (-5, 0, 11))))) == False + assert ask(Q.hermitian(SparseMatrix(((25, 15, -5), (15, z, 0), (-5, 0, 11))))) == None + + # antihermitian + A = Matrix([[0, -2 - I, 0], [2 - I, 0, -I], [0, -I, 0]]) + B = Matrix([[-I, 2 + I, 0], [-2 + I, 0, 2 + I], [0, -2 + I, -I]]) + assert ask(Q.antihermitian(A)) is True + assert ask(Q.antihermitian(B)) is True + assert ask(Q.antihermitian(A**2)) is False + C = (B**3) + C.simplify() + assert ask(Q.antihermitian(C)) is True + _A = Matrix([[0, -2 - I, 0], [z, 0, -I], [0, -I, 0]]) + assert ask(Q.antihermitian(_A)) is None + + +@_both_exp_pow +def test_algebraic(): + assert ask(Q.algebraic(x)) is None + + assert ask(Q.algebraic(I)) is True + assert ask(Q.algebraic(2*I)) is True + assert ask(Q.algebraic(I/3)) is True + + assert ask(Q.algebraic(sqrt(7))) is True + assert ask(Q.algebraic(2*sqrt(7))) is True + assert ask(Q.algebraic(sqrt(7)/3)) is True + + assert ask(Q.algebraic(I*sqrt(3))) is True + assert ask(Q.algebraic(sqrt(1 + I*sqrt(3)))) is True + + assert ask(Q.algebraic(1 + I*sqrt(3)**Rational(17, 31))) is True + assert ask(Q.algebraic(1 + I*sqrt(3)**(17/pi))) is None + + for f in [exp, sin, tan, asin, atan, cos]: + assert ask(Q.algebraic(f(7))) is False + assert ask(Q.algebraic(f(7, evaluate=False))) is False + assert ask(Q.algebraic(f(0, evaluate=False))) is True + assert ask(Q.algebraic(f(x)), Q.algebraic(x)) is None + assert ask(Q.algebraic(f(x)), Q.algebraic(x) & Q.nonzero(x)) is False + + for g in [log, acos]: + assert ask(Q.algebraic(g(7))) is False + assert ask(Q.algebraic(g(7, evaluate=False))) is False + assert ask(Q.algebraic(g(1, evaluate=False))) is True + assert ask(Q.algebraic(g(x)), Q.algebraic(x)) is None + assert ask(Q.algebraic(g(x)), Q.algebraic(x) & Q.nonzero(x - 1)) is False + + for h in [cot, acot]: + assert ask(Q.algebraic(h(7))) is False + assert ask(Q.algebraic(h(7, evaluate=False))) is False + assert ask(Q.algebraic(h(x)), Q.algebraic(x)) is False + + assert ask(Q.algebraic(sqrt(sin(7)))) is None + assert ask(Q.algebraic(sqrt(y + I*sqrt(7)))) is None + + assert ask(Q.algebraic(2.47)) is True + + assert ask(Q.algebraic(x), Q.transcendental(x)) is False + assert ask(Q.transcendental(x), Q.algebraic(x)) is False + + #https://github.com/sympy/sympy/issues/27445 + assert ask(Q.algebraic(Pow(1, x, evaluate=False)), Q.algebraic(x)) is None + assert ask(Q.algebraic(Pow(x, y))) is None + assert ask(Q.algebraic(Pow(1, x, evaluate=False))) is None + assert ask(Q.algebraic(x**(pi*I))) is None + assert ask(Q.algebraic(pi**n),Q.integer(n) & Q.positive(n)) is False + assert ask(Q.algebraic(x**y),Q.algebraic(x) & Q.rational(y)) is True + + +def test_global(): + """Test ask with global assumptions""" + assert ask(Q.integer(x)) is None + global_assumptions.add(Q.integer(x)) + assert ask(Q.integer(x)) is True + global_assumptions.clear() + assert ask(Q.integer(x)) is None + + +def test_custom_context(): + """Test ask with custom assumptions context""" + assert ask(Q.integer(x)) is None + local_context = AssumptionsContext() + local_context.add(Q.integer(x)) + assert ask(Q.integer(x), context=local_context) is True + assert ask(Q.integer(x)) is None + + +def test_functions_in_assumptions(): + assert ask(Q.negative(x), Q.real(x) >> Q.positive(x)) is False + assert ask(Q.negative(x), Equivalent(Q.real(x), Q.positive(x))) is False + assert ask(Q.negative(x), Xor(Q.real(x), Q.negative(x))) is False + + +def test_composite_ask(): + assert ask(Q.negative(x) & Q.integer(x), + assumptions=Q.real(x) >> Q.positive(x)) is False + + +def test_composite_proposition(): + assert ask(True) is True + assert ask(False) is False + assert ask(~Q.negative(x), Q.positive(x)) is True + assert ask(~Q.real(x), Q.commutative(x)) is None + assert ask(Q.negative(x) & Q.integer(x), Q.positive(x)) is False + assert ask(Q.negative(x) & Q.integer(x)) is None + assert ask(Q.real(x) | Q.integer(x), Q.positive(x)) is True + assert ask(Q.real(x) | Q.integer(x)) is None + assert ask(Q.real(x) >> Q.positive(x), Q.negative(x)) is False + assert ask(Implies( + Q.real(x), Q.positive(x), evaluate=False), Q.negative(x)) is False + assert ask(Implies(Q.real(x), Q.positive(x), evaluate=False)) is None + assert ask(Equivalent(Q.integer(x), Q.even(x)), Q.even(x)) is True + assert ask(Equivalent(Q.integer(x), Q.even(x))) is None + assert ask(Equivalent(Q.positive(x), Q.integer(x)), Q.integer(x)) is None + assert ask(Q.real(x) | Q.integer(x), Q.real(x) | Q.integer(x)) is True + +def test_tautology(): + assert ask(Q.real(x) | ~Q.real(x)) is True + assert ask(Q.real(x) & ~Q.real(x)) is False + +def test_composite_assumptions(): + assert ask(Q.real(x), Q.real(x) & Q.real(y)) is True + assert ask(Q.positive(x), Q.positive(x) | Q.positive(y)) is None + assert ask(Q.positive(x), Q.real(x) >> Q.positive(y)) is None + assert ask(Q.real(x), ~(Q.real(x) >> Q.real(y))) is True + +def test_key_extensibility(): + """test that you can add keys to the ask system at runtime""" + # make sure the key is not defined + raises(AttributeError, lambda: ask(Q.my_key(x))) + + # Old handler system + class MyAskHandler(AskHandler): + @staticmethod + def Symbol(expr, assumptions): + return True + try: + with warns_deprecated_sympy(): + register_handler('my_key', MyAskHandler) + with warns_deprecated_sympy(): + assert ask(Q.my_key(x)) is True + with warns_deprecated_sympy(): + assert ask(Q.my_key(x + 1)) is None + finally: + # We have to disable the stacklevel testing here because this raises + # the warning twice from two different places + with warns_deprecated_sympy(): + remove_handler('my_key', MyAskHandler) + del Q.my_key + raises(AttributeError, lambda: ask(Q.my_key(x))) + + # New handler system + class MyPredicate(Predicate): + pass + try: + Q.my_key = MyPredicate() + @Q.my_key.register(Symbol) + def _(expr, assumptions): + return True + assert ask(Q.my_key(x)) is True + assert ask(Q.my_key(x+1)) is None + finally: + del Q.my_key + raises(AttributeError, lambda: ask(Q.my_key(x))) + + +def test_type_extensibility(): + """test that new types can be added to the ask system at runtime + """ + from sympy.core import Basic + + class MyType(Basic): + pass + + @Q.prime.register(MyType) + def _(expr, assumptions): + return True + + assert ask(Q.prime(MyType())) is True + + +def test_single_fact_lookup(): + known_facts = And(Implies(Q.integer, Q.rational), + Implies(Q.rational, Q.real), + Implies(Q.real, Q.complex)) + known_facts_keys = {Q.integer, Q.rational, Q.real, Q.complex} + + known_facts_cnf = to_cnf(known_facts) + mapping = single_fact_lookup(known_facts_keys, known_facts_cnf) + + assert mapping[Q.rational] == {Q.real, Q.rational, Q.complex} + + +def test_generate_known_facts_dict(): + known_facts = And(Implies(Q.integer(x), Q.rational(x)), + Implies(Q.rational(x), Q.real(x)), + Implies(Q.real(x), Q.complex(x))) + known_facts_keys = {Q.integer(x), Q.rational(x), Q.real(x), Q.complex(x)} + + assert generate_known_facts_dict(known_facts_keys, known_facts) == \ + {Q.complex: ({Q.complex}, set()), + Q.integer: ({Q.complex, Q.integer, Q.rational, Q.real}, set()), + Q.rational: ({Q.complex, Q.rational, Q.real}, set()), + Q.real: ({Q.complex, Q.real}, set())} + + +@slow +def test_known_facts_consistent(): + """"Test that ask_generated.py is up-to-date""" + x = Symbol('x') + fact = get_known_facts(x) + # test cnf clauses of fact between unary predicates + cnf = CNF.to_CNF(fact) + clauses = set() + clauses.update(frozenset(Literal(lit.arg.function, lit.is_Not) for lit in sorted(cl, key=str)) for cl in cnf.clauses) + assert get_all_known_facts() == clauses + # test dictionary of fact between unary predicates + keys = [pred(x) for pred in get_known_facts_keys()] + mapping = generate_known_facts_dict(keys, fact) + assert get_known_facts_dict() == mapping + + +def test_Add_queries(): + assert ask(Q.prime(12345678901234567890 + (cos(1)**2 + sin(1)**2))) is True + assert ask(Q.even(Add(S(2), S(2), evaluate=False))) is True + assert ask(Q.prime(Add(S(2), S(2), evaluate=False))) is False + assert ask(Q.integer(Add(S(2), S(2), evaluate=False))) is True + + +def test_positive_assuming(): + with assuming(Q.positive(x + 1)): + assert not ask(Q.positive(x)) + + +def test_issue_5421(): + raises(TypeError, lambda: ask(pi/log(x), Q.real)) + + +def test_issue_3906(): + raises(TypeError, lambda: ask(Q.positive)) + + +def test_issue_5833(): + assert ask(Q.positive(log(x)**2), Q.positive(x)) is None + assert ask(~Q.negative(log(x)**2), Q.positive(x)) is True + + +def test_issue_6732(): + raises(ValueError, lambda: ask(Q.positive(x), Q.positive(x) & Q.negative(x))) + raises(ValueError, lambda: ask(Q.negative(x), Q.positive(x) & Q.negative(x))) + + +def test_issue_7246(): + assert ask(Q.positive(atan(p)), Q.positive(p)) is True + assert ask(Q.positive(atan(p)), Q.negative(p)) is False + assert ask(Q.positive(atan(p)), Q.zero(p)) is False + assert ask(Q.positive(atan(x))) is None + + assert ask(Q.positive(asin(p)), Q.positive(p)) is None + assert ask(Q.positive(asin(p)), Q.zero(p)) is None + assert ask(Q.positive(asin(Rational(1, 7)))) is True + assert ask(Q.positive(asin(x)), Q.positive(x) & Q.nonpositive(x - 1)) is True + assert ask(Q.positive(asin(x)), Q.negative(x) & Q.nonnegative(x + 1)) is False + + assert ask(Q.positive(acos(p)), Q.positive(p)) is None + assert ask(Q.positive(acos(Rational(1, 7)))) is True + assert ask(Q.positive(acos(x)), Q.nonnegative(x + 1) & Q.nonpositive(x - 1)) is True + assert ask(Q.positive(acos(x)), Q.nonnegative(x - 1)) is None + + assert ask(Q.positive(acot(x)), Q.positive(x)) is True + assert ask(Q.positive(acot(x)), Q.real(x)) is True + assert ask(Q.positive(acot(x)), Q.imaginary(x)) is False + assert ask(Q.positive(acot(x))) is None + + +@XFAIL +def test_issue_7246_failing(): + #Move this test to test_issue_7246 once + #the new assumptions module is improved. + assert ask(Q.positive(acos(x)), Q.zero(x)) is True + + +def test_check_old_assumption(): + x = symbols('x', real=True) + assert ask(Q.real(x)) is True + assert ask(Q.imaginary(x)) is False + assert ask(Q.complex(x)) is True + + x = symbols('x', imaginary=True) + assert ask(Q.real(x)) is False + assert ask(Q.imaginary(x)) is True + assert ask(Q.complex(x)) is True + + x = symbols('x', complex=True) + assert ask(Q.real(x)) is None + assert ask(Q.complex(x)) is True + + x = symbols('x', positive=True) + assert ask(Q.positive(x)) is True + assert ask(Q.negative(x)) is False + assert ask(Q.real(x)) is True + + x = symbols('x', commutative=False) + assert ask(Q.commutative(x)) is False + + x = symbols('x', negative=True) + assert ask(Q.positive(x)) is False + assert ask(Q.negative(x)) is True + + x = symbols('x', nonnegative=True) + assert ask(Q.negative(x)) is False + assert ask(Q.positive(x)) is None + assert ask(Q.zero(x)) is None + + x = symbols('x', finite=True) + assert ask(Q.finite(x)) is True + + x = symbols('x', prime=True) + assert ask(Q.prime(x)) is True + assert ask(Q.composite(x)) is False + + x = symbols('x', composite=True) + assert ask(Q.prime(x)) is False + assert ask(Q.composite(x)) is True + + x = symbols('x', even=True) + assert ask(Q.even(x)) is True + assert ask(Q.odd(x)) is False + + x = symbols('x', odd=True) + assert ask(Q.even(x)) is False + assert ask(Q.odd(x)) is True + + x = symbols('x', nonzero=True) + assert ask(Q.nonzero(x)) is True + assert ask(Q.zero(x)) is False + + x = symbols('x', zero=True) + assert ask(Q.zero(x)) is True + + x = symbols('x', integer=True) + assert ask(Q.integer(x)) is True + + x = symbols('x', rational=True) + assert ask(Q.rational(x)) is True + assert ask(Q.irrational(x)) is False + + x = symbols('x', irrational=True) + assert ask(Q.irrational(x)) is True + assert ask(Q.rational(x)) is False + + +def test_issue_9636(): + assert ask(Q.integer(1.0)) is None + assert ask(Q.prime(3.0)) is None + assert ask(Q.composite(4.0)) is None + assert ask(Q.even(2.0)) is None + assert ask(Q.odd(3.0)) is None + + +def test_autosimp_used_to_fail(): + # See issue #9807 + assert ask(Q.imaginary(0**I)) is None + assert ask(Q.imaginary(0**(-I))) is None + assert ask(Q.real(0**I)) is None + assert ask(Q.real(0**(-I))) is None + + +def test_custom_AskHandler(): + from sympy.logic.boolalg import conjuncts + + # Old handler system + class MersenneHandler(AskHandler): + @staticmethod + def Integer(expr, assumptions): + if ask(Q.integer(log(expr + 1, 2))): + return True + @staticmethod + def Symbol(expr, assumptions): + if expr in conjuncts(assumptions): + return True + try: + with warns_deprecated_sympy(): + register_handler('mersenne', MersenneHandler) + n = Symbol('n', integer=True) + with warns_deprecated_sympy(): + assert ask(Q.mersenne(7)) + with warns_deprecated_sympy(): + assert ask(Q.mersenne(n), Q.mersenne(n)) + finally: + del Q.mersenne + + # New handler system + class MersennePredicate(Predicate): + pass + try: + Q.mersenne = MersennePredicate() + @Q.mersenne.register(Integer) + def _(expr, assumptions): + if ask(Q.integer(log(expr + 1, 2))): + return True + @Q.mersenne.register(Symbol) + def _(expr, assumptions): + if expr in conjuncts(assumptions): + return True + assert ask(Q.mersenne(7)) + assert ask(Q.mersenne(n), Q.mersenne(n)) + finally: + del Q.mersenne + + +def test_polyadic_predicate(): + + class SexyPredicate(Predicate): + pass + try: + Q.sexyprime = SexyPredicate() + + @Q.sexyprime.register(Integer, Integer) + def _(int1, int2, assumptions): + args = sorted([int1, int2]) + if not all(ask(Q.prime(a), assumptions) for a in args): + return False + return args[1] - args[0] == 6 + + @Q.sexyprime.register(Integer, Integer, Integer) + def _(int1, int2, int3, assumptions): + args = sorted([int1, int2, int3]) + if not all(ask(Q.prime(a), assumptions) for a in args): + return False + return args[2] - args[1] == 6 and args[1] - args[0] == 6 + + assert ask(Q.sexyprime(5, 11)) + assert ask(Q.sexyprime(7, 13, 19)) + finally: + del Q.sexyprime + + +def test_Predicate_handler_is_unique(): + + # Undefined predicate does not have a handler + assert Predicate('mypredicate').handler is None + + # Handler of defined predicate is unique to the class + class MyPredicate(Predicate): + pass + mp1 = MyPredicate(Str('mp1')) + mp2 = MyPredicate(Str('mp2')) + assert mp1.handler is mp2.handler + + +def test_relational(): + assert ask(Q.eq(x, 0), Q.zero(x)) + assert not ask(Q.eq(x, 0), Q.nonzero(x)) + assert not ask(Q.ne(x, 0), Q.zero(x)) + assert ask(Q.ne(x, 0), Q.nonzero(x)) + + +def test_issue_25221(): + assert ask(Q.transcendental(x), Q.algebraic(x) | Q.positive(y,y)) is None + assert ask(Q.transcendental(x), Q.algebraic(x) | (0 > y)) is None + assert ask(Q.transcendental(x), Q.algebraic(x) | Q.gt(0,y)) is None + + +def test_issue_27440(): + nan = S.NaN + assert ask(Q.negative(nan)) is None diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_refine.py b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_refine.py new file mode 100644 index 0000000000000000000000000000000000000000..81533a88b232cd5c3cfb9be17d09dad404d679dc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_refine.py @@ -0,0 +1,227 @@ +from sympy.assumptions.ask import Q +from sympy.assumptions.refine import refine +from sympy.core.expr import Expr +from sympy.core.numbers import (I, Rational, nan, pi) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.complexes import (Abs, arg, im, re, sign) +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (atan, atan2) +from sympy.abc import w, x, y, z +from sympy.core.relational import Eq, Ne +from sympy.functions.elementary.piecewise import Piecewise +from sympy.matrices.expressions.matexpr import MatrixSymbol + + +def test_Abs(): + assert refine(Abs(x), Q.positive(x)) == x + assert refine(1 + Abs(x), Q.positive(x)) == 1 + x + assert refine(Abs(x), Q.negative(x)) == -x + assert refine(1 + Abs(x), Q.negative(x)) == 1 - x + + assert refine(Abs(x**2)) != x**2 + assert refine(Abs(x**2), Q.real(x)) == x**2 + + +def test_pow1(): + assert refine((-1)**x, Q.even(x)) == 1 + assert refine((-1)**x, Q.odd(x)) == -1 + assert refine((-2)**x, Q.even(x)) == 2**x + + # nested powers + assert refine(sqrt(x**2)) != Abs(x) + assert refine(sqrt(x**2), Q.complex(x)) != Abs(x) + assert refine(sqrt(x**2), Q.real(x)) == Abs(x) + assert refine(sqrt(x**2), Q.positive(x)) == x + assert refine((x**3)**Rational(1, 3)) != x + + assert refine((x**3)**Rational(1, 3), Q.real(x)) != x + assert refine((x**3)**Rational(1, 3), Q.positive(x)) == x + + assert refine(sqrt(1/x), Q.real(x)) != 1/sqrt(x) + assert refine(sqrt(1/x), Q.positive(x)) == 1/sqrt(x) + + # powers of (-1) + assert refine((-1)**(x + y), Q.even(x)) == (-1)**y + assert refine((-1)**(x + y + z), Q.odd(x) & Q.odd(z)) == (-1)**y + assert refine((-1)**(x + y + 1), Q.odd(x)) == (-1)**y + assert refine((-1)**(x + y + 2), Q.odd(x)) == (-1)**(y + 1) + assert refine((-1)**(x + 3)) == (-1)**(x + 1) + + # continuation + assert refine((-1)**((-1)**x/2 - S.Half), Q.integer(x)) == (-1)**x + assert refine((-1)**((-1)**x/2 + S.Half), Q.integer(x)) == (-1)**(x + 1) + assert refine((-1)**((-1)**x/2 + 5*S.Half), Q.integer(x)) == (-1)**(x + 1) + + +def test_pow2(): + assert refine((-1)**((-1)**x/2 - 7*S.Half), Q.integer(x)) == (-1)**(x + 1) + assert refine((-1)**((-1)**x/2 - 9*S.Half), Q.integer(x)) == (-1)**x + + # powers of Abs + assert refine(Abs(x)**2, Q.real(x)) == x**2 + assert refine(Abs(x)**3, Q.real(x)) == Abs(x)**3 + assert refine(Abs(x)**2) == Abs(x)**2 + + +def test_exp(): + x = Symbol('x', integer=True) + assert refine(exp(pi*I*2*x)) == 1 + assert refine(exp(pi*I*2*(x + S.Half))) == -1 + assert refine(exp(pi*I*2*(x + Rational(1, 4)))) == I + assert refine(exp(pi*I*2*(x + Rational(3, 4)))) == -I + + +def test_Piecewise(): + assert refine(Piecewise((1, x < 0), (3, True)), (x < 0)) == 1 + assert refine(Piecewise((1, x < 0), (3, True)), ~(x < 0)) == 3 + assert refine(Piecewise((1, x < 0), (3, True)), (y < 0)) == \ + Piecewise((1, x < 0), (3, True)) + assert refine(Piecewise((1, x > 0), (3, True)), (x > 0)) == 1 + assert refine(Piecewise((1, x > 0), (3, True)), ~(x > 0)) == 3 + assert refine(Piecewise((1, x > 0), (3, True)), (y > 0)) == \ + Piecewise((1, x > 0), (3, True)) + assert refine(Piecewise((1, x <= 0), (3, True)), (x <= 0)) == 1 + assert refine(Piecewise((1, x <= 0), (3, True)), ~(x <= 0)) == 3 + assert refine(Piecewise((1, x <= 0), (3, True)), (y <= 0)) == \ + Piecewise((1, x <= 0), (3, True)) + assert refine(Piecewise((1, x >= 0), (3, True)), (x >= 0)) == 1 + assert refine(Piecewise((1, x >= 0), (3, True)), ~(x >= 0)) == 3 + assert refine(Piecewise((1, x >= 0), (3, True)), (y >= 0)) == \ + Piecewise((1, x >= 0), (3, True)) + assert refine(Piecewise((1, Eq(x, 0)), (3, True)), (Eq(x, 0)))\ + == 1 + assert refine(Piecewise((1, Eq(x, 0)), (3, True)), (Eq(0, x)))\ + == 1 + assert refine(Piecewise((1, Eq(x, 0)), (3, True)), ~(Eq(x, 0)))\ + == 3 + assert refine(Piecewise((1, Eq(x, 0)), (3, True)), ~(Eq(0, x)))\ + == 3 + assert refine(Piecewise((1, Eq(x, 0)), (3, True)), (Eq(y, 0)))\ + == Piecewise((1, Eq(x, 0)), (3, True)) + assert refine(Piecewise((1, Ne(x, 0)), (3, True)), (Ne(x, 0)))\ + == 1 + assert refine(Piecewise((1, Ne(x, 0)), (3, True)), ~(Ne(x, 0)))\ + == 3 + assert refine(Piecewise((1, Ne(x, 0)), (3, True)), (Ne(y, 0)))\ + == Piecewise((1, Ne(x, 0)), (3, True)) + + +def test_atan2(): + assert refine(atan2(y, x), Q.real(y) & Q.positive(x)) == atan(y/x) + assert refine(atan2(y, x), Q.negative(y) & Q.positive(x)) == atan(y/x) + assert refine(atan2(y, x), Q.negative(y) & Q.negative(x)) == atan(y/x) - pi + assert refine(atan2(y, x), Q.positive(y) & Q.negative(x)) == atan(y/x) + pi + assert refine(atan2(y, x), Q.zero(y) & Q.negative(x)) == pi + assert refine(atan2(y, x), Q.positive(y) & Q.zero(x)) == pi/2 + assert refine(atan2(y, x), Q.negative(y) & Q.zero(x)) == -pi/2 + assert refine(atan2(y, x), Q.zero(y) & Q.zero(x)) is nan + + +def test_re(): + assert refine(re(x), Q.real(x)) == x + assert refine(re(x), Q.imaginary(x)) is S.Zero + assert refine(re(x+y), Q.real(x) & Q.real(y)) == x + y + assert refine(re(x+y), Q.real(x) & Q.imaginary(y)) == x + assert refine(re(x*y), Q.real(x) & Q.real(y)) == x * y + assert refine(re(x*y), Q.real(x) & Q.imaginary(y)) == 0 + assert refine(re(x*y*z), Q.real(x) & Q.real(y) & Q.real(z)) == x * y * z + + +def test_im(): + assert refine(im(x), Q.imaginary(x)) == -I*x + assert refine(im(x), Q.real(x)) is S.Zero + assert refine(im(x+y), Q.imaginary(x) & Q.imaginary(y)) == -I*x - I*y + assert refine(im(x+y), Q.real(x) & Q.imaginary(y)) == -I*y + assert refine(im(x*y), Q.imaginary(x) & Q.real(y)) == -I*x*y + assert refine(im(x*y), Q.imaginary(x) & Q.imaginary(y)) == 0 + assert refine(im(1/x), Q.imaginary(x)) == -I/x + assert refine(im(x*y*z), Q.imaginary(x) & Q.imaginary(y) + & Q.imaginary(z)) == -I*x*y*z + + +def test_complex(): + assert refine(re(1/(x + I*y)), Q.real(x) & Q.real(y)) == \ + x/(x**2 + y**2) + assert refine(im(1/(x + I*y)), Q.real(x) & Q.real(y)) == \ + -y/(x**2 + y**2) + assert refine(re((w + I*x) * (y + I*z)), Q.real(w) & Q.real(x) & Q.real(y) + & Q.real(z)) == w*y - x*z + assert refine(im((w + I*x) * (y + I*z)), Q.real(w) & Q.real(x) & Q.real(y) + & Q.real(z)) == w*z + x*y + + +def test_sign(): + x = Symbol('x', real = True) + assert refine(sign(x), Q.positive(x)) == 1 + assert refine(sign(x), Q.negative(x)) == -1 + assert refine(sign(x), Q.zero(x)) == 0 + assert refine(sign(x), True) == sign(x) + assert refine(sign(Abs(x)), Q.nonzero(x)) == 1 + + x = Symbol('x', imaginary=True) + assert refine(sign(x), Q.positive(im(x))) == S.ImaginaryUnit + assert refine(sign(x), Q.negative(im(x))) == -S.ImaginaryUnit + assert refine(sign(x), True) == sign(x) + + x = Symbol('x', complex=True) + assert refine(sign(x), Q.zero(x)) == 0 + +def test_arg(): + x = Symbol('x', complex = True) + assert refine(arg(x), Q.positive(x)) == 0 + assert refine(arg(x), Q.negative(x)) == pi + +def test_func_args(): + class MyClass(Expr): + # A class with nontrivial .func + + def __init__(self, *args): + self.my_member = "" + + @property + def func(self): + def my_func(*args): + obj = MyClass(*args) + obj.my_member = self.my_member + return obj + return my_func + + x = MyClass() + x.my_member = "A very important value" + assert x.my_member == refine(x).my_member + +def test_issue_refine_9384(): + assert refine(Piecewise((1, x < 0), (0, True)), Q.positive(x)) == 0 + assert refine(Piecewise((1, x < 0), (0, True)), Q.negative(x)) == 1 + assert refine(Piecewise((1, x > 0), (0, True)), Q.positive(x)) == 1 + assert refine(Piecewise((1, x > 0), (0, True)), Q.negative(x)) == 0 + + +def test_eval_refine(): + class MockExpr(Expr): + def _eval_refine(self, assumptions): + return True + + mock_obj = MockExpr() + assert refine(mock_obj) + +def test_refine_issue_12724(): + expr1 = refine(Abs(x * y), Q.positive(x)) + expr2 = refine(Abs(x * y * z), Q.positive(x)) + assert expr1 == x * Abs(y) + assert expr2 == x * Abs(y * z) + y1 = Symbol('y1', real = True) + expr3 = refine(Abs(x * y1**2 * z), Q.positive(x)) + assert expr3 == x * y1**2 * Abs(z) + + +def test_matrixelement(): + x = MatrixSymbol('x', 3, 3) + i = Symbol('i', positive = True) + j = Symbol('j', positive = True) + assert refine(x[0, 1], Q.symmetric(x)) == x[0, 1] + assert refine(x[1, 0], Q.symmetric(x)) == x[0, 1] + assert refine(x[i, j], Q.symmetric(x)) == x[j, i] + assert refine(x[j, i], Q.symmetric(x)) == x[j, i] diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_rel_queries.py b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_rel_queries.py new file mode 100644 index 0000000000000000000000000000000000000000..46fe3a35dc1adb23668e88d5794fe1c0ab22f33a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_rel_queries.py @@ -0,0 +1,172 @@ +from sympy.assumptions.lra_satask import lra_satask +from sympy.logic.algorithms.lra_theory import UnhandledInput +from sympy.assumptions.ask import Q, ask + +from sympy.core import symbols, Symbol +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.core.numbers import I + +from sympy.testing.pytest import raises, XFAIL +x, y, z = symbols("x y z", real=True) + +def test_lra_satask(): + im = Symbol('im', imaginary=True) + + # test preprocessing of unequalities is working correctly + assert lra_satask(Q.eq(x, 1), ~Q.ne(x, 0)) is False + assert lra_satask(Q.eq(x, 0), ~Q.ne(x, 0)) is True + assert lra_satask(~Q.ne(x, 0), Q.eq(x, 0)) is True + assert lra_satask(~Q.eq(x, 0), Q.eq(x, 0)) is False + assert lra_satask(Q.ne(x, 0), Q.eq(x, 0)) is False + + # basic tests + assert lra_satask(Q.ne(x, x)) is False + assert lra_satask(Q.eq(x, x)) is True + assert lra_satask(Q.gt(x, 0), Q.gt(x, 1)) is True + + # check that True/False are handled + assert lra_satask(Q.gt(x, 0), True) is None + assert raises(ValueError, lambda: lra_satask(Q.gt(x, 0), False)) + + # check imaginary numbers are correctly handled + # (im * I).is_real returns True so this is an edge case + raises(UnhandledInput, lambda: lra_satask(Q.gt(im * I, 0), Q.gt(im * I, 0))) + + # check matrix inputs + X = MatrixSymbol("X", 2, 2) + raises(UnhandledInput, lambda: lra_satask(Q.lt(X, 2) & Q.gt(X, 3))) + + +def test_old_assumptions(): + # test unhandled old assumptions + w = symbols("w") + raises(UnhandledInput, lambda: lra_satask(Q.lt(w, 2) & Q.gt(w, 3))) + w = symbols("w", rational=False, real=True) + raises(UnhandledInput, lambda: lra_satask(Q.lt(w, 2) & Q.gt(w, 3))) + w = symbols("w", odd=True, real=True) + raises(UnhandledInput, lambda: lra_satask(Q.lt(w, 2) & Q.gt(w, 3))) + w = symbols("w", even=True, real=True) + raises(UnhandledInput, lambda: lra_satask(Q.lt(w, 2) & Q.gt(w, 3))) + w = symbols("w", prime=True, real=True) + raises(UnhandledInput, lambda: lra_satask(Q.lt(w, 2) & Q.gt(w, 3))) + w = symbols("w", composite=True, real=True) + raises(UnhandledInput, lambda: lra_satask(Q.lt(w, 2) & Q.gt(w, 3))) + w = symbols("w", integer=True, real=True) + raises(UnhandledInput, lambda: lra_satask(Q.lt(w, 2) & Q.gt(w, 3))) + w = symbols("w", integer=False, real=True) + raises(UnhandledInput, lambda: lra_satask(Q.lt(w, 2) & Q.gt(w, 3))) + + # test handled + w = symbols("w", positive=True, real=True) + assert lra_satask(Q.le(w, 0)) is False + assert lra_satask(Q.gt(w, 0)) is True + w = symbols("w", negative=True, real=True) + assert lra_satask(Q.lt(w, 0)) is True + assert lra_satask(Q.ge(w, 0)) is False + w = symbols("w", zero=True, real=True) + assert lra_satask(Q.eq(w, 0)) is True + assert lra_satask(Q.ne(w, 0)) is False + w = symbols("w", nonzero=True, real=True) + assert lra_satask(Q.ne(w, 0)) is True + assert lra_satask(Q.eq(w, 1)) is None + w = symbols("w", nonpositive=True, real=True) + assert lra_satask(Q.le(w, 0)) is True + assert lra_satask(Q.gt(w, 0)) is False + w = symbols("w", nonnegative=True, real=True) + assert lra_satask(Q.ge(w, 0)) is True + assert lra_satask(Q.lt(w, 0)) is False + + +def test_rel_queries(): + assert ask(Q.lt(x, 2) & Q.gt(x, 3)) is False + assert ask(Q.positive(x - z), (x > y) & (y > z)) is True + assert ask(x + y > 2, (x < 0) & (y <0)) is False + assert ask(x > z, (x > y) & (y > z)) is True + + +def test_unhandled_queries(): + X = MatrixSymbol("X", 2, 2) + assert ask(Q.lt(X, 2) & Q.gt(X, 3)) is None + + +def test_all_pred(): + # test usable pred + assert lra_satask(Q.extended_positive(x), (x > 2)) is True + assert lra_satask(Q.positive_infinite(x)) is False + assert lra_satask(Q.negative_infinite(x)) is False + + # test disallowed pred + raises(UnhandledInput, lambda: lra_satask((x > 0), (x > 2) & Q.prime(x))) + raises(UnhandledInput, lambda: lra_satask((x > 0), (x > 2) & Q.composite(x))) + raises(UnhandledInput, lambda: lra_satask((x > 0), (x > 2) & Q.odd(x))) + raises(UnhandledInput, lambda: lra_satask((x > 0), (x > 2) & Q.even(x))) + raises(UnhandledInput, lambda: lra_satask((x > 0), (x > 2) & Q.integer(x))) + + +def test_number_line_properties(): + # From: + # https://en.wikipedia.org/wiki/Inequality_(mathematics)#Properties_on_the_number_line + + a, b, c = symbols("a b c", real=True) + + # Transitivity + # If a <= b and b <= c, then a <= c. + assert ask(a <= c, (a <= b) & (b <= c)) is True + # If a <= b and b < c, then a < c. + assert ask(a < c, (a <= b) & (b < c)) is True + # If a < b and b <= c, then a < c. + assert ask(a < c, (a < b) & (b <= c)) is True + + # Addition and subtraction + # If a <= b, then a + c <= b + c and a - c <= b - c. + assert ask(a + c <= b + c, a <= b) is True + assert ask(a - c <= b - c, a <= b) is True + + +@XFAIL +def test_failing_number_line_properties(): + # From: + # https://en.wikipedia.org/wiki/Inequality_(mathematics)#Properties_on_the_number_line + + a, b, c = symbols("a b c", real=True) + + # Multiplication and division + # If a <= b and c > 0, then ac <= bc and a/c <= b/c. (True for non-zero c) + assert ask(a*c <= b*c, (a <= b) & (c > 0) & ~ Q.zero(c)) is True + assert ask(a/c <= b/c, (a <= b) & (c > 0) & ~ Q.zero(c)) is True + # If a <= b and c < 0, then ac >= bc and a/c >= b/c. (True for non-zero c) + assert ask(a*c >= b*c, (a <= b) & (c < 0) & ~ Q.zero(c)) is True + assert ask(a/c >= b/c, (a <= b) & (c < 0) & ~ Q.zero(c)) is True + + # Additive inverse + # If a <= b, then -a >= -b. + assert ask(-a >= -b, a <= b) is True + + # Multiplicative inverse + # For a, b that are both negative or both positive: + # If a <= b, then 1/a >= 1/b . + assert ask(1/a >= 1/b, (a <= b) & Q.positive(x) & Q.positive(b)) is True + assert ask(1/a >= 1/b, (a <= b) & Q.negative(x) & Q.negative(b)) is True + + +def test_equality(): + # test symmetry and reflexivity + assert ask(Q.eq(x, x)) is True + assert ask(Q.eq(y, x), Q.eq(x, y)) is True + assert ask(Q.eq(y, x), ~Q.eq(z, z) | Q.eq(x, y)) is True + + # test transitivity + assert ask(Q.eq(x,z), Q.eq(x,y) & Q.eq(y,z)) is True + + +@XFAIL +def test_equality_failing(): + # Note that implementing the substitution property of equality + # most likely requires a redesign of the new assumptions. + # See issue #25485 for why this is the case and general ideas + # about how things could be redesigned. + + # test substitution property + assert ask(Q.prime(x), Q.eq(x, y) & Q.prime(y)) is True + assert ask(Q.real(x), Q.eq(x, y) & Q.real(y)) is True + assert ask(Q.imaginary(x), Q.eq(x, y) & Q.imaginary(y)) is True diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_satask.py b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_satask.py new file mode 100644 index 0000000000000000000000000000000000000000..5831b69e3e6bf2b1a906d1140967510c2ea8b630 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_satask.py @@ -0,0 +1,378 @@ +from sympy.assumptions.ask import Q +from sympy.assumptions.assume import assuming +from sympy.core.numbers import (I, pi) +from sympy.core.relational import (Eq, Gt) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.complexes import Abs +from sympy.logic.boolalg import Implies +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.assumptions.cnf import CNF, Literal +from sympy.assumptions.satask import (satask, extract_predargs, + get_relevant_clsfacts) + +from sympy.testing.pytest import raises, XFAIL + + +x, y, z = symbols('x y z') + + +def test_satask(): + # No relevant facts + assert satask(Q.real(x), Q.real(x)) is True + assert satask(Q.real(x), ~Q.real(x)) is False + assert satask(Q.real(x)) is None + + assert satask(Q.real(x), Q.positive(x)) is True + assert satask(Q.positive(x), Q.real(x)) is None + assert satask(Q.real(x), ~Q.positive(x)) is None + assert satask(Q.positive(x), ~Q.real(x)) is False + + raises(ValueError, lambda: satask(Q.real(x), Q.real(x) & ~Q.real(x))) + + with assuming(Q.positive(x)): + assert satask(Q.real(x)) is True + assert satask(~Q.positive(x)) is False + raises(ValueError, lambda: satask(Q.real(x), ~Q.positive(x))) + + assert satask(Q.zero(x), Q.nonzero(x)) is False + assert satask(Q.positive(x), Q.zero(x)) is False + assert satask(Q.real(x), Q.zero(x)) is True + assert satask(Q.zero(x), Q.zero(x*y)) is None + assert satask(Q.zero(x*y), Q.zero(x)) + + +def test_zero(): + """ + Everything in this test doesn't work with the ask handlers, and most + things would be very difficult or impossible to make work under that + model. + + """ + assert satask(Q.zero(x) | Q.zero(y), Q.zero(x*y)) is True + assert satask(Q.zero(x*y), Q.zero(x) | Q.zero(y)) is True + + assert satask(Implies(Q.zero(x), Q.zero(x*y))) is True + + # This one in particular requires computing the fixed-point of the + # relevant facts, because going from Q.nonzero(x*y) -> ~Q.zero(x*y) and + # Q.zero(x*y) -> Equivalent(Q.zero(x*y), Q.zero(x) | Q.zero(y)) takes two + # steps. + assert satask(Q.zero(x) | Q.zero(y), Q.nonzero(x*y)) is False + + assert satask(Q.zero(x), Q.zero(x**2)) is True + + +def test_zero_positive(): + assert satask(Q.zero(x + y), Q.positive(x) & Q.positive(y)) is False + assert satask(Q.positive(x) & Q.positive(y), Q.zero(x + y)) is False + assert satask(Q.nonzero(x + y), Q.positive(x) & Q.positive(y)) is True + assert satask(Q.positive(x) & Q.positive(y), Q.nonzero(x + y)) is None + + # This one requires several levels of forward chaining + assert satask(Q.zero(x*(x + y)), Q.positive(x) & Q.positive(y)) is False + + assert satask(Q.positive(pi*x*y + 1), Q.positive(x) & Q.positive(y)) is True + assert satask(Q.positive(pi*x*y - 5), Q.positive(x) & Q.positive(y)) is None + + +def test_zero_pow(): + assert satask(Q.zero(x**y), Q.zero(x) & Q.positive(y)) is True + assert satask(Q.zero(x**y), Q.nonzero(x) & Q.zero(y)) is False + + assert satask(Q.zero(x), Q.zero(x**y)) is True + + assert satask(Q.zero(x**y), Q.zero(x)) is None + + +@XFAIL +# Requires correct Q.square calculation first +def test_invertible(): + A = MatrixSymbol('A', 5, 5) + B = MatrixSymbol('B', 5, 5) + assert satask(Q.invertible(A*B), Q.invertible(A) & Q.invertible(B)) is True + assert satask(Q.invertible(A), Q.invertible(A*B)) is True + assert satask(Q.invertible(A) & Q.invertible(B), Q.invertible(A*B)) is True + + +def test_prime(): + assert satask(Q.prime(5)) is True + assert satask(Q.prime(6)) is False + assert satask(Q.prime(-5)) is False + + assert satask(Q.prime(x*y), Q.integer(x) & Q.integer(y)) is None + assert satask(Q.prime(x*y), Q.prime(x) & Q.prime(y)) is False + + +def test_old_assump(): + assert satask(Q.positive(1)) is True + assert satask(Q.positive(-1)) is False + assert satask(Q.positive(0)) is False + assert satask(Q.positive(I)) is False + assert satask(Q.positive(pi)) is True + + assert satask(Q.negative(1)) is False + assert satask(Q.negative(-1)) is True + assert satask(Q.negative(0)) is False + assert satask(Q.negative(I)) is False + assert satask(Q.negative(pi)) is False + + assert satask(Q.zero(1)) is False + assert satask(Q.zero(-1)) is False + assert satask(Q.zero(0)) is True + assert satask(Q.zero(I)) is False + assert satask(Q.zero(pi)) is False + + assert satask(Q.nonzero(1)) is True + assert satask(Q.nonzero(-1)) is True + assert satask(Q.nonzero(0)) is False + assert satask(Q.nonzero(I)) is False + assert satask(Q.nonzero(pi)) is True + + assert satask(Q.nonpositive(1)) is False + assert satask(Q.nonpositive(-1)) is True + assert satask(Q.nonpositive(0)) is True + assert satask(Q.nonpositive(I)) is False + assert satask(Q.nonpositive(pi)) is False + + assert satask(Q.nonnegative(1)) is True + assert satask(Q.nonnegative(-1)) is False + assert satask(Q.nonnegative(0)) is True + assert satask(Q.nonnegative(I)) is False + assert satask(Q.nonnegative(pi)) is True + + +def test_rational_irrational(): + assert satask(Q.irrational(2)) is False + assert satask(Q.rational(2)) is True + assert satask(Q.irrational(pi)) is True + assert satask(Q.rational(pi)) is False + assert satask(Q.irrational(I)) is False + assert satask(Q.rational(I)) is False + + assert satask(Q.irrational(x*y*z), Q.irrational(x) & Q.irrational(y) & + Q.rational(z)) is None + assert satask(Q.irrational(x*y*z), Q.irrational(x) & Q.rational(y) & + Q.rational(z)) is True + assert satask(Q.irrational(pi*x*y), Q.rational(x) & Q.rational(y)) is True + + assert satask(Q.irrational(x + y + z), Q.irrational(x) & Q.irrational(y) & + Q.rational(z)) is None + assert satask(Q.irrational(x + y + z), Q.irrational(x) & Q.rational(y) & + Q.rational(z)) is True + assert satask(Q.irrational(pi + x + y), Q.rational(x) & Q.rational(y)) is True + + assert satask(Q.irrational(x*y*z), Q.rational(x) & Q.rational(y) & + Q.rational(z)) is False + assert satask(Q.rational(x*y*z), Q.rational(x) & Q.rational(y) & + Q.rational(z)) is True + + assert satask(Q.irrational(x + y + z), Q.rational(x) & Q.rational(y) & + Q.rational(z)) is False + assert satask(Q.rational(x + y + z), Q.rational(x) & Q.rational(y) & + Q.rational(z)) is True + + +def test_even_satask(): + assert satask(Q.even(2)) is True + assert satask(Q.even(3)) is False + + assert satask(Q.even(x*y), Q.even(x) & Q.odd(y)) is True + assert satask(Q.even(x*y), Q.even(x) & Q.integer(y)) is True + assert satask(Q.even(x*y), Q.even(x) & Q.even(y)) is True + assert satask(Q.even(x*y), Q.odd(x) & Q.odd(y)) is False + assert satask(Q.even(x*y), Q.even(x)) is None + assert satask(Q.even(x*y), Q.odd(x) & Q.integer(y)) is None + assert satask(Q.even(x*y), Q.odd(x) & Q.odd(y)) is False + + assert satask(Q.even(abs(x)), Q.even(x)) is True + assert satask(Q.even(abs(x)), Q.odd(x)) is False + assert satask(Q.even(x), Q.even(abs(x))) is None # x could be complex + + +def test_odd_satask(): + assert satask(Q.odd(2)) is False + assert satask(Q.odd(3)) is True + + assert satask(Q.odd(x*y), Q.even(x) & Q.odd(y)) is False + assert satask(Q.odd(x*y), Q.even(x) & Q.integer(y)) is False + assert satask(Q.odd(x*y), Q.even(x) & Q.even(y)) is False + assert satask(Q.odd(x*y), Q.odd(x) & Q.odd(y)) is True + assert satask(Q.odd(x*y), Q.even(x)) is None + assert satask(Q.odd(x*y), Q.odd(x) & Q.integer(y)) is None + assert satask(Q.odd(x*y), Q.odd(x) & Q.odd(y)) is True + + assert satask(Q.odd(abs(x)), Q.even(x)) is False + assert satask(Q.odd(abs(x)), Q.odd(x)) is True + assert satask(Q.odd(x), Q.odd(abs(x))) is None # x could be complex + + +def test_integer(): + assert satask(Q.integer(1)) is True + assert satask(Q.integer(S.Half)) is False + + assert satask(Q.integer(x + y), Q.integer(x) & Q.integer(y)) is True + assert satask(Q.integer(x + y), Q.integer(x)) is None + + assert satask(Q.integer(x + y), Q.integer(x) & ~Q.integer(y)) is False + assert satask(Q.integer(x + y + z), Q.integer(x) & Q.integer(y) & + ~Q.integer(z)) is False + assert satask(Q.integer(x + y + z), Q.integer(x) & ~Q.integer(y) & + ~Q.integer(z)) is None + assert satask(Q.integer(x + y + z), Q.integer(x) & ~Q.integer(y)) is None + assert satask(Q.integer(x + y), Q.integer(x) & Q.irrational(y)) is False + + assert satask(Q.integer(x*y), Q.integer(x) & Q.integer(y)) is True + assert satask(Q.integer(x*y), Q.integer(x)) is None + + assert satask(Q.integer(x*y), Q.integer(x) & ~Q.integer(y)) is None + assert satask(Q.integer(x*y), Q.integer(x) & ~Q.rational(y)) is False + assert satask(Q.integer(x*y*z), Q.integer(x) & Q.integer(y) & + ~Q.rational(z)) is False + assert satask(Q.integer(x*y*z), Q.integer(x) & ~Q.rational(y) & + ~Q.rational(z)) is None + assert satask(Q.integer(x*y*z), Q.integer(x) & ~Q.rational(y)) is None + assert satask(Q.integer(x*y), Q.integer(x) & Q.irrational(y)) is False + + +def test_abs(): + assert satask(Q.nonnegative(abs(x))) is True + assert satask(Q.positive(abs(x)), ~Q.zero(x)) is True + assert satask(Q.zero(x), ~Q.zero(abs(x))) is False + assert satask(Q.zero(x), Q.zero(abs(x))) is True + assert satask(Q.nonzero(x), ~Q.zero(abs(x))) is None # x could be complex + assert satask(Q.zero(abs(x)), Q.zero(x)) is True + + +def test_imaginary(): + assert satask(Q.imaginary(2*I)) is True + assert satask(Q.imaginary(x*y), Q.imaginary(x)) is None + assert satask(Q.imaginary(x*y), Q.imaginary(x) & Q.real(y)) is True + assert satask(Q.imaginary(x), Q.real(x)) is False + assert satask(Q.imaginary(1)) is False + assert satask(Q.imaginary(x*y), Q.real(x) & Q.real(y)) is False + assert satask(Q.imaginary(x + y), Q.real(x) & Q.real(y)) is False + + +def test_real(): + assert satask(Q.real(x*y), Q.real(x) & Q.real(y)) is True + assert satask(Q.real(x + y), Q.real(x) & Q.real(y)) is True + assert satask(Q.real(x*y*z), Q.real(x) & Q.real(y) & Q.real(z)) is True + assert satask(Q.real(x*y*z), Q.real(x) & Q.real(y)) is None + assert satask(Q.real(x*y*z), Q.real(x) & Q.real(y) & Q.imaginary(z)) is False + assert satask(Q.real(x + y + z), Q.real(x) & Q.real(y) & Q.real(z)) is True + assert satask(Q.real(x + y + z), Q.real(x) & Q.real(y)) is None + + +def test_pos_neg(): + assert satask(~Q.positive(x), Q.negative(x)) is True + assert satask(~Q.negative(x), Q.positive(x)) is True + assert satask(Q.positive(x + y), Q.positive(x) & Q.positive(y)) is True + assert satask(Q.negative(x + y), Q.negative(x) & Q.negative(y)) is True + assert satask(Q.positive(x + y), Q.negative(x) & Q.negative(y)) is False + assert satask(Q.negative(x + y), Q.positive(x) & Q.positive(y)) is False + + +def test_pow_pos_neg(): + assert satask(Q.nonnegative(x**2), Q.positive(x)) is True + assert satask(Q.nonpositive(x**2), Q.positive(x)) is False + assert satask(Q.positive(x**2), Q.positive(x)) is True + assert satask(Q.negative(x**2), Q.positive(x)) is False + assert satask(Q.real(x**2), Q.positive(x)) is True + + assert satask(Q.nonnegative(x**2), Q.negative(x)) is True + assert satask(Q.nonpositive(x**2), Q.negative(x)) is False + assert satask(Q.positive(x**2), Q.negative(x)) is True + assert satask(Q.negative(x**2), Q.negative(x)) is False + assert satask(Q.real(x**2), Q.negative(x)) is True + + assert satask(Q.nonnegative(x**2), Q.nonnegative(x)) is True + assert satask(Q.nonpositive(x**2), Q.nonnegative(x)) is None + assert satask(Q.positive(x**2), Q.nonnegative(x)) is None + assert satask(Q.negative(x**2), Q.nonnegative(x)) is False + assert satask(Q.real(x**2), Q.nonnegative(x)) is True + + assert satask(Q.nonnegative(x**2), Q.nonpositive(x)) is True + assert satask(Q.nonpositive(x**2), Q.nonpositive(x)) is None + assert satask(Q.positive(x**2), Q.nonpositive(x)) is None + assert satask(Q.negative(x**2), Q.nonpositive(x)) is False + assert satask(Q.real(x**2), Q.nonpositive(x)) is True + + assert satask(Q.nonnegative(x**3), Q.positive(x)) is True + assert satask(Q.nonpositive(x**3), Q.positive(x)) is False + assert satask(Q.positive(x**3), Q.positive(x)) is True + assert satask(Q.negative(x**3), Q.positive(x)) is False + assert satask(Q.real(x**3), Q.positive(x)) is True + + assert satask(Q.nonnegative(x**3), Q.negative(x)) is False + assert satask(Q.nonpositive(x**3), Q.negative(x)) is True + assert satask(Q.positive(x**3), Q.negative(x)) is False + assert satask(Q.negative(x**3), Q.negative(x)) is True + assert satask(Q.real(x**3), Q.negative(x)) is True + + assert satask(Q.nonnegative(x**3), Q.nonnegative(x)) is True + assert satask(Q.nonpositive(x**3), Q.nonnegative(x)) is None + assert satask(Q.positive(x**3), Q.nonnegative(x)) is None + assert satask(Q.negative(x**3), Q.nonnegative(x)) is False + assert satask(Q.real(x**3), Q.nonnegative(x)) is True + + assert satask(Q.nonnegative(x**3), Q.nonpositive(x)) is None + assert satask(Q.nonpositive(x**3), Q.nonpositive(x)) is True + assert satask(Q.positive(x**3), Q.nonpositive(x)) is False + assert satask(Q.negative(x**3), Q.nonpositive(x)) is None + assert satask(Q.real(x**3), Q.nonpositive(x)) is True + + # If x is zero, x**negative is not real. + assert satask(Q.nonnegative(x**-2), Q.nonpositive(x)) is None + assert satask(Q.nonpositive(x**-2), Q.nonpositive(x)) is None + assert satask(Q.positive(x**-2), Q.nonpositive(x)) is None + assert satask(Q.negative(x**-2), Q.nonpositive(x)) is None + assert satask(Q.real(x**-2), Q.nonpositive(x)) is None + + # We could deduce things for negative powers if x is nonzero, but it + # isn't implemented yet. + + +def test_prime_composite(): + assert satask(Q.prime(x), Q.composite(x)) is False + assert satask(Q.composite(x), Q.prime(x)) is False + assert satask(Q.composite(x), ~Q.prime(x)) is None + assert satask(Q.prime(x), ~Q.composite(x)) is None + # since 1 is neither prime nor composite the following should hold + assert satask(Q.prime(x), Q.integer(x) & Q.positive(x) & ~Q.composite(x)) is None + assert satask(Q.prime(2)) is True + assert satask(Q.prime(4)) is False + assert satask(Q.prime(1)) is False + assert satask(Q.composite(1)) is False + + +def test_extract_predargs(): + props = CNF.from_prop(Q.zero(Abs(x*y)) & Q.zero(x*y)) + assump = CNF.from_prop(Q.zero(x)) + context = CNF.from_prop(Q.zero(y)) + assert extract_predargs(props) == {Abs(x*y), x*y} + assert extract_predargs(props, assump) == {Abs(x*y), x*y, x} + assert extract_predargs(props, assump, context) == {Abs(x*y), x*y, x, y} + + props = CNF.from_prop(Eq(x, y)) + assump = CNF.from_prop(Gt(y, z)) + assert extract_predargs(props, assump) == {x, y, z} + + +def test_get_relevant_clsfacts(): + exprs = {Abs(x*y)} + exprs, facts = get_relevant_clsfacts(exprs) + assert exprs == {x*y} + assert facts.clauses == \ + {frozenset({Literal(Q.odd(Abs(x*y)), False), Literal(Q.odd(x*y), True)}), + frozenset({Literal(Q.zero(Abs(x*y)), False), Literal(Q.zero(x*y), True)}), + frozenset({Literal(Q.even(Abs(x*y)), False), Literal(Q.even(x*y), True)}), + frozenset({Literal(Q.zero(Abs(x*y)), True), Literal(Q.zero(x*y), False)}), + frozenset({Literal(Q.even(Abs(x*y)), False), + Literal(Q.odd(Abs(x*y)), False), + Literal(Q.odd(x*y), True)}), + frozenset({Literal(Q.even(Abs(x*y)), False), + Literal(Q.even(x*y), True), + Literal(Q.odd(Abs(x*y)), False)}), + frozenset({Literal(Q.positive(Abs(x*y)), False), + Literal(Q.zero(Abs(x*y)), False)})} diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_sathandlers.py b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_sathandlers.py new file mode 100644 index 0000000000000000000000000000000000000000..9d568ad8efe6ba7cf7f5eb03879ad6764c16e729 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_sathandlers.py @@ -0,0 +1,50 @@ +from sympy.assumptions.ask import Q +from sympy.core.basic import Basic +from sympy.core.expr import Expr +from sympy.core.mul import Mul +from sympy.core.symbol import symbols +from sympy.logic.boolalg import (And, Or) + +from sympy.assumptions.sathandlers import (ClassFactRegistry, allargs, + anyarg, exactlyonearg,) + +x, y, z = symbols('x y z') + + +def test_class_handler_registry(): + my_handler_registry = ClassFactRegistry() + + # The predicate doesn't matter here, so just pass + @my_handler_registry.register(Mul) + def fact1(expr): + pass + @my_handler_registry.multiregister(Expr) + def fact2(expr): + pass + + assert my_handler_registry[Basic] == (frozenset(), frozenset()) + assert my_handler_registry[Expr] == (frozenset(), frozenset({fact2})) + assert my_handler_registry[Mul] == (frozenset({fact1}), frozenset({fact2})) + + +def test_allargs(): + assert allargs(x, Q.zero(x), x*y) == And(Q.zero(x), Q.zero(y)) + assert allargs(x, Q.positive(x) | Q.negative(x), x*y) == And(Q.positive(x) | Q.negative(x), Q.positive(y) | Q.negative(y)) + + +def test_anyarg(): + assert anyarg(x, Q.zero(x), x*y) == Or(Q.zero(x), Q.zero(y)) + assert anyarg(x, Q.positive(x) & Q.negative(x), x*y) == \ + Or(Q.positive(x) & Q.negative(x), Q.positive(y) & Q.negative(y)) + + +def test_exactlyonearg(): + assert exactlyonearg(x, Q.zero(x), x*y) == \ + Or(Q.zero(x) & ~Q.zero(y), Q.zero(y) & ~Q.zero(x)) + assert exactlyonearg(x, Q.zero(x), x*y*z) == \ + Or(Q.zero(x) & ~Q.zero(y) & ~Q.zero(z), Q.zero(y) + & ~Q.zero(x) & ~Q.zero(z), Q.zero(z) & ~Q.zero(x) & ~Q.zero(y)) + assert exactlyonearg(x, Q.positive(x) | Q.negative(x), x*y) == \ + Or((Q.positive(x) | Q.negative(x)) & + ~(Q.positive(y) | Q.negative(y)), (Q.positive(y) | Q.negative(y)) & + ~(Q.positive(x) | Q.negative(x))) diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_wrapper.py b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..af9afd5d51fb1341e0b08149dc842b78a39c329b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/tests/test_wrapper.py @@ -0,0 +1,39 @@ +from sympy.assumptions.ask import Q +from sympy.assumptions.wrapper import (AssumptionsWrapper, is_infinite, + is_extended_real) +from sympy.core.symbol import Symbol +from sympy.core.assumptions import _assume_defined + + +def test_all_predicates(): + for fact in _assume_defined: + method_name = f'_eval_is_{fact}' + assert hasattr(AssumptionsWrapper, method_name) + + +def test_AssumptionsWrapper(): + x = Symbol('x', positive=True) + y = Symbol('y') + assert AssumptionsWrapper(x).is_positive + assert AssumptionsWrapper(y).is_positive is None + assert AssumptionsWrapper(y, Q.positive(y)).is_positive + + +def test_is_infinite(): + x = Symbol('x', infinite=True) + y = Symbol('y', infinite=False) + z = Symbol('z') + assert is_infinite(x) + assert not is_infinite(y) + assert is_infinite(z) is None + assert is_infinite(z, Q.infinite(z)) + + +def test_is_extended_real(): + x = Symbol('x', extended_real=True) + y = Symbol('y', extended_real=False) + z = Symbol('z') + assert is_extended_real(x) + assert not is_extended_real(y) + assert is_extended_real(z) is None + assert is_extended_real(z, Q.extended_real(z)) diff --git a/venv/lib/python3.11/site-packages/sympy/assumptions/wrapper.py b/venv/lib/python3.11/site-packages/sympy/assumptions/wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..cb06e9de770ed41a2b3d6fe63381ad1cb59acacc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/assumptions/wrapper.py @@ -0,0 +1,164 @@ +""" +Functions and wrapper object to call assumption property and predicate +query with same syntax. + +In SymPy, there are two assumption systems. Old assumption system is +defined in sympy/core/assumptions, and it can be accessed by attribute +such as ``x.is_even``. New assumption system is defined in +sympy/assumptions, and it can be accessed by predicates such as +``Q.even(x)``. + +Old assumption is fast, while new assumptions can freely take local facts. +In general, old assumption is used in evaluation method and new assumption +is used in refinement method. + +In most cases, both evaluation and refinement follow the same process, and +the only difference is which assumption system is used. This module provides +``is_[...]()`` functions and ``AssumptionsWrapper()`` class which allows +using two systems with same syntax so that parallel code implementation can be +avoided. + +Examples +======== + +For multiple use, use ``AssumptionsWrapper()``. + +>>> from sympy import Q, Symbol +>>> from sympy.assumptions.wrapper import AssumptionsWrapper +>>> x = Symbol('x') +>>> _x = AssumptionsWrapper(x, Q.even(x)) +>>> _x.is_integer +True +>>> _x.is_odd +False + +For single use, use ``is_[...]()`` functions. + +>>> from sympy.assumptions.wrapper import is_infinite +>>> a = Symbol('a') +>>> print(is_infinite(a)) +None +>>> is_infinite(a, Q.finite(a)) +False + +""" + +from sympy.assumptions import ask, Q +from sympy.core.basic import Basic +from sympy.core.sympify import _sympify + + +def make_eval_method(fact): + def getit(self): + pred = getattr(Q, fact) + ret = ask(pred(self.expr), self.assumptions) + return ret + return getit + + +# we subclass Basic to use the fact deduction and caching +class AssumptionsWrapper(Basic): + """ + Wrapper over ``Basic`` instances to call predicate query by + ``.is_[...]`` property + + Parameters + ========== + + expr : Basic + + assumptions : Boolean, optional + + Examples + ======== + + >>> from sympy import Q, Symbol + >>> from sympy.assumptions.wrapper import AssumptionsWrapper + >>> x = Symbol('x', even=True) + >>> AssumptionsWrapper(x).is_integer + True + >>> y = Symbol('y') + >>> AssumptionsWrapper(y, Q.even(y)).is_integer + True + + With ``AssumptionsWrapper``, both evaluation and refinement can be supported + by single implementation. + + >>> from sympy import Function + >>> class MyAbs(Function): + ... @classmethod + ... def eval(cls, x, assumptions=True): + ... _x = AssumptionsWrapper(x, assumptions) + ... if _x.is_nonnegative: + ... return x + ... if _x.is_negative: + ... return -x + ... def _eval_refine(self, assumptions): + ... return MyAbs.eval(self.args[0], assumptions) + >>> MyAbs(x) + MyAbs(x) + >>> MyAbs(x).refine(Q.positive(x)) + x + >>> MyAbs(Symbol('y', negative=True)) + -y + + """ + def __new__(cls, expr, assumptions=None): + if assumptions is None: + return expr + obj = super().__new__(cls, expr, _sympify(assumptions)) + obj.expr = expr + obj.assumptions = assumptions + return obj + + _eval_is_algebraic = make_eval_method("algebraic") + _eval_is_antihermitian = make_eval_method("antihermitian") + _eval_is_commutative = make_eval_method("commutative") + _eval_is_complex = make_eval_method("complex") + _eval_is_composite = make_eval_method("composite") + _eval_is_even = make_eval_method("even") + _eval_is_extended_negative = make_eval_method("extended_negative") + _eval_is_extended_nonnegative = make_eval_method("extended_nonnegative") + _eval_is_extended_nonpositive = make_eval_method("extended_nonpositive") + _eval_is_extended_nonzero = make_eval_method("extended_nonzero") + _eval_is_extended_positive = make_eval_method("extended_positive") + _eval_is_extended_real = make_eval_method("extended_real") + _eval_is_finite = make_eval_method("finite") + _eval_is_hermitian = make_eval_method("hermitian") + _eval_is_imaginary = make_eval_method("imaginary") + _eval_is_infinite = make_eval_method("infinite") + _eval_is_integer = make_eval_method("integer") + _eval_is_irrational = make_eval_method("irrational") + _eval_is_negative = make_eval_method("negative") + _eval_is_noninteger = make_eval_method("noninteger") + _eval_is_nonnegative = make_eval_method("nonnegative") + _eval_is_nonpositive = make_eval_method("nonpositive") + _eval_is_nonzero = make_eval_method("nonzero") + _eval_is_odd = make_eval_method("odd") + _eval_is_polar = make_eval_method("polar") + _eval_is_positive = make_eval_method("positive") + _eval_is_prime = make_eval_method("prime") + _eval_is_rational = make_eval_method("rational") + _eval_is_real = make_eval_method("real") + _eval_is_transcendental = make_eval_method("transcendental") + _eval_is_zero = make_eval_method("zero") + + +# one shot functions which are faster than AssumptionsWrapper + +def is_infinite(obj, assumptions=None): + if assumptions is None: + return obj.is_infinite + return ask(Q.infinite(obj), assumptions) + + +def is_extended_real(obj, assumptions=None): + if assumptions is None: + return obj.is_extended_real + return ask(Q.extended_real(obj), assumptions) + + +def is_extended_nonnegative(obj, assumptions=None): + if assumptions is None: + return obj.is_extended_nonnegative + return ask(Q.extended_nonnegative(obj), assumptions) diff --git a/venv/lib/python3.11/site-packages/sympy/benchmarks/__init__.py b/venv/lib/python3.11/site-packages/sympy/benchmarks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/benchmarks/bench_discrete_log.py b/venv/lib/python3.11/site-packages/sympy/benchmarks/bench_discrete_log.py new file mode 100644 index 0000000000000000000000000000000000000000..76b273909e415318a7d3bace00ffff2a0bc53762 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/benchmarks/bench_discrete_log.py @@ -0,0 +1,83 @@ +import sys +from time import time +from sympy.ntheory.residue_ntheory import (discrete_log, + _discrete_log_trial_mul, _discrete_log_shanks_steps, + _discrete_log_pollard_rho, _discrete_log_pohlig_hellman) + + +# Cyclic group (Z/pZ)* with p prime, order p - 1 and generator g +data_set_1 = [ + # p, p - 1, g + [191, 190, 19], + [46639, 46638, 6], + [14789363, 14789362, 2], + [4254225211, 4254225210, 2], + [432751500361, 432751500360, 7], + [158505390797053, 158505390797052, 2], + [6575202655312007, 6575202655312006, 5], + [8430573471995353769, 8430573471995353768, 3], + [3938471339744997827267, 3938471339744997827266, 2], + [875260951364705563393093, 875260951364705563393092, 5], + ] + + +# Cyclic sub-groups of (Z/nZ)* with prime order p and generator g +# (n, p are primes and n = 2 * p + 1) +data_set_2 = [ + # n, p, g + [227, 113, 3], + [2447, 1223, 2], + [24527, 12263, 2], + [245639, 122819, 2], + [2456747, 1228373, 3], + [24567899, 12283949, 3], + [245679023, 122839511, 2], + [2456791307, 1228395653, 3], + [24567913439, 12283956719, 2], + [245679135407, 122839567703, 2], + [2456791354763, 1228395677381, 3], + [24567913550903, 12283956775451, 2], + [245679135509519, 122839567754759, 2], + ] + + +# Cyclic sub-groups of (Z/nZ)* with smooth order o and generator g +data_set_3 = [ + # n, o, g + [2**118, 2**116, 3], + ] + + +def bench_discrete_log(data_set, algo=None): + if algo is None: + f = discrete_log + elif algo == 'trial': + f = _discrete_log_trial_mul + elif algo == 'shanks': + f = _discrete_log_shanks_steps + elif algo == 'rho': + f = _discrete_log_pollard_rho + elif algo == 'ph': + f = _discrete_log_pohlig_hellman + else: + raise ValueError("Argument 'algo' should be one" + " of ('trial', 'shanks', 'rho' or 'ph')") + + for i, data in enumerate(data_set): + for j, (n, p, g) in enumerate(data): + t = time() + l = f(n, pow(g, p - 1, n), g, p) + t = time() - t + print('[%02d-%03d] %15.10f' % (i, j, t)) + assert l == p - 1 + + +if __name__ == '__main__': + algo = sys.argv[1] \ + if len(sys.argv) > 1 else None + data_set = [ + data_set_1, + data_set_2, + data_set_3, + ] + bench_discrete_log(data_set, algo) diff --git a/venv/lib/python3.11/site-packages/sympy/benchmarks/bench_meijerint.py b/venv/lib/python3.11/site-packages/sympy/benchmarks/bench_meijerint.py new file mode 100644 index 0000000000000000000000000000000000000000..d648c3e02463d5a7ee1dcbe3b22af5cc22fef43d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/benchmarks/bench_meijerint.py @@ -0,0 +1,261 @@ +# conceal the implicit import from the code quality tester +from sympy.core.numbers import (oo, pi) +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.special.bessel import besseli +from sympy.functions.special.gamma_functions import gamma +from sympy.integrals.integrals import integrate +from sympy.integrals.transforms import (mellin_transform, + inverse_fourier_transform, inverse_mellin_transform, + laplace_transform, inverse_laplace_transform, fourier_transform) + +LT = laplace_transform +FT = fourier_transform +MT = mellin_transform +IFT = inverse_fourier_transform +ILT = inverse_laplace_transform +IMT = inverse_mellin_transform + +from sympy.abc import x, y +nu, beta, rho = symbols('nu beta rho') + +apos, bpos, cpos, dpos, posk, p = symbols('a b c d k p', positive=True) +k = Symbol('k', real=True) +negk = Symbol('k', negative=True) + +mu1, mu2 = symbols('mu1 mu2', real=True, nonzero=True, finite=True) +sigma1, sigma2 = symbols('sigma1 sigma2', real=True, nonzero=True, + finite=True, positive=True) +rate = Symbol('lambda', positive=True) + + +def normal(x, mu, sigma): + return 1/sqrt(2*pi*sigma**2)*exp(-(x - mu)**2/2/sigma**2) + + +def exponential(x, rate): + return rate*exp(-rate*x) +alpha, beta = symbols('alpha beta', positive=True) +betadist = x**(alpha - 1)*(1 + x)**(-alpha - beta)*gamma(alpha + beta) \ + /gamma(alpha)/gamma(beta) +kint = Symbol('k', integer=True, positive=True) +chi = 2**(1 - kint/2)*x**(kint - 1)*exp(-x**2/2)/gamma(kint/2) +chisquared = 2**(-k/2)/gamma(k/2)*x**(k/2 - 1)*exp(-x/2) +dagum = apos*p/x*(x/bpos)**(apos*p)/(1 + x**apos/bpos**apos)**(p + 1) +d1, d2 = symbols('d1 d2', positive=True) +f = sqrt(((d1*x)**d1 * d2**d2)/(d1*x + d2)**(d1 + d2))/x \ + /gamma(d1/2)/gamma(d2/2)*gamma((d1 + d2)/2) +nupos, sigmapos = symbols('nu sigma', positive=True) +rice = x/sigmapos**2*exp(-(x**2 + nupos**2)/2/sigmapos**2)*besseli(0, x* + nupos/sigmapos**2) +mu = Symbol('mu', real=True) +laplace = exp(-abs(x - mu)/bpos)/2/bpos + +u = Symbol('u', polar=True) +tpos = Symbol('t', positive=True) + + +def E(expr): + integrate(expr*exponential(x, rate)*normal(y, mu1, sigma1), + (x, 0, oo), (y, -oo, oo), meijerg=True) + integrate(expr*exponential(x, rate)*normal(y, mu1, sigma1), + (y, -oo, oo), (x, 0, oo), meijerg=True) + +bench = [ + 'MT(x**nu*Heaviside(x - 1), x, s)', + 'MT(x**nu*Heaviside(1 - x), x, s)', + 'MT((1-x)**(beta - 1)*Heaviside(1-x), x, s)', + 'MT((x-1)**(beta - 1)*Heaviside(x-1), x, s)', + 'MT((1+x)**(-rho), x, s)', + 'MT(abs(1-x)**(-rho), x, s)', + 'MT((1-x)**(beta-1)*Heaviside(1-x) + a*(x-1)**(beta-1)*Heaviside(x-1), x, s)', + 'MT((x**a-b**a)/(x-b), x, s)', + 'MT((x**a-bpos**a)/(x-bpos), x, s)', + 'MT(exp(-x), x, s)', + 'MT(exp(-1/x), x, s)', + 'MT(log(x)**4*Heaviside(1-x), x, s)', + 'MT(log(x)**3*Heaviside(x-1), x, s)', + 'MT(log(x + 1), x, s)', + 'MT(log(1/x + 1), x, s)', + 'MT(log(abs(1 - x)), x, s)', + 'MT(log(abs(1 - 1/x)), x, s)', + 'MT(log(x)/(x+1), x, s)', + 'MT(log(x)**2/(x+1), x, s)', + 'MT(log(x)/(x+1)**2, x, s)', + 'MT(erf(sqrt(x)), x, s)', + + 'MT(besselj(a, 2*sqrt(x)), x, s)', + 'MT(sin(sqrt(x))*besselj(a, sqrt(x)), x, s)', + 'MT(cos(sqrt(x))*besselj(a, sqrt(x)), x, s)', + 'MT(besselj(a, sqrt(x))**2, x, s)', + 'MT(besselj(a, sqrt(x))*besselj(-a, sqrt(x)), x, s)', + 'MT(besselj(a - 1, sqrt(x))*besselj(a, sqrt(x)), x, s)', + 'MT(besselj(a, sqrt(x))*besselj(b, sqrt(x)), x, s)', + 'MT(besselj(a, sqrt(x))**2 + besselj(-a, sqrt(x))**2, x, s)', + 'MT(bessely(a, 2*sqrt(x)), x, s)', + 'MT(sin(sqrt(x))*bessely(a, sqrt(x)), x, s)', + 'MT(cos(sqrt(x))*bessely(a, sqrt(x)), x, s)', + 'MT(besselj(a, sqrt(x))*bessely(a, sqrt(x)), x, s)', + 'MT(besselj(a, sqrt(x))*bessely(b, sqrt(x)), x, s)', + 'MT(bessely(a, sqrt(x))**2, x, s)', + + 'MT(besselk(a, 2*sqrt(x)), x, s)', + 'MT(besselj(a, 2*sqrt(2*sqrt(x)))*besselk(a, 2*sqrt(2*sqrt(x))), x, s)', + 'MT(besseli(a, sqrt(x))*besselk(a, sqrt(x)), x, s)', + 'MT(besseli(b, sqrt(x))*besselk(a, sqrt(x)), x, s)', + 'MT(exp(-x/2)*besselk(a, x/2), x, s)', + + # later: ILT, IMT + + 'LT((t-apos)**bpos*exp(-cpos*(t-apos))*Heaviside(t-apos), t, s)', + 'LT(t**apos, t, s)', + 'LT(Heaviside(t), t, s)', + 'LT(Heaviside(t - apos), t, s)', + 'LT(1 - exp(-apos*t), t, s)', + 'LT((exp(2*t)-1)*exp(-bpos - t)*Heaviside(t)/2, t, s, noconds=True)', + 'LT(exp(t), t, s)', + 'LT(exp(2*t), t, s)', + 'LT(exp(apos*t), t, s)', + 'LT(log(t/apos), t, s)', + 'LT(erf(t), t, s)', + 'LT(sin(apos*t), t, s)', + 'LT(cos(apos*t), t, s)', + 'LT(exp(-apos*t)*sin(bpos*t), t, s)', + 'LT(exp(-apos*t)*cos(bpos*t), t, s)', + 'LT(besselj(0, t), t, s, noconds=True)', + 'LT(besselj(1, t), t, s, noconds=True)', + + 'FT(Heaviside(1 - abs(2*apos*x)), x, k)', + 'FT(Heaviside(1-abs(apos*x))*(1-abs(apos*x)), x, k)', + 'FT(exp(-apos*x)*Heaviside(x), x, k)', + 'IFT(1/(apos + 2*pi*I*x), x, posk, noconds=False)', + 'IFT(1/(apos + 2*pi*I*x), x, -posk, noconds=False)', + 'IFT(1/(apos + 2*pi*I*x), x, negk)', + 'FT(x*exp(-apos*x)*Heaviside(x), x, k)', + 'FT(exp(-apos*x)*sin(bpos*x)*Heaviside(x), x, k)', + 'FT(exp(-apos*x**2), x, k)', + 'IFT(sqrt(pi/apos)*exp(-(pi*k)**2/apos), k, x)', + 'FT(exp(-apos*abs(x)), x, k)', + + 'integrate(normal(x, mu1, sigma1), (x, -oo, oo), meijerg=True)', + 'integrate(x*normal(x, mu1, sigma1), (x, -oo, oo), meijerg=True)', + 'integrate(x**2*normal(x, mu1, sigma1), (x, -oo, oo), meijerg=True)', + 'integrate(x**3*normal(x, mu1, sigma1), (x, -oo, oo), meijerg=True)', + 'integrate(normal(x, mu1, sigma1)*normal(y, mu2, sigma2),' + ' (x, -oo, oo), (y, -oo, oo), meijerg=True)', + 'integrate(x*normal(x, mu1, sigma1)*normal(y, mu2, sigma2),' + ' (x, -oo, oo), (y, -oo, oo), meijerg=True)', + 'integrate(y*normal(x, mu1, sigma1)*normal(y, mu2, sigma2),' + ' (x, -oo, oo), (y, -oo, oo), meijerg=True)', + 'integrate(x*y*normal(x, mu1, sigma1)*normal(y, mu2, sigma2),' + ' (x, -oo, oo), (y, -oo, oo), meijerg=True)', + 'integrate((x+y+1)*normal(x, mu1, sigma1)*normal(y, mu2, sigma2),' + ' (x, -oo, oo), (y, -oo, oo), meijerg=True)', + 'integrate((x+y-1)*normal(x, mu1, sigma1)*normal(y, mu2, sigma2),' + ' (x, -oo, oo), (y, -oo, oo), meijerg=True)', + 'integrate(x**2*normal(x, mu1, sigma1)*normal(y, mu2, sigma2),' + ' (x, -oo, oo), (y, -oo, oo), meijerg=True)', + 'integrate(y**2*normal(x, mu1, sigma1)*normal(y, mu2, sigma2),' + ' (x, -oo, oo), (y, -oo, oo), meijerg=True)', + 'integrate(exponential(x, rate), (x, 0, oo), meijerg=True)', + 'integrate(x*exponential(x, rate), (x, 0, oo), meijerg=True)', + 'integrate(x**2*exponential(x, rate), (x, 0, oo), meijerg=True)', + 'E(1)', + 'E(x*y)', + 'E(x*y**2)', + 'E((x+y+1)**2)', + 'E(x+y+1)', + 'E((x+y-1)**2)', + 'integrate(betadist, (x, 0, oo), meijerg=True)', + 'integrate(x*betadist, (x, 0, oo), meijerg=True)', + 'integrate(x**2*betadist, (x, 0, oo), meijerg=True)', + 'integrate(chi, (x, 0, oo), meijerg=True)', + 'integrate(x*chi, (x, 0, oo), meijerg=True)', + 'integrate(x**2*chi, (x, 0, oo), meijerg=True)', + 'integrate(chisquared, (x, 0, oo), meijerg=True)', + 'integrate(x*chisquared, (x, 0, oo), meijerg=True)', + 'integrate(x**2*chisquared, (x, 0, oo), meijerg=True)', + 'integrate(((x-k)/sqrt(2*k))**3*chisquared, (x, 0, oo), meijerg=True)', + 'integrate(dagum, (x, 0, oo), meijerg=True)', + 'integrate(x*dagum, (x, 0, oo), meijerg=True)', + 'integrate(x**2*dagum, (x, 0, oo), meijerg=True)', + 'integrate(f, (x, 0, oo), meijerg=True)', + 'integrate(x*f, (x, 0, oo), meijerg=True)', + 'integrate(x**2*f, (x, 0, oo), meijerg=True)', + 'integrate(rice, (x, 0, oo), meijerg=True)', + 'integrate(laplace, (x, -oo, oo), meijerg=True)', + 'integrate(x*laplace, (x, -oo, oo), meijerg=True)', + 'integrate(x**2*laplace, (x, -oo, oo), meijerg=True)', + 'integrate(log(x) * x**(k-1) * exp(-x) / gamma(k), (x, 0, oo))', + + 'integrate(sin(z*x)*(x**2-1)**(-(y+S(1)/2)), (x, 1, oo), meijerg=True)', + 'integrate(besselj(0,x)*besselj(1,x)*exp(-x**2), (x, 0, oo), meijerg=True)', + 'integrate(besselj(0,x)*besselj(1,x)*besselk(0,x), (x, 0, oo), meijerg=True)', + 'integrate(besselj(0,x)*besselj(1,x)*exp(-x**2), (x, 0, oo), meijerg=True)', + 'integrate(besselj(a,x)*besselj(b,x)/x, (x,0,oo), meijerg=True)', + + 'hyperexpand(meijerg((-s - a/2 + 1, -s + a/2 + 1), (-a/2 - S(1)/2, -s + a/2 + S(3)/2), (a/2, -a/2), (-a/2 - S(1)/2, -s + a/2 + S(3)/2), 1))', + "gammasimp(S('2**(2*s)*(-pi*gamma(-a + 1)*gamma(a + 1)*gamma(-a - s + 1)*gamma(-a + s - 1/2)*gamma(a - s + 3/2)*gamma(a + s + 1)/(a*(a + s)) - gamma(-a - 1/2)*gamma(-a + 1)*gamma(a + 1)*gamma(a + 3/2)*gamma(-s + 3/2)*gamma(s - 1/2)*gamma(-a + s + 1)*gamma(a - s + 1)/(a*(-a + s)))*gamma(-2*s + 1)*gamma(s + 1)/(pi*s*gamma(-a - 1/2)*gamma(a + 3/2)*gamma(-s + 1)*gamma(-s + 3/2)*gamma(s - 1/2)*gamma(-a - s + 1)*gamma(-a + s - 1/2)*gamma(a - s + 1)*gamma(a - s + 3/2))'))", + + 'mellin_transform(E1(x), x, s)', + 'inverse_mellin_transform(gamma(s)/s, s, x, (0, oo))', + 'mellin_transform(expint(a, x), x, s)', + 'mellin_transform(Si(x), x, s)', + 'inverse_mellin_transform(-2**s*sqrt(pi)*gamma((s + 1)/2)/(2*s*gamma(-s/2 + 1)), s, x, (-1, 0))', + 'mellin_transform(Ci(sqrt(x)), x, s)', + 'inverse_mellin_transform(-4**s*sqrt(pi)*gamma(s)/(2*s*gamma(-s + S(1)/2)),s, u, (0, 1))', + 'laplace_transform(Ci(x), x, s)', + 'laplace_transform(expint(a, x), x, s)', + 'laplace_transform(expint(1, x), x, s)', + 'laplace_transform(expint(2, x), x, s)', + 'inverse_laplace_transform(-log(1 + s**2)/2/s, s, u)', + 'inverse_laplace_transform(log(s + 1)/s, s, x)', + 'inverse_laplace_transform((s - log(s + 1))/s**2, s, x)', + 'laplace_transform(Chi(x), x, s)', + 'laplace_transform(Shi(x), x, s)', + + 'integrate(exp(-z*x)/x, (x, 1, oo), meijerg=True, conds="none")', + 'integrate(exp(-z*x)/x**2, (x, 1, oo), meijerg=True, conds="none")', + 'integrate(exp(-z*x)/x**3, (x, 1, oo), meijerg=True,conds="none")', + 'integrate(-cos(x)/x, (x, tpos, oo), meijerg=True)', + 'integrate(-sin(x)/x, (x, tpos, oo), meijerg=True)', + 'integrate(sin(x)/x, (x, 0, z), meijerg=True)', + 'integrate(sinh(x)/x, (x, 0, z), meijerg=True)', + 'integrate(exp(-x)/x, x, meijerg=True)', + 'integrate(exp(-x)/x**2, x, meijerg=True)', + 'integrate(cos(u)/u, u, meijerg=True)', + 'integrate(cosh(u)/u, u, meijerg=True)', + 'integrate(expint(1, x), x, meijerg=True)', + 'integrate(expint(2, x), x, meijerg=True)', + 'integrate(Si(x), x, meijerg=True)', + 'integrate(Ci(u), u, meijerg=True)', + 'integrate(Shi(x), x, meijerg=True)', + 'integrate(Chi(u), u, meijerg=True)', + 'integrate(Si(x)*exp(-x), (x, 0, oo), meijerg=True)', + 'integrate(expint(1, x)*sin(x), (x, 0, oo), meijerg=True)' +] + +from time import time +from sympy.core.cache import clear_cache +import sys + +timings = [] + +if __name__ == '__main__': + for n, string in enumerate(bench): + clear_cache() + _t = time() + exec(string) + _t = time() - _t + timings += [(_t, string)] + sys.stdout.write('.') + sys.stdout.flush() + if n % (len(bench) // 10) == 0: + sys.stdout.write('%s' % (10*n // len(bench))) + print() + + timings.sort(key=lambda x: -x[0]) + + for ti, string in timings: + print('%.2fs %s' % (ti, string)) diff --git a/venv/lib/python3.11/site-packages/sympy/benchmarks/bench_symbench.py b/venv/lib/python3.11/site-packages/sympy/benchmarks/bench_symbench.py new file mode 100644 index 0000000000000000000000000000000000000000..8ea700b44b677107f5345196a8895e8ed5a9d56d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/benchmarks/bench_symbench.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +from sympy.core.random import random +from sympy.core.numbers import (I, Integer, pi) +from sympy.core.symbol import Symbol +from sympy.core.sympify import sympify +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin +from sympy.polys.polytools import factor +from sympy.simplify.simplify import simplify +from sympy.abc import x, y, z +from timeit import default_timer as clock + + +def bench_R1(): + "real(f(f(f(f(f(f(f(f(f(f(i/2)))))))))))" + def f(z): + return sqrt(Integer(1)/3)*z**2 + I/3 + f(f(f(f(f(f(f(f(f(f(I/2)))))))))).as_real_imag()[0] + + +def bench_R2(): + "Hermite polynomial hermite(15, y)" + def hermite(n, y): + if n == 1: + return 2*y + if n == 0: + return 1 + return (2*y*hermite(n - 1, y) - 2*(n - 1)*hermite(n - 2, y)).expand() + + hermite(15, y) + + +def bench_R3(): + "a = [bool(f==f) for _ in range(10)]" + f = x + y + z + [bool(f == f) for _ in range(10)] + + +def bench_R4(): + # we don't have Tuples + pass + + +def bench_R5(): + "blowup(L, 8); L=uniq(L)" + def blowup(L, n): + for i in range(n): + L.append( (L[i] + L[i + 1]) * L[i + 2] ) + + def uniq(x): + v = set(x) + return v + L = [x, y, z] + blowup(L, 8) + L = uniq(L) + + +def bench_R6(): + "sum(simplify((x+sin(i))/x+(x-sin(i))/x) for i in range(100))" + sum(simplify((x + sin(i))/x + (x - sin(i))/x) for i in range(100)) + + +def bench_R7(): + "[f.subs(x, random()) for _ in range(10**4)]" + f = x**24 + 34*x**12 + 45*x**3 + 9*x**18 + 34*x**10 + 32*x**21 + [f.subs(x, random()) for _ in range(10**4)] + + +def bench_R8(): + "right(x^2,0,5,10^4)" + def right(f, a, b, n): + a = sympify(a) + b = sympify(b) + n = sympify(n) + x = f.atoms(Symbol).pop() + Deltax = (b - a)/n + c = a + est = 0 + for i in range(n): + c += Deltax + est += f.subs(x, c) + return est*Deltax + + right(x**2, 0, 5, 10**4) + + +def _bench_R9(): + "factor(x^20 - pi^5*y^20)" + factor(x**20 - pi**5*y**20) + + +def bench_R10(): + "v = [-pi,-pi+1/10..,pi]" + def srange(min, max, step): + v = [min] + while (max - v[-1]).evalf() > 0: + v.append(v[-1] + step) + return v[:-1] + srange(-pi, pi, sympify(1)/10) + + +def bench_R11(): + "a = [random() + random()*I for w in [0..1000]]" + [random() + random()*I for w in range(1000)] + + +def bench_S1(): + "e=(x+y+z+1)**7;f=e*(e+1);f.expand()" + e = (x + y + z + 1)**7 + f = e*(e + 1) + f.expand() + + +if __name__ == '__main__': + benchmarks = [ + bench_R1, + bench_R2, + bench_R3, + bench_R5, + bench_R6, + bench_R7, + bench_R8, + #_bench_R9, + bench_R10, + bench_R11, + #bench_S1, + ] + + report = [] + for b in benchmarks: + t = clock() + b() + t = clock() - t + print("%s%65s: %f" % (b.__name__, b.__doc__, t)) diff --git a/venv/lib/python3.11/site-packages/sympy/calculus/__init__.py b/venv/lib/python3.11/site-packages/sympy/calculus/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..865c0556769e35ca7e8a09a4c208a1cfbd17369c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/calculus/__init__.py @@ -0,0 +1,25 @@ +"""Calculus-related methods.""" + +from .euler import euler_equations +from .singularities import (singularities, is_increasing, + is_strictly_increasing, is_decreasing, + is_strictly_decreasing, is_monotonic) +from .finite_diff import finite_diff_weights, apply_finite_diff, differentiate_finite +from .util import (periodicity, not_empty_in, is_convex, + stationary_points, minimum, maximum) +from .accumulationbounds import AccumBounds + +__all__ = [ +'euler_equations', + +'singularities', 'is_increasing', +'is_strictly_increasing', 'is_decreasing', +'is_strictly_decreasing', 'is_monotonic', + +'finite_diff_weights', 'apply_finite_diff', 'differentiate_finite', + +'periodicity', 'not_empty_in', 'is_convex', 'stationary_points', +'minimum', 'maximum', + +'AccumBounds' +] diff --git a/venv/lib/python3.11/site-packages/sympy/calculus/accumulationbounds.py b/venv/lib/python3.11/site-packages/sympy/calculus/accumulationbounds.py new file mode 100644 index 0000000000000000000000000000000000000000..8a2b0e634da1028c25399c8b03884865d981a56e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/calculus/accumulationbounds.py @@ -0,0 +1,804 @@ +from sympy.core import Add, Mul, Pow, S +from sympy.core.basic import Basic +from sympy.core.expr import Expr +from sympy.core.numbers import _sympifyit, oo, zoo +from sympy.core.relational import is_le, is_lt, is_ge, is_gt +from sympy.core.sympify import _sympify +from sympy.functions.elementary.miscellaneous import Min, Max +from sympy.logic.boolalg import And +from sympy.multipledispatch import dispatch +from sympy.series.order import Order +from sympy.sets.sets import FiniteSet + + +class AccumulationBounds(Expr): + r"""An accumulation bounds. + + # Note AccumulationBounds has an alias: AccumBounds + + AccumulationBounds represent an interval `[a, b]`, which is always closed + at the ends. Here `a` and `b` can be any value from extended real numbers. + + The intended meaning of AccummulationBounds is to give an approximate + location of the accumulation points of a real function at a limit point. + + Let `a` and `b` be reals such that `a \le b`. + + `\left\langle a, b\right\rangle = \{x \in \mathbb{R} \mid a \le x \le b\}` + + `\left\langle -\infty, b\right\rangle = \{x \in \mathbb{R} \mid x \le b\} \cup \{-\infty, \infty\}` + + `\left\langle a, \infty \right\rangle = \{x \in \mathbb{R} \mid a \le x\} \cup \{-\infty, \infty\}` + + `\left\langle -\infty, \infty \right\rangle = \mathbb{R} \cup \{-\infty, \infty\}` + + ``oo`` and ``-oo`` are added to the second and third definition respectively, + since if either ``-oo`` or ``oo`` is an argument, then the other one should + be included (though not as an end point). This is forced, since we have, + for example, ``1/AccumBounds(0, 1) = AccumBounds(1, oo)``, and the limit at + `0` is not one-sided. As `x` tends to `0-`, then `1/x \rightarrow -\infty`, so `-\infty` + should be interpreted as belonging to ``AccumBounds(1, oo)`` though it need + not appear explicitly. + + In many cases it suffices to know that the limit set is bounded. + However, in some other cases more exact information could be useful. + For example, all accumulation values of `\cos(x) + 1` are non-negative. + (``AccumBounds(-1, 1) + 1 = AccumBounds(0, 2)``) + + A AccumulationBounds object is defined to be real AccumulationBounds, + if its end points are finite reals. + + Let `X`, `Y` be real AccumulationBounds, then their sum, difference, + product are defined to be the following sets: + + `X + Y = \{ x+y \mid x \in X \cap y \in Y\}` + + `X - Y = \{ x-y \mid x \in X \cap y \in Y\}` + + `X \times Y = \{ x \times y \mid x \in X \cap y \in Y\}` + + When an AccumBounds is raised to a negative power, if 0 is contained + between the bounds then an infinite range is returned, otherwise if an + endpoint is 0 then a semi-infinite range with consistent sign will be returned. + + AccumBounds in expressions behave a lot like Intervals but the + semantics are not necessarily the same. Division (or exponentiation + to a negative integer power) could be handled with *intervals* by + returning a union of the results obtained after splitting the + bounds between negatives and positives, but that is not done with + AccumBounds. In addition, bounds are assumed to be independent of + each other; if the same bound is used in more than one place in an + expression, the result may not be the supremum or infimum of the + expression (see below). Finally, when a boundary is ``1``, + exponentiation to the power of ``oo`` yields ``oo``, neither + ``1`` nor ``nan``. + + Examples + ======== + + >>> from sympy import AccumBounds, sin, exp, log, pi, E, S, oo + >>> from sympy.abc import x + + >>> AccumBounds(0, 1) + AccumBounds(1, 2) + AccumBounds(1, 3) + + >>> AccumBounds(0, 1) - AccumBounds(0, 2) + AccumBounds(-2, 1) + + >>> AccumBounds(-2, 3)*AccumBounds(-1, 1) + AccumBounds(-3, 3) + + >>> AccumBounds(1, 2)*AccumBounds(3, 5) + AccumBounds(3, 10) + + The exponentiation of AccumulationBounds is defined + as follows: + + If 0 does not belong to `X` or `n > 0` then + + `X^n = \{ x^n \mid x \in X\}` + + >>> AccumBounds(1, 4)**(S(1)/2) + AccumBounds(1, 2) + + otherwise, an infinite or semi-infinite result is obtained: + + >>> 1/AccumBounds(-1, 1) + AccumBounds(-oo, oo) + >>> 1/AccumBounds(0, 2) + AccumBounds(1/2, oo) + >>> 1/AccumBounds(-oo, 0) + AccumBounds(-oo, 0) + + A boundary of 1 will always generate all nonnegatives: + + >>> AccumBounds(1, 2)**oo + AccumBounds(0, oo) + >>> AccumBounds(0, 1)**oo + AccumBounds(0, oo) + + If the exponent is itself an AccumulationBounds or is not an + integer then unevaluated results will be returned unless the base + values are positive: + + >>> AccumBounds(2, 3)**AccumBounds(-1, 2) + AccumBounds(1/3, 9) + >>> AccumBounds(-2, 3)**AccumBounds(-1, 2) + AccumBounds(-2, 3)**AccumBounds(-1, 2) + + >>> AccumBounds(-2, -1)**(S(1)/2) + sqrt(AccumBounds(-2, -1)) + + Note: `\left\langle a, b\right\rangle^2` is not same as `\left\langle a, b\right\rangle \times \left\langle a, b\right\rangle` + + >>> AccumBounds(-1, 1)**2 + AccumBounds(0, 1) + + >>> AccumBounds(1, 3) < 4 + True + + >>> AccumBounds(1, 3) < -1 + False + + Some elementary functions can also take AccumulationBounds as input. + A function `f` evaluated for some real AccumulationBounds `\left\langle a, b \right\rangle` + is defined as `f(\left\langle a, b\right\rangle) = \{ f(x) \mid a \le x \le b \}` + + >>> sin(AccumBounds(pi/6, pi/3)) + AccumBounds(1/2, sqrt(3)/2) + + >>> exp(AccumBounds(0, 1)) + AccumBounds(1, E) + + >>> log(AccumBounds(1, E)) + AccumBounds(0, 1) + + Some symbol in an expression can be substituted for a AccumulationBounds + object. But it does not necessarily evaluate the AccumulationBounds for + that expression. + + The same expression can be evaluated to different values depending upon + the form it is used for substitution since each instance of an + AccumulationBounds is considered independent. For example: + + >>> (x**2 + 2*x + 1).subs(x, AccumBounds(-1, 1)) + AccumBounds(-1, 4) + + >>> ((x + 1)**2).subs(x, AccumBounds(-1, 1)) + AccumBounds(0, 4) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Interval_arithmetic + + .. [2] https://fab.cba.mit.edu/classes/S62.12/docs/Hickey_interval.pdf + + Notes + ===== + + Do not use ``AccumulationBounds`` for floating point interval arithmetic + calculations, use ``mpmath.iv`` instead. + """ + + is_extended_real = True + is_number = False + + def __new__(cls, min, max) -> Expr: # type: ignore + + min = _sympify(min) + max = _sympify(max) + + # Only allow real intervals (use symbols with 'is_extended_real=True'). + if not min.is_extended_real or not max.is_extended_real: + raise ValueError("Only real AccumulationBounds are supported") + + if max == min: + return max + + # Make sure that the created AccumBounds object will be valid. + if max.is_number and min.is_number: + bad = max.is_comparable and min.is_comparable and max < min + else: + bad = (max - min).is_extended_negative + if bad: + raise ValueError( + "Lower limit should be smaller than upper limit") + + return Basic.__new__(cls, min, max) + + # setting the operation priority + _op_priority = 11.0 + + def _eval_is_real(self): + if self.min.is_real and self.max.is_real: + return True + + @property + def min(self): + """ + Returns the minimum possible value attained by AccumulationBounds + object. + + Examples + ======== + + >>> from sympy import AccumBounds + >>> AccumBounds(1, 3).min + 1 + + """ + return self.args[0] + + @property + def max(self): + """ + Returns the maximum possible value attained by AccumulationBounds + object. + + Examples + ======== + + >>> from sympy import AccumBounds + >>> AccumBounds(1, 3).max + 3 + + """ + return self.args[1] + + @property + def delta(self): + """ + Returns the difference of maximum possible value attained by + AccumulationBounds object and minimum possible value attained + by AccumulationBounds object. + + Examples + ======== + + >>> from sympy import AccumBounds + >>> AccumBounds(1, 3).delta + 2 + + """ + return self.max - self.min + + @property + def mid(self): + """ + Returns the mean of maximum possible value attained by + AccumulationBounds object and minimum possible value + attained by AccumulationBounds object. + + Examples + ======== + + >>> from sympy import AccumBounds + >>> AccumBounds(1, 3).mid + 2 + + """ + return (self.min + self.max) / 2 + + @_sympifyit('other', NotImplemented) + def _eval_power(self, other): + return self.__pow__(other) + + @_sympifyit('other', NotImplemented) + def __add__(self, other): + if isinstance(other, Expr): + if isinstance(other, AccumBounds): + return AccumBounds( + Add(self.min, other.min), + Add(self.max, other.max)) + if other is S.Infinity and self.min is S.NegativeInfinity or \ + other is S.NegativeInfinity and self.max is S.Infinity: + return AccumBounds(-oo, oo) + elif other.is_extended_real: + if self.min is S.NegativeInfinity and self.max is S.Infinity: + return AccumBounds(-oo, oo) + elif self.min is S.NegativeInfinity: + return AccumBounds(-oo, self.max + other) + elif self.max is S.Infinity: + return AccumBounds(self.min + other, oo) + else: + return AccumBounds(Add(self.min, other), Add(self.max, other)) + return Add(self, other, evaluate=False) + return NotImplemented + + __radd__ = __add__ + + def __neg__(self): + return AccumBounds(-self.max, -self.min) + + @_sympifyit('other', NotImplemented) + def __sub__(self, other): + if isinstance(other, Expr): + if isinstance(other, AccumBounds): + return AccumBounds( + Add(self.min, -other.max), + Add(self.max, -other.min)) + if other is S.NegativeInfinity and self.min is S.NegativeInfinity or \ + other is S.Infinity and self.max is S.Infinity: + return AccumBounds(-oo, oo) + elif other.is_extended_real: + if self.min is S.NegativeInfinity and self.max is S.Infinity: + return AccumBounds(-oo, oo) + elif self.min is S.NegativeInfinity: + return AccumBounds(-oo, self.max - other) + elif self.max is S.Infinity: + return AccumBounds(self.min - other, oo) + else: + return AccumBounds( + Add(self.min, -other), + Add(self.max, -other)) + return Add(self, -other, evaluate=False) + return NotImplemented + + @_sympifyit('other', NotImplemented) + def __rsub__(self, other): + return self.__neg__() + other + + @_sympifyit('other', NotImplemented) + def __mul__(self, other): + if self.args == (-oo, oo): + return self + if isinstance(other, Expr): + if isinstance(other, AccumBounds): + if other.args == (-oo, oo): + return other + v = set() + for a in self.args: + vi = other*a + v.update(vi.args or (vi,)) + return AccumBounds(Min(*v), Max(*v)) + if other is S.Infinity: + if self.min.is_zero: + return AccumBounds(0, oo) + if self.max.is_zero: + return AccumBounds(-oo, 0) + if other is S.NegativeInfinity: + if self.min.is_zero: + return AccumBounds(-oo, 0) + if self.max.is_zero: + return AccumBounds(0, oo) + if other.is_extended_real: + if other.is_zero: + if self.max is S.Infinity: + return AccumBounds(0, oo) + if self.min is S.NegativeInfinity: + return AccumBounds(-oo, 0) + return S.Zero + if other.is_extended_positive: + return AccumBounds( + Mul(self.min, other), + Mul(self.max, other)) + elif other.is_extended_negative: + return AccumBounds( + Mul(self.max, other), + Mul(self.min, other)) + if isinstance(other, Order): + return other + return Mul(self, other, evaluate=False) + return NotImplemented + + __rmul__ = __mul__ + + @_sympifyit('other', NotImplemented) + def __truediv__(self, other): + if isinstance(other, Expr): + if isinstance(other, AccumBounds): + if other.min.is_positive or other.max.is_negative: + return self * AccumBounds(1/other.max, 1/other.min) + + if (self.min.is_extended_nonpositive and self.max.is_extended_nonnegative and + other.min.is_extended_nonpositive and other.max.is_extended_nonnegative): + if self.min.is_zero and other.min.is_zero: + return AccumBounds(0, oo) + if self.max.is_zero and other.min.is_zero: + return AccumBounds(-oo, 0) + return AccumBounds(-oo, oo) + + if self.max.is_extended_negative: + if other.min.is_extended_negative: + if other.max.is_zero: + return AccumBounds(self.max / other.min, oo) + if other.max.is_extended_positive: + # if we were dealing with intervals we would return + # Union(Interval(-oo, self.max/other.max), + # Interval(self.max/other.min, oo)) + return AccumBounds(-oo, oo) + + if other.min.is_zero and other.max.is_extended_positive: + return AccumBounds(-oo, self.max / other.max) + + if self.min.is_extended_positive: + if other.min.is_extended_negative: + if other.max.is_zero: + return AccumBounds(-oo, self.min / other.min) + if other.max.is_extended_positive: + # if we were dealing with intervals we would return + # Union(Interval(-oo, self.min/other.min), + # Interval(self.min/other.max, oo)) + return AccumBounds(-oo, oo) + + if other.min.is_zero and other.max.is_extended_positive: + return AccumBounds(self.min / other.max, oo) + + elif other.is_extended_real: + if other in (S.Infinity, S.NegativeInfinity): + if self == AccumBounds(-oo, oo): + return AccumBounds(-oo, oo) + if self.max is S.Infinity: + return AccumBounds(Min(0, other), Max(0, other)) + if self.min is S.NegativeInfinity: + return AccumBounds(Min(0, -other), Max(0, -other)) + if other.is_extended_positive: + return AccumBounds(self.min / other, self.max / other) + elif other.is_extended_negative: + return AccumBounds(self.max / other, self.min / other) + if (1 / other) is S.ComplexInfinity: + return Mul(self, 1 / other, evaluate=False) + else: + return Mul(self, 1 / other) + + return NotImplemented + + @_sympifyit('other', NotImplemented) + def __rtruediv__(self, other): + if isinstance(other, Expr): + if other.is_extended_real: + if other.is_zero: + return S.Zero + if (self.min.is_extended_nonpositive and self.max.is_extended_nonnegative): + if self.min.is_zero: + if other.is_extended_positive: + return AccumBounds(Mul(other, 1 / self.max), oo) + if other.is_extended_negative: + return AccumBounds(-oo, Mul(other, 1 / self.max)) + if self.max.is_zero: + if other.is_extended_positive: + return AccumBounds(-oo, Mul(other, 1 / self.min)) + if other.is_extended_negative: + return AccumBounds(Mul(other, 1 / self.min), oo) + return AccumBounds(-oo, oo) + else: + return AccumBounds(Min(other / self.min, other / self.max), + Max(other / self.min, other / self.max)) + return Mul(other, 1 / self, evaluate=False) + else: + return NotImplemented + + @_sympifyit('other', NotImplemented) + def __pow__(self, other): + if isinstance(other, Expr): + if other is S.Infinity: + if self.min.is_extended_nonnegative: + if self.max < 1: + return S.Zero + if self.min > 1: + return S.Infinity + return AccumBounds(0, oo) + elif self.max.is_extended_negative: + if self.min > -1: + return S.Zero + if self.max < -1: + return zoo + return S.NaN + else: + if self.min > -1: + if self.max < 1: + return S.Zero + return AccumBounds(0, oo) + return AccumBounds(-oo, oo) + + if other is S.NegativeInfinity: + return (1/self)**oo + + # generically true + if (self.max - self.min).is_nonnegative: + # well defined + if self.min.is_nonnegative: + # no 0 to worry about + if other.is_nonnegative: + # no infinity to worry about + return self.func(self.min**other, self.max**other) + + if other.is_zero: + return S.One # x**0 = 1 + + if other.is_Integer or other.is_integer: + if self.min.is_extended_positive: + return AccumBounds( + Min(self.min**other, self.max**other), + Max(self.min**other, self.max**other)) + elif self.max.is_extended_negative: + return AccumBounds( + Min(self.max**other, self.min**other), + Max(self.max**other, self.min**other)) + + if other % 2 == 0: + if other.is_extended_negative: + if self.min.is_zero: + return AccumBounds(self.max**other, oo) + if self.max.is_zero: + return AccumBounds(self.min**other, oo) + return (1/self)**(-other) + return AccumBounds( + S.Zero, Max(self.min**other, self.max**other)) + elif other % 2 == 1: + if other.is_extended_negative: + if self.min.is_zero: + return AccumBounds(self.max**other, oo) + if self.max.is_zero: + return AccumBounds(-oo, self.min**other) + return (1/self)**(-other) + return AccumBounds(self.min**other, self.max**other) + + # non-integer exponent + # 0**neg or neg**frac yields complex + if (other.is_number or other.is_rational) and ( + self.min.is_extended_nonnegative or ( + other.is_extended_nonnegative and + self.min.is_extended_nonnegative)): + num, den = other.as_numer_denom() + if num is S.One: + return AccumBounds(*[i**(1/den) for i in self.args]) + + elif den is not S.One: # e.g. if other is not Float + return (self**num)**(1/den) # ok for non-negative base + + if isinstance(other, AccumBounds): + if (self.min.is_extended_positive or + self.min.is_extended_nonnegative and + other.min.is_extended_nonnegative): + p = [self**i for i in other.args] + if not any(i.is_Pow for i in p): + a = [j for i in p for j in i.args or (i,)] + try: + return self.func(min(a), max(a)) + except TypeError: # can't sort + pass + + return Pow(self, other, evaluate=False) + + return NotImplemented + + @_sympifyit('other', NotImplemented) + def __rpow__(self, other): + if other.is_real and other.is_extended_nonnegative and ( + self.max - self.min).is_extended_positive: + if other is S.One: + return S.One + if other.is_extended_positive: + a, b = [other**i for i in self.args] + if min(a, b) != a: + a, b = b, a + return self.func(a, b) + if other.is_zero: + if self.min.is_zero: + return self.func(0, 1) + if self.min.is_extended_positive: + return S.Zero + + return Pow(other, self, evaluate=False) + + def __abs__(self): + if self.max.is_extended_negative: + return self.__neg__() + elif self.min.is_extended_negative: + return AccumBounds(S.Zero, Max(abs(self.min), self.max)) + else: + return self + + + def __contains__(self, other): + """ + Returns ``True`` if other is contained in self, where other + belongs to extended real numbers, ``False`` if not contained, + otherwise TypeError is raised. + + Examples + ======== + + >>> from sympy import AccumBounds, oo + >>> 1 in AccumBounds(-1, 3) + True + + -oo and oo go together as limits (in AccumulationBounds). + + >>> -oo in AccumBounds(1, oo) + True + + >>> oo in AccumBounds(-oo, 0) + True + + """ + other = _sympify(other) + + if other in (S.Infinity, S.NegativeInfinity): + if self.min is S.NegativeInfinity or self.max is S.Infinity: + return True + return False + + rv = And(self.min <= other, self.max >= other) + if rv not in (True, False): + raise TypeError("input failed to evaluate") + return rv + + def intersection(self, other): + """ + Returns the intersection of 'self' and 'other'. + Here other can be an instance of :py:class:`~.FiniteSet` or AccumulationBounds. + + Parameters + ========== + + other : AccumulationBounds + Another AccumulationBounds object with which the intersection + has to be computed. + + Returns + ======= + + AccumulationBounds + Intersection of ``self`` and ``other``. + + Examples + ======== + + >>> from sympy import AccumBounds, FiniteSet + >>> AccumBounds(1, 3).intersection(AccumBounds(2, 4)) + AccumBounds(2, 3) + + >>> AccumBounds(1, 3).intersection(AccumBounds(4, 6)) + EmptySet + + >>> AccumBounds(1, 4).intersection(FiniteSet(1, 2, 5)) + {1, 2} + + """ + if not isinstance(other, (AccumBounds, FiniteSet)): + raise TypeError( + "Input must be AccumulationBounds or FiniteSet object") + + if isinstance(other, FiniteSet): + fin_set = S.EmptySet + for i in other: + if i in self: + fin_set = fin_set + FiniteSet(i) + return fin_set + + if self.max < other.min or self.min > other.max: + return S.EmptySet + + if self.min <= other.min: + if self.max <= other.max: + return AccumBounds(other.min, self.max) + if self.max > other.max: + return other + + if other.min <= self.min: + if other.max < self.max: + return AccumBounds(self.min, other.max) + if other.max > self.max: + return self + + def union(self, other): + # TODO : Devise a better method for Union of AccumBounds + # this method is not actually correct and + # can be made better + if not isinstance(other, AccumBounds): + raise TypeError( + "Input must be AccumulationBounds or FiniteSet object") + + if self.min <= other.min and self.max >= other.min: + return AccumBounds(self.min, Max(self.max, other.max)) + + if other.min <= self.min and other.max >= self.min: + return AccumBounds(other.min, Max(self.max, other.max)) + + +@dispatch(AccumulationBounds, AccumulationBounds) # type: ignore # noqa:F811 +def _eval_is_le(lhs, rhs): # noqa:F811 + if is_le(lhs.max, rhs.min): + return True + if is_gt(lhs.min, rhs.max): + return False + + +@dispatch(AccumulationBounds, Basic) # type: ignore # noqa:F811 +def _eval_is_le(lhs, rhs): # noqa: F811 + + """ + Returns ``True `` if range of values attained by ``lhs`` AccumulationBounds + object is greater than the range of values attained by ``rhs``, + where ``rhs`` may be any value of type AccumulationBounds object or + extended real number value, ``False`` if ``rhs`` satisfies + the same property, else an unevaluated :py:class:`~.Relational`. + + Examples + ======== + + >>> from sympy import AccumBounds, oo + >>> AccumBounds(1, 3) > AccumBounds(4, oo) + False + >>> AccumBounds(1, 4) > AccumBounds(3, 4) + AccumBounds(1, 4) > AccumBounds(3, 4) + >>> AccumBounds(1, oo) > -1 + True + + """ + if not rhs.is_extended_real: + raise TypeError( + "Invalid comparison of %s %s" % + (type(rhs), rhs)) + elif rhs.is_comparable: + if is_le(lhs.max, rhs): + return True + if is_gt(lhs.min, rhs): + return False + + +@dispatch(AccumulationBounds, AccumulationBounds) +def _eval_is_ge(lhs, rhs): # noqa:F811 + if is_ge(lhs.min, rhs.max): + return True + if is_lt(lhs.max, rhs.min): + return False + + +@dispatch(AccumulationBounds, Expr) # type:ignore +def _eval_is_ge(lhs, rhs): # noqa: F811 + """ + Returns ``True`` if range of values attained by ``lhs`` AccumulationBounds + object is less that the range of values attained by ``rhs``, where + other may be any value of type AccumulationBounds object or extended + real number value, ``False`` if ``rhs`` satisfies the same + property, else an unevaluated :py:class:`~.Relational`. + + Examples + ======== + + >>> from sympy import AccumBounds, oo + >>> AccumBounds(1, 3) >= AccumBounds(4, oo) + False + >>> AccumBounds(1, 4) >= AccumBounds(3, 4) + AccumBounds(1, 4) >= AccumBounds(3, 4) + >>> AccumBounds(1, oo) >= 1 + True + """ + + if not rhs.is_extended_real: + raise TypeError( + "Invalid comparison of %s %s" % + (type(rhs), rhs)) + elif rhs.is_comparable: + if is_ge(lhs.min, rhs): + return True + if is_lt(lhs.max, rhs): + return False + + +@dispatch(Expr, AccumulationBounds) # type:ignore +def _eval_is_ge(lhs, rhs): # noqa:F811 + if not lhs.is_extended_real: + raise TypeError( + "Invalid comparison of %s %s" % + (type(lhs), lhs)) + elif lhs.is_comparable: + if is_le(rhs.max, lhs): + return True + if is_gt(rhs.min, lhs): + return False + + +@dispatch(AccumulationBounds, AccumulationBounds) # type:ignore +def _eval_is_ge(lhs, rhs): # noqa:F811 + if is_ge(lhs.min, rhs.max): + return True + if is_lt(lhs.max, rhs.min): + return False + +# setting an alias for AccumulationBounds +AccumBounds = AccumulationBounds diff --git a/venv/lib/python3.11/site-packages/sympy/calculus/euler.py b/venv/lib/python3.11/site-packages/sympy/calculus/euler.py new file mode 100644 index 0000000000000000000000000000000000000000..817acf76091dfba2dba40487ca7735e307c0fc15 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/calculus/euler.py @@ -0,0 +1,108 @@ +""" +This module implements a method to find +Euler-Lagrange Equations for given Lagrangian. +""" +from itertools import combinations_with_replacement +from sympy.core.function import (Derivative, Function, diff) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.core.sympify import sympify +from sympy.utilities.iterables import iterable + + +def euler_equations(L, funcs=(), vars=()): + r""" + Find the Euler-Lagrange equations [1]_ for a given Lagrangian. + + Parameters + ========== + + L : Expr + The Lagrangian that should be a function of the functions listed + in the second argument and their derivatives. + + For example, in the case of two functions $f(x,y)$, $g(x,y)$ and + two independent variables $x$, $y$ the Lagrangian has the form: + + .. math:: L\left(f(x,y),g(x,y),\frac{\partial f(x,y)}{\partial x}, + \frac{\partial f(x,y)}{\partial y}, + \frac{\partial g(x,y)}{\partial x}, + \frac{\partial g(x,y)}{\partial y},x,y\right) + + In many cases it is not necessary to provide anything, except the + Lagrangian, it will be auto-detected (and an error raised if this + cannot be done). + + funcs : Function or an iterable of Functions + The functions that the Lagrangian depends on. The Euler equations + are differential equations for each of these functions. + + vars : Symbol or an iterable of Symbols + The Symbols that are the independent variables of the functions. + + Returns + ======= + + eqns : list of Eq + The list of differential equations, one for each function. + + Examples + ======== + + >>> from sympy import euler_equations, Symbol, Function + >>> x = Function('x') + >>> t = Symbol('t') + >>> L = (x(t).diff(t))**2/2 - x(t)**2/2 + >>> euler_equations(L, x(t), t) + [Eq(-x(t) - Derivative(x(t), (t, 2)), 0)] + >>> u = Function('u') + >>> x = Symbol('x') + >>> L = (u(t, x).diff(t))**2/2 - (u(t, x).diff(x))**2/2 + >>> euler_equations(L, u(t, x), [t, x]) + [Eq(-Derivative(u(t, x), (t, 2)) + Derivative(u(t, x), (x, 2)), 0)] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Euler%E2%80%93Lagrange_equation + + """ + + funcs = tuple(funcs) if iterable(funcs) else (funcs,) + + if not funcs: + funcs = tuple(L.atoms(Function)) + else: + for f in funcs: + if not isinstance(f, Function): + raise TypeError('Function expected, got: %s' % f) + + vars = tuple(vars) if iterable(vars) else (vars,) + + if not vars: + vars = funcs[0].args + else: + vars = tuple(sympify(var) for var in vars) + + if not all(isinstance(v, Symbol) for v in vars): + raise TypeError('Variables are not symbols, got %s' % vars) + + for f in funcs: + if not vars == f.args: + raise ValueError("Variables %s do not match args: %s" % (vars, f)) + + order = max([len(d.variables) for d in L.atoms(Derivative) + if d.expr in funcs] + [0]) + + eqns = [] + for f in funcs: + eq = diff(L, f) + for i in range(1, order + 1): + for p in combinations_with_replacement(vars, i): + eq = eq + S.NegativeOne**i*diff(L, diff(f, *p), *p) + new_eq = Eq(eq, 0) + if isinstance(new_eq, Eq): + eqns.append(new_eq) + + return eqns diff --git a/venv/lib/python3.11/site-packages/sympy/calculus/finite_diff.py b/venv/lib/python3.11/site-packages/sympy/calculus/finite_diff.py new file mode 100644 index 0000000000000000000000000000000000000000..17eece149aadad236cefeb350e1ef4a383c84f01 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/calculus/finite_diff.py @@ -0,0 +1,476 @@ +""" +Finite difference weights +========================= + +This module implements an algorithm for efficient generation of finite +difference weights for ordinary differentials of functions for +derivatives from 0 (interpolation) up to arbitrary order. + +The core algorithm is provided in the finite difference weight generating +function (``finite_diff_weights``), and two convenience functions are provided +for: + +- estimating a derivative (or interpolate) directly from a series of points + is also provided (``apply_finite_diff``). +- differentiating by using finite difference approximations + (``differentiate_finite``). + +""" + +from sympy.core.function import Derivative +from sympy.core.singleton import S +from sympy.core.function import Subs +from sympy.core.traversal import preorder_traversal +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.utilities.iterables import iterable + + + +def finite_diff_weights(order, x_list, x0=S.One): + """ + Calculates the finite difference weights for an arbitrarily spaced + one-dimensional grid (``x_list``) for derivatives at ``x0`` of order + 0, 1, ..., up to ``order`` using a recursive formula. Order of accuracy + is at least ``len(x_list) - order``, if ``x_list`` is defined correctly. + + Parameters + ========== + + order: int + Up to what derivative order weights should be calculated. + 0 corresponds to interpolation. + x_list: sequence + Sequence of (unique) values for the independent variable. + It is useful (but not necessary) to order ``x_list`` from + nearest to furthest from ``x0``; see examples below. + x0: Number or Symbol + Root or value of the independent variable for which the finite + difference weights should be generated. Default is ``S.One``. + + Returns + ======= + + list + A list of sublists, each corresponding to coefficients for + increasing derivative order, and each containing lists of + coefficients for increasing subsets of x_list. + + Examples + ======== + + >>> from sympy import finite_diff_weights, S + >>> res = finite_diff_weights(1, [-S(1)/2, S(1)/2, S(3)/2, S(5)/2], 0) + >>> res + [[[1, 0, 0, 0], + [1/2, 1/2, 0, 0], + [3/8, 3/4, -1/8, 0], + [5/16, 15/16, -5/16, 1/16]], + [[0, 0, 0, 0], + [-1, 1, 0, 0], + [-1, 1, 0, 0], + [-23/24, 7/8, 1/8, -1/24]]] + >>> res[0][-1] # FD weights for 0th derivative, using full x_list + [5/16, 15/16, -5/16, 1/16] + >>> res[1][-1] # FD weights for 1st derivative + [-23/24, 7/8, 1/8, -1/24] + >>> res[1][-2] # FD weights for 1st derivative, using x_list[:-1] + [-1, 1, 0, 0] + >>> res[1][-1][0] # FD weight for 1st deriv. for x_list[0] + -23/24 + >>> res[1][-1][1] # FD weight for 1st deriv. for x_list[1], etc. + 7/8 + + Each sublist contains the most accurate formula at the end. + Note, that in the above example ``res[1][1]`` is the same as ``res[1][2]``. + Since res[1][2] has an order of accuracy of + ``len(x_list[:3]) - order = 3 - 1 = 2``, the same is true for ``res[1][1]``! + + >>> res = finite_diff_weights(1, [S(0), S(1), -S(1), S(2), -S(2)], 0)[1] + >>> res + [[0, 0, 0, 0, 0], + [-1, 1, 0, 0, 0], + [0, 1/2, -1/2, 0, 0], + [-1/2, 1, -1/3, -1/6, 0], + [0, 2/3, -2/3, -1/12, 1/12]] + >>> res[0] # no approximation possible, using x_list[0] only + [0, 0, 0, 0, 0] + >>> res[1] # classic forward step approximation + [-1, 1, 0, 0, 0] + >>> res[2] # classic centered approximation + [0, 1/2, -1/2, 0, 0] + >>> res[3:] # higher order approximations + [[-1/2, 1, -1/3, -1/6, 0], [0, 2/3, -2/3, -1/12, 1/12]] + + Let us compare this to a differently defined ``x_list``. Pay attention to + ``foo[i][k]`` corresponding to the gridpoint defined by ``x_list[k]``. + + >>> foo = finite_diff_weights(1, [-S(2), -S(1), S(0), S(1), S(2)], 0)[1] + >>> foo + [[0, 0, 0, 0, 0], + [-1, 1, 0, 0, 0], + [1/2, -2, 3/2, 0, 0], + [1/6, -1, 1/2, 1/3, 0], + [1/12, -2/3, 0, 2/3, -1/12]] + >>> foo[1] # not the same and of lower accuracy as res[1]! + [-1, 1, 0, 0, 0] + >>> foo[2] # classic double backward step approximation + [1/2, -2, 3/2, 0, 0] + >>> foo[4] # the same as res[4] + [1/12, -2/3, 0, 2/3, -1/12] + + Note that, unless you plan on using approximations based on subsets of + ``x_list``, the order of gridpoints does not matter. + + The capability to generate weights at arbitrary points can be + used e.g. to minimize Runge's phenomenon by using Chebyshev nodes: + + >>> from sympy import cos, symbols, pi, simplify + >>> N, (h, x) = 4, symbols('h x') + >>> x_list = [x+h*cos(i*pi/(N)) for i in range(N,-1,-1)] # chebyshev nodes + >>> print(x_list) + [-h + x, -sqrt(2)*h/2 + x, x, sqrt(2)*h/2 + x, h + x] + >>> mycoeffs = finite_diff_weights(1, x_list, 0)[1][4] + >>> [simplify(c) for c in mycoeffs] #doctest: +NORMALIZE_WHITESPACE + [(h**3/2 + h**2*x - 3*h*x**2 - 4*x**3)/h**4, + (-sqrt(2)*h**3 - 4*h**2*x + 3*sqrt(2)*h*x**2 + 8*x**3)/h**4, + (6*h**2*x - 8*x**3)/h**4, + (sqrt(2)*h**3 - 4*h**2*x - 3*sqrt(2)*h*x**2 + 8*x**3)/h**4, + (-h**3/2 + h**2*x + 3*h*x**2 - 4*x**3)/h**4] + + Notes + ===== + + If weights for a finite difference approximation of 3rd order + derivative is wanted, weights for 0th, 1st and 2nd order are + calculated "for free", so are formulae using subsets of ``x_list``. + This is something one can take advantage of to save computational cost. + Be aware that one should define ``x_list`` from nearest to furthest from + ``x0``. If not, subsets of ``x_list`` will yield poorer approximations, + which might not grand an order of accuracy of ``len(x_list) - order``. + + See also + ======== + + sympy.calculus.finite_diff.apply_finite_diff + + References + ========== + + .. [1] Generation of Finite Difference Formulas on Arbitrarily Spaced + Grids, Bengt Fornberg; Mathematics of computation; 51; 184; + (1988); 699-706; doi:10.1090/S0025-5718-1988-0935077-0 + + """ + # The notation below closely corresponds to the one used in the paper. + order = S(order) + if not order.is_number: + raise ValueError("Cannot handle symbolic order.") + if order < 0: + raise ValueError("Negative derivative order illegal.") + if int(order) != order: + raise ValueError("Non-integer order illegal") + M = order + N = len(x_list) - 1 + delta = [[[0 for nu in range(N+1)] for n in range(N+1)] for + m in range(M+1)] + delta[0][0][0] = S.One + c1 = S.One + for n in range(1, N+1): + c2 = S.One + for nu in range(n): + c3 = x_list[n] - x_list[nu] + c2 = c2 * c3 + if n <= M: + delta[n][n-1][nu] = 0 + for m in range(min(n, M)+1): + delta[m][n][nu] = (x_list[n]-x0)*delta[m][n-1][nu] -\ + m*delta[m-1][n-1][nu] + delta[m][n][nu] /= c3 + for m in range(min(n, M)+1): + delta[m][n][n] = c1/c2*(m*delta[m-1][n-1][n-1] - + (x_list[n-1]-x0)*delta[m][n-1][n-1]) + c1 = c2 + return delta + + +def apply_finite_diff(order, x_list, y_list, x0=S.Zero): + """ + Calculates the finite difference approximation of + the derivative of requested order at ``x0`` from points + provided in ``x_list`` and ``y_list``. + + Parameters + ========== + + order: int + order of derivative to approximate. 0 corresponds to interpolation. + x_list: sequence + Sequence of (unique) values for the independent variable. + y_list: sequence + The function value at corresponding values for the independent + variable in x_list. + x0: Number or Symbol + At what value of the independent variable the derivative should be + evaluated. Defaults to 0. + + Returns + ======= + + sympy.core.add.Add or sympy.core.numbers.Number + The finite difference expression approximating the requested + derivative order at ``x0``. + + Examples + ======== + + >>> from sympy import apply_finite_diff + >>> cube = lambda arg: (1.0*arg)**3 + >>> xlist = range(-3,3+1) + >>> apply_finite_diff(2, xlist, map(cube, xlist), 2) - 12 # doctest: +SKIP + -3.55271367880050e-15 + + we see that the example above only contain rounding errors. + apply_finite_diff can also be used on more abstract objects: + + >>> from sympy import IndexedBase, Idx + >>> x, y = map(IndexedBase, 'xy') + >>> i = Idx('i') + >>> x_list, y_list = zip(*[(x[i+j], y[i+j]) for j in range(-1,2)]) + >>> apply_finite_diff(1, x_list, y_list, x[i]) + ((x[i + 1] - x[i])/(-x[i - 1] + x[i]) - 1)*y[i]/(x[i + 1] - x[i]) - + (x[i + 1] - x[i])*y[i - 1]/((x[i + 1] - x[i - 1])*(-x[i - 1] + x[i])) + + (-x[i - 1] + x[i])*y[i + 1]/((x[i + 1] - x[i - 1])*(x[i + 1] - x[i])) + + Notes + ===== + + Order = 0 corresponds to interpolation. + Only supply so many points you think makes sense + to around x0 when extracting the derivative (the function + need to be well behaved within that region). Also beware + of Runge's phenomenon. + + See also + ======== + + sympy.calculus.finite_diff.finite_diff_weights + + References + ========== + + Fortran 90 implementation with Python interface for numerics: finitediff_ + + .. _finitediff: https://github.com/bjodah/finitediff + + """ + + # In the original paper the following holds for the notation: + # M = order + # N = len(x_list) - 1 + + N = len(x_list) - 1 + if len(x_list) != len(y_list): + raise ValueError("x_list and y_list not equal in length.") + + delta = finite_diff_weights(order, x_list, x0) + + derivative = 0 + for nu in range(len(x_list)): + derivative += delta[order][N][nu]*y_list[nu] + return derivative + + +def _as_finite_diff(derivative, points=1, x0=None, wrt=None): + """ + Returns an approximation of a derivative of a function in + the form of a finite difference formula. The expression is a + weighted sum of the function at a number of discrete values of + (one of) the independent variable(s). + + Parameters + ========== + + derivative: a Derivative instance + + points: sequence or coefficient, optional + If sequence: discrete values (length >= order+1) of the + independent variable used for generating the finite + difference weights. + If it is a coefficient, it will be used as the step-size + for generating an equidistant sequence of length order+1 + centered around ``x0``. default: 1 (step-size 1) + + x0: number or Symbol, optional + the value of the independent variable (``wrt``) at which the + derivative is to be approximated. Default: same as ``wrt``. + + wrt: Symbol, optional + "with respect to" the variable for which the (partial) + derivative is to be approximated for. If not provided it + is required that the Derivative is ordinary. Default: ``None``. + + Examples + ======== + + >>> from sympy import symbols, Function, exp, sqrt, Symbol + >>> from sympy.calculus.finite_diff import _as_finite_diff + >>> x, h = symbols('x h') + >>> f = Function('f') + >>> _as_finite_diff(f(x).diff(x)) + -f(x - 1/2) + f(x + 1/2) + + The default step size and number of points are 1 and ``order + 1`` + respectively. We can change the step size by passing a symbol + as a parameter: + + >>> _as_finite_diff(f(x).diff(x), h) + -f(-h/2 + x)/h + f(h/2 + x)/h + + We can also specify the discretized values to be used in a sequence: + + >>> _as_finite_diff(f(x).diff(x), [x, x+h, x+2*h]) + -3*f(x)/(2*h) + 2*f(h + x)/h - f(2*h + x)/(2*h) + + The algorithm is not restricted to use equidistant spacing, nor + do we need to make the approximation around ``x0``, but we can get + an expression estimating the derivative at an offset: + + >>> e, sq2 = exp(1), sqrt(2) + >>> xl = [x-h, x+h, x+e*h] + >>> _as_finite_diff(f(x).diff(x, 1), xl, x+h*sq2) + 2*h*((h + sqrt(2)*h)/(2*h) - (-sqrt(2)*h + h)/(2*h))*f(E*h + x)/((-h + E*h)*(h + E*h)) + + (-(-sqrt(2)*h + h)/(2*h) - (-sqrt(2)*h + E*h)/(2*h))*f(-h + x)/(h + E*h) + + (-(h + sqrt(2)*h)/(2*h) + (-sqrt(2)*h + E*h)/(2*h))*f(h + x)/(-h + E*h) + + Partial derivatives are also supported: + + >>> y = Symbol('y') + >>> d2fdxdy=f(x,y).diff(x,y) + >>> _as_finite_diff(d2fdxdy, wrt=x) + -Derivative(f(x - 1/2, y), y) + Derivative(f(x + 1/2, y), y) + + See also + ======== + + sympy.calculus.finite_diff.apply_finite_diff + sympy.calculus.finite_diff.finite_diff_weights + + """ + if derivative.is_Derivative: + pass + elif derivative.is_Atom: + return derivative + else: + return derivative.fromiter( + [_as_finite_diff(ar, points, x0, wrt) for ar + in derivative.args], **derivative.assumptions0) + + if wrt is None: + old = None + for v in derivative.variables: + if old is v: + continue + derivative = _as_finite_diff(derivative, points, x0, v) + old = v + return derivative + + order = derivative.variables.count(wrt) + + if x0 is None: + x0 = wrt + + if not iterable(points): + if getattr(points, 'is_Function', False) and wrt in points.args: + points = points.subs(wrt, x0) + # points is simply the step-size, let's make it a + # equidistant sequence centered around x0 + if order % 2 == 0: + # even order => odd number of points, grid point included + points = [x0 + points*i for i + in range(-order//2, order//2 + 1)] + else: + # odd order => even number of points, half-way wrt grid point + points = [x0 + points*S(i)/2 for i + in range(-order, order + 1, 2)] + others = [wrt, 0] + for v in set(derivative.variables): + if v == wrt: + continue + others += [v, derivative.variables.count(v)] + if len(points) < order+1: + raise ValueError("Too few points for order %d" % order) + return apply_finite_diff(order, points, [ + Derivative(derivative.expr.subs({wrt: x}), *others) for + x in points], x0) + + +def differentiate_finite(expr, *symbols, + points=1, x0=None, wrt=None, evaluate=False): + r""" Differentiate expr and replace Derivatives with finite differences. + + Parameters + ========== + + expr : expression + \*symbols : differentiate with respect to symbols + points: sequence, coefficient or undefined function, optional + see ``Derivative.as_finite_difference`` + x0: number or Symbol, optional + see ``Derivative.as_finite_difference`` + wrt: Symbol, optional + see ``Derivative.as_finite_difference`` + + Examples + ======== + + >>> from sympy import sin, Function, differentiate_finite + >>> from sympy.abc import x, y, h + >>> f, g = Function('f'), Function('g') + >>> differentiate_finite(f(x)*g(x), x, points=[x-h, x+h]) + -f(-h + x)*g(-h + x)/(2*h) + f(h + x)*g(h + x)/(2*h) + + ``differentiate_finite`` works on any expression, including the expressions + with embedded derivatives: + + >>> differentiate_finite(f(x) + sin(x), x, 2) + -2*f(x) + f(x - 1) + f(x + 1) - 2*sin(x) + sin(x - 1) + sin(x + 1) + >>> differentiate_finite(f(x, y), x, y) + f(x - 1/2, y - 1/2) - f(x - 1/2, y + 1/2) - f(x + 1/2, y - 1/2) + f(x + 1/2, y + 1/2) + >>> differentiate_finite(f(x)*g(x).diff(x), x) + (-g(x) + g(x + 1))*f(x + 1/2) - (g(x) - g(x - 1))*f(x - 1/2) + + To make finite difference with non-constant discretization step use + undefined functions: + + >>> dx = Function('dx') + >>> differentiate_finite(f(x)*g(x).diff(x), points=dx(x)) + -(-g(x - dx(x)/2 - dx(x - dx(x)/2)/2)/dx(x - dx(x)/2) + + g(x - dx(x)/2 + dx(x - dx(x)/2)/2)/dx(x - dx(x)/2))*f(x - dx(x)/2)/dx(x) + + (-g(x + dx(x)/2 - dx(x + dx(x)/2)/2)/dx(x + dx(x)/2) + + g(x + dx(x)/2 + dx(x + dx(x)/2)/2)/dx(x + dx(x)/2))*f(x + dx(x)/2)/dx(x) + + """ + if any(term.is_Derivative for term in list(preorder_traversal(expr))): + evaluate = False + + Dexpr = expr.diff(*symbols, evaluate=evaluate) + if evaluate: + sympy_deprecation_warning(""" + The evaluate flag to differentiate_finite() is deprecated. + + evaluate=True expands the intermediate derivatives before computing + differences, but this usually not what you want, as it does not + satisfy the product rule. + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-differentiate_finite-evaluate", + ) + return Dexpr.replace( + lambda arg: arg.is_Derivative, + lambda arg: arg.as_finite_difference(points=points, x0=x0, wrt=wrt)) + else: + DFexpr = Dexpr.as_finite_difference(points=points, x0=x0, wrt=wrt) + return DFexpr.replace( + lambda arg: isinstance(arg, Subs), + lambda arg: arg.expr.as_finite_difference( + points=points, x0=arg.point[0], wrt=arg.variables[0])) diff --git a/venv/lib/python3.11/site-packages/sympy/calculus/singularities.py b/venv/lib/python3.11/site-packages/sympy/calculus/singularities.py new file mode 100644 index 0000000000000000000000000000000000000000..5adafc59efaf0bff44707f6b5e3f074be6bc1f32 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/calculus/singularities.py @@ -0,0 +1,406 @@ +""" +Singularities +============= + +This module implements algorithms for finding singularities for a function +and identifying types of functions. + +The differential calculus methods in this module include methods to identify +the following function types in the given ``Interval``: +- Increasing +- Strictly Increasing +- Decreasing +- Strictly Decreasing +- Monotonic + +""" + +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.core.sympify import sympify +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.trigonometric import sec, csc, cot, tan, cos +from sympy.functions.elementary.hyperbolic import ( + sech, csch, coth, tanh, cosh, asech, acsch, atanh, acoth) +from sympy.utilities.misc import filldedent + + +def singularities(expression, symbol, domain=None): + """ + Find singularities of a given function. + + Parameters + ========== + + expression : Expr + The target function in which singularities need to be found. + symbol : Symbol + The symbol over the values of which the singularity in + expression in being searched for. + + Returns + ======= + + Set + A set of values for ``symbol`` for which ``expression`` has a + singularity. An ``EmptySet`` is returned if ``expression`` has no + singularities for any given value of ``Symbol``. + + Raises + ====== + + NotImplementedError + Methods for determining the singularities of this function have + not been developed. + + Notes + ===== + + This function does not find non-isolated singularities + nor does it find branch points of the expression. + + Currently supported functions are: + - univariate continuous (real or complex) functions + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Mathematical_singularity + + Examples + ======== + + >>> from sympy import singularities, Symbol, log + >>> x = Symbol('x', real=True) + >>> y = Symbol('y', real=False) + >>> singularities(x**2 + x + 1, x) + EmptySet + >>> singularities(1/(x + 1), x) + {-1} + >>> singularities(1/(y**2 + 1), y) + {-I, I} + >>> singularities(1/(y**3 + 1), y) + {-1, 1/2 - sqrt(3)*I/2, 1/2 + sqrt(3)*I/2} + >>> singularities(log(x), x) + {0} + + """ + from sympy.solvers.solveset import solveset + + if domain is None: + domain = S.Reals if symbol.is_real else S.Complexes + try: + sings = S.EmptySet + e = expression.rewrite([sec, csc, cot, tan], cos) + e = e.rewrite([sech, csch, coth, tanh], cosh) + for i in e.atoms(Pow): + if i.exp.is_infinite: + raise NotImplementedError + if i.exp.is_negative: + # XXX: exponent of varying sign not handled + sings += solveset(i.base, symbol, domain) + for i in expression.atoms(log, asech, acsch): + sings += solveset(i.args[0], symbol, domain) + for i in expression.atoms(atanh, acoth): + sings += solveset(i.args[0] - 1, symbol, domain) + sings += solveset(i.args[0] + 1, symbol, domain) + return sings + except NotImplementedError: + raise NotImplementedError(filldedent(''' + Methods for determining the singularities + of this function have not been developed.''')) + + +########################################################################### +# DIFFERENTIAL CALCULUS METHODS # +########################################################################### + + +def monotonicity_helper(expression, predicate, interval=S.Reals, symbol=None): + """ + Helper function for functions checking function monotonicity. + + Parameters + ========== + + expression : Expr + The target function which is being checked + predicate : function + The property being tested for. The function takes in an integer + and returns a boolean. The integer input is the derivative and + the boolean result should be true if the property is being held, + and false otherwise. + interval : Set, optional + The range of values in which we are testing, defaults to all reals. + symbol : Symbol, optional + The symbol present in expression which gets varied over the given range. + + It returns a boolean indicating whether the interval in which + the function's derivative satisfies given predicate is a superset + of the given interval. + + Returns + ======= + + Boolean + True if ``predicate`` is true for all the derivatives when ``symbol`` + is varied in ``range``, False otherwise. + + """ + from sympy.solvers.solveset import solveset + + expression = sympify(expression) + free = expression.free_symbols + + if symbol is None: + if len(free) > 1: + raise NotImplementedError( + 'The function has not yet been implemented' + ' for all multivariate expressions.' + ) + + variable = symbol or (free.pop() if free else Symbol('x')) + derivative = expression.diff(variable) + predicate_interval = solveset(predicate(derivative), variable, S.Reals) + return interval.is_subset(predicate_interval) + + +def is_increasing(expression, interval=S.Reals, symbol=None): + """ + Return whether the function is increasing in the given interval. + + Parameters + ========== + + expression : Expr + The target function which is being checked. + interval : Set, optional + The range of values in which we are testing (defaults to set of + all real numbers). + symbol : Symbol, optional + The symbol present in expression which gets varied over the given range. + + Returns + ======= + + Boolean + True if ``expression`` is increasing (either strictly increasing or + constant) in the given ``interval``, False otherwise. + + Examples + ======== + + >>> from sympy import is_increasing + >>> from sympy.abc import x, y + >>> from sympy import S, Interval, oo + >>> is_increasing(x**3 - 3*x**2 + 4*x, S.Reals) + True + >>> is_increasing(-x**2, Interval(-oo, 0)) + True + >>> is_increasing(-x**2, Interval(0, oo)) + False + >>> is_increasing(4*x**3 - 6*x**2 - 72*x + 30, Interval(-2, 3)) + False + >>> is_increasing(x**2 + y, Interval(1, 2), x) + True + + """ + return monotonicity_helper(expression, lambda x: x >= 0, interval, symbol) + + +def is_strictly_increasing(expression, interval=S.Reals, symbol=None): + """ + Return whether the function is strictly increasing in the given interval. + + Parameters + ========== + + expression : Expr + The target function which is being checked. + interval : Set, optional + The range of values in which we are testing (defaults to set of + all real numbers). + symbol : Symbol, optional + The symbol present in expression which gets varied over the given range. + + Returns + ======= + + Boolean + True if ``expression`` is strictly increasing in the given ``interval``, + False otherwise. + + Examples + ======== + + >>> from sympy import is_strictly_increasing + >>> from sympy.abc import x, y + >>> from sympy import Interval, oo + >>> is_strictly_increasing(4*x**3 - 6*x**2 - 72*x + 30, Interval.Ropen(-oo, -2)) + True + >>> is_strictly_increasing(4*x**3 - 6*x**2 - 72*x + 30, Interval.Lopen(3, oo)) + True + >>> is_strictly_increasing(4*x**3 - 6*x**2 - 72*x + 30, Interval.open(-2, 3)) + False + >>> is_strictly_increasing(-x**2, Interval(0, oo)) + False + >>> is_strictly_increasing(-x**2 + y, Interval(-oo, 0), x) + False + + """ + return monotonicity_helper(expression, lambda x: x > 0, interval, symbol) + + +def is_decreasing(expression, interval=S.Reals, symbol=None): + """ + Return whether the function is decreasing in the given interval. + + Parameters + ========== + + expression : Expr + The target function which is being checked. + interval : Set, optional + The range of values in which we are testing (defaults to set of + all real numbers). + symbol : Symbol, optional + The symbol present in expression which gets varied over the given range. + + Returns + ======= + + Boolean + True if ``expression`` is decreasing (either strictly decreasing or + constant) in the given ``interval``, False otherwise. + + Examples + ======== + + >>> from sympy import is_decreasing + >>> from sympy.abc import x, y + >>> from sympy import S, Interval, oo + >>> is_decreasing(1/(x**2 - 3*x), Interval.open(S(3)/2, 3)) + True + >>> is_decreasing(1/(x**2 - 3*x), Interval.open(1.5, 3)) + True + >>> is_decreasing(1/(x**2 - 3*x), Interval.Lopen(3, oo)) + True + >>> is_decreasing(1/(x**2 - 3*x), Interval.Ropen(-oo, S(3)/2)) + False + >>> is_decreasing(1/(x**2 - 3*x), Interval.Ropen(-oo, 1.5)) + False + >>> is_decreasing(-x**2, Interval(-oo, 0)) + False + >>> is_decreasing(-x**2 + y, Interval(-oo, 0), x) + False + + """ + return monotonicity_helper(expression, lambda x: x <= 0, interval, symbol) + + +def is_strictly_decreasing(expression, interval=S.Reals, symbol=None): + """ + Return whether the function is strictly decreasing in the given interval. + + Parameters + ========== + + expression : Expr + The target function which is being checked. + interval : Set, optional + The range of values in which we are testing (defaults to set of + all real numbers). + symbol : Symbol, optional + The symbol present in expression which gets varied over the given range. + + Returns + ======= + + Boolean + True if ``expression`` is strictly decreasing in the given ``interval``, + False otherwise. + + Examples + ======== + + >>> from sympy import is_strictly_decreasing + >>> from sympy.abc import x, y + >>> from sympy import S, Interval, oo + >>> is_strictly_decreasing(1/(x**2 - 3*x), Interval.Lopen(3, oo)) + True + >>> is_strictly_decreasing(1/(x**2 - 3*x), Interval.Ropen(-oo, S(3)/2)) + False + >>> is_strictly_decreasing(1/(x**2 - 3*x), Interval.Ropen(-oo, 1.5)) + False + >>> is_strictly_decreasing(-x**2, Interval(-oo, 0)) + False + >>> is_strictly_decreasing(-x**2 + y, Interval(-oo, 0), x) + False + + """ + return monotonicity_helper(expression, lambda x: x < 0, interval, symbol) + + +def is_monotonic(expression, interval=S.Reals, symbol=None): + """ + Return whether the function is monotonic in the given interval. + + Parameters + ========== + + expression : Expr + The target function which is being checked. + interval : Set, optional + The range of values in which we are testing (defaults to set of + all real numbers). + symbol : Symbol, optional + The symbol present in expression which gets varied over the given range. + + Returns + ======= + + Boolean + True if ``expression`` is monotonic in the given ``interval``, + False otherwise. + + Raises + ====== + + NotImplementedError + Monotonicity check has not been implemented for the queried function. + + Examples + ======== + + >>> from sympy import is_monotonic + >>> from sympy.abc import x, y + >>> from sympy import S, Interval, oo + >>> is_monotonic(1/(x**2 - 3*x), Interval.open(S(3)/2, 3)) + True + >>> is_monotonic(1/(x**2 - 3*x), Interval.open(1.5, 3)) + True + >>> is_monotonic(1/(x**2 - 3*x), Interval.Lopen(3, oo)) + True + >>> is_monotonic(x**3 - 3*x**2 + 4*x, S.Reals) + True + >>> is_monotonic(-x**2, S.Reals) + False + >>> is_monotonic(x**2 + y + 1, Interval(1, 2), x) + True + + """ + from sympy.solvers.solveset import solveset + + expression = sympify(expression) + + free = expression.free_symbols + if symbol is None and len(free) > 1: + raise NotImplementedError( + 'is_monotonic has not yet been implemented' + ' for all multivariate expressions.' + ) + + variable = symbol or (free.pop() if free else Symbol('x')) + turning_points = solveset(expression.diff(variable), variable, interval) + return interval.intersection(turning_points) is S.EmptySet diff --git a/venv/lib/python3.11/site-packages/sympy/calculus/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/calculus/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_accumulationbounds.py b/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_accumulationbounds.py new file mode 100644 index 0000000000000000000000000000000000000000..bcc47c66327fe21ddca3a6b73ca5914e0441b38e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_accumulationbounds.py @@ -0,0 +1,336 @@ +from sympy.core.numbers import (E, Rational, oo, pi, zoo) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import (Max, Min, sqrt) +from sympy.functions.elementary.trigonometric import (cos, sin, tan) +from sympy.calculus.accumulationbounds import AccumBounds +from sympy.core import Add, Mul, Pow +from sympy.core.expr import unchanged +from sympy.testing.pytest import raises, XFAIL +from sympy.abc import x + +a = Symbol('a', real=True) +B = AccumBounds + + +def test_AccumBounds(): + assert B(1, 2).args == (1, 2) + assert B(1, 2).delta is S.One + assert B(1, 2).mid == Rational(3, 2) + assert B(1, 3).is_real == True + + assert B(1, 1) is S.One + + assert B(1, 2) + 1 == B(2, 3) + assert 1 + B(1, 2) == B(2, 3) + assert B(1, 2) + B(2, 3) == B(3, 5) + + assert -B(1, 2) == B(-2, -1) + + assert B(1, 2) - 1 == B(0, 1) + assert 1 - B(1, 2) == B(-1, 0) + assert B(2, 3) - B(1, 2) == B(0, 2) + + assert x + B(1, 2) == Add(B(1, 2), x) + assert a + B(1, 2) == B(1 + a, 2 + a) + assert B(1, 2) - x == Add(B(1, 2), -x) + + assert B(-oo, 1) + oo == B(-oo, oo) + assert B(1, oo) + oo is oo + assert B(1, oo) - oo == B(-oo, oo) + assert (-oo - B(-1, oo)) is -oo + assert B(-oo, 1) - oo is -oo + + assert B(1, oo) - oo == B(-oo, oo) + assert B(-oo, 1) - (-oo) == B(-oo, oo) + assert (oo - B(1, oo)) == B(-oo, oo) + assert (-oo - B(1, oo)) is -oo + + assert B(1, 2)/2 == B(S.Half, 1) + assert 2/B(2, 3) == B(Rational(2, 3), 1) + assert 1/B(-1, 1) == B(-oo, oo) + + assert abs(B(1, 2)) == B(1, 2) + assert abs(B(-2, -1)) == B(1, 2) + assert abs(B(-2, 1)) == B(0, 2) + assert abs(B(-1, 2)) == B(0, 2) + c = Symbol('c') + raises(ValueError, lambda: B(0, c)) + raises(ValueError, lambda: B(1, -1)) + r = Symbol('r', real=True) + raises(ValueError, lambda: B(r, r - 1)) + + +def test_AccumBounds_mul(): + assert B(1, 2)*2 == B(2, 4) + assert 2*B(1, 2) == B(2, 4) + assert B(1, 2)*B(2, 3) == B(2, 6) + assert B(0, 2)*B(2, oo) == B(0, oo) + l, r = B(-oo, oo), B(-a, a) + assert l*r == B(-oo, oo) + assert r*l == B(-oo, oo) + l, r = B(1, oo), B(-3, -2) + assert l*r == B(-oo, -2) + assert r*l == B(-oo, -2) + assert B(1, 2)*0 == 0 + assert B(1, oo)*0 == B(0, oo) + assert B(-oo, 1)*0 == B(-oo, 0) + assert B(-oo, oo)*0 == B(-oo, oo) + + assert B(1, 2)*x == Mul(B(1, 2), x, evaluate=False) + + assert B(0, 2)*oo == B(0, oo) + assert B(-2, 0)*oo == B(-oo, 0) + assert B(0, 2)*(-oo) == B(-oo, 0) + assert B(-2, 0)*(-oo) == B(0, oo) + assert B(-1, 1)*oo == B(-oo, oo) + assert B(-1, 1)*(-oo) == B(-oo, oo) + assert B(-oo, oo)*oo == B(-oo, oo) + + +def test_AccumBounds_div(): + assert B(-1, 3)/B(3, 4) == B(Rational(-1, 3), 1) + assert B(-2, 4)/B(-3, 4) == B(-oo, oo) + assert B(-3, -2)/B(-4, 0) == B(S.Half, oo) + + # these two tests can have a better answer + # after Union of B is improved + assert B(-3, -2)/B(-2, 1) == B(-oo, oo) + assert B(2, 3)/B(-2, 2) == B(-oo, oo) + + assert B(-3, -2)/B(0, 4) == B(-oo, Rational(-1, 2)) + assert B(2, 4)/B(-3, 0) == B(-oo, Rational(-2, 3)) + assert B(2, 4)/B(0, 3) == B(Rational(2, 3), oo) + + assert B(0, 1)/B(0, 1) == B(0, oo) + assert B(-1, 0)/B(0, 1) == B(-oo, 0) + assert B(-1, 2)/B(-2, 2) == B(-oo, oo) + + assert 1/B(-1, 2) == B(-oo, oo) + assert 1/B(0, 2) == B(S.Half, oo) + assert (-1)/B(0, 2) == B(-oo, Rational(-1, 2)) + assert 1/B(-oo, 0) == B(-oo, 0) + assert 1/B(-1, 0) == B(-oo, -1) + assert (-2)/B(-oo, 0) == B(0, oo) + assert 1/B(-oo, -1) == B(-1, 0) + + assert B(1, 2)/a == Mul(B(1, 2), 1/a, evaluate=False) + + assert B(1, 2)/0 == B(1, 2)*zoo + assert B(1, oo)/oo == B(0, oo) + assert B(1, oo)/(-oo) == B(-oo, 0) + assert B(-oo, -1)/oo == B(-oo, 0) + assert B(-oo, -1)/(-oo) == B(0, oo) + assert B(-oo, oo)/oo == B(-oo, oo) + assert B(-oo, oo)/(-oo) == B(-oo, oo) + assert B(-1, oo)/oo == B(0, oo) + assert B(-1, oo)/(-oo) == B(-oo, 0) + assert B(-oo, 1)/oo == B(-oo, 0) + assert B(-oo, 1)/(-oo) == B(0, oo) + + +def test_issue_18795(): + r = Symbol('r', real=True) + a = B(-1,1) + c = B(7, oo) + b = B(-oo, oo) + assert c - tan(r) == B(7-tan(r), oo) + assert b + tan(r) == B(-oo, oo) + assert (a + r)/a == B(-oo, oo)*B(r - 1, r + 1) + assert (b + a)/a == B(-oo, oo) + + +def test_AccumBounds_func(): + assert (x**2 + 2*x + 1).subs(x, B(-1, 1)) == B(-1, 4) + assert exp(B(0, 1)) == B(1, E) + assert exp(B(-oo, oo)) == B(0, oo) + assert log(B(3, 6)) == B(log(3), log(6)) + + +@XFAIL +def test_AccumBounds_powf(): + nn = Symbol('nn', nonnegative=True) + assert B(1 + nn, 2 + nn)**B(1, 2) == B(1 + nn, (2 + nn)**2) + i = Symbol('i', integer=True, negative=True) + assert B(1, 2)**i == B(2**i, 1) + + +def test_AccumBounds_pow(): + assert B(0, 2)**2 == B(0, 4) + assert B(-1, 1)**2 == B(0, 1) + assert B(1, 2)**2 == B(1, 4) + assert B(-1, 2)**3 == B(-1, 8) + assert B(-1, 1)**0 == 1 + + assert B(1, 2)**Rational(5, 2) == B(1, 4*sqrt(2)) + assert B(0, 2)**S.Half == B(0, sqrt(2)) + + neg = Symbol('neg', negative=True) + assert unchanged(Pow, B(neg, 1), S.Half) + nn = Symbol('nn', nonnegative=True) + assert B(nn, nn + 1)**S.Half == B(sqrt(nn), sqrt(nn + 1)) + assert B(nn, nn + 1)**nn == B(nn**nn, (nn + 1)**nn) + assert unchanged(Pow, B(nn, nn + 1), x) + i = Symbol('i', integer=True) + assert B(1, 2)**i == B(Min(1, 2**i), Max(1, 2**i)) + i = Symbol('i', integer=True, nonnegative=True) + assert B(1, 2)**i == B(1, 2**i) + assert B(0, 1)**i == B(0**i, 1) + + assert B(1, 5)**(-2) == B(Rational(1, 25), 1) + assert B(-1, 3)**(-2) == B(0, oo) + assert B(0, 2)**(-3) == B(Rational(1, 8), oo) + assert B(-2, 0)**(-3) == B(-oo, -Rational(1, 8)) + assert B(0, 2)**(-2) == B(Rational(1, 4), oo) + assert B(-1, 2)**(-3) == B(-oo, oo) + assert B(-3, -2)**(-3) == B(Rational(-1, 8), Rational(-1, 27)) + assert B(-3, -2)**(-2) == B(Rational(1, 9), Rational(1, 4)) + assert B(0, oo)**S.Half == B(0, oo) + assert B(-oo, 0)**(-2) == B(0, oo) + assert B(-2, 0)**(-2) == B(Rational(1, 4), oo) + + assert B(Rational(1, 3), S.Half)**oo is S.Zero + assert B(0, S.Half)**oo is S.Zero + assert B(S.Half, 1)**oo == B(0, oo) + assert B(0, 1)**oo == B(0, oo) + assert B(2, 3)**oo is oo + assert B(1, 2)**oo == B(0, oo) + assert B(S.Half, 3)**oo == B(0, oo) + assert B(Rational(-1, 3), Rational(-1, 4))**oo is S.Zero + assert B(-1, Rational(-1, 2))**oo is S.NaN + assert B(-3, -2)**oo is zoo + assert B(-2, -1)**oo is S.NaN + assert B(-2, Rational(-1, 2))**oo is S.NaN + assert B(Rational(-1, 2), S.Half)**oo is S.Zero + assert B(Rational(-1, 2), 1)**oo == B(0, oo) + assert B(Rational(-2, 3), 2)**oo == B(0, oo) + assert B(-1, 1)**oo == B(-oo, oo) + assert B(-1, S.Half)**oo == B(-oo, oo) + assert B(-1, 2)**oo == B(-oo, oo) + assert B(-2, S.Half)**oo == B(-oo, oo) + + assert B(1, 2)**x == Pow(B(1, 2), x, evaluate=False) + + assert B(2, 3)**(-oo) is S.Zero + assert B(0, 2)**(-oo) == B(0, oo) + assert B(-1, 2)**(-oo) == B(-oo, oo) + + assert (tan(x)**sin(2*x)).subs(x, B(0, pi/2)) == \ + Pow(B(-oo, oo), B(0, 1)) + + +def test_AccumBounds_exponent(): + # base is 0 + z = 0**B(a, a + S.Half) + assert z.subs(a, 0) == B(0, 1) + assert z.subs(a, 1) == 0 + p = z.subs(a, -1) + assert p.is_Pow and p.args == (0, B(-1, -S.Half)) + # base > 0 + # when base is 1 the type of bounds does not matter + assert 1**B(a, a + 1) == 1 + # otherwise we need to know if 0 is in the bounds + assert S.Half**B(-2, 2) == B(S(1)/4, 4) + assert 2**B(-2, 2) == B(S(1)/4, 4) + + # +eps may introduce +oo + # if there is a negative integer exponent + assert B(0, 1)**B(S(1)/2, 1) == B(0, 1) + assert B(0, 1)**B(0, 1) == B(0, 1) + + # positive bases have positive bounds + assert B(2, 3)**B(-3, -2) == B(S(1)/27, S(1)/4) + assert B(2, 3)**B(-3, 2) == B(S(1)/27, 9) + + # bounds generating imaginary parts unevaluated + assert unchanged(Pow, B(-1, 1), B(1, 2)) + assert B(0, S(1)/2)**B(1, oo) == B(0, S(1)/2) + assert B(0, 1)**B(1, oo) == B(0, oo) + assert B(0, 2)**B(1, oo) == B(0, oo) + assert B(0, oo)**B(1, oo) == B(0, oo) + assert B(S(1)/2, 1)**B(1, oo) == B(0, oo) + assert B(S(1)/2, 1)**B(-oo, -1) == B(0, oo) + assert B(S(1)/2, 1)**B(-oo, oo) == B(0, oo) + assert B(S(1)/2, 2)**B(1, oo) == B(0, oo) + assert B(S(1)/2, 2)**B(-oo, -1) == B(0, oo) + assert B(S(1)/2, 2)**B(-oo, oo) == B(0, oo) + assert B(S(1)/2, oo)**B(1, oo) == B(0, oo) + assert B(S(1)/2, oo)**B(-oo, -1) == B(0, oo) + assert B(S(1)/2, oo)**B(-oo, oo) == B(0, oo) + assert B(1, 2)**B(1, oo) == B(0, oo) + assert B(1, 2)**B(-oo, -1) == B(0, oo) + assert B(1, 2)**B(-oo, oo) == B(0, oo) + assert B(1, oo)**B(1, oo) == B(0, oo) + assert B(1, oo)**B(-oo, -1) == B(0, oo) + assert B(1, oo)**B(-oo, oo) == B(0, oo) + assert B(2, oo)**B(1, oo) == B(2, oo) + assert B(2, oo)**B(-oo, -1) == B(0, S(1)/2) + assert B(2, oo)**B(-oo, oo) == B(0, oo) + + +def test_comparison_AccumBounds(): + assert (B(1, 3) < 4) == S.true + assert (B(1, 3) < -1) == S.false + assert (B(1, 3) < 2).rel_op == '<' + assert (B(1, 3) <= 2).rel_op == '<=' + + assert (B(1, 3) > 4) == S.false + assert (B(1, 3) > -1) == S.true + assert (B(1, 3) > 2).rel_op == '>' + assert (B(1, 3) >= 2).rel_op == '>=' + + assert (B(1, 3) < B(4, 6)) == S.true + assert (B(1, 3) < B(2, 4)).rel_op == '<' + assert (B(1, 3) < B(-2, 0)) == S.false + + assert (B(1, 3) <= B(4, 6)) == S.true + assert (B(1, 3) <= B(-2, 0)) == S.false + + assert (B(1, 3) > B(4, 6)) == S.false + assert (B(1, 3) > B(-2, 0)) == S.true + + assert (B(1, 3) >= B(4, 6)) == S.false + assert (B(1, 3) >= B(-2, 0)) == S.true + + # issue 13499 + assert (cos(x) > 0).subs(x, oo) == (B(-1, 1) > 0) + + c = Symbol('c') + raises(TypeError, lambda: (B(0, 1) < c)) + raises(TypeError, lambda: (B(0, 1) <= c)) + raises(TypeError, lambda: (B(0, 1) > c)) + raises(TypeError, lambda: (B(0, 1) >= c)) + + +def test_contains_AccumBounds(): + assert (1 in B(1, 2)) == S.true + raises(TypeError, lambda: a in B(1, 2)) + assert 0 in B(-1, 0) + raises(TypeError, lambda: + (cos(1)**2 + sin(1)**2 - 1) in B(-1, 0)) + assert (-oo in B(1, oo)) == S.true + assert (oo in B(-oo, 0)) == S.true + + # issue 13159 + assert Mul(0, B(-1, 1)) == Mul(B(-1, 1), 0) == 0 + import itertools + for perm in itertools.permutations([0, B(-1, 1), x]): + assert Mul(*perm) == 0 + + +def test_intersection_AccumBounds(): + assert B(0, 3).intersection(B(1, 2)) == B(1, 2) + assert B(0, 3).intersection(B(1, 4)) == B(1, 3) + assert B(0, 3).intersection(B(-1, 2)) == B(0, 2) + assert B(0, 3).intersection(B(-1, 4)) == B(0, 3) + assert B(0, 1).intersection(B(2, 3)) == S.EmptySet + raises(TypeError, lambda: B(0, 3).intersection(1)) + + +def test_union_AccumBounds(): + assert B(0, 3).union(B(1, 2)) == B(0, 3) + assert B(0, 3).union(B(1, 4)) == B(0, 4) + assert B(0, 3).union(B(-1, 2)) == B(-1, 3) + assert B(0, 3).union(B(-1, 4)) == B(-1, 4) + raises(TypeError, lambda: B(0, 3).union(1)) diff --git a/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_euler.py b/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_euler.py new file mode 100644 index 0000000000000000000000000000000000000000..56371c8c787d9459d1390e18c306fddde94d2745 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_euler.py @@ -0,0 +1,74 @@ +from sympy.core.function import (Derivative as D, Function) +from sympy.core.relational import Eq +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.testing.pytest import raises +from sympy.calculus.euler import euler_equations as euler + + +def test_euler_interface(): + x = Function('x') + y = Symbol('y') + t = Symbol('t') + raises(TypeError, lambda: euler()) + raises(TypeError, lambda: euler(D(x(t), t)*y(t), [x(t), y])) + raises(ValueError, lambda: euler(D(x(t), t)*x(y), [x(t), x(y)])) + raises(TypeError, lambda: euler(D(x(t), t)**2, x(0))) + raises(TypeError, lambda: euler(D(x(t), t)*y(t), [t])) + assert euler(D(x(t), t)**2/2, {x(t)}) == [Eq(-D(x(t), t, t), 0)] + assert euler(D(x(t), t)**2/2, x(t), {t}) == [Eq(-D(x(t), t, t), 0)] + + +def test_euler_pendulum(): + x = Function('x') + t = Symbol('t') + L = D(x(t), t)**2/2 + cos(x(t)) + assert euler(L, x(t), t) == [Eq(-sin(x(t)) - D(x(t), t, t), 0)] + + +def test_euler_henonheiles(): + x = Function('x') + y = Function('y') + t = Symbol('t') + L = sum(D(z(t), t)**2/2 - z(t)**2/2 for z in [x, y]) + L += -x(t)**2*y(t) + y(t)**3/3 + assert euler(L, [x(t), y(t)], t) == [Eq(-2*x(t)*y(t) - x(t) - + D(x(t), t, t), 0), + Eq(-x(t)**2 + y(t)**2 - + y(t) - D(y(t), t, t), 0)] + + +def test_euler_sineg(): + psi = Function('psi') + t = Symbol('t') + x = Symbol('x') + L = D(psi(t, x), t)**2/2 - D(psi(t, x), x)**2/2 + cos(psi(t, x)) + assert euler(L, psi(t, x), [t, x]) == [Eq(-sin(psi(t, x)) - + D(psi(t, x), t, t) + + D(psi(t, x), x, x), 0)] + + +def test_euler_high_order(): + # an example from hep-th/0309038 + m = Symbol('m') + k = Symbol('k') + x = Function('x') + y = Function('y') + t = Symbol('t') + L = (m*D(x(t), t)**2/2 + m*D(y(t), t)**2/2 - + k*D(x(t), t)*D(y(t), t, t) + k*D(y(t), t)*D(x(t), t, t)) + assert euler(L, [x(t), y(t)]) == [Eq(2*k*D(y(t), t, t, t) - + m*D(x(t), t, t), 0), + Eq(-2*k*D(x(t), t, t, t) - + m*D(y(t), t, t), 0)] + + w = Symbol('w') + L = D(x(t, w), t, w)**2/2 + assert euler(L) == [Eq(D(x(t, w), t, t, w, w), 0)] + +def test_issue_18653(): + x, y, z = symbols("x y z") + f, g, h = symbols("f g h", cls=Function, args=(x, y)) + f, g, h = f(), g(), h() + expr2 = f.diff(x)*h.diff(z) + assert euler(expr2, (f,), (x, y)) == [] diff --git a/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_finite_diff.py b/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_finite_diff.py new file mode 100644 index 0000000000000000000000000000000000000000..e9ecfbdd61b15f516c54bd6d716ba1f264ee2ca0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_finite_diff.py @@ -0,0 +1,164 @@ +from itertools import product + +from sympy.core.function import (Function, diff) +from sympy.core.numbers import Rational +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import exp +from sympy.calculus.finite_diff import ( + apply_finite_diff, differentiate_finite, finite_diff_weights, + _as_finite_diff +) +from sympy.testing.pytest import raises, warns_deprecated_sympy + + +def test_apply_finite_diff(): + x, h = symbols('x h') + f = Function('f') + assert (apply_finite_diff(1, [x-h, x+h], [f(x-h), f(x+h)], x) - + (f(x+h)-f(x-h))/(2*h)).simplify() == 0 + + assert (apply_finite_diff(1, [5, 6, 7], [f(5), f(6), f(7)], 5) - + (Rational(-3, 2)*f(5) + 2*f(6) - S.Half*f(7))).simplify() == 0 + raises(ValueError, lambda: apply_finite_diff(1, [x, h], [f(x)])) + + +def test_finite_diff_weights(): + + d = finite_diff_weights(1, [5, 6, 7], 5) + assert d[1][2] == [Rational(-3, 2), 2, Rational(-1, 2)] + + # Table 1, p. 702 in doi:10.1090/S0025-5718-1988-0935077-0 + # -------------------------------------------------------- + xl = [0, 1, -1, 2, -2, 3, -3, 4, -4] + + # d holds all coefficients + d = finite_diff_weights(4, xl, S.Zero) + + # Zeroeth derivative + for i in range(5): + assert d[0][i] == [S.One] + [S.Zero]*8 + + # First derivative + assert d[1][0] == [S.Zero]*9 + assert d[1][2] == [S.Zero, S.Half, Rational(-1, 2)] + [S.Zero]*6 + assert d[1][4] == [S.Zero, Rational(2, 3), Rational(-2, 3), Rational(-1, 12), Rational(1, 12)] + [S.Zero]*4 + assert d[1][6] == [S.Zero, Rational(3, 4), Rational(-3, 4), Rational(-3, 20), Rational(3, 20), + Rational(1, 60), Rational(-1, 60)] + [S.Zero]*2 + assert d[1][8] == [S.Zero, Rational(4, 5), Rational(-4, 5), Rational(-1, 5), Rational(1, 5), + Rational(4, 105), Rational(-4, 105), Rational(-1, 280), Rational(1, 280)] + + # Second derivative + for i in range(2): + assert d[2][i] == [S.Zero]*9 + assert d[2][2] == [-S(2), S.One, S.One] + [S.Zero]*6 + assert d[2][4] == [Rational(-5, 2), Rational(4, 3), Rational(4, 3), Rational(-1, 12), Rational(-1, 12)] + [S.Zero]*4 + assert d[2][6] == [Rational(-49, 18), Rational(3, 2), Rational(3, 2), Rational(-3, 20), Rational(-3, 20), + Rational(1, 90), Rational(1, 90)] + [S.Zero]*2 + assert d[2][8] == [Rational(-205, 72), Rational(8, 5), Rational(8, 5), Rational(-1, 5), Rational(-1, 5), + Rational(8, 315), Rational(8, 315), Rational(-1, 560), Rational(-1, 560)] + + # Third derivative + for i in range(3): + assert d[3][i] == [S.Zero]*9 + assert d[3][4] == [S.Zero, -S.One, S.One, S.Half, Rational(-1, 2)] + [S.Zero]*4 + assert d[3][6] == [S.Zero, Rational(-13, 8), Rational(13, 8), S.One, -S.One, + Rational(-1, 8), Rational(1, 8)] + [S.Zero]*2 + assert d[3][8] == [S.Zero, Rational(-61, 30), Rational(61, 30), Rational(169, 120), Rational(-169, 120), + Rational(-3, 10), Rational(3, 10), Rational(7, 240), Rational(-7, 240)] + + # Fourth derivative + for i in range(4): + assert d[4][i] == [S.Zero]*9 + assert d[4][4] == [S(6), -S(4), -S(4), S.One, S.One] + [S.Zero]*4 + assert d[4][6] == [Rational(28, 3), Rational(-13, 2), Rational(-13, 2), S(2), S(2), + Rational(-1, 6), Rational(-1, 6)] + [S.Zero]*2 + assert d[4][8] == [Rational(91, 8), Rational(-122, 15), Rational(-122, 15), Rational(169, 60), Rational(169, 60), + Rational(-2, 5), Rational(-2, 5), Rational(7, 240), Rational(7, 240)] + + # Table 2, p. 703 in doi:10.1090/S0025-5718-1988-0935077-0 + # -------------------------------------------------------- + xl = [[j/S(2) for j in list(range(-i*2+1, 0, 2))+list(range(1, i*2+1, 2))] + for i in range(1, 5)] + + # d holds all coefficients + d = [finite_diff_weights({0: 1, 1: 2, 2: 4, 3: 4}[i], xl[i], 0) for + i in range(4)] + + # Zeroth derivative + assert d[0][0][1] == [S.Half, S.Half] + assert d[1][0][3] == [Rational(-1, 16), Rational(9, 16), Rational(9, 16), Rational(-1, 16)] + assert d[2][0][5] == [Rational(3, 256), Rational(-25, 256), Rational(75, 128), Rational(75, 128), + Rational(-25, 256), Rational(3, 256)] + assert d[3][0][7] == [Rational(-5, 2048), Rational(49, 2048), Rational(-245, 2048), Rational(1225, 2048), + Rational(1225, 2048), Rational(-245, 2048), Rational(49, 2048), Rational(-5, 2048)] + + # First derivative + assert d[0][1][1] == [-S.One, S.One] + assert d[1][1][3] == [Rational(1, 24), Rational(-9, 8), Rational(9, 8), Rational(-1, 24)] + assert d[2][1][5] == [Rational(-3, 640), Rational(25, 384), Rational(-75, 64), + Rational(75, 64), Rational(-25, 384), Rational(3, 640)] + assert d[3][1][7] == [Rational(5, 7168), Rational(-49, 5120), + Rational(245, 3072), Rational(-1225, 1024), + Rational(1225, 1024), Rational(-245, 3072), + Rational(49, 5120), Rational(-5, 7168)] + + # Reasonably the rest of the table is also correct... (testing of that + # deemed excessive at the moment) + raises(ValueError, lambda: finite_diff_weights(-1, [1, 2])) + raises(ValueError, lambda: finite_diff_weights(1.2, [1, 2])) + x = symbols('x') + raises(ValueError, lambda: finite_diff_weights(x, [1, 2])) + + +def test_as_finite_diff(): + x = symbols('x') + f = Function('f') + dx = Function('dx') + + _as_finite_diff(f(x).diff(x), [x-2, x-1, x, x+1, x+2]) + + # Use of undefined functions in ``points`` + df_true = -f(x+dx(x)/2-dx(x+dx(x)/2)/2) / dx(x+dx(x)/2) \ + + f(x+dx(x)/2+dx(x+dx(x)/2)/2) / dx(x+dx(x)/2) + df_test = diff(f(x), x).as_finite_difference(points=dx(x), x0=x+dx(x)/2) + assert (df_test - df_true).simplify() == 0 + + +def test_differentiate_finite(): + x, y, h = symbols('x y h') + f = Function('f') + with warns_deprecated_sympy(): + res0 = differentiate_finite(f(x, y) + exp(42), x, y, evaluate=True) + xm, xp, ym, yp = [v + sign*S.Half for v, sign in product([x, y], [-1, 1])] + ref0 = f(xm, ym) + f(xp, yp) - f(xm, yp) - f(xp, ym) + assert (res0 - ref0).simplify() == 0 + + g = Function('g') + with warns_deprecated_sympy(): + res1 = differentiate_finite(f(x)*g(x) + 42, x, evaluate=True) + ref1 = (-f(x - S.Half) + f(x + S.Half))*g(x) + \ + (-g(x - S.Half) + g(x + S.Half))*f(x) + assert (res1 - ref1).simplify() == 0 + + res2 = differentiate_finite(f(x) + x**3 + 42, x, points=[x-1, x+1]) + ref2 = (f(x + 1) + (x + 1)**3 - f(x - 1) - (x - 1)**3)/2 + assert (res2 - ref2).simplify() == 0 + raises(TypeError, lambda: differentiate_finite(f(x)*g(x), x, + pints=[x-1, x+1])) + + res3 = differentiate_finite(f(x)*g(x).diff(x), x) + ref3 = (-g(x) + g(x + 1))*f(x + S.Half) - (g(x) - g(x - 1))*f(x - S.Half) + assert res3 == ref3 + + res4 = differentiate_finite(f(x)*g(x).diff(x).diff(x), x) + ref4 = -((g(x - Rational(3, 2)) - 2*g(x - S.Half) + g(x + S.Half))*f(x - S.Half)) \ + + (g(x - S.Half) - 2*g(x + S.Half) + g(x + Rational(3, 2)))*f(x + S.Half) + assert res4 == ref4 + + res5_expr = f(x).diff(x)*g(x).diff(x) + res5 = differentiate_finite(res5_expr, points=[x-h, x, x+h]) + ref5 = (-2*f(x)/h + f(-h + x)/(2*h) + 3*f(h + x)/(2*h))*(-2*g(x)/h + g(-h + x)/(2*h) \ + + 3*g(h + x)/(2*h))/(2*h) - (2*f(x)/h - 3*f(-h + x)/(2*h) - \ + f(h + x)/(2*h))*(2*g(x)/h - 3*g(-h + x)/(2*h) - g(h + x)/(2*h))/(2*h) + assert res5 == ref5 diff --git a/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_singularities.py b/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_singularities.py new file mode 100644 index 0000000000000000000000000000000000000000..19a042332326658021ce12a38f4e058f55903869 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_singularities.py @@ -0,0 +1,122 @@ +from sympy.core.numbers import (I, Rational, pi, oo) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol, Dummy +from sympy.core.function import Lambda +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.trigonometric import sec, csc +from sympy.functions.elementary.hyperbolic import (coth, sech, + atanh, asech, acoth, acsch) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.calculus.singularities import ( + singularities, + is_increasing, + is_strictly_increasing, + is_decreasing, + is_strictly_decreasing, + is_monotonic +) +from sympy.sets import Interval, FiniteSet, Union, ImageSet +from sympy.testing.pytest import raises +from sympy.abc import x, y + + +def test_singularities(): + x = Symbol('x') + assert singularities(x**2, x) == S.EmptySet + assert singularities(x/(x**2 + 3*x + 2), x) == FiniteSet(-2, -1) + assert singularities(1/(x**2 + 1), x) == FiniteSet(I, -I) + assert singularities(x/(x**3 + 1), x) == \ + FiniteSet(-1, (1 - sqrt(3) * I) / 2, (1 + sqrt(3) * I) / 2) + assert singularities(1/(y**2 + 2*I*y + 1), y) == \ + FiniteSet(-I + sqrt(2)*I, -I - sqrt(2)*I) + _n = Dummy('n') + assert singularities(sech(x), x).dummy_eq(Union( + ImageSet(Lambda(_n, 2*_n*I*pi + I*pi/2), S.Integers), + ImageSet(Lambda(_n, 2*_n*I*pi + 3*I*pi/2), S.Integers))) + assert singularities(coth(x), x).dummy_eq(Union( + ImageSet(Lambda(_n, 2*_n*I*pi + I*pi), S.Integers), + ImageSet(Lambda(_n, 2*_n*I*pi), S.Integers))) + assert singularities(atanh(x), x) == FiniteSet(-1, 1) + assert singularities(acoth(x), x) == FiniteSet(-1, 1) + assert singularities(asech(x), x) == FiniteSet(0) + assert singularities(acsch(x), x) == FiniteSet(0) + + x = Symbol('x', real=True) + assert singularities(1/(x**2 + 1), x) == S.EmptySet + assert singularities(exp(1/x), x, S.Reals) == FiniteSet(0) + assert singularities(exp(1/x), x, Interval(1, 2)) == S.EmptySet + assert singularities(log((x - 2)**2), x, Interval(1, 3)) == FiniteSet(2) + raises(NotImplementedError, lambda: singularities(x**-oo, x)) + assert singularities(sec(x), x, Interval(0, 3*pi)) == FiniteSet( + pi/2, 3*pi/2, 5*pi/2) + assert singularities(csc(x), x, Interval(0, 3*pi)) == FiniteSet( + 0, pi, 2*pi, 3*pi) + + +def test_is_increasing(): + """Test whether is_increasing returns correct value.""" + a = Symbol('a', negative=True) + + assert is_increasing(x**3 - 3*x**2 + 4*x, S.Reals) + assert is_increasing(-x**2, Interval(-oo, 0)) + assert not is_increasing(-x**2, Interval(0, oo)) + assert not is_increasing(4*x**3 - 6*x**2 - 72*x + 30, Interval(-2, 3)) + assert is_increasing(x**2 + y, Interval(1, oo), x) + assert is_increasing(-x**2*a, Interval(1, oo), x) + assert is_increasing(1) + + assert is_increasing(4*x**3 - 6*x**2 - 72*x + 30, Interval(-2, 3)) is False + + +def test_is_strictly_increasing(): + """Test whether is_strictly_increasing returns correct value.""" + assert is_strictly_increasing( + 4*x**3 - 6*x**2 - 72*x + 30, Interval.Ropen(-oo, -2)) + assert is_strictly_increasing( + 4*x**3 - 6*x**2 - 72*x + 30, Interval.Lopen(3, oo)) + assert not is_strictly_increasing( + 4*x**3 - 6*x**2 - 72*x + 30, Interval.open(-2, 3)) + assert not is_strictly_increasing(-x**2, Interval(0, oo)) + assert not is_strictly_decreasing(1) + + assert is_strictly_increasing(4*x**3 - 6*x**2 - 72*x + 30, Interval.open(-2, 3)) is False + + +def test_is_decreasing(): + """Test whether is_decreasing returns correct value.""" + b = Symbol('b', positive=True) + + assert is_decreasing(1/(x**2 - 3*x), Interval.open(Rational(3,2), 3)) + assert is_decreasing(1/(x**2 - 3*x), Interval.open(1.5, 3)) + assert is_decreasing(1/(x**2 - 3*x), Interval.Lopen(3, oo)) + assert not is_decreasing(1/(x**2 - 3*x), Interval.Ropen(-oo, Rational(3, 2))) + assert not is_decreasing(-x**2, Interval(-oo, 0)) + assert not is_decreasing(-x**2*b, Interval(-oo, 0), x) + + +def test_is_strictly_decreasing(): + """Test whether is_strictly_decreasing returns correct value.""" + assert is_strictly_decreasing(1/(x**2 - 3*x), Interval.Lopen(3, oo)) + assert not is_strictly_decreasing( + 1/(x**2 - 3*x), Interval.Ropen(-oo, Rational(3, 2))) + assert not is_strictly_decreasing(-x**2, Interval(-oo, 0)) + assert not is_strictly_decreasing(1) + assert is_strictly_decreasing(1/(x**2 - 3*x), Interval.open(Rational(3,2), 3)) + assert is_strictly_decreasing(1/(x**2 - 3*x), Interval.open(1.5, 3)) + + +def test_is_monotonic(): + """Test whether is_monotonic returns correct value.""" + assert is_monotonic(1/(x**2 - 3*x), Interval.open(Rational(3,2), 3)) + assert is_monotonic(1/(x**2 - 3*x), Interval.open(1.5, 3)) + assert is_monotonic(1/(x**2 - 3*x), Interval.Lopen(3, oo)) + assert is_monotonic(x**3 - 3*x**2 + 4*x, S.Reals) + assert not is_monotonic(-x**2, S.Reals) + assert is_monotonic(x**2 + y + 1, Interval(1, 2), x) + raises(NotImplementedError, lambda: is_monotonic(x**2 + y + 1)) + + +def test_issue_23401(): + x = Symbol('x') + expr = (x + 1)/(-1.0e-3*x**2 + 0.1*x + 0.1) + assert is_increasing(expr, Interval(1,2), x) diff --git a/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_util.py b/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_util.py new file mode 100644 index 0000000000000000000000000000000000000000..c18b7a79fd54fdb2638cc746d43ab26753fc72a9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/calculus/tests/test_util.py @@ -0,0 +1,392 @@ +from sympy.core.function import Lambda +from sympy.core.numbers import (E, I, Rational, oo, pi) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol) +from sympy.functions.elementary.complexes import (Abs, re) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.integers import frac +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import ( + cos, cot, csc, sec, sin, tan, asin, acos, atan, acot, asec, acsc) +from sympy.functions.elementary.hyperbolic import (sinh, cosh, tanh, coth, + sech, csch, asinh, acosh, atanh, acoth, asech, acsch) +from sympy.functions.special.gamma_functions import gamma +from sympy.functions.special.error_functions import expint +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.simplify.simplify import simplify +from sympy.calculus.util import (function_range, continuous_domain, not_empty_in, + periodicity, lcim, is_convex, + stationary_points, minimum, maximum) +from sympy.sets.sets import (Interval, FiniteSet, Complement, Union) +from sympy.sets.fancysets import ImageSet +from sympy.sets.conditionset import ConditionSet +from sympy.testing.pytest import XFAIL, raises, _both_exp_pow, slow +from sympy.abc import x, y + +a = Symbol('a', real=True) + +def test_function_range(): + assert function_range(sin(x), x, Interval(-pi/2, pi/2) + ) == Interval(-1, 1) + assert function_range(sin(x), x, Interval(0, pi) + ) == Interval(0, 1) + assert function_range(tan(x), x, Interval(0, pi) + ) == Interval(-oo, oo) + assert function_range(tan(x), x, Interval(pi/2, pi) + ) == Interval(-oo, 0) + assert function_range((x + 3)/(x - 2), x, Interval(-5, 5) + ) == Union(Interval(-oo, Rational(2, 7)), Interval(Rational(8, 3), oo)) + assert function_range(1/(x**2), x, Interval(-1, 1) + ) == Interval(1, oo) + assert function_range(exp(x), x, Interval(-1, 1) + ) == Interval(exp(-1), exp(1)) + assert function_range(log(x) - x, x, S.Reals + ) == Interval(-oo, -1) + assert function_range(sqrt(3*x - 1), x, Interval(0, 2) + ) == Interval(0, sqrt(5)) + assert function_range(x*(x - 1) - (x**2 - x), x, S.Reals + ) == FiniteSet(0) + assert function_range(x*(x - 1) - (x**2 - x) + y, x, S.Reals + ) == FiniteSet(y) + assert function_range(sin(x), x, Union(Interval(-5, -3), FiniteSet(4)) + ) == Union(Interval(-sin(3), 1), FiniteSet(sin(4))) + assert function_range(cos(x), x, Interval(-oo, -4) + ) == Interval(-1, 1) + assert function_range(cos(x), x, S.EmptySet) == S.EmptySet + assert function_range(x/sqrt(x**2+1), x, S.Reals) == Interval.open(-1,1) + raises(NotImplementedError, lambda : function_range( + exp(x)*(sin(x) - cos(x))/2 - x, x, S.Reals)) + raises(NotImplementedError, lambda : function_range( + sin(x) + x, x, S.Reals)) # issue 13273 + raises(NotImplementedError, lambda : function_range( + log(x), x, S.Integers)) + raises(NotImplementedError, lambda : function_range( + sin(x)/2, x, S.Naturals)) + + +@slow +def test_function_range1(): + assert function_range(tan(x)**2 + tan(3*x)**2 + 1, x, S.Reals) == Interval(1,oo) + + +def test_continuous_domain(): + assert continuous_domain(sin(x), x, Interval(0, 2*pi)) == Interval(0, 2*pi) + assert continuous_domain(tan(x), x, Interval(0, 2*pi)) == \ + Union(Interval(0, pi/2, False, True), Interval(pi/2, pi*Rational(3, 2), True, True), + Interval(pi*Rational(3, 2), 2*pi, True, False)) + assert continuous_domain(cot(x), x, Interval(0, 2*pi)) == Union( + Interval.open(0, pi), Interval.open(pi, 2*pi)) + assert continuous_domain((x - 1)/((x - 1)**2), x, S.Reals) == \ + Union(Interval(-oo, 1, True, True), Interval(1, oo, True, True)) + assert continuous_domain(log(x) + log(4*x - 1), x, S.Reals) == \ + Interval(Rational(1, 4), oo, True, True) + assert continuous_domain(1/sqrt(x - 3), x, S.Reals) == Interval(3, oo, True, True) + assert continuous_domain(1/x - 2, x, S.Reals) == \ + Union(Interval.open(-oo, 0), Interval.open(0, oo)) + assert continuous_domain(1/(x**2 - 4) + 2, x, S.Reals) == \ + Union(Interval.open(-oo, -2), Interval.open(-2, 2), Interval.open(2, oo)) + assert continuous_domain((x+1)**pi, x, S.Reals) == Interval(-1, oo) + assert continuous_domain((x+1)**(pi/2), x, S.Reals) == Interval(-1, oo) + assert continuous_domain(x**x, x, S.Reals) == Interval(0, oo) + assert continuous_domain((x+1)**log(x**2), x, S.Reals) == Union( + Interval.Ropen(-1, 0), Interval.open(0, oo)) + domain = continuous_domain(log(tan(x)**2 + 1), x, S.Reals) + assert not domain.contains(3*pi/2) + assert domain.contains(5) + d = Symbol('d', even=True, zero=False) + assert continuous_domain(x**(1/d), x, S.Reals) == Interval(0, oo) + n = Dummy('n') + assert continuous_domain(1/sin(x), x, S.Reals).dummy_eq(Complement( + S.Reals, Union(ImageSet(Lambda(n, 2*n*pi + pi), S.Integers), + ImageSet(Lambda(n, 2*n*pi), S.Integers)))) + assert continuous_domain(sin(x) + cos(x), x, S.Reals) == S.Reals + assert continuous_domain(asin(x), x, S.Reals) == Interval(-1, 1) # issue #21786 + assert continuous_domain(1/acos(log(x)), x, S.Reals) == Interval.Ropen(exp(-1), E) + assert continuous_domain(sinh(x)+cosh(x), x, S.Reals) == S.Reals + assert continuous_domain(tanh(x)+sech(x), x, S.Reals) == S.Reals + assert continuous_domain(atan(x)+asinh(x), x, S.Reals) == S.Reals + assert continuous_domain(acosh(x), x, S.Reals) == Interval(1, oo) + assert continuous_domain(atanh(x), x, S.Reals) == Interval.open(-1, 1) + assert continuous_domain(atanh(x)+acosh(x), x, S.Reals) == S.EmptySet + assert continuous_domain(asech(x), x, S.Reals) == Interval.Lopen(0, 1) + assert continuous_domain(acoth(x), x, S.Reals) == Union( + Interval.open(-oo, -1), Interval.open(1, oo)) + assert continuous_domain(asec(x), x, S.Reals) == Union( + Interval(-oo, -1), Interval(1, oo)) + assert continuous_domain(acsc(x), x, S.Reals) == Union( + Interval(-oo, -1), Interval(1, oo)) + for f in (coth, acsch, csch): + assert continuous_domain(f(x), x, S.Reals) == Union( + Interval.open(-oo, 0), Interval.open(0, oo)) + assert continuous_domain(acot(x), x, S.Reals).contains(0) == False + assert continuous_domain(1/(exp(x) - x), x, S.Reals) == Complement( + S.Reals, ConditionSet(x, Eq(-x + exp(x), 0), S.Reals)) + assert continuous_domain(frac(x**2), x, Interval(-2,-1)) == Union( + Interval.open(-2, -sqrt(3)), Interval.open(-sqrt(2), -1), + Interval.open(-sqrt(3), -sqrt(2))) + assert continuous_domain(frac(x), x, S.Reals) == Complement( + S.Reals, S.Integers) + raises(NotImplementedError, lambda : continuous_domain( + 1/(x**2+1), x, S.Complexes)) + raises(NotImplementedError, lambda : continuous_domain( + gamma(x), x, Interval(-5,0))) + assert continuous_domain(x + gamma(pi), x, S.Reals) == S.Reals + + +@XFAIL +def test_continuous_domain_acot(): + acot_cont = Piecewise((pi+acot(x), x<0), (acot(x), True)) + assert continuous_domain(acot_cont, x, S.Reals) == S.Reals + +@XFAIL +def test_continuous_domain_gamma(): + assert continuous_domain(gamma(x), x, S.Reals).contains(-1) == False + +@XFAIL +def test_continuous_domain_neg_power(): + assert continuous_domain((x-2)**(1-x), x, S.Reals) == Interval.open(2, oo) + + +def test_not_empty_in(): + assert not_empty_in(FiniteSet(x, 2*x).intersect(Interval(1, 2, True, False)), x) == \ + Interval(S.Half, 2, True, False) + assert not_empty_in(FiniteSet(x, x**2).intersect(Interval(1, 2)), x) == \ + Union(Interval(-sqrt(2), -1), Interval(1, 2)) + assert not_empty_in(FiniteSet(x**2 + x, x).intersect(Interval(2, 4)), x) == \ + Union(Interval(-sqrt(17)/2 - S.Half, -2), + Interval(1, Rational(-1, 2) + sqrt(17)/2), Interval(2, 4)) + assert not_empty_in(FiniteSet(x/(x - 1)).intersect(S.Reals), x) == \ + Complement(S.Reals, FiniteSet(1)) + assert not_empty_in(FiniteSet(a/(a - 1)).intersect(S.Reals), a) == \ + Complement(S.Reals, FiniteSet(1)) + assert not_empty_in(FiniteSet((x**2 - 3*x + 2)/(x - 1)).intersect(S.Reals), x) == \ + Complement(S.Reals, FiniteSet(1)) + assert not_empty_in(FiniteSet(3, 4, x/(x - 1)).intersect(Interval(2, 3)), x) == \ + Interval(-oo, oo) + assert not_empty_in(FiniteSet(4, x/(x - 1)).intersect(Interval(2, 3)), x) == \ + Interval(S(3)/2, 2) + assert not_empty_in(FiniteSet(x/(x**2 - 1)).intersect(S.Reals), x) == \ + Complement(S.Reals, FiniteSet(-1, 1)) + assert not_empty_in(FiniteSet(x, x**2).intersect(Union(Interval(1, 3, True, True), + Interval(4, 5))), x) == \ + Union(Interval(-sqrt(5), -2), Interval(-sqrt(3), -1, True, True), + Interval(1, 3, True, True), Interval(4, 5)) + assert not_empty_in(FiniteSet(1).intersect(Interval(3, 4)), x) == S.EmptySet + assert not_empty_in(FiniteSet(x**2/(x + 2)).intersect(Interval(1, oo)), x) == \ + Union(Interval(-2, -1, True, False), Interval(2, oo)) + raises(ValueError, lambda: not_empty_in(x)) + raises(ValueError, lambda: not_empty_in(Interval(0, 1), x)) + raises(NotImplementedError, + lambda: not_empty_in(FiniteSet(x).intersect(S.Reals), x, a)) + + +@_both_exp_pow +def test_periodicity(): + assert periodicity(sin(2*x), x) == pi + assert periodicity((-2)*tan(4*x), x) == pi/4 + assert periodicity(sin(x)**2, x) == 2*pi + assert periodicity(3**tan(3*x), x) == pi/3 + assert periodicity(tan(x)*cos(x), x) == 2*pi + assert periodicity(sin(x)**(tan(x)), x) == 2*pi + assert periodicity(tan(x)*sec(x), x) == 2*pi + assert periodicity(sin(2*x)*cos(2*x) - y, x) == pi/2 + assert periodicity(tan(x) + cot(x), x) == pi + assert periodicity(sin(x) - cos(2*x), x) == 2*pi + assert periodicity(sin(x) - 1, x) == 2*pi + assert periodicity(sin(4*x) + sin(x)*cos(x), x) == pi + assert periodicity(exp(sin(x)), x) == 2*pi + assert periodicity(log(cot(2*x)) - sin(cos(2*x)), x) == pi + assert periodicity(sin(2*x)*exp(tan(x) - csc(2*x)), x) == pi + assert periodicity(cos(sec(x) - csc(2*x)), x) == 2*pi + assert periodicity(tan(sin(2*x)), x) == pi + assert periodicity(2*tan(x)**2, x) == pi + assert periodicity(sin(x%4), x) == 4 + assert periodicity(sin(x)%4, x) == 2*pi + assert periodicity(tan((3*x-2)%4), x) == Rational(4, 3) + assert periodicity((sqrt(2)*(x+1)+x) % 3, x) == 3 / (sqrt(2)+1) + assert periodicity((x**2+1) % x, x) is None + assert periodicity(sin(re(x)), x) == 2*pi + assert periodicity(sin(x)**2 + cos(x)**2, x) is S.Zero + assert periodicity(tan(x), y) is S.Zero + assert periodicity(sin(x) + I*cos(x), x) == 2*pi + assert periodicity(x - sin(2*y), y) == pi + + assert periodicity(exp(x), x) is None + assert periodicity(exp(I*x), x) == 2*pi + assert periodicity(exp(I*a), a) == 2*pi + assert periodicity(exp(a), a) is None + assert periodicity(exp(log(sin(a) + I*cos(2*a)), evaluate=False), a) == 2*pi + assert periodicity(exp(log(sin(2*a) + I*cos(a)), evaluate=False), a) == 2*pi + assert periodicity(exp(sin(a)), a) == 2*pi + assert periodicity(exp(2*I*a), a) == pi + assert periodicity(exp(a + I*sin(a)), a) is None + assert periodicity(exp(cos(a/2) + sin(a)), a) == 4*pi + assert periodicity(log(x), x) is None + assert periodicity(exp(x)**sin(x), x) is None + assert periodicity(sin(x)**y, y) is None + + assert periodicity(Abs(sin(Abs(sin(x)))), x) == pi + assert all(periodicity(Abs(f(x)), x) == pi for f in ( + cos, sin, sec, csc, tan, cot)) + assert periodicity(Abs(sin(tan(x))), x) == pi + assert periodicity(Abs(sin(sin(x) + tan(x))), x) == 2*pi + assert periodicity(sin(x) > S.Half, x) == 2*pi + + assert periodicity(x > 2, x) is None + assert periodicity(x**3 - x**2 + 1, x) is None + assert periodicity(Abs(x), x) is None + assert periodicity(Abs(x**2 - 1), x) is None + + assert periodicity((x**2 + 4)%2, x) is None + assert periodicity((E**x)%3, x) is None + + assert periodicity(sin(expint(1, x))/expint(1, x), x) is None + # returning `None` for any Piecewise + p = Piecewise((0, x < -1), (x**2, x <= 1), (log(x), True)) + assert periodicity(p, x) is None + + m = MatrixSymbol('m', 3, 3) + raises(NotImplementedError, lambda: periodicity(sin(m), m)) + raises(NotImplementedError, lambda: periodicity(sin(m[0, 0]), m)) + raises(NotImplementedError, lambda: periodicity(sin(m), m[0, 0])) + raises(NotImplementedError, lambda: periodicity(sin(m[0, 0]), m[0, 0])) + + +def test_periodicity_check(): + assert periodicity(tan(x), x, check=True) == pi + assert periodicity(sin(x) + cos(x), x, check=True) == 2*pi + assert periodicity(sec(x), x) == 2*pi + assert periodicity(sin(x*y), x) == 2*pi/abs(y) + assert periodicity(Abs(sec(sec(x))), x) == pi + + +def test_lcim(): + assert lcim([S.Half, S(2), S(3)]) == 6 + assert lcim([pi/2, pi/4, pi]) == pi + assert lcim([2*pi, pi/2]) == 2*pi + assert lcim([S.One, 2*pi]) is None + assert lcim([S(2) + 2*E, E/3 + Rational(1, 3), S.One + E]) == S(2) + 2*E + + +def test_is_convex(): + assert is_convex(1/x, x, domain=Interval.open(0, oo)) == True + assert is_convex(1/x, x, domain=Interval(-oo, 0)) == False + assert is_convex(x**2, x, domain=Interval(0, oo)) == True + assert is_convex(1/x**3, x, domain=Interval.Lopen(0, oo)) == True + assert is_convex(-1/x**3, x, domain=Interval.Ropen(-oo, 0)) == True + assert is_convex(log(x) ,x) == False + assert is_convex(x**2+y**2, x, y) == True + assert is_convex(cos(x) + cos(y), x) == False + assert is_convex(8*x**2 - 2*y**2, x, y) == False + + +def test_stationary_points(): + assert stationary_points(sin(x), x, Interval(-pi/2, pi/2) + ) == {-pi/2, pi/2} + assert stationary_points(sin(x), x, Interval.Ropen(0, pi/4) + ) is S.EmptySet + assert stationary_points(tan(x), x, + ) is S.EmptySet + assert stationary_points(sin(x)*cos(x), x, Interval(0, pi) + ) == {pi/4, pi*Rational(3, 4)} + assert stationary_points(sec(x), x, Interval(0, pi) + ) == {0, pi} + assert stationary_points((x+3)*(x-2), x + ) == FiniteSet(Rational(-1, 2)) + assert stationary_points((x + 3)/(x - 2), x, Interval(-5, 5) + ) is S.EmptySet + assert stationary_points((x**2+3)/(x-2), x + ) == {2 - sqrt(7), 2 + sqrt(7)} + assert stationary_points((x**2+3)/(x-2), x, Interval(0, 5) + ) == {2 + sqrt(7)} + assert stationary_points(x**4 + x**3 - 5*x**2, x, S.Reals + ) == FiniteSet(-2, 0, Rational(5, 4)) + assert stationary_points(exp(x), x + ) is S.EmptySet + assert stationary_points(log(x) - x, x, S.Reals + ) == {1} + assert stationary_points(cos(x), x, Union(Interval(0, 5), Interval(-6, -3)) + ) == {0, -pi, pi} + assert stationary_points(y, x, S.Reals + ) == S.Reals + assert stationary_points(y, x, S.EmptySet) == S.EmptySet + + +def test_maximum(): + assert maximum(sin(x), x) is S.One + assert maximum(sin(x), x, Interval(0, 1)) == sin(1) + assert maximum(tan(x), x) is oo + assert maximum(tan(x), x, Interval(-pi/4, pi/4)) is S.One + assert maximum(sin(x)*cos(x), x, S.Reals) == S.Half + assert simplify(maximum(sin(x)*cos(x), x, Interval(pi*Rational(3, 8), pi*Rational(5, 8))) + ) == sqrt(2)/4 + assert maximum((x+3)*(x-2), x) is oo + assert maximum((x+3)*(x-2), x, Interval(-5, 0)) == S(14) + assert maximum((x+3)/(x-2), x, Interval(-5, 0)) == Rational(2, 7) + assert simplify(maximum(-x**4-x**3+x**2+10, x) + ) == 41*sqrt(41)/512 + Rational(5419, 512) + assert maximum(exp(x), x, Interval(-oo, 2)) == exp(2) + assert maximum(log(x) - x, x, S.Reals) is S.NegativeOne + assert maximum(cos(x), x, Union(Interval(0, 5), Interval(-6, -3)) + ) is S.One + assert maximum(cos(x)-sin(x), x, S.Reals) == sqrt(2) + assert maximum(y, x, S.Reals) == y + assert maximum(abs(a**3 + a), a, Interval(0, 2)) == 10 + assert maximum(abs(60*a**3 + 24*a), a, Interval(0, 2)) == 528 + assert maximum(abs(12*a*(5*a**2 + 2)), a, Interval(0, 2)) == 528 + assert maximum(x/sqrt(x**2+1), x, S.Reals) == 1 + + raises(ValueError, lambda : maximum(sin(x), x, S.EmptySet)) + raises(ValueError, lambda : maximum(log(cos(x)), x, S.EmptySet)) + raises(ValueError, lambda : maximum(1/(x**2 + y**2 + 1), x, S.EmptySet)) + raises(ValueError, lambda : maximum(sin(x), sin(x))) + raises(ValueError, lambda : maximum(sin(x), x*y, S.EmptySet)) + raises(ValueError, lambda : maximum(sin(x), S.One)) + + +def test_minimum(): + assert minimum(sin(x), x) is S.NegativeOne + assert minimum(sin(x), x, Interval(1, 4)) == sin(4) + assert minimum(tan(x), x) is -oo + assert minimum(tan(x), x, Interval(-pi/4, pi/4)) is S.NegativeOne + assert minimum(sin(x)*cos(x), x, S.Reals) == Rational(-1, 2) + assert simplify(minimum(sin(x)*cos(x), x, Interval(pi*Rational(3, 8), pi*Rational(5, 8))) + ) == -sqrt(2)/4 + assert minimum((x+3)*(x-2), x) == Rational(-25, 4) + assert minimum((x+3)/(x-2), x, Interval(-5, 0)) == Rational(-3, 2) + assert minimum(x**4-x**3+x**2+10, x) == S(10) + assert minimum(exp(x), x, Interval(-2, oo)) == exp(-2) + assert minimum(log(x) - x, x, S.Reals) is -oo + assert minimum(cos(x), x, Union(Interval(0, 5), Interval(-6, -3)) + ) is S.NegativeOne + assert minimum(cos(x)-sin(x), x, S.Reals) == -sqrt(2) + assert minimum(y, x, S.Reals) == y + assert minimum(x/sqrt(x**2+1), x, S.Reals) == -1 + + raises(ValueError, lambda : minimum(sin(x), x, S.EmptySet)) + raises(ValueError, lambda : minimum(log(cos(x)), x, S.EmptySet)) + raises(ValueError, lambda : minimum(1/(x**2 + y**2 + 1), x, S.EmptySet)) + raises(ValueError, lambda : minimum(sin(x), sin(x))) + raises(ValueError, lambda : minimum(sin(x), x*y, S.EmptySet)) + raises(ValueError, lambda : minimum(sin(x), S.One)) + + +def test_issue_19869(): + assert (maximum(sqrt(3)*(x - 1)/(3*sqrt(x**2 + 1)), x) + ) == sqrt(3)/3 + + +def test_issue_16469(): + f = abs(a) + assert function_range(f, a, S.Reals) == Interval(0, oo, False, True) + + +@_both_exp_pow +def test_issue_18747(): + assert periodicity(exp(pi*I*(x/4 + S.Half/2)), x) == 8 + + +def test_issue_25942(): + assert (acos(x) > pi/3).as_set() == Interval.Ropen(-1, S(1)/2) diff --git a/venv/lib/python3.11/site-packages/sympy/calculus/util.py b/venv/lib/python3.11/site-packages/sympy/calculus/util.py new file mode 100644 index 0000000000000000000000000000000000000000..dac316358873de2418dd8ab56445823f07a37b1b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/calculus/util.py @@ -0,0 +1,895 @@ +from .accumulationbounds import AccumBounds, AccumulationBounds # noqa: F401 +from .singularities import singularities +from sympy.core import Pow, S +from sympy.core.function import diff, expand_mul, Function +from sympy.core.kind import NumberKind +from sympy.core.mod import Mod +from sympy.core.numbers import equal_valued +from sympy.core.relational import Relational +from sympy.core.symbol import Symbol, Dummy +from sympy.core.sympify import _sympify +from sympy.functions.elementary.complexes import Abs, im, re +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.integers import frac +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import ( + TrigonometricFunction, sin, cos, tan, cot, csc, sec, + asin, acos, acot, atan, asec, acsc) +from sympy.functions.elementary.hyperbolic import (sinh, cosh, tanh, coth, + sech, csch, asinh, acosh, atanh, acoth, asech, acsch) +from sympy.polys.polytools import degree, lcm_list +from sympy.sets.sets import (Interval, Intersection, FiniteSet, Union, + Complement) +from sympy.sets.fancysets import ImageSet +from sympy.sets.conditionset import ConditionSet +from sympy.utilities import filldedent +from sympy.utilities.iterables import iterable +from sympy.matrices.dense import hessian + + +def continuous_domain(f, symbol, domain): + """ + Returns the domain on which the function expression f is continuous. + + This function is limited by the ability to determine the various + singularities and discontinuities of the given function. + The result is either given as a union of intervals or constructed using + other set operations. + + Parameters + ========== + + f : :py:class:`~.Expr` + The concerned function. + symbol : :py:class:`~.Symbol` + The variable for which the intervals are to be determined. + domain : :py:class:`~.Interval` + The domain over which the continuity of the symbol has to be checked. + + Examples + ======== + + >>> from sympy import Interval, Symbol, S, tan, log, pi, sqrt + >>> from sympy.calculus.util import continuous_domain + >>> x = Symbol('x') + >>> continuous_domain(1/x, x, S.Reals) + Union(Interval.open(-oo, 0), Interval.open(0, oo)) + >>> continuous_domain(tan(x), x, Interval(0, pi)) + Union(Interval.Ropen(0, pi/2), Interval.Lopen(pi/2, pi)) + >>> continuous_domain(sqrt(x - 2), x, Interval(-5, 5)) + Interval(2, 5) + >>> continuous_domain(log(2*x - 1), x, S.Reals) + Interval.open(1/2, oo) + + Returns + ======= + + :py:class:`~.Interval` + Union of all intervals where the function is continuous. + + Raises + ====== + + NotImplementedError + If the method to determine continuity of such a function + has not yet been developed. + + """ + from sympy.solvers.inequalities import solve_univariate_inequality + + if not domain.is_subset(S.Reals): + raise NotImplementedError(filldedent(''' + Domain must be a subset of S.Reals. + ''')) + implemented = [Pow, exp, log, Abs, frac, + sin, cos, tan, cot, sec, csc, + asin, acos, atan, acot, asec, acsc, + sinh, cosh, tanh, coth, sech, csch, + asinh, acosh, atanh, acoth, asech, acsch] + used = [fct.func for fct in f.atoms(Function) if fct.has(symbol)] + if any(func not in implemented for func in used): + raise NotImplementedError(filldedent(''' + Unable to determine the domain of the given function. + ''')) + + x = Symbol('x') + constraints = { + log: (x > 0,), + asin: (x >= -1, x <= 1), + acos: (x >= -1, x <= 1), + acosh: (x >= 1,), + atanh: (x > -1, x < 1), + asech: (x > 0, x <= 1) + } + constraints_union = { + asec: (x <= -1, x >= 1), + acsc: (x <= -1, x >= 1), + acoth: (x < -1, x > 1) + } + + cont_domain = domain + for atom in f.atoms(Pow): + den = atom.exp.as_numer_denom()[1] + if atom.exp.is_rational and den.is_odd: + pass # 0**negative handled by singularities() + else: + constraint = solve_univariate_inequality(atom.base >= 0, + symbol).as_set() + cont_domain = Intersection(constraint, cont_domain) + + for atom in f.atoms(Function): + if atom.func in constraints: + for c in constraints[atom.func]: + constraint_relational = c.subs(x, atom.args[0]) + constraint_set = solve_univariate_inequality( + constraint_relational, symbol).as_set() + cont_domain = Intersection(constraint_set, cont_domain) + elif atom.func in constraints_union: + constraint_set = S.EmptySet + for c in constraints_union[atom.func]: + constraint_relational = c.subs(x, atom.args[0]) + constraint_set += solve_univariate_inequality( + constraint_relational, symbol).as_set() + cont_domain = Intersection(constraint_set, cont_domain) + # XXX: the discontinuities below could be factored out in + # a new "discontinuities()". + elif atom.func == acot: + from sympy.solvers.solveset import solveset_real + # Sympy's acot() has a step discontinuity at 0. Since it's + # neither an essential singularity nor a pole, singularities() + # will not report it. But it's still relevant for determining + # the continuity of the function f. + cont_domain -= solveset_real(atom.args[0], symbol) + # Note that the above may introduce spurious discontinuities, e.g. + # for abs(acot(x)) at 0. + elif atom.func == frac: + from sympy.solvers.solveset import solveset_real + r = function_range(atom.args[0], symbol, domain) + r = Intersection(r, S.Integers) + if r.is_finite_set: + discont = S.EmptySet + for n in r: + discont += solveset_real(atom.args[0]-n, symbol) + else: + discont = ConditionSet( + symbol, S.Integers.contains(atom.args[0]), cont_domain) + cont_domain -= discont + + return cont_domain - singularities(f, symbol, domain) + + +def function_range(f, symbol, domain): + """ + Finds the range of a function in a given domain. + This method is limited by the ability to determine the singularities and + determine limits. + + Parameters + ========== + + f : :py:class:`~.Expr` + The concerned function. + symbol : :py:class:`~.Symbol` + The variable for which the range of function is to be determined. + domain : :py:class:`~.Interval` + The domain under which the range of the function has to be found. + + Examples + ======== + + >>> from sympy import Interval, Symbol, S, exp, log, pi, sqrt, sin, tan + >>> from sympy.calculus.util import function_range + >>> x = Symbol('x') + >>> function_range(sin(x), x, Interval(0, 2*pi)) + Interval(-1, 1) + >>> function_range(tan(x), x, Interval(-pi/2, pi/2)) + Interval(-oo, oo) + >>> function_range(1/x, x, S.Reals) + Union(Interval.open(-oo, 0), Interval.open(0, oo)) + >>> function_range(exp(x), x, S.Reals) + Interval.open(0, oo) + >>> function_range(log(x), x, S.Reals) + Interval(-oo, oo) + >>> function_range(sqrt(x), x, Interval(-5, 9)) + Interval(0, 3) + + Returns + ======= + + :py:class:`~.Interval` + Union of all ranges for all intervals under domain where function is + continuous. + + Raises + ====== + + NotImplementedError + If any of the intervals, in the given domain, for which function + is continuous are not finite or real, + OR if the critical points of the function on the domain cannot be found. + """ + + if domain is S.EmptySet: + return S.EmptySet + + period = periodicity(f, symbol) + if period == S.Zero: + # the expression is constant wrt symbol + return FiniteSet(f.expand()) + + from sympy.series.limits import limit + from sympy.solvers.solveset import solveset + + if period is not None: + if isinstance(domain, Interval): + if (domain.inf - domain.sup).is_infinite: + domain = Interval(0, period) + elif isinstance(domain, Union): + for sub_dom in domain.args: + if isinstance(sub_dom, Interval) and \ + ((sub_dom.inf - sub_dom.sup).is_infinite): + domain = Interval(0, period) + + intervals = continuous_domain(f, symbol, domain) + range_int = S.EmptySet + if isinstance(intervals,(Interval, FiniteSet)): + interval_iter = (intervals,) + elif isinstance(intervals, Union): + interval_iter = intervals.args + else: + raise NotImplementedError("Unable to find range for the given domain.") + + for interval in interval_iter: + if isinstance(interval, FiniteSet): + for singleton in interval: + if singleton in domain: + range_int += FiniteSet(f.subs(symbol, singleton)) + elif isinstance(interval, Interval): + vals = S.EmptySet + critical_values = S.EmptySet + bounds = ((interval.left_open, interval.inf, '+'), + (interval.right_open, interval.sup, '-')) + + for is_open, limit_point, direction in bounds: + if is_open: + critical_values += FiniteSet(limit(f, symbol, limit_point, direction)) + vals += critical_values + else: + vals += FiniteSet(f.subs(symbol, limit_point)) + + critical_points = solveset(f.diff(symbol), symbol, interval) + + if not iterable(critical_points): + raise NotImplementedError( + 'Unable to find critical points for {}'.format(f)) + if isinstance(critical_points, ImageSet): + raise NotImplementedError( + 'Infinite number of critical points for {}'.format(f)) + + for critical_point in critical_points: + vals += FiniteSet(f.subs(symbol, critical_point)) + + left_open, right_open = False, False + + if critical_values is not S.EmptySet: + if critical_values.inf == vals.inf: + left_open = True + + if critical_values.sup == vals.sup: + right_open = True + + range_int += Interval(vals.inf, vals.sup, left_open, right_open) + else: + raise NotImplementedError("Unable to find range for the given domain.") + + return range_int + + +def not_empty_in(finset_intersection, *syms): + """ + Finds the domain of the functions in ``finset_intersection`` in which the + ``finite_set`` is not-empty. + + Parameters + ========== + + finset_intersection : Intersection of FiniteSet + The unevaluated intersection of FiniteSet containing + real-valued functions with Union of Sets + syms : Tuple of symbols + Symbol for which domain is to be found + + Raises + ====== + + NotImplementedError + The algorithms to find the non-emptiness of the given FiniteSet are + not yet implemented. + ValueError + The input is not valid. + RuntimeError + It is a bug, please report it to the github issue tracker + (https://github.com/sympy/sympy/issues). + + Examples + ======== + + >>> from sympy import FiniteSet, Interval, not_empty_in, oo + >>> from sympy.abc import x + >>> not_empty_in(FiniteSet(x/2).intersect(Interval(0, 1)), x) + Interval(0, 2) + >>> not_empty_in(FiniteSet(x, x**2).intersect(Interval(1, 2)), x) + Union(Interval(1, 2), Interval(-sqrt(2), -1)) + >>> not_empty_in(FiniteSet(x**2/(x + 2)).intersect(Interval(1, oo)), x) + Union(Interval.Lopen(-2, -1), Interval(2, oo)) + """ + + # TODO: handle piecewise defined functions + # TODO: handle transcendental functions + # TODO: handle multivariate functions + if len(syms) == 0: + raise ValueError("One or more symbols must be given in syms.") + + if finset_intersection is S.EmptySet: + return S.EmptySet + + if isinstance(finset_intersection, Union): + elm_in_sets = finset_intersection.args[0] + return Union(not_empty_in(finset_intersection.args[1], *syms), + elm_in_sets) + + if isinstance(finset_intersection, FiniteSet): + finite_set = finset_intersection + _sets = S.Reals + else: + finite_set = finset_intersection.args[1] + _sets = finset_intersection.args[0] + + if not isinstance(finite_set, FiniteSet): + raise ValueError('A FiniteSet must be given, not %s: %s' % + (type(finite_set), finite_set)) + + if len(syms) == 1: + symb = syms[0] + else: + raise NotImplementedError('more than one variables %s not handled' % + (syms,)) + + def elm_domain(expr, intrvl): + """ Finds the domain of an expression in any given interval """ + from sympy.solvers.solveset import solveset + + _start = intrvl.start + _end = intrvl.end + _singularities = solveset(expr.as_numer_denom()[1], symb, + domain=S.Reals) + + if intrvl.right_open: + if _end is S.Infinity: + _domain1 = S.Reals + else: + _domain1 = solveset(expr < _end, symb, domain=S.Reals) + else: + _domain1 = solveset(expr <= _end, symb, domain=S.Reals) + + if intrvl.left_open: + if _start is S.NegativeInfinity: + _domain2 = S.Reals + else: + _domain2 = solveset(expr > _start, symb, domain=S.Reals) + else: + _domain2 = solveset(expr >= _start, symb, domain=S.Reals) + + # domain in the interval + expr_with_sing = Intersection(_domain1, _domain2) + expr_domain = Complement(expr_with_sing, _singularities) + return expr_domain + + if isinstance(_sets, Interval): + return Union(*[elm_domain(element, _sets) for element in finite_set]) + + if isinstance(_sets, Union): + _domain = S.EmptySet + for intrvl in _sets.args: + _domain_element = Union(*[elm_domain(element, intrvl) + for element in finite_set]) + _domain = Union(_domain, _domain_element) + return _domain + + +def periodicity(f, symbol, check=False): + """ + Tests the given function for periodicity in the given symbol. + + Parameters + ========== + + f : :py:class:`~.Expr` + The concerned function. + symbol : :py:class:`~.Symbol` + The variable for which the period is to be determined. + check : bool, optional + The flag to verify whether the value being returned is a period or not. + + Returns + ======= + + period + The period of the function is returned. + ``None`` is returned when the function is aperiodic or has a complex period. + The value of $0$ is returned as the period of a constant function. + + Raises + ====== + + NotImplementedError + The value of the period computed cannot be verified. + + + Notes + ===== + + Currently, we do not support functions with a complex period. + The period of functions having complex periodic values such + as ``exp``, ``sinh`` is evaluated to ``None``. + + The value returned might not be the "fundamental" period of the given + function i.e. it may not be the smallest periodic value of the function. + + The verification of the period through the ``check`` flag is not reliable + due to internal simplification of the given expression. Hence, it is set + to ``False`` by default. + + Examples + ======== + >>> from sympy import periodicity, Symbol, sin, cos, tan, exp + >>> x = Symbol('x') + >>> f = sin(x) + sin(2*x) + sin(3*x) + >>> periodicity(f, x) + 2*pi + >>> periodicity(sin(x)*cos(x), x) + pi + >>> periodicity(exp(tan(2*x) - 1), x) + pi/2 + >>> periodicity(sin(4*x)**cos(2*x), x) + pi + >>> periodicity(exp(x), x) + """ + if symbol.kind is not NumberKind: + raise NotImplementedError("Cannot use symbol of kind %s" % symbol.kind) + temp = Dummy('x', real=True) + f = f.subs(symbol, temp) + symbol = temp + + def _check(orig_f, period): + '''Return the checked period or raise an error.''' + new_f = orig_f.subs(symbol, symbol + period) + if new_f.equals(orig_f): + return period + else: + raise NotImplementedError(filldedent(''' + The period of the given function cannot be verified. + When `%s` was replaced with `%s + %s` in `%s`, the result + was `%s` which was not recognized as being the same as + the original function. + So either the period was wrong or the two forms were + not recognized as being equal. + Set check=False to obtain the value.''' % + (symbol, symbol, period, orig_f, new_f))) + + orig_f = f + period = None + + if isinstance(f, Relational): + f = f.lhs - f.rhs + + f = f.simplify() + + if symbol not in f.free_symbols: + return S.Zero + + if isinstance(f, TrigonometricFunction): + try: + period = f.period(symbol) + except NotImplementedError: + pass + + if isinstance(f, Abs): + arg = f.args[0] + if isinstance(arg, (sec, csc, cos)): + # all but tan and cot might have a + # a period that is half as large + # so recast as sin + arg = sin(arg.args[0]) + period = periodicity(arg, symbol) + if period is not None and isinstance(arg, sin): + # the argument of Abs was a trigonometric other than + # cot or tan; test to see if the half-period + # is valid. Abs(arg) has behaviour equivalent to + # orig_f, so use that for test: + orig_f = Abs(arg) + try: + return _check(orig_f, period/2) + except NotImplementedError as err: + if check: + raise NotImplementedError(err) + # else let new orig_f and period be + # checked below + + if isinstance(f, exp) or (f.is_Pow and f.base == S.Exp1): + f = Pow(S.Exp1, expand_mul(f.exp)) + if im(f) != 0: + period_real = periodicity(re(f), symbol) + period_imag = periodicity(im(f), symbol) + if period_real is not None and period_imag is not None: + period = lcim([period_real, period_imag]) + + if f.is_Pow and f.base != S.Exp1: + base, expo = f.args + base_has_sym = base.has(symbol) + expo_has_sym = expo.has(symbol) + + if base_has_sym and not expo_has_sym: + period = periodicity(base, symbol) + + elif expo_has_sym and not base_has_sym: + period = periodicity(expo, symbol) + + else: + period = _periodicity(f.args, symbol) + + elif f.is_Mul: + coeff, g = f.as_independent(symbol, as_Add=False) + if isinstance(g, TrigonometricFunction) or not equal_valued(coeff, 1): + period = periodicity(g, symbol) + else: + period = _periodicity(g.args, symbol) + + elif f.is_Add: + k, g = f.as_independent(symbol) + if k is not S.Zero: + return periodicity(g, symbol) + + period = _periodicity(g.args, symbol) + + elif isinstance(f, Mod): + a, n = f.args + + if a == symbol: + period = n + elif isinstance(a, TrigonometricFunction): + period = periodicity(a, symbol) + #check if 'f' is linear in 'symbol' + elif (a.is_polynomial(symbol) and degree(a, symbol) == 1 and + symbol not in n.free_symbols): + period = Abs(n / a.diff(symbol)) + + elif isinstance(f, Piecewise): + pass # not handling Piecewise yet as the return type is not favorable + + elif period is None: + from sympy.solvers.decompogen import compogen, decompogen + g_s = decompogen(f, symbol) + num_of_gs = len(g_s) + if num_of_gs > 1: + for index, g in enumerate(reversed(g_s)): + start_index = num_of_gs - 1 - index + g = compogen(g_s[start_index:], symbol) + if g not in (orig_f, f): # Fix for issue 12620 + period = periodicity(g, symbol) + if period is not None: + break + + if period is not None: + if check: + return _check(orig_f, period) + return period + + return None + + +def _periodicity(args, symbol): + """ + Helper for `periodicity` to find the period of a list of simpler + functions. + It uses the `lcim` method to find the least common period of + all the functions. + + Parameters + ========== + + args : Tuple of :py:class:`~.Symbol` + All the symbols present in a function. + + symbol : :py:class:`~.Symbol` + The symbol over which the function is to be evaluated. + + Returns + ======= + + period + The least common period of the function for all the symbols + of the function. + ``None`` if for at least one of the symbols the function is aperiodic. + + """ + periods = [] + for f in args: + period = periodicity(f, symbol) + if period is None: + return None + + if period is not S.Zero: + periods.append(period) + + if len(periods) > 1: + return lcim(periods) + + if periods: + return periods[0] + + +def lcim(numbers): + """Returns the least common integral multiple of a list of numbers. + + The numbers can be rational or irrational or a mixture of both. + `None` is returned for incommensurable numbers. + + Parameters + ========== + + numbers : list + Numbers (rational and/or irrational) for which lcim is to be found. + + Returns + ======= + + number + lcim if it exists, otherwise ``None`` for incommensurable numbers. + + Examples + ======== + + >>> from sympy.calculus.util import lcim + >>> from sympy import S, pi + >>> lcim([S(1)/2, S(3)/4, S(5)/6]) + 15/2 + >>> lcim([2*pi, 3*pi, pi, pi/2]) + 6*pi + >>> lcim([S(1), 2*pi]) + """ + result = None + if all(num.is_irrational for num in numbers): + factorized_nums = [num.factor() for num in numbers] + factors_num = [num.as_coeff_Mul() for num in factorized_nums] + term = factors_num[0][1] + if all(factor == term for coeff, factor in factors_num): + common_term = term + coeffs = [coeff for coeff, factor in factors_num] + result = lcm_list(coeffs) * common_term + + elif all(num.is_rational for num in numbers): + result = lcm_list(numbers) + + else: + pass + + return result + +def is_convex(f, *syms, domain=S.Reals): + r"""Determines the convexity of the function passed in the argument. + + Parameters + ========== + + f : :py:class:`~.Expr` + The concerned function. + syms : Tuple of :py:class:`~.Symbol` + The variables with respect to which the convexity is to be determined. + domain : :py:class:`~.Interval`, optional + The domain over which the convexity of the function has to be checked. + If unspecified, S.Reals will be the default domain. + + Returns + ======= + + bool + The method returns ``True`` if the function is convex otherwise it + returns ``False``. + + Raises + ====== + + NotImplementedError + The check for the convexity of multivariate functions is not implemented yet. + + Notes + ===== + + To determine concavity of a function pass `-f` as the concerned function. + To determine logarithmic convexity of a function pass `\log(f)` as + concerned function. + To determine logarithmic concavity of a function pass `-\log(f)` as + concerned function. + + Currently, convexity check of multivariate functions is not handled. + + Examples + ======== + + >>> from sympy import is_convex, symbols, exp, oo, Interval + >>> x = symbols('x') + >>> is_convex(exp(x), x) + True + >>> is_convex(x**3, x, domain = Interval(-1, oo)) + False + >>> is_convex(1/x**2, x, domain=Interval.open(0, oo)) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Convex_function + .. [2] http://www.ifp.illinois.edu/~angelia/L3_convfunc.pdf + .. [3] https://en.wikipedia.org/wiki/Logarithmically_convex_function + .. [4] https://en.wikipedia.org/wiki/Logarithmically_concave_function + .. [5] https://en.wikipedia.org/wiki/Concave_function + + """ + if len(syms) > 1 : + return hessian(f, syms).is_positive_semidefinite + from sympy.solvers.inequalities import solve_univariate_inequality + f = _sympify(f) + var = syms[0] + if any(s in domain for s in singularities(f, var)): + return False + condition = f.diff(var, 2) < 0 + if solve_univariate_inequality(condition, var, False, domain): + return False + return True + + +def stationary_points(f, symbol, domain=S.Reals): + """ + Returns the stationary points of a function (where derivative of the + function is 0) in the given domain. + + Parameters + ========== + + f : :py:class:`~.Expr` + The concerned function. + symbol : :py:class:`~.Symbol` + The variable for which the stationary points are to be determined. + domain : :py:class:`~.Interval` + The domain over which the stationary points have to be checked. + If unspecified, ``S.Reals`` will be the default domain. + + Returns + ======= + + Set + A set of stationary points for the function. If there are no + stationary point, an :py:class:`~.EmptySet` is returned. + + Examples + ======== + + >>> from sympy import Interval, Symbol, S, sin, pi, pprint, stationary_points + >>> x = Symbol('x') + + >>> stationary_points(1/x, x, S.Reals) + EmptySet + + >>> pprint(stationary_points(sin(x), x), use_unicode=False) + pi 3*pi + {2*n*pi + -- | n in Integers} U {2*n*pi + ---- | n in Integers} + 2 2 + + >>> stationary_points(sin(x),x, Interval(0, 4*pi)) + {pi/2, 3*pi/2, 5*pi/2, 7*pi/2} + + """ + from sympy.solvers.solveset import solveset + + if domain is S.EmptySet: + return S.EmptySet + + domain = continuous_domain(f, symbol, domain) + set = solveset(diff(f, symbol), symbol, domain) + + return set + + +def maximum(f, symbol, domain=S.Reals): + """ + Returns the maximum value of a function in the given domain. + + Parameters + ========== + + f : :py:class:`~.Expr` + The concerned function. + symbol : :py:class:`~.Symbol` + The variable for maximum value needs to be determined. + domain : :py:class:`~.Interval` + The domain over which the maximum have to be checked. + If unspecified, then the global maximum is returned. + + Returns + ======= + + number + Maximum value of the function in given domain. + + Examples + ======== + + >>> from sympy import Interval, Symbol, S, sin, cos, pi, maximum + >>> x = Symbol('x') + + >>> f = -x**2 + 2*x + 5 + >>> maximum(f, x, S.Reals) + 6 + + >>> maximum(sin(x), x, Interval(-pi, pi/4)) + sqrt(2)/2 + + >>> maximum(sin(x)*cos(x), x) + 1/2 + + """ + if isinstance(symbol, Symbol): + if domain is S.EmptySet: + raise ValueError("Maximum value not defined for empty domain.") + + return function_range(f, symbol, domain).sup + else: + raise ValueError("%s is not a valid symbol." % symbol) + + +def minimum(f, symbol, domain=S.Reals): + """ + Returns the minimum value of a function in the given domain. + + Parameters + ========== + + f : :py:class:`~.Expr` + The concerned function. + symbol : :py:class:`~.Symbol` + The variable for minimum value needs to be determined. + domain : :py:class:`~.Interval` + The domain over which the minimum have to be checked. + If unspecified, then the global minimum is returned. + + Returns + ======= + + number + Minimum value of the function in the given domain. + + Examples + ======== + + >>> from sympy import Interval, Symbol, S, sin, cos, minimum + >>> x = Symbol('x') + + >>> f = x**2 + 2*x + 5 + >>> minimum(f, x, S.Reals) + 4 + + >>> minimum(sin(x), x, Interval(2, 3)) + sin(3) + + >>> minimum(sin(x)*cos(x), x) + -1/2 + + """ + if isinstance(symbol, Symbol): + if domain is S.EmptySet: + raise ValueError("Minimum value not defined for empty domain.") + + return function_range(f, symbol, domain).inf + else: + raise ValueError("%s is not a valid symbol." % symbol) diff --git a/venv/lib/python3.11/site-packages/sympy/categories/__init__.py b/venv/lib/python3.11/site-packages/sympy/categories/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4c5007308a1b232e57f9ed164276862df0c5f265 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/categories/__init__.py @@ -0,0 +1,33 @@ +""" +Category Theory module. + +Provides some of the fundamental category-theory-related classes, +including categories, morphisms, diagrams. Functors are not +implemented yet. + +The general reference work this module tries to follow is + + [JoyOfCats] J. Adamek, H. Herrlich. G. E. Strecker: Abstract and + Concrete Categories. The Joy of Cats. + +The latest version of this book should be available for free download +from + + katmat.math.uni-bremen.de/acc/acc.pdf + +""" + +from .baseclasses import (Object, Morphism, IdentityMorphism, + NamedMorphism, CompositeMorphism, Category, + Diagram) + +from .diagram_drawing import (DiagramGrid, XypicDiagramDrawer, + xypic_draw_diagram, preview_diagram) + +__all__ = [ + 'Object', 'Morphism', 'IdentityMorphism', 'NamedMorphism', + 'CompositeMorphism', 'Category', 'Diagram', + + 'DiagramGrid', 'XypicDiagramDrawer', 'xypic_draw_diagram', + 'preview_diagram', +] diff --git a/venv/lib/python3.11/site-packages/sympy/categories/baseclasses.py b/venv/lib/python3.11/site-packages/sympy/categories/baseclasses.py new file mode 100644 index 0000000000000000000000000000000000000000..e6ab5153ae4e95f193030864c8f32a52254f2458 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/categories/baseclasses.py @@ -0,0 +1,978 @@ +from sympy.core import S, Basic, Dict, Symbol, Tuple, sympify +from sympy.core.symbol import Str +from sympy.sets import Set, FiniteSet, EmptySet +from sympy.utilities.iterables import iterable + + +class Class(Set): + r""" + The base class for any kind of class in the set-theoretic sense. + + Explanation + =========== + + In axiomatic set theories, everything is a class. A class which + can be a member of another class is a set. A class which is not a + member of another class is a proper class. The class `\{1, 2\}` + is a set; the class of all sets is a proper class. + + This class is essentially a synonym for :class:`sympy.core.Set`. + The goal of this class is to assure easier migration to the + eventual proper implementation of set theory. + """ + is_proper = False + + +class Object(Symbol): + """ + The base class for any kind of object in an abstract category. + + Explanation + =========== + + While technically any instance of :class:`~.Basic` will do, this + class is the recommended way to create abstract objects in + abstract categories. + """ + + +class Morphism(Basic): + """ + The base class for any morphism in an abstract category. + + Explanation + =========== + + In abstract categories, a morphism is an arrow between two + category objects. The object where the arrow starts is called the + domain, while the object where the arrow ends is called the + codomain. + + Two morphisms between the same pair of objects are considered to + be the same morphisms. To distinguish between morphisms between + the same objects use :class:`NamedMorphism`. + + It is prohibited to instantiate this class. Use one of the + derived classes instead. + + See Also + ======== + + IdentityMorphism, NamedMorphism, CompositeMorphism + """ + def __new__(cls, domain, codomain): + raise(NotImplementedError( + "Cannot instantiate Morphism. Use derived classes instead.")) + + @property + def domain(self): + """ + Returns the domain of the morphism. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> f = NamedMorphism(A, B, "f") + >>> f.domain + Object("A") + + """ + return self.args[0] + + @property + def codomain(self): + """ + Returns the codomain of the morphism. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> f = NamedMorphism(A, B, "f") + >>> f.codomain + Object("B") + + """ + return self.args[1] + + def compose(self, other): + r""" + Composes self with the supplied morphism. + + The order of elements in the composition is the usual order, + i.e., to construct `g\circ f` use ``g.compose(f)``. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> g * f + CompositeMorphism((NamedMorphism(Object("A"), Object("B"), "f"), + NamedMorphism(Object("B"), Object("C"), "g"))) + >>> (g * f).domain + Object("A") + >>> (g * f).codomain + Object("C") + + """ + return CompositeMorphism(other, self) + + def __mul__(self, other): + r""" + Composes self with the supplied morphism. + + The semantics of this operation is given by the following + equation: ``g * f == g.compose(f)`` for composable morphisms + ``g`` and ``f``. + + See Also + ======== + + compose + """ + return self.compose(other) + + +class IdentityMorphism(Morphism): + """ + Represents an identity morphism. + + Explanation + =========== + + An identity morphism is a morphism with equal domain and codomain, + which acts as an identity with respect to composition. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, IdentityMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> f = NamedMorphism(A, B, "f") + >>> id_A = IdentityMorphism(A) + >>> id_B = IdentityMorphism(B) + >>> f * id_A == f + True + >>> id_B * f == f + True + + See Also + ======== + + Morphism + """ + def __new__(cls, domain): + return Basic.__new__(cls, domain) + + @property + def codomain(self): + return self.domain + + +class NamedMorphism(Morphism): + """ + Represents a morphism which has a name. + + Explanation + =========== + + Names are used to distinguish between morphisms which have the + same domain and codomain: two named morphisms are equal if they + have the same domains, codomains, and names. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> f = NamedMorphism(A, B, "f") + >>> f + NamedMorphism(Object("A"), Object("B"), "f") + >>> f.name + 'f' + + See Also + ======== + + Morphism + """ + def __new__(cls, domain, codomain, name): + if not name: + raise ValueError("Empty morphism names not allowed.") + + if not isinstance(name, Str): + name = Str(name) + + return Basic.__new__(cls, domain, codomain, name) + + @property + def name(self): + """ + Returns the name of the morphism. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> f = NamedMorphism(A, B, "f") + >>> f.name + 'f' + + """ + return self.args[2].name + + +class CompositeMorphism(Morphism): + r""" + Represents a morphism which is a composition of other morphisms. + + Explanation + =========== + + Two composite morphisms are equal if the morphisms they were + obtained from (components) are the same and were listed in the + same order. + + The arguments to the constructor for this class should be listed + in diagram order: to obtain the composition `g\circ f` from the + instances of :class:`Morphism` ``g`` and ``f`` use + ``CompositeMorphism(f, g)``. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, CompositeMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> g * f + CompositeMorphism((NamedMorphism(Object("A"), Object("B"), "f"), + NamedMorphism(Object("B"), Object("C"), "g"))) + >>> CompositeMorphism(f, g) == g * f + True + + """ + @staticmethod + def _add_morphism(t, morphism): + """ + Intelligently adds ``morphism`` to tuple ``t``. + + Explanation + =========== + + If ``morphism`` is a composite morphism, its components are + added to the tuple. If ``morphism`` is an identity, nothing + is added to the tuple. + + No composability checks are performed. + """ + if isinstance(morphism, CompositeMorphism): + # ``morphism`` is a composite morphism; we have to + # denest its components. + return t + morphism.components + elif isinstance(morphism, IdentityMorphism): + # ``morphism`` is an identity. Nothing happens. + return t + else: + return t + Tuple(morphism) + + def __new__(cls, *components): + if components and not isinstance(components[0], Morphism): + # Maybe the user has explicitly supplied a list of + # morphisms. + return CompositeMorphism.__new__(cls, *components[0]) + + normalised_components = Tuple() + + for current, following in zip(components, components[1:]): + if not isinstance(current, Morphism) or \ + not isinstance(following, Morphism): + raise TypeError("All components must be morphisms.") + + if current.codomain != following.domain: + raise ValueError("Uncomposable morphisms.") + + normalised_components = CompositeMorphism._add_morphism( + normalised_components, current) + + # We haven't added the last morphism to the list of normalised + # components. Add it now. + normalised_components = CompositeMorphism._add_morphism( + normalised_components, components[-1]) + + if not normalised_components: + # If ``normalised_components`` is empty, only identities + # were supplied. Since they all were composable, they are + # all the same identities. + return components[0] + elif len(normalised_components) == 1: + # No sense to construct a whole CompositeMorphism. + return normalised_components[0] + + return Basic.__new__(cls, normalised_components) + + @property + def components(self): + """ + Returns the components of this composite morphism. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> (g * f).components + (NamedMorphism(Object("A"), Object("B"), "f"), + NamedMorphism(Object("B"), Object("C"), "g")) + + """ + return self.args[0] + + @property + def domain(self): + """ + Returns the domain of this composite morphism. + + The domain of the composite morphism is the domain of its + first component. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> (g * f).domain + Object("A") + + """ + return self.components[0].domain + + @property + def codomain(self): + """ + Returns the codomain of this composite morphism. + + The codomain of the composite morphism is the codomain of its + last component. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> (g * f).codomain + Object("C") + + """ + return self.components[-1].codomain + + def flatten(self, new_name): + """ + Forgets the composite structure of this morphism. + + Explanation + =========== + + If ``new_name`` is not empty, returns a :class:`NamedMorphism` + with the supplied name, otherwise returns a :class:`Morphism`. + In both cases the domain of the new morphism is the domain of + this composite morphism and the codomain of the new morphism + is the codomain of this composite morphism. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> (g * f).flatten("h") + NamedMorphism(Object("A"), Object("C"), "h") + + """ + return NamedMorphism(self.domain, self.codomain, new_name) + + +class Category(Basic): + r""" + An (abstract) category. + + Explanation + =========== + + A category [JoyOfCats] is a quadruple `\mbox{K} = (O, \hom, id, + \circ)` consisting of + + * a (set-theoretical) class `O`, whose members are called + `K`-objects, + + * for each pair `(A, B)` of `K`-objects, a set `\hom(A, B)` whose + members are called `K`-morphisms from `A` to `B`, + + * for a each `K`-object `A`, a morphism `id:A\rightarrow A`, + called the `K`-identity of `A`, + + * a composition law `\circ` associating with every `K`-morphisms + `f:A\rightarrow B` and `g:B\rightarrow C` a `K`-morphism `g\circ + f:A\rightarrow C`, called the composite of `f` and `g`. + + Composition is associative, `K`-identities are identities with + respect to composition, and the sets `\hom(A, B)` are pairwise + disjoint. + + This class knows nothing about its objects and morphisms. + Concrete cases of (abstract) categories should be implemented as + classes derived from this one. + + Certain instances of :class:`Diagram` can be asserted to be + commutative in a :class:`Category` by supplying the argument + ``commutative_diagrams`` in the constructor. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, Diagram, Category + >>> from sympy import FiniteSet + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g]) + >>> K = Category("K", commutative_diagrams=[d]) + >>> K.commutative_diagrams == FiniteSet(d) + True + + See Also + ======== + + Diagram + """ + def __new__(cls, name, objects=EmptySet, commutative_diagrams=EmptySet): + if not name: + raise ValueError("A Category cannot have an empty name.") + + if not isinstance(name, Str): + name = Str(name) + + if not isinstance(objects, Class): + objects = Class(objects) + + new_category = Basic.__new__(cls, name, objects, + FiniteSet(*commutative_diagrams)) + return new_category + + @property + def name(self): + """ + Returns the name of this category. + + Examples + ======== + + >>> from sympy.categories import Category + >>> K = Category("K") + >>> K.name + 'K' + + """ + return self.args[0].name + + @property + def objects(self): + """ + Returns the class of objects of this category. + + Examples + ======== + + >>> from sympy.categories import Object, Category + >>> from sympy import FiniteSet + >>> A = Object("A") + >>> B = Object("B") + >>> K = Category("K", FiniteSet(A, B)) + >>> K.objects + Class({Object("A"), Object("B")}) + + """ + return self.args[1] + + @property + def commutative_diagrams(self): + """ + Returns the :class:`~.FiniteSet` of diagrams which are known to + be commutative in this category. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, Diagram, Category + >>> from sympy import FiniteSet + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g]) + >>> K = Category("K", commutative_diagrams=[d]) + >>> K.commutative_diagrams == FiniteSet(d) + True + + """ + return self.args[2] + + def hom(self, A, B): + raise NotImplementedError( + "hom-sets are not implemented in Category.") + + def all_morphisms(self): + raise NotImplementedError( + "Obtaining the class of morphisms is not implemented in Category.") + + +class Diagram(Basic): + r""" + Represents a diagram in a certain category. + + Explanation + =========== + + Informally, a diagram is a collection of objects of a category and + certain morphisms between them. A diagram is still a monoid with + respect to morphism composition; i.e., identity morphisms, as well + as all composites of morphisms included in the diagram belong to + the diagram. For a more formal approach to this notion see + [Pare1970]. + + The components of composite morphisms are also added to the + diagram. No properties are assigned to such morphisms by default. + + A commutative diagram is often accompanied by a statement of the + following kind: "if such morphisms with such properties exist, + then such morphisms which such properties exist and the diagram is + commutative". To represent this, an instance of :class:`Diagram` + includes a collection of morphisms which are the premises and + another collection of conclusions. ``premises`` and + ``conclusions`` associate morphisms belonging to the corresponding + categories with the :class:`~.FiniteSet`'s of their properties. + + The set of properties of a composite morphism is the intersection + of the sets of properties of its components. The domain and + codomain of a conclusion morphism should be among the domains and + codomains of the morphisms listed as the premises of a diagram. + + No checks are carried out of whether the supplied object and + morphisms do belong to one and the same category. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, Diagram + >>> from sympy import pprint, default_sort_key + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g]) + >>> premises_keys = sorted(d.premises.keys(), key=default_sort_key) + >>> pprint(premises_keys, use_unicode=False) + [g*f:A-->C, id:A-->A, id:B-->B, id:C-->C, f:A-->B, g:B-->C] + >>> pprint(d.premises, use_unicode=False) + {g*f:A-->C: EmptySet, id:A-->A: EmptySet, id:B-->B: EmptySet, + id:C-->C: EmptySet, f:A-->B: EmptySet, g:B-->C: EmptySet} + >>> d = Diagram([f, g], {g * f: "unique"}) + >>> pprint(d.conclusions,use_unicode=False) + {g*f:A-->C: {unique}} + + References + ========== + + [Pare1970] B. Pareigis: Categories and functors. Academic Press, 1970. + + """ + @staticmethod + def _set_dict_union(dictionary, key, value): + """ + If ``key`` is in ``dictionary``, set the new value of ``key`` + to be the union between the old value and ``value``. + Otherwise, set the value of ``key`` to ``value. + + Returns ``True`` if the key already was in the dictionary and + ``False`` otherwise. + """ + if key in dictionary: + dictionary[key] = dictionary[key] | value + return True + else: + dictionary[key] = value + return False + + @staticmethod + def _add_morphism_closure(morphisms, morphism, props, add_identities=True, + recurse_composites=True): + """ + Adds a morphism and its attributes to the supplied dictionary + ``morphisms``. If ``add_identities`` is True, also adds the + identity morphisms for the domain and the codomain of + ``morphism``. + """ + if not Diagram._set_dict_union(morphisms, morphism, props): + # We have just added a new morphism. + + if isinstance(morphism, IdentityMorphism): + if props: + # Properties for identity morphisms don't really + # make sense, because very much is known about + # identity morphisms already, so much that they + # are trivial. Having properties for identity + # morphisms would only be confusing. + raise ValueError( + "Instances of IdentityMorphism cannot have properties.") + return + + if add_identities: + empty = EmptySet + + id_dom = IdentityMorphism(morphism.domain) + id_cod = IdentityMorphism(morphism.codomain) + + Diagram._set_dict_union(morphisms, id_dom, empty) + Diagram._set_dict_union(morphisms, id_cod, empty) + + for existing_morphism, existing_props in list(morphisms.items()): + new_props = existing_props & props + if morphism.domain == existing_morphism.codomain: + left = morphism * existing_morphism + Diagram._set_dict_union(morphisms, left, new_props) + if morphism.codomain == existing_morphism.domain: + right = existing_morphism * morphism + Diagram._set_dict_union(morphisms, right, new_props) + + if isinstance(morphism, CompositeMorphism) and recurse_composites: + # This is a composite morphism, add its components as + # well. + empty = EmptySet + for component in morphism.components: + Diagram._add_morphism_closure(morphisms, component, empty, + add_identities) + + def __new__(cls, *args): + """ + Construct a new instance of Diagram. + + Explanation + =========== + + If no arguments are supplied, an empty diagram is created. + + If at least an argument is supplied, ``args[0]`` is + interpreted as the premises of the diagram. If ``args[0]`` is + a list, it is interpreted as a list of :class:`Morphism`'s, in + which each :class:`Morphism` has an empty set of properties. + If ``args[0]`` is a Python dictionary or a :class:`Dict`, it + is interpreted as a dictionary associating to some + :class:`Morphism`'s some properties. + + If at least two arguments are supplied ``args[1]`` is + interpreted as the conclusions of the diagram. The type of + ``args[1]`` is interpreted in exactly the same way as the type + of ``args[0]``. If only one argument is supplied, the diagram + has no conclusions. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> from sympy.categories import IdentityMorphism, Diagram + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g]) + >>> IdentityMorphism(A) in d.premises.keys() + True + >>> g * f in d.premises.keys() + True + >>> d = Diagram([f, g], {g * f: "unique"}) + >>> d.conclusions[g * f] + {unique} + + """ + premises = {} + conclusions = {} + + # Here we will keep track of the objects which appear in the + # premises. + objects = EmptySet + + if len(args) >= 1: + # We've got some premises in the arguments. + premises_arg = args[0] + + if isinstance(premises_arg, list): + # The user has supplied a list of morphisms, none of + # which have any attributes. + empty = EmptySet + + for morphism in premises_arg: + objects |= FiniteSet(morphism.domain, morphism.codomain) + Diagram._add_morphism_closure(premises, morphism, empty) + elif isinstance(premises_arg, (dict, Dict)): + # The user has supplied a dictionary of morphisms and + # their properties. + for morphism, props in premises_arg.items(): + objects |= FiniteSet(morphism.domain, morphism.codomain) + Diagram._add_morphism_closure( + premises, morphism, FiniteSet(*props) if iterable(props) else FiniteSet(props)) + + if len(args) >= 2: + # We also have some conclusions. + conclusions_arg = args[1] + + if isinstance(conclusions_arg, list): + # The user has supplied a list of morphisms, none of + # which have any attributes. + empty = EmptySet + + for morphism in conclusions_arg: + # Check that no new objects appear in conclusions. + if ((sympify(objects.contains(morphism.domain)) is S.true) and + (sympify(objects.contains(morphism.codomain)) is S.true)): + # No need to add identities and recurse + # composites this time. + Diagram._add_morphism_closure( + conclusions, morphism, empty, add_identities=False, + recurse_composites=False) + elif isinstance(conclusions_arg, (dict, Dict)): + # The user has supplied a dictionary of morphisms and + # their properties. + for morphism, props in conclusions_arg.items(): + # Check that no new objects appear in conclusions. + if (morphism.domain in objects) and \ + (morphism.codomain in objects): + # No need to add identities and recurse + # composites this time. + Diagram._add_morphism_closure( + conclusions, morphism, FiniteSet(*props) if iterable(props) else FiniteSet(props), + add_identities=False, recurse_composites=False) + + return Basic.__new__(cls, Dict(premises), Dict(conclusions), objects) + + @property + def premises(self): + """ + Returns the premises of this diagram. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> from sympy.categories import IdentityMorphism, Diagram + >>> from sympy import pretty + >>> A = Object("A") + >>> B = Object("B") + >>> f = NamedMorphism(A, B, "f") + >>> id_A = IdentityMorphism(A) + >>> id_B = IdentityMorphism(B) + >>> d = Diagram([f]) + >>> print(pretty(d.premises, use_unicode=False)) + {id:A-->A: EmptySet, id:B-->B: EmptySet, f:A-->B: EmptySet} + + """ + return self.args[0] + + @property + def conclusions(self): + """ + Returns the conclusions of this diagram. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> from sympy.categories import IdentityMorphism, Diagram + >>> from sympy import FiniteSet + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g]) + >>> IdentityMorphism(A) in d.premises.keys() + True + >>> g * f in d.premises.keys() + True + >>> d = Diagram([f, g], {g * f: "unique"}) + >>> d.conclusions[g * f] == FiniteSet("unique") + True + + """ + return self.args[1] + + @property + def objects(self): + """ + Returns the :class:`~.FiniteSet` of objects that appear in this + diagram. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, Diagram + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g]) + >>> d.objects + {Object("A"), Object("B"), Object("C")} + + """ + return self.args[2] + + def hom(self, A, B): + """ + Returns a 2-tuple of sets of morphisms between objects ``A`` and + ``B``: one set of morphisms listed as premises, and the other set + of morphisms listed as conclusions. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, Diagram + >>> from sympy import pretty + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g], {g * f: "unique"}) + >>> print(pretty(d.hom(A, C), use_unicode=False)) + ({g*f:A-->C}, {g*f:A-->C}) + + See Also + ======== + Object, Morphism + """ + premises = EmptySet + conclusions = EmptySet + + for morphism in self.premises.keys(): + if (morphism.domain == A) and (morphism.codomain == B): + premises |= FiniteSet(morphism) + for morphism in self.conclusions.keys(): + if (morphism.domain == A) and (morphism.codomain == B): + conclusions |= FiniteSet(morphism) + + return (premises, conclusions) + + def is_subdiagram(self, diagram): + """ + Checks whether ``diagram`` is a subdiagram of ``self``. + Diagram `D'` is a subdiagram of `D` if all premises + (conclusions) of `D'` are contained in the premises + (conclusions) of `D`. The morphisms contained + both in `D'` and `D` should have the same properties for `D'` + to be a subdiagram of `D`. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, Diagram + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g], {g * f: "unique"}) + >>> d1 = Diagram([f]) + >>> d.is_subdiagram(d1) + True + >>> d1.is_subdiagram(d) + False + """ + premises = all((m in self.premises) and + (diagram.premises[m] == self.premises[m]) + for m in diagram.premises) + if not premises: + return False + + conclusions = all((m in self.conclusions) and + (diagram.conclusions[m] == self.conclusions[m]) + for m in diagram.conclusions) + + # Premises is surely ``True`` here. + return conclusions + + def subdiagram_from_objects(self, objects): + """ + If ``objects`` is a subset of the objects of ``self``, returns + a diagram which has as premises all those premises of ``self`` + which have a domains and codomains in ``objects``, likewise + for conclusions. Properties are preserved. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, Diagram + >>> from sympy import FiniteSet + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g], {f: "unique", g*f: "veryunique"}) + >>> d1 = d.subdiagram_from_objects(FiniteSet(A, B)) + >>> d1 == Diagram([f], {f: "unique"}) + True + """ + if not objects.is_subset(self.objects): + raise ValueError( + "Supplied objects should all belong to the diagram.") + + new_premises = {} + for morphism, props in self.premises.items(): + if ((sympify(objects.contains(morphism.domain)) is S.true) and + (sympify(objects.contains(morphism.codomain)) is S.true)): + new_premises[morphism] = props + + new_conclusions = {} + for morphism, props in self.conclusions.items(): + if ((sympify(objects.contains(morphism.domain)) is S.true) and + (sympify(objects.contains(morphism.codomain)) is S.true)): + new_conclusions[morphism] = props + + return Diagram(new_premises, new_conclusions) diff --git a/venv/lib/python3.11/site-packages/sympy/categories/diagram_drawing.py b/venv/lib/python3.11/site-packages/sympy/categories/diagram_drawing.py new file mode 100644 index 0000000000000000000000000000000000000000..2a9b507cd86cf0e633b5abf7a0c9a353740af334 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/categories/diagram_drawing.py @@ -0,0 +1,2580 @@ +r""" +This module contains the functionality to arrange the nodes of a +diagram on an abstract grid, and then to produce a graphical +representation of the grid. + +The currently supported back-ends are Xy-pic [Xypic]. + +Layout Algorithm +================ + +This section provides an overview of the algorithms implemented in +:class:`DiagramGrid` to lay out diagrams. + +The first step of the algorithm is the removal composite and identity +morphisms which do not have properties in the supplied diagram. The +premises and conclusions of the diagram are then merged. + +The generic layout algorithm begins with the construction of the +"skeleton" of the diagram. The skeleton is an undirected graph which +has the objects of the diagram as vertices and has an (undirected) +edge between each pair of objects between which there exist morphisms. +The direction of the morphisms does not matter at this stage. The +skeleton also includes an edge between each pair of vertices `A` and +`C` such that there exists an object `B` which is connected via +a morphism to `A`, and via a morphism to `C`. + +The skeleton constructed in this way has the property that every +object is a vertex of a triangle formed by three edges of the +skeleton. This property lies at the base of the generic layout +algorithm. + +After the skeleton has been constructed, the algorithm lists all +triangles which can be formed. Note that some triangles will not have +all edges corresponding to morphisms which will actually be drawn. +Triangles which have only one edge or less which will actually be +drawn are immediately discarded. + +The list of triangles is sorted according to the number of edges which +correspond to morphisms, then the triangle with the least number of such +edges is selected. One of such edges is picked and the corresponding +objects are placed horizontally, on a grid. This edge is recorded to +be in the fringe. The algorithm then finds a "welding" of a triangle +to the fringe. A welding is an edge in the fringe where a triangle +could be attached. If the algorithm succeeds in finding such a +welding, it adds to the grid that vertex of the triangle which was not +yet included in any edge in the fringe and records the two new edges in +the fringe. This process continues iteratively until all objects of +the diagram has been placed or until no more weldings can be found. + +An edge is only removed from the fringe when a welding to this edge +has been found, and there is no room around this edge to place +another vertex. + +When no more weldings can be found, but there are still triangles +left, the algorithm searches for a possibility of attaching one of the +remaining triangles to the existing structure by a vertex. If such a +possibility is found, the corresponding edge of the found triangle is +placed in the found space and the iterative process of welding +triangles restarts. + +When logical groups are supplied, each of these groups is laid out +independently. Then a diagram is constructed in which groups are +objects and any two logical groups between which there exist morphisms +are connected via a morphism. This diagram is laid out. Finally, +the grid which includes all objects of the initial diagram is +constructed by replacing the cells which contain logical groups with +the corresponding laid out grids, and by correspondingly expanding the +rows and columns. + +The sequential layout algorithm begins by constructing the +underlying undirected graph defined by the morphisms obtained after +simplifying premises and conclusions and merging them (see above). +The vertex with the minimal degree is then picked up and depth-first +search is started from it. All objects which are located at distance +`n` from the root in the depth-first search tree, are positioned in +the `n`-th column of the resulting grid. The sequential layout will +therefore attempt to lay the objects out along a line. + +References +========== + +.. [Xypic] https://xy-pic.sourceforge.net/ + +""" +from sympy.categories import (CompositeMorphism, IdentityMorphism, + NamedMorphism, Diagram) +from sympy.core import Dict, Symbol, default_sort_key +from sympy.printing.latex import latex +from sympy.sets import FiniteSet +from sympy.utilities.iterables import iterable +from sympy.utilities.decorator import doctest_depends_on + +from itertools import chain + + +__doctest_requires__ = {('preview_diagram',): 'pyglet'} + + +class _GrowableGrid: + """ + Holds a growable grid of objects. + + Explanation + =========== + + It is possible to append or prepend a row or a column to the grid + using the corresponding methods. Prepending rows or columns has + the effect of changing the coordinates of the already existing + elements. + + This class currently represents a naive implementation of the + functionality with little attempt at optimisation. + """ + def __init__(self, width, height): + self._width = width + self._height = height + + self._array = [[None for j in range(width)] for i in range(height)] + + @property + def width(self): + return self._width + + @property + def height(self): + return self._height + + def __getitem__(self, i_j): + """ + Returns the element located at in the i-th line and j-th + column. + """ + i, j = i_j + return self._array[i][j] + + def __setitem__(self, i_j, newvalue): + """ + Sets the element located at in the i-th line and j-th + column. + """ + i, j = i_j + self._array[i][j] = newvalue + + def append_row(self): + """ + Appends an empty row to the grid. + """ + self._height += 1 + self._array.append([None for j in range(self._width)]) + + def append_column(self): + """ + Appends an empty column to the grid. + """ + self._width += 1 + for i in range(self._height): + self._array[i].append(None) + + def prepend_row(self): + """ + Prepends the grid with an empty row. + """ + self._height += 1 + self._array.insert(0, [None for j in range(self._width)]) + + def prepend_column(self): + """ + Prepends the grid with an empty column. + """ + self._width += 1 + for i in range(self._height): + self._array[i].insert(0, None) + + +class DiagramGrid: + r""" + Constructs and holds the fitting of the diagram into a grid. + + Explanation + =========== + + The mission of this class is to analyse the structure of the + supplied diagram and to place its objects on a grid such that, + when the objects and the morphisms are actually drawn, the diagram + would be "readable", in the sense that there will not be many + intersections of moprhisms. This class does not perform any + actual drawing. It does strive nevertheless to offer sufficient + metadata to draw a diagram. + + Consider the following simple diagram. + + >>> from sympy.categories import Object, NamedMorphism + >>> from sympy.categories import Diagram, DiagramGrid + >>> from sympy import pprint + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> diagram = Diagram([f, g]) + + The simplest way to have a diagram laid out is the following: + + >>> grid = DiagramGrid(diagram) + >>> (grid.width, grid.height) + (2, 2) + >>> pprint(grid) + A B + + C + + Sometimes one sees the diagram as consisting of logical groups. + One can advise ``DiagramGrid`` as to such groups by employing the + ``groups`` keyword argument. + + Consider the following diagram: + + >>> D = Object("D") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> h = NamedMorphism(D, A, "h") + >>> k = NamedMorphism(D, B, "k") + >>> diagram = Diagram([f, g, h, k]) + + Lay it out with generic layout: + + >>> grid = DiagramGrid(diagram) + >>> pprint(grid) + A B D + + C + + Now, we can group the objects `A` and `D` to have them near one + another: + + >>> grid = DiagramGrid(diagram, groups=[[A, D], B, C]) + >>> pprint(grid) + B C + + A D + + Note how the positioning of the other objects changes. + + Further indications can be supplied to the constructor of + :class:`DiagramGrid` using keyword arguments. The currently + supported hints are explained in the following paragraphs. + + :class:`DiagramGrid` does not automatically guess which layout + would suit the supplied diagram better. Consider, for example, + the following linear diagram: + + >>> E = Object("E") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> h = NamedMorphism(C, D, "h") + >>> i = NamedMorphism(D, E, "i") + >>> diagram = Diagram([f, g, h, i]) + + When laid out with the generic layout, it does not get to look + linear: + + >>> grid = DiagramGrid(diagram) + >>> pprint(grid) + A B + + C D + + E + + To get it laid out in a line, use ``layout="sequential"``: + + >>> grid = DiagramGrid(diagram, layout="sequential") + >>> pprint(grid) + A B C D E + + One may sometimes need to transpose the resulting layout. While + this can always be done by hand, :class:`DiagramGrid` provides a + hint for that purpose: + + >>> grid = DiagramGrid(diagram, layout="sequential", transpose=True) + >>> pprint(grid) + A + + B + + C + + D + + E + + Separate hints can also be provided for each group. For an + example, refer to ``tests/test_drawing.py``, and see the different + ways in which the five lemma [FiveLemma] can be laid out. + + See Also + ======== + + Diagram + + References + ========== + + .. [FiveLemma] https://en.wikipedia.org/wiki/Five_lemma + """ + @staticmethod + def _simplify_morphisms(morphisms): + """ + Given a dictionary mapping morphisms to their properties, + returns a new dictionary in which there are no morphisms which + do not have properties, and which are compositions of other + morphisms included in the dictionary. Identities are dropped + as well. + """ + newmorphisms = {} + for morphism, props in morphisms.items(): + if isinstance(morphism, CompositeMorphism) and not props: + continue + elif isinstance(morphism, IdentityMorphism): + continue + else: + newmorphisms[morphism] = props + return newmorphisms + + @staticmethod + def _merge_premises_conclusions(premises, conclusions): + """ + Given two dictionaries of morphisms and their properties, + produces a single dictionary which includes elements from both + dictionaries. If a morphism has some properties in premises + and also in conclusions, the properties in conclusions take + priority. + """ + return dict(chain(premises.items(), conclusions.items())) + + @staticmethod + def _juxtapose_edges(edge1, edge2): + """ + If ``edge1`` and ``edge2`` have precisely one common endpoint, + returns an edge which would form a triangle with ``edge1`` and + ``edge2``. + + If ``edge1`` and ``edge2`` do not have a common endpoint, + returns ``None``. + + If ``edge1`` and ``edge`` are the same edge, returns ``None``. + """ + intersection = edge1 & edge2 + if len(intersection) != 1: + # The edges either have no common points or are equal. + return None + + # The edges have a common endpoint. Extract the different + # endpoints and set up the new edge. + return (edge1 - intersection) | (edge2 - intersection) + + @staticmethod + def _add_edge_append(dictionary, edge, elem): + """ + If ``edge`` is not in ``dictionary``, adds ``edge`` to the + dictionary and sets its value to ``[elem]``. Otherwise + appends ``elem`` to the value of existing entry. + + Note that edges are undirected, thus `(A, B) = (B, A)`. + """ + if edge in dictionary: + dictionary[edge].append(elem) + else: + dictionary[edge] = [elem] + + @staticmethod + def _build_skeleton(morphisms): + """ + Creates a dictionary which maps edges to corresponding + morphisms. Thus for a morphism `f:A\rightarrow B`, the edge + `(A, B)` will be associated with `f`. This function also adds + to the list those edges which are formed by juxtaposition of + two edges already in the list. These new edges are not + associated with any morphism and are only added to assure that + the diagram can be decomposed into triangles. + """ + edges = {} + # Create edges for morphisms. + for morphism in morphisms: + DiagramGrid._add_edge_append( + edges, frozenset([morphism.domain, morphism.codomain]), morphism) + + # Create new edges by juxtaposing existing edges. + edges1 = dict(edges) + for w in edges1: + for v in edges1: + wv = DiagramGrid._juxtapose_edges(w, v) + if wv and wv not in edges: + edges[wv] = [] + + return edges + + @staticmethod + def _list_triangles(edges): + """ + Builds the set of triangles formed by the supplied edges. The + triangles are arbitrary and need not be commutative. A + triangle is a set that contains all three of its sides. + """ + triangles = set() + + for w in edges: + for v in edges: + wv = DiagramGrid._juxtapose_edges(w, v) + if wv and wv in edges: + triangles.add(frozenset([w, v, wv])) + + return triangles + + @staticmethod + def _drop_redundant_triangles(triangles, skeleton): + """ + Returns a list which contains only those triangles who have + morphisms associated with at least two edges. + """ + return [tri for tri in triangles + if len([e for e in tri if skeleton[e]]) >= 2] + + @staticmethod + def _morphism_length(morphism): + """ + Returns the length of a morphism. The length of a morphism is + the number of components it consists of. A non-composite + morphism is of length 1. + """ + if isinstance(morphism, CompositeMorphism): + return len(morphism.components) + else: + return 1 + + @staticmethod + def _compute_triangle_min_sizes(triangles, edges): + r""" + Returns a dictionary mapping triangles to their minimal sizes. + The minimal size of a triangle is the sum of maximal lengths + of morphisms associated to the sides of the triangle. The + length of a morphism is the number of components it consists + of. A non-composite morphism is of length 1. + + Sorting triangles by this metric attempts to address two + aspects of layout. For triangles with only simple morphisms + in the edge, this assures that triangles with all three edges + visible will get typeset after triangles with less visible + edges, which sometimes minimizes the necessity in diagonal + arrows. For triangles with composite morphisms in the edges, + this assures that objects connected with shorter morphisms + will be laid out first, resulting the visual proximity of + those objects which are connected by shorter morphisms. + """ + triangle_sizes = {} + for triangle in triangles: + size = 0 + for e in triangle: + morphisms = edges[e] + if morphisms: + size += max(DiagramGrid._morphism_length(m) + for m in morphisms) + triangle_sizes[triangle] = size + return triangle_sizes + + @staticmethod + def _triangle_objects(triangle): + """ + Given a triangle, returns the objects included in it. + """ + # A triangle is a frozenset of three two-element frozensets + # (the edges). This chains the three edges together and + # creates a frozenset from the iterator, thus producing a + # frozenset of objects of the triangle. + return frozenset(chain(*tuple(triangle))) + + @staticmethod + def _other_vertex(triangle, edge): + """ + Given a triangle and an edge of it, returns the vertex which + opposes the edge. + """ + # This gets the set of objects of the triangle and then + # subtracts the set of objects employed in ``edge`` to get the + # vertex opposite to ``edge``. + return list(DiagramGrid._triangle_objects(triangle) - set(edge))[0] + + @staticmethod + def _empty_point(pt, grid): + """ + Checks if the cell at coordinates ``pt`` is either empty or + out of the bounds of the grid. + """ + if (pt[0] < 0) or (pt[1] < 0) or \ + (pt[0] >= grid.height) or (pt[1] >= grid.width): + return True + return grid[pt] is None + + @staticmethod + def _put_object(coords, obj, grid, fringe): + """ + Places an object at the coordinate ``cords`` in ``grid``, + growing the grid and updating ``fringe``, if necessary. + Returns (0, 0) if no row or column has been prepended, (1, 0) + if a row was prepended, (0, 1) if a column was prepended and + (1, 1) if both a column and a row were prepended. + """ + (i, j) = coords + offset = (0, 0) + if i == -1: + grid.prepend_row() + i = 0 + offset = (1, 0) + for k in range(len(fringe)): + ((i1, j1), (i2, j2)) = fringe[k] + fringe[k] = ((i1 + 1, j1), (i2 + 1, j2)) + elif i == grid.height: + grid.append_row() + + if j == -1: + j = 0 + offset = (offset[0], 1) + grid.prepend_column() + for k in range(len(fringe)): + ((i1, j1), (i2, j2)) = fringe[k] + fringe[k] = ((i1, j1 + 1), (i2, j2 + 1)) + elif j == grid.width: + grid.append_column() + + grid[i, j] = obj + return offset + + @staticmethod + def _choose_target_cell(pt1, pt2, edge, obj, skeleton, grid): + """ + Given two points, ``pt1`` and ``pt2``, and the welding edge + ``edge``, chooses one of the two points to place the opposing + vertex ``obj`` of the triangle. If neither of this points + fits, returns ``None``. + """ + pt1_empty = DiagramGrid._empty_point(pt1, grid) + pt2_empty = DiagramGrid._empty_point(pt2, grid) + + if pt1_empty and pt2_empty: + # Both cells are empty. Of these two, choose that cell + # which will assure that a visible edge of the triangle + # will be drawn perpendicularly to the current welding + # edge. + + A = grid[edge[0]] + + if skeleton.get(frozenset([A, obj])): + return pt1 + else: + return pt2 + if pt1_empty: + return pt1 + elif pt2_empty: + return pt2 + else: + return None + + @staticmethod + def _find_triangle_to_weld(triangles, fringe, grid): + """ + Finds, if possible, a triangle and an edge in the ``fringe`` to + which the triangle could be attached. Returns the tuple + containing the triangle and the index of the corresponding + edge in the ``fringe``. + + This function relies on the fact that objects are unique in + the diagram. + """ + for triangle in triangles: + for (a, b) in fringe: + if frozenset([grid[a], grid[b]]) in triangle: + return (triangle, (a, b)) + return None + + @staticmethod + def _weld_triangle(tri, welding_edge, fringe, grid, skeleton): + """ + If possible, welds the triangle ``tri`` to ``fringe`` and + returns ``False``. If this method encounters a degenerate + situation in the fringe and corrects it such that a restart of + the search is required, it returns ``True`` (which means that + a restart in finding triangle weldings is required). + + A degenerate situation is a situation when an edge listed in + the fringe does not belong to the visual boundary of the + diagram. + """ + a, b = welding_edge + target_cell = None + + obj = DiagramGrid._other_vertex(tri, (grid[a], grid[b])) + + # We now have a triangle and an edge where it can be welded to + # the fringe. Decide where to place the other vertex of the + # triangle and check for degenerate situations en route. + + if (abs(a[0] - b[0]) == 1) and (abs(a[1] - b[1]) == 1): + # A diagonal edge. + target_cell = (a[0], b[1]) + if grid[target_cell]: + # That cell is already occupied. + target_cell = (b[0], a[1]) + + if grid[target_cell]: + # Degenerate situation, this edge is not + # on the actual fringe. Correct the + # fringe and go on. + fringe.remove((a, b)) + return True + elif a[0] == b[0]: + # A horizontal edge. We first attempt to build the + # triangle in the downward direction. + + down_left = a[0] + 1, a[1] + down_right = a[0] + 1, b[1] + + target_cell = DiagramGrid._choose_target_cell( + down_left, down_right, (a, b), obj, skeleton, grid) + + if not target_cell: + # No room below this edge. Check above. + up_left = a[0] - 1, a[1] + up_right = a[0] - 1, b[1] + + target_cell = DiagramGrid._choose_target_cell( + up_left, up_right, (a, b), obj, skeleton, grid) + + if not target_cell: + # This edge is not in the fringe, remove it + # and restart. + fringe.remove((a, b)) + return True + elif a[1] == b[1]: + # A vertical edge. We will attempt to place the other + # vertex of the triangle to the right of this edge. + right_up = a[0], a[1] + 1 + right_down = b[0], a[1] + 1 + + target_cell = DiagramGrid._choose_target_cell( + right_up, right_down, (a, b), obj, skeleton, grid) + + if not target_cell: + # No room to the left. See what's to the right. + left_up = a[0], a[1] - 1 + left_down = b[0], a[1] - 1 + + target_cell = DiagramGrid._choose_target_cell( + left_up, left_down, (a, b), obj, skeleton, grid) + + if not target_cell: + # This edge is not in the fringe, remove it + # and restart. + fringe.remove((a, b)) + return True + + # We now know where to place the other vertex of the + # triangle. + offset = DiagramGrid._put_object(target_cell, obj, grid, fringe) + + # Take care of the displacement of coordinates if a row or + # a column was prepended. + target_cell = (target_cell[0] + offset[0], + target_cell[1] + offset[1]) + a = (a[0] + offset[0], a[1] + offset[1]) + b = (b[0] + offset[0], b[1] + offset[1]) + + fringe.extend([(a, target_cell), (b, target_cell)]) + + # No restart is required. + return False + + @staticmethod + def _triangle_key(tri, triangle_sizes): + """ + Returns a key for the supplied triangle. It should be the + same independently of the hash randomisation. + """ + objects = sorted( + DiagramGrid._triangle_objects(tri), key=default_sort_key) + return (triangle_sizes[tri], default_sort_key(objects)) + + @staticmethod + def _pick_root_edge(tri, skeleton): + """ + For a given triangle always picks the same root edge. The + root edge is the edge that will be placed first on the grid. + """ + candidates = [sorted(e, key=default_sort_key) + for e in tri if skeleton[e]] + sorted_candidates = sorted(candidates, key=default_sort_key) + # Don't forget to assure the proper ordering of the vertices + # in this edge. + return tuple(sorted(sorted_candidates[0], key=default_sort_key)) + + @staticmethod + def _drop_irrelevant_triangles(triangles, placed_objects): + """ + Returns only those triangles whose set of objects is not + completely included in ``placed_objects``. + """ + return [tri for tri in triangles if not placed_objects.issuperset( + DiagramGrid._triangle_objects(tri))] + + @staticmethod + def _grow_pseudopod(triangles, fringe, grid, skeleton, placed_objects): + """ + Starting from an object in the existing structure on the ``grid``, + adds an edge to which a triangle from ``triangles`` could be + welded. If this method has found a way to do so, it returns + the object it has just added. + + This method should be applied when ``_weld_triangle`` cannot + find weldings any more. + """ + for i in range(grid.height): + for j in range(grid.width): + obj = grid[i, j] + if not obj: + continue + + # Here we need to choose a triangle which has only + # ``obj`` in common with the existing structure. The + # situations when this is not possible should be + # handled elsewhere. + + def good_triangle(tri): + objs = DiagramGrid._triangle_objects(tri) + return obj in objs and \ + placed_objects & (objs - {obj}) == set() + + tris = [tri for tri in triangles if good_triangle(tri)] + if not tris: + # This object is not interesting. + continue + + # Pick the "simplest" of the triangles which could be + # attached. Remember that the list of triangles is + # sorted according to their "simplicity" (see + # _compute_triangle_min_sizes for the metric). + # + # Note that ``tris`` are sequentially built from + # ``triangles``, so we don't have to worry about hash + # randomisation. + tri = tris[0] + + # We have found a triangle which could be attached to + # the existing structure by a vertex. + + candidates = sorted([e for e in tri if skeleton[e]], + key=lambda e: FiniteSet(*e).sort_key()) + edges = [e for e in candidates if obj in e] + + # Note that a meaningful edge (i.e., and edge that is + # associated with a morphism) containing ``obj`` + # always exists. That's because all triangles are + # guaranteed to have at least two meaningful edges. + # See _drop_redundant_triangles. + + # Get the object at the other end of the edge. + edge = edges[0] + other_obj = tuple(edge - frozenset([obj]))[0] + + # Now check for free directions. When checking for + # free directions, prefer the horizontal and vertical + # directions. + neighbours = [(i - 1, j), (i, j + 1), (i + 1, j), (i, j - 1), + (i - 1, j - 1), (i - 1, j + 1), (i + 1, j - 1), (i + 1, j + 1)] + + for pt in neighbours: + if DiagramGrid._empty_point(pt, grid): + # We have a found a place to grow the + # pseudopod into. + offset = DiagramGrid._put_object( + pt, other_obj, grid, fringe) + + i += offset[0] + j += offset[1] + pt = (pt[0] + offset[0], pt[1] + offset[1]) + fringe.append(((i, j), pt)) + + return other_obj + + # This diagram is actually cooler that I can handle. Fail cowardly. + return None + + @staticmethod + def _handle_groups(diagram, groups, merged_morphisms, hints): + """ + Given the slightly preprocessed morphisms of the diagram, + produces a grid laid out according to ``groups``. + + If a group has hints, it is laid out with those hints only, + without any influence from ``hints``. Otherwise, it is laid + out with ``hints``. + """ + def lay_out_group(group, local_hints): + """ + If ``group`` is a set of objects, uses a ``DiagramGrid`` + to lay it out and returns the grid. Otherwise returns the + object (i.e., ``group``). If ``local_hints`` is not + empty, it is supplied to ``DiagramGrid`` as the dictionary + of hints. Otherwise, the ``hints`` argument of + ``_handle_groups`` is used. + """ + if isinstance(group, FiniteSet): + # Set up the corresponding object-to-group + # mappings. + for obj in group: + obj_groups[obj] = group + + # Lay out the current group. + if local_hints: + groups_grids[group] = DiagramGrid( + diagram.subdiagram_from_objects(group), **local_hints) + else: + groups_grids[group] = DiagramGrid( + diagram.subdiagram_from_objects(group), **hints) + else: + obj_groups[group] = group + + def group_to_finiteset(group): + """ + Converts ``group`` to a :class:``FiniteSet`` if it is an + iterable. + """ + if iterable(group): + return FiniteSet(*group) + else: + return group + + obj_groups = {} + groups_grids = {} + + # We would like to support various containers to represent + # groups. To achieve that, before laying each group out, it + # should be converted to a FiniteSet, because that is what the + # following code expects. + + if isinstance(groups, (dict, Dict)): + finiteset_groups = {} + for group, local_hints in groups.items(): + finiteset_group = group_to_finiteset(group) + finiteset_groups[finiteset_group] = local_hints + lay_out_group(group, local_hints) + groups = finiteset_groups + else: + finiteset_groups = [] + for group in groups: + finiteset_group = group_to_finiteset(group) + finiteset_groups.append(finiteset_group) + lay_out_group(finiteset_group, None) + groups = finiteset_groups + + new_morphisms = [] + for morphism in merged_morphisms: + dom = obj_groups[morphism.domain] + cod = obj_groups[morphism.codomain] + # Note that we are not really interested in morphisms + # which do not employ two different groups, because + # these do not influence the layout. + if dom != cod: + # These are essentially unnamed morphisms; they are + # not going to mess in the final layout. By giving + # them the same names, we avoid unnecessary + # duplicates. + new_morphisms.append(NamedMorphism(dom, cod, "dummy")) + + # Lay out the new diagram. Since these are dummy morphisms, + # properties and conclusions are irrelevant. + top_grid = DiagramGrid(Diagram(new_morphisms)) + + # We now have to substitute the groups with the corresponding + # grids, laid out at the beginning of this function. Compute + # the size of each row and column in the grid, so that all + # nested grids fit. + + def group_size(group): + """ + For the supplied group (or object, eventually), returns + the size of the cell that will hold this group (object). + """ + if group in groups_grids: + grid = groups_grids[group] + return (grid.height, grid.width) + else: + return (1, 1) + + row_heights = [max(group_size(top_grid[i, j])[0] + for j in range(top_grid.width)) + for i in range(top_grid.height)] + + column_widths = [max(group_size(top_grid[i, j])[1] + for i in range(top_grid.height)) + for j in range(top_grid.width)] + + grid = _GrowableGrid(sum(column_widths), sum(row_heights)) + + real_row = 0 + real_column = 0 + for logical_row in range(top_grid.height): + for logical_column in range(top_grid.width): + obj = top_grid[logical_row, logical_column] + + if obj in groups_grids: + # This is a group. Copy the corresponding grid in + # place. + local_grid = groups_grids[obj] + for i in range(local_grid.height): + for j in range(local_grid.width): + grid[real_row + i, + real_column + j] = local_grid[i, j] + else: + # This is an object. Just put it there. + grid[real_row, real_column] = obj + + real_column += column_widths[logical_column] + real_column = 0 + real_row += row_heights[logical_row] + + return grid + + @staticmethod + def _generic_layout(diagram, merged_morphisms): + """ + Produces the generic layout for the supplied diagram. + """ + all_objects = set(diagram.objects) + if len(all_objects) == 1: + # There only one object in the diagram, just put in on 1x1 + # grid. + grid = _GrowableGrid(1, 1) + grid[0, 0] = tuple(all_objects)[0] + return grid + + skeleton = DiagramGrid._build_skeleton(merged_morphisms) + + grid = _GrowableGrid(2, 1) + + if len(skeleton) == 1: + # This diagram contains only one morphism. Draw it + # horizontally. + objects = sorted(all_objects, key=default_sort_key) + grid[0, 0] = objects[0] + grid[0, 1] = objects[1] + + return grid + + triangles = DiagramGrid._list_triangles(skeleton) + triangles = DiagramGrid._drop_redundant_triangles(triangles, skeleton) + triangle_sizes = DiagramGrid._compute_triangle_min_sizes( + triangles, skeleton) + + triangles = sorted(triangles, key=lambda tri: + DiagramGrid._triangle_key(tri, triangle_sizes)) + + # Place the first edge on the grid. + root_edge = DiagramGrid._pick_root_edge(triangles[0], skeleton) + grid[0, 0], grid[0, 1] = root_edge + fringe = [((0, 0), (0, 1))] + + # Record which objects we now have on the grid. + placed_objects = set(root_edge) + + while placed_objects != all_objects: + welding = DiagramGrid._find_triangle_to_weld( + triangles, fringe, grid) + + if welding: + (triangle, welding_edge) = welding + + restart_required = DiagramGrid._weld_triangle( + triangle, welding_edge, fringe, grid, skeleton) + if restart_required: + continue + + placed_objects.update( + DiagramGrid._triangle_objects(triangle)) + else: + # No more weldings found. Try to attach triangles by + # vertices. + new_obj = DiagramGrid._grow_pseudopod( + triangles, fringe, grid, skeleton, placed_objects) + + if not new_obj: + # No more triangles can be attached, not even by + # the edge. We will set up a new diagram out of + # what has been left, laid it out independently, + # and then attach it to this one. + + remaining_objects = all_objects - placed_objects + + remaining_diagram = diagram.subdiagram_from_objects( + FiniteSet(*remaining_objects)) + remaining_grid = DiagramGrid(remaining_diagram) + + # Now, let's glue ``remaining_grid`` to ``grid``. + final_width = grid.width + remaining_grid.width + final_height = max(grid.height, remaining_grid.height) + final_grid = _GrowableGrid(final_width, final_height) + + for i in range(grid.width): + for j in range(grid.height): + final_grid[i, j] = grid[i, j] + + start_j = grid.width + for i in range(remaining_grid.height): + for j in range(remaining_grid.width): + final_grid[i, start_j + j] = remaining_grid[i, j] + + return final_grid + + placed_objects.add(new_obj) + + triangles = DiagramGrid._drop_irrelevant_triangles( + triangles, placed_objects) + + return grid + + @staticmethod + def _get_undirected_graph(objects, merged_morphisms): + """ + Given the objects and the relevant morphisms of a diagram, + returns the adjacency lists of the underlying undirected + graph. + """ + adjlists = {obj: [] for obj in objects} + + for morphism in merged_morphisms: + adjlists[morphism.domain].append(morphism.codomain) + adjlists[morphism.codomain].append(morphism.domain) + + # Assure that the objects in the adjacency list are always in + # the same order. + for obj in adjlists.keys(): + adjlists[obj].sort(key=default_sort_key) + + return adjlists + + @staticmethod + def _sequential_layout(diagram, merged_morphisms): + r""" + Lays out the diagram in "sequential" layout. This method + will attempt to produce a result as close to a line as + possible. For linear diagrams, the result will actually be a + line. + """ + objects = diagram.objects + sorted_objects = sorted(objects, key=default_sort_key) + + # Set up the adjacency lists of the underlying undirected + # graph of ``merged_morphisms``. + adjlists = DiagramGrid._get_undirected_graph(objects, merged_morphisms) + + root = min(sorted_objects, key=lambda x: len(adjlists[x])) + grid = _GrowableGrid(1, 1) + grid[0, 0] = root + + placed_objects = {root} + + def place_objects(pt, placed_objects): + """ + Does depth-first search in the underlying graph of the + diagram and places the objects en route. + """ + # We will start placing new objects from here. + new_pt = (pt[0], pt[1] + 1) + + for adjacent_obj in adjlists[grid[pt]]: + if adjacent_obj in placed_objects: + # This object has already been placed. + continue + + DiagramGrid._put_object(new_pt, adjacent_obj, grid, []) + placed_objects.add(adjacent_obj) + placed_objects.update(place_objects(new_pt, placed_objects)) + + new_pt = (new_pt[0] + 1, new_pt[1]) + + return placed_objects + + place_objects((0, 0), placed_objects) + + return grid + + @staticmethod + def _drop_inessential_morphisms(merged_morphisms): + r""" + Removes those morphisms which should appear in the diagram, + but which have no relevance to object layout. + + Currently this removes "loop" morphisms: the non-identity + morphisms with the same domains and codomains. + """ + morphisms = [m for m in merged_morphisms if m.domain != m.codomain] + return morphisms + + @staticmethod + def _get_connected_components(objects, merged_morphisms): + """ + Given a container of morphisms, returns a list of connected + components formed by these morphisms. A connected component + is represented by a diagram consisting of the corresponding + morphisms. + """ + component_index = {} + for o in objects: + component_index[o] = None + + # Get the underlying undirected graph of the diagram. + adjlist = DiagramGrid._get_undirected_graph(objects, merged_morphisms) + + def traverse_component(object, current_index): + """ + Does a depth-first search traversal of the component + containing ``object``. + """ + component_index[object] = current_index + for o in adjlist[object]: + if component_index[o] is None: + traverse_component(o, current_index) + + # Traverse all components. + current_index = 0 + for o in adjlist: + if component_index[o] is None: + traverse_component(o, current_index) + current_index += 1 + + # List the objects of the components. + component_objects = [[] for i in range(current_index)] + for o, idx in component_index.items(): + component_objects[idx].append(o) + + # Finally, list the morphisms belonging to each component. + # + # Note: If some objects are isolated, they will not get any + # morphisms at this stage, and since the layout algorithm + # relies, we are essentially going to lose this object. + # Therefore, check if there are isolated objects and, for each + # of them, provide the trivial identity morphism. It will get + # discarded later, but the object will be there. + + component_morphisms = [] + for component in component_objects: + current_morphisms = {} + for m in merged_morphisms: + if (m.domain in component) and (m.codomain in component): + current_morphisms[m] = merged_morphisms[m] + + if len(component) == 1: + # Let's add an identity morphism, for the sake of + # surely having morphisms in this component. + current_morphisms[IdentityMorphism(component[0])] = FiniteSet() + + component_morphisms.append(Diagram(current_morphisms)) + + return component_morphisms + + def __init__(self, diagram, groups=None, **hints): + premises = DiagramGrid._simplify_morphisms(diagram.premises) + conclusions = DiagramGrid._simplify_morphisms(diagram.conclusions) + all_merged_morphisms = DiagramGrid._merge_premises_conclusions( + premises, conclusions) + merged_morphisms = DiagramGrid._drop_inessential_morphisms( + all_merged_morphisms) + + # Store the merged morphisms for later use. + self._morphisms = all_merged_morphisms + + components = DiagramGrid._get_connected_components( + diagram.objects, all_merged_morphisms) + + if groups and (groups != diagram.objects): + # Lay out the diagram according to the groups. + self._grid = DiagramGrid._handle_groups( + diagram, groups, merged_morphisms, hints) + elif len(components) > 1: + # Note that we check for connectedness _before_ checking + # the layout hints because the layout strategies don't + # know how to deal with disconnected diagrams. + + # The diagram is disconnected. Lay out the components + # independently. + grids = [] + + # Sort the components to eventually get the grids arranged + # in a fixed, hash-independent order. + components = sorted(components, key=default_sort_key) + + for component in components: + grid = DiagramGrid(component, **hints) + grids.append(grid) + + # Throw the grids together, in a line. + total_width = sum(g.width for g in grids) + total_height = max(g.height for g in grids) + + grid = _GrowableGrid(total_width, total_height) + start_j = 0 + for g in grids: + for i in range(g.height): + for j in range(g.width): + grid[i, start_j + j] = g[i, j] + + start_j += g.width + + self._grid = grid + elif "layout" in hints: + if hints["layout"] == "sequential": + self._grid = DiagramGrid._sequential_layout( + diagram, merged_morphisms) + else: + self._grid = DiagramGrid._generic_layout(diagram, merged_morphisms) + + if hints.get("transpose"): + # Transpose the resulting grid. + grid = _GrowableGrid(self._grid.height, self._grid.width) + for i in range(self._grid.height): + for j in range(self._grid.width): + grid[j, i] = self._grid[i, j] + self._grid = grid + + @property + def width(self): + """ + Returns the number of columns in this diagram layout. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> from sympy.categories import Diagram, DiagramGrid + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> diagram = Diagram([f, g]) + >>> grid = DiagramGrid(diagram) + >>> grid.width + 2 + + """ + return self._grid.width + + @property + def height(self): + """ + Returns the number of rows in this diagram layout. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> from sympy.categories import Diagram, DiagramGrid + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> diagram = Diagram([f, g]) + >>> grid = DiagramGrid(diagram) + >>> grid.height + 2 + + """ + return self._grid.height + + def __getitem__(self, i_j): + """ + Returns the object placed in the row ``i`` and column ``j``. + The indices are 0-based. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> from sympy.categories import Diagram, DiagramGrid + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> diagram = Diagram([f, g]) + >>> grid = DiagramGrid(diagram) + >>> (grid[0, 0], grid[0, 1]) + (Object("A"), Object("B")) + >>> (grid[1, 0], grid[1, 1]) + (None, Object("C")) + + """ + i, j = i_j + return self._grid[i, j] + + @property + def morphisms(self): + """ + Returns those morphisms (and their properties) which are + sufficiently meaningful to be drawn. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> from sympy.categories import Diagram, DiagramGrid + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> diagram = Diagram([f, g]) + >>> grid = DiagramGrid(diagram) + >>> grid.morphisms + {NamedMorphism(Object("A"), Object("B"), "f"): EmptySet, + NamedMorphism(Object("B"), Object("C"), "g"): EmptySet} + + """ + return self._morphisms + + def __str__(self): + """ + Produces a string representation of this class. + + This method returns a string representation of the underlying + list of lists of objects. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> from sympy.categories import Diagram, DiagramGrid + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> diagram = Diagram([f, g]) + >>> grid = DiagramGrid(diagram) + >>> print(grid) + [[Object("A"), Object("B")], + [None, Object("C")]] + + """ + return repr(self._grid._array) + + +class ArrowStringDescription: + r""" + Stores the information necessary for producing an Xy-pic + description of an arrow. + + The principal goal of this class is to abstract away the string + representation of an arrow and to also provide the functionality + to produce the actual Xy-pic string. + + ``unit`` sets the unit which will be used to specify the amount of + curving and other distances. ``horizontal_direction`` should be a + string of ``"r"`` or ``"l"`` specifying the horizontal offset of the + target cell of the arrow relatively to the current one. + ``vertical_direction`` should specify the vertical offset using a + series of either ``"d"`` or ``"u"``. ``label_position`` should be + either ``"^"``, ``"_"``, or ``"|"`` to specify that the label should + be positioned above the arrow, below the arrow or just over the arrow, + in a break. Note that the notions "above" and "below" are relative + to arrow direction. ``label`` stores the morphism label. + + This works as follows (disregard the yet unexplained arguments): + + >>> from sympy.categories.diagram_drawing import ArrowStringDescription + >>> astr = ArrowStringDescription( + ... unit="mm", curving=None, curving_amount=None, + ... looping_start=None, looping_end=None, horizontal_direction="d", + ... vertical_direction="r", label_position="_", label="f") + >>> print(str(astr)) + \ar[dr]_{f} + + ``curving`` should be one of ``"^"``, ``"_"`` to specify in which + direction the arrow is going to curve. ``curving_amount`` is a number + describing how many ``unit``'s the morphism is going to curve: + + >>> astr = ArrowStringDescription( + ... unit="mm", curving="^", curving_amount=12, + ... looping_start=None, looping_end=None, horizontal_direction="d", + ... vertical_direction="r", label_position="_", label="f") + >>> print(str(astr)) + \ar@/^12mm/[dr]_{f} + + ``looping_start`` and ``looping_end`` are currently only used for + loop morphisms, those which have the same domain and codomain. + These two attributes should store a valid Xy-pic direction and + specify, correspondingly, the direction the arrow gets out into + and the direction the arrow gets back from: + + >>> astr = ArrowStringDescription( + ... unit="mm", curving=None, curving_amount=None, + ... looping_start="u", looping_end="l", horizontal_direction="", + ... vertical_direction="", label_position="_", label="f") + >>> print(str(astr)) + \ar@(u,l)[]_{f} + + ``label_displacement`` controls how far the arrow label is from + the ends of the arrow. For example, to position the arrow label + near the arrow head, use ">": + + >>> astr = ArrowStringDescription( + ... unit="mm", curving="^", curving_amount=12, + ... looping_start=None, looping_end=None, horizontal_direction="d", + ... vertical_direction="r", label_position="_", label="f") + >>> astr.label_displacement = ">" + >>> print(str(astr)) + \ar@/^12mm/[dr]_>{f} + + Finally, ``arrow_style`` is used to specify the arrow style. To + get a dashed arrow, for example, use "{-->}" as arrow style: + + >>> astr = ArrowStringDescription( + ... unit="mm", curving="^", curving_amount=12, + ... looping_start=None, looping_end=None, horizontal_direction="d", + ... vertical_direction="r", label_position="_", label="f") + >>> astr.arrow_style = "{-->}" + >>> print(str(astr)) + \ar@/^12mm/@{-->}[dr]_{f} + + Notes + ===== + + Instances of :class:`ArrowStringDescription` will be constructed + by :class:`XypicDiagramDrawer` and provided for further use in + formatters. The user is not expected to construct instances of + :class:`ArrowStringDescription` themselves. + + To be able to properly utilise this class, the reader is encouraged + to checkout the Xy-pic user guide, available at [Xypic]. + + See Also + ======== + + XypicDiagramDrawer + + References + ========== + + .. [Xypic] https://xy-pic.sourceforge.net/ + """ + def __init__(self, unit, curving, curving_amount, looping_start, + looping_end, horizontal_direction, vertical_direction, + label_position, label): + self.unit = unit + self.curving = curving + self.curving_amount = curving_amount + self.looping_start = looping_start + self.looping_end = looping_end + self.horizontal_direction = horizontal_direction + self.vertical_direction = vertical_direction + self.label_position = label_position + self.label = label + + self.label_displacement = "" + self.arrow_style = "" + + # This flag shows that the position of the label of this + # morphism was set while typesetting a curved morphism and + # should not be modified later. + self.forced_label_position = False + + def __str__(self): + if self.curving: + curving_str = "@/%s%d%s/" % (self.curving, self.curving_amount, + self.unit) + else: + curving_str = "" + + if self.looping_start and self.looping_end: + looping_str = "@(%s,%s)" % (self.looping_start, self.looping_end) + else: + looping_str = "" + + if self.arrow_style: + + style_str = "@" + self.arrow_style + else: + style_str = "" + + return "\\ar%s%s%s[%s%s]%s%s{%s}" % \ + (curving_str, looping_str, style_str, self.horizontal_direction, + self.vertical_direction, self.label_position, + self.label_displacement, self.label) + + +class XypicDiagramDrawer: + r""" + Given a :class:`~.Diagram` and the corresponding + :class:`DiagramGrid`, produces the Xy-pic representation of the + diagram. + + The most important method in this class is ``draw``. Consider the + following triangle diagram: + + >>> from sympy.categories import Object, NamedMorphism, Diagram + >>> from sympy.categories import DiagramGrid, XypicDiagramDrawer + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> diagram = Diagram([f, g], {g * f: "unique"}) + + To draw this diagram, its objects need to be laid out with a + :class:`DiagramGrid`:: + + >>> grid = DiagramGrid(diagram) + + Finally, the drawing: + + >>> drawer = XypicDiagramDrawer() + >>> print(drawer.draw(diagram, grid)) + \xymatrix{ + A \ar[d]_{g\circ f} \ar[r]^{f} & B \ar[ld]^{g} \\ + C & + } + + For further details see the docstring of this method. + + To control the appearance of the arrows, formatters are used. The + dictionary ``arrow_formatters`` maps morphisms to formatter + functions. A formatter is accepts an + :class:`ArrowStringDescription` and is allowed to modify any of + the arrow properties exposed thereby. For example, to have all + morphisms with the property ``unique`` appear as dashed arrows, + and to have their names prepended with `\exists !`, the following + should be done: + + >>> def formatter(astr): + ... astr.label = r"\exists !" + astr.label + ... astr.arrow_style = "{-->}" + >>> drawer.arrow_formatters["unique"] = formatter + >>> print(drawer.draw(diagram, grid)) + \xymatrix{ + A \ar@{-->}[d]_{\exists !g\circ f} \ar[r]^{f} & B \ar[ld]^{g} \\ + C & + } + + To modify the appearance of all arrows in the diagram, set + ``default_arrow_formatter``. For example, to place all morphism + labels a little bit farther from the arrow head so that they look + more centred, do as follows: + + >>> def default_formatter(astr): + ... astr.label_displacement = "(0.45)" + >>> drawer.default_arrow_formatter = default_formatter + >>> print(drawer.draw(diagram, grid)) + \xymatrix{ + A \ar@{-->}[d]_(0.45){\exists !g\circ f} \ar[r]^(0.45){f} & B \ar[ld]^(0.45){g} \\ + C & + } + + In some diagrams some morphisms are drawn as curved arrows. + Consider the following diagram: + + >>> D = Object("D") + >>> E = Object("E") + >>> h = NamedMorphism(D, A, "h") + >>> k = NamedMorphism(D, B, "k") + >>> diagram = Diagram([f, g, h, k]) + >>> grid = DiagramGrid(diagram) + >>> drawer = XypicDiagramDrawer() + >>> print(drawer.draw(diagram, grid)) + \xymatrix{ + A \ar[r]_{f} & B \ar[d]^{g} & D \ar[l]^{k} \ar@/_3mm/[ll]_{h} \\ + & C & + } + + To control how far the morphisms are curved by default, one can + use the ``unit`` and ``default_curving_amount`` attributes: + + >>> drawer.unit = "cm" + >>> drawer.default_curving_amount = 1 + >>> print(drawer.draw(diagram, grid)) + \xymatrix{ + A \ar[r]_{f} & B \ar[d]^{g} & D \ar[l]^{k} \ar@/_1cm/[ll]_{h} \\ + & C & + } + + In some diagrams, there are multiple curved morphisms between the + same two objects. To control by how much the curving changes + between two such successive morphisms, use + ``default_curving_step``: + + >>> drawer.default_curving_step = 1 + >>> h1 = NamedMorphism(A, D, "h1") + >>> diagram = Diagram([f, g, h, k, h1]) + >>> grid = DiagramGrid(diagram) + >>> print(drawer.draw(diagram, grid)) + \xymatrix{ + A \ar[r]_{f} \ar@/^1cm/[rr]^{h_{1}} & B \ar[d]^{g} & D \ar[l]^{k} \ar@/_2cm/[ll]_{h} \\ + & C & + } + + The default value of ``default_curving_step`` is 4 units. + + See Also + ======== + + draw, ArrowStringDescription + """ + def __init__(self): + self.unit = "mm" + self.default_curving_amount = 3 + self.default_curving_step = 4 + + # This dictionary maps properties to the corresponding arrow + # formatters. + self.arrow_formatters = {} + + # This is the default arrow formatter which will be applied to + # each arrow independently of its properties. + self.default_arrow_formatter = None + + @staticmethod + def _process_loop_morphism(i, j, grid, morphisms_str_info, object_coords): + """ + Produces the information required for constructing the string + representation of a loop morphism. This function is invoked + from ``_process_morphism``. + + See Also + ======== + + _process_morphism + """ + curving = "" + label_pos = "^" + looping_start = "" + looping_end = "" + + # This is a loop morphism. Count how many morphisms stick + # in each of the four quadrants. Note that straight + # vertical and horizontal morphisms count in two quadrants + # at the same time (i.e., a morphism going up counts both + # in the first and the second quadrants). + + # The usual numbering (counterclockwise) of quadrants + # applies. + quadrant = [0, 0, 0, 0] + + obj = grid[i, j] + + for m, m_str_info in morphisms_str_info.items(): + if (m.domain == obj) and (m.codomain == obj): + # That's another loop morphism. Check how it + # loops and mark the corresponding quadrants as + # busy. + (l_s, l_e) = (m_str_info.looping_start, m_str_info.looping_end) + + if (l_s, l_e) == ("r", "u"): + quadrant[0] += 1 + elif (l_s, l_e) == ("u", "l"): + quadrant[1] += 1 + elif (l_s, l_e) == ("l", "d"): + quadrant[2] += 1 + elif (l_s, l_e) == ("d", "r"): + quadrant[3] += 1 + + continue + if m.domain == obj: + (end_i, end_j) = object_coords[m.codomain] + goes_out = True + elif m.codomain == obj: + (end_i, end_j) = object_coords[m.domain] + goes_out = False + else: + continue + + d_i = end_i - i + d_j = end_j - j + m_curving = m_str_info.curving + + if (d_i != 0) and (d_j != 0): + # This is really a diagonal morphism. Detect the + # quadrant. + if (d_i > 0) and (d_j > 0): + quadrant[0] += 1 + elif (d_i > 0) and (d_j < 0): + quadrant[1] += 1 + elif (d_i < 0) and (d_j < 0): + quadrant[2] += 1 + elif (d_i < 0) and (d_j > 0): + quadrant[3] += 1 + elif d_i == 0: + # Knowing where the other end of the morphism is + # and which way it goes, we now have to decide + # which quadrant is now the upper one and which is + # the lower one. + if d_j > 0: + if goes_out: + upper_quadrant = 0 + lower_quadrant = 3 + else: + upper_quadrant = 3 + lower_quadrant = 0 + else: + if goes_out: + upper_quadrant = 2 + lower_quadrant = 1 + else: + upper_quadrant = 1 + lower_quadrant = 2 + + if m_curving: + if m_curving == "^": + quadrant[upper_quadrant] += 1 + elif m_curving == "_": + quadrant[lower_quadrant] += 1 + else: + # This morphism counts in both upper and lower + # quadrants. + quadrant[upper_quadrant] += 1 + quadrant[lower_quadrant] += 1 + elif d_j == 0: + # Knowing where the other end of the morphism is + # and which way it goes, we now have to decide + # which quadrant is now the left one and which is + # the right one. + if d_i < 0: + if goes_out: + left_quadrant = 1 + right_quadrant = 0 + else: + left_quadrant = 0 + right_quadrant = 1 + else: + if goes_out: + left_quadrant = 3 + right_quadrant = 2 + else: + left_quadrant = 2 + right_quadrant = 3 + + if m_curving: + if m_curving == "^": + quadrant[left_quadrant] += 1 + elif m_curving == "_": + quadrant[right_quadrant] += 1 + else: + # This morphism counts in both upper and lower + # quadrants. + quadrant[left_quadrant] += 1 + quadrant[right_quadrant] += 1 + + # Pick the freest quadrant to curve our morphism into. + freest_quadrant = 0 + for i in range(4): + if quadrant[i] < quadrant[freest_quadrant]: + freest_quadrant = i + + # Now set up proper looping. + (looping_start, looping_end) = [("r", "u"), ("u", "l"), ("l", "d"), + ("d", "r")][freest_quadrant] + + return (curving, label_pos, looping_start, looping_end) + + @staticmethod + def _process_horizontal_morphism(i, j, target_j, grid, morphisms_str_info, + object_coords): + """ + Produces the information required for constructing the string + representation of a horizontal morphism. This function is + invoked from ``_process_morphism``. + + See Also + ======== + + _process_morphism + """ + # The arrow is horizontal. Check if it goes from left to + # right (``backwards == False``) or from right to left + # (``backwards == True``). + backwards = False + start = j + end = target_j + if end < start: + (start, end) = (end, start) + backwards = True + + # Let's see which objects are there between ``start`` and + # ``end``, and then count how many morphisms stick out + # upwards, and how many stick out downwards. + # + # For example, consider the situation: + # + # B1 C1 + # | | + # A--B--C--D + # | + # B2 + # + # Between the objects `A` and `D` there are two objects: + # `B` and `C`. Further, there are two morphisms which + # stick out upward (the ones between `B1` and `B` and + # between `C` and `C1`) and one morphism which sticks out + # downward (the one between `B and `B2`). + # + # We need this information to decide how to curve the + # arrow between `A` and `D`. First of all, since there + # are two objects between `A` and `D``, we must curve the + # arrow. Then, we will have it curve downward, because + # there is more space (less morphisms stick out downward + # than upward). + up = [] + down = [] + straight_horizontal = [] + for k in range(start + 1, end): + obj = grid[i, k] + if not obj: + continue + + for m in morphisms_str_info: + if m.domain == obj: + (end_i, end_j) = object_coords[m.codomain] + elif m.codomain == obj: + (end_i, end_j) = object_coords[m.domain] + else: + continue + + if end_i > i: + down.append(m) + elif end_i < i: + up.append(m) + elif not morphisms_str_info[m].curving: + # This is a straight horizontal morphism, + # because it has no curving. + straight_horizontal.append(m) + + if len(up) < len(down): + # More morphisms stick out downward than upward, let's + # curve the morphism up. + if backwards: + curving = "_" + label_pos = "_" + else: + curving = "^" + label_pos = "^" + + # Assure that the straight horizontal morphisms have + # their labels on the lower side of the arrow. + for m in straight_horizontal: + (i1, j1) = object_coords[m.domain] + (i2, j2) = object_coords[m.codomain] + + m_str_info = morphisms_str_info[m] + if j1 < j2: + m_str_info.label_position = "_" + else: + m_str_info.label_position = "^" + + # Don't allow any further modifications of the + # position of this label. + m_str_info.forced_label_position = True + else: + # More morphisms stick out downward than upward, let's + # curve the morphism up. + if backwards: + curving = "^" + label_pos = "^" + else: + curving = "_" + label_pos = "_" + + # Assure that the straight horizontal morphisms have + # their labels on the upper side of the arrow. + for m in straight_horizontal: + (i1, j1) = object_coords[m.domain] + (i2, j2) = object_coords[m.codomain] + + m_str_info = morphisms_str_info[m] + if j1 < j2: + m_str_info.label_position = "^" + else: + m_str_info.label_position = "_" + + # Don't allow any further modifications of the + # position of this label. + m_str_info.forced_label_position = True + + return (curving, label_pos) + + @staticmethod + def _process_vertical_morphism(i, j, target_i, grid, morphisms_str_info, + object_coords): + """ + Produces the information required for constructing the string + representation of a vertical morphism. This function is + invoked from ``_process_morphism``. + + See Also + ======== + + _process_morphism + """ + # This arrow is vertical. Check if it goes from top to + # bottom (``backwards == False``) or from bottom to top + # (``backwards == True``). + backwards = False + start = i + end = target_i + if end < start: + (start, end) = (end, start) + backwards = True + + # Let's see which objects are there between ``start`` and + # ``end``, and then count how many morphisms stick out to + # the left, and how many stick out to the right. + # + # See the corresponding comment in the previous branch of + # this if-statement for more details. + left = [] + right = [] + straight_vertical = [] + for k in range(start + 1, end): + obj = grid[k, j] + if not obj: + continue + + for m in morphisms_str_info: + if m.domain == obj: + (end_i, end_j) = object_coords[m.codomain] + elif m.codomain == obj: + (end_i, end_j) = object_coords[m.domain] + else: + continue + + if end_j > j: + right.append(m) + elif end_j < j: + left.append(m) + elif not morphisms_str_info[m].curving: + # This is a straight vertical morphism, + # because it has no curving. + straight_vertical.append(m) + + if len(left) < len(right): + # More morphisms stick out to the left than to the + # right, let's curve the morphism to the right. + if backwards: + curving = "^" + label_pos = "^" + else: + curving = "_" + label_pos = "_" + + # Assure that the straight vertical morphisms have + # their labels on the left side of the arrow. + for m in straight_vertical: + (i1, j1) = object_coords[m.domain] + (i2, j2) = object_coords[m.codomain] + + m_str_info = morphisms_str_info[m] + if i1 < i2: + m_str_info.label_position = "^" + else: + m_str_info.label_position = "_" + + # Don't allow any further modifications of the + # position of this label. + m_str_info.forced_label_position = True + else: + # More morphisms stick out to the right than to the + # left, let's curve the morphism to the left. + if backwards: + curving = "_" + label_pos = "_" + else: + curving = "^" + label_pos = "^" + + # Assure that the straight vertical morphisms have + # their labels on the right side of the arrow. + for m in straight_vertical: + (i1, j1) = object_coords[m.domain] + (i2, j2) = object_coords[m.codomain] + + m_str_info = morphisms_str_info[m] + if i1 < i2: + m_str_info.label_position = "_" + else: + m_str_info.label_position = "^" + + # Don't allow any further modifications of the + # position of this label. + m_str_info.forced_label_position = True + + return (curving, label_pos) + + def _process_morphism(self, diagram, grid, morphism, object_coords, + morphisms, morphisms_str_info): + """ + Given the required information, produces the string + representation of ``morphism``. + """ + def repeat_string_cond(times, str_gt, str_lt): + """ + If ``times > 0``, repeats ``str_gt`` ``times`` times. + Otherwise, repeats ``str_lt`` ``-times`` times. + """ + if times > 0: + return str_gt * times + else: + return str_lt * (-times) + + def count_morphisms_undirected(A, B): + """ + Counts how many processed morphisms there are between the + two supplied objects. + """ + return len([m for m in morphisms_str_info + if {m.domain, m.codomain} == {A, B}]) + + def count_morphisms_filtered(dom, cod, curving): + """ + Counts the processed morphisms which go out of ``dom`` + into ``cod`` with curving ``curving``. + """ + return len([m for m, m_str_info in morphisms_str_info.items() + if (m.domain, m.codomain) == (dom, cod) and + (m_str_info.curving == curving)]) + + (i, j) = object_coords[morphism.domain] + (target_i, target_j) = object_coords[morphism.codomain] + + # We now need to determine the direction of + # the arrow. + delta_i = target_i - i + delta_j = target_j - j + vertical_direction = repeat_string_cond(delta_i, + "d", "u") + horizontal_direction = repeat_string_cond(delta_j, + "r", "l") + + curving = "" + label_pos = "^" + looping_start = "" + looping_end = "" + + if (delta_i == 0) and (delta_j == 0): + # This is a loop morphism. + (curving, label_pos, looping_start, + looping_end) = XypicDiagramDrawer._process_loop_morphism( + i, j, grid, morphisms_str_info, object_coords) + elif (delta_i == 0) and (abs(j - target_j) > 1): + # This is a horizontal morphism. + (curving, label_pos) = XypicDiagramDrawer._process_horizontal_morphism( + i, j, target_j, grid, morphisms_str_info, object_coords) + elif (delta_j == 0) and (abs(i - target_i) > 1): + # This is a vertical morphism. + (curving, label_pos) = XypicDiagramDrawer._process_vertical_morphism( + i, j, target_i, grid, morphisms_str_info, object_coords) + + count = count_morphisms_undirected(morphism.domain, morphism.codomain) + curving_amount = "" + if curving: + # This morphisms should be curved anyway. + curving_amount = self.default_curving_amount + count * \ + self.default_curving_step + elif count: + # There are no objects between the domain and codomain of + # the current morphism, but this is not there already are + # some morphisms with the same domain and codomain, so we + # have to curve this one. + curving = "^" + filtered_morphisms = count_morphisms_filtered( + morphism.domain, morphism.codomain, curving) + curving_amount = self.default_curving_amount + \ + filtered_morphisms * \ + self.default_curving_step + + # Let's now get the name of the morphism. + morphism_name = "" + if isinstance(morphism, IdentityMorphism): + morphism_name = "id_{%s}" + latex(grid[i, j]) + elif isinstance(morphism, CompositeMorphism): + component_names = [latex(Symbol(component.name)) for + component in morphism.components] + component_names.reverse() + morphism_name = "\\circ ".join(component_names) + elif isinstance(morphism, NamedMorphism): + morphism_name = latex(Symbol(morphism.name)) + + return ArrowStringDescription( + self.unit, curving, curving_amount, looping_start, + looping_end, horizontal_direction, vertical_direction, + label_pos, morphism_name) + + @staticmethod + def _check_free_space_horizontal(dom_i, dom_j, cod_j, grid): + """ + For a horizontal morphism, checks whether there is free space + (i.e., space not occupied by any objects) above the morphism + or below it. + """ + if dom_j < cod_j: + (start, end) = (dom_j, cod_j) + backwards = False + else: + (start, end) = (cod_j, dom_j) + backwards = True + + # Check for free space above. + if dom_i == 0: + free_up = True + else: + free_up = all(grid[dom_i - 1, j] for j in + range(start, end + 1)) + + # Check for free space below. + if dom_i == grid.height - 1: + free_down = True + else: + free_down = not any(grid[dom_i + 1, j] for j in + range(start, end + 1)) + + return (free_up, free_down, backwards) + + @staticmethod + def _check_free_space_vertical(dom_i, cod_i, dom_j, grid): + """ + For a vertical morphism, checks whether there is free space + (i.e., space not occupied by any objects) to the left of the + morphism or to the right of it. + """ + if dom_i < cod_i: + (start, end) = (dom_i, cod_i) + backwards = False + else: + (start, end) = (cod_i, dom_i) + backwards = True + + # Check if there's space to the left. + if dom_j == 0: + free_left = True + else: + free_left = not any(grid[i, dom_j - 1] for i in + range(start, end + 1)) + + if dom_j == grid.width - 1: + free_right = True + else: + free_right = not any(grid[i, dom_j + 1] for i in + range(start, end + 1)) + + return (free_left, free_right, backwards) + + @staticmethod + def _check_free_space_diagonal(dom_i, cod_i, dom_j, cod_j, grid): + """ + For a diagonal morphism, checks whether there is free space + (i.e., space not occupied by any objects) above the morphism + or below it. + """ + def abs_xrange(start, end): + if start < end: + return range(start, end + 1) + else: + return range(end, start + 1) + + if dom_i < cod_i and dom_j < cod_j: + # This morphism goes from top-left to + # bottom-right. + (start_i, start_j) = (dom_i, dom_j) + (end_i, end_j) = (cod_i, cod_j) + backwards = False + elif dom_i > cod_i and dom_j > cod_j: + # This morphism goes from bottom-right to + # top-left. + (start_i, start_j) = (cod_i, cod_j) + (end_i, end_j) = (dom_i, dom_j) + backwards = True + if dom_i < cod_i and dom_j > cod_j: + # This morphism goes from top-right to + # bottom-left. + (start_i, start_j) = (dom_i, dom_j) + (end_i, end_j) = (cod_i, cod_j) + backwards = True + elif dom_i > cod_i and dom_j < cod_j: + # This morphism goes from bottom-left to + # top-right. + (start_i, start_j) = (cod_i, cod_j) + (end_i, end_j) = (dom_i, dom_j) + backwards = False + + # This is an attempt at a fast and furious strategy to + # decide where there is free space on the two sides of + # a diagonal morphism. For a diagonal morphism + # starting at ``(start_i, start_j)`` and ending at + # ``(end_i, end_j)`` the rectangle defined by these + # two points is considered. The slope of the diagonal + # ``alpha`` is then computed. Then, for every cell + # ``(i, j)`` within the rectangle, the slope + # ``alpha1`` of the line through ``(start_i, + # start_j)`` and ``(i, j)`` is considered. If + # ``alpha1`` is between 0 and ``alpha``, the point + # ``(i, j)`` is above the diagonal, if ``alpha1`` is + # between ``alpha`` and infinity, the point is below + # the diagonal. Also note that, with some beforehand + # precautions, this trick works for both the main and + # the secondary diagonals of the rectangle. + + # I have considered the possibility to only follow the + # shorter diagonals immediately above and below the + # main (or secondary) diagonal. This, however, + # wouldn't have resulted in much performance gain or + # better detection of outer edges, because of + # relatively small sizes of diagram grids, while the + # code would have become harder to understand. + + alpha = float(end_i - start_i)/(end_j - start_j) + free_up = True + free_down = True + for i in abs_xrange(start_i, end_i): + if not free_up and not free_down: + break + + for j in abs_xrange(start_j, end_j): + if not free_up and not free_down: + break + + if (i, j) == (start_i, start_j): + continue + + if j == start_j: + alpha1 = "inf" + else: + alpha1 = float(i - start_i)/(j - start_j) + + if grid[i, j]: + if (alpha1 == "inf") or (abs(alpha1) > abs(alpha)): + free_down = False + elif abs(alpha1) < abs(alpha): + free_up = False + + return (free_up, free_down, backwards) + + def _push_labels_out(self, morphisms_str_info, grid, object_coords): + """ + For all straight morphisms which form the visual boundary of + the laid out diagram, puts their labels on their outer sides. + """ + def set_label_position(free1, free2, pos1, pos2, backwards, m_str_info): + """ + Given the information about room available to one side and + to the other side of a morphism (``free1`` and ``free2``), + sets the position of the morphism label in such a way that + it is on the freer side. This latter operations involves + choice between ``pos1`` and ``pos2``, taking ``backwards`` + in consideration. + + Thus this function will do nothing if either both ``free1 + == True`` and ``free2 == True`` or both ``free1 == False`` + and ``free2 == False``. In either case, choosing one side + over the other presents no advantage. + """ + if backwards: + (pos1, pos2) = (pos2, pos1) + + if free1 and not free2: + m_str_info.label_position = pos1 + elif free2 and not free1: + m_str_info.label_position = pos2 + + for m, m_str_info in morphisms_str_info.items(): + if m_str_info.curving or m_str_info.forced_label_position: + # This is either a curved morphism, and curved + # morphisms have other magic, or the position of this + # label has already been fixed. + continue + + if m.domain == m.codomain: + # This is a loop morphism, their labels, again have a + # different magic. + continue + + (dom_i, dom_j) = object_coords[m.domain] + (cod_i, cod_j) = object_coords[m.codomain] + + if dom_i == cod_i: + # Horizontal morphism. + (free_up, free_down, + backwards) = XypicDiagramDrawer._check_free_space_horizontal( + dom_i, dom_j, cod_j, grid) + + set_label_position(free_up, free_down, "^", "_", + backwards, m_str_info) + elif dom_j == cod_j: + # Vertical morphism. + (free_left, free_right, + backwards) = XypicDiagramDrawer._check_free_space_vertical( + dom_i, cod_i, dom_j, grid) + + set_label_position(free_left, free_right, "_", "^", + backwards, m_str_info) + else: + # A diagonal morphism. + (free_up, free_down, + backwards) = XypicDiagramDrawer._check_free_space_diagonal( + dom_i, cod_i, dom_j, cod_j, grid) + + set_label_position(free_up, free_down, "^", "_", + backwards, m_str_info) + + @staticmethod + def _morphism_sort_key(morphism, object_coords): + """ + Provides a morphism sorting key such that horizontal or + vertical morphisms between neighbouring objects come + first, then horizontal or vertical morphisms between more + far away objects, and finally, all other morphisms. + """ + (i, j) = object_coords[morphism.domain] + (target_i, target_j) = object_coords[morphism.codomain] + + if morphism.domain == morphism.codomain: + # Loop morphisms should get after diagonal morphisms + # so that the proper direction in which to curve the + # loop can be determined. + return (3, 0, default_sort_key(morphism)) + + if target_i == i: + return (1, abs(target_j - j), default_sort_key(morphism)) + + if target_j == j: + return (1, abs(target_i - i), default_sort_key(morphism)) + + # Diagonal morphism. + return (2, 0, default_sort_key(morphism)) + + @staticmethod + def _build_xypic_string(diagram, grid, morphisms, + morphisms_str_info, diagram_format): + """ + Given a collection of :class:`ArrowStringDescription` + describing the morphisms of a diagram and the object layout + information of a diagram, produces the final Xy-pic picture. + """ + # Build the mapping between objects and morphisms which have + # them as domains. + object_morphisms = {} + for obj in diagram.objects: + object_morphisms[obj] = [] + for morphism in morphisms: + object_morphisms[morphism.domain].append(morphism) + + result = "\\xymatrix%s{\n" % diagram_format + + for i in range(grid.height): + for j in range(grid.width): + obj = grid[i, j] + if obj: + result += latex(obj) + " " + + morphisms_to_draw = object_morphisms[obj] + for morphism in morphisms_to_draw: + result += str(morphisms_str_info[morphism]) + " " + + # Don't put the & after the last column. + if j < grid.width - 1: + result += "& " + + # Don't put the line break after the last row. + if i < grid.height - 1: + result += "\\\\" + result += "\n" + + result += "}\n" + + return result + + def draw(self, diagram, grid, masked=None, diagram_format=""): + r""" + Returns the Xy-pic representation of ``diagram`` laid out in + ``grid``. + + Consider the following simple triangle diagram. + + >>> from sympy.categories import Object, NamedMorphism, Diagram + >>> from sympy.categories import DiagramGrid, XypicDiagramDrawer + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> diagram = Diagram([f, g], {g * f: "unique"}) + + To draw this diagram, its objects need to be laid out with a + :class:`DiagramGrid`:: + + >>> grid = DiagramGrid(diagram) + + Finally, the drawing: + + >>> drawer = XypicDiagramDrawer() + >>> print(drawer.draw(diagram, grid)) + \xymatrix{ + A \ar[d]_{g\circ f} \ar[r]^{f} & B \ar[ld]^{g} \\ + C & + } + + The argument ``masked`` can be used to skip morphisms in the + presentation of the diagram: + + >>> print(drawer.draw(diagram, grid, masked=[g * f])) + \xymatrix{ + A \ar[r]^{f} & B \ar[ld]^{g} \\ + C & + } + + Finally, the ``diagram_format`` argument can be used to + specify the format string of the diagram. For example, to + increase the spacing by 1 cm, proceeding as follows: + + >>> print(drawer.draw(diagram, grid, diagram_format="@+1cm")) + \xymatrix@+1cm{ + A \ar[d]_{g\circ f} \ar[r]^{f} & B \ar[ld]^{g} \\ + C & + } + + """ + # This method works in several steps. It starts by removing + # the masked morphisms, if necessary, and then maps objects to + # their positions in the grid (coordinate tuples). Remember + # that objects are unique in ``Diagram`` and in the layout + # produced by ``DiagramGrid``, so every object is mapped to a + # single coordinate pair. + # + # The next step is the central step and is concerned with + # analysing the morphisms of the diagram and deciding how to + # draw them. For example, how to curve the arrows is decided + # at this step. The bulk of the analysis is implemented in + # ``_process_morphism``, to the result of which the + # appropriate formatters are applied. + # + # The result of the previous step is a list of + # ``ArrowStringDescription``. After the analysis and + # application of formatters, some extra logic tries to assure + # better positioning of morphism labels (for example, an + # attempt is made to avoid the situations when arrows cross + # labels). This functionality constitutes the next step and + # is implemented in ``_push_labels_out``. Note that label + # positions which have been set via a formatter are not + # affected in this step. + # + # Finally, at the closing step, the array of + # ``ArrowStringDescription`` and the layout information + # incorporated in ``DiagramGrid`` are combined to produce the + # resulting Xy-pic picture. This part of code lies in + # ``_build_xypic_string``. + + if not masked: + morphisms_props = grid.morphisms + else: + morphisms_props = {} + for m, props in grid.morphisms.items(): + if m in masked: + continue + morphisms_props[m] = props + + # Build the mapping between objects and their position in the + # grid. + object_coords = {} + for i in range(grid.height): + for j in range(grid.width): + if grid[i, j]: + object_coords[grid[i, j]] = (i, j) + + morphisms = sorted(morphisms_props, + key=lambda m: XypicDiagramDrawer._morphism_sort_key( + m, object_coords)) + + # Build the tuples defining the string representations of + # morphisms. + morphisms_str_info = {} + for morphism in morphisms: + string_description = self._process_morphism( + diagram, grid, morphism, object_coords, morphisms, + morphisms_str_info) + + if self.default_arrow_formatter: + self.default_arrow_formatter(string_description) + + for prop in morphisms_props[morphism]: + # prop is a Symbol. TODO: Find out why. + if prop.name in self.arrow_formatters: + formatter = self.arrow_formatters[prop.name] + formatter(string_description) + + morphisms_str_info[morphism] = string_description + + # Reposition the labels a bit. + self._push_labels_out(morphisms_str_info, grid, object_coords) + + return XypicDiagramDrawer._build_xypic_string( + diagram, grid, morphisms, morphisms_str_info, diagram_format) + + +def xypic_draw_diagram(diagram, masked=None, diagram_format="", + groups=None, **hints): + r""" + Provides a shortcut combining :class:`DiagramGrid` and + :class:`XypicDiagramDrawer`. Returns an Xy-pic presentation of + ``diagram``. The argument ``masked`` is a list of morphisms which + will be not be drawn. The argument ``diagram_format`` is the + format string inserted after "\xymatrix". ``groups`` should be a + set of logical groups. The ``hints`` will be passed directly to + the constructor of :class:`DiagramGrid`. + + For more information about the arguments, see the docstrings of + :class:`DiagramGrid` and ``XypicDiagramDrawer.draw``. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, Diagram + >>> from sympy.categories import xypic_draw_diagram + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> diagram = Diagram([f, g], {g * f: "unique"}) + >>> print(xypic_draw_diagram(diagram)) + \xymatrix{ + A \ar[d]_{g\circ f} \ar[r]^{f} & B \ar[ld]^{g} \\ + C & + } + + See Also + ======== + + XypicDiagramDrawer, DiagramGrid + """ + grid = DiagramGrid(diagram, groups, **hints) + drawer = XypicDiagramDrawer() + return drawer.draw(diagram, grid, masked, diagram_format) + + +@doctest_depends_on(exe=('latex', 'dvipng'), modules=('pyglet',)) +def preview_diagram(diagram, masked=None, diagram_format="", groups=None, + output='png', viewer=None, euler=True, **hints): + """ + Combines the functionality of ``xypic_draw_diagram`` and + ``sympy.printing.preview``. The arguments ``masked``, + ``diagram_format``, ``groups``, and ``hints`` are passed to + ``xypic_draw_diagram``, while ``output``, ``viewer, and ``euler`` + are passed to ``preview``. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, Diagram + >>> from sympy.categories import preview_diagram + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g], {g * f: "unique"}) + >>> preview_diagram(d) + + See Also + ======== + + XypicDiagramDrawer + """ + from sympy.printing import preview + latex_output = xypic_draw_diagram(diagram, masked, diagram_format, + groups, **hints) + preview(latex_output, output, viewer, euler, ("xypic",)) diff --git a/venv/lib/python3.11/site-packages/sympy/categories/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/categories/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/categories/tests/test_baseclasses.py b/venv/lib/python3.11/site-packages/sympy/categories/tests/test_baseclasses.py new file mode 100644 index 0000000000000000000000000000000000000000..cfac32229768fb5903b23b11ffb236912c0b931e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/categories/tests/test_baseclasses.py @@ -0,0 +1,209 @@ +from sympy.categories import (Object, Morphism, IdentityMorphism, + NamedMorphism, CompositeMorphism, + Diagram, Category) +from sympy.categories.baseclasses import Class +from sympy.testing.pytest import raises +from sympy.core.containers import (Dict, Tuple) +from sympy.sets import EmptySet +from sympy.sets.sets import FiniteSet + + +def test_morphisms(): + A = Object("A") + B = Object("B") + C = Object("C") + D = Object("D") + + # Test the base morphism. + f = NamedMorphism(A, B, "f") + assert f.domain == A + assert f.codomain == B + assert f == NamedMorphism(A, B, "f") + + # Test identities. + id_A = IdentityMorphism(A) + id_B = IdentityMorphism(B) + assert id_A.domain == A + assert id_A.codomain == A + assert id_A == IdentityMorphism(A) + assert id_A != id_B + + # Test named morphisms. + g = NamedMorphism(B, C, "g") + assert g.name == "g" + assert g != f + assert g == NamedMorphism(B, C, "g") + assert g != NamedMorphism(B, C, "f") + + # Test composite morphisms. + assert f == CompositeMorphism(f) + + k = g.compose(f) + assert k.domain == A + assert k.codomain == C + assert k.components == Tuple(f, g) + assert g * f == k + assert CompositeMorphism(f, g) == k + + assert CompositeMorphism(g * f) == g * f + + # Test the associativity of composition. + h = NamedMorphism(C, D, "h") + + p = h * g + u = h * g * f + + assert h * k == u + assert p * f == u + assert CompositeMorphism(f, g, h) == u + + # Test flattening. + u2 = u.flatten("u") + assert isinstance(u2, NamedMorphism) + assert u2.name == "u" + assert u2.domain == A + assert u2.codomain == D + + # Test identities. + assert f * id_A == f + assert id_B * f == f + assert id_A * id_A == id_A + assert CompositeMorphism(id_A) == id_A + + # Test bad compositions. + raises(ValueError, lambda: f * g) + + raises(TypeError, lambda: f.compose(None)) + raises(TypeError, lambda: id_A.compose(None)) + raises(TypeError, lambda: f * None) + raises(TypeError, lambda: id_A * None) + + raises(TypeError, lambda: CompositeMorphism(f, None, 1)) + + raises(ValueError, lambda: NamedMorphism(A, B, "")) + raises(NotImplementedError, lambda: Morphism(A, B)) + + +def test_diagram(): + A = Object("A") + B = Object("B") + C = Object("C") + + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + id_A = IdentityMorphism(A) + id_B = IdentityMorphism(B) + + empty = EmptySet + + # Test the addition of identities. + d1 = Diagram([f]) + + assert d1.objects == FiniteSet(A, B) + assert d1.hom(A, B) == (FiniteSet(f), empty) + assert d1.hom(A, A) == (FiniteSet(id_A), empty) + assert d1.hom(B, B) == (FiniteSet(id_B), empty) + + assert d1 == Diagram([id_A, f]) + assert d1 == Diagram([f, f]) + + # Test the addition of composites. + d2 = Diagram([f, g]) + homAC = d2.hom(A, C)[0] + + assert d2.objects == FiniteSet(A, B, C) + assert g * f in d2.premises.keys() + assert homAC == FiniteSet(g * f) + + # Test equality, inequality and hash. + d11 = Diagram([f]) + + assert d1 == d11 + assert d1 != d2 + assert hash(d1) == hash(d11) + + d11 = Diagram({f: "unique"}) + assert d1 != d11 + + # Make sure that (re-)adding composites (with new properties) + # works as expected. + d = Diagram([f, g], {g * f: "unique"}) + assert d.conclusions == Dict({g * f: FiniteSet("unique")}) + + # Check the hom-sets when there are premises and conclusions. + assert d.hom(A, C) == (FiniteSet(g * f), FiniteSet(g * f)) + d = Diagram([f, g], [g * f]) + assert d.hom(A, C) == (FiniteSet(g * f), FiniteSet(g * f)) + + # Check how the properties of composite morphisms are computed. + d = Diagram({f: ["unique", "isomorphism"], g: "unique"}) + assert d.premises[g * f] == FiniteSet("unique") + + # Check that conclusion morphisms with new objects are not allowed. + d = Diagram([f], [g]) + assert d.conclusions == Dict({}) + + # Test an empty diagram. + d = Diagram() + assert d.premises == Dict({}) + assert d.conclusions == Dict({}) + assert d.objects == empty + + # Check a SymPy Dict object. + d = Diagram(Dict({f: FiniteSet("unique", "isomorphism"), g: "unique"})) + assert d.premises[g * f] == FiniteSet("unique") + + # Check the addition of components of composite morphisms. + d = Diagram([g * f]) + assert f in d.premises + assert g in d.premises + + # Check subdiagrams. + d = Diagram([f, g], {g * f: "unique"}) + + d1 = Diagram([f]) + assert d.is_subdiagram(d1) + assert not d1.is_subdiagram(d) + + d = Diagram([NamedMorphism(B, A, "f'")]) + assert not d.is_subdiagram(d1) + assert not d1.is_subdiagram(d) + + d1 = Diagram([f, g], {g * f: ["unique", "something"]}) + assert not d.is_subdiagram(d1) + assert not d1.is_subdiagram(d) + + d = Diagram({f: "blooh"}) + d1 = Diagram({f: "bleeh"}) + assert not d.is_subdiagram(d1) + assert not d1.is_subdiagram(d) + + d = Diagram([f, g], {f: "unique", g * f: "veryunique"}) + d1 = d.subdiagram_from_objects(FiniteSet(A, B)) + assert d1 == Diagram([f], {f: "unique"}) + raises(ValueError, lambda: d.subdiagram_from_objects(FiniteSet(A, + Object("D")))) + + raises(ValueError, lambda: Diagram({IdentityMorphism(A): "unique"})) + + +def test_category(): + A = Object("A") + B = Object("B") + C = Object("C") + + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + + d1 = Diagram([f, g]) + d2 = Diagram([f]) + + objects = d1.objects | d2.objects + + K = Category("K", objects, commutative_diagrams=[d1, d2]) + + assert K.name == "K" + assert K.objects == Class(objects) + assert K.commutative_diagrams == FiniteSet(d1, d2) + + raises(ValueError, lambda: Category("")) diff --git a/venv/lib/python3.11/site-packages/sympy/categories/tests/test_drawing.py b/venv/lib/python3.11/site-packages/sympy/categories/tests/test_drawing.py new file mode 100644 index 0000000000000000000000000000000000000000..63a13266cd6b58f6a85aad4af0813b395acbb5e1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/categories/tests/test_drawing.py @@ -0,0 +1,919 @@ +from sympy.categories.diagram_drawing import _GrowableGrid, ArrowStringDescription +from sympy.categories import (DiagramGrid, Object, NamedMorphism, + Diagram, XypicDiagramDrawer, xypic_draw_diagram) +from sympy.sets.sets import FiniteSet + + +def test_GrowableGrid(): + grid = _GrowableGrid(1, 2) + + # Check dimensions. + assert grid.width == 1 + assert grid.height == 2 + + # Check initialization of elements. + assert grid[0, 0] is None + assert grid[1, 0] is None + + # Check assignment to elements. + grid[0, 0] = 1 + grid[1, 0] = "two" + + assert grid[0, 0] == 1 + assert grid[1, 0] == "two" + + # Check appending a row. + grid.append_row() + + assert grid.width == 1 + assert grid.height == 3 + + assert grid[0, 0] == 1 + assert grid[1, 0] == "two" + assert grid[2, 0] is None + + # Check appending a column. + grid.append_column() + assert grid.width == 2 + assert grid.height == 3 + + assert grid[0, 0] == 1 + assert grid[1, 0] == "two" + assert grid[2, 0] is None + + assert grid[0, 1] is None + assert grid[1, 1] is None + assert grid[2, 1] is None + + grid = _GrowableGrid(1, 2) + grid[0, 0] = 1 + grid[1, 0] = "two" + + # Check prepending a row. + grid.prepend_row() + assert grid.width == 1 + assert grid.height == 3 + + assert grid[0, 0] is None + assert grid[1, 0] == 1 + assert grid[2, 0] == "two" + + # Check prepending a column. + grid.prepend_column() + assert grid.width == 2 + assert grid.height == 3 + + assert grid[0, 0] is None + assert grid[1, 0] is None + assert grid[2, 0] is None + + assert grid[0, 1] is None + assert grid[1, 1] == 1 + assert grid[2, 1] == "two" + + +def test_DiagramGrid(): + # Set up some objects and morphisms. + A = Object("A") + B = Object("B") + C = Object("C") + D = Object("D") + E = Object("E") + + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + h = NamedMorphism(D, A, "h") + k = NamedMorphism(D, B, "k") + + # A one-morphism diagram. + d = Diagram([f]) + grid = DiagramGrid(d) + + assert grid.width == 2 + assert grid.height == 1 + assert grid[0, 0] == A + assert grid[0, 1] == B + assert grid.morphisms == {f: FiniteSet()} + + # A triangle. + d = Diagram([f, g], {g * f: "unique"}) + grid = DiagramGrid(d) + + assert grid.width == 2 + assert grid.height == 2 + assert grid[0, 0] == A + assert grid[0, 1] == B + assert grid[1, 0] == C + assert grid[1, 1] is None + assert grid.morphisms == {f: FiniteSet(), g: FiniteSet(), + g * f: FiniteSet("unique")} + + # A triangle with a "loop" morphism. + l_A = NamedMorphism(A, A, "l_A") + d = Diagram([f, g, l_A]) + grid = DiagramGrid(d) + + assert grid.width == 2 + assert grid.height == 2 + assert grid[0, 0] == A + assert grid[0, 1] == B + assert grid[1, 0] is None + assert grid[1, 1] == C + assert grid.morphisms == {f: FiniteSet(), g: FiniteSet(), l_A: FiniteSet()} + + # A simple diagram. + d = Diagram([f, g, h, k]) + grid = DiagramGrid(d) + + assert grid.width == 3 + assert grid.height == 2 + assert grid[0, 0] == A + assert grid[0, 1] == B + assert grid[0, 2] == D + assert grid[1, 0] is None + assert grid[1, 1] == C + assert grid[1, 2] is None + assert grid.morphisms == {f: FiniteSet(), g: FiniteSet(), h: FiniteSet(), + k: FiniteSet()} + + assert str(grid) == '[[Object("A"), Object("B"), Object("D")], ' \ + '[None, Object("C"), None]]' + + # A chain of morphisms. + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + h = NamedMorphism(C, D, "h") + k = NamedMorphism(D, E, "k") + d = Diagram([f, g, h, k]) + grid = DiagramGrid(d) + + assert grid.width == 3 + assert grid.height == 3 + assert grid[0, 0] == A + assert grid[0, 1] == B + assert grid[0, 2] is None + assert grid[1, 0] is None + assert grid[1, 1] == C + assert grid[1, 2] == D + assert grid[2, 0] is None + assert grid[2, 1] is None + assert grid[2, 2] == E + assert grid.morphisms == {f: FiniteSet(), g: FiniteSet(), h: FiniteSet(), + k: FiniteSet()} + + # A square. + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, D, "g") + h = NamedMorphism(A, C, "h") + k = NamedMorphism(C, D, "k") + d = Diagram([f, g, h, k]) + grid = DiagramGrid(d) + + assert grid.width == 2 + assert grid.height == 2 + assert grid[0, 0] == A + assert grid[0, 1] == B + assert grid[1, 0] == C + assert grid[1, 1] == D + assert grid.morphisms == {f: FiniteSet(), g: FiniteSet(), h: FiniteSet(), + k: FiniteSet()} + + # A strange diagram which resulted from a typo when creating a + # test for five lemma, but which allowed to stop one extra problem + # in the algorithm. + A = Object("A") + B = Object("B") + C = Object("C") + D = Object("D") + E = Object("E") + A_ = Object("A'") + B_ = Object("B'") + C_ = Object("C'") + D_ = Object("D'") + E_ = Object("E'") + + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + h = NamedMorphism(C, D, "h") + i = NamedMorphism(D, E, "i") + + # These 4 morphisms should be between primed objects. + j = NamedMorphism(A, B, "j") + k = NamedMorphism(B, C, "k") + l = NamedMorphism(C, D, "l") + m = NamedMorphism(D, E, "m") + + o = NamedMorphism(A, A_, "o") + p = NamedMorphism(B, B_, "p") + q = NamedMorphism(C, C_, "q") + r = NamedMorphism(D, D_, "r") + s = NamedMorphism(E, E_, "s") + + d = Diagram([f, g, h, i, j, k, l, m, o, p, q, r, s]) + grid = DiagramGrid(d) + + assert grid.width == 3 + assert grid.height == 4 + assert grid[0, 0] is None + assert grid[0, 1] == A + assert grid[0, 2] == A_ + assert grid[1, 0] == C + assert grid[1, 1] == B + assert grid[1, 2] == B_ + assert grid[2, 0] == C_ + assert grid[2, 1] == D + assert grid[2, 2] == D_ + assert grid[3, 0] is None + assert grid[3, 1] == E + assert grid[3, 2] == E_ + + morphisms = {} + for m in [f, g, h, i, j, k, l, m, o, p, q, r, s]: + morphisms[m] = FiniteSet() + assert grid.morphisms == morphisms + + # A cube. + A1 = Object("A1") + A2 = Object("A2") + A3 = Object("A3") + A4 = Object("A4") + A5 = Object("A5") + A6 = Object("A6") + A7 = Object("A7") + A8 = Object("A8") + + # The top face of the cube. + f1 = NamedMorphism(A1, A2, "f1") + f2 = NamedMorphism(A1, A3, "f2") + f3 = NamedMorphism(A2, A4, "f3") + f4 = NamedMorphism(A3, A4, "f3") + + # The bottom face of the cube. + f5 = NamedMorphism(A5, A6, "f5") + f6 = NamedMorphism(A5, A7, "f6") + f7 = NamedMorphism(A6, A8, "f7") + f8 = NamedMorphism(A7, A8, "f8") + + # The remaining morphisms. + f9 = NamedMorphism(A1, A5, "f9") + f10 = NamedMorphism(A2, A6, "f10") + f11 = NamedMorphism(A3, A7, "f11") + f12 = NamedMorphism(A4, A8, "f11") + + d = Diagram([f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12]) + grid = DiagramGrid(d) + + assert grid.width == 4 + assert grid.height == 3 + assert grid[0, 0] is None + assert grid[0, 1] == A5 + assert grid[0, 2] == A6 + assert grid[0, 3] is None + assert grid[1, 0] is None + assert grid[1, 1] == A1 + assert grid[1, 2] == A2 + assert grid[1, 3] is None + assert grid[2, 0] == A7 + assert grid[2, 1] == A3 + assert grid[2, 2] == A4 + assert grid[2, 3] == A8 + + morphisms = {} + for m in [f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12]: + morphisms[m] = FiniteSet() + assert grid.morphisms == morphisms + + # A line diagram. + A = Object("A") + B = Object("B") + C = Object("C") + D = Object("D") + E = Object("E") + + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + h = NamedMorphism(C, D, "h") + i = NamedMorphism(D, E, "i") + d = Diagram([f, g, h, i]) + grid = DiagramGrid(d, layout="sequential") + + assert grid.width == 5 + assert grid.height == 1 + assert grid[0, 0] == A + assert grid[0, 1] == B + assert grid[0, 2] == C + assert grid[0, 3] == D + assert grid[0, 4] == E + assert grid.morphisms == {f: FiniteSet(), g: FiniteSet(), h: FiniteSet(), + i: FiniteSet()} + + # Test the transposed version. + grid = DiagramGrid(d, layout="sequential", transpose=True) + + assert grid.width == 1 + assert grid.height == 5 + assert grid[0, 0] == A + assert grid[1, 0] == B + assert grid[2, 0] == C + assert grid[3, 0] == D + assert grid[4, 0] == E + assert grid.morphisms == {f: FiniteSet(), g: FiniteSet(), h: FiniteSet(), + i: FiniteSet()} + + # A pullback. + m1 = NamedMorphism(A, B, "m1") + m2 = NamedMorphism(A, C, "m2") + s1 = NamedMorphism(B, D, "s1") + s2 = NamedMorphism(C, D, "s2") + f1 = NamedMorphism(E, B, "f1") + f2 = NamedMorphism(E, C, "f2") + g = NamedMorphism(E, A, "g") + + d = Diagram([m1, m2, s1, s2, f1, f2], {g: "unique"}) + grid = DiagramGrid(d) + + assert grid.width == 3 + assert grid.height == 2 + assert grid[0, 0] == A + assert grid[0, 1] == B + assert grid[0, 2] == E + assert grid[1, 0] == C + assert grid[1, 1] == D + assert grid[1, 2] is None + + morphisms = {g: FiniteSet("unique")} + for m in [m1, m2, s1, s2, f1, f2]: + morphisms[m] = FiniteSet() + assert grid.morphisms == morphisms + + # Test the pullback with sequential layout, just for stress + # testing. + grid = DiagramGrid(d, layout="sequential") + + assert grid.width == 5 + assert grid.height == 1 + assert grid[0, 0] == D + assert grid[0, 1] == B + assert grid[0, 2] == A + assert grid[0, 3] == C + assert grid[0, 4] == E + assert grid.morphisms == morphisms + + # Test a pullback with object grouping. + grid = DiagramGrid(d, groups=FiniteSet(E, FiniteSet(A, B, C, D))) + + assert grid.width == 3 + assert grid.height == 2 + assert grid[0, 0] == E + assert grid[0, 1] == A + assert grid[0, 2] == B + assert grid[1, 0] is None + assert grid[1, 1] == C + assert grid[1, 2] == D + assert grid.morphisms == morphisms + + # Five lemma, actually. + A = Object("A") + B = Object("B") + C = Object("C") + D = Object("D") + E = Object("E") + A_ = Object("A'") + B_ = Object("B'") + C_ = Object("C'") + D_ = Object("D'") + E_ = Object("E'") + + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + h = NamedMorphism(C, D, "h") + i = NamedMorphism(D, E, "i") + + j = NamedMorphism(A_, B_, "j") + k = NamedMorphism(B_, C_, "k") + l = NamedMorphism(C_, D_, "l") + m = NamedMorphism(D_, E_, "m") + + o = NamedMorphism(A, A_, "o") + p = NamedMorphism(B, B_, "p") + q = NamedMorphism(C, C_, "q") + r = NamedMorphism(D, D_, "r") + s = NamedMorphism(E, E_, "s") + + d = Diagram([f, g, h, i, j, k, l, m, o, p, q, r, s]) + grid = DiagramGrid(d) + + assert grid.width == 5 + assert grid.height == 3 + assert grid[0, 0] is None + assert grid[0, 1] == A + assert grid[0, 2] == A_ + assert grid[0, 3] is None + assert grid[0, 4] is None + assert grid[1, 0] == C + assert grid[1, 1] == B + assert grid[1, 2] == B_ + assert grid[1, 3] == C_ + assert grid[1, 4] is None + assert grid[2, 0] == D + assert grid[2, 1] == E + assert grid[2, 2] is None + assert grid[2, 3] == D_ + assert grid[2, 4] == E_ + + morphisms = {} + for m in [f, g, h, i, j, k, l, m, o, p, q, r, s]: + morphisms[m] = FiniteSet() + assert grid.morphisms == morphisms + + # Test the five lemma with object grouping. + grid = DiagramGrid(d, FiniteSet( + FiniteSet(A, B, C, D, E), FiniteSet(A_, B_, C_, D_, E_))) + + assert grid.width == 6 + assert grid.height == 3 + assert grid[0, 0] == A + assert grid[0, 1] == B + assert grid[0, 2] is None + assert grid[0, 3] == A_ + assert grid[0, 4] == B_ + assert grid[0, 5] is None + assert grid[1, 0] is None + assert grid[1, 1] == C + assert grid[1, 2] == D + assert grid[1, 3] is None + assert grid[1, 4] == C_ + assert grid[1, 5] == D_ + assert grid[2, 0] is None + assert grid[2, 1] is None + assert grid[2, 2] == E + assert grid[2, 3] is None + assert grid[2, 4] is None + assert grid[2, 5] == E_ + assert grid.morphisms == morphisms + + # Test the five lemma with object grouping, but mixing containers + # to represent groups. + grid = DiagramGrid(d, [(A, B, C, D, E), {A_, B_, C_, D_, E_}]) + + assert grid.width == 6 + assert grid.height == 3 + assert grid[0, 0] == A + assert grid[0, 1] == B + assert grid[0, 2] is None + assert grid[0, 3] == A_ + assert grid[0, 4] == B_ + assert grid[0, 5] is None + assert grid[1, 0] is None + assert grid[1, 1] == C + assert grid[1, 2] == D + assert grid[1, 3] is None + assert grid[1, 4] == C_ + assert grid[1, 5] == D_ + assert grid[2, 0] is None + assert grid[2, 1] is None + assert grid[2, 2] == E + assert grid[2, 3] is None + assert grid[2, 4] is None + assert grid[2, 5] == E_ + assert grid.morphisms == morphisms + + # Test the five lemma with object grouping and hints. + grid = DiagramGrid(d, { + FiniteSet(A, B, C, D, E): {"layout": "sequential", + "transpose": True}, + FiniteSet(A_, B_, C_, D_, E_): {"layout": "sequential", + "transpose": True}}, + transpose=True) + + assert grid.width == 5 + assert grid.height == 2 + assert grid[0, 0] == A + assert grid[0, 1] == B + assert grid[0, 2] == C + assert grid[0, 3] == D + assert grid[0, 4] == E + assert grid[1, 0] == A_ + assert grid[1, 1] == B_ + assert grid[1, 2] == C_ + assert grid[1, 3] == D_ + assert grid[1, 4] == E_ + assert grid.morphisms == morphisms + + # A two-triangle disconnected diagram. + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + f_ = NamedMorphism(A_, B_, "f") + g_ = NamedMorphism(B_, C_, "g") + d = Diagram([f, g, f_, g_], {g * f: "unique", g_ * f_: "unique"}) + grid = DiagramGrid(d) + + assert grid.width == 4 + assert grid.height == 2 + assert grid[0, 0] == A + assert grid[0, 1] == B + assert grid[0, 2] == A_ + assert grid[0, 3] == B_ + assert grid[1, 0] == C + assert grid[1, 1] is None + assert grid[1, 2] == C_ + assert grid[1, 3] is None + assert grid.morphisms == {f: FiniteSet(), g: FiniteSet(), f_: FiniteSet(), + g_: FiniteSet(), g * f: FiniteSet("unique"), + g_ * f_: FiniteSet("unique")} + + # A two-morphism disconnected diagram. + f = NamedMorphism(A, B, "f") + g = NamedMorphism(C, D, "g") + d = Diagram([f, g]) + grid = DiagramGrid(d) + + assert grid.width == 4 + assert grid.height == 1 + assert grid[0, 0] == A + assert grid[0, 1] == B + assert grid[0, 2] == C + assert grid[0, 3] == D + assert grid.morphisms == {f: FiniteSet(), g: FiniteSet()} + + # Test a one-object diagram. + f = NamedMorphism(A, A, "f") + d = Diagram([f]) + grid = DiagramGrid(d) + + assert grid.width == 1 + assert grid.height == 1 + assert grid[0, 0] == A + + # Test a two-object disconnected diagram. + g = NamedMorphism(B, B, "g") + d = Diagram([f, g]) + grid = DiagramGrid(d) + + assert grid.width == 2 + assert grid.height == 1 + assert grid[0, 0] == A + assert grid[0, 1] == B + + +def test_DiagramGrid_pseudopod(): + # Test a diagram in which even growing a pseudopod does not + # eventually help. + A = Object("A") + B = Object("B") + C = Object("C") + D = Object("D") + E = Object("E") + F = Object("F") + A_ = Object("A'") + B_ = Object("B'") + C_ = Object("C'") + D_ = Object("D'") + E_ = Object("E'") + + f1 = NamedMorphism(A, B, "f1") + f2 = NamedMorphism(A, C, "f2") + f3 = NamedMorphism(A, D, "f3") + f4 = NamedMorphism(A, E, "f4") + f5 = NamedMorphism(A, A_, "f5") + f6 = NamedMorphism(A, B_, "f6") + f7 = NamedMorphism(A, C_, "f7") + f8 = NamedMorphism(A, D_, "f8") + f9 = NamedMorphism(A, E_, "f9") + f10 = NamedMorphism(A, F, "f10") + d = Diagram([f1, f2, f3, f4, f5, f6, f7, f8, f9, f10]) + grid = DiagramGrid(d) + + assert grid.width == 5 + assert grid.height == 3 + assert grid[0, 0] == E + assert grid[0, 1] == C + assert grid[0, 2] == C_ + assert grid[0, 3] == E_ + assert grid[0, 4] == F + assert grid[1, 0] == D + assert grid[1, 1] == A + assert grid[1, 2] == A_ + assert grid[1, 3] is None + assert grid[1, 4] is None + assert grid[2, 0] == D_ + assert grid[2, 1] == B + assert grid[2, 2] == B_ + assert grid[2, 3] is None + assert grid[2, 4] is None + + morphisms = {} + for f in [f1, f2, f3, f4, f5, f6, f7, f8, f9, f10]: + morphisms[f] = FiniteSet() + assert grid.morphisms == morphisms + + +def test_ArrowStringDescription(): + astr = ArrowStringDescription("cm", "", None, "", "", "d", "r", "_", "f") + assert str(astr) == "\\ar[dr]_{f}" + + astr = ArrowStringDescription("cm", "", 12, "", "", "d", "r", "_", "f") + assert str(astr) == "\\ar[dr]_{f}" + + astr = ArrowStringDescription("cm", "^", 12, "", "", "d", "r", "_", "f") + assert str(astr) == "\\ar@/^12cm/[dr]_{f}" + + astr = ArrowStringDescription("cm", "", 12, "r", "", "d", "r", "_", "f") + assert str(astr) == "\\ar[dr]_{f}" + + astr = ArrowStringDescription("cm", "", 12, "r", "u", "d", "r", "_", "f") + assert str(astr) == "\\ar@(r,u)[dr]_{f}" + + astr = ArrowStringDescription("cm", "", 12, "r", "u", "d", "r", "_", "f") + assert str(astr) == "\\ar@(r,u)[dr]_{f}" + + astr = ArrowStringDescription("cm", "", 12, "r", "u", "d", "r", "_", "f") + astr.arrow_style = "{-->}" + assert str(astr) == "\\ar@(r,u)@{-->}[dr]_{f}" + + astr = ArrowStringDescription("cm", "_", 12, "", "", "d", "r", "_", "f") + astr.arrow_style = "{-->}" + assert str(astr) == "\\ar@/_12cm/@{-->}[dr]_{f}" + + +def test_XypicDiagramDrawer_line(): + # A linear diagram. + A = Object("A") + B = Object("B") + C = Object("C") + D = Object("D") + E = Object("E") + + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + h = NamedMorphism(C, D, "h") + i = NamedMorphism(D, E, "i") + d = Diagram([f, g, h, i]) + grid = DiagramGrid(d, layout="sequential") + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "A \\ar[r]^{f} & B \\ar[r]^{g} & C \\ar[r]^{h} & D \\ar[r]^{i} & E \n" \ + "}\n" + + # The same diagram, transposed. + grid = DiagramGrid(d, layout="sequential", transpose=True) + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "A \\ar[d]^{f} \\\\\n" \ + "B \\ar[d]^{g} \\\\\n" \ + "C \\ar[d]^{h} \\\\\n" \ + "D \\ar[d]^{i} \\\\\n" \ + "E \n" \ + "}\n" + + +def test_XypicDiagramDrawer_triangle(): + # A triangle diagram. + A = Object("A") + B = Object("B") + C = Object("C") + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + + d = Diagram([f, g], {g * f: "unique"}) + grid = DiagramGrid(d) + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "A \\ar[d]_{g\\circ f} \\ar[r]^{f} & B \\ar[ld]^{g} \\\\\n" \ + "C & \n" \ + "}\n" + + # The same diagram, transposed. + grid = DiagramGrid(d, transpose=True) + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "A \\ar[r]^{g\\circ f} \\ar[d]_{f} & C \\\\\n" \ + "B \\ar[ru]_{g} & \n" \ + "}\n" + + # The same diagram, with a masked morphism. + assert drawer.draw(d, grid, masked=[g]) == "\\xymatrix{\n" \ + "A \\ar[r]^{g\\circ f} \\ar[d]_{f} & C \\\\\n" \ + "B & \n" \ + "}\n" + + # The same diagram with a formatter for "unique". + def formatter(astr): + astr.label = "\\exists !" + astr.label + astr.arrow_style = "{-->}" + + drawer.arrow_formatters["unique"] = formatter + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "A \\ar@{-->}[r]^{\\exists !g\\circ f} \\ar[d]_{f} & C \\\\\n" \ + "B \\ar[ru]_{g} & \n" \ + "}\n" + + # The same diagram with a default formatter. + def default_formatter(astr): + astr.label_displacement = "(0.45)" + + drawer.default_arrow_formatter = default_formatter + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "A \\ar@{-->}[r]^(0.45){\\exists !g\\circ f} \\ar[d]_(0.45){f} & C \\\\\n" \ + "B \\ar[ru]_(0.45){g} & \n" \ + "}\n" + + # A triangle diagram with a lot of morphisms between the same + # objects. + f1 = NamedMorphism(B, A, "f1") + f2 = NamedMorphism(A, B, "f2") + g1 = NamedMorphism(C, B, "g1") + g2 = NamedMorphism(B, C, "g2") + d = Diagram([f, f1, f2, g, g1, g2], {f1 * g1: "unique", g2 * f2: "unique"}) + + grid = DiagramGrid(d, transpose=True) + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid, masked=[f1*g1*g2*f2, g2*f2*f1*g1]) == \ + "\\xymatrix{\n" \ + "A \\ar[r]^{g_{2}\\circ f_{2}} \\ar[d]_{f} \\ar@/^3mm/[d]^{f_{2}} " \ + "& C \\ar@/^3mm/[l]^{f_{1}\\circ g_{1}} \\ar@/^3mm/[ld]^{g_{1}} \\\\\n" \ + "B \\ar@/^3mm/[u]^{f_{1}} \\ar[ru]_{g} \\ar@/^3mm/[ru]^{g_{2}} & \n" \ + "}\n" + + +def test_XypicDiagramDrawer_cube(): + # A cube diagram. + A1 = Object("A1") + A2 = Object("A2") + A3 = Object("A3") + A4 = Object("A4") + A5 = Object("A5") + A6 = Object("A6") + A7 = Object("A7") + A8 = Object("A8") + + # The top face of the cube. + f1 = NamedMorphism(A1, A2, "f1") + f2 = NamedMorphism(A1, A3, "f2") + f3 = NamedMorphism(A2, A4, "f3") + f4 = NamedMorphism(A3, A4, "f3") + + # The bottom face of the cube. + f5 = NamedMorphism(A5, A6, "f5") + f6 = NamedMorphism(A5, A7, "f6") + f7 = NamedMorphism(A6, A8, "f7") + f8 = NamedMorphism(A7, A8, "f8") + + # The remaining morphisms. + f9 = NamedMorphism(A1, A5, "f9") + f10 = NamedMorphism(A2, A6, "f10") + f11 = NamedMorphism(A3, A7, "f11") + f12 = NamedMorphism(A4, A8, "f11") + + d = Diagram([f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12]) + grid = DiagramGrid(d) + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "& A_{5} \\ar[r]^{f_{5}} \\ar[ldd]_{f_{6}} & A_{6} \\ar[rdd]^{f_{7}} " \ + "& \\\\\n" \ + "& A_{1} \\ar[r]^{f_{1}} \\ar[d]^{f_{2}} \\ar[u]^{f_{9}} & A_{2} " \ + "\\ar[d]^{f_{3}} \\ar[u]_{f_{10}} & \\\\\n" \ + "A_{7} \\ar@/_3mm/[rrr]_{f_{8}} & A_{3} \\ar[r]^{f_{3}} \\ar[l]_{f_{11}} " \ + "& A_{4} \\ar[r]^{f_{11}} & A_{8} \n" \ + "}\n" + + # The same diagram, transposed. + grid = DiagramGrid(d, transpose=True) + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "& & A_{7} \\ar@/^3mm/[ddd]^{f_{8}} \\\\\n" \ + "A_{5} \\ar[d]_{f_{5}} \\ar[rru]^{f_{6}} & A_{1} \\ar[d]^{f_{1}} " \ + "\\ar[r]^{f_{2}} \\ar[l]^{f_{9}} & A_{3} \\ar[d]_{f_{3}} " \ + "\\ar[u]^{f_{11}} \\\\\n" \ + "A_{6} \\ar[rrd]_{f_{7}} & A_{2} \\ar[r]^{f_{3}} \\ar[l]^{f_{10}} " \ + "& A_{4} \\ar[d]_{f_{11}} \\\\\n" \ + "& & A_{8} \n" \ + "}\n" + + +def test_XypicDiagramDrawer_curved_and_loops(): + # A simple diagram, with a curved arrow. + A = Object("A") + B = Object("B") + C = Object("C") + D = Object("D") + + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + h = NamedMorphism(D, A, "h") + k = NamedMorphism(D, B, "k") + d = Diagram([f, g, h, k]) + grid = DiagramGrid(d) + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "A \\ar[r]_{f} & B \\ar[d]^{g} & D \\ar[l]^{k} \\ar@/_3mm/[ll]_{h} \\\\\n" \ + "& C & \n" \ + "}\n" + + # The same diagram, transposed. + grid = DiagramGrid(d, transpose=True) + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "A \\ar[d]^{f} & \\\\\n" \ + "B \\ar[r]^{g} & C \\\\\n" \ + "D \\ar[u]_{k} \\ar@/^3mm/[uu]^{h} & \n" \ + "}\n" + + # The same diagram, larger and rotated. + assert drawer.draw(d, grid, diagram_format="@+1cm@dr") == \ + "\\xymatrix@+1cm@dr{\n" \ + "A \\ar[d]^{f} & \\\\\n" \ + "B \\ar[r]^{g} & C \\\\\n" \ + "D \\ar[u]_{k} \\ar@/^3mm/[uu]^{h} & \n" \ + "}\n" + + # A simple diagram with three curved arrows. + h1 = NamedMorphism(D, A, "h1") + h2 = NamedMorphism(A, D, "h2") + k = NamedMorphism(D, B, "k") + d = Diagram([f, g, h, k, h1, h2]) + grid = DiagramGrid(d) + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "A \\ar[r]_{f} \\ar@/^3mm/[rr]^{h_{2}} & B \\ar[d]^{g} & D \\ar[l]^{k} " \ + "\\ar@/_7mm/[ll]_{h} \\ar@/_11mm/[ll]_{h_{1}} \\\\\n" \ + "& C & \n" \ + "}\n" + + # The same diagram, transposed. + grid = DiagramGrid(d, transpose=True) + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "A \\ar[d]^{f} \\ar@/_3mm/[dd]_{h_{2}} & \\\\\n" \ + "B \\ar[r]^{g} & C \\\\\n" \ + "D \\ar[u]_{k} \\ar@/^7mm/[uu]^{h} \\ar@/^11mm/[uu]^{h_{1}} & \n" \ + "}\n" + + # The same diagram, with "loop" morphisms. + l_A = NamedMorphism(A, A, "l_A") + l_D = NamedMorphism(D, D, "l_D") + l_C = NamedMorphism(C, C, "l_C") + d = Diagram([f, g, h, k, h1, h2, l_A, l_D, l_C]) + grid = DiagramGrid(d) + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "A \\ar[r]_{f} \\ar@/^3mm/[rr]^{h_{2}} \\ar@(u,l)[]^{l_{A}} " \ + "& B \\ar[d]^{g} & D \\ar[l]^{k} \\ar@/_7mm/[ll]_{h} " \ + "\\ar@/_11mm/[ll]_{h_{1}} \\ar@(r,u)[]^{l_{D}} \\\\\n" \ + "& C \\ar@(l,d)[]^{l_{C}} & \n" \ + "}\n" + + # The same diagram with "loop" morphisms, transposed. + grid = DiagramGrid(d, transpose=True) + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "A \\ar[d]^{f} \\ar@/_3mm/[dd]_{h_{2}} \\ar@(r,u)[]^{l_{A}} & \\\\\n" \ + "B \\ar[r]^{g} & C \\ar@(r,u)[]^{l_{C}} \\\\\n" \ + "D \\ar[u]_{k} \\ar@/^7mm/[uu]^{h} \\ar@/^11mm/[uu]^{h_{1}} " \ + "\\ar@(l,d)[]^{l_{D}} & \n" \ + "}\n" + + # The same diagram with two "loop" morphisms per object. + l_A_ = NamedMorphism(A, A, "n_A") + l_D_ = NamedMorphism(D, D, "n_D") + l_C_ = NamedMorphism(C, C, "n_C") + d = Diagram([f, g, h, k, h1, h2, l_A, l_D, l_C, l_A_, l_D_, l_C_]) + grid = DiagramGrid(d) + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "A \\ar[r]_{f} \\ar@/^3mm/[rr]^{h_{2}} \\ar@(u,l)[]^{l_{A}} " \ + "\\ar@/^3mm/@(l,d)[]^{n_{A}} & B \\ar[d]^{g} & D \\ar[l]^{k} " \ + "\\ar@/_7mm/[ll]_{h} \\ar@/_11mm/[ll]_{h_{1}} \\ar@(r,u)[]^{l_{D}} " \ + "\\ar@/^3mm/@(d,r)[]^{n_{D}} \\\\\n" \ + "& C \\ar@(l,d)[]^{l_{C}} \\ar@/^3mm/@(d,r)[]^{n_{C}} & \n" \ + "}\n" + + # The same diagram with two "loop" morphisms per object, transposed. + grid = DiagramGrid(d, transpose=True) + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == "\\xymatrix{\n" \ + "A \\ar[d]^{f} \\ar@/_3mm/[dd]_{h_{2}} \\ar@(r,u)[]^{l_{A}} " \ + "\\ar@/^3mm/@(u,l)[]^{n_{A}} & \\\\\n" \ + "B \\ar[r]^{g} & C \\ar@(r,u)[]^{l_{C}} \\ar@/^3mm/@(d,r)[]^{n_{C}} \\\\\n" \ + "D \\ar[u]_{k} \\ar@/^7mm/[uu]^{h} \\ar@/^11mm/[uu]^{h_{1}} " \ + "\\ar@(l,d)[]^{l_{D}} \\ar@/^3mm/@(d,r)[]^{n_{D}} & \n" \ + "}\n" + + +def test_xypic_draw_diagram(): + # A linear diagram. + A = Object("A") + B = Object("B") + C = Object("C") + D = Object("D") + E = Object("E") + + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + h = NamedMorphism(C, D, "h") + i = NamedMorphism(D, E, "i") + d = Diagram([f, g, h, i]) + + grid = DiagramGrid(d, layout="sequential") + drawer = XypicDiagramDrawer() + assert drawer.draw(d, grid) == xypic_draw_diagram(d, layout="sequential") diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/__init__.py b/venv/lib/python3.11/site-packages/sympy/codegen/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..62b195633bae28371bdf9e79317050d7fa7125ae --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/__init__.py @@ -0,0 +1,24 @@ +""" The ``sympy.codegen`` module contains classes and functions for building +abstract syntax trees of algorithms. These trees may then be printed by the +code-printers in ``sympy.printing``. + +There are several submodules available: +- ``sympy.codegen.ast``: AST nodes useful across multiple languages. +- ``sympy.codegen.cnodes``: AST nodes useful for the C family of languages. +- ``sympy.codegen.fnodes``: AST nodes useful for Fortran. +- ``sympy.codegen.cfunctions``: functions specific to C (C99 math functions) +- ``sympy.codegen.ffunctions``: functions specific to Fortran (e.g. ``kind``). + + + +""" +from .ast import ( + Assignment, aug_assign, CodeBlock, For, Attribute, Variable, Declaration, + While, Scope, Print, FunctionPrototype, FunctionDefinition, FunctionCall +) + +__all__ = [ + 'Assignment', 'aug_assign', 'CodeBlock', 'For', 'Attribute', 'Variable', + 'Declaration', 'While', 'Scope', 'Print', 'FunctionPrototype', + 'FunctionDefinition', 'FunctionCall', +] diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/abstract_nodes.py b/venv/lib/python3.11/site-packages/sympy/codegen/abstract_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..ae0a8b3e996a7112edf2568c00138e11c3f3327d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/abstract_nodes.py @@ -0,0 +1,18 @@ +"""This module provides containers for python objects that are valid +printing targets but are not a subclass of SymPy's Printable. +""" + + +from sympy.core.containers import Tuple + + +class List(Tuple): + """Represents a (frozen) (Python) list (for code printing purposes).""" + def __eq__(self, other): + if isinstance(other, list): + return self == List(*other) + else: + return self.args == other + + def __hash__(self): + return super().__hash__() diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/algorithms.py b/venv/lib/python3.11/site-packages/sympy/codegen/algorithms.py new file mode 100644 index 0000000000000000000000000000000000000000..f4890eb8c25e565095600e2713c0de270aa0cf97 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/algorithms.py @@ -0,0 +1,180 @@ +from sympy.core.containers import Tuple +from sympy.core.numbers import oo +from sympy.core.relational import (Gt, Lt) +from sympy.core.symbol import (Dummy, Symbol) +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.miscellaneous import Min, Max +from sympy.logic.boolalg import And +from sympy.codegen.ast import ( + Assignment, AddAugmentedAssignment, break_, CodeBlock, Declaration, FunctionDefinition, + Print, Return, Scope, While, Variable, Pointer, real +) +from sympy.codegen.cfunctions import isnan + +""" This module collects functions for constructing ASTs representing algorithms. """ + +def newtons_method(expr, wrt, atol=1e-12, delta=None, *, rtol=4e-16, debug=False, + itermax=None, counter=None, delta_fn=lambda e, x: -e/e.diff(x), + cse=False, handle_nan=None, + bounds=None): + """ Generates an AST for Newton-Raphson method (a root-finding algorithm). + + Explanation + =========== + + Returns an abstract syntax tree (AST) based on ``sympy.codegen.ast`` for Netwon's + method of root-finding. + + Parameters + ========== + + expr : expression + wrt : Symbol + With respect to, i.e. what is the variable. + atol : number or expression + Absolute tolerance (stopping criterion) + rtol : number or expression + Relative tolerance (stopping criterion) + delta : Symbol + Will be a ``Dummy`` if ``None``. + debug : bool + Whether to print convergence information during iterations + itermax : number or expr + Maximum number of iterations. + counter : Symbol + Will be a ``Dummy`` if ``None``. + delta_fn: Callable[[Expr, Symbol], Expr] + computes the step, default is newtons method. For e.g. Halley's method + use delta_fn=lambda e, x: -2*e*e.diff(x)/(2*e.diff(x)**2 - e*e.diff(x, 2)) + cse: bool + Perform common sub-expression elimination on delta expression + handle_nan: Token + How to handle occurrence of not-a-number (NaN). + bounds: Optional[tuple[Expr, Expr]] + Perform optimization within bounds + + Examples + ======== + + >>> from sympy import symbols, cos + >>> from sympy.codegen.ast import Assignment + >>> from sympy.codegen.algorithms import newtons_method + >>> x, dx, atol = symbols('x dx atol') + >>> expr = cos(x) - x**3 + >>> algo = newtons_method(expr, x, atol=atol, delta=dx) + >>> algo.has(Assignment(dx, -expr/expr.diff(x))) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Newton%27s_method + + """ + + if delta is None: + delta = Dummy() + Wrapper = Scope + name_d = 'delta' + else: + Wrapper = lambda x: x + name_d = delta.name + + delta_expr = delta_fn(expr, wrt) + if cse: + from sympy.simplify.cse_main import cse + cses, (red,) = cse([delta_expr.factor()]) + whl_bdy = [Assignment(dum, sub_e) for dum, sub_e in cses] + whl_bdy += [Assignment(delta, red)] + else: + whl_bdy = [Assignment(delta, delta_expr)] + if handle_nan is not None: + whl_bdy += [While(isnan(delta), CodeBlock(handle_nan, break_))] + whl_bdy += [AddAugmentedAssignment(wrt, delta)] + if bounds is not None: + whl_bdy += [Assignment(wrt, Min(Max(wrt, bounds[0]), bounds[1]))] + if debug: + prnt = Print([wrt, delta], r"{}=%12.5g {}=%12.5g\n".format(wrt.name, name_d)) + whl_bdy += [prnt] + req = Gt(Abs(delta), atol + rtol*Abs(wrt)) + declars = [Declaration(Variable(delta, type=real, value=oo))] + if itermax is not None: + counter = counter or Dummy(integer=True) + v_counter = Variable.deduced(counter, 0) + declars.append(Declaration(v_counter)) + whl_bdy.append(AddAugmentedAssignment(counter, 1)) + req = And(req, Lt(counter, itermax)) + whl = While(req, CodeBlock(*whl_bdy)) + blck = declars + if debug: + blck.append(Print([wrt], r"{}=%12.5g\n".format(wrt.name))) + blck += [whl] + return Wrapper(CodeBlock(*blck)) + + +def _symbol_of(arg): + if isinstance(arg, Declaration): + arg = arg.variable.symbol + elif isinstance(arg, Variable): + arg = arg.symbol + return arg + + +def newtons_method_function(expr, wrt, params=None, func_name="newton", attrs=Tuple(), *, delta=None, **kwargs): + """ Generates an AST for a function implementing the Newton-Raphson method. + + Parameters + ========== + + expr : expression + wrt : Symbol + With respect to, i.e. what is the variable + params : iterable of symbols + Symbols appearing in expr that are taken as constants during the iterations + (these will be accepted as parameters to the generated function). + func_name : str + Name of the generated function. + attrs : Tuple + Attribute instances passed as ``attrs`` to ``FunctionDefinition``. + \\*\\*kwargs : + Keyword arguments passed to :func:`sympy.codegen.algorithms.newtons_method`. + + Examples + ======== + + >>> from sympy import symbols, cos + >>> from sympy.codegen.algorithms import newtons_method_function + >>> from sympy.codegen.pyutils import render_as_module + >>> x = symbols('x') + >>> expr = cos(x) - x**3 + >>> func = newtons_method_function(expr, x) + >>> py_mod = render_as_module(func) # source code as string + >>> namespace = {} + >>> exec(py_mod, namespace, namespace) + >>> res = eval('newton(0.5)', namespace) + >>> abs(res - 0.865474033102) < 1e-12 + True + + See Also + ======== + + sympy.codegen.algorithms.newtons_method + + """ + if params is None: + params = (wrt,) + pointer_subs = {p.symbol: Symbol('(*%s)' % p.symbol.name) + for p in params if isinstance(p, Pointer)} + if delta is None: + delta = Symbol('d_' + wrt.name) + if expr.has(delta): + delta = None # will use Dummy + algo = newtons_method(expr, wrt, delta=delta, **kwargs).xreplace(pointer_subs) + if isinstance(algo, Scope): + algo = algo.body + not_in_params = expr.free_symbols.difference({_symbol_of(p) for p in params}) + if not_in_params: + raise ValueError("Missing symbols in params: %s" % ', '.join(map(str, not_in_params))) + declars = tuple(Variable(p, real) for p in params) + body = CodeBlock(algo, Return(wrt)) + return FunctionDefinition(real, func_name, declars, body, attrs=attrs) diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/approximations.py b/venv/lib/python3.11/site-packages/sympy/codegen/approximations.py new file mode 100644 index 0000000000000000000000000000000000000000..c6486926938224c5052dc5adfac29807e82eb4d8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/approximations.py @@ -0,0 +1,187 @@ +import math +from sympy.sets.sets import Interval +from sympy.calculus.singularities import is_increasing, is_decreasing +from sympy.codegen.rewriting import Optimization +from sympy.core.function import UndefinedFunction + +""" +This module collects classes useful for approximate rewriting of expressions. +This can be beneficial when generating numeric code for which performance is +of greater importance than precision (e.g. for preconditioners used in iterative +methods). +""" + +class SumApprox(Optimization): + """ + Approximates sum by neglecting small terms. + + Explanation + =========== + + If terms are expressions which can be determined to be monotonic, then + bounds for those expressions are added. + + Parameters + ========== + + bounds : dict + Mapping expressions to length 2 tuple of bounds (low, high). + reltol : number + Threshold for when to ignore a term. Taken relative to the largest + lower bound among bounds. + + Examples + ======== + + >>> from sympy import exp + >>> from sympy.abc import x, y, z + >>> from sympy.codegen.rewriting import optimize + >>> from sympy.codegen.approximations import SumApprox + >>> bounds = {x: (-1, 1), y: (1000, 2000), z: (-10, 3)} + >>> sum_approx3 = SumApprox(bounds, reltol=1e-3) + >>> sum_approx2 = SumApprox(bounds, reltol=1e-2) + >>> sum_approx1 = SumApprox(bounds, reltol=1e-1) + >>> expr = 3*(x + y + exp(z)) + >>> optimize(expr, [sum_approx3]) + 3*(x + y + exp(z)) + >>> optimize(expr, [sum_approx2]) + 3*y + 3*exp(z) + >>> optimize(expr, [sum_approx1]) + 3*y + + """ + + def __init__(self, bounds, reltol, **kwargs): + super().__init__(**kwargs) + self.bounds = bounds + self.reltol = reltol + + def __call__(self, expr): + return expr.factor().replace(self.query, lambda arg: self.value(arg)) + + def query(self, expr): + return expr.is_Add + + def value(self, add): + for term in add.args: + if term.is_number or term in self.bounds or len(term.free_symbols) != 1: + continue + fs, = term.free_symbols + if fs not in self.bounds: + continue + intrvl = Interval(*self.bounds[fs]) + if is_increasing(term, intrvl, fs): + self.bounds[term] = ( + term.subs({fs: self.bounds[fs][0]}), + term.subs({fs: self.bounds[fs][1]}) + ) + elif is_decreasing(term, intrvl, fs): + self.bounds[term] = ( + term.subs({fs: self.bounds[fs][1]}), + term.subs({fs: self.bounds[fs][0]}) + ) + else: + return add + + if all(term.is_number or term in self.bounds for term in add.args): + bounds = [(term, term) if term.is_number else self.bounds[term] for term in add.args] + largest_abs_guarantee = 0 + for lo, hi in bounds: + if lo <= 0 <= hi: + continue + largest_abs_guarantee = max(largest_abs_guarantee, + min(abs(lo), abs(hi))) + new_terms = [] + for term, (lo, hi) in zip(add.args, bounds): + if max(abs(lo), abs(hi)) >= largest_abs_guarantee*self.reltol: + new_terms.append(term) + return add.func(*new_terms) + else: + return add + + +class SeriesApprox(Optimization): + """ Approximates functions by expanding them as a series. + + Parameters + ========== + + bounds : dict + Mapping expressions to length 2 tuple of bounds (low, high). + reltol : number + Threshold for when to ignore a term. Taken relative to the largest + lower bound among bounds. + max_order : int + Largest order to include in series expansion + n_point_checks : int (even) + The validity of an expansion (with respect to reltol) is checked at + discrete points (linearly spaced over the bounds of the variable). The + number of points used in this numerical check is given by this number. + + Examples + ======== + + >>> from sympy import sin, pi + >>> from sympy.abc import x, y + >>> from sympy.codegen.rewriting import optimize + >>> from sympy.codegen.approximations import SeriesApprox + >>> bounds = {x: (-.1, .1), y: (pi-1, pi+1)} + >>> series_approx2 = SeriesApprox(bounds, reltol=1e-2) + >>> series_approx3 = SeriesApprox(bounds, reltol=1e-3) + >>> series_approx8 = SeriesApprox(bounds, reltol=1e-8) + >>> expr = sin(x)*sin(y) + >>> optimize(expr, [series_approx2]) + x*(-y + (y - pi)**3/6 + pi) + >>> optimize(expr, [series_approx3]) + (-x**3/6 + x)*sin(y) + >>> optimize(expr, [series_approx8]) + sin(x)*sin(y) + + """ + def __init__(self, bounds, reltol, max_order=4, n_point_checks=4, **kwargs): + super().__init__(**kwargs) + self.bounds = bounds + self.reltol = reltol + self.max_order = max_order + if n_point_checks % 2 == 1: + raise ValueError("Checking the solution at expansion point is not helpful") + self.n_point_checks = n_point_checks + self._prec = math.ceil(-math.log10(self.reltol)) + + def __call__(self, expr): + return expr.factor().replace(self.query, lambda arg: self.value(arg)) + + def query(self, expr): + return (expr.is_Function and not isinstance(expr, UndefinedFunction) + and len(expr.args) == 1) + + def value(self, fexpr): + free_symbols = fexpr.free_symbols + if len(free_symbols) != 1: + return fexpr + symb, = free_symbols + if symb not in self.bounds: + return fexpr + lo, hi = self.bounds[symb] + x0 = (lo + hi)/2 + cheapest = None + for n in range(self.max_order+1, 0, -1): + fseri = fexpr.series(symb, x0=x0, n=n).removeO() + n_ok = True + for idx in range(self.n_point_checks): + x = lo + idx*(hi - lo)/(self.n_point_checks - 1) + val = fseri.xreplace({symb: x}) + ref = fexpr.xreplace({symb: x}) + if abs((1 - val/ref).evalf(self._prec)) > self.reltol: + n_ok = False + break + + if n_ok: + cheapest = fseri + else: + break + + if cheapest is None: + return fexpr + else: + return cheapest diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/ast.py b/venv/lib/python3.11/site-packages/sympy/codegen/ast.py new file mode 100644 index 0000000000000000000000000000000000000000..dd774ca87c5c9d4b55c8ea7a3b68837035b0d06d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/ast.py @@ -0,0 +1,1906 @@ +""" +Types used to represent a full function/module as an Abstract Syntax Tree. + +Most types are small, and are merely used as tokens in the AST. A tree diagram +has been included below to illustrate the relationships between the AST types. + + +AST Type Tree +------------- +:: + + *Basic* + | + | + CodegenAST + | + |--->AssignmentBase + | |--->Assignment + | |--->AugmentedAssignment + | |--->AddAugmentedAssignment + | |--->SubAugmentedAssignment + | |--->MulAugmentedAssignment + | |--->DivAugmentedAssignment + | |--->ModAugmentedAssignment + | + |--->CodeBlock + | + | + |--->Token + |--->Attribute + |--->For + |--->String + | |--->QuotedString + | |--->Comment + |--->Type + | |--->IntBaseType + | | |--->_SizedIntType + | | |--->SignedIntType + | | |--->UnsignedIntType + | |--->FloatBaseType + | |--->FloatType + | |--->ComplexBaseType + | |--->ComplexType + |--->Node + | |--->Variable + | | |---> Pointer + | |--->FunctionPrototype + | |--->FunctionDefinition + |--->Element + |--->Declaration + |--->While + |--->Scope + |--->Stream + |--->Print + |--->FunctionCall + |--->BreakToken + |--->ContinueToken + |--->NoneToken + |--->Return + + +Predefined types +---------------- + +A number of ``Type`` instances are provided in the ``sympy.codegen.ast`` module +for convenience. Perhaps the two most common ones for code-generation (of numeric +codes) are ``float32`` and ``float64`` (known as single and double precision respectively). +There are also precision generic versions of Types (for which the codeprinters selects the +underlying data type at time of printing): ``real``, ``integer``, ``complex_``, ``bool_``. + +The other ``Type`` instances defined are: + +- ``intc``: Integer type used by C's "int". +- ``intp``: Integer type used by C's "unsigned". +- ``int8``, ``int16``, ``int32``, ``int64``: n-bit integers. +- ``uint8``, ``uint16``, ``uint32``, ``uint64``: n-bit unsigned integers. +- ``float80``: known as "extended precision" on modern x86/amd64 hardware. +- ``complex64``: Complex number represented by two ``float32`` numbers +- ``complex128``: Complex number represented by two ``float64`` numbers + +Using the nodes +--------------- + +It is possible to construct simple algorithms using the AST nodes. Let's construct a loop applying +Newton's method:: + + >>> from sympy import symbols, cos + >>> from sympy.codegen.ast import While, Assignment, aug_assign, Print, QuotedString + >>> t, dx, x = symbols('tol delta val') + >>> expr = cos(x) - x**3 + >>> whl = While(abs(dx) > t, [ + ... Assignment(dx, -expr/expr.diff(x)), + ... aug_assign(x, '+', dx), + ... Print([x]) + ... ]) + >>> from sympy import pycode + >>> py_str = pycode(whl) + >>> print(py_str) + while (abs(delta) > tol): + delta = (val**3 - math.cos(val))/(-3*val**2 - math.sin(val)) + val += delta + print(val) + >>> import math + >>> tol, val, delta = 1e-5, 0.5, float('inf') + >>> exec(py_str) + 1.1121416371 + 0.909672693737 + 0.867263818209 + 0.865477135298 + 0.865474033111 + >>> print('%3.1g' % (math.cos(val) - val**3)) + -3e-11 + +If we want to generate Fortran code for the same while loop we simple call ``fcode``:: + + >>> from sympy import fcode + >>> print(fcode(whl, standard=2003, source_format='free')) + do while (abs(delta) > tol) + delta = (val**3 - cos(val))/(-3*val**2 - sin(val)) + val = val + delta + print *, val + end do + +There is a function constructing a loop (or a complete function) like this in +:mod:`sympy.codegen.algorithms`. + +""" + +from __future__ import annotations +from typing import Any + +from collections import defaultdict + +from sympy.core.relational import (Ge, Gt, Le, Lt) +from sympy.core import Symbol, Tuple, Dummy +from sympy.core.basic import Basic +from sympy.core.expr import Expr, Atom +from sympy.core.numbers import Float, Integer, oo +from sympy.core.sympify import _sympify, sympify, SympifyError +from sympy.utilities.iterables import (iterable, topological_sort, + numbered_symbols, filter_symbols) + + +def _mk_Tuple(args): + """ + Create a SymPy Tuple object from an iterable, converting Python strings to + AST strings. + + Parameters + ========== + + args: iterable + Arguments to :class:`sympy.Tuple`. + + Returns + ======= + + sympy.Tuple + """ + args = [String(arg) if isinstance(arg, str) else arg for arg in args] + return Tuple(*args) + + +class CodegenAST(Basic): + __slots__ = () + + +class Token(CodegenAST): + """ Base class for the AST types. + + Explanation + =========== + + Defining fields are set in ``_fields``. Attributes (defined in _fields) + are only allowed to contain instances of Basic (unless atomic, see + ``String``). The arguments to ``__new__()`` correspond to the attributes in + the order defined in ``_fields`. The ``defaults`` class attribute is a + dictionary mapping attribute names to their default values. + + Subclasses should not need to override the ``__new__()`` method. They may + define a class or static method named ``_construct_`` for each + attribute to process the value passed to ``__new__()``. Attributes listed + in the class attribute ``not_in_args`` are not passed to :class:`~.Basic`. + """ + + __slots__: tuple[str, ...] = () + _fields = __slots__ + defaults: dict[str, Any] = {} + not_in_args: list[str] = [] + indented_args = ['body'] + + @property + def is_Atom(self): + return len(self._fields) == 0 + + @classmethod + def _get_constructor(cls, attr): + """ Get the constructor function for an attribute by name. """ + return getattr(cls, '_construct_%s' % attr, lambda x: x) + + @classmethod + def _construct(cls, attr, arg): + """ Construct an attribute value from argument passed to ``__new__()``. """ + # arg may be ``NoneToken()``, so comparison is done using == instead of ``is`` operator + if arg == None: + return cls.defaults.get(attr, none) + else: + if isinstance(arg, Dummy): # SymPy's replace uses Dummy instances + return arg + else: + return cls._get_constructor(attr)(arg) + + def __new__(cls, *args, **kwargs): + # Pass through existing instances when given as sole argument + if len(args) == 1 and not kwargs and isinstance(args[0], cls): + return args[0] + + if len(args) > len(cls._fields): + raise ValueError("Too many arguments (%d), expected at most %d" % (len(args), len(cls._fields))) + + attrvals = [] + + # Process positional arguments + for attrname, argval in zip(cls._fields, args): + if attrname in kwargs: + raise TypeError('Got multiple values for attribute %r' % attrname) + + attrvals.append(cls._construct(attrname, argval)) + + # Process keyword arguments + for attrname in cls._fields[len(args):]: + if attrname in kwargs: + argval = kwargs.pop(attrname) + + elif attrname in cls.defaults: + argval = cls.defaults[attrname] + + else: + raise TypeError('No value for %r given and attribute has no default' % attrname) + + attrvals.append(cls._construct(attrname, argval)) + + if kwargs: + raise ValueError("Unknown keyword arguments: %s" % ' '.join(kwargs)) + + # Parent constructor + basic_args = [ + val for attr, val in zip(cls._fields, attrvals) + if attr not in cls.not_in_args + ] + obj = CodegenAST.__new__(cls, *basic_args) + + # Set attributes + for attr, arg in zip(cls._fields, attrvals): + setattr(obj, attr, arg) + + return obj + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + for attr in self._fields: + if getattr(self, attr) != getattr(other, attr): + return False + return True + + def _hashable_content(self): + return tuple([getattr(self, attr) for attr in self._fields]) + + def __hash__(self): + return super().__hash__() + + def _joiner(self, k, indent_level): + return (',\n' + ' '*indent_level) if k in self.indented_args else ', ' + + def _indented(self, printer, k, v, *args, **kwargs): + il = printer._context['indent_level'] + def _print(arg): + if isinstance(arg, Token): + return printer._print(arg, *args, joiner=self._joiner(k, il), **kwargs) + else: + return printer._print(arg, *args, **kwargs) + + if isinstance(v, Tuple): + joined = self._joiner(k, il).join([_print(arg) for arg in v.args]) + if k in self.indented_args: + return '(\n' + ' '*il + joined + ',\n' + ' '*(il - 4) + ')' + else: + return ('({0},)' if len(v.args) == 1 else '({0})').format(joined) + else: + return _print(v) + + def _sympyrepr(self, printer, *args, joiner=', ', **kwargs): + from sympy.printing.printer import printer_context + exclude = kwargs.get('exclude', ()) + values = [getattr(self, k) for k in self._fields] + indent_level = printer._context.get('indent_level', 0) + + arg_reprs = [] + + for i, (attr, value) in enumerate(zip(self._fields, values)): + if attr in exclude: + continue + + # Skip attributes which have the default value + if attr in self.defaults and value == self.defaults[attr]: + continue + + ilvl = indent_level + 4 if attr in self.indented_args else 0 + with printer_context(printer, indent_level=ilvl): + indented = self._indented(printer, attr, value, *args, **kwargs) + arg_reprs.append(('{1}' if i == 0 else '{0}={1}').format(attr, indented.lstrip())) + + return "{}({})".format(self.__class__.__name__, joiner.join(arg_reprs)) + + _sympystr = _sympyrepr + + def __repr__(self): # sympy.core.Basic.__repr__ uses sstr + from sympy.printing import srepr + return srepr(self) + + def kwargs(self, exclude=(), apply=None): + """ Get instance's attributes as dict of keyword arguments. + + Parameters + ========== + + exclude : collection of str + Collection of keywords to exclude. + + apply : callable, optional + Function to apply to all values. + """ + kwargs = {k: getattr(self, k) for k in self._fields if k not in exclude} + if apply is not None: + return {k: apply(v) for k, v in kwargs.items()} + else: + return kwargs + +class BreakToken(Token): + """ Represents 'break' in C/Python ('exit' in Fortran). + + Use the premade instance ``break_`` or instantiate manually. + + Examples + ======== + + >>> from sympy import ccode, fcode + >>> from sympy.codegen.ast import break_ + >>> ccode(break_) + 'break' + >>> fcode(break_, source_format='free') + 'exit' + """ + +break_ = BreakToken() + + +class ContinueToken(Token): + """ Represents 'continue' in C/Python ('cycle' in Fortran) + + Use the premade instance ``continue_`` or instantiate manually. + + Examples + ======== + + >>> from sympy import ccode, fcode + >>> from sympy.codegen.ast import continue_ + >>> ccode(continue_) + 'continue' + >>> fcode(continue_, source_format='free') + 'cycle' + """ + +continue_ = ContinueToken() + +class NoneToken(Token): + """ The AST equivalence of Python's NoneType + + The corresponding instance of Python's ``None`` is ``none``. + + Examples + ======== + + >>> from sympy.codegen.ast import none, Variable + >>> from sympy import pycode + >>> print(pycode(Variable('x').as_Declaration(value=none))) + x = None + + """ + def __eq__(self, other): + return other is None or isinstance(other, NoneToken) + + def _hashable_content(self): + return () + + def __hash__(self): + return super().__hash__() + + +none = NoneToken() + + +class AssignmentBase(CodegenAST): + """ Abstract base class for Assignment and AugmentedAssignment. + + Attributes: + =========== + + op : str + Symbol for assignment operator, e.g. "=", "+=", etc. + """ + + def __new__(cls, lhs, rhs): + lhs = _sympify(lhs) + rhs = _sympify(rhs) + + cls._check_args(lhs, rhs) + + return super().__new__(cls, lhs, rhs) + + @property + def lhs(self): + return self.args[0] + + @property + def rhs(self): + return self.args[1] + + @classmethod + def _check_args(cls, lhs, rhs): + """ Check arguments to __new__ and raise exception if any problems found. + + Derived classes may wish to override this. + """ + from sympy.matrices.expressions.matexpr import ( + MatrixElement, MatrixSymbol) + from sympy.tensor.indexed import Indexed + from sympy.tensor.array.expressions import ArrayElement + + # Tuple of things that can be on the lhs of an assignment + assignable = (Symbol, MatrixSymbol, MatrixElement, Indexed, Element, Variable, + ArrayElement) + if not isinstance(lhs, assignable): + raise TypeError("Cannot assign to lhs of type %s." % type(lhs)) + + # Indexed types implement shape, but don't define it until later. This + # causes issues in assignment validation. For now, matrices are defined + # as anything with a shape that is not an Indexed + lhs_is_mat = hasattr(lhs, 'shape') and not isinstance(lhs, Indexed) + rhs_is_mat = hasattr(rhs, 'shape') and not isinstance(rhs, Indexed) + + # If lhs and rhs have same structure, then this assignment is ok + if lhs_is_mat: + if not rhs_is_mat: + raise ValueError("Cannot assign a scalar to a matrix.") + elif lhs.shape != rhs.shape: + raise ValueError("Dimensions of lhs and rhs do not align.") + elif rhs_is_mat and not lhs_is_mat: + raise ValueError("Cannot assign a matrix to a scalar.") + + +class Assignment(AssignmentBase): + """ + Represents variable assignment for code generation. + + Parameters + ========== + + lhs : Expr + SymPy object representing the lhs of the expression. These should be + singular objects, such as one would use in writing code. Notable types + include Symbol, MatrixSymbol, MatrixElement, and Indexed. Types that + subclass these types are also supported. + + rhs : Expr + SymPy object representing the rhs of the expression. This can be any + type, provided its shape corresponds to that of the lhs. For example, + a Matrix type can be assigned to MatrixSymbol, but not to Symbol, as + the dimensions will not align. + + Examples + ======== + + >>> from sympy import symbols, MatrixSymbol, Matrix + >>> from sympy.codegen.ast import Assignment + >>> x, y, z = symbols('x, y, z') + >>> Assignment(x, y) + Assignment(x, y) + >>> Assignment(x, 0) + Assignment(x, 0) + >>> A = MatrixSymbol('A', 1, 3) + >>> mat = Matrix([x, y, z]).T + >>> Assignment(A, mat) + Assignment(A, Matrix([[x, y, z]])) + >>> Assignment(A[0, 1], x) + Assignment(A[0, 1], x) + """ + + op = ':=' + + +class AugmentedAssignment(AssignmentBase): + """ + Base class for augmented assignments. + + Attributes: + =========== + + binop : str + Symbol for binary operation being applied in the assignment, such as "+", + "*", etc. + """ + binop: str | None + + @property + def op(self): + return self.binop + '=' + + +class AddAugmentedAssignment(AugmentedAssignment): + binop = '+' + + +class SubAugmentedAssignment(AugmentedAssignment): + binop = '-' + + +class MulAugmentedAssignment(AugmentedAssignment): + binop = '*' + + +class DivAugmentedAssignment(AugmentedAssignment): + binop = '/' + + +class ModAugmentedAssignment(AugmentedAssignment): + binop = '%' + + +# Mapping from binary op strings to AugmentedAssignment subclasses +augassign_classes = { + cls.binop: cls for cls in [ + AddAugmentedAssignment, SubAugmentedAssignment, MulAugmentedAssignment, + DivAugmentedAssignment, ModAugmentedAssignment + ] +} + + +def aug_assign(lhs, op, rhs): + """ + Create 'lhs op= rhs'. + + Explanation + =========== + + Represents augmented variable assignment for code generation. This is a + convenience function. You can also use the AugmentedAssignment classes + directly, like AddAugmentedAssignment(x, y). + + Parameters + ========== + + lhs : Expr + SymPy object representing the lhs of the expression. These should be + singular objects, such as one would use in writing code. Notable types + include Symbol, MatrixSymbol, MatrixElement, and Indexed. Types that + subclass these types are also supported. + + op : str + Operator (+, -, /, \\*, %). + + rhs : Expr + SymPy object representing the rhs of the expression. This can be any + type, provided its shape corresponds to that of the lhs. For example, + a Matrix type can be assigned to MatrixSymbol, but not to Symbol, as + the dimensions will not align. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.codegen.ast import aug_assign + >>> x, y = symbols('x, y') + >>> aug_assign(x, '+', y) + AddAugmentedAssignment(x, y) + """ + if op not in augassign_classes: + raise ValueError("Unrecognized operator %s" % op) + return augassign_classes[op](lhs, rhs) + + +class CodeBlock(CodegenAST): + """ + Represents a block of code. + + Explanation + =========== + + For now only assignments are supported. This restriction will be lifted in + the future. + + Useful attributes on this object are: + + ``left_hand_sides``: + Tuple of left-hand sides of assignments, in order. + ``left_hand_sides``: + Tuple of right-hand sides of assignments, in order. + ``free_symbols``: Free symbols of the expressions in the right-hand sides + which do not appear in the left-hand side of an assignment. + + Useful methods on this object are: + + ``topological_sort``: + Class method. Return a CodeBlock with assignments + sorted so that variables are assigned before they + are used. + ``cse``: + Return a new CodeBlock with common subexpressions eliminated and + pulled out as assignments. + + Examples + ======== + + >>> from sympy import symbols, ccode + >>> from sympy.codegen.ast import CodeBlock, Assignment + >>> x, y = symbols('x y') + >>> c = CodeBlock(Assignment(x, 1), Assignment(y, x + 1)) + >>> print(ccode(c)) + x = 1; + y = x + 1; + + """ + def __new__(cls, *args): + left_hand_sides = [] + right_hand_sides = [] + for i in args: + if isinstance(i, Assignment): + lhs, rhs = i.args + left_hand_sides.append(lhs) + right_hand_sides.append(rhs) + + obj = CodegenAST.__new__(cls, *args) + + obj.left_hand_sides = Tuple(*left_hand_sides) + obj.right_hand_sides = Tuple(*right_hand_sides) + return obj + + def __iter__(self): + return iter(self.args) + + def _sympyrepr(self, printer, *args, **kwargs): + il = printer._context.get('indent_level', 0) + joiner = ',\n' + ' '*il + joined = joiner.join(map(printer._print, self.args)) + return ('{}(\n'.format(' '*(il-4) + self.__class__.__name__,) + + ' '*il + joined + '\n' + ' '*(il - 4) + ')') + + _sympystr = _sympyrepr + + @property + def free_symbols(self): + return super().free_symbols - set(self.left_hand_sides) + + @classmethod + def topological_sort(cls, assignments): + """ + Return a CodeBlock with topologically sorted assignments so that + variables are assigned before they are used. + + Examples + ======== + + The existing order of assignments is preserved as much as possible. + + This function assumes that variables are assigned to only once. + + This is a class constructor so that the default constructor for + CodeBlock can error when variables are used before they are assigned. + + >>> from sympy import symbols + >>> from sympy.codegen.ast import CodeBlock, Assignment + >>> x, y, z = symbols('x y z') + + >>> assignments = [ + ... Assignment(x, y + z), + ... Assignment(y, z + 1), + ... Assignment(z, 2), + ... ] + >>> CodeBlock.topological_sort(assignments) + CodeBlock( + Assignment(z, 2), + Assignment(y, z + 1), + Assignment(x, y + z) + ) + + """ + + if not all(isinstance(i, Assignment) for i in assignments): + # Will support more things later + raise NotImplementedError("CodeBlock.topological_sort only supports Assignments") + + if any(isinstance(i, AugmentedAssignment) for i in assignments): + raise NotImplementedError("CodeBlock.topological_sort does not yet work with AugmentedAssignments") + + # Create a graph where the nodes are assignments and there is a directed edge + # between nodes that use a variable and nodes that assign that + # variable, like + + # [(x := 1, y := x + 1), (x := 1, z := y + z), (y := x + 1, z := y + z)] + + # If we then topologically sort these nodes, they will be in + # assignment order, like + + # x := 1 + # y := x + 1 + # z := y + z + + # A = The nodes + # + # enumerate keeps nodes in the same order they are already in if + # possible. It will also allow us to handle duplicate assignments to + # the same variable when those are implemented. + A = list(enumerate(assignments)) + + # var_map = {variable: [nodes for which this variable is assigned to]} + # like {x: [(1, x := y + z), (4, x := 2 * w)], ...} + var_map = defaultdict(list) + for node in A: + i, a = node + var_map[a.lhs].append(node) + + # E = Edges in the graph + E = [] + for dst_node in A: + i, a = dst_node + for s in a.rhs.free_symbols: + for src_node in var_map[s]: + E.append((src_node, dst_node)) + + ordered_assignments = topological_sort([A, E]) + + # De-enumerate the result + return cls(*[a for i, a in ordered_assignments]) + + def cse(self, symbols=None, optimizations=None, postprocess=None, + order='canonical'): + """ + Return a new code block with common subexpressions eliminated. + + Explanation + =========== + + See the docstring of :func:`sympy.simplify.cse_main.cse` for more + information. + + Examples + ======== + + >>> from sympy import symbols, sin + >>> from sympy.codegen.ast import CodeBlock, Assignment + >>> x, y, z = symbols('x y z') + + >>> c = CodeBlock( + ... Assignment(x, 1), + ... Assignment(y, sin(x) + 1), + ... Assignment(z, sin(x) - 1), + ... ) + ... + >>> c.cse() + CodeBlock( + Assignment(x, 1), + Assignment(x0, sin(x)), + Assignment(y, x0 + 1), + Assignment(z, x0 - 1) + ) + + """ + from sympy.simplify.cse_main import cse + + # Check that the CodeBlock only contains assignments to unique variables + if not all(isinstance(i, Assignment) for i in self.args): + # Will support more things later + raise NotImplementedError("CodeBlock.cse only supports Assignments") + + if any(isinstance(i, AugmentedAssignment) for i in self.args): + raise NotImplementedError("CodeBlock.cse does not yet work with AugmentedAssignments") + + for i, lhs in enumerate(self.left_hand_sides): + if lhs in self.left_hand_sides[:i]: + raise NotImplementedError("Duplicate assignments to the same " + "variable are not yet supported (%s)" % lhs) + + # Ensure new symbols for subexpressions do not conflict with existing + existing_symbols = self.atoms(Symbol) + if symbols is None: + symbols = numbered_symbols() + symbols = filter_symbols(symbols, existing_symbols) + + replacements, reduced_exprs = cse(list(self.right_hand_sides), + symbols=symbols, optimizations=optimizations, postprocess=postprocess, + order=order) + + new_block = [Assignment(var, expr) for var, expr in + zip(self.left_hand_sides, reduced_exprs)] + new_assignments = [Assignment(var, expr) for var, expr in replacements] + return self.topological_sort(new_assignments + new_block) + + +class For(Token): + """Represents a 'for-loop' in the code. + + Expressions are of the form: + "for target in iter: + body..." + + Parameters + ========== + + target : symbol + iter : iterable + body : CodeBlock or iterable +! When passed an iterable it is used to instantiate a CodeBlock. + + Examples + ======== + + >>> from sympy import symbols, Range + >>> from sympy.codegen.ast import aug_assign, For + >>> x, i, j, k = symbols('x i j k') + >>> for_i = For(i, Range(10), [aug_assign(x, '+', i*j*k)]) + >>> for_i # doctest: -NORMALIZE_WHITESPACE + For(i, iterable=Range(0, 10, 1), body=CodeBlock( + AddAugmentedAssignment(x, i*j*k) + )) + >>> for_ji = For(j, Range(7), [for_i]) + >>> for_ji # doctest: -NORMALIZE_WHITESPACE + For(j, iterable=Range(0, 7, 1), body=CodeBlock( + For(i, iterable=Range(0, 10, 1), body=CodeBlock( + AddAugmentedAssignment(x, i*j*k) + )) + )) + >>> for_kji =For(k, Range(5), [for_ji]) + >>> for_kji # doctest: -NORMALIZE_WHITESPACE + For(k, iterable=Range(0, 5, 1), body=CodeBlock( + For(j, iterable=Range(0, 7, 1), body=CodeBlock( + For(i, iterable=Range(0, 10, 1), body=CodeBlock( + AddAugmentedAssignment(x, i*j*k) + )) + )) + )) + """ + __slots__ = _fields = ('target', 'iterable', 'body') + _construct_target = staticmethod(_sympify) + + @classmethod + def _construct_body(cls, itr): + if isinstance(itr, CodeBlock): + return itr + else: + return CodeBlock(*itr) + + @classmethod + def _construct_iterable(cls, itr): + if not iterable(itr): + raise TypeError("iterable must be an iterable") + if isinstance(itr, list): # _sympify errors on lists because they are mutable + itr = tuple(itr) + return _sympify(itr) + + +class String(Atom, Token): + """ SymPy object representing a string. + + Atomic object which is not an expression (as opposed to Symbol). + + Parameters + ========== + + text : str + + Examples + ======== + + >>> from sympy.codegen.ast import String + >>> f = String('foo') + >>> f + foo + >>> str(f) + 'foo' + >>> f.text + 'foo' + >>> print(repr(f)) + String('foo') + + """ + __slots__ = _fields = ('text',) + not_in_args = ['text'] + is_Atom = True + + @classmethod + def _construct_text(cls, text): + if not isinstance(text, str): + raise TypeError("Argument text is not a string type.") + return text + + def _sympystr(self, printer, *args, **kwargs): + return self.text + + def kwargs(self, exclude = (), apply = None): + return {} + + #to be removed when Atom is given a suitable func + @property + def func(self): + return lambda: self + + def _latex(self, printer): + from sympy.printing.latex import latex_escape + return r'\texttt{{"{}"}}'.format(latex_escape(self.text)) + +class QuotedString(String): + """ Represents a string which should be printed with quotes. """ + +class Comment(String): + """ Represents a comment. """ + +class Node(Token): + """ Subclass of Token, carrying the attribute 'attrs' (Tuple) + + Examples + ======== + + >>> from sympy.codegen.ast import Node, value_const, pointer_const + >>> n1 = Node([value_const]) + >>> n1.attr_params('value_const') # get the parameters of attribute (by name) + () + >>> from sympy.codegen.fnodes import dimension + >>> n2 = Node([value_const, dimension(5, 3)]) + >>> n2.attr_params(value_const) # get the parameters of attribute (by Attribute instance) + () + >>> n2.attr_params('dimension') # get the parameters of attribute (by name) + (5, 3) + >>> n2.attr_params(pointer_const) is None + True + + """ + + __slots__: tuple[str, ...] = ('attrs',) + _fields = __slots__ + + defaults: dict[str, Any] = {'attrs': Tuple()} + + _construct_attrs = staticmethod(_mk_Tuple) + + def attr_params(self, looking_for): + """ Returns the parameters of the Attribute with name ``looking_for`` in self.attrs """ + for attr in self.attrs: + if str(attr.name) == str(looking_for): + return attr.parameters + + +class Type(Token): + """ Represents a type. + + Explanation + =========== + + The naming is a super-set of NumPy naming. Type has a classmethod + ``from_expr`` which offer type deduction. It also has a method + ``cast_check`` which casts the argument to its type, possibly raising an + exception if rounding error is not within tolerances, or if the value is not + representable by the underlying data type (e.g. unsigned integers). + + Parameters + ========== + + name : str + Name of the type, e.g. ``object``, ``int16``, ``float16`` (where the latter two + would use the ``Type`` sub-classes ``IntType`` and ``FloatType`` respectively). + If a ``Type`` instance is given, the said instance is returned. + + Examples + ======== + + >>> from sympy.codegen.ast import Type + >>> t = Type.from_expr(42) + >>> t + integer + >>> print(repr(t)) + IntBaseType(String('integer')) + >>> from sympy.codegen.ast import uint8 + >>> uint8.cast_check(-1) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Minimum value for data type bigger than new value. + >>> from sympy.codegen.ast import float32 + >>> v6 = 0.123456 + >>> float32.cast_check(v6) + 0.123456 + >>> v10 = 12345.67894 + >>> float32.cast_check(v10) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Casting gives a significantly different value. + >>> boost_mp50 = Type('boost::multiprecision::cpp_dec_float_50') + >>> from sympy import cxxcode + >>> from sympy.codegen.ast import Declaration, Variable + >>> cxxcode(Declaration(Variable('x', type=boost_mp50))) + 'boost::multiprecision::cpp_dec_float_50 x' + + References + ========== + + .. [1] https://numpy.org/doc/stable/user/basics.types.html + + """ + __slots__: tuple[str, ...] = ('name',) + _fields = __slots__ + + _construct_name = String + + def _sympystr(self, printer, *args, **kwargs): + return str(self.name) + + @classmethod + def from_expr(cls, expr): + """ Deduces type from an expression or a ``Symbol``. + + Parameters + ========== + + expr : number or SymPy object + The type will be deduced from type or properties. + + Examples + ======== + + >>> from sympy.codegen.ast import Type, integer, complex_ + >>> Type.from_expr(2) == integer + True + >>> from sympy import Symbol + >>> Type.from_expr(Symbol('z', complex=True)) == complex_ + True + >>> Type.from_expr(sum) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Could not deduce type from expr. + + Raises + ====== + + ValueError when type deduction fails. + + """ + if isinstance(expr, (float, Float)): + return real + if isinstance(expr, (int, Integer)) or getattr(expr, 'is_integer', False): + return integer + if getattr(expr, 'is_real', False): + return real + if isinstance(expr, complex) or getattr(expr, 'is_complex', False): + return complex_ + if isinstance(expr, bool) or getattr(expr, 'is_Relational', False): + return bool_ + else: + raise ValueError("Could not deduce type from expr.") + + def _check(self, value): + pass + + def cast_check(self, value, rtol=None, atol=0, precision_targets=None): + """ Casts a value to the data type of the instance. + + Parameters + ========== + + value : number + rtol : floating point number + Relative tolerance. (will be deduced if not given). + atol : floating point number + Absolute tolerance (in addition to ``rtol``). + type_aliases : dict + Maps substitutions for Type, e.g. {integer: int64, real: float32} + + Examples + ======== + + >>> from sympy.codegen.ast import integer, float32, int8 + >>> integer.cast_check(3.0) == 3 + True + >>> float32.cast_check(1e-40) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Minimum value for data type bigger than new value. + >>> int8.cast_check(256) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Maximum value for data type smaller than new value. + >>> v10 = 12345.67894 + >>> float32.cast_check(v10) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Casting gives a significantly different value. + >>> from sympy.codegen.ast import float64 + >>> float64.cast_check(v10) + 12345.67894 + >>> from sympy import Float + >>> v18 = Float('0.123456789012345646') + >>> float64.cast_check(v18) + Traceback (most recent call last): + ... + ValueError: Casting gives a significantly different value. + >>> from sympy.codegen.ast import float80 + >>> float80.cast_check(v18) + 0.123456789012345649 + + """ + val = sympify(value) + + ten = Integer(10) + exp10 = getattr(self, 'decimal_dig', None) + + if rtol is None: + rtol = 1e-15 if exp10 is None else 2.0*ten**(-exp10) + + def tol(num): + return atol + rtol*abs(num) + + new_val = self.cast_nocheck(value) + self._check(new_val) + + delta = new_val - val + if abs(delta) > tol(val): # rounding, e.g. int(3.5) != 3.5 + raise ValueError("Casting gives a significantly different value.") + + return new_val + + def _latex(self, printer): + from sympy.printing.latex import latex_escape + type_name = latex_escape(self.__class__.__name__) + name = latex_escape(self.name.text) + return r"\text{{{}}}\left(\texttt{{{}}}\right)".format(type_name, name) + + +class IntBaseType(Type): + """ Integer base type, contains no size information. """ + __slots__ = () + cast_nocheck = lambda self, i: Integer(int(i)) + + +class _SizedIntType(IntBaseType): + __slots__ = ('nbits',) + _fields = Type._fields + __slots__ + + _construct_nbits = Integer + + def _check(self, value): + if value < self.min: + raise ValueError("Value is too small: %d < %d" % (value, self.min)) + if value > self.max: + raise ValueError("Value is too big: %d > %d" % (value, self.max)) + + +class SignedIntType(_SizedIntType): + """ Represents a signed integer type. """ + __slots__ = () + @property + def min(self): + return -2**(self.nbits-1) + + @property + def max(self): + return 2**(self.nbits-1) - 1 + + +class UnsignedIntType(_SizedIntType): + """ Represents an unsigned integer type. """ + __slots__ = () + @property + def min(self): + return 0 + + @property + def max(self): + return 2**self.nbits - 1 + +two = Integer(2) + +class FloatBaseType(Type): + """ Represents a floating point number type. """ + __slots__ = () + cast_nocheck = Float + +class FloatType(FloatBaseType): + """ Represents a floating point type with fixed bit width. + + Base 2 & one sign bit is assumed. + + Parameters + ========== + + name : str + Name of the type. + nbits : integer + Number of bits used (storage). + nmant : integer + Number of bits used to represent the mantissa. + nexp : integer + Number of bits used to represent the mantissa. + + Examples + ======== + + >>> from sympy import S + >>> from sympy.codegen.ast import FloatType + >>> half_precision = FloatType('f16', nbits=16, nmant=10, nexp=5) + >>> half_precision.max + 65504 + >>> half_precision.tiny == S(2)**-14 + True + >>> half_precision.eps == S(2)**-10 + True + >>> half_precision.dig == 3 + True + >>> half_precision.decimal_dig == 5 + True + >>> half_precision.cast_check(1.0) + 1.0 + >>> half_precision.cast_check(1e5) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Maximum value for data type smaller than new value. + """ + + __slots__ = ('nbits', 'nmant', 'nexp',) + _fields = Type._fields + __slots__ + + _construct_nbits = _construct_nmant = _construct_nexp = Integer + + + @property + def max_exponent(self): + """ The largest positive number n, such that 2**(n - 1) is a representable finite value. """ + # cf. C++'s ``std::numeric_limits::max_exponent`` + return two**(self.nexp - 1) + + @property + def min_exponent(self): + """ The lowest negative number n, such that 2**(n - 1) is a valid normalized number. """ + # cf. C++'s ``std::numeric_limits::min_exponent`` + return 3 - self.max_exponent + + @property + def max(self): + """ Maximum value representable. """ + return (1 - two**-(self.nmant+1))*two**self.max_exponent + + @property + def tiny(self): + """ The minimum positive normalized value. """ + # See C macros: FLT_MIN, DBL_MIN, LDBL_MIN + # or C++'s ``std::numeric_limits::min`` + # or numpy.finfo(dtype).tiny + return two**(self.min_exponent - 1) + + + @property + def eps(self): + """ Difference between 1.0 and the next representable value. """ + return two**(-self.nmant) + + @property + def dig(self): + """ Number of decimal digits that are guaranteed to be preserved in text. + + When converting text -> float -> text, you are guaranteed that at least ``dig`` + number of digits are preserved with respect to rounding or overflow. + """ + from sympy.functions import floor, log + return floor(self.nmant * log(2)/log(10)) + + @property + def decimal_dig(self): + """ Number of digits needed to store & load without loss. + + Explanation + =========== + + Number of decimal digits needed to guarantee that two consecutive conversions + (float -> text -> float) to be idempotent. This is useful when one do not want + to loose precision due to rounding errors when storing a floating point value + as text. + """ + from sympy.functions import ceiling, log + return ceiling((self.nmant + 1) * log(2)/log(10) + 1) + + def cast_nocheck(self, value): + """ Casts without checking if out of bounds or subnormal. """ + if value == oo: # float(oo) or oo + return float(oo) + elif value == -oo: # float(-oo) or -oo + return float(-oo) + return Float(str(sympify(value).evalf(self.decimal_dig)), self.decimal_dig) + + def _check(self, value): + if value < -self.max: + raise ValueError("Value is too small: %d < %d" % (value, -self.max)) + if value > self.max: + raise ValueError("Value is too big: %d > %d" % (value, self.max)) + if abs(value) < self.tiny: + raise ValueError("Smallest (absolute) value for data type bigger than new value.") + +class ComplexBaseType(FloatBaseType): + + __slots__ = () + + def cast_nocheck(self, value): + """ Casts without checking if out of bounds or subnormal. """ + from sympy.functions import re, im + return ( + super().cast_nocheck(re(value)) + + super().cast_nocheck(im(value))*1j + ) + + def _check(self, value): + from sympy.functions import re, im + super()._check(re(value)) + super()._check(im(value)) + + +class ComplexType(ComplexBaseType, FloatType): + """ Represents a complex floating point number. """ + __slots__ = () + + +# NumPy types: +intc = IntBaseType('intc') +intp = IntBaseType('intp') +int8 = SignedIntType('int8', 8) +int16 = SignedIntType('int16', 16) +int32 = SignedIntType('int32', 32) +int64 = SignedIntType('int64', 64) +uint8 = UnsignedIntType('uint8', 8) +uint16 = UnsignedIntType('uint16', 16) +uint32 = UnsignedIntType('uint32', 32) +uint64 = UnsignedIntType('uint64', 64) +float16 = FloatType('float16', 16, nexp=5, nmant=10) # IEEE 754 binary16, Half precision +float32 = FloatType('float32', 32, nexp=8, nmant=23) # IEEE 754 binary32, Single precision +float64 = FloatType('float64', 64, nexp=11, nmant=52) # IEEE 754 binary64, Double precision +float80 = FloatType('float80', 80, nexp=15, nmant=63) # x86 extended precision (1 integer part bit), "long double" +float128 = FloatType('float128', 128, nexp=15, nmant=112) # IEEE 754 binary128, Quadruple precision +float256 = FloatType('float256', 256, nexp=19, nmant=236) # IEEE 754 binary256, Octuple precision + +complex64 = ComplexType('complex64', nbits=64, **float32.kwargs(exclude=('name', 'nbits'))) +complex128 = ComplexType('complex128', nbits=128, **float64.kwargs(exclude=('name', 'nbits'))) + +# Generic types (precision may be chosen by code printers): +untyped = Type('untyped') +real = FloatBaseType('real') +integer = IntBaseType('integer') +complex_ = ComplexBaseType('complex') +bool_ = Type('bool') + + +class Attribute(Token): + """ Attribute (possibly parametrized) + + For use with :class:`sympy.codegen.ast.Node` (which takes instances of + ``Attribute`` as ``attrs``). + + Parameters + ========== + + name : str + parameters : Tuple + + Examples + ======== + + >>> from sympy.codegen.ast import Attribute + >>> volatile = Attribute('volatile') + >>> volatile + volatile + >>> print(repr(volatile)) + Attribute(String('volatile')) + >>> a = Attribute('foo', [1, 2, 3]) + >>> a + foo(1, 2, 3) + >>> a.parameters == (1, 2, 3) + True + """ + __slots__ = _fields = ('name', 'parameters') + defaults = {'parameters': Tuple()} + + _construct_name = String + _construct_parameters = staticmethod(_mk_Tuple) + + def _sympystr(self, printer, *args, **kwargs): + result = str(self.name) + if self.parameters: + result += '(%s)' % ', '.join((printer._print( + arg, *args, **kwargs) for arg in self.parameters)) + return result + +value_const = Attribute('value_const') +pointer_const = Attribute('pointer_const') + + +class Variable(Node): + """ Represents a variable. + + Parameters + ========== + + symbol : Symbol + type : Type (optional) + Type of the variable. + attrs : iterable of Attribute instances + Will be stored as a Tuple. + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.codegen.ast import Variable, float32, integer + >>> x = Symbol('x') + >>> v = Variable(x, type=float32) + >>> v.attrs + () + >>> v == Variable('x') + False + >>> v == Variable('x', type=float32) + True + >>> v + Variable(x, type=float32) + + One may also construct a ``Variable`` instance with the type deduced from + assumptions about the symbol using the ``deduced`` classmethod: + + >>> i = Symbol('i', integer=True) + >>> v = Variable.deduced(i) + >>> v.type == integer + True + >>> v == Variable('i') + False + >>> from sympy.codegen.ast import value_const + >>> value_const in v.attrs + False + >>> w = Variable('w', attrs=[value_const]) + >>> w + Variable(w, attrs=(value_const,)) + >>> value_const in w.attrs + True + >>> w.as_Declaration(value=42) + Declaration(Variable(w, value=42, attrs=(value_const,))) + + """ + + __slots__ = ('symbol', 'type', 'value') + _fields = __slots__ + Node._fields + + defaults = Node.defaults.copy() + defaults.update({'type': untyped, 'value': none}) + + _construct_symbol = staticmethod(sympify) + _construct_value = staticmethod(sympify) + + @classmethod + def deduced(cls, symbol, value=None, attrs=Tuple(), cast_check=True): + """ Alt. constructor with type deduction from ``Type.from_expr``. + + Deduces type primarily from ``symbol``, secondarily from ``value``. + + Parameters + ========== + + symbol : Symbol + value : expr + (optional) value of the variable. + attrs : iterable of Attribute instances + cast_check : bool + Whether to apply ``Type.cast_check`` on ``value``. + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.codegen.ast import Variable, complex_ + >>> n = Symbol('n', integer=True) + >>> str(Variable.deduced(n).type) + 'integer' + >>> x = Symbol('x', real=True) + >>> v = Variable.deduced(x) + >>> v.type + real + >>> z = Symbol('z', complex=True) + >>> Variable.deduced(z).type == complex_ + True + + """ + if isinstance(symbol, Variable): + return symbol + + try: + type_ = Type.from_expr(symbol) + except ValueError: + type_ = Type.from_expr(value) + + if value is not None and cast_check: + value = type_.cast_check(value) + return cls(symbol, type=type_, value=value, attrs=attrs) + + def as_Declaration(self, **kwargs): + """ Convenience method for creating a Declaration instance. + + Explanation + =========== + + If the variable of the Declaration need to wrap a modified + variable keyword arguments may be passed (overriding e.g. + the ``value`` of the Variable instance). + + Examples + ======== + + >>> from sympy.codegen.ast import Variable, NoneToken + >>> x = Variable('x') + >>> decl1 = x.as_Declaration() + >>> # value is special NoneToken() which must be tested with == operator + >>> decl1.variable.value is None # won't work + False + >>> decl1.variable.value == None # not PEP-8 compliant + True + >>> decl1.variable.value == NoneToken() # OK + True + >>> decl2 = x.as_Declaration(value=42.0) + >>> decl2.variable.value == 42.0 + True + + """ + kw = self.kwargs() + kw.update(kwargs) + return Declaration(self.func(**kw)) + + def _relation(self, rhs, op): + try: + rhs = _sympify(rhs) + except SympifyError: + raise TypeError("Invalid comparison %s < %s" % (self, rhs)) + return op(self, rhs, evaluate=False) + + __lt__ = lambda self, other: self._relation(other, Lt) + __le__ = lambda self, other: self._relation(other, Le) + __ge__ = lambda self, other: self._relation(other, Ge) + __gt__ = lambda self, other: self._relation(other, Gt) + +class Pointer(Variable): + """ Represents a pointer. See ``Variable``. + + Examples + ======== + + Can create instances of ``Element``: + + >>> from sympy import Symbol + >>> from sympy.codegen.ast import Pointer + >>> i = Symbol('i', integer=True) + >>> p = Pointer('x') + >>> p[i+1] + Element(x, indices=(i + 1,)) + + """ + __slots__ = () + + def __getitem__(self, key): + try: + return Element(self.symbol, key) + except TypeError: + return Element(self.symbol, (key,)) + + +class Element(Token): + """ Element in (a possibly N-dimensional) array. + + Examples + ======== + + >>> from sympy.codegen.ast import Element + >>> elem = Element('x', 'ijk') + >>> elem.symbol.name == 'x' + True + >>> elem.indices + (i, j, k) + >>> from sympy import ccode + >>> ccode(elem) + 'x[i][j][k]' + >>> ccode(Element('x', 'ijk', strides='lmn', offset='o')) + 'x[i*l + j*m + k*n + o]' + + """ + __slots__ = _fields = ('symbol', 'indices', 'strides', 'offset') + defaults = {'strides': none, 'offset': none} + _construct_symbol = staticmethod(sympify) + _construct_indices = staticmethod(lambda arg: Tuple(*arg)) + _construct_strides = staticmethod(lambda arg: Tuple(*arg)) + _construct_offset = staticmethod(sympify) + + +class Declaration(Token): + """ Represents a variable declaration + + Parameters + ========== + + variable : Variable + + Examples + ======== + + >>> from sympy.codegen.ast import Declaration, NoneToken, untyped + >>> z = Declaration('z') + >>> z.variable.type == untyped + True + >>> # value is special NoneToken() which must be tested with == operator + >>> z.variable.value is None # won't work + False + >>> z.variable.value == None # not PEP-8 compliant + True + >>> z.variable.value == NoneToken() # OK + True + """ + __slots__ = _fields = ('variable',) + _construct_variable = Variable + + +class While(Token): + """ Represents a 'for-loop' in the code. + + Expressions are of the form: + "while condition: + body..." + + Parameters + ========== + + condition : expression convertible to Boolean + body : CodeBlock or iterable + When passed an iterable it is used to instantiate a CodeBlock. + + Examples + ======== + + >>> from sympy import symbols, Gt, Abs + >>> from sympy.codegen import aug_assign, Assignment, While + >>> x, dx = symbols('x dx') + >>> expr = 1 - x**2 + >>> whl = While(Gt(Abs(dx), 1e-9), [ + ... Assignment(dx, -expr/expr.diff(x)), + ... aug_assign(x, '+', dx) + ... ]) + + """ + __slots__ = _fields = ('condition', 'body') + _construct_condition = staticmethod(lambda cond: _sympify(cond)) + + @classmethod + def _construct_body(cls, itr): + if isinstance(itr, CodeBlock): + return itr + else: + return CodeBlock(*itr) + + +class Scope(Token): + """ Represents a scope in the code. + + Parameters + ========== + + body : CodeBlock or iterable + When passed an iterable it is used to instantiate a CodeBlock. + + """ + __slots__ = _fields = ('body',) + + @classmethod + def _construct_body(cls, itr): + if isinstance(itr, CodeBlock): + return itr + else: + return CodeBlock(*itr) + + +class Stream(Token): + """ Represents a stream. + + There are two predefined Stream instances ``stdout`` & ``stderr``. + + Parameters + ========== + + name : str + + Examples + ======== + + >>> from sympy import pycode, Symbol + >>> from sympy.codegen.ast import Print, stderr, QuotedString + >>> print(pycode(Print(['x'], file=stderr))) + print(x, file=sys.stderr) + >>> x = Symbol('x') + >>> print(pycode(Print([QuotedString('x')], file=stderr))) # print literally "x" + print("x", file=sys.stderr) + + """ + __slots__ = _fields = ('name',) + _construct_name = String + +stdout = Stream('stdout') +stderr = Stream('stderr') + + +class Print(Token): + r""" Represents print command in the code. + + Parameters + ========== + + formatstring : str + *args : Basic instances (or convertible to such through sympify) + + Examples + ======== + + >>> from sympy.codegen.ast import Print + >>> from sympy import pycode + >>> print(pycode(Print('x y'.split(), "coordinate: %12.5g %12.5g\\n"))) + print("coordinate: %12.5g %12.5g\n" % (x, y), end="") + + """ + + __slots__ = _fields = ('print_args', 'format_string', 'file') + defaults = {'format_string': none, 'file': none} + + _construct_print_args = staticmethod(_mk_Tuple) + _construct_format_string = QuotedString + _construct_file = Stream + + +class FunctionPrototype(Node): + """ Represents a function prototype + + Allows the user to generate forward declaration in e.g. C/C++. + + Parameters + ========== + + return_type : Type + name : str + parameters: iterable of Variable instances + attrs : iterable of Attribute instances + + Examples + ======== + + >>> from sympy import ccode, symbols + >>> from sympy.codegen.ast import real, FunctionPrototype + >>> x, y = symbols('x y', real=True) + >>> fp = FunctionPrototype(real, 'foo', [x, y]) + >>> ccode(fp) + 'double foo(double x, double y)' + + """ + + __slots__ = ('return_type', 'name', 'parameters') + _fields: tuple[str, ...] = __slots__ + Node._fields + + _construct_return_type = Type + _construct_name = String + + @staticmethod + def _construct_parameters(args): + def _var(arg): + if isinstance(arg, Declaration): + return arg.variable + elif isinstance(arg, Variable): + return arg + else: + return Variable.deduced(arg) + return Tuple(*map(_var, args)) + + @classmethod + def from_FunctionDefinition(cls, func_def): + if not isinstance(func_def, FunctionDefinition): + raise TypeError("func_def is not an instance of FunctionDefinition") + return cls(**func_def.kwargs(exclude=('body',))) + + +class FunctionDefinition(FunctionPrototype): + """ Represents a function definition in the code. + + Parameters + ========== + + return_type : Type + name : str + parameters: iterable of Variable instances + body : CodeBlock or iterable + attrs : iterable of Attribute instances + + Examples + ======== + + >>> from sympy import ccode, symbols + >>> from sympy.codegen.ast import real, FunctionPrototype + >>> x, y = symbols('x y', real=True) + >>> fp = FunctionPrototype(real, 'foo', [x, y]) + >>> ccode(fp) + 'double foo(double x, double y)' + >>> from sympy.codegen.ast import FunctionDefinition, Return + >>> body = [Return(x*y)] + >>> fd = FunctionDefinition.from_FunctionPrototype(fp, body) + >>> print(ccode(fd)) + double foo(double x, double y){ + return x*y; + } + """ + + __slots__ = ('body', ) + _fields = FunctionPrototype._fields[:-1] + __slots__ + Node._fields + + @classmethod + def _construct_body(cls, itr): + if isinstance(itr, CodeBlock): + return itr + else: + return CodeBlock(*itr) + + @classmethod + def from_FunctionPrototype(cls, func_proto, body): + if not isinstance(func_proto, FunctionPrototype): + raise TypeError("func_proto is not an instance of FunctionPrototype") + return cls(body=body, **func_proto.kwargs()) + + +class Return(Token): + """ Represents a return command in the code. + + Parameters + ========== + + return : Basic + + Examples + ======== + + >>> from sympy.codegen.ast import Return + >>> from sympy.printing.pycode import pycode + >>> from sympy import Symbol + >>> x = Symbol('x') + >>> print(pycode(Return(x))) + return x + + """ + __slots__ = _fields = ('return',) + _construct_return=staticmethod(_sympify) + + +class FunctionCall(Token, Expr): + """ Represents a call to a function in the code. + + Parameters + ========== + + name : str + function_args : Tuple + + Examples + ======== + + >>> from sympy.codegen.ast import FunctionCall + >>> from sympy import pycode + >>> fcall = FunctionCall('foo', 'bar baz'.split()) + >>> print(pycode(fcall)) + foo(bar, baz) + + """ + __slots__ = _fields = ('name', 'function_args') + + _construct_name = String + _construct_function_args = staticmethod(lambda args: Tuple(*args)) + + +class Raise(Token): + """ Prints as 'raise ...' in Python, 'throw ...' in C++""" + __slots__ = _fields = ('exception',) + + +class RuntimeError_(Token): + """ Represents 'std::runtime_error' in C++ and 'RuntimeError' in Python. + + Note that the latter is uncommon, and you might want to use e.g. ValueError. + """ + __slots__ = _fields = ('message',) + _construct_message = String diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/cfunctions.py b/venv/lib/python3.11/site-packages/sympy/codegen/cfunctions.py new file mode 100644 index 0000000000000000000000000000000000000000..7b79291f128aef1cb83b327782840508e59a9cc8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/cfunctions.py @@ -0,0 +1,558 @@ +""" +This module contains SymPy functions mathcin corresponding to special math functions in the +C standard library (since C99, also available in C++11). + +The functions defined in this module allows the user to express functions such as ``expm1`` +as a SymPy function for symbolic manipulation. + +""" +from sympy.core.function import ArgumentIndexError, Function +from sympy.core.numbers import Rational +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.logic.boolalg import BooleanFunction, true, false + +def _expm1(x): + return exp(x) - S.One + + +class expm1(Function): + """ + Represents the exponential function minus one. + + Explanation + =========== + + The benefit of using ``expm1(x)`` over ``exp(x) - 1`` + is that the latter is prone to cancellation under finite precision + arithmetic when x is close to zero. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.codegen.cfunctions import expm1 + >>> '%.0e' % expm1(1e-99).evalf() + '1e-99' + >>> from math import exp + >>> exp(1e-99) - 1 + 0.0 + >>> expm1(x).diff(x) + exp(x) + + See Also + ======== + + log1p + """ + nargs = 1 + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex == 1: + return exp(*self.args) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_expand_func(self, **hints): + return _expm1(*self.args) + + def _eval_rewrite_as_exp(self, arg, **kwargs): + return exp(arg) - S.One + + _eval_rewrite_as_tractable = _eval_rewrite_as_exp + + @classmethod + def eval(cls, arg): + exp_arg = exp.eval(arg) + if exp_arg is not None: + return exp_arg - S.One + + def _eval_is_real(self): + return self.args[0].is_real + + def _eval_is_finite(self): + return self.args[0].is_finite + + +def _log1p(x): + return log(x + S.One) + + +class log1p(Function): + """ + Represents the natural logarithm of a number plus one. + + Explanation + =========== + + The benefit of using ``log1p(x)`` over ``log(x + 1)`` + is that the latter is prone to cancellation under finite precision + arithmetic when x is close to zero. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.codegen.cfunctions import log1p + >>> from sympy import expand_log + >>> '%.0e' % expand_log(log1p(1e-99)).evalf() + '1e-99' + >>> from math import log + >>> log(1 + 1e-99) + 0.0 + >>> log1p(x).diff(x) + 1/(x + 1) + + See Also + ======== + + expm1 + """ + nargs = 1 + + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex == 1: + return S.One/(self.args[0] + S.One) + else: + raise ArgumentIndexError(self, argindex) + + + def _eval_expand_func(self, **hints): + return _log1p(*self.args) + + def _eval_rewrite_as_log(self, arg, **kwargs): + return _log1p(arg) + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + @classmethod + def eval(cls, arg): + if arg.is_Rational: + return log(arg + S.One) + elif not arg.is_Float: # not safe to add 1 to Float + return log.eval(arg + S.One) + elif arg.is_number: + return log(Rational(arg) + S.One) + + def _eval_is_real(self): + return (self.args[0] + S.One).is_nonnegative + + def _eval_is_finite(self): + if (self.args[0] + S.One).is_zero: + return False + return self.args[0].is_finite + + def _eval_is_positive(self): + return self.args[0].is_positive + + def _eval_is_zero(self): + return self.args[0].is_zero + + def _eval_is_nonnegative(self): + return self.args[0].is_nonnegative + +_Two = S(2) + +def _exp2(x): + return Pow(_Two, x) + +class exp2(Function): + """ + Represents the exponential function with base two. + + Explanation + =========== + + The benefit of using ``exp2(x)`` over ``2**x`` + is that the latter is not as efficient under finite precision + arithmetic. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.codegen.cfunctions import exp2 + >>> exp2(2).evalf() == 4.0 + True + >>> exp2(x).diff(x) + log(2)*exp2(x) + + See Also + ======== + + log2 + """ + nargs = 1 + + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex == 1: + return self*log(_Two) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Pow(self, arg, **kwargs): + return _exp2(arg) + + _eval_rewrite_as_tractable = _eval_rewrite_as_Pow + + def _eval_expand_func(self, **hints): + return _exp2(*self.args) + + @classmethod + def eval(cls, arg): + if arg.is_number: + return _exp2(arg) + + +def _log2(x): + return log(x)/log(_Two) + + +class log2(Function): + """ + Represents the logarithm function with base two. + + Explanation + =========== + + The benefit of using ``log2(x)`` over ``log(x)/log(2)`` + is that the latter is not as efficient under finite precision + arithmetic. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.codegen.cfunctions import log2 + >>> log2(4).evalf() == 2.0 + True + >>> log2(x).diff(x) + 1/(x*log(2)) + + See Also + ======== + + exp2 + log10 + """ + nargs = 1 + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex == 1: + return S.One/(log(_Two)*self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + + @classmethod + def eval(cls, arg): + if arg.is_number: + result = log.eval(arg, base=_Two) + if result.is_Atom: + return result + elif arg.is_Pow and arg.base == _Two: + return arg.exp + + def _eval_evalf(self, *args, **kwargs): + return self.rewrite(log).evalf(*args, **kwargs) + + def _eval_expand_func(self, **hints): + return _log2(*self.args) + + def _eval_rewrite_as_log(self, arg, **kwargs): + return _log2(arg) + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + +def _fma(x, y, z): + return x*y + z + + +class fma(Function): + """ + Represents "fused multiply add". + + Explanation + =========== + + The benefit of using ``fma(x, y, z)`` over ``x*y + z`` + is that, under finite precision arithmetic, the former is + supported by special instructions on some CPUs. + + Examples + ======== + + >>> from sympy.abc import x, y, z + >>> from sympy.codegen.cfunctions import fma + >>> fma(x, y, z).diff(x) + y + + """ + nargs = 3 + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex in (1, 2): + return self.args[2 - argindex] + elif argindex == 3: + return S.One + else: + raise ArgumentIndexError(self, argindex) + + + def _eval_expand_func(self, **hints): + return _fma(*self.args) + + def _eval_rewrite_as_tractable(self, arg, limitvar=None, **kwargs): + return _fma(arg) + + +_Ten = S(10) + + +def _log10(x): + return log(x)/log(_Ten) + + +class log10(Function): + """ + Represents the logarithm function with base ten. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.codegen.cfunctions import log10 + >>> log10(100).evalf() == 2.0 + True + >>> log10(x).diff(x) + 1/(x*log(10)) + + See Also + ======== + + log2 + """ + nargs = 1 + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex == 1: + return S.One/(log(_Ten)*self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + + @classmethod + def eval(cls, arg): + if arg.is_number: + result = log.eval(arg, base=_Ten) + if result.is_Atom: + return result + elif arg.is_Pow and arg.base == _Ten: + return arg.exp + + def _eval_expand_func(self, **hints): + return _log10(*self.args) + + def _eval_rewrite_as_log(self, arg, **kwargs): + return _log10(arg) + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + +def _Sqrt(x): + return Pow(x, S.Half) + + +class Sqrt(Function): # 'sqrt' already defined in sympy.functions.elementary.miscellaneous + """ + Represents the square root function. + + Explanation + =========== + + The reason why one would use ``Sqrt(x)`` over ``sqrt(x)`` + is that the latter is internally represented as ``Pow(x, S.Half)`` which + may not be what one wants when doing code-generation. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.codegen.cfunctions import Sqrt + >>> Sqrt(x) + Sqrt(x) + >>> Sqrt(x).diff(x) + 1/(2*sqrt(x)) + + See Also + ======== + + Cbrt + """ + nargs = 1 + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex == 1: + return Pow(self.args[0], Rational(-1, 2))/_Two + else: + raise ArgumentIndexError(self, argindex) + + def _eval_expand_func(self, **hints): + return _Sqrt(*self.args) + + def _eval_rewrite_as_Pow(self, arg, **kwargs): + return _Sqrt(arg) + + _eval_rewrite_as_tractable = _eval_rewrite_as_Pow + + +def _Cbrt(x): + return Pow(x, Rational(1, 3)) + + +class Cbrt(Function): # 'cbrt' already defined in sympy.functions.elementary.miscellaneous + """ + Represents the cube root function. + + Explanation + =========== + + The reason why one would use ``Cbrt(x)`` over ``cbrt(x)`` + is that the latter is internally represented as ``Pow(x, Rational(1, 3))`` which + may not be what one wants when doing code-generation. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.codegen.cfunctions import Cbrt + >>> Cbrt(x) + Cbrt(x) + >>> Cbrt(x).diff(x) + 1/(3*x**(2/3)) + + See Also + ======== + + Sqrt + """ + nargs = 1 + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex == 1: + return Pow(self.args[0], Rational(-_Two/3))/3 + else: + raise ArgumentIndexError(self, argindex) + + + def _eval_expand_func(self, **hints): + return _Cbrt(*self.args) + + def _eval_rewrite_as_Pow(self, arg, **kwargs): + return _Cbrt(arg) + + _eval_rewrite_as_tractable = _eval_rewrite_as_Pow + + +def _hypot(x, y): + return sqrt(Pow(x, 2) + Pow(y, 2)) + + +class hypot(Function): + """ + Represents the hypotenuse function. + + Explanation + =========== + + The hypotenuse function is provided by e.g. the math library + in the C99 standard, hence one may want to represent the function + symbolically when doing code-generation. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy.codegen.cfunctions import hypot + >>> hypot(3, 4).evalf() == 5.0 + True + >>> hypot(x, y) + hypot(x, y) + >>> hypot(x, y).diff(x) + x/hypot(x, y) + + """ + nargs = 2 + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex in (1, 2): + return 2*self.args[argindex-1]/(_Two*self.func(*self.args)) + else: + raise ArgumentIndexError(self, argindex) + + + def _eval_expand_func(self, **hints): + return _hypot(*self.args) + + def _eval_rewrite_as_Pow(self, arg, **kwargs): + return _hypot(arg) + + _eval_rewrite_as_tractable = _eval_rewrite_as_Pow + + +class isnan(BooleanFunction): + nargs = 1 + + @classmethod + def eval(cls, arg): + if arg is S.NaN: + return true + elif arg.is_number: + return false + else: + return None + + +class isinf(BooleanFunction): + nargs = 1 + + @classmethod + def eval(cls, arg): + if arg.is_infinite: + return true + elif arg.is_finite: + return false + else: + return None diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/cnodes.py b/venv/lib/python3.11/site-packages/sympy/codegen/cnodes.py new file mode 100644 index 0000000000000000000000000000000000000000..dd2a324ee49cabcd42e99ca5d8e5379cc9262c4e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/cnodes.py @@ -0,0 +1,156 @@ +""" +AST nodes specific to the C family of languages +""" + +from sympy.codegen.ast import ( + Attribute, Declaration, Node, String, Token, Type, none, + FunctionCall, CodeBlock + ) +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.sympify import sympify + +void = Type('void') + +restrict = Attribute('restrict') # guarantees no pointer aliasing +volatile = Attribute('volatile') +static = Attribute('static') + + +def alignof(arg): + """ Generate of FunctionCall instance for calling 'alignof' """ + return FunctionCall('alignof', [String(arg) if isinstance(arg, str) else arg]) + + +def sizeof(arg): + """ Generate of FunctionCall instance for calling 'sizeof' + + Examples + ======== + + >>> from sympy.codegen.ast import real + >>> from sympy.codegen.cnodes import sizeof + >>> from sympy import ccode + >>> ccode(sizeof(real)) + 'sizeof(double)' + """ + return FunctionCall('sizeof', [String(arg) if isinstance(arg, str) else arg]) + + +class CommaOperator(Basic): + """ Represents the comma operator in C """ + def __new__(cls, *args): + return Basic.__new__(cls, *[sympify(arg) for arg in args]) + + +class Label(Node): + """ Label for use with e.g. goto statement. + + Examples + ======== + + >>> from sympy import ccode, Symbol + >>> from sympy.codegen.cnodes import Label, PreIncrement + >>> print(ccode(Label('foo'))) + foo: + >>> print(ccode(Label('bar', [PreIncrement(Symbol('a'))]))) + bar: + ++(a); + + """ + __slots__ = _fields = ('name', 'body') + defaults = {'body': none} + _construct_name = String + + @classmethod + def _construct_body(cls, itr): + if isinstance(itr, CodeBlock): + return itr + else: + return CodeBlock(*itr) + + +class goto(Token): + """ Represents goto in C """ + __slots__ = _fields = ('label',) + _construct_label = Label + + +class PreDecrement(Basic): + """ Represents the pre-decrement operator + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.codegen.cnodes import PreDecrement + >>> from sympy import ccode + >>> ccode(PreDecrement(x)) + '--(x)' + + """ + nargs = 1 + + +class PostDecrement(Basic): + """ Represents the post-decrement operator + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.codegen.cnodes import PostDecrement + >>> from sympy import ccode + >>> ccode(PostDecrement(x)) + '(x)--' + + """ + nargs = 1 + + +class PreIncrement(Basic): + """ Represents the pre-increment operator + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.codegen.cnodes import PreIncrement + >>> from sympy import ccode + >>> ccode(PreIncrement(x)) + '++(x)' + + """ + nargs = 1 + + +class PostIncrement(Basic): + """ Represents the post-increment operator + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.codegen.cnodes import PostIncrement + >>> from sympy import ccode + >>> ccode(PostIncrement(x)) + '(x)++' + + """ + nargs = 1 + + +class struct(Node): + """ Represents a struct in C """ + __slots__ = _fields = ('name', 'declarations') + defaults = {'name': none} + _construct_name = String + + @classmethod + def _construct_declarations(cls, args): + return Tuple(*[Declaration(arg) for arg in args]) + + +class union(struct): + """ Represents a union in C """ + __slots__ = () diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/cutils.py b/venv/lib/python3.11/site-packages/sympy/codegen/cutils.py new file mode 100644 index 0000000000000000000000000000000000000000..2182ac1f3455da490a0bb57f8d6731fe8a29a232 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/cutils.py @@ -0,0 +1,8 @@ +from sympy.printing.c import C99CodePrinter + +def render_as_source_file(content, Printer=C99CodePrinter, settings=None): + """ Renders a C source file (with required #include statements) """ + printer = Printer(settings or {}) + code_str = printer.doprint(content) + includes = '\n'.join(['#include <%s>' % h for h in printer.headers]) + return includes + '\n\n' + code_str diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/cxxnodes.py b/venv/lib/python3.11/site-packages/sympy/codegen/cxxnodes.py new file mode 100644 index 0000000000000000000000000000000000000000..7f7aafd01ab2de99ad0f668275889863fc73f5aa --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/cxxnodes.py @@ -0,0 +1,14 @@ +""" +AST nodes specific to C++. +""" + +from sympy.codegen.ast import Attribute, String, Token, Type, none + +class using(Token): + """ Represents a 'using' statement in C++ """ + __slots__ = _fields = ('type', 'alias') + defaults = {'alias': none} + _construct_type = Type + _construct_alias = String + +constexpr = Attribute('constexpr') diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/fnodes.py b/venv/lib/python3.11/site-packages/sympy/codegen/fnodes.py new file mode 100644 index 0000000000000000000000000000000000000000..8b972fcfe4d4de4cfe74d75705f42c5e112a9b43 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/fnodes.py @@ -0,0 +1,658 @@ +""" +AST nodes specific to Fortran. + +The functions defined in this module allows the user to express functions such as ``dsign`` +as a SymPy function for symbolic manipulation. +""" + +from __future__ import annotations +from sympy.codegen.ast import ( + Attribute, CodeBlock, FunctionCall, Node, none, String, + Token, _mk_Tuple, Variable +) +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.expr import Expr +from sympy.core.function import Function +from sympy.core.numbers import Float, Integer +from sympy.core.symbol import Str +from sympy.core.sympify import sympify +from sympy.logic import true, false +from sympy.utilities.iterables import iterable + + + +pure = Attribute('pure') +elemental = Attribute('elemental') # (all elemental procedures are also pure) + +intent_in = Attribute('intent_in') +intent_out = Attribute('intent_out') +intent_inout = Attribute('intent_inout') + +allocatable = Attribute('allocatable') + +class Program(Token): + """ Represents a 'program' block in Fortran. + + Examples + ======== + + >>> from sympy.codegen.ast import Print + >>> from sympy.codegen.fnodes import Program + >>> prog = Program('myprogram', [Print([42])]) + >>> from sympy import fcode + >>> print(fcode(prog, source_format='free')) + program myprogram + print *, 42 + end program + + """ + __slots__ = _fields = ('name', 'body') + _construct_name = String + _construct_body = staticmethod(lambda body: CodeBlock(*body)) + + +class use_rename(Token): + """ Represents a renaming in a use statement in Fortran. + + Examples + ======== + + >>> from sympy.codegen.fnodes import use_rename, use + >>> from sympy import fcode + >>> ren = use_rename("thingy", "convolution2d") + >>> print(fcode(ren, source_format='free')) + thingy => convolution2d + >>> full = use('signallib', only=['snr', ren]) + >>> print(fcode(full, source_format='free')) + use signallib, only: snr, thingy => convolution2d + + """ + __slots__ = _fields = ('local', 'original') + _construct_local = String + _construct_original = String + +def _name(arg): + if hasattr(arg, 'name'): + return arg.name + else: + return String(arg) + +class use(Token): + """ Represents a use statement in Fortran. + + Examples + ======== + + >>> from sympy.codegen.fnodes import use + >>> from sympy import fcode + >>> fcode(use('signallib'), source_format='free') + 'use signallib' + >>> fcode(use('signallib', [('metric', 'snr')]), source_format='free') + 'use signallib, metric => snr' + >>> fcode(use('signallib', only=['snr', 'convolution2d']), source_format='free') + 'use signallib, only: snr, convolution2d' + + """ + __slots__ = _fields = ('namespace', 'rename', 'only') + defaults = {'rename': none, 'only': none} + _construct_namespace = staticmethod(_name) + _construct_rename = staticmethod(lambda args: Tuple(*[arg if isinstance(arg, use_rename) else use_rename(*arg) for arg in args])) + _construct_only = staticmethod(lambda args: Tuple(*[arg if isinstance(arg, use_rename) else _name(arg) for arg in args])) + + +class Module(Token): + """ Represents a module in Fortran. + + Examples + ======== + + >>> from sympy.codegen.fnodes import Module + >>> from sympy import fcode + >>> print(fcode(Module('signallib', ['implicit none'], []), source_format='free')) + module signallib + implicit none + + contains + + + end module + + """ + __slots__ = _fields = ('name', 'declarations', 'definitions') + defaults = {'declarations': Tuple()} + _construct_name = String + + @classmethod + def _construct_declarations(cls, args): + args = [Str(arg) if isinstance(arg, str) else arg for arg in args] + return CodeBlock(*args) + + _construct_definitions = staticmethod(lambda arg: CodeBlock(*arg)) + + +class Subroutine(Node): + """ Represents a subroutine in Fortran. + + Examples + ======== + + >>> from sympy import fcode, symbols + >>> from sympy.codegen.ast import Print + >>> from sympy.codegen.fnodes import Subroutine + >>> x, y = symbols('x y', real=True) + >>> sub = Subroutine('mysub', [x, y], [Print([x**2 + y**2, x*y])]) + >>> print(fcode(sub, source_format='free', standard=2003)) + subroutine mysub(x, y) + real*8 :: x + real*8 :: y + print *, x**2 + y**2, x*y + end subroutine + + """ + __slots__ = ('name', 'parameters', 'body') + _fields = __slots__ + Node._fields + _construct_name = String + _construct_parameters = staticmethod(lambda params: Tuple(*map(Variable.deduced, params))) + + @classmethod + def _construct_body(cls, itr): + if isinstance(itr, CodeBlock): + return itr + else: + return CodeBlock(*itr) + +class SubroutineCall(Token): + """ Represents a call to a subroutine in Fortran. + + Examples + ======== + + >>> from sympy.codegen.fnodes import SubroutineCall + >>> from sympy import fcode + >>> fcode(SubroutineCall('mysub', 'x y'.split())) + ' call mysub(x, y)' + + """ + __slots__ = _fields = ('name', 'subroutine_args') + _construct_name = staticmethod(_name) + _construct_subroutine_args = staticmethod(_mk_Tuple) + + +class Do(Token): + """ Represents a Do loop in in Fortran. + + Examples + ======== + + >>> from sympy import fcode, symbols + >>> from sympy.codegen.ast import aug_assign, Print + >>> from sympy.codegen.fnodes import Do + >>> i, n = symbols('i n', integer=True) + >>> r = symbols('r', real=True) + >>> body = [aug_assign(r, '+', 1/i), Print([i, r])] + >>> do1 = Do(body, i, 1, n) + >>> print(fcode(do1, source_format='free')) + do i = 1, n + r = r + 1d0/i + print *, i, r + end do + >>> do2 = Do(body, i, 1, n, 2) + >>> print(fcode(do2, source_format='free')) + do i = 1, n, 2 + r = r + 1d0/i + print *, i, r + end do + + """ + + __slots__ = _fields = ('body', 'counter', 'first', 'last', 'step', 'concurrent') + defaults = {'step': Integer(1), 'concurrent': false} + _construct_body = staticmethod(lambda body: CodeBlock(*body)) + _construct_counter = staticmethod(sympify) + _construct_first = staticmethod(sympify) + _construct_last = staticmethod(sympify) + _construct_step = staticmethod(sympify) + _construct_concurrent = staticmethod(lambda arg: true if arg else false) + + +class ArrayConstructor(Token): + """ Represents an array constructor. + + Examples + ======== + + >>> from sympy import fcode + >>> from sympy.codegen.fnodes import ArrayConstructor + >>> ac = ArrayConstructor([1, 2, 3]) + >>> fcode(ac, standard=95, source_format='free') + '(/1, 2, 3/)' + >>> fcode(ac, standard=2003, source_format='free') + '[1, 2, 3]' + + """ + __slots__ = _fields = ('elements',) + _construct_elements = staticmethod(_mk_Tuple) + + +class ImpliedDoLoop(Token): + """ Represents an implied do loop in Fortran. + + Examples + ======== + + >>> from sympy import Symbol, fcode + >>> from sympy.codegen.fnodes import ImpliedDoLoop, ArrayConstructor + >>> i = Symbol('i', integer=True) + >>> idl = ImpliedDoLoop(i**3, i, -3, 3, 2) # -27, -1, 1, 27 + >>> ac = ArrayConstructor([-28, idl, 28]) # -28, -27, -1, 1, 27, 28 + >>> fcode(ac, standard=2003, source_format='free') + '[-28, (i**3, i = -3, 3, 2), 28]' + + """ + __slots__ = _fields = ('expr', 'counter', 'first', 'last', 'step') + defaults = {'step': Integer(1)} + _construct_expr = staticmethod(sympify) + _construct_counter = staticmethod(sympify) + _construct_first = staticmethod(sympify) + _construct_last = staticmethod(sympify) + _construct_step = staticmethod(sympify) + + +class Extent(Basic): + """ Represents a dimension extent. + + Examples + ======== + + >>> from sympy.codegen.fnodes import Extent + >>> e = Extent(-3, 3) # -3, -2, -1, 0, 1, 2, 3 + >>> from sympy import fcode + >>> fcode(e, source_format='free') + '-3:3' + >>> from sympy.codegen.ast import Variable, real + >>> from sympy.codegen.fnodes import dimension, intent_out + >>> dim = dimension(e, e) + >>> arr = Variable('x', real, attrs=[dim, intent_out]) + >>> fcode(arr.as_Declaration(), source_format='free', standard=2003) + 'real*8, dimension(-3:3, -3:3), intent(out) :: x' + + """ + def __new__(cls, *args): + if len(args) == 2: + low, high = args + return Basic.__new__(cls, sympify(low), sympify(high)) + elif len(args) == 0 or (len(args) == 1 and args[0] in (':', None)): + return Basic.__new__(cls) # assumed shape + else: + raise ValueError("Expected 0 or 2 args (or one argument == None or ':')") + + def _sympystr(self, printer): + if len(self.args) == 0: + return ':' + return ":".join(str(arg) for arg in self.args) + +assumed_extent = Extent() # or Extent(':'), Extent(None) + + +def dimension(*args): + """ Creates a 'dimension' Attribute with (up to 7) extents. + + Examples + ======== + + >>> from sympy import fcode + >>> from sympy.codegen.fnodes import dimension, intent_in + >>> dim = dimension('2', ':') # 2 rows, runtime determined number of columns + >>> from sympy.codegen.ast import Variable, integer + >>> arr = Variable('a', integer, attrs=[dim, intent_in]) + >>> fcode(arr.as_Declaration(), source_format='free', standard=2003) + 'integer*4, dimension(2, :), intent(in) :: a' + + """ + if len(args) > 7: + raise ValueError("Fortran only supports up to 7 dimensional arrays") + parameters = [] + for arg in args: + if isinstance(arg, Extent): + parameters.append(arg) + elif isinstance(arg, str): + if arg == ':': + parameters.append(Extent()) + else: + parameters.append(String(arg)) + elif iterable(arg): + parameters.append(Extent(*arg)) + else: + parameters.append(sympify(arg)) + if len(args) == 0: + raise ValueError("Need at least one dimension") + return Attribute('dimension', parameters) + + +assumed_size = dimension('*') + +def array(symbol, dim, intent=None, *, attrs=(), value=None, type=None): + """ Convenience function for creating a Variable instance for a Fortran array. + + Parameters + ========== + + symbol : symbol + dim : Attribute or iterable + If dim is an ``Attribute`` it need to have the name 'dimension'. If it is + not an ``Attribute``, then it is passed to :func:`dimension` as ``*dim`` + intent : str + One of: 'in', 'out', 'inout' or None + \\*\\*kwargs: + Keyword arguments for ``Variable`` ('type' & 'value') + + Examples + ======== + + >>> from sympy import fcode + >>> from sympy.codegen.ast import integer, real + >>> from sympy.codegen.fnodes import array + >>> arr = array('a', '*', 'in', type=integer) + >>> print(fcode(arr.as_Declaration(), source_format='free', standard=2003)) + integer*4, dimension(*), intent(in) :: a + >>> x = array('x', [3, ':', ':'], intent='out', type=real) + >>> print(fcode(x.as_Declaration(value=1), source_format='free', standard=2003)) + real*8, dimension(3, :, :), intent(out) :: x = 1 + + """ + if isinstance(dim, Attribute): + if str(dim.name) != 'dimension': + raise ValueError("Got an unexpected Attribute argument as dim: %s" % str(dim)) + else: + dim = dimension(*dim) + + attrs = list(attrs) + [dim] + if intent is not None: + if intent not in (intent_in, intent_out, intent_inout): + intent = {'in': intent_in, 'out': intent_out, 'inout': intent_inout}[intent] + attrs.append(intent) + if type is None: + return Variable.deduced(symbol, value=value, attrs=attrs) + else: + return Variable(symbol, type, value=value, attrs=attrs) + +def _printable(arg): + return String(arg) if isinstance(arg, str) else sympify(arg) + + +def allocated(array): + """ Creates an AST node for a function call to Fortran's "allocated(...)" + + Examples + ======== + + >>> from sympy import fcode + >>> from sympy.codegen.fnodes import allocated + >>> alloc = allocated('x') + >>> fcode(alloc, source_format='free') + 'allocated(x)' + + """ + return FunctionCall('allocated', [_printable(array)]) + + +def lbound(array, dim=None, kind=None): + """ Creates an AST node for a function call to Fortran's "lbound(...)" + + Parameters + ========== + + array : Symbol or String + dim : expr + kind : expr + + Examples + ======== + + >>> from sympy import fcode + >>> from sympy.codegen.fnodes import lbound + >>> lb = lbound('arr', dim=2) + >>> fcode(lb, source_format='free') + 'lbound(arr, 2)' + + """ + return FunctionCall( + 'lbound', + [_printable(array)] + + ([_printable(dim)] if dim else []) + + ([_printable(kind)] if kind else []) + ) + + +def ubound(array, dim=None, kind=None): + return FunctionCall( + 'ubound', + [_printable(array)] + + ([_printable(dim)] if dim else []) + + ([_printable(kind)] if kind else []) + ) + + +def shape(source, kind=None): + """ Creates an AST node for a function call to Fortran's "shape(...)" + + Parameters + ========== + + source : Symbol or String + kind : expr + + Examples + ======== + + >>> from sympy import fcode + >>> from sympy.codegen.fnodes import shape + >>> shp = shape('x') + >>> fcode(shp, source_format='free') + 'shape(x)' + + """ + return FunctionCall( + 'shape', + [_printable(source)] + + ([_printable(kind)] if kind else []) + ) + + +def size(array, dim=None, kind=None): + """ Creates an AST node for a function call to Fortran's "size(...)" + + Examples + ======== + + >>> from sympy import fcode, Symbol + >>> from sympy.codegen.ast import FunctionDefinition, real, Return + >>> from sympy.codegen.fnodes import array, sum_, size + >>> a = Symbol('a', real=True) + >>> body = [Return((sum_(a**2)/size(a))**.5)] + >>> arr = array(a, dim=[':'], intent='in') + >>> fd = FunctionDefinition(real, 'rms', [arr], body) + >>> print(fcode(fd, source_format='free', standard=2003)) + real*8 function rms(a) + real*8, dimension(:), intent(in) :: a + rms = sqrt(sum(a**2)*1d0/size(a)) + end function + + """ + return FunctionCall( + 'size', + [_printable(array)] + + ([_printable(dim)] if dim else []) + + ([_printable(kind)] if kind else []) + ) + + +def reshape(source, shape, pad=None, order=None): + """ Creates an AST node for a function call to Fortran's "reshape(...)" + + Parameters + ========== + + source : Symbol or String + shape : ArrayExpr + + """ + return FunctionCall( + 'reshape', + [_printable(source), _printable(shape)] + + ([_printable(pad)] if pad else []) + + ([_printable(order)] if pad else []) + ) + + +def bind_C(name=None): + """ Creates an Attribute ``bind_C`` with a name. + + Parameters + ========== + + name : str + + Examples + ======== + + >>> from sympy import fcode, Symbol + >>> from sympy.codegen.ast import FunctionDefinition, real, Return + >>> from sympy.codegen.fnodes import array, sum_, bind_C + >>> a = Symbol('a', real=True) + >>> s = Symbol('s', integer=True) + >>> arr = array(a, dim=[s], intent='in') + >>> body = [Return((sum_(a**2)/s)**.5)] + >>> fd = FunctionDefinition(real, 'rms', [arr, s], body, attrs=[bind_C('rms')]) + >>> print(fcode(fd, source_format='free', standard=2003)) + real*8 function rms(a, s) bind(C, name="rms") + real*8, dimension(s), intent(in) :: a + integer*4 :: s + rms = sqrt(sum(a**2)/s) + end function + + """ + return Attribute('bind_C', [String(name)] if name else []) + +class GoTo(Token): + """ Represents a goto statement in Fortran + + Examples + ======== + + >>> from sympy.codegen.fnodes import GoTo + >>> go = GoTo([10, 20, 30], 'i') + >>> from sympy import fcode + >>> fcode(go, source_format='free') + 'go to (10, 20, 30), i' + + """ + __slots__ = _fields = ('labels', 'expr') + defaults = {'expr': none} + _construct_labels = staticmethod(_mk_Tuple) + _construct_expr = staticmethod(sympify) + + +class FortranReturn(Token): + """ AST node explicitly mapped to a fortran "return". + + Explanation + =========== + + Because a return statement in fortran is different from C, and + in order to aid reuse of our codegen ASTs the ordinary + ``.codegen.ast.Return`` is interpreted as assignment to + the result variable of the function. If one for some reason needs + to generate a fortran RETURN statement, this node should be used. + + Examples + ======== + + >>> from sympy.codegen.fnodes import FortranReturn + >>> from sympy import fcode + >>> fcode(FortranReturn('x')) + ' return x' + + """ + __slots__ = _fields = ('return_value',) + defaults = {'return_value': none} + _construct_return_value = staticmethod(sympify) + + +class FFunction(Function): + _required_standard = 77 + + def _fcode(self, printer): + name = self.__class__.__name__ + if printer._settings['standard'] < self._required_standard: + raise NotImplementedError("%s requires Fortran %d or newer" % + (name, self._required_standard)) + return '{}({})'.format(name, ', '.join(map(printer._print, self.args))) + + +class F95Function(FFunction): + _required_standard = 95 + + +class isign(FFunction): + """ Fortran sign intrinsic for integer arguments. """ + nargs = 2 + + +class dsign(FFunction): + """ Fortran sign intrinsic for double precision arguments. """ + nargs = 2 + + +class cmplx(FFunction): + """ Fortran complex conversion function. """ + nargs = 2 # may be extended to (2, 3) at a later point + + +class kind(FFunction): + """ Fortran kind function. """ + nargs = 1 + + +class merge(F95Function): + """ Fortran merge function """ + nargs = 3 + + +class _literal(Float): + _token: str + _decimals: int + + def _fcode(self, printer, *args, **kwargs): + mantissa, sgnd_ex = ('%.{}e'.format(self._decimals) % self).split('e') + mantissa = mantissa.strip('0').rstrip('.') + ex_sgn, ex_num = sgnd_ex[0], sgnd_ex[1:].lstrip('0') + ex_sgn = '' if ex_sgn == '+' else ex_sgn + return (mantissa or '0') + self._token + ex_sgn + (ex_num or '0') + + +class literal_sp(_literal): + """ Fortran single precision real literal """ + _token = 'e' + _decimals = 9 + + +class literal_dp(_literal): + """ Fortran double precision real literal """ + _token = 'd' + _decimals = 17 + + +class sum_(Token, Expr): + __slots__ = _fields = ('array', 'dim', 'mask') + defaults = {'dim': none, 'mask': none} + _construct_array = staticmethod(sympify) + _construct_dim = staticmethod(sympify) + + +class product_(Token, Expr): + __slots__ = _fields = ('array', 'dim', 'mask') + defaults = {'dim': none, 'mask': none} + _construct_array = staticmethod(sympify) + _construct_dim = staticmethod(sympify) diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/futils.py b/venv/lib/python3.11/site-packages/sympy/codegen/futils.py new file mode 100644 index 0000000000000000000000000000000000000000..4a1f5751fbd4d6b44d99c69a74ad89a8496f8648 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/futils.py @@ -0,0 +1,40 @@ +from itertools import chain +from sympy.codegen.fnodes import Module +from sympy.core.symbol import Dummy +from sympy.printing.fortran import FCodePrinter + +""" This module collects utilities for rendering Fortran code. """ + + +def render_as_module(definitions, name, declarations=(), printer_settings=None): + """ Creates a ``Module`` instance and renders it as a string. + + This generates Fortran source code for a module with the correct ``use`` statements. + + Parameters + ========== + + definitions : iterable + Passed to :class:`sympy.codegen.fnodes.Module`. + name : str + Passed to :class:`sympy.codegen.fnodes.Module`. + declarations : iterable + Passed to :class:`sympy.codegen.fnodes.Module`. It will be extended with + use statements, 'implicit none' and public list generated from ``definitions``. + printer_settings : dict + Passed to ``FCodePrinter`` (default: ``{'standard': 2003, 'source_format': 'free'}``). + + """ + printer_settings = printer_settings or {'standard': 2003, 'source_format': 'free'} + printer = FCodePrinter(printer_settings) + dummy = Dummy() + if isinstance(definitions, Module): + raise ValueError("This function expects to construct a module on its own.") + mod = Module(name, chain(declarations, [dummy]), definitions) + fstr = printer.doprint(mod) + module_use_str = ' %s\n' % ' \n'.join(['use %s, only: %s' % (k, ', '.join(v)) for + k, v in printer.module_uses.items()]) + module_use_str += ' implicit none\n' + module_use_str += ' private\n' + module_use_str += ' public %s\n' % ', '.join([str(node.name) for node in definitions if getattr(node, 'name', None)]) + return fstr.replace(printer.doprint(dummy), module_use_str) diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/matrix_nodes.py b/venv/lib/python3.11/site-packages/sympy/codegen/matrix_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..cf0a13a81c963e2c9e2b885dabd0ff3e2d2b3eb9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/matrix_nodes.py @@ -0,0 +1,71 @@ +""" +Additional AST nodes for operations on matrices. The nodes in this module +are meant to represent optimization of matrix expressions within codegen's +target languages that cannot be represented by SymPy expressions. + +As an example, we can use :meth:`sympy.codegen.rewriting.optimize` and the +``matin_opt`` optimization provided in :mod:`sympy.codegen.rewriting` to +transform matrix multiplication under certain assumptions: + + >>> from sympy import symbols, MatrixSymbol + >>> n = symbols('n', integer=True) + >>> A = MatrixSymbol('A', n, n) + >>> x = MatrixSymbol('x', n, 1) + >>> expr = A**(-1) * x + >>> from sympy import assuming, Q + >>> from sympy.codegen.rewriting import matinv_opt, optimize + >>> with assuming(Q.fullrank(A)): + ... optimize(expr, [matinv_opt]) + MatrixSolve(A, vector=x) +""" + +from .ast import Token +from sympy.matrices import MatrixExpr +from sympy.core.sympify import sympify + + +class MatrixSolve(Token, MatrixExpr): + """Represents an operation to solve a linear matrix equation. + + Parameters + ========== + + matrix : MatrixSymbol + + Matrix representing the coefficients of variables in the linear + equation. This matrix must be square and full-rank (i.e. all columns must + be linearly independent) for the solving operation to be valid. + + vector : MatrixSymbol + + One-column matrix representing the solutions to the equations + represented in ``matrix``. + + Examples + ======== + + >>> from sympy import symbols, MatrixSymbol + >>> from sympy.codegen.matrix_nodes import MatrixSolve + >>> n = symbols('n', integer=True) + >>> A = MatrixSymbol('A', n, n) + >>> x = MatrixSymbol('x', n, 1) + >>> from sympy.printing.numpy import NumPyPrinter + >>> NumPyPrinter().doprint(MatrixSolve(A, x)) + 'numpy.linalg.solve(A, x)' + >>> from sympy import octave_code + >>> octave_code(MatrixSolve(A, x)) + 'A \\\\ x' + + """ + __slots__ = _fields = ('matrix', 'vector') + + _construct_matrix = staticmethod(sympify) + _construct_vector = staticmethod(sympify) + + @property + def shape(self): + return self.vector.shape + + def _eval_derivative(self, x): + A, b = self.matrix, self.vector + return MatrixSolve(A, b.diff(x) - A.diff(x) * MatrixSolve(A, b)) diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/numpy_nodes.py b/venv/lib/python3.11/site-packages/sympy/codegen/numpy_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..c47c87f0e1df74cd314bb70674ebff732fe500aa --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/numpy_nodes.py @@ -0,0 +1,177 @@ +from sympy.core.function import Add, ArgumentIndexError, Function +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.sorting import default_sort_key +from sympy.core.sympify import sympify +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.miscellaneous import Max, Min +from .ast import Token, none + + +def _logaddexp(x1, x2, *, evaluate=True): + return log(Add(exp(x1, evaluate=evaluate), exp(x2, evaluate=evaluate), evaluate=evaluate)) + + +_two = S.One*2 +_ln2 = log(_two) + + +def _lb(x, *, evaluate=True): + return log(x, evaluate=evaluate)/_ln2 + + +def _exp2(x, *, evaluate=True): + return Pow(_two, x, evaluate=evaluate) + + +def _logaddexp2(x1, x2, *, evaluate=True): + return _lb(Add(_exp2(x1, evaluate=evaluate), + _exp2(x2, evaluate=evaluate), evaluate=evaluate)) + + +class logaddexp(Function): + """ Logarithm of the sum of exponentiations of the inputs. + + Helper class for use with e.g. numpy.logaddexp + + See Also + ======== + + https://numpy.org/doc/stable/reference/generated/numpy.logaddexp.html + """ + nargs = 2 + + def __new__(cls, *args): + return Function.__new__(cls, *sorted(args, key=default_sort_key)) + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex == 1: + wrt, other = self.args + elif argindex == 2: + other, wrt = self.args + else: + raise ArgumentIndexError(self, argindex) + return S.One/(S.One + exp(other-wrt)) + + def _eval_rewrite_as_log(self, x1, x2, **kwargs): + return _logaddexp(x1, x2) + + def _eval_evalf(self, *args, **kwargs): + return self.rewrite(log).evalf(*args, **kwargs) + + def _eval_simplify(self, *args, **kwargs): + a, b = (x.simplify(**kwargs) for x in self.args) + candidate = _logaddexp(a, b) + if candidate != _logaddexp(a, b, evaluate=False): + return candidate + else: + return logaddexp(a, b) + + +class logaddexp2(Function): + """ Logarithm of the sum of exponentiations of the inputs in base-2. + + Helper class for use with e.g. numpy.logaddexp2 + + See Also + ======== + + https://numpy.org/doc/stable/reference/generated/numpy.logaddexp2.html + """ + nargs = 2 + + def __new__(cls, *args): + return Function.__new__(cls, *sorted(args, key=default_sort_key)) + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex == 1: + wrt, other = self.args + elif argindex == 2: + other, wrt = self.args + else: + raise ArgumentIndexError(self, argindex) + return S.One/(S.One + _exp2(other-wrt)) + + def _eval_rewrite_as_log(self, x1, x2, **kwargs): + return _logaddexp2(x1, x2) + + def _eval_evalf(self, *args, **kwargs): + return self.rewrite(log).evalf(*args, **kwargs) + + def _eval_simplify(self, *args, **kwargs): + a, b = (x.simplify(**kwargs).factor() for x in self.args) + candidate = _logaddexp2(a, b) + if candidate != _logaddexp2(a, b, evaluate=False): + return candidate + else: + return logaddexp2(a, b) + + +class amin(Token): + """ Minimum value along an axis. + + Helper class for use with e.g. numpy.amin + + + See Also + ======== + + https://numpy.org/doc/stable/reference/generated/numpy.amin.html + """ + __slots__ = _fields = ('array', 'axis') + defaults = {'axis': none} + _construct_axis = staticmethod(sympify) + + +class amax(Token): + """ Maximum value along an axis. + + Helper class for use with e.g. numpy.amax + + + See Also + ======== + + https://numpy.org/doc/stable/reference/generated/numpy.amax.html + """ + __slots__ = _fields = ('array', 'axis') + defaults = {'axis': none} + _construct_axis = staticmethod(sympify) + + +class maximum(Function): + """ Element-wise maximum of array elements. + + Helper class for use with e.g. numpy.maximum + + + See Also + ======== + + https://numpy.org/doc/stable/reference/generated/numpy.maximum.html + """ + + def _eval_rewrite_as_Max(self, *args): + return Max(*self.args) + + +class minimum(Function): + """ Element-wise minimum of array elements. + + Helper class for use with e.g. numpy.minimum + + + See Also + ======== + + https://numpy.org/doc/stable/reference/generated/numpy.minimum.html + """ + + def _eval_rewrite_as_Min(self, *args): + return Min(*self.args) diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/pynodes.py b/venv/lib/python3.11/site-packages/sympy/codegen/pynodes.py new file mode 100644 index 0000000000000000000000000000000000000000..f0a08b4a79d32f63d345947d6be310b44504dbf5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/pynodes.py @@ -0,0 +1,11 @@ +from .abstract_nodes import List as AbstractList +from .ast import Token + + +class List(AbstractList): + pass + + +class NumExprEvaluate(Token): + """represents a call to :class:`numexpr`s :func:`evaluate`""" + __slots__ = _fields = ('expr',) diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/pyutils.py b/venv/lib/python3.11/site-packages/sympy/codegen/pyutils.py new file mode 100644 index 0000000000000000000000000000000000000000..e14eabe92ce50105a4055b71a49767aae04610b9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/pyutils.py @@ -0,0 +1,24 @@ +from sympy.printing.pycode import PythonCodePrinter + +""" This module collects utilities for rendering Python code. """ + + +def render_as_module(content, standard='python3'): + """Renders Python code as a module (with the required imports). + + Parameters + ========== + + standard : + See the parameter ``standard`` in + :meth:`sympy.printing.pycode.pycode` + """ + + printer = PythonCodePrinter({'standard':standard}) + pystr = printer.doprint(content) + if printer._settings['fully_qualified_modules']: + module_imports_str = '\n'.join('import %s' % k for k in printer.module_imports) + else: + module_imports_str = '\n'.join(['from %s import %s' % (k, ', '.join(v)) for + k, v in printer.module_imports.items()]) + return module_imports_str + '\n\n' + pystr diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/rewriting.py b/venv/lib/python3.11/site-packages/sympy/codegen/rewriting.py new file mode 100644 index 0000000000000000000000000000000000000000..274b7770b46ded6711468ab2a01db3a53d6fde87 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/rewriting.py @@ -0,0 +1,357 @@ +""" +Classes and functions useful for rewriting expressions for optimized code +generation. Some languages (or standards thereof), e.g. C99, offer specialized +math functions for better performance and/or precision. + +Using the ``optimize`` function in this module, together with a collection of +rules (represented as instances of ``Optimization``), one can rewrite the +expressions for this purpose:: + + >>> from sympy import Symbol, exp, log + >>> from sympy.codegen.rewriting import optimize, optims_c99 + >>> x = Symbol('x') + >>> optimize(3*exp(2*x) - 3, optims_c99) + 3*expm1(2*x) + >>> optimize(exp(2*x) - 1 - exp(-33), optims_c99) + expm1(2*x) - exp(-33) + >>> optimize(log(3*x + 3), optims_c99) + log1p(x) + log(3) + >>> optimize(log(2*x + 3), optims_c99) + log(2*x + 3) + +The ``optims_c99`` imported above is tuple containing the following instances +(which may be imported from ``sympy.codegen.rewriting``): + +- ``expm1_opt`` +- ``log1p_opt`` +- ``exp2_opt`` +- ``log2_opt`` +- ``log2const_opt`` + + +""" +from sympy.core.function import expand_log +from sympy.core.singleton import S +from sympy.core.symbol import Wild +from sympy.functions.elementary.complexes import sign +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import (Max, Min) +from sympy.functions.elementary.trigonometric import (cos, sin, sinc) +from sympy.assumptions import Q, ask +from sympy.codegen.cfunctions import log1p, log2, exp2, expm1 +from sympy.codegen.matrix_nodes import MatrixSolve +from sympy.core.expr import UnevaluatedExpr +from sympy.core.power import Pow +from sympy.codegen.numpy_nodes import logaddexp, logaddexp2 +from sympy.codegen.scipy_nodes import cosm1, powm1 +from sympy.core.mul import Mul +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.utilities.iterables import sift + + +class Optimization: + """ Abstract base class for rewriting optimization. + + Subclasses should implement ``__call__`` taking an expression + as argument. + + Parameters + ========== + cost_function : callable returning number + priority : number + + """ + def __init__(self, cost_function=None, priority=1): + self.cost_function = cost_function + self.priority=priority + + def cheapest(self, *args): + return min(args, key=self.cost_function) + + +class ReplaceOptim(Optimization): + """ Rewriting optimization calling replace on expressions. + + Explanation + =========== + + The instance can be used as a function on expressions for which + it will apply the ``replace`` method (see + :meth:`sympy.core.basic.Basic.replace`). + + Parameters + ========== + + query : + First argument passed to replace. + value : + Second argument passed to replace. + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.codegen.rewriting import ReplaceOptim + >>> from sympy.codegen.cfunctions import exp2 + >>> x = Symbol('x') + >>> exp2_opt = ReplaceOptim(lambda p: p.is_Pow and p.base == 2, + ... lambda p: exp2(p.exp)) + >>> exp2_opt(2**x) + exp2(x) + + """ + + def __init__(self, query, value, **kwargs): + super().__init__(**kwargs) + self.query = query + self.value = value + + def __call__(self, expr): + return expr.replace(self.query, self.value) + + +def optimize(expr, optimizations): + """ Apply optimizations to an expression. + + Parameters + ========== + + expr : expression + optimizations : iterable of ``Optimization`` instances + The optimizations will be sorted with respect to ``priority`` (highest first). + + Examples + ======== + + >>> from sympy import log, Symbol + >>> from sympy.codegen.rewriting import optims_c99, optimize + >>> x = Symbol('x') + >>> optimize(log(x+3)/log(2) + log(x**2 + 1), optims_c99) + log1p(x**2) + log2(x + 3) + + """ + + for optim in sorted(optimizations, key=lambda opt: opt.priority, reverse=True): + new_expr = optim(expr) + if optim.cost_function is None: + expr = new_expr + else: + expr = optim.cheapest(expr, new_expr) + return expr + + +exp2_opt = ReplaceOptim( + lambda p: p.is_Pow and p.base == 2, + lambda p: exp2(p.exp) +) + + +_d = Wild('d', properties=[lambda x: x.is_Dummy]) +_u = Wild('u', properties=[lambda x: not x.is_number and not x.is_Add]) +_v = Wild('v') +_w = Wild('w') +_n = Wild('n', properties=[lambda x: x.is_number]) + +sinc_opt1 = ReplaceOptim( + sin(_w)/_w, sinc(_w) +) +sinc_opt2 = ReplaceOptim( + sin(_n*_w)/_w, _n*sinc(_n*_w) +) +sinc_opts = (sinc_opt1, sinc_opt2) + +log2_opt = ReplaceOptim(_v*log(_w)/log(2), _v*log2(_w), cost_function=lambda expr: expr.count( + lambda e: ( # division & eval of transcendentals are expensive floating point operations... + e.is_Pow and e.exp.is_negative # division + or (isinstance(e, (log, log2)) and not e.args[0].is_number)) # transcendental + ) +) + +log2const_opt = ReplaceOptim(log(2)*log2(_w), log(_w)) + +logsumexp_2terms_opt = ReplaceOptim( + lambda l: (isinstance(l, log) + and l.args[0].is_Add + and len(l.args[0].args) == 2 + and all(isinstance(t, exp) for t in l.args[0].args)), + lambda l: ( + Max(*[e.args[0] for e in l.args[0].args]) + + log1p(exp(Min(*[e.args[0] for e in l.args[0].args]))) + ) +) + + +class FuncMinusOneOptim(ReplaceOptim): + """Specialization of ReplaceOptim for functions evaluating "f(x) - 1". + + Explanation + =========== + + Numerical functions which go toward one as x go toward zero is often best + implemented by a dedicated function in order to avoid catastrophic + cancellation. One such example is ``expm1(x)`` in the C standard library + which evaluates ``exp(x) - 1``. Such functions preserves many more + significant digits when its argument is much smaller than one, compared + to subtracting one afterwards. + + Parameters + ========== + + func : + The function which is subtracted by one. + func_m_1 : + The specialized function evaluating ``func(x) - 1``. + opportunistic : bool + When ``True``, apply the transformation as long as the magnitude of the + remaining number terms decreases. When ``False``, only apply the + transformation if it completely eliminates the number term. + + Examples + ======== + + >>> from sympy import symbols, exp + >>> from sympy.codegen.rewriting import FuncMinusOneOptim + >>> from sympy.codegen.cfunctions import expm1 + >>> x, y = symbols('x y') + >>> expm1_opt = FuncMinusOneOptim(exp, expm1) + >>> expm1_opt(exp(x) + 2*exp(5*y) - 3) + expm1(x) + 2*expm1(5*y) + + + """ + + def __init__(self, func, func_m_1, opportunistic=True): + weight = 10 # <-- this is an arbitrary number (heuristic) + super().__init__(lambda e: e.is_Add, self.replace_in_Add, + cost_function=lambda expr: expr.count_ops() - weight*expr.count(func_m_1)) + self.func = func + self.func_m_1 = func_m_1 + self.opportunistic = opportunistic + + def _group_Add_terms(self, add): + numbers, non_num = sift(add.args, lambda arg: arg.is_number, binary=True) + numsum = sum(numbers) + terms_with_func, other = sift(non_num, lambda arg: arg.has(self.func), binary=True) + return numsum, terms_with_func, other + + def replace_in_Add(self, e): + """ passed as second argument to Basic.replace(...) """ + numsum, terms_with_func, other_non_num_terms = self._group_Add_terms(e) + if numsum == 0: + return e + substituted, untouched = [], [] + for with_func in terms_with_func: + if with_func.is_Mul: + func, coeff = sift(with_func.args, lambda arg: arg.func == self.func, binary=True) + if len(func) == 1 and len(coeff) == 1: + func, coeff = func[0], coeff[0] + else: + coeff = None + elif with_func.func == self.func: + func, coeff = with_func, S.One + else: + coeff = None + + if coeff is not None and coeff.is_number and sign(coeff) == -sign(numsum): + if self.opportunistic: + do_substitute = abs(coeff+numsum) < abs(numsum) + else: + do_substitute = coeff+numsum == 0 + + if do_substitute: # advantageous substitution + numsum += coeff + substituted.append(coeff*self.func_m_1(*func.args)) + continue + untouched.append(with_func) + + return e.func(numsum, *substituted, *untouched, *other_non_num_terms) + + def __call__(self, expr): + alt1 = super().__call__(expr) + alt2 = super().__call__(expr.factor()) + return self.cheapest(alt1, alt2) + + +expm1_opt = FuncMinusOneOptim(exp, expm1) +cosm1_opt = FuncMinusOneOptim(cos, cosm1) +powm1_opt = FuncMinusOneOptim(Pow, powm1) + +log1p_opt = ReplaceOptim( + lambda e: isinstance(e, log), + lambda l: expand_log(l.replace( + log, lambda arg: log(arg.factor()) + )).replace(log(_u+1), log1p(_u)) +) + +def create_expand_pow_optimization(limit, *, base_req=lambda b: b.is_symbol): + """ Creates an instance of :class:`ReplaceOptim` for expanding ``Pow``. + + Explanation + =========== + + The requirements for expansions are that the base needs to be a symbol + and the exponent needs to be an Integer (and be less than or equal to + ``limit``). + + Parameters + ========== + + limit : int + The highest power which is expanded into multiplication. + base_req : function returning bool + Requirement on base for expansion to happen, default is to return + the ``is_symbol`` attribute of the base. + + Examples + ======== + + >>> from sympy import Symbol, sin + >>> from sympy.codegen.rewriting import create_expand_pow_optimization + >>> x = Symbol('x') + >>> expand_opt = create_expand_pow_optimization(3) + >>> expand_opt(x**5 + x**3) + x**5 + x*x*x + >>> expand_opt(x**5 + x**3 + sin(x)**3) + x**5 + sin(x)**3 + x*x*x + >>> opt2 = create_expand_pow_optimization(3, base_req=lambda b: not b.is_Function) + >>> opt2((x+1)**2 + sin(x)**2) + sin(x)**2 + (x + 1)*(x + 1) + + """ + return ReplaceOptim( + lambda e: e.is_Pow and base_req(e.base) and e.exp.is_Integer and abs(e.exp) <= limit, + lambda p: ( + UnevaluatedExpr(Mul(*([p.base]*+p.exp), evaluate=False)) if p.exp > 0 else + 1/UnevaluatedExpr(Mul(*([p.base]*-p.exp), evaluate=False)) + )) + +# Optimization procedures for turning A**(-1) * x into MatrixSolve(A, x) +def _matinv_predicate(expr): + # TODO: We should be able to support more than 2 elements + if expr.is_MatMul and len(expr.args) == 2: + left, right = expr.args + if left.is_Inverse and right.shape[1] == 1: + inv_arg = left.arg + if isinstance(inv_arg, MatrixSymbol): + return bool(ask(Q.fullrank(left.arg))) + + return False + +def _matinv_transform(expr): + left, right = expr.args + inv_arg = left.arg + return MatrixSolve(inv_arg, right) + + +matinv_opt = ReplaceOptim(_matinv_predicate, _matinv_transform) + + +logaddexp_opt = ReplaceOptim(log(exp(_v)+exp(_w)), logaddexp(_v, _w)) +logaddexp2_opt = ReplaceOptim(log(Pow(2, _v)+Pow(2, _w)), logaddexp2(_v, _w)*log(2)) + +# Collections of optimizations: +optims_c99 = (expm1_opt, log1p_opt, exp2_opt, log2_opt, log2const_opt) + +optims_numpy = optims_c99 + (logaddexp_opt, logaddexp2_opt,) + sinc_opts + +optims_scipy = (cosm1_opt, powm1_opt) diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/scipy_nodes.py b/venv/lib/python3.11/site-packages/sympy/codegen/scipy_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..059a853fc8cac6e0b9a1a3c7395dd3a15384dcba --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/scipy_nodes.py @@ -0,0 +1,79 @@ +from sympy.core.function import Add, ArgumentIndexError, Function +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.trigonometric import cos, sin + + +def _cosm1(x, *, evaluate=True): + return Add(cos(x, evaluate=evaluate), -S.One, evaluate=evaluate) + + +class cosm1(Function): + """ Minus one plus cosine of x, i.e. cos(x) - 1. For use when x is close to zero. + + Helper class for use with e.g. scipy.special.cosm1 + See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.cosm1.html + """ + nargs = 1 + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex == 1: + return -sin(*self.args) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_cos(self, x, **kwargs): + return _cosm1(x) + + def _eval_evalf(self, *args, **kwargs): + return self.rewrite(cos).evalf(*args, **kwargs) + + def _eval_simplify(self, **kwargs): + x, = self.args + candidate = _cosm1(x.simplify(**kwargs)) + if candidate != _cosm1(x, evaluate=False): + return candidate + else: + return cosm1(x) + + +def _powm1(x, y, *, evaluate=True): + return Add(Pow(x, y, evaluate=evaluate), -S.One, evaluate=evaluate) + + +class powm1(Function): + """ Minus one plus x to the power of y, i.e. x**y - 1. For use when x is close to one or y is close to zero. + + Helper class for use with e.g. scipy.special.powm1 + See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.powm1.html + """ + nargs = 2 + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex == 1: + return Pow(self.args[0], self.args[1])*self.args[1]/self.args[0] + elif argindex == 2: + return log(self.args[0])*Pow(*self.args) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Pow(self, x, y, **kwargs): + return _powm1(x, y) + + def _eval_evalf(self, *args, **kwargs): + return self.rewrite(Pow).evalf(*args, **kwargs) + + def _eval_simplify(self, **kwargs): + x, y = self.args + candidate = _powm1(x.simplify(**kwargs), y.simplify(**kwargs)) + if candidate != _powm1(x, y, evaluate=False): + return candidate + else: + return powm1(x, y) diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/codegen/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_abstract_nodes.py b/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_abstract_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..89e1f73ff8cb24a4a865aa51304ec66e9901e3cb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_abstract_nodes.py @@ -0,0 +1,14 @@ +from sympy.core.symbol import symbols +from sympy.codegen.abstract_nodes import List + + +def test_List(): + l = List(2, 3, 4) + assert l == List(2, 3, 4) + assert str(l) == "[2, 3, 4]" + x, y, z = symbols('x y z') + l = List(x**2,y**3,z**4) + # contrary to python's built-in list, we can call e.g. "replace" on List. + m = l.replace(lambda arg: arg.is_Pow and arg.exp>2, lambda p: p.base-p.exp) + assert m == [x**2, y-3, z-4] + hash(m) diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_algorithms.py b/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_algorithms.py new file mode 100644 index 0000000000000000000000000000000000000000..c684229ec18a1e02a97eee6db8537b8d12af0582 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_algorithms.py @@ -0,0 +1,180 @@ +import tempfile +from sympy import log, Min, Max, sqrt +from sympy.core.numbers import Float +from sympy.core.symbol import Symbol, symbols +from sympy.functions.elementary.trigonometric import cos +from sympy.codegen.ast import Assignment, Raise, RuntimeError_, QuotedString +from sympy.codegen.algorithms import newtons_method, newtons_method_function +from sympy.codegen.cfunctions import expm1 +from sympy.codegen.fnodes import bind_C +from sympy.codegen.futils import render_as_module as f_module +from sympy.codegen.pyutils import render_as_module as py_module +from sympy.external import import_module +from sympy.printing.codeprinter import ccode +from sympy.utilities._compilation import compile_link_import_strings, has_c, has_fortran +from sympy.utilities._compilation.util import may_xfail +from sympy.testing.pytest import skip, raises, skip_under_pyodide + +cython = import_module('cython') +wurlitzer = import_module('wurlitzer') + +def test_newtons_method(): + x, dx, atol = symbols('x dx atol') + expr = cos(x) - x**3 + algo = newtons_method(expr, x, atol, dx) + assert algo.has(Assignment(dx, -expr/expr.diff(x))) + + +@may_xfail +def test_newtons_method_function__ccode(): + x = Symbol('x', real=True) + expr = cos(x) - x**3 + func = newtons_method_function(expr, x) + + if not cython: + skip("cython not installed.") + if not has_c(): + skip("No C compiler found.") + + compile_kw = {"std": 'c99'} + with tempfile.TemporaryDirectory() as folder: + mod, info = compile_link_import_strings([ + ('newton.c', ('#include \n' + '#include \n') + ccode(func)), + ('_newton.pyx', ("#cython: language_level={}\n".format("3") + + "cdef extern double newton(double)\n" + "def py_newton(x):\n" + " return newton(x)\n")) + ], build_dir=folder, compile_kwargs=compile_kw) + assert abs(mod.py_newton(0.5) - 0.865474033102) < 1e-12 + + +@may_xfail +def test_newtons_method_function__fcode(): + x = Symbol('x', real=True) + expr = cos(x) - x**3 + func = newtons_method_function(expr, x, attrs=[bind_C(name='newton')]) + + if not cython: + skip("cython not installed.") + if not has_fortran(): + skip("No Fortran compiler found.") + + f_mod = f_module([func], 'mod_newton') + with tempfile.TemporaryDirectory() as folder: + mod, info = compile_link_import_strings([ + ('newton.f90', f_mod), + ('_newton.pyx', ("#cython: language_level={}\n".format("3") + + "cdef extern double newton(double*)\n" + "def py_newton(double x):\n" + " return newton(&x)\n")) + ], build_dir=folder) + assert abs(mod.py_newton(0.5) - 0.865474033102) < 1e-12 + + +def test_newtons_method_function__pycode(): + x = Symbol('x', real=True) + expr = cos(x) - x**3 + func = newtons_method_function(expr, x) + py_mod = py_module(func) + namespace = {} + exec(py_mod, namespace, namespace) + res = eval('newton(0.5)', namespace) + assert abs(res - 0.865474033102) < 1e-12 + + +@may_xfail +@skip_under_pyodide("Emscripten does not support process spawning") +def test_newtons_method_function__ccode_parameters(): + args = x, A, k, p = symbols('x A k p') + expr = A*cos(k*x) - p*x**3 + raises(ValueError, lambda: newtons_method_function(expr, x)) + use_wurlitzer = wurlitzer + + func = newtons_method_function(expr, x, args, debug=use_wurlitzer) + + if not has_c(): + skip("No C compiler found.") + if not cython: + skip("cython not installed.") + + compile_kw = {"std": 'c99'} + with tempfile.TemporaryDirectory() as folder: + mod, info = compile_link_import_strings([ + ('newton_par.c', ('#include \n' + '#include \n') + ccode(func)), + ('_newton_par.pyx', ("#cython: language_level={}\n".format("3") + + "cdef extern double newton(double, double, double, double)\n" + "def py_newton(x, A=1, k=1, p=1):\n" + " return newton(x, A, k, p)\n")) + ], compile_kwargs=compile_kw, build_dir=folder) + + if use_wurlitzer: + with wurlitzer.pipes() as (out, err): + result = mod.py_newton(0.5) + else: + result = mod.py_newton(0.5) + + assert abs(result - 0.865474033102) < 1e-12 + + if not use_wurlitzer: + skip("C-level output only tested when package 'wurlitzer' is available.") + + out, err = out.read(), err.read() + assert err == '' + assert out == """\ +x= 0.5 +x= 1.1121 d_x= 0.61214 +x= 0.90967 d_x= -0.20247 +x= 0.86726 d_x= -0.042409 +x= 0.86548 d_x= -0.0017867 +x= 0.86547 d_x= -3.1022e-06 +x= 0.86547 d_x= -9.3421e-12 +x= 0.86547 d_x= 3.6902e-17 +""" # try to run tests with LC_ALL=C if this assertion fails + + +def test_newtons_method_function__rtol_cse_nan(): + a, b, c, N_geo, N_tot = symbols('a b c N_geo N_tot', real=True, nonnegative=True) + i = Symbol('i', integer=True, nonnegative=True) + N_ari = N_tot - N_geo - 1 + delta_ari = (c-b)/N_ari + ln_delta_geo = log(b) + log(-expm1((log(a)-log(b))/N_geo)) + eqb_log = ln_delta_geo - log(delta_ari) + + def _clamp(low, expr, high): + return Min(Max(low, expr), high) + + meth_kw = { + 'clamped_newton': {'delta_fn': lambda e, x: _clamp( + (sqrt(a*x)-x)*0.99, + -e/e.diff(x), + (sqrt(c*x)-x)*0.99 + )}, + 'halley': {'delta_fn': lambda e, x: (-2*(e*e.diff(x))/(2*e.diff(x)**2 - e*e.diff(x, 2)))}, + 'halley_alt': {'delta_fn': lambda e, x: (-e/e.diff(x)/(1-e/e.diff(x)*e.diff(x,2)/2/e.diff(x)))}, + } + args = eqb_log, b + for use_cse in [False, True]: + kwargs = { + 'params': (b, a, c, N_geo, N_tot), 'itermax': 60, 'debug': True, 'cse': use_cse, + 'counter': i, 'atol': 1e-100, 'rtol': 2e-16, 'bounds': (a,c), + 'handle_nan': Raise(RuntimeError_(QuotedString("encountered NaN."))) + } + func = {k: newtons_method_function(*args, func_name=f"{k}_b", **dict(kwargs, **kw)) for k, kw in meth_kw.items()} + py_mod = {k: py_module(v) for k, v in func.items()} + namespace = {} + root_find_b = {} + for k, v in py_mod.items(): + ns = namespace[k] = {} + exec(v, ns, ns) + root_find_b[k] = ns[f'{k}_b'] + ref = Float('13.2261515064168768938151923226496') + reftol = {'clamped_newton': 2e-16, 'halley': 2e-16, 'halley_alt': 3e-16} + guess = 4.0 + for meth, func in root_find_b.items(): + result = func(guess, 1e-2, 1e2, 50, 100) + req = ref*reftol[meth] + if use_cse: + req *= 2 + assert abs(result - ref) < req diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_applications.py b/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_applications.py new file mode 100644 index 0000000000000000000000000000000000000000..9519c06b96042b383314ef928d2ad0c1a2f92650 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_applications.py @@ -0,0 +1,58 @@ +# This file contains tests that exercise multiple AST nodes + +import tempfile + +from sympy.external import import_module +from sympy.printing.codeprinter import ccode +from sympy.utilities._compilation import compile_link_import_strings, has_c +from sympy.utilities._compilation.util import may_xfail +from sympy.testing.pytest import skip, skip_under_pyodide +from sympy.codegen.ast import ( + FunctionDefinition, FunctionPrototype, Variable, Pointer, real, Assignment, + integer, CodeBlock, While +) +from sympy.codegen.cnodes import void, PreIncrement +from sympy.codegen.cutils import render_as_source_file + +cython = import_module('cython') +np = import_module('numpy') + +def _mk_func1(): + declars = n, inp, out = Variable('n', integer), Pointer('inp', real), Pointer('out', real) + i = Variable('i', integer) + whl = While(i2, lambda p: p.base-p.exp) + assert m == [x**2, y-3, z-4] diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_pyutils.py b/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_pyutils.py new file mode 100644 index 0000000000000000000000000000000000000000..0a2f0ff358f333635c8d44195a5c39d63ac8f16f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_pyutils.py @@ -0,0 +1,7 @@ +from sympy.codegen.ast import Print +from sympy.codegen.pyutils import render_as_module + +def test_standard(): + ast = Print('x y'.split(), r"coordinate: %12.5g %12.5g\n") + assert render_as_module(ast, standard='python3') == \ + '\n\nprint("coordinate: %12.5g %12.5g\\n" % (x, y), end="")' diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_rewriting.py b/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_rewriting.py new file mode 100644 index 0000000000000000000000000000000000000000..51e0c9ecc940f60186cc04d4bf15650281d31cd8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_rewriting.py @@ -0,0 +1,479 @@ +import tempfile +from sympy.core.numbers import pi, Rational +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.trigonometric import (cos, sin, sinc) +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.assumptions import assuming, Q +from sympy.external import import_module +from sympy.printing.codeprinter import ccode +from sympy.codegen.matrix_nodes import MatrixSolve +from sympy.codegen.cfunctions import log2, exp2, expm1, log1p +from sympy.codegen.numpy_nodes import logaddexp, logaddexp2 +from sympy.codegen.scipy_nodes import cosm1, powm1 +from sympy.codegen.rewriting import ( + optimize, cosm1_opt, log2_opt, exp2_opt, expm1_opt, log1p_opt, powm1_opt, optims_c99, + create_expand_pow_optimization, matinv_opt, logaddexp_opt, logaddexp2_opt, + optims_numpy, optims_scipy, sinc_opts, FuncMinusOneOptim +) +from sympy.testing.pytest import XFAIL, skip +from sympy.utilities import lambdify +from sympy.utilities._compilation import compile_link_import_strings, has_c +from sympy.utilities._compilation.util import may_xfail + +cython = import_module('cython') +numpy = import_module('numpy') +scipy = import_module('scipy') + + +def test_log2_opt(): + x = Symbol('x') + expr1 = 7*log(3*x + 5)/(log(2)) + opt1 = optimize(expr1, [log2_opt]) + assert opt1 == 7*log2(3*x + 5) + assert opt1.rewrite(log) == expr1 + + expr2 = 3*log(5*x + 7)/(13*log(2)) + opt2 = optimize(expr2, [log2_opt]) + assert opt2 == 3*log2(5*x + 7)/13 + assert opt2.rewrite(log) == expr2 + + expr3 = log(x)/log(2) + opt3 = optimize(expr3, [log2_opt]) + assert opt3 == log2(x) + assert opt3.rewrite(log) == expr3 + + expr4 = log(x)/log(2) + log(x+1) + opt4 = optimize(expr4, [log2_opt]) + assert opt4 == log2(x) + log(2)*log2(x+1) + assert opt4.rewrite(log) == expr4 + + expr5 = log(17) + opt5 = optimize(expr5, [log2_opt]) + assert opt5 == expr5 + + expr6 = log(x + 3)/log(2) + opt6 = optimize(expr6, [log2_opt]) + assert str(opt6) == 'log2(x + 3)' + assert opt6.rewrite(log) == expr6 + + +def test_exp2_opt(): + x = Symbol('x') + expr1 = 1 + 2**x + opt1 = optimize(expr1, [exp2_opt]) + assert opt1 == 1 + exp2(x) + assert opt1.rewrite(Pow) == expr1 + + expr2 = 1 + 3**x + assert expr2 == optimize(expr2, [exp2_opt]) + + +def test_expm1_opt(): + x = Symbol('x') + + expr1 = exp(x) - 1 + opt1 = optimize(expr1, [expm1_opt]) + assert expm1(x) - opt1 == 0 + assert opt1.rewrite(exp) == expr1 + + expr2 = 3*exp(x) - 3 + opt2 = optimize(expr2, [expm1_opt]) + assert 3*expm1(x) == opt2 + assert opt2.rewrite(exp) == expr2 + + expr3 = 3*exp(x) - 5 + opt3 = optimize(expr3, [expm1_opt]) + assert 3*expm1(x) - 2 == opt3 + assert opt3.rewrite(exp) == expr3 + expm1_opt_non_opportunistic = FuncMinusOneOptim(exp, expm1, opportunistic=False) + assert expr3 == optimize(expr3, [expm1_opt_non_opportunistic]) + assert opt1 == optimize(expr1, [expm1_opt_non_opportunistic]) + assert opt2 == optimize(expr2, [expm1_opt_non_opportunistic]) + + expr4 = 3*exp(x) + log(x) - 3 + opt4 = optimize(expr4, [expm1_opt]) + assert 3*expm1(x) + log(x) == opt4 + assert opt4.rewrite(exp) == expr4 + + expr5 = 3*exp(2*x) - 3 + opt5 = optimize(expr5, [expm1_opt]) + assert 3*expm1(2*x) == opt5 + assert opt5.rewrite(exp) == expr5 + + expr6 = (2*exp(x) + 1)/(exp(x) + 1) + 1 + opt6 = optimize(expr6, [expm1_opt]) + assert opt6.count_ops() <= expr6.count_ops() + + def ev(e): + return e.subs(x, 3).evalf() + assert abs(ev(expr6) - ev(opt6)) < 1e-15 + + y = Symbol('y') + expr7 = (2*exp(x) - 1)/(1 - exp(y)) - 1/(1-exp(y)) + opt7 = optimize(expr7, [expm1_opt]) + assert -2*expm1(x)/expm1(y) == opt7 + assert (opt7.rewrite(exp) - expr7).factor() == 0 + + expr8 = (1+exp(x))**2 - 4 + opt8 = optimize(expr8, [expm1_opt]) + tgt8a = (exp(x) + 3)*expm1(x) + tgt8b = 2*expm1(x) + expm1(2*x) + # Both tgt8a & tgt8b seem to give full precision (~16 digits for double) + # for x=1e-7 (compare with expr8 which only achieves ~8 significant digits). + # If we can show that either tgt8a or tgt8b is preferable, we can + # change this test to ensure the preferable version is returned. + assert (tgt8a - tgt8b).rewrite(exp).factor() == 0 + assert opt8 in (tgt8a, tgt8b) + assert (opt8.rewrite(exp) - expr8).factor() == 0 + + expr9 = sin(expr8) + opt9 = optimize(expr9, [expm1_opt]) + tgt9a = sin(tgt8a) + tgt9b = sin(tgt8b) + assert opt9 in (tgt9a, tgt9b) + assert (opt9.rewrite(exp) - expr9.rewrite(exp)).factor().is_zero + + +def test_expm1_two_exp_terms(): + x, y = map(Symbol, 'x y'.split()) + expr1 = exp(x) + exp(y) - 2 + opt1 = optimize(expr1, [expm1_opt]) + assert opt1 == expm1(x) + expm1(y) + + +def test_cosm1_opt(): + x = Symbol('x') + + expr1 = cos(x) - 1 + opt1 = optimize(expr1, [cosm1_opt]) + assert cosm1(x) - opt1 == 0 + assert opt1.rewrite(cos) == expr1 + + expr2 = 3*cos(x) - 3 + opt2 = optimize(expr2, [cosm1_opt]) + assert 3*cosm1(x) == opt2 + assert opt2.rewrite(cos) == expr2 + + expr3 = 3*cos(x) - 5 + opt3 = optimize(expr3, [cosm1_opt]) + assert 3*cosm1(x) - 2 == opt3 + assert opt3.rewrite(cos) == expr3 + cosm1_opt_non_opportunistic = FuncMinusOneOptim(cos, cosm1, opportunistic=False) + assert expr3 == optimize(expr3, [cosm1_opt_non_opportunistic]) + assert opt1 == optimize(expr1, [cosm1_opt_non_opportunistic]) + assert opt2 == optimize(expr2, [cosm1_opt_non_opportunistic]) + + expr4 = 3*cos(x) + log(x) - 3 + opt4 = optimize(expr4, [cosm1_opt]) + assert 3*cosm1(x) + log(x) == opt4 + assert opt4.rewrite(cos) == expr4 + + expr5 = 3*cos(2*x) - 3 + opt5 = optimize(expr5, [cosm1_opt]) + assert 3*cosm1(2*x) == opt5 + assert opt5.rewrite(cos) == expr5 + + expr6 = 2 - 2*cos(x) + opt6 = optimize(expr6, [cosm1_opt]) + assert -2*cosm1(x) == opt6 + assert opt6.rewrite(cos) == expr6 + + +def test_cosm1_two_cos_terms(): + x, y = map(Symbol, 'x y'.split()) + expr1 = cos(x) + cos(y) - 2 + opt1 = optimize(expr1, [cosm1_opt]) + assert opt1 == cosm1(x) + cosm1(y) + + +def test_expm1_cosm1_mixed(): + x = Symbol('x') + expr1 = exp(x) + cos(x) - 2 + opt1 = optimize(expr1, [expm1_opt, cosm1_opt]) + assert opt1 == cosm1(x) + expm1(x) + + +def _check_num_lambdify(expr, opt, val_subs, approx_ref, lambdify_kw=None, poorness=1e10): + """ poorness=1e10 signifies that `expr` loses precision of at least ten decimal digits. """ + num_ref = expr.subs(val_subs).evalf() + eps = numpy.finfo(numpy.float64).eps + assert abs(num_ref - approx_ref) < approx_ref*eps + f1 = lambdify(list(val_subs.keys()), opt, **(lambdify_kw or {})) + args_float = tuple(map(float, val_subs.values())) + num_err1 = abs(f1(*args_float) - approx_ref) + assert num_err1 < abs(num_ref*eps) + f2 = lambdify(list(val_subs.keys()), expr, **(lambdify_kw or {})) + num_err2 = abs(f2(*args_float) - approx_ref) + assert num_err2 > abs(num_ref*eps*poorness) # this only ensures that the *test* works as intended + + +def test_cosm1_apart(): + x = Symbol('x') + + expr1 = 1/cos(x) - 1 + opt1 = optimize(expr1, [cosm1_opt]) + assert opt1 == -cosm1(x)/cos(x) + if scipy: + _check_num_lambdify(expr1, opt1, {x: S(10)**-30}, 5e-61, lambdify_kw={"modules": 'scipy'}) + + expr2 = 2/cos(x) - 2 + opt2 = optimize(expr2, optims_scipy) + assert opt2 == -2*cosm1(x)/cos(x) + if scipy: + _check_num_lambdify(expr2, opt2, {x: S(10)**-30}, 1e-60, lambdify_kw={"modules": 'scipy'}) + + expr3 = pi/cos(3*x) - pi + opt3 = optimize(expr3, [cosm1_opt]) + assert opt3 == -pi*cosm1(3*x)/cos(3*x) + if scipy: + _check_num_lambdify(expr3, opt3, {x: S(10)**-30/3}, float(5e-61*pi), lambdify_kw={"modules": 'scipy'}) + + +def test_powm1(): + args = x, y = map(Symbol, "xy") + + expr1 = x**y - 1 + opt1 = optimize(expr1, [powm1_opt]) + assert opt1 == powm1(x, y) + for arg in args: + assert expr1.diff(arg) == opt1.diff(arg) + if scipy and tuple(map(int, scipy.version.version.split('.')[:3])) >= (1, 10, 0): + subs1_a = {x: Rational(*(1.0+1e-13).as_integer_ratio()), y: pi} + ref1_f64_a = 3.139081648208105e-13 + _check_num_lambdify(expr1, opt1, subs1_a, ref1_f64_a, lambdify_kw={"modules": 'scipy'}, poorness=10**11) + + subs1_b = {x: pi, y: Rational(*(1e-10).as_integer_ratio())} + ref1_f64_b = 1.1447298859149205e-10 + _check_num_lambdify(expr1, opt1, subs1_b, ref1_f64_b, lambdify_kw={"modules": 'scipy'}, poorness=10**9) + + +def test_log1p_opt(): + x = Symbol('x') + expr1 = log(x + 1) + opt1 = optimize(expr1, [log1p_opt]) + assert log1p(x) - opt1 == 0 + assert opt1.rewrite(log) == expr1 + + expr2 = log(3*x + 3) + opt2 = optimize(expr2, [log1p_opt]) + assert log1p(x) + log(3) == opt2 + assert (opt2.rewrite(log) - expr2).simplify() == 0 + + expr3 = log(2*x + 1) + opt3 = optimize(expr3, [log1p_opt]) + assert log1p(2*x) - opt3 == 0 + assert opt3.rewrite(log) == expr3 + + expr4 = log(x+3) + opt4 = optimize(expr4, [log1p_opt]) + assert str(opt4) == 'log(x + 3)' + + +def test_optims_c99(): + x = Symbol('x') + + expr1 = 2**x + log(x)/log(2) + log(x + 1) + exp(x) - 1 + opt1 = optimize(expr1, optims_c99).simplify() + assert opt1 == exp2(x) + log2(x) + log1p(x) + expm1(x) + assert opt1.rewrite(exp).rewrite(log).rewrite(Pow) == expr1 + + expr2 = log(x)/log(2) + log(x + 1) + opt2 = optimize(expr2, optims_c99) + assert opt2 == log2(x) + log1p(x) + assert opt2.rewrite(log) == expr2 + + expr3 = log(x)/log(2) + log(17*x + 17) + opt3 = optimize(expr3, optims_c99) + delta3 = opt3 - (log2(x) + log(17) + log1p(x)) + assert delta3 == 0 + assert (opt3.rewrite(log) - expr3).simplify() == 0 + + expr4 = 2**x + 3*log(5*x + 7)/(13*log(2)) + 11*exp(x) - 11 + log(17*x + 17) + opt4 = optimize(expr4, optims_c99).simplify() + delta4 = opt4 - (exp2(x) + 3*log2(5*x + 7)/13 + 11*expm1(x) + log(17) + log1p(x)) + assert delta4 == 0 + assert (opt4.rewrite(exp).rewrite(log).rewrite(Pow) - expr4).simplify() == 0 + + expr5 = 3*exp(2*x) - 3 + opt5 = optimize(expr5, optims_c99) + delta5 = opt5 - 3*expm1(2*x) + assert delta5 == 0 + assert opt5.rewrite(exp) == expr5 + + expr6 = exp(2*x) - 3 + opt6 = optimize(expr6, optims_c99) + assert opt6 in (expm1(2*x) - 2, expr6) # expm1(2*x) - 2 is not better or worse + + expr7 = log(3*x + 3) + opt7 = optimize(expr7, optims_c99) + delta7 = opt7 - (log(3) + log1p(x)) + assert delta7 == 0 + assert (opt7.rewrite(log) - expr7).simplify() == 0 + + expr8 = log(2*x + 3) + opt8 = optimize(expr8, optims_c99) + assert opt8 == expr8 + + +def test_create_expand_pow_optimization(): + cc = lambda x: ccode( + optimize(x, [create_expand_pow_optimization(4)])) + x = Symbol('x') + assert cc(x**4) == 'x*x*x*x' + assert cc(x**4 + x**2) == 'x*x + x*x*x*x' + assert cc(x**5 + x**4) == 'pow(x, 5) + x*x*x*x' + assert cc(sin(x)**4) == 'pow(sin(x), 4)' + # gh issue 15335 + assert cc(x**(-4)) == '1.0/(x*x*x*x)' + assert cc(x**(-5)) == 'pow(x, -5)' + assert cc(-x**4) == '-(x*x*x*x)' + assert cc(x**4 - x**2) == '-(x*x) + x*x*x*x' + i = Symbol('i', integer=True) + assert cc(x**i - x**2) == 'pow(x, i) - (x*x)' + y = Symbol('y', real=True) + assert cc(Abs(exp(y**4))) == "exp(y*y*y*y)" + + # gh issue 20753 + cc2 = lambda x: ccode(optimize(x, [create_expand_pow_optimization( + 4, base_req=lambda b: b.is_Function)])) + assert cc2(x**3 + sin(x)**3) == "pow(x, 3) + sin(x)*sin(x)*sin(x)" + + +def test_matsolve(): + n = Symbol('n', integer=True) + A = MatrixSymbol('A', n, n) + x = MatrixSymbol('x', n, 1) + + with assuming(Q.fullrank(A)): + assert optimize(A**(-1) * x, [matinv_opt]) == MatrixSolve(A, x) + assert optimize(A**(-1) * x + x, [matinv_opt]) == MatrixSolve(A, x) + x + + +def test_logaddexp_opt(): + x, y = map(Symbol, 'x y'.split()) + expr1 = log(exp(x) + exp(y)) + opt1 = optimize(expr1, [logaddexp_opt]) + assert logaddexp(x, y) - opt1 == 0 + assert logaddexp(y, x) - opt1 == 0 + assert opt1.rewrite(log) == expr1 + + +def test_logaddexp2_opt(): + x, y = map(Symbol, 'x y'.split()) + expr1 = log(2**x + 2**y)/log(2) + opt1 = optimize(expr1, [logaddexp2_opt]) + assert logaddexp2(x, y) - opt1 == 0 + assert logaddexp2(y, x) - opt1 == 0 + assert opt1.rewrite(log) == expr1 + + +def test_sinc_opts(): + def check(d): + for k, v in d.items(): + assert optimize(k, sinc_opts) == v + + x = Symbol('x') + check({ + sin(x)/x : sinc(x), + sin(2*x)/(2*x) : sinc(2*x), + sin(3*x)/x : 3*sinc(3*x), + x*sin(x) : x*sin(x) + }) + + y = Symbol('y') + check({ + sin(x*y)/(x*y) : sinc(x*y), + y*sin(x/y)/x : sinc(x/y), + sin(sin(x))/sin(x) : sinc(sin(x)), + sin(3*sin(x))/sin(x) : 3*sinc(3*sin(x)), + sin(x)/y : sin(x)/y + }) + + +def test_optims_numpy(): + def check(d): + for k, v in d.items(): + assert optimize(k, optims_numpy) == v + + x = Symbol('x') + check({ + sin(2*x)/(2*x) + exp(2*x) - 1: sinc(2*x) + expm1(2*x), + log(x+3)/log(2) + log(x**2 + 1): log1p(x**2) + log2(x+3) + }) + + +@XFAIL # room for improvement, ideally this test case should pass. +def test_optims_numpy_TODO(): + def check(d): + for k, v in d.items(): + assert optimize(k, optims_numpy) == v + + x, y = map(Symbol, 'x y'.split()) + check({ + log(x*y)*sin(x*y)*log(x*y+1)/(log(2)*x*y): log2(x*y)*sinc(x*y)*log1p(x*y), + exp(x*sin(y)/y) - 1: expm1(x*sinc(y)) + }) + + +@may_xfail +def test_compiled_ccode_with_rewriting(): + if not cython: + skip("cython not installed.") + if not has_c(): + skip("No C compiler found.") + + x = Symbol('x') + about_two = 2**(58/S(117))*3**(97/S(117))*5**(4/S(39))*7**(92/S(117))/S(30)*pi + # about_two: 1.999999999999581826 + unchanged = 2*exp(x) - about_two + xval = S(10)**-11 + ref = unchanged.subs(x, xval).n(19) # 2.0418173913673213e-11 + + rewritten = optimize(2*exp(x) - about_two, [expm1_opt]) + + # Unfortunately, we need to call ``.n()`` on our expressions before we hand them + # to ``ccode``, and we need to request a large number of significant digits. + # In this test, results converged for double precision when the following number + # of significant digits were chosen: + NUMBER_OF_DIGITS = 25 # TODO: this should ideally be automatically handled. + + func_c = ''' +#include + +double func_unchanged(double x) { + return %(unchanged)s; +} +double func_rewritten(double x) { + return %(rewritten)s; +} +''' % {"unchanged": ccode(unchanged.n(NUMBER_OF_DIGITS)), + "rewritten": ccode(rewritten.n(NUMBER_OF_DIGITS))} + + func_pyx = ''' +#cython: language_level=3 +cdef extern double func_unchanged(double) +cdef extern double func_rewritten(double) +def py_unchanged(x): + return func_unchanged(x) +def py_rewritten(x): + return func_rewritten(x) +''' + with tempfile.TemporaryDirectory() as folder: + mod, info = compile_link_import_strings( + [('func.c', func_c), ('_func.pyx', func_pyx)], + build_dir=folder, compile_kwargs={"std": 'c99'} + ) + err_rewritten = abs(mod.py_rewritten(1e-11) - ref) + err_unchanged = abs(mod.py_unchanged(1e-11) - ref) + assert 1e-27 < err_rewritten < 1e-25 # highly accurate. + assert 1e-19 < err_unchanged < 1e-16 # quite poor. + + # Tolerances used above were determined as follows: + # >>> no_opt = unchanged.subs(x, xval.evalf()).evalf() + # >>> with_opt = rewritten.n(25).subs(x, 1e-11).evalf() + # >>> with_opt - ref, no_opt - ref + # (1.1536301877952077e-26, 1.6547074214222335e-18) diff --git a/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_scipy_nodes.py b/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_scipy_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..c0d1461037eec81ade0c99b18fbbf5a4517ce0b7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/codegen/tests/test_scipy_nodes.py @@ -0,0 +1,44 @@ +from itertools import product +from sympy.core.power import Pow +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.trigonometric import cos +from sympy.core.numbers import pi +from sympy.codegen.scipy_nodes import cosm1, powm1 + +x, y, z = symbols('x y z') + + +def test_cosm1(): + cm1_xy = cosm1(x*y) + ref_xy = cos(x*y) - 1 + for wrt, deriv_order in product([x, y, z], range(3)): + assert ( + cm1_xy.diff(wrt, deriv_order) - + ref_xy.diff(wrt, deriv_order) + ).rewrite(cos).simplify() == 0 + + expr_minus2 = cosm1(pi) + assert expr_minus2.rewrite(cos) == -2 + assert cosm1(3.14).simplify() == cosm1(3.14) # cannot simplify with 3.14 + assert cosm1(pi/2).simplify() == -1 + assert (1/cos(x) - 1 + cosm1(x)/cos(x)).simplify() == 0 + + +def test_powm1(): + cases = { + powm1(x, y): x**y - 1, + powm1(x*y, z): (x*y)**z - 1, + powm1(x, y*z): x**(y*z)-1, + powm1(x*y*z, x*y*z): (x*y*z)**(x*y*z)-1 + } + for pm1_e, ref_e in cases.items(): + for wrt, deriv_order in product([x, y, z], range(3)): + der = pm1_e.diff(wrt, deriv_order) + ref = ref_e.diff(wrt, deriv_order) + delta = (der - ref).rewrite(Pow) + assert delta.simplify() == 0 + + eulers_constant_m1 = powm1(x, 1/log(x)) + assert eulers_constant_m1.rewrite(Pow) == exp(1) - 1 + assert eulers_constant_m1.simplify() == exp(1) - 1 diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/__init__.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..220c27a42975555f8fe7770fcef0dd18475c89e1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/__init__.py @@ -0,0 +1,43 @@ +from sympy.combinatorics.permutations import Permutation, Cycle +from sympy.combinatorics.prufer import Prufer +from sympy.combinatorics.generators import cyclic, alternating, symmetric, dihedral +from sympy.combinatorics.subsets import Subset +from sympy.combinatorics.partitions import (Partition, IntegerPartition, + RGS_rank, RGS_unrank, RGS_enum) +from sympy.combinatorics.polyhedron import (Polyhedron, tetrahedron, cube, + octahedron, dodecahedron, icosahedron) +from sympy.combinatorics.perm_groups import PermutationGroup, Coset, SymmetricPermutationGroup +from sympy.combinatorics.group_constructs import DirectProduct +from sympy.combinatorics.graycode import GrayCode +from sympy.combinatorics.named_groups import (SymmetricGroup, DihedralGroup, + CyclicGroup, AlternatingGroup, AbelianGroup, RubikGroup) +from sympy.combinatorics.pc_groups import PolycyclicGroup, Collector +from sympy.combinatorics.free_groups import free_group + +__all__ = [ + 'Permutation', 'Cycle', + + 'Prufer', + + 'cyclic', 'alternating', 'symmetric', 'dihedral', + + 'Subset', + + 'Partition', 'IntegerPartition', 'RGS_rank', 'RGS_unrank', 'RGS_enum', + + 'Polyhedron', 'tetrahedron', 'cube', 'octahedron', 'dodecahedron', + 'icosahedron', + + 'PermutationGroup', 'Coset', 'SymmetricPermutationGroup', + + 'DirectProduct', + + 'GrayCode', + + 'SymmetricGroup', 'DihedralGroup', 'CyclicGroup', 'AlternatingGroup', + 'AbelianGroup', 'RubikGroup', + + 'PolycyclicGroup', 'Collector', + + 'free_group', +] diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/coset_table.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/coset_table.py new file mode 100644 index 0000000000000000000000000000000000000000..0687aa34ec70e9062f65ff6413213848cf03e51b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/coset_table.py @@ -0,0 +1,1259 @@ +from sympy.combinatorics.free_groups import free_group +from sympy.printing.defaults import DefaultPrinting + +from itertools import chain, product +from bisect import bisect_left + + +############################################################################### +# COSET TABLE # +############################################################################### + +class CosetTable(DefaultPrinting): + # coset_table: Mathematically a coset table + # represented using a list of lists + # alpha: Mathematically a coset (precisely, a live coset) + # represented by an integer between i with 1 <= i <= n + # alpha in c + # x: Mathematically an element of "A" (set of generators and + # their inverses), represented using "FpGroupElement" + # fp_grp: Finitely Presented Group with < X|R > as presentation. + # H: subgroup of fp_grp. + # NOTE: We start with H as being only a list of words in generators + # of "fp_grp". Since `.subgroup` method has not been implemented. + + r""" + + Properties + ========== + + [1] `0 \in \Omega` and `\tau(1) = \epsilon` + [2] `\alpha^x = \beta \Leftrightarrow \beta^{x^{-1}} = \alpha` + [3] If `\alpha^x = \beta`, then `H \tau(\alpha)x = H \tau(\beta)` + [4] `\forall \alpha \in \Omega, 1^{\tau(\alpha)} = \alpha` + + References + ========== + + .. [1] Holt, D., Eick, B., O'Brien, E. + "Handbook of Computational Group Theory" + + .. [2] John J. Cannon; Lucien A. Dimino; George Havas; Jane M. Watson + Mathematics of Computation, Vol. 27, No. 123. (Jul., 1973), pp. 463-490. + "Implementation and Analysis of the Todd-Coxeter Algorithm" + + """ + # default limit for the number of cosets allowed in a + # coset enumeration. + coset_table_max_limit = 4096000 + # limit for the current instance + coset_table_limit = None + # maximum size of deduction stack above or equal to + # which it is emptied + max_stack_size = 100 + + def __init__(self, fp_grp, subgroup, max_cosets=None): + if not max_cosets: + max_cosets = CosetTable.coset_table_max_limit + self.fp_group = fp_grp + self.subgroup = subgroup + self.coset_table_limit = max_cosets + # "p" is setup independent of Omega and n + self.p = [0] + # a list of the form `[gen_1, gen_1^{-1}, ... , gen_k, gen_k^{-1}]` + self.A = list(chain.from_iterable((gen, gen**-1) \ + for gen in self.fp_group.generators)) + #P[alpha, x] Only defined when alpha^x is defined. + self.P = [[None]*len(self.A)] + # the mathematical coset table which is a list of lists + self.table = [[None]*len(self.A)] + self.A_dict = {x: self.A.index(x) for x in self.A} + self.A_dict_inv = {} + for x, index in self.A_dict.items(): + if index % 2 == 0: + self.A_dict_inv[x] = self.A_dict[x] + 1 + else: + self.A_dict_inv[x] = self.A_dict[x] - 1 + # used in the coset-table based method of coset enumeration. Each of + # the element is called a "deduction" which is the form (alpha, x) whenever + # a value is assigned to alpha^x during a definition or "deduction process" + self.deduction_stack = [] + # Attributes for modified methods. + H = self.subgroup + self._grp = free_group(', ' .join(["a_%d" % i for i in range(len(H))]))[0] + self.P = [[None]*len(self.A)] + self.p_p = {} + + @property + def omega(self): + """Set of live cosets. """ + return [coset for coset in range(len(self.p)) if self.p[coset] == coset] + + def copy(self): + """ + Return a shallow copy of Coset Table instance ``self``. + + """ + self_copy = self.__class__(self.fp_group, self.subgroup) + self_copy.table = [list(perm_rep) for perm_rep in self.table] + self_copy.p = list(self.p) + self_copy.deduction_stack = list(self.deduction_stack) + return self_copy + + def __str__(self): + return "Coset Table on %s with %s as subgroup generators" \ + % (self.fp_group, self.subgroup) + + __repr__ = __str__ + + @property + def n(self): + """The number `n` represents the length of the sublist containing the + live cosets. + + """ + if not self.table: + return 0 + return max(self.omega) + 1 + + # Pg. 152 [1] + def is_complete(self): + r""" + The coset table is called complete if it has no undefined entries + on the live cosets; that is, `\alpha^x` is defined for all + `\alpha \in \Omega` and `x \in A`. + + """ + return not any(None in self.table[coset] for coset in self.omega) + + # Pg. 153 [1] + def define(self, alpha, x, modified=False): + r""" + This routine is used in the relator-based strategy of Todd-Coxeter + algorithm if some `\alpha^x` is undefined. We check whether there is + space available for defining a new coset. If there is enough space + then we remedy this by adjoining a new coset `\beta` to `\Omega` + (i.e to set of live cosets) and put that equal to `\alpha^x`, then + make an assignment satisfying Property[1]. If there is not enough space + then we halt the Coset Table creation. The maximum amount of space that + can be used by Coset Table can be manipulated using the class variable + ``CosetTable.coset_table_max_limit``. + + See Also + ======== + + define_c + + """ + A = self.A + table = self.table + len_table = len(table) + if len_table >= self.coset_table_limit: + # abort the further generation of cosets + raise ValueError("the coset enumeration has defined more than " + "%s cosets. Try with a greater value max number of cosets " + % self.coset_table_limit) + table.append([None]*len(A)) + self.P.append([None]*len(self.A)) + # beta is the new coset generated + beta = len_table + self.p.append(beta) + table[alpha][self.A_dict[x]] = beta + table[beta][self.A_dict_inv[x]] = alpha + # P[alpha][x] = epsilon, P[beta][x**-1] = epsilon + if modified: + self.P[alpha][self.A_dict[x]] = self._grp.identity + self.P[beta][self.A_dict_inv[x]] = self._grp.identity + self.p_p[beta] = self._grp.identity + + def define_c(self, alpha, x): + r""" + A variation of ``define`` routine, described on Pg. 165 [1], used in + the coset table-based strategy of Todd-Coxeter algorithm. It differs + from ``define`` routine in that for each definition it also adds the + tuple `(\alpha, x)` to the deduction stack. + + See Also + ======== + + define + + """ + A = self.A + table = self.table + len_table = len(table) + if len_table >= self.coset_table_limit: + # abort the further generation of cosets + raise ValueError("the coset enumeration has defined more than " + "%s cosets. Try with a greater value max number of cosets " + % self.coset_table_limit) + table.append([None]*len(A)) + # beta is the new coset generated + beta = len_table + self.p.append(beta) + table[alpha][self.A_dict[x]] = beta + table[beta][self.A_dict_inv[x]] = alpha + # append to deduction stack + self.deduction_stack.append((alpha, x)) + + def scan_c(self, alpha, word): + """ + A variation of ``scan`` routine, described on pg. 165 of [1], which + puts at tuple, whenever a deduction occurs, to deduction stack. + + See Also + ======== + + scan, scan_check, scan_and_fill, scan_and_fill_c + + """ + # alpha is an integer representing a "coset" + # since scanning can be in two cases + # 1. for alpha=0 and w in Y (i.e generating set of H) + # 2. alpha in Omega (set of live cosets), w in R (relators) + A_dict = self.A_dict + A_dict_inv = self.A_dict_inv + table = self.table + f = alpha + i = 0 + r = len(word) + b = alpha + j = r - 1 + # list of union of generators and their inverses + while i <= j and table[f][A_dict[word[i]]] is not None: + f = table[f][A_dict[word[i]]] + i += 1 + if i > j: + if f != b: + self.coincidence_c(f, b) + return + while j >= i and table[b][A_dict_inv[word[j]]] is not None: + b = table[b][A_dict_inv[word[j]]] + j -= 1 + if j < i: + # we have an incorrect completed scan with coincidence f ~ b + # run the "coincidence" routine + self.coincidence_c(f, b) + elif j == i: + # deduction process + table[f][A_dict[word[i]]] = b + table[b][A_dict_inv[word[i]]] = f + self.deduction_stack.append((f, word[i])) + # otherwise scan is incomplete and yields no information + + # alpha, beta coincide, i.e. alpha, beta represent the pair of cosets where + # coincidence occurs + def coincidence_c(self, alpha, beta): + """ + A variation of ``coincidence`` routine used in the coset-table based + method of coset enumeration. The only difference being on addition of + a new coset in coset table(i.e new coset introduction), then it is + appended to ``deduction_stack``. + + See Also + ======== + + coincidence + + """ + A_dict = self.A_dict + A_dict_inv = self.A_dict_inv + table = self.table + # behaves as a queue + q = [] + self.merge(alpha, beta, q) + while len(q) > 0: + gamma = q.pop(0) + for x in A_dict: + delta = table[gamma][A_dict[x]] + if delta is not None: + table[delta][A_dict_inv[x]] = None + # only line of difference from ``coincidence`` routine + self.deduction_stack.append((delta, x**-1)) + mu = self.rep(gamma) + nu = self.rep(delta) + if table[mu][A_dict[x]] is not None: + self.merge(nu, table[mu][A_dict[x]], q) + elif table[nu][A_dict_inv[x]] is not None: + self.merge(mu, table[nu][A_dict_inv[x]], q) + else: + table[mu][A_dict[x]] = nu + table[nu][A_dict_inv[x]] = mu + + def scan(self, alpha, word, y=None, fill=False, modified=False): + r""" + ``scan`` performs a scanning process on the input ``word``. + It first locates the largest prefix ``s`` of ``word`` for which + `\alpha^s` is defined (i.e is not ``None``), ``s`` may be empty. Let + ``word=sv``, let ``t`` be the longest suffix of ``v`` for which + `\alpha^{t^{-1}}` is defined, and let ``v=ut``. Then three + possibilities are there: + + 1. If ``t=v``, then we say that the scan completes, and if, in addition + `\alpha^s = \alpha^{t^{-1}}`, then we say that the scan completes + correctly. + + 2. It can also happen that scan does not complete, but `|u|=1`; that + is, the word ``u`` consists of a single generator `x \in A`. In that + case, if `\alpha^s = \beta` and `\alpha^{t^{-1}} = \gamma`, then we can + set `\beta^x = \gamma` and `\gamma^{x^{-1}} = \beta`. These assignments + are known as deductions and enable the scan to complete correctly. + + 3. See ``coicidence`` routine for explanation of third condition. + + Notes + ===== + + The code for the procedure of scanning `\alpha \in \Omega` + under `w \in A*` is defined on pg. 155 [1] + + See Also + ======== + + scan_c, scan_check, scan_and_fill, scan_and_fill_c + + Scan and Fill + ============= + + Performed when the default argument fill=True. + + Modified Scan + ============= + + Performed when the default argument modified=True + + """ + # alpha is an integer representing a "coset" + # since scanning can be in two cases + # 1. for alpha=0 and w in Y (i.e generating set of H) + # 2. alpha in Omega (set of live cosets), w in R (relators) + A_dict = self.A_dict + A_dict_inv = self.A_dict_inv + table = self.table + f = alpha + i = 0 + r = len(word) + b = alpha + j = r - 1 + b_p = y + if modified: + f_p = self._grp.identity + flag = 0 + while fill or flag == 0: + flag = 1 + while i <= j and table[f][A_dict[word[i]]] is not None: + if modified: + f_p = f_p*self.P[f][A_dict[word[i]]] + f = table[f][A_dict[word[i]]] + i += 1 + if i > j: + if f != b: + if modified: + self.modified_coincidence(f, b, f_p**-1*y) + else: + self.coincidence(f, b) + return + while j >= i and table[b][A_dict_inv[word[j]]] is not None: + if modified: + b_p = b_p*self.P[b][self.A_dict_inv[word[j]]] + b = table[b][A_dict_inv[word[j]]] + j -= 1 + if j < i: + # we have an incorrect completed scan with coincidence f ~ b + # run the "coincidence" routine + if modified: + self.modified_coincidence(f, b, f_p**-1*b_p) + else: + self.coincidence(f, b) + elif j == i: + # deduction process + table[f][A_dict[word[i]]] = b + table[b][A_dict_inv[word[i]]] = f + if modified: + self.P[f][self.A_dict[word[i]]] = f_p**-1*b_p + self.P[b][self.A_dict_inv[word[i]]] = b_p**-1*f_p + return + elif fill: + self.define(f, word[i], modified=modified) + # otherwise scan is incomplete and yields no information + + # used in the low-index subgroups algorithm + def scan_check(self, alpha, word): + r""" + Another version of ``scan`` routine, described on, it checks whether + `\alpha` scans correctly under `word`, it is a straightforward + modification of ``scan``. ``scan_check`` returns ``False`` (rather than + calling ``coincidence``) if the scan completes incorrectly; otherwise + it returns ``True``. + + See Also + ======== + + scan, scan_c, scan_and_fill, scan_and_fill_c + + """ + # alpha is an integer representing a "coset" + # since scanning can be in two cases + # 1. for alpha=0 and w in Y (i.e generating set of H) + # 2. alpha in Omega (set of live cosets), w in R (relators) + A_dict = self.A_dict + A_dict_inv = self.A_dict_inv + table = self.table + f = alpha + i = 0 + r = len(word) + b = alpha + j = r - 1 + while i <= j and table[f][A_dict[word[i]]] is not None: + f = table[f][A_dict[word[i]]] + i += 1 + if i > j: + return f == b + while j >= i and table[b][A_dict_inv[word[j]]] is not None: + b = table[b][A_dict_inv[word[j]]] + j -= 1 + if j < i: + # we have an incorrect completed scan with coincidence f ~ b + # return False, instead of calling coincidence routine + return False + elif j == i: + # deduction process + table[f][A_dict[word[i]]] = b + table[b][A_dict_inv[word[i]]] = f + return True + + def merge(self, k, lamda, q, w=None, modified=False): + """ + Merge two classes with representatives ``k`` and ``lamda``, described + on Pg. 157 [1] (for pseudocode), start by putting ``p[k] = lamda``. + It is more efficient to choose the new representative from the larger + of the two classes being merged, i.e larger among ``k`` and ``lamda``. + procedure ``merge`` performs the merging operation, adds the deleted + class representative to the queue ``q``. + + Parameters + ========== + + 'k', 'lamda' being the two class representatives to be merged. + + Notes + ===== + + Pg. 86-87 [1] contains a description of this method. + + See Also + ======== + + coincidence, rep + + """ + p = self.p + rep = self.rep + phi = rep(k, modified=modified) + psi = rep(lamda, modified=modified) + if phi != psi: + mu = min(phi, psi) + v = max(phi, psi) + p[v] = mu + if modified: + if v == phi: + self.p_p[phi] = self.p_p[k]**-1*w*self.p_p[lamda] + else: + self.p_p[psi] = self.p_p[lamda]**-1*w**-1*self.p_p[k] + q.append(v) + + def rep(self, k, modified=False): + r""" + Parameters + ========== + + `k \in [0 \ldots n-1]`, as for ``self`` only array ``p`` is used + + Returns + ======= + + Representative of the class containing ``k``. + + Returns the representative of `\sim` class containing ``k``, it also + makes some modification to array ``p`` of ``self`` to ease further + computations, described on Pg. 157 [1]. + + The information on classes under `\sim` is stored in array `p` of + ``self`` argument, which will always satisfy the property: + + `p[\alpha] \sim \alpha` and `p[\alpha]=\alpha \iff \alpha=rep(\alpha)` + `\forall \in [0 \ldots n-1]`. + + So, for `\alpha \in [0 \ldots n-1]`, we find `rep(self, \alpha)` by + continually replacing `\alpha` by `p[\alpha]` until it becomes + constant (i.e satisfies `p[\alpha] = \alpha`):w + + To increase the efficiency of later ``rep`` calculations, whenever we + find `rep(self, \alpha)=\beta`, we set + `p[\gamma] = \beta \forall \gamma \in p-chain` from `\alpha` to `\beta` + + Notes + ===== + + ``rep`` routine is also described on Pg. 85-87 [1] in Atkinson's + algorithm, this results from the fact that ``coincidence`` routine + introduces functionality similar to that introduced by the + ``minimal_block`` routine on Pg. 85-87 [1]. + + See Also + ======== + + coincidence, merge + + """ + p = self.p + lamda = k + rho = p[lamda] + if modified: + s = p[:] + while rho != lamda: + if modified: + s[rho] = lamda + lamda = rho + rho = p[lamda] + if modified: + rho = s[lamda] + while rho != k: + mu = rho + rho = s[mu] + p[rho] = lamda + self.p_p[rho] = self.p_p[rho]*self.p_p[mu] + else: + mu = k + rho = p[mu] + while rho != lamda: + p[mu] = lamda + mu = rho + rho = p[mu] + return lamda + + # alpha, beta coincide, i.e. alpha, beta represent the pair of cosets + # where coincidence occurs + def coincidence(self, alpha, beta, w=None, modified=False): + r""" + The third situation described in ``scan`` routine is handled by this + routine, described on Pg. 156-161 [1]. + + The unfortunate situation when the scan completes but not correctly, + then ``coincidence`` routine is run. i.e when for some `i` with + `1 \le i \le r+1`, we have `w=st` with `s = x_1 x_2 \dots x_{i-1}`, + `t = x_i x_{i+1} \dots x_r`, and `\beta = \alpha^s` and + `\gamma = \alpha^{t-1}` are defined but unequal. This means that + `\beta` and `\gamma` represent the same coset of `H` in `G`. Described + on Pg. 156 [1]. ``rep`` + + See Also + ======== + + scan + + """ + A_dict = self.A_dict + A_dict_inv = self.A_dict_inv + table = self.table + # behaves as a queue + q = [] + if modified: + self.modified_merge(alpha, beta, w, q) + else: + self.merge(alpha, beta, q) + while len(q) > 0: + gamma = q.pop(0) + for x in A_dict: + delta = table[gamma][A_dict[x]] + if delta is not None: + table[delta][A_dict_inv[x]] = None + mu = self.rep(gamma, modified=modified) + nu = self.rep(delta, modified=modified) + if table[mu][A_dict[x]] is not None: + if modified: + v = self.p_p[delta]**-1*self.P[gamma][self.A_dict[x]]**-1 + v = v*self.p_p[gamma]*self.P[mu][self.A_dict[x]] + self.modified_merge(nu, table[mu][self.A_dict[x]], v, q) + else: + self.merge(nu, table[mu][A_dict[x]], q) + elif table[nu][A_dict_inv[x]] is not None: + if modified: + v = self.p_p[gamma]**-1*self.P[gamma][self.A_dict[x]] + v = v*self.p_p[delta]*self.P[mu][self.A_dict_inv[x]] + self.modified_merge(mu, table[nu][self.A_dict_inv[x]], v, q) + else: + self.merge(mu, table[nu][A_dict_inv[x]], q) + else: + table[mu][A_dict[x]] = nu + table[nu][A_dict_inv[x]] = mu + if modified: + v = self.p_p[gamma]**-1*self.P[gamma][self.A_dict[x]]*self.p_p[delta] + self.P[mu][self.A_dict[x]] = v + self.P[nu][self.A_dict_inv[x]] = v**-1 + + # method used in the HLT strategy + def scan_and_fill(self, alpha, word): + """ + A modified version of ``scan`` routine used in the relator-based + method of coset enumeration, described on pg. 162-163 [1], which + follows the idea that whenever the procedure is called and the scan + is incomplete then it makes new definitions to enable the scan to + complete; i.e it fills in the gaps in the scan of the relator or + subgroup generator. + + """ + self.scan(alpha, word, fill=True) + + def scan_and_fill_c(self, alpha, word): + """ + A modified version of ``scan`` routine, described on Pg. 165 second + para. [1], with modification similar to that of ``scan_anf_fill`` the + only difference being it calls the coincidence procedure used in the + coset-table based method i.e. the routine ``coincidence_c`` is used. + + See Also + ======== + + scan, scan_and_fill + + """ + A_dict = self.A_dict + A_dict_inv = self.A_dict_inv + table = self.table + r = len(word) + f = alpha + i = 0 + b = alpha + j = r - 1 + # loop until it has filled the alpha row in the table. + while True: + # do the forward scanning + while i <= j and table[f][A_dict[word[i]]] is not None: + f = table[f][A_dict[word[i]]] + i += 1 + if i > j: + if f != b: + self.coincidence_c(f, b) + return + # forward scan was incomplete, scan backwards + while j >= i and table[b][A_dict_inv[word[j]]] is not None: + b = table[b][A_dict_inv[word[j]]] + j -= 1 + if j < i: + self.coincidence_c(f, b) + elif j == i: + table[f][A_dict[word[i]]] = b + table[b][A_dict_inv[word[i]]] = f + self.deduction_stack.append((f, word[i])) + else: + self.define_c(f, word[i]) + + # method used in the HLT strategy + def look_ahead(self): + """ + When combined with the HLT method this is known as HLT+Lookahead + method of coset enumeration, described on pg. 164 [1]. Whenever + ``define`` aborts due to lack of space available this procedure is + executed. This routine helps in recovering space resulting from + "coincidence" of cosets. + + """ + R = self.fp_group.relators + p = self.p + # complete scan all relators under all cosets(obviously live) + # without making new definitions + for beta in self.omega: + for w in R: + self.scan(beta, w) + if p[beta] < beta: + break + + # Pg. 166 + def process_deductions(self, R_c_x, R_c_x_inv): + """ + Processes the deductions that have been pushed onto ``deduction_stack``, + described on Pg. 166 [1] and is used in coset-table based enumeration. + + See Also + ======== + + deduction_stack + + """ + p = self.p + table = self.table + while len(self.deduction_stack) > 0: + if len(self.deduction_stack) >= CosetTable.max_stack_size: + self.look_ahead() + del self.deduction_stack[:] + continue + else: + alpha, x = self.deduction_stack.pop() + if p[alpha] == alpha: + for w in R_c_x: + self.scan_c(alpha, w) + if p[alpha] < alpha: + break + beta = table[alpha][self.A_dict[x]] + if beta is not None and p[beta] == beta: + for w in R_c_x_inv: + self.scan_c(beta, w) + if p[beta] < beta: + break + + def process_deductions_check(self, R_c_x, R_c_x_inv): + """ + A variation of ``process_deductions``, this calls ``scan_check`` + wherever ``process_deductions`` calls ``scan``, described on Pg. [1]. + + See Also + ======== + + process_deductions + + """ + table = self.table + while len(self.deduction_stack) > 0: + alpha, x = self.deduction_stack.pop() + if not all(self.scan_check(alpha, w) for w in R_c_x): + return False + beta = table[alpha][self.A_dict[x]] + if beta is not None: + if not all(self.scan_check(beta, w) for w in R_c_x_inv): + return False + return True + + def switch(self, beta, gamma): + r"""Switch the elements `\beta, \gamma \in \Omega` of ``self``, used + by the ``standardize`` procedure, described on Pg. 167 [1]. + + See Also + ======== + + standardize + + """ + A = self.A + A_dict = self.A_dict + table = self.table + for x in A: + z = table[gamma][A_dict[x]] + table[gamma][A_dict[x]] = table[beta][A_dict[x]] + table[beta][A_dict[x]] = z + for alpha in range(len(self.p)): + if self.p[alpha] == alpha: + if table[alpha][A_dict[x]] == beta: + table[alpha][A_dict[x]] = gamma + elif table[alpha][A_dict[x]] == gamma: + table[alpha][A_dict[x]] = beta + + def standardize(self): + r""" + A coset table is standardized if when running through the cosets and + within each coset through the generator images (ignoring generator + inverses), the cosets appear in order of the integers + `0, 1, \dots, n`. "Standardize" reorders the elements of `\Omega` + such that, if we scan the coset table first by elements of `\Omega` + and then by elements of A, then the cosets occur in ascending order. + ``standardize()`` is used at the end of an enumeration to permute the + cosets so that they occur in some sort of standard order. + + Notes + ===== + + procedure is described on pg. 167-168 [1], it also makes use of the + ``switch`` routine to replace by smaller integer value. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> from sympy.combinatorics.fp_groups import FpGroup, coset_enumeration_r + >>> F, x, y = free_group("x, y") + + # Example 5.3 from [1] + >>> f = FpGroup(F, [x**2*y**2, x**3*y**5]) + >>> C = coset_enumeration_r(f, []) + >>> C.compress() + >>> C.table + [[1, 3, 1, 3], [2, 0, 2, 0], [3, 1, 3, 1], [0, 2, 0, 2]] + >>> C.standardize() + >>> C.table + [[1, 2, 1, 2], [3, 0, 3, 0], [0, 3, 0, 3], [2, 1, 2, 1]] + + """ + A = self.A + A_dict = self.A_dict + gamma = 1 + for alpha, x in product(range(self.n), A): + beta = self.table[alpha][A_dict[x]] + if beta >= gamma: + if beta > gamma: + self.switch(gamma, beta) + gamma += 1 + if gamma == self.n: + return + + # Compression of a Coset Table + def compress(self): + """Removes the non-live cosets from the coset table, described on + pg. 167 [1]. + + """ + gamma = -1 + A = self.A + A_dict = self.A_dict + A_dict_inv = self.A_dict_inv + table = self.table + chi = tuple([i for i in range(len(self.p)) if self.p[i] != i]) + for alpha in self.omega: + gamma += 1 + if gamma != alpha: + # replace alpha by gamma in coset table + for x in A: + beta = table[alpha][A_dict[x]] + table[gamma][A_dict[x]] = beta + # XXX: The line below uses == rather than = which means + # that it has no effect. It is not clear though if it is + # correct simply to delete the line or to change it to + # use =. Changing it causes some tests to fail. + # + # https://github.com/sympy/sympy/issues/27633 + table[beta][A_dict_inv[x]] == gamma # noqa: B015 + # all the cosets in the table are live cosets + self.p = list(range(gamma + 1)) + # delete the useless columns + del table[len(self.p):] + # re-define values + for row in table: + for j in range(len(self.A)): + row[j] -= bisect_left(chi, row[j]) + + def conjugates(self, R): + R_c = list(chain.from_iterable((rel.cyclic_conjugates(), \ + (rel**-1).cyclic_conjugates()) for rel in R)) + R_set = set() + for conjugate in R_c: + R_set = R_set.union(conjugate) + R_c_list = [] + for x in self.A: + r = {word for word in R_set if word[0] == x} + R_c_list.append(r) + R_set.difference_update(r) + return R_c_list + + def coset_representative(self, coset): + ''' + Compute the coset representative of a given coset. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> from sympy.combinatorics.fp_groups import FpGroup, coset_enumeration_r + >>> F, x, y = free_group("x, y") + >>> f = FpGroup(F, [x**3, y**3, x**-1*y**-1*x*y]) + >>> C = coset_enumeration_r(f, [x]) + >>> C.compress() + >>> C.table + [[0, 0, 1, 2], [1, 1, 2, 0], [2, 2, 0, 1]] + >>> C.coset_representative(0) + + >>> C.coset_representative(1) + y + >>> C.coset_representative(2) + y**-1 + + ''' + for x in self.A: + gamma = self.table[coset][self.A_dict[x]] + if coset == 0: + return self.fp_group.identity + if gamma < coset: + return self.coset_representative(gamma)*x**-1 + + ############################## + # Modified Methods # + ############################## + + def modified_define(self, alpha, x): + r""" + Define a function p_p from from [1..n] to A* as + an additional component of the modified coset table. + + Parameters + ========== + + \alpha \in \Omega + x \in A* + + See Also + ======== + + define + + """ + self.define(alpha, x, modified=True) + + def modified_scan(self, alpha, w, y, fill=False): + r""" + Parameters + ========== + \alpha \in \Omega + w \in A* + y \in (YUY^-1) + fill -- `modified_scan_and_fill` when set to True. + + See Also + ======== + + scan + """ + self.scan(alpha, w, y=y, fill=fill, modified=True) + + def modified_scan_and_fill(self, alpha, w, y): + self.modified_scan(alpha, w, y, fill=True) + + def modified_merge(self, k, lamda, w, q): + r""" + Parameters + ========== + + 'k', 'lamda' -- the two class representatives to be merged. + q -- queue of length l of elements to be deleted from `\Omega` *. + w -- Word in (YUY^-1) + + See Also + ======== + + merge + """ + self.merge(k, lamda, q, w=w, modified=True) + + def modified_rep(self, k): + r""" + Parameters + ========== + + `k \in [0 \ldots n-1]` + + See Also + ======== + + rep + """ + self.rep(k, modified=True) + + def modified_coincidence(self, alpha, beta, w): + r""" + Parameters + ========== + + A coincident pair `\alpha, \beta \in \Omega, w \in Y \cup Y^{-1}` + + See Also + ======== + + coincidence + + """ + self.coincidence(alpha, beta, w=w, modified=True) + +############################################################################### +# COSET ENUMERATION # +############################################################################### + +# relator-based method +def coset_enumeration_r(fp_grp, Y, max_cosets=None, draft=None, + incomplete=False, modified=False): + """ + This is easier of the two implemented methods of coset enumeration. + and is often called the HLT method, after Hazelgrove, Leech, Trotter + The idea is that we make use of ``scan_and_fill`` makes new definitions + whenever the scan is incomplete to enable the scan to complete; this way + we fill in the gaps in the scan of the relator or subgroup generator, + that's why the name relator-based method. + + An instance of `CosetTable` for `fp_grp` can be passed as the keyword + argument `draft` in which case the coset enumeration will start with + that instance and attempt to complete it. + + When `incomplete` is `True` and the function is unable to complete for + some reason, the partially complete table will be returned. + + # TODO: complete the docstring + + See Also + ======== + + scan_and_fill, + + Examples + ======== + + >>> from sympy.combinatorics.free_groups import free_group + >>> from sympy.combinatorics.fp_groups import FpGroup, coset_enumeration_r + >>> F, x, y = free_group("x, y") + + # Example 5.1 from [1] + >>> f = FpGroup(F, [x**3, y**3, x**-1*y**-1*x*y]) + >>> C = coset_enumeration_r(f, [x]) + >>> for i in range(len(C.p)): + ... if C.p[i] == i: + ... print(C.table[i]) + [0, 0, 1, 2] + [1, 1, 2, 0] + [2, 2, 0, 1] + >>> C.p + [0, 1, 2, 1, 1] + + # Example from exercises Q2 [1] + >>> f = FpGroup(F, [x**2*y**2, y**-1*x*y*x**-3]) + >>> C = coset_enumeration_r(f, []) + >>> C.compress(); C.standardize() + >>> C.table + [[1, 2, 3, 4], + [5, 0, 6, 7], + [0, 5, 7, 6], + [7, 6, 5, 0], + [6, 7, 0, 5], + [2, 1, 4, 3], + [3, 4, 2, 1], + [4, 3, 1, 2]] + + # Example 5.2 + >>> f = FpGroup(F, [x**2, y**3, (x*y)**3]) + >>> Y = [x*y] + >>> C = coset_enumeration_r(f, Y) + >>> for i in range(len(C.p)): + ... if C.p[i] == i: + ... print(C.table[i]) + [1, 1, 2, 1] + [0, 0, 0, 2] + [3, 3, 1, 0] + [2, 2, 3, 3] + + # Example 5.3 + >>> f = FpGroup(F, [x**2*y**2, x**3*y**5]) + >>> Y = [] + >>> C = coset_enumeration_r(f, Y) + >>> for i in range(len(C.p)): + ... if C.p[i] == i: + ... print(C.table[i]) + [1, 3, 1, 3] + [2, 0, 2, 0] + [3, 1, 3, 1] + [0, 2, 0, 2] + + # Example 5.4 + >>> F, a, b, c, d, e = free_group("a, b, c, d, e") + >>> f = FpGroup(F, [a*b*c**-1, b*c*d**-1, c*d*e**-1, d*e*a**-1, e*a*b**-1]) + >>> Y = [a] + >>> C = coset_enumeration_r(f, Y) + >>> for i in range(len(C.p)): + ... if C.p[i] == i: + ... print(C.table[i]) + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + # example of "compress" method + >>> C.compress() + >>> C.table + [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] + + # Exercises Pg. 161, Q2. + >>> F, x, y = free_group("x, y") + >>> f = FpGroup(F, [x**2*y**2, y**-1*x*y*x**-3]) + >>> Y = [] + >>> C = coset_enumeration_r(f, Y) + >>> C.compress() + >>> C.standardize() + >>> C.table + [[1, 2, 3, 4], + [5, 0, 6, 7], + [0, 5, 7, 6], + [7, 6, 5, 0], + [6, 7, 0, 5], + [2, 1, 4, 3], + [3, 4, 2, 1], + [4, 3, 1, 2]] + + # John J. Cannon; Lucien A. Dimino; George Havas; Jane M. Watson + # Mathematics of Computation, Vol. 27, No. 123. (Jul., 1973), pp. 463-490 + # from 1973chwd.pdf + # Table 1. Ex. 1 + >>> F, r, s, t = free_group("r, s, t") + >>> E1 = FpGroup(F, [t**-1*r*t*r**-2, r**-1*s*r*s**-2, s**-1*t*s*t**-2]) + >>> C = coset_enumeration_r(E1, [r]) + >>> for i in range(len(C.p)): + ... if C.p[i] == i: + ... print(C.table[i]) + [0, 0, 0, 0, 0, 0] + + Ex. 2 + >>> F, a, b = free_group("a, b") + >>> Cox = FpGroup(F, [a**6, b**6, (a*b)**2, (a**2*b**2)**2, (a**3*b**3)**5]) + >>> C = coset_enumeration_r(Cox, [a]) + >>> index = 0 + >>> for i in range(len(C.p)): + ... if C.p[i] == i: + ... index += 1 + >>> index + 500 + + # Ex. 3 + >>> F, a, b = free_group("a, b") + >>> B_2_4 = FpGroup(F, [a**4, b**4, (a*b)**4, (a**-1*b)**4, (a**2*b)**4, \ + (a*b**2)**4, (a**2*b**2)**4, (a**-1*b*a*b)**4, (a*b**-1*a*b)**4]) + >>> C = coset_enumeration_r(B_2_4, [a]) + >>> index = 0 + >>> for i in range(len(C.p)): + ... if C.p[i] == i: + ... index += 1 + >>> index + 1024 + + References + ========== + + .. [1] Holt, D., Eick, B., O'Brien, E. + "Handbook of computational group theory" + + """ + # 1. Initialize a coset table C for < X|R > + C = CosetTable(fp_grp, Y, max_cosets=max_cosets) + # Define coset table methods. + if modified: + _scan_and_fill = C.modified_scan_and_fill + _define = C.modified_define + else: + _scan_and_fill = C.scan_and_fill + _define = C.define + if draft: + C.table = draft.table[:] + C.p = draft.p[:] + R = fp_grp.relators + A_dict = C.A_dict + p = C.p + for i in range(len(Y)): + if modified: + _scan_and_fill(0, Y[i], C._grp.generators[i]) + else: + _scan_and_fill(0, Y[i]) + alpha = 0 + while alpha < C.n: + if p[alpha] == alpha: + try: + for w in R: + if modified: + _scan_and_fill(alpha, w, C._grp.identity) + else: + _scan_and_fill(alpha, w) + # if alpha was eliminated during the scan then break + if p[alpha] < alpha: + break + if p[alpha] == alpha: + for x in A_dict: + if C.table[alpha][A_dict[x]] is None: + _define(alpha, x) + except ValueError as e: + if incomplete: + return C + raise e + alpha += 1 + return C + +def modified_coset_enumeration_r(fp_grp, Y, max_cosets=None, draft=None, + incomplete=False): + r""" + Introduce a new set of symbols y \in Y that correspond to the + generators of the subgroup. Store the elements of Y as a + word P[\alpha, x] and compute the coset table similar to that of + the regular coset enumeration methods. + + Examples + ======== + + >>> from sympy.combinatorics.free_groups import free_group + >>> from sympy.combinatorics.fp_groups import FpGroup + >>> from sympy.combinatorics.coset_table import modified_coset_enumeration_r + >>> F, x, y = free_group("x, y") + >>> f = FpGroup(F, [x**3, y**3, x**-1*y**-1*x*y]) + >>> C = modified_coset_enumeration_r(f, [x]) + >>> C.table + [[0, 0, 1, 2], [1, 1, 2, 0], [2, 2, 0, 1], [None, 1, None, None], [1, 3, None, None]] + + See Also + ======== + + coset_enumertation_r + + References + ========== + + .. [1] Holt, D., Eick, B., O'Brien, E., + "Handbook of Computational Group Theory", + Section 5.3.2 + """ + return coset_enumeration_r(fp_grp, Y, max_cosets=max_cosets, draft=draft, + incomplete=incomplete, modified=True) + +# Pg. 166 +# coset-table based method +def coset_enumeration_c(fp_grp, Y, max_cosets=None, draft=None, + incomplete=False): + """ + >>> from sympy.combinatorics.free_groups import free_group + >>> from sympy.combinatorics.fp_groups import FpGroup, coset_enumeration_c + >>> F, x, y = free_group("x, y") + >>> f = FpGroup(F, [x**3, y**3, x**-1*y**-1*x*y]) + >>> C = coset_enumeration_c(f, [x]) + >>> C.table + [[0, 0, 1, 2], [1, 1, 2, 0], [2, 2, 0, 1]] + + """ + # Initialize a coset table C for < X|R > + X = fp_grp.generators + R = fp_grp.relators + C = CosetTable(fp_grp, Y, max_cosets=max_cosets) + if draft: + C.table = draft.table[:] + C.p = draft.p[:] + C.deduction_stack = draft.deduction_stack + for alpha, x in product(range(len(C.table)), X): + if C.table[alpha][C.A_dict[x]] is not None: + C.deduction_stack.append((alpha, x)) + A = C.A + # replace all the elements by cyclic reductions + R_cyc_red = [rel.identity_cyclic_reduction() for rel in R] + R_c = list(chain.from_iterable((rel.cyclic_conjugates(), (rel**-1).cyclic_conjugates()) \ + for rel in R_cyc_red)) + R_set = set() + for conjugate in R_c: + R_set = R_set.union(conjugate) + # a list of subsets of R_c whose words start with "x". + R_c_list = [] + for x in C.A: + r = {word for word in R_set if word[0] == x} + R_c_list.append(r) + R_set.difference_update(r) + for w in Y: + C.scan_and_fill_c(0, w) + for x in A: + C.process_deductions(R_c_list[C.A_dict[x]], R_c_list[C.A_dict_inv[x]]) + alpha = 0 + while alpha < len(C.table): + if C.p[alpha] == alpha: + try: + for x in C.A: + if C.p[alpha] != alpha: + break + if C.table[alpha][C.A_dict[x]] is None: + C.define_c(alpha, x) + C.process_deductions(R_c_list[C.A_dict[x]], R_c_list[C.A_dict_inv[x]]) + except ValueError as e: + if incomplete: + return C + raise e + alpha += 1 + return C diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/fp_groups.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/fp_groups.py new file mode 100644 index 0000000000000000000000000000000000000000..95530ccd44f025eca5f029c6ba60d3727cbcb29d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/fp_groups.py @@ -0,0 +1,1352 @@ +"""Finitely Presented Groups and its algorithms. """ + +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.combinatorics.free_groups import (FreeGroup, FreeGroupElement, + free_group) +from sympy.combinatorics.rewritingsystem import RewritingSystem +from sympy.combinatorics.coset_table import (CosetTable, + coset_enumeration_r, + coset_enumeration_c) +from sympy.combinatorics import PermutationGroup +from sympy.matrices.normalforms import invariant_factors +from sympy.matrices import Matrix +from sympy.polys.polytools import gcd +from sympy.printing.defaults import DefaultPrinting +from sympy.utilities import public +from sympy.utilities.magic import pollute + +from itertools import product + + +@public +def fp_group(fr_grp, relators=()): + _fp_group = FpGroup(fr_grp, relators) + return (_fp_group,) + tuple(_fp_group._generators) + +@public +def xfp_group(fr_grp, relators=()): + _fp_group = FpGroup(fr_grp, relators) + return (_fp_group, _fp_group._generators) + +# Does not work. Both symbols and pollute are undefined. Never tested. +@public +def vfp_group(fr_grpm, relators): + _fp_group = FpGroup(symbols, relators) + pollute([sym.name for sym in _fp_group.symbols], _fp_group.generators) + return _fp_group + + +def _parse_relators(rels): + """Parse the passed relators.""" + return rels + + +############################################################################### +# FINITELY PRESENTED GROUPS # +############################################################################### + + +class FpGroup(DefaultPrinting): + """ + The FpGroup would take a FreeGroup and a list/tuple of relators, the + relators would be specified in such a way that each of them be equal to the + identity of the provided free group. + + """ + is_group = True + is_FpGroup = True + is_PermutationGroup = False + + def __init__(self, fr_grp, relators): + relators = _parse_relators(relators) + self.free_group = fr_grp + self.relators = relators + self.generators = self._generators() + self.dtype = type("FpGroupElement", (FpGroupElement,), {"group": self}) + + # CosetTable instance on identity subgroup + self._coset_table = None + # returns whether coset table on identity subgroup + # has been standardized + self._is_standardized = False + + self._order = None + self._center = None + + self._rewriting_system = RewritingSystem(self) + self._perm_isomorphism = None + return + + def _generators(self): + return self.free_group.generators + + def make_confluent(self): + ''' + Try to make the group's rewriting system confluent + + ''' + self._rewriting_system.make_confluent() + return + + def reduce(self, word): + ''' + Return the reduced form of `word` in `self` according to the group's + rewriting system. If it's confluent, the reduced form is the unique normal + form of the word in the group. + + ''' + return self._rewriting_system.reduce(word) + + def equals(self, word1, word2): + ''' + Compare `word1` and `word2` for equality in the group + using the group's rewriting system. If the system is + confluent, the returned answer is necessarily correct. + (If it is not, `False` could be returned in some cases + where in fact `word1 == word2`) + + ''' + if self.reduce(word1*word2**-1) == self.identity: + return True + elif self._rewriting_system.is_confluent: + return False + return None + + @property + def identity(self): + return self.free_group.identity + + def __contains__(self, g): + return g in self.free_group + + def subgroup(self, gens, C=None, homomorphism=False): + ''' + Return the subgroup generated by `gens` using the + Reidemeister-Schreier algorithm + homomorphism -- When set to True, return a dictionary containing the images + of the presentation generators in the original group. + + Examples + ======== + + >>> from sympy.combinatorics.fp_groups import FpGroup + >>> from sympy.combinatorics import free_group + >>> F, x, y = free_group("x, y") + >>> f = FpGroup(F, [x**3, y**5, (x*y)**2]) + >>> H = [x*y, x**-1*y**-1*x*y*x] + >>> K, T = f.subgroup(H, homomorphism=True) + >>> T(K.generators) + [x*y, x**-1*y**2*x**-1] + + ''' + + if not all(isinstance(g, FreeGroupElement) for g in gens): + raise ValueError("Generators must be `FreeGroupElement`s") + if not all(g.group == self.free_group for g in gens): + raise ValueError("Given generators are not members of the group") + if homomorphism: + g, rels, _gens = reidemeister_presentation(self, gens, C=C, homomorphism=True) + else: + g, rels = reidemeister_presentation(self, gens, C=C) + if g: + g = FpGroup(g[0].group, rels) + else: + g = FpGroup(free_group('')[0], []) + if homomorphism: + from sympy.combinatorics.homomorphisms import homomorphism + return g, homomorphism(g, self, g.generators, _gens, check=False) + return g + + def coset_enumeration(self, H, strategy="relator_based", max_cosets=None, + draft=None, incomplete=False): + """ + Return an instance of ``coset table``, when Todd-Coxeter algorithm is + run over the ``self`` with ``H`` as subgroup, using ``strategy`` + argument as strategy. The returned coset table is compressed but not + standardized. + + An instance of `CosetTable` for `fp_grp` can be passed as the keyword + argument `draft` in which case the coset enumeration will start with + that instance and attempt to complete it. + + When `incomplete` is `True` and the function is unable to complete for + some reason, the partially complete table will be returned. + + """ + if not max_cosets: + max_cosets = CosetTable.coset_table_max_limit + if strategy == 'relator_based': + C = coset_enumeration_r(self, H, max_cosets=max_cosets, + draft=draft, incomplete=incomplete) + else: + C = coset_enumeration_c(self, H, max_cosets=max_cosets, + draft=draft, incomplete=incomplete) + if C.is_complete(): + C.compress() + return C + + def standardize_coset_table(self): + """ + Standardized the coset table ``self`` and makes the internal variable + ``_is_standardized`` equal to ``True``. + + """ + self._coset_table.standardize() + self._is_standardized = True + + def coset_table(self, H, strategy="relator_based", max_cosets=None, + draft=None, incomplete=False): + """ + Return the mathematical coset table of ``self`` in ``H``. + + """ + if not H: + if self._coset_table is not None: + if not self._is_standardized: + self.standardize_coset_table() + else: + C = self.coset_enumeration([], strategy, max_cosets=max_cosets, + draft=draft, incomplete=incomplete) + self._coset_table = C + self.standardize_coset_table() + return self._coset_table.table + else: + C = self.coset_enumeration(H, strategy, max_cosets=max_cosets, + draft=draft, incomplete=incomplete) + C.standardize() + return C.table + + def order(self, strategy="relator_based"): + """ + Returns the order of the finitely presented group ``self``. It uses + the coset enumeration with identity group as subgroup, i.e ``H=[]``. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> from sympy.combinatorics.fp_groups import FpGroup + >>> F, x, y = free_group("x, y") + >>> f = FpGroup(F, [x, y**2]) + >>> f.order(strategy="coset_table_based") + 2 + + """ + if self._order is not None: + return self._order + if self._coset_table is not None: + self._order = len(self._coset_table.table) + elif len(self.relators) == 0: + self._order = self.free_group.order() + elif len(self.generators) == 1: + self._order = abs(gcd([r.array_form[0][1] for r in self.relators])) + elif self._is_infinite(): + self._order = S.Infinity + else: + gens, C = self._finite_index_subgroup() + if C: + ind = len(C.table) + self._order = ind*self.subgroup(gens, C=C).order() + else: + self._order = self.index([]) + return self._order + + def _is_infinite(self): + ''' + Test if the group is infinite. Return `True` if the test succeeds + and `None` otherwise + + ''' + used_gens = set() + for r in self.relators: + used_gens.update(r.contains_generators()) + if not set(self.generators) <= used_gens: + return True + # Abelianisation test: check is the abelianisation is infinite + abelian_rels = [] + for rel in self.relators: + abelian_rels.append([rel.exponent_sum(g) for g in self.generators]) + m = Matrix(Matrix(abelian_rels)) + if 0 in invariant_factors(m): + return True + else: + return None + + + def _finite_index_subgroup(self, s=None): + ''' + Find the elements of `self` that generate a finite index subgroup + and, if found, return the list of elements and the coset table of `self` by + the subgroup, otherwise return `(None, None)` + + ''' + gen = self.most_frequent_generator() + rels = list(self.generators) + rels.extend(self.relators) + if not s: + if len(self.generators) == 2: + s = [gen] + [g for g in self.generators if g != gen] + else: + rand = self.free_group.identity + i = 0 + while ((rand in rels or rand**-1 in rels or rand.is_identity) + and i<10): + rand = self.random() + i += 1 + s = [gen, rand] + [g for g in self.generators if g != gen] + mid = (len(s)+1)//2 + half1 = s[:mid] + half2 = s[mid:] + draft1 = None + draft2 = None + m = 200 + C = None + while not C and (m/2 < CosetTable.coset_table_max_limit): + m = min(m, CosetTable.coset_table_max_limit) + draft1 = self.coset_enumeration(half1, max_cosets=m, + draft=draft1, incomplete=True) + if draft1.is_complete(): + C = draft1 + half = half1 + else: + draft2 = self.coset_enumeration(half2, max_cosets=m, + draft=draft2, incomplete=True) + if draft2.is_complete(): + C = draft2 + half = half2 + if not C: + m *= 2 + if not C: + return None, None + C.compress() + return half, C + + def most_frequent_generator(self): + gens = self.generators + rels = self.relators + freqs = [sum(r.generator_count(g) for r in rels) for g in gens] + return gens[freqs.index(max(freqs))] + + def random(self): + import random + r = self.free_group.identity + for i in range(random.randint(2,3)): + r = r*random.choice(self.generators)**random.choice([1,-1]) + return r + + def index(self, H, strategy="relator_based"): + """ + Return the index of subgroup ``H`` in group ``self``. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> from sympy.combinatorics.fp_groups import FpGroup + >>> F, x, y = free_group("x, y") + >>> f = FpGroup(F, [x**5, y**4, y*x*y**3*x**3]) + >>> f.index([x]) + 4 + + """ + # TODO: use |G:H| = |G|/|H| (currently H can't be made into a group) + # when we know |G| and |H| + + if H == []: + return self.order() + else: + C = self.coset_enumeration(H, strategy) + return len(C.table) + + def __str__(self): + if self.free_group.rank > 30: + str_form = "" % self.free_group.rank + else: + str_form = "" % str(self.generators) + return str_form + + __repr__ = __str__ + +#============================================================================== +# PERMUTATION GROUP METHODS +#============================================================================== + + def _to_perm_group(self): + ''' + Return an isomorphic permutation group and the isomorphism. + The implementation is dependent on coset enumeration so + will only terminate for finite groups. + + ''' + from sympy.combinatorics import Permutation + from sympy.combinatorics.homomorphisms import homomorphism + if self.order() is S.Infinity: + raise NotImplementedError("Permutation presentation of infinite " + "groups is not implemented") + if self._perm_isomorphism: + T = self._perm_isomorphism + P = T.image() + else: + C = self.coset_table([]) + gens = self.generators + images = [[C[i][2*gens.index(g)] for i in range(len(C))] for g in gens] + images = [Permutation(i) for i in images] + P = PermutationGroup(images) + T = homomorphism(self, P, gens, images, check=False) + self._perm_isomorphism = T + return P, T + + def _perm_group_list(self, method_name, *args): + ''' + Given the name of a `PermutationGroup` method (returning a subgroup + or a list of subgroups) and (optionally) additional arguments it takes, + return a list or a list of lists containing the generators of this (or + these) subgroups in terms of the generators of `self`. + + ''' + P, T = self._to_perm_group() + perm_result = getattr(P, method_name)(*args) + single = False + if isinstance(perm_result, PermutationGroup): + perm_result, single = [perm_result], True + result = [] + for group in perm_result: + gens = group.generators + result.append(T.invert(gens)) + return result[0] if single else result + + def derived_series(self): + ''' + Return the list of lists containing the generators + of the subgroups in the derived series of `self`. + + ''' + return self._perm_group_list('derived_series') + + def lower_central_series(self): + ''' + Return the list of lists containing the generators + of the subgroups in the lower central series of `self`. + + ''' + return self._perm_group_list('lower_central_series') + + def center(self): + ''' + Return the list of generators of the center of `self`. + + ''' + return self._perm_group_list('center') + + + def derived_subgroup(self): + ''' + Return the list of generators of the derived subgroup of `self`. + + ''' + return self._perm_group_list('derived_subgroup') + + + def centralizer(self, other): + ''' + Return the list of generators of the centralizer of `other` + (a list of elements of `self`) in `self`. + + ''' + T = self._to_perm_group()[1] + other = T(other) + return self._perm_group_list('centralizer', other) + + def normal_closure(self, other): + ''' + Return the list of generators of the normal closure of `other` + (a list of elements of `self`) in `self`. + + ''' + T = self._to_perm_group()[1] + other = T(other) + return self._perm_group_list('normal_closure', other) + + def _perm_property(self, attr): + ''' + Given an attribute of a `PermutationGroup`, return + its value for a permutation group isomorphic to `self`. + + ''' + P = self._to_perm_group()[0] + return getattr(P, attr) + + @property + def is_abelian(self): + ''' + Check if `self` is abelian. + + ''' + return self._perm_property("is_abelian") + + @property + def is_nilpotent(self): + ''' + Check if `self` is nilpotent. + + ''' + return self._perm_property("is_nilpotent") + + @property + def is_solvable(self): + ''' + Check if `self` is solvable. + + ''' + return self._perm_property("is_solvable") + + @property + def elements(self): + ''' + List the elements of `self`. + + ''' + P, T = self._to_perm_group() + return T.invert(P.elements) + + @property + def is_cyclic(self): + """ + Return ``True`` if group is Cyclic. + + """ + if len(self.generators) <= 1: + return True + try: + P, T = self._to_perm_group() + except NotImplementedError: + raise NotImplementedError("Check for infinite Cyclic group " + "is not implemented") + return P.is_cyclic + + def abelian_invariants(self): + """ + Return Abelian Invariants of a group. + """ + try: + P, T = self._to_perm_group() + except NotImplementedError: + raise NotImplementedError("abelian invariants is not implemented" + "for infinite group") + return P.abelian_invariants() + + def composition_series(self): + """ + Return subnormal series of maximum length for a group. + """ + try: + P, T = self._to_perm_group() + except NotImplementedError: + raise NotImplementedError("composition series is not implemented" + "for infinite group") + return P.composition_series() + + +class FpSubgroup(DefaultPrinting): + ''' + The class implementing a subgroup of an FpGroup or a FreeGroup + (only finite index subgroups are supported at this point). This + is to be used if one wishes to check if an element of the original + group belongs to the subgroup + + ''' + def __init__(self, G, gens, normal=False): + super().__init__() + self.parent = G + self.generators = list({g for g in gens if g != G.identity}) + self._min_words = None #for use in __contains__ + self.C = None + self.normal = normal + + def __contains__(self, g): + + if isinstance(self.parent, FreeGroup): + if self._min_words is None: + # make _min_words - a list of subwords such that + # g is in the subgroup if and only if it can be + # partitioned into these subwords. Infinite families of + # subwords are presented by tuples, e.g. (r, w) + # stands for the family of subwords r*w**n*r**-1 + + def _process(w): + # this is to be used before adding new words + # into _min_words; if the word w is not cyclically + # reduced, it will generate an infinite family of + # subwords so should be written as a tuple; + # if it is, w**-1 should be added to the list + # as well + p, r = w.cyclic_reduction(removed=True) + if not r.is_identity: + return [(r, p)] + else: + return [w, w**-1] + + # make the initial list + gens = [] + for w in self.generators: + if self.normal: + w = w.cyclic_reduction() + gens.extend(_process(w)) + + for w1 in gens: + for w2 in gens: + # if w1 and w2 are equal or are inverses, continue + if w1 == w2 or (not isinstance(w1, tuple) + and w1**-1 == w2): + continue + + # if the start of one word is the inverse of the + # end of the other, their multiple should be added + # to _min_words because of cancellation + if isinstance(w1, tuple): + # start, end + s1, s2 = w1[0][0], w1[0][0]**-1 + else: + s1, s2 = w1[0], w1[len(w1)-1] + + if isinstance(w2, tuple): + # start, end + r1, r2 = w2[0][0], w2[0][0]**-1 + else: + r1, r2 = w2[0], w2[len(w1)-1] + + # p1 and p2 are w1 and w2 or, in case when + # w1 or w2 is an infinite family, a representative + p1, p2 = w1, w2 + if isinstance(w1, tuple): + p1 = w1[0]*w1[1]*w1[0]**-1 + if isinstance(w2, tuple): + p2 = w2[0]*w2[1]*w2[0]**-1 + + # add the product of the words to the list is necessary + if r1**-1 == s2 and not (p1*p2).is_identity: + new = _process(p1*p2) + if new not in gens: + gens.extend(new) + + if r2**-1 == s1 and not (p2*p1).is_identity: + new = _process(p2*p1) + if new not in gens: + gens.extend(new) + + self._min_words = gens + + min_words = self._min_words + + def _is_subword(w): + # check if w is a word in _min_words or one of + # the infinite families in it + w, r = w.cyclic_reduction(removed=True) + if r.is_identity or self.normal: + return w in min_words + else: + t = [s[1] for s in min_words if isinstance(s, tuple) + and s[0] == r] + return [s for s in t if w.power_of(s)] != [] + + # store the solution of words for which the result of + # _word_break (below) is known + known = {} + + def _word_break(w): + # check if w can be written as a product of words + # in min_words + if len(w) == 0: + return True + i = 0 + while i < len(w): + i += 1 + prefix = w.subword(0, i) + if not _is_subword(prefix): + continue + rest = w.subword(i, len(w)) + if rest not in known: + known[rest] = _word_break(rest) + if known[rest]: + return True + return False + + if self.normal: + g = g.cyclic_reduction() + return _word_break(g) + else: + if self.C is None: + C = self.parent.coset_enumeration(self.generators) + self.C = C + i = 0 + C = self.C + for j in range(len(g)): + i = C.table[i][C.A_dict[g[j]]] + return i == 0 + + def order(self): + if not self.generators: + return S.One + if isinstance(self.parent, FreeGroup): + return S.Infinity + if self.C is None: + C = self.parent.coset_enumeration(self.generators) + self.C = C + # This is valid because `len(self.C.table)` (the index of the subgroup) + # will always be finite - otherwise coset enumeration doesn't terminate + return self.parent.order()/len(self.C.table) + + def to_FpGroup(self): + if isinstance(self.parent, FreeGroup): + gen_syms = [('x_%d'%i) for i in range(len(self.generators))] + return free_group(', '.join(gen_syms))[0] + return self.parent.subgroup(C=self.C) + + def __str__(self): + if len(self.generators) > 30: + str_form = "" % len(self.generators) + else: + str_form = "" % str(self.generators) + return str_form + + __repr__ = __str__ + + +############################################################################### +# LOW INDEX SUBGROUPS # +############################################################################### + +def low_index_subgroups(G, N, Y=()): + """ + Implements the Low Index Subgroups algorithm, i.e find all subgroups of + ``G`` upto a given index ``N``. This implements the method described in + [Sim94]. This procedure involves a backtrack search over incomplete Coset + Tables, rather than over forced coincidences. + + Parameters + ========== + + G: An FpGroup < X|R > + N: positive integer, representing the maximum index value for subgroups + Y: (an optional argument) specifying a list of subgroup generators, such + that each of the resulting subgroup contains the subgroup generated by Y. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> from sympy.combinatorics.fp_groups import FpGroup, low_index_subgroups + >>> F, x, y = free_group("x, y") + >>> f = FpGroup(F, [x**2, y**3, (x*y)**4]) + >>> L = low_index_subgroups(f, 4) + >>> for coset_table in L: + ... print(coset_table.table) + [[0, 0, 0, 0]] + [[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 3, 3]] + [[0, 0, 1, 2], [2, 2, 2, 0], [1, 1, 0, 1]] + [[1, 1, 0, 0], [0, 0, 1, 1]] + + References + ========== + + .. [1] Holt, D., Eick, B., O'Brien, E. + "Handbook of Computational Group Theory" + Section 5.4 + + .. [2] Marston Conder and Peter Dobcsanyi + "Applications and Adaptions of the Low Index Subgroups Procedure" + + """ + C = CosetTable(G, []) + R = G.relators + # length chosen for the length of the short relators + len_short_rel = 5 + # elements of R2 only checked at the last step for complete + # coset tables + R2 = {rel for rel in R if len(rel) > len_short_rel} + # elements of R1 are used in inner parts of the process to prune + # branches of the search tree, + R1 = {rel.identity_cyclic_reduction() for rel in set(R) - R2} + R1_c_list = C.conjugates(R1) + S = [] + descendant_subgroups(S, C, R1_c_list, C.A[0], R2, N, Y) + return S + + +def descendant_subgroups(S, C, R1_c_list, x, R2, N, Y): + A_dict = C.A_dict + A_dict_inv = C.A_dict_inv + if C.is_complete(): + # if C is complete then it only needs to test + # whether the relators in R2 are satisfied + for w, alpha in product(R2, C.omega): + if not C.scan_check(alpha, w): + return + # relators in R2 are satisfied, append the table to list + S.append(C) + else: + # find the first undefined entry in Coset Table + for alpha, x in product(range(len(C.table)), C.A): + if C.table[alpha][A_dict[x]] is None: + # this is "x" in pseudo-code (using "y" makes it clear) + undefined_coset, undefined_gen = alpha, x + break + # for filling up the undefine entry we try all possible values + # of beta in Omega or beta = n where beta^(undefined_gen^-1) is undefined + reach = C.omega + [C.n] + for beta in reach: + if beta < N: + if beta == C.n or C.table[beta][A_dict_inv[undefined_gen]] is None: + try_descendant(S, C, R1_c_list, R2, N, undefined_coset, \ + undefined_gen, beta, Y) + + +def try_descendant(S, C, R1_c_list, R2, N, alpha, x, beta, Y): + r""" + Solves the problem of trying out each individual possibility + for `\alpha^x. + + """ + D = C.copy() + if beta == D.n and beta < N: + D.table.append([None]*len(D.A)) + D.p.append(beta) + D.table[alpha][D.A_dict[x]] = beta + D.table[beta][D.A_dict_inv[x]] = alpha + D.deduction_stack.append((alpha, x)) + if not D.process_deductions_check(R1_c_list[D.A_dict[x]], \ + R1_c_list[D.A_dict_inv[x]]): + return + for w in Y: + if not D.scan_check(0, w): + return + if first_in_class(D, Y): + descendant_subgroups(S, D, R1_c_list, x, R2, N, Y) + + +def first_in_class(C, Y=()): + """ + Checks whether the subgroup ``H=G1`` corresponding to the Coset Table + could possibly be the canonical representative of its conjugacy class. + + Parameters + ========== + + C: CosetTable + + Returns + ======= + + bool: True/False + + If this returns False, then no descendant of C can have that property, and + so we can abandon C. If it returns True, then we need to process further + the node of the search tree corresponding to C, and so we call + ``descendant_subgroups`` recursively on C. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> from sympy.combinatorics.fp_groups import FpGroup, CosetTable, first_in_class + >>> F, x, y = free_group("x, y") + >>> f = FpGroup(F, [x**2, y**3, (x*y)**4]) + >>> C = CosetTable(f, []) + >>> C.table = [[0, 0, None, None]] + >>> first_in_class(C) + True + >>> C.table = [[1, 1, 1, None], [0, 0, None, 1]]; C.p = [0, 1] + >>> first_in_class(C) + True + >>> C.table = [[1, 1, 2, 1], [0, 0, 0, None], [None, None, None, 0]] + >>> C.p = [0, 1, 2] + >>> first_in_class(C) + False + >>> C.table = [[1, 1, 1, 2], [0, 0, 2, 0], [2, None, 0, 1]] + >>> first_in_class(C) + False + + # TODO:: Sims points out in [Sim94] that performance can be improved by + # remembering some of the information computed by ``first_in_class``. If + # the ``continue alpha`` statement is executed at line 14, then the same thing + # will happen for that value of alpha in any descendant of the table C, and so + # the values the values of alpha for which this occurs could profitably be + # stored and passed through to the descendants of C. Of course this would + # make the code more complicated. + + # The code below is taken directly from the function on page 208 of [Sim94] + # nu[alpha] + + """ + n = C.n + # lamda is the largest numbered point in Omega_c_alpha which is currently defined + lamda = -1 + # for alpha in Omega_c, nu[alpha] is the point in Omega_c_alpha corresponding to alpha + nu = [None]*n + # for alpha in Omega_c_alpha, mu[alpha] is the point in Omega_c corresponding to alpha + mu = [None]*n + # mutually nu and mu are the mutually-inverse equivalence maps between + # Omega_c_alpha and Omega_c + next_alpha = False + # For each 0!=alpha in [0 .. nc-1], we start by constructing the equivalent + # standardized coset table C_alpha corresponding to H_alpha + for alpha in range(1, n): + # reset nu to "None" after previous value of alpha + for beta in range(lamda+1): + nu[mu[beta]] = None + # we only want to reject our current table in favour of a preceding + # table in the ordering in which 1 is replaced by alpha, if the subgroup + # G_alpha corresponding to this preceding table definitely contains the + # given subgroup + for w in Y: + # TODO: this should support input of a list of general words + # not just the words which are in "A" (i.e gen and gen^-1) + if C.table[alpha][C.A_dict[w]] != alpha: + # continue with alpha + next_alpha = True + break + if next_alpha: + next_alpha = False + continue + # try alpha as the new point 0 in Omega_C_alpha + mu[0] = alpha + nu[alpha] = 0 + # compare corresponding entries in C and C_alpha + lamda = 0 + for beta in range(n): + for x in C.A: + gamma = C.table[beta][C.A_dict[x]] + delta = C.table[mu[beta]][C.A_dict[x]] + # if either of the entries is undefined, + # we move with next alpha + if gamma is None or delta is None: + # continue with alpha + next_alpha = True + break + if nu[delta] is None: + # delta becomes the next point in Omega_C_alpha + lamda += 1 + nu[delta] = lamda + mu[lamda] = delta + if nu[delta] < gamma: + return False + if nu[delta] > gamma: + # continue with alpha + next_alpha = True + break + if next_alpha: + next_alpha = False + break + return True + +#======================================================================== +# Simplifying Presentation +#======================================================================== + +def simplify_presentation(*args, change_gens=False): + ''' + For an instance of `FpGroup`, return a simplified isomorphic copy of + the group (e.g. remove redundant generators or relators). Alternatively, + a list of generators and relators can be passed in which case the + simplified lists will be returned. + + By default, the generators of the group are unchanged. If you would + like to remove redundant generators, set the keyword argument + `change_gens = True`. + + ''' + if len(args) == 1: + if not isinstance(args[0], FpGroup): + raise TypeError("The argument must be an instance of FpGroup") + G = args[0] + gens, rels = simplify_presentation(G.generators, G.relators, + change_gens=change_gens) + if gens: + return FpGroup(gens[0].group, rels) + return FpGroup(FreeGroup([]), []) + elif len(args) == 2: + gens, rels = args[0][:], args[1][:] + if not gens: + return gens, rels + identity = gens[0].group.identity + else: + if len(args) == 0: + m = "Not enough arguments" + else: + m = "Too many arguments" + raise RuntimeError(m) + + prev_gens = [] + prev_rels = [] + while not set(prev_rels) == set(rels): + prev_rels = rels + while change_gens and not set(prev_gens) == set(gens): + prev_gens = gens + gens, rels = elimination_technique_1(gens, rels, identity) + rels = _simplify_relators(rels) + + if change_gens: + syms = [g.array_form[0][0] for g in gens] + F = free_group(syms)[0] + identity = F.identity + gens = F.generators + subs = dict(zip(syms, gens)) + for j, r in enumerate(rels): + a = r.array_form + rel = identity + for sym, p in a: + rel = rel*subs[sym]**p + rels[j] = rel + return gens, rels + +def _simplify_relators(rels): + """ + Simplifies a set of relators. All relators are checked to see if they are + of the form `gen^n`. If any such relators are found then all other relators + are processed for strings in the `gen` known order. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> from sympy.combinatorics.fp_groups import _simplify_relators + >>> F, x, y = free_group("x, y") + >>> w1 = [x**2*y**4, x**3] + >>> _simplify_relators(w1) + [x**3, x**-1*y**4] + + >>> w2 = [x**2*y**-4*x**5, x**3, x**2*y**8, y**5] + >>> _simplify_relators(w2) + [x**-1*y**-2, x**-1*y*x**-1, x**3, y**5] + + >>> w3 = [x**6*y**4, x**4] + >>> _simplify_relators(w3) + [x**4, x**2*y**4] + + >>> w4 = [x**2, x**5, y**3] + >>> _simplify_relators(w4) + [x, y**3] + + """ + rels = rels[:] + + if not rels: + return [] + + identity = rels[0].group.identity + + # build dictionary with "gen: n" where gen^n is one of the relators + exps = {} + for i in range(len(rels)): + rel = rels[i] + if rel.number_syllables() == 1: + g = rel[0] + exp = abs(rel.array_form[0][1]) + if rel.array_form[0][1] < 0: + rels[i] = rels[i]**-1 + g = g**-1 + if g in exps: + exp = gcd(exp, exps[g].array_form[0][1]) + exps[g] = g**exp + + one_syllables_words = list(exps.values()) + # decrease some of the exponents in relators, making use of the single + # syllable relators + for i, rel in enumerate(rels): + if rel in one_syllables_words: + continue + rel = rel.eliminate_words(one_syllables_words, _all = True) + # if rels[i] contains g**n where abs(n) is greater than half of the power p + # of g in exps, g**n can be replaced by g**(n-p) (or g**(p-n) if n<0) + for g in rel.contains_generators(): + if g in exps: + exp = exps[g].array_form[0][1] + max_exp = (exp + 1)//2 + rel = rel.eliminate_word(g**(max_exp), g**(max_exp-exp), _all = True) + rel = rel.eliminate_word(g**(-max_exp), g**(-(max_exp-exp)), _all = True) + rels[i] = rel + + rels = [r.identity_cyclic_reduction() for r in rels] + + rels += one_syllables_words # include one_syllable_words in the list of relators + rels = list(set(rels)) # get unique values in rels + rels.sort() + + # remove entries in rels + try: + rels.remove(identity) + except ValueError: + pass + return rels + +# Pg 350, section 2.5.1 from [2] +def elimination_technique_1(gens, rels, identity): + rels = rels[:] + # the shorter relators are examined first so that generators selected for + # elimination will have shorter strings as equivalent + rels.sort() + gens = gens[:] + redundant_gens = {} + redundant_rels = [] + used_gens = set() + # examine each relator in relator list for any generator occurring exactly + # once + for rel in rels: + # don't look for a redundant generator in a relator which + # depends on previously found ones + contained_gens = rel.contains_generators() + if any(g in contained_gens for g in redundant_gens): + continue + contained_gens = list(contained_gens) + contained_gens.sort(reverse = True) + for gen in contained_gens: + if rel.generator_count(gen) == 1 and gen not in used_gens: + k = rel.exponent_sum(gen) + gen_index = rel.index(gen**k) + bk = rel.subword(gen_index + 1, len(rel)) + fw = rel.subword(0, gen_index) + chi = bk*fw + redundant_gens[gen] = chi**(-1*k) + used_gens.update(chi.contains_generators()) + redundant_rels.append(rel) + break + rels = [r for r in rels if r not in redundant_rels] + # eliminate the redundant generators from remaining relators + rels = [r.eliminate_words(redundant_gens, _all = True).identity_cyclic_reduction() for r in rels] + rels = list(set(rels)) + try: + rels.remove(identity) + except ValueError: + pass + gens = [g for g in gens if g not in redundant_gens] + return gens, rels + +############################################################################### +# SUBGROUP PRESENTATIONS # +############################################################################### + +# Pg 175 [1] +def define_schreier_generators(C, homomorphism=False): + ''' + Parameters + ========== + + C -- Coset table. + homomorphism -- When set to True, return a dictionary containing the images + of the presentation generators in the original group. + ''' + y = [] + gamma = 1 + f = C.fp_group + X = f.generators + if homomorphism: + # `_gens` stores the elements of the parent group to + # to which the schreier generators correspond to. + _gens = {} + # compute the schreier Traversal + tau = {} + tau[0] = f.identity + C.P = [[None]*len(C.A) for i in range(C.n)] + for alpha, x in product(C.omega, C.A): + beta = C.table[alpha][C.A_dict[x]] + if beta == gamma: + C.P[alpha][C.A_dict[x]] = "" + C.P[beta][C.A_dict_inv[x]] = "" + gamma += 1 + if homomorphism: + tau[beta] = tau[alpha]*x + elif x in X and C.P[alpha][C.A_dict[x]] is None: + y_alpha_x = '%s_%s' % (x, alpha) + y.append(y_alpha_x) + C.P[alpha][C.A_dict[x]] = y_alpha_x + if homomorphism: + _gens[y_alpha_x] = tau[alpha]*x*tau[beta]**-1 + grp_gens = list(free_group(', '.join(y))) + C._schreier_free_group = grp_gens.pop(0) + C._schreier_generators = grp_gens + if homomorphism: + C._schreier_gen_elem = _gens + # replace all elements of P by, free group elements + for i, j in product(range(len(C.P)), range(len(C.A))): + # if equals "", replace by identity element + if C.P[i][j] == "": + C.P[i][j] = C._schreier_free_group.identity + elif isinstance(C.P[i][j], str): + r = C._schreier_generators[y.index(C.P[i][j])] + C.P[i][j] = r + beta = C.table[i][j] + C.P[beta][j + 1] = r**-1 + +def reidemeister_relators(C): + R = C.fp_group.relators + rels = [rewrite(C, coset, word) for word in R for coset in range(C.n)] + order_1_gens = {i for i in rels if len(i) == 1} + + # remove all the order 1 generators from relators + rels = list(filter(lambda rel: rel not in order_1_gens, rels)) + + # replace order 1 generators by identity element in reidemeister relators + for i in range(len(rels)): + w = rels[i] + w = w.eliminate_words(order_1_gens, _all=True) + rels[i] = w + + C._schreier_generators = [i for i in C._schreier_generators + if not (i in order_1_gens or i**-1 in order_1_gens)] + + # Tietze transformation 1 i.e TT_1 + # remove cyclic conjugate elements from relators + i = 0 + while i < len(rels): + w = rels[i] + j = i + 1 + while j < len(rels): + if w.is_cyclic_conjugate(rels[j]): + del rels[j] + else: + j += 1 + i += 1 + + C._reidemeister_relators = rels + + +def rewrite(C, alpha, w): + """ + Parameters + ========== + + C: CosetTable + alpha: A live coset + w: A word in `A*` + + Returns + ======= + + rho(tau(alpha), w) + + Examples + ======== + + >>> from sympy.combinatorics.fp_groups import FpGroup, CosetTable, define_schreier_generators, rewrite + >>> from sympy.combinatorics import free_group + >>> F, x, y = free_group("x, y") + >>> f = FpGroup(F, [x**2, y**3, (x*y)**6]) + >>> C = CosetTable(f, []) + >>> C.table = [[1, 1, 2, 3], [0, 0, 4, 5], [4, 4, 3, 0], [5, 5, 0, 2], [2, 2, 5, 1], [3, 3, 1, 4]] + >>> C.p = [0, 1, 2, 3, 4, 5] + >>> define_schreier_generators(C) + >>> rewrite(C, 0, (x*y)**6) + x_4*y_2*x_3*x_1*x_2*y_4*x_5 + + """ + v = C._schreier_free_group.identity + for i in range(len(w)): + x_i = w[i] + v = v*C.P[alpha][C.A_dict[x_i]] + alpha = C.table[alpha][C.A_dict[x_i]] + return v + +# Pg 350, section 2.5.2 from [2] +def elimination_technique_2(C): + """ + This technique eliminates one generator at a time. Heuristically this + seems superior in that we may select for elimination the generator with + shortest equivalent string at each stage. + + >>> from sympy.combinatorics import free_group + >>> from sympy.combinatorics.fp_groups import FpGroup, coset_enumeration_r, \ + reidemeister_relators, define_schreier_generators, elimination_technique_2 + >>> F, x, y = free_group("x, y") + >>> f = FpGroup(F, [x**3, y**5, (x*y)**2]); H = [x*y, x**-1*y**-1*x*y*x] + >>> C = coset_enumeration_r(f, H) + >>> C.compress(); C.standardize() + >>> define_schreier_generators(C) + >>> reidemeister_relators(C) + >>> elimination_technique_2(C) + ([y_1, y_2], [y_2**-3, y_2*y_1*y_2*y_1*y_2*y_1, y_1**2]) + + """ + rels = C._reidemeister_relators + rels.sort(reverse=True) + gens = C._schreier_generators + for i in range(len(gens) - 1, -1, -1): + rel = rels[i] + for j in range(len(gens) - 1, -1, -1): + gen = gens[j] + if rel.generator_count(gen) == 1: + k = rel.exponent_sum(gen) + gen_index = rel.index(gen**k) + bk = rel.subword(gen_index + 1, len(rel)) + fw = rel.subword(0, gen_index) + rep_by = (bk*fw)**(-1*k) + del rels[i] + del gens[j] + rels = [rel.eliminate_word(gen, rep_by) for rel in rels] + break + C._reidemeister_relators = rels + C._schreier_generators = gens + return C._schreier_generators, C._reidemeister_relators + +def reidemeister_presentation(fp_grp, H, C=None, homomorphism=False): + """ + Parameters + ========== + + fp_group: A finitely presented group, an instance of FpGroup + H: A subgroup whose presentation is to be found, given as a list + of words in generators of `fp_grp` + homomorphism: When set to True, return a homomorphism from the subgroup + to the parent group + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> from sympy.combinatorics.fp_groups import FpGroup, reidemeister_presentation + >>> F, x, y = free_group("x, y") + + Example 5.6 Pg. 177 from [1] + >>> f = FpGroup(F, [x**3, y**5, (x*y)**2]) + >>> H = [x*y, x**-1*y**-1*x*y*x] + >>> reidemeister_presentation(f, H) + ((y_1, y_2), (y_1**2, y_2**3, y_2*y_1*y_2*y_1*y_2*y_1)) + + Example 5.8 Pg. 183 from [1] + >>> f = FpGroup(F, [x**3, y**3, (x*y)**3]) + >>> H = [x*y, x*y**-1] + >>> reidemeister_presentation(f, H) + ((x_0, y_0), (x_0**3, y_0**3, x_0*y_0*x_0*y_0*x_0*y_0)) + + Exercises Q2. Pg 187 from [1] + >>> f = FpGroup(F, [x**2*y**2, y**-1*x*y*x**-3]) + >>> H = [x] + >>> reidemeister_presentation(f, H) + ((x_0,), (x_0**4,)) + + Example 5.9 Pg. 183 from [1] + >>> f = FpGroup(F, [x**3*y**-3, (x*y)**3, (x*y**-1)**2]) + >>> H = [x] + >>> reidemeister_presentation(f, H) + ((x_0,), (x_0**6,)) + + """ + if not C: + C = coset_enumeration_r(fp_grp, H) + C.compress(); C.standardize() + define_schreier_generators(C, homomorphism=homomorphism) + reidemeister_relators(C) + gens, rels = C._schreier_generators, C._reidemeister_relators + gens, rels = simplify_presentation(gens, rels, change_gens=True) + + C.schreier_generators = tuple(gens) + C.reidemeister_relators = tuple(rels) + + if homomorphism: + _gens = [C._schreier_gen_elem[str(gen)] for gen in gens] + return C.schreier_generators, C.reidemeister_relators, _gens + + return C.schreier_generators, C.reidemeister_relators + + +FpGroupElement = FreeGroupElement diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/free_groups.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/free_groups.py new file mode 100644 index 0000000000000000000000000000000000000000..2ec85f4fac73fba95d4644a37ecb27140e45a4c5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/free_groups.py @@ -0,0 +1,1360 @@ +from __future__ import annotations + +from sympy.core import S +from sympy.core.expr import Expr +from sympy.core.symbol import Symbol, symbols as _symbols +from sympy.core.sympify import CantSympify +from sympy.printing.defaults import DefaultPrinting +from sympy.utilities import public +from sympy.utilities.iterables import flatten, is_sequence +from sympy.utilities.magic import pollute +from sympy.utilities.misc import as_int + + +@public +def free_group(symbols): + """Construct a free group returning ``(FreeGroup, (f_0, f_1, ..., f_(n-1))``. + + Parameters + ========== + + symbols : str, Symbol/Expr or sequence of str, Symbol/Expr (may be empty) + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> F, x, y, z = free_group("x, y, z") + >>> F + + >>> x**2*y**-1 + x**2*y**-1 + >>> type(_) + + + """ + _free_group = FreeGroup(symbols) + return (_free_group,) + tuple(_free_group.generators) + +@public +def xfree_group(symbols): + """Construct a free group returning ``(FreeGroup, (f_0, f_1, ..., f_(n-1)))``. + + Parameters + ========== + + symbols : str, Symbol/Expr or sequence of str, Symbol/Expr (may be empty) + + Examples + ======== + + >>> from sympy.combinatorics.free_groups import xfree_group + >>> F, (x, y, z) = xfree_group("x, y, z") + >>> F + + >>> y**2*x**-2*z**-1 + y**2*x**-2*z**-1 + >>> type(_) + + + """ + _free_group = FreeGroup(symbols) + return (_free_group, _free_group.generators) + +@public +def vfree_group(symbols): + """Construct a free group and inject ``f_0, f_1, ..., f_(n-1)`` as symbols + into the global namespace. + + Parameters + ========== + + symbols : str, Symbol/Expr or sequence of str, Symbol/Expr (may be empty) + + Examples + ======== + + >>> from sympy.combinatorics.free_groups import vfree_group + >>> vfree_group("x, y, z") + + >>> x**2*y**-2*z # noqa: F821 + x**2*y**-2*z + >>> type(_) + + + """ + _free_group = FreeGroup(symbols) + pollute([sym.name for sym in _free_group.symbols], _free_group.generators) + return _free_group + + +def _parse_symbols(symbols): + if not symbols: + return () + if isinstance(symbols, str): + return _symbols(symbols, seq=True) + elif isinstance(symbols, (Expr, FreeGroupElement)): + return (symbols,) + elif is_sequence(symbols): + if all(isinstance(s, str) for s in symbols): + return _symbols(symbols) + elif all(isinstance(s, Expr) for s in symbols): + return symbols + raise ValueError("The type of `symbols` must be one of the following: " + "a str, Symbol/Expr or a sequence of " + "one of these types") + + +############################################################################## +# FREE GROUP # +############################################################################## + +_free_group_cache: dict[int, FreeGroup] = {} + +class FreeGroup(DefaultPrinting): + """ + Free group with finite or infinite number of generators. Its input API + is that of a str, Symbol/Expr or a sequence of one of + these types (which may be empty) + + See Also + ======== + + sympy.polys.rings.PolyRing + + References + ========== + + .. [1] https://www.gap-system.org/Manuals/doc/ref/chap37.html + + .. [2] https://en.wikipedia.org/wiki/Free_group + + """ + is_associative = True + is_group = True + is_FreeGroup = True + is_PermutationGroup = False + relators: list[Expr] = [] + + def __new__(cls, symbols): + symbols = tuple(_parse_symbols(symbols)) + rank = len(symbols) + _hash = hash((cls.__name__, symbols, rank)) + obj = _free_group_cache.get(_hash) + + if obj is None: + obj = object.__new__(cls) + obj._hash = _hash + obj._rank = rank + # dtype method is used to create new instances of FreeGroupElement + obj.dtype = type("FreeGroupElement", (FreeGroupElement,), {"group": obj}) + obj.symbols = symbols + obj.generators = obj._generators() + obj._gens_set = set(obj.generators) + for symbol, generator in zip(obj.symbols, obj.generators): + if isinstance(symbol, Symbol): + name = symbol.name + if hasattr(obj, name): + setattr(obj, name, generator) + + _free_group_cache[_hash] = obj + + return obj + + def __getnewargs__(self): + """Return a tuple of arguments that must be passed to __new__ in order to support pickling this object.""" + return (self.symbols,) + + def __getstate__(self): + # Don't pickle any fields because they are regenerated within __new__ + return None + + def _generators(group): + """Returns the generators of the FreeGroup. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> F, x, y, z = free_group("x, y, z") + >>> F.generators + (x, y, z) + + """ + gens = [] + for sym in group.symbols: + elm = ((sym, 1),) + gens.append(group.dtype(elm)) + return tuple(gens) + + def clone(self, symbols=None): + return self.__class__(symbols or self.symbols) + + def __contains__(self, i): + """Return True if ``i`` is contained in FreeGroup.""" + if not isinstance(i, FreeGroupElement): + return False + group = i.group + return self == group + + def __hash__(self): + return self._hash + + def __len__(self): + return self.rank + + def __str__(self): + if self.rank > 30: + str_form = "" % self.rank + else: + str_form = "" + return str_form + + __repr__ = __str__ + + def __getitem__(self, index): + symbols = self.symbols[index] + return self.clone(symbols=symbols) + + def __eq__(self, other): + """No ``FreeGroup`` is equal to any "other" ``FreeGroup``. + """ + return self is other + + def index(self, gen): + """Return the index of the generator `gen` from ``(f_0, ..., f_(n-1))``. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> F, x, y = free_group("x, y") + >>> F.index(y) + 1 + >>> F.index(x) + 0 + + """ + if isinstance(gen, self.dtype): + return self.generators.index(gen) + else: + raise ValueError("expected a generator of Free Group %s, got %s" % (self, gen)) + + def order(self): + """Return the order of the free group. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> F, x, y = free_group("x, y") + >>> F.order() + oo + + >>> free_group("")[0].order() + 1 + + """ + if self.rank == 0: + return S.One + else: + return S.Infinity + + @property + def elements(self): + """ + Return the elements of the free group. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> (z,) = free_group("") + >>> z.elements + {} + + """ + if self.rank == 0: + # A set containing Identity element of `FreeGroup` self is returned + return {self.identity} + else: + raise ValueError("Group contains infinitely many elements" + ", hence cannot be represented") + + @property + def rank(self): + r""" + In group theory, the `rank` of a group `G`, denoted `G.rank`, + can refer to the smallest cardinality of a generating set + for G, that is + + \operatorname{rank}(G)=\min\{ |X|: X\subseteq G, \left\langle X\right\rangle =G\}. + + """ + return self._rank + + @property + def is_abelian(self): + """Returns if the group is Abelian. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, x, y, z = free_group("x y z") + >>> f.is_abelian + False + + """ + return self.rank in (0, 1) + + @property + def identity(self): + """Returns the identity element of free group.""" + return self.dtype() + + def contains(self, g): + """Tests if Free Group element ``g`` belong to self, ``G``. + + In mathematical terms any linear combination of generators + of a Free Group is contained in it. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, x, y, z = free_group("x y z") + >>> f.contains(x**3*y**2) + True + + """ + if not isinstance(g, FreeGroupElement): + return False + elif self != g.group: + return False + else: + return True + + def center(self): + """Returns the center of the free group `self`.""" + return {self.identity} + + +############################################################################ +# FreeGroupElement # +############################################################################ + + +class FreeGroupElement(CantSympify, DefaultPrinting, tuple): + """Used to create elements of FreeGroup. It cannot be used directly to + create a free group element. It is called by the `dtype` method of the + `FreeGroup` class. + + """ + __slots__ = () + is_assoc_word = True + + def new(self, init): + return self.__class__(init) + + _hash = None + + def __hash__(self): + _hash = self._hash + if _hash is None: + self._hash = _hash = hash((self.group, frozenset(tuple(self)))) + return _hash + + def copy(self): + return self.new(self) + + @property + def is_identity(self): + return not self.array_form + + @property + def array_form(self): + """ + SymPy provides two different internal kinds of representation + of associative words. The first one is called the `array_form` + which is a tuple containing `tuples` as its elements, where the + size of each tuple is two. At the first position the tuple + contains the `symbol-generator`, while at the second position + of tuple contains the exponent of that generator at the position. + Since elements (i.e. words) do not commute, the indexing of tuple + makes that property to stay. + + The structure in ``array_form`` of ``FreeGroupElement`` is of form: + + ``( ( symbol_of_gen, exponent ), ( , ), ... ( , ) )`` + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, x, y, z = free_group("x y z") + >>> (x*z).array_form + ((x, 1), (z, 1)) + >>> (x**2*z*y*x**2).array_form + ((x, 2), (z, 1), (y, 1), (x, 2)) + + See Also + ======== + + letter_repr + + """ + return tuple(self) + + @property + def letter_form(self): + """ + The letter representation of a ``FreeGroupElement`` is a tuple + of generator symbols, with each entry corresponding to a group + generator. Inverses of the generators are represented by + negative generator symbols. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, a, b, c, d = free_group("a b c d") + >>> (a**3).letter_form + (a, a, a) + >>> (a**2*d**-2*a*b**-4).letter_form + (a, a, -d, -d, a, -b, -b, -b, -b) + >>> (a**-2*b**3*d).letter_form + (-a, -a, b, b, b, d) + + See Also + ======== + + array_form + + """ + return tuple(flatten([(i,)*j if j > 0 else (-i,)*(-j) + for i, j in self.array_form])) + + def __getitem__(self, i): + group = self.group + r = self.letter_form[i] + if r.is_Symbol: + return group.dtype(((r, 1),)) + else: + return group.dtype(((-r, -1),)) + + def index(self, gen): + if len(gen) != 1: + raise ValueError() + return (self.letter_form).index(gen.letter_form[0]) + + @property + def letter_form_elm(self): + """ + """ + group = self.group + r = self.letter_form + return [group.dtype(((elm,1),)) if elm.is_Symbol \ + else group.dtype(((-elm,-1),)) for elm in r] + + @property + def ext_rep(self): + """This is called the External Representation of ``FreeGroupElement`` + """ + return tuple(flatten(self.array_form)) + + def __contains__(self, gen): + return gen.array_form[0][0] in tuple([r[0] for r in self.array_form]) + + def __str__(self): + if self.is_identity: + return "" + + str_form = "" + array_form = self.array_form + for i in range(len(array_form)): + if i == len(array_form) - 1: + if array_form[i][1] == 1: + str_form += str(array_form[i][0]) + else: + str_form += str(array_form[i][0]) + \ + "**" + str(array_form[i][1]) + else: + if array_form[i][1] == 1: + str_form += str(array_form[i][0]) + "*" + else: + str_form += str(array_form[i][0]) + \ + "**" + str(array_form[i][1]) + "*" + return str_form + + __repr__ = __str__ + + def __pow__(self, n): + n = as_int(n) + result = self.group.identity + if n == 0: + return result + if n < 0: + n = -n + x = self.inverse() + else: + x = self + while True: + if n % 2: + result *= x + n >>= 1 + if not n: + break + x *= x + return result + + def __mul__(self, other): + """Returns the product of elements belonging to the same ``FreeGroup``. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, x, y, z = free_group("x y z") + >>> x*y**2*y**-4 + x*y**-2 + >>> z*y**-2 + z*y**-2 + >>> x**2*y*y**-1*x**-2 + + + """ + group = self.group + if not isinstance(other, group.dtype): + raise TypeError("only FreeGroup elements of same FreeGroup can " + "be multiplied") + if self.is_identity: + return other + if other.is_identity: + return self + r = list(self.array_form + other.array_form) + zero_mul_simp(r, len(self.array_form) - 1) + return group.dtype(tuple(r)) + + def __truediv__(self, other): + group = self.group + if not isinstance(other, group.dtype): + raise TypeError("only FreeGroup elements of same FreeGroup can " + "be multiplied") + return self*(other.inverse()) + + def __rtruediv__(self, other): + group = self.group + if not isinstance(other, group.dtype): + raise TypeError("only FreeGroup elements of same FreeGroup can " + "be multiplied") + return other*(self.inverse()) + + def __add__(self, other): + return NotImplemented + + def inverse(self): + """ + Returns the inverse of a ``FreeGroupElement`` element + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, x, y, z = free_group("x y z") + >>> x.inverse() + x**-1 + >>> (x*y).inverse() + y**-1*x**-1 + + """ + group = self.group + r = tuple([(i, -j) for i, j in self.array_form[::-1]]) + return group.dtype(r) + + def order(self): + """Find the order of a ``FreeGroupElement``. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, x, y = free_group("x y") + >>> (x**2*y*y**-1*x**-2).order() + 1 + + """ + if self.is_identity: + return S.One + else: + return S.Infinity + + def commutator(self, other): + """ + Return the commutator of `self` and `x`: ``~x*~self*x*self`` + + """ + group = self.group + if not isinstance(other, group.dtype): + raise ValueError("commutator of only FreeGroupElement of the same " + "FreeGroup exists") + else: + return self.inverse()*other.inverse()*self*other + + def eliminate_words(self, words, _all=False, inverse=True): + ''' + Replace each subword from the dictionary `words` by words[subword]. + If words is a list, replace the words by the identity. + + ''' + again = True + new = self + if isinstance(words, dict): + while again: + again = False + for sub in words: + prev = new + new = new.eliminate_word(sub, words[sub], _all=_all, inverse=inverse) + if new != prev: + again = True + else: + while again: + again = False + for sub in words: + prev = new + new = new.eliminate_word(sub, _all=_all, inverse=inverse) + if new != prev: + again = True + return new + + def eliminate_word(self, gen, by=None, _all=False, inverse=True): + """ + For an associative word `self`, a subword `gen`, and an associative + word `by` (identity by default), return the associative word obtained by + replacing each occurrence of `gen` in `self` by `by`. If `_all = True`, + the occurrences of `gen` that may appear after the first substitution will + also be replaced and so on until no occurrences are found. This might not + always terminate (e.g. `(x).eliminate_word(x, x**2, _all=True)`). + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, x, y = free_group("x y") + >>> w = x**5*y*x**2*y**-4*x + >>> w.eliminate_word( x, x**2 ) + x**10*y*x**4*y**-4*x**2 + >>> w.eliminate_word( x, y**-1 ) + y**-11 + >>> w.eliminate_word(x**5) + y*x**2*y**-4*x + >>> w.eliminate_word(x*y, y) + x**4*y*x**2*y**-4*x + + See Also + ======== + substituted_word + + """ + if by is None: + by = self.group.identity + if self.is_independent(gen) or gen == by: + return self + if gen == self: + return by + if gen**-1 == by: + _all = False + word = self + l = len(gen) + + try: + i = word.subword_index(gen) + k = 1 + except ValueError: + if not inverse: + return word + try: + i = word.subword_index(gen**-1) + k = -1 + except ValueError: + return word + + word = word.subword(0, i)*by**k*word.subword(i+l, len(word)).eliminate_word(gen, by) + + if _all: + return word.eliminate_word(gen, by, _all=True, inverse=inverse) + else: + return word + + def __len__(self): + """ + For an associative word `self`, returns the number of letters in it. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, a, b = free_group("a b") + >>> w = a**5*b*a**2*b**-4*a + >>> len(w) + 13 + >>> len(a**17) + 17 + >>> len(w**0) + 0 + + """ + return sum(abs(j) for (i, j) in self) + + def __eq__(self, other): + """ + Two associative words are equal if they are words over the + same alphabet and if they are sequences of the same letters. + This is equivalent to saying that the external representations + of the words are equal. + There is no "universal" empty word, every alphabet has its own + empty word. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, swapnil0, swapnil1 = free_group("swapnil0 swapnil1") + >>> f + + >>> g, swap0, swap1 = free_group("swap0 swap1") + >>> g + + + >>> swapnil0 == swapnil1 + False + >>> swapnil0*swapnil1 == swapnil1/swapnil1*swapnil0*swapnil1 + True + >>> swapnil0*swapnil1 == swapnil1*swapnil0 + False + >>> swapnil1**0 == swap0**0 + False + + """ + group = self.group + if not isinstance(other, group.dtype): + return False + return tuple.__eq__(self, other) + + def __lt__(self, other): + """ + The ordering of associative words is defined by length and + lexicography (this ordering is called short-lex ordering), that + is, shorter words are smaller than longer words, and words of the + same length are compared w.r.t. the lexicographical ordering induced + by the ordering of generators. Generators are sorted according + to the order in which they were created. If the generators are + invertible then each generator `g` is larger than its inverse `g^{-1}`, + and `g^{-1}` is larger than every generator that is smaller than `g`. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, a, b = free_group("a b") + >>> b < a + False + >>> a < a.inverse() + False + + """ + group = self.group + if not isinstance(other, group.dtype): + raise TypeError("only FreeGroup elements of same FreeGroup can " + "be compared") + l = len(self) + m = len(other) + # implement lenlex order + if l < m: + return True + elif l > m: + return False + for i in range(l): + a = self[i].array_form[0] + b = other[i].array_form[0] + p = group.symbols.index(a[0]) + q = group.symbols.index(b[0]) + if p < q: + return True + elif p > q: + return False + elif a[1] < b[1]: + return True + elif a[1] > b[1]: + return False + return False + + def __le__(self, other): + return (self == other or self < other) + + def __gt__(self, other): + """ + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, x, y, z = free_group("x y z") + >>> y**2 > x**2 + True + >>> y*z > z*y + False + >>> x > x.inverse() + True + + """ + group = self.group + if not isinstance(other, group.dtype): + raise TypeError("only FreeGroup elements of same FreeGroup can " + "be compared") + return not self <= other + + def __ge__(self, other): + return not self < other + + def exponent_sum(self, gen): + """ + For an associative word `self` and a generator or inverse of generator + `gen`, ``exponent_sum`` returns the number of times `gen` appears in + `self` minus the number of times its inverse appears in `self`. If + neither `gen` nor its inverse occur in `self` then 0 is returned. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> F, x, y = free_group("x, y") + >>> w = x**2*y**3 + >>> w.exponent_sum(x) + 2 + >>> w.exponent_sum(x**-1) + -2 + >>> w = x**2*y**4*x**-3 + >>> w.exponent_sum(x) + -1 + + See Also + ======== + + generator_count + + """ + if len(gen) != 1: + raise ValueError("gen must be a generator or inverse of a generator") + s = gen.array_form[0] + return s[1]*sum(i[1] for i in self.array_form if i[0] == s[0]) + + def generator_count(self, gen): + """ + For an associative word `self` and a generator `gen`, + ``generator_count`` returns the multiplicity of generator + `gen` in `self`. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> F, x, y = free_group("x, y") + >>> w = x**2*y**3 + >>> w.generator_count(x) + 2 + >>> w = x**2*y**4*x**-3 + >>> w.generator_count(x) + 5 + + See Also + ======== + + exponent_sum + + """ + if len(gen) != 1 or gen.array_form[0][1] < 0: + raise ValueError("gen must be a generator") + s = gen.array_form[0] + return s[1]*sum(abs(i[1]) for i in self.array_form if i[0] == s[0]) + + def subword(self, from_i, to_j, strict=True): + """ + For an associative word `self` and two positive integers `from_i` and + `to_j`, `subword` returns the subword of `self` that begins at position + `from_i` and ends at `to_j - 1`, indexing is done with origin 0. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, a, b = free_group("a b") + >>> w = a**5*b*a**2*b**-4*a + >>> w.subword(2, 6) + a**3*b + + """ + group = self.group + if not strict: + from_i = max(from_i, 0) + to_j = min(len(self), to_j) + if from_i < 0 or to_j > len(self): + raise ValueError("`from_i`, `to_j` must be positive and no greater than " + "the length of associative word") + if to_j <= from_i: + return group.identity + else: + letter_form = self.letter_form[from_i: to_j] + array_form = letter_form_to_array_form(letter_form, group) + return group.dtype(array_form) + + def subword_index(self, word, start = 0): + ''' + Find the index of `word` in `self`. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, a, b = free_group("a b") + >>> w = a**2*b*a*b**3 + >>> w.subword_index(a*b*a*b) + 1 + + ''' + l = len(word) + self_lf = self.letter_form + word_lf = word.letter_form + index = None + for i in range(start,len(self_lf)-l+1): + if self_lf[i:i+l] == word_lf: + index = i + break + if index is not None: + return index + else: + raise ValueError("The given word is not a subword of self") + + def is_dependent(self, word): + """ + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> F, x, y = free_group("x, y") + >>> (x**4*y**-3).is_dependent(x**4*y**-2) + True + >>> (x**2*y**-1).is_dependent(x*y) + False + >>> (x*y**2*x*y**2).is_dependent(x*y**2) + True + >>> (x**12).is_dependent(x**-4) + True + + See Also + ======== + + is_independent + + """ + try: + return self.subword_index(word) is not None + except ValueError: + pass + try: + return self.subword_index(word**-1) is not None + except ValueError: + return False + + def is_independent(self, word): + """ + + See Also + ======== + + is_dependent + + """ + return not self.is_dependent(word) + + def contains_generators(self): + """ + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> F, x, y, z = free_group("x, y, z") + >>> (x**2*y**-1).contains_generators() + {x, y} + >>> (x**3*z).contains_generators() + {x, z} + + """ + group = self.group + gens = {group.dtype(((syllable[0], 1),)) for syllable in self.array_form} + return gens + + def cyclic_subword(self, from_i, to_j): + group = self.group + l = len(self) + letter_form = self.letter_form + period1 = int(from_i/l) + if from_i >= l: + from_i -= l*period1 + to_j -= l*period1 + diff = to_j - from_i + word = letter_form[from_i: to_j] + period2 = int(to_j/l) - 1 + word += letter_form*period2 + letter_form[:diff-l+from_i-l*period2] + word = letter_form_to_array_form(word, group) + return group.dtype(word) + + def cyclic_conjugates(self): + """Returns a words which are cyclic to the word `self`. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> F, x, y = free_group("x, y") + >>> w = x*y*x*y*x + >>> w.cyclic_conjugates() + {x*y*x**2*y, x**2*y*x*y, y*x*y*x**2, y*x**2*y*x, x*y*x*y*x} + >>> s = x*y*x**2*y*x + >>> s.cyclic_conjugates() + {x**2*y*x**2*y, y*x**2*y*x**2, x*y*x**2*y*x} + + References + ========== + + .. [1] https://planetmath.org/cyclicpermutation + + """ + return {self.cyclic_subword(i, i+len(self)) for i in range(len(self))} + + def is_cyclic_conjugate(self, w): + """ + Checks whether words ``self``, ``w`` are cyclic conjugates. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> F, x, y = free_group("x, y") + >>> w1 = x**2*y**5 + >>> w2 = x*y**5*x + >>> w1.is_cyclic_conjugate(w2) + True + >>> w3 = x**-1*y**5*x**-1 + >>> w3.is_cyclic_conjugate(w2) + False + + """ + l1 = len(self) + l2 = len(w) + if l1 != l2: + return False + w1 = self.identity_cyclic_reduction() + w2 = w.identity_cyclic_reduction() + letter1 = w1.letter_form + letter2 = w2.letter_form + str1 = ' '.join(map(str, letter1)) + str2 = ' '.join(map(str, letter2)) + if len(str1) != len(str2): + return False + + return str1 in str2 + ' ' + str2 + + def number_syllables(self): + """Returns the number of syllables of the associative word `self`. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, swapnil0, swapnil1 = free_group("swapnil0 swapnil1") + >>> (swapnil1**3*swapnil0*swapnil1**-1).number_syllables() + 3 + + """ + return len(self.array_form) + + def exponent_syllable(self, i): + """ + Returns the exponent of the `i`-th syllable of the associative word + `self`. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, a, b = free_group("a b") + >>> w = a**5*b*a**2*b**-4*a + >>> w.exponent_syllable( 2 ) + 2 + + """ + return self.array_form[i][1] + + def generator_syllable(self, i): + """ + Returns the symbol of the generator that is involved in the + i-th syllable of the associative word `self`. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, a, b = free_group("a b") + >>> w = a**5*b*a**2*b**-4*a + >>> w.generator_syllable( 3 ) + b + + """ + return self.array_form[i][0] + + def sub_syllables(self, from_i, to_j): + """ + `sub_syllables` returns the subword of the associative word `self` that + consists of syllables from positions `from_to` to `to_j`, where + `from_to` and `to_j` must be positive integers and indexing is done + with origin 0. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> f, a, b = free_group("a, b") + >>> w = a**5*b*a**2*b**-4*a + >>> w.sub_syllables(1, 2) + b + >>> w.sub_syllables(3, 3) + + + """ + if not isinstance(from_i, int) or not isinstance(to_j, int): + raise ValueError("both arguments should be integers") + group = self.group + if to_j <= from_i: + return group.identity + else: + r = tuple(self.array_form[from_i: to_j]) + return group.dtype(r) + + def substituted_word(self, from_i, to_j, by): + """ + Returns the associative word obtained by replacing the subword of + `self` that begins at position `from_i` and ends at position `to_j - 1` + by the associative word `by`. `from_i` and `to_j` must be positive + integers, indexing is done with origin 0. In other words, + `w.substituted_word(w, from_i, to_j, by)` is the product of the three + words: `w.subword(0, from_i)`, `by`, and + `w.subword(to_j len(w))`. + + See Also + ======== + + eliminate_word + + """ + lw = len(self) + if from_i >= to_j or from_i > lw or to_j > lw: + raise ValueError("values should be within bounds") + + # otherwise there are four possibilities + + # first if from=1 and to=lw then + if from_i == 0 and to_j == lw: + return by + elif from_i == 0: # second if from_i=1 (and to_j < lw) then + return by*self.subword(to_j, lw) + elif to_j == lw: # third if to_j=1 (and from_i > 1) then + return self.subword(0, from_i)*by + else: # finally + return self.subword(0, from_i)*by*self.subword(to_j, lw) + + def is_cyclically_reduced(self): + r"""Returns whether the word is cyclically reduced or not. + A word is cyclically reduced if by forming the cycle of the + word, the word is not reduced, i.e a word w = `a_1 ... a_n` + is called cyclically reduced if `a_1 \ne a_n^{-1}`. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> F, x, y = free_group("x, y") + >>> (x**2*y**-1*x**-1).is_cyclically_reduced() + False + >>> (y*x**2*y**2).is_cyclically_reduced() + True + + """ + if not self: + return True + return self[0] != self[-1]**-1 + + def identity_cyclic_reduction(self): + """Return a unique cyclically reduced version of the word. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> F, x, y = free_group("x, y") + >>> (x**2*y**2*x**-1).identity_cyclic_reduction() + x*y**2 + >>> (x**-3*y**-1*x**5).identity_cyclic_reduction() + x**2*y**-1 + + References + ========== + + .. [1] https://planetmath.org/cyclicallyreduced + + """ + word = self.copy() + group = self.group + while not word.is_cyclically_reduced(): + exp1 = word.exponent_syllable(0) + exp2 = word.exponent_syllable(-1) + r = exp1 + exp2 + if r == 0: + rep = word.array_form[1: word.number_syllables() - 1] + else: + rep = ((word.generator_syllable(0), exp1 + exp2),) + \ + word.array_form[1: word.number_syllables() - 1] + word = group.dtype(rep) + return word + + def cyclic_reduction(self, removed=False): + """Return a cyclically reduced version of the word. Unlike + `identity_cyclic_reduction`, this will not cyclically permute + the reduced word - just remove the "unreduced" bits on either + side of it. Compare the examples with those of + `identity_cyclic_reduction`. + + When `removed` is `True`, return a tuple `(word, r)` where + self `r` is such that before the reduction the word was either + `r*word*r**-1`. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> F, x, y = free_group("x, y") + >>> (x**2*y**2*x**-1).cyclic_reduction() + x*y**2 + >>> (x**-3*y**-1*x**5).cyclic_reduction() + y**-1*x**2 + >>> (x**-3*y**-1*x**5).cyclic_reduction(removed=True) + (y**-1*x**2, x**-3) + + """ + word = self.copy() + g = self.group.identity + while not word.is_cyclically_reduced(): + exp1 = abs(word.exponent_syllable(0)) + exp2 = abs(word.exponent_syllable(-1)) + exp = min(exp1, exp2) + start = word[0]**abs(exp) + end = word[-1]**abs(exp) + word = start**-1*word*end**-1 + g = g*start + if removed: + return word, g + return word + + def power_of(self, other): + ''' + Check if `self == other**n` for some integer n. + + Examples + ======== + + >>> from sympy.combinatorics import free_group + >>> F, x, y = free_group("x, y") + >>> ((x*y)**2).power_of(x*y) + True + >>> (x**-3*y**-2*x**3).power_of(x**-3*y*x**3) + True + + ''' + if self.is_identity: + return True + + l = len(other) + if l == 1: + # self has to be a power of one generator + gens = self.contains_generators() + s = other in gens or other**-1 in gens + return len(gens) == 1 and s + + # if self is not cyclically reduced and it is a power of other, + # other isn't cyclically reduced and the parts removed during + # their reduction must be equal + reduced, r1 = self.cyclic_reduction(removed=True) + if not r1.is_identity: + other, r2 = other.cyclic_reduction(removed=True) + if r1 == r2: + return reduced.power_of(other) + return False + + if len(self) < l or len(self) % l: + return False + + prefix = self.subword(0, l) + if prefix == other or prefix**-1 == other: + rest = self.subword(l, len(self)) + return rest.power_of(other) + return False + + +def letter_form_to_array_form(array_form, group): + """ + This method converts a list given with possible repetitions of elements in + it. It returns a new list such that repetitions of consecutive elements is + removed and replace with a tuple element of size two such that the first + index contains `value` and the second index contains the number of + consecutive repetitions of `value`. + + """ + a = list(array_form[:]) + new_array = [] + n = 1 + symbols = group.symbols + for i in range(len(a)): + if i == len(a) - 1: + if a[i] == a[i - 1]: + if (-a[i]) in symbols: + new_array.append((-a[i], -n)) + else: + new_array.append((a[i], n)) + else: + if (-a[i]) in symbols: + new_array.append((-a[i], -1)) + else: + new_array.append((a[i], 1)) + return new_array + elif a[i] == a[i + 1]: + n += 1 + else: + if (-a[i]) in symbols: + new_array.append((-a[i], -n)) + else: + new_array.append((a[i], n)) + n = 1 + + +def zero_mul_simp(l, index): + """Used to combine two reduced words.""" + while index >=0 and index < len(l) - 1 and l[index][0] == l[index + 1][0]: + exp = l[index][1] + l[index + 1][1] + base = l[index][0] + l[index] = (base, exp) + del l[index + 1] + if l[index][1] == 0: + del l[index] + index -= 1 diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/galois.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/galois.py new file mode 100644 index 0000000000000000000000000000000000000000..f8ff89886283903e116bf40e1be6f6131386e802 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/galois.py @@ -0,0 +1,611 @@ +r""" +Construct transitive subgroups of symmetric groups, useful in Galois theory. + +Besides constructing instances of the :py:class:`~.PermutationGroup` class to +represent the transitive subgroups of $S_n$ for small $n$, this module provides +*names* for these groups. + +In some applications, it may be preferable to know the name of a group, +rather than receive an instance of the :py:class:`~.PermutationGroup` +class, and then have to do extra work to determine which group it is, by +checking various properties. + +Names are instances of ``Enum`` classes defined in this module. With a name in +hand, the name's ``get_perm_group`` method can then be used to retrieve a +:py:class:`~.PermutationGroup`. + +The names used for groups in this module are taken from [1]. + +References +========== + +.. [1] Cohen, H. *A Course in Computational Algebraic Number Theory*. + +""" + +from collections import defaultdict +from enum import Enum +import itertools + +from sympy.combinatorics.named_groups import ( + SymmetricGroup, AlternatingGroup, CyclicGroup, DihedralGroup, + set_symmetric_group_properties, set_alternating_group_properties, +) +from sympy.combinatorics.perm_groups import PermutationGroup +from sympy.combinatorics.permutations import Permutation + + +class S1TransitiveSubgroups(Enum): + """ + Names for the transitive subgroups of S1. + """ + S1 = "S1" + + def get_perm_group(self): + return SymmetricGroup(1) + + +class S2TransitiveSubgroups(Enum): + """ + Names for the transitive subgroups of S2. + """ + S2 = "S2" + + def get_perm_group(self): + return SymmetricGroup(2) + + +class S3TransitiveSubgroups(Enum): + """ + Names for the transitive subgroups of S3. + """ + A3 = "A3" + S3 = "S3" + + def get_perm_group(self): + if self == S3TransitiveSubgroups.A3: + return AlternatingGroup(3) + elif self == S3TransitiveSubgroups.S3: + return SymmetricGroup(3) + + +class S4TransitiveSubgroups(Enum): + """ + Names for the transitive subgroups of S4. + """ + C4 = "C4" + V = "V" + D4 = "D4" + A4 = "A4" + S4 = "S4" + + def get_perm_group(self): + if self == S4TransitiveSubgroups.C4: + return CyclicGroup(4) + elif self == S4TransitiveSubgroups.V: + return four_group() + elif self == S4TransitiveSubgroups.D4: + return DihedralGroup(4) + elif self == S4TransitiveSubgroups.A4: + return AlternatingGroup(4) + elif self == S4TransitiveSubgroups.S4: + return SymmetricGroup(4) + + +class S5TransitiveSubgroups(Enum): + """ + Names for the transitive subgroups of S5. + """ + C5 = "C5" + D5 = "D5" + M20 = "M20" + A5 = "A5" + S5 = "S5" + + def get_perm_group(self): + if self == S5TransitiveSubgroups.C5: + return CyclicGroup(5) + elif self == S5TransitiveSubgroups.D5: + return DihedralGroup(5) + elif self == S5TransitiveSubgroups.M20: + return M20() + elif self == S5TransitiveSubgroups.A5: + return AlternatingGroup(5) + elif self == S5TransitiveSubgroups.S5: + return SymmetricGroup(5) + + +class S6TransitiveSubgroups(Enum): + """ + Names for the transitive subgroups of S6. + """ + C6 = "C6" + S3 = "S3" + D6 = "D6" + A4 = "A4" + G18 = "G18" + A4xC2 = "A4 x C2" + S4m = "S4-" + S4p = "S4+" + G36m = "G36-" + G36p = "G36+" + S4xC2 = "S4 x C2" + PSL2F5 = "PSL2(F5)" + G72 = "G72" + PGL2F5 = "PGL2(F5)" + A6 = "A6" + S6 = "S6" + + def get_perm_group(self): + if self == S6TransitiveSubgroups.C6: + return CyclicGroup(6) + elif self == S6TransitiveSubgroups.S3: + return S3_in_S6() + elif self == S6TransitiveSubgroups.D6: + return DihedralGroup(6) + elif self == S6TransitiveSubgroups.A4: + return A4_in_S6() + elif self == S6TransitiveSubgroups.G18: + return G18() + elif self == S6TransitiveSubgroups.A4xC2: + return A4xC2() + elif self == S6TransitiveSubgroups.S4m: + return S4m() + elif self == S6TransitiveSubgroups.S4p: + return S4p() + elif self == S6TransitiveSubgroups.G36m: + return G36m() + elif self == S6TransitiveSubgroups.G36p: + return G36p() + elif self == S6TransitiveSubgroups.S4xC2: + return S4xC2() + elif self == S6TransitiveSubgroups.PSL2F5: + return PSL2F5() + elif self == S6TransitiveSubgroups.G72: + return G72() + elif self == S6TransitiveSubgroups.PGL2F5: + return PGL2F5() + elif self == S6TransitiveSubgroups.A6: + return AlternatingGroup(6) + elif self == S6TransitiveSubgroups.S6: + return SymmetricGroup(6) + + +def four_group(): + """ + Return a representation of the Klein four-group as a transitive subgroup + of S4. + """ + return PermutationGroup( + Permutation(0, 1)(2, 3), + Permutation(0, 2)(1, 3) + ) + + +def M20(): + """ + Return a representation of the metacyclic group M20, a transitive subgroup + of S5 that is one of the possible Galois groups for polys of degree 5. + + Notes + ===== + + See [1], Page 323. + + """ + G = PermutationGroup(Permutation(0, 1, 2, 3, 4), Permutation(1, 2, 4, 3)) + G._degree = 5 + G._order = 20 + G._is_transitive = True + G._is_sym = False + G._is_alt = False + G._is_cyclic = False + G._is_dihedral = False + return G + + +def S3_in_S6(): + """ + Return a representation of S3 as a transitive subgroup of S6. + + Notes + ===== + + The representation is found by viewing the group as the symmetries of a + triangular prism. + + """ + G = PermutationGroup(Permutation(0, 1, 2)(3, 4, 5), Permutation(0, 3)(2, 4)(1, 5)) + set_symmetric_group_properties(G, 3, 6) + return G + + +def A4_in_S6(): + """ + Return a representation of A4 as a transitive subgroup of S6. + + Notes + ===== + + This was computed using :py:func:`~.find_transitive_subgroups_of_S6`. + + """ + G = PermutationGroup(Permutation(0, 4, 5)(1, 3, 2), Permutation(0, 1, 2)(3, 5, 4)) + set_alternating_group_properties(G, 4, 6) + return G + + +def S4m(): + """ + Return a representation of the S4- transitive subgroup of S6. + + Notes + ===== + + This was computed using :py:func:`~.find_transitive_subgroups_of_S6`. + + """ + G = PermutationGroup(Permutation(1, 4, 5, 3), Permutation(0, 4)(1, 5)(2, 3)) + set_symmetric_group_properties(G, 4, 6) + return G + + +def S4p(): + """ + Return a representation of the S4+ transitive subgroup of S6. + + Notes + ===== + + This was computed using :py:func:`~.find_transitive_subgroups_of_S6`. + + """ + G = PermutationGroup(Permutation(0, 2, 4, 1)(3, 5), Permutation(0, 3)(4, 5)) + set_symmetric_group_properties(G, 4, 6) + return G + + +def A4xC2(): + """ + Return a representation of the (A4 x C2) transitive subgroup of S6. + + Notes + ===== + + This was computed using :py:func:`~.find_transitive_subgroups_of_S6`. + + """ + return PermutationGroup( + Permutation(0, 4, 5)(1, 3, 2), Permutation(0, 1, 2)(3, 5, 4), + Permutation(5)(2, 4)) + + +def S4xC2(): + """ + Return a representation of the (S4 x C2) transitive subgroup of S6. + + Notes + ===== + + This was computed using :py:func:`~.find_transitive_subgroups_of_S6`. + + """ + return PermutationGroup( + Permutation(1, 4, 5, 3), Permutation(0, 4)(1, 5)(2, 3), + Permutation(1, 4)(3, 5)) + + +def G18(): + """ + Return a representation of the group G18, a transitive subgroup of S6 + isomorphic to the semidirect product of C3^2 with C2. + + Notes + ===== + + This was computed using :py:func:`~.find_transitive_subgroups_of_S6`. + + """ + return PermutationGroup( + Permutation(5)(0, 1, 2), Permutation(3, 4, 5), + Permutation(0, 4)(1, 5)(2, 3)) + + +def G36m(): + """ + Return a representation of the group G36-, a transitive subgroup of S6 + isomorphic to the semidirect product of C3^2 with C2^2. + + Notes + ===== + + This was computed using :py:func:`~.find_transitive_subgroups_of_S6`. + + """ + return PermutationGroup( + Permutation(5)(0, 1, 2), Permutation(3, 4, 5), + Permutation(1, 2)(3, 5), Permutation(0, 4)(1, 5)(2, 3)) + + +def G36p(): + """ + Return a representation of the group G36+, a transitive subgroup of S6 + isomorphic to the semidirect product of C3^2 with C4. + + Notes + ===== + + This was computed using :py:func:`~.find_transitive_subgroups_of_S6`. + + """ + return PermutationGroup( + Permutation(5)(0, 1, 2), Permutation(3, 4, 5), + Permutation(0, 5, 2, 3)(1, 4)) + + +def G72(): + """ + Return a representation of the group G72, a transitive subgroup of S6 + isomorphic to the semidirect product of C3^2 with D4. + + Notes + ===== + + See [1], Page 325. + + """ + return PermutationGroup( + Permutation(5)(0, 1, 2), + Permutation(0, 4, 1, 3)(2, 5), Permutation(0, 3)(1, 4)(2, 5)) + + +def PSL2F5(): + r""" + Return a representation of the group $PSL_2(\mathbb{F}_5)$, as a transitive + subgroup of S6, isomorphic to $A_5$. + + Notes + ===== + + This was computed using :py:func:`~.find_transitive_subgroups_of_S6`. + + """ + G = PermutationGroup( + Permutation(0, 4, 5)(1, 3, 2), Permutation(0, 4, 3, 1, 5)) + set_alternating_group_properties(G, 5, 6) + return G + + +def PGL2F5(): + r""" + Return a representation of the group $PGL_2(\mathbb{F}_5)$, as a transitive + subgroup of S6, isomorphic to $S_5$. + + Notes + ===== + + See [1], Page 325. + + """ + G = PermutationGroup( + Permutation(0, 1, 2, 3, 4), Permutation(0, 5)(1, 2)(3, 4)) + set_symmetric_group_properties(G, 5, 6) + return G + + +def find_transitive_subgroups_of_S6(*targets, print_report=False): + r""" + Search for certain transitive subgroups of $S_6$. + + The symmetric group $S_6$ has 16 different transitive subgroups, up to + conjugacy. Some are more easily constructed than others. For example, the + dihedral group $D_6$ is immediately found, but it is not at all obvious how + to realize $S_4$ or $S_5$ *transitively* within $S_6$. + + In some cases there are well-known constructions that can be used. For + example, $S_5$ is isomorphic to $PGL_2(\mathbb{F}_5)$, which acts in a + natural way on the projective line $P^1(\mathbb{F}_5)$, a set of order 6. + + In absence of such special constructions however, we can simply search for + generators. For example, transitive instances of $A_4$ and $S_4$ can be + found within $S_6$ in this way. + + Once we are engaged in such searches, it may then be easier (if less + elegant) to find even those groups like $S_5$ that do have special + constructions, by mere search. + + This function locates generators for transitive instances in $S_6$ of the + following subgroups: + + * $A_4$ + * $S_4^-$ ($S_4$ not contained within $A_6$) + * $S_4^+$ ($S_4$ contained within $A_6$) + * $A_4 \times C_2$ + * $S_4 \times C_2$ + * $G_{18} = C_3^2 \rtimes C_2$ + * $G_{36}^- = C_3^2 \rtimes C_2^2$ + * $G_{36}^+ = C_3^2 \rtimes C_4$ + * $G_{72} = C_3^2 \rtimes D_4$ + * $A_5$ + * $S_5$ + + Note: Each of these groups also has a dedicated function in this module + that returns the group immediately, using generators that were found by + this search procedure. + + The search procedure serves as a record of how these generators were + found. Also, due to randomness in the generation of the elements of + permutation groups, it can be called again, in order to (probably) get + different generators for the same groups. + + Parameters + ========== + + targets : list of :py:class:`~.S6TransitiveSubgroups` values + The groups you want to find. + + print_report : bool (default False) + If True, print to stdout the generators found for each group. + + Returns + ======= + + dict + mapping each name in *targets* to the :py:class:`~.PermutationGroup` + that was found + + References + ========== + + .. [2] https://en.wikipedia.org/wiki/Projective_linear_group#Exceptional_isomorphisms + .. [3] https://en.wikipedia.org/wiki/Automorphisms_of_the_symmetric_and_alternating_groups#PGL%282,5%29 + + """ + def elts_by_order(G): + """Sort the elements of a group by their order. """ + elts = defaultdict(list) + for g in G.elements: + elts[g.order()].append(g) + return elts + + def order_profile(G, name=None): + """Determine how many elements a group has, of each order. """ + elts = elts_by_order(G) + profile = {o:len(e) for o, e in elts.items()} + if name: + print(f'{name}: ' + ' '.join(f'{len(profile[r])}@{r}' for r in sorted(profile.keys()))) + return profile + + S6 = SymmetricGroup(6) + A6 = AlternatingGroup(6) + S6_by_order = elts_by_order(S6) + + def search(existing_gens, needed_gen_orders, order, alt=None, profile=None, anti_profile=None): + """ + Find a transitive subgroup of S6. + + Parameters + ========== + + existing_gens : list of Permutation + Optionally empty list of generators that must be in the group. + + needed_gen_orders : list of positive int + Nonempty list of the orders of the additional generators that are + to be found. + + order: int + The order of the group being sought. + + alt: bool, None + If True, require the group to be contained in A6. + If False, require the group not to be contained in A6. + + profile : dict + If given, the group's order profile must equal this. + + anti_profile : dict + If given, the group's order profile must *not* equal this. + + """ + for gens in itertools.product(*[S6_by_order[n] for n in needed_gen_orders]): + if len(set(gens)) < len(gens): + continue + G = PermutationGroup(existing_gens + list(gens)) + if G.order() == order and G.is_transitive(): + if alt is not None and G.is_subgroup(A6) != alt: + continue + if profile and order_profile(G) != profile: + continue + if anti_profile and order_profile(G) == anti_profile: + continue + return G + + def match_known_group(G, alt=None): + needed = [g.order() for g in G.generators] + return search([], needed, G.order(), alt=alt, profile=order_profile(G)) + + found = {} + + def finish_up(name, G): + found[name] = G + if print_report: + print("=" * 40) + print(f"{name}:") + print(G.generators) + + if S6TransitiveSubgroups.A4 in targets or S6TransitiveSubgroups.A4xC2 in targets: + A4_in_S6 = match_known_group(AlternatingGroup(4)) + finish_up(S6TransitiveSubgroups.A4, A4_in_S6) + + if S6TransitiveSubgroups.S4m in targets or S6TransitiveSubgroups.S4xC2 in targets: + S4m_in_S6 = match_known_group(SymmetricGroup(4), alt=False) + finish_up(S6TransitiveSubgroups.S4m, S4m_in_S6) + + if S6TransitiveSubgroups.S4p in targets: + S4p_in_S6 = match_known_group(SymmetricGroup(4), alt=True) + finish_up(S6TransitiveSubgroups.S4p, S4p_in_S6) + + if S6TransitiveSubgroups.A4xC2 in targets: + A4xC2_in_S6 = search(A4_in_S6.generators, [2], 24, anti_profile=order_profile(SymmetricGroup(4))) + finish_up(S6TransitiveSubgroups.A4xC2, A4xC2_in_S6) + + if S6TransitiveSubgroups.S4xC2 in targets: + S4xC2_in_S6 = search(S4m_in_S6.generators, [2], 48) + finish_up(S6TransitiveSubgroups.S4xC2, S4xC2_in_S6) + + # For the normal factor N = C3^2 in any of the G_n subgroups, we take one + # obvious instance of C3^2 in S6: + N_gens = [Permutation(5)(0, 1, 2), Permutation(5)(3, 4, 5)] + + if S6TransitiveSubgroups.G18 in targets: + G18_in_S6 = search(N_gens, [2], 18) + finish_up(S6TransitiveSubgroups.G18, G18_in_S6) + + if S6TransitiveSubgroups.G36m in targets: + G36m_in_S6 = search(N_gens, [2, 2], 36, alt=False) + finish_up(S6TransitiveSubgroups.G36m, G36m_in_S6) + + if S6TransitiveSubgroups.G36p in targets: + G36p_in_S6 = search(N_gens, [4], 36, alt=True) + finish_up(S6TransitiveSubgroups.G36p, G36p_in_S6) + + if S6TransitiveSubgroups.G72 in targets: + G72_in_S6 = search(N_gens, [4, 2], 72) + finish_up(S6TransitiveSubgroups.G72, G72_in_S6) + + # The PSL2(F5) and PGL2(F5) subgroups are isomorphic to A5 and S5, resp. + + if S6TransitiveSubgroups.PSL2F5 in targets: + PSL2F5_in_S6 = match_known_group(AlternatingGroup(5)) + finish_up(S6TransitiveSubgroups.PSL2F5, PSL2F5_in_S6) + + if S6TransitiveSubgroups.PGL2F5 in targets: + PGL2F5_in_S6 = match_known_group(SymmetricGroup(5)) + finish_up(S6TransitiveSubgroups.PGL2F5, PGL2F5_in_S6) + + # There is little need to "search" for any of the groups C6, S3, D6, A6, + # or S6, since they all have obvious realizations within S6. However, we + # support them here just in case a random representation is desired. + + if S6TransitiveSubgroups.C6 in targets: + C6 = match_known_group(CyclicGroup(6)) + finish_up(S6TransitiveSubgroups.C6, C6) + + if S6TransitiveSubgroups.S3 in targets: + S3 = match_known_group(SymmetricGroup(3)) + finish_up(S6TransitiveSubgroups.S3, S3) + + if S6TransitiveSubgroups.D6 in targets: + D6 = match_known_group(DihedralGroup(6)) + finish_up(S6TransitiveSubgroups.D6, D6) + + if S6TransitiveSubgroups.A6 in targets: + A6 = match_known_group(A6) + finish_up(S6TransitiveSubgroups.A6, A6) + + if S6TransitiveSubgroups.S6 in targets: + S6 = match_known_group(S6) + finish_up(S6TransitiveSubgroups.S6, S6) + + return found diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/generators.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/generators.py new file mode 100644 index 0000000000000000000000000000000000000000..19e274a4c5b4bf0781855db6bc6bf499349fff31 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/generators.py @@ -0,0 +1,301 @@ +from sympy.combinatorics.permutations import Permutation +from sympy.core.symbol import symbols +from sympy.matrices import Matrix +from sympy.utilities.iterables import variations, rotate_left + + +def symmetric(n): + """ + Generates the symmetric group of order n, Sn. + + Examples + ======== + + >>> from sympy.combinatorics.generators import symmetric + >>> list(symmetric(3)) + [(2), (1 2), (2)(0 1), (0 1 2), (0 2 1), (0 2)] + """ + yield from (Permutation(perm) for perm in variations(range(n), n)) + + +def cyclic(n): + """ + Generates the cyclic group of order n, Cn. + + Examples + ======== + + >>> from sympy.combinatorics.generators import cyclic + >>> list(cyclic(5)) + [(4), (0 1 2 3 4), (0 2 4 1 3), + (0 3 1 4 2), (0 4 3 2 1)] + + See Also + ======== + + dihedral + """ + gen = list(range(n)) + for i in range(n): + yield Permutation(gen) + gen = rotate_left(gen, 1) + + +def alternating(n): + """ + Generates the alternating group of order n, An. + + Examples + ======== + + >>> from sympy.combinatorics.generators import alternating + >>> list(alternating(3)) + [(2), (0 1 2), (0 2 1)] + """ + for perm in variations(range(n), n): + p = Permutation(perm) + if p.is_even: + yield p + + +def dihedral(n): + """ + Generates the dihedral group of order 2n, Dn. + + The result is given as a subgroup of Sn, except for the special cases n=1 + (the group S2) and n=2 (the Klein 4-group) where that's not possible + and embeddings in S2 and S4 respectively are given. + + Examples + ======== + + >>> from sympy.combinatorics.generators import dihedral + >>> list(dihedral(3)) + [(2), (0 2), (0 1 2), (1 2), (0 2 1), (2)(0 1)] + + See Also + ======== + + cyclic + """ + if n == 1: + yield Permutation([0, 1]) + yield Permutation([1, 0]) + elif n == 2: + yield Permutation([0, 1, 2, 3]) + yield Permutation([1, 0, 3, 2]) + yield Permutation([2, 3, 0, 1]) + yield Permutation([3, 2, 1, 0]) + else: + gen = list(range(n)) + for i in range(n): + yield Permutation(gen) + yield Permutation(gen[::-1]) + gen = rotate_left(gen, 1) + + +def rubik_cube_generators(): + """Return the permutations of the 3x3 Rubik's cube, see + https://www.gap-system.org/Doc/Examples/rubik.html + """ + a = [ + [(1, 3, 8, 6), (2, 5, 7, 4), (9, 33, 25, 17), (10, 34, 26, 18), + (11, 35, 27, 19)], + [(9, 11, 16, 14), (10, 13, 15, 12), (1, 17, 41, 40), (4, 20, 44, 37), + (6, 22, 46, 35)], + [(17, 19, 24, 22), (18, 21, 23, 20), (6, 25, 43, 16), (7, 28, 42, 13), + (8, 30, 41, 11)], + [(25, 27, 32, 30), (26, 29, 31, 28), (3, 38, 43, 19), (5, 36, 45, 21), + (8, 33, 48, 24)], + [(33, 35, 40, 38), (34, 37, 39, 36), (3, 9, 46, 32), (2, 12, 47, 29), + (1, 14, 48, 27)], + [(41, 43, 48, 46), (42, 45, 47, 44), (14, 22, 30, 38), + (15, 23, 31, 39), (16, 24, 32, 40)] + ] + return [Permutation([[i - 1 for i in xi] for xi in x], size=48) for x in a] + + +def rubik(n): + """Return permutations for an nxn Rubik's cube. + + Permutations returned are for rotation of each of the slice + from the face up to the last face for each of the 3 sides (in this order): + front, right and bottom. Hence, the first n - 1 permutations are for the + slices from the front. + """ + + if n < 2: + raise ValueError('dimension of cube must be > 1') + + # 1-based reference to rows and columns in Matrix + def getr(f, i): + return faces[f].col(n - i) + + def getl(f, i): + return faces[f].col(i - 1) + + def getu(f, i): + return faces[f].row(i - 1) + + def getd(f, i): + return faces[f].row(n - i) + + def setr(f, i, s): + faces[f][:, n - i] = Matrix(n, 1, s) + + def setl(f, i, s): + faces[f][:, i - 1] = Matrix(n, 1, s) + + def setu(f, i, s): + faces[f][i - 1, :] = Matrix(1, n, s) + + def setd(f, i, s): + faces[f][n - i, :] = Matrix(1, n, s) + + # motion of a single face + def cw(F, r=1): + for _ in range(r): + face = faces[F] + rv = [] + for c in range(n): + for r in range(n - 1, -1, -1): + rv.append(face[r, c]) + faces[F] = Matrix(n, n, rv) + + def ccw(F): + cw(F, 3) + + # motion of plane i from the F side; + # fcw(0) moves the F face, fcw(1) moves the plane + # just behind the front face, etc... + def fcw(i, r=1): + for _ in range(r): + if i == 0: + cw(F) + i += 1 + temp = getr(L, i) + setr(L, i, list(getu(D, i))) + setu(D, i, list(reversed(getl(R, i)))) + setl(R, i, list(getd(U, i))) + setd(U, i, list(reversed(temp))) + i -= 1 + + def fccw(i): + fcw(i, 3) + + # motion of the entire cube from the F side + def FCW(r=1): + for _ in range(r): + cw(F) + ccw(B) + cw(U) + t = faces[U] + cw(L) + faces[U] = faces[L] + cw(D) + faces[L] = faces[D] + cw(R) + faces[D] = faces[R] + faces[R] = t + + def FCCW(): + FCW(3) + + # motion of the entire cube from the U side + def UCW(r=1): + for _ in range(r): + cw(U) + ccw(D) + t = faces[F] + faces[F] = faces[R] + faces[R] = faces[B] + faces[B] = faces[L] + faces[L] = t + + def UCCW(): + UCW(3) + + # defining the permutations for the cube + + U, F, R, B, L, D = names = symbols('U, F, R, B, L, D') + + # the faces are represented by nxn matrices + faces = {} + count = 0 + for fi in range(6): + f = [] + for a in range(n**2): + f.append(count) + count += 1 + faces[names[fi]] = Matrix(n, n, f) + + # this will either return the value of the current permutation + # (show != 1) or else append the permutation to the group, g + def perm(show=0): + # add perm to the list of perms + p = [] + for f in names: + p.extend(faces[f]) + if show: + return p + g.append(Permutation(p)) + + g = [] # container for the group's permutations + I = list(range(6*n**2)) # the identity permutation used for checking + + # define permutations corresponding to cw rotations of the planes + # up TO the last plane from that direction; by not including the + # last plane, the orientation of the cube is maintained. + + # F slices + for i in range(n - 1): + fcw(i) + perm() + fccw(i) # restore + assert perm(1) == I + + # R slices + # bring R to front + UCW() + for i in range(n - 1): + fcw(i) + # put it back in place + UCCW() + # record + perm() + # restore + # bring face to front + UCW() + fccw(i) + # restore + UCCW() + assert perm(1) == I + + # D slices + # bring up bottom + FCW() + UCCW() + FCCW() + for i in range(n - 1): + # turn strip + fcw(i) + # put bottom back on the bottom + FCW() + UCW() + FCCW() + # record + perm() + # restore + # bring up bottom + FCW() + UCCW() + FCCW() + # turn strip + fccw(i) + # put bottom back on the bottom + FCW() + UCW() + FCCW() + assert perm(1) == I + + return g diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/graycode.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/graycode.py new file mode 100644 index 0000000000000000000000000000000000000000..930fd337862a70e920a985947d74375b27741293 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/graycode.py @@ -0,0 +1,430 @@ +from sympy.core import Basic, Integer + +import random + + +class GrayCode(Basic): + """ + A Gray code is essentially a Hamiltonian walk on + a n-dimensional cube with edge length of one. + The vertices of the cube are represented by vectors + whose values are binary. The Hamilton walk visits + each vertex exactly once. The Gray code for a 3d + cube is ['000','100','110','010','011','111','101', + '001']. + + A Gray code solves the problem of sequentially + generating all possible subsets of n objects in such + a way that each subset is obtained from the previous + one by either deleting or adding a single object. + In the above example, 1 indicates that the object is + present, and 0 indicates that its absent. + + Gray codes have applications in statistics as well when + we want to compute various statistics related to subsets + in an efficient manner. + + Examples + ======== + + >>> from sympy.combinatorics import GrayCode + >>> a = GrayCode(3) + >>> list(a.generate_gray()) + ['000', '001', '011', '010', '110', '111', '101', '100'] + >>> a = GrayCode(4) + >>> list(a.generate_gray()) + ['0000', '0001', '0011', '0010', '0110', '0111', '0101', '0100', \ + '1100', '1101', '1111', '1110', '1010', '1011', '1001', '1000'] + + References + ========== + + .. [1] Nijenhuis,A. and Wilf,H.S.(1978). + Combinatorial Algorithms. Academic Press. + .. [2] Knuth, D. (2011). The Art of Computer Programming, Vol 4 + Addison Wesley + + + """ + + _skip = False + _current = 0 + _rank = None + + def __new__(cls, n, *args, **kw_args): + """ + Default constructor. + + It takes a single argument ``n`` which gives the dimension of the Gray + code. The starting Gray code string (``start``) or the starting ``rank`` + may also be given; the default is to start at rank = 0 ('0...0'). + + Examples + ======== + + >>> from sympy.combinatorics import GrayCode + >>> a = GrayCode(3) + >>> a + GrayCode(3) + >>> a.n + 3 + + >>> a = GrayCode(3, start='100') + >>> a.current + '100' + + >>> a = GrayCode(4, rank=4) + >>> a.current + '0110' + >>> a.rank + 4 + + """ + if n < 1 or int(n) != n: + raise ValueError( + 'Gray code dimension must be a positive integer, not %i' % n) + n = Integer(n) + args = (n,) + args + obj = Basic.__new__(cls, *args) + if 'start' in kw_args: + obj._current = kw_args["start"] + if len(obj._current) > n: + raise ValueError('Gray code start has length %i but ' + 'should not be greater than %i' % (len(obj._current), n)) + elif 'rank' in kw_args: + if int(kw_args["rank"]) != kw_args["rank"]: + raise ValueError('Gray code rank must be a positive integer, ' + 'not %i' % kw_args["rank"]) + obj._rank = int(kw_args["rank"]) % obj.selections + obj._current = obj.unrank(n, obj._rank) + return obj + + def next(self, delta=1): + """ + Returns the Gray code a distance ``delta`` (default = 1) from the + current value in canonical order. + + + Examples + ======== + + >>> from sympy.combinatorics import GrayCode + >>> a = GrayCode(3, start='110') + >>> a.next().current + '111' + >>> a.next(-1).current + '010' + """ + return GrayCode(self.n, rank=(self.rank + delta) % self.selections) + + @property + def selections(self): + """ + Returns the number of bit vectors in the Gray code. + + Examples + ======== + + >>> from sympy.combinatorics import GrayCode + >>> a = GrayCode(3) + >>> a.selections + 8 + """ + return 2**self.n + + @property + def n(self): + """ + Returns the dimension of the Gray code. + + Examples + ======== + + >>> from sympy.combinatorics import GrayCode + >>> a = GrayCode(5) + >>> a.n + 5 + """ + return self.args[0] + + def generate_gray(self, **hints): + """ + Generates the sequence of bit vectors of a Gray Code. + + Examples + ======== + + >>> from sympy.combinatorics import GrayCode + >>> a = GrayCode(3) + >>> list(a.generate_gray()) + ['000', '001', '011', '010', '110', '111', '101', '100'] + >>> list(a.generate_gray(start='011')) + ['011', '010', '110', '111', '101', '100'] + >>> list(a.generate_gray(rank=4)) + ['110', '111', '101', '100'] + + See Also + ======== + + skip + + References + ========== + + .. [1] Knuth, D. (2011). The Art of Computer Programming, + Vol 4, Addison Wesley + + """ + bits = self.n + start = None + if "start" in hints: + start = hints["start"] + elif "rank" in hints: + start = GrayCode.unrank(self.n, hints["rank"]) + if start is not None: + self._current = start + current = self.current + graycode_bin = gray_to_bin(current) + if len(graycode_bin) > self.n: + raise ValueError('Gray code start has length %i but should ' + 'not be greater than %i' % (len(graycode_bin), bits)) + self._current = int(current, 2) + graycode_int = int(''.join(graycode_bin), 2) + for i in range(graycode_int, 1 << bits): + if self._skip: + self._skip = False + else: + yield self.current + bbtc = (i ^ (i + 1)) + gbtc = (bbtc ^ (bbtc >> 1)) + self._current = (self._current ^ gbtc) + self._current = 0 + + def skip(self): + """ + Skips the bit generation. + + Examples + ======== + + >>> from sympy.combinatorics import GrayCode + >>> a = GrayCode(3) + >>> for i in a.generate_gray(): + ... if i == '010': + ... a.skip() + ... print(i) + ... + 000 + 001 + 011 + 010 + 111 + 101 + 100 + + See Also + ======== + + generate_gray + """ + self._skip = True + + @property + def rank(self): + """ + Ranks the Gray code. + + A ranking algorithm determines the position (or rank) + of a combinatorial object among all the objects w.r.t. + a given order. For example, the 4 bit binary reflected + Gray code (BRGC) '0101' has a rank of 6 as it appears in + the 6th position in the canonical ordering of the family + of 4 bit Gray codes. + + Examples + ======== + + >>> from sympy.combinatorics import GrayCode + >>> a = GrayCode(3) + >>> list(a.generate_gray()) + ['000', '001', '011', '010', '110', '111', '101', '100'] + >>> GrayCode(3, start='100').rank + 7 + >>> GrayCode(3, rank=7).current + '100' + + See Also + ======== + + unrank + + References + ========== + + .. [1] https://web.archive.org/web/20200224064753/http://statweb.stanford.edu/~susan/courses/s208/node12.html + + """ + if self._rank is None: + self._rank = int(gray_to_bin(self.current), 2) + return self._rank + + @property + def current(self): + """ + Returns the currently referenced Gray code as a bit string. + + Examples + ======== + + >>> from sympy.combinatorics import GrayCode + >>> GrayCode(3, start='100').current + '100' + """ + rv = self._current or '0' + if not isinstance(rv, str): + rv = bin(rv)[2:] + return rv.rjust(self.n, '0') + + @classmethod + def unrank(self, n, rank): + """ + Unranks an n-bit sized Gray code of rank k. This method exists + so that a derivative GrayCode class can define its own code of + a given rank. + + The string here is generated in reverse order to allow for tail-call + optimization. + + Examples + ======== + + >>> from sympy.combinatorics import GrayCode + >>> GrayCode(5, rank=3).current + '00010' + >>> GrayCode.unrank(5, 3) + '00010' + + See Also + ======== + + rank + """ + def _unrank(k, n): + if n == 1: + return str(k % 2) + m = 2**(n - 1) + if k < m: + return '0' + _unrank(k, n - 1) + return '1' + _unrank(m - (k % m) - 1, n - 1) + return _unrank(rank, n) + + +def random_bitstring(n): + """ + Generates a random bitlist of length n. + + Examples + ======== + + >>> from sympy.combinatorics.graycode import random_bitstring + >>> random_bitstring(3) # doctest: +SKIP + 100 + """ + return ''.join([random.choice('01') for i in range(n)]) + + +def gray_to_bin(bin_list): + """ + Convert from Gray coding to binary coding. + + We assume big endian encoding. + + Examples + ======== + + >>> from sympy.combinatorics.graycode import gray_to_bin + >>> gray_to_bin('100') + '111' + + See Also + ======== + + bin_to_gray + """ + b = [bin_list[0]] + for i in range(1, len(bin_list)): + b += str(int(b[i - 1] != bin_list[i])) + return ''.join(b) + + +def bin_to_gray(bin_list): + """ + Convert from binary coding to gray coding. + + We assume big endian encoding. + + Examples + ======== + + >>> from sympy.combinatorics.graycode import bin_to_gray + >>> bin_to_gray('111') + '100' + + See Also + ======== + + gray_to_bin + """ + b = [bin_list[0]] + for i in range(1, len(bin_list)): + b += str(int(bin_list[i]) ^ int(bin_list[i - 1])) + return ''.join(b) + + +def get_subset_from_bitstring(super_set, bitstring): + """ + Gets the subset defined by the bitstring. + + Examples + ======== + + >>> from sympy.combinatorics.graycode import get_subset_from_bitstring + >>> get_subset_from_bitstring(['a', 'b', 'c', 'd'], '0011') + ['c', 'd'] + >>> get_subset_from_bitstring(['c', 'a', 'c', 'c'], '1100') + ['c', 'a'] + + See Also + ======== + + graycode_subsets + """ + if len(super_set) != len(bitstring): + raise ValueError("The sizes of the lists are not equal") + return [super_set[i] for i, j in enumerate(bitstring) + if bitstring[i] == '1'] + + +def graycode_subsets(gray_code_set): + """ + Generates the subsets as enumerated by a Gray code. + + Examples + ======== + + >>> from sympy.combinatorics.graycode import graycode_subsets + >>> list(graycode_subsets(['a', 'b', 'c'])) + [[], ['c'], ['b', 'c'], ['b'], ['a', 'b'], ['a', 'b', 'c'], \ + ['a', 'c'], ['a']] + >>> list(graycode_subsets(['a', 'b', 'c', 'c'])) + [[], ['c'], ['c', 'c'], ['c'], ['b', 'c'], ['b', 'c', 'c'], \ + ['b', 'c'], ['b'], ['a', 'b'], ['a', 'b', 'c'], ['a', 'b', 'c', 'c'], \ + ['a', 'b', 'c'], ['a', 'c'], ['a', 'c', 'c'], ['a', 'c'], ['a']] + + See Also + ======== + + get_subset_from_bitstring + """ + for bitstring in list(GrayCode(len(gray_code_set)).generate_gray()): + yield get_subset_from_bitstring(gray_code_set, bitstring) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/group_constructs.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/group_constructs.py new file mode 100644 index 0000000000000000000000000000000000000000..a5c16ec254191646b26eee869763e2926e187da5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/group_constructs.py @@ -0,0 +1,61 @@ +from sympy.combinatorics.perm_groups import PermutationGroup +from sympy.combinatorics.permutations import Permutation +from sympy.utilities.iterables import uniq + +_af_new = Permutation._af_new + + +def DirectProduct(*groups): + """ + Returns the direct product of several groups as a permutation group. + + Explanation + =========== + + This is implemented much like the __mul__ procedure for taking the direct + product of two permutation groups, but the idea of shifting the + generators is realized in the case of an arbitrary number of groups. + A call to DirectProduct(G1, G2, ..., Gn) is generally expected to be faster + than a call to G1*G2*...*Gn (and thus the need for this algorithm). + + Examples + ======== + + >>> from sympy.combinatorics.group_constructs import DirectProduct + >>> from sympy.combinatorics.named_groups import CyclicGroup + >>> C = CyclicGroup(4) + >>> G = DirectProduct(C, C, C) + >>> G.order() + 64 + + See Also + ======== + + sympy.combinatorics.perm_groups.PermutationGroup.__mul__ + + """ + degrees = [] + gens_count = [] + total_degree = 0 + total_gens = 0 + for group in groups: + current_deg = group.degree + current_num_gens = len(group.generators) + degrees.append(current_deg) + total_degree += current_deg + gens_count.append(current_num_gens) + total_gens += current_num_gens + array_gens = [] + for i in range(total_gens): + array_gens.append(list(range(total_degree))) + current_gen = 0 + current_deg = 0 + for i in range(len(gens_count)): + for j in range(current_gen, current_gen + gens_count[i]): + gen = ((groups[i].generators)[j - current_gen]).array_form + array_gens[j][current_deg:current_deg + degrees[i]] = \ + [x + current_deg for x in gen] + current_gen += gens_count[i] + current_deg += degrees[i] + perm_gens = list(uniq([_af_new(list(a)) for a in array_gens])) + return PermutationGroup(perm_gens, dups=False) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/group_numbers.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/group_numbers.py new file mode 100644 index 0000000000000000000000000000000000000000..8099dfc2ed8a6943aa1261f9374ed84f0ad3c522 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/group_numbers.py @@ -0,0 +1,294 @@ +from itertools import chain, combinations + +from sympy.external.gmpy import gcd +from sympy.ntheory.factor_ import factorint +from sympy.utilities.misc import as_int + + +def _is_nilpotent_number(factors: dict) -> bool: + """ Check whether `n` is a nilpotent number. + Note that ``factors`` is a prime factorization of `n`. + + This is a low-level helper for ``is_nilpotent_number``, for internal use. + """ + for p in factors.keys(): + for q, e in factors.items(): + # We want to calculate + # any(pow(q, k, p) == 1 for k in range(1, e + 1)) + m = 1 + for _ in range(e): + m = m*q % p + if m == 1: + return False + return True + + +def is_nilpotent_number(n) -> bool: + """ + Check whether `n` is a nilpotent number. A number `n` is said to be + nilpotent if and only if every finite group of order `n` is nilpotent. + For more information see [1]_. + + Examples + ======== + + >>> from sympy.combinatorics.group_numbers import is_nilpotent_number + >>> from sympy import randprime + >>> is_nilpotent_number(21) + False + >>> is_nilpotent_number(randprime(1, 30)**12) + True + + References + ========== + + .. [1] Pakianathan, J., Shankar, K., Nilpotent Numbers, + The American Mathematical Monthly, 107(7), 631-634. + .. [2] https://oeis.org/A056867 + + """ + n = as_int(n) + if n <= 0: + raise ValueError("n must be a positive integer, not %i" % n) + return _is_nilpotent_number(factorint(n)) + + +def is_abelian_number(n) -> bool: + """ + Check whether `n` is an abelian number. A number `n` is said to be abelian + if and only if every finite group of order `n` is abelian. For more + information see [1]_. + + Examples + ======== + + >>> from sympy.combinatorics.group_numbers import is_abelian_number + >>> from sympy import randprime + >>> is_abelian_number(4) + True + >>> is_abelian_number(randprime(1, 2000)**2) + True + >>> is_abelian_number(60) + False + + References + ========== + + .. [1] Pakianathan, J., Shankar, K., Nilpotent Numbers, + The American Mathematical Monthly, 107(7), 631-634. + .. [2] https://oeis.org/A051532 + + """ + n = as_int(n) + if n <= 0: + raise ValueError("n must be a positive integer, not %i" % n) + factors = factorint(n) + return all(e < 3 for e in factors.values()) and _is_nilpotent_number(factors) + + +def is_cyclic_number(n) -> bool: + """ + Check whether `n` is a cyclic number. A number `n` is said to be cyclic + if and only if every finite group of order `n` is cyclic. For more + information see [1]_. + + Examples + ======== + + >>> from sympy.combinatorics.group_numbers import is_cyclic_number + >>> from sympy import randprime + >>> is_cyclic_number(15) + True + >>> is_cyclic_number(randprime(1, 2000)**2) + False + >>> is_cyclic_number(4) + False + + References + ========== + + .. [1] Pakianathan, J., Shankar, K., Nilpotent Numbers, + The American Mathematical Monthly, 107(7), 631-634. + .. [2] https://oeis.org/A003277 + + """ + n = as_int(n) + if n <= 0: + raise ValueError("n must be a positive integer, not %i" % n) + factors = factorint(n) + return all(e == 1 for e in factors.values()) and _is_nilpotent_number(factors) + + +def _holder_formula(prime_factors): + r""" Number of groups of order `n`. + where `n` is squarefree and its prime factors are ``prime_factors``. + i.e., ``n == math.prod(prime_factors)`` + + Explanation + =========== + + When `n` is squarefree, the number of groups of order `n` is expressed by + + .. math :: + \sum_{d \mid n} \prod_p \frac{p^{c(p, d)} - 1}{p - 1} + + where `n=de`, `p` is the prime factor of `e`, + and `c(p, d)` is the number of prime factors `q` of `d` such that `q \equiv 1 \pmod{p}` [2]_. + + The formula is elegant, but can be improved when implemented as an algorithm. + Since `n` is assumed to be squarefree, the divisor `d` of `n` can be identified with the power set of prime factors. + We let `N` be the set of prime factors of `n`. + `F = \{p \in N : \forall q \in N, q \not\equiv 1 \pmod{p} \}, M = N \setminus F`, we have the following. + + .. math :: + \sum_{d \in 2^{M}} \prod_{p \in M \setminus d} \frac{p^{c(p, F \cup d)} - 1}{p - 1} + + Practically, many prime factors are expected to be members of `F`, thus reducing computation time. + + Parameters + ========== + + prime_factors : set + The set of prime factors of ``n``. where `n` is squarefree. + + Returns + ======= + + int : Number of groups of order ``n`` + + Examples + ======== + + >>> from sympy.combinatorics.group_numbers import _holder_formula + >>> _holder_formula({2}) # n = 2 + 1 + >>> _holder_formula({2, 3}) # n = 2*3 = 6 + 2 + + See Also + ======== + + groups_count + + References + ========== + + .. [1] Otto Holder, Die Gruppen der Ordnungen p^3, pq^2, pqr, p^4, + Math. Ann. 43 pp. 301-412 (1893). + http://dx.doi.org/10.1007/BF01443651 + .. [2] John H. Conway, Heiko Dietrich and E.A. O'Brien, + Counting groups: gnus, moas and other exotica + The Mathematical Intelligencer 30, 6-15 (2008) + https://doi.org/10.1007/BF02985731 + + """ + F = {p for p in prime_factors if all(q % p != 1 for q in prime_factors)} + M = prime_factors - F + + s = 0 + powerset = chain.from_iterable(combinations(M, r) for r in range(len(M)+1)) + for ps in powerset: + ps = set(ps) + prod = 1 + for p in M - ps: + c = len([q for q in F | ps if q % p == 1]) + prod *= (p**c - 1) // (p - 1) + if not prod: + break + s += prod + return s + + +def groups_count(n): + r""" Number of groups of order `n`. + In [1]_, ``gnu(n)`` is given, so we follow this notation here as well. + + Parameters + ========== + + n : Integer + ``n`` is a positive integer + + Returns + ======= + + int : ``gnu(n)`` + + Raises + ====== + + ValueError + Number of groups of order ``n`` is unknown or not implemented. + For example, gnu(`2^{11}`) is not yet known. + On the other hand, gnu(99) is known to be 2, + but this has not yet been implemented in this function. + + Examples + ======== + + >>> from sympy.combinatorics.group_numbers import groups_count + >>> groups_count(3) # There is only one cyclic group of order 3 + 1 + >>> # There are two groups of order 10: the cyclic group and the dihedral group + >>> groups_count(10) + 2 + + See Also + ======== + + is_cyclic_number + `n` is cyclic iff gnu(n) = 1 + + References + ========== + + .. [1] John H. Conway, Heiko Dietrich and E.A. O'Brien, + Counting groups: gnus, moas and other exotica + The Mathematical Intelligencer 30, 6-15 (2008) + https://doi.org/10.1007/BF02985731 + .. [2] https://oeis.org/A000001 + + """ + n = as_int(n) + if n <= 0: + raise ValueError("n must be a positive integer, not %i" % n) + factors = factorint(n) + if len(factors) == 1: + (p, e) = list(factors.items())[0] + if p == 2: + A000679 = [1, 1, 2, 5, 14, 51, 267, 2328, 56092, 10494213, 49487367289] + if e < len(A000679): + return A000679[e] + if p == 3: + A090091 = [1, 1, 2, 5, 15, 67, 504, 9310, 1396077, 5937876645] + if e < len(A090091): + return A090091[e] + if e <= 2: # gnu(p) = 1, gnu(p**2) = 2 + return e + if e == 3: # gnu(p**3) = 5 + return 5 + if e == 4: # if p is an odd prime, gnu(p**4) = 15 + return 15 + if e == 5: # if p >= 5, gnu(p**5) is expressed by the following equation + return 61 + 2*p + 2*gcd(p-1, 3) + gcd(p-1, 4) + if e == 6: # if p >= 6, gnu(p**6) is expressed by the following equation + return 3*p**2 + 39*p + 344 +\ + 24*gcd(p-1, 3) + 11*gcd(p-1, 4) + 2*gcd(p-1, 5) + if e == 7: # if p >= 7, gnu(p**7) is expressed by the following equation + if p == 5: + return 34297 + return 3*p**5 + 12*p**4 + 44*p**3 + 170*p**2 + 707*p + 2455 +\ + (4*p**2 + 44*p + 291)*gcd(p-1, 3) + (p**2 + 19*p + 135)*gcd(p-1, 4) + \ + (3*p + 31)*gcd(p-1, 5) + 4*gcd(p-1, 7) + 5*gcd(p-1, 8) + gcd(p-1, 9) + if any(e > 1 for e in factors.values()): # n is not squarefree + # some known values for small n that have more than 1 factor and are not square free (https://oeis.org/A000001) + small = {12: 5, 18: 5, 20: 5, 24: 15, 28: 4, 36: 14, 40: 14, 44: 4, 45: 2, 48: 52, + 50: 5, 52: 5, 54: 15, 56: 13, 60: 13, 63: 4, 68: 5, 72: 50, 75: 3, 76: 4, + 80: 52, 84: 15, 88: 12, 90: 10, 92: 4} + if n in small: + return small[n] + raise ValueError("Number of groups of order n is unknown or not implemented") + if len(factors) == 2: # n is squarefree semiprime + p, q = sorted(factors.keys()) + return 2 if q % p == 1 else 1 + return _holder_formula(set(factors.keys())) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/homomorphisms.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/homomorphisms.py new file mode 100644 index 0000000000000000000000000000000000000000..256f56b3aa7c5404d332f42e37d4f1117ea81db7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/homomorphisms.py @@ -0,0 +1,549 @@ +import itertools +from sympy.combinatorics.fp_groups import FpGroup, FpSubgroup, simplify_presentation +from sympy.combinatorics.free_groups import FreeGroup +from sympy.combinatorics.perm_groups import PermutationGroup +from sympy.core.intfunc import igcd +from sympy.functions.combinatorial.numbers import totient +from sympy.core.singleton import S + +class GroupHomomorphism: + ''' + A class representing group homomorphisms. Instantiate using `homomorphism()`. + + References + ========== + + .. [1] Holt, D., Eick, B. and O'Brien, E. (2005). Handbook of computational group theory. + + ''' + + def __init__(self, domain, codomain, images): + self.domain = domain + self.codomain = codomain + self.images = images + self._inverses = None + self._kernel = None + self._image = None + + def _invs(self): + ''' + Return a dictionary with `{gen: inverse}` where `gen` is a rewriting + generator of `codomain` (e.g. strong generator for permutation groups) + and `inverse` is an element of its preimage + + ''' + image = self.image() + inverses = {} + for k in list(self.images.keys()): + v = self.images[k] + if not (v in inverses + or v.is_identity): + inverses[v] = k + if isinstance(self.codomain, PermutationGroup): + gens = image.strong_gens + else: + gens = image.generators + for g in gens: + if g in inverses or g.is_identity: + continue + w = self.domain.identity + if isinstance(self.codomain, PermutationGroup): + parts = image._strong_gens_slp[g][::-1] + else: + parts = g + for s in parts: + if s in inverses: + w = w*inverses[s] + else: + w = w*inverses[s**-1]**-1 + inverses[g] = w + + return inverses + + def invert(self, g): + ''' + Return an element of the preimage of ``g`` or of each element + of ``g`` if ``g`` is a list. + + Explanation + =========== + + If the codomain is an FpGroup, the inverse for equal + elements might not always be the same unless the FpGroup's + rewriting system is confluent. However, making a system + confluent can be time-consuming. If it's important, try + `self.codomain.make_confluent()` first. + + ''' + from sympy.combinatorics import Permutation + from sympy.combinatorics.free_groups import FreeGroupElement + if isinstance(g, (Permutation, FreeGroupElement)): + if isinstance(self.codomain, FpGroup): + g = self.codomain.reduce(g) + if self._inverses is None: + self._inverses = self._invs() + image = self.image() + w = self.domain.identity + if isinstance(self.codomain, PermutationGroup): + gens = image.generator_product(g)[::-1] + else: + gens = g + # the following can't be "for s in gens:" + # because that would be equivalent to + # "for s in gens.array_form:" when g is + # a FreeGroupElement. On the other hand, + # when you call gens by index, the generator + # (or inverse) at position i is returned. + for i in range(len(gens)): + s = gens[i] + if s.is_identity: + continue + if s in self._inverses: + w = w*self._inverses[s] + else: + w = w*self._inverses[s**-1]**-1 + return w + elif isinstance(g, list): + return [self.invert(e) for e in g] + + def kernel(self): + ''' + Compute the kernel of `self`. + + ''' + if self._kernel is None: + self._kernel = self._compute_kernel() + return self._kernel + + def _compute_kernel(self): + G = self.domain + G_order = G.order() + if G_order is S.Infinity: + raise NotImplementedError( + "Kernel computation is not implemented for infinite groups") + gens = [] + if isinstance(G, PermutationGroup): + K = PermutationGroup(G.identity) + else: + K = FpSubgroup(G, gens, normal=True) + i = self.image().order() + while K.order()*i != G_order: + r = G.random() + k = r*self.invert(self(r))**-1 + if k not in K: + gens.append(k) + if isinstance(G, PermutationGroup): + K = PermutationGroup(gens) + else: + K = FpSubgroup(G, gens, normal=True) + return K + + def image(self): + ''' + Compute the image of `self`. + + ''' + if self._image is None: + values = list(set(self.images.values())) + if isinstance(self.codomain, PermutationGroup): + self._image = self.codomain.subgroup(values) + else: + self._image = FpSubgroup(self.codomain, values) + return self._image + + def _apply(self, elem): + ''' + Apply `self` to `elem`. + + ''' + if elem not in self.domain: + if isinstance(elem, (list, tuple)): + return [self._apply(e) for e in elem] + raise ValueError("The supplied element does not belong to the domain") + if elem.is_identity: + return self.codomain.identity + else: + images = self.images + value = self.codomain.identity + if isinstance(self.domain, PermutationGroup): + gens = self.domain.generator_product(elem, original=True) + for g in gens: + if g in self.images: + value = images[g]*value + else: + value = images[g**-1]**-1*value + else: + i = 0 + for _, p in elem.array_form: + if p < 0: + g = elem[i]**-1 + else: + g = elem[i] + value = value*images[g]**p + i += abs(p) + return value + + def __call__(self, elem): + return self._apply(elem) + + def is_injective(self): + ''' + Check if the homomorphism is injective + + ''' + return self.kernel().order() == 1 + + def is_surjective(self): + ''' + Check if the homomorphism is surjective + + ''' + im = self.image().order() + oth = self.codomain.order() + if im is S.Infinity and oth is S.Infinity: + return None + else: + return im == oth + + def is_isomorphism(self): + ''' + Check if `self` is an isomorphism. + + ''' + return self.is_injective() and self.is_surjective() + + def is_trivial(self): + ''' + Check is `self` is a trivial homomorphism, i.e. all elements + are mapped to the identity. + + ''' + return self.image().order() == 1 + + def compose(self, other): + ''' + Return the composition of `self` and `other`, i.e. + the homomorphism phi such that for all g in the domain + of `other`, phi(g) = self(other(g)) + + ''' + if not other.image().is_subgroup(self.domain): + raise ValueError("The image of `other` must be a subgroup of " + "the domain of `self`") + images = {g: self(other(g)) for g in other.images} + return GroupHomomorphism(other.domain, self.codomain, images) + + def restrict_to(self, H): + ''' + Return the restriction of the homomorphism to the subgroup `H` + of the domain. + + ''' + if not isinstance(H, PermutationGroup) or not H.is_subgroup(self.domain): + raise ValueError("Given H is not a subgroup of the domain") + domain = H + images = {g: self(g) for g in H.generators} + return GroupHomomorphism(domain, self.codomain, images) + + def invert_subgroup(self, H): + ''' + Return the subgroup of the domain that is the inverse image + of the subgroup ``H`` of the homomorphism image + + ''' + if not H.is_subgroup(self.image()): + raise ValueError("Given H is not a subgroup of the image") + gens = [] + P = PermutationGroup(self.image().identity) + for h in H.generators: + h_i = self.invert(h) + if h_i not in P: + gens.append(h_i) + P = PermutationGroup(gens) + for k in self.kernel().generators: + if k*h_i not in P: + gens.append(k*h_i) + P = PermutationGroup(gens) + return P + +def homomorphism(domain, codomain, gens, images=(), check=True): + ''' + Create (if possible) a group homomorphism from the group ``domain`` + to the group ``codomain`` defined by the images of the domain's + generators ``gens``. ``gens`` and ``images`` can be either lists or tuples + of equal sizes. If ``gens`` is a proper subset of the group's generators, + the unspecified generators will be mapped to the identity. If the + images are not specified, a trivial homomorphism will be created. + + If the given images of the generators do not define a homomorphism, + an exception is raised. + + If ``check`` is ``False``, do not check whether the given images actually + define a homomorphism. + + ''' + if not isinstance(domain, (PermutationGroup, FpGroup, FreeGroup)): + raise TypeError("The domain must be a group") + if not isinstance(codomain, (PermutationGroup, FpGroup, FreeGroup)): + raise TypeError("The codomain must be a group") + + generators = domain.generators + if not all(g in generators for g in gens): + raise ValueError("The supplied generators must be a subset of the domain's generators") + if not all(g in codomain for g in images): + raise ValueError("The images must be elements of the codomain") + + if images and len(images) != len(gens): + raise ValueError("The number of images must be equal to the number of generators") + + gens = list(gens) + images = list(images) + + images.extend([codomain.identity]*(len(generators)-len(images))) + gens.extend([g for g in generators if g not in gens]) + images = dict(zip(gens,images)) + + if check and not _check_homomorphism(domain, codomain, images): + raise ValueError("The given images do not define a homomorphism") + return GroupHomomorphism(domain, codomain, images) + +def _check_homomorphism(domain, codomain, images): + """ + Check that a given mapping of generators to images defines a homomorphism. + + Parameters + ========== + domain : PermutationGroup, FpGroup, FreeGroup + codomain : PermutationGroup, FpGroup, FreeGroup + images : dict + The set of keys must be equal to domain.generators. + The values must be elements of the codomain. + + """ + pres = domain if hasattr(domain, 'relators') else domain.presentation() + rels = pres.relators + gens = pres.generators + symbols = [g.ext_rep[0] for g in gens] + symbols_to_domain_generators = dict(zip(symbols, domain.generators)) + identity = codomain.identity + + def _image(r): + w = identity + for symbol, power in r.array_form: + g = symbols_to_domain_generators[symbol] + w *= images[g]**power + return w + + for r in rels: + if isinstance(codomain, FpGroup): + s = codomain.equals(_image(r), identity) + if s is None: + # only try to make the rewriting system + # confluent when it can't determine the + # truth of equality otherwise + success = codomain.make_confluent() + s = codomain.equals(_image(r), identity) + if s is None and not success: + raise RuntimeError("Can't determine if the images " + "define a homomorphism. Try increasing " + "the maximum number of rewriting rules " + "(group._rewriting_system.set_max(new_value); " + "the current value is stored in group._rewriting" + "_system.maxeqns)") + else: + s = _image(r).is_identity + if not s: + return False + return True + +def orbit_homomorphism(group, omega): + ''' + Return the homomorphism induced by the action of the permutation + group ``group`` on the set ``omega`` that is closed under the action. + + ''' + from sympy.combinatorics import Permutation + from sympy.combinatorics.named_groups import SymmetricGroup + codomain = SymmetricGroup(len(omega)) + identity = codomain.identity + omega = list(omega) + images = {g: identity*Permutation([omega.index(o^g) for o in omega]) for g in group.generators} + group._schreier_sims(base=omega) + H = GroupHomomorphism(group, codomain, images) + if len(group.basic_stabilizers) > len(omega): + H._kernel = group.basic_stabilizers[len(omega)] + else: + H._kernel = PermutationGroup([group.identity]) + return H + +def block_homomorphism(group, blocks): + ''' + Return the homomorphism induced by the action of the permutation + group ``group`` on the block system ``blocks``. The latter should be + of the same form as returned by the ``minimal_block`` method for + permutation groups, namely a list of length ``group.degree`` where + the i-th entry is a representative of the block i belongs to. + + ''' + from sympy.combinatorics import Permutation + from sympy.combinatorics.named_groups import SymmetricGroup + + n = len(blocks) + + # number the blocks; m is the total number, + # b is such that b[i] is the number of the block i belongs to, + # p is the list of length m such that p[i] is the representative + # of the i-th block + m = 0 + p = [] + b = [None]*n + for i in range(n): + if blocks[i] == i: + p.append(i) + b[i] = m + m += 1 + for i in range(n): + b[i] = b[blocks[i]] + + codomain = SymmetricGroup(m) + # the list corresponding to the identity permutation in codomain + identity = range(m) + images = {g: Permutation([b[p[i]^g] for i in identity]) for g in group.generators} + H = GroupHomomorphism(group, codomain, images) + return H + +def group_isomorphism(G, H, isomorphism=True): + ''' + Compute an isomorphism between 2 given groups. + + Parameters + ========== + + G : A finite ``FpGroup`` or a ``PermutationGroup``. + First group. + + H : A finite ``FpGroup`` or a ``PermutationGroup`` + Second group. + + isomorphism : bool + This is used to avoid the computation of homomorphism + when the user only wants to check if there exists + an isomorphism between the groups. + + Returns + ======= + + If isomorphism = False -- Returns a boolean. + If isomorphism = True -- Returns a boolean and an isomorphism between `G` and `H`. + + Examples + ======== + + >>> from sympy.combinatorics import free_group, Permutation + >>> from sympy.combinatorics.perm_groups import PermutationGroup + >>> from sympy.combinatorics.fp_groups import FpGroup + >>> from sympy.combinatorics.homomorphisms import group_isomorphism + >>> from sympy.combinatorics.named_groups import DihedralGroup, AlternatingGroup + + >>> D = DihedralGroup(8) + >>> p = Permutation(0, 1, 2, 3, 4, 5, 6, 7) + >>> P = PermutationGroup(p) + >>> group_isomorphism(D, P) + (False, None) + + >>> F, a, b = free_group("a, b") + >>> G = FpGroup(F, [a**3, b**3, (a*b)**2]) + >>> H = AlternatingGroup(4) + >>> (check, T) = group_isomorphism(G, H) + >>> check + True + >>> T(b*a*b**-1*a**-1*b**-1) + (0 2 3) + + Notes + ===== + + Uses the approach suggested by Robert Tarjan to compute the isomorphism between two groups. + First, the generators of ``G`` are mapped to the elements of ``H`` and + we check if the mapping induces an isomorphism. + + ''' + if not isinstance(G, (PermutationGroup, FpGroup)): + raise TypeError("The group must be a PermutationGroup or an FpGroup") + if not isinstance(H, (PermutationGroup, FpGroup)): + raise TypeError("The group must be a PermutationGroup or an FpGroup") + + if isinstance(G, FpGroup) and isinstance(H, FpGroup): + G = simplify_presentation(G) + H = simplify_presentation(H) + # Two infinite FpGroups with the same generators are isomorphic + # when the relators are same but are ordered differently. + if G.generators == H.generators and (G.relators).sort() == (H.relators).sort(): + if not isomorphism: + return True + return (True, homomorphism(G, H, G.generators, H.generators)) + + # `_H` is the permutation group isomorphic to `H`. + _H = H + g_order = G.order() + h_order = H.order() + + if g_order is S.Infinity: + raise NotImplementedError("Isomorphism methods are not implemented for infinite groups.") + + if isinstance(H, FpGroup): + if h_order is S.Infinity: + raise NotImplementedError("Isomorphism methods are not implemented for infinite groups.") + _H, h_isomorphism = H._to_perm_group() + + if (g_order != h_order) or (G.is_abelian != H.is_abelian): + if not isomorphism: + return False + return (False, None) + + if not isomorphism: + # Two groups of the same cyclic numbered order + # are isomorphic to each other. + n = g_order + if (igcd(n, totient(n))) == 1: + return True + + # Match the generators of `G` with subsets of `_H` + gens = list(G.generators) + for subset in itertools.permutations(_H, len(gens)): + images = list(subset) + images.extend([_H.identity]*(len(G.generators)-len(images))) + _images = dict(zip(gens,images)) + if _check_homomorphism(G, _H, _images): + if isinstance(H, FpGroup): + images = h_isomorphism.invert(images) + T = homomorphism(G, H, G.generators, images, check=False) + if T.is_isomorphism(): + # It is a valid isomorphism + if not isomorphism: + return True + return (True, T) + + if not isomorphism: + return False + return (False, None) + +def is_isomorphic(G, H): + ''' + Check if the groups are isomorphic to each other + + Parameters + ========== + + G : A finite ``FpGroup`` or a ``PermutationGroup`` + First group. + + H : A finite ``FpGroup`` or a ``PermutationGroup`` + Second group. + + Returns + ======= + + boolean + ''' + return group_isomorphism(G, H, isomorphism=False) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/named_groups.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/named_groups.py new file mode 100644 index 0000000000000000000000000000000000000000..59f10c40ef716e3b644e00f936323e9f6936eb88 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/named_groups.py @@ -0,0 +1,332 @@ +from sympy.combinatorics.group_constructs import DirectProduct +from sympy.combinatorics.perm_groups import PermutationGroup +from sympy.combinatorics.permutations import Permutation + +_af_new = Permutation._af_new + + +def AbelianGroup(*cyclic_orders): + """ + Returns the direct product of cyclic groups with the given orders. + + Explanation + =========== + + According to the structure theorem for finite abelian groups ([1]), + every finite abelian group can be written as the direct product of + finitely many cyclic groups. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import AbelianGroup + >>> AbelianGroup(3, 4) + PermutationGroup([ + (6)(0 1 2), + (3 4 5 6)]) + >>> _.is_group + True + + See Also + ======== + + DirectProduct + + References + ========== + + .. [1] https://groupprops.subwiki.org/wiki/Structure_theorem_for_finitely_generated_abelian_groups + + """ + groups = [] + degree = 0 + order = 1 + for size in cyclic_orders: + degree += size + order *= size + groups.append(CyclicGroup(size)) + G = DirectProduct(*groups) + G._is_abelian = True + G._degree = degree + G._order = order + + return G + + +def AlternatingGroup(n): + """ + Generates the alternating group on ``n`` elements as a permutation group. + + Explanation + =========== + + For ``n > 2``, the generators taken are ``(0 1 2), (0 1 2 ... n-1)`` for + ``n`` odd + and ``(0 1 2), (1 2 ... n-1)`` for ``n`` even (See [1], p.31, ex.6.9.). + After the group is generated, some of its basic properties are set. + The cases ``n = 1, 2`` are handled separately. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import AlternatingGroup + >>> G = AlternatingGroup(4) + >>> G.is_group + True + >>> a = list(G.generate_dimino()) + >>> len(a) + 12 + >>> all(perm.is_even for perm in a) + True + + See Also + ======== + + SymmetricGroup, CyclicGroup, DihedralGroup + + References + ========== + + .. [1] Armstrong, M. "Groups and Symmetry" + + """ + # small cases are special + if n in (1, 2): + return PermutationGroup([Permutation([0])]) + + a = list(range(n)) + a[0], a[1], a[2] = a[1], a[2], a[0] + gen1 = a + if n % 2: + a = list(range(1, n)) + a.append(0) + gen2 = a + else: + a = list(range(2, n)) + a.append(1) + a.insert(0, 0) + gen2 = a + gens = [gen1, gen2] + if gen1 == gen2: + gens = gens[:1] + G = PermutationGroup([_af_new(a) for a in gens], dups=False) + + set_alternating_group_properties(G, n, n) + G._is_alt = True + return G + + +def set_alternating_group_properties(G, n, degree): + """Set known properties of an alternating group. """ + if n < 4: + G._is_abelian = True + G._is_nilpotent = True + else: + G._is_abelian = False + G._is_nilpotent = False + if n < 5: + G._is_solvable = True + else: + G._is_solvable = False + G._degree = degree + G._is_transitive = True + G._is_dihedral = False + + +def CyclicGroup(n): + """ + Generates the cyclic group of order ``n`` as a permutation group. + + Explanation + =========== + + The generator taken is the ``n``-cycle ``(0 1 2 ... n-1)`` + (in cycle notation). After the group is generated, some of its basic + properties are set. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import CyclicGroup + >>> G = CyclicGroup(6) + >>> G.is_group + True + >>> G.order() + 6 + >>> list(G.generate_schreier_sims(af=True)) + [[0, 1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 0], [2, 3, 4, 5, 0, 1], + [3, 4, 5, 0, 1, 2], [4, 5, 0, 1, 2, 3], [5, 0, 1, 2, 3, 4]] + + See Also + ======== + + SymmetricGroup, DihedralGroup, AlternatingGroup + + """ + a = list(range(1, n)) + a.append(0) + gen = _af_new(a) + G = PermutationGroup([gen]) + + G._is_abelian = True + G._is_nilpotent = True + G._is_solvable = True + G._degree = n + G._is_transitive = True + G._order = n + G._is_dihedral = (n == 2) + return G + + +def DihedralGroup(n): + r""" + Generates the dihedral group `D_n` as a permutation group. + + Explanation + =========== + + The dihedral group `D_n` is the group of symmetries of the regular + ``n``-gon. The generators taken are the ``n``-cycle ``a = (0 1 2 ... n-1)`` + (a rotation of the ``n``-gon) and ``b = (0 n-1)(1 n-2)...`` + (a reflection of the ``n``-gon) in cycle rotation. It is easy to see that + these satisfy ``a**n = b**2 = 1`` and ``bab = ~a`` so they indeed generate + `D_n` (See [1]). After the group is generated, some of its basic properties + are set. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> G = DihedralGroup(5) + >>> G.is_group + True + >>> a = list(G.generate_dimino()) + >>> [perm.cyclic_form for perm in a] + [[], [[0, 1, 2, 3, 4]], [[0, 2, 4, 1, 3]], + [[0, 3, 1, 4, 2]], [[0, 4, 3, 2, 1]], [[0, 4], [1, 3]], + [[1, 4], [2, 3]], [[0, 1], [2, 4]], [[0, 2], [3, 4]], + [[0, 3], [1, 2]]] + + See Also + ======== + + SymmetricGroup, CyclicGroup, AlternatingGroup + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Dihedral_group + + """ + # small cases are special + if n == 1: + return PermutationGroup([Permutation([1, 0])]) + if n == 2: + return PermutationGroup([Permutation([1, 0, 3, 2]), + Permutation([2, 3, 0, 1]), Permutation([3, 2, 1, 0])]) + + a = list(range(1, n)) + a.append(0) + gen1 = _af_new(a) + a = list(range(n)) + a.reverse() + gen2 = _af_new(a) + G = PermutationGroup([gen1, gen2]) + # if n is a power of 2, group is nilpotent + if n & (n-1) == 0: + G._is_nilpotent = True + else: + G._is_nilpotent = False + G._is_dihedral = True + G._is_abelian = False + G._is_solvable = True + G._degree = n + G._is_transitive = True + G._order = 2*n + return G + + +def SymmetricGroup(n): + """ + Generates the symmetric group on ``n`` elements as a permutation group. + + Explanation + =========== + + The generators taken are the ``n``-cycle + ``(0 1 2 ... n-1)`` and the transposition ``(0 1)`` (in cycle notation). + (See [1]). After the group is generated, some of its basic properties + are set. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> G = SymmetricGroup(4) + >>> G.is_group + True + >>> G.order() + 24 + >>> list(G.generate_schreier_sims(af=True)) + [[0, 1, 2, 3], [1, 2, 3, 0], [2, 3, 0, 1], [3, 1, 2, 0], [0, 2, 3, 1], + [1, 3, 0, 2], [2, 0, 1, 3], [3, 2, 0, 1], [0, 3, 1, 2], [1, 0, 2, 3], + [2, 1, 3, 0], [3, 0, 1, 2], [0, 1, 3, 2], [1, 2, 0, 3], [2, 3, 1, 0], + [3, 1, 0, 2], [0, 2, 1, 3], [1, 3, 2, 0], [2, 0, 3, 1], [3, 2, 1, 0], + [0, 3, 2, 1], [1, 0, 3, 2], [2, 1, 0, 3], [3, 0, 2, 1]] + + See Also + ======== + + CyclicGroup, DihedralGroup, AlternatingGroup + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Symmetric_group#Generators_and_relations + + """ + if n == 1: + G = PermutationGroup([Permutation([0])]) + elif n == 2: + G = PermutationGroup([Permutation([1, 0])]) + else: + a = list(range(1, n)) + a.append(0) + gen1 = _af_new(a) + a = list(range(n)) + a[0], a[1] = a[1], a[0] + gen2 = _af_new(a) + G = PermutationGroup([gen1, gen2]) + set_symmetric_group_properties(G, n, n) + G._is_sym = True + return G + + +def set_symmetric_group_properties(G, n, degree): + """Set known properties of a symmetric group. """ + if n < 3: + G._is_abelian = True + G._is_nilpotent = True + else: + G._is_abelian = False + G._is_nilpotent = False + if n < 5: + G._is_solvable = True + else: + G._is_solvable = False + G._degree = degree + G._is_transitive = True + G._is_dihedral = (n in [2, 3]) # cf Landau's func and Stirling's approx + + +def RubikGroup(n): + """Return a group of Rubik's cube generators + + >>> from sympy.combinatorics.named_groups import RubikGroup + >>> RubikGroup(2).is_group + True + """ + from sympy.combinatorics.generators import rubik + if n <= 1: + raise ValueError("Invalid cube. n has to be greater than 1") + return PermutationGroup(rubik(n)) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/partitions.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/partitions.py new file mode 100644 index 0000000000000000000000000000000000000000..dfe646baabbb5bf2350cba859a265ac32bbfaf53 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/partitions.py @@ -0,0 +1,745 @@ +from sympy.core import Basic, Dict, sympify, Tuple +from sympy.core.numbers import Integer +from sympy.core.sorting import default_sort_key +from sympy.core.sympify import _sympify +from sympy.functions.combinatorial.numbers import bell +from sympy.matrices import zeros +from sympy.sets.sets import FiniteSet, Union +from sympy.utilities.iterables import flatten, group +from sympy.utilities.misc import as_int + + +from collections import defaultdict + + +class Partition(FiniteSet): + """ + This class represents an abstract partition. + + A partition is a set of disjoint sets whose union equals a given set. + + See Also + ======== + + sympy.utilities.iterables.partitions, + sympy.utilities.iterables.multiset_partitions + """ + + _rank = None + _partition = None + + def __new__(cls, *partition): + """ + Generates a new partition object. + + This method also verifies if the arguments passed are + valid and raises a ValueError if they are not. + + Examples + ======== + + Creating Partition from Python lists: + + >>> from sympy.combinatorics import Partition + >>> a = Partition([1, 2], [3]) + >>> a + Partition({3}, {1, 2}) + >>> a.partition + [[1, 2], [3]] + >>> len(a) + 2 + >>> a.members + (1, 2, 3) + + Creating Partition from Python sets: + + >>> Partition({1, 2, 3}, {4, 5}) + Partition({4, 5}, {1, 2, 3}) + + Creating Partition from SymPy finite sets: + + >>> from sympy import FiniteSet + >>> a = FiniteSet(1, 2, 3) + >>> b = FiniteSet(4, 5) + >>> Partition(a, b) + Partition({4, 5}, {1, 2, 3}) + """ + args = [] + dups = False + for arg in partition: + if isinstance(arg, list): + as_set = set(arg) + if len(as_set) < len(arg): + dups = True + break # error below + arg = as_set + args.append(_sympify(arg)) + + if not all(isinstance(part, FiniteSet) for part in args): + raise ValueError( + "Each argument to Partition should be " \ + "a list, set, or a FiniteSet") + + # sort so we have a canonical reference for RGS + U = Union(*args) + if dups or len(U) < sum(len(arg) for arg in args): + raise ValueError("Partition contained duplicate elements.") + + obj = FiniteSet.__new__(cls, *args) + obj.members = tuple(U) + obj.size = len(U) + return obj + + def sort_key(self, order=None): + """Return a canonical key that can be used for sorting. + + Ordering is based on the size and sorted elements of the partition + and ties are broken with the rank. + + Examples + ======== + + >>> from sympy import default_sort_key + >>> from sympy.combinatorics import Partition + >>> from sympy.abc import x + >>> a = Partition([1, 2]) + >>> b = Partition([3, 4]) + >>> c = Partition([1, x]) + >>> d = Partition(list(range(4))) + >>> l = [d, b, a + 1, a, c] + >>> l.sort(key=default_sort_key); l + [Partition({1, 2}), Partition({1}, {2}), Partition({1, x}), Partition({3, 4}), Partition({0, 1, 2, 3})] + """ + if order is None: + members = self.members + else: + members = tuple(sorted(self.members, + key=lambda w: default_sort_key(w, order))) + return tuple(map(default_sort_key, (self.size, members, self.rank))) + + @property + def partition(self): + """Return partition as a sorted list of lists. + + Examples + ======== + + >>> from sympy.combinatorics import Partition + >>> Partition([1], [2, 3]).partition + [[1], [2, 3]] + """ + if self._partition is None: + self._partition = sorted([sorted(p, key=default_sort_key) + for p in self.args]) + return self._partition + + def __add__(self, other): + """ + Return permutation whose rank is ``other`` greater than current rank, + (mod the maximum rank for the set). + + Examples + ======== + + >>> from sympy.combinatorics import Partition + >>> a = Partition([1, 2], [3]) + >>> a.rank + 1 + >>> (a + 1).rank + 2 + >>> (a + 100).rank + 1 + """ + other = as_int(other) + offset = self.rank + other + result = RGS_unrank((offset) % + RGS_enum(self.size), + self.size) + return Partition.from_rgs(result, self.members) + + def __sub__(self, other): + """ + Return permutation whose rank is ``other`` less than current rank, + (mod the maximum rank for the set). + + Examples + ======== + + >>> from sympy.combinatorics import Partition + >>> a = Partition([1, 2], [3]) + >>> a.rank + 1 + >>> (a - 1).rank + 0 + >>> (a - 100).rank + 1 + """ + return self.__add__(-other) + + def __le__(self, other): + """ + Checks if a partition is less than or equal to + the other based on rank. + + Examples + ======== + + >>> from sympy.combinatorics import Partition + >>> a = Partition([1, 2], [3, 4, 5]) + >>> b = Partition([1], [2, 3], [4], [5]) + >>> a.rank, b.rank + (9, 34) + >>> a <= a + True + >>> a <= b + True + """ + return self.sort_key() <= sympify(other).sort_key() + + def __lt__(self, other): + """ + Checks if a partition is less than the other. + + Examples + ======== + + >>> from sympy.combinatorics import Partition + >>> a = Partition([1, 2], [3, 4, 5]) + >>> b = Partition([1], [2, 3], [4], [5]) + >>> a.rank, b.rank + (9, 34) + >>> a < b + True + """ + return self.sort_key() < sympify(other).sort_key() + + @property + def rank(self): + """ + Gets the rank of a partition. + + Examples + ======== + + >>> from sympy.combinatorics import Partition + >>> a = Partition([1, 2], [3], [4, 5]) + >>> a.rank + 13 + """ + if self._rank is not None: + return self._rank + self._rank = RGS_rank(self.RGS) + return self._rank + + @property + def RGS(self): + """ + Returns the "restricted growth string" of the partition. + + Explanation + =========== + + The RGS is returned as a list of indices, L, where L[i] indicates + the block in which element i appears. For example, in a partition + of 3 elements (a, b, c) into 2 blocks ([c], [a, b]) the RGS is + [1, 1, 0]: "a" is in block 1, "b" is in block 1 and "c" is in block 0. + + Examples + ======== + + >>> from sympy.combinatorics import Partition + >>> a = Partition([1, 2], [3], [4, 5]) + >>> a.members + (1, 2, 3, 4, 5) + >>> a.RGS + (0, 0, 1, 2, 2) + >>> a + 1 + Partition({3}, {4}, {5}, {1, 2}) + >>> _.RGS + (0, 0, 1, 2, 3) + """ + rgs = {} + partition = self.partition + for i, part in enumerate(partition): + for j in part: + rgs[j] = i + return tuple([rgs[i] for i in sorted( + [i for p in partition for i in p], key=default_sort_key)]) + + @classmethod + def from_rgs(self, rgs, elements): + """ + Creates a set partition from a restricted growth string. + + Explanation + =========== + + The indices given in rgs are assumed to be the index + of the element as given in elements *as provided* (the + elements are not sorted by this routine). Block numbering + starts from 0. If any block was not referenced in ``rgs`` + an error will be raised. + + Examples + ======== + + >>> from sympy.combinatorics import Partition + >>> Partition.from_rgs([0, 1, 2, 0, 1], list('abcde')) + Partition({c}, {a, d}, {b, e}) + >>> Partition.from_rgs([0, 1, 2, 0, 1], list('cbead')) + Partition({e}, {a, c}, {b, d}) + >>> a = Partition([1, 4], [2], [3, 5]) + >>> Partition.from_rgs(a.RGS, a.members) + Partition({2}, {1, 4}, {3, 5}) + """ + if len(rgs) != len(elements): + raise ValueError('mismatch in rgs and element lengths') + max_elem = max(rgs) + 1 + partition = [[] for i in range(max_elem)] + j = 0 + for i in rgs: + partition[i].append(elements[j]) + j += 1 + if not all(p for p in partition): + raise ValueError('some blocks of the partition were empty.') + return Partition(*partition) + + +class IntegerPartition(Basic): + """ + This class represents an integer partition. + + Explanation + =========== + + In number theory and combinatorics, a partition of a positive integer, + ``n``, also called an integer partition, is a way of writing ``n`` as a + list of positive integers that sum to n. Two partitions that differ only + in the order of summands are considered to be the same partition; if order + matters then the partitions are referred to as compositions. For example, + 4 has five partitions: [4], [3, 1], [2, 2], [2, 1, 1], and [1, 1, 1, 1]; + the compositions [1, 2, 1] and [1, 1, 2] are the same as partition + [2, 1, 1]. + + See Also + ======== + + sympy.utilities.iterables.partitions, + sympy.utilities.iterables.multiset_partitions + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Partition_%28number_theory%29 + """ + + _dict = None + _keys = None + + def __new__(cls, partition, integer=None): + """ + Generates a new IntegerPartition object from a list or dictionary. + + Explanation + =========== + + The partition can be given as a list of positive integers or a + dictionary of (integer, multiplicity) items. If the partition is + preceded by an integer an error will be raised if the partition + does not sum to that given integer. + + Examples + ======== + + >>> from sympy.combinatorics.partitions import IntegerPartition + >>> a = IntegerPartition([5, 4, 3, 1, 1]) + >>> a + IntegerPartition(14, (5, 4, 3, 1, 1)) + >>> print(a) + [5, 4, 3, 1, 1] + >>> IntegerPartition({1:3, 2:1}) + IntegerPartition(5, (2, 1, 1, 1)) + + If the value that the partition should sum to is given first, a check + will be made to see n error will be raised if there is a discrepancy: + + >>> IntegerPartition(10, [5, 4, 3, 1]) + Traceback (most recent call last): + ... + ValueError: The partition is not valid + + """ + if integer is not None: + integer, partition = partition, integer + if isinstance(partition, (dict, Dict)): + _ = [] + for k, v in sorted(partition.items(), reverse=True): + if not v: + continue + k, v = as_int(k), as_int(v) + _.extend([k]*v) + partition = tuple(_) + else: + partition = tuple(sorted(map(as_int, partition), reverse=True)) + sum_ok = False + if integer is None: + integer = sum(partition) + sum_ok = True + else: + integer = as_int(integer) + + if not sum_ok and sum(partition) != integer: + raise ValueError("Partition did not add to %s" % integer) + if any(i < 1 for i in partition): + raise ValueError("All integer summands must be greater than one") + + obj = Basic.__new__(cls, Integer(integer), Tuple(*partition)) + obj.partition = list(partition) + obj.integer = integer + return obj + + def prev_lex(self): + """Return the previous partition of the integer, n, in lexical order, + wrapping around to [1, ..., 1] if the partition is [n]. + + Examples + ======== + + >>> from sympy.combinatorics.partitions import IntegerPartition + >>> p = IntegerPartition([4]) + >>> print(p.prev_lex()) + [3, 1] + >>> p.partition > p.prev_lex().partition + True + """ + d = defaultdict(int) + d.update(self.as_dict()) + keys = self._keys + if keys == [1]: + return IntegerPartition({self.integer: 1}) + if keys[-1] != 1: + d[keys[-1]] -= 1 + if keys[-1] == 2: + d[1] = 2 + else: + d[keys[-1] - 1] = d[1] = 1 + else: + d[keys[-2]] -= 1 + left = d[1] + keys[-2] + new = keys[-2] + d[1] = 0 + while left: + new -= 1 + if left - new >= 0: + d[new] += left//new + left -= d[new]*new + return IntegerPartition(self.integer, d) + + def next_lex(self): + """Return the next partition of the integer, n, in lexical order, + wrapping around to [n] if the partition is [1, ..., 1]. + + Examples + ======== + + >>> from sympy.combinatorics.partitions import IntegerPartition + >>> p = IntegerPartition([3, 1]) + >>> print(p.next_lex()) + [4] + >>> p.partition < p.next_lex().partition + True + """ + d = defaultdict(int) + d.update(self.as_dict()) + key = self._keys + a = key[-1] + if a == self.integer: + d.clear() + d[1] = self.integer + elif a == 1: + if d[a] > 1: + d[a + 1] += 1 + d[a] -= 2 + else: + b = key[-2] + d[b + 1] += 1 + d[1] = (d[b] - 1)*b + d[b] = 0 + else: + if d[a] > 1: + if len(key) == 1: + d.clear() + d[a + 1] = 1 + d[1] = self.integer - a - 1 + else: + a1 = a + 1 + d[a1] += 1 + d[1] = d[a]*a - a1 + d[a] = 0 + else: + b = key[-2] + b1 = b + 1 + d[b1] += 1 + need = d[b]*b + d[a]*a - b1 + d[a] = d[b] = 0 + d[1] = need + return IntegerPartition(self.integer, d) + + def as_dict(self): + """Return the partition as a dictionary whose keys are the + partition integers and the values are the multiplicity of that + integer. + + Examples + ======== + + >>> from sympy.combinatorics.partitions import IntegerPartition + >>> IntegerPartition([1]*3 + [2] + [3]*4).as_dict() + {1: 3, 2: 1, 3: 4} + """ + if self._dict is None: + groups = group(self.partition, multiple=False) + self._keys = [g[0] for g in groups] + self._dict = dict(groups) + return self._dict + + @property + def conjugate(self): + """ + Computes the conjugate partition of itself. + + Examples + ======== + + >>> from sympy.combinatorics.partitions import IntegerPartition + >>> a = IntegerPartition([6, 3, 3, 2, 1]) + >>> a.conjugate + [5, 4, 3, 1, 1, 1] + """ + j = 1 + temp_arr = list(self.partition) + [0] + k = temp_arr[0] + b = [0]*k + while k > 0: + while k > temp_arr[j]: + b[k - 1] = j + k -= 1 + j += 1 + return b + + def __lt__(self, other): + """Return True if self is less than other when the partition + is listed from smallest to biggest. + + Examples + ======== + + >>> from sympy.combinatorics.partitions import IntegerPartition + >>> a = IntegerPartition([3, 1]) + >>> a < a + False + >>> b = a.next_lex() + >>> a < b + True + >>> a == b + False + """ + return list(reversed(self.partition)) < list(reversed(other.partition)) + + def __le__(self, other): + """Return True if self is less than other when the partition + is listed from smallest to biggest. + + Examples + ======== + + >>> from sympy.combinatorics.partitions import IntegerPartition + >>> a = IntegerPartition([4]) + >>> a <= a + True + """ + return list(reversed(self.partition)) <= list(reversed(other.partition)) + + def as_ferrers(self, char='#'): + """ + Prints the ferrer diagram of a partition. + + Examples + ======== + + >>> from sympy.combinatorics.partitions import IntegerPartition + >>> print(IntegerPartition([1, 1, 5]).as_ferrers()) + ##### + # + # + """ + return "\n".join([char*i for i in self.partition]) + + def __str__(self): + return str(list(self.partition)) + + +def random_integer_partition(n, seed=None): + """ + Generates a random integer partition summing to ``n`` as a list + of reverse-sorted integers. + + Examples + ======== + + >>> from sympy.combinatorics.partitions import random_integer_partition + + For the following, a seed is given so a known value can be shown; in + practice, the seed would not be given. + + >>> random_integer_partition(100, seed=[1, 1, 12, 1, 2, 1, 85, 1]) + [85, 12, 2, 1] + >>> random_integer_partition(10, seed=[1, 2, 3, 1, 5, 1]) + [5, 3, 1, 1] + >>> random_integer_partition(1) + [1] + """ + from sympy.core.random import _randint + + n = as_int(n) + if n < 1: + raise ValueError('n must be a positive integer') + + randint = _randint(seed) + + partition = [] + while (n > 0): + k = randint(1, n) + mult = randint(1, n//k) + partition.append((k, mult)) + n -= k*mult + partition.sort(reverse=True) + partition = flatten([[k]*m for k, m in partition]) + return partition + + +def RGS_generalized(m): + """ + Computes the m + 1 generalized unrestricted growth strings + and returns them as rows in matrix. + + Examples + ======== + + >>> from sympy.combinatorics.partitions import RGS_generalized + >>> RGS_generalized(6) + Matrix([ + [ 1, 1, 1, 1, 1, 1, 1], + [ 1, 2, 3, 4, 5, 6, 0], + [ 2, 5, 10, 17, 26, 0, 0], + [ 5, 15, 37, 77, 0, 0, 0], + [ 15, 52, 151, 0, 0, 0, 0], + [ 52, 203, 0, 0, 0, 0, 0], + [203, 0, 0, 0, 0, 0, 0]]) + """ + d = zeros(m + 1) + for i in range(m + 1): + d[0, i] = 1 + + for i in range(1, m + 1): + for j in range(m): + if j <= m - i: + d[i, j] = j * d[i - 1, j] + d[i - 1, j + 1] + else: + d[i, j] = 0 + return d + + +def RGS_enum(m): + """ + RGS_enum computes the total number of restricted growth strings + possible for a superset of size m. + + Examples + ======== + + >>> from sympy.combinatorics.partitions import RGS_enum + >>> from sympy.combinatorics import Partition + >>> RGS_enum(4) + 15 + >>> RGS_enum(5) + 52 + >>> RGS_enum(6) + 203 + + We can check that the enumeration is correct by actually generating + the partitions. Here, the 15 partitions of 4 items are generated: + + >>> a = Partition(list(range(4))) + >>> s = set() + >>> for i in range(20): + ... s.add(a) + ... a += 1 + ... + >>> assert len(s) == 15 + + """ + if (m < 1): + return 0 + elif (m == 1): + return 1 + else: + return bell(m) + + +def RGS_unrank(rank, m): + """ + Gives the unranked restricted growth string for a given + superset size. + + Examples + ======== + + >>> from sympy.combinatorics.partitions import RGS_unrank + >>> RGS_unrank(14, 4) + [0, 1, 2, 3] + >>> RGS_unrank(0, 4) + [0, 0, 0, 0] + """ + if m < 1: + raise ValueError("The superset size must be >= 1") + if rank < 0 or RGS_enum(m) <= rank: + raise ValueError("Invalid arguments") + + L = [1] * (m + 1) + j = 1 + D = RGS_generalized(m) + for i in range(2, m + 1): + v = D[m - i, j] + cr = j*v + if cr <= rank: + L[i] = j + 1 + rank -= cr + j += 1 + else: + L[i] = int(rank / v + 1) + rank %= v + return [x - 1 for x in L[1:]] + + +def RGS_rank(rgs): + """ + Computes the rank of a restricted growth string. + + Examples + ======== + + >>> from sympy.combinatorics.partitions import RGS_rank, RGS_unrank + >>> RGS_rank([0, 1, 2, 1, 3]) + 42 + >>> RGS_rank(RGS_unrank(4, 7)) + 4 + """ + rgs_size = len(rgs) + rank = 0 + D = RGS_generalized(rgs_size) + for i in range(1, rgs_size): + n = len(rgs[(i + 1):]) + m = max(rgs[0:i]) + rank += D[n, m + 1] * rgs[i] + return rank diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/pc_groups.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/pc_groups.py new file mode 100644 index 0000000000000000000000000000000000000000..abf7b82258d8bb61ba350509fcbb3110a035fc88 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/pc_groups.py @@ -0,0 +1,710 @@ +from sympy.ntheory.primetest import isprime +from sympy.combinatorics.perm_groups import PermutationGroup +from sympy.printing.defaults import DefaultPrinting +from sympy.combinatorics.free_groups import free_group + + +class PolycyclicGroup(DefaultPrinting): + + is_group = True + is_solvable = True + + def __init__(self, pc_sequence, pc_series, relative_order, collector=None): + """ + + Parameters + ========== + + pc_sequence : list + A sequence of elements whose classes generate the cyclic factor + groups of pc_series. + pc_series : list + A subnormal sequence of subgroups where each factor group is cyclic. + relative_order : list + The orders of factor groups of pc_series. + collector : Collector + By default, it is None. Collector class provides the + polycyclic presentation with various other functionalities. + + """ + self.pcgs = pc_sequence + self.pc_series = pc_series + self.relative_order = relative_order + self.collector = Collector(self.pcgs, pc_series, relative_order) if not collector else collector + + def is_prime_order(self): + return all(isprime(order) for order in self.relative_order) + + def length(self): + return len(self.pcgs) + + +class Collector(DefaultPrinting): + + """ + References + ========== + + .. [1] Holt, D., Eick, B., O'Brien, E. + "Handbook of Computational Group Theory" + Section 8.1.3 + """ + + def __init__(self, pcgs, pc_series, relative_order, free_group_=None, pc_presentation=None): + """ + + Most of the parameters for the Collector class are the same as for PolycyclicGroup. + Others are described below. + + Parameters + ========== + + free_group_ : tuple + free_group_ provides the mapping of polycyclic generating + sequence with the free group elements. + pc_presentation : dict + Provides the presentation of polycyclic groups with the + help of power and conjugate relators. + + See Also + ======== + + PolycyclicGroup + + """ + self.pcgs = pcgs + self.pc_series = pc_series + self.relative_order = relative_order + self.free_group = free_group('x:{}'.format(len(pcgs)))[0] if not free_group_ else free_group_ + self.index = {s: i for i, s in enumerate(self.free_group.symbols)} + self.pc_presentation = self.pc_relators() + + def minimal_uncollected_subword(self, word): + r""" + Returns the minimal uncollected subwords. + + Explanation + =========== + + A word ``v`` defined on generators in ``X`` is a minimal + uncollected subword of the word ``w`` if ``v`` is a subword + of ``w`` and it has one of the following form + + * `v = {x_{i+1}}^{a_j}x_i` + + * `v = {x_{i+1}}^{a_j}{x_i}^{-1}` + + * `v = {x_i}^{a_j}` + + for `a_j` not in `\{1, \ldots, s-1\}`. Where, ``s`` is the power + exponent of the corresponding generator. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> from sympy.combinatorics import free_group + >>> G = SymmetricGroup(4) + >>> PcGroup = G.polycyclic_group() + >>> collector = PcGroup.collector + >>> F, x1, x2 = free_group("x1, x2") + >>> word = x2**2*x1**7 + >>> collector.minimal_uncollected_subword(word) + ((x2, 2),) + + """ + # To handle the case word = + if not word: + return None + + array = word.array_form + re = self.relative_order + index = self.index + + for i in range(len(array)): + s1, e1 = array[i] + + if re[index[s1]] and (e1 < 0 or e1 > re[index[s1]]-1): + return ((s1, e1), ) + + for i in range(len(array)-1): + s1, e1 = array[i] + s2, e2 = array[i+1] + + if index[s1] > index[s2]: + e = 1 if e2 > 0 else -1 + return ((s1, e1), (s2, e)) + + return None + + def relations(self): + """ + Separates the given relators of pc presentation in power and + conjugate relations. + + Returns + ======= + + (power_rel, conj_rel) + Separates pc presentation into power and conjugate relations. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> G = SymmetricGroup(3) + >>> PcGroup = G.polycyclic_group() + >>> collector = PcGroup.collector + >>> power_rel, conj_rel = collector.relations() + >>> power_rel + {x0**2: (), x1**3: ()} + >>> conj_rel + {x0**-1*x1*x0: x1**2} + + See Also + ======== + + pc_relators + + """ + power_relators = {} + conjugate_relators = {} + for key, value in self.pc_presentation.items(): + if len(key.array_form) == 1: + power_relators[key] = value + else: + conjugate_relators[key] = value + return power_relators, conjugate_relators + + def subword_index(self, word, w): + """ + Returns the start and ending index of a given + subword in a word. + + Parameters + ========== + + word : FreeGroupElement + word defined on free group elements for a + polycyclic group. + w : FreeGroupElement + subword of a given word, whose starting and + ending index to be computed. + + Returns + ======= + + (i, j) + A tuple containing starting and ending index of ``w`` + in the given word. If not exists, (-1,-1) is returned. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> from sympy.combinatorics import free_group + >>> G = SymmetricGroup(4) + >>> PcGroup = G.polycyclic_group() + >>> collector = PcGroup.collector + >>> F, x1, x2 = free_group("x1, x2") + >>> word = x2**2*x1**7 + >>> w = x2**2*x1 + >>> collector.subword_index(word, w) + (0, 3) + >>> w = x1**7 + >>> collector.subword_index(word, w) + (2, 9) + >>> w = x1**8 + >>> collector.subword_index(word, w) + (-1, -1) + + """ + low = -1 + high = -1 + for i in range(len(word)-len(w)+1): + if word.subword(i, i+len(w)) == w: + low = i + high = i+len(w) + break + return low, high + + def map_relation(self, w): + """ + Return a conjugate relation. + + Explanation + =========== + + Given a word formed by two free group elements, the + corresponding conjugate relation with those free + group elements is formed and mapped with the collected + word in the polycyclic presentation. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> from sympy.combinatorics import free_group + >>> G = SymmetricGroup(3) + >>> PcGroup = G.polycyclic_group() + >>> collector = PcGroup.collector + >>> F, x0, x1 = free_group("x0, x1") + >>> w = x1*x0 + >>> collector.map_relation(w) + x1**2 + + See Also + ======== + + pc_presentation + + """ + array = w.array_form + s1 = array[0][0] + s2 = array[1][0] + key = ((s2, -1), (s1, 1), (s2, 1)) + key = self.free_group.dtype(key) + return self.pc_presentation[key] + + + def collected_word(self, word): + r""" + Return the collected form of a word. + + Explanation + =========== + + A word ``w`` is called collected, if `w = {x_{i_1}}^{a_1} * \ldots * + {x_{i_r}}^{a_r}` with `i_1 < i_2< \ldots < i_r` and `a_j` is in + `\{1, \ldots, {s_j}-1\}`. + + Otherwise w is uncollected. + + Parameters + ========== + + word : FreeGroupElement + An uncollected word. + + Returns + ======= + + word + A collected word of form `w = {x_{i_1}}^{a_1}, \ldots, + {x_{i_r}}^{a_r}` with `i_1, i_2, \ldots, i_r` and `a_j \in + \{1, \ldots, {s_j}-1\}`. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> from sympy.combinatorics.perm_groups import PermutationGroup + >>> from sympy.combinatorics import free_group + >>> G = SymmetricGroup(4) + >>> PcGroup = G.polycyclic_group() + >>> collector = PcGroup.collector + >>> F, x0, x1, x2, x3 = free_group("x0, x1, x2, x3") + >>> word = x3*x2*x1*x0 + >>> collected_word = collector.collected_word(word) + >>> free_to_perm = {} + >>> free_group = collector.free_group + >>> for sym, gen in zip(free_group.symbols, collector.pcgs): + ... free_to_perm[sym] = gen + >>> G1 = PermutationGroup() + >>> for w in word: + ... sym = w[0] + ... perm = free_to_perm[sym] + ... G1 = PermutationGroup([perm] + G1.generators) + >>> G2 = PermutationGroup() + >>> for w in collected_word: + ... sym = w[0] + ... perm = free_to_perm[sym] + ... G2 = PermutationGroup([perm] + G2.generators) + + The two are not identical, but they are equivalent: + + >>> G1.equals(G2), G1 == G2 + (True, False) + + See Also + ======== + + minimal_uncollected_subword + + """ + free_group = self.free_group + while True: + w = self.minimal_uncollected_subword(word) + if not w: + break + + low, high = self.subword_index(word, free_group.dtype(w)) + if low == -1: + continue + + s1, e1 = w[0] + if len(w) == 1: + re = self.relative_order[self.index[s1]] + q = e1 // re + r = e1-q*re + + key = ((w[0][0], re), ) + key = free_group.dtype(key) + if self.pc_presentation[key]: + presentation = self.pc_presentation[key].array_form + sym, exp = presentation[0] + word_ = ((w[0][0], r), (sym, q*exp)) + word_ = free_group.dtype(word_) + else: + if r != 0: + word_ = ((w[0][0], r), ) + word_ = free_group.dtype(word_) + else: + word_ = None + word = word.eliminate_word(free_group.dtype(w), word_) + + if len(w) == 2 and w[1][1] > 0: + s2, e2 = w[1] + s2 = ((s2, 1), ) + s2 = free_group.dtype(s2) + word_ = self.map_relation(free_group.dtype(w)) + word_ = s2*word_**e1 + word_ = free_group.dtype(word_) + word = word.substituted_word(low, high, word_) + + elif len(w) == 2 and w[1][1] < 0: + s2, e2 = w[1] + s2 = ((s2, 1), ) + s2 = free_group.dtype(s2) + word_ = self.map_relation(free_group.dtype(w)) + word_ = s2**-1*word_**e1 + word_ = free_group.dtype(word_) + word = word.substituted_word(low, high, word_) + + return word + + + def pc_relators(self): + r""" + Return the polycyclic presentation. + + Explanation + =========== + + There are two types of relations used in polycyclic + presentation. + + * Power relations : Power relators are of the form `x_i^{re_i}`, + where `i \in \{0, \ldots, \mathrm{len(pcgs)}\}`, ``x`` represents polycyclic + generator and ``re`` is the corresponding relative order. + + * Conjugate relations : Conjugate relators are of the form `x_j^-1x_ix_j`, + where `j < i \in \{0, \ldots, \mathrm{len(pcgs)}\}`. + + Returns + ======= + + A dictionary with power and conjugate relations as key and + their collected form as corresponding values. + + Notes + ===== + + Identity Permutation is mapped with empty ``()``. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> from sympy.combinatorics.permutations import Permutation + >>> S = SymmetricGroup(49).sylow_subgroup(7) + >>> der = S.derived_series() + >>> G = der[len(der)-2] + >>> PcGroup = G.polycyclic_group() + >>> collector = PcGroup.collector + >>> pcgs = PcGroup.pcgs + >>> len(pcgs) + 6 + >>> free_group = collector.free_group + >>> pc_resentation = collector.pc_presentation + >>> free_to_perm = {} + >>> for s, g in zip(free_group.symbols, pcgs): + ... free_to_perm[s] = g + + >>> for k, v in pc_resentation.items(): + ... k_array = k.array_form + ... if v != (): + ... v_array = v.array_form + ... lhs = Permutation() + ... for gen in k_array: + ... s = gen[0] + ... e = gen[1] + ... lhs = lhs*free_to_perm[s]**e + ... if v == (): + ... assert lhs.is_identity + ... continue + ... rhs = Permutation() + ... for gen in v_array: + ... s = gen[0] + ... e = gen[1] + ... rhs = rhs*free_to_perm[s]**e + ... assert lhs == rhs + + """ + free_group = self.free_group + rel_order = self.relative_order + pc_relators = {} + perm_to_free = {} + pcgs = self.pcgs + + for gen, s in zip(pcgs, free_group.generators): + perm_to_free[gen**-1] = s**-1 + perm_to_free[gen] = s + + pcgs = pcgs[::-1] + series = self.pc_series[::-1] + rel_order = rel_order[::-1] + collected_gens = [] + + for i, gen in enumerate(pcgs): + re = rel_order[i] + relation = perm_to_free[gen]**re + G = series[i] + + l = G.generator_product(gen**re, original = True) + l.reverse() + + word = free_group.identity + for g in l: + word = word*perm_to_free[g] + + word = self.collected_word(word) + pc_relators[relation] = word if word else () + self.pc_presentation = pc_relators + + collected_gens.append(gen) + if len(collected_gens) > 1: + conj = collected_gens[len(collected_gens)-1] + conjugator = perm_to_free[conj] + + for j in range(len(collected_gens)-1): + conjugated = perm_to_free[collected_gens[j]] + + relation = conjugator**-1*conjugated*conjugator + gens = conj**-1*collected_gens[j]*conj + + l = G.generator_product(gens, original = True) + l.reverse() + word = free_group.identity + for g in l: + word = word*perm_to_free[g] + + word = self.collected_word(word) + pc_relators[relation] = word if word else () + self.pc_presentation = pc_relators + + return pc_relators + + def exponent_vector(self, element): + r""" + Return the exponent vector of length equal to the + length of polycyclic generating sequence. + + Explanation + =========== + + For a given generator/element ``g`` of the polycyclic group, + it can be represented as `g = {x_1}^{e_1}, \ldots, {x_n}^{e_n}`, + where `x_i` represents polycyclic generators and ``n`` is + the number of generators in the free_group equal to the length + of pcgs. + + Parameters + ========== + + element : Permutation + Generator of a polycyclic group. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> from sympy.combinatorics.permutations import Permutation + >>> G = SymmetricGroup(4) + >>> PcGroup = G.polycyclic_group() + >>> collector = PcGroup.collector + >>> pcgs = PcGroup.pcgs + >>> collector.exponent_vector(G[0]) + [1, 0, 0, 0] + >>> exp = collector.exponent_vector(G[1]) + >>> g = Permutation() + >>> for i in range(len(exp)): + ... g = g*pcgs[i]**exp[i] if exp[i] else g + >>> assert g == G[1] + + References + ========== + + .. [1] Holt, D., Eick, B., O'Brien, E. + "Handbook of Computational Group Theory" + Section 8.1.1, Definition 8.4 + + """ + free_group = self.free_group + G = PermutationGroup() + for g in self.pcgs: + G = PermutationGroup([g] + G.generators) + gens = G.generator_product(element, original = True) + gens.reverse() + + perm_to_free = {} + for sym, g in zip(free_group.generators, self.pcgs): + perm_to_free[g**-1] = sym**-1 + perm_to_free[g] = sym + w = free_group.identity + for g in gens: + w = w*perm_to_free[g] + + word = self.collected_word(w) + + index = self.index + exp_vector = [0]*len(free_group) + word = word.array_form + for t in word: + exp_vector[index[t[0]]] = t[1] + return exp_vector + + def depth(self, element): + r""" + Return the depth of a given element. + + Explanation + =========== + + The depth of a given element ``g`` is defined by + `\mathrm{dep}[g] = i` if `e_1 = e_2 = \ldots = e_{i-1} = 0` + and `e_i != 0`, where ``e`` represents the exponent-vector. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> G = SymmetricGroup(3) + >>> PcGroup = G.polycyclic_group() + >>> collector = PcGroup.collector + >>> collector.depth(G[0]) + 2 + >>> collector.depth(G[1]) + 1 + + References + ========== + + .. [1] Holt, D., Eick, B., O'Brien, E. + "Handbook of Computational Group Theory" + Section 8.1.1, Definition 8.5 + + """ + exp_vector = self.exponent_vector(element) + return next((i+1 for i, x in enumerate(exp_vector) if x), len(self.pcgs)+1) + + def leading_exponent(self, element): + r""" + Return the leading non-zero exponent. + + Explanation + =========== + + The leading exponent for a given element `g` is defined + by `\mathrm{leading\_exponent}[g]` `= e_i`, if `\mathrm{depth}[g] = i`. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> G = SymmetricGroup(3) + >>> PcGroup = G.polycyclic_group() + >>> collector = PcGroup.collector + >>> collector.leading_exponent(G[1]) + 1 + + """ + exp_vector = self.exponent_vector(element) + depth = self.depth(element) + if depth != len(self.pcgs)+1: + return exp_vector[depth-1] + return None + + def _sift(self, z, g): + h = g + d = self.depth(h) + while d < len(self.pcgs) and z[d-1] != 1: + k = z[d-1] + e = self.leading_exponent(h)*(self.leading_exponent(k))**-1 + e = e % self.relative_order[d-1] + h = k**-e*h + d = self.depth(h) + return h + + def induced_pcgs(self, gens): + """ + + Parameters + ========== + + gens : list + A list of generators on which polycyclic subgroup + is to be defined. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> S = SymmetricGroup(8) + >>> G = S.sylow_subgroup(2) + >>> PcGroup = G.polycyclic_group() + >>> collector = PcGroup.collector + >>> gens = [G[0], G[1]] + >>> ipcgs = collector.induced_pcgs(gens) + >>> [gen.order() for gen in ipcgs] + [2, 2, 2] + >>> G = S.sylow_subgroup(3) + >>> PcGroup = G.polycyclic_group() + >>> collector = PcGroup.collector + >>> gens = [G[0], G[1]] + >>> ipcgs = collector.induced_pcgs(gens) + >>> [gen.order() for gen in ipcgs] + [3] + + """ + z = [1]*len(self.pcgs) + G = gens + while G: + g = G.pop(0) + h = self._sift(z, g) + d = self.depth(h) + if d < len(self.pcgs): + for gen in z: + if gen != 1: + G.append(h**-1*gen**-1*h*gen) + z[d-1] = h + z = [gen for gen in z if gen != 1] + return z + + def constructive_membership_test(self, ipcgs, g): + """ + Return the exponent vector for induced pcgs. + """ + e = [0]*len(ipcgs) + h = g + d = self.depth(h) + for i, gen in enumerate(ipcgs): + while self.depth(gen) == d: + f = self.leading_exponent(h)*self.leading_exponent(gen) + f = f % self.relative_order[d-1] + h = gen**(-f)*h + e[i] = f + d = self.depth(h) + if h == 1: + return e + return False diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/perm_groups.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/perm_groups.py new file mode 100644 index 0000000000000000000000000000000000000000..24359cb546246d63470de92b2fb2ab0804fc9886 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/perm_groups.py @@ -0,0 +1,5459 @@ +from math import factorial as _factorial, log, prod +from itertools import chain, product + + +from sympy.combinatorics import Permutation +from sympy.combinatorics.permutations import (_af_commutes_with, _af_invert, + _af_rmul, _af_rmuln, _af_pow, Cycle) +from sympy.combinatorics.util import (_check_cycles_alt_sym, + _distribute_gens_by_base, _orbits_transversals_from_bsgs, + _handle_precomputed_bsgs, _base_ordering, _strong_gens_from_distr, + _strip, _strip_af) +from sympy.core import Basic +from sympy.core.random import _randrange, randrange, choice +from sympy.core.symbol import Symbol +from sympy.core.sympify import _sympify +from sympy.functions.combinatorial.factorials import factorial +from sympy.ntheory import primefactors, sieve +from sympy.ntheory.factor_ import (factorint, multiplicity) +from sympy.ntheory.primetest import isprime +from sympy.utilities.iterables import has_variety, is_sequence, uniq + +rmul = Permutation.rmul_with_af +_af_new = Permutation._af_new + + +class PermutationGroup(Basic): + r"""The class defining a Permutation group. + + Explanation + =========== + + ``PermutationGroup([p1, p2, ..., pn])`` returns the permutation group + generated by the list of permutations. This group can be supplied + to Polyhedron if one desires to decorate the elements to which the + indices of the permutation refer. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> from sympy.combinatorics import Polyhedron + + The permutations corresponding to motion of the front, right and + bottom face of a $2 \times 2$ Rubik's cube are defined: + + >>> F = Permutation(2, 19, 21, 8)(3, 17, 20, 10)(4, 6, 7, 5) + >>> R = Permutation(1, 5, 21, 14)(3, 7, 23, 12)(8, 10, 11, 9) + >>> D = Permutation(6, 18, 14, 10)(7, 19, 15, 11)(20, 22, 23, 21) + + These are passed as permutations to PermutationGroup: + + >>> G = PermutationGroup(F, R, D) + >>> G.order() + 3674160 + + The group can be supplied to a Polyhedron in order to track the + objects being moved. An example involving the $2 \times 2$ Rubik's cube is + given there, but here is a simple demonstration: + + >>> a = Permutation(2, 1) + >>> b = Permutation(1, 0) + >>> G = PermutationGroup(a, b) + >>> P = Polyhedron(list('ABC'), pgroup=G) + >>> P.corners + (A, B, C) + >>> P.rotate(0) # apply permutation 0 + >>> P.corners + (A, C, B) + >>> P.reset() + >>> P.corners + (A, B, C) + + Or one can make a permutation as a product of selected permutations + and apply them to an iterable directly: + + >>> P10 = G.make_perm([0, 1]) + >>> P10('ABC') + ['C', 'A', 'B'] + + See Also + ======== + + sympy.combinatorics.polyhedron.Polyhedron, + sympy.combinatorics.permutations.Permutation + + References + ========== + + .. [1] Holt, D., Eick, B., O'Brien, E. + "Handbook of Computational Group Theory" + + .. [2] Seress, A. + "Permutation Group Algorithms" + + .. [3] https://en.wikipedia.org/wiki/Schreier_vector + + .. [4] https://en.wikipedia.org/wiki/Nielsen_transformation#Product_replacement_algorithm + + .. [5] Frank Celler, Charles R.Leedham-Green, Scott H.Murray, + Alice C.Niemeyer, and E.A.O'Brien. "Generating Random + Elements of a Finite Group" + + .. [6] https://en.wikipedia.org/wiki/Block_%28permutation_group_theory%29 + + .. [7] https://algorithmist.com/wiki/Union_find + + .. [8] https://en.wikipedia.org/wiki/Multiply_transitive_group#Multiply_transitive_groups + + .. [9] https://en.wikipedia.org/wiki/Center_%28group_theory%29 + + .. [10] https://en.wikipedia.org/wiki/Centralizer_and_normalizer + + .. [11] https://groupprops.subwiki.org/wiki/Derived_subgroup + + .. [12] https://en.wikipedia.org/wiki/Nilpotent_group + + .. [13] https://www.math.colostate.edu/~hulpke/CGT/cgtnotes.pdf + + .. [14] https://docs.gap-system.org/doc/ref/manual.pdf + + """ + is_group = True + + def __new__(cls, *args, dups=True, **kwargs): + """The default constructor. Accepts Cycle and Permutation forms. + Removes duplicates unless ``dups`` keyword is ``False``. + """ + if not args: + args = [Permutation()] + else: + args = list(args[0] if is_sequence(args[0]) else args) + if not args: + args = [Permutation()] + if any(isinstance(a, Cycle) for a in args): + args = [Permutation(a) for a in args] + if has_variety(a.size for a in args): + degree = kwargs.pop('degree', None) + if degree is None: + degree = max(a.size for a in args) + for i in range(len(args)): + if args[i].size != degree: + args[i] = Permutation(args[i], size=degree) + if dups: + args = list(uniq([_af_new(list(a)) for a in args])) + if len(args) > 1: + args = [g for g in args if not g.is_identity] + return Basic.__new__(cls, *args, **kwargs) + + def __init__(self, *args, **kwargs): + self._generators = list(self.args) + self._order = None + self._elements = [] + self._center = None + self._is_abelian = None + self._is_transitive = None + self._is_sym = None + self._is_alt = None + self._is_primitive = None + self._is_nilpotent = None + self._is_solvable = None + self._is_trivial = None + self._transitivity_degree = None + self._max_div = None + self._is_perfect = None + self._is_cyclic = None + self._is_dihedral = None + self._r = len(self._generators) + self._degree = self._generators[0].size + + # these attributes are assigned after running schreier_sims + self._base = [] + self._strong_gens = [] + self._strong_gens_slp = [] + self._basic_orbits = [] + self._transversals = [] + self._transversal_slp = [] + + # these attributes are assigned after running _random_pr_init + self._random_gens = [] + + # finite presentation of the group as an instance of `FpGroup` + self._fp_presentation = None + + def __getitem__(self, i): + return self._generators[i] + + def __contains__(self, i): + """Return ``True`` if *i* is contained in PermutationGroup. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> p = Permutation(1, 2, 3) + >>> Permutation(3) in PermutationGroup(p) + True + + """ + if not isinstance(i, Permutation): + raise TypeError("A PermutationGroup contains only Permutations as " + "elements, not elements of type %s" % type(i)) + return self.contains(i) + + def __len__(self): + return len(self._generators) + + def equals(self, other): + """Return ``True`` if PermutationGroup generated by elements in the + group are same i.e they represent the same PermutationGroup. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> p = Permutation(0, 1, 2, 3, 4, 5) + >>> G = PermutationGroup([p, p**2]) + >>> H = PermutationGroup([p**2, p]) + >>> G.generators == H.generators + False + >>> G.equals(H) + True + + """ + if not isinstance(other, PermutationGroup): + return False + + set_self_gens = set(self.generators) + set_other_gens = set(other.generators) + + # before reaching the general case there are also certain + # optimisation and obvious cases requiring less or no actual + # computation. + if set_self_gens == set_other_gens: + return True + + # in the most general case it will check that each generator of + # one group belongs to the other PermutationGroup and vice-versa + for gen1 in set_self_gens: + if not other.contains(gen1): + return False + for gen2 in set_other_gens: + if not self.contains(gen2): + return False + return True + + def __mul__(self, other): + """ + Return the direct product of two permutation groups as a permutation + group. + + Explanation + =========== + + This implementation realizes the direct product by shifting the index + set for the generators of the second group: so if we have ``G`` acting + on ``n1`` points and ``H`` acting on ``n2`` points, ``G*H`` acts on + ``n1 + n2`` points. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import CyclicGroup + >>> G = CyclicGroup(5) + >>> H = G*G + >>> H + PermutationGroup([ + (9)(0 1 2 3 4), + (5 6 7 8 9)]) + >>> H.order() + 25 + + """ + if isinstance(other, Permutation): + return Coset(other, self, dir='+') + gens1 = [perm._array_form for perm in self.generators] + gens2 = [perm._array_form for perm in other.generators] + n1 = self._degree + n2 = other._degree + start = list(range(n1)) + end = list(range(n1, n1 + n2)) + for i in range(len(gens2)): + gens2[i] = [x + n1 for x in gens2[i]] + gens2 = [start + gen for gen in gens2] + gens1 = [gen + end for gen in gens1] + together = gens1 + gens2 + gens = [_af_new(x) for x in together] + return PermutationGroup(gens) + + def _random_pr_init(self, r, n, _random_prec_n=None): + r"""Initialize random generators for the product replacement algorithm. + + Explanation + =========== + + The implementation uses a modification of the original product + replacement algorithm due to Leedham-Green, as described in [1], + pp. 69-71; also, see [2], pp. 27-29 for a detailed theoretical + analysis of the original product replacement algorithm, and [4]. + + The product replacement algorithm is used for producing random, + uniformly distributed elements of a group `G` with a set of generators + `S`. For the initialization ``_random_pr_init``, a list ``R`` of + `\max\{r, |S|\}` group generators is created as the attribute + ``G._random_gens``, repeating elements of `S` if necessary, and the + identity element of `G` is appended to ``R`` - we shall refer to this + last element as the accumulator. Then the function ``random_pr()`` + is called ``n`` times, randomizing the list ``R`` while preserving + the generation of `G` by ``R``. The function ``random_pr()`` itself + takes two random elements ``g, h`` among all elements of ``R`` but + the accumulator and replaces ``g`` with a randomly chosen element + from `\{gh, g(~h), hg, (~h)g\}`. Then the accumulator is multiplied + by whatever ``g`` was replaced by. The new value of the accumulator is + then returned by ``random_pr()``. + + The elements returned will eventually (for ``n`` large enough) become + uniformly distributed across `G` ([5]). For practical purposes however, + the values ``n = 50, r = 11`` are suggested in [1]. + + Notes + ===== + + THIS FUNCTION HAS SIDE EFFECTS: it changes the attribute + self._random_gens + + See Also + ======== + + random_pr + + """ + deg = self.degree + random_gens = [x._array_form for x in self.generators] + k = len(random_gens) + if k < r: + for i in range(k, r): + random_gens.append(random_gens[i - k]) + acc = list(range(deg)) + random_gens.append(acc) + self._random_gens = random_gens + + # handle randomized input for testing purposes + if _random_prec_n is None: + for i in range(n): + self.random_pr() + else: + for i in range(n): + self.random_pr(_random_prec=_random_prec_n[i]) + + def _union_find_merge(self, first, second, ranks, parents, not_rep): + """Merges two classes in a union-find data structure. + + Explanation + =========== + + Used in the implementation of Atkinson's algorithm as suggested in [1], + pp. 83-87. The class merging process uses union by rank as an + optimization. ([7]) + + Notes + ===== + + THIS FUNCTION HAS SIDE EFFECTS: the list of class representatives, + ``parents``, the list of class sizes, ``ranks``, and the list of + elements that are not representatives, ``not_rep``, are changed due to + class merging. + + See Also + ======== + + minimal_block, _union_find_rep + + References + ========== + + .. [1] Holt, D., Eick, B., O'Brien, E. + "Handbook of computational group theory" + + .. [7] https://algorithmist.com/wiki/Union_find + + """ + rep_first = self._union_find_rep(first, parents) + rep_second = self._union_find_rep(second, parents) + if rep_first != rep_second: + # union by rank + if ranks[rep_first] >= ranks[rep_second]: + new_1, new_2 = rep_first, rep_second + else: + new_1, new_2 = rep_second, rep_first + total_rank = ranks[new_1] + ranks[new_2] + if total_rank > self.max_div: + return -1 + parents[new_2] = new_1 + ranks[new_1] = total_rank + not_rep.append(new_2) + return 1 + return 0 + + def _union_find_rep(self, num, parents): + """Find representative of a class in a union-find data structure. + + Explanation + =========== + + Used in the implementation of Atkinson's algorithm as suggested in [1], + pp. 83-87. After the representative of the class to which ``num`` + belongs is found, path compression is performed as an optimization + ([7]). + + Notes + ===== + + THIS FUNCTION HAS SIDE EFFECTS: the list of class representatives, + ``parents``, is altered due to path compression. + + See Also + ======== + + minimal_block, _union_find_merge + + References + ========== + + .. [1] Holt, D., Eick, B., O'Brien, E. + "Handbook of computational group theory" + + .. [7] https://algorithmist.com/wiki/Union_find + + """ + rep, parent = num, parents[num] + while parent != rep: + rep = parent + parent = parents[rep] + # path compression + temp, parent = num, parents[num] + while parent != rep: + parents[temp] = rep + temp = parent + parent = parents[temp] + return rep + + @property + def base(self): + r"""Return a base from the Schreier-Sims algorithm. + + Explanation + =========== + + For a permutation group `G`, a base is a sequence of points + `B = (b_1, b_2, \dots, b_k)` such that no element of `G` apart + from the identity fixes all the points in `B`. The concepts of + a base and strong generating set and their applications are + discussed in depth in [1], pp. 87-89 and [2], pp. 55-57. + + An alternative way to think of `B` is that it gives the + indices of the stabilizer cosets that contain more than the + identity permutation. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> G = PermutationGroup([Permutation(0, 1, 3)(2, 4)]) + >>> G.base + [0, 2] + + See Also + ======== + + strong_gens, basic_transversals, basic_orbits, basic_stabilizers + + """ + if self._base == []: + self.schreier_sims() + return self._base + + def baseswap(self, base, strong_gens, pos, randomized=False, + transversals=None, basic_orbits=None, strong_gens_distr=None): + r"""Swap two consecutive base points in base and strong generating set. + + Explanation + =========== + + If a base for a group `G` is given by `(b_1, b_2, \dots, b_k)`, this + function returns a base `(b_1, b_2, \dots, b_{i+1}, b_i, \dots, b_k)`, + where `i` is given by ``pos``, and a strong generating set relative + to that base. The original base and strong generating set are not + modified. + + The randomized version (default) is of Las Vegas type. + + Parameters + ========== + + base, strong_gens + The base and strong generating set. + pos + The position at which swapping is performed. + randomized + A switch between randomized and deterministic version. + transversals + The transversals for the basic orbits, if known. + basic_orbits + The basic orbits, if known. + strong_gens_distr + The strong generators distributed by basic stabilizers, if known. + + Returns + ======= + + (base, strong_gens) + ``base`` is the new base, and ``strong_gens`` is a generating set + relative to it. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> from sympy.combinatorics.testutil import _verify_bsgs + >>> from sympy.combinatorics.perm_groups import PermutationGroup + >>> S = SymmetricGroup(4) + >>> S.schreier_sims() + >>> S.base + [0, 1, 2] + >>> base, gens = S.baseswap(S.base, S.strong_gens, 1, randomized=False) + >>> base, gens + ([0, 2, 1], + [(0 1 2 3), (3)(0 1), (1 3 2), + (2 3), (1 3)]) + + check that base, gens is a BSGS + + >>> S1 = PermutationGroup(gens) + >>> _verify_bsgs(S1, base, gens) + True + + See Also + ======== + + schreier_sims + + Notes + ===== + + The deterministic version of the algorithm is discussed in + [1], pp. 102-103; the randomized version is discussed in [1], p.103, and + [2], p.98. It is of Las Vegas type. + Notice that [1] contains a mistake in the pseudocode and + discussion of BASESWAP: on line 3 of the pseudocode, + `|\beta_{i+1}^{\left\langle T\right\rangle}|` should be replaced by + `|\beta_{i}^{\left\langle T\right\rangle}|`, and the same for the + discussion of the algorithm. + + """ + # construct the basic orbits, generators for the stabilizer chain + # and transversal elements from whatever was provided + transversals, basic_orbits, strong_gens_distr = \ + _handle_precomputed_bsgs(base, strong_gens, transversals, + basic_orbits, strong_gens_distr) + base_len = len(base) + degree = self.degree + # size of orbit of base[pos] under the stabilizer we seek to insert + # in the stabilizer chain at position pos + 1 + size = len(basic_orbits[pos])*len(basic_orbits[pos + 1]) \ + //len(_orbit(degree, strong_gens_distr[pos], base[pos + 1])) + # initialize the wanted stabilizer by a subgroup + if pos + 2 > base_len - 1: + T = [] + else: + T = strong_gens_distr[pos + 2][:] + # randomized version + if randomized is True: + stab_pos = PermutationGroup(strong_gens_distr[pos]) + schreier_vector = stab_pos.schreier_vector(base[pos + 1]) + # add random elements of the stabilizer until they generate it + while len(_orbit(degree, T, base[pos])) != size: + new = stab_pos.random_stab(base[pos + 1], + schreier_vector=schreier_vector) + T.append(new) + # deterministic version + else: + Gamma = set(basic_orbits[pos]) + Gamma.remove(base[pos]) + if base[pos + 1] in Gamma: + Gamma.remove(base[pos + 1]) + # add elements of the stabilizer until they generate it by + # ruling out member of the basic orbit of base[pos] along the way + while len(_orbit(degree, T, base[pos])) != size: + gamma = next(iter(Gamma)) + x = transversals[pos][gamma] + temp = x._array_form.index(base[pos + 1]) # (~x)(base[pos + 1]) + if temp not in basic_orbits[pos + 1]: + Gamma = Gamma - _orbit(degree, T, gamma) + else: + y = transversals[pos + 1][temp] + el = rmul(x, y) + if el(base[pos]) not in _orbit(degree, T, base[pos]): + T.append(el) + Gamma = Gamma - _orbit(degree, T, base[pos]) + # build the new base and strong generating set + strong_gens_new_distr = strong_gens_distr[:] + strong_gens_new_distr[pos + 1] = T + base_new = base[:] + base_new[pos], base_new[pos + 1] = base_new[pos + 1], base_new[pos] + strong_gens_new = _strong_gens_from_distr(strong_gens_new_distr) + for gen in T: + if gen not in strong_gens_new: + strong_gens_new.append(gen) + return base_new, strong_gens_new + + @property + def basic_orbits(self): + r""" + Return the basic orbits relative to a base and strong generating set. + + Explanation + =========== + + If `(b_1, b_2, \dots, b_k)` is a base for a group `G`, and + `G^{(i)} = G_{b_1, b_2, \dots, b_{i-1}}` is the ``i``-th basic stabilizer + (so that `G^{(1)} = G`), the ``i``-th basic orbit relative to this base + is the orbit of `b_i` under `G^{(i)}`. See [1], pp. 87-89 for more + information. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> S = SymmetricGroup(4) + >>> S.basic_orbits + [[0, 1, 2, 3], [1, 2, 3], [2, 3]] + + See Also + ======== + + base, strong_gens, basic_transversals, basic_stabilizers + + """ + if self._basic_orbits == []: + self.schreier_sims() + return self._basic_orbits + + @property + def basic_stabilizers(self): + r""" + Return a chain of stabilizers relative to a base and strong generating + set. + + Explanation + =========== + + The ``i``-th basic stabilizer `G^{(i)}` relative to a base + `(b_1, b_2, \dots, b_k)` is `G_{b_1, b_2, \dots, b_{i-1}}`. For more + information, see [1], pp. 87-89. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import AlternatingGroup + >>> A = AlternatingGroup(4) + >>> A.schreier_sims() + >>> A.base + [0, 1] + >>> for g in A.basic_stabilizers: + ... print(g) + ... + PermutationGroup([ + (3)(0 1 2), + (1 2 3)]) + PermutationGroup([ + (1 2 3)]) + + See Also + ======== + + base, strong_gens, basic_orbits, basic_transversals + + """ + + if self._transversals == []: + self.schreier_sims() + strong_gens = self._strong_gens + base = self._base + if not base: # e.g. if self is trivial + return [] + strong_gens_distr = _distribute_gens_by_base(base, strong_gens) + basic_stabilizers = [] + for gens in strong_gens_distr: + basic_stabilizers.append(PermutationGroup(gens)) + return basic_stabilizers + + @property + def basic_transversals(self): + """ + Return basic transversals relative to a base and strong generating set. + + Explanation + =========== + + The basic transversals are transversals of the basic orbits. They + are provided as a list of dictionaries, each dictionary having + keys - the elements of one of the basic orbits, and values - the + corresponding transversal elements. See [1], pp. 87-89 for more + information. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import AlternatingGroup + >>> A = AlternatingGroup(4) + >>> A.basic_transversals + [{0: (3), 1: (3)(0 1 2), 2: (3)(0 2 1), 3: (0 3 1)}, {1: (3), 2: (1 2 3), 3: (1 3 2)}] + + See Also + ======== + + strong_gens, base, basic_orbits, basic_stabilizers + + """ + + if self._transversals == []: + self.schreier_sims() + return self._transversals + + def composition_series(self): + r""" + Return the composition series for a group as a list + of permutation groups. + + Explanation + =========== + + The composition series for a group `G` is defined as a + subnormal series `G = H_0 > H_1 > H_2 \ldots` A composition + series is a subnormal series such that each factor group + `H(i+1) / H(i)` is simple. + A subnormal series is a composition series only if it is of + maximum length. + + The algorithm works as follows: + Starting with the derived series the idea is to fill + the gap between `G = der[i]` and `H = der[i+1]` for each + `i` independently. Since, all subgroups of the abelian group + `G/H` are normal so, first step is to take the generators + `g` of `G` and add them to generators of `H` one by one. + + The factor groups formed are not simple in general. Each + group is obtained from the previous one by adding one + generator `g`, if the previous group is denoted by `H` + then the next group `K` is generated by `g` and `H`. + The factor group `K/H` is cyclic and it's order is + `K.order()//G.order()`. The series is then extended between + `K` and `H` by groups generated by powers of `g` and `H`. + The series formed is then prepended to the already existing + series. + + Examples + ======== + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> from sympy.combinatorics.named_groups import CyclicGroup + >>> S = SymmetricGroup(12) + >>> G = S.sylow_subgroup(2) + >>> C = G.composition_series() + >>> [H.order() for H in C] + [1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1] + >>> G = S.sylow_subgroup(3) + >>> C = G.composition_series() + >>> [H.order() for H in C] + [243, 81, 27, 9, 3, 1] + >>> G = CyclicGroup(12) + >>> C = G.composition_series() + >>> [H.order() for H in C] + [12, 6, 3, 1] + + """ + der = self.derived_series() + if not all(g.is_identity for g in der[-1].generators): + raise NotImplementedError('Group should be solvable') + series = [] + + for i in range(len(der)-1): + H = der[i+1] + up_seg = [] + for g in der[i].generators: + K = PermutationGroup([g] + H.generators) + order = K.order() // H.order() + down_seg = [] + for p, e in factorint(order).items(): + for _ in range(e): + down_seg.append(PermutationGroup([g] + H.generators)) + g = g**p + up_seg = down_seg + up_seg + H = K + up_seg[0] = der[i] + series.extend(up_seg) + series.append(der[-1]) + return series + + def coset_transversal(self, H): + """Return a transversal of the right cosets of self by its subgroup H + using the second method described in [1], Subsection 4.6.7 + + """ + + if not H.is_subgroup(self): + raise ValueError("The argument must be a subgroup") + + if H.order() == 1: + return self.elements + + self._schreier_sims(base=H.base) # make G.base an extension of H.base + + base = self.base + base_ordering = _base_ordering(base, self.degree) + identity = Permutation(self.degree - 1) + + transversals = self.basic_transversals[:] + # transversals is a list of dictionaries. Get rid of the keys + # so that it is a list of lists and sort each list in + # the increasing order of base[l]^x + for l, t in enumerate(transversals): + transversals[l] = sorted(t.values(), + key = lambda x: base_ordering[base[l]^x]) + + orbits = H.basic_orbits + h_stabs = H.basic_stabilizers + g_stabs = self.basic_stabilizers + + indices = [x.order()//y.order() for x, y in zip(g_stabs, h_stabs)] + + # T^(l) should be a right transversal of H^(l) in G^(l) for + # 1<=l<=len(base). While H^(l) is the trivial group, T^(l) + # contains all the elements of G^(l) so we might just as well + # start with l = len(h_stabs)-1 + if len(g_stabs) > len(h_stabs): + T = g_stabs[len(h_stabs)].elements + else: + T = [identity] + l = len(h_stabs)-1 + t_len = len(T) + while l > -1: + T_next = [] + for u in transversals[l]: + if u == identity: + continue + b = base_ordering[base[l]^u] + for t in T: + p = t*u + if all(base_ordering[h^p] >= b for h in orbits[l]): + T_next.append(p) + if t_len + len(T_next) == indices[l]: + break + if t_len + len(T_next) == indices[l]: + break + T += T_next + t_len += len(T_next) + l -= 1 + T.remove(identity) + T = [identity] + T + return T + + def _coset_representative(self, g, H): + """Return the representative of Hg from the transversal that + would be computed by ``self.coset_transversal(H)``. + + """ + if H.order() == 1: + return g + # The base of self must be an extension of H.base. + if not(self.base[:len(H.base)] == H.base): + self._schreier_sims(base=H.base) + orbits = H.basic_orbits[:] + h_transversals = [list(_.values()) for _ in H.basic_transversals] + transversals = [list(_.values()) for _ in self.basic_transversals] + base = self.base + base_ordering = _base_ordering(base, self.degree) + def step(l, x): + gamma = min(orbits[l], key = lambda y: base_ordering[y^x]) + i = [base[l]^h for h in h_transversals[l]].index(gamma) + x = h_transversals[l][i]*x + if l < len(orbits)-1: + for u in transversals[l]: + if base[l]^u == base[l]^x: + break + x = step(l+1, x*u**-1)*u + return x + return step(0, g) + + def coset_table(self, H): + """Return the standardised (right) coset table of self in H as + a list of lists. + """ + # Maybe this should be made to return an instance of CosetTable + # from fp_groups.py but the class would need to be changed first + # to be compatible with PermutationGroups + + if not H.is_subgroup(self): + raise ValueError("The argument must be a subgroup") + T = self.coset_transversal(H) + n = len(T) + + A = list(chain.from_iterable((gen, gen**-1) + for gen in self.generators)) + + table = [] + for i in range(n): + row = [self._coset_representative(T[i]*x, H) for x in A] + row = [T.index(r) for r in row] + table.append(row) + + # standardize (this is the same as the algorithm used in coset_table) + # If CosetTable is made compatible with PermutationGroups, this + # should be replaced by table.standardize() + A = range(len(A)) + gamma = 1 + for alpha, a in product(range(n), A): + beta = table[alpha][a] + if beta >= gamma: + if beta > gamma: + for x in A: + z = table[gamma][x] + table[gamma][x] = table[beta][x] + table[beta][x] = z + for i in range(n): + if table[i][x] == beta: + table[i][x] = gamma + elif table[i][x] == gamma: + table[i][x] = beta + gamma += 1 + if gamma >= n-1: + return table + + def center(self): + r""" + Return the center of a permutation group. + + Explanation + =========== + + The center for a group `G` is defined as + `Z(G) = \{z\in G | \forall g\in G, zg = gz \}`, + the set of elements of `G` that commute with all elements of `G`. + It is equal to the centralizer of `G` inside `G`, and is naturally a + subgroup of `G` ([9]). + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> D = DihedralGroup(4) + >>> G = D.center() + >>> G.order() + 2 + + See Also + ======== + + centralizer + + Notes + ===== + + This is a naive implementation that is a straightforward application + of ``.centralizer()`` + + """ + if not self._center: + self._center = self.centralizer(self) + return self._center + + def centralizer(self, other): + r""" + Return the centralizer of a group/set/element. + + Explanation + =========== + + The centralizer of a set of permutations ``S`` inside + a group ``G`` is the set of elements of ``G`` that commute with all + elements of ``S``:: + + `C_G(S) = \{ g \in G | gs = sg \forall s \in S\}` ([10]) + + Usually, ``S`` is a subset of ``G``, but if ``G`` is a proper subgroup of + the full symmetric group, we allow for ``S`` to have elements outside + ``G``. + + It is naturally a subgroup of ``G``; the centralizer of a permutation + group is equal to the centralizer of any set of generators for that + group, since any element commuting with the generators commutes with + any product of the generators. + + Parameters + ========== + + other + a permutation group/list of permutations/single permutation + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import (SymmetricGroup, + ... CyclicGroup) + >>> S = SymmetricGroup(6) + >>> C = CyclicGroup(6) + >>> H = S.centralizer(C) + >>> H.is_subgroup(C) + True + + See Also + ======== + + subgroup_search + + Notes + ===== + + The implementation is an application of ``.subgroup_search()`` with + tests using a specific base for the group ``G``. + + """ + if hasattr(other, 'generators'): + if other.is_trivial or self.is_trivial: + return self + degree = self.degree + identity = _af_new(list(range(degree))) + orbits = other.orbits() + num_orbits = len(orbits) + orbits.sort(key=lambda x: -len(x)) + long_base = [] + orbit_reps = [None]*num_orbits + orbit_reps_indices = [None]*num_orbits + orbit_descr = [None]*degree + for i in range(num_orbits): + orbit = list(orbits[i]) + orbit_reps[i] = orbit[0] + orbit_reps_indices[i] = len(long_base) + for point in orbit: + orbit_descr[point] = i + long_base = long_base + orbit + base, strong_gens = self.schreier_sims_incremental(base=long_base) + strong_gens_distr = _distribute_gens_by_base(base, strong_gens) + i = 0 + for i in range(len(base)): + if strong_gens_distr[i] == [identity]: + break + base = base[:i] + base_len = i + for j in range(num_orbits): + if base[base_len - 1] in orbits[j]: + break + rel_orbits = orbits[: j + 1] + num_rel_orbits = len(rel_orbits) + transversals = [None]*num_rel_orbits + for j in range(num_rel_orbits): + rep = orbit_reps[j] + transversals[j] = dict( + other.orbit_transversal(rep, pairs=True)) + trivial_test = lambda x: True + tests = [None]*base_len + for l in range(base_len): + if base[l] in orbit_reps: + tests[l] = trivial_test + else: + def test(computed_words, l=l): + g = computed_words[l] + rep_orb_index = orbit_descr[base[l]] + rep = orbit_reps[rep_orb_index] + im = g._array_form[base[l]] + im_rep = g._array_form[rep] + tr_el = transversals[rep_orb_index][base[l]] + # using the definition of transversal, + # base[l]^g = rep^(tr_el*g); + # if g belongs to the centralizer, then + # base[l]^g = (rep^g)^tr_el + return im == tr_el._array_form[im_rep] + tests[l] = test + + def prop(g): + return [rmul(g, gen) for gen in other.generators] == \ + [rmul(gen, g) for gen in other.generators] + return self.subgroup_search(prop, base=base, + strong_gens=strong_gens, tests=tests) + elif hasattr(other, '__getitem__'): + gens = list(other) + return self.centralizer(PermutationGroup(gens)) + elif hasattr(other, 'array_form'): + return self.centralizer(PermutationGroup([other])) + + def commutator(self, G, H): + """ + Return the commutator of two subgroups. + + Explanation + =========== + + For a permutation group ``K`` and subgroups ``G``, ``H``, the + commutator of ``G`` and ``H`` is defined as the group generated + by all the commutators `[g, h] = hgh^{-1}g^{-1}` for ``g`` in ``G`` and + ``h`` in ``H``. It is naturally a subgroup of ``K`` ([1], p.27). + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import (SymmetricGroup, + ... AlternatingGroup) + >>> S = SymmetricGroup(5) + >>> A = AlternatingGroup(5) + >>> G = S.commutator(S, A) + >>> G.is_subgroup(A) + True + + See Also + ======== + + derived_subgroup + + Notes + ===== + + The commutator of two subgroups `H, G` is equal to the normal closure + of the commutators of all the generators, i.e. `hgh^{-1}g^{-1}` for `h` + a generator of `H` and `g` a generator of `G` ([1], p.28) + + """ + ggens = G.generators + hgens = H.generators + commutators = [] + for ggen in ggens: + for hgen in hgens: + commutator = rmul(hgen, ggen, ~hgen, ~ggen) + if commutator not in commutators: + commutators.append(commutator) + res = self.normal_closure(commutators) + return res + + def coset_factor(self, g, factor_index=False): + """Return ``G``'s (self's) coset factorization of ``g`` + + Explanation + =========== + + If ``g`` is an element of ``G`` then it can be written as the product + of permutations drawn from the Schreier-Sims coset decomposition, + + The permutations returned in ``f`` are those for which + the product gives ``g``: ``g = f[n]*...f[1]*f[0]`` where ``n = len(B)`` + and ``B = G.base``. f[i] is one of the permutations in + ``self._basic_orbits[i]``. + + If factor_index==True, + returns a tuple ``[b[0],..,b[n]]``, where ``b[i]`` + belongs to ``self._basic_orbits[i]`` + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation(0, 1, 3, 7, 6, 4)(2, 5) + >>> b = Permutation(0, 1, 3, 2)(4, 5, 7, 6) + >>> G = PermutationGroup([a, b]) + + Define g: + + >>> g = Permutation(7)(1, 2, 4)(3, 6, 5) + + Confirm that it is an element of G: + + >>> G.contains(g) + True + + Thus, it can be written as a product of factors (up to + 3) drawn from u. See below that a factor from u1 and u2 + and the Identity permutation have been used: + + >>> f = G.coset_factor(g) + >>> f[2]*f[1]*f[0] == g + True + >>> f1 = G.coset_factor(g, True); f1 + [0, 4, 4] + >>> tr = G.basic_transversals + >>> f[0] == tr[0][f1[0]] + True + + If g is not an element of G then [] is returned: + + >>> c = Permutation(5, 6, 7) + >>> G.coset_factor(c) + [] + + See Also + ======== + + sympy.combinatorics.util._strip + + """ + if isinstance(g, (Cycle, Permutation)): + g = g.list() + if len(g) != self._degree: + # this could either adjust the size or return [] immediately + # but we don't choose between the two and just signal a possible + # error + raise ValueError('g should be the same size as permutations of G') + I = list(range(self._degree)) + basic_orbits = self.basic_orbits + transversals = self._transversals + factors = [] + base = self.base + h = g + for i in range(len(base)): + beta = h[base[i]] + if beta == base[i]: + factors.append(beta) + continue + if beta not in basic_orbits[i]: + return [] + u = transversals[i][beta]._array_form + h = _af_rmul(_af_invert(u), h) + factors.append(beta) + if h != I: + return [] + if factor_index: + return factors + tr = self.basic_transversals + factors = [tr[i][factors[i]] for i in range(len(base))] + return factors + + def generator_product(self, g, original=False): + r''' + Return a list of strong generators `[s1, \dots, sn]` + s.t `g = sn \times \dots \times s1`. If ``original=True``, make the + list contain only the original group generators + + ''' + product = [] + if g.is_identity: + return [] + if g in self.strong_gens: + if not original or g in self.generators: + return [g] + else: + slp = self._strong_gens_slp[g] + for s in slp: + product.extend(self.generator_product(s, original=True)) + return product + elif g**-1 in self.strong_gens: + g = g**-1 + if not original or g in self.generators: + return [g**-1] + else: + slp = self._strong_gens_slp[g] + for s in slp: + product.extend(self.generator_product(s, original=True)) + l = len(product) + product = [product[l-i-1]**-1 for i in range(l)] + return product + + f = self.coset_factor(g, True) + for i, j in enumerate(f): + slp = self._transversal_slp[i][j] + for s in slp: + if not original: + product.append(self.strong_gens[s]) + else: + s = self.strong_gens[s] + product.extend(self.generator_product(s, original=True)) + return product + + def coset_rank(self, g): + """rank using Schreier-Sims representation. + + Explanation + =========== + + The coset rank of ``g`` is the ordering number in which + it appears in the lexicographic listing according to the + coset decomposition + + The ordering is the same as in G.generate(method='coset'). + If ``g`` does not belong to the group it returns None. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation(0, 1, 3, 7, 6, 4)(2, 5) + >>> b = Permutation(0, 1, 3, 2)(4, 5, 7, 6) + >>> G = PermutationGroup([a, b]) + >>> c = Permutation(7)(2, 4)(3, 5) + >>> G.coset_rank(c) + 16 + >>> G.coset_unrank(16) + (7)(2 4)(3 5) + + See Also + ======== + + coset_factor + + """ + factors = self.coset_factor(g, True) + if not factors: + return None + rank = 0 + b = 1 + transversals = self._transversals + base = self._base + basic_orbits = self._basic_orbits + for i in range(len(base)): + k = factors[i] + j = basic_orbits[i].index(k) + rank += b*j + b = b*len(transversals[i]) + return rank + + def coset_unrank(self, rank, af=False): + """unrank using Schreier-Sims representation + + coset_unrank is the inverse operation of coset_rank + if 0 <= rank < order; otherwise it returns None. + + """ + if rank < 0 or rank >= self.order(): + return None + base = self.base + transversals = self.basic_transversals + basic_orbits = self.basic_orbits + m = len(base) + v = [0]*m + for i in range(m): + rank, c = divmod(rank, len(transversals[i])) + v[i] = basic_orbits[i][c] + a = [transversals[i][v[i]]._array_form for i in range(m)] + h = _af_rmuln(*a) + if af: + return h + else: + return _af_new(h) + + @property + def degree(self): + """Returns the size of the permutations in the group. + + Explanation + =========== + + The number of permutations comprising the group is given by + ``len(group)``; the number of permutations that can be generated + by the group is given by ``group.order()``. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([1, 0, 2]) + >>> G = PermutationGroup([a]) + >>> G.degree + 3 + >>> len(G) + 1 + >>> G.order() + 2 + >>> list(G.generate()) + [(2), (2)(0 1)] + + See Also + ======== + + order + """ + return self._degree + + @property + def identity(self): + ''' + Return the identity element of the permutation group. + + ''' + return _af_new(list(range(self.degree))) + + @property + def elements(self): + """Returns all the elements of the permutation group as a list + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> p = PermutationGroup(Permutation(1, 3), Permutation(1, 2)) + >>> p.elements + [(3), (3)(1 2), (1 3), (2 3), (1 2 3), (1 3 2)] + + """ + if not self._elements: + self._elements = list(self.generate()) + + return self._elements + + def derived_series(self): + r"""Return the derived series for the group. + + Explanation + =========== + + The derived series for a group `G` is defined as + `G = G_0 > G_1 > G_2 > \ldots` where `G_i = [G_{i-1}, G_{i-1}]`, + i.e. `G_i` is the derived subgroup of `G_{i-1}`, for + `i\in\mathbb{N}`. When we have `G_k = G_{k-1}` for some + `k\in\mathbb{N}`, the series terminates. + + Returns + ======= + + A list of permutation groups containing the members of the derived + series in the order `G = G_0, G_1, G_2, \ldots`. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import (SymmetricGroup, + ... AlternatingGroup, DihedralGroup) + >>> A = AlternatingGroup(5) + >>> len(A.derived_series()) + 1 + >>> S = SymmetricGroup(4) + >>> len(S.derived_series()) + 4 + >>> S.derived_series()[1].is_subgroup(AlternatingGroup(4)) + True + >>> S.derived_series()[2].is_subgroup(DihedralGroup(2)) + True + + See Also + ======== + + derived_subgroup + + """ + res = [self] + current = self + nxt = self.derived_subgroup() + while not current.is_subgroup(nxt): + res.append(nxt) + current = nxt + nxt = nxt.derived_subgroup() + return res + + def derived_subgroup(self): + r"""Compute the derived subgroup. + + Explanation + =========== + + The derived subgroup, or commutator subgroup is the subgroup generated + by all commutators `[g, h] = hgh^{-1}g^{-1}` for `g, h\in G` ; it is + equal to the normal closure of the set of commutators of the generators + ([1], p.28, [11]). + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([1, 0, 2, 4, 3]) + >>> b = Permutation([0, 1, 3, 2, 4]) + >>> G = PermutationGroup([a, b]) + >>> C = G.derived_subgroup() + >>> list(C.generate(af=True)) + [[0, 1, 2, 3, 4], [0, 1, 3, 4, 2], [0, 1, 4, 2, 3]] + + See Also + ======== + + derived_series + + """ + r = self._r + gens = [p._array_form for p in self.generators] + set_commutators = set() + degree = self._degree + rng = list(range(degree)) + for i in range(r): + for j in range(r): + p1 = gens[i] + p2 = gens[j] + c = list(range(degree)) + for k in rng: + c[p2[p1[k]]] = p1[p2[k]] + ct = tuple(c) + if ct not in set_commutators: + set_commutators.add(ct) + cms = [_af_new(p) for p in set_commutators] + G2 = self.normal_closure(cms) + return G2 + + def generate(self, method="coset", af=False): + """Return iterator to generate the elements of the group. + + Explanation + =========== + + Iteration is done with one of these methods:: + + method='coset' using the Schreier-Sims coset representation + method='dimino' using the Dimino method + + If ``af = True`` it yields the array form of the permutations + + Examples + ======== + + >>> from sympy.combinatorics import PermutationGroup + >>> from sympy.combinatorics.polyhedron import tetrahedron + + The permutation group given in the tetrahedron object is also + true groups: + + >>> G = tetrahedron.pgroup + >>> G.is_group + True + + Also the group generated by the permutations in the tetrahedron + pgroup -- even the first two -- is a proper group: + + >>> H = PermutationGroup(G[0], G[1]) + >>> J = PermutationGroup(list(H.generate())); J + PermutationGroup([ + (0 1)(2 3), + (1 2 3), + (1 3 2), + (0 3 1), + (0 2 3), + (0 3)(1 2), + (0 1 3), + (3)(0 2 1), + (0 3 2), + (3)(0 1 2), + (0 2)(1 3)]) + >>> _.is_group + True + """ + if method == "coset": + return self.generate_schreier_sims(af) + elif method == "dimino": + return self.generate_dimino(af) + else: + raise NotImplementedError('No generation defined for %s' % method) + + def generate_dimino(self, af=False): + """Yield group elements using Dimino's algorithm. + + If ``af == True`` it yields the array form of the permutations. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([0, 2, 1, 3]) + >>> b = Permutation([0, 2, 3, 1]) + >>> g = PermutationGroup([a, b]) + >>> list(g.generate_dimino(af=True)) + [[0, 1, 2, 3], [0, 2, 1, 3], [0, 2, 3, 1], + [0, 1, 3, 2], [0, 3, 2, 1], [0, 3, 1, 2]] + + References + ========== + + .. [1] The Implementation of Various Algorithms for Permutation Groups in + the Computer Algebra System: AXIOM, N.J. Doye, M.Sc. Thesis + + """ + idn = list(range(self.degree)) + order = 0 + element_list = [idn] + set_element_list = {tuple(idn)} + if af: + yield idn + else: + yield _af_new(idn) + gens = [p._array_form for p in self.generators] + + for i in range(len(gens)): + # D elements of the subgroup G_i generated by gens[:i] + D = element_list.copy() + N = [idn] + while N: + A = N + N = [] + for a in A: + for g in gens[:i + 1]: + ag = _af_rmul(a, g) + if tuple(ag) not in set_element_list: + # produce G_i*g + for d in D: + order += 1 + ap = _af_rmul(d, ag) + if af: + yield ap + else: + p = _af_new(ap) + yield p + element_list.append(ap) + set_element_list.add(tuple(ap)) + N.append(ap) + self._order = len(element_list) + + def generate_schreier_sims(self, af=False): + """Yield group elements using the Schreier-Sims representation + in coset_rank order + + If ``af = True`` it yields the array form of the permutations + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([0, 2, 1, 3]) + >>> b = Permutation([0, 2, 3, 1]) + >>> g = PermutationGroup([a, b]) + >>> list(g.generate_schreier_sims(af=True)) + [[0, 1, 2, 3], [0, 2, 1, 3], [0, 3, 2, 1], + [0, 1, 3, 2], [0, 2, 3, 1], [0, 3, 1, 2]] + """ + + n = self._degree + u = self.basic_transversals + basic_orbits = self._basic_orbits + if len(u) == 0: + for x in self.generators: + if af: + yield x._array_form + else: + yield x + return + if len(u) == 1: + for i in basic_orbits[0]: + if af: + yield u[0][i]._array_form + else: + yield u[0][i] + return + + u = list(reversed(u)) + basic_orbits = basic_orbits[::-1] + # stg stack of group elements + stg = [list(range(n))] + posmax = [len(x) for x in u] + n1 = len(posmax) - 1 + pos = [0]*n1 + h = 0 + while 1: + # backtrack when finished iterating over coset + if pos[h] >= posmax[h]: + if h == 0: + return + pos[h] = 0 + h -= 1 + stg.pop() + continue + p = _af_rmul(u[h][basic_orbits[h][pos[h]]]._array_form, stg[-1]) + pos[h] += 1 + stg.append(p) + h += 1 + if h == n1: + if af: + for i in basic_orbits[-1]: + p = _af_rmul(u[-1][i]._array_form, stg[-1]) + yield p + else: + for i in basic_orbits[-1]: + p = _af_rmul(u[-1][i]._array_form, stg[-1]) + p1 = _af_new(p) + yield p1 + stg.pop() + h -= 1 + + @property + def generators(self): + """Returns the generators of the group. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([0, 2, 1]) + >>> b = Permutation([1, 0, 2]) + >>> G = PermutationGroup([a, b]) + >>> G.generators + [(1 2), (2)(0 1)] + + """ + return self._generators + + def contains(self, g, strict=True): + """Test if permutation ``g`` belong to self, ``G``. + + Explanation + =========== + + If ``g`` is an element of ``G`` it can be written as a product + of factors drawn from the cosets of ``G``'s stabilizers. To see + if ``g`` is one of the actual generators defining the group use + ``G.has(g)``. + + If ``strict`` is not ``True``, ``g`` will be resized, if necessary, + to match the size of permutations in ``self``. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + + >>> a = Permutation(1, 2) + >>> b = Permutation(2, 3, 1) + >>> G = PermutationGroup(a, b, degree=5) + >>> G.contains(G[0]) # trivial check + True + >>> elem = Permutation([[2, 3]], size=5) + >>> G.contains(elem) + True + >>> G.contains(Permutation(4)(0, 1, 2, 3)) + False + + If strict is False, a permutation will be resized, if + necessary: + + >>> H = PermutationGroup(Permutation(5)) + >>> H.contains(Permutation(3)) + False + >>> H.contains(Permutation(3), strict=False) + True + + To test if a given permutation is present in the group: + + >>> elem in G.generators + False + >>> G.has(elem) + False + + See Also + ======== + + coset_factor, sympy.core.basic.Basic.has, __contains__ + + """ + if not isinstance(g, Permutation): + return False + if g.size != self.degree: + if strict: + return False + g = Permutation(g, size=self.degree) + if g in self.generators: + return True + return bool(self.coset_factor(g.array_form, True)) + + @property + def is_perfect(self): + """Return ``True`` if the group is perfect. + A group is perfect if it equals to its derived subgroup. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation(1,2,3)(4,5) + >>> b = Permutation(1,2,3,4,5) + >>> G = PermutationGroup([a, b]) + >>> G.is_perfect + False + + """ + if self._is_perfect is None: + self._is_perfect = self.equals(self.derived_subgroup()) + return self._is_perfect + + @property + def is_abelian(self): + """Test if the group is Abelian. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([0, 2, 1]) + >>> b = Permutation([1, 0, 2]) + >>> G = PermutationGroup([a, b]) + >>> G.is_abelian + False + >>> a = Permutation([0, 2, 1]) + >>> G = PermutationGroup([a]) + >>> G.is_abelian + True + + """ + if self._is_abelian is not None: + return self._is_abelian + + self._is_abelian = True + gens = [p._array_form for p in self.generators] + for x in gens: + for y in gens: + if y <= x: + continue + if not _af_commutes_with(x, y): + self._is_abelian = False + return False + return True + + def abelian_invariants(self): + """ + Returns the abelian invariants for the given group. + Let ``G`` be a nontrivial finite abelian group. Then G is isomorphic to + the direct product of finitely many nontrivial cyclic groups of + prime-power order. + + Explanation + =========== + + The prime-powers that occur as the orders of the factors are uniquely + determined by G. More precisely, the primes that occur in the orders of the + factors in any such decomposition of ``G`` are exactly the primes that divide + ``|G|`` and for any such prime ``p``, if the orders of the factors that are + p-groups in one such decomposition of ``G`` are ``p^{t_1} >= p^{t_2} >= ... p^{t_r}``, + then the orders of the factors that are p-groups in any such decomposition of ``G`` + are ``p^{t_1} >= p^{t_2} >= ... p^{t_r}``. + + The uniquely determined integers ``p^{t_1} >= p^{t_2} >= ... p^{t_r}``, taken + for all primes that divide ``|G|`` are called the invariants of the nontrivial + group ``G`` as suggested in ([14], p. 542). + + Notes + ===== + + We adopt the convention that the invariants of a trivial group are []. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([0, 2, 1]) + >>> b = Permutation([1, 0, 2]) + >>> G = PermutationGroup([a, b]) + >>> G.abelian_invariants() + [2] + >>> from sympy.combinatorics import CyclicGroup + >>> G = CyclicGroup(7) + >>> G.abelian_invariants() + [7] + + """ + if self.is_trivial: + return [] + gns = self.generators + inv = [] + G = self + H = G.derived_subgroup() + Hgens = H.generators + for p in primefactors(G.order()): + ranks = [] + while True: + pows = [] + for g in gns: + elm = g**p + if not H.contains(elm): + pows.append(elm) + K = PermutationGroup(Hgens + pows) if pows else H + r = G.order()//K.order() + G = K + gns = pows + if r == 1: + break + ranks.append(multiplicity(p, r)) + + if ranks: + pows = [1]*ranks[0] + for i in ranks: + for j in range(i): + pows[j] = pows[j]*p + inv.extend(pows) + inv.sort() + return inv + + def is_elementary(self, p): + """Return ``True`` if the group is elementary abelian. An elementary + abelian group is a finite abelian group, where every nontrivial + element has order `p`, where `p` is a prime. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([0, 2, 1]) + >>> G = PermutationGroup([a]) + >>> G.is_elementary(2) + True + >>> a = Permutation([0, 2, 1, 3]) + >>> b = Permutation([3, 1, 2, 0]) + >>> G = PermutationGroup([a, b]) + >>> G.is_elementary(2) + True + >>> G.is_elementary(3) + False + + """ + return self.is_abelian and all(g.order() == p for g in self.generators) + + def _eval_is_alt_sym_naive(self, only_sym=False, only_alt=False): + """A naive test using the group order.""" + if only_sym and only_alt: + raise ValueError( + "Both {} and {} cannot be set to True" + .format(only_sym, only_alt)) + + n = self.degree + sym_order = _factorial(n) + order = self.order() + + if order == sym_order: + self._is_sym = True + self._is_alt = False + return not only_alt + + if 2*order == sym_order: + self._is_sym = False + self._is_alt = True + return not only_sym + + return False + + def _eval_is_alt_sym_monte_carlo(self, eps=0.05, perms=None): + """A test using monte-carlo algorithm. + + Parameters + ========== + + eps : float, optional + The criterion for the incorrect ``False`` return. + + perms : list[Permutation], optional + If explicitly given, it tests over the given candidates + for testing. + + If ``None``, it randomly computes ``N_eps`` and chooses + ``N_eps`` sample of the permutation from the group. + + See Also + ======== + + _check_cycles_alt_sym + """ + if perms is None: + n = self.degree + if n < 17: + c_n = 0.34 + else: + c_n = 0.57 + d_n = (c_n*log(2))/log(n) + N_eps = int(-log(eps)/d_n) + + perms = (self.random_pr() for i in range(N_eps)) + return self._eval_is_alt_sym_monte_carlo(perms=perms) + + for perm in perms: + if _check_cycles_alt_sym(perm): + return True + return False + + def is_alt_sym(self, eps=0.05, _random_prec=None): + r"""Monte Carlo test for the symmetric/alternating group for degrees + >= 8. + + Explanation + =========== + + More specifically, it is one-sided Monte Carlo with the + answer True (i.e., G is symmetric/alternating) guaranteed to be + correct, and the answer False being incorrect with probability eps. + + For degree < 8, the order of the group is checked so the test + is deterministic. + + Notes + ===== + + The algorithm itself uses some nontrivial results from group theory and + number theory: + 1) If a transitive group ``G`` of degree ``n`` contains an element + with a cycle of length ``n/2 < p < n-2`` for ``p`` a prime, ``G`` is the + symmetric or alternating group ([1], pp. 81-82) + 2) The proportion of elements in the symmetric/alternating group having + the property described in 1) is approximately `\log(2)/\log(n)` + ([1], p.82; [2], pp. 226-227). + The helper function ``_check_cycles_alt_sym`` is used to + go over the cycles in a permutation and look for ones satisfying 1). + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> D = DihedralGroup(10) + >>> D.is_alt_sym() + False + + See Also + ======== + + _check_cycles_alt_sym + + """ + if _random_prec is not None: + N_eps = _random_prec['N_eps'] + perms= (_random_prec[i] for i in range(N_eps)) + return self._eval_is_alt_sym_monte_carlo(perms=perms) + + if self._is_sym or self._is_alt: + return True + if self._is_sym is False and self._is_alt is False: + return False + + n = self.degree + if n < 8: + return self._eval_is_alt_sym_naive() + elif self.is_transitive(): + return self._eval_is_alt_sym_monte_carlo(eps=eps) + + self._is_sym, self._is_alt = False, False + return False + + @property + def is_nilpotent(self): + """Test if the group is nilpotent. + + Explanation + =========== + + A group `G` is nilpotent if it has a central series of finite length. + Alternatively, `G` is nilpotent if its lower central series terminates + with the trivial group. Every nilpotent group is also solvable + ([1], p.29, [12]). + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import (SymmetricGroup, + ... CyclicGroup) + >>> C = CyclicGroup(6) + >>> C.is_nilpotent + True + >>> S = SymmetricGroup(5) + >>> S.is_nilpotent + False + + See Also + ======== + + lower_central_series, is_solvable + + """ + if self._is_nilpotent is None: + lcs = self.lower_central_series() + terminator = lcs[len(lcs) - 1] + gens = terminator.generators + degree = self.degree + identity = _af_new(list(range(degree))) + if all(g == identity for g in gens): + self._is_solvable = True + self._is_nilpotent = True + return True + else: + self._is_nilpotent = False + return False + else: + return self._is_nilpotent + + def is_normal(self, gr, strict=True): + """Test if ``G=self`` is a normal subgroup of ``gr``. + + Explanation + =========== + + G is normal in gr if + for each g2 in G, g1 in gr, ``g = g1*g2*g1**-1`` belongs to G + It is sufficient to check this for each g1 in gr.generators and + g2 in G.generators. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([1, 2, 0]) + >>> b = Permutation([1, 0, 2]) + >>> G = PermutationGroup([a, b]) + >>> G1 = PermutationGroup([a, Permutation([2, 0, 1])]) + >>> G1.is_normal(G) + True + + """ + if not self.is_subgroup(gr, strict=strict): + return False + d_self = self.degree + d_gr = gr.degree + if self.is_trivial and (d_self == d_gr or not strict): + return True + if self._is_abelian: + return True + new_self = self.copy() + if not strict and d_self != d_gr: + if d_self < d_gr: + new_self = PermGroup(new_self.generators + [Permutation(d_gr - 1)]) + else: + gr = PermGroup(gr.generators + [Permutation(d_self - 1)]) + gens2 = [p._array_form for p in new_self.generators] + gens1 = [p._array_form for p in gr.generators] + for g1 in gens1: + for g2 in gens2: + p = _af_rmuln(g1, g2, _af_invert(g1)) + if not new_self.coset_factor(p, True): + return False + return True + + def is_primitive(self, randomized=True): + r"""Test if a group is primitive. + + Explanation + =========== + + A permutation group ``G`` acting on a set ``S`` is called primitive if + ``S`` contains no nontrivial block under the action of ``G`` + (a block is nontrivial if its cardinality is more than ``1``). + + Notes + ===== + + The algorithm is described in [1], p.83, and uses the function + minimal_block to search for blocks of the form `\{0, k\}` for ``k`` + ranging over representatives for the orbits of `G_0`, the stabilizer of + ``0``. This algorithm has complexity `O(n^2)` where ``n`` is the degree + of the group, and will perform badly if `G_0` is small. + + There are two implementations offered: one finds `G_0` + deterministically using the function ``stabilizer``, and the other + (default) produces random elements of `G_0` using ``random_stab``, + hoping that they generate a subgroup of `G_0` with not too many more + orbits than `G_0` (this is suggested in [1], p.83). Behavior is changed + by the ``randomized`` flag. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> D = DihedralGroup(10) + >>> D.is_primitive() + False + + See Also + ======== + + minimal_block, random_stab + + """ + if self._is_primitive is not None: + return self._is_primitive + + if self.is_transitive() is False: + return False + + if randomized: + random_stab_gens = [] + v = self.schreier_vector(0) + for _ in range(len(self)): + random_stab_gens.append(self.random_stab(0, v)) + stab = PermutationGroup(random_stab_gens) + else: + stab = self.stabilizer(0) + orbits = stab.orbits() + for orb in orbits: + x = orb.pop() + if x != 0 and any(e != 0 for e in self.minimal_block([0, x])): + self._is_primitive = False + return False + self._is_primitive = True + return True + + def minimal_blocks(self, randomized=True): + ''' + For a transitive group, return the list of all minimal + block systems. If a group is intransitive, return `False`. + + Examples + ======== + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> DihedralGroup(6).minimal_blocks() + [[0, 1, 0, 1, 0, 1], [0, 1, 2, 0, 1, 2]] + >>> G = PermutationGroup(Permutation(1,2,5)) + >>> G.minimal_blocks() + False + + See Also + ======== + + minimal_block, is_transitive, is_primitive + + ''' + def _number_blocks(blocks): + # number the blocks of a block system + # in order and return the number of + # blocks and the tuple with the + # reordering + n = len(blocks) + appeared = {} + m = 0 + b = [None]*n + for i in range(n): + if blocks[i] not in appeared: + appeared[blocks[i]] = m + b[i] = m + m += 1 + else: + b[i] = appeared[blocks[i]] + return tuple(b), m + + if not self.is_transitive(): + return False + blocks = [] + num_blocks = [] + rep_blocks = [] + if randomized: + random_stab_gens = [] + v = self.schreier_vector(0) + for i in range(len(self)): + random_stab_gens.append(self.random_stab(0, v)) + stab = PermutationGroup(random_stab_gens) + else: + stab = self.stabilizer(0) + orbits = stab.orbits() + for orb in orbits: + x = orb.pop() + if x != 0: + block = self.minimal_block([0, x]) + num_block, _ = _number_blocks(block) + # a representative block (containing 0) + rep = {j for j in range(self.degree) if num_block[j] == 0} + # check if the system is minimal with + # respect to the already discovere ones + minimal = True + blocks_remove_mask = [False] * len(blocks) + for i, r in enumerate(rep_blocks): + if len(r) > len(rep) and rep.issubset(r): + # i-th block system is not minimal + blocks_remove_mask[i] = True + elif len(r) < len(rep) and r.issubset(rep): + # the system being checked is not minimal + minimal = False + break + # remove non-minimal representative blocks + blocks = [b for i, b in enumerate(blocks) if not blocks_remove_mask[i]] + num_blocks = [n for i, n in enumerate(num_blocks) if not blocks_remove_mask[i]] + rep_blocks = [r for i, r in enumerate(rep_blocks) if not blocks_remove_mask[i]] + + if minimal and num_block not in num_blocks: + blocks.append(block) + num_blocks.append(num_block) + rep_blocks.append(rep) + return blocks + + @property + def is_solvable(self): + """Test if the group is solvable. + + ``G`` is solvable if its derived series terminates with the trivial + group ([1], p.29). + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> S = SymmetricGroup(3) + >>> S.is_solvable + True + + See Also + ======== + + is_nilpotent, derived_series + + """ + if self._is_solvable is None: + if self.order() % 2 != 0: + return True + ds = self.derived_series() + terminator = ds[len(ds) - 1] + gens = terminator.generators + degree = self.degree + identity = _af_new(list(range(degree))) + if all(g == identity for g in gens): + self._is_solvable = True + return True + else: + self._is_solvable = False + return False + else: + return self._is_solvable + + def is_subgroup(self, G, strict=True): + """Return ``True`` if all elements of ``self`` belong to ``G``. + + If ``strict`` is ``False`` then if ``self``'s degree is smaller + than ``G``'s, the elements will be resized to have the same degree. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> from sympy.combinatorics import SymmetricGroup, CyclicGroup + + Testing is strict by default: the degree of each group must be the + same: + + >>> p = Permutation(0, 1, 2, 3, 4, 5) + >>> G1 = PermutationGroup([Permutation(0, 1, 2), Permutation(0, 1)]) + >>> G2 = PermutationGroup([Permutation(0, 2), Permutation(0, 1, 2)]) + >>> G3 = PermutationGroup([p, p**2]) + >>> assert G1.order() == G2.order() == G3.order() == 6 + >>> G1.is_subgroup(G2) + True + >>> G1.is_subgroup(G3) + False + >>> G3.is_subgroup(PermutationGroup(G3[1])) + False + >>> G3.is_subgroup(PermutationGroup(G3[0])) + True + + To ignore the size, set ``strict`` to ``False``: + + >>> S3 = SymmetricGroup(3) + >>> S5 = SymmetricGroup(5) + >>> S3.is_subgroup(S5, strict=False) + True + >>> C7 = CyclicGroup(7) + >>> G = S5*C7 + >>> S5.is_subgroup(G, False) + True + >>> C7.is_subgroup(G, 0) + False + + """ + if isinstance(G, SymmetricPermutationGroup): + if self.degree != G.degree: + return False + return True + if not isinstance(G, PermutationGroup): + return False + if self == G or self.generators[0]==Permutation(): + return True + if G.order() % self.order() != 0: + return False + if self.degree == G.degree or \ + (self.degree < G.degree and not strict): + gens = self.generators + else: + return False + return all(G.contains(g, strict=strict) for g in gens) + + @property + def is_polycyclic(self): + """Return ``True`` if a group is polycyclic. A group is polycyclic if + it has a subnormal series with cyclic factors. For finite groups, + this is the same as if the group is solvable. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([0, 2, 1, 3]) + >>> b = Permutation([2, 0, 1, 3]) + >>> G = PermutationGroup([a, b]) + >>> G.is_polycyclic + True + + """ + return self.is_solvable + + def is_transitive(self, strict=True): + """Test if the group is transitive. + + Explanation + =========== + + A group is transitive if it has a single orbit. + + If ``strict`` is ``False`` the group is transitive if it has + a single orbit of length different from 1. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([0, 2, 1, 3]) + >>> b = Permutation([2, 0, 1, 3]) + >>> G1 = PermutationGroup([a, b]) + >>> G1.is_transitive() + False + >>> G1.is_transitive(strict=False) + True + >>> c = Permutation([2, 3, 0, 1]) + >>> G2 = PermutationGroup([a, c]) + >>> G2.is_transitive() + True + >>> d = Permutation([1, 0, 2, 3]) + >>> e = Permutation([0, 1, 3, 2]) + >>> G3 = PermutationGroup([d, e]) + >>> G3.is_transitive() or G3.is_transitive(strict=False) + False + + """ + if self._is_transitive: # strict or not, if True then True + return self._is_transitive + if strict: + if self._is_transitive is not None: # we only store strict=True + return self._is_transitive + + ans = len(self.orbit(0)) == self.degree + self._is_transitive = ans + return ans + + got_orb = False + for x in self.orbits(): + if len(x) > 1: + if got_orb: + return False + got_orb = True + return got_orb + + @property + def is_trivial(self): + """Test if the group is the trivial group. + + This is true if the group contains only the identity permutation. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> G = PermutationGroup([Permutation([0, 1, 2])]) + >>> G.is_trivial + True + + """ + if self._is_trivial is None: + self._is_trivial = len(self) == 1 and self[0].is_Identity + return self._is_trivial + + def lower_central_series(self): + r"""Return the lower central series for the group. + + The lower central series for a group `G` is the series + `G = G_0 > G_1 > G_2 > \ldots` where + `G_k = [G, G_{k-1}]`, i.e. every term after the first is equal to the + commutator of `G` and the previous term in `G1` ([1], p.29). + + Returns + ======= + + A list of permutation groups in the order `G = G_0, G_1, G_2, \ldots` + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import (AlternatingGroup, + ... DihedralGroup) + >>> A = AlternatingGroup(4) + >>> len(A.lower_central_series()) + 2 + >>> A.lower_central_series()[1].is_subgroup(DihedralGroup(2)) + True + + See Also + ======== + + commutator, derived_series + + """ + res = [self] + current = self + nxt = self.commutator(self, current) + while not current.is_subgroup(nxt): + res.append(nxt) + current = nxt + nxt = self.commutator(self, current) + return res + + @property + def max_div(self): + """Maximum proper divisor of the degree of a permutation group. + + Explanation + =========== + + Obviously, this is the degree divided by its minimal proper divisor + (larger than ``1``, if one exists). As it is guaranteed to be prime, + the ``sieve`` from ``sympy.ntheory`` is used. + This function is also used as an optimization tool for the functions + ``minimal_block`` and ``_union_find_merge``. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> G = PermutationGroup([Permutation([0, 2, 1, 3])]) + >>> G.max_div + 2 + + See Also + ======== + + minimal_block, _union_find_merge + + """ + if self._max_div is not None: + return self._max_div + n = self.degree + if n == 1: + return 1 + for x in sieve: + if n % x == 0: + d = n//x + self._max_div = d + return d + + def minimal_block(self, points): + r"""For a transitive group, finds the block system generated by + ``points``. + + Explanation + =========== + + If a group ``G`` acts on a set ``S``, a nonempty subset ``B`` of ``S`` + is called a block under the action of ``G`` if for all ``g`` in ``G`` + we have ``gB = B`` (``g`` fixes ``B``) or ``gB`` and ``B`` have no + common points (``g`` moves ``B`` entirely). ([1], p.23; [6]). + + The distinct translates ``gB`` of a block ``B`` for ``g`` in ``G`` + partition the set ``S`` and this set of translates is known as a block + system. Moreover, we obviously have that all blocks in the partition + have the same size, hence the block size divides ``|S|`` ([1], p.23). + A ``G``-congruence is an equivalence relation ``~`` on the set ``S`` + such that ``a ~ b`` implies ``g(a) ~ g(b)`` for all ``g`` in ``G``. + For a transitive group, the equivalence classes of a ``G``-congruence + and the blocks of a block system are the same thing ([1], p.23). + + The algorithm below checks the group for transitivity, and then finds + the ``G``-congruence generated by the pairs ``(p_0, p_1), (p_0, p_2), + ..., (p_0,p_{k-1})`` which is the same as finding the maximal block + system (i.e., the one with minimum block size) such that + ``p_0, ..., p_{k-1}`` are in the same block ([1], p.83). + + It is an implementation of Atkinson's algorithm, as suggested in [1], + and manipulates an equivalence relation on the set ``S`` using a + union-find data structure. The running time is just above + `O(|points||S|)`. ([1], pp. 83-87; [7]). + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> D = DihedralGroup(10) + >>> D.minimal_block([0, 5]) + [0, 1, 2, 3, 4, 0, 1, 2, 3, 4] + >>> D.minimal_block([0, 1]) + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + See Also + ======== + + _union_find_rep, _union_find_merge, is_transitive, is_primitive + + """ + if not self.is_transitive(): + return False + n = self.degree + gens = self.generators + # initialize the list of equivalence class representatives + parents = list(range(n)) + ranks = [1]*n + not_rep = [] + k = len(points) + # the block size must divide the degree of the group + if k > self.max_div: + return [0]*n + for i in range(k - 1): + parents[points[i + 1]] = points[0] + not_rep.append(points[i + 1]) + ranks[points[0]] = k + i = 0 + len_not_rep = k - 1 + while i < len_not_rep: + gamma = not_rep[i] + i += 1 + for gen in gens: + # find has side effects: performs path compression on the list + # of representatives + delta = self._union_find_rep(gamma, parents) + # union has side effects: performs union by rank on the list + # of representatives + temp = self._union_find_merge(gen(gamma), gen(delta), ranks, + parents, not_rep) + if temp == -1: + return [0]*n + len_not_rep += temp + for i in range(n): + # force path compression to get the final state of the equivalence + # relation + self._union_find_rep(i, parents) + + # rewrite result so that block representatives are minimal + new_reps = {} + return [new_reps.setdefault(r, i) for i, r in enumerate(parents)] + + def conjugacy_class(self, x): + r"""Return the conjugacy class of an element in the group. + + Explanation + =========== + + The conjugacy class of an element ``g`` in a group ``G`` is the set of + elements ``x`` in ``G`` that are conjugate with ``g``, i.e. for which + + ``g = xax^{-1}`` + + for some ``a`` in ``G``. + + Note that conjugacy is an equivalence relation, and therefore that + conjugacy classes are partitions of ``G``. For a list of all the + conjugacy classes of the group, use the conjugacy_classes() method. + + In a permutation group, each conjugacy class corresponds to a particular + `cycle structure': for example, in ``S_3``, the conjugacy classes are: + + * the identity class, ``{()}`` + * all transpositions, ``{(1 2), (1 3), (2 3)}`` + * all 3-cycles, ``{(1 2 3), (1 3 2)}`` + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, SymmetricGroup + >>> S3 = SymmetricGroup(3) + >>> S3.conjugacy_class(Permutation(0, 1, 2)) + {(0 1 2), (0 2 1)} + + Notes + ===== + + This procedure computes the conjugacy class directly by finding the + orbit of the element under conjugation in G. This algorithm is only + feasible for permutation groups of relatively small order, but is like + the orbit() function itself in that respect. + """ + # Ref: "Computing the conjugacy classes of finite groups"; Butler, G. + # Groups '93 Galway/St Andrews; edited by Campbell, C. M. + new_class = {x} + last_iteration = new_class + + while len(last_iteration) > 0: + this_iteration = set() + + for y in last_iteration: + for s in self.generators: + conjugated = s * y * (~s) + if conjugated not in new_class: + this_iteration.add(conjugated) + + new_class.update(last_iteration) + last_iteration = this_iteration + + return new_class + + + def conjugacy_classes(self): + r"""Return the conjugacy classes of the group. + + Explanation + =========== + + As described in the documentation for the .conjugacy_class() function, + conjugacy is an equivalence relation on a group G which partitions the + set of elements. This method returns a list of all these conjugacy + classes of G. + + Examples + ======== + + >>> from sympy.combinatorics import SymmetricGroup + >>> SymmetricGroup(3).conjugacy_classes() + [{(2)}, {(0 1 2), (0 2 1)}, {(0 2), (1 2), (2)(0 1)}] + + """ + identity = _af_new(list(range(self.degree))) + known_elements = {identity} + classes = [known_elements.copy()] + + for x in self.generate(): + if x not in known_elements: + new_class = self.conjugacy_class(x) + classes.append(new_class) + known_elements.update(new_class) + + return classes + + def normal_closure(self, other, k=10): + r"""Return the normal closure of a subgroup/set of permutations. + + Explanation + =========== + + If ``S`` is a subset of a group ``G``, the normal closure of ``A`` in ``G`` + is defined as the intersection of all normal subgroups of ``G`` that + contain ``A`` ([1], p.14). Alternatively, it is the group generated by + the conjugates ``x^{-1}yx`` for ``x`` a generator of ``G`` and ``y`` a + generator of the subgroup ``\left\langle S\right\rangle`` generated by + ``S`` (for some chosen generating set for ``\left\langle S\right\rangle``) + ([1], p.73). + + Parameters + ========== + + other + a subgroup/list of permutations/single permutation + k + an implementation-specific parameter that determines the number + of conjugates that are adjoined to ``other`` at once + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import (SymmetricGroup, + ... CyclicGroup, AlternatingGroup) + >>> S = SymmetricGroup(5) + >>> C = CyclicGroup(5) + >>> G = S.normal_closure(C) + >>> G.order() + 60 + >>> G.is_subgroup(AlternatingGroup(5)) + True + + See Also + ======== + + commutator, derived_subgroup, random_pr + + Notes + ===== + + The algorithm is described in [1], pp. 73-74; it makes use of the + generation of random elements for permutation groups by the product + replacement algorithm. + + """ + if hasattr(other, 'generators'): + degree = self.degree + identity = _af_new(list(range(degree))) + + if all(g == identity for g in other.generators): + return other + Z = PermutationGroup(other.generators[:]) + base, strong_gens = Z.schreier_sims_incremental() + strong_gens_distr = _distribute_gens_by_base(base, strong_gens) + basic_orbits, basic_transversals = \ + _orbits_transversals_from_bsgs(base, strong_gens_distr) + + self._random_pr_init(r=10, n=20) + + _loop = True + while _loop: + Z._random_pr_init(r=10, n=10) + for _ in range(k): + g = self.random_pr() + h = Z.random_pr() + conj = h^g + res = _strip(conj, base, basic_orbits, basic_transversals) + if res[0] != identity or res[1] != len(base) + 1: + gens = Z.generators + gens.append(conj) + Z = PermutationGroup(gens) + strong_gens.append(conj) + temp_base, temp_strong_gens = \ + Z.schreier_sims_incremental(base, strong_gens) + base, strong_gens = temp_base, temp_strong_gens + strong_gens_distr = \ + _distribute_gens_by_base(base, strong_gens) + basic_orbits, basic_transversals = \ + _orbits_transversals_from_bsgs(base, + strong_gens_distr) + _loop = False + for g in self.generators: + for h in Z.generators: + conj = h^g + res = _strip(conj, base, basic_orbits, + basic_transversals) + if res[0] != identity or res[1] != len(base) + 1: + _loop = True + break + if _loop: + break + return Z + elif hasattr(other, '__getitem__'): + return self.normal_closure(PermutationGroup(other)) + elif hasattr(other, 'array_form'): + return self.normal_closure(PermutationGroup([other])) + + def orbit(self, alpha, action='tuples'): + r"""Compute the orbit of alpha `\{g(\alpha) | g \in G\}` as a set. + + Explanation + =========== + + The time complexity of the algorithm used here is `O(|Orb|*r)` where + `|Orb|` is the size of the orbit and ``r`` is the number of generators of + the group. For a more detailed analysis, see [1], p.78, [2], pp. 19-21. + Here alpha can be a single point, or a list of points. + + If alpha is a single point, the ordinary orbit is computed. + if alpha is a list of points, there are three available options: + + 'union' - computes the union of the orbits of the points in the list + 'tuples' - computes the orbit of the list interpreted as an ordered + tuple under the group action ( i.e., g((1,2,3)) = (g(1), g(2), g(3)) ) + 'sets' - computes the orbit of the list interpreted as a sets + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([1, 2, 0, 4, 5, 6, 3]) + >>> G = PermutationGroup([a]) + >>> G.orbit(0) + {0, 1, 2} + >>> G.orbit([0, 4], 'union') + {0, 1, 2, 3, 4, 5, 6} + + See Also + ======== + + orbit_transversal + + """ + return _orbit(self.degree, self.generators, alpha, action) + + def orbit_rep(self, alpha, beta, schreier_vector=None): + """Return a group element which sends ``alpha`` to ``beta``. + + Explanation + =========== + + If ``beta`` is not in the orbit of ``alpha``, the function returns + ``False``. This implementation makes use of the schreier vector. + For a proof of correctness, see [1], p.80 + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import AlternatingGroup + >>> G = AlternatingGroup(5) + >>> G.orbit_rep(0, 4) + (0 4 1 2 3) + + See Also + ======== + + schreier_vector + + """ + if schreier_vector is None: + schreier_vector = self.schreier_vector(alpha) + if schreier_vector[beta] is None: + return False + k = schreier_vector[beta] + gens = [x._array_form for x in self.generators] + a = [] + while k != -1: + a.append(gens[k]) + beta = gens[k].index(beta) # beta = (~gens[k])(beta) + k = schreier_vector[beta] + if a: + return _af_new(_af_rmuln(*a)) + else: + return _af_new(list(range(self._degree))) + + def orbit_transversal(self, alpha, pairs=False): + r"""Computes a transversal for the orbit of ``alpha`` as a set. + + Explanation + =========== + + For a permutation group `G`, a transversal for the orbit + `Orb = \{g(\alpha) | g \in G\}` is a set + `\{g_\beta | g_\beta(\alpha) = \beta\}` for `\beta \in Orb`. + Note that there may be more than one possible transversal. + If ``pairs`` is set to ``True``, it returns the list of pairs + `(\beta, g_\beta)`. For a proof of correctness, see [1], p.79 + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> G = DihedralGroup(6) + >>> G.orbit_transversal(0) + [(5), (0 1 2 3 4 5), (0 5)(1 4)(2 3), (0 2 4)(1 3 5), (5)(0 4)(1 3), (0 3)(1 4)(2 5)] + + See Also + ======== + + orbit + + """ + return _orbit_transversal(self._degree, self.generators, alpha, pairs) + + def orbits(self, rep=False): + """Return the orbits of ``self``, ordered according to lowest element + in each orbit. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation(1, 5)(2, 3)(4, 0, 6) + >>> b = Permutation(1, 5)(3, 4)(2, 6, 0) + >>> G = PermutationGroup([a, b]) + >>> G.orbits() + [{0, 2, 3, 4, 6}, {1, 5}] + """ + return _orbits(self._degree, self._generators) + + def order(self): + """Return the order of the group: the number of permutations that + can be generated from elements of the group. + + The number of permutations comprising the group is given by + ``len(group)``; the length of each permutation in the group is + given by ``group.size``. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + + >>> a = Permutation([1, 0, 2]) + >>> G = PermutationGroup([a]) + >>> G.degree + 3 + >>> len(G) + 1 + >>> G.order() + 2 + >>> list(G.generate()) + [(2), (2)(0 1)] + + >>> a = Permutation([0, 2, 1]) + >>> b = Permutation([1, 0, 2]) + >>> G = PermutationGroup([a, b]) + >>> G.order() + 6 + + See Also + ======== + + degree + + """ + if self._order is not None: + return self._order + if self._is_sym: + n = self._degree + self._order = factorial(n) + return self._order + if self._is_alt: + n = self._degree + self._order = factorial(n)/2 + return self._order + + m = prod([len(x) for x in self.basic_transversals]) + self._order = m + return m + + def index(self, H): + """ + Returns the index of a permutation group. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation(1,2,3) + >>> b =Permutation(3) + >>> G = PermutationGroup([a]) + >>> H = PermutationGroup([b]) + >>> G.index(H) + 3 + + """ + if H.is_subgroup(self): + return self.order()//H.order() + + @property + def is_symmetric(self): + """Return ``True`` if the group is symmetric. + + Examples + ======== + + >>> from sympy.combinatorics import SymmetricGroup + >>> g = SymmetricGroup(5) + >>> g.is_symmetric + True + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> g = PermutationGroup( + ... Permutation(0, 1, 2, 3, 4), + ... Permutation(2, 3)) + >>> g.is_symmetric + True + + Notes + ===== + + This uses a naive test involving the computation of the full + group order. + If you need more quicker taxonomy for large groups, you can use + :meth:`PermutationGroup.is_alt_sym`. + However, :meth:`PermutationGroup.is_alt_sym` may not be accurate + and is not able to distinguish between an alternating group and + a symmetric group. + + See Also + ======== + + is_alt_sym + """ + _is_sym = self._is_sym + if _is_sym is not None: + return _is_sym + + n = self.degree + if n >= 8: + if self.is_transitive(): + _is_alt_sym = self._eval_is_alt_sym_monte_carlo() + if _is_alt_sym: + if any(g.is_odd for g in self.generators): + self._is_sym, self._is_alt = True, False + return True + + self._is_sym, self._is_alt = False, True + return False + + return self._eval_is_alt_sym_naive(only_sym=True) + + self._is_sym, self._is_alt = False, False + return False + + return self._eval_is_alt_sym_naive(only_sym=True) + + + @property + def is_alternating(self): + """Return ``True`` if the group is alternating. + + Examples + ======== + + >>> from sympy.combinatorics import AlternatingGroup + >>> g = AlternatingGroup(5) + >>> g.is_alternating + True + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> g = PermutationGroup( + ... Permutation(0, 1, 2, 3, 4), + ... Permutation(2, 3, 4)) + >>> g.is_alternating + True + + Notes + ===== + + This uses a naive test involving the computation of the full + group order. + If you need more quicker taxonomy for large groups, you can use + :meth:`PermutationGroup.is_alt_sym`. + However, :meth:`PermutationGroup.is_alt_sym` may not be accurate + and is not able to distinguish between an alternating group and + a symmetric group. + + See Also + ======== + + is_alt_sym + """ + _is_alt = self._is_alt + if _is_alt is not None: + return _is_alt + + n = self.degree + if n >= 8: + if self.is_transitive(): + _is_alt_sym = self._eval_is_alt_sym_monte_carlo() + if _is_alt_sym: + if all(g.is_even for g in self.generators): + self._is_sym, self._is_alt = False, True + return True + + self._is_sym, self._is_alt = True, False + return False + + return self._eval_is_alt_sym_naive(only_alt=True) + + self._is_sym, self._is_alt = False, False + return False + + return self._eval_is_alt_sym_naive(only_alt=True) + + @classmethod + def _distinct_primes_lemma(cls, primes): + """Subroutine to test if there is only one cyclic group for the + order.""" + primes = sorted(primes) + l = len(primes) + for i in range(l): + for j in range(i+1, l): + if primes[j] % primes[i] == 1: + return None + return True + + @property + def is_cyclic(self): + r""" + Return ``True`` if the group is Cyclic. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import AbelianGroup + >>> G = AbelianGroup(3, 4) + >>> G.is_cyclic + True + >>> G = AbelianGroup(4, 4) + >>> G.is_cyclic + False + + Notes + ===== + + If the order of a group $n$ can be factored into the distinct + primes $p_1, p_2, \dots , p_s$ and if + + .. math:: + \forall i, j \in \{1, 2, \dots, s \}: + p_i \not \equiv 1 \pmod {p_j} + + holds true, there is only one group of the order $n$ which + is a cyclic group [1]_. This is a generalization of the lemma + that the group of order $15, 35, \dots$ are cyclic. + + And also, these additional lemmas can be used to test if a + group is cyclic if the order of the group is already found. + + - If the group is abelian and the order of the group is + square-free, the group is cyclic. + - If the order of the group is less than $6$ and is not $4$, the + group is cyclic. + - If the order of the group is prime, the group is cyclic. + + References + ========== + + .. [1] 1978: John S. Rose: A Course on Group Theory, + Introduction to Finite Group Theory: 1.4 + """ + if self._is_cyclic is not None: + return self._is_cyclic + + if len(self.generators) == 1: + self._is_cyclic = True + self._is_abelian = True + return True + + if self._is_abelian is False: + self._is_cyclic = False + return False + + order = self.order() + + if order < 6: + self._is_abelian = True + if order != 4: + self._is_cyclic = True + return True + + factors = factorint(order) + if all(v == 1 for v in factors.values()): + if self._is_abelian: + self._is_cyclic = True + return True + + primes = list(factors.keys()) + if PermutationGroup._distinct_primes_lemma(primes) is True: + self._is_cyclic = True + self._is_abelian = True + return True + + if not self.is_abelian: + self._is_cyclic = False + return False + + self._is_cyclic = all( + any(g**(order//p) != self.identity for g in self.generators) + for p, e in factors.items() if e > 1 + ) + return self._is_cyclic + + @property + def is_dihedral(self): + r""" + Return ``True`` if the group is dihedral. + + Examples + ======== + + >>> from sympy.combinatorics.perm_groups import PermutationGroup + >>> from sympy.combinatorics.permutations import Permutation + >>> from sympy.combinatorics.named_groups import SymmetricGroup, CyclicGroup + >>> G = PermutationGroup(Permutation(1, 6)(2, 5)(3, 4), Permutation(0, 1, 2, 3, 4, 5, 6)) + >>> G.is_dihedral + True + >>> G = SymmetricGroup(3) + >>> G.is_dihedral + True + >>> G = CyclicGroup(6) + >>> G.is_dihedral + False + + References + ========== + + .. [Di1] https://math.stackexchange.com/questions/827230/given-a-cayley-table-is-there-an-algorithm-to-determine-if-it-is-a-dihedral-gro/827273#827273 + .. [Di2] https://kconrad.math.uconn.edu/blurbs/grouptheory/dihedral.pdf + .. [Di3] https://kconrad.math.uconn.edu/blurbs/grouptheory/dihedral2.pdf + .. [Di4] https://en.wikipedia.org/wiki/Dihedral_group + """ + if self._is_dihedral is not None: + return self._is_dihedral + + order = self.order() + + if order % 2 == 1: + self._is_dihedral = False + return False + if order == 2: + self._is_dihedral = True + return True + if order == 4: + # The dihedral group of order 4 is the Klein 4-group. + self._is_dihedral = not self.is_cyclic + return self._is_dihedral + if self.is_abelian: + # The only abelian dihedral groups are the ones of orders 2 and 4. + self._is_dihedral = False + return False + + # Now we know the group is of even order >= 6, and nonabelian. + n = order // 2 + + # Handle special cases where there are exactly two generators. + gens = self.generators + if len(gens) == 2: + x, y = gens + a, b = x.order(), y.order() + # Make a >= b + if a < b: + x, y, a, b = y, x, b, a + # Using Theorem 2.1 of [Di3]: + if a == 2 == b: + self._is_dihedral = True + return True + # Using Theorem 1.1 of [Di3]: + if a == n and b == 2 and y*x*y == ~x: + self._is_dihedral = True + return True + + # Proceed with algorithm of [Di1] + # Find elements of orders 2 and n + order_2, order_n = [], [] + for p in self.elements: + k = p.order() + if k == 2: + order_2.append(p) + elif k == n: + order_n.append(p) + + if len(order_2) != n + 1 - (n % 2): + self._is_dihedral = False + return False + + if not order_n: + self._is_dihedral = False + return False + + x = order_n[0] + # Want an element y of order 2 that is not a power of x + # (i.e. that is not the 180-deg rotation, when n is even). + y = order_2[0] + if n % 2 == 0 and y == x**(n//2): + y = order_2[1] + + self._is_dihedral = (y*x*y == ~x) + return self._is_dihedral + + def pointwise_stabilizer(self, points, incremental=True): + r"""Return the pointwise stabilizer for a set of points. + + Explanation + =========== + + For a permutation group `G` and a set of points + `\{p_1, p_2,\ldots, p_k\}`, the pointwise stabilizer of + `p_1, p_2, \ldots, p_k` is defined as + `G_{p_1,\ldots, p_k} = + \{g\in G | g(p_i) = p_i \forall i\in\{1, 2,\ldots,k\}\}` ([1],p20). + It is a subgroup of `G`. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> S = SymmetricGroup(7) + >>> Stab = S.pointwise_stabilizer([2, 3, 5]) + >>> Stab.is_subgroup(S.stabilizer(2).stabilizer(3).stabilizer(5)) + True + + See Also + ======== + + stabilizer, schreier_sims_incremental + + Notes + ===== + + When incremental == True, + rather than the obvious implementation using successive calls to + ``.stabilizer()``, this uses the incremental Schreier-Sims algorithm + to obtain a base with starting segment - the given points. + + """ + if incremental: + base, strong_gens = self.schreier_sims_incremental(base=points) + stab_gens = [] + degree = self.degree + for gen in strong_gens: + if [gen(point) for point in points] == points: + stab_gens.append(gen) + if not stab_gens: + stab_gens = _af_new(list(range(degree))) + return PermutationGroup(stab_gens) + else: + gens = self._generators + degree = self.degree + for x in points: + gens = _stabilizer(degree, gens, x) + return PermutationGroup(gens) + + def make_perm(self, n, seed=None): + """ + Multiply ``n`` randomly selected permutations from + pgroup together, starting with the identity + permutation. If ``n`` is a list of integers, those + integers will be used to select the permutations and they + will be applied in L to R order: make_perm((A, B, C)) will + give CBA(I) where I is the identity permutation. + + ``seed`` is used to set the seed for the random selection + of permutations from pgroup. If this is a list of integers, + the corresponding permutations from pgroup will be selected + in the order give. This is mainly used for testing purposes. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a, b = [Permutation([1, 0, 3, 2]), Permutation([1, 3, 0, 2])] + >>> G = PermutationGroup([a, b]) + >>> G.make_perm(1, [0]) + (0 1)(2 3) + >>> G.make_perm(3, [0, 1, 0]) + (0 2 3 1) + >>> G.make_perm([0, 1, 0]) + (0 2 3 1) + + See Also + ======== + + random + """ + if is_sequence(n): + if seed is not None: + raise ValueError('If n is a sequence, seed should be None') + n, seed = len(n), n + else: + try: + n = int(n) + except TypeError: + raise ValueError('n must be an integer or a sequence.') + randomrange = _randrange(seed) + + # start with the identity permutation + result = Permutation(list(range(self.degree))) + m = len(self) + for _ in range(n): + p = self[randomrange(m)] + result = rmul(result, p) + return result + + def random(self, af=False): + """Return a random group element + """ + rank = randrange(self.order()) + return self.coset_unrank(rank, af) + + def random_pr(self, gen_count=11, iterations=50, _random_prec=None): + """Return a random group element using product replacement. + + Explanation + =========== + + For the details of the product replacement algorithm, see + ``_random_pr_init`` In ``random_pr`` the actual 'product replacement' + is performed. Notice that if the attribute ``_random_gens`` + is empty, it needs to be initialized by ``_random_pr_init``. + + See Also + ======== + + _random_pr_init + + """ + if self._random_gens == []: + self._random_pr_init(gen_count, iterations) + random_gens = self._random_gens + r = len(random_gens) - 1 + + # handle randomized input for testing purposes + if _random_prec is None: + s = randrange(r) + t = randrange(r - 1) + if t == s: + t = r - 1 + x = choice([1, 2]) + e = choice([-1, 1]) + else: + s = _random_prec['s'] + t = _random_prec['t'] + if t == s: + t = r - 1 + x = _random_prec['x'] + e = _random_prec['e'] + + if x == 1: + random_gens[s] = _af_rmul(random_gens[s], _af_pow(random_gens[t], e)) + random_gens[r] = _af_rmul(random_gens[r], random_gens[s]) + else: + random_gens[s] = _af_rmul(_af_pow(random_gens[t], e), random_gens[s]) + random_gens[r] = _af_rmul(random_gens[s], random_gens[r]) + return _af_new(random_gens[r]) + + def random_stab(self, alpha, schreier_vector=None, _random_prec=None): + """Random element from the stabilizer of ``alpha``. + + The schreier vector for ``alpha`` is an optional argument used + for speeding up repeated calls. The algorithm is described in [1], p.81 + + See Also + ======== + + random_pr, orbit_rep + + """ + if schreier_vector is None: + schreier_vector = self.schreier_vector(alpha) + if _random_prec is None: + rand = self.random_pr() + else: + rand = _random_prec['rand'] + beta = rand(alpha) + h = self.orbit_rep(alpha, beta, schreier_vector) + return rmul(~h, rand) + + def schreier_sims(self): + """Schreier-Sims algorithm. + + Explanation + =========== + + It computes the generators of the chain of stabilizers + `G > G_{b_1} > .. > G_{b1,..,b_r} > 1` + in which `G_{b_1,..,b_i}` stabilizes `b_1,..,b_i`, + and the corresponding ``s`` cosets. + An element of the group can be written as the product + `h_1*..*h_s`. + + We use the incremental Schreier-Sims algorithm. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([0, 2, 1]) + >>> b = Permutation([1, 0, 2]) + >>> G = PermutationGroup([a, b]) + >>> G.schreier_sims() + >>> G.basic_transversals + [{0: (2)(0 1), 1: (2), 2: (1 2)}, + {0: (2), 2: (0 2)}] + """ + if self._transversals: + return + self._schreier_sims() + return + + def _schreier_sims(self, base=None): + schreier = self.schreier_sims_incremental(base=base, slp_dict=True) + base, strong_gens = schreier[:2] + self._base = base + self._strong_gens = strong_gens + self._strong_gens_slp = schreier[2] + if not base: + self._transversals = [] + self._basic_orbits = [] + return + + strong_gens_distr = _distribute_gens_by_base(base, strong_gens) + basic_orbits, transversals, slps = _orbits_transversals_from_bsgs(base,\ + strong_gens_distr, slp=True) + + # rewrite the indices stored in slps in terms of strong_gens + for i, slp in enumerate(slps): + gens = strong_gens_distr[i] + for k in slp: + slp[k] = [strong_gens.index(gens[s]) for s in slp[k]] + + self._transversals = transversals + self._basic_orbits = [sorted(x) for x in basic_orbits] + self._transversal_slp = slps + + def schreier_sims_incremental(self, base=None, gens=None, slp_dict=False): + """Extend a sequence of points and generating set to a base and strong + generating set. + + Parameters + ========== + + base + The sequence of points to be extended to a base. Optional + parameter with default value ``[]``. + gens + The generating set to be extended to a strong generating set + relative to the base obtained. Optional parameter with default + value ``self.generators``. + + slp_dict + If `True`, return a dictionary `{g: gens}` for each strong + generator `g` where `gens` is a list of strong generators + coming before `g` in `strong_gens`, such that the product + of the elements of `gens` is equal to `g`. + + Returns + ======= + + (base, strong_gens) + ``base`` is the base obtained, and ``strong_gens`` is the strong + generating set relative to it. The original parameters ``base``, + ``gens`` remain unchanged. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import AlternatingGroup + >>> from sympy.combinatorics.testutil import _verify_bsgs + >>> A = AlternatingGroup(7) + >>> base = [2, 3] + >>> seq = [2, 3] + >>> base, strong_gens = A.schreier_sims_incremental(base=seq) + >>> _verify_bsgs(A, base, strong_gens) + True + >>> base[:2] + [2, 3] + + Notes + ===== + + This version of the Schreier-Sims algorithm runs in polynomial time. + There are certain assumptions in the implementation - if the trivial + group is provided, ``base`` and ``gens`` are returned immediately, + as any sequence of points is a base for the trivial group. If the + identity is present in the generators ``gens``, it is removed as + it is a redundant generator. + The implementation is described in [1], pp. 90-93. + + See Also + ======== + + schreier_sims, schreier_sims_random + + """ + if base is None: + base = [] + if gens is None: + gens = self.generators[:] + degree = self.degree + id_af = list(range(degree)) + # handle the trivial group + if len(gens) == 1 and gens[0].is_Identity: + if slp_dict: + return base, gens, {gens[0]: [gens[0]]} + return base, gens + # prevent side effects + _base, _gens = base[:], gens[:] + # remove the identity as a generator + _gens = [x for x in _gens if not x.is_Identity] + # make sure no generator fixes all base points + for gen in _gens: + if all(x == gen._array_form[x] for x in _base): + for new in id_af: + if gen._array_form[new] != new: + break + else: + assert None # can this ever happen? + _base.append(new) + # distribute generators according to basic stabilizers + strong_gens_distr = _distribute_gens_by_base(_base, _gens) + strong_gens_slp = [] + # initialize the basic stabilizers, basic orbits and basic transversals + orbs = {} + transversals = {} + slps = {} + base_len = len(_base) + for i in range(base_len): + transversals[i], slps[i] = _orbit_transversal(degree, strong_gens_distr[i], + _base[i], pairs=True, af=True, slp=True) + transversals[i] = dict(transversals[i]) + orbs[i] = list(transversals[i].keys()) + # main loop: amend the stabilizer chain until we have generators + # for all stabilizers + i = base_len - 1 + while i >= 0: + # this flag is used to continue with the main loop from inside + # a nested loop + continue_i = False + # test the generators for being a strong generating set + db = {} + for beta, u_beta in list(transversals[i].items()): + for j, gen in enumerate(strong_gens_distr[i]): + gb = gen._array_form[beta] + u1 = transversals[i][gb] + g1 = _af_rmul(gen._array_form, u_beta) + slp = [(i, g) for g in slps[i][beta]] + slp = [(i, j)] + slp + if g1 != u1: + # test if the schreier generator is in the i+1-th + # would-be basic stabilizer + y = True + try: + u1_inv = db[gb] + except KeyError: + u1_inv = db[gb] = _af_invert(u1) + schreier_gen = _af_rmul(u1_inv, g1) + u1_inv_slp = slps[i][gb][:] + u1_inv_slp.reverse() + u1_inv_slp = [(i, (g,)) for g in u1_inv_slp] + slp = u1_inv_slp + slp + h, j, slp = _strip_af(schreier_gen, _base, orbs, transversals, i, slp=slp, slps=slps) + if j <= base_len: + # new strong generator h at level j + y = False + elif h: + # h fixes all base points + y = False + moved = 0 + while h[moved] == moved: + moved += 1 + _base.append(moved) + base_len += 1 + strong_gens_distr.append([]) + if y is False: + # if a new strong generator is found, update the + # data structures and start over + h = _af_new(h) + strong_gens_slp.append((h, slp)) + for l in range(i + 1, j): + strong_gens_distr[l].append(h) + transversals[l], slps[l] =\ + _orbit_transversal(degree, strong_gens_distr[l], + _base[l], pairs=True, af=True, slp=True) + transversals[l] = dict(transversals[l]) + orbs[l] = list(transversals[l].keys()) + i = j - 1 + # continue main loop using the flag + continue_i = True + if continue_i is True: + break + if continue_i is True: + break + if continue_i is True: + continue + i -= 1 + + strong_gens = _gens[:] + + if slp_dict: + # create the list of the strong generators strong_gens and + # rewrite the indices of strong_gens_slp in terms of the + # elements of strong_gens + for k, slp in strong_gens_slp: + strong_gens.append(k) + for i in range(len(slp)): + s = slp[i] + if isinstance(s[1], tuple): + slp[i] = strong_gens_distr[s[0]][s[1][0]]**-1 + else: + slp[i] = strong_gens_distr[s[0]][s[1]] + strong_gens_slp = dict(strong_gens_slp) + # add the original generators + for g in _gens: + strong_gens_slp[g] = [g] + return (_base, strong_gens, strong_gens_slp) + + strong_gens.extend([k for k, _ in strong_gens_slp]) + return _base, strong_gens + + def schreier_sims_random(self, base=None, gens=None, consec_succ=10, + _random_prec=None): + r"""Randomized Schreier-Sims algorithm. + + Explanation + =========== + + The randomized Schreier-Sims algorithm takes the sequence ``base`` + and the generating set ``gens``, and extends ``base`` to a base, and + ``gens`` to a strong generating set relative to that base with + probability of a wrong answer at most `2^{-consec\_succ}`, + provided the random generators are sufficiently random. + + Parameters + ========== + + base + The sequence to be extended to a base. + gens + The generating set to be extended to a strong generating set. + consec_succ + The parameter defining the probability of a wrong answer. + _random_prec + An internal parameter used for testing purposes. + + Returns + ======= + + (base, strong_gens) + ``base`` is the base and ``strong_gens`` is the strong generating + set relative to it. + + Examples + ======== + + >>> from sympy.combinatorics.testutil import _verify_bsgs + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> S = SymmetricGroup(5) + >>> base, strong_gens = S.schreier_sims_random(consec_succ=5) + >>> _verify_bsgs(S, base, strong_gens) #doctest: +SKIP + True + + Notes + ===== + + The algorithm is described in detail in [1], pp. 97-98. It extends + the orbits ``orbs`` and the permutation groups ``stabs`` to + basic orbits and basic stabilizers for the base and strong generating + set produced in the end. + The idea of the extension process + is to "sift" random group elements through the stabilizer chain + and amend the stabilizers/orbits along the way when a sift + is not successful. + The helper function ``_strip`` is used to attempt + to decompose a random group element according to the current + state of the stabilizer chain and report whether the element was + fully decomposed (successful sift) or not (unsuccessful sift). In + the latter case, the level at which the sift failed is reported and + used to amend ``stabs``, ``base``, ``gens`` and ``orbs`` accordingly. + The halting condition is for ``consec_succ`` consecutive successful + sifts to pass. This makes sure that the current ``base`` and ``gens`` + form a BSGS with probability at least `1 - 1/\text{consec\_succ}`. + + See Also + ======== + + schreier_sims + + """ + if base is None: + base = [] + if gens is None: + gens = self.generators + base_len = len(base) + n = self.degree + # make sure no generator fixes all base points + for gen in gens: + if all(gen(x) == x for x in base): + new = 0 + while gen._array_form[new] == new: + new += 1 + base.append(new) + base_len += 1 + # distribute generators according to basic stabilizers + strong_gens_distr = _distribute_gens_by_base(base, gens) + # initialize the basic stabilizers, basic transversals and basic orbits + transversals = {} + orbs = {} + for i in range(base_len): + transversals[i] = dict(_orbit_transversal(n, strong_gens_distr[i], + base[i], pairs=True)) + orbs[i] = list(transversals[i].keys()) + # initialize the number of consecutive elements sifted + c = 0 + # start sifting random elements while the number of consecutive sifts + # is less than consec_succ + while c < consec_succ: + if _random_prec is None: + g = self.random_pr() + else: + g = _random_prec['g'].pop() + h, j = _strip(g, base, orbs, transversals) + y = True + # determine whether a new base point is needed + if j <= base_len: + y = False + elif not h.is_Identity: + y = False + moved = 0 + while h(moved) == moved: + moved += 1 + base.append(moved) + base_len += 1 + strong_gens_distr.append([]) + # if the element doesn't sift, amend the strong generators and + # associated stabilizers and orbits + if y is False: + for l in range(1, j): + strong_gens_distr[l].append(h) + transversals[l] = dict(_orbit_transversal(n, + strong_gens_distr[l], base[l], pairs=True)) + orbs[l] = list(transversals[l].keys()) + c = 0 + else: + c += 1 + # build the strong generating set + strong_gens = strong_gens_distr[0][:] + for gen in strong_gens_distr[1]: + if gen not in strong_gens: + strong_gens.append(gen) + return base, strong_gens + + def schreier_vector(self, alpha): + """Computes the schreier vector for ``alpha``. + + Explanation + =========== + + The Schreier vector efficiently stores information + about the orbit of ``alpha``. It can later be used to quickly obtain + elements of the group that send ``alpha`` to a particular element + in the orbit. Notice that the Schreier vector depends on the order + in which the group generators are listed. For a definition, see [3]. + Since list indices start from zero, we adopt the convention to use + "None" instead of 0 to signify that an element does not belong + to the orbit. + For the algorithm and its correctness, see [2], pp.78-80. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([2, 4, 6, 3, 1, 5, 0]) + >>> b = Permutation([0, 1, 3, 5, 4, 6, 2]) + >>> G = PermutationGroup([a, b]) + >>> G.schreier_vector(0) + [-1, None, 0, 1, None, 1, 0] + + See Also + ======== + + orbit + + """ + n = self.degree + v = [None]*n + v[alpha] = -1 + orb = [alpha] + used = [False]*n + used[alpha] = True + gens = self.generators + r = len(gens) + for b in orb: + for i in range(r): + temp = gens[i]._array_form[b] + if used[temp] is False: + orb.append(temp) + used[temp] = True + v[temp] = i + return v + + def stabilizer(self, alpha): + r"""Return the stabilizer subgroup of ``alpha``. + + Explanation + =========== + + The stabilizer of `\alpha` is the group `G_\alpha = + \{g \in G | g(\alpha) = \alpha\}`. + For a proof of correctness, see [1], p.79. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> G = DihedralGroup(6) + >>> G.stabilizer(5) + PermutationGroup([ + (5)(0 4)(1 3)]) + + See Also + ======== + + orbit + + """ + return PermGroup(_stabilizer(self._degree, self._generators, alpha)) + + @property + def strong_gens(self): + r"""Return a strong generating set from the Schreier-Sims algorithm. + + Explanation + =========== + + A generating set `S = \{g_1, g_2, \dots, g_t\}` for a permutation group + `G` is a strong generating set relative to the sequence of points + (referred to as a "base") `(b_1, b_2, \dots, b_k)` if, for + `1 \leq i \leq k` we have that the intersection of the pointwise + stabilizer `G^{(i+1)} := G_{b_1, b_2, \dots, b_i}` with `S` generates + the pointwise stabilizer `G^{(i+1)}`. The concepts of a base and + strong generating set and their applications are discussed in depth + in [1], pp. 87-89 and [2], pp. 55-57. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> D = DihedralGroup(4) + >>> D.strong_gens + [(0 1 2 3), (0 3)(1 2), (1 3)] + >>> D.base + [0, 1] + + See Also + ======== + + base, basic_transversals, basic_orbits, basic_stabilizers + + """ + if self._strong_gens == []: + self.schreier_sims() + return self._strong_gens + + def subgroup(self, gens): + """ + Return the subgroup generated by `gens` which is a list of + elements of the group + """ + + if not all(g in self for g in gens): + raise ValueError("The group does not contain the supplied generators") + + G = PermutationGroup(gens) + return G + + def subgroup_search(self, prop, base=None, strong_gens=None, tests=None, + init_subgroup=None): + """Find the subgroup of all elements satisfying the property ``prop``. + + Explanation + =========== + + This is done by a depth-first search with respect to base images that + uses several tests to prune the search tree. + + Parameters + ========== + + prop + The property to be used. Has to be callable on group elements + and always return ``True`` or ``False``. It is assumed that + all group elements satisfying ``prop`` indeed form a subgroup. + base + A base for the supergroup. + strong_gens + A strong generating set for the supergroup. + tests + A list of callables of length equal to the length of ``base``. + These are used to rule out group elements by partial base images, + so that ``tests[l](g)`` returns False if the element ``g`` is known + not to satisfy prop base on where g sends the first ``l + 1`` base + points. + init_subgroup + if a subgroup of the sought group is + known in advance, it can be passed to the function as this + parameter. + + Returns + ======= + + res + The subgroup of all elements satisfying ``prop``. The generating + set for this group is guaranteed to be a strong generating set + relative to the base ``base``. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import (SymmetricGroup, + ... AlternatingGroup) + >>> from sympy.combinatorics.testutil import _verify_bsgs + >>> S = SymmetricGroup(7) + >>> prop_even = lambda x: x.is_even + >>> base, strong_gens = S.schreier_sims_incremental() + >>> G = S.subgroup_search(prop_even, base=base, strong_gens=strong_gens) + >>> G.is_subgroup(AlternatingGroup(7)) + True + >>> _verify_bsgs(G, base, G.generators) + True + + Notes + ===== + + This function is extremely lengthy and complicated and will require + some careful attention. The implementation is described in + [1], pp. 114-117, and the comments for the code here follow the lines + of the pseudocode in the book for clarity. + + The complexity is exponential in general, since the search process by + itself visits all members of the supergroup. However, there are a lot + of tests which are used to prune the search tree, and users can define + their own tests via the ``tests`` parameter, so in practice, and for + some computations, it's not terrible. + + A crucial part in the procedure is the frequent base change performed + (this is line 11 in the pseudocode) in order to obtain a new basic + stabilizer. The book mentiones that this can be done by using + ``.baseswap(...)``, however the current implementation uses a more + straightforward way to find the next basic stabilizer - calling the + function ``.stabilizer(...)`` on the previous basic stabilizer. + + """ + # initialize BSGS and basic group properties + def get_reps(orbits): + # get the minimal element in the base ordering + return [min(orbit, key = lambda x: base_ordering[x]) \ + for orbit in orbits] + + def update_nu(l): + temp_index = len(basic_orbits[l]) + 1 -\ + len(res_basic_orbits_init_base[l]) + # this corresponds to the element larger than all points + if temp_index >= len(sorted_orbits[l]): + nu[l] = base_ordering[degree] + else: + nu[l] = sorted_orbits[l][temp_index] + + if base is None: + base, strong_gens = self.schreier_sims_incremental() + base_len = len(base) + degree = self.degree + identity = _af_new(list(range(degree))) + base_ordering = _base_ordering(base, degree) + # add an element larger than all points + base_ordering.append(degree) + # add an element smaller than all points + base_ordering.append(-1) + # compute BSGS-related structures + strong_gens_distr = _distribute_gens_by_base(base, strong_gens) + basic_orbits, transversals = _orbits_transversals_from_bsgs(base, + strong_gens_distr) + # handle subgroup initialization and tests + if init_subgroup is None: + init_subgroup = PermutationGroup([identity]) + if tests is None: + trivial_test = lambda x: True + tests = [] + for i in range(base_len): + tests.append(trivial_test) + # line 1: more initializations. + res = init_subgroup + f = base_len - 1 + l = base_len - 1 + # line 2: set the base for K to the base for G + res_base = base[:] + # line 3: compute BSGS and related structures for K + res_base, res_strong_gens = res.schreier_sims_incremental( + base=res_base) + res_strong_gens_distr = _distribute_gens_by_base(res_base, + res_strong_gens) + res_generators = res.generators + res_basic_orbits_init_base = \ + [_orbit(degree, res_strong_gens_distr[i], res_base[i])\ + for i in range(base_len)] + # initialize orbit representatives + orbit_reps = [None]*base_len + # line 4: orbit representatives for f-th basic stabilizer of K + orbits = _orbits(degree, res_strong_gens_distr[f]) + orbit_reps[f] = get_reps(orbits) + # line 5: remove the base point from the representatives to avoid + # getting the identity element as a generator for K + orbit_reps[f].remove(base[f]) + # line 6: more initializations + c = [0]*base_len + u = [identity]*base_len + sorted_orbits = [None]*base_len + for i in range(base_len): + sorted_orbits[i] = basic_orbits[i][:] + sorted_orbits[i].sort(key=lambda point: base_ordering[point]) + # line 7: initializations + mu = [None]*base_len + nu = [None]*base_len + # this corresponds to the element smaller than all points + mu[l] = degree + 1 + update_nu(l) + # initialize computed words + computed_words = [identity]*base_len + # line 8: main loop + while True: + # apply all the tests + while l < base_len - 1 and \ + computed_words[l](base[l]) in orbit_reps[l] and \ + base_ordering[mu[l]] < \ + base_ordering[computed_words[l](base[l])] < \ + base_ordering[nu[l]] and \ + tests[l](computed_words): + # line 11: change the (partial) base of K + new_point = computed_words[l](base[l]) + res_base[l] = new_point + new_stab_gens = _stabilizer(degree, res_strong_gens_distr[l], + new_point) + res_strong_gens_distr[l + 1] = new_stab_gens + # line 12: calculate minimal orbit representatives for the + # l+1-th basic stabilizer + orbits = _orbits(degree, new_stab_gens) + orbit_reps[l + 1] = get_reps(orbits) + # line 13: amend sorted orbits + l += 1 + temp_orbit = [computed_words[l - 1](point) for point + in basic_orbits[l]] + temp_orbit.sort(key=lambda point: base_ordering[point]) + sorted_orbits[l] = temp_orbit + # lines 14 and 15: update variables used minimality tests + new_mu = degree + 1 + for i in range(l): + if base[l] in res_basic_orbits_init_base[i]: + candidate = computed_words[i](base[i]) + if base_ordering[candidate] > base_ordering[new_mu]: + new_mu = candidate + mu[l] = new_mu + update_nu(l) + # line 16: determine the new transversal element + c[l] = 0 + temp_point = sorted_orbits[l][c[l]] + gamma = computed_words[l - 1]._array_form.index(temp_point) + u[l] = transversals[l][gamma] + # update computed words + computed_words[l] = rmul(computed_words[l - 1], u[l]) + # lines 17 & 18: apply the tests to the group element found + g = computed_words[l] + temp_point = g(base[l]) + if l == base_len - 1 and \ + base_ordering[mu[l]] < \ + base_ordering[temp_point] < base_ordering[nu[l]] and \ + temp_point in orbit_reps[l] and \ + tests[l](computed_words) and \ + prop(g): + # line 19: reset the base of K + res_generators.append(g) + res_base = base[:] + # line 20: recalculate basic orbits (and transversals) + res_strong_gens.append(g) + res_strong_gens_distr = _distribute_gens_by_base(res_base, + res_strong_gens) + res_basic_orbits_init_base = \ + [_orbit(degree, res_strong_gens_distr[i], res_base[i]) \ + for i in range(base_len)] + # line 21: recalculate orbit representatives + # line 22: reset the search depth + orbit_reps[f] = get_reps(orbits) + l = f + # line 23: go up the tree until in the first branch not fully + # searched + while l >= 0 and c[l] == len(basic_orbits[l]) - 1: + l = l - 1 + # line 24: if the entire tree is traversed, return K + if l == -1: + return PermutationGroup(res_generators) + # lines 25-27: update orbit representatives + if l < f: + # line 26 + f = l + c[l] = 0 + # line 27 + temp_orbits = _orbits(degree, res_strong_gens_distr[f]) + orbit_reps[f] = get_reps(temp_orbits) + # line 28: update variables used for minimality testing + mu[l] = degree + 1 + temp_index = len(basic_orbits[l]) + 1 - \ + len(res_basic_orbits_init_base[l]) + if temp_index >= len(sorted_orbits[l]): + nu[l] = base_ordering[degree] + else: + nu[l] = sorted_orbits[l][temp_index] + # line 29: set the next element from the current branch and update + # accordingly + c[l] += 1 + if l == 0: + gamma = sorted_orbits[l][c[l]] + else: + gamma = computed_words[l - 1]._array_form.index(sorted_orbits[l][c[l]]) + + u[l] = transversals[l][gamma] + if l == 0: + computed_words[l] = u[l] + else: + computed_words[l] = rmul(computed_words[l - 1], u[l]) + + @property + def transitivity_degree(self): + r"""Compute the degree of transitivity of the group. + + Explanation + =========== + + A permutation group `G` acting on `\Omega = \{0, 1, \dots, n-1\}` is + ``k``-fold transitive, if, for any `k` points + `(a_1, a_2, \dots, a_k) \in \Omega` and any `k` points + `(b_1, b_2, \dots, b_k) \in \Omega` there exists `g \in G` such that + `g(a_1) = b_1, g(a_2) = b_2, \dots, g(a_k) = b_k` + The degree of transitivity of `G` is the maximum ``k`` such that + `G` is ``k``-fold transitive. ([8]) + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> a = Permutation([1, 2, 0]) + >>> b = Permutation([1, 0, 2]) + >>> G = PermutationGroup([a, b]) + >>> G.transitivity_degree + 3 + + See Also + ======== + + is_transitive, orbit + + """ + if self._transitivity_degree is None: + n = self.degree + G = self + # if G is k-transitive, a tuple (a_0,..,a_k) + # can be brought to (b_0,...,b_(k-1), b_k) + # where b_0,...,b_(k-1) are fixed points; + # consider the group G_k which stabilizes b_0,...,b_(k-1) + # if G_k is transitive on the subset excluding b_0,...,b_(k-1) + # then G is (k+1)-transitive + for i in range(n): + orb = G.orbit(i) + if len(orb) != n - i: + self._transitivity_degree = i + return i + G = G.stabilizer(i) + self._transitivity_degree = n + return n + else: + return self._transitivity_degree + + def _p_elements_group(self, p): + ''' + For an abelian p-group, return the subgroup consisting of + all elements of order p (and the identity) + + ''' + gens = self.generators[:] + gens = sorted(gens, key=lambda x: x.order(), reverse=True) + gens_p = [g**(g.order()/p) for g in gens] + gens_r = [] + for i in range(len(gens)): + x = gens[i] + x_order = x.order() + # x_p has order p + x_p = x**(x_order/p) + if i > 0: + P = PermutationGroup(gens_p[:i]) + else: + P = PermutationGroup(self.identity) + if x**(x_order/p) not in P: + gens_r.append(x**(x_order/p)) + else: + # replace x by an element of order (x.order()/p) + # so that gens still generates G + g = P.generator_product(x_p, original=True) + for s in g: + x = x*s**-1 + x_order = x_order/p + # insert x to gens so that the sorting is preserved + del gens[i] + del gens_p[i] + j = i - 1 + while j < len(gens) and gens[j].order() >= x_order: + j += 1 + gens = gens[:j] + [x] + gens[j:] + gens_p = gens_p[:j] + [x] + gens_p[j:] + return PermutationGroup(gens_r) + + def _sylow_alt_sym(self, p): + ''' + Return a p-Sylow subgroup of a symmetric or an + alternating group. + + Explanation + =========== + + The algorithm for this is hinted at in [1], Chapter 4, + Exercise 4. + + For Sym(n) with n = p^i, the idea is as follows. Partition + the interval [0..n-1] into p equal parts, each of length p^(i-1): + [0..p^(i-1)-1], [p^(i-1)..2*p^(i-1)-1]...[(p-1)*p^(i-1)..p^i-1]. + Find a p-Sylow subgroup of Sym(p^(i-1)) (treated as a subgroup + of ``self``) acting on each of the parts. Call the subgroups + P_1, P_2...P_p. The generators for the subgroups P_2...P_p + can be obtained from those of P_1 by applying a "shifting" + permutation to them, that is, a permutation mapping [0..p^(i-1)-1] + to the second part (the other parts are obtained by using the shift + multiple times). The union of this permutation and the generators + of P_1 is a p-Sylow subgroup of ``self``. + + For n not equal to a power of p, partition + [0..n-1] in accordance with how n would be written in base p. + E.g. for p=2 and n=11, 11 = 2^3 + 2^2 + 1 so the partition + is [[0..7], [8..9], {10}]. To generate a p-Sylow subgroup, + take the union of the generators for each of the parts. + For the above example, {(0 1), (0 2)(1 3), (0 4), (1 5)(2 7)} + from the first part, {(8 9)} from the second part and + nothing from the third. This gives 4 generators in total, and + the subgroup they generate is p-Sylow. + + Alternating groups are treated the same except when p=2. In this + case, (0 1)(s s+1) should be added for an appropriate s (the start + of a part) for each part in the partitions. + + See Also + ======== + + sylow_subgroup, is_alt_sym + + ''' + n = self.degree + gens = [] + identity = Permutation(n-1) + # the case of 2-sylow subgroups of alternating groups + # needs special treatment + alt = p == 2 and all(g.is_even for g in self.generators) + + # find the presentation of n in base p + coeffs = [] + m = n + while m > 0: + coeffs.append(m % p) + m = m // p + + power = len(coeffs)-1 + # for a symmetric group, gens[:i] is the generating + # set for a p-Sylow subgroup on [0..p**(i-1)-1]. For + # alternating groups, the same is given by gens[:2*(i-1)] + for i in range(1, power+1): + if i == 1 and alt: + # (0 1) shouldn't be added for alternating groups + continue + gen = Permutation([(j + p**(i-1)) % p**i for j in range(p**i)]) + gens.append(identity*gen) + if alt: + gen = Permutation(0, 1)*gen*Permutation(0, 1)*gen + gens.append(gen) + + # the first point in the current part (see the algorithm + # description in the docstring) + start = 0 + + while power > 0: + a = coeffs[power] + + # make the permutation shifting the start of the first + # part ([0..p^i-1] for some i) to the current one + for _ in range(a): + shift = Permutation() + if start > 0: + for i in range(p**power): + shift = shift(i, start + i) + + if alt: + gen = Permutation(0, 1)*shift*Permutation(0, 1)*shift + gens.append(gen) + j = 2*(power - 1) + else: + j = power + + for i, gen in enumerate(gens[:j]): + if alt and i % 2 == 1: + continue + # shift the generator to the start of the + # partition part + gen = shift*gen*shift + gens.append(gen) + + start += p**power + power = power-1 + + return gens + + def sylow_subgroup(self, p): + ''' + Return a p-Sylow subgroup of the group. + + The algorithm is described in [1], Chapter 4, Section 7 + + Examples + ======== + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> from sympy.combinatorics.named_groups import SymmetricGroup + >>> from sympy.combinatorics.named_groups import AlternatingGroup + + >>> D = DihedralGroup(6) + >>> S = D.sylow_subgroup(2) + >>> S.order() + 4 + >>> G = SymmetricGroup(6) + >>> S = G.sylow_subgroup(5) + >>> S.order() + 5 + + >>> G1 = AlternatingGroup(3) + >>> G2 = AlternatingGroup(5) + >>> G3 = AlternatingGroup(9) + + >>> S1 = G1.sylow_subgroup(3) + >>> S2 = G2.sylow_subgroup(3) + >>> S3 = G3.sylow_subgroup(3) + + >>> len1 = len(S1.lower_central_series()) + >>> len2 = len(S2.lower_central_series()) + >>> len3 = len(S3.lower_central_series()) + + >>> len1 == len2 + True + >>> len1 < len3 + True + + ''' + from sympy.combinatorics.homomorphisms import ( + orbit_homomorphism, block_homomorphism) + + if not isprime(p): + raise ValueError("p must be a prime") + + def is_p_group(G): + # check if the order of G is a power of p + # and return the power + m = G.order() + n = 0 + while m % p == 0: + m = m/p + n += 1 + if m == 1: + return True, n + return False, n + + def _sylow_reduce(mu, nu): + # reduction based on two homomorphisms + # mu and nu with trivially intersecting + # kernels + Q = mu.image().sylow_subgroup(p) + Q = mu.invert_subgroup(Q) + nu = nu.restrict_to(Q) + R = nu.image().sylow_subgroup(p) + return nu.invert_subgroup(R) + + order = self.order() + if order % p != 0: + return PermutationGroup([self.identity]) + p_group, n = is_p_group(self) + if p_group: + return self + + if self.is_alt_sym(): + return PermutationGroup(self._sylow_alt_sym(p)) + + # if there is a non-trivial orbit with size not divisible + # by p, the sylow subgroup is contained in its stabilizer + # (by orbit-stabilizer theorem) + orbits = self.orbits() + non_p_orbits = [o for o in orbits if len(o) % p != 0 and len(o) != 1] + if non_p_orbits: + G = self.stabilizer(list(non_p_orbits[0]).pop()) + return G.sylow_subgroup(p) + + if not self.is_transitive(): + # apply _sylow_reduce to orbit actions + orbits = sorted(orbits, key=len) + omega1 = orbits.pop() + omega2 = orbits[0].union(*orbits) + mu = orbit_homomorphism(self, omega1) + nu = orbit_homomorphism(self, omega2) + return _sylow_reduce(mu, nu) + + blocks = self.minimal_blocks() + if len(blocks) > 1: + # apply _sylow_reduce to block system actions + mu = block_homomorphism(self, blocks[0]) + nu = block_homomorphism(self, blocks[1]) + return _sylow_reduce(mu, nu) + elif len(blocks) == 1: + block = list(blocks)[0] + if any(e != 0 for e in block): + # self is imprimitive + mu = block_homomorphism(self, block) + if not is_p_group(mu.image())[0]: + S = mu.image().sylow_subgroup(p) + return mu.invert_subgroup(S).sylow_subgroup(p) + + # find an element of order p + g = self.random() + g_order = g.order() + while g_order % p != 0 or g_order == 0: + g = self.random() + g_order = g.order() + g = g**(g_order // p) + if order % p**2 != 0: + return PermutationGroup(g) + + C = self.centralizer(g) + while C.order() % p**n != 0: + S = C.sylow_subgroup(p) + s_order = S.order() + Z = S.center() + P = Z._p_elements_group(p) + h = P.random() + C_h = self.centralizer(h) + while C_h.order() % p*s_order != 0: + h = P.random() + C_h = self.centralizer(h) + C = C_h + + return C.sylow_subgroup(p) + + def _block_verify(self, L, alpha): + delta = sorted(self.orbit(alpha)) + # p[i] will be the number of the block + # delta[i] belongs to + p = [-1]*len(delta) + blocks = [-1]*len(delta) + + B = [[]] # future list of blocks + u = [0]*len(delta) # u[i] in L s.t. alpha^u[i] = B[0][i] + + t = L.orbit_transversal(alpha, pairs=True) + for a, beta in t: + B[0].append(a) + i_a = delta.index(a) + p[i_a] = 0 + blocks[i_a] = alpha + u[i_a] = beta + + rho = 0 + m = 0 # number of blocks - 1 + + while rho <= m: + beta = B[rho][0] + for g in self.generators: + d = beta^g + i_d = delta.index(d) + sigma = p[i_d] + if sigma < 0: + # define a new block + m += 1 + sigma = m + u[i_d] = u[delta.index(beta)]*g + p[i_d] = sigma + rep = d + blocks[i_d] = rep + newb = [rep] + for gamma in B[rho][1:]: + i_gamma = delta.index(gamma) + d = gamma^g + i_d = delta.index(d) + if p[i_d] < 0: + u[i_d] = u[i_gamma]*g + p[i_d] = sigma + blocks[i_d] = rep + newb.append(d) + else: + # B[rho] is not a block + s = u[i_gamma]*g*u[i_d]**(-1) + return False, s + + B.append(newb) + else: + for h in B[rho][1:]: + if h^g not in B[sigma]: + # B[rho] is not a block + s = u[delta.index(beta)]*g*u[i_d]**(-1) + return False, s + rho += 1 + + return True, blocks + + def _verify(H, K, phi, z, alpha): + ''' + Return a list of relators ``rels`` in generators ``gens`_h` that + are mapped to ``H.generators`` by ``phi`` so that given a finite + presentation of ``K`` on a subset of ``gens_h`` + is a finite presentation of ``H``. + + Explanation + =========== + + ``H`` should be generated by the union of ``K.generators`` and ``z`` + (a single generator), and ``H.stabilizer(alpha) == K``; ``phi`` is a + canonical injection from a free group into a permutation group + containing ``H``. + + The algorithm is described in [1], Chapter 6. + + Examples + ======== + + >>> from sympy.combinatorics import free_group, Permutation, PermutationGroup + >>> from sympy.combinatorics.homomorphisms import homomorphism + >>> from sympy.combinatorics.fp_groups import FpGroup + + >>> H = PermutationGroup(Permutation(0, 2), Permutation (1, 5)) + >>> K = PermutationGroup(Permutation(5)(0, 2)) + >>> F = free_group("x_0 x_1")[0] + >>> gens = F.generators + >>> phi = homomorphism(F, H, F.generators, H.generators) + >>> rels_k = [gens[0]**2] # relators for presentation of K + >>> z= Permutation(1, 5) + >>> check, rels_h = H._verify(K, phi, z, 1) + >>> check + True + >>> rels = rels_k + rels_h + >>> G = FpGroup(F, rels) # presentation of H + >>> G.order() == H.order() + True + + See also + ======== + + strong_presentation, presentation, stabilizer + + ''' + + orbit = H.orbit(alpha) + beta = alpha^(z**-1) + + K_beta = K.stabilizer(beta) + + # orbit representatives of K_beta + gammas = [alpha, beta] + orbits = list({tuple(K_beta.orbit(o)) for o in orbit}) + orbit_reps = [orb[0] for orb in orbits] + for rep in orbit_reps: + if rep not in gammas: + gammas.append(rep) + + # orbit transversal of K + betas = [alpha, beta] + transversal = {alpha: phi.invert(H.identity), beta: phi.invert(z**-1)} + + for s, g in K.orbit_transversal(beta, pairs=True): + if s not in transversal: + transversal[s] = transversal[beta]*phi.invert(g) + + + union = K.orbit(alpha).union(K.orbit(beta)) + while (len(union) < len(orbit)): + for gamma in gammas: + if gamma in union: + r = gamma^z + if r not in union: + betas.append(r) + transversal[r] = transversal[gamma]*phi.invert(z) + for s, g in K.orbit_transversal(r, pairs=True): + if s not in transversal: + transversal[s] = transversal[r]*phi.invert(g) + union = union.union(K.orbit(r)) + break + + # compute relators + rels = [] + + for b in betas: + k_gens = K.stabilizer(b).generators + for y in k_gens: + new_rel = transversal[b] + gens = K.generator_product(y, original=True) + for g in gens[::-1]: + new_rel = new_rel*phi.invert(g) + new_rel = new_rel*transversal[b]**-1 + + perm = phi(new_rel) + try: + gens = K.generator_product(perm, original=True) + except ValueError: + return False, perm + for g in gens: + new_rel = new_rel*phi.invert(g)**-1 + if new_rel not in rels: + rels.append(new_rel) + + for gamma in gammas: + new_rel = transversal[gamma]*phi.invert(z)*transversal[gamma^z]**-1 + perm = phi(new_rel) + try: + gens = K.generator_product(perm, original=True) + except ValueError: + return False, perm + for g in gens: + new_rel = new_rel*phi.invert(g)**-1 + if new_rel not in rels: + rels.append(new_rel) + + return True, rels + + def strong_presentation(self): + ''' + Return a strong finite presentation of group. The generators + of the returned group are in the same order as the strong + generators of group. + + The algorithm is based on Sims' Verify algorithm described + in [1], Chapter 6. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> P = DihedralGroup(4) + >>> G = P.strong_presentation() + >>> P.order() == G.order() + True + + See Also + ======== + + presentation, _verify + + ''' + from sympy.combinatorics.fp_groups import (FpGroup, + simplify_presentation) + from sympy.combinatorics.free_groups import free_group + from sympy.combinatorics.homomorphisms import (block_homomorphism, + homomorphism, GroupHomomorphism) + + strong_gens = self.strong_gens[:] + stabs = self.basic_stabilizers[:] + base = self.base[:] + + # injection from a free group on len(strong_gens) + # generators into G + gen_syms = [('x_%d'%i) for i in range(len(strong_gens))] + F = free_group(', '.join(gen_syms))[0] + phi = homomorphism(F, self, F.generators, strong_gens) + + H = PermutationGroup(self.identity) + while stabs: + alpha = base.pop() + K = H + H = stabs.pop() + new_gens = [g for g in H.generators if g not in K] + + if K.order() == 1: + z = new_gens.pop() + rels = [F.generators[-1]**z.order()] + intermediate_gens = [z] + K = PermutationGroup(intermediate_gens) + + # add generators one at a time building up from K to H + while new_gens: + z = new_gens.pop() + intermediate_gens = [z] + intermediate_gens + K_s = PermutationGroup(intermediate_gens) + orbit = K_s.orbit(alpha) + orbit_k = K.orbit(alpha) + + # split into cases based on the orbit of K_s + if orbit_k == orbit: + if z in K: + rel = phi.invert(z) + perm = z + else: + t = K.orbit_rep(alpha, alpha^z) + rel = phi.invert(z)*phi.invert(t)**-1 + perm = z*t**-1 + for g in K.generator_product(perm, original=True): + rel = rel*phi.invert(g)**-1 + new_rels = [rel] + elif len(orbit_k) == 1: + # `success` is always true because `strong_gens` + # and `base` are already a verified BSGS. Later + # this could be changed to start with a randomly + # generated (potential) BSGS, and then new elements + # would have to be appended to it when `success` + # is false. + success, new_rels = K_s._verify(K, phi, z, alpha) + else: + # K.orbit(alpha) should be a block + # under the action of K_s on K_s.orbit(alpha) + check, block = K_s._block_verify(K, alpha) + if check: + # apply _verify to the action of K_s + # on the block system; for convenience, + # add the blocks as additional points + # that K_s should act on + t = block_homomorphism(K_s, block) + m = t.codomain.degree # number of blocks + d = K_s.degree + + # conjugating with p will shift + # permutations in t.image() to + # higher numbers, e.g. + # p*(0 1)*p = (m m+1) + p = Permutation() + for i in range(m): + p *= Permutation(i, i+d) + + t_img = t.images + # combine generators of K_s with their + # action on the block system + images = {g: g*p*t_img[g]*p for g in t_img} + for g in self.strong_gens[:-len(K_s.generators)]: + images[g] = g + K_s_act = PermutationGroup(list(images.values())) + f = GroupHomomorphism(self, K_s_act, images) + + K_act = PermutationGroup([f(g) for g in K.generators]) + success, new_rels = K_s_act._verify(K_act, f.compose(phi), f(z), d) + + for n in new_rels: + if n not in rels: + rels.append(n) + K = K_s + + group = FpGroup(F, rels) + return simplify_presentation(group) + + def presentation(self, eliminate_gens=True): + ''' + Return an `FpGroup` presentation of the group. + + The algorithm is described in [1], Chapter 6.1. + + ''' + from sympy.combinatorics.fp_groups import (FpGroup, + simplify_presentation) + from sympy.combinatorics.coset_table import CosetTable + from sympy.combinatorics.free_groups import free_group + from sympy.combinatorics.homomorphisms import homomorphism + + if self._fp_presentation: + return self._fp_presentation + + def _factor_group_by_rels(G, rels): + if isinstance(G, FpGroup): + rels.extend(G.relators) + return FpGroup(G.free_group, list(set(rels))) + return FpGroup(G, rels) + + gens = self.generators + len_g = len(gens) + + if len_g == 1: + order = gens[0].order() + # handle the trivial group + if order == 1: + return free_group([])[0] + F, x = free_group('x') + return FpGroup(F, [x**order]) + + if self.order() > 20: + half_gens = self.generators[0:(len_g+1)//2] + else: + half_gens = [] + H = PermutationGroup(half_gens) + H_p = H.presentation() + + len_h = len(H_p.generators) + + C = self.coset_table(H) + n = len(C) # subgroup index + + gen_syms = [('x_%d'%i) for i in range(len(gens))] + F = free_group(', '.join(gen_syms))[0] + + # mapping generators of H_p to those of F + images = [F.generators[i] for i in range(len_h)] + R = homomorphism(H_p, F, H_p.generators, images, check=False) + + # rewrite relators + rels = R(H_p.relators) + G_p = FpGroup(F, rels) + + # injective homomorphism from G_p into self + T = homomorphism(G_p, self, G_p.generators, gens) + + C_p = CosetTable(G_p, []) + + C_p.table = [[None]*(2*len_g) for i in range(n)] + + # initiate the coset transversal + transversal = [None]*n + transversal[0] = G_p.identity + + # fill in the coset table as much as possible + for i in range(2*len_h): + C_p.table[0][i] = 0 + + gamma = 1 + for alpha, x in product(range(n), range(2*len_g)): + beta = C[alpha][x] + if beta == gamma: + gen = G_p.generators[x//2]**((-1)**(x % 2)) + transversal[beta] = transversal[alpha]*gen + C_p.table[alpha][x] = beta + C_p.table[beta][x + (-1)**(x % 2)] = alpha + gamma += 1 + if gamma == n: + break + + C_p.p = list(range(n)) + beta = x = 0 + + while not C_p.is_complete(): + # find the first undefined entry + while C_p.table[beta][x] == C[beta][x]: + x = (x + 1) % (2*len_g) + if x == 0: + beta = (beta + 1) % n + + # define a new relator + gen = G_p.generators[x//2]**((-1)**(x % 2)) + new_rel = transversal[beta]*gen*transversal[C[beta][x]]**-1 + perm = T(new_rel) + nxt = G_p.identity + for s in H.generator_product(perm, original=True): + nxt = nxt*T.invert(s)**-1 + new_rel = new_rel*nxt + + # continue coset enumeration + G_p = _factor_group_by_rels(G_p, [new_rel]) + C_p.scan_and_fill(0, new_rel) + C_p = G_p.coset_enumeration([], strategy="coset_table", + draft=C_p, max_cosets=n, incomplete=True) + + self._fp_presentation = simplify_presentation(G_p) + return self._fp_presentation + + def polycyclic_group(self): + """ + Return the PolycyclicGroup instance with below parameters: + + Explanation + =========== + + * pc_sequence : Polycyclic sequence is formed by collecting all + the missing generators between the adjacent groups in the + derived series of given permutation group. + + * pc_series : Polycyclic series is formed by adding all the missing + generators of ``der[i+1]`` in ``der[i]``, where ``der`` represents + the derived series. + + * relative_order : A list, computed by the ratio of adjacent groups in + pc_series. + + """ + from sympy.combinatorics.pc_groups import PolycyclicGroup + if not self.is_polycyclic: + raise ValueError("The group must be solvable") + + der = self.derived_series() + pc_series = [] + pc_sequence = [] + relative_order = [] + pc_series.append(der[-1]) + der.reverse() + + for i in range(len(der)-1): + H = der[i] + for g in der[i+1].generators: + if g not in H: + H = PermutationGroup([g] + H.generators) + pc_series.insert(0, H) + pc_sequence.insert(0, g) + + G1 = pc_series[0].order() + G2 = pc_series[1].order() + relative_order.insert(0, G1 // G2) + + return PolycyclicGroup(pc_sequence, pc_series, relative_order, collector=None) + + +def _orbit(degree, generators, alpha, action='tuples'): + r"""Compute the orbit of alpha `\{g(\alpha) | g \in G\}` as a set. + + Explanation + =========== + + The time complexity of the algorithm used here is `O(|Orb|*r)` where + `|Orb|` is the size of the orbit and ``r`` is the number of generators of + the group. For a more detailed analysis, see [1], p.78, [2], pp. 19-21. + Here alpha can be a single point, or a list of points. + + If alpha is a single point, the ordinary orbit is computed. + if alpha is a list of points, there are three available options: + + 'union' - computes the union of the orbits of the points in the list + 'tuples' - computes the orbit of the list interpreted as an ordered + tuple under the group action ( i.e., g((1, 2, 3)) = (g(1), g(2), g(3)) ) + 'sets' - computes the orbit of the list interpreted as a sets + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> from sympy.combinatorics.perm_groups import _orbit + >>> a = Permutation([1, 2, 0, 4, 5, 6, 3]) + >>> G = PermutationGroup([a]) + >>> _orbit(G.degree, G.generators, 0) + {0, 1, 2} + >>> _orbit(G.degree, G.generators, [0, 4], 'union') + {0, 1, 2, 3, 4, 5, 6} + + See Also + ======== + + orbit, orbit_transversal + + """ + if not hasattr(alpha, '__getitem__'): + alpha = [alpha] + + gens = [x._array_form for x in generators] + if len(alpha) == 1 or action == 'union': + orb = alpha + used = [False]*degree + for el in alpha: + used[el] = True + for b in orb: + for gen in gens: + temp = gen[b] + if used[temp] == False: + orb.append(temp) + used[temp] = True + return set(orb) + elif action == 'tuples': + alpha = tuple(alpha) + orb = [alpha] + used = {alpha} + for b in orb: + for gen in gens: + temp = tuple([gen[x] for x in b]) + if temp not in used: + orb.append(temp) + used.add(temp) + return set(orb) + elif action == 'sets': + alpha = frozenset(alpha) + orb = [alpha] + used = {alpha} + for b in orb: + for gen in gens: + temp = frozenset([gen[x] for x in b]) + if temp not in used: + orb.append(temp) + used.add(temp) + return {tuple(x) for x in orb} + + +def _orbits(degree, generators): + """Compute the orbits of G. + + If ``rep=False`` it returns a list of sets else it returns a list of + representatives of the orbits + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy.combinatorics.perm_groups import _orbits + >>> a = Permutation([0, 2, 1]) + >>> b = Permutation([1, 0, 2]) + >>> _orbits(a.size, [a, b]) + [{0, 1, 2}] + """ + + orbs = [] + sorted_I = list(range(degree)) + I = set(sorted_I) + while I: + i = sorted_I[0] + orb = _orbit(degree, generators, i) + orbs.append(orb) + # remove all indices that are in this orbit + I -= orb + sorted_I = [i for i in sorted_I if i not in orb] + return orbs + + +def _orbit_transversal(degree, generators, alpha, pairs, af=False, slp=False): + r"""Computes a transversal for the orbit of ``alpha`` as a set. + + Explanation + =========== + + generators generators of the group ``G`` + + For a permutation group ``G``, a transversal for the orbit + `Orb = \{g(\alpha) | g \in G\}` is a set + `\{g_\beta | g_\beta(\alpha) = \beta\}` for `\beta \in Orb`. + Note that there may be more than one possible transversal. + If ``pairs`` is set to ``True``, it returns the list of pairs + `(\beta, g_\beta)`. For a proof of correctness, see [1], p.79 + + if ``af`` is ``True``, the transversal elements are given in + array form. + + If `slp` is `True`, a dictionary `{beta: slp_beta}` is returned + for `\beta \in Orb` where `slp_beta` is a list of indices of the + generators in `generators` s.t. if `slp_beta = [i_1 \dots i_n]` + `g_\beta = generators[i_n] \times \dots \times generators[i_1]`. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> from sympy.combinatorics.perm_groups import _orbit_transversal + >>> G = DihedralGroup(6) + >>> _orbit_transversal(G.degree, G.generators, 0, False) + [(5), (0 1 2 3 4 5), (0 5)(1 4)(2 3), (0 2 4)(1 3 5), (5)(0 4)(1 3), (0 3)(1 4)(2 5)] + """ + + tr = [(alpha, list(range(degree)))] + slp_dict = {alpha: []} + used = [False]*degree + used[alpha] = True + gens = [x._array_form for x in generators] + for x, px in tr: + px_slp = slp_dict[x] + for gen in gens: + temp = gen[x] + if used[temp] == False: + slp_dict[temp] = [gens.index(gen)] + px_slp + tr.append((temp, _af_rmul(gen, px))) + used[temp] = True + if pairs: + if not af: + tr = [(x, _af_new(y)) for x, y in tr] + if not slp: + return tr + return tr, slp_dict + + if af: + tr = [y for _, y in tr] + if not slp: + return tr + return tr, slp_dict + + tr = [_af_new(y) for _, y in tr] + if not slp: + return tr + return tr, slp_dict + + +def _stabilizer(degree, generators, alpha): + r"""Return the stabilizer subgroup of ``alpha``. + + Explanation + =========== + + The stabilizer of `\alpha` is the group `G_\alpha = + \{g \in G | g(\alpha) = \alpha\}`. + For a proof of correctness, see [1], p.79. + + degree : degree of G + generators : generators of G + + Examples + ======== + + >>> from sympy.combinatorics.perm_groups import _stabilizer + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> G = DihedralGroup(6) + >>> _stabilizer(G.degree, G.generators, 5) + [(5)(0 4)(1 3), (5)] + + See Also + ======== + + orbit + + """ + orb = [alpha] + table = {alpha: list(range(degree))} + table_inv = {alpha: list(range(degree))} + used = [False]*degree + used[alpha] = True + gens = [x._array_form for x in generators] + stab_gens = [] + for b in orb: + for gen in gens: + temp = gen[b] + if used[temp] is False: + gen_temp = _af_rmul(gen, table[b]) + orb.append(temp) + table[temp] = gen_temp + table_inv[temp] = _af_invert(gen_temp) + used[temp] = True + else: + schreier_gen = _af_rmuln(table_inv[temp], gen, table[b]) + if schreier_gen not in stab_gens: + stab_gens.append(schreier_gen) + return [_af_new(x) for x in stab_gens] + + +PermGroup = PermutationGroup + + +class SymmetricPermutationGroup(Basic): + """ + The class defining the lazy form of SymmetricGroup. + + deg : int + + """ + def __new__(cls, deg): + deg = _sympify(deg) + obj = Basic.__new__(cls, deg) + return obj + + def __init__(self, *args, **kwargs): + self._deg = self.args[0] + self._order = None + + def __contains__(self, i): + """Return ``True`` if *i* is contained in SymmetricPermutationGroup. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, SymmetricPermutationGroup + >>> G = SymmetricPermutationGroup(4) + >>> Permutation(1, 2, 3) in G + True + + """ + if not isinstance(i, Permutation): + raise TypeError("A SymmetricPermutationGroup contains only Permutations as " + "elements, not elements of type %s" % type(i)) + return i.size == self.degree + + def order(self): + """ + Return the order of the SymmetricPermutationGroup. + + Examples + ======== + + >>> from sympy.combinatorics import SymmetricPermutationGroup + >>> G = SymmetricPermutationGroup(4) + >>> G.order() + 24 + """ + if self._order is not None: + return self._order + n = self._deg + self._order = factorial(n) + return self._order + + @property + def degree(self): + """ + Return the degree of the SymmetricPermutationGroup. + + Examples + ======== + + >>> from sympy.combinatorics import SymmetricPermutationGroup + >>> G = SymmetricPermutationGroup(4) + >>> G.degree + 4 + + """ + return self._deg + + @property + def identity(self): + ''' + Return the identity element of the SymmetricPermutationGroup. + + Examples + ======== + + >>> from sympy.combinatorics import SymmetricPermutationGroup + >>> G = SymmetricPermutationGroup(4) + >>> G.identity() + (3) + + ''' + return _af_new(list(range(self._deg))) + + +class Coset(Basic): + """A left coset of a permutation group with respect to an element. + + Parameters + ========== + + g : Permutation + + H : PermutationGroup + + dir : "+" or "-", If not specified by default it will be "+" + here ``dir`` specified the type of coset "+" represent the + right coset and "-" represent the left coset. + + G : PermutationGroup, optional + The group which contains *H* as its subgroup and *g* as its + element. + + If not specified, it would automatically become a symmetric + group ``SymmetricPermutationGroup(g.size)`` and + ``SymmetricPermutationGroup(H.degree)`` if ``g.size`` and ``H.degree`` + are matching.``SymmetricPermutationGroup`` is a lazy form of SymmetricGroup + used for representation purpose. + + """ + + def __new__(cls, g, H, G=None, dir="+"): + g = _sympify(g) + if not isinstance(g, Permutation): + raise NotImplementedError + + H = _sympify(H) + if not isinstance(H, PermutationGroup): + raise NotImplementedError + + if G is not None: + G = _sympify(G) + if not isinstance(G, (PermutationGroup, SymmetricPermutationGroup)): + raise NotImplementedError + if not H.is_subgroup(G): + raise ValueError("{} must be a subgroup of {}.".format(H, G)) + if g not in G: + raise ValueError("{} must be an element of {}.".format(g, G)) + else: + g_size = g.size + h_degree = H.degree + if g_size != h_degree: + raise ValueError( + "The size of the permutation {} and the degree of " + "the permutation group {} should be matching " + .format(g, H)) + G = SymmetricPermutationGroup(g.size) + + if isinstance(dir, str): + dir = Symbol(dir) + elif not isinstance(dir, Symbol): + raise TypeError("dir must be of type basestring or " + "Symbol, not %s" % type(dir)) + if str(dir) not in ('+', '-'): + raise ValueError("dir must be one of '+' or '-' not %s" % dir) + obj = Basic.__new__(cls, g, H, G, dir) + return obj + + def __init__(self, *args, **kwargs): + self._dir = self.args[3] + + @property + def is_left_coset(self): + """ + Check if the coset is left coset that is ``gH``. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup, Coset + >>> a = Permutation(1, 2) + >>> b = Permutation(0, 1) + >>> G = PermutationGroup([a, b]) + >>> cst = Coset(a, G, dir="-") + >>> cst.is_left_coset + True + + """ + return str(self._dir) == '-' + + @property + def is_right_coset(self): + """ + Check if the coset is right coset that is ``Hg``. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, PermutationGroup, Coset + >>> a = Permutation(1, 2) + >>> b = Permutation(0, 1) + >>> G = PermutationGroup([a, b]) + >>> cst = Coset(a, G, dir="+") + >>> cst.is_right_coset + True + + """ + return str(self._dir) == '+' + + def as_list(self): + """ + Return all the elements of coset in the form of list. + """ + g = self.args[0] + H = self.args[1] + cst = [] + if str(self._dir) == '+': + for h in H.elements: + cst.append(h*g) + else: + for h in H.elements: + cst.append(g*h) + return cst diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/permutations.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/permutations.py new file mode 100644 index 0000000000000000000000000000000000000000..3718467b69cbab2b9b0dd73e8fa160cceb0324bf --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/permutations.py @@ -0,0 +1,3114 @@ +import random +from collections import defaultdict +from collections.abc import Iterable +from functools import reduce + +from sympy.core.parameters import global_parameters +from sympy.core.basic import Atom +from sympy.core.expr import Expr +from sympy.core.numbers import int_valued +from sympy.core.numbers import Integer +from sympy.core.sympify import _sympify +from sympy.matrices import zeros +from sympy.polys.polytools import lcm +from sympy.printing.repr import srepr +from sympy.utilities.iterables import (flatten, has_variety, minlex, + has_dups, runs, is_sequence) +from sympy.utilities.misc import as_int +from mpmath.libmp.libintmath import ifac +from sympy.multipledispatch import dispatch + +def _af_rmul(a, b): + """ + Return the product b*a; input and output are array forms. The ith value + is a[b[i]]. + + Examples + ======== + + >>> from sympy.combinatorics.permutations import _af_rmul, Permutation + + >>> a, b = [1, 0, 2], [0, 2, 1] + >>> _af_rmul(a, b) + [1, 2, 0] + >>> [a[b[i]] for i in range(3)] + [1, 2, 0] + + This handles the operands in reverse order compared to the ``*`` operator: + + >>> a = Permutation(a) + >>> b = Permutation(b) + >>> list(a*b) + [2, 0, 1] + >>> [b(a(i)) for i in range(3)] + [2, 0, 1] + + See Also + ======== + + rmul, _af_rmuln + """ + return [a[i] for i in b] + + +def _af_rmuln(*abc): + """ + Given [a, b, c, ...] return the product of ...*c*b*a using array forms. + The ith value is a[b[c[i]]]. + + Examples + ======== + + >>> from sympy.combinatorics.permutations import _af_rmul, Permutation + + >>> a, b = [1, 0, 2], [0, 2, 1] + >>> _af_rmul(a, b) + [1, 2, 0] + >>> [a[b[i]] for i in range(3)] + [1, 2, 0] + + This handles the operands in reverse order compared to the ``*`` operator: + + >>> a = Permutation(a); b = Permutation(b) + >>> list(a*b) + [2, 0, 1] + >>> [b(a(i)) for i in range(3)] + [2, 0, 1] + + See Also + ======== + + rmul, _af_rmul + """ + a = abc + m = len(a) + if m == 3: + p0, p1, p2 = a + return [p0[p1[i]] for i in p2] + if m == 4: + p0, p1, p2, p3 = a + return [p0[p1[p2[i]]] for i in p3] + if m == 5: + p0, p1, p2, p3, p4 = a + return [p0[p1[p2[p3[i]]]] for i in p4] + if m == 6: + p0, p1, p2, p3, p4, p5 = a + return [p0[p1[p2[p3[p4[i]]]]] for i in p5] + if m == 7: + p0, p1, p2, p3, p4, p5, p6 = a + return [p0[p1[p2[p3[p4[p5[i]]]]]] for i in p6] + if m == 8: + p0, p1, p2, p3, p4, p5, p6, p7 = a + return [p0[p1[p2[p3[p4[p5[p6[i]]]]]]] for i in p7] + if m == 1: + return a[0][:] + if m == 2: + a, b = a + return [a[i] for i in b] + if m == 0: + raise ValueError("String must not be empty") + p0 = _af_rmuln(*a[:m//2]) + p1 = _af_rmuln(*a[m//2:]) + return [p0[i] for i in p1] + + +def _af_parity(pi): + """ + Computes the parity of a permutation in array form. + + Explanation + =========== + + The parity of a permutation reflects the parity of the + number of inversions in the permutation, i.e., the + number of pairs of x and y such that x > y but p[x] < p[y]. + + Examples + ======== + + >>> from sympy.combinatorics.permutations import _af_parity + >>> _af_parity([0, 1, 2, 3]) + 0 + >>> _af_parity([3, 2, 0, 1]) + 1 + + See Also + ======== + + Permutation + """ + n = len(pi) + a = [0] * n + c = 0 + for j in range(n): + if a[j] == 0: + c += 1 + a[j] = 1 + i = j + while pi[i] != j: + i = pi[i] + a[i] = 1 + return (n - c) % 2 + + +def _af_invert(a): + """ + Finds the inverse, ~A, of a permutation, A, given in array form. + + Examples + ======== + + >>> from sympy.combinatorics.permutations import _af_invert, _af_rmul + >>> A = [1, 2, 0, 3] + >>> _af_invert(A) + [2, 0, 1, 3] + >>> _af_rmul(_, A) + [0, 1, 2, 3] + + See Also + ======== + + Permutation, __invert__ + """ + inv_form = [0] * len(a) + for i, ai in enumerate(a): + inv_form[ai] = i + return inv_form + + +def _af_pow(a, n): + """ + Routine for finding powers of a permutation. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy.combinatorics.permutations import _af_pow + >>> p = Permutation([2, 0, 3, 1]) + >>> p.order() + 4 + >>> _af_pow(p._array_form, 4) + [0, 1, 2, 3] + """ + if n == 0: + return list(range(len(a))) + if n < 0: + return _af_pow(_af_invert(a), -n) + if n == 1: + return a[:] + elif n == 2: + b = [a[i] for i in a] + elif n == 3: + b = [a[a[i]] for i in a] + elif n == 4: + b = [a[a[a[i]]] for i in a] + else: + # use binary multiplication + b = list(range(len(a))) + while 1: + if n & 1: + b = [b[i] for i in a] + n -= 1 + if not n: + break + if n % 4 == 0: + a = [a[a[a[i]]] for i in a] + n = n // 4 + elif n % 2 == 0: + a = [a[i] for i in a] + n = n // 2 + return b + + +def _af_commutes_with(a, b): + """ + Checks if the two permutations with array forms + given by ``a`` and ``b`` commute. + + Examples + ======== + + >>> from sympy.combinatorics.permutations import _af_commutes_with + >>> _af_commutes_with([1, 2, 0], [0, 2, 1]) + False + + See Also + ======== + + Permutation, commutes_with + """ + return not any(a[b[i]] != b[a[i]] for i in range(len(a) - 1)) + + +class Cycle(dict): + """ + Wrapper around dict which provides the functionality of a disjoint cycle. + + Explanation + =========== + + A cycle shows the rule to use to move subsets of elements to obtain + a permutation. The Cycle class is more flexible than Permutation in + that 1) all elements need not be present in order to investigate how + multiple cycles act in sequence and 2) it can contain singletons: + + >>> from sympy.combinatorics.permutations import Perm, Cycle + + A Cycle will automatically parse a cycle given as a tuple on the rhs: + + >>> Cycle(1, 2)(2, 3) + (1 3 2) + + The identity cycle, Cycle(), can be used to start a product: + + >>> Cycle()(1, 2)(2, 3) + (1 3 2) + + The array form of a Cycle can be obtained by calling the list + method (or passing it to the list function) and all elements from + 0 will be shown: + + >>> a = Cycle(1, 2) + >>> a.list() + [0, 2, 1] + >>> list(a) + [0, 2, 1] + + If a larger (or smaller) range is desired use the list method and + provide the desired size -- but the Cycle cannot be truncated to + a size smaller than the largest element that is out of place: + + >>> b = Cycle(2, 4)(1, 2)(3, 1, 4)(1, 3) + >>> b.list() + [0, 2, 1, 3, 4] + >>> b.list(b.size + 1) + [0, 2, 1, 3, 4, 5] + >>> b.list(-1) + [0, 2, 1] + + Singletons are not shown when printing with one exception: the largest + element is always shown -- as a singleton if necessary: + + >>> Cycle(1, 4, 10)(4, 5) + (1 5 4 10) + >>> Cycle(1, 2)(4)(5)(10) + (1 2)(10) + + The array form can be used to instantiate a Permutation so other + properties of the permutation can be investigated: + + >>> Perm(Cycle(1, 2)(3, 4).list()).transpositions() + [(1, 2), (3, 4)] + + Notes + ===== + + The underlying structure of the Cycle is a dictionary and although + the __iter__ method has been redefined to give the array form of the + cycle, the underlying dictionary items are still available with the + such methods as items(): + + >>> list(Cycle(1, 2).items()) + [(1, 2), (2, 1)] + + See Also + ======== + + Permutation + """ + def __missing__(self, arg): + """Enter arg into dictionary and return arg.""" + return as_int(arg) + + def __iter__(self): + yield from self.list() + + def __call__(self, *other): + """Return product of cycles processed from R to L. + + Examples + ======== + + >>> from sympy.combinatorics import Cycle + >>> Cycle(1, 2)(2, 3) + (1 3 2) + + An instance of a Cycle will automatically parse list-like + objects and Permutations that are on the right. It is more + flexible than the Permutation in that all elements need not + be present: + + >>> a = Cycle(1, 2) + >>> a(2, 3) + (1 3 2) + >>> a(2, 3)(4, 5) + (1 3 2)(4 5) + + """ + rv = Cycle(*other) + for k, v in zip(list(self.keys()), [rv[self[k]] for k in self.keys()]): + rv[k] = v + return rv + + def list(self, size=None): + """Return the cycles as an explicit list starting from 0 up + to the greater of the largest value in the cycles and size. + + Truncation of trailing unmoved items will occur when size + is less than the maximum element in the cycle; if this is + desired, setting ``size=-1`` will guarantee such trimming. + + Examples + ======== + + >>> from sympy.combinatorics import Cycle + >>> p = Cycle(2, 3)(4, 5) + >>> p.list() + [0, 1, 3, 2, 5, 4] + >>> p.list(10) + [0, 1, 3, 2, 5, 4, 6, 7, 8, 9] + + Passing a length too small will trim trailing, unchanged elements + in the permutation: + + >>> Cycle(2, 4)(1, 2, 4).list(-1) + [0, 2, 1] + """ + if not self and size is None: + raise ValueError('must give size for empty Cycle') + if size is not None: + big = max([i for i in self.keys() if self[i] != i] + [0]) + size = max(size, big + 1) + else: + size = self.size + return [self[i] for i in range(size)] + + def __repr__(self): + """We want it to print as a Cycle, not as a dict. + + Examples + ======== + + >>> from sympy.combinatorics import Cycle + >>> Cycle(1, 2) + (1 2) + >>> print(_) + (1 2) + >>> list(Cycle(1, 2).items()) + [(1, 2), (2, 1)] + """ + if not self: + return 'Cycle()' + cycles = Permutation(self).cyclic_form + s = ''.join(str(tuple(c)) for c in cycles) + big = self.size - 1 + if not any(i == big for c in cycles for i in c): + s += '(%s)' % big + return 'Cycle%s' % s + + def __str__(self): + """We want it to be printed in a Cycle notation with no + comma in-between. + + Examples + ======== + + >>> from sympy.combinatorics import Cycle + >>> Cycle(1, 2) + (1 2) + >>> Cycle(1, 2, 4)(5, 6) + (1 2 4)(5 6) + """ + if not self: + return '()' + cycles = Permutation(self).cyclic_form + s = ''.join(str(tuple(c)) for c in cycles) + big = self.size - 1 + if not any(i == big for c in cycles for i in c): + s += '(%s)' % big + s = s.replace(',', '') + return s + + def __init__(self, *args): + """Load up a Cycle instance with the values for the cycle. + + Examples + ======== + + >>> from sympy.combinatorics import Cycle + >>> Cycle(1, 2, 6) + (1 2 6) + """ + + if not args: + return + if len(args) == 1: + if isinstance(args[0], Permutation): + for c in args[0].cyclic_form: + self.update(self(*c)) + return + elif isinstance(args[0], Cycle): + for k, v in args[0].items(): + self[k] = v + return + args = [as_int(a) for a in args] + if any(i < 0 for i in args): + raise ValueError('negative integers are not allowed in a cycle.') + if has_dups(args): + raise ValueError('All elements must be unique in a cycle.') + for i in range(-len(args), 0): + self[args[i]] = args[i + 1] + + @property + def size(self): + if not self: + return 0 + return max(self.keys()) + 1 + + def copy(self): + return Cycle(self) + + +class Permutation(Atom): + r""" + A permutation, alternatively known as an 'arrangement number' or 'ordering' + is an arrangement of the elements of an ordered list into a one-to-one + mapping with itself. The permutation of a given arrangement is given by + indicating the positions of the elements after re-arrangement [2]_. For + example, if one started with elements ``[x, y, a, b]`` (in that order) and + they were reordered as ``[x, y, b, a]`` then the permutation would be + ``[0, 1, 3, 2]``. Notice that (in SymPy) the first element is always referred + to as 0 and the permutation uses the indices of the elements in the + original ordering, not the elements ``(a, b, ...)`` themselves. + + >>> from sympy.combinatorics import Permutation + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + + Permutations Notation + ===================== + + Permutations are commonly represented in disjoint cycle or array forms. + + Array Notation and 2-line Form + ------------------------------------ + + In the 2-line form, the elements and their final positions are shown + as a matrix with 2 rows: + + [0 1 2 ... n-1] + [p(0) p(1) p(2) ... p(n-1)] + + Since the first line is always ``range(n)``, where n is the size of p, + it is sufficient to represent the permutation by the second line, + referred to as the "array form" of the permutation. This is entered + in brackets as the argument to the Permutation class: + + >>> p = Permutation([0, 2, 1]); p + Permutation([0, 2, 1]) + + Given i in range(p.size), the permutation maps i to i^p + + >>> [i^p for i in range(p.size)] + [0, 2, 1] + + The composite of two permutations p*q means first apply p, then q, so + i^(p*q) = (i^p)^q which is i^p^q according to Python precedence rules: + + >>> q = Permutation([2, 1, 0]) + >>> [i^p^q for i in range(3)] + [2, 0, 1] + >>> [i^(p*q) for i in range(3)] + [2, 0, 1] + + One can use also the notation p(i) = i^p, but then the composition + rule is (p*q)(i) = q(p(i)), not p(q(i)): + + >>> [(p*q)(i) for i in range(p.size)] + [2, 0, 1] + >>> [q(p(i)) for i in range(p.size)] + [2, 0, 1] + >>> [p(q(i)) for i in range(p.size)] + [1, 2, 0] + + Disjoint Cycle Notation + ----------------------- + + In disjoint cycle notation, only the elements that have shifted are + indicated. + + For example, [1, 3, 2, 0] can be represented as (0, 1, 3)(2). + This can be understood from the 2 line format of the given permutation. + In the 2-line form, + [0 1 2 3] + [1 3 2 0] + + The element in the 0th position is 1, so 0 -> 1. The element in the 1st + position is three, so 1 -> 3. And the element in the third position is again + 0, so 3 -> 0. Thus, 0 -> 1 -> 3 -> 0, and 2 -> 2. Thus, this can be represented + as 2 cycles: (0, 1, 3)(2). + In common notation, singular cycles are not explicitly written as they can be + inferred implicitly. + + Only the relative ordering of elements in a cycle matter: + + >>> Permutation(1,2,3) == Permutation(2,3,1) == Permutation(3,1,2) + True + + The disjoint cycle notation is convenient when representing + permutations that have several cycles in them: + + >>> Permutation(1, 2)(3, 5) == Permutation([[1, 2], [3, 5]]) + True + + It also provides some economy in entry when computing products of + permutations that are written in disjoint cycle notation: + + >>> Permutation(1, 2)(1, 3)(2, 3) + Permutation([0, 3, 2, 1]) + >>> _ == Permutation([[1, 2]])*Permutation([[1, 3]])*Permutation([[2, 3]]) + True + + Caution: when the cycles have common elements between them then the order + in which the permutations are applied matters. This module applies + the permutations from *left to right*. + + >>> Permutation(1, 2)(2, 3) == Permutation([(1, 2), (2, 3)]) + True + >>> Permutation(1, 2)(2, 3).list() + [0, 3, 1, 2] + + In the above case, (1,2) is computed before (2,3). + As 0 -> 0, 0 -> 0, element in position 0 is 0. + As 1 -> 2, 2 -> 3, element in position 1 is 3. + As 2 -> 1, 1 -> 1, element in position 2 is 1. + As 3 -> 3, 3 -> 2, element in position 3 is 2. + + If the first and second elements had been + swapped first, followed by the swapping of the second + and third, the result would have been [0, 2, 3, 1]. + If, you want to apply the cycles in the conventional + right to left order, call the function with arguments in reverse order + as demonstrated below: + + >>> Permutation([(1, 2), (2, 3)][::-1]).list() + [0, 2, 3, 1] + + Entering a singleton in a permutation is a way to indicate the size of the + permutation. The ``size`` keyword can also be used. + + Array-form entry: + + >>> Permutation([[1, 2], [9]]) + Permutation([0, 2, 1], size=10) + >>> Permutation([[1, 2]], size=10) + Permutation([0, 2, 1], size=10) + + Cyclic-form entry: + + >>> Permutation(1, 2, size=10) + Permutation([0, 2, 1], size=10) + >>> Permutation(9)(1, 2) + Permutation([0, 2, 1], size=10) + + Caution: no singleton containing an element larger than the largest + in any previous cycle can be entered. This is an important difference + in how Permutation and Cycle handle the ``__call__`` syntax. A singleton + argument at the start of a Permutation performs instantiation of the + Permutation and is permitted: + + >>> Permutation(5) + Permutation([], size=6) + + A singleton entered after instantiation is a call to the permutation + -- a function call -- and if the argument is out of range it will + trigger an error. For this reason, it is better to start the cycle + with the singleton: + + The following fails because there is no element 3: + + >>> Permutation(1, 2)(3) + Traceback (most recent call last): + ... + IndexError: list index out of range + + This is ok: only the call to an out of range singleton is prohibited; + otherwise the permutation autosizes: + + >>> Permutation(3)(1, 2) + Permutation([0, 2, 1, 3]) + >>> Permutation(1, 2)(3, 4) == Permutation(3, 4)(1, 2) + True + + + Equality testing + ---------------- + + The array forms must be the same in order for permutations to be equal: + + >>> Permutation([1, 0, 2, 3]) == Permutation([1, 0]) + False + + + Identity Permutation + -------------------- + + The identity permutation is a permutation in which no element is out of + place. It can be entered in a variety of ways. All the following create + an identity permutation of size 4: + + >>> I = Permutation([0, 1, 2, 3]) + >>> all(p == I for p in [ + ... Permutation(3), + ... Permutation(range(4)), + ... Permutation([], size=4), + ... Permutation(size=4)]) + True + + Watch out for entering the range *inside* a set of brackets (which is + cycle notation): + + >>> I == Permutation([range(4)]) + False + + + Permutation Printing + ==================== + + There are a few things to note about how Permutations are printed. + + .. deprecated:: 1.6 + + Configuring Permutation printing by setting + ``Permutation.print_cyclic`` is deprecated. Users should use the + ``perm_cyclic`` flag to the printers, as described below. + + 1) If you prefer one form (array or cycle) over another, you can set + ``init_printing`` with the ``perm_cyclic`` flag. + + >>> from sympy import init_printing + >>> p = Permutation(1, 2)(4, 5)(3, 4) + >>> p + Permutation([0, 2, 1, 4, 5, 3]) + + >>> init_printing(perm_cyclic=True, pretty_print=False) + >>> p + (1 2)(3 4 5) + + 2) Regardless of the setting, a list of elements in the array for cyclic + form can be obtained and either of those can be copied and supplied as + the argument to Permutation: + + >>> p.array_form + [0, 2, 1, 4, 5, 3] + >>> p.cyclic_form + [[1, 2], [3, 4, 5]] + >>> Permutation(_) == p + True + + 3) Printing is economical in that as little as possible is printed while + retaining all information about the size of the permutation: + + >>> init_printing(perm_cyclic=False, pretty_print=False) + >>> Permutation([1, 0, 2, 3]) + Permutation([1, 0, 2, 3]) + >>> Permutation([1, 0, 2, 3], size=20) + Permutation([1, 0], size=20) + >>> Permutation([1, 0, 2, 4, 3, 5, 6], size=20) + Permutation([1, 0, 2, 4, 3], size=20) + + >>> p = Permutation([1, 0, 2, 3]) + >>> init_printing(perm_cyclic=True, pretty_print=False) + >>> p + (3)(0 1) + >>> init_printing(perm_cyclic=False, pretty_print=False) + + The 2 was not printed but it is still there as can be seen with the + array_form and size methods: + + >>> p.array_form + [1, 0, 2, 3] + >>> p.size + 4 + + Short introduction to other methods + =================================== + + The permutation can act as a bijective function, telling what element is + located at a given position + + >>> q = Permutation([5, 2, 3, 4, 1, 0]) + >>> q.array_form[1] # the hard way + 2 + >>> q(1) # the easy way + 2 + >>> {i: q(i) for i in range(q.size)} # showing the bijection + {0: 5, 1: 2, 2: 3, 3: 4, 4: 1, 5: 0} + + The full cyclic form (including singletons) can be obtained: + + >>> p.full_cyclic_form + [[0, 1], [2], [3]] + + Any permutation can be factored into transpositions of pairs of elements: + + >>> Permutation([[1, 2], [3, 4, 5]]).transpositions() + [(1, 2), (3, 5), (3, 4)] + >>> Permutation.rmul(*[Permutation([ti], size=6) for ti in _]).cyclic_form + [[1, 2], [3, 4, 5]] + + The number of permutations on a set of n elements is given by n! and is + called the cardinality. + + >>> p.size + 4 + >>> p.cardinality + 24 + + A given permutation has a rank among all the possible permutations of the + same elements, but what that rank is depends on how the permutations are + enumerated. (There are a number of different methods of doing so.) The + lexicographic rank is given by the rank method and this rank is used to + increment a permutation with addition/subtraction: + + >>> p.rank() + 6 + >>> p + 1 + Permutation([1, 0, 3, 2]) + >>> p.next_lex() + Permutation([1, 0, 3, 2]) + >>> _.rank() + 7 + >>> p.unrank_lex(p.size, rank=7) + Permutation([1, 0, 3, 2]) + + The product of two permutations p and q is defined as their composition as + functions, (p*q)(i) = q(p(i)) [6]_. + + >>> p = Permutation([1, 0, 2, 3]) + >>> q = Permutation([2, 3, 1, 0]) + >>> list(q*p) + [2, 3, 0, 1] + >>> list(p*q) + [3, 2, 1, 0] + >>> [q(p(i)) for i in range(p.size)] + [3, 2, 1, 0] + + The permutation can be 'applied' to any list-like object, not only + Permutations: + + >>> p(['zero', 'one', 'four', 'two']) + ['one', 'zero', 'four', 'two'] + >>> p('zo42') + ['o', 'z', '4', '2'] + + If you have a list of arbitrary elements, the corresponding permutation + can be found with the from_sequence method: + + >>> Permutation.from_sequence('SymPy') + Permutation([1, 3, 2, 0, 4]) + + Checking if a Permutation is contained in a Group + ================================================= + + Generally if you have a group of permutations G on n symbols, and + you're checking if a permutation on less than n symbols is part + of that group, the check will fail. + + Here is an example for n=5 and we check if the cycle + (1,2,3) is in G: + + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=True, pretty_print=False) + >>> from sympy.combinatorics import Cycle, Permutation + >>> from sympy.combinatorics.perm_groups import PermutationGroup + >>> G = PermutationGroup(Cycle(2, 3)(4, 5), Cycle(1, 2, 3, 4, 5)) + >>> p1 = Permutation(Cycle(2, 5, 3)) + >>> p2 = Permutation(Cycle(1, 2, 3)) + >>> a1 = Permutation(Cycle(1, 2, 3).list(6)) + >>> a2 = Permutation(Cycle(1, 2, 3)(5)) + >>> a3 = Permutation(Cycle(1, 2, 3),size=6) + >>> for p in [p1,p2,a1,a2,a3]: p, G.contains(p) + ((2 5 3), True) + ((1 2 3), False) + ((5)(1 2 3), True) + ((5)(1 2 3), True) + ((5)(1 2 3), True) + + The check for p2 above will fail. + + Checking if p1 is in G works because SymPy knows + G is a group on 5 symbols, and p1 is also on 5 symbols + (its largest element is 5). + + For ``a1``, the ``.list(6)`` call will extend the permutation to 5 + symbols, so the test will work as well. In the case of ``a2`` the + permutation is being extended to 5 symbols by using a singleton, + and in the case of ``a3`` it's extended through the constructor + argument ``size=6``. + + There is another way to do this, which is to tell the ``contains`` + method that the number of symbols the group is on does not need to + match perfectly the number of symbols for the permutation: + + >>> G.contains(p2,strict=False) + True + + This can be via the ``strict`` argument to the ``contains`` method, + and SymPy will try to extend the permutation on its own and then + perform the containment check. + + See Also + ======== + + Cycle + + References + ========== + + .. [1] Skiena, S. 'Permutations.' 1.1 in Implementing Discrete Mathematics + Combinatorics and Graph Theory with Mathematica. Reading, MA: + Addison-Wesley, pp. 3-16, 1990. + + .. [2] Knuth, D. E. The Art of Computer Programming, Vol. 4: Combinatorial + Algorithms, 1st ed. Reading, MA: Addison-Wesley, 2011. + + .. [3] Wendy Myrvold and Frank Ruskey. 2001. Ranking and unranking + permutations in linear time. Inf. Process. Lett. 79, 6 (September 2001), + 281-284. DOI=10.1016/S0020-0190(01)00141-7 + + .. [4] D. L. Kreher, D. R. Stinson 'Combinatorial Algorithms' + CRC Press, 1999 + + .. [5] Graham, R. L.; Knuth, D. E.; and Patashnik, O. + Concrete Mathematics: A Foundation for Computer Science, 2nd ed. + Reading, MA: Addison-Wesley, 1994. + + .. [6] https://en.wikipedia.org/w/index.php?oldid=499948155#Product_and_inverse + + .. [7] https://en.wikipedia.org/wiki/Lehmer_code + + """ + + is_Permutation = True + + _array_form = None + _cyclic_form = None + _cycle_structure = None + _size = None + _rank = None + + def __new__(cls, *args, size=None, **kwargs): + """ + Constructor for the Permutation object from a list or a + list of lists in which all elements of the permutation may + appear only once. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + + Permutations entered in array-form are left unaltered: + + >>> Permutation([0, 2, 1]) + Permutation([0, 2, 1]) + + Permutations entered in cyclic form are converted to array form; + singletons need not be entered, but can be entered to indicate the + largest element: + + >>> Permutation([[4, 5, 6], [0, 1]]) + Permutation([1, 0, 2, 3, 5, 6, 4]) + >>> Permutation([[4, 5, 6], [0, 1], [19]]) + Permutation([1, 0, 2, 3, 5, 6, 4], size=20) + + All manipulation of permutations assumes that the smallest element + is 0 (in keeping with 0-based indexing in Python) so if the 0 is + missing when entering a permutation in array form, an error will be + raised: + + >>> Permutation([2, 1]) + Traceback (most recent call last): + ... + ValueError: Integers 0 through 2 must be present. + + If a permutation is entered in cyclic form, it can be entered without + singletons and the ``size`` specified so those values can be filled + in, otherwise the array form will only extend to the maximum value + in the cycles: + + >>> Permutation([[1, 4], [3, 5, 2]], size=10) + Permutation([0, 4, 3, 5, 1, 2], size=10) + >>> _.array_form + [0, 4, 3, 5, 1, 2, 6, 7, 8, 9] + """ + if size is not None: + size = int(size) + + #a) () + #b) (1) = identity + #c) (1, 2) = cycle + #d) ([1, 2, 3]) = array form + #e) ([[1, 2]]) = cyclic form + #f) (Cycle) = conversion to permutation + #g) (Permutation) = adjust size or return copy + ok = True + if not args: # a + return cls._af_new(list(range(size or 0))) + elif len(args) > 1: # c + return cls._af_new(Cycle(*args).list(size)) + if len(args) == 1: + a = args[0] + if isinstance(a, cls): # g + if size is None or size == a.size: + return a + return cls(a.array_form, size=size) + if isinstance(a, Cycle): # f + return cls._af_new(a.list(size)) + if not is_sequence(a): # b + if size is not None and a + 1 > size: + raise ValueError('size is too small when max is %s' % a) + return cls._af_new(list(range(a + 1))) + if has_variety(is_sequence(ai) for ai in a): + ok = False + else: + ok = False + if not ok: + raise ValueError("Permutation argument must be a list of ints, " + "a list of lists, Permutation or Cycle.") + + # safe to assume args are valid; this also makes a copy + # of the args + args = list(args[0]) + + is_cycle = args and is_sequence(args[0]) + if is_cycle: # e + args = [[int(i) for i in c] for c in args] + else: # d + args = [int(i) for i in args] + + # if there are n elements present, 0, 1, ..., n-1 should be present + # unless a cycle notation has been provided. A 0 will be added + # for convenience in case one wants to enter permutations where + # counting starts from 1. + + temp = flatten(args) + if has_dups(temp) and not is_cycle: + raise ValueError('there were repeated elements.') + temp = set(temp) + + if not is_cycle: + if temp != set(range(len(temp))): + raise ValueError('Integers 0 through %s must be present.' % + max(temp)) + if size is not None and temp and max(temp) + 1 > size: + raise ValueError('max element should not exceed %s' % (size - 1)) + + if is_cycle: + # it's not necessarily canonical so we won't store + # it -- use the array form instead + c = Cycle() + for ci in args: + c = c(*ci) + aform = c.list() + else: + aform = list(args) + if size and size > len(aform): + # don't allow for truncation of permutation which + # might split a cycle and lead to an invalid aform + # but do allow the permutation size to be increased + aform.extend(list(range(len(aform), size))) + + return cls._af_new(aform) + + @classmethod + def _af_new(cls, perm): + """A method to produce a Permutation object from a list; + the list is bound to the _array_form attribute, so it must + not be modified; this method is meant for internal use only; + the list ``a`` is supposed to be generated as a temporary value + in a method, so p = Perm._af_new(a) is the only object + to hold a reference to ``a``:: + + Examples + ======== + + >>> from sympy.combinatorics.permutations import Perm + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + >>> a = [2, 1, 3, 0] + >>> p = Perm._af_new(a) + >>> p + Permutation([2, 1, 3, 0]) + + """ + p = super().__new__(cls) + p._array_form = perm + p._size = len(perm) + return p + + def copy(self): + return self.__class__(self.array_form) + + def __getnewargs__(self): + return (self.array_form,) + + def _hashable_content(self): + # the array_form (a list) is the Permutation arg, so we need to + # return a tuple, instead + return tuple(self.array_form) + + @property + def array_form(self): + """ + Return a copy of the attribute _array_form + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([[2, 0], [3, 1]]) + >>> p.array_form + [2, 3, 0, 1] + >>> Permutation([[2, 0, 3, 1]]).array_form + [3, 2, 0, 1] + >>> Permutation([2, 0, 3, 1]).array_form + [2, 0, 3, 1] + >>> Permutation([[1, 2], [4, 5]]).array_form + [0, 2, 1, 3, 5, 4] + """ + return self._array_form[:] + + def list(self, size=None): + """Return the permutation as an explicit list, possibly + trimming unmoved elements if size is less than the maximum + element in the permutation; if this is desired, setting + ``size=-1`` will guarantee such trimming. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation(2, 3)(4, 5) + >>> p.list() + [0, 1, 3, 2, 5, 4] + >>> p.list(10) + [0, 1, 3, 2, 5, 4, 6, 7, 8, 9] + + Passing a length too small will trim trailing, unchanged elements + in the permutation: + + >>> Permutation(2, 4)(1, 2, 4).list(-1) + [0, 2, 1] + >>> Permutation(3).list(-1) + [] + """ + if not self and size is None: + raise ValueError('must give size for empty Cycle') + rv = self.array_form + if size is not None: + if size > self.size: + rv.extend(list(range(self.size, size))) + else: + # find first value from rhs where rv[i] != i + i = self.size - 1 + while rv: + if rv[-1] != i: + break + rv.pop() + i -= 1 + return rv + + @property + def cyclic_form(self): + """ + This is used to convert to the cyclic notation + from the canonical notation. Singletons are omitted. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([0, 3, 1, 2]) + >>> p.cyclic_form + [[1, 3, 2]] + >>> Permutation([1, 0, 2, 4, 3, 5]).cyclic_form + [[0, 1], [3, 4]] + + See Also + ======== + + array_form, full_cyclic_form + """ + if self._cyclic_form is not None: + return list(self._cyclic_form) + array_form = self.array_form + unchecked = [True] * len(array_form) + cyclic_form = [] + for i in range(len(array_form)): + if unchecked[i]: + cycle = [] + cycle.append(i) + unchecked[i] = False + j = i + while unchecked[array_form[j]]: + j = array_form[j] + cycle.append(j) + unchecked[j] = False + if len(cycle) > 1: + cyclic_form.append(cycle) + assert cycle == list(minlex(cycle)) + cyclic_form.sort() + self._cyclic_form = cyclic_form.copy() + return cyclic_form + + @property + def full_cyclic_form(self): + """Return permutation in cyclic form including singletons. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> Permutation([0, 2, 1]).full_cyclic_form + [[0], [1, 2]] + """ + need = set(range(self.size)) - set(flatten(self.cyclic_form)) + rv = self.cyclic_form + [[i] for i in need] + rv.sort() + return rv + + @property + def size(self): + """ + Returns the number of elements in the permutation. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> Permutation([[3, 2], [0, 1]]).size + 4 + + See Also + ======== + + cardinality, length, order, rank + """ + return self._size + + def support(self): + """Return the elements in permutation, P, for which P[i] != i. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([[3, 2], [0, 1], [4]]) + >>> p.array_form + [1, 0, 3, 2, 4] + >>> p.support() + [0, 1, 2, 3] + """ + a = self.array_form + return [i for i, e in enumerate(a) if e != i] + + def __add__(self, other): + """Return permutation that is other higher in rank than self. + + The rank is the lexicographical rank, with the identity permutation + having rank of 0. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> I = Permutation([0, 1, 2, 3]) + >>> a = Permutation([2, 1, 3, 0]) + >>> I + a.rank() == a + True + + See Also + ======== + + __sub__, inversion_vector + + """ + rank = (self.rank() + other) % self.cardinality + rv = self.unrank_lex(self.size, rank) + rv._rank = rank + return rv + + def __sub__(self, other): + """Return the permutation that is other lower in rank than self. + + See Also + ======== + + __add__ + """ + return self.__add__(-other) + + @staticmethod + def rmul(*args): + """ + Return product of Permutations [a, b, c, ...] as the Permutation whose + ith value is a(b(c(i))). + + a, b, c, ... can be Permutation objects or tuples. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + + >>> a, b = [1, 0, 2], [0, 2, 1] + >>> a = Permutation(a); b = Permutation(b) + >>> list(Permutation.rmul(a, b)) + [1, 2, 0] + >>> [a(b(i)) for i in range(3)] + [1, 2, 0] + + This handles the operands in reverse order compared to the ``*`` operator: + + >>> a = Permutation(a); b = Permutation(b) + >>> list(a*b) + [2, 0, 1] + >>> [b(a(i)) for i in range(3)] + [2, 0, 1] + + Notes + ===== + + All items in the sequence will be parsed by Permutation as + necessary as long as the first item is a Permutation: + + >>> Permutation.rmul(a, [0, 2, 1]) == Permutation.rmul(a, b) + True + + The reverse order of arguments will raise a TypeError. + + """ + rv = args[0] + for i in range(1, len(args)): + rv = args[i]*rv + return rv + + @classmethod + def rmul_with_af(cls, *args): + """ + same as rmul, but the elements of args are Permutation objects + which have _array_form + """ + a = [x._array_form for x in args] + rv = cls._af_new(_af_rmuln(*a)) + return rv + + def mul_inv(self, other): + """ + other*~self, self and other have _array_form + """ + a = _af_invert(self._array_form) + b = other._array_form + return self._af_new(_af_rmul(a, b)) + + def __rmul__(self, other): + """This is needed to coerce other to Permutation in rmul.""" + cls = type(self) + return cls(other)*self + + def __mul__(self, other): + """ + Return the product a*b as a Permutation; the ith value is b(a(i)). + + Examples + ======== + + >>> from sympy.combinatorics.permutations import _af_rmul, Permutation + + >>> a, b = [1, 0, 2], [0, 2, 1] + >>> a = Permutation(a); b = Permutation(b) + >>> list(a*b) + [2, 0, 1] + >>> [b(a(i)) for i in range(3)] + [2, 0, 1] + + This handles operands in reverse order compared to _af_rmul and rmul: + + >>> al = list(a); bl = list(b) + >>> _af_rmul(al, bl) + [1, 2, 0] + >>> [al[bl[i]] for i in range(3)] + [1, 2, 0] + + It is acceptable for the arrays to have different lengths; the shorter + one will be padded to match the longer one: + + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + >>> b*Permutation([1, 0]) + Permutation([1, 2, 0]) + >>> Permutation([1, 0])*b + Permutation([2, 0, 1]) + + It is also acceptable to allow coercion to handle conversion of a + single list to the left of a Permutation: + + >>> [0, 1]*a # no change: 2-element identity + Permutation([1, 0, 2]) + >>> [[0, 1]]*a # exchange first two elements + Permutation([0, 1, 2]) + + You cannot use more than 1 cycle notation in a product of cycles + since coercion can only handle one argument to the left. To handle + multiple cycles it is convenient to use Cycle instead of Permutation: + + >>> [[1, 2]]*[[2, 3]]*Permutation([]) # doctest: +SKIP + >>> from sympy.combinatorics.permutations import Cycle + >>> Cycle(1, 2)(2, 3) + (1 3 2) + + """ + from sympy.combinatorics.perm_groups import PermutationGroup, Coset + if isinstance(other, PermutationGroup): + return Coset(self, other, dir='-') + a = self.array_form + # __rmul__ makes sure the other is a Permutation + b = other.array_form + if not b: + perm = a + else: + b.extend(list(range(len(b), len(a)))) + perm = [b[i] for i in a] + b[len(a):] + return self._af_new(perm) + + def commutes_with(self, other): + """ + Checks if the elements are commuting. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> a = Permutation([1, 4, 3, 0, 2, 5]) + >>> b = Permutation([0, 1, 2, 3, 4, 5]) + >>> a.commutes_with(b) + True + >>> b = Permutation([2, 3, 5, 4, 1, 0]) + >>> a.commutes_with(b) + False + """ + a = self.array_form + b = other.array_form + return _af_commutes_with(a, b) + + def __pow__(self, n): + """ + Routine for finding powers of a permutation. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + >>> p = Permutation([2, 0, 3, 1]) + >>> p.order() + 4 + >>> p**4 + Permutation([0, 1, 2, 3]) + """ + if isinstance(n, Permutation): + raise NotImplementedError( + 'p**p is not defined; do you mean p^p (conjugate)?') + n = int(n) + return self._af_new(_af_pow(self.array_form, n)) + + def __rxor__(self, i): + """Return self(i) when ``i`` is an int. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation(1, 2, 9) + >>> 2^p == p(2) == 9 + True + """ + if int_valued(i): + return self(i) + else: + raise NotImplementedError( + "i^p = p(i) when i is an integer, not %s." % i) + + def __xor__(self, h): + """Return the conjugate permutation ``~h*self*h` `. + + Explanation + =========== + + If ``a`` and ``b`` are conjugates, ``a = h*b*~h`` and + ``b = ~h*a*h`` and both have the same cycle structure. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation(1, 2, 9) + >>> q = Permutation(6, 9, 8) + >>> p*q != q*p + True + + Calculate and check properties of the conjugate: + + >>> c = p^q + >>> c == ~q*p*q and p == q*c*~q + True + + The expression q^p^r is equivalent to q^(p*r): + + >>> r = Permutation(9)(4, 6, 8) + >>> q^p^r == q^(p*r) + True + + If the term to the left of the conjugate operator, i, is an integer + then this is interpreted as selecting the ith element from the + permutation to the right: + + >>> all(i^p == p(i) for i in range(p.size)) + True + + Note that the * operator as higher precedence than the ^ operator: + + >>> q^r*p^r == q^(r*p)^r == Permutation(9)(1, 6, 4) + True + + Notes + ===== + + In Python the precedence rule is p^q^r = (p^q)^r which differs + in general from p^(q^r) + + >>> q^p^r + (9)(1 4 8) + >>> q^(p^r) + (9)(1 8 6) + + For a given r and p, both of the following are conjugates of p: + ~r*p*r and r*p*~r. But these are not necessarily the same: + + >>> ~r*p*r == r*p*~r + True + + >>> p = Permutation(1, 2, 9)(5, 6) + >>> ~r*p*r == r*p*~r + False + + The conjugate ~r*p*r was chosen so that ``p^q^r`` would be equivalent + to ``p^(q*r)`` rather than ``p^(r*q)``. To obtain r*p*~r, pass ~r to + this method: + + >>> p^~r == r*p*~r + True + """ + + if self.size != h.size: + raise ValueError("The permutations must be of equal size.") + a = [None]*self.size + h = h._array_form + p = self._array_form + for i in range(self.size): + a[h[i]] = h[p[i]] + return self._af_new(a) + + def transpositions(self): + """ + Return the permutation decomposed into a list of transpositions. + + Explanation + =========== + + It is always possible to express a permutation as the product of + transpositions, see [1] + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([[1, 2, 3], [0, 4, 5, 6, 7]]) + >>> t = p.transpositions() + >>> t + [(0, 7), (0, 6), (0, 5), (0, 4), (1, 3), (1, 2)] + >>> print(''.join(str(c) for c in t)) + (0, 7)(0, 6)(0, 5)(0, 4)(1, 3)(1, 2) + >>> Permutation.rmul(*[Permutation([ti], size=p.size) for ti in t]) == p + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Transposition_%28mathematics%29#Properties + + """ + a = self.cyclic_form + res = [] + for x in a: + nx = len(x) + if nx == 2: + res.append(tuple(x)) + elif nx > 2: + first = x[0] + res.extend((first, y) for y in x[nx - 1:0:-1]) + return res + + @classmethod + def from_sequence(self, i, key=None): + """Return the permutation needed to obtain ``i`` from the sorted + elements of ``i``. If custom sorting is desired, a key can be given. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + + >>> Permutation.from_sequence('SymPy') + (4)(0 1 3) + >>> _(sorted("SymPy")) + ['S', 'y', 'm', 'P', 'y'] + >>> Permutation.from_sequence('SymPy', key=lambda x: x.lower()) + (4)(0 2)(1 3) + """ + ic = list(zip(i, list(range(len(i))))) + if key: + ic.sort(key=lambda x: key(x[0])) + else: + ic.sort() + return ~Permutation([i[1] for i in ic]) + + def __invert__(self): + """ + Return the inverse of the permutation. + + A permutation multiplied by its inverse is the identity permutation. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + >>> p = Permutation([[2, 0], [3, 1]]) + >>> ~p + Permutation([2, 3, 0, 1]) + >>> _ == p**-1 + True + >>> p*~p == ~p*p == Permutation([0, 1, 2, 3]) + True + """ + return self._af_new(_af_invert(self._array_form)) + + def __iter__(self): + """Yield elements from array form. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> list(Permutation(range(3))) + [0, 1, 2] + """ + yield from self.array_form + + def __repr__(self): + return srepr(self) + + def __call__(self, *i): + """ + Allows applying a permutation instance as a bijective function. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([[2, 0], [3, 1]]) + >>> p.array_form + [2, 3, 0, 1] + >>> [p(i) for i in range(4)] + [2, 3, 0, 1] + + If an array is given then the permutation selects the items + from the array (i.e. the permutation is applied to the array): + + >>> from sympy.abc import x + >>> p([x, 1, 0, x**2]) + [0, x**2, x, 1] + """ + # list indices can be Integer or int; leave this + # as it is (don't test or convert it) because this + # gets called a lot and should be fast + if len(i) == 1: + i = i[0] + if not isinstance(i, Iterable): + i = as_int(i) + if i < 0 or i > self.size: + raise TypeError( + "{} should be an integer between 0 and {}" + .format(i, self.size-1)) + return self._array_form[i] + # P([a, b, c]) + if len(i) != self.size: + raise TypeError( + "{} should have the length {}.".format(i, self.size)) + return [i[j] for j in self._array_form] + # P(1, 2, 3) + return self*Permutation(Cycle(*i), size=self.size) + + def atoms(self): + """ + Returns all the elements of a permutation + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> Permutation([0, 1, 2, 3, 4, 5]).atoms() + {0, 1, 2, 3, 4, 5} + >>> Permutation([[0, 1], [2, 3], [4, 5]]).atoms() + {0, 1, 2, 3, 4, 5} + """ + return set(self.array_form) + + def apply(self, i): + r"""Apply the permutation to an expression. + + Parameters + ========== + + i : Expr + It should be an integer between $0$ and $n-1$ where $n$ + is the size of the permutation. + + If it is a symbol or a symbolic expression that can + have integer values, an ``AppliedPermutation`` object + will be returned which can represent an unevaluated + function. + + Notes + ===== + + Any permutation can be defined as a bijective function + $\sigma : \{ 0, 1, \dots, n-1 \} \rightarrow \{ 0, 1, \dots, n-1 \}$ + where $n$ denotes the size of the permutation. + + The definition may even be extended for any set with distinctive + elements, such that the permutation can even be applied for + real numbers or such, however, it is not implemented for now for + computational reasons and the integrity with the group theory + module. + + This function is similar to the ``__call__`` magic, however, + ``__call__`` magic already has some other applications like + permuting an array or attaching new cycles, which would + not always be mathematically consistent. + + This also guarantees that the return type is a SymPy integer, + which guarantees the safety to use assumptions. + """ + i = _sympify(i) + if i.is_integer is False: + raise NotImplementedError("{} should be an integer.".format(i)) + + n = self.size + if (i < 0) == True or (i >= n) == True: + raise NotImplementedError( + "{} should be an integer between 0 and {}".format(i, n-1)) + + if i.is_Integer: + return Integer(self._array_form[i]) + return AppliedPermutation(self, i) + + def next_lex(self): + """ + Returns the next permutation in lexicographical order. + If self is the last permutation in lexicographical order + it returns None. + See [4] section 2.4. + + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([2, 3, 1, 0]) + >>> p = Permutation([2, 3, 1, 0]); p.rank() + 17 + >>> p = p.next_lex(); p.rank() + 18 + + See Also + ======== + + rank, unrank_lex + """ + perm = self.array_form[:] + n = len(perm) + i = n - 2 + while perm[i + 1] < perm[i]: + i -= 1 + if i == -1: + return None + else: + j = n - 1 + while perm[j] < perm[i]: + j -= 1 + perm[j], perm[i] = perm[i], perm[j] + i += 1 + j = n - 1 + while i < j: + perm[j], perm[i] = perm[i], perm[j] + i += 1 + j -= 1 + return self._af_new(perm) + + @classmethod + def unrank_nonlex(self, n, r): + """ + This is a linear time unranking algorithm that does not + respect lexicographic order [3]. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + >>> Permutation.unrank_nonlex(4, 5) + Permutation([2, 0, 3, 1]) + >>> Permutation.unrank_nonlex(4, -1) + Permutation([0, 1, 2, 3]) + + See Also + ======== + + next_nonlex, rank_nonlex + """ + def _unrank1(n, r, a): + if n > 0: + a[n - 1], a[r % n] = a[r % n], a[n - 1] + _unrank1(n - 1, r//n, a) + + id_perm = list(range(n)) + n = int(n) + r = r % ifac(n) + _unrank1(n, r, id_perm) + return self._af_new(id_perm) + + def rank_nonlex(self, inv_perm=None): + """ + This is a linear time ranking algorithm that does not + enforce lexicographic order [3]. + + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([0, 1, 2, 3]) + >>> p.rank_nonlex() + 23 + + See Also + ======== + + next_nonlex, unrank_nonlex + """ + def _rank1(n, perm, inv_perm): + if n == 1: + return 0 + s = perm[n - 1] + t = inv_perm[n - 1] + perm[n - 1], perm[t] = perm[t], s + inv_perm[n - 1], inv_perm[s] = inv_perm[s], t + return s + n*_rank1(n - 1, perm, inv_perm) + + if inv_perm is None: + inv_perm = (~self).array_form + if not inv_perm: + return 0 + perm = self.array_form[:] + r = _rank1(len(perm), perm, inv_perm) + return r + + def next_nonlex(self): + """ + Returns the next permutation in nonlex order [3]. + If self is the last permutation in this order it returns None. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + >>> p = Permutation([2, 0, 3, 1]); p.rank_nonlex() + 5 + >>> p = p.next_nonlex(); p + Permutation([3, 0, 1, 2]) + >>> p.rank_nonlex() + 6 + + See Also + ======== + + rank_nonlex, unrank_nonlex + """ + r = self.rank_nonlex() + if r == ifac(self.size) - 1: + return None + return self.unrank_nonlex(self.size, r + 1) + + def rank(self): + """ + Returns the lexicographic rank of the permutation. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([0, 1, 2, 3]) + >>> p.rank() + 0 + >>> p = Permutation([3, 2, 1, 0]) + >>> p.rank() + 23 + + See Also + ======== + + next_lex, unrank_lex, cardinality, length, order, size + """ + if self._rank is not None: + return self._rank + rank = 0 + rho = self.array_form[:] + n = self.size - 1 + size = n + 1 + psize = int(ifac(n)) + for j in range(size - 1): + rank += rho[j]*psize + for i in range(j + 1, size): + if rho[i] > rho[j]: + rho[i] -= 1 + psize //= n + n -= 1 + self._rank = rank + return rank + + @property + def cardinality(self): + """ + Returns the number of all possible permutations. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([0, 1, 2, 3]) + >>> p.cardinality + 24 + + See Also + ======== + + length, order, rank, size + """ + return int(ifac(self.size)) + + def parity(self): + """ + Computes the parity of a permutation. + + Explanation + =========== + + The parity of a permutation reflects the parity of the + number of inversions in the permutation, i.e., the + number of pairs of x and y such that ``x > y`` but ``p[x] < p[y]``. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([0, 1, 2, 3]) + >>> p.parity() + 0 + >>> p = Permutation([3, 2, 0, 1]) + >>> p.parity() + 1 + + See Also + ======== + + _af_parity + """ + if self._cyclic_form is not None: + return (self.size - self.cycles) % 2 + + return _af_parity(self.array_form) + + @property + def is_even(self): + """ + Checks if a permutation is even. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([0, 1, 2, 3]) + >>> p.is_even + True + >>> p = Permutation([3, 2, 1, 0]) + >>> p.is_even + True + + See Also + ======== + + is_odd + """ + return not self.is_odd + + @property + def is_odd(self): + """ + Checks if a permutation is odd. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([0, 1, 2, 3]) + >>> p.is_odd + False + >>> p = Permutation([3, 2, 0, 1]) + >>> p.is_odd + True + + See Also + ======== + + is_even + """ + return bool(self.parity() % 2) + + @property + def is_Singleton(self): + """ + Checks to see if the permutation contains only one number and is + thus the only possible permutation of this set of numbers + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> Permutation([0]).is_Singleton + True + >>> Permutation([0, 1]).is_Singleton + False + + See Also + ======== + + is_Empty + """ + return self.size == 1 + + @property + def is_Empty(self): + """ + Checks to see if the permutation is a set with zero elements + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> Permutation([]).is_Empty + True + >>> Permutation([0]).is_Empty + False + + See Also + ======== + + is_Singleton + """ + return self.size == 0 + + @property + def is_identity(self): + return self.is_Identity + + @property + def is_Identity(self): + """ + Returns True if the Permutation is an identity permutation. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([]) + >>> p.is_Identity + True + >>> p = Permutation([[0], [1], [2]]) + >>> p.is_Identity + True + >>> p = Permutation([0, 1, 2]) + >>> p.is_Identity + True + >>> p = Permutation([0, 2, 1]) + >>> p.is_Identity + False + + See Also + ======== + + order + """ + af = self.array_form + return not af or all(i == af[i] for i in range(self.size)) + + def ascents(self): + """ + Returns the positions of ascents in a permutation, ie, the location + where p[i] < p[i+1] + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([4, 0, 1, 3, 2]) + >>> p.ascents() + [1, 2] + + See Also + ======== + + descents, inversions, min, max + """ + a = self.array_form + pos = [i for i in range(len(a) - 1) if a[i] < a[i + 1]] + return pos + + def descents(self): + """ + Returns the positions of descents in a permutation, ie, the location + where p[i] > p[i+1] + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([4, 0, 1, 3, 2]) + >>> p.descents() + [0, 3] + + See Also + ======== + + ascents, inversions, min, max + """ + a = self.array_form + pos = [i for i in range(len(a) - 1) if a[i] > a[i + 1]] + return pos + + def max(self) -> int: + """ + The maximum element moved by the permutation. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([1, 0, 2, 3, 4]) + >>> p.max() + 1 + + See Also + ======== + + min, descents, ascents, inversions + """ + a = self.array_form + if not a: + return 0 + return max(_a for i, _a in enumerate(a) if _a != i) + + def min(self) -> int: + """ + The minimum element moved by the permutation. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([0, 1, 4, 3, 2]) + >>> p.min() + 2 + + See Also + ======== + + max, descents, ascents, inversions + """ + a = self.array_form + if not a: + return 0 + return min(_a for i, _a in enumerate(a) if _a != i) + + def inversions(self): + """ + Computes the number of inversions of a permutation. + + Explanation + =========== + + An inversion is where i > j but p[i] < p[j]. + + For small length of p, it iterates over all i and j + values and calculates the number of inversions. + For large length of p, it uses a variation of merge + sort to calculate the number of inversions. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([0, 1, 2, 3, 4, 5]) + >>> p.inversions() + 0 + >>> Permutation([3, 2, 1, 0]).inversions() + 6 + + See Also + ======== + + descents, ascents, min, max + + References + ========== + + .. [1] https://www.cp.eng.chula.ac.th/~prabhas//teaching/algo/algo2008/count-inv.htm + + """ + inversions = 0 + a = self.array_form + n = len(a) + if n < 130: + for i in range(n - 1): + b = a[i] + for c in a[i + 1:]: + if b > c: + inversions += 1 + else: + k = 1 + right = 0 + arr = a[:] + temp = a[:] + while k < n: + i = 0 + while i + k < n: + right = i + k * 2 - 1 + if right >= n: + right = n - 1 + inversions += _merge(arr, temp, i, i + k, right) + i = i + k * 2 + k = k * 2 + return inversions + + def commutator(self, x): + """Return the commutator of ``self`` and ``x``: ``~x*~self*x*self`` + + If f and g are part of a group, G, then the commutator of f and g + is the group identity iff f and g commute, i.e. fg == gf. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + >>> p = Permutation([0, 2, 3, 1]) + >>> x = Permutation([2, 0, 3, 1]) + >>> c = p.commutator(x); c + Permutation([2, 1, 3, 0]) + >>> c == ~x*~p*x*p + True + + >>> I = Permutation(3) + >>> p = [I + i for i in range(6)] + >>> for i in range(len(p)): + ... for j in range(len(p)): + ... c = p[i].commutator(p[j]) + ... if p[i]*p[j] == p[j]*p[i]: + ... assert c == I + ... else: + ... assert c != I + ... + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Commutator + """ + + a = self.array_form + b = x.array_form + n = len(a) + if len(b) != n: + raise ValueError("The permutations must be of equal size.") + inva = [None]*n + for i in range(n): + inva[a[i]] = i + invb = [None]*n + for i in range(n): + invb[b[i]] = i + return self._af_new([a[b[inva[i]]] for i in invb]) + + def signature(self): + """ + Gives the signature of the permutation needed to place the + elements of the permutation in canonical order. + + The signature is calculated as (-1)^ + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([0, 1, 2]) + >>> p.inversions() + 0 + >>> p.signature() + 1 + >>> q = Permutation([0,2,1]) + >>> q.inversions() + 1 + >>> q.signature() + -1 + + See Also + ======== + + inversions + """ + if self.is_even: + return 1 + return -1 + + def order(self): + """ + Computes the order of a permutation. + + When the permutation is raised to the power of its + order it equals the identity permutation. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + >>> p = Permutation([3, 1, 5, 2, 4, 0]) + >>> p.order() + 4 + >>> (p**(p.order())) + Permutation([], size=6) + + See Also + ======== + + identity, cardinality, length, rank, size + """ + + return reduce(lcm, [len(cycle) for cycle in self.cyclic_form], 1) + + def length(self): + """ + Returns the number of integers moved by a permutation. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> Permutation([0, 3, 2, 1]).length() + 2 + >>> Permutation([[0, 1], [2, 3]]).length() + 4 + + See Also + ======== + + min, max, support, cardinality, order, rank, size + """ + + return len(self.support()) + + @property + def cycle_structure(self): + """Return the cycle structure of the permutation as a dictionary + indicating the multiplicity of each cycle length. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> Permutation(3).cycle_structure + {1: 4} + >>> Permutation(0, 4, 3)(1, 2)(5, 6).cycle_structure + {2: 2, 3: 1} + """ + if self._cycle_structure: + rv = self._cycle_structure + else: + rv = defaultdict(int) + singletons = self.size + for c in self.cyclic_form: + rv[len(c)] += 1 + singletons -= len(c) + if singletons: + rv[1] = singletons + self._cycle_structure = rv + return dict(rv) # make a copy + + @property + def cycles(self): + """ + Returns the number of cycles contained in the permutation + (including singletons). + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> Permutation([0, 1, 2]).cycles + 3 + >>> Permutation([0, 1, 2]).full_cyclic_form + [[0], [1], [2]] + >>> Permutation(0, 1)(2, 3).cycles + 2 + + See Also + ======== + sympy.functions.combinatorial.numbers.stirling + """ + return len(self.full_cyclic_form) + + def index(self): + """ + Returns the index of a permutation. + + The index of a permutation is the sum of all subscripts j such + that p[j] is greater than p[j+1]. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([3, 0, 2, 1, 4]) + >>> p.index() + 2 + """ + a = self.array_form + + return sum(j for j in range(len(a) - 1) if a[j] > a[j + 1]) + + def runs(self): + """ + Returns the runs of a permutation. + + An ascending sequence in a permutation is called a run [5]. + + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([2, 5, 7, 3, 6, 0, 1, 4, 8]) + >>> p.runs() + [[2, 5, 7], [3, 6], [0, 1, 4, 8]] + >>> q = Permutation([1,3,2,0]) + >>> q.runs() + [[1, 3], [2], [0]] + """ + return runs(self.array_form) + + def inversion_vector(self): + """Return the inversion vector of the permutation. + + The inversion vector consists of elements whose value + indicates the number of elements in the permutation + that are lesser than it and lie on its right hand side. + + The inversion vector is the same as the Lehmer encoding of a + permutation. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([4, 8, 0, 7, 1, 5, 3, 6, 2]) + >>> p.inversion_vector() + [4, 7, 0, 5, 0, 2, 1, 1] + >>> p = Permutation([3, 2, 1, 0]) + >>> p.inversion_vector() + [3, 2, 1] + + The inversion vector increases lexicographically with the rank + of the permutation, the -ith element cycling through 0..i. + + >>> p = Permutation(2) + >>> while p: + ... print('%s %s %s' % (p, p.inversion_vector(), p.rank())) + ... p = p.next_lex() + (2) [0, 0] 0 + (1 2) [0, 1] 1 + (2)(0 1) [1, 0] 2 + (0 1 2) [1, 1] 3 + (0 2 1) [2, 0] 4 + (0 2) [2, 1] 5 + + See Also + ======== + + from_inversion_vector + """ + self_array_form = self.array_form + n = len(self_array_form) + inversion_vector = [0] * (n - 1) + + for i in range(n - 1): + val = 0 + for j in range(i + 1, n): + if self_array_form[j] < self_array_form[i]: + val += 1 + inversion_vector[i] = val + return inversion_vector + + def rank_trotterjohnson(self): + """ + Returns the Trotter Johnson rank, which we get from the minimal + change algorithm. See [4] section 2.4. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([0, 1, 2, 3]) + >>> p.rank_trotterjohnson() + 0 + >>> p = Permutation([0, 2, 1, 3]) + >>> p.rank_trotterjohnson() + 7 + + See Also + ======== + + unrank_trotterjohnson, next_trotterjohnson + """ + if self.array_form == [] or self.is_Identity: + return 0 + if self.array_form == [1, 0]: + return 1 + perm = self.array_form + n = self.size + rank = 0 + for j in range(1, n): + k = 1 + i = 0 + while perm[i] != j: + if perm[i] < j: + k += 1 + i += 1 + j1 = j + 1 + if rank % 2 == 0: + rank = j1*rank + j1 - k + else: + rank = j1*rank + k - 1 + return rank + + @classmethod + def unrank_trotterjohnson(cls, size, rank): + """ + Trotter Johnson permutation unranking. See [4] section 2.4. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + >>> Permutation.unrank_trotterjohnson(5, 10) + Permutation([0, 3, 1, 2, 4]) + + See Also + ======== + + rank_trotterjohnson, next_trotterjohnson + """ + perm = [0]*size + r2 = 0 + n = ifac(size) + pj = 1 + for j in range(2, size + 1): + pj *= j + r1 = (rank * pj) // n + k = r1 - j*r2 + if r2 % 2 == 0: + for i in range(j - 1, j - k - 1, -1): + perm[i] = perm[i - 1] + perm[j - k - 1] = j - 1 + else: + for i in range(j - 1, k, -1): + perm[i] = perm[i - 1] + perm[k] = j - 1 + r2 = r1 + return cls._af_new(perm) + + def next_trotterjohnson(self): + """ + Returns the next permutation in Trotter-Johnson order. + If self is the last permutation it returns None. + See [4] section 2.4. If it is desired to generate all such + permutations, they can be generated in order more quickly + with the ``generate_bell`` function. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + >>> p = Permutation([3, 0, 2, 1]) + >>> p.rank_trotterjohnson() + 4 + >>> p = p.next_trotterjohnson(); p + Permutation([0, 3, 2, 1]) + >>> p.rank_trotterjohnson() + 5 + + See Also + ======== + + rank_trotterjohnson, unrank_trotterjohnson, sympy.utilities.iterables.generate_bell + """ + pi = self.array_form[:] + n = len(pi) + st = 0 + rho = pi[:] + done = False + m = n-1 + while m > 0 and not done: + d = rho.index(m) + for i in range(d, m): + rho[i] = rho[i + 1] + par = _af_parity(rho[:m]) + if par == 1: + if d == m: + m -= 1 + else: + pi[st + d], pi[st + d + 1] = pi[st + d + 1], pi[st + d] + done = True + else: + if d == 0: + m -= 1 + st += 1 + else: + pi[st + d], pi[st + d - 1] = pi[st + d - 1], pi[st + d] + done = True + if m == 0: + return None + return self._af_new(pi) + + def get_precedence_matrix(self): + """ + Gets the precedence matrix. This is used for computing the + distance between two permutations. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + >>> p = Permutation.josephus(3, 6, 1) + >>> p + Permutation([2, 5, 3, 1, 4, 0]) + >>> p.get_precedence_matrix() + Matrix([ + [0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 1, 0], + [1, 1, 0, 1, 1, 1], + [1, 1, 0, 0, 1, 0], + [1, 0, 0, 0, 0, 0], + [1, 1, 0, 1, 1, 0]]) + + See Also + ======== + + get_precedence_distance, get_adjacency_matrix, get_adjacency_distance + """ + m = zeros(self.size) + perm = self.array_form + for i in range(m.rows): + for j in range(i + 1, m.cols): + m[perm[i], perm[j]] = 1 + return m + + def get_precedence_distance(self, other): + """ + Computes the precedence distance between two permutations. + + Explanation + =========== + + Suppose p and p' represent n jobs. The precedence metric + counts the number of times a job j is preceded by job i + in both p and p'. This metric is commutative. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([2, 0, 4, 3, 1]) + >>> q = Permutation([3, 1, 2, 4, 0]) + >>> p.get_precedence_distance(q) + 7 + >>> q.get_precedence_distance(p) + 7 + + See Also + ======== + + get_precedence_matrix, get_adjacency_matrix, get_adjacency_distance + """ + if self.size != other.size: + raise ValueError("The permutations must be of equal size.") + self_prec_mat = self.get_precedence_matrix() + other_prec_mat = other.get_precedence_matrix() + n_prec = 0 + for i in range(self.size): + for j in range(self.size): + if i == j: + continue + if self_prec_mat[i, j] * other_prec_mat[i, j] == 1: + n_prec += 1 + d = self.size * (self.size - 1)//2 - n_prec + return d + + def get_adjacency_matrix(self): + """ + Computes the adjacency matrix of a permutation. + + Explanation + =========== + + If job i is adjacent to job j in a permutation p + then we set m[i, j] = 1 where m is the adjacency + matrix of p. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation.josephus(3, 6, 1) + >>> p.get_adjacency_matrix() + Matrix([ + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1], + [0, 1, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0]]) + >>> q = Permutation([0, 1, 2, 3]) + >>> q.get_adjacency_matrix() + Matrix([ + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + [0, 0, 0, 0]]) + + See Also + ======== + + get_precedence_matrix, get_precedence_distance, get_adjacency_distance + """ + m = zeros(self.size) + perm = self.array_form + for i in range(self.size - 1): + m[perm[i], perm[i + 1]] = 1 + return m + + def get_adjacency_distance(self, other): + """ + Computes the adjacency distance between two permutations. + + Explanation + =========== + + This metric counts the number of times a pair i,j of jobs is + adjacent in both p and p'. If n_adj is this quantity then + the adjacency distance is n - n_adj - 1 [1] + + [1] Reeves, Colin R. Landscapes, Operators and Heuristic search, Annals + of Operational Research, 86, pp 473-490. (1999) + + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([0, 3, 1, 2, 4]) + >>> q = Permutation.josephus(4, 5, 2) + >>> p.get_adjacency_distance(q) + 3 + >>> r = Permutation([0, 2, 1, 4, 3]) + >>> p.get_adjacency_distance(r) + 4 + + See Also + ======== + + get_precedence_matrix, get_precedence_distance, get_adjacency_matrix + """ + if self.size != other.size: + raise ValueError("The permutations must be of the same size.") + self_adj_mat = self.get_adjacency_matrix() + other_adj_mat = other.get_adjacency_matrix() + n_adj = 0 + for i in range(self.size): + for j in range(self.size): + if i == j: + continue + if self_adj_mat[i, j] * other_adj_mat[i, j] == 1: + n_adj += 1 + d = self.size - n_adj - 1 + return d + + def get_positional_distance(self, other): + """ + Computes the positional distance between two permutations. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> p = Permutation([0, 3, 1, 2, 4]) + >>> q = Permutation.josephus(4, 5, 2) + >>> r = Permutation([3, 1, 4, 0, 2]) + >>> p.get_positional_distance(q) + 12 + >>> p.get_positional_distance(r) + 12 + + See Also + ======== + + get_precedence_distance, get_adjacency_distance + """ + a = self.array_form + b = other.array_form + if len(a) != len(b): + raise ValueError("The permutations must be of the same size.") + return sum(abs(a[i] - b[i]) for i in range(len(a))) + + @classmethod + def josephus(cls, m, n, s=1): + """Return as a permutation the shuffling of range(n) using the Josephus + scheme in which every m-th item is selected until all have been chosen. + The returned permutation has elements listed by the order in which they + were selected. + + The parameter ``s`` stops the selection process when there are ``s`` + items remaining and these are selected by continuing the selection, + counting by 1 rather than by ``m``. + + Consider selecting every 3rd item from 6 until only 2 remain:: + + choices chosen + ======== ====== + 012345 + 01 345 2 + 01 34 25 + 01 4 253 + 0 4 2531 + 0 25314 + 253140 + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> Permutation.josephus(3, 6, 2).array_form + [2, 5, 3, 1, 4, 0] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Flavius_Josephus + .. [2] https://en.wikipedia.org/wiki/Josephus_problem + .. [3] https://web.archive.org/web/20171008094331/http://www.wou.edu/~burtonl/josephus.html + + """ + from collections import deque + m -= 1 + Q = deque(list(range(n))) + perm = [] + while len(Q) > max(s, 1): + for dp in range(m): + Q.append(Q.popleft()) + perm.append(Q.popleft()) + perm.extend(list(Q)) + return cls(perm) + + @classmethod + def from_inversion_vector(cls, inversion): + """ + Calculates the permutation from the inversion vector. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + >>> Permutation.from_inversion_vector([3, 2, 1, 0, 0]) + Permutation([3, 2, 1, 0, 4, 5]) + + """ + size = len(inversion) + N = list(range(size + 1)) + perm = [] + try: + for k in range(size): + val = N[inversion[k]] + perm.append(val) + N.remove(val) + except IndexError: + raise ValueError("The inversion vector is not valid.") + perm.extend(N) + return cls._af_new(perm) + + @classmethod + def random(cls, n): + """ + Generates a random permutation of length ``n``. + + Uses the underlying Python pseudo-random number generator. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> Permutation.random(2) in (Permutation([1, 0]), Permutation([0, 1])) + True + + """ + perm_array = list(range(n)) + random.shuffle(perm_array) + return cls._af_new(perm_array) + + @classmethod + def unrank_lex(cls, size, rank): + """ + Lexicographic permutation unranking. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy import init_printing + >>> init_printing(perm_cyclic=False, pretty_print=False) + >>> a = Permutation.unrank_lex(5, 10) + >>> a.rank() + 10 + >>> a + Permutation([0, 2, 4, 1, 3]) + + See Also + ======== + + rank, next_lex + """ + perm_array = [0] * size + psize = 1 + for i in range(size): + new_psize = psize*(i + 1) + d = (rank % new_psize) // psize + rank -= d*psize + perm_array[size - i - 1] = d + for j in range(size - i, size): + if perm_array[j] > d - 1: + perm_array[j] += 1 + psize = new_psize + return cls._af_new(perm_array) + + def resize(self, n): + """Resize the permutation to the new size ``n``. + + Parameters + ========== + + n : int + The new size of the permutation. + + Raises + ====== + + ValueError + If the permutation cannot be resized to the given size. + This may only happen when resized to a smaller size than + the original. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + + Increasing the size of a permutation: + + >>> p = Permutation(0, 1, 2) + >>> p = p.resize(5) + >>> p + (4)(0 1 2) + + Decreasing the size of the permutation: + + >>> p = p.resize(4) + >>> p + (3)(0 1 2) + + If resizing to the specific size breaks the cycles: + + >>> p.resize(2) + Traceback (most recent call last): + ... + ValueError: The permutation cannot be resized to 2 because the + cycle (0, 1, 2) may break. + """ + aform = self.array_form + l = len(aform) + if n > l: + aform += list(range(l, n)) + return Permutation._af_new(aform) + + elif n < l: + cyclic_form = self.full_cyclic_form + new_cyclic_form = [] + for cycle in cyclic_form: + cycle_min = min(cycle) + cycle_max = max(cycle) + if cycle_min <= n-1: + if cycle_max > n-1: + raise ValueError( + "The permutation cannot be resized to {} " + "because the cycle {} may break." + .format(n, tuple(cycle))) + + new_cyclic_form.append(cycle) + return Permutation(new_cyclic_form) + + return self + + # XXX Deprecated flag + print_cyclic = None + + +def _merge(arr, temp, left, mid, right): + """ + Merges two sorted arrays and calculates the inversion count. + + Helper function for calculating inversions. This method is + for internal use only. + """ + i = k = left + j = mid + inv_count = 0 + while i < mid and j <= right: + if arr[i] < arr[j]: + temp[k] = arr[i] + k += 1 + i += 1 + else: + temp[k] = arr[j] + k += 1 + j += 1 + inv_count += (mid -i) + while i < mid: + temp[k] = arr[i] + k += 1 + i += 1 + if j <= right: + k += right - j + 1 + j += right - j + 1 + arr[left:k + 1] = temp[left:k + 1] + else: + arr[left:right + 1] = temp[left:right + 1] + return inv_count + +Perm = Permutation +_af_new = Perm._af_new + + +class AppliedPermutation(Expr): + """A permutation applied to a symbolic variable. + + Parameters + ========== + + perm : Permutation + x : Expr + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.combinatorics import Permutation + + Creating a symbolic permutation function application: + + >>> x = Symbol('x') + >>> p = Permutation(0, 1, 2) + >>> p.apply(x) + AppliedPermutation((0 1 2), x) + >>> _.subs(x, 1) + 2 + """ + def __new__(cls, perm, x, evaluate=None): + if evaluate is None: + evaluate = global_parameters.evaluate + + perm = _sympify(perm) + x = _sympify(x) + + if not isinstance(perm, Permutation): + raise ValueError("{} must be a Permutation instance." + .format(perm)) + + if evaluate: + if x.is_Integer: + return perm.apply(x) + + obj = super().__new__(cls, perm, x) + return obj + + +@dispatch(Permutation, Permutation) +def _eval_is_eq(lhs, rhs): + if lhs._size != rhs._size: + return None + return lhs._array_form == rhs._array_form diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/polyhedron.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/polyhedron.py new file mode 100644 index 0000000000000000000000000000000000000000..2bc05d7d97c840649661f3290442499c841ca7c1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/polyhedron.py @@ -0,0 +1,1019 @@ +from sympy.combinatorics import Permutation as Perm +from sympy.combinatorics.perm_groups import PermutationGroup +from sympy.core import Basic, Tuple, default_sort_key +from sympy.sets import FiniteSet +from sympy.utilities.iterables import (minlex, unflatten, flatten) +from sympy.utilities.misc import as_int + +rmul = Perm.rmul + + +class Polyhedron(Basic): + """ + Represents the polyhedral symmetry group (PSG). + + Explanation + =========== + + The PSG is one of the symmetry groups of the Platonic solids. + There are three polyhedral groups: the tetrahedral group + of order 12, the octahedral group of order 24, and the + icosahedral group of order 60. + + All doctests have been given in the docstring of the + constructor of the object. + + References + ========== + + .. [1] https://mathworld.wolfram.com/PolyhedralGroup.html + + """ + _edges = None + + def __new__(cls, corners, faces=(), pgroup=()): + """ + The constructor of the Polyhedron group object. + + Explanation + =========== + + It takes up to three parameters: the corners, faces, and + allowed transformations. + + The corners/vertices are entered as a list of arbitrary + expressions that are used to identify each vertex. + + The faces are entered as a list of tuples of indices; a tuple + of indices identifies the vertices which define the face. They + should be entered in a cw or ccw order; they will be standardized + by reversal and rotation to be give the lowest lexical ordering. + If no faces are given then no edges will be computed. + + >>> from sympy.combinatorics.polyhedron import Polyhedron + >>> Polyhedron(list('abc'), [(1, 2, 0)]).faces + {(0, 1, 2)} + >>> Polyhedron(list('abc'), [(1, 0, 2)]).faces + {(0, 1, 2)} + + The allowed transformations are entered as allowable permutations + of the vertices for the polyhedron. Instance of Permutations + (as with faces) should refer to the supplied vertices by index. + These permutation are stored as a PermutationGroup. + + Examples + ======== + + >>> from sympy.combinatorics.permutations import Permutation + >>> from sympy import init_printing + >>> from sympy.abc import w, x, y, z + >>> init_printing(pretty_print=False, perm_cyclic=False) + + Here we construct the Polyhedron object for a tetrahedron. + + >>> corners = [w, x, y, z] + >>> faces = [(0, 1, 2), (0, 2, 3), (0, 3, 1), (1, 2, 3)] + + Next, allowed transformations of the polyhedron must be given. This + is given as permutations of vertices. + + Although the vertices of a tetrahedron can be numbered in 24 (4!) + different ways, there are only 12 different orientations for a + physical tetrahedron. The following permutations, applied once or + twice, will generate all 12 of the orientations. (The identity + permutation, Permutation(range(4)), is not included since it does + not change the orientation of the vertices.) + + >>> pgroup = [Permutation([[0, 1, 2], [3]]), \ + Permutation([[0, 1, 3], [2]]), \ + Permutation([[0, 2, 3], [1]]), \ + Permutation([[1, 2, 3], [0]]), \ + Permutation([[0, 1], [2, 3]]), \ + Permutation([[0, 2], [1, 3]]), \ + Permutation([[0, 3], [1, 2]])] + + The Polyhedron is now constructed and demonstrated: + + >>> tetra = Polyhedron(corners, faces, pgroup) + >>> tetra.size + 4 + >>> tetra.edges + {(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)} + >>> tetra.corners + (w, x, y, z) + + It can be rotated with an arbitrary permutation of vertices, e.g. + the following permutation is not in the pgroup: + + >>> tetra.rotate(Permutation([0, 1, 3, 2])) + >>> tetra.corners + (w, x, z, y) + + An allowed permutation of the vertices can be constructed by + repeatedly applying permutations from the pgroup to the vertices. + Here is a demonstration that applying p and p**2 for every p in + pgroup generates all the orientations of a tetrahedron and no others: + + >>> all = ( (w, x, y, z), \ + (x, y, w, z), \ + (y, w, x, z), \ + (w, z, x, y), \ + (z, w, y, x), \ + (w, y, z, x), \ + (y, z, w, x), \ + (x, z, y, w), \ + (z, y, x, w), \ + (y, x, z, w), \ + (x, w, z, y), \ + (z, x, w, y) ) + + >>> got = [] + >>> for p in (pgroup + [p**2 for p in pgroup]): + ... h = Polyhedron(corners) + ... h.rotate(p) + ... got.append(h.corners) + ... + >>> set(got) == set(all) + True + + The make_perm method of a PermutationGroup will randomly pick + permutations, multiply them together, and return the permutation that + can be applied to the polyhedron to give the orientation produced + by those individual permutations. + + Here, 3 permutations are used: + + >>> tetra.pgroup.make_perm(3) # doctest: +SKIP + Permutation([0, 3, 1, 2]) + + To select the permutations that should be used, supply a list + of indices to the permutations in pgroup in the order they should + be applied: + + >>> use = [0, 0, 2] + >>> p002 = tetra.pgroup.make_perm(3, use) + >>> p002 + Permutation([1, 0, 3, 2]) + + + Apply them one at a time: + + >>> tetra.reset() + >>> for i in use: + ... tetra.rotate(pgroup[i]) + ... + >>> tetra.vertices + (x, w, z, y) + >>> sequentially = tetra.vertices + + Apply the composite permutation: + + >>> tetra.reset() + >>> tetra.rotate(p002) + >>> tetra.corners + (x, w, z, y) + >>> tetra.corners in all and tetra.corners == sequentially + True + + Notes + ===== + + Defining permutation groups + --------------------------- + + It is not necessary to enter any permutations, nor is necessary to + enter a complete set of transformations. In fact, for a polyhedron, + all configurations can be constructed from just two permutations. + For example, the orientations of a tetrahedron can be generated from + an axis passing through a vertex and face and another axis passing + through a different vertex or from an axis passing through the + midpoints of two edges opposite of each other. + + For simplicity of presentation, consider a square -- + not a cube -- with vertices 1, 2, 3, and 4: + + 1-----2 We could think of axes of rotation being: + | | 1) through the face + | | 2) from midpoint 1-2 to 3-4 or 1-3 to 2-4 + 3-----4 3) lines 1-4 or 2-3 + + + To determine how to write the permutations, imagine 4 cameras, + one at each corner, labeled A-D: + + A B A B + 1-----2 1-----3 vertex index: + | | | | 1 0 + | | | | 2 1 + 3-----4 2-----4 3 2 + C D C D 4 3 + + original after rotation + along 1-4 + + A diagonal and a face axis will be chosen for the "permutation group" + from which any orientation can be constructed. + + >>> pgroup = [] + + Imagine a clockwise rotation when viewing 1-4 from camera A. The new + orientation is (in camera-order): 1, 3, 2, 4 so the permutation is + given using the *indices* of the vertices as: + + >>> pgroup.append(Permutation((0, 2, 1, 3))) + + Now imagine rotating clockwise when looking down an axis entering the + center of the square as viewed. The new camera-order would be + 3, 1, 4, 2 so the permutation is (using indices): + + >>> pgroup.append(Permutation((2, 0, 3, 1))) + + The square can now be constructed: + ** use real-world labels for the vertices, entering them in + camera order + ** for the faces we use zero-based indices of the vertices + in *edge-order* as the face is traversed; neither the + direction nor the starting point matter -- the faces are + only used to define edges (if so desired). + + >>> square = Polyhedron((1, 2, 3, 4), [(0, 1, 3, 2)], pgroup) + + To rotate the square with a single permutation we can do: + + >>> square.rotate(square.pgroup[0]) + >>> square.corners + (1, 3, 2, 4) + + To use more than one permutation (or to use one permutation more + than once) it is more convenient to use the make_perm method: + + >>> p011 = square.pgroup.make_perm([0, 1, 1]) # diag flip + 2 rotations + >>> square.reset() # return to initial orientation + >>> square.rotate(p011) + >>> square.corners + (4, 2, 3, 1) + + Thinking outside the box + ------------------------ + + Although the Polyhedron object has a direct physical meaning, it + actually has broader application. In the most general sense it is + just a decorated PermutationGroup, allowing one to connect the + permutations to something physical. For example, a Rubik's cube is + not a proper polyhedron, but the Polyhedron class can be used to + represent it in a way that helps to visualize the Rubik's cube. + + >>> from sympy import flatten, unflatten, symbols + >>> from sympy.combinatorics import RubikGroup + >>> facelets = flatten([symbols(s+'1:5') for s in 'UFRBLD']) + >>> def show(): + ... pairs = unflatten(r2.corners, 2) + ... print(pairs[::2]) + ... print(pairs[1::2]) + ... + >>> r2 = Polyhedron(facelets, pgroup=RubikGroup(2)) + >>> show() + [(U1, U2), (F1, F2), (R1, R2), (B1, B2), (L1, L2), (D1, D2)] + [(U3, U4), (F3, F4), (R3, R4), (B3, B4), (L3, L4), (D3, D4)] + >>> r2.rotate(0) # cw rotation of F + >>> show() + [(U1, U2), (F3, F1), (U3, R2), (B1, B2), (L1, D1), (R3, R1)] + [(L4, L2), (F4, F2), (U4, R4), (B3, B4), (L3, D2), (D3, D4)] + + Predefined Polyhedra + ==================== + + For convenience, the vertices and faces are defined for the following + standard solids along with a permutation group for transformations. + When the polyhedron is oriented as indicated below, the vertices in + a given horizontal plane are numbered in ccw direction, starting from + the vertex that will give the lowest indices in a given face. (In the + net of the vertices, indices preceded by "-" indicate replication of + the lhs index in the net.) + + tetrahedron, tetrahedron_faces + ------------------------------ + + 4 vertices (vertex up) net: + + 0 0-0 + 1 2 3-1 + + 4 faces: + + (0, 1, 2) (0, 2, 3) (0, 3, 1) (1, 2, 3) + + cube, cube_faces + ---------------- + + 8 vertices (face up) net: + + 0 1 2 3-0 + 4 5 6 7-4 + + 6 faces: + + (0, 1, 2, 3) + (0, 1, 5, 4) (1, 2, 6, 5) (2, 3, 7, 6) (0, 3, 7, 4) + (4, 5, 6, 7) + + octahedron, octahedron_faces + ---------------------------- + + 6 vertices (vertex up) net: + + 0 0 0-0 + 1 2 3 4-1 + 5 5 5-5 + + 8 faces: + + (0, 1, 2) (0, 2, 3) (0, 3, 4) (0, 1, 4) + (1, 2, 5) (2, 3, 5) (3, 4, 5) (1, 4, 5) + + dodecahedron, dodecahedron_faces + -------------------------------- + + 20 vertices (vertex up) net: + + 0 1 2 3 4 -0 + 5 6 7 8 9 -5 + 14 10 11 12 13-14 + 15 16 17 18 19-15 + + 12 faces: + + (0, 1, 2, 3, 4) (0, 1, 6, 10, 5) (1, 2, 7, 11, 6) + (2, 3, 8, 12, 7) (3, 4, 9, 13, 8) (0, 4, 9, 14, 5) + (5, 10, 16, 15, 14) (6, 10, 16, 17, 11) (7, 11, 17, 18, 12) + (8, 12, 18, 19, 13) (9, 13, 19, 15, 14)(15, 16, 17, 18, 19) + + icosahedron, icosahedron_faces + ------------------------------ + + 12 vertices (face up) net: + + 0 0 0 0 -0 + 1 2 3 4 5 -1 + 6 7 8 9 10 -6 + 11 11 11 11 -11 + + 20 faces: + + (0, 1, 2) (0, 2, 3) (0, 3, 4) + (0, 4, 5) (0, 1, 5) (1, 2, 6) + (2, 3, 7) (3, 4, 8) (4, 5, 9) + (1, 5, 10) (2, 6, 7) (3, 7, 8) + (4, 8, 9) (5, 9, 10) (1, 6, 10) + (6, 7, 11) (7, 8, 11) (8, 9, 11) + (9, 10, 11) (6, 10, 11) + + >>> from sympy.combinatorics.polyhedron import cube + >>> cube.edges + {(0, 1), (0, 3), (0, 4), (1, 2), (1, 5), (2, 3), (2, 6), (3, 7), (4, 5), (4, 7), (5, 6), (6, 7)} + + If you want to use letters or other names for the corners you + can still use the pre-calculated faces: + + >>> corners = list('abcdefgh') + >>> Polyhedron(corners, cube.faces).corners + (a, b, c, d, e, f, g, h) + + References + ========== + + .. [1] www.ocf.berkeley.edu/~wwu/articles/platonicsolids.pdf + + """ + faces = [minlex(f, directed=False, key=default_sort_key) for f in faces] + corners, faces, pgroup = args = \ + [Tuple(*a) for a in (corners, faces, pgroup)] + obj = Basic.__new__(cls, *args) + obj._corners = tuple(corners) # in order given + obj._faces = FiniteSet(*faces) + if pgroup and pgroup[0].size != len(corners): + raise ValueError("Permutation size unequal to number of corners.") + # use the identity permutation if none are given + obj._pgroup = PermutationGroup( + pgroup or [Perm(range(len(corners)))] ) + return obj + + @property + def corners(self): + """ + Get the corners of the Polyhedron. + + The method ``vertices`` is an alias for ``corners``. + + Examples + ======== + + >>> from sympy.combinatorics import Polyhedron + >>> from sympy.abc import a, b, c, d + >>> p = Polyhedron(list('abcd')) + >>> p.corners == p.vertices == (a, b, c, d) + True + + See Also + ======== + + array_form, cyclic_form + """ + return self._corners + vertices = corners + + @property + def array_form(self): + """Return the indices of the corners. + + The indices are given relative to the original position of corners. + + Examples + ======== + + >>> from sympy.combinatorics.polyhedron import tetrahedron + >>> tetrahedron = tetrahedron.copy() + >>> tetrahedron.array_form + [0, 1, 2, 3] + + >>> tetrahedron.rotate(0) + >>> tetrahedron.array_form + [0, 2, 3, 1] + >>> tetrahedron.pgroup[0].array_form + [0, 2, 3, 1] + + See Also + ======== + + corners, cyclic_form + """ + corners = list(self.args[0]) + return [corners.index(c) for c in self.corners] + + @property + def cyclic_form(self): + """Return the indices of the corners in cyclic notation. + + The indices are given relative to the original position of corners. + + See Also + ======== + + corners, array_form + """ + return Perm._af_new(self.array_form).cyclic_form + + @property + def size(self): + """ + Get the number of corners of the Polyhedron. + """ + return len(self._corners) + + @property + def faces(self): + """ + Get the faces of the Polyhedron. + """ + return self._faces + + @property + def pgroup(self): + """ + Get the permutations of the Polyhedron. + """ + return self._pgroup + + @property + def edges(self): + """ + Given the faces of the polyhedra we can get the edges. + + Examples + ======== + + >>> from sympy.combinatorics import Polyhedron + >>> from sympy.abc import a, b, c + >>> corners = (a, b, c) + >>> faces = [(0, 1, 2)] + >>> Polyhedron(corners, faces).edges + {(0, 1), (0, 2), (1, 2)} + + """ + if self._edges is None: + output = set() + for face in self.faces: + for i in range(len(face)): + edge = tuple(sorted([face[i], face[i - 1]])) + output.add(edge) + self._edges = FiniteSet(*output) + return self._edges + + def rotate(self, perm): + """ + Apply a permutation to the polyhedron *in place*. The permutation + may be given as a Permutation instance or an integer indicating + which permutation from pgroup of the Polyhedron should be + applied. + + This is an operation that is analogous to rotation about + an axis by a fixed increment. + + Notes + ===== + + When a Permutation is applied, no check is done to see if that + is a valid permutation for the Polyhedron. For example, a cube + could be given a permutation which effectively swaps only 2 + vertices. A valid permutation (that rotates the object in a + physical way) will be obtained if one only uses + permutations from the ``pgroup`` of the Polyhedron. On the other + hand, allowing arbitrary rotations (applications of permutations) + gives a way to follow named elements rather than indices since + Polyhedron allows vertices to be named while Permutation works + only with indices. + + Examples + ======== + + >>> from sympy.combinatorics import Polyhedron, Permutation + >>> from sympy.combinatorics.polyhedron import cube + >>> cube = cube.copy() + >>> cube.corners + (0, 1, 2, 3, 4, 5, 6, 7) + >>> cube.rotate(0) + >>> cube.corners + (1, 2, 3, 0, 5, 6, 7, 4) + + A non-physical "rotation" that is not prohibited by this method: + + >>> cube.reset() + >>> cube.rotate(Permutation([[1, 2]], size=8)) + >>> cube.corners + (0, 2, 1, 3, 4, 5, 6, 7) + + Polyhedron can be used to follow elements of set that are + identified by letters instead of integers: + + >>> shadow = h5 = Polyhedron(list('abcde')) + >>> p = Permutation([3, 0, 1, 2, 4]) + >>> h5.rotate(p) + >>> h5.corners + (d, a, b, c, e) + >>> _ == shadow.corners + True + >>> copy = h5.copy() + >>> h5.rotate(p) + >>> h5.corners == copy.corners + False + """ + if not isinstance(perm, Perm): + perm = self.pgroup[perm] + # and we know it's valid + else: + if perm.size != self.size: + raise ValueError('Polyhedron and Permutation sizes differ.') + a = perm.array_form + corners = [self.corners[a[i]] for i in range(len(self.corners))] + self._corners = tuple(corners) + + def reset(self): + """Return corners to their original positions. + + Examples + ======== + + >>> from sympy.combinatorics.polyhedron import tetrahedron as T + >>> T = T.copy() + >>> T.corners + (0, 1, 2, 3) + >>> T.rotate(0) + >>> T.corners + (0, 2, 3, 1) + >>> T.reset() + >>> T.corners + (0, 1, 2, 3) + """ + self._corners = self.args[0] + + +def _pgroup_calcs(): + """Return the permutation groups for each of the polyhedra and the face + definitions: tetrahedron, cube, octahedron, dodecahedron, icosahedron, + tetrahedron_faces, cube_faces, octahedron_faces, dodecahedron_faces, + icosahedron_faces + + Explanation + =========== + + (This author did not find and did not know of a better way to do it though + there likely is such a way.) + + Although only 2 permutations are needed for a polyhedron in order to + generate all the possible orientations, a group of permutations is + provided instead. A set of permutations is called a "group" if:: + + a*b = c (for any pair of permutations in the group, a and b, their + product, c, is in the group) + + a*(b*c) = (a*b)*c (for any 3 permutations in the group associativity holds) + + there is an identity permutation, I, such that I*a = a*I for all elements + in the group + + a*b = I (the inverse of each permutation is also in the group) + + None of the polyhedron groups defined follow these definitions of a group. + Instead, they are selected to contain those permutations whose powers + alone will construct all orientations of the polyhedron, i.e. for + permutations ``a``, ``b``, etc... in the group, ``a, a**2, ..., a**o_a``, + ``b, b**2, ..., b**o_b``, etc... (where ``o_i`` is the order of + permutation ``i``) generate all permutations of the polyhedron instead of + mixed products like ``a*b``, ``a*b**2``, etc.... + + Note that for a polyhedron with n vertices, the valid permutations of the + vertices exclude those that do not maintain its faces. e.g. the + permutation BCDE of a square's four corners, ABCD, is a valid + permutation while CBDE is not (because this would twist the square). + + Examples + ======== + + The is_group checks for: closure, the presence of the Identity permutation, + and the presence of the inverse for each of the elements in the group. This + confirms that none of the polyhedra are true groups: + + >>> from sympy.combinatorics.polyhedron import ( + ... tetrahedron, cube, octahedron, dodecahedron, icosahedron) + ... + >>> polyhedra = (tetrahedron, cube, octahedron, dodecahedron, icosahedron) + >>> [h.pgroup.is_group for h in polyhedra] + ... + [True, True, True, True, True] + + Although tests in polyhedron's test suite check that powers of the + permutations in the groups generate all permutations of the vertices + of the polyhedron, here we also demonstrate the powers of the given + permutations create a complete group for the tetrahedron: + + >>> from sympy.combinatorics import Permutation, PermutationGroup + >>> for h in polyhedra[:1]: + ... G = h.pgroup + ... perms = set() + ... for g in G: + ... for e in range(g.order()): + ... p = tuple((g**e).array_form) + ... perms.add(p) + ... + ... perms = [Permutation(p) for p in perms] + ... assert PermutationGroup(perms).is_group + + In addition to doing the above, the tests in the suite confirm that the + faces are all present after the application of each permutation. + + References + ========== + + .. [1] https://dogschool.tripod.com/trianglegroup.html + + """ + def _pgroup_of_double(polyh, ordered_faces, pgroup): + n = len(ordered_faces[0]) + # the vertices of the double which sits inside a give polyhedron + # can be found by tracking the faces of the outer polyhedron. + # A map between face and the vertex of the double is made so that + # after rotation the position of the vertices can be located + fmap = dict(zip(ordered_faces, + range(len(ordered_faces)))) + flat_faces = flatten(ordered_faces) + new_pgroup = [] + for p in pgroup: + h = polyh.copy() + h.rotate(p) + c = h.corners + # reorder corners in the order they should appear when + # enumerating the faces + reorder = unflatten([c[j] for j in flat_faces], n) + # make them canonical + reorder = [tuple(map(as_int, + minlex(f, directed=False))) + for f in reorder] + # map face to vertex: the resulting list of vertices are the + # permutation that we seek for the double + new_pgroup.append(Perm([fmap[f] for f in reorder])) + return new_pgroup + + tetrahedron_faces = [ + (0, 1, 2), (0, 2, 3), (0, 3, 1), # upper 3 + (1, 2, 3), # bottom + ] + + # cw from top + # + _t_pgroup = [ + Perm([[1, 2, 3], [0]]), # cw from top + Perm([[0, 1, 2], [3]]), # cw from front face + Perm([[0, 3, 2], [1]]), # cw from back right face + Perm([[0, 3, 1], [2]]), # cw from back left face + Perm([[0, 1], [2, 3]]), # through front left edge + Perm([[0, 2], [1, 3]]), # through front right edge + Perm([[0, 3], [1, 2]]), # through back edge + ] + + tetrahedron = Polyhedron( + range(4), + tetrahedron_faces, + _t_pgroup) + + cube_faces = [ + (0, 1, 2, 3), # upper + (0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (0, 3, 7, 4), # middle 4 + (4, 5, 6, 7), # lower + ] + + # U, D, F, B, L, R = up, down, front, back, left, right + _c_pgroup = [Perm(p) for p in + [ + [1, 2, 3, 0, 5, 6, 7, 4], # cw from top, U + [4, 0, 3, 7, 5, 1, 2, 6], # cw from F face + [4, 5, 1, 0, 7, 6, 2, 3], # cw from R face + + [1, 0, 4, 5, 2, 3, 7, 6], # cw through UF edge + [6, 2, 1, 5, 7, 3, 0, 4], # cw through UR edge + [6, 7, 3, 2, 5, 4, 0, 1], # cw through UB edge + [3, 7, 4, 0, 2, 6, 5, 1], # cw through UL edge + [4, 7, 6, 5, 0, 3, 2, 1], # cw through FL edge + [6, 5, 4, 7, 2, 1, 0, 3], # cw through FR edge + + [0, 3, 7, 4, 1, 2, 6, 5], # cw through UFL vertex + [5, 1, 0, 4, 6, 2, 3, 7], # cw through UFR vertex + [5, 6, 2, 1, 4, 7, 3, 0], # cw through UBR vertex + [7, 4, 0, 3, 6, 5, 1, 2], # cw through UBL + ]] + + cube = Polyhedron( + range(8), + cube_faces, + _c_pgroup) + + octahedron_faces = [ + (0, 1, 2), (0, 2, 3), (0, 3, 4), (0, 1, 4), # top 4 + (1, 2, 5), (2, 3, 5), (3, 4, 5), (1, 4, 5), # bottom 4 + ] + + octahedron = Polyhedron( + range(6), + octahedron_faces, + _pgroup_of_double(cube, cube_faces, _c_pgroup)) + + dodecahedron_faces = [ + (0, 1, 2, 3, 4), # top + (0, 1, 6, 10, 5), (1, 2, 7, 11, 6), (2, 3, 8, 12, 7), # upper 5 + (3, 4, 9, 13, 8), (0, 4, 9, 14, 5), + (5, 10, 16, 15, 14), (6, 10, 16, 17, 11), (7, 11, 17, 18, + 12), # lower 5 + (8, 12, 18, 19, 13), (9, 13, 19, 15, 14), + (15, 16, 17, 18, 19) # bottom + ] + + def _string_to_perm(s): + rv = [Perm(range(20))] + p = None + for si in s: + if si not in '01': + count = int(si) - 1 + else: + count = 1 + if si == '0': + p = _f0 + elif si == '1': + p = _f1 + rv.extend([p]*count) + return Perm.rmul(*rv) + + # top face cw + _f0 = Perm([ + 1, 2, 3, 4, 0, 6, 7, 8, 9, 5, 11, + 12, 13, 14, 10, 16, 17, 18, 19, 15]) + # front face cw + _f1 = Perm([ + 5, 0, 4, 9, 14, 10, 1, 3, 13, 15, + 6, 2, 8, 19, 16, 17, 11, 7, 12, 18]) + # the strings below, like 0104 are shorthand for F0*F1*F0**4 and are + # the remaining 4 face rotations, 15 edge permutations, and the + # 10 vertex rotations. + _dodeca_pgroup = [_f0, _f1] + [_string_to_perm(s) for s in ''' + 0104 140 014 0410 + 010 1403 03104 04103 102 + 120 1304 01303 021302 03130 + 0412041 041204103 04120410 041204104 041204102 + 10 01 1402 0140 04102 0412 1204 1302 0130 03120'''.strip().split()] + + dodecahedron = Polyhedron( + range(20), + dodecahedron_faces, + _dodeca_pgroup) + + icosahedron_faces = [ + (0, 1, 2), (0, 2, 3), (0, 3, 4), (0, 4, 5), (0, 1, 5), + (1, 6, 7), (1, 2, 7), (2, 7, 8), (2, 3, 8), (3, 8, 9), + (3, 4, 9), (4, 9, 10), (4, 5, 10), (5, 6, 10), (1, 5, 6), + (6, 7, 11), (7, 8, 11), (8, 9, 11), (9, 10, 11), (6, 10, 11)] + + icosahedron = Polyhedron( + range(12), + icosahedron_faces, + _pgroup_of_double( + dodecahedron, dodecahedron_faces, _dodeca_pgroup)) + + return (tetrahedron, cube, octahedron, dodecahedron, icosahedron, + tetrahedron_faces, cube_faces, octahedron_faces, + dodecahedron_faces, icosahedron_faces) + +# ----------------------------------------------------------------------- +# Standard Polyhedron groups +# +# These are generated using _pgroup_calcs() above. However to save +# import time we encode them explicitly here. +# ----------------------------------------------------------------------- + +tetrahedron = Polyhedron( + Tuple(0, 1, 2, 3), + Tuple( + Tuple(0, 1, 2), + Tuple(0, 2, 3), + Tuple(0, 1, 3), + Tuple(1, 2, 3)), + Tuple( + Perm(1, 2, 3), + Perm(3)(0, 1, 2), + Perm(0, 3, 2), + Perm(0, 3, 1), + Perm(0, 1)(2, 3), + Perm(0, 2)(1, 3), + Perm(0, 3)(1, 2) + )) + +cube = Polyhedron( + Tuple(0, 1, 2, 3, 4, 5, 6, 7), + Tuple( + Tuple(0, 1, 2, 3), + Tuple(0, 1, 5, 4), + Tuple(1, 2, 6, 5), + Tuple(2, 3, 7, 6), + Tuple(0, 3, 7, 4), + Tuple(4, 5, 6, 7)), + Tuple( + Perm(0, 1, 2, 3)(4, 5, 6, 7), + Perm(0, 4, 5, 1)(2, 3, 7, 6), + Perm(0, 4, 7, 3)(1, 5, 6, 2), + Perm(0, 1)(2, 4)(3, 5)(6, 7), + Perm(0, 6)(1, 2)(3, 5)(4, 7), + Perm(0, 6)(1, 7)(2, 3)(4, 5), + Perm(0, 3)(1, 7)(2, 4)(5, 6), + Perm(0, 4)(1, 7)(2, 6)(3, 5), + Perm(0, 6)(1, 5)(2, 4)(3, 7), + Perm(1, 3, 4)(2, 7, 5), + Perm(7)(0, 5, 2)(3, 4, 6), + Perm(0, 5, 7)(1, 6, 3), + Perm(0, 7, 2)(1, 4, 6))) + +octahedron = Polyhedron( + Tuple(0, 1, 2, 3, 4, 5), + Tuple( + Tuple(0, 1, 2), + Tuple(0, 2, 3), + Tuple(0, 3, 4), + Tuple(0, 1, 4), + Tuple(1, 2, 5), + Tuple(2, 3, 5), + Tuple(3, 4, 5), + Tuple(1, 4, 5)), + Tuple( + Perm(5)(1, 2, 3, 4), + Perm(0, 4, 5, 2), + Perm(0, 1, 5, 3), + Perm(0, 1)(2, 4)(3, 5), + Perm(0, 2)(1, 3)(4, 5), + Perm(0, 3)(1, 5)(2, 4), + Perm(0, 4)(1, 3)(2, 5), + Perm(0, 5)(1, 4)(2, 3), + Perm(0, 5)(1, 2)(3, 4), + Perm(0, 4, 1)(2, 3, 5), + Perm(0, 1, 2)(3, 4, 5), + Perm(0, 2, 3)(1, 5, 4), + Perm(0, 4, 3)(1, 5, 2))) + +dodecahedron = Polyhedron( + Tuple(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19), + Tuple( + Tuple(0, 1, 2, 3, 4), + Tuple(0, 1, 6, 10, 5), + Tuple(1, 2, 7, 11, 6), + Tuple(2, 3, 8, 12, 7), + Tuple(3, 4, 9, 13, 8), + Tuple(0, 4, 9, 14, 5), + Tuple(5, 10, 16, 15, 14), + Tuple(6, 10, 16, 17, 11), + Tuple(7, 11, 17, 18, 12), + Tuple(8, 12, 18, 19, 13), + Tuple(9, 13, 19, 15, 14), + Tuple(15, 16, 17, 18, 19)), + Tuple( + Perm(0, 1, 2, 3, 4)(5, 6, 7, 8, 9)(10, 11, 12, 13, 14)(15, 16, 17, 18, 19), + Perm(0, 5, 10, 6, 1)(2, 4, 14, 16, 11)(3, 9, 15, 17, 7)(8, 13, 19, 18, 12), + Perm(0, 10, 17, 12, 3)(1, 6, 11, 7, 2)(4, 5, 16, 18, 8)(9, 14, 15, 19, 13), + Perm(0, 6, 17, 19, 9)(1, 11, 18, 13, 4)(2, 7, 12, 8, 3)(5, 10, 16, 15, 14), + Perm(0, 2, 12, 19, 14)(1, 7, 18, 15, 5)(3, 8, 13, 9, 4)(6, 11, 17, 16, 10), + Perm(0, 4, 9, 14, 5)(1, 3, 13, 15, 10)(2, 8, 19, 16, 6)(7, 12, 18, 17, 11), + Perm(0, 1)(2, 5)(3, 10)(4, 6)(7, 14)(8, 16)(9, 11)(12, 15)(13, 17)(18, 19), + Perm(0, 7)(1, 2)(3, 6)(4, 11)(5, 12)(8, 10)(9, 17)(13, 16)(14, 18)(15, 19), + Perm(0, 12)(1, 8)(2, 3)(4, 7)(5, 18)(6, 13)(9, 11)(10, 19)(14, 17)(15, 16), + Perm(0, 8)(1, 13)(2, 9)(3, 4)(5, 12)(6, 19)(7, 14)(10, 18)(11, 15)(16, 17), + Perm(0, 4)(1, 9)(2, 14)(3, 5)(6, 13)(7, 15)(8, 10)(11, 19)(12, 16)(17, 18), + Perm(0, 5)(1, 14)(2, 15)(3, 16)(4, 10)(6, 9)(7, 19)(8, 17)(11, 13)(12, 18), + Perm(0, 11)(1, 6)(2, 10)(3, 16)(4, 17)(5, 7)(8, 15)(9, 18)(12, 14)(13, 19), + Perm(0, 18)(1, 12)(2, 7)(3, 11)(4, 17)(5, 19)(6, 8)(9, 16)(10, 13)(14, 15), + Perm(0, 18)(1, 19)(2, 13)(3, 8)(4, 12)(5, 17)(6, 15)(7, 9)(10, 16)(11, 14), + Perm(0, 13)(1, 19)(2, 15)(3, 14)(4, 9)(5, 8)(6, 18)(7, 16)(10, 12)(11, 17), + Perm(0, 16)(1, 15)(2, 19)(3, 18)(4, 17)(5, 10)(6, 14)(7, 13)(8, 12)(9, 11), + Perm(0, 18)(1, 17)(2, 16)(3, 15)(4, 19)(5, 12)(6, 11)(7, 10)(8, 14)(9, 13), + Perm(0, 15)(1, 19)(2, 18)(3, 17)(4, 16)(5, 14)(6, 13)(7, 12)(8, 11)(9, 10), + Perm(0, 17)(1, 16)(2, 15)(3, 19)(4, 18)(5, 11)(6, 10)(7, 14)(8, 13)(9, 12), + Perm(0, 19)(1, 18)(2, 17)(3, 16)(4, 15)(5, 13)(6, 12)(7, 11)(8, 10)(9, 14), + Perm(1, 4, 5)(2, 9, 10)(3, 14, 6)(7, 13, 16)(8, 15, 11)(12, 19, 17), + Perm(19)(0, 6, 2)(3, 5, 11)(4, 10, 7)(8, 14, 17)(9, 16, 12)(13, 15, 18), + Perm(0, 11, 8)(1, 7, 3)(4, 6, 12)(5, 17, 13)(9, 10, 18)(14, 16, 19), + Perm(0, 7, 13)(1, 12, 9)(2, 8, 4)(5, 11, 19)(6, 18, 14)(10, 17, 15), + Perm(0, 3, 9)(1, 8, 14)(2, 13, 5)(6, 12, 15)(7, 19, 10)(11, 18, 16), + Perm(0, 14, 10)(1, 9, 16)(2, 13, 17)(3, 19, 11)(4, 15, 6)(7, 8, 18), + Perm(0, 16, 7)(1, 10, 11)(2, 5, 17)(3, 14, 18)(4, 15, 12)(8, 9, 19), + Perm(0, 16, 13)(1, 17, 8)(2, 11, 12)(3, 6, 18)(4, 10, 19)(5, 15, 9), + Perm(0, 11, 15)(1, 17, 14)(2, 18, 9)(3, 12, 13)(4, 7, 19)(5, 6, 16), + Perm(0, 8, 15)(1, 12, 16)(2, 18, 10)(3, 19, 5)(4, 13, 14)(6, 7, 17))) + +icosahedron = Polyhedron( + Tuple(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), + Tuple( + Tuple(0, 1, 2), + Tuple(0, 2, 3), + Tuple(0, 3, 4), + Tuple(0, 4, 5), + Tuple(0, 1, 5), + Tuple(1, 6, 7), + Tuple(1, 2, 7), + Tuple(2, 7, 8), + Tuple(2, 3, 8), + Tuple(3, 8, 9), + Tuple(3, 4, 9), + Tuple(4, 9, 10), + Tuple(4, 5, 10), + Tuple(5, 6, 10), + Tuple(1, 5, 6), + Tuple(6, 7, 11), + Tuple(7, 8, 11), + Tuple(8, 9, 11), + Tuple(9, 10, 11), + Tuple(6, 10, 11)), + Tuple( + Perm(11)(1, 2, 3, 4, 5)(6, 7, 8, 9, 10), + Perm(0, 5, 6, 7, 2)(3, 4, 10, 11, 8), + Perm(0, 1, 7, 8, 3)(4, 5, 6, 11, 9), + Perm(0, 2, 8, 9, 4)(1, 7, 11, 10, 5), + Perm(0, 3, 9, 10, 5)(1, 2, 8, 11, 6), + Perm(0, 4, 10, 6, 1)(2, 3, 9, 11, 7), + Perm(0, 1)(2, 5)(3, 6)(4, 7)(8, 10)(9, 11), + Perm(0, 2)(1, 3)(4, 7)(5, 8)(6, 9)(10, 11), + Perm(0, 3)(1, 9)(2, 4)(5, 8)(6, 11)(7, 10), + Perm(0, 4)(1, 9)(2, 10)(3, 5)(6, 8)(7, 11), + Perm(0, 5)(1, 4)(2, 10)(3, 6)(7, 9)(8, 11), + Perm(0, 6)(1, 5)(2, 10)(3, 11)(4, 7)(8, 9), + Perm(0, 7)(1, 2)(3, 6)(4, 11)(5, 8)(9, 10), + Perm(0, 8)(1, 9)(2, 3)(4, 7)(5, 11)(6, 10), + Perm(0, 9)(1, 11)(2, 10)(3, 4)(5, 8)(6, 7), + Perm(0, 10)(1, 9)(2, 11)(3, 6)(4, 5)(7, 8), + Perm(0, 11)(1, 6)(2, 10)(3, 9)(4, 8)(5, 7), + Perm(0, 11)(1, 8)(2, 7)(3, 6)(4, 10)(5, 9), + Perm(0, 11)(1, 10)(2, 9)(3, 8)(4, 7)(5, 6), + Perm(0, 11)(1, 7)(2, 6)(3, 10)(4, 9)(5, 8), + Perm(0, 11)(1, 9)(2, 8)(3, 7)(4, 6)(5, 10), + Perm(0, 5, 1)(2, 4, 6)(3, 10, 7)(8, 9, 11), + Perm(0, 1, 2)(3, 5, 7)(4, 6, 8)(9, 10, 11), + Perm(0, 2, 3)(1, 8, 4)(5, 7, 9)(6, 11, 10), + Perm(0, 3, 4)(1, 8, 10)(2, 9, 5)(6, 7, 11), + Perm(0, 4, 5)(1, 3, 10)(2, 9, 6)(7, 8, 11), + Perm(0, 10, 7)(1, 5, 6)(2, 4, 11)(3, 9, 8), + Perm(0, 6, 8)(1, 7, 2)(3, 5, 11)(4, 10, 9), + Perm(0, 7, 9)(1, 11, 4)(2, 8, 3)(5, 6, 10), + Perm(0, 8, 10)(1, 7, 6)(2, 11, 5)(3, 9, 4), + Perm(0, 9, 6)(1, 3, 11)(2, 8, 7)(4, 10, 5))) + +tetrahedron_faces = [tuple(arg) for arg in tetrahedron.faces] + +cube_faces = [tuple(arg) for arg in cube.faces] + +octahedron_faces = [tuple(arg) for arg in octahedron.faces] + +dodecahedron_faces = [tuple(arg) for arg in dodecahedron.faces] + +icosahedron_faces = [tuple(arg) for arg in icosahedron.faces] diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/prufer.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/prufer.py new file mode 100644 index 0000000000000000000000000000000000000000..e389df87cddc0152b2376e18b9f3df2e94d3d2fb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/prufer.py @@ -0,0 +1,435 @@ +from sympy.core import Basic +from sympy.core.containers import Tuple +from sympy.tensor.array import Array +from sympy.core.sympify import _sympify +from sympy.utilities.iterables import flatten, iterable +from sympy.utilities.misc import as_int + +from collections import defaultdict + + +class Prufer(Basic): + """ + The Prufer correspondence is an algorithm that describes the + bijection between labeled trees and the Prufer code. A Prufer + code of a labeled tree is unique up to isomorphism and has + a length of n - 2. + + Prufer sequences were first used by Heinz Prufer to give a + proof of Cayley's formula. + + References + ========== + + .. [1] https://mathworld.wolfram.com/LabeledTree.html + + """ + _prufer_repr = None + _tree_repr = None + _nodes = None + _rank = None + + @property + def prufer_repr(self): + """Returns Prufer sequence for the Prufer object. + + This sequence is found by removing the highest numbered vertex, + recording the node it was attached to, and continuing until only + two vertices remain. The Prufer sequence is the list of recorded nodes. + + Examples + ======== + + >>> from sympy.combinatorics.prufer import Prufer + >>> Prufer([[0, 3], [1, 3], [2, 3], [3, 4], [4, 5]]).prufer_repr + [3, 3, 3, 4] + >>> Prufer([1, 0, 0]).prufer_repr + [1, 0, 0] + + See Also + ======== + + to_prufer + + """ + if self._prufer_repr is None: + self._prufer_repr = self.to_prufer(self._tree_repr[:], self.nodes) + return self._prufer_repr + + @property + def tree_repr(self): + """Returns the tree representation of the Prufer object. + + Examples + ======== + + >>> from sympy.combinatorics.prufer import Prufer + >>> Prufer([[0, 3], [1, 3], [2, 3], [3, 4], [4, 5]]).tree_repr + [[0, 3], [1, 3], [2, 3], [3, 4], [4, 5]] + >>> Prufer([1, 0, 0]).tree_repr + [[1, 2], [0, 1], [0, 3], [0, 4]] + + See Also + ======== + + to_tree + + """ + if self._tree_repr is None: + self._tree_repr = self.to_tree(self._prufer_repr[:]) + return self._tree_repr + + @property + def nodes(self): + """Returns the number of nodes in the tree. + + Examples + ======== + + >>> from sympy.combinatorics.prufer import Prufer + >>> Prufer([[0, 3], [1, 3], [2, 3], [3, 4], [4, 5]]).nodes + 6 + >>> Prufer([1, 0, 0]).nodes + 5 + + """ + return self._nodes + + @property + def rank(self): + """Returns the rank of the Prufer sequence. + + Examples + ======== + + >>> from sympy.combinatorics.prufer import Prufer + >>> p = Prufer([[0, 3], [1, 3], [2, 3], [3, 4], [4, 5]]) + >>> p.rank + 778 + >>> p.next(1).rank + 779 + >>> p.prev().rank + 777 + + See Also + ======== + + prufer_rank, next, prev, size + + """ + if self._rank is None: + self._rank = self.prufer_rank() + return self._rank + + @property + def size(self): + """Return the number of possible trees of this Prufer object. + + Examples + ======== + + >>> from sympy.combinatorics.prufer import Prufer + >>> Prufer([0]*4).size == Prufer([6]*4).size == 1296 + True + + See Also + ======== + + prufer_rank, rank, next, prev + + """ + return self.prev(self.rank).prev().rank + 1 + + @staticmethod + def to_prufer(tree, n): + """Return the Prufer sequence for a tree given as a list of edges where + ``n`` is the number of nodes in the tree. + + Examples + ======== + + >>> from sympy.combinatorics.prufer import Prufer + >>> a = Prufer([[0, 1], [0, 2], [0, 3]]) + >>> a.prufer_repr + [0, 0] + >>> Prufer.to_prufer([[0, 1], [0, 2], [0, 3]], 4) + [0, 0] + + See Also + ======== + prufer_repr: returns Prufer sequence of a Prufer object. + + """ + d = defaultdict(int) + L = [] + for edge in tree: + # Increment the value of the corresponding + # node in the degree list as we encounter an + # edge involving it. + d[edge[0]] += 1 + d[edge[1]] += 1 + for i in range(n - 2): + # find the smallest leaf + for x in range(n): + if d[x] == 1: + break + # find the node it was connected to + y = None + for edge in tree: + if x == edge[0]: + y = edge[1] + elif x == edge[1]: + y = edge[0] + if y is not None: + break + # record and update + L.append(y) + for j in (x, y): + d[j] -= 1 + if not d[j]: + d.pop(j) + tree.remove(edge) + return L + + @staticmethod + def to_tree(prufer): + """Return the tree (as a list of edges) of the given Prufer sequence. + + Examples + ======== + + >>> from sympy.combinatorics.prufer import Prufer + >>> a = Prufer([0, 2], 4) + >>> a.tree_repr + [[0, 1], [0, 2], [2, 3]] + >>> Prufer.to_tree([0, 2]) + [[0, 1], [0, 2], [2, 3]] + + References + ========== + + .. [1] https://hamberg.no/erlend/posts/2010-11-06-prufer-sequence-compact-tree-representation.html + + See Also + ======== + tree_repr: returns tree representation of a Prufer object. + + """ + tree = [] + last = [] + n = len(prufer) + 2 + d = defaultdict(lambda: 1) + for p in prufer: + d[p] += 1 + for i in prufer: + for j in range(n): + # find the smallest leaf (degree = 1) + if d[j] == 1: + break + # (i, j) is the new edge that we append to the tree + # and remove from the degree dictionary + d[i] -= 1 + d[j] -= 1 + tree.append(sorted([i, j])) + last = [i for i in range(n) if d[i] == 1] or [0, 1] + tree.append(last) + + return tree + + @staticmethod + def edges(*runs): + """Return a list of edges and the number of nodes from the given runs + that connect nodes in an integer-labelled tree. + + All node numbers will be shifted so that the minimum node is 0. It is + not a problem if edges are repeated in the runs; only unique edges are + returned. There is no assumption made about what the range of the node + labels should be, but all nodes from the smallest through the largest + must be present. + + Examples + ======== + + >>> from sympy.combinatorics.prufer import Prufer + >>> Prufer.edges([1, 2, 3], [2, 4, 5]) # a T + ([[0, 1], [1, 2], [1, 3], [3, 4]], 5) + + Duplicate edges are removed: + + >>> Prufer.edges([0, 1, 2, 3], [1, 4, 5], [1, 4, 6]) # a K + ([[0, 1], [1, 2], [1, 4], [2, 3], [4, 5], [4, 6]], 7) + + """ + e = set() + nmin = runs[0][0] + for r in runs: + for i in range(len(r) - 1): + a, b = r[i: i + 2] + if b < a: + a, b = b, a + e.add((a, b)) + rv = [] + got = set() + nmin = nmax = None + for ei in e: + got.update(ei) + nmin = min(ei[0], nmin) if nmin is not None else ei[0] + nmax = max(ei[1], nmax) if nmax is not None else ei[1] + rv.append(list(ei)) + missing = set(range(nmin, nmax + 1)) - got + if missing: + missing = [i + nmin for i in missing] + if len(missing) == 1: + msg = 'Node %s is missing.' % missing.pop() + else: + msg = 'Nodes %s are missing.' % sorted(missing) + raise ValueError(msg) + if nmin != 0: + for i, ei in enumerate(rv): + rv[i] = [n - nmin for n in ei] + nmax -= nmin + return sorted(rv), nmax + 1 + + def prufer_rank(self): + """Computes the rank of a Prufer sequence. + + Examples + ======== + + >>> from sympy.combinatorics.prufer import Prufer + >>> a = Prufer([[0, 1], [0, 2], [0, 3]]) + >>> a.prufer_rank() + 0 + + See Also + ======== + + rank, next, prev, size + + """ + r = 0 + p = 1 + for i in range(self.nodes - 3, -1, -1): + r += p*self.prufer_repr[i] + p *= self.nodes + return r + + @classmethod + def unrank(self, rank, n): + """Finds the unranked Prufer sequence. + + Examples + ======== + + >>> from sympy.combinatorics.prufer import Prufer + >>> Prufer.unrank(0, 4) + Prufer([0, 0]) + + """ + n, rank = as_int(n), as_int(rank) + L = defaultdict(int) + for i in range(n - 3, -1, -1): + L[i] = rank % n + rank = (rank - L[i])//n + return Prufer([L[i] for i in range(len(L))]) + + def __new__(cls, *args, **kw_args): + """The constructor for the Prufer object. + + Examples + ======== + + >>> from sympy.combinatorics.prufer import Prufer + + A Prufer object can be constructed from a list of edges: + + >>> a = Prufer([[0, 1], [0, 2], [0, 3]]) + >>> a.prufer_repr + [0, 0] + + If the number of nodes is given, no checking of the nodes will + be performed; it will be assumed that nodes 0 through n - 1 are + present: + + >>> Prufer([[0, 1], [0, 2], [0, 3]], 4) + Prufer([[0, 1], [0, 2], [0, 3]], 4) + + A Prufer object can be constructed from a Prufer sequence: + + >>> b = Prufer([1, 3]) + >>> b.tree_repr + [[0, 1], [1, 3], [2, 3]] + + """ + arg0 = Array(args[0]) if args[0] else Tuple() + args = (arg0,) + tuple(_sympify(arg) for arg in args[1:]) + ret_obj = Basic.__new__(cls, *args, **kw_args) + args = [list(args[0])] + if args[0] and iterable(args[0][0]): + if not args[0][0]: + raise ValueError( + 'Prufer expects at least one edge in the tree.') + if len(args) > 1: + nnodes = args[1] + else: + nodes = set(flatten(args[0])) + nnodes = max(nodes) + 1 + if nnodes != len(nodes): + missing = set(range(nnodes)) - nodes + if len(missing) == 1: + msg = 'Node %s is missing.' % missing.pop() + else: + msg = 'Nodes %s are missing.' % sorted(missing) + raise ValueError(msg) + ret_obj._tree_repr = [list(i) for i in args[0]] + ret_obj._nodes = nnodes + else: + ret_obj._prufer_repr = args[0] + ret_obj._nodes = len(ret_obj._prufer_repr) + 2 + return ret_obj + + def next(self, delta=1): + """Generates the Prufer sequence that is delta beyond the current one. + + Examples + ======== + + >>> from sympy.combinatorics.prufer import Prufer + >>> a = Prufer([[0, 1], [0, 2], [0, 3]]) + >>> b = a.next(1) # == a.next() + >>> b.tree_repr + [[0, 2], [0, 1], [1, 3]] + >>> b.rank + 1 + + See Also + ======== + + prufer_rank, rank, prev, size + + """ + return Prufer.unrank(self.rank + delta, self.nodes) + + def prev(self, delta=1): + """Generates the Prufer sequence that is -delta before the current one. + + Examples + ======== + + >>> from sympy.combinatorics.prufer import Prufer + >>> a = Prufer([[0, 1], [1, 2], [2, 3], [1, 4]]) + >>> a.rank + 36 + >>> b = a.prev() + >>> b + Prufer([1, 2, 0]) + >>> b.rank + 35 + + See Also + ======== + + prufer_rank, rank, next, size + + """ + return Prufer.unrank(self.rank -delta, self.nodes) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/rewritingsystem.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/rewritingsystem.py new file mode 100644 index 0000000000000000000000000000000000000000..b9e8dfe4c831db6490c987c85a57d0d1a939de61 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/rewritingsystem.py @@ -0,0 +1,453 @@ +from collections import deque +from sympy.combinatorics.rewritingsystem_fsm import StateMachine + +class RewritingSystem: + ''' + A class implementing rewriting systems for `FpGroup`s. + + References + ========== + .. [1] Epstein, D., Holt, D. and Rees, S. (1991). + The use of Knuth-Bendix methods to solve the word problem in automatic groups. + Journal of Symbolic Computation, 12(4-5), pp.397-414. + + .. [2] GAP's Manual on its KBMAG package + https://www.gap-system.org/Manuals/pkg/kbmag-1.5.3/doc/manual.pdf + + ''' + def __init__(self, group): + self.group = group + self.alphabet = group.generators + self._is_confluent = None + + # these values are taken from [2] + self.maxeqns = 32767 # max rules + self.tidyint = 100 # rules before tidying + + # _max_exceeded is True if maxeqns is exceeded + # at any point + self._max_exceeded = False + + # Reduction automaton + self.reduction_automaton = None + self._new_rules = {} + + # dictionary of reductions + self.rules = {} + self.rules_cache = deque([], 50) + self._init_rules() + + + # All the transition symbols in the automaton + generators = list(self.alphabet) + generators += [gen**-1 for gen in generators] + # Create a finite state machine as an instance of the StateMachine object + self.reduction_automaton = StateMachine('Reduction automaton for '+ repr(self.group), generators) + self.construct_automaton() + + def set_max(self, n): + ''' + Set the maximum number of rules that can be defined + + ''' + if n > self.maxeqns: + self._max_exceeded = False + self.maxeqns = n + return + + @property + def is_confluent(self): + ''' + Return `True` if the system is confluent + + ''' + if self._is_confluent is None: + self._is_confluent = self._check_confluence() + return self._is_confluent + + def _init_rules(self): + identity = self.group.free_group.identity + for r in self.group.relators: + self.add_rule(r, identity) + self._remove_redundancies() + return + + def _add_rule(self, r1, r2): + ''' + Add the rule r1 -> r2 with no checking or further + deductions + + ''' + if len(self.rules) + 1 > self.maxeqns: + self._is_confluent = self._check_confluence() + self._max_exceeded = True + raise RuntimeError("Too many rules were defined.") + self.rules[r1] = r2 + # Add the newly added rule to the `new_rules` dictionary. + if self.reduction_automaton: + self._new_rules[r1] = r2 + + def add_rule(self, w1, w2, check=False): + new_keys = set() + + if w1 == w2: + return new_keys + + if w1 < w2: + w1, w2 = w2, w1 + + if (w1, w2) in self.rules_cache: + return new_keys + self.rules_cache.append((w1, w2)) + + s1, s2 = w1, w2 + + # The following is the equivalent of checking + # s1 for overlaps with the implicit reductions + # {g*g**-1 -> } and {g**-1*g -> } + # for any generator g without installing the + # redundant rules that would result from processing + # the overlaps. See [1], Section 3 for details. + + if len(s1) - len(s2) < 3: + if s1 not in self.rules: + new_keys.add(s1) + if not check: + self._add_rule(s1, s2) + if s2**-1 > s1**-1 and s2**-1 not in self.rules: + new_keys.add(s2**-1) + if not check: + self._add_rule(s2**-1, s1**-1) + + # overlaps on the right + while len(s1) - len(s2) > -1: + g = s1[len(s1)-1] + s1 = s1.subword(0, len(s1)-1) + s2 = s2*g**-1 + if len(s1) - len(s2) < 0: + if s2 not in self.rules: + if not check: + self._add_rule(s2, s1) + new_keys.add(s2) + elif len(s1) - len(s2) < 3: + new = self.add_rule(s1, s2, check) + new_keys.update(new) + + # overlaps on the left + while len(w1) - len(w2) > -1: + g = w1[0] + w1 = w1.subword(1, len(w1)) + w2 = g**-1*w2 + if len(w1) - len(w2) < 0: + if w2 not in self.rules: + if not check: + self._add_rule(w2, w1) + new_keys.add(w2) + elif len(w1) - len(w2) < 3: + new = self.add_rule(w1, w2, check) + new_keys.update(new) + + return new_keys + + def _remove_redundancies(self, changes=False): + ''' + Reduce left- and right-hand sides of reduction rules + and remove redundant equations (i.e. those for which + lhs == rhs). If `changes` is `True`, return a set + containing the removed keys and a set containing the + added keys + + ''' + removed = set() + added = set() + rules = self.rules.copy() + for r in rules: + v = self.reduce(r, exclude=r) + w = self.reduce(rules[r]) + if v != r: + del self.rules[r] + removed.add(r) + if v > w: + added.add(v) + self.rules[v] = w + elif v < w: + added.add(w) + self.rules[w] = v + else: + self.rules[v] = w + if changes: + return removed, added + return + + def make_confluent(self, check=False): + ''' + Try to make the system confluent using the Knuth-Bendix + completion algorithm + + ''' + if self._max_exceeded: + return self._is_confluent + lhs = list(self.rules.keys()) + + def _overlaps(r1, r2): + len1 = len(r1) + len2 = len(r2) + result = [] + for j in range(1, len1 + len2): + if (r1.subword(len1 - j, len1 + len2 - j, strict=False) + == r2.subword(j - len1, j, strict=False)): + a = r1.subword(0, len1-j, strict=False) + a = a*r2.subword(0, j-len1, strict=False) + b = r2.subword(j-len1, j, strict=False) + c = r2.subword(j, len2, strict=False) + c = c*r1.subword(len1 + len2 - j, len1, strict=False) + result.append(a*b*c) + return result + + def _process_overlap(w, r1, r2, check): + s = w.eliminate_word(r1, self.rules[r1]) + s = self.reduce(s) + t = w.eliminate_word(r2, self.rules[r2]) + t = self.reduce(t) + if s != t: + if check: + # system not confluent + return [0] + try: + new_keys = self.add_rule(t, s, check) + return new_keys + except RuntimeError: + return False + return + + added = 0 + i = 0 + while i < len(lhs): + r1 = lhs[i] + i += 1 + # j could be i+1 to not + # check each pair twice but lhs + # is extended in the loop and the new + # elements have to be checked with the + # preceding ones. there is probably a better way + # to handle this + j = 0 + while j < len(lhs): + r2 = lhs[j] + j += 1 + if r1 == r2: + continue + overlaps = _overlaps(r1, r2) + overlaps.extend(_overlaps(r1**-1, r2)) + if not overlaps: + continue + for w in overlaps: + new_keys = _process_overlap(w, r1, r2, check) + if new_keys: + if check: + return False + lhs.extend(new_keys) + added += len(new_keys) + elif new_keys == False: + # too many rules were added so the process + # couldn't complete + return self._is_confluent + + if added > self.tidyint and not check: + # tidy up + r, a = self._remove_redundancies(changes=True) + added = 0 + if r: + # reset i since some elements were removed + i = min(lhs.index(s) for s in r) + lhs = [l for l in lhs if l not in r] + lhs.extend(a) + if r1 in r: + # r1 was removed as redundant + break + + self._is_confluent = True + if not check: + self._remove_redundancies() + return True + + def _check_confluence(self): + return self.make_confluent(check=True) + + def reduce(self, word, exclude=None): + ''' + Apply reduction rules to `word` excluding the reduction rule + for the lhs equal to `exclude` + + ''' + rules = {r: self.rules[r] for r in self.rules if r != exclude} + # the following is essentially `eliminate_words()` code from the + # `FreeGroupElement` class, the only difference being the first + # "if" statement + again = True + new = word + while again: + again = False + for r in rules: + prev = new + if rules[r]**-1 > r**-1: + new = new.eliminate_word(r, rules[r], _all=True, inverse=False) + else: + new = new.eliminate_word(r, rules[r], _all=True) + if new != prev: + again = True + return new + + def _compute_inverse_rules(self, rules): + ''' + Compute the inverse rules for a given set of rules. + The inverse rules are used in the automaton for word reduction. + + Arguments: + rules (dictionary): Rules for which the inverse rules are to computed. + + Returns: + Dictionary of inverse_rules. + + ''' + inverse_rules = {} + for r in rules: + rule_key_inverse = r**-1 + rule_value_inverse = (rules[r])**-1 + if (rule_value_inverse < rule_key_inverse): + inverse_rules[rule_key_inverse] = rule_value_inverse + else: + inverse_rules[rule_value_inverse] = rule_key_inverse + return inverse_rules + + def construct_automaton(self): + ''' + Construct the automaton based on the set of reduction rules of the system. + + Automata Design: + The accept states of the automaton are the proper prefixes of the left hand side of the rules. + The complete left hand side of the rules are the dead states of the automaton. + + ''' + self._add_to_automaton(self.rules) + + def _add_to_automaton(self, rules): + ''' + Add new states and transitions to the automaton. + + Summary: + States corresponding to the new rules added to the system are computed and added to the automaton. + Transitions in the previously added states are also modified if necessary. + + Arguments: + rules (dictionary) -- Dictionary of the newly added rules. + + ''' + # Automaton variables + automaton_alphabet = [] + proper_prefixes = {} + + # compute the inverses of all the new rules added + all_rules = rules + inverse_rules = self._compute_inverse_rules(all_rules) + all_rules.update(inverse_rules) + + # Keep track of the accept_states. + accept_states = [] + + for rule in all_rules: + # The symbols present in the new rules are the symbols to be verified at each state. + # computes the automaton_alphabet, as the transitions solely depend upon the new states. + automaton_alphabet += rule.letter_form_elm + # Compute the proper prefixes for every rule. + proper_prefixes[rule] = [] + letter_word_array = list(rule.letter_form_elm) + len_letter_word_array = len(letter_word_array) + for i in range (1, len_letter_word_array): + letter_word_array[i] = letter_word_array[i-1]*letter_word_array[i] + # Add accept states. + elem = letter_word_array[i-1] + if elem not in self.reduction_automaton.states: + self.reduction_automaton.add_state(elem, state_type='a') + accept_states.append(elem) + proper_prefixes[rule] = letter_word_array + # Check for overlaps between dead and accept states. + if rule in accept_states: + self.reduction_automaton.states[rule].state_type = 'd' + self.reduction_automaton.states[rule].rh_rule = all_rules[rule] + accept_states.remove(rule) + # Add dead states + if rule not in self.reduction_automaton.states: + self.reduction_automaton.add_state(rule, state_type='d', rh_rule=all_rules[rule]) + + automaton_alphabet = set(automaton_alphabet) + + # Add new transitions for every state. + for state in self.reduction_automaton.states: + current_state_name = state + current_state_type = self.reduction_automaton.states[state].state_type + # Transitions will be modified only when suffixes of the current_state + # belongs to the proper_prefixes of the new rules. + # The rest are ignored if they cannot lead to a dead state after a finite number of transisitons. + if current_state_type == 's': + for letter in automaton_alphabet: + if letter in self.reduction_automaton.states: + self.reduction_automaton.states[state].add_transition(letter, letter) + else: + self.reduction_automaton.states[state].add_transition(letter, current_state_name) + elif current_state_type == 'a': + # Check if the transition to any new state in possible. + for letter in automaton_alphabet: + _next = current_state_name*letter + while len(_next) and _next not in self.reduction_automaton.states: + _next = _next.subword(1, len(_next)) + if not len(_next): + _next = 'start' + self.reduction_automaton.states[state].add_transition(letter, _next) + + # Add transitions for new states. All symbols used in the automaton are considered here. + # Ignore this if `reduction_automaton.automaton_alphabet` = `automaton_alphabet`. + if len(self.reduction_automaton.automaton_alphabet) != len(automaton_alphabet): + for state in accept_states: + current_state_name = state + for letter in self.reduction_automaton.automaton_alphabet: + _next = current_state_name*letter + while len(_next) and _next not in self.reduction_automaton.states: + _next = _next.subword(1, len(_next)) + if not len(_next): + _next = 'start' + self.reduction_automaton.states[state].add_transition(letter, _next) + + def reduce_using_automaton(self, word): + ''' + Reduce a word using an automaton. + + Summary: + All the symbols of the word are stored in an array and are given as the input to the automaton. + If the automaton reaches a dead state that subword is replaced and the automaton is run from the beginning. + The complete word has to be replaced when the word is read and the automaton reaches a dead state. + So, this process is repeated until the word is read completely and the automaton reaches the accept state. + + Arguments: + word (instance of FreeGroupElement) -- Word that needs to be reduced. + + ''' + # Modify the automaton if new rules are found. + if self._new_rules: + self._add_to_automaton(self._new_rules) + self._new_rules = {} + + flag = 1 + while flag: + flag = 0 + current_state = self.reduction_automaton.states['start'] + for i, s in enumerate(word.letter_form_elm): + next_state_name = current_state.transitions[s] + next_state = self.reduction_automaton.states[next_state_name] + if next_state.state_type == 'd': + subst = next_state.rh_rule + word = word.substituted_word(i - len(next_state_name) + 1, i+1, subst) + flag = 1 + break + current_state = next_state + return word diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/rewritingsystem_fsm.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/rewritingsystem_fsm.py new file mode 100644 index 0000000000000000000000000000000000000000..21916530040ac321180692d1a0811da4ae36a056 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/rewritingsystem_fsm.py @@ -0,0 +1,60 @@ +class State: + ''' + A representation of a state managed by a ``StateMachine``. + + Attributes: + name (instance of FreeGroupElement or string) -- State name which is also assigned to the Machine. + transisitons (OrderedDict) -- Represents all the transitions of the state object. + state_type (string) -- Denotes the type (accept/start/dead) of the state. + rh_rule (instance of FreeGroupElement) -- right hand rule for dead state. + state_machine (instance of StateMachine object) -- The finite state machine that the state belongs to. + ''' + + def __init__(self, name, state_machine, state_type=None, rh_rule=None): + self.name = name + self.transitions = {} + self.state_machine = state_machine + self.state_type = state_type[0] + self.rh_rule = rh_rule + + def add_transition(self, letter, state): + ''' + Add a transition from the current state to a new state. + + Keyword Arguments: + letter -- The alphabet element the current state reads to make the state transition. + state -- This will be an instance of the State object which represents a new state after in the transition after the alphabet is read. + + ''' + self.transitions[letter] = state + +class StateMachine: + ''' + Representation of a finite state machine the manages the states and the transitions of the automaton. + + Attributes: + states (dictionary) -- Collection of all registered `State` objects. + name (str) -- Name of the state machine. + ''' + + def __init__(self, name, automaton_alphabet): + self.name = name + self.automaton_alphabet = automaton_alphabet + self.states = {} # Contains all the states in the machine. + self.add_state('start', state_type='s') + + def add_state(self, state_name, state_type=None, rh_rule=None): + ''' + Instantiate a state object and stores it in the 'states' dictionary. + + Arguments: + state_name (instance of FreeGroupElement or string) -- name of the new states. + state_type (string) -- Denotes the type (accept/start/dead) of the state added. + rh_rule (instance of FreeGroupElement) -- right hand rule for dead state. + + ''' + new_state = State(state_name, self, state_type, rh_rule) + self.states[state_name] = new_state + + def __repr__(self): + return "%s" % (self.name) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/schur_number.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/schur_number.py new file mode 100644 index 0000000000000000000000000000000000000000..83aac98e543d4b54d4e6af17adca6e4f4de1b9ac --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/schur_number.py @@ -0,0 +1,160 @@ +""" +The Schur number S(k) is the largest integer n for which the interval [1,n] +can be partitioned into k sum-free sets.(https://mathworld.wolfram.com/SchurNumber.html) +""" +import math +from sympy.core import S +from sympy.core.basic import Basic +from sympy.core.function import Function +from sympy.core.numbers import Integer + + +class SchurNumber(Function): + r""" + This function creates a SchurNumber object + which is evaluated for `k \le 5` otherwise only + the lower bound information can be retrieved. + + Examples + ======== + + >>> from sympy.combinatorics.schur_number import SchurNumber + + Since S(3) = 13, hence the output is a number + >>> SchurNumber(3) + 13 + + We do not know the Schur number for values greater than 5, hence + only the object is returned + >>> SchurNumber(6) + SchurNumber(6) + + Now, the lower bound information can be retrieved using lower_bound() + method + >>> SchurNumber(6).lower_bound() + 536 + + """ + + @classmethod + def eval(cls, k): + if k.is_Number: + if k is S.Infinity: + return S.Infinity + if k.is_zero: + return S.Zero + if not k.is_integer or k.is_negative: + raise ValueError("k should be a positive integer") + first_known_schur_numbers = {1: 1, 2: 4, 3: 13, 4: 44, 5: 160} + if k <= 5: + return Integer(first_known_schur_numbers[k]) + + def lower_bound(self): + f_ = self.args[0] + # Improved lower bounds known for S(6) and S(7) + if f_ == 6: + return Integer(536) + if f_ == 7: + return Integer(1680) + # For other cases, use general expression + if f_.is_Integer: + return 3*self.func(f_ - 1).lower_bound() - 1 + return (3**f_ - 1)/2 + + +def _schur_subsets_number(n): + + if n is S.Infinity: + raise ValueError("Input must be finite") + if n <= 0: + raise ValueError("n must be a non-zero positive integer.") + elif n <= 3: + min_k = 1 + else: + min_k = math.ceil(math.log(2*n + 1, 3)) + + return Integer(min_k) + + +def schur_partition(n): + """ + + This function returns the partition in the minimum number of sum-free subsets + according to the lower bound given by the Schur Number. + + Parameters + ========== + + n: a number + n is the upper limit of the range [1, n] for which we need to find and + return the minimum number of free subsets according to the lower bound + of schur number + + Returns + ======= + + List of lists + List of the minimum number of sum-free subsets + + Notes + ===== + + It is possible for some n to make the partition into less + subsets since the only known Schur numbers are: + S(1) = 1, S(2) = 4, S(3) = 13, S(4) = 44. + e.g for n = 44 the lower bound from the function above is 5 subsets but it has been proven + that can be done with 4 subsets. + + Examples + ======== + + For n = 1, 2, 3 the answer is the set itself + + >>> from sympy.combinatorics.schur_number import schur_partition + >>> schur_partition(2) + [[1, 2]] + + For n > 3, the answer is the minimum number of sum-free subsets: + + >>> schur_partition(5) + [[3, 2], [5], [1, 4]] + + >>> schur_partition(8) + [[3, 2], [6, 5, 8], [1, 4, 7]] + """ + + if isinstance(n, Basic) and not n.is_Number: + raise ValueError("Input value must be a number") + + number_of_subsets = _schur_subsets_number(n) + if n == 1: + sum_free_subsets = [[1]] + elif n == 2: + sum_free_subsets = [[1, 2]] + elif n == 3: + sum_free_subsets = [[1, 2, 3]] + else: + sum_free_subsets = [[1, 4], [2, 3]] + + while len(sum_free_subsets) < number_of_subsets: + sum_free_subsets = _generate_next_list(sum_free_subsets, n) + missed_elements = [3*k + 1 for k in range(len(sum_free_subsets), (n-1)//3 + 1)] + sum_free_subsets[-1] += missed_elements + + return sum_free_subsets + + +def _generate_next_list(current_list, n): + new_list = [] + + for item in current_list: + temp_1 = [number*3 for number in item if number*3 <= n] + temp_2 = [number*3 - 1 for number in item if number*3 - 1 <= n] + new_item = temp_1 + temp_2 + new_list.append(new_item) + + last_list = [3*k + 1 for k in range(len(current_list)+1) if 3*k + 1 <= n] + new_list.append(last_list) + current_list = new_list + + return current_list diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/subsets.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/subsets.py new file mode 100644 index 0000000000000000000000000000000000000000..e540cb2395cb27e04c9d513831cb834a05ec2abd --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/subsets.py @@ -0,0 +1,619 @@ +from itertools import combinations + +from sympy.combinatorics.graycode import GrayCode + + +class Subset(): + """ + Represents a basic subset object. + + Explanation + =========== + + We generate subsets using essentially two techniques, + binary enumeration and lexicographic enumeration. + The Subset class takes two arguments, the first one + describes the initial subset to consider and the second + describes the superset. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd']) + >>> a.next_binary().subset + ['b'] + >>> a.prev_binary().subset + ['c'] + """ + + _rank_binary = None + _rank_lex = None + _rank_graycode = None + _subset = None + _superset = None + + def __new__(cls, subset, superset): + """ + Default constructor. + + It takes the ``subset`` and its ``superset`` as its parameters. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd']) + >>> a.subset + ['c', 'd'] + >>> a.superset + ['a', 'b', 'c', 'd'] + >>> a.size + 2 + """ + if len(subset) > len(superset): + raise ValueError('Invalid arguments have been provided. The ' + 'superset must be larger than the subset.') + for elem in subset: + if elem not in superset: + raise ValueError('The superset provided is invalid as it does ' + 'not contain the element {}'.format(elem)) + obj = object.__new__(cls) + obj._subset = subset + obj._superset = superset + return obj + + def __eq__(self, other): + """Return a boolean indicating whether a == b on the basis of + whether both objects are of the class Subset and if the values + of the subset and superset attributes are the same. + """ + if not isinstance(other, Subset): + return NotImplemented + return self.subset == other.subset and self.superset == other.superset + + def iterate_binary(self, k): + """ + This is a helper function. It iterates over the + binary subsets by ``k`` steps. This variable can be + both positive or negative. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd']) + >>> a.iterate_binary(-2).subset + ['d'] + >>> a = Subset(['a', 'b', 'c'], ['a', 'b', 'c', 'd']) + >>> a.iterate_binary(2).subset + [] + + See Also + ======== + + next_binary, prev_binary + """ + bin_list = Subset.bitlist_from_subset(self.subset, self.superset) + n = (int(''.join(bin_list), 2) + k) % 2**self.superset_size + bits = bin(n)[2:].rjust(self.superset_size, '0') + return Subset.subset_from_bitlist(self.superset, bits) + + def next_binary(self): + """ + Generates the next binary ordered subset. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd']) + >>> a.next_binary().subset + ['b'] + >>> a = Subset(['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'd']) + >>> a.next_binary().subset + [] + + See Also + ======== + + prev_binary, iterate_binary + """ + return self.iterate_binary(1) + + def prev_binary(self): + """ + Generates the previous binary ordered subset. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset([], ['a', 'b', 'c', 'd']) + >>> a.prev_binary().subset + ['a', 'b', 'c', 'd'] + >>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd']) + >>> a.prev_binary().subset + ['c'] + + See Also + ======== + + next_binary, iterate_binary + """ + return self.iterate_binary(-1) + + def next_lexicographic(self): + """ + Generates the next lexicographically ordered subset. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd']) + >>> a.next_lexicographic().subset + ['d'] + >>> a = Subset(['d'], ['a', 'b', 'c', 'd']) + >>> a.next_lexicographic().subset + [] + + See Also + ======== + + prev_lexicographic + """ + i = self.superset_size - 1 + indices = Subset.subset_indices(self.subset, self.superset) + + if i in indices: + if i - 1 in indices: + indices.remove(i - 1) + else: + indices.remove(i) + i = i - 1 + while i >= 0 and i not in indices: + i = i - 1 + if i >= 0: + indices.remove(i) + indices.append(i+1) + else: + while i not in indices and i >= 0: + i = i - 1 + indices.append(i + 1) + + ret_set = [] + super_set = self.superset + for i in indices: + ret_set.append(super_set[i]) + return Subset(ret_set, super_set) + + def prev_lexicographic(self): + """ + Generates the previous lexicographically ordered subset. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset([], ['a', 'b', 'c', 'd']) + >>> a.prev_lexicographic().subset + ['d'] + >>> a = Subset(['c','d'], ['a', 'b', 'c', 'd']) + >>> a.prev_lexicographic().subset + ['c'] + + See Also + ======== + + next_lexicographic + """ + i = self.superset_size - 1 + indices = Subset.subset_indices(self.subset, self.superset) + + while i >= 0 and i not in indices: + i = i - 1 + + if i == 0 or i - 1 in indices: + indices.remove(i) + else: + if i >= 0: + indices.remove(i) + indices.append(i - 1) + indices.append(self.superset_size - 1) + + ret_set = [] + super_set = self.superset + for i in indices: + ret_set.append(super_set[i]) + return Subset(ret_set, super_set) + + def iterate_graycode(self, k): + """ + Helper function used for prev_gray and next_gray. + It performs ``k`` step overs to get the respective Gray codes. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset([1, 2, 3], [1, 2, 3, 4]) + >>> a.iterate_graycode(3).subset + [1, 4] + >>> a.iterate_graycode(-2).subset + [1, 2, 4] + + See Also + ======== + + next_gray, prev_gray + """ + unranked_code = GrayCode.unrank(self.superset_size, + (self.rank_gray + k) % self.cardinality) + return Subset.subset_from_bitlist(self.superset, + unranked_code) + + def next_gray(self): + """ + Generates the next Gray code ordered subset. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset([1, 2, 3], [1, 2, 3, 4]) + >>> a.next_gray().subset + [1, 3] + + See Also + ======== + + iterate_graycode, prev_gray + """ + return self.iterate_graycode(1) + + def prev_gray(self): + """ + Generates the previous Gray code ordered subset. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset([2, 3, 4], [1, 2, 3, 4, 5]) + >>> a.prev_gray().subset + [2, 3, 4, 5] + + See Also + ======== + + iterate_graycode, next_gray + """ + return self.iterate_graycode(-1) + + @property + def rank_binary(self): + """ + Computes the binary ordered rank. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset([], ['a','b','c','d']) + >>> a.rank_binary + 0 + >>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd']) + >>> a.rank_binary + 3 + + See Also + ======== + + iterate_binary, unrank_binary + """ + if self._rank_binary is None: + self._rank_binary = int("".join( + Subset.bitlist_from_subset(self.subset, + self.superset)), 2) + return self._rank_binary + + @property + def rank_lexicographic(self): + """ + Computes the lexicographic ranking of the subset. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd']) + >>> a.rank_lexicographic + 14 + >>> a = Subset([2, 4, 5], [1, 2, 3, 4, 5, 6]) + >>> a.rank_lexicographic + 43 + """ + if self._rank_lex is None: + def _ranklex(self, subset_index, i, n): + if subset_index == [] or i > n: + return 0 + if i in subset_index: + subset_index.remove(i) + return 1 + _ranklex(self, subset_index, i + 1, n) + return 2**(n - i - 1) + _ranklex(self, subset_index, i + 1, n) + indices = Subset.subset_indices(self.subset, self.superset) + self._rank_lex = _ranklex(self, indices, 0, self.superset_size) + return self._rank_lex + + @property + def rank_gray(self): + """ + Computes the Gray code ranking of the subset. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset(['c','d'], ['a','b','c','d']) + >>> a.rank_gray + 2 + >>> a = Subset([2, 4, 5], [1, 2, 3, 4, 5, 6]) + >>> a.rank_gray + 27 + + See Also + ======== + + iterate_graycode, unrank_gray + """ + if self._rank_graycode is None: + bits = Subset.bitlist_from_subset(self.subset, self.superset) + self._rank_graycode = GrayCode(len(bits), start=bits).rank + return self._rank_graycode + + @property + def subset(self): + """ + Gets the subset represented by the current instance. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd']) + >>> a.subset + ['c', 'd'] + + See Also + ======== + + superset, size, superset_size, cardinality + """ + return self._subset + + @property + def size(self): + """ + Gets the size of the subset. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd']) + >>> a.size + 2 + + See Also + ======== + + subset, superset, superset_size, cardinality + """ + return len(self.subset) + + @property + def superset(self): + """ + Gets the superset of the subset. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd']) + >>> a.superset + ['a', 'b', 'c', 'd'] + + See Also + ======== + + subset, size, superset_size, cardinality + """ + return self._superset + + @property + def superset_size(self): + """ + Returns the size of the superset. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd']) + >>> a.superset_size + 4 + + See Also + ======== + + subset, superset, size, cardinality + """ + return len(self.superset) + + @property + def cardinality(self): + """ + Returns the number of all possible subsets. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd']) + >>> a.cardinality + 16 + + See Also + ======== + + subset, superset, size, superset_size + """ + return 2**(self.superset_size) + + @classmethod + def subset_from_bitlist(self, super_set, bitlist): + """ + Gets the subset defined by the bitlist. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> Subset.subset_from_bitlist(['a', 'b', 'c', 'd'], '0011').subset + ['c', 'd'] + + See Also + ======== + + bitlist_from_subset + """ + if len(super_set) != len(bitlist): + raise ValueError("The sizes of the lists are not equal") + ret_set = [] + for i in range(len(bitlist)): + if bitlist[i] == '1': + ret_set.append(super_set[i]) + return Subset(ret_set, super_set) + + @classmethod + def bitlist_from_subset(self, subset, superset): + """ + Gets the bitlist corresponding to a subset. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> Subset.bitlist_from_subset(['c', 'd'], ['a', 'b', 'c', 'd']) + '0011' + + See Also + ======== + + subset_from_bitlist + """ + bitlist = ['0'] * len(superset) + if isinstance(subset, Subset): + subset = subset.subset + for i in Subset.subset_indices(subset, superset): + bitlist[i] = '1' + return ''.join(bitlist) + + @classmethod + def unrank_binary(self, rank, superset): + """ + Gets the binary ordered subset of the specified rank. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> Subset.unrank_binary(4, ['a', 'b', 'c', 'd']).subset + ['b'] + + See Also + ======== + + iterate_binary, rank_binary + """ + bits = bin(rank)[2:].rjust(len(superset), '0') + return Subset.subset_from_bitlist(superset, bits) + + @classmethod + def unrank_gray(self, rank, superset): + """ + Gets the Gray code ordered subset of the specified rank. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> Subset.unrank_gray(4, ['a', 'b', 'c']).subset + ['a', 'b'] + >>> Subset.unrank_gray(0, ['a', 'b', 'c']).subset + [] + + See Also + ======== + + iterate_graycode, rank_gray + """ + graycode_bitlist = GrayCode.unrank(len(superset), rank) + return Subset.subset_from_bitlist(superset, graycode_bitlist) + + @classmethod + def subset_indices(self, subset, superset): + """Return indices of subset in superset in a list; the list is empty + if all elements of ``subset`` are not in ``superset``. + + Examples + ======== + + >>> from sympy.combinatorics import Subset + >>> superset = [1, 3, 2, 5, 4] + >>> Subset.subset_indices([3, 2, 1], superset) + [1, 2, 0] + >>> Subset.subset_indices([1, 6], superset) + [] + >>> Subset.subset_indices([], superset) + [] + + """ + a, b = superset, subset + sb = set(b) + d = {} + for i, ai in enumerate(a): + if ai in sb: + d[ai] = i + sb.remove(ai) + if not sb: + break + else: + return [] + return [d[bi] for bi in b] + + +def ksubsets(superset, k): + """ + Finds the subsets of size ``k`` in lexicographic order. + + This uses the itertools generator. + + Examples + ======== + + >>> from sympy.combinatorics.subsets import ksubsets + >>> list(ksubsets([1, 2, 3], 2)) + [(1, 2), (1, 3), (2, 3)] + >>> list(ksubsets([1, 2, 3, 4, 5], 2)) + [(1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 4), \ + (2, 5), (3, 4), (3, 5), (4, 5)] + + See Also + ======== + + Subset + """ + return combinations(superset, k) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tensor_can.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tensor_can.py new file mode 100644 index 0000000000000000000000000000000000000000..d43e96ed27a21f988aaaa8fd16827987541f7373 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tensor_can.py @@ -0,0 +1,1189 @@ +from sympy.combinatorics.permutations import Permutation, _af_rmul, \ + _af_invert, _af_new +from sympy.combinatorics.perm_groups import PermutationGroup, _orbit, \ + _orbit_transversal +from sympy.combinatorics.util import _distribute_gens_by_base, \ + _orbits_transversals_from_bsgs + +""" + References for tensor canonicalization: + + [1] R. Portugal "Algorithmic simplification of tensor expressions", + J. Phys. A 32 (1999) 7779-7789 + + [2] R. Portugal, B.F. Svaiter "Group-theoretic Approach for Symbolic + Tensor Manipulation: I. Free Indices" + arXiv:math-ph/0107031v1 + + [3] L.R.U. Manssur, R. Portugal "Group-theoretic Approach for Symbolic + Tensor Manipulation: II. Dummy Indices" + arXiv:math-ph/0107032v1 + + [4] xperm.c part of XPerm written by J. M. Martin-Garcia + http://www.xact.es/index.html +""" + + +def dummy_sgs(dummies, sym, n): + """ + Return the strong generators for dummy indices. + + Parameters + ========== + + dummies : List of dummy indices. + `dummies[2k], dummies[2k+1]` are paired indices. + In base form, the dummy indices are always in + consecutive positions. + sym : symmetry under interchange of contracted dummies:: + * None no symmetry + * 0 commuting + * 1 anticommuting + + n : number of indices + + Examples + ======== + + >>> from sympy.combinatorics.tensor_can import dummy_sgs + >>> dummy_sgs(list(range(2, 8)), 0, 8) + [[0, 1, 3, 2, 4, 5, 6, 7, 8, 9], [0, 1, 2, 3, 5, 4, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 7, 6, 8, 9], [0, 1, 4, 5, 2, 3, 6, 7, 8, 9], + [0, 1, 2, 3, 6, 7, 4, 5, 8, 9]] + """ + if len(dummies) > n: + raise ValueError("List too large") + res = [] + # exchange of contravariant and covariant indices + if sym is not None: + for j in dummies[::2]: + a = list(range(n + 2)) + if sym == 1: + a[n] = n + 1 + a[n + 1] = n + a[j], a[j + 1] = a[j + 1], a[j] + res.append(a) + # rename dummy indices + for j in dummies[:-3:2]: + a = list(range(n + 2)) + a[j:j + 4] = a[j + 2], a[j + 3], a[j], a[j + 1] + res.append(a) + return res + + +def _min_dummies(dummies, sym, indices): + """ + Return list of minima of the orbits of indices in group of dummies. + See ``double_coset_can_rep`` for the description of ``dummies`` and ``sym``. + ``indices`` is the initial list of dummy indices. + + Examples + ======== + + >>> from sympy.combinatorics.tensor_can import _min_dummies + >>> _min_dummies([list(range(2, 8))], [0], list(range(10))) + [0, 1, 2, 2, 2, 2, 2, 2, 8, 9] + """ + num_types = len(sym) + m = [min(dx) if dx else None for dx in dummies] + res = indices[:] + for i in range(num_types): + for c, i in enumerate(indices): + for j in range(num_types): + if i in dummies[j]: + res[c] = m[j] + break + return res + + +def _trace_S(s, j, b, S_cosets): + """ + Return the representative h satisfying s[h[b]] == j + + If there is not such a representative return None + """ + for h in S_cosets[b]: + if s[h[b]] == j: + return h + return None + + +def _trace_D(gj, p_i, Dxtrav): + """ + Return the representative h satisfying h[gj] == p_i + + If there is not such a representative return None + """ + for h in Dxtrav: + if h[gj] == p_i: + return h + return None + + +def _dumx_remove(dumx, dumx_flat, p0): + """ + remove p0 from dumx + """ + res = [] + for dx in dumx: + if p0 not in dx: + res.append(dx) + continue + k = dx.index(p0) + if k % 2 == 0: + p0_paired = dx[k + 1] + else: + p0_paired = dx[k - 1] + dx.remove(p0) + dx.remove(p0_paired) + dumx_flat.remove(p0) + dumx_flat.remove(p0_paired) + res.append(dx) + + +def transversal2coset(size, base, transversal): + a = [] + j = 0 + for i in range(size): + if i in base: + a.append(sorted(transversal[j].values())) + j += 1 + else: + a.append([list(range(size))]) + j = len(a) - 1 + while a[j] == [list(range(size))]: + j -= 1 + return a[:j + 1] + + +def double_coset_can_rep(dummies, sym, b_S, sgens, S_transversals, g): + r""" + Butler-Portugal algorithm for tensor canonicalization with dummy indices. + + Parameters + ========== + + dummies + list of lists of dummy indices, + one list for each type of index; + the dummy indices are put in order contravariant, covariant + [d0, -d0, d1, -d1, ...]. + + sym + list of the symmetries of the index metric for each type. + + possible symmetries of the metrics + * 0 symmetric + * 1 antisymmetric + * None no symmetry + + b_S + base of a minimal slot symmetry BSGS. + + sgens + generators of the slot symmetry BSGS. + + S_transversals + transversals for the slot BSGS. + + g + permutation representing the tensor. + + Returns + ======= + + Return 0 if the tensor is zero, else return the array form of + the permutation representing the canonical form of the tensor. + + Notes + ===== + + A tensor with dummy indices can be represented in a number + of equivalent ways which typically grows exponentially with + the number of indices. To be able to establish if two tensors + with many indices are equal becomes computationally very slow + in absence of an efficient algorithm. + + The Butler-Portugal algorithm [3] is an efficient algorithm to + put tensors in canonical form, solving the above problem. + + Portugal observed that a tensor can be represented by a permutation, + and that the class of tensors equivalent to it under slot and dummy + symmetries is equivalent to the double coset `D*g*S` + (Note: in this documentation we use the conventions for multiplication + of permutations p, q with (p*q)(i) = p[q[i]] which is opposite + to the one used in the Permutation class) + + Using the algorithm by Butler to find a representative of the + double coset one can find a canonical form for the tensor. + + To see this correspondence, + let `g` be a permutation in array form; a tensor with indices `ind` + (the indices including both the contravariant and the covariant ones) + can be written as + + `t = T(ind[g[0]], \dots, ind[g[n-1]])`, + + where `n = len(ind)`; + `g` has size `n + 2`, the last two indices for the sign of the tensor + (trick introduced in [4]). + + A slot symmetry transformation `s` is a permutation acting on the slots + `t \rightarrow T(ind[(g*s)[0]], \dots, ind[(g*s)[n-1]])` + + A dummy symmetry transformation acts on `ind` + `t \rightarrow T(ind[(d*g)[0]], \dots, ind[(d*g)[n-1]])` + + Being interested only in the transformations of the tensor under + these symmetries, one can represent the tensor by `g`, which transforms + as + + `g -> d*g*s`, so it belongs to the coset `D*g*S`, or in other words + to the set of all permutations allowed by the slot and dummy symmetries. + + Let us explain the conventions by an example. + + Given a tensor `T^{d3 d2 d1}{}_{d1 d2 d3}` with the slot symmetries + `T^{a0 a1 a2 a3 a4 a5} = -T^{a2 a1 a0 a3 a4 a5}` + + `T^{a0 a1 a2 a3 a4 a5} = -T^{a4 a1 a2 a3 a0 a5}` + + and symmetric metric, find the tensor equivalent to it which + is the lowest under the ordering of indices: + lexicographic ordering `d1, d2, d3` and then contravariant + before covariant index; that is the canonical form of the tensor. + + The canonical form is `-T^{d1 d2 d3}{}_{d1 d2 d3}` + obtained using `T^{a0 a1 a2 a3 a4 a5} = -T^{a2 a1 a0 a3 a4 a5}`. + + To convert this problem in the input for this function, + use the following ordering of the index names + (- for covariant for short) `d1, -d1, d2, -d2, d3, -d3` + + `T^{d3 d2 d1}{}_{d1 d2 d3}` corresponds to `g = [4, 2, 0, 1, 3, 5, 6, 7]` + where the last two indices are for the sign + + `sgens = [Permutation(0, 2)(6, 7), Permutation(0, 4)(6, 7)]` + + sgens[0] is the slot symmetry `-(0, 2)` + `T^{a0 a1 a2 a3 a4 a5} = -T^{a2 a1 a0 a3 a4 a5}` + + sgens[1] is the slot symmetry `-(0, 4)` + `T^{a0 a1 a2 a3 a4 a5} = -T^{a4 a1 a2 a3 a0 a5}` + + The dummy symmetry group D is generated by the strong base generators + `[(0, 1), (2, 3), (4, 5), (0, 2)(1, 3), (0, 4)(1, 5)]` + where the first three interchange covariant and contravariant + positions of the same index (d1 <-> -d1) and the last two interchange + the dummy indices themselves (d1 <-> d2). + + The dummy symmetry acts from the left + `d = [1, 0, 2, 3, 4, 5, 6, 7]` exchange `d1 \leftrightarrow -d1` + `T^{d3 d2 d1}{}_{d1 d2 d3} == T^{d3 d2}{}_{d1}{}^{d1}{}_{d2 d3}` + + `g=[4, 2, 0, 1, 3, 5, 6, 7] -> [4, 2, 1, 0, 3, 5, 6, 7] = _af_rmul(d, g)` + which differs from `_af_rmul(g, d)`. + + The slot symmetry acts from the right + `s = [2, 1, 0, 3, 4, 5, 7, 6]` exchanges slots 0 and 2 and changes sign + `T^{d3 d2 d1}{}_{d1 d2 d3} == -T^{d1 d2 d3}{}_{d1 d2 d3}` + + `g=[4,2,0,1,3,5,6,7] -> [0, 2, 4, 1, 3, 5, 7, 6] = _af_rmul(g, s)` + + Example in which the tensor is zero, same slot symmetries as above: + `T^{d2}{}_{d1 d3}{}^{d1 d3}{}_{d2}` + + `= -T^{d3}{}_{d1 d3}{}^{d1 d2}{}_{d2}` under slot symmetry `-(0,4)`; + + `= T_{d3 d1}{}^{d3}{}^{d1 d2}{}_{d2}` under slot symmetry `-(0,2)`; + + `= T^{d3}{}_{d1 d3}{}^{d1 d2}{}_{d2}` symmetric metric; + + `= 0` since two of these lines have tensors differ only for the sign. + + The double coset D*g*S consists of permutations `h = d*g*s` corresponding + to equivalent tensors; if there are two `h` which are the same apart + from the sign, return zero; otherwise + choose as representative the tensor with indices + ordered lexicographically according to `[d1, -d1, d2, -d2, d3, -d3]` + that is ``rep = min(D*g*S) = min([d*g*s for d in D for s in S])`` + + The indices are fixed one by one; first choose the lowest index + for slot 0, then the lowest remaining index for slot 1, etc. + Doing this one obtains a chain of stabilizers + + `S \rightarrow S_{b0} \rightarrow S_{b0,b1} \rightarrow \dots` and + `D \rightarrow D_{p0} \rightarrow D_{p0,p1} \rightarrow \dots` + + where ``[b0, b1, ...] = range(b)`` is a base of the symmetric group; + the strong base `b_S` of S is an ordered sublist of it; + therefore it is sufficient to compute once the + strong base generators of S using the Schreier-Sims algorithm; + the stabilizers of the strong base generators are the + strong base generators of the stabilizer subgroup. + + ``dbase = [p0, p1, ...]`` is not in general in lexicographic order, + so that one must recompute the strong base generators each time; + however this is trivial, there is no need to use the Schreier-Sims + algorithm for D. + + The algorithm keeps a TAB of elements `(s_i, d_i, h_i)` + where `h_i = d_i \times g \times s_i` satisfying `h_i[j] = p_j` for `0 \le j < i` + starting from `s_0 = id, d_0 = id, h_0 = g`. + + The equations `h_0[0] = p_0, h_1[1] = p_1, \dots` are solved in this order, + choosing each time the lowest possible value of p_i + + For `j < i` + `d_i*g*s_i*S_{b_0, \dots, b_{i-1}}*b_j = D_{p_0, \dots, p_{i-1}}*p_j` + so that for dx in `D_{p_0,\dots,p_{i-1}}` and sx in + `S_{base[0], \dots, base[i-1]}` one has `dx*d_i*g*s_i*sx*b_j = p_j` + + Search for dx, sx such that this equation holds for `j = i`; + it can be written as `s_i*sx*b_j = J, dx*d_i*g*J = p_j` + `sx*b_j = s_i**-1*J; sx = trace(s_i**-1, S_{b_0,...,b_{i-1}})` + `dx**-1*p_j = d_i*g*J; dx = trace(d_i*g*J, D_{p_0,...,p_{i-1}})` + + `s_{i+1} = s_i*trace(s_i**-1*J, S_{b_0,...,b_{i-1}})` + `d_{i+1} = trace(d_i*g*J, D_{p_0,...,p_{i-1}})**-1*d_i` + `h_{i+1}*b_i = d_{i+1}*g*s_{i+1}*b_i = p_i` + + `h_n*b_j = p_j` for all j, so that `h_n` is the solution. + + Add the found `(s, d, h)` to TAB1. + + At the end of the iteration sort TAB1 with respect to the `h`; + if there are two consecutive `h` in TAB1 which differ only for the + sign, the tensor is zero, so return 0; + if there are two consecutive `h` which are equal, keep only one. + + Then stabilize the slot generators under `i` and the dummy generators + under `p_i`. + + Assign `TAB = TAB1` at the end of the iteration step. + + At the end `TAB` contains a unique `(s, d, h)`, since all the slots + of the tensor `h` have been fixed to have the minimum value according + to the symmetries. The algorithm returns `h`. + + It is important that the slot BSGS has lexicographic minimal base, + otherwise there is an `i` which does not belong to the slot base + for which `p_i` is fixed by the dummy symmetry only, while `i` + is not invariant from the slot stabilizer, so `p_i` is not in + general the minimal value. + + This algorithm differs slightly from the original algorithm [3]: + the canonical form is minimal lexicographically, and + the BSGS has minimal base under lexicographic order. + Equal tensors `h` are eliminated from TAB. + + + Examples + ======== + + >>> from sympy.combinatorics.permutations import Permutation + >>> from sympy.combinatorics.tensor_can import double_coset_can_rep, get_transversals + >>> gens = [Permutation(x) for x in [[2, 1, 0, 3, 4, 5, 7, 6], [4, 1, 2, 3, 0, 5, 7, 6]]] + >>> base = [0, 2] + >>> g = Permutation([4, 2, 0, 1, 3, 5, 6, 7]) + >>> transversals = get_transversals(base, gens) + >>> double_coset_can_rep([list(range(6))], [0], base, gens, transversals, g) + [0, 1, 2, 3, 4, 5, 7, 6] + + >>> g = Permutation([4, 1, 3, 0, 5, 2, 6, 7]) + >>> double_coset_can_rep([list(range(6))], [0], base, gens, transversals, g) + 0 + """ + size = g.size + g = g.array_form + num_dummies = size - 2 + indices = list(range(num_dummies)) + all_metrics_with_sym = not any(_ is None for _ in sym) + num_types = len(sym) + dumx = dummies[:] + dumx_flat = [] + for dx in dumx: + dumx_flat.extend(dx) + b_S = b_S[:] + sgensx = [h._array_form for h in sgens] + if b_S: + S_transversals = transversal2coset(size, b_S, S_transversals) + # strong generating set for D + dsgsx = [] + for i in range(num_types): + dsgsx.extend(dummy_sgs(dumx[i], sym[i], num_dummies)) + idn = list(range(size)) + # TAB = list of entries (s, d, h) where h = _af_rmuln(d,g,s) + # for short, in the following d*g*s means _af_rmuln(d,g,s) + TAB = [(idn, idn, g)] + for i in range(size - 2): + b = i + testb = b in b_S and sgensx + if testb: + sgensx1 = [_af_new(_) for _ in sgensx] + deltab = _orbit(size, sgensx1, b) + else: + deltab = {b} + # p1 = min(IMAGES) = min(Union D_p*h*deltab for h in TAB) + if all_metrics_with_sym: + md = _min_dummies(dumx, sym, indices) + else: + md = [min(_orbit(size, [_af_new( + ddx) for ddx in dsgsx], ii)) for ii in range(size - 2)] + + p_i = min(min(md[h[x]] for x in deltab) for s, d, h in TAB) + dsgsx1 = [_af_new(_) for _ in dsgsx] + Dxtrav = _orbit_transversal(size, dsgsx1, p_i, False, af=True) \ + if dsgsx else None + if Dxtrav: + Dxtrav = [_af_invert(x) for x in Dxtrav] + # compute the orbit of p_i + for ii in range(num_types): + if p_i in dumx[ii]: + # the orbit is made by all the indices in dum[ii] + if sym[ii] is not None: + deltap = dumx[ii] + else: + # the orbit is made by all the even indices if p_i + # is even, by all the odd indices if p_i is odd + p_i_index = dumx[ii].index(p_i) % 2 + deltap = dumx[ii][p_i_index::2] + break + else: + deltap = [p_i] + TAB1 = [] + while TAB: + s, d, h = TAB.pop() + if min(md[h[x]] for x in deltab) != p_i: + continue + deltab1 = [x for x in deltab if md[h[x]] == p_i] + # NEXT = s*deltab1 intersection (d*g)**-1*deltap + dg = _af_rmul(d, g) + dginv = _af_invert(dg) + sdeltab = [s[x] for x in deltab1] + gdeltap = [dginv[x] for x in deltap] + NEXT = [x for x in sdeltab if x in gdeltap] + # d, s satisfy + # d*g*s*base[i-1] = p_{i-1}; using the stabilizers + # d*g*s*S_{base[0],...,base[i-1]}*base[i-1] = + # D_{p_0,...,p_{i-1}}*p_{i-1} + # so that to find d1, s1 satisfying d1*g*s1*b = p_i + # one can look for dx in D_{p_0,...,p_{i-1}} and + # sx in S_{base[0],...,base[i-1]} + # d1 = dx*d; s1 = s*sx + # d1*g*s1*b = dx*d*g*s*sx*b = p_i + for j in NEXT: + if testb: + # solve s1*b = j with s1 = s*sx for some element sx + # of the stabilizer of ..., base[i-1] + # sx*b = s**-1*j; sx = _trace_S(s, j,...) + # s1 = s*trace_S(s**-1*j,...) + s1 = _trace_S(s, j, b, S_transversals) + if not s1: + continue + else: + s1 = [s[ix] for ix in s1] + else: + s1 = s + # assert s1[b] == j # invariant + # solve d1*g*j = p_i with d1 = dx*d for some element dg + # of the stabilizer of ..., p_{i-1} + # dx**-1*p_i = d*g*j; dx**-1 = trace_D(d*g*j,...) + # d1 = trace_D(d*g*j,...)**-1*d + # to save an inversion in the inner loop; notice we did + # Dxtrav = [perm_af_invert(x) for x in Dxtrav] out of the loop + if Dxtrav: + d1 = _trace_D(dg[j], p_i, Dxtrav) + if not d1: + continue + else: + if p_i != dg[j]: + continue + d1 = idn + assert d1[dg[j]] == p_i # invariant + d1 = [d1[ix] for ix in d] + h1 = [d1[g[ix]] for ix in s1] + # assert h1[b] == p_i # invariant + TAB1.append((s1, d1, h1)) + + # if TAB contains equal permutations, keep only one of them; + # if TAB contains equal permutations up to the sign, return 0 + TAB1.sort(key=lambda x: x[-1]) + prev = [0] * size + while TAB1: + s, d, h = TAB1.pop() + if h[:-2] == prev[:-2]: + if h[-1] != prev[-1]: + return 0 + else: + TAB.append((s, d, h)) + prev = h + + # stabilize the SGS + sgensx = [h for h in sgensx if h[b] == b] + if b in b_S: + b_S.remove(b) + _dumx_remove(dumx, dumx_flat, p_i) + dsgsx = [] + for i in range(num_types): + dsgsx.extend(dummy_sgs(dumx[i], sym[i], num_dummies)) + return TAB[0][-1] + + +def canonical_free(base, gens, g, num_free): + """ + Canonicalization of a tensor with respect to free indices + choosing the minimum with respect to lexicographical ordering + in the free indices. + + Explanation + =========== + + ``base``, ``gens`` BSGS for slot permutation group + ``g`` permutation representing the tensor + ``num_free`` number of free indices + The indices must be ordered with first the free indices + + See explanation in double_coset_can_rep + The algorithm is a variation of the one given in [2]. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy.combinatorics.tensor_can import canonical_free + >>> gens = [[1, 0, 2, 3, 5, 4], [2, 3, 0, 1, 4, 5],[0, 1, 3, 2, 5, 4]] + >>> gens = [Permutation(h) for h in gens] + >>> base = [0, 2] + >>> g = Permutation([2, 1, 0, 3, 4, 5]) + >>> canonical_free(base, gens, g, 4) + [0, 3, 1, 2, 5, 4] + + Consider the product of Riemann tensors + ``T = R^{a}_{d0}^{d1,d2}*R_{d2,d1}^{d0,b}`` + The order of the indices is ``[a, b, d0, -d0, d1, -d1, d2, -d2]`` + The permutation corresponding to the tensor is + ``g = [0, 3, 4, 6, 7, 5, 2, 1, 8, 9]`` + + In particular ``a`` is position ``0``, ``b`` is in position ``9``. + Use the slot symmetries to get `T` is a form which is the minimal + in lexicographic order in the free indices ``a`` and ``b``, e.g. + ``-R^{a}_{d0}^{d1,d2}*R^{b,d0}_{d2,d1}`` corresponding to + ``[0, 3, 4, 6, 1, 2, 7, 5, 9, 8]`` + + >>> from sympy.combinatorics.tensor_can import riemann_bsgs, tensor_gens + >>> base, gens = riemann_bsgs + >>> size, sbase, sgens = tensor_gens(base, gens, [[], []], 0) + >>> g = Permutation([0, 3, 4, 6, 7, 5, 2, 1, 8, 9]) + >>> canonical_free(sbase, [Permutation(h) for h in sgens], g, 2) + [0, 3, 4, 6, 1, 2, 7, 5, 9, 8] + """ + g = g.array_form + size = len(g) + if not base: + return g[:] + + transversals = get_transversals(base, gens) + for x in sorted(g[:-2]): + if x not in base: + base.append(x) + h = g + for transv in transversals: + h_i = [size]*num_free + # find the element s in transversals[i] such that + # _af_rmul(h, s) has its free elements with the lowest position in h + s = None + for sk in transv.values(): + h1 = _af_rmul(h, sk) + hi = [h1.index(ix) for ix in range(num_free)] + if hi < h_i: + h_i = hi + s = sk + if s: + h = _af_rmul(h, s) + return h + + +def _get_map_slots(size, fixed_slots): + res = list(range(size)) + pos = 0 + for i in range(size): + if i in fixed_slots: + continue + res[i] = pos + pos += 1 + return res + + +def _lift_sgens(size, fixed_slots, free, s): + a = [] + j = k = 0 + fd = [y for _, y in sorted(zip(fixed_slots, free))] + num_free = len(free) + for i in range(size): + if i in fixed_slots: + a.append(fd[k]) + k += 1 + else: + a.append(s[j] + num_free) + j += 1 + return a + + +def canonicalize(g, dummies, msym, *v): + """ + canonicalize tensor formed by tensors + + Parameters + ========== + + g : permutation representing the tensor + + dummies : list representing the dummy indices + it can be a list of dummy indices of the same type + or a list of lists of dummy indices, one list for each + type of index; + the dummy indices must come after the free indices, + and put in order contravariant, covariant + [d0, -d0, d1,-d1,...] + + msym : symmetry of the metric(s) + it can be an integer or a list; + in the first case it is the symmetry of the dummy index metric; + in the second case it is the list of the symmetries of the + index metric for each type + + v : list, (base_i, gens_i, n_i, sym_i) for tensors of type `i` + + base_i, gens_i : BSGS for tensors of this type. + The BSGS should have minimal base under lexicographic ordering; + if not, an attempt is made do get the minimal BSGS; + in case of failure, + canonicalize_naive is used, which is much slower. + + n_i : number of tensors of type `i`. + + sym_i : symmetry under exchange of component tensors of type `i`. + + Both for msym and sym_i the cases are + * None no symmetry + * 0 commuting + * 1 anticommuting + + Returns + ======= + + 0 if the tensor is zero, else return the array form of + the permutation representing the canonical form of the tensor. + + Algorithm + ========= + + First one uses canonical_free to get the minimum tensor under + lexicographic order, using only the slot symmetries. + If the component tensors have not minimal BSGS, it is attempted + to find it; if the attempt fails canonicalize_naive + is used instead. + + Compute the residual slot symmetry keeping fixed the free indices + using tensor_gens(base, gens, list_free_indices, sym). + + Reduce the problem eliminating the free indices. + + Then use double_coset_can_rep and lift back the result reintroducing + the free indices. + + Examples + ======== + + one type of index with commuting metric; + + `A_{a b}` and `B_{a b}` antisymmetric and commuting + + `T = A_{d0 d1} * B^{d0}{}_{d2} * B^{d2 d1}` + + `ord = [d0,-d0,d1,-d1,d2,-d2]` order of the indices + + g = [1, 3, 0, 5, 4, 2, 6, 7] + + `T_c = 0` + + >>> from sympy.combinatorics.tensor_can import get_symmetric_group_sgs, canonicalize, bsgs_direct_product + >>> from sympy.combinatorics import Permutation + >>> base2a, gens2a = get_symmetric_group_sgs(2, 1) + >>> t0 = (base2a, gens2a, 1, 0) + >>> t1 = (base2a, gens2a, 2, 0) + >>> g = Permutation([1, 3, 0, 5, 4, 2, 6, 7]) + >>> canonicalize(g, range(6), 0, t0, t1) + 0 + + same as above, but with `B_{a b}` anticommuting + + `T_c = -A^{d0 d1} * B_{d0}{}^{d2} * B_{d1 d2}` + + can = [0,2,1,4,3,5,7,6] + + >>> t1 = (base2a, gens2a, 2, 1) + >>> canonicalize(g, range(6), 0, t0, t1) + [0, 2, 1, 4, 3, 5, 7, 6] + + two types of indices `[a,b,c,d,e,f]` and `[m,n]`, in this order, + both with commuting metric + + `f^{a b c}` antisymmetric, commuting + + `A_{m a}` no symmetry, commuting + + `T = f^c{}_{d a} * f^f{}_{e b} * A_m{}^d * A^{m b} * A_n{}^a * A^{n e}` + + ord = [c,f,a,-a,b,-b,d,-d,e,-e,m,-m,n,-n] + + g = [0,7,3, 1,9,5, 11,6, 10,4, 13,2, 12,8, 14,15] + + The canonical tensor is + `T_c = -f^{c a b} * f^{f d e} * A^m{}_a * A_{m d} * A^n{}_b * A_{n e}` + + can = [0,2,4, 1,6,8, 10,3, 11,7, 12,5, 13,9, 15,14] + + >>> base_f, gens_f = get_symmetric_group_sgs(3, 1) + >>> base1, gens1 = get_symmetric_group_sgs(1) + >>> base_A, gens_A = bsgs_direct_product(base1, gens1, base1, gens1) + >>> t0 = (base_f, gens_f, 2, 0) + >>> t1 = (base_A, gens_A, 4, 0) + >>> dummies = [range(2, 10), range(10, 14)] + >>> g = Permutation([0, 7, 3, 1, 9, 5, 11, 6, 10, 4, 13, 2, 12, 8, 14, 15]) + >>> canonicalize(g, dummies, [0, 0], t0, t1) + [0, 2, 4, 1, 6, 8, 10, 3, 11, 7, 12, 5, 13, 9, 15, 14] + """ + from sympy.combinatorics.testutil import canonicalize_naive + if not isinstance(msym, list): + if msym not in (0, 1, None): + raise ValueError('msym must be 0, 1 or None') + num_types = 1 + else: + num_types = len(msym) + if not all(msymx in (0, 1, None) for msymx in msym): + raise ValueError('msym entries must be 0, 1 or None') + if len(dummies) != num_types: + raise ValueError( + 'dummies and msym must have the same number of elements') + size = g.size + num_tensors = 0 + v1 = [] + for base_i, gens_i, n_i, sym_i in v: + # check that the BSGS is minimal; + # this property is used in double_coset_can_rep; + # if it is not minimal use canonicalize_naive + if not _is_minimal_bsgs(base_i, gens_i): + mbsgs = get_minimal_bsgs(base_i, gens_i) + if not mbsgs: + can = canonicalize_naive(g, dummies, msym, *v) + return can + base_i, gens_i = mbsgs + v1.append((base_i, gens_i, [[]] * n_i, sym_i)) + num_tensors += n_i + + if num_types == 1 and not isinstance(msym, list): + dummies = [dummies] + msym = [msym] + flat_dummies = [] + for dumx in dummies: + flat_dummies.extend(dumx) + + if flat_dummies and flat_dummies != list(range(flat_dummies[0], flat_dummies[-1] + 1)): + raise ValueError('dummies is not valid') + + # slot symmetry of the tensor + size1, sbase, sgens = gens_products(*v1) + if size != size1: + raise ValueError( + 'g has size %d, generators have size %d' % (size, size1)) + free = [i for i in range(size - 2) if i not in flat_dummies] + num_free = len(free) + + # g1 minimal tensor under slot symmetry + g1 = canonical_free(sbase, sgens, g, num_free) + if not flat_dummies: + return g1 + # save the sign of g1 + sign = 0 if g1[-1] == size - 1 else 1 + + # the free indices are kept fixed. + # Determine free_i, the list of slots of tensors which are fixed + # since they are occupied by free indices, which are fixed. + start = 0 + for i, (base_i, gens_i, n_i, sym_i) in enumerate(v): + free_i = [] + len_tens = gens_i[0].size - 2 + # for each component tensor get a list od fixed islots + for j in range(n_i): + # get the elements corresponding to the component tensor + h = g1[start:(start + len_tens)] + fr = [] + # get the positions of the fixed elements in h + for k in free: + if k in h: + fr.append(h.index(k)) + free_i.append(fr) + start += len_tens + v1[i] = (base_i, gens_i, free_i, sym_i) + # BSGS of the tensor with fixed free indices + # if tensor_gens fails in gens_product, use canonicalize_naive + size, sbase, sgens = gens_products(*v1) + + # reduce the permutations getting rid of the free indices + pos_free = [g1.index(x) for x in range(num_free)] + size_red = size - num_free + g1_red = [x - num_free for x in g1 if x in flat_dummies] + if sign: + g1_red.extend([size_red - 1, size_red - 2]) + else: + g1_red.extend([size_red - 2, size_red - 1]) + map_slots = _get_map_slots(size, pos_free) + sbase_red = [map_slots[i] for i in sbase if i not in pos_free] + sgens_red = [_af_new([map_slots[i] for i in y._array_form if i not in pos_free]) for y in sgens] + dummies_red = [[x - num_free for x in y] for y in dummies] + transv_red = get_transversals(sbase_red, sgens_red) + g1_red = _af_new(g1_red) + g2 = double_coset_can_rep( + dummies_red, msym, sbase_red, sgens_red, transv_red, g1_red) + if g2 == 0: + return 0 + # lift to the case with the free indices + g3 = _lift_sgens(size, pos_free, free, g2) + return g3 + + +def perm_af_direct_product(gens1, gens2, signed=True): + """ + Direct products of the generators gens1 and gens2. + + Examples + ======== + + >>> from sympy.combinatorics.tensor_can import perm_af_direct_product + >>> gens1 = [[1, 0, 2, 3], [0, 1, 3, 2]] + >>> gens2 = [[1, 0]] + >>> perm_af_direct_product(gens1, gens2, False) + [[1, 0, 2, 3, 4, 5], [0, 1, 3, 2, 4, 5], [0, 1, 2, 3, 5, 4]] + >>> gens1 = [[1, 0, 2, 3, 5, 4], [0, 1, 3, 2, 4, 5]] + >>> gens2 = [[1, 0, 2, 3]] + >>> perm_af_direct_product(gens1, gens2, True) + [[1, 0, 2, 3, 4, 5, 7, 6], [0, 1, 3, 2, 4, 5, 6, 7], [0, 1, 2, 3, 5, 4, 6, 7]] + """ + gens1 = [list(x) for x in gens1] + gens2 = [list(x) for x in gens2] + s = 2 if signed else 0 + n1 = len(gens1[0]) - s + n2 = len(gens2[0]) - s + start = list(range(n1)) + end = list(range(n1, n1 + n2)) + if signed: + gens1 = [gen[:-2] + end + [gen[-2] + n2, gen[-1] + n2] + for gen in gens1] + gens2 = [start + [x + n1 for x in gen] for gen in gens2] + else: + gens1 = [gen + end for gen in gens1] + gens2 = [start + [x + n1 for x in gen] for gen in gens2] + + res = gens1 + gens2 + + return res + + +def bsgs_direct_product(base1, gens1, base2, gens2, signed=True): + """ + Direct product of two BSGS. + + Parameters + ========== + + base1 : base of the first BSGS. + + gens1 : strong generating sequence of the first BSGS. + + base2, gens2 : similarly for the second BSGS. + + signed : flag for signed permutations. + + Examples + ======== + + >>> from sympy.combinatorics.tensor_can import (get_symmetric_group_sgs, bsgs_direct_product) + >>> base1, gens1 = get_symmetric_group_sgs(1) + >>> base2, gens2 = get_symmetric_group_sgs(2) + >>> bsgs_direct_product(base1, gens1, base2, gens2) + ([1], [(4)(1 2)]) + """ + s = 2 if signed else 0 + n1 = gens1[0].size - s + base = list(base1) + base += [x + n1 for x in base2] + gens1 = [h._array_form for h in gens1] + gens2 = [h._array_form for h in gens2] + gens = perm_af_direct_product(gens1, gens2, signed) + size = len(gens[0]) + id_af = list(range(size)) + gens = [h for h in gens if h != id_af] + if not gens: + gens = [id_af] + return base, [_af_new(h) for h in gens] + + +def get_symmetric_group_sgs(n, antisym=False): + """ + Return base, gens of the minimal BSGS for (anti)symmetric tensor + + Parameters + ========== + + n : rank of the tensor + antisym : bool + ``antisym = False`` symmetric tensor + ``antisym = True`` antisymmetric tensor + + Examples + ======== + + >>> from sympy.combinatorics.tensor_can import get_symmetric_group_sgs + >>> get_symmetric_group_sgs(3) + ([0, 1], [(4)(0 1), (4)(1 2)]) + """ + if n == 1: + return [], [_af_new(list(range(3)))] + gens = [Permutation(n - 1)(i, i + 1)._array_form for i in range(n - 1)] + if antisym == 0: + gens = [x + [n, n + 1] for x in gens] + else: + gens = [x + [n + 1, n] for x in gens] + base = list(range(n - 1)) + return base, [_af_new(h) for h in gens] + +riemann_bsgs = [0, 2], [Permutation(0, 1)(4, 5), Permutation(2, 3)(4, 5), + Permutation(5)(0, 2)(1, 3)] + + +def get_transversals(base, gens): + """ + Return transversals for the group with BSGS base, gens + """ + if not base: + return [] + stabs = _distribute_gens_by_base(base, gens) + orbits, transversals = _orbits_transversals_from_bsgs(base, stabs) + transversals = [{x: h._array_form for x, h in y.items()} for y in + transversals] + return transversals + + +def _is_minimal_bsgs(base, gens): + """ + Check if the BSGS has minimal base under lexigographic order. + + base, gens BSGS + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy.combinatorics.tensor_can import riemann_bsgs, _is_minimal_bsgs + >>> _is_minimal_bsgs(*riemann_bsgs) + True + >>> riemann_bsgs1 = ([2, 0], ([Permutation(5)(0, 1)(4, 5), Permutation(5)(0, 2)(1, 3)])) + >>> _is_minimal_bsgs(*riemann_bsgs1) + False + """ + base1 = [] + sgs1 = gens[:] + size = gens[0].size + for i in range(size): + if not all(h._array_form[i] == i for h in sgs1): + base1.append(i) + sgs1 = [h for h in sgs1 if h._array_form[i] == i] + return base1 == base + + +def get_minimal_bsgs(base, gens): + """ + Compute a minimal GSGS + + base, gens BSGS + + If base, gens is a minimal BSGS return it; else return a minimal BSGS + if it fails in finding one, it returns None + + TODO: use baseswap in the case in which if it fails in finding a + minimal BSGS + + Examples + ======== + + >>> from sympy.combinatorics import Permutation + >>> from sympy.combinatorics.tensor_can import get_minimal_bsgs + >>> riemann_bsgs1 = ([2, 0], ([Permutation(5)(0, 1)(4, 5), Permutation(5)(0, 2)(1, 3)])) + >>> get_minimal_bsgs(*riemann_bsgs1) + ([0, 2], [(0 1)(4 5), (5)(0 2)(1 3), (2 3)(4 5)]) + """ + G = PermutationGroup(gens) + base, gens = G.schreier_sims_incremental() + if not _is_minimal_bsgs(base, gens): + return None + return base, gens + + +def tensor_gens(base, gens, list_free_indices, sym=0): + """ + Returns size, res_base, res_gens BSGS for n tensors of the + same type. + + Explanation + =========== + + base, gens BSGS for tensors of this type + list_free_indices list of the slots occupied by fixed indices + for each of the tensors + + sym symmetry under commutation of two tensors + sym None no symmetry + sym 0 commuting + sym 1 anticommuting + + Examples + ======== + + >>> from sympy.combinatorics.tensor_can import tensor_gens, get_symmetric_group_sgs + + two symmetric tensors with 3 indices without free indices + + >>> base, gens = get_symmetric_group_sgs(3) + >>> tensor_gens(base, gens, [[], []]) + (8, [0, 1, 3, 4], [(7)(0 1), (7)(1 2), (7)(3 4), (7)(4 5), (7)(0 3)(1 4)(2 5)]) + + two symmetric tensors with 3 indices with free indices in slot 1 and 0 + + >>> tensor_gens(base, gens, [[1], [0]]) + (8, [0, 4], [(7)(0 2), (7)(4 5)]) + + four symmetric tensors with 3 indices, two of which with free indices + + """ + def _get_bsgs(G, base, gens, free_indices): + """ + return the BSGS for G.pointwise_stabilizer(free_indices) + """ + if not free_indices: + return base[:], gens[:] + else: + H = G.pointwise_stabilizer(free_indices) + base, sgs = H.schreier_sims_incremental() + return base, sgs + + # if not base there is no slot symmetry for the component tensors + # if list_free_indices.count([]) < 2 there is no commutation symmetry + # so there is no resulting slot symmetry + if not base and list_free_indices.count([]) < 2: + n = len(list_free_indices) + size = gens[0].size + size = n * (size - 2) + 2 + return size, [], [_af_new(list(range(size)))] + + # if any(list_free_indices) one needs to compute the pointwise + # stabilizer, so G is needed + if any(list_free_indices): + G = PermutationGroup(gens) + else: + G = None + + # no_free list of lists of indices for component tensors without fixed + # indices + no_free = [] + size = gens[0].size + id_af = list(range(size)) + num_indices = size - 2 + if not list_free_indices[0]: + no_free.append(list(range(num_indices))) + res_base, res_gens = _get_bsgs(G, base, gens, list_free_indices[0]) + for i in range(1, len(list_free_indices)): + base1, gens1 = _get_bsgs(G, base, gens, list_free_indices[i]) + res_base, res_gens = bsgs_direct_product(res_base, res_gens, + base1, gens1, 1) + if not list_free_indices[i]: + no_free.append(list(range(size - 2, size - 2 + num_indices))) + size += num_indices + nr = size - 2 + res_gens = [h for h in res_gens if h._array_form != id_af] + # if sym there are no commuting tensors stop here + if sym is None or not no_free: + if not res_gens: + res_gens = [_af_new(id_af)] + return size, res_base, res_gens + + # if the component tensors have moinimal BSGS, so is their direct + # product P; the slot symmetry group is S = P*C, where C is the group + # to (anti)commute the component tensors with no free indices + # a stabilizer has the property S_i = P_i*C_i; + # the BSGS of P*C has SGS_P + SGS_C and the base is + # the ordered union of the bases of P and C. + # If P has minimal BSGS, so has S with this base. + base_comm = [] + for i in range(len(no_free) - 1): + ind1 = no_free[i] + ind2 = no_free[i + 1] + a = list(range(ind1[0])) + a.extend(ind2) + a.extend(ind1) + base_comm.append(ind1[0]) + a.extend(list(range(ind2[-1] + 1, nr))) + if sym == 0: + a.extend([nr, nr + 1]) + else: + a.extend([nr + 1, nr]) + res_gens.append(_af_new(a)) + res_base = list(res_base) + # each base is ordered; order the union of the two bases + for i in base_comm: + if i not in res_base: + res_base.append(i) + res_base.sort() + if not res_gens: + res_gens = [_af_new(id_af)] + + return size, res_base, res_gens + + +def gens_products(*v): + """ + Returns size, res_base, res_gens BSGS for n tensors of different types. + + Explanation + =========== + + v is a sequence of (base_i, gens_i, free_i, sym_i) + where + base_i, gens_i BSGS of tensor of type `i` + free_i list of the fixed slots for each of the tensors + of type `i`; if there are `n_i` tensors of type `i` + and none of them have fixed slots, `free = [[]]*n_i` + sym 0 (1) if the tensors of type `i` (anti)commute among themselves + + Examples + ======== + + >>> from sympy.combinatorics.tensor_can import get_symmetric_group_sgs, gens_products + >>> base, gens = get_symmetric_group_sgs(2) + >>> gens_products((base, gens, [[], []], 0)) + (6, [0, 2], [(5)(0 1), (5)(2 3), (5)(0 2)(1 3)]) + >>> gens_products((base, gens, [[1], []], 0)) + (6, [2], [(5)(2 3)]) + """ + res_size, res_base, res_gens = tensor_gens(*v[0]) + for i in range(1, len(v)): + size, base, gens = tensor_gens(*v[i]) + res_base, res_gens = bsgs_direct_product(res_base, res_gens, base, + gens, 1) + res_size = res_gens[0].size + id_af = list(range(res_size)) + res_gens = [h for h in res_gens if h != id_af] + if not res_gens: + res_gens = [id_af] + return res_size, res_base, res_gens diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_coset_table.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_coset_table.py new file mode 100644 index 0000000000000000000000000000000000000000..ab3f62880445c5deb526797ee0623fe3510bcbc3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_coset_table.py @@ -0,0 +1,825 @@ +from sympy.combinatorics.fp_groups import FpGroup +from sympy.combinatorics.coset_table import (CosetTable, + coset_enumeration_r, coset_enumeration_c) +from sympy.combinatorics.coset_table import modified_coset_enumeration_r +from sympy.combinatorics.free_groups import free_group + +from sympy.testing.pytest import slow + +""" +References +========== + +[1] Holt, D., Eick, B., O'Brien, E. +"Handbook of Computational Group Theory" + +[2] John J. Cannon; Lucien A. Dimino; George Havas; Jane M. Watson +Mathematics of Computation, Vol. 27, No. 123. (Jul., 1973), pp. 463-490. +"Implementation and Analysis of the Todd-Coxeter Algorithm" + +""" + +def test_scan_1(): + # Example 5.1 from [1] + F, x, y = free_group("x, y") + f = FpGroup(F, [x**3, y**3, x**-1*y**-1*x*y]) + c = CosetTable(f, [x]) + + c.scan_and_fill(0, x) + assert c.table == [[0, 0, None, None]] + assert c.p == [0] + assert c.n == 1 + assert c.omega == [0] + + c.scan_and_fill(0, x**3) + assert c.table == [[0, 0, None, None]] + assert c.p == [0] + assert c.n == 1 + assert c.omega == [0] + + c.scan_and_fill(0, y**3) + assert c.table == [[0, 0, 1, 2], [None, None, 2, 0], [None, None, 0, 1]] + assert c.p == [0, 1, 2] + assert c.n == 3 + assert c.omega == [0, 1, 2] + + c.scan_and_fill(0, x**-1*y**-1*x*y) + assert c.table == [[0, 0, 1, 2], [None, None, 2, 0], [2, 2, 0, 1]] + assert c.p == [0, 1, 2] + assert c.n == 3 + assert c.omega == [0, 1, 2] + + c.scan_and_fill(1, x**3) + assert c.table == [[0, 0, 1, 2], [3, 4, 2, 0], [2, 2, 0, 1], \ + [4, 1, None, None], [1, 3, None, None]] + assert c.p == [0, 1, 2, 3, 4] + assert c.n == 5 + assert c.omega == [0, 1, 2, 3, 4] + + c.scan_and_fill(1, y**3) + assert c.table == [[0, 0, 1, 2], [3, 4, 2, 0], [2, 2, 0, 1], \ + [4, 1, None, None], [1, 3, None, None]] + assert c.p == [0, 1, 2, 3, 4] + assert c.n == 5 + assert c.omega == [0, 1, 2, 3, 4] + + c.scan_and_fill(1, x**-1*y**-1*x*y) + assert c.table == [[0, 0, 1, 2], [1, 1, 2, 0], [2, 2, 0, 1], \ + [None, 1, None, None], [1, 3, None, None]] + assert c.p == [0, 1, 2, 1, 1] + assert c.n == 3 + assert c.omega == [0, 1, 2] + + # Example 5.2 from [1] + f = FpGroup(F, [x**2, y**3, (x*y)**3]) + c = CosetTable(f, [x*y]) + + c.scan_and_fill(0, x*y) + assert c.table == [[1, None, None, 1], [None, 0, 0, None]] + assert c.p == [0, 1] + assert c.n == 2 + assert c.omega == [0, 1] + + c.scan_and_fill(0, x**2) + assert c.table == [[1, 1, None, 1], [0, 0, 0, None]] + assert c.p == [0, 1] + assert c.n == 2 + assert c.omega == [0, 1] + + c.scan_and_fill(0, y**3) + assert c.table == [[1, 1, 2, 1], [0, 0, 0, 2], [None, None, 1, 0]] + assert c.p == [0, 1, 2] + assert c.n == 3 + assert c.omega == [0, 1, 2] + + c.scan_and_fill(0, (x*y)**3) + assert c.table == [[1, 1, 2, 1], [0, 0, 0, 2], [None, None, 1, 0]] + assert c.p == [0, 1, 2] + assert c.n == 3 + assert c.omega == [0, 1, 2] + + c.scan_and_fill(1, x**2) + assert c.table == [[1, 1, 2, 1], [0, 0, 0, 2], [None, None, 1, 0]] + assert c.p == [0, 1, 2] + assert c.n == 3 + assert c.omega == [0, 1, 2] + + c.scan_and_fill(1, y**3) + assert c.table == [[1, 1, 2, 1], [0, 0, 0, 2], [None, None, 1, 0]] + assert c.p == [0, 1, 2] + assert c.n == 3 + assert c.omega == [0, 1, 2] + + c.scan_and_fill(1, (x*y)**3) + assert c.table == [[1, 1, 2, 1], [0, 0, 0, 2], [3, 4, 1, 0], [None, 2, 4, None], [2, None, None, 3]] + assert c.p == [0, 1, 2, 3, 4] + assert c.n == 5 + assert c.omega == [0, 1, 2, 3, 4] + + c.scan_and_fill(2, x**2) + assert c.table == [[1, 1, 2, 1], [0, 0, 0, 2], [3, 3, 1, 0], [2, 2, 3, 3], [2, None, None, 3]] + assert c.p == [0, 1, 2, 3, 3] + assert c.n == 4 + assert c.omega == [0, 1, 2, 3] + + +@slow +def test_coset_enumeration(): + # this test function contains the combined tests for the two strategies + # i.e. HLT and Felsch strategies. + + # Example 5.1 from [1] + F, x, y = free_group("x, y") + f = FpGroup(F, [x**3, y**3, x**-1*y**-1*x*y]) + C_r = coset_enumeration_r(f, [x]) + C_r.compress(); C_r.standardize() + C_c = coset_enumeration_c(f, [x]) + C_c.compress(); C_c.standardize() + table1 = [[0, 0, 1, 2], [1, 1, 2, 0], [2, 2, 0, 1]] + assert C_r.table == table1 + assert C_c.table == table1 + + # E1 from [2] Pg. 474 + F, r, s, t = free_group("r, s, t") + E1 = FpGroup(F, [t**-1*r*t*r**-2, r**-1*s*r*s**-2, s**-1*t*s*t**-2]) + C_r = coset_enumeration_r(E1, []) + C_r.compress() + C_c = coset_enumeration_c(E1, []) + C_c.compress() + table2 = [[0, 0, 0, 0, 0, 0]] + assert C_r.table == table2 + # test for issue #11449 + assert C_c.table == table2 + + # Cox group from [2] Pg. 474 + F, a, b = free_group("a, b") + Cox = FpGroup(F, [a**6, b**6, (a*b)**2, (a**2*b**2)**2, (a**3*b**3)**5]) + C_r = coset_enumeration_r(Cox, [a]) + C_r.compress(); C_r.standardize() + C_c = coset_enumeration_c(Cox, [a]) + C_c.compress(); C_c.standardize() + table3 = [[0, 0, 1, 2], + [2, 3, 4, 0], + [5, 1, 0, 6], + [1, 7, 8, 9], + [9, 10, 11, 1], + [12, 2, 9, 13], + [14, 9, 2, 11], + [3, 12, 15, 16], + [16, 17, 18, 3], + [6, 4, 3, 5], + [4, 19, 20, 21], + [21, 22, 6, 4], + [7, 5, 23, 24], + [25, 23, 5, 18], + [19, 6, 22, 26], + [24, 27, 28, 7], + [29, 8, 7, 30], + [8, 31, 32, 33], + [33, 34, 13, 8], + [10, 14, 35, 35], + [35, 36, 37, 10], + [30, 11, 10, 29], + [11, 38, 39, 14], + [13, 39, 38, 12], + [40, 15, 12, 41], + [42, 13, 34, 43], + [44, 35, 14, 45], + [15, 46, 47, 34], + [34, 48, 49, 15], + [50, 16, 21, 51], + [52, 21, 16, 49], + [17, 50, 53, 54], + [54, 55, 56, 17], + [41, 18, 17, 40], + [18, 28, 27, 25], + [26, 20, 19, 19], + [20, 57, 58, 59], + [59, 60, 51, 20], + [22, 52, 61, 23], + [23, 62, 63, 22], + [64, 24, 33, 65], + [48, 33, 24, 61], + [62, 25, 54, 66], + [67, 54, 25, 68], + [57, 26, 59, 69], + [70, 59, 26, 63], + [27, 64, 71, 72], + [72, 73, 68, 27], + [28, 41, 74, 75], + [75, 76, 30, 28], + [31, 29, 77, 78], + [79, 77, 29, 37], + [38, 30, 76, 80], + [78, 81, 82, 31], + [43, 32, 31, 42], + [32, 83, 84, 85], + [85, 86, 65, 32], + [36, 44, 87, 88], + [88, 89, 90, 36], + [45, 37, 36, 44], + [37, 82, 81, 79], + [80, 74, 41, 38], + [39, 42, 91, 92], + [92, 93, 45, 39], + [46, 40, 94, 95], + [96, 94, 40, 56], + [97, 91, 42, 82], + [83, 43, 98, 99], + [100, 98, 43, 47], + [101, 87, 44, 90], + [82, 45, 93, 97], + [95, 102, 103, 46], + [104, 47, 46, 105], + [47, 106, 107, 100], + [61, 108, 109, 48], + [105, 49, 48, 104], + [49, 110, 111, 52], + [51, 111, 110, 50], + [112, 53, 50, 113], + [114, 51, 60, 115], + [116, 61, 52, 117], + [53, 118, 119, 60], + [60, 70, 66, 53], + [55, 67, 120, 121], + [121, 122, 123, 55], + [113, 56, 55, 112], + [56, 103, 102, 96], + [69, 124, 125, 57], + [115, 58, 57, 114], + [58, 126, 127, 128], + [128, 128, 69, 58], + [66, 129, 130, 62], + [117, 63, 62, 116], + [63, 125, 124, 70], + [65, 109, 108, 64], + [131, 71, 64, 132], + [133, 65, 86, 134], + [135, 66, 70, 136], + [68, 130, 129, 67], + [137, 120, 67, 138], + [132, 68, 73, 131], + [139, 69, 128, 140], + [71, 141, 142, 86], + [86, 143, 144, 71], + [145, 72, 75, 146], + [147, 75, 72, 144], + [73, 145, 148, 120], + [120, 149, 150, 73], + [74, 151, 152, 94], + [94, 153, 146, 74], + [76, 147, 154, 77], + [77, 155, 156, 76], + [157, 78, 85, 158], + [143, 85, 78, 154], + [155, 79, 88, 159], + [160, 88, 79, 161], + [151, 80, 92, 162], + [163, 92, 80, 156], + [81, 157, 164, 165], + [165, 166, 161, 81], + [99, 107, 106, 83], + [134, 84, 83, 133], + [84, 167, 168, 169], + [169, 170, 158, 84], + [87, 171, 172, 93], + [93, 163, 159, 87], + [89, 160, 173, 174], + [174, 175, 176, 89], + [90, 90, 89, 101], + [91, 177, 178, 98], + [98, 179, 162, 91], + [180, 95, 100, 181], + [179, 100, 95, 152], + [153, 96, 121, 148], + [182, 121, 96, 183], + [177, 97, 165, 184], + [185, 165, 97, 172], + [186, 99, 169, 187], + [188, 169, 99, 178], + [171, 101, 174, 189], + [190, 174, 101, 176], + [102, 180, 191, 192], + [192, 193, 183, 102], + [103, 113, 194, 195], + [195, 196, 105, 103], + [106, 104, 197, 198], + [199, 197, 104, 109], + [110, 105, 196, 200], + [198, 201, 133, 106], + [107, 186, 202, 203], + [203, 204, 181, 107], + [108, 116, 205, 206], + [206, 207, 132, 108], + [109, 133, 201, 199], + [200, 194, 113, 110], + [111, 114, 208, 209], + [209, 210, 117, 111], + [118, 112, 211, 212], + [213, 211, 112, 123], + [214, 208, 114, 125], + [126, 115, 215, 216], + [217, 215, 115, 119], + [218, 205, 116, 130], + [125, 117, 210, 214], + [212, 219, 220, 118], + [136, 119, 118, 135], + [119, 221, 222, 217], + [122, 182, 223, 224], + [224, 225, 226, 122], + [138, 123, 122, 137], + [123, 220, 219, 213], + [124, 139, 227, 228], + [228, 229, 136, 124], + [216, 222, 221, 126], + [140, 127, 126, 139], + [127, 230, 231, 232], + [232, 233, 140, 127], + [129, 135, 234, 235], + [235, 236, 138, 129], + [130, 132, 207, 218], + [141, 131, 237, 238], + [239, 237, 131, 150], + [167, 134, 240, 241], + [242, 240, 134, 142], + [243, 234, 135, 220], + [221, 136, 229, 244], + [149, 137, 245, 246], + [247, 245, 137, 226], + [220, 138, 236, 243], + [244, 227, 139, 221], + [230, 140, 233, 248], + [238, 249, 250, 141], + [251, 142, 141, 252], + [142, 253, 254, 242], + [154, 255, 256, 143], + [252, 144, 143, 251], + [144, 257, 258, 147], + [146, 258, 257, 145], + [259, 148, 145, 260], + [261, 146, 153, 262], + [263, 154, 147, 264], + [148, 265, 266, 153], + [246, 267, 268, 149], + [260, 150, 149, 259], + [150, 250, 249, 239], + [162, 269, 270, 151], + [262, 152, 151, 261], + [152, 271, 272, 179], + [159, 273, 274, 155], + [264, 156, 155, 263], + [156, 270, 269, 163], + [158, 256, 255, 157], + [275, 164, 157, 276], + [277, 158, 170, 278], + [279, 159, 163, 280], + [161, 274, 273, 160], + [281, 173, 160, 282], + [276, 161, 166, 275], + [283, 162, 179, 284], + [164, 285, 286, 170], + [170, 188, 184, 164], + [166, 185, 189, 173], + [173, 287, 288, 166], + [241, 254, 253, 167], + [278, 168, 167, 277], + [168, 289, 290, 291], + [291, 292, 187, 168], + [189, 293, 294, 171], + [280, 172, 171, 279], + [172, 295, 296, 185], + [175, 190, 297, 297], + [297, 298, 299, 175], + [282, 176, 175, 281], + [176, 294, 293, 190], + [184, 296, 295, 177], + [284, 178, 177, 283], + [178, 300, 301, 188], + [181, 272, 271, 180], + [302, 191, 180, 303], + [304, 181, 204, 305], + [183, 266, 265, 182], + [306, 223, 182, 307], + [303, 183, 193, 302], + [308, 184, 188, 309], + [310, 189, 185, 311], + [187, 301, 300, 186], + [305, 202, 186, 304], + [312, 187, 292, 313], + [314, 297, 190, 315], + [191, 316, 317, 204], + [204, 318, 319, 191], + [320, 192, 195, 321], + [322, 195, 192, 319], + [193, 320, 323, 223], + [223, 324, 325, 193], + [194, 326, 327, 211], + [211, 328, 321, 194], + [196, 322, 329, 197], + [197, 330, 331, 196], + [332, 198, 203, 333], + [318, 203, 198, 329], + [330, 199, 206, 334], + [335, 206, 199, 336], + [326, 200, 209, 337], + [338, 209, 200, 331], + [201, 332, 339, 240], + [240, 340, 336, 201], + [202, 341, 342, 292], + [292, 343, 333, 202], + [205, 344, 345, 210], + [210, 338, 334, 205], + [207, 335, 346, 237], + [237, 347, 348, 207], + [208, 349, 350, 215], + [215, 351, 337, 208], + [352, 212, 217, 353], + [351, 217, 212, 327], + [328, 213, 224, 323], + [354, 224, 213, 355], + [349, 214, 228, 356], + [357, 228, 214, 345], + [358, 216, 232, 359], + [360, 232, 216, 350], + [344, 218, 235, 361], + [362, 235, 218, 348], + [219, 352, 363, 364], + [364, 365, 355, 219], + [222, 358, 366, 367], + [367, 368, 353, 222], + [225, 354, 369, 370], + [370, 371, 372, 225], + [307, 226, 225, 306], + [226, 268, 267, 247], + [227, 373, 374, 233], + [233, 360, 356, 227], + [229, 357, 361, 234], + [234, 375, 376, 229], + [248, 231, 230, 230], + [231, 377, 378, 379], + [379, 380, 359, 231], + [236, 362, 381, 245], + [245, 382, 383, 236], + [384, 238, 242, 385], + [340, 242, 238, 346], + [347, 239, 246, 381], + [386, 246, 239, 387], + [388, 241, 291, 389], + [343, 291, 241, 339], + [375, 243, 364, 390], + [391, 364, 243, 383], + [373, 244, 367, 392], + [393, 367, 244, 376], + [382, 247, 370, 394], + [395, 370, 247, 396], + [377, 248, 379, 397], + [398, 379, 248, 374], + [249, 384, 399, 400], + [400, 401, 387, 249], + [250, 260, 402, 403], + [403, 404, 252, 250], + [253, 251, 405, 406], + [407, 405, 251, 256], + [257, 252, 404, 408], + [406, 409, 277, 253], + [254, 388, 410, 411], + [411, 412, 385, 254], + [255, 263, 413, 414], + [414, 415, 276, 255], + [256, 277, 409, 407], + [408, 402, 260, 257], + [258, 261, 416, 417], + [417, 418, 264, 258], + [265, 259, 419, 420], + [421, 419, 259, 268], + [422, 416, 261, 270], + [271, 262, 423, 424], + [425, 423, 262, 266], + [426, 413, 263, 274], + [270, 264, 418, 422], + [420, 427, 307, 265], + [266, 303, 428, 425], + [267, 386, 429, 430], + [430, 431, 396, 267], + [268, 307, 427, 421], + [269, 283, 432, 433], + [433, 434, 280, 269], + [424, 428, 303, 271], + [272, 304, 435, 436], + [436, 437, 284, 272], + [273, 279, 438, 439], + [439, 440, 282, 273], + [274, 276, 415, 426], + [285, 275, 441, 442], + [443, 441, 275, 288], + [289, 278, 444, 445], + [446, 444, 278, 286], + [447, 438, 279, 294], + [295, 280, 434, 448], + [287, 281, 449, 450], + [451, 449, 281, 299], + [294, 282, 440, 447], + [448, 432, 283, 295], + [300, 284, 437, 452], + [442, 453, 454, 285], + [309, 286, 285, 308], + [286, 455, 456, 446], + [450, 457, 458, 287], + [311, 288, 287, 310], + [288, 454, 453, 443], + [445, 456, 455, 289], + [313, 290, 289, 312], + [290, 459, 460, 461], + [461, 462, 389, 290], + [293, 310, 463, 464], + [464, 465, 315, 293], + [296, 308, 466, 467], + [467, 468, 311, 296], + [298, 314, 469, 470], + [470, 471, 472, 298], + [315, 299, 298, 314], + [299, 458, 457, 451], + [452, 435, 304, 300], + [301, 312, 473, 474], + [474, 475, 309, 301], + [316, 302, 476, 477], + [478, 476, 302, 325], + [341, 305, 479, 480], + [481, 479, 305, 317], + [324, 306, 482, 483], + [484, 482, 306, 372], + [485, 466, 308, 454], + [455, 309, 475, 486], + [487, 463, 310, 458], + [454, 311, 468, 485], + [486, 473, 312, 455], + [459, 313, 488, 489], + [490, 488, 313, 342], + [491, 469, 314, 472], + [458, 315, 465, 487], + [477, 492, 485, 316], + [463, 317, 316, 468], + [317, 487, 493, 481], + [329, 447, 464, 318], + [468, 319, 318, 463], + [319, 467, 448, 322], + [321, 448, 467, 320], + [475, 323, 320, 466], + [432, 321, 328, 437], + [438, 329, 322, 434], + [323, 474, 452, 328], + [483, 494, 486, 324], + [466, 325, 324, 475], + [325, 485, 492, 478], + [337, 422, 433, 326], + [437, 327, 326, 432], + [327, 436, 424, 351], + [334, 426, 439, 330], + [434, 331, 330, 438], + [331, 433, 422, 338], + [333, 464, 447, 332], + [449, 339, 332, 440], + [465, 333, 343, 469], + [413, 334, 338, 418], + [336, 439, 426, 335], + [441, 346, 335, 415], + [440, 336, 340, 449], + [416, 337, 351, 423], + [339, 451, 470, 343], + [346, 443, 450, 340], + [480, 493, 487, 341], + [469, 342, 341, 465], + [342, 491, 495, 490], + [361, 407, 414, 344], + [418, 345, 344, 413], + [345, 417, 408, 357], + [381, 446, 442, 347], + [415, 348, 347, 441], + [348, 414, 407, 362], + [356, 408, 417, 349], + [423, 350, 349, 416], + [350, 425, 420, 360], + [353, 424, 436, 352], + [479, 363, 352, 435], + [428, 353, 368, 476], + [355, 452, 474, 354], + [488, 369, 354, 473], + [435, 355, 365, 479], + [402, 356, 360, 419], + [405, 361, 357, 404], + [359, 420, 425, 358], + [476, 366, 358, 428], + [427, 359, 380, 482], + [444, 381, 362, 409], + [363, 481, 477, 368], + [368, 393, 390, 363], + [365, 391, 394, 369], + [369, 490, 480, 365], + [366, 478, 483, 380], + [380, 398, 392, 366], + [371, 395, 496, 497], + [497, 498, 489, 371], + [473, 372, 371, 488], + [372, 486, 494, 484], + [392, 400, 403, 373], + [419, 374, 373, 402], + [374, 421, 430, 398], + [390, 411, 406, 375], + [404, 376, 375, 405], + [376, 403, 400, 393], + [397, 430, 421, 377], + [482, 378, 377, 427], + [378, 484, 497, 499], + [499, 499, 397, 378], + [394, 461, 445, 382], + [409, 383, 382, 444], + [383, 406, 411, 391], + [385, 450, 443, 384], + [492, 399, 384, 453], + [457, 385, 412, 493], + [387, 442, 446, 386], + [494, 429, 386, 456], + [453, 387, 401, 492], + [389, 470, 451, 388], + [493, 410, 388, 457], + [471, 389, 462, 495], + [412, 390, 393, 399], + [462, 394, 391, 410], + [401, 392, 398, 429], + [396, 445, 461, 395], + [498, 496, 395, 460], + [456, 396, 431, 494], + [431, 397, 499, 496], + [399, 477, 481, 412], + [429, 483, 478, 401], + [410, 480, 490, 462], + [496, 497, 484, 431], + [489, 495, 491, 459], + [495, 460, 459, 471], + [460, 489, 498, 498], + [472, 472, 471, 491]] + + assert C_r.table == table3 + assert C_c.table == table3 + + # Group denoted by B2,4 from [2] Pg. 474 + F, a, b = free_group("a, b") + B_2_4 = FpGroup(F, [a**4, b**4, (a*b)**4, (a**-1*b)**4, (a**2*b)**4, \ + (a*b**2)**4, (a**2*b**2)**4, (a**-1*b*a*b)**4, (a*b**-1*a*b)**4]) + C_r = coset_enumeration_r(B_2_4, [a]) + C_c = coset_enumeration_c(B_2_4, [a]) + index_r = 0 + for i in range(len(C_r.p)): + if C_r.p[i] == i: + index_r += 1 + assert index_r == 1024 + + index_c = 0 + for i in range(len(C_c.p)): + if C_c.p[i] == i: + index_c += 1 + assert index_c == 1024 + + # trivial Macdonald group G(2,2) from [2] Pg. 480 + M = FpGroup(F, [b**-1*a**-1*b*a*b**-1*a*b*a**-2, a**-1*b**-1*a*b*a**-1*b*a*b**-2]) + C_r = coset_enumeration_r(M, [a]) + C_r.compress(); C_r.standardize() + C_c = coset_enumeration_c(M, [a]) + C_c.compress(); C_c.standardize() + table4 = [[0, 0, 0, 0]] + assert C_r.table == table4 + assert C_c.table == table4 + + +def test_look_ahead(): + # Section 3.2 [Test Example] Example (d) from [2] + F, a, b, c = free_group("a, b, c") + f = FpGroup(F, [a**11, b**5, c**4, (a*c)**3, b**2*c**-1*b**-1*c, a**4*b**-1*a**-1*b]) + H = [c, b, c**2] + table0 = [[1, 2, 0, 0, 0, 0], + [3, 0, 4, 5, 6, 7], + [0, 8, 9, 10, 11, 12], + [5, 1, 10, 13, 14, 15], + [16, 5, 16, 1, 17, 18], + [4, 3, 1, 8, 19, 20], + [12, 21, 22, 23, 24, 1], + [25, 26, 27, 28, 1, 24], + [2, 10, 5, 16, 22, 28], + [10, 13, 13, 2, 29, 30]] + CosetTable.max_stack_size = 10 + C_c = coset_enumeration_c(f, H) + C_c.compress(); C_c.standardize() + assert C_c.table[: 10] == table0 + +def test_modified_methods(): + ''' + Tests for modified coset table methods. + Example 5.7 from [1] Holt, D., Eick, B., O'Brien + "Handbook of Computational Group Theory". + + ''' + F, x, y = free_group("x, y") + f = FpGroup(F, [x**3, y**5, (x*y)**2]) + H = [x*y, x**-1*y**-1*x*y*x] + C = CosetTable(f, H) + C.modified_define(0, x) + identity = C._grp.identity + a_0 = C._grp.generators[0] + a_1 = C._grp.generators[1] + + assert C.P == [[identity, None, None, None], + [None, identity, None, None]] + assert C.table == [[1, None, None, None], + [None, 0, None, None]] + + C.modified_define(1, x) + assert C.table == [[1, None, None, None], + [2, 0, None, None], + [None, 1, None, None]] + assert C.P == [[identity, None, None, None], + [identity, identity, None, None], + [None, identity, None, None]] + + C.modified_scan(0, x**3, C._grp.identity, fill=False) + assert C.P == [[identity, identity, None, None], + [identity, identity, None, None], + [identity, identity, None, None]] + assert C.table == [[1, 2, None, None], + [2, 0, None, None], + [0, 1, None, None]] + + C.modified_scan(0, x*y, C._grp.generators[0], fill=False) + assert C.P == [[identity, identity, None, a_0**-1], + [identity, identity, a_0, None], + [identity, identity, None, None]] + assert C.table == [[1, 2, None, 1], + [2, 0, 0, None], + [0, 1, None, None]] + + C.modified_define(2, y**-1) + assert C.table == [[1, 2, None, 1], + [2, 0, 0, None], + [0, 1, None, 3], + [None, None, 2, None]] + assert C.P == [[identity, identity, None, a_0**-1], + [identity, identity, a_0, None], + [identity, identity, None, identity], + [None, None, identity, None]] + + C.modified_scan(0, x**-1*y**-1*x*y*x, C._grp.generators[1]) + assert C.table == [[1, 2, None, 1], + [2, 0, 0, None], + [0, 1, None, 3], + [3, 3, 2, None]] + assert C.P == [[identity, identity, None, a_0**-1], + [identity, identity, a_0, None], + [identity, identity, None, identity], + [a_1, a_1**-1, identity, None]] + + C.modified_scan(2, (x*y)**2, C._grp.identity) + assert C.table == [[1, 2, 3, 1], + [2, 0, 0, None], + [0, 1, None, 3], + [3, 3, 2, 0]] + assert C.P == [[identity, identity, a_1**-1, a_0**-1], + [identity, identity, a_0, None], + [identity, identity, None, identity], + [a_1, a_1**-1, identity, a_1]] + + C.modified_define(2, y) + assert C.table == [[1, 2, 3, 1], + [2, 0, 0, None], + [0, 1, 4, 3], + [3, 3, 2, 0], + [None, None, None, 2]] + assert C.P == [[identity, identity, a_1**-1, a_0**-1], + [identity, identity, a_0, None], + [identity, identity, identity, identity], + [a_1, a_1**-1, identity, a_1], + [None, None, None, identity]] + + C.modified_scan(0, y**5, C._grp.identity) + assert C.table == [[1, 2, 3, 1], [2, 0, 0, 4], [0, 1, 4, 3], [3, 3, 2, 0], [None, None, 1, 2]] + assert C.P == [[identity, identity, a_1**-1, a_0**-1], + [identity, identity, a_0, a_0*a_1**-1], + [identity, identity, identity, identity], + [a_1, a_1**-1, identity, a_1], + [None, None, a_1*a_0**-1, identity]] + + C.modified_scan(1, (x*y)**2, C._grp.identity) + assert C.table == [[1, 2, 3, 1], + [2, 0, 0, 4], + [0, 1, 4, 3], + [3, 3, 2, 0], + [4, 4, 1, 2]] + assert C.P == [[identity, identity, a_1**-1, a_0**-1], + [identity, identity, a_0, a_0*a_1**-1], + [identity, identity, identity, identity], + [a_1, a_1**-1, identity, a_1], + [a_0*a_1**-1, a_1*a_0**-1, a_1*a_0**-1, identity]] + + # Modified coset enumeration test + f = FpGroup(F, [x**3, y**3, x**-1*y**-1*x*y]) + C = coset_enumeration_r(f, [x]) + C_m = modified_coset_enumeration_r(f, [x]) + assert C_m.table == C.table diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_fp_groups.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_fp_groups.py new file mode 100644 index 0000000000000000000000000000000000000000..3f57bdf8eff92a3022d8e01cd74ce98575987929 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_fp_groups.py @@ -0,0 +1,257 @@ +from sympy.core.singleton import S +from sympy.combinatorics.fp_groups import (FpGroup, low_index_subgroups, + reidemeister_presentation, FpSubgroup, + simplify_presentation) +from sympy.combinatorics.free_groups import (free_group, FreeGroup) + +from sympy.testing.pytest import slow + +""" +References +========== + +[1] Holt, D., Eick, B., O'Brien, E. +"Handbook of Computational Group Theory" + +[2] John J. Cannon; Lucien A. Dimino; George Havas; Jane M. Watson +Mathematics of Computation, Vol. 27, No. 123. (Jul., 1973), pp. 463-490. +"Implementation and Analysis of the Todd-Coxeter Algorithm" + +[3] PROC. SECOND INTERNAT. CONF. THEORY OF GROUPS, CANBERRA 1973, +pp. 347-356. "A Reidemeister-Schreier program" by George Havas. +http://staff.itee.uq.edu.au/havas/1973cdhw.pdf + +""" + +def test_low_index_subgroups(): + F, x, y = free_group("x, y") + + # Example 5.10 from [1] Pg. 194 + f = FpGroup(F, [x**2, y**3, (x*y)**4]) + L = low_index_subgroups(f, 4) + t1 = [[[0, 0, 0, 0]], + [[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 3, 3]], + [[0, 0, 1, 2], [2, 2, 2, 0], [1, 1, 0, 1]], + [[1, 1, 0, 0], [0, 0, 1, 1]]] + for i in range(len(t1)): + assert L[i].table == t1[i] + + f = FpGroup(F, [x**2, y**3, (x*y)**7]) + L = low_index_subgroups(f, 15) + t2 = [[[0, 0, 0, 0]], + [[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 4, 5], + [4, 4, 5, 3], [6, 6, 3, 4], [5, 5, 6, 6]], + [[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 4, 5], + [6, 6, 5, 3], [5, 5, 3, 4], [4, 4, 6, 6]], + [[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 4, 5], + [6, 6, 5, 3], [7, 7, 3, 4], [4, 4, 8, 9], [5, 5, 10, 11], + [11, 11, 9, 6], [9, 9, 6, 8], [12, 12, 11, 7], [8, 8, 7, 10], + [10, 10, 13, 14], [14, 14, 14, 12], [13, 13, 12, 13]], + [[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 4, 5], + [6, 6, 5, 3], [7, 7, 3, 4], [4, 4, 8, 9], [5, 5, 10, 11], + [11, 11, 9, 6], [12, 12, 6, 8], [10, 10, 11, 7], [8, 8, 7, 10], + [9, 9, 13, 14], [14, 14, 14, 12], [13, 13, 12, 13]], + [[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 4, 5], + [6, 6, 5, 3], [7, 7, 3, 4], [4, 4, 8, 9], [5, 5, 10, 11], + [11, 11, 9, 6], [12, 12, 6, 8], [13, 13, 11, 7], [8, 8, 7, 10], + [9, 9, 12, 12], [10, 10, 13, 13]], + [[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 3, 3], [2, 2, 5, 6] + , [7, 7, 6, 4], [8, 8, 4, 5], [5, 5, 8, 9], [6, 6, 9, 7], + [10, 10, 7, 8], [9, 9, 11, 12], [11, 11, 12, 10], [13, 13, 10, 11], + [12, 12, 13, 13]], + [[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 3, 3], [2, 2, 5, 6] + , [7, 7, 6, 4], [8, 8, 4, 5], [5, 5, 8, 9], [6, 6, 9, 7], + [10, 10, 7, 8], [9, 9, 11, 12], [13, 13, 12, 10], [12, 12, 10, 11], + [11, 11, 13, 13]], + [[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 4, 4] + , [7, 7, 6, 3], [8, 8, 3, 5], [5, 5, 8, 9], [6, 6, 9, 7], + [10, 10, 7, 8], [9, 9, 11, 12], [13, 13, 12, 10], [12, 12, 10, 11], + [11, 11, 13, 13]], + [[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8] + , [5, 5, 6, 3], [9, 9, 3, 5], [10, 10, 8, 4], [8, 8, 4, 7], + [6, 6, 10, 11], [7, 7, 11, 9], [12, 12, 9, 10], [11, 11, 13, 14], + [14, 14, 14, 12], [13, 13, 12, 13]], + [[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8] + , [6, 6, 6, 3], [5, 5, 3, 5], [8, 8, 8, 4], [7, 7, 4, 7]], + [[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8] + , [9, 9, 6, 3], [6, 6, 3, 5], [10, 10, 8, 4], [11, 11, 4, 7], + [5, 5, 10, 12], [7, 7, 12, 9], [8, 8, 11, 11], [13, 13, 9, 10], + [12, 12, 13, 13]], + [[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8] + , [9, 9, 6, 3], [6, 6, 3, 5], [10, 10, 8, 4], [11, 11, 4, 7], + [5, 5, 12, 11], [7, 7, 10, 10], [8, 8, 9, 12], [13, 13, 11, 9], + [12, 12, 13, 13]], + [[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8] + , [9, 9, 6, 3], [10, 10, 3, 5], [7, 7, 8, 4], [11, 11, 4, 7], + [5, 5, 9, 9], [6, 6, 11, 12], [8, 8, 12, 10], [13, 13, 10, 11], + [12, 12, 13, 13]], + [[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8] + , [9, 9, 6, 3], [10, 10, 3, 5], [7, 7, 8, 4], [11, 11, 4, 7], + [5, 5, 12, 11], [6, 6, 10, 10], [8, 8, 9, 12], [13, 13, 11, 9], + [12, 12, 13, 13]], + [[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8] + , [9, 9, 6, 3], [10, 10, 3, 5], [11, 11, 8, 4], [12, 12, 4, 7], + [5, 5, 9, 9], [6, 6, 12, 13], [7, 7, 11, 11], [8, 8, 13, 10], + [13, 13, 10, 12]], + [[1, 1, 0, 0], [0, 0, 2, 3], [4, 4, 3, 1], [5, 5, 1, 2], [2, 2, 4, 4] + , [3, 3, 6, 7], [7, 7, 7, 5], [6, 6, 5, 6]]] + for i in range(len(t2)): + assert L[i].table == t2[i] + + f = FpGroup(F, [x**2, y**3, (x*y)**7]) + L = low_index_subgroups(f, 10, [x]) + t3 = [[[0, 0, 0, 0]], + [[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 4, 5], [4, 4, 5, 3], + [6, 6, 3, 4], [5, 5, 6, 6]], + [[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 4, 5], [6, 6, 5, 3], + [5, 5, 3, 4], [4, 4, 6, 6]], + [[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8], + [6, 6, 6, 3], [5, 5, 3, 5], [8, 8, 8, 4], [7, 7, 4, 7]]] + for i in range(len(t3)): + assert L[i].table == t3[i] + + +def test_subgroup_presentations(): + F, x, y = free_group("x, y") + f = FpGroup(F, [x**3, y**5, (x*y)**2]) + H = [x*y, x**-1*y**-1*x*y*x] + p1 = reidemeister_presentation(f, H) + assert str(p1) == "((y_1, y_2), (y_1**2, y_2**3, y_2*y_1*y_2*y_1*y_2*y_1))" + + H = f.subgroup(H) + assert (H.generators, H.relators) == p1 + + f = FpGroup(F, [x**3, y**3, (x*y)**3]) + H = [x*y, x*y**-1] + p2 = reidemeister_presentation(f, H) + assert str(p2) == "((x_0, y_0), (x_0**3, y_0**3, x_0*y_0*x_0*y_0*x_0*y_0))" + + f = FpGroup(F, [x**2*y**2, y**-1*x*y*x**-3]) + H = [x] + p3 = reidemeister_presentation(f, H) + assert str(p3) == "((x_0,), (x_0**4,))" + + f = FpGroup(F, [x**3*y**-3, (x*y)**3, (x*y**-1)**2]) + H = [x] + p4 = reidemeister_presentation(f, H) + assert str(p4) == "((x_0,), (x_0**6,))" + + # this presentation can be improved, the most simplified form + # of presentation is + # See [2] Pg 474 group PSL_2(11) + # This is the group PSL_2(11) + F, a, b, c = free_group("a, b, c") + f = FpGroup(F, [a**11, b**5, c**4, (b*c**2)**2, (a*b*c)**3, (a**4*c**2)**3, b**2*c**-1*b**-1*c, a**4*b**-1*a**-1*b]) + H = [a, b, c**2] + gens, rels = reidemeister_presentation(f, H) + assert str(gens) == "(b_1, c_3)" + assert len(rels) == 18 + + +@slow +def test_order(): + F, x, y = free_group("x, y") + f = FpGroup(F, [x**4, y**2, x*y*x**-1*y]) + assert f.order() == 8 + + f = FpGroup(F, [x*y*x**-1*y**-1, y**2]) + assert f.order() is S.Infinity + + F, a, b, c = free_group("a, b, c") + f = FpGroup(F, [a**250, b**2, c*b*c**-1*b, c**4, c**-1*a**-1*c*a, a**-1*b**-1*a*b]) + assert f.order() == 2000 + + F, x = free_group("x") + f = FpGroup(F, []) + assert f.order() is S.Infinity + + f = FpGroup(free_group('')[0], []) + assert f.order() == 1 + +def test_fp_subgroup(): + def _test_subgroup(K, T, S): + _gens = T(K.generators) + assert all(elem in S for elem in _gens) + assert T.is_injective() + assert T.image().order() == S.order() + F, x, y = free_group("x, y") + f = FpGroup(F, [x**4, y**2, x*y*x**-1*y]) + S = FpSubgroup(f, [x*y]) + assert (x*y)**-3 in S + K, T = f.subgroup([x*y], homomorphism=True) + assert T(K.generators) == [y*x**-1] + _test_subgroup(K, T, S) + + S = FpSubgroup(f, [x**-1*y*x]) + assert x**-1*y**4*x in S + assert x**-1*y**4*x**2 not in S + K, T = f.subgroup([x**-1*y*x], homomorphism=True) + assert T(K.generators[0]**3) == y**3 + _test_subgroup(K, T, S) + + f = FpGroup(F, [x**3, y**5, (x*y)**2]) + H = [x*y, x**-1*y**-1*x*y*x] + K, T = f.subgroup(H, homomorphism=True) + S = FpSubgroup(f, H) + _test_subgroup(K, T, S) + +def test_permutation_methods(): + F, x, y = free_group("x, y") + # DihedralGroup(8) + G = FpGroup(F, [x**2, y**8, x*y*x**-1*y]) + T = G._to_perm_group()[1] + assert T.is_isomorphism() + assert G.center() == [y**4] + + # DiheadralGroup(4) + G = FpGroup(F, [x**2, y**4, x*y*x**-1*y]) + S = FpSubgroup(G, G.normal_closure([x])) + assert x in S + assert y**-1*x*y in S + + # Z_5xZ_4 + G = FpGroup(F, [x*y*x**-1*y**-1, y**5, x**4]) + assert G.is_abelian + assert G.is_solvable + + # AlternatingGroup(5) + G = FpGroup(F, [x**3, y**2, (x*y)**5]) + assert not G.is_solvable + + # AlternatingGroup(4) + G = FpGroup(F, [x**3, y**2, (x*y)**3]) + assert len(G.derived_series()) == 3 + S = FpSubgroup(G, G.derived_subgroup()) + assert S.order() == 4 + + +def test_simplify_presentation(): + # ref #16083 + G = simplify_presentation(FpGroup(FreeGroup([]), [])) + assert not G.generators + assert not G.relators + + # CyclicGroup(3) + # The second generator in is trivial due to relators {x^2, x^5} + F, x, y = free_group("x, y") + G = simplify_presentation(FpGroup(F, [x**2, x**5, y**3])) + assert x in G.relators + +def test_cyclic(): + F, x, y = free_group("x, y") + f = FpGroup(F, [x*y, x**-1*y**-1*x*y*x]) + assert f.is_cyclic + f = FpGroup(F, [x*y, x*y**-1]) + assert f.is_cyclic + f = FpGroup(F, [x**4, y**2, x*y*x**-1*y]) + assert not f.is_cyclic + + +def test_abelian_invariants(): + F, x, y = free_group("x, y") + f = FpGroup(F, [x*y, x**-1*y**-1*x*y*x]) + assert f.abelian_invariants() == [] + f = FpGroup(F, [x*y, x*y**-1]) + assert f.abelian_invariants() == [2] + f = FpGroup(F, [x**4, y**2, x*y*x**-1*y]) + assert f.abelian_invariants() == [2, 4] diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_free_groups.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_free_groups.py new file mode 100644 index 0000000000000000000000000000000000000000..439be4b7c5e8bb5ff592c9b7f07773e82952b3d5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_free_groups.py @@ -0,0 +1,226 @@ +from sympy.combinatorics.free_groups import free_group, FreeGroup +from sympy.core import Symbol +from sympy.testing.pytest import raises +from sympy.core.numbers import oo + +F, x, y, z = free_group("x, y, z") + + +def test_FreeGroup__init__(): + x, y, z = map(Symbol, "xyz") + + assert len(FreeGroup("x, y, z").generators) == 3 + assert len(FreeGroup(x).generators) == 1 + assert len(FreeGroup(("x", "y", "z"))) == 3 + assert len(FreeGroup((x, y, z)).generators) == 3 + + +def test_FreeGroup__getnewargs__(): + x, y, z = map(Symbol, "xyz") + assert FreeGroup("x, y, z").__getnewargs__() == ((x, y, z),) + + +def test_free_group(): + G, a, b, c = free_group("a, b, c") + assert F.generators == (x, y, z) + assert x*z**2 in F + assert x in F + assert y*z**-1 in F + assert (y*z)**0 in F + assert a not in F + assert a**0 not in F + assert len(F) == 3 + assert str(F) == '' + assert not F == G + assert F.order() is oo + assert F.is_abelian == False + assert F.center() == {F.identity} + + (e,) = free_group("") + assert e.order() == 1 + assert e.generators == () + assert e.elements == {e.identity} + assert e.is_abelian == True + + +def test_FreeGroup__hash__(): + assert hash(F) + + +def test_FreeGroup__eq__(): + assert free_group("x, y, z")[0] == free_group("x, y, z")[0] + assert free_group("x, y, z")[0] is free_group("x, y, z")[0] + + assert free_group("x, y, z")[0] != free_group("a, x, y")[0] + assert free_group("x, y, z")[0] is not free_group("a, x, y")[0] + + assert free_group("x, y")[0] != free_group("x, y, z")[0] + assert free_group("x, y")[0] is not free_group("x, y, z")[0] + + assert free_group("x, y, z")[0] != free_group("x, y")[0] + assert free_group("x, y, z")[0] is not free_group("x, y")[0] + + +def test_FreeGroup__getitem__(): + assert F[0:] == FreeGroup("x, y, z") + assert F[1:] == FreeGroup("y, z") + assert F[2:] == FreeGroup("z") + + +def test_FreeGroupElm__hash__(): + assert hash(x*y*z) + + +def test_FreeGroupElm_copy(): + f = x*y*z**3 + g = f.copy() + h = x*y*z**7 + + assert f == g + assert f != h + + +def test_FreeGroupElm_inverse(): + assert x.inverse() == x**-1 + assert (x*y).inverse() == y**-1*x**-1 + assert (y*x*y**-1).inverse() == y*x**-1*y**-1 + assert (y**2*x**-1).inverse() == x*y**-2 + + +def test_FreeGroupElm_type_error(): + raises(TypeError, lambda: 2/x) + raises(TypeError, lambda: x**2 + y**2) + raises(TypeError, lambda: x/2) + + +def test_FreeGroupElm_methods(): + assert (x**0).order() == 1 + assert (y**2).order() is oo + assert (x**-1*y).commutator(x) == y**-1*x**-1*y*x + assert len(x**2*y**-1) == 3 + assert len(x**-1*y**3*z) == 5 + + +def test_FreeGroupElm_eliminate_word(): + w = x**5*y*x**2*y**-4*x + assert w.eliminate_word( x, x**2 ) == x**10*y*x**4*y**-4*x**2 + w3 = x**2*y**3*x**-1*y + assert w3.eliminate_word(x, x**2) == x**4*y**3*x**-2*y + assert w3.eliminate_word(x, y) == y**5 + assert w3.eliminate_word(x, y**4) == y**8 + assert w3.eliminate_word(y, x**-1) == x**-3 + assert w3.eliminate_word(x, y*z) == y*z*y*z*y**3*z**-1 + assert (y**-3).eliminate_word(y, x**-1*z**-1) == z*x*z*x*z*x + #assert w3.eliminate_word(x, y*x) == y*x*y*x**2*y*x*y*x*y*x*z**3 + #assert w3.eliminate_word(x, x*y) == x*y*x**2*y*x*y*x*y*x*y*z**3 + + +def test_FreeGroupElm_array_form(): + assert (x*z).array_form == ((Symbol('x'), 1), (Symbol('z'), 1)) + assert (x**2*z*y*x**-2).array_form == \ + ((Symbol('x'), 2), (Symbol('z'), 1), (Symbol('y'), 1), (Symbol('x'), -2)) + assert (x**-2*y**-1).array_form == ((Symbol('x'), -2), (Symbol('y'), -1)) + + +def test_FreeGroupElm_letter_form(): + assert (x**3).letter_form == (Symbol('x'), Symbol('x'), Symbol('x')) + assert (x**2*z**-2*x).letter_form == \ + (Symbol('x'), Symbol('x'), -Symbol('z'), -Symbol('z'), Symbol('x')) + + +def test_FreeGroupElm_ext_rep(): + assert (x**2*z**-2*x).ext_rep == \ + (Symbol('x'), 2, Symbol('z'), -2, Symbol('x'), 1) + assert (x**-2*y**-1).ext_rep == (Symbol('x'), -2, Symbol('y'), -1) + assert (x*z).ext_rep == (Symbol('x'), 1, Symbol('z'), 1) + + +def test_FreeGroupElm__mul__pow__(): + x1 = x.group.dtype(((Symbol('x'), 1),)) + assert x**2 == x1*x + + assert (x**2*y*x**-2)**4 == x**2*y**4*x**-2 + assert (x**2)**2 == x**4 + assert (x**-1)**-1 == x + assert (x**-1)**0 == F.identity + assert (y**2)**-2 == y**-4 + + assert x**2*x**-1 == x + assert x**2*y**2*y**-1 == x**2*y + assert x*x**-1 == F.identity + + assert x/x == F.identity + assert x/x**2 == x**-1 + assert (x**2*y)/(x**2*y**-1) == x**2*y**2*x**-2 + assert (x**2*y)/(y**-1*x**2) == x**2*y*x**-2*y + + assert x*(x**-1*y*z*y**-1) == y*z*y**-1 + assert x**2*(x**-2*y**-1*z**2*y) == y**-1*z**2*y + + a = F.identity + for n in range(10): + assert a == x**n + assert a**-1 == x**-n + a *= x + + +def test_FreeGroupElm__len__(): + assert len(x**5*y*x**2*y**-4*x) == 13 + assert len(x**17) == 17 + assert len(y**0) == 0 + + +def test_FreeGroupElm_comparison(): + assert not (x*y == y*x) + assert x**0 == y**0 + + assert x**2 < y**3 + assert not x**3 < y**2 + assert x*y < x**2*y + assert x**2*y**2 < y**4 + assert not y**4 < y**-4 + assert not y**4 < x**-4 + assert y**-2 < y**2 + + assert x**2 <= y**2 + assert x**2 <= x**2 + + assert not y*z > z*y + assert x > x**-1 + + assert not x**2 >= y**2 + + +def test_FreeGroupElm_syllables(): + w = x**5*y*x**2*y**-4*x + assert w.number_syllables() == 5 + assert w.exponent_syllable(2) == 2 + assert w.generator_syllable(3) == Symbol('y') + assert w.sub_syllables(1, 2) == y + assert w.sub_syllables(3, 3) == F.identity + + +def test_FreeGroup_exponents(): + w1 = x**2*y**3 + assert w1.exponent_sum(x) == 2 + assert w1.exponent_sum(x**-1) == -2 + assert w1.generator_count(x) == 2 + + w2 = x**2*y**4*x**-3 + assert w2.exponent_sum(x) == -1 + assert w2.generator_count(x) == 5 + + +def test_FreeGroup_generators(): + assert (x**2*y**4*z**-1).contains_generators() == {x, y, z} + assert (x**-1*y**3).contains_generators() == {x, y} + + +def test_FreeGroupElm_words(): + w = x**5*y*x**2*y**-4*x + assert w.subword(2, 6) == x**3*y + assert w.subword(3, 2) == F.identity + assert w.subword(6, 10) == x**2*y**-2 + + assert w.substituted_word(0, 7, y**-1) == y**-1*x*y**-4*x + assert w.substituted_word(0, 7, y**2*x) == y**2*x**2*y**-4*x diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_galois.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_galois.py new file mode 100644 index 0000000000000000000000000000000000000000..0d2ac29a846db88444d275b72a85ce3debaeaf05 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_galois.py @@ -0,0 +1,82 @@ +"""Test groups defined by the galois module. """ + +from sympy.combinatorics.galois import ( + S4TransitiveSubgroups, S5TransitiveSubgroups, S6TransitiveSubgroups, + find_transitive_subgroups_of_S6, +) +from sympy.combinatorics.homomorphisms import is_isomorphic +from sympy.combinatorics.named_groups import ( + SymmetricGroup, AlternatingGroup, CyclicGroup, +) + + +def test_four_group(): + G = S4TransitiveSubgroups.V.get_perm_group() + A4 = AlternatingGroup(4) + assert G.is_subgroup(A4) + assert G.degree == 4 + assert G.is_transitive() + assert G.order() == 4 + assert not G.is_cyclic + + +def test_M20(): + G = S5TransitiveSubgroups.M20.get_perm_group() + S5 = SymmetricGroup(5) + A5 = AlternatingGroup(5) + assert G.is_subgroup(S5) + assert not G.is_subgroup(A5) + assert G.degree == 5 + assert G.is_transitive() + assert G.order() == 20 + + +# Setting this True means that for each of the transitive subgroups of S6, +# we run a test not only on the fixed representation, but also on one freshly +# generated by the search procedure. +INCLUDE_SEARCH_REPS = False +S6_randomized = {} +if INCLUDE_SEARCH_REPS: + S6_randomized = find_transitive_subgroups_of_S6(*list(S6TransitiveSubgroups)) + + +def get_versions_of_S6_subgroup(name): + vers = [name.get_perm_group()] + if INCLUDE_SEARCH_REPS: + vers.append(S6_randomized[name]) + return vers + + +def test_S6_transitive_subgroups(): + """ + Test enough characteristics to distinguish all 16 transitive subgroups. + """ + ts = S6TransitiveSubgroups + A6 = AlternatingGroup(6) + for name, alt, order, is_isom, not_isom in [ + (ts.C6, False, 6, CyclicGroup(6), None), + (ts.S3, False, 6, SymmetricGroup(3), None), + (ts.D6, False, 12, None, None), + (ts.A4, True, 12, None, None), + (ts.G18, False, 18, None, None), + (ts.A4xC2, False, 24, None, SymmetricGroup(4)), + (ts.S4m, False, 24, SymmetricGroup(4), None), + (ts.S4p, True, 24, None, None), + (ts.G36m, False, 36, None, None), + (ts.G36p, True, 36, None, None), + (ts.S4xC2, False, 48, None, None), + (ts.PSL2F5, True, 60, None, None), + (ts.G72, False, 72, None, None), + (ts.PGL2F5, False, 120, None, None), + (ts.A6, True, 360, None, None), + (ts.S6, False, 720, None, None), + ]: + for G in get_versions_of_S6_subgroup(name): + assert G.is_transitive() + assert G.degree == 6 + assert G.is_subgroup(A6) is alt + assert G.order() == order + if is_isom: + assert is_isomorphic(G, is_isom) + if not_isom: + assert not is_isomorphic(G, not_isom) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_generators.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_generators.py new file mode 100644 index 0000000000000000000000000000000000000000..795ef8f08f6ec212879f528c6a0c2f0bd73037f0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_generators.py @@ -0,0 +1,105 @@ +from sympy.combinatorics.generators import symmetric, cyclic, alternating, \ + dihedral, rubik +from sympy.combinatorics.permutations import Permutation +from sympy.testing.pytest import raises + +def test_generators(): + + assert list(cyclic(6)) == [ + Permutation([0, 1, 2, 3, 4, 5]), + Permutation([1, 2, 3, 4, 5, 0]), + Permutation([2, 3, 4, 5, 0, 1]), + Permutation([3, 4, 5, 0, 1, 2]), + Permutation([4, 5, 0, 1, 2, 3]), + Permutation([5, 0, 1, 2, 3, 4])] + + assert list(cyclic(10)) == [ + Permutation([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), + Permutation([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]), + Permutation([2, 3, 4, 5, 6, 7, 8, 9, 0, 1]), + Permutation([3, 4, 5, 6, 7, 8, 9, 0, 1, 2]), + Permutation([4, 5, 6, 7, 8, 9, 0, 1, 2, 3]), + Permutation([5, 6, 7, 8, 9, 0, 1, 2, 3, 4]), + Permutation([6, 7, 8, 9, 0, 1, 2, 3, 4, 5]), + Permutation([7, 8, 9, 0, 1, 2, 3, 4, 5, 6]), + Permutation([8, 9, 0, 1, 2, 3, 4, 5, 6, 7]), + Permutation([9, 0, 1, 2, 3, 4, 5, 6, 7, 8])] + + assert list(alternating(4)) == [ + Permutation([0, 1, 2, 3]), + Permutation([0, 2, 3, 1]), + Permutation([0, 3, 1, 2]), + Permutation([1, 0, 3, 2]), + Permutation([1, 2, 0, 3]), + Permutation([1, 3, 2, 0]), + Permutation([2, 0, 1, 3]), + Permutation([2, 1, 3, 0]), + Permutation([2, 3, 0, 1]), + Permutation([3, 0, 2, 1]), + Permutation([3, 1, 0, 2]), + Permutation([3, 2, 1, 0])] + + assert list(symmetric(3)) == [ + Permutation([0, 1, 2]), + Permutation([0, 2, 1]), + Permutation([1, 0, 2]), + Permutation([1, 2, 0]), + Permutation([2, 0, 1]), + Permutation([2, 1, 0])] + + assert list(symmetric(4)) == [ + Permutation([0, 1, 2, 3]), + Permutation([0, 1, 3, 2]), + Permutation([0, 2, 1, 3]), + Permutation([0, 2, 3, 1]), + Permutation([0, 3, 1, 2]), + Permutation([0, 3, 2, 1]), + Permutation([1, 0, 2, 3]), + Permutation([1, 0, 3, 2]), + Permutation([1, 2, 0, 3]), + Permutation([1, 2, 3, 0]), + Permutation([1, 3, 0, 2]), + Permutation([1, 3, 2, 0]), + Permutation([2, 0, 1, 3]), + Permutation([2, 0, 3, 1]), + Permutation([2, 1, 0, 3]), + Permutation([2, 1, 3, 0]), + Permutation([2, 3, 0, 1]), + Permutation([2, 3, 1, 0]), + Permutation([3, 0, 1, 2]), + Permutation([3, 0, 2, 1]), + Permutation([3, 1, 0, 2]), + Permutation([3, 1, 2, 0]), + Permutation([3, 2, 0, 1]), + Permutation([3, 2, 1, 0])] + + assert list(dihedral(1)) == [ + Permutation([0, 1]), Permutation([1, 0])] + + assert list(dihedral(2)) == [ + Permutation([0, 1, 2, 3]), + Permutation([1, 0, 3, 2]), + Permutation([2, 3, 0, 1]), + Permutation([3, 2, 1, 0])] + + assert list(dihedral(3)) == [ + Permutation([0, 1, 2]), + Permutation([2, 1, 0]), + Permutation([1, 2, 0]), + Permutation([0, 2, 1]), + Permutation([2, 0, 1]), + Permutation([1, 0, 2])] + + assert list(dihedral(5)) == [ + Permutation([0, 1, 2, 3, 4]), + Permutation([4, 3, 2, 1, 0]), + Permutation([1, 2, 3, 4, 0]), + Permutation([0, 4, 3, 2, 1]), + Permutation([2, 3, 4, 0, 1]), + Permutation([1, 0, 4, 3, 2]), + Permutation([3, 4, 0, 1, 2]), + Permutation([2, 1, 0, 4, 3]), + Permutation([4, 0, 1, 2, 3]), + Permutation([3, 2, 1, 0, 4])] + + raises(ValueError, lambda: rubik(1)) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_graycode.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_graycode.py new file mode 100644 index 0000000000000000000000000000000000000000..a754a3c401b07c9c12cb9bdeeefdfc94f6cb8b5c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_graycode.py @@ -0,0 +1,72 @@ +from sympy.combinatorics.graycode import (GrayCode, bin_to_gray, + random_bitstring, get_subset_from_bitstring, graycode_subsets, + gray_to_bin) +from sympy.testing.pytest import raises + +def test_graycode(): + g = GrayCode(2) + got = [] + for i in g.generate_gray(): + if i.startswith('0'): + g.skip() + got.append(i) + assert got == '00 11 10'.split() + a = GrayCode(6) + assert a.current == '0'*6 + assert a.rank == 0 + assert len(list(a.generate_gray())) == 64 + codes = ['011001', '011011', '011010', + '011110', '011111', '011101', '011100', '010100', '010101', '010111', + '010110', '010010', '010011', '010001', '010000', '110000', '110001', + '110011', '110010', '110110', '110111', '110101', '110100', '111100', + '111101', '111111', '111110', '111010', '111011', '111001', '111000', + '101000', '101001', '101011', '101010', '101110', '101111', '101101', + '101100', '100100', '100101', '100111', '100110', '100010', '100011', + '100001', '100000'] + assert list(a.generate_gray(start='011001')) == codes + assert list( + a.generate_gray(rank=GrayCode(6, start='011001').rank)) == codes + assert a.next().current == '000001' + assert a.next(2).current == '000011' + assert a.next(-1).current == '100000' + + a = GrayCode(5, start='10010') + assert a.rank == 28 + a = GrayCode(6, start='101000') + assert a.rank == 48 + + assert GrayCode(6, rank=4).current == '000110' + assert GrayCode(6, rank=4).rank == 4 + assert [GrayCode(4, start=s).rank for s in + GrayCode(4).generate_gray()] == [0, 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15] + a = GrayCode(15, rank=15) + assert a.current == '000000000001000' + + assert bin_to_gray('111') == '100' + + a = random_bitstring(5) + assert type(a) is str + assert len(a) == 5 + assert all(i in ['0', '1'] for i in a) + + assert get_subset_from_bitstring( + ['a', 'b', 'c', 'd'], '0011') == ['c', 'd'] + assert get_subset_from_bitstring('abcd', '1001') == ['a', 'd'] + assert list(graycode_subsets(['a', 'b', 'c'])) == \ + [[], ['c'], ['b', 'c'], ['b'], ['a', 'b'], ['a', 'b', 'c'], + ['a', 'c'], ['a']] + + raises(ValueError, lambda: GrayCode(0)) + raises(ValueError, lambda: GrayCode(2.2)) + raises(ValueError, lambda: GrayCode(2, start=[1, 1, 0])) + raises(ValueError, lambda: GrayCode(2, rank=2.5)) + raises(ValueError, lambda: get_subset_from_bitstring(['c', 'a', 'c'], '1100')) + raises(ValueError, lambda: list(GrayCode(3).generate_gray(start="1111"))) + + +def test_live_issue_117(): + assert bin_to_gray('0100') == '0110' + assert bin_to_gray('0101') == '0111' + for bits in ('0100', '0101'): + assert gray_to_bin(bin_to_gray(bits)) == bits diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_group_constructs.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_group_constructs.py new file mode 100644 index 0000000000000000000000000000000000000000..d0f7d6394bbc2e285650ea95d36be8e2ed5ea69e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_group_constructs.py @@ -0,0 +1,15 @@ +from sympy.combinatorics.group_constructs import DirectProduct +from sympy.combinatorics.named_groups import CyclicGroup, DihedralGroup + + +def test_direct_product_n(): + C = CyclicGroup(4) + D = DihedralGroup(4) + G = DirectProduct(C, C, C) + assert G.order() == 64 + assert G.degree == 12 + assert len(G.orbits()) == 3 + assert G.is_abelian is True + H = DirectProduct(D, C) + assert H.order() == 32 + assert H.is_abelian is False diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_group_numbers.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_group_numbers.py new file mode 100644 index 0000000000000000000000000000000000000000..743f1dcc8b642c19706687eeeddf6c9070b59166 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_group_numbers.py @@ -0,0 +1,110 @@ +from sympy.combinatorics.group_numbers import (is_nilpotent_number, + is_abelian_number, is_cyclic_number, _holder_formula, groups_count) +from sympy.ntheory.factor_ import factorint +from sympy.ntheory.generate import prime +from sympy.testing.pytest import raises +from sympy import randprime + + +def test_is_nilpotent_number(): + assert is_nilpotent_number(21) == False + assert is_nilpotent_number(randprime(1, 30)**12) == True + raises(ValueError, lambda: is_nilpotent_number(-5)) + + A056867 = [1, 2, 3, 4, 5, 7, 8, 9, 11, 13, 15, 16, 17, 19, + 23, 25, 27, 29, 31, 32, 33, 35, 37, 41, 43, 45, + 47, 49, 51, 53, 59, 61, 64, 65, 67, 69, 71, 73, + 77, 79, 81, 83, 85, 87, 89, 91, 95, 97, 99] + for n in range(1, 100): + assert is_nilpotent_number(n) == (n in A056867) + + +def test_is_abelian_number(): + assert is_abelian_number(4) == True + assert is_abelian_number(randprime(1, 2000)**2) == True + assert is_abelian_number(randprime(1000, 100000)) == True + assert is_abelian_number(60) == False + assert is_abelian_number(24) == False + raises(ValueError, lambda: is_abelian_number(-5)) + + A051532 = [1, 2, 3, 4, 5, 7, 9, 11, 13, 15, 17, 19, 23, 25, + 29, 31, 33, 35, 37, 41, 43, 45, 47, 49, 51, 53, + 59, 61, 65, 67, 69, 71, 73, 77, 79, 83, 85, 87, + 89, 91, 95, 97, 99] + for n in range(1, 100): + assert is_abelian_number(n) == (n in A051532) + + +A003277 = [1, 2, 3, 5, 7, 11, 13, 15, 17, 19, 23, 29, + 31, 33, 35, 37, 41, 43, 47, 51, 53, 59, 61, + 65, 67, 69, 71, 73, 77, 79, 83, 85, 87, 89, + 91, 95, 97] + + +def test_is_cyclic_number(): + assert is_cyclic_number(15) == True + assert is_cyclic_number(randprime(1, 2000)**2) == False + assert is_cyclic_number(randprime(1000, 100000)) == True + assert is_cyclic_number(4) == False + raises(ValueError, lambda: is_cyclic_number(-5)) + + for n in range(1, 100): + assert is_cyclic_number(n) == (n in A003277) + + +def test_holder_formula(): + # semiprime + assert _holder_formula({3, 5}) == 1 + assert _holder_formula({5, 11}) == 2 + # n in A003277 is always 1 + for n in A003277: + assert _holder_formula(set(factorint(n).keys())) == 1 + # otherwise + assert _holder_formula({2, 3, 5, 7}) == 12 + + +def test_groups_count(): + A000001 = [0, 1, 1, 1, 2, 1, 2, 1, 5, 2, 2, 1, 5, 1, + 2, 1, 14, 1, 5, 1, 5, 2, 2, 1, 15, 2, 2, + 5, 4, 1, 4, 1, 51, 1, 2, 1, 14, 1, 2, 2, + 14, 1, 6, 1, 4, 2, 2, 1, 52, 2, 5, 1, 5, + 1, 15, 2, 13, 2, 2, 1, 13, 1, 2, 4, 267, + 1, 4, 1, 5, 1, 4, 1, 50, 1, 2, 3, 4, 1, + 6, 1, 52, 15, 2, 1, 15, 1, 2, 1, 12, 1, + 10, 1, 4, 2] + for n in range(1, len(A000001)): + try: + assert groups_count(n) == A000001[n] + except ValueError: + pass + + A000679 = [1, 1, 2, 5, 14, 51, 267, 2328, 56092, 10494213, 49487367289] + for e in range(1, len(A000679)): + assert groups_count(2**e) == A000679[e] + + A090091 = [1, 1, 2, 5, 15, 67, 504, 9310, 1396077, 5937876645] + for e in range(1, len(A090091)): + assert groups_count(3**e) == A090091[e] + + A090130 = [1, 1, 2, 5, 15, 77, 684, 34297] + for e in range(1, len(A090130)): + assert groups_count(5**e) == A090130[e] + + A090140 = [1, 1, 2, 5, 15, 83, 860, 113147] + for e in range(1, len(A090140)): + assert groups_count(7**e) == A090140[e] + + A232105 = [51, 67, 77, 83, 87, 97, 101, 107, 111, 125, 131, + 145, 149, 155, 159, 173, 183, 193, 203, 207, 217] + for i in range(len(A232105)): + assert groups_count(prime(i+1)**5) == A232105[i] + + A232106 = [267, 504, 684, 860, 1192, 1476, 1944, 2264, 2876, + 4068, 4540, 6012, 7064, 7664, 8852, 10908, 13136] + for i in range(len(A232106)): + assert groups_count(prime(i+1)**6) == A232106[i] + + A232107 = [2328, 9310, 34297, 113147, 750735, 1600573, + 5546909, 9380741, 23316851, 71271069, 98488755] + for i in range(len(A232107)): + assert groups_count(prime(i+1)**7) == A232107[i] diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_homomorphisms.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_homomorphisms.py new file mode 100644 index 0000000000000000000000000000000000000000..0936bbddf46a16dccdfbaebda8d1c675c131f05a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_homomorphisms.py @@ -0,0 +1,114 @@ +from sympy.combinatorics import Permutation +from sympy.combinatorics.perm_groups import PermutationGroup +from sympy.combinatorics.homomorphisms import homomorphism, group_isomorphism, is_isomorphic +from sympy.combinatorics.free_groups import free_group +from sympy.combinatorics.fp_groups import FpGroup +from sympy.combinatorics.named_groups import AlternatingGroup, DihedralGroup, CyclicGroup +from sympy.testing.pytest import raises + +def test_homomorphism(): + # FpGroup -> PermutationGroup + F, a, b = free_group("a, b") + G = FpGroup(F, [a**3, b**3, (a*b)**2]) + + c = Permutation(3)(0, 1, 2) + d = Permutation(3)(1, 2, 3) + A = AlternatingGroup(4) + T = homomorphism(G, A, [a, b], [c, d]) + assert T(a*b**2*a**-1) == c*d**2*c**-1 + assert T.is_isomorphism() + assert T(T.invert(Permutation(3)(0, 2, 3))) == Permutation(3)(0, 2, 3) + + T = homomorphism(G, AlternatingGroup(4), G.generators) + assert T.is_trivial() + assert T.kernel().order() == G.order() + + E, e = free_group("e") + G = FpGroup(E, [e**8]) + P = PermutationGroup([Permutation(0, 1, 2, 3), Permutation(0, 2)]) + T = homomorphism(G, P, [e], [Permutation(0, 1, 2, 3)]) + assert T.image().order() == 4 + assert T(T.invert(Permutation(0, 2)(1, 3))) == Permutation(0, 2)(1, 3) + + T = homomorphism(E, AlternatingGroup(4), E.generators, [c]) + assert T.invert(c**2) == e**-1 #order(c) == 3 so c**2 == c**-1 + + # FreeGroup -> FreeGroup + T = homomorphism(F, E, [a], [e]) + assert T(a**-2*b**4*a**2).is_identity + + # FreeGroup -> FpGroup + G = FpGroup(F, [a*b*a**-1*b**-1]) + T = homomorphism(F, G, F.generators, G.generators) + assert T.invert(a**-1*b**-1*a**2) == a*b**-1 + + # PermutationGroup -> PermutationGroup + D = DihedralGroup(8) + p = Permutation(0, 1, 2, 3, 4, 5, 6, 7) + P = PermutationGroup(p) + T = homomorphism(P, D, [p], [p]) + assert T.is_injective() + assert not T.is_isomorphism() + assert T.invert(p**3) == p**3 + + T2 = homomorphism(F, P, [F.generators[0]], P.generators) + T = T.compose(T2) + assert T.domain == F + assert T.codomain == D + assert T(a*b) == p + + D3 = DihedralGroup(3) + T = homomorphism(D3, D3, D3.generators, D3.generators) + assert T.is_isomorphism() + + +def test_isomorphisms(): + + F, a, b = free_group("a, b") + E, c, d = free_group("c, d") + # Infinite groups with differently ordered relators. + G = FpGroup(F, [a**2, b**3]) + H = FpGroup(F, [b**3, a**2]) + assert is_isomorphic(G, H) + + # Trivial Case + # FpGroup -> FpGroup + H = FpGroup(F, [a**3, b**3, (a*b)**2]) + F, c, d = free_group("c, d") + G = FpGroup(F, [c**3, d**3, (c*d)**2]) + check, T = group_isomorphism(G, H) + assert check + assert T(c**3*d**2) == a**3*b**2 + + # FpGroup -> PermutationGroup + # FpGroup is converted to the equivalent isomorphic group. + F, a, b = free_group("a, b") + G = FpGroup(F, [a**3, b**3, (a*b)**2]) + H = AlternatingGroup(4) + check, T = group_isomorphism(G, H) + assert check + assert T(b*a*b**-1*a**-1*b**-1) == Permutation(0, 2, 3) + assert T(b*a*b*a**-1*b**-1) == Permutation(0, 3, 2) + + # PermutationGroup -> PermutationGroup + D = DihedralGroup(8) + p = Permutation(0, 1, 2, 3, 4, 5, 6, 7) + P = PermutationGroup(p) + assert not is_isomorphic(D, P) + + A = CyclicGroup(5) + B = CyclicGroup(7) + assert not is_isomorphic(A, B) + + # Two groups of the same prime order are isomorphic to each other. + G = FpGroup(F, [a, b**5]) + H = CyclicGroup(5) + assert G.order() == H.order() + assert is_isomorphic(G, H) + + +def test_check_homomorphism(): + a = Permutation(1,2,3,4) + b = Permutation(1,3) + G = PermutationGroup([a, b]) + raises(ValueError, lambda: homomorphism(G, G, [a], [a])) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_named_groups.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_named_groups.py new file mode 100644 index 0000000000000000000000000000000000000000..59bcb6ef3f020335de76d7a72152a0b58cbc6976 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_named_groups.py @@ -0,0 +1,70 @@ +from sympy.combinatorics.named_groups import (SymmetricGroup, CyclicGroup, + DihedralGroup, AlternatingGroup, + AbelianGroup, RubikGroup) +from sympy.testing.pytest import raises + + +def test_SymmetricGroup(): + G = SymmetricGroup(5) + elements = list(G.generate()) + assert (G.generators[0]).size == 5 + assert len(elements) == 120 + assert G.is_solvable is False + assert G.is_abelian is False + assert G.is_nilpotent is False + assert G.is_transitive() is True + H = SymmetricGroup(1) + assert H.order() == 1 + L = SymmetricGroup(2) + assert L.order() == 2 + + +def test_CyclicGroup(): + G = CyclicGroup(10) + elements = list(G.generate()) + assert len(elements) == 10 + assert (G.derived_subgroup()).order() == 1 + assert G.is_abelian is True + assert G.is_solvable is True + assert G.is_nilpotent is True + H = CyclicGroup(1) + assert H.order() == 1 + L = CyclicGroup(2) + assert L.order() == 2 + + +def test_DihedralGroup(): + G = DihedralGroup(6) + elements = list(G.generate()) + assert len(elements) == 12 + assert G.is_transitive() is True + assert G.is_abelian is False + assert G.is_solvable is True + assert G.is_nilpotent is False + H = DihedralGroup(1) + assert H.order() == 2 + L = DihedralGroup(2) + assert L.order() == 4 + assert L.is_abelian is True + assert L.is_nilpotent is True + + +def test_AlternatingGroup(): + G = AlternatingGroup(5) + elements = list(G.generate()) + assert len(elements) == 60 + assert [perm.is_even for perm in elements] == [True]*60 + H = AlternatingGroup(1) + assert H.order() == 1 + L = AlternatingGroup(2) + assert L.order() == 1 + + +def test_AbelianGroup(): + A = AbelianGroup(3, 3, 3) + assert A.order() == 27 + assert A.is_abelian is True + + +def test_RubikGroup(): + raises(ValueError, lambda: RubikGroup(1)) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_partitions.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_partitions.py new file mode 100644 index 0000000000000000000000000000000000000000..32e70e53a53aadbb17c8292bbef8f52d1144d6e0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_partitions.py @@ -0,0 +1,118 @@ +from sympy.core.sorting import ordered, default_sort_key +from sympy.combinatorics.partitions import (Partition, IntegerPartition, + RGS_enum, RGS_unrank, RGS_rank, + random_integer_partition) +from sympy.testing.pytest import raises +from sympy.utilities.iterables import partitions +from sympy.sets.sets import Set, FiniteSet + + +def test_partition_constructor(): + raises(ValueError, lambda: Partition([1, 1, 2])) + raises(ValueError, lambda: Partition([1, 2, 3], [2, 3, 4])) + raises(ValueError, lambda: Partition(1, 2, 3)) + raises(ValueError, lambda: Partition(*list(range(3)))) + + assert Partition([1, 2, 3], [4, 5]) == Partition([4, 5], [1, 2, 3]) + assert Partition({1, 2, 3}, {4, 5}) == Partition([1, 2, 3], [4, 5]) + + a = FiniteSet(1, 2, 3) + b = FiniteSet(4, 5) + assert Partition(a, b) == Partition([1, 2, 3], [4, 5]) + assert Partition({a, b}) == Partition(FiniteSet(a, b)) + assert Partition({a, b}) != Partition(a, b) + +def test_partition(): + from sympy.abc import x + + a = Partition([1, 2, 3], [4]) + b = Partition([1, 2], [3, 4]) + c = Partition([x]) + l = [a, b, c] + l.sort(key=default_sort_key) + assert l == [c, a, b] + l.sort(key=lambda w: default_sort_key(w, order='rev-lex')) + assert l == [c, a, b] + + assert (a == b) is False + assert a <= b + assert (a > b) is False + assert a != b + assert a < b + + assert (a + 2).partition == [[1, 2], [3, 4]] + assert (b - 1).partition == [[1, 2, 4], [3]] + + assert (a - 1).partition == [[1, 2, 3, 4]] + assert (a + 1).partition == [[1, 2, 4], [3]] + assert (b + 1).partition == [[1, 2], [3], [4]] + + assert a.rank == 1 + assert b.rank == 3 + + assert a.RGS == (0, 0, 0, 1) + assert b.RGS == (0, 0, 1, 1) + + +def test_integer_partition(): + # no zeros in partition + raises(ValueError, lambda: IntegerPartition(list(range(3)))) + # check fails since 1 + 2 != 100 + raises(ValueError, lambda: IntegerPartition(100, list(range(1, 3)))) + a = IntegerPartition(8, [1, 3, 4]) + b = a.next_lex() + c = IntegerPartition([1, 3, 4]) + d = IntegerPartition(8, {1: 3, 3: 1, 2: 1}) + assert a == c + assert a.integer == d.integer + assert a.conjugate == [3, 2, 2, 1] + assert (a == b) is False + assert a <= b + assert (a > b) is False + assert a != b + + for i in range(1, 11): + next = set() + prev = set() + a = IntegerPartition([i]) + ans = {IntegerPartition(p) for p in partitions(i)} + n = len(ans) + for j in range(n): + next.add(a) + a = a.next_lex() + IntegerPartition(i, a.partition) # check it by giving i + for j in range(n): + prev.add(a) + a = a.prev_lex() + IntegerPartition(i, a.partition) # check it by giving i + assert next == ans + assert prev == ans + + assert IntegerPartition([1, 2, 3]).as_ferrers() == '###\n##\n#' + assert IntegerPartition([1, 1, 3]).as_ferrers('o') == 'ooo\no\no' + assert str(IntegerPartition([1, 1, 3])) == '[3, 1, 1]' + assert IntegerPartition([1, 1, 3]).partition == [3, 1, 1] + + raises(ValueError, lambda: random_integer_partition(-1)) + assert random_integer_partition(1) == [1] + assert random_integer_partition(10, seed=[1, 3, 2, 1, 5, 1] + ) == [5, 2, 1, 1, 1] + + +def test_rgs(): + raises(ValueError, lambda: RGS_unrank(-1, 3)) + raises(ValueError, lambda: RGS_unrank(3, 0)) + raises(ValueError, lambda: RGS_unrank(10, 1)) + + raises(ValueError, lambda: Partition.from_rgs(list(range(3)), list(range(2)))) + raises(ValueError, lambda: Partition.from_rgs(list(range(1, 3)), list(range(2)))) + assert RGS_enum(-1) == 0 + assert RGS_enum(1) == 1 + assert RGS_unrank(7, 5) == [0, 0, 1, 0, 2] + assert RGS_unrank(23, 14) == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 2] + assert RGS_rank(RGS_unrank(40, 100)) == 40 + +def test_ordered_partition_9608(): + a = Partition([1, 2, 3], [4]) + b = Partition([1, 2], [3, 4]) + assert list(ordered([a,b], Set._infimum_key)) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_pc_groups.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_pc_groups.py new file mode 100644 index 0000000000000000000000000000000000000000..b0c146279921e1e6499534fe9e33b993348d1503 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_pc_groups.py @@ -0,0 +1,87 @@ +from sympy.combinatorics.permutations import Permutation +from sympy.combinatorics.named_groups import SymmetricGroup, AlternatingGroup, DihedralGroup +from sympy.matrices import Matrix + +def test_pc_presentation(): + Groups = [SymmetricGroup(3), SymmetricGroup(4), SymmetricGroup(9).sylow_subgroup(3), + SymmetricGroup(9).sylow_subgroup(2), SymmetricGroup(8).sylow_subgroup(2), DihedralGroup(10)] + + S = SymmetricGroup(125).sylow_subgroup(5) + G = S.derived_series()[2] + Groups.append(G) + + G = SymmetricGroup(25).sylow_subgroup(5) + Groups.append(G) + + S = SymmetricGroup(11**2).sylow_subgroup(11) + G = S.derived_series()[2] + Groups.append(G) + + for G in Groups: + PcGroup = G.polycyclic_group() + collector = PcGroup.collector + pc_presentation = collector.pc_presentation + + pcgs = PcGroup.pcgs + free_group = collector.free_group + free_to_perm = {} + for s, g in zip(free_group.symbols, pcgs): + free_to_perm[s] = g + + for k, v in pc_presentation.items(): + k_array = k.array_form + if v != (): + v_array = v.array_form + + lhs = Permutation() + for gen in k_array: + s = gen[0] + e = gen[1] + lhs = lhs*free_to_perm[s]**e + + if v == (): + assert lhs.is_identity + continue + + rhs = Permutation() + for gen in v_array: + s = gen[0] + e = gen[1] + rhs = rhs*free_to_perm[s]**e + + assert lhs == rhs + + +def test_exponent_vector(): + + Groups = [SymmetricGroup(3), SymmetricGroup(4), SymmetricGroup(9).sylow_subgroup(3), + SymmetricGroup(9).sylow_subgroup(2), SymmetricGroup(8).sylow_subgroup(2)] + + for G in Groups: + PcGroup = G.polycyclic_group() + collector = PcGroup.collector + + pcgs = PcGroup.pcgs + # free_group = collector.free_group + + for gen in G.generators: + exp = collector.exponent_vector(gen) + g = Permutation() + for i in range(len(exp)): + g = g*pcgs[i]**exp[i] if exp[i] else g + assert g == gen + + +def test_induced_pcgs(): + G = [SymmetricGroup(9).sylow_subgroup(3), SymmetricGroup(20).sylow_subgroup(2), AlternatingGroup(4), + DihedralGroup(4), DihedralGroup(10), DihedralGroup(9), SymmetricGroup(3), SymmetricGroup(4)] + + for g in G: + PcGroup = g.polycyclic_group() + collector = PcGroup.collector + gens = list(g.generators) + ipcgs = collector.induced_pcgs(gens) + m = [] + for i in ipcgs: + m.append(collector.exponent_vector(i)) + assert Matrix(m).is_upper diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_perm_groups.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_perm_groups.py new file mode 100644 index 0000000000000000000000000000000000000000..763b8fb0ae357500d68c29fe1c9e6b156e224949 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_perm_groups.py @@ -0,0 +1,1243 @@ +from sympy.core.containers import Tuple +from sympy.combinatorics.generators import rubik_cube_generators +from sympy.combinatorics.homomorphisms import is_isomorphic +from sympy.combinatorics.named_groups import SymmetricGroup, CyclicGroup,\ + DihedralGroup, AlternatingGroup, AbelianGroup, RubikGroup +from sympy.combinatorics.perm_groups import (PermutationGroup, + _orbit_transversal, Coset, SymmetricPermutationGroup) +from sympy.combinatorics.permutations import Permutation +from sympy.combinatorics.polyhedron import tetrahedron as Tetra, cube +from sympy.combinatorics.testutil import _verify_bsgs, _verify_centralizer,\ + _verify_normal_closure +from sympy.testing.pytest import skip, XFAIL, slow + +rmul = Permutation.rmul + + +def test_has(): + a = Permutation([1, 0]) + G = PermutationGroup([a]) + assert G.is_abelian + a = Permutation([2, 0, 1]) + b = Permutation([2, 1, 0]) + G = PermutationGroup([a, b]) + assert not G.is_abelian + + G = PermutationGroup([a]) + assert G.has(a) + assert not G.has(b) + + a = Permutation([2, 0, 1, 3, 4, 5]) + b = Permutation([0, 2, 1, 3, 4]) + assert PermutationGroup(a, b).degree == \ + PermutationGroup(a, b).degree == 6 + + g = PermutationGroup(Permutation(0, 2, 1)) + assert Tuple(1, g).has(g) + + +def test_generate(): + a = Permutation([1, 0]) + g = list(PermutationGroup([a]).generate()) + assert g == [Permutation([0, 1]), Permutation([1, 0])] + assert len(list(PermutationGroup(Permutation((0, 1))).generate())) == 1 + g = PermutationGroup([a]).generate(method='dimino') + assert list(g) == [Permutation([0, 1]), Permutation([1, 0])] + a = Permutation([2, 0, 1]) + b = Permutation([2, 1, 0]) + G = PermutationGroup([a, b]) + g = G.generate() + v1 = [p.array_form for p in list(g)] + v1.sort() + assert v1 == [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, + 1], [2, 1, 0]] + v2 = list(G.generate(method='dimino', af=True)) + assert v1 == sorted(v2) + a = Permutation([2, 0, 1, 3, 4, 5]) + b = Permutation([2, 1, 3, 4, 5, 0]) + g = PermutationGroup([a, b]).generate(af=True) + assert len(list(g)) == 360 + + +def test_order(): + a = Permutation([2, 0, 1, 3, 4, 5, 6, 7, 8, 9]) + b = Permutation([2, 1, 3, 4, 5, 6, 7, 8, 9, 0]) + g = PermutationGroup([a, b]) + assert g.order() == 1814400 + assert PermutationGroup().order() == 1 + + +def test_equality(): + p_1 = Permutation(0, 1, 3) + p_2 = Permutation(0, 2, 3) + p_3 = Permutation(0, 1, 2) + p_4 = Permutation(0, 1, 3) + g_1 = PermutationGroup(p_1, p_2) + g_2 = PermutationGroup(p_3, p_4) + g_3 = PermutationGroup(p_2, p_1) + g_4 = PermutationGroup(p_1, p_2) + + assert g_1 != g_2 + assert g_1.generators != g_2.generators + assert g_1.equals(g_2) + assert g_1 != g_3 + assert g_1.equals(g_3) + assert g_1 == g_4 + + +def test_stabilizer(): + S = SymmetricGroup(2) + H = S.stabilizer(0) + assert H.generators == [Permutation(1)] + a = Permutation([2, 0, 1, 3, 4, 5]) + b = Permutation([2, 1, 3, 4, 5, 0]) + G = PermutationGroup([a, b]) + G0 = G.stabilizer(0) + assert G0.order() == 60 + + gens_cube = [[1, 3, 5, 7, 0, 2, 4, 6], [1, 3, 0, 2, 5, 7, 4, 6]] + gens = [Permutation(p) for p in gens_cube] + G = PermutationGroup(gens) + G2 = G.stabilizer(2) + assert G2.order() == 6 + G2_1 = G2.stabilizer(1) + v = list(G2_1.generate(af=True)) + assert v == [[0, 1, 2, 3, 4, 5, 6, 7], [3, 1, 2, 0, 7, 5, 6, 4]] + + gens = ( + (1, 2, 0, 4, 5, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19), + (0, 1, 2, 3, 4, 5, 19, 6, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 7, 17, 18), + (0, 1, 2, 3, 4, 5, 6, 7, 9, 18, 16, 11, 12, 13, 14, 15, 8, 17, 10, 19)) + gens = [Permutation(p) for p in gens] + G = PermutationGroup(gens) + G2 = G.stabilizer(2) + assert G2.order() == 181440 + S = SymmetricGroup(3) + assert [G.order() for G in S.basic_stabilizers] == [6, 2] + + +def test_center(): + # the center of the dihedral group D_n is of order 2 for even n + for i in (4, 6, 10): + D = DihedralGroup(i) + assert (D.center()).order() == 2 + # the center of the dihedral group D_n is of order 1 for odd n>2 + for i in (3, 5, 7): + D = DihedralGroup(i) + assert (D.center()).order() == 1 + # the center of an abelian group is the group itself + for i in (2, 3, 5): + for j in (1, 5, 7): + for k in (1, 1, 11): + G = AbelianGroup(i, j, k) + assert G.center().is_subgroup(G) + # the center of a nonabelian simple group is trivial + for i in(1, 5, 9): + A = AlternatingGroup(i) + assert (A.center()).order() == 1 + # brute-force verifications + D = DihedralGroup(5) + A = AlternatingGroup(3) + C = CyclicGroup(4) + G.is_subgroup(D*A*C) + assert _verify_centralizer(G, G) + + +def test_centralizer(): + # the centralizer of the trivial group is the entire group + S = SymmetricGroup(2) + assert S.centralizer(Permutation(list(range(2)))).is_subgroup(S) + A = AlternatingGroup(5) + assert A.centralizer(Permutation(list(range(5)))).is_subgroup(A) + # a centralizer in the trivial group is the trivial group itself + triv = PermutationGroup([Permutation([0, 1, 2, 3])]) + D = DihedralGroup(4) + assert triv.centralizer(D).is_subgroup(triv) + # brute-force verifications for centralizers of groups + for i in (4, 5, 6): + S = SymmetricGroup(i) + A = AlternatingGroup(i) + C = CyclicGroup(i) + D = DihedralGroup(i) + for gp in (S, A, C, D): + for gp2 in (S, A, C, D): + if not gp2.is_subgroup(gp): + assert _verify_centralizer(gp, gp2) + # verify the centralizer for all elements of several groups + S = SymmetricGroup(5) + elements = list(S.generate_dimino()) + for element in elements: + assert _verify_centralizer(S, element) + A = AlternatingGroup(5) + elements = list(A.generate_dimino()) + for element in elements: + assert _verify_centralizer(A, element) + D = DihedralGroup(7) + elements = list(D.generate_dimino()) + for element in elements: + assert _verify_centralizer(D, element) + # verify centralizers of small groups within small groups + small = [] + for i in (1, 2, 3): + small.append(SymmetricGroup(i)) + small.append(AlternatingGroup(i)) + small.append(DihedralGroup(i)) + small.append(CyclicGroup(i)) + for gp in small: + for gp2 in small: + if gp.degree == gp2.degree: + assert _verify_centralizer(gp, gp2) + + +def test_coset_rank(): + gens_cube = [[1, 3, 5, 7, 0, 2, 4, 6], [1, 3, 0, 2, 5, 7, 4, 6]] + gens = [Permutation(p) for p in gens_cube] + G = PermutationGroup(gens) + i = 0 + for h in G.generate(af=True): + rk = G.coset_rank(h) + assert rk == i + h1 = G.coset_unrank(rk, af=True) + assert h == h1 + i += 1 + assert G.coset_unrank(48) is None + assert G.coset_unrank(G.coset_rank(gens[0])) == gens[0] + + +def test_coset_factor(): + a = Permutation([0, 2, 1]) + G = PermutationGroup([a]) + c = Permutation([2, 1, 0]) + assert not G.coset_factor(c) + assert G.coset_rank(c) is None + + a = Permutation([2, 0, 1, 3, 4, 5]) + b = Permutation([2, 1, 3, 4, 5, 0]) + g = PermutationGroup([a, b]) + assert g.order() == 360 + d = Permutation([1, 0, 2, 3, 4, 5]) + assert not g.coset_factor(d.array_form) + assert not g.contains(d) + assert Permutation(2) in G + c = Permutation([1, 0, 2, 3, 5, 4]) + v = g.coset_factor(c, True) + tr = g.basic_transversals + p = Permutation.rmul(*[tr[i][v[i]] for i in range(len(g.base))]) + assert p == c + v = g.coset_factor(c) + p = Permutation.rmul(*v) + assert p == c + assert g.contains(c) + G = PermutationGroup([Permutation([2, 1, 0])]) + p = Permutation([1, 0, 2]) + assert G.coset_factor(p) == [] + + +def test_orbits(): + a = Permutation([2, 0, 1]) + b = Permutation([2, 1, 0]) + g = PermutationGroup([a, b]) + assert g.orbit(0) == {0, 1, 2} + assert g.orbits() == [{0, 1, 2}] + assert g.is_transitive() and g.is_transitive(strict=False) + assert g.orbit_transversal(0) == \ + [Permutation( + [0, 1, 2]), Permutation([2, 0, 1]), Permutation([1, 2, 0])] + assert g.orbit_transversal(0, True) == \ + [(0, Permutation([0, 1, 2])), (2, Permutation([2, 0, 1])), + (1, Permutation([1, 2, 0]))] + + G = DihedralGroup(6) + transversal, slps = _orbit_transversal(G.degree, G.generators, 0, True, slp=True) + for i, t in transversal: + slp = slps[i] + w = G.identity + for s in slp: + w = G.generators[s]*w + assert w == t + + a = Permutation(list(range(1, 100)) + [0]) + G = PermutationGroup([a]) + assert [min(o) for o in G.orbits()] == [0] + G = PermutationGroup(rubik_cube_generators()) + assert [min(o) for o in G.orbits()] == [0, 1] + assert not G.is_transitive() and not G.is_transitive(strict=False) + G = PermutationGroup([Permutation(0, 1, 3), Permutation(3)(0, 1)]) + assert not G.is_transitive() and G.is_transitive(strict=False) + assert PermutationGroup( + Permutation(3)).is_transitive(strict=False) is False + + +def test_is_normal(): + gens_s5 = [Permutation(p) for p in [[1, 2, 3, 4, 0], [2, 1, 4, 0, 3]]] + G1 = PermutationGroup(gens_s5) + assert G1.order() == 120 + gens_a5 = [Permutation(p) for p in [[1, 0, 3, 2, 4], [2, 1, 4, 3, 0]]] + G2 = PermutationGroup(gens_a5) + assert G2.order() == 60 + assert G2.is_normal(G1) + gens3 = [Permutation(p) for p in [[2, 1, 3, 0, 4], [1, 2, 0, 3, 4]]] + G3 = PermutationGroup(gens3) + assert not G3.is_normal(G1) + assert G3.order() == 12 + G4 = G1.normal_closure(G3.generators) + assert G4.order() == 60 + gens5 = [Permutation(p) for p in [[1, 2, 3, 0, 4], [1, 2, 0, 3, 4]]] + G5 = PermutationGroup(gens5) + assert G5.order() == 24 + G6 = G1.normal_closure(G5.generators) + assert G6.order() == 120 + assert G1.is_subgroup(G6) + assert not G1.is_subgroup(G4) + assert G2.is_subgroup(G4) + I5 = PermutationGroup(Permutation(4)) + assert I5.is_normal(G5) + assert I5.is_normal(G6, strict=False) + p1 = Permutation([1, 0, 2, 3, 4]) + p2 = Permutation([0, 1, 2, 4, 3]) + p3 = Permutation([3, 4, 2, 1, 0]) + id_ = Permutation([0, 1, 2, 3, 4]) + H = PermutationGroup([p1, p3]) + H_n1 = PermutationGroup([p1, p2]) + H_n2_1 = PermutationGroup(p1) + H_n2_2 = PermutationGroup(p2) + H_id = PermutationGroup(id_) + assert H_n1.is_normal(H) + assert H_n2_1.is_normal(H_n1) + assert H_n2_2.is_normal(H_n1) + assert H_id.is_normal(H_n2_1) + assert H_id.is_normal(H_n1) + assert H_id.is_normal(H) + assert not H_n2_1.is_normal(H) + assert not H_n2_2.is_normal(H) + + +def test_eq(): + a = [[1, 2, 0, 3, 4, 5], [1, 0, 2, 3, 4, 5], [2, 1, 0, 3, 4, 5], [ + 1, 2, 0, 3, 4, 5]] + a = [Permutation(p) for p in a + [[1, 2, 3, 4, 5, 0]]] + g = Permutation([1, 2, 3, 4, 5, 0]) + G1, G2, G3 = [PermutationGroup(x) for x in [a[:2], a[2:4], [g, g**2]]] + assert G1.order() == G2.order() == G3.order() == 6 + assert G1.is_subgroup(G2) + assert not G1.is_subgroup(G3) + G4 = PermutationGroup([Permutation([0, 1])]) + assert not G1.is_subgroup(G4) + assert G4.is_subgroup(G1, 0) + assert PermutationGroup(g, g).is_subgroup(PermutationGroup(g)) + assert SymmetricGroup(3).is_subgroup(SymmetricGroup(4), 0) + assert SymmetricGroup(3).is_subgroup(SymmetricGroup(3)*CyclicGroup(5), 0) + assert not CyclicGroup(5).is_subgroup(SymmetricGroup(3)*CyclicGroup(5), 0) + assert CyclicGroup(3).is_subgroup(SymmetricGroup(3)*CyclicGroup(5), 0) + + +def test_derived_subgroup(): + a = Permutation([1, 0, 2, 4, 3]) + b = Permutation([0, 1, 3, 2, 4]) + G = PermutationGroup([a, b]) + C = G.derived_subgroup() + assert C.order() == 3 + assert C.is_normal(G) + assert C.is_subgroup(G, 0) + assert not G.is_subgroup(C, 0) + gens_cube = [[1, 3, 5, 7, 0, 2, 4, 6], [1, 3, 0, 2, 5, 7, 4, 6]] + gens = [Permutation(p) for p in gens_cube] + G = PermutationGroup(gens) + C = G.derived_subgroup() + assert C.order() == 12 + + +def test_is_solvable(): + a = Permutation([1, 2, 0]) + b = Permutation([1, 0, 2]) + G = PermutationGroup([a, b]) + assert G.is_solvable + G = PermutationGroup([a]) + assert G.is_solvable + a = Permutation([1, 2, 3, 4, 0]) + b = Permutation([1, 0, 2, 3, 4]) + G = PermutationGroup([a, b]) + assert not G.is_solvable + P = SymmetricGroup(10) + S = P.sylow_subgroup(3) + assert S.is_solvable + +def test_rubik1(): + gens = rubik_cube_generators() + gens1 = [gens[-1]] + [p**2 for p in gens[1:]] + G1 = PermutationGroup(gens1) + assert G1.order() == 19508428800 + gens2 = [p**2 for p in gens] + G2 = PermutationGroup(gens2) + assert G2.order() == 663552 + assert G2.is_subgroup(G1, 0) + C1 = G1.derived_subgroup() + assert C1.order() == 4877107200 + assert C1.is_subgroup(G1, 0) + assert not G2.is_subgroup(C1, 0) + + G = RubikGroup(2) + assert G.order() == 3674160 + + +@XFAIL +def test_rubik(): + skip('takes too much time') + G = PermutationGroup(rubik_cube_generators()) + assert G.order() == 43252003274489856000 + G1 = PermutationGroup(G[:3]) + assert G1.order() == 170659735142400 + assert not G1.is_normal(G) + G2 = G.normal_closure(G1.generators) + assert G2.is_subgroup(G) + + +def test_direct_product(): + C = CyclicGroup(4) + D = DihedralGroup(4) + G = C*C*C + assert G.order() == 64 + assert G.degree == 12 + assert len(G.orbits()) == 3 + assert G.is_abelian is True + H = D*C + assert H.order() == 32 + assert H.is_abelian is False + + +def test_orbit_rep(): + G = DihedralGroup(6) + assert G.orbit_rep(1, 3) in [Permutation([2, 3, 4, 5, 0, 1]), + Permutation([4, 3, 2, 1, 0, 5])] + H = CyclicGroup(4)*G + assert H.orbit_rep(1, 5) is False + + +def test_schreier_vector(): + G = CyclicGroup(50) + v = [0]*50 + v[23] = -1 + assert G.schreier_vector(23) == v + H = DihedralGroup(8) + assert H.schreier_vector(2) == [0, 1, -1, 0, 0, 1, 0, 0] + L = SymmetricGroup(4) + assert L.schreier_vector(1) == [1, -1, 0, 0] + + +def test_random_pr(): + D = DihedralGroup(6) + r = 11 + n = 3 + _random_prec_n = {} + _random_prec_n[0] = {'s': 7, 't': 3, 'x': 2, 'e': -1} + _random_prec_n[1] = {'s': 5, 't': 5, 'x': 1, 'e': -1} + _random_prec_n[2] = {'s': 3, 't': 4, 'x': 2, 'e': 1} + D._random_pr_init(r, n, _random_prec_n=_random_prec_n) + assert D._random_gens[11] == [0, 1, 2, 3, 4, 5] + _random_prec = {'s': 2, 't': 9, 'x': 1, 'e': -1} + assert D.random_pr(_random_prec=_random_prec) == \ + Permutation([0, 5, 4, 3, 2, 1]) + + +def test_is_alt_sym(): + G = DihedralGroup(10) + assert G.is_alt_sym() is False + assert G._eval_is_alt_sym_naive() is False + assert G._eval_is_alt_sym_naive(only_alt=True) is False + assert G._eval_is_alt_sym_naive(only_sym=True) is False + + S = SymmetricGroup(10) + assert S._eval_is_alt_sym_naive() is True + assert S._eval_is_alt_sym_naive(only_alt=True) is False + assert S._eval_is_alt_sym_naive(only_sym=True) is True + + N_eps = 10 + _random_prec = {'N_eps': N_eps, + 0: Permutation([[2], [1, 4], [0, 6, 7, 8, 9, 3, 5]]), + 1: Permutation([[1, 8, 7, 6, 3, 5, 2, 9], [0, 4]]), + 2: Permutation([[5, 8], [4, 7], [0, 1, 2, 3, 6, 9]]), + 3: Permutation([[3], [0, 8, 2, 7, 4, 1, 6, 9, 5]]), + 4: Permutation([[8], [4, 7, 9], [3, 6], [0, 5, 1, 2]]), + 5: Permutation([[6], [0, 2, 4, 5, 1, 8, 3, 9, 7]]), + 6: Permutation([[6, 9, 8], [4, 5], [1, 3, 7], [0, 2]]), + 7: Permutation([[4], [0, 2, 9, 1, 3, 8, 6, 5, 7]]), + 8: Permutation([[1, 5, 6, 3], [0, 2, 7, 8, 4, 9]]), + 9: Permutation([[8], [6, 7], [2, 3, 4, 5], [0, 1, 9]])} + assert S.is_alt_sym(_random_prec=_random_prec) is True + + A = AlternatingGroup(10) + assert A._eval_is_alt_sym_naive() is True + assert A._eval_is_alt_sym_naive(only_alt=True) is True + assert A._eval_is_alt_sym_naive(only_sym=True) is False + + _random_prec = {'N_eps': N_eps, + 0: Permutation([[1, 6, 4, 2, 7, 8, 5, 9, 3], [0]]), + 1: Permutation([[1], [0, 5, 8, 4, 9, 2, 3, 6, 7]]), + 2: Permutation([[1, 9, 8, 3, 2, 5], [0, 6, 7, 4]]), + 3: Permutation([[6, 8, 9], [4, 5], [1, 3, 7, 2], [0]]), + 4: Permutation([[8], [5], [4], [2, 6, 9, 3], [1], [0, 7]]), + 5: Permutation([[3, 6], [0, 8, 1, 7, 5, 9, 4, 2]]), + 6: Permutation([[5], [2, 9], [1, 8, 3], [0, 4, 7, 6]]), + 7: Permutation([[1, 8, 4, 7, 2, 3], [0, 6, 9, 5]]), + 8: Permutation([[5, 8, 7], [3], [1, 4, 2, 6], [0, 9]]), + 9: Permutation([[4, 9, 6], [3, 8], [1, 2], [0, 5, 7]])} + assert A.is_alt_sym(_random_prec=_random_prec) is False + + G = PermutationGroup( + Permutation(1, 3, size=8)(0, 2, 4, 6), + Permutation(5, 7, size=8)(0, 2, 4, 6)) + assert G.is_alt_sym() is False + + # Tests for monte-carlo c_n parameter setting, and which guarantees + # to give False. + G = DihedralGroup(10) + assert G._eval_is_alt_sym_monte_carlo() is False + G = DihedralGroup(20) + assert G._eval_is_alt_sym_monte_carlo() is False + + # A dry-running test to check if it looks up for the updated cache. + G = DihedralGroup(6) + G.is_alt_sym() + assert G.is_alt_sym() is False + + +def test_minimal_block(): + D = DihedralGroup(6) + block_system = D.minimal_block([0, 3]) + for i in range(3): + assert block_system[i] == block_system[i + 3] + S = SymmetricGroup(6) + assert S.minimal_block([0, 1]) == [0, 0, 0, 0, 0, 0] + + assert Tetra.pgroup.minimal_block([0, 1]) == [0, 0, 0, 0] + + P1 = PermutationGroup(Permutation(1, 5)(2, 4), Permutation(0, 1, 2, 3, 4, 5)) + P2 = PermutationGroup(Permutation(0, 1, 2, 3, 4, 5), Permutation(1, 5)(2, 4)) + assert P1.minimal_block([0, 2]) == [0, 1, 0, 1, 0, 1] + assert P2.minimal_block([0, 2]) == [0, 1, 0, 1, 0, 1] + + +def test_minimal_blocks(): + P = PermutationGroup(Permutation(1, 5)(2, 4), Permutation(0, 1, 2, 3, 4, 5)) + assert P.minimal_blocks() == [[0, 1, 0, 1, 0, 1], [0, 1, 2, 0, 1, 2]] + + P = SymmetricGroup(5) + assert P.minimal_blocks() == [[0]*5] + + P = PermutationGroup(Permutation(0, 3)) + assert P.minimal_blocks() is False + + +def test_max_div(): + S = SymmetricGroup(10) + assert S.max_div == 5 + + +def test_is_primitive(): + S = SymmetricGroup(5) + assert S.is_primitive() is True + C = CyclicGroup(7) + assert C.is_primitive() is True + + a = Permutation(0, 1, 2, size=6) + b = Permutation(3, 4, 5, size=6) + G = PermutationGroup(a, b) + assert G.is_primitive() is False + + +def test_random_stab(): + S = SymmetricGroup(5) + _random_el = Permutation([1, 3, 2, 0, 4]) + _random_prec = {'rand': _random_el} + g = S.random_stab(2, _random_prec=_random_prec) + assert g == Permutation([1, 3, 2, 0, 4]) + h = S.random_stab(1) + assert h(1) == 1 + + +def test_transitivity_degree(): + perm = Permutation([1, 2, 0]) + C = PermutationGroup([perm]) + assert C.transitivity_degree == 1 + gen1 = Permutation([1, 2, 0, 3, 4]) + gen2 = Permutation([1, 2, 3, 4, 0]) + # alternating group of degree 5 + Alt = PermutationGroup([gen1, gen2]) + assert Alt.transitivity_degree == 3 + + +def test_schreier_sims_random(): + assert sorted(Tetra.pgroup.base) == [0, 1] + + S = SymmetricGroup(3) + base = [0, 1] + strong_gens = [Permutation([1, 2, 0]), Permutation([1, 0, 2]), + Permutation([0, 2, 1])] + assert S.schreier_sims_random(base, strong_gens, 5) == (base, strong_gens) + D = DihedralGroup(3) + _random_prec = {'g': [Permutation([2, 0, 1]), Permutation([1, 2, 0]), + Permutation([1, 0, 2])]} + base = [0, 1] + strong_gens = [Permutation([1, 2, 0]), Permutation([2, 1, 0]), + Permutation([0, 2, 1])] + assert D.schreier_sims_random([], D.generators, 2, + _random_prec=_random_prec) == (base, strong_gens) + + +def test_baseswap(): + S = SymmetricGroup(4) + S.schreier_sims() + base = S.base + strong_gens = S.strong_gens + assert base == [0, 1, 2] + deterministic = S.baseswap(base, strong_gens, 1, randomized=False) + randomized = S.baseswap(base, strong_gens, 1) + assert deterministic[0] == [0, 2, 1] + assert _verify_bsgs(S, deterministic[0], deterministic[1]) is True + assert randomized[0] == [0, 2, 1] + assert _verify_bsgs(S, randomized[0], randomized[1]) is True + + +def test_schreier_sims_incremental(): + identity = Permutation([0, 1, 2, 3, 4]) + TrivialGroup = PermutationGroup([identity]) + base, strong_gens = TrivialGroup.schreier_sims_incremental(base=[0, 1, 2]) + assert _verify_bsgs(TrivialGroup, base, strong_gens) is True + S = SymmetricGroup(5) + base, strong_gens = S.schreier_sims_incremental(base=[0, 1, 2]) + assert _verify_bsgs(S, base, strong_gens) is True + D = DihedralGroup(2) + base, strong_gens = D.schreier_sims_incremental(base=[1]) + assert _verify_bsgs(D, base, strong_gens) is True + A = AlternatingGroup(7) + gens = A.generators[:] + gen0 = gens[0] + gen1 = gens[1] + gen1 = rmul(gen1, ~gen0) + gen0 = rmul(gen0, gen1) + gen1 = rmul(gen0, gen1) + base, strong_gens = A.schreier_sims_incremental(base=[0, 1], gens=gens) + assert _verify_bsgs(A, base, strong_gens) is True + C = CyclicGroup(11) + gen = C.generators[0] + base, strong_gens = C.schreier_sims_incremental(gens=[gen**3]) + assert _verify_bsgs(C, base, strong_gens) is True + + +def _subgroup_search(i, j, k): + prop_true = lambda x: True + prop_fix_points = lambda x: [x(point) for point in points] == points + prop_comm_g = lambda x: rmul(x, g) == rmul(g, x) + prop_even = lambda x: x.is_even + for i in range(i, j, k): + S = SymmetricGroup(i) + A = AlternatingGroup(i) + C = CyclicGroup(i) + Sym = S.subgroup_search(prop_true) + assert Sym.is_subgroup(S) + Alt = S.subgroup_search(prop_even) + assert Alt.is_subgroup(A) + Sym = S.subgroup_search(prop_true, init_subgroup=C) + assert Sym.is_subgroup(S) + points = [7] + assert S.stabilizer(7).is_subgroup(S.subgroup_search(prop_fix_points)) + points = [3, 4] + assert S.stabilizer(3).stabilizer(4).is_subgroup( + S.subgroup_search(prop_fix_points)) + points = [3, 5] + fix35 = A.subgroup_search(prop_fix_points) + points = [5] + fix5 = A.subgroup_search(prop_fix_points) + assert A.subgroup_search(prop_fix_points, init_subgroup=fix35 + ).is_subgroup(fix5) + base, strong_gens = A.schreier_sims_incremental() + g = A.generators[0] + comm_g = \ + A.subgroup_search(prop_comm_g, base=base, strong_gens=strong_gens) + assert _verify_bsgs(comm_g, base, comm_g.generators) is True + assert [prop_comm_g(gen) is True for gen in comm_g.generators] + + +def test_subgroup_search(): + _subgroup_search(10, 15, 2) + + +@XFAIL +def test_subgroup_search2(): + skip('takes too much time') + _subgroup_search(16, 17, 1) + + +def test_normal_closure(): + # the normal closure of the trivial group is trivial + S = SymmetricGroup(3) + identity = Permutation([0, 1, 2]) + closure = S.normal_closure(identity) + assert closure.is_trivial + # the normal closure of the entire group is the entire group + A = AlternatingGroup(4) + assert A.normal_closure(A).is_subgroup(A) + # brute-force verifications for subgroups + for i in (3, 4, 5): + S = SymmetricGroup(i) + A = AlternatingGroup(i) + D = DihedralGroup(i) + C = CyclicGroup(i) + for gp in (A, D, C): + assert _verify_normal_closure(S, gp) + # brute-force verifications for all elements of a group + S = SymmetricGroup(5) + elements = list(S.generate_dimino()) + for element in elements: + assert _verify_normal_closure(S, element) + # small groups + small = [] + for i in (1, 2, 3): + small.append(SymmetricGroup(i)) + small.append(AlternatingGroup(i)) + small.append(DihedralGroup(i)) + small.append(CyclicGroup(i)) + for gp in small: + for gp2 in small: + if gp2.is_subgroup(gp, 0) and gp2.degree == gp.degree: + assert _verify_normal_closure(gp, gp2) + + +def test_derived_series(): + # the derived series of the trivial group consists only of the trivial group + triv = PermutationGroup([Permutation([0, 1, 2])]) + assert triv.derived_series()[0].is_subgroup(triv) + # the derived series for a simple group consists only of the group itself + for i in (5, 6, 7): + A = AlternatingGroup(i) + assert A.derived_series()[0].is_subgroup(A) + # the derived series for S_4 is S_4 > A_4 > K_4 > triv + S = SymmetricGroup(4) + series = S.derived_series() + assert series[1].is_subgroup(AlternatingGroup(4)) + assert series[2].is_subgroup(DihedralGroup(2)) + assert series[3].is_trivial + + +def test_lower_central_series(): + # the lower central series of the trivial group consists of the trivial + # group + triv = PermutationGroup([Permutation([0, 1, 2])]) + assert triv.lower_central_series()[0].is_subgroup(triv) + # the lower central series of a simple group consists of the group itself + for i in (5, 6, 7): + A = AlternatingGroup(i) + assert A.lower_central_series()[0].is_subgroup(A) + # GAP-verified example + S = SymmetricGroup(6) + series = S.lower_central_series() + assert len(series) == 2 + assert series[1].is_subgroup(AlternatingGroup(6)) + + +def test_commutator(): + # the commutator of the trivial group and the trivial group is trivial + S = SymmetricGroup(3) + triv = PermutationGroup([Permutation([0, 1, 2])]) + assert S.commutator(triv, triv).is_subgroup(triv) + # the commutator of the trivial group and any other group is again trivial + A = AlternatingGroup(3) + assert S.commutator(triv, A).is_subgroup(triv) + # the commutator is commutative + for i in (3, 4, 5): + S = SymmetricGroup(i) + A = AlternatingGroup(i) + D = DihedralGroup(i) + assert S.commutator(A, D).is_subgroup(S.commutator(D, A)) + # the commutator of an abelian group is trivial + S = SymmetricGroup(7) + A1 = AbelianGroup(2, 5) + A2 = AbelianGroup(3, 4) + triv = PermutationGroup([Permutation([0, 1, 2, 3, 4, 5, 6])]) + assert S.commutator(A1, A1).is_subgroup(triv) + assert S.commutator(A2, A2).is_subgroup(triv) + # examples calculated by hand + S = SymmetricGroup(3) + A = AlternatingGroup(3) + assert S.commutator(A, S).is_subgroup(A) + + +def test_is_nilpotent(): + # every abelian group is nilpotent + for i in (1, 2, 3): + C = CyclicGroup(i) + Ab = AbelianGroup(i, i + 2) + assert C.is_nilpotent + assert Ab.is_nilpotent + Ab = AbelianGroup(5, 7, 10) + assert Ab.is_nilpotent + # A_5 is not solvable and thus not nilpotent + assert AlternatingGroup(5).is_nilpotent is False + + +def test_is_trivial(): + for i in range(5): + triv = PermutationGroup([Permutation(list(range(i)))]) + assert triv.is_trivial + + +def test_pointwise_stabilizer(): + S = SymmetricGroup(2) + stab = S.pointwise_stabilizer([0]) + assert stab.generators == [Permutation(1)] + S = SymmetricGroup(5) + points = [] + stab = S + for point in (2, 0, 3, 4, 1): + stab = stab.stabilizer(point) + points.append(point) + assert S.pointwise_stabilizer(points).is_subgroup(stab) + + +def test_make_perm(): + assert cube.pgroup.make_perm(5, seed=list(range(5))) == \ + Permutation([4, 7, 6, 5, 0, 3, 2, 1]) + assert cube.pgroup.make_perm(7, seed=list(range(7))) == \ + Permutation([6, 7, 3, 2, 5, 4, 0, 1]) + + +def test_elements(): + from sympy.sets.sets import FiniteSet + + p = Permutation(2, 3) + assert set(PermutationGroup(p).elements) == {Permutation(3), Permutation(2, 3)} + assert FiniteSet(*PermutationGroup(p).elements) \ + == FiniteSet(Permutation(2, 3), Permutation(3)) + + +def test_is_group(): + assert PermutationGroup(Permutation(1,2), Permutation(2,4)).is_group is True + assert SymmetricGroup(4).is_group is True + + +def test_PermutationGroup(): + assert PermutationGroup() == PermutationGroup(Permutation()) + assert (PermutationGroup() == 0) is False + + +def test_coset_transvesal(): + G = AlternatingGroup(5) + H = PermutationGroup(Permutation(0,1,2),Permutation(1,2)(3,4)) + assert G.coset_transversal(H) == \ + [Permutation(4), Permutation(2, 3, 4), Permutation(2, 4, 3), + Permutation(1, 2, 4), Permutation(4)(1, 2, 3), Permutation(1, 3)(2, 4), + Permutation(0, 1, 2, 3, 4), Permutation(0, 1, 2, 4, 3), + Permutation(0, 1, 3, 2, 4), Permutation(0, 2, 4, 1, 3)] + + +def test_coset_table(): + G = PermutationGroup(Permutation(0,1,2,3), Permutation(0,1,2), + Permutation(0,4,2,7), Permutation(5,6), Permutation(0,7)) + H = PermutationGroup(Permutation(0,1,2,3), Permutation(0,7)) + assert G.coset_table(H) == \ + [[0, 0, 0, 0, 1, 2, 3, 3, 0, 0], [4, 5, 2, 5, 6, 0, 7, 7, 1, 1], + [5, 4, 5, 1, 0, 6, 8, 8, 6, 6], [3, 3, 3, 3, 7, 8, 0, 0, 3, 3], + [2, 1, 4, 4, 4, 4, 9, 9, 4, 4], [1, 2, 1, 2, 5, 5, 10, 10, 5, 5], + [6, 6, 6, 6, 2, 1, 11, 11, 2, 2], [9, 10, 8, 10, 11, 3, 1, 1, 7, 7], + [10, 9, 10, 7, 3, 11, 2, 2, 11, 11], [8, 7, 9, 9, 9, 9, 4, 4, 9, 9], + [7, 8, 7, 8, 10, 10, 5, 5, 10, 10], [11, 11, 11, 11, 8, 7, 6, 6, 8, 8]] + + +def test_subgroup(): + G = PermutationGroup(Permutation(0,1,2), Permutation(0,2,3)) + H = G.subgroup([Permutation(0,1,3)]) + assert H.is_subgroup(G) + + +def test_generator_product(): + G = SymmetricGroup(5) + p = Permutation(0, 2, 3)(1, 4) + gens = G.generator_product(p) + assert all(g in G.strong_gens for g in gens) + w = G.identity + for g in gens: + w = g*w + assert w == p + + +def test_sylow_subgroup(): + P = PermutationGroup(Permutation(1, 5)(2, 4), Permutation(0, 1, 2, 3, 4, 5)) + S = P.sylow_subgroup(2) + assert S.order() == 4 + + P = DihedralGroup(12) + S = P.sylow_subgroup(3) + assert S.order() == 3 + + P = PermutationGroup( + Permutation(1, 5)(2, 4), Permutation(0, 1, 2, 3, 4, 5), Permutation(0, 2)) + S = P.sylow_subgroup(3) + assert S.order() == 9 + S = P.sylow_subgroup(2) + assert S.order() == 8 + + P = SymmetricGroup(10) + S = P.sylow_subgroup(2) + assert S.order() == 256 + S = P.sylow_subgroup(3) + assert S.order() == 81 + S = P.sylow_subgroup(5) + assert S.order() == 25 + + # the length of the lower central series + # of a p-Sylow subgroup of Sym(n) grows with + # the highest exponent exp of p such + # that n >= p**exp + exp = 1 + length = 0 + for i in range(2, 9): + P = SymmetricGroup(i) + S = P.sylow_subgroup(2) + ls = S.lower_central_series() + if i // 2**exp > 0: + # length increases with exponent + assert len(ls) > length + length = len(ls) + exp += 1 + else: + assert len(ls) == length + + G = SymmetricGroup(100) + S = G.sylow_subgroup(3) + assert G.order() % S.order() == 0 + assert G.order()/S.order() % 3 > 0 + + G = AlternatingGroup(100) + S = G.sylow_subgroup(2) + assert G.order() % S.order() == 0 + assert G.order()/S.order() % 2 > 0 + + G = DihedralGroup(18) + S = G.sylow_subgroup(p=2) + assert S.order() == 4 + + G = DihedralGroup(50) + S = G.sylow_subgroup(p=2) + assert S.order() == 4 + + +@slow +def test_presentation(): + def _test(P): + G = P.presentation() + return G.order() == P.order() + + def _strong_test(P): + G = P.strong_presentation() + chk = len(G.generators) == len(P.strong_gens) + return chk and G.order() == P.order() + + P = PermutationGroup(Permutation(0,1,5,2)(3,7,4,6), Permutation(0,3,5,4)(1,6,2,7)) + assert _test(P) + + P = AlternatingGroup(5) + assert _test(P) + + P = SymmetricGroup(5) + assert _test(P) + + P = PermutationGroup( + [Permutation(0,3,1,2), Permutation(3)(0,1), Permutation(0,1)(2,3)]) + assert _strong_test(P) + + P = DihedralGroup(6) + assert _strong_test(P) + + a = Permutation(0,1)(2,3) + b = Permutation(0,2)(3,1) + c = Permutation(4,5) + P = PermutationGroup(c, a, b) + assert _strong_test(P) + + +def test_polycyclic(): + a = Permutation([0, 1, 2]) + b = Permutation([2, 1, 0]) + G = PermutationGroup([a, b]) + assert G.is_polycyclic is True + + a = Permutation([1, 2, 3, 4, 0]) + b = Permutation([1, 0, 2, 3, 4]) + G = PermutationGroup([a, b]) + assert G.is_polycyclic is False + + +def test_elementary(): + a = Permutation([1, 5, 2, 0, 3, 6, 4]) + G = PermutationGroup([a]) + assert G.is_elementary(7) is False + + a = Permutation(0, 1)(2, 3) + b = Permutation(0, 2)(3, 1) + G = PermutationGroup([a, b]) + assert G.is_elementary(2) is True + c = Permutation(4, 5, 6) + G = PermutationGroup([a, b, c]) + assert G.is_elementary(2) is False + + G = SymmetricGroup(4).sylow_subgroup(2) + assert G.is_elementary(2) is False + H = AlternatingGroup(4).sylow_subgroup(2) + assert H.is_elementary(2) is True + + +def test_perfect(): + G = AlternatingGroup(3) + assert G.is_perfect is False + G = AlternatingGroup(5) + assert G.is_perfect is True + + +def test_index(): + G = PermutationGroup(Permutation(0,1,2), Permutation(0,2,3)) + H = G.subgroup([Permutation(0,1,3)]) + assert G.index(H) == 4 + + +def test_cyclic(): + G = SymmetricGroup(2) + assert G.is_cyclic + G = AbelianGroup(3, 7) + assert G.is_cyclic + G = AbelianGroup(7, 7) + assert not G.is_cyclic + G = AlternatingGroup(3) + assert G.is_cyclic + G = AlternatingGroup(4) + assert not G.is_cyclic + + # Order less than 6 + G = PermutationGroup(Permutation(0, 1, 2), Permutation(0, 2, 1)) + assert G.is_cyclic + G = PermutationGroup( + Permutation(0, 1, 2, 3), + Permutation(0, 2)(1, 3) + ) + assert G.is_cyclic + G = PermutationGroup( + Permutation(3), + Permutation(0, 1)(2, 3), + Permutation(0, 2)(1, 3), + Permutation(0, 3)(1, 2) + ) + assert G.is_cyclic is False + + # Order 15 + G = PermutationGroup( + Permutation(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14), + Permutation(0, 2, 4, 6, 8, 10, 12, 14, 1, 3, 5, 7, 9, 11, 13) + ) + assert G.is_cyclic + + # Distinct prime orders + assert PermutationGroup._distinct_primes_lemma([3, 5]) is True + assert PermutationGroup._distinct_primes_lemma([5, 7]) is True + assert PermutationGroup._distinct_primes_lemma([2, 3]) is None + assert PermutationGroup._distinct_primes_lemma([3, 5, 7]) is None + assert PermutationGroup._distinct_primes_lemma([5, 7, 13]) is True + + G = PermutationGroup( + Permutation(0, 1, 2, 3), + Permutation(0, 2)(1, 3)) + assert G.is_cyclic + assert G._is_abelian + + # Non-abelian and therefore not cyclic + G = PermutationGroup(*SymmetricGroup(3).generators) + assert G.is_cyclic is False + + # Abelian and cyclic + G = PermutationGroup( + Permutation(0, 1, 2, 3), + Permutation(4, 5, 6) + ) + assert G.is_cyclic + + # Abelian but not cyclic + G = PermutationGroup( + Permutation(0, 1), + Permutation(2, 3), + Permutation(4, 5, 6) + ) + assert G.is_cyclic is False + + +def test_dihedral(): + G = SymmetricGroup(2) + assert G.is_dihedral + G = SymmetricGroup(3) + assert G.is_dihedral + + G = AbelianGroup(2, 2) + assert G.is_dihedral + G = CyclicGroup(4) + assert not G.is_dihedral + + G = AbelianGroup(3, 5) + assert not G.is_dihedral + G = AbelianGroup(2) + assert G.is_dihedral + G = AbelianGroup(6) + assert not G.is_dihedral + + # D6, generated by two adjacent flips + G = PermutationGroup( + Permutation(1, 5)(2, 4), + Permutation(0, 1)(3, 4)(2, 5)) + assert G.is_dihedral + + # D7, generated by a flip and a rotation + G = PermutationGroup( + Permutation(1, 6)(2, 5)(3, 4), + Permutation(0, 1, 2, 3, 4, 5, 6)) + assert G.is_dihedral + + # S4, presented by three generators, fails due to having exactly 9 + # elements of order 2: + G = PermutationGroup( + Permutation(0, 1), Permutation(0, 2), + Permutation(0, 3)) + assert not G.is_dihedral + + # D7, given by three generators + G = PermutationGroup( + Permutation(1, 6)(2, 5)(3, 4), + Permutation(2, 0)(3, 6)(4, 5), + Permutation(0, 1, 2, 3, 4, 5, 6)) + assert G.is_dihedral + + +def test_abelian_invariants(): + G = AbelianGroup(2, 3, 4) + assert G.abelian_invariants() == [2, 3, 4] + G=PermutationGroup([Permutation(1, 2, 3, 4), Permutation(1, 2), Permutation(5, 6)]) + assert G.abelian_invariants() == [2, 2] + G = AlternatingGroup(7) + assert G.abelian_invariants() == [] + G = AlternatingGroup(4) + assert G.abelian_invariants() == [3] + G = DihedralGroup(4) + assert G.abelian_invariants() == [2, 2] + + G = PermutationGroup([Permutation(1, 2, 3, 4, 5, 6, 7)]) + assert G.abelian_invariants() == [7] + G = DihedralGroup(12) + S = G.sylow_subgroup(3) + assert S.abelian_invariants() == [3] + G = PermutationGroup(Permutation(0, 1, 2), Permutation(0, 2, 3)) + assert G.abelian_invariants() == [3] + G = PermutationGroup([Permutation(0, 1), Permutation(0, 2, 4, 6)(1, 3, 5, 7)]) + assert G.abelian_invariants() == [2, 4] + G = SymmetricGroup(30) + S = G.sylow_subgroup(2) + assert S.abelian_invariants() == [2, 2, 2, 2, 2, 2, 2, 2, 2, 2] + S = G.sylow_subgroup(3) + assert S.abelian_invariants() == [3, 3, 3, 3] + S = G.sylow_subgroup(5) + assert S.abelian_invariants() == [5, 5, 5] + + +def test_composition_series(): + a = Permutation(1, 2, 3) + b = Permutation(1, 2) + G = PermutationGroup([a, b]) + comp_series = G.composition_series() + assert comp_series == G.derived_series() + # The first group in the composition series is always the group itself and + # the last group in the series is the trivial group. + S = SymmetricGroup(4) + assert S.composition_series()[0] == S + assert len(S.composition_series()) == 5 + A = AlternatingGroup(4) + assert A.composition_series()[0] == A + assert len(A.composition_series()) == 4 + + # the composition series for C_8 is C_8 > C_4 > C_2 > triv + G = CyclicGroup(8) + series = G.composition_series() + assert is_isomorphic(series[1], CyclicGroup(4)) + assert is_isomorphic(series[2], CyclicGroup(2)) + assert series[3].is_trivial + + +def test_is_symmetric(): + a = Permutation(0, 1, 2) + b = Permutation(0, 1, size=3) + assert PermutationGroup(a, b).is_symmetric is True + + a = Permutation(0, 2, 1) + b = Permutation(1, 2, size=3) + assert PermutationGroup(a, b).is_symmetric is True + + a = Permutation(0, 1, 2, 3) + b = Permutation(0, 3)(1, 2) + assert PermutationGroup(a, b).is_symmetric is False + +def test_conjugacy_class(): + S = SymmetricGroup(4) + x = Permutation(1, 2, 3) + C = {Permutation(0, 1, 2, size = 4), Permutation(0, 1, 3), + Permutation(0, 2, 1, size = 4), Permutation(0, 2, 3), + Permutation(0, 3, 1), Permutation(0, 3, 2), + Permutation(1, 2, 3), Permutation(1, 3, 2)} + assert S.conjugacy_class(x) == C + +def test_conjugacy_classes(): + S = SymmetricGroup(3) + expected = [{Permutation(size = 3)}, + {Permutation(0, 1, size = 3), Permutation(0, 2), Permutation(1, 2)}, + {Permutation(0, 1, 2), Permutation(0, 2, 1)}] + computed = S.conjugacy_classes() + + assert len(expected) == len(computed) + assert all(e in computed for e in expected) + +def test_coset_class(): + a = Permutation(1, 2) + b = Permutation(0, 1) + G = PermutationGroup([a, b]) + #Creating right coset + rht_coset = G*a + #Checking whether it is left coset or right coset + assert rht_coset.is_right_coset + assert not rht_coset.is_left_coset + #Creating list representation of coset + list_repr = rht_coset.as_list() + expected = [Permutation(0, 2), Permutation(0, 2, 1), Permutation(1, 2), + Permutation(2), Permutation(2)(0, 1), Permutation(0, 1, 2)] + for ele in list_repr: + assert ele in expected + #Creating left coset + left_coset = a*G + #Checking whether it is left coset or right coset + assert not left_coset.is_right_coset + assert left_coset.is_left_coset + #Creating list representation of Coset + list_repr = left_coset.as_list() + expected = [Permutation(2)(0, 1), Permutation(0, 1, 2), Permutation(1, 2), + Permutation(2), Permutation(0, 2), Permutation(0, 2, 1)] + for ele in list_repr: + assert ele in expected + + G = PermutationGroup(Permutation(1, 2, 3, 4), Permutation(2, 3, 4)) + H = PermutationGroup(Permutation(1, 2, 3, 4)) + g = Permutation(1, 3)(2, 4) + rht_coset = Coset(g, H, G, dir='+') + assert rht_coset.is_right_coset + list_repr = rht_coset.as_list() + expected = [Permutation(1, 2, 3, 4), Permutation(4), Permutation(1, 3)(2, 4), + Permutation(1, 4, 3, 2)] + for ele in list_repr: + assert ele in expected + +def test_symmetricpermutationgroup(): + a = SymmetricPermutationGroup(5) + assert a.degree == 5 + assert a.order() == 120 + assert a.identity() == Permutation(4) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_permutations.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_permutations.py new file mode 100644 index 0000000000000000000000000000000000000000..b52fcfec0e2fb3be872efaa814077760e121c748 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_permutations.py @@ -0,0 +1,564 @@ +from itertools import permutations +from copy import copy + +from sympy.core.expr import unchanged +from sympy.core.numbers import Integer +from sympy.core.relational import Eq +from sympy.core.symbol import Symbol +from sympy.core.singleton import S +from sympy.combinatorics.permutations import \ + Permutation, _af_parity, _af_rmul, _af_rmuln, AppliedPermutation, Cycle +from sympy.printing import sstr, srepr, pretty, latex +from sympy.testing.pytest import raises, warns_deprecated_sympy + + +rmul = Permutation.rmul +a = Symbol('a', integer=True) + + +def test_Permutation(): + # don't auto fill 0 + raises(ValueError, lambda: Permutation([1])) + p = Permutation([0, 1, 2, 3]) + # call as bijective + assert [p(i) for i in range(p.size)] == list(p) + # call as operator + assert p(list(range(p.size))) == list(p) + # call as function + assert list(p(1, 2)) == [0, 2, 1, 3] + raises(TypeError, lambda: p(-1)) + raises(TypeError, lambda: p(5)) + # conversion to list + assert list(p) == list(range(4)) + assert p.copy() == p + assert copy(p) == p + assert Permutation(size=4) == Permutation(3) + assert Permutation(Permutation(3), size=5) == Permutation(4) + # cycle form with size + assert Permutation([[1, 2]], size=4) == Permutation([[1, 2], [0], [3]]) + # random generation + assert Permutation.random(2) in (Permutation([1, 0]), Permutation([0, 1])) + + p = Permutation([2, 5, 1, 6, 3, 0, 4]) + q = Permutation([[1], [0, 3, 5, 6, 2, 4]]) + assert len({p, p}) == 1 + r = Permutation([1, 3, 2, 0, 4, 6, 5]) + ans = Permutation(_af_rmuln(*[w.array_form for w in (p, q, r)])).array_form + assert rmul(p, q, r).array_form == ans + # make sure no other permutation of p, q, r could have given + # that answer + for a, b, c in permutations((p, q, r)): + if (a, b, c) == (p, q, r): + continue + assert rmul(a, b, c).array_form != ans + + assert p.support() == list(range(7)) + assert q.support() == [0, 2, 3, 4, 5, 6] + assert Permutation(p.cyclic_form).array_form == p.array_form + assert p.cardinality == 5040 + assert q.cardinality == 5040 + assert q.cycles == 2 + assert rmul(q, p) == Permutation([4, 6, 1, 2, 5, 3, 0]) + assert rmul(p, q) == Permutation([6, 5, 3, 0, 2, 4, 1]) + assert _af_rmul(p.array_form, q.array_form) == \ + [6, 5, 3, 0, 2, 4, 1] + + assert rmul(Permutation([[1, 2, 3], [0, 4]]), + Permutation([[1, 2, 4], [0], [3]])).cyclic_form == \ + [[0, 4, 2], [1, 3]] + assert q.array_form == [3, 1, 4, 5, 0, 6, 2] + assert q.cyclic_form == [[0, 3, 5, 6, 2, 4]] + assert q.full_cyclic_form == [[0, 3, 5, 6, 2, 4], [1]] + assert p.cyclic_form == [[0, 2, 1, 5], [3, 6, 4]] + t = p.transpositions() + assert t == [(0, 5), (0, 1), (0, 2), (3, 4), (3, 6)] + assert Permutation.rmul(*[Permutation(Cycle(*ti)) for ti in (t)]) + assert Permutation([1, 0]).transpositions() == [(0, 1)] + + assert p**13 == p + assert q**0 == Permutation(list(range(q.size))) + assert q**-2 == ~q**2 + assert q**2 == Permutation([5, 1, 0, 6, 3, 2, 4]) + assert q**3 == q**2*q + assert q**4 == q**2*q**2 + + a = Permutation(1, 3) + b = Permutation(2, 0, 3) + I = Permutation(3) + assert ~a == a**-1 + assert a*~a == I + assert a*b**-1 == a*~b + + ans = Permutation(0, 5, 3, 1, 6)(2, 4) + assert (p + q.rank()).rank() == ans.rank() + assert (p + q.rank())._rank == ans.rank() + assert (q + p.rank()).rank() == ans.rank() + raises(TypeError, lambda: p + Permutation(list(range(10)))) + + assert (p - q.rank()).rank() == Permutation(0, 6, 3, 1, 2, 5, 4).rank() + assert p.rank() - q.rank() < 0 # for coverage: make sure mod is used + assert (q - p.rank()).rank() == Permutation(1, 4, 6, 2)(3, 5).rank() + + assert p*q == Permutation(_af_rmuln(*[list(w) for w in (q, p)])) + assert p*Permutation([]) == p + assert Permutation([])*p == p + assert p*Permutation([[0, 1]]) == Permutation([2, 5, 0, 6, 3, 1, 4]) + assert Permutation([[0, 1]])*p == Permutation([5, 2, 1, 6, 3, 0, 4]) + + pq = p ^ q + assert pq == Permutation([5, 6, 0, 4, 1, 2, 3]) + assert pq == rmul(q, p, ~q) + qp = q ^ p + assert qp == Permutation([4, 3, 6, 2, 1, 5, 0]) + assert qp == rmul(p, q, ~p) + raises(ValueError, lambda: p ^ Permutation([])) + + assert p.commutator(q) == Permutation(0, 1, 3, 4, 6, 5, 2) + assert q.commutator(p) == Permutation(0, 2, 5, 6, 4, 3, 1) + assert p.commutator(q) == ~q.commutator(p) + raises(ValueError, lambda: p.commutator(Permutation([]))) + + assert len(p.atoms()) == 7 + assert q.atoms() == {0, 1, 2, 3, 4, 5, 6} + + assert p.inversion_vector() == [2, 4, 1, 3, 1, 0] + assert q.inversion_vector() == [3, 1, 2, 2, 0, 1] + + assert Permutation.from_inversion_vector(p.inversion_vector()) == p + assert Permutation.from_inversion_vector(q.inversion_vector()).array_form\ + == q.array_form + raises(ValueError, lambda: Permutation.from_inversion_vector([0, 2])) + assert Permutation(list(range(500, -1, -1))).inversions() == 125250 + + s = Permutation([0, 4, 1, 3, 2]) + assert s.parity() == 0 + _ = s.cyclic_form # needed to create a value for _cyclic_form + assert len(s._cyclic_form) != s.size and s.parity() == 0 + assert not s.is_odd + assert s.is_even + assert Permutation([0, 1, 4, 3, 2]).parity() == 1 + assert _af_parity([0, 4, 1, 3, 2]) == 0 + assert _af_parity([0, 1, 4, 3, 2]) == 1 + + s = Permutation([0]) + + assert s.is_Singleton + assert Permutation([]).is_Empty + + r = Permutation([3, 2, 1, 0]) + assert (r**2).is_Identity + + assert rmul(~p, p).is_Identity + assert (~p)**13 == Permutation([5, 2, 0, 4, 6, 1, 3]) + assert p.max() == 6 + assert p.min() == 0 + + q = Permutation([[6], [5], [0, 1, 2, 3, 4]]) + + assert q.max() == 4 + assert q.min() == 0 + + p = Permutation([1, 5, 2, 0, 3, 6, 4]) + q = Permutation([[1, 2, 3, 5, 6], [0, 4]]) + + assert p.ascents() == [0, 3, 4] + assert q.ascents() == [1, 2, 4] + assert r.ascents() == [] + + assert p.descents() == [1, 2, 5] + assert q.descents() == [0, 3, 5] + assert Permutation(r.descents()).is_Identity + + assert p.inversions() == 7 + # test the merge-sort with a longer permutation + big = list(p) + list(range(p.max() + 1, p.max() + 130)) + assert Permutation(big).inversions() == 7 + assert p.signature() == -1 + assert q.inversions() == 11 + assert q.signature() == -1 + assert rmul(p, ~p).inversions() == 0 + assert rmul(p, ~p).signature() == 1 + + assert p.order() == 6 + assert q.order() == 10 + assert (p**(p.order())).is_Identity + + assert p.length() == 6 + assert q.length() == 7 + assert r.length() == 4 + + assert p.runs() == [[1, 5], [2], [0, 3, 6], [4]] + assert q.runs() == [[4], [2, 3, 5], [0, 6], [1]] + assert r.runs() == [[3], [2], [1], [0]] + + assert p.index() == 8 + assert q.index() == 8 + assert r.index() == 3 + + assert p.get_precedence_distance(q) == q.get_precedence_distance(p) + assert p.get_adjacency_distance(q) == p.get_adjacency_distance(q) + assert p.get_positional_distance(q) == p.get_positional_distance(q) + p = Permutation([0, 1, 2, 3]) + q = Permutation([3, 2, 1, 0]) + assert p.get_precedence_distance(q) == 6 + assert p.get_adjacency_distance(q) == 3 + assert p.get_positional_distance(q) == 8 + p = Permutation([0, 3, 1, 2, 4]) + q = Permutation.josephus(4, 5, 2) + assert p.get_adjacency_distance(q) == 3 + raises(ValueError, lambda: p.get_adjacency_distance(Permutation([]))) + raises(ValueError, lambda: p.get_positional_distance(Permutation([]))) + raises(ValueError, lambda: p.get_precedence_distance(Permutation([]))) + + a = [Permutation.unrank_nonlex(4, i) for i in range(5)] + iden = Permutation([0, 1, 2, 3]) + for i in range(5): + for j in range(i + 1, 5): + assert a[i].commutes_with(a[j]) == \ + (rmul(a[i], a[j]) == rmul(a[j], a[i])) + if a[i].commutes_with(a[j]): + assert a[i].commutator(a[j]) == iden + assert a[j].commutator(a[i]) == iden + + a = Permutation(3) + b = Permutation(0, 6, 3)(1, 2) + assert a.cycle_structure == {1: 4} + assert b.cycle_structure == {2: 1, 3: 1, 1: 2} + # issue 11130 + raises(ValueError, lambda: Permutation(3, size=3)) + raises(ValueError, lambda: Permutation([1, 2, 0, 3], size=3)) + + +def test_Permutation_subclassing(): + # Subclass that adds permutation application on iterables + class CustomPermutation(Permutation): + def __call__(self, *i): + try: + return super().__call__(*i) + except TypeError: + pass + + try: + perm_obj = i[0] + return [self._array_form[j] for j in perm_obj] + except TypeError: + raise TypeError('unrecognized argument') + + def __eq__(self, other): + if isinstance(other, Permutation): + return self._hashable_content() == other._hashable_content() + else: + return super().__eq__(other) + + def __hash__(self): + return super().__hash__() + + p = CustomPermutation([1, 2, 3, 0]) + q = Permutation([1, 2, 3, 0]) + + assert p == q + raises(TypeError, lambda: q([1, 2])) + assert [2, 3] == p([1, 2]) + + assert type(p * q) == CustomPermutation + assert type(q * p) == Permutation # True because q.__mul__(p) is called! + + # Run all tests for the Permutation class also on the subclass + def wrapped_test_Permutation(): + # Monkeypatch the class definition in the globals + globals()['__Perm'] = globals()['Permutation'] + globals()['Permutation'] = CustomPermutation + test_Permutation() + globals()['Permutation'] = globals()['__Perm'] # Restore + del globals()['__Perm'] + + wrapped_test_Permutation() + + +def test_josephus(): + assert Permutation.josephus(4, 6, 1) == Permutation([3, 1, 0, 2, 5, 4]) + assert Permutation.josephus(1, 5, 1).is_Identity + + +def test_ranking(): + assert Permutation.unrank_lex(5, 10).rank() == 10 + p = Permutation.unrank_lex(15, 225) + assert p.rank() == 225 + p1 = p.next_lex() + assert p1.rank() == 226 + assert Permutation.unrank_lex(15, 225).rank() == 225 + assert Permutation.unrank_lex(10, 0).is_Identity + p = Permutation.unrank_lex(4, 23) + assert p.rank() == 23 + assert p.array_form == [3, 2, 1, 0] + assert p.next_lex() is None + + p = Permutation([1, 5, 2, 0, 3, 6, 4]) + q = Permutation([[1, 2, 3, 5, 6], [0, 4]]) + a = [Permutation.unrank_trotterjohnson(4, i).array_form for i in range(5)] + assert a == [[0, 1, 2, 3], [0, 1, 3, 2], [0, 3, 1, 2], [3, 0, 1, + 2], [3, 0, 2, 1] ] + assert [Permutation(pa).rank_trotterjohnson() for pa in a] == list(range(5)) + assert Permutation([0, 1, 2, 3]).next_trotterjohnson() == \ + Permutation([0, 1, 3, 2]) + + assert q.rank_trotterjohnson() == 2283 + assert p.rank_trotterjohnson() == 3389 + assert Permutation([1, 0]).rank_trotterjohnson() == 1 + a = Permutation(list(range(3))) + b = a + l = [] + tj = [] + for i in range(6): + l.append(a) + tj.append(b) + a = a.next_lex() + b = b.next_trotterjohnson() + assert a == b is None + assert {tuple(a) for a in l} == {tuple(a) for a in tj} + + p = Permutation([2, 5, 1, 6, 3, 0, 4]) + q = Permutation([[6], [5], [0, 1, 2, 3, 4]]) + assert p.rank() == 1964 + assert q.rank() == 870 + assert Permutation([]).rank_nonlex() == 0 + prank = p.rank_nonlex() + assert prank == 1600 + assert Permutation.unrank_nonlex(7, 1600) == p + qrank = q.rank_nonlex() + assert qrank == 41 + assert Permutation.unrank_nonlex(7, 41) == Permutation(q.array_form) + + a = [Permutation.unrank_nonlex(4, i).array_form for i in range(24)] + assert a == [ + [1, 2, 3, 0], [3, 2, 0, 1], [1, 3, 0, 2], [1, 2, 0, 3], [2, 3, 1, 0], + [2, 0, 3, 1], [3, 0, 1, 2], [2, 0, 1, 3], [1, 3, 2, 0], [3, 0, 2, 1], + [1, 0, 3, 2], [1, 0, 2, 3], [2, 1, 3, 0], [2, 3, 0, 1], [3, 1, 0, 2], + [2, 1, 0, 3], [3, 2, 1, 0], [0, 2, 3, 1], [0, 3, 1, 2], [0, 2, 1, 3], + [3, 1, 2, 0], [0, 3, 2, 1], [0, 1, 3, 2], [0, 1, 2, 3]] + + N = 10 + p1 = Permutation(a[0]) + for i in range(1, N+1): + p1 = p1*Permutation(a[i]) + p2 = Permutation.rmul_with_af(*[Permutation(h) for h in a[N::-1]]) + assert p1 == p2 + + ok = [] + p = Permutation([1, 0]) + for i in range(3): + ok.append(p.array_form) + p = p.next_nonlex() + if p is None: + ok.append(None) + break + assert ok == [[1, 0], [0, 1], None] + assert Permutation([3, 2, 0, 1]).next_nonlex() == Permutation([1, 3, 0, 2]) + assert [Permutation(pa).rank_nonlex() for pa in a] == list(range(24)) + + +def test_mul(): + a, b = [0, 2, 1, 3], [0, 1, 3, 2] + assert _af_rmul(a, b) == [0, 2, 3, 1] + assert _af_rmuln(a, b, list(range(4))) == [0, 2, 3, 1] + assert rmul(Permutation(a), Permutation(b)).array_form == [0, 2, 3, 1] + + a = Permutation([0, 2, 1, 3]) + b = (0, 1, 3, 2) + c = (3, 1, 2, 0) + assert Permutation.rmul(a, b, c) == Permutation([1, 2, 3, 0]) + assert Permutation.rmul(a, c) == Permutation([3, 2, 1, 0]) + raises(TypeError, lambda: Permutation.rmul(b, c)) + + n = 6 + m = 8 + a = [Permutation.unrank_nonlex(n, i).array_form for i in range(m)] + h = list(range(n)) + for i in range(m): + h = _af_rmul(h, a[i]) + h2 = _af_rmuln(*a[:i + 1]) + assert h == h2 + + +def test_args(): + p = Permutation([(0, 3, 1, 2), (4, 5)]) + assert p._cyclic_form is None + assert Permutation(p) == p + assert p.cyclic_form == [[0, 3, 1, 2], [4, 5]] + assert p._array_form == [3, 2, 0, 1, 5, 4] + p = Permutation((0, 3, 1, 2)) + assert p._cyclic_form is None + assert p._array_form == [0, 3, 1, 2] + assert Permutation([0]) == Permutation((0, )) + assert Permutation([[0], [1]]) == Permutation(((0, ), (1, ))) == \ + Permutation(((0, ), [1])) + assert Permutation([[1, 2]]) == Permutation([0, 2, 1]) + assert Permutation([[1], [4, 2]]) == Permutation([0, 1, 4, 3, 2]) + assert Permutation([[1], [4, 2]], size=1) == Permutation([0, 1, 4, 3, 2]) + assert Permutation( + [[1], [4, 2]], size=6) == Permutation([0, 1, 4, 3, 2, 5]) + assert Permutation([[0, 1], [0, 2]]) == Permutation(0, 1, 2) + assert Permutation([], size=3) == Permutation([0, 1, 2]) + assert Permutation(3).list(5) == [0, 1, 2, 3, 4] + assert Permutation(3).list(-1) == [] + assert Permutation(5)(1, 2).list(-1) == [0, 2, 1] + assert Permutation(5)(1, 2).list() == [0, 2, 1, 3, 4, 5] + raises(ValueError, lambda: Permutation([1, 2], [0])) + # enclosing brackets needed + raises(ValueError, lambda: Permutation([[1, 2], 0])) + # enclosing brackets needed on 0 + raises(ValueError, lambda: Permutation([1, 1, 0])) + raises(ValueError, lambda: Permutation([4, 5], size=10)) # where are 0-3? + # but this is ok because cycles imply that only those listed moved + assert Permutation(4, 5) == Permutation([0, 1, 2, 3, 5, 4]) + + +def test_Cycle(): + assert str(Cycle()) == '()' + assert Cycle(Cycle(1,2)) == Cycle(1, 2) + assert Cycle(1,2).copy() == Cycle(1,2) + assert list(Cycle(1, 3, 2)) == [0, 3, 1, 2] + assert Cycle(1, 2)(2, 3) == Cycle(1, 3, 2) + assert Cycle(1, 2)(2, 3)(4, 5) == Cycle(1, 3, 2)(4, 5) + assert Permutation(Cycle(1, 2)(2, 1, 0, 3)).cyclic_form, Cycle(0, 2, 1) + raises(ValueError, lambda: Cycle().list()) + assert Cycle(1, 2).list() == [0, 2, 1] + assert Cycle(1, 2).list(4) == [0, 2, 1, 3] + assert Cycle(3).list(2) == [0, 1] + assert Cycle(3).list(6) == [0, 1, 2, 3, 4, 5] + assert Permutation(Cycle(1, 2), size=4) == \ + Permutation([0, 2, 1, 3]) + assert str(Cycle(1, 2)(4, 5)) == '(1 2)(4 5)' + assert str(Cycle(1, 2)) == '(1 2)' + assert Cycle(Permutation(list(range(3)))) == Cycle() + assert Cycle(1, 2).list() == [0, 2, 1] + assert Cycle(1, 2).list(4) == [0, 2, 1, 3] + assert Cycle().size == 0 + raises(ValueError, lambda: Cycle((1, 2))) + raises(ValueError, lambda: Cycle(1, 2, 1)) + raises(TypeError, lambda: Cycle(1, 2)*{}) + raises(ValueError, lambda: Cycle(4)[a]) + raises(ValueError, lambda: Cycle(2, -4, 3)) + + # check round-trip + p = Permutation([[1, 2], [4, 3]], size=5) + assert Permutation(Cycle(p)) == p + + +def test_from_sequence(): + assert Permutation.from_sequence('SymPy') == Permutation(4)(0, 1, 3) + assert Permutation.from_sequence('SymPy', key=lambda x: x.lower()) == \ + Permutation(4)(0, 2)(1, 3) + + +def test_resize(): + p = Permutation(0, 1, 2) + assert p.resize(5) == Permutation(0, 1, 2, size=5) + assert p.resize(4) == Permutation(0, 1, 2, size=4) + assert p.resize(3) == p + raises(ValueError, lambda: p.resize(2)) + + p = Permutation(0, 1, 2)(3, 4)(5, 6) + assert p.resize(3) == Permutation(0, 1, 2) + raises(ValueError, lambda: p.resize(4)) + + +def test_printing_cyclic(): + p1 = Permutation([0, 2, 1]) + assert repr(p1) == 'Permutation(1, 2)' + assert str(p1) == '(1 2)' + p2 = Permutation() + assert repr(p2) == 'Permutation()' + assert str(p2) == '()' + p3 = Permutation([1, 2, 0, 3]) + assert repr(p3) == 'Permutation(3)(0, 1, 2)' + + +def test_printing_non_cyclic(): + p1 = Permutation([0, 1, 2, 3, 4, 5]) + assert srepr(p1, perm_cyclic=False) == 'Permutation([], size=6)' + assert sstr(p1, perm_cyclic=False) == 'Permutation([], size=6)' + p2 = Permutation([0, 1, 2]) + assert srepr(p2, perm_cyclic=False) == 'Permutation([0, 1, 2])' + assert sstr(p2, perm_cyclic=False) == 'Permutation([0, 1, 2])' + + p3 = Permutation([0, 2, 1]) + assert srepr(p3, perm_cyclic=False) == 'Permutation([0, 2, 1])' + assert sstr(p3, perm_cyclic=False) == 'Permutation([0, 2, 1])' + p4 = Permutation([0, 1, 3, 2, 4, 5, 6, 7]) + assert srepr(p4, perm_cyclic=False) == 'Permutation([0, 1, 3, 2], size=8)' + + +def test_deprecated_print_cyclic(): + p = Permutation(0, 1, 2) + try: + Permutation.print_cyclic = True + with warns_deprecated_sympy(): + assert sstr(p) == '(0 1 2)' + with warns_deprecated_sympy(): + assert srepr(p) == 'Permutation(0, 1, 2)' + with warns_deprecated_sympy(): + assert pretty(p) == '(0 1 2)' + with warns_deprecated_sympy(): + assert latex(p) == r'\left( 0\; 1\; 2\right)' + + Permutation.print_cyclic = False + with warns_deprecated_sympy(): + assert sstr(p) == 'Permutation([1, 2, 0])' + with warns_deprecated_sympy(): + assert srepr(p) == 'Permutation([1, 2, 0])' + with warns_deprecated_sympy(): + assert pretty(p, use_unicode=False) == '/0 1 2\\\n\\1 2 0/' + with warns_deprecated_sympy(): + assert latex(p) == \ + r'\begin{pmatrix} 0 & 1 & 2 \\ 1 & 2 & 0 \end{pmatrix}' + finally: + Permutation.print_cyclic = None + + +def test_permutation_equality(): + a = Permutation(0, 1, 2) + b = Permutation(0, 1, 2) + assert Eq(a, b) is S.true + c = Permutation(0, 2, 1) + assert Eq(a, c) is S.false + + d = Permutation(0, 1, 2, size=4) + assert unchanged(Eq, a, d) + e = Permutation(0, 2, 1, size=4) + assert unchanged(Eq, a, e) + + i = Permutation() + assert unchanged(Eq, i, 0) + assert unchanged(Eq, 0, i) + + +def test_issue_17661(): + c1 = Cycle(1,2) + c2 = Cycle(1,2) + assert c1 == c2 + assert repr(c1) == 'Cycle(1, 2)' + assert c1 == c2 + + +def test_permutation_apply(): + x = Symbol('x') + p = Permutation(0, 1, 2) + assert p.apply(0) == 1 + assert isinstance(p.apply(0), Integer) + assert p.apply(x) == AppliedPermutation(p, x) + assert AppliedPermutation(p, x).subs(x, 0) == 1 + + x = Symbol('x', integer=False) + raises(NotImplementedError, lambda: p.apply(x)) + x = Symbol('x', negative=True) + raises(NotImplementedError, lambda: p.apply(x)) + + +def test_AppliedPermutation(): + x = Symbol('x') + p = Permutation(0, 1, 2) + raises(ValueError, lambda: AppliedPermutation((0, 1, 2), x)) + assert AppliedPermutation(p, 1, evaluate=True) == 2 + assert AppliedPermutation(p, 1, evaluate=False).__class__ == \ + AppliedPermutation diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_polyhedron.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_polyhedron.py new file mode 100644 index 0000000000000000000000000000000000000000..abf469bb560eef1f378eff4740a84b80b696035f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_polyhedron.py @@ -0,0 +1,105 @@ +from sympy.core.symbol import symbols +from sympy.sets.sets import FiniteSet +from sympy.combinatorics.polyhedron import (Polyhedron, + tetrahedron, cube as square, octahedron, dodecahedron, icosahedron, + cube_faces) +from sympy.combinatorics.permutations import Permutation +from sympy.combinatorics.perm_groups import PermutationGroup +from sympy.testing.pytest import raises + +rmul = Permutation.rmul + + +def test_polyhedron(): + raises(ValueError, lambda: Polyhedron(list('ab'), + pgroup=[Permutation([0])])) + pgroup = [Permutation([[0, 7, 2, 5], [6, 1, 4, 3]]), + Permutation([[0, 7, 1, 6], [5, 2, 4, 3]]), + Permutation([[3, 6, 0, 5], [4, 1, 7, 2]]), + Permutation([[7, 4, 5], [1, 3, 0], [2], [6]]), + Permutation([[1, 3, 2], [7, 6, 5], [4], [0]]), + Permutation([[4, 7, 6], [2, 0, 3], [1], [5]]), + Permutation([[1, 2, 0], [4, 5, 6], [3], [7]]), + Permutation([[4, 2], [0, 6], [3, 7], [1, 5]]), + Permutation([[3, 5], [7, 1], [2, 6], [0, 4]]), + Permutation([[2, 5], [1, 6], [0, 4], [3, 7]]), + Permutation([[4, 3], [7, 0], [5, 1], [6, 2]]), + Permutation([[4, 1], [0, 5], [6, 2], [7, 3]]), + Permutation([[7, 2], [3, 6], [0, 4], [1, 5]]), + Permutation([0, 1, 2, 3, 4, 5, 6, 7])] + corners = tuple(symbols('A:H')) + faces = cube_faces + cube = Polyhedron(corners, faces, pgroup) + + assert cube.edges == FiniteSet(*( + (0, 1), (6, 7), (1, 2), (5, 6), (0, 3), (2, 3), + (4, 7), (4, 5), (3, 7), (1, 5), (0, 4), (2, 6))) + + for i in range(3): # add 180 degree face rotations + cube.rotate(cube.pgroup[i]**2) + + assert cube.corners == corners + + for i in range(3, 7): # add 240 degree axial corner rotations + cube.rotate(cube.pgroup[i]**2) + + assert cube.corners == corners + cube.rotate(1) + raises(ValueError, lambda: cube.rotate(Permutation([0, 1]))) + assert cube.corners != corners + assert cube.array_form == [7, 6, 4, 5, 3, 2, 0, 1] + assert cube.cyclic_form == [[0, 7, 1, 6], [2, 4, 3, 5]] + cube.reset() + assert cube.corners == corners + + def check(h, size, rpt, target): + + assert len(h.faces) + len(h.vertices) - len(h.edges) == 2 + assert h.size == size + + got = set() + for p in h.pgroup: + # make sure it restores original + P = h.copy() + hit = P.corners + for i in range(rpt): + P.rotate(p) + if P.corners == hit: + break + else: + print('error in permutation', p.array_form) + for i in range(rpt): + P.rotate(p) + got.add(tuple(P.corners)) + c = P.corners + f = [[c[i] for i in f] for f in P.faces] + assert h.faces == Polyhedron(c, f).faces + assert len(got) == target + assert PermutationGroup([Permutation(g) for g in got]).is_group + + for h, size, rpt, target in zip( + (tetrahedron, square, octahedron, dodecahedron, icosahedron), + (4, 8, 6, 20, 12), + (3, 4, 4, 5, 5), + (12, 24, 24, 60, 60)): + check(h, size, rpt, target) + + +def test_pgroups(): + from sympy.combinatorics.polyhedron import (cube, tetrahedron_faces, + octahedron_faces, dodecahedron_faces, icosahedron_faces) + from sympy.combinatorics.polyhedron import _pgroup_calcs + (tetrahedron2, cube2, octahedron2, dodecahedron2, icosahedron2, + tetrahedron_faces2, cube_faces2, octahedron_faces2, + dodecahedron_faces2, icosahedron_faces2) = _pgroup_calcs() + + assert tetrahedron == tetrahedron2 + assert cube == cube2 + assert octahedron == octahedron2 + assert dodecahedron == dodecahedron2 + assert icosahedron == icosahedron2 + assert sorted(map(sorted, tetrahedron_faces)) == sorted(map(sorted, tetrahedron_faces2)) + assert sorted(cube_faces) == sorted(cube_faces2) + assert sorted(octahedron_faces) == sorted(octahedron_faces2) + assert sorted(dodecahedron_faces) == sorted(dodecahedron_faces2) + assert sorted(icosahedron_faces) == sorted(icosahedron_faces2) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_prufer.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_prufer.py new file mode 100644 index 0000000000000000000000000000000000000000..b077c7cf3f023a4c36d7039505e6165ab29f275a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_prufer.py @@ -0,0 +1,74 @@ +from sympy.combinatorics.prufer import Prufer +from sympy.testing.pytest import raises + + +def test_prufer(): + # number of nodes is optional + assert Prufer([[0, 1], [0, 2], [0, 3], [0, 4]], 5).nodes == 5 + assert Prufer([[0, 1], [0, 2], [0, 3], [0, 4]]).nodes == 5 + + a = Prufer([[0, 1], [0, 2], [0, 3], [0, 4]]) + assert a.rank == 0 + assert a.nodes == 5 + assert a.prufer_repr == [0, 0, 0] + + a = Prufer([[2, 4], [1, 4], [1, 3], [0, 5], [0, 4]]) + assert a.rank == 924 + assert a.nodes == 6 + assert a.tree_repr == [[2, 4], [1, 4], [1, 3], [0, 5], [0, 4]] + assert a.prufer_repr == [4, 1, 4, 0] + + assert Prufer.edges([0, 1, 2, 3], [1, 4, 5], [1, 4, 6]) == \ + ([[0, 1], [1, 2], [1, 4], [2, 3], [4, 5], [4, 6]], 7) + assert Prufer([0]*4).size == Prufer([6]*4).size == 1296 + + # accept iterables but convert to list of lists + tree = [(0, 1), (1, 5), (0, 3), (0, 2), (2, 6), (4, 7), (2, 4)] + tree_lists = [list(t) for t in tree] + assert Prufer(tree).tree_repr == tree_lists + assert sorted(Prufer(set(tree)).tree_repr) == sorted(tree_lists) + + raises(ValueError, lambda: Prufer([[1, 2], [3, 4]])) # 0 is missing + raises(ValueError, lambda: Prufer([[2, 3], [3, 4]])) # 0, 1 are missing + assert Prufer(*Prufer.edges([1, 2], [3, 4])).prufer_repr == [1, 3] + raises(ValueError, lambda: Prufer.edges( + [1, 3], [3, 4])) # a broken tree but edges doesn't care + raises(ValueError, lambda: Prufer.edges([1, 2], [5, 6])) + raises(ValueError, lambda: Prufer([[]])) + + a = Prufer([[0, 1], [0, 2], [0, 3]]) + b = a.next() + assert b.tree_repr == [[0, 2], [0, 1], [1, 3]] + assert b.rank == 1 + + +def test_round_trip(): + def doit(t, b): + e, n = Prufer.edges(*t) + t = Prufer(e, n) + a = sorted(t.tree_repr) + b = [i - 1 for i in b] + assert t.prufer_repr == b + assert sorted(Prufer(b).tree_repr) == a + assert Prufer.unrank(t.rank, n).prufer_repr == b + + doit([[1, 2]], []) + doit([[2, 1, 3]], [1]) + doit([[1, 3, 2]], [3]) + doit([[1, 2, 3]], [2]) + doit([[2, 1, 4], [1, 3]], [1, 1]) + doit([[3, 2, 1, 4]], [2, 1]) + doit([[3, 2, 1], [2, 4]], [2, 2]) + doit([[1, 3, 2, 4]], [3, 2]) + doit([[1, 4, 2, 3]], [4, 2]) + doit([[3, 1, 4, 2]], [4, 1]) + doit([[4, 2, 1, 3]], [1, 2]) + doit([[1, 2, 4, 3]], [2, 4]) + doit([[1, 3, 4, 2]], [3, 4]) + doit([[2, 4, 1], [4, 3]], [4, 4]) + doit([[1, 2, 3, 4]], [2, 3]) + doit([[2, 3, 1], [3, 4]], [3, 3]) + doit([[1, 4, 3, 2]], [4, 3]) + doit([[2, 1, 4, 3]], [1, 4]) + doit([[2, 1, 3, 4]], [1, 3]) + doit([[6, 2, 1, 4], [1, 3, 5, 8], [3, 7]], [1, 2, 1, 3, 3, 5]) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_rewriting.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_rewriting.py new file mode 100644 index 0000000000000000000000000000000000000000..97c562bd57a2cd6318fa1dcb13c6f6278c861cca --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_rewriting.py @@ -0,0 +1,49 @@ +from sympy.combinatorics.fp_groups import FpGroup +from sympy.combinatorics.free_groups import free_group +from sympy.testing.pytest import raises + + +def test_rewriting(): + F, a, b = free_group("a, b") + G = FpGroup(F, [a*b*a**-1*b**-1]) + a, b = G.generators + R = G._rewriting_system + assert R.is_confluent + + assert G.reduce(b**-1*a) == a*b**-1 + assert G.reduce(b**3*a**4*b**-2*a) == a**5*b + assert G.equals(b**2*a**-1*b, b**4*a**-1*b**-1) + + assert R.reduce_using_automaton(b*a*a**2*b**-1) == a**3 + assert R.reduce_using_automaton(b**3*a**4*b**-2*a) == a**5*b + assert R.reduce_using_automaton(b**-1*a) == a*b**-1 + + G = FpGroup(F, [a**3, b**3, (a*b)**2]) + R = G._rewriting_system + R.make_confluent() + # R._is_confluent should be set to True after + # a successful run of make_confluent + assert R.is_confluent + # but also the system should actually be confluent + assert R._check_confluence() + assert G.reduce(b*a**-1*b**-1*a**3*b**4*a**-1*b**-15) == a**-1*b**-1 + # check for automaton reduction + assert R.reduce_using_automaton(b*a**-1*b**-1*a**3*b**4*a**-1*b**-15) == a**-1*b**-1 + + G = FpGroup(F, [a**2, b**3, (a*b)**4]) + R = G._rewriting_system + assert G.reduce(a**2*b**-2*a**2*b) == b**-1 + assert R.reduce_using_automaton(a**2*b**-2*a**2*b) == b**-1 + assert G.reduce(a**3*b**-2*a**2*b) == a**-1*b**-1 + assert R.reduce_using_automaton(a**3*b**-2*a**2*b) == a**-1*b**-1 + # Check after adding a rule + R.add_rule(a**2, b) + assert R.reduce_using_automaton(a**2*b**-2*a**2*b) == b**-1 + assert R.reduce_using_automaton(a**4*b**-2*a**2*b**3) == b + + R.set_max(15) + raises(RuntimeError, lambda: R.add_rule(a**-3, b)) + R.set_max(20) + R.add_rule(a**-3, b) + + assert R.add_rule(a, a) == set() diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_schur_number.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_schur_number.py new file mode 100644 index 0000000000000000000000000000000000000000..e6beb9b11fa993a99b71d89b8485050fc3575b8e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_schur_number.py @@ -0,0 +1,55 @@ +from sympy.core import S, Rational +from sympy.combinatorics.schur_number import schur_partition, SchurNumber +from sympy.core.random import _randint +from sympy.testing.pytest import raises +from sympy.core.symbol import symbols + + +def _sum_free_test(subset): + """ + Checks if subset is sum-free(There are no x,y,z in the subset such that + x + y = z) + """ + for i in subset: + for j in subset: + assert (i + j in subset) is False + + +def test_schur_partition(): + raises(ValueError, lambda: schur_partition(S.Infinity)) + raises(ValueError, lambda: schur_partition(-1)) + raises(ValueError, lambda: schur_partition(0)) + assert schur_partition(2) == [[1, 2]] + + random_number_generator = _randint(1000) + for _ in range(5): + n = random_number_generator(1, 1000) + result = schur_partition(n) + t = 0 + numbers = [] + for item in result: + _sum_free_test(item) + """ + Checks if the occurrence of all numbers is exactly one + """ + t += len(item) + for l in item: + assert (l in numbers) is False + numbers.append(l) + assert n == t + + x = symbols("x") + raises(ValueError, lambda: schur_partition(x)) + +def test_schur_number(): + first_known_schur_numbers = {1: 1, 2: 4, 3: 13, 4: 44, 5: 160} + for k in first_known_schur_numbers: + assert SchurNumber(k) == first_known_schur_numbers[k] + + assert SchurNumber(S.Infinity) == S.Infinity + assert SchurNumber(0) == 0 + raises(ValueError, lambda: SchurNumber(0.5)) + + n = symbols("n") + assert SchurNumber(n).lower_bound() == 3**n/2 - Rational(1, 2) + assert SchurNumber(8).lower_bound() == 5039 diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_subsets.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_subsets.py new file mode 100644 index 0000000000000000000000000000000000000000..1d50076da1c685294c2d2561dcc2a6af629eaf83 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_subsets.py @@ -0,0 +1,63 @@ +from sympy.combinatorics.subsets import Subset, ksubsets +from sympy.testing.pytest import raises + + +def test_subset(): + a = Subset(['c', 'd'], ['a', 'b', 'c', 'd']) + assert a.next_binary() == Subset(['b'], ['a', 'b', 'c', 'd']) + assert a.prev_binary() == Subset(['c'], ['a', 'b', 'c', 'd']) + assert a.next_lexicographic() == Subset(['d'], ['a', 'b', 'c', 'd']) + assert a.prev_lexicographic() == Subset(['c'], ['a', 'b', 'c', 'd']) + assert a.next_gray() == Subset(['c'], ['a', 'b', 'c', 'd']) + assert a.prev_gray() == Subset(['d'], ['a', 'b', 'c', 'd']) + assert a.rank_binary == 3 + assert a.rank_lexicographic == 14 + assert a.rank_gray == 2 + assert a.cardinality == 16 + assert a.size == 2 + assert Subset.bitlist_from_subset(a, ['a', 'b', 'c', 'd']) == '0011' + + a = Subset([2, 5, 7], [1, 2, 3, 4, 5, 6, 7]) + assert a.next_binary() == Subset([2, 5, 6], [1, 2, 3, 4, 5, 6, 7]) + assert a.prev_binary() == Subset([2, 5], [1, 2, 3, 4, 5, 6, 7]) + assert a.next_lexicographic() == Subset([2, 6], [1, 2, 3, 4, 5, 6, 7]) + assert a.prev_lexicographic() == Subset([2, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7]) + assert a.next_gray() == Subset([2, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7]) + assert a.prev_gray() == Subset([2, 5], [1, 2, 3, 4, 5, 6, 7]) + assert a.rank_binary == 37 + assert a.rank_lexicographic == 93 + assert a.rank_gray == 57 + assert a.cardinality == 128 + + superset = ['a', 'b', 'c', 'd'] + assert Subset.unrank_binary(4, superset).rank_binary == 4 + assert Subset.unrank_gray(10, superset).rank_gray == 10 + + superset = [1, 2, 3, 4, 5, 6, 7, 8, 9] + assert Subset.unrank_binary(33, superset).rank_binary == 33 + assert Subset.unrank_gray(25, superset).rank_gray == 25 + + a = Subset([], ['a', 'b', 'c', 'd']) + i = 1 + while a.subset != Subset(['d'], ['a', 'b', 'c', 'd']).subset: + a = a.next_lexicographic() + i = i + 1 + assert i == 16 + + i = 1 + while a.subset != Subset([], ['a', 'b', 'c', 'd']).subset: + a = a.prev_lexicographic() + i = i + 1 + assert i == 16 + + raises(ValueError, lambda: Subset(['a', 'b'], ['a'])) + raises(ValueError, lambda: Subset(['a'], ['b', 'c'])) + raises(ValueError, lambda: Subset.subset_from_bitlist(['a', 'b'], '010')) + + assert Subset(['a'], ['a', 'b']) != Subset(['b'], ['a', 'b']) + assert Subset(['a'], ['a', 'b']) != Subset(['a'], ['a', 'c']) + +def test_ksubsets(): + assert list(ksubsets([1, 2, 3], 2)) == [(1, 2), (1, 3), (2, 3)] + assert list(ksubsets([1, 2, 3, 4, 5], 2)) == [(1, 2), (1, 3), (1, 4), + (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5)] diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_tensor_can.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_tensor_can.py new file mode 100644 index 0000000000000000000000000000000000000000..3922419f20b92536426bfaae4b7e94df5db671b5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_tensor_can.py @@ -0,0 +1,560 @@ +from sympy.combinatorics.permutations import Permutation, Perm +from sympy.combinatorics.tensor_can import (perm_af_direct_product, dummy_sgs, + riemann_bsgs, get_symmetric_group_sgs, canonicalize, bsgs_direct_product) +from sympy.combinatorics.testutil import canonicalize_naive, graph_certificate +from sympy.testing.pytest import skip, XFAIL + +def test_perm_af_direct_product(): + gens1 = [[1,0,2,3], [0,1,3,2]] + gens2 = [[1,0]] + assert perm_af_direct_product(gens1, gens2, 0) == [[1, 0, 2, 3, 4, 5], [0, 1, 3, 2, 4, 5], [0, 1, 2, 3, 5, 4]] + gens1 = [[1,0,2,3,5,4], [0,1,3,2,4,5]] + gens2 = [[1,0,2,3]] + assert [[1, 0, 2, 3, 4, 5, 7, 6], [0, 1, 3, 2, 4, 5, 6, 7], [0, 1, 2, 3, 5, 4, 6, 7]] + +def test_dummy_sgs(): + a = dummy_sgs([1,2], 0, 4) + assert a == [[0,2,1,3,4,5]] + a = dummy_sgs([2,3,4,5], 0, 8) + assert a == [x._array_form for x in [Perm(9)(2,3), Perm(9)(4,5), + Perm(9)(2,4)(3,5)]] + + a = dummy_sgs([2,3,4,5], 1, 8) + assert a == [x._array_form for x in [Perm(2,3)(8,9), Perm(4,5)(8,9), + Perm(9)(2,4)(3,5)]] + +def test_get_symmetric_group_sgs(): + assert get_symmetric_group_sgs(2) == ([0], [Permutation(3)(0,1)]) + assert get_symmetric_group_sgs(2, 1) == ([0], [Permutation(0,1)(2,3)]) + assert get_symmetric_group_sgs(3) == ([0,1], [Permutation(4)(0,1), Permutation(4)(1,2)]) + assert get_symmetric_group_sgs(3, 1) == ([0,1], [Permutation(0,1)(3,4), Permutation(1,2)(3,4)]) + assert get_symmetric_group_sgs(4) == ([0,1,2], [Permutation(5)(0,1), Permutation(5)(1,2), Permutation(5)(2,3)]) + assert get_symmetric_group_sgs(4, 1) == ([0,1,2], [Permutation(0,1)(4,5), Permutation(1,2)(4,5), Permutation(2,3)(4,5)]) + + +def test_canonicalize_no_slot_sym(): + # cases in which there is no slot symmetry after fixing the + # free indices; here and in the following if the symmetry of the + # metric is not specified, it is assumed to be symmetric. + # If it is not specified, tensors are commuting. + + # A_d0 * B^d0; g = [1,0, 2,3]; T_c = A^d0*B_d0; can = [0,1,2,3] + base1, gens1 = get_symmetric_group_sgs(1) + dummies = [0, 1] + g = Permutation([1,0,2,3]) + can = canonicalize(g, dummies, 0, (base1,gens1,1,0), (base1,gens1,1,0)) + assert can == [0,1,2,3] + # equivalently + can = canonicalize(g, dummies, 0, (base1, gens1, 2, None)) + assert can == [0,1,2,3] + + # with antisymmetric metric; T_c = -A^d0*B_d0; can = [0,1,3,2] + can = canonicalize(g, dummies, 1, (base1,gens1,1,0), (base1,gens1,1,0)) + assert can == [0,1,3,2] + + # A^a * B^b; ord = [a,b]; g = [0,1,2,3]; can = g + g = Permutation([0,1,2,3]) + dummies = [] + t0 = t1 = (base1, gens1, 1, 0) + can = canonicalize(g, dummies, 0, t0, t1) + assert can == [0,1,2,3] + # B^b * A^a + g = Permutation([1,0,2,3]) + can = canonicalize(g, dummies, 0, t0, t1) + assert can == [1,0,2,3] + + # A symmetric + # A^{b}_{d0}*A^{d0, a} order a,b,d0,-d0; T_c = A^{a d0}*A{b}_{d0} + # g = [1,3,2,0,4,5]; can = [0,2,1,3,4,5] + base2, gens2 = get_symmetric_group_sgs(2) + dummies = [2,3] + g = Permutation([1,3,2,0,4,5]) + can = canonicalize(g, dummies, 0, (base2, gens2, 2, 0)) + assert can == [0, 2, 1, 3, 4, 5] + # with antisymmetric metric + can = canonicalize(g, dummies, 1, (base2, gens2, 2, 0)) + assert can == [0, 2, 1, 3, 4, 5] + # A^{a}_{d0}*A^{d0, b} + g = Permutation([0,3,2,1,4,5]) + can = canonicalize(g, dummies, 1, (base2, gens2, 2, 0)) + assert can == [0, 2, 1, 3, 5, 4] + + # A, B symmetric + # A^b_d0*B^{d0,a}; g=[1,3,2,0,4,5] + # T_c = A^{b,d0}*B_{a,d0}; can = [1,2,0,3,4,5] + dummies = [2,3] + g = Permutation([1,3,2,0,4,5]) + can = canonicalize(g, dummies, 0, (base2,gens2,1,0), (base2,gens2,1,0)) + assert can == [1,2,0,3,4,5] + # same with antisymmetric metric + can = canonicalize(g, dummies, 1, (base2,gens2,1,0), (base2,gens2,1,0)) + assert can == [1,2,0,3,5,4] + + # A^{d1}_{d0}*B^d0*C_d1 ord=[d0,-d0,d1,-d1]; g = [2,1,0,3,4,5] + # T_c = A^{d0 d1}*B_d0*C_d1; can = [0,2,1,3,4,5] + base1, gens1 = get_symmetric_group_sgs(1) + base2, gens2 = get_symmetric_group_sgs(2) + g = Permutation([2,1,0,3,4,5]) + dummies = [0,1,2,3] + t0 = (base2, gens2, 1, 0) + t1 = t2 = (base1, gens1, 1, 0) + can = canonicalize(g, dummies, 0, t0, t1, t2) + assert can == [0, 2, 1, 3, 4, 5] + + # A without symmetry + # A^{d1}_{d0}*B^d0*C_d1 ord=[d0,-d0,d1,-d1]; g = [2,1,0,3,4,5] + # T_c = A^{d0 d1}*B_d1*C_d0; can = [0,2,3,1,4,5] + g = Permutation([2,1,0,3,4,5]) + dummies = [0,1,2,3] + t0 = ([], [Permutation(list(range(4)))], 1, 0) + can = canonicalize(g, dummies, 0, t0, t1, t2) + assert can == [0,2,3,1,4,5] + # A, B without symmetry + # A^{d1}_{d0}*B_{d1}^{d0}; g = [2,1,3,0,4,5] + # T_c = A^{d0 d1}*B_{d0 d1}; can = [0,2,1,3,4,5] + t0 = t1 = ([], [Permutation(list(range(4)))], 1, 0) + dummies = [0,1,2,3] + g = Permutation([2,1,3,0,4,5]) + can = canonicalize(g, dummies, 0, t0, t1) + assert can == [0, 2, 1, 3, 4, 5] + # A_{d0}^{d1}*B_{d1}^{d0}; g = [1,2,3,0,4,5] + # T_c = A^{d0 d1}*B_{d1 d0}; can = [0,2,3,1,4,5] + g = Permutation([1,2,3,0,4,5]) + can = canonicalize(g, dummies, 0, t0, t1) + assert can == [0,2,3,1,4,5] + + # A, B, C without symmetry + # A^{d1 d0}*B_{a d0}*C_{d1 b} ord=[a,b,d0,-d0,d1,-d1] + # g=[4,2,0,3,5,1,6,7] + # T_c=A^{d0 d1}*B_{a d1}*C_{d0 b}; can = [2,4,0,5,3,1,6,7] + t0 = t1 = t2 = ([], [Permutation(list(range(4)))], 1, 0) + dummies = [2,3,4,5] + g = Permutation([4,2,0,3,5,1,6,7]) + can = canonicalize(g, dummies, 0, t0, t1, t2) + assert can == [2,4,0,5,3,1,6,7] + + # A symmetric, B and C without symmetry + # A^{d1 d0}*B_{a d0}*C_{d1 b} ord=[a,b,d0,-d0,d1,-d1] + # g=[4,2,0,3,5,1,6,7] + # T_c = A^{d0 d1}*B_{a d0}*C_{d1 b}; can = [2,4,0,3,5,1,6,7] + t0 = (base2,gens2,1,0) + t1 = t2 = ([], [Permutation(list(range(4)))], 1, 0) + dummies = [2,3,4,5] + g = Permutation([4,2,0,3,5,1,6,7]) + can = canonicalize(g, dummies, 0, t0, t1, t2) + assert can == [2,4,0,3,5,1,6,7] + + # A and C symmetric, B without symmetry + # A^{d1 d0}*B_{a d0}*C_{d1 b} ord=[a,b,d0,-d0,d1,-d1] + # g=[4,2,0,3,5,1,6,7] + # T_c = A^{d0 d1}*B_{a d0}*C_{b d1}; can = [2,4,0,3,1,5,6,7] + t0 = t2 = (base2,gens2,1,0) + t1 = ([], [Permutation(list(range(4)))], 1, 0) + dummies = [2,3,4,5] + g = Permutation([4,2,0,3,5,1,6,7]) + can = canonicalize(g, dummies, 0, t0, t1, t2) + assert can == [2,4,0,3,1,5,6,7] + + # A symmetric, B without symmetry, C antisymmetric + # A^{d1 d0}*B_{a d0}*C_{d1 b} ord=[a,b,d0,-d0,d1,-d1] + # g=[4,2,0,3,5,1,6,7] + # T_c = -A^{d0 d1}*B_{a d0}*C_{b d1}; can = [2,4,0,3,1,5,7,6] + t0 = (base2,gens2, 1, 0) + t1 = ([], [Permutation(list(range(4)))], 1, 0) + base2a, gens2a = get_symmetric_group_sgs(2, 1) + t2 = (base2a, gens2a, 1, 0) + dummies = [2,3,4,5] + g = Permutation([4,2,0,3,5,1,6,7]) + can = canonicalize(g, dummies, 0, t0, t1, t2) + assert can == [2,4,0,3,1,5,7,6] + + +def test_canonicalize_no_dummies(): + base1, gens1 = get_symmetric_group_sgs(1) + base2, gens2 = get_symmetric_group_sgs(2) + base2a, gens2a = get_symmetric_group_sgs(2, 1) + + # A commuting + # A^c A^b A^a; ord = [a,b,c]; g = [2,1,0,3,4] + # T_c = A^a A^b A^c; can = list(range(5)) + g = Permutation([2,1,0,3,4]) + can = canonicalize(g, [], 0, (base1, gens1, 3, 0)) + assert can == list(range(5)) + + # A anticommuting + # A^c A^b A^a; ord = [a,b,c]; g = [2,1,0,3,4] + # T_c = -A^a A^b A^c; can = [0,1,2,4,3] + g = Permutation([2,1,0,3,4]) + can = canonicalize(g, [], 0, (base1, gens1, 3, 1)) + assert can == [0,1,2,4,3] + + # A commuting and symmetric + # A^{b,d}*A^{c,a}; ord = [a,b,c,d]; g = [1,3,2,0,4,5] + # T_c = A^{a c}*A^{b d}; can = [0,2,1,3,4,5] + g = Permutation([1,3,2,0,4,5]) + can = canonicalize(g, [], 0, (base2, gens2, 2, 0)) + assert can == [0,2,1,3,4,5] + + # A anticommuting and symmetric + # A^{b,d}*A^{c,a}; ord = [a,b,c,d]; g = [1,3,2,0,4,5] + # T_c = -A^{a c}*A^{b d}; can = [0,2,1,3,5,4] + g = Permutation([1,3,2,0,4,5]) + can = canonicalize(g, [], 0, (base2, gens2, 2, 1)) + assert can == [0,2,1,3,5,4] + # A^{c,a}*A^{b,d} ; g = [2,0,1,3,4,5] + # T_c = A^{a c}*A^{b d}; can = [0,2,1,3,4,5] + g = Permutation([2,0,1,3,4,5]) + can = canonicalize(g, [], 0, (base2, gens2, 2, 1)) + assert can == [0,2,1,3,4,5] + +def test_no_metric_symmetry(): + # no metric symmetry + # A^d1_d0 * A^d0_d1; ord = [d0,-d0,d1,-d1]; g= [2,1,0,3,4,5] + # T_c = A^d0_d1 * A^d1_d0; can = [0,3,2,1,4,5] + g = Permutation([2,1,0,3,4,5]) + can = canonicalize(g, list(range(4)), None, [[], [Permutation(list(range(4)))], 2, 0]) + assert can == [0,3,2,1,4,5] + + # A^d1_d2 * A^d0_d3 * A^d2_d1 * A^d3_d0 + # ord = [d0,-d0,d1,-d1,d2,-d2,d3,-d3] + # 0 1 2 3 4 5 6 7 + # g = [2,5,0,7,4,3,6,1,8,9] + # T_c = A^d0_d1 * A^d1_d0 * A^d2_d3 * A^d3_d2 + # can = [0,3,2,1,4,7,6,5,8,9] + g = Permutation([2,5,0,7,4,3,6,1,8,9]) + #can = canonicalize(g, list(range(8)), 0, [[], [list(range(4))], 4, 0]) + #assert can == [0, 2, 3, 1, 4, 6, 7, 5, 8, 9] + can = canonicalize(g, list(range(8)), None, [[], [Permutation(list(range(4)))], 4, 0]) + assert can == [0, 3, 2, 1, 4, 7, 6, 5, 8, 9] + + # A^d0_d2 * A^d1_d3 * A^d3_d0 * A^d2_d1 + # g = [0,5,2,7,6,1,4,3,8,9] + # T_c = A^d0_d1 * A^d1_d2 * A^d2_d3 * A^d3_d0 + # can = [0,3,2,5,4,7,6,1,8,9] + g = Permutation([0,5,2,7,6,1,4,3,8,9]) + can = canonicalize(g, list(range(8)), None, [[], [Permutation(list(range(4)))], 4, 0]) + assert can == [0,3,2,5,4,7,6,1,8,9] + + g = Permutation([12,7,10,3,14,13,4,11,6,1,2,9,0,15,8,5,16,17]) + can = canonicalize(g, list(range(16)), None, [[], [Permutation(list(range(4)))], 8, 0]) + assert can == [0,3,2,5,4,7,6,1,8,11,10,13,12,15,14,9,16,17] + +def test_canonical_free(): + # t = A^{d0 a1}*A_d0^a0 + # ord = [a0,a1,d0,-d0]; g = [2,1,3,0,4,5]; dummies = [[2,3]] + # t_c = A_d0^a0*A^{d0 a1} + # can = [3,0, 2,1, 4,5] + g = Permutation([2,1,3,0,4,5]) + dummies = [[2,3]] + can = canonicalize(g, dummies, [None], ([], [Permutation(3)], 2, 0)) + assert can == [3,0, 2,1, 4,5] + +def test_canonicalize1(): + base1, gens1 = get_symmetric_group_sgs(1) + base1a, gens1a = get_symmetric_group_sgs(1, 1) + base2, gens2 = get_symmetric_group_sgs(2) + base3, gens3 = get_symmetric_group_sgs(3) + base2a, gens2a = get_symmetric_group_sgs(2, 1) + base3a, gens3a = get_symmetric_group_sgs(3, 1) + + # A_d0*A^d0; ord = [d0,-d0]; g = [1,0,2,3] + # T_c = A^d0*A_d0; can = [0,1,2,3] + g = Permutation([1,0,2,3]) + can = canonicalize(g, [0, 1], 0, (base1, gens1, 2, 0)) + assert can == list(range(4)) + + # A commuting + # A_d0*A_d1*A_d2*A^d2*A^d1*A^d0; ord=[d0,-d0,d1,-d1,d2,-d2] + # g = [1,3,5,4,2,0,6,7] + # T_c = A^d0*A_d0*A^d1*A_d1*A^d2*A_d2; can = list(range(8)) + g = Permutation([1,3,5,4,2,0,6,7]) + can = canonicalize(g, list(range(6)), 0, (base1, gens1, 6, 0)) + assert can == list(range(8)) + + # A anticommuting + # A_d0*A_d1*A_d2*A^d2*A^d1*A^d0; ord=[d0,-d0,d1,-d1,d2,-d2] + # g = [1,3,5,4,2,0,6,7] + # T_c 0; can = 0 + g = Permutation([1,3,5,4,2,0,6,7]) + can = canonicalize(g, list(range(6)), 0, (base1, gens1, 6, 1)) + assert can == 0 + can1 = canonicalize_naive(g, list(range(6)), 0, (base1, gens1, 6, 1)) + assert can1 == 0 + + # A commuting symmetric + # A^{d0 b}*A^a_d1*A^d1_d0; ord=[a,b,d0,-d0,d1,-d1] + # g = [2,1,0,5,4,3,6,7] + # T_c = A^{a d0}*A^{b d1}*A_{d0 d1}; can = [0,2,1,4,3,5,6,7] + g = Permutation([2,1,0,5,4,3,6,7]) + can = canonicalize(g, list(range(2,6)), 0, (base2, gens2, 3, 0)) + assert can == [0,2,1,4,3,5,6,7] + + # A, B commuting symmetric + # A^{d0 b}*A^d1_d0*B^a_d1; ord=[a,b,d0,-d0,d1,-d1] + # g = [2,1,4,3,0,5,6,7] + # T_c = A^{b d0}*A_d0^d1*B^a_d1; can = [1,2,3,4,0,5,6,7] + g = Permutation([2,1,4,3,0,5,6,7]) + can = canonicalize(g, list(range(2,6)), 0, (base2,gens2,2,0), (base2,gens2,1,0)) + assert can == [1,2,3,4,0,5,6,7] + + # A commuting symmetric + # A^{d1 d0 b}*A^{a}_{d1 d0}; ord=[a,b, d0,-d0,d1,-d1] + # g = [4,2,1,0,5,3,6,7] + # T_c = A^{a d0 d1}*A^{b}_{d0 d1}; can = [0,2,4,1,3,5,6,7] + g = Permutation([4,2,1,0,5,3,6,7]) + can = canonicalize(g, list(range(2,6)), 0, (base3, gens3, 2, 0)) + assert can == [0,2,4,1,3,5,6,7] + + + # A^{d3 d0 d2}*A^a0_{d1 d2}*A^d1_d3^a1*A^{a2 a3}_d0 + # ord = [a0,a1,a2,a3,d0,-d0,d1,-d1,d2,-d2,d3,-d3] + # 0 1 2 3 4 5 6 7 8 9 10 11 + # g = [10,4,8, 0,7,9, 6,11,1, 2,3,5, 12,13] + # T_c = A^{a0 d0 d1}*A^a1_d0^d2*A^{a2 a3 d3}*A_{d1 d2 d3} + # can = [0,4,6, 1,5,8, 2,3,10, 7,9,11, 12,13] + g = Permutation([10,4,8, 0,7,9, 6,11,1, 2,3,5, 12,13]) + can = canonicalize(g, list(range(4,12)), 0, (base3, gens3, 4, 0)) + assert can == [0,4,6, 1,5,8, 2,3,10, 7,9,11, 12,13] + + # A commuting symmetric, B antisymmetric + # A^{d0 d1 d2} * A_{d2 d3 d1} * B_d0^d3 + # ord = [d0,-d0,d1,-d1,d2,-d2,d3,-d3] + # g = [0,2,4,5,7,3,1,6,8,9] + # in this esxample and in the next three, + # renaming dummy indices and using symmetry of A, + # T = A^{d0 d1 d2} * A_{d0 d1 d3} * B_d2^d3 + # can = 0 + g = Permutation([0,2,4,5,7,3,1,6,8,9]) + can = canonicalize(g, list(range(8)), 0, (base3, gens3,2,0), (base2a,gens2a,1,0)) + assert can == 0 + # A anticommuting symmetric, B anticommuting + # A^{d0 d1 d2} * A_{d2 d3 d1} * B_d0^d3 + # T_c = A^{d0 d1 d2} * A_{d0 d1}^d3 * B_{d2 d3} + # can = [0,2,4, 1,3,6, 5,7, 8,9] + can = canonicalize(g, list(range(8)), 0, (base3, gens3,2,1), (base2a,gens2a,1,0)) + assert can == [0,2,4, 1,3,6, 5,7, 8,9] + # A anticommuting symmetric, B antisymmetric commuting, antisymmetric metric + # A^{d0 d1 d2} * A_{d2 d3 d1} * B_d0^d3 + # T_c = -A^{d0 d1 d2} * A_{d0 d1}^d3 * B_{d2 d3} + # can = [0,2,4, 1,3,6, 5,7, 9,8] + can = canonicalize(g, list(range(8)), 1, (base3, gens3,2,1), (base2a,gens2a,1,0)) + assert can == [0,2,4, 1,3,6, 5,7, 9,8] + + # A anticommuting symmetric, B anticommuting anticommuting, + # no metric symmetry + # A^{d0 d1 d2} * A_{d2 d3 d1} * B_d0^d3 + # T_c = A^{d0 d1 d2} * A_{d0 d1 d3} * B_d2^d3 + # can = [0,2,4, 1,3,7, 5,6, 8,9] + can = canonicalize(g, list(range(8)), None, (base3, gens3,2,1), (base2a,gens2a,1,0)) + assert can == [0,2,4,1,3,7,5,6,8,9] + + # Gamma anticommuting + # Gamma_{mu nu} * gamma^rho * Gamma^{nu mu alpha} + # ord = [alpha, rho, mu,-mu,nu,-nu] + # g = [3,5,1,4,2,0,6,7] + # T_c = -Gamma^{mu nu} * gamma^rho * Gamma_{alpha mu nu} + # can = [2,4,1,0,3,5,7,6]] + g = Permutation([3,5,1,4,2,0,6,7]) + t0 = (base2a, gens2a, 1, None) + t1 = (base1, gens1, 1, None) + t2 = (base3a, gens3a, 1, None) + can = canonicalize(g, list(range(2, 6)), 0, t0, t1, t2) + assert can == [2,4,1,0,3,5,7,6] + + # Gamma_{mu nu} * Gamma^{gamma beta} * gamma_rho * Gamma^{nu mu alpha} + # ord = [alpha, beta, gamma, -rho, mu,-mu,nu,-nu] + # 0 1 2 3 4 5 6 7 + # g = [5,7,2,1,3,6,4,0,8,9] + # T_c = Gamma^{mu nu} * Gamma^{beta gamma} * gamma_rho * Gamma^alpha_{mu nu} # can = [4,6,1,2,3,0,5,7,8,9] + t0 = (base2a, gens2a, 2, None) + g = Permutation([5,7,2,1,3,6,4,0,8,9]) + can = canonicalize(g, list(range(4, 8)), 0, t0, t1, t2) + assert can == [4,6,1,2,3,0,5,7,8,9] + + # f^a_{b,c} antisymmetric in b,c; A_mu^a no symmetry + # f^c_{d a} * f_{c e b} * A_mu^d * A_nu^a * A^{nu e} * A^{mu b} + # ord = [mu,-mu,nu,-nu,a,-a,b,-b,c,-c,d,-d, e, -e] + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + # g = [8,11,5, 9,13,7, 1,10, 3,4, 2,12, 0,6, 14,15] + # T_c = -f^{a b c} * f_a^{d e} * A^mu_b * A_{mu d} * A^nu_c * A_{nu e} + # can = [4,6,8, 5,10,12, 0,7, 1,11, 2,9, 3,13, 15,14] + g = Permutation([8,11,5, 9,13,7, 1,10, 3,4, 2,12, 0,6, 14,15]) + base_f, gens_f = bsgs_direct_product(base1, gens1, base2a, gens2a) + base_A, gens_A = bsgs_direct_product(base1, gens1, base1, gens1) + t0 = (base_f, gens_f, 2, 0) + t1 = (base_A, gens_A, 4, 0) + can = canonicalize(g, [list(range(4)), list(range(4, 14))], [0, 0], t0, t1) + assert can == [4,6,8, 5,10,12, 0,7, 1,11, 2,9, 3,13, 15,14] + + +def test_riemann_invariants(): + baser, gensr = riemann_bsgs + # R^{d0 d1}_{d1 d0}; ord = [d0,-d0,d1,-d1]; g = [0,2,3,1,4,5] + # T_c = -R^{d0 d1}_{d0 d1}; can = [0,2,1,3,5,4] + g = Permutation([0,2,3,1,4,5]) + can = canonicalize(g, list(range(2, 4)), 0, (baser, gensr, 1, 0)) + assert can == [0,2,1,3,5,4] + # use a non minimal BSGS + can = canonicalize(g, list(range(2, 4)), 0, ([2, 0], [Permutation([1,0,2,3,5,4]), Permutation([2,3,0,1,4,5])], 1, 0)) + assert can == [0,2,1,3,5,4] + + """ + The following tests in test_riemann_invariants and in + test_riemann_invariants1 have been checked using xperm.c from XPerm in + in [1] and with an older version contained in [2] + + [1] xperm.c part of xPerm written by J. M. Martin-Garcia + http://www.xact.es/index.html + [2] test_xperm.cc in cadabra by Kasper Peeters, http://cadabra.phi-sci.com/ + """ + # R_d11^d1_d0^d5 * R^{d6 d4 d0}_d5 * R_{d7 d2 d8 d9} * + # R_{d10 d3 d6 d4} * R^{d2 d7 d11}_d1 * R^{d8 d9 d3 d10} + # ord: contravariant d_k ->2*k, covariant d_k -> 2*k+1 + # T_c = R^{d0 d1 d2 d3} * R_{d0 d1}^{d4 d5} * R_{d2 d3}^{d6 d7} * + # R_{d4 d5}^{d8 d9} * R_{d6 d7}^{d10 d11} * R_{d8 d9 d10 d11} + g = Permutation([23,2,1,10,12,8,0,11,15,5,17,19,21,7,13,9,4,14,22,3,16,18,6,20,24,25]) + can = canonicalize(g, list(range(24)), 0, (baser, gensr, 6, 0)) + assert can == [0,2,4,6,1,3,8,10,5,7,12,14,9,11,16,18,13,15,20,22,17,19,21,23,24,25] + + # use a non minimal BSGS + can = canonicalize(g, list(range(24)), 0, ([2, 0], [Permutation([1,0,2,3,5,4]), Permutation([2,3,0,1,4,5])], 6, 0)) + assert can == [0,2,4,6,1,3,8,10,5,7,12,14,9,11,16,18,13,15,20,22,17,19,21,23,24,25] + + g = Permutation([0,2,5,7,4,6,9,11,8,10,13,15,12,14,17,19,16,18,21,23,20,22,25,27,24,26,29,31,28,30,33,35,32,34,37,39,36,38,1,3,40,41]) + can = canonicalize(g, list(range(40)), 0, (baser, gensr, 10, 0)) + assert can == [0,2,4,6,1,3,8,10,5,7,12,14,9,11,16,18,13,15,20,22,17,19,24,26,21,23,28,30,25,27,32,34,29,31,36,38,33,35,37,39,40,41] + + +@XFAIL +def test_riemann_invariants1(): + skip('takes too much time') + baser, gensr = riemann_bsgs + g = Permutation([17, 44, 11, 3, 0, 19, 23, 15, 38, 4, 25, 27, 43, 36, 22, 14, 8, 30, 41, 20, 2, 10, 12, 28, 18, 1, 29, 13, 37, 42, 33, 7, 9, 31, 24, 26, 39, 5, 34, 47, 32, 6, 21, 40, 35, 46, 45, 16, 48, 49]) + can = canonicalize(g, list(range(48)), 0, (baser, gensr, 12, 0)) + assert can == [0, 2, 4, 6, 1, 3, 8, 10, 5, 7, 12, 14, 9, 11, 16, 18, 13, 15, 20, 22, 17, 19, 24, 26, 21, 23, 28, 30, 25, 27, 32, 34, 29, 31, 36, 38, 33, 35, 40, 42, 37, 39, 44, 46, 41, 43, 45, 47, 48, 49] + + g = Permutation([0,2,4,6, 7,8,10,12, 14,16,18,20, 19,22,24,26, 5,21,28,30, 32,34,36,38, 40,42,44,46, 13,48,50,52, 15,49,54,56, 17,33,41,58, 9,23,60,62, 29,35,63,64, 3,45,66,68, 25,37,47,57, 11,31,69,70, 27,39,53,72, 1,59,73,74, 55,61,67,76, 43,65,75,78, 51,71,77,79, 80,81]) + can = canonicalize(g, list(range(80)), 0, (baser, gensr, 20, 0)) + assert can == [0,2,4,6, 1,8,10,12, 3,14,16,18, 5,20,22,24, 7,26,28,30, 9,15,32,34, 11,36,23,38, 13,40,42,44, 17,39,29,46, 19,48,43,50, 21,45,52,54, 25,56,33,58, 27,60,53,62, 31,51,64,66, 35,65,47,68, 37,70,49,72, 41,74,57,76, 55,67,59,78, 61,69,71,75, 63,79,73,77, 80,81] + + +def test_riemann_products(): + baser, gensr = riemann_bsgs + base1, gens1 = get_symmetric_group_sgs(1) + base2, gens2 = get_symmetric_group_sgs(2) + base2a, gens2a = get_symmetric_group_sgs(2, 1) + + # R^{a b d0}_d0 = 0 + g = Permutation([0,1,2,3,4,5]) + can = canonicalize(g, list(range(2,4)), 0, (baser, gensr, 1, 0)) + assert can == 0 + + # R^{d0 b a}_d0 ; ord = [a,b,d0,-d0}; g = [2,1,0,3,4,5] + # T_c = -R^{a d0 b}_d0; can = [0,2,1,3,5,4] + g = Permutation([2,1,0,3,4,5]) + can = canonicalize(g, list(range(2, 4)), 0, (baser, gensr, 1, 0)) + assert can == [0,2,1,3,5,4] + + # R^d1_d2^b_d0 * R^{d0 a}_d1^d2; ord=[a,b,d0,-d0,d1,-d1,d2,-d2] + # g = [4,7,1,3,2,0,5,6,8,9] + # T_c = -R^{a d0 d1 d2}* R^b_{d0 d1 d2} + # can = [0,2,4,6,1,3,5,7,9,8] + g = Permutation([4,7,1,3,2,0,5,6,8,9]) + can = canonicalize(g, list(range(2,8)), 0, (baser, gensr, 2, 0)) + assert can == [0,2,4,6,1,3,5,7,9,8] + can1 = canonicalize_naive(g, list(range(2,8)), 0, (baser, gensr, 2, 0)) + assert can == can1 + + # A symmetric commuting + # R^{d6 d5}_d2^d1 * R^{d4 d0 d2 d3} * A_{d6 d0} A_{d3 d1} * A_{d4 d5} + # g = [12,10,5,2, 8,0,4,6, 13,1, 7,3, 9,11,14,15] + # T_c = -R^{d0 d1 d2 d3} * R_d0^{d4 d5 d6} * A_{d1 d4}*A_{d2 d5}*A_{d3 d6} + + g = Permutation([12,10,5,2,8,0,4,6,13,1,7,3,9,11,14,15]) + can = canonicalize(g, list(range(14)), 0, ((baser,gensr,2,0)), (base2,gens2,3,0)) + assert can == [0, 2, 4, 6, 1, 8, 10, 12, 3, 9, 5, 11, 7, 13, 15, 14] + + # R^{d2 a0 a2 d0} * R^d1_d2^{a1 a3} * R^{a4 a5}_{d0 d1} + # ord = [a0,a1,a2,a3,a4,a5,d0,-d0,d1,-d1,d2,-d2] + # 0 1 2 3 4 5 6 7 8 9 10 11 + # can = [0, 6, 2, 8, 1, 3, 7, 10, 4, 5, 9, 11, 12, 13] + # T_c = R^{a0 d0 a2 d1}*R^{a1 a3}_d0^d2*R^{a4 a5}_{d1 d2} + g = Permutation([10,0,2,6,8,11,1,3,4,5,7,9,12,13]) + can = canonicalize(g, list(range(6,12)), 0, (baser, gensr, 3, 0)) + assert can == [0, 6, 2, 8, 1, 3, 7, 10, 4, 5, 9, 11, 12, 13] + #can1 = canonicalize_naive(g, list(range(6,12)), 0, (baser, gensr, 3, 0)) + #assert can == can1 + + # A^n_{i, j} antisymmetric in i,j + # A_m0^d0_a1 * A_m1^a0_d0; ord = [m0,m1,a0,a1,d0,-d0] + # g = [0,4,3,1,2,5,6,7] + # T_c = -A_{m a1}^d0 * A_m1^a0_d0 + # can = [0,3,4,1,2,5,7,6] + base, gens = bsgs_direct_product(base1, gens1, base2a, gens2a) + dummies = list(range(4, 6)) + g = Permutation([0,4,3,1,2,5,6,7]) + can = canonicalize(g, dummies, 0, (base, gens, 2, 0)) + assert can == [0, 3, 4, 1, 2, 5, 7, 6] + + + # A^n_{i, j} symmetric in i,j + # A^m0_a0^d2 * A^n0_d2^d1 * A^n1_d1^d0 * A_{m0 d0}^a1 + # ordering: first the free indices; then first n, then d + # ord=[n0,n1,a0,a1, m0,-m0,d0,-d0,d1,-d1,d2,-d2] + # 0 1 2 3 4 5 6 7 8 9 10 11] + # g = [4,2,10, 0,11,8, 1,9,6, 5,7,3, 12,13] + # if the dummy indices m_i and d_i were separated, + # one gets + # T_c = A^{n0 d0 d1} * A^n1_d0^d2 * A^m0^a0_d1 * A_m0^a1_d2 + # can = [0, 6, 8, 1, 7, 10, 4, 2, 9, 5, 3, 11, 12, 13] + # If they are not, so can is + # T_c = A^{n0 m0 d0} A^n1_m0^d1 A^{d2 a0}_d0 A_d2^a1_d1 + # can = [0, 4, 6, 1, 5, 8, 10, 2, 7, 11, 3, 9, 12, 13] + # case with single type of indices + + base, gens = bsgs_direct_product(base1, gens1, base2, gens2) + dummies = list(range(4, 12)) + g = Permutation([4,2,10, 0,11,8, 1,9,6, 5,7,3, 12,13]) + can = canonicalize(g, dummies, 0, (base, gens, 4, 0)) + assert can == [0, 4, 6, 1, 5, 8, 10, 2, 7, 11, 3, 9, 12, 13] + # case with separated indices + dummies = [list(range(4, 6)), list(range(6,12))] + sym = [0, 0] + can = canonicalize(g, dummies, sym, (base, gens, 4, 0)) + assert can == [0, 6, 8, 1, 7, 10, 4, 2, 9, 5, 3, 11, 12, 13] + # case with separated indices with the second type of index + # with antisymmetric metric: there is a sign change + sym = [0, 1] + can = canonicalize(g, dummies, sym, (base, gens, 4, 0)) + assert can == [0, 6, 8, 1, 7, 10, 4, 2, 9, 5, 3, 11, 13, 12] + +def test_graph_certificate(): + # test tensor invariants constructed from random regular graphs; + # checked graph isomorphism with networkx + import random + def randomize_graph(size, g): + p = list(range(size)) + random.shuffle(p) + g1a = {} + for k, v in g1.items(): + g1a[p[k]] = [p[i] for i in v] + return g1a + + g1 = {0: [2, 3, 7], 1: [4, 5, 7], 2: [0, 4, 6], 3: [0, 6, 7], 4: [1, 2, 5], 5: [1, 4, 6], 6: [2, 3, 5], 7: [0, 1, 3]} + g2 = {0: [2, 3, 7], 1: [2, 4, 5], 2: [0, 1, 5], 3: [0, 6, 7], 4: [1, 5, 6], 5: [1, 2, 4], 6: [3, 4, 7], 7: [0, 3, 6]} + + c1 = graph_certificate(g1) + c2 = graph_certificate(g2) + assert c1 != c2 + g1a = randomize_graph(8, g1) + c1a = graph_certificate(g1a) + assert c1 == c1a + + g1 = {0: [8, 1, 9, 7], 1: [0, 9, 3, 4], 2: [3, 4, 6, 7], 3: [1, 2, 5, 6], 4: [8, 1, 2, 5], 5: [9, 3, 4, 7], 6: [8, 2, 3, 7], 7: [0, 2, 5, 6], 8: [0, 9, 4, 6], 9: [8, 0, 5, 1]} + g2 = {0: [1, 2, 5, 6], 1: [0, 9, 5, 7], 2: [0, 4, 6, 7], 3: [8, 9, 6, 7], 4: [8, 2, 6, 7], 5: [0, 9, 8, 1], 6: [0, 2, 3, 4], 7: [1, 2, 3, 4], 8: [9, 3, 4, 5], 9: [8, 1, 3, 5]} + c1 = graph_certificate(g1) + c2 = graph_certificate(g2) + assert c1 != c2 + g1a = randomize_graph(10, g1) + c1a = graph_certificate(g1a) + assert c1 == c1a diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_testutil.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_testutil.py new file mode 100644 index 0000000000000000000000000000000000000000..736e7a4ff86967e41dca71cf12de6c387a82d26d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_testutil.py @@ -0,0 +1,55 @@ +from sympy.combinatorics.named_groups import SymmetricGroup, AlternatingGroup,\ + CyclicGroup +from sympy.combinatorics.testutil import _verify_bsgs, _cmp_perm_lists,\ + _naive_list_centralizer, _verify_centralizer,\ + _verify_normal_closure +from sympy.combinatorics.permutations import Permutation +from sympy.combinatorics.perm_groups import PermutationGroup +from sympy.core.random import shuffle + + +def test_cmp_perm_lists(): + S = SymmetricGroup(4) + els = list(S.generate_dimino()) + other = els.copy() + shuffle(other) + assert _cmp_perm_lists(els, other) is True + + +def test_naive_list_centralizer(): + # verified by GAP + S = SymmetricGroup(3) + A = AlternatingGroup(3) + assert _naive_list_centralizer(S, S) == [Permutation([0, 1, 2])] + assert PermutationGroup(_naive_list_centralizer(S, A)).is_subgroup(A) + + +def test_verify_bsgs(): + S = SymmetricGroup(5) + S.schreier_sims() + base = S.base + strong_gens = S.strong_gens + assert _verify_bsgs(S, base, strong_gens) is True + assert _verify_bsgs(S, base[:-1], strong_gens) is False + assert _verify_bsgs(S, base, S.generators) is False + + +def test_verify_centralizer(): + # verified by GAP + S = SymmetricGroup(3) + A = AlternatingGroup(3) + triv = PermutationGroup([Permutation([0, 1, 2])]) + assert _verify_centralizer(S, S, centr=triv) + assert _verify_centralizer(S, A, centr=A) + + +def test_verify_normal_closure(): + # verified by GAP + S = SymmetricGroup(3) + A = AlternatingGroup(3) + assert _verify_normal_closure(S, A, closure=A) + S = SymmetricGroup(5) + A = AlternatingGroup(5) + C = CyclicGroup(5) + assert _verify_normal_closure(S, A, closure=A) + assert _verify_normal_closure(S, C, closure=A) diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_util.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_util.py new file mode 100644 index 0000000000000000000000000000000000000000..bca183e81f354e398aee9ae809fe79b20c7f2468 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/tests/test_util.py @@ -0,0 +1,120 @@ +from sympy.combinatorics.named_groups import SymmetricGroup, DihedralGroup,\ + AlternatingGroup +from sympy.combinatorics.permutations import Permutation +from sympy.combinatorics.util import _check_cycles_alt_sym, _strip,\ + _distribute_gens_by_base, _strong_gens_from_distr,\ + _orbits_transversals_from_bsgs, _handle_precomputed_bsgs, _base_ordering,\ + _remove_gens +from sympy.combinatorics.testutil import _verify_bsgs + + +def test_check_cycles_alt_sym(): + perm1 = Permutation([[0, 1, 2, 3, 4, 5, 6], [7], [8], [9]]) + perm2 = Permutation([[0, 1, 2, 3, 4, 5], [6, 7, 8, 9]]) + perm3 = Permutation([[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]) + assert _check_cycles_alt_sym(perm1) is True + assert _check_cycles_alt_sym(perm2) is False + assert _check_cycles_alt_sym(perm3) is False + + +def test_strip(): + D = DihedralGroup(5) + D.schreier_sims() + member = Permutation([4, 0, 1, 2, 3]) + not_member1 = Permutation([0, 1, 4, 3, 2]) + not_member2 = Permutation([3, 1, 4, 2, 0]) + identity = Permutation([0, 1, 2, 3, 4]) + res1 = _strip(member, D.base, D.basic_orbits, D.basic_transversals) + res2 = _strip(not_member1, D.base, D.basic_orbits, D.basic_transversals) + res3 = _strip(not_member2, D.base, D.basic_orbits, D.basic_transversals) + assert res1[0] == identity + assert res1[1] == len(D.base) + 1 + assert res2[0] == not_member1 + assert res2[1] == len(D.base) + 1 + assert res3[0] != identity + assert res3[1] == 2 + + +def test_distribute_gens_by_base(): + base = [0, 1, 2] + gens = [Permutation([0, 1, 2, 3]), Permutation([0, 1, 3, 2]), + Permutation([0, 2, 3, 1]), Permutation([3, 2, 1, 0])] + assert _distribute_gens_by_base(base, gens) == [gens, + [Permutation([0, 1, 2, 3]), + Permutation([0, 1, 3, 2]), + Permutation([0, 2, 3, 1])], + [Permutation([0, 1, 2, 3]), + Permutation([0, 1, 3, 2])]] + + +def test_strong_gens_from_distr(): + strong_gens_distr = [[Permutation([0, 2, 1]), Permutation([1, 2, 0]), + Permutation([1, 0, 2])], [Permutation([0, 2, 1])]] + assert _strong_gens_from_distr(strong_gens_distr) == \ + [Permutation([0, 2, 1]), + Permutation([1, 2, 0]), + Permutation([1, 0, 2])] + + +def test_orbits_transversals_from_bsgs(): + S = SymmetricGroup(4) + S.schreier_sims() + base = S.base + strong_gens = S.strong_gens + strong_gens_distr = _distribute_gens_by_base(base, strong_gens) + result = _orbits_transversals_from_bsgs(base, strong_gens_distr) + orbits = result[0] + transversals = result[1] + base_len = len(base) + for i in range(base_len): + for el in orbits[i]: + assert transversals[i][el](base[i]) == el + for j in range(i): + assert transversals[i][el](base[j]) == base[j] + order = 1 + for i in range(base_len): + order *= len(orbits[i]) + assert S.order() == order + + +def test_handle_precomputed_bsgs(): + A = AlternatingGroup(5) + A.schreier_sims() + base = A.base + strong_gens = A.strong_gens + result = _handle_precomputed_bsgs(base, strong_gens) + strong_gens_distr = _distribute_gens_by_base(base, strong_gens) + assert strong_gens_distr == result[2] + transversals = result[0] + orbits = result[1] + base_len = len(base) + for i in range(base_len): + for el in orbits[i]: + assert transversals[i][el](base[i]) == el + for j in range(i): + assert transversals[i][el](base[j]) == base[j] + order = 1 + for i in range(base_len): + order *= len(orbits[i]) + assert A.order() == order + + +def test_base_ordering(): + base = [2, 4, 5] + degree = 7 + assert _base_ordering(base, degree) == [3, 4, 0, 5, 1, 2, 6] + + +def test_remove_gens(): + S = SymmetricGroup(10) + base, strong_gens = S.schreier_sims_incremental() + new_gens = _remove_gens(base, strong_gens) + assert _verify_bsgs(S, base, new_gens) is True + A = AlternatingGroup(7) + base, strong_gens = A.schreier_sims_incremental() + new_gens = _remove_gens(base, strong_gens) + assert _verify_bsgs(A, base, new_gens) is True + D = DihedralGroup(2) + base, strong_gens = D.schreier_sims_incremental() + new_gens = _remove_gens(base, strong_gens) + assert _verify_bsgs(D, base, new_gens) is True diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/testutil.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/testutil.py new file mode 100644 index 0000000000000000000000000000000000000000..9fe664ce9e7437b97feec90f2052eb2987c57a4e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/testutil.py @@ -0,0 +1,357 @@ +from sympy.combinatorics import Permutation +from sympy.combinatorics.util import _distribute_gens_by_base + +rmul = Permutation.rmul + + +def _cmp_perm_lists(first, second): + """ + Compare two lists of permutations as sets. + + Explanation + =========== + + This is used for testing purposes. Since the array form of a + permutation is currently a list, Permutation is not hashable + and cannot be put into a set. + + Examples + ======== + + >>> from sympy.combinatorics.permutations import Permutation + >>> from sympy.combinatorics.testutil import _cmp_perm_lists + >>> a = Permutation([0, 2, 3, 4, 1]) + >>> b = Permutation([1, 2, 0, 4, 3]) + >>> c = Permutation([3, 4, 0, 1, 2]) + >>> ls1 = [a, b, c] + >>> ls2 = [b, c, a] + >>> _cmp_perm_lists(ls1, ls2) + True + + """ + return {tuple(a) for a in first} == \ + {tuple(a) for a in second} + + +def _naive_list_centralizer(self, other, af=False): + from sympy.combinatorics.perm_groups import PermutationGroup + """ + Return a list of elements for the centralizer of a subgroup/set/element. + + Explanation + =========== + + This is a brute force implementation that goes over all elements of the + group and checks for membership in the centralizer. It is used to + test ``.centralizer()`` from ``sympy.combinatorics.perm_groups``. + + Examples + ======== + + >>> from sympy.combinatorics.testutil import _naive_list_centralizer + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> D = DihedralGroup(4) + >>> _naive_list_centralizer(D, D) + [Permutation([0, 1, 2, 3]), Permutation([2, 3, 0, 1])] + + See Also + ======== + + sympy.combinatorics.perm_groups.centralizer + + """ + from sympy.combinatorics.permutations import _af_commutes_with + if hasattr(other, 'generators'): + elements = list(self.generate_dimino(af=True)) + gens = [x._array_form for x in other.generators] + commutes_with_gens = lambda x: all(_af_commutes_with(x, gen) for gen in gens) + centralizer_list = [] + if not af: + for element in elements: + if commutes_with_gens(element): + centralizer_list.append(Permutation._af_new(element)) + else: + for element in elements: + if commutes_with_gens(element): + centralizer_list.append(element) + return centralizer_list + elif hasattr(other, 'getitem'): + return _naive_list_centralizer(self, PermutationGroup(other), af) + elif hasattr(other, 'array_form'): + return _naive_list_centralizer(self, PermutationGroup([other]), af) + + +def _verify_bsgs(group, base, gens): + """ + Verify the correctness of a base and strong generating set. + + Explanation + =========== + + This is a naive implementation using the definition of a base and a strong + generating set relative to it. There are other procedures for + verifying a base and strong generating set, but this one will + serve for more robust testing. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import AlternatingGroup + >>> from sympy.combinatorics.testutil import _verify_bsgs + >>> A = AlternatingGroup(4) + >>> A.schreier_sims() + >>> _verify_bsgs(A, A.base, A.strong_gens) + True + + See Also + ======== + + sympy.combinatorics.perm_groups.PermutationGroup.schreier_sims + + """ + from sympy.combinatorics.perm_groups import PermutationGroup + strong_gens_distr = _distribute_gens_by_base(base, gens) + current_stabilizer = group + for i in range(len(base)): + candidate = PermutationGroup(strong_gens_distr[i]) + if current_stabilizer.order() != candidate.order(): + return False + current_stabilizer = current_stabilizer.stabilizer(base[i]) + if current_stabilizer.order() != 1: + return False + return True + + +def _verify_centralizer(group, arg, centr=None): + """ + Verify the centralizer of a group/set/element inside another group. + + This is used for testing ``.centralizer()`` from + ``sympy.combinatorics.perm_groups`` + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import (SymmetricGroup, + ... AlternatingGroup) + >>> from sympy.combinatorics.perm_groups import PermutationGroup + >>> from sympy.combinatorics.permutations import Permutation + >>> from sympy.combinatorics.testutil import _verify_centralizer + >>> S = SymmetricGroup(5) + >>> A = AlternatingGroup(5) + >>> centr = PermutationGroup([Permutation([0, 1, 2, 3, 4])]) + >>> _verify_centralizer(S, A, centr) + True + + See Also + ======== + + _naive_list_centralizer, + sympy.combinatorics.perm_groups.PermutationGroup.centralizer, + _cmp_perm_lists + + """ + if centr is None: + centr = group.centralizer(arg) + centr_list = list(centr.generate_dimino(af=True)) + centr_list_naive = _naive_list_centralizer(group, arg, af=True) + return _cmp_perm_lists(centr_list, centr_list_naive) + + +def _verify_normal_closure(group, arg, closure=None): + from sympy.combinatorics.perm_groups import PermutationGroup + """ + Verify the normal closure of a subgroup/subset/element in a group. + + This is used to test + sympy.combinatorics.perm_groups.PermutationGroup.normal_closure + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import (SymmetricGroup, + ... AlternatingGroup) + >>> from sympy.combinatorics.testutil import _verify_normal_closure + >>> S = SymmetricGroup(3) + >>> A = AlternatingGroup(3) + >>> _verify_normal_closure(S, A, closure=A) + True + + See Also + ======== + + sympy.combinatorics.perm_groups.PermutationGroup.normal_closure + + """ + if closure is None: + closure = group.normal_closure(arg) + conjugates = set() + if hasattr(arg, 'generators'): + subgr_gens = arg.generators + elif hasattr(arg, '__getitem__'): + subgr_gens = arg + elif hasattr(arg, 'array_form'): + subgr_gens = [arg] + for el in group.generate_dimino(): + conjugates.update(gen ^ el for gen in subgr_gens) + naive_closure = PermutationGroup(list(conjugates)) + return closure.is_subgroup(naive_closure) + + +def canonicalize_naive(g, dummies, sym, *v): + """ + Canonicalize tensor formed by tensors of the different types. + + Explanation + =========== + + sym_i symmetry under exchange of two component tensors of type `i` + None no symmetry + 0 commuting + 1 anticommuting + + Parameters + ========== + + g : Permutation representing the tensor. + dummies : List of dummy indices. + msym : Symmetry of the metric. + v : A list of (base_i, gens_i, n_i, sym_i) for tensors of type `i`. + base_i, gens_i BSGS for tensors of this type + n_i number of tensors of type `i` + + Returns + ======= + + Returns 0 if the tensor is zero, else returns the array form of + the permutation representing the canonical form of the tensor. + + Examples + ======== + + >>> from sympy.combinatorics.testutil import canonicalize_naive + >>> from sympy.combinatorics.tensor_can import get_symmetric_group_sgs + >>> from sympy.combinatorics import Permutation + >>> g = Permutation([1, 3, 2, 0, 4, 5]) + >>> base2, gens2 = get_symmetric_group_sgs(2) + >>> canonicalize_naive(g, [2, 3], 0, (base2, gens2, 2, 0)) + [0, 2, 1, 3, 4, 5] + """ + from sympy.combinatorics.perm_groups import PermutationGroup + from sympy.combinatorics.tensor_can import gens_products, dummy_sgs + from sympy.combinatorics.permutations import _af_rmul + v1 = [] + for i in range(len(v)): + base_i, gens_i, n_i, sym_i = v[i] + v1.append((base_i, gens_i, [[]]*n_i, sym_i)) + size, sbase, sgens = gens_products(*v1) + dgens = dummy_sgs(dummies, sym, size-2) + if isinstance(sym, int): + num_types = 1 + dummies = [dummies] + sym = [sym] + else: + num_types = len(sym) + dgens = [] + for i in range(num_types): + dgens.extend(dummy_sgs(dummies[i], sym[i], size - 2)) + S = PermutationGroup(sgens) + D = PermutationGroup([Permutation(x) for x in dgens]) + dlist = list(D.generate(af=True)) + g = g.array_form + st = set() + for s in S.generate(af=True): + h = _af_rmul(g, s) + for d in dlist: + q = tuple(_af_rmul(d, h)) + st.add(q) + a = list(st) + a.sort() + prev = (0,)*size + for h in a: + if h[:-2] == prev[:-2]: + if h[-1] != prev[-1]: + return 0 + prev = h + return list(a[0]) + + +def graph_certificate(gr): + """ + Return a certificate for the graph + + Parameters + ========== + + gr : adjacency list + + Explanation + =========== + + The graph is assumed to be unoriented and without + external lines. + + Associate to each vertex of the graph a symmetric tensor with + number of indices equal to the degree of the vertex; indices + are contracted when they correspond to the same line of the graph. + The canonical form of the tensor gives a certificate for the graph. + + This is not an efficient algorithm to get the certificate of a graph. + + Examples + ======== + + >>> from sympy.combinatorics.testutil import graph_certificate + >>> gr1 = {0:[1, 2, 3, 5], 1:[0, 2, 4], 2:[0, 1, 3, 4], 3:[0, 2, 4], 4:[1, 2, 3, 5], 5:[0, 4]} + >>> gr2 = {0:[1, 5], 1:[0, 2, 3, 4], 2:[1, 3, 5], 3:[1, 2, 4, 5], 4:[1, 3, 5], 5:[0, 2, 3, 4]} + >>> c1 = graph_certificate(gr1) + >>> c2 = graph_certificate(gr2) + >>> c1 + [0, 2, 4, 6, 1, 8, 10, 12, 3, 14, 16, 18, 5, 9, 15, 7, 11, 17, 13, 19, 20, 21] + >>> c1 == c2 + True + """ + from sympy.combinatorics.permutations import _af_invert + from sympy.combinatorics.tensor_can import get_symmetric_group_sgs, canonicalize + items = list(gr.items()) + items.sort(key=lambda x: len(x[1]), reverse=True) + pvert = [x[0] for x in items] + pvert = _af_invert(pvert) + + # the indices of the tensor are twice the number of lines of the graph + num_indices = 0 + for v, neigh in items: + num_indices += len(neigh) + # associate to each vertex its indices; for each line + # between two vertices assign the + # even index to the vertex which comes first in items, + # the odd index to the other vertex + vertices = [[] for i in items] + i = 0 + for v, neigh in items: + for v2 in neigh: + if pvert[v] < pvert[v2]: + vertices[pvert[v]].append(i) + vertices[pvert[v2]].append(i+1) + i += 2 + g = [] + for v in vertices: + g.extend(v) + assert len(g) == num_indices + g += [num_indices, num_indices + 1] + size = num_indices + 2 + assert sorted(g) == list(range(size)) + g = Permutation(g) + vlen = [0]*(len(vertices[0])+1) + for neigh in vertices: + vlen[len(neigh)] += 1 + v = [] + for i in range(len(vlen)): + n = vlen[i] + if n: + base, gens = get_symmetric_group_sgs(i) + v.append((base, gens, n, 0)) + v.reverse() + dummies = list(range(num_indices)) + can = canonicalize(g, dummies, 0, *v) + return can diff --git a/venv/lib/python3.11/site-packages/sympy/combinatorics/util.py b/venv/lib/python3.11/site-packages/sympy/combinatorics/util.py new file mode 100644 index 0000000000000000000000000000000000000000..fc73b02f94f4aae6f1b98bb3f0c837fd5a1d1e6d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/combinatorics/util.py @@ -0,0 +1,532 @@ +from sympy.combinatorics.permutations import Permutation, _af_invert, _af_rmul +from sympy.ntheory import isprime + +rmul = Permutation.rmul +_af_new = Permutation._af_new + +############################################ +# +# Utilities for computational group theory +# +############################################ + + +def _base_ordering(base, degree): + r""" + Order `\{0, 1, \dots, n-1\}` so that base points come first and in order. + + Parameters + ========== + + base : the base + degree : the degree of the associated permutation group + + Returns + ======= + + A list ``base_ordering`` such that ``base_ordering[point]`` is the + number of ``point`` in the ordering. + + Examples + ======== + + >>> from sympy.combinatorics import SymmetricGroup + >>> from sympy.combinatorics.util import _base_ordering + >>> S = SymmetricGroup(4) + >>> S.schreier_sims() + >>> _base_ordering(S.base, S.degree) + [0, 1, 2, 3] + + Notes + ===== + + This is used in backtrack searches, when we define a relation `\ll` on + the underlying set for a permutation group of degree `n`, + `\{0, 1, \dots, n-1\}`, so that if `(b_1, b_2, \dots, b_k)` is a base we + have `b_i \ll b_j` whenever `i>> from sympy.combinatorics.util import _check_cycles_alt_sym + >>> from sympy.combinatorics import Permutation + >>> a = Permutation([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [11, 12]]) + >>> _check_cycles_alt_sym(a) + False + >>> b = Permutation([[0, 1, 2, 3, 4, 5, 6], [7, 8, 9, 10]]) + >>> _check_cycles_alt_sym(b) + True + + See Also + ======== + + sympy.combinatorics.perm_groups.PermutationGroup.is_alt_sym + + """ + n = perm.size + af = perm.array_form + current_len = 0 + total_len = 0 + used = set() + for i in range(n//2): + if i not in used and i < n//2 - total_len: + current_len = 1 + used.add(i) + j = i + while af[j] != i: + current_len += 1 + j = af[j] + used.add(j) + total_len += current_len + if current_len > n//2 and current_len < n - 2 and isprime(current_len): + return True + return False + + +def _distribute_gens_by_base(base, gens): + r""" + Distribute the group elements ``gens`` by membership in basic stabilizers. + + Explanation + =========== + + Notice that for a base `(b_1, b_2, \dots, b_k)`, the basic stabilizers + are defined as `G^{(i)} = G_{b_1, \dots, b_{i-1}}` for + `i \in\{1, 2, \dots, k\}`. + + Parameters + ========== + + base : a sequence of points in `\{0, 1, \dots, n-1\}` + gens : a list of elements of a permutation group of degree `n`. + + Returns + ======= + list + List of length `k`, where `k` is the length of *base*. The `i`-th entry + contains those elements in *gens* which fix the first `i` elements of + *base* (so that the `0`-th entry is equal to *gens* itself). If no + element fixes the first `i` elements of *base*, the `i`-th element is + set to a list containing the identity element. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> from sympy.combinatorics.util import _distribute_gens_by_base + >>> D = DihedralGroup(3) + >>> D.schreier_sims() + >>> D.strong_gens + [(0 1 2), (0 2), (1 2)] + >>> D.base + [0, 1] + >>> _distribute_gens_by_base(D.base, D.strong_gens) + [[(0 1 2), (0 2), (1 2)], + [(1 2)]] + + See Also + ======== + + _strong_gens_from_distr, _orbits_transversals_from_bsgs, + _handle_precomputed_bsgs + + """ + base_len = len(base) + degree = gens[0].size + stabs = [[] for _ in range(base_len)] + max_stab_index = 0 + for gen in gens: + j = 0 + while j < base_len - 1 and gen._array_form[base[j]] == base[j]: + j += 1 + if j > max_stab_index: + max_stab_index = j + for k in range(j + 1): + stabs[k].append(gen) + for i in range(max_stab_index + 1, base_len): + stabs[i].append(_af_new(list(range(degree)))) + return stabs + + +def _handle_precomputed_bsgs(base, strong_gens, transversals=None, + basic_orbits=None, strong_gens_distr=None): + """ + Calculate BSGS-related structures from those present. + + Explanation + =========== + + The base and strong generating set must be provided; if any of the + transversals, basic orbits or distributed strong generators are not + provided, they will be calculated from the base and strong generating set. + + Parameters + ========== + + base : the base + strong_gens : the strong generators + transversals : basic transversals + basic_orbits : basic orbits + strong_gens_distr : strong generators distributed by membership in basic stabilizers + + Returns + ======= + + (transversals, basic_orbits, strong_gens_distr) + where *transversals* are the basic transversals, *basic_orbits* are the + basic orbits, and *strong_gens_distr* are the strong generators distributed + by membership in basic stabilizers. + + Examples + ======== + + >>> from sympy.combinatorics.named_groups import DihedralGroup + >>> from sympy.combinatorics.util import _handle_precomputed_bsgs + >>> D = DihedralGroup(3) + >>> D.schreier_sims() + >>> _handle_precomputed_bsgs(D.base, D.strong_gens, + ... basic_orbits=D.basic_orbits) + ([{0: (2), 1: (0 1 2), 2: (0 2)}, {1: (2), 2: (1 2)}], [[0, 1, 2], [1, 2]], [[(0 1 2), (0 2), (1 2)], [(1 2)]]) + + See Also + ======== + + _orbits_transversals_from_bsgs, _distribute_gens_by_base + + """ + if strong_gens_distr is None: + strong_gens_distr = _distribute_gens_by_base(base, strong_gens) + if transversals is None: + if basic_orbits is None: + basic_orbits, transversals = \ + _orbits_transversals_from_bsgs(base, strong_gens_distr) + else: + transversals = \ + _orbits_transversals_from_bsgs(base, strong_gens_distr, + transversals_only=True) + else: + if basic_orbits is None: + base_len = len(base) + basic_orbits = [None]*base_len + for i in range(base_len): + basic_orbits[i] = list(transversals[i].keys()) + return transversals, basic_orbits, strong_gens_distr + + +def _orbits_transversals_from_bsgs(base, strong_gens_distr, + transversals_only=False, slp=False): + """ + Compute basic orbits and transversals from a base and strong generating set. + + Explanation + =========== + + The generators are provided as distributed across the basic stabilizers. + If the optional argument ``transversals_only`` is set to True, only the + transversals are returned. + + Parameters + ========== + + base : The base. + strong_gens_distr : Strong generators distributed by membership in basic stabilizers. + transversals_only : bool, default: False + A flag switching between returning only the + transversals and both orbits and transversals. + slp : bool, default: False + If ``True``, return a list of dictionaries containing the + generator presentations of the elements of the transversals, + i.e. the list of indices of generators from ``strong_gens_distr[i]`` + such that their product is the relevant transversal element. + + Examples + ======== + + >>> from sympy.combinatorics import SymmetricGroup + >>> from sympy.combinatorics.util import _distribute_gens_by_base + >>> S = SymmetricGroup(3) + >>> S.schreier_sims() + >>> strong_gens_distr = _distribute_gens_by_base(S.base, S.strong_gens) + >>> (S.base, strong_gens_distr) + ([0, 1], [[(0 1 2), (2)(0 1), (1 2)], [(1 2)]]) + + See Also + ======== + + _distribute_gens_by_base, _handle_precomputed_bsgs + + """ + from sympy.combinatorics.perm_groups import _orbit_transversal + base_len = len(base) + degree = strong_gens_distr[0][0].size + transversals = [None]*base_len + slps = [None]*base_len + if transversals_only is False: + basic_orbits = [None]*base_len + for i in range(base_len): + transversals[i], slps[i] = _orbit_transversal(degree, strong_gens_distr[i], + base[i], pairs=True, slp=True) + transversals[i] = dict(transversals[i]) + if transversals_only is False: + basic_orbits[i] = list(transversals[i].keys()) + if transversals_only: + return transversals + else: + if not slp: + return basic_orbits, transversals + return basic_orbits, transversals, slps + + +def _remove_gens(base, strong_gens, basic_orbits=None, strong_gens_distr=None): + """ + Remove redundant generators from a strong generating set. + + Parameters + ========== + + base : a base + strong_gens : a strong generating set relative to *base* + basic_orbits : basic orbits + strong_gens_distr : strong generators distributed by membership in basic stabilizers + + Returns + ======= + + A strong generating set with respect to ``base`` which is a subset of + ``strong_gens``. + + Examples + ======== + + >>> from sympy.combinatorics import SymmetricGroup + >>> from sympy.combinatorics.util import _remove_gens + >>> from sympy.combinatorics.testutil import _verify_bsgs + >>> S = SymmetricGroup(15) + >>> base, strong_gens = S.schreier_sims_incremental() + >>> new_gens = _remove_gens(base, strong_gens) + >>> len(new_gens) + 14 + >>> _verify_bsgs(S, base, new_gens) + True + + Notes + ===== + + This procedure is outlined in [1],p.95. + + References + ========== + + .. [1] Holt, D., Eick, B., O'Brien, E. + "Handbook of computational group theory" + + """ + from sympy.combinatorics.perm_groups import _orbit + base_len = len(base) + degree = strong_gens[0].size + if strong_gens_distr is None: + strong_gens_distr = _distribute_gens_by_base(base, strong_gens) + if basic_orbits is None: + basic_orbits = [] + for i in range(base_len): + basic_orbit = _orbit(degree, strong_gens_distr[i], base[i]) + basic_orbits.append(basic_orbit) + strong_gens_distr.append([]) + res = strong_gens[:] + for i in range(base_len - 1, -1, -1): + gens_copy = strong_gens_distr[i][:] + for gen in strong_gens_distr[i]: + if gen not in strong_gens_distr[i + 1]: + temp_gens = gens_copy[:] + temp_gens.remove(gen) + if temp_gens == []: + continue + temp_orbit = _orbit(degree, temp_gens, base[i]) + if temp_orbit == basic_orbits[i]: + gens_copy.remove(gen) + res.remove(gen) + return res + + +def _strip(g, base, orbits, transversals): + """ + Attempt to decompose a permutation using a (possibly partial) BSGS + structure. + + Explanation + =========== + + This is done by treating the sequence ``base`` as an actual base, and + the orbits ``orbits`` and transversals ``transversals`` as basic orbits and + transversals relative to it. + + This process is called "sifting". A sift is unsuccessful when a certain + orbit element is not found or when after the sift the decomposition + does not end with the identity element. + + The argument ``transversals`` is a list of dictionaries that provides + transversal elements for the orbits ``orbits``. + + Parameters + ========== + + g : permutation to be decomposed + base : sequence of points + orbits : list + A list in which the ``i``-th entry is an orbit of ``base[i]`` + under some subgroup of the pointwise stabilizer of ` + `base[0], base[1], ..., base[i - 1]``. The groups themselves are implicit + in this function since the only information we need is encoded in the orbits + and transversals + transversals : list + A list of orbit transversals associated with the orbits *orbits*. + + Examples + ======== + + >>> from sympy.combinatorics import Permutation, SymmetricGroup + >>> from sympy.combinatorics.util import _strip + >>> S = SymmetricGroup(5) + >>> S.schreier_sims() + >>> g = Permutation([0, 2, 3, 1, 4]) + >>> _strip(g, S.base, S.basic_orbits, S.basic_transversals) + ((4), 5) + + Notes + ===== + + The algorithm is described in [1],pp.89-90. The reason for returning + both the current state of the element being decomposed and the level + at which the sifting ends is that they provide important information for + the randomized version of the Schreier-Sims algorithm. + + References + ========== + + .. [1] Holt, D., Eick, B., O'Brien, E."Handbook of computational group theory" + + See Also + ======== + + sympy.combinatorics.perm_groups.PermutationGroup.schreier_sims + sympy.combinatorics.perm_groups.PermutationGroup.schreier_sims_random + + """ + h = g._array_form + base_len = len(base) + for i in range(base_len): + beta = h[base[i]] + if beta == base[i]: + continue + if beta not in orbits[i]: + return _af_new(h), i + 1 + u = transversals[i][beta]._array_form + h = _af_rmul(_af_invert(u), h) + return _af_new(h), base_len + 1 + + +def _strip_af(h, base, orbits, transversals, j, slp=[], slps={}): + """ + optimized _strip, with h, transversals and result in array form + if the stripped elements is the identity, it returns False, base_len + 1 + + j h[base[i]] == base[i] for i <= j + + """ + base_len = len(base) + for i in range(j+1, base_len): + beta = h[base[i]] + if beta == base[i]: + continue + if beta not in orbits[i]: + if not slp: + return h, i + 1 + return h, i + 1, slp + u = transversals[i][beta] + if h == u: + if not slp: + return False, base_len + 1 + return False, base_len + 1, slp + h = _af_rmul(_af_invert(u), h) + if slp: + u_slp = slps[i][beta][:] + u_slp.reverse() + u_slp = [(i, (g,)) for g in u_slp] + slp = u_slp + slp + if not slp: + return h, base_len + 1 + return h, base_len + 1, slp + + +def _strong_gens_from_distr(strong_gens_distr): + """ + Retrieve strong generating set from generators of basic stabilizers. + + This is just the union of the generators of the first and second basic + stabilizers. + + Parameters + ========== + + strong_gens_distr : strong generators distributed by membership in basic stabilizers + + Examples + ======== + + >>> from sympy.combinatorics import SymmetricGroup + >>> from sympy.combinatorics.util import (_strong_gens_from_distr, + ... _distribute_gens_by_base) + >>> S = SymmetricGroup(3) + >>> S.schreier_sims() + >>> S.strong_gens + [(0 1 2), (2)(0 1), (1 2)] + >>> strong_gens_distr = _distribute_gens_by_base(S.base, S.strong_gens) + >>> _strong_gens_from_distr(strong_gens_distr) + [(0 1 2), (2)(0 1), (1 2)] + + See Also + ======== + + _distribute_gens_by_base + + """ + if len(strong_gens_distr) == 1: + return strong_gens_distr[0][:] + else: + result = strong_gens_distr[0] + for gen in strong_gens_distr[1]: + if gen not in result: + result.append(gen) + return result diff --git a/venv/lib/python3.11/site-packages/sympy/concrete/__init__.py b/venv/lib/python3.11/site-packages/sympy/concrete/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7048bd548cca04780479539339dd8cc49a21990f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/concrete/__init__.py @@ -0,0 +1,8 @@ +from .products import product, Product +from .summations import summation, Sum + +__all__ = [ + 'product', 'Product', + + 'summation', 'Sum', +] diff --git a/venv/lib/python3.11/site-packages/sympy/concrete/delta.py b/venv/lib/python3.11/site-packages/sympy/concrete/delta.py new file mode 100644 index 0000000000000000000000000000000000000000..0471d2b2891a66a1b97b7a3a37bbd31d7b173a81 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/concrete/delta.py @@ -0,0 +1,327 @@ +""" +This module implements sums and products containing the Kronecker Delta function. + +References +========== + +.. [1] https://mathworld.wolfram.com/KroneckerDelta.html + +""" +from .products import product +from .summations import Sum, summation +from sympy.core import Add, Mul, S, Dummy +from sympy.core.cache import cacheit +from sympy.core.sorting import default_sort_key +from sympy.functions import KroneckerDelta, Piecewise, piecewise_fold +from sympy.polys.polytools import factor +from sympy.sets.sets import Interval +from sympy.solvers.solvers import solve + + +@cacheit +def _expand_delta(expr, index): + """ + Expand the first Add containing a simple KroneckerDelta. + """ + if not expr.is_Mul: + return expr + delta = None + func = Add + terms = [S.One] + for h in expr.args: + if delta is None and h.is_Add and _has_simple_delta(h, index): + delta = True + func = h.func + terms = [terms[0]*t for t in h.args] + else: + terms = [t*h for t in terms] + return func(*terms) + + +@cacheit +def _extract_delta(expr, index): + """ + Extract a simple KroneckerDelta from the expression. + + Explanation + =========== + + Returns the tuple ``(delta, newexpr)`` where: + + - ``delta`` is a simple KroneckerDelta expression if one was found, + or ``None`` if no simple KroneckerDelta expression was found. + + - ``newexpr`` is a Mul containing the remaining terms; ``expr`` is + returned unchanged if no simple KroneckerDelta expression was found. + + Examples + ======== + + >>> from sympy import KroneckerDelta + >>> from sympy.concrete.delta import _extract_delta + >>> from sympy.abc import x, y, i, j, k + >>> _extract_delta(4*x*y*KroneckerDelta(i, j), i) + (KroneckerDelta(i, j), 4*x*y) + >>> _extract_delta(4*x*y*KroneckerDelta(i, j), k) + (None, 4*x*y*KroneckerDelta(i, j)) + + See Also + ======== + + sympy.functions.special.tensor_functions.KroneckerDelta + deltaproduct + deltasummation + """ + if not _has_simple_delta(expr, index): + return (None, expr) + if isinstance(expr, KroneckerDelta): + return (expr, S.One) + if not expr.is_Mul: + raise ValueError("Incorrect expr") + delta = None + terms = [] + + for arg in expr.args: + if delta is None and _is_simple_delta(arg, index): + delta = arg + else: + terms.append(arg) + return (delta, expr.func(*terms)) + + +@cacheit +def _has_simple_delta(expr, index): + """ + Returns True if ``expr`` is an expression that contains a KroneckerDelta + that is simple in the index ``index``, meaning that this KroneckerDelta + is nonzero for a single value of the index ``index``. + """ + if expr.has(KroneckerDelta): + if _is_simple_delta(expr, index): + return True + if expr.is_Add or expr.is_Mul: + return any(_has_simple_delta(arg, index) for arg in expr.args) + return False + + +@cacheit +def _is_simple_delta(delta, index): + """ + Returns True if ``delta`` is a KroneckerDelta and is nonzero for a single + value of the index ``index``. + """ + if isinstance(delta, KroneckerDelta) and delta.has(index): + p = (delta.args[0] - delta.args[1]).as_poly(index) + if p: + return p.degree() == 1 + return False + + +@cacheit +def _remove_multiple_delta(expr): + """ + Evaluate products of KroneckerDelta's. + """ + if expr.is_Add: + return expr.func(*list(map(_remove_multiple_delta, expr.args))) + if not expr.is_Mul: + return expr + eqs = [] + newargs = [] + for arg in expr.args: + if isinstance(arg, KroneckerDelta): + eqs.append(arg.args[0] - arg.args[1]) + else: + newargs.append(arg) + if not eqs: + return expr + solns = solve(eqs, dict=True) + if len(solns) == 0: + return S.Zero + elif len(solns) == 1: + newargs += [KroneckerDelta(k, v) for k, v in solns[0].items()] + expr2 = expr.func(*newargs) + if expr != expr2: + return _remove_multiple_delta(expr2) + return expr + + +@cacheit +def _simplify_delta(expr): + """ + Rewrite a KroneckerDelta's indices in its simplest form. + """ + if isinstance(expr, KroneckerDelta): + try: + slns = solve(expr.args[0] - expr.args[1], dict=True) + if slns and len(slns) == 1: + return Mul(*[KroneckerDelta(*(key, value)) + for key, value in slns[0].items()]) + except NotImplementedError: + pass + return expr + + +@cacheit +def deltaproduct(f, limit): + """ + Handle products containing a KroneckerDelta. + + See Also + ======== + + deltasummation + sympy.functions.special.tensor_functions.KroneckerDelta + sympy.concrete.products.product + """ + if ((limit[2] - limit[1]) < 0) == True: + return S.One + + if not f.has(KroneckerDelta): + return product(f, limit) + + if f.is_Add: + # Identify the term in the Add that has a simple KroneckerDelta + delta = None + terms = [] + for arg in sorted(f.args, key=default_sort_key): + if delta is None and _has_simple_delta(arg, limit[0]): + delta = arg + else: + terms.append(arg) + newexpr = f.func(*terms) + k = Dummy("kprime", integer=True) + if isinstance(limit[1], int) and isinstance(limit[2], int): + result = deltaproduct(newexpr, limit) + sum(deltaproduct(newexpr, (limit[0], limit[1], ik - 1)) * + delta.subs(limit[0], ik) * + deltaproduct(newexpr, (limit[0], ik + 1, limit[2])) for ik in range(int(limit[1]), int(limit[2] + 1)) + ) + else: + result = deltaproduct(newexpr, limit) + deltasummation( + deltaproduct(newexpr, (limit[0], limit[1], k - 1)) * + delta.subs(limit[0], k) * + deltaproduct(newexpr, (limit[0], k + 1, limit[2])), + (k, limit[1], limit[2]), + no_piecewise=_has_simple_delta(newexpr, limit[0]) + ) + return _remove_multiple_delta(result) + + delta, _ = _extract_delta(f, limit[0]) + + if not delta: + g = _expand_delta(f, limit[0]) + if f != g: + try: + return factor(deltaproduct(g, limit)) + except AssertionError: + return deltaproduct(g, limit) + return product(f, limit) + + return _remove_multiple_delta(f.subs(limit[0], limit[1])*KroneckerDelta(limit[2], limit[1])) + \ + S.One*_simplify_delta(KroneckerDelta(limit[2], limit[1] - 1)) + + +@cacheit +def deltasummation(f, limit, no_piecewise=False): + """ + Handle summations containing a KroneckerDelta. + + Explanation + =========== + + The idea for summation is the following: + + - If we are dealing with a KroneckerDelta expression, i.e. KroneckerDelta(g(x), j), + we try to simplify it. + + If we could simplify it, then we sum the resulting expression. + We already know we can sum a simplified expression, because only + simple KroneckerDelta expressions are involved. + + If we could not simplify it, there are two cases: + + 1) The expression is a simple expression: we return the summation, + taking care if we are dealing with a Derivative or with a proper + KroneckerDelta. + + 2) The expression is not simple (i.e. KroneckerDelta(cos(x))): we can do + nothing at all. + + - If the expr is a multiplication expr having a KroneckerDelta term: + + First we expand it. + + If the expansion did work, then we try to sum the expansion. + + If not, we try to extract a simple KroneckerDelta term, then we have two + cases: + + 1) We have a simple KroneckerDelta term, so we return the summation. + + 2) We did not have a simple term, but we do have an expression with + simplified KroneckerDelta terms, so we sum this expression. + + Examples + ======== + + >>> from sympy import oo, symbols + >>> from sympy.abc import k + >>> i, j = symbols('i, j', integer=True, finite=True) + >>> from sympy.concrete.delta import deltasummation + >>> from sympy import KroneckerDelta + >>> deltasummation(KroneckerDelta(i, k), (k, -oo, oo)) + 1 + >>> deltasummation(KroneckerDelta(i, k), (k, 0, oo)) + Piecewise((1, i >= 0), (0, True)) + >>> deltasummation(KroneckerDelta(i, k), (k, 1, 3)) + Piecewise((1, (i >= 1) & (i <= 3)), (0, True)) + >>> deltasummation(k*KroneckerDelta(i, j)*KroneckerDelta(j, k), (k, -oo, oo)) + j*KroneckerDelta(i, j) + >>> deltasummation(j*KroneckerDelta(i, j), (j, -oo, oo)) + i + >>> deltasummation(i*KroneckerDelta(i, j), (i, -oo, oo)) + j + + See Also + ======== + + deltaproduct + sympy.functions.special.tensor_functions.KroneckerDelta + sympy.concrete.sums.summation + """ + if ((limit[2] - limit[1]) < 0) == True: + return S.Zero + + if not f.has(KroneckerDelta): + return summation(f, limit) + + x = limit[0] + + g = _expand_delta(f, x) + if g.is_Add: + return piecewise_fold( + g.func(*[deltasummation(h, limit, no_piecewise) for h in g.args])) + + # try to extract a simple KroneckerDelta term + delta, expr = _extract_delta(g, x) + + if (delta is not None) and (delta.delta_range is not None): + dinf, dsup = delta.delta_range + if (limit[1] - dinf <= 0) == True and (limit[2] - dsup >= 0) == True: + no_piecewise = True + + if not delta: + return summation(f, limit) + + solns = solve(delta.args[0] - delta.args[1], x) + if len(solns) == 0: + return S.Zero + elif len(solns) != 1: + return Sum(f, limit) + value = solns[0] + if no_piecewise: + return expr.subs(x, value) + return Piecewise( + (expr.subs(x, value), Interval(*limit[1:3]).as_relational(value)), + (S.Zero, True) + ) diff --git a/venv/lib/python3.11/site-packages/sympy/concrete/expr_with_intlimits.py b/venv/lib/python3.11/site-packages/sympy/concrete/expr_with_intlimits.py new file mode 100644 index 0000000000000000000000000000000000000000..8e109913cdb2f3018096972b14651b990f4b985e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/concrete/expr_with_intlimits.py @@ -0,0 +1,354 @@ +from sympy.concrete.expr_with_limits import ExprWithLimits +from sympy.core.singleton import S +from sympy.core.relational import Eq + +class ReorderError(NotImplementedError): + """ + Exception raised when trying to reorder dependent limits. + """ + def __init__(self, expr, msg): + super().__init__( + "%s could not be reordered: %s." % (expr, msg)) + +class ExprWithIntLimits(ExprWithLimits): + """ + Superclass for Product and Sum. + + See Also + ======== + + sympy.concrete.expr_with_limits.ExprWithLimits + sympy.concrete.products.Product + sympy.concrete.summations.Sum + """ + __slots__ = () + + def change_index(self, var, trafo, newvar=None): + r""" + Change index of a Sum or Product. + + Perform a linear transformation `x \mapsto a x + b` on the index variable + `x`. For `a` the only values allowed are `\pm 1`. A new variable to be used + after the change of index can also be specified. + + Explanation + =========== + + ``change_index(expr, var, trafo, newvar=None)`` where ``var`` specifies the + index variable `x` to transform. The transformation ``trafo`` must be linear + and given in terms of ``var``. If the optional argument ``newvar`` is + provided then ``var`` gets replaced by ``newvar`` in the final expression. + + Examples + ======== + + >>> from sympy import Sum, Product, simplify + >>> from sympy.abc import x, y, a, b, c, d, u, v, i, j, k, l + + >>> S = Sum(x, (x, a, b)) + >>> S.doit() + -a**2/2 + a/2 + b**2/2 + b/2 + + >>> Sn = S.change_index(x, x + 1, y) + >>> Sn + Sum(y - 1, (y, a + 1, b + 1)) + >>> Sn.doit() + -a**2/2 + a/2 + b**2/2 + b/2 + + >>> Sn = S.change_index(x, -x, y) + >>> Sn + Sum(-y, (y, -b, -a)) + >>> Sn.doit() + -a**2/2 + a/2 + b**2/2 + b/2 + + >>> Sn = S.change_index(x, x+u) + >>> Sn + Sum(-u + x, (x, a + u, b + u)) + >>> Sn.doit() + -a**2/2 - a*u + a/2 + b**2/2 + b*u + b/2 - u*(-a + b + 1) + u + >>> simplify(Sn.doit()) + -a**2/2 + a/2 + b**2/2 + b/2 + + >>> Sn = S.change_index(x, -x - u, y) + >>> Sn + Sum(-u - y, (y, -b - u, -a - u)) + >>> Sn.doit() + -a**2/2 - a*u + a/2 + b**2/2 + b*u + b/2 - u*(-a + b + 1) + u + >>> simplify(Sn.doit()) + -a**2/2 + a/2 + b**2/2 + b/2 + + >>> P = Product(i*j**2, (i, a, b), (j, c, d)) + >>> P + Product(i*j**2, (i, a, b), (j, c, d)) + >>> P2 = P.change_index(i, i+3, k) + >>> P2 + Product(j**2*(k - 3), (k, a + 3, b + 3), (j, c, d)) + >>> P3 = P2.change_index(j, -j, l) + >>> P3 + Product(l**2*(k - 3), (k, a + 3, b + 3), (l, -d, -c)) + + When dealing with symbols only, we can make a + general linear transformation: + + >>> Sn = S.change_index(x, u*x+v, y) + >>> Sn + Sum((-v + y)/u, (y, b*u + v, a*u + v)) + >>> Sn.doit() + -v*(a*u - b*u + 1)/u + (a**2*u**2/2 + a*u*v + a*u/2 - b**2*u**2/2 - b*u*v + b*u/2 + v)/u + >>> simplify(Sn.doit()) + a**2*u/2 + a/2 - b**2*u/2 + b/2 + + However, the last result can be inconsistent with usual + summation where the index increment is always 1. This is + obvious as we get back the original value only for ``u`` + equal +1 or -1. + + See Also + ======== + + sympy.concrete.expr_with_intlimits.ExprWithIntLimits.index, + reorder_limit, + sympy.concrete.expr_with_intlimits.ExprWithIntLimits.reorder, + sympy.concrete.summations.Sum.reverse_order, + sympy.concrete.products.Product.reverse_order + """ + if newvar is None: + newvar = var + + limits = [] + for limit in self.limits: + if limit[0] == var: + p = trafo.as_poly(var) + if p.degree() != 1: + raise ValueError("Index transformation is not linear") + alpha = p.coeff_monomial(var) + beta = p.coeff_monomial(S.One) + if alpha.is_number: + if alpha == S.One: + limits.append((newvar, alpha*limit[1] + beta, alpha*limit[2] + beta)) + elif alpha == S.NegativeOne: + limits.append((newvar, alpha*limit[2] + beta, alpha*limit[1] + beta)) + else: + raise ValueError("Linear transformation results in non-linear summation stepsize") + else: + # Note that the case of alpha being symbolic can give issues if alpha < 0. + limits.append((newvar, alpha*limit[2] + beta, alpha*limit[1] + beta)) + else: + limits.append(limit) + + function = self.function.subs(var, (var - beta)/alpha) + function = function.subs(var, newvar) + + return self.func(function, *limits) + + + def index(expr, x): + """ + Return the index of a dummy variable in the list of limits. + + Explanation + =========== + + ``index(expr, x)`` returns the index of the dummy variable ``x`` in the + limits of ``expr``. Note that we start counting with 0 at the inner-most + limits tuple. + + Examples + ======== + + >>> from sympy.abc import x, y, a, b, c, d + >>> from sympy import Sum, Product + >>> Sum(x*y, (x, a, b), (y, c, d)).index(x) + 0 + >>> Sum(x*y, (x, a, b), (y, c, d)).index(y) + 1 + >>> Product(x*y, (x, a, b), (y, c, d)).index(x) + 0 + >>> Product(x*y, (x, a, b), (y, c, d)).index(y) + 1 + + See Also + ======== + + reorder_limit, reorder, sympy.concrete.summations.Sum.reverse_order, + sympy.concrete.products.Product.reverse_order + """ + variables = [limit[0] for limit in expr.limits] + + if variables.count(x) != 1: + raise ValueError(expr, "Number of instances of variable not equal to one") + else: + return variables.index(x) + + def reorder(expr, *arg): + """ + Reorder limits in a expression containing a Sum or a Product. + + Explanation + =========== + + ``expr.reorder(*arg)`` reorders the limits in the expression ``expr`` + according to the list of tuples given by ``arg``. These tuples can + contain numerical indices or index variable names or involve both. + + Examples + ======== + + >>> from sympy import Sum, Product + >>> from sympy.abc import x, y, z, a, b, c, d, e, f + + >>> Sum(x*y, (x, a, b), (y, c, d)).reorder((x, y)) + Sum(x*y, (y, c, d), (x, a, b)) + + >>> Sum(x*y*z, (x, a, b), (y, c, d), (z, e, f)).reorder((x, y), (x, z), (y, z)) + Sum(x*y*z, (z, e, f), (y, c, d), (x, a, b)) + + >>> P = Product(x*y*z, (x, a, b), (y, c, d), (z, e, f)) + >>> P.reorder((x, y), (x, z), (y, z)) + Product(x*y*z, (z, e, f), (y, c, d), (x, a, b)) + + We can also select the index variables by counting them, starting + with the inner-most one: + + >>> Sum(x**2, (x, a, b), (x, c, d)).reorder((0, 1)) + Sum(x**2, (x, c, d), (x, a, b)) + + And of course we can mix both schemes: + + >>> Sum(x*y, (x, a, b), (y, c, d)).reorder((y, x)) + Sum(x*y, (y, c, d), (x, a, b)) + >>> Sum(x*y, (x, a, b), (y, c, d)).reorder((y, 0)) + Sum(x*y, (y, c, d), (x, a, b)) + + See Also + ======== + + reorder_limit, index, sympy.concrete.summations.Sum.reverse_order, + sympy.concrete.products.Product.reverse_order + """ + new_expr = expr + + for r in arg: + if len(r) != 2: + raise ValueError(r, "Invalid number of arguments") + + index1 = r[0] + index2 = r[1] + + if not isinstance(r[0], int): + index1 = expr.index(r[0]) + if not isinstance(r[1], int): + index2 = expr.index(r[1]) + + new_expr = new_expr.reorder_limit(index1, index2) + + return new_expr + + + def reorder_limit(expr, x, y): + """ + Interchange two limit tuples of a Sum or Product expression. + + Explanation + =========== + + ``expr.reorder_limit(x, y)`` interchanges two limit tuples. The + arguments ``x`` and ``y`` are integers corresponding to the index + variables of the two limits which are to be interchanged. The + expression ``expr`` has to be either a Sum or a Product. + + Examples + ======== + + >>> from sympy.abc import x, y, z, a, b, c, d, e, f + >>> from sympy import Sum, Product + + >>> Sum(x*y*z, (x, a, b), (y, c, d), (z, e, f)).reorder_limit(0, 2) + Sum(x*y*z, (z, e, f), (y, c, d), (x, a, b)) + >>> Sum(x**2, (x, a, b), (x, c, d)).reorder_limit(1, 0) + Sum(x**2, (x, c, d), (x, a, b)) + + >>> Product(x*y*z, (x, a, b), (y, c, d), (z, e, f)).reorder_limit(0, 2) + Product(x*y*z, (z, e, f), (y, c, d), (x, a, b)) + + See Also + ======== + + index, reorder, sympy.concrete.summations.Sum.reverse_order, + sympy.concrete.products.Product.reverse_order + """ + var = {limit[0] for limit in expr.limits} + limit_x = expr.limits[x] + limit_y = expr.limits[y] + + if (len(set(limit_x[1].free_symbols).intersection(var)) == 0 and + len(set(limit_x[2].free_symbols).intersection(var)) == 0 and + len(set(limit_y[1].free_symbols).intersection(var)) == 0 and + len(set(limit_y[2].free_symbols).intersection(var)) == 0): + + limits = [] + for i, limit in enumerate(expr.limits): + if i == x: + limits.append(limit_y) + elif i == y: + limits.append(limit_x) + else: + limits.append(limit) + + return type(expr)(expr.function, *limits) + else: + raise ReorderError(expr, "could not interchange the two limits specified") + + @property + def has_empty_sequence(self): + """ + Returns True if the Sum or Product is computed for an empty sequence. + + Examples + ======== + + >>> from sympy import Sum, Product, Symbol + >>> m = Symbol('m') + >>> Sum(m, (m, 1, 0)).has_empty_sequence + True + + >>> Sum(m, (m, 1, 1)).has_empty_sequence + False + + >>> M = Symbol('M', integer=True, positive=True) + >>> Product(m, (m, 1, M)).has_empty_sequence + False + + >>> Product(m, (m, 2, M)).has_empty_sequence + + >>> Product(m, (m, M + 1, M)).has_empty_sequence + True + + >>> N = Symbol('N', integer=True, positive=True) + >>> Sum(m, (m, N, M)).has_empty_sequence + + >>> N = Symbol('N', integer=True, negative=True) + >>> Sum(m, (m, N, M)).has_empty_sequence + False + + See Also + ======== + + has_reversed_limits + has_finite_limits + + """ + ret_None = False + for lim in self.limits: + dif = lim[1] - lim[2] + eq = Eq(dif, 1) + if eq == True: + return True + elif eq == False: + continue + else: + ret_None = True + + if ret_None: + return None + return False diff --git a/venv/lib/python3.11/site-packages/sympy/concrete/expr_with_limits.py b/venv/lib/python3.11/site-packages/sympy/concrete/expr_with_limits.py new file mode 100644 index 0000000000000000000000000000000000000000..034e6ab0a7663525632abe0224fbac973c505c08 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/concrete/expr_with_limits.py @@ -0,0 +1,603 @@ +from sympy.core.add import Add +from sympy.core.containers import Tuple +from sympy.core.expr import Expr +from sympy.core.function import AppliedUndef, UndefinedFunction +from sympy.core.mul import Mul +from sympy.core.relational import Equality, Relational +from sympy.core.singleton import S +from sympy.core.symbol import Symbol, Dummy +from sympy.core.sympify import sympify +from sympy.functions.elementary.piecewise import (piecewise_fold, + Piecewise) +from sympy.logic.boolalg import BooleanFunction +from sympy.matrices.matrixbase import MatrixBase +from sympy.sets.sets import Interval, Set +from sympy.sets.fancysets import Range +from sympy.tensor.indexed import Idx +from sympy.utilities import flatten +from sympy.utilities.iterables import sift, is_sequence +from sympy.utilities.exceptions import sympy_deprecation_warning + + +def _common_new(cls, function, *symbols, discrete, **assumptions): + """Return either a special return value or the tuple, + (function, limits, orientation). This code is common to + both ExprWithLimits and AddWithLimits.""" + function = sympify(function) + + if isinstance(function, Equality): + # This transforms e.g. Integral(Eq(x, y)) to Eq(Integral(x), Integral(y)) + # but that is only valid for definite integrals. + limits, orientation = _process_limits(*symbols, discrete=discrete) + if not (limits and all(len(limit) == 3 for limit in limits)): + sympy_deprecation_warning( + """ + Creating a indefinite integral with an Eq() argument is + deprecated. + + This is because indefinite integrals do not preserve equality + due to the arbitrary constants. If you want an equality of + indefinite integrals, use Eq(Integral(a, x), Integral(b, x)) + explicitly. + """, + deprecated_since_version="1.6", + active_deprecations_target="deprecated-indefinite-integral-eq", + stacklevel=5, + ) + + lhs = function.lhs + rhs = function.rhs + return Equality(cls(lhs, *symbols, **assumptions), \ + cls(rhs, *symbols, **assumptions)) + + if function is S.NaN: + return S.NaN + + if symbols: + limits, orientation = _process_limits(*symbols, discrete=discrete) + for i, li in enumerate(limits): + if len(li) == 4: + function = function.subs(li[0], li[-1]) + limits[i] = Tuple(*li[:-1]) + else: + # symbol not provided -- we can still try to compute a general form + free = function.free_symbols + if len(free) != 1: + raise ValueError( + "specify dummy variables for %s" % function) + limits, orientation = [Tuple(s) for s in free], 1 + + # denest any nested calls + while cls == type(function): + limits = list(function.limits) + limits + function = function.function + + # Any embedded piecewise functions need to be brought out to the + # top level. We only fold Piecewise that contain the integration + # variable. + reps = {} + symbols_of_integration = {i[0] for i in limits} + for p in function.atoms(Piecewise): + if not p.has(*symbols_of_integration): + reps[p] = Dummy() + # mask off those that don't + function = function.xreplace(reps) + # do the fold + function = piecewise_fold(function) + # remove the masking + function = function.xreplace({v: k for k, v in reps.items()}) + + return function, limits, orientation + + +def _process_limits(*symbols, discrete=None): + """Process the list of symbols and convert them to canonical limits, + storing them as Tuple(symbol, lower, upper). The orientation of + the function is also returned when the upper limit is missing + so (x, 1, None) becomes (x, None, 1) and the orientation is changed. + In the case that a limit is specified as (symbol, Range), a list of + length 4 may be returned if a change of variables is needed; the + expression that should replace the symbol in the expression is + the fourth element in the list. + """ + limits = [] + orientation = 1 + if discrete is None: + err_msg = 'discrete must be True or False' + elif discrete: + err_msg = 'use Range, not Interval or Relational' + else: + err_msg = 'use Interval or Relational, not Range' + for V in symbols: + if isinstance(V, (Relational, BooleanFunction)): + if discrete: + raise TypeError(err_msg) + variable = V.atoms(Symbol).pop() + V = (variable, V.as_set()) + elif isinstance(V, Symbol) or getattr(V, '_diff_wrt', False): + if isinstance(V, Idx): + if V.lower is None or V.upper is None: + limits.append(Tuple(V)) + else: + limits.append(Tuple(V, V.lower, V.upper)) + else: + limits.append(Tuple(V)) + continue + if is_sequence(V) and not isinstance(V, Set): + if len(V) == 2 and isinstance(V[1], Set): + V = list(V) + if isinstance(V[1], Interval): # includes Reals + if discrete: + raise TypeError(err_msg) + V[1:] = V[1].inf, V[1].sup + elif isinstance(V[1], Range): + if not discrete: + raise TypeError(err_msg) + lo = V[1].inf + hi = V[1].sup + dx = abs(V[1].step) # direction doesn't matter + if dx == 1: + V[1:] = [lo, hi] + else: + if lo is not S.NegativeInfinity: + V = [V[0]] + [0, (hi - lo)//dx, dx*V[0] + lo] + else: + V = [V[0]] + [0, S.Infinity, -dx*V[0] + hi] + else: + # more complicated sets would require splitting, e.g. + # Union(Interval(1, 3), interval(6,10)) + raise NotImplementedError( + 'expecting Range' if discrete else + 'Relational or single Interval' ) + V = sympify(flatten(V)) # list of sympified elements/None + if isinstance(V[0], (Symbol, Idx)) or getattr(V[0], '_diff_wrt', False): + newsymbol = V[0] + if len(V) == 3: + # general case + if V[2] is None and V[1] is not None: + orientation *= -1 + V = [newsymbol] + [i for i in V[1:] if i is not None] + + lenV = len(V) + if not isinstance(newsymbol, Idx) or lenV == 3: + if lenV == 4: + limits.append(Tuple(*V)) + continue + if lenV == 3: + if isinstance(newsymbol, Idx): + # Idx represents an integer which may have + # specified values it can take on; if it is + # given such a value, an error is raised here + # if the summation would try to give it a larger + # or smaller value than permitted. None and Symbolic + # values will not raise an error. + lo, hi = newsymbol.lower, newsymbol.upper + try: + if lo is not None and not bool(V[1] >= lo): + raise ValueError("Summation will set Idx value too low.") + except TypeError: + pass + try: + if hi is not None and not bool(V[2] <= hi): + raise ValueError("Summation will set Idx value too high.") + except TypeError: + pass + limits.append(Tuple(*V)) + continue + if lenV == 1 or (lenV == 2 and V[1] is None): + limits.append(Tuple(newsymbol)) + continue + elif lenV == 2: + limits.append(Tuple(newsymbol, V[1])) + continue + + raise ValueError('Invalid limits given: %s' % str(symbols)) + + return limits, orientation + + +class ExprWithLimits(Expr): + __slots__ = ('is_commutative',) + + def __new__(cls, function, *symbols, **assumptions): + from sympy.concrete.products import Product + pre = _common_new(cls, function, *symbols, + discrete=issubclass(cls, Product), **assumptions) + if isinstance(pre, tuple): + function, limits, _ = pre + else: + return pre + + # limits must have upper and lower bounds; the indefinite form + # is not supported. This restriction does not apply to AddWithLimits + if any(len(l) != 3 or None in l for l in limits): + raise ValueError('ExprWithLimits requires values for lower and upper bounds.') + + obj = Expr.__new__(cls, **assumptions) + arglist = [function] + arglist.extend(limits) + obj._args = tuple(arglist) + obj.is_commutative = function.is_commutative # limits already checked + + return obj + + @property + def function(self): + """Return the function applied across limits. + + Examples + ======== + + >>> from sympy import Integral + >>> from sympy.abc import x + >>> Integral(x**2, (x,)).function + x**2 + + See Also + ======== + + limits, variables, free_symbols + """ + return self._args[0] + + @property + def kind(self): + return self.function.kind + + @property + def limits(self): + """Return the limits of expression. + + Examples + ======== + + >>> from sympy import Integral + >>> from sympy.abc import x, i + >>> Integral(x**i, (i, 1, 3)).limits + ((i, 1, 3),) + + See Also + ======== + + function, variables, free_symbols + """ + return self._args[1:] + + @property + def variables(self): + """Return a list of the limit variables. + + >>> from sympy import Sum + >>> from sympy.abc import x, i + >>> Sum(x**i, (i, 1, 3)).variables + [i] + + See Also + ======== + + function, limits, free_symbols + as_dummy : Rename dummy variables + sympy.integrals.integrals.Integral.transform : Perform mapping on the dummy variable + """ + return [l[0] for l in self.limits] + + @property + def bound_symbols(self): + """Return only variables that are dummy variables. + + Examples + ======== + + >>> from sympy import Integral + >>> from sympy.abc import x, i, j, k + >>> Integral(x**i, (i, 1, 3), (j, 2), k).bound_symbols + [i, j] + + See Also + ======== + + function, limits, free_symbols + as_dummy : Rename dummy variables + sympy.integrals.integrals.Integral.transform : Perform mapping on the dummy variable + """ + return [l[0] for l in self.limits if len(l) != 1] + + @property + def free_symbols(self): + """ + This method returns the symbols in the object, excluding those + that take on a specific value (i.e. the dummy symbols). + + Examples + ======== + + >>> from sympy import Sum + >>> from sympy.abc import x, y + >>> Sum(x, (x, y, 1)).free_symbols + {y} + """ + # don't test for any special values -- nominal free symbols + # should be returned, e.g. don't return set() if the + # function is zero -- treat it like an unevaluated expression. + function, limits = self.function, self.limits + # mask off non-symbol integration variables that have + # more than themself as a free symbol + reps = {i[0]: i[0] if i[0].free_symbols == {i[0]} else Dummy() + for i in self.limits} + function = function.xreplace(reps) + isyms = function.free_symbols + for xab in limits: + v = reps[xab[0]] + if len(xab) == 1: + isyms.add(v) + continue + # take out the target symbol + if v in isyms: + isyms.remove(v) + # add in the new symbols + for i in xab[1:]: + isyms.update(i.free_symbols) + reps = {v: k for k, v in reps.items()} + return {reps.get(_, _) for _ in isyms} + + @property + def is_number(self): + """Return True if the Sum has no free symbols, else False.""" + return not self.free_symbols + + def _eval_interval(self, x, a, b): + limits = [(i if i[0] != x else (x, a, b)) for i in self.limits] + integrand = self.function + return self.func(integrand, *limits) + + def _eval_subs(self, old, new): + """ + Perform substitutions over non-dummy variables + of an expression with limits. Also, can be used + to specify point-evaluation of an abstract antiderivative. + + Examples + ======== + + >>> from sympy import Sum, oo + >>> from sympy.abc import s, n + >>> Sum(1/n**s, (n, 1, oo)).subs(s, 2) + Sum(n**(-2), (n, 1, oo)) + + >>> from sympy import Integral + >>> from sympy.abc import x, a + >>> Integral(a*x**2, x).subs(x, 4) + Integral(a*x**2, (x, 4)) + + See Also + ======== + + variables : Lists the integration variables + transform : Perform mapping on the dummy variable for integrals + change_index : Perform mapping on the sum and product dummy variables + + """ + func, limits = self.function, list(self.limits) + + # If one of the expressions we are replacing is used as a func index + # one of two things happens. + # - the old variable first appears as a free variable + # so we perform all free substitutions before it becomes + # a func index. + # - the old variable first appears as a func index, in + # which case we ignore. See change_index. + + # Reorder limits to match standard mathematical practice for scoping + limits.reverse() + + if not isinstance(old, Symbol) or \ + old.free_symbols.intersection(self.free_symbols): + sub_into_func = True + for i, xab in enumerate(limits): + if 1 == len(xab) and old == xab[0]: + if new._diff_wrt: + xab = (new,) + else: + xab = (old, old) + limits[i] = Tuple(xab[0], *[l._subs(old, new) for l in xab[1:]]) + if len(xab[0].free_symbols.intersection(old.free_symbols)) != 0: + sub_into_func = False + break + if isinstance(old, (AppliedUndef, UndefinedFunction)): + sy2 = set(self.variables).intersection(set(new.atoms(Symbol))) + sy1 = set(self.variables).intersection(set(old.args)) + if not sy2.issubset(sy1): + raise ValueError( + "substitution cannot create dummy dependencies") + sub_into_func = True + if sub_into_func: + func = func.subs(old, new) + else: + # old is a Symbol and a dummy variable of some limit + for i, xab in enumerate(limits): + if len(xab) == 3: + limits[i] = Tuple(xab[0], *[l._subs(old, new) for l in xab[1:]]) + if old == xab[0]: + break + # simplify redundant limits (x, x) to (x, ) + for i, xab in enumerate(limits): + if len(xab) == 2 and (xab[0] - xab[1]).is_zero: + limits[i] = Tuple(xab[0], ) + + # Reorder limits back to representation-form + limits.reverse() + + return self.func(func, *limits) + + @property + def has_finite_limits(self): + """ + Returns True if the limits are known to be finite, either by the + explicit bounds, assumptions on the bounds, or assumptions on the + variables. False if known to be infinite, based on the bounds. + None if not enough information is available to determine. + + Examples + ======== + + >>> from sympy import Sum, Integral, Product, oo, Symbol + >>> x = Symbol('x') + >>> Sum(x, (x, 1, 8)).has_finite_limits + True + + >>> Integral(x, (x, 1, oo)).has_finite_limits + False + + >>> M = Symbol('M') + >>> Sum(x, (x, 1, M)).has_finite_limits + + >>> N = Symbol('N', integer=True) + >>> Product(x, (x, 1, N)).has_finite_limits + True + + See Also + ======== + + has_reversed_limits + + """ + + ret_None = False + for lim in self.limits: + if len(lim) == 3: + if any(l.is_infinite for l in lim[1:]): + # Any of the bounds are +/-oo + return False + elif any(l.is_infinite is None for l in lim[1:]): + # Maybe there are assumptions on the variable? + if lim[0].is_infinite is None: + ret_None = True + else: + if lim[0].is_infinite is None: + ret_None = True + + if ret_None: + return None + return True + + @property + def has_reversed_limits(self): + """ + Returns True if the limits are known to be in reversed order, either + by the explicit bounds, assumptions on the bounds, or assumptions on the + variables. False if known to be in normal order, based on the bounds. + None if not enough information is available to determine. + + Examples + ======== + + >>> from sympy import Sum, Integral, Product, oo, Symbol + >>> x = Symbol('x') + >>> Sum(x, (x, 8, 1)).has_reversed_limits + True + + >>> Sum(x, (x, 1, oo)).has_reversed_limits + False + + >>> M = Symbol('M') + >>> Integral(x, (x, 1, M)).has_reversed_limits + + >>> N = Symbol('N', integer=True, positive=True) + >>> Sum(x, (x, 1, N)).has_reversed_limits + False + + >>> Product(x, (x, 2, N)).has_reversed_limits + + >>> Product(x, (x, 2, N)).subs(N, N + 2).has_reversed_limits + False + + See Also + ======== + + sympy.concrete.expr_with_intlimits.ExprWithIntLimits.has_empty_sequence + + """ + ret_None = False + for lim in self.limits: + if len(lim) == 3: + var, a, b = lim + dif = b - a + if dif.is_extended_negative: + return True + elif dif.is_extended_nonnegative: + continue + else: + ret_None = True + else: + return None + if ret_None: + return None + return False + + +class AddWithLimits(ExprWithLimits): + r"""Represents unevaluated oriented additions. + Parent class for Integral and Sum. + """ + + __slots__ = () + + def __new__(cls, function, *symbols, **assumptions): + from sympy.concrete.summations import Sum + pre = _common_new(cls, function, *symbols, + discrete=issubclass(cls, Sum), **assumptions) + if isinstance(pre, tuple): + function, limits, orientation = pre + else: + return pre + + obj = Expr.__new__(cls, **assumptions) + arglist = [orientation*function] # orientation not used in ExprWithLimits + arglist.extend(limits) + obj._args = tuple(arglist) + obj.is_commutative = function.is_commutative # limits already checked + + return obj + + def _eval_adjoint(self): + if all(x.is_real for x in flatten(self.limits)): + return self.func(self.function.adjoint(), *self.limits) + return None + + def _eval_conjugate(self): + if all(x.is_real for x in flatten(self.limits)): + return self.func(self.function.conjugate(), *self.limits) + return None + + def _eval_transpose(self): + if all(x.is_real for x in flatten(self.limits)): + return self.func(self.function.transpose(), *self.limits) + return None + + def _eval_factor(self, **hints): + if 1 == len(self.limits): + summand = self.function.factor(**hints) + if summand.is_Mul: + out = sift(summand.args, lambda w: w.is_commutative \ + and not set(self.variables) & w.free_symbols) + return Mul(*out[True])*self.func(Mul(*out[False]), \ + *self.limits) + else: + summand = self.func(self.function, *self.limits[0:-1]).factor() + if not summand.has(self.variables[-1]): + return self.func(1, [self.limits[-1]]).doit()*summand + elif isinstance(summand, Mul): + return self.func(summand, self.limits[-1]).factor() + return self + + def _eval_expand_basic(self, **hints): + summand = self.function.expand(**hints) + force = hints.get('force', False) + if (summand.is_Add and (force or summand.is_commutative and + self.has_finite_limits is not False)): + return Add(*[self.func(i, *self.limits) for i in summand.args]) + elif isinstance(summand, MatrixBase): + return summand.applyfunc(lambda x: self.func(x, *self.limits)) + elif summand != self.function: + return self.func(summand, *self.limits) + return self diff --git a/venv/lib/python3.11/site-packages/sympy/concrete/gosper.py b/venv/lib/python3.11/site-packages/sympy/concrete/gosper.py new file mode 100644 index 0000000000000000000000000000000000000000..76eb20ef4f94bc6bff6324a1dcc09f90e05274b3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/concrete/gosper.py @@ -0,0 +1,222 @@ +"""Gosper's algorithm for hypergeometric summation. """ + +from sympy.core import S, Dummy, symbols +from sympy.polys import Poly, parallel_poly_from_expr, factor +from sympy.utilities.iterables import is_sequence + + +def gosper_normal(f, g, n, polys=True): + r""" + Compute the Gosper's normal form of ``f`` and ``g``. + + Explanation + =========== + + Given relatively prime univariate polynomials ``f`` and ``g``, + rewrite their quotient to a normal form defined as follows: + + .. math:: + \frac{f(n)}{g(n)} = Z \cdot \frac{A(n) C(n+1)}{B(n) C(n)} + + where ``Z`` is an arbitrary constant and ``A``, ``B``, ``C`` are + monic polynomials in ``n`` with the following properties: + + 1. `\gcd(A(n), B(n+h)) = 1 \forall h \in \mathbb{N}` + 2. `\gcd(B(n), C(n+1)) = 1` + 3. `\gcd(A(n), C(n)) = 1` + + This normal form, or rational factorization in other words, is a + crucial step in Gosper's algorithm and in solving of difference + equations. It can be also used to decide if two hypergeometric + terms are similar or not. + + This procedure will return a tuple containing elements of this + factorization in the form ``(Z*A, B, C)``. + + Examples + ======== + + >>> from sympy.concrete.gosper import gosper_normal + >>> from sympy.abc import n + + >>> gosper_normal(4*n+5, 2*(4*n+1)*(2*n+3), n, polys=False) + (1/4, n + 3/2, n + 1/4) + + """ + (p, q), opt = parallel_poly_from_expr( + (f, g), n, field=True, extension=True) + + a, A = p.LC(), p.monic() + b, B = q.LC(), q.monic() + + C, Z = A.one, a/b + h = Dummy('h') + + D = Poly(n + h, n, h, domain=opt.domain) + + R = A.resultant(B.compose(D)) + roots = {r for r in R.ground_roots().keys() if r.is_Integer and r >= 0} + for i in sorted(roots): + d = A.gcd(B.shift(+i)) + + A = A.quo(d) + B = B.quo(d.shift(-i)) + + for j in range(1, i + 1): + C *= d.shift(-j) + + A = A.mul_ground(Z) + + if not polys: + A = A.as_expr() + B = B.as_expr() + C = C.as_expr() + + return A, B, C + + +def gosper_term(f, n): + r""" + Compute Gosper's hypergeometric term for ``f``. + + Explanation + =========== + + Suppose ``f`` is a hypergeometric term such that: + + .. math:: + s_n = \sum_{k=0}^{n-1} f_k + + and `f_k` does not depend on `n`. Returns a hypergeometric + term `g_n` such that `g_{n+1} - g_n = f_n`. + + Examples + ======== + + >>> from sympy.concrete.gosper import gosper_term + >>> from sympy import factorial + >>> from sympy.abc import n + + >>> gosper_term((4*n + 1)*factorial(n)/factorial(2*n + 1), n) + (-n - 1/2)/(n + 1/4) + + """ + from sympy.simplify import hypersimp + r = hypersimp(f, n) + + if r is None: + return None # 'f' is *not* a hypergeometric term + + p, q = r.as_numer_denom() + + A, B, C = gosper_normal(p, q, n) + B = B.shift(-1) + + N = S(A.degree()) + M = S(B.degree()) + K = S(C.degree()) + + if (N != M) or (A.LC() != B.LC()): + D = {K - max(N, M)} + elif not N: + D = {K - N + 1, S.Zero} + else: + D = {K - N + 1, (B.nth(N - 1) - A.nth(N - 1))/A.LC()} + + for d in set(D): + if not d.is_Integer or d < 0: + D.remove(d) + + if not D: + return None # 'f(n)' is *not* Gosper-summable + + d = max(D) + + coeffs = symbols('c:%s' % (d + 1), cls=Dummy) + domain = A.get_domain().inject(*coeffs) + + x = Poly(coeffs, n, domain=domain) + H = A*x.shift(1) - B*x - C + + from sympy.solvers.solvers import solve + solution = solve(H.coeffs(), coeffs) + + if solution is None: + return None # 'f(n)' is *not* Gosper-summable + + x = x.as_expr().subs(solution) + + for coeff in coeffs: + if coeff not in solution: + x = x.subs(coeff, 0) + + if x.is_zero: + return None # 'f(n)' is *not* Gosper-summable + else: + return B.as_expr()*x/C.as_expr() + + +def gosper_sum(f, k): + r""" + Gosper's hypergeometric summation algorithm. + + Explanation + =========== + + Given a hypergeometric term ``f`` such that: + + .. math :: + s_n = \sum_{k=0}^{n-1} f_k + + and `f(n)` does not depend on `n`, returns `g_{n} - g(0)` where + `g_{n+1} - g_n = f_n`, or ``None`` if `s_n` cannot be expressed + in closed form as a sum of hypergeometric terms. + + Examples + ======== + + >>> from sympy.concrete.gosper import gosper_sum + >>> from sympy import factorial + >>> from sympy.abc import n, k + + >>> f = (4*k + 1)*factorial(k)/factorial(2*k + 1) + >>> gosper_sum(f, (k, 0, n)) + (-factorial(n) + 2*factorial(2*n + 1))/factorial(2*n + 1) + >>> _.subs(n, 2) == sum(f.subs(k, i) for i in [0, 1, 2]) + True + >>> gosper_sum(f, (k, 3, n)) + (-60*factorial(n) + factorial(2*n + 1))/(60*factorial(2*n + 1)) + >>> _.subs(n, 5) == sum(f.subs(k, i) for i in [3, 4, 5]) + True + + References + ========== + + .. [1] Marko Petkovsek, Herbert S. Wilf, Doron Zeilberger, A = B, + AK Peters, Ltd., Wellesley, MA, USA, 1997, pp. 73--100 + + """ + indefinite = False + + if is_sequence(k): + k, a, b = k + else: + indefinite = True + + g = gosper_term(f, k) + + if g is None: + return None + + if indefinite: + result = f*g + else: + result = (f*(g + 1)).subs(k, b) - (f*g).subs(k, a) + + if result is S.NaN: + try: + result = (f*(g + 1)).limit(k, b) - (f*g).limit(k, a) + except NotImplementedError: + result = None + + return factor(result) diff --git a/venv/lib/python3.11/site-packages/sympy/concrete/guess.py b/venv/lib/python3.11/site-packages/sympy/concrete/guess.py new file mode 100644 index 0000000000000000000000000000000000000000..90aac54b442ce67c90cbb262e10d525e5f2ba316 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/concrete/guess.py @@ -0,0 +1,473 @@ +"""Various algorithms for helping identifying numbers and sequences.""" + + +from sympy.concrete.products import (Product, product) +from sympy.core import Function, S +from sympy.core.add import Add +from sympy.core.numbers import Integer, Rational +from sympy.core.symbol import Symbol, symbols +from sympy.core.sympify import sympify +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.integers import floor +from sympy.integrals.integrals import integrate +from sympy.polys.polyfuncs import rational_interpolate as rinterp +from sympy.polys.polytools import lcm +from sympy.simplify.radsimp import denom +from sympy.utilities import public + + +@public +def find_simple_recurrence_vector(l): + """ + This function is used internally by other functions from the + sympy.concrete.guess module. While most users may want to rather use the + function find_simple_recurrence when looking for recurrence relations + among rational numbers, the current function may still be useful when + some post-processing has to be done. + + Explanation + =========== + + The function returns a vector of length n when a recurrence relation of + order n is detected in the sequence of rational numbers v. + + If the returned vector has a length 1, then the returned value is always + the list [0], which means that no relation has been found. + + While the functions is intended to be used with rational numbers, it should + work for other kinds of real numbers except for some cases involving + quadratic numbers; for that reason it should be used with some caution when + the argument is not a list of rational numbers. + + Examples + ======== + + >>> from sympy.concrete.guess import find_simple_recurrence_vector + >>> from sympy import fibonacci + >>> find_simple_recurrence_vector([fibonacci(k) for k in range(12)]) + [1, -1, -1] + + See Also + ======== + + See the function sympy.concrete.guess.find_simple_recurrence which is more + user-friendly. + + """ + q1 = [0] + q2 = [1] + b, z = 0, len(l) >> 1 + while len(q2) <= z: + while l[b]==0: + b += 1 + if b == len(l): + c = 1 + for x in q2: + c = lcm(c, denom(x)) + if q2[0]*c < 0: c = -c + for k in range(len(q2)): + q2[k] = int(q2[k]*c) + return q2 + a = S.One/l[b] + m = [a] + for k in range(b+1, len(l)): + m.append(-sum(l[j+1]*m[b-j-1] for j in range(b, k))*a) + l, m = m, [0] * max(len(q2), b+len(q1)) + for k, q in enumerate(q2): + m[k] = a*q + for k, q in enumerate(q1): + m[k+b] += q + while m[-1]==0: m.pop() # because trailing zeros can occur + q1, q2, b = q2, m, 1 + return [0] + +@public +def find_simple_recurrence(v, A=Function('a'), N=Symbol('n')): + """ + Detects and returns a recurrence relation from a sequence of several integer + (or rational) terms. The name of the function in the returned expression is + 'a' by default; the main variable is 'n' by default. The smallest index in + the returned expression is always n (and never n-1, n-2, etc.). + + Examples + ======== + + >>> from sympy.concrete.guess import find_simple_recurrence + >>> from sympy import fibonacci + >>> find_simple_recurrence([fibonacci(k) for k in range(12)]) + -a(n) - a(n + 1) + a(n + 2) + + >>> from sympy import Function, Symbol + >>> a = [1, 1, 1] + >>> for k in range(15): a.append(5*a[-1]-3*a[-2]+8*a[-3]) + >>> find_simple_recurrence(a, A=Function('f'), N=Symbol('i')) + -8*f(i) + 3*f(i + 1) - 5*f(i + 2) + f(i + 3) + + """ + p = find_simple_recurrence_vector(v) + n = len(p) + if n <= 1: return S.Zero + + return Add(*[A(N+n-1-k)*p[k] for k in range(n)]) + + +@public +def rationalize(x, maxcoeff=10000): + """ + Helps identifying a rational number from a float (or mpmath.mpf) value by + using a continued fraction. The algorithm stops as soon as a large partial + quotient is detected (greater than 10000 by default). + + Examples + ======== + + >>> from sympy.concrete.guess import rationalize + >>> from mpmath import cos, pi + >>> rationalize(cos(pi/3)) + 1/2 + + >>> from mpmath import mpf + >>> rationalize(mpf("0.333333333333333")) + 1/3 + + While the function is rather intended to help 'identifying' rational + values, it may be used in some cases for approximating real numbers. + (Though other functions may be more relevant in that case.) + + >>> rationalize(pi, maxcoeff = 250) + 355/113 + + See Also + ======== + + Several other methods can approximate a real number as a rational, like: + + * fractions.Fraction.from_decimal + * fractions.Fraction.from_float + * mpmath.identify + * mpmath.pslq by using the following syntax: mpmath.pslq([x, 1]) + * mpmath.findpoly by using the following syntax: mpmath.findpoly(x, 1) + * sympy.simplify.nsimplify (which is a more general function) + + The main difference between the current function and all these variants is + that control focuses on magnitude of partial quotients here rather than on + global precision of the approximation. If the real is "known to be" a + rational number, the current function should be able to detect it correctly + with the default settings even when denominator is great (unless its + expansion contains unusually big partial quotients) which may occur + when studying sequences of increasing numbers. If the user cares more + on getting simple fractions, other methods may be more convenient. + + """ + p0, p1 = 0, 1 + q0, q1 = 1, 0 + a = floor(x) + while a < maxcoeff or q1==0: + p = a*p1 + p0 + q = a*q1 + q0 + p0, p1 = p1, p + q0, q1 = q1, q + if x==a: break + x = 1/(x-a) + a = floor(x) + return sympify(p) / q + + +@public +def guess_generating_function_rational(v, X=Symbol('x')): + """ + Tries to "guess" a rational generating function for a sequence of rational + numbers v. + + Examples + ======== + + >>> from sympy.concrete.guess import guess_generating_function_rational + >>> from sympy import fibonacci + >>> l = [fibonacci(k) for k in range(5,15)] + >>> guess_generating_function_rational(l) + (3*x + 5)/(-x**2 - x + 1) + + See Also + ======== + + sympy.series.approximants + mpmath.pade + + """ + # a) compute the denominator as q + q = find_simple_recurrence_vector(v) + n = len(q) + if n <= 1: return None + # b) compute the numerator as p + p = [sum(v[i-k]*q[k] for k in range(min(i+1, n))) + for i in range(len(v)>>1)] + return (sum(p[k]*X**k for k in range(len(p))) + / sum(q[k]*X**k for k in range(n))) + + +@public +def guess_generating_function(v, X=Symbol('x'), types=['all'], maxsqrtn=2): + """ + Tries to "guess" a generating function for a sequence of rational numbers v. + Only a few patterns are implemented yet. + + Explanation + =========== + + The function returns a dictionary where keys are the name of a given type of + generating function. Six types are currently implemented: + + type | formal definition + -------+---------------------------------------------------------------- + ogf | f(x) = Sum( a_k * x^k , k: 0..infinity ) + egf | f(x) = Sum( a_k * x^k / k! , k: 0..infinity ) + lgf | f(x) = Sum( (-1)^(k+1) a_k * x^k / k , k: 1..infinity ) + | (with initial index being hold as 1 rather than 0) + hlgf | f(x) = Sum( a_k * x^k / k , k: 1..infinity ) + | (with initial index being hold as 1 rather than 0) + lgdogf | f(x) = derivate( log(Sum( a_k * x^k, k: 0..infinity )), x) + lgdegf | f(x) = derivate( log(Sum( a_k * x^k / k!, k: 0..infinity )), x) + + In order to spare time, the user can select only some types of generating + functions (default being ['all']). While forgetting to use a list in the + case of a single type may seem to work most of the time as in: types='ogf' + this (convenient) syntax may lead to unexpected extra results in some cases. + + Discarding a type when calling the function does not mean that the type will + not be present in the returned dictionary; it only means that no extra + computation will be performed for that type, but the function may still add + it in the result when it can be easily converted from another type. + + Two generating functions (lgdogf and lgdegf) are not even computed if the + initial term of the sequence is 0; it may be useful in that case to try + again after having removed the leading zeros. + + Examples + ======== + + >>> from sympy.concrete.guess import guess_generating_function as ggf + >>> ggf([k+1 for k in range(12)], types=['ogf', 'lgf', 'hlgf']) + {'hlgf': 1/(1 - x), 'lgf': 1/(x + 1), 'ogf': 1/(x**2 - 2*x + 1)} + + >>> from sympy import sympify + >>> l = sympify("[3/2, 11/2, 0, -121/2, -363/2, 121]") + >>> ggf(l) + {'ogf': (x + 3/2)/(11*x**2 - 3*x + 1)} + + >>> from sympy import fibonacci + >>> ggf([fibonacci(k) for k in range(5, 15)], types=['ogf']) + {'ogf': (3*x + 5)/(-x**2 - x + 1)} + + >>> from sympy import factorial + >>> ggf([factorial(k) for k in range(12)], types=['ogf', 'egf', 'lgf']) + {'egf': 1/(1 - x)} + + >>> ggf([k+1 for k in range(12)], types=['egf']) + {'egf': (x + 1)*exp(x), 'lgdegf': (x + 2)/(x + 1)} + + N-th root of a rational function can also be detected (below is an example + coming from the sequence A108626 from https://oeis.org). + The greatest n-th root to be tested is specified as maxsqrtn (default 2). + + >>> ggf([1, 2, 5, 14, 41, 124, 383, 1200, 3799, 12122, 38919])['ogf'] + sqrt(1/(x**4 + 2*x**2 - 4*x + 1)) + + References + ========== + + .. [1] "Concrete Mathematics", R.L. Graham, D.E. Knuth, O. Patashnik + .. [2] https://oeis.org/wiki/Generating_functions + + """ + # List of all types of all g.f. known by the algorithm + if 'all' in types: + types = ('ogf', 'egf', 'lgf', 'hlgf', 'lgdogf', 'lgdegf') + + result = {} + + # Ordinary Generating Function (ogf) + if 'ogf' in types: + # Perform some convolutions of the sequence with itself + t = [1] + [0]*(len(v) - 1) + for d in range(max(1, maxsqrtn)): + t = [sum(t[n-i]*v[i] for i in range(n+1)) for n in range(len(v))] + g = guess_generating_function_rational(t, X=X) + if g: + result['ogf'] = g**Rational(1, d+1) + break + + # Exponential Generating Function (egf) + if 'egf' in types: + # Transform sequence (division by factorial) + w, f = [], S.One + for i, k in enumerate(v): + f *= i if i else 1 + w.append(k/f) + # Perform some convolutions of the sequence with itself + t = [1] + [0]*(len(w) - 1) + for d in range(max(1, maxsqrtn)): + t = [sum(t[n-i]*w[i] for i in range(n+1)) for n in range(len(w))] + g = guess_generating_function_rational(t, X=X) + if g: + result['egf'] = g**Rational(1, d+1) + break + + # Logarithmic Generating Function (lgf) + if 'lgf' in types: + # Transform sequence (multiplication by (-1)^(n+1) / n) + w, f = [], S.NegativeOne + for i, k in enumerate(v): + f = -f + w.append(f*k/Integer(i+1)) + # Perform some convolutions of the sequence with itself + t = [1] + [0]*(len(w) - 1) + for d in range(max(1, maxsqrtn)): + t = [sum(t[n-i]*w[i] for i in range(n+1)) for n in range(len(w))] + g = guess_generating_function_rational(t, X=X) + if g: + result['lgf'] = g**Rational(1, d+1) + break + + # Hyperbolic logarithmic Generating Function (hlgf) + if 'hlgf' in types: + # Transform sequence (division by n+1) + w = [] + for i, k in enumerate(v): + w.append(k/Integer(i+1)) + # Perform some convolutions of the sequence with itself + t = [1] + [0]*(len(w) - 1) + for d in range(max(1, maxsqrtn)): + t = [sum(t[n-i]*w[i] for i in range(n+1)) for n in range(len(w))] + g = guess_generating_function_rational(t, X=X) + if g: + result['hlgf'] = g**Rational(1, d+1) + break + + # Logarithmic derivative of ordinary generating Function (lgdogf) + if v[0] != 0 and ('lgdogf' in types + or ('ogf' in types and 'ogf' not in result)): + # Transform sequence by computing f'(x)/f(x) + # because log(f(x)) = integrate( f'(x)/f(x) ) + a, w = sympify(v[0]), [] + for n in range(len(v)-1): + w.append( + (v[n+1]*(n+1) - sum(w[-i-1]*v[i+1] for i in range(n)))/a) + # Perform some convolutions of the sequence with itself + t = [1] + [0]*(len(w) - 1) + for d in range(max(1, maxsqrtn)): + t = [sum(t[n-i]*w[i] for i in range(n+1)) for n in range(len(w))] + g = guess_generating_function_rational(t, X=X) + if g: + result['lgdogf'] = g**Rational(1, d+1) + if 'ogf' not in result: + result['ogf'] = exp(integrate(result['lgdogf'], X)) + break + + # Logarithmic derivative of exponential generating Function (lgdegf) + if v[0] != 0 and ('lgdegf' in types + or ('egf' in types and 'egf' not in result)): + # Transform sequence / step 1 (division by factorial) + z, f = [], S.One + for i, k in enumerate(v): + f *= i if i else 1 + z.append(k/f) + # Transform sequence / step 2 by computing f'(x)/f(x) + # because log(f(x)) = integrate( f'(x)/f(x) ) + a, w = z[0], [] + for n in range(len(z)-1): + w.append( + (z[n+1]*(n+1) - sum(w[-i-1]*z[i+1] for i in range(n)))/a) + # Perform some convolutions of the sequence with itself + t = [1] + [0]*(len(w) - 1) + for d in range(max(1, maxsqrtn)): + t = [sum(t[n-i]*w[i] for i in range(n+1)) for n in range(len(w))] + g = guess_generating_function_rational(t, X=X) + if g: + result['lgdegf'] = g**Rational(1, d+1) + if 'egf' not in result: + result['egf'] = exp(integrate(result['lgdegf'], X)) + break + + return result + + +@public +def guess(l, all=False, evaluate=True, niter=2, variables=None): + """ + This function is adapted from the Rate.m package for Mathematica + written by Christian Krattenthaler. + It tries to guess a formula from a given sequence of rational numbers. + + Explanation + =========== + + In order to speed up the process, the 'all' variable is set to False by + default, stopping the computation as some results are returned during an + iteration; the variable can be set to True if more iterations are needed + (other formulas may be found; however they may be equivalent to the first + ones). + + Another option is the 'evaluate' variable (default is True); setting it + to False will leave the involved products unevaluated. + + By default, the number of iterations is set to 2 but a greater value (up + to len(l)-1) can be specified with the optional 'niter' variable. + More and more convoluted results are found when the order of the + iteration gets higher: + + * first iteration returns polynomial or rational functions; + * second iteration returns products of rising factorials and their + inverses; + * third iteration returns products of products of rising factorials + and their inverses; + * etc. + + The returned formulas contain symbols i0, i1, i2, ... where the main + variables is i0 (and auxiliary variables are i1, i2, ...). A list of + other symbols can be provided in the 'variables' option; the length of + the least should be the value of 'niter' (more is acceptable but only + the first symbols will be used); in this case, the main variable will be + the first symbol in the list. + + Examples + ======== + + >>> from sympy.concrete.guess import guess + >>> guess([1,2,6,24,120], evaluate=False) + [Product(i1 + 1, (i1, 1, i0 - 1))] + + >>> from sympy import symbols + >>> r = guess([1,2,7,42,429,7436,218348,10850216], niter=4) + >>> i0 = symbols("i0") + >>> [r[0].subs(i0,n).doit() for n in range(1,10)] + [1, 2, 7, 42, 429, 7436, 218348, 10850216, 911835460] + """ + if any(a==0 for a in l[:-1]): + return [] + N = len(l) + niter = min(N-1, niter) + myprod = product if evaluate else Product + g = [] + res = [] + if variables is None: + symb = symbols('i:'+str(niter)) + else: + symb = variables + for k, s in enumerate(symb): + g.append(l) + n, r = len(l), [] + for i in range(n-2-1, -1, -1): + ri = rinterp(enumerate(g[k][:-1], start=1), i, X=s) + if ((denom(ri).subs({s:n}) != 0) + and (ri.subs({s:n}) - g[k][-1] == 0) + and ri not in r): + r.append(ri) + if r: + for i in range(k-1, -1, -1): + r = [g[i][0] + * myprod(v, (symb[i+1], 1, symb[i]-1)) for v in r] + if not all: return r + res += r + l = [Rational(l[i+1], l[i]) for i in range(N-k-1)] + return res diff --git a/venv/lib/python3.11/site-packages/sympy/concrete/products.py b/venv/lib/python3.11/site-packages/sympy/concrete/products.py new file mode 100644 index 0000000000000000000000000000000000000000..dd035551683531f80b0e4467fe1cdfbb6ebbe9ad --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/concrete/products.py @@ -0,0 +1,605 @@ +from __future__ import annotations + +from .expr_with_intlimits import ExprWithIntLimits +from .summations import Sum, summation, _dummy_with_inherited_properties_concrete +from sympy.core.expr import Expr +from sympy.core.exprtools import factor_terms +from sympy.core.function import Derivative +from sympy.core.mul import Mul +from sympy.core.singleton import S +from sympy.core.symbol import Dummy, Symbol +from sympy.functions.combinatorial.factorials import RisingFactorial +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.polys import quo, roots + + +class Product(ExprWithIntLimits): + r""" + Represents unevaluated products. + + Explanation + =========== + + ``Product`` represents a finite or infinite product, with the first + argument being the general form of terms in the series, and the second + argument being ``(dummy_variable, start, end)``, with ``dummy_variable`` + taking all integer values from ``start`` through ``end``. In accordance + with long-standing mathematical convention, the end term is included in + the product. + + Finite products + =============== + + For finite products (and products with symbolic limits assumed to be finite) + we follow the analogue of the summation convention described by Karr [1], + especially definition 3 of section 1.4. The product: + + .. math:: + + \prod_{m \leq i < n} f(i) + + has *the obvious meaning* for `m < n`, namely: + + .. math:: + + \prod_{m \leq i < n} f(i) = f(m) f(m+1) \cdot \ldots \cdot f(n-2) f(n-1) + + with the upper limit value `f(n)` excluded. The product over an empty set is + one if and only if `m = n`: + + .. math:: + + \prod_{m \leq i < n} f(i) = 1 \quad \mathrm{for} \quad m = n + + Finally, for all other products over empty sets we assume the following + definition: + + .. math:: + + \prod_{m \leq i < n} f(i) = \frac{1}{\prod_{n \leq i < m} f(i)} \quad \mathrm{for} \quad m > n + + It is important to note that above we define all products with the upper + limit being exclusive. This is in contrast to the usual mathematical notation, + but does not affect the product convention. Indeed we have: + + .. math:: + + \prod_{m \leq i < n} f(i) = \prod_{i = m}^{n - 1} f(i) + + where the difference in notation is intentional to emphasize the meaning, + with limits typeset on the top being inclusive. + + Examples + ======== + + >>> from sympy.abc import a, b, i, k, m, n, x + >>> from sympy import Product, oo + >>> Product(k, (k, 1, m)) + Product(k, (k, 1, m)) + >>> Product(k, (k, 1, m)).doit() + factorial(m) + >>> Product(k**2,(k, 1, m)) + Product(k**2, (k, 1, m)) + >>> Product(k**2,(k, 1, m)).doit() + factorial(m)**2 + + Wallis' product for pi: + + >>> W = Product(2*i/(2*i-1) * 2*i/(2*i+1), (i, 1, oo)) + >>> W + Product(4*i**2/((2*i - 1)*(2*i + 1)), (i, 1, oo)) + + Direct computation currently fails: + + >>> W.doit() + Product(4*i**2/((2*i - 1)*(2*i + 1)), (i, 1, oo)) + + But we can approach the infinite product by a limit of finite products: + + >>> from sympy import limit + >>> W2 = Product(2*i/(2*i-1)*2*i/(2*i+1), (i, 1, n)) + >>> W2 + Product(4*i**2/((2*i - 1)*(2*i + 1)), (i, 1, n)) + >>> W2e = W2.doit() + >>> W2e + 4**n*factorial(n)**2/(2**(2*n)*RisingFactorial(1/2, n)*RisingFactorial(3/2, n)) + >>> limit(W2e, n, oo) + pi/2 + + By the same formula we can compute sin(pi/2): + + >>> from sympy import combsimp, pi, gamma, simplify + >>> P = pi * x * Product(1 - x**2/k**2, (k, 1, n)) + >>> P = P.subs(x, pi/2) + >>> P + pi**2*Product(1 - pi**2/(4*k**2), (k, 1, n))/2 + >>> Pe = P.doit() + >>> Pe + pi**2*RisingFactorial(1 - pi/2, n)*RisingFactorial(1 + pi/2, n)/(2*factorial(n)**2) + >>> limit(Pe, n, oo).gammasimp() + sin(pi**2/2) + >>> Pe.rewrite(gamma) + (-1)**n*pi**2*gamma(pi/2)*gamma(n + 1 + pi/2)/(2*gamma(1 + pi/2)*gamma(-n + pi/2)*gamma(n + 1)**2) + + Products with the lower limit being larger than the upper one: + + >>> Product(1/i, (i, 6, 1)).doit() + 120 + >>> Product(i, (i, 2, 5)).doit() + 120 + + The empty product: + + >>> Product(i, (i, n, n-1)).doit() + 1 + + An example showing that the symbolic result of a product is still + valid for seemingly nonsensical values of the limits. Then the Karr + convention allows us to give a perfectly valid interpretation to + those products by interchanging the limits according to the above rules: + + >>> P = Product(2, (i, 10, n)).doit() + >>> P + 2**(n - 9) + >>> P.subs(n, 5) + 1/16 + >>> Product(2, (i, 10, 5)).doit() + 1/16 + >>> 1/Product(2, (i, 6, 9)).doit() + 1/16 + + An explicit example of the Karr summation convention applied to products: + + >>> P1 = Product(x, (i, a, b)).doit() + >>> P1 + x**(-a + b + 1) + >>> P2 = Product(x, (i, b+1, a-1)).doit() + >>> P2 + x**(a - b - 1) + >>> simplify(P1 * P2) + 1 + + And another one: + + >>> P1 = Product(i, (i, b, a)).doit() + >>> P1 + RisingFactorial(b, a - b + 1) + >>> P2 = Product(i, (i, a+1, b-1)).doit() + >>> P2 + RisingFactorial(a + 1, -a + b - 1) + >>> P1 * P2 + RisingFactorial(b, a - b + 1)*RisingFactorial(a + 1, -a + b - 1) + >>> combsimp(P1 * P2) + 1 + + See Also + ======== + + Sum, summation + product + + References + ========== + + .. [1] Michael Karr, "Summation in Finite Terms", Journal of the ACM, + Volume 28 Issue 2, April 1981, Pages 305-350 + https://dl.acm.org/doi/10.1145/322248.322255 + .. [2] https://en.wikipedia.org/wiki/Multiplication#Capital_Pi_notation + .. [3] https://en.wikipedia.org/wiki/Empty_product + """ + + __slots__ = () + + limits: tuple[tuple[Symbol, Expr, Expr]] + + def __new__(cls, function, *symbols, **assumptions): + obj = ExprWithIntLimits.__new__(cls, function, *symbols, **assumptions) + return obj + + def _eval_rewrite_as_Sum(self, *args, **kwargs): + return exp(Sum(log(self.function), *self.limits)) + + @property + def term(self): + return self._args[0] + function = term + + def _eval_is_zero(self): + if self.has_empty_sequence: + return False + + z = self.term.is_zero + if z is True: + return True + if self.has_finite_limits: + # A Product is zero only if its term is zero assuming finite limits. + return z + + def _eval_is_extended_real(self): + if self.has_empty_sequence: + return True + + return self.function.is_extended_real + + def _eval_is_positive(self): + if self.has_empty_sequence: + return True + if self.function.is_positive and self.has_finite_limits: + return True + + def _eval_is_nonnegative(self): + if self.has_empty_sequence: + return True + if self.function.is_nonnegative and self.has_finite_limits: + return True + + def _eval_is_extended_nonnegative(self): + if self.has_empty_sequence: + return True + if self.function.is_extended_nonnegative: + return True + + def _eval_is_extended_nonpositive(self): + if self.has_empty_sequence: + return True + + def _eval_is_finite(self): + if self.has_finite_limits and self.function.is_finite: + return True + + def doit(self, **hints): + # first make sure any definite limits have product + # variables with matching assumptions + reps = {} + for xab in self.limits: + d = _dummy_with_inherited_properties_concrete(xab) + if d: + reps[xab[0]] = d + if reps: + undo = {v: k for k, v in reps.items()} + did = self.xreplace(reps).doit(**hints) + if isinstance(did, tuple): # when separate=True + did = tuple([i.xreplace(undo) for i in did]) + else: + did = did.xreplace(undo) + return did + + from sympy.simplify.powsimp import powsimp + f = self.function + for index, limit in enumerate(self.limits): + i, a, b = limit + dif = b - a + if dif.is_integer and dif.is_negative: + a, b = b + 1, a - 1 + f = 1 / f + + g = self._eval_product(f, (i, a, b)) + if g in (None, S.NaN): + return self.func(powsimp(f), *self.limits[index:]) + else: + f = g + + if hints.get('deep', True): + return f.doit(**hints) + else: + return powsimp(f) + + def _eval_conjugate(self): + return self.func(self.function.conjugate(), *self.limits) + + def _eval_product(self, term, limits): + + (k, a, n) = limits + + if k not in term.free_symbols: + if (term - 1).is_zero: + return S.One + return term**(n - a + 1) + + if a == n: + return term.subs(k, a) + + from .delta import deltaproduct, _has_simple_delta + if term.has(KroneckerDelta) and _has_simple_delta(term, limits[0]): + return deltaproduct(term, limits) + + dif = n - a + definite = dif.is_Integer + if definite and (dif < 100): + return self._eval_product_direct(term, limits) + + elif term.is_polynomial(k): + poly = term.as_poly(k) + + A = B = Q = S.One + + all_roots = roots(poly) + + M = 0 + for r, m in all_roots.items(): + M += m + A *= RisingFactorial(a - r, n - a + 1)**m + Q *= (n - r)**m + + if M < poly.degree(): + arg = quo(poly, Q.as_poly(k)) + B = self.func(arg, (k, a, n)).doit() + + return poly.LC()**(n - a + 1) * A * B + + elif term.is_Add: + factored = factor_terms(term, fraction=True) + if factored.is_Mul: + return self._eval_product(factored, (k, a, n)) + + elif term.is_Mul: + # Factor in part without the summation variable and part with + without_k, with_k = term.as_coeff_mul(k) + + if len(with_k) >= 2: + # More than one term including k, so still a multiplication + exclude, include = [], [] + for t in with_k: + p = self._eval_product(t, (k, a, n)) + + if p is not None: + exclude.append(p) + else: + include.append(t) + + if not exclude: + return None + else: + arg = term._new_rawargs(*include) + A = Mul(*exclude) + B = self.func(arg, (k, a, n)).doit() + return without_k**(n - a + 1)*A * B + else: + # Just a single term + p = self._eval_product(with_k[0], (k, a, n)) + if p is None: + p = self.func(with_k[0], (k, a, n)).doit() + return without_k**(n - a + 1)*p + + + elif term.is_Pow: + if not term.base.has(k): + s = summation(term.exp, (k, a, n)) + + return term.base**s + elif not term.exp.has(k): + p = self._eval_product(term.base, (k, a, n)) + + if p is not None: + return p**term.exp + + elif isinstance(term, Product): + evaluated = term.doit() + f = self._eval_product(evaluated, limits) + if f is None: + return self.func(evaluated, limits) + else: + return f + + if definite: + return self._eval_product_direct(term, limits) + + def _eval_simplify(self, **kwargs): + from sympy.simplify.simplify import product_simplify + rv = product_simplify(self, **kwargs) + return rv.doit() if kwargs['doit'] else rv + + def _eval_transpose(self): + if self.is_commutative: + return self.func(self.function.transpose(), *self.limits) + return None + + def _eval_product_direct(self, term, limits): + (k, a, n) = limits + return Mul(*[term.subs(k, a + i) for i in range(n - a + 1)]) + + def _eval_derivative(self, x): + if isinstance(x, Symbol) and x not in self.free_symbols: + return S.Zero + f, limits = self.function, list(self.limits) + limit = limits.pop(-1) + if limits: + f = self.func(f, *limits) + i, a, b = limit + if x in a.free_symbols or x in b.free_symbols: + return None + h = Dummy() + rv = Sum( Product(f, (i, a, h - 1)) * Product(f, (i, h + 1, b)) * Derivative(f, x, evaluate=True).subs(i, h), (h, a, b)) + return rv + + def is_convergent(self): + r""" + See docs of :obj:`.Sum.is_convergent()` for explanation of convergence + in SymPy. + + Explanation + =========== + + The infinite product: + + .. math:: + + \prod_{1 \leq i < \infty} f(i) + + is defined by the sequence of partial products: + + .. math:: + + \prod_{i=1}^{n} f(i) = f(1) f(2) \cdots f(n) + + as n increases without bound. The product converges to a non-zero + value if and only if the sum: + + .. math:: + + \sum_{1 \leq i < \infty} \log{f(n)} + + converges. + + Examples + ======== + + >>> from sympy import Product, Symbol, cos, pi, exp, oo + >>> n = Symbol('n', integer=True) + >>> Product(n/(n + 1), (n, 1, oo)).is_convergent() + False + >>> Product(1/n**2, (n, 1, oo)).is_convergent() + False + >>> Product(cos(pi/n), (n, 1, oo)).is_convergent() + True + >>> Product(exp(-n**2), (n, 1, oo)).is_convergent() + False + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Infinite_product + """ + sequence_term = self.function + log_sum = log(sequence_term) + lim = self.limits + try: + is_conv = Sum(log_sum, *lim).is_convergent() + except NotImplementedError: + if Sum(sequence_term - 1, *lim).is_absolutely_convergent() is S.true: + return S.true + raise NotImplementedError("The algorithm to find the product convergence of %s " + "is not yet implemented" % (sequence_term)) + return is_conv + + def reverse_order(expr, *indices): + """ + Reverse the order of a limit in a Product. + + Explanation + =========== + + ``reverse_order(expr, *indices)`` reverses some limits in the expression + ``expr`` which can be either a ``Sum`` or a ``Product``. The selectors in + the argument ``indices`` specify some indices whose limits get reversed. + These selectors are either variable names or numerical indices counted + starting from the inner-most limit tuple. + + Examples + ======== + + >>> from sympy import gamma, Product, simplify, Sum + >>> from sympy.abc import x, y, a, b, c, d + >>> P = Product(x, (x, a, b)) + >>> Pr = P.reverse_order(x) + >>> Pr + Product(1/x, (x, b + 1, a - 1)) + >>> Pr = Pr.doit() + >>> Pr + 1/RisingFactorial(b + 1, a - b - 1) + >>> simplify(Pr.rewrite(gamma)) + Piecewise((gamma(b + 1)/gamma(a), b > -1), ((-1)**(-a + b + 1)*gamma(1 - a)/gamma(-b), True)) + >>> P = P.doit() + >>> P + RisingFactorial(a, -a + b + 1) + >>> simplify(P.rewrite(gamma)) + Piecewise((gamma(b + 1)/gamma(a), a > 0), ((-1)**(-a + b + 1)*gamma(1 - a)/gamma(-b), True)) + + While one should prefer variable names when specifying which limits + to reverse, the index counting notation comes in handy in case there + are several symbols with the same name. + + >>> S = Sum(x*y, (x, a, b), (y, c, d)) + >>> S + Sum(x*y, (x, a, b), (y, c, d)) + >>> S0 = S.reverse_order(0) + >>> S0 + Sum(-x*y, (x, b + 1, a - 1), (y, c, d)) + >>> S1 = S0.reverse_order(1) + >>> S1 + Sum(x*y, (x, b + 1, a - 1), (y, d + 1, c - 1)) + + Of course we can mix both notations: + + >>> Sum(x*y, (x, a, b), (y, 2, 5)).reverse_order(x, 1) + Sum(x*y, (x, b + 1, a - 1), (y, 6, 1)) + >>> Sum(x*y, (x, a, b), (y, 2, 5)).reverse_order(y, x) + Sum(x*y, (x, b + 1, a - 1), (y, 6, 1)) + + See Also + ======== + + sympy.concrete.expr_with_intlimits.ExprWithIntLimits.index, + reorder_limit, + sympy.concrete.expr_with_intlimits.ExprWithIntLimits.reorder + + References + ========== + + .. [1] Michael Karr, "Summation in Finite Terms", Journal of the ACM, + Volume 28 Issue 2, April 1981, Pages 305-350 + https://dl.acm.org/doi/10.1145/322248.322255 + + """ + l_indices = list(indices) + + for i, indx in enumerate(l_indices): + if not isinstance(indx, int): + l_indices[i] = expr.index(indx) + + e = 1 + limits = [] + for i, limit in enumerate(expr.limits): + l = limit + if i in l_indices: + e = -e + l = (limit[0], limit[2] + 1, limit[1] - 1) + limits.append(l) + + return Product(expr.function ** e, *limits) + + +def product(*args, **kwargs): + r""" + Compute the product. + + Explanation + =========== + + The notation for symbols is similar to the notation used in Sum or + Integral. product(f, (i, a, b)) computes the product of f with + respect to i from a to b, i.e., + + :: + + b + _____ + product(f(n), (i, a, b)) = | | f(n) + | | + i = a + + If it cannot compute the product, it returns an unevaluated Product object. + Repeated products can be computed by introducing additional symbols tuples:: + + Examples + ======== + + >>> from sympy import product, symbols + >>> i, n, m, k = symbols('i n m k', integer=True) + + >>> product(i, (i, 1, k)) + factorial(k) + >>> product(m, (i, 1, k)) + m**k + >>> product(i, (i, 1, k), (k, 1, n)) + Product(factorial(k), (k, 1, n)) + + """ + + prod = Product(*args, **kwargs) + + if isinstance(prod, Product): + return prod.doit(deep=False) + else: + return prod diff --git a/venv/lib/python3.11/site-packages/sympy/concrete/summations.py b/venv/lib/python3.11/site-packages/sympy/concrete/summations.py new file mode 100644 index 0000000000000000000000000000000000000000..96bdbd69b7820798fade4bc5ec145172b95947ef --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/concrete/summations.py @@ -0,0 +1,1659 @@ +from __future__ import annotations + +from sympy.calculus.singularities import is_decreasing +from sympy.calculus.accumulationbounds import AccumulationBounds +from .expr_with_intlimits import ExprWithIntLimits +from .expr_with_limits import AddWithLimits +from .gosper import gosper_sum +from sympy.core.expr import Expr +from sympy.core.add import Add +from sympy.core.containers import Tuple +from sympy.core.function import Derivative, expand +from sympy.core.mul import Mul +from sympy.core.numbers import Float, _illegal +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.sorting import ordered +from sympy.core.symbol import Dummy, Wild, Symbol, symbols +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.combinatorial.numbers import bernoulli, harmonic +from sympy.functions.elementary.complexes import re +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import cot, csc +from sympy.functions.special.hyper import hyper +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.functions.special.zeta_functions import zeta +from sympy.integrals.integrals import Integral +from sympy.logic.boolalg import And, Not +from sympy.polys.partfrac import apart +from sympy.polys.polyerrors import PolynomialError, PolificationFailed +from sympy.polys.polytools import parallel_poly_from_expr, Poly, factor +from sympy.polys.rationaltools import together +from sympy.series.limitseq import limit_seq +from sympy.series.order import O +from sympy.series.residues import residue +from sympy.sets.contains import Contains +from sympy.sets.sets import FiniteSet, Interval +from sympy.utilities.iterables import sift +import itertools + + +class Sum(AddWithLimits, ExprWithIntLimits): + r""" + Represents unevaluated summation. + + Explanation + =========== + + ``Sum`` represents a finite or infinite series, with the first argument + being the general form of terms in the series, and the second argument + being ``(dummy_variable, start, end)``, with ``dummy_variable`` taking + all integer values from ``start`` through ``end``. In accordance with + long-standing mathematical convention, the end term is included in the + summation. + + Finite sums + =========== + + For finite sums (and sums with symbolic limits assumed to be finite) we + follow the summation convention described by Karr [1], especially + definition 3 of section 1.4. The sum: + + .. math:: + + \sum_{m \leq i < n} f(i) + + has *the obvious meaning* for `m < n`, namely: + + .. math:: + + \sum_{m \leq i < n} f(i) = f(m) + f(m+1) + \ldots + f(n-2) + f(n-1) + + with the upper limit value `f(n)` excluded. The sum over an empty set is + zero if and only if `m = n`: + + .. math:: + + \sum_{m \leq i < n} f(i) = 0 \quad \mathrm{for} \quad m = n + + Finally, for all other sums over empty sets we assume the following + definition: + + .. math:: + + \sum_{m \leq i < n} f(i) = - \sum_{n \leq i < m} f(i) \quad \mathrm{for} \quad m > n + + It is important to note that Karr defines all sums with the upper + limit being exclusive. This is in contrast to the usual mathematical notation, + but does not affect the summation convention. Indeed we have: + + .. math:: + + \sum_{m \leq i < n} f(i) = \sum_{i = m}^{n - 1} f(i) + + where the difference in notation is intentional to emphasize the meaning, + with limits typeset on the top being inclusive. + + Examples + ======== + + >>> from sympy.abc import i, k, m, n, x + >>> from sympy import Sum, factorial, oo, IndexedBase, Function + >>> Sum(k, (k, 1, m)) + Sum(k, (k, 1, m)) + >>> Sum(k, (k, 1, m)).doit() + m**2/2 + m/2 + >>> Sum(k**2, (k, 1, m)) + Sum(k**2, (k, 1, m)) + >>> Sum(k**2, (k, 1, m)).doit() + m**3/3 + m**2/2 + m/6 + >>> Sum(x**k, (k, 0, oo)) + Sum(x**k, (k, 0, oo)) + >>> Sum(x**k, (k, 0, oo)).doit() + Piecewise((1/(1 - x), Abs(x) < 1), (Sum(x**k, (k, 0, oo)), True)) + >>> Sum(x**k/factorial(k), (k, 0, oo)).doit() + exp(x) + + Here are examples to do summation with symbolic indices. You + can use either Function of IndexedBase classes: + + >>> f = Function('f') + >>> Sum(f(n), (n, 0, 3)).doit() + f(0) + f(1) + f(2) + f(3) + >>> Sum(f(n), (n, 0, oo)).doit() + Sum(f(n), (n, 0, oo)) + >>> f = IndexedBase('f') + >>> Sum(f[n]**2, (n, 0, 3)).doit() + f[0]**2 + f[1]**2 + f[2]**2 + f[3]**2 + + An example showing that the symbolic result of a summation is still + valid for seemingly nonsensical values of the limits. Then the Karr + convention allows us to give a perfectly valid interpretation to + those sums by interchanging the limits according to the above rules: + + >>> S = Sum(i, (i, 1, n)).doit() + >>> S + n**2/2 + n/2 + >>> S.subs(n, -4) + 6 + >>> Sum(i, (i, 1, -4)).doit() + 6 + >>> Sum(-i, (i, -3, 0)).doit() + 6 + + An explicit example of the Karr summation convention: + + >>> S1 = Sum(i**2, (i, m, m+n-1)).doit() + >>> S1 + m**2*n + m*n**2 - m*n + n**3/3 - n**2/2 + n/6 + >>> S2 = Sum(i**2, (i, m+n, m-1)).doit() + >>> S2 + -m**2*n - m*n**2 + m*n - n**3/3 + n**2/2 - n/6 + >>> S1 + S2 + 0 + >>> S3 = Sum(i, (i, m, m-1)).doit() + >>> S3 + 0 + + See Also + ======== + + summation + Product, sympy.concrete.products.product + + References + ========== + + .. [1] Michael Karr, "Summation in Finite Terms", Journal of the ACM, + Volume 28 Issue 2, April 1981, Pages 305-350 + https://dl.acm.org/doi/10.1145/322248.322255 + .. [2] https://en.wikipedia.org/wiki/Summation#Capital-sigma_notation + .. [3] https://en.wikipedia.org/wiki/Empty_sum + """ + + __slots__ = () + + limits: tuple[tuple[Symbol, Expr, Expr]] + + def __new__(cls, function, *symbols, **assumptions): + obj = AddWithLimits.__new__(cls, function, *symbols, **assumptions) + if not hasattr(obj, 'limits'): + return obj + if any(len(l) != 3 or None in l for l in obj.limits): + raise ValueError('Sum requires values for lower and upper bounds.') + + return obj + + def _eval_is_zero(self): + # a Sum is only zero if its function is zero or if all terms + # cancel out. This only answers whether the summand is zero; if + # not then None is returned since we don't analyze whether all + # terms cancel out. + if self.function.is_zero or self.has_empty_sequence: + return True + + def _eval_is_extended_real(self): + if self.has_empty_sequence: + return True + return self.function.is_extended_real + + def _eval_is_positive(self): + if self.has_finite_limits and self.has_reversed_limits is False: + return self.function.is_positive + + def _eval_is_negative(self): + if self.has_finite_limits and self.has_reversed_limits is False: + return self.function.is_negative + + def _eval_is_finite(self): + if self.has_finite_limits and self.function.is_finite: + return True + + def doit(self, **hints): + if hints.get('deep', True): + f = self.function.doit(**hints) + else: + f = self.function + + # first make sure any definite limits have summation + # variables with matching assumptions + reps = {} + for xab in self.limits: + d = _dummy_with_inherited_properties_concrete(xab) + if d: + reps[xab[0]] = d + if reps: + undo = {v: k for k, v in reps.items()} + did = self.xreplace(reps).doit(**hints) + if isinstance(did, tuple): # when separate=True + did = tuple([i.xreplace(undo) for i in did]) + elif did is not None: + did = did.xreplace(undo) + else: + did = self + return did + + + if self.function.is_Matrix: + expanded = self.expand() + if self != expanded: + return expanded.doit() + return _eval_matrix_sum(self) + + for n, limit in enumerate(self.limits): + i, a, b = limit + dif = b - a + if dif == -1: + # Any summation over an empty set is zero + return S.Zero + if dif.is_integer and dif.is_negative: + a, b = b + 1, a - 1 + f = -f + + newf = eval_sum(f, (i, a, b)) + if newf is None: + if f == self.function: + zeta_function = self.eval_zeta_function(f, (i, a, b)) + if zeta_function is not None: + return zeta_function + return self + else: + return self.func(f, *self.limits[n:]) + f = newf + + if hints.get('deep', True): + # eval_sum could return partially unevaluated + # result with Piecewise. In this case we won't + # doit() recursively. + if not isinstance(f, Piecewise): + return f.doit(**hints) + + return f + + def eval_zeta_function(self, f, limits): + """ + Check whether the function matches with the zeta function. + + If it matches, then return a `Piecewise` expression because + zeta function does not converge unless `s > 1` and `q > 0` + """ + i, a, b = limits + if a.is_comparable and b.is_comparable and a > b: + return self.eval_zeta_function(f, (i, b + S.One, a - S.One)) + if b is not S.Infinity: + return + w, y, z = Wild('w', exclude=[i]), Wild('y', exclude=[i]), Wild('z', exclude=[i]) + if result := f.match((w * i + y) ** (-z)): + coeff = 1 / result[w] ** result[z] + s = result[z] + q = result[y] / result[w] + a + return Piecewise((coeff * zeta(s, q), + And(Not(Contains(-q, S.Naturals0)), re(s) > S.One)), + (self, True)) + + def _eval_derivative(self, x): + """ + Differentiate wrt x as long as x is not in the free symbols of any of + the upper or lower limits. + + Explanation + =========== + + Sum(a*b*x, (x, 1, a)) can be differentiated wrt x or b but not `a` + since the value of the sum is discontinuous in `a`. In a case + involving a limit variable, the unevaluated derivative is returned. + """ + + # diff already confirmed that x is in the free symbols of self, but we + # don't want to differentiate wrt any free symbol in the upper or lower + # limits + # XXX remove this test for free_symbols when the default _eval_derivative is in + if isinstance(x, Symbol) and x not in self.free_symbols: + return S.Zero + + # get limits and the function + f, limits = self.function, list(self.limits) + + limit = limits.pop(-1) + + if limits: # f is the argument to a Sum + f = self.func(f, *limits) + + _, a, b = limit + if x in a.free_symbols or x in b.free_symbols: + return None + df = Derivative(f, x, evaluate=True) + rv = self.func(df, limit) + return rv + + def _eval_difference_delta(self, n, step): + k, _, upper = self.args[-1] + new_upper = upper.subs(n, n + step) + + if len(self.args) == 2: + f = self.args[0] + else: + f = self.func(*self.args[:-1]) + + return Sum(f, (k, upper + 1, new_upper)).doit() + + def _eval_simplify(self, **kwargs): + + function = self.function + + if kwargs.get('deep', True): + function = function.simplify(**kwargs) + + # split the function into adds + terms = Add.make_args(expand(function)) + s_t = [] # Sum Terms + o_t = [] # Other Terms + + for term in terms: + if term.has(Sum): + # if there is an embedded sum here + # it is of the form x * (Sum(whatever)) + # hence we make a Mul out of it, and simplify all interior sum terms + subterms = Mul.make_args(expand(term)) + out_terms = [] + for subterm in subterms: + # go through each term + if isinstance(subterm, Sum): + # if it's a sum, simplify it + out_terms.append(subterm._eval_simplify(**kwargs)) + else: + # otherwise, add it as is + out_terms.append(subterm) + + # turn it back into a Mul + s_t.append(Mul(*out_terms)) + else: + o_t.append(term) + + # next try to combine any interior sums for further simplification + from sympy.simplify.simplify import factor_sum, sum_combine + result = Add(sum_combine(s_t), *o_t) + + return factor_sum(result, limits=self.limits) + + def is_convergent(self): + r""" + Checks for the convergence of a Sum. + + Explanation + =========== + + We divide the study of convergence of infinite sums and products in + two parts. + + First Part: + One part is the question whether all the terms are well defined, i.e., + they are finite in a sum and also non-zero in a product. Zero + is the analogy of (minus) infinity in products as + :math:`e^{-\infty} = 0`. + + Second Part: + The second part is the question of convergence after infinities, + and zeros in products, have been omitted assuming that their number + is finite. This means that we only consider the tail of the sum or + product, starting from some point after which all terms are well + defined. + + For example, in a sum of the form: + + .. math:: + + \sum_{1 \leq i < \infty} \frac{1}{n^2 + an + b} + + where a and b are numbers. The routine will return true, even if there + are infinities in the term sequence (at most two). An analogous + product would be: + + .. math:: + + \prod_{1 \leq i < \infty} e^{\frac{1}{n^2 + an + b}} + + This is how convergence is interpreted. It is concerned with what + happens at the limit. Finding the bad terms is another independent + matter. + + Note: It is responsibility of user to see that the sum or product + is well defined. + + There are various tests employed to check the convergence like + divergence test, root test, integral test, alternating series test, + comparison tests, Dirichlet tests. It returns true if Sum is convergent + and false if divergent and NotImplementedError if it cannot be checked. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Convergence_tests + + Examples + ======== + + >>> from sympy import factorial, S, Sum, Symbol, oo + >>> n = Symbol('n', integer=True) + >>> Sum(n/(n - 1), (n, 4, 7)).is_convergent() + True + >>> Sum(n/(2*n + 1), (n, 1, oo)).is_convergent() + False + >>> Sum(factorial(n)/5**n, (n, 1, oo)).is_convergent() + False + >>> Sum(1/n**(S(6)/5), (n, 1, oo)).is_convergent() + True + + See Also + ======== + + Sum.is_absolutely_convergent + sympy.concrete.products.Product.is_convergent + """ + p, q, r = symbols('p q r', cls=Wild) + + sym = self.limits[0][0] + lower_limit = self.limits[0][1] + upper_limit = self.limits[0][2] + sequence_term = self.function.simplify() + + if len(sequence_term.free_symbols) > 1: + raise NotImplementedError("convergence checking for more than one symbol " + "containing series is not handled") + + if lower_limit.is_finite and upper_limit.is_finite: + return S.true + + # transform sym -> -sym and swap the upper_limit = S.Infinity + # and lower_limit = - upper_limit + if lower_limit is S.NegativeInfinity: + if upper_limit is S.Infinity: + return Sum(sequence_term, (sym, 0, S.Infinity)).is_convergent() and \ + Sum(sequence_term, (sym, S.NegativeInfinity, 0)).is_convergent() + from sympy.simplify.simplify import simplify + sequence_term = simplify(sequence_term.xreplace({sym: -sym})) + lower_limit = -upper_limit + upper_limit = S.Infinity + + sym_ = Dummy(sym.name, integer=True, positive=True) + sequence_term = sequence_term.xreplace({sym: sym_}) + sym = sym_ + + interval = Interval(lower_limit, upper_limit) + + # Piecewise function handle + if sequence_term.is_Piecewise: + for func, cond in sequence_term.args: + # see if it represents something going to oo + if cond == True or cond.as_set().sup is S.Infinity: + s = Sum(func, (sym, lower_limit, upper_limit)) + return s.is_convergent() + return S.true + + ### -------- Divergence test ----------- ### + try: + lim_val = limit_seq(sequence_term, sym) + if lim_val is not None and lim_val.is_zero is False: + return S.false + except NotImplementedError: + pass + + try: + lim_val_abs = limit_seq(abs(sequence_term), sym) + if lim_val_abs is not None and lim_val_abs.is_zero is False: + return S.false + except NotImplementedError: + pass + + order = O(sequence_term, (sym, S.Infinity)) + + ### --------- p-series test (1/n**p) ---------- ### + p_series_test = order.expr.match(sym**p) + if p_series_test is not None: + if p_series_test[p] < -1: + return S.true + if p_series_test[p] >= -1: + return S.false + + ### ------------- comparison test ------------- ### + # 1/(n**p*log(n)**q*log(log(n))**r) comparison + n_log_test = (order.expr.match(1/(sym**p*log(1/sym)**q*log(-log(1/sym))**r)) or + order.expr.match(1/(sym**p*(-log(1/sym))**q*log(-log(1/sym))**r))) + if n_log_test is not None: + if (n_log_test[p] > 1 or + (n_log_test[p] == 1 and n_log_test[q] > 1) or + (n_log_test[p] == n_log_test[q] == 1 and n_log_test[r] > 1)): + return S.true + return S.false + + ### ------------- Limit comparison test -----------### + # (1/n) comparison + try: + lim_comp = limit_seq(sym*sequence_term, sym) + if lim_comp is not None and lim_comp.is_number and lim_comp > 0: + return S.false + except NotImplementedError: + pass + + ### ----------- ratio test ---------------- ### + next_sequence_term = sequence_term.xreplace({sym: sym + 1}) + from sympy.simplify.combsimp import combsimp + from sympy.simplify.powsimp import powsimp + ratio = combsimp(powsimp(next_sequence_term/sequence_term)) + try: + lim_ratio = limit_seq(ratio, sym) + if lim_ratio is not None and lim_ratio.is_number and lim_ratio is not S.NaN: + if abs(lim_ratio) > 1: + return S.false + if abs(lim_ratio) < 1: + return S.true + except NotImplementedError: + lim_ratio = None + + ### ---------- Raabe's test -------------- ### + if lim_ratio == 1: # ratio test inconclusive + test_val = sym*(sequence_term/ + sequence_term.subs(sym, sym + 1) - 1) + test_val = test_val.gammasimp() + try: + lim_val = limit_seq(test_val, sym) + if lim_val is not None and lim_val.is_number: + if lim_val > 1: + return S.true + if lim_val < 1: + return S.false + except NotImplementedError: + pass + + ### ----------- root test ---------------- ### + # lim = Limit(abs(sequence_term)**(1/sym), sym, S.Infinity) + try: + lim_evaluated = limit_seq(abs(sequence_term)**(1/sym), sym) + if lim_evaluated is not None and lim_evaluated.is_number: + if lim_evaluated < 1: + return S.true + if lim_evaluated > 1: + return S.false + except NotImplementedError: + pass + + ### ------------- alternating series test ----------- ### + dict_val = sequence_term.match(S.NegativeOne**(sym + p)*q) + if not dict_val[p].has(sym) and is_decreasing(dict_val[q], interval): + return S.true + + ### ------------- integral test -------------- ### + check_interval = None + from sympy.solvers.solveset import solveset + maxima = solveset(sequence_term.diff(sym), sym, interval) + if not maxima: + check_interval = interval + elif isinstance(maxima, FiniteSet) and maxima.sup.is_number: + check_interval = Interval(maxima.sup, interval.sup) + if (check_interval is not None and + (is_decreasing(sequence_term, check_interval) or + is_decreasing(-sequence_term, check_interval))): + integral_val = Integral( + sequence_term, (sym, lower_limit, upper_limit)) + try: + integral_val_evaluated = integral_val.doit() + if integral_val_evaluated.is_number: + return S(integral_val_evaluated.is_finite) + except NotImplementedError: + pass + + ### ----- Dirichlet and bounded times convergent tests ----- ### + # TODO + # + # Dirichlet_test + # https://en.wikipedia.org/wiki/Dirichlet%27s_test + # + # Bounded times convergent test + # It is based on comparison theorems for series. + # In particular, if the general term of a series can + # be written as a product of two terms a_n and b_n + # and if a_n is bounded and if Sum(b_n) is absolutely + # convergent, then the original series Sum(a_n * b_n) + # is absolutely convergent and so convergent. + # + # The following code can grows like 2**n where n is the + # number of args in order.expr + # Possibly combined with the potentially slow checks + # inside the loop, could make this test extremely slow + # for larger summation expressions. + + if order.expr.is_Mul: + args = order.expr.args + argset = set(args) + + ### -------------- Dirichlet tests -------------- ### + m = Dummy('m', integer=True) + def _dirichlet_test(g_n): + try: + ing_val = limit_seq(Sum(g_n, (sym, interval.inf, m)).doit(), m) + if ing_val is not None and ing_val.is_finite: + return S.true + except NotImplementedError: + pass + + ### -------- bounded times convergent test ---------### + def _bounded_convergent_test(g1_n, g2_n): + try: + lim_val = limit_seq(g1_n, sym) + if lim_val is not None and (lim_val.is_finite or ( + isinstance(lim_val, AccumulationBounds) + and (lim_val.max - lim_val.min).is_finite)): + if Sum(g2_n, (sym, lower_limit, upper_limit)).is_absolutely_convergent(): + return S.true + except NotImplementedError: + pass + + for n in range(1, len(argset)): + for a_tuple in itertools.combinations(args, n): + b_set = argset - set(a_tuple) + a_n = Mul(*a_tuple) + b_n = Mul(*b_set) + + if is_decreasing(a_n, interval): + dirich = _dirichlet_test(b_n) + if dirich is not None: + return dirich + + bc_test = _bounded_convergent_test(a_n, b_n) + if bc_test is not None: + return bc_test + + _sym = self.limits[0][0] + sequence_term = sequence_term.xreplace({sym: _sym}) + raise NotImplementedError("The algorithm to find the Sum convergence of %s " + "is not yet implemented" % (sequence_term)) + + def is_absolutely_convergent(self): + """ + Checks for the absolute convergence of an infinite series. + + Same as checking convergence of absolute value of sequence_term of + an infinite series. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Absolute_convergence + + Examples + ======== + + >>> from sympy import Sum, Symbol, oo + >>> n = Symbol('n', integer=True) + >>> Sum((-1)**n, (n, 1, oo)).is_absolutely_convergent() + False + >>> Sum((-1)**n/n**2, (n, 1, oo)).is_absolutely_convergent() + True + + See Also + ======== + + Sum.is_convergent + """ + return Sum(abs(self.function), self.limits).is_convergent() + + def euler_maclaurin(self, m=0, n=0, eps=0, eval_integral=True): + """ + Return an Euler-Maclaurin approximation of self, where m is the + number of leading terms to sum directly and n is the number of + terms in the tail. + + With m = n = 0, this is simply the corresponding integral + plus a first-order endpoint correction. + + Returns (s, e) where s is the Euler-Maclaurin approximation + and e is the estimated error (taken to be the magnitude of + the first omitted term in the tail): + + >>> from sympy.abc import k, a, b + >>> from sympy import Sum + >>> Sum(1/k, (k, 2, 5)).doit().evalf() + 1.28333333333333 + >>> s, e = Sum(1/k, (k, 2, 5)).euler_maclaurin() + >>> s + -log(2) + 7/20 + log(5) + >>> from sympy import sstr + >>> print(sstr((s.evalf(), e.evalf()), full_prec=True)) + (1.26629073187415, 0.0175000000000000) + + The endpoints may be symbolic: + + >>> s, e = Sum(1/k, (k, a, b)).euler_maclaurin() + >>> s + -log(a) + log(b) + 1/(2*b) + 1/(2*a) + >>> e + Abs(1/(12*b**2) - 1/(12*a**2)) + + If the function is a polynomial of degree at most 2n+1, the + Euler-Maclaurin formula becomes exact (and e = 0 is returned): + + >>> Sum(k, (k, 2, b)).euler_maclaurin() + (b**2/2 + b/2 - 1, 0) + >>> Sum(k, (k, 2, b)).doit() + b**2/2 + b/2 - 1 + + With a nonzero eps specified, the summation is ended + as soon as the remainder term is less than the epsilon. + """ + m = int(m) + n = int(n) + f = self.function + if len(self.limits) != 1: + raise ValueError("More than 1 limit") + i, a, b = self.limits[0] + if (a > b) == True: + if a - b == 1: + return S.Zero, S.Zero + a, b = b + 1, a - 1 + f = -f + s = S.Zero + if m: + if b.is_Integer and a.is_Integer: + m = min(m, b - a + 1) + if not eps or f.is_polynomial(i): + s = Add(*[f.subs(i, a + k) for k in range(m)]) + else: + term = f.subs(i, a) + if term: + test = abs(term.evalf(3)) < eps + if test == True: + return s, abs(term) + elif not (test == False): + # a symbolic Relational class, can't go further + return term, S.Zero + s = term + for k in range(1, m): + term = f.subs(i, a + k) + if abs(term.evalf(3)) < eps and term != 0: + return s, abs(term) + s += term + if b - a + 1 == m: + return s, S.Zero + a += m + x = Dummy('x') + I = Integral(f.subs(i, x), (x, a, b)) + if eval_integral: + I = I.doit() + s += I + + def fpoint(expr): + if b is S.Infinity: + return expr.subs(i, a), 0 + return expr.subs(i, a), expr.subs(i, b) + fa, fb = fpoint(f) + iterm = (fa + fb)/2 + g = f.diff(i) + for k in range(1, n + 2): + ga, gb = fpoint(g) + term = bernoulli(2*k)/factorial(2*k)*(gb - ga) + if k > n: + break + if eps and term: + term_evalf = term.evalf(3) + if term_evalf is S.NaN: + return S.NaN, S.NaN + if abs(term_evalf) < eps: + break + s += term + g = g.diff(i, 2, simplify=False) + return s + iterm, abs(term) + + + def reverse_order(self, *indices): + """ + Reverse the order of a limit in a Sum. + + Explanation + =========== + + ``reverse_order(self, *indices)`` reverses some limits in the expression + ``self`` which can be either a ``Sum`` or a ``Product``. The selectors in + the argument ``indices`` specify some indices whose limits get reversed. + These selectors are either variable names or numerical indices counted + starting from the inner-most limit tuple. + + Examples + ======== + + >>> from sympy import Sum + >>> from sympy.abc import x, y, a, b, c, d + + >>> Sum(x, (x, 0, 3)).reverse_order(x) + Sum(-x, (x, 4, -1)) + >>> Sum(x*y, (x, 1, 5), (y, 0, 6)).reverse_order(x, y) + Sum(x*y, (x, 6, 0), (y, 7, -1)) + >>> Sum(x, (x, a, b)).reverse_order(x) + Sum(-x, (x, b + 1, a - 1)) + >>> Sum(x, (x, a, b)).reverse_order(0) + Sum(-x, (x, b + 1, a - 1)) + + While one should prefer variable names when specifying which limits + to reverse, the index counting notation comes in handy in case there + are several symbols with the same name. + + >>> S = Sum(x**2, (x, a, b), (x, c, d)) + >>> S + Sum(x**2, (x, a, b), (x, c, d)) + >>> S0 = S.reverse_order(0) + >>> S0 + Sum(-x**2, (x, b + 1, a - 1), (x, c, d)) + >>> S1 = S0.reverse_order(1) + >>> S1 + Sum(x**2, (x, b + 1, a - 1), (x, d + 1, c - 1)) + + Of course we can mix both notations: + + >>> Sum(x*y, (x, a, b), (y, 2, 5)).reverse_order(x, 1) + Sum(x*y, (x, b + 1, a - 1), (y, 6, 1)) + >>> Sum(x*y, (x, a, b), (y, 2, 5)).reverse_order(y, x) + Sum(x*y, (x, b + 1, a - 1), (y, 6, 1)) + + See Also + ======== + + sympy.concrete.expr_with_intlimits.ExprWithIntLimits.index, reorder_limit, + sympy.concrete.expr_with_intlimits.ExprWithIntLimits.reorder + + References + ========== + + .. [1] Michael Karr, "Summation in Finite Terms", Journal of the ACM, + Volume 28 Issue 2, April 1981, Pages 305-350 + https://dl.acm.org/doi/10.1145/322248.322255 + """ + l_indices = list(indices) + + for i, indx in enumerate(l_indices): + if not isinstance(indx, int): + l_indices[i] = self.index(indx) + + e = 1 + limits = [] + for i, limit in enumerate(self.limits): + l = limit + if i in l_indices: + e = -e + l = (limit[0], limit[2] + 1, limit[1] - 1) + limits.append(l) + + return Sum(e * self.function, *limits) + + def _eval_rewrite_as_Product(self, *args, **kwargs): + from sympy.concrete.products import Product + if self.function.is_extended_real: + return log(Product(exp(self.function), *self.limits)) + + +def summation(f, *symbols, **kwargs): + r""" + Compute the summation of f with respect to symbols. + + Explanation + =========== + + The notation for symbols is similar to the notation used in Integral. + summation(f, (i, a, b)) computes the sum of f with respect to i from a to b, + i.e., + + :: + + b + ____ + \ ` + summation(f, (i, a, b)) = ) f + /___, + i = a + + If it cannot compute the sum, it returns an unevaluated Sum object. + Repeated sums can be computed by introducing additional symbols tuples:: + + Examples + ======== + + >>> from sympy import summation, oo, symbols, log + >>> i, n, m = symbols('i n m', integer=True) + + >>> summation(2*i - 1, (i, 1, n)) + n**2 + >>> summation(1/2**i, (i, 0, oo)) + 2 + >>> summation(1/log(n)**n, (n, 2, oo)) + Sum(log(n)**(-n), (n, 2, oo)) + >>> summation(i, (i, 0, n), (n, 0, m)) + m**3/6 + m**2/2 + m/3 + + >>> from sympy.abc import x + >>> from sympy import factorial + >>> summation(x**n/factorial(n), (n, 0, oo)) + exp(x) + + See Also + ======== + + Sum + Product, sympy.concrete.products.product + + """ + return Sum(f, *symbols, **kwargs).doit(deep=False) + + +def telescopic_direct(L, R, n, limits): + """ + Returns the direct summation of the terms of a telescopic sum + + Explanation + =========== + + L is the term with lower index + R is the term with higher index + n difference between the indexes of L and R + + Examples + ======== + + >>> from sympy.concrete.summations import telescopic_direct + >>> from sympy.abc import k, a, b + >>> telescopic_direct(1/k, -1/(k+2), 2, (k, a, b)) + -1/(b + 2) - 1/(b + 1) + 1/(a + 1) + 1/a + + """ + (i, a, b) = limits + return Add(*[L.subs(i, a + m) + R.subs(i, b - m) for m in range(n)]) + + +def telescopic(L, R, limits): + ''' + Tries to perform the summation using the telescopic property. + + Return None if not possible. + ''' + (i, a, b) = limits + if L.is_Add or R.is_Add: + return None + + # We want to solve(L.subs(i, i + m) + R, m) + # First we try a simple match since this does things that + # solve doesn't do, e.g. solve(cos(k+m)-cos(k), m) gives + # a more complicated solution than m == 0. + + k = Wild("k") + sol = (-R).match(L.subs(i, i + k)) + s = None + if sol and k in sol: + s = sol[k] + if not (s.is_Integer and L.subs(i, i + s) + R == 0): + # invalid match or match didn't work + s = None + + # But there are things that match doesn't do that solve + # can do, e.g. determine that 1/(x + m) = 1/(1 - x) when m = 1 + + if s is None: + m = Dummy('m') + try: + from sympy.solvers.solvers import solve + sol = solve(L.subs(i, i + m) + R, m) or [] + except NotImplementedError: + return None + sol = [si for si in sol if si.is_Integer and + (L.subs(i, i + si) + R).expand().is_zero] + if len(sol) != 1: + return None + s = sol[0] + + if s < 0: + return telescopic_direct(R, L, abs(s), (i, a, b)) + elif s > 0: + return telescopic_direct(L, R, s, (i, a, b)) + + +def eval_sum(f, limits): + (i, a, b) = limits + if f.is_zero: + return S.Zero + if i not in f.free_symbols: + return f*(b - a + 1) + if a == b: + return f.subs(i, a) + if a.is_comparable and b.is_comparable and a > b: + return eval_sum(f, (i, b + S.One, a - S.One)) + if isinstance(f, Piecewise): + if not any(i in arg.args[1].free_symbols for arg in f.args): + # Piecewise conditions do not depend on the dummy summation variable, + # therefore we can fold: Sum(Piecewise((e, c), ...), limits) + # --> Piecewise((Sum(e, limits), c), ...) + newargs = [] + for arg in f.args: + newexpr = eval_sum(arg.expr, limits) + if newexpr is None: + return None + newargs.append((newexpr, arg.cond)) + return f.func(*newargs) + + if f.has(KroneckerDelta): + from .delta import deltasummation, _has_simple_delta + f = f.replace( + lambda x: isinstance(x, Sum), + lambda x: x.factor() + ) + if _has_simple_delta(f, limits[0]): + return deltasummation(f, limits) + + dif = b - a + definite = dif.is_Integer + # Doing it directly may be faster if there are very few terms. + if definite and (dif < 100): + return eval_sum_direct(f, (i, a, b)) + if isinstance(f, Piecewise): + return None + # Try to do it symbolically. Even when the number of terms is + # known, this can save time when b-a is big. + value = eval_sum_symbolic(f.expand(), (i, a, b)) + if value is not None: + return value + # Do it directly + if definite: + return eval_sum_direct(f, (i, a, b)) + + +def eval_sum_direct(expr, limits): + """ + Evaluate expression directly, but perform some simple checks first + to possibly result in a smaller expression and faster execution. + """ + (i, a, b) = limits + + dif = b - a + # Linearity + if expr.is_Mul: + # Try factor out everything not including i + without_i, with_i = expr.as_independent(i) + if without_i != 1: + s = eval_sum_direct(with_i, (i, a, b)) + if s: + r = without_i*s + if r is not S.NaN: + return r + else: + # Try term by term + L, R = expr.as_two_terms() + + if not L.has(i): + sR = eval_sum_direct(R, (i, a, b)) + if sR: + return L*sR + + if not R.has(i): + sL = eval_sum_direct(L, (i, a, b)) + if sL: + return sL*R + + # do this whether its an Add or Mul + # e.g. apart(1/(25*i**2 + 45*i + 14)) and + # apart(1/((5*i + 2)*(5*i + 7))) -> + # -1/(5*(5*i + 7)) + 1/(5*(5*i + 2)) + try: + expr = apart(expr, i) # see if it becomes an Add + except PolynomialError: + pass + + if expr.is_Add: + # Try factor out everything not including i + without_i, with_i = expr.as_independent(i) + if without_i != 0: + s = eval_sum_direct(with_i, (i, a, b)) + if s: + r = without_i*(dif + 1) + s + if r is not S.NaN: + return r + else: + # Try term by term + L, R = expr.as_two_terms() + lsum = eval_sum_direct(L, (i, a, b)) + rsum = eval_sum_direct(R, (i, a, b)) + + if None not in (lsum, rsum): + r = lsum + rsum + if r is not S.NaN: + return r + + return Add(*[expr.subs(i, a + j) for j in range(dif + 1)]) + + +def eval_sum_symbolic(f, limits): + f_orig = f + (i, a, b) = limits + if not f.has(i): + return f*(b - a + 1) + + # Linearity + if f.is_Mul: + # Try factor out everything not including i + without_i, with_i = f.as_independent(i) + if without_i != 1: + s = eval_sum_symbolic(with_i, (i, a, b)) + if s: + r = without_i*s + if r is not S.NaN: + return r + else: + # Try term by term + L, R = f.as_two_terms() + + if not L.has(i): + sR = eval_sum_symbolic(R, (i, a, b)) + if sR: + return L*sR + + if not R.has(i): + sL = eval_sum_symbolic(L, (i, a, b)) + if sL: + return sL*R + + # do this whether its an Add or Mul + # e.g. apart(1/(25*i**2 + 45*i + 14)) and + # apart(1/((5*i + 2)*(5*i + 7))) -> + # -1/(5*(5*i + 7)) + 1/(5*(5*i + 2)) + try: + f = apart(f, i) + except PolynomialError: + pass + + if f.is_Add: + L, R = f.as_two_terms() + lrsum = telescopic(L, R, (i, a, b)) + + if lrsum: + return lrsum + + # Try factor out everything not including i + without_i, with_i = f.as_independent(i) + if without_i != 0: + s = eval_sum_symbolic(with_i, (i, a, b)) + if s: + r = without_i*(b - a + 1) + s + if r is not S.NaN: + return r + else: + # Try term by term + lsum = eval_sum_symbolic(L, (i, a, b)) + rsum = eval_sum_symbolic(R, (i, a, b)) + + if None not in (lsum, rsum): + r = lsum + rsum + if r is not S.NaN: + return r + + + # Polynomial terms with Faulhaber's formula + n = Wild('n') + result = f.match(i**n) + + if result is not None: + n = result[n] + + if n.is_Integer: + if n >= 0: + if (b is S.Infinity and a is not S.NegativeInfinity) or \ + (a is S.NegativeInfinity and b is not S.Infinity): + return S.Infinity + return ((bernoulli(n + 1, b + 1) - bernoulli(n + 1, a))/(n + 1)).expand() + elif a.is_Integer and a >= 1: + if n == -1: + return harmonic(b) - harmonic(a - 1) + else: + return harmonic(b, abs(n)) - harmonic(a - 1, abs(n)) + + if not (a.has(S.Infinity, S.NegativeInfinity) or + b.has(S.Infinity, S.NegativeInfinity)): + # Geometric terms + c1 = Wild('c1', exclude=[i]) + c2 = Wild('c2', exclude=[i]) + c3 = Wild('c3', exclude=[i]) + wexp = Wild('wexp') + + # Here we first attempt powsimp on f for easier matching with the + # exponential pattern, and attempt expansion on the exponent for easier + # matching with the linear pattern. + e = f.powsimp().match(c1 ** wexp) + if e is not None: + e_exp = e.pop(wexp).expand().match(c2*i + c3) + if e_exp is not None: + e.update(e_exp) + + p = (c1**c3).subs(e) + q = (c1**c2).subs(e) + r = p*(q**a - q**(b + 1))/(1 - q) + l = p*(b - a + 1) + return Piecewise((l, Eq(q, S.One)), (r, True)) + + r = gosper_sum(f, (i, a, b)) + + if isinstance(r, (Mul,Add)): + from sympy.simplify.radsimp import denom + from sympy.solvers.solvers import solve + non_limit = r.free_symbols - Tuple(*limits[1:]).free_symbols + den = denom(together(r)) + den_sym = non_limit & den.free_symbols + args = [] + for v in ordered(den_sym): + try: + s = solve(den, v) + m = Eq(v, s[0]) if s else S.false + if m != False: + args.append((Sum(f_orig.subs(*m.args), limits).doit(), m)) + break + except NotImplementedError: + continue + + args.append((r, True)) + return Piecewise(*args) + + if r not in (None, S.NaN): + return r + + h = eval_sum_hyper(f_orig, (i, a, b)) + if h is not None: + return h + + r = eval_sum_residue(f_orig, (i, a, b)) + if r is not None: + return r + + factored = f_orig.factor() + if factored != f_orig: + return eval_sum_symbolic(factored, (i, a, b)) + + +def _eval_sum_hyper(f, i, a): + """ Returns (res, cond). Sums from a to oo. """ + if a != 0: + return _eval_sum_hyper(f.subs(i, i + a), i, 0) + + if f.subs(i, 0) == 0: + from sympy.simplify.simplify import simplify + if simplify(f.subs(i, Dummy('i', integer=True, positive=True))) == 0: + return S.Zero, True + return _eval_sum_hyper(f.subs(i, i + 1), i, 0) + + from sympy.simplify.simplify import hypersimp + hs = hypersimp(f, i) + if hs is None: + return None + + if isinstance(hs, Float): + from sympy.simplify.simplify import nsimplify + hs = nsimplify(hs) + + from sympy.simplify.combsimp import combsimp + from sympy.simplify.hyperexpand import hyperexpand + from sympy.simplify.radsimp import fraction + numer, denom = fraction(factor(hs)) + top, topl = numer.as_coeff_mul(i) + bot, botl = denom.as_coeff_mul(i) + ab = [top, bot] + factors = [topl, botl] + params = [[], []] + for k in range(2): + for fac in factors[k]: + mul = 1 + if fac.is_Pow: + mul = fac.exp + fac = fac.base + if not mul.is_Integer: + return None + p = Poly(fac, i) + if p.degree() != 1: + return None + m, n = p.all_coeffs() + ab[k] *= m**mul + params[k] += [n/m]*mul + + # Add "1" to numerator parameters, to account for implicit n! in + # hypergeometric series. + ap = params[0] + [1] + bq = params[1] + x = ab[0]/ab[1] + h = hyper(ap, bq, x) + f = combsimp(f) + return f.subs(i, 0)*hyperexpand(h), h.convergence_statement + + +def eval_sum_hyper(f, i_a_b): + i, a, b = i_a_b + + if f.is_hypergeometric(i) is False: + return + + if (b - a).is_Integer: + # We are never going to do better than doing the sum in the obvious way + return None + + old_sum = Sum(f, (i, a, b)) + + if b != S.Infinity: + if a is S.NegativeInfinity: + res = _eval_sum_hyper(f.subs(i, -i), i, -b) + if res is not None: + return Piecewise(res, (old_sum, True)) + else: + n_illegal = lambda x: sum(x.count(_) for _ in _illegal) + had = n_illegal(f) + # check that no extra illegals are introduced + res1 = _eval_sum_hyper(f, i, a) + if res1 is None or n_illegal(res1) > had: + return + res2 = _eval_sum_hyper(f, i, b + 1) + if res2 is None or n_illegal(res2) > had: + return + (res1, cond1), (res2, cond2) = res1, res2 + cond = And(cond1, cond2) + if cond == False: + return None + return Piecewise((res1 - res2, cond), (old_sum, True)) + + if a is S.NegativeInfinity: + res1 = _eval_sum_hyper(f.subs(i, -i), i, 1) + res2 = _eval_sum_hyper(f, i, 0) + if res1 is None or res2 is None: + return None + res1, cond1 = res1 + res2, cond2 = res2 + cond = And(cond1, cond2) + if cond == False or cond.as_set() == S.EmptySet: + return None + return Piecewise((res1 + res2, cond), (old_sum, True)) + + # Now b == oo, a != -oo + res = _eval_sum_hyper(f, i, a) + if res is not None: + r, c = res + if c == False: + if r.is_number: + f = f.subs(i, Dummy('i', integer=True, positive=True) + a) + if f.is_positive or f.is_zero: + return S.Infinity + elif f.is_negative: + return S.NegativeInfinity + return None + return Piecewise(res, (old_sum, True)) + + +def eval_sum_residue(f, i_a_b): + r"""Compute the infinite summation with residues + + Notes + ===== + + If $f(n), g(n)$ are polynomials with $\deg(g(n)) - \deg(f(n)) \ge 2$, + some infinite summations can be computed by the following residue + evaluations. + + .. math:: + \sum_{n=-\infty, g(n) \ne 0}^{\infty} \frac{f(n)}{g(n)} = + -\pi \sum_{\alpha|g(\alpha)=0} + \text{Res}(\cot(\pi x) \frac{f(x)}{g(x)}, \alpha) + + .. math:: + \sum_{n=-\infty, g(n) \ne 0}^{\infty} (-1)^n \frac{f(n)}{g(n)} = + -\pi \sum_{\alpha|g(\alpha)=0} + \text{Res}(\csc(\pi x) \frac{f(x)}{g(x)}, \alpha) + + Examples + ======== + + >>> from sympy import Sum, oo, Symbol + >>> x = Symbol('x') + + Doubly infinite series of rational functions. + + >>> Sum(1 / (x**2 + 1), (x, -oo, oo)).doit() + pi/tanh(pi) + + Doubly infinite alternating series of rational functions. + + >>> Sum((-1)**x / (x**2 + 1), (x, -oo, oo)).doit() + pi/sinh(pi) + + Infinite series of even rational functions. + + >>> Sum(1 / (x**2 + 1), (x, 0, oo)).doit() + 1/2 + pi/(2*tanh(pi)) + + Infinite series of alternating even rational functions. + + >>> Sum((-1)**x / (x**2 + 1), (x, 0, oo)).doit() + pi/(2*sinh(pi)) + 1/2 + + This also have heuristics to transform arbitrarily shifted summand or + arbitrarily shifted summation range to the canonical problem the + formula can handle. + + >>> Sum(1 / (x**2 + 2*x + 2), (x, -1, oo)).doit() + 1/2 + pi/(2*tanh(pi)) + >>> Sum(1 / (x**2 + 4*x + 5), (x, -2, oo)).doit() + 1/2 + pi/(2*tanh(pi)) + >>> Sum(1 / (x**2 + 1), (x, 1, oo)).doit() + -1/2 + pi/(2*tanh(pi)) + >>> Sum(1 / (x**2 + 1), (x, 2, oo)).doit() + -1 + pi/(2*tanh(pi)) + + References + ========== + + .. [#] http://www.supermath.info/InfiniteSeriesandtheResidueTheorem.pdf + + .. [#] Asmar N.H., Grafakos L. (2018) Residue Theory. + In: Complex Analysis with Applications. + Undergraduate Texts in Mathematics. Springer, Cham. + https://doi.org/10.1007/978-3-319-94063-2_5 + """ + i, a, b = i_a_b + + # If lower limit > upper limit: Karr Summation Convention + if a.is_comparable and b.is_comparable and a > b: + return eval_sum_residue(f, (i, b + S.One, a - S.One)) + + def is_even_function(numer, denom): + """Test if the rational function is an even function""" + numer_even = all(i % 2 == 0 for (i,) in numer.monoms()) + denom_even = all(i % 2 == 0 for (i,) in denom.monoms()) + numer_odd = all(i % 2 == 1 for (i,) in numer.monoms()) + denom_odd = all(i % 2 == 1 for (i,) in denom.monoms()) + return (numer_even and denom_even) or (numer_odd and denom_odd) + + def match_rational(f, i): + numer, denom = f.as_numer_denom() + try: + (numer, denom), opt = parallel_poly_from_expr((numer, denom), i) + except (PolificationFailed, PolynomialError): + return None + return numer, denom + + def get_poles(denom): + roots = denom.sqf_part().all_roots() + roots = sift(roots, lambda x: x.is_integer) + if None in roots: + return None + int_roots, nonint_roots = roots[True], roots[False] + return int_roots, nonint_roots + + def get_shift(denom): + n = denom.degree(i) + a = denom.coeff_monomial(i**n) + b = denom.coeff_monomial(i**(n-1)) + shift = - b / a / n + return shift + + #Need a dummy symbol with no assumptions set for get_residue_factor + z = Dummy('z') + + def get_residue_factor(numer, denom, alternating): + residue_factor = (numer.as_expr() / denom.as_expr()).subs(i, z) + if not alternating: + residue_factor *= cot(S.Pi * z) + else: + residue_factor *= csc(S.Pi * z) + return residue_factor + + # We don't know how to deal with symbolic constants in summand + if f.free_symbols - {i}: + return None + + if not (a.is_Integer or a in (S.Infinity, S.NegativeInfinity)): + return None + if not (b.is_Integer or b in (S.Infinity, S.NegativeInfinity)): + return None + + # Quick exit heuristic for the sums which doesn't have infinite range + if a != S.NegativeInfinity and b != S.Infinity: + return None + + match = match_rational(f, i) + if match: + alternating = False + numer, denom = match + else: + match = match_rational(f / S.NegativeOne**i, i) + if match: + alternating = True + numer, denom = match + else: + return None + + if denom.degree(i) - numer.degree(i) < 2: + return None + + if (a, b) == (S.NegativeInfinity, S.Infinity): + poles = get_poles(denom) + if poles is None: + return None + int_roots, nonint_roots = poles + + if int_roots: + return None + + residue_factor = get_residue_factor(numer, denom, alternating) + residues = [residue(residue_factor, z, root) for root in nonint_roots] + return -S.Pi * sum(residues) + + if not (a.is_finite and b is S.Infinity): + return None + + if not is_even_function(numer, denom): + # Try shifting summation and check if the summand can be made + # and even function from the origin. + # Sum(f(n), (n, a, b)) => Sum(f(n + s), (n, a - s, b - s)) + shift = get_shift(denom) + + if not shift.is_Integer: + return None + if shift == 0: + return None + + numer = numer.shift(shift) + denom = denom.shift(shift) + + if not is_even_function(numer, denom): + return None + + if alternating: + f = S.NegativeOne**i * (S.NegativeOne**shift * numer.as_expr() / denom.as_expr()) + else: + f = numer.as_expr() / denom.as_expr() + return eval_sum_residue(f, (i, a-shift, b-shift)) + + poles = get_poles(denom) + if poles is None: + return None + int_roots, nonint_roots = poles + + if int_roots: + int_roots = [int(root) for root in int_roots] + int_roots_max = max(int_roots) + int_roots_min = min(int_roots) + # Integer valued poles must be next to each other + # and also symmetric from origin (Because the function is even) + if not len(int_roots) == int_roots_max - int_roots_min + 1: + return None + + # Check whether the summation indices contain poles + if a <= max(int_roots): + return None + + residue_factor = get_residue_factor(numer, denom, alternating) + residues = [residue(residue_factor, z, root) for root in int_roots + nonint_roots] + full_sum = -S.Pi * sum(residues) + + if not int_roots: + # Compute Sum(f, (i, 0, oo)) by adding a extraneous evaluation + # at the origin. + half_sum = (full_sum + f.xreplace({i: 0})) / 2 + + # Add and subtract extraneous evaluations + extraneous_neg = [f.xreplace({i: i0}) for i0 in range(int(a), 0)] + extraneous_pos = [f.xreplace({i: i0}) for i0 in range(0, int(a))] + result = half_sum + sum(extraneous_neg) - sum(extraneous_pos) + + return result + + # Compute Sum(f, (i, min(poles) + 1, oo)) + half_sum = full_sum / 2 + + # Subtract extraneous evaluations + extraneous = [f.xreplace({i: i0}) for i0 in range(max(int_roots) + 1, int(a))] + result = half_sum - sum(extraneous) + + return result + + +def _eval_matrix_sum(expression): + f = expression.function + for limit in expression.limits: + i, a, b = limit + dif = b - a + if dif.is_Integer: + if (dif < 0) == True: + a, b = b + 1, a - 1 + f = -f + + newf = eval_sum_direct(f, (i, a, b)) + if newf is not None: + return newf.doit() + + +def _dummy_with_inherited_properties_concrete(limits): + """ + Return a Dummy symbol that inherits as many assumptions as possible + from the provided symbol and limits. + + If the symbol already has all True assumption shared by the limits + then return None. + """ + x, a, b = limits + l = [a, b] + + assumptions_to_consider = ['extended_nonnegative', 'nonnegative', + 'extended_nonpositive', 'nonpositive', + 'extended_positive', 'positive', + 'extended_negative', 'negative', + 'integer', 'rational', 'finite', + 'zero', 'real', 'extended_real'] + + assumptions_to_keep = {} + assumptions_to_add = {} + for assum in assumptions_to_consider: + assum_true = x._assumptions.get(assum, None) + if assum_true: + assumptions_to_keep[assum] = True + elif all(getattr(i, 'is_' + assum) for i in l): + assumptions_to_add[assum] = True + if assumptions_to_add: + assumptions_to_keep.update(assumptions_to_add) + return Dummy('d', **assumptions_to_keep) diff --git a/venv/lib/python3.11/site-packages/sympy/concrete/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/concrete/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_delta.py b/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_delta.py new file mode 100644 index 0000000000000000000000000000000000000000..9dc6e88d16346acc7dc775446d7de3f3696d0e03 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_delta.py @@ -0,0 +1,499 @@ +from sympy.concrete import Sum +from sympy.concrete.delta import deltaproduct as dp, deltasummation as ds, _extract_delta +from sympy.core import Eq, S, symbols, oo +from sympy.functions import KroneckerDelta as KD, Piecewise, piecewise_fold +from sympy.logic import And +from sympy.testing.pytest import raises + +i, j, k, l, m = symbols("i j k l m", integer=True, finite=True) +x, y = symbols("x y", commutative=False) + + +def test_deltaproduct_trivial(): + assert dp(x, (j, 1, 0)) == 1 + assert dp(x, (j, 1, 3)) == x**3 + assert dp(x + y, (j, 1, 3)) == (x + y)**3 + assert dp(x*y, (j, 1, 3)) == (x*y)**3 + assert dp(KD(i, j), (k, 1, 3)) == KD(i, j) + assert dp(x*KD(i, j), (k, 1, 3)) == x**3*KD(i, j) + assert dp(x*y*KD(i, j), (k, 1, 3)) == (x*y)**3*KD(i, j) + + +def test_deltaproduct_basic(): + assert dp(KD(i, j), (j, 1, 3)) == 0 + assert dp(KD(i, j), (j, 1, 1)) == KD(i, 1) + assert dp(KD(i, j), (j, 2, 2)) == KD(i, 2) + assert dp(KD(i, j), (j, 3, 3)) == KD(i, 3) + assert dp(KD(i, j), (j, 1, k)) == KD(i, 1)*KD(k, 1) + KD(k, 0) + assert dp(KD(i, j), (j, k, 3)) == KD(i, 3)*KD(k, 3) + KD(k, 4) + assert dp(KD(i, j), (j, k, l)) == KD(i, l)*KD(k, l) + KD(k, l + 1) + + +def test_deltaproduct_mul_x_kd(): + assert dp(x*KD(i, j), (j, 1, 3)) == 0 + assert dp(x*KD(i, j), (j, 1, 1)) == x*KD(i, 1) + assert dp(x*KD(i, j), (j, 2, 2)) == x*KD(i, 2) + assert dp(x*KD(i, j), (j, 3, 3)) == x*KD(i, 3) + assert dp(x*KD(i, j), (j, 1, k)) == x*KD(i, 1)*KD(k, 1) + KD(k, 0) + assert dp(x*KD(i, j), (j, k, 3)) == x*KD(i, 3)*KD(k, 3) + KD(k, 4) + assert dp(x*KD(i, j), (j, k, l)) == x*KD(i, l)*KD(k, l) + KD(k, l + 1) + + +def test_deltaproduct_mul_add_x_y_kd(): + assert dp((x + y)*KD(i, j), (j, 1, 3)) == 0 + assert dp((x + y)*KD(i, j), (j, 1, 1)) == (x + y)*KD(i, 1) + assert dp((x + y)*KD(i, j), (j, 2, 2)) == (x + y)*KD(i, 2) + assert dp((x + y)*KD(i, j), (j, 3, 3)) == (x + y)*KD(i, 3) + assert dp((x + y)*KD(i, j), (j, 1, k)) == \ + (x + y)*KD(i, 1)*KD(k, 1) + KD(k, 0) + assert dp((x + y)*KD(i, j), (j, k, 3)) == \ + (x + y)*KD(i, 3)*KD(k, 3) + KD(k, 4) + assert dp((x + y)*KD(i, j), (j, k, l)) == \ + (x + y)*KD(i, l)*KD(k, l) + KD(k, l + 1) + + +def test_deltaproduct_add_kd_kd(): + assert dp(KD(i, k) + KD(j, k), (k, 1, 3)) == 0 + assert dp(KD(i, k) + KD(j, k), (k, 1, 1)) == KD(i, 1) + KD(j, 1) + assert dp(KD(i, k) + KD(j, k), (k, 2, 2)) == KD(i, 2) + KD(j, 2) + assert dp(KD(i, k) + KD(j, k), (k, 3, 3)) == KD(i, 3) + KD(j, 3) + assert dp(KD(i, k) + KD(j, k), (k, 1, l)) == KD(l, 0) + \ + KD(i, 1)*KD(l, 1) + KD(j, 1)*KD(l, 1) + \ + KD(i, 1)*KD(j, 2)*KD(l, 2) + KD(j, 1)*KD(i, 2)*KD(l, 2) + assert dp(KD(i, k) + KD(j, k), (k, l, 3)) == KD(l, 4) + \ + KD(i, 3)*KD(l, 3) + KD(j, 3)*KD(l, 3) + \ + KD(i, 2)*KD(j, 3)*KD(l, 2) + KD(i, 3)*KD(j, 2)*KD(l, 2) + assert dp(KD(i, k) + KD(j, k), (k, l, m)) == KD(l, m + 1) + \ + KD(i, m)*KD(l, m) + KD(j, m)*KD(l, m) + \ + KD(i, m)*KD(j, m - 1)*KD(l, m - 1) + KD(i, m - 1)*KD(j, m)*KD(l, m - 1) + + +def test_deltaproduct_mul_x_add_kd_kd(): + assert dp(x*(KD(i, k) + KD(j, k)), (k, 1, 3)) == 0 + assert dp(x*(KD(i, k) + KD(j, k)), (k, 1, 1)) == x*(KD(i, 1) + KD(j, 1)) + assert dp(x*(KD(i, k) + KD(j, k)), (k, 2, 2)) == x*(KD(i, 2) + KD(j, 2)) + assert dp(x*(KD(i, k) + KD(j, k)), (k, 3, 3)) == x*(KD(i, 3) + KD(j, 3)) + assert dp(x*(KD(i, k) + KD(j, k)), (k, 1, l)) == KD(l, 0) + \ + x*KD(i, 1)*KD(l, 1) + x*KD(j, 1)*KD(l, 1) + \ + x**2*KD(i, 1)*KD(j, 2)*KD(l, 2) + x**2*KD(j, 1)*KD(i, 2)*KD(l, 2) + assert dp(x*(KD(i, k) + KD(j, k)), (k, l, 3)) == KD(l, 4) + \ + x*KD(i, 3)*KD(l, 3) + x*KD(j, 3)*KD(l, 3) + \ + x**2*KD(i, 2)*KD(j, 3)*KD(l, 2) + x**2*KD(i, 3)*KD(j, 2)*KD(l, 2) + assert dp(x*(KD(i, k) + KD(j, k)), (k, l, m)) == KD(l, m + 1) + \ + x*KD(i, m)*KD(l, m) + x*KD(j, m)*KD(l, m) + \ + x**2*KD(i, m - 1)*KD(j, m)*KD(l, m - 1) + \ + x**2*KD(i, m)*KD(j, m - 1)*KD(l, m - 1) + + +def test_deltaproduct_mul_add_x_y_add_kd_kd(): + assert dp((x + y)*(KD(i, k) + KD(j, k)), (k, 1, 3)) == 0 + assert dp((x + y)*(KD(i, k) + KD(j, k)), (k, 1, 1)) == \ + (x + y)*(KD(i, 1) + KD(j, 1)) + assert dp((x + y)*(KD(i, k) + KD(j, k)), (k, 2, 2)) == \ + (x + y)*(KD(i, 2) + KD(j, 2)) + assert dp((x + y)*(KD(i, k) + KD(j, k)), (k, 3, 3)) == \ + (x + y)*(KD(i, 3) + KD(j, 3)) + assert dp((x + y)*(KD(i, k) + KD(j, k)), (k, 1, l)) == KD(l, 0) + \ + (x + y)*KD(i, 1)*KD(l, 1) + (x + y)*KD(j, 1)*KD(l, 1) + \ + (x + y)**2*KD(i, 1)*KD(j, 2)*KD(l, 2) + \ + (x + y)**2*KD(j, 1)*KD(i, 2)*KD(l, 2) + assert dp((x + y)*(KD(i, k) + KD(j, k)), (k, l, 3)) == KD(l, 4) + \ + (x + y)*KD(i, 3)*KD(l, 3) + (x + y)*KD(j, 3)*KD(l, 3) + \ + (x + y)**2*KD(i, 2)*KD(j, 3)*KD(l, 2) + \ + (x + y)**2*KD(i, 3)*KD(j, 2)*KD(l, 2) + assert dp((x + y)*(KD(i, k) + KD(j, k)), (k, l, m)) == KD(l, m + 1) + \ + (x + y)*KD(i, m)*KD(l, m) + (x + y)*KD(j, m)*KD(l, m) + \ + (x + y)**2*KD(i, m - 1)*KD(j, m)*KD(l, m - 1) + \ + (x + y)**2*KD(i, m)*KD(j, m - 1)*KD(l, m - 1) + + +def test_deltaproduct_add_mul_x_y_mul_x_kd(): + assert dp(x*y + x*KD(i, j), (j, 1, 3)) == (x*y)**3 + \ + x*(x*y)**2*KD(i, 1) + (x*y)*x*(x*y)*KD(i, 2) + (x*y)**2*x*KD(i, 3) + assert dp(x*y + x*KD(i, j), (j, 1, 1)) == x*y + x*KD(i, 1) + assert dp(x*y + x*KD(i, j), (j, 2, 2)) == x*y + x*KD(i, 2) + assert dp(x*y + x*KD(i, j), (j, 3, 3)) == x*y + x*KD(i, 3) + assert dp(x*y + x*KD(i, j), (j, 1, k)) == \ + (x*y)**k + Piecewise( + ((x*y)**(i - 1)*x*(x*y)**(k - i), And(1 <= i, i <= k)), + (0, True) + ) + assert dp(x*y + x*KD(i, j), (j, k, 3)) == \ + (x*y)**(-k + 4) + Piecewise( + ((x*y)**(i - k)*x*(x*y)**(3 - i), And(k <= i, i <= 3)), + (0, True) + ) + assert dp(x*y + x*KD(i, j), (j, k, l)) == \ + (x*y)**(-k + l + 1) + Piecewise( + ((x*y)**(i - k)*x*(x*y)**(l - i), And(k <= i, i <= l)), + (0, True) + ) + + +def test_deltaproduct_mul_x_add_y_kd(): + assert dp(x*(y + KD(i, j)), (j, 1, 3)) == (x*y)**3 + \ + x*(x*y)**2*KD(i, 1) + (x*y)*x*(x*y)*KD(i, 2) + (x*y)**2*x*KD(i, 3) + assert dp(x*(y + KD(i, j)), (j, 1, 1)) == x*(y + KD(i, 1)) + assert dp(x*(y + KD(i, j)), (j, 2, 2)) == x*(y + KD(i, 2)) + assert dp(x*(y + KD(i, j)), (j, 3, 3)) == x*(y + KD(i, 3)) + assert dp(x*(y + KD(i, j)), (j, 1, k)) == \ + (x*y)**k + Piecewise( + ((x*y)**(i - 1)*x*(x*y)**(k - i), And(1 <= i, i <= k)), + (0, True) + ).expand() + assert dp(x*(y + KD(i, j)), (j, k, 3)) == \ + ((x*y)**(-k + 4) + Piecewise( + ((x*y)**(i - k)*x*(x*y)**(3 - i), And(k <= i, i <= 3)), + (0, True) + )).expand() + assert dp(x*(y + KD(i, j)), (j, k, l)) == \ + ((x*y)**(-k + l + 1) + Piecewise( + ((x*y)**(i - k)*x*(x*y)**(l - i), And(k <= i, i <= l)), + (0, True) + )).expand() + + +def test_deltaproduct_mul_x_add_y_twokd(): + assert dp(x*(y + 2*KD(i, j)), (j, 1, 3)) == (x*y)**3 + \ + 2*x*(x*y)**2*KD(i, 1) + 2*x*y*x*x*y*KD(i, 2) + 2*(x*y)**2*x*KD(i, 3) + assert dp(x*(y + 2*KD(i, j)), (j, 1, 1)) == x*(y + 2*KD(i, 1)) + assert dp(x*(y + 2*KD(i, j)), (j, 2, 2)) == x*(y + 2*KD(i, 2)) + assert dp(x*(y + 2*KD(i, j)), (j, 3, 3)) == x*(y + 2*KD(i, 3)) + assert dp(x*(y + 2*KD(i, j)), (j, 1, k)) == \ + (x*y)**k + Piecewise( + (2*(x*y)**(i - 1)*x*(x*y)**(k - i), And(1 <= i, i <= k)), + (0, True) + ).expand() + assert dp(x*(y + 2*KD(i, j)), (j, k, 3)) == \ + ((x*y)**(-k + 4) + Piecewise( + (2*(x*y)**(i - k)*x*(x*y)**(3 - i), And(k <= i, i <= 3)), + (0, True) + )).expand() + assert dp(x*(y + 2*KD(i, j)), (j, k, l)) == \ + ((x*y)**(-k + l + 1) + Piecewise( + (2*(x*y)**(i - k)*x*(x*y)**(l - i), And(k <= i, i <= l)), + (0, True) + )).expand() + + +def test_deltaproduct_mul_add_x_y_add_y_kd(): + assert dp((x + y)*(y + KD(i, j)), (j, 1, 3)) == ((x + y)*y)**3 + \ + (x + y)*((x + y)*y)**2*KD(i, 1) + \ + (x + y)*y*(x + y)**2*y*KD(i, 2) + \ + ((x + y)*y)**2*(x + y)*KD(i, 3) + assert dp((x + y)*(y + KD(i, j)), (j, 1, 1)) == (x + y)*(y + KD(i, 1)) + assert dp((x + y)*(y + KD(i, j)), (j, 2, 2)) == (x + y)*(y + KD(i, 2)) + assert dp((x + y)*(y + KD(i, j)), (j, 3, 3)) == (x + y)*(y + KD(i, 3)) + assert dp((x + y)*(y + KD(i, j)), (j, 1, k)) == \ + ((x + y)*y)**k + Piecewise( + (((x + y)*y)**(-1)*((x + y)*y)**i*(x + y)*((x + y)*y + )**k*((x + y)*y)**(-i), (i >= 1) & (i <= k)), (0, True)) + assert dp((x + y)*(y + KD(i, j)), (j, k, 3)) == ( + (x + y)*y)**4*((x + y)*y)**(-k) + Piecewise((((x + y)*y)**i*( + (x + y)*y)**(-k)*(x + y)*((x + y)*y)**3*((x + y)*y)**(-i), + (i >= k) & (i <= 3)), (0, True)) + assert dp((x + y)*(y + KD(i, j)), (j, k, l)) == \ + (x + y)*y*((x + y)*y)**l*((x + y)*y)**(-k) + Piecewise( + (((x + y)*y)**i*((x + y)*y)**(-k)*(x + y)*((x + y)*y + )**l*((x + y)*y)**(-i), (i >= k) & (i <= l)), (0, True)) + + +def test_deltaproduct_mul_add_x_kd_add_y_kd(): + assert dp((x + KD(i, k))*(y + KD(i, j)), (j, 1, 3)) == \ + KD(i, 1)*(KD(i, k) + x)*((KD(i, k) + x)*y)**2 + \ + KD(i, 2)*(KD(i, k) + x)*y*(KD(i, k) + x)**2*y + \ + KD(i, 3)*((KD(i, k) + x)*y)**2*(KD(i, k) + x) + \ + ((KD(i, k) + x)*y)**3 + assert dp((x + KD(i, k))*(y + KD(i, j)), (j, 1, 1)) == \ + (x + KD(i, k))*(y + KD(i, 1)) + assert dp((x + KD(i, k))*(y + KD(i, j)), (j, 2, 2)) == \ + (x + KD(i, k))*(y + KD(i, 2)) + assert dp((x + KD(i, k))*(y + KD(i, j)), (j, 3, 3)) == \ + (x + KD(i, k))*(y + KD(i, 3)) + assert dp((x + KD(i, k))*(y + KD(i, j)), (j, 1, k)) == \ + ((KD(i, k) + x)*y)**k + Piecewise( + (((KD(i, k) + x)*y)**(-1)*((KD(i, k) + x)*y)**i*(KD(i, k) + x + )*((KD(i, k) + x)*y)**k*((KD(i, k) + x)*y)**(-i), (i >= 1 + ) & (i <= k)), (0, True)) + assert dp((x + KD(i, k))*(y + KD(i, j)), (j, k, 3)) == ( + (KD(i, k) + x)*y)**4*((KD(i, k) + x)*y)**(-k) + Piecewise( + (((KD(i, k) + x)*y)**i*((KD(i, k) + x)*y)**(-k)*(KD(i, k) + + x)*((KD(i, k) + x)*y)**3*((KD(i, k) + x)*y)**(-i), + (i >= k) & (i <= 3)), (0, True)) + assert dp((x + KD(i, k))*(y + KD(i, j)), (j, k, l)) == ( + KD(i, k) + x)*y*((KD(i, k) + x)*y)**l*((KD(i, k) + x)*y + )**(-k) + Piecewise((((KD(i, k) + x)*y)**i*((KD(i, k) + x + )*y)**(-k)*(KD(i, k) + x)*((KD(i, k) + x)*y)**l*((KD(i, k) + x + )*y)**(-i), (i >= k) & (i <= l)), (0, True)) + + +def test_deltasummation_trivial(): + assert ds(x, (j, 1, 0)) == 0 + assert ds(x, (j, 1, 3)) == 3*x + assert ds(x + y, (j, 1, 3)) == 3*(x + y) + assert ds(x*y, (j, 1, 3)) == 3*x*y + assert ds(KD(i, j), (k, 1, 3)) == 3*KD(i, j) + assert ds(x*KD(i, j), (k, 1, 3)) == 3*x*KD(i, j) + assert ds(x*y*KD(i, j), (k, 1, 3)) == 3*x*y*KD(i, j) + + +def test_deltasummation_basic_numerical(): + n = symbols('n', integer=True, nonzero=True) + assert ds(KD(n, 0), (n, 1, 3)) == 0 + + # return unevaluated, until it gets implemented + assert ds(KD(i**2, j**2), (j, -oo, oo)) == \ + Sum(KD(i**2, j**2), (j, -oo, oo)) + + assert Piecewise((KD(i, k), And(1 <= i, i <= 3)), (0, True)) == \ + ds(KD(i, j)*KD(j, k), (j, 1, 3)) == \ + ds(KD(j, k)*KD(i, j), (j, 1, 3)) + + assert ds(KD(i, k), (k, -oo, oo)) == 1 + assert ds(KD(i, k), (k, 0, oo)) == Piecewise((1, S.Zero <= i), (0, True)) + assert ds(KD(i, k), (k, 1, 3)) == \ + Piecewise((1, And(1 <= i, i <= 3)), (0, True)) + assert ds(k*KD(i, j)*KD(j, k), (k, -oo, oo)) == j*KD(i, j) + assert ds(j*KD(i, j), (j, -oo, oo)) == i + assert ds(i*KD(i, j), (i, -oo, oo)) == j + assert ds(x, (i, 1, 3)) == 3*x + assert ds((i + j)*KD(i, j), (j, -oo, oo)) == 2*i + + +def test_deltasummation_basic_symbolic(): + assert ds(KD(i, j), (j, 1, 3)) == \ + Piecewise((1, And(1 <= i, i <= 3)), (0, True)) + assert ds(KD(i, j), (j, 1, 1)) == Piecewise((1, Eq(i, 1)), (0, True)) + assert ds(KD(i, j), (j, 2, 2)) == Piecewise((1, Eq(i, 2)), (0, True)) + assert ds(KD(i, j), (j, 3, 3)) == Piecewise((1, Eq(i, 3)), (0, True)) + assert ds(KD(i, j), (j, 1, k)) == \ + Piecewise((1, And(1 <= i, i <= k)), (0, True)) + assert ds(KD(i, j), (j, k, 3)) == \ + Piecewise((1, And(k <= i, i <= 3)), (0, True)) + assert ds(KD(i, j), (j, k, l)) == \ + Piecewise((1, And(k <= i, i <= l)), (0, True)) + + +def test_deltasummation_mul_x_kd(): + assert ds(x*KD(i, j), (j, 1, 3)) == \ + Piecewise((x, And(1 <= i, i <= 3)), (0, True)) + assert ds(x*KD(i, j), (j, 1, 1)) == Piecewise((x, Eq(i, 1)), (0, True)) + assert ds(x*KD(i, j), (j, 2, 2)) == Piecewise((x, Eq(i, 2)), (0, True)) + assert ds(x*KD(i, j), (j, 3, 3)) == Piecewise((x, Eq(i, 3)), (0, True)) + assert ds(x*KD(i, j), (j, 1, k)) == \ + Piecewise((x, And(1 <= i, i <= k)), (0, True)) + assert ds(x*KD(i, j), (j, k, 3)) == \ + Piecewise((x, And(k <= i, i <= 3)), (0, True)) + assert ds(x*KD(i, j), (j, k, l)) == \ + Piecewise((x, And(k <= i, i <= l)), (0, True)) + + +def test_deltasummation_mul_add_x_y_kd(): + assert ds((x + y)*KD(i, j), (j, 1, 3)) == \ + Piecewise((x + y, And(1 <= i, i <= 3)), (0, True)) + assert ds((x + y)*KD(i, j), (j, 1, 1)) == \ + Piecewise((x + y, Eq(i, 1)), (0, True)) + assert ds((x + y)*KD(i, j), (j, 2, 2)) == \ + Piecewise((x + y, Eq(i, 2)), (0, True)) + assert ds((x + y)*KD(i, j), (j, 3, 3)) == \ + Piecewise((x + y, Eq(i, 3)), (0, True)) + assert ds((x + y)*KD(i, j), (j, 1, k)) == \ + Piecewise((x + y, And(1 <= i, i <= k)), (0, True)) + assert ds((x + y)*KD(i, j), (j, k, 3)) == \ + Piecewise((x + y, And(k <= i, i <= 3)), (0, True)) + assert ds((x + y)*KD(i, j), (j, k, l)) == \ + Piecewise((x + y, And(k <= i, i <= l)), (0, True)) + + +def test_deltasummation_add_kd_kd(): + assert ds(KD(i, k) + KD(j, k), (k, 1, 3)) == piecewise_fold( + Piecewise((1, And(1 <= i, i <= 3)), (0, True)) + + Piecewise((1, And(1 <= j, j <= 3)), (0, True))) + assert ds(KD(i, k) + KD(j, k), (k, 1, 1)) == piecewise_fold( + Piecewise((1, Eq(i, 1)), (0, True)) + + Piecewise((1, Eq(j, 1)), (0, True))) + assert ds(KD(i, k) + KD(j, k), (k, 2, 2)) == piecewise_fold( + Piecewise((1, Eq(i, 2)), (0, True)) + + Piecewise((1, Eq(j, 2)), (0, True))) + assert ds(KD(i, k) + KD(j, k), (k, 3, 3)) == piecewise_fold( + Piecewise((1, Eq(i, 3)), (0, True)) + + Piecewise((1, Eq(j, 3)), (0, True))) + assert ds(KD(i, k) + KD(j, k), (k, 1, l)) == piecewise_fold( + Piecewise((1, And(1 <= i, i <= l)), (0, True)) + + Piecewise((1, And(1 <= j, j <= l)), (0, True))) + assert ds(KD(i, k) + KD(j, k), (k, l, 3)) == piecewise_fold( + Piecewise((1, And(l <= i, i <= 3)), (0, True)) + + Piecewise((1, And(l <= j, j <= 3)), (0, True))) + assert ds(KD(i, k) + KD(j, k), (k, l, m)) == piecewise_fold( + Piecewise((1, And(l <= i, i <= m)), (0, True)) + + Piecewise((1, And(l <= j, j <= m)), (0, True))) + + +def test_deltasummation_add_mul_x_kd_kd(): + assert ds(x*KD(i, k) + KD(j, k), (k, 1, 3)) == piecewise_fold( + Piecewise((x, And(1 <= i, i <= 3)), (0, True)) + + Piecewise((1, And(1 <= j, j <= 3)), (0, True))) + assert ds(x*KD(i, k) + KD(j, k), (k, 1, 1)) == piecewise_fold( + Piecewise((x, Eq(i, 1)), (0, True)) + + Piecewise((1, Eq(j, 1)), (0, True))) + assert ds(x*KD(i, k) + KD(j, k), (k, 2, 2)) == piecewise_fold( + Piecewise((x, Eq(i, 2)), (0, True)) + + Piecewise((1, Eq(j, 2)), (0, True))) + assert ds(x*KD(i, k) + KD(j, k), (k, 3, 3)) == piecewise_fold( + Piecewise((x, Eq(i, 3)), (0, True)) + + Piecewise((1, Eq(j, 3)), (0, True))) + assert ds(x*KD(i, k) + KD(j, k), (k, 1, l)) == piecewise_fold( + Piecewise((x, And(1 <= i, i <= l)), (0, True)) + + Piecewise((1, And(1 <= j, j <= l)), (0, True))) + assert ds(x*KD(i, k) + KD(j, k), (k, l, 3)) == piecewise_fold( + Piecewise((x, And(l <= i, i <= 3)), (0, True)) + + Piecewise((1, And(l <= j, j <= 3)), (0, True))) + assert ds(x*KD(i, k) + KD(j, k), (k, l, m)) == piecewise_fold( + Piecewise((x, And(l <= i, i <= m)), (0, True)) + + Piecewise((1, And(l <= j, j <= m)), (0, True))) + + +def test_deltasummation_mul_x_add_kd_kd(): + assert ds(x*(KD(i, k) + KD(j, k)), (k, 1, 3)) == piecewise_fold( + Piecewise((x, And(1 <= i, i <= 3)), (0, True)) + + Piecewise((x, And(1 <= j, j <= 3)), (0, True))) + assert ds(x*(KD(i, k) + KD(j, k)), (k, 1, 1)) == piecewise_fold( + Piecewise((x, Eq(i, 1)), (0, True)) + + Piecewise((x, Eq(j, 1)), (0, True))) + assert ds(x*(KD(i, k) + KD(j, k)), (k, 2, 2)) == piecewise_fold( + Piecewise((x, Eq(i, 2)), (0, True)) + + Piecewise((x, Eq(j, 2)), (0, True))) + assert ds(x*(KD(i, k) + KD(j, k)), (k, 3, 3)) == piecewise_fold( + Piecewise((x, Eq(i, 3)), (0, True)) + + Piecewise((x, Eq(j, 3)), (0, True))) + assert ds(x*(KD(i, k) + KD(j, k)), (k, 1, l)) == piecewise_fold( + Piecewise((x, And(1 <= i, i <= l)), (0, True)) + + Piecewise((x, And(1 <= j, j <= l)), (0, True))) + assert ds(x*(KD(i, k) + KD(j, k)), (k, l, 3)) == piecewise_fold( + Piecewise((x, And(l <= i, i <= 3)), (0, True)) + + Piecewise((x, And(l <= j, j <= 3)), (0, True))) + assert ds(x*(KD(i, k) + KD(j, k)), (k, l, m)) == piecewise_fold( + Piecewise((x, And(l <= i, i <= m)), (0, True)) + + Piecewise((x, And(l <= j, j <= m)), (0, True))) + + +def test_deltasummation_mul_add_x_y_add_kd_kd(): + assert ds((x + y)*(KD(i, k) + KD(j, k)), (k, 1, 3)) == piecewise_fold( + Piecewise((x + y, And(1 <= i, i <= 3)), (0, True)) + + Piecewise((x + y, And(1 <= j, j <= 3)), (0, True))) + assert ds((x + y)*(KD(i, k) + KD(j, k)), (k, 1, 1)) == piecewise_fold( + Piecewise((x + y, Eq(i, 1)), (0, True)) + + Piecewise((x + y, Eq(j, 1)), (0, True))) + assert ds((x + y)*(KD(i, k) + KD(j, k)), (k, 2, 2)) == piecewise_fold( + Piecewise((x + y, Eq(i, 2)), (0, True)) + + Piecewise((x + y, Eq(j, 2)), (0, True))) + assert ds((x + y)*(KD(i, k) + KD(j, k)), (k, 3, 3)) == piecewise_fold( + Piecewise((x + y, Eq(i, 3)), (0, True)) + + Piecewise((x + y, Eq(j, 3)), (0, True))) + assert ds((x + y)*(KD(i, k) + KD(j, k)), (k, 1, l)) == piecewise_fold( + Piecewise((x + y, And(1 <= i, i <= l)), (0, True)) + + Piecewise((x + y, And(1 <= j, j <= l)), (0, True))) + assert ds((x + y)*(KD(i, k) + KD(j, k)), (k, l, 3)) == piecewise_fold( + Piecewise((x + y, And(l <= i, i <= 3)), (0, True)) + + Piecewise((x + y, And(l <= j, j <= 3)), (0, True))) + assert ds((x + y)*(KD(i, k) + KD(j, k)), (k, l, m)) == piecewise_fold( + Piecewise((x + y, And(l <= i, i <= m)), (0, True)) + + Piecewise((x + y, And(l <= j, j <= m)), (0, True))) + + +def test_deltasummation_add_mul_x_y_mul_x_kd(): + assert ds(x*y + x*KD(i, j), (j, 1, 3)) == \ + Piecewise((3*x*y + x, And(1 <= i, i <= 3)), (3*x*y, True)) + assert ds(x*y + x*KD(i, j), (j, 1, 1)) == \ + Piecewise((x*y + x, Eq(i, 1)), (x*y, True)) + assert ds(x*y + x*KD(i, j), (j, 2, 2)) == \ + Piecewise((x*y + x, Eq(i, 2)), (x*y, True)) + assert ds(x*y + x*KD(i, j), (j, 3, 3)) == \ + Piecewise((x*y + x, Eq(i, 3)), (x*y, True)) + assert ds(x*y + x*KD(i, j), (j, 1, k)) == \ + Piecewise((k*x*y + x, And(1 <= i, i <= k)), (k*x*y, True)) + assert ds(x*y + x*KD(i, j), (j, k, 3)) == \ + Piecewise(((4 - k)*x*y + x, And(k <= i, i <= 3)), ((4 - k)*x*y, True)) + assert ds(x*y + x*KD(i, j), (j, k, l)) == Piecewise( + ((l - k + 1)*x*y + x, And(k <= i, i <= l)), ((l - k + 1)*x*y, True)) + + +def test_deltasummation_mul_x_add_y_kd(): + assert ds(x*(y + KD(i, j)), (j, 1, 3)) == \ + Piecewise((3*x*y + x, And(1 <= i, i <= 3)), (3*x*y, True)) + assert ds(x*(y + KD(i, j)), (j, 1, 1)) == \ + Piecewise((x*y + x, Eq(i, 1)), (x*y, True)) + assert ds(x*(y + KD(i, j)), (j, 2, 2)) == \ + Piecewise((x*y + x, Eq(i, 2)), (x*y, True)) + assert ds(x*(y + KD(i, j)), (j, 3, 3)) == \ + Piecewise((x*y + x, Eq(i, 3)), (x*y, True)) + assert ds(x*(y + KD(i, j)), (j, 1, k)) == \ + Piecewise((k*x*y + x, And(1 <= i, i <= k)), (k*x*y, True)) + assert ds(x*(y + KD(i, j)), (j, k, 3)) == \ + Piecewise(((4 - k)*x*y + x, And(k <= i, i <= 3)), ((4 - k)*x*y, True)) + assert ds(x*(y + KD(i, j)), (j, k, l)) == Piecewise( + ((l - k + 1)*x*y + x, And(k <= i, i <= l)), ((l - k + 1)*x*y, True)) + + +def test_deltasummation_mul_x_add_y_twokd(): + assert ds(x*(y + 2*KD(i, j)), (j, 1, 3)) == \ + Piecewise((3*x*y + 2*x, And(1 <= i, i <= 3)), (3*x*y, True)) + assert ds(x*(y + 2*KD(i, j)), (j, 1, 1)) == \ + Piecewise((x*y + 2*x, Eq(i, 1)), (x*y, True)) + assert ds(x*(y + 2*KD(i, j)), (j, 2, 2)) == \ + Piecewise((x*y + 2*x, Eq(i, 2)), (x*y, True)) + assert ds(x*(y + 2*KD(i, j)), (j, 3, 3)) == \ + Piecewise((x*y + 2*x, Eq(i, 3)), (x*y, True)) + assert ds(x*(y + 2*KD(i, j)), (j, 1, k)) == \ + Piecewise((k*x*y + 2*x, And(1 <= i, i <= k)), (k*x*y, True)) + assert ds(x*(y + 2*KD(i, j)), (j, k, 3)) == Piecewise( + ((4 - k)*x*y + 2*x, And(k <= i, i <= 3)), ((4 - k)*x*y, True)) + assert ds(x*(y + 2*KD(i, j)), (j, k, l)) == Piecewise( + ((l - k + 1)*x*y + 2*x, And(k <= i, i <= l)), ((l - k + 1)*x*y, True)) + + +def test_deltasummation_mul_add_x_y_add_y_kd(): + assert ds((x + y)*(y + KD(i, j)), (j, 1, 3)) == Piecewise( + (3*(x + y)*y + x + y, And(1 <= i, i <= 3)), (3*(x + y)*y, True)) + assert ds((x + y)*(y + KD(i, j)), (j, 1, 1)) == \ + Piecewise(((x + y)*y + x + y, Eq(i, 1)), ((x + y)*y, True)) + assert ds((x + y)*(y + KD(i, j)), (j, 2, 2)) == \ + Piecewise(((x + y)*y + x + y, Eq(i, 2)), ((x + y)*y, True)) + assert ds((x + y)*(y + KD(i, j)), (j, 3, 3)) == \ + Piecewise(((x + y)*y + x + y, Eq(i, 3)), ((x + y)*y, True)) + assert ds((x + y)*(y + KD(i, j)), (j, 1, k)) == Piecewise( + (k*(x + y)*y + x + y, And(1 <= i, i <= k)), (k*(x + y)*y, True)) + assert ds((x + y)*(y + KD(i, j)), (j, k, 3)) == Piecewise( + ((4 - k)*(x + y)*y + x + y, And(k <= i, i <= 3)), + ((4 - k)*(x + y)*y, True)) + assert ds((x + y)*(y + KD(i, j)), (j, k, l)) == Piecewise( + ((l - k + 1)*(x + y)*y + x + y, And(k <= i, i <= l)), + ((l - k + 1)*(x + y)*y, True)) + + +def test_deltasummation_mul_add_x_kd_add_y_kd(): + assert ds((x + KD(i, k))*(y + KD(i, j)), (j, 1, 3)) == piecewise_fold( + Piecewise((KD(i, k) + x, And(1 <= i, i <= 3)), (0, True)) + + 3*(KD(i, k) + x)*y) + assert ds((x + KD(i, k))*(y + KD(i, j)), (j, 1, 1)) == piecewise_fold( + Piecewise((KD(i, k) + x, Eq(i, 1)), (0, True)) + + (KD(i, k) + x)*y) + assert ds((x + KD(i, k))*(y + KD(i, j)), (j, 2, 2)) == piecewise_fold( + Piecewise((KD(i, k) + x, Eq(i, 2)), (0, True)) + + (KD(i, k) + x)*y) + assert ds((x + KD(i, k))*(y + KD(i, j)), (j, 3, 3)) == piecewise_fold( + Piecewise((KD(i, k) + x, Eq(i, 3)), (0, True)) + + (KD(i, k) + x)*y) + assert ds((x + KD(i, k))*(y + KD(i, j)), (j, 1, k)) == piecewise_fold( + Piecewise((KD(i, k) + x, And(1 <= i, i <= k)), (0, True)) + + k*(KD(i, k) + x)*y) + assert ds((x + KD(i, k))*(y + KD(i, j)), (j, k, 3)) == piecewise_fold( + Piecewise((KD(i, k) + x, And(k <= i, i <= 3)), (0, True)) + + (4 - k)*(KD(i, k) + x)*y) + assert ds((x + KD(i, k))*(y + KD(i, j)), (j, k, l)) == piecewise_fold( + Piecewise((KD(i, k) + x, And(k <= i, i <= l)), (0, True)) + + (l - k + 1)*(KD(i, k) + x)*y) + + +def test_extract_delta(): + raises(ValueError, lambda: _extract_delta(KD(i, j) + KD(k, l), i)) diff --git a/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_gosper.py b/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_gosper.py new file mode 100644 index 0000000000000000000000000000000000000000..77b642a9b7cd55f96840a8e20e517206b6a6f8f0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_gosper.py @@ -0,0 +1,204 @@ +"""Tests for Gosper's algorithm for hypergeometric summation. """ + +from sympy.core.numbers import (Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.combinatorial.factorials import (binomial, factorial) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.special.gamma_functions import gamma +from sympy.polys.polytools import Poly +from sympy.simplify.simplify import simplify +from sympy.concrete.gosper import gosper_normal, gosper_sum, gosper_term +from sympy.abc import a, b, j, k, m, n, r, x + + +def test_gosper_normal(): + eq = 4*n + 5, 2*(4*n + 1)*(2*n + 3), n + assert gosper_normal(*eq) == \ + (Poly(Rational(1, 4), n), Poly(n + Rational(3, 2)), Poly(n + Rational(1, 4))) + assert gosper_normal(*eq, polys=False) == \ + (Rational(1, 4), n + Rational(3, 2), n + Rational(1, 4)) + + +def test_gosper_term(): + assert gosper_term((4*k + 1)*factorial( + k)/factorial(2*k + 1), k) == (-k - S.Half)/(k + Rational(1, 4)) + + +def test_gosper_sum(): + assert gosper_sum(1, (k, 0, n)) == 1 + n + assert gosper_sum(k, (k, 0, n)) == n*(1 + n)/2 + assert gosper_sum(k**2, (k, 0, n)) == n*(1 + n)*(1 + 2*n)/6 + assert gosper_sum(k**3, (k, 0, n)) == n**2*(1 + n)**2/4 + + assert gosper_sum(2**k, (k, 0, n)) == 2*2**n - 1 + + assert gosper_sum(factorial(k), (k, 0, n)) is None + assert gosper_sum(binomial(n, k), (k, 0, n)) is None + + assert gosper_sum(factorial(k)/k**2, (k, 0, n)) is None + assert gosper_sum((k - 3)*factorial(k), (k, 0, n)) is None + + assert gosper_sum(k*factorial(k), k) == factorial(k) + assert gosper_sum( + k*factorial(k), (k, 0, n)) == n*factorial(n) + factorial(n) - 1 + + assert gosper_sum((-1)**k*binomial(n, k), (k, 0, n)) == 0 + assert gosper_sum(( + -1)**k*binomial(n, k), (k, 0, m)) == -(-1)**m*(m - n)*binomial(n, m)/n + + assert gosper_sum((4*k + 1)*factorial(k)/factorial(2*k + 1), (k, 0, n)) == \ + (2*factorial(2*n + 1) - factorial(n))/factorial(2*n + 1) + + # issue 6033: + assert gosper_sum( + n*(n + a + b)*a**n*b**n/(factorial(n + a)*factorial(n + b)), \ + (n, 0, m)).simplify() == -exp(m*log(a) + m*log(b))*gamma(a + 1) \ + *gamma(b + 1)/(gamma(a)*gamma(b)*gamma(a + m + 1)*gamma(b + m + 1)) \ + + 1/(gamma(a)*gamma(b)) + + +def test_gosper_sum_indefinite(): + assert gosper_sum(k, k) == k*(k - 1)/2 + assert gosper_sum(k**2, k) == k*(k - 1)*(2*k - 1)/6 + + assert gosper_sum(1/(k*(k + 1)), k) == -1/k + assert gosper_sum(-(27*k**4 + 158*k**3 + 430*k**2 + 678*k + 445)*gamma(2*k + + 4)/(3*(3*k + 7)*gamma(3*k + 6)), k) == \ + (3*k + 5)*(k**2 + 2*k + 5)*gamma(2*k + 4)/gamma(3*k + 6) + + +def test_gosper_sum_parametric(): + assert gosper_sum(binomial(S.Half, m - j + 1)*binomial(S.Half, m + j), (j, 1, n)) == \ + n*(1 + m - n)*(-1 + 2*m + 2*n)*binomial(S.Half, 1 + m - n)* \ + binomial(S.Half, m + n)/(m*(1 + 2*m)) + + +def test_gosper_sum_algebraic(): + assert gosper_sum( + n**2 + sqrt(2), (n, 0, m)) == (m + 1)*(2*m**2 + m + 6*sqrt(2))/6 + + +def test_gosper_sum_iterated(): + f1 = binomial(2*k, k)/4**k + f2 = (1 + 2*n)*binomial(2*n, n)/4**n + f3 = (1 + 2*n)*(3 + 2*n)*binomial(2*n, n)/(3*4**n) + f4 = (1 + 2*n)*(3 + 2*n)*(5 + 2*n)*binomial(2*n, n)/(15*4**n) + f5 = (1 + 2*n)*(3 + 2*n)*(5 + 2*n)*(7 + 2*n)*binomial(2*n, n)/(105*4**n) + + assert gosper_sum(f1, (k, 0, n)) == f2 + assert gosper_sum(f2, (n, 0, n)) == f3 + assert gosper_sum(f3, (n, 0, n)) == f4 + assert gosper_sum(f4, (n, 0, n)) == f5 + +# the AeqB tests test expressions given in +# www.math.upenn.edu/~wilf/AeqB.pdf + + +def test_gosper_sum_AeqB_part1(): + f1a = n**4 + f1b = n**3*2**n + f1c = 1/(n**2 + sqrt(5)*n - 1) + f1d = n**4*4**n/binomial(2*n, n) + f1e = factorial(3*n)/(factorial(n)*factorial(n + 1)*factorial(n + 2)*27**n) + f1f = binomial(2*n, n)**2/((n + 1)*4**(2*n)) + f1g = (4*n - 1)*binomial(2*n, n)**2/((2*n - 1)**2*4**(2*n)) + f1h = n*factorial(n - S.Half)**2/factorial(n + 1)**2 + + g1a = m*(m + 1)*(2*m + 1)*(3*m**2 + 3*m - 1)/30 + g1b = 26 + 2**(m + 1)*(m**3 - 3*m**2 + 9*m - 13) + g1c = (m + 1)*(m*(m**2 - 7*m + 3)*sqrt(5) - ( + 3*m**3 - 7*m**2 + 19*m - 6))/(2*m**3*sqrt(5) + m**4 + 5*m**2 - 1)/6 + g1d = Rational(-2, 231) + 2*4**m*(m + 1)*(63*m**4 + 112*m**3 + 18*m**2 - + 22*m + 3)/(693*binomial(2*m, m)) + g1e = Rational(-9, 2) + (81*m**2 + 261*m + 200)*factorial( + 3*m + 2)/(40*27**m*factorial(m)*factorial(m + 1)*factorial(m + 2)) + g1f = (2*m + 1)**2*binomial(2*m, m)**2/(4**(2*m)*(m + 1)) + g1g = -binomial(2*m, m)**2/4**(2*m) + g1h = 4*pi -(2*m + 1)**2*(3*m + 4)*factorial(m - S.Half)**2/factorial(m + 1)**2 + + g = gosper_sum(f1a, (n, 0, m)) + assert g is not None and simplify(g - g1a) == 0 + g = gosper_sum(f1b, (n, 0, m)) + assert g is not None and simplify(g - g1b) == 0 + g = gosper_sum(f1c, (n, 0, m)) + assert g is not None and simplify(g - g1c) == 0 + g = gosper_sum(f1d, (n, 0, m)) + assert g is not None and simplify(g - g1d) == 0 + g = gosper_sum(f1e, (n, 0, m)) + assert g is not None and simplify(g - g1e) == 0 + g = gosper_sum(f1f, (n, 0, m)) + assert g is not None and simplify(g - g1f) == 0 + g = gosper_sum(f1g, (n, 0, m)) + assert g is not None and simplify(g - g1g) == 0 + g = gosper_sum(f1h, (n, 0, m)) + # need to call rewrite(gamma) here because we have terms involving + # factorial(1/2) + assert g is not None and simplify(g - g1h).rewrite(gamma) == 0 + + +def test_gosper_sum_AeqB_part2(): + f2a = n**2*a**n + f2b = (n - r/2)*binomial(r, n) + f2c = factorial(n - 1)**2/(factorial(n - x)*factorial(n + x)) + + g2a = -a*(a + 1)/(a - 1)**3 + a**( + m + 1)*(a**2*m**2 - 2*a*m**2 + m**2 - 2*a*m + 2*m + a + 1)/(a - 1)**3 + g2b = (m - r)*binomial(r, m)/2 + ff = factorial(1 - x)*factorial(1 + x) + g2c = 1/ff*( + 1 - 1/x**2) + factorial(m)**2/(x**2*factorial(m - x)*factorial(m + x)) + + g = gosper_sum(f2a, (n, 0, m)) + assert g is not None and simplify(g - g2a) == 0 + g = gosper_sum(f2b, (n, 0, m)) + assert g is not None and simplify(g - g2b) == 0 + g = gosper_sum(f2c, (n, 1, m)) + assert g is not None and simplify(g - g2c) == 0 + + +def test_gosper_nan(): + a = Symbol('a', positive=True) + b = Symbol('b', positive=True) + n = Symbol('n', integer=True) + m = Symbol('m', integer=True) + f2d = n*(n + a + b)*a**n*b**n/(factorial(n + a)*factorial(n + b)) + g2d = 1/(factorial(a - 1)*factorial( + b - 1)) - a**(m + 1)*b**(m + 1)/(factorial(a + m)*factorial(b + m)) + g = gosper_sum(f2d, (n, 0, m)) + assert simplify(g - g2d) == 0 + + +def test_gosper_sum_AeqB_part3(): + f3a = 1/n**4 + f3b = (6*n + 3)/(4*n**4 + 8*n**3 + 8*n**2 + 4*n + 3) + f3c = 2**n*(n**2 - 2*n - 1)/(n**2*(n + 1)**2) + f3d = n**2*4**n/((n + 1)*(n + 2)) + f3e = 2**n/(n + 1) + f3f = 4*(n - 1)*(n**2 - 2*n - 1)/(n**2*(n + 1)**2*(n - 2)**2*(n - 3)**2) + f3g = (n**4 - 14*n**2 - 24*n - 9)*2**n/(n**2*(n + 1)**2*(n + 2)**2* + (n + 3)**2) + + # g3a -> no closed form + g3b = m*(m + 2)/(2*m**2 + 4*m + 3) + g3c = 2**m/m**2 - 2 + g3d = Rational(2, 3) + 4**(m + 1)*(m - 1)/(m + 2)/3 + # g3e -> no closed form + g3f = -(Rational(-1, 16) + 1/((m - 2)**2*(m + 1)**2)) # the AeqB key is wrong + g3g = Rational(-2, 9) + 2**(m + 1)/((m + 1)**2*(m + 3)**2) + + g = gosper_sum(f3a, (n, 1, m)) + assert g is None + g = gosper_sum(f3b, (n, 1, m)) + assert g is not None and simplify(g - g3b) == 0 + g = gosper_sum(f3c, (n, 1, m - 1)) + assert g is not None and simplify(g - g3c) == 0 + g = gosper_sum(f3d, (n, 1, m)) + assert g is not None and simplify(g - g3d) == 0 + g = gosper_sum(f3e, (n, 0, m - 1)) + assert g is None + g = gosper_sum(f3f, (n, 4, m)) + assert g is not None and simplify(g - g3f) == 0 + g = gosper_sum(f3g, (n, 1, m)) + assert g is not None and simplify(g - g3g) == 0 diff --git a/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_guess.py b/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_guess.py new file mode 100644 index 0000000000000000000000000000000000000000..5ac5d02b89ad62a70a29bd450b71b284b6aea76d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_guess.py @@ -0,0 +1,82 @@ +from sympy.concrete.guess import ( + find_simple_recurrence_vector, + find_simple_recurrence, + rationalize, + guess_generating_function_rational, + guess_generating_function, + guess + ) +from sympy.concrete.products import Product +from sympy.core.function import Function +from sympy.core.numbers import Rational +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import (RisingFactorial, factorial) +from sympy.functions.combinatorial.numbers import fibonacci +from sympy.functions.elementary.exponential import exp + + +def test_find_simple_recurrence_vector(): + assert find_simple_recurrence_vector( + [fibonacci(k) for k in range(12)]) == [1, -1, -1] + + +def test_find_simple_recurrence(): + a = Function('a') + n = Symbol('n') + assert find_simple_recurrence([fibonacci(k) for k in range(12)]) == ( + -a(n) - a(n + 1) + a(n + 2)) + + f = Function('a') + i = Symbol('n') + a = [1, 1, 1] + for k in range(15): a.append(5*a[-1]-3*a[-2]+8*a[-3]) + assert find_simple_recurrence(a, A=f, N=i) == ( + -8*f(i) + 3*f(i + 1) - 5*f(i + 2) + f(i + 3)) + assert find_simple_recurrence([0, 2, 15, 74, 12, 3, 0, + 1, 2, 85, 4, 5, 63]) == 0 + + +def test_rationalize(): + from mpmath import cos, pi, mpf + assert rationalize(cos(pi/3)) == S.Half + assert rationalize(mpf("0.333333333333333")) == Rational(1, 3) + assert rationalize(mpf("-0.333333333333333")) == Rational(-1, 3) + assert rationalize(pi, maxcoeff = 250) == Rational(355, 113) + + +def test_guess_generating_function_rational(): + x = Symbol('x') + assert guess_generating_function_rational([fibonacci(k) + for k in range(5, 15)]) == ((3*x + 5)/(-x**2 - x + 1)) + + +def test_guess_generating_function(): + x = Symbol('x') + assert guess_generating_function([fibonacci(k) + for k in range(5, 15)])['ogf'] == ((3*x + 5)/(-x**2 - x + 1)) + assert guess_generating_function( + [1, 2, 5, 14, 41, 124, 383, 1200, 3799, 12122, 38919])['ogf'] == ( + (1/(x**4 + 2*x**2 - 4*x + 1))**S.Half) + assert guess_generating_function(sympify( + "[3/2, 11/2, 0, -121/2, -363/2, 121, 4719/2, 11495/2, -8712, -178717/2]") + )['ogf'] == (x + Rational(3, 2))/(11*x**2 - 3*x + 1) + assert guess_generating_function([factorial(k) for k in range(12)], + types=['egf'])['egf'] == 1/(-x + 1) + assert guess_generating_function([k+1 for k in range(12)], + types=['egf']) == {'egf': (x + 1)*exp(x), 'lgdegf': (x + 2)/(x + 1)} + + +def test_guess(): + i0, i1 = symbols('i0 i1') + assert guess([1, 2, 6, 24, 120], evaluate=False) == [Product(i1 + 1, (i1, 1, i0 - 1))] + assert guess([1, 2, 6, 24, 120]) == [RisingFactorial(2, i0 - 1)] + assert guess([1, 2, 7, 42, 429, 7436, 218348, 10850216], niter=4) == [ + 2**(i0 - 1)*(Rational(27, 16))**(i0**2/2 - 3*i0/2 + + 1)*Product(RisingFactorial(Rational(5, 3), i1 - 1)*RisingFactorial(Rational(7, 3), i1 + - 1)/(RisingFactorial(Rational(3, 2), i1 - 1)*RisingFactorial(Rational(5, 2), i1 - + 1)), (i1, 1, i0 - 1))] + assert guess([1, 0, 2]) == [] + x, y = symbols('x y') + assert guess([1, 2, 6, 24, 120], variables=[x, y]) == [RisingFactorial(2, x - 1)] diff --git a/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_products.py b/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_products.py new file mode 100644 index 0000000000000000000000000000000000000000..9be053a7040014c6ed38c1279a609fcb2426258e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_products.py @@ -0,0 +1,410 @@ +from sympy.concrete.products import (Product, product) +from sympy.concrete.summations import Sum +from sympy.core.function import (Derivative, Function, diff) +from sympy.core.numbers import (Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.functions.combinatorial.factorials import (rf, factorial) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.simplify.combsimp import combsimp +from sympy.simplify.simplify import simplify +from sympy.testing.pytest import raises + +a, k, n, m, x = symbols('a,k,n,m,x', integer=True) +f = Function('f') + + +def test_karr_convention(): + # Test the Karr product convention that we want to hold. + # See his paper "Summation in Finite Terms" for a detailed + # reasoning why we really want exactly this definition. + # The convention is described for sums on page 309 and + # essentially in section 1.4, definition 3. For products + # we can find in analogy: + # + # \prod_{m <= i < n} f(i) 'has the obvious meaning' for m < n + # \prod_{m <= i < n} f(i) = 0 for m = n + # \prod_{m <= i < n} f(i) = 1 / \prod_{n <= i < m} f(i) for m > n + # + # It is important to note that he defines all products with + # the upper limit being *exclusive*. + # In contrast, SymPy and the usual mathematical notation has: + # + # prod_{i = a}^b f(i) = f(a) * f(a+1) * ... * f(b-1) * f(b) + # + # with the upper limit *inclusive*. So translating between + # the two we find that: + # + # \prod_{m <= i < n} f(i) = \prod_{i = m}^{n-1} f(i) + # + # where we intentionally used two different ways to typeset the + # products and its limits. + + i = Symbol("i", integer=True) + k = Symbol("k", integer=True) + j = Symbol("j", integer=True, positive=True) + + # A simple example with a concrete factors and symbolic limits. + + # The normal product: m = k and n = k + j and therefore m < n: + m = k + n = k + j + + a = m + b = n - 1 + S1 = Product(i**2, (i, a, b)).doit() + + # The reversed product: m = k + j and n = k and therefore m > n: + m = k + j + n = k + + a = m + b = n - 1 + S2 = Product(i**2, (i, a, b)).doit() + + assert S1 * S2 == 1 + + # Test the empty product: m = k and n = k and therefore m = n: + m = k + n = k + + a = m + b = n - 1 + Sz = Product(i**2, (i, a, b)).doit() + + assert Sz == 1 + + # Another example this time with an unspecified factor and + # numeric limits. (We can not do both tests in the same example.) + f = Function("f") + + # The normal product with m < n: + m = 2 + n = 11 + + a = m + b = n - 1 + S1 = Product(f(i), (i, a, b)).doit() + + # The reversed product with m > n: + m = 11 + n = 2 + + a = m + b = n - 1 + S2 = Product(f(i), (i, a, b)).doit() + + assert simplify(S1 * S2) == 1 + + # Test the empty product with m = n: + m = 5 + n = 5 + + a = m + b = n - 1 + Sz = Product(f(i), (i, a, b)).doit() + + assert Sz == 1 + + +def test_karr_proposition_2a(): + # Test Karr, page 309, proposition 2, part a + i, u, v = symbols('i u v', integer=True) + + def test_the_product(m, n): + # g + g = i**3 + 2*i**2 - 3*i + # f = Delta g + f = simplify(g.subs(i, i+1) / g) + # The product + a = m + b = n - 1 + P = Product(f, (i, a, b)).doit() + # Test if Product_{m <= i < n} f(i) = g(n) / g(m) + assert combsimp(P / (g.subs(i, n) / g.subs(i, m))) == 1 + + # m < n + test_the_product(u, u + v) + # m = n + test_the_product(u, u) + # m > n + test_the_product(u + v, u) + + +def test_karr_proposition_2b(): + # Test Karr, page 309, proposition 2, part b + i, u, v, w = symbols('i u v w', integer=True) + + def test_the_product(l, n, m): + # Productmand + s = i**3 + # First product + a = l + b = n - 1 + S1 = Product(s, (i, a, b)).doit() + # Second product + a = l + b = m - 1 + S2 = Product(s, (i, a, b)).doit() + # Third product + a = m + b = n - 1 + S3 = Product(s, (i, a, b)).doit() + # Test if S1 = S2 * S3 as required + assert combsimp(S1 / (S2 * S3)) == 1 + + # l < m < n + test_the_product(u, u + v, u + v + w) + # l < m = n + test_the_product(u, u + v, u + v) + # l < m > n + test_the_product(u, u + v + w, v) + # l = m < n + test_the_product(u, u, u + v) + # l = m = n + test_the_product(u, u, u) + # l = m > n + test_the_product(u + v, u + v, u) + # l > m < n + test_the_product(u + v, u, u + w) + # l > m = n + test_the_product(u + v, u, u) + # l > m > n + test_the_product(u + v + w, u + v, u) + + +def test_simple_products(): + assert product(2, (k, a, n)) == 2**(n - a + 1) + assert product(k, (k, 1, n)) == factorial(n) + assert product(k**3, (k, 1, n)) == factorial(n)**3 + + assert product(k + 1, (k, 0, n - 1)) == factorial(n) + assert product(k + 1, (k, a, n - 1)) == rf(1 + a, n - a) + + assert product(cos(k), (k, 0, 5)) == cos(1)*cos(2)*cos(3)*cos(4)*cos(5) + assert product(cos(k), (k, 3, 5)) == cos(3)*cos(4)*cos(5) + assert product(cos(k), (k, 1, Rational(5, 2))) != cos(1)*cos(2) + + assert isinstance(product(k**k, (k, 1, n)), Product) + + assert Product(x**k, (k, 1, n)).variables == [k] + + raises(ValueError, lambda: Product(n)) + raises(ValueError, lambda: Product(n, k)) + raises(ValueError, lambda: Product(n, k, 1)) + raises(ValueError, lambda: Product(n, k, 1, 10)) + raises(ValueError, lambda: Product(n, (k, 1))) + + assert product(1, (n, 1, oo)) == 1 # issue 8301 + assert product(2, (n, 1, oo)) is oo + assert product(-1, (n, 1, oo)).func is Product + + +def test_multiple_products(): + assert product(x, (n, 1, k), (k, 1, m)) == x**(m**2/2 + m/2) + assert product(f(n), ( + n, 1, m), (m, 1, k)) == Product(f(n), (n, 1, m), (m, 1, k)).doit() + assert Product(f(n), (m, 1, k), (n, 1, k)).doit() == \ + Product(Product(f(n), (m, 1, k)), (n, 1, k)).doit() == \ + product(f(n), (m, 1, k), (n, 1, k)) == \ + product(product(f(n), (m, 1, k)), (n, 1, k)) == \ + Product(f(n)**k, (n, 1, k)) + assert Product( + x, (x, 1, k), (k, 1, n)).doit() == Product(factorial(k), (k, 1, n)) + + assert Product(x**k, (n, 1, k), (k, 1, m)).variables == [n, k] + + +def test_rational_products(): + assert product(1 + 1/k, (k, 1, n)) == rf(2, n)/factorial(n) + + +def test_special_products(): + # Wallis product + assert product((4*k)**2 / (4*k**2 - 1), (k, 1, n)) == \ + 4**n*factorial(n)**2/rf(S.Half, n)/rf(Rational(3, 2), n) + + # Euler's product formula for sin + assert product(1 + a/k**2, (k, 1, n)) == \ + rf(1 - sqrt(-a), n)*rf(1 + sqrt(-a), n)/factorial(n)**2 + + +def test__eval_product(): + from sympy.abc import i, n + # issue 4809 + a = Function('a') + assert product(2*a(i), (i, 1, n)) == 2**n * Product(a(i), (i, 1, n)) + # issue 4810 + assert product(2**i, (i, 1, n)) == 2**(n*(n + 1)/2) + k, m = symbols('k m', integer=True) + assert product(2**i, (i, k, m)) == 2**(-k**2/2 + k/2 + m**2/2 + m/2) + n = Symbol('n', negative=True, integer=True) + p = Symbol('p', positive=True, integer=True) + assert product(2**i, (i, n, p)) == 2**(-n**2/2 + n/2 + p**2/2 + p/2) + assert product(2**i, (i, p, n)) == 2**(n**2/2 + n/2 - p**2/2 + p/2) + + +def test_product_pow(): + # issue 4817 + assert product(2**f(k), (k, 1, n)) == 2**Sum(f(k), (k, 1, n)) + assert product(2**(2*f(k)), (k, 1, n)) == 2**Sum(2*f(k), (k, 1, n)) + + +def test_infinite_product(): + # issue 5737 + assert isinstance(Product(2**(1/factorial(n)), (n, 0, oo)), Product) + + +def test_conjugate_transpose(): + p = Product(x**k, (k, 1, 3)) + assert p.adjoint().doit() == p.doit().adjoint() + assert p.conjugate().doit() == p.doit().conjugate() + assert p.transpose().doit() == p.doit().transpose() + + A, B = symbols("A B", commutative=False) + p = Product(A*B**k, (k, 1, 3)) + assert p.adjoint().doit() == p.doit().adjoint() + assert p.conjugate().doit() == p.doit().conjugate() + assert p.transpose().doit() == p.doit().transpose() + + p = Product(B**k*A, (k, 1, 3)) + assert p.adjoint().doit() == p.doit().adjoint() + assert p.conjugate().doit() == p.doit().conjugate() + assert p.transpose().doit() == p.doit().transpose() + + +def test_simplify_prod(): + y, t, b, c, v, d = symbols('y, t, b, c, v, d', integer = True) + + _simplify = lambda e: simplify(e, doit=False) + assert _simplify(Product(x*y, (x, n, m), (y, a, k)) * \ + Product(y, (x, n, m), (y, a, k))) == \ + Product(x*y**2, (x, n, m), (y, a, k)) + assert _simplify(3 * y* Product(x, (x, n, m)) * Product(x, (x, m + 1, a))) \ + == 3 * y * Product(x, (x, n, a)) + assert _simplify(Product(x, (x, k + 1, a)) * Product(x, (x, n, k))) == \ + Product(x, (x, n, a)) + assert _simplify(Product(x, (x, k + 1, a)) * Product(x + 1, (x, n, k))) == \ + Product(x, (x, k + 1, a)) * Product(x + 1, (x, n, k)) + assert _simplify(Product(x, (t, a, b)) * Product(y, (t, a, b)) * \ + Product(x, (t, b+1, c))) == Product(x*y, (t, a, b)) * \ + Product(x, (t, b+1, c)) + assert _simplify(Product(x, (t, a, b)) * Product(x, (t, b+1, c)) * \ + Product(y, (t, a, b))) == Product(x*y, (t, a, b)) * \ + Product(x, (t, b+1, c)) + assert _simplify(Product(sin(t)**2 + cos(t)**2 + 1, (t, a, b))) == \ + Product(2, (t, a, b)) + assert _simplify(Product(sin(t)**2 + cos(t)**2 - 1, (t, a, b))) == \ + Product(0, (t, a, b)) + assert _simplify(Product(v*Product(sin(t)**2 + cos(t)**2, (t, a, b)), + (v, c, d))) == Product(v*Product(1, (t, a, b)), (v, c, d)) + + +def test_change_index(): + b, y, c, d, z = symbols('b, y, c, d, z', integer = True) + + assert Product(x, (x, a, b)).change_index(x, x + 1, y) == \ + Product(y - 1, (y, a + 1, b + 1)) + assert Product(x**2, (x, a, b)).change_index(x, x - 1) == \ + Product((x + 1)**2, (x, a - 1, b - 1)) + assert Product(x**2, (x, a, b)).change_index(x, -x, y) == \ + Product((-y)**2, (y, -b, -a)) + assert Product(x, (x, a, b)).change_index(x, -x - 1) == \ + Product(-x - 1, (x, - b - 1, -a - 1)) + assert Product(x*y, (x, a, b), (y, c, d)).change_index(x, x - 1, z) == \ + Product((z + 1)*y, (z, a - 1, b - 1), (y, c, d)) + + +def test_reorder(): + b, y, c, d, z = symbols('b, y, c, d, z', integer = True) + + assert Product(x*y, (x, a, b), (y, c, d)).reorder((0, 1)) == \ + Product(x*y, (y, c, d), (x, a, b)) + assert Product(x, (x, a, b), (x, c, d)).reorder((0, 1)) == \ + Product(x, (x, c, d), (x, a, b)) + assert Product(x*y + z, (x, a, b), (z, m, n), (y, c, d)).reorder(\ + (2, 0), (0, 1)) == Product(x*y + z, (z, m, n), (y, c, d), (x, a, b)) + assert Product(x*y*z, (x, a, b), (y, c, d), (z, m, n)).reorder(\ + (0, 1), (1, 2), (0, 2)) == \ + Product(x*y*z, (x, a, b), (z, m, n), (y, c, d)) + assert Product(x*y*z, (x, a, b), (y, c, d), (z, m, n)).reorder(\ + (x, y), (y, z), (x, z)) == \ + Product(x*y*z, (x, a, b), (z, m, n), (y, c, d)) + assert Product(x*y, (x, a, b), (y, c, d)).reorder((x, 1)) == \ + Product(x*y, (y, c, d), (x, a, b)) + assert Product(x*y, (x, a, b), (y, c, d)).reorder((y, x)) == \ + Product(x*y, (y, c, d), (x, a, b)) + + +def test_Product_is_convergent(): + assert Product(1/n**2, (n, 1, oo)).is_convergent() is S.false + assert Product(exp(1/n**2), (n, 1, oo)).is_convergent() is S.true + assert Product(1/n, (n, 1, oo)).is_convergent() is S.false + assert Product(1 + 1/n, (n, 1, oo)).is_convergent() is S.false + assert Product(1 + 1/n**2, (n, 1, oo)).is_convergent() is S.true + + +def test_reverse_order(): + x, y, a, b, c, d= symbols('x, y, a, b, c, d', integer = True) + + assert Product(x, (x, 0, 3)).reverse_order(0) == Product(1/x, (x, 4, -1)) + assert Product(x*y, (x, 1, 5), (y, 0, 6)).reverse_order(0, 1) == \ + Product(x*y, (x, 6, 0), (y, 7, -1)) + assert Product(x, (x, 1, 2)).reverse_order(0) == Product(1/x, (x, 3, 0)) + assert Product(x, (x, 1, 3)).reverse_order(0) == Product(1/x, (x, 4, 0)) + assert Product(x, (x, 1, a)).reverse_order(0) == Product(1/x, (x, a + 1, 0)) + assert Product(x, (x, a, 5)).reverse_order(0) == Product(1/x, (x, 6, a - 1)) + assert Product(x, (x, a + 1, a + 5)).reverse_order(0) == \ + Product(1/x, (x, a + 6, a)) + assert Product(x, (x, a + 1, a + 2)).reverse_order(0) == \ + Product(1/x, (x, a + 3, a)) + assert Product(x, (x, a + 1, a + 1)).reverse_order(0) == \ + Product(1/x, (x, a + 2, a)) + assert Product(x, (x, a, b)).reverse_order(0) == Product(1/x, (x, b + 1, a - 1)) + assert Product(x, (x, a, b)).reverse_order(x) == Product(1/x, (x, b + 1, a - 1)) + assert Product(x*y, (x, a, b), (y, 2, 5)).reverse_order(x, 1) == \ + Product(x*y, (x, b + 1, a - 1), (y, 6, 1)) + assert Product(x*y, (x, a, b), (y, 2, 5)).reverse_order(y, x) == \ + Product(x*y, (x, b + 1, a - 1), (y, 6, 1)) + + +def test_issue_9983(): + n = Symbol('n', integer=True, positive=True) + p = Product(1 + 1/n**Rational(2, 3), (n, 1, oo)) + assert p.is_convergent() is S.false + assert product(1 + 1/n**Rational(2, 3), (n, 1, oo)) == p.doit() + + +def test_issue_13546(): + n = Symbol('n') + k = Symbol('k') + p = Product(n + 1 / 2**k, (k, 0, n-1)).doit() + assert p.subs(n, 2).doit() == Rational(15, 2) + + +def test_issue_14036(): + a, n = symbols('a n') + assert product(1 - a**2 / (n*pi)**2, [n, 1, oo]) != 0 + + +def test_rewrite_Sum(): + assert Product(1 - S.Half**2/k**2, (k, 1, oo)).rewrite(Sum) == \ + exp(Sum(log(1 - 1/(4*k**2)), (k, 1, oo))) + + +def test_KroneckerDelta_Product(): + y = Symbol('y') + assert Product(x*KroneckerDelta(x, y), (x, 0, 1)).doit() == 0 + + +def test_issue_20848(): + _i = Dummy('i') + t, y, z = symbols('t y z') + assert diff(Product(x, (y, 1, z)), x).as_dummy() == Sum(Product(x, (y, 1, _i - 1))*Product(x, (y, _i + 1, z)), (_i, 1, z)).as_dummy() + assert diff(Product(x, (y, 1, z)), x).doit() == x**(z - 1)*z + assert diff(Product(x, (y, x, z)), x) == Derivative(Product(x, (y, x, z)), x) + assert diff(Product(t, (x, 1, z)), x) == S(0) + assert Product(sin(n*x), (n, -1, 1)).diff(x).doit() == S(0) diff --git a/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_sums_products.py b/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_sums_products.py new file mode 100644 index 0000000000000000000000000000000000000000..b190afe0bd403819d3525453879d7d5d39e20a56 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/concrete/tests/test_sums_products.py @@ -0,0 +1,1676 @@ +from math import prod + +from sympy.concrete.expr_with_intlimits import ReorderError +from sympy.concrete.products import (Product, product) +from sympy.concrete.summations import (Sum, summation, telescopic, + eval_sum_residue, _dummy_with_inherited_properties_concrete) +from sympy.core.function import (Derivative, Function) +from sympy.core import (Catalan, EulerGamma) +from sympy.core.facts import InconsistentAssumptions +from sympy.core.mod import Mod +from sympy.core.numbers import (E, I, Rational, nan, oo, pi) +from sympy.core.relational import Eq, Ne +from sympy.core.numbers import Float +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import (rf, binomial, factorial) +from sympy.functions.combinatorial.numbers import harmonic +from sympy.functions.elementary.complexes import Abs, re +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import (sinh, tanh) +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (cos, sin, atan) +from sympy.functions.special.gamma_functions import (gamma, lowergamma) +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.functions.special.zeta_functions import zeta +from sympy.integrals.integrals import Integral +from sympy.logic.boolalg import And, Or +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.special import Identity +from sympy.matrices import (Matrix, SparseMatrix, + ImmutableDenseMatrix, ImmutableSparseMatrix, diag) +from sympy.sets.contains import Contains +from sympy.sets.fancysets import Range +from sympy.sets.sets import Interval +from sympy.simplify.combsimp import combsimp +from sympy.simplify.simplify import simplify +from sympy.tensor.indexed import (Idx, Indexed, IndexedBase) +from sympy.testing.pytest import XFAIL, raises, slow +from sympy.abc import a, b, c, d, k, m, x, y, z + +n = Symbol('n', integer=True) +f, g = symbols('f g', cls=Function) + +def test_karr_convention(): + # Test the Karr summation convention that we want to hold. + # See his paper "Summation in Finite Terms" for a detailed + # reasoning why we really want exactly this definition. + # The convention is described on page 309 and essentially + # in section 1.4, definition 3: + # + # \sum_{m <= i < n} f(i) 'has the obvious meaning' for m < n + # \sum_{m <= i < n} f(i) = 0 for m = n + # \sum_{m <= i < n} f(i) = - \sum_{n <= i < m} f(i) for m > n + # + # It is important to note that he defines all sums with + # the upper limit being *exclusive*. + # In contrast, SymPy and the usual mathematical notation has: + # + # sum_{i = a}^b f(i) = f(a) + f(a+1) + ... + f(b-1) + f(b) + # + # with the upper limit *inclusive*. So translating between + # the two we find that: + # + # \sum_{m <= i < n} f(i) = \sum_{i = m}^{n-1} f(i) + # + # where we intentionally used two different ways to typeset the + # sum and its limits. + + i = Symbol("i", integer=True) + k = Symbol("k", integer=True) + j = Symbol("j", integer=True) + + # A simple example with a concrete summand and symbolic limits. + + # The normal sum: m = k and n = k + j and therefore m < n: + m = k + n = k + j + + a = m + b = n - 1 + S1 = Sum(i**2, (i, a, b)).doit() + + # The reversed sum: m = k + j and n = k and therefore m > n: + m = k + j + n = k + + a = m + b = n - 1 + S2 = Sum(i**2, (i, a, b)).doit() + + assert simplify(S1 + S2) == 0 + + # Test the empty sum: m = k and n = k and therefore m = n: + m = k + n = k + + a = m + b = n - 1 + Sz = Sum(i**2, (i, a, b)).doit() + + assert Sz == 0 + + # Another example this time with an unspecified summand and + # numeric limits. (We can not do both tests in the same example.) + + # The normal sum with m < n: + m = 2 + n = 11 + + a = m + b = n - 1 + S1 = Sum(f(i), (i, a, b)).doit() + + # The reversed sum with m > n: + m = 11 + n = 2 + + a = m + b = n - 1 + S2 = Sum(f(i), (i, a, b)).doit() + + assert simplify(S1 + S2) == 0 + + # Test the empty sum with m = n: + m = 5 + n = 5 + + a = m + b = n - 1 + Sz = Sum(f(i), (i, a, b)).doit() + + assert Sz == 0 + + e = Piecewise((exp(-i), Mod(i, 2) > 0), (0, True)) + s = Sum(e, (i, 0, 11)) + assert s.n(3) == s.doit().n(3) + + # issue #27893 + n = Symbol('n', integer=True) + assert Sum(1/(x**2 + 1), (x, oo, 0)).doit(deep=False) == Rational(-1, 2) + pi / (2 * tanh(pi)) + assert Sum(c**x/factorial(x), (x, oo, 0)).doit(deep=False).simplify() == exp(c) - 1 # exponential series + assert Sum((-1)**x/x, (x, oo,0)).doit() == -log(2) # alternating harmnic series + assert Sum((1/2)**x,(x, oo, -1)).doit() == S(2) # geometric series + assert Sum(1/x, (x, oo, 0)).doit() == oo # harmonic series, divergent + assert Sum((-1)**x/(2*x+1), (x, oo, -1)).doit() == pi/4 # leibniz series + assert Sum((((-1)**x) * c**(2*x+1)) / factorial(2*x+1), (x, oo, -1)).doit() == sin(c) # sinusoidal series + assert Sum((((-1)**x) * c**(2*x+1)) / (2*x+1), (x, 0, oo)).doit() \ + == Piecewise((atan(c), Ne(c**2, -1) & (Abs(c**2) <= 1)), \ + (Sum((-1)**x*c**(2*x + 1)/(2*x + 1), (x, 0, oo)), True)) # arctangent series + assert Sum(binomial(n, x) * c**x, (x, 0, oo)).doit() \ + == Piecewise(((c + 1)**n, \ + ((n <= -1) & (Abs(c) < 1)) \ + | ((n > 0) & (Abs(c) <= 1)) \ + | ((n <= 0) & (n > -1) & Ne(c, -1) & (Abs(c) <= 1))), \ + (Sum(c**x*binomial(n, x), (x, 0, oo)), True)) # binomial series + assert Sum(1/x**n, (x, oo, 0)).doit() \ + == Piecewise((zeta(n), n > 1), (Sum(x**(-n), (x, oo, 0)), True)) # Euler's zeta function + +def test_karr_proposition_2a(): + # Test Karr, page 309, proposition 2, part a + i = Symbol("i", integer=True) + u = Symbol("u", integer=True) + v = Symbol("v", integer=True) + + def test_the_sum(m, n): + # g + g = i**3 + 2*i**2 - 3*i + # f = Delta g + f = simplify(g.subs(i, i+1) - g) + # The sum + a = m + b = n - 1 + S = Sum(f, (i, a, b)).doit() + # Test if Sum_{m <= i < n} f(i) = g(n) - g(m) + assert simplify(S - (g.subs(i, n) - g.subs(i, m))) == 0 + + # m < n + test_the_sum(u, u+v) + # m = n + test_the_sum(u, u ) + # m > n + test_the_sum(u+v, u ) + + +def test_karr_proposition_2b(): + # Test Karr, page 309, proposition 2, part b + i = Symbol("i", integer=True) + u = Symbol("u", integer=True) + v = Symbol("v", integer=True) + w = Symbol("w", integer=True) + + def test_the_sum(l, n, m): + # Summand + s = i**3 + # First sum + a = l + b = n - 1 + S1 = Sum(s, (i, a, b)).doit() + # Second sum + a = l + b = m - 1 + S2 = Sum(s, (i, a, b)).doit() + # Third sum + a = m + b = n - 1 + S3 = Sum(s, (i, a, b)).doit() + # Test if S1 = S2 + S3 as required + assert S1 - (S2 + S3) == 0 + + # l < m < n + test_the_sum(u, u+v, u+v+w) + # l < m = n + test_the_sum(u, u+v, u+v ) + # l < m > n + test_the_sum(u, u+v+w, v ) + # l = m < n + test_the_sum(u, u, u+v ) + # l = m = n + test_the_sum(u, u, u ) + # l = m > n + test_the_sum(u+v, u+v, u ) + # l > m < n + test_the_sum(u+v, u, u+w ) + # l > m = n + test_the_sum(u+v, u, u ) + # l > m > n + test_the_sum(u+v+w, u+v, u ) + + +def test_arithmetic_sums(): + assert summation(1, (n, a, b)) == b - a + 1 + assert Sum(S.NaN, (n, a, b)) is S.NaN + assert Sum(x, (n, a, a)).doit() == x + assert Sum(x, (x, a, a)).doit() == a + assert Sum(x, (n, 1, a)).doit() == a*x + assert Sum(x, (x, Range(1, 11))).doit() == 55 + assert Sum(x, (x, Range(1, 11, 2))).doit() == 25 + assert Sum(x, (x, Range(1, 10, 2))) == Sum(x, (x, Range(9, 0, -2))) + lo, hi = 1, 2 + s1 = Sum(n, (n, lo, hi)) + s2 = Sum(n, (n, hi, lo)) + assert s1 != s2 + assert s1.doit() == 3 and s2.doit() == 0 + lo, hi = x, x + 1 + s1 = Sum(n, (n, lo, hi)) + s2 = Sum(n, (n, hi, lo)) + assert s1 != s2 + assert s1.doit() == 2*x + 1 and s2.doit() == 0 + assert Sum(Integral(x, (x, 1, y)) + x, (x, 1, 2)).doit() == \ + y**2 + 2 + assert summation(1, (n, 1, 10)) == 10 + assert summation(2*n, (n, 0, 10**10)) == 100000000010000000000 + assert summation(4*n*m, (n, a, 1), (m, 1, d)).expand() == \ + 2*d + 2*d**2 + a*d + a*d**2 - d*a**2 - a**2*d**2 + assert summation(cos(n), (n, -2, 1)) == cos(-2) + cos(-1) + cos(0) + cos(1) + assert summation(cos(n), (n, x, x + 2)) == cos(x) + cos(x + 1) + cos(x + 2) + assert isinstance(summation(cos(n), (n, x, x + S.Half)), Sum) + assert summation(k, (k, 0, oo)) is oo + assert summation(k, (k, Range(1, 11))) == 55 + + +def test_polynomial_sums(): + assert summation(n**2, (n, 3, 8)) == 199 + assert summation(n, (n, a, b)) == \ + ((a + b)*(b - a + 1)/2).expand() + assert summation(n**2, (n, 1, b)) == \ + ((2*b**3 + 3*b**2 + b)/6).expand() + assert summation(n**3, (n, 1, b)) == \ + ((b**4 + 2*b**3 + b**2)/4).expand() + assert summation(n**6, (n, 1, b)) == \ + ((6*b**7 + 21*b**6 + 21*b**5 - 7*b**3 + b)/42).expand() + + +def test_geometric_sums(): + assert summation(pi**n, (n, 0, b)) == (1 - pi**(b + 1)) / (1 - pi) + assert summation(2 * 3**n, (n, 0, b)) == 3**(b + 1) - 1 + assert summation(S.Half**n, (n, 1, oo)) == 1 + assert summation(2**n, (n, 0, b)) == 2**(b + 1) - 1 + assert summation(2**n, (n, 1, oo)) is oo + assert summation(2**(-n), (n, 1, oo)) == 1 + assert summation(3**(-n), (n, 4, oo)) == Rational(1, 54) + assert summation(2**(-4*n + 3), (n, 1, oo)) == Rational(8, 15) + assert summation(2**(n + 1), (n, 1, b)).expand() == 4*(2**b - 1) + + # issue 6664: + assert summation(x**n, (n, 0, oo)) == \ + Piecewise((1/(-x + 1), Abs(x) < 1), (Sum(x**n, (n, 0, oo)), True)) + + assert summation(-2**n, (n, 0, oo)) is -oo + assert summation(I**n, (n, 0, oo)) == Sum(I**n, (n, 0, oo)) + + # issue 6802: + assert summation((-1)**(2*x + 2), (x, 0, n)) == n + 1 + assert summation((-2)**(2*x + 2), (x, 0, n)) == 4*4**(n + 1)/S(3) - Rational(4, 3) + assert summation((-1)**x, (x, 0, n)) == -(-1)**(n + 1)/S(2) + S.Half + assert summation(y**x, (x, a, b)) == \ + Piecewise((-a + b + 1, Eq(y, 1)), ((y**a - y**(b + 1))/(-y + 1), True)) + assert summation((-2)**(y*x + 2), (x, 0, n)) == \ + 4*Piecewise((n + 1, Eq((-2)**y, 1)), + ((-(-2)**(y*(n + 1)) + 1)/(-(-2)**y + 1), True)) + + # issue 8251: + assert summation((1/(n + 1)**2)*n**2, (n, 0, oo)) is oo + + #issue 9908: + assert Sum(1/(n**3 - 1), (n, -oo, -2)).doit() == summation(1/(n**3 - 1), (n, -oo, -2)) + + #issue 11642: + result = Sum(0.5**n, (n, 1, oo)).doit() + assert result == 1.0 + assert result.is_Float + + result = Sum(0.25**n, (n, 1, oo)).doit() + assert result == 1/3. + assert result.is_Float + + result = Sum(0.99999**n, (n, 1, oo)).doit() + assert result == 99999.0 + assert result.is_Float + + result = Sum(S.Half**n, (n, 1, oo)).doit() + assert result == 1 + assert not result.is_Float + + result = Sum(Rational(3, 5)**n, (n, 1, oo)).doit() + assert result == Rational(3, 2) + assert not result.is_Float + + assert Sum(1.0**n, (n, 1, oo)).doit() is oo + assert Sum(2.43**n, (n, 1, oo)).doit() is oo + + # Issue 13979 + i, k, q = symbols('i k q', integer=True) + result = summation( + exp(-2*I*pi*k*i/n) * exp(2*I*pi*q*i/n) / n, (i, 0, n - 1) + ) + assert result.simplify() == Piecewise( + (1, Eq(exp(-2*I*pi*(k - q)/n), 1)), (0, True) + ) + + #Issue 23491 + assert Sum(1/(n**2 + 1), (n, 1, oo)).doit() == S(-1)/2 + pi/(2*tanh(pi)) + +def test_harmonic_sums(): + assert summation(1/k, (k, 0, n)) == Sum(1/k, (k, 0, n)) + assert summation(1/k, (k, 1, n)) == harmonic(n) + assert summation(n/k, (k, 1, n)) == n*harmonic(n) + assert summation(1/k, (k, 5, n)) == harmonic(n) - harmonic(4) + + +def test_composite_sums(): + f = S.Half*(7 - 6*n + Rational(1, 7)*n**3) + s = summation(f, (n, a, b)) + assert not isinstance(s, Sum) + A = 0 + for i in range(-3, 5): + A += f.subs(n, i) + B = s.subs(a, -3).subs(b, 4) + assert A == B + + +def test_hypergeometric_sums(): + assert summation( + binomial(2*k, k)/4**k, (k, 0, n)) == (1 + 2*n)*binomial(2*n, n)/4**n + assert summation(binomial(2*k, k)/5**k, (k, -oo, oo)) == sqrt(5) + + +def test_other_sums(): + f = m**2 + m*exp(m) + g = 3*exp(Rational(3, 2))/2 + exp(S.Half)/2 - exp(Rational(-1, 2))/2 - 3*exp(Rational(-3, 2))/2 + 5 + + assert summation(f, (m, Rational(-3, 2), Rational(3, 2))) == g + assert summation(f, (m, -1.5, 1.5)).evalf().epsilon_eq(g.evalf(), 1e-10) + +fac = factorial + + +def NS(e, n=15, **options): + return str(sympify(e).evalf(n, **options)) + + +def test_evalf_fast_series(): + # Euler transformed series for sqrt(1+x) + assert NS(Sum( + fac(2*n + 1)/fac(n)**2/2**(3*n + 1), (n, 0, oo)), 100) == NS(sqrt(2), 100) + + # Some series for exp(1) + estr = NS(E, 100) + assert NS(Sum(1/fac(n), (n, 0, oo)), 100) == estr + assert NS(1/Sum((1 - 2*n)/fac(2*n), (n, 0, oo)), 100) == estr + assert NS(Sum((2*n + 1)/fac(2*n), (n, 0, oo)), 100) == estr + assert NS(Sum((4*n + 3)/2**(2*n + 1)/fac(2*n + 1), (n, 0, oo))**2, 100) == estr + + pistr = NS(pi, 100) + # Ramanujan series for pi + assert NS(9801/sqrt(8)/Sum(fac( + 4*n)*(1103 + 26390*n)/fac(n)**4/396**(4*n), (n, 0, oo)), 100) == pistr + assert NS(1/Sum( + binomial(2*n, n)**3 * (42*n + 5)/2**(12*n + 4), (n, 0, oo)), 100) == pistr + # Machin's formula for pi + assert NS(16*Sum((-1)**n/(2*n + 1)/5**(2*n + 1), (n, 0, oo)) - + 4*Sum((-1)**n/(2*n + 1)/239**(2*n + 1), (n, 0, oo)), 100) == pistr + + # Apery's constant + astr = NS(zeta(3), 100) + P = 126392*n**5 + 412708*n**4 + 531578*n**3 + 336367*n**2 + 104000* \ + n + 12463 + assert NS(Sum((-1)**n * P / 24 * (fac(2*n + 1)*fac(2*n)*fac( + n))**3 / fac(3*n + 2) / fac(4*n + 3)**3, (n, 0, oo)), 100) == astr + assert NS(Sum((-1)**n * (205*n**2 + 250*n + 77)/64 * fac(n)**10 / + fac(2*n + 1)**5, (n, 0, oo)), 100) == astr + + +def test_evalf_fast_series_issue_4021(): + # Catalan's constant + assert NS(Sum((-1)**(n - 1)*2**(8*n)*(40*n**2 - 24*n + 3)*fac(2*n)**3* + fac(n)**2/n**3/(2*n - 1)/fac(4*n)**2, (n, 1, oo))/64, 100) == \ + NS(Catalan, 100) + astr = NS(zeta(3), 100) + assert NS(5*Sum( + (-1)**(n - 1)*fac(n)**2 / n**3 / fac(2*n), (n, 1, oo))/2, 100) == astr + assert NS(Sum((-1)**(n - 1)*(56*n**2 - 32*n + 5) / (2*n - 1)**2 * fac(n - 1) + **3 / fac(3*n), (n, 1, oo))/4, 100) == astr + + +def test_evalf_slow_series(): + assert NS(Sum((-1)**n / n, (n, 1, oo)), 15) == NS(-log(2), 15) + assert NS(Sum((-1)**n / n, (n, 1, oo)), 50) == NS(-log(2), 50) + assert NS(Sum(1/n**2, (n, 1, oo)), 15) == NS(pi**2/6, 15) + assert NS(Sum(1/n**2, (n, 1, oo)), 100) == NS(pi**2/6, 100) + assert NS(Sum(1/n**2, (n, 1, oo)), 500) == NS(pi**2/6, 500) + assert NS(Sum((-1)**n / (2*n + 1)**3, (n, 0, oo)), 15) == NS(pi**3/32, 15) + assert NS(Sum((-1)**n / (2*n + 1)**3, (n, 0, oo)), 50) == NS(pi**3/32, 50) + + +def test_evalf_oo_to_oo(): + # There used to be an error in certain cases + # Does not evaluate, but at least do not throw an error + # Evaluates symbolically to 0, which is not correct + assert Sum(1/(n**2+1), (n, -oo, oo)).evalf() == Sum(1/(n**2+1), (n, -oo, oo)) + # This evaluates if from 1 to oo and symbolically + assert Sum(1/(factorial(abs(n))), (n, -oo, -1)).evalf() == Sum(1/(factorial(abs(n))), (n, -oo, -1)) + + +def test_euler_maclaurin(): + # Exact polynomial sums with E-M + def check_exact(f, a, b, m, n): + A = Sum(f, (k, a, b)) + s, e = A.euler_maclaurin(m, n) + assert (e == 0) and (s.expand() == A.doit()) + check_exact(k**4, a, b, 0, 2) + check_exact(k**4 + 2*k, a, b, 1, 2) + check_exact(k**4 + k**2, a, b, 1, 5) + check_exact(k**5, 2, 6, 1, 2) + check_exact(k**5, 2, 6, 1, 3) + assert Sum(x-1, (x, 0, 2)).euler_maclaurin(m=30, n=30, eps=2**-15) == (0, 0) + # Not exact + assert Sum(k**6, (k, a, b)).euler_maclaurin(0, 2)[1] != 0 + # Numerical test + for mi, ni in [(2, 4), (2, 20), (10, 20), (18, 20)]: + A = Sum(1/k**3, (k, 1, oo)) + s, e = A.euler_maclaurin(mi, ni) + assert abs((s - zeta(3)).evalf()) < e.evalf() + + raises(ValueError, lambda: Sum(1, (x, 0, 1), (k, 0, 1)).euler_maclaurin()) + + +@slow +def test_evalf_euler_maclaurin(): + assert NS(Sum(1/k**k, (k, 1, oo)), 15) == '1.29128599706266' + assert NS(Sum(1/k**k, (k, 1, oo)), + 50) == '1.2912859970626635404072825905956005414986193682745' + assert NS(Sum(1/k - log(1 + 1/k), (k, 1, oo)), 15) == NS(EulerGamma, 15) + assert NS(Sum(1/k - log(1 + 1/k), (k, 1, oo)), 50) == NS(EulerGamma, 50) + assert NS(Sum(log(k)/k**2, (k, 1, oo)), 15) == '0.937548254315844' + assert NS(Sum(log(k)/k**2, (k, 1, oo)), + 50) == '0.93754825431584375370257409456786497789786028861483' + assert NS(Sum(1/k, (k, 1000000, 2000000)), 15) == '0.693147930560008' + assert NS(Sum(1/k, (k, 1000000, 2000000)), + 50) == '0.69314793056000780941723211364567656807940638436025' + + +def test_evalf_symbolic(): + # issue 6328 + expr = Sum(f(x), (x, 1, 3)) + Sum(g(x), (x, 1, 3)) + assert expr.evalf() == expr + + +def test_evalf_issue_3273(): + assert Sum(0, (k, 1, oo)).evalf() == 0 + + +def test_simple_products(): + assert Product(S.NaN, (x, 1, 3)) is S.NaN + assert product(S.NaN, (x, 1, 3)) is S.NaN + assert Product(x, (n, a, a)).doit() == x + assert Product(x, (x, a, a)).doit() == a + assert Product(x, (y, 1, a)).doit() == x**a + + lo, hi = 1, 2 + s1 = Product(n, (n, lo, hi)) + s2 = Product(n, (n, hi, lo)) + assert s1 != s2 + # This IS correct according to Karr product convention + assert s1.doit() == 2 + assert s2.doit() == 1 + + lo, hi = x, x + 1 + s1 = Product(n, (n, lo, hi)) + s2 = Product(n, (n, hi, lo)) + s3 = 1 / Product(n, (n, hi + 1, lo - 1)) + assert s1 != s2 + # This IS correct according to Karr product convention + assert s1.doit() == x*(x + 1) + assert s2.doit() == 1 + assert s3.doit() == x*(x + 1) + + assert Product(Integral(2*x, (x, 1, y)) + 2*x, (x, 1, 2)).doit() == \ + (y**2 + 1)*(y**2 + 3) + assert product(2, (n, a, b)) == 2**(b - a + 1) + assert product(n, (n, 1, b)) == factorial(b) + assert product(n**3, (n, 1, b)) == factorial(b)**3 + assert product(3**(2 + n), (n, a, b)) \ + == 3**(2*(1 - a + b) + b/2 + (b**2)/2 + a/2 - (a**2)/2) + assert product(cos(n), (n, 3, 5)) == cos(3)*cos(4)*cos(5) + assert product(cos(n), (n, x, x + 2)) == cos(x)*cos(x + 1)*cos(x + 2) + assert isinstance(product(cos(n), (n, x, x + S.Half)), Product) + # If Product managed to evaluate this one, it most likely got it wrong! + assert isinstance(Product(n**n, (n, 1, b)), Product) + + +def test_rational_products(): + assert combsimp(product(1 + 1/n, (n, a, b))) == (1 + b)/a + assert combsimp(product(n + 1, (n, a, b))) == gamma(2 + b)/gamma(1 + a) + assert combsimp(product((n + 1)/(n - 1), (n, a, b))) == b*(1 + b)/(a*(a - 1)) + assert combsimp(product(n/(n + 1)/(n + 2), (n, a, b))) == \ + a*gamma(a + 2)/(b + 1)/gamma(b + 3) + assert combsimp(product(n*(n + 1)/(n - 1)/(n - 2), (n, a, b))) == \ + b**2*(b - 1)*(1 + b)/(a - 1)**2/(a*(a - 2)) + + +def test_wallis_product(): + # Wallis product, given in two different forms to ensure that Product + # can factor simple rational expressions + A = Product(4*n**2 / (4*n**2 - 1), (n, 1, b)) + B = Product((2*n)*(2*n)/(2*n - 1)/(2*n + 1), (n, 1, b)) + R = pi*gamma(b + 1)**2/(2*gamma(b + S.Half)*gamma(b + Rational(3, 2))) + assert simplify(A.doit()) == R + assert simplify(B.doit()) == R + # This one should eventually also be doable (Euler's product formula for sin) + # assert Product(1+x/n**2, (n, 1, b)) == ... + + +def test_telescopic_sums(): + #checks also input 2 of comment 1 issue 4127 + assert Sum(1/k - 1/(k + 1), (k, 1, n)).doit() == 1 - 1/(1 + n) + assert Sum( + f(k) - f(k + 2), (k, m, n)).doit() == -f(1 + n) - f(2 + n) + f(m) + f(1 + m) + assert Sum(cos(k) - cos(k + 3), (k, 1, n)).doit() == -cos(1 + n) - \ + cos(2 + n) - cos(3 + n) + cos(1) + cos(2) + cos(3) + + # dummy variable shouldn't matter + assert telescopic(1/m, -m/(1 + m), (m, n - 1, n)) == \ + telescopic(1/k, -k/(1 + k), (k, n - 1, n)) + + assert Sum(1/x/(x - 1), (x, a, b)).doit() == 1/(a - 1) - 1/b + eq = 1/((5*n + 2)*(5*(n + 1) + 2)) + assert Sum(eq, (n, 0, oo)).doit() == S(1)/10 + nz = symbols('nz', nonzero=True) + v = Sum(eq.subs(5, nz), (n, 0, oo)).doit() + assert v.subs(nz, 5).simplify() == S(1)/10 + # check that apart is being used in non-symbolic case + s = Sum(eq, (n, 0, k)).doit() + v = Sum(eq, (n, 0, 10**100)).doit() + assert v == s.subs(k, 10**100) + + +def test_sum_reconstruct(): + s = Sum(n**2, (n, -1, 1)) + assert s == Sum(*s.args) + raises(ValueError, lambda: Sum(x, x)) + raises(ValueError, lambda: Sum(x, (x, 1))) + + +def test_limit_subs(): + for F in (Sum, Product, Integral): + assert F(a*exp(a), (a, -2, 2)) == F(a*exp(a), (a, -b, b)).subs(b, 2) + assert F(a, (a, F(b, (b, 1, 2)), 4)).subs(F(b, (b, 1, 2)), c) == \ + F(a, (a, c, 4)) + assert F(x, (x, 1, x + y)).subs(x, 1) == F(x, (x, 1, y + 1)) + + +def test_function_subs(): + S = Sum(x*f(y),(x,0,oo),(y,0,oo)) + assert S.subs(f(y),y) == Sum(x*y,(x,0,oo),(y,0,oo)) + assert S.subs(f(x),x) == S + raises(ValueError, lambda: S.subs(f(y),x+y) ) + S = Sum(x*log(y),(x,0,oo),(y,0,oo)) + assert S.subs(log(y),y) == S + S = Sum(x*f(y),(x,0,oo),(y,0,oo)) + assert S.subs(f(y),y) == Sum(x*y,(x,0,oo),(y,0,oo)) + + +def test_equality(): + # if this fails remove special handling below + raises(ValueError, lambda: Sum(x, x)) + r = symbols('x', real=True) + for F in (Sum, Product, Integral): + try: + assert F(x, x) != F(y, y) + assert F(x, (x, 1, 2)) != F(x, x) + assert F(x, (x, x)) != F(x, x) # or else they print the same + assert F(1, x) != F(1, y) + except ValueError: + pass + assert F(a, (x, 1, 2)) != F(a, (x, 1, 3)) # diff limit + assert F(a, (x, 1, x)) != F(a, (y, 1, y)) + assert F(a, (x, 1, 2)) != F(b, (x, 1, 2)) # diff expression + assert F(x, (x, 1, 2)) != F(r, (r, 1, 2)) # diff assumptions + assert F(1, (x, 1, x)) != F(1, (y, 1, x)) # only dummy is diff + assert F(1, (x, 1, x)).dummy_eq(F(1, (y, 1, x))) + + # issue 5265 + assert Sum(x, (x, 1, x)).subs(x, a) == Sum(x, (x, 1, a)) + + +def test_Sum_doit(): + assert Sum(n*Integral(a**2), (n, 0, 2)).doit() == a**3 + assert Sum(n*Integral(a**2), (n, 0, 2)).doit(deep=False) == \ + 3*Integral(a**2) + assert summation(n*Integral(a**2), (n, 0, 2)) == 3*Integral(a**2) + + # test nested sum evaluation + s = Sum( Sum( Sum(2,(z,1,n+1)), (y,x+1,n)), (x,1,n)) + assert 0 == (s.doit() - n*(n+1)*(n-1)).factor() + + # Integer assumes finite + assert Sum(KroneckerDelta(x, y), (x, -oo, oo)).doit() == Piecewise((1, And(-oo < y, y < oo)), (0, True)) + assert Sum(KroneckerDelta(m, n), (m, -oo, oo)).doit() == 1 + assert Sum(m*KroneckerDelta(x, y), (x, -oo, oo)).doit() == Piecewise((m, And(-oo < y, y < oo)), (0, True)) + assert Sum(x*KroneckerDelta(m, n), (m, -oo, oo)).doit() == x + assert Sum(Sum(KroneckerDelta(m, n), (m, 1, 3)), (n, 1, 3)).doit() == 3 + assert Sum(Sum(KroneckerDelta(k, m), (m, 1, 3)), (n, 1, 3)).doit() == \ + 3 * Piecewise((1, And(1 <= k, k <= 3)), (0, True)) + assert Sum(f(n) * Sum(KroneckerDelta(m, n), (m, 0, oo)), (n, 1, 3)).doit() == \ + f(1) + f(2) + f(3) + assert Sum(f(n) * Sum(KroneckerDelta(m, n), (m, 0, oo)), (n, 1, oo)).doit() == \ + Sum(f(n), (n, 1, oo)) + + # issue 2597 + nmax = symbols('N', integer=True, positive=True) + pw = Piecewise((1, And(1 <= n, n <= nmax)), (0, True)) + assert Sum(pw, (n, 1, nmax)).doit() == Sum(Piecewise((1, nmax >= n), + (0, True)), (n, 1, nmax)) + + q, s = symbols('q, s') + assert summation(1/n**(2*s), (n, 1, oo)) == Piecewise((zeta(2*s), 2*re(s) > 1), + (Sum(n**(-2*s), (n, 1, oo)), True)) + assert summation(1/(n+1)**s, (n, 0, oo)) == Piecewise((zeta(s), re(s) > 1), + (Sum((n + 1)**(-s), (n, 0, oo)), True)) + assert summation(1/(n+q)**s, (n, 0, oo)) == Piecewise( + (zeta(s, q), And(~Contains(-q, S.Naturals0), re(s) > 1)), + (Sum((n + q)**(-s), (n, 0, oo)), True)) + assert summation(1/(n+q)**s, (n, q, oo)) == Piecewise( + (zeta(s, 2*q), And(~Contains(-2*q, S.Naturals0), re(s) > 1)), + (Sum((n + q)**(-s), (n, q, oo)), True)) + assert summation(1/n**2, (n, 1, oo)) == zeta(2) + assert summation(1/n**s, (n, 0, oo)) == Sum(n**(-s), (n, 0, oo)) + assert summation(1/(n+1)**(2+I), (n, 0, oo)) == zeta(2+I) + t = symbols('t', real=True, positive=True) + assert summation(1/(n+I)**(t+1), (n, 0, oo)) == zeta(t+1, I) + + +def test_Product_doit(): + assert Product(n*Integral(a**2), (n, 1, 3)).doit() == 2 * a**9 / 9 + assert Product(n*Integral(a**2), (n, 1, 3)).doit(deep=False) == \ + 6*Integral(a**2)**3 + assert product(n*Integral(a**2), (n, 1, 3)) == 6*Integral(a**2)**3 + + +def test_Sum_interface(): + assert isinstance(Sum(0, (n, 0, 2)), Sum) + assert Sum(nan, (n, 0, 2)) is nan + assert Sum(nan, (n, 0, oo)) is nan + assert Sum(0, (n, 0, 2)).doit() == 0 + assert isinstance(Sum(0, (n, 0, oo)), Sum) + assert Sum(0, (n, 0, oo)).doit() == 0 + raises(ValueError, lambda: Sum(1)) + raises(ValueError, lambda: summation(1)) + + +def test_diff(): + assert Sum(x, (x, 1, 2)).diff(x) == 0 + assert Sum(x*y, (x, 1, 2)).diff(x) == 0 + assert Sum(x*y, (y, 1, 2)).diff(x) == Sum(y, (y, 1, 2)) + e = Sum(x*y, (x, 1, a)) + assert e.diff(a) == Derivative(e, a) + assert Sum(x*y, (x, 1, 3), (a, 2, 5)).diff(y).doit() == \ + Sum(x*y, (x, 1, 3), (a, 2, 5)).doit().diff(y) == 24 + assert Sum(x, (x, 1, 2)).diff(y) == 0 + + +def test_hypersum(): + assert simplify(summation(x**n/fac(n), (n, 1, oo))) == -1 + exp(x) + assert summation((-1)**n * x**(2*n) / fac(2*n), (n, 0, oo)) == cos(x) + assert simplify(summation((-1)**n*x**(2*n + 1) / + factorial(2*n + 1), (n, 3, oo))) == -x + sin(x) + x**3/6 - x**5/120 + + assert summation(1/(n + 2)**3, (n, 1, oo)) == Rational(-9, 8) + zeta(3) + assert summation(1/n**4, (n, 1, oo)) == pi**4/90 + + s = summation(x**n*n, (n, -oo, 0)) + assert s.is_Piecewise + assert s.args[0].args[0] == -1/(x*(1 - 1/x)**2) + assert s.args[0].args[1] == (abs(1/x) < 1) + + m = Symbol('n', integer=True, positive=True) + assert summation(binomial(m, k), (k, 0, m)) == 2**m + + +def test_issue_4170(): + assert summation(1/factorial(k), (k, 0, oo)) == E + + +def test_is_commutative(): + from sympy.physics.secondquant import NO, F, Fd + m = Symbol('m', commutative=False) + for f in (Sum, Product, Integral): + assert f(z, (z, 1, 1)).is_commutative is True + assert f(z*y, (z, 1, 6)).is_commutative is True + assert f(m*x, (x, 1, 2)).is_commutative is False + + assert f(NO(Fd(x)*F(y))*z, (z, 1, 2)).is_commutative is False + + +def test_is_zero(): + for func in [Sum, Product]: + assert func(0, (x, 1, 1)).is_zero is True + assert func(x, (x, 1, 1)).is_zero is None + + assert Sum(0, (x, 1, 0)).is_zero is True + assert Product(0, (x, 1, 0)).is_zero is False + + +def test_is_number(): + # is number should not rely on evaluation or assumptions, + # it should be equivalent to `not foo.free_symbols` + assert Sum(1, (x, 1, 1)).is_number is True + assert Sum(1, (x, 1, x)).is_number is False + assert Sum(0, (x, y, z)).is_number is False + assert Sum(x, (y, 1, 2)).is_number is False + assert Sum(x, (y, 1, 1)).is_number is False + assert Sum(x, (x, 1, 2)).is_number is True + assert Sum(x*y, (x, 1, 2), (y, 1, 3)).is_number is True + + assert Product(2, (x, 1, 1)).is_number is True + assert Product(2, (x, 1, y)).is_number is False + assert Product(0, (x, y, z)).is_number is False + assert Product(1, (x, y, z)).is_number is False + assert Product(x, (y, 1, x)).is_number is False + assert Product(x, (y, 1, 2)).is_number is False + assert Product(x, (y, 1, 1)).is_number is False + assert Product(x, (x, 1, 2)).is_number is True + + +def test_free_symbols(): + for func in [Sum, Product]: + assert func(1, (x, 1, 2)).free_symbols == set() + assert func(0, (x, 1, y)).free_symbols == {y} + assert func(2, (x, 1, y)).free_symbols == {y} + assert func(x, (x, 1, 2)).free_symbols == set() + assert func(x, (x, 1, y)).free_symbols == {y} + assert func(x, (y, 1, y)).free_symbols == {x, y} + assert func(x, (y, 1, 2)).free_symbols == {x} + assert func(x, (y, 1, 1)).free_symbols == {x} + assert func(x, (y, 1, z)).free_symbols == {x, z} + assert func(x, (x, 1, y), (y, 1, 2)).free_symbols == set() + assert func(x, (x, 1, y), (y, 1, z)).free_symbols == {z} + assert func(x, (x, 1, y), (y, 1, y)).free_symbols == {y} + assert func(x, (y, 1, y), (y, 1, z)).free_symbols == {x, z} + assert Sum(1, (x, 1, y)).free_symbols == {y} + # free_symbols answers whether the object *as written* has free symbols, + # not whether the evaluated expression has free symbols + assert Product(1, (x, 1, y)).free_symbols == {y} + # don't count free symbols that are not independent of integration + # variable(s) + assert func(f(x), (f(x), 1, 2)).free_symbols == set() + assert func(f(x), (f(x), 1, x)).free_symbols == {x} + assert func(f(x), (f(x), 1, y)).free_symbols == {y} + assert func(f(x), (z, 1, y)).free_symbols == {x, y} + + +def test_conjugate_transpose(): + A, B = symbols("A B", commutative=False) + p = Sum(A*B**n, (n, 1, 3)) + assert p.adjoint().doit() == p.doit().adjoint() + assert p.conjugate().doit() == p.doit().conjugate() + assert p.transpose().doit() == p.doit().transpose() + + p = Sum(B**n*A, (n, 1, 3)) + assert p.adjoint().doit() == p.doit().adjoint() + assert p.conjugate().doit() == p.doit().conjugate() + assert p.transpose().doit() == p.doit().transpose() + + +def test_noncommutativity_honoured(): + A, B = symbols("A B", commutative=False) + M = symbols('M', integer=True, positive=True) + p = Sum(A*B**n, (n, 1, M)) + assert p.doit() == A*Piecewise((M, Eq(B, 1)), + ((B - B**(M + 1))*(1 - B)**(-1), True)) + + p = Sum(B**n*A, (n, 1, M)) + assert p.doit() == Piecewise((M, Eq(B, 1)), + ((B - B**(M + 1))*(1 - B)**(-1), True))*A + + p = Sum(B**n*A*B**n, (n, 1, M)) + assert p.doit() == p + + +def test_issue_4171(): + assert summation(factorial(2*k + 1)/factorial(2*k), (k, 0, oo)) is oo + assert summation(2*k + 1, (k, 0, oo)) is oo + + +def test_issue_6273(): + assert Sum(x, (x, 1, n)).n(2, subs={n: 1}) == Float(1, 2) + + +def test_issue_6274(): + assert Sum(x, (x, 1, 0)).doit() == 0 + assert NS(Sum(x, (x, 1, 0))) == '0' + assert Sum(n, (n, 10, 5)).doit() == -30 + assert NS(Sum(n, (n, 10, 5))) == '-30.0000000000000' + + +def test_simplify_sum(): + y, t, v = symbols('y, t, v') + + _simplify = lambda e: simplify(e, doit=False) + assert _simplify(Sum(x*y, (x, n, m), (y, a, k)) + \ + Sum(y, (x, n, m), (y, a, k))) == Sum(y * (x + 1), (x, n, m), (y, a, k)) + assert _simplify(Sum(x, (x, n, m)) + Sum(x, (x, m + 1, a))) == \ + Sum(x, (x, n, a)) + assert _simplify(Sum(x, (x, k + 1, a)) + Sum(x, (x, n, k))) == \ + Sum(x, (x, n, a)) + assert _simplify(Sum(x, (x, k + 1, a)) + Sum(x + 1, (x, n, k))) == \ + Sum(x, (x, n, a)) + Sum(1, (x, n, k)) + assert _simplify(Sum(x, (x, 0, 3)) * 3 + 3 * Sum(x, (x, 4, 6)) + \ + 4 * Sum(z, (z, 0, 1))) == 4*Sum(z, (z, 0, 1)) + 3*Sum(x, (x, 0, 6)) + assert _simplify(3*Sum(x**2, (x, a, b)) + Sum(x, (x, a, b))) == \ + Sum(x*(3*x + 1), (x, a, b)) + assert _simplify(Sum(x**3, (x, n, k)) * 3 + 3 * Sum(x, (x, n, k)) + \ + 4 * y * Sum(z, (z, n, k))) + 1 == \ + 4*y*Sum(z, (z, n, k)) + 3*Sum(x**3 + x, (x, n, k)) + 1 + assert _simplify(Sum(x, (x, a, b)) + 1 + Sum(x, (x, b + 1, c))) == \ + 1 + Sum(x, (x, a, c)) + assert _simplify(Sum(x, (t, a, b)) + Sum(y, (t, a, b)) + \ + Sum(x, (t, b+1, c))) == x * Sum(1, (t, a, c)) + y * Sum(1, (t, a, b)) + assert _simplify(Sum(x, (t, a, b)) + Sum(x, (t, b+1, c)) + \ + Sum(y, (t, a, b))) == x * Sum(1, (t, a, c)) + y * Sum(1, (t, a, b)) + assert _simplify(Sum(x, (t, a, b)) + 2 * Sum(x, (t, b+1, c))) == \ + _simplify(Sum(x, (t, a, b)) + Sum(x, (t, b+1, c)) + Sum(x, (t, b+1, c))) + assert _simplify(Sum(x, (x, a, b))*Sum(x**2, (x, a, b))) == \ + Sum(x, (x, a, b)) * Sum(x**2, (x, a, b)) + assert _simplify(Sum(x, (t, a, b)) + Sum(y, (t, a, b)) + Sum(z, (t, a, b))) \ + == (x + y + z) * Sum(1, (t, a, b)) # issue 8596 + assert _simplify(Sum(x, (t, a, b)) + Sum(y, (t, a, b)) + Sum(z, (t, a, b)) + \ + Sum(v, (t, a, b))) == (x + y + z + v) * Sum(1, (t, a, b)) # issue 8596 + assert _simplify(Sum(x * y, (x, a, b)) / (3 * y)) == \ + (Sum(x, (x, a, b)) / 3) + assert _simplify(Sum(f(x) * y * z, (x, a, b)) / (y * z)) \ + == Sum(f(x), (x, a, b)) + assert _simplify(Sum(c * x, (x, a, b)) - c * Sum(x, (x, a, b))) == 0 + assert _simplify(c * (Sum(x, (x, a, b)) + y)) == c * (y + Sum(x, (x, a, b))) + assert _simplify(c * (Sum(x, (x, a, b)) + y * Sum(x, (x, a, b)))) == \ + c * (y + 1) * Sum(x, (x, a, b)) + assert _simplify(Sum(Sum(c * x, (x, a, b)), (y, a, b))) == \ + c * Sum(x, (x, a, b), (y, a, b)) + assert _simplify(Sum((3 + y) * Sum(c * x, (x, a, b)), (y, a, b))) == \ + c * Sum((3 + y), (y, a, b)) * Sum(x, (x, a, b)) + assert _simplify(Sum((3 + t) * Sum(c * t, (x, a, b)), (y, a, b))) == \ + c*t*(t + 3)*Sum(1, (x, a, b))*Sum(1, (y, a, b)) + assert _simplify(Sum(Sum(d * t, (x, a, b - 1)) + \ + Sum(d * t, (x, b, c)), (t, a, b))) == \ + d * Sum(1, (x, a, c)) * Sum(t, (t, a, b)) + assert _simplify(Sum(sin(t)**2 + cos(t)**2 + 1, (t, a, b))) == \ + 2 * Sum(1, (t, a, b)) + + +def test_change_index(): + b, v, w = symbols('b, v, w', integer = True) + + assert Sum(x, (x, a, b)).change_index(x, x + 1, y) == \ + Sum(y - 1, (y, a + 1, b + 1)) + assert Sum(x**2, (x, a, b)).change_index( x, x - 1) == \ + Sum((x+1)**2, (x, a - 1, b - 1)) + assert Sum(x**2, (x, a, b)).change_index( x, -x, y) == \ + Sum((-y)**2, (y, -b, -a)) + assert Sum(x, (x, a, b)).change_index( x, -x - 1) == \ + Sum(-x - 1, (x, -b - 1, -a - 1)) + assert Sum(x*y, (x, a, b), (y, c, d)).change_index( x, x - 1, z) == \ + Sum((z + 1)*y, (z, a - 1, b - 1), (y, c, d)) + assert Sum(x, (x, a, b)).change_index( x, x + v) == \ + Sum(-v + x, (x, a + v, b + v)) + assert Sum(x, (x, a, b)).change_index( x, -x - v) == \ + Sum(-v - x, (x, -b - v, -a - v)) + assert Sum(x, (x, a, b)).change_index(x, w*x, v) == \ + Sum(v/w, (v, b*w, a*w)) + raises(ValueError, lambda: Sum(x, (x, a, b)).change_index(x, 2*x)) + + +def test_reorder(): + b, y, c, d, z = symbols('b, y, c, d, z', integer = True) + + assert Sum(x*y, (x, a, b), (y, c, d)).reorder((0, 1)) == \ + Sum(x*y, (y, c, d), (x, a, b)) + assert Sum(x, (x, a, b), (x, c, d)).reorder((0, 1)) == \ + Sum(x, (x, c, d), (x, a, b)) + assert Sum(x*y + z, (x, a, b), (z, m, n), (y, c, d)).reorder(\ + (2, 0), (0, 1)) == Sum(x*y + z, (z, m, n), (y, c, d), (x, a, b)) + assert Sum(x*y*z, (x, a, b), (y, c, d), (z, m, n)).reorder(\ + (0, 1), (1, 2), (0, 2)) == Sum(x*y*z, (x, a, b), (z, m, n), (y, c, d)) + assert Sum(x*y*z, (x, a, b), (y, c, d), (z, m, n)).reorder(\ + (x, y), (y, z), (x, z)) == Sum(x*y*z, (x, a, b), (z, m, n), (y, c, d)) + assert Sum(x*y, (x, a, b), (y, c, d)).reorder((x, 1)) == \ + Sum(x*y, (y, c, d), (x, a, b)) + assert Sum(x*y, (x, a, b), (y, c, d)).reorder((y, x)) == \ + Sum(x*y, (y, c, d), (x, a, b)) + + +def test_reverse_order(): + assert Sum(x, (x, 0, 3)).reverse_order(0) == Sum(-x, (x, 4, -1)) + assert Sum(x*y, (x, 1, 5), (y, 0, 6)).reverse_order(0, 1) == \ + Sum(x*y, (x, 6, 0), (y, 7, -1)) + assert Sum(x, (x, 1, 2)).reverse_order(0) == Sum(-x, (x, 3, 0)) + assert Sum(x, (x, 1, 3)).reverse_order(0) == Sum(-x, (x, 4, 0)) + assert Sum(x, (x, 1, a)).reverse_order(0) == Sum(-x, (x, a + 1, 0)) + assert Sum(x, (x, a, 5)).reverse_order(0) == Sum(-x, (x, 6, a - 1)) + assert Sum(x, (x, a + 1, a + 5)).reverse_order(0) == \ + Sum(-x, (x, a + 6, a)) + assert Sum(x, (x, a + 1, a + 2)).reverse_order(0) == \ + Sum(-x, (x, a + 3, a)) + assert Sum(x, (x, a + 1, a + 1)).reverse_order(0) == \ + Sum(-x, (x, a + 2, a)) + assert Sum(x, (x, a, b)).reverse_order(0) == Sum(-x, (x, b + 1, a - 1)) + assert Sum(x, (x, a, b)).reverse_order(x) == Sum(-x, (x, b + 1, a - 1)) + assert Sum(x*y, (x, a, b), (y, 2, 5)).reverse_order(x, 1) == \ + Sum(x*y, (x, b + 1, a - 1), (y, 6, 1)) + assert Sum(x*y, (x, a, b), (y, 2, 5)).reverse_order(y, x) == \ + Sum(x*y, (x, b + 1, a - 1), (y, 6, 1)) + + +def test_issue_7097(): + assert sum(x**n/n for n in range(1, 401)) == summation(x**n/n, (n, 1, 400)) + + +def test_factor_expand_subs(): + # test factoring + assert Sum(4 * x, (x, 1, y)).factor() == 4 * Sum(x, (x, 1, y)) + assert Sum(x * a, (x, 1, y)).factor() == a * Sum(x, (x, 1, y)) + assert Sum(4 * x * a, (x, 1, y)).factor() == 4 * a * Sum(x, (x, 1, y)) + assert Sum(4 * x * y, (x, 1, y)).factor() == 4 * y * Sum(x, (x, 1, y)) + + # test expand + _x = Symbol('x', zero=False) + assert Sum(x+1,(x,1,y)).expand() == Sum(x,(x,1,y)) + Sum(1,(x,1,y)) + assert Sum(x+a*x**2,(x,1,y)).expand() == Sum(x,(x,1,y)) + Sum(a*x**2,(x,1,y)) + assert Sum(_x**(n + 1)*(n + 1), (n, -1, oo)).expand() \ + == Sum(n*_x*_x**n + _x*_x**n, (n, -1, oo)) + assert Sum(x**(n + 1)*(n + 1), (n, -1, oo)).expand(power_exp=False) \ + == Sum(n*x**(n + 1) + x**(n + 1), (n, -1, oo)) + assert Sum(x**(n + 1)*(n + 1), (n, -1, oo)).expand(force=True) \ + == Sum(x*x**n, (n, -1, oo)) + Sum(n*x*x**n, (n, -1, oo)) + assert Sum(a*n+a*n**2,(n,0,4)).expand() \ + == Sum(a*n,(n,0,4)) + Sum(a*n**2,(n,0,4)) + assert Sum(_x**a*_x**n,(x,0,3)) \ + == Sum(_x**(a+n),(x,0,3)).expand(power_exp=True) + _a, _n = symbols('a n', positive=True) + assert Sum(x**(_a+_n),(x,0,3)).expand(power_exp=True) \ + == Sum(x**_a*x**_n, (x, 0, 3)) + assert Sum(x**(_a-_n),(x,0,3)).expand(power_exp=True) \ + == Sum(x**(_a-_n),(x,0,3)).expand(power_exp=False) + + # test subs + assert Sum(1/(1+a*x**2),(x,0,3)).subs([(a,3)]) == Sum(1/(1+3*x**2),(x,0,3)) + assert Sum(x*y,(x,0,y),(y,0,x)).subs([(x,3)]) == Sum(x*y,(x,0,y),(y,0,3)) + assert Sum(x,(x,1,10)).subs([(x,y-2)]) == Sum(x,(x,1,10)) + assert Sum(1/x,(x,1,10)).subs([(x,(3+n)**3)]) == Sum(1/x,(x,1,10)) + assert Sum(1/x,(x,1,10)).subs([(x,3*x-2)]) == Sum(1/x,(x,1,10)) + + +def test_distribution_over_equality(): + assert Product(Eq(x*2, f(x)), (x, 1, 3)).doit() == Eq(48, f(1)*f(2)*f(3)) + assert Sum(Eq(f(x), x**2), (x, 0, y)) == \ + Eq(Sum(f(x), (x, 0, y)), Sum(x**2, (x, 0, y))) + + +def test_issue_2787(): + n, k = symbols('n k', positive=True, integer=True) + p = symbols('p', positive=True) + binomial_dist = binomial(n, k)*p**k*(1 - p)**(n - k) + s = Sum(binomial_dist*k, (k, 0, n)) + res = s.doit().simplify() + ans = Piecewise( + (n*p, x), + (Sum(k*p**k*binomial(n, k)*(1 - p)**(n - k), (k, 0, n)), + True)).subs(x, (Eq(n, 1) | (n > 1)) & (p/Abs(p - 1) <= 1)) + ans2 = Piecewise( + (n*p, x), + (factorial(n)*Sum(p**k*(1 - p)**(-k + n)/ + (factorial(-k + n)*factorial(k - 1)), (k, 0, n)), + True)).subs(x, (Eq(n, 1) | (n > 1)) & (p/Abs(p - 1) <= 1)) + assert res in [ans, ans2] # XXX system dependent + # Issue #17165: make sure that another simplify does not complicate + # the result by much. Why didn't first simplify replace + # Eq(n, 1) | (n > 1) with True? + assert res.simplify().count_ops() <= res.count_ops() + 2 + + +def test_issue_4668(): + assert summation(1/n, (n, 2, oo)) is oo + + +def test_matrix_sum(): + A = Matrix([[0, 1], [n, 0]]) + + result = Sum(A, (n, 0, 3)).doit() + assert result == Matrix([[0, 4], [6, 0]]) + assert result.__class__ == ImmutableDenseMatrix + + A = SparseMatrix([[0, 1], [n, 0]]) + + result = Sum(A, (n, 0, 3)).doit() + assert result.__class__ == ImmutableSparseMatrix + + +def test_failing_matrix_sum(): + n = Symbol('n') + # TODO Implement matrix geometric series summation. + A = Matrix([[0, 1, 0], [-1, 0, 0], [0, 0, 0]]) + assert Sum(A ** n, (n, 1, 4)).doit() == \ + Matrix([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) + # issue sympy/sympy#16989 + assert summation(A**n, (n, 1, 1)) == A + + +def test_indexed_idx_sum(): + i = symbols('i', cls=Idx) + r = Indexed('r', i) + assert Sum(r, (i, 0, 3)).doit() == sum(r.xreplace({i: j}) for j in range(4)) + assert Product(r, (i, 0, 3)).doit() == prod([r.xreplace({i: j}) for j in range(4)]) + + j = symbols('j', integer=True) + assert Sum(r, (i, j, j+2)).doit() == sum(r.xreplace({i: j+k}) for k in range(3)) + assert Product(r, (i, j, j+2)).doit() == prod([r.xreplace({i: j+k}) for k in range(3)]) + + k = Idx('k', range=(1, 3)) + A = IndexedBase('A') + assert Sum(A[k], k).doit() == sum(A[Idx(j, (1, 3))] for j in range(1, 4)) + assert Product(A[k], k).doit() == prod([A[Idx(j, (1, 3))] for j in range(1, 4)]) + + raises(ValueError, lambda: Sum(A[k], (k, 1, 4))) + raises(ValueError, lambda: Sum(A[k], (k, 0, 3))) + raises(ValueError, lambda: Sum(A[k], (k, 2, oo))) + + raises(ValueError, lambda: Product(A[k], (k, 1, 4))) + raises(ValueError, lambda: Product(A[k], (k, 0, 3))) + raises(ValueError, lambda: Product(A[k], (k, 2, oo))) + + +@slow +def test_is_convergent(): + # divergence tests -- + assert Sum(n/(2*n + 1), (n, 1, oo)).is_convergent() is S.false + assert Sum(factorial(n)/5**n, (n, 1, oo)).is_convergent() is S.false + assert Sum(3**(-2*n - 1)*n**n, (n, 1, oo)).is_convergent() is S.false + assert Sum((-1)**n*n, (n, 3, oo)).is_convergent() is S.false + assert Sum((-1)**n, (n, 1, oo)).is_convergent() is S.false + assert Sum(log(1/n), (n, 2, oo)).is_convergent() is S.false + assert Sum(sin(n), (n, 1, oo)).is_convergent() is S.false + + # Raabe's test -- + assert Sum(Product((3*m),(m,1,n))/Product((3*m+4),(m,1,n)),(n,1,oo)).is_convergent() is S.true + + # root test -- + assert Sum((-12)**n/n, (n, 1, oo)).is_convergent() is S.false + + # integral test -- + + # p-series test -- + assert Sum(1/(n**2 + 1), (n, 1, oo)).is_convergent() is S.true + assert Sum(1/n**Rational(6, 5), (n, 1, oo)).is_convergent() is S.true + assert Sum(2/(n*sqrt(n - 1)), (n, 2, oo)).is_convergent() is S.true + assert Sum(1/(sqrt(n)*sqrt(n)), (n, 2, oo)).is_convergent() is S.false + assert Sum(factorial(n) / factorial(n+2), (n, 1, oo)).is_convergent() is S.true + assert Sum(rf(5,n)/rf(7,n),(n,1,oo)).is_convergent() is S.true + assert Sum((rf(1, n)*rf(2, n))/(rf(3, n)*factorial(n)),(n,1,oo)).is_convergent() is S.false + + # comparison test -- + assert Sum(1/(n + log(n)), (n, 1, oo)).is_convergent() is S.false + assert Sum(1/(n**2*log(n)), (n, 2, oo)).is_convergent() is S.true + assert Sum(1/(n*log(n)), (n, 2, oo)).is_convergent() is S.false + assert Sum(2/(n*log(n)*log(log(n))**2), (n, 5, oo)).is_convergent() is S.true + assert Sum(2/(n*log(n)**2), (n, 2, oo)).is_convergent() is S.true + assert Sum((n - 1)/(n**2*log(n)**3), (n, 2, oo)).is_convergent() is S.true + assert Sum(1/(n*log(n)*log(log(n))), (n, 5, oo)).is_convergent() is S.false + assert Sum((n - 1)/(n*log(n)**3), (n, 3, oo)).is_convergent() is S.false + assert Sum(2/(n**2*log(n)), (n, 2, oo)).is_convergent() is S.true + assert Sum(1/(n*sqrt(log(n))*log(log(n))), (n, 100, oo)).is_convergent() is S.false + assert Sum(log(log(n))/(n*log(n)**2), (n, 100, oo)).is_convergent() is S.true + assert Sum(log(n)/n**2, (n, 5, oo)).is_convergent() is S.true + + # alternating series tests -- + assert Sum((-1)**(n - 1)/(n**2 - 1), (n, 3, oo)).is_convergent() is S.true + + # with -negativeInfinite Limits + assert Sum(1/(n**2 + 1), (n, -oo, 1)).is_convergent() is S.true + assert Sum(1/(n - 1), (n, -oo, -1)).is_convergent() is S.false + assert Sum(1/(n**2 - 1), (n, -oo, -5)).is_convergent() is S.true + assert Sum(1/(n**2 - 1), (n, -oo, 2)).is_convergent() is S.true + assert Sum(1/(n**2 - 1), (n, -oo, oo)).is_convergent() is S.true + + # piecewise functions + f = Piecewise((n**(-2), n <= 1), (n**2, n > 1)) + assert Sum(f, (n, 1, oo)).is_convergent() is S.false + assert Sum(f, (n, -oo, oo)).is_convergent() is S.false + assert Sum(f, (n, 1, 100)).is_convergent() is S.true + #assert Sum(f, (n, -oo, 1)).is_convergent() is S.true + + # integral test + + assert Sum(log(n)/n**3, (n, 1, oo)).is_convergent() is S.true + assert Sum(-log(n)/n**3, (n, 1, oo)).is_convergent() is S.true + # the following function has maxima located at (x, y) = + # (1.2, 0.43), (3.0, -0.25) and (6.8, 0.050) + eq = (x - 2)*(x**2 - 6*x + 4)*exp(-x) + assert Sum(eq, (x, 1, oo)).is_convergent() is S.true + assert Sum(eq, (x, 1, 2)).is_convergent() is S.true + assert Sum(1/(x**3), (x, 1, oo)).is_convergent() is S.true + assert Sum(1/(x**S.Half), (x, 1, oo)).is_convergent() is S.false + + # issue 19545 + assert Sum(1/n - 3/(3*n +2), (n, 1, oo)).is_convergent() is S.true + + # issue 19836 + assert Sum(4/(n + 2) - 5/(n + 1) + 1/n,(n, 7, oo)).is_convergent() is S.true + + +def test_is_absolutely_convergent(): + assert Sum((-1)**n, (n, 1, oo)).is_absolutely_convergent() is S.false + assert Sum((-1)**n/n**2, (n, 1, oo)).is_absolutely_convergent() is S.true + + +@XFAIL +def test_convergent_failing(): + # dirichlet tests + assert Sum(sin(n)/n, (n, 1, oo)).is_convergent() is S.true + assert Sum(sin(2*n)/n, (n, 1, oo)).is_convergent() is S.true + + +def test_issue_6966(): + i, k, m = symbols('i k m', integer=True) + z_i, q_i = symbols('z_i q_i') + a_k = Sum(-q_i*z_i/k,(i,1,m)) + b_k = a_k.diff(z_i) + assert isinstance(b_k, Sum) + assert b_k == Sum(-q_i/k,(i,1,m)) + + +def test_issue_10156(): + cx = Sum(2*y**2*x, (x, 1,3)) + e = 2*y*Sum(2*cx*x**2, (x, 1, 9)) + assert e.factor() == \ + 8*y**3*Sum(x, (x, 1, 3))*Sum(x**2, (x, 1, 9)) + + +def test_issue_10973(): + assert Sum((-n + (n**3 + 1)**(S(1)/3))/log(n), (n, 1, oo)).is_convergent() is S.true + + +def test_issue_14103(): + assert Sum(sin(n)**2 + cos(n)**2 - 1, (n, 1, oo)).is_convergent() is S.true + assert Sum(sin(pi*n), (n, 1, oo)).is_convergent() is S.true + + +def test_issue_14129(): + x = Symbol('x', zero=False) + assert Sum( k*x**k, (k, 0, n-1)).doit() == \ + Piecewise((n**2/2 - n/2, Eq(x, 1)), ((n*x*x**n - + n*x**n - x*x**n + x)/(x - 1)**2, True)) + assert Sum( x**k, (k, 0, n-1)).doit() == \ + Piecewise((n, Eq(x, 1)), ((-x**n + 1)/(-x + 1), True)) + assert Sum( k*(x/y+x)**k, (k, 0, n-1)).doit() == \ + Piecewise((n*(n - 1)/2, Eq(x, y/(y + 1))), + (x*(y + 1)*(n*x*y*(x + x/y)**(n - 1) + + n*x*(x + x/y)**(n - 1) - n*y*(x + x/y)**(n - 1) - + x*y*(x + x/y)**(n - 1) - x*(x + x/y)**(n - 1) + y)/ + (x*y + x - y)**2, True)) + + +def test_issue_14112(): + assert Sum((-1)**n/sqrt(n), (n, 1, oo)).is_absolutely_convergent() is S.false + assert Sum((-1)**(2*n)/n, (n, 1, oo)).is_convergent() is S.false + assert Sum((-2)**n + (-3)**n, (n, 1, oo)).is_convergent() is S.false + + +def test_issue_14219(): + A = diag(0, 2, -3) + res = diag(1, 15, -20) + assert Sum(A**n, (n, 0, 3)).doit() == res + + +def test_sin_times_absolutely_convergent(): + assert Sum(sin(n) / n**3, (n, 1, oo)).is_convergent() is S.true + assert Sum(sin(n) * log(n) / n**3, (n, 1, oo)).is_convergent() is S.true + + +def test_issue_14111(): + assert Sum(1/log(log(n)), (n, 22, oo)).is_convergent() is S.false + + +def test_issue_14484(): + assert Sum(sin(n)/log(log(n)), (n, 22, oo)).is_convergent() is S.false + + +def test_issue_14640(): + i, n = symbols("i n", integer=True) + a, b, c = symbols("a b c", zero=False) + + assert Sum(a**-i/(a - b), (i, 0, n)).doit() == Sum( + 1/(a*a**i - a**i*b), (i, 0, n)).doit() == Piecewise( + (n + 1, Eq(1/a, 1)), + ((-a**(-n - 1) + 1)/(1 - 1/a), True))/(a - b) + + assert Sum((b*a**i - c*a**i)**-2, (i, 0, n)).doit() == Piecewise( + (n + 1, Eq(a**(-2), 1)), + ((-a**(-2*n - 2) + 1)/(1 - 1/a**2), True))/(b - c)**2 + + s = Sum(i*(a**(n - i) - b**(n - i))/(a - b), (i, 0, n)).doit() + assert not s.has(Sum) + assert s.subs({a: 2, b: 3, n: 5}) == 122 + + +def test_issue_15943(): + s = Sum(binomial(n, k)*factorial(n - k), (k, 0, n)).doit().rewrite(gamma) + assert s == -E*(n + 1)*gamma(n + 1)*lowergamma(n + 1, 1)/gamma(n + 2 + ) + E*gamma(n + 1) + assert s.simplify() == E*(factorial(n) - lowergamma(n + 1, 1)) + + +def test_Sum_dummy_eq(): + assert not Sum(x, (x, a, b)).dummy_eq(1) + assert not Sum(x, (x, a, b)).dummy_eq(Sum(x, (x, a, b), (a, 1, 2))) + assert not Sum(x, (x, a, b)).dummy_eq(Sum(x, (x, a, c))) + assert Sum(x, (x, a, b)).dummy_eq(Sum(x, (x, a, b))) + d = Dummy() + assert Sum(x, (x, a, d)).dummy_eq(Sum(x, (x, a, c)), c) + assert not Sum(x, (x, a, d)).dummy_eq(Sum(x, (x, a, c))) + assert Sum(x, (x, a, c)).dummy_eq(Sum(y, (y, a, c))) + assert Sum(x, (x, a, d)).dummy_eq(Sum(y, (y, a, c)), c) + assert not Sum(x, (x, a, d)).dummy_eq(Sum(y, (y, a, c))) + + +def test_issue_15852(): + assert summation(x**y*y, (y, -oo, oo)).doit() == Sum(x**y*y, (y, -oo, oo)) + + +def test_exceptions(): + S = Sum(x, (x, a, b)) + raises(ValueError, lambda: S.change_index(x, x**2, y)) + S = Sum(x, (x, a, b), (x, 1, 4)) + raises(ValueError, lambda: S.index(x)) + S = Sum(x, (x, a, b), (y, 1, 4)) + raises(ValueError, lambda: S.reorder([x])) + S = Sum(x, (x, y, b), (y, 1, 4)) + raises(ReorderError, lambda: S.reorder_limit(0, 1)) + S = Sum(x*y, (x, a, b), (y, 1, 4)) + raises(NotImplementedError, lambda: S.is_convergent()) + + +def test_sumproducts_assumptions(): + M = Symbol('M', integer=True, positive=True) + + m = Symbol('m', integer=True) + for func in [Sum, Product]: + assert func(m, (m, -M, M)).is_positive is None + assert func(m, (m, -M, M)).is_nonpositive is None + assert func(m, (m, -M, M)).is_negative is None + assert func(m, (m, -M, M)).is_nonnegative is None + assert func(m, (m, -M, M)).is_finite is True + + m = Symbol('m', integer=True, nonnegative=True) + for func in [Sum, Product]: + assert func(m, (m, 0, M)).is_positive is None + assert func(m, (m, 0, M)).is_nonpositive is None + assert func(m, (m, 0, M)).is_negative is False + assert func(m, (m, 0, M)).is_nonnegative is True + assert func(m, (m, 0, M)).is_finite is True + + m = Symbol('m', integer=True, positive=True) + for func in [Sum, Product]: + assert func(m, (m, 1, M)).is_positive is True + assert func(m, (m, 1, M)).is_nonpositive is False + assert func(m, (m, 1, M)).is_negative is False + assert func(m, (m, 1, M)).is_nonnegative is True + assert func(m, (m, 1, M)).is_finite is True + + m = Symbol('m', integer=True, negative=True) + assert Sum(m, (m, -M, -1)).is_positive is False + assert Sum(m, (m, -M, -1)).is_nonpositive is True + assert Sum(m, (m, -M, -1)).is_negative is True + assert Sum(m, (m, -M, -1)).is_nonnegative is False + assert Sum(m, (m, -M, -1)).is_finite is True + assert Product(m, (m, -M, -1)).is_positive is None + assert Product(m, (m, -M, -1)).is_nonpositive is None + assert Product(m, (m, -M, -1)).is_negative is None + assert Product(m, (m, -M, -1)).is_nonnegative is None + assert Product(m, (m, -M, -1)).is_finite is True + + m = Symbol('m', integer=True, nonpositive=True) + assert Sum(m, (m, -M, 0)).is_positive is False + assert Sum(m, (m, -M, 0)).is_nonpositive is True + assert Sum(m, (m, -M, 0)).is_negative is None + assert Sum(m, (m, -M, 0)).is_nonnegative is None + assert Sum(m, (m, -M, 0)).is_finite is True + assert Product(m, (m, -M, 0)).is_positive is None + assert Product(m, (m, -M, 0)).is_nonpositive is None + assert Product(m, (m, -M, 0)).is_negative is None + assert Product(m, (m, -M, 0)).is_nonnegative is None + assert Product(m, (m, -M, 0)).is_finite is True + + m = Symbol('m', integer=True) + assert Sum(2, (m, 0, oo)).is_positive is None + assert Sum(2, (m, 0, oo)).is_nonpositive is None + assert Sum(2, (m, 0, oo)).is_negative is None + assert Sum(2, (m, 0, oo)).is_nonnegative is None + assert Sum(2, (m, 0, oo)).is_finite is None + + assert Product(2, (m, 0, oo)).is_positive is None + assert Product(2, (m, 0, oo)).is_nonpositive is None + assert Product(2, (m, 0, oo)).is_negative is False + assert Product(2, (m, 0, oo)).is_nonnegative is None + assert Product(2, (m, 0, oo)).is_finite is None + + assert Product(0, (x, M, M-1)).is_positive is True + assert Product(0, (x, M, M-1)).is_finite is True + + +def test_expand_with_assumptions(): + M = Symbol('M', integer=True, positive=True) + x = Symbol('x', positive=True) + m = Symbol('m', nonnegative=True) + assert log(Product(x**m, (m, 0, M))).expand() == Sum(m*log(x), (m, 0, M)) + assert log(Product(exp(x**m), (m, 0, M))).expand() == Sum(x**m, (m, 0, M)) + assert log(Product(x**m, (m, 0, M))).rewrite(Sum).expand() == Sum(m*log(x), (m, 0, M)) + assert log(Product(exp(x**m), (m, 0, M))).rewrite(Sum).expand() == Sum(x**m, (m, 0, M)) + + n = Symbol('n', nonnegative=True) + i, j = symbols('i,j', positive=True, integer=True) + x, y = symbols('x,y', positive=True) + assert log(Product(x**i*y**j, (i, 1, n), (j, 1, m))).expand() \ + == Sum(i*log(x) + j*log(y), (i, 1, n), (j, 1, m)) + + m = Symbol('m', nonnegative=True, integer=True) + s = Sum(x**m, (m, 0, M)) + s_as_product = s.rewrite(Product) + assert s_as_product.has(Product) + assert s_as_product == log(Product(exp(x**m), (m, 0, M))) + assert s_as_product.expand() == s + s5 = s.subs(M, 5) + s5_as_product = s5.rewrite(Product) + assert s5_as_product.has(Product) + assert s5_as_product.doit().expand() == s5.doit() + + +def test_has_finite_limits(): + x = Symbol('x') + assert Sum(1, (x, 1, 9)).has_finite_limits is True + assert Sum(1, (x, 1, oo)).has_finite_limits is False + M = Symbol('M') + assert Sum(1, (x, 1, M)).has_finite_limits is None + M = Symbol('M', positive=True) + assert Sum(1, (x, 1, M)).has_finite_limits is True + x = Symbol('x', positive=True) + M = Symbol('M') + assert Sum(1, (x, 1, M)).has_finite_limits is True + + assert Sum(1, (x, 1, M), (y, -oo, oo)).has_finite_limits is False + +def test_has_reversed_limits(): + assert Sum(1, (x, 1, 1)).has_reversed_limits is False + assert Sum(1, (x, 1, 9)).has_reversed_limits is False + assert Sum(1, (x, 1, -9)).has_reversed_limits is True + assert Sum(1, (x, 1, 0)).has_reversed_limits is True + assert Sum(1, (x, 1, oo)).has_reversed_limits is False + M = Symbol('M') + assert Sum(1, (x, 1, M)).has_reversed_limits is None + M = Symbol('M', positive=True, integer=True) + assert Sum(1, (x, 1, M)).has_reversed_limits is False + assert Sum(1, (x, 1, M), (y, -oo, oo)).has_reversed_limits is False + M = Symbol('M', negative=True) + assert Sum(1, (x, 1, M)).has_reversed_limits is True + + assert Sum(1, (x, 1, M), (y, -oo, oo)).has_reversed_limits is True + assert Sum(1, (x, oo, oo)).has_reversed_limits is None + + +def test_has_empty_sequence(): + assert Sum(1, (x, 1, 1)).has_empty_sequence is False + assert Sum(1, (x, 1, 9)).has_empty_sequence is False + assert Sum(1, (x, 1, -9)).has_empty_sequence is False + assert Sum(1, (x, 1, 0)).has_empty_sequence is True + assert Sum(1, (x, y, y - 1)).has_empty_sequence is True + assert Sum(1, (x, 3, 2), (y, -oo, oo)).has_empty_sequence is True + assert Sum(1, (y, -oo, oo), (x, 3, 2)).has_empty_sequence is True + assert Sum(1, (x, oo, oo)).has_empty_sequence is False + + +def test_empty_sequence(): + assert Product(x*y, (x, -oo, oo), (y, 1, 0)).doit() == 1 + assert Product(x*y, (y, 1, 0), (x, -oo, oo)).doit() == 1 + assert Sum(x, (x, -oo, oo), (y, 1, 0)).doit() == 0 + assert Sum(x, (y, 1, 0), (x, -oo, oo)).doit() == 0 + + +def test_issue_8016(): + k = Symbol('k', integer=True) + n, m = symbols('n, m', integer=True, positive=True) + s = Sum(binomial(m, k)*binomial(m, n - k)*(-1)**k, (k, 0, n)) + assert s.doit().simplify() == \ + cos(pi*n/2)*gamma(m + 1)/gamma(n/2 + 1)/gamma(m - n/2 + 1) + + +def test_issue_14313(): + assert Sum(S.Half**floor(n/2), (n, 1, oo)).is_convergent() + + +def test_issue_14563(): + # The assertion was failing due to no assumptions methods in Sums and Product + assert 1 % Sum(1, (x, 0, 1)) == 1 + + +def test_issue_16735(): + assert Sum(5**n/gamma(n+1), (n, 1, oo)).is_convergent() is S.true + + +def test_issue_14871(): + assert Sum((Rational(1, 10))**n*rf(0, n)/factorial(n), (n, 0, oo)).rewrite(factorial).doit() == 1 + + +def test_issue_17165(): + n = symbols("n", integer=True) + x = symbols('x') + s = (x*Sum(x**n, (n, -1, oo))) + ssimp = s.doit().simplify() + + assert ssimp == Piecewise((-1/(x - 1), (x > -1) & (x < 1)), + (x*Sum(x**n, (n, -1, oo)), True)), ssimp + assert ssimp.simplify() == ssimp + + +def test_issue_19379(): + assert Sum(factorial(n)/factorial(n + 2), (n, 1, oo)).is_convergent() is S.true + + +def test_issue_20777(): + assert Sum(exp(x*sin(n/m)), (n, 1, m)).doit() == Sum(exp(x*sin(n/m)), (n, 1, m)) + + +def test__dummy_with_inherited_properties_concrete(): + x = Symbol('x') + + from sympy.core.containers import Tuple + d = _dummy_with_inherited_properties_concrete(Tuple(x, 0, 5)) + assert d.is_real + assert d.is_integer + assert d.is_nonnegative + assert d.is_extended_nonnegative + + d = _dummy_with_inherited_properties_concrete(Tuple(x, 1, 9)) + assert d.is_real + assert d.is_integer + assert d.is_positive + assert d.is_odd is None + + d = _dummy_with_inherited_properties_concrete(Tuple(x, -5, 5)) + assert d.is_real + assert d.is_integer + assert d.is_positive is None + assert d.is_extended_nonnegative is None + assert d.is_odd is None + + d = _dummy_with_inherited_properties_concrete(Tuple(x, -1.5, 1.5)) + assert d.is_real + assert d.is_integer is None + assert d.is_positive is None + assert d.is_extended_nonnegative is None + + N = Symbol('N', integer=True, positive=True) + d = _dummy_with_inherited_properties_concrete(Tuple(x, 2, N)) + assert d.is_real + assert d.is_positive + assert d.is_integer + + # Return None if no assumptions are added + N = Symbol('N', integer=True, positive=True) + d = _dummy_with_inherited_properties_concrete(Tuple(N, 2, 4)) + assert d is None + + x = Symbol('x', negative=True) + raises(InconsistentAssumptions, + lambda: _dummy_with_inherited_properties_concrete(Tuple(x, 1, 5))) + + +def test_matrixsymbol_summation_numerical_limits(): + A = MatrixSymbol('A', 3, 3) + n = Symbol('n', integer=True) + + assert Sum(A**n, (n, 0, 2)).doit() == Identity(3) + A + A**2 + assert Sum(A, (n, 0, 2)).doit() == 3*A + assert Sum(n*A, (n, 0, 2)).doit() == 3*A + + B = Matrix([[0, n, 0], [-1, 0, 0], [0, 0, 2]]) + ans = Matrix([[0, 6, 0], [-4, 0, 0], [0, 0, 8]]) + 4*A + assert Sum(A+B, (n, 0, 3)).doit() == ans + ans = A*Matrix([[0, 6, 0], [-4, 0, 0], [0, 0, 8]]) + assert Sum(A*B, (n, 0, 3)).doit() == ans + + ans = (A**2*Matrix([[-2, 0, 0], [0,-2, 0], [0, 0, 4]]) + + A**3*Matrix([[0, -9, 0], [3, 0, 0], [0, 0, 8]]) + + A*Matrix([[0, 1, 0], [-1, 0, 0], [0, 0, 2]])) + assert Sum(A**n*B**n, (n, 1, 3)).doit() == ans + + +def test_issue_21651(): + i = Symbol('i') + a = Sum(floor(2*2**(-i)), (i, S.One, 2)) + assert a.doit() == S.One + + +@XFAIL +def test_matrixsymbol_summation_symbolic_limits(): + N = Symbol('N', integer=True, positive=True) + + A = MatrixSymbol('A', 3, 3) + n = Symbol('n', integer=True) + assert Sum(A, (n, 0, N)).doit() == (N+1)*A + assert Sum(n*A, (n, 0, N)).doit() == (N**2/2+N/2)*A + + +def test_summation_by_residues(): + x = Symbol('x') + + # Examples from Nakhle H. Asmar, Loukas Grafakos, + # Complex Analysis with Applications + assert eval_sum_residue(1 / (x**2 + 1), (x, -oo, oo)) == pi/tanh(pi) + assert eval_sum_residue(1 / x**6, (x, S(1), oo)) == pi**6/945 + assert eval_sum_residue(1 / (x**2 + 9), (x, -oo, oo)) == pi/(3*tanh(3*pi)) + assert eval_sum_residue(1 / (x**2 + 1)**2, (x, -oo, oo)).cancel() == \ + (-pi**2*tanh(pi)**2 + pi*tanh(pi) + pi**2)/(2*tanh(pi)**2) + assert eval_sum_residue(x**2 / (x**2 + 1)**2, (x, -oo, oo)).cancel() == \ + (-pi**2 + pi*tanh(pi) + pi**2*tanh(pi)**2)/(2*tanh(pi)**2) + assert eval_sum_residue(1 / (4*x**2 - 1), (x, -oo, oo)) == 0 + assert eval_sum_residue(x**2 / (x**2 - S(1)/4)**2, (x, -oo, oo)) == pi**2/2 + assert eval_sum_residue(1 / (4*x**2 - 1)**2, (x, -oo, oo)) == pi**2/8 + assert eval_sum_residue(1 / ((x - S(1)/2)**2 + 1), (x, -oo, oo)) == pi*tanh(pi) + assert eval_sum_residue(1 / x**2, (x, S(1), oo)) == pi**2/6 + assert eval_sum_residue(1 / x**4, (x, S(1), oo)) == pi**4/90 + assert eval_sum_residue(1 / x**2 / (x**2 + 4), (x, S(1), oo)) == \ + -pi*(-pi/12 - 1/(16*pi) + 1/(8*tanh(2*pi)))/2 + + # Some examples made from 1 / (x**2 + 1) + assert eval_sum_residue(1 / (x**2 + 1), (x, S(0), oo)) == \ + S(1)/2 + pi/(2*tanh(pi)) + assert eval_sum_residue(1 / (x**2 + 1), (x, S(1), oo)) == \ + -S(1)/2 + pi/(2*tanh(pi)) + assert eval_sum_residue(1 / (x**2 + 1), (x, S(-1), oo)) == \ + 1 + pi/(2*tanh(pi)) + assert eval_sum_residue((-1)**x / (x**2 + 1), (x, -oo, oo)) == \ + pi/sinh(pi) + assert eval_sum_residue((-1)**x / (x**2 + 1), (x, S(0), oo)) == \ + pi/(2*sinh(pi)) + S(1)/2 + assert eval_sum_residue((-1)**x / (x**2 + 1), (x, S(1), oo)) == \ + -S(1)/2 + pi/(2*sinh(pi)) + assert eval_sum_residue((-1)**x / (x**2 + 1), (x, S(-1), oo)) == \ + pi/(2*sinh(pi)) + + # Some examples made from shifting of 1 / (x**2 + 1) + assert eval_sum_residue(1 / (x**2 + 2*x + 2), (x, S(-1), oo)) == S(1)/2 + pi/(2*tanh(pi)) + assert eval_sum_residue(1 / (x**2 + 4*x + 5), (x, S(-2), oo)) == S(1)/2 + pi/(2*tanh(pi)) + assert eval_sum_residue(1 / (x**2 - 2*x + 2), (x, S(1), oo)) == S(1)/2 + pi/(2*tanh(pi)) + assert eval_sum_residue(1 / (x**2 - 4*x + 5), (x, S(2), oo)) == S(1)/2 + pi/(2*tanh(pi)) + assert eval_sum_residue((-1)**x * -1 / (x**2 + 2*x + 2), (x, S(-1), oo)) == S(1)/2 + pi/(2*sinh(pi)) + assert eval_sum_residue((-1)**x * -1 / (x**2 -2*x + 2), (x, S(1), oo)) == S(1)/2 + pi/(2*sinh(pi)) + + # Some examples made from 1 / x**2 + assert eval_sum_residue(1 / x**2, (x, S(2), oo)) == -1 + pi**2/6 + assert eval_sum_residue(1 / x**2, (x, S(3), oo)) == -S(5)/4 + pi**2/6 + assert eval_sum_residue((-1)**x / x**2, (x, S(1), oo)) == -pi**2/12 + assert eval_sum_residue((-1)**x / x**2, (x, S(2), oo)) == 1 - pi**2/12 + + +@slow +def test_summation_by_residues_failing(): + x = Symbol('x') + + # Failing because of the bug in residue computation + assert eval_sum_residue(x**2 / (x**4 + 1), (x, S(1), oo)) + assert eval_sum_residue(1 / ((x - 1)*(x - 2) + 1), (x, -oo, oo)) != 0 + + +def test_process_limits(): + from sympy.concrete.expr_with_limits import _process_limits + + # these should be (x, Range(3)) not Range(3) + raises(ValueError, lambda: _process_limits( + Range(3), discrete=True)) + raises(ValueError, lambda: _process_limits( + Range(3), discrete=False)) + # these should be (x, union) not union + # (but then we would get a TypeError because we don't + # handle non-contiguous sets: see below use of `union`) + union = Or(x < 1, x > 3).as_set() + raises(ValueError, lambda: _process_limits( + union, discrete=True)) + raises(ValueError, lambda: _process_limits( + union, discrete=False)) + + # error not triggered if not needed + assert _process_limits((x, 1, 2)) == ([(x, 1, 2)], 1) + + # this equivalence is used to detect Reals in _process_limits + assert isinstance(S.Reals, Interval) + + C = Integral # continuous limits + assert C(x, x >= 5) == C(x, (x, 5, oo)) + assert C(x, x < 3) == C(x, (x, -oo, 3)) + ans = C(x, (x, 0, 3)) + assert C(x, And(x >= 0, x < 3)) == ans + assert C(x, (x, Interval.Ropen(0, 3))) == ans + raises(TypeError, lambda: C(x, (x, Range(3)))) + + # discrete limits + for D in (Sum, Product): + r, ans = Range(3, 10, 2), D(2*x + 3, (x, 0, 3)) + assert D(x, (x, r)) == ans + assert D(x, (x, r.reversed)) == ans + r, ans = Range(3, oo, 2), D(2*x + 3, (x, 0, oo)) + assert D(x, (x, r)) == ans + assert D(x, (x, r.reversed)) == ans + r, ans = Range(-oo, 5, 2), D(3 - 2*x, (x, 0, oo)) + assert D(x, (x, r)) == ans + assert D(x, (x, r.reversed)) == ans + raises(TypeError, lambda: D(x, x > 0)) + raises(ValueError, lambda: D(x, Interval(1, 3))) + raises(NotImplementedError, lambda: D(x, (x, union))) + + +def test_pr_22677(): + b = Symbol('b', integer=True, positive=True) + assert Sum(1/x**2,(x, 0, b)).doit() == Sum(x**(-2), (x, 0, b)) + assert Sum(1/(x - b)**2,(x, 0, b-1)).doit() == Sum( + (-b + x)**(-2), (x, 0, b - 1)) + + +def test_issue_23952(): + p, q = symbols("p q", real=True, nonnegative=True) + k1, k2 = symbols("k1 k2", integer=True, nonnegative=True) + n = Symbol("n", integer=True, positive=True) + expr = Sum(abs(k1 - k2)*p**k1 *(1 - q)**(n - k2), + (k1, 0, n), (k2, 0, n)) + assert expr.subs(p,0).subs(q,1).subs(n, 3).doit() == 3 diff --git a/venv/lib/python3.11/site-packages/sympy/conftest.py b/venv/lib/python3.11/site-packages/sympy/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..788ec7ed8a38de422b76c4ac19057513670d2d38 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/conftest.py @@ -0,0 +1,96 @@ +import sys + +sys._running_pytest = True # type: ignore +from sympy.external.importtools import version_tuple + +import pytest +from sympy.core.cache import clear_cache, USE_CACHE +from sympy.external.gmpy import GROUND_TYPES +from sympy.utilities.misc import ARCH +import re + +try: + import hypothesis + + hypothesis.settings.register_profile("sympy_hypothesis_profile", deadline=None) + hypothesis.settings.load_profile("sympy_hypothesis_profile") +except ImportError: + raise ImportError( + "hypothesis is a required dependency to run the SymPy test suite. " + "Install it with 'pip install hypothesis' or 'conda install -c conda-forge hypothesis'" + ) + + +sp = re.compile(r"([0-9]+)/([1-9][0-9]*)") + + +def process_split(config, items): + split = config.getoption("--split") + if not split: + return + m = sp.match(split) + if not m: + raise ValueError( + "split must be a string of the form a/b " "where a and b are ints." + ) + i, t = map(int, m.groups()) + start, end = (i - 1) * len(items) // t, i * len(items) // t + + if i < t: + # remove elements from end of list first + del items[end:] + del items[:start] + + +def pytest_report_header(config): + s = "architecture: %s\n" % ARCH + s += "cache: %s\n" % USE_CACHE + version = "" + if GROUND_TYPES == "gmpy": + import gmpy2 + + version = gmpy2.version() + elif GROUND_TYPES == "flint": + try: + from flint import __version__ + except ImportError: + version = "unknown" + else: + version = f'(python-flint=={__version__})' + s += "ground types: %s %s\n" % (GROUND_TYPES, version) + return s + + +def pytest_terminal_summary(terminalreporter): + if terminalreporter.stats.get("error", None) or terminalreporter.stats.get( + "failed", None + ): + terminalreporter.write_sep(" ", "DO *NOT* COMMIT!", red=True, bold=True) + + +def pytest_addoption(parser): + parser.addoption("--split", action="store", default="", help="split tests") + + +def pytest_collection_modifyitems(config, items): + """pytest hook.""" + # handle splits + process_split(config, items) + + +@pytest.fixture(autouse=True, scope="module") +def file_clear_cache(): + clear_cache() + + +@pytest.fixture(autouse=True, scope="module") +def check_disabled(request): + if getattr(request.module, "disabled", False): + pytest.skip("test requirements not met.") + elif getattr(request.module, "ipython", False): + # need to check version and options for ipython tests + if ( + version_tuple(pytest.__version__) < version_tuple("2.6.3") + and pytest.config.getvalue("-s") != "no" + ): + pytest.skip("run py.test with -s or upgrade to newer version.") diff --git a/venv/lib/python3.11/site-packages/sympy/core/__init__.py b/venv/lib/python3.11/site-packages/sympy/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4c03c9fc11479bb6d93a3bff3dfd0992ef994a19 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/__init__.py @@ -0,0 +1,103 @@ +"""Core module. Provides the basic operations needed in sympy. +""" + +from .sympify import sympify, SympifyError +from .cache import cacheit +from .assumptions import assumptions, check_assumptions, failing_assumptions, common_assumptions +from .basic import Basic, Atom +from .singleton import S +from .expr import Expr, AtomicExpr, UnevaluatedExpr +from .symbol import Symbol, Wild, Dummy, symbols, var +from .numbers import Number, Float, Rational, Integer, NumberSymbol, \ + RealNumber, igcd, ilcm, seterr, E, I, nan, oo, pi, zoo, \ + AlgebraicNumber, comp, mod_inverse +from .power import Pow +from .intfunc import integer_nthroot, integer_log, num_digits, trailing +from .mul import Mul, prod +from .add import Add +from .mod import Mod +from .relational import ( Rel, Eq, Ne, Lt, Le, Gt, Ge, + Equality, GreaterThan, LessThan, Unequality, StrictGreaterThan, + StrictLessThan ) +from .multidimensional import vectorize +from .function import Lambda, WildFunction, Derivative, diff, FunctionClass, \ + Function, Subs, expand, PoleError, count_ops, \ + expand_mul, expand_log, expand_func, \ + expand_trig, expand_complex, expand_multinomial, nfloat, \ + expand_power_base, expand_power_exp, arity +from .evalf import PrecisionExhausted, N +from .containers import Tuple, Dict +from .exprtools import gcd_terms, factor_terms, factor_nc +from .parameters import evaluate +from .kind import UndefinedKind, NumberKind, BooleanKind +from .traversal import preorder_traversal, bottom_up, use, postorder_traversal +from .sorting import default_sort_key, ordered + +# expose singletons +Catalan = S.Catalan +EulerGamma = S.EulerGamma +GoldenRatio = S.GoldenRatio +TribonacciConstant = S.TribonacciConstant + +__all__ = [ + 'sympify', 'SympifyError', + + 'cacheit', + + 'assumptions', 'check_assumptions', 'failing_assumptions', + 'common_assumptions', + + 'Basic', 'Atom', + + 'S', + + 'Expr', 'AtomicExpr', 'UnevaluatedExpr', + + 'Symbol', 'Wild', 'Dummy', 'symbols', 'var', + + 'Number', 'Float', 'Rational', 'Integer', 'NumberSymbol', 'RealNumber', + 'igcd', 'ilcm', 'seterr', 'E', 'I', 'nan', 'oo', 'pi', 'zoo', + 'AlgebraicNumber', 'comp', 'mod_inverse', + + 'Pow', + + 'integer_nthroot', 'integer_log', 'num_digits', 'trailing', + + 'Mul', 'prod', + + 'Add', + + 'Mod', + + 'Rel', 'Eq', 'Ne', 'Lt', 'Le', 'Gt', 'Ge', 'Equality', 'GreaterThan', + 'LessThan', 'Unequality', 'StrictGreaterThan', 'StrictLessThan', + + 'vectorize', + + 'Lambda', 'WildFunction', 'Derivative', 'diff', 'FunctionClass', + 'Function', 'Subs', 'expand', 'PoleError', 'count_ops', 'expand_mul', + 'expand_log', 'expand_func', 'expand_trig', 'expand_complex', + 'expand_multinomial', 'nfloat', 'expand_power_base', 'expand_power_exp', + 'arity', + + 'PrecisionExhausted', 'N', + + 'evalf', # The module? + + 'Tuple', 'Dict', + + 'gcd_terms', 'factor_terms', 'factor_nc', + + 'evaluate', + + 'Catalan', + 'EulerGamma', + 'GoldenRatio', + 'TribonacciConstant', + + 'UndefinedKind', 'NumberKind', 'BooleanKind', + + 'preorder_traversal', 'bottom_up', 'use', 'postorder_traversal', + + 'default_sort_key', 'ordered', +] diff --git a/venv/lib/python3.11/site-packages/sympy/core/_print_helpers.py b/venv/lib/python3.11/site-packages/sympy/core/_print_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..d704ed220d444e2d8510b280dca85c8ae6149d4c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/_print_helpers.py @@ -0,0 +1,65 @@ +""" +Base class to provide str and repr hooks that `init_printing` can overwrite. + +This is exposed publicly in the `printing.defaults` module, +but cannot be defined there without causing circular imports. +""" + +class Printable: + """ + The default implementation of printing for SymPy classes. + + This implements a hack that allows us to print elements of built-in + Python containers in a readable way. Natively Python uses ``repr()`` + even if ``str()`` was explicitly requested. Mix in this trait into + a class to get proper default printing. + + This also adds support for LaTeX printing in jupyter notebooks. + """ + + # Since this class is used as a mixin we set empty slots. That means that + # instances of any subclasses that use slots will not need to have a + # __dict__. + __slots__ = () + + # Note, we always use the default ordering (lex) in __str__ and __repr__, + # regardless of the global setting. See issue 5487. + def __str__(self): + from sympy.printing.str import sstr + return sstr(self, order=None) + + __repr__ = __str__ + + def _repr_disabled(self): + """ + No-op repr function used to disable jupyter display hooks. + + When :func:`sympy.init_printing` is used to disable certain display + formats, this function is copied into the appropriate ``_repr_*_`` + attributes. + + While we could just set the attributes to `None``, doing it this way + allows derived classes to call `super()`. + """ + return None + + # We don't implement _repr_png_ here because it would add a large amount of + # data to any notebook containing SymPy expressions, without adding + # anything useful to the notebook. It can still enabled manually, e.g., + # for the qtconsole, with init_printing(). + _repr_png_ = _repr_disabled + + _repr_svg_ = _repr_disabled + + def _repr_latex_(self): + """ + IPython/Jupyter LaTeX printing + + To change the behavior of this (e.g., pass in some settings to LaTeX), + use init_printing(). init_printing() will also enable LaTeX printing + for built in numeric types like ints and container types that contain + SymPy objects, like lists and dictionaries of expressions. + """ + from sympy.printing.latex import latex + s = latex(self, mode='plain') + return "$\\displaystyle %s$" % s diff --git a/venv/lib/python3.11/site-packages/sympy/core/add.py b/venv/lib/python3.11/site-packages/sympy/core/add.py new file mode 100644 index 0000000000000000000000000000000000000000..2d280f3286c34cd0dea14bf61194ed03ee6bf6ae --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/add.py @@ -0,0 +1,1280 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, ClassVar +from collections import defaultdict +from functools import reduce +from operator import attrgetter +from .basic import _args_sortkey +from .parameters import global_parameters +from .logic import _fuzzy_group, fuzzy_or, fuzzy_not +from .singleton import S +from .operations import AssocOp, AssocOpDispatcher +from .cache import cacheit +from .intfunc import ilcm, igcd +from .expr import Expr +from .kind import UndefinedKind +from sympy.utilities.iterables import is_sequence, sift + + +if TYPE_CHECKING: + from sympy.core.numbers import Number + from sympy.series.order import Order + + +def _could_extract_minus_sign(expr): + # assume expr is Add-like + # We choose the one with less arguments with minus signs + negative_args = sum(1 for i in expr.args + if i.could_extract_minus_sign()) + positive_args = len(expr.args) - negative_args + if positive_args > negative_args: + return False + elif positive_args < negative_args: + return True + # choose based on .sort_key() to prefer + # x - 1 instead of 1 - x and + # 3 - sqrt(2) instead of -3 + sqrt(2) + return bool(expr.sort_key() < (-expr).sort_key()) + + +def _addsort(args): + # in-place sorting of args + args.sort(key=_args_sortkey) + + +def _unevaluated_Add(*args): + """Return a well-formed unevaluated Add: Numbers are collected and + put in slot 0 and args are sorted. Use this when args have changed + but you still want to return an unevaluated Add. + + Examples + ======== + + >>> from sympy.core.add import _unevaluated_Add as uAdd + >>> from sympy import S, Add + >>> from sympy.abc import x, y + >>> a = uAdd(*[S(1.0), x, S(2)]) + >>> a.args[0] + 3.00000000000000 + >>> a.args[1] + x + + Beyond the Number being in slot 0, there is no other assurance of + order for the arguments since they are hash sorted. So, for testing + purposes, output produced by this in some other function can only + be tested against the output of this function or as one of several + options: + + >>> opts = (Add(x, y, evaluate=False), Add(y, x, evaluate=False)) + >>> a = uAdd(x, y) + >>> assert a in opts and a == uAdd(x, y) + >>> uAdd(x + 1, x + 2) + x + x + 3 + """ + args = list(args) + newargs = [] + co = S.Zero + while args: + a = args.pop() + if a.is_Add: + # this will keep nesting from building up + # so that x + (x + 1) -> x + x + 1 (3 args) + args.extend(a.args) + elif a.is_Number: + co += a + else: + newargs.append(a) + _addsort(newargs) + if co: + newargs.insert(0, co) + return Add._from_args(newargs) + + +class Add(Expr, AssocOp): + """ + Expression representing addition operation for algebraic group. + + .. deprecated:: 1.7 + + Using arguments that aren't subclasses of :class:`~.Expr` in core + operators (:class:`~.Mul`, :class:`~.Add`, and :class:`~.Pow`) is + deprecated. See :ref:`non-expr-args-deprecated` for details. + + Every argument of ``Add()`` must be ``Expr``. Infix operator ``+`` + on most scalar objects in SymPy calls this class. + + Another use of ``Add()`` is to represent the structure of abstract + addition so that its arguments can be substituted to return different + class. Refer to examples section for this. + + ``Add()`` evaluates the argument unless ``evaluate=False`` is passed. + The evaluation logic includes: + + 1. Flattening + ``Add(x, Add(y, z))`` -> ``Add(x, y, z)`` + + 2. Identity removing + ``Add(x, 0, y)`` -> ``Add(x, y)`` + + 3. Coefficient collecting by ``.as_coeff_Mul()`` + ``Add(x, 2*x)`` -> ``Mul(3, x)`` + + 4. Term sorting + ``Add(y, x, 2)`` -> ``Add(2, x, y)`` + + If no argument is passed, identity element 0 is returned. If single + element is passed, that element is returned. + + Note that ``Add(*args)`` is more efficient than ``sum(args)`` because + it flattens the arguments. ``sum(a, b, c, ...)`` recursively adds the + arguments as ``a + (b + (c + ...))``, which has quadratic complexity. + On the other hand, ``Add(a, b, c, d)`` does not assume nested + structure, making the complexity linear. + + Since addition is group operation, every argument should have the + same :obj:`sympy.core.kind.Kind()`. + + Examples + ======== + + >>> from sympy import Add, I + >>> from sympy.abc import x, y + >>> Add(x, 1) + x + 1 + >>> Add(x, x) + 2*x + >>> 2*x**2 + 3*x + I*y + 2*y + 2*x/5 + 1.0*y + 1 + 2*x**2 + 17*x/5 + 3.0*y + I*y + 1 + + If ``evaluate=False`` is passed, result is not evaluated. + + >>> Add(1, 2, evaluate=False) + 1 + 2 + >>> Add(x, x, evaluate=False) + x + x + + ``Add()`` also represents the general structure of addition operation. + + >>> from sympy import MatrixSymbol + >>> A,B = MatrixSymbol('A', 2,2), MatrixSymbol('B', 2,2) + >>> expr = Add(x,y).subs({x:A, y:B}) + >>> expr + A + B + >>> type(expr) + + + Note that the printers do not display in args order. + + >>> Add(x, 1) + x + 1 + >>> Add(x, 1).args + (1, x) + + See Also + ======== + + MatAdd + + """ + + __slots__ = () + + is_Add = True + + _args_type = Expr + + identity: ClassVar[Expr] + + if TYPE_CHECKING: + + def __new__(cls, *args: Expr | complex, evaluate: bool=True) -> Expr: # type: ignore + ... + + @property + def args(self) -> tuple[Expr, ...]: + ... + + @classmethod + def flatten(cls, seq: list[Expr]) -> tuple[list[Expr], list[Expr], None]: + """ + Takes the sequence "seq" of nested Adds and returns a flatten list. + + Returns: (commutative_part, noncommutative_part, order_symbols) + + Applies associativity, all terms are commutable with respect to + addition. + + NB: the removal of 0 is already handled by AssocOp.__new__ + + See Also + ======== + + sympy.core.mul.Mul.flatten + + """ + from sympy.calculus.accumulationbounds import AccumBounds + from sympy.matrices.expressions import MatrixExpr + from sympy.tensor.tensor import TensExpr, TensAdd + rv = None + if len(seq) == 2: + a, b = seq + if b.is_Rational: + a, b = b, a + if a.is_Rational: + if b.is_Mul: + rv = [a, b], [], None + if rv: + if all(s.is_commutative for s in rv[0]): + return rv + return [], rv[0], None + + # term -> coeff + # e.g. x**2 -> 5 for ... + 5*x**2 + ... + terms: dict[Expr, Number] = {} + + # coefficient (Number or zoo) to always be in slot 0 + # e.g. 3 + ... + coeff: Expr = S.Zero + + order_factors: list[Order] = [] + + extra: list[MatrixExpr] = [] + + for o in seq: + + # O(x) + if o.is_Order: + if o.expr.is_zero: # type: ignore + continue + if any(o1.contains(o) for o1 in order_factors): + continue + order_factors = [o1 for o1 in order_factors if not o.contains(o1)] # type: ignore + order_factors = [o] + order_factors # type: ignore + continue + + # 3 or NaN + elif o.is_Number: + if (o is S.NaN or coeff is S.ComplexInfinity and + o.is_finite is False) and not extra: + # we know for sure the result will be nan + return [S.NaN], [], None + if coeff.is_Number or isinstance(coeff, AccumBounds): + coeff += o + if coeff is S.NaN and not extra: + # we know for sure the result will be nan + return [S.NaN], [], None + continue + + elif isinstance(o, AccumBounds): + coeff = o.__add__(coeff) + continue + + elif isinstance(o, MatrixExpr): + # can't add 0 to Matrix so make sure coeff is not 0 + extra.append(o) + continue + + elif isinstance(o, TensExpr): + coeff = TensAdd(o, coeff).doit(deep=False) + continue + + elif o is S.ComplexInfinity: + if coeff.is_finite is False and not extra: + # we know for sure the result will be nan + return [S.NaN], [], None + coeff = S.ComplexInfinity + continue + + # Add([...]) + elif o.is_Add: + # NB: here we assume Add is always commutative + o_args: tuple[Expr, ...] = o.args # type: ignore + seq.extend(o_args) # TODO zerocopy? + continue + + # Mul([...]) + elif o.is_Mul: + c, s = o.as_coeff_Mul() + + # check for unevaluated Pow, e.g. 2**3 or 2**(-1/2) + elif o.is_Pow: + b, e = o.as_base_exp() + if b.is_Number and (e.is_Integer or + (e.is_Rational and e.is_negative)): + seq.append(b**e) + continue + c, s = S.One, o + + else: + # everything else + c = S.One + s = o + + # now we have: + # o = c*s, where + # + # c is a Number + # s is an expression with number factor extracted + # let's collect terms with the same s, so e.g. + # 2*x**2 + 3*x**2 -> 5*x**2 + if s in terms: + terms[s] += c + if terms[s] is S.NaN and not extra: + # we know for sure the result will be nan + return [S.NaN], [], None + else: + terms[s] = c + + # now let's construct new args: + # [2*x**2, x**3, 7*x**4, pi, ...] + newseq = [] + noncommutative = False + for s, c in terms.items(): + # 0*s + if c.is_zero: + continue + # 1*s + elif c is S.One: + newseq.append(s) + # c*s + else: + if s.is_Mul: + # Mul, already keeps its arguments in perfect order. + # so we can simply put c in slot0 and go the fast way. + # + # XXX: This breaks VectorMul unless it overrides + # _new_rawargs + cs = s._new_rawargs(*((c,) + s.args)) # type: ignore + newseq.append(cs) + elif s.is_Add: + # we just re-create the unevaluated Mul + newseq.append(Mul(c, s, evaluate=False)) + else: + # alternatively we have to call all Mul's machinery (slow) + newseq.append(Mul(c, s)) + + noncommutative = noncommutative or not s.is_commutative + + # oo, -oo + if coeff is S.Infinity: + newseq = [f for f in newseq if not (f.is_extended_nonnegative or f.is_real)] + + elif coeff is S.NegativeInfinity: + newseq = [f for f in newseq if not (f.is_extended_nonpositive or f.is_real)] + + if coeff is S.ComplexInfinity: + # zoo might be + # infinite_real + finite_im + # finite_real + infinite_im + # infinite_real + infinite_im + # addition of a finite real or imaginary number won't be able to + # change the zoo nature; adding an infinite qualtity would result + # in a NaN condition if it had sign opposite of the infinite + # portion of zoo, e.g., infinite_real - infinite_real. + newseq = [c for c in newseq if not (c.is_finite and + c.is_extended_real is not None)] + + # process O(x) + if order_factors: + newseq2 = [] + for t in newseq: + # x + O(x) -> O(x) + if not any(o.contains(t) for o in order_factors): + newseq2.append(t) + newseq = newseq2 + order_factors # type: ignore + # 1 + O(1) -> O(1) + for o in order_factors: + if o.contains(coeff): + coeff = S.Zero + break + + # order args canonically + _addsort(newseq) + + # current code expects coeff to be first + if coeff is not S.Zero: + newseq.insert(0, coeff) + + if extra: + newseq += extra + noncommutative = True + + # we are done + if noncommutative: + return [], newseq, None + else: + return newseq, [], None + + @classmethod + def class_key(cls): + return 3, 1, cls.__name__ + + @property + def kind(self): + k = attrgetter('kind') + kinds = map(k, self.args) + kinds = frozenset(kinds) + if len(kinds) != 1: + # Since addition is group operator, kind must be same. + # We know that this is unexpected signature, so return this. + result = UndefinedKind + else: + result, = kinds + return result + + def could_extract_minus_sign(self): + return _could_extract_minus_sign(self) + + @cacheit + def as_coeff_add(self, *deps): + """ + Returns a tuple (coeff, args) where self is treated as an Add and coeff + is the Number term and args is a tuple of all other terms. + + Examples + ======== + + >>> from sympy.abc import x + >>> (7 + 3*x).as_coeff_add() + (7, (3*x,)) + >>> (7*x).as_coeff_add() + (0, (7*x,)) + """ + if deps: + l1, l2 = sift(self.args, lambda x: x.has_free(*deps), binary=True) + return self._new_rawargs(*l2), tuple(l1) + coeff, notrat = self.args[0].as_coeff_add() + if coeff is not S.Zero: + return coeff, notrat + self.args[1:] + return S.Zero, self.args + + def as_coeff_Add(self, rational=False, deps=None) -> tuple[Number, Expr]: + """ + Efficiently extract the coefficient of a summation. + """ + coeff, args = self.args[0], self.args[1:] + + if coeff.is_Number and not rational or coeff.is_Rational: + return coeff, self._new_rawargs(*args) # type: ignore + return S.Zero, self + + # Note, we intentionally do not implement Add.as_coeff_mul(). Rather, we + # let Expr.as_coeff_mul() just always return (S.One, self) for an Add. See + # issue 5524. + + def _eval_power(self, expt): + from .evalf import pure_complex + from .relational import is_eq + if len(self.args) == 2 and any(_.is_infinite for _ in self.args): + if expt.is_zero is False and is_eq(expt, S.One) is False: + # looking for literal a + I*b + a, b = self.args + if a.coeff(S.ImaginaryUnit): + a, b = b, a + ico = b.coeff(S.ImaginaryUnit) + if ico and ico.is_extended_real and a.is_extended_real: + if expt.is_extended_negative: + return S.Zero + if expt.is_extended_positive: + return S.ComplexInfinity + return + if expt.is_Rational and self.is_number: + ri = pure_complex(self) + if ri: + r, i = ri + if expt.q == 2: + from sympy.functions.elementary.miscellaneous import sqrt + D = sqrt(r**2 + i**2) + if D.is_Rational: + from .exprtools import factor_terms + from sympy.functions.elementary.complexes import sign + from .function import expand_multinomial + # (r, i, D) is a Pythagorean triple + root = sqrt(factor_terms((D - r)/2))**expt.p + return root*expand_multinomial(( + # principle value + (D + r)/abs(i) + sign(i)*S.ImaginaryUnit)**expt.p) + elif expt == -1: + return _unevaluated_Mul( + r - i*S.ImaginaryUnit, + 1/(r**2 + i**2)) + + @cacheit + def _eval_derivative(self, s): + return self.func(*[a.diff(s) for a in self.args]) + + def _eval_nseries(self, x, n, logx, cdir=0): + terms = [t.nseries(x, n=n, logx=logx, cdir=cdir) for t in self.args] + return self.func(*terms) + + def _matches_simple(self, expr, repl_dict): + # handle (w+3).matches('x+5') -> {w: x+2} + coeff, terms = self.as_coeff_add() + if len(terms) == 1: + return terms[0].matches(expr - coeff, repl_dict) + return + + def matches(self, expr, repl_dict=None, old=False): + return self._matches_commutative(expr, repl_dict, old) + + @staticmethod + def _combine_inverse(lhs, rhs): + """ + Returns lhs - rhs, but treats oo like a symbol so oo - oo + returns 0, instead of a nan. + """ + from sympy.simplify.simplify import signsimp + inf = (S.Infinity, S.NegativeInfinity) + if lhs.has(*inf) or rhs.has(*inf): + from .symbol import Dummy + oo = Dummy('oo') + reps = { + S.Infinity: oo, + S.NegativeInfinity: -oo} + ireps = {v: k for k, v in reps.items()} + eq = lhs.xreplace(reps) - rhs.xreplace(reps) + if eq.has(oo): + eq = eq.replace( + lambda x: x.is_Pow and x.base is oo, + lambda x: x.base) + rv = eq.xreplace(ireps) + else: + rv = lhs - rhs + srv = signsimp(rv) + return srv if srv.is_Number else rv + + @cacheit + def as_two_terms(self): + """Return head and tail of self. + + This is the most efficient way to get the head and tail of an + expression. + + - if you want only the head, use self.args[0]; + - if you want to process the arguments of the tail then use + self.as_coef_add() which gives the head and a tuple containing + the arguments of the tail when treated as an Add. + - if you want the coefficient when self is treated as a Mul + then use self.as_coeff_mul()[0] + + >>> from sympy.abc import x, y + >>> (3*x - 2*y + 5).as_two_terms() + (5, 3*x - 2*y) + """ + return self.args[0], self._new_rawargs(*self.args[1:]) + + def as_numer_denom(self) -> tuple[Expr, Expr]: + """ + Decomposes an expression to its numerator part and its + denominator part. + + Examples + ======== + + >>> from sympy.abc import x, y, z + >>> (x*y/z).as_numer_denom() + (x*y, z) + >>> (x*(y + 1)/y**7).as_numer_denom() + (x*(y + 1), y**7) + + See Also + ======== + + sympy.core.expr.Expr.as_numer_denom + """ + # clear rational denominator + content, expr = self.primitive() + if not isinstance(expr, Add): + return Mul(content, expr, evaluate=False).as_numer_denom() + ncon, dcon = content.as_numer_denom() + + # collect numerators and denominators of the terms + nd = defaultdict(list) + for f in expr.args: + ni, di = f.as_numer_denom() + nd[di].append(ni) + + # check for quick exit + if len(nd) == 1: + d, n = nd.popitem() + return self.func( + *[_keep_coeff(ncon, ni) for ni in n]), _keep_coeff(dcon, d) + + # sum up the terms having a common denominator + nd2 = {d: self.func(*n) if len(n) > 1 else n[0] for d, n in nd.items()} + + # assemble single numerator and denominator + denoms, numers = [list(i) for i in zip(*iter(nd2.items()))] + n, d = self.func(*[Mul(*(denoms[:i] + [numers[i]] + denoms[i + 1:])) + for i in range(len(numers))]), Mul(*denoms) + + return _keep_coeff(ncon, n), _keep_coeff(dcon, d) + + def _eval_is_polynomial(self, syms): + return all(term._eval_is_polynomial(syms) for term in self.args) + + def _eval_is_rational_function(self, syms): + return all(term._eval_is_rational_function(syms) for term in self.args) + + def _eval_is_meromorphic(self, x, a): + return _fuzzy_group((arg.is_meromorphic(x, a) for arg in self.args), + quick_exit=True) + + def _eval_is_algebraic_expr(self, syms): + return all(term._eval_is_algebraic_expr(syms) for term in self.args) + + # assumption methods + _eval_is_real = lambda self: _fuzzy_group( + (a.is_real for a in self.args), quick_exit=True) + _eval_is_extended_real = lambda self: _fuzzy_group( + (a.is_extended_real for a in self.args), quick_exit=True) + _eval_is_complex = lambda self: _fuzzy_group( + (a.is_complex for a in self.args), quick_exit=True) + _eval_is_antihermitian = lambda self: _fuzzy_group( + (a.is_antihermitian for a in self.args), quick_exit=True) + _eval_is_finite = lambda self: _fuzzy_group( + (a.is_finite for a in self.args), quick_exit=True) + _eval_is_hermitian = lambda self: _fuzzy_group( + (a.is_hermitian for a in self.args), quick_exit=True) + _eval_is_integer = lambda self: _fuzzy_group( + (a.is_integer for a in self.args), quick_exit=True) + _eval_is_rational = lambda self: _fuzzy_group( + (a.is_rational for a in self.args), quick_exit=True) + _eval_is_algebraic = lambda self: _fuzzy_group( + (a.is_algebraic for a in self.args), quick_exit=True) + _eval_is_commutative = lambda self: _fuzzy_group( + a.is_commutative for a in self.args) + + def _eval_is_infinite(self): + sawinf = False + for a in self.args: + ainf = a.is_infinite + if ainf is None: + return None + elif ainf is True: + # infinite+infinite might not be infinite + if sawinf is True: + return None + sawinf = True + return sawinf + + def _eval_is_imaginary(self): + nz = [] + im_I = [] + for a in self.args: + if a.is_extended_real: + if a.is_zero: + pass + elif a.is_zero is False: + nz.append(a) + else: + return + elif a.is_imaginary: + im_I.append(a*S.ImaginaryUnit) + elif a.is_Mul and S.ImaginaryUnit in a.args: + coeff, ai = a.as_coeff_mul(S.ImaginaryUnit) + if ai == (S.ImaginaryUnit,) and coeff.is_extended_real: + im_I.append(-coeff) + else: + return + else: + return + b = self.func(*nz) + if b != self: + if b.is_zero: + return fuzzy_not(self.func(*im_I).is_zero) + elif b.is_zero is False: + return False + + def _eval_is_zero(self): + if self.is_commutative is False: + # issue 10528: there is no way to know if a nc symbol + # is zero or not + return + nz = [] + z = 0 + im_or_z = False + im = 0 + for a in self.args: + if a.is_extended_real: + if a.is_zero: + z += 1 + elif a.is_zero is False: + nz.append(a) + else: + return + elif a.is_imaginary: + im += 1 + elif a.is_Mul and S.ImaginaryUnit in a.args: + coeff, ai = a.as_coeff_mul(S.ImaginaryUnit) + if ai == (S.ImaginaryUnit,) and coeff.is_extended_real: + im_or_z = True + else: + return + else: + return + if z == len(self.args): + return True + if len(nz) in [0, len(self.args)]: + return None + b = self.func(*nz) + if b.is_zero: + if not im_or_z: + if im == 0: + return True + elif im == 1: + return False + if b.is_zero is False: + return False + + def _eval_is_odd(self): + l = [f for f in self.args if not (f.is_even is True)] + if not l: + return False + if l[0].is_odd: + return self._new_rawargs(*l[1:]).is_even + + def _eval_is_irrational(self): + for t in self.args: + a = t.is_irrational + if a: + others = list(self.args) + others.remove(t) + if all(x.is_rational is True for x in others): + return True + return None + if a is None: + return + return False + + def _all_nonneg_or_nonppos(self): + nn = np = 0 + for a in self.args: + if a.is_nonnegative: + if np: + return False + nn = 1 + elif a.is_nonpositive: + if nn: + return False + np = 1 + else: + break + else: + return True + + def _eval_is_extended_positive(self): + if self.is_number: + return super()._eval_is_extended_positive() + c, a = self.as_coeff_Add() + if not c.is_zero: + from .exprtools import _monotonic_sign + v = _monotonic_sign(a) + if v is not None: + s = v + c + if s != self and s.is_extended_positive and a.is_extended_nonnegative: + return True + if len(self.free_symbols) == 1: + v = _monotonic_sign(self) + if v is not None and v != self and v.is_extended_positive: + return True + pos = nonneg = nonpos = unknown_sign = False + saw_INF = set() + args = [a for a in self.args if not a.is_zero] + if not args: + return False + for a in args: + ispos = a.is_extended_positive + infinite = a.is_infinite + if infinite: + saw_INF.add(fuzzy_or((ispos, a.is_extended_nonnegative))) + if True in saw_INF and False in saw_INF: + return + if ispos: + pos = True + continue + elif a.is_extended_nonnegative: + nonneg = True + continue + elif a.is_extended_nonpositive: + nonpos = True + continue + + if infinite is None: + return + unknown_sign = True + + if saw_INF: + if len(saw_INF) > 1: + return + return saw_INF.pop() + elif unknown_sign: + return + elif not nonpos and not nonneg and pos: + return True + elif not nonpos and pos: + return True + elif not pos and not nonneg: + return False + + def _eval_is_extended_nonnegative(self): + if not self.is_number: + c, a = self.as_coeff_Add() + if not c.is_zero and a.is_extended_nonnegative: + from .exprtools import _monotonic_sign + v = _monotonic_sign(a) + if v is not None: + s = v + c + if s != self and s.is_extended_nonnegative: + return True + if len(self.free_symbols) == 1: + v = _monotonic_sign(self) + if v is not None and v != self and v.is_extended_nonnegative: + return True + + def _eval_is_extended_nonpositive(self): + if not self.is_number: + c, a = self.as_coeff_Add() + if not c.is_zero and a.is_extended_nonpositive: + from .exprtools import _monotonic_sign + v = _monotonic_sign(a) + if v is not None: + s = v + c + if s != self and s.is_extended_nonpositive: + return True + if len(self.free_symbols) == 1: + v = _monotonic_sign(self) + if v is not None and v != self and v.is_extended_nonpositive: + return True + + def _eval_is_extended_negative(self): + if self.is_number: + return super()._eval_is_extended_negative() + c, a = self.as_coeff_Add() + if not c.is_zero: + from .exprtools import _monotonic_sign + v = _monotonic_sign(a) + if v is not None: + s = v + c + if s != self and s.is_extended_negative and a.is_extended_nonpositive: + return True + if len(self.free_symbols) == 1: + v = _monotonic_sign(self) + if v is not None and v != self and v.is_extended_negative: + return True + neg = nonpos = nonneg = unknown_sign = False + saw_INF = set() + args = [a for a in self.args if not a.is_zero] + if not args: + return False + for a in args: + isneg = a.is_extended_negative + infinite = a.is_infinite + if infinite: + saw_INF.add(fuzzy_or((isneg, a.is_extended_nonpositive))) + if True in saw_INF and False in saw_INF: + return + if isneg: + neg = True + continue + elif a.is_extended_nonpositive: + nonpos = True + continue + elif a.is_extended_nonnegative: + nonneg = True + continue + + if infinite is None: + return + unknown_sign = True + + if saw_INF: + if len(saw_INF) > 1: + return + return saw_INF.pop() + elif unknown_sign: + return + elif not nonneg and not nonpos and neg: + return True + elif not nonneg and neg: + return True + elif not neg and not nonpos: + return False + + def _eval_subs(self, old, new): + if not old.is_Add: + if old is S.Infinity and -old in self.args: + # foo - oo is foo + (-oo) internally + return self.xreplace({-old: -new}) + return None + + coeff_self, terms_self = self.as_coeff_Add() + coeff_old, terms_old = old.as_coeff_Add() + + if coeff_self.is_Rational and coeff_old.is_Rational: + if terms_self == terms_old: # (2 + a).subs( 3 + a, y) -> -1 + y + return self.func(new, coeff_self, -coeff_old) + if terms_self == -terms_old: # (2 + a).subs(-3 - a, y) -> -1 - y + return self.func(-new, coeff_self, coeff_old) + + if coeff_self.is_Rational and coeff_old.is_Rational \ + or coeff_self == coeff_old: + args_old, args_self = self.func.make_args( + terms_old), self.func.make_args(terms_self) + if len(args_old) < len(args_self): # (a+b+c).subs(b+c,x) -> a+x + self_set = set(args_self) + old_set = set(args_old) + + if old_set < self_set: + ret_set = self_set - old_set + return self.func(new, coeff_self, -coeff_old, + *[s._subs(old, new) for s in ret_set]) + + args_old = self.func.make_args( + -terms_old) # (a+b+c+d).subs(-b-c,x) -> a-x+d + old_set = set(args_old) + if old_set < self_set: + ret_set = self_set - old_set + return self.func(-new, coeff_self, coeff_old, + *[s._subs(old, new) for s in ret_set]) + + def removeO(self): + args = [a for a in self.args if not a.is_Order] + return self._new_rawargs(*args) + + def getO(self): + args = [a for a in self.args if a.is_Order] + if args: + return self._new_rawargs(*args) + + @cacheit + def extract_leading_order(self, symbols, point=None): + """ + Returns the leading term and its order. + + Examples + ======== + + >>> from sympy.abc import x + >>> (x + 1 + 1/x**5).extract_leading_order(x) + ((x**(-5), O(x**(-5))),) + >>> (1 + x).extract_leading_order(x) + ((1, O(1)),) + >>> (x + x**2).extract_leading_order(x) + ((x, O(x)),) + + """ + from sympy.series.order import Order + lst = [] + symbols = list(symbols if is_sequence(symbols) else [symbols]) + if not point: + point = [0]*len(symbols) + seq = [(f, Order(f, *zip(symbols, point))) for f in self.args] + for ef, of in seq: + for e, o in lst: + if o.contains(of) and o != of: + of = None + break + if of is None: + continue + new_lst = [(ef, of)] + for e, o in lst: + if of.contains(o) and o != of: + continue + new_lst.append((e, o)) + lst = new_lst + return tuple(lst) + + def as_real_imag(self, deep=True, **hints): + """ + Return a tuple representing a complex number. + + Examples + ======== + + >>> from sympy import I + >>> (7 + 9*I).as_real_imag() + (7, 9) + >>> ((1 + I)/(1 - I)).as_real_imag() + (0, 1) + >>> ((1 + 2*I)*(1 + 3*I)).as_real_imag() + (-5, 5) + """ + sargs = self.args + re_part, im_part = [], [] + for term in sargs: + re, im = term.as_real_imag(deep=deep) + re_part.append(re) + im_part.append(im) + return (self.func(*re_part), self.func(*im_part)) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.core.symbol import Dummy, Symbol + from sympy.series.order import Order + from sympy.functions.elementary.exponential import log + from sympy.functions.elementary.piecewise import Piecewise, piecewise_fold + from .function import expand_mul + + o = self.getO() + if o is None: + o = Order(0) + old = self.removeO() + + if old.has(Piecewise): + old = piecewise_fold(old) + + # This expansion is the last part of expand_log. expand_log also calls + # expand_mul with factor=True, which would be more expensive + if any(isinstance(a, log) for a in self.args): + logflags = {"deep": True, "log": True, "mul": False, "power_exp": False, + "power_base": False, "multinomial": False, "basic": False, "force": False, + "factor": False} + old = old.expand(**logflags) + expr = expand_mul(old) + + if not expr.is_Add: + return expr.as_leading_term(x, logx=logx, cdir=cdir) + + infinite = [t for t in expr.args if t.is_infinite] + + _logx = Dummy('logx') if logx is None else logx + leading_terms = [t.as_leading_term(x, logx=_logx, cdir=cdir) for t in expr.args] + + min, new_expr = Order(0), S.Zero + + try: + for term in leading_terms: + order = Order(term, x) + if not min or order not in min: + min = order + new_expr = term + elif min in order: + new_expr += term + + except TypeError: + return expr + + if logx is None: + new_expr = new_expr.subs(_logx, log(x)) + + is_zero = new_expr.is_zero + if is_zero is None: + new_expr = new_expr.trigsimp().cancel() + is_zero = new_expr.is_zero + if is_zero is True: + # simple leading term analysis gave us cancelled terms but we have to send + # back a term, so compute the leading term (via series) + try: + n0 = min.getn() + except NotImplementedError: + n0 = S.One + if n0.has(Symbol): + n0 = S.One + res = Order(1) + incr = S.One + while res.is_Order: + res = old._eval_nseries(x, n=n0+incr, logx=logx, cdir=cdir).cancel().powsimp().trigsimp() + incr *= 2 + return res.as_leading_term(x, logx=logx, cdir=cdir) + + elif new_expr is S.NaN: + return old.func._from_args(infinite) + o + + else: + return new_expr + + def _eval_adjoint(self): + return self.func(*[t.adjoint() for t in self.args]) + + def _eval_conjugate(self): + return self.func(*[t.conjugate() for t in self.args]) + + def _eval_transpose(self): + return self.func(*[t.transpose() for t in self.args]) + + def primitive(self): + """ + Return ``(R, self/R)`` where ``R``` is the Rational GCD of ``self```. + + ``R`` is collected only from the leading coefficient of each term. + + Examples + ======== + + >>> from sympy.abc import x, y + + >>> (2*x + 4*y).primitive() + (2, x + 2*y) + + >>> (2*x/3 + 4*y/9).primitive() + (2/9, 3*x + 2*y) + + >>> (2*x/3 + 4.2*y).primitive() + (1/3, 2*x + 12.6*y) + + No subprocessing of term factors is performed: + + >>> ((2 + 2*x)*x + 2).primitive() + (1, x*(2*x + 2) + 2) + + Recursive processing can be done with the ``as_content_primitive()`` + method: + + >>> ((2 + 2*x)*x + 2).as_content_primitive() + (2, x*(x + 1) + 1) + + See also: primitive() function in polytools.py + + """ + + terms = [] + inf = False + for a in self.args: + c, m = a.as_coeff_Mul() + if not c.is_Rational: + c = S.One + m = a + inf = inf or m is S.ComplexInfinity + terms.append((c.p, c.q, m)) + + if not inf: + ngcd = reduce(igcd, [t[0] for t in terms], 0) + dlcm = reduce(ilcm, [t[1] for t in terms], 1) + else: + ngcd = reduce(igcd, [t[0] for t in terms if t[1]], 0) + dlcm = reduce(ilcm, [t[1] for t in terms if t[1]], 1) + + if ngcd == dlcm == 1: + return S.One, self + if not inf: + for i, (p, q, term) in enumerate(terms): + terms[i] = _keep_coeff(Rational((p//ngcd)*(dlcm//q)), term) + else: + for i, (p, q, term) in enumerate(terms): + if q: + terms[i] = _keep_coeff(Rational((p//ngcd)*(dlcm//q)), term) + else: + terms[i] = _keep_coeff(Rational(p, q), term) + + # we don't need a complete re-flattening since no new terms will join + # so we just use the same sort as is used in Add.flatten. When the + # coefficient changes, the ordering of terms may change, e.g. + # (3*x, 6*y) -> (2*y, x) + # + # We do need to make sure that term[0] stays in position 0, however. + # + if terms[0].is_Number or terms[0] is S.ComplexInfinity: + c = terms.pop(0) + else: + c = None + _addsort(terms) + if c: + terms.insert(0, c) + return Rational(ngcd, dlcm), self._new_rawargs(*terms) + + def as_content_primitive(self, radical=False, clear=True): + """Return the tuple (R, self/R) where R is the positive Rational + extracted from self. If radical is True (default is False) then + common radicals will be removed and included as a factor of the + primitive expression. + + Examples + ======== + + >>> from sympy import sqrt + >>> (3 + 3*sqrt(2)).as_content_primitive() + (3, 1 + sqrt(2)) + + Radical content can also be factored out of the primitive: + + >>> (2*sqrt(2) + 4*sqrt(10)).as_content_primitive(radical=True) + (2, sqrt(2)*(1 + 2*sqrt(5))) + + See docstring of Expr.as_content_primitive for more examples. + """ + con, prim = self.func(*[_keep_coeff(*a.as_content_primitive( + radical=radical, clear=clear)) for a in self.args]).primitive() + if not clear and not con.is_Integer and prim.is_Add: + con, d = con.as_numer_denom() + _p = prim/d + if any(a.as_coeff_Mul()[0].is_Integer for a in _p.args): + prim = _p + else: + con /= d + if radical and prim.is_Add: + # look for common radicals that can be removed + args = prim.args + rads = [] + common_q = None + for m in args: + term_rads = defaultdict(list) + for ai in Mul.make_args(m): + if ai.is_Pow: + b, e = ai.as_base_exp() + if e.is_Rational and b.is_Integer: + term_rads[e.q].append(abs(int(b))**e.p) + if not term_rads: + break + if common_q is None: + common_q = set(term_rads.keys()) + else: + common_q = common_q & set(term_rads.keys()) + if not common_q: + break + rads.append(term_rads) + else: + # process rads + # keep only those in common_q + for r in rads: + for q in list(r.keys()): + if q not in common_q: + r.pop(q) + for q in r: + r[q] = Mul(*r[q]) + # find the gcd of bases for each q + G = [] + for q in common_q: + g = reduce(igcd, [r[q] for r in rads], 0) + if g != 1: + G.append(g**Rational(1, q)) + if G: + G = Mul(*G) + args = [ai/G for ai in args] + prim = G*prim.func(*args) + + return con, prim + + @property + def _sorted_args(self): + from .sorting import default_sort_key + return tuple(sorted(self.args, key=default_sort_key)) + + def _eval_difference_delta(self, n, step): + from sympy.series.limitseq import difference_delta as dd + return self.func(*[dd(a, n, step) for a in self.args]) + + @property + def _mpc_(self): + """ + Convert self to an mpmath mpc if possible + """ + from .numbers import Float + re_part, rest = self.as_coeff_Add() + im_part, imag_unit = rest.as_coeff_Mul() + if not imag_unit == S.ImaginaryUnit: + # ValueError may seem more reasonable but since it's a @property, + # we need to use AttributeError to keep from confusing things like + # hasattr. + raise AttributeError("Cannot convert Add to mpc. Must be of the form Number + Number*I") + + return (Float(re_part)._mpf_, Float(im_part)._mpf_) + + def __neg__(self): + if not global_parameters.distribute: + return super().__neg__() + return Mul(S.NegativeOne, self) + +add = AssocOpDispatcher('add') + +from .mul import Mul, _keep_coeff, _unevaluated_Mul +from .numbers import Rational diff --git a/venv/lib/python3.11/site-packages/sympy/core/alphabets.py b/venv/lib/python3.11/site-packages/sympy/core/alphabets.py new file mode 100644 index 0000000000000000000000000000000000000000..1ea2ae1c410ccd30e7ec9551f4cd8b19a36cdba1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/alphabets.py @@ -0,0 +1,4 @@ +greeks = ('alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', + 'eta', 'theta', 'iota', 'kappa', 'lambda', 'mu', 'nu', + 'xi', 'omicron', 'pi', 'rho', 'sigma', 'tau', 'upsilon', + 'phi', 'chi', 'psi', 'omega') diff --git a/venv/lib/python3.11/site-packages/sympy/core/assumptions.py b/venv/lib/python3.11/site-packages/sympy/core/assumptions.py new file mode 100644 index 0000000000000000000000000000000000000000..677e86c5e39390b0b188a5158dd2fabfbac4c760 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/assumptions.py @@ -0,0 +1,692 @@ +""" +This module contains the machinery handling assumptions. +Do also consider the guide :ref:`assumptions-guide`. + +All symbolic objects have assumption attributes that can be accessed via +``.is_`` attribute. + +Assumptions determine certain properties of symbolic objects and can +have 3 possible values: ``True``, ``False``, ``None``. ``True`` is returned if the +object has the property and ``False`` is returned if it does not or cannot +(i.e. does not make sense): + + >>> from sympy import I + >>> I.is_algebraic + True + >>> I.is_real + False + >>> I.is_prime + False + +When the property cannot be determined (or when a method is not +implemented) ``None`` will be returned. For example, a generic symbol, ``x``, +may or may not be positive so a value of ``None`` is returned for ``x.is_positive``. + +By default, all symbolic values are in the largest set in the given context +without specifying the property. For example, a symbol that has a property +being integer, is also real, complex, etc. + +Here follows a list of possible assumption names: + +.. glossary:: + + commutative + object commutes with any other object with + respect to multiplication operation. See [12]_. + + complex + object can have only values from the set + of complex numbers. See [13]_. + + imaginary + object value is a number that can be written as a real + number multiplied by the imaginary unit ``I``. See + [3]_. Please note that ``0`` is not considered to be an + imaginary number, see + `issue #7649 `_. + + real + object can have only values from the set + of real numbers. + + extended_real + object can have only values from the set + of real numbers, ``oo`` and ``-oo``. + + integer + object can have only values from the set + of integers. + + odd + even + object can have only values from the set of + odd (even) integers [2]_. + + prime + object is a natural number greater than 1 that has + no positive divisors other than 1 and itself. See [6]_. + + composite + object is a positive integer that has at least one positive + divisor other than 1 or the number itself. See [4]_. + + zero + object has the value of 0. + + nonzero + object is a real number that is not zero. + + rational + object can have only values from the set + of rationals. + + algebraic + object can have only values from the set + of algebraic numbers [11]_. + + transcendental + object can have only values from the set + of transcendental numbers [10]_. + + irrational + object value cannot be represented exactly by :class:`~.Rational`, see [5]_. + + finite + infinite + object absolute value is bounded (arbitrarily large). + See [7]_, [8]_, [9]_. + + negative + nonnegative + object can have only negative (nonnegative) + values [1]_. + + positive + nonpositive + object can have only positive (nonpositive) values. + + extended_negative + extended_nonnegative + extended_positive + extended_nonpositive + extended_nonzero + as without the extended part, but also including infinity with + corresponding sign, e.g., extended_positive includes ``oo`` + + hermitian + antihermitian + object belongs to the field of Hermitian + (antihermitian) operators. + +Examples +======== + + >>> from sympy import Symbol + >>> x = Symbol('x', real=True); x + x + >>> x.is_real + True + >>> x.is_complex + True + +See Also +======== + +.. seealso:: + + :py:class:`sympy.core.numbers.ImaginaryUnit` + :py:class:`sympy.core.numbers.Zero` + :py:class:`sympy.core.numbers.One` + :py:class:`sympy.core.numbers.Infinity` + :py:class:`sympy.core.numbers.NegativeInfinity` + :py:class:`sympy.core.numbers.ComplexInfinity` + +Notes +===== + +The fully-resolved assumptions for any SymPy expression +can be obtained as follows: + + >>> from sympy.core.assumptions import assumptions + >>> x = Symbol('x',positive=True) + >>> assumptions(x + I) + {'commutative': True, 'complex': True, 'composite': False, 'even': + False, 'extended_negative': False, 'extended_nonnegative': False, + 'extended_nonpositive': False, 'extended_nonzero': False, + 'extended_positive': False, 'extended_real': False, 'finite': True, + 'imaginary': False, 'infinite': False, 'integer': False, 'irrational': + False, 'negative': False, 'noninteger': False, 'nonnegative': False, + 'nonpositive': False, 'nonzero': False, 'odd': False, 'positive': + False, 'prime': False, 'rational': False, 'real': False, 'zero': + False} + +Developers Notes +================ + +The current (and possibly incomplete) values are stored +in the ``obj._assumptions dictionary``; queries to getter methods +(with property decorators) or attributes of objects/classes +will return values and update the dictionary. + + >>> eq = x**2 + I + >>> eq._assumptions + {} + >>> eq.is_finite + True + >>> eq._assumptions + {'finite': True, 'infinite': False} + +For a :class:`~.Symbol`, there are two locations for assumptions that may +be of interest. The ``assumptions0`` attribute gives the full set of +assumptions derived from a given set of initial assumptions. The +latter assumptions are stored as ``Symbol._assumptions_orig`` + + >>> Symbol('x', prime=True, even=True)._assumptions_orig + {'even': True, 'prime': True} + +The ``_assumptions_orig`` are not necessarily canonical nor are they filtered +in any way: they records the assumptions used to instantiate a Symbol and (for +storage purposes) represent a more compact representation of the assumptions +needed to recreate the full set in ``Symbol.assumptions0``. + + +References +========== + +.. [1] https://en.wikipedia.org/wiki/Negative_number +.. [2] https://en.wikipedia.org/wiki/Parity_%28mathematics%29 +.. [3] https://en.wikipedia.org/wiki/Imaginary_number +.. [4] https://en.wikipedia.org/wiki/Composite_number +.. [5] https://en.wikipedia.org/wiki/Irrational_number +.. [6] https://en.wikipedia.org/wiki/Prime_number +.. [7] https://en.wikipedia.org/wiki/Finite +.. [8] https://docs.python.org/3/library/math.html#math.isfinite +.. [9] https://numpy.org/doc/stable/reference/generated/numpy.isfinite.html +.. [10] https://en.wikipedia.org/wiki/Transcendental_number +.. [11] https://en.wikipedia.org/wiki/Algebraic_number +.. [12] https://en.wikipedia.org/wiki/Commutative_property +.. [13] https://en.wikipedia.org/wiki/Complex_number + +""" + +from sympy.utilities.exceptions import sympy_deprecation_warning + +from .facts import FactRules, FactKB +from .sympify import sympify + +from sympy.core.random import _assumptions_shuffle as shuffle +from sympy.core.assumptions_generated import generated_assumptions as _assumptions + +def _load_pre_generated_assumption_rules() -> FactRules: + """ Load the assumption rules from pre-generated data + + To update the pre-generated data, see :method::`_generate_assumption_rules` + """ + _assume_rules=FactRules._from_python(_assumptions) + return _assume_rules + +def _generate_assumption_rules(): + """ Generate the default assumption rules + + This method should only be called to update the pre-generated + assumption rules. + + To update the pre-generated assumptions run: bin/ask_update.py + + """ + _assume_rules = FactRules([ + + 'integer -> rational', + 'rational -> real', + 'rational -> algebraic', + 'algebraic -> complex', + 'transcendental == complex & !algebraic', + 'real -> hermitian', + 'imaginary -> complex', + 'imaginary -> antihermitian', + 'extended_real -> commutative', + 'complex -> commutative', + 'complex -> finite', + + 'odd == integer & !even', + 'even == integer & !odd', + + 'real -> complex', + 'extended_real -> real | infinite', + 'real == extended_real & finite', + + 'extended_real == extended_negative | zero | extended_positive', + 'extended_negative == extended_nonpositive & extended_nonzero', + 'extended_positive == extended_nonnegative & extended_nonzero', + + 'extended_nonpositive == extended_real & !extended_positive', + 'extended_nonnegative == extended_real & !extended_negative', + + 'real == negative | zero | positive', + 'negative == nonpositive & nonzero', + 'positive == nonnegative & nonzero', + + 'nonpositive == real & !positive', + 'nonnegative == real & !negative', + + 'positive == extended_positive & finite', + 'negative == extended_negative & finite', + 'nonpositive == extended_nonpositive & finite', + 'nonnegative == extended_nonnegative & finite', + 'nonzero == extended_nonzero & finite', + + 'zero -> even & finite', + 'zero == extended_nonnegative & extended_nonpositive', + 'zero == nonnegative & nonpositive', + 'nonzero -> real', + + 'prime -> integer & positive', + 'composite -> integer & positive & !prime', + '!composite -> !positive | !even | prime', + + 'irrational == real & !rational', + + 'imaginary -> !extended_real', + + 'infinite == !finite', + 'noninteger == extended_real & !integer', + 'extended_nonzero == extended_real & !zero', + ]) + return _assume_rules + + +_assume_rules = _load_pre_generated_assumption_rules() +_assume_defined = _assume_rules.defined_facts.copy() +_assume_defined.add('polar') +_assume_defined = frozenset(_assume_defined) + + +def assumptions(expr, _check=None): + """return the T/F assumptions of ``expr``""" + n = sympify(expr) + if n.is_Symbol: + rv = n.assumptions0 # are any important ones missing? + if _check is not None: + rv = {k: rv[k] for k in set(rv) & set(_check)} + return rv + rv = {} + for k in _assume_defined if _check is None else _check: + v = getattr(n, 'is_{}'.format(k)) + if v is not None: + rv[k] = v + return rv + + +def common_assumptions(exprs, check=None): + """return those assumptions which have the same True or False + value for all the given expressions. + + Examples + ======== + + >>> from sympy.core import common_assumptions + >>> from sympy import oo, pi, sqrt + >>> common_assumptions([-4, 0, sqrt(2), 2, pi, oo]) + {'commutative': True, 'composite': False, + 'extended_real': True, 'imaginary': False, 'odd': False} + + By default, all assumptions are tested; pass an iterable of the + assumptions to limit those that are reported: + + >>> common_assumptions([0, 1, 2], ['positive', 'integer']) + {'integer': True} + """ + check = _assume_defined if check is None else set(check) + if not check or not exprs: + return {} + + # get all assumptions for each + assume = [assumptions(i, _check=check) for i in sympify(exprs)] + # focus on those of interest that are True + for i, e in enumerate(assume): + assume[i] = {k: e[k] for k in set(e) & check} + # what assumptions are in common? + common = set.intersection(*[set(i) for i in assume]) + # which ones hold the same value + a = assume[0] + return {k: a[k] for k in common if all(a[k] == b[k] + for b in assume)} + + +def failing_assumptions(expr, **assumptions): + """ + Return a dictionary containing assumptions with values not + matching those of the passed assumptions. + + Examples + ======== + + >>> from sympy import failing_assumptions, Symbol + + >>> x = Symbol('x', positive=True) + >>> y = Symbol('y') + >>> failing_assumptions(6*x + y, positive=True) + {'positive': None} + + >>> failing_assumptions(x**2 - 1, positive=True) + {'positive': None} + + If *expr* satisfies all of the assumptions, an empty dictionary is returned. + + >>> failing_assumptions(x**2, positive=True) + {} + + """ + expr = sympify(expr) + failed = {} + for k in assumptions: + test = getattr(expr, 'is_%s' % k, None) + if test is not assumptions[k]: + failed[k] = test + return failed # {} or {assumption: value != desired} + + +def check_assumptions(expr, against=None, **assume): + """ + Checks whether assumptions of ``expr`` match the T/F assumptions + given (or possessed by ``against``). True is returned if all + assumptions match; False is returned if there is a mismatch and + the assumption in ``expr`` is not None; else None is returned. + + Explanation + =========== + + *assume* is a dict of assumptions with True or False values + + Examples + ======== + + >>> from sympy import Symbol, pi, I, exp, check_assumptions + >>> check_assumptions(-5, integer=True) + True + >>> check_assumptions(pi, real=True, integer=False) + True + >>> check_assumptions(pi, negative=True) + False + >>> check_assumptions(exp(I*pi/7), real=False) + True + >>> x = Symbol('x', positive=True) + >>> check_assumptions(2*x + 1, positive=True) + True + >>> check_assumptions(-2*x - 5, positive=True) + False + + To check assumptions of *expr* against another variable or expression, + pass the expression or variable as ``against``. + + >>> check_assumptions(2*x + 1, x) + True + + To see if a number matches the assumptions of an expression, pass + the number as the first argument, else its specific assumptions + may not have a non-None value in the expression: + + >>> check_assumptions(x, 3) + >>> check_assumptions(3, x) + True + + ``None`` is returned if ``check_assumptions()`` could not conclude. + + >>> check_assumptions(2*x - 1, x) + + >>> z = Symbol('z') + >>> check_assumptions(z, real=True) + + See Also + ======== + + failing_assumptions + + """ + expr = sympify(expr) + if against is not None: + if assume: + raise ValueError( + 'Expecting `against` or `assume`, not both.') + assume = assumptions(against) + known = True + for k, v in assume.items(): + if v is None: + continue + e = getattr(expr, 'is_' + k, None) + if e is None: + known = None + elif v != e: + return False + return known + + +class StdFactKB(FactKB): + """A FactKB specialized for the built-in rules + + This is the only kind of FactKB that Basic objects should use. + """ + def __init__(self, facts=None): + super().__init__(_assume_rules) + # save a copy of the facts dict + if not facts: + self._generator = {} + elif not isinstance(facts, FactKB): + self._generator = facts.copy() + else: + self._generator = facts.generator + if facts: + self.deduce_all_facts(facts) + + def copy(self): + return self.__class__(self) + + @property + def generator(self): + return self._generator.copy() + + +def as_property(fact): + """Convert a fact name to the name of the corresponding property""" + return 'is_%s' % fact + + +def make_property(fact): + """Create the automagic property corresponding to a fact.""" + + def getit(self): + try: + return self._assumptions[fact] + except KeyError: + if self._assumptions is self.default_assumptions: + self._assumptions = self.default_assumptions.copy() + return _ask(fact, self) + + getit.func_name = as_property(fact) + return property(getit) + + +def _ask(fact, obj): + """ + Find the truth value for a property of an object. + + This function is called when a request is made to see what a fact + value is. + + For this we use several techniques: + + First, the fact-evaluation function is tried, if it exists (for + example _eval_is_integer). Then we try related facts. For example + + rational --> integer + + another example is joined rule: + + integer & !odd --> even + + so in the latter case if we are looking at what 'even' value is, + 'integer' and 'odd' facts will be asked. + + In all cases, when we settle on some fact value, its implications are + deduced, and the result is cached in ._assumptions. + """ + # FactKB which is dict-like and maps facts to their known values: + assumptions = obj._assumptions + + # A dict that maps facts to their handlers: + handler_map = obj._prop_handler + + # This is our queue of facts to check: + facts_to_check = [fact] + facts_queued = {fact} + + # Loop over the queue as it extends + for fact_i in facts_to_check: + + # If fact_i has already been determined then we don't need to rerun the + # handler. There is a potential race condition for multithreaded code + # though because it's possible that fact_i was checked in another + # thread. The main logic of the loop below would potentially skip + # checking assumptions[fact] in this case so we check it once after the + # loop to be sure. + if fact_i in assumptions: + continue + + # Now we call the associated handler for fact_i if it exists. + fact_i_value = None + handler_i = handler_map.get(fact_i) + if handler_i is not None: + fact_i_value = handler_i(obj) + + # If we get a new value for fact_i then we should update our knowledge + # of fact_i as well as any related facts that can be inferred using the + # inference rules connecting the fact_i and any other fact values that + # are already known. + if fact_i_value is not None: + assumptions.deduce_all_facts(((fact_i, fact_i_value),)) + + # Usually if assumptions[fact] is now not None then that is because of + # the call to deduce_all_facts above. The handler for fact_i returned + # True or False and knowing fact_i (which is equal to fact in the first + # iteration) implies knowing a value for fact. It is also possible + # though that independent code e.g. called indirectly by the handler or + # called in another thread in a multithreaded context might have + # resulted in assumptions[fact] being set. Either way we return it. + fact_value = assumptions.get(fact) + if fact_value is not None: + return fact_value + + # Extend the queue with other facts that might determine fact_i. Here + # we randomise the order of the facts that are checked. This should not + # lead to any non-determinism if all handlers are logically consistent + # with the inference rules for the facts. Non-deterministic assumptions + # queries can result from bugs in the handlers that are exposed by this + # call to shuffle. These are pushed to the back of the queue meaning + # that the inference graph is traversed in breadth-first order. + new_facts_to_check = list(_assume_rules.prereq[fact_i] - facts_queued) + shuffle(new_facts_to_check) + facts_to_check.extend(new_facts_to_check) + facts_queued.update(new_facts_to_check) + + # The above loop should be able to handle everything fine in a + # single-threaded context but in multithreaded code it is possible that + # this thread skipped computing a particular fact that was computed in + # another thread (due to the continue). In that case it is possible that + # fact was inferred and is now stored in the assumptions dict but it wasn't + # checked for in the body of the loop. This is an obscure case but to make + # sure we catch it we check once here at the end of the loop. + if fact in assumptions: + return assumptions[fact] + + # This query can not be answered. It's possible that e.g. another thread + # has already stored None for fact but assumptions._tell does not mind if + # we call _tell twice setting the same value. If this raises + # InconsistentAssumptions then it probably means that another thread + # attempted to compute this and got a value of True or False rather than + # None. In that case there must be a bug in at least one of the handlers. + # If the handlers are all deterministic and are consistent with the + # inference rules then the same value should be computed for fact in all + # threads. + assumptions._tell(fact, None) + return None + + +def _prepare_class_assumptions(cls): + """Precompute class level assumptions and generate handlers. + + This is called by Basic.__init_subclass__ each time a Basic subclass is + defined. + """ + + local_defs = {} + for k in _assume_defined: + attrname = as_property(k) + v = cls.__dict__.get(attrname, '') + if isinstance(v, (bool, int, type(None))): + if v is not None: + v = bool(v) + local_defs[k] = v + + defs = {} + for base in reversed(cls.__bases__): + assumptions = getattr(base, '_explicit_class_assumptions', None) + if assumptions is not None: + defs.update(assumptions) + defs.update(local_defs) + + cls._explicit_class_assumptions = defs + cls.default_assumptions = StdFactKB(defs) + + cls._prop_handler = {} + for k in _assume_defined: + eval_is_meth = getattr(cls, '_eval_is_%s' % k, None) + if eval_is_meth is not None: + cls._prop_handler[k] = eval_is_meth + + # Put definite results directly into the class dict, for speed + for k, v in cls.default_assumptions.items(): + setattr(cls, as_property(k), v) + + # protection e.g. for Integer.is_even=F <- (Rational.is_integer=F) + derived_from_bases = set() + for base in cls.__bases__: + default_assumptions = getattr(base, 'default_assumptions', None) + # is an assumption-aware class + if default_assumptions is not None: + derived_from_bases.update(default_assumptions) + + for fact in derived_from_bases - set(cls.default_assumptions): + pname = as_property(fact) + if pname not in cls.__dict__: + setattr(cls, pname, make_property(fact)) + + # Finally, add any missing automagic property (e.g. for Basic) + for fact in _assume_defined: + pname = as_property(fact) + if not hasattr(cls, pname): + setattr(cls, pname, make_property(fact)) + + +# XXX: ManagedProperties used to be the metaclass for Basic but now Basic does +# not use a metaclass. We leave this here for backwards compatibility for now +# in case someone has been using the ManagedProperties class in downstream +# code. The reason that it might have been used is that when subclassing a +# class and wanting to use a metaclass the metaclass must be a subclass of the +# metaclass for the class that is being subclassed. Anyone wanting to subclass +# Basic and use a metaclass in their subclass would have needed to subclass +# ManagedProperties. Here ManagedProperties is not the metaclass for Basic any +# more but it should still be usable as a metaclass for Basic subclasses since +# it is a subclass of type which is now the metaclass for Basic. +class ManagedProperties(type): + def __init__(cls, *args, **kwargs): + msg = ("The ManagedProperties metaclass. " + "Basic does not use metaclasses any more") + sympy_deprecation_warning(msg, + deprecated_since_version="1.12", + active_deprecations_target='managedproperties') + + # Here we still call this function in case someone is using + # ManagedProperties for something that is not a Basic subclass. For + # Basic subclasses this function is now called by __init_subclass__ and + # so this metaclass is not needed any more. + _prepare_class_assumptions(cls) diff --git a/venv/lib/python3.11/site-packages/sympy/core/assumptions_generated.py b/venv/lib/python3.11/site-packages/sympy/core/assumptions_generated.py new file mode 100644 index 0000000000000000000000000000000000000000..b4b2597a72b500155370db385b58e61f0f951984 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/assumptions_generated.py @@ -0,0 +1,1615 @@ +""" +Do NOT manually edit this file. +Instead, run ./bin/ask_update.py. +""" + +defined_facts = [ + 'algebraic', + 'antihermitian', + 'commutative', + 'complex', + 'composite', + 'even', + 'extended_negative', + 'extended_nonnegative', + 'extended_nonpositive', + 'extended_nonzero', + 'extended_positive', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'integer', + 'irrational', + 'negative', + 'noninteger', + 'nonnegative', + 'nonpositive', + 'nonzero', + 'odd', + 'positive', + 'prime', + 'rational', + 'real', + 'transcendental', + 'zero', +] # defined_facts + + +full_implications = dict( [ + # Implications of algebraic = True: + (('algebraic', True), set( ( + ('commutative', True), + ('complex', True), + ('finite', True), + ('infinite', False), + ('transcendental', False), + ) ), + ), + # Implications of algebraic = False: + (('algebraic', False), set( ( + ('composite', False), + ('even', False), + ('integer', False), + ('odd', False), + ('prime', False), + ('rational', False), + ('zero', False), + ) ), + ), + # Implications of antihermitian = True: + (('antihermitian', True), set( ( + ) ), + ), + # Implications of antihermitian = False: + (('antihermitian', False), set( ( + ('imaginary', False), + ) ), + ), + # Implications of commutative = True: + (('commutative', True), set( ( + ) ), + ), + # Implications of commutative = False: + (('commutative', False), set( ( + ('algebraic', False), + ('complex', False), + ('composite', False), + ('even', False), + ('extended_negative', False), + ('extended_nonnegative', False), + ('extended_nonpositive', False), + ('extended_nonzero', False), + ('extended_positive', False), + ('extended_real', False), + ('imaginary', False), + ('integer', False), + ('irrational', False), + ('negative', False), + ('noninteger', False), + ('nonnegative', False), + ('nonpositive', False), + ('nonzero', False), + ('odd', False), + ('positive', False), + ('prime', False), + ('rational', False), + ('real', False), + ('transcendental', False), + ('zero', False), + ) ), + ), + # Implications of complex = True: + (('complex', True), set( ( + ('commutative', True), + ('finite', True), + ('infinite', False), + ) ), + ), + # Implications of complex = False: + (('complex', False), set( ( + ('algebraic', False), + ('composite', False), + ('even', False), + ('imaginary', False), + ('integer', False), + ('irrational', False), + ('negative', False), + ('nonnegative', False), + ('nonpositive', False), + ('nonzero', False), + ('odd', False), + ('positive', False), + ('prime', False), + ('rational', False), + ('real', False), + ('transcendental', False), + ('zero', False), + ) ), + ), + # Implications of composite = True: + (('composite', True), set( ( + ('algebraic', True), + ('commutative', True), + ('complex', True), + ('extended_negative', False), + ('extended_nonnegative', True), + ('extended_nonpositive', False), + ('extended_nonzero', True), + ('extended_positive', True), + ('extended_real', True), + ('finite', True), + ('hermitian', True), + ('imaginary', False), + ('infinite', False), + ('integer', True), + ('irrational', False), + ('negative', False), + ('noninteger', False), + ('nonnegative', True), + ('nonpositive', False), + ('nonzero', True), + ('positive', True), + ('prime', False), + ('rational', True), + ('real', True), + ('transcendental', False), + ('zero', False), + ) ), + ), + # Implications of composite = False: + (('composite', False), set( ( + ) ), + ), + # Implications of even = True: + (('even', True), set( ( + ('algebraic', True), + ('commutative', True), + ('complex', True), + ('extended_real', True), + ('finite', True), + ('hermitian', True), + ('imaginary', False), + ('infinite', False), + ('integer', True), + ('irrational', False), + ('noninteger', False), + ('odd', False), + ('rational', True), + ('real', True), + ('transcendental', False), + ) ), + ), + # Implications of even = False: + (('even', False), set( ( + ('zero', False), + ) ), + ), + # Implications of extended_negative = True: + (('extended_negative', True), set( ( + ('commutative', True), + ('composite', False), + ('extended_nonnegative', False), + ('extended_nonpositive', True), + ('extended_nonzero', True), + ('extended_positive', False), + ('extended_real', True), + ('imaginary', False), + ('nonnegative', False), + ('positive', False), + ('prime', False), + ('zero', False), + ) ), + ), + # Implications of extended_negative = False: + (('extended_negative', False), set( ( + ('negative', False), + ) ), + ), + # Implications of extended_nonnegative = True: + (('extended_nonnegative', True), set( ( + ('commutative', True), + ('extended_negative', False), + ('extended_real', True), + ('imaginary', False), + ('negative', False), + ) ), + ), + # Implications of extended_nonnegative = False: + (('extended_nonnegative', False), set( ( + ('composite', False), + ('extended_positive', False), + ('nonnegative', False), + ('positive', False), + ('prime', False), + ('zero', False), + ) ), + ), + # Implications of extended_nonpositive = True: + (('extended_nonpositive', True), set( ( + ('commutative', True), + ('composite', False), + ('extended_positive', False), + ('extended_real', True), + ('imaginary', False), + ('positive', False), + ('prime', False), + ) ), + ), + # Implications of extended_nonpositive = False: + (('extended_nonpositive', False), set( ( + ('extended_negative', False), + ('negative', False), + ('nonpositive', False), + ('zero', False), + ) ), + ), + # Implications of extended_nonzero = True: + (('extended_nonzero', True), set( ( + ('commutative', True), + ('extended_real', True), + ('imaginary', False), + ('zero', False), + ) ), + ), + # Implications of extended_nonzero = False: + (('extended_nonzero', False), set( ( + ('composite', False), + ('extended_negative', False), + ('extended_positive', False), + ('negative', False), + ('nonzero', False), + ('positive', False), + ('prime', False), + ) ), + ), + # Implications of extended_positive = True: + (('extended_positive', True), set( ( + ('commutative', True), + ('extended_negative', False), + ('extended_nonnegative', True), + ('extended_nonpositive', False), + ('extended_nonzero', True), + ('extended_real', True), + ('imaginary', False), + ('negative', False), + ('nonpositive', False), + ('zero', False), + ) ), + ), + # Implications of extended_positive = False: + (('extended_positive', False), set( ( + ('composite', False), + ('positive', False), + ('prime', False), + ) ), + ), + # Implications of extended_real = True: + (('extended_real', True), set( ( + ('commutative', True), + ('imaginary', False), + ) ), + ), + # Implications of extended_real = False: + (('extended_real', False), set( ( + ('composite', False), + ('even', False), + ('extended_negative', False), + ('extended_nonnegative', False), + ('extended_nonpositive', False), + ('extended_nonzero', False), + ('extended_positive', False), + ('integer', False), + ('irrational', False), + ('negative', False), + ('noninteger', False), + ('nonnegative', False), + ('nonpositive', False), + ('nonzero', False), + ('odd', False), + ('positive', False), + ('prime', False), + ('rational', False), + ('real', False), + ('zero', False), + ) ), + ), + # Implications of finite = True: + (('finite', True), set( ( + ('infinite', False), + ) ), + ), + # Implications of finite = False: + (('finite', False), set( ( + ('algebraic', False), + ('complex', False), + ('composite', False), + ('even', False), + ('imaginary', False), + ('infinite', True), + ('integer', False), + ('irrational', False), + ('negative', False), + ('nonnegative', False), + ('nonpositive', False), + ('nonzero', False), + ('odd', False), + ('positive', False), + ('prime', False), + ('rational', False), + ('real', False), + ('transcendental', False), + ('zero', False), + ) ), + ), + # Implications of hermitian = True: + (('hermitian', True), set( ( + ) ), + ), + # Implications of hermitian = False: + (('hermitian', False), set( ( + ('composite', False), + ('even', False), + ('integer', False), + ('irrational', False), + ('negative', False), + ('nonnegative', False), + ('nonpositive', False), + ('nonzero', False), + ('odd', False), + ('positive', False), + ('prime', False), + ('rational', False), + ('real', False), + ('zero', False), + ) ), + ), + # Implications of imaginary = True: + (('imaginary', True), set( ( + ('antihermitian', True), + ('commutative', True), + ('complex', True), + ('composite', False), + ('even', False), + ('extended_negative', False), + ('extended_nonnegative', False), + ('extended_nonpositive', False), + ('extended_nonzero', False), + ('extended_positive', False), + ('extended_real', False), + ('finite', True), + ('infinite', False), + ('integer', False), + ('irrational', False), + ('negative', False), + ('noninteger', False), + ('nonnegative', False), + ('nonpositive', False), + ('nonzero', False), + ('odd', False), + ('positive', False), + ('prime', False), + ('rational', False), + ('real', False), + ('zero', False), + ) ), + ), + # Implications of imaginary = False: + (('imaginary', False), set( ( + ) ), + ), + # Implications of infinite = True: + (('infinite', True), set( ( + ('algebraic', False), + ('complex', False), + ('composite', False), + ('even', False), + ('finite', False), + ('imaginary', False), + ('integer', False), + ('irrational', False), + ('negative', False), + ('nonnegative', False), + ('nonpositive', False), + ('nonzero', False), + ('odd', False), + ('positive', False), + ('prime', False), + ('rational', False), + ('real', False), + ('transcendental', False), + ('zero', False), + ) ), + ), + # Implications of infinite = False: + (('infinite', False), set( ( + ('finite', True), + ) ), + ), + # Implications of integer = True: + (('integer', True), set( ( + ('algebraic', True), + ('commutative', True), + ('complex', True), + ('extended_real', True), + ('finite', True), + ('hermitian', True), + ('imaginary', False), + ('infinite', False), + ('irrational', False), + ('noninteger', False), + ('rational', True), + ('real', True), + ('transcendental', False), + ) ), + ), + # Implications of integer = False: + (('integer', False), set( ( + ('composite', False), + ('even', False), + ('odd', False), + ('prime', False), + ('zero', False), + ) ), + ), + # Implications of irrational = True: + (('irrational', True), set( ( + ('commutative', True), + ('complex', True), + ('composite', False), + ('even', False), + ('extended_nonzero', True), + ('extended_real', True), + ('finite', True), + ('hermitian', True), + ('imaginary', False), + ('infinite', False), + ('integer', False), + ('noninteger', True), + ('nonzero', True), + ('odd', False), + ('prime', False), + ('rational', False), + ('real', True), + ('zero', False), + ) ), + ), + # Implications of irrational = False: + (('irrational', False), set( ( + ) ), + ), + # Implications of negative = True: + (('negative', True), set( ( + ('commutative', True), + ('complex', True), + ('composite', False), + ('extended_negative', True), + ('extended_nonnegative', False), + ('extended_nonpositive', True), + ('extended_nonzero', True), + ('extended_positive', False), + ('extended_real', True), + ('finite', True), + ('hermitian', True), + ('imaginary', False), + ('infinite', False), + ('nonnegative', False), + ('nonpositive', True), + ('nonzero', True), + ('positive', False), + ('prime', False), + ('real', True), + ('zero', False), + ) ), + ), + # Implications of negative = False: + (('negative', False), set( ( + ) ), + ), + # Implications of noninteger = True: + (('noninteger', True), set( ( + ('commutative', True), + ('composite', False), + ('even', False), + ('extended_nonzero', True), + ('extended_real', True), + ('imaginary', False), + ('integer', False), + ('odd', False), + ('prime', False), + ('zero', False), + ) ), + ), + # Implications of noninteger = False: + (('noninteger', False), set( ( + ) ), + ), + # Implications of nonnegative = True: + (('nonnegative', True), set( ( + ('commutative', True), + ('complex', True), + ('extended_negative', False), + ('extended_nonnegative', True), + ('extended_real', True), + ('finite', True), + ('hermitian', True), + ('imaginary', False), + ('infinite', False), + ('negative', False), + ('real', True), + ) ), + ), + # Implications of nonnegative = False: + (('nonnegative', False), set( ( + ('composite', False), + ('positive', False), + ('prime', False), + ('zero', False), + ) ), + ), + # Implications of nonpositive = True: + (('nonpositive', True), set( ( + ('commutative', True), + ('complex', True), + ('composite', False), + ('extended_nonpositive', True), + ('extended_positive', False), + ('extended_real', True), + ('finite', True), + ('hermitian', True), + ('imaginary', False), + ('infinite', False), + ('positive', False), + ('prime', False), + ('real', True), + ) ), + ), + # Implications of nonpositive = False: + (('nonpositive', False), set( ( + ('negative', False), + ('zero', False), + ) ), + ), + # Implications of nonzero = True: + (('nonzero', True), set( ( + ('commutative', True), + ('complex', True), + ('extended_nonzero', True), + ('extended_real', True), + ('finite', True), + ('hermitian', True), + ('imaginary', False), + ('infinite', False), + ('real', True), + ('zero', False), + ) ), + ), + # Implications of nonzero = False: + (('nonzero', False), set( ( + ('composite', False), + ('negative', False), + ('positive', False), + ('prime', False), + ) ), + ), + # Implications of odd = True: + (('odd', True), set( ( + ('algebraic', True), + ('commutative', True), + ('complex', True), + ('even', False), + ('extended_nonzero', True), + ('extended_real', True), + ('finite', True), + ('hermitian', True), + ('imaginary', False), + ('infinite', False), + ('integer', True), + ('irrational', False), + ('noninteger', False), + ('nonzero', True), + ('rational', True), + ('real', True), + ('transcendental', False), + ('zero', False), + ) ), + ), + # Implications of odd = False: + (('odd', False), set( ( + ) ), + ), + # Implications of positive = True: + (('positive', True), set( ( + ('commutative', True), + ('complex', True), + ('extended_negative', False), + ('extended_nonnegative', True), + ('extended_nonpositive', False), + ('extended_nonzero', True), + ('extended_positive', True), + ('extended_real', True), + ('finite', True), + ('hermitian', True), + ('imaginary', False), + ('infinite', False), + ('negative', False), + ('nonnegative', True), + ('nonpositive', False), + ('nonzero', True), + ('real', True), + ('zero', False), + ) ), + ), + # Implications of positive = False: + (('positive', False), set( ( + ('composite', False), + ('prime', False), + ) ), + ), + # Implications of prime = True: + (('prime', True), set( ( + ('algebraic', True), + ('commutative', True), + ('complex', True), + ('composite', False), + ('extended_negative', False), + ('extended_nonnegative', True), + ('extended_nonpositive', False), + ('extended_nonzero', True), + ('extended_positive', True), + ('extended_real', True), + ('finite', True), + ('hermitian', True), + ('imaginary', False), + ('infinite', False), + ('integer', True), + ('irrational', False), + ('negative', False), + ('noninteger', False), + ('nonnegative', True), + ('nonpositive', False), + ('nonzero', True), + ('positive', True), + ('rational', True), + ('real', True), + ('transcendental', False), + ('zero', False), + ) ), + ), + # Implications of prime = False: + (('prime', False), set( ( + ) ), + ), + # Implications of rational = True: + (('rational', True), set( ( + ('algebraic', True), + ('commutative', True), + ('complex', True), + ('extended_real', True), + ('finite', True), + ('hermitian', True), + ('imaginary', False), + ('infinite', False), + ('irrational', False), + ('real', True), + ('transcendental', False), + ) ), + ), + # Implications of rational = False: + (('rational', False), set( ( + ('composite', False), + ('even', False), + ('integer', False), + ('odd', False), + ('prime', False), + ('zero', False), + ) ), + ), + # Implications of real = True: + (('real', True), set( ( + ('commutative', True), + ('complex', True), + ('extended_real', True), + ('finite', True), + ('hermitian', True), + ('imaginary', False), + ('infinite', False), + ) ), + ), + # Implications of real = False: + (('real', False), set( ( + ('composite', False), + ('even', False), + ('integer', False), + ('irrational', False), + ('negative', False), + ('nonnegative', False), + ('nonpositive', False), + ('nonzero', False), + ('odd', False), + ('positive', False), + ('prime', False), + ('rational', False), + ('zero', False), + ) ), + ), + # Implications of transcendental = True: + (('transcendental', True), set( ( + ('algebraic', False), + ('commutative', True), + ('complex', True), + ('composite', False), + ('even', False), + ('finite', True), + ('infinite', False), + ('integer', False), + ('odd', False), + ('prime', False), + ('rational', False), + ('zero', False), + ) ), + ), + # Implications of transcendental = False: + (('transcendental', False), set( ( + ) ), + ), + # Implications of zero = True: + (('zero', True), set( ( + ('algebraic', True), + ('commutative', True), + ('complex', True), + ('composite', False), + ('even', True), + ('extended_negative', False), + ('extended_nonnegative', True), + ('extended_nonpositive', True), + ('extended_nonzero', False), + ('extended_positive', False), + ('extended_real', True), + ('finite', True), + ('hermitian', True), + ('imaginary', False), + ('infinite', False), + ('integer', True), + ('irrational', False), + ('negative', False), + ('noninteger', False), + ('nonnegative', True), + ('nonpositive', True), + ('nonzero', False), + ('odd', False), + ('positive', False), + ('prime', False), + ('rational', True), + ('real', True), + ('transcendental', False), + ) ), + ), + # Implications of zero = False: + (('zero', False), set( ( + ) ), + ), + ] ) # full_implications + + +prereq = { + + # facts that could determine the value of algebraic + 'algebraic': { + 'commutative', + 'complex', + 'composite', + 'even', + 'finite', + 'infinite', + 'integer', + 'odd', + 'prime', + 'rational', + 'transcendental', + 'zero', + }, + + # facts that could determine the value of antihermitian + 'antihermitian': { + 'imaginary', + }, + + # facts that could determine the value of commutative + 'commutative': { + 'algebraic', + 'complex', + 'composite', + 'even', + 'extended_negative', + 'extended_nonnegative', + 'extended_nonpositive', + 'extended_nonzero', + 'extended_positive', + 'extended_real', + 'imaginary', + 'integer', + 'irrational', + 'negative', + 'noninteger', + 'nonnegative', + 'nonpositive', + 'nonzero', + 'odd', + 'positive', + 'prime', + 'rational', + 'real', + 'transcendental', + 'zero', + }, + + # facts that could determine the value of complex + 'complex': { + 'algebraic', + 'commutative', + 'composite', + 'even', + 'finite', + 'imaginary', + 'infinite', + 'integer', + 'irrational', + 'negative', + 'nonnegative', + 'nonpositive', + 'nonzero', + 'odd', + 'positive', + 'prime', + 'rational', + 'real', + 'transcendental', + 'zero', + }, + + # facts that could determine the value of composite + 'composite': { + 'algebraic', + 'commutative', + 'complex', + 'extended_negative', + 'extended_nonnegative', + 'extended_nonpositive', + 'extended_nonzero', + 'extended_positive', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'integer', + 'irrational', + 'negative', + 'noninteger', + 'nonnegative', + 'nonpositive', + 'nonzero', + 'positive', + 'prime', + 'rational', + 'real', + 'transcendental', + 'zero', + }, + + # facts that could determine the value of even + 'even': { + 'algebraic', + 'commutative', + 'complex', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'integer', + 'irrational', + 'noninteger', + 'odd', + 'rational', + 'real', + 'transcendental', + 'zero', + }, + + # facts that could determine the value of extended_negative + 'extended_negative': { + 'commutative', + 'composite', + 'extended_nonnegative', + 'extended_nonpositive', + 'extended_nonzero', + 'extended_positive', + 'extended_real', + 'imaginary', + 'negative', + 'nonnegative', + 'positive', + 'prime', + 'zero', + }, + + # facts that could determine the value of extended_nonnegative + 'extended_nonnegative': { + 'commutative', + 'composite', + 'extended_negative', + 'extended_positive', + 'extended_real', + 'imaginary', + 'negative', + 'nonnegative', + 'positive', + 'prime', + 'zero', + }, + + # facts that could determine the value of extended_nonpositive + 'extended_nonpositive': { + 'commutative', + 'composite', + 'extended_negative', + 'extended_positive', + 'extended_real', + 'imaginary', + 'negative', + 'nonpositive', + 'positive', + 'prime', + 'zero', + }, + + # facts that could determine the value of extended_nonzero + 'extended_nonzero': { + 'commutative', + 'composite', + 'extended_negative', + 'extended_positive', + 'extended_real', + 'imaginary', + 'irrational', + 'negative', + 'noninteger', + 'nonzero', + 'odd', + 'positive', + 'prime', + 'zero', + }, + + # facts that could determine the value of extended_positive + 'extended_positive': { + 'commutative', + 'composite', + 'extended_negative', + 'extended_nonnegative', + 'extended_nonpositive', + 'extended_nonzero', + 'extended_real', + 'imaginary', + 'negative', + 'nonpositive', + 'positive', + 'prime', + 'zero', + }, + + # facts that could determine the value of extended_real + 'extended_real': { + 'commutative', + 'composite', + 'even', + 'extended_negative', + 'extended_nonnegative', + 'extended_nonpositive', + 'extended_nonzero', + 'extended_positive', + 'imaginary', + 'integer', + 'irrational', + 'negative', + 'noninteger', + 'nonnegative', + 'nonpositive', + 'nonzero', + 'odd', + 'positive', + 'prime', + 'rational', + 'real', + 'zero', + }, + + # facts that could determine the value of finite + 'finite': { + 'algebraic', + 'complex', + 'composite', + 'even', + 'imaginary', + 'infinite', + 'integer', + 'irrational', + 'negative', + 'nonnegative', + 'nonpositive', + 'nonzero', + 'odd', + 'positive', + 'prime', + 'rational', + 'real', + 'transcendental', + 'zero', + }, + + # facts that could determine the value of hermitian + 'hermitian': { + 'composite', + 'even', + 'integer', + 'irrational', + 'negative', + 'nonnegative', + 'nonpositive', + 'nonzero', + 'odd', + 'positive', + 'prime', + 'rational', + 'real', + 'zero', + }, + + # facts that could determine the value of imaginary + 'imaginary': { + 'antihermitian', + 'commutative', + 'complex', + 'composite', + 'even', + 'extended_negative', + 'extended_nonnegative', + 'extended_nonpositive', + 'extended_nonzero', + 'extended_positive', + 'extended_real', + 'finite', + 'infinite', + 'integer', + 'irrational', + 'negative', + 'noninteger', + 'nonnegative', + 'nonpositive', + 'nonzero', + 'odd', + 'positive', + 'prime', + 'rational', + 'real', + 'zero', + }, + + # facts that could determine the value of infinite + 'infinite': { + 'algebraic', + 'complex', + 'composite', + 'even', + 'finite', + 'imaginary', + 'integer', + 'irrational', + 'negative', + 'nonnegative', + 'nonpositive', + 'nonzero', + 'odd', + 'positive', + 'prime', + 'rational', + 'real', + 'transcendental', + 'zero', + }, + + # facts that could determine the value of integer + 'integer': { + 'algebraic', + 'commutative', + 'complex', + 'composite', + 'even', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'irrational', + 'noninteger', + 'odd', + 'prime', + 'rational', + 'real', + 'transcendental', + 'zero', + }, + + # facts that could determine the value of irrational + 'irrational': { + 'commutative', + 'complex', + 'composite', + 'even', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'integer', + 'odd', + 'prime', + 'rational', + 'real', + 'zero', + }, + + # facts that could determine the value of negative + 'negative': { + 'commutative', + 'complex', + 'composite', + 'extended_negative', + 'extended_nonnegative', + 'extended_nonpositive', + 'extended_nonzero', + 'extended_positive', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'nonnegative', + 'nonpositive', + 'nonzero', + 'positive', + 'prime', + 'real', + 'zero', + }, + + # facts that could determine the value of noninteger + 'noninteger': { + 'commutative', + 'composite', + 'even', + 'extended_real', + 'imaginary', + 'integer', + 'irrational', + 'odd', + 'prime', + 'zero', + }, + + # facts that could determine the value of nonnegative + 'nonnegative': { + 'commutative', + 'complex', + 'composite', + 'extended_negative', + 'extended_nonnegative', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'negative', + 'positive', + 'prime', + 'real', + 'zero', + }, + + # facts that could determine the value of nonpositive + 'nonpositive': { + 'commutative', + 'complex', + 'composite', + 'extended_nonpositive', + 'extended_positive', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'negative', + 'positive', + 'prime', + 'real', + 'zero', + }, + + # facts that could determine the value of nonzero + 'nonzero': { + 'commutative', + 'complex', + 'composite', + 'extended_nonzero', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'irrational', + 'negative', + 'odd', + 'positive', + 'prime', + 'real', + 'zero', + }, + + # facts that could determine the value of odd + 'odd': { + 'algebraic', + 'commutative', + 'complex', + 'even', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'integer', + 'irrational', + 'noninteger', + 'rational', + 'real', + 'transcendental', + 'zero', + }, + + # facts that could determine the value of positive + 'positive': { + 'commutative', + 'complex', + 'composite', + 'extended_negative', + 'extended_nonnegative', + 'extended_nonpositive', + 'extended_nonzero', + 'extended_positive', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'negative', + 'nonnegative', + 'nonpositive', + 'nonzero', + 'prime', + 'real', + 'zero', + }, + + # facts that could determine the value of prime + 'prime': { + 'algebraic', + 'commutative', + 'complex', + 'composite', + 'extended_negative', + 'extended_nonnegative', + 'extended_nonpositive', + 'extended_nonzero', + 'extended_positive', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'integer', + 'irrational', + 'negative', + 'noninteger', + 'nonnegative', + 'nonpositive', + 'nonzero', + 'positive', + 'rational', + 'real', + 'transcendental', + 'zero', + }, + + # facts that could determine the value of rational + 'rational': { + 'algebraic', + 'commutative', + 'complex', + 'composite', + 'even', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'integer', + 'irrational', + 'odd', + 'prime', + 'real', + 'transcendental', + 'zero', + }, + + # facts that could determine the value of real + 'real': { + 'commutative', + 'complex', + 'composite', + 'even', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'integer', + 'irrational', + 'negative', + 'nonnegative', + 'nonpositive', + 'nonzero', + 'odd', + 'positive', + 'prime', + 'rational', + 'zero', + }, + + # facts that could determine the value of transcendental + 'transcendental': { + 'algebraic', + 'commutative', + 'complex', + 'composite', + 'even', + 'finite', + 'infinite', + 'integer', + 'odd', + 'prime', + 'rational', + 'zero', + }, + + # facts that could determine the value of zero + 'zero': { + 'algebraic', + 'commutative', + 'complex', + 'composite', + 'even', + 'extended_negative', + 'extended_nonnegative', + 'extended_nonpositive', + 'extended_nonzero', + 'extended_positive', + 'extended_real', + 'finite', + 'hermitian', + 'imaginary', + 'infinite', + 'integer', + 'irrational', + 'negative', + 'noninteger', + 'nonnegative', + 'nonpositive', + 'nonzero', + 'odd', + 'positive', + 'prime', + 'rational', + 'real', + 'transcendental', + }, + +} # prereq + + +# Note: the order of the beta rules is used in the beta_triggers +beta_rules = [ + + # Rules implying composite = True + ({('even', True), ('positive', True), ('prime', False)}, + ('composite', True)), + + # Rules implying even = False + ({('composite', False), ('positive', True), ('prime', False)}, + ('even', False)), + + # Rules implying even = True + ({('integer', True), ('odd', False)}, + ('even', True)), + + # Rules implying extended_negative = True + ({('extended_positive', False), ('extended_real', True), ('zero', False)}, + ('extended_negative', True)), + ({('extended_nonpositive', True), ('extended_nonzero', True)}, + ('extended_negative', True)), + + # Rules implying extended_nonnegative = True + ({('extended_negative', False), ('extended_real', True)}, + ('extended_nonnegative', True)), + + # Rules implying extended_nonpositive = True + ({('extended_positive', False), ('extended_real', True)}, + ('extended_nonpositive', True)), + + # Rules implying extended_nonzero = True + ({('extended_real', True), ('zero', False)}, + ('extended_nonzero', True)), + + # Rules implying extended_positive = True + ({('extended_negative', False), ('extended_real', True), ('zero', False)}, + ('extended_positive', True)), + ({('extended_nonnegative', True), ('extended_nonzero', True)}, + ('extended_positive', True)), + + # Rules implying extended_real = False + ({('infinite', False), ('real', False)}, + ('extended_real', False)), + ({('extended_negative', False), ('extended_positive', False), ('zero', False)}, + ('extended_real', False)), + + # Rules implying infinite = True + ({('extended_real', True), ('real', False)}, + ('infinite', True)), + + # Rules implying irrational = True + ({('rational', False), ('real', True)}, + ('irrational', True)), + + # Rules implying negative = True + ({('positive', False), ('real', True), ('zero', False)}, + ('negative', True)), + ({('nonpositive', True), ('nonzero', True)}, + ('negative', True)), + ({('extended_negative', True), ('finite', True)}, + ('negative', True)), + + # Rules implying noninteger = True + ({('extended_real', True), ('integer', False)}, + ('noninteger', True)), + + # Rules implying nonnegative = True + ({('negative', False), ('real', True)}, + ('nonnegative', True)), + ({('extended_nonnegative', True), ('finite', True)}, + ('nonnegative', True)), + + # Rules implying nonpositive = True + ({('positive', False), ('real', True)}, + ('nonpositive', True)), + ({('extended_nonpositive', True), ('finite', True)}, + ('nonpositive', True)), + + # Rules implying nonzero = True + ({('extended_nonzero', True), ('finite', True)}, + ('nonzero', True)), + + # Rules implying odd = True + ({('even', False), ('integer', True)}, + ('odd', True)), + + # Rules implying positive = False + ({('composite', False), ('even', True), ('prime', False)}, + ('positive', False)), + + # Rules implying positive = True + ({('negative', False), ('real', True), ('zero', False)}, + ('positive', True)), + ({('nonnegative', True), ('nonzero', True)}, + ('positive', True)), + ({('extended_positive', True), ('finite', True)}, + ('positive', True)), + + # Rules implying prime = True + ({('composite', False), ('even', True), ('positive', True)}, + ('prime', True)), + + # Rules implying real = False + ({('negative', False), ('positive', False), ('zero', False)}, + ('real', False)), + + # Rules implying real = True + ({('extended_real', True), ('infinite', False)}, + ('real', True)), + ({('extended_real', True), ('finite', True)}, + ('real', True)), + + # Rules implying transcendental = True + ({('algebraic', False), ('complex', True)}, + ('transcendental', True)), + + # Rules implying zero = True + ({('extended_negative', False), ('extended_positive', False), ('extended_real', True)}, + ('zero', True)), + ({('negative', False), ('positive', False), ('real', True)}, + ('zero', True)), + ({('extended_nonnegative', True), ('extended_nonpositive', True)}, + ('zero', True)), + ({('nonnegative', True), ('nonpositive', True)}, + ('zero', True)), + +] # beta_rules +beta_triggers = { + ('algebraic', False): [32, 11, 3, 8, 29, 14, 25, 13, 17, 7], + ('algebraic', True): [10, 30, 31, 27, 16, 21, 19, 22], + ('antihermitian', False): [], + ('commutative', False): [], + ('complex', False): [10, 12, 11, 3, 8, 17, 7], + ('complex', True): [32, 10, 30, 31, 27, 16, 21, 19, 22], + ('composite', False): [1, 28, 24], + ('composite', True): [23, 2], + ('even', False): [23, 11, 3, 8, 29, 14, 25, 7], + ('even', True): [3, 33, 8, 6, 5, 14, 34, 25, 20, 18, 27, 16, 21, 19, 22, 0, 28, 24, 7], + ('extended_negative', False): [11, 33, 8, 5, 29, 34, 25, 18], + ('extended_negative', True): [30, 12, 31, 29, 14, 20, 16, 21, 22, 17], + ('extended_nonnegative', False): [11, 3, 6, 29, 14, 20, 7], + ('extended_nonnegative', True): [30, 12, 31, 33, 8, 9, 6, 29, 34, 25, 18, 19, 35, 17, 7], + ('extended_nonpositive', False): [11, 8, 5, 29, 25, 18, 7], + ('extended_nonpositive', True): [30, 12, 31, 3, 33, 4, 5, 29, 14, 34, 20, 21, 35, 17, 7], + ('extended_nonzero', False): [11, 33, 6, 5, 29, 34, 20, 18], + ('extended_nonzero', True): [30, 12, 31, 3, 8, 4, 9, 6, 5, 29, 14, 25, 22, 17], + ('extended_positive', False): [11, 3, 33, 6, 29, 14, 34, 20], + ('extended_positive', True): [30, 12, 31, 29, 25, 18, 27, 19, 22, 17], + ('extended_real', False): [], + ('extended_real', True): [30, 12, 31, 3, 33, 8, 6, 5, 17, 7], + ('finite', False): [11, 3, 8, 17, 7], + ('finite', True): [10, 30, 31, 27, 16, 21, 19, 22], + ('hermitian', False): [10, 12, 11, 3, 8, 17, 7], + ('imaginary', True): [32], + ('infinite', False): [10, 30, 31, 27, 16, 21, 19, 22], + ('infinite', True): [11, 3, 8, 17, 7], + ('integer', False): [11, 3, 8, 29, 14, 25, 17, 7], + ('integer', True): [23, 2, 3, 33, 8, 6, 5, 14, 34, 25, 20, 18, 27, 16, 21, 19, 22, 7], + ('irrational', True): [32, 3, 8, 4, 9, 6, 5, 14, 25, 15, 26, 20, 18, 27, 16, 21, 19], + ('negative', False): [29, 34, 25, 18], + ('negative', True): [32, 13, 17], + ('noninteger', True): [30, 12, 31, 3, 8, 4, 9, 6, 5, 29, 14, 25, 22], + ('nonnegative', False): [11, 3, 8, 29, 14, 20, 7], + ('nonnegative', True): [32, 33, 8, 9, 6, 34, 25, 26, 20, 27, 21, 22, 35, 36, 13, 17, 7], + ('nonpositive', False): [11, 3, 8, 29, 25, 18, 7], + ('nonpositive', True): [32, 3, 33, 4, 5, 14, 34, 15, 18, 16, 19, 22, 35, 36, 13, 17, 7], + ('nonzero', False): [29, 34, 20, 18], + ('nonzero', True): [32, 3, 8, 4, 9, 6, 5, 14, 25, 15, 26, 20, 18, 27, 16, 21, 19, 13, 17], + ('odd', False): [2], + ('odd', True): [3, 8, 4, 9, 6, 5, 14, 25, 15, 26, 20, 18, 27, 16, 21, 19], + ('positive', False): [29, 14, 34, 20], + ('positive', True): [32, 0, 1, 28, 13, 17], + ('prime', False): [0, 1, 24], + ('prime', True): [23, 2], + ('rational', False): [11, 3, 8, 29, 14, 25, 13, 17, 7], + ('rational', True): [3, 33, 8, 6, 5, 14, 34, 25, 20, 18, 27, 16, 21, 19, 22, 17, 7], + ('real', False): [10, 12, 11, 3, 8, 17, 7], + ('real', True): [32, 3, 33, 8, 6, 5, 14, 34, 25, 20, 18, 27, 16, 21, 19, 22, 13, 17, 7], + ('transcendental', True): [10, 30, 31, 11, 3, 8, 29, 14, 25, 27, 16, 21, 19, 22, 13, 17, 7], + ('zero', False): [11, 3, 8, 29, 14, 25, 7], + ('zero', True): [], +} # beta_triggers + + +generated_assumptions = {'defined_facts': defined_facts, 'full_implications': full_implications, + 'prereq': prereq, 'beta_rules': beta_rules, 'beta_triggers': beta_triggers} diff --git a/venv/lib/python3.11/site-packages/sympy/core/backend.py b/venv/lib/python3.11/site-packages/sympy/core/backend.py new file mode 100644 index 0000000000000000000000000000000000000000..34a4e05a4a4ac50d0830960cb324871a20e9a12d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/backend.py @@ -0,0 +1,120 @@ +import os +USE_SYMENGINE = os.getenv('USE_SYMENGINE', '0') +USE_SYMENGINE = USE_SYMENGINE.lower() in ('1', 't', 'true') # type: ignore + +if USE_SYMENGINE: + from symengine import (Symbol, Integer, sympify as sympify_symengine, S, + SympifyError, exp, log, gamma, sqrt, I, E, pi, Matrix, + sin, cos, tan, cot, csc, sec, asin, acos, atan, acot, acsc, asec, + sinh, cosh, tanh, coth, asinh, acosh, atanh, acoth, + lambdify, symarray, diff, zeros, eye, diag, ones, + expand, Function, symbols, var, Add, Mul, Derivative, + ImmutableMatrix, MatrixBase, Rational, Basic) + from symengine.lib.symengine_wrapper import gcd as igcd + from symengine import AppliedUndef + + def sympify(a, *, strict=False): + """ + Notes + ===== + + SymEngine's ``sympify`` does not accept keyword arguments and is + therefore not compatible with SymPy's ``sympify`` with ``strict=True`` + (which ensures that only the types for which an explicit conversion has + been defined are converted). This wrapper adds an additional parameter + ``strict`` (with default ``False``) that will raise a ``SympifyError`` + if ``strict=True`` and the argument passed to the parameter ``a`` is a + string. + + See Also + ======== + + sympify: Converts an arbitrary expression to a type that can be used + inside SymPy. + + """ + # The parameter ``a`` is used for this function to keep compatibility + # with the SymEngine docstring. + if strict and isinstance(a, str): + raise SympifyError(a) + return sympify_symengine(a) + + # Keep the SymEngine docstring and append the additional "Notes" and "See + # Also" sections. Replacement of spaces is required to correctly format the + # indentation of the combined docstring. + sympify.__doc__ = ( + sympify_symengine.__doc__ + + sympify.__doc__.replace(' ', ' ') # type: ignore + ) +else: + from sympy.core.add import Add + from sympy.core.basic import Basic + from sympy.core.function import (diff, Function, AppliedUndef, + expand, Derivative) + from sympy.core.mul import Mul + from sympy.core.intfunc import igcd + from sympy.core.numbers import pi, I, Integer, Rational, E + from sympy.core.singleton import S + from sympy.core.symbol import Symbol, var, symbols + from sympy.core.sympify import SympifyError, sympify + from sympy.functions.elementary.exponential import log, exp + from sympy.functions.elementary.hyperbolic import (coth, sinh, + acosh, acoth, tanh, asinh, atanh, cosh) + from sympy.functions.elementary.miscellaneous import sqrt + from sympy.functions.elementary.trigonometric import (csc, + asec, cos, atan, sec, acot, asin, tan, sin, cot, acsc, acos) + from sympy.functions.special.gamma_functions import gamma + from sympy.matrices.dense import (eye, zeros, diag, Matrix, + ones, symarray) + from sympy.matrices.immutable import ImmutableMatrix + from sympy.matrices.matrixbase import MatrixBase + from sympy.utilities.lambdify import lambdify + + +# +# XXX: Handling of immutable and mutable matrices in SymEngine is inconsistent +# with SymPy's matrix classes in at least SymEngine version 0.7.0. Until that +# is fixed the function below is needed for consistent behaviour when +# attempting to simplify a matrix. +# +# Expected behaviour of a SymPy mutable/immutable matrix .simplify() method: +# +# Matrix.simplify() : works in place, returns None +# ImmutableMatrix.simplify() : returns a simplified copy +# +# In SymEngine both mutable and immutable matrices simplify in place and return +# None. This is inconsistent with the matrix being "immutable" and also the +# returned None leads to problems in the mechanics module. +# +# The simplify function should not be used because simplify(M) sympifies the +# matrix M and the SymEngine matrices all sympify to SymPy matrices. If we want +# to work with SymEngine matrices then we need to use their .simplify() method +# but that method does not work correctly with immutable matrices. +# +# The _simplify_matrix function can be removed when the SymEngine bug is fixed. +# Since this should be a temporary problem we do not make this function part of +# the public API. +# +# SymEngine issue: https://github.com/symengine/symengine.py/issues/363 +# + +def _simplify_matrix(M): + """Return a simplified copy of the matrix M""" + if not isinstance(M, (Matrix, ImmutableMatrix)): + raise TypeError("The matrix M must be an instance of Matrix or ImmutableMatrix") + Mnew = M.as_mutable() # makes a copy if mutable + Mnew.simplify() + if isinstance(M, ImmutableMatrix): + Mnew = Mnew.as_immutable() + return Mnew + + +__all__ = [ + 'Symbol', 'Integer', 'sympify', 'S', 'SympifyError', 'exp', 'log', + 'gamma', 'sqrt', 'I', 'E', 'pi', 'Matrix', 'sin', 'cos', 'tan', 'cot', + 'csc', 'sec', 'asin', 'acos', 'atan', 'acot', 'acsc', 'asec', 'sinh', + 'cosh', 'tanh', 'coth', 'asinh', 'acosh', 'atanh', 'acoth', 'lambdify', + 'symarray', 'diff', 'zeros', 'eye', 'diag', 'ones', 'expand', 'Function', + 'symbols', 'var', 'Add', 'Mul', 'Derivative', 'ImmutableMatrix', + 'MatrixBase', 'Rational', 'Basic', 'igcd', 'AppliedUndef', +] diff --git a/venv/lib/python3.11/site-packages/sympy/core/basic.py b/venv/lib/python3.11/site-packages/sympy/core/basic.py new file mode 100644 index 0000000000000000000000000000000000000000..92f6e710113ce0523ad15100abb0acd00f03f741 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/basic.py @@ -0,0 +1,2355 @@ +"""Base class for all the objects in SymPy""" +from __future__ import annotations + +from collections import Counter +from collections.abc import Mapping, Iterable +from itertools import zip_longest +from functools import cmp_to_key +from typing import TYPE_CHECKING, overload + +from .assumptions import _prepare_class_assumptions +from .cache import cacheit +from .sympify import _sympify, sympify, SympifyError, _external_converter +from .sorting import ordered +from .kind import Kind, UndefinedKind +from ._print_helpers import Printable + +from sympy.utilities.decorator import deprecated +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.utilities.iterables import iterable, numbered_symbols +from sympy.utilities.misc import filldedent, func_name + + +if TYPE_CHECKING: + from typing import ClassVar, TypeVar, Any + from typing_extensions import Self + from .assumptions import StdFactKB + from .symbol import Symbol + + Tbasic = TypeVar("Tbasic", bound='Basic') + + +def as_Basic(expr): + """Return expr as a Basic instance using strict sympify + or raise a TypeError; this is just a wrapper to _sympify, + raising a TypeError instead of a SympifyError.""" + try: + return _sympify(expr) + except SympifyError: + raise TypeError( + 'Argument must be a Basic object, not `%s`' % func_name( + expr)) + + +# Key for sorting commutative args in canonical order +# by name. This is used for canonical ordering of the +# args for Add and Mul *if* the names of both classes +# being compared appear here. Some things in this list +# are not spelled the same as their name so they do not, +# in effect, appear here. See Basic.compare. +ordering_of_classes = [ + # singleton numbers + 'Zero', 'One', 'Half', 'Infinity', 'NaN', 'NegativeOne', 'NegativeInfinity', + # numbers + 'Integer', 'Rational', 'Float', + # singleton symbols + 'Exp1', 'Pi', 'ImaginaryUnit', + # symbols + 'Symbol', 'Wild', + # arithmetic operations + 'Pow', 'Mul', 'Add', + # function values + 'Derivative', 'Integral', + # defined singleton functions + 'Abs', 'Sign', 'Sqrt', + 'Floor', 'Ceiling', + 'Re', 'Im', 'Arg', + 'Conjugate', + 'Exp', 'Log', + 'Sin', 'Cos', 'Tan', 'Cot', 'ASin', 'ACos', 'ATan', 'ACot', + 'Sinh', 'Cosh', 'Tanh', 'Coth', 'ASinh', 'ACosh', 'ATanh', 'ACoth', + 'RisingFactorial', 'FallingFactorial', + 'factorial', 'binomial', + 'Gamma', 'LowerGamma', 'UpperGamma', 'PolyGamma', + 'Erf', + # special polynomials + 'Chebyshev', 'Chebyshev2', + # undefined functions + 'Function', 'WildFunction', + # anonymous functions + 'Lambda', + # Landau O symbol + 'Order', + # relational operations + 'Equality', 'Unequality', 'StrictGreaterThan', 'StrictLessThan', + 'GreaterThan', 'LessThan', +] + +def _cmp_name(x: type, y: type) -> int: + """return -1, 0, 1 if the name of x is before that of y. + A string comparison is done if either name does not appear + in `ordering_of_classes`. This is the helper for + ``Basic.compare`` + + Examples + ======== + + >>> from sympy import cos, tan, sin + >>> from sympy.core import basic + >>> save = basic.ordering_of_classes + >>> basic.ordering_of_classes = () + >>> basic._cmp_name(cos, tan) + -1 + >>> basic.ordering_of_classes = ["tan", "sin", "cos"] + >>> basic._cmp_name(cos, tan) + 1 + >>> basic._cmp_name(sin, cos) + -1 + >>> basic.ordering_of_classes = save + + """ + n1 = x.__name__ + n2 = y.__name__ + if n1 == n2: + return 0 + + # If the other object is not a Basic subclass, then we are not equal to it. + if not issubclass(y, Basic): + return -1 + + UNKNOWN = len(ordering_of_classes) + 1 + try: + i1 = ordering_of_classes.index(n1) + except ValueError: + i1 = UNKNOWN + try: + i2 = ordering_of_classes.index(n2) + except ValueError: + i2 = UNKNOWN + if i1 == UNKNOWN and i2 == UNKNOWN: + return (n1 > n2) - (n1 < n2) + return (i1 > i2) - (i1 < i2) + + + +@cacheit +def _get_postprocessors(clsname, arg_type): + # Since only Add, Mul, Pow can be clsname, this cache + # is not quadratic. + postprocessors = set() + mappings = _get_postprocessors_for_type(arg_type) + for mapping in mappings: + f = mapping.get(clsname, None) + if f is not None: + postprocessors.update(f) + return postprocessors + +@cacheit +def _get_postprocessors_for_type(arg_type): + return tuple( + Basic._constructor_postprocessor_mapping[cls] + for cls in arg_type.mro() + if cls in Basic._constructor_postprocessor_mapping + ) + + +class Basic(Printable): + """ + Base class for all SymPy objects. + + Notes and conventions + ===================== + + 1) Always use ``.args``, when accessing parameters of some instance: + + >>> from sympy import cot + >>> from sympy.abc import x, y + + >>> cot(x).args + (x,) + + >>> cot(x).args[0] + x + + >>> (x*y).args + (x, y) + + >>> (x*y).args[1] + y + + + 2) Never use internal methods or variables (the ones prefixed with ``_``): + + >>> cot(x)._args # do not use this, use cot(x).args instead + (x,) + + + 3) By "SymPy object" we mean something that can be returned by + ``sympify``. But not all objects one encounters using SymPy are + subclasses of Basic. For example, mutable objects are not: + + >>> from sympy import Basic, Matrix, sympify + >>> A = Matrix([[1, 2], [3, 4]]).as_mutable() + >>> isinstance(A, Basic) + False + + >>> B = sympify(A) + >>> isinstance(B, Basic) + True + """ + __slots__ = ('_mhash', # hash value + '_args', # arguments + '_assumptions' + ) + + _args: tuple[Basic, ...] + _mhash: int | None + + @property + def __sympy__(self): + return True + + def __init_subclass__(cls): + # Initialize the default_assumptions FactKB and also any assumptions + # property methods. This method will only be called for subclasses of + # Basic but not for Basic itself so we call + # _prepare_class_assumptions(Basic) below the class definition. + super().__init_subclass__() + _prepare_class_assumptions(cls) + + # To be overridden with True in the appropriate subclasses + is_number = False + is_Atom = False + is_Symbol = False + is_symbol = False + is_Indexed = False + is_Dummy = False + is_Wild = False + is_Function = False + is_Add = False + is_Mul = False + is_Pow = False + is_Number = False + is_Float = False + is_Rational = False + is_Integer = False + is_NumberSymbol = False + is_Order = False + is_Derivative = False + is_Piecewise = False + is_Poly = False + is_AlgebraicNumber = False + is_Relational = False + is_Equality = False + is_Boolean = False + is_Not = False + is_Matrix = False + is_Vector = False + is_Point = False + is_MatAdd = False + is_MatMul = False + + default_assumptions: ClassVar[StdFactKB] + + is_composite: bool | None + is_noninteger: bool | None + is_extended_positive: bool | None + is_negative: bool | None + is_complex: bool | None + is_extended_nonpositive: bool | None + is_integer: bool | None + is_positive: bool | None + is_rational: bool | None + is_extended_nonnegative: bool | None + is_infinite: bool | None + is_antihermitian: bool | None + is_extended_negative: bool | None + is_extended_real: bool | None + is_finite: bool | None + is_polar: bool | None + is_imaginary: bool | None + is_transcendental: bool | None + is_extended_nonzero: bool | None + is_nonzero: bool | None + is_odd: bool | None + is_algebraic: bool | None + is_prime: bool | None + is_commutative: bool | None + is_nonnegative: bool | None + is_nonpositive: bool | None + is_hermitian: bool | None + is_irrational: bool | None + is_real: bool | None + is_zero: bool | None + is_even: bool | None + + kind: Kind = UndefinedKind + + def __new__(cls, *args): + obj = object.__new__(cls) + obj._assumptions = cls.default_assumptions + obj._mhash = None # will be set by __hash__ method. + + obj._args = args # all items in args must be Basic objects + return obj + + def copy(self): + return self.func(*self.args) + + def __getnewargs__(self): + return self.args + + def __getstate__(self): + return None + + def __setstate__(self, state): + for name, value in state.items(): + setattr(self, name, value) + + def __reduce_ex__(self, protocol): + if protocol < 2: + msg = "Only pickle protocol 2 or higher is supported by SymPy" + raise NotImplementedError(msg) + return super().__reduce_ex__(protocol) + + def __hash__(self) -> int: + # hash cannot be cached using cache_it because infinite recurrence + # occurs as hash is needed for setting cache dictionary keys + h = self._mhash + if h is None: + h = hash((type(self).__name__,) + self._hashable_content()) + self._mhash = h + return h + + def _hashable_content(self): + """Return a tuple of information about self that can be used to + compute the hash. If a class defines additional attributes, + like ``name`` in Symbol, then this method should be updated + accordingly to return such relevant attributes. + + Defining more than _hashable_content is necessary if __eq__ has + been defined by a class. See note about this in Basic.__eq__.""" + return self._args + + @property + def assumptions0(self): + """ + Return object `type` assumptions. + + For example: + + Symbol('x', real=True) + Symbol('x', integer=True) + + are different objects. In other words, besides Python type (Symbol in + this case), the initial assumptions are also forming their typeinfo. + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.abc import x + >>> x.assumptions0 + {'commutative': True} + >>> x = Symbol("x", positive=True) + >>> x.assumptions0 + {'commutative': True, 'complex': True, 'extended_negative': False, + 'extended_nonnegative': True, 'extended_nonpositive': False, + 'extended_nonzero': True, 'extended_positive': True, 'extended_real': + True, 'finite': True, 'hermitian': True, 'imaginary': False, + 'infinite': False, 'negative': False, 'nonnegative': True, + 'nonpositive': False, 'nonzero': True, 'positive': True, 'real': + True, 'zero': False} + """ + return {} + + def compare(self, other): + """ + Return -1, 0, 1 if the object is less than, equal, + or greater than other in a canonical sense. + Non-Basic are always greater than Basic. + If both names of the classes being compared appear + in the `ordering_of_classes` then the ordering will + depend on the appearance of the names there. + If either does not appear in that list, then the + comparison is based on the class name. + If the names are the same then a comparison is made + on the length of the hashable content. + Items of the equal-lengthed contents are then + successively compared using the same rules. If there + is never a difference then 0 is returned. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> x.compare(y) + -1 + >>> x.compare(x) + 0 + >>> y.compare(x) + 1 + + """ + # all redefinitions of __cmp__ method should start with the + # following lines: + if self is other: + return 0 + n1 = self.__class__ + n2 = other.__class__ + c = _cmp_name(n1, n2) + if c: + return c + # + st = self._hashable_content() + ot = other._hashable_content() + len_st = len(st) + len_ot = len(ot) + c = (len_st > len_ot) - (len_st < len_ot) + if c: + return c + for l, r in zip(st, ot): + if isinstance(l, Basic): + c = l.compare(r) + elif isinstance(l, frozenset): + l = Basic(*l) if isinstance(l, frozenset) else l + r = Basic(*r) if isinstance(r, frozenset) else r + c = l.compare(r) + else: + c = (l > r) - (l < r) + if c: + return c + return 0 + + @classmethod + def fromiter(cls, args, **assumptions): + """ + Create a new object from an iterable. + + This is a convenience function that allows one to create objects from + any iterable, without having to convert to a list or tuple first. + + Examples + ======== + + >>> from sympy import Tuple + >>> Tuple.fromiter(i for i in range(5)) + (0, 1, 2, 3, 4) + + """ + return cls(*tuple(args), **assumptions) + + @classmethod + def class_key(cls) -> tuple[int, int, str]: + """Nice order of classes.""" + return 5, 0, cls.__name__ + + @cacheit + def sort_key(self, order=None): + """ + Return a sort key. + + Examples + ======== + + >>> from sympy import S, I + + >>> sorted([S(1)/2, I, -I], key=lambda x: x.sort_key()) + [1/2, -I, I] + + >>> S("[x, 1/x, 1/x**2, x**2, x**(1/2), x**(1/4), x**(3/2)]") + [x, 1/x, x**(-2), x**2, sqrt(x), x**(1/4), x**(3/2)] + >>> sorted(_, key=lambda x: x.sort_key()) + [x**(-2), 1/x, x**(1/4), sqrt(x), x, x**(3/2), x**2] + + """ + + # XXX: remove this when issue 5169 is fixed + def inner_key(arg): + if isinstance(arg, Basic): + return arg.sort_key(order) + else: + return arg + + args = self._sorted_args + args = len(args), tuple([inner_key(arg) for arg in args]) + return self.class_key(), args, S.One.sort_key(), S.One + + def _do_eq_sympify(self, other): + """Returns a boolean indicating whether a == b when either a + or b is not a Basic. This is only done for types that were either + added to `converter` by a 3rd party or when the object has `_sympy_` + defined. This essentially reuses the code in `_sympify` that is + specific for this use case. Non-user defined types that are meant + to work with SymPy should be handled directly in the __eq__ methods + of the `Basic` classes it could equate to and not be converted. Note + that after conversion, `==` is used again since it is not + necessarily clear whether `self` or `other`'s __eq__ method needs + to be used.""" + for superclass in type(other).__mro__: + conv = _external_converter.get(superclass) + if conv is not None: + return self == conv(other) + if hasattr(other, '_sympy_'): + return self == other._sympy_() + return NotImplemented + + def __eq__(self, other): + """Return a boolean indicating whether a == b on the basis of + their symbolic trees. + + This is the same as a.compare(b) == 0 but faster. + + Notes + ===== + + If a class that overrides __eq__() needs to retain the + implementation of __hash__() from a parent class, the + interpreter must be told this explicitly by setting + __hash__ : Callable[[object], int] = .__hash__. + Otherwise the inheritance of __hash__() will be blocked, + just as if __hash__ had been explicitly set to None. + + References + ========== + + from https://docs.python.org/dev/reference/datamodel.html#object.__hash__ + """ + if self is other: + return True + + if not isinstance(other, Basic): + return self._do_eq_sympify(other) + + # check for pure number expr + if not (self.is_Number and other.is_Number) and ( + type(self) != type(other)): + return False + a, b = self._hashable_content(), other._hashable_content() + if a != b: + return False + # check number *in* an expression + for a, b in zip(a, b): + if not isinstance(a, Basic): + continue + if a.is_Number and type(a) != type(b): + return False + return True + + def __ne__(self, other): + """``a != b`` -> Compare two symbolic trees and see whether they are different + + this is the same as: + + ``a.compare(b) != 0`` + + but faster + """ + return not self == other + + def dummy_eq(self, other, symbol=None): + """ + Compare two expressions and handle dummy symbols. + + Examples + ======== + + >>> from sympy import Dummy + >>> from sympy.abc import x, y + + >>> u = Dummy('u') + + >>> (u**2 + 1).dummy_eq(x**2 + 1) + True + >>> (u**2 + 1) == (x**2 + 1) + False + + >>> (u**2 + y).dummy_eq(x**2 + y, x) + True + >>> (u**2 + y).dummy_eq(x**2 + y, y) + False + + """ + s = self.as_dummy() + o = _sympify(other) + o = o.as_dummy() + + dummy_symbols = [i for i in s.free_symbols if i.is_Dummy] + + if len(dummy_symbols) == 1: + dummy = dummy_symbols.pop() + else: + return s == o + + if symbol is None: + symbols = o.free_symbols + + if len(symbols) == 1: + symbol = symbols.pop() + else: + return s == o + + tmp = dummy.__class__() + + return s.xreplace({dummy: tmp}) == o.xreplace({symbol: tmp}) + + @overload + def atoms(self) -> set[Basic]: ... + @overload + def atoms(self, *types: Tbasic | type[Tbasic]) -> set[Tbasic]: ... + + def atoms(self, *types: Tbasic | type[Tbasic]) -> set[Basic] | set[Tbasic]: + """Returns the atoms that form the current object. + + By default, only objects that are truly atomic and cannot + be divided into smaller pieces are returned: symbols, numbers, + and number symbols like I and pi. It is possible to request + atoms of any type, however, as demonstrated below. + + Examples + ======== + + >>> from sympy import I, pi, sin + >>> from sympy.abc import x, y + >>> (1 + x + 2*sin(y + I*pi)).atoms() + {1, 2, I, pi, x, y} + + If one or more types are given, the results will contain only + those types of atoms. + + >>> from sympy import Number, NumberSymbol, Symbol + >>> (1 + x + 2*sin(y + I*pi)).atoms(Symbol) + {x, y} + + >>> (1 + x + 2*sin(y + I*pi)).atoms(Number) + {1, 2} + + >>> (1 + x + 2*sin(y + I*pi)).atoms(Number, NumberSymbol) + {1, 2, pi} + + >>> (1 + x + 2*sin(y + I*pi)).atoms(Number, NumberSymbol, I) + {1, 2, I, pi} + + Note that I (imaginary unit) and zoo (complex infinity) are special + types of number symbols and are not part of the NumberSymbol class. + + The type can be given implicitly, too: + + >>> (1 + x + 2*sin(y + I*pi)).atoms(x) # x is a Symbol + {x, y} + + Be careful to check your assumptions when using the implicit option + since ``S(1).is_Integer = True`` but ``type(S(1))`` is ``One``, a special type + of SymPy atom, while ``type(S(2))`` is type ``Integer`` and will find all + integers in an expression: + + >>> from sympy import S + >>> (1 + x + 2*sin(y + I*pi)).atoms(S(1)) + {1} + + >>> (1 + x + 2*sin(y + I*pi)).atoms(S(2)) + {1, 2} + + Finally, arguments to atoms() can select more than atomic atoms: any + SymPy type (loaded in core/__init__.py) can be listed as an argument + and those types of "atoms" as found in scanning the arguments of the + expression recursively: + + >>> from sympy import Function, Mul + >>> from sympy.core.function import AppliedUndef + >>> f = Function('f') + >>> (1 + f(x) + 2*sin(y + I*pi)).atoms(Function) + {f(x), sin(y + I*pi)} + >>> (1 + f(x) + 2*sin(y + I*pi)).atoms(AppliedUndef) + {f(x)} + + >>> (1 + x + 2*sin(y + I*pi)).atoms(Mul) + {I*pi, 2*sin(y + I*pi)} + + """ + nodes = _preorder_traversal(self) + if types: + types2 = tuple([t if isinstance(t, type) else type(t) for t in types]) + return {node for node in nodes if isinstance(node, types2)} + else: + return {node for node in nodes if not node.args} + + @property + def free_symbols(self) -> set[Basic]: + """Return from the atoms of self those which are free symbols. + + Not all free symbols are ``Symbol`` (see examples) + + For most expressions, all symbols are free symbols. For some classes + this is not true. e.g. Integrals use Symbols for the dummy variables + which are bound variables, so Integral has a method to return all + symbols except those. Derivative keeps track of symbols with respect + to which it will perform a derivative; those are + bound variables, too, so it has its own free_symbols method. + + Any other method that uses bound variables should implement a + free_symbols method. + + Examples + ======== + + >>> from sympy import Derivative, Integral, IndexedBase + >>> from sympy.abc import x, y, n + >>> (x + 1).free_symbols + {x} + >>> Integral(x, y).free_symbols + {x, y} + + Not all free symbols are actually symbols: + + >>> IndexedBase('F')[0].free_symbols + {F, F[0]} + + The symbols of differentiation are not included unless they + appear in the expression being differentiated. + + >>> Derivative(x + y, y).free_symbols + {x, y} + >>> Derivative(x, y).free_symbols + {x} + >>> Derivative(x, (y, n)).free_symbols + {n, x} + + If you want to know if a symbol is in the variables of the + Derivative you can do so as follows: + + >>> Derivative(x, y).has_free(y) + True + """ + empty: set[Basic] = set() + return empty.union(*(a.free_symbols for a in self.args)) + + @property + def expr_free_symbols(self): + sympy_deprecation_warning(""" + The expr_free_symbols property is deprecated. Use free_symbols to get + the free symbols of an expression. + """, + deprecated_since_version="1.9", + active_deprecations_target="deprecated-expr-free-symbols") + return set() + + def as_dummy(self) -> "Self": + """Return the expression with any objects having structurally + bound symbols replaced with unique, canonical symbols within + the object in which they appear and having only the default + assumption for commutativity being True. When applied to a + symbol a new symbol having only the same commutativity will be + returned. + + Examples + ======== + + >>> from sympy import Integral, Symbol + >>> from sympy.abc import x + >>> r = Symbol('r', real=True) + >>> Integral(r, (r, x)).as_dummy() + Integral(_0, (_0, x)) + >>> _.variables[0].is_real is None + True + >>> r.as_dummy() + _r + + Notes + ===== + + Any object that has structurally bound variables should have + a property, ``bound_symbols`` that returns those symbols + appearing in the object. + """ + from .symbol import Dummy, Symbol + def can(x): + # mask free that shadow bound + free = x.free_symbols + bound = set(x.bound_symbols) + d = {i: Dummy() for i in bound & free} + x = x.subs(d) + # replace bound with canonical names + x = x.xreplace(x.canonical_variables) + # return after undoing masking + return x.xreplace({v: k for k, v in d.items()}) + if not self.has(Symbol): + return self + return self.replace( + lambda x: hasattr(x, 'bound_symbols'), + can, + simultaneous=False) # type:ignore + + @property + def canonical_variables(self) -> dict[Basic, Symbol]: + """Return a dictionary mapping any variable defined in + ``self.bound_symbols`` to Symbols that do not clash + with any free symbols in the expression. + + Examples + ======== + + >>> from sympy import Lambda + >>> from sympy.abc import x + >>> Lambda(x, 2*x).canonical_variables + {x: _0} + """ + bound: list[Basic] | None = getattr(self, 'bound_symbols', None) + if bound is None: + return {} + dums = numbered_symbols('_') + reps = {} + # watch out for free symbol that are not in bound symbols; + # those that are in bound symbols are about to get changed + + # XXX: free_symbols only returns particular kinds of expressions that + # generally have a .name attribute. There is not a proper class/type + # that represents this. + names = {i.name for i in self.free_symbols - set(bound)} # type: ignore + for b in bound: + d = next(dums) + if b.is_Symbol: + while d.name in names: + d = next(dums) + reps[b] = d + return reps + + def rcall(self, *args): + """Apply on the argument recursively through the expression tree. + + This method is used to simulate a common abuse of notation for + operators. For instance, in SymPy the following will not work: + + ``(x+Lambda(y, 2*y))(z) == x+2*z``, + + however, you can use: + + >>> from sympy import Lambda + >>> from sympy.abc import x, y, z + >>> (x + Lambda(y, 2*y)).rcall(z) + x + 2*z + """ + if callable(self): + return self(*args) + elif self.args: + newargs = [sub.rcall(*args) for sub in self.args] + return self.func(*newargs) + else: + return self + + def is_hypergeometric(self, k): + from sympy.simplify.simplify import hypersimp + from sympy.functions.elementary.piecewise import Piecewise + if self.has(Piecewise): + return None + return hypersimp(self, k) is not None + + @property + def is_comparable(self): + """Return True if self can be computed to a real number + (or already is a real number) with precision, else False. + + Examples + ======== + + >>> from sympy import exp_polar, pi, I + >>> (I*exp_polar(I*pi/2)).is_comparable + True + >>> (I*exp_polar(I*pi*2)).is_comparable + False + + A False result does not mean that `self` cannot be rewritten + into a form that would be comparable. For example, the + difference computed below is zero but without simplification + it does not evaluate to a zero with precision: + + >>> e = 2**pi*(1 + 2**pi) + >>> dif = e - e.expand() + >>> dif.is_comparable + False + >>> dif.n(2)._prec + 1 + + """ + return self._eval_is_comparable() + + def _eval_is_comparable(self) -> bool: + # Expr.is_comparable overrides this + return False + + @property + def func(self): + """ + The top-level function in an expression. + + The following should hold for all objects:: + + >> x == x.func(*x.args) + + Examples + ======== + + >>> from sympy.abc import x + >>> a = 2*x + >>> a.func + + >>> a.args + (2, x) + >>> a.func(*a.args) + 2*x + >>> a == a.func(*a.args) + True + + """ + return self.__class__ + + @property + def args(self) -> tuple[Basic, ...]: + """Returns a tuple of arguments of 'self'. + + Examples + ======== + + >>> from sympy import cot + >>> from sympy.abc import x, y + + >>> cot(x).args + (x,) + + >>> cot(x).args[0] + x + + >>> (x*y).args + (x, y) + + >>> (x*y).args[1] + y + + Notes + ===== + + Never use self._args, always use self.args. + Only use _args in __new__ when creating a new function. + Do not override .args() from Basic (so that it is easy to + change the interface in the future if needed). + """ + return self._args + + @property + def _sorted_args(self): + """ + The same as ``args``. Derived classes which do not fix an + order on their arguments should override this method to + produce the sorted representation. + """ + return self.args + + def as_content_primitive(self, radical=False, clear=True): + """A stub to allow Basic args (like Tuple) to be skipped when computing + the content and primitive components of an expression. + + See Also + ======== + + sympy.core.expr.Expr.as_content_primitive + """ + return S.One, self + + @overload + def subs(self, arg1: Mapping[Basic | complex, Basic | complex], arg2: None=None, **kwargs: Any) -> Basic: ... + @overload + def subs(self, arg1: Iterable[tuple[Basic | complex, Basic | complex]], arg2: None=None, **kwargs: Any) -> Basic: ... + @overload + def subs(self, arg1: Basic | complex, arg2: Basic | complex, **kwargs: Any) -> Basic: ... + + def subs(self, arg1: Mapping[Basic | complex, Basic | complex] + | Iterable[tuple[Basic | complex, Basic | complex]] | Basic | complex, + arg2: Basic | complex | None = None, **kwargs: Any) -> Basic: + """ + Substitutes old for new in an expression after sympifying args. + + `args` is either: + - two arguments, e.g. foo.subs(old, new) + - one iterable argument, e.g. foo.subs(iterable). The iterable may be + o an iterable container with (old, new) pairs. In this case the + replacements are processed in the order given with successive + patterns possibly affecting replacements already made. + o a dict or set whose key/value items correspond to old/new pairs. + In this case the old/new pairs will be sorted by op count and in + case of a tie, by number of args and the default_sort_key. The + resulting sorted list is then processed as an iterable container + (see previous). + + If the keyword ``simultaneous`` is True, the subexpressions will not be + evaluated until all the substitutions have been made. + + Examples + ======== + + >>> from sympy import pi, exp, limit, oo + >>> from sympy.abc import x, y + >>> (1 + x*y).subs(x, pi) + pi*y + 1 + >>> (1 + x*y).subs({x:pi, y:2}) + 1 + 2*pi + >>> (1 + x*y).subs([(x, pi), (y, 2)]) + 1 + 2*pi + >>> reps = [(y, x**2), (x, 2)] + >>> (x + y).subs(reps) + 6 + >>> (x + y).subs(reversed(reps)) + x**2 + 2 + + >>> (x**2 + x**4).subs(x**2, y) + y**2 + y + + To replace only the x**2 but not the x**4, use xreplace: + + >>> (x**2 + x**4).xreplace({x**2: y}) + x**4 + y + + To delay evaluation until all substitutions have been made, + set the keyword ``simultaneous`` to True: + + >>> (x/y).subs([(x, 0), (y, 0)]) + 0 + >>> (x/y).subs([(x, 0), (y, 0)], simultaneous=True) + nan + + This has the added feature of not allowing subsequent substitutions + to affect those already made: + + >>> ((x + y)/y).subs({x + y: y, y: x + y}) + 1 + >>> ((x + y)/y).subs({x + y: y, y: x + y}, simultaneous=True) + y/(x + y) + + In order to obtain a canonical result, unordered iterables are + sorted by count_op length, number of arguments and by the + default_sort_key to break any ties. All other iterables are left + unsorted. + + >>> from sympy import sqrt, sin, cos + >>> from sympy.abc import a, b, c, d, e + + >>> A = (sqrt(sin(2*x)), a) + >>> B = (sin(2*x), b) + >>> C = (cos(2*x), c) + >>> D = (x, d) + >>> E = (exp(x), e) + + >>> expr = sqrt(sin(2*x))*sin(exp(x)*x)*cos(2*x) + sin(2*x) + + >>> expr.subs(dict([A, B, C, D, E])) + a*c*sin(d*e) + b + + The resulting expression represents a literal replacement of the + old arguments with the new arguments. This may not reflect the + limiting behavior of the expression: + + >>> (x**3 - 3*x).subs({x: oo}) + nan + + >>> limit(x**3 - 3*x, x, oo) + oo + + If the substitution will be followed by numerical + evaluation, it is better to pass the substitution to + evalf as + + >>> (1/x).evalf(subs={x: 3.0}, n=21) + 0.333333333333333333333 + + rather than + + >>> (1/x).subs({x: 3.0}).evalf(21) + 0.333333333333333314830 + + as the former will ensure that the desired level of precision is + obtained. + + See Also + ======== + replace: replacement capable of doing wildcard-like matching, + parsing of match, and conditional replacements + xreplace: exact node replacement in expr tree; also capable of + using matching rules + sympy.core.evalf.EvalfMixin.evalf: calculates the given formula to a desired level of precision + + """ + from .containers import Dict + from .symbol import Dummy, Symbol + from .numbers import _illegal + + items: Iterable[tuple[Basic | complex, Basic | complex]] + + unordered = False + if arg2 is None: + + if isinstance(arg1, set): + items = arg1 + unordered = True + elif isinstance(arg1, (Dict, Mapping)): + unordered = True + items = arg1.items() # type: ignore + elif not iterable(arg1): + raise ValueError(filldedent(""" + When a single argument is passed to subs + it should be a dictionary of old: new pairs or an iterable + of (old, new) tuples.""")) + else: + items = arg1 # type: ignore + else: + items = [(arg1, arg2)] # type: ignore + + def sympify_old(old) -> Basic: + if isinstance(old, str): + # Use Symbol rather than parse_expr for old + return Symbol(old) + elif isinstance(old, type): + # Allow a type e.g. Function('f') or sin + return sympify(old, strict=False) + else: + return sympify(old, strict=True) + + def sympify_new(new) -> Basic: + if isinstance(new, (str, type)): + # Allow a type or parse a string input + return sympify(new, strict=False) + else: + return sympify(new, strict=True) + + sequence = [(sympify_old(s1), sympify_new(s2)) for s1, s2 in items] + + # skip if there is no change + sequence = [(s1, s2) for s1, s2 in sequence if not _aresame(s1, s2)] + + simultaneous = kwargs.pop('simultaneous', False) + + if unordered: + from .sorting import _nodes, default_sort_key + sequence_dict = dict(sequence) + # order so more complex items are first and items + # of identical complexity are ordered so + # f(x) < f(y) < x < y + # \___ 2 __/ \_1_/ <- number of nodes + # + # For more complex ordering use an unordered sequence. + k = list(ordered(sequence_dict, default=False, keys=( + lambda x: -_nodes(x), + default_sort_key, + ))) + sequence = [(k, sequence_dict[k]) for k in k] + # do infinities first + if not simultaneous: + redo = [i for i, seq in enumerate(sequence) if seq[1] in _illegal] + for i in reversed(redo): + sequence.insert(0, sequence.pop(i)) + + if simultaneous: # XXX should this be the default for dict subs? + reps = {} + rv = self + kwargs['hack2'] = True + m = Dummy('subs_m') + for old, new in sequence: + com = new.is_commutative + if com is None: + com = True + d = Dummy('subs_d', commutative=com) + # using d*m so Subs will be used on dummy variables + # in things like Derivative(f(x, y), x) in which x + # is both free and bound + rv = rv._subs(old, d*m, **kwargs) + if not isinstance(rv, Basic): + break + reps[d] = new + reps[m] = S.One # get rid of m + return rv.xreplace(reps) + else: + rv = self + for old, new in sequence: + rv = rv._subs(old, new, **kwargs) + if not isinstance(rv, Basic): + break + return rv + + @cacheit + def _subs(self, old, new, **hints): + """Substitutes an expression old -> new. + + If self is not equal to old then _eval_subs is called. + If _eval_subs does not want to make any special replacement + then a None is received which indicates that the fallback + should be applied wherein a search for replacements is made + amongst the arguments of self. + + >>> from sympy import Add + >>> from sympy.abc import x, y, z + + Examples + ======== + + Add's _eval_subs knows how to target x + y in the following + so it makes the change: + + >>> (x + y + z).subs(x + y, 1) + z + 1 + + Add's _eval_subs does not need to know how to find x + y in + the following: + + >>> Add._eval_subs(z*(x + y) + 3, x + y, 1) is None + True + + The returned None will cause the fallback routine to traverse the args and + pass the z*(x + y) arg to Mul where the change will take place and the + substitution will succeed: + + >>> (z*(x + y) + 3).subs(x + y, 1) + z + 3 + + ** Developers Notes ** + + An _eval_subs routine for a class should be written if: + + 1) any arguments are not instances of Basic (e.g. bool, tuple); + + 2) some arguments should not be targeted (as in integration + variables); + + 3) if there is something other than a literal replacement + that should be attempted (as in Piecewise where the condition + may be updated without doing a replacement). + + If it is overridden, here are some special cases that might arise: + + 1) If it turns out that no special change was made and all + the original sub-arguments should be checked for + replacements then None should be returned. + + 2) If it is necessary to do substitutions on a portion of + the expression then _subs should be called. _subs will + handle the case of any sub-expression being equal to old + (which usually would not be the case) while its fallback + will handle the recursion into the sub-arguments. For + example, after Add's _eval_subs removes some matching terms + it must process the remaining terms so it calls _subs + on each of the un-matched terms and then adds them + onto the terms previously obtained. + + 3) If the initial expression should remain unchanged then + the original expression should be returned. (Whenever an + expression is returned, modified or not, no further + substitution of old -> new is attempted.) Sum's _eval_subs + routine uses this strategy when a substitution is attempted + on any of its summation variables. + """ + + def fallback(self, old, new): + """ + Try to replace old with new in any of self's arguments. + """ + hit = False + args = list(self.args) + for i, arg in enumerate(args): + if not hasattr(arg, '_eval_subs'): + continue + arg = arg._subs(old, new, **hints) + if not _aresame(arg, args[i]): + hit = True + args[i] = arg + if hit: + rv = self.func(*args) + hack2 = hints.get('hack2', False) + if hack2 and self.is_Mul and not rv.is_Mul: # 2-arg hack + coeff = S.One + nonnumber = [] + for i in args: + if i.is_Number: + coeff *= i + else: + nonnumber.append(i) + nonnumber = self.func(*nonnumber) + if coeff is S.One: + return nonnumber + else: + return self.func(coeff, nonnumber, evaluate=False) + return rv + return self + + if _aresame(self, old): + return new + + rv = self._eval_subs(old, new) + if rv is None: + rv = fallback(self, old, new) + return rv + + def _eval_subs(self, old, new) -> Basic | None: + """Override this stub if you want to do anything more than + attempt a replacement of old with new in the arguments of self. + + See also + ======== + + _subs + """ + return None + + def xreplace(self, rule): + """ + Replace occurrences of objects within the expression. + + Parameters + ========== + + rule : dict-like + Expresses a replacement rule + + Returns + ======= + + xreplace : the result of the replacement + + Examples + ======== + + >>> from sympy import symbols, pi, exp + >>> x, y, z = symbols('x y z') + >>> (1 + x*y).xreplace({x: pi}) + pi*y + 1 + >>> (1 + x*y).xreplace({x: pi, y: 2}) + 1 + 2*pi + + Replacements occur only if an entire node in the expression tree is + matched: + + >>> (x*y + z).xreplace({x*y: pi}) + z + pi + >>> (x*y*z).xreplace({x*y: pi}) + x*y*z + >>> (2*x).xreplace({2*x: y, x: z}) + y + >>> (2*2*x).xreplace({2*x: y, x: z}) + 4*z + >>> (x + y + 2).xreplace({x + y: 2}) + x + y + 2 + >>> (x + 2 + exp(x + 2)).xreplace({x + 2: y}) + x + exp(y) + 2 + + xreplace does not differentiate between free and bound symbols. In the + following, subs(x, y) would not change x since it is a bound symbol, + but xreplace does: + + >>> from sympy import Integral + >>> Integral(x, (x, 1, 2*x)).xreplace({x: y}) + Integral(y, (y, 1, 2*y)) + + Trying to replace x with an expression raises an error: + + >>> Integral(x, (x, 1, 2*x)).xreplace({x: 2*y}) # doctest: +SKIP + ValueError: Invalid limits given: ((2*y, 1, 4*y),) + + See Also + ======== + replace: replacement capable of doing wildcard-like matching, + parsing of match, and conditional replacements + subs: substitution of subexpressions as defined by the objects + themselves. + + """ + value, _ = self._xreplace(rule) + return value + + def _xreplace(self, rule): + """ + Helper for xreplace. Tracks whether a replacement actually occurred. + """ + if self in rule: + return rule[self], True + elif rule: + args = [] + changed = False + for a in self.args: + _xreplace = getattr(a, '_xreplace', None) + if _xreplace is not None: + a_xr = _xreplace(rule) + args.append(a_xr[0]) + changed |= a_xr[1] + else: + args.append(a) + args = tuple(args) + if changed: + return self.func(*args), True + return self, False + + @cacheit + def has(self, *patterns): + """ + Test whether any subexpression matches any of the patterns. + + Examples + ======== + + >>> from sympy import sin + >>> from sympy.abc import x, y, z + >>> (x**2 + sin(x*y)).has(z) + False + >>> (x**2 + sin(x*y)).has(x, y, z) + True + >>> x.has(x) + True + + Note ``has`` is a structural algorithm with no knowledge of + mathematics. Consider the following half-open interval: + + >>> from sympy import Interval + >>> i = Interval.Lopen(0, 5); i + Interval.Lopen(0, 5) + >>> i.args + (0, 5, True, False) + >>> i.has(4) # there is no "4" in the arguments + False + >>> i.has(0) # there *is* a "0" in the arguments + True + + Instead, use ``contains`` to determine whether a number is in the + interval or not: + + >>> i.contains(4) + True + >>> i.contains(0) + False + + + Note that ``expr.has(*patterns)`` is exactly equivalent to + ``any(expr.has(p) for p in patterns)``. In particular, ``False`` is + returned when the list of patterns is empty. + + >>> x.has() + False + + """ + return self._has(iterargs, *patterns) + + def has_xfree(self, s: set[Basic]): + """Return True if self has any of the patterns in s as a + free argument, else False. This is like `Basic.has_free` + but this will only report exact argument matches. + + Examples + ======== + + >>> from sympy import Function + >>> from sympy.abc import x, y + >>> f = Function('f') + >>> f(x).has_xfree({f}) + False + >>> f(x).has_xfree({f(x)}) + True + >>> f(x + 1).has_xfree({x}) + True + >>> f(x + 1).has_xfree({x + 1}) + True + >>> f(x + y + 1).has_xfree({x + 1}) + False + """ + # protect O(1) containment check by requiring: + if type(s) is not set: + raise TypeError('expecting set argument') + return any(a in s for a in iterfreeargs(self)) + + @cacheit + def has_free(self, *patterns): + """Return True if self has object(s) ``x`` as a free expression + else False. + + Examples + ======== + + >>> from sympy import Integral, Function + >>> from sympy.abc import x, y + >>> f = Function('f') + >>> g = Function('g') + >>> expr = Integral(f(x), (f(x), 1, g(y))) + >>> expr.free_symbols + {y} + >>> expr.has_free(g(y)) + True + >>> expr.has_free(*(x, f(x))) + False + + This works for subexpressions and types, too: + + >>> expr.has_free(g) + True + >>> (x + y + 1).has_free(y + 1) + True + """ + if not patterns: + return False + p0 = patterns[0] + if len(patterns) == 1 and iterable(p0) and not isinstance(p0, Basic): + # Basic can contain iterables (though not non-Basic, ideally) + # but don't encourage mixed passing patterns + raise TypeError(filldedent(''' + Expecting 1 or more Basic args, not a single + non-Basic iterable. Don't forget to unpack + iterables: `eq.has_free(*patterns)`''')) + # try quick test first + s = set(patterns) + rv = self.has_xfree(s) + if rv: + return rv + # now try matching through slower _has + return self._has(iterfreeargs, *patterns) + + def _has(self, iterargs, *patterns): + # separate out types and unhashable objects + type_set = set() # only types + p_set = set() # hashable non-types + for p in patterns: + if isinstance(p, type) and issubclass(p, Basic): + type_set.add(p) + continue + if not isinstance(p, Basic): + try: + p = _sympify(p) + except SympifyError: + continue # Basic won't have this in it + p_set.add(p) # fails if object defines __eq__ but + # doesn't define __hash__ + types = tuple(type_set) # + for i in iterargs(self): # + if i in p_set: # <--- here, too + return True + if isinstance(i, types): + return True + + # use matcher if defined, e.g. operations defines + # matcher that checks for exact subset containment, + # (x + y + 1).has(x + 1) -> True + for i in p_set - type_set: # types don't have matchers + if not hasattr(i, '_has_matcher'): + continue + match = i._has_matcher() + if any(match(arg) for arg in iterargs(self)): + return True + + # no success + return False + + def replace(self, query, value, map=False, simultaneous=True, exact=None) -> Basic: + """ + Replace matching subexpressions of ``self`` with ``value``. + + If ``map = True`` then also return the mapping {old: new} where ``old`` + was a sub-expression found with query and ``new`` is the replacement + value for it. If the expression itself does not match the query, then + the returned value will be ``self.xreplace(map)`` otherwise it should + be ``self.subs(ordered(map.items()))``. + + Traverses an expression tree and performs replacement of matching + subexpressions from the bottom to the top of the tree. The default + approach is to do the replacement in a simultaneous fashion so + changes made are targeted only once. If this is not desired or causes + problems, ``simultaneous`` can be set to False. + + In addition, if an expression containing more than one Wild symbol + is being used to match subexpressions and the ``exact`` flag is None + it will be set to True so the match will only succeed if all non-zero + values are received for each Wild that appears in the match pattern. + Setting this to False accepts a match of 0; while setting it True + accepts all matches that have a 0 in them. See example below for + cautions. + + The list of possible combinations of queries and replacement values + is listed below: + + Examples + ======== + + Initial setup + + >>> from sympy import log, sin, cos, tan, Wild, Mul, Add + >>> from sympy.abc import x, y + >>> f = log(sin(x)) + tan(sin(x**2)) + + 1.1. type -> type + obj.replace(type, newtype) + + When object of type ``type`` is found, replace it with the + result of passing its argument(s) to ``newtype``. + + >>> f.replace(sin, cos) + log(cos(x)) + tan(cos(x**2)) + >>> sin(x).replace(sin, cos, map=True) + (cos(x), {sin(x): cos(x)}) + >>> (x*y).replace(Mul, Add) + x + y + + 1.2. type -> func + obj.replace(type, func) + + When object of type ``type`` is found, apply ``func`` to its + argument(s). ``func`` must be written to handle the number + of arguments of ``type``. + + >>> f.replace(sin, lambda arg: sin(2*arg)) + log(sin(2*x)) + tan(sin(2*x**2)) + >>> (x*y).replace(Mul, lambda *args: sin(2*Mul(*args))) + sin(2*x*y) + + 2.1. pattern -> expr + obj.replace(pattern(wild), expr(wild)) + + Replace subexpressions matching ``pattern`` with the expression + written in terms of the Wild symbols in ``pattern``. + + >>> a, b = map(Wild, 'ab') + >>> f.replace(sin(a), tan(a)) + log(tan(x)) + tan(tan(x**2)) + >>> f.replace(sin(a), tan(a/2)) + log(tan(x/2)) + tan(tan(x**2/2)) + >>> f.replace(sin(a), a) + log(x) + tan(x**2) + >>> (x*y).replace(a*x, a) + y + + Matching is exact by default when more than one Wild symbol + is used: matching fails unless the match gives non-zero + values for all Wild symbols: + + >>> (2*x + y).replace(a*x + b, b - a) + y - 2 + >>> (2*x).replace(a*x + b, b - a) + 2*x + + When set to False, the results may be non-intuitive: + + >>> (2*x).replace(a*x + b, b - a, exact=False) + 2/x + + 2.2. pattern -> func + obj.replace(pattern(wild), lambda wild: expr(wild)) + + All behavior is the same as in 2.1 but now a function in terms of + pattern variables is used rather than an expression: + + >>> f.replace(sin(a), lambda a: sin(2*a)) + log(sin(2*x)) + tan(sin(2*x**2)) + + 3.1. func -> func + obj.replace(filter, func) + + Replace subexpression ``e`` with ``func(e)`` if ``filter(e)`` + is True. + + >>> g = 2*sin(x**3) + >>> g.replace(lambda expr: expr.is_Number, lambda expr: expr**2) + 4*sin(x**9) + + The expression itself is also targeted by the query but is done in + such a fashion that changes are not made twice. + + >>> e = x*(x*y + 1) + >>> e.replace(lambda x: x.is_Mul, lambda x: 2*x) + 2*x*(2*x*y + 1) + + When matching a single symbol, `exact` will default to True, but + this may or may not be the behavior that is desired: + + Here, we want `exact=False`: + + >>> from sympy import Function + >>> f = Function('f') + >>> e = f(1) + f(0) + >>> q = f(a), lambda a: f(a + 1) + >>> e.replace(*q, exact=False) + f(1) + f(2) + >>> e.replace(*q, exact=True) + f(0) + f(2) + + But here, the nature of matching makes selecting + the right setting tricky: + + >>> e = x**(1 + y) + >>> (x**(1 + y)).replace(x**(1 + a), lambda a: x**-a, exact=False) + x + >>> (x**(1 + y)).replace(x**(1 + a), lambda a: x**-a, exact=True) + x**(-x - y + 1) + >>> (x**y).replace(x**(1 + a), lambda a: x**-a, exact=False) + x + >>> (x**y).replace(x**(1 + a), lambda a: x**-a, exact=True) + x**(1 - y) + + It is probably better to use a different form of the query + that describes the target expression more precisely: + + >>> (1 + x**(1 + y)).replace( + ... lambda x: x.is_Pow and x.exp.is_Add and x.exp.args[0] == 1, + ... lambda x: x.base**(1 - (x.exp - 1))) + ... + x**(1 - y) + 1 + + See Also + ======== + + subs: substitution of subexpressions as defined by the objects + themselves. + xreplace: exact node replacement in expr tree; also capable of + using matching rules + + """ + + try: + query = _sympify(query) + except SympifyError: + pass + try: + value = _sympify(value) + except SympifyError: + pass + if isinstance(query, type): + _query = lambda expr: isinstance(expr, query) + + if isinstance(value, type): + _value = lambda expr, result: value(*expr.args) + elif callable(value): + _value = lambda expr, result: value(*expr.args) + else: + raise TypeError( + "given a type, replace() expects another " + "type or a callable") + elif isinstance(query, Basic): + _query = lambda expr: expr.match(query) + if exact is None: + from .symbol import Wild + exact = (len(query.atoms(Wild)) > 1) + + if isinstance(value, Basic): + if exact: + _value = lambda expr, result: (value.subs(result) + if all(result.values()) else expr) + else: + _value = lambda expr, result: value.subs(result) + elif callable(value): + # match dictionary keys get the trailing underscore stripped + # from them and are then passed as keywords to the callable; + # if ``exact`` is True, only accept match if there are no null + # values amongst those matched. + if exact: + _value = lambda expr, result: (value(** + {str(k)[:-1]: v for k, v in result.items()}) + if all(val for val in result.values()) else expr) + else: + _value = lambda expr, result: value(** + {str(k)[:-1]: v for k, v in result.items()}) + else: + raise TypeError( + "given an expression, replace() expects " + "another expression or a callable") + elif callable(query): + _query = query + + if callable(value): + _value = lambda expr, result: value(expr) + else: + raise TypeError( + "given a callable, replace() expects " + "another callable") + else: + raise TypeError( + "first argument to replace() must be a " + "type, an expression or a callable") + + def walk(rv, F): + """Apply ``F`` to args and then to result. + """ + args = getattr(rv, 'args', None) + if args is not None: + if args: + newargs = tuple([walk(a, F) for a in args]) + if args != newargs: + rv = rv.func(*newargs) + if simultaneous: + # if rv is something that was already + # matched (that was changed) then skip + # applying F again + for i, e in enumerate(args): + if rv == e and e != newargs[i]: + return rv + rv = F(rv) + return rv + + mapping = {} # changes that took place + + def rec_replace(expr): + result = _query(expr) + if result or result == {}: + v = _value(expr, result) + if v is not None and v != expr: + if map: + mapping[expr] = v + expr = v + return expr + + rv = walk(self, rec_replace) + return (rv, mapping) if map else rv # type: ignore + + def find(self, query, group=False): + """Find all subexpressions matching a query.""" + query = _make_find_query(query) + results = list(filter(query, _preorder_traversal(self))) + + if not group: + return set(results) + return dict(Counter(results)) + + def count(self, query): + """Count the number of matching subexpressions.""" + query = _make_find_query(query) + return sum(bool(query(sub)) for sub in _preorder_traversal(self)) + + def matches(self, expr, repl_dict=None, old=False): + """ + Helper method for match() that looks for a match between Wild symbols + in self and expressions in expr. + + Examples + ======== + + >>> from sympy import symbols, Wild, Basic + >>> a, b, c = symbols('a b c') + >>> x = Wild('x') + >>> Basic(a + x, x).matches(Basic(a + b, c)) is None + True + >>> Basic(a + x, x).matches(Basic(a + b + c, b + c)) + {x_: b + c} + """ + expr = sympify(expr) + if not isinstance(expr, self.__class__): + return None + + if repl_dict is None: + repl_dict = {} + else: + repl_dict = repl_dict.copy() + + if self == expr: + return repl_dict + + if len(self.args) != len(expr.args): + return None + + d = repl_dict # already a copy + for arg, other_arg in zip(self.args, expr.args): + if arg == other_arg: + continue + if arg.is_Relational: + try: + d = arg.xreplace(d).matches(other_arg, d, old=old) + except TypeError: # Should be InvalidComparisonError when introduced + d = None + else: + d = arg.xreplace(d).matches(other_arg, d, old=old) + if d is None: + return None + return d + + def match(self, pattern, old=False): + """ + Pattern matching. + + Wild symbols match all. + + Return ``None`` when expression (self) does not match with pattern. + Otherwise return a dictionary such that:: + + pattern.xreplace(self.match(pattern)) == self + + Examples + ======== + + >>> from sympy import Wild, Sum + >>> from sympy.abc import x, y + >>> p = Wild("p") + >>> q = Wild("q") + >>> r = Wild("r") + >>> e = (x+y)**(x+y) + >>> e.match(p**p) + {p_: x + y} + >>> e.match(p**q) + {p_: x + y, q_: x + y} + >>> e = (2*x)**2 + >>> e.match(p*q**r) + {p_: 4, q_: x, r_: 2} + >>> (p*q**r).xreplace(e.match(p*q**r)) + 4*x**2 + + Since match is purely structural expressions that are equivalent up to + bound symbols will not match: + + >>> print(Sum(x, (x, 1, 2)).match(Sum(y, (y, 1, p)))) + None + + An expression with bound symbols can be matched if the pattern uses + a distinct ``Wild`` for each bound symbol: + + >>> Sum(x, (x, 1, 2)).match(Sum(q, (q, 1, p))) + {p_: 2, q_: x} + + The ``old`` flag will give the old-style pattern matching where + expressions and patterns are essentially solved to give the match. Both + of the following give None unless ``old=True``: + + >>> (x - 2).match(p - x, old=True) + {p_: 2*x - 2} + >>> (2/x).match(p*x, old=True) + {p_: 2/x**2} + + See Also + ======== + + matches: pattern.matches(expr) is the same as expr.match(pattern) + xreplace: exact structural replacement + replace: structural replacement with pattern matching + Wild: symbolic placeholders for expressions in pattern matching + """ + pattern = sympify(pattern) + return pattern.matches(self, old=old) + + def count_ops(self, visual=False): + """Wrapper for count_ops that returns the operation count.""" + from .function import count_ops + return count_ops(self, visual) + + def doit(self, **hints): + """Evaluate objects that are not evaluated by default like limits, + integrals, sums and products. All objects of this kind will be + evaluated recursively, unless some species were excluded via 'hints' + or unless the 'deep' hint was set to 'False'. + + >>> from sympy import Integral + >>> from sympy.abc import x + + >>> 2*Integral(x, x) + 2*Integral(x, x) + + >>> (2*Integral(x, x)).doit() + x**2 + + >>> (2*Integral(x, x)).doit(deep=False) + 2*Integral(x, x) + + """ + if hints.get('deep', True): + terms = [term.doit(**hints) if isinstance(term, Basic) else term + for term in self.args] + return self.func(*terms) + else: + return self + + def simplify(self, **kwargs) -> Basic: + """See the simplify function in sympy.simplify""" + from sympy.simplify.simplify import simplify + return simplify(self, **kwargs) + + def refine(self, assumption=True): + """See the refine function in sympy.assumptions""" + from sympy.assumptions.refine import refine + return refine(self, assumption) + + def _eval_derivative_n_times(self, s, n): + # This is the default evaluator for derivatives (as called by `diff` + # and `Derivative`), it will attempt a loop to derive the expression + # `n` times by calling the corresponding `_eval_derivative` method, + # while leaving the derivative unevaluated if `n` is symbolic. This + # method should be overridden if the object has a closed form for its + # symbolic n-th derivative. + from .numbers import Integer + if isinstance(n, (int, Integer)): + obj = self + for i in range(n): + prev = obj + obj = obj._eval_derivative(s) + if obj is None: + return None + elif obj == prev: + break + return obj + else: + return None + + def rewrite(self, *args, deep=True, **hints): + """ + Rewrite *self* using a defined rule. + + Rewriting transforms an expression to another, which is mathematically + equivalent but structurally different. For example you can rewrite + trigonometric functions as complex exponentials or combinatorial + functions as gamma function. + + This method takes a *pattern* and a *rule* as positional arguments. + *pattern* is optional parameter which defines the types of expressions + that will be transformed. If it is not passed, all possible expressions + will be rewritten. *rule* defines how the expression will be rewritten. + + Parameters + ========== + + args : Expr + A *rule*, or *pattern* and *rule*. + - *pattern* is a type or an iterable of types. + - *rule* can be any object. + + deep : bool, optional + If ``True``, subexpressions are recursively transformed. Default is + ``True``. + + Examples + ======== + + If *pattern* is unspecified, all possible expressions are transformed. + + >>> from sympy import cos, sin, exp, I + >>> from sympy.abc import x + >>> expr = cos(x) + I*sin(x) + >>> expr.rewrite(exp) + exp(I*x) + + Pattern can be a type or an iterable of types. + + >>> expr.rewrite(sin, exp) + exp(I*x)/2 + cos(x) - exp(-I*x)/2 + >>> expr.rewrite([cos,], exp) + exp(I*x)/2 + I*sin(x) + exp(-I*x)/2 + >>> expr.rewrite([cos, sin], exp) + exp(I*x) + + Rewriting behavior can be implemented by defining ``_eval_rewrite()`` + method. + + >>> from sympy import Expr, sqrt, pi + >>> class MySin(Expr): + ... def _eval_rewrite(self, rule, args, **hints): + ... x, = args + ... if rule == cos: + ... return cos(pi/2 - x, evaluate=False) + ... if rule == sqrt: + ... return sqrt(1 - cos(x)**2) + >>> MySin(MySin(x)).rewrite(cos) + cos(-cos(-x + pi/2) + pi/2) + >>> MySin(x).rewrite(sqrt) + sqrt(1 - cos(x)**2) + + Defining ``_eval_rewrite_as_[...]()`` method is supported for backwards + compatibility reason. This may be removed in the future and using it is + discouraged. + + >>> class MySin(Expr): + ... def _eval_rewrite_as_cos(self, *args, **hints): + ... x, = args + ... return cos(pi/2 - x, evaluate=False) + >>> MySin(x).rewrite(cos) + cos(-x + pi/2) + + """ + if not args: + return self + + hints.update(deep=deep) + + pattern = args[:-1] + rule = args[-1] + + # Special case: map `abs` to `Abs` + if rule is abs: + from sympy.functions.elementary.complexes import Abs + rule = Abs + + # support old design by _eval_rewrite_as_[...] method + if isinstance(rule, str): + method = "_eval_rewrite_as_%s" % rule + elif hasattr(rule, "__name__"): + # rule is class or function + clsname = rule.__name__ + method = "_eval_rewrite_as_%s" % clsname + else: + # rule is instance + clsname = rule.__class__.__name__ + method = "_eval_rewrite_as_%s" % clsname + + if pattern: + if iterable(pattern[0]): + pattern = pattern[0] + pattern = tuple(p for p in pattern if self.has(p)) + if not pattern: + return self + # hereafter, empty pattern is interpreted as all pattern. + + return self._rewrite(pattern, rule, method, **hints) + + def _rewrite(self, pattern, rule, method, **hints): + deep = hints.pop('deep', True) + if deep: + args = [a._rewrite(pattern, rule, method, **hints) + for a in self.args] + else: + args = self.args + if not pattern or any(isinstance(self, p) for p in pattern): + meth = getattr(self, method, None) + if meth is not None: + rewritten = meth(*args, **hints) + else: + rewritten = self._eval_rewrite(rule, args, **hints) + if rewritten is not None: + return rewritten + if not args: + return self + return self.func(*args) + + def _eval_rewrite(self, rule, args, **hints): + return None + + _constructor_postprocessor_mapping = {} # type: ignore + + @classmethod + def _exec_constructor_postprocessors(cls, obj): + # WARNING: This API is experimental. + + # This is an experimental API that introduces constructor + # postprosessors for SymPy Core elements. If an argument of a SymPy + # expression has a `_constructor_postprocessor_mapping` attribute, it will + # be interpreted as a dictionary containing lists of postprocessing + # functions for matching expression node names. + + clsname = obj.__class__.__name__ + postprocessors = {f for i in obj.args + for f in _get_postprocessors(clsname, type(i))} + for f in postprocessors: + obj = f(obj) + + return obj + + def _sage_(self): + """ + Convert *self* to a symbolic expression of SageMath. + + This version of the method is merely a placeholder. + """ + old_method = self._sage_ + from sage.interfaces.sympy import sympy_init # type: ignore + sympy_init() # may monkey-patch _sage_ method into self's class or superclasses + if old_method == self._sage_: + raise NotImplementedError('conversion to SageMath is not implemented') + else: + # call the freshly monkey-patched method + return self._sage_() + + def could_extract_minus_sign(self) -> bool: + return False # see Expr.could_extract_minus_sign + + def is_same(a, b, approx=None): + """Return True if a and b are structurally the same, else False. + If `approx` is supplied, it will be used to test whether two + numbers are the same or not. By default, only numbers of the + same type will compare equal, so S.Half != Float(0.5). + + Examples + ======== + + In SymPy (unlike Python) two numbers do not compare the same if they are + not of the same type: + + >>> from sympy import S + >>> 2.0 == S(2) + False + >>> 0.5 == S.Half + False + + By supplying a function with which to compare two numbers, such + differences can be ignored. e.g. `equal_valued` will return True + for decimal numbers having a denominator that is a power of 2, + regardless of precision. + + >>> from sympy import Float + >>> from sympy.core.numbers import equal_valued + >>> (S.Half/4).is_same(Float(0.125, 1), equal_valued) + True + >>> Float(1, 2).is_same(Float(1, 10), equal_valued) + True + + But decimals without a power of 2 denominator will compare + as not being the same. + + >>> Float(0.1, 9).is_same(Float(0.1, 10), equal_valued) + False + + But arbitrary differences can be ignored by supplying a function + to test the equivalence of two numbers: + + >>> import math + >>> Float(0.1, 9).is_same(Float(0.1, 10), math.isclose) + True + + Other objects might compare the same even though types are not the + same. This routine will only return True if two expressions are + identical in terms of class types. + + >>> from sympy import eye, Basic + >>> eye(1) == S(eye(1)) # mutable vs immutable + True + >>> Basic.is_same(eye(1), S(eye(1))) + False + + """ + from .numbers import Number + from .traversal import postorder_traversal as pot + for t in zip_longest(pot(a), pot(b)): + if None in t: + return False + a, b = t + if isinstance(a, Number): + if not isinstance(b, Number): + return False + if approx: + return approx(a, b) + if not (a == b and a.__class__ == b.__class__): + return False + return True + +_aresame = Basic.is_same # for sake of others importing this + +# key used by Mul and Add to make canonical args +_args_sortkey = cmp_to_key(Basic.compare) + +# For all Basic subclasses _prepare_class_assumptions is called by +# Basic.__init_subclass__ but that method is not called for Basic itself so we +# call the function here instead. +_prepare_class_assumptions(Basic) + + +class Atom(Basic): + """ + A parent class for atomic things. An atom is an expression with no subexpressions. + + Examples + ======== + + Symbol, Number, Rational, Integer, ... + But not: Add, Mul, Pow, ... + """ + + is_Atom = True + + __slots__ = () + + def matches(self, expr, repl_dict=None, old=False): + if self == expr: + if repl_dict is None: + return {} + return repl_dict.copy() + + def xreplace(self, rule, hack2=False): + return rule.get(self, self) + + def doit(self, **hints): + return self + + @classmethod + def class_key(cls): + return 2, 0, cls.__name__ + + @cacheit + def sort_key(self, order=None): + return self.class_key(), (1, (str(self),)), S.One.sort_key(), S.One + + def _eval_simplify(self, **kwargs): + return self + + @property + def _sorted_args(self): + # this is here as a safeguard against accidentally using _sorted_args + # on Atoms -- they cannot be rebuilt as atom.func(*atom._sorted_args) + # since there are no args. So the calling routine should be checking + # to see that this property is not called for Atoms. + raise AttributeError('Atoms have no args. It might be necessary' + ' to make a check for Atoms in the calling code.') + + +def _atomic(e, recursive=False): + """Return atom-like quantities as far as substitution is + concerned: Derivatives, Functions and Symbols. Do not + return any 'atoms' that are inside such quantities unless + they also appear outside, too, unless `recursive` is True. + + Examples + ======== + + >>> from sympy import Derivative, Function, cos + >>> from sympy.abc import x, y + >>> from sympy.core.basic import _atomic + >>> f = Function('f') + >>> _atomic(x + y) + {x, y} + >>> _atomic(x + f(y)) + {x, f(y)} + >>> _atomic(Derivative(f(x), x) + cos(x) + y) + {y, cos(x), Derivative(f(x), x)} + + """ + pot = _preorder_traversal(e) + seen = set() + if isinstance(e, Basic): + free = getattr(e, "free_symbols", None) + if free is None: + return {e} + else: + return set() + from .symbol import Symbol + from .function import Derivative, Function + atoms = set() + for p in pot: + if p in seen: + pot.skip() + continue + seen.add(p) + if isinstance(p, Symbol) and p in free: + atoms.add(p) + elif isinstance(p, (Derivative, Function)): + if not recursive: + pot.skip() + atoms.add(p) + return atoms + + +def _make_find_query(query): + """Convert the argument of Basic.find() into a callable""" + try: + query = _sympify(query) + except SympifyError: + pass + if isinstance(query, type): + return lambda expr: isinstance(expr, query) + elif isinstance(query, Basic): + return lambda expr: expr.match(query) is not None + return query + +# Delayed to avoid cyclic import +from .singleton import S +from .traversal import (preorder_traversal as _preorder_traversal, + iterargs, iterfreeargs) + +preorder_traversal = deprecated( + """ + Using preorder_traversal from the sympy.core.basic submodule is + deprecated. + + Instead, use preorder_traversal from the top-level sympy namespace, like + + sympy.preorder_traversal + """, + deprecated_since_version="1.10", + active_deprecations_target="deprecated-traversal-functions-moved", +)(_preorder_traversal) diff --git a/venv/lib/python3.11/site-packages/sympy/core/benchmarks/__init__.py b/venv/lib/python3.11/site-packages/sympy/core/benchmarks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_arit.py b/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_arit.py new file mode 100644 index 0000000000000000000000000000000000000000..39860943b763a30cf4f91578dbac37dc7e6e444e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_arit.py @@ -0,0 +1,43 @@ +from sympy.core import Add, Mul, symbols + +x, y, z = symbols('x,y,z') + + +def timeit_neg(): + -x + + +def timeit_Add_x1(): + x + 1 + + +def timeit_Add_1x(): + 1 + x + + +def timeit_Add_x05(): + x + 0.5 + + +def timeit_Add_xy(): + x + y + + +def timeit_Add_xyz(): + Add(*[x, y, z]) + + +def timeit_Mul_xy(): + x*y + + +def timeit_Mul_xyz(): + Mul(*[x, y, z]) + + +def timeit_Div_xy(): + x/y + + +def timeit_Div_2y(): + 2/y diff --git a/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_assumptions.py b/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_assumptions.py new file mode 100644 index 0000000000000000000000000000000000000000..1a8e47928b76034dd1d7ba8b8f87bd527bb1cdeb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_assumptions.py @@ -0,0 +1,12 @@ +from sympy.core import Symbol, Integer + +x = Symbol('x') +i3 = Integer(3) + + +def timeit_x_is_integer(): + x.is_integer + + +def timeit_Integer_is_irrational(): + i3.is_irrational diff --git a/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_basic.py b/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_basic.py new file mode 100644 index 0000000000000000000000000000000000000000..df2a382ecbd3cf6eb1f8555577dabb5e07c6643b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_basic.py @@ -0,0 +1,15 @@ +from sympy.core import symbols, S + +x, y = symbols('x,y') + + +def timeit_Symbol_meth_lookup(): + x.diff # no call, just method lookup + + +def timeit_S_lookup(): + S.Exp1 + + +def timeit_Symbol_eq_xy(): + x == y diff --git a/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_expand.py b/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_expand.py new file mode 100644 index 0000000000000000000000000000000000000000..4f5ac513e368cb7e9b542926bc25a5695de6d914 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_expand.py @@ -0,0 +1,23 @@ +from sympy.core import symbols, I + +x, y, z = symbols('x,y,z') + +p = 3*x**2*y*z**7 + 7*x*y*z**2 + 4*x + x*y**4 +e = (x + y + z + 1)**32 + + +def timeit_expand_nothing_todo(): + p.expand() + + +def bench_expand_32(): + """(x+y+z+1)**32 -> expand""" + e.expand() + + +def timeit_expand_complex_number_1(): + ((2 + 3*I)**1000).expand(complex=True) + + +def timeit_expand_complex_number_2(): + ((2 + 3*I/4)**1000).expand(complex=True) diff --git a/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_numbers.py b/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_numbers.py new file mode 100644 index 0000000000000000000000000000000000000000..5c7484c389232b3622fb4b6724e4ab8534dde382 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_numbers.py @@ -0,0 +1,92 @@ +from sympy.core.numbers import Integer, Rational, pi, oo +from sympy.core.intfunc import integer_nthroot, igcd +from sympy.core.singleton import S + +i3 = Integer(3) +i4 = Integer(4) +r34 = Rational(3, 4) +q45 = Rational(4, 5) + + +def timeit_Integer_create(): + Integer(2) + + +def timeit_Integer_int(): + int(i3) + + +def timeit_neg_one(): + -S.One + + +def timeit_Integer_neg(): + -i3 + + +def timeit_Integer_abs(): + abs(i3) + + +def timeit_Integer_sub(): + i3 - i3 + + +def timeit_abs_pi(): + abs(pi) + + +def timeit_neg_oo(): + -oo + + +def timeit_Integer_add_i1(): + i3 + 1 + + +def timeit_Integer_add_ij(): + i3 + i4 + + +def timeit_Integer_add_Rational(): + i3 + r34 + + +def timeit_Integer_mul_i4(): + i3*4 + + +def timeit_Integer_mul_ij(): + i3*i4 + + +def timeit_Integer_mul_Rational(): + i3*r34 + + +def timeit_Integer_eq_i3(): + i3 == 3 + + +def timeit_Integer_ed_Rational(): + i3 == r34 + + +def timeit_integer_nthroot(): + integer_nthroot(100, 2) + + +def timeit_number_igcd_23_17(): + igcd(23, 17) + + +def timeit_number_igcd_60_3600(): + igcd(60, 3600) + + +def timeit_Rational_add_r1(): + r34 + 1 + + +def timeit_Rational_add_rq(): + r34 + q45 diff --git a/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_sympify.py b/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_sympify.py new file mode 100644 index 0000000000000000000000000000000000000000..d8cc0abc1e35439a1a495454abf87769d5b40d04 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/benchmarks/bench_sympify.py @@ -0,0 +1,11 @@ +from sympy.core import sympify, Symbol + +x = Symbol('x') + + +def timeit_sympify_1(): + sympify(1) + + +def timeit_sympify_x(): + sympify(x) diff --git a/venv/lib/python3.11/site-packages/sympy/core/cache.py b/venv/lib/python3.11/site-packages/sympy/core/cache.py new file mode 100644 index 0000000000000000000000000000000000000000..ec11600a5e40ad446a6e5dde8820d46ea915b06a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/cache.py @@ -0,0 +1,210 @@ +""" Caching facility for SymPy """ +from importlib import import_module +from typing import Callable + +class _cache(list): + """ List of cached functions """ + + def print_cache(self): + """print cache info""" + + for item in self: + name = item.__name__ + myfunc = item + while hasattr(myfunc, '__wrapped__'): + if hasattr(myfunc, 'cache_info'): + info = myfunc.cache_info() + break + else: + myfunc = myfunc.__wrapped__ + else: + info = None + + print(name, info) + + def clear_cache(self): + """clear cache content""" + for item in self: + myfunc = item + while hasattr(myfunc, '__wrapped__'): + if hasattr(myfunc, 'cache_clear'): + myfunc.cache_clear() + break + else: + myfunc = myfunc.__wrapped__ + + +# global cache registry: +CACHE = _cache() +# make clear and print methods available +print_cache = CACHE.print_cache +clear_cache = CACHE.clear_cache + +from functools import lru_cache, wraps + +def __cacheit(maxsize): + """caching decorator. + + important: the result of cached function must be *immutable* + + + Examples + ======== + + >>> from sympy import cacheit + >>> @cacheit + ... def f(a, b): + ... return a+b + + >>> @cacheit + ... def f(a, b): # noqa: F811 + ... return [a, b] # <-- WRONG, returns mutable object + + to force cacheit to check returned results mutability and consistency, + set environment variable SYMPY_USE_CACHE to 'debug' + """ + def func_wrapper(func): + cfunc = lru_cache(maxsize, typed=True)(func) + + @wraps(func) + def wrapper(*args, **kwargs): + try: + retval = cfunc(*args, **kwargs) + except TypeError as e: + if not e.args or not e.args[0].startswith('unhashable type:'): + raise + retval = func(*args, **kwargs) + return retval + + wrapper.cache_info = cfunc.cache_info + wrapper.cache_clear = cfunc.cache_clear + + CACHE.append(wrapper) + return wrapper + + return func_wrapper +######################################## + + +def __cacheit_nocache(func): + return func + + +def __cacheit_debug(maxsize): + """cacheit + code to check cache consistency""" + def func_wrapper(func): + cfunc = __cacheit(maxsize)(func) + + @wraps(func) + def wrapper(*args, **kw_args): + # always call function itself and compare it with cached version + r1 = func(*args, **kw_args) + r2 = cfunc(*args, **kw_args) + + # try to see if the result is immutable + # + # this works because: + # + # hash([1,2,3]) -> raise TypeError + # hash({'a':1, 'b':2}) -> raise TypeError + # hash((1,[2,3])) -> raise TypeError + # + # hash((1,2,3)) -> just computes the hash + hash(r1), hash(r2) + + # also see if returned values are the same + if r1 != r2: + raise RuntimeError("Returned values are not the same") + return r1 + return wrapper + return func_wrapper + + +def _getenv(key, default=None): + from os import getenv + return getenv(key, default) + +# SYMPY_USE_CACHE=yes/no/debug +USE_CACHE = _getenv('SYMPY_USE_CACHE', 'yes').lower() +# SYMPY_CACHE_SIZE=some_integer/None +# special cases : +# SYMPY_CACHE_SIZE=0 -> No caching +# SYMPY_CACHE_SIZE=None -> Unbounded caching +scs = _getenv('SYMPY_CACHE_SIZE', '1000') +if scs.lower() == 'none': + SYMPY_CACHE_SIZE = None +else: + try: + SYMPY_CACHE_SIZE = int(scs) + except ValueError: + raise RuntimeError( + 'SYMPY_CACHE_SIZE must be a valid integer or None. ' + \ + 'Got: %s' % SYMPY_CACHE_SIZE) + +if USE_CACHE == 'no': + cacheit = __cacheit_nocache +elif USE_CACHE == 'yes': + cacheit = __cacheit(SYMPY_CACHE_SIZE) +elif USE_CACHE == 'debug': + cacheit = __cacheit_debug(SYMPY_CACHE_SIZE) # a lot slower +else: + raise RuntimeError( + 'unrecognized value for SYMPY_USE_CACHE: %s' % USE_CACHE) + + +def cached_property(func): + '''Decorator to cache property method''' + attrname = '__' + func.__name__ + _cached_property_sentinel = object() + def propfunc(self): + val = getattr(self, attrname, _cached_property_sentinel) + if val is _cached_property_sentinel: + val = func(self) + setattr(self, attrname, val) + return val + return property(propfunc) + + +def lazy_function(module : str, name : str) -> Callable: + """Create a lazy proxy for a function in a module. + + The module containing the function is not imported until the function is used. + + """ + func = None + + def _get_function(): + nonlocal func + if func is None: + func = getattr(import_module(module), name) + return func + + # The metaclass is needed so that help() shows the docstring + class LazyFunctionMeta(type): + @property + def __doc__(self): + docstring = _get_function().__doc__ + docstring += f"\n\nNote: this is a {self.__class__.__name__} wrapper of '{module}.{name}'" + return docstring + + class LazyFunction(metaclass=LazyFunctionMeta): + def __call__(self, *args, **kwargs): + # inline get of function for performance gh-23832 + nonlocal func + if func is None: + func = getattr(import_module(module), name) + return func(*args, **kwargs) + + @property + def __doc__(self): + docstring = _get_function().__doc__ + docstring += f"\n\nNote: this is a {self.__class__.__name__} wrapper of '{module}.{name}'" + return docstring + + def __str__(self): + return _get_function().__str__() + + def __repr__(self): + return f"<{__class__.__name__} object at 0x{id(self):x}>: wrapping '{module}.{name}'" + + return LazyFunction() diff --git a/venv/lib/python3.11/site-packages/sympy/core/compatibility.py b/venv/lib/python3.11/site-packages/sympy/core/compatibility.py new file mode 100644 index 0000000000000000000000000000000000000000..637a2698dbb39a042d3d664404bb0a4cba7fd004 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/compatibility.py @@ -0,0 +1,35 @@ +""" +.. deprecated:: 1.10 + + ``sympy.core.compatibility`` is deprecated. See + :ref:`sympy-core-compatibility`. + +Reimplementations of constructs introduced in later versions of Python than +we support. Also some functions that are needed SymPy-wide and are located +here for easy import. + +""" + + +from sympy.utilities.exceptions import sympy_deprecation_warning + +sympy_deprecation_warning(""" +The sympy.core.compatibility submodule is deprecated. + +This module was only ever intended for internal use. Some of the functions +that were in this module are available from the top-level SymPy namespace, +i.e., + + from sympy import ordered, default_sort_key + +The remaining were only intended for internal SymPy use and should not be used +by user code. +""", + deprecated_since_version="1.10", + active_deprecations_target="deprecated-sympy-core-compatibility", + ) + + +from .sorting import ordered, _nodes, default_sort_key # noqa:F401 +from sympy.utilities.misc import as_int as _as_int # noqa:F401 +from sympy.utilities.iterables import iterable, is_sequence, NotIterable # noqa:F401 diff --git a/venv/lib/python3.11/site-packages/sympy/core/containers.py b/venv/lib/python3.11/site-packages/sympy/core/containers.py new file mode 100644 index 0000000000000000000000000000000000000000..35352009e87f3a7809a53031080cefdadb6528be --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/containers.py @@ -0,0 +1,415 @@ +"""Module for SymPy containers + + (SymPy objects that store other SymPy objects) + + The containers implemented in this module are subclassed to Basic. + They are supposed to work seamlessly within the SymPy framework. +""" + +from __future__ import annotations + +from collections import OrderedDict +from collections.abc import MutableSet +from typing import Any, Callable + +from .basic import Basic +from .sorting import default_sort_key, ordered +from .sympify import _sympify, sympify, _sympy_converter, SympifyError +from sympy.core.kind import Kind +from sympy.utilities.iterables import iterable +from sympy.utilities.misc import as_int + + +class Tuple(Basic): + """ + Wrapper around the builtin tuple object. + + Explanation + =========== + + The Tuple is a subclass of Basic, so that it works well in the + SymPy framework. The wrapped tuple is available as self.args, but + you can also access elements or slices with [:] syntax. + + Parameters + ========== + + sympify : bool + If ``False``, ``sympify`` is not called on ``args``. This + can be used for speedups for very large tuples where the + elements are known to already be SymPy objects. + + Examples + ======== + + >>> from sympy import Tuple, symbols + >>> a, b, c, d = symbols('a b c d') + >>> Tuple(a, b, c)[1:] + (b, c) + >>> Tuple(a, b, c).subs(a, d) + (d, b, c) + + """ + + def __new__(cls, *args, **kwargs): + if kwargs.get('sympify', True): + args = (sympify(arg) for arg in args) + obj = Basic.__new__(cls, *args) + return obj + + def __getitem__(self, i): + if isinstance(i, slice): + indices = i.indices(len(self)) + return Tuple(*(self.args[j] for j in range(*indices))) + return self.args[i] + + def __len__(self): + return len(self.args) + + def __contains__(self, item): + return item in self.args + + def __iter__(self): + return iter(self.args) + + def __add__(self, other): + if isinstance(other, Tuple): + return Tuple(*(self.args + other.args)) + elif isinstance(other, tuple): + return Tuple(*(self.args + other)) + else: + return NotImplemented + + def __radd__(self, other): + if isinstance(other, Tuple): + return Tuple(*(other.args + self.args)) + elif isinstance(other, tuple): + return Tuple(*(other + self.args)) + else: + return NotImplemented + + def __mul__(self, other): + try: + n = as_int(other) + except ValueError: + raise TypeError("Can't multiply sequence by non-integer of type '%s'" % type(other)) + return self.func(*(self.args*n)) + + __rmul__ = __mul__ + + def __eq__(self, other): + if isinstance(other, Basic): + return super().__eq__(other) + return self.args == other + + def __ne__(self, other): + if isinstance(other, Basic): + return super().__ne__(other) + return self.args != other + + def __hash__(self): + return hash(self.args) + + def _to_mpmath(self, prec): + return tuple(a._to_mpmath(prec) for a in self.args) + + def __lt__(self, other): + return _sympify(self.args < other.args) + + def __le__(self, other): + return _sympify(self.args <= other.args) + + # XXX: Basic defines count() as something different, so we can't + # redefine it here. Originally this lead to cse() test failure. + def tuple_count(self, value) -> int: + """Return number of occurrences of value.""" + return self.args.count(value) + + def index(self, value, start=None, stop=None): + """Searches and returns the first index of the value.""" + # XXX: One would expect: + # + # return self.args.index(value, start, stop) + # + # here. Any trouble with that? Yes: + # + # >>> (1,).index(1, None, None) + # Traceback (most recent call last): + # File "", line 1, in + # TypeError: slice indices must be integers or None or have an __index__ method + # + # See: http://bugs.python.org/issue13340 + + if start is None and stop is None: + return self.args.index(value) + elif stop is None: + return self.args.index(value, start) + else: + return self.args.index(value, start, stop) + + @property + def kind(self): + """ + The kind of a Tuple instance. + + The kind of a Tuple is always of :class:`TupleKind` but + parametrised by the number of elements and the kind of each element. + + Examples + ======== + + >>> from sympy import Tuple, Matrix + >>> Tuple(1, 2).kind + TupleKind(NumberKind, NumberKind) + >>> Tuple(Matrix([1, 2]), 1).kind + TupleKind(MatrixKind(NumberKind), NumberKind) + >>> Tuple(1, 2).kind.element_kind + (NumberKind, NumberKind) + + See Also + ======== + + sympy.matrices.kind.MatrixKind + sympy.core.kind.NumberKind + """ + return TupleKind(*(i.kind for i in self.args)) + +_sympy_converter[tuple] = lambda tup: Tuple(*tup) + + + + + +def tuple_wrapper(method): + """ + Decorator that converts any tuple in the function arguments into a Tuple. + + Explanation + =========== + + The motivation for this is to provide simple user interfaces. The user can + call a function with regular tuples in the argument, and the wrapper will + convert them to Tuples before handing them to the function. + + Explanation + =========== + + >>> from sympy.core.containers import tuple_wrapper + >>> def f(*args): + ... return args + >>> g = tuple_wrapper(f) + + The decorated function g sees only the Tuple argument: + + >>> g(0, (1, 2), 3) + (0, (1, 2), 3) + + """ + def wrap_tuples(*args, **kw_args): + newargs = [] + for arg in args: + if isinstance(arg, tuple): + newargs.append(Tuple(*arg)) + else: + newargs.append(arg) + return method(*newargs, **kw_args) + return wrap_tuples + + +class Dict(Basic): + """ + Wrapper around the builtin dict object. + + Explanation + =========== + + The Dict is a subclass of Basic, so that it works well in the + SymPy framework. Because it is immutable, it may be included + in sets, but its values must all be given at instantiation and + cannot be changed afterwards. Otherwise it behaves identically + to the Python dict. + + Examples + ======== + + >>> from sympy import Dict, Symbol + + >>> D = Dict({1: 'one', 2: 'two'}) + >>> for key in D: + ... if key == 1: + ... print('%s %s' % (key, D[key])) + 1 one + + The args are sympified so the 1 and 2 are Integers and the values + are Symbols. Queries automatically sympify args so the following work: + + >>> 1 in D + True + >>> D.has(Symbol('one')) # searches keys and values + True + >>> 'one' in D # not in the keys + False + >>> D[1] + one + + """ + + elements: frozenset[Tuple] + _dict: dict[Basic, Basic] + + def __new__(cls, *args): + if len(args) == 1 and isinstance(args[0], (dict, Dict)): + items = [Tuple(k, v) for k, v in args[0].items()] + elif iterable(args) and all(len(arg) == 2 for arg in args): + items = [Tuple(k, v) for k, v in args] + else: + raise TypeError('Pass Dict args as Dict((k1, v1), ...) or Dict({k1: v1, ...})') + elements = frozenset(items) + obj = Basic.__new__(cls, *ordered(items)) + obj.elements = elements + obj._dict = dict(items) # In case Tuple decides it wants to sympify + return obj + + def __getitem__(self, key): + """x.__getitem__(y) <==> x[y]""" + try: + key = _sympify(key) + except SympifyError: + raise KeyError(key) + + return self._dict[key] + + def __setitem__(self, key, value): + raise NotImplementedError("SymPy Dicts are Immutable") + + def items(self): + '''Returns a set-like object providing a view on dict's items. + ''' + return self._dict.items() + + def keys(self): + '''Returns the list of the dict's keys.''' + return self._dict.keys() + + def values(self): + '''Returns the list of the dict's values.''' + return self._dict.values() + + def __iter__(self): + '''x.__iter__() <==> iter(x)''' + return iter(self._dict) + + def __len__(self): + '''x.__len__() <==> len(x)''' + return self._dict.__len__() + + def get(self, key, default=None): + '''Returns the value for key if the key is in the dictionary.''' + try: + key = _sympify(key) + except SympifyError: + return default + return self._dict.get(key, default) + + def __contains__(self, key): + '''D.__contains__(k) -> True if D has a key k, else False''' + try: + key = _sympify(key) + except SympifyError: + return False + return key in self._dict + + def __lt__(self, other): + return _sympify(self.args < other.args) + + @property + def _sorted_args(self): + return tuple(sorted(self.args, key=default_sort_key)) + + def __eq__(self, other): + if isinstance(other, dict): + return self == Dict(other) + return super().__eq__(other) + + __hash__ : Callable[[Basic], Any] = Basic.__hash__ + +# this handles dict, defaultdict, OrderedDict +_sympy_converter[dict] = lambda d: Dict(*d.items()) + +class OrderedSet(MutableSet): + def __init__(self, iterable=None): + if iterable: + self.map = OrderedDict((item, None) for item in iterable) + else: + self.map = OrderedDict() + + def __len__(self): + return len(self.map) + + def __contains__(self, key): + return key in self.map + + def add(self, key): + self.map[key] = None + + def discard(self, key): + self.map.pop(key) + + def pop(self, last=True): + return self.map.popitem(last=last)[0] + + def __iter__(self): + yield from self.map.keys() + + def __repr__(self): + if not self.map: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, list(self.map.keys())) + + def intersection(self, other): + return self.__class__([val for val in self if val in other]) + + def difference(self, other): + return self.__class__([val for val in self if val not in other]) + + def update(self, iterable): + for val in iterable: + self.add(val) + +class TupleKind(Kind): + """ + TupleKind is a subclass of Kind, which is used to define Kind of ``Tuple``. + + Parameters of TupleKind will be kinds of all the arguments in Tuples, for + example + + Parameters + ========== + + args : tuple(element_kind) + element_kind is kind of element. + args is tuple of kinds of element + + Examples + ======== + + >>> from sympy import Tuple + >>> Tuple(1, 2).kind + TupleKind(NumberKind, NumberKind) + >>> Tuple(1, 2).kind.element_kind + (NumberKind, NumberKind) + + See Also + ======== + + sympy.core.kind.NumberKind + MatrixKind + sympy.sets.sets.SetKind + """ + def __new__(cls, *args): + obj = super().__new__(cls, *args) + obj.element_kind = args + return obj + + def __repr__(self): + return "TupleKind{}".format(self.element_kind) diff --git a/venv/lib/python3.11/site-packages/sympy/core/core.py b/venv/lib/python3.11/site-packages/sympy/core/core.py new file mode 100644 index 0000000000000000000000000000000000000000..8a45bb06919d7a8ef88e2c9958decac705c0b8ee --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/core.py @@ -0,0 +1,21 @@ +""" The core's core. """ +from __future__ import annotations + + +class Registry: + """ + Base class for registry objects. + + Registries map a name to an object using attribute notation. Registry + classes behave singletonically: all their instances share the same state, + which is stored in the class object. + + All subclasses should set `__slots__ = ()`. + """ + __slots__ = () + + def __setattr__(self, name, obj): + setattr(self.__class__, name, obj) + + def __delattr__(self, name): + delattr(self.__class__, name) diff --git a/venv/lib/python3.11/site-packages/sympy/core/coreerrors.py b/venv/lib/python3.11/site-packages/sympy/core/coreerrors.py new file mode 100644 index 0000000000000000000000000000000000000000..d2dbdd5227d7b0495145072d31bd993f13f31f0d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/coreerrors.py @@ -0,0 +1,23 @@ +"""Definitions of common exceptions for :mod:`sympy.core` module. """ + +from typing import Callable + + +class BaseCoreError(Exception): + """Base class for core related exceptions. """ + + +class NonCommutativeExpression(BaseCoreError): + """Raised when expression didn't have commutative property. """ + + +class LazyExceptionMessage: + """Wrapper class that lets you specify an expensive to compute + error message that is only evaluated if the error is rendered.""" + callback: Callable[[], str] + + def __init__(self, callback: Callable[[], str]): + self.callback = callback + + def __str__(self): + return self.callback() diff --git a/venv/lib/python3.11/site-packages/sympy/core/decorators.py b/venv/lib/python3.11/site-packages/sympy/core/decorators.py new file mode 100644 index 0000000000000000000000000000000000000000..afd6ae0c72dc32d260586c6411507e4859a9f8ff --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/decorators.py @@ -0,0 +1,250 @@ +""" +SymPy core decorators. + +The purpose of this module is to expose decorators without any other +dependencies, so that they can be easily imported anywhere in sympy/core. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from functools import wraps +from .sympify import SympifyError, sympify + + +if TYPE_CHECKING: + from typing import Callable, TypeVar, Union + T1 = TypeVar('T1') + T2 = TypeVar('T2') + T3 = TypeVar('T3') + + +def _sympifyit(arg, retval=None) -> Callable[[Callable[[T1, T2], T3]], Callable[[T1, T2], T3]]: + """ + decorator to smartly _sympify function arguments + + Explanation + =========== + + @_sympifyit('other', NotImplemented) + def add(self, other): + ... + + In add, other can be thought of as already being a SymPy object. + + If it is not, the code is likely to catch an exception, then other will + be explicitly _sympified, and the whole code restarted. + + if _sympify(arg) fails, NotImplemented will be returned + + See also + ======== + + __sympifyit + """ + def deco(func): + return __sympifyit(func, arg, retval) + + return deco + + +def __sympifyit(func, arg, retval=None): + """Decorator to _sympify `arg` argument for function `func`. + + Do not use directly -- use _sympifyit instead. + """ + + # we support f(a,b) only + if not func.__code__.co_argcount: + raise LookupError("func not found") + # only b is _sympified + assert func.__code__.co_varnames[1] == arg + if retval is None: + @wraps(func) + def __sympifyit_wrapper(a, b): + return func(a, sympify(b, strict=True)) + + else: + @wraps(func) + def __sympifyit_wrapper(a, b): + try: + # If an external class has _op_priority, it knows how to deal + # with SymPy objects. Otherwise, it must be converted. + if not hasattr(b, '_op_priority'): + b = sympify(b, strict=True) + return func(a, b) + except SympifyError: + return retval + + return __sympifyit_wrapper + + +def call_highest_priority(method_name: str + ) -> Callable[[Callable[[T1, T2], T3]], Callable[[T1, T2], T3]]: + """A decorator for binary special methods to handle _op_priority. + + Explanation + =========== + + Binary special methods in Expr and its subclasses use a special attribute + '_op_priority' to determine whose special method will be called to + handle the operation. In general, the object having the highest value of + '_op_priority' will handle the operation. Expr and subclasses that define + custom binary special methods (__mul__, etc.) should decorate those + methods with this decorator to add the priority logic. + + The ``method_name`` argument is the name of the method of the other class + that will be called. Use this decorator in the following manner:: + + # Call other.__rmul__ if other._op_priority > self._op_priority + @call_highest_priority('__rmul__') + def __mul__(self, other): + ... + + # Call other.__mul__ if other._op_priority > self._op_priority + @call_highest_priority('__mul__') + def __rmul__(self, other): + ... + """ + def priority_decorator(func: Callable[[T1, T2], T3]) -> Callable[[T1, T2], T3]: + @wraps(func) + def binary_op_wrapper(self: T1, other: T2) -> T3: + if hasattr(other, '_op_priority'): + if other._op_priority > self._op_priority: # type: ignore + f: Union[Callable[[T1], T3], None] = getattr(other, method_name, None) + if f is not None: + return f(self) + return func(self, other) + return binary_op_wrapper + return priority_decorator + + +def sympify_method_args(cls: type[T1]) -> type[T1]: + '''Decorator for a class with methods that sympify arguments. + + Explanation + =========== + + The sympify_method_args decorator is to be used with the sympify_return + decorator for automatic sympification of method arguments. This is + intended for the common idiom of writing a class like : + + Examples + ======== + + >>> from sympy import Basic, SympifyError, S + >>> from sympy.core.sympify import _sympify + + >>> class MyTuple(Basic): + ... def __add__(self, other): + ... try: + ... other = _sympify(other) + ... except SympifyError: + ... return NotImplemented + ... if not isinstance(other, MyTuple): + ... return NotImplemented + ... return MyTuple(*(self.args + other.args)) + + >>> MyTuple(S(1), S(2)) + MyTuple(S(3), S(4)) + MyTuple(1, 2, 3, 4) + + In the above it is important that we return NotImplemented when other is + not sympifiable and also when the sympified result is not of the expected + type. This allows the MyTuple class to be used cooperatively with other + classes that overload __add__ and want to do something else in combination + with instance of Tuple. + + Using this decorator the above can be written as + + >>> from sympy.core.decorators import sympify_method_args, sympify_return + + >>> @sympify_method_args + ... class MyTuple(Basic): + ... @sympify_return([('other', 'MyTuple')], NotImplemented) + ... def __add__(self, other): + ... return MyTuple(*(self.args + other.args)) + + >>> MyTuple(S(1), S(2)) + MyTuple(S(3), S(4)) + MyTuple(1, 2, 3, 4) + + The idea here is that the decorators take care of the boiler-plate code + for making this happen in each method that potentially needs to accept + unsympified arguments. Then the body of e.g. the __add__ method can be + written without needing to worry about calling _sympify or checking the + type of the resulting object. + + The parameters for sympify_return are a list of tuples of the form + (parameter_name, expected_type) and the value to return (e.g. + NotImplemented). The expected_type parameter can be a type e.g. Tuple or a + string 'Tuple'. Using a string is useful for specifying a Type within its + class body (as in the above example). + + Notes: Currently sympify_return only works for methods that take a single + argument (not including self). Specifying an expected_type as a string + only works for the class in which the method is defined. + ''' + # Extract the wrapped methods from each of the wrapper objects created by + # the sympify_return decorator. Doing this here allows us to provide the + # cls argument which is used for forward string referencing. + for attrname, obj in cls.__dict__.items(): + if isinstance(obj, _SympifyWrapper): + setattr(cls, attrname, obj.make_wrapped(cls)) + return cls + + +def sympify_return(*args): + '''Function/method decorator to sympify arguments automatically + + See the docstring of sympify_method_args for explanation. + ''' + # Store a wrapper object for the decorated method + def wrapper(func: Callable[[T1, T2], T3]) -> Callable[[T1, T2], T3]: + return _SympifyWrapper(func, args) # type: ignore + return wrapper + + +class _SympifyWrapper: + '''Internal class used by sympify_return and sympify_method_args''' + + def __init__(self, func, args): + self.func = func + self.args = args + + def make_wrapped(self, cls): + func = self.func + parameters, retval = self.args + + # XXX: Handle more than one parameter? + [(parameter, expectedcls)] = parameters + + # Handle forward references to the current class using strings + if expectedcls == cls.__name__: + expectedcls = cls + + # Raise RuntimeError since this is a failure at import time and should + # not be recoverable. + nargs = func.__code__.co_argcount + # we support f(a, b) only + if nargs != 2: + raise RuntimeError('sympify_return can only be used with 2 argument functions') + # only b is _sympified + if func.__code__.co_varnames[1] != parameter: + raise RuntimeError('parameter name mismatch "%s" in %s' % + (parameter, func.__name__)) + + @wraps(func) + def _func(self, other): + # XXX: The check for _op_priority here should be removed. It is + # needed to stop mutable matrices from being sympified to + # immutable matrices which breaks things in quantum... + if not hasattr(other, '_op_priority'): + try: + other = sympify(other, strict=True) + except SympifyError: + return retval + if not isinstance(other, expectedcls): + return retval + return func(self, other) + + return _func diff --git a/venv/lib/python3.11/site-packages/sympy/core/evalf.py b/venv/lib/python3.11/site-packages/sympy/core/evalf.py new file mode 100644 index 0000000000000000000000000000000000000000..55a981090360556b357ec1cade5576e226633be8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/evalf.py @@ -0,0 +1,1808 @@ +""" +Adaptive numerical evaluation of SymPy expressions, using mpmath +for mathematical functions. +""" +from __future__ import annotations +from typing import Callable, TYPE_CHECKING, Any, overload, Type + +import math + +import mpmath.libmp as libmp +from mpmath import ( + make_mpc, make_mpf, mp, mpc, mpf, nsum, quadts, quadosc, workprec) +from mpmath import inf as mpmath_inf +from mpmath.libmp import (from_int, from_man_exp, from_rational, fhalf, + fnan, finf, fninf, fnone, fone, fzero, mpf_abs, mpf_add, + mpf_atan, mpf_atan2, mpf_cmp, mpf_cos, mpf_e, mpf_exp, mpf_log, mpf_lt, + mpf_mul, mpf_neg, mpf_pi, mpf_pow, mpf_pow_int, mpf_shift, mpf_sin, + mpf_sqrt, normalize, round_nearest, to_int, to_str, mpf_tan) +from mpmath.libmp import bitcount as mpmath_bitcount +from mpmath.libmp.backend import MPZ +from mpmath.libmp.libmpc import _infs_nan +from mpmath.libmp.libmpf import dps_to_prec, prec_to_dps + +from .sympify import sympify +from .singleton import S +from sympy.external.gmpy import SYMPY_INTS +from sympy.utilities.iterables import is_sequence +from sympy.utilities.lambdify import lambdify +from sympy.utilities.misc import as_int + +if TYPE_CHECKING: + from sympy.core.expr import Expr + from sympy.core.add import Add + from sympy.core.mul import Mul + from sympy.core.power import Pow + from sympy.core.symbol import Symbol + from sympy.integrals.integrals import Integral + from sympy.concrete.summations import Sum + from sympy.concrete.products import Product + from sympy.functions.elementary.exponential import exp, log + from sympy.functions.elementary.complexes import Abs, re, im + from sympy.functions.elementary.integers import ceiling, floor + from sympy.functions.elementary.trigonometric import atan + from .numbers import Float, Rational, Integer, AlgebraicNumber, Number + +LG10 = math.log2(10) +rnd = round_nearest + + +def bitcount(n): + """Return smallest integer, b, such that |n|/2**b < 1. + """ + return mpmath_bitcount(abs(int(n))) + +# Used in a few places as placeholder values to denote exponents and +# precision levels, e.g. of exact numbers. Must be careful to avoid +# passing these to mpmath functions or returning them in final results. +INF = float(mpmath_inf) +MINUS_INF = float(-mpmath_inf) + +# ~= 100 digits. Real men set this to INF. +DEFAULT_MAXPREC = 333 + + +class PrecisionExhausted(ArithmeticError): + pass + +#----------------------------------------------------------------------------# +# # +# Helper functions for arithmetic and complex parts # +# # +#----------------------------------------------------------------------------# + +""" +An mpf value tuple is a tuple of integers (sign, man, exp, bc) +representing a floating-point number: [1, -1][sign]*man*2**exp where +sign is 0 or 1 and bc should correspond to the number of bits used to +represent the mantissa (man) in binary notation, e.g. +""" + +MPF_TUP = tuple[int, int, int, int] # mpf value tuple + +""" +Explanation +=========== + +>>> from sympy.core.evalf import bitcount +>>> sign, man, exp, bc = 0, 5, 1, 3 +>>> n = [1, -1][sign]*man*2**exp +>>> n, bitcount(man) +(10, 3) + +A temporary result is a tuple (re, im, re_acc, im_acc) where +re and im are nonzero mpf value tuples representing approximate +numbers, or None to denote exact zeros. + +re_acc, im_acc are integers denoting log2(e) where e is the estimated +relative accuracy of the respective complex part, but may be anything +if the corresponding complex part is None. + +""" +TMP_RES = Any # temporary result, should be some variant of +# tUnion[tTuple[Optional[MPF_TUP], Optional[MPF_TUP], +# Optional[int], Optional[int]], +# 'ComplexInfinity'] +# but mypy reports error because it doesn't know as we know +# 1. re and re_acc are either both None or both MPF_TUP +# 2. sometimes the result can't be zoo + +# type of the "options" parameter in internal evalf functions +OPT_DICT = dict[str, Any] + + +def fastlog(x: MPF_TUP | None) -> int | Any: + """Fast approximation of log2(x) for an mpf value tuple x. + + Explanation + =========== + + Calculated as exponent + width of mantissa. This is an + approximation for two reasons: 1) it gives the ceil(log2(abs(x))) + value and 2) it is too high by 1 in the case that x is an exact + power of 2. Although this is easy to remedy by testing to see if + the odd mpf mantissa is 1 (indicating that one was dealing with + an exact power of 2) that would decrease the speed and is not + necessary as this is only being used as an approximation for the + number of bits in x. The correct return value could be written as + "x[2] + (x[3] if x[1] != 1 else 0)". + Since mpf tuples always have an odd mantissa, no check is done + to see if the mantissa is a multiple of 2 (in which case the + result would be too large by 1). + + Examples + ======== + + >>> from sympy import log + >>> from sympy.core.evalf import fastlog, bitcount + >>> s, m, e = 0, 5, 1 + >>> bc = bitcount(m) + >>> n = [1, -1][s]*m*2**e + >>> n, (log(n)/log(2)).evalf(2), fastlog((s, m, e, bc)) + (10, 3.3, 4) + """ + + if not x or x == fzero: + return MINUS_INF + return x[2] + x[3] + + +def pure_complex(v: Expr, or_real=False) -> tuple[Number, Number] | None: + """Return a and b if v matches a + I*b where b is not zero and + a and b are Numbers, else None. If `or_real` is True then 0 will + be returned for `b` if `v` is a real number. + + Examples + ======== + + >>> from sympy.core.evalf import pure_complex + >>> from sympy import sqrt, I, S + >>> a, b, surd = S(2), S(3), sqrt(2) + >>> pure_complex(a) + >>> pure_complex(a, or_real=True) + (2, 0) + >>> pure_complex(surd) + >>> pure_complex(a + b*I) + (2, 3) + >>> pure_complex(I) + (0, 1) + """ + h, t = v.as_coeff_Add() + if t: + c, i = t.as_coeff_Mul() + if i is S.ImaginaryUnit: + return h, c + elif or_real: + return h, S.Zero + return None + + +# I don't know what this is, see function scaled_zero below +SCALED_ZERO_TUP = tuple[list[int], int, int, int] + + + +@overload +def scaled_zero(mag: SCALED_ZERO_TUP, sign=1) -> MPF_TUP: + ... +@overload +def scaled_zero(mag: int, sign=1) -> tuple[SCALED_ZERO_TUP, int]: + ... +def scaled_zero(mag: SCALED_ZERO_TUP | int, sign=1) -> \ + MPF_TUP | tuple[SCALED_ZERO_TUP, int]: + """Return an mpf representing a power of two with magnitude ``mag`` + and -1 for precision. Or, if ``mag`` is a scaled_zero tuple, then just + remove the sign from within the list that it was initially wrapped + in. + + Examples + ======== + + >>> from sympy.core.evalf import scaled_zero + >>> from sympy import Float + >>> z, p = scaled_zero(100) + >>> z, p + (([0], 1, 100, 1), -1) + >>> ok = scaled_zero(z) + >>> ok + (0, 1, 100, 1) + >>> Float(ok) + 1.26765060022823e+30 + >>> Float(ok, p) + 0.e+30 + >>> ok, p = scaled_zero(100, -1) + >>> Float(scaled_zero(ok), p) + -0.e+30 + """ + if isinstance(mag, tuple) and len(mag) == 4 and iszero(mag, scaled=True): + return (mag[0][0],) + mag[1:] + elif isinstance(mag, SYMPY_INTS): + if sign not in [-1, 1]: + raise ValueError('sign must be +/-1') + rv, p = mpf_shift(fone, mag), -1 + s = 0 if sign == 1 else 1 + rv = ([s],) + rv[1:] + return rv, p + else: + raise ValueError('scaled zero expects int or scaled_zero tuple.') + + +def iszero(mpf: MPF_TUP | SCALED_ZERO_TUP | None, scaled=False) -> bool | None: + if not scaled: + return not mpf or not mpf[1] and not mpf[-1] + return mpf and isinstance(mpf[0], list) and mpf[1] == mpf[-1] == 1 + + +def complex_accuracy(result: TMP_RES) -> int | Any: + """ + Returns relative accuracy of a complex number with given accuracies + for the real and imaginary parts. The relative accuracy is defined + in the complex norm sense as ||z|+|error|| / |z| where error + is equal to (real absolute error) + (imag absolute error)*i. + + The full expression for the (logarithmic) error can be approximated + easily by using the max norm to approximate the complex norm. + + In the worst case (re and im equal), this is wrong by a factor + sqrt(2), or by log2(sqrt(2)) = 0.5 bit. + """ + if result is S.ComplexInfinity: + return INF + re, im, re_acc, im_acc = result + if not im: + if not re: + return INF + return re_acc + if not re: + return im_acc + re_size = fastlog(re) + im_size = fastlog(im) + absolute_error = max(re_size - re_acc, im_size - im_acc) + relative_error = absolute_error - max(re_size, im_size) + return -relative_error + + +def get_abs(expr: Expr, prec: int, options: OPT_DICT) -> TMP_RES: + result = evalf(expr, prec + 2, options) + if result is S.ComplexInfinity: + return finf, None, prec, None + re, im, re_acc, im_acc = result + if not re: + re, re_acc, im, im_acc = im, im_acc, re, re_acc + if im: + if expr.is_number: + abs_expr, _, acc, _ = evalf(abs(N(expr, prec + 2)), + prec + 2, options) + return abs_expr, None, acc, None + else: + if 'subs' in options: + return libmp.mpc_abs((re, im), prec), None, re_acc, None + return abs(expr), None, prec, None + elif re: + return mpf_abs(re), None, re_acc, None + else: + return None, None, None, None + + +def get_complex_part(expr: Expr, no: int, prec: int, options: OPT_DICT) -> TMP_RES: + """no = 0 for real part, no = 1 for imaginary part""" + workprec = prec + i = 0 + while 1: + res = evalf(expr, workprec, options) + if res is S.ComplexInfinity: + return fnan, None, prec, None + value, accuracy = res[no::2] + # XXX is the last one correct? Consider re((1+I)**2).n() + if (not value) or accuracy >= prec or -value[2] > prec: + return value, None, accuracy, None + workprec += max(30, 2**i) + i += 1 + + +def evalf_abs(expr: 'Abs', prec: int, options: OPT_DICT) -> TMP_RES: + return get_abs(expr.args[0], prec, options) + + +def evalf_re(expr: 're', prec: int, options: OPT_DICT) -> TMP_RES: + return get_complex_part(expr.args[0], 0, prec, options) + + +def evalf_im(expr: 'im', prec: int, options: OPT_DICT) -> TMP_RES: + return get_complex_part(expr.args[0], 1, prec, options) + + +def finalize_complex(re: MPF_TUP, im: MPF_TUP, prec: int) -> TMP_RES: + if re == fzero and im == fzero: + raise ValueError("got complex zero with unknown accuracy") + elif re == fzero: + return None, im, None, prec + elif im == fzero: + return re, None, prec, None + + size_re = fastlog(re) + size_im = fastlog(im) + if size_re > size_im: + re_acc = prec + im_acc = prec + min(-(size_re - size_im), 0) + else: + im_acc = prec + re_acc = prec + min(-(size_im - size_re), 0) + return re, im, re_acc, im_acc + + +def chop_parts(value: TMP_RES, prec: int) -> TMP_RES: + """ + Chop off tiny real or complex parts. + """ + if value is S.ComplexInfinity: + return value + re, im, re_acc, im_acc = value + # Method 1: chop based on absolute value + if re and re not in _infs_nan and (fastlog(re) < -prec + 4): + re, re_acc = None, None + if im and im not in _infs_nan and (fastlog(im) < -prec + 4): + im, im_acc = None, None + # Method 2: chop if inaccurate and relatively small + if re and im: + delta = fastlog(re) - fastlog(im) + if re_acc < 2 and (delta - re_acc <= -prec + 4): + re, re_acc = None, None + if im_acc < 2 and (delta - im_acc >= prec - 4): + im, im_acc = None, None + return re, im, re_acc, im_acc + + +def check_target(expr: Expr, result: TMP_RES, prec: int): + a = complex_accuracy(result) + if a < prec: + raise PrecisionExhausted("Failed to distinguish the expression: \n\n%s\n\n" + "from zero. Try simplifying the input, using chop=True, or providing " + "a higher maxn for evalf" % (expr)) + + +def get_integer_part(expr: Expr, no: int, options: OPT_DICT, return_ints=False) -> \ + TMP_RES | tuple[int, int]: + """ + With no = 1, computes ceiling(expr) + With no = -1, computes floor(expr) + + Note: this function either gives the exact result or signals failure. + """ + from sympy.functions.elementary.complexes import re, im + # The expression is likely less than 2^30 or so + assumed_size = 30 + result = evalf(expr, assumed_size, options) + if result is S.ComplexInfinity: + raise ValueError("Cannot get integer part of Complex Infinity") + ire, iim, ire_acc, iim_acc = result + + # We now know the size, so we can calculate how much extra precision + # (if any) is needed to get within the nearest integer + if ire and iim: + gap = max(fastlog(ire) - ire_acc, fastlog(iim) - iim_acc) + elif ire: + gap = fastlog(ire) - ire_acc + elif iim: + gap = fastlog(iim) - iim_acc + else: + # ... or maybe the expression was exactly zero + if return_ints: + return 0, 0 + else: + return None, None, None, None + + margin = 10 + + if gap >= -margin: + prec = margin + assumed_size + gap + ire, iim, ire_acc, iim_acc = evalf( + expr, prec, options) + else: + prec = assumed_size + + # We can now easily find the nearest integer, but to find floor/ceil, we + # must also calculate whether the difference to the nearest integer is + # positive or negative (which may fail if very close). + def calc_part(re_im: Expr, nexpr: MPF_TUP): + from .add import Add + _, _, exponent, _ = nexpr + is_int = exponent == 0 + nint = int(to_int(nexpr, rnd)) + if is_int: + # make sure that we had enough precision to distinguish + # between nint and the re or im part (re_im) of expr that + # was passed to calc_part + ire, iim, ire_acc, iim_acc = evalf( + re_im - nint, 10, options) # don't need much precision + assert not iim + size = -fastlog(ire) + 2 # -ve b/c ire is less than 1 + if size > prec: + ire, iim, ire_acc, iim_acc = evalf( + re_im, size, options) + assert not iim + nexpr = ire + nint = int(to_int(nexpr, rnd)) + _, _, new_exp, _ = ire + is_int = new_exp == 0 + if not is_int: + # if there are subs and they all contain integer re/im parts + # then we can (hopefully) safely substitute them into the + # expression + s = options.get('subs', False) + if s: + # use strict=False with as_int because we take + # 2.0 == 2 + def is_int_reim(x): + """Check for integer or integer + I*integer.""" + try: + as_int(x, strict=False) + return True + except ValueError: + try: + [as_int(i, strict=False) for i in x.as_real_imag()] + return True + except ValueError: + return False + + if all(is_int_reim(v) for v in s.values()): + re_im = re_im.subs(s) + + re_im = Add(re_im, -nint, evaluate=False) + x, _, x_acc, _ = evalf(re_im, 10, options) + try: + check_target(re_im, (x, None, x_acc, None), 3) + except PrecisionExhausted: + if not re_im.equals(0): + raise PrecisionExhausted + x = fzero + nint += int(no*(mpf_cmp(x or fzero, fzero) == no)) + nint = from_int(nint) + return nint, INF + + re_, im_, re_acc, im_acc = None, None, None, None + + if ire is not None and ire != fzero: + re_, re_acc = calc_part(re(expr, evaluate=False), ire) + if iim is not None and iim != fzero: + im_, im_acc = calc_part(im(expr, evaluate=False), iim) + + if return_ints: + return int(to_int(re_ or fzero)), int(to_int(im_ or fzero)) + return re_, im_, re_acc, im_acc + + +def evalf_ceiling(expr: 'ceiling', prec: int, options: OPT_DICT) -> TMP_RES: + return get_integer_part(expr.args[0], 1, options) + + +def evalf_floor(expr: 'floor', prec: int, options: OPT_DICT) -> TMP_RES: + return get_integer_part(expr.args[0], -1, options) + + +def evalf_float(expr: 'Float', prec: int, options: OPT_DICT) -> TMP_RES: + return expr._mpf_, None, prec, None + + +def evalf_rational(expr: 'Rational', prec: int, options: OPT_DICT) -> TMP_RES: + return from_rational(expr.p, expr.q, prec), None, prec, None + + +def evalf_integer(expr: 'Integer', prec: int, options: OPT_DICT) -> TMP_RES: + return from_int(expr.p, prec), None, prec, None + +#----------------------------------------------------------------------------# +# # +# Arithmetic operations # +# # +#----------------------------------------------------------------------------# + + +def add_terms(terms: list, prec: int, target_prec: int) -> \ + tuple[MPF_TUP | SCALED_ZERO_TUP | None, int | None]: + """ + Helper for evalf_add. Adds a list of (mpfval, accuracy) terms. + + Returns + ======= + + - None, None if there are no non-zero terms; + - terms[0] if there is only 1 term; + - scaled_zero if the sum of the terms produces a zero by cancellation + e.g. mpfs representing 1 and -1 would produce a scaled zero which need + special handling since they are not actually zero and they are purposely + malformed to ensure that they cannot be used in anything but accuracy + calculations; + - a tuple that is scaled to target_prec that corresponds to the + sum of the terms. + + The returned mpf tuple will be normalized to target_prec; the input + prec is used to define the working precision. + + XXX explain why this is needed and why one cannot just loop using mpf_add + """ + + terms = [t for t in terms if not iszero(t[0])] + if not terms: + return None, None + elif len(terms) == 1: + return terms[0] + + # see if any argument is NaN or oo and thus warrants a special return + special = [] + from .numbers import Float + for t in terms: + arg = Float._new(t[0], 1) + if arg is S.NaN or arg.is_infinite: + special.append(arg) + if special: + from .add import Add + rv = evalf(Add(*special), prec + 4, {}) + return rv[0], rv[2] + + working_prec = 2*prec + sum_man, sum_exp = 0, 0 + absolute_err: list[int] = [] + + for x, accuracy in terms: + sign, man, exp, bc = x + if sign: + man = -man + absolute_err.append(bc + exp - accuracy) + delta = exp - sum_exp + if exp >= sum_exp: + # x much larger than existing sum? + # first: quick test + if ((delta > working_prec) and + ((not sum_man) or + delta - bitcount(abs(sum_man)) > working_prec)): + sum_man = man + sum_exp = exp + else: + sum_man += (man << delta) + else: + delta = -delta + # x much smaller than existing sum? + if delta - bc > working_prec: + if not sum_man: + sum_man, sum_exp = man, exp + else: + sum_man = (sum_man << delta) + man + sum_exp = exp + absolute_error = max(absolute_err) + if not sum_man: + return scaled_zero(absolute_error) + if sum_man < 0: + sum_sign = 1 + sum_man = -sum_man + else: + sum_sign = 0 + sum_bc = bitcount(sum_man) + sum_accuracy = sum_exp + sum_bc - absolute_error + r = normalize(sum_sign, sum_man, sum_exp, sum_bc, target_prec, + rnd), sum_accuracy + return r + + +def evalf_add(v: 'Add', prec: int, options: OPT_DICT) -> TMP_RES: + res = pure_complex(v) + if res: + h, c = res + re, _, re_acc, _ = evalf(h, prec, options) + im, _, im_acc, _ = evalf(c, prec, options) + return re, im, re_acc, im_acc + + oldmaxprec = options.get('maxprec', DEFAULT_MAXPREC) + + i = 0 + target_prec = prec + while 1: + options['maxprec'] = min(oldmaxprec, 2*prec) + + terms = [evalf(arg, prec + 10, options) for arg in v.args] + n = terms.count(S.ComplexInfinity) + if n >= 2: + return fnan, None, prec, None + re, re_acc = add_terms( + [a[0::2] for a in terms if isinstance(a, tuple) and a[0]], prec, target_prec) + im, im_acc = add_terms( + [a[1::2] for a in terms if isinstance(a, tuple) and a[1]], prec, target_prec) + if n == 1: + if re in (finf, fninf, fnan) or im in (finf, fninf, fnan): + return fnan, None, prec, None + return S.ComplexInfinity + acc = complex_accuracy((re, im, re_acc, im_acc)) + if acc >= target_prec: + if options.get('verbose'): + print("ADD: wanted", target_prec, "accurate bits, got", re_acc, im_acc) + break + else: + if (prec - target_prec) > options['maxprec']: + break + + prec = prec + max(10 + 2**i, target_prec - acc) + i += 1 + if options.get('verbose'): + print("ADD: restarting with prec", prec) + + options['maxprec'] = oldmaxprec + if iszero(re, scaled=True): + re = scaled_zero(re) + if iszero(im, scaled=True): + im = scaled_zero(im) + return re, im, re_acc, im_acc + + +def evalf_mul(v: 'Mul', prec: int, options: OPT_DICT) -> TMP_RES: + res = pure_complex(v) + if res: + # the only pure complex that is a mul is h*I + _, h = res + im, _, im_acc, _ = evalf(h, prec, options) + return None, im, None, im_acc + args = list(v.args) + + # see if any argument is NaN or oo and thus warrants a special return + has_zero = False + special = [] + from .numbers import Float + for arg in args: + result = evalf(arg, prec, options) + if result is S.ComplexInfinity: + special.append(result) + continue + if result[0] is None: + if result[1] is None: + has_zero = True + continue + num = Float._new(result[0], 1) + if num is S.NaN: + return fnan, None, prec, None + if num.is_infinite: + special.append(num) + if special: + if has_zero: + return fnan, None, prec, None + from .mul import Mul + return evalf(Mul(*special), prec + 4, {}) + if has_zero: + return None, None, None, None + + # With guard digits, multiplication in the real case does not destroy + # accuracy. This is also true in the complex case when considering the + # total accuracy; however accuracy for the real or imaginary parts + # separately may be lower. + acc = prec + + # XXX: big overestimate + working_prec = prec + len(args) + 5 + + # Empty product is 1 + start = man, exp, bc = MPZ(1), 0, 1 + + # First, we multiply all pure real or pure imaginary numbers. + # direction tells us that the result should be multiplied by + # I**direction; all other numbers get put into complex_factors + # to be multiplied out after the first phase. + last = len(args) + direction = 0 + args.append(S.One) + complex_factors = [] + + for i, arg in enumerate(args): + if i != last and pure_complex(arg): + args[-1] = (args[-1]*arg).expand() + continue + elif i == last and arg is S.One: + continue + re, im, re_acc, im_acc = evalf(arg, working_prec, options) + if re and im: + complex_factors.append((re, im, re_acc, im_acc)) + continue + elif re: + (s, m, e, b), w_acc = re, re_acc + elif im: + (s, m, e, b), w_acc = im, im_acc + direction += 1 + else: + return None, None, None, None + direction += 2*s + man *= m + exp += e + bc += b + while bc > 3*working_prec: + man >>= working_prec + exp += working_prec + bc -= working_prec + acc = min(acc, w_acc) + sign = (direction & 2) >> 1 + if not complex_factors: + v = normalize(sign, man, exp, bitcount(man), prec, rnd) + # multiply by i + if direction & 1: + return None, v, None, acc + else: + return v, None, acc, None + else: + # initialize with the first term + if (man, exp, bc) != start: + # there was a real part; give it an imaginary part + re, im = (sign, man, exp, bitcount(man)), (0, MPZ(0), 0, 0) + i0 = 0 + else: + # there is no real part to start (other than the starting 1) + wre, wim, wre_acc, wim_acc = complex_factors[0] + acc = min(acc, + complex_accuracy((wre, wim, wre_acc, wim_acc))) + re = wre + im = wim + i0 = 1 + + for wre, wim, wre_acc, wim_acc in complex_factors[i0:]: + # acc is the overall accuracy of the product; we aren't + # computing exact accuracies of the product. + acc = min(acc, + complex_accuracy((wre, wim, wre_acc, wim_acc))) + + use_prec = working_prec + A = mpf_mul(re, wre, use_prec) + B = mpf_mul(mpf_neg(im), wim, use_prec) + C = mpf_mul(re, wim, use_prec) + D = mpf_mul(im, wre, use_prec) + re = mpf_add(A, B, use_prec) + im = mpf_add(C, D, use_prec) + if options.get('verbose'): + print("MUL: wanted", prec, "accurate bits, got", acc) + # multiply by I + if direction & 1: + re, im = mpf_neg(im), re + return re, im, acc, acc + + +def evalf_pow(v: 'Pow', prec: int, options) -> TMP_RES: + + target_prec = prec + base, exp = v.args + + # We handle x**n separately. This has two purposes: 1) it is much + # faster, because we avoid calling evalf on the exponent, and 2) it + # allows better handling of real/imaginary parts that are exactly zero + if exp.is_Integer: + p: int = exp.p # type: ignore + # Exact + if not p: + return fone, None, prec, None + # Exponentiation by p magnifies relative error by |p|, so the + # base must be evaluated with increased precision if p is large + prec += int(math.log2(abs(p))) + result = evalf(base, prec + 5, options) + if result is S.ComplexInfinity: + if p < 0: + return None, None, None, None + return result + re, im, re_acc, im_acc = result + # Real to integer power + if re and not im: + return mpf_pow_int(re, p, target_prec), None, target_prec, None + # (x*I)**n = I**n * x**n + if im and not re: + z = mpf_pow_int(im, p, target_prec) + case = p % 4 + if case == 0: + return z, None, target_prec, None + if case == 1: + return None, z, None, target_prec + if case == 2: + return mpf_neg(z), None, target_prec, None + if case == 3: + return None, mpf_neg(z), None, target_prec + # Zero raised to an integer power + if not re: + if p < 0: + return S.ComplexInfinity + return None, None, None, None + # General complex number to arbitrary integer power + re, im = libmp.mpc_pow_int((re, im), p, prec) + # Assumes full accuracy in input + return finalize_complex(re, im, target_prec) + + result = evalf(base, prec + 5, options) + if result is S.ComplexInfinity: + if exp.is_Rational: + if exp < 0: + return None, None, None, None + return result + raise NotImplementedError + + # Pure square root + if exp is S.Half: + xre, xim, _, _ = result + # General complex square root + if xim: + re, im = libmp.mpc_sqrt((xre or fzero, xim), prec) + return finalize_complex(re, im, prec) + if not xre: + return None, None, None, None + # Square root of a negative real number + if mpf_lt(xre, fzero): + return None, mpf_sqrt(mpf_neg(xre), prec), None, prec + # Positive square root + return mpf_sqrt(xre, prec), None, prec, None + + # We first evaluate the exponent to find its magnitude + # This determines the working precision that must be used + prec += 10 + result = evalf(exp, prec, options) + if result is S.ComplexInfinity: + return fnan, None, prec, None + yre, yim, _, _ = result + # Special cases: x**0 + if not (yre or yim): + return fone, None, prec, None + + ysize = fastlog(yre) + # Restart if too big + # XXX: prec + ysize might exceed maxprec + if ysize > 5: + prec += ysize + yre, yim, _, _ = evalf(exp, prec, options) + + # Pure exponential function; no need to evalf the base + if base is S.Exp1: + if yim: + re, im = libmp.mpc_exp((yre or fzero, yim), prec) + return finalize_complex(re, im, target_prec) + return mpf_exp(yre, target_prec), None, target_prec, None + + xre, xim, _, _ = evalf(base, prec + 5, options) + # 0**y + if not (xre or xim): + if yim: + return fnan, None, prec, None + if yre[0] == 1: # y < 0 + return S.ComplexInfinity + return None, None, None, None + + # (real ** complex) or (complex ** complex) + if yim: + re, im = libmp.mpc_pow( + (xre or fzero, xim or fzero), (yre or fzero, yim), + target_prec) + return finalize_complex(re, im, target_prec) + # complex ** real + if xim: + re, im = libmp.mpc_pow_mpf((xre or fzero, xim), yre, target_prec) + return finalize_complex(re, im, target_prec) + # negative ** real + elif mpf_lt(xre, fzero): + re, im = libmp.mpc_pow_mpf((xre, fzero), yre, target_prec) + return finalize_complex(re, im, target_prec) + # positive ** real + else: + return mpf_pow(xre, yre, target_prec), None, target_prec, None + + +#----------------------------------------------------------------------------# +# # +# Special functions # +# # +#----------------------------------------------------------------------------# + + +def evalf_exp(expr: 'exp', prec: int, options: OPT_DICT) -> TMP_RES: + from .power import Pow + return evalf_pow(Pow(S.Exp1, expr.exp, evaluate=False), prec, options) + + +def evalf_trig(v: Expr, prec: int, options: OPT_DICT) -> TMP_RES: + """ + This function handles sin , cos and tan of complex arguments. + + """ + from sympy.functions.elementary.trigonometric import cos, sin, tan + if isinstance(v, cos): + func = mpf_cos + elif isinstance(v, sin): + func = mpf_sin + elif isinstance(v,tan): + func = mpf_tan + else: + raise NotImplementedError + arg = v.args[0] + # 20 extra bits is possibly overkill. It does make the need + # to restart very unlikely + xprec = prec + 20 + re, im, re_acc, im_acc = evalf(arg, xprec, options) + if im: + if 'subs' in options: + v = v.subs(options['subs']) + return evalf(v._eval_evalf(prec), prec, options) + if not re: + if isinstance(v, cos): + return fone, None, prec, None + elif isinstance(v, sin): + return None, None, None, None + elif isinstance(v,tan): + return None, None, None, None + else: + raise NotImplementedError + # For trigonometric functions, we are interested in the + # fixed-point (absolute) accuracy of the argument. + xsize = fastlog(re) + # Magnitude <= 1.0. OK to compute directly, because there is no + # danger of hitting the first root of cos (with sin, magnitude + # <= 2.0 would actually be ok) + if xsize < 1: + return func(re, prec, rnd), None, prec, None + # Very large + if xsize >= 10: + xprec = prec + xsize + re, im, re_acc, im_acc = evalf(arg, xprec, options) + # Need to repeat in case the argument is very close to a + # multiple of pi (or pi/2), hitting close to a root + while 1: + y = func(re, prec, rnd) + ysize = fastlog(y) + gap = -ysize + accuracy = (xprec - xsize) - gap + if accuracy < prec: + if options.get('verbose'): + print("SIN/COS/TAN", accuracy, "wanted", prec, "gap", gap) + print(to_str(y, 10)) + if xprec > options.get('maxprec', DEFAULT_MAXPREC): + return y, None, accuracy, None + xprec += gap + re, im, re_acc, im_acc = evalf(arg, xprec, options) + continue + else: + return y, None, prec, None + + +def evalf_log(expr: 'log', prec: int, options: OPT_DICT) -> TMP_RES: + if len(expr.args)>1: + expr = expr.doit() + return evalf(expr, prec, options) + arg = expr.args[0] + workprec = prec + 10 + result = evalf(arg, workprec, options) + if result is S.ComplexInfinity: + return result + xre, xim, xacc, _ = result + + # evalf can return NoneTypes if chop=True + # issue 18516, 19623 + if xre is xim is None: + # Dear reviewer, I do not know what -inf is; + # it looks to be (1, 0, -789, -3) + # but I'm not sure in general, + # so we just let mpmath figure + # it out by taking log of 0 directly. + # It would be better to return -inf instead. + xre = fzero + + if xim: + from sympy.functions.elementary.complexes import Abs + from sympy.functions.elementary.exponential import log + + # XXX: use get_abs etc instead + re = evalf_log( + log(Abs(arg, evaluate=False), evaluate=False), prec, options) + im = mpf_atan2(xim, xre or fzero, prec) + return re[0], im, re[2], prec + + imaginary_term = (mpf_cmp(xre, fzero) < 0) + + re = mpf_log(mpf_abs(xre), prec, rnd) + size = fastlog(re) + if prec - size > workprec and re != fzero: + from .add import Add + # We actually need to compute 1+x accurately, not x + add = Add(S.NegativeOne, arg, evaluate=False) + xre, xim, _, _ = evalf_add(add, prec, options) + prec2 = workprec - fastlog(xre) + # xre is now x - 1 so we add 1 back here to calculate x + re = mpf_log(mpf_abs(mpf_add(xre, fone, prec2)), prec, rnd) + + re_acc = prec + + if imaginary_term: + return re, mpf_pi(prec), re_acc, prec + else: + return re, None, re_acc, None + + +def evalf_atan(v: 'atan', prec: int, options: OPT_DICT) -> TMP_RES: + arg = v.args[0] + xre, xim, reacc, imacc = evalf(arg, prec + 5, options) + if xre is xim is None: + return (None,)*4 + if xim: + raise NotImplementedError + return mpf_atan(xre, prec, rnd), None, prec, None + + +def evalf_subs(prec: int, subs: dict) -> dict: + """ Change all Float entries in `subs` to have precision prec. """ + newsubs = {} + for a, b in subs.items(): + b = S(b) + if b.is_Float: + b = b._eval_evalf(prec) + newsubs[a] = b + return newsubs + + +def evalf_piecewise(expr: Expr, prec: int, options: OPT_DICT) -> TMP_RES: + from .numbers import Float, Integer + if 'subs' in options: + expr = expr.subs(evalf_subs(prec, options['subs'])) + newopts = options.copy() + del newopts['subs'] + if hasattr(expr, 'func'): + return evalf(expr, prec, newopts) + if isinstance(expr, float): + return evalf(Float(expr), prec, newopts) + if isinstance(expr, int): + return evalf(Integer(expr), prec, newopts) + + # We still have undefined symbols + raise NotImplementedError + + +def evalf_alg_num(a: 'AlgebraicNumber', prec: int, options: OPT_DICT) -> TMP_RES: + return evalf(a.to_root(), prec, options) + +#----------------------------------------------------------------------------# +# # +# High-level operations # +# # +#----------------------------------------------------------------------------# + + +def as_mpmath(x: Any, prec: int, options: OPT_DICT) -> mpc | mpf: + from .numbers import Infinity, NegativeInfinity, Zero + x = sympify(x) + if isinstance(x, Zero) or x == 0.0: + return mpf(0) + if isinstance(x, Infinity): + return mpf('inf') + if isinstance(x, NegativeInfinity): + return mpf('-inf') + # XXX + result = evalf(x, prec, options) + return quad_to_mpmath(result) + + +def do_integral(expr: 'Integral', prec: int, options: OPT_DICT) -> TMP_RES: + func = expr.args[0] + x, xlow, xhigh = expr.args[1] + if xlow == xhigh: + xlow = xhigh = 0 + elif x not in func.free_symbols: + # only the difference in limits matters in this case + # so if there is a symbol in common that will cancel + # out when taking the difference, then use that + # difference + if xhigh.free_symbols & xlow.free_symbols: + diff = xhigh - xlow + if diff.is_number: + xlow, xhigh = 0, diff + + oldmaxprec = options.get('maxprec', DEFAULT_MAXPREC) + options['maxprec'] = min(oldmaxprec, 2*prec) + + with workprec(prec + 5): + xlow = as_mpmath(xlow, prec + 15, options) + xhigh = as_mpmath(xhigh, prec + 15, options) + + # Integration is like summation, and we can phone home from + # the integrand function to update accuracy summation style + # Note that this accuracy is inaccurate, since it fails + # to account for the variable quadrature weights, + # but it is better than nothing + + from sympy.functions.elementary.trigonometric import cos, sin + from .symbol import Wild + + have_part = [False, False] + max_real_term: float | int = MINUS_INF + max_imag_term: float | int = MINUS_INF + + def f(t: Expr) -> mpc | mpf: + nonlocal max_real_term, max_imag_term + re, im, re_acc, im_acc = evalf(func, mp.prec, {'subs': {x: t}}) + + have_part[0] = re or have_part[0] + have_part[1] = im or have_part[1] + + max_real_term = max(max_real_term, fastlog(re)) + max_imag_term = max(max_imag_term, fastlog(im)) + + if im: + return mpc(re or fzero, im) + return mpf(re or fzero) + + if options.get('quad') == 'osc': + A = Wild('A', exclude=[x]) + B = Wild('B', exclude=[x]) + D = Wild('D') + m = func.match(cos(A*x + B)*D) + if not m: + m = func.match(sin(A*x + B)*D) + if not m: + raise ValueError("An integrand of the form sin(A*x+B)*f(x) " + "or cos(A*x+B)*f(x) is required for oscillatory quadrature") + period = as_mpmath(2*S.Pi/m[A], prec + 15, options) + result = quadosc(f, [xlow, xhigh], period=period) + # XXX: quadosc does not do error detection yet + quadrature_error = MINUS_INF + else: + result, quadrature_err = quadts(f, [xlow, xhigh], error=1) + quadrature_error = fastlog(quadrature_err._mpf_) + + options['maxprec'] = oldmaxprec + + if have_part[0]: + re: MPF_TUP | None = result.real._mpf_ + re_acc: int | None + if re == fzero: + re_s, re_acc = scaled_zero(int(-max(prec, max_real_term, quadrature_error))) + re = scaled_zero(re_s) # handled ok in evalf_integral + else: + re_acc = int(-max(max_real_term - fastlog(re) - prec, quadrature_error)) + else: + re, re_acc = None, None + + if have_part[1]: + im: MPF_TUP | None = result.imag._mpf_ + im_acc: int | None + if im == fzero: + im_s, im_acc = scaled_zero(int(-max(prec, max_imag_term, quadrature_error))) + im = scaled_zero(im_s) # handled ok in evalf_integral + else: + im_acc = int(-max(max_imag_term - fastlog(im) - prec, quadrature_error)) + else: + im, im_acc = None, None + + result = re, im, re_acc, im_acc + return result + + +def evalf_integral(expr: 'Integral', prec: int, options: OPT_DICT) -> TMP_RES: + limits = expr.limits + if len(limits) != 1 or len(limits[0]) != 3: + raise NotImplementedError + workprec = prec + i = 0 + maxprec = options.get('maxprec', INF) + while 1: + result = do_integral(expr, workprec, options) + accuracy = complex_accuracy(result) + if accuracy >= prec: # achieved desired precision + break + if workprec >= maxprec: # can't increase accuracy any more + break + if accuracy == -1: + # maybe the answer really is zero and maybe we just haven't increased + # the precision enough. So increase by doubling to not take too long + # to get to maxprec. + workprec *= 2 + else: + workprec += max(prec, 2**i) + workprec = min(workprec, maxprec) + i += 1 + return result + + +def check_convergence(numer: Expr, denom: Expr, n: Symbol) -> tuple[int, Any, Any]: + """ + Returns + ======= + + (h, g, p) where + -- h is: + > 0 for convergence of rate 1/factorial(n)**h + < 0 for divergence of rate factorial(n)**(-h) + = 0 for geometric or polynomial convergence or divergence + + -- abs(g) is: + > 1 for geometric convergence of rate 1/h**n + < 1 for geometric divergence of rate h**n + = 1 for polynomial convergence or divergence + + (g < 0 indicates an alternating series) + + -- p is: + > 1 for polynomial convergence of rate 1/n**h + <= 1 for polynomial divergence of rate n**(-h) + + """ + from sympy.polys.polytools import Poly + npol = Poly(numer, n) + dpol = Poly(denom, n) + p = npol.degree() + q = dpol.degree() + rate = q - p + if rate: + return rate, None, None + constant = dpol.LC() / npol.LC() + from .numbers import equal_valued + if not equal_valued(abs(constant), 1): + return rate, constant, None + if npol.degree() == dpol.degree() == 0: + return rate, constant, 0 + pc = npol.all_coeffs()[1] + qc = dpol.all_coeffs()[1] + return rate, constant, (qc - pc)/dpol.LC() + + +def hypsum(expr: Expr, n: Symbol, start: int, prec: int) -> mpf: + """ + Sum a rapidly convergent infinite hypergeometric series with + given general term, e.g. e = hypsum(1/factorial(n), n). The + quotient between successive terms must be a quotient of integer + polynomials. + """ + from .numbers import Float, equal_valued + from sympy.simplify.simplify import hypersimp + + if prec == float('inf'): + raise NotImplementedError('does not support inf prec') + + if start: + expr = expr.subs(n, n + start) + hs = hypersimp(expr, n) + if hs is None: + raise NotImplementedError("a hypergeometric series is required") + num, den = hs.as_numer_denom() + + func1 = lambdify(n, num) + func2 = lambdify(n, den) + + h, g, p = check_convergence(num, den, n) + + if h < 0: + raise ValueError("Sum diverges like (n!)^%i" % (-h)) + + eterm = expr.subs(n, 0) + if not eterm.is_Rational: + raise NotImplementedError("Non rational term functionality is not implemented.") + + term: Rational = eterm # type: ignore + + # Direct summation if geometric or faster + if h > 0 or (h == 0 and abs(g) > 1): + term = (MPZ(term.p) << prec) // term.q + s = term + k = 1 + while abs(term) > 5: + term *= MPZ(func1(k - 1)) + term //= MPZ(func2(k - 1)) + s += term + k += 1 + return from_man_exp(s, -prec) + else: + alt = g < 0 + if abs(g) < 1: + raise ValueError("Sum diverges like (%i)^n" % abs(1/g)) + if p < 1 or (equal_valued(p, 1) and not alt): + raise ValueError("Sum diverges like n^%i" % (-p)) + # We have polynomial convergence: use Richardson extrapolation + vold = None + ndig = prec_to_dps(prec) + while True: + # Need to use at least quad precision because a lot of cancellation + # might occur in the extrapolation process; we check the answer to + # make sure that the desired precision has been reached, too. + prec2 = 4*prec + term0 = (MPZ(term.p) << prec2) // term.q + + def summand(k, _term=[term0]): + if k: + k = int(k) + _term[0] *= MPZ(func1(k - 1)) + _term[0] //= MPZ(func2(k - 1)) + return make_mpf(from_man_exp(_term[0], -prec2)) + + with workprec(prec): + v = nsum(summand, [0, mpmath_inf], method='richardson') + vf = Float(v, ndig) + if vold is not None and vold == vf: + break + prec += prec # double precision each time + vold = vf + + return v._mpf_ + + +def evalf_prod(expr: 'Product', prec: int, options: OPT_DICT) -> TMP_RES: + if all((l[1] - l[2]).is_Integer for l in expr.limits): + result = evalf(expr.doit(), prec=prec, options=options) + else: + from sympy.concrete.summations import Sum + result = evalf(expr.rewrite(Sum), prec=prec, options=options) + return result + + +def evalf_sum(expr: 'Sum', prec: int, options: OPT_DICT) -> TMP_RES: + from .numbers import Float + if 'subs' in options: + expr = expr.subs(options['subs']) # type: ignore + func = expr.function + limits = expr.limits + if len(limits) != 1 or len(limits[0]) != 3: + raise NotImplementedError + if func.is_zero: + return None, None, prec, None + prec2 = prec + 10 + try: + n, a, b = limits[0] + if b is not S.Infinity or a is S.NegativeInfinity or a != int(a): + raise NotImplementedError + # Use fast hypergeometric summation if possible + v = hypsum(func, n, int(a), prec2) + delta = prec - fastlog(v) + if fastlog(v) < -10: + v = hypsum(func, n, int(a), delta) + return v, None, min(prec, delta), None + except NotImplementedError: + # Euler-Maclaurin summation for general series + eps = Float(2.0)**(-prec) + for i in range(1, 5): + m = n = 2**i * prec + s, err = expr.euler_maclaurin(m=m, n=n, eps=eps, + eval_integral=False) + err = err.evalf() + if err is S.NaN: + raise NotImplementedError + if err <= eps: + break + err = fastlog(evalf(abs(err), 20, options)[0]) + re, im, re_acc, im_acc = evalf(s, prec2, options) + if re_acc is None: + re_acc = -err + if im_acc is None: + im_acc = -err + return re, im, re_acc, im_acc + + +#----------------------------------------------------------------------------# +# # +# Symbolic interface # +# # +#----------------------------------------------------------------------------# + +def evalf_symbol(x: Expr, prec: int, options: OPT_DICT) -> TMP_RES: + val = options['subs'][x] + if isinstance(val, mpf): + if not val: + return None, None, None, None + return val._mpf_, None, prec, None + else: + if '_cache' not in options: + options['_cache'] = {} + cache = options['_cache'] + cached, cached_prec = cache.get(x, (None, MINUS_INF)) + if cached_prec >= prec: + return cached + v = evalf(sympify(val), prec, options) + cache[x] = (v, prec) + return v + +evalf_table: dict[Type[Expr], Callable[[Expr, int, OPT_DICT], TMP_RES]] = {} + + +def _create_evalf_table(): + global evalf_table + from sympy.concrete.products import Product + from sympy.concrete.summations import Sum + from .add import Add + from .mul import Mul + from .numbers import Exp1, Float, Half, ImaginaryUnit, Integer, NaN, NegativeOne, One, Pi, Rational, \ + Zero, ComplexInfinity, AlgebraicNumber + from .power import Pow + from .symbol import Dummy, Symbol + from sympy.functions.elementary.complexes import Abs, im, re + from sympy.functions.elementary.exponential import exp, log + from sympy.functions.elementary.integers import ceiling, floor + from sympy.functions.elementary.piecewise import Piecewise + from sympy.functions.elementary.trigonometric import atan, cos, sin, tan + from sympy.integrals.integrals import Integral + evalf_table = { + Symbol: evalf_symbol, + Dummy: evalf_symbol, + Float: evalf_float, + Rational: evalf_rational, + Integer: evalf_integer, + Zero: lambda x, prec, options: (None, None, prec, None), + One: lambda x, prec, options: (fone, None, prec, None), + Half: lambda x, prec, options: (fhalf, None, prec, None), + Pi: lambda x, prec, options: (mpf_pi(prec), None, prec, None), + Exp1: lambda x, prec, options: (mpf_e(prec), None, prec, None), + ImaginaryUnit: lambda x, prec, options: (None, fone, None, prec), + NegativeOne: lambda x, prec, options: (fnone, None, prec, None), + ComplexInfinity: lambda x, prec, options: S.ComplexInfinity, + NaN: lambda x, prec, options: (fnan, None, prec, None), + + exp: evalf_exp, + + cos: evalf_trig, + sin: evalf_trig, + tan: evalf_trig, + + Add: evalf_add, + Mul: evalf_mul, + Pow: evalf_pow, + + log: evalf_log, + atan: evalf_atan, + Abs: evalf_abs, + + re: evalf_re, + im: evalf_im, + floor: evalf_floor, + ceiling: evalf_ceiling, + + Integral: evalf_integral, + Sum: evalf_sum, + Product: evalf_prod, + Piecewise: evalf_piecewise, + + AlgebraicNumber: evalf_alg_num, + } + + +def evalf(x: Expr, prec: int, options: OPT_DICT) -> TMP_RES: + """ + Evaluate the ``Expr`` instance, ``x`` + to a binary precision of ``prec``. This + function is supposed to be used internally. + + Parameters + ========== + + x : Expr + The formula to evaluate to a float. + prec : int + The binary precision that the output should have. + options : dict + A dictionary with the same entries as + ``EvalfMixin.evalf`` and in addition, + ``maxprec`` which is the maximum working precision. + + Returns + ======= + + An optional tuple, ``(re, im, re_acc, im_acc)`` + which are the real, imaginary, real accuracy + and imaginary accuracy respectively. ``re`` is + an mpf value tuple and so is ``im``. ``re_acc`` + and ``im_acc`` are ints. + + NB: all these return values can be ``None``. + If all values are ``None``, then that represents 0. + Note that 0 is also represented as ``fzero = (0, 0, 0, 0)``. + """ + from sympy.functions.elementary.complexes import re as re_, im as im_ + try: + rf = evalf_table[type(x)] + r = rf(x, prec, options) + except KeyError: + # Fall back to ordinary evalf if possible + if 'subs' in options: + x = x.subs(evalf_subs(prec, options['subs'])) + xe = x._eval_evalf(prec) + if xe is None: + raise NotImplementedError + as_real_imag = getattr(xe, "as_real_imag", None) + if as_real_imag is None: + raise NotImplementedError # e.g. FiniteSet(-1.0, 1.0).evalf() + re, im = as_real_imag() + if re.has(re_) or im.has(im_): + raise NotImplementedError + if not re: + re = None + reprec = None + elif re.is_number: + re = re._to_mpmath(prec, allow_ints=False)._mpf_ + reprec = prec + else: + raise NotImplementedError + if not im: + im = None + imprec = None + elif im.is_number: + im = im._to_mpmath(prec, allow_ints=False)._mpf_ + imprec = prec + else: + raise NotImplementedError + r = re, im, reprec, imprec + + if options.get("verbose"): + print("### input", x) + print("### output", to_str(r[0] or fzero, 50) if isinstance(r, tuple) else r) + print("### raw", r) # r[0], r[2] + print() + chop = options.get('chop', False) + if chop: + if chop is True: + chop_prec = prec + else: + # convert (approximately) from given tolerance; + # the formula here will will make 1e-i rounds to 0 for + # i in the range +/-27 while 2e-i will not be chopped + chop_prec = int(round(-3.321*math.log10(chop) + 2.5)) + if chop_prec == 3: + chop_prec -= 1 + r = chop_parts(r, chop_prec) + if options.get("strict"): + check_target(x, r, prec) + return r + + +def quad_to_mpmath(q, ctx=None): + """Turn the quad returned by ``evalf`` into an ``mpf`` or ``mpc``. """ + mpc = make_mpc if ctx is None else ctx.make_mpc + mpf = make_mpf if ctx is None else ctx.make_mpf + if q is S.ComplexInfinity: + raise NotImplementedError + re, im, _, _ = q + if im: + if not re: + re = fzero + return mpc((re, im)) + elif re: + return mpf(re) + else: + return mpf(fzero) + + +class EvalfMixin: + """Mixin class adding evalf capability.""" + + __slots__: tuple[str, ...] = () + + def evalf(self, n=15, subs=None, maxn=100, chop=False, strict=False, quad=None, verbose=False): + """ + Evaluate the given formula to an accuracy of *n* digits. + + Parameters + ========== + + subs : dict, optional + Substitute numerical values for symbols, e.g. + ``subs={x:3, y:1+pi}``. The substitutions must be given as a + dictionary. + + maxn : int, optional + Allow a maximum temporary working precision of maxn digits. + + chop : bool or number, optional + Specifies how to replace tiny real or imaginary parts in + subresults by exact zeros. + + When ``True`` the chop value defaults to standard precision. + + Otherwise the chop value is used to determine the + magnitude of "small" for purposes of chopping. + + >>> from sympy import N + >>> x = 1e-4 + >>> N(x, chop=True) + 0.000100000000000000 + >>> N(x, chop=1e-5) + 0.000100000000000000 + >>> N(x, chop=1e-4) + 0 + + strict : bool, optional + Raise ``PrecisionExhausted`` if any subresult fails to + evaluate to full accuracy, given the available maxprec. + + quad : str, optional + Choose algorithm for numerical quadrature. By default, + tanh-sinh quadrature is used. For oscillatory + integrals on an infinite interval, try ``quad='osc'``. + + verbose : bool, optional + Print debug information. + + Notes + ===== + + When Floats are naively substituted into an expression, + precision errors may adversely affect the result. For example, + adding 1e16 (a Float) to 1 will truncate to 1e16; if 1e16 is + then subtracted, the result will be 0. + That is exactly what happens in the following: + + >>> from sympy.abc import x, y, z + >>> values = {x: 1e16, y: 1, z: 1e16} + >>> (x + y - z).subs(values) + 0 + + Using the subs argument for evalf is the accurate way to + evaluate such an expression: + + >>> (x + y - z).evalf(subs=values) + 1.00000000000000 + """ + from .numbers import Float, Number + n = n if n is not None else 15 + + if subs and is_sequence(subs): + raise TypeError('subs must be given as a dictionary') + + # for sake of sage that doesn't like evalf(1) + if n == 1 and isinstance(self, Number): + from .expr import _mag + rv = self.evalf(2, subs, maxn, chop, strict, quad, verbose) + m = _mag(rv) + rv = rv.round(1 - m) + return rv + + if not evalf_table: + _create_evalf_table() + prec = dps_to_prec(n) + options = {'maxprec': max(prec, int(maxn*LG10)), 'chop': chop, + 'strict': strict, 'verbose': verbose} + if subs is not None: + options['subs'] = subs + if quad is not None: + options['quad'] = quad + try: + result = evalf(self, prec + 4, options) + except NotImplementedError: + # Fall back to the ordinary evalf + if hasattr(self, 'subs') and subs is not None: # issue 20291 + v = self.subs(subs)._eval_evalf(prec) + else: + v = self._eval_evalf(prec) + if v is None: + return self + elif not v.is_number: + return v + try: + # If the result is numerical, normalize it + result = evalf(v, prec, options) + except NotImplementedError: + # Probably contains symbols or unknown functions + return v + if result is S.ComplexInfinity: + return result + re, im, re_acc, im_acc = result + if re is S.NaN or im is S.NaN: + return S.NaN + if re: + p = max(min(prec, re_acc), 1) + re = Float._new(re, p) + else: + re = S.Zero + if im: + p = max(min(prec, im_acc), 1) + im = Float._new(im, p) + return re + im*S.ImaginaryUnit + else: + return re + + n = evalf + + def _evalf(self, prec: int) -> Expr: + """Helper for evalf. Does the same thing but takes binary precision""" + r = self._eval_evalf(prec) + if r is None: + r = self # type: ignore + return r # type: ignore + + def _eval_evalf(self, prec: int) -> Expr | None: + return None + + def _to_mpmath(self, prec, allow_ints=True): + # mpmath functions accept ints as input + errmsg = "cannot convert to mpmath number" + if allow_ints and self.is_Integer: + return self.p + if hasattr(self, '_as_mpf_val'): + return make_mpf(self._as_mpf_val(prec)) + try: + result = evalf(self, prec, {}) + return quad_to_mpmath(result) + except NotImplementedError: + v = self._eval_evalf(prec) + if v is None: + raise ValueError(errmsg) + if v.is_Float: + return make_mpf(v._mpf_) + # Number + Number*I is also fine + re, im = v.as_real_imag() + if allow_ints and re.is_Integer: + re = from_int(re.p) + elif re.is_Float: + re = re._mpf_ + else: + raise ValueError(errmsg) + if allow_ints and im.is_Integer: + im = from_int(im.p) + elif im.is_Float: + im = im._mpf_ + else: + raise ValueError(errmsg) + return make_mpc((re, im)) + + +def N(x, n=15, **options): + r""" + Calls x.evalf(n, \*\*options). + + Explanations + ============ + + Both .n() and N() are equivalent to .evalf(); use the one that you like better. + See also the docstring of .evalf() for information on the options. + + Examples + ======== + + >>> from sympy import Sum, oo, N + >>> from sympy.abc import k + >>> Sum(1/k**k, (k, 1, oo)) + Sum(k**(-k), (k, 1, oo)) + >>> N(_, 4) + 1.291 + + """ + # by using rational=True, any evaluation of a string + # will be done using exact values for the Floats + return sympify(x, rational=True).evalf(n, **options) + + +def _evalf_with_bounded_error(x: Expr, eps: Expr | None = None, + m: int = 0, + options: OPT_DICT | None = None) -> TMP_RES: + """ + Evaluate *x* to within a bounded absolute error. + + Parameters + ========== + + x : Expr + The quantity to be evaluated. + eps : Expr, None, optional (default=None) + Positive real upper bound on the acceptable error. + m : int, optional (default=0) + If *eps* is None, then use 2**(-m) as the upper bound on the error. + options: OPT_DICT + As in the ``evalf`` function. + + Returns + ======= + + A tuple ``(re, im, re_acc, im_acc)``, as returned by ``evalf``. + + See Also + ======== + + evalf + + """ + if eps is not None: + if not (eps.is_Rational or eps.is_Float) or not eps > 0: + raise ValueError("eps must be positive") + r, _, _, _ = evalf(1/eps, 1, {}) + m = fastlog(r) + + c, d, _, _ = evalf(x, 1, {}) + # Note: If x = a + b*I, then |a| <= 2|c| and |b| <= 2|d|, with equality + # only in the zero case. + # If a is non-zero, then |c| = 2**nc for some integer nc, and c has + # bitcount 1. Therefore 2**fastlog(c) = 2**(nc+1) = 2|c| is an upper bound + # on |a|. Likewise for b and d. + nr, ni = fastlog(c), fastlog(d) + n = max(nr, ni) + 1 + # If x is 0, then n is MINUS_INF, and p will be 1. Otherwise, + # n - 1 bits get us past the integer parts of a and b, and +1 accounts for + # the factor of <= sqrt(2) that is |x|/max(|a|, |b|). + p = max(1, m + n + 1) + + options = options or {} + return evalf(x, p, options) diff --git a/venv/lib/python3.11/site-packages/sympy/core/expr.py b/venv/lib/python3.11/site-packages/sympy/core/expr.py new file mode 100644 index 0000000000000000000000000000000000000000..e66ff239a679942f2cc95c3f66af1fc13f7229d9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/expr.py @@ -0,0 +1,4194 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, overload +from collections.abc import Iterable, Mapping +from functools import reduce +import re + +from .sympify import sympify, _sympify +from .basic import Basic, Atom +from .singleton import S +from .evalf import EvalfMixin, pure_complex, DEFAULT_MAXPREC +from .decorators import call_highest_priority, sympify_method_args, sympify_return +from .cache import cacheit +from .logic import fuzzy_or, fuzzy_not +from .intfunc import mod_inverse +from .sorting import default_sort_key +from .kind import NumberKind +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.utilities.misc import as_int, func_name, filldedent +from sympy.utilities.iterables import has_variety, sift +from mpmath.libmp import mpf_log, prec_to_dps +from mpmath.libmp.libintmath import giant_steps + + +if TYPE_CHECKING: + from typing import Any + from typing_extensions import Self + from .numbers import Number + +from collections import defaultdict + + +def _corem(eq, c): # helper for extract_additively + # return co, diff from co*c + diff + co = [] + non = [] + for i in Add.make_args(eq): + ci = i.coeff(c) + if not ci: + non.append(i) + else: + co.append(ci) + return Add(*co), Add(*non) + + +@sympify_method_args +class Expr(Basic, EvalfMixin): + """ + Base class for algebraic expressions. + + Explanation + =========== + + Everything that requires arithmetic operations to be defined + should subclass this class, instead of Basic (which should be + used only for argument storage and expression manipulation, i.e. + pattern matching, substitutions, etc). + + If you want to override the comparisons of expressions: + Should use _eval_is_ge for inequality, or _eval_is_eq, with multiple dispatch. + _eval_is_ge return true if x >= y, false if x < y, and None if the two types + are not comparable or the comparison is indeterminate + + See Also + ======== + + sympy.core.basic.Basic + """ + + __slots__: tuple[str, ...] = () + + if TYPE_CHECKING: + + def __new__(cls, *args: Basic) -> Self: + ... + + @overload # type: ignore + def subs(self, arg1: Mapping[Basic | complex, Expr | complex], arg2: None=None) -> Expr: ... + @overload + def subs(self, arg1: Iterable[tuple[Basic | complex, Expr | complex]], arg2: None=None, **kwargs: Any) -> Expr: ... + @overload + def subs(self, arg1: Expr | complex, arg2: Expr | complex) -> Expr: ... + @overload + def subs(self, arg1: Mapping[Basic | complex, Basic | complex], arg2: None=None, **kwargs: Any) -> Basic: ... + @overload + def subs(self, arg1: Iterable[tuple[Basic | complex, Basic | complex]], arg2: None=None, **kwargs: Any) -> Basic: ... + @overload + def subs(self, arg1: Basic | complex, arg2: Basic | complex, **kwargs: Any) -> Basic: ... + + def subs(self, arg1: Mapping[Basic | complex, Basic | complex] | Basic | complex, # type: ignore + arg2: Basic | complex | None = None, **kwargs: Any) -> Basic: + ... + + def simplify(self, **kwargs) -> Expr: + ... + + def evalf(self, n: int = 15, subs: dict[Basic, Basic | float] | None = None, + maxn: int = 100, chop: bool = False, strict: bool = False, + quad: str | None = None, verbose: bool = False) -> Expr: + ... + + n = evalf + + is_scalar = True # self derivative is 1 + + @property + def _diff_wrt(self): + """Return True if one can differentiate with respect to this + object, else False. + + Explanation + =========== + + Subclasses such as Symbol, Function and Derivative return True + to enable derivatives wrt them. The implementation in Derivative + separates the Symbol and non-Symbol (_diff_wrt=True) variables and + temporarily converts the non-Symbols into Symbols when performing + the differentiation. By default, any object deriving from Expr + will behave like a scalar with self.diff(self) == 1. If this is + not desired then the object must also set `is_scalar = False` or + else define an _eval_derivative routine. + + Note, see the docstring of Derivative for how this should work + mathematically. In particular, note that expr.subs(yourclass, Symbol) + should be well-defined on a structural level, or this will lead to + inconsistent results. + + Examples + ======== + + >>> from sympy import Expr + >>> e = Expr() + >>> e._diff_wrt + False + >>> class MyScalar(Expr): + ... _diff_wrt = True + ... + >>> MyScalar().diff(MyScalar()) + 1 + >>> class MySymbol(Expr): + ... _diff_wrt = True + ... is_scalar = False + ... + >>> MySymbol().diff(MySymbol()) + Derivative(MySymbol(), MySymbol()) + """ + return False + + @cacheit + def sort_key(self, order=None): + + coeff, expr = self.as_coeff_Mul() + + if expr.is_Pow: + base, exp = expr.as_base_exp() + if base is S.Exp1: + # If we remove this, many doctests will go crazy: + # (keeps E**x sorted like the exp(x) function, + # part of exp(x) to E**x transition) + base, exp = Function("exp")(exp), S.One + expr = base + else: + exp = S.One + + if expr.is_Dummy: + args = (expr.sort_key(),) + elif expr.is_Atom: + args = (str(expr),) + else: + if expr.is_Add: + args = expr.as_ordered_terms(order=order) + elif expr.is_Mul: + args = expr.as_ordered_factors(order=order) + else: + args = expr.args + + args = tuple( + [ default_sort_key(arg, order=order) for arg in args ]) + + args = (len(args), tuple(args)) + exp = exp.sort_key(order=order) + + return expr.class_key(), args, exp, coeff + + def _hashable_content(self): + """Return a tuple of information about self that can be used to + compute the hash. If a class defines additional attributes, + like ``name`` in Symbol, then this method should be updated + accordingly to return such relevant attributes. + Defining more than _hashable_content is necessary if __eq__ has + been defined by a class. See note about this in Basic.__eq__.""" + return self._args + + # *************** + # * Arithmetics * + # *************** + # Expr and its subclasses use _op_priority to determine which object + # passed to a binary special method (__mul__, etc.) will handle the + # operation. In general, the 'call_highest_priority' decorator will choose + # the object with the highest _op_priority to handle the call. + # Custom subclasses that want to define their own binary special methods + # should set an _op_priority value that is higher than the default. + # + # **NOTE**: + # This is a temporary fix, and will eventually be replaced with + # something better and more powerful. See issue 5510. + _op_priority = 10.0 + + @property + def _add_handler(self): + return Add + + @property + def _mul_handler(self): + return Mul + + def __pos__(self) -> Expr: + return self + + def __neg__(self) -> Expr: + # Mul has its own __neg__ routine, so we just + # create a 2-args Mul with the -1 in the canonical + # slot 0. + c = self.is_commutative + return Mul._from_args((S.NegativeOne, self), c) + + def __abs__(self) -> Expr: + from sympy.functions.elementary.complexes import Abs + return Abs(self) + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__radd__') + def __add__(self, other) -> Expr: + return Add(self, other) + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__add__') + def __radd__(self, other) -> Expr: + return Add(other, self) + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__rsub__') + def __sub__(self, other) -> Expr: + return Add(self, -other) + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__sub__') + def __rsub__(self, other) -> Expr: + return Add(other, -self) + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__rmul__') + def __mul__(self, other) -> Expr: + return Mul(self, other) + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__mul__') + def __rmul__(self, other) -> Expr: + return Mul(other, self) + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__rpow__') + def _pow(self, other): + return Pow(self, other) + + def __pow__(self, other, mod=None) -> Expr: + if mod is None: + return self._pow(other) + try: + _self, other, mod = as_int(self), as_int(other), as_int(mod) + if other >= 0: + return _sympify(pow(_self, other, mod)) + else: + return _sympify(mod_inverse(pow(_self, -other, mod), mod)) + except ValueError: + power = self._pow(other) + try: + return power%mod + except TypeError: + return NotImplemented + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__pow__') + def __rpow__(self, other) -> Expr: + return Pow(other, self) + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__rtruediv__') + def __truediv__(self, other) -> Expr: + denom = Pow(other, S.NegativeOne) + if self is S.One: + return denom + else: + return Mul(self, denom) + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__truediv__') + def __rtruediv__(self, other) -> Expr: + denom = Pow(self, S.NegativeOne) + if other is S.One: + return denom + else: + return Mul(other, denom) + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__rmod__') + def __mod__(self, other) -> Expr: + return Mod(self, other) + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__mod__') + def __rmod__(self, other) -> Expr: + return Mod(other, self) + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__rfloordiv__') + def __floordiv__(self, other) -> Expr: + from sympy.functions.elementary.integers import floor + return floor(self / other) + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__floordiv__') + def __rfloordiv__(self, other) -> Expr: + from sympy.functions.elementary.integers import floor + return floor(other / self) + + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__rdivmod__') + def __divmod__(self, other) -> tuple[Expr, Expr]: + from sympy.functions.elementary.integers import floor + return floor(self / other), Mod(self, other) + + @sympify_return([('other', 'Expr')], NotImplemented) + @call_highest_priority('__divmod__') + def __rdivmod__(self, other) -> tuple[Expr, Expr]: + from sympy.functions.elementary.integers import floor + return floor(other / self), Mod(other, self) + + def __int__(self) -> int: + if not self.is_number: + raise TypeError("Cannot convert symbols to int") + r = self.round(2) + if not r.is_Number: + raise TypeError("Cannot convert complex to int") + if r in (S.NaN, S.Infinity, S.NegativeInfinity): + raise TypeError("Cannot convert %s to int" % r) + i = int(r) + if not i: + return i + if int_valued(r): + # non-integer self should pass one of these tests + if (self > i) is S.true: + return i + if (self < i) is S.true: + return i - 1 + ok = self.equals(i) + if ok is None: + raise TypeError('cannot compute int value accurately') + if ok: + return i + # off by one + return i - (1 if i > 0 else -1) + return i + + def __float__(self) -> float: + # Don't bother testing if it's a number; if it's not this is going + # to fail, and if it is we still need to check that it evalf'ed to + # a number. + result = self.evalf() + if result.is_Number: + return float(result) + if result.is_number and result.as_real_imag()[1]: + raise TypeError("Cannot convert complex to float") + raise TypeError("Cannot convert expression to float") + + def __complex__(self) -> complex: + result = self.evalf() + re, im = result.as_real_imag() + return complex(float(re), float(im)) + + @sympify_return([('other', 'Expr')], NotImplemented) + def __ge__(self, other): + from .relational import GreaterThan + return GreaterThan(self, other) + + @sympify_return([('other', 'Expr')], NotImplemented) + def __le__(self, other): + from .relational import LessThan + return LessThan(self, other) + + @sympify_return([('other', 'Expr')], NotImplemented) + def __gt__(self, other): + from .relational import StrictGreaterThan + return StrictGreaterThan(self, other) + + @sympify_return([('other', 'Expr')], NotImplemented) + def __lt__(self, other): + from .relational import StrictLessThan + return StrictLessThan(self, other) + + def __trunc__(self): + if not self.is_number: + raise TypeError("Cannot truncate symbols and expressions") + else: + return Integer(self) + + def __format__(self, format_spec: str): + if self.is_number: + mt = re.match(r'\+?\d*\.(\d+)f', format_spec) + if mt: + prec = int(mt.group(1)) + rounded = self.round(prec) + if rounded.is_Integer: + return format(int(rounded), format_spec) + if rounded.is_Float: + return format(rounded, format_spec) + return super().__format__(format_spec) + + @staticmethod + def _from_mpmath(x, prec): + if hasattr(x, "_mpf_"): + return Float._new(x._mpf_, prec) + elif hasattr(x, "_mpc_"): + re, im = x._mpc_ + re = Float._new(re, prec) + im = Float._new(im, prec)*S.ImaginaryUnit + return re + im + else: + raise TypeError("expected mpmath number (mpf or mpc)") + + @property + def is_number(self): + """Returns True if ``self`` has no free symbols and no + undefined functions (AppliedUndef, to be precise). It will be + faster than ``if not self.free_symbols``, however, since + ``is_number`` will fail as soon as it hits a free symbol + or undefined function. + + Examples + ======== + + >>> from sympy import Function, Integral, cos, sin, pi + >>> from sympy.abc import x + >>> f = Function('f') + + >>> x.is_number + False + >>> f(1).is_number + False + >>> (2*x).is_number + False + >>> (2 + Integral(2, x)).is_number + False + >>> (2 + Integral(2, (x, 1, 2))).is_number + True + + Not all numbers are Numbers in the SymPy sense: + + >>> pi.is_number, pi.is_Number + (True, False) + + If something is a number it should evaluate to a number with + real and imaginary parts that are Numbers; the result may not + be comparable, however, since the real and/or imaginary part + of the result may not have precision. + + >>> cos(1).is_number and cos(1).is_comparable + True + + >>> z = cos(1)**2 + sin(1)**2 - 1 + >>> z.is_number + True + >>> z.is_comparable + False + + See Also + ======== + + sympy.core.basic.Basic.is_comparable + """ + return all(obj.is_number for obj in self.args) + + def _eval_is_comparable(self): + # Basic._eval_is_comparable always returns False, so we override it + # here + is_extended_real = self.is_extended_real + if is_extended_real is False: + return False + if not self.is_number: + return False + + # XXX: as_real_imag() can be a very expensive operation. It should not + # be used here because is_comparable is used implicitly in many places. + # Probably this method should just return self.evalf(2).is_Number. + + n, i = self.as_real_imag() + + if not n.is_Number: + n = n.evalf(2) + if not n.is_Number: + return False + + if not i.is_Number: + i = i.evalf(2) + if not i.is_Number: + return False + + if i: + # if _prec = 1 we can't decide and if not, + # the answer is False because numbers with + # imaginary parts can't be compared + # so return False + return False + else: + return n._prec != 1 + + def _random(self, n=None, re_min=-1, im_min=-1, re_max=1, im_max=1): + """Return self evaluated, if possible, replacing free symbols with + random complex values, if necessary. + + Explanation + =========== + + The random complex value for each free symbol is generated + by the random_complex_number routine giving real and imaginary + parts in the range given by the re_min, re_max, im_min, and im_max + values. The returned value is evaluated to a precision of n + (if given) else the maximum of 15 and the precision needed + to get more than 1 digit of precision. If the expression + could not be evaluated to a number, or could not be evaluated + to more than 1 digit of precision, then None is returned. + + Examples + ======== + + >>> from sympy import sqrt + >>> from sympy.abc import x, y + >>> x._random() # doctest: +SKIP + 0.0392918155679172 + 0.916050214307199*I + >>> x._random(2) # doctest: +SKIP + -0.77 - 0.87*I + >>> (x + y/2)._random(2) # doctest: +SKIP + -0.57 + 0.16*I + >>> sqrt(2)._random(2) + 1.4 + + See Also + ======== + + sympy.core.random.random_complex_number + """ + + free = self.free_symbols + prec = 1 + if free: + from sympy.core.random import random_complex_number + a, c, b, d = re_min, re_max, im_min, im_max + reps = dict(list(zip(free, [random_complex_number(a, b, c, d, rational=True) + for zi in free]))) + try: + nmag = abs(self.evalf(2, subs=reps)) + except (ValueError, TypeError): + # if an out of range value resulted in evalf problems + # then return None -- XXX is there a way to know how to + # select a good random number for a given expression? + # e.g. when calculating n! negative values for n should not + # be used + return None + else: + reps = {} + nmag = abs(self.evalf(2)) + + if not hasattr(nmag, '_prec'): + # e.g. exp_polar(2*I*pi) doesn't evaluate but is_number is True + return None + + if nmag._prec == 1: + # increase the precision up to the default maximum + # precision to see if we can get any significance + + # evaluate + for prec in giant_steps(2, DEFAULT_MAXPREC): + nmag = abs(self.evalf(prec, subs=reps)) + if nmag._prec != 1: + break + + if nmag._prec != 1: + if n is None: + n = max(prec, 15) + return self.evalf(n, subs=reps) + + # never got any significance + return None + + def is_constant(self, *wrt, **flags): + """Return True if self is constant, False if not, or None if + the constancy could not be determined conclusively. + + Explanation + =========== + + If an expression has no free symbols then it is a constant. If + there are free symbols it is possible that the expression is a + constant, perhaps (but not necessarily) zero. To test such + expressions, a few strategies are tried: + + 1) numerical evaluation at two random points. If two such evaluations + give two different values and the values have a precision greater than + 1 then self is not constant. If the evaluations agree or could not be + obtained with any precision, no decision is made. The numerical testing + is done only if ``wrt`` is different than the free symbols. + + 2) differentiation with respect to variables in 'wrt' (or all free + symbols if omitted) to see if the expression is constant or not. This + will not always lead to an expression that is zero even though an + expression is constant (see added test in test_expr.py). If + all derivatives are zero then self is constant with respect to the + given symbols. + + 3) finding out zeros of denominator expression with free_symbols. + It will not be constant if there are zeros. It gives more negative + answers for expression that are not constant. + + If neither evaluation nor differentiation can prove the expression is + constant, None is returned unless two numerical values happened to be + the same and the flag ``failing_number`` is True -- in that case the + numerical value will be returned. + + If flag simplify=False is passed, self will not be simplified; + the default is True since self should be simplified before testing. + + Examples + ======== + + >>> from sympy import cos, sin, Sum, S, pi + >>> from sympy.abc import a, n, x, y + >>> x.is_constant() + False + >>> S(2).is_constant() + True + >>> Sum(x, (x, 1, 10)).is_constant() + True + >>> Sum(x, (x, 1, n)).is_constant() + False + >>> Sum(x, (x, 1, n)).is_constant(y) + True + >>> Sum(x, (x, 1, n)).is_constant(n) + False + >>> Sum(x, (x, 1, n)).is_constant(x) + True + >>> eq = a*cos(x)**2 + a*sin(x)**2 - a + >>> eq.is_constant() + True + >>> eq.subs({x: pi, a: 2}) == eq.subs({x: pi, a: 3}) == 0 + True + + >>> (0**x).is_constant() + False + >>> x.is_constant() + False + >>> (x**x).is_constant() + False + >>> one = cos(x)**2 + sin(x)**2 + >>> one.is_constant() + True + >>> ((one - 1)**(x + 1)).is_constant() in (True, False) # could be 0 or 1 + True + """ + + simplify = flags.get('simplify', True) + + if self.is_number: + return True + free = self.free_symbols + if not free: + return True # assume f(1) is some constant + + # if we are only interested in some symbols and they are not in the + # free symbols then this expression is constant wrt those symbols + wrt = set(wrt) + if wrt and not wrt & free: + return True + wrt = wrt or free + + # simplify unless this has already been done + expr = self + if simplify: + expr = expr.simplify() + + # is_zero should be a quick assumptions check; it can be wrong for + # numbers (see test_is_not_constant test), giving False when it + # shouldn't, but hopefully it will never give True unless it is sure. + if expr.is_zero: + return True + + # Don't attempt substitution or differentiation with non-number symbols + wrt_number = {sym for sym in wrt if sym.kind is NumberKind} + + # try numerical evaluation to see if we get two different values + failing_number = None + if wrt_number == free: + # try 0 (for a) and 1 (for b) + try: + a = expr.subs(list(zip(free, [0]*len(free))), + simultaneous=True) + if a is S.NaN: + # evaluation may succeed when substitution fails + a = expr._random(None, 0, 0, 0, 0) + except ZeroDivisionError: + a = None + if a is not None and a is not S.NaN: + try: + b = expr.subs(list(zip(free, [1]*len(free))), + simultaneous=True) + if b is S.NaN: + # evaluation may succeed when substitution fails + b = expr._random(None, 1, 0, 1, 0) + except ZeroDivisionError: + b = None + if b is not None and b is not S.NaN and b.equals(a) is False: + return False + # try random real + b = expr._random(None, -1, 0, 1, 0) + if b is not None and b is not S.NaN and b.equals(a) is False: + return False + # try random complex + b = expr._random() + if b is not None and b is not S.NaN: + if b.equals(a) is False: + return False + failing_number = a if a.is_number else b + + # now we will test each wrt symbol (or all free symbols) to see if the + # expression depends on them or not using differentiation. This is + # not sufficient for all expressions, however, so we don't return + # False if we get a derivative other than 0 with free symbols. + for w in wrt_number: + deriv = expr.diff(w) + if simplify: + deriv = deriv.simplify() + if deriv != 0: + if not (pure_complex(deriv, or_real=True)): + if flags.get('failing_number', False): + return failing_number + return False + from sympy.solvers.solvers import denoms + return fuzzy_not(fuzzy_or(den.is_zero for den in denoms(self))) + + def equals(self, other, failing_expression=False): + """Return True if self == other, False if it does not, or None. If + failing_expression is True then the expression which did not simplify + to a 0 will be returned instead of None. + + Explanation + =========== + + If ``self`` is a Number (or complex number) that is not zero, then + the result is False. + + If ``self`` is a number and has not evaluated to zero, evalf will be + used to test whether the expression evaluates to zero. If it does so + and the result has significance (i.e. the precision is either -1, for + a Rational result, or is greater than 1) then the evalf value will be + used to return True or False. + + """ + from sympy.simplify.simplify import nsimplify, simplify + from sympy.solvers.solvers import solve + from sympy.polys.polyerrors import NotAlgebraic + from sympy.polys.numberfields import minimal_polynomial + + other = sympify(other) + + if not isinstance(other, Expr): + return False + + if self == other: + return True + + # they aren't the same so see if we can make the difference 0; + # don't worry about doing simplification steps one at a time + # because if the expression ever goes to 0 then the subsequent + # simplification steps that are done will be very fast. + diff = factor_terms(simplify(self - other), radical=True) + + if not diff: + return True + + if not diff.has(Add, Mod): + # if there is no expanding to be done after simplifying + # then this can't be a zero + return False + + factors = diff.as_coeff_mul()[1] + if len(factors) > 1: # avoid infinity recursion + fac_zero = [fac.equals(0) for fac in factors] + if None not in fac_zero: # every part can be decided + return any(fac_zero) + + constant = diff.is_constant(simplify=False, failing_number=True) + + if constant is False: + return False + + if not diff.is_number: + if constant is None: + # e.g. unless the right simplification is done, a symbolic + # zero is possible (see expression of issue 6829: without + # simplification constant will be None). + return + + if constant is True: + # this gives a number whether there are free symbols or not + ndiff = diff._random() + # is_comparable will work whether the result is real + # or complex; it could be None, however. + if ndiff and ndiff.is_comparable: + return False + + # sometimes we can use a simplified result to give a clue as to + # what the expression should be; if the expression is *not* zero + # then we should have been able to compute that and so now + # we can just consider the cases where the approximation appears + # to be zero -- we try to prove it via minimal_polynomial. + # + # removed + # ns = nsimplify(diff) + # if diff.is_number and (not ns or ns == diff): + # + # The thought was that if it nsimplifies to 0 that's a sure sign + # to try the following to prove it; or if it changed but wasn't + # zero that might be a sign that it's not going to be easy to + # prove. But tests seem to be working without that logic. + # + if diff.is_number: + # try to prove via self-consistency + surds = [s for s in diff.atoms(Pow) if s.args[0].is_Integer] + # it seems to work better to try big ones first + surds.sort(key=lambda x: -x.args[0]) + for s in surds: + try: + # simplify is False here -- this expression has already + # been identified as being hard to identify as zero; + # we will handle the checking ourselves using nsimplify + # to see if we are in the right ballpark or not and if so + # *then* the simplification will be attempted. + sol = solve(diff, s, simplify=False) + if sol: + if s in sol: + # the self-consistent result is present + return True + if all(si.is_Integer for si in sol): + # perfect powers are removed at instantiation + # so surd s cannot be an integer + return False + if all(i.is_algebraic is False for i in sol): + # a surd is algebraic + return False + if any(si in surds for si in sol): + # it wasn't equal to s but it is in surds + # and different surds are not equal + return False + if any(nsimplify(s - si) == 0 and + simplify(s - si) == 0 for si in sol): + return True + if s.is_real: + if any(nsimplify(si, [s]) == s and simplify(si) == s + for si in sol): + return True + except NotImplementedError: + pass + + # try to prove with minimal_polynomial but know when + # *not* to use this or else it can take a long time. e.g. issue 8354 + if True: # change True to condition that assures non-hang + try: + mp = minimal_polynomial(diff) + if mp.is_Symbol: + return True + return False + except (NotAlgebraic, NotImplementedError): + pass + + # diff has not simplified to zero; constant is either None, True + # or the number with significance (is_comparable) that was randomly + # calculated twice as the same value. + if constant not in (True, None) and constant != 0: + return False + + if failing_expression: + return diff + return None + + def _eval_is_extended_positive_negative(self, positive): + from sympy.polys.numberfields import minimal_polynomial + from sympy.polys.polyerrors import NotAlgebraic + if self.is_number: + # check to see that we can get a value + try: + n2 = self._eval_evalf(2) + # XXX: This shouldn't be caught here + # Catches ValueError: hypsum() failed to converge to the requested + # 34 bits of accuracy + except ValueError: + return None + if n2 is None: + return None + if getattr(n2, '_prec', 1) == 1: # no significance + return None + if n2 is S.NaN: + return None + + f = self.evalf(2) + if f.is_Float: + match = f, S.Zero + else: + match = pure_complex(f) + if match is None: + return False + r, i = match + if not (i.is_Number and r.is_Number): + return False + if r._prec != 1 and i._prec != 1: + return bool(not i and ((r > 0) if positive else (r < 0))) + elif r._prec == 1 and (not i or i._prec == 1) and \ + self._eval_is_algebraic() and not self.has(Function): + try: + if minimal_polynomial(self).is_Symbol: + return False + except (NotAlgebraic, NotImplementedError): + pass + + def _eval_is_extended_positive(self): + return self._eval_is_extended_positive_negative(positive=True) + + def _eval_is_extended_negative(self): + return self._eval_is_extended_positive_negative(positive=False) + + def _eval_interval(self, x, a, b): + """ + Returns evaluation over an interval. For most functions this is: + + self.subs(x, b) - self.subs(x, a), + + possibly using limit() if NaN is returned from subs, or if + singularities are found between a and b. + + If b or a is None, it only evaluates -self.subs(x, a) or self.subs(b, x), + respectively. + + """ + from sympy.calculus.accumulationbounds import AccumBounds + from sympy.functions.elementary.exponential import log + from sympy.series.limits import limit, Limit + from sympy.sets.sets import Interval + from sympy.solvers.solveset import solveset + + if (a is None and b is None): + raise ValueError('Both interval ends cannot be None.') + + def _eval_endpoint(left): + c = a if left else b + if c is None: + return S.Zero + else: + C = self.subs(x, c) + if C.has(S.NaN, S.Infinity, S.NegativeInfinity, + S.ComplexInfinity, AccumBounds): + if (a < b) != False: + C = limit(self, x, c, "+" if left else "-") + else: + C = limit(self, x, c, "-" if left else "+") + + if isinstance(C, Limit): + raise NotImplementedError("Could not compute limit") + return C + + if a == b: + return S.Zero + + A = _eval_endpoint(left=True) + if A is S.NaN: + return A + + B = _eval_endpoint(left=False) + + if (a and b) is None: + return B - A + + value = B - A + + if a.is_comparable and b.is_comparable: + if a < b: + domain = Interval(a, b) + else: + domain = Interval(b, a) + # check the singularities of self within the interval + # if singularities is a ConditionSet (not iterable), catch the exception and pass + singularities = solveset(self.cancel().as_numer_denom()[1], x, + domain=domain) + for logterm in self.atoms(log): + singularities = singularities | solveset(logterm.args[0], x, + domain=domain) + try: + for s in singularities: + if value is S.NaN: + # no need to keep adding, it will stay NaN + break + if not s.is_comparable: + continue + if (a < s) == (s < b) == True: + value += -limit(self, x, s, "+") + limit(self, x, s, "-") + elif (b < s) == (s < a) == True: + value += limit(self, x, s, "+") - limit(self, x, s, "-") + except TypeError: + pass + + return value + + def _eval_power(self, expt) -> Expr | None: + # subclass to compute self**other for cases when + # other is not NaN, 0, or 1 + return None + + def _eval_conjugate(self): + if self.is_extended_real: + return self + elif self.is_imaginary: + return -self + + def conjugate(self): + """Returns the complex conjugate of 'self'.""" + from sympy.functions.elementary.complexes import conjugate as c + return c(self) + + def dir(self, x, cdir): + if self.is_zero: + return S.Zero + from sympy.functions.elementary.exponential import log + minexp = S.Zero + arg = self + while arg: + minexp += S.One + arg = arg.diff(x) + coeff = arg.subs(x, 0) + if coeff is S.NaN: + coeff = arg.limit(x, 0) + if coeff is S.ComplexInfinity: + try: + coeff, _ = arg.leadterm(x) + if coeff.has(log(x)): + raise ValueError() + except ValueError: + coeff = arg.limit(x, 0) + if coeff != S.Zero: + break + return coeff*cdir**minexp + + def _eval_transpose(self): + from sympy.functions.elementary.complexes import conjugate + if self.is_commutative: + return self + elif self.is_hermitian: + return conjugate(self) + elif self.is_antihermitian: + return -conjugate(self) + + def transpose(self): + from sympy.functions.elementary.complexes import transpose + return transpose(self) + + def _eval_adjoint(self): + from sympy.functions.elementary.complexes import conjugate, transpose + if self.is_hermitian: + return self + elif self.is_antihermitian: + return -self + obj = self._eval_conjugate() + if obj is not None: + return transpose(obj) + obj = self._eval_transpose() + if obj is not None: + return conjugate(obj) + + def adjoint(self): + from sympy.functions.elementary.complexes import adjoint + return adjoint(self) + + @classmethod + def _parse_order(cls, order): + """Parse and configure the ordering of terms. """ + from sympy.polys.orderings import monomial_key + + startswith = getattr(order, "startswith", None) + if startswith is None: + reverse = False + else: + reverse = startswith('rev-') + if reverse: + order = order[4:] + + monom_key = monomial_key(order) + + def neg(monom): + return tuple([neg(m) if isinstance(m, tuple) else -m for m in monom]) + + def key(term): + _, ((re, im), monom, ncpart) = term + + monom = neg(monom_key(monom)) + ncpart = tuple([e.sort_key(order=order) for e in ncpart]) + coeff = ((bool(im), im), (re, im)) + + return monom, ncpart, coeff + + return key, reverse + + def as_ordered_factors(self, order=None): + """Return list of ordered factors (if Mul) else [self].""" + return [self] + + def as_poly(self, *gens, **args): + """Converts ``self`` to a polynomial or returns ``None``. + + Explanation + =========== + + >>> from sympy import sin + >>> from sympy.abc import x, y + + >>> print((x**2 + x*y).as_poly()) + Poly(x**2 + x*y, x, y, domain='ZZ') + + >>> print((x**2 + x*y).as_poly(x, y)) + Poly(x**2 + x*y, x, y, domain='ZZ') + + >>> print((x**2 + sin(y)).as_poly(x, y)) + None + + """ + from sympy.polys.polyerrors import PolynomialError, GeneratorsNeeded + from sympy.polys.polytools import Poly + + try: + poly = Poly(self, *gens, **args) + + if not poly.is_Poly: + return None + else: + return poly + except (PolynomialError, GeneratorsNeeded): + # PolynomialError is caught for e.g. exp(x).as_poly(x) + # GeneratorsNeeded is caught for e.g. S(2).as_poly() + return None + + def as_ordered_terms(self, order=None, data=False): + """ + Transform an expression to an ordered list of terms. + + Examples + ======== + + >>> from sympy import sin, cos + >>> from sympy.abc import x + + >>> (sin(x)**2*cos(x) + sin(x)**2 + 1).as_ordered_terms() + [sin(x)**2*cos(x), sin(x)**2, 1] + + """ + + from .numbers import Number, NumberSymbol + + if order is None and self.is_Add: + # Spot the special case of Add(Number, Mul(Number, expr)) with the + # first number positive and the second number negative + key = lambda x:not isinstance(x, (Number, NumberSymbol)) + add_args = sorted(Add.make_args(self), key=key) + if (len(add_args) == 2 + and isinstance(add_args[0], (Number, NumberSymbol)) + and isinstance(add_args[1], Mul)): + mul_args = sorted(Mul.make_args(add_args[1]), key=key) + if (len(mul_args) == 2 + and isinstance(mul_args[0], Number) + and add_args[0].is_positive + and mul_args[0].is_negative): + return add_args + + key, reverse = self._parse_order(order) + terms, gens = self.as_terms() + + if not any(term.is_Order for term, _ in terms): + ordered = sorted(terms, key=key, reverse=reverse) + else: + _terms, _order = [], [] + + for term, repr in terms: + if not term.is_Order: + _terms.append((term, repr)) + else: + _order.append((term, repr)) + + ordered = sorted(_terms, key=key, reverse=True) \ + + sorted(_order, key=key, reverse=True) + + if data: + return ordered, gens + else: + return [term for term, _ in ordered] + + def as_terms(self): + """Transform an expression to a list of terms. """ + from .exprtools import decompose_power + + gens, terms = set(), [] + + for term in Add.make_args(self): + coeff, _term = term.as_coeff_Mul() + + coeff = complex(coeff) + cpart, ncpart = {}, [] + + if _term is not S.One: + for factor in Mul.make_args(_term): + if factor.is_number: + try: + coeff *= complex(factor) + except (TypeError, ValueError): + pass + else: + continue + + if factor.is_commutative: + base, exp = decompose_power(factor) + + cpart[base] = exp + gens.add(base) + else: + ncpart.append(factor) + + coeff = coeff.real, coeff.imag + ncpart = tuple(ncpart) + + terms.append((term, (coeff, cpart, ncpart))) + + gens = sorted(gens, key=default_sort_key) + + k, indices = len(gens), {} + + for i, g in enumerate(gens): + indices[g] = i + + result = [] + + for term, (coeff, cpart, ncpart) in terms: + monom = [0]*k + + for base, exp in cpart.items(): + monom[indices[base]] = exp + + result.append((term, (coeff, tuple(monom), ncpart))) + + return result, gens + + def removeO(self) -> Expr: + """Removes the additive O(..) symbol if there is one""" + return self + + def getO(self) -> Expr | None: + """Returns the additive O(..) symbol if there is one, else None.""" + return None + + def getn(self): + """ + Returns the order of the expression. + + Explanation + =========== + + The order is determined either from the O(...) term. If there + is no O(...) term, it returns None. + + Examples + ======== + + >>> from sympy import O + >>> from sympy.abc import x + >>> (1 + x + O(x**2)).getn() + 2 + >>> (1 + x).getn() + + """ + o = self.getO() + if o is None: + return None + elif o.is_Order: + o = o.expr + if o is S.One: + return S.Zero + if o.is_Symbol: + return S.One + if o.is_Pow: + return o.args[1] + if o.is_Mul: # x**n*log(x)**n or x**n/log(x)**n + for oi in o.args: + if oi.is_Symbol: + return S.One + if oi.is_Pow: + from .symbol import Dummy, Symbol + syms = oi.atoms(Symbol) + if len(syms) == 1: + x = syms.pop() + oi = oi.subs(x, Dummy('x', positive=True)) + if oi.base.is_Symbol and oi.exp.is_Rational: + return abs(oi.exp) + + raise NotImplementedError('not sure of order of %s' % o) + + def count_ops(self, visual=False): + from .function import count_ops + return count_ops(self, visual) + + def args_cnc(self, cset=False, warn=True, split_1=True): + """Return [commutative factors, non-commutative factors] of self. + + Explanation + =========== + + self is treated as a Mul and the ordering of the factors is maintained. + If ``cset`` is True the commutative factors will be returned in a set. + If there were repeated factors (as may happen with an unevaluated Mul) + then an error will be raised unless it is explicitly suppressed by + setting ``warn`` to False. + + Note: -1 is always separated from a Number unless split_1 is False. + + Examples + ======== + + >>> from sympy import symbols, oo + >>> A, B = symbols('A B', commutative=0) + >>> x, y = symbols('x y') + >>> (-2*x*y).args_cnc() + [[-1, 2, x, y], []] + >>> (-2.5*x).args_cnc() + [[-1, 2.5, x], []] + >>> (-2*x*A*B*y).args_cnc() + [[-1, 2, x, y], [A, B]] + >>> (-2*x*A*B*y).args_cnc(split_1=False) + [[-2, x, y], [A, B]] + >>> (-2*x*y).args_cnc(cset=True) + [{-1, 2, x, y}, []] + + The arg is always treated as a Mul: + + >>> (-2 + x + A).args_cnc() + [[], [x - 2 + A]] + >>> (-oo).args_cnc() # -oo is a singleton + [[-1, oo], []] + """ + args = list(Mul.make_args(self)) + + for i, mi in enumerate(args): + if not mi.is_commutative: + c = args[:i] + nc = args[i:] + break + else: + c = args + nc = [] + + if c and split_1 and ( + c[0].is_Number and + c[0].is_extended_negative and + c[0] is not S.NegativeOne): + c[:1] = [S.NegativeOne, -c[0]] + + if cset: + clen = len(c) + c = set(c) + if clen and warn and len(c) != clen: + raise ValueError('repeated commutative arguments: %s' % + [ci for ci in c if list(self.args).count(ci) > 1]) + return [c, nc] + + def coeff(self, x: Expr, n=1, right=False, _first=True): + """ + Returns the coefficient from the term(s) containing ``x**n``. If ``n`` + is zero then all terms independent of ``x`` will be returned. + + Explanation + =========== + + When ``x`` is noncommutative, the coefficient to the left (default) or + right of ``x`` can be returned. The keyword 'right' is ignored when + ``x`` is commutative. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.abc import x, y, z + + You can select terms that have an explicit negative in front of them: + + >>> (-x + 2*y).coeff(-1) + x + >>> (x - 2*y).coeff(-1) + 2*y + + You can select terms with no Rational coefficient: + + >>> (x + 2*y).coeff(1) + x + >>> (3 + 2*x + 4*x**2).coeff(1) + 0 + + You can select terms independent of x by making n=0; in this case + expr.as_independent(x)[0] is returned (and 0 will be returned instead + of None): + + >>> (3 + 2*x + 4*x**2).coeff(x, 0) + 3 + >>> eq = ((x + 1)**3).expand() + 1 + >>> eq + x**3 + 3*x**2 + 3*x + 2 + >>> [eq.coeff(x, i) for i in reversed(range(4))] + [1, 3, 3, 2] + >>> eq -= 2 + >>> [eq.coeff(x, i) for i in reversed(range(4))] + [1, 3, 3, 0] + + You can select terms that have a numerical term in front of them: + + >>> (-x - 2*y).coeff(2) + -y + >>> from sympy import sqrt + >>> (x + sqrt(2)*x).coeff(sqrt(2)) + x + + The matching is exact: + + >>> (3 + 2*x + 4*x**2).coeff(x) + 2 + >>> (3 + 2*x + 4*x**2).coeff(x**2) + 4 + >>> (3 + 2*x + 4*x**2).coeff(x**3) + 0 + >>> (z*(x + y)**2).coeff((x + y)**2) + z + >>> (z*(x + y)**2).coeff(x + y) + 0 + + In addition, no factoring is done, so 1 + z*(1 + y) is not obtained + from the following: + + >>> (x + z*(x + x*y)).coeff(x) + 1 + + If such factoring is desired, factor_terms can be used first: + + >>> from sympy import factor_terms + >>> factor_terms(x + z*(x + x*y)).coeff(x) + z*(y + 1) + 1 + + >>> n, m, o = symbols('n m o', commutative=False) + >>> n.coeff(n) + 1 + >>> (3*n).coeff(n) + 3 + >>> (n*m + m*n*m).coeff(n) # = (1 + m)*n*m + 1 + m + >>> (n*m + m*n*m).coeff(n, right=True) # = (1 + m)*n*m + m + + If there is more than one possible coefficient 0 is returned: + + >>> (n*m + m*n).coeff(n) + 0 + + If there is only one possible coefficient, it is returned: + + >>> (n*m + x*m*n).coeff(m*n) + x + >>> (n*m + x*m*n).coeff(m*n, right=1) + 1 + + See Also + ======== + + as_coefficient: separate the expression into a coefficient and factor + as_coeff_Add: separate the additive constant from an expression + as_coeff_Mul: separate the multiplicative constant from an expression + as_independent: separate x-dependent terms/factors from others + sympy.polys.polytools.Poly.coeff_monomial: efficiently find the single coefficient of a monomial in Poly + sympy.polys.polytools.Poly.nth: like coeff_monomial but powers of monomial terms are used + """ + x = sympify(x) + if not isinstance(x, Basic): + return S.Zero + + n = as_int(n) + + if not x: + return S.Zero + + if x == self: + if n == 1: + return S.One + return S.Zero + + co2: list[Expr] + + if x is S.One: + co2 = [a for a in Add.make_args(self) if a.as_coeff_Mul()[0] is S.One] + if not co2: + return S.Zero + return Add(*co2) + + if n == 0: + if x.is_Add and self.is_Add: + c = self.coeff(x, right=right) + if not c: + return S.Zero + if not right: + return self - Add(*[a*x for a in Add.make_args(c)]) + return self - Add(*[x*a for a in Add.make_args(c)]) + return self.as_independent(x, as_Add=True)[0] + + # continue with the full method, looking for this power of x: + x = x**n + + def incommon(l1, l2): + if not l1 or not l2: + return [] + n = min(len(l1), len(l2)) + for i in range(n): + if l1[i] != l2[i]: + return l1[:i] + return l1[:] + + def find(l, sub, first=True): + """ Find where list sub appears in list l. When ``first`` is True + the first occurrence from the left is returned, else the last + occurrence is returned. Return None if sub is not in l. + + Examples + ======== + + >> l = range(5)*2 + >> find(l, [2, 3]) + 2 + >> find(l, [2, 3], first=0) + 7 + >> find(l, [2, 4]) + None + + """ + if not sub or not l or len(sub) > len(l): + return None + n = len(sub) + if not first: + l.reverse() + sub.reverse() + for i in range(len(l) - n + 1): + if all(l[i + j] == sub[j] for j in range(n)): + break + else: + i = None + if not first: + l.reverse() + sub.reverse() + if i is not None and not first: + i = len(l) - (i + n) + return i + + co2 = [] + co: list[tuple[set[Expr], list[Expr]]] = [] + args = Add.make_args(self) + self_c = self.is_commutative + x_c = x.is_commutative + if self_c and not x_c: + return S.Zero + if _first and self.is_Add and not self_c and not x_c: + # get the part that depends on x exactly + xargs = Mul.make_args(x) + d = Add(*[i for i in Add.make_args(self.as_independent(x)[1]) + if all(xi in Mul.make_args(i) for xi in xargs)]) + rv = d.coeff(x, right=right, _first=False) + if not rv.is_Add or not right: + return rv + c_part, nc_part = zip(*[i.args_cnc() for i in rv.args]) + if has_variety(c_part): + return rv + return Add(*[Mul._from_args(i) for i in nc_part]) + + one_c = self_c or x_c + xargs, nx = x.args_cnc(cset=True, warn=bool(not x_c)) + # find the parts that pass the commutative terms + for a in args: + margs, nc = a.args_cnc(cset=True, warn=bool(not self_c)) + if nc is None: + nc = [] + if len(xargs) > len(margs): + continue + resid = margs.difference(xargs) + if len(resid) + len(xargs) == len(margs): + if one_c: + co2.append(Mul(*(list(resid) + nc))) + else: + co.append((resid, nc)) + if one_c: + if co2 == []: + return S.Zero + elif co2: + return Add(*co2) + else: # both nc + # now check the non-comm parts + if not co: + return S.Zero + if all(n == co[0][1] for r, n in co): + ii = find(co[0][1], nx, right) + if ii is not None: + if not right: + return Mul(Add(*[Mul(*r) for r, c in co]), Mul(*co[0][1][:ii])) + else: + return Mul(*co[0][1][ii + len(nx):]) + beg = reduce(incommon, (n[1] for n in co)) + if beg: + ii = find(beg, nx, right) + if ii is not None: + if not right: + gcdc = co[0][0] + for i in range(1, len(co)): + gcdc = gcdc.intersection(co[i][0]) + if not gcdc: + break + return Mul(*(list(gcdc) + beg[:ii])) + else: + m = ii + len(nx) + return Add(*[Mul(*(list(r) + n[m:])) for r, n in co]) + end = list(reversed( + reduce(incommon, (list(reversed(n[1])) for n in co)))) + if end: + ii = find(end, nx, right) + if ii is not None: + if not right: + return Add(*[Mul(*(list(r) + n[:-len(end) + ii])) for r, n in co]) + else: + return Mul(*end[ii + len(nx):]) + # look for single match + hit = None + for i, (r, n) in enumerate(co): + ii = find(n, nx, right) + if ii is not None: + if not hit: + hit = ii, r, n + else: + break + else: + if hit: + ii, r, n = hit + if not right: + return Mul(*(list(r) + n[:ii])) + else: + return Mul(*n[ii + len(nx):]) + + return S.Zero + + def as_expr(self, *gens): + """ + Convert a polynomial to a SymPy expression. + + Examples + ======== + + >>> from sympy import sin + >>> from sympy.abc import x, y + + >>> f = (x**2 + x*y).as_poly(x, y) + >>> f.as_expr() + x**2 + x*y + + >>> sin(x).as_expr() + sin(x) + + """ + return self + + def as_coefficient(self, expr: Expr) -> Expr | None: + """ + Extracts symbolic coefficient at the given expression. In + other words, this functions separates 'self' into the product + of 'expr' and 'expr'-free coefficient. If such separation + is not possible it will return None. + + Examples + ======== + + >>> from sympy import E, pi, sin, I, Poly + >>> from sympy.abc import x + + >>> E.as_coefficient(E) + 1 + >>> (2*E).as_coefficient(E) + 2 + >>> (2*sin(E)*E).as_coefficient(E) + + Two terms have E in them so a sum is returned. (If one were + desiring the coefficient of the term exactly matching E then + the constant from the returned expression could be selected. + Or, for greater precision, a method of Poly can be used to + indicate the desired term from which the coefficient is + desired.) + + >>> (2*E + x*E).as_coefficient(E) + x + 2 + >>> _.args[0] # just want the exact match + 2 + >>> p = Poly(2*E + x*E); p + Poly(x*E + 2*E, x, E, domain='ZZ') + >>> p.coeff_monomial(E) + 2 + >>> p.nth(0, 1) + 2 + + Since the following cannot be written as a product containing + E as a factor, None is returned. (If the coefficient ``2*x`` is + desired then the ``coeff`` method should be used.) + + >>> (2*E*x + x).as_coefficient(E) + >>> (2*E*x + x).coeff(E) + 2*x + + >>> (E*(x + 1) + x).as_coefficient(E) + + >>> (2*pi*I).as_coefficient(pi*I) + 2 + >>> (2*I).as_coefficient(pi*I) + + See Also + ======== + + coeff: return sum of terms have a given factor + as_coeff_Add: separate the additive constant from an expression + as_coeff_Mul: separate the multiplicative constant from an expression + as_independent: separate x-dependent terms/factors from others + sympy.polys.polytools.Poly.coeff_monomial: efficiently find the single coefficient of a monomial in Poly + sympy.polys.polytools.Poly.nth: like coeff_monomial but powers of monomial terms are used + + + """ + + r = self.extract_multiplicatively(expr) + if r and not r.has(expr): + return r + else: + return None + + def as_independent(self, *deps, **hint) -> tuple[Expr, Expr]: + """ + A mostly naive separation of a Mul or Add into arguments that are not + are dependent on deps. To obtain as complete a separation of variables + as possible, use a separation method first, e.g.: + + * separatevars() to change Mul, Add and Pow (including exp) into Mul + * .expand(mul=True) to change Add or Mul into Add + * .expand(log=True) to change log expr into an Add + + The only non-naive thing that is done here is to respect noncommutative + ordering of variables and to always return (0, 0) for `self` of zero + regardless of hints. + + For nonzero `self`, the returned tuple (i, d) has the + following interpretation: + + * i will has no variable that appears in deps + * d will either have terms that contain variables that are in deps, or + be equal to 0 (when self is an Add) or 1 (when self is a Mul) + * if self is an Add then self = i + d + * if self is a Mul then self = i*d + * otherwise (self, S.One) or (S.One, self) is returned. + + To force the expression to be treated as an Add, use the hint as_Add=True + + Examples + ======== + + -- self is an Add + + >>> from sympy import sin, cos, exp + >>> from sympy.abc import x, y, z + + >>> (x + x*y).as_independent(x) + (0, x*y + x) + >>> (x + x*y).as_independent(y) + (x, x*y) + >>> (2*x*sin(x) + y + x + z).as_independent(x) + (y + z, 2*x*sin(x) + x) + >>> (2*x*sin(x) + y + x + z).as_independent(x, y) + (z, 2*x*sin(x) + x + y) + + -- self is a Mul + + >>> (x*sin(x)*cos(y)).as_independent(x) + (cos(y), x*sin(x)) + + non-commutative terms cannot always be separated out when self is a Mul + + >>> from sympy import symbols + >>> n1, n2, n3 = symbols('n1 n2 n3', commutative=False) + >>> (n1 + n1*n2).as_independent(n2) + (n1, n1*n2) + >>> (n2*n1 + n1*n2).as_independent(n2) + (0, n1*n2 + n2*n1) + >>> (n1*n2*n3).as_independent(n1) + (1, n1*n2*n3) + >>> (n1*n2*n3).as_independent(n2) + (n1, n2*n3) + >>> ((x-n1)*(x-y)).as_independent(x) + (1, (x - y)*(x - n1)) + + -- self is anything else: + + >>> (sin(x)).as_independent(x) + (1, sin(x)) + >>> (sin(x)).as_independent(y) + (sin(x), 1) + >>> exp(x+y).as_independent(x) + (1, exp(x + y)) + + -- force self to be treated as an Add: + + >>> (3*x).as_independent(x, as_Add=True) + (0, 3*x) + + -- force self to be treated as a Mul: + + >>> (3+x).as_independent(x, as_Add=False) + (1, x + 3) + >>> (-3+x).as_independent(x, as_Add=False) + (1, x - 3) + + Note how the below differs from the above in making the + constant on the dep term positive. + + >>> (y*(-3+x)).as_independent(x) + (y, x - 3) + + -- use .as_independent() for true independence testing instead + of .has(). The former considers only symbols in the free + symbols while the latter considers all symbols + + >>> from sympy import Integral + >>> I = Integral(x, (x, 1, 2)) + >>> I.has(x) + True + >>> x in I.free_symbols + False + >>> I.as_independent(x) == (I, 1) + True + >>> (I + x).as_independent(x) == (I, x) + True + + Note: when trying to get independent terms, a separation method + might need to be used first. In this case, it is important to keep + track of what you send to this routine so you know how to interpret + the returned values + + >>> from sympy import separatevars, log + >>> separatevars(exp(x+y)).as_independent(x) + (exp(y), exp(x)) + >>> (x + x*y).as_independent(y) + (x, x*y) + >>> separatevars(x + x*y).as_independent(y) + (x, y + 1) + >>> (x*(1 + y)).as_independent(y) + (x, y + 1) + >>> (x*(1 + y)).expand(mul=True).as_independent(y) + (x, x*y) + >>> a, b=symbols('a b', positive=True) + >>> (log(a*b).expand(log=True)).as_independent(b) + (log(a), log(b)) + + See Also + ======== + + separatevars + expand_log + sympy.core.add.Add.as_two_terms + sympy.core.mul.Mul.as_two_terms + as_coeff_mul + """ + from .symbol import Symbol + from .add import _unevaluated_Add + from .mul import _unevaluated_Mul + + if self is S.Zero: + return (self, self) + + func = self.func + want: type[Add] | type[Mul] + if hint.get('as_Add', isinstance(self, Add) ): + want = Add + else: + want = Mul + + # sift out deps into symbolic and other and ignore + # all symbols but those that are in the free symbols + sym = set() + other = [] + for d in deps: + if isinstance(d, Symbol): # Symbol.is_Symbol is True + sym.add(d) + else: + other.append(d) + + def has(e): + """return the standard has() if there are no literal symbols, else + check to see that symbol-deps are in the free symbols.""" + has_other = e.has(*other) + if not sym: + return has_other + return has_other or e.has(*(e.free_symbols & sym)) + + if (want is not func or + func is not Add and func is not Mul): + if has(self): + return (want.identity, self) + else: + return (self, want.identity) + else: + if func is Add: + args = list(self.args) + else: + args, nc = self.args_cnc() + + d = sift(args, has) + depend = d[True] + indep = d[False] + if func is Add: # all terms were treated as commutative + return (Add(*indep), _unevaluated_Add(*depend)) + else: # handle noncommutative by stopping at first dependent term + for i, n in enumerate(nc): + if has(n): + depend.extend(nc[i:]) + break + indep.append(n) + return Mul(*indep), _unevaluated_Mul(*depend) + + def as_real_imag(self, deep=True, **hints) -> tuple[Expr, Expr]: + """Performs complex expansion on 'self' and returns a tuple + containing collected both real and imaginary parts. This + method cannot be confused with re() and im() functions, + which does not perform complex expansion at evaluation. + + However it is possible to expand both re() and im() + functions and get exactly the same results as with + a single call to this function. + + >>> from sympy import symbols, I + + >>> x, y = symbols('x,y', real=True) + + >>> (x + y*I).as_real_imag() + (x, y) + + >>> from sympy.abc import z, w + + >>> (z + w*I).as_real_imag() + (re(z) - im(w), re(w) + im(z)) + + """ + if hints.get('ignore') == self: + return None # type: ignore + else: + from sympy.functions.elementary.complexes import im, re + return (re(self), im(self)) + + def as_powers_dict(self): + """Return self as a dictionary of factors with each factor being + treated as a power. The keys are the bases of the factors and the + values, the corresponding exponents. The resulting dictionary should + be used with caution if the expression is a Mul and contains non- + commutative factors since the order that they appeared will be lost in + the dictionary. + + See Also + ======== + as_ordered_factors: An alternative for noncommutative applications, + returning an ordered list of factors. + args_cnc: Similar to as_ordered_factors, but guarantees separation + of commutative and noncommutative factors. + """ + d = defaultdict(int) + d.update([self.as_base_exp()]) + return d + + def as_coefficients_dict(self, *syms): + """Return a dictionary mapping terms to their Rational coefficient. + Since the dictionary is a defaultdict, inquiries about terms which + were not present will return a coefficient of 0. + + If symbols ``syms`` are provided, any multiplicative terms + independent of them will be considered a coefficient and a + regular dictionary of syms-dependent generators as keys and + their corresponding coefficients as values will be returned. + + Examples + ======== + + >>> from sympy.abc import a, x, y + >>> (3*x + a*x + 4).as_coefficients_dict() + {1: 4, x: 3, a*x: 1} + >>> _[a] + 0 + >>> (3*a*x).as_coefficients_dict() + {a*x: 3} + >>> (3*a*x).as_coefficients_dict(x) + {x: 3*a} + >>> (3*a*x).as_coefficients_dict(y) + {1: 3*a*x} + + """ + d = defaultdict(list) + if not syms: + for ai in Add.make_args(self): + c, m = ai.as_coeff_Mul() + d[m].append(c) + for k, v in d.items(): + if len(v) == 1: + d[k] = v[0] + else: + d[k] = Add(*v) + else: + ind, dep = self.as_independent(*syms, as_Add=True) + for i in Add.make_args(dep): + if i.is_Mul: + c, x = i.as_coeff_mul(*syms) + if c is S.One: + d[i].append(c) + else: + d[i._new_rawargs(*x)].append(c) + elif i: + d[i].append(S.One) + d = {k: Add(*d[k]) for k in d} + if ind is not S.Zero: + d.update({S.One: ind}) + di = defaultdict(int) + di.update(d) + return di + + def as_base_exp(self) -> tuple[Expr, Expr]: + # a -> b ** e + return self, S.One + + def as_coeff_mul(self, *deps, **kwargs) -> tuple[Expr, tuple[Expr, ...]]: + """Return the tuple (c, args) where self is written as a Mul, ``m``. + + c should be a Rational multiplied by any factors of the Mul that are + independent of deps. + + args should be a tuple of all other factors of m; args is empty + if self is a Number or if self is independent of deps (when given). + + This should be used when you do not know if self is a Mul or not but + you want to treat self as a Mul or if you want to process the + individual arguments of the tail of self as a Mul. + + - if you know self is a Mul and want only the head, use self.args[0]; + - if you do not want to process the arguments of the tail but need the + tail then use self.as_two_terms() which gives the head and tail; + - if you want to split self into an independent and dependent parts + use ``self.as_independent(*deps)`` + + >>> from sympy import S + >>> from sympy.abc import x, y + >>> (S(3)).as_coeff_mul() + (3, ()) + >>> (3*x*y).as_coeff_mul() + (3, (x, y)) + >>> (3*x*y).as_coeff_mul(x) + (3*y, (x,)) + >>> (3*y).as_coeff_mul(x) + (3*y, ()) + """ + if deps: + if not self.has(*deps): + return self, () + return S.One, (self,) + + def as_coeff_add(self, *deps) -> tuple[Expr, tuple[Expr, ...]]: + """Return the tuple (c, args) where self is written as an Add, ``a``. + + c should be a Rational added to any terms of the Add that are + independent of deps. + + args should be a tuple of all other terms of ``a``; args is empty + if self is a Number or if self is independent of deps (when given). + + This should be used when you do not know if self is an Add or not but + you want to treat self as an Add or if you want to process the + individual arguments of the tail of self as an Add. + + - if you know self is an Add and want only the head, use self.args[0]; + - if you do not want to process the arguments of the tail but need the + tail then use self.as_two_terms() which gives the head and tail. + - if you want to split self into an independent and dependent parts + use ``self.as_independent(*deps)`` + + >>> from sympy import S + >>> from sympy.abc import x, y + >>> (S(3)).as_coeff_add() + (3, ()) + >>> (3 + x).as_coeff_add() + (3, (x,)) + >>> (3 + x + y).as_coeff_add(x) + (y + 3, (x,)) + >>> (3 + y).as_coeff_add(x) + (y + 3, ()) + + """ + if deps: + if not self.has_free(*deps): + return self, () + return S.Zero, (self,) + + def primitive(self) -> tuple[Number, Expr]: + """Return the positive Rational that can be extracted non-recursively + from every term of self (i.e., self is treated like an Add). This is + like the as_coeff_Mul() method but primitive always extracts a positive + Rational (never a negative or a Float). + + Examples + ======== + + >>> from sympy.abc import x + >>> (3*(x + 1)**2).primitive() + (3, (x + 1)**2) + >>> a = (6*x + 2); a.primitive() + (2, 3*x + 1) + >>> b = (x/2 + 3); b.primitive() + (1/2, x + 6) + >>> (a*b).primitive() == (1, a*b) + True + """ + if not self: + return S.One, S.Zero + c, r = self.as_coeff_Mul(rational=True) + if c.is_negative: + c, r = -c, -r + return c, r + + def as_content_primitive(self, radical=False, clear=True): + """This method should recursively remove a Rational from all arguments + and return that (content) and the new self (primitive). The content + should always be positive and ``Mul(*foo.as_content_primitive()) == foo``. + The primitive need not be in canonical form and should try to preserve + the underlying structure if possible (i.e. expand_mul should not be + applied to self). + + Examples + ======== + + >>> from sympy import sqrt + >>> from sympy.abc import x, y, z + + >>> eq = 2 + 2*x + 2*y*(3 + 3*y) + + The as_content_primitive function is recursive and retains structure: + + >>> eq.as_content_primitive() + (2, x + 3*y*(y + 1) + 1) + + Integer powers will have Rationals extracted from the base: + + >>> ((2 + 6*x)**2).as_content_primitive() + (4, (3*x + 1)**2) + >>> ((2 + 6*x)**(2*y)).as_content_primitive() + (1, (2*(3*x + 1))**(2*y)) + + Terms may end up joining once their as_content_primitives are added: + + >>> ((5*(x*(1 + y)) + 2*x*(3 + 3*y))).as_content_primitive() + (11, x*(y + 1)) + >>> ((3*(x*(1 + y)) + 2*x*(3 + 3*y))).as_content_primitive() + (9, x*(y + 1)) + >>> ((3*(z*(1 + y)) + 2.0*x*(3 + 3*y))).as_content_primitive() + (1, 6.0*x*(y + 1) + 3*z*(y + 1)) + >>> ((5*(x*(1 + y)) + 2*x*(3 + 3*y))**2).as_content_primitive() + (121, x**2*(y + 1)**2) + >>> ((x*(1 + y) + 0.4*x*(3 + 3*y))**2).as_content_primitive() + (1, 4.84*x**2*(y + 1)**2) + + Radical content can also be factored out of the primitive: + + >>> (2*sqrt(2) + 4*sqrt(10)).as_content_primitive(radical=True) + (2, sqrt(2)*(1 + 2*sqrt(5))) + + If clear=False (default is True) then content will not be removed + from an Add if it can be distributed to leave one or more + terms with integer coefficients. + + >>> (x/2 + y).as_content_primitive() + (1/2, x + 2*y) + >>> (x/2 + y).as_content_primitive(clear=False) + (1, x/2 + y) + """ + return S.One, self + + def as_numer_denom(self) -> tuple[Expr, Expr]: + """Return the numerator and the denominator of an expression. + + expression -> a/b -> a, b + + This is just a stub that should be defined by + an object's class methods to get anything else. + + See Also + ======== + + normal: return ``a/b`` instead of ``(a, b)`` + + """ + return self, S.One + + def normal(self): + """Return the expression as a fraction. + + expression -> a/b + + See Also + ======== + + as_numer_denom: return ``(a, b)`` instead of ``a/b`` + + """ + from .mul import _unevaluated_Mul + n, d = self.as_numer_denom() + if d is S.One: + return n + if d.is_Number: + return _unevaluated_Mul(n, 1/d) + else: + return n/d + + def extract_multiplicatively(self, c: Expr) -> Expr | None: + """Return None if it's not possible to make self in the form + c * something in a nice way, i.e. preserving the properties + of arguments of self. + + Examples + ======== + + >>> from sympy import symbols, Rational + + >>> x, y = symbols('x,y', real=True) + + >>> ((x*y)**3).extract_multiplicatively(x**2 * y) + x*y**2 + + >>> ((x*y)**3).extract_multiplicatively(x**4 * y) + + >>> (2*x).extract_multiplicatively(2) + x + + >>> (2*x).extract_multiplicatively(3) + + >>> (Rational(1, 2)*x).extract_multiplicatively(3) + x/6 + + """ + from sympy.functions.elementary.exponential import exp + from .add import _unevaluated_Add + c = sympify(c) + if self is S.NaN: + return None + if c is S.One: + return self + elif c == self: + return S.One + + if c.is_Add: + cc, pc = c.primitive() + if cc is not S.One: + c = Mul(cc, pc, evaluate=False) + + if c.is_Mul: + a, b = c.as_two_terms() # type: ignore + x = self.extract_multiplicatively(a) + if x is not None: + return x.extract_multiplicatively(b) + else: + return x + + quotient = self / c + if self.is_Number: + if self is S.Infinity: + if c.is_positive: + return S.Infinity + elif self is S.NegativeInfinity: + if c.is_negative: + return S.Infinity + elif c.is_positive: + return S.NegativeInfinity + elif self is S.ComplexInfinity: + if not c.is_zero: + return S.ComplexInfinity + elif self.is_Integer: + if not quotient.is_Integer: + return None + elif self.is_positive and quotient.is_negative: + return None + else: + return quotient + elif self.is_Rational: + if not quotient.is_Rational: + return None + elif self.is_positive and quotient.is_negative: + return None + else: + return quotient + elif self.is_Float: + if not quotient.is_Float: + return None + elif self.is_positive and quotient.is_negative: + return None + else: + return quotient + elif self.is_NumberSymbol or self.is_Symbol or self is S.ImaginaryUnit: + if quotient.is_Mul and len(quotient.args) == 2: + if quotient.args[0].is_Integer and quotient.args[0].is_positive and quotient.args[1] == self: + return quotient + elif quotient.is_Integer and c.is_Number: + return quotient + elif self.is_Add: + cs, ps = self.primitive() + # assert cs >= 1 + if c.is_Number and c is not S.NegativeOne: + # assert c != 1 (handled at top) + if cs is not S.One: + if c.is_negative: + xc = cs.extract_multiplicatively(-c) + if xc is not None: + xc = -xc + else: + xc = cs.extract_multiplicatively(c) + if xc is not None: + return xc*ps # rely on 2-arg Mul to restore Add + return None # |c| != 1 can only be extracted from cs + if c == ps: + return cs + # check args of ps + newargs = [] + arg: Expr + for arg in ps.args: # type: ignore + newarg = arg.extract_multiplicatively(c) + if newarg is None: + return None # all or nothing + newargs.append(newarg) + if cs is not S.One: + args = [cs*t for t in newargs] + # args may be in different order + return _unevaluated_Add(*args) + else: + return Add._from_args(newargs) + elif self.is_Mul: + args: list[Expr] = list(self.args) # type: ignore + for i, arg in enumerate(args): + newarg = arg.extract_multiplicatively(c) + if newarg is not None: + args[i] = newarg + return Mul(*args) + elif self.is_Pow or isinstance(self, exp): + sb, se = self.as_base_exp() + cb, ce = c.as_base_exp() + if cb == sb: + new_exp = se.extract_additively(ce) + if new_exp is not None: + return Pow(sb, new_exp) + elif c == sb: + new_exp = se.extract_additively(1) + if new_exp is not None: + return Pow(sb, new_exp) + + return None + + def extract_additively(self, c): + """Return self - c if it's possible to subtract c from self and + make all matching coefficients move towards zero, else return None. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> e = 2*x + 3 + >>> e.extract_additively(x + 1) + x + 2 + >>> e.extract_additively(3*x) + >>> e.extract_additively(4) + >>> (y*(x + 1)).extract_additively(x + 1) + >>> ((x + 1)*(x + 2*y + 1) + 3).extract_additively(x + 1) + (x + 1)*(x + 2*y) + 3 + + See Also + ======== + extract_multiplicatively + coeff + as_coefficient + + """ + + c = sympify(c) + if self is S.NaN: + return None + if c.is_zero: + return self + elif c == self: + return S.Zero + elif self == S.Zero: + return None + + if self.is_Number: + if not c.is_Number: + return None + co = self + diff = co - c + # XXX should we match types? i.e should 3 - .1 succeed? + if (co > 0 and diff >= 0 and diff < co or + co < 0 and diff <= 0 and diff > co): + return diff + return None + + if c.is_Number: + co, t = self.as_coeff_Add() + xa = co.extract_additively(c) + if xa is None: + return None + return xa + t + + # handle the args[0].is_Number case separately + # since we will have trouble looking for the coeff of + # a number. + if c.is_Add and c.args[0].is_Number: + # whole term as a term factor + co = self.coeff(c) + xa0 = (co.extract_additively(1) or 0)*c + if xa0: + diff = self - co*c + return (xa0 + (diff.extract_additively(c) or diff)) or None + # term-wise + h, t = c.as_coeff_Add() + sh, st = self.as_coeff_Add() + xa = sh.extract_additively(h) + if xa is None: + return None + xa2 = st.extract_additively(t) + if xa2 is None: + return None + return xa + xa2 + + # whole term as a term factor + co, diff = _corem(self, c) + xa0 = (co.extract_additively(1) or 0)*c + if xa0: + return (xa0 + (diff.extract_additively(c) or diff)) or None + # term-wise + coeffs = [] + for a in Add.make_args(c): + ac, at = a.as_coeff_Mul() + co = self.coeff(at) + if not co: + return None + coc, cot = co.as_coeff_Add() + xa = coc.extract_additively(ac) + if xa is None: + return None + self -= co*at + coeffs.append((cot + xa)*at) + coeffs.append(self) + return Add(*coeffs) + + @property + def expr_free_symbols(self): + """ + Like ``free_symbols``, but returns the free symbols only if + they are contained in an expression node. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> (x + y).expr_free_symbols # doctest: +SKIP + {x, y} + + If the expression is contained in a non-expression object, do not return + the free symbols. Compare: + + >>> from sympy import Tuple + >>> t = Tuple(x + y) + >>> t.expr_free_symbols # doctest: +SKIP + set() + >>> t.free_symbols + {x, y} + """ + sympy_deprecation_warning(""" + The expr_free_symbols property is deprecated. Use free_symbols to get + the free symbols of an expression. + """, + deprecated_since_version="1.9", + active_deprecations_target="deprecated-expr-free-symbols") + return {j for i in self.args for j in i.expr_free_symbols} + + def could_extract_minus_sign(self) -> bool: + """Return True if self has -1 as a leading factor or has + more literal negative signs than positive signs in a sum, + otherwise False. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> e = x - y + >>> {i.could_extract_minus_sign() for i in (e, -e)} + {False, True} + + Though the ``y - x`` is considered like ``-(x - y)``, since it + is in a product without a leading factor of -1, the result is + false below: + + >>> (x*(y - x)).could_extract_minus_sign() + False + + To put something in canonical form wrt to sign, use `signsimp`: + + >>> from sympy import signsimp + >>> signsimp(x*(y - x)) + -x*(x - y) + >>> _.could_extract_minus_sign() + True + """ + return False + + def extract_branch_factor(self, allow_half=False): + """ + Try to write self as ``exp_polar(2*pi*I*n)*z`` in a nice way. + Return (z, n). + + >>> from sympy import exp_polar, I, pi + >>> from sympy.abc import x, y + >>> exp_polar(I*pi).extract_branch_factor() + (exp_polar(I*pi), 0) + >>> exp_polar(2*I*pi).extract_branch_factor() + (1, 1) + >>> exp_polar(-pi*I).extract_branch_factor() + (exp_polar(I*pi), -1) + >>> exp_polar(3*pi*I + x).extract_branch_factor() + (exp_polar(x + I*pi), 1) + >>> (y*exp_polar(-5*pi*I)*exp_polar(3*pi*I + 2*pi*x)).extract_branch_factor() + (y*exp_polar(2*pi*x), -1) + >>> exp_polar(-I*pi/2).extract_branch_factor() + (exp_polar(-I*pi/2), 0) + + If allow_half is True, also extract exp_polar(I*pi): + + >>> exp_polar(I*pi).extract_branch_factor(allow_half=True) + (1, 1/2) + >>> exp_polar(2*I*pi).extract_branch_factor(allow_half=True) + (1, 1) + >>> exp_polar(3*I*pi).extract_branch_factor(allow_half=True) + (1, 3/2) + >>> exp_polar(-I*pi).extract_branch_factor(allow_half=True) + (1, -1/2) + """ + from sympy.functions.elementary.exponential import exp_polar + from sympy.functions.elementary.integers import ceiling + + n = S.Zero + res = S.One + args = Mul.make_args(self) + exps = [] + for arg in args: + if isinstance(arg, exp_polar): + exps += [arg.exp] + else: + res *= arg + piimult = S.Zero + extras = [] + + ipi = S.Pi*S.ImaginaryUnit + while exps: + exp = exps.pop() + if exp.is_Add: + exps += exp.args + continue + if exp.is_Mul: + coeff = exp.as_coefficient(ipi) + if coeff is not None: + piimult += coeff + continue + extras += [exp] + if piimult.is_number: + coeff = piimult + tail = () + else: + coeff, tail = piimult.as_coeff_add(*piimult.free_symbols) + # round down to nearest multiple of 2 + branchfact = ceiling(coeff/2 - S.Half)*2 + n += branchfact/2 + c = coeff - branchfact + if allow_half: + nc = c.extract_additively(1) + if nc is not None: + n += S.Half + c = nc + newexp = ipi*Add(*((c, ) + tail)) + Add(*extras) + if newexp != 0: + res *= exp_polar(newexp) + return res, n + + def is_polynomial(self, *syms): + r""" + Return True if self is a polynomial in syms and False otherwise. + + This checks if self is an exact polynomial in syms. This function + returns False for expressions that are "polynomials" with symbolic + exponents. Thus, you should be able to apply polynomial algorithms to + expressions for which this returns True, and Poly(expr, \*syms) should + work if and only if expr.is_polynomial(\*syms) returns True. The + polynomial does not have to be in expanded form. If no symbols are + given, all free symbols in the expression will be used. + + This is not part of the assumptions system. You cannot do + Symbol('z', polynomial=True). + + Examples + ======== + + >>> from sympy import Symbol, Function + >>> x = Symbol('x') + >>> ((x**2 + 1)**4).is_polynomial(x) + True + >>> ((x**2 + 1)**4).is_polynomial() + True + >>> (2**x + 1).is_polynomial(x) + False + >>> (2**x + 1).is_polynomial(2**x) + True + >>> f = Function('f') + >>> (f(x) + 1).is_polynomial(x) + False + >>> (f(x) + 1).is_polynomial(f(x)) + True + >>> (1/f(x) + 1).is_polynomial(f(x)) + False + + >>> n = Symbol('n', nonnegative=True, integer=True) + >>> (x**n + 1).is_polynomial(x) + False + + This function does not attempt any nontrivial simplifications that may + result in an expression that does not appear to be a polynomial to + become one. + + >>> from sympy import sqrt, factor, cancel + >>> y = Symbol('y', positive=True) + >>> a = sqrt(y**2 + 2*y + 1) + >>> a.is_polynomial(y) + False + >>> factor(a) + y + 1 + >>> factor(a).is_polynomial(y) + True + + >>> b = (y**2 + 2*y + 1)/(y + 1) + >>> b.is_polynomial(y) + False + >>> cancel(b) + y + 1 + >>> cancel(b).is_polynomial(y) + True + + See also .is_rational_function() + + """ + if syms: + syms = set(map(sympify, syms)) + else: + syms = self.free_symbols + if not syms: + return True + + return self._eval_is_polynomial(syms) + + def _eval_is_polynomial(self, syms) -> bool | None: + if self in syms: + return True + if not self.has_free(*syms): + # constant polynomial + return True + # subclasses should return True or False + return None + + def is_rational_function(self, *syms): + """ + Test whether function is a ratio of two polynomials in the given + symbols, syms. When syms is not given, all free symbols will be used. + The rational function does not have to be in expanded or in any kind of + canonical form. + + This function returns False for expressions that are "rational + functions" with symbolic exponents. Thus, you should be able to call + .as_numer_denom() and apply polynomial algorithms to the result for + expressions for which this returns True. + + This is not part of the assumptions system. You cannot do + Symbol('z', rational_function=True). + + Examples + ======== + + >>> from sympy import Symbol, sin + >>> from sympy.abc import x, y + + >>> (x/y).is_rational_function() + True + + >>> (x**2).is_rational_function() + True + + >>> (x/sin(y)).is_rational_function(y) + False + + >>> n = Symbol('n', integer=True) + >>> (x**n + 1).is_rational_function(x) + False + + This function does not attempt any nontrivial simplifications that may + result in an expression that does not appear to be a rational function + to become one. + + >>> from sympy import sqrt, factor + >>> y = Symbol('y', positive=True) + >>> a = sqrt(y**2 + 2*y + 1)/y + >>> a.is_rational_function(y) + False + >>> factor(a) + (y + 1)/y + >>> factor(a).is_rational_function(y) + True + + See also is_algebraic_expr(). + + """ + if syms: + syms = set(map(sympify, syms)) + else: + syms = self.free_symbols + if not syms: + return self not in _illegal + + return self._eval_is_rational_function(syms) + + def _eval_is_rational_function(self, syms) -> bool | None: + if self in syms: + return True + if not self.has_xfree(syms): + return True + # subclasses should return True or False + return None + + def is_meromorphic(self, x, a): + """ + This tests whether an expression is meromorphic as + a function of the given symbol ``x`` at the point ``a``. + + This method is intended as a quick test that will return + None if no decision can be made without simplification or + more detailed analysis. + + Examples + ======== + + >>> from sympy import zoo, log, sin, sqrt + >>> from sympy.abc import x + + >>> f = 1/x**2 + 1 - 2*x**3 + >>> f.is_meromorphic(x, 0) + True + >>> f.is_meromorphic(x, 1) + True + >>> f.is_meromorphic(x, zoo) + True + + >>> g = x**log(3) + >>> g.is_meromorphic(x, 0) + False + >>> g.is_meromorphic(x, 1) + True + >>> g.is_meromorphic(x, zoo) + False + + >>> h = sin(1/x)*x**2 + >>> h.is_meromorphic(x, 0) + False + >>> h.is_meromorphic(x, 1) + True + >>> h.is_meromorphic(x, zoo) + True + + Multivalued functions are considered meromorphic when their + branches are meromorphic. Thus most functions are meromorphic + everywhere except at essential singularities and branch points. + In particular, they will be meromorphic also on branch cuts + except at their endpoints. + + >>> log(x).is_meromorphic(x, -1) + True + >>> log(x).is_meromorphic(x, 0) + False + >>> sqrt(x).is_meromorphic(x, -1) + True + >>> sqrt(x).is_meromorphic(x, 0) + False + + """ + if not x.is_symbol: + raise TypeError("{} should be of symbol type".format(x)) + a = sympify(a) + + return self._eval_is_meromorphic(x, a) + + def _eval_is_meromorphic(self, x, a) -> bool | None: + if self == x: + return True + if not self.has_free(x): + return True + # subclasses should return True or False + return None + + def is_algebraic_expr(self, *syms): + """ + This tests whether a given expression is algebraic or not, in the + given symbols, syms. When syms is not given, all free symbols + will be used. The rational function does not have to be in expanded + or in any kind of canonical form. + + This function returns False for expressions that are "algebraic + expressions" with symbolic exponents. This is a simple extension to the + is_rational_function, including rational exponentiation. + + Examples + ======== + + >>> from sympy import Symbol, sqrt + >>> x = Symbol('x', real=True) + >>> sqrt(1 + x).is_rational_function() + False + >>> sqrt(1 + x).is_algebraic_expr() + True + + This function does not attempt any nontrivial simplifications that may + result in an expression that does not appear to be an algebraic + expression to become one. + + >>> from sympy import exp, factor + >>> a = sqrt(exp(x)**2 + 2*exp(x) + 1)/(exp(x) + 1) + >>> a.is_algebraic_expr(x) + False + >>> factor(a).is_algebraic_expr() + True + + See Also + ======== + + is_rational_function + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Algebraic_expression + + """ + if syms: + syms = set(map(sympify, syms)) + else: + syms = self.free_symbols + if not syms: + return True + + return self._eval_is_algebraic_expr(syms) + + def _eval_is_algebraic_expr(self, syms) -> bool | None: + if self in syms: + return True + if not self.has_free(*syms): + return True + # subclasses should return True or False + return None + + ################################################################################### + ##################### SERIES, LEADING TERM, LIMIT, ORDER METHODS ################## + ################################################################################### + + def series(self, x=None, x0=0, n=6, dir="+", logx=None, cdir=0): + """ + Series expansion of "self" around ``x = x0`` yielding either terms of + the series one by one (the lazy series given when n=None), else + all the terms at once when n != None. + + Returns the series expansion of "self" around the point ``x = x0`` + with respect to ``x`` up to ``O((x - x0)**n, x, x0)`` (default n is 6). + + If ``x=None`` and ``self`` is univariate, the univariate symbol will + be supplied, otherwise an error will be raised. + + Parameters + ========== + + expr : Expression + The expression whose series is to be expanded. + + x : Symbol + It is the variable of the expression to be calculated. + + x0 : Value + The value around which ``x`` is calculated. Can be any value + from ``-oo`` to ``oo``. + + n : Value + The value used to represent the order in terms of ``x**n``, + up to which the series is to be expanded. + + dir : String, optional + The series-expansion can be bi-directional. If ``dir="+"``, + then (x->x0+). If ``dir="-", then (x->x0-). For infinite + ``x0`` (``oo`` or ``-oo``), the ``dir`` argument is determined + from the direction of the infinity (i.e., ``dir="-"`` for + ``oo``). + + logx : optional + It is used to replace any log(x) in the returned series with a + symbolic value rather than evaluating the actual value. + + cdir : optional + It stands for complex direction, and indicates the direction + from which the expansion needs to be evaluated. + + Examples + ======== + + >>> from sympy import cos, exp, tan + >>> from sympy.abc import x, y + >>> cos(x).series() + 1 - x**2/2 + x**4/24 + O(x**6) + >>> cos(x).series(n=4) + 1 - x**2/2 + O(x**4) + >>> cos(x).series(x, x0=1, n=2) + cos(1) - (x - 1)*sin(1) + O((x - 1)**2, (x, 1)) + >>> e = cos(x + exp(y)) + >>> e.series(y, n=2) + cos(x + 1) - y*sin(x + 1) + O(y**2) + >>> e.series(x, n=2) + cos(exp(y)) - x*sin(exp(y)) + O(x**2) + + If ``n=None`` then a generator of the series terms will be returned. + + >>> term=cos(x).series(n=None) + >>> [next(term) for i in range(2)] + [1, -x**2/2] + + For ``dir=+`` (default) the series is calculated from the right and + for ``dir=-`` the series from the left. For smooth functions this + flag will not alter the results. + + >>> abs(x).series(dir="+") + x + >>> abs(x).series(dir="-") + -x + >>> f = tan(x) + >>> f.series(x, 2, 6, "+") + tan(2) + (1 + tan(2)**2)*(x - 2) + (x - 2)**2*(tan(2)**3 + tan(2)) + + (x - 2)**3*(1/3 + 4*tan(2)**2/3 + tan(2)**4) + (x - 2)**4*(tan(2)**5 + + 5*tan(2)**3/3 + 2*tan(2)/3) + (x - 2)**5*(2/15 + 17*tan(2)**2/15 + + 2*tan(2)**4 + tan(2)**6) + O((x - 2)**6, (x, 2)) + + >>> f.series(x, 2, 3, "-") + tan(2) + (2 - x)*(-tan(2)**2 - 1) + (2 - x)**2*(tan(2)**3 + tan(2)) + + O((x - 2)**3, (x, 2)) + + For rational expressions this method may return original expression without the Order term. + >>> (1/x).series(x, n=8) + 1/x + + Returns + ======= + + Expr : Expression + Series expansion of the expression about x0 + + Raises + ====== + + TypeError + If "n" and "x0" are infinity objects + + PoleError + If "x0" is an infinity object + + """ + if x is None: + syms = self.free_symbols + if not syms: + return self + elif len(syms) > 1: + raise ValueError('x must be given for multivariate functions.') + x = syms.pop() + + from .symbol import Dummy, Symbol + if isinstance(x, Symbol): + dep = x in self.free_symbols + else: + d = Dummy() + dep = d in self.xreplace({x: d}).free_symbols + if not dep: + if n is None: + return (s for s in [self]) + else: + return self + + if len(dir) != 1 or dir not in '+-': + raise ValueError("Dir must be '+' or '-'") + + if n is not None: + n = int(n) + if n < 0: + raise ValueError("Number of terms should be nonnegative") + + x0 = sympify(x0) + cdir = sympify(cdir) + from sympy.functions.elementary.complexes import im, sign + + if not cdir.is_zero: + if cdir.is_real: + dir = '+' if cdir.is_positive else '-' + else: + dir = '+' if im(cdir).is_positive else '-' + else: + if x0 and x0.is_infinite: + cdir = sign(x0).simplify() + elif str(dir) == "+": + cdir = S.One + elif str(dir) == "-": + cdir = S.NegativeOne + elif cdir == S.Zero: + cdir = S.One + + cdir = cdir/abs(cdir) + + if x0 and x0.is_infinite: + from .function import PoleError + try: + s = self.subs(x, cdir/x).series(x, n=n, dir='+', cdir=1) + if n is None: + return (si.subs(x, cdir/x) for si in s) + return s.subs(x, cdir/x) + except PoleError: + s = self.subs(x, cdir*x).aseries(x, n=n) + return s.subs(x, cdir*x) + + # use rep to shift origin to x0 and change sign (if dir is negative) + # and undo the process with rep2 + if x0 or cdir != 1: + s = self.subs({x: x0 + cdir*x}).series(x, x0=0, n=n, dir='+', logx=logx, cdir=1) + if n is None: # lseries... + return (si.subs({x: x/cdir - x0/cdir}) for si in s) + return s.subs({x: x/cdir - x0/cdir}) + + # from here on it's x0=0 and dir='+' handling + + if x.is_positive is x.is_negative is None or x.is_Symbol is not True: + # replace x with an x that has a positive assumption + xpos = Dummy('x', positive=True) + rv = self.subs(x, xpos).series(xpos, x0, n, dir, logx=logx, cdir=cdir) + if n is None: + return (s.subs(xpos, x) for s in rv) + else: + return rv.subs(xpos, x) + + from sympy.series.order import Order + if n is not None: # nseries handling + s1 = self._eval_nseries(x, n=n, logx=logx, cdir=cdir) + o = s1.getO() or S.Zero + if o: + # make sure the requested order is returned + ngot = o.getn() + if ngot > n: + # leave o in its current form (e.g. with x*log(x)) so + # it eats terms properly, then replace it below + if n != 0: + s1 += o.subs(x, x**Rational(n, ngot)) + else: + s1 += Order(1, x) + elif ngot < n: + # increase the requested number of terms to get the desired + # number keep increasing (up to 9) until the received order + # is different than the original order and then predict how + # many additional terms are needed + from sympy.functions.elementary.integers import ceiling + for more in range(1, 9): + s1 = self._eval_nseries(x, n=n + more, logx=logx, cdir=cdir) + newn = s1.getn() + if newn != ngot: + ndo = n + ceiling((n - ngot)*more/(newn - ngot)) + s1 = self._eval_nseries(x, n=ndo, logx=logx, cdir=cdir) + while s1.getn() < n: + s1 = self._eval_nseries(x, n=ndo, logx=logx, cdir=cdir) + ndo += 1 + break + else: + raise ValueError('Could not calculate %s terms for %s' + % (str(n), self)) + s1 += Order(x**n, x) + o = s1.getO() + s1 = s1.removeO() + elif s1.has(Order): + # asymptotic expansion + return s1 + else: + o = Order(x**n, x) + s1done = s1.doit() + try: + if (s1done + o).removeO() == s1done: + o = S.Zero + except NotImplementedError: + return s1 + + try: + from sympy.simplify.radsimp import collect + return collect(s1, x) + o + except NotImplementedError: + return s1 + o + + else: # lseries handling + def yield_lseries(s): + """Return terms of lseries one at a time.""" + for si in s: + if not si.is_Add: + yield si + continue + # yield terms 1 at a time if possible + # by increasing order until all the + # terms have been returned + yielded = 0 + o = Order(si, x)*x + ndid = 0 + ndo = len(si.args) + while 1: + do = (si - yielded + o).removeO() + o *= x + if not do or do.is_Order: + continue + if do.is_Add: + ndid += len(do.args) + else: + ndid += 1 + yield do + if ndid == ndo: + break + yielded += do + + return yield_lseries(self.removeO()._eval_lseries(x, logx=logx, cdir=cdir)) + + def aseries(self, x=None, n=6, bound=0, hir=False): + """Asymptotic Series expansion of self. + This is equivalent to ``self.series(x, oo, n)``. + + Parameters + ========== + + self : Expression + The expression whose series is to be expanded. + + x : Symbol + It is the variable of the expression to be calculated. + + n : Value + The value used to represent the order in terms of ``x**n``, + up to which the series is to be expanded. + + hir : Boolean + Set this parameter to be True to produce hierarchical series. + It stops the recursion at an early level and may provide nicer + and more useful results. + + bound : Value, Integer + Use the ``bound`` parameter to give limit on rewriting + coefficients in its normalised form. + + Examples + ======== + + >>> from sympy import sin, exp + >>> from sympy.abc import x + + >>> e = sin(1/x + exp(-x)) - sin(1/x) + + >>> e.aseries(x) + (1/(24*x**4) - 1/(2*x**2) + 1 + O(x**(-6), (x, oo)))*exp(-x) + + >>> e.aseries(x, n=3, hir=True) + -exp(-2*x)*sin(1/x)/2 + exp(-x)*cos(1/x) + O(exp(-3*x), (x, oo)) + + >>> e = exp(exp(x)/(1 - 1/x)) + + >>> e.aseries(x) + exp(exp(x)/(1 - 1/x)) + + >>> e.aseries(x, bound=3) # doctest: +SKIP + exp(exp(x)/x**2)*exp(exp(x)/x)*exp(-exp(x) + exp(x)/(1 - 1/x) - exp(x)/x - exp(x)/x**2)*exp(exp(x)) + + For rational expressions this method may return original expression without the Order term. + >>> (1/x).aseries(x, n=8) + 1/x + + Returns + ======= + + Expr + Asymptotic series expansion of the expression. + + Notes + ===== + + This algorithm is directly induced from the limit computational algorithm provided by Gruntz. + It majorly uses the mrv and rewrite sub-routines. The overall idea of this algorithm is first + to look for the most rapidly varying subexpression w of a given expression f and then expands f + in a series in w. Then same thing is recursively done on the leading coefficient + till we get constant coefficients. + + If the most rapidly varying subexpression of a given expression f is f itself, + the algorithm tries to find a normalised representation of the mrv set and rewrites f + using this normalised representation. + + If the expansion contains an order term, it will be either ``O(x ** (-n))`` or ``O(w ** (-n))`` + where ``w`` belongs to the most rapidly varying expression of ``self``. + + References + ========== + + .. [1] Gruntz, Dominik. A new algorithm for computing asymptotic series. + In: Proc. 1993 Int. Symp. Symbolic and Algebraic Computation. 1993. + pp. 239-244. + .. [2] Gruntz thesis - p90 + .. [3] https://en.wikipedia.org/wiki/Asymptotic_expansion + + See Also + ======== + + Expr.aseries: See the docstring of this function for complete details of this wrapper. + """ + + from .symbol import Dummy + + if x.is_positive is x.is_negative is None: + xpos = Dummy('x', positive=True) + return self.subs(x, xpos).aseries(xpos, n, bound, hir).subs(xpos, x) + + from .function import PoleError + from sympy.series.gruntz import mrv, rewrite + + try: + om, exps = mrv(self, x) + except PoleError: + return self + + # We move one level up by replacing `x` by `exp(x)`, and then + # computing the asymptotic series for f(exp(x)). Then asymptotic series + # can be obtained by moving one-step back, by replacing x by ln(x). + + from sympy.functions.elementary.exponential import exp, log + from sympy.series.order import Order + + if x in om: + s = self.subs(x, exp(x)).aseries(x, n, bound, hir).subs(x, log(x)) + if s.getO(): + return s + Order(1/x**n, (x, S.Infinity)) + return s + + k = Dummy('k', positive=True) + # f is rewritten in terms of omega + func, logw = rewrite(exps, om, x, k) + + if self in om: + if bound <= 0: + return self + s = (self.exp).aseries(x, n, bound=bound) + s = s.func(*[t.removeO() for t in s.args]) + try: + res = exp(s.subs(x, 1/x).as_leading_term(x).subs(x, 1/x)) + except PoleError: + res = self + + func = exp(self.args[0] - res.args[0]) / k + logw = log(1/res) + + s = func.series(k, 0, n) + from sympy.core.function import expand_mul + s = expand_mul(s) + # Hierarchical series + if hir: + return s.subs(k, exp(logw)) + + o = s.getO() + terms = sorted(Add.make_args(s.removeO()), key=lambda i: int(i.as_coeff_exponent(k)[1])) + s = S.Zero + has_ord = False + + # Then we recursively expand these coefficients one by one into + # their asymptotic series in terms of their most rapidly varying subexpressions. + for t in terms: + coeff, expo = t.as_coeff_exponent(k) + if coeff.has(x): + # Recursive step + snew = coeff.aseries(x, n, bound=bound-1) + if has_ord and snew.getO(): + break + elif snew.getO(): + has_ord = True + s += (snew * k**expo) + else: + s += t + + if not o or has_ord: + return s.subs(k, exp(logw)) + return (s + o).subs(k, exp(logw)) + + + def taylor_term(self, n, x, *previous_terms): + """General method for the taylor term. + + This method is slow, because it differentiates n-times. Subclasses can + redefine it to make it faster by using the "previous_terms". + """ + from .symbol import Dummy + from sympy.functions.combinatorial.factorials import factorial + + x = sympify(x) + _x = Dummy('x') + return self.subs(x, _x).diff(_x, n).subs(_x, x).subs(x, 0) * x**n / factorial(n) + + def lseries(self, x=None, x0=0, dir='+', logx=None, cdir=0): + """ + Wrapper for series yielding an iterator of the terms of the series. + + Note: an infinite series will yield an infinite iterator. The following, + for exaxmple, will never terminate. It will just keep printing terms + of the sin(x) series:: + + for term in sin(x).lseries(x): + print term + + The advantage of lseries() over nseries() is that many times you are + just interested in the next term in the series (i.e. the first term for + example), but you do not know how many you should ask for in nseries() + using the "n" parameter. + + See also nseries(). + """ + return self.series(x, x0, n=None, dir=dir, logx=logx, cdir=cdir) + + def _eval_lseries(self, x, logx=None, cdir=0): + # default implementation of lseries is using nseries(), and adaptively + # increasing the "n". As you can see, it is not very efficient, because + # we are calculating the series over and over again. Subclasses should + # override this method and implement much more efficient yielding of + # terms. + n = 0 + series = self._eval_nseries(x, n=n, logx=logx, cdir=cdir) + + while series.is_Order: + n += 1 + series = self._eval_nseries(x, n=n, logx=logx, cdir=cdir) + + e = series.removeO() + yield e + if e is S.Zero: + return + + while 1: + while 1: + n += 1 + series = self._eval_nseries(x, n=n, logx=logx, cdir=cdir).removeO() + if e != series: + break + if (series - self).cancel() is S.Zero: + return + yield series - e + e = series + + def nseries(self, x=None, x0=0, n=6, dir='+', logx=None, cdir=0): + """ + Wrapper to _eval_nseries if assumptions allow, else to series. + + If x is given, x0 is 0, dir='+', and self has x, then _eval_nseries is + called. This calculates "n" terms in the innermost expressions and + then builds up the final series just by "cross-multiplying" everything + out. + + The optional ``logx`` parameter can be used to replace any log(x) in the + returned series with a symbolic value to avoid evaluating log(x) at 0. A + symbol to use in place of log(x) should be provided. + + Advantage -- it's fast, because we do not have to determine how many + terms we need to calculate in advance. + + Disadvantage -- you may end up with less terms than you may have + expected, but the O(x**n) term appended will always be correct and + so the result, though perhaps shorter, will also be correct. + + If any of those assumptions is not met, this is treated like a + wrapper to series which will try harder to return the correct + number of terms. + + See also lseries(). + + Examples + ======== + + >>> from sympy import sin, log, Symbol + >>> from sympy.abc import x, y + >>> sin(x).nseries(x, 0, 6) + x - x**3/6 + x**5/120 + O(x**6) + >>> log(x+1).nseries(x, 0, 5) + x - x**2/2 + x**3/3 - x**4/4 + O(x**5) + + Handling of the ``logx`` parameter --- in the following example the + expansion fails since ``sin`` does not have an asymptotic expansion + at -oo (the limit of log(x) as x approaches 0): + + >>> e = sin(log(x)) + >>> e.nseries(x, 0, 6) + Traceback (most recent call last): + ... + PoleError: ... + ... + >>> logx = Symbol('logx') + >>> e.nseries(x, 0, 6, logx=logx) + sin(logx) + + In the following example, the expansion works but only returns self + unless the ``logx`` parameter is used: + + >>> e = x**y + >>> e.nseries(x, 0, 2) + x**y + >>> e.nseries(x, 0, 2, logx=logx) + exp(logx*y) + + """ + if x and x not in self.free_symbols: + return self + if x is None or x0 or dir != '+': # {see XPOS above} or (x.is_positive == x.is_negative == None): + return self.series(x, x0, n, dir, cdir=cdir) + else: + return self._eval_nseries(x, n=n, logx=logx, cdir=cdir) + + def _eval_nseries(self, x, n, logx, cdir): + """ + Return terms of series for self up to O(x**n) at x=0 + from the positive direction. + + This is a method that should be overridden in subclasses. Users should + never call this method directly (use .nseries() instead), so you do not + have to write docstrings for _eval_nseries(). + """ + raise NotImplementedError(filldedent(""" + The _eval_nseries method should be added to + %s to give terms up to O(x**n) at x=0 + from the positive direction so it is available when + nseries calls it.""" % self.func) + ) + + def limit(self, x, xlim, dir='+'): + """ Compute limit x->xlim. + """ + from sympy.series.limits import limit + return limit(self, x, xlim, dir) + + @cacheit + def as_leading_term(self, *symbols, logx=None, cdir=0): + """ + Returns the leading (nonzero) term of the series expansion of self. + + The _eval_as_leading_term routines are used to do this, and they must + always return a non-zero value. + + Examples + ======== + + >>> from sympy.abc import x + >>> (1 + x + x**2).as_leading_term(x) + 1 + >>> (1/x**2 + x + x**2).as_leading_term(x) + x**(-2) + + """ + if len(symbols) > 1: + c = self + for x in symbols: + c = c.as_leading_term(x, logx=logx, cdir=cdir) + return c + elif not symbols: + return self + x = sympify(symbols[0]) + cdir = sympify(cdir) + if not x.is_symbol: + raise ValueError('expecting a Symbol but got %s' % x) + if x not in self.free_symbols: + return self + obj = self._eval_as_leading_term(x, logx=logx, cdir=cdir) + if obj is not None: + from sympy.simplify.powsimp import powsimp + return powsimp(obj, deep=True, combine='exp') + raise NotImplementedError('as_leading_term(%s, %s)' % (self, x)) + + def _eval_as_leading_term(self, x, logx, cdir): + return self + + def as_coeff_exponent(self, x) -> tuple[Expr, Expr]: + """ ``c*x**e -> c,e`` where x can be any symbolic expression. + """ + from sympy.simplify.radsimp import collect + s = collect(self, x) + c, p = s.as_coeff_mul(x) + if len(p) == 1: + b, e = p[0].as_base_exp() + if b == x: + return c, e + return s, S.Zero + + def leadterm(self, x, logx=None, cdir=0): + """ + Returns the leading term a*x**b as a tuple (a, b). + + Examples + ======== + + >>> from sympy.abc import x + >>> (1+x+x**2).leadterm(x) + (1, 0) + >>> (1/x**2+x+x**2).leadterm(x) + (1, -2) + + """ + from .symbol import Dummy + from sympy.functions.elementary.exponential import log + l = self.as_leading_term(x, logx=logx, cdir=cdir) + d = Dummy('logx') + if l.has(log(x)): + l = l.subs(log(x), d) + c, e = l.as_coeff_exponent(x) + if x in c.free_symbols: + raise ValueError(filldedent(""" + cannot compute leadterm(%s, %s). The coefficient + should have been free of %s but got %s""" % (self, x, x, c))) + c = c.subs(d, log(x)) + return c, e + + def as_coeff_Mul(self, rational: bool = False) -> tuple['Number', Expr]: + """Efficiently extract the coefficient of a product.""" + return S.One, self + + def as_coeff_Add(self, rational=False) -> tuple['Number', Expr]: + """Efficiently extract the coefficient of a summation.""" + return S.Zero, self + + def fps(self, x=None, x0=0, dir=1, hyper=True, order=4, rational=True, + full=False): + """ + Compute formal power power series of self. + + See the docstring of the :func:`fps` function in sympy.series.formal for + more information. + """ + from sympy.series.formal import fps + + return fps(self, x, x0, dir, hyper, order, rational, full) + + def fourier_series(self, limits=None): + """Compute fourier sine/cosine series of self. + + See the docstring of the :func:`fourier_series` in sympy.series.fourier + for more information. + """ + from sympy.series.fourier import fourier_series + + return fourier_series(self, limits) + + ################################################################################### + ##################### DERIVATIVE, INTEGRAL, FUNCTIONAL METHODS #################### + ################################################################################### + + def diff(self, *symbols, **assumptions): + assumptions.setdefault("evaluate", True) + return _derivative_dispatch(self, *symbols, **assumptions) + + ########################################################################### + ###################### EXPRESSION EXPANSION METHODS ####################### + ########################################################################### + + # Relevant subclasses should override _eval_expand_hint() methods. See + # the docstring of expand() for more info. + + def _eval_expand_complex(self, **hints): + real, imag = self.as_real_imag(**hints) + return real + S.ImaginaryUnit*imag + + @staticmethod + def _expand_hint(expr, hint, deep=True, **hints): + """ + Helper for ``expand()``. Recursively calls ``expr._eval_expand_hint()``. + + Returns ``(expr, hit)``, where expr is the (possibly) expanded + ``expr`` and ``hit`` is ``True`` if ``expr`` was truly expanded and + ``False`` otherwise. + """ + hit = False + # XXX: Hack to support non-Basic args + # | + # V + if deep and getattr(expr, 'args', ()) and not expr.is_Atom: + sargs = [] + for arg in expr.args: + arg, arghit = Expr._expand_hint(arg, hint, **hints) + hit |= arghit + sargs.append(arg) + + if hit: + expr = expr.func(*sargs) + + if hasattr(expr, hint): + newexpr = getattr(expr, hint)(**hints) + if newexpr != expr: + return (newexpr, True) + + return (expr, hit) + + @cacheit + def expand(self, deep=True, modulus=None, power_base=True, power_exp=True, + mul=True, log=True, multinomial=True, basic=True, **hints): + """ + Expand an expression using hints. + + See the docstring of the expand() function in sympy.core.function for + more information. + + """ + from sympy.simplify.radsimp import fraction + + hints.update(power_base=power_base, power_exp=power_exp, mul=mul, + log=log, multinomial=multinomial, basic=basic) + + expr = self + # default matches fraction's default + _fraction = lambda x: fraction(x, hints.get('exact', False)) + if hints.pop('frac', False): + n, d = [a.expand(deep=deep, modulus=modulus, **hints) + for a in _fraction(self)] + return n/d + elif hints.pop('denom', False): + n, d = _fraction(self) + return n/d.expand(deep=deep, modulus=modulus, **hints) + elif hints.pop('numer', False): + n, d = _fraction(self) + return n.expand(deep=deep, modulus=modulus, **hints)/d + + # Although the hints are sorted here, an earlier hint may get applied + # at a given node in the expression tree before another because of how + # the hints are applied. e.g. expand(log(x*(y + z))) -> log(x*y + + # x*z) because while applying log at the top level, log and mul are + # applied at the deeper level in the tree so that when the log at the + # upper level gets applied, the mul has already been applied at the + # lower level. + + # Additionally, because hints are only applied once, the expression + # may not be expanded all the way. For example, if mul is applied + # before multinomial, x*(x + 1)**2 won't be expanded all the way. For + # now, we just use a special case to make multinomial run before mul, + # so that at least polynomials will be expanded all the way. In the + # future, smarter heuristics should be applied. + # TODO: Smarter heuristics + + def _expand_hint_key(hint): + """Make multinomial come before mul""" + if hint == 'mul': + return 'mulz' + return hint + + for hint in sorted(hints.keys(), key=_expand_hint_key): + use_hint = hints[hint] + if use_hint: + hint = '_eval_expand_' + hint + expr, hit = Expr._expand_hint(expr, hint, deep=deep, **hints) + + while True: + was = expr + if hints.get('multinomial', False): + expr, _ = Expr._expand_hint( + expr, '_eval_expand_multinomial', deep=deep, **hints) + if hints.get('mul', False): + expr, _ = Expr._expand_hint( + expr, '_eval_expand_mul', deep=deep, **hints) + if hints.get('log', False): + expr, _ = Expr._expand_hint( + expr, '_eval_expand_log', deep=deep, **hints) + if expr == was: + break + + if modulus is not None: + modulus = sympify(modulus) + + if not modulus.is_Integer or modulus <= 0: + raise ValueError( + "modulus must be a positive integer, got %s" % modulus) + + terms = [] + + for term in Add.make_args(expr): + coeff, tail = term.as_coeff_Mul(rational=True) + + coeff %= modulus + + if coeff: + terms.append(coeff*tail) + + expr = Add(*terms) + + return expr + + ########################################################################### + ################### GLOBAL ACTION VERB WRAPPER METHODS #################### + ########################################################################### + + def integrate(self, *args, **kwargs): + """See the integrate function in sympy.integrals""" + from sympy.integrals.integrals import integrate + return integrate(self, *args, **kwargs) + + def nsimplify(self, constants=(), tolerance=None, full=False): + """See the nsimplify function in sympy.simplify""" + from sympy.simplify.simplify import nsimplify + return nsimplify(self, constants, tolerance, full) + + def separate(self, deep=False, force=False): + """See the separate function in sympy.simplify""" + from .function import expand_power_base + return expand_power_base(self, deep=deep, force=force) + + def collect(self, syms, func=None, evaluate=True, exact=False, distribute_order_term=True): + """See the collect function in sympy.simplify""" + from sympy.simplify.radsimp import collect + return collect(self, syms, func, evaluate, exact, distribute_order_term) + + def together(self, *args, **kwargs): + """See the together function in sympy.polys""" + from sympy.polys.rationaltools import together + return together(self, *args, **kwargs) + + def apart(self, x=None, **args): + """See the apart function in sympy.polys""" + from sympy.polys.partfrac import apart + return apart(self, x, **args) + + def ratsimp(self): + """See the ratsimp function in sympy.simplify""" + from sympy.simplify.ratsimp import ratsimp + return ratsimp(self) + + def trigsimp(self, **args): + """See the trigsimp function in sympy.simplify""" + from sympy.simplify.trigsimp import trigsimp + return trigsimp(self, **args) + + def radsimp(self, **kwargs): + """See the radsimp function in sympy.simplify""" + from sympy.simplify.radsimp import radsimp + return radsimp(self, **kwargs) + + def powsimp(self, *args, **kwargs): + """See the powsimp function in sympy.simplify""" + from sympy.simplify.powsimp import powsimp + return powsimp(self, *args, **kwargs) + + def combsimp(self): + """See the combsimp function in sympy.simplify""" + from sympy.simplify.combsimp import combsimp + return combsimp(self) + + def gammasimp(self): + """See the gammasimp function in sympy.simplify""" + from sympy.simplify.gammasimp import gammasimp + return gammasimp(self) + + def factor(self, *gens, **args): + """See the factor() function in sympy.polys.polytools""" + from sympy.polys.polytools import factor + return factor(self, *gens, **args) + + def cancel(self, *gens, **args): + """See the cancel function in sympy.polys""" + from sympy.polys.polytools import cancel + return cancel(self, *gens, **args) + + def invert(self, g, *gens, **args): + """Return the multiplicative inverse of ``self`` mod ``g`` + where ``self`` (and ``g``) may be symbolic expressions). + + See Also + ======== + sympy.core.intfunc.mod_inverse, sympy.polys.polytools.invert + """ + if self.is_number and getattr(g, 'is_number', True): + return mod_inverse(self, g) + from sympy.polys.polytools import invert + return invert(self, g, *gens, **args) + + def round(self, n=None): + """Return x rounded to the given decimal place. + + If a complex number would results, apply round to the real + and imaginary components of the number. + + Examples + ======== + + >>> from sympy import pi, E, I, S, Number + >>> pi.round() + 3 + >>> pi.round(2) + 3.14 + >>> (2*pi + E*I).round() + 6 + 3*I + + The round method has a chopping effect: + + >>> (2*pi + I/10).round() + 6 + >>> (pi/10 + 2*I).round() + 2*I + >>> (pi/10 + E*I).round(2) + 0.31 + 2.72*I + + Notes + ===== + + The Python ``round`` function uses the SymPy ``round`` method so it + will always return a SymPy number (not a Python float or int): + + >>> isinstance(round(S(123), -2), Number) + True + """ + x = self + + if not x.is_number: + raise TypeError("Cannot round symbolic expression") + if not x.is_Atom: + if not pure_complex(x.n(2), or_real=True): + raise TypeError( + 'Expected a number but got %s:' % func_name(x)) + elif x in _illegal: + return x + if not (xr := x.is_extended_real): + r, i = x.as_real_imag() + if xr is False: + return r.round(n) + S.ImaginaryUnit*i.round(n) + if i.equals(0): + return r.round(n) + if not x: + return S.Zero if n is None else x + + p = as_int(n or 0) + + if x.is_Integer: + return Integer(round(int(x), p)) + + digits_to_decimal = _mag(x) # _mag(12) = 2, _mag(.012) = -1 + allow = digits_to_decimal + p + precs = [f._prec for f in x.atoms(Float)] + dps = prec_to_dps(max(precs)) if precs else None + if dps is None: + # assume everything is exact so use the Python + # float default or whatever was requested + dps = max(15, allow) + else: + allow = min(allow, dps) + # this will shift all digits to right of decimal + # and give us dps to work with as an int + shift = -digits_to_decimal + dps + extra = 1 # how far we look past known digits + # NOTE + # mpmath will calculate the binary representation to + # an arbitrary number of digits but we must base our + # answer on a finite number of those digits, e.g. + # .575 2589569785738035/2**52 in binary. + # mpmath shows us that the first 18 digits are + # >>> Float(.575).n(18) + # 0.574999999999999956 + # The default precision is 15 digits and if we ask + # for 15 we get + # >>> Float(.575).n(15) + # 0.575000000000000 + # mpmath handles rounding at the 15th digit. But we + # need to be careful since the user might be asking + # for rounding at the last digit and our semantics + # are to round toward the even final digit when there + # is a tie. So the extra digit will be used to make + # that decision. In this case, the value is the same + # to 15 digits: + # >>> Float(.575).n(16) + # 0.5750000000000000 + # Now converting this to the 15 known digits gives + # 575000000000000.0 + # which rounds to integer + # 5750000000000000 + # And now we can round to the desired digt, e.g. at + # the second from the left and we get + # 5800000000000000 + # and rescaling that gives + # 0.58 + # as the final result. + # If the value is made slightly less than 0.575 we might + # still obtain the same value: + # >>> Float(.575-1e-16).n(16)*10**15 + # 574999999999999.8 + # What 15 digits best represents the known digits (which are + # to the left of the decimal? 5750000000000000, the same as + # before. The only way we will round down (in this case) is + # if we declared that we had more than 15 digits of precision. + # For example, if we use 16 digits of precision, the integer + # we deal with is + # >>> Float(.575-1e-16).n(17)*10**16 + # 5749999999999998.4 + # and this now rounds to 5749999999999998 and (if we round to + # the 2nd digit from the left) we get 5700000000000000. + # + xf = x.n(dps + extra)*Pow(10, shift) + if xf.is_Number and xf._prec == 1: # xf.is_Add will raise below + # is x == 0? + if x.equals(0): + return Float(0) + raise ValueError('not computing with precision') + xi = Integer(xf) + # use the last digit to select the value of xi + # nearest to x before rounding at the desired digit + sign = 1 if x > 0 else -1 + dif2 = sign*(xf - xi).n(extra) + if dif2 < 0: + raise NotImplementedError( + 'not expecting int(x) to round away from 0') + if dif2 > .5: + xi += sign # round away from 0 + elif dif2 == .5: + xi += sign if xi%2 else -sign # round toward even + # shift p to the new position + ip = p - shift + # let Python handle the int rounding then rescale + xr = round(xi.p, ip) + # restore scale + rv = Rational(xr, Pow(10, shift)) + # return Float or Integer + if rv.is_Integer: + if n is None: # the single-arg case + return rv + # use str or else it won't be a float + return Float(str(rv), dps) # keep same precision + else: + if not allow and rv > self: + allow += 1 + return Float(rv, allow) + + __round__ = round + + def _eval_derivative_matrix_lines(self, x): + from sympy.matrices.expressions.matexpr import _LeftRightArgs + return [_LeftRightArgs([S.One, S.One], higher=self._eval_derivative(x))] + + +class AtomicExpr(Atom, Expr): + """ + A parent class for object which are both atoms and Exprs. + + For example: Symbol, Number, Rational, Integer, ... + But not: Add, Mul, Pow, ... + """ + is_number = False + is_Atom = True + + __slots__ = () + + def _eval_derivative(self, s): + if self == s: + return S.One + return S.Zero + + def _eval_derivative_n_times(self, s, n): + from .containers import Tuple + from sympy.matrices.expressions.matexpr import MatrixExpr + from sympy.matrices.matrixbase import MatrixBase + if isinstance(s, (MatrixBase, Tuple, Iterable, MatrixExpr)): + return super()._eval_derivative_n_times(s, n) + from .relational import Eq + from sympy.functions.elementary.piecewise import Piecewise + if self == s: + return Piecewise((self, Eq(n, 0)), (1, Eq(n, 1)), (0, True)) + else: + return Piecewise((self, Eq(n, 0)), (0, True)) + + def _eval_is_polynomial(self, syms): + return True + + def _eval_is_rational_function(self, syms): + return self not in _illegal + + def _eval_is_meromorphic(self, x, a): + from sympy.calculus.accumulationbounds import AccumBounds + return (not self.is_Number or self.is_finite) and not isinstance(self, AccumBounds) + + def _eval_is_algebraic_expr(self, syms): + return True + + def _eval_nseries(self, x, n, logx, cdir=0): + return self + + @property + def expr_free_symbols(self): + sympy_deprecation_warning(""" + The expr_free_symbols property is deprecated. Use free_symbols to get + the free symbols of an expression. + """, + deprecated_since_version="1.9", + active_deprecations_target="deprecated-expr-free-symbols") + return {self} + + +def _mag(x): + r"""Return integer $i$ such that $0.1 \le x/10^i < 1$ + + Examples + ======== + + >>> from sympy.core.expr import _mag + >>> from sympy import Float + >>> _mag(Float(.1)) + 0 + >>> _mag(Float(.01)) + -1 + >>> _mag(Float(1234)) + 4 + """ + from math import log10, ceil, log + xpos = abs(x.n()) + if not xpos: + return S.Zero + try: + mag_first_dig = int(ceil(log10(xpos))) + except (ValueError, OverflowError): + mag_first_dig = int(ceil(Float(mpf_log(xpos._mpf_, 53))/log(10))) + # check that we aren't off by 1 + if (xpos/S(10)**mag_first_dig) >= 1: + assert 1 <= (xpos/S(10)**mag_first_dig) < 10 + mag_first_dig += 1 + return mag_first_dig + + +class UnevaluatedExpr(Expr): + """ + Expression that is not evaluated unless released. + + Examples + ======== + + >>> from sympy import UnevaluatedExpr + >>> from sympy.abc import x + >>> x*(1/x) + 1 + >>> x*UnevaluatedExpr(1/x) + x*1/x + + """ + + def __new__(cls, arg, **kwargs): + arg = _sympify(arg) + obj = Expr.__new__(cls, arg, **kwargs) + return obj + + def doit(self, **hints): + if hints.get("deep", True): + return self.args[0].doit(**hints) + else: + return self.args[0] + + + +def unchanged(func, *args): + """Return True if `func` applied to the `args` is unchanged. + Can be used instead of `assert foo == foo`. + + Examples + ======== + + >>> from sympy import Piecewise, cos, pi + >>> from sympy.core.expr import unchanged + >>> from sympy.abc import x + + >>> unchanged(cos, 1) # instead of assert cos(1) == cos(1) + True + + >>> unchanged(cos, pi) + False + + Comparison of args uses the builtin capabilities of the object's + arguments to test for equality so args can be defined loosely. Here, + the ExprCondPair arguments of Piecewise compare as equal to the + tuples that can be used to create the Piecewise: + + >>> unchanged(Piecewise, (x, x > 1), (0, True)) + True + """ + f = func(*args) + return f.func == func and f.args == args + + +class ExprBuilder: + def __init__(self, op, args=None, validator=None, check=True): + if not hasattr(op, "__call__"): + raise TypeError("op {} needs to be callable".format(op)) + self.op = op + if args is None: + self.args = [] + else: + self.args = args + self.validator = validator + if (validator is not None) and check: + self.validate() + + @staticmethod + def _build_args(args): + return [i.build() if isinstance(i, ExprBuilder) else i for i in args] + + def validate(self): + if self.validator is None: + return + args = self._build_args(self.args) + self.validator(*args) + + def build(self, check=True): + args = self._build_args(self.args) + if self.validator and check: + self.validator(*args) + return self.op(*args) + + def append_argument(self, arg, check=True): + self.args.append(arg) + if self.validator and check: + self.validate(*self.args) + + def __getitem__(self, item): + if item == 0: + return self.op + else: + return self.args[item-1] + + def __repr__(self): + return str(self.build()) + + def search_element(self, elem): + for i, arg in enumerate(self.args): + if isinstance(arg, ExprBuilder): + ret = arg.search_index(elem) + if ret is not None: + return (i,) + ret + elif id(arg) == id(elem): + return (i,) + return None + + +from .mul import Mul +from .add import Add +from .power import Pow +from .function import Function, _derivative_dispatch +from .mod import Mod +from .exprtools import factor_terms +from .numbers import Float, Integer, Rational, _illegal, int_valued diff --git a/venv/lib/python3.11/site-packages/sympy/core/exprtools.py b/venv/lib/python3.11/site-packages/sympy/core/exprtools.py new file mode 100644 index 0000000000000000000000000000000000000000..4868e4416a72e91dc12c9113d90b9c31c5e26011 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/exprtools.py @@ -0,0 +1,1573 @@ +"""Tools for manipulating of large commutative expressions. """ + +from __future__ import annotations + +from .add import Add +from .mul import Mul, _keep_coeff +from .power import Pow +from .basic import Basic +from .expr import Expr +from .function import expand_power_exp +from .sympify import sympify +from .numbers import Rational, Integer, Number, I, equal_valued +from .singleton import S +from .sorting import default_sort_key, ordered +from .symbol import Dummy +from .traversal import preorder_traversal +from .coreerrors import NonCommutativeExpression +from .containers import Tuple, Dict +from sympy.external.gmpy import SYMPY_INTS +from sympy.utilities.iterables import (common_prefix, common_suffix, + variations, iterable, is_sequence) + +from collections import defaultdict + + +_eps = Dummy(positive=True) + + +def _isnumber(i): + return isinstance(i, (SYMPY_INTS, float)) or i.is_Number + + +def _monotonic_sign(self): + """Return the value closest to 0 that ``self`` may have if all symbols + are signed and the result is uniformly the same sign for all values of symbols. + If a symbol is only signed but not known to be an + integer or the result is 0 then a symbol representative of the sign of self + will be returned. Otherwise, None is returned if a) the sign could be positive + or negative or b) self is not in one of the following forms: + + - L(x, y, ...) + A: a function linear in all symbols x, y, ... with an + additive constant; if A is zero then the function can be a monomial whose + sign is monotonic over the range of the variables, e.g. (x + 1)**3 if x is + nonnegative. + - A/L(x, y, ...) + B: the inverse of a function linear in all symbols x, y, ... + that does not have a sign change from positive to negative for any set + of values for the variables. + - M(x, y, ...) + A: a monomial M whose factors are all signed and a constant, A. + - A/M(x, y, ...) + B: the inverse of a monomial and constants A and B. + - P(x): a univariate polynomial + + Examples + ======== + + >>> from sympy.core.exprtools import _monotonic_sign as F + >>> from sympy import Dummy + >>> nn = Dummy(integer=True, nonnegative=True) + >>> p = Dummy(integer=True, positive=True) + >>> p2 = Dummy(integer=True, positive=True) + >>> F(nn + 1) + 1 + >>> F(p - 1) + _nneg + >>> F(nn*p + 1) + 1 + >>> F(p2*p + 1) + 2 + >>> F(nn - 1) # could be negative, zero or positive + """ + if not self.is_extended_real: + return + + if (-self).is_Symbol: + rv = _monotonic_sign(-self) + return rv if rv is None else -rv + + if not self.is_Add and self.as_numer_denom()[1].is_number: + s = self + if s.is_prime: + if s.is_odd: + return Integer(3) + else: + return Integer(2) + elif s.is_composite: + if s.is_odd: + return Integer(9) + else: + return Integer(4) + elif s.is_positive: + if s.is_even: + if s.is_prime is False: + return Integer(4) + else: + return Integer(2) + elif s.is_integer: + return S.One + else: + return _eps + elif s.is_extended_negative: + if s.is_even: + return Integer(-2) + elif s.is_integer: + return S.NegativeOne + else: + return -_eps + if s.is_zero or s.is_extended_nonpositive or s.is_extended_nonnegative: + return S.Zero + return None + + # univariate polynomial + free = self.free_symbols + if len(free) == 1: + if self.is_polynomial(): + from sympy.polys.polytools import real_roots + from sympy.polys.polyroots import roots + from sympy.polys.polyerrors import PolynomialError + x = free.pop() + x0 = _monotonic_sign(x) + if x0 in (_eps, -_eps): + x0 = S.Zero + if x0 is not None: + d = self.diff(x) + if d.is_number: + currentroots = [] + else: + try: + currentroots = real_roots(d) + except (PolynomialError, NotImplementedError): + currentroots = [r for r in roots(d, x) if r.is_extended_real] + y = self.subs(x, x0) + if x.is_nonnegative and all( + (r - x0).is_nonpositive for r in currentroots): + if y.is_nonnegative and d.is_positive: + if y: + return y if y.is_positive else Dummy('pos', positive=True) + else: + return Dummy('nneg', nonnegative=True) + if y.is_nonpositive and d.is_negative: + if y: + return y if y.is_negative else Dummy('neg', negative=True) + else: + return Dummy('npos', nonpositive=True) + elif x.is_nonpositive and all( + (r - x0).is_nonnegative for r in currentroots): + if y.is_nonnegative and d.is_negative: + if y: + return Dummy('pos', positive=True) + else: + return Dummy('nneg', nonnegative=True) + if y.is_nonpositive and d.is_positive: + if y: + return Dummy('neg', negative=True) + else: + return Dummy('npos', nonpositive=True) + else: + n, d = self.as_numer_denom() + den = None + if n.is_number: + den = _monotonic_sign(d) + elif not d.is_number: + if _monotonic_sign(n) is not None: + den = _monotonic_sign(d) + if den is not None and (den.is_positive or den.is_negative): + v = n*den + if v.is_positive: + return Dummy('pos', positive=True) + elif v.is_nonnegative: + return Dummy('nneg', nonnegative=True) + elif v.is_negative: + return Dummy('neg', negative=True) + elif v.is_nonpositive: + return Dummy('npos', nonpositive=True) + return None + + # multivariate + c, a = self.as_coeff_Add() + v = None + if not a.is_polynomial(): + # F/A or A/F where A is a number and F is a signed, rational monomial + n, d = a.as_numer_denom() + if not (n.is_number or d.is_number): + return + if ( + a.is_Mul or a.is_Pow) and \ + a.is_rational and \ + all(p.exp.is_Integer for p in a.atoms(Pow) if p.is_Pow) and \ + (a.is_positive or a.is_negative): + v = S.One + for ai in Mul.make_args(a): + if ai.is_number: + v *= ai + continue + reps = {} + for x in ai.free_symbols: + reps[x] = _monotonic_sign(x) + if reps[x] is None: + return + v *= ai.subs(reps) + elif c: + # signed linear expression + if not any(p for p in a.atoms(Pow) if not p.is_number) and (a.is_nonpositive or a.is_nonnegative): + free = list(a.free_symbols) + p = {} + for i in free: + v = _monotonic_sign(i) + if v is None: + return + p[i] = v or (_eps if i.is_nonnegative else -_eps) + v = a.xreplace(p) + if v is not None: + rv = v + c + if v.is_nonnegative and rv.is_positive: + return rv.subs(_eps, 0) + if v.is_nonpositive and rv.is_negative: + return rv.subs(_eps, 0) + + +def decompose_power(expr: Expr) -> tuple[Expr, int]: + """ + Decompose power into symbolic base and integer exponent. + + Examples + ======== + + >>> from sympy.core.exprtools import decompose_power + >>> from sympy.abc import x, y + >>> from sympy import exp + + >>> decompose_power(x) + (x, 1) + >>> decompose_power(x**2) + (x, 2) + >>> decompose_power(exp(2*y/3)) + (exp(y/3), 2) + + """ + base, exp = expr.as_base_exp() + + if exp.is_Number: + if exp.is_Rational: + if not exp.is_Integer: + base = Pow(base, Rational(1, exp.q)) # type: ignore + e = exp.p # type: ignore + else: + base, e = expr, 1 + else: + exp, tail = exp.as_coeff_Mul(rational=True) + + if exp is S.NegativeOne: + base, e = Pow(base, tail), -1 + elif exp is not S.One: + # todo: after dropping python 3.7 support, use overload and Literal + # in as_coeff_Mul to make exp Rational, and remove these 2 ignores + tail = _keep_coeff(Rational(1, exp.q), tail) # type: ignore + base, e = Pow(base, tail), exp.p # type: ignore + else: + base, e = expr, 1 + + return base, e + + +def decompose_power_rat(expr: Expr) -> tuple[Expr, Rational]: + """ + Decompose power into symbolic base and rational exponent; + if the exponent is not a Rational, then separate only the + integer coefficient. + + Examples + ======== + + >>> from sympy.core.exprtools import decompose_power_rat + >>> from sympy.abc import x + >>> from sympy import sqrt, exp + + >>> decompose_power_rat(sqrt(x)) + (x, 1/2) + >>> decompose_power_rat(exp(-3*x/2)) + (exp(x/2), -3) + + """ + base, exp = expr.as_base_exp() + if not exp.is_Rational: + base, exp_i = decompose_power(expr) + exp = Integer(exp_i) + return base, exp # type: ignore + + +class Factors: + """Efficient representation of ``f_1*f_2*...*f_n``.""" + + __slots__ = ('factors', 'gens') + + def __init__(self, factors=None): # Factors + """Initialize Factors from dict or expr. + + Examples + ======== + + >>> from sympy.core.exprtools import Factors + >>> from sympy.abc import x + >>> from sympy import I + >>> e = 2*x**3 + >>> Factors(e) + Factors({2: 1, x: 3}) + >>> Factors(e.as_powers_dict()) + Factors({2: 1, x: 3}) + >>> f = _ + >>> f.factors # underlying dictionary + {2: 1, x: 3} + >>> f.gens # base of each factor + frozenset({2, x}) + >>> Factors(0) + Factors({0: 1}) + >>> Factors(I) + Factors({I: 1}) + + Notes + ===== + + Although a dictionary can be passed, only minimal checking is + performed: powers of -1 and I are made canonical. + + """ + if isinstance(factors, (SYMPY_INTS, float)): + factors = S(factors) + if isinstance(factors, Factors): + factors = factors.factors.copy() + elif factors in (None, S.One): + factors = {} + elif factors is S.Zero or factors == 0: + factors = {S.Zero: S.One} + elif isinstance(factors, Number): + n = factors + factors = {} + if n < 0: + factors[S.NegativeOne] = S.One + n = -n + if n is not S.One: + if n.is_Float or n.is_Integer or n is S.Infinity: + factors[n] = S.One + elif n.is_Rational: + # since we're processing Numbers, the denominator is + # stored with a negative exponent; all other factors + # are left . + if n.p != 1: + factors[Integer(n.p)] = S.One + factors[Integer(n.q)] = S.NegativeOne + else: + raise ValueError('Expected Float|Rational|Integer, not %s' % n) + elif isinstance(factors, Basic) and not factors.args: + factors = {factors: S.One} + elif isinstance(factors, Expr): + c, nc = factors.args_cnc() + i = c.count(I) + for _ in range(i): + c.remove(I) + factors = dict(Mul._from_args(c).as_powers_dict()) + # Handle all rational Coefficients + for f in list(factors.keys()): + if isinstance(f, Rational) and not isinstance(f, Integer): + p, q = Integer(f.p), Integer(f.q) + factors[p] = (factors[p] if p in factors else S.Zero) + factors[f] + factors[q] = (factors[q] if q in factors else S.Zero) - factors[f] + factors.pop(f) + if i: + factors[I] = factors.get(I, S.Zero) + i + if nc: + factors[Mul(*nc, evaluate=False)] = S.One + else: + factors = factors.copy() # /!\ should be dict-like + + # tidy up -/+1 and I exponents if Rational + + handle = [k for k in factors if k is I or k in (-1, 1)] + if handle: + i1 = S.One + for k in handle: + if not _isnumber(factors[k]): + continue + i1 *= k**factors.pop(k) + if i1 is not S.One: + for a in i1.args if i1.is_Mul else [i1]: # at worst, -1.0*I*(-1)**e + if a is S.NegativeOne: + factors[a] = S.One + elif a is I: + factors[I] = S.One + elif a.is_Pow: + factors[a.base] = factors.get(a.base, S.Zero) + a.exp + elif equal_valued(a, 1): + factors[a] = S.One + elif equal_valued(a, -1): + factors[-a] = S.One + factors[S.NegativeOne] = S.One + else: + raise ValueError('unexpected factor in i1: %s' % a) + + self.factors = factors + keys = getattr(factors, 'keys', None) + if keys is None: + raise TypeError('expecting Expr or dictionary') + self.gens = frozenset(keys()) + + def __hash__(self): # Factors + keys = tuple(ordered(self.factors.keys())) + values = [self.factors[k] for k in keys] + return hash((keys, values)) + + def __repr__(self): # Factors + return "Factors({%s})" % ', '.join( + ['%s: %s' % (k, v) for k, v in ordered(self.factors.items())]) + + @property + def is_zero(self): # Factors + """ + >>> from sympy.core.exprtools import Factors + >>> Factors(0).is_zero + True + """ + f = self.factors + return len(f) == 1 and S.Zero in f + + @property + def is_one(self): # Factors + """ + >>> from sympy.core.exprtools import Factors + >>> Factors(1).is_one + True + """ + return not self.factors + + def as_expr(self): # Factors + """Return the underlying expression. + + Examples + ======== + + >>> from sympy.core.exprtools import Factors + >>> from sympy.abc import x, y + >>> Factors((x*y**2).as_powers_dict()).as_expr() + x*y**2 + + """ + + args = [] + for factor, exp in self.factors.items(): + if exp != 1: + if isinstance(exp, Integer): + b, e = factor.as_base_exp() + e = _keep_coeff(exp, e) + args.append(b**e) + else: + args.append(factor**exp) + else: + args.append(factor) + return Mul(*args) + + def mul(self, other): # Factors + """Return Factors of ``self * other``. + + Examples + ======== + + >>> from sympy.core.exprtools import Factors + >>> from sympy.abc import x, y, z + >>> a = Factors((x*y**2).as_powers_dict()) + >>> b = Factors((x*y/z).as_powers_dict()) + >>> a.mul(b) + Factors({x: 2, y: 3, z: -1}) + >>> a*b + Factors({x: 2, y: 3, z: -1}) + """ + if not isinstance(other, Factors): + other = Factors(other) + if any(f.is_zero for f in (self, other)): + return Factors(S.Zero) + factors = dict(self.factors) + + for factor, exp in other.factors.items(): + if factor in factors: + exp = factors[factor] + exp + + if not exp: + del factors[factor] + continue + + factors[factor] = exp + + return Factors(factors) + + def normal(self, other): + """Return ``self`` and ``other`` with ``gcd`` removed from each. + The only differences between this and method ``div`` is that this + is 1) optimized for the case when there are few factors in common and + 2) this does not raise an error if ``other`` is zero. + + See Also + ======== + div + + """ + if not isinstance(other, Factors): + other = Factors(other) + if other.is_zero: + return (Factors(), Factors(S.Zero)) + if self.is_zero: + return (Factors(S.Zero), Factors()) + + self_factors = dict(self.factors) + other_factors = dict(other.factors) + + for factor, self_exp in self.factors.items(): + try: + other_exp = other.factors[factor] + except KeyError: + continue + + exp = self_exp - other_exp + + if not exp: + del self_factors[factor] + del other_factors[factor] + elif _isnumber(exp): + if exp > 0: + self_factors[factor] = exp + del other_factors[factor] + else: + del self_factors[factor] + other_factors[factor] = -exp + else: + r = self_exp.extract_additively(other_exp) + if r is not None: + if r: + self_factors[factor] = r + del other_factors[factor] + else: # should be handled already + del self_factors[factor] + del other_factors[factor] + else: + sc, sa = self_exp.as_coeff_Add() + if sc: + oc, oa = other_exp.as_coeff_Add() + diff = sc - oc + if diff > 0: + self_factors[factor] -= oc + other_exp = oa + elif diff < 0: + self_factors[factor] -= sc + other_factors[factor] -= sc + other_exp = oa - diff + else: + self_factors[factor] = sa + other_exp = oa + if other_exp: + other_factors[factor] = other_exp + else: + del other_factors[factor] + + return Factors(self_factors), Factors(other_factors) + + def div(self, other): # Factors + """Return ``self`` and ``other`` with ``gcd`` removed from each. + This is optimized for the case when there are many factors in common. + + Examples + ======== + + >>> from sympy.core.exprtools import Factors + >>> from sympy.abc import x, y, z + >>> from sympy import S + + >>> a = Factors((x*y**2).as_powers_dict()) + >>> a.div(a) + (Factors({}), Factors({})) + >>> a.div(x*z) + (Factors({y: 2}), Factors({z: 1})) + + The ``/`` operator only gives ``quo``: + + >>> a/x + Factors({y: 2}) + + Factors treats its factors as though they are all in the numerator, so + if you violate this assumption the results will be correct but will + not strictly correspond to the numerator and denominator of the ratio: + + >>> a.div(x/z) + (Factors({y: 2}), Factors({z: -1})) + + Factors is also naive about bases: it does not attempt any denesting + of Rational-base terms, for example the following does not become + 2**(2*x)/2. + + >>> Factors(2**(2*x + 2)).div(S(8)) + (Factors({2: 2*x + 2}), Factors({8: 1})) + + factor_terms can clean up such Rational-bases powers: + + >>> from sympy import factor_terms + >>> n, d = Factors(2**(2*x + 2)).div(S(8)) + >>> n.as_expr()/d.as_expr() + 2**(2*x + 2)/8 + >>> factor_terms(_) + 2**(2*x)/2 + + """ + quo, rem = dict(self.factors), {} + + if not isinstance(other, Factors): + other = Factors(other) + if other.is_zero: + raise ZeroDivisionError + if self.is_zero: + return (Factors(S.Zero), Factors()) + + for factor, exp in other.factors.items(): + if factor in quo: + d = quo[factor] - exp + if _isnumber(d): + if d <= 0: + del quo[factor] + + if d >= 0: + if d: + quo[factor] = d + + continue + + exp = -d + + else: + r = quo[factor].extract_additively(exp) + if r is not None: + if r: + quo[factor] = r + else: # should be handled already + del quo[factor] + else: + other_exp = exp + sc, sa = quo[factor].as_coeff_Add() + if sc: + oc, oa = other_exp.as_coeff_Add() + diff = sc - oc + if diff > 0: + quo[factor] -= oc + other_exp = oa + elif diff < 0: + quo[factor] -= sc + other_exp = oa - diff + else: + quo[factor] = sa + other_exp = oa + if other_exp: + rem[factor] = other_exp + else: + assert factor not in rem + continue + + rem[factor] = exp + + return Factors(quo), Factors(rem) + + def quo(self, other): # Factors + """Return numerator Factor of ``self / other``. + + Examples + ======== + + >>> from sympy.core.exprtools import Factors + >>> from sympy.abc import x, y, z + >>> a = Factors((x*y**2).as_powers_dict()) + >>> b = Factors((x*y/z).as_powers_dict()) + >>> a.quo(b) # same as a/b + Factors({y: 1}) + """ + return self.div(other)[0] + + def rem(self, other): # Factors + """Return denominator Factors of ``self / other``. + + Examples + ======== + + >>> from sympy.core.exprtools import Factors + >>> from sympy.abc import x, y, z + >>> a = Factors((x*y**2).as_powers_dict()) + >>> b = Factors((x*y/z).as_powers_dict()) + >>> a.rem(b) + Factors({z: -1}) + >>> a.rem(a) + Factors({}) + """ + return self.div(other)[1] + + def pow(self, other): # Factors + """Return self raised to a non-negative integer power. + + Examples + ======== + + >>> from sympy.core.exprtools import Factors + >>> from sympy.abc import x, y + >>> a = Factors((x*y**2).as_powers_dict()) + >>> a**2 + Factors({x: 2, y: 4}) + + """ + if isinstance(other, Factors): + other = other.as_expr() + if other.is_Integer: + other = int(other) + if isinstance(other, SYMPY_INTS) and other >= 0: + factors = {} + + if other: + for factor, exp in self.factors.items(): + factors[factor] = exp*other + + return Factors(factors) + else: + raise ValueError("expected non-negative integer, got %s" % other) + + def gcd(self, other): # Factors + """Return Factors of ``gcd(self, other)``. The keys are + the intersection of factors with the minimum exponent for + each factor. + + Examples + ======== + + >>> from sympy.core.exprtools import Factors + >>> from sympy.abc import x, y, z + >>> a = Factors((x*y**2).as_powers_dict()) + >>> b = Factors((x*y/z).as_powers_dict()) + >>> a.gcd(b) + Factors({x: 1, y: 1}) + """ + if not isinstance(other, Factors): + other = Factors(other) + if other.is_zero: + return Factors(self.factors) + + factors = {} + + for factor, exp in self.factors.items(): + factor, exp = sympify(factor), sympify(exp) + if factor in other.factors: + lt = (exp - other.factors[factor]).is_negative + if lt == True: + factors[factor] = exp + elif lt == False: + factors[factor] = other.factors[factor] + + return Factors(factors) + + def lcm(self, other): # Factors + """Return Factors of ``lcm(self, other)`` which are + the union of factors with the maximum exponent for + each factor. + + Examples + ======== + + >>> from sympy.core.exprtools import Factors + >>> from sympy.abc import x, y, z + >>> a = Factors((x*y**2).as_powers_dict()) + >>> b = Factors((x*y/z).as_powers_dict()) + >>> a.lcm(b) + Factors({x: 1, y: 2, z: -1}) + """ + if not isinstance(other, Factors): + other = Factors(other) + if any(f.is_zero for f in (self, other)): + return Factors(S.Zero) + + factors = dict(self.factors) + + for factor, exp in other.factors.items(): + if factor in factors: + exp = max(exp, factors[factor]) + + factors[factor] = exp + + return Factors(factors) + + def __mul__(self, other): # Factors + return self.mul(other) + + def __divmod__(self, other): # Factors + return self.div(other) + + def __truediv__(self, other): # Factors + return self.quo(other) + + def __mod__(self, other): # Factors + return self.rem(other) + + def __pow__(self, other): # Factors + return self.pow(other) + + def __eq__(self, other): # Factors + if not isinstance(other, Factors): + other = Factors(other) + return self.factors == other.factors + + def __ne__(self, other): # Factors + return not self == other + + +class Term: + """Efficient representation of ``coeff*(numer/denom)``. """ + + __slots__ = ('coeff', 'numer', 'denom') + + def __init__(self, term, numer=None, denom=None): # Term + if numer is None and denom is None: + if not term.is_commutative: + raise NonCommutativeExpression( + 'commutative expression expected') + + coeff, factors = term.as_coeff_mul() + numer, denom = defaultdict(int), defaultdict(int) + + for factor in factors: + base, exp = decompose_power(factor) + + if base.is_Add: + cont, base = base.primitive() + coeff *= cont**exp + + if exp > 0: + numer[base] += exp + else: + denom[base] += -exp + + numer = Factors(numer) + denom = Factors(denom) + else: + coeff = term + + if numer is None: + numer = Factors() + + if denom is None: + denom = Factors() + + self.coeff = coeff + self.numer = numer + self.denom = denom + + def __hash__(self): # Term + return hash((self.coeff, self.numer, self.denom)) + + def __repr__(self): # Term + return "Term(%s, %s, %s)" % (self.coeff, self.numer, self.denom) + + def as_expr(self): # Term + return self.coeff*(self.numer.as_expr()/self.denom.as_expr()) + + def mul(self, other): # Term + coeff = self.coeff*other.coeff + numer = self.numer.mul(other.numer) + denom = self.denom.mul(other.denom) + + numer, denom = numer.normal(denom) + + return Term(coeff, numer, denom) + + def inv(self): # Term + return Term(1/self.coeff, self.denom, self.numer) + + def quo(self, other): # Term + return self.mul(other.inv()) + + def pow(self, other): # Term + if other < 0: + return self.inv().pow(-other) + else: + return Term(self.coeff ** other, + self.numer.pow(other), + self.denom.pow(other)) + + def gcd(self, other): # Term + return Term(self.coeff.gcd(other.coeff), + self.numer.gcd(other.numer), + self.denom.gcd(other.denom)) + + def lcm(self, other): # Term + return Term(self.coeff.lcm(other.coeff), + self.numer.lcm(other.numer), + self.denom.lcm(other.denom)) + + def __mul__(self, other): # Term + if isinstance(other, Term): + return self.mul(other) + else: + return NotImplemented + + def __truediv__(self, other): # Term + if isinstance(other, Term): + return self.quo(other) + else: + return NotImplemented + + def __pow__(self, other): # Term + if isinstance(other, SYMPY_INTS): + return self.pow(other) + else: + return NotImplemented + + def __eq__(self, other): # Term + return (self.coeff == other.coeff and + self.numer == other.numer and + self.denom == other.denom) + + def __ne__(self, other): # Term + return not self == other + + +def _gcd_terms(terms, isprimitive=False, fraction=True): + """Helper function for :func:`gcd_terms`. + + Parameters + ========== + + isprimitive : boolean, optional + If ``isprimitive`` is True then the call to primitive + for an Add will be skipped. This is useful when the + content has already been extracted. + + fraction : boolean, optional + If ``fraction`` is True then the expression will appear over a common + denominator, the lcm of all term denominators. + """ + + if isinstance(terms, Basic) and not isinstance(terms, Tuple): + terms = Add.make_args(terms) + + terms = list(map(Term, [t for t in terms if t])) + + # there is some simplification that may happen if we leave this + # here rather than duplicate it before the mapping of Term onto + # the terms + if len(terms) == 0: + return S.Zero, S.Zero, S.One + + if len(terms) == 1: + cont = terms[0].coeff + numer = terms[0].numer.as_expr() + denom = terms[0].denom.as_expr() + + else: + cont = terms[0] + for term in terms[1:]: + cont = cont.gcd(term) + + for i, term in enumerate(terms): + terms[i] = term.quo(cont) + + if fraction: + denom = terms[0].denom + + for term in terms[1:]: + denom = denom.lcm(term.denom) + + numers = [] + for term in terms: + numer = term.numer.mul(denom.quo(term.denom)) + numers.append(term.coeff*numer.as_expr()) + else: + numers = [t.as_expr() for t in terms] + denom = Term(S.One).numer + + cont = cont.as_expr() + numer = Add(*numers) + denom = denom.as_expr() + + if not isprimitive and numer.is_Add: + _cont, numer = numer.primitive() + cont *= _cont + + return cont, numer, denom + + +def gcd_terms(terms, isprimitive=False, clear=True, fraction=True): + """Compute the GCD of ``terms`` and put them together. + + Parameters + ========== + + terms : Expr + Can be an expression or a non-Basic sequence of expressions + which will be handled as though they are terms from a sum. + + isprimitive : bool, optional + If ``isprimitive`` is True the _gcd_terms will not run the primitive + method on the terms. + + clear : bool, optional + It controls the removal of integers from the denominator of an Add + expression. When True (default), all numerical denominator will be cleared; + when False the denominators will be cleared only if all terms had numerical + denominators other than 1. + + fraction : bool, optional + When True (default), will put the expression over a common + denominator. + + Examples + ======== + + >>> from sympy import gcd_terms + >>> from sympy.abc import x, y + + >>> gcd_terms((x + 1)**2*y + (x + 1)*y**2) + y*(x + 1)*(x + y + 1) + >>> gcd_terms(x/2 + 1) + (x + 2)/2 + >>> gcd_terms(x/2 + 1, clear=False) + x/2 + 1 + >>> gcd_terms(x/2 + y/2, clear=False) + (x + y)/2 + >>> gcd_terms(x/2 + 1/x) + (x**2 + 2)/(2*x) + >>> gcd_terms(x/2 + 1/x, fraction=False) + (x + 2/x)/2 + >>> gcd_terms(x/2 + 1/x, fraction=False, clear=False) + x/2 + 1/x + + >>> gcd_terms(x/2/y + 1/x/y) + (x**2 + 2)/(2*x*y) + >>> gcd_terms(x/2/y + 1/x/y, clear=False) + (x**2/2 + 1)/(x*y) + >>> gcd_terms(x/2/y + 1/x/y, clear=False, fraction=False) + (x/2 + 1/x)/y + + The ``clear`` flag was ignored in this case because the returned + expression was a rational expression, not a simple sum. + + See Also + ======== + + factor_terms, sympy.polys.polytools.terms_gcd + + """ + def mask(terms): + """replace nc portions of each term with a unique Dummy symbols + and return the replacements to restore them""" + args = [(a, []) if a.is_commutative else a.args_cnc() for a in terms] + reps = [] + for i, (c, nc) in enumerate(args): + if nc: + nc = Mul(*nc) + d = Dummy() + reps.append((d, nc)) + c.append(d) + args[i] = Mul(*c) + else: + args[i] = c + return args, dict(reps) + + isadd = isinstance(terms, Add) + addlike = isadd or not isinstance(terms, Basic) and \ + is_sequence(terms, include=set) and \ + not isinstance(terms, Dict) + + if addlike: + if isadd: # i.e. an Add + terms = list(terms.args) + else: + terms = sympify(terms) + terms, reps = mask(terms) + cont, numer, denom = _gcd_terms(terms, isprimitive, fraction) + numer = numer.xreplace(reps) + coeff, factors = cont.as_coeff_Mul() + if not clear: + c, _coeff = coeff.as_coeff_Mul() + if not c.is_Integer and not clear and numer.is_Add: + n, d = c.as_numer_denom() + _numer = numer/d + if any(a.as_coeff_Mul()[0].is_Integer + for a in _numer.args): + numer = _numer + coeff = n*_coeff + return _keep_coeff(coeff, factors*numer/denom, clear=clear) + + if not isinstance(terms, Basic): + return terms + + if terms.is_Atom: + return terms + + if terms.is_Mul: + c, args = terms.as_coeff_mul() + return _keep_coeff(c, Mul(*[gcd_terms(i, isprimitive, clear, fraction) + for i in args]), clear=clear) + + def handle(a): + # don't treat internal args like terms of an Add + if not isinstance(a, Expr): + if isinstance(a, Basic): + if not a.args: + return a + return a.func(*[handle(i) for i in a.args]) + return type(a)([handle(i) for i in a]) + return gcd_terms(a, isprimitive, clear, fraction) + + if isinstance(terms, Dict): + return Dict(*[(k, handle(v)) for k, v in terms.args]) + return terms.func(*[handle(i) for i in terms.args]) + + +def _factor_sum_int(expr, **kwargs): + """Return Sum or Integral object with factors that are not + in the wrt variables removed. In cases where there are additive + terms in the function of the object that are independent, the + object will be separated into two objects. + + Examples + ======== + + >>> from sympy import Sum, factor_terms + >>> from sympy.abc import x, y + >>> factor_terms(Sum(x + y, (x, 1, 3))) + y*Sum(1, (x, 1, 3)) + Sum(x, (x, 1, 3)) + >>> factor_terms(Sum(x*y, (x, 1, 3))) + y*Sum(x, (x, 1, 3)) + + Notes + ===== + + If a function in the summand or integrand is replaced + with a symbol, then this simplification should not be + done or else an incorrect result will be obtained when + the symbol is replaced with an expression that depends + on the variables of summation/integration: + + >>> eq = Sum(y, (x, 1, 3)) + >>> factor_terms(eq).subs(y, x).doit() + 3*x + >>> eq.subs(y, x).doit() + 6 + """ + result = expr.function + if result == 0: + return S.Zero + limits = expr.limits + + # get the wrt variables + wrt = {i.args[0] for i in limits} + + # factor out any common terms that are independent of wrt + f = factor_terms(result, **kwargs) + i, d = f.as_independent(*wrt) + if isinstance(f, Add): + return i * expr.func(1, *limits) + expr.func(d, *limits) + else: + return i * expr.func(d, *limits) + + +def factor_terms(expr: Expr | complex, radical=False, clear=False, fraction=False, sign=True) -> Expr: + """Remove common factors from terms in all arguments without + changing the underlying structure of the expr. No expansion or + simplification (and no processing of non-commutatives) is performed. + + Parameters + ========== + + radical: bool, optional + If radical=True then a radical common to all terms will be factored + out of any Add sub-expressions of the expr. + + clear : bool, optional + If clear=False (default) then coefficients will not be separated + from a single Add if they can be distributed to leave one or more + terms with integer coefficients. + + fraction : bool, optional + If fraction=True (default is False) then a common denominator will be + constructed for the expression. + + sign : bool, optional + If sign=True (default) then even if the only factor in common is a -1, + it will be factored out of the expression. + + Examples + ======== + + >>> from sympy import factor_terms, Symbol + >>> from sympy.abc import x, y + >>> factor_terms(x + x*(2 + 4*y)**3) + x*(8*(2*y + 1)**3 + 1) + >>> A = Symbol('A', commutative=False) + >>> factor_terms(x*A + x*A + x*y*A) + x*(y*A + 2*A) + + When ``clear`` is False, a rational will only be factored out of an + Add expression if all terms of the Add have coefficients that are + fractions: + + >>> factor_terms(x/2 + 1, clear=False) + x/2 + 1 + >>> factor_terms(x/2 + 1, clear=True) + (x + 2)/2 + + If a -1 is all that can be factored out, to *not* factor it out, the + flag ``sign`` must be False: + + >>> factor_terms(-x - y) + -(x + y) + >>> factor_terms(-x - y, sign=False) + -x - y + >>> factor_terms(-2*x - 2*y, sign=False) + -2*(x + y) + + See Also + ======== + + gcd_terms, sympy.polys.polytools.terms_gcd + + """ + def do(expr): + from sympy.concrete.summations import Sum + from sympy.integrals.integrals import Integral + is_iterable = iterable(expr) + + if not isinstance(expr, Basic) or expr.is_Atom: + if is_iterable: + return type(expr)([do(i) for i in expr]) + return expr + + if expr.is_Pow or expr.is_Function or \ + is_iterable or not hasattr(expr, 'args_cnc'): + args = expr.args + newargs = tuple([do(i) for i in args]) + if newargs == args: + return expr + return expr.func(*newargs) + + if isinstance(expr, (Sum, Integral)): + return _factor_sum_int(expr, + radical=radical, clear=clear, + fraction=fraction, sign=sign) + + cont, p = expr.as_content_primitive(radical=radical, clear=clear) + if p.is_Add: + list_args = [do(a) for a in Add.make_args(p)] + # get a common negative (if there) which gcd_terms does not remove + if not any(a.as_coeff_Mul()[0].extract_multiplicatively(-1) is None + for a in list_args): + cont = -cont + list_args = [-a for a in list_args] + # watch out for exp(-(x+2)) which gcd_terms will change to exp(-x-2) + special = {} + for i, a in enumerate(list_args): + b, e = a.as_base_exp() + if e.is_Mul and e != Mul(*e.args): + list_args[i] = Dummy() + special[list_args[i]] = a + # rebuild p not worrying about the order which gcd_terms will fix + p = Add._from_args(list_args) + p = gcd_terms(p, + isprimitive=True, + clear=clear, + fraction=fraction).xreplace(special) + elif p.args: + p = p.func( + *[do(a) for a in p.args]) + rv = _keep_coeff(cont, p, clear=clear, sign=sign) + return rv + expr2 = sympify(expr) + return do(expr2) + + +def _mask_nc(eq, name=None): + """ + Return ``eq`` with non-commutative objects replaced with Dummy + symbols. A dictionary that can be used to restore the original + values is returned: if it is None, the expression is noncommutative + and cannot be made commutative. The third value returned is a list + of any non-commutative symbols that appear in the returned equation. + + Explanation + =========== + + All non-commutative objects other than Symbols are replaced with + a non-commutative Symbol. Identical objects will be identified + by identical symbols. + + If there is only 1 non-commutative object in an expression it will + be replaced with a commutative symbol. Otherwise, the non-commutative + entities are retained and the calling routine should handle + replacements in this case since some care must be taken to keep + track of the ordering of symbols when they occur within Muls. + + Parameters + ========== + + name : str + ``name``, if given, is the name that will be used with numbered Dummy + variables that will replace the non-commutative objects and is mainly + used for doctesting purposes. + + Examples + ======== + + >>> from sympy.physics.secondquant import Commutator, NO, F, Fd + >>> from sympy import symbols + >>> from sympy.core.exprtools import _mask_nc + >>> from sympy.abc import x, y + >>> A, B, C = symbols('A,B,C', commutative=False) + + One nc-symbol: + + >>> _mask_nc(A**2 - x**2, 'd') + (_d0**2 - x**2, {_d0: A}, []) + + Multiple nc-symbols: + + >>> _mask_nc(A**2 - B**2, 'd') + (A**2 - B**2, {}, [A, B]) + + An nc-object with nc-symbols but no others outside of it: + + >>> _mask_nc(1 + x*Commutator(A, B), 'd') + (_d0*x + 1, {_d0: Commutator(A, B)}, []) + >>> _mask_nc(NO(Fd(x)*F(y)), 'd') + (_d0, {_d0: NO(CreateFermion(x)*AnnihilateFermion(y))}, []) + + Multiple nc-objects: + + >>> eq = x*Commutator(A, B) + x*Commutator(A, C)*Commutator(A, B) + >>> _mask_nc(eq, 'd') + (x*_d0 + x*_d1*_d0, {_d0: Commutator(A, B), _d1: Commutator(A, C)}, [_d0, _d1]) + + Multiple nc-objects and nc-symbols: + + >>> eq = A*Commutator(A, B) + B*Commutator(A, C) + >>> _mask_nc(eq, 'd') + (A*_d0 + B*_d1, {_d0: Commutator(A, B), _d1: Commutator(A, C)}, [_d0, _d1, A, B]) + + """ + name = name or 'mask' + # Make Dummy() append sequential numbers to the name + + def numbered_names(): + i = 0 + while True: + yield name + str(i) + i += 1 + + names = numbered_names() + + def Dummy(*args, **kwargs): + from .symbol import Dummy + return Dummy(next(names), *args, **kwargs) + + expr = eq + if expr.is_commutative: + return eq, {}, [] + + # identify nc-objects; symbols and other + rep = [] + nc_obj = set() + nc_syms = set() + pot = preorder_traversal(expr, keys=default_sort_key) + for a in pot: + if any(a == r[0] for r in rep): + pot.skip() + elif not a.is_commutative: + if a.is_symbol: + nc_syms.add(a) + pot.skip() + elif not (a.is_Add or a.is_Mul or a.is_Pow): + nc_obj.add(a) + pot.skip() + + # If there is only one nc symbol or object, it can be factored regularly + # but polys is going to complain, so replace it with a Dummy. + if len(nc_obj) == 1 and not nc_syms: + rep.append((nc_obj.pop(), Dummy())) + elif len(nc_syms) == 1 and not nc_obj: + rep.append((nc_syms.pop(), Dummy())) + + # Any remaining nc-objects will be replaced with an nc-Dummy and + # identified as an nc-Symbol to watch out for + nc_obj = sorted(nc_obj, key=default_sort_key) + for n in nc_obj: + nc = Dummy(commutative=False) + rep.append((n, nc)) + nc_syms.add(nc) + expr = expr.subs(rep) + + nc_syms = list(nc_syms) + nc_syms.sort(key=default_sort_key) + return expr, {v: k for k, v in rep}, nc_syms + + +def factor_nc(expr): + """Return the factored form of ``expr`` while handling non-commutative + expressions. + + Examples + ======== + + >>> from sympy import factor_nc, Symbol + >>> from sympy.abc import x + >>> A = Symbol('A', commutative=False) + >>> B = Symbol('B', commutative=False) + >>> factor_nc((x**2 + 2*A*x + A**2).expand()) + (x + A)**2 + >>> factor_nc(((x + A)*(x + B)).expand()) + (x + A)*(x + B) + """ + expr = sympify(expr) + if not isinstance(expr, Expr) or not expr.args: + return expr + if not expr.is_Add: + return expr.func(*[factor_nc(a) for a in expr.args]) + expr = expr.func(*[expand_power_exp(i) for i in expr.args]) + + from sympy.polys.polytools import gcd, factor + expr, rep, nc_symbols = _mask_nc(expr) + + if rep: + return factor(expr).subs(rep) + else: + args = [a.args_cnc() for a in Add.make_args(expr)] + c = g = l = r = S.One + hit = False + # find any commutative gcd term + for i, a in enumerate(args): + if i == 0: + c = Mul._from_args(a[0]) + elif a[0]: + c = gcd(c, Mul._from_args(a[0])) + else: + c = S.One + if c is not S.One: + hit = True + c, g = c.as_coeff_Mul() + if g is not S.One: + for i, (cc, _) in enumerate(args): + cc = list(Mul.make_args(Mul._from_args(list(cc))/g)) + args[i][0] = cc + for i, (cc, _) in enumerate(args): + if cc: + cc[0] = cc[0]/c + else: + cc = [1/c] + args[i][0] = cc + # find any noncommutative common prefix + for i, a in enumerate(args): + if i == 0: + n = a[1][:] + else: + n = common_prefix(n, a[1]) + if not n: + # is there a power that can be extracted? + if not args[0][1]: + break + b, e = args[0][1][0].as_base_exp() + ok = False + if e.is_Integer: + for t in args: + if not t[1]: + break + bt, et = t[1][0].as_base_exp() + if et.is_Integer and bt == b: + e = min(e, et) + else: + break + else: + ok = hit = True + l = b**e + il = b**-e + for _ in args: + _[1][0] = il*_[1][0] + break + if not ok: + break + else: + hit = True + lenn = len(n) + l = Mul(*n) + for _ in args: + _[1] = _[1][lenn:] + # find any noncommutative common suffix + for i, a in enumerate(args): + if i == 0: + n = a[1][:] + else: + n = common_suffix(n, a[1]) + if not n: + # is there a power that can be extracted? + if not args[0][1]: + break + b, e = args[0][1][-1].as_base_exp() + ok = False + if e.is_Integer: + for t in args: + if not t[1]: + break + bt, et = t[1][-1].as_base_exp() + if et.is_Integer and bt == b: + e = min(e, et) + else: + break + else: + ok = hit = True + r = b**e + il = b**-e + for _ in args: + _[1][-1] = _[1][-1]*il + break + if not ok: + break + else: + hit = True + lenn = len(n) + r = Mul(*n) + for _ in args: + _[1] = _[1][:len(_[1]) - lenn] + if hit: + mid = Add(*[Mul(*cc)*Mul(*nc) for cc, nc in args]) + else: + mid = expr + + from sympy.simplify.powsimp import powsimp + + # sort the symbols so the Dummys would appear in the same + # order as the original symbols, otherwise you may introduce + # a factor of -1, e.g. A**2 - B**2) -- {A:y, B:x} --> y**2 - x**2 + # and the former factors into two terms, (A - B)*(A + B) while the + # latter factors into 3 terms, (-1)*(x - y)*(x + y) + rep1 = [(n, Dummy()) for n in sorted(nc_symbols, key=default_sort_key)] + unrep1 = [(v, k) for k, v in rep1] + unrep1.reverse() + new_mid, r2, _ = _mask_nc(mid.subs(rep1)) + new_mid = powsimp(factor(new_mid)) + + new_mid = new_mid.subs(r2).subs(unrep1) + + if new_mid.is_Pow: + return _keep_coeff(c, g*l*new_mid*r) + + if new_mid.is_Mul: + def _pemexpand(expr): + "Expand with the minimal set of hints necessary to check the result." + return expr.expand(deep=True, mul=True, power_exp=True, + power_base=False, basic=False, multinomial=True, log=False) + # XXX TODO there should be a way to inspect what order the terms + # must be in and just select the plausible ordering without + # checking permutations + cfac = [] + ncfac = [] + for f in new_mid.args: + if f.is_commutative: + cfac.append(f) + else: + b, e = f.as_base_exp() + if e.is_Integer: + ncfac.extend([b]*e) + else: + ncfac.append(f) + pre_mid = g*Mul(*cfac)*l + target = _pemexpand(expr/c) + for s in variations(ncfac, len(ncfac)): + ok = pre_mid*Mul(*s)*r + if _pemexpand(ok) == target: + return _keep_coeff(c, ok) + + # mid was an Add that didn't factor successfully + return _keep_coeff(c, g*l*mid*r) diff --git a/venv/lib/python3.11/site-packages/sympy/core/facts.py b/venv/lib/python3.11/site-packages/sympy/core/facts.py new file mode 100644 index 0000000000000000000000000000000000000000..0b98d9b14bbac661d3c0fd1d1fd87977a792fb74 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/facts.py @@ -0,0 +1,634 @@ +r"""This is rule-based deduction system for SymPy + +The whole thing is split into two parts + + - rules compilation and preparation of tables + - runtime inference + +For rule-based inference engines, the classical work is RETE algorithm [1], +[2] Although we are not implementing it in full (or even significantly) +it's still worth a read to understand the underlying ideas. + +In short, every rule in a system of rules is one of two forms: + + - atom -> ... (alpha rule) + - And(atom1, atom2, ...) -> ... (beta rule) + + +The major complexity is in efficient beta-rules processing and usually for an +expert system a lot of effort goes into code that operates on beta-rules. + + +Here we take minimalistic approach to get something usable first. + + - (preparation) of alpha- and beta- networks, everything except + - (runtime) FactRules.deduce_all_facts + + _____________________________________ + ( Kirr: I've never thought that doing ) + ( logic stuff is that difficult... ) + ------------------------------------- + o ^__^ + o (oo)\_______ + (__)\ )\/\ + ||----w | + || || + + +Some references on the topic +---------------------------- + +[1] https://en.wikipedia.org/wiki/Rete_algorithm +[2] http://reports-archive.adm.cs.cmu.edu/anon/1995/CMU-CS-95-113.pdf + +https://en.wikipedia.org/wiki/Propositional_formula +https://en.wikipedia.org/wiki/Inference_rule +https://en.wikipedia.org/wiki/List_of_rules_of_inference +""" + +from collections import defaultdict +from typing import Iterator + +from .logic import Logic, And, Or, Not + + +def _base_fact(atom): + """Return the literal fact of an atom. + + Effectively, this merely strips the Not around a fact. + """ + if isinstance(atom, Not): + return atom.arg + else: + return atom + + +def _as_pair(atom): + if isinstance(atom, Not): + return (atom.arg, False) + else: + return (atom, True) + +# XXX this prepares forward-chaining rules for alpha-network + + +def transitive_closure(implications): + """ + Computes the transitive closure of a list of implications + + Uses Warshall's algorithm, as described at + http://www.cs.hope.edu/~cusack/Notes/Notes/DiscreteMath/Warshall.pdf. + """ + full_implications = set(implications) + literals = set().union(*map(set, full_implications)) + + for k in literals: + for i in literals: + if (i, k) in full_implications: + for j in literals: + if (k, j) in full_implications: + full_implications.add((i, j)) + + return full_implications + + +def deduce_alpha_implications(implications): + """deduce all implications + + Description by example + ---------------------- + + given set of logic rules: + + a -> b + b -> c + + we deduce all possible rules: + + a -> b, c + b -> c + + + implications: [] of (a,b) + return: {} of a -> set([b, c, ...]) + """ + implications = implications + [(Not(j), Not(i)) for (i, j) in implications] + res = defaultdict(set) + full_implications = transitive_closure(implications) + for a, b in full_implications: + if a == b: + continue # skip a->a cyclic input + + res[a].add(b) + + # Clean up tautologies and check consistency + for a, impl in res.items(): + impl.discard(a) + na = Not(a) + if na in impl: + raise ValueError( + 'implications are inconsistent: %s -> %s %s' % (a, na, impl)) + + return res + + +def apply_beta_to_alpha_route(alpha_implications, beta_rules): + """apply additional beta-rules (And conditions) to already-built + alpha implication tables + + TODO: write about + + - static extension of alpha-chains + - attaching refs to beta-nodes to alpha chains + + + e.g. + + alpha_implications: + + a -> [b, !c, d] + b -> [d] + ... + + + beta_rules: + + &(b,d) -> e + + + then we'll extend a's rule to the following + + a -> [b, !c, d, e] + """ + x_impl = {} + for x in alpha_implications.keys(): + x_impl[x] = (set(alpha_implications[x]), []) + for bcond, bimpl in beta_rules: + for bk in bcond.args: + if bk in x_impl: + continue + x_impl[bk] = (set(), []) + + # static extensions to alpha rules: + # A: x -> a,b B: &(a,b) -> c ==> A: x -> a,b,c + seen_static_extension = True + while seen_static_extension: + seen_static_extension = False + + for bcond, bimpl in beta_rules: + if not isinstance(bcond, And): + raise TypeError("Cond is not And") + bargs = set(bcond.args) + for x, (ximpls, bb) in x_impl.items(): + x_all = ximpls | {x} + # A: ... -> a B: &(...) -> a is non-informative + if bimpl not in x_all and bargs.issubset(x_all): + ximpls.add(bimpl) + + # we introduced new implication - now we have to restore + # completeness of the whole set. + bimpl_impl = x_impl.get(bimpl) + if bimpl_impl is not None: + ximpls |= bimpl_impl[0] + seen_static_extension = True + + # attach beta-nodes which can be possibly triggered by an alpha-chain + for bidx, (bcond, bimpl) in enumerate(beta_rules): + bargs = set(bcond.args) + for x, (ximpls, bb) in x_impl.items(): + x_all = ximpls | {x} + # A: ... -> a B: &(...) -> a (non-informative) + if bimpl in x_all: + continue + # A: x -> a... B: &(!a,...) -> ... (will never trigger) + # A: x -> a... B: &(...) -> !a (will never trigger) + if any(Not(xi) in bargs or Not(xi) == bimpl for xi in x_all): + continue + + if bargs & x_all: + bb.append(bidx) + + return x_impl + + +def rules_2prereq(rules): + """build prerequisites table from rules + + Description by example + ---------------------- + + given set of logic rules: + + a -> b, c + b -> c + + we build prerequisites (from what points something can be deduced): + + b <- a + c <- a, b + + rules: {} of a -> [b, c, ...] + return: {} of c <- [a, b, ...] + + Note however, that this prerequisites may be *not* enough to prove a + fact. An example is 'a -> b' rule, where prereq(a) is b, and prereq(b) + is a. That's because a=T -> b=T, and b=F -> a=F, but a=F -> b=? + """ + prereq = defaultdict(set) + for (a, _), impl in rules.items(): + if isinstance(a, Not): + a = a.args[0] + for (i, _) in impl: + if isinstance(i, Not): + i = i.args[0] + prereq[i].add(a) + return prereq + +################ +# RULES PROVER # +################ + + +class TautologyDetected(Exception): + """(internal) Prover uses it for reporting detected tautology""" + pass + + +class Prover: + """ai - prover of logic rules + + given a set of initial rules, Prover tries to prove all possible rules + which follow from given premises. + + As a result proved_rules are always either in one of two forms: alpha or + beta: + + Alpha rules + ----------- + + This are rules of the form:: + + a -> b & c & d & ... + + + Beta rules + ---------- + + This are rules of the form:: + + &(a,b,...) -> c & d & ... + + + i.e. beta rules are join conditions that say that something follows when + *several* facts are true at the same time. + """ + + def __init__(self): + self.proved_rules = [] + self._rules_seen = set() + + def split_alpha_beta(self): + """split proved rules into alpha and beta chains""" + rules_alpha = [] # a -> b + rules_beta = [] # &(...) -> b + for a, b in self.proved_rules: + if isinstance(a, And): + rules_beta.append((a, b)) + else: + rules_alpha.append((a, b)) + return rules_alpha, rules_beta + + @property + def rules_alpha(self): + return self.split_alpha_beta()[0] + + @property + def rules_beta(self): + return self.split_alpha_beta()[1] + + def process_rule(self, a, b): + """process a -> b rule""" # TODO write more? + if (not a) or isinstance(b, bool): + return + if isinstance(a, bool): + return + if (a, b) in self._rules_seen: + return + else: + self._rules_seen.add((a, b)) + + # this is the core of processing + try: + self._process_rule(a, b) + except TautologyDetected: + pass + + def _process_rule(self, a, b): + # right part first + + # a -> b & c --> a -> b ; a -> c + # (?) FIXME this is only correct when b & c != null ! + + if isinstance(b, And): + sorted_bargs = sorted(b.args, key=str) + for barg in sorted_bargs: + self.process_rule(a, barg) + + # a -> b | c --> !b & !c -> !a + # --> a & !b -> c + # --> a & !c -> b + elif isinstance(b, Or): + sorted_bargs = sorted(b.args, key=str) + # detect tautology first + if not isinstance(a, Logic): # Atom + # tautology: a -> a|c|... + if a in sorted_bargs: + raise TautologyDetected(a, b, 'a -> a|c|...') + self.process_rule(And(*[Not(barg) for barg in b.args]), Not(a)) + + for bidx in range(len(sorted_bargs)): + barg = sorted_bargs[bidx] + brest = sorted_bargs[:bidx] + sorted_bargs[bidx + 1:] + self.process_rule(And(a, Not(barg)), Or(*brest)) + + # left part + + # a & b -> c --> IRREDUCIBLE CASE -- WE STORE IT AS IS + # (this will be the basis of beta-network) + elif isinstance(a, And): + sorted_aargs = sorted(a.args, key=str) + if b in sorted_aargs: + raise TautologyDetected(a, b, 'a & b -> a') + self.proved_rules.append((a, b)) + # XXX NOTE at present we ignore !c -> !a | !b + + elif isinstance(a, Or): + sorted_aargs = sorted(a.args, key=str) + if b in sorted_aargs: + raise TautologyDetected(a, b, 'a | b -> a') + for aarg in sorted_aargs: + self.process_rule(aarg, b) + + else: + # both `a` and `b` are atoms + self.proved_rules.append((a, b)) # a -> b + self.proved_rules.append((Not(b), Not(a))) # !b -> !a + +######################################## + + +class FactRules: + """Rules that describe how to deduce facts in logic space + + When defined, these rules allow implications to quickly be determined + for a set of facts. For this precomputed deduction tables are used. + see `deduce_all_facts` (forward-chaining) + + Also it is possible to gather prerequisites for a fact, which is tried + to be proven. (backward-chaining) + + + Definition Syntax + ----------------- + + a -> b -- a=T -> b=T (and automatically b=F -> a=F) + a -> !b -- a=T -> b=F + a == b -- a -> b & b -> a + a -> b & c -- a=T -> b=T & c=T + # TODO b | c + + + Internals + --------- + + .full_implications[k, v]: all the implications of fact k=v + .beta_triggers[k, v]: beta rules that might be triggered when k=v + .prereq -- {} k <- [] of k's prerequisites + + .defined_facts -- set of defined fact names + """ + + def __init__(self, rules): + """Compile rules into internal lookup tables""" + + if isinstance(rules, str): + rules = rules.splitlines() + + # --- parse and process rules --- + P = Prover() + + for rule in rules: + # XXX `a` is hardcoded to be always atom + a, op, b = rule.split(None, 2) + + a = Logic.fromstring(a) + b = Logic.fromstring(b) + + if op == '->': + P.process_rule(a, b) + elif op == '==': + P.process_rule(a, b) + P.process_rule(b, a) + else: + raise ValueError('unknown op %r' % op) + + # --- build deduction networks --- + self.beta_rules = [] + for bcond, bimpl in P.rules_beta: + self.beta_rules.append( + ({_as_pair(a) for a in bcond.args}, _as_pair(bimpl))) + + # deduce alpha implications + impl_a = deduce_alpha_implications(P.rules_alpha) + + # now: + # - apply beta rules to alpha chains (static extension), and + # - further associate beta rules to alpha chain (for inference + # at runtime) + impl_ab = apply_beta_to_alpha_route(impl_a, P.rules_beta) + + # extract defined fact names + self.defined_facts = {_base_fact(k) for k in impl_ab.keys()} + + # build rels (forward chains) + full_implications = defaultdict(set) + beta_triggers = defaultdict(set) + for k, (impl, betaidxs) in impl_ab.items(): + full_implications[_as_pair(k)] = {_as_pair(i) for i in impl} + beta_triggers[_as_pair(k)] = betaidxs + + self.full_implications = full_implications + self.beta_triggers = beta_triggers + + # build prereq (backward chains) + prereq = defaultdict(set) + rel_prereq = rules_2prereq(full_implications) + for k, pitems in rel_prereq.items(): + prereq[k] |= pitems + self.prereq = prereq + + def _to_python(self) -> str: + """ Generate a string with plain python representation of the instance """ + return '\n'.join(self.print_rules()) + + @classmethod + def _from_python(cls, data : dict): + """ Generate an instance from the plain python representation """ + self = cls('') + for key in ['full_implications', 'beta_triggers', 'prereq']: + d=defaultdict(set) + d.update(data[key]) + setattr(self, key, d) + self.beta_rules = data['beta_rules'] + self.defined_facts = set(data['defined_facts']) + + return self + + def _defined_facts_lines(self): + yield 'defined_facts = [' + for fact in sorted(self.defined_facts): + yield f' {fact!r},' + yield '] # defined_facts' + + def _full_implications_lines(self): + yield 'full_implications = dict( [' + for fact in sorted(self.defined_facts): + for value in (True, False): + yield f' # Implications of {fact} = {value}:' + yield f' (({fact!r}, {value!r}), set( (' + implications = self.full_implications[(fact, value)] + for implied in sorted(implications): + yield f' {implied!r},' + yield ' ) ),' + yield ' ),' + yield ' ] ) # full_implications' + + def _prereq_lines(self): + yield 'prereq = {' + yield '' + for fact in sorted(self.prereq): + yield f' # facts that could determine the value of {fact}' + yield f' {fact!r}: {{' + for pfact in sorted(self.prereq[fact]): + yield f' {pfact!r},' + yield ' },' + yield '' + yield '} # prereq' + + def _beta_rules_lines(self): + reverse_implications = defaultdict(list) + for n, (pre, implied) in enumerate(self.beta_rules): + reverse_implications[implied].append((pre, n)) + + yield '# Note: the order of the beta rules is used in the beta_triggers' + yield 'beta_rules = [' + yield '' + m = 0 + indices = {} + for implied in sorted(reverse_implications): + fact, value = implied + yield f' # Rules implying {fact} = {value}' + for pre, n in reverse_implications[implied]: + indices[n] = m + m += 1 + setstr = ", ".join(map(str, sorted(pre))) + yield f' ({{{setstr}}},' + yield f' {implied!r}),' + yield '' + yield '] # beta_rules' + + yield 'beta_triggers = {' + for query in sorted(self.beta_triggers): + fact, value = query + triggers = [indices[n] for n in self.beta_triggers[query]] + yield f' {query!r}: {triggers!r},' + yield '} # beta_triggers' + + def print_rules(self) -> Iterator[str]: + """ Returns a generator with lines to represent the facts and rules """ + yield from self._defined_facts_lines() + yield '' + yield '' + yield from self._full_implications_lines() + yield '' + yield '' + yield from self._prereq_lines() + yield '' + yield '' + yield from self._beta_rules_lines() + yield '' + yield '' + yield "generated_assumptions = {'defined_facts': defined_facts, 'full_implications': full_implications," + yield " 'prereq': prereq, 'beta_rules': beta_rules, 'beta_triggers': beta_triggers}" + + +class InconsistentAssumptions(ValueError): + def __str__(self): + kb, fact, value = self.args + return "%s, %s=%s" % (kb, fact, value) + + +class FactKB(dict): + """ + A simple propositional knowledge base relying on compiled inference rules. + """ + def __str__(self): + return '{\n%s}' % ',\n'.join( + ["\t%s: %s" % i for i in sorted(self.items())]) + + def __init__(self, rules): + self.rules = rules + + def _tell(self, k, v): + """Add fact k=v to the knowledge base. + + Returns True if the KB has actually been updated, False otherwise. + """ + if k in self and self[k] is not None: + if self[k] == v: + return False + else: + raise InconsistentAssumptions(self, k, v) + else: + self[k] = v + return True + + # ********************************************* + # * This is the workhorse, so keep it *fast*. * + # ********************************************* + def deduce_all_facts(self, facts): + """ + Update the KB with all the implications of a list of facts. + + Facts can be specified as a dictionary or as a list of (key, value) + pairs. + """ + # keep frequently used attributes locally, so we'll avoid extra + # attribute access overhead + full_implications = self.rules.full_implications + beta_triggers = self.rules.beta_triggers + beta_rules = self.rules.beta_rules + + if isinstance(facts, dict): + facts = facts.items() + + while facts: + beta_maytrigger = set() + + # --- alpha chains --- + for k, v in facts: + if not self._tell(k, v) or v is None: + continue + + # lookup routing tables + for key, value in full_implications[k, v]: + self._tell(key, value) + + beta_maytrigger.update(beta_triggers[k, v]) + + # --- beta chains --- + facts = [] + for bidx in beta_maytrigger: + bcond, bimpl = beta_rules[bidx] + if all(self.get(k) is v for k, v in bcond): + facts.append(bimpl) diff --git a/venv/lib/python3.11/site-packages/sympy/core/function.py b/venv/lib/python3.11/site-packages/sympy/core/function.py new file mode 100644 index 0000000000000000000000000000000000000000..ac850845e0bb2aaf9b535635567d6e2629527ad7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/function.py @@ -0,0 +1,3423 @@ +""" +There are three types of functions implemented in SymPy: + + 1) defined functions (in the sense that they can be evaluated) like + exp or sin; they have a name and a body: + f = exp + 2) undefined function which have a name but no body. Undefined + functions can be defined using a Function class as follows: + f = Function('f') + (the result will be a Function instance) + 3) anonymous function (or lambda function) which have a body (defined + with dummy variables) but have no name: + f = Lambda(x, exp(x)*x) + f = Lambda((x, y), exp(x)*y) + The fourth type of functions are composites, like (sin + cos)(x); these work in + SymPy core, but are not yet part of SymPy. + + Examples + ======== + + >>> import sympy + >>> f = sympy.Function("f") + >>> from sympy.abc import x + >>> f(x) + f(x) + >>> print(sympy.srepr(f(x).func)) + Function('f') + >>> f(x).args + (x,) + +""" + +from __future__ import annotations + +from typing import Any +from collections.abc import Iterable +import copyreg + +from .add import Add +from .basic import Basic, _atomic +from .cache import cacheit +from .containers import Tuple, Dict +from .decorators import _sympifyit +from .evalf import pure_complex +from .expr import Expr, AtomicExpr +from .logic import fuzzy_and, fuzzy_or, fuzzy_not, FuzzyBool +from .mul import Mul +from .numbers import Rational, Float, Integer +from .operations import LatticeOp +from .parameters import global_parameters +from .rules import Transform +from .singleton import S +from .sympify import sympify, _sympify + +from .sorting import default_sort_key, ordered +from sympy.utilities.exceptions import (sympy_deprecation_warning, + SymPyDeprecationWarning, ignore_warnings) +from sympy.utilities.iterables import (has_dups, sift, iterable, + is_sequence, uniq, topological_sort) +from sympy.utilities.lambdify import MPMATH_TRANSLATIONS +from sympy.utilities.misc import as_int, filldedent, func_name + +import mpmath +from mpmath.libmp.libmpf import prec_to_dps + +import inspect +from collections import Counter + +def _coeff_isneg(a): + """Return True if the leading Number is negative. + + Examples + ======== + + >>> from sympy.core.function import _coeff_isneg + >>> from sympy import S, Symbol, oo, pi + >>> _coeff_isneg(-3*pi) + True + >>> _coeff_isneg(S(3)) + False + >>> _coeff_isneg(-oo) + True + >>> _coeff_isneg(Symbol('n', negative=True)) # coeff is 1 + False + + For matrix expressions: + + >>> from sympy import MatrixSymbol, sqrt + >>> A = MatrixSymbol("A", 3, 3) + >>> _coeff_isneg(-sqrt(2)*A) + True + >>> _coeff_isneg(sqrt(2)*A) + False + """ + + if a.is_MatMul: + a = a.args[0] + if a.is_Mul: + a = a.args[0] + return a.is_Number and a.is_extended_negative + + +class PoleError(Exception): + pass + + +class ArgumentIndexError(ValueError): + def __str__(self): + return ("Invalid operation with argument number %s for Function %s" % + (self.args[1], self.args[0])) + + +class BadSignatureError(TypeError): + '''Raised when a Lambda is created with an invalid signature''' + pass + + +class BadArgumentsError(TypeError): + '''Raised when a Lambda is called with an incorrect number of arguments''' + pass + + +# Python 3 version that does not raise a Deprecation warning +def arity(cls): + """Return the arity of the function if it is known, else None. + + Explanation + =========== + + When default values are specified for some arguments, they are + optional and the arity is reported as a tuple of possible values. + + Examples + ======== + + >>> from sympy import arity, log + >>> arity(lambda x: x) + 1 + >>> arity(log) + (1, 2) + >>> arity(lambda *x: sum(x)) is None + True + """ + eval_ = getattr(cls, 'eval', cls) + + parameters = inspect.signature(eval_).parameters.items() + if [p for _, p in parameters if p.kind == p.VAR_POSITIONAL]: + return + p_or_k = [p for _, p in parameters if p.kind == p.POSITIONAL_OR_KEYWORD] + # how many have no default and how many have a default value + no, yes = map(len, sift(p_or_k, + lambda p:p.default == p.empty, binary=True)) + return no if not yes else tuple(range(no, no + yes + 1)) + +class FunctionClass(type): + """ + Base class for function classes. FunctionClass is a subclass of type. + + Use Function('' [ , signature ]) to create + undefined function classes. + """ + _new = type.__new__ + + def __init__(cls, *args, **kwargs): + # honor kwarg value or class-defined value before using + # the number of arguments in the eval function (if present) + nargs = kwargs.pop('nargs', cls.__dict__.get('nargs', arity(cls))) + if nargs is None and 'nargs' not in cls.__dict__: + for supcls in cls.__mro__: + if hasattr(supcls, '_nargs'): + nargs = supcls._nargs + break + else: + continue + + # Canonicalize nargs here; change to set in nargs. + if is_sequence(nargs): + if not nargs: + raise ValueError(filldedent(''' + Incorrectly specified nargs as %s: + if there are no arguments, it should be + `nargs = 0`; + if there are any number of arguments, + it should be + `nargs = None`''' % str(nargs))) + nargs = tuple(ordered(set(nargs))) + elif nargs is not None: + nargs = (as_int(nargs),) + cls._nargs = nargs + + # When __init__ is called from UndefinedFunction it is called with + # just one arg but when it is called from subclassing Function it is + # called with the usual (name, bases, namespace) type() signature. + if len(args) == 3: + namespace = args[2] + if 'eval' in namespace and not isinstance(namespace['eval'], classmethod): + raise TypeError("eval on Function subclasses should be a class method (defined with @classmethod)") + + @property + def __signature__(self): + """ + Allow Python 3's inspect.signature to give a useful signature for + Function subclasses. + """ + # Python 3 only, but backports (like the one in IPython) still might + # call this. + try: + from inspect import signature + except ImportError: + return None + + # TODO: Look at nargs + return signature(self.eval) + + @property + def free_symbols(self): + return set() + + @property + def xreplace(self): + # Function needs args so we define a property that returns + # a function that takes args...and then use that function + # to return the right value + return lambda rule, **_: rule.get(self, self) + + @property + def nargs(self): + """Return a set of the allowed number of arguments for the function. + + Examples + ======== + + >>> from sympy import Function + >>> f = Function('f') + + If the function can take any number of arguments, the set of whole + numbers is returned: + + >>> Function('f').nargs + Naturals0 + + If the function was initialized to accept one or more arguments, a + corresponding set will be returned: + + >>> Function('f', nargs=1).nargs + {1} + >>> Function('f', nargs=(2, 1)).nargs + {1, 2} + + The undefined function, after application, also has the nargs + attribute; the actual number of arguments is always available by + checking the ``args`` attribute: + + >>> f = Function('f') + >>> f(1).nargs + Naturals0 + >>> len(f(1).args) + 1 + """ + from sympy.sets.sets import FiniteSet + # XXX it would be nice to handle this in __init__ but there are import + # problems with trying to import FiniteSet there + return FiniteSet(*self._nargs) if self._nargs else S.Naturals0 + + def _valid_nargs(self, n : int) -> bool: + """ Return True if the specified integer is a valid number of arguments + + The number of arguments n is guaranteed to be an integer and positive + + """ + if self._nargs: + return n in self._nargs + + nargs = self.nargs + return nargs is S.Naturals0 or n in nargs + + def __repr__(cls): + return cls.__name__ + + +class Application(Basic, metaclass=FunctionClass): + """ + Base class for applied functions. + + Explanation + =========== + + Instances of Application represent the result of applying an application of + any type to any object. + """ + + is_Function = True + + @cacheit + def __new__(cls, *args, **options): + from sympy.sets.fancysets import Naturals0 + from sympy.sets.sets import FiniteSet + + args = list(map(sympify, args)) + evaluate = options.pop('evaluate', global_parameters.evaluate) + # WildFunction (and anything else like it) may have nargs defined + # and we throw that value away here + options.pop('nargs', None) + + if options: + raise ValueError("Unknown options: %s" % options) + + if evaluate: + evaluated = cls.eval(*args) + if evaluated is not None: + return evaluated + + obj = super().__new__(cls, *args, **options) + + # make nargs uniform here + sentinel = object() + objnargs = getattr(obj, "nargs", sentinel) + if objnargs is not sentinel: + # things passing through here: + # - functions subclassed from Function (e.g. myfunc(1).nargs) + # - functions like cos(1).nargs + # - AppliedUndef with given nargs like Function('f', nargs=1)(1).nargs + # Canonicalize nargs here + if is_sequence(objnargs): + nargs = tuple(ordered(set(objnargs))) + elif objnargs is not None: + nargs = (as_int(objnargs),) + else: + nargs = None + else: + # things passing through here: + # - WildFunction('f').nargs + # - AppliedUndef with no nargs like Function('f')(1).nargs + nargs = obj._nargs # note the underscore here + # convert to FiniteSet + obj.nargs = FiniteSet(*nargs) if nargs else Naturals0() + return obj + + @classmethod + def eval(cls, *args): + """ + Returns a canonical form of cls applied to arguments args. + + Explanation + =========== + + The ``eval()`` method is called when the class ``cls`` is about to be + instantiated and it should return either some simplified instance + (possible of some other class), or if the class ``cls`` should be + unmodified, return None. + + Examples of ``eval()`` for the function "sign" + + .. code-block:: python + + @classmethod + def eval(cls, arg): + if arg is S.NaN: + return S.NaN + if arg.is_zero: return S.Zero + if arg.is_positive: return S.One + if arg.is_negative: return S.NegativeOne + if isinstance(arg, Mul): + coeff, terms = arg.as_coeff_Mul(rational=True) + if coeff is not S.One: + return cls(coeff) * cls(terms) + + """ + return + + @property + def func(self): + return self.__class__ + + def _eval_subs(self, old, new): + if (old.is_Function and new.is_Function and + callable(old) and callable(new) and + old == self.func and len(self.args) in new.nargs): + return new(*[i._subs(old, new) for i in self.args]) + + +class Function(Application, Expr): + r""" + Base class for applied mathematical functions. + + It also serves as a constructor for undefined function classes. + + See the :ref:`custom-functions` guide for details on how to subclass + ``Function`` and what methods can be defined. + + Examples + ======== + + **Undefined Functions** + + To create an undefined function, pass a string of the function name to + ``Function``. + + >>> from sympy import Function, Symbol + >>> x = Symbol('x') + >>> f = Function('f') + >>> g = Function('g')(x) + >>> f + f + >>> f(x) + f(x) + >>> g + g(x) + >>> f(x).diff(x) + Derivative(f(x), x) + >>> g.diff(x) + Derivative(g(x), x) + + Assumptions can be passed to ``Function`` the same as with a + :class:`~.Symbol`. Alternatively, you can use a ``Symbol`` with + assumptions for the function name and the function will inherit the name + and assumptions associated with the ``Symbol``: + + >>> f_real = Function('f', real=True) + >>> f_real(x).is_real + True + >>> f_real_inherit = Function(Symbol('f', real=True)) + >>> f_real_inherit(x).is_real + True + + Note that assumptions on a function are unrelated to the assumptions on + the variables it is called on. If you want to add a relationship, subclass + ``Function`` and define custom assumptions handler methods. See the + :ref:`custom-functions-assumptions` section of the :ref:`custom-functions` + guide for more details. + + **Custom Function Subclasses** + + The :ref:`custom-functions` guide has several + :ref:`custom-functions-complete-examples` of how to subclass ``Function`` + to create a custom function. + + """ + + @property + def _diff_wrt(self): + return False + + @cacheit + def __new__(cls, *args, **options) -> type[AppliedUndef]: # type: ignore + # Handle calls like Function('f') + if cls is Function: + return UndefinedFunction(*args, **options) # type: ignore + else: + return cls._new_(*args, **options) # type: ignore + + @classmethod + def _new_(cls, *args, **options) -> Expr: + n = len(args) + + if not cls._valid_nargs(n): + # XXX: exception message must be in exactly this format to + # make it work with NumPy's functions like vectorize(). See, + # for example, https://github.com/numpy/numpy/issues/1697. + # The ideal solution would be just to attach metadata to + # the exception and change NumPy to take advantage of this. + temp = ('%(name)s takes %(qual)s %(args)s ' + 'argument%(plural)s (%(given)s given)') + raise TypeError(temp % { + 'name': cls, + 'qual': 'exactly' if len(cls.nargs) == 1 else 'at least', + 'args': min(cls.nargs), + 'plural': 's'*(min(cls.nargs) != 1), + 'given': n}) + + evaluate = options.get('evaluate', global_parameters.evaluate) + result = super().__new__(cls, *args, **options) + if evaluate and isinstance(result, cls) and result.args: + _should_evalf = [cls._should_evalf(a) for a in result.args] + pr2 = min(_should_evalf) + if pr2 > 0: + pr = max(_should_evalf) + result = result.evalf(prec_to_dps(pr)) + + return _sympify(result) + + @classmethod + def _should_evalf(cls, arg): + """ + Decide if the function should automatically evalf(). + + Explanation + =========== + + By default (in this implementation), this happens if (and only if) the + ARG is a floating point number (including complex numbers). + This function is used by __new__. + + Returns the precision to evalf to, or -1 if it should not evalf. + """ + if arg.is_Float: + return arg._prec + if not arg.is_Add: + return -1 + m = pure_complex(arg) + if m is None: + return -1 + # the elements of m are of type Number, so have a _prec + return max(m[0]._prec, m[1]._prec) + + @classmethod + def class_key(cls): + from sympy.sets.fancysets import Naturals0 + funcs = { + 'exp': 10, + 'log': 11, + 'sin': 20, + 'cos': 21, + 'tan': 22, + 'cot': 23, + 'sinh': 30, + 'cosh': 31, + 'tanh': 32, + 'coth': 33, + 'conjugate': 40, + 're': 41, + 'im': 42, + 'arg': 43, + } + name = cls.__name__ + + try: + i = funcs[name] + except KeyError: + i = 0 if isinstance(cls.nargs, Naturals0) else 10000 + + return 4, i, name + + def _eval_evalf(self, prec): + + def _get_mpmath_func(fname): + """Lookup mpmath function based on name""" + if isinstance(self, AppliedUndef): + # Shouldn't lookup in mpmath but might have ._imp_ + return None + + if not hasattr(mpmath, fname): + fname = MPMATH_TRANSLATIONS.get(fname, None) + if fname is None: + return None + return getattr(mpmath, fname) + + _eval_mpmath = getattr(self, '_eval_mpmath', None) + if _eval_mpmath is None: + func = _get_mpmath_func(self.func.__name__) + args = self.args + else: + func, args = _eval_mpmath() + + # Fall-back evaluation + if func is None: + imp = getattr(self, '_imp_', None) + if imp is None: + return None + try: + return Float(imp(*[i.evalf(prec) for i in self.args]), prec) + except (TypeError, ValueError): + return None + + # Convert all args to mpf or mpc + # Convert the arguments to *higher* precision than requested for the + # final result. + # XXX + 5 is a guess, it is similar to what is used in evalf.py. Should + # we be more intelligent about it? + try: + args = [arg._to_mpmath(prec + 5) for arg in args] + def bad(m): + from mpmath import mpf, mpc + # the precision of an mpf value is the last element + # if that is 1 (and m[1] is not 1 which would indicate a + # power of 2), then the eval failed; so check that none of + # the arguments failed to compute to a finite precision. + # Note: An mpc value has two parts, the re and imag tuple; + # check each of those parts, too. Anything else is allowed to + # pass + if isinstance(m, mpf): + m = m._mpf_ + return m[1] !=1 and m[-1] == 1 + elif isinstance(m, mpc): + m, n = m._mpc_ + return m[1] !=1 and m[-1] == 1 and \ + n[1] !=1 and n[-1] == 1 + else: + return False + if any(bad(a) for a in args): + raise ValueError # one or more args failed to compute with significance + except ValueError: + return + + with mpmath.workprec(prec): + v = func(*args) + + return Expr._from_mpmath(v, prec) + + def _eval_derivative(self, s): + # f(x).diff(s) -> x.diff(s) * f.fdiff(1)(s) + i = 0 + l = [] + for a in self.args: + i += 1 + da = a.diff(s) + if da.is_zero: + continue + try: + df = self.fdiff(i) + except ArgumentIndexError: + df = Function.fdiff(self, i) + l.append(df * da) + return Add(*l) + + def _eval_is_commutative(self): + return fuzzy_and(a.is_commutative for a in self.args) + + def _eval_is_meromorphic(self, x, a): + if not self.args: + return True + if any(arg.has(x) for arg in self.args[1:]): + return False + + arg = self.args[0] + if not arg._eval_is_meromorphic(x, a): + return None + + return fuzzy_not(type(self).is_singular(arg.subs(x, a))) + + _singularities: FuzzyBool | tuple[Expr, ...] = None + + @classmethod + def is_singular(cls, a): + """ + Tests whether the argument is an essential singularity + or a branch point, or the functions is non-holomorphic. + """ + ss = cls._singularities + if ss in (True, None, False): + return ss + + return fuzzy_or(a.is_infinite if s is S.ComplexInfinity + else (a - s).is_zero for s in ss) + + def _eval_aseries(self, n, args0, x, logx): + """ + Compute an asymptotic expansion around args0, in terms of self.args. + This function is only used internally by _eval_nseries and should not + be called directly; derived classes can overwrite this to implement + asymptotic expansions. + """ + raise PoleError(filldedent(''' + Asymptotic expansion of %s around %s is + not implemented.''' % (type(self), args0))) + + def _eval_nseries(self, x, n, logx, cdir=0): + """ + This function does compute series for multivariate functions, + but the expansion is always in terms of *one* variable. + + Examples + ======== + + >>> from sympy import atan2 + >>> from sympy.abc import x, y + >>> atan2(x, y).series(x, n=2) + atan2(0, y) + x/y + O(x**2) + >>> atan2(x, y).series(y, n=2) + -y/x + atan2(x, 0) + O(y**2) + + This function also computes asymptotic expansions, if necessary + and possible: + + >>> from sympy import loggamma + >>> loggamma(1/x)._eval_nseries(x,0,None) + -1/x - log(x)/x + log(x)/2 + O(1) + + """ + from .symbol import uniquely_named_symbol + from sympy.series.order import Order + from sympy.sets.sets import FiniteSet + args = self.args + args0 = [t.limit(x, 0) for t in args] + if any(t.is_finite is False for t in args0): + from .numbers import oo, zoo, nan + a = [t.as_leading_term(x, logx=logx) for t in args] + a0 = [t.limit(x, 0) for t in a] + if any(t.has(oo, -oo, zoo, nan) for t in a0): + return self._eval_aseries(n, args0, x, logx) + # Careful: the argument goes to oo, but only logarithmically so. We + # are supposed to do a power series expansion "around the + # logarithmic term". e.g. + # f(1+x+log(x)) + # -> f(1+logx) + x*f'(1+logx) + O(x**2) + # where 'logx' is given in the argument + a = [t._eval_nseries(x, n, logx) for t in args] + z = [r - r0 for (r, r0) in zip(a, a0)] + p = [Dummy() for _ in z] + q = [] + v = None + for ai, zi, pi in zip(a0, z, p): + if zi.has(x): + if v is not None: + raise NotImplementedError + q.append(ai + pi) + v = pi + else: + q.append(ai) + e1 = self.func(*q) + if v is None: + return e1 + s = e1._eval_nseries(v, n, logx) + o = s.getO() + s = s.removeO() + s = s.subs(v, zi).expand() + Order(o.expr.subs(v, zi), x) + return s + if (self.func.nargs is S.Naturals0 + or (self.func.nargs == FiniteSet(1) and args0[0]) + or any(c > 1 for c in self.func.nargs)): + e = self + e1 = e.expand() + if e == e1: + #for example when e = sin(x+1) or e = sin(cos(x)) + #let's try the general algorithm + if len(e.args) == 1: + # issue 14411 + e = e.func(e.args[0].cancel()) + term = e.subs(x, S.Zero) + if term.is_finite is False or term is S.NaN: + raise PoleError("Cannot expand %s around 0" % (self)) + series = term + fact = S.One + + _x = uniquely_named_symbol('xi', self) + e = e.subs(x, _x) + for i in range(1, n): + fact *= Rational(i) + e = e.diff(_x) + subs = e.subs(_x, S.Zero) + if subs is S.NaN: + # try to evaluate a limit if we have to + subs = e.limit(_x, S.Zero) + if subs.is_finite is False: + raise PoleError("Cannot expand %s around 0" % (self)) + term = subs*(x**i)/fact + term = term.expand() + series += term + return series + Order(x**n, x) + return e1.nseries(x, n=n, logx=logx) + arg = self.args[0] + l = [] + g = None + # try to predict a number of terms needed + nterms = n + 2 + cf = Order(arg.as_leading_term(x), x).getn() + if cf != 0: + nterms = (n/cf).ceiling() + for i in range(nterms): + g = self.taylor_term(i, arg, g) + g = g.nseries(x, n=n, logx=logx) + l.append(g) + return Add(*l) + Order(x**n, x) + + def fdiff(self, argindex=1): + """ + Returns the first derivative of the function. + """ + if not (1 <= argindex <= len(self.args)): + raise ArgumentIndexError(self, argindex) + ix = argindex - 1 + A = self.args[ix] + if A._diff_wrt: + if len(self.args) == 1 or not A.is_Symbol: + return _derivative_dispatch(self, A) + for i, v in enumerate(self.args): + if i != ix and A in v.free_symbols: + # it can't be in any other argument's free symbols + # issue 8510 + break + else: + return _derivative_dispatch(self, A) + + # See issue 4624 and issue 4719, 5600 and 8510 + D = Dummy('xi_%i' % argindex, dummy_index=hash(A)) + args = self.args[:ix] + (D,) + self.args[ix + 1:] + return Subs(Derivative(self.func(*args), D), D, A) + + def _eval_as_leading_term(self, x, logx, cdir): + """Stub that should be overridden by new Functions to return + the first non-zero term in a series if ever an x-dependent + argument whose leading term vanishes as x -> 0 might be encountered. + See, for example, cos._eval_as_leading_term. + """ + from sympy.series.order import Order + args = [a.as_leading_term(x, logx=logx) for a in self.args] + o = Order(1, x) + if any(x in a.free_symbols and o.contains(a) for a in args): + # Whereas x and any finite number are contained in O(1, x), + # expressions like 1/x are not. If any arg simplified to a + # vanishing expression as x -> 0 (like x or x**2, but not + # 3, 1/x, etc...) then the _eval_as_leading_term is needed + # to supply the first non-zero term of the series, + # + # e.g. expression leading term + # ---------- ------------ + # cos(1/x) cos(1/x) + # cos(cos(x)) cos(1) + # cos(x) 1 <- _eval_as_leading_term needed + # sin(x) x <- _eval_as_leading_term needed + # + raise NotImplementedError( + '%s has no _eval_as_leading_term routine' % self.func) + else: + return self + + +class DefinedFunction(Function): + """Base class for defined functions like ``sin``, ``cos``, ...""" + + @cacheit + def __new__(cls, *args, **options) -> Expr: # type: ignore + return cls._new_(*args, **options) + + +class AppliedUndef(Function): + """ + Base class for expressions resulting from the application of an undefined + function. + """ + + is_number = False + + name: str + + def __new__(cls, *args, **options) -> Expr: # type: ignore + args = tuple(map(sympify, args)) + u = [a.name for a in args if isinstance(a, UndefinedFunction)] + if u: + raise TypeError('Invalid argument: expecting an expression, not UndefinedFunction%s: %s' % ( + 's'*(len(u) > 1), ', '.join(u))) + obj: Expr = super().__new__(cls, *args, **options) # type: ignore + return obj + + def _eval_as_leading_term(self, x, logx, cdir): + return self + + @property + def _diff_wrt(self): + """ + Allow derivatives wrt to undefined functions. + + Examples + ======== + + >>> from sympy import Function, Symbol + >>> f = Function('f') + >>> x = Symbol('x') + >>> f(x)._diff_wrt + True + >>> f(x).diff(x) + Derivative(f(x), x) + """ + return True + + +class UndefSageHelper: + """ + Helper to facilitate Sage conversion. + """ + def __get__(self, ins, typ): + import sage.all as sage + if ins is None: + return lambda: sage.function(typ.__name__) + else: + args = [arg._sage_() for arg in ins.args] + return lambda : sage.function(ins.__class__.__name__)(*args) + +_undef_sage_helper = UndefSageHelper() + + +class UndefinedFunction(FunctionClass): + """ + The (meta)class of undefined functions. + """ + name: str + _sage_: UndefSageHelper + + def __new__(mcl, name, bases=(AppliedUndef,), __dict__=None, **kwargs) -> type[AppliedUndef]: + from .symbol import _filter_assumptions + # Allow Function('f', real=True) + # and/or Function(Symbol('f', real=True)) + assumptions, kwargs = _filter_assumptions(kwargs) + if isinstance(name, Symbol): + assumptions = name._merge(assumptions) + name = name.name + elif not isinstance(name, str): + raise TypeError('expecting string or Symbol for name') + else: + commutative = assumptions.get('commutative', None) + assumptions = Symbol(name, **assumptions).assumptions0 + if commutative is None: + assumptions.pop('commutative') + __dict__ = __dict__ or {} + # put the `is_*` for into __dict__ + __dict__.update({'is_%s' % k: v for k, v in assumptions.items()}) + # You can add other attributes, although they do have to be hashable + # (but seriously, if you want to add anything other than assumptions, + # just subclass Function) + __dict__.update(kwargs) + # add back the sanitized assumptions without the is_ prefix + kwargs.update(assumptions) + # Save these for __eq__ + __dict__.update({'_kwargs': kwargs}) + # do this for pickling + __dict__['__module__'] = None + obj = super().__new__(mcl, name, bases, __dict__) # type: ignore + obj.name = name + obj._sage_ = _undef_sage_helper + return obj # type: ignore + + def __instancecheck__(cls, instance): + return cls in type(instance).__mro__ + + _kwargs: dict[str, bool | None] = {} + + def __hash__(self): + return hash((self.class_key(), frozenset(self._kwargs.items()))) + + def __eq__(self, other): + return (isinstance(other, self.__class__) and + self.class_key() == other.class_key() and + self._kwargs == other._kwargs) + + def __ne__(self, other): + return not self == other + + @property + def _diff_wrt(self): + return False + + +# Using copyreg is the only way to make a dynamically generated instance of a +# metaclass picklable without using a custom pickler. It is not possible to +# define e.g. __reduce__ on the metaclass because obj.__reduce__ will retrieve +# the __reduce__ method for reducing instances of the type rather than for the +# type itself. +def _reduce_undef(f): + return (_rebuild_undef, (f.name, f._kwargs)) + +def _rebuild_undef(name, kwargs): + return Function(name, **kwargs) + +copyreg.pickle(UndefinedFunction, _reduce_undef) + + +# XXX: The type: ignore on WildFunction is because mypy complains: +# +# sympy/core/function.py:939: error: Cannot determine type of 'sort_key' in +# base class 'Expr' +# +# Somehow this is because of the @cacheit decorator but it is not clear how to +# fix it. + + +class WildFunction(Function, AtomicExpr): # type: ignore + """ + A WildFunction function matches any function (with its arguments). + + Examples + ======== + + >>> from sympy import WildFunction, Function, cos + >>> from sympy.abc import x, y + >>> F = WildFunction('F') + >>> f = Function('f') + >>> F.nargs + Naturals0 + >>> x.match(F) + >>> F.match(F) + {F_: F_} + >>> f(x).match(F) + {F_: f(x)} + >>> cos(x).match(F) + {F_: cos(x)} + >>> f(x, y).match(F) + {F_: f(x, y)} + + To match functions with a given number of arguments, set ``nargs`` to the + desired value at instantiation: + + >>> F = WildFunction('F', nargs=2) + >>> F.nargs + {2} + >>> f(x).match(F) + >>> f(x, y).match(F) + {F_: f(x, y)} + + To match functions with a range of arguments, set ``nargs`` to a tuple + containing the desired number of arguments, e.g. if ``nargs = (1, 2)`` + then functions with 1 or 2 arguments will be matched. + + >>> F = WildFunction('F', nargs=(1, 2)) + >>> F.nargs + {1, 2} + >>> f(x).match(F) + {F_: f(x)} + >>> f(x, y).match(F) + {F_: f(x, y)} + >>> f(x, y, 1).match(F) + + """ + + # XXX: What is this class attribute used for? + include: set[Any] = set() + + def __init__(cls, name, **assumptions): + from sympy.sets.sets import Set, FiniteSet + cls.name = name + nargs = assumptions.pop('nargs', S.Naturals0) + if not isinstance(nargs, Set): + # Canonicalize nargs here. See also FunctionClass. + if is_sequence(nargs): + nargs = tuple(ordered(set(nargs))) + elif nargs is not None: + nargs = (as_int(nargs),) + nargs = FiniteSet(*nargs) + cls.nargs = nargs + + def matches(self, expr, repl_dict=None, old=False): + if not isinstance(expr, (AppliedUndef, Function)): + return None + if len(expr.args) not in self.nargs: + return None + + if repl_dict is None: + repl_dict = {} + else: + repl_dict = repl_dict.copy() + + repl_dict[self] = expr + return repl_dict + + +class Derivative(Expr): + """ + Carries out differentiation of the given expression with respect to symbols. + + Examples + ======== + + >>> from sympy import Derivative, Function, symbols, Subs + >>> from sympy.abc import x, y + >>> f, g = symbols('f g', cls=Function) + + >>> Derivative(x**2, x, evaluate=True) + 2*x + + Denesting of derivatives retains the ordering of variables: + + >>> Derivative(Derivative(f(x, y), y), x) + Derivative(f(x, y), y, x) + + Contiguously identical symbols are merged into a tuple giving + the symbol and the count: + + >>> Derivative(f(x), x, x, y, x) + Derivative(f(x), (x, 2), y, x) + + If the derivative cannot be performed, and evaluate is True, the + order of the variables of differentiation will be made canonical: + + >>> Derivative(f(x, y), y, x, evaluate=True) + Derivative(f(x, y), x, y) + + Derivatives with respect to undefined functions can be calculated: + + >>> Derivative(f(x)**2, f(x), evaluate=True) + 2*f(x) + + Such derivatives will show up when the chain rule is used to + evaluate a derivative: + + >>> f(g(x)).diff(x) + Derivative(f(g(x)), g(x))*Derivative(g(x), x) + + Substitution is used to represent derivatives of functions with + arguments that are not symbols or functions: + + >>> f(2*x + 3).diff(x) == 2*Subs(f(y).diff(y), y, 2*x + 3) + True + + Notes + ===== + + Simplification of high-order derivatives: + + Because there can be a significant amount of simplification that can be + done when multiple differentiations are performed, results will be + automatically simplified in a fairly conservative fashion unless the + keyword ``simplify`` is set to False. + + >>> from sympy import sqrt, diff, Function, symbols + >>> from sympy.abc import x, y, z + >>> f, g = symbols('f,g', cls=Function) + + >>> e = sqrt((x + 1)**2 + x) + >>> diff(e, (x, 5), simplify=False).count_ops() + 136 + >>> diff(e, (x, 5)).count_ops() + 30 + + Ordering of variables: + + If evaluate is set to True and the expression cannot be evaluated, the + list of differentiation symbols will be sorted, that is, the expression is + assumed to have continuous derivatives up to the order asked. + + Derivative wrt non-Symbols: + + For the most part, one may not differentiate wrt non-symbols. + For example, we do not allow differentiation wrt `x*y` because + there are multiple ways of structurally defining where x*y appears + in an expression: a very strict definition would make + (x*y*z).diff(x*y) == 0. Derivatives wrt defined functions (like + cos(x)) are not allowed, either: + + >>> (x*y*z).diff(x*y) + Traceback (most recent call last): + ... + ValueError: Can't calculate derivative wrt x*y. + + To make it easier to work with variational calculus, however, + derivatives wrt AppliedUndef and Derivatives are allowed. + For example, in the Euler-Lagrange method one may write + F(t, u, v) where u = f(t) and v = f'(t). These variables can be + written explicitly as functions of time:: + + >>> from sympy.abc import t + >>> F = Function('F') + >>> U = f(t) + >>> V = U.diff(t) + + The derivative wrt f(t) can be obtained directly: + + >>> direct = F(t, U, V).diff(U) + + When differentiation wrt a non-Symbol is attempted, the non-Symbol + is temporarily converted to a Symbol while the differentiation + is performed and the same answer is obtained: + + >>> indirect = F(t, U, V).subs(U, x).diff(x).subs(x, U) + >>> assert direct == indirect + + The implication of this non-symbol replacement is that all + functions are treated as independent of other functions and the + symbols are independent of the functions that contain them:: + + >>> x.diff(f(x)) + 0 + >>> g(x).diff(f(x)) + 0 + + It also means that derivatives are assumed to depend only + on the variables of differentiation, not on anything contained + within the expression being differentiated:: + + >>> F = f(x) + >>> Fx = F.diff(x) + >>> Fx.diff(F) # derivative depends on x, not F + 0 + >>> Fxx = Fx.diff(x) + >>> Fxx.diff(Fx) # derivative depends on x, not Fx + 0 + + The last example can be made explicit by showing the replacement + of Fx in Fxx with y: + + >>> Fxx.subs(Fx, y) + Derivative(y, x) + + Since that in itself will evaluate to zero, differentiating + wrt Fx will also be zero: + + >>> _.doit() + 0 + + Replacing undefined functions with concrete expressions + + One must be careful to replace undefined functions with expressions + that contain variables consistent with the function definition and + the variables of differentiation or else insconsistent result will + be obtained. Consider the following example: + + >>> eq = f(x)*g(y) + >>> eq.subs(f(x), x*y).diff(x, y).doit() + y*Derivative(g(y), y) + g(y) + >>> eq.diff(x, y).subs(f(x), x*y).doit() + y*Derivative(g(y), y) + + The results differ because `f(x)` was replaced with an expression + that involved both variables of differentiation. In the abstract + case, differentiation of `f(x)` by `y` is 0; in the concrete case, + the presence of `y` made that derivative nonvanishing and produced + the extra `g(y)` term. + + Defining differentiation for an object + + An object must define ._eval_derivative(symbol) method that returns + the differentiation result. This function only needs to consider the + non-trivial case where expr contains symbol and it should call the diff() + method internally (not _eval_derivative); Derivative should be the only + one to call _eval_derivative. + + Any class can allow derivatives to be taken with respect to + itself (while indicating its scalar nature). See the + docstring of Expr._diff_wrt. + + See Also + ======== + _sort_variable_count + """ + + is_Derivative = True + + @property + def _diff_wrt(self): + """An expression may be differentiated wrt a Derivative if + it is in elementary form. + + Examples + ======== + + >>> from sympy import Function, Derivative, cos + >>> from sympy.abc import x + >>> f = Function('f') + + >>> Derivative(f(x), x)._diff_wrt + True + >>> Derivative(cos(x), x)._diff_wrt + False + >>> Derivative(x + 1, x)._diff_wrt + False + + A Derivative might be an unevaluated form of what will not be + a valid variable of differentiation if evaluated. For example, + + >>> Derivative(f(f(x)), x).doit() + Derivative(f(x), x)*Derivative(f(f(x)), f(x)) + + Such an expression will present the same ambiguities as arise + when dealing with any other product, like ``2*x``, so ``_diff_wrt`` + is False: + + >>> Derivative(f(f(x)), x)._diff_wrt + False + """ + return self.expr._diff_wrt and isinstance(self.doit(), Derivative) + + def __new__(cls, expr, *variables, **kwargs): + expr = sympify(expr) + if not isinstance(expr, Basic): + raise TypeError(f"Cannot represent derivative of {type(expr)}") + symbols_or_none = getattr(expr, "free_symbols", None) + has_symbol_set = isinstance(symbols_or_none, set) + + if not has_symbol_set: + raise ValueError(filldedent(''' + Since there are no variables in the expression %s, + it cannot be differentiated.''' % expr)) + + # determine value for variables if it wasn't given + if not variables: + variables = expr.free_symbols + if len(variables) != 1: + if expr.is_number: + return S.Zero + if len(variables) == 0: + raise ValueError(filldedent(''' + Since there are no variables in the expression, + the variable(s) of differentiation must be supplied + to differentiate %s''' % expr)) + else: + raise ValueError(filldedent(''' + Since there is more than one variable in the + expression, the variable(s) of differentiation + must be supplied to differentiate %s''' % expr)) + + # Split the list of variables into a list of the variables we are diff + # wrt, where each element of the list has the form (s, count) where + # s is the entity to diff wrt and count is the order of the + # derivative. + variable_count = [] + array_likes = (tuple, list, Tuple) + + from sympy.tensor.array import Array, NDimArray + + for i, v in enumerate(variables): + if isinstance(v, UndefinedFunction): + raise TypeError( + "cannot differentiate wrt " + "UndefinedFunction: %s" % v) + + if isinstance(v, array_likes): + if len(v) == 0: + # Ignore empty tuples: Derivative(expr, ... , (), ... ) + continue + if isinstance(v[0], array_likes): + # Derive by array: Derivative(expr, ... , [[x, y, z]], ... ) + if len(v) == 1: + v = Array(v[0]) + count = 1 + else: + v, count = v + v = Array(v) + else: + v, count = v + if count == 0: + continue + variable_count.append(Tuple(v, count)) + continue + + v = sympify(v) + if isinstance(v, Integer): + if i == 0: + raise ValueError("First variable cannot be a number: %i" % v) + count = v + prev, prevcount = variable_count[-1] + if prevcount != 1: + raise TypeError("tuple {} followed by number {}".format((prev, prevcount), v)) + if count == 0: + variable_count.pop() + else: + variable_count[-1] = Tuple(prev, count) + else: + count = 1 + variable_count.append(Tuple(v, count)) + + # light evaluation of contiguous, identical + # items: (x, 1), (x, 1) -> (x, 2) + merged = [] + for t in variable_count: + v, c = t + if c.is_negative: + raise ValueError( + 'order of differentiation must be nonnegative') + if merged and merged[-1][0] == v: + c += merged[-1][1] + if not c: + merged.pop() + else: + merged[-1] = Tuple(v, c) + else: + merged.append(t) + variable_count = merged + + # sanity check of variables of differentation; we waited + # until the counts were computed since some variables may + # have been removed because the count was 0 + for v, c in variable_count: + # v must have _diff_wrt True + if not v._diff_wrt: + __ = '' # filler to make error message neater + raise ValueError(filldedent(''' + Can't calculate derivative wrt %s.%s''' % (v, + __))) + + # We make a special case for 0th derivative, because there is no + # good way to unambiguously print this. + if len(variable_count) == 0: + return expr + + evaluate = kwargs.get('evaluate', False) + + if evaluate: + if isinstance(expr, Derivative): + expr = expr.canonical + variable_count = [ + (v.canonical if isinstance(v, Derivative) else v, c) + for v, c in variable_count] + + # Look for a quick exit if there are symbols that don't appear in + # expression at all. Note, this cannot check non-symbols like + # Derivatives as those can be created by intermediate + # derivatives. + zero = False + free = expr.free_symbols + from sympy.matrices.expressions.matexpr import MatrixExpr + + for v, c in variable_count: + vfree = v.free_symbols + if c.is_positive and vfree: + if isinstance(v, AppliedUndef): + # these match exactly since + # x.diff(f(x)) == g(x).diff(f(x)) == 0 + # and are not created by differentiation + D = Dummy() + if not expr.xreplace({v: D}).has(D): + zero = True + break + elif isinstance(v, MatrixExpr): + zero = False + break + elif isinstance(v, Symbol) and v not in free: + zero = True + break + else: + if not free & vfree: + # e.g. v is IndexedBase or Matrix + zero = True + break + if zero: + return cls._get_zero_with_shape_like(expr) + + # make the order of symbols canonical + #TODO: check if assumption of discontinuous derivatives exist + variable_count = cls._sort_variable_count(variable_count) + + # denest + if isinstance(expr, Derivative): + variable_count = list(expr.variable_count) + variable_count + expr = expr.expr + return _derivative_dispatch(expr, *variable_count, **kwargs) + + # we return here if evaluate is False or if there is no + # _eval_derivative method + if not evaluate or not hasattr(expr, '_eval_derivative'): + # return an unevaluated Derivative + if evaluate and variable_count == [(expr, 1)] and expr.is_scalar: + # special hack providing evaluation for classes + # that have defined is_scalar=True but have no + # _eval_derivative defined + return S.One + return Expr.__new__(cls, expr, *variable_count) + + # evaluate the derivative by calling _eval_derivative method + # of expr for each variable + # ------------------------------------------------------------- + nderivs = 0 # how many derivatives were performed + unhandled = [] + from sympy.matrices.matrixbase import MatrixBase + for i, (v, count) in enumerate(variable_count): + + old_expr = expr + old_v = None + + is_symbol = v.is_symbol or isinstance(v, + (Iterable, Tuple, MatrixBase, NDimArray)) + + if not is_symbol: + old_v = v + v = Dummy('xi') + expr = expr.xreplace({old_v: v}) + # Derivatives and UndefinedFunctions are independent + # of all others + clashing = not (isinstance(old_v, (Derivative, AppliedUndef))) + if v not in expr.free_symbols and not clashing: + return expr.diff(v) # expr's version of 0 + if not old_v.is_scalar and not hasattr( + old_v, '_eval_derivative'): + # special hack providing evaluation for classes + # that have defined is_scalar=True but have no + # _eval_derivative defined + expr *= old_v.diff(old_v) + + obj = cls._dispatch_eval_derivative_n_times(expr, v, count) + if obj is not None and obj.is_zero: + return obj + + nderivs += count + + if old_v is not None: + if obj is not None: + # remove the dummy that was used + obj = obj.subs(v, old_v) + # restore expr + expr = old_expr + + if obj is None: + # we've already checked for quick-exit conditions + # that give 0 so the remaining variables + # are contained in the expression but the expression + # did not compute a derivative so we stop taking + # derivatives + unhandled = variable_count[i:] + break + + expr = obj + + # what we have so far can be made canonical + expr = expr.replace( + lambda x: isinstance(x, Derivative), + lambda x: x.canonical) + + if unhandled: + if isinstance(expr, Derivative): + unhandled = list(expr.variable_count) + unhandled + expr = expr.expr + expr = Expr.__new__(cls, expr, *unhandled) + + if (nderivs > 1) == True and kwargs.get('simplify', True): + from .exprtools import factor_terms + from sympy.simplify.simplify import signsimp + expr = factor_terms(signsimp(expr)) + return expr + + @property + def canonical(cls): + return cls.func(cls.expr, + *Derivative._sort_variable_count(cls.variable_count)) + + @classmethod + def _sort_variable_count(cls, vc): + """ + Sort (variable, count) pairs into canonical order while + retaining order of variables that do not commute during + differentiation: + + * symbols and functions commute with each other + * derivatives commute with each other + * a derivative does not commute with anything it contains + * any other object is not allowed to commute if it has + free symbols in common with another object + + Examples + ======== + + >>> from sympy import Derivative, Function, symbols + >>> vsort = Derivative._sort_variable_count + >>> x, y, z = symbols('x y z') + >>> f, g, h = symbols('f g h', cls=Function) + + Contiguous items are collapsed into one pair: + + >>> vsort([(x, 1), (x, 1)]) + [(x, 2)] + >>> vsort([(y, 1), (f(x), 1), (y, 1), (f(x), 1)]) + [(y, 2), (f(x), 2)] + + Ordering is canonical. + + >>> def vsort0(*v): + ... # docstring helper to + ... # change vi -> (vi, 0), sort, and return vi vals + ... return [i[0] for i in vsort([(i, 0) for i in v])] + + >>> vsort0(y, x) + [x, y] + >>> vsort0(g(y), g(x), f(y)) + [f(y), g(x), g(y)] + + Symbols are sorted as far to the left as possible but never + move to the left of a derivative having the same symbol in + its variables; the same applies to AppliedUndef which are + always sorted after Symbols: + + >>> dfx = f(x).diff(x) + >>> assert vsort0(dfx, y) == [y, dfx] + >>> assert vsort0(dfx, x) == [dfx, x] + """ + if not vc: + return [] + vc = list(vc) + if len(vc) == 1: + return [Tuple(*vc[0])] + V = list(range(len(vc))) + E = [] + v = lambda i: vc[i][0] + D = Dummy() + def _block(d, v, wrt=False): + # return True if v should not come before d else False + if d == v: + return wrt + if d.is_Symbol: + return False + if isinstance(d, Derivative): + # a derivative blocks if any of it's variables contain + # v; the wrt flag will return True for an exact match + # and will cause an AppliedUndef to block if v is in + # the arguments + if any(_block(k, v, wrt=True) + for k in d._wrt_variables): + return True + return False + if not wrt and isinstance(d, AppliedUndef): + return False + if v.is_Symbol: + return v in d.free_symbols + if isinstance(v, AppliedUndef): + return _block(d.xreplace({v: D}), D) + return d.free_symbols & v.free_symbols + for i in range(len(vc)): + for j in range(i): + if _block(v(j), v(i)): + E.append((j,i)) + # this is the default ordering to use in case of ties + O = dict(zip(ordered(uniq([i for i, c in vc])), range(len(vc)))) + ix = topological_sort((V, E), key=lambda i: O[v(i)]) + # merge counts of contiguously identical items + merged = [] + for v, c in [vc[i] for i in ix]: + if merged and merged[-1][0] == v: + merged[-1][1] += c + else: + merged.append([v, c]) + return [Tuple(*i) for i in merged] + + def _eval_is_commutative(self): + return self.expr.is_commutative + + def _eval_derivative(self, v): + # If v (the variable of differentiation) is not in + # self.variables, we might be able to take the derivative. + if v not in self._wrt_variables: + dedv = self.expr.diff(v) + if isinstance(dedv, Derivative): + return dedv.func(dedv.expr, *(self.variable_count + dedv.variable_count)) + # dedv (d(self.expr)/dv) could have simplified things such that the + # derivative wrt things in self.variables can now be done. Thus, + # we set evaluate=True to see if there are any other derivatives + # that can be done. The most common case is when dedv is a simple + # number so that the derivative wrt anything else will vanish. + return self.func(dedv, *self.variables, evaluate=True) + # In this case v was in self.variables so the derivative wrt v has + # already been attempted and was not computed, either because it + # couldn't be or evaluate=False originally. + variable_count = list(self.variable_count) + variable_count.append((v, 1)) + return self.func(self.expr, *variable_count, evaluate=False) + + def doit(self, **hints): + expr = self.expr + if hints.get('deep', True): + expr = expr.doit(**hints) + hints['evaluate'] = True + rv = self.func(expr, *self.variable_count, **hints) + if rv!= self and rv.has(Derivative): + rv = rv.doit(**hints) + return rv + + @_sympifyit('z0', NotImplementedError) + def doit_numerically(self, z0): + """ + Evaluate the derivative at z numerically. + + When we can represent derivatives at a point, this should be folded + into the normal evalf. For now, we need a special method. + """ + if len(self.free_symbols) != 1 or len(self.variables) != 1: + raise NotImplementedError('partials and higher order derivatives') + z = list(self.free_symbols)[0] + + def eval(x): + f0 = self.expr.subs(z, Expr._from_mpmath(x, prec=mpmath.mp.prec)) + f0 = f0.evalf(prec_to_dps(mpmath.mp.prec)) + return f0._to_mpmath(mpmath.mp.prec) + return Expr._from_mpmath(mpmath.diff(eval, + z0._to_mpmath(mpmath.mp.prec)), + mpmath.mp.prec) + + @property + def expr(self): + return self._args[0] + + @property + def _wrt_variables(self): + # return the variables of differentiation without + # respect to the type of count (int or symbolic) + return [i[0] for i in self.variable_count] + + @property + def variables(self): + # TODO: deprecate? YES, make this 'enumerated_variables' and + # name _wrt_variables as variables + # TODO: support for `d^n`? + rv = [] + for v, count in self.variable_count: + if not count.is_Integer: + raise TypeError(filldedent(''' + Cannot give expansion for symbolic count. If you just + want a list of all variables of differentiation, use + _wrt_variables.''')) + rv.extend([v]*count) + return tuple(rv) + + @property + def variable_count(self): + return self._args[1:] + + @property + def derivative_count(self): + return sum([count for _, count in self.variable_count], 0) + + @property + def free_symbols(self): + ret = self.expr.free_symbols + # Add symbolic counts to free_symbols + for _, count in self.variable_count: + ret.update(count.free_symbols) + return ret + + @property + def kind(self): + return self.args[0].kind + + def _eval_subs(self, old, new): + # The substitution (old, new) cannot be done inside + # Derivative(expr, vars) for a variety of reasons + # as handled below. + if old in self._wrt_variables: + # first handle the counts + expr = self.func(self.expr, *[(v, c.subs(old, new)) + for v, c in self.variable_count]) + if expr != self: + return expr._eval_subs(old, new) + # quick exit case + if not getattr(new, '_diff_wrt', False): + # case (0): new is not a valid variable of + # differentiation + if isinstance(old, Symbol): + # don't introduce a new symbol if the old will do + return Subs(self, old, new) + else: + xi = Dummy('xi') + return Subs(self.xreplace({old: xi}), xi, new) + + # If both are Derivatives with the same expr, check if old is + # equivalent to self or if old is a subderivative of self. + if old.is_Derivative and old.expr == self.expr: + if self.canonical == old.canonical: + return new + + # collections.Counter doesn't have __le__ + def _subset(a, b): + return all((a[i] <= b[i]) == True for i in a) + + old_vars = Counter(dict(reversed(old.variable_count))) + self_vars = Counter(dict(reversed(self.variable_count))) + if _subset(old_vars, self_vars): + return _derivative_dispatch(new, *(self_vars - old_vars).items()).canonical + + args = list(self.args) + newargs = [x._subs(old, new) for x in args] + if args[0] == old: + # complete replacement of self.expr + # we already checked that the new is valid so we know + # it won't be a problem should it appear in variables + return _derivative_dispatch(*newargs) + + if newargs[0] != args[0]: + # case (1) can't change expr by introducing something that is in + # the _wrt_variables if it was already in the expr + # e.g. + # for Derivative(f(x, g(y)), y), x cannot be replaced with + # anything that has y in it; for f(g(x), g(y)).diff(g(y)) + # g(x) cannot be replaced with anything that has g(y) + syms = {vi: Dummy() for vi in self._wrt_variables + if not vi.is_Symbol} + wrt = {syms.get(vi, vi) for vi in self._wrt_variables} + forbidden = args[0].xreplace(syms).free_symbols & wrt + nfree = new.xreplace(syms).free_symbols + ofree = old.xreplace(syms).free_symbols + if (nfree - ofree) & forbidden: + return Subs(self, old, new) + + viter = ((i, j) for ((i, _), (j, _)) in zip(newargs[1:], args[1:])) + if any(i != j for i, j in viter): # a wrt-variable change + # case (2) can't change vars by introducing a variable + # that is contained in expr, e.g. + # for Derivative(f(z, g(h(x), y)), y), y cannot be changed to + # x, h(x), or g(h(x), y) + for a in _atomic(self.expr, recursive=True): + for i in range(1, len(newargs)): + vi, _ = newargs[i] + if a == vi and vi != args[i][0]: + return Subs(self, old, new) + # more arg-wise checks + vc = newargs[1:] + oldv = self._wrt_variables + newe = self.expr + subs = [] + for i, (vi, ci) in enumerate(vc): + if not vi._diff_wrt: + # case (3) invalid differentiation expression so + # create a replacement dummy + xi = Dummy('xi_%i' % i) + # replace the old valid variable with the dummy + # in the expression + newe = newe.xreplace({oldv[i]: xi}) + # and replace the bad variable with the dummy + vc[i] = (xi, ci) + # and record the dummy with the new (invalid) + # differentiation expression + subs.append((xi, vi)) + + if subs: + # handle any residual substitution in the expression + newe = newe._subs(old, new) + # return the Subs-wrapped derivative + return Subs(Derivative(newe, *vc), *zip(*subs)) + + # everything was ok + return _derivative_dispatch(*newargs) + + def _eval_lseries(self, x, logx, cdir=0): + dx = self.variables + for term in self.expr.lseries(x, logx=logx, cdir=cdir): + yield self.func(term, *dx) + + def _eval_nseries(self, x, n, logx, cdir=0): + arg = self.expr.nseries(x, n=n, logx=logx) + o = arg.getO() + dx = self.variables + rv = [self.func(a, *dx) for a in Add.make_args(arg.removeO())] + if o: + rv.append(o/x) + return Add(*rv) + + def _eval_as_leading_term(self, x, logx, cdir): + series_gen = self.expr.lseries(x) + d = S.Zero + for leading_term in series_gen: + d = diff(leading_term, *self.variables) + if d != 0: + break + return d + + def as_finite_difference(self, points=1, x0=None, wrt=None): + """ Expresses a Derivative instance as a finite difference. + + Parameters + ========== + + points : sequence or coefficient, optional + If sequence: discrete values (length >= order+1) of the + independent variable used for generating the finite + difference weights. + If it is a coefficient, it will be used as the step-size + for generating an equidistant sequence of length order+1 + centered around ``x0``. Default: 1 (step-size 1) + + x0 : number or Symbol, optional + the value of the independent variable (``wrt``) at which the + derivative is to be approximated. Default: same as ``wrt``. + + wrt : Symbol, optional + "with respect to" the variable for which the (partial) + derivative is to be approximated for. If not provided it + is required that the derivative is ordinary. Default: ``None``. + + + Examples + ======== + + >>> from sympy import symbols, Function, exp, sqrt, Symbol + >>> x, h = symbols('x h') + >>> f = Function('f') + >>> f(x).diff(x).as_finite_difference() + -f(x - 1/2) + f(x + 1/2) + + The default step size and number of points are 1 and + ``order + 1`` respectively. We can change the step size by + passing a symbol as a parameter: + + >>> f(x).diff(x).as_finite_difference(h) + -f(-h/2 + x)/h + f(h/2 + x)/h + + We can also specify the discretized values to be used in a + sequence: + + >>> f(x).diff(x).as_finite_difference([x, x+h, x+2*h]) + -3*f(x)/(2*h) + 2*f(h + x)/h - f(2*h + x)/(2*h) + + The algorithm is not restricted to use equidistant spacing, nor + do we need to make the approximation around ``x0``, but we can get + an expression estimating the derivative at an offset: + + >>> e, sq2 = exp(1), sqrt(2) + >>> xl = [x-h, x+h, x+e*h] + >>> f(x).diff(x, 1).as_finite_difference(xl, x+h*sq2) # doctest: +ELLIPSIS + 2*h*((h + sqrt(2)*h)/(2*h) - (-sqrt(2)*h + h)/(2*h))*f(E*h + x)/... + + To approximate ``Derivative`` around ``x0`` using a non-equidistant + spacing step, the algorithm supports assignment of undefined + functions to ``points``: + + >>> dx = Function('dx') + >>> f(x).diff(x).as_finite_difference(points=dx(x), x0=x-h) + -f(-h + x - dx(-h + x)/2)/dx(-h + x) + f(-h + x + dx(-h + x)/2)/dx(-h + x) + + Partial derivatives are also supported: + + >>> y = Symbol('y') + >>> d2fdxdy=f(x,y).diff(x,y) + >>> d2fdxdy.as_finite_difference(wrt=x) + -Derivative(f(x - 1/2, y), y) + Derivative(f(x + 1/2, y), y) + + We can apply ``as_finite_difference`` to ``Derivative`` instances in + compound expressions using ``replace``: + + >>> (1 + 42**f(x).diff(x)).replace(lambda arg: arg.is_Derivative, + ... lambda arg: arg.as_finite_difference()) + 42**(-f(x - 1/2) + f(x + 1/2)) + 1 + + + See also + ======== + + sympy.calculus.finite_diff.apply_finite_diff + sympy.calculus.finite_diff.differentiate_finite + sympy.calculus.finite_diff.finite_diff_weights + + """ + from sympy.calculus.finite_diff import _as_finite_diff + return _as_finite_diff(self, points, x0, wrt) + + @classmethod + def _get_zero_with_shape_like(cls, expr): + return S.Zero + + @classmethod + def _dispatch_eval_derivative_n_times(cls, expr, v, count): + # Evaluate the derivative `n` times. If + # `_eval_derivative_n_times` is not overridden by the current + # object, the default in `Basic` will call a loop over + # `_eval_derivative`: + return expr._eval_derivative_n_times(v, count) + + +def _derivative_dispatch(expr, *variables, **kwargs): + from sympy.matrices.matrixbase import MatrixBase + from sympy.matrices.expressions.matexpr import MatrixExpr + from sympy.tensor.array import NDimArray + array_types = (MatrixBase, MatrixExpr, NDimArray, list, tuple, Tuple) + if isinstance(expr, array_types) or any(isinstance(i[0], array_types) if isinstance(i, (tuple, list, Tuple)) else isinstance(i, array_types) for i in variables): + from sympy.tensor.array.array_derivatives import ArrayDerivative + return ArrayDerivative(expr, *variables, **kwargs) + return Derivative(expr, *variables, **kwargs) + + +class Lambda(Expr): + """ + Lambda(x, expr) represents a lambda function similar to Python's + 'lambda x: expr'. A function of several variables is written as + Lambda((x, y, ...), expr). + + Examples + ======== + + A simple example: + + >>> from sympy import Lambda + >>> from sympy.abc import x + >>> f = Lambda(x, x**2) + >>> f(4) + 16 + + For multivariate functions, use: + + >>> from sympy.abc import y, z, t + >>> f2 = Lambda((x, y, z, t), x + y**z + t**z) + >>> f2(1, 2, 3, 4) + 73 + + It is also possible to unpack tuple arguments: + + >>> f = Lambda(((x, y), z), x + y + z) + >>> f((1, 2), 3) + 6 + + A handy shortcut for lots of arguments: + + >>> p = x, y, z + >>> f = Lambda(p, x + y*z) + >>> f(*p) + x + y*z + + """ + is_Function = True + + def __new__(cls, signature, expr) -> Lambda: + if iterable(signature) and not isinstance(signature, (tuple, Tuple)): + sympy_deprecation_warning( + """ + Using a non-tuple iterable as the first argument to Lambda + is deprecated. Use Lambda(tuple(args), expr) instead. + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-non-tuple-lambda", + ) + signature = tuple(signature) + _sig = signature if iterable(signature) else (signature,) + sig: Tuple = sympify(_sig) # type: ignore + cls._check_signature(sig) + + if len(sig) == 1 and sig[0] == expr: + return S.IdentityFunction + + return Expr.__new__(cls, sig, sympify(expr)) + + @classmethod + def _check_signature(cls, sig): + syms = set() + + def rcheck(args): + for a in args: + if a.is_symbol: + if a in syms: + raise BadSignatureError("Duplicate symbol %s" % a) + syms.add(a) + elif isinstance(a, Tuple): + rcheck(a) + else: + raise BadSignatureError("Lambda signature should be only tuples" + " and symbols, not %s" % a) + + if not isinstance(sig, Tuple): + raise BadSignatureError("Lambda signature should be a tuple not %s" % sig) + # Recurse through the signature: + rcheck(sig) + + @property + def signature(self): + """The expected form of the arguments to be unpacked into variables""" + return self._args[0] + + @property + def expr(self): + """The return value of the function""" + return self._args[1] + + @property + def variables(self): + """The variables used in the internal representation of the function""" + def _variables(args): + if isinstance(args, Tuple): + for arg in args: + yield from _variables(arg) + else: + yield args + return tuple(_variables(self.signature)) + + @property + def nargs(self): + from sympy.sets.sets import FiniteSet + return FiniteSet(len(self.signature)) + + bound_symbols = variables + + @property + def free_symbols(self): + return self.expr.free_symbols - set(self.variables) + + def __call__(self, *args): + n = len(args) + if n not in self.nargs: # Lambda only ever has 1 value in nargs + # XXX: exception message must be in exactly this format to + # make it work with NumPy's functions like vectorize(). See, + # for example, https://github.com/numpy/numpy/issues/1697. + # The ideal solution would be just to attach metadata to + # the exception and change NumPy to take advantage of this. + ## XXX does this apply to Lambda? If not, remove this comment. + temp = ('%(name)s takes exactly %(args)s ' + 'argument%(plural)s (%(given)s given)') + raise BadArgumentsError(temp % { + 'name': self, + 'args': list(self.nargs)[0], + 'plural': 's'*(list(self.nargs)[0] != 1), + 'given': n}) + + d = self._match_signature(self.signature, args) + + return self.expr.xreplace(d) + + def _match_signature(self, sig, args): + + symargmap = {} + + def rmatch(pars, args): + for par, arg in zip(pars, args): + if par.is_symbol: + symargmap[par] = arg + elif isinstance(par, Tuple): + if not isinstance(arg, (tuple, Tuple)) or len(args) != len(pars): + raise BadArgumentsError("Can't match %s and %s" % (args, pars)) + rmatch(par, arg) + + rmatch(sig, args) + + return symargmap + + @property + def is_identity(self): + """Return ``True`` if this ``Lambda`` is an identity function. """ + return self.signature == self.expr + + def _eval_evalf(self, prec): + return self.func(self.args[0], self.args[1].evalf(n=prec_to_dps(prec))) + + +class Subs(Expr): + """ + Represents unevaluated substitutions of an expression. + + ``Subs(expr, x, x0)`` represents the expression resulting + from substituting x with x0 in expr. + + Parameters + ========== + + expr : Expr + An expression. + + x : tuple, variable + A variable or list of distinct variables. + + x0 : tuple or list of tuples + A point or list of evaluation points + corresponding to those variables. + + Examples + ======== + + >>> from sympy import Subs, Function, sin, cos + >>> from sympy.abc import x, y, z + >>> f = Function('f') + + Subs are created when a particular substitution cannot be made. The + x in the derivative cannot be replaced with 0 because 0 is not a + valid variables of differentiation: + + >>> f(x).diff(x).subs(x, 0) + Subs(Derivative(f(x), x), x, 0) + + Once f is known, the derivative and evaluation at 0 can be done: + + >>> _.subs(f, sin).doit() == sin(x).diff(x).subs(x, 0) == cos(0) + True + + Subs can also be created directly with one or more variables: + + >>> Subs(f(x)*sin(y) + z, (x, y), (0, 1)) + Subs(z + f(x)*sin(y), (x, y), (0, 1)) + >>> _.doit() + z + f(0)*sin(1) + + Notes + ===== + + ``Subs`` objects are generally useful to represent unevaluated derivatives + calculated at a point. + + The variables may be expressions, but they are subjected to the limitations + of subs(), so it is usually a good practice to use only symbols for + variables, since in that case there can be no ambiguity. + + There's no automatic expansion - use the method .doit() to effect all + possible substitutions of the object and also of objects inside the + expression. + + When evaluating derivatives at a point that is not a symbol, a Subs object + is returned. One is also able to calculate derivatives of Subs objects - in + this case the expression is always expanded (for the unevaluated form, use + Derivative()). + + In order to allow expressions to combine before doit is done, a + representation of the Subs expression is used internally to make + expressions that are superficially different compare the same: + + >>> a, b = Subs(x, x, 0), Subs(y, y, 0) + >>> a + b + 2*Subs(x, x, 0) + + This can lead to unexpected consequences when using methods + like `has` that are cached: + + >>> s = Subs(x, x, 0) + >>> s.has(x), s.has(y) + (True, False) + >>> ss = s.subs(x, y) + >>> ss.has(x), ss.has(y) + (True, False) + >>> s, ss + (Subs(x, x, 0), Subs(y, y, 0)) + """ + def __new__(cls, expr, variables, point, **assumptions): + if not is_sequence(variables, Tuple): + variables = [variables] + variables = Tuple(*variables) + + if has_dups(variables): + repeated = [str(v) for v, i in Counter(variables).items() if i > 1] + __ = ', '.join(repeated) + raise ValueError(filldedent(''' + The following expressions appear more than once: %s + ''' % __)) + + point = Tuple(*(point if is_sequence(point, Tuple) else [point])) + + if len(point) != len(variables): + raise ValueError('Number of point values must be the same as ' + 'the number of variables.') + + if not point: + return sympify(expr) + + # denest + if isinstance(expr, Subs): + variables = expr.variables + variables + point = expr.point + point + expr = expr.expr + else: + expr = sympify(expr) + + # use symbols with names equal to the point value (with prepended _) + # to give a variable-independent expression + pre = "_" + pts = sorted(set(point), key=default_sort_key) + from sympy.printing.str import StrPrinter + class CustomStrPrinter(StrPrinter): + def _print_Dummy(self, expr): + return str(expr) + str(expr.dummy_index) + def mystr(expr, **settings): + p = CustomStrPrinter(settings) + return p.doprint(expr) + while 1: + s_pts = {p: Symbol(pre + mystr(p)) for p in pts} + reps = [(v, s_pts[p]) + for v, p in zip(variables, point)] + # if any underscore-prepended symbol is already a free symbol + # and is a variable with a different point value, then there + # is a clash, e.g. _0 clashes in Subs(_0 + _1, (_0, _1), (1, 0)) + # because the new symbol that would be created is _1 but _1 + # is already mapped to 0 so __0 and __1 are used for the new + # symbols + if any(r in expr.free_symbols and + r in variables and + Symbol(pre + mystr(point[variables.index(r)])) != r + for _, r in reps): + pre += "_" + continue + break + + obj = Expr.__new__(cls, expr, Tuple(*variables), point) + obj._expr = expr.xreplace(dict(reps)) + return obj + + def _eval_is_commutative(self): + return self.expr.is_commutative + + def doit(self, **hints): + e, v, p = self.args + + # remove self mappings + for i, (vi, pi) in enumerate(zip(v, p)): + if vi == pi: + v = v[:i] + v[i + 1:] + p = p[:i] + p[i + 1:] + if not v: + return self.expr + + if isinstance(e, Derivative): + # apply functions first, e.g. f -> cos + undone = [] + for i, vi in enumerate(v): + if isinstance(vi, FunctionClass): + e = e.subs(vi, p[i]) + else: + undone.append((vi, p[i])) + if not isinstance(e, Derivative): + e = e.doit() + if isinstance(e, Derivative): + # do Subs that aren't related to differentiation + undone2 = [] + D = Dummy() + arg = e.args[0] + for vi, pi in undone: + if D not in e.xreplace({vi: D}).free_symbols: + if arg.has(vi): + e = e.subs(vi, pi) + else: + undone2.append((vi, pi)) + undone = undone2 + # differentiate wrt variables that are present + wrt = [] + D = Dummy() + expr = e.expr + free = expr.free_symbols + for vi, ci in e.variable_count: + if isinstance(vi, Symbol) and vi in free: + expr = expr.diff((vi, ci)) + elif D in expr.subs(vi, D).free_symbols: + expr = expr.diff((vi, ci)) + else: + wrt.append((vi, ci)) + # inject remaining subs + rv = expr.subs(undone) + # do remaining differentiation *in order given* + for vc in wrt: + rv = rv.diff(vc) + else: + # inject remaining subs + rv = e.subs(undone) + else: + rv = e.doit(**hints).subs(list(zip(v, p))) + + if hints.get('deep', True) and rv != self: + rv = rv.doit(**hints) + return rv + + def evalf(self, prec=None, **options): + return self.doit().evalf(prec, **options) + + n = evalf # type:ignore + + @property + def variables(self): + """The variables to be evaluated""" + return self._args[1] + + bound_symbols = variables + + @property + def expr(self): + """The expression on which the substitution operates""" + return self._args[0] + + @property + def point(self): + """The values for which the variables are to be substituted""" + return self._args[2] + + @property + def free_symbols(self): + return (self.expr.free_symbols - set(self.variables) | + set(self.point.free_symbols)) + + @property + def expr_free_symbols(self): + sympy_deprecation_warning(""" + The expr_free_symbols property is deprecated. Use free_symbols to get + the free symbols of an expression. + """, + deprecated_since_version="1.9", + active_deprecations_target="deprecated-expr-free-symbols") + # Don't show the warning twice from the recursive call + with ignore_warnings(SymPyDeprecationWarning): + return (self.expr.expr_free_symbols - set(self.variables) | + set(self.point.expr_free_symbols)) + + def __eq__(self, other): + if not isinstance(other, Subs): + return False + return self._hashable_content() == other._hashable_content() + + def __ne__(self, other): + return not(self == other) + + def __hash__(self): + return super().__hash__() + + def _hashable_content(self): + return (self._expr.xreplace(self.canonical_variables), + ) + tuple(ordered([(v, p) for v, p in + zip(self.variables, self.point) if not self.expr.has(v)])) + + def _eval_subs(self, old, new): + # Subs doit will do the variables in order; the semantics + # of subs for Subs is have the following invariant for + # Subs object foo: + # foo.doit().subs(reps) == foo.subs(reps).doit() + pt = list(self.point) + if old in self.variables: + if _atomic(new) == {new} and not any( + i.has(new) for i in self.args): + # the substitution is neutral + return self.xreplace({old: new}) + # any occurrence of old before this point will get + # handled by replacements from here on + i = self.variables.index(old) + for j in range(i, len(self.variables)): + pt[j] = pt[j]._subs(old, new) + return self.func(self.expr, self.variables, pt) + v = [i._subs(old, new) for i in self.variables] + if v != list(self.variables): + return self.func(self.expr, self.variables + (old,), pt + [new]) + expr = self.expr._subs(old, new) + pt = [i._subs(old, new) for i in self.point] + return self.func(expr, v, pt) + + def _eval_derivative(self, s): + # Apply the chain rule of the derivative on the substitution variables: + f = self.expr + vp = V, P = self.variables, self.point + val = Add.fromiter(p.diff(s)*Subs(f.diff(v), *vp).doit() + for v, p in zip(V, P)) + + # these are all the free symbols in the expr + efree = f.free_symbols + # some symbols like IndexedBase include themselves and args + # as free symbols + compound = {i for i in efree if len(i.free_symbols) > 1} + # hide them and see what independent free symbols remain + dums = {Dummy() for i in compound} + masked = f.xreplace(dict(zip(compound, dums))) + ifree = masked.free_symbols - dums + # include the compound symbols + free = ifree | compound + # remove the variables already handled + free -= set(V) + # add back any free symbols of remaining compound symbols + free |= {i for j in free & compound for i in j.free_symbols} + # if symbols of s are in free then there is more to do + if free & s.free_symbols: + val += Subs(f.diff(s), self.variables, self.point).doit() + return val + + def _eval_nseries(self, x, n, logx, cdir=0): + if x in self.point: + # x is the variable being substituted into + apos = self.point.index(x) + other = self.variables[apos] + else: + other = x + arg = self.expr.nseries(other, n=n, logx=logx) + o = arg.getO() + terms = Add.make_args(arg.removeO()) + rv = Add(*[self.func(a, *self.args[1:]) for a in terms]) + if o: + rv += o.subs(other, x) + return rv + + def _eval_as_leading_term(self, x, logx, cdir): + if x in self.point: + ipos = self.point.index(x) + xvar = self.variables[ipos] + return self.expr.as_leading_term(xvar) + if x in self.variables: + # if `x` is a dummy variable, it means it won't exist after the + # substitution has been performed: + return self + # The variable is independent of the substitution: + return self.expr.as_leading_term(x) + + +def diff(f, *symbols, **kwargs): + """ + Differentiate f with respect to symbols. + + Explanation + =========== + + This is just a wrapper to unify .diff() and the Derivative class; its + interface is similar to that of integrate(). You can use the same + shortcuts for multiple variables as with Derivative. For example, + diff(f(x), x, x, x) and diff(f(x), x, 3) both return the third derivative + of f(x). + + You can pass evaluate=False to get an unevaluated Derivative class. Note + that if there are 0 symbols (such as diff(f(x), x, 0), then the result will + be the function (the zeroth derivative), even if evaluate=False. + + Examples + ======== + + >>> from sympy import sin, cos, Function, diff + >>> from sympy.abc import x, y + >>> f = Function('f') + + >>> diff(sin(x), x) + cos(x) + >>> diff(f(x), x, x, x) + Derivative(f(x), (x, 3)) + >>> diff(f(x), x, 3) + Derivative(f(x), (x, 3)) + >>> diff(sin(x)*cos(y), x, 2, y, 2) + sin(x)*cos(y) + + >>> type(diff(sin(x), x)) + cos + >>> type(diff(sin(x), x, evaluate=False)) + + >>> type(diff(sin(x), x, 0)) + sin + >>> type(diff(sin(x), x, 0, evaluate=False)) + sin + + >>> diff(sin(x)) + cos(x) + >>> diff(sin(x*y)) + Traceback (most recent call last): + ... + ValueError: specify differentiation variables to differentiate sin(x*y) + + Note that ``diff(sin(x))`` syntax is meant only for convenience + in interactive sessions and should be avoided in library code. + + References + ========== + + .. [1] https://reference.wolfram.com/legacy/v5_2/Built-inFunctions/AlgebraicComputation/Calculus/D.html + + See Also + ======== + + Derivative + idiff: computes the derivative implicitly + + """ + if hasattr(f, 'diff'): + return f.diff(*symbols, **kwargs) + kwargs.setdefault('evaluate', True) + return _derivative_dispatch(f, *symbols, **kwargs) + + +def expand(e, deep=True, modulus=None, power_base=True, power_exp=True, + mul=True, log=True, multinomial=True, basic=True, **hints): + r""" + Expand an expression using methods given as hints. + + Explanation + =========== + + Hints evaluated unless explicitly set to False are: ``basic``, ``log``, + ``multinomial``, ``mul``, ``power_base``, and ``power_exp`` The following + hints are supported but not applied unless set to True: ``complex``, + ``func``, and ``trig``. In addition, the following meta-hints are + supported by some or all of the other hints: ``frac``, ``numer``, + ``denom``, ``modulus``, and ``force``. ``deep`` is supported by all + hints. Additionally, subclasses of Expr may define their own hints or + meta-hints. + + The ``basic`` hint is used for any special rewriting of an object that + should be done automatically (along with the other hints like ``mul``) + when expand is called. This is a catch-all hint to handle any sort of + expansion that may not be described by the existing hint names. To use + this hint an object should override the ``_eval_expand_basic`` method. + Objects may also define their own expand methods, which are not run by + default. See the API section below. + + If ``deep`` is set to ``True`` (the default), things like arguments of + functions are recursively expanded. Use ``deep=False`` to only expand on + the top level. + + If the ``force`` hint is used, assumptions about variables will be ignored + in making the expansion. + + Hints + ===== + + These hints are run by default + + mul + --- + + Distributes multiplication over addition: + + >>> from sympy import cos, exp, sin + >>> from sympy.abc import x, y, z + >>> (y*(x + z)).expand(mul=True) + x*y + y*z + + multinomial + ----------- + + Expand (x + y + ...)**n where n is a positive integer. + + >>> ((x + y + z)**2).expand(multinomial=True) + x**2 + 2*x*y + 2*x*z + y**2 + 2*y*z + z**2 + + power_exp + --------- + + Expand addition in exponents into multiplied bases. + + >>> exp(x + y).expand(power_exp=True) + exp(x)*exp(y) + >>> (2**(x + y)).expand(power_exp=True) + 2**x*2**y + + power_base + ---------- + + Split powers of multiplied bases. + + This only happens by default if assumptions allow, or if the + ``force`` meta-hint is used: + + >>> ((x*y)**z).expand(power_base=True) + (x*y)**z + >>> ((x*y)**z).expand(power_base=True, force=True) + x**z*y**z + >>> ((2*y)**z).expand(power_base=True) + 2**z*y**z + + Note that in some cases where this expansion always holds, SymPy performs + it automatically: + + >>> (x*y)**2 + x**2*y**2 + + log + --- + + Pull out power of an argument as a coefficient and split logs products + into sums of logs. + + Note that these only work if the arguments of the log function have the + proper assumptions--the arguments must be positive and the exponents must + be real--or else the ``force`` hint must be True: + + >>> from sympy import log, symbols + >>> log(x**2*y).expand(log=True) + log(x**2*y) + >>> log(x**2*y).expand(log=True, force=True) + 2*log(x) + log(y) + >>> x, y = symbols('x,y', positive=True) + >>> log(x**2*y).expand(log=True) + 2*log(x) + log(y) + + basic + ----- + + This hint is intended primarily as a way for custom subclasses to enable + expansion by default. + + These hints are not run by default: + + complex + ------- + + Split an expression into real and imaginary parts. + + >>> x, y = symbols('x,y') + >>> (x + y).expand(complex=True) + re(x) + re(y) + I*im(x) + I*im(y) + >>> cos(x).expand(complex=True) + -I*sin(re(x))*sinh(im(x)) + cos(re(x))*cosh(im(x)) + + Note that this is just a wrapper around ``as_real_imag()``. Most objects + that wish to redefine ``_eval_expand_complex()`` should consider + redefining ``as_real_imag()`` instead. + + func + ---- + + Expand other functions. + + >>> from sympy import gamma + >>> gamma(x + 1).expand(func=True) + x*gamma(x) + + trig + ---- + + Do trigonometric expansions. + + >>> cos(x + y).expand(trig=True) + -sin(x)*sin(y) + cos(x)*cos(y) + >>> sin(2*x).expand(trig=True) + 2*sin(x)*cos(x) + + Note that the forms of ``sin(n*x)`` and ``cos(n*x)`` in terms of ``sin(x)`` + and ``cos(x)`` are not unique, due to the identity `\sin^2(x) + \cos^2(x) + = 1`. The current implementation uses the form obtained from Chebyshev + polynomials, but this may change. See `this MathWorld article + `_ for more + information. + + Notes + ===== + + - You can shut off unwanted methods:: + + >>> (exp(x + y)*(x + y)).expand() + x*exp(x)*exp(y) + y*exp(x)*exp(y) + >>> (exp(x + y)*(x + y)).expand(power_exp=False) + x*exp(x + y) + y*exp(x + y) + >>> (exp(x + y)*(x + y)).expand(mul=False) + (x + y)*exp(x)*exp(y) + + - Use deep=False to only expand on the top level:: + + >>> exp(x + exp(x + y)).expand() + exp(x)*exp(exp(x)*exp(y)) + >>> exp(x + exp(x + y)).expand(deep=False) + exp(x)*exp(exp(x + y)) + + - Hints are applied in an arbitrary, but consistent order (in the current + implementation, they are applied in alphabetical order, except + multinomial comes before mul, but this may change). Because of this, + some hints may prevent expansion by other hints if they are applied + first. For example, ``mul`` may distribute multiplications and prevent + ``log`` and ``power_base`` from expanding them. Also, if ``mul`` is + applied before ``multinomial`, the expression might not be fully + distributed. The solution is to use the various ``expand_hint`` helper + functions or to use ``hint=False`` to this function to finely control + which hints are applied. Here are some examples:: + + >>> from sympy import expand, expand_mul, expand_power_base + >>> x, y, z = symbols('x,y,z', positive=True) + + >>> expand(log(x*(y + z))) + log(x) + log(y + z) + + Here, we see that ``log`` was applied before ``mul``. To get the mul + expanded form, either of the following will work:: + + >>> expand_mul(log(x*(y + z))) + log(x*y + x*z) + >>> expand(log(x*(y + z)), log=False) + log(x*y + x*z) + + A similar thing can happen with the ``power_base`` hint:: + + >>> expand((x*(y + z))**x) + (x*y + x*z)**x + + To get the ``power_base`` expanded form, either of the following will + work:: + + >>> expand((x*(y + z))**x, mul=False) + x**x*(y + z)**x + >>> expand_power_base((x*(y + z))**x) + x**x*(y + z)**x + + >>> expand((x + y)*y/x) + y + y**2/x + + The parts of a rational expression can be targeted:: + + >>> expand((x + y)*y/x/(x + 1), frac=True) + (x*y + y**2)/(x**2 + x) + >>> expand((x + y)*y/x/(x + 1), numer=True) + (x*y + y**2)/(x*(x + 1)) + >>> expand((x + y)*y/x/(x + 1), denom=True) + y*(x + y)/(x**2 + x) + + - The ``modulus`` meta-hint can be used to reduce the coefficients of an + expression post-expansion:: + + >>> expand((3*x + 1)**2) + 9*x**2 + 6*x + 1 + >>> expand((3*x + 1)**2, modulus=5) + 4*x**2 + x + 1 + + - Either ``expand()`` the function or ``.expand()`` the method can be + used. Both are equivalent:: + + >>> expand((x + 1)**2) + x**2 + 2*x + 1 + >>> ((x + 1)**2).expand() + x**2 + 2*x + 1 + + API + === + + Objects can define their own expand hints by defining + ``_eval_expand_hint()``. The function should take the form:: + + def _eval_expand_hint(self, **hints): + # Only apply the method to the top-level expression + ... + + See also the example below. Objects should define ``_eval_expand_hint()`` + methods only if ``hint`` applies to that specific object. The generic + ``_eval_expand_hint()`` method defined in Expr will handle the no-op case. + + Each hint should be responsible for expanding that hint only. + Furthermore, the expansion should be applied to the top-level expression + only. ``expand()`` takes care of the recursion that happens when + ``deep=True``. + + You should only call ``_eval_expand_hint()`` methods directly if you are + 100% sure that the object has the method, as otherwise you are liable to + get unexpected ``AttributeError``s. Note, again, that you do not need to + recursively apply the hint to args of your object: this is handled + automatically by ``expand()``. ``_eval_expand_hint()`` should + generally not be used at all outside of an ``_eval_expand_hint()`` method. + If you want to apply a specific expansion from within another method, use + the public ``expand()`` function, method, or ``expand_hint()`` functions. + + In order for expand to work, objects must be rebuildable by their args, + i.e., ``obj.func(*obj.args) == obj`` must hold. + + Expand methods are passed ``**hints`` so that expand hints may use + 'metahints'--hints that control how different expand methods are applied. + For example, the ``force=True`` hint described above that causes + ``expand(log=True)`` to ignore assumptions is such a metahint. The + ``deep`` meta-hint is handled exclusively by ``expand()`` and is not + passed to ``_eval_expand_hint()`` methods. + + Note that expansion hints should generally be methods that perform some + kind of 'expansion'. For hints that simply rewrite an expression, use the + .rewrite() API. + + Examples + ======== + + >>> from sympy import Expr, sympify + >>> class MyClass(Expr): + ... def __new__(cls, *args): + ... args = sympify(args) + ... return Expr.__new__(cls, *args) + ... + ... def _eval_expand_double(self, *, force=False, **hints): + ... ''' + ... Doubles the args of MyClass. + ... + ... If there more than four args, doubling is not performed, + ... unless force=True is also used (False by default). + ... ''' + ... if not force and len(self.args) > 4: + ... return self + ... return self.func(*(self.args + self.args)) + ... + >>> a = MyClass(1, 2, MyClass(3, 4)) + >>> a + MyClass(1, 2, MyClass(3, 4)) + >>> a.expand(double=True) + MyClass(1, 2, MyClass(3, 4, 3, 4), 1, 2, MyClass(3, 4, 3, 4)) + >>> a.expand(double=True, deep=False) + MyClass(1, 2, MyClass(3, 4), 1, 2, MyClass(3, 4)) + + >>> b = MyClass(1, 2, 3, 4, 5) + >>> b.expand(double=True) + MyClass(1, 2, 3, 4, 5) + >>> b.expand(double=True, force=True) + MyClass(1, 2, 3, 4, 5, 1, 2, 3, 4, 5) + + See Also + ======== + + expand_log, expand_mul, expand_multinomial, expand_complex, expand_trig, + expand_power_base, expand_power_exp, expand_func, sympy.simplify.hyperexpand.hyperexpand + + """ + # don't modify this; modify the Expr.expand method + hints['power_base'] = power_base + hints['power_exp'] = power_exp + hints['mul'] = mul + hints['log'] = log + hints['multinomial'] = multinomial + hints['basic'] = basic + return sympify(e).expand(deep=deep, modulus=modulus, **hints) + +# This is a special application of two hints + +def _mexpand(expr, recursive=False): + # expand multinomials and then expand products; this may not always + # be sufficient to give a fully expanded expression (see + # test_issue_8247_8354 in test_arit) + if expr is None: + return + was = None + while was != expr: + was, expr = expr, expand_mul(expand_multinomial(expr)) + if not recursive: + break + return expr + + +# These are simple wrappers around single hints. + + +def expand_mul(expr, deep=True): + """ + Wrapper around expand that only uses the mul hint. See the expand + docstring for more information. + + Examples + ======== + + >>> from sympy import symbols, expand_mul, exp, log + >>> x, y = symbols('x,y', positive=True) + >>> expand_mul(exp(x+y)*(x+y)*log(x*y**2)) + x*exp(x + y)*log(x*y**2) + y*exp(x + y)*log(x*y**2) + + """ + return sympify(expr).expand(deep=deep, mul=True, power_exp=False, + power_base=False, basic=False, multinomial=False, log=False) + + +def expand_multinomial(expr, deep=True): + """ + Wrapper around expand that only uses the multinomial hint. See the expand + docstring for more information. + + Examples + ======== + + >>> from sympy import symbols, expand_multinomial, exp + >>> x, y = symbols('x y', positive=True) + >>> expand_multinomial((x + exp(x + 1))**2) + x**2 + 2*x*exp(x + 1) + exp(2*x + 2) + + """ + return sympify(expr).expand(deep=deep, mul=False, power_exp=False, + power_base=False, basic=False, multinomial=True, log=False) + + +def expand_log(expr, deep=True, force=False, factor=False): + """ + Wrapper around expand that only uses the log hint. See the expand + docstring for more information. + + Examples + ======== + + >>> from sympy import symbols, expand_log, exp, log + >>> x, y = symbols('x,y', positive=True) + >>> expand_log(exp(x+y)*(x+y)*log(x*y**2)) + (x + y)*(log(x) + 2*log(y))*exp(x + y) + + """ + from sympy.functions.elementary.exponential import log + from sympy.simplify.radsimp import fraction + if factor is False: + def _handleMul(x): + # look for the simple case of expanded log(b**a)/log(b) -> a in args + n, d = fraction(x) + n = [i for i in n.atoms(log) if i.args[0].is_Integer] + d = [i for i in d.atoms(log) if i.args[0].is_Integer] + if len(n) == 1 and len(d) == 1: + n = n[0] + d = d[0] + from sympy import multiplicity + m = multiplicity(d.args[0], n.args[0]) + if m: + r = m + log(n.args[0]//d.args[0]**m)/d + x = x.subs(n, d*r) + x1 = expand_mul(expand_log(x, deep=deep, force=force, factor=True)) + if x1.count(log) <= x.count(log): + return x1 + return x + + expr = expr.replace( + lambda x: x.is_Mul and all(any(isinstance(i, log) and i.args[0].is_Rational + for i in Mul.make_args(j)) for j in x.as_numer_denom()), + _handleMul) + + return sympify(expr).expand(deep=deep, log=True, mul=False, + power_exp=False, power_base=False, multinomial=False, + basic=False, force=force, factor=factor) + + +def expand_func(expr, deep=True): + """ + Wrapper around expand that only uses the func hint. See the expand + docstring for more information. + + Examples + ======== + + >>> from sympy import expand_func, gamma + >>> from sympy.abc import x + >>> expand_func(gamma(x + 2)) + x*(x + 1)*gamma(x) + + """ + return sympify(expr).expand(deep=deep, func=True, basic=False, + log=False, mul=False, power_exp=False, power_base=False, multinomial=False) + + +def expand_trig(expr, deep=True): + """ + Wrapper around expand that only uses the trig hint. See the expand + docstring for more information. + + Examples + ======== + + >>> from sympy import expand_trig, sin + >>> from sympy.abc import x, y + >>> expand_trig(sin(x+y)*(x+y)) + (x + y)*(sin(x)*cos(y) + sin(y)*cos(x)) + + """ + return sympify(expr).expand(deep=deep, trig=True, basic=False, + log=False, mul=False, power_exp=False, power_base=False, multinomial=False) + + +def expand_complex(expr, deep=True): + """ + Wrapper around expand that only uses the complex hint. See the expand + docstring for more information. + + Examples + ======== + + >>> from sympy import expand_complex, exp, sqrt, I + >>> from sympy.abc import z + >>> expand_complex(exp(z)) + I*exp(re(z))*sin(im(z)) + exp(re(z))*cos(im(z)) + >>> expand_complex(sqrt(I)) + sqrt(2)/2 + sqrt(2)*I/2 + + See Also + ======== + + sympy.core.expr.Expr.as_real_imag + """ + return sympify(expr).expand(deep=deep, complex=True, basic=False, + log=False, mul=False, power_exp=False, power_base=False, multinomial=False) + + +def expand_power_base(expr, deep=True, force=False): + """ + Wrapper around expand that only uses the power_base hint. + + A wrapper to expand(power_base=True) which separates a power with a base + that is a Mul into a product of powers, without performing any other + expansions, provided that assumptions about the power's base and exponent + allow. + + deep=False (default is True) will only apply to the top-level expression. + + force=True (default is False) will cause the expansion to ignore + assumptions about the base and exponent. When False, the expansion will + only happen if the base is non-negative or the exponent is an integer. + + >>> from sympy.abc import x, y, z + >>> from sympy import expand_power_base, sin, cos, exp, Symbol + + >>> (x*y)**2 + x**2*y**2 + + >>> (2*x)**y + (2*x)**y + >>> expand_power_base(_) + 2**y*x**y + + >>> expand_power_base((x*y)**z) + (x*y)**z + >>> expand_power_base((x*y)**z, force=True) + x**z*y**z + >>> expand_power_base(sin((x*y)**z), deep=False) + sin((x*y)**z) + >>> expand_power_base(sin((x*y)**z), force=True) + sin(x**z*y**z) + + >>> expand_power_base((2*sin(x))**y + (2*cos(x))**y) + 2**y*sin(x)**y + 2**y*cos(x)**y + + >>> expand_power_base((2*exp(y))**x) + 2**x*exp(y)**x + + >>> expand_power_base((2*cos(x))**y) + 2**y*cos(x)**y + + Notice that sums are left untouched. If this is not the desired behavior, + apply full ``expand()`` to the expression: + + >>> expand_power_base(((x+y)*z)**2) + z**2*(x + y)**2 + >>> (((x+y)*z)**2).expand() + x**2*z**2 + 2*x*y*z**2 + y**2*z**2 + + >>> expand_power_base((2*y)**(1+z)) + 2**(z + 1)*y**(z + 1) + >>> ((2*y)**(1+z)).expand() + 2*2**z*y**(z + 1) + + The power that is unexpanded can be expanded safely when + ``y != 0``, otherwise different values might be obtained for the expression: + + >>> prev = _ + + If we indicate that ``y`` is positive but then replace it with + a value of 0 after expansion, the expression becomes 0: + + >>> p = Symbol('p', positive=True) + >>> prev.subs(y, p).expand().subs(p, 0) + 0 + + But if ``z = -1`` the expression would not be zero: + + >>> prev.subs(y, 0).subs(z, -1) + 1 + + See Also + ======== + + expand + + """ + return sympify(expr).expand(deep=deep, log=False, mul=False, + power_exp=False, power_base=True, multinomial=False, + basic=False, force=force) + + +def expand_power_exp(expr, deep=True): + """ + Wrapper around expand that only uses the power_exp hint. + + See the expand docstring for more information. + + Examples + ======== + + >>> from sympy import expand_power_exp, Symbol + >>> from sympy.abc import x, y + >>> expand_power_exp(3**(y + 2)) + 9*3**y + >>> expand_power_exp(x**(y + 2)) + x**(y + 2) + + If ``x = 0`` the value of the expression depends on the + value of ``y``; if the expression were expanded the result + would be 0. So expansion is only done if ``x != 0``: + + >>> expand_power_exp(Symbol('x', zero=False)**(y + 2)) + x**2*x**y + """ + return sympify(expr).expand(deep=deep, complex=False, basic=False, + log=False, mul=False, power_exp=True, power_base=False, multinomial=False) + + +def count_ops(expr, visual=False): + """ + Return a representation (integer or expression) of the operations in expr. + + Parameters + ========== + + expr : Expr + If expr is an iterable, the sum of the op counts of the + items will be returned. + + visual : bool, optional + If ``False`` (default) then the sum of the coefficients of the + visual expression will be returned. + If ``True`` then the number of each type of operation is shown + with the core class types (or their virtual equivalent) multiplied by the + number of times they occur. + + Examples + ======== + + >>> from sympy.abc import a, b, x, y + >>> from sympy import sin, count_ops + + Although there is not a SUB object, minus signs are interpreted as + either negations or subtractions: + + >>> (x - y).count_ops(visual=True) + SUB + >>> (-x).count_ops(visual=True) + NEG + + Here, there are two Adds and a Pow: + + >>> (1 + a + b**2).count_ops(visual=True) + 2*ADD + POW + + In the following, an Add, Mul, Pow and two functions: + + >>> (sin(x)*x + sin(x)**2).count_ops(visual=True) + ADD + MUL + POW + 2*SIN + + for a total of 5: + + >>> (sin(x)*x + sin(x)**2).count_ops(visual=False) + 5 + + Note that "what you type" is not always what you get. The expression + 1/x/y is translated by sympy into 1/(x*y) so it gives a DIV and MUL rather + than two DIVs: + + >>> (1/x/y).count_ops(visual=True) + DIV + MUL + + The visual option can be used to demonstrate the difference in + operations for expressions in different forms. Here, the Horner + representation is compared with the expanded form of a polynomial: + + >>> eq=x*(1 + x*(2 + x*(3 + x))) + >>> count_ops(eq.expand(), visual=True) - count_ops(eq, visual=True) + -MUL + 3*POW + + The count_ops function also handles iterables: + + >>> count_ops([x, sin(x), None, True, x + 2], visual=False) + 2 + >>> count_ops([x, sin(x), None, True, x + 2], visual=True) + ADD + SIN + >>> count_ops({x: sin(x), x + 2: y + 1}, visual=True) + 2*ADD + SIN + + """ + from .relational import Relational + from sympy.concrete.summations import Sum + from sympy.integrals.integrals import Integral + from sympy.logic.boolalg import BooleanFunction + from sympy.simplify.radsimp import fraction + + expr = sympify(expr) + if isinstance(expr, Expr) and not expr.is_Relational: + + ops = [] + args = [expr] + NEG = Symbol('NEG') + DIV = Symbol('DIV') + SUB = Symbol('SUB') + ADD = Symbol('ADD') + EXP = Symbol('EXP') + while args: + a = args.pop() + + # if the following fails because the object is + # not Basic type, then the object should be fixed + # since it is the intention that all args of Basic + # should themselves be Basic + if a.is_Rational: + #-1/3 = NEG + DIV + if a is not S.One: + if a.p < 0: + ops.append(NEG) + if a.q != 1: + ops.append(DIV) + continue + elif a.is_Mul or a.is_MatMul: + if _coeff_isneg(a): + ops.append(NEG) + if a.args[0] is S.NegativeOne: + a = a.as_two_terms()[1] + else: + a = -a + n, d = fraction(a) + if n.is_Integer: + ops.append(DIV) + if n < 0: + ops.append(NEG) + args.append(d) + continue # won't be -Mul but could be Add + elif d is not S.One: + if not d.is_Integer: + args.append(d) + ops.append(DIV) + args.append(n) + continue # could be -Mul + elif a.is_Add or a.is_MatAdd: + aargs = list(a.args) + negs = 0 + for i, ai in enumerate(aargs): + if _coeff_isneg(ai): + negs += 1 + args.append(-ai) + if i > 0: + ops.append(SUB) + else: + args.append(ai) + if i > 0: + ops.append(ADD) + if negs == len(aargs): # -x - y = NEG + SUB + ops.append(NEG) + elif _coeff_isneg(aargs[0]): # -x + y = SUB, but already recorded ADD + ops.append(SUB - ADD) + continue + if a.is_Pow and a.exp is S.NegativeOne: + ops.append(DIV) + args.append(a.base) # won't be -Mul but could be Add + continue + if a == S.Exp1: + ops.append(EXP) + continue + if a.is_Pow and a.base == S.Exp1: + ops.append(EXP) + args.append(a.exp) + continue + if a.is_Mul or isinstance(a, LatticeOp): + o = Symbol(a.func.__name__.upper()) + # count the args + ops.append(o*(len(a.args) - 1)) + elif a.args and ( + a.is_Pow or a.is_Function or isinstance(a, (Derivative, Integral, Sum))): + # if it's not in the list above we don't + # consider a.func something to count, e.g. + # Tuple, MatrixSymbol, etc... + if isinstance(a.func, UndefinedFunction): + o = Symbol("FUNC_" + a.func.__name__.upper()) + else: + o = Symbol(a.func.__name__.upper()) + ops.append(o) + + if not a.is_Symbol: + args.extend(a.args) + + elif isinstance(expr, Dict): + ops = [count_ops(k, visual=visual) + + count_ops(v, visual=visual) for k, v in expr.items()] + elif iterable(expr): + ops = [count_ops(i, visual=visual) for i in expr] + elif isinstance(expr, (Relational, BooleanFunction)): + ops = [] + for arg in expr.args: + ops.append(count_ops(arg, visual=True)) + o = Symbol(func_name(expr, short=True).upper()) + ops.append(o) + elif not isinstance(expr, Basic): + ops = [] + else: # it's Basic not isinstance(expr, Expr): + if not isinstance(expr, Basic): + raise TypeError("Invalid type of expr") + else: + ops = [] + args = [expr] + while args: + a = args.pop() + + if a.args: + o = Symbol(type(a).__name__.upper()) + if a.is_Boolean: + ops.append(o*(len(a.args)-1)) + else: + ops.append(o) + args.extend(a.args) + + if not ops: + if visual: + return S.Zero + return 0 + + ops = Add(*ops) + + if visual: + return ops + + if ops.is_Number: + return int(ops) + + return sum(int((a.args or [1])[0]) for a in Add.make_args(ops)) + + +def nfloat(expr, n=15, exponent=False, dkeys=False): + """Make all Rationals in expr Floats except those in exponents + (unless the exponents flag is set to True) and those in undefined + functions. When processing dictionaries, do not modify the keys + unless ``dkeys=True``. + + Examples + ======== + + >>> from sympy import nfloat, cos, pi, sqrt + >>> from sympy.abc import x, y + >>> nfloat(x**4 + x/2 + cos(pi/3) + 1 + sqrt(y)) + x**4 + 0.5*x + sqrt(y) + 1.5 + >>> nfloat(x**4 + sqrt(y), exponent=True) + x**4.0 + y**0.5 + + Container types are not modified: + + >>> type(nfloat((1, 2))) is tuple + True + """ + from sympy.matrices.matrixbase import MatrixBase + + kw = {"n": n, "exponent": exponent, "dkeys": dkeys} + + if isinstance(expr, MatrixBase): + return expr.applyfunc(lambda e: nfloat(e, **kw)) + + # handling of iterable containers + if iterable(expr, exclude=str): + if isinstance(expr, (dict, Dict)): + if dkeys: + args = [tuple((nfloat(i, **kw) for i in a)) + for a in expr.items()] + else: + args = [(k, nfloat(v, **kw)) for k, v in expr.items()] + if isinstance(expr, dict): + return type(expr)(args) + else: + return expr.func(*args) + elif isinstance(expr, Basic): + return expr.func(*[nfloat(a, **kw) for a in expr.args]) + return type(expr)([nfloat(a, **kw) for a in expr]) + + rv = sympify(expr) + + if rv.is_Number: + return Float(rv, n) + elif rv.is_number: + # evalf doesn't always set the precision + rv = rv.n(n) + if rv.is_Number: + rv = Float(rv.n(n), n) + else: + pass # pure_complex(rv) is likely True + return rv + elif rv.is_Atom: + return rv + elif rv.is_Relational: + args_nfloat = (nfloat(arg, **kw) for arg in rv.args) + return rv.func(*args_nfloat) + + + # watch out for RootOf instances that don't like to have + # their exponents replaced with Dummies and also sometimes have + # problems with evaluating at low precision (issue 6393) + from sympy.polys.rootoftools import RootOf + rv = rv.xreplace({ro: ro.n(n) for ro in rv.atoms(RootOf)}) + + from .power import Pow + if not exponent: + reps = [(p, Pow(p.base, Dummy())) for p in rv.atoms(Pow)] + rv = rv.xreplace(dict(reps)) + rv = rv.n(n) + if not exponent: + rv = rv.xreplace({d.exp: p.exp for p, d in reps}) + else: + # Pow._eval_evalf special cases Integer exponents so if + # exponent is suppose to be handled we have to do so here + rv = rv.xreplace(Transform( + lambda x: Pow(x.base, Float(x.exp, n)), + lambda x: x.is_Pow and x.exp.is_Integer)) + + return rv.xreplace(Transform( + lambda x: x.func(*nfloat(x.args, n, exponent)), + lambda x: isinstance(x, Function) and not isinstance(x, AppliedUndef))) + + +from .symbol import Dummy, Symbol diff --git a/venv/lib/python3.11/site-packages/sympy/core/intfunc.py b/venv/lib/python3.11/site-packages/sympy/core/intfunc.py new file mode 100644 index 0000000000000000000000000000000000000000..50cb625dafcc1e795933311780e26423ddc6015a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/intfunc.py @@ -0,0 +1,530 @@ +""" +The routines here were removed from numbers.py, power.py, +digits.py and factor_.py so they could be imported into core +without raising circular import errors. + +Although the name 'intfunc' was chosen to represent functions that +work with integers, it can also be thought of as containing +internal/core functions that are needed by the classes of the core. +""" + +import math +import sys +from functools import lru_cache + +from .sympify import sympify +from .singleton import S +from sympy.external.gmpy import (gcd as number_gcd, lcm as number_lcm, sqrt, + iroot, bit_scan1, gcdext) +from sympy.utilities.misc import as_int, filldedent + + +def num_digits(n, base=10): + """Return the number of digits needed to express n in give base. + + Examples + ======== + + >>> from sympy.core.intfunc import num_digits + >>> num_digits(10) + 2 + >>> num_digits(10, 2) # 1010 -> 4 digits + 4 + >>> num_digits(-100, 16) # -64 -> 2 digits + 2 + + + Parameters + ========== + + n: integer + The number whose digits are counted. + + b: integer + The base in which digits are computed. + + See Also + ======== + sympy.ntheory.digits.digits, sympy.ntheory.digits.count_digits + """ + if base < 0: + raise ValueError('base must be int greater than 1') + if not n: + return 1 + e, t = integer_log(abs(n), base) + return 1 + e + + +def integer_log(n, b): + r""" + Returns ``(e, bool)`` where e is the largest nonnegative integer + such that :math:`|n| \geq |b^e|` and ``bool`` is True if $n = b^e$. + + Examples + ======== + + >>> from sympy import integer_log + >>> integer_log(125, 5) + (3, True) + >>> integer_log(17, 9) + (1, False) + + If the base is positive and the number negative the + return value will always be the same except for 2: + + >>> integer_log(-4, 2) + (2, False) + >>> integer_log(-16, 4) + (0, False) + + When the base is negative, the returned value + will only be True if the parity of the exponent is + correct for the sign of the base: + + >>> integer_log(4, -2) + (2, True) + >>> integer_log(8, -2) + (3, False) + >>> integer_log(-8, -2) + (3, True) + >>> integer_log(-4, -2) + (2, False) + + See Also + ======== + integer_nthroot + sympy.ntheory.primetest.is_square + sympy.ntheory.factor_.multiplicity + sympy.ntheory.factor_.perfect_power + """ + n = as_int(n) + b = as_int(b) + + if b < 0: + e, t = integer_log(abs(n), -b) + # (-2)**3 == -8 + # (-2)**2 = 4 + t = t and e % 2 == (n < 0) + return e, t + if b <= 1: + raise ValueError('base must be 2 or more') + if n < 0: + if b != 2: + return 0, False + e, t = integer_log(-n, b) + return e, False + if n == 0: + raise ValueError('n cannot be 0') + + if n < b: + return 0, n == 1 + if b == 2: + e = n.bit_length() - 1 + return e, trailing(n) == e + t = trailing(b) + if 2**t == b: + e = int(n.bit_length() - 1)//t + n_ = 1 << (t*e) + return e, n_ == n + + d = math.floor(math.log10(n) / math.log10(b)) + n_ = b ** d + while n_ <= n: # this will iterate 0, 1 or 2 times + d += 1 + n_ *= b + return d - (n_ > n), (n_ == n or n_//b == n) + + +def trailing(n): + """Count the number of trailing zero digits in the binary + representation of n, i.e. determine the largest power of 2 + that divides n. + + Examples + ======== + + >>> from sympy import trailing + >>> trailing(128) + 7 + >>> trailing(63) + 0 + + See Also + ======== + sympy.ntheory.factor_.multiplicity + + """ + if not n: + return 0 + return bit_scan1(int(n)) + + +@lru_cache(1024) +def igcd(*args): + """Computes nonnegative integer greatest common divisor. + + Explanation + =========== + + The algorithm is based on the well known Euclid's algorithm [1]_. To + improve speed, ``igcd()`` has its own caching mechanism. + If you do not need the cache mechanism, using ``sympy.external.gmpy.gcd``. + + Examples + ======== + + >>> from sympy import igcd + >>> igcd(2, 4) + 2 + >>> igcd(5, 10, 15) + 5 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Euclidean_algorithm + + """ + if len(args) < 2: + raise TypeError("igcd() takes at least 2 arguments (%s given)" % len(args)) + return int(number_gcd(*map(as_int, args))) + + +igcd2 = math.gcd + + +def igcd_lehmer(a, b): + r"""Computes greatest common divisor of two integers. + + Explanation + =========== + + Euclid's algorithm for the computation of the greatest + common divisor ``gcd(a, b)`` of two (positive) integers + $a$ and $b$ is based on the division identity + $$ a = q \times b + r$$, + where the quotient $q$ and the remainder $r$ are integers + and $0 \le r < b$. Then each common divisor of $a$ and $b$ + divides $r$, and it follows that ``gcd(a, b) == gcd(b, r)``. + The algorithm works by constructing the sequence + r0, r1, r2, ..., where r0 = a, r1 = b, and each rn + is the remainder from the division of the two preceding + elements. + + In Python, ``q = a // b`` and ``r = a % b`` are obtained by the + floor division and the remainder operations, respectively. + These are the most expensive arithmetic operations, especially + for large a and b. + + Lehmer's algorithm [1]_ is based on the observation that the quotients + ``qn = r(n-1) // rn`` are in general small integers even + when a and b are very large. Hence the quotients can be + usually determined from a relatively small number of most + significant bits. + + The efficiency of the algorithm is further enhanced by not + computing each long remainder in Euclid's sequence. The remainders + are linear combinations of a and b with integer coefficients + derived from the quotients. The coefficients can be computed + as far as the quotients can be determined from the chosen + most significant parts of a and b. Only then a new pair of + consecutive remainders is computed and the algorithm starts + anew with this pair. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Lehmer%27s_GCD_algorithm + + """ + a, b = abs(as_int(a)), abs(as_int(b)) + if a < b: + a, b = b, a + + # The algorithm works by using one or two digit division + # whenever possible. The outer loop will replace the + # pair (a, b) with a pair of shorter consecutive elements + # of the Euclidean gcd sequence until a and b + # fit into two Python (long) int digits. + nbits = 2 * sys.int_info.bits_per_digit + + while a.bit_length() > nbits and b != 0: + # Quotients are mostly small integers that can + # be determined from most significant bits. + n = a.bit_length() - nbits + x, y = int(a >> n), int(b >> n) # most significant bits + + # Elements of the Euclidean gcd sequence are linear + # combinations of a and b with integer coefficients. + # Compute the coefficients of consecutive pairs + # a' = A*a + B*b, b' = C*a + D*b + # using small integer arithmetic as far as possible. + A, B, C, D = 1, 0, 0, 1 # initial values + + while True: + # The coefficients alternate in sign while looping. + # The inner loop combines two steps to keep track + # of the signs. + + # At this point we have + # A > 0, B <= 0, C <= 0, D > 0, + # x' = x + B <= x < x" = x + A, + # y' = y + C <= y < y" = y + D, + # and + # x'*N <= a' < x"*N, y'*N <= b' < y"*N, + # where N = 2**n. + + # Now, if y' > 0, and x"//y' and x'//y" agree, + # then their common value is equal to q = a'//b'. + # In addition, + # x'%y" = x' - q*y" < x" - q*y' = x"%y', + # and + # (x'%y")*N < a'%b' < (x"%y')*N. + + # On the other hand, we also have x//y == q, + # and therefore + # x'%y" = x + B - q*(y + D) = x%y + B', + # x"%y' = x + A - q*(y + C) = x%y + A', + # where + # B' = B - q*D < 0, A' = A - q*C > 0. + + if y + C <= 0: + break + q = (x + A) // (y + C) + + # Now x'//y" <= q, and equality holds if + # x' - q*y" = (x - q*y) + (B - q*D) >= 0. + # This is a minor optimization to avoid division. + x_qy, B_qD = x - q * y, B - q * D + if x_qy + B_qD < 0: + break + + # Next step in the Euclidean sequence. + x, y = y, x_qy + A, B, C, D = C, D, A - q * C, B_qD + + # At this point the signs of the coefficients + # change and their roles are interchanged. + # A <= 0, B > 0, C > 0, D < 0, + # x' = x + A <= x < x" = x + B, + # y' = y + D < y < y" = y + C. + + if y + D <= 0: + break + q = (x + B) // (y + D) + x_qy, A_qC = x - q * y, A - q * C + if x_qy + A_qC < 0: + break + + x, y = y, x_qy + A, B, C, D = C, D, A_qC, B - q * D + # Now the conditions on top of the loop + # are again satisfied. + # A > 0, B < 0, C < 0, D > 0. + + if B == 0: + # This can only happen when y == 0 in the beginning + # and the inner loop does nothing. + # Long division is forced. + a, b = b, a % b + continue + + # Compute new long arguments using the coefficients. + a, b = A * a + B * b, C * a + D * b + + # Small divisors. Finish with the standard algorithm. + while b: + a, b = b, a % b + + return a + + +def ilcm(*args): + """Computes integer least common multiple. + + Examples + ======== + + >>> from sympy import ilcm + >>> ilcm(5, 10) + 10 + >>> ilcm(7, 3) + 21 + >>> ilcm(5, 10, 15) + 30 + + """ + if len(args) < 2: + raise TypeError("ilcm() takes at least 2 arguments (%s given)" % len(args)) + return int(number_lcm(*map(as_int, args))) + + +def igcdex(a, b): + """Returns x, y, g such that g = x*a + y*b = gcd(a, b). + + Examples + ======== + + >>> from sympy.core.intfunc import igcdex + >>> igcdex(2, 3) + (-1, 1, 1) + >>> igcdex(10, 12) + (-1, 1, 2) + + >>> x, y, g = igcdex(100, 2004) + >>> x, y, g + (-20, 1, 4) + >>> x*100 + y*2004 + 4 + + """ + g, x, y = gcdext(int(a), int(b)) + return x, y, g + + +def mod_inverse(a, m): + r""" + Return the number $c$ such that, $a \times c = 1 \pmod{m}$ + where $c$ has the same sign as $m$. If no such value exists, + a ValueError is raised. + + Examples + ======== + + >>> from sympy import mod_inverse, S + + Suppose we wish to find multiplicative inverse $x$ of + 3 modulo 11. This is the same as finding $x$ such + that $3x = 1 \pmod{11}$. One value of x that satisfies + this congruence is 4. Because $3 \times 4 = 12$ and $12 = 1 \pmod{11}$. + This is the value returned by ``mod_inverse``: + + >>> mod_inverse(3, 11) + 4 + >>> mod_inverse(-3, 11) + 7 + + When there is a common factor between the numerators of + `a` and `m` the inverse does not exist: + + >>> mod_inverse(2, 4) + Traceback (most recent call last): + ... + ValueError: inverse of 2 mod 4 does not exist + + >>> mod_inverse(S(2)/7, S(5)/2) + 7/2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Modular_multiplicative_inverse + .. [2] https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm + """ + c = None + try: + a, m = as_int(a), as_int(m) + if m != 1 and m != -1: + x, _, g = igcdex(a, m) + if g == 1: + c = x % m + except ValueError: + a, m = sympify(a), sympify(m) + if not (a.is_number and m.is_number): + raise TypeError( + filldedent( + """ + Expected numbers for arguments; symbolic `mod_inverse` + is not implemented + but symbolic expressions can be handled with the + similar function, + sympy.polys.polytools.invert""" + ) + ) + big = m > 1 + if big not in (S.true, S.false): + raise ValueError("m > 1 did not evaluate; try to simplify %s" % m) + elif big: + c = 1 / a + if c is None: + raise ValueError("inverse of %s (mod %s) does not exist" % (a, m)) + return c + + +def isqrt(n): + r""" Return the largest integer less than or equal to `\sqrt{n}`. + + Parameters + ========== + + n : non-negative integer + + Returns + ======= + + int : `\left\lfloor\sqrt{n}\right\rfloor` + + Raises + ====== + + ValueError + If n is negative. + TypeError + If n is of a type that cannot be compared to ``int``. + Therefore, a TypeError is raised for ``str``, but not for ``float``. + + Examples + ======== + + >>> from sympy.core.intfunc import isqrt + >>> isqrt(0) + 0 + >>> isqrt(9) + 3 + >>> isqrt(10) + 3 + >>> isqrt("30") + Traceback (most recent call last): + ... + TypeError: '<' not supported between instances of 'str' and 'int' + >>> from sympy.core.numbers import Rational + >>> isqrt(Rational(-1, 2)) + Traceback (most recent call last): + ... + ValueError: n must be nonnegative + + """ + if n < 0: + raise ValueError("n must be nonnegative") + return int(sqrt(int(n))) + + +def integer_nthroot(y, n): + """ + Return a tuple containing x = floor(y**(1/n)) + and a boolean indicating whether the result is exact (that is, + whether x**n == y). + + Examples + ======== + + >>> from sympy import integer_nthroot + >>> integer_nthroot(16, 2) + (4, True) + >>> integer_nthroot(26, 2) + (5, False) + + To simply determine if a number is a perfect square, the is_square + function should be used: + + >>> from sympy.ntheory.primetest import is_square + >>> is_square(26) + False + + See Also + ======== + sympy.ntheory.primetest.is_square + integer_log + """ + x, b = iroot(as_int(y), as_int(n)) + return int(x), b diff --git a/venv/lib/python3.11/site-packages/sympy/core/kind.py b/venv/lib/python3.11/site-packages/sympy/core/kind.py new file mode 100644 index 0000000000000000000000000000000000000000..83c5929eda14114659f2a5a72eb2d8b91a560f0e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/kind.py @@ -0,0 +1,388 @@ +""" +Module to efficiently partition SymPy objects. + +This system is introduced because class of SymPy object does not always +represent the mathematical classification of the entity. For example, +``Integral(1, x)`` and ``Integral(Matrix([1,2]), x)`` are both instance +of ``Integral`` class. However the former is number and the latter is +matrix. + +One way to resolve this is defining subclass for each mathematical type, +such as ``MatAdd`` for the addition between matrices. Basic algebraic +operation such as addition or multiplication take this approach, but +defining every class for every mathematical object is not scalable. + +Therefore, we define the "kind" of the object and let the expression +infer the kind of itself from its arguments. Function and class can +filter the arguments by their kind, and behave differently according to +the type of itself. + +This module defines basic kinds for core objects. Other kinds such as +``ArrayKind`` or ``MatrixKind`` can be found in corresponding modules. + +.. notes:: + This approach is experimental, and can be replaced or deleted in the future. + See https://github.com/sympy/sympy/pull/20549. +""" + +from collections import defaultdict + +from .cache import cacheit +from sympy.multipledispatch.dispatcher import (Dispatcher, + ambiguity_warn, ambiguity_register_error_ignore_dup, + str_signature, RaiseNotImplementedError) + + +class KindMeta(type): + """ + Metaclass for ``Kind``. + + Assigns empty ``dict`` as class attribute ``_inst`` for every class, + in order to endow singleton-like behavior. + """ + def __new__(cls, clsname, bases, dct): + dct['_inst'] = {} + return super().__new__(cls, clsname, bases, dct) + + +class Kind(object, metaclass=KindMeta): + """ + Base class for kinds. + + Kind of the object represents the mathematical classification that + the entity falls into. It is expected that functions and classes + recognize and filter the argument by its kind. + + Kind of every object must be carefully selected so that it shows the + intention of design. Expressions may have different kind according + to the kind of its arguments. For example, arguments of ``Add`` + must have common kind since addition is group operator, and the + resulting ``Add()`` has the same kind. + + For the performance, each kind is as broad as possible and is not + based on set theory. For example, ``NumberKind`` includes not only + complex number but expression containing ``S.Infinity`` or ``S.NaN`` + which are not strictly number. + + Kind may have arguments as parameter. For example, ``MatrixKind()`` + may be constructed with one element which represents the kind of its + elements. + + ``Kind`` behaves in singleton-like fashion. Same signature will + return the same object. + + """ + def __new__(cls, *args): + if args in cls._inst: + inst = cls._inst[args] + else: + inst = super().__new__(cls) + cls._inst[args] = inst + return inst + + +class _UndefinedKind(Kind): + """ + Default kind for all SymPy object. If the kind is not defined for + the object, or if the object cannot infer the kind from its + arguments, this will be returned. + + Examples + ======== + + >>> from sympy import Expr + >>> Expr().kind + UndefinedKind + """ + def __new__(cls): + return super().__new__(cls) + + def __repr__(self): + return "UndefinedKind" + +UndefinedKind = _UndefinedKind() + + +class _NumberKind(Kind): + """ + Kind for all numeric object. + + This kind represents every number, including complex numbers, + infinity and ``S.NaN``. Other objects such as quaternions do not + have this kind. + + Most ``Expr`` are initially designed to represent the number, so + this will be the most common kind in SymPy core. For example + ``Symbol()``, which represents a scalar, has this kind as long as it + is commutative. + + Numbers form a field. Any operation between number-kind objects will + result this kind as well. + + Examples + ======== + + >>> from sympy import S, oo, Symbol + >>> S.One.kind + NumberKind + >>> (-oo).kind + NumberKind + >>> S.NaN.kind + NumberKind + + Commutative symbol are treated as number. + + >>> x = Symbol('x') + >>> x.kind + NumberKind + >>> Symbol('y', commutative=False).kind + UndefinedKind + + Operation between numbers results number. + + >>> (x+1).kind + NumberKind + + See Also + ======== + + sympy.core.expr.Expr.is_Number : check if the object is strictly + subclass of ``Number`` class. + + sympy.core.expr.Expr.is_number : check if the object is number + without any free symbol. + + """ + def __new__(cls): + return super().__new__(cls) + + def __repr__(self): + return "NumberKind" + +NumberKind = _NumberKind() + + +class _BooleanKind(Kind): + """ + Kind for boolean objects. + + SymPy's ``S.true``, ``S.false``, and built-in ``True`` and ``False`` + have this kind. Boolean number ``1`` and ``0`` are not relevant. + + Examples + ======== + + >>> from sympy import S, Q + >>> S.true.kind + BooleanKind + >>> Q.even(3).kind + BooleanKind + """ + def __new__(cls): + return super().__new__(cls) + + def __repr__(self): + return "BooleanKind" + +BooleanKind = _BooleanKind() + + +class KindDispatcher: + """ + Dispatcher to select a kind from multiple kinds by binary dispatching. + + .. notes:: + This approach is experimental, and can be replaced or deleted in + the future. + + Explanation + =========== + + SymPy object's :obj:`sympy.core.kind.Kind()` vaguely represents the + algebraic structure where the object belongs to. Therefore, with + given operation, we can always find a dominating kind among the + different kinds. This class selects the kind by recursive binary + dispatching. If the result cannot be determined, ``UndefinedKind`` + is returned. + + Examples + ======== + + Multiplication between numbers return number. + + >>> from sympy import NumberKind, Mul + >>> Mul._kind_dispatcher(NumberKind, NumberKind) + NumberKind + + Multiplication between number and unknown-kind object returns unknown kind. + + >>> from sympy import UndefinedKind + >>> Mul._kind_dispatcher(NumberKind, UndefinedKind) + UndefinedKind + + Any number and order of kinds is allowed. + + >>> Mul._kind_dispatcher(UndefinedKind, NumberKind) + UndefinedKind + >>> Mul._kind_dispatcher(NumberKind, UndefinedKind, NumberKind) + UndefinedKind + + Since matrix forms a vector space over scalar field, multiplication + between matrix with numeric element and number returns matrix with + numeric element. + + >>> from sympy.matrices import MatrixKind + >>> Mul._kind_dispatcher(MatrixKind(NumberKind), NumberKind) + MatrixKind(NumberKind) + + If a matrix with number element and another matrix with unknown-kind + element are multiplied, we know that the result is matrix but the + kind of its elements is unknown. + + >>> Mul._kind_dispatcher(MatrixKind(NumberKind), MatrixKind(UndefinedKind)) + MatrixKind(UndefinedKind) + + Parameters + ========== + + name : str + + commutative : bool, optional + If True, binary dispatch will be automatically registered in + reversed order as well. + + doc : str, optional + + """ + def __init__(self, name, commutative=False, doc=None): + self.name = name + self.doc = doc + self.commutative = commutative + self._dispatcher = Dispatcher(name) + + def __repr__(self): + return "" % self.name + + def register(self, *types, **kwargs): + """ + Register the binary dispatcher for two kind classes. + + If *self.commutative* is ``True``, signature in reversed order is + automatically registered as well. + """ + on_ambiguity = kwargs.pop("on_ambiguity", None) + if not on_ambiguity: + if self.commutative: + on_ambiguity = ambiguity_register_error_ignore_dup + else: + on_ambiguity = ambiguity_warn + kwargs.update(on_ambiguity=on_ambiguity) + + if not len(types) == 2: + raise RuntimeError( + "Only binary dispatch is supported, but got %s types: <%s>." % ( + len(types), str_signature(types) + )) + + def _(func): + self._dispatcher.add(types, func, **kwargs) + if self.commutative: + self._dispatcher.add(tuple(reversed(types)), func, **kwargs) + return _ + + def __call__(self, *args, **kwargs): + if self.commutative: + kinds = frozenset(args) + else: + kinds = [] + prev = None + for a in args: + if prev is not a: + kinds.append(a) + prev = a + return self.dispatch_kinds(kinds, **kwargs) + + @cacheit + def dispatch_kinds(self, kinds, **kwargs): + # Quick exit for the case where all kinds are same + if len(kinds) == 1: + result, = kinds + if not isinstance(result, Kind): + raise RuntimeError("%s is not a kind." % result) + return result + + for i,kind in enumerate(kinds): + if not isinstance(kind, Kind): + raise RuntimeError("%s is not a kind." % kind) + + if i == 0: + result = kind + else: + prev_kind = result + + t1, t2 = type(prev_kind), type(kind) + k1, k2 = prev_kind, kind + func = self._dispatcher.dispatch(t1, t2) + if func is None and self.commutative: + # try reversed order + func = self._dispatcher.dispatch(t2, t1) + k1, k2 = k2, k1 + if func is None: + # unregistered kind relation + result = UndefinedKind + else: + result = func(k1, k2) + if not isinstance(result, Kind): + raise RuntimeError( + "Dispatcher for {!r} and {!r} must return a Kind, but got {!r}".format( + prev_kind, kind, result + )) + + return result + + @property + def __doc__(self): + docs = [ + "Kind dispatcher : %s" % self.name, + "Note that support for this is experimental. See the docs for :class:`KindDispatcher` for details" + ] + + if self.doc: + docs.append(self.doc) + + s = "Registered kind classes\n" + s += '=' * len(s) + docs.append(s) + + amb_sigs = [] + + typ_sigs = defaultdict(list) + for sigs in self._dispatcher.ordering[::-1]: + key = self._dispatcher.funcs[sigs] + typ_sigs[key].append(sigs) + + for func, sigs in typ_sigs.items(): + + sigs_str = ', '.join('<%s>' % str_signature(sig) for sig in sigs) + + if isinstance(func, RaiseNotImplementedError): + amb_sigs.append(sigs_str) + continue + + s = 'Inputs: %s\n' % sigs_str + s += '-' * len(s) + '\n' + if func.__doc__: + s += func.__doc__.strip() + else: + s += func.__name__ + docs.append(s) + + if amb_sigs: + s = "Ambiguous kind classes\n" + s += '=' * len(s) + docs.append(s) + + s = '\n'.join(amb_sigs) + docs.append(s) + + return '\n\n'.join(docs) diff --git a/venv/lib/python3.11/site-packages/sympy/core/logic.py b/venv/lib/python3.11/site-packages/sympy/core/logic.py new file mode 100644 index 0000000000000000000000000000000000000000..1c318063049a4657952c8ca84e0f0fdeef62a207 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/logic.py @@ -0,0 +1,425 @@ +"""Logic expressions handling + +NOTE +---- + +at present this is mainly needed for facts.py, feel free however to improve +this stuff for general purpose. +""" + +from __future__ import annotations +from typing import Optional + +# Type of a fuzzy bool +FuzzyBool = Optional[bool] + + +def _torf(args): + """Return True if all args are True, False if they + are all False, else None. + + >>> from sympy.core.logic import _torf + >>> _torf((True, True)) + True + >>> _torf((False, False)) + False + >>> _torf((True, False)) + """ + sawT = sawF = False + for a in args: + if a is True: + if sawF: + return + sawT = True + elif a is False: + if sawT: + return + sawF = True + else: + return + return sawT + + +def _fuzzy_group(args, quick_exit=False): + """Return True if all args are True, None if there is any None else False + unless ``quick_exit`` is True (then return None as soon as a second False + is seen. + + ``_fuzzy_group`` is like ``fuzzy_and`` except that it is more + conservative in returning a False, waiting to make sure that all + arguments are True or False and returning None if any arguments are + None. It also has the capability of permiting only a single False and + returning None if more than one is seen. For example, the presence of a + single transcendental amongst rationals would indicate that the group is + no longer rational; but a second transcendental in the group would make the + determination impossible. + + + Examples + ======== + + >>> from sympy.core.logic import _fuzzy_group + + By default, multiple Falses mean the group is broken: + + >>> _fuzzy_group([False, False, True]) + False + + If multiple Falses mean the group status is unknown then set + `quick_exit` to True so None can be returned when the 2nd False is seen: + + >>> _fuzzy_group([False, False, True], quick_exit=True) + + But if only a single False is seen then the group is known to + be broken: + + >>> _fuzzy_group([False, True, True], quick_exit=True) + False + + """ + saw_other = False + for a in args: + if a is True: + continue + if a is None: + return + if quick_exit and saw_other: + return + saw_other = True + return not saw_other + + +def fuzzy_bool(x): + """Return True, False or None according to x. + + Whereas bool(x) returns True or False, fuzzy_bool allows + for the None value and non-false values (which become None), too. + + Examples + ======== + + >>> from sympy.core.logic import fuzzy_bool + >>> from sympy.abc import x + >>> fuzzy_bool(x), fuzzy_bool(None) + (None, None) + >>> bool(x), bool(None) + (True, False) + + """ + if x is None: + return None + if x in (True, False): + return bool(x) + + +def fuzzy_and(args): + """Return True (all True), False (any False) or None. + + Examples + ======== + + >>> from sympy.core.logic import fuzzy_and + >>> from sympy import Dummy + + If you had a list of objects to test the commutivity of + and you want the fuzzy_and logic applied, passing an + iterator will allow the commutativity to only be computed + as many times as necessary. With this list, False can be + returned after analyzing the first symbol: + + >>> syms = [Dummy(commutative=False), Dummy()] + >>> fuzzy_and(s.is_commutative for s in syms) + False + + That False would require less work than if a list of pre-computed + items was sent: + + >>> fuzzy_and([s.is_commutative for s in syms]) + False + """ + + rv = True + for ai in args: + ai = fuzzy_bool(ai) + if ai is False: + return False + if rv: # this will stop updating if a None is ever trapped + rv = ai + return rv + + +def fuzzy_not(v): + """ + Not in fuzzy logic + + Return None if `v` is None else `not v`. + + Examples + ======== + + >>> from sympy.core.logic import fuzzy_not + >>> fuzzy_not(True) + False + >>> fuzzy_not(None) + >>> fuzzy_not(False) + True + + """ + if v is None: + return v + else: + return not v + + +def fuzzy_or(args): + """ + Or in fuzzy logic. Returns True (any True), False (all False), or None + + See the docstrings of fuzzy_and and fuzzy_not for more info. fuzzy_or is + related to the two by the standard De Morgan's law. + + >>> from sympy.core.logic import fuzzy_or + >>> fuzzy_or([True, False]) + True + >>> fuzzy_or([True, None]) + True + >>> fuzzy_or([False, False]) + False + >>> print(fuzzy_or([False, None])) + None + + """ + rv = False + for ai in args: + ai = fuzzy_bool(ai) + if ai is True: + return True + if rv is False: # this will stop updating if a None is ever trapped + rv = ai + return rv + + +def fuzzy_xor(args): + """Return None if any element of args is not True or False, else + True (if there are an odd number of True elements), else False.""" + t = 0 + for a in args: + ai = fuzzy_bool(a) + if ai: + t += 1 + elif ai is None: + return + return t % 2 == 1 + + +def fuzzy_nand(args): + """Return False if all args are True, True if they are all False, + else None.""" + return fuzzy_not(fuzzy_and(args)) + + +class Logic: + """Logical expression""" + # {} 'op' -> LogicClass + op_2class: dict[str, type[Logic]] = {} + + def __new__(cls, *args): + obj = object.__new__(cls) + obj.args = args + return obj + + def __getnewargs__(self): + return self.args + + def __hash__(self): + return hash((type(self).__name__,) + tuple(self.args)) + + def __eq__(a, b): + if not isinstance(b, type(a)): + return False + else: + return a.args == b.args + + def __ne__(a, b): + if not isinstance(b, type(a)): + return True + else: + return a.args != b.args + + def __lt__(self, other): + if self.__cmp__(other) == -1: + return True + return False + + def __cmp__(self, other): + if type(self) is not type(other): + a = str(type(self)) + b = str(type(other)) + else: + a = self.args + b = other.args + return (a > b) - (a < b) + + def __str__(self): + return '%s(%s)' % (self.__class__.__name__, + ', '.join(str(a) for a in self.args)) + + __repr__ = __str__ + + @staticmethod + def fromstring(text): + """Logic from string with space around & and | but none after !. + + e.g. + + !a & b | c + """ + lexpr = None # current logical expression + schedop = None # scheduled operation + for term in text.split(): + # operation symbol + if term in '&|': + if schedop is not None: + raise ValueError( + 'double op forbidden: "%s %s"' % (term, schedop)) + if lexpr is None: + raise ValueError( + '%s cannot be in the beginning of expression' % term) + schedop = term + continue + if '&' in term or '|' in term: + raise ValueError('& and | must have space around them') + if term[0] == '!': + if len(term) == 1: + raise ValueError('do not include space after "!"') + term = Not(term[1:]) + + # already scheduled operation, e.g. '&' + if schedop: + lexpr = Logic.op_2class[schedop](lexpr, term) + schedop = None + continue + + # this should be atom + if lexpr is not None: + raise ValueError( + 'missing op between "%s" and "%s"' % (lexpr, term)) + + lexpr = term + + # let's check that we ended up in correct state + if schedop is not None: + raise ValueError('premature end-of-expression in "%s"' % text) + if lexpr is None: + raise ValueError('"%s" is empty' % text) + + # everything looks good now + return lexpr + + +class AndOr_Base(Logic): + + def __new__(cls, *args): + bargs = [] + for a in args: + if a == cls.op_x_notx: + return a + elif a == (not cls.op_x_notx): + continue # skip this argument + bargs.append(a) + + args = sorted(set(cls.flatten(bargs)), key=hash) + + for a in args: + if Not(a) in args: + return cls.op_x_notx + + if len(args) == 1: + return args.pop() + elif len(args) == 0: + return not cls.op_x_notx + + return Logic.__new__(cls, *args) + + @classmethod + def flatten(cls, args): + # quick-n-dirty flattening for And and Or + args_queue = list(args) + res = [] + + while True: + try: + arg = args_queue.pop(0) + except IndexError: + break + if isinstance(arg, Logic): + if isinstance(arg, cls): + args_queue.extend(arg.args) + continue + res.append(arg) + + args = tuple(res) + return args + + +class And(AndOr_Base): + op_x_notx = False + + def _eval_propagate_not(self): + # !(a&b&c ...) == !a | !b | !c ... + return Or(*[Not(a) for a in self.args]) + + # (a|b|...) & c == (a&c) | (b&c) | ... + def expand(self): + + # first locate Or + for i, arg in enumerate(self.args): + if isinstance(arg, Or): + arest = self.args[:i] + self.args[i + 1:] + + orterms = [And(*(arest + (a,))) for a in arg.args] + for j in range(len(orterms)): + if isinstance(orterms[j], Logic): + orterms[j] = orterms[j].expand() + + res = Or(*orterms) + return res + + return self + + +class Or(AndOr_Base): + op_x_notx = True + + def _eval_propagate_not(self): + # !(a|b|c ...) == !a & !b & !c ... + return And(*[Not(a) for a in self.args]) + + +class Not(Logic): + + def __new__(cls, arg): + if isinstance(arg, str): + return Logic.__new__(cls, arg) + + elif isinstance(arg, bool): + return not arg + elif isinstance(arg, Not): + return arg.args[0] + + elif isinstance(arg, Logic): + # XXX this is a hack to expand right from the beginning + arg = arg._eval_propagate_not() + return arg + + else: + raise ValueError('Not: unknown argument %r' % (arg,)) + + @property + def arg(self): + return self.args[0] + + +Logic.op_2class['&'] = And +Logic.op_2class['|'] = Or +Logic.op_2class['!'] = Not diff --git a/venv/lib/python3.11/site-packages/sympy/core/mod.py b/venv/lib/python3.11/site-packages/sympy/core/mod.py new file mode 100644 index 0000000000000000000000000000000000000000..8be0c56e497eb5ed0041801488044b50f907962c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/mod.py @@ -0,0 +1,260 @@ +from .add import Add +from .exprtools import gcd_terms +from .function import DefinedFunction +from .kind import NumberKind +from .logic import fuzzy_and, fuzzy_not +from .mul import Mul +from .numbers import equal_valued +from .relational import is_le, is_lt, is_ge, is_gt +from .singleton import S + + +class Mod(DefinedFunction): + """Represents a modulo operation on symbolic expressions. + + Parameters + ========== + + p : Expr + Dividend. + + q : Expr + Divisor. + + Notes + ===== + + The convention used is the same as Python's: the remainder always has the + same sign as the divisor. + + Many objects can be evaluated modulo ``n`` much faster than they can be + evaluated directly (or at all). For this, ``evaluate=False`` is + necessary to prevent eager evaluation: + + >>> from sympy import binomial, factorial, Mod, Pow + >>> Mod(Pow(2, 10**16, evaluate=False), 97) + 61 + >>> Mod(factorial(10**9, evaluate=False), 10**9 + 9) + 712524808 + >>> Mod(binomial(10**18, 10**12, evaluate=False), (10**5 + 3)**2) + 3744312326 + + Examples + ======== + + >>> from sympy.abc import x, y + >>> x**2 % y + Mod(x**2, y) + >>> _.subs({x: 5, y: 6}) + 1 + + """ + + kind = NumberKind + + @classmethod + def eval(cls, p, q): + def number_eval(p, q): + """Try to return p % q if both are numbers or +/-p is known + to be less than or equal q. + """ + + if q.is_zero: + raise ZeroDivisionError("Modulo by zero") + if p is S.NaN or q is S.NaN or p.is_finite is False or q.is_finite is False: + return S.NaN + if p is S.Zero or p in (q, -q) or (p.is_integer and q == 1): + return S.Zero + + if q.is_Number: + if p.is_Number: + return p%q + if q == 2: + if p.is_even: + return S.Zero + elif p.is_odd: + return S.One + + if hasattr(p, '_eval_Mod'): + rv = getattr(p, '_eval_Mod')(q) + if rv is not None: + return rv + + # by ratio + r = p/q + if r.is_integer: + return S.Zero + try: + d = int(r) + except TypeError: + pass + else: + if isinstance(d, int): + rv = p - d*q + if (rv*q < 0) == True: + rv += q + return rv + + # by difference + # -2|q| < p < 2|q| + if q.is_positive: + comp1, comp2 = is_le, is_lt + elif q.is_negative: + comp1, comp2 = is_ge, is_gt + else: + return + ls = -2*q + r = p - q + for _ in range(4): + if not comp1(ls, p): + return + if comp2(r, ls): + return p - ls + ls += q + + rv = number_eval(p, q) + if rv is not None: + return rv + + # denest + if isinstance(p, cls): + qinner = p.args[1] + if qinner % q == 0: + return cls(p.args[0], q) + elif (qinner*(q - qinner)).is_nonnegative: + # |qinner| < |q| and have same sign + return p + elif isinstance(-p, cls): + qinner = (-p).args[1] + if qinner % q == 0: + return cls(-(-p).args[0], q) + elif (qinner*(q + qinner)).is_nonpositive: + # |qinner| < |q| and have different sign + return p + elif isinstance(p, Add): + # separating into modulus and non modulus + both_l = non_mod_l, mod_l = [], [] + for arg in p.args: + both_l[isinstance(arg, cls)].append(arg) + # if q same for all + if mod_l and all(inner.args[1] == q for inner in mod_l): + net = Add(*non_mod_l) + Add(*[i.args[0] for i in mod_l]) + return cls(net, q) + + elif isinstance(p, Mul): + # separating into modulus and non modulus + both_l = non_mod_l, mod_l = [], [] + for arg in p.args: + both_l[isinstance(arg, cls)].append(arg) + + if mod_l and all(inner.args[1] == q for inner in mod_l) and all(t.is_integer for t in p.args) and q.is_integer: + # finding distributive term + non_mod_l = [cls(x, q) for x in non_mod_l] + mod = [] + non_mod = [] + for j in non_mod_l: + if isinstance(j, cls): + mod.append(j.args[0]) + else: + non_mod.append(j) + prod_mod = Mul(*mod) + prod_non_mod = Mul(*non_mod) + prod_mod1 = Mul(*[i.args[0] for i in mod_l]) + net = prod_mod1*prod_mod + return prod_non_mod*cls(net, q) + + if q.is_Integer and q is not S.One: + if all(t.is_integer for t in p.args): + non_mod_l = [i % q if i.is_Integer else i for i in p.args] + if any(iq is S.Zero for iq in non_mod_l): + return S.Zero + + p = Mul(*(non_mod_l + mod_l)) + + # XXX other possibilities? + + from sympy.polys.polyerrors import PolynomialError + from sympy.polys.polytools import gcd + + # extract gcd; any further simplification should be done by the user + try: + G = gcd(p, q) + if not equal_valued(G, 1): + p, q = [gcd_terms(i/G, clear=False, fraction=False) + for i in (p, q)] + except PolynomialError: # issue 21373 + G = S.One + pwas, qwas = p, q + + # simplify terms + # (x + y + 2) % x -> Mod(y + 2, x) + if p.is_Add: + args = [] + for i in p.args: + a = cls(i, q) + if a.count(cls) > i.count(cls): + args.append(i) + else: + args.append(a) + if args != list(p.args): + p = Add(*args) + + else: + # handle coefficients if they are not Rational + # since those are not handled by factor_terms + # e.g. Mod(.6*x, .3*y) -> 0.3*Mod(2*x, y) + cp, p = p.as_coeff_Mul() + cq, q = q.as_coeff_Mul() + ok = False + if not cp.is_Rational or not cq.is_Rational: + r = cp % cq + if equal_valued(r, 0): + G *= cq + p *= int(cp/cq) + ok = True + if not ok: + p = cp*p + q = cq*q + + # simple -1 extraction + if p.could_extract_minus_sign() and q.could_extract_minus_sign(): + G, p, q = [-i for i in (G, p, q)] + + # check again to see if p and q can now be handled as numbers + rv = number_eval(p, q) + if rv is not None: + return rv*G + + # put 1.0 from G on inside + if G.is_Float and equal_valued(G, 1): + p *= G + return cls(p, q, evaluate=False) + elif G.is_Mul and G.args[0].is_Float and equal_valued(G.args[0], 1): + p = G.args[0]*p + G = Mul._from_args(G.args[1:]) + return G*cls(p, q, evaluate=(p, q) != (pwas, qwas)) + + def _eval_is_integer(self): + p, q = self.args + if fuzzy_and([p.is_integer, q.is_integer, fuzzy_not(q.is_zero)]): + return True + + def _eval_is_nonnegative(self): + if self.args[1].is_positive: + return True + + def _eval_is_nonpositive(self): + if self.args[1].is_negative: + return True + + def _eval_rewrite_as_floor(self, a, b, **kwargs): + from sympy.functions.elementary.integers import floor + return a - b*floor(a/b) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.functions.elementary.integers import floor + return self.rewrite(floor)._eval_as_leading_term(x, logx=logx, cdir=cdir) + + def _eval_nseries(self, x, n, logx, cdir=0): + from sympy.functions.elementary.integers import floor + return self.rewrite(floor)._eval_nseries(x, n, logx=logx, cdir=cdir) diff --git a/venv/lib/python3.11/site-packages/sympy/core/mul.py b/venv/lib/python3.11/site-packages/sympy/core/mul.py new file mode 100644 index 0000000000000000000000000000000000000000..fd83c8610a76db4e7bc7a2a71b98e437bd00a28e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/mul.py @@ -0,0 +1,2214 @@ +from __future__ import annotations +from typing import TYPE_CHECKING, ClassVar + +from collections import defaultdict +from functools import reduce +from itertools import product +import operator + +from .sympify import sympify +from .basic import Basic, _args_sortkey +from .singleton import S +from .operations import AssocOp, AssocOpDispatcher +from .cache import cacheit +from .intfunc import integer_nthroot, trailing +from .logic import fuzzy_not, _fuzzy_group +from .expr import Expr +from .parameters import global_parameters +from .kind import KindDispatcher +from .traversal import bottom_up +from sympy.utilities.iterables import sift + + +# internal marker to indicate: +# "there are still non-commutative objects -- don't forget to process them" +class NC_Marker: + is_Order = False + is_Mul = False + is_Number = False + is_Poly = False + + is_commutative = False + + +def _mulsort(args): + # in-place sorting of args + args.sort(key=_args_sortkey) + + +def _unevaluated_Mul(*args): + """Return a well-formed unevaluated Mul: Numbers are collected and + put in slot 0, any arguments that are Muls will be flattened, and args + are sorted. Use this when args have changed but you still want to return + an unevaluated Mul. + + Examples + ======== + + >>> from sympy.core.mul import _unevaluated_Mul as uMul + >>> from sympy import S, sqrt, Mul + >>> from sympy.abc import x + >>> a = uMul(*[S(3.0), x, S(2)]) + >>> a.args[0] + 6.00000000000000 + >>> a.args[1] + x + + Two unevaluated Muls with the same arguments will + always compare as equal during testing: + + >>> m = uMul(sqrt(2), sqrt(3)) + >>> m == uMul(sqrt(3), sqrt(2)) + True + >>> u = Mul(sqrt(3), sqrt(2), evaluate=False) + >>> m == uMul(u) + True + >>> m == Mul(*m.args) + False + + """ + cargs = [] + ncargs = [] + args = list(args) + co = S.One + for a in args: + if a.is_Mul: + a_c, a_nc = a.args_cnc() + args.extend(a_c) # grow args + ncargs.extend(a_nc) + elif a.is_Number: + co *= a + elif a.is_commutative: + cargs.append(a) + else: + ncargs.append(a) + _mulsort(cargs) + if co is not S.One: + cargs.insert(0, co) + return Mul._from_args(cargs+ncargs) + + +class Mul(Expr, AssocOp): + """ + Expression representing multiplication operation for algebraic field. + + .. deprecated:: 1.7 + + Using arguments that aren't subclasses of :class:`~.Expr` in core + operators (:class:`~.Mul`, :class:`~.Add`, and :class:`~.Pow`) is + deprecated. See :ref:`non-expr-args-deprecated` for details. + + Every argument of ``Mul()`` must be ``Expr``. Infix operator ``*`` + on most scalar objects in SymPy calls this class. + + Another use of ``Mul()`` is to represent the structure of abstract + multiplication so that its arguments can be substituted to return + different class. Refer to examples section for this. + + ``Mul()`` evaluates the argument unless ``evaluate=False`` is passed. + The evaluation logic includes: + + 1. Flattening + ``Mul(x, Mul(y, z))`` -> ``Mul(x, y, z)`` + + 2. Identity removing + ``Mul(x, 1, y)`` -> ``Mul(x, y)`` + + 3. Exponent collecting by ``.as_base_exp()`` + ``Mul(x, x**2)`` -> ``Pow(x, 3)`` + + 4. Term sorting + ``Mul(y, x, 2)`` -> ``Mul(2, x, y)`` + + Since multiplication can be vector space operation, arguments may + have the different :obj:`sympy.core.kind.Kind()`. Kind of the + resulting object is automatically inferred. + + Examples + ======== + + >>> from sympy import Mul + >>> from sympy.abc import x, y + >>> Mul(x, 1) + x + >>> Mul(x, x) + x**2 + + If ``evaluate=False`` is passed, result is not evaluated. + + >>> Mul(1, 2, evaluate=False) + 1*2 + >>> Mul(x, x, evaluate=False) + x*x + + ``Mul()`` also represents the general structure of multiplication + operation. + + >>> from sympy import MatrixSymbol + >>> A = MatrixSymbol('A', 2,2) + >>> expr = Mul(x,y).subs({y:A}) + >>> expr + x*A + >>> type(expr) + + + See Also + ======== + + MatMul + + """ + __slots__ = () + + is_Mul = True + + _args_type = Expr + _kind_dispatcher = KindDispatcher("Mul_kind_dispatcher", commutative=True) + + identity: ClassVar[Expr] + + @property + def kind(self): + arg_kinds = (a.kind for a in self.args) + return self._kind_dispatcher(*arg_kinds) + + if TYPE_CHECKING: + + def __new__(cls, *args: Expr | complex, evaluate: bool=True) -> Expr: # type: ignore + ... + + @property + def args(self) -> tuple[Expr, ...]: + ... + + def could_extract_minus_sign(self): + if self == (-self): + return False # e.g. zoo*x == -zoo*x + c = self.args[0] + return c.is_Number and c.is_extended_negative + + def __neg__(self): + c, args = self.as_coeff_mul() + if args[0] is not S.ComplexInfinity: + c = -c + if c is not S.One: + if args[0].is_Number: + args = list(args) + if c is S.NegativeOne: + args[0] = -args[0] + else: + args[0] *= c + else: + args = (c,) + args + return self._from_args(args, self.is_commutative) + + @classmethod + def flatten(cls, seq): + """Return commutative, noncommutative and order arguments by + combining related terms. + + Notes + ===== + * In an expression like ``a*b*c``, Python process this through SymPy + as ``Mul(Mul(a, b), c)``. This can have undesirable consequences. + + - Sometimes terms are not combined as one would like: + {c.f. https://github.com/sympy/sympy/issues/4596} + + >>> from sympy import Mul, sqrt + >>> from sympy.abc import x, y, z + >>> 2*(x + 1) # this is the 2-arg Mul behavior + 2*x + 2 + >>> y*(x + 1)*2 + 2*y*(x + 1) + >>> 2*(x + 1)*y # 2-arg result will be obtained first + y*(2*x + 2) + >>> Mul(2, x + 1, y) # all 3 args simultaneously processed + 2*y*(x + 1) + >>> 2*((x + 1)*y) # parentheses can control this behavior + 2*y*(x + 1) + + Powers with compound bases may not find a single base to + combine with unless all arguments are processed at once. + Post-processing may be necessary in such cases. + {c.f. https://github.com/sympy/sympy/issues/5728} + + >>> a = sqrt(x*sqrt(y)) + >>> a**3 + (x*sqrt(y))**(3/2) + >>> Mul(a,a,a) + (x*sqrt(y))**(3/2) + >>> a*a*a + x*sqrt(y)*sqrt(x*sqrt(y)) + >>> _.subs(a.base, z).subs(z, a.base) + (x*sqrt(y))**(3/2) + + - If more than two terms are being multiplied then all the + previous terms will be re-processed for each new argument. + So if each of ``a``, ``b`` and ``c`` were :class:`Mul` + expression, then ``a*b*c`` (or building up the product + with ``*=``) will process all the arguments of ``a`` and + ``b`` twice: once when ``a*b`` is computed and again when + ``c`` is multiplied. + + Using ``Mul(a, b, c)`` will process all arguments once. + + * The results of Mul are cached according to arguments, so flatten + will only be called once for ``Mul(a, b, c)``. If you can + structure a calculation so the arguments are most likely to be + repeats then this can save time in computing the answer. For + example, say you had a Mul, M, that you wished to divide by ``d[i]`` + and multiply by ``n[i]`` and you suspect there are many repeats + in ``n``. It would be better to compute ``M*n[i]/d[i]`` rather + than ``M/d[i]*n[i]`` since every time n[i] is a repeat, the + product, ``M*n[i]`` will be returned without flattening -- the + cached value will be returned. If you divide by the ``d[i]`` + first (and those are more unique than the ``n[i]``) then that will + create a new Mul, ``M/d[i]`` the args of which will be traversed + again when it is multiplied by ``n[i]``. + + {c.f. https://github.com/sympy/sympy/issues/5706} + + This consideration is moot if the cache is turned off. + + NB + -- + The validity of the above notes depends on the implementation + details of Mul and flatten which may change at any time. Therefore, + you should only consider them when your code is highly performance + sensitive. + + Removal of 1 from the sequence is already handled by AssocOp.__new__. + """ + + from sympy.calculus.accumulationbounds import AccumBounds + from sympy.matrices.expressions import MatrixExpr + rv = None + if len(seq) == 2: + a, b = seq + if b.is_Rational: + a, b = b, a + seq = [a, b] + assert a is not S.One + if a.is_Rational and not a.is_zero: + r, b = b.as_coeff_Mul() + if b.is_Add: + if r is not S.One: # 2-arg hack + # leave the Mul as a Mul? + ar = a*r + if ar is S.One: + arb = b + else: + arb = cls(a*r, b, evaluate=False) + rv = [arb], [], None + elif global_parameters.distribute and b.is_commutative: + newb = Add(*[_keep_coeff(a, bi) for bi in b.args]) + rv = [newb], [], None + if rv: + return rv + + # apply associativity, separate commutative part of seq + c_part = [] # out: commutative factors + nc_part = [] # out: non-commutative factors + + nc_seq = [] + + coeff = S.One # standalone term + # e.g. 3 * ... + + c_powers = [] # (base,exp) n + # e.g. (x,n) for x + + num_exp = [] # (num-base, exp) y + # e.g. (3, y) for ... * 3 * ... + + neg1e = S.Zero # exponent on -1 extracted from Number-based Pow and I + + pnum_rat = {} # (num-base, Rat-exp) 1/2 + # e.g. (3, 1/2) for ... * 3 * ... + + order_symbols = None + + # --- PART 1 --- + # + # "collect powers and coeff": + # + # o coeff + # o c_powers + # o num_exp + # o neg1e + # o pnum_rat + # + # NOTE: this is optimized for all-objects-are-commutative case + for o in seq: + # O(x) + if o.is_Order: + o, order_symbols = o.as_expr_variables(order_symbols) + + # Mul([...]) + if o.is_Mul: + if o.is_commutative: + seq.extend(o.args) # XXX zerocopy? + + else: + # NCMul can have commutative parts as well + for q in o.args: + if q.is_commutative: + seq.append(q) + else: + nc_seq.append(q) + + # append non-commutative marker, so we don't forget to + # process scheduled non-commutative objects + seq.append(NC_Marker) + + continue + + # 3 + elif o.is_Number: + if o is S.NaN or coeff is S.ComplexInfinity and o.is_zero: + # we know for sure the result will be nan + return [S.NaN], [], None + elif coeff.is_Number or isinstance(coeff, AccumBounds): # it could be zoo + coeff *= o + if coeff is S.NaN: + # we know for sure the result will be nan + return [S.NaN], [], None + continue + + elif isinstance(o, AccumBounds): + coeff = o.__mul__(coeff) + continue + + elif o is S.ComplexInfinity: + if not coeff: + # 0 * zoo = NaN + return [S.NaN], [], None + coeff = S.ComplexInfinity + continue + + elif not coeff and isinstance(o, Add) and any( + _ in (S.NegativeInfinity, S.ComplexInfinity, S.Infinity) + for __ in o.args for _ in Mul.make_args(__)): + # e.g 0 * (x + oo) = NaN but not + # 0 * (1 + Integral(x, (x, 0, oo))) which is + # treated like 0 * x -> 0 + return [S.NaN], [], None + + elif o is S.ImaginaryUnit: + neg1e += S.Half + continue + + elif o.is_commutative: + # e + # o = b + b, e = o.as_base_exp() + + # y + # 3 + if o.is_Pow: + if b.is_Number: + + # get all the factors with numeric base so they can be + # combined below, but don't combine negatives unless + # the exponent is an integer + if e.is_Rational: + if e.is_Integer: + coeff *= Pow(b, e) # it is an unevaluated power + continue + elif e.is_negative: # also a sign of an unevaluated power + seq.append(Pow(b, e)) + continue + elif b.is_negative: + neg1e += e + b = -b + if b is not S.One: + pnum_rat.setdefault(b, []).append(e) + continue + elif b.is_positive or e.is_integer: + num_exp.append((b, e)) + continue + + c_powers.append((b, e)) + + # NON-COMMUTATIVE + # TODO: Make non-commutative exponents not combine automatically + else: + if o is not NC_Marker: + nc_seq.append(o) + + # process nc_seq (if any) + while nc_seq: + o = nc_seq.pop(0) + if not nc_part: + nc_part.append(o) + continue + + # b c b+c + # try to combine last terms: a * a -> a + o1 = nc_part.pop() + b1, e1 = o1.as_base_exp() + b2, e2 = o.as_base_exp() + new_exp = e1 + e2 + # Only allow powers to combine if the new exponent is + # not an Add. This allow things like a**2*b**3 == a**5 + # if a.is_commutative == False, but prohibits + # a**x*a**y and x**a*x**b from combining (x,y commute). + if b1 == b2 and (not new_exp.is_Add): + o12 = b1 ** new_exp + + # now o12 could be a commutative object + if o12.is_commutative: + seq.append(o12) + continue + else: + nc_seq.insert(0, o12) + + else: + nc_part.extend([o1, o]) + + # We do want a combined exponent if it would not be an Add, such as + # y 2y 3y + # x * x -> x + # We determine if two exponents have the same term by using + # as_coeff_Mul. + # + # Unfortunately, this isn't smart enough to consider combining into + # exponents that might already be adds, so things like: + # z - y y + # x * x will be left alone. This is because checking every possible + # combination can slow things down. + + # gather exponents of common bases... + def _gather(c_powers): + common_b = {} # b:e + for b, e in c_powers: + co = e.as_coeff_Mul() + common_b.setdefault(b, {}).setdefault( + co[1], []).append(co[0]) + for b, d in common_b.items(): + for di, li in d.items(): + d[di] = Add(*li) + new_c_powers = [] + for b, e in common_b.items(): + new_c_powers.extend([(b, c*t) for t, c in e.items()]) + return new_c_powers + + # in c_powers + c_powers = _gather(c_powers) + + # and in num_exp + num_exp = _gather(num_exp) + + # --- PART 2 --- + # + # o process collected powers (x**0 -> 1; x**1 -> x; otherwise Pow) + # o combine collected powers (2**x * 3**x -> 6**x) + # with numeric base + + # ................................ + # now we have: + # - coeff: + # - c_powers: (b, e) + # - num_exp: (2, e) + # - pnum_rat: {(1/3, [1/3, 2/3, 1/4])} + + # 0 1 + # x -> 1 x -> x + + # this should only need to run twice; if it fails because + # it needs to be run more times, perhaps this should be + # changed to a "while True" loop -- the only reason it + # isn't such now is to allow a less-than-perfect result to + # be obtained rather than raising an error or entering an + # infinite loop + for i in range(2): + new_c_powers = [] + changed = False + for b, e in c_powers: + if e.is_zero: + # canceling out infinities yields NaN + if (b.is_Add or b.is_Mul) and any(infty in b.args + for infty in (S.ComplexInfinity, S.Infinity, + S.NegativeInfinity)): + return [S.NaN], [], None + continue + if e is S.One: + if b.is_Number: + coeff *= b + continue + p = b + if e is not S.One: + p = Pow(b, e) + # check to make sure that the base doesn't change + # after exponentiation; to allow for unevaluated + # Pow, we only do so if b is not already a Pow + if p.is_Pow and not b.is_Pow: + bi = b + b, e = p.as_base_exp() + if b != bi: + changed = True + c_part.append(p) + new_c_powers.append((b, e)) + # there might have been a change, but unless the base + # matches some other base, there is nothing to do + if changed and len({ + b for b, e in new_c_powers}) != len(new_c_powers): + # start over again + c_part = [] + c_powers = _gather(new_c_powers) + else: + break + + # x x x + # 2 * 3 -> 6 + inv_exp_dict = {} # exp:Mul(num-bases) x x + # e.g. x:6 for ... * 2 * 3 * ... + for b, e in num_exp: + inv_exp_dict.setdefault(e, []).append(b) + for e, b in inv_exp_dict.items(): + inv_exp_dict[e] = cls(*b) + c_part.extend([Pow(b, e) for e, b in inv_exp_dict.items() if e]) + + # b, e -> e' = sum(e), b + # {(1/5, [1/3]), (1/2, [1/12, 1/4]} -> {(1/3, [1/5, 1/2])} + comb_e = {} + for b, e in pnum_rat.items(): + comb_e.setdefault(Add(*e), []).append(b) + del pnum_rat + # process them, reducing exponents to values less than 1 + # and updating coeff if necessary else adding them to + # num_rat for further processing + num_rat = [] + for e, b in comb_e.items(): + b = cls(*b) + if e.q == 1: + coeff *= Pow(b, e) + continue + if e.p > e.q: + e_i, ep = divmod(e.p, e.q) + coeff *= Pow(b, e_i) + e = Rational(ep, e.q) + num_rat.append((b, e)) + del comb_e + + # extract gcd of bases in num_rat + # 2**(1/3)*6**(1/4) -> 2**(1/3+1/4)*3**(1/4) + pnew = defaultdict(list) + i = 0 # steps through num_rat which may grow + while i < len(num_rat): + bi, ei = num_rat[i] + if bi == 1: + i += 1 + continue + grow = [] + for j in range(i + 1, len(num_rat)): + bj, ej = num_rat[j] + g = bi.gcd(bj) + if g is not S.One: + # 4**r1*6**r2 -> 2**(r1+r2) * 2**r1 * 3**r2 + # this might have a gcd with something else + e = ei + ej + if e.q == 1: + coeff *= Pow(g, e) + else: + if e.p > e.q: + e_i, ep = divmod(e.p, e.q) # change e in place + coeff *= Pow(g, e_i) + e = Rational(ep, e.q) + grow.append((g, e)) + # update the jth item + num_rat[j] = (bj/g, ej) + # update bi that we are checking with + bi = bi/g + if bi is S.One: + break + if bi is not S.One: + obj = Pow(bi, ei) + if obj.is_Number: + coeff *= obj + else: + # changes like sqrt(12) -> 2*sqrt(3) + for obj in Mul.make_args(obj): + if obj.is_Number: + coeff *= obj + else: + assert obj.is_Pow + bi, ei = obj.args + pnew[ei].append(bi) + + num_rat.extend(grow) + i += 1 + + # combine bases of the new powers + for e, b in pnew.items(): + pnew[e] = cls(*b) + + # handle -1 and I + if neg1e: + # treat I as (-1)**(1/2) and compute -1's total exponent + p, q = neg1e.as_numer_denom() + # if the integer part is odd, extract -1 + n, p = divmod(p, q) + if n % 2: + coeff = -coeff + # if it's a multiple of 1/2 extract I + if q == 2: + c_part.append(S.ImaginaryUnit) + elif p: + # see if there is any positive base this power of + # -1 can join + neg1e = Rational(p, q) + for e, b in pnew.items(): + if e == neg1e and b.is_positive: + pnew[e] = -b + break + else: + # keep it separate; we've already evaluated it as + # much as possible so evaluate=False + c_part.append(Pow(S.NegativeOne, neg1e, evaluate=False)) + + # add all the pnew powers + c_part.extend([Pow(b, e) for e, b in pnew.items()]) + + # oo, -oo + if coeff in (S.Infinity, S.NegativeInfinity): + def _handle_for_oo(c_part, coeff_sign): + new_c_part = [] + for t in c_part: + if t.is_extended_positive: + continue + if t.is_extended_negative: + coeff_sign *= -1 + continue + new_c_part.append(t) + return new_c_part, coeff_sign + c_part, coeff_sign = _handle_for_oo(c_part, 1) + nc_part, coeff_sign = _handle_for_oo(nc_part, coeff_sign) + coeff *= coeff_sign + + # zoo + if coeff is S.ComplexInfinity: + # zoo might be + # infinite_real + bounded_im + # bounded_real + infinite_im + # infinite_real + infinite_im + # and non-zero real or imaginary will not change that status. + c_part = [c for c in c_part if not (fuzzy_not(c.is_zero) and + c.is_extended_real is not None)] + nc_part = [c for c in nc_part if not (fuzzy_not(c.is_zero) and + c.is_extended_real is not None)] + + # 0 + elif coeff.is_zero: + # we know for sure the result will be 0 except the multiplicand + # is infinity or a matrix + if any(isinstance(c, MatrixExpr) for c in nc_part): + return [coeff], nc_part, order_symbols + if any(c.is_finite == False for c in c_part): + return [S.NaN], [], order_symbols + return [coeff], [], order_symbols + + # check for straggling Numbers that were produced + _new = [] + for i in c_part: + if i.is_Number: + coeff *= i + else: + _new.append(i) + c_part = _new + + # order commutative part canonically + _mulsort(c_part) + + # current code expects coeff to be always in slot-0 + if coeff is not S.One: + c_part.insert(0, coeff) + + # we are done + if (global_parameters.distribute and not nc_part and len(c_part) == 2 and + c_part[0].is_Number and c_part[0].is_finite and c_part[1].is_Add): + # 2*(1+a) -> 2 + 2 * a + coeff = c_part[0] + c_part = [Add(*[coeff*f for f in c_part[1].args])] + + return c_part, nc_part, order_symbols + + def _eval_power(self, expt): + + # don't break up NC terms: (A*B)**3 != A**3*B**3, it is A*B*A*B*A*B + cargs, nc = self.args_cnc(split_1=False) + + if expt.is_Integer: + return Mul(*[Pow(b, expt, evaluate=False) for b in cargs]) * \ + Pow(Mul._from_args(nc), expt, evaluate=False) + if expt.is_Rational and expt.q == 2: + if self.is_imaginary: + a = self.as_real_imag()[1] + if a.is_Rational: + n, d = abs(a/2).as_numer_denom() + n, t = integer_nthroot(n, 2) + if t: + d, t = integer_nthroot(d, 2) + if t: + from sympy.functions.elementary.complexes import sign + r = sympify(n)/d + return _unevaluated_Mul(r**expt.p, (1 + sign(a)*S.ImaginaryUnit)**expt.p) + + p = Pow(self, expt, evaluate=False) + + if expt.is_Rational or expt.is_Float: + return p._eval_expand_power_base() + + return p + + @classmethod + def class_key(cls): + return 3, 0, cls.__name__ + + def _eval_evalf(self, prec): + c, m = self.as_coeff_Mul() + if c is S.NegativeOne: + if m.is_Mul: + rv = -AssocOp._eval_evalf(m, prec) + else: + mnew = m._eval_evalf(prec) + if mnew is not None: + m = mnew + rv = -m + else: + rv = AssocOp._eval_evalf(self, prec) + if rv.is_number: + return rv.expand() + return rv + + @property + def _mpc_(self): + """ + Convert self to an mpmath mpc if possible + """ + from .numbers import Float + im_part, imag_unit = self.as_coeff_Mul() + if imag_unit is not S.ImaginaryUnit: + # ValueError may seem more reasonable but since it's a @property, + # we need to use AttributeError to keep from confusing things like + # hasattr. + raise AttributeError("Cannot convert Mul to mpc. Must be of the form Number*I") + + return (Float(0)._mpf_, Float(im_part)._mpf_) + + @cacheit + def as_two_terms(self): + """Return head and tail of self. + + This is the most efficient way to get the head and tail of an + expression. + + - if you want only the head, use self.args[0]; + - if you want to process the arguments of the tail then use + self.as_coef_mul() which gives the head and a tuple containing + the arguments of the tail when treated as a Mul. + - if you want the coefficient when self is treated as an Add + then use self.as_coeff_add()[0] + + Examples + ======== + + >>> from sympy.abc import x, y + >>> (3*x*y).as_two_terms() + (3, x*y) + """ + args = self.args + + if len(args) == 1: + return S.One, self + elif len(args) == 2: + return args + + else: + return args[0], self._new_rawargs(*args[1:]) + + @cacheit + def as_coeff_mul(self, *deps, rational=True, **kwargs): + if deps: + l1, l2 = sift(self.args, lambda x: x.has(*deps), binary=True) + return self._new_rawargs(*l2), tuple(l1) + args = self.args + if args[0].is_Number: + if not rational or args[0].is_Rational: + return args[0], args[1:] + elif args[0].is_extended_negative: + return S.NegativeOne, (-args[0],) + args[1:] + return S.One, args + + def as_coeff_Mul(self, rational=False): + """ + Efficiently extract the coefficient of a product. + """ + coeff, args = self.args[0], self.args[1:] + + if coeff.is_Number: + if not rational or coeff.is_Rational: + if len(args) == 1: + return coeff, args[0] + else: + return coeff, self._new_rawargs(*args) + elif coeff.is_extended_negative: + return S.NegativeOne, self._new_rawargs(*((-coeff,) + args)) + return S.One, self + + def as_real_imag(self, deep=True, **hints): + from sympy.functions.elementary.complexes import Abs, im, re + other = [] + coeffr = [] + coeffi = [] + addterms = S.One + for a in self.args: + r, i = a.as_real_imag() + if i.is_zero: + coeffr.append(r) + elif r.is_zero: + coeffi.append(i*S.ImaginaryUnit) + elif a.is_commutative: + aconj = a.conjugate() if other else None + # search for complex conjugate pairs: + for i, x in enumerate(other): + if x == aconj: + coeffr.append(Abs(x)**2) + del other[i] + break + else: + if a.is_Add: + addterms *= a + else: + other.append(a) + else: + other.append(a) + m = self.func(*other) + if hints.get('ignore') == m: + return + if len(coeffi) % 2: + imco = im(coeffi.pop(0)) + # all other pairs make a real factor; they will be + # put into reco below + else: + imco = S.Zero + reco = self.func(*(coeffr + coeffi)) + r, i = (reco*re(m), reco*im(m)) + if addterms == 1: + if m == 1: + if imco.is_zero: + return (reco, S.Zero) + else: + return (S.Zero, reco*imco) + if imco is S.Zero: + return (r, i) + return (-imco*i, imco*r) + from .function import expand_mul + addre, addim = expand_mul(addterms, deep=False).as_real_imag() + if imco is S.Zero: + return (r*addre - i*addim, i*addre + r*addim) + else: + r, i = -imco*i, imco*r + return (r*addre - i*addim, r*addim + i*addre) + + @staticmethod + def _expandsums(sums): + """ + Helper function for _eval_expand_mul. + + sums must be a list of instances of Basic. + """ + + L = len(sums) + if L == 1: + return sums[0].args + terms = [] + left = Mul._expandsums(sums[:L//2]) + right = Mul._expandsums(sums[L//2:]) + + terms = [Mul(a, b) for a in left for b in right] + added = Add(*terms) + return Add.make_args(added) # it may have collapsed down to one term + + def _eval_expand_mul(self, **hints): + from sympy.simplify.radsimp import fraction + + # Handle things like 1/(x*(x + 1)), which are automatically converted + # to 1/x*1/(x + 1) + expr = self + # default matches fraction's default + n, d = fraction(expr, hints.get('exact', False)) + if d.is_Mul: + n, d = [i._eval_expand_mul(**hints) if i.is_Mul else i + for i in (n, d)] + expr = n/d + if not expr.is_Mul: + return expr + + plain, sums, rewrite = [], [], False + for factor in expr.args: + if factor.is_Add: + sums.append(factor) + rewrite = True + else: + if factor.is_commutative: + plain.append(factor) + else: + sums.append(Basic(factor)) # Wrapper + + if not rewrite: + return expr + else: + plain = self.func(*plain) + if sums: + deep = hints.get("deep", False) + terms = self.func._expandsums(sums) + args = [] + for term in terms: + t = self.func(plain, term) + if t.is_Mul and any(a.is_Add for a in t.args) and deep: + t = t._eval_expand_mul() + args.append(t) + return Add(*args) + else: + return plain + + @cacheit + def _eval_derivative(self, s): + args = list(self.args) + terms = [] + for i in range(len(args)): + d = args[i].diff(s) + if d: + # Note: reduce is used in step of Mul as Mul is unable to + # handle subtypes and operation priority: + terms.append(reduce(lambda x, y: x*y, (args[:i] + [d] + args[i + 1:]), S.One)) + return Add.fromiter(terms) + + @cacheit + def _eval_derivative_n_times(self, s, n): + from .function import AppliedUndef + from .symbol import Symbol, symbols, Dummy + if not isinstance(s, (AppliedUndef, Symbol)): + # other types of s may not be well behaved, e.g. + # (cos(x)*sin(y)).diff([[x, y, z]]) + return super()._eval_derivative_n_times(s, n) + from .numbers import Integer + args = self.args + m = len(args) + if isinstance(n, (int, Integer)): + # https://en.wikipedia.org/wiki/General_Leibniz_rule#More_than_two_factors + terms = [] + from sympy.ntheory.multinomial import multinomial_coefficients_iterator + for kvals, c in multinomial_coefficients_iterator(m, n): + p = Mul(*[arg.diff((s, k)) for k, arg in zip(kvals, args)]) + terms.append(c * p) + return Add(*terms) + from sympy.concrete.summations import Sum + from sympy.functions.combinatorial.factorials import factorial + from sympy.functions.elementary.miscellaneous import Max + kvals = symbols("k1:%i" % m, cls=Dummy) + klast = n - sum(kvals) + nfact = factorial(n) + e, l = (# better to use the multinomial? + nfact/prod(map(factorial, kvals))/factorial(klast)*\ + Mul(*[args[t].diff((s, kvals[t])) for t in range(m-1)])*\ + args[-1].diff((s, Max(0, klast))), + [(k, 0, n) for k in kvals]) + return Sum(e, *l) + + def _eval_difference_delta(self, n, step): + from sympy.series.limitseq import difference_delta as dd + arg0 = self.args[0] + rest = Mul(*self.args[1:]) + return (arg0.subs(n, n + step) * dd(rest, n, step) + dd(arg0, n, step) * + rest) + + def _matches_simple(self, expr, repl_dict): + # handle (w*3).matches('x*5') -> {w: x*5/3} + coeff, terms = self.as_coeff_Mul() + terms = Mul.make_args(terms) + if len(terms) == 1: + newexpr = self.__class__._combine_inverse(expr, coeff) + return terms[0].matches(newexpr, repl_dict) + return + + def matches(self, expr, repl_dict=None, old=False): + expr = sympify(expr) + if self.is_commutative and expr.is_commutative: + return self._matches_commutative(expr, repl_dict, old) + elif self.is_commutative is not expr.is_commutative: + return None + + # Proceed only if both both expressions are non-commutative + c1, nc1 = self.args_cnc() + c2, nc2 = expr.args_cnc() + c1, c2 = [c or [1] for c in [c1, c2]] + + # TODO: Should these be self.func? + comm_mul_self = Mul(*c1) + comm_mul_expr = Mul(*c2) + + repl_dict = comm_mul_self.matches(comm_mul_expr, repl_dict, old) + + # If the commutative arguments didn't match and aren't equal, then + # then the expression as a whole doesn't match + if not repl_dict and c1 != c2: + return None + + # Now match the non-commutative arguments, expanding powers to + # multiplications + nc1 = Mul._matches_expand_pows(nc1) + nc2 = Mul._matches_expand_pows(nc2) + + repl_dict = Mul._matches_noncomm(nc1, nc2, repl_dict) + + return repl_dict or None + + @staticmethod + def _matches_expand_pows(arg_list): + new_args = [] + for arg in arg_list: + if arg.is_Pow and arg.exp > 0: + new_args.extend([arg.base] * arg.exp) + else: + new_args.append(arg) + return new_args + + @staticmethod + def _matches_noncomm(nodes, targets, repl_dict=None): + """Non-commutative multiplication matcher. + + `nodes` is a list of symbols within the matcher multiplication + expression, while `targets` is a list of arguments in the + multiplication expression being matched against. + """ + if repl_dict is None: + repl_dict = {} + else: + repl_dict = repl_dict.copy() + + # List of possible future states to be considered + agenda = [] + # The current matching state, storing index in nodes and targets + state = (0, 0) + node_ind, target_ind = state + # Mapping between wildcard indices and the index ranges they match + wildcard_dict = {} + + while target_ind < len(targets) and node_ind < len(nodes): + node = nodes[node_ind] + + if node.is_Wild: + Mul._matches_add_wildcard(wildcard_dict, state) + + states_matches = Mul._matches_new_states(wildcard_dict, state, + nodes, targets) + if states_matches: + new_states, new_matches = states_matches + agenda.extend(new_states) + if new_matches: + for match in new_matches: + repl_dict[match] = new_matches[match] + if not agenda: + return None + else: + state = agenda.pop() + node_ind, target_ind = state + + return repl_dict + + @staticmethod + def _matches_add_wildcard(dictionary, state): + node_ind, target_ind = state + if node_ind in dictionary: + begin, end = dictionary[node_ind] + dictionary[node_ind] = (begin, target_ind) + else: + dictionary[node_ind] = (target_ind, target_ind) + + @staticmethod + def _matches_new_states(dictionary, state, nodes, targets): + node_ind, target_ind = state + node = nodes[node_ind] + target = targets[target_ind] + + # Don't advance at all if we've exhausted the targets but not the nodes + if target_ind >= len(targets) - 1 and node_ind < len(nodes) - 1: + return None + + if node.is_Wild: + match_attempt = Mul._matches_match_wilds(dictionary, node_ind, + nodes, targets) + if match_attempt: + # If the same node has been matched before, don't return + # anything if the current match is diverging from the previous + # match + other_node_inds = Mul._matches_get_other_nodes(dictionary, + nodes, node_ind) + for ind in other_node_inds: + other_begin, other_end = dictionary[ind] + curr_begin, curr_end = dictionary[node_ind] + + other_targets = targets[other_begin:other_end + 1] + current_targets = targets[curr_begin:curr_end + 1] + + for curr, other in zip(current_targets, other_targets): + if curr != other: + return None + + # A wildcard node can match more than one target, so only the + # target index is advanced + new_state = [(node_ind, target_ind + 1)] + # Only move on to the next node if there is one + if node_ind < len(nodes) - 1: + new_state.append((node_ind + 1, target_ind + 1)) + return new_state, match_attempt + else: + # If we're not at a wildcard, then make sure we haven't exhausted + # nodes but not targets, since in this case one node can only match + # one target + if node_ind >= len(nodes) - 1 and target_ind < len(targets) - 1: + return None + + match_attempt = node.matches(target) + + if match_attempt: + return [(node_ind + 1, target_ind + 1)], match_attempt + elif node == target: + return [(node_ind + 1, target_ind + 1)], None + else: + return None + + @staticmethod + def _matches_match_wilds(dictionary, wildcard_ind, nodes, targets): + """Determine matches of a wildcard with sub-expression in `target`.""" + wildcard = nodes[wildcard_ind] + begin, end = dictionary[wildcard_ind] + terms = targets[begin:end + 1] + # TODO: Should this be self.func? + mult = Mul(*terms) if len(terms) > 1 else terms[0] + return wildcard.matches(mult) + + @staticmethod + def _matches_get_other_nodes(dictionary, nodes, node_ind): + """Find other wildcards that may have already been matched.""" + ind_node = nodes[node_ind] + return [ind for ind in dictionary if nodes[ind] == ind_node] + + @staticmethod + def _combine_inverse(lhs, rhs): + """ + Returns lhs/rhs, but treats arguments like symbols, so things + like oo/oo return 1 (instead of a nan) and ``I`` behaves like + a symbol instead of sqrt(-1). + """ + from sympy.simplify.simplify import signsimp + from .symbol import Dummy + if lhs == rhs: + return S.One + + def check(l, r): + if l.is_Float and r.is_comparable: + # if both objects are added to 0 they will share the same "normalization" + # and are more likely to compare the same. Since Add(foo, 0) will not allow + # the 0 to pass, we use __add__ directly. + return l.__add__(0) == r.evalf().__add__(0) + return False + if check(lhs, rhs) or check(rhs, lhs): + return S.One + if any(i.is_Pow or i.is_Mul for i in (lhs, rhs)): + # gruntz and limit wants a literal I to not combine + # with a power of -1 + d = Dummy('I') + _i = {S.ImaginaryUnit: d} + i_ = {d: S.ImaginaryUnit} + a = lhs.xreplace(_i).as_powers_dict() + b = rhs.xreplace(_i).as_powers_dict() + blen = len(b) + for bi in tuple(b.keys()): + if bi in a: + a[bi] -= b.pop(bi) + if not a[bi]: + a.pop(bi) + if len(b) != blen: + lhs = Mul(*[k**v for k, v in a.items()]).xreplace(i_) + rhs = Mul(*[k**v for k, v in b.items()]).xreplace(i_) + rv = lhs/rhs + srv = signsimp(rv) + return srv if srv.is_Number else rv + + def as_powers_dict(self): + d = defaultdict(int) + for term in self.args: + for b, e in term.as_powers_dict().items(): + d[b] += e + return d + + def as_numer_denom(self): + # don't use _from_args to rebuild the numerators and denominators + # as the order is not guaranteed to be the same once they have + # been separated from each other + numers, denoms = list(zip(*[f.as_numer_denom() for f in self.args])) + return self.func(*numers), self.func(*denoms) + + def as_base_exp(self): + e1 = None + bases = [] + nc = 0 + for m in self.args: + b, e = m.as_base_exp() + if not b.is_commutative: + nc += 1 + if e1 is None: + e1 = e + elif e != e1 or nc > 1 or not e.is_Integer: + return self, S.One + bases.append(b) + return self.func(*bases), e1 + + def _eval_is_polynomial(self, syms): + return all(term._eval_is_polynomial(syms) for term in self.args) + + def _eval_is_rational_function(self, syms): + return all(term._eval_is_rational_function(syms) for term in self.args) + + def _eval_is_meromorphic(self, x, a): + return _fuzzy_group((arg.is_meromorphic(x, a) for arg in self.args), + quick_exit=True) + + def _eval_is_algebraic_expr(self, syms): + return all(term._eval_is_algebraic_expr(syms) for term in self.args) + + _eval_is_commutative = lambda self: _fuzzy_group( + a.is_commutative for a in self.args) + + def _eval_is_complex(self): + comp = _fuzzy_group(a.is_complex for a in self.args) + if comp is False: + if any(a.is_infinite for a in self.args): + if any(a.is_zero is not False for a in self.args): + return None + return False + return comp + + def _eval_is_zero_infinite_helper(self): + # + # Helper used by _eval_is_zero and _eval_is_infinite. + # + # Three-valued logic is tricky so let us reason this carefully. It + # would be nice to say that we just check is_zero/is_infinite in all + # args but we need to be careful about the case that one arg is zero + # and another is infinite like Mul(0, oo) or more importantly a case + # where it is not known if the arguments are zero or infinite like + # Mul(y, 1/x). If either y or x could be zero then there is a + # *possibility* that we have Mul(0, oo) which should give None for both + # is_zero and is_infinite. + # + # We keep track of whether we have seen a zero or infinity but we also + # need to keep track of whether we have *possibly* seen one which + # would be indicated by None. + # + # For each argument there is the possibility that is_zero might give + # True, False or None and likewise that is_infinite might give True, + # False or None, giving 9 combinations. The True cases for is_zero and + # is_infinite are mutually exclusive though so there are 3 main cases: + # + # - is_zero = True + # - is_infinite = True + # - is_zero and is_infinite are both either False or None + # + # At the end seen_zero and seen_infinite can be any of 9 combinations + # of True/False/None. Unless one is False though we cannot return + # anything except None: + # + # - is_zero=True needs seen_zero=True and seen_infinite=False + # - is_zero=False needs seen_zero=False + # - is_infinite=True needs seen_infinite=True and seen_zero=False + # - is_infinite=False needs seen_infinite=False + # - anything else gives both is_zero=None and is_infinite=None + # + # The loop only sets the flags to True or None and never back to False. + # Hence as soon as neither flag is False we exit early returning None. + # In particular as soon as we encounter a single arg that has + # is_zero=is_infinite=None we exit. This is a common case since it is + # the default assumptions for a Symbol and also the case for most + # expressions containing such a symbol. The early exit gives a big + # speedup for something like Mul(*symbols('x:1000')).is_zero. + # + seen_zero = seen_infinite = False + + for a in self.args: + if a.is_zero: + if seen_infinite is not False: + return None, None + seen_zero = True + elif a.is_infinite: + if seen_zero is not False: + return None, None + seen_infinite = True + else: + if seen_zero is False and a.is_zero is None: + if seen_infinite is not False: + return None, None + seen_zero = None + if seen_infinite is False and a.is_infinite is None: + if seen_zero is not False: + return None, None + seen_infinite = None + + return seen_zero, seen_infinite + + def _eval_is_zero(self): + # True iff any arg is zero and no arg is infinite but need to handle + # three valued logic carefully. + seen_zero, seen_infinite = self._eval_is_zero_infinite_helper() + + if seen_zero is False: + return False + elif seen_zero is True and seen_infinite is False: + return True + else: + return None + + def _eval_is_infinite(self): + # True iff any arg is infinite and no arg is zero but need to handle + # three valued logic carefully. + seen_zero, seen_infinite = self._eval_is_zero_infinite_helper() + + if seen_infinite is True and seen_zero is False: + return True + elif seen_infinite is False: + return False + else: + return None + + # We do not need to implement _eval_is_finite because the assumptions + # system can infer it from finite = not infinite. + + def _eval_is_rational(self): + r = _fuzzy_group((a.is_rational for a in self.args), quick_exit=True) + if r: + return r + elif r is False: + # All args except one are rational + if all(a.is_zero is False for a in self.args): + return False + + def _eval_is_algebraic(self): + r = _fuzzy_group((a.is_algebraic for a in self.args), quick_exit=True) + if r: + return r + elif r is False: + # All args except one are algebraic + if all(a.is_zero is False for a in self.args): + return False + + # without involving odd/even checks this code would suffice: + #_eval_is_integer = lambda self: _fuzzy_group( + # (a.is_integer for a in self.args), quick_exit=True) + def _eval_is_integer(self): + is_rational = self._eval_is_rational() + if is_rational is False: + return False + + numerators = [] + denominators = [] + unknown = False + for a in self.args: + hit = False + if a.is_integer: + if abs(a) is not S.One: + numerators.append(a) + elif a.is_Rational: + n, d = a.as_numer_denom() + if abs(n) is not S.One: + numerators.append(n) + if d is not S.One: + denominators.append(d) + elif a.is_Pow: + b, e = a.as_base_exp() + if not b.is_integer or not e.is_integer: + hit = unknown = True + if e.is_negative: + denominators.append(2 if a is S.Half else + Pow(a, S.NegativeOne)) + elif not hit: + # int b and pos int e: a = b**e is integer + assert not e.is_positive + # for rational self and e equal to zero: a = b**e is 1 + assert not e.is_zero + return # sign of e unknown -> self.is_integer unknown + else: + # x**2, 2**x, or x**y with x and y int-unknown -> unknown + return + else: + return + + if not denominators and not unknown: + return True + + allodd = lambda x: all(i.is_odd for i in x) + alleven = lambda x: all(i.is_even for i in x) + anyeven = lambda x: any(i.is_even for i in x) + + from .relational import is_gt + if not numerators and denominators and all( + is_gt(_, S.One) for _ in denominators): + return False + elif unknown: + return + elif allodd(numerators) and anyeven(denominators): + return False + elif anyeven(numerators) and denominators == [2]: + return True + elif alleven(numerators) and allodd(denominators + ) and (Mul(*denominators, evaluate=False) - 1 + ).is_positive: + return False + if len(denominators) == 1: + d = denominators[0] + if d.is_Integer and d.is_even: + # if minimal power of 2 in num vs den is not + # negative then we have an integer + if (Add(*[i.as_base_exp()[1] for i in + numerators if i.is_even]) - trailing(d.p) + ).is_nonnegative: + return True + if len(numerators) == 1: + n = numerators[0] + if n.is_Integer and n.is_even: + # if minimal power of 2 in den vs num is positive + # then we have have a non-integer + if (Add(*[i.as_base_exp()[1] for i in + denominators if i.is_even]) - trailing(n.p) + ).is_positive: + return False + + def _eval_is_polar(self): + has_polar = any(arg.is_polar for arg in self.args) + return has_polar and \ + all(arg.is_polar or arg.is_positive for arg in self.args) + + def _eval_is_extended_real(self): + return self._eval_real_imag(True) + + def _eval_real_imag(self, real): + zero = False + t_not_re_im = None + + for t in self.args: + if (t.is_complex or t.is_infinite) is False and t.is_extended_real is False: + return False + elif t.is_imaginary: # I + real = not real + elif t.is_extended_real: # 2 + if not zero: + z = t.is_zero + if not z and zero is False: + zero = z + elif z: + if all(a.is_finite for a in self.args): + return True + return + elif t.is_extended_real is False: + # symbolic or literal like `2 + I` or symbolic imaginary + if t_not_re_im: + return # complex terms might cancel + t_not_re_im = t + elif t.is_imaginary is False: # symbolic like `2` or `2 + I` + if t_not_re_im: + return # complex terms might cancel + t_not_re_im = t + else: + return + + if t_not_re_im: + if t_not_re_im.is_extended_real is False: + if real: # like 3 + return zero # 3*(smthng like 2 + I or i) is not real + if t_not_re_im.is_imaginary is False: # symbolic 2 or 2 + I + if not real: # like I + return zero # I*(smthng like 2 or 2 + I) is not real + elif zero is False: + return real # can't be trumped by 0 + elif real: + return real # doesn't matter what zero is + + def _eval_is_imaginary(self): + if all(a.is_zero is False and a.is_finite for a in self.args): + return self._eval_real_imag(False) + + def _eval_is_hermitian(self): + return self._eval_herm_antiherm(True) + + def _eval_is_antihermitian(self): + return self._eval_herm_antiherm(False) + + def _eval_herm_antiherm(self, herm): + for t in self.args: + if t.is_hermitian is None or t.is_antihermitian is None: + return + if t.is_hermitian: + continue + elif t.is_antihermitian: + herm = not herm + else: + return + + if herm is not False: + return herm + + is_zero = self._eval_is_zero() + if is_zero: + return True + elif is_zero is False: + return herm + + def _eval_is_irrational(self): + for t in self.args: + a = t.is_irrational + if a: + others = list(self.args) + others.remove(t) + if all((x.is_rational and fuzzy_not(x.is_zero)) is True for x in others): + return True + return + if a is None: + return + if all(x.is_real for x in self.args): + return False + + def _eval_is_extended_positive(self): + """Return True if self is positive, False if not, and None if it + cannot be determined. + + Explanation + =========== + + This algorithm is non-recursive and works by keeping track of the + sign which changes when a negative or nonpositive is encountered. + Whether a nonpositive or nonnegative is seen is also tracked since + the presence of these makes it impossible to return True, but + possible to return False if the end result is nonpositive. e.g. + + pos * neg * nonpositive -> pos or zero -> None is returned + pos * neg * nonnegative -> neg or zero -> False is returned + """ + return self._eval_pos_neg(1) + + def _eval_pos_neg(self, sign): + saw_NON = saw_NOT = False + for t in self.args: + if t.is_extended_positive: + continue + elif t.is_extended_negative: + sign = -sign + elif t.is_zero: + if all(a.is_finite for a in self.args): + return False + return + elif t.is_extended_nonpositive: + sign = -sign + saw_NON = True + elif t.is_extended_nonnegative: + saw_NON = True + # FIXME: is_positive/is_negative is False doesn't take account of + # Symbol('x', infinite=True, extended_real=True) which has + # e.g. is_positive is False but has uncertain sign. + elif t.is_positive is False: + sign = -sign + if saw_NOT: + return + saw_NOT = True + elif t.is_negative is False: + if saw_NOT: + return + saw_NOT = True + else: + return + if sign == 1 and saw_NON is False and saw_NOT is False: + return True + if sign < 0: + return False + + def _eval_is_extended_negative(self): + return self._eval_pos_neg(-1) + + def _eval_is_odd(self): + is_integer = self._eval_is_integer() + if is_integer is not True: + return is_integer + + from sympy.simplify.radsimp import fraction + n, d = fraction(self) + if d.is_Integer and d.is_even: + # if minimal power of 2 in num vs den is + # positive then we have an even number + if (Add(*[i.as_base_exp()[1] for i in + Mul.make_args(n) if i.is_even]) - trailing(d.p) + ).is_positive: + return False + return + r, acc = True, 1 + for t in self.args: + if abs(t) is S.One: + continue + if t.is_even: + return False + if r is False: + pass + elif acc != 1 and (acc + t).is_odd: + r = False + elif t.is_even is None: + r = None + acc = t + return r + + def _eval_is_even(self): + from sympy.simplify.radsimp import fraction + n, d = fraction(self) + if n.is_Integer and n.is_even: + # if minimal power of 2 in den vs num is not + # negative then this is not an integer and + # can't be even + if (Add(*[i.as_base_exp()[1] for i in + Mul.make_args(d) if i.is_even]) - trailing(n.p) + ).is_nonnegative: + return False + + def _eval_is_composite(self): + """ + Here we count the number of arguments that have a minimum value + greater than two. + If there are more than one of such a symbol then the result is composite. + Else, the result cannot be determined. + """ + number_of_args = 0 # count of symbols with minimum value greater than one + for arg in self.args: + if not (arg.is_integer and arg.is_positive): + return None + if (arg-1).is_positive: + number_of_args += 1 + + if number_of_args > 1: + return True + + def _eval_subs(self, old, new): + from sympy.functions.elementary.complexes import sign + from sympy.ntheory.factor_ import multiplicity + from sympy.simplify.powsimp import powdenest + from sympy.simplify.radsimp import fraction + + if not old.is_Mul: + return None + + # try keep replacement literal so -2*x doesn't replace 4*x + if old.args[0].is_Number and old.args[0] < 0: + if self.args[0].is_Number: + if self.args[0] < 0: + return self._subs(-old, -new) + return None + + def base_exp(a): + # if I and -1 are in a Mul, they get both end up with + # a -1 base (see issue 6421); all we want here are the + # true Pow or exp separated into base and exponent + from sympy.functions.elementary.exponential import exp + if a.is_Pow or isinstance(a, exp): + return a.as_base_exp() + return a, S.One + + def breakup(eq): + """break up powers of eq when treated as a Mul: + b**(Rational*e) -> b**e, Rational + commutatives come back as a dictionary {b**e: Rational} + noncommutatives come back as a list [(b**e, Rational)] + """ + + (c, nc) = (defaultdict(int), []) + for a in Mul.make_args(eq): + a = powdenest(a) + (b, e) = base_exp(a) + if e is not S.One: + (co, _) = e.as_coeff_mul() + b = Pow(b, e/co) + e = co + if a.is_commutative: + c[b] += e + else: + nc.append([b, e]) + return (c, nc) + + def rejoin(b, co): + """ + Put rational back with exponent; in general this is not ok, but + since we took it from the exponent for analysis, it's ok to put + it back. + """ + + (b, e) = base_exp(b) + return Pow(b, e*co) + + def ndiv(a, b): + """if b divides a in an extractive way (like 1/4 divides 1/2 + but not vice versa, and 2/5 does not divide 1/3) then return + the integer number of times it divides, else return 0. + """ + if not b.q % a.q or not a.q % b.q: + return int(a/b) + return 0 + + # give Muls in the denominator a chance to be changed (see issue 5651) + # rv will be the default return value + rv = None + n, d = fraction(self) + self2 = self + if d is not S.One: + self2 = n._subs(old, new)/d._subs(old, new) + if not self2.is_Mul: + return self2._subs(old, new) + if self2 != self: + rv = self2 + + # Now continue with regular substitution. + + # handle the leading coefficient and use it to decide if anything + # should even be started; we always know where to find the Rational + # so it's a quick test + + co_self = self2.args[0] + co_old = old.args[0] + co_xmul = None + if co_old.is_Rational and co_self.is_Rational: + # if coeffs are the same there will be no updating to do + # below after breakup() step; so skip (and keep co_xmul=None) + if co_old != co_self: + co_xmul = co_self.extract_multiplicatively(co_old) + elif co_old.is_Rational: + return rv + + # break self and old into factors + + (c, nc) = breakup(self2) + (old_c, old_nc) = breakup(old) + + # update the coefficients if we had an extraction + # e.g. if co_self were 2*(3/35*x)**2 and co_old = 3/5 + # then co_self in c is replaced by (3/5)**2 and co_residual + # is 2*(1/7)**2 + + if co_xmul and co_xmul.is_Rational and abs(co_old) != 1: + mult = S(multiplicity(abs(co_old), co_self)) + c.pop(co_self) + if co_old in c: + c[co_old] += mult + else: + c[co_old] = mult + co_residual = co_self/co_old**mult + else: + co_residual = 1 + + # do quick tests to see if we can't succeed + + ok = True + if len(old_nc) > len(nc): + # more non-commutative terms + ok = False + elif len(old_c) > len(c): + # more commutative terms + ok = False + elif {i[0] for i in old_nc}.difference({i[0] for i in nc}): + # unmatched non-commutative bases + ok = False + elif set(old_c).difference(set(c)): + # unmatched commutative terms + ok = False + elif any(sign(c[b]) != sign(old_c[b]) for b in old_c): + # differences in sign + ok = False + if not ok: + return rv + + if not old_c: + cdid = None + else: + rat = [] + for (b, old_e) in old_c.items(): + c_e = c[b] + rat.append(ndiv(c_e, old_e)) + if not rat[-1]: + return rv + cdid = min(rat) + + if not old_nc: + ncdid = None + for i in range(len(nc)): + nc[i] = rejoin(*nc[i]) + else: + ncdid = 0 # number of nc replacements we did + take = len(old_nc) # how much to look at each time + limit = cdid or S.Infinity # max number that we can take + failed = [] # failed terms will need subs if other terms pass + i = 0 + while limit and i + take <= len(nc): + hit = False + + # the bases must be equivalent in succession, and + # the powers must be extractively compatible on the + # first and last factor but equal in between. + + rat = [] + for j in range(take): + if nc[i + j][0] != old_nc[j][0]: + break + elif j == 0: + rat.append(ndiv(nc[i + j][1], old_nc[j][1])) + elif j == take - 1: + rat.append(ndiv(nc[i + j][1], old_nc[j][1])) + elif nc[i + j][1] != old_nc[j][1]: + break + else: + rat.append(1) + j += 1 + else: + ndo = min(rat) + if ndo: + if take == 1: + if cdid: + ndo = min(cdid, ndo) + nc[i] = Pow(new, ndo)*rejoin(nc[i][0], + nc[i][1] - ndo*old_nc[0][1]) + else: + ndo = 1 + + # the left residual + + l = rejoin(nc[i][0], nc[i][1] - ndo* + old_nc[0][1]) + + # eliminate all middle terms + + mid = new + + # the right residual (which may be the same as the middle if take == 2) + + ir = i + take - 1 + r = (nc[ir][0], nc[ir][1] - ndo* + old_nc[-1][1]) + if r[1]: + if i + take < len(nc): + nc[i:i + take] = [l*mid, r] + else: + r = rejoin(*r) + nc[i:i + take] = [l*mid*r] + else: + + # there was nothing left on the right + + nc[i:i + take] = [l*mid] + + limit -= ndo + ncdid += ndo + hit = True + if not hit: + + # do the subs on this failing factor + + failed.append(i) + i += 1 + else: + + if not ncdid: + return rv + + # although we didn't fail, certain nc terms may have + # failed so we rebuild them after attempting a partial + # subs on them + + failed.extend(range(i, len(nc))) + for i in failed: + nc[i] = rejoin(*nc[i]).subs(old, new) + + # rebuild the expression + + if cdid is None: + do = ncdid + elif ncdid is None: + do = cdid + else: + do = min(ncdid, cdid) + + margs = [] + for b in c: + if b in old_c: + + # calculate the new exponent + + e = c[b] - old_c[b]*do + margs.append(rejoin(b, e)) + else: + margs.append(rejoin(b.subs(old, new), c[b])) + if cdid and not ncdid: + + # in case we are replacing commutative with non-commutative, + # we want the new term to come at the front just like the + # rest of this routine + + margs = [Pow(new, cdid)] + margs + return co_residual*self2.func(*margs)*self2.func(*nc) + + def _eval_nseries(self, x, n, logx, cdir=0): + from .function import PoleError + from sympy.functions.elementary.integers import ceiling + from sympy.series.order import Order + + def coeff_exp(term, x): + lt = term.as_coeff_exponent(x) + if lt[0].has(x): + try: + lt = term.leadterm(x) + except ValueError: + return term, S.Zero + return lt + + ords = [] + + try: + for t in self.args: + coeff, exp = t.leadterm(x) + if not coeff.has(x): + ords.append((t, exp)) + else: + raise ValueError + + n0 = sum(t[1] for t in ords if t[1].is_number) + facs = [] + for t, m in ords: + n1 = ceiling(n - n0 + (m if m.is_number else 0)) + s = t.nseries(x, n=n1, logx=logx, cdir=cdir) + ns = s.getn() + if ns is not None: + if ns < n1: # less than expected + n -= n1 - ns # reduce n + facs.append(s) + + except (ValueError, NotImplementedError, TypeError, PoleError): + # XXX: Catching so many generic exceptions around a large block of + # code will mask bugs. Whatever purpose catching these exceptions + # serves should be handled in a different way. + n0 = sympify(sum(t[1] for t in ords if t[1].is_number)) + if n0.is_nonnegative: + n0 = S.Zero + facs = [t.nseries(x, n=ceiling(n-n0), logx=logx, cdir=cdir) for t in self.args] + from sympy.simplify.powsimp import powsimp + res = powsimp(self.func(*facs).expand(), combine='exp', deep=True) + if res.has(Order): + res += Order(x**n, x) + return res + + res = S.Zero + ords2 = [Add.make_args(factor) for factor in facs] + + for fac in product(*ords2): + ords3 = [coeff_exp(term, x) for term in fac] + coeffs, powers = zip(*ords3) + power = sum(powers) + if (power - n).is_negative: + res += Mul(*coeffs)*(x**power) + + def max_degree(e, x): + if e is x: + return S.One + if e.is_Atom: + return S.Zero + if e.is_Add: + return max(max_degree(a, x) for a in e.args) + if e.is_Mul: + return Add(*[max_degree(a, x) for a in e.args]) + if e.is_Pow: + return max_degree(e.base, x)*e.exp + return S.Zero + + if self.is_polynomial(x): + from sympy.polys.polyerrors import PolynomialError + from sympy.polys.polytools import degree + try: + if max_degree(self, x) >= n or degree(self, x) != degree(res, x): + res += Order(x**n, x) + except PolynomialError: + pass + else: + return res + + if res != self: + if (self - res).subs(x, 0) == S.Zero and n > 0: + lt = self._eval_as_leading_term(x, logx=logx, cdir=cdir) + if lt == S.Zero: + return res + res += Order(x**n, x) + return res + + def _eval_as_leading_term(self, x, logx, cdir): + return self.func(*[t.as_leading_term(x, logx=logx, cdir=cdir) for t in self.args]) + + def _eval_conjugate(self): + return self.func(*[t.conjugate() for t in self.args]) + + def _eval_transpose(self): + return self.func(*[t.transpose() for t in self.args[::-1]]) + + def _eval_adjoint(self): + return self.func(*[t.adjoint() for t in self.args[::-1]]) + + def as_content_primitive(self, radical=False, clear=True): + """Return the tuple (R, self/R) where R is the positive Rational + extracted from self. + + Examples + ======== + + >>> from sympy import sqrt + >>> (-3*sqrt(2)*(2 - 2*sqrt(2))).as_content_primitive() + (6, -sqrt(2)*(1 - sqrt(2))) + + See docstring of Expr.as_content_primitive for more examples. + """ + + coef = S.One + args = [] + for a in self.args: + c, p = a.as_content_primitive(radical=radical, clear=clear) + coef *= c + if p is not S.One: + args.append(p) + # don't use self._from_args here to reconstruct args + # since there may be identical args now that should be combined + # e.g. (2+2*x)*(3+3*x) should be (6, (1 + x)**2) not (6, (1+x)*(1+x)) + return coef, self.func(*args) + + def as_ordered_factors(self, order=None): + """Transform an expression into an ordered list of factors. + + Examples + ======== + + >>> from sympy import sin, cos + >>> from sympy.abc import x, y + + >>> (2*x*y*sin(x)*cos(x)).as_ordered_factors() + [2, x, y, sin(x), cos(x)] + + """ + cpart, ncpart = self.args_cnc() + cpart.sort(key=lambda expr: expr.sort_key(order=order)) + return cpart + ncpart + + @property + def _sorted_args(self): + return tuple(self.as_ordered_factors()) + +mul = AssocOpDispatcher('mul') + + +def prod(a, start=1): + """Return product of elements of a. Start with int 1 so if only + ints are included then an int result is returned. + + Examples + ======== + + >>> from sympy import prod, S + >>> prod(range(3)) + 0 + >>> type(_) is int + True + >>> prod([S(2), 3]) + 6 + >>> _.is_Integer + True + + You can start the product at something other than 1: + + >>> prod([1, 2], 3) + 6 + + """ + return reduce(operator.mul, a, start) + + +def _keep_coeff(coeff, factors, clear=True, sign=False): + """Return ``coeff*factors`` unevaluated if necessary. + + If ``clear`` is False, do not keep the coefficient as a factor + if it can be distributed on a single factor such that one or + more terms will still have integer coefficients. + + If ``sign`` is True, allow a coefficient of -1 to remain factored out. + + Examples + ======== + + >>> from sympy.core.mul import _keep_coeff + >>> from sympy.abc import x, y + >>> from sympy import S + + >>> _keep_coeff(S.Half, x + 2) + (x + 2)/2 + >>> _keep_coeff(S.Half, x + 2, clear=False) + x/2 + 1 + >>> _keep_coeff(S.Half, (x + 2)*y, clear=False) + y*(x + 2)/2 + >>> _keep_coeff(S(-1), x + y) + -x - y + >>> _keep_coeff(S(-1), x + y, sign=True) + -(x + y) + """ + if not coeff.is_Number: + if factors.is_Number: + factors, coeff = coeff, factors + else: + return coeff*factors + if factors is S.One: + return coeff + if coeff is S.One: + return factors + elif coeff is S.NegativeOne and not sign: + return -factors + elif factors.is_Add: + if not clear and coeff.is_Rational and coeff.q != 1: + args = [i.as_coeff_Mul() for i in factors.args] + args = [(_keep_coeff(c, coeff), m) for c, m in args] + if any(c.is_Integer for c, _ in args): + return Add._from_args([Mul._from_args( + i[1:] if i[0] == 1 else i) for i in args]) + return Mul(coeff, factors, evaluate=False) + elif factors.is_Mul: + margs = list(factors.args) + if margs[0].is_Number: + margs[0] *= coeff + if margs[0] == 1: + margs.pop(0) + else: + margs.insert(0, coeff) + return Mul._from_args(margs) + else: + m = coeff*factors + if m.is_Number and not factors.is_Number: + m = Mul._from_args((coeff, factors)) + return m + +def expand_2arg(e): + def do(e): + if e.is_Mul: + c, r = e.as_coeff_Mul() + if c.is_Number and r.is_Add: + return _unevaluated_Add(*[c*ri for ri in r.args]) + return e + return bottom_up(e, do) + + +from .numbers import Rational +from .power import Pow +from .add import Add, _unevaluated_Add diff --git a/venv/lib/python3.11/site-packages/sympy/core/multidimensional.py b/venv/lib/python3.11/site-packages/sympy/core/multidimensional.py new file mode 100644 index 0000000000000000000000000000000000000000..133e0ab6cba6a87c627feb6f6034a6daed1128c5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/multidimensional.py @@ -0,0 +1,131 @@ +""" +Provides functionality for multidimensional usage of scalar-functions. + +Read the vectorize docstring for more details. +""" + +from functools import wraps + + +def apply_on_element(f, args, kwargs, n): + """ + Returns a structure with the same dimension as the specified argument, + where each basic element is replaced by the function f applied on it. All + other arguments stay the same. + """ + # Get the specified argument. + if isinstance(n, int): + structure = args[n] + is_arg = True + elif isinstance(n, str): + structure = kwargs[n] + is_arg = False + + # Define reduced function that is only dependent on the specified argument. + def f_reduced(x): + if hasattr(x, "__iter__"): + return list(map(f_reduced, x)) + else: + if is_arg: + args[n] = x + else: + kwargs[n] = x + return f(*args, **kwargs) + + # f_reduced will call itself recursively so that in the end f is applied to + # all basic elements. + return list(map(f_reduced, structure)) + + +def iter_copy(structure): + """ + Returns a copy of an iterable object (also copying all embedded iterables). + """ + return [iter_copy(i) if hasattr(i, "__iter__") else i for i in structure] + + +def structure_copy(structure): + """ + Returns a copy of the given structure (numpy-array, list, iterable, ..). + """ + if hasattr(structure, "copy"): + return structure.copy() + return iter_copy(structure) + + +class vectorize: + """ + Generalizes a function taking scalars to accept multidimensional arguments. + + Examples + ======== + + >>> from sympy import vectorize, diff, sin, symbols, Function + >>> x, y, z = symbols('x y z') + >>> f, g, h = list(map(Function, 'fgh')) + + >>> @vectorize(0) + ... def vsin(x): + ... return sin(x) + + >>> vsin([1, x, y]) + [sin(1), sin(x), sin(y)] + + >>> @vectorize(0, 1) + ... def vdiff(f, y): + ... return diff(f, y) + + >>> vdiff([f(x, y, z), g(x, y, z), h(x, y, z)], [x, y, z]) + [[Derivative(f(x, y, z), x), Derivative(f(x, y, z), y), Derivative(f(x, y, z), z)], [Derivative(g(x, y, z), x), Derivative(g(x, y, z), y), Derivative(g(x, y, z), z)], [Derivative(h(x, y, z), x), Derivative(h(x, y, z), y), Derivative(h(x, y, z), z)]] + """ + def __init__(self, *mdargs): + """ + The given numbers and strings characterize the arguments that will be + treated as data structures, where the decorated function will be applied + to every single element. + If no argument is given, everything is treated multidimensional. + """ + for a in mdargs: + if not isinstance(a, (int, str)): + raise TypeError("a is of invalid type") + self.mdargs = mdargs + + def __call__(self, f): + """ + Returns a wrapper for the one-dimensional function that can handle + multidimensional arguments. + """ + @wraps(f) + def wrapper(*args, **kwargs): + # Get arguments that should be treated multidimensional + if self.mdargs: + mdargs = self.mdargs + else: + mdargs = range(len(args)) + kwargs.keys() + + arglength = len(args) + + for n in mdargs: + if isinstance(n, int): + if n >= arglength: + continue + entry = args[n] + is_arg = True + elif isinstance(n, str): + try: + entry = kwargs[n] + except KeyError: + continue + is_arg = False + if hasattr(entry, "__iter__"): + # Create now a copy of the given array and manipulate then + # the entries directly. + if is_arg: + args = list(args) + args[n] = structure_copy(entry) + else: + kwargs[n] = structure_copy(entry) + result = apply_on_element(wrapper, args, kwargs, n) + return result + return f(*args, **kwargs) + return wrapper diff --git a/venv/lib/python3.11/site-packages/sympy/core/numbers.py b/venv/lib/python3.11/site-packages/sympy/core/numbers.py new file mode 100644 index 0000000000000000000000000000000000000000..9fa13fbb96aa25a8e60e048c0147a5e660804ccc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/numbers.py @@ -0,0 +1,4482 @@ +from __future__ import annotations + +from typing import overload + +import numbers +import decimal +import fractions +import math + +from .containers import Tuple +from .sympify import (SympifyError, _sympy_converter, sympify, _convert_numpy_types, + _sympify, _is_numpy_instance) +from .singleton import S, Singleton +from .basic import Basic +from .expr import Expr, AtomicExpr +from .evalf import pure_complex +from .cache import cacheit, clear_cache +from .decorators import _sympifyit +from .intfunc import num_digits, igcd, ilcm, mod_inverse, integer_nthroot +from .logic import fuzzy_not +from .kind import NumberKind +from .sorting import ordered +from sympy.external.gmpy import SYMPY_INTS, gmpy, flint +from sympy.multipledispatch import dispatch +import mpmath +import mpmath.libmp as mlib +from mpmath.libmp import bitcount, round_nearest as rnd +from mpmath.libmp.backend import MPZ +from mpmath.libmp import mpf_pow, mpf_pi, mpf_e, phi_fixed +from mpmath.ctx_mp_python import mpnumeric +from mpmath.libmp.libmpf import ( + finf as _mpf_inf, fninf as _mpf_ninf, + fnan as _mpf_nan, fzero, _normalize as mpf_normalize, + prec_to_dps, dps_to_prec) +from sympy.utilities.misc import debug +from sympy.utilities.exceptions import sympy_deprecation_warning +from .parameters import global_parameters + +_LOG2 = math.log(2) + + +def comp(z1, z2, tol=None): + r"""Return a bool indicating whether the error between z1 and z2 + is $\le$ ``tol``. + + Examples + ======== + + If ``tol`` is ``None`` then ``True`` will be returned if + :math:`|z1 - z2|\times 10^p \le 5` where $p$ is minimum value of the + decimal precision of each value. + + >>> from sympy import comp, pi + >>> pi4 = pi.n(4); pi4 + 3.142 + >>> comp(_, 3.142) + True + >>> comp(pi4, 3.141) + False + >>> comp(pi4, 3.143) + False + + A comparison of strings will be made + if ``z1`` is a Number and ``z2`` is a string or ``tol`` is ''. + + >>> comp(pi4, 3.1415) + True + >>> comp(pi4, 3.1415, '') + False + + When ``tol`` is provided and $z2$ is non-zero and + :math:`|z1| > 1` the error is normalized by :math:`|z1|`: + + >>> abs(pi4 - 3.14)/pi4 + 0.000509791731426756 + >>> comp(pi4, 3.14, .001) # difference less than 0.1% + True + >>> comp(pi4, 3.14, .0005) # difference less than 0.1% + False + + When :math:`|z1| \le 1` the absolute error is used: + + >>> 1/pi4 + 0.3183 + >>> abs(1/pi4 - 0.3183)/(1/pi4) + 3.07371499106316e-5 + >>> abs(1/pi4 - 0.3183) + 9.78393554684764e-6 + >>> comp(1/pi4, 0.3183, 1e-5) + True + + To see if the absolute error between ``z1`` and ``z2`` is less + than or equal to ``tol``, call this as ``comp(z1 - z2, 0, tol)`` + or ``comp(z1 - z2, tol=tol)``: + + >>> abs(pi4 - 3.14) + 0.00160156249999988 + >>> comp(pi4 - 3.14, 0, .002) + True + >>> comp(pi4 - 3.14, 0, .001) + False + """ + if isinstance(z2, str): + if not pure_complex(z1, or_real=True): + raise ValueError('when z2 is a str z1 must be a Number') + return str(z1) == z2 + if not z1: + z1, z2 = z2, z1 + if not z1: + return True + if not tol: + a, b = z1, z2 + if tol == '': + return str(a) == str(b) + if tol is None: + a, b = sympify(a), sympify(b) + if not all(i.is_number for i in (a, b)): + raise ValueError('expecting 2 numbers') + fa = a.atoms(Float) + fb = b.atoms(Float) + if not fa and not fb: + # no floats -- compare exactly + return a == b + # get a to be pure_complex + for _ in range(2): + ca = pure_complex(a, or_real=True) + if not ca: + if fa: + a = a.n(prec_to_dps(min(i._prec for i in fa))) + ca = pure_complex(a, or_real=True) + break + else: + fa, fb = fb, fa + a, b = b, a + cb = pure_complex(b) + if not cb and fb: + b = b.n(prec_to_dps(min(i._prec for i in fb))) + cb = pure_complex(b, or_real=True) + if ca and cb and (ca[1] or cb[1]): + return all(comp(i, j) for i, j in zip(ca, cb)) + tol = 10**prec_to_dps(min(a._prec, getattr(b, '_prec', a._prec))) + return int(abs(a - b)*tol) <= 5 + diff = abs(z1 - z2) + az1 = abs(z1) + if z2 and az1 > 1: + return diff/az1 <= tol + else: + return diff <= tol + + +def mpf_norm(mpf, prec): + """Return the mpf tuple normalized appropriately for the indicated + precision after doing a check to see if zero should be returned or + not when the mantissa is 0. ``mpf_normlize`` always assumes that this + is zero, but it may not be since the mantissa for mpf's values "+inf", + "-inf" and "nan" have a mantissa of zero, too. + + Note: this is not intended to validate a given mpf tuple, so sending + mpf tuples that were not created by mpmath may produce bad results. This + is only a wrapper to ``mpf_normalize`` which provides the check for non- + zero mpfs that have a 0 for the mantissa. + """ + sign, man, expt, bc = mpf + if not man: + # hack for mpf_normalize which does not do this; + # it assumes that if man is zero the result is 0 + # (see issue 6639) + if not bc: + return fzero + else: + # don't change anything; this should already + # be a well formed mpf tuple + return mpf + + # Necessary if mpmath is using the gmpy backend + from mpmath.libmp.backend import MPZ + rv = mpf_normalize(sign, MPZ(man), expt, bc, prec, rnd) + return rv + +# TODO: we should use the warnings module +_errdict = {"divide": False} + + +def seterr(divide=False): + """ + Should SymPy raise an exception on 0/0 or return a nan? + + divide == True .... raise an exception + divide == False ... return nan + """ + if _errdict["divide"] != divide: + clear_cache() + _errdict["divide"] = divide + + +def _as_integer_ratio(p): + neg_pow, man, expt, _ = getattr(p, '_mpf_', mpmath.mpf(p)._mpf_) + p = [1, -1][neg_pow % 2]*man + if expt < 0: + q = 2**-expt + else: + q = 1 + p *= 2**expt + return int(p), int(q) + + +def _decimal_to_Rational_prec(dec): + """Convert an ordinary decimal instance to a Rational.""" + if not dec.is_finite(): + raise TypeError("dec must be finite, got %s." % dec) + s, d, e = dec.as_tuple() + prec = len(d) + if e >= 0: # it's an integer + rv = Integer(int(dec)) + else: + s = (-1)**s + d = sum(di*10**i for i, di in enumerate(reversed(d))) + rv = Rational(s*d, 10**-e) + return rv, prec + +_dig = str.maketrans(dict.fromkeys('1234567890')) + +def _literal_float(s): + """return True if s is space-trimmed number literal else False + + Python allows underscore as digit separators: there must be a + digit on each side. So neither a leading underscore nor a + double underscore are valid as part of a number. A number does + not have to precede the decimal point, but there must be a + digit before the optional "e" or "E" that begins the signs + exponent of the number which must be an integer, perhaps with + underscore separators. + + SymPy allows space as a separator; if the calling routine replaces + them with underscores then the same semantics will be enforced + for them as for underscores: there can only be 1 *between* digits. + + We don't check for error from float(s) because we don't know + whether s is malicious or not. A regex for this could maybe + be written but will it be understood by most who read it? + """ + # mantissa and exponent + parts = s.split('e') + if len(parts) > 2: + return False + if len(parts) == 2: + m, e = parts + if e.startswith(tuple('+-')): + e = e[1:] + if not e: + return False + else: + m, e = s, '1' + # integer and fraction of mantissa + parts = m.split('.') + if len(parts) > 2: + return False + elif len(parts) == 2: + i, f = parts + else: + i, f = m, '1' + if not i and not f: + return False + if i and i[0] in '+-': + i = i[1:] + if not i: # -.3e4 -> -0.3e4 + i = '1' + f = f or '1' + # check that all groups contain only digits and are not null + for n in (i, f, e): + for g in n.split('_'): + if not g or g.translate(_dig): + return False + return True + +# (a,b) -> gcd(a,b) + +# TODO caching with decorator, but not to degrade performance + + +class Number(AtomicExpr): + """Represents atomic numbers in SymPy. + + Explanation + =========== + + Floating point numbers are represented by the Float class. + Rational numbers (of any size) are represented by the Rational class. + Integer numbers (of any size) are represented by the Integer class. + Float and Rational are subclasses of Number; Integer is a subclass + of Rational. + + For example, ``2/3`` is represented as ``Rational(2, 3)`` which is + a different object from the floating point number obtained with + Python division ``2/3``. Even for numbers that are exactly + represented in binary, there is a difference between how two forms, + such as ``Rational(1, 2)`` and ``Float(0.5)``, are used in SymPy. + The rational form is to be preferred in symbolic computations. + + Other kinds of numbers, such as algebraic numbers ``sqrt(2)`` or + complex numbers ``3 + 4*I``, are not instances of Number class as + they are not atomic. + + See Also + ======== + + Float, Integer, Rational + """ + is_commutative = True + is_number = True + is_Number = True + + __slots__ = () + + # Used to make max(x._prec, y._prec) return x._prec when only x is a float + _prec = -1 + + kind = NumberKind + + def __new__(cls, *obj): + if len(obj) == 1: + obj = obj[0] + + if isinstance(obj, Number): + return obj + if isinstance(obj, SYMPY_INTS): + return Integer(obj) + if isinstance(obj, tuple) and len(obj) == 2: + return Rational(*obj) + if isinstance(obj, (float, mpmath.mpf, decimal.Decimal)): + return Float(obj) + if isinstance(obj, str): + _obj = obj.lower() # float('INF') == float('inf') + if _obj == 'nan': + return S.NaN + elif _obj == 'inf': + return S.Infinity + elif _obj == '+inf': + return S.Infinity + elif _obj == '-inf': + return S.NegativeInfinity + val = sympify(obj) + if isinstance(val, Number): + return val + else: + raise ValueError('String "%s" does not denote a Number' % obj) + msg = "expected str|int|long|float|Decimal|Number object but got %r" + raise TypeError(msg % type(obj).__name__) + + def could_extract_minus_sign(self): + return bool(self.is_extended_negative) + + def invert(self, other, *gens, **args): + from sympy.polys.polytools import invert + if getattr(other, 'is_number', True): + return mod_inverse(self, other) + return invert(self, other, *gens, **args) + + def __divmod__(self, other): + from sympy.functions.elementary.complexes import sign + + try: + other = Number(other) + if self.is_infinite or S.NaN in (self, other): + return (S.NaN, S.NaN) + except TypeError: + return NotImplemented + if not other: + raise ZeroDivisionError('modulo by zero') + if self.is_Integer and other.is_Integer: + return Tuple(*divmod(self.p, other.p)) + elif isinstance(other, Float): + rat = self/Rational(other) + else: + rat = self/other + if other.is_finite: + w = int(rat) if rat >= 0 else int(rat) - 1 + r = self - other*w + if r == Float(other): + w += 1 + r = 0 + if isinstance(self, Float) or isinstance(other, Float): + r = Float(r) # in case w or r is 0 + else: + w = 0 if not self or (sign(self) == sign(other)) else -1 + r = other if w else self + return Tuple(w, r) + + def __rdivmod__(self, other): + try: + other = Number(other) + except TypeError: + return NotImplemented + return divmod(other, self) + + def _as_mpf_val(self, prec): + """Evaluation of mpf tuple accurate to at least prec bits.""" + raise NotImplementedError('%s needs ._as_mpf_val() method' % + (self.__class__.__name__)) + + def _eval_evalf(self, prec): + return Float._new(self._as_mpf_val(prec), prec) + + def _as_mpf_op(self, prec): + prec = max(prec, self._prec) + return self._as_mpf_val(prec), prec + + def __float__(self): + return mlib.to_float(self._as_mpf_val(53)) + + def floor(self): + raise NotImplementedError('%s needs .floor() method' % + (self.__class__.__name__)) + + def ceiling(self): + raise NotImplementedError('%s needs .ceiling() method' % + (self.__class__.__name__)) + + def __floor__(self): + return self.floor() + + def __ceil__(self): + return self.ceiling() + + def _eval_conjugate(self): + return self + + def _eval_order(self, *symbols): + from sympy.series.order import Order + # Order(5, x, y) -> Order(1,x,y) + return Order(S.One, *symbols) + + def _eval_subs(self, old, new): + if old == -self: + return -new + return self # there is no other possibility + + @classmethod + def class_key(cls): + return 1, 0, 'Number' + + @cacheit + def sort_key(self, order=None): + return self.class_key(), (0, ()), (), self + + def __neg__(self) -> Number: + raise NotImplementedError + + @overload + def __add__(self, other: Number | int | float) -> Number: ... + @overload + def __add__(self, other: Expr) -> Expr: ... + + @_sympifyit('other', NotImplemented) + def __add__(self, other) -> Expr: + if isinstance(other, Number) and global_parameters.evaluate: + if other is S.NaN: + return S.NaN + elif other is S.Infinity: + return S.Infinity + elif other is S.NegativeInfinity: + return S.NegativeInfinity + return AtomicExpr.__add__(self, other) + + @_sympifyit('other', NotImplemented) + def __sub__(self, other): + if isinstance(other, Number) and global_parameters.evaluate: + if other is S.NaN: + return S.NaN + elif other is S.Infinity: + return S.NegativeInfinity + elif other is S.NegativeInfinity: + return S.Infinity + return AtomicExpr.__sub__(self, other) + + @_sympifyit('other', NotImplemented) + def __mul__(self, other): + if isinstance(other, Number) and global_parameters.evaluate: + if other is S.NaN: + return S.NaN + elif other is S.Infinity: + if self.is_zero: + return S.NaN + elif self.is_positive: + return S.Infinity + else: + return S.NegativeInfinity + elif other is S.NegativeInfinity: + if self.is_zero: + return S.NaN + elif self.is_positive: + return S.NegativeInfinity + else: + return S.Infinity + elif isinstance(other, Tuple): + return NotImplemented + return AtomicExpr.__mul__(self, other) + + @_sympifyit('other', NotImplemented) + def __truediv__(self, other): + if isinstance(other, Number) and global_parameters.evaluate: + if other is S.NaN: + return S.NaN + elif other in (S.Infinity, S.NegativeInfinity): + return S.Zero + return AtomicExpr.__truediv__(self, other) + + def __eq__(self, other): + raise NotImplementedError('%s needs .__eq__() method' % + (self.__class__.__name__)) + + def __ne__(self, other): + raise NotImplementedError('%s needs .__ne__() method' % + (self.__class__.__name__)) + + def __lt__(self, other): + try: + other = _sympify(other) + except SympifyError: + raise TypeError("Invalid comparison %s < %s" % (self, other)) + raise NotImplementedError('%s needs .__lt__() method' % + (self.__class__.__name__)) + + def __le__(self, other): + try: + other = _sympify(other) + except SympifyError: + raise TypeError("Invalid comparison %s <= %s" % (self, other)) + raise NotImplementedError('%s needs .__le__() method' % + (self.__class__.__name__)) + + def __gt__(self, other): + try: + other = _sympify(other) + except SympifyError: + raise TypeError("Invalid comparison %s > %s" % (self, other)) + return _sympify(other).__lt__(self) + + def __ge__(self, other): + try: + other = _sympify(other) + except SympifyError: + raise TypeError("Invalid comparison %s >= %s" % (self, other)) + return _sympify(other).__le__(self) + + def __hash__(self): + return super().__hash__() + + def is_constant(self, *wrt, **flags): + return True + + def as_coeff_mul(self, *deps, rational=True, **kwargs): + # a -> c*t + if self.is_Rational or not rational: + return self, () + elif self.is_negative: + return S.NegativeOne, (-self,) + return S.One, (self,) + + def as_coeff_add(self, *deps): + # a -> c + t + if self.is_Rational: + return self, () + return S.Zero, (self,) + + def as_coeff_Mul(self, rational=False): + """Efficiently extract the coefficient of a product.""" + if not rational: + return self, S.One + return S.One, self + + def as_coeff_Add(self, rational=False): + """Efficiently extract the coefficient of a summation.""" + if not rational: + return self, S.Zero + return S.Zero, self + + def gcd(self, other): + """Compute GCD of `self` and `other`. """ + from sympy.polys.polytools import gcd + return gcd(self, other) + + def lcm(self, other): + """Compute LCM of `self` and `other`. """ + from sympy.polys.polytools import lcm + return lcm(self, other) + + def cofactors(self, other): + """Compute GCD and cofactors of `self` and `other`. """ + from sympy.polys.polytools import cofactors + return cofactors(self, other) + + +class Float(Number): + """Represent a floating-point number of arbitrary precision. + + Examples + ======== + + >>> from sympy import Float + >>> Float(3.5) + 3.50000000000000 + >>> Float(3) + 3.00000000000000 + + Creating Floats from strings (and Python ``int`` and ``long`` + types) will give a minimum precision of 15 digits, but the + precision will automatically increase to capture all digits + entered. + + >>> Float(1) + 1.00000000000000 + >>> Float(10**20) + 100000000000000000000. + >>> Float('1e20') + 100000000000000000000. + + However, *floating-point* numbers (Python ``float`` types) retain + only 15 digits of precision: + + >>> Float(1e20) + 1.00000000000000e+20 + >>> Float(1.23456789123456789) + 1.23456789123457 + + It may be preferable to enter high-precision decimal numbers + as strings: + + >>> Float('1.23456789123456789') + 1.23456789123456789 + + The desired number of digits can also be specified: + + >>> Float('1e-3', 3) + 0.00100 + >>> Float(100, 4) + 100.0 + + Float can automatically count significant figures if a null string + is sent for the precision; spaces or underscores are also allowed. (Auto- + counting is only allowed for strings, ints and longs). + + >>> Float('123 456 789.123_456', '') + 123456789.123456 + >>> Float('12e-3', '') + 0.012 + >>> Float(3, '') + 3. + + If a number is written in scientific notation, only the digits before the + exponent are considered significant if a decimal appears, otherwise the + "e" signifies only how to move the decimal: + + >>> Float('60.e2', '') # 2 digits significant + 6.0e+3 + >>> Float('60e2', '') # 4 digits significant + 6000. + >>> Float('600e-2', '') # 3 digits significant + 6.00 + + Notes + ===== + + Floats are inexact by their nature unless their value is a binary-exact + value. + + >>> approx, exact = Float(.1, 1), Float(.125, 1) + + For calculation purposes, evalf needs to be able to change the precision + but this will not increase the accuracy of the inexact value. The + following is the most accurate 5-digit approximation of a value of 0.1 + that had only 1 digit of precision: + + >>> approx.evalf(5) + 0.099609 + + By contrast, 0.125 is exact in binary (as it is in base 10) and so it + can be passed to Float or evalf to obtain an arbitrary precision with + matching accuracy: + + >>> Float(exact, 5) + 0.12500 + >>> exact.evalf(20) + 0.12500000000000000000 + + Trying to make a high-precision Float from a float is not disallowed, + but one must keep in mind that the *underlying float* (not the apparent + decimal value) is being obtained with high precision. For example, 0.3 + does not have a finite binary representation. The closest rational is + the fraction 5404319552844595/2**54. So if you try to obtain a Float of + 0.3 to 20 digits of precision you will not see the same thing as 0.3 + followed by 19 zeros: + + >>> Float(0.3, 20) + 0.29999999999999998890 + + If you want a 20-digit value of the decimal 0.3 (not the floating point + approximation of 0.3) you should send the 0.3 as a string. The underlying + representation is still binary but a higher precision than Python's float + is used: + + >>> Float('0.3', 20) + 0.30000000000000000000 + + Although you can increase the precision of an existing Float using Float + it will not increase the accuracy -- the underlying value is not changed: + + >>> def show(f): # binary rep of Float + ... from sympy import Mul, Pow + ... s, m, e, b = f._mpf_ + ... v = Mul(int(m), Pow(2, int(e), evaluate=False), evaluate=False) + ... print('%s at prec=%s' % (v, f._prec)) + ... + >>> t = Float('0.3', 3) + >>> show(t) + 4915/2**14 at prec=13 + >>> show(Float(t, 20)) # higher prec, not higher accuracy + 4915/2**14 at prec=70 + >>> show(Float(t, 2)) # lower prec + 307/2**10 at prec=10 + + The same thing happens when evalf is used on a Float: + + >>> show(t.evalf(20)) + 4915/2**14 at prec=70 + >>> show(t.evalf(2)) + 307/2**10 at prec=10 + + Finally, Floats can be instantiated with an mpf tuple (n, c, p) to + produce the number (-1)**n*c*2**p: + + >>> n, c, p = 1, 5, 0 + >>> (-1)**n*c*2**p + -5 + >>> Float((1, 5, 0)) + -5.00000000000000 + + An actual mpf tuple also contains the number of bits in c as the last + element of the tuple: + + >>> _._mpf_ + (1, 5, 0, 3) + + This is not needed for instantiation and is not the same thing as the + precision. The mpf tuple and the precision are two separate quantities + that Float tracks. + + In SymPy, a Float is a number that can be computed with arbitrary + precision. Although floating point 'inf' and 'nan' are not such + numbers, Float can create these numbers: + + >>> Float('-inf') + -oo + >>> _.is_Float + False + + Zero in Float only has a single value. Values are not separate for + positive and negative zeroes. + """ + __slots__ = ('_mpf_', '_prec') + + _mpf_: tuple[int, int, int, int] + + # A Float, though rational in form, does not behave like + # a rational in all Python expressions so we deal with + # exceptions (where we want to deal with the rational + # form of the Float as a rational) at the source rather + # than assigning a mathematically loaded category of 'rational' + is_rational = None + is_irrational = None + is_number = True + + is_real = True + is_extended_real = True + + is_Float = True + + _remove_non_digits = str.maketrans(dict.fromkeys("-+_.")) + + def __new__(cls, num, dps=None, precision=None): + if dps is not None and precision is not None: + raise ValueError('Both decimal and binary precision supplied. ' + 'Supply only one. ') + + if isinstance(num, str): + _num = num = num.strip() # Python ignores leading and trailing space + num = num.replace(' ', '_').lower() # Float treats spaces as digit sep; E -> e + if num.startswith('.') and len(num) > 1: + num = '0' + num + elif num.startswith('-.') and len(num) > 2: + num = '-0.' + num[2:] + elif num in ('inf', '+inf'): + return S.Infinity + elif num == '-inf': + return S.NegativeInfinity + elif num == 'nan': + return S.NaN + elif not _literal_float(num): + raise ValueError('string-float not recognized: %s' % _num) + elif isinstance(num, float) and num == 0: + num = '0' + elif isinstance(num, float) and num == float('inf'): + return S.Infinity + elif isinstance(num, float) and num == float('-inf'): + return S.NegativeInfinity + elif isinstance(num, float) and math.isnan(num): + return S.NaN + elif isinstance(num, (SYMPY_INTS, Integer)): + num = str(num) + elif num is S.Infinity: + return num + elif num is S.NegativeInfinity: + return num + elif num is S.NaN: + return num + elif _is_numpy_instance(num): # support for numpy datatypes + num = _convert_numpy_types(num) + elif isinstance(num, mpmath.mpf): + if precision is None: + if dps is None: + precision = num.context.prec + num = num._mpf_ + + if dps is None and precision is None: + dps = 15 + if isinstance(num, Float): + return num + if isinstance(num, str): + try: + Num = decimal.Decimal(num) + except decimal.InvalidOperation: + pass + else: + isint = '.' not in num + num, dps = _decimal_to_Rational_prec(Num) + if num.is_Integer and isint: + # 12e3 is shorthand for int, not float; + # 12.e3 would be the float version + dps = max(dps, num_digits(num)) + dps = max(15, dps) + precision = dps_to_prec(dps) + elif precision == '' and dps is None or precision is None and dps == '': + if not isinstance(num, str): + raise ValueError('The null string can only be used when ' + 'the number to Float is passed as a string or an integer.') + try: + Num = decimal.Decimal(num) + except decimal.InvalidOperation: + raise ValueError('string-float not recognized by Decimal: %s' % num) + else: + isint = '.' not in num + num, dps = _decimal_to_Rational_prec(Num) + if num.is_Integer and isint: + # without dec, e-notation is short for int + dps = max(dps, num_digits(num)) + precision = dps_to_prec(dps) + + # decimal precision(dps) is set and maybe binary precision(precision) + # as well.From here on binary precision is used to compute the Float. + # Hence, if supplied use binary precision else translate from decimal + # precision. + + if precision is None or precision == '': + precision = dps_to_prec(dps) + + precision = int(precision) + + if isinstance(num, float): + _mpf_ = mlib.from_float(num, precision, rnd) + elif isinstance(num, str): + _mpf_ = mlib.from_str(num, precision, rnd) + elif isinstance(num, decimal.Decimal): + if num.is_finite(): + _mpf_ = mlib.from_str(str(num), precision, rnd) + elif num.is_nan(): + return S.NaN + elif num.is_infinite(): + if num > 0: + return S.Infinity + return S.NegativeInfinity + else: + raise ValueError("unexpected decimal value %s" % str(num)) + elif isinstance(num, tuple) and len(num) in (3, 4): + if isinstance(num[1], str): + # it's a hexadecimal (coming from a pickled object) + num = list(num) + # If we're loading an object pickled in Python 2 into + # Python 3, we may need to strip a tailing 'L' because + # of a shim for int on Python 3, see issue #13470. + # Strip leading '0x' - gmpy2 only documents such inputs + # with base prefix as valid when the 2nd argument (base) is 0. + # When mpmath uses Sage as the backend, however, it + # ends up including '0x' when preparing the picklable tuple. + # See issue #19690. + num[1] = num[1].removeprefix('0x').removesuffix('L') + # Now we can assume that it is in standard form + num[1] = MPZ(num[1], 16) + _mpf_ = tuple(num) + else: + if len(num) == 4: + # handle normalization hack + return Float._new(num, precision) + else: + if not all(( + num[0] in (0, 1), + num[1] >= 0, + all(type(i) in (int, int) for i in num) + )): + raise ValueError('malformed mpf: %s' % (num,)) + # don't compute number or else it may + # over/underflow + return Float._new( + (num[0], num[1], num[2], bitcount(num[1])), + precision) + elif isinstance(num, (Number, NumberSymbol)): + _mpf_ = num._as_mpf_val(precision) + else: + _mpf_ = mpmath.mpf(num, prec=precision)._mpf_ + + return cls._new(_mpf_, precision, zero=False) + + @classmethod + def _new(cls, _mpf_, _prec, zero=True): + # special cases + if zero and _mpf_ == fzero: + return S.Zero # Float(0) -> 0.0; Float._new((0,0,0,0)) -> 0 + elif _mpf_ == _mpf_nan: + return S.NaN + elif _mpf_ == _mpf_inf: + return S.Infinity + elif _mpf_ == _mpf_ninf: + return S.NegativeInfinity + + obj = Expr.__new__(cls) + obj._mpf_ = mpf_norm(_mpf_, _prec) + obj._prec = _prec + return obj + + def __getnewargs_ex__(self): + sign, man, exp, bc = self._mpf_ + arg = (sign, hex(man)[2:], exp, bc) + kwargs = {'precision': self._prec} + return ((arg,), kwargs) + + def _hashable_content(self): + return (self._mpf_, self._prec) + + def floor(self): + return Integer(int(mlib.to_int( + mlib.mpf_floor(self._mpf_, self._prec)))) + + def ceiling(self): + return Integer(int(mlib.to_int( + mlib.mpf_ceil(self._mpf_, self._prec)))) + + def __floor__(self): + return self.floor() + + def __ceil__(self): + return self.ceiling() + + @property + def num(self): + return mpmath.mpf(self._mpf_) + + def _as_mpf_val(self, prec): + rv = mpf_norm(self._mpf_, prec) + if rv != self._mpf_ and self._prec == prec: + debug(self._mpf_, rv) + return rv + + def _as_mpf_op(self, prec): + return self._mpf_, max(prec, self._prec) + + def _eval_is_finite(self): + if self._mpf_ in (_mpf_inf, _mpf_ninf): + return False + return True + + def _eval_is_infinite(self): + if self._mpf_ in (_mpf_inf, _mpf_ninf): + return True + return False + + def _eval_is_integer(self): + if self._mpf_ == fzero: + return True + if not int_valued(self): + return False + + def _eval_is_negative(self): + if self._mpf_ in (_mpf_ninf, _mpf_inf): + return False + return self.num < 0 + + def _eval_is_positive(self): + if self._mpf_ in (_mpf_ninf, _mpf_inf): + return False + return self.num > 0 + + def _eval_is_extended_negative(self): + if self._mpf_ == _mpf_ninf: + return True + if self._mpf_ == _mpf_inf: + return False + return self.num < 0 + + def _eval_is_extended_positive(self): + if self._mpf_ == _mpf_inf: + return True + if self._mpf_ == _mpf_ninf: + return False + return self.num > 0 + + def _eval_is_zero(self): + return self._mpf_ == fzero + + def __bool__(self): + return self._mpf_ != fzero + + def __neg__(self): + if not self: + return self + return Float._new(mlib.mpf_neg(self._mpf_), self._prec) + + @_sympifyit('other', NotImplemented) + def __add__(self, other): + if isinstance(other, Number) and global_parameters.evaluate: + rhs, prec = other._as_mpf_op(self._prec) + return Float._new(mlib.mpf_add(self._mpf_, rhs, prec, rnd), prec) + return Number.__add__(self, other) + + @_sympifyit('other', NotImplemented) + def __sub__(self, other): + if isinstance(other, Number) and global_parameters.evaluate: + rhs, prec = other._as_mpf_op(self._prec) + return Float._new(mlib.mpf_sub(self._mpf_, rhs, prec, rnd), prec) + return Number.__sub__(self, other) + + @_sympifyit('other', NotImplemented) + def __mul__(self, other): + if isinstance(other, Number) and global_parameters.evaluate: + rhs, prec = other._as_mpf_op(self._prec) + return Float._new(mlib.mpf_mul(self._mpf_, rhs, prec, rnd), prec) + return Number.__mul__(self, other) + + @_sympifyit('other', NotImplemented) + def __truediv__(self, other): + if isinstance(other, Number) and other != 0 and global_parameters.evaluate: + rhs, prec = other._as_mpf_op(self._prec) + return Float._new(mlib.mpf_div(self._mpf_, rhs, prec, rnd), prec) + return Number.__truediv__(self, other) + + @_sympifyit('other', NotImplemented) + def __mod__(self, other): + if isinstance(other, Rational) and other.q != 1 and global_parameters.evaluate: + # calculate mod with Rationals, *then* round the result + return Float(Rational.__mod__(Rational(self), other), + precision=self._prec) + if isinstance(other, Float) and global_parameters.evaluate: + r = self/other + if int_valued(r): + return Float(0, precision=max(self._prec, other._prec)) + if isinstance(other, Number) and global_parameters.evaluate: + rhs, prec = other._as_mpf_op(self._prec) + return Float._new(mlib.mpf_mod(self._mpf_, rhs, prec, rnd), prec) + return Number.__mod__(self, other) + + @_sympifyit('other', NotImplemented) + def __rmod__(self, other): + if isinstance(other, Float) and global_parameters.evaluate: + return other.__mod__(self) + if isinstance(other, Number) and global_parameters.evaluate: + rhs, prec = other._as_mpf_op(self._prec) + return Float._new(mlib.mpf_mod(rhs, self._mpf_, prec, rnd), prec) + return Number.__rmod__(self, other) + + def _eval_power(self, expt): + """ + expt is symbolic object but not equal to 0, 1 + + (-p)**r -> exp(r*log(-p)) -> exp(r*(log(p) + I*Pi)) -> + -> p**r*(sin(Pi*r) + cos(Pi*r)*I) + """ + if equal_valued(self, 0): + if expt.is_extended_positive: + return self + if expt.is_extended_negative: + return S.ComplexInfinity + if isinstance(expt, Number): + if isinstance(expt, Integer): + prec = self._prec + return Float._new( + mlib.mpf_pow_int(self._mpf_, expt.p, prec, rnd), prec) + elif isinstance(expt, Rational) and \ + expt.p == 1 and expt.q % 2 and self.is_negative: + return Pow(S.NegativeOne, expt, evaluate=False)*( + -self)._eval_power(expt) + expt, prec = expt._as_mpf_op(self._prec) + mpfself = self._mpf_ + try: + y = mpf_pow(mpfself, expt, prec, rnd) + return Float._new(y, prec) + except mlib.ComplexResult: + re, im = mlib.mpc_pow( + (mpfself, fzero), (expt, fzero), prec, rnd) + return Float._new(re, prec) + \ + Float._new(im, prec)*S.ImaginaryUnit + + def __abs__(self): + return Float._new(mlib.mpf_abs(self._mpf_), self._prec) + + def __int__(self): + if self._mpf_ == fzero: + return 0 + return int(mlib.to_int(self._mpf_)) # uses round_fast = round_down + + def __eq__(self, other): + if isinstance(other, float): + other = Float(other) + return Basic.__eq__(self, other) + + def __ne__(self, other): + eq = self.__eq__(other) + if eq is NotImplemented: + return eq + else: + return not eq + + def __hash__(self): + float_val = float(self) + if not math.isinf(float_val): + return hash(float_val) + return Basic.__hash__(self) + + def _Frel(self, other, op): + try: + other = _sympify(other) + except SympifyError: + return NotImplemented + if other.is_Rational: + # test self*other.q other.p without losing precision + ''' + >>> f = Float(.1,2) + >>> i = 1234567890 + >>> (f*i)._mpf_ + (0, 471, 18, 9) + >>> mlib.mpf_mul(f._mpf_, mlib.from_int(i)) + (0, 505555550955, -12, 39) + ''' + smpf = mlib.mpf_mul(self._mpf_, mlib.from_int(other.q)) + ompf = mlib.from_int(other.p) + return _sympify(bool(op(smpf, ompf))) + elif other.is_Float: + return _sympify(bool( + op(self._mpf_, other._mpf_))) + elif other.is_comparable and other not in ( + S.Infinity, S.NegativeInfinity): + other = other.evalf(prec_to_dps(self._prec)) + if other._prec > 1: + if other.is_Number: + return _sympify(bool( + op(self._mpf_, other._as_mpf_val(self._prec)))) + + def __gt__(self, other): + if isinstance(other, NumberSymbol): + return other.__lt__(self) + rv = self._Frel(other, mlib.mpf_gt) + if rv is None: + return Expr.__gt__(self, other) + return rv + + def __ge__(self, other): + if isinstance(other, NumberSymbol): + return other.__le__(self) + rv = self._Frel(other, mlib.mpf_ge) + if rv is None: + return Expr.__ge__(self, other) + return rv + + def __lt__(self, other): + if isinstance(other, NumberSymbol): + return other.__gt__(self) + rv = self._Frel(other, mlib.mpf_lt) + if rv is None: + return Expr.__lt__(self, other) + return rv + + def __le__(self, other): + if isinstance(other, NumberSymbol): + return other.__ge__(self) + rv = self._Frel(other, mlib.mpf_le) + if rv is None: + return Expr.__le__(self, other) + return rv + + def epsilon_eq(self, other, epsilon="1e-15"): + return abs(self - other) < Float(epsilon) + + def __format__(self, format_spec): + return format(decimal.Decimal(str(self)), format_spec) + + +# Add sympify converters +_sympy_converter[float] = _sympy_converter[decimal.Decimal] = Float + +# this is here to work nicely in Sage +RealNumber = Float + + +class Rational(Number): + """Represents rational numbers (p/q) of any size. + + Examples + ======== + + >>> from sympy import Rational, nsimplify, S, pi + >>> Rational(1, 2) + 1/2 + + Rational is unprejudiced in accepting input. If a float is passed, the + underlying value of the binary representation will be returned: + + >>> Rational(.5) + 1/2 + >>> Rational(.2) + 3602879701896397/18014398509481984 + + If the simpler representation of the float is desired then consider + limiting the denominator to the desired value or convert the float to + a string (which is roughly equivalent to limiting the denominator to + 10**12): + + >>> Rational(str(.2)) + 1/5 + >>> Rational(.2).limit_denominator(10**12) + 1/5 + + An arbitrarily precise Rational is obtained when a string literal is + passed: + + >>> Rational("1.23") + 123/100 + >>> Rational('1e-2') + 1/100 + >>> Rational(".1") + 1/10 + >>> Rational('1e-2/3.2') + 1/320 + + The conversion of other types of strings can be handled by + the sympify() function, and conversion of floats to expressions + or simple fractions can be handled with nsimplify: + + >>> S('.[3]') # repeating digits in brackets + 1/3 + >>> S('3**2/10') # general expressions + 9/10 + >>> nsimplify(.3) # numbers that have a simple form + 3/10 + + But if the input does not reduce to a literal Rational, an error will + be raised: + + >>> Rational(pi) + Traceback (most recent call last): + ... + TypeError: invalid input: pi + + + Low-level + --------- + + Access numerator and denominator as .p and .q: + + >>> r = Rational(3, 4) + >>> r + 3/4 + >>> r.p + 3 + >>> r.q + 4 + + Note that p and q return integers (not SymPy Integers) so some care + is needed when using them in expressions: + + >>> r.p/r.q + 0.75 + + See Also + ======== + sympy.core.sympify.sympify, sympy.simplify.simplify.nsimplify + """ + is_real = True + is_integer = False + is_rational = True + is_number = True + + __slots__ = ('p', 'q') + + p: int + q: int + + is_Rational = True + + @cacheit + def __new__(cls, p, q=None, gcd=None): + if q is None: + if isinstance(p, Rational): + return p + + if isinstance(p, SYMPY_INTS): + pass + else: + if isinstance(p, (float, Float)): + return Rational(*_as_integer_ratio(p)) + + if not isinstance(p, str): + try: + p = sympify(p) + except (SympifyError, SyntaxError): + pass # error will raise below + else: + if p.count('/') > 1: + raise TypeError('invalid input: %s' % p) + p = p.replace(' ', '') + pq = p.rsplit('/', 1) + if len(pq) == 2: + p, q = pq + fp = fractions.Fraction(p) + fq = fractions.Fraction(q) + p = fp/fq + try: + p = fractions.Fraction(p) + except ValueError: + pass # error will raise below + else: + return cls._new(p.numerator, p.denominator, 1) + + if not isinstance(p, Rational): + raise TypeError('invalid input: %s' % p) + + q = 1 + + Q = 1 + + if not isinstance(p, SYMPY_INTS): + p = Rational(p) + Q *= p.q + p = p.p + else: + p = int(p) + + if not isinstance(q, SYMPY_INTS): + q = Rational(q) + p *= q.q + Q *= q.p + else: + Q *= int(q) + q = Q + + if gcd is not None: + sympy_deprecation_warning( + "gcd is deprecated in Rational, use nsimplify instead", + deprecated_since_version="1.11", + active_deprecations_target="deprecated-rational-gcd", + stacklevel=4, + ) + return cls._new(p, q, gcd) + + # p and q are now ints + return cls._new(p, q) + + @classmethod + def _new(cls, p, q, gcd=None): + if q == 0: + if p == 0: + if _errdict["divide"]: + raise ValueError("Indeterminate 0/0") + else: + return S.NaN + return S.ComplexInfinity + + if q < 0: + q = -q + p = -p + + if gcd is None: + gcd = igcd(abs(p), q) + + if gcd > 1: + p //= gcd + q //= gcd + + return cls.from_coprime_ints(p, q) + + @classmethod + def from_coprime_ints(cls, p: int, q: int) -> Rational: + """Create a Rational from a pair of coprime integers. + + Both ``p`` and ``q`` should be strictly of type ``int``. + + The caller should ensure that ``gcd(p,q) == 1`` and ``q > 0``. + + This may be more efficient than ``Rational(p, q)``. The validity of the + arguments may or may not be checked so it should not be relied upon to + pass unvalidated or invalid arguments to this function. + """ + if q == 1: + return Integer(p) + if p == 1 and q == 2: + return S.Half + + obj = Expr.__new__(cls) + obj.p = p + obj.q = q + return obj + + def limit_denominator(self, max_denominator=1000000): + """Closest Rational to self with denominator at most max_denominator. + + Examples + ======== + + >>> from sympy import Rational + >>> Rational('3.141592653589793').limit_denominator(10) + 22/7 + >>> Rational('3.141592653589793').limit_denominator(100) + 311/99 + + """ + f = fractions.Fraction(self.p, self.q) + return Rational(f.limit_denominator(fractions.Fraction(int(max_denominator)))) + + def __getnewargs__(self): + return (self.p, self.q) + + def _hashable_content(self): + return (self.p, self.q) + + def _eval_is_positive(self): + return self.p > 0 + + def _eval_is_zero(self): + return self.p == 0 + + def __neg__(self): + return Rational(-self.p, self.q) + + @_sympifyit('other', NotImplemented) + def __add__(self, other): + if global_parameters.evaluate: + if isinstance(other, Integer): + return Rational._new(self.p + self.q*other.p, self.q, 1) + elif isinstance(other, Rational): + #TODO: this can probably be optimized more + return Rational(self.p*other.q + self.q*other.p, self.q*other.q) + elif isinstance(other, Float): + return other + self + else: + return Number.__add__(self, other) + return Number.__add__(self, other) + __radd__ = __add__ + + @_sympifyit('other', NotImplemented) + def __sub__(self, other): + if global_parameters.evaluate: + if isinstance(other, Integer): + return Rational._new(self.p - self.q*other.p, self.q, 1) + elif isinstance(other, Rational): + return Rational(self.p*other.q - self.q*other.p, self.q*other.q) + elif isinstance(other, Float): + return -other + self + else: + return Number.__sub__(self, other) + return Number.__sub__(self, other) + @_sympifyit('other', NotImplemented) + def __rsub__(self, other): + if global_parameters.evaluate: + if isinstance(other, Integer): + return Rational._new(self.q*other.p - self.p, self.q, 1) + elif isinstance(other, Rational): + return Rational(self.q*other.p - self.p*other.q, self.q*other.q) + elif isinstance(other, Float): + return -self + other + else: + return Number.__rsub__(self, other) + return Number.__rsub__(self, other) + @_sympifyit('other', NotImplemented) + def __mul__(self, other): + if global_parameters.evaluate: + if isinstance(other, Integer): + return Rational._new(self.p*other.p, self.q, igcd(other.p, self.q)) + elif isinstance(other, Rational): + return Rational._new(self.p*other.p, self.q*other.q, igcd(self.p, other.q)*igcd(self.q, other.p)) + elif isinstance(other, Float): + return other*self + else: + return Number.__mul__(self, other) + return Number.__mul__(self, other) + __rmul__ = __mul__ + + @_sympifyit('other', NotImplemented) + def __truediv__(self, other): + if global_parameters.evaluate: + if isinstance(other, Integer): + if self.p and other.p == S.Zero: + return S.ComplexInfinity + else: + return Rational._new(self.p, self.q*other.p, igcd(self.p, other.p)) + elif isinstance(other, Rational): + return Rational._new(self.p*other.q, self.q*other.p, igcd(self.p, other.p)*igcd(self.q, other.q)) + elif isinstance(other, Float): + return self*(1/other) + else: + return Number.__truediv__(self, other) + return Number.__truediv__(self, other) + @_sympifyit('other', NotImplemented) + def __rtruediv__(self, other): + if global_parameters.evaluate: + if isinstance(other, Integer): + return Rational._new(other.p*self.q, self.p, igcd(self.p, other.p)) + elif isinstance(other, Rational): + return Rational._new(other.p*self.q, other.q*self.p, igcd(self.p, other.p)*igcd(self.q, other.q)) + elif isinstance(other, Float): + return other*(1/self) + else: + return Number.__rtruediv__(self, other) + return Number.__rtruediv__(self, other) + + @_sympifyit('other', NotImplemented) + def __mod__(self, other): + if global_parameters.evaluate: + if isinstance(other, Rational): + n = (self.p*other.q) // (other.p*self.q) + return Rational(self.p*other.q - n*other.p*self.q, self.q*other.q) + if isinstance(other, Float): + # calculate mod with Rationals, *then* round the answer + return Float(self.__mod__(Rational(other)), + precision=other._prec) + return Number.__mod__(self, other) + return Number.__mod__(self, other) + + @_sympifyit('other', NotImplemented) + def __rmod__(self, other): + if isinstance(other, Rational): + return Rational.__mod__(other, self) + return Number.__rmod__(self, other) + + def _eval_power(self, expt): + if isinstance(expt, Number): + if isinstance(expt, Float): + return self._eval_evalf(expt._prec)**expt + if expt.is_extended_negative: + # (3/4)**-2 -> (4/3)**2 + ne = -expt + if (ne is S.One): + return Rational(self.q, self.p) + if self.is_negative: + return S.NegativeOne**expt*Rational(self.q, -self.p)**ne + else: + return Rational(self.q, self.p)**ne + if expt is S.Infinity: # -oo already caught by test for negative + if self.p > self.q: + # (3/2)**oo -> oo + return S.Infinity + if self.p < -self.q: + # (-3/2)**oo -> oo + I*oo + return S.Infinity + S.Infinity*S.ImaginaryUnit + return S.Zero + if isinstance(expt, Integer): + # (4/3)**2 -> 4**2 / 3**2 + return Rational._new(self.p**expt.p, self.q**expt.p, 1) + if isinstance(expt, Rational): + intpart = expt.p // expt.q + if intpart: + intpart += 1 + remfracpart = intpart*expt.q - expt.p + ratfracpart = Rational(remfracpart, expt.q) + if self.p != 1: + return Integer(self.p)**expt*Integer(self.q)**ratfracpart*Rational._new(1, self.q**intpart, 1) + return Integer(self.q)**ratfracpart*Rational._new(1, self.q**intpart, 1) + else: + remfracpart = expt.q - expt.p + ratfracpart = Rational(remfracpart, expt.q) + if self.p != 1: + return Integer(self.p)**expt*Integer(self.q)**ratfracpart*Rational._new(1, self.q, 1) + return Integer(self.q)**ratfracpart*Rational._new(1, self.q, 1) + + if self.is_extended_negative and expt.is_even: + return (-self)**expt + + return + + def _as_mpf_val(self, prec): + return mlib.from_rational(self.p, self.q, prec, rnd) + + def _mpmath_(self, prec, rnd): + return mpmath.make_mpf(mlib.from_rational(self.p, self.q, prec, rnd)) + + def __abs__(self): + return Rational(abs(self.p), self.q) + + def __int__(self): + p, q = self.p, self.q + if p < 0: + return -int(-p//q) + return int(p//q) + + def floor(self): + return Integer(self.p // self.q) + + def ceiling(self): + return -Integer(-self.p // self.q) + + def __floor__(self): + return self.floor() + + def __ceil__(self): + return self.ceiling() + + def __eq__(self, other): + try: + other = _sympify(other) + except SympifyError: + return NotImplemented + if not isinstance(other, Number): + # S(0) == S.false is False + # S(0) == False is True + return False + if other.is_NumberSymbol: + if other.is_irrational: + return False + return other.__eq__(self) + if other.is_Rational: + # a Rational is always in reduced form so will never be 2/4 + # so we can just check equivalence of args + return self.p == other.p and self.q == other.q + return False + + def __ne__(self, other): + return not self == other + + def _Rrel(self, other, attr): + # if you want self < other, pass self, other, __gt__ + try: + other = _sympify(other) + except SympifyError: + return NotImplemented + if other.is_Number: + op = None + s, o = self, other + if other.is_NumberSymbol: + op = getattr(o, attr) + elif other.is_Float: + op = getattr(o, attr) + elif other.is_Rational: + s, o = Integer(s.p*o.q), Integer(s.q*o.p) + op = getattr(o, attr) + if op: + return op(s) + if o.is_number and o.is_extended_real: + return Integer(s.p), s.q*o + + def __gt__(self, other): + rv = self._Rrel(other, '__lt__') + if rv is None: + rv = self, other + elif not isinstance(rv, tuple): + return rv + return Expr.__gt__(*rv) + + def __ge__(self, other): + rv = self._Rrel(other, '__le__') + if rv is None: + rv = self, other + elif not isinstance(rv, tuple): + return rv + return Expr.__ge__(*rv) + + def __lt__(self, other): + rv = self._Rrel(other, '__gt__') + if rv is None: + rv = self, other + elif not isinstance(rv, tuple): + return rv + return Expr.__lt__(*rv) + + def __le__(self, other): + rv = self._Rrel(other, '__ge__') + if rv is None: + rv = self, other + elif not isinstance(rv, tuple): + return rv + return Expr.__le__(*rv) + + def __hash__(self): + return super().__hash__() + + def factors(self, limit=None, use_trial=True, use_rho=False, + use_pm1=False, verbose=False, visual=False): + """A wrapper to factorint which return factors of self that are + smaller than limit (or cheap to compute). Special methods of + factoring are disabled by default so that only trial division is used. + """ + from sympy.ntheory.factor_ import factorrat + + return factorrat(self, limit=limit, use_trial=use_trial, + use_rho=use_rho, use_pm1=use_pm1, + verbose=verbose).copy() + + @property + def numerator(self): + return self.p + + @property + def denominator(self): + return self.q + + @_sympifyit('other', NotImplemented) + def gcd(self, other): + if isinstance(other, Rational): + if other == S.Zero: + return other + return Rational( + igcd(self.p, other.p), + ilcm(self.q, other.q)) + return Number.gcd(self, other) + + @_sympifyit('other', NotImplemented) + def lcm(self, other): + if isinstance(other, Rational): + return Rational( + self.p // igcd(self.p, other.p) * other.p, + igcd(self.q, other.q)) + return Number.lcm(self, other) + + def as_numer_denom(self): + return Integer(self.p), Integer(self.q) + + def as_content_primitive(self, radical=False, clear=True): + """Return the tuple (R, self/R) where R is the positive Rational + extracted from self. + + Examples + ======== + + >>> from sympy import S + >>> (S(-3)/2).as_content_primitive() + (3/2, -1) + + See docstring of Expr.as_content_primitive for more examples. + """ + + if self: + if self.is_positive: + return self, S.One + return -self, S.NegativeOne + return S.One, self + + def as_coeff_Mul(self, rational=False): + """Efficiently extract the coefficient of a product.""" + return self, S.One + + def as_coeff_Add(self, rational=False): + """Efficiently extract the coefficient of a summation.""" + return self, S.Zero + + +class Integer(Rational): + """Represents integer numbers of any size. + + Examples + ======== + + >>> from sympy import Integer + >>> Integer(3) + 3 + + If a float or a rational is passed to Integer, the fractional part + will be discarded; the effect is of rounding toward zero. + + >>> Integer(3.8) + 3 + >>> Integer(-3.8) + -3 + + A string is acceptable input if it can be parsed as an integer: + + >>> Integer("9" * 20) + 99999999999999999999 + + It is rarely needed to explicitly instantiate an Integer, because + Python integers are automatically converted to Integer when they + are used in SymPy expressions. + """ + q = 1 + is_integer = True + is_number = True + + is_Integer = True + + __slots__ = () + + def _as_mpf_val(self, prec): + return mlib.from_int(self.p, prec, rnd) + + def _mpmath_(self, prec, rnd): + return mpmath.make_mpf(self._as_mpf_val(prec)) + + @cacheit + def __new__(cls, i): + if isinstance(i, str): + i = i.replace(' ', '') + # whereas we cannot, in general, make a Rational from an + # arbitrary expression, we can make an Integer unambiguously + # (except when a non-integer expression happens to round to + # an integer). So we proceed by taking int() of the input and + # let the int routines determine whether the expression can + # be made into an int or whether an error should be raised. + try: + ival = int(i) + except TypeError: + raise TypeError( + "Argument of Integer should be of numeric type, got %s." % i) + # We only work with well-behaved integer types. This converts, for + # example, numpy.int32 instances. + if ival == 1: + return S.One + if ival == -1: + return S.NegativeOne + if ival == 0: + return S.Zero + obj = Expr.__new__(cls) + obj.p = ival + return obj + + def __getnewargs__(self): + return (self.p,) + + # Arithmetic operations are here for efficiency + def __int__(self): + return self.p + + def floor(self): + return Integer(self.p) + + def ceiling(self): + return Integer(self.p) + + def __floor__(self): + return self.floor() + + def __ceil__(self): + return self.ceiling() + + def __neg__(self): + return Integer(-self.p) + + def __abs__(self): + if self.p >= 0: + return self + else: + return Integer(-self.p) + + def __divmod__(self, other): + if isinstance(other, Integer) and global_parameters.evaluate: + return Tuple(*(divmod(self.p, other.p))) + else: + return Number.__divmod__(self, other) + + def __rdivmod__(self, other): + if isinstance(other, int) and global_parameters.evaluate: + return Tuple(*(divmod(other, self.p))) + else: + try: + other = Number(other) + except TypeError: + msg = "unsupported operand type(s) for divmod(): '%s' and '%s'" + oname = type(other).__name__ + sname = type(self).__name__ + raise TypeError(msg % (oname, sname)) + return Number.__divmod__(other, self) + + # TODO make it decorator + bytecodehacks? + def __add__(self, other): + if global_parameters.evaluate: + if isinstance(other, int): + return Integer(self.p + other) + elif isinstance(other, Integer): + return Integer(self.p + other.p) + elif isinstance(other, Rational): + return Rational._new(self.p*other.q + other.p, other.q, 1) + return Rational.__add__(self, other) + else: + return Add(self, other) + + def __radd__(self, other): + if global_parameters.evaluate: + if isinstance(other, int): + return Integer(other + self.p) + elif isinstance(other, Rational): + return Rational._new(other.p + self.p*other.q, other.q, 1) + return Rational.__radd__(self, other) + return Rational.__radd__(self, other) + + def __sub__(self, other): + if global_parameters.evaluate: + if isinstance(other, int): + return Integer(self.p - other) + elif isinstance(other, Integer): + return Integer(self.p - other.p) + elif isinstance(other, Rational): + return Rational._new(self.p*other.q - other.p, other.q, 1) + return Rational.__sub__(self, other) + return Rational.__sub__(self, other) + + def __rsub__(self, other): + if global_parameters.evaluate: + if isinstance(other, int): + return Integer(other - self.p) + elif isinstance(other, Rational): + return Rational._new(other.p - self.p*other.q, other.q, 1) + return Rational.__rsub__(self, other) + return Rational.__rsub__(self, other) + + def __mul__(self, other): + if global_parameters.evaluate: + if isinstance(other, int): + return Integer(self.p*other) + elif isinstance(other, Integer): + return Integer(self.p*other.p) + elif isinstance(other, Rational): + return Rational._new(self.p*other.p, other.q, igcd(self.p, other.q)) + return Rational.__mul__(self, other) + return Rational.__mul__(self, other) + + def __rmul__(self, other): + if global_parameters.evaluate: + if isinstance(other, int): + return Integer(other*self.p) + elif isinstance(other, Rational): + return Rational._new(other.p*self.p, other.q, igcd(self.p, other.q)) + return Rational.__rmul__(self, other) + return Rational.__rmul__(self, other) + + def __mod__(self, other): + if global_parameters.evaluate: + if isinstance(other, int): + return Integer(self.p % other) + elif isinstance(other, Integer): + return Integer(self.p % other.p) + return Rational.__mod__(self, other) + return Rational.__mod__(self, other) + + def __rmod__(self, other): + if global_parameters.evaluate: + if isinstance(other, int): + return Integer(other % self.p) + elif isinstance(other, Integer): + return Integer(other.p % self.p) + return Rational.__rmod__(self, other) + return Rational.__rmod__(self, other) + + def __eq__(self, other): + if isinstance(other, int): + return (self.p == other) + elif isinstance(other, Integer): + return (self.p == other.p) + return Rational.__eq__(self, other) + + def __ne__(self, other): + return not self == other + + def __gt__(self, other): + try: + other = _sympify(other) + except SympifyError: + return NotImplemented + if other.is_Integer: + return _sympify(self.p > other.p) + return Rational.__gt__(self, other) + + def __lt__(self, other): + try: + other = _sympify(other) + except SympifyError: + return NotImplemented + if other.is_Integer: + return _sympify(self.p < other.p) + return Rational.__lt__(self, other) + + def __ge__(self, other): + try: + other = _sympify(other) + except SympifyError: + return NotImplemented + if other.is_Integer: + return _sympify(self.p >= other.p) + return Rational.__ge__(self, other) + + def __le__(self, other): + try: + other = _sympify(other) + except SympifyError: + return NotImplemented + if other.is_Integer: + return _sympify(self.p <= other.p) + return Rational.__le__(self, other) + + def __hash__(self): + return hash(self.p) + + def __index__(self): + return self.p + + ######################################## + + def _eval_is_odd(self): + return bool(self.p % 2) + + def _eval_power(self, expt): + """ + Tries to do some simplifications on self**expt + + Returns None if no further simplifications can be done. + + Explanation + =========== + + When exponent is a fraction (so we have for example a square root), + we try to find a simpler representation by factoring the argument + up to factors of 2**15, e.g. + + - sqrt(4) becomes 2 + - sqrt(-4) becomes 2*I + - (2**(3+7)*3**(6+7))**Rational(1,7) becomes 6*18**(3/7) + + Further simplification would require a special call to factorint on + the argument which is not done here for sake of speed. + + """ + from sympy.ntheory.factor_ import perfect_power + + if expt is S.Infinity: + if self.p > S.One: + return S.Infinity + # cases -1, 0, 1 are done in their respective classes + return S.Infinity + S.ImaginaryUnit*S.Infinity + if expt is S.NegativeInfinity: + return Rational._new(1, self, 1)**S.Infinity + if not isinstance(expt, Number): + # simplify when expt is even + # (-2)**k --> 2**k + if self.is_negative and expt.is_even: + return (-self)**expt + if isinstance(expt, Float): + # Rational knows how to exponentiate by a Float + return super()._eval_power(expt) + if not isinstance(expt, Rational): + return + if expt is S.Half and self.is_negative: + # we extract I for this special case since everyone is doing so + return S.ImaginaryUnit*Pow(-self, expt) + if expt.is_negative: + # invert base and change sign on exponent + ne = -expt + if self.is_negative: + return S.NegativeOne**expt*Rational._new(1, -self.p, 1)**ne + else: + return Rational._new(1, self.p, 1)**ne + # see if base is a perfect root, sqrt(4) --> 2 + x, xexact = integer_nthroot(abs(self.p), expt.q) + if xexact: + # if it's a perfect root we've finished + result = Integer(x**abs(expt.p)) + if self.is_negative: + result *= S.NegativeOne**expt + return result + + # The following is an algorithm where we collect perfect roots + # from the factors of base. + + # if it's not an nth root, it still might be a perfect power + b_pos = int(abs(self.p)) + p = perfect_power(b_pos) + if p is not False: + # XXX: Convert to int because perfect_power may return fmpz + # Ideally that should be fixed in perfect_power though... + dict = {int(p[0]): int(p[1])} + else: + dict = Integer(b_pos).factors(limit=2**15) + + # now process the dict of factors + out_int = 1 # integer part + out_rad = 1 # extracted radicals + sqr_int = 1 + sqr_gcd = 0 + sqr_dict = {} + for prime, exponent in dict.items(): + exponent *= expt.p + # remove multiples of expt.q: (2**12)**(1/10) -> 2*(2**2)**(1/10) + div_e, div_m = divmod(exponent, expt.q) + if div_e > 0: + out_int *= prime**div_e + if div_m > 0: + # see if the reduced exponent shares a gcd with e.q + # (2**2)**(1/10) -> 2**(1/5) + g = igcd(div_m, expt.q) + if g != 1: + out_rad *= Pow(prime, Rational._new(div_m//g, expt.q//g, 1)) + else: + sqr_dict[prime] = div_m + # identify gcd of remaining powers + for p, ex in sqr_dict.items(): + if sqr_gcd == 0: + sqr_gcd = ex + else: + sqr_gcd = igcd(sqr_gcd, ex) + if sqr_gcd == 1: + break + for k, v in sqr_dict.items(): + sqr_int *= k**(v//sqr_gcd) + if sqr_int == b_pos and out_int == 1 and out_rad == 1: + result = None + else: + result = out_int*out_rad*Pow(sqr_int, Rational(sqr_gcd, expt.q)) + if self.is_negative: + result *= Pow(S.NegativeOne, expt) + return result + + def _eval_is_prime(self): + from sympy.ntheory.primetest import isprime + + return isprime(self) + + def _eval_is_composite(self): + if self > 1: + return fuzzy_not(self.is_prime) + else: + return False + + def as_numer_denom(self): + return self, S.One + + @_sympifyit('other', NotImplemented) + def __floordiv__(self, other): + if not isinstance(other, Expr): + return NotImplemented + if isinstance(other, Integer): + return Integer(self.p // other) + return divmod(self, other)[0] + + def __rfloordiv__(self, other): + return Integer(Integer(other).p // self.p) + + # These bitwise operations (__lshift__, __rlshift__, ..., __invert__) are defined + # for Integer only and not for general SymPy expressions. This is to achieve + # compatibility with the numbers.Integral ABC which only defines these operations + # among instances of numbers.Integral. Therefore, these methods check explicitly for + # integer types rather than using sympify because they should not accept arbitrary + # symbolic expressions and there is no symbolic analogue of numbers.Integral's + # bitwise operations. + def __lshift__(self, other): + if isinstance(other, (int, Integer, numbers.Integral)): + return Integer(self.p << int(other)) + else: + return NotImplemented + + def __rlshift__(self, other): + if isinstance(other, (int, numbers.Integral)): + return Integer(int(other) << self.p) + else: + return NotImplemented + + def __rshift__(self, other): + if isinstance(other, (int, Integer, numbers.Integral)): + return Integer(self.p >> int(other)) + else: + return NotImplemented + + def __rrshift__(self, other): + if isinstance(other, (int, numbers.Integral)): + return Integer(int(other) >> self.p) + else: + return NotImplemented + + def __and__(self, other): + if isinstance(other, (int, Integer, numbers.Integral)): + return Integer(self.p & int(other)) + else: + return NotImplemented + + def __rand__(self, other): + if isinstance(other, (int, numbers.Integral)): + return Integer(int(other) & self.p) + else: + return NotImplemented + + def __xor__(self, other): + if isinstance(other, (int, Integer, numbers.Integral)): + return Integer(self.p ^ int(other)) + else: + return NotImplemented + + def __rxor__(self, other): + if isinstance(other, (int, numbers.Integral)): + return Integer(int(other) ^ self.p) + else: + return NotImplemented + + def __or__(self, other): + if isinstance(other, (int, Integer, numbers.Integral)): + return Integer(self.p | int(other)) + else: + return NotImplemented + + def __ror__(self, other): + if isinstance(other, (int, numbers.Integral)): + return Integer(int(other) | self.p) + else: + return NotImplemented + + def __invert__(self): + return Integer(~self.p) + +# Add sympify converters +_sympy_converter[int] = Integer + + +class AlgebraicNumber(Expr): + r""" + Class for representing algebraic numbers in SymPy. + + Symbolically, an instance of this class represents an element + $\alpha \in \mathbb{Q}(\theta) \hookrightarrow \mathbb{C}$. That is, the + algebraic number $\alpha$ is represented as an element of a particular + number field $\mathbb{Q}(\theta)$, with a particular embedding of this + field into the complex numbers. + + Formally, the primitive element $\theta$ is given by two data points: (1) + its minimal polynomial (which defines $\mathbb{Q}(\theta)$), and (2) a + particular complex number that is a root of this polynomial (which defines + the embedding $\mathbb{Q}(\theta) \hookrightarrow \mathbb{C}$). Finally, + the algebraic number $\alpha$ which we represent is then given by the + coefficients of a polynomial in $\theta$. + """ + + __slots__ = ('rep', 'root', 'alias', 'minpoly', '_own_minpoly') + + is_AlgebraicNumber = True + is_algebraic = True + is_number = True + + + kind = NumberKind + + # Optional alias symbol is not free. + # Actually, alias should be a Str, but some methods + # expect that it be an instance of Expr. + free_symbols: set[Basic] = set() + + def __new__(cls, expr, coeffs=None, alias=None, **args): + r""" + Construct a new algebraic number $\alpha$ belonging to a number field + $k = \mathbb{Q}(\theta)$. + + There are four instance attributes to be determined: + + =========== ============================================================================ + Attribute Type/Meaning + =========== ============================================================================ + ``root`` :py:class:`~.Expr` for $\theta$ as a complex number + ``minpoly`` :py:class:`~.Poly`, the minimal polynomial of $\theta$ + ``rep`` :py:class:`~sympy.polys.polyclasses.DMP` giving $\alpha$ as poly in $\theta$ + ``alias`` :py:class:`~.Symbol` for $\theta$, or ``None`` + =========== ============================================================================ + + See Parameters section for how they are determined. + + Parameters + ========== + + expr : :py:class:`~.Expr`, or pair $(m, r)$ + There are three distinct modes of construction, depending on what + is passed as *expr*. + + **(1)** *expr* is an :py:class:`~.AlgebraicNumber`: + In this case we begin by copying all four instance attributes from + *expr*. If *coeffs* were also given, we compose the two coeff + polynomials (see below). If an *alias* was given, it overrides. + + **(2)** *expr* is any other type of :py:class:`~.Expr`: + Then ``root`` will equal *expr*. Therefore it + must express an algebraic quantity, and we will compute its + ``minpoly``. + + **(3)** *expr* is an ordered pair $(m, r)$ giving the + ``minpoly`` $m$, and a ``root`` $r$ thereof, which together + define $\theta$. In this case $m$ may be either a univariate + :py:class:`~.Poly` or any :py:class:`~.Expr` which represents the + same, while $r$ must be some :py:class:`~.Expr` representing a + complex number that is a root of $m$, including both explicit + expressions in radicals, and instances of + :py:class:`~.ComplexRootOf` or :py:class:`~.AlgebraicNumber`. + + coeffs : list, :py:class:`~.ANP`, None, optional (default=None) + This defines ``rep``, giving the algebraic number $\alpha$ as a + polynomial in $\theta$. + + If a list, the elements should be integers or rational numbers. + If an :py:class:`~.ANP`, we take its coefficients (using its + :py:meth:`~.ANP.to_list()` method). If ``None``, then the list of + coefficients defaults to ``[1, 0]``, meaning that $\alpha = \theta$ + is the primitive element of the field. + + If *expr* was an :py:class:`~.AlgebraicNumber`, let $g(x)$ be its + ``rep`` polynomial, and let $f(x)$ be the polynomial defined by + *coeffs*. Then ``self.rep`` will represent the composition + $(f \circ g)(x)$. + + alias : str, :py:class:`~.Symbol`, None, optional (default=None) + This is a way to provide a name for the primitive element. We + described several ways in which the *expr* argument can define the + value of the primitive element, but none of these methods gave it + a name. Here, for example, *alias* could be set as + ``Symbol('theta')``, in order to make this symbol appear when + $\alpha$ is printed, or rendered as a polynomial, using the + :py:meth:`~.as_poly()` method. + + Examples + ======== + + Recall that we are constructing an algebraic number as a field element + $\alpha \in \mathbb{Q}(\theta)$. + + >>> from sympy import AlgebraicNumber, sqrt, CRootOf, S + >>> from sympy.abc import x + + Example (1): $\alpha = \theta = \sqrt{2}$ + + >>> a1 = AlgebraicNumber(sqrt(2)) + >>> a1.minpoly_of_element().as_expr(x) + x**2 - 2 + >>> a1.evalf(10) + 1.414213562 + + Example (2): $\alpha = 3 \sqrt{2} - 5$, $\theta = \sqrt{2}$. We can + either build on the last example: + + >>> a2 = AlgebraicNumber(a1, [3, -5]) + >>> a2.as_expr() + -5 + 3*sqrt(2) + + or start from scratch: + + >>> a2 = AlgebraicNumber(sqrt(2), [3, -5]) + >>> a2.as_expr() + -5 + 3*sqrt(2) + + Example (3): $\alpha = 6 \sqrt{2} - 11$, $\theta = \sqrt{2}$. Again we + can build on the previous example, and we see that the coeff polys are + composed: + + >>> a3 = AlgebraicNumber(a2, [2, -1]) + >>> a3.as_expr() + -11 + 6*sqrt(2) + + reflecting the fact that $(2x - 1) \circ (3x - 5) = 6x - 11$. + + Example (4): $\alpha = \sqrt{2}$, $\theta = \sqrt{2} + \sqrt{3}$. The + easiest way is to use the :py:func:`~.to_number_field()` function: + + >>> from sympy import to_number_field + >>> a4 = to_number_field(sqrt(2), sqrt(2) + sqrt(3)) + >>> a4.minpoly_of_element().as_expr(x) + x**2 - 2 + >>> a4.to_root() + sqrt(2) + >>> a4.primitive_element() + sqrt(2) + sqrt(3) + >>> a4.coeffs() + [1/2, 0, -9/2, 0] + + but if you already knew the right coefficients, you could construct it + directly: + + >>> a4 = AlgebraicNumber(sqrt(2) + sqrt(3), [S(1)/2, 0, S(-9)/2, 0]) + >>> a4.to_root() + sqrt(2) + >>> a4.primitive_element() + sqrt(2) + sqrt(3) + + Example (5): Construct the Golden Ratio as an element of the 5th + cyclotomic field, supposing we already know its coefficients. This time + we introduce the alias $\zeta$ for the primitive element of the field: + + >>> from sympy import cyclotomic_poly + >>> from sympy.abc import zeta + >>> a5 = AlgebraicNumber(CRootOf(cyclotomic_poly(5), -1), + ... [-1, -1, 0, 0], alias=zeta) + >>> a5.as_poly().as_expr() + -zeta**3 - zeta**2 + >>> a5.evalf() + 1.61803398874989 + + (The index ``-1`` to ``CRootOf`` selects the complex root with the + largest real and imaginary parts, which in this case is + $\mathrm{e}^{2i\pi/5}$. See :py:class:`~.ComplexRootOf`.) + + Example (6): Building on the last example, construct the number + $2 \phi \in \mathbb{Q}(\phi)$, where $\phi$ is the Golden Ratio: + + >>> from sympy.abc import phi + >>> a6 = AlgebraicNumber(a5.to_root(), coeffs=[2, 0], alias=phi) + >>> a6.as_poly().as_expr() + 2*phi + >>> a6.primitive_element().evalf() + 1.61803398874989 + + Note that we needed to use ``a5.to_root()``, since passing ``a5`` as + the first argument would have constructed the number $2 \phi$ as an + element of the field $\mathbb{Q}(\zeta)$: + + >>> a6_wrong = AlgebraicNumber(a5, coeffs=[2, 0]) + >>> a6_wrong.as_poly().as_expr() + -2*zeta**3 - 2*zeta**2 + >>> a6_wrong.primitive_element().evalf() + 0.309016994374947 + 0.951056516295154*I + + """ + from sympy.polys.polyclasses import ANP, DMP + from sympy.polys.numberfields import minimal_polynomial + + expr = sympify(expr) + rep0 = None + alias0 = None + + if isinstance(expr, (tuple, Tuple)): + minpoly, root = expr + + if not minpoly.is_Poly: + from sympy.polys.polytools import Poly + minpoly = Poly(minpoly) + elif expr.is_AlgebraicNumber: + minpoly, root, rep0, alias0 = (expr.minpoly, expr.root, + expr.rep, expr.alias) + else: + minpoly, root = minimal_polynomial( + expr, args.get('gen'), polys=True), expr + + dom = minpoly.get_domain() + + if coeffs is not None: + if not isinstance(coeffs, ANP): + rep = DMP.from_sympy_list(sympify(coeffs), 0, dom) + scoeffs = Tuple(*coeffs) + else: + rep = DMP.from_list(coeffs.to_list(), 0, dom) + scoeffs = Tuple(*coeffs.to_list()) + + else: + rep = DMP.from_list([1, 0], 0, dom) + scoeffs = Tuple(1, 0) + + if rep0 is not None: + from sympy.polys.densetools import dup_compose + c = dup_compose(rep.to_list(), rep0.to_list(), dom) + rep = DMP.from_list(c, 0, dom) + scoeffs = Tuple(*c) + + if rep.degree() >= minpoly.degree(): + rep = rep.rem(minpoly.rep) + + sargs = (root, scoeffs) + + alias = alias or alias0 + if alias is not None: + from .symbol import Symbol + if not isinstance(alias, Symbol): + alias = Symbol(alias) + sargs = sargs + (alias,) + + obj = Expr.__new__(cls, *sargs) + + obj.rep = rep + obj.root = root + obj.alias = alias + obj.minpoly = minpoly + + obj._own_minpoly = None + + return obj + + def __hash__(self): + return super().__hash__() + + def _eval_evalf(self, prec): + return self.as_expr()._evalf(prec) + + @property + def is_aliased(self): + """Returns ``True`` if ``alias`` was set. """ + return self.alias is not None + + def as_poly(self, x=None): + """Create a Poly instance from ``self``. """ + from sympy.polys.polytools import Poly, PurePoly + if x is not None: + return Poly.new(self.rep, x) + else: + if self.alias is not None: + return Poly.new(self.rep, self.alias) + else: + from .symbol import Dummy + return PurePoly.new(self.rep, Dummy('x')) + + def as_expr(self, x=None): + """Create a Basic expression from ``self``. """ + return self.as_poly(x or self.root).as_expr().expand() + + def coeffs(self): + """Returns all SymPy coefficients of an algebraic number. """ + return [ self.rep.dom.to_sympy(c) for c in self.rep.all_coeffs() ] + + def native_coeffs(self): + """Returns all native coefficients of an algebraic number. """ + return self.rep.all_coeffs() + + def to_algebraic_integer(self): + """Convert ``self`` to an algebraic integer. """ + from sympy.polys.polytools import Poly + + f = self.minpoly + + if f.LC() == 1: + return self + + coeff = f.LC()**(f.degree() - 1) + poly = f.compose(Poly(f.gen/f.LC())) + + minpoly = poly*coeff + root = f.LC()*self.root + + return AlgebraicNumber((minpoly, root), self.coeffs()) + + def _eval_simplify(self, **kwargs): + from sympy.polys.rootoftools import CRootOf + from sympy.polys import minpoly + measure, ratio = kwargs['measure'], kwargs['ratio'] + for r in [r for r in self.minpoly.all_roots() if r.func != CRootOf]: + if minpoly(self.root - r).is_Symbol: + # use the matching root if it's simpler + if measure(r) < ratio*measure(self.root): + return AlgebraicNumber(r) + return self + + def field_element(self, coeffs): + r""" + Form another element of the same number field. + + Explanation + =========== + + If we represent $\alpha \in \mathbb{Q}(\theta)$, form another element + $\beta \in \mathbb{Q}(\theta)$ of the same number field. + + Parameters + ========== + + coeffs : list, :py:class:`~.ANP` + Like the *coeffs* arg to the class + :py:meth:`constructor<.AlgebraicNumber.__new__>`, defines the + new element as a polynomial in the primitive element. + + If a list, the elements should be integers or rational numbers. + If an :py:class:`~.ANP`, we take its coefficients (using its + :py:meth:`~.ANP.to_list()` method). + + Examples + ======== + + >>> from sympy import AlgebraicNumber, sqrt + >>> a = AlgebraicNumber(sqrt(5), [-1, 1]) + >>> b = a.field_element([3, 2]) + >>> print(a) + 1 - sqrt(5) + >>> print(b) + 2 + 3*sqrt(5) + >>> print(b.primitive_element() == a.primitive_element()) + True + + See Also + ======== + + AlgebraicNumber + """ + return AlgebraicNumber( + (self.minpoly, self.root), coeffs=coeffs, alias=self.alias) + + @property + def is_primitive_element(self): + r""" + Say whether this algebraic number $\alpha \in \mathbb{Q}(\theta)$ is + equal to the primitive element $\theta$ for its field. + """ + c = self.coeffs() + # Second case occurs if self.minpoly is linear: + return c == [1, 0] or c == [self.root] + + def primitive_element(self): + r""" + Get the primitive element $\theta$ for the number field + $\mathbb{Q}(\theta)$ to which this algebraic number $\alpha$ belongs. + + Returns + ======= + + AlgebraicNumber + + """ + if self.is_primitive_element: + return self + return self.field_element([1, 0]) + + def to_primitive_element(self, radicals=True): + r""" + Convert ``self`` to an :py:class:`~.AlgebraicNumber` instance that is + equal to its own primitive element. + + Explanation + =========== + + If we represent $\alpha \in \mathbb{Q}(\theta)$, $\alpha \neq \theta$, + construct a new :py:class:`~.AlgebraicNumber` that represents + $\alpha \in \mathbb{Q}(\alpha)$. + + Examples + ======== + + >>> from sympy import sqrt, to_number_field + >>> from sympy.abc import x + >>> a = to_number_field(sqrt(2), sqrt(2) + sqrt(3)) + + The :py:class:`~.AlgebraicNumber` ``a`` represents the number + $\sqrt{2}$ in the field $\mathbb{Q}(\sqrt{2} + \sqrt{3})$. Rendering + ``a`` as a polynomial, + + >>> a.as_poly().as_expr(x) + x**3/2 - 9*x/2 + + reflects the fact that $\sqrt{2} = \theta^3/2 - 9 \theta/2$, where + $\theta = \sqrt{2} + \sqrt{3}$. + + ``a`` is not equal to its own primitive element. Its minpoly + + >>> a.minpoly.as_poly().as_expr(x) + x**4 - 10*x**2 + 1 + + is that of $\theta$. + + Converting to a primitive element, + + >>> a_prim = a.to_primitive_element() + >>> a_prim.minpoly.as_poly().as_expr(x) + x**2 - 2 + + we obtain an :py:class:`~.AlgebraicNumber` whose ``minpoly`` is that of + the number itself. + + Parameters + ========== + + radicals : boolean, optional (default=True) + If ``True``, then we will try to return an + :py:class:`~.AlgebraicNumber` whose ``root`` is an expression + in radicals. If that is not possible (or if *radicals* is + ``False``), ``root`` will be a :py:class:`~.ComplexRootOf`. + + Returns + ======= + + AlgebraicNumber + + See Also + ======== + + is_primitive_element + + """ + if self.is_primitive_element: + return self + m = self.minpoly_of_element() + r = self.to_root(radicals=radicals) + return AlgebraicNumber((m, r)) + + def minpoly_of_element(self): + r""" + Compute the minimal polynomial for this algebraic number. + + Explanation + =========== + + Recall that we represent an element $\alpha \in \mathbb{Q}(\theta)$. + Our instance attribute ``self.minpoly`` is the minimal polynomial for + our primitive element $\theta$. This method computes the minimal + polynomial for $\alpha$. + + """ + if self._own_minpoly is None: + if self.is_primitive_element: + self._own_minpoly = self.minpoly + else: + from sympy.polys.numberfields.minpoly import minpoly + theta = self.primitive_element() + self._own_minpoly = minpoly(self.as_expr(theta), polys=True) + return self._own_minpoly + + def to_root(self, radicals=True, minpoly=None): + """ + Convert to an :py:class:`~.Expr` that is not an + :py:class:`~.AlgebraicNumber`, specifically, either a + :py:class:`~.ComplexRootOf`, or, optionally and where possible, an + expression in radicals. + + Parameters + ========== + + radicals : boolean, optional (default=True) + If ``True``, then we will try to return the root as an expression + in radicals. If that is not possible, we will return a + :py:class:`~.ComplexRootOf`. + + minpoly : :py:class:`~.Poly` + If the minimal polynomial for `self` has been pre-computed, it can + be passed in order to save time. + + """ + if self.is_primitive_element and not isinstance(self.root, AlgebraicNumber): + return self.root + m = minpoly or self.minpoly_of_element() + roots = m.all_roots(radicals=radicals) + if len(roots) == 1: + return roots[0] + ex = self.as_expr() + for b in roots: + if m.same_root(b, ex): + return b + + +class RationalConstant(Rational): + """ + Abstract base class for rationals with specific behaviors + + Derived classes must define class attributes p and q and should probably all + be singletons. + """ + __slots__ = () + + def __new__(cls): + return AtomicExpr.__new__(cls) + + +class IntegerConstant(Integer): + __slots__ = () + + def __new__(cls): + return AtomicExpr.__new__(cls) + + +class Zero(IntegerConstant, metaclass=Singleton): + """The number zero. + + Zero is a singleton, and can be accessed by ``S.Zero`` + + Examples + ======== + + >>> from sympy import S, Integer + >>> Integer(0) is S.Zero + True + >>> 1/S.Zero + zoo + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Zero + """ + + p = 0 + q = 1 + is_positive = False + is_negative = False + is_zero = True + is_number = True + is_comparable = True + + __slots__ = () + + def __getnewargs__(self): + return () + + @staticmethod + def __abs__(): + return S.Zero + + @staticmethod + def __neg__(): + return S.Zero + + def _eval_power(self, expt): + if expt.is_extended_positive: + return self + if expt.is_extended_negative: + return S.ComplexInfinity + if expt.is_extended_real is False: + return S.NaN + if expt.is_zero: + return S.One + + # infinities are already handled with pos and neg + # tests above; now throw away leading numbers on Mul + # exponent since 0**-x = zoo**x even when x == 0 + coeff, terms = expt.as_coeff_Mul() + if coeff.is_negative: + return S.ComplexInfinity**terms + if coeff is not S.One: # there is a Number to discard + return self**terms + + def _eval_order(self, *symbols): + # Order(0,x) -> 0 + return self + + def __bool__(self): + return False + + +class One(IntegerConstant, metaclass=Singleton): + """The number one. + + One is a singleton, and can be accessed by ``S.One``. + + Examples + ======== + + >>> from sympy import S, Integer + >>> Integer(1) is S.One + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/1_%28number%29 + """ + is_number = True + is_positive = True + + p = 1 + q = 1 + + __slots__ = () + + def __getnewargs__(self): + return () + + @staticmethod + def __abs__(): + return S.One + + @staticmethod + def __neg__(): + return S.NegativeOne + + def _eval_power(self, expt): + return self + + def _eval_order(self, *symbols): + return + + @staticmethod + def factors(limit=None, use_trial=True, use_rho=False, use_pm1=False, + verbose=False, visual=False): + if visual: + return S.One + else: + return {} + + +class NegativeOne(IntegerConstant, metaclass=Singleton): + """The number negative one. + + NegativeOne is a singleton, and can be accessed by ``S.NegativeOne``. + + Examples + ======== + + >>> from sympy import S, Integer + >>> Integer(-1) is S.NegativeOne + True + + See Also + ======== + + One + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/%E2%88%921_%28number%29 + + """ + is_number = True + + p = -1 + q = 1 + + __slots__ = () + + def __getnewargs__(self): + return () + + @staticmethod + def __abs__(): + return S.One + + @staticmethod + def __neg__(): + return S.One + + def _eval_power(self, expt): + if expt.is_odd: + return S.NegativeOne + if expt.is_even: + return S.One + if isinstance(expt, Number): + if isinstance(expt, Float): + return Float(-1.0)**expt + if expt is S.NaN: + return S.NaN + if expt in (S.Infinity, S.NegativeInfinity): + return S.NaN + if expt is S.Half: + return S.ImaginaryUnit + if isinstance(expt, Rational): + if expt.q == 2: + return S.ImaginaryUnit**Integer(expt.p) + i, r = divmod(expt.p, expt.q) + if i: + return self**i*self**Rational(r, expt.q) + return + + +class Half(RationalConstant, metaclass=Singleton): + """The rational number 1/2. + + Half is a singleton, and can be accessed by ``S.Half``. + + Examples + ======== + + >>> from sympy import S, Rational + >>> Rational(1, 2) is S.Half + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/One_half + """ + is_number = True + + p = 1 + q = 2 + + __slots__ = () + + def __getnewargs__(self): + return () + + @staticmethod + def __abs__(): + return S.Half + + +class Infinity(Number, metaclass=Singleton): + r"""Positive infinite quantity. + + Explanation + =========== + + In real analysis the symbol `\infty` denotes an unbounded + limit: `x\to\infty` means that `x` grows without bound. + + Infinity is often used not only to define a limit but as a value + in the affinely extended real number system. Points labeled `+\infty` + and `-\infty` can be added to the topological space of the real numbers, + producing the two-point compactification of the real numbers. Adding + algebraic properties to this gives us the extended real numbers. + + Infinity is a singleton, and can be accessed by ``S.Infinity``, + or can be imported as ``oo``. + + Examples + ======== + + >>> from sympy import oo, exp, limit, Symbol + >>> 1 + oo + oo + >>> 42/oo + 0 + >>> x = Symbol('x') + >>> limit(exp(x), x, oo) + oo + + See Also + ======== + + NegativeInfinity, NaN + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Infinity + """ + + is_commutative = True + is_number = True + is_complex = False + is_extended_real = True + is_infinite = True + is_comparable = True + is_extended_positive = True + is_prime = False + + __slots__ = () + + def __new__(cls): + return AtomicExpr.__new__(cls) + + def _latex(self, printer): + return r"\infty" + + def _eval_subs(self, old, new): + if self == old: + return new + + def _eval_evalf(self, prec=None): + return Float('inf') + + def evalf(self, prec=None, **options): + return self._eval_evalf(prec) + + @_sympifyit('other', NotImplemented) + def __add__(self, other): + if isinstance(other, Number) and global_parameters.evaluate: + if other in (S.NegativeInfinity, S.NaN): + return S.NaN + return self + return Number.__add__(self, other) + __radd__ = __add__ + + @_sympifyit('other', NotImplemented) + def __sub__(self, other): + if isinstance(other, Number) and global_parameters.evaluate: + if other in (S.Infinity, S.NaN): + return S.NaN + return self + return Number.__sub__(self, other) + + @_sympifyit('other', NotImplemented) + def __rsub__(self, other): + return (-self).__add__(other) + + @_sympifyit('other', NotImplemented) + def __mul__(self, other): + if isinstance(other, Number) and global_parameters.evaluate: + if other.is_zero or other is S.NaN: + return S.NaN + if other.is_extended_positive: + return self + return S.NegativeInfinity + return Number.__mul__(self, other) + __rmul__ = __mul__ + + @_sympifyit('other', NotImplemented) + def __truediv__(self, other): + if isinstance(other, Number) and global_parameters.evaluate: + if other is S.Infinity or \ + other is S.NegativeInfinity or \ + other is S.NaN: + return S.NaN + if other.is_extended_nonnegative: + return self + return S.NegativeInfinity + return Number.__truediv__(self, other) + + def __abs__(self): + return S.Infinity + + def __neg__(self): + return S.NegativeInfinity + + def _eval_power(self, expt): + """ + ``expt`` is symbolic object but not equal to 0 or 1. + + ================ ======= ============================== + Expression Result Notes + ================ ======= ============================== + ``oo ** nan`` ``nan`` + ``oo ** -p`` ``0`` ``p`` is number, ``oo`` + ================ ======= ============================== + + See Also + ======== + Pow + NaN + NegativeInfinity + + """ + if expt.is_extended_positive: + return S.Infinity + if expt.is_extended_negative: + return S.Zero + if expt is S.NaN: + return S.NaN + if expt is S.ComplexInfinity: + return S.NaN + if expt.is_extended_real is False and expt.is_number: + from sympy.functions.elementary.complexes import re + expt_real = re(expt) + if expt_real.is_positive: + return S.ComplexInfinity + if expt_real.is_negative: + return S.Zero + if expt_real.is_zero: + return S.NaN + + return self**expt.evalf() + + def _as_mpf_val(self, prec): + return mlib.finf + + def __hash__(self): + return super().__hash__() + + def __eq__(self, other): + return other is S.Infinity or other == float('inf') + + def __ne__(self, other): + return other is not S.Infinity and other != float('inf') + + __gt__ = Expr.__gt__ + __ge__ = Expr.__ge__ + __lt__ = Expr.__lt__ + __le__ = Expr.__le__ + + @_sympifyit('other', NotImplemented) + def __mod__(self, other): + if not isinstance(other, Expr): + return NotImplemented + return S.NaN + + __rmod__ = __mod__ + + def floor(self): + return self + + def ceiling(self): + return self + +oo = S.Infinity + + +class NegativeInfinity(Number, metaclass=Singleton): + """Negative infinite quantity. + + NegativeInfinity is a singleton, and can be accessed + by ``S.NegativeInfinity``. + + See Also + ======== + + Infinity + """ + + is_extended_real = True + is_complex = False + is_commutative = True + is_infinite = True + is_comparable = True + is_extended_negative = True + is_number = True + is_prime = False + + __slots__ = () + + def __new__(cls): + return AtomicExpr.__new__(cls) + + def _latex(self, printer): + return r"-\infty" + + def _eval_subs(self, old, new): + if self == old: + return new + + def _eval_evalf(self, prec=None): + return Float('-inf') + + def evalf(self, prec=None, **options): + return self._eval_evalf(prec) + + @_sympifyit('other', NotImplemented) + def __add__(self, other): + if isinstance(other, Number) and global_parameters.evaluate: + if other in (S.Infinity, S.NaN): + return S.NaN + return self + return Number.__add__(self, other) + __radd__ = __add__ + + @_sympifyit('other', NotImplemented) + def __sub__(self, other): + if isinstance(other, Number) and global_parameters.evaluate: + if other in (S.NegativeInfinity, S.NaN): + return S.NaN + return self + return Number.__sub__(self, other) + + @_sympifyit('other', NotImplemented) + def __rsub__(self, other): + return (-self).__add__(other) + + @_sympifyit('other', NotImplemented) + def __mul__(self, other): + if isinstance(other, Number) and global_parameters.evaluate: + if other.is_zero or other is S.NaN: + return S.NaN + if other.is_extended_positive: + return self + return S.Infinity + return Number.__mul__(self, other) + __rmul__ = __mul__ + + @_sympifyit('other', NotImplemented) + def __truediv__(self, other): + if isinstance(other, Number) and global_parameters.evaluate: + if other is S.Infinity or \ + other is S.NegativeInfinity or \ + other is S.NaN: + return S.NaN + if other.is_extended_nonnegative: + return self + return S.Infinity + return Number.__truediv__(self, other) + + def __abs__(self): + return S.Infinity + + def __neg__(self): + return S.Infinity + + def _eval_power(self, expt): + """ + ``expt`` is symbolic object but not equal to 0 or 1. + + ================ ======= ============================== + Expression Result Notes + ================ ======= ============================== + ``(-oo) ** nan`` ``nan`` + ``(-oo) ** oo`` ``nan`` + ``(-oo) ** -oo`` ``nan`` + ``(-oo) ** e`` ``oo`` ``e`` is positive even integer + ``(-oo) ** o`` ``-oo`` ``o`` is positive odd integer + ================ ======= ============================== + + See Also + ======== + + Infinity + Pow + NaN + + """ + if expt.is_number: + if expt is S.NaN or \ + expt is S.Infinity or \ + expt is S.NegativeInfinity: + return S.NaN + + if isinstance(expt, Integer) and expt.is_extended_positive: + if expt.is_odd: + return S.NegativeInfinity + else: + return S.Infinity + + inf_part = S.Infinity**expt + s_part = S.NegativeOne**expt + if inf_part == 0 and s_part.is_finite: + return inf_part + if (inf_part is S.ComplexInfinity and + s_part.is_finite and not s_part.is_zero): + return S.ComplexInfinity + return s_part*inf_part + + def _as_mpf_val(self, prec): + return mlib.fninf + + def __hash__(self): + return super().__hash__() + + def __eq__(self, other): + return other is S.NegativeInfinity or other == float('-inf') + + def __ne__(self, other): + return other is not S.NegativeInfinity and other != float('-inf') + + __gt__ = Expr.__gt__ + __ge__ = Expr.__ge__ + __lt__ = Expr.__lt__ + __le__ = Expr.__le__ + + @_sympifyit('other', NotImplemented) + def __mod__(self, other): + if not isinstance(other, Expr): + return NotImplemented + return S.NaN + + __rmod__ = __mod__ + + def floor(self): + return self + + def ceiling(self): + return self + + def as_powers_dict(self): + return {S.NegativeOne: 1, S.Infinity: 1} + + +class NaN(Number, metaclass=Singleton): + """ + Not a Number. + + Explanation + =========== + + This serves as a place holder for numeric values that are indeterminate. + Most operations on NaN, produce another NaN. Most indeterminate forms, + such as ``0/0`` or ``oo - oo` produce NaN. Two exceptions are ``0**0`` + and ``oo**0``, which all produce ``1`` (this is consistent with Python's + float). + + NaN is loosely related to floating point nan, which is defined in the + IEEE 754 floating point standard, and corresponds to the Python + ``float('nan')``. Differences are noted below. + + NaN is mathematically not equal to anything else, even NaN itself. This + explains the initially counter-intuitive results with ``Eq`` and ``==`` in + the examples below. + + NaN is not comparable so inequalities raise a TypeError. This is in + contrast with floating point nan where all inequalities are false. + + NaN is a singleton, and can be accessed by ``S.NaN``, or can be imported + as ``nan``. + + Examples + ======== + + >>> from sympy import nan, S, oo, Eq + >>> nan is S.NaN + True + >>> oo - oo + nan + >>> nan + 1 + nan + >>> Eq(nan, nan) # mathematical equality + False + >>> nan == nan # structural equality + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/NaN + + """ + is_commutative = True + is_extended_real = None + is_real = None + is_rational = None + is_algebraic = None + is_transcendental = None + is_integer = None + is_comparable = False + is_finite = None + is_zero = None + is_prime = None + is_positive = None + is_negative = None + is_number = True + + __slots__ = () + + def __new__(cls): + return AtomicExpr.__new__(cls) + + def _latex(self, printer): + return r"\text{NaN}" + + def __neg__(self): + return self + + @_sympifyit('other', NotImplemented) + def __add__(self, other): + return self + + @_sympifyit('other', NotImplemented) + def __sub__(self, other): + return self + + @_sympifyit('other', NotImplemented) + def __mul__(self, other): + return self + + @_sympifyit('other', NotImplemented) + def __truediv__(self, other): + return self + + def floor(self): + return self + + def ceiling(self): + return self + + def _as_mpf_val(self, prec): + return _mpf_nan + + def __hash__(self): + return super().__hash__() + + def __eq__(self, other): + # NaN is structurally equal to another NaN + return other is S.NaN + + def __ne__(self, other): + return other is not S.NaN + + # Expr will _sympify and raise TypeError + __gt__ = Expr.__gt__ + __ge__ = Expr.__ge__ + __lt__ = Expr.__lt__ + __le__ = Expr.__le__ + +nan = S.NaN + +@dispatch(NaN, Expr) # type:ignore +def _eval_is_eq(a, b): # noqa:F811 + return False + + +class ComplexInfinity(AtomicExpr, metaclass=Singleton): + r"""Complex infinity. + + Explanation + =========== + + In complex analysis the symbol `\tilde\infty`, called "complex + infinity", represents a quantity with infinite magnitude, but + undetermined complex phase. + + ComplexInfinity is a singleton, and can be accessed by + ``S.ComplexInfinity``, or can be imported as ``zoo``. + + Examples + ======== + + >>> from sympy import zoo + >>> zoo + 42 + zoo + >>> 42/zoo + 0 + >>> zoo + zoo + nan + >>> zoo*zoo + zoo + + See Also + ======== + + Infinity + """ + + is_commutative = True + is_infinite = True + is_number = True + is_prime = False + is_complex = False + is_extended_real = False + + kind = NumberKind + + __slots__ = () + + def __new__(cls): + return AtomicExpr.__new__(cls) + + def _latex(self, printer): + return r"\tilde{\infty}" + + @staticmethod + def __abs__(): + return S.Infinity + + def floor(self): + return self + + def ceiling(self): + return self + + @staticmethod + def __neg__(): + return S.ComplexInfinity + + def _eval_power(self, expt): + if expt is S.ComplexInfinity: + return S.NaN + + if isinstance(expt, Number): + if expt.is_zero: + return S.NaN + else: + if expt.is_positive: + return S.ComplexInfinity + else: + return S.Zero + + +zoo = S.ComplexInfinity + + +class NumberSymbol(AtomicExpr): + + is_commutative = True + is_finite = True + is_number = True + + __slots__ = () + + is_NumberSymbol = True + + kind = NumberKind + + def __new__(cls): + return AtomicExpr.__new__(cls) + + def approximation(self, number_cls): + """ Return an interval with number_cls endpoints + that contains the value of NumberSymbol. + If not implemented, then return None. + """ + + def _eval_evalf(self, prec): + return Float._new(self._as_mpf_val(prec), prec) + + def __eq__(self, other): + try: + other = _sympify(other) + except SympifyError: + return NotImplemented + if self is other: + return True + if other.is_Number and self.is_irrational: + return False + + return False # NumberSymbol != non-(Number|self) + + def __ne__(self, other): + return not self == other + + def __le__(self, other): + if self is other: + return S.true + return Expr.__le__(self, other) + + def __ge__(self, other): + if self is other: + return S.true + return Expr.__ge__(self, other) + + def __int__(self): + # subclass with appropriate return value + raise NotImplementedError + + def __hash__(self): + return super().__hash__() + + +class Exp1(NumberSymbol, metaclass=Singleton): + r"""The `e` constant. + + Explanation + =========== + + The transcendental number `e = 2.718281828\ldots` is the base of the + natural logarithm and of the exponential function, `e = \exp(1)`. + Sometimes called Euler's number or Napier's constant. + + Exp1 is a singleton, and can be accessed by ``S.Exp1``, + or can be imported as ``E``. + + Examples + ======== + + >>> from sympy import exp, log, E + >>> E is exp(1) + True + >>> log(E) + 1 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/E_%28mathematical_constant%29 + """ + + is_real = True + is_positive = True + is_negative = False # XXX Forces is_negative/is_nonnegative + is_irrational = True + is_number = True + is_algebraic = False + is_transcendental = True + + __slots__ = () + + def _latex(self, printer): + return r"e" + + @staticmethod + def __abs__(): + return S.Exp1 + + def __int__(self): + return 2 + + def _as_mpf_val(self, prec): + return mpf_e(prec) + + def approximation_interval(self, number_cls): + if issubclass(number_cls, Integer): + return (Integer(2), Integer(3)) + elif issubclass(number_cls, Rational): + pass + + def _eval_power(self, expt): + if global_parameters.exp_is_pow: + return self._eval_power_exp_is_pow(expt) + else: + from sympy.functions.elementary.exponential import exp + return exp(expt) + + def _eval_power_exp_is_pow(self, arg): + if arg.is_Number: + if arg is oo: + return oo + elif arg == -oo: + return S.Zero + from sympy.functions.elementary.exponential import log + if isinstance(arg, log): + return arg.args[0] + + # don't autoexpand Pow or Mul (see the issue 3351): + elif not arg.is_Add: + Ioo = I*oo + if arg in [Ioo, -Ioo]: + return nan + + coeff = arg.coeff(pi*I) + if coeff: + if (2*coeff).is_integer: + if coeff.is_even: + return S.One + elif coeff.is_odd: + return S.NegativeOne + elif (coeff + S.Half).is_even: + return -I + elif (coeff + S.Half).is_odd: + return I + elif coeff.is_Rational: + ncoeff = coeff % 2 # restrict to [0, 2pi) + if ncoeff > 1: # restrict to (-pi, pi] + ncoeff -= 2 + if ncoeff != coeff: + return S.Exp1**(ncoeff*S.Pi*S.ImaginaryUnit) + + # Warning: code in risch.py will be very sensitive to changes + # in this (see DifferentialExtension). + + # look for a single log factor + + coeff, terms = arg.as_coeff_Mul() + + # but it can't be multiplied by oo + if coeff in (oo, -oo): + return + + coeffs, log_term = [coeff], None + for term in Mul.make_args(terms): + if isinstance(term, log): + if log_term is None: + log_term = term.args[0] + else: + return + elif term.is_comparable: + coeffs.append(term) + else: + return + + return log_term**Mul(*coeffs) if log_term else None + elif arg.is_Add: + out = [] + add = [] + argchanged = False + for a in arg.args: + if a is S.One: + add.append(a) + continue + newa = self**a + if isinstance(newa, Pow) and newa.base is self: + if newa.exp != a: + add.append(newa.exp) + argchanged = True + else: + add.append(a) + else: + out.append(newa) + if out or argchanged: + return Mul(*out)*Pow(self, Add(*add), evaluate=False) + elif arg.is_Matrix: + return arg.exp() + + def _eval_rewrite_as_sin(self, **kwargs): + from sympy.functions.elementary.trigonometric import sin + return sin(I + S.Pi/2) - I*sin(I) + + def _eval_rewrite_as_cos(self, **kwargs): + from sympy.functions.elementary.trigonometric import cos + return cos(I) + I*cos(I + S.Pi/2) + +E = S.Exp1 + + +class Pi(NumberSymbol, metaclass=Singleton): + r"""The `\pi` constant. + + Explanation + =========== + + The transcendental number `\pi = 3.141592654\ldots` represents the ratio + of a circle's circumference to its diameter, the area of the unit circle, + the half-period of trigonometric functions, and many other things + in mathematics. + + Pi is a singleton, and can be accessed by ``S.Pi``, or can + be imported as ``pi``. + + Examples + ======== + + >>> from sympy import S, pi, oo, sin, exp, integrate, Symbol + >>> S.Pi + pi + >>> pi > 3 + True + >>> pi.is_irrational + True + >>> x = Symbol('x') + >>> sin(x + 2*pi) + sin(x) + >>> integrate(exp(-x**2), (x, -oo, oo)) + sqrt(pi) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Pi + """ + + is_real = True + is_positive = True + is_negative = False + is_irrational = True + is_number = True + is_algebraic = False + is_transcendental = True + + __slots__ = () + + def _latex(self, printer): + return r"\pi" + + @staticmethod + def __abs__(): + return S.Pi + + def __int__(self): + return 3 + + def _as_mpf_val(self, prec): + return mpf_pi(prec) + + def approximation_interval(self, number_cls): + if issubclass(number_cls, Integer): + return (Integer(3), Integer(4)) + elif issubclass(number_cls, Rational): + return (Rational(223, 71, 1), Rational(22, 7, 1)) + +pi = S.Pi + + +class GoldenRatio(NumberSymbol, metaclass=Singleton): + r"""The golden ratio, `\phi`. + + Explanation + =========== + + `\phi = \frac{1 + \sqrt{5}}{2}` is an algebraic number. Two quantities + are in the golden ratio if their ratio is the same as the ratio of + their sum to the larger of the two quantities, i.e. their maximum. + + GoldenRatio is a singleton, and can be accessed by ``S.GoldenRatio``. + + Examples + ======== + + >>> from sympy import S + >>> S.GoldenRatio > 1 + True + >>> S.GoldenRatio.expand(func=True) + 1/2 + sqrt(5)/2 + >>> S.GoldenRatio.is_irrational + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Golden_ratio + """ + + is_real = True + is_positive = True + is_negative = False + is_irrational = True + is_number = True + is_algebraic = True + is_transcendental = False + + __slots__ = () + + def _latex(self, printer): + return r"\phi" + + def __int__(self): + return 1 + + def _as_mpf_val(self, prec): + # XXX track down why this has to be increased + rv = mlib.from_man_exp(phi_fixed(prec + 10), -prec - 10) + return mpf_norm(rv, prec) + + def _eval_expand_func(self, **hints): + from sympy.functions.elementary.miscellaneous import sqrt + return S.Half + S.Half*sqrt(5) + + def approximation_interval(self, number_cls): + if issubclass(number_cls, Integer): + return (S.One, Rational(2)) + elif issubclass(number_cls, Rational): + pass + + _eval_rewrite_as_sqrt = _eval_expand_func + + +class TribonacciConstant(NumberSymbol, metaclass=Singleton): + r"""The tribonacci constant. + + Explanation + =========== + + The tribonacci numbers are like the Fibonacci numbers, but instead + of starting with two predetermined terms, the sequence starts with + three predetermined terms and each term afterwards is the sum of the + preceding three terms. + + The tribonacci constant is the ratio toward which adjacent tribonacci + numbers tend. It is a root of the polynomial `x^3 - x^2 - x - 1 = 0`, + and also satisfies the equation `x + x^{-3} = 2`. + + TribonacciConstant is a singleton, and can be accessed + by ``S.TribonacciConstant``. + + Examples + ======== + + >>> from sympy import S + >>> S.TribonacciConstant > 1 + True + >>> S.TribonacciConstant.expand(func=True) + 1/3 + (19 - 3*sqrt(33))**(1/3)/3 + (3*sqrt(33) + 19)**(1/3)/3 + >>> S.TribonacciConstant.is_irrational + True + >>> S.TribonacciConstant.n(20) + 1.8392867552141611326 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Generalizations_of_Fibonacci_numbers#Tribonacci_numbers + """ + + is_real = True + is_positive = True + is_negative = False + is_irrational = True + is_number = True + is_algebraic = True + is_transcendental = False + + __slots__ = () + + def _latex(self, printer): + return r"\text{TribonacciConstant}" + + def __int__(self): + return 1 + + def _as_mpf_val(self, prec): + return self._eval_evalf(prec)._mpf_ + + def _eval_evalf(self, prec): + rv = self._eval_expand_func(function=True)._eval_evalf(prec + 4) + return Float(rv, precision=prec) + + def _eval_expand_func(self, **hints): + from sympy.functions.elementary.miscellaneous import cbrt, sqrt + return (1 + cbrt(19 - 3*sqrt(33)) + cbrt(19 + 3*sqrt(33))) / 3 + + def approximation_interval(self, number_cls): + if issubclass(number_cls, Integer): + return (S.One, Rational(2)) + elif issubclass(number_cls, Rational): + pass + + _eval_rewrite_as_sqrt = _eval_expand_func + + +class EulerGamma(NumberSymbol, metaclass=Singleton): + r"""The Euler-Mascheroni constant. + + Explanation + =========== + + `\gamma = 0.5772157\ldots` (also called Euler's constant) is a mathematical + constant recurring in analysis and number theory. It is defined as the + limiting difference between the harmonic series and the + natural logarithm: + + .. math:: \gamma = \lim\limits_{n\to\infty} + \left(\sum\limits_{k=1}^n\frac{1}{k} - \ln n\right) + + EulerGamma is a singleton, and can be accessed by ``S.EulerGamma``. + + Examples + ======== + + >>> from sympy import S + >>> S.EulerGamma.is_irrational + >>> S.EulerGamma > 0 + True + >>> S.EulerGamma > 1 + False + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Euler%E2%80%93Mascheroni_constant + """ + + is_real = True + is_positive = True + is_negative = False + is_irrational = None + is_number = True + + __slots__ = () + + def _latex(self, printer): + return r"\gamma" + + def __int__(self): + return 0 + + def _as_mpf_val(self, prec): + # XXX track down why this has to be increased + v = mlib.libhyper.euler_fixed(prec + 10) + rv = mlib.from_man_exp(v, -prec - 10) + return mpf_norm(rv, prec) + + def approximation_interval(self, number_cls): + if issubclass(number_cls, Integer): + return (S.Zero, S.One) + elif issubclass(number_cls, Rational): + return (S.Half, Rational(3, 5, 1)) + + +class Catalan(NumberSymbol, metaclass=Singleton): + r"""Catalan's constant. + + Explanation + =========== + + $G = 0.91596559\ldots$ is given by the infinite series + + .. math:: G = \sum_{k=0}^{\infty} \frac{(-1)^k}{(2k+1)^2} + + Catalan is a singleton, and can be accessed by ``S.Catalan``. + + Examples + ======== + + >>> from sympy import S + >>> S.Catalan.is_irrational + >>> S.Catalan > 0 + True + >>> S.Catalan > 1 + False + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Catalan%27s_constant + """ + + is_real = True + is_positive = True + is_negative = False + is_irrational = None + is_number = True + + __slots__ = () + + def __int__(self): + return 0 + + def _as_mpf_val(self, prec): + # XXX track down why this has to be increased + v = mlib.catalan_fixed(prec + 10) + rv = mlib.from_man_exp(v, -prec - 10) + return mpf_norm(rv, prec) + + def approximation_interval(self, number_cls): + if issubclass(number_cls, Integer): + return (S.Zero, S.One) + elif issubclass(number_cls, Rational): + return (Rational(9, 10, 1), S.One) + + def _eval_rewrite_as_Sum(self, k_sym=None, symbols=None, **hints): + if (k_sym is not None) or (symbols is not None): + return self + from .symbol import Dummy + from sympy.concrete.summations import Sum + k = Dummy('k', integer=True, nonnegative=True) + return Sum(S.NegativeOne**k / (2*k+1)**2, (k, 0, S.Infinity)) + + def _latex(self, printer): + return "G" + + +class ImaginaryUnit(AtomicExpr, metaclass=Singleton): + r"""The imaginary unit, `i = \sqrt{-1}`. + + I is a singleton, and can be accessed by ``S.I``, or can be + imported as ``I``. + + Examples + ======== + + >>> from sympy import I, sqrt + >>> sqrt(-1) + I + >>> I*I + -1 + >>> 1/I + -I + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Imaginary_unit + """ + + is_commutative = True + is_imaginary = True + is_finite = True + is_number = True + is_algebraic = True + is_transcendental = False + + kind = NumberKind + + __slots__ = () + + def _latex(self, printer): + return printer._settings['imaginary_unit_latex'] + + @staticmethod + def __abs__(): + return S.One + + def _eval_evalf(self, prec): + return self + + def _eval_conjugate(self): + return -S.ImaginaryUnit + + def _eval_power(self, expt): + """ + b is I = sqrt(-1) + e is symbolic object but not equal to 0, 1 + + I**r -> (-1)**(r/2) -> exp(r/2*Pi*I) -> sin(Pi*r/2) + cos(Pi*r/2)*I, r is decimal + I**0 mod 4 -> 1 + I**1 mod 4 -> I + I**2 mod 4 -> -1 + I**3 mod 4 -> -I + """ + + if isinstance(expt, Integer): + expt = expt % 4 + if expt == 0: + return S.One + elif expt == 1: + return S.ImaginaryUnit + elif expt == 2: + return S.NegativeOne + elif expt == 3: + return -S.ImaginaryUnit + if isinstance(expt, Rational): + i, r = divmod(expt, 2) + rv = Pow(S.ImaginaryUnit, r, evaluate=False) + if i % 2: + return Mul(S.NegativeOne, rv, evaluate=False) + return rv + + def as_base_exp(self): + return S.NegativeOne, S.Half + + @property + def _mpc_(self): + return (Float(0)._mpf_, Float(1)._mpf_) + + +I = S.ImaginaryUnit + + +def int_valued(x): + """return True only for a literal Number whose internal + representation as a fraction has a denominator of 1, + else False, i.e. integer, with no fractional part. + """ + if isinstance(x, (SYMPY_INTS, int)): + return True + if type(x) is float: + return x.is_integer() + if isinstance(x, Integer): + return True + if isinstance(x, Float): + # x = s*m*2**p; _mpf_ = s,m,e,p + return x._mpf_[2] >= 0 + return False # or add new types to recognize + + +def equal_valued(x, y): + """Compare expressions treating plain floats as rationals. + + Examples + ======== + + >>> from sympy import S, symbols, Rational, Float + >>> from sympy.core.numbers import equal_valued + >>> equal_valued(1, 2) + False + >>> equal_valued(1, 1) + True + + In SymPy expressions with Floats compare unequal to corresponding + expressions with rationals: + + >>> x = symbols('x') + >>> x**2 == x**2.0 + False + + However an individual Float compares equal to a Rational: + + >>> Rational(1, 2) == Float(0.5) + False + + In a future version of SymPy this might change so that Rational and Float + compare unequal. This function provides the behavior currently expected of + ``==`` so that it could still be used if the behavior of ``==`` were to + change in future. + + >>> equal_valued(1, 1.0) # Float vs Rational + True + >>> equal_valued(S(1).n(3), S(1).n(5)) # Floats of different precision + True + + Explanation + =========== + + In future SymPy versions Float and Rational might compare unequal and floats + with different precisions might compare unequal. In that context a function + is needed that can check if a number is equal to 1 or 0 etc. The idea is + that instead of testing ``if x == 1:`` if we want to accept floats like + ``1.0`` as well then the test can be written as ``if equal_valued(x, 1):`` + or ``if equal_valued(x, 2):``. Since this function is intended to be used + in situations where one or both operands are expected to be concrete + numbers like 1 or 0 the function does not recurse through the args of any + compound expression to compare any nested floats. + + References + ========== + + .. [1] https://github.com/sympy/sympy/pull/20033 + """ + x = _sympify(x) + y = _sympify(y) + + # Handle everything except Float/Rational first + if not x.is_Float and not y.is_Float: + return x == y + elif x.is_Float and y.is_Float: + # Compare values without regard for precision + return x._mpf_ == y._mpf_ + elif x.is_Float: + x, y = y, x + if not x.is_Rational: + return False + + # Now y is Float and x is Rational. A simple approach at this point would + # just be x == Rational(y) but if y has a large exponent creating a + # Rational could be prohibitively expensive. + + sign, man, exp, _ = y._mpf_ + p, q = x.p, x.q + + if sign: + man = -man + + if exp == 0: + # y odd integer + return q == 1 and man == p + elif exp > 0: + # y even integer + if q != 1: + return False + if p.bit_length() != man.bit_length() + exp: + return False + return man << exp == p + else: + # y non-integer. Need p == man and q == 2**-exp + if p != man: + return False + neg_exp = -exp + if q.bit_length() - 1 != neg_exp: + return False + return (1 << neg_exp) == q + + +def all_close(expr1, expr2, rtol=1e-5, atol=1e-8): + """Return True if expr1 and expr2 are numerically close. + + The expressions must have the same structure, but any Rational, Integer, or + Float numbers they contain are compared approximately using rtol and atol. + Any other parts of expressions are compared exactly. However, allowance is + made to allow for the additive and multiplicative identities. + + Relative tolerance is measured with respect to expr2 so when used in + testing expr2 should be the expected correct answer. + + Examples + ======== + + >>> from sympy import exp + >>> from sympy.abc import x, y + >>> from sympy.core.numbers import all_close + >>> expr1 = 0.1*exp(x - y) + >>> expr2 = exp(x - y)/10 + >>> expr1 + 0.1*exp(x - y) + >>> expr2 + exp(x - y)/10 + >>> expr1 == expr2 + False + >>> all_close(expr1, expr2) + True + + Identities are automatically supplied: + + >>> all_close(x, x + 1e-10) + True + >>> all_close(x, 1.0*x) + True + >>> all_close(x, 1.0*x + 1e-10) + True + + """ + NUM_TYPES = (Rational, Float) + + def _all_close(obj1, obj2): + if type(obj1) == type(obj2) and isinstance(obj1, (list, tuple)): + if len(obj1) != len(obj2): + return False + return all(_all_close(e1, e2) for e1, e2 in zip(obj1, obj2)) + else: + return _all_close_expr(_sympify(obj1), _sympify(obj2)) + + def _all_close_expr(expr1, expr2): + num1 = isinstance(expr1, NUM_TYPES) + num2 = isinstance(expr2, NUM_TYPES) + if num1 != num2: + return False + elif num1: + return _close_num(expr1, expr2) + if expr1.is_Add or expr1.is_Mul or expr2.is_Add or expr2.is_Mul: + return _all_close_ac(expr1, expr2) + if expr1.func != expr2.func or len(expr1.args) != len(expr2.args): + return False + args = zip(expr1.args, expr2.args) + return all(_all_close_expr(a1, a2) for a1, a2 in args) + + def _close_num(num1, num2): + return bool(abs(num1 - num2) <= atol + rtol*abs(num2)) + + def _all_close_ac(expr1, expr2): + # compare expressions with associative commutative operators for + # approximate equality by seeing that all terms have equivalent + # coefficients (which are always Rational or Float) + if expr1.is_Mul or expr2.is_Mul: + # as_coeff_mul automatically will supply coeff of 1 + c1, e1 = expr1.as_coeff_mul(rational=False) + c2, e2 = expr2.as_coeff_mul(rational=False) + if not _close_num(c1, c2): + return False + s1 = set(e1) + s2 = set(e2) + common = s1 & s2 + s1 -= common + s2 -= common + if not s1: + return True + if not any(i.has(Float) for j in (s1, s2) for i in j): + return False + # factors might not be matching, e.g. + # x != x**1.0, exp(x) != exp(1.0*x), etc... + s1 = [i.as_base_exp() for i in ordered(s1)] + s2 = [i.as_base_exp() for i in ordered(s2)] + unmatched = list(range(len(s1))) + for be1 in s1: + for i in unmatched: + be2 = s2[i] + if _all_close(be1, be2): + unmatched.remove(i) + break + else: + return False + return not(unmatched) + assert expr1.is_Add or expr2.is_Add + cd1 = expr1.as_coefficients_dict() + cd2 = expr2.as_coefficients_dict() + # this test will assure that the key of 1 is in + # each dict and that they have equal values + if not _close_num(cd1[1], cd2[1]): + return False + if len(cd1) != len(cd2): + return False + for k in list(cd1): + if k in cd2: + if not _close_num(cd1.pop(k), cd2.pop(k)): + return False + # k (or a close version in cd2) might have + # Floats in a factor of the term which will + # be handled below + else: + if not cd1: + return True + for k1 in cd1: + for k2 in cd2: + if _all_close_expr(k1, k2): + # found a matching key + # XXX there could be a corner case where + # more than 1 might match and the numbers are + # such that one is better than the other + # that is not being considered here + if not _close_num(cd1[k1], cd2[k2]): + return False + break + else: + # no key matched + return False + return True + + return _all_close(expr1, expr2) + + +@dispatch(Tuple, Number) # type:ignore +def _eval_is_eq(self, other): # noqa: F811 + return False + + +def sympify_fractions(f): + return Rational._new(f.numerator, f.denominator, 1) + +_sympy_converter[fractions.Fraction] = sympify_fractions + + +if gmpy is not None: + + def sympify_mpz(x): + return Integer(int(x)) + + def sympify_mpq(x): + return Rational(int(x.numerator), int(x.denominator)) + + _sympy_converter[type(gmpy.mpz(1))] = sympify_mpz + _sympy_converter[type(gmpy.mpq(1, 2))] = sympify_mpq + + +if flint is not None: + + def sympify_fmpz(x): + return Integer(int(x)) + + def sympify_fmpq(x): + return Rational(int(x.numerator), int(x.denominator)) + + _sympy_converter[type(flint.fmpz(1))] = sympify_fmpz + _sympy_converter[type(flint.fmpq(1, 2))] = sympify_fmpq + + +def sympify_mpmath(x): + return Expr._from_mpmath(x, x.context.prec) + +_sympy_converter[mpnumeric] = sympify_mpmath + + +def sympify_complex(a): + real, imag = list(map(sympify, (a.real, a.imag))) + return real + S.ImaginaryUnit*imag + +_sympy_converter[complex] = sympify_complex + +from .power import Pow +from .mul import Mul +Mul.identity = One() +from .add import Add +Add.identity = Zero() + + +def _register_classes(): + numbers.Number.register(Number) + numbers.Real.register(Float) + numbers.Rational.register(Rational) + numbers.Integral.register(Integer) + +_register_classes() + +_illegal = (S.NaN, S.Infinity, S.NegativeInfinity, S.ComplexInfinity) diff --git a/venv/lib/python3.11/site-packages/sympy/core/operations.py b/venv/lib/python3.11/site-packages/sympy/core/operations.py new file mode 100644 index 0000000000000000000000000000000000000000..70d22127eb4d3f69fc5e304ab38f5cce9c4bb551 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/operations.py @@ -0,0 +1,741 @@ +from __future__ import annotations + +from typing import overload, TYPE_CHECKING + +from operator import attrgetter +from collections import defaultdict + +from sympy.utilities.exceptions import sympy_deprecation_warning + +from .sympify import _sympify as _sympify_, sympify +from .basic import Basic +from .cache import cacheit +from .sorting import ordered +from .logic import fuzzy_and +from .parameters import global_parameters +from sympy.utilities.iterables import sift +from sympy.multipledispatch.dispatcher import (Dispatcher, + ambiguity_register_error_ignore_dup, + str_signature, RaiseNotImplementedError) + + +if TYPE_CHECKING: + from sympy.core.expr import Expr + from sympy.core.add import Add + from sympy.core.mul import Mul + from sympy.logic.boolalg import Boolean, And, Or + + +class AssocOp(Basic): + """ Associative operations, can separate noncommutative and + commutative parts. + + (a op b) op c == a op (b op c) == a op b op c. + + Base class for Add and Mul. + + This is an abstract base class, concrete derived classes must define + the attribute `identity`. + + .. deprecated:: 1.7 + + Using arguments that aren't subclasses of :class:`~.Expr` in core + operators (:class:`~.Mul`, :class:`~.Add`, and :class:`~.Pow`) is + deprecated. See :ref:`non-expr-args-deprecated` for details. + + Parameters + ========== + + *args : + Arguments which are operated + + evaluate : bool, optional + Evaluate the operation. If not passed, refer to ``global_parameters.evaluate``. + """ + + # for performance reason, we don't let is_commutative go to assumptions, + # and keep it right here + __slots__: tuple[str, ...] = ('is_commutative',) + + _args_type: type[Basic] | None = None + + @cacheit + def __new__(cls, *args, evaluate=None, _sympify=True): + # Allow faster processing by passing ``_sympify=False``, if all arguments + # are already sympified. + if _sympify: + args = list(map(_sympify_, args)) + + # Disallow non-Expr args in Add/Mul + typ = cls._args_type + if typ is not None: + from .relational import Relational + if any(isinstance(arg, Relational) for arg in args): + raise TypeError("Relational cannot be used in %s" % cls.__name__) + + # This should raise TypeError once deprecation period is over: + for arg in args: + if not isinstance(arg, typ): + sympy_deprecation_warning( + f""" + +Using non-Expr arguments in {cls.__name__} is deprecated (in this case, one of +the arguments has type {type(arg).__name__!r}). + +If you really did intend to use a multiplication or addition operation with +this object, use the * or + operator instead. + + """, + deprecated_since_version="1.7", + active_deprecations_target="non-expr-args-deprecated", + stacklevel=4, + ) + + if evaluate is None: + evaluate = global_parameters.evaluate + if not evaluate: + obj = cls._from_args(args) + obj = cls._exec_constructor_postprocessors(obj) + return obj + + args = [a for a in args if a is not cls.identity] + + if len(args) == 0: + return cls.identity + if len(args) == 1: + return args[0] + + c_part, nc_part, order_symbols = cls.flatten(args) + is_commutative = not nc_part + obj = cls._from_args(c_part + nc_part, is_commutative) + obj = cls._exec_constructor_postprocessors(obj) + + if order_symbols is not None: + from sympy.series.order import Order + return Order(obj, *order_symbols) + return obj + + @classmethod + def _from_args(cls, args, is_commutative=None): + """Create new instance with already-processed args. + If the args are not in canonical order, then a non-canonical + result will be returned, so use with caution. The order of + args may change if the sign of the args is changed.""" + if len(args) == 0: + return cls.identity + elif len(args) == 1: + return args[0] + + obj = super().__new__(cls, *args) + if is_commutative is None: + is_commutative = fuzzy_and(a.is_commutative for a in args) + obj.is_commutative = is_commutative + return obj + + def _new_rawargs(self, *args, reeval=True, **kwargs): + """Create new instance of own class with args exactly as provided by + caller but returning the self class identity if args is empty. + + Examples + ======== + + This is handy when we want to optimize things, e.g. + + >>> from sympy import Mul, S + >>> from sympy.abc import x, y + >>> e = Mul(3, x, y) + >>> e.args + (3, x, y) + >>> Mul(*e.args[1:]) + x*y + >>> e._new_rawargs(*e.args[1:]) # the same as above, but faster + x*y + + Note: use this with caution. There is no checking of arguments at + all. This is best used when you are rebuilding an Add or Mul after + simply removing one or more args. If, for example, modifications, + result in extra 1s being inserted they will show up in the result: + + >>> m = (x*y)._new_rawargs(S.One, x); m + 1*x + >>> m == x + False + >>> m.is_Mul + True + + Another issue to be aware of is that the commutativity of the result + is based on the commutativity of self. If you are rebuilding the + terms that came from a commutative object then there will be no + problem, but if self was non-commutative then what you are + rebuilding may now be commutative. + + Although this routine tries to do as little as possible with the + input, getting the commutativity right is important, so this level + of safety is enforced: commutativity will always be recomputed if + self is non-commutative and kwarg `reeval=False` has not been + passed. + """ + if reeval and self.is_commutative is False: + is_commutative = None + else: + is_commutative = self.is_commutative + return self._from_args(args, is_commutative) + + @classmethod + def flatten(cls, seq): + """Return seq so that none of the elements are of type `cls`. This is + the vanilla routine that will be used if a class derived from AssocOp + does not define its own flatten routine.""" + # apply associativity, no commutativity property is used + new_seq = [] + while seq: + o = seq.pop() + if o.__class__ is cls: # classes must match exactly + seq.extend(o.args) + else: + new_seq.append(o) + new_seq.reverse() + + # c_part, nc_part, order_symbols + return [], new_seq, None + + def _matches_commutative(self, expr, repl_dict=None, old=False): + """ + Matches Add/Mul "pattern" to an expression "expr". + + repl_dict ... a dictionary of (wild: expression) pairs, that get + returned with the results + + This function is the main workhorse for Add/Mul. + + Examples + ======== + + >>> from sympy import symbols, Wild, sin + >>> a = Wild("a") + >>> b = Wild("b") + >>> c = Wild("c") + >>> x, y, z = symbols("x y z") + >>> (a+sin(b)*c)._matches_commutative(x+sin(y)*z) + {a_: x, b_: y, c_: z} + + In the example above, "a+sin(b)*c" is the pattern, and "x+sin(y)*z" is + the expression. + + The repl_dict contains parts that were already matched. For example + here: + + >>> (x+sin(b)*c)._matches_commutative(x+sin(y)*z, repl_dict={a: x}) + {a_: x, b_: y, c_: z} + + the only function of the repl_dict is to return it in the + result, e.g. if you omit it: + + >>> (x+sin(b)*c)._matches_commutative(x+sin(y)*z) + {b_: y, c_: z} + + the "a: x" is not returned in the result, but otherwise it is + equivalent. + + """ + from .function import _coeff_isneg + # make sure expr is Expr if pattern is Expr + from .expr import Expr + if isinstance(self, Expr) and not isinstance(expr, Expr): + return None + + if repl_dict is None: + repl_dict = {} + + # handle simple patterns + if self == expr: + return repl_dict + + d = self._matches_simple(expr, repl_dict) + if d is not None: + return d + + # eliminate exact part from pattern: (2+a+w1+w2).matches(expr) -> (w1+w2).matches(expr-a-2) + from .function import WildFunction + from .symbol import Wild + wild_part, exact_part = sift(self.args, lambda p: + p.has(Wild, WildFunction) and not expr.has(p), + binary=True) + if not exact_part: + wild_part = list(ordered(wild_part)) + if self.is_Add: + # in addition to normal ordered keys, impose + # sorting on Muls with leading Number to put + # them in order + wild_part = sorted(wild_part, key=lambda x: + x.args[0] if x.is_Mul and x.args[0].is_Number else + 0) + else: + exact = self._new_rawargs(*exact_part) + free = expr.free_symbols + if free and (exact.free_symbols - free): + # there are symbols in the exact part that are not + # in the expr; but if there are no free symbols, let + # the matching continue + return None + newexpr = self._combine_inverse(expr, exact) + if not old and (expr.is_Add or expr.is_Mul): + check = newexpr + if _coeff_isneg(check): + check = -check + if check.count_ops() > expr.count_ops(): + return None + newpattern = self._new_rawargs(*wild_part) + return newpattern.matches(newexpr, repl_dict) + + # now to real work ;) + i = 0 + saw = set() + while expr not in saw: + saw.add(expr) + args = tuple(ordered(self.make_args(expr))) + if self.is_Add and expr.is_Add: + # in addition to normal ordered keys, impose + # sorting on Muls with leading Number to put + # them in order + args = tuple(sorted(args, key=lambda x: + x.args[0] if x.is_Mul and x.args[0].is_Number else + 0)) + expr_list = (self.identity,) + args + for last_op in reversed(expr_list): + for w in reversed(wild_part): + d1 = w.matches(last_op, repl_dict) + if d1 is not None: + d2 = self.xreplace(d1).matches(expr, d1) + if d2 is not None: + return d2 + + if i == 0: + if self.is_Mul: + # make e**i look like Mul + if expr.is_Pow and expr.exp.is_Integer: + from .mul import Mul + if expr.exp > 0: + expr = Mul(*[expr.base, expr.base**(expr.exp - 1)], evaluate=False) + else: + expr = Mul(*[1/expr.base, expr.base**(expr.exp + 1)], evaluate=False) + i += 1 + continue + + elif self.is_Add: + # make i*e look like Add + c, e = expr.as_coeff_Mul() + if abs(c) > 1: + from .add import Add + if c > 0: + expr = Add(*[e, (c - 1)*e], evaluate=False) + else: + expr = Add(*[-e, (c + 1)*e], evaluate=False) + i += 1 + continue + + # try collection on non-Wild symbols + from sympy.simplify.radsimp import collect + was = expr + did = set() + for w in reversed(wild_part): + c, w = w.as_coeff_mul(Wild) + free = c.free_symbols - did + if free: + did.update(free) + expr = collect(expr, free) + if expr != was: + i += 0 + continue + + break # if we didn't continue, there is nothing more to do + + return + + def _has_matcher(self): + """Helper for .has() that checks for containment of + subexpressions within an expr by using sets of args + of similar nodes, e.g. x + 1 in x + y + 1 checks + to see that {x, 1} & {x, y, 1} == {x, 1} + """ + def _ncsplit(expr): + # this is not the same as args_cnc because here + # we don't assume expr is a Mul -- hence deal with args -- + # and always return a set. + cpart, ncpart = sift(expr.args, + lambda arg: arg.is_commutative is True, binary=True) + return set(cpart), ncpart + + c, nc = _ncsplit(self) + cls = self.__class__ + + def is_in(expr): + if isinstance(expr, cls): + if expr == self: + return True + _c, _nc = _ncsplit(expr) + if (c & _c) == c: + if not nc: + return True + elif len(nc) <= len(_nc): + for i in range(len(_nc) - len(nc) + 1): + if _nc[i:i + len(nc)] == nc: + return True + return False + return is_in + + def _eval_evalf(self, prec): + """ + Evaluate the parts of self that are numbers; if the whole thing + was a number with no functions it would have been evaluated, but + it wasn't so we must judiciously extract the numbers and reconstruct + the object. This is *not* simply replacing numbers with evaluated + numbers. Numbers should be handled in the largest pure-number + expression as possible. So the code below separates ``self`` into + number and non-number parts and evaluates the number parts and + walks the args of the non-number part recursively (doing the same + thing). + """ + from .add import Add + from .mul import Mul + from .symbol import Symbol + from .function import AppliedUndef + if isinstance(self, (Mul, Add)): + x, tail = self.as_independent(Symbol, AppliedUndef) + # if x is an AssocOp Function then the _evalf below will + # call _eval_evalf (here) so we must break the recursion + if not (tail is self.identity or + isinstance(x, AssocOp) and x.is_Function or + x is self.identity and isinstance(tail, AssocOp)): + # here, we have a number so we just call to _evalf with prec; + # prec is not the same as n, it is the binary precision so + # that's why we don't call to evalf. + x = x._evalf(prec) if x is not self.identity else self.identity + args = [] + tail_args = tuple(self.func.make_args(tail)) + for a in tail_args: + # here we call to _eval_evalf since we don't know what we + # are dealing with and all other _eval_evalf routines should + # be doing the same thing (i.e. taking binary prec and + # finding the evalf-able args) + newa = a._eval_evalf(prec) + if newa is None: + args.append(a) + else: + args.append(newa) + return self.func(x, *args) + + # this is the same as above, but there were no pure-number args to + # deal with + args = [] + for a in self.args: + newa = a._eval_evalf(prec) + if newa is None: + args.append(a) + else: + args.append(newa) + return self.func(*args) + + @overload + @classmethod + def make_args(cls: type[Add], expr: Expr) -> tuple[Expr, ...]: ... # type: ignore + @overload + @classmethod + def make_args(cls: type[Mul], expr: Expr) -> tuple[Expr, ...]: ... # type: ignore + @overload + @classmethod + def make_args(cls: type[And], expr: Boolean) -> tuple[Boolean, ...]: ... # type: ignore + @overload + @classmethod + def make_args(cls: type[Or], expr: Boolean) -> tuple[Boolean, ...]: ... # type: ignore + + @classmethod + def make_args(cls: type[Basic], expr: Basic) -> tuple[Basic, ...]: + """ + Return a sequence of elements `args` such that cls(*args) == expr + + Examples + ======== + + >>> from sympy import Symbol, Mul, Add + >>> x, y = map(Symbol, 'xy') + + >>> Mul.make_args(x*y) + (x, y) + >>> Add.make_args(x*y) + (x*y,) + >>> set(Add.make_args(x*y + y)) == set([y, x*y]) + True + + """ + if isinstance(expr, cls): + return expr.args + else: + return (sympify(expr),) + + def doit(self, **hints): + if hints.get('deep', True): + terms = [term.doit(**hints) for term in self.args] + else: + terms = self.args + return self.func(*terms, evaluate=True) + +class ShortCircuit(Exception): + pass + + +class LatticeOp(AssocOp): + """ + Join/meet operations of an algebraic lattice[1]. + + Explanation + =========== + + These binary operations are associative (op(op(a, b), c) = op(a, op(b, c))), + commutative (op(a, b) = op(b, a)) and idempotent (op(a, a) = op(a) = a). + Common examples are AND, OR, Union, Intersection, max or min. They have an + identity element (op(identity, a) = a) and an absorbing element + conventionally called zero (op(zero, a) = zero). + + This is an abstract base class, concrete derived classes must declare + attributes zero and identity. All defining properties are then respected. + + Examples + ======== + + >>> from sympy import Integer + >>> from sympy.core.operations import LatticeOp + >>> class my_join(LatticeOp): + ... zero = Integer(0) + ... identity = Integer(1) + >>> my_join(2, 3) == my_join(3, 2) + True + >>> my_join(2, my_join(3, 4)) == my_join(2, 3, 4) + True + >>> my_join(0, 1, 4, 2, 3, 4) + 0 + >>> my_join(1, 2) + 2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Lattice_%28order%29 + """ + + is_commutative = True + + def __new__(cls, *args, **options): + args = (_sympify_(arg) for arg in args) + + try: + # /!\ args is a generator and _new_args_filter + # must be careful to handle as such; this + # is done so short-circuiting can be done + # without having to sympify all values + _args = frozenset(cls._new_args_filter(args)) + except ShortCircuit: + return sympify(cls.zero) + if not _args: + return sympify(cls.identity) + elif len(_args) == 1: + return set(_args).pop() + else: + # XXX in almost every other case for __new__, *_args is + # passed along, but the expectation here is for _args + obj = super(AssocOp, cls).__new__(cls, *ordered(_args)) + obj._argset = _args + return obj + + @classmethod + def _new_args_filter(cls, arg_sequence, call_cls=None): + """Generator filtering args""" + ncls = call_cls or cls + for arg in arg_sequence: + if arg == ncls.zero: + raise ShortCircuit(arg) + elif arg == ncls.identity: + continue + elif arg.func == ncls: + yield from arg.args + else: + yield arg + + @classmethod + def make_args(cls, expr): + """ + Return a set of args such that cls(*arg_set) == expr. + """ + if isinstance(expr, cls): + return expr._argset + else: + return frozenset([sympify(expr)]) + + +class AssocOpDispatcher: + """ + Handler dispatcher for associative operators + + .. notes:: + This approach is experimental, and can be replaced or deleted in the future. + See https://github.com/sympy/sympy/pull/19463. + + Explanation + =========== + + If arguments of different types are passed, the classes which handle the operation for each type + are collected. Then, a class which performs the operation is selected by recursive binary dispatching. + Dispatching relation can be registered by ``register_handlerclass`` method. + + Priority registration is unordered. You cannot make ``A*B`` and ``B*A`` refer to + different handler classes. All logic dealing with the order of arguments must be implemented + in the handler class. + + Examples + ======== + + >>> from sympy import Add, Expr, Symbol + >>> from sympy.core.add import add + + >>> class NewExpr(Expr): + ... @property + ... def _add_handler(self): + ... return NewAdd + >>> class NewAdd(NewExpr, Add): + ... pass + >>> add.register_handlerclass((Add, NewAdd), NewAdd) + + >>> a, b = Symbol('a'), NewExpr() + >>> add(a, b) == NewAdd(a, b) + True + + """ + def __init__(self, name, doc=None): + self.name = name + self.doc = doc + self.handlerattr = "_%s_handler" % name + self._handlergetter = attrgetter(self.handlerattr) + self._dispatcher = Dispatcher(name) + + def __repr__(self): + return "" % self.name + + def register_handlerclass(self, classes, typ, on_ambiguity=ambiguity_register_error_ignore_dup): + """ + Register the handler class for two classes, in both straight and reversed order. + + Paramteters + =========== + + classes : tuple of two types + Classes who are compared with each other. + + typ: + Class which is registered to represent *cls1* and *cls2*. + Handler method of *self* must be implemented in this class. + """ + if not len(classes) == 2: + raise RuntimeError( + "Only binary dispatch is supported, but got %s types: <%s>." % ( + len(classes), str_signature(classes) + )) + if len(set(classes)) == 1: + raise RuntimeError( + "Duplicate types <%s> cannot be dispatched." % str_signature(classes) + ) + self._dispatcher.add(tuple(classes), typ, on_ambiguity=on_ambiguity) + self._dispatcher.add(tuple(reversed(classes)), typ, on_ambiguity=on_ambiguity) + + @cacheit + def __call__(self, *args, _sympify=True, **kwargs): + """ + Parameters + ========== + + *args : + Arguments which are operated + """ + if _sympify: + args = tuple(map(_sympify_, args)) + handlers = frozenset(map(self._handlergetter, args)) + + # no need to sympify again + return self.dispatch(handlers)(*args, _sympify=False, **kwargs) + + @cacheit + def dispatch(self, handlers): + """ + Select the handler class, and return its handler method. + """ + + # Quick exit for the case where all handlers are same + if len(handlers) == 1: + h, = handlers + if not isinstance(h, type): + raise RuntimeError("Handler {!r} is not a type.".format(h)) + return h + + # Recursively select with registered binary priority + for i, typ in enumerate(handlers): + + if not isinstance(typ, type): + raise RuntimeError("Handler {!r} is not a type.".format(typ)) + + if i == 0: + handler = typ + else: + prev_handler = handler + handler = self._dispatcher.dispatch(prev_handler, typ) + + if not isinstance(handler, type): + raise RuntimeError( + "Dispatcher for {!r} and {!r} must return a type, but got {!r}".format( + prev_handler, typ, handler + )) + + # return handler class + return handler + + @property + def __doc__(self): + docs = [ + "Multiply dispatched associative operator: %s" % self.name, + "Note that support for this is experimental, see the docs for :class:`AssocOpDispatcher` for details" + ] + + if self.doc: + docs.append(self.doc) + + s = "Registered handler classes\n" + s += '=' * len(s) + docs.append(s) + + amb_sigs = [] + + typ_sigs = defaultdict(list) + for sigs in self._dispatcher.ordering[::-1]: + key = self._dispatcher.funcs[sigs] + typ_sigs[key].append(sigs) + + for typ, sigs in typ_sigs.items(): + + sigs_str = ', '.join('<%s>' % str_signature(sig) for sig in sigs) + + if isinstance(typ, RaiseNotImplementedError): + amb_sigs.append(sigs_str) + continue + + s = 'Inputs: %s\n' % sigs_str + s += '-' * len(s) + '\n' + s += typ.__name__ + docs.append(s) + + if amb_sigs: + s = "Ambiguous handler classes\n" + s += '=' * len(s) + docs.append(s) + + s = '\n'.join(amb_sigs) + docs.append(s) + + return '\n\n'.join(docs) diff --git a/venv/lib/python3.11/site-packages/sympy/core/parameters.py b/venv/lib/python3.11/site-packages/sympy/core/parameters.py new file mode 100644 index 0000000000000000000000000000000000000000..d911a3652bf02fa5b24c43b138035a57be687228 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/parameters.py @@ -0,0 +1,161 @@ +"""Thread-safe global parameters""" + +from .cache import clear_cache +from contextlib import contextmanager +from threading import local + +class _global_parameters(local): + """ + Thread-local global parameters. + + Explanation + =========== + + This class generates thread-local container for SymPy's global parameters. + Every global parameters must be passed as keyword argument when generating + its instance. + A variable, `global_parameters` is provided as default instance for this class. + + WARNING! Although the global parameters are thread-local, SymPy's cache is not + by now. + This may lead to undesired result in multi-threading operations. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.core.cache import clear_cache + >>> from sympy.core.parameters import global_parameters as gp + + >>> gp.evaluate + True + >>> x+x + 2*x + + >>> log = [] + >>> def f(): + ... clear_cache() + ... gp.evaluate = False + ... log.append(x+x) + ... clear_cache() + >>> import threading + >>> thread = threading.Thread(target=f) + >>> thread.start() + >>> thread.join() + + >>> print(log) + [x + x] + + >>> gp.evaluate + True + >>> x+x + 2*x + + References + ========== + + .. [1] https://docs.python.org/3/library/threading.html + + """ + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + def __setattr__(self, name, value): + if getattr(self, name) != value: + clear_cache() + return super().__setattr__(name, value) + +global_parameters = _global_parameters(evaluate=True, distribute=True, exp_is_pow=False) + +class evaluate: + """ Control automatic evaluation + + Explanation + =========== + + This context manager controls whether or not all SymPy functions evaluate + by default. + + Note that much of SymPy expects evaluated expressions. This functionality + is experimental and is unlikely to function as intended on large + expressions. + + Examples + ======== + + >>> from sympy import evaluate + >>> from sympy.abc import x + >>> print(x + x) + 2*x + >>> with evaluate(False): + ... print(x + x) + x + x + """ + def __init__(self, x): + self.x = x + self.old = [] + + def __enter__(self): + self.old.append(global_parameters.evaluate) + global_parameters.evaluate = self.x + + def __exit__(self, exc_type, exc_val, exc_tb): + global_parameters.evaluate = self.old.pop() + +@contextmanager +def distribute(x): + """ Control automatic distribution of Number over Add + + Explanation + =========== + + This context manager controls whether or not Mul distribute Number over + Add. Plan is to avoid distributing Number over Add in all of sympy. Once + that is done, this contextmanager will be removed. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.core.parameters import distribute + >>> print(2*(x + 1)) + 2*x + 2 + >>> with distribute(False): + ... print(2*(x + 1)) + 2*(x + 1) + """ + + old = global_parameters.distribute + + try: + global_parameters.distribute = x + yield + finally: + global_parameters.distribute = old + + +@contextmanager +def _exp_is_pow(x): + """ + Control whether `e^x` should be represented as ``exp(x)`` or a ``Pow(E, x)``. + + Examples + ======== + + >>> from sympy import exp + >>> from sympy.abc import x + >>> from sympy.core.parameters import _exp_is_pow + >>> with _exp_is_pow(True): print(type(exp(x))) + + >>> with _exp_is_pow(False): print(type(exp(x))) + exp + """ + old = global_parameters.exp_is_pow + + clear_cache() + try: + global_parameters.exp_is_pow = x + yield + finally: + clear_cache() + global_parameters.exp_is_pow = old diff --git a/venv/lib/python3.11/site-packages/sympy/core/power.py b/venv/lib/python3.11/site-packages/sympy/core/power.py new file mode 100644 index 0000000000000000000000000000000000000000..0f257d030553ecc7b887ca9d1199ccc19b9a642f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/power.py @@ -0,0 +1,1847 @@ +from __future__ import annotations +from typing import Callable, TYPE_CHECKING +from itertools import product + +from .sympify import _sympify +from .cache import cacheit +from .singleton import S +from .expr import Expr +from .evalf import PrecisionExhausted +from .function import (expand_complex, expand_multinomial, + expand_mul, _mexpand, PoleError) +from .logic import fuzzy_bool, fuzzy_not, fuzzy_and, fuzzy_or +from .parameters import global_parameters +from .relational import is_gt, is_lt +from .kind import NumberKind, UndefinedKind +from sympy.utilities.iterables import sift +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.utilities.misc import as_int +from sympy.multipledispatch import Dispatcher + + +class Pow(Expr): + """ + Defines the expression x**y as "x raised to a power y" + + .. deprecated:: 1.7 + + Using arguments that aren't subclasses of :class:`~.Expr` in core + operators (:class:`~.Mul`, :class:`~.Add`, and :class:`~.Pow`) is + deprecated. See :ref:`non-expr-args-deprecated` for details. + + Singleton definitions involving (0, 1, -1, oo, -oo, I, -I): + + +--------------+---------+-----------------------------------------------+ + | expr | value | reason | + +==============+=========+===============================================+ + | z**0 | 1 | Although arguments over 0**0 exist, see [2]. | + +--------------+---------+-----------------------------------------------+ + | z**1 | z | | + +--------------+---------+-----------------------------------------------+ + | (-oo)**(-1) | 0 | | + +--------------+---------+-----------------------------------------------+ + | (-1)**-1 | -1 | | + +--------------+---------+-----------------------------------------------+ + | S.Zero**-1 | zoo | This is not strictly true, as 0**-1 may be | + | | | undefined, but is convenient in some contexts | + | | | where the base is assumed to be positive. | + +--------------+---------+-----------------------------------------------+ + | 1**-1 | 1 | | + +--------------+---------+-----------------------------------------------+ + | oo**-1 | 0 | | + +--------------+---------+-----------------------------------------------+ + | 0**oo | 0 | Because for all complex numbers z near | + | | | 0, z**oo -> 0. | + +--------------+---------+-----------------------------------------------+ + | 0**-oo | zoo | This is not strictly true, as 0**oo may be | + | | | oscillating between positive and negative | + | | | values or rotating in the complex plane. | + | | | It is convenient, however, when the base | + | | | is positive. | + +--------------+---------+-----------------------------------------------+ + | 1**oo | nan | Because there are various cases where | + | 1**-oo | | lim(x(t),t)=1, lim(y(t),t)=oo (or -oo), | + | | | but lim( x(t)**y(t), t) != 1. See [3]. | + +--------------+---------+-----------------------------------------------+ + | b**zoo | nan | Because b**z has no limit as z -> zoo | + +--------------+---------+-----------------------------------------------+ + | (-1)**oo | nan | Because of oscillations in the limit. | + | (-1)**(-oo) | | | + +--------------+---------+-----------------------------------------------+ + | oo**oo | oo | | + +--------------+---------+-----------------------------------------------+ + | oo**-oo | 0 | | + +--------------+---------+-----------------------------------------------+ + | (-oo)**oo | nan | | + | (-oo)**-oo | | | + +--------------+---------+-----------------------------------------------+ + | oo**I | nan | oo**e could probably be best thought of as | + | (-oo)**I | | the limit of x**e for real x as x tends to | + | | | oo. If e is I, then the limit does not exist | + | | | and nan is used to indicate that. | + +--------------+---------+-----------------------------------------------+ + | oo**(1+I) | zoo | If the real part of e is positive, then the | + | (-oo)**(1+I) | | limit of abs(x**e) is oo. So the limit value | + | | | is zoo. | + +--------------+---------+-----------------------------------------------+ + | oo**(-1+I) | 0 | If the real part of e is negative, then the | + | -oo**(-1+I) | | limit is 0. | + +--------------+---------+-----------------------------------------------+ + + Because symbolic computations are more flexible than floating point + calculations and we prefer to never return an incorrect answer, + we choose not to conform to all IEEE 754 conventions. This helps + us avoid extra test-case code in the calculation of limits. + + See Also + ======== + + sympy.core.numbers.Infinity + sympy.core.numbers.NegativeInfinity + sympy.core.numbers.NaN + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Exponentiation + .. [2] https://en.wikipedia.org/wiki/Zero_to_the_power_of_zero + .. [3] https://en.wikipedia.org/wiki/Indeterminate_forms + + """ + is_Pow = True + + __slots__ = ('is_commutative',) + + if TYPE_CHECKING: + + @property + def args(self) -> tuple[Expr, Expr]: + ... + + @property + def base(self) -> Expr: + return self.args[0] + + @property + def exp(self) -> Expr: + return self.args[1] + + @property + def kind(self): + if self.exp.kind is NumberKind: + return self.base.kind + else: + return UndefinedKind + + @cacheit + def __new__(cls, b: Expr | complex, e: Expr | complex, evaluate=None) -> Expr: # type: ignore + if evaluate is None: + evaluate = global_parameters.evaluate + + base = _sympify(b) + exp = _sympify(e) + + # XXX: This can be removed when non-Expr args are disallowed rather + # than deprecated. + from .relational import Relational + if isinstance(base, Relational) or isinstance(exp, Relational): + raise TypeError('Relational cannot be used in Pow') + + # XXX: This should raise TypeError once deprecation period is over: + for arg in [base, exp]: + if not isinstance(arg, Expr): + sympy_deprecation_warning( + f""" + Using non-Expr arguments in Pow is deprecated (in this case, one of the + arguments is of type {type(arg).__name__!r}). + + If you really did intend to construct a power with this base, use the ** + operator instead.""", + deprecated_since_version="1.7", + active_deprecations_target="non-expr-args-deprecated", + stacklevel=4, + ) + + if evaluate: + if exp is S.ComplexInfinity: + return S.NaN + if exp is S.Infinity: + if is_gt(base, S.One): + return S.Infinity + if is_gt(base, S.NegativeOne) and is_lt(base, S.One): + return S.Zero + if is_lt(base, S.NegativeOne): + if base.is_finite: + return S.ComplexInfinity + if base.is_finite is False: + return S.NaN + if exp is S.Zero: + return S.One + elif exp is S.One: + return base + elif exp == -1 and not base: + return S.ComplexInfinity + elif exp.__class__.__name__ == "AccumulationBounds": + if base == S.Exp1: + from sympy.calculus.accumulationbounds import AccumBounds + return AccumBounds(Pow(base, exp.min), Pow(base, exp.max)) + # autosimplification if base is a number and exp odd/even + # if base is Number then the base will end up positive; we + # do not do this with arbitrary expressions since symbolic + # cancellation might occur as in (x - 1)/(1 - x) -> -1. If + # we returned Piecewise((-1, Ne(x, 1))) for such cases then + # we could do this...but we don't + elif (exp.is_Symbol and exp.is_integer or exp.is_Integer + ) and (base.is_number and base.is_Mul or base.is_Number + ) and base.could_extract_minus_sign(): + if exp.is_even: + base = -base + elif exp.is_odd: + return -Pow(-base, exp) + if S.NaN in (base, exp): # XXX S.NaN**x -> S.NaN under assumption that x != 0 + return S.NaN + elif base is S.One: + if abs(exp).is_infinite: + return S.NaN + return S.One + else: + # recognize base as E + from sympy.functions.elementary.exponential import exp_polar + if not exp.is_Atom and base is not S.Exp1 and not isinstance(base, exp_polar): + from .exprtools import factor_terms + from sympy.functions.elementary.exponential import log + from sympy.simplify.radsimp import fraction + c, ex = factor_terms(exp, sign=False).as_coeff_Mul() + num, den = fraction(ex) + if isinstance(den, log) and den.args[0] == base: + return S.Exp1**(c*num) + elif den.is_Add: + from sympy.functions.elementary.complexes import sign, im + s = sign(im(base)) + if s.is_Number and s and den == \ + log(-factor_terms(base, sign=False)) + s*S.ImaginaryUnit*S.Pi: + return S.Exp1**(c*num) + + obj = base._eval_power(exp) + if obj is not None: + return obj + obj = Expr.__new__(cls, base, exp) + obj = cls._exec_constructor_postprocessors(obj) + if not isinstance(obj, Pow): + return obj + obj.is_commutative = (base.is_commutative and exp.is_commutative) + return obj + + def inverse(self, argindex=1): + if self.base == S.Exp1: + from sympy.functions.elementary.exponential import log + return log + return None + + @classmethod + def class_key(cls): + return 3, 2, cls.__name__ + + def _eval_refine(self, assumptions): + from sympy.assumptions.ask import ask, Q + b, e = self.as_base_exp() + if ask(Q.integer(e), assumptions) and b.could_extract_minus_sign(): + if ask(Q.even(e), assumptions): + return Pow(-b, e) + elif ask(Q.odd(e), assumptions): + return -Pow(-b, e) + + def _eval_power(self, expt): + b, e = self.as_base_exp() + if b is S.NaN: + return (b**e)**expt # let __new__ handle it + + s = None + if expt.is_integer: + s = 1 + elif b.is_polar: # e.g. exp_polar, besselj, var('p', polar=True)... + s = 1 + elif e.is_extended_real is not None: + from sympy.functions.elementary.complexes import arg, im, re, sign + from sympy.functions.elementary.exponential import exp, log + from sympy.functions.elementary.integers import floor + # helper functions =========================== + def _half(e): + """Return True if the exponent has a literal 2 as the + denominator, else None.""" + if getattr(e, 'q', None) == 2: + return True + n, d = e.as_numer_denom() + if n.is_integer and d == 2: + return True + def _n2(e): + """Return ``e`` evaluated to a Number with 2 significant + digits, else None.""" + try: + rv = e.evalf(2, strict=True) + if rv.is_Number: + return rv + except PrecisionExhausted: + pass + # =================================================== + if e.is_extended_real: + # we need _half(expt) with constant floor or + # floor(S.Half - e*arg(b)/2/pi) == 0 + + + # handle -1 as special case + if e == -1: + # floor arg. is 1/2 + arg(b)/2/pi + if _half(expt): + if b.is_negative is True: + return S.NegativeOne**expt*Pow(-b, e*expt) + elif b.is_negative is False: # XXX ok if im(b) != 0? + return Pow(b, -expt) + elif e.is_even: + if b.is_extended_real: + b = abs(b) + if b.is_imaginary: + b = abs(im(b))*S.ImaginaryUnit + + if (abs(e) < 1) == True or e == 1: + s = 1 # floor = 0 + elif b.is_extended_nonnegative: + s = 1 # floor = 0 + elif re(b).is_extended_nonnegative and (abs(e) < 2) == True: + s = 1 # floor = 0 + elif _half(expt): + s = exp(2*S.Pi*S.ImaginaryUnit*expt*floor( + S.Half - e*arg(b)/(2*S.Pi))) + if s.is_extended_real and _n2(sign(s) - s) == 0: + s = sign(s) + else: + s = None + else: + # e.is_extended_real is False requires: + # _half(expt) with constant floor or + # floor(S.Half - im(e*log(b))/2/pi) == 0 + try: + s = exp(2*S.ImaginaryUnit*S.Pi*expt* + floor(S.Half - im(e*log(b))/2/S.Pi)) + # be careful to test that s is -1 or 1 b/c sign(I) == I: + # so check that s is real + if s.is_extended_real and _n2(sign(s) - s) == 0: + s = sign(s) + else: + s = None + except PrecisionExhausted: + s = None + + if s is not None: + return s*Pow(b, e*expt) + + def _eval_Mod(self, q): + r"""A dispatched function to compute `b^e \bmod q`, dispatched + by ``Mod``. + + Notes + ===== + + Algorithms: + + 1. For unevaluated integer power, use built-in ``pow`` function + with 3 arguments, if powers are not too large wrt base. + + 2. For very large powers, use totient reduction if $e \ge \log(m)$. + Bound on m, is for safe factorization memory wise i.e. $m^{1/4}$. + For pollard-rho to be faster than built-in pow $\log(e) > m^{1/4}$ + check is added. + + 3. For any unevaluated power found in `b` or `e`, the step 2 + will be recursed down to the base and the exponent + such that the $b \bmod q$ becomes the new base and + $\phi(q) + e \bmod \phi(q)$ becomes the new exponent, and then + the computation for the reduced expression can be done. + """ + + base, exp = self.base, self.exp + + if exp.is_integer and exp.is_positive: + if q.is_integer and base % q == 0: + return S.Zero + + from sympy.functions.combinatorial.numbers import totient + + if base.is_Integer and exp.is_Integer and q.is_Integer: + b, e, m = int(base), int(exp), int(q) + mb = m.bit_length() + if mb <= 80 and e >= mb and e.bit_length()**4 >= m: + phi = int(totient(m)) + return Integer(pow(b, phi + e%phi, m)) + return Integer(pow(b, e, m)) + + from .mod import Mod + + if isinstance(base, Pow) and base.is_integer and base.is_number: + base = Mod(base, q) + return Mod(Pow(base, exp, evaluate=False), q) + + if isinstance(exp, Pow) and exp.is_integer and exp.is_number: + bit_length = int(q).bit_length() + # XXX Mod-Pow actually attempts to do a hanging evaluation + # if this dispatched function returns None. + # May need some fixes in the dispatcher itself. + if bit_length <= 80: + phi = totient(q) + exp = phi + Mod(exp, phi) + return Mod(Pow(base, exp, evaluate=False), q) + + def _eval_is_even(self): + if self.exp.is_integer and self.exp.is_positive: + return self.base.is_even + + def _eval_is_negative(self): + ext_neg = Pow._eval_is_extended_negative(self) + if ext_neg is True: + return self.is_finite + return ext_neg + + def _eval_is_extended_positive(self): + if self.base == self.exp: + if self.base.is_extended_nonnegative: + return True + elif self.base.is_positive: + if self.exp.is_real: + return True + elif self.base.is_extended_negative: + if self.exp.is_even: + return True + if self.exp.is_odd: + return False + elif self.base.is_zero: + if self.exp.is_extended_real: + return self.exp.is_zero + elif self.base.is_extended_nonpositive: + if self.exp.is_odd: + return False + elif self.base.is_imaginary: + if self.exp.is_integer: + m = self.exp % 4 + if m.is_zero: + return True + if m.is_integer and m.is_zero is False: + return False + if self.exp.is_imaginary: + from sympy.functions.elementary.exponential import log + return log(self.base).is_imaginary + + def _eval_is_extended_negative(self): + if self.exp is S.Half: + if self.base.is_complex or self.base.is_extended_real: + return False + if self.base.is_extended_negative: + if self.exp.is_odd and self.base.is_finite: + return True + if self.exp.is_even: + return False + elif self.base.is_extended_positive: + if self.exp.is_extended_real: + return False + elif self.base.is_zero: + if self.exp.is_extended_real: + return False + elif self.base.is_extended_nonnegative: + if self.exp.is_extended_nonnegative: + return False + elif self.base.is_extended_nonpositive: + if self.exp.is_even: + return False + elif self.base.is_extended_real: + if self.exp.is_even: + return False + + def _eval_is_zero(self): + if self.base.is_zero: + if self.exp.is_extended_positive: + return True + elif self.exp.is_extended_nonpositive: + return False + elif self.base == S.Exp1: + return self.exp is S.NegativeInfinity + elif self.base.is_zero is False: + if self.base.is_finite and self.exp.is_finite: + return False + elif self.exp.is_negative: + return self.base.is_infinite + elif self.exp.is_nonnegative: + return False + elif self.exp.is_infinite and self.exp.is_extended_real: + if (1 - abs(self.base)).is_extended_positive: + return self.exp.is_extended_positive + elif (1 - abs(self.base)).is_extended_negative: + return self.exp.is_extended_negative + elif self.base.is_finite and self.exp.is_negative: + # when self.base.is_zero is None + return False + + def _eval_is_integer(self): + b, e = self.args + if b.is_rational: + if b.is_integer is False and e.is_positive: + return False # rat**nonneg + if b.is_integer and e.is_integer: + if b is S.NegativeOne: + return True + if e.is_nonnegative or e.is_positive: + return True + if b.is_integer and e.is_negative and (e.is_finite or e.is_integer): + if fuzzy_not((b - 1).is_zero) and fuzzy_not((b + 1).is_zero): + return False + if b.is_Number and e.is_Number: + check = self.func(*self.args) + return check.is_Integer + if e.is_negative and b.is_positive and (b - 1).is_positive: + return False + if e.is_negative and b.is_negative and (b + 1).is_negative: + return False + + def _eval_is_extended_real(self): + if self.base is S.Exp1: + if self.exp.is_extended_real: + return True + elif self.exp.is_imaginary: + return (2*S.ImaginaryUnit*self.exp/S.Pi).is_even + + from sympy.functions.elementary.exponential import log, exp + real_b = self.base.is_extended_real + if real_b is None: + if self.base.func == exp and self.base.exp.is_imaginary: + return self.exp.is_imaginary + if self.base.func == Pow and self.base.base is S.Exp1 and self.base.exp.is_imaginary: + return self.exp.is_imaginary + return + real_e = self.exp.is_extended_real + if real_e is None: + return + if real_b and real_e: + if self.base.is_extended_positive: + return True + elif self.base.is_extended_nonnegative and self.exp.is_extended_nonnegative: + return True + elif self.exp.is_integer and self.base.is_extended_nonzero: + return True + elif self.exp.is_integer and self.exp.is_nonnegative: + return True + elif self.base.is_extended_negative: + if self.exp.is_Rational: + return False + if real_e and self.exp.is_extended_negative and self.base.is_zero is False: + return Pow(self.base, -self.exp).is_extended_real + im_b = self.base.is_imaginary + im_e = self.exp.is_imaginary + if im_b: + if self.exp.is_integer: + if self.exp.is_even: + return True + elif self.exp.is_odd: + return False + elif im_e and log(self.base).is_imaginary: + return True + elif self.exp.is_Add: + c, a = self.exp.as_coeff_Add() + if c and c.is_Integer: + return Mul( + self.base**c, self.base**a, evaluate=False).is_extended_real + elif self.base in (-S.ImaginaryUnit, S.ImaginaryUnit): + if (self.exp/2).is_integer is False: + return False + if real_b and im_e: + if self.base is S.NegativeOne: + return True + c = self.exp.coeff(S.ImaginaryUnit) + if c: + if self.base.is_rational and c.is_rational: + if self.base.is_nonzero and (self.base - 1).is_nonzero and c.is_nonzero: + return False + ok = (c*log(self.base)/S.Pi).is_integer + if ok is not None: + return ok + + if real_b is False and real_e: # we already know it's not imag + if isinstance(self.exp, Rational) and self.exp.p == 1: + return False + from sympy.functions.elementary.complexes import arg + i = arg(self.base)*self.exp/S.Pi + if i.is_complex: # finite + return i.is_integer + + def _eval_is_complex(self): + + if self.base == S.Exp1: + return fuzzy_or([self.exp.is_complex, self.exp.is_extended_negative]) + + if all(a.is_complex for a in self.args) and self._eval_is_finite(): + return True + + def _eval_is_imaginary(self): + if self.base.is_commutative is False: + return False + + if self.base.is_imaginary: + if self.exp.is_integer: + odd = self.exp.is_odd + if odd is not None: + return odd + return + + if self.base == S.Exp1: + f = 2 * self.exp / (S.Pi*S.ImaginaryUnit) + # exp(pi*integer) = 1 or -1, so not imaginary + if f.is_even: + return False + # exp(pi*integer + pi/2) = I or -I, so it is imaginary + if f.is_odd: + return True + return None + + if self.exp.is_imaginary: + from sympy.functions.elementary.exponential import log + imlog = log(self.base).is_imaginary + if imlog is not None: + return False # I**i -> real; (2*I)**i -> complex ==> not imaginary + + if self.base.is_extended_real and self.exp.is_extended_real: + if self.base.is_positive: + return False + else: + rat = self.exp.is_rational + if not rat: + return rat + if self.exp.is_integer: + return False + else: + half = (2*self.exp).is_integer + if half: + return self.base.is_negative + return half + + if self.base.is_extended_real is False: # we already know it's not imag + from sympy.functions.elementary.complexes import arg + i = arg(self.base)*self.exp/S.Pi + isodd = (2*i).is_odd + if isodd is not None: + return isodd + + def _eval_is_odd(self): + if self.exp.is_integer: + if self.exp.is_positive: + return self.base.is_odd + elif self.exp.is_nonnegative and self.base.is_odd: + return True + elif self.base is S.NegativeOne: + return True + + def _eval_is_finite(self): + if self.exp.is_negative: + if self.base.is_zero: + return False + if self.base.is_infinite or self.base.is_nonzero: + return True + c1 = self.base.is_finite + if c1 is None: + return + c2 = self.exp.is_finite + if c2 is None: + return + if c1 and c2: + if self.exp.is_nonnegative or fuzzy_not(self.base.is_zero): + return True + + def _eval_is_prime(self): + ''' + An integer raised to the n(>=2)-th power cannot be a prime. + ''' + if self.base.is_integer and self.exp.is_integer and (self.exp - 1).is_positive: + return False + + def _eval_is_composite(self): + """ + A power is composite if both base and exponent are greater than 1 + """ + if (self.base.is_integer and self.exp.is_integer and + ((self.base - 1).is_positive and (self.exp - 1).is_positive or + (self.base + 1).is_negative and self.exp.is_positive and self.exp.is_even)): + return True + + def _eval_is_polar(self): + return self.base.is_polar + + def _eval_subs(self, old, new): + from sympy.calculus.accumulationbounds import AccumBounds + + if isinstance(self.exp, AccumBounds): + b = self.base.subs(old, new) + e = self.exp.subs(old, new) + if isinstance(e, AccumBounds): + return e.__rpow__(b) + return self.func(b, e) + + from sympy.functions.elementary.exponential import exp, log + + def _check(ct1, ct2, old): + """Return (bool, pow, remainder_pow) where, if bool is True, then the + exponent of Pow `old` will combine with `pow` so the substitution + is valid, otherwise bool will be False. + + For noncommutative objects, `pow` will be an integer, and a factor + `Pow(old.base, remainder_pow)` needs to be included. If there is + no such factor, None is returned. For commutative objects, + remainder_pow is always None. + + cti are the coefficient and terms of an exponent of self or old + In this _eval_subs routine a change like (b**(2*x)).subs(b**x, y) + will give y**2 since (b**x)**2 == b**(2*x); if that equality does + not hold then the substitution should not occur so `bool` will be + False. + + """ + coeff1, terms1 = ct1 + coeff2, terms2 = ct2 + if terms1 == terms2: + if old.is_commutative: + # Allow fractional powers for commutative objects + pow = coeff1/coeff2 + try: + as_int(pow, strict=False) + combines = True + except ValueError: + b, e = old.as_base_exp() + # These conditions ensure that (b**e)**f == b**(e*f) for any f + combines = b.is_positive and e.is_real or b.is_nonnegative and e.is_nonnegative + + return combines, pow, None + else: + # With noncommutative symbols, substitute only integer powers + if not isinstance(terms1, tuple): + terms1 = (terms1,) + if not all(term.is_integer for term in terms1): + return False, None, None + + try: + # Round pow toward zero + pow, remainder = divmod(as_int(coeff1), as_int(coeff2)) + if pow < 0 and remainder != 0: + pow += 1 + remainder -= as_int(coeff2) + + if remainder == 0: + remainder_pow = None + else: + remainder_pow = Mul(remainder, *terms1) + + return True, pow, remainder_pow + except ValueError: + # Can't substitute + pass + + return False, None, None + + if old == self.base or (old == exp and self.base == S.Exp1): + if new.is_Function and isinstance(new, Callable): + return new(self.exp._subs(old, new)) + else: + return new**self.exp._subs(old, new) + + # issue 10829: (4**x - 3*y + 2).subs(2**x, y) -> y**2 - 3*y + 2 + if isinstance(old, self.func) and self.exp == old.exp: + l = log(self.base, old.base) + if l.is_Number: + return Pow(new, l) + + if isinstance(old, self.func) and self.base == old.base: + if self.exp.is_Add is False: + ct1 = self.exp.as_independent(Symbol, as_Add=False) + ct2 = old.exp.as_independent(Symbol, as_Add=False) + ok, pow, remainder_pow = _check(ct1, ct2, old) + if ok: + # issue 5180: (x**(6*y)).subs(x**(3*y),z)->z**2 + result = self.func(new, pow) + if remainder_pow is not None: + result = Mul(result, Pow(old.base, remainder_pow)) + return result + else: # b**(6*x + a).subs(b**(3*x), y) -> y**2 * b**a + # exp(exp(x) + exp(x**2)).subs(exp(exp(x)), w) -> w * exp(exp(x**2)) + oarg = old.exp + new_l = [] + o_al = [] + ct2 = oarg.as_coeff_mul() + for a in self.exp.args: + newa = a._subs(old, new) + ct1 = newa.as_coeff_mul() + ok, pow, remainder_pow = _check(ct1, ct2, old) + if ok: + new_l.append(new**pow) + if remainder_pow is not None: + o_al.append(remainder_pow) + continue + elif not old.is_commutative and not newa.is_integer: + # If any term in the exponent is non-integer, + # we do not do any substitutions in the noncommutative case + return + o_al.append(newa) + if new_l: + expo = Add(*o_al) + new_l.append(Pow(self.base, expo, evaluate=False) if expo != 1 else self.base) + return Mul(*new_l) + + if (isinstance(old, exp) or (old.is_Pow and old.base is S.Exp1)) and self.exp.is_extended_real and self.base.is_positive: + ct1 = old.exp.as_independent(Symbol, as_Add=False) + ct2 = (self.exp*log(self.base)).as_independent( + Symbol, as_Add=False) + ok, pow, remainder_pow = _check(ct1, ct2, old) + if ok: + result = self.func(new, pow) # (2**x).subs(exp(x*log(2)), z) -> z + if remainder_pow is not None: + result = Mul(result, Pow(old.base, remainder_pow)) + return result + + def as_base_exp(self): + """Return base and exp of self. + + Explanation + =========== + + If base a Rational less than 1, then return 1/Rational, -exp. + If this extra processing is not needed, the base and exp + properties will give the raw arguments. + + Examples + ======== + + >>> from sympy import Pow, S + >>> p = Pow(S.Half, 2, evaluate=False) + >>> p.as_base_exp() + (2, -2) + >>> p.args + (1/2, 2) + >>> p.base, p.exp + (1/2, 2) + + """ + b, e = self.args + if b.is_Rational and b.p == 1 and b.q != 1: + return Integer(b.q), -e + return b, e + + def _eval_adjoint(self): + from sympy.functions.elementary.complexes import adjoint + i, p = self.exp.is_integer, self.base.is_positive + if i: + return adjoint(self.base)**self.exp + if p: + return self.base**adjoint(self.exp) + if i is False and p is False: + expanded = expand_complex(self) + if expanded != self: + return adjoint(expanded) + + def _eval_conjugate(self): + from sympy.functions.elementary.complexes import conjugate as c + i, p = self.exp.is_integer, self.base.is_positive + if i: + return c(self.base)**self.exp + if p: + return self.base**c(self.exp) + if i is False and p is False: + expanded = expand_complex(self) + if expanded != self: + return c(expanded) + if self.is_extended_real: + return self + + def _eval_transpose(self): + from sympy.functions.elementary.complexes import transpose + if self.base == S.Exp1: + return self.func(S.Exp1, self.exp.transpose()) + i, p = self.exp.is_integer, (self.base.is_complex or self.base.is_infinite) + if p: + return self.base**self.exp + if i: + return transpose(self.base)**self.exp + if i is False and p is False: + expanded = expand_complex(self) + if expanded != self: + return transpose(expanded) + + def _eval_expand_power_exp(self, **hints): + """a**(n + m) -> a**n*a**m""" + b = self.base + e = self.exp + if b == S.Exp1: + from sympy.concrete.summations import Sum + if isinstance(e, Sum) and e.is_commutative: + from sympy.concrete.products import Product + return Product(self.func(b, e.function), *e.limits) + if e.is_Add and (hints.get('force', False) or + b.is_zero is False or e._all_nonneg_or_nonppos()): + if e.is_commutative: + return Mul(*[self.func(b, x) for x in e.args]) + if b.is_commutative: + c, nc = sift(e.args, lambda x: x.is_commutative, binary=True) + if c: + return Mul(*[self.func(b, x) for x in c] + )*b**Add._from_args(nc) + return self + + def _eval_expand_power_base(self, **hints): + """(a*b)**n -> a**n * b**n""" + force = hints.get('force', False) + + b = self.base + e = self.exp + if not b.is_Mul: + return self + + cargs, nc = b.args_cnc(split_1=False) + + # expand each term - this is top-level-only + # expansion but we have to watch out for things + # that don't have an _eval_expand method + if nc: + nc = [i._eval_expand_power_base(**hints) + if hasattr(i, '_eval_expand_power_base') else i + for i in nc] + + if e.is_Integer: + if e.is_positive: + rv = Mul(*nc*e) + else: + rv = Mul(*[i**-1 for i in nc[::-1]]*-e) + if cargs: + rv *= Mul(*cargs)**e + return rv + + if not cargs: + return self.func(Mul(*nc), e, evaluate=False) + + nc = [Mul(*nc)] + + # sift the commutative bases + other, maybe_real = sift(cargs, lambda x: x.is_extended_real is False, + binary=True) + def pred(x): + if x is S.ImaginaryUnit: + return S.ImaginaryUnit + polar = x.is_polar + if polar: + return True + if polar is None: + return fuzzy_bool(x.is_extended_nonnegative) + sifted = sift(maybe_real, pred) + nonneg = sifted[True] + other += sifted[None] + neg = sifted[False] + imag = sifted[S.ImaginaryUnit] + if imag: + I = S.ImaginaryUnit + i = len(imag) % 4 + if i == 0: + pass + elif i == 1: + other.append(I) + elif i == 2: + if neg: + nonn = -neg.pop() + if nonn is not S.One: + nonneg.append(nonn) + else: + neg.append(S.NegativeOne) + else: + if neg: + nonn = -neg.pop() + if nonn is not S.One: + nonneg.append(nonn) + else: + neg.append(S.NegativeOne) + other.append(I) + del imag + + # bring out the bases that can be separated from the base + + if force or e.is_integer: + # treat all commutatives the same and put nc in other + cargs = nonneg + neg + other + other = nc + else: + # this is just like what is happening automatically, except + # that now we are doing it for an arbitrary exponent for which + # no automatic expansion is done + + assert not e.is_Integer + + # handle negatives by making them all positive and putting + # the residual -1 in other + if len(neg) > 1: + o = S.One + if not other and neg[0].is_Number: + o *= neg.pop(0) + if len(neg) % 2: + o = -o + for n in neg: + nonneg.append(-n) + if o is not S.One: + other.append(o) + elif neg and other: + if neg[0].is_Number and neg[0] is not S.NegativeOne: + other.append(S.NegativeOne) + nonneg.append(-neg[0]) + else: + other.extend(neg) + else: + other.extend(neg) + del neg + + cargs = nonneg + other += nc + + rv = S.One + if cargs: + if e.is_Rational: + npow, cargs = sift(cargs, lambda x: x.is_Pow and + x.exp.is_Rational and x.base.is_number, + binary=True) + rv = Mul(*[self.func(b.func(*b.args), e) for b in npow]) + rv *= Mul(*[self.func(b, e, evaluate=False) for b in cargs]) + if other: + rv *= self.func(Mul(*other), e, evaluate=False) + return rv + + def _eval_expand_multinomial(self, **hints): + """(a + b + ..)**n -> a**n + n*a**(n-1)*b + .., n is nonzero integer""" + + base, exp = self.args + result = self + + if exp.is_Rational and exp.p > 0 and base.is_Add: + if not exp.is_Integer: + n = Integer(exp.p // exp.q) + + if not n: + return result + else: + radical, result = self.func(base, exp - n), [] + + expanded_base_n = self.func(base, n) + if expanded_base_n.is_Pow: + expanded_base_n = \ + expanded_base_n._eval_expand_multinomial() + for term in Add.make_args(expanded_base_n): + result.append(term*radical) + + return Add(*result) + + n = int(exp) + + if base.is_commutative: + order_terms, other_terms = [], [] + + for b in base.args: + if b.is_Order: + order_terms.append(b) + else: + other_terms.append(b) + + if order_terms: + # (f(x) + O(x^n))^m -> f(x)^m + m*f(x)^{m-1} *O(x^n) + f = Add(*other_terms) + o = Add(*order_terms) + + if n == 2: + return expand_multinomial(f**n, deep=False) + n*f*o + else: + g = expand_multinomial(f**(n - 1), deep=False) + return expand_mul(f*g, deep=False) + n*g*o + + if base.is_number: + # Efficiently expand expressions of the form (a + b*I)**n + # where 'a' and 'b' are real numbers and 'n' is integer. + a, b = base.as_real_imag() + + if a.is_Rational and b.is_Rational: + if not a.is_Integer: + if not b.is_Integer: + k = self.func(a.q * b.q, n) + a, b = a.p*b.q, a.q*b.p + else: + k = self.func(a.q, n) + a, b = a.p, a.q*b + elif not b.is_Integer: + k = self.func(b.q, n) + a, b = a*b.q, b.p + else: + k = 1 + + a, b, c, d = int(a), int(b), 1, 0 + + while n: + if n & 1: + c, d = a*c - b*d, b*c + a*d + n -= 1 + a, b = a*a - b*b, 2*a*b + n //= 2 + + I = S.ImaginaryUnit + + if k == 1: + return c + I*d + else: + return Integer(c)/k + I*d/k + + p = other_terms + # (x + y)**3 -> x**3 + 3*x**2*y + 3*x*y**2 + y**3 + # in this particular example: + # p = [x,y]; n = 3 + # so now it's easy to get the correct result -- we get the + # coefficients first: + from sympy.ntheory.multinomial import multinomial_coefficients + from sympy.polys.polyutils import basic_from_dict + expansion_dict = multinomial_coefficients(len(p), n) + # in our example: {(3, 0): 1, (1, 2): 3, (0, 3): 1, (2, 1): 3} + # and now construct the expression. + return basic_from_dict(expansion_dict, *p) + else: + if n == 2: + return Add(*[f*g for f in base.args for g in base.args]) + else: + multi = (base**(n - 1))._eval_expand_multinomial() + if multi.is_Add: + return Add(*[f*g for f in base.args + for g in multi.args]) + else: + # XXX can this ever happen if base was an Add? + return Add(*[f*multi for f in base.args]) + elif (exp.is_Rational and exp.p < 0 and base.is_Add and + abs(exp.p) > exp.q): + return 1 / self.func(base, -exp)._eval_expand_multinomial() + elif exp.is_Add and base.is_Number and (hints.get('force', False) or + base.is_zero is False or exp._all_nonneg_or_nonppos()): + # a + b a b + # n --> n n, where n, a, b are Numbers + # XXX should be in expand_power_exp? + coeff, tail = [], [] + for term in exp.args: + if term.is_Number: + coeff.append(self.func(base, term)) + else: + tail.append(term) + return Mul(*(coeff + [self.func(base, Add._from_args(tail))])) + else: + return result + + def as_real_imag(self, deep=True, **hints): + if self.exp.is_Integer: + from sympy.polys.polytools import poly + + exp = self.exp + re_e, im_e = self.base.as_real_imag(deep=deep) + if not im_e: + return self, S.Zero + a, b = symbols('a b', cls=Dummy) + if exp >= 0: + if re_e.is_Number and im_e.is_Number: + # We can be more efficient in this case + expr = expand_multinomial(self.base**exp) + if expr != self: + return expr.as_real_imag() + + expr = poly( + (a + b)**exp) # a = re, b = im; expr = (a + b*I)**exp + else: + mag = re_e**2 + im_e**2 + re_e, im_e = re_e/mag, -im_e/mag + if re_e.is_Number and im_e.is_Number: + # We can be more efficient in this case + expr = expand_multinomial((re_e + im_e*S.ImaginaryUnit)**-exp) + if expr != self: + return expr.as_real_imag() + + expr = poly((a + b)**-exp) + + # Terms with even b powers will be real + r = [i for i in expr.terms() if not i[0][1] % 2] + re_part = Add(*[cc*a**aa*b**bb for (aa, bb), cc in r]) + # Terms with odd b powers will be imaginary + r = [i for i in expr.terms() if i[0][1] % 4 == 1] + im_part1 = Add(*[cc*a**aa*b**bb for (aa, bb), cc in r]) + r = [i for i in expr.terms() if i[0][1] % 4 == 3] + im_part3 = Add(*[cc*a**aa*b**bb for (aa, bb), cc in r]) + + return (re_part.subs({a: re_e, b: S.ImaginaryUnit*im_e}), + im_part1.subs({a: re_e, b: im_e}) + im_part3.subs({a: re_e, b: -im_e})) + + from sympy.functions.elementary.trigonometric import atan2, cos, sin + + if self.exp.is_Rational: + re_e, im_e = self.base.as_real_imag(deep=deep) + + if im_e.is_zero and self.exp is S.Half: + if re_e.is_extended_nonnegative: + return self, S.Zero + if re_e.is_extended_nonpositive: + return S.Zero, (-self.base)**self.exp + + # XXX: This is not totally correct since for x**(p/q) with + # x being imaginary there are actually q roots, but + # only a single one is returned from here. + r = self.func(self.func(re_e, 2) + self.func(im_e, 2), S.Half) + + t = atan2(im_e, re_e) + + rp, tp = self.func(r, self.exp), t*self.exp + + return rp*cos(tp), rp*sin(tp) + elif self.base is S.Exp1: + from sympy.functions.elementary.exponential import exp + re_e, im_e = self.exp.as_real_imag() + if deep: + re_e = re_e.expand(deep, **hints) + im_e = im_e.expand(deep, **hints) + c, s = cos(im_e), sin(im_e) + return exp(re_e)*c, exp(re_e)*s + else: + from sympy.functions.elementary.complexes import im, re + if deep: + hints['complex'] = False + + expanded = self.expand(deep, **hints) + if hints.get('ignore') == expanded: + return None + else: + return (re(expanded), im(expanded)) + else: + return re(self), im(self) + + def _eval_derivative(self, s): + from sympy.functions.elementary.exponential import log + dbase = self.base.diff(s) + dexp = self.exp.diff(s) + return self * (dexp * log(self.base) + dbase * self.exp/self.base) + + def _eval_evalf(self, prec): + base, exp = self.as_base_exp() + if base == S.Exp1: + # Use mpmath function associated to class "exp": + from sympy.functions.elementary.exponential import exp as exp_function + return exp_function(self.exp, evaluate=False)._eval_evalf(prec) + base = base._evalf(prec) + if not exp.is_Integer: + exp = exp._evalf(prec) + if exp.is_negative and base.is_number and base.is_extended_real is False: + base = base.conjugate() / (base * base.conjugate())._evalf(prec) + exp = -exp + return self.func(base, exp).expand() + return self.func(base, exp) + + def _eval_is_polynomial(self, syms): + if self.exp.has(*syms): + return False + + if self.base.has(*syms): + return bool(self.base._eval_is_polynomial(syms) and + self.exp.is_Integer and (self.exp >= 0)) + else: + return True + + def _eval_is_rational(self): + # The evaluation of self.func below can be very expensive in the case + # of integer**integer if the exponent is large. We should try to exit + # before that if possible: + if (self.exp.is_integer and self.base.is_rational + and fuzzy_not(fuzzy_and([self.exp.is_negative, self.base.is_zero]))): + return True + p = self.func(*self.as_base_exp()) # in case it's unevaluated + if not p.is_Pow: + return p.is_rational + b, e = p.as_base_exp() + if e.is_Rational and b.is_Rational: + # we didn't check that e is not an Integer + # because Rational**Integer autosimplifies + return False + if e.is_integer: + if b.is_rational: + if fuzzy_not(b.is_zero) or e.is_nonnegative: + return True + if b == e: # always rational, even for 0**0 + return True + elif b.is_irrational: + return e.is_zero + if b is S.Exp1: + if e.is_rational and e.is_nonzero: + return False + + def _eval_is_algebraic(self): + def _is_one(expr): + try: + return (expr - 1).is_zero + except ValueError: + # when the operation is not allowed + return False + + if self.base.is_zero or _is_one(self.base): + return True + elif self.base is S.Exp1: + s = self.func(*self.args) + if s.func == self.func: + if self.exp.is_nonzero: + if self.exp.is_algebraic: + return False + elif (self.exp/S.Pi).is_rational: + return False + elif (self.exp/(S.ImaginaryUnit*S.Pi)).is_rational: + return True + else: + return s.is_algebraic + elif self.exp.is_rational: + if self.base.is_algebraic is False: + return self.exp.is_zero + if self.base.is_zero is False: + if self.exp.is_nonzero: + return self.base.is_algebraic + elif self.base.is_algebraic: + return True + if self.exp.is_positive: + return self.base.is_algebraic + elif self.base.is_algebraic and self.exp.is_algebraic: + if ((fuzzy_not(self.base.is_zero) + and fuzzy_not(_is_one(self.base))) + or self.base.is_integer is False + or self.base.is_irrational): + return self.exp.is_rational + + def _eval_is_rational_function(self, syms): + if self.exp.has(*syms): + return False + + if self.base.has(*syms): + return self.base._eval_is_rational_function(syms) and \ + self.exp.is_Integer + else: + return True + + def _eval_is_meromorphic(self, x, a): + # f**g is meromorphic if g is an integer and f is meromorphic. + # E**(log(f)*g) is meromorphic if log(f)*g is meromorphic + # and finite. + base_merom = self.base._eval_is_meromorphic(x, a) + exp_integer = self.exp.is_Integer + if exp_integer: + return base_merom + + exp_merom = self.exp._eval_is_meromorphic(x, a) + if base_merom is False: + # f**g = E**(log(f)*g) may be meromorphic if the + # singularities of log(f) and g cancel each other, + # for example, if g = 1/log(f). Hence, + return False if exp_merom else None + elif base_merom is None: + return None + + b = self.base.subs(x, a) + # b is extended complex as base is meromorphic. + # log(base) is finite and meromorphic when b != 0, zoo. + b_zero = b.is_zero + if b_zero: + log_defined = False + else: + log_defined = fuzzy_and((b.is_finite, fuzzy_not(b_zero))) + + if log_defined is False: # zero or pole of base + return exp_integer # False or None + elif log_defined is None: + return None + + if not exp_merom: + return exp_merom # False or None + + return self.exp.subs(x, a).is_finite + + def _eval_is_algebraic_expr(self, syms): + if self.exp.has(*syms): + return False + + if self.base.has(*syms): + return self.base._eval_is_algebraic_expr(syms) and \ + self.exp.is_Rational + else: + return True + + def _eval_rewrite_as_exp(self, base, expo, **kwargs): + from sympy.functions.elementary.exponential import exp, log + + if base.is_zero or base.has(exp) or expo.has(exp): + return base**expo + + evaluate = expo.has(Symbol) + + if base.has(Symbol): + # delay evaluation if expo is non symbolic + # (as exp(x*log(5)) automatically reduces to x**5) + if global_parameters.exp_is_pow: + return Pow(S.Exp1, log(base)*expo, evaluate=evaluate) + else: + return exp(log(base)*expo, evaluate=evaluate) + + else: + from sympy.functions.elementary.complexes import arg, Abs + return exp((log(Abs(base)) + S.ImaginaryUnit*arg(base))*expo) + + def as_numer_denom(self): + if not self.is_commutative: + return self, S.One + base, exp = self.as_base_exp() + n, d = base.as_numer_denom() + # this should be the same as ExpBase.as_numer_denom wrt + # exponent handling + neg_exp = exp.is_negative + if exp.is_Mul and not neg_exp and not exp.is_positive: + neg_exp = exp.could_extract_minus_sign() + int_exp = exp.is_integer + # the denominator cannot be separated from the numerator if + # its sign is unknown unless the exponent is an integer, e.g. + # sqrt(a/b) != sqrt(a)/sqrt(b) when a=1 and b=-1. But if the + # denominator is negative the numerator and denominator can + # be negated and the denominator (now positive) separated. + if not (d.is_extended_real or int_exp): + n = base + d = S.One + dnonpos = d.is_nonpositive + if dnonpos: + n, d = -n, -d + elif dnonpos is None and not int_exp: + n = base + d = S.One + if neg_exp: + n, d = d, n + exp = -exp + if exp.is_infinite: + if n is S.One and d is not S.One: + return n, self.func(d, exp) + if n is not S.One and d is S.One: + return self.func(n, exp), d + return self.func(n, exp), self.func(d, exp) + + def matches(self, expr, repl_dict=None, old=False): + expr = _sympify(expr) + if repl_dict is None: + repl_dict = {} + + # special case, pattern = 1 and expr.exp can match to 0 + if expr is S.One: + d = self.exp.matches(S.Zero, repl_dict) + if d is not None: + return d + + # make sure the expression to be matched is an Expr + if not isinstance(expr, Expr): + return None + + b, e = expr.as_base_exp() + + # special case number + sb, se = self.as_base_exp() + if sb.is_Symbol and se.is_Integer and expr: + if e.is_rational: + return sb.matches(b**(e/se), repl_dict) + return sb.matches(expr**(1/se), repl_dict) + + d = repl_dict.copy() + d = self.base.matches(b, d) + if d is None: + return None + + d = self.exp.xreplace(d).matches(e, d) + if d is None: + return Expr.matches(self, expr, repl_dict) + return d + + def _eval_nseries(self, x, n, logx, cdir=0): + # NOTE! This function is an important part of the gruntz algorithm + # for computing limits. It has to return a generalized power + # series with coefficients in C(log, log(x)). In more detail: + # It has to return an expression + # c_0*x**e_0 + c_1*x**e_1 + ... (finitely many terms) + # where e_i are numbers (not necessarily integers) and c_i are + # expressions involving only numbers, the log function, and log(x). + # The series expansion of b**e is computed as follows: + # 1) We express b as f*(1 + g) where f is the leading term of b. + # g has order O(x**d) where d is strictly positive. + # 2) Then b**e = (f**e)*((1 + g)**e). + # (1 + g)**e is computed using binomial series. + from sympy.functions.elementary.exponential import exp, log + from sympy.series.limits import limit + from sympy.series.order import Order + from sympy.core.sympify import sympify + if self.base is S.Exp1: + e_series = self.exp.nseries(x, n=n, logx=logx) + if e_series.is_Order: + return 1 + e_series + e0 = limit(e_series.removeO(), x, 0) + if e0 is S.NegativeInfinity: + return Order(x**n, x) + if e0 is S.Infinity: + return self + t = e_series - e0 + exp_series = term = exp(e0) + # series of exp(e0 + t) in t + for i in range(1, n): + term *= t/i + term = term.nseries(x, n=n, logx=logx) + exp_series += term + exp_series += Order(t**n, x) + from sympy.simplify.powsimp import powsimp + return powsimp(exp_series, deep=True, combine='exp') + from sympy.simplify.powsimp import powdenest + from .numbers import _illegal + self = powdenest(self, force=True).trigsimp() + b, e = self.as_base_exp() + + if e.has(*_illegal): + raise PoleError() + + if e.has(x): + return exp(e*log(b))._eval_nseries(x, n=n, logx=logx, cdir=cdir) + + if logx is not None and b.has(log): + from .symbol import Wild + c, ex = symbols('c, ex', cls=Wild, exclude=[x]) + b = b.replace(log(c*x**ex), log(c) + ex*logx) + self = b**e + + b = b.removeO() + try: + from sympy.functions.special.gamma_functions import polygamma + if b.has(polygamma, S.EulerGamma) and logx is not None: + raise ValueError() + _, m = b.leadterm(x) + except (ValueError, NotImplementedError, PoleError): + b = b._eval_nseries(x, n=max(2, n), logx=logx, cdir=cdir).removeO() + if b.has(S.NaN, S.ComplexInfinity): + raise NotImplementedError() + _, m = b.leadterm(x) + + if e.has(log): + from sympy.simplify.simplify import logcombine + e = logcombine(e).cancel() + + if not (m.is_zero or e.is_number and e.is_real): + if self == self._eval_as_leading_term(x, logx=logx, cdir=cdir): + res = exp(e*log(b))._eval_nseries(x, n=n, logx=logx, cdir=cdir) + if res == exp(e*log(b)): + return self + return res + + f = b.as_leading_term(x, logx=logx) + g = (_mexpand(b) - f).cancel() + g = g/f + if not m.is_number: + raise NotImplementedError() + maxpow = n - m*e + if maxpow.has(Symbol): + maxpow = sympify(n) + + if maxpow.is_negative: + return Order(x**(m*e), x) + + if g.is_zero: + r = f**e + if r != self: + r += Order(x**n, x) + return r + + def coeff_exp(term, x): + coeff, exp = S.One, S.Zero + for factor in Mul.make_args(term): + if factor.has(x): + base, exp = factor.as_base_exp() + if base != x: + try: + return term.leadterm(x) + except ValueError: + return term, S.Zero + else: + coeff *= factor + return coeff, exp + + def mul(d1, d2): + res = {} + for e1, e2 in product(d1, d2): + ex = e1 + e2 + if ex < maxpow: + res[ex] = res.get(ex, S.Zero) + d1[e1]*d2[e2] + return res + + try: + c, d = g.leadterm(x, logx=logx) + except (ValueError, NotImplementedError): + if limit(g/x**maxpow, x, 0) == 0: + # g has higher order zero + return f**e + e*f**e*g # first term of binomial series + else: + raise NotImplementedError() + if c.is_Float and d == S.Zero: + # Convert floats like 0.5 to exact SymPy numbers like S.Half, to + # prevent rounding errors which can induce wrong values of d leading + # to a NotImplementedError being returned from the block below. + g = g.replace(lambda x: x.is_Float, lambda x: Rational(x)) + _, d = g.leadterm(x, logx=logx) + if not d.is_positive: + g = g.simplify() + if g.is_zero: + return f**e + _, d = g.leadterm(x, logx=logx) + if not d.is_positive: + g = ((b - f)/f).expand() + _, d = g.leadterm(x, logx=logx) + if not d.is_positive: + raise NotImplementedError() + + from sympy.functions.elementary.integers import ceiling + gpoly = g._eval_nseries(x, n=ceiling(maxpow), logx=logx, cdir=cdir).removeO() + gterms = {} + + for term in Add.make_args(gpoly): + co1, e1 = coeff_exp(term, x) + gterms[e1] = gterms.get(e1, S.Zero) + co1 + + k = S.One + terms = {S.Zero: S.One} + tk = gterms + + from sympy.functions.combinatorial.factorials import factorial, ff + + while (k*d - maxpow).is_negative: + coeff = ff(e, k)/factorial(k) + for ex in tk: + terms[ex] = terms.get(ex, S.Zero) + coeff*tk[ex] + tk = mul(tk, gterms) + k += S.One + + from sympy.functions.elementary.complexes import im + + if not e.is_integer and m.is_zero and f.is_negative: + ndir = (b - f).dir(x, cdir) + if im(ndir).is_negative: + inco, inex = coeff_exp(f**e*(-1)**(-2*e), x) + elif im(ndir).is_zero: + inco, inex = coeff_exp(exp(e*log(b)).as_leading_term(x, logx=logx, cdir=cdir), x) + else: + inco, inex = coeff_exp(f**e, x) + else: + inco, inex = coeff_exp(f**e, x) + res = S.Zero + + for e1 in terms: + ex = e1 + inex + res += terms[e1]*inco*x**(ex) + + if not (e.is_integer and e.is_positive and (e*d - n).is_nonpositive and + res == _mexpand(self)): + try: + res += Order(x**n, x) + except NotImplementedError: + return exp(e*log(b))._eval_nseries(x, n=n, logx=logx, cdir=cdir) + return res + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.functions.elementary.exponential import exp, log + e = self.exp + b = self.base + if self.base is S.Exp1: + arg = e.as_leading_term(x, logx=logx) + arg0 = arg.subs(x, 0) + if arg0 is S.NaN: + arg0 = arg.limit(x, 0) + if arg0.is_infinite is False: + return S.Exp1**arg0 + raise PoleError("Cannot expand %s around 0" % (self)) + elif e.has(x): + lt = exp(e * log(b)) + return lt.as_leading_term(x, logx=logx, cdir=cdir) + else: + from sympy.functions.elementary.complexes import im + try: + f = b.as_leading_term(x, logx=logx, cdir=cdir) + except PoleError: + return self + if not e.is_integer and f.is_negative and not f.has(x): + ndir = (b - f).dir(x, cdir) + if im(ndir).is_negative: + # Normally, f**e would evaluate to exp(e*log(f)) but on branch cuts + # an other value is expected through the following computation + # exp(e*(log(f) - 2*pi*I)) == f**e*exp(-2*e*pi*I) == f**e*(-1)**(-2*e). + return self.func(f, e) * (-1)**(-2*e) + elif im(ndir).is_zero: + log_leadterm = log(b)._eval_as_leading_term(x, logx=logx, cdir=cdir) + if log_leadterm.is_infinite is False: + return exp(e*log_leadterm) + return self.func(f, e) + + @cacheit + def _taylor_term(self, n, x, *previous_terms): # of (1 + x)**e + from sympy.functions.combinatorial.factorials import binomial + return binomial(self.exp, n) * self.func(x, n) + + def taylor_term(self, n, x, *previous_terms): + if self.base is not S.Exp1: + return super().taylor_term(n, x, *previous_terms) + if n < 0: + return S.Zero + if n == 0: + return S.One + from .sympify import sympify + x = sympify(x) + if previous_terms: + p = previous_terms[-1] + if p is not None: + return p * x / n + from sympy.functions.combinatorial.factorials import factorial + return x**n/factorial(n) + + def _eval_rewrite_as_sin(self, base, exp, **hints): + if self.base is S.Exp1: + from sympy.functions.elementary.trigonometric import sin + return sin(S.ImaginaryUnit*self.exp + S.Pi/2) - S.ImaginaryUnit*sin(S.ImaginaryUnit*self.exp) + + def _eval_rewrite_as_cos(self, base, exp, **hints): + if self.base is S.Exp1: + from sympy.functions.elementary.trigonometric import cos + return cos(S.ImaginaryUnit*self.exp) + S.ImaginaryUnit*cos(S.ImaginaryUnit*self.exp + S.Pi/2) + + def _eval_rewrite_as_tanh(self, base, exp, **hints): + if self.base is S.Exp1: + from sympy.functions.elementary.hyperbolic import tanh + return (1 + tanh(self.exp/2))/(1 - tanh(self.exp/2)) + + def _eval_rewrite_as_sqrt(self, base, exp, **kwargs): + from sympy.functions.elementary.trigonometric import sin, cos + if base is not S.Exp1: + return None + if exp.is_Mul: + coeff = exp.coeff(S.Pi * S.ImaginaryUnit) + if coeff and coeff.is_number: + cosine, sine = cos(S.Pi*coeff), sin(S.Pi*coeff) + if not isinstance(cosine, cos) and not isinstance (sine, sin): + return cosine + S.ImaginaryUnit*sine + + def as_content_primitive(self, radical=False, clear=True): + """Return the tuple (R, self/R) where R is the positive Rational + extracted from self. + + Examples + ======== + + >>> from sympy import sqrt + >>> sqrt(4 + 4*sqrt(2)).as_content_primitive() + (2, sqrt(1 + sqrt(2))) + >>> sqrt(3 + 3*sqrt(2)).as_content_primitive() + (1, sqrt(3)*sqrt(1 + sqrt(2))) + + >>> from sympy import expand_power_base, powsimp, Mul + >>> from sympy.abc import x, y + + >>> ((2*x + 2)**2).as_content_primitive() + (4, (x + 1)**2) + >>> (4**((1 + y)/2)).as_content_primitive() + (2, 4**(y/2)) + >>> (3**((1 + y)/2)).as_content_primitive() + (1, 3**((y + 1)/2)) + >>> (3**((5 + y)/2)).as_content_primitive() + (9, 3**((y + 1)/2)) + >>> eq = 3**(2 + 2*x) + >>> powsimp(eq) == eq + True + >>> eq.as_content_primitive() + (9, 3**(2*x)) + >>> powsimp(Mul(*_)) + 3**(2*x + 2) + + >>> eq = (2 + 2*x)**y + >>> s = expand_power_base(eq); s.is_Mul, s + (False, (2*x + 2)**y) + >>> eq.as_content_primitive() + (1, (2*(x + 1))**y) + >>> s = expand_power_base(_[1]); s.is_Mul, s + (True, 2**y*(x + 1)**y) + + See docstring of Expr.as_content_primitive for more examples. + """ + + b, e = self.as_base_exp() + b = _keep_coeff(*b.as_content_primitive(radical=radical, clear=clear)) + ce, pe = e.as_content_primitive(radical=radical, clear=clear) + if b.is_Rational: + #e + #= ce*pe + #= ce*(h + t) + #= ce*h + ce*t + #=> self + #= b**(ce*h)*b**(ce*t) + #= b**(cehp/cehq)*b**(ce*t) + #= b**(iceh + r/cehq)*b**(ce*t) + #= b**(iceh)*b**(r/cehq)*b**(ce*t) + #= b**(iceh)*b**(ce*t + r/cehq) + h, t = pe.as_coeff_Add() + if h.is_Rational and b != S.Zero: + ceh = ce*h + c = self.func(b, ceh) + r = S.Zero + if not c.is_Rational: + iceh, r = divmod(ceh.p, ceh.q) + c = self.func(b, iceh) + return c, self.func(b, _keep_coeff(ce, t + r/ce/ceh.q)) + e = _keep_coeff(ce, pe) + # b**e = (h*t)**e = h**e*t**e = c*m*t**e + if e.is_Rational and b.is_Mul: + h, t = b.as_content_primitive(radical=radical, clear=clear) # h is positive + c, m = self.func(h, e).as_coeff_Mul() # so c is positive + m, me = m.as_base_exp() + if m is S.One or me == e: # probably always true + # return the following, not return c, m*Pow(t, e) + # which would change Pow into Mul; we let SymPy + # decide what to do by using the unevaluated Mul, e.g + # should it stay as sqrt(2 + 2*sqrt(5)) or become + # sqrt(2)*sqrt(1 + sqrt(5)) + return c, self.func(_keep_coeff(m, t), e) + return S.One, self.func(b, e) + + def is_constant(self, *wrt, **flags): + expr = self + if flags.get('simplify', True): + expr = expr.simplify() + b, e = expr.as_base_exp() + bz = b.equals(0) + if bz: # recalculate with assumptions in case it's unevaluated + new = b**e + if new != expr: + return new.is_constant() + econ = e.is_constant(*wrt) + bcon = b.is_constant(*wrt) + if bcon: + if econ: + return True + bz = b.equals(0) + if bz is False: + return False + elif bcon is None: + return None + + return e.equals(0) + + def _eval_difference_delta(self, n, step): + b, e = self.args + if e.has(n) and not b.has(n): + new_e = e.subs(n, n + step) + return (b**(new_e - e) - 1) * self + +power = Dispatcher('power') +power.add((object, object), Pow) + +from .add import Add +from .numbers import Integer, Rational +from .mul import Mul, _keep_coeff +from .symbol import Symbol, Dummy, symbols diff --git a/venv/lib/python3.11/site-packages/sympy/core/random.py b/venv/lib/python3.11/site-packages/sympy/core/random.py new file mode 100644 index 0000000000000000000000000000000000000000..c02986283523b39462a1e2c0b97e3fb230cff100 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/random.py @@ -0,0 +1,227 @@ +""" +When you need to use random numbers in SymPy library code, import from here +so there is only one generator working for SymPy. Imports from here should +behave the same as if they were being imported from Python's random module. +But only the routines currently used in SymPy are included here. To use others +import ``rng`` and access the method directly. For example, to capture the +current state of the generator use ``rng.getstate()``. + +There is intentionally no Random to import from here. If you want +to control the state of the generator, import ``seed`` and call it +with or without an argument to set the state. + +Examples +======== + +>>> from sympy.core.random import random, seed +>>> assert random() < 1 +>>> seed(1); a = random() +>>> b = random() +>>> seed(1); c = random() +>>> assert a == c +>>> assert a != b # remote possibility this will fail + +""" +from sympy.utilities.iterables import is_sequence +from sympy.utilities.misc import as_int + +import random as _random +rng = _random.Random() + +choice = rng.choice +random = rng.random +randint = rng.randint +randrange = rng.randrange +sample = rng.sample +# seed = rng.seed +shuffle = rng.shuffle +uniform = rng.uniform + +_assumptions_rng = _random.Random() +_assumptions_shuffle = _assumptions_rng.shuffle + + +def seed(a=None, version=2): + rng.seed(a=a, version=version) + _assumptions_rng.seed(a=a, version=version) + + +def random_complex_number(a=2, b=-1, c=3, d=1, rational=False, tolerance=None): + """ + Return a random complex number. + + To reduce chance of hitting branch cuts or anything, we guarantee + b <= Im z <= d, a <= Re z <= c + + When rational is True, a rational approximation to a random number + is obtained within specified tolerance, if any. + """ + from sympy.core.numbers import I + from sympy.simplify.simplify import nsimplify + A, B = uniform(a, c), uniform(b, d) + if not rational: + return A + I*B + return (nsimplify(A, rational=True, tolerance=tolerance) + + I*nsimplify(B, rational=True, tolerance=tolerance)) + + +def verify_numerically(f, g, z=None, tol=1.0e-6, a=2, b=-1, c=3, d=1): + """ + Test numerically that f and g agree when evaluated in the argument z. + + If z is None, all symbols will be tested. This routine does not test + whether there are Floats present with precision higher than 15 digits + so if there are, your results may not be what you expect due to round- + off errors. + + Examples + ======== + + >>> from sympy import sin, cos + >>> from sympy.abc import x + >>> from sympy.core.random import verify_numerically as tn + >>> tn(sin(x)**2 + cos(x)**2, 1, x) + True + """ + from sympy.core.symbol import Symbol + from sympy.core.sympify import sympify + from sympy.core.numbers import comp + f, g = (sympify(i) for i in (f, g)) + if z is None: + z = f.free_symbols | g.free_symbols + elif isinstance(z, Symbol): + z = [z] + reps = list(zip(z, [random_complex_number(a, b, c, d) for _ in z])) + z1 = f.subs(reps).n() + z2 = g.subs(reps).n() + return comp(z1, z2, tol) + + +def test_derivative_numerically(f, z, tol=1.0e-6, a=2, b=-1, c=3, d=1): + """ + Test numerically that the symbolically computed derivative of f + with respect to z is correct. + + This routine does not test whether there are Floats present with + precision higher than 15 digits so if there are, your results may + not be what you expect due to round-off errors. + + Examples + ======== + + >>> from sympy import sin + >>> from sympy.abc import x + >>> from sympy.core.random import test_derivative_numerically as td + >>> td(sin(x), x) + True + """ + from sympy.core.numbers import comp + from sympy.core.function import Derivative + z0 = random_complex_number(a, b, c, d) + f1 = f.diff(z).subs(z, z0) + f2 = Derivative(f, z).doit_numerically(z0) + return comp(f1.n(), f2.n(), tol) + + +def _randrange(seed=None): + """Return a randrange generator. + + ``seed`` can be + + * None - return randomly seeded generator + * int - return a generator seeded with the int + * list - the values to be returned will be taken from the list + in the order given; the provided list is not modified. + + Examples + ======== + + >>> from sympy.core.random import _randrange + >>> rr = _randrange() + >>> rr(1000) # doctest: +SKIP + 999 + >>> rr = _randrange(3) + >>> rr(1000) # doctest: +SKIP + 238 + >>> rr = _randrange([0, 5, 1, 3, 4]) + >>> rr(3), rr(3) + (0, 1) + """ + if seed is None: + return randrange + elif isinstance(seed, int): + rng.seed(seed) + return randrange + elif is_sequence(seed): + seed = list(seed) # make a copy + seed.reverse() + + def give(a, b=None, seq=seed): + if b is None: + a, b = 0, a + a, b = as_int(a), as_int(b) + w = b - a + if w < 1: + raise ValueError('_randrange got empty range') + try: + x = seq.pop() + except IndexError: + raise ValueError('_randrange sequence was too short') + if a <= x < b: + return x + else: + return give(a, b, seq) + return give + else: + raise ValueError('_randrange got an unexpected seed') + + +def _randint(seed=None): + """Return a randint generator. + + ``seed`` can be + + * None - return randomly seeded generator + * int - return a generator seeded with the int + * list - the values to be returned will be taken from the list + in the order given; the provided list is not modified. + + Examples + ======== + + >>> from sympy.core.random import _randint + >>> ri = _randint() + >>> ri(1, 1000) # doctest: +SKIP + 999 + >>> ri = _randint(3) + >>> ri(1, 1000) # doctest: +SKIP + 238 + >>> ri = _randint([0, 5, 1, 2, 4]) + >>> ri(1, 3), ri(1, 3) + (1, 2) + """ + if seed is None: + return randint + elif isinstance(seed, int): + rng.seed(seed) + return randint + elif is_sequence(seed): + seed = list(seed) # make a copy + seed.reverse() + + def give(a, b, seq=seed): + a, b = as_int(a), as_int(b) + w = b - a + if w < 0: + raise ValueError('_randint got empty range') + try: + x = seq.pop() + except IndexError: + raise ValueError('_randint sequence was too short') + if a <= x <= b: + return x + else: + return give(a, b, seq) + return give + else: + raise ValueError('_randint got an unexpected seed') diff --git a/venv/lib/python3.11/site-packages/sympy/core/relational.py b/venv/lib/python3.11/site-packages/sympy/core/relational.py new file mode 100644 index 0000000000000000000000000000000000000000..28bf039c9be67a6f5cd6f11df1968961c0760373 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/relational.py @@ -0,0 +1,1622 @@ +from __future__ import annotations + +from .basic import Atom, Basic +from .coreerrors import LazyExceptionMessage +from .sorting import ordered +from .evalf import EvalfMixin +from .function import AppliedUndef +from .numbers import int_valued +from .singleton import S +from .sympify import _sympify, SympifyError +from .parameters import global_parameters +from .logic import fuzzy_bool, fuzzy_xor, fuzzy_and, fuzzy_not +from sympy.logic.boolalg import Boolean, BooleanAtom +from sympy.utilities.iterables import sift +from sympy.utilities.misc import filldedent +from sympy.utilities.exceptions import sympy_deprecation_warning + + +__all__ = ( + 'Rel', 'Eq', 'Ne', 'Lt', 'Le', 'Gt', 'Ge', + 'Relational', 'Equality', 'Unequality', 'StrictLessThan', 'LessThan', + 'StrictGreaterThan', 'GreaterThan', +) + +from .expr import Expr +from sympy.multipledispatch import dispatch +from .containers import Tuple +from .symbol import Symbol + + +def _nontrivBool(side): + return isinstance(side, Boolean) and \ + not isinstance(side, Atom) + + +# Note, see issue 4986. Ideally, we wouldn't want to subclass both Boolean +# and Expr. +# from .. import Expr + + +def _canonical(cond): + # return a condition in which all relationals are canonical + reps = {r: r.canonical for r in cond.atoms(Relational)} + return cond.xreplace(reps) + # XXX: AttributeError was being caught here but it wasn't triggered by any of + # the tests so I've removed it... + + +def _canonical_coeff(rel): + # return -2*x + 1 < 0 as x > 1/2 + # XXX make this part of Relational.canonical? + rel = rel.canonical + if not rel.is_Relational or rel.rhs.is_Boolean: + return rel # Eq(x, True) + if not isinstance(rel.lhs, Expr): + return rel.reversed # e.g.: Eq(True, x) -> Eq(x, True) + b, l = rel.lhs.as_coeff_Add(rational=True) + m, lhs = l.as_coeff_Mul(rational=True) + rhs = (rel.rhs - b)/m + if m < 0: + return rel.reversed.func(lhs, rhs) + return rel.func(lhs, rhs) + + +class Relational(Boolean, EvalfMixin): + """Base class for all relation types. + + Explanation + =========== + + Subclasses of Relational should generally be instantiated directly, but + Relational can be instantiated with a valid ``rop`` value to dispatch to + the appropriate subclass. + + Parameters + ========== + + rop : str or None + Indicates what subclass to instantiate. Valid values can be found + in the keys of Relational.ValidRelationOperator. + + Examples + ======== + + >>> from sympy import Rel + >>> from sympy.abc import x, y + >>> Rel(y, x + x**2, '==') + Eq(y, x**2 + x) + + A relation's type can be defined upon creation using ``rop``. + The relation type of an existing expression can be obtained + using its ``rel_op`` property. + Here is a table of all the relation types, along with their + ``rop`` and ``rel_op`` values: + + +---------------------+----------------------------+------------+ + |Relation |``rop`` |``rel_op`` | + +=====================+============================+============+ + |``Equality`` |``==`` or ``eq`` or ``None``|``==`` | + +---------------------+----------------------------+------------+ + |``Unequality`` |``!=`` or ``ne`` |``!=`` | + +---------------------+----------------------------+------------+ + |``GreaterThan`` |``>=`` or ``ge`` |``>=`` | + +---------------------+----------------------------+------------+ + |``LessThan`` |``<=`` or ``le`` |``<=`` | + +---------------------+----------------------------+------------+ + |``StrictGreaterThan``|``>`` or ``gt`` |``>`` | + +---------------------+----------------------------+------------+ + |``StrictLessThan`` |``<`` or ``lt`` |``<`` | + +---------------------+----------------------------+------------+ + + For example, setting ``rop`` to ``==`` produces an + ``Equality`` relation, ``Eq()``. + So does setting ``rop`` to ``eq``, or leaving ``rop`` unspecified. + That is, the first three ``Rel()`` below all produce the same result. + Using a ``rop`` from a different row in the table produces a + different relation type. + For example, the fourth ``Rel()`` below using ``lt`` for ``rop`` + produces a ``StrictLessThan`` inequality: + + >>> from sympy import Rel + >>> from sympy.abc import x, y + >>> Rel(y, x + x**2, '==') + Eq(y, x**2 + x) + >>> Rel(y, x + x**2, 'eq') + Eq(y, x**2 + x) + >>> Rel(y, x + x**2) + Eq(y, x**2 + x) + >>> Rel(y, x + x**2, 'lt') + y < x**2 + x + + To obtain the relation type of an existing expression, + get its ``rel_op`` property. + For example, ``rel_op`` is ``==`` for the ``Equality`` relation above, + and ``<`` for the strict less than inequality above: + + >>> from sympy import Rel + >>> from sympy.abc import x, y + >>> my_equality = Rel(y, x + x**2, '==') + >>> my_equality.rel_op + '==' + >>> my_inequality = Rel(y, x + x**2, 'lt') + >>> my_inequality.rel_op + '<' + + """ + __slots__ = () + + ValidRelationOperator: dict[str | None, type[Relational]] = {} + + is_Relational = True + + # ValidRelationOperator - Defined below, because the necessary classes + # have not yet been defined + + def __new__(cls, lhs, rhs, rop=None, **assumptions): + # If called by a subclass, do nothing special and pass on to Basic. + if cls is not Relational: + return Basic.__new__(cls, lhs, rhs, **assumptions) + + # XXX: Why do this? There should be a separate function to make a + # particular subclass of Relational from a string. + # + # If called directly with an operator, look up the subclass + # corresponding to that operator and delegate to it + cls = cls.ValidRelationOperator.get(rop, None) + if cls is None: + raise ValueError("Invalid relational operator symbol: %r" % rop) + + if not issubclass(cls, (Eq, Ne)): + # validate that Booleans are not being used in a relational + # other than Eq/Ne; + # Note: Symbol is a subclass of Boolean but is considered + # acceptable here. + if any(map(_nontrivBool, (lhs, rhs))): + raise TypeError(filldedent(''' + A Boolean argument can only be used in + Eq and Ne; all other relationals expect + real expressions. + ''')) + + return cls(lhs, rhs, **assumptions) + + @property + def lhs(self): + """The left-hand side of the relation.""" + return self._args[0] + + @property + def rhs(self): + """The right-hand side of the relation.""" + return self._args[1] + + @property + def reversed(self): + """Return the relationship with sides reversed. + + Examples + ======== + + >>> from sympy import Eq + >>> from sympy.abc import x + >>> Eq(x, 1) + Eq(x, 1) + >>> _.reversed + Eq(1, x) + >>> x < 1 + x < 1 + >>> _.reversed + 1 > x + """ + ops = {Eq: Eq, Gt: Lt, Ge: Le, Lt: Gt, Le: Ge, Ne: Ne} + a, b = self.args + return Relational.__new__(ops.get(self.func, self.func), b, a) + + @property + def reversedsign(self): + """Return the relationship with signs reversed. + + Examples + ======== + + >>> from sympy import Eq + >>> from sympy.abc import x + >>> Eq(x, 1) + Eq(x, 1) + >>> _.reversedsign + Eq(-x, -1) + >>> x < 1 + x < 1 + >>> _.reversedsign + -x > -1 + """ + a, b = self.args + if not (isinstance(a, BooleanAtom) or isinstance(b, BooleanAtom)): + ops = {Eq: Eq, Gt: Lt, Ge: Le, Lt: Gt, Le: Ge, Ne: Ne} + return Relational.__new__(ops.get(self.func, self.func), -a, -b) + else: + return self + + @property + def negated(self): + """Return the negated relationship. + + Examples + ======== + + >>> from sympy import Eq + >>> from sympy.abc import x + >>> Eq(x, 1) + Eq(x, 1) + >>> _.negated + Ne(x, 1) + >>> x < 1 + x < 1 + >>> _.negated + x >= 1 + + Notes + ===== + + This works more or less identical to ``~``/``Not``. The difference is + that ``negated`` returns the relationship even if ``evaluate=False``. + Hence, this is useful in code when checking for e.g. negated relations + to existing ones as it will not be affected by the `evaluate` flag. + + """ + ops = {Eq: Ne, Ge: Lt, Gt: Le, Le: Gt, Lt: Ge, Ne: Eq} + # If there ever will be new Relational subclasses, the following line + # will work until it is properly sorted out + # return ops.get(self.func, lambda a, b, evaluate=False: ~(self.func(a, + # b, evaluate=evaluate)))(*self.args, evaluate=False) + return Relational.__new__(ops.get(self.func), *self.args) + + @property + def weak(self): + """return the non-strict version of the inequality or self + + EXAMPLES + ======== + + >>> from sympy.abc import x + >>> (x < 1).weak + x <= 1 + >>> _.weak + x <= 1 + """ + return self + + @property + def strict(self): + """return the strict version of the inequality or self + + EXAMPLES + ======== + + >>> from sympy.abc import x + >>> (x <= 1).strict + x < 1 + >>> _.strict + x < 1 + """ + return self + + def _eval_evalf(self, prec): + return self.func(*[s._evalf(prec) for s in self.args]) + + @property + def canonical(self): + """Return a canonical form of the relational by putting a + number on the rhs, canonically removing a sign or else + ordering the args canonically. No other simplification is + attempted. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> x < 2 + x < 2 + >>> _.reversed.canonical + x < 2 + >>> (-y < x).canonical + x > -y + >>> (-y > x).canonical + x < -y + >>> (-y < -x).canonical + x < y + + The canonicalization is recursively applied: + + >>> from sympy import Eq + >>> Eq(x < y, y > x).canonical + True + """ + args = tuple([i.canonical if isinstance(i, Relational) else i for i in self.args]) + if args != self.args: + r = self.func(*args) + if not isinstance(r, Relational): + return r + else: + r = self + if r.rhs.is_number: + if r.rhs.is_Number and r.lhs.is_Number and r.lhs > r.rhs: + r = r.reversed + elif r.lhs.is_number: + r = r.reversed + elif tuple(ordered(args)) != args: + r = r.reversed + + LHS_CEMS = getattr(r.lhs, 'could_extract_minus_sign', None) + RHS_CEMS = getattr(r.rhs, 'could_extract_minus_sign', None) + + if isinstance(r.lhs, BooleanAtom) or isinstance(r.rhs, BooleanAtom): + return r + + # Check if first value has negative sign + if LHS_CEMS and LHS_CEMS(): + return r.reversedsign + elif not r.rhs.is_number and RHS_CEMS and RHS_CEMS(): + # Right hand side has a minus, but not lhs. + # How does the expression with reversed signs behave? + # This is so that expressions of the type + # Eq(x, -y) and Eq(-x, y) + # have the same canonical representation + expr1, _ = ordered([r.lhs, -r.rhs]) + if expr1 != r.lhs: + return r.reversed.reversedsign + + return r + + def equals(self, other, failing_expression=False): + """Return True if the sides of the relationship are mathematically + identical and the type of relationship is the same. + If failing_expression is True, return the expression whose truth value + was unknown.""" + if isinstance(other, Relational): + if other in (self, self.reversed): + return True + a, b = self, other + if a.func in (Eq, Ne) or b.func in (Eq, Ne): + if a.func != b.func: + return False + left, right = [i.equals(j, + failing_expression=failing_expression) + for i, j in zip(a.args, b.args)] + if left is True: + return right + if right is True: + return left + lr, rl = [i.equals(j, failing_expression=failing_expression) + for i, j in zip(a.args, b.reversed.args)] + if lr is True: + return rl + if rl is True: + return lr + e = (left, right, lr, rl) + if all(i is False for i in e): + return False + for i in e: + if i not in (True, False): + return i + else: + if b.func != a.func: + b = b.reversed + if a.func != b.func: + return False + left = a.lhs.equals(b.lhs, + failing_expression=failing_expression) + if left is False: + return False + right = a.rhs.equals(b.rhs, + failing_expression=failing_expression) + if right is False: + return False + if left is True: + return right + return left + + def _eval_simplify(self, **kwargs): + from .add import Add + from .expr import Expr + r = self + r = r.func(*[i.simplify(**kwargs) for i in r.args]) + if r.is_Relational: + if not isinstance(r.lhs, Expr) or not isinstance(r.rhs, Expr): + return r + dif = r.lhs - r.rhs + # replace dif with a valid Number that will + # allow a definitive comparison with 0 + v = None + if dif.is_comparable: + v = dif.n(2) + if any(i._prec == 1 for i in v.as_real_imag()): + rv, iv = [i.n(2) for i in dif.as_real_imag()] + v = rv + S.ImaginaryUnit*iv + elif dif.equals(0): # XXX this is expensive + v = S.Zero + if v is not None: + r = r.func._eval_relation(v, S.Zero) + r = r.canonical + # If there is only one symbol in the expression, + # try to write it on a simplified form + free = list(filter(lambda x: x.is_real is not False, r.free_symbols)) + if len(free) == 1: + try: + from sympy.solvers.solveset import linear_coeffs + x = free.pop() + dif = r.lhs - r.rhs + m, b = linear_coeffs(dif, x) + if m.is_zero is False: + if m.is_negative: + # Dividing with a negative number, so change order of arguments + # canonical will put the symbol back on the lhs later + r = r.func(-b / m, x) + else: + r = r.func(x, -b / m) + else: + r = r.func(b, S.Zero) + except ValueError: + # maybe not a linear function, try polynomial + from sympy.polys.polyerrors import PolynomialError + from sympy.polys.polytools import gcd, Poly, poly + try: + p = poly(dif, x) + c = p.all_coeffs() + constant = c[-1] + c[-1] = 0 + scale = gcd(c) + c = [ctmp / scale for ctmp in c] + r = r.func(Poly.from_list(c, x).as_expr(), -constant / scale) + except PolynomialError: + pass + elif len(free) >= 2: + try: + from sympy.solvers.solveset import linear_coeffs + from sympy.polys.polytools import gcd + free = list(ordered(free)) + dif = r.lhs - r.rhs + m = linear_coeffs(dif, *free) + constant = m[-1] + del m[-1] + scale = gcd(m) + m = [mtmp / scale for mtmp in m] + nzm = list(filter(lambda f: f[0] != 0, list(zip(m, free)))) + if scale.is_zero is False: + if constant != 0: + # lhs: expression, rhs: constant + newexpr = Add(*[i * j for i, j in nzm]) + r = r.func(newexpr, -constant / scale) + else: + # keep first term on lhs + lhsterm = nzm[0][0] * nzm[0][1] + del nzm[0] + newexpr = Add(*[i * j for i, j in nzm]) + r = r.func(lhsterm, -newexpr) + + else: + r = r.func(constant, S.Zero) + except ValueError: + pass + # Did we get a simplified result? + r = r.canonical + measure = kwargs['measure'] + if measure(r) < kwargs['ratio'] * measure(self): + return r + else: + return self + + def _eval_trigsimp(self, **opts): + from sympy.simplify.trigsimp import trigsimp + return self.func(trigsimp(self.lhs, **opts), trigsimp(self.rhs, **opts)) + + def expand(self, **kwargs): + args = (arg.expand(**kwargs) for arg in self.args) + return self.func(*args) + + def __bool__(self) -> bool: + raise TypeError( + LazyExceptionMessage( + lambda: f"cannot determine truth value of Relational: {self}" + ) + ) + + def _eval_as_set(self): + # self is univariate and periodicity(self, x) in (0, None) + from sympy.solvers.inequalities import solve_univariate_inequality + from sympy.sets.conditionset import ConditionSet + syms = self.free_symbols + assert len(syms) == 1 + x = syms.pop() + try: + xset = solve_univariate_inequality(self, x, relational=False) + except NotImplementedError: + # solve_univariate_inequality raises NotImplementedError for + # unsolvable equations/inequalities. + xset = ConditionSet(x, self, S.Reals) + return xset + + @property + def binary_symbols(self): + # override where necessary + return set() + + +Rel = Relational + + +class Equality(Relational): + """ + An equal relation between two objects. + + Explanation + =========== + + Represents that two objects are equal. If they can be easily shown + to be definitively equal (or unequal), this will reduce to True (or + False). Otherwise, the relation is maintained as an unevaluated + Equality object. Use the ``simplify`` function on this object for + more nontrivial evaluation of the equality relation. + + As usual, the keyword argument ``evaluate=False`` can be used to + prevent any evaluation. + + Examples + ======== + + >>> from sympy import Eq, simplify, exp, cos + >>> from sympy.abc import x, y + >>> Eq(y, x + x**2) + Eq(y, x**2 + x) + >>> Eq(2, 5) + False + >>> Eq(2, 5, evaluate=False) + Eq(2, 5) + >>> _.doit() + False + >>> Eq(exp(x), exp(x).rewrite(cos)) + Eq(exp(x), sinh(x) + cosh(x)) + >>> simplify(_) + True + + See Also + ======== + + sympy.logic.boolalg.Equivalent : for representing equality between two + boolean expressions + + Notes + ===== + + Python treats 1 and True (and 0 and False) as being equal; SymPy + does not. And integer will always compare as unequal to a Boolean: + + >>> Eq(True, 1), True == 1 + (False, True) + + This class is not the same as the == operator. The == operator tests + for exact structural equality between two expressions; this class + compares expressions mathematically. + + If either object defines an ``_eval_Eq`` method, it can be used in place of + the default algorithm. If ``lhs._eval_Eq(rhs)`` or ``rhs._eval_Eq(lhs)`` + returns anything other than None, that return value will be substituted for + the Equality. If None is returned by ``_eval_Eq``, an Equality object will + be created as usual. + + Since this object is already an expression, it does not respond to + the method ``as_expr`` if one tries to create `x - y` from ``Eq(x, y)``. + If ``eq = Eq(x, y)`` then write `eq.lhs - eq.rhs` to get ``x - y``. + + .. deprecated:: 1.5 + + ``Eq(expr)`` with a single argument is a shorthand for ``Eq(expr, 0)``, + but this behavior is deprecated and will be removed in a future version + of SymPy. + + """ + rel_op = '==' + + __slots__ = () + + is_Equality = True + + def __new__(cls, lhs, rhs, **options): + evaluate = options.pop('evaluate', global_parameters.evaluate) + lhs = _sympify(lhs) + rhs = _sympify(rhs) + if evaluate: + val = is_eq(lhs, rhs) + if val is None: + return cls(lhs, rhs, evaluate=False) + else: + return _sympify(val) + + return Relational.__new__(cls, lhs, rhs) + + @classmethod + def _eval_relation(cls, lhs, rhs): + return _sympify(lhs == rhs) + + def _eval_rewrite_as_Add(self, L, R, evaluate=True, **kwargs): + """ + return Eq(L, R) as L - R. To control the evaluation of + the result set pass `evaluate=True` to give L - R; + if `evaluate=None` then terms in L and R will not cancel + but they will be listed in canonical order; otherwise + non-canonical args will be returned. If one side is 0, the + non-zero side will be returned. + + .. deprecated:: 1.13 + + The method ``Eq.rewrite(Add)`` is deprecated. + See :ref:`eq-rewrite-Add` for details. + + Examples + ======== + + >>> from sympy import Eq, Add + >>> from sympy.abc import b, x + >>> eq = Eq(x + b, x - b) + >>> eq.rewrite(Add) #doctest: +SKIP + 2*b + >>> eq.rewrite(Add, evaluate=None).args #doctest: +SKIP + (b, b, x, -x) + >>> eq.rewrite(Add, evaluate=False).args #doctest: +SKIP + (b, x, b, -x) + """ + sympy_deprecation_warning(""" + Eq.rewrite(Add) is deprecated. + + For ``eq = Eq(a, b)`` use ``eq.lhs - eq.rhs`` to obtain + ``a - b``. + """, + deprecated_since_version="1.13", + active_deprecations_target="eq-rewrite-Add", + stacklevel=5, + ) + from .add import _unevaluated_Add, Add + if L == 0: + return R + if R == 0: + return L + if evaluate: + # allow cancellation of args + return L - R + args = Add.make_args(L) + Add.make_args(-R) + if evaluate is None: + # no cancellation, but canonical + return _unevaluated_Add(*args) + # no cancellation, not canonical + return Add._from_args(args) + + @property + def binary_symbols(self): + if S.true in self.args or S.false in self.args: + if self.lhs.is_Symbol: + return {self.lhs} + elif self.rhs.is_Symbol: + return {self.rhs} + return set() + + def _eval_simplify(self, **kwargs): + # standard simplify + e = super()._eval_simplify(**kwargs) + if not isinstance(e, Equality): + return e + from .expr import Expr + if not isinstance(e.lhs, Expr) or not isinstance(e.rhs, Expr): + return e + free = self.free_symbols + if len(free) == 1: + try: + from .add import Add + from sympy.solvers.solveset import linear_coeffs + x = free.pop() + m, b = linear_coeffs( + Add(e.lhs, -e.rhs, evaluate=False), x) + if m.is_zero is False: + enew = e.func(x, -b / m) + else: + enew = e.func(m * x, -b) + measure = kwargs['measure'] + if measure(enew) <= kwargs['ratio'] * measure(e): + e = enew + except ValueError: + pass + return e.canonical + + def integrate(self, *args, **kwargs): + """See the integrate function in sympy.integrals""" + from sympy.integrals.integrals import integrate + return integrate(self, *args, **kwargs) + + def as_poly(self, *gens, **kwargs): + '''Returns lhs-rhs as a Poly + + Examples + ======== + + >>> from sympy import Eq + >>> from sympy.abc import x + >>> Eq(x**2, 1).as_poly(x) + Poly(x**2 - 1, x, domain='ZZ') + ''' + return (self.lhs - self.rhs).as_poly(*gens, **kwargs) + + +Eq = Equality + + +class Unequality(Relational): + """An unequal relation between two objects. + + Explanation + =========== + + Represents that two objects are not equal. If they can be shown to be + definitively equal, this will reduce to False; if definitively unequal, + this will reduce to True. Otherwise, the relation is maintained as an + Unequality object. + + Examples + ======== + + >>> from sympy import Ne + >>> from sympy.abc import x, y + >>> Ne(y, x+x**2) + Ne(y, x**2 + x) + + See Also + ======== + Equality + + Notes + ===== + This class is not the same as the != operator. The != operator tests + for exact structural equality between two expressions; this class + compares expressions mathematically. + + This class is effectively the inverse of Equality. As such, it uses the + same algorithms, including any available `_eval_Eq` methods. + + """ + rel_op = '!=' + + __slots__ = () + + def __new__(cls, lhs, rhs, **options): + lhs = _sympify(lhs) + rhs = _sympify(rhs) + evaluate = options.pop('evaluate', global_parameters.evaluate) + if evaluate: + val = is_neq(lhs, rhs) + if val is None: + return cls(lhs, rhs, evaluate=False) + else: + return _sympify(val) + + return Relational.__new__(cls, lhs, rhs, **options) + + @classmethod + def _eval_relation(cls, lhs, rhs): + return _sympify(lhs != rhs) + + @property + def binary_symbols(self): + if S.true in self.args or S.false in self.args: + if self.lhs.is_Symbol: + return {self.lhs} + elif self.rhs.is_Symbol: + return {self.rhs} + return set() + + def _eval_simplify(self, **kwargs): + # simplify as an equality + eq = Equality(*self.args)._eval_simplify(**kwargs) + if isinstance(eq, Equality): + # send back Ne with the new args + return self.func(*eq.args) + return eq.negated # result of Ne is the negated Eq + + +Ne = Unequality + + +class _Inequality(Relational): + """Internal base class for all *Than types. + + Each subclass must implement _eval_relation to provide the method for + comparing two real numbers. + + """ + __slots__ = () + + def __new__(cls, lhs, rhs, **options): + + try: + lhs = _sympify(lhs) + rhs = _sympify(rhs) + except SympifyError: + return NotImplemented + + evaluate = options.pop('evaluate', global_parameters.evaluate) + if evaluate: + for me in (lhs, rhs): + if me.is_extended_real is False: + raise TypeError("Invalid comparison of non-real %s" % me) + if me is S.NaN: + raise TypeError("Invalid NaN comparison") + # First we invoke the appropriate inequality method of `lhs` + # (e.g., `lhs.__lt__`). That method will try to reduce to + # boolean or raise an exception. It may keep calling + # superclasses until it reaches `Expr` (e.g., `Expr.__lt__`). + # In some cases, `Expr` will just invoke us again (if neither it + # nor a subclass was able to reduce to boolean or raise an + # exception). In that case, it must call us with + # `evaluate=False` to prevent infinite recursion. + return cls._eval_relation(lhs, rhs, **options) + + # make a "non-evaluated" Expr for the inequality + return Relational.__new__(cls, lhs, rhs, **options) + + @classmethod + def _eval_relation(cls, lhs, rhs, **options): + val = cls._eval_fuzzy_relation(lhs, rhs) + if val is None: + return cls(lhs, rhs, evaluate=False) + else: + return _sympify(val) + + +class _Greater(_Inequality): + """Not intended for general use + + _Greater is only used so that GreaterThan and StrictGreaterThan may + subclass it for the .gts and .lts properties. + + """ + __slots__ = () + + @property + def gts(self): + return self._args[0] + + @property + def lts(self): + return self._args[1] + + +class _Less(_Inequality): + """Not intended for general use. + + _Less is only used so that LessThan and StrictLessThan may subclass it for + the .gts and .lts properties. + + """ + __slots__ = () + + @property + def gts(self): + return self._args[1] + + @property + def lts(self): + return self._args[0] + + +class GreaterThan(_Greater): + r"""Class representations of inequalities. + + Explanation + =========== + + The ``*Than`` classes represent inequal relationships, where the left-hand + side is generally bigger or smaller than the right-hand side. For example, + the GreaterThan class represents an inequal relationship where the + left-hand side is at least as big as the right side, if not bigger. In + mathematical notation: + + lhs $\ge$ rhs + + In total, there are four ``*Than`` classes, to represent the four + inequalities: + + +-----------------+--------+ + |Class Name | Symbol | + +=================+========+ + |GreaterThan | ``>=`` | + +-----------------+--------+ + |LessThan | ``<=`` | + +-----------------+--------+ + |StrictGreaterThan| ``>`` | + +-----------------+--------+ + |StrictLessThan | ``<`` | + +-----------------+--------+ + + All classes take two arguments, lhs and rhs. + + +----------------------------+-----------------+ + |Signature Example | Math Equivalent | + +============================+=================+ + |GreaterThan(lhs, rhs) | lhs $\ge$ rhs | + +----------------------------+-----------------+ + |LessThan(lhs, rhs) | lhs $\le$ rhs | + +----------------------------+-----------------+ + |StrictGreaterThan(lhs, rhs) | lhs $>$ rhs | + +----------------------------+-----------------+ + |StrictLessThan(lhs, rhs) | lhs $<$ rhs | + +----------------------------+-----------------+ + + In addition to the normal .lhs and .rhs of Relations, ``*Than`` inequality + objects also have the .lts and .gts properties, which represent the "less + than side" and "greater than side" of the operator. Use of .lts and .gts + in an algorithm rather than .lhs and .rhs as an assumption of inequality + direction will make more explicit the intent of a certain section of code, + and will make it similarly more robust to client code changes: + + >>> from sympy import GreaterThan, StrictGreaterThan + >>> from sympy import LessThan, StrictLessThan + >>> from sympy import And, Ge, Gt, Le, Lt, Rel, S + >>> from sympy.abc import x, y, z + >>> from sympy.core.relational import Relational + + >>> e = GreaterThan(x, 1) + >>> e + x >= 1 + >>> '%s >= %s is the same as %s <= %s' % (e.gts, e.lts, e.lts, e.gts) + 'x >= 1 is the same as 1 <= x' + + Examples + ======== + + One generally does not instantiate these classes directly, but uses various + convenience methods: + + >>> for f in [Ge, Gt, Le, Lt]: # convenience wrappers + ... print(f(x, 2)) + x >= 2 + x > 2 + x <= 2 + x < 2 + + Another option is to use the Python inequality operators (``>=``, ``>``, + ``<=``, ``<``) directly. Their main advantage over the ``Ge``, ``Gt``, + ``Le``, and ``Lt`` counterparts, is that one can write a more + "mathematical looking" statement rather than littering the math with + oddball function calls. However there are certain (minor) caveats of + which to be aware (search for 'gotcha', below). + + >>> x >= 2 + x >= 2 + >>> _ == Ge(x, 2) + True + + However, it is also perfectly valid to instantiate a ``*Than`` class less + succinctly and less conveniently: + + >>> Rel(x, 1, ">") + x > 1 + >>> Relational(x, 1, ">") + x > 1 + + >>> StrictGreaterThan(x, 1) + x > 1 + >>> GreaterThan(x, 1) + x >= 1 + >>> LessThan(x, 1) + x <= 1 + >>> StrictLessThan(x, 1) + x < 1 + + Notes + ===== + + There are a couple of "gotchas" to be aware of when using Python's + operators. + + The first is that what your write is not always what you get: + + >>> 1 < x + x > 1 + + Due to the order that Python parses a statement, it may + not immediately find two objects comparable. When ``1 < x`` + is evaluated, Python recognizes that the number 1 is a native + number and that x is *not*. Because a native Python number does + not know how to compare itself with a SymPy object + Python will try the reflective operation, ``x > 1`` and that is the + form that gets evaluated, hence returned. + + If the order of the statement is important (for visual output to + the console, perhaps), one can work around this annoyance in a + couple ways: + + (1) "sympify" the literal before comparison + + >>> S(1) < x + 1 < x + + (2) use one of the wrappers or less succinct methods described + above + + >>> Lt(1, x) + 1 < x + >>> Relational(1, x, "<") + 1 < x + + The second gotcha involves writing equality tests between relationals + when one or both sides of the test involve a literal relational: + + >>> e = x < 1; e + x < 1 + >>> e == e # neither side is a literal + True + >>> e == x < 1 # expecting True, too + False + >>> e != x < 1 # expecting False + x < 1 + >>> x < 1 != x < 1 # expecting False or the same thing as before + Traceback (most recent call last): + ... + TypeError: cannot determine truth value of Relational + + The solution for this case is to wrap literal relationals in + parentheses: + + >>> e == (x < 1) + True + >>> e != (x < 1) + False + >>> (x < 1) != (x < 1) + False + + The third gotcha involves chained inequalities not involving + ``==`` or ``!=``. Occasionally, one may be tempted to write: + + >>> e = x < y < z + Traceback (most recent call last): + ... + TypeError: symbolic boolean expression has no truth value. + + Due to an implementation detail or decision of Python [1]_, + there is no way for SymPy to create a chained inequality with + that syntax so one must use And: + + >>> e = And(x < y, y < z) + >>> type( e ) + And + >>> e + (x < y) & (y < z) + + Although this can also be done with the '&' operator, it cannot + be done with the 'and' operarator: + + >>> (x < y) & (y < z) + (x < y) & (y < z) + >>> (x < y) and (y < z) + Traceback (most recent call last): + ... + TypeError: cannot determine truth value of Relational + + .. [1] This implementation detail is that Python provides no reliable + method to determine that a chained inequality is being built. + Chained comparison operators are evaluated pairwise, using "and" + logic (see + https://docs.python.org/3/reference/expressions.html#not-in). This + is done in an efficient way, so that each object being compared + is only evaluated once and the comparison can short-circuit. For + example, ``1 > 2 > 3`` is evaluated by Python as ``(1 > 2) and (2 + > 3)``. The ``and`` operator coerces each side into a bool, + returning the object itself when it short-circuits. The bool of + the --Than operators will raise TypeError on purpose, because + SymPy cannot determine the mathematical ordering of symbolic + expressions. Thus, if we were to compute ``x > y > z``, with + ``x``, ``y``, and ``z`` being Symbols, Python converts the + statement (roughly) into these steps: + + (1) x > y > z + (2) (x > y) and (y > z) + (3) (GreaterThanObject) and (y > z) + (4) (GreaterThanObject.__bool__()) and (y > z) + (5) TypeError + + Because of the ``and`` added at step 2, the statement gets turned into a + weak ternary statement, and the first object's ``__bool__`` method will + raise TypeError. Thus, creating a chained inequality is not possible. + + In Python, there is no way to override the ``and`` operator, or to + control how it short circuits, so it is impossible to make something + like ``x > y > z`` work. There was a PEP to change this, + :pep:`335`, but it was officially closed in March, 2012. + + """ + __slots__ = () + + rel_op = '>=' + + @classmethod + def _eval_fuzzy_relation(cls, lhs, rhs): + return is_ge(lhs, rhs) + + @property + def strict(self): + return Gt(*self.args) + +Ge = GreaterThan + + +class LessThan(_Less): + __doc__ = GreaterThan.__doc__ + __slots__ = () + + rel_op = '<=' + + @classmethod + def _eval_fuzzy_relation(cls, lhs, rhs): + return is_le(lhs, rhs) + + @property + def strict(self): + return Lt(*self.args) + +Le = LessThan + + +class StrictGreaterThan(_Greater): + __doc__ = GreaterThan.__doc__ + __slots__ = () + + rel_op = '>' + + @classmethod + def _eval_fuzzy_relation(cls, lhs, rhs): + return is_gt(lhs, rhs) + + @property + def weak(self): + return Ge(*self.args) + + +Gt = StrictGreaterThan + + +class StrictLessThan(_Less): + __doc__ = GreaterThan.__doc__ + __slots__ = () + + rel_op = '<' + + @classmethod + def _eval_fuzzy_relation(cls, lhs, rhs): + return is_lt(lhs, rhs) + + @property + def weak(self): + return Le(*self.args) + +Lt = StrictLessThan + +# A class-specific (not object-specific) data item used for a minor speedup. +# It is defined here, rather than directly in the class, because the classes +# that it references have not been defined until now (e.g. StrictLessThan). +Relational.ValidRelationOperator = { + None: Equality, + '==': Equality, + 'eq': Equality, + '!=': Unequality, + '<>': Unequality, + 'ne': Unequality, + '>=': GreaterThan, + 'ge': GreaterThan, + '<=': LessThan, + 'le': LessThan, + '>': StrictGreaterThan, + 'gt': StrictGreaterThan, + '<': StrictLessThan, + 'lt': StrictLessThan, +} + + +def _n2(a, b): + """Return (a - b).evalf(2) if a and b are comparable, else None. + This should only be used when a and b are already sympified. + """ + # /!\ it is very important (see issue 8245) not to + # use a re-evaluated number in the calculation of dif + if a.is_comparable and b.is_comparable: + dif = (a - b).evalf(2) + if dif.is_comparable: + return dif + + +@dispatch(Expr, Expr) +def _eval_is_ge(lhs, rhs): + return None + + +@dispatch(Basic, Basic) +def _eval_is_eq(lhs, rhs): + return None + + +@dispatch(Tuple, Expr) # type: ignore +def _eval_is_eq(lhs, rhs): # noqa:F811 + return False + + +@dispatch(Tuple, AppliedUndef) # type: ignore +def _eval_is_eq(lhs, rhs): # noqa:F811 + return None + + +@dispatch(Tuple, Symbol) # type: ignore +def _eval_is_eq(lhs, rhs): # noqa:F811 + return None + + +@dispatch(Tuple, Tuple) # type: ignore +def _eval_is_eq(lhs, rhs): # noqa:F811 + if len(lhs) != len(rhs): + return False + + return fuzzy_and(fuzzy_bool(is_eq(s, o)) for s, o in zip(lhs, rhs)) + + +def is_lt(lhs, rhs, assumptions=None): + """Fuzzy bool for lhs is strictly less than rhs. + + See the docstring for :func:`~.is_ge` for more. + """ + return fuzzy_not(is_ge(lhs, rhs, assumptions)) + + +def is_gt(lhs, rhs, assumptions=None): + """Fuzzy bool for lhs is strictly greater than rhs. + + See the docstring for :func:`~.is_ge` for more. + """ + return fuzzy_not(is_le(lhs, rhs, assumptions)) + + +def is_le(lhs, rhs, assumptions=None): + """Fuzzy bool for lhs is less than or equal to rhs. + + See the docstring for :func:`~.is_ge` for more. + """ + return is_ge(rhs, lhs, assumptions) + + +def is_ge(lhs, rhs, assumptions=None): + """ + Fuzzy bool for *lhs* is greater than or equal to *rhs*. + + Parameters + ========== + + lhs : Expr + The left-hand side of the expression, must be sympified, + and an instance of expression. Throws an exception if + lhs is not an instance of expression. + + rhs : Expr + The right-hand side of the expression, must be sympified + and an instance of expression. Throws an exception if + lhs is not an instance of expression. + + assumptions: Boolean, optional + Assumptions taken to evaluate the inequality. + + Returns + ======= + + ``True`` if *lhs* is greater than or equal to *rhs*, ``False`` if *lhs* + is less than *rhs*, and ``None`` if the comparison between *lhs* and + *rhs* is indeterminate. + + Explanation + =========== + + This function is intended to give a relatively fast determination and + deliberately does not attempt slow calculations that might help in + obtaining a determination of True or False in more difficult cases. + + The four comparison functions ``is_le``, ``is_lt``, ``is_ge``, and ``is_gt`` are + each implemented in terms of ``is_ge`` in the following way: + + is_ge(x, y) := is_ge(x, y) + is_le(x, y) := is_ge(y, x) + is_lt(x, y) := fuzzy_not(is_ge(x, y)) + is_gt(x, y) := fuzzy_not(is_ge(y, x)) + + Therefore, supporting new type with this function will ensure behavior for + other three functions as well. + + To maintain these equivalences in fuzzy logic it is important that in cases where + either x or y is non-real all comparisons will give None. + + Examples + ======== + + >>> from sympy import S, Q + >>> from sympy.core.relational import is_ge, is_le, is_gt, is_lt + >>> from sympy.abc import x + >>> is_ge(S(2), S(0)) + True + >>> is_ge(S(0), S(2)) + False + >>> is_le(S(0), S(2)) + True + >>> is_gt(S(0), S(2)) + False + >>> is_lt(S(2), S(0)) + False + + Assumptions can be passed to evaluate the quality which is otherwise + indeterminate. + + >>> print(is_ge(x, S(0))) + None + >>> is_ge(x, S(0), assumptions=Q.positive(x)) + True + + New types can be supported by dispatching to ``_eval_is_ge``. + + >>> from sympy import Expr, sympify + >>> from sympy.multipledispatch import dispatch + >>> class MyExpr(Expr): + ... def __new__(cls, arg): + ... return super().__new__(cls, sympify(arg)) + ... @property + ... def value(self): + ... return self.args[0] + >>> @dispatch(MyExpr, MyExpr) + ... def _eval_is_ge(a, b): + ... return is_ge(a.value, b.value) + >>> a = MyExpr(1) + >>> b = MyExpr(2) + >>> is_ge(b, a) + True + >>> is_le(a, b) + True + """ + from sympy.assumptions.wrapper import AssumptionsWrapper, is_extended_nonnegative + + if not (isinstance(lhs, Expr) and isinstance(rhs, Expr)): + raise TypeError("Can only compare inequalities with Expr") + + retval = _eval_is_ge(lhs, rhs) + + if retval is not None: + return retval + else: + n2 = _n2(lhs, rhs) + if n2 is not None: + # use float comparison for infinity. + # otherwise get stuck in infinite recursion + if n2 in (S.Infinity, S.NegativeInfinity): + n2 = float(n2) + return n2 >= 0 + + _lhs = AssumptionsWrapper(lhs, assumptions) + _rhs = AssumptionsWrapper(rhs, assumptions) + if _lhs.is_extended_real and _rhs.is_extended_real: + if (_lhs.is_infinite and _lhs.is_extended_positive) or (_rhs.is_infinite and _rhs.is_extended_negative): + return True + diff = lhs - rhs + if diff is not S.NaN: + rv = is_extended_nonnegative(diff, assumptions) + if rv is not None: + return rv + + +def is_neq(lhs, rhs, assumptions=None): + """Fuzzy bool for lhs does not equal rhs. + + See the docstring for :func:`~.is_eq` for more. + """ + return fuzzy_not(is_eq(lhs, rhs, assumptions)) + + +def is_eq(lhs, rhs, assumptions=None): + """ + Fuzzy bool representing mathematical equality between *lhs* and *rhs*. + + Parameters + ========== + + lhs : Expr + The left-hand side of the expression, must be sympified. + + rhs : Expr + The right-hand side of the expression, must be sympified. + + assumptions: Boolean, optional + Assumptions taken to evaluate the equality. + + Returns + ======= + + ``True`` if *lhs* is equal to *rhs*, ``False`` is *lhs* is not equal to *rhs*, + and ``None`` if the comparison between *lhs* and *rhs* is indeterminate. + + Explanation + =========== + + This function is intended to give a relatively fast determination and + deliberately does not attempt slow calculations that might help in + obtaining a determination of True or False in more difficult cases. + + :func:`~.is_neq` calls this function to return its value, so supporting + new type with this function will ensure correct behavior for ``is_neq`` + as well. + + Examples + ======== + + >>> from sympy import Q, S + >>> from sympy.core.relational import is_eq, is_neq + >>> from sympy.abc import x + >>> is_eq(S(0), S(0)) + True + >>> is_neq(S(0), S(0)) + False + >>> is_eq(S(0), S(2)) + False + >>> is_neq(S(0), S(2)) + True + + Assumptions can be passed to evaluate the equality which is otherwise + indeterminate. + + >>> print(is_eq(x, S(0))) + None + >>> is_eq(x, S(0), assumptions=Q.zero(x)) + True + + New types can be supported by dispatching to ``_eval_is_eq``. + + >>> from sympy import Basic, sympify + >>> from sympy.multipledispatch import dispatch + >>> class MyBasic(Basic): + ... def __new__(cls, arg): + ... return Basic.__new__(cls, sympify(arg)) + ... @property + ... def value(self): + ... return self.args[0] + ... + >>> @dispatch(MyBasic, MyBasic) + ... def _eval_is_eq(a, b): + ... return is_eq(a.value, b.value) + ... + >>> a = MyBasic(1) + >>> b = MyBasic(1) + >>> is_eq(a, b) + True + >>> is_neq(a, b) + False + + """ + # here, _eval_Eq is only called for backwards compatibility + # new code should use is_eq with multiple dispatch as + # outlined in the docstring + for side1, side2 in (lhs, rhs), (rhs, lhs): + eval_func = getattr(side1, '_eval_Eq', None) + if eval_func is not None: + retval = eval_func(side2) + if retval is not None: + return retval + + retval = _eval_is_eq(lhs, rhs) + if retval is not None: + return retval + + if dispatch(type(lhs), type(rhs)) != dispatch(type(rhs), type(lhs)): + retval = _eval_is_eq(rhs, lhs) + if retval is not None: + return retval + + # retval is still None, so go through the equality logic + # If expressions have the same structure, they must be equal. + if lhs == rhs: + return True # e.g. True == True + elif all(isinstance(i, BooleanAtom) for i in (rhs, lhs)): + return False # True != False + elif not (lhs.is_Symbol or rhs.is_Symbol) and ( + isinstance(lhs, Boolean) != + isinstance(rhs, Boolean)): + return False # only Booleans can equal Booleans + + from sympy.assumptions.wrapper import (AssumptionsWrapper, + is_infinite, is_extended_real) + from .add import Add + + _lhs = AssumptionsWrapper(lhs, assumptions) + _rhs = AssumptionsWrapper(rhs, assumptions) + + if _lhs.is_infinite or _rhs.is_infinite: + if fuzzy_xor([_lhs.is_infinite, _rhs.is_infinite]): + return False + if fuzzy_xor([_lhs.is_extended_real, _rhs.is_extended_real]): + return False + if fuzzy_and([_lhs.is_extended_real, _rhs.is_extended_real]): + return fuzzy_xor([_lhs.is_extended_positive, fuzzy_not(_rhs.is_extended_positive)]) + + # Try to split real/imaginary parts and equate them + I = S.ImaginaryUnit + + def split_real_imag(expr): + real_imag = lambda t: ( + 'real' if is_extended_real(t, assumptions) else + 'imag' if is_extended_real(I*t, assumptions) else None) + return sift(Add.make_args(expr), real_imag) + + lhs_ri = split_real_imag(lhs) + if not lhs_ri[None]: + rhs_ri = split_real_imag(rhs) + if not rhs_ri[None]: + eq_real = is_eq(Add(*lhs_ri['real']), Add(*rhs_ri['real']), assumptions) + eq_imag = is_eq(I * Add(*lhs_ri['imag']), I * Add(*rhs_ri['imag']), assumptions) + return fuzzy_and(map(fuzzy_bool, [eq_real, eq_imag])) + + from sympy.functions.elementary.complexes import arg + # Compare e.g. zoo with 1+I*oo by comparing args + arglhs = arg(lhs) + argrhs = arg(rhs) + # Guard against Eq(nan, nan) -> False + if not (arglhs == S.NaN and argrhs == S.NaN): + return fuzzy_bool(is_eq(arglhs, argrhs, assumptions)) + + if all(isinstance(i, Expr) for i in (lhs, rhs)): + # see if the difference evaluates + dif = lhs - rhs + _dif = AssumptionsWrapper(dif, assumptions) + z = _dif.is_zero + if z is not None: + if z is False and _dif.is_commutative: # issue 10728 + return False + if z: + return True + + # is_zero cannot help decide integer/rational with Float + c, t = dif.as_coeff_Add() + if c.is_Float: + if int_valued(c): + if t.is_integer is False: + return False + elif t.is_rational is False: + return False + + n2 = _n2(lhs, rhs) + if n2 is not None: + return _sympify(n2 == 0) + + # see if the ratio evaluates + n, d = dif.as_numer_denom() + rv = None + _n = AssumptionsWrapper(n, assumptions) + _d = AssumptionsWrapper(d, assumptions) + if _n.is_zero: + rv = _d.is_nonzero + elif _n.is_finite: + if _d.is_infinite: + rv = True + elif _n.is_zero is False: + rv = _d.is_infinite + if rv is None: + # if the condition that makes the denominator + # infinite does not make the original expression + # True then False can be returned + from sympy.simplify.simplify import clear_coefficients + l, r = clear_coefficients(d, S.Infinity) + args = [_.subs(l, r) for _ in (lhs, rhs)] + if args != [lhs, rhs]: + rv = fuzzy_bool(is_eq(*args, assumptions)) + if rv is True: + rv = None + elif any(is_infinite(a, assumptions) for a in Add.make_args(n)): + # (inf or nan)/x != 0 + rv = False + if rv is not None: + return rv diff --git a/venv/lib/python3.11/site-packages/sympy/core/rules.py b/venv/lib/python3.11/site-packages/sympy/core/rules.py new file mode 100644 index 0000000000000000000000000000000000000000..5ae331f71b21c8a6ef35f499c5c5c89239349e9c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/rules.py @@ -0,0 +1,66 @@ +""" +Replacement rules. +""" + +class Transform: + """ + Immutable mapping that can be used as a generic transformation rule. + + Parameters + ========== + + transform : callable + Computes the value corresponding to any key. + + filter : callable, optional + If supplied, specifies which objects are in the mapping. + + Examples + ======== + + >>> from sympy.core.rules import Transform + >>> from sympy.abc import x + + This Transform will return, as a value, one more than the key: + + >>> add1 = Transform(lambda x: x + 1) + >>> add1[1] + 2 + >>> add1[x] + x + 1 + + By default, all values are considered to be in the dictionary. If a filter + is supplied, only the objects for which it returns True are considered as + being in the dictionary: + + >>> add1_odd = Transform(lambda x: x + 1, lambda x: x%2 == 1) + >>> 2 in add1_odd + False + >>> add1_odd.get(2, 0) + 0 + >>> 3 in add1_odd + True + >>> add1_odd[3] + 4 + >>> add1_odd.get(3, 0) + 4 + """ + + def __init__(self, transform, filter=lambda x: True): + self._transform = transform + self._filter = filter + + def __contains__(self, item): + return self._filter(item) + + def __getitem__(self, key): + if self._filter(key): + return self._transform(key) + else: + raise KeyError(key) + + def get(self, item, default=None): + if item in self: + return self[item] + else: + return default diff --git a/venv/lib/python3.11/site-packages/sympy/core/singleton.py b/venv/lib/python3.11/site-packages/sympy/core/singleton.py new file mode 100644 index 0000000000000000000000000000000000000000..e8b9df959393270140bc3ef11b3d9a4e948c5e80 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/singleton.py @@ -0,0 +1,199 @@ +"""Singleton mechanism""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .core import Registry +from .sympify import sympify + + +if TYPE_CHECKING: + from sympy.core.numbers import ( + Zero as _Zero, + One as _One, + NegativeOne as _NegativeOne, + Half as _Half, + Infinity as _Infinity, + NegativeInfinity as _NegativeInfinity, + ComplexInfinity as _ComplexInfinity, + NaN as _NaN, + ) + + +class SingletonRegistry(Registry): + """ + The registry for the singleton classes (accessible as ``S``). + + Explanation + =========== + + This class serves as two separate things. + + The first thing it is is the ``SingletonRegistry``. Several classes in + SymPy appear so often that they are singletonized, that is, using some + metaprogramming they are made so that they can only be instantiated once + (see the :class:`sympy.core.singleton.Singleton` class for details). For + instance, every time you create ``Integer(0)``, this will return the same + instance, :class:`sympy.core.numbers.Zero`. All singleton instances are + attributes of the ``S`` object, so ``Integer(0)`` can also be accessed as + ``S.Zero``. + + Singletonization offers two advantages: it saves memory, and it allows + fast comparison. It saves memory because no matter how many times the + singletonized objects appear in expressions in memory, they all point to + the same single instance in memory. The fast comparison comes from the + fact that you can use ``is`` to compare exact instances in Python + (usually, you need to use ``==`` to compare things). ``is`` compares + objects by memory address, and is very fast. + + Examples + ======== + + >>> from sympy import S, Integer + >>> a = Integer(0) + >>> a is S.Zero + True + + For the most part, the fact that certain objects are singletonized is an + implementation detail that users should not need to worry about. In SymPy + library code, ``is`` comparison is often used for performance purposes + The primary advantage of ``S`` for end users is the convenient access to + certain instances that are otherwise difficult to type, like ``S.Half`` + (instead of ``Rational(1, 2)``). + + When using ``is`` comparison, make sure the argument is sympified. For + instance, + + >>> x = 0 + >>> x is S.Zero + False + + This problem is not an issue when using ``==``, which is recommended for + most use-cases: + + >>> 0 == S.Zero + True + + The second thing ``S`` is is a shortcut for + :func:`sympy.core.sympify.sympify`. :func:`sympy.core.sympify.sympify` is + the function that converts Python objects such as ``int(1)`` into SymPy + objects such as ``Integer(1)``. It also converts the string form of an + expression into a SymPy expression, like ``sympify("x**2")`` -> + ``Symbol("x")**2``. ``S(1)`` is the same thing as ``sympify(1)`` + (basically, ``S.__call__`` has been defined to call ``sympify``). + + This is for convenience, since ``S`` is a single letter. It's mostly + useful for defining rational numbers. Consider an expression like ``x + + 1/2``. If you enter this directly in Python, it will evaluate the ``1/2`` + and give ``0.5``, because both arguments are ints (see also + :ref:`tutorial-gotchas-final-notes`). However, in SymPy, you usually want + the quotient of two integers to give an exact rational number. The way + Python's evaluation works, at least one side of an operator needs to be a + SymPy object for the SymPy evaluation to take over. You could write this + as ``x + Rational(1, 2)``, but this is a lot more typing. A shorter + version is ``x + S(1)/2``. Since ``S(1)`` returns ``Integer(1)``, the + division will return a ``Rational`` type, since it will call + ``Integer.__truediv__``, which knows how to return a ``Rational``. + + """ + __slots__ = () + + Zero: _Zero + One: _One + NegativeOne: _NegativeOne + Half: _Half + Infinity: _Infinity + NegativeInfinity: _NegativeInfinity + ComplexInfinity: _ComplexInfinity + NaN: _NaN + + # Also allow things like S(5) + __call__ = staticmethod(sympify) + + def __init__(self): + self._classes_to_install = {} + # Dict of classes that have been registered, but that have not have been + # installed as an attribute of this SingletonRegistry. + # Installation automatically happens at the first attempt to access the + # attribute. + # The purpose of this is to allow registration during class + # initialization during import, but not trigger object creation until + # actual use (which should not happen until after all imports are + # finished). + + def register(self, cls): + # Make sure a duplicate class overwrites the old one + if hasattr(self, cls.__name__): + delattr(self, cls.__name__) + self._classes_to_install[cls.__name__] = cls + + def __getattr__(self, name): + """Python calls __getattr__ if no attribute of that name was installed + yet. + + Explanation + =========== + + This __getattr__ checks whether a class with the requested name was + already registered but not installed; if no, raises an AttributeError. + Otherwise, retrieves the class, calculates its singleton value, installs + it as an attribute of the given name, and unregisters the class.""" + if name not in self._classes_to_install: + raise AttributeError( + "Attribute '%s' was not installed on SymPy registry %s" % ( + name, self)) + class_to_install = self._classes_to_install[name] + value_to_install = class_to_install() + self.__setattr__(name, value_to_install) + del self._classes_to_install[name] + return value_to_install + + def __repr__(self): + return "S" + +S = SingletonRegistry() + + +class Singleton(type): + """ + Metaclass for singleton classes. + + Explanation + =========== + + A singleton class has only one instance which is returned every time the + class is instantiated. Additionally, this instance can be accessed through + the global registry object ``S`` as ``S.``. + + Examples + ======== + + >>> from sympy import S, Basic + >>> from sympy.core.singleton import Singleton + >>> class MySingleton(Basic, metaclass=Singleton): + ... pass + >>> Basic() is Basic() + False + >>> MySingleton() is MySingleton() + True + >>> S.MySingleton is MySingleton() + True + + Notes + ===== + + Instance creation is delayed until the first time the value is accessed. + (SymPy versions before 1.0 would create the instance during class + creation time, which would be prone to import cycles.) + """ + def __init__(cls, *args, **kwargs): + cls._instance = obj = Basic.__new__(cls) + cls.__new__ = lambda cls: obj + cls.__getnewargs__ = lambda obj: () + cls.__getstate__ = lambda obj: None + S.register(cls) + + +# Delayed to avoid cyclic import +from .basic import Basic diff --git a/venv/lib/python3.11/site-packages/sympy/core/sorting.py b/venv/lib/python3.11/site-packages/sympy/core/sorting.py new file mode 100644 index 0000000000000000000000000000000000000000..399a7efa1f6cbe1ebdf6307c14b411df36fc7de0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/sorting.py @@ -0,0 +1,312 @@ +from collections import defaultdict + +from .sympify import sympify, SympifyError +from sympy.utilities.iterables import iterable, uniq + + +__all__ = ['default_sort_key', 'ordered'] + + +def default_sort_key(item, order=None): + """Return a key that can be used for sorting. + + The key has the structure: + + (class_key, (len(args), args), exponent.sort_key(), coefficient) + + This key is supplied by the sort_key routine of Basic objects when + ``item`` is a Basic object or an object (other than a string) that + sympifies to a Basic object. Otherwise, this function produces the + key. + + The ``order`` argument is passed along to the sort_key routine and is + used to determine how the terms *within* an expression are ordered. + (See examples below) ``order`` options are: 'lex', 'grlex', 'grevlex', + and reversed values of the same (e.g. 'rev-lex'). The default order + value is None (which translates to 'lex'). + + Examples + ======== + + >>> from sympy import S, I, default_sort_key, sin, cos, sqrt + >>> from sympy.core.function import UndefinedFunction + >>> from sympy.abc import x + + The following are equivalent ways of getting the key for an object: + + >>> x.sort_key() == default_sort_key(x) + True + + Here are some examples of the key that is produced: + + >>> default_sort_key(UndefinedFunction('f')) + ((0, 0, 'UndefinedFunction'), (1, ('f',)), ((1, 0, 'Number'), + (0, ()), (), 1), 1) + >>> default_sort_key('1') + ((0, 0, 'str'), (1, ('1',)), ((1, 0, 'Number'), (0, ()), (), 1), 1) + >>> default_sort_key(S.One) + ((1, 0, 'Number'), (0, ()), (), 1) + >>> default_sort_key(2) + ((1, 0, 'Number'), (0, ()), (), 2) + + While sort_key is a method only defined for SymPy objects, + default_sort_key will accept anything as an argument so it is + more robust as a sorting key. For the following, using key= + lambda i: i.sort_key() would fail because 2 does not have a sort_key + method; that's why default_sort_key is used. Note, that it also + handles sympification of non-string items likes ints: + + >>> a = [2, I, -I] + >>> sorted(a, key=default_sort_key) + [2, -I, I] + + The returned key can be used anywhere that a key can be specified for + a function, e.g. sort, min, max, etc...: + + >>> a.sort(key=default_sort_key); a[0] + 2 + >>> min(a, key=default_sort_key) + 2 + + Notes + ===== + + The key returned is useful for getting items into a canonical order + that will be the same across platforms. It is not directly useful for + sorting lists of expressions: + + >>> a, b = x, 1/x + + Since ``a`` has only 1 term, its value of sort_key is unaffected by + ``order``: + + >>> a.sort_key() == a.sort_key('rev-lex') + True + + If ``a`` and ``b`` are combined then the key will differ because there + are terms that can be ordered: + + >>> eq = a + b + >>> eq.sort_key() == eq.sort_key('rev-lex') + False + >>> eq.as_ordered_terms() + [x, 1/x] + >>> eq.as_ordered_terms('rev-lex') + [1/x, x] + + But since the keys for each of these terms are independent of ``order``'s + value, they do not sort differently when they appear separately in a list: + + >>> sorted(eq.args, key=default_sort_key) + [1/x, x] + >>> sorted(eq.args, key=lambda i: default_sort_key(i, order='rev-lex')) + [1/x, x] + + The order of terms obtained when using these keys is the order that would + be obtained if those terms were *factors* in a product. + + Although it is useful for quickly putting expressions in canonical order, + it does not sort expressions based on their complexity defined by the + number of operations, power of variables and others: + + >>> sorted([sin(x)*cos(x), sin(x)], key=default_sort_key) + [sin(x)*cos(x), sin(x)] + >>> sorted([x, x**2, sqrt(x), x**3], key=default_sort_key) + [sqrt(x), x, x**2, x**3] + + See Also + ======== + + ordered, sympy.core.expr.Expr.as_ordered_factors, sympy.core.expr.Expr.as_ordered_terms + + """ + from .basic import Basic + from .singleton import S + + if isinstance(item, Basic): + return item.sort_key(order=order) + + if iterable(item, exclude=str): + if isinstance(item, dict): + args = item.items() + unordered = True + elif isinstance(item, set): + args = item + unordered = True + else: + # e.g. tuple, list + args = list(item) + unordered = False + + args = [default_sort_key(arg, order=order) for arg in args] + + if unordered: + # e.g. dict, set + args = sorted(args) + + cls_index, args = 10, (len(args), tuple(args)) + else: + if not isinstance(item, str): + try: + item = sympify(item, strict=True) + except SympifyError: + # e.g. lambda x: x + pass + else: + if isinstance(item, Basic): + # e.g int -> Integer + return default_sort_key(item) + # e.g. UndefinedFunction + + # e.g. str + cls_index, args = 0, (1, (str(item),)) + + return (cls_index, 0, item.__class__.__name__ + ), args, S.One.sort_key(), S.One + + +def _node_count(e): + # this not only counts nodes, it affirms that the + # args are Basic (i.e. have an args property). If + # some object has a non-Basic arg, it needs to be + # fixed since it is intended that all Basic args + # are of Basic type (though this is not easy to enforce). + if e.is_Float: + return 0.5 + return 1 + sum(map(_node_count, e.args)) + + +def _nodes(e): + """ + A helper for ordered() which returns the node count of ``e`` which + for Basic objects is the number of Basic nodes in the expression tree + but for other objects is 1 (unless the object is an iterable or dict + for which the sum of nodes is returned). + """ + from .basic import Basic + from .function import Derivative + + if isinstance(e, Basic): + if isinstance(e, Derivative): + return _nodes(e.expr) + sum(i[1] if i[1].is_Number else + _nodes(i[1]) for i in e.variable_count) + return _node_count(e) + elif iterable(e): + return 1 + sum(_nodes(ei) for ei in e) + elif isinstance(e, dict): + return 1 + sum(_nodes(k) + _nodes(v) for k, v in e.items()) + else: + return 1 + + +def ordered(seq, keys=None, default=True, warn=False): + """Return an iterator of the seq where keys are used to break ties + in a conservative fashion: if, after applying a key, there are no + ties then no other keys will be computed. + + Two default keys will be applied if 1) keys are not provided or + 2) the given keys do not resolve all ties (but only if ``default`` + is True). The two keys are ``_nodes`` (which places smaller + expressions before large) and ``default_sort_key`` which (if the + ``sort_key`` for an object is defined properly) should resolve + any ties. This strategy is similar to sorting done by + ``Basic.compare``, but differs in that ``ordered`` never makes a + decision based on an objects name. + + If ``warn`` is True then an error will be raised if there were no + keys remaining to break ties. This can be used if it was expected that + there should be no ties between items that are not identical. + + Examples + ======== + + >>> from sympy import ordered, count_ops + >>> from sympy.abc import x, y + + The count_ops is not sufficient to break ties in this list and the first + two items appear in their original order (i.e. the sorting is stable): + + >>> list(ordered([y + 2, x + 2, x**2 + y + 3], + ... count_ops, default=False, warn=False)) + ... + [y + 2, x + 2, x**2 + y + 3] + + The default_sort_key allows the tie to be broken: + + >>> list(ordered([y + 2, x + 2, x**2 + y + 3])) + ... + [x + 2, y + 2, x**2 + y + 3] + + Here, sequences are sorted by length, then sum: + + >>> seq, keys = [[[1, 2, 1], [0, 3, 1], [1, 1, 3], [2], [1]], [ + ... lambda x: len(x), + ... lambda x: sum(x)]] + ... + >>> list(ordered(seq, keys, default=False, warn=False)) + [[1], [2], [1, 2, 1], [0, 3, 1], [1, 1, 3]] + + If ``warn`` is True, an error will be raised if there were not + enough keys to break ties: + + >>> list(ordered(seq, keys, default=False, warn=True)) + Traceback (most recent call last): + ... + ValueError: not enough keys to break ties + + + Notes + ===== + + The decorated sort is one of the fastest ways to sort a sequence for + which special item comparison is desired: the sequence is decorated, + sorted on the basis of the decoration (e.g. making all letters lower + case) and then undecorated. If one wants to break ties for items that + have the same decorated value, a second key can be used. But if the + second key is expensive to compute then it is inefficient to decorate + all items with both keys: only those items having identical first key + values need to be decorated. This function applies keys successively + only when needed to break ties. By yielding an iterator, use of the + tie-breaker is delayed as long as possible. + + This function is best used in cases when use of the first key is + expected to be a good hashing function; if there are no unique hashes + from application of a key, then that key should not have been used. The + exception, however, is that even if there are many collisions, if the + first group is small and one does not need to process all items in the + list then time will not be wasted sorting what one was not interested + in. For example, if one were looking for the minimum in a list and + there were several criteria used to define the sort order, then this + function would be good at returning that quickly if the first group + of candidates is small relative to the number of items being processed. + + """ + + d = defaultdict(list) + if keys: + if isinstance(keys, (list, tuple)): + keys = list(keys) + f = keys.pop(0) + else: + f = keys + keys = [] + for a in seq: + d[f(a)].append(a) + else: + if not default: + raise ValueError('if default=False then keys must be provided') + d[None].extend(seq) + + for k, value in sorted(d.items()): + if len(value) > 1: + if keys: + value = ordered(value, keys, default, warn) + elif default: + value = ordered(value, (_nodes, default_sort_key,), + default=False, warn=warn) + elif warn: + u = list(uniq(value)) + if len(u) > 1: + raise ValueError( + 'not enough keys to break ties: %s' % u) + yield from value diff --git a/venv/lib/python3.11/site-packages/sympy/core/symbol.py b/venv/lib/python3.11/site-packages/sympy/core/symbol.py new file mode 100644 index 0000000000000000000000000000000000000000..2e03ff0c84c1668b70ec5b3d7f8bc854a2e5e4ac --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/symbol.py @@ -0,0 +1,993 @@ +from __future__ import annotations + + +from .assumptions import StdFactKB, _assume_defined +from .basic import Basic, Atom +from .cache import cacheit +from .containers import Tuple +from .expr import Expr, AtomicExpr +from .function import AppliedUndef, FunctionClass +from .kind import NumberKind, UndefinedKind +from .logic import fuzzy_bool +from .singleton import S +from .sorting import ordered +from .sympify import sympify +from sympy.logic.boolalg import Boolean +from sympy.utilities.iterables import sift, is_sequence +from sympy.utilities.misc import filldedent + +import string +import re as _re +import random +from itertools import product +from typing import Any + + +class Str(Atom): + """ + Represents string in SymPy. + + Explanation + =========== + + Previously, ``Symbol`` was used where string is needed in ``args`` of SymPy + objects, e.g. denoting the name of the instance. However, since ``Symbol`` + represents mathematical scalar, this class should be used instead. + + """ + __slots__ = ('name',) + + def __new__(cls, name, **kwargs): + if not isinstance(name, str): + raise TypeError("name should be a string, not %s" % repr(type(name))) + obj = Expr.__new__(cls, **kwargs) + obj.name = name + return obj + + def __getnewargs__(self): + return (self.name,) + + def _hashable_content(self): + return (self.name,) + + +def _filter_assumptions(kwargs): + """Split the given dict into assumptions and non-assumptions. + Keys are taken as assumptions if they correspond to an + entry in ``_assume_defined``. + """ + assumptions, nonassumptions = map(dict, sift(kwargs.items(), + lambda i: i[0] in _assume_defined, + binary=True)) + Symbol._sanitize(assumptions) + return assumptions, nonassumptions + +def _symbol(s, matching_symbol=None, **assumptions): + """Return s if s is a Symbol, else if s is a string, return either + the matching_symbol if the names are the same or else a new symbol + with the same assumptions as the matching symbol (or the + assumptions as provided). + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.core.symbol import _symbol + >>> _symbol('y') + y + >>> _.is_real is None + True + >>> _symbol('y', real=True).is_real + True + + >>> x = Symbol('x') + >>> _symbol(x, real=True) + x + >>> _.is_real is None # ignore attribute if s is a Symbol + True + + Below, the variable sym has the name 'foo': + + >>> sym = Symbol('foo', real=True) + + Since 'x' is not the same as sym's name, a new symbol is created: + + >>> _symbol('x', sym).name + 'x' + + It will acquire any assumptions give: + + >>> _symbol('x', sym, real=False).is_real + False + + Since 'foo' is the same as sym's name, sym is returned + + >>> _symbol('foo', sym) + foo + + Any assumptions given are ignored: + + >>> _symbol('foo', sym, real=False).is_real + True + + NB: the symbol here may not be the same as a symbol with the same + name defined elsewhere as a result of different assumptions. + + See Also + ======== + + sympy.core.symbol.Symbol + + """ + if isinstance(s, str): + if matching_symbol and matching_symbol.name == s: + return matching_symbol + return Symbol(s, **assumptions) + elif isinstance(s, Symbol): + return s + else: + raise ValueError('symbol must be string for symbol name or Symbol') + +def uniquely_named_symbol(xname, exprs=(), compare=str, modify=None, **assumptions): + """ + Return a symbol whose name is derivated from *xname* but is unique + from any other symbols in *exprs*. + + *xname* and symbol names in *exprs* are passed to *compare* to be + converted to comparable forms. If ``compare(xname)`` is not unique, + it is recursively passed to *modify* until unique name is acquired. + + Parameters + ========== + + xname : str or Symbol + Base name for the new symbol. + + exprs : Expr or iterable of Expr + Expressions whose symbols are compared to *xname*. + + compare : function + Unary function which transforms *xname* and symbol names from + *exprs* to comparable form. + + modify : function + Unary function which modifies the string. Default is appending + the number, or increasing the number if exists. + + Examples + ======== + + By default, a number is appended to *xname* to generate unique name. + If the number already exists, it is recursively increased. + + >>> from sympy.core.symbol import uniquely_named_symbol, Symbol + >>> uniquely_named_symbol('x', Symbol('x')) + x0 + >>> uniquely_named_symbol('x', (Symbol('x'), Symbol('x0'))) + x1 + >>> uniquely_named_symbol('x0', (Symbol('x1'), Symbol('x0'))) + x2 + + Name generation can be controlled by passing *modify* parameter. + + >>> from sympy.abc import x + >>> uniquely_named_symbol('x', x, modify=lambda s: 2*s) + xx + + """ + def numbered_string_incr(s, start=0): + if not s: + return str(start) + i = len(s) - 1 + while i != -1: + if not s[i].isdigit(): + break + i -= 1 + n = str(int(s[i + 1:] or start - 1) + 1) + return s[:i + 1] + n + + default = None + if is_sequence(xname): + xname, default = xname + x = compare(xname) + if not exprs: + return _symbol(x, default, **assumptions) + if not is_sequence(exprs): + exprs = [exprs] + names = set().union( + [i.name for e in exprs for i in e.atoms(Symbol)] + + [i.func.name for e in exprs for i in e.atoms(AppliedUndef)]) + if modify is None: + modify = numbered_string_incr + while any(x == compare(s) for s in names): + x = modify(x) + return _symbol(x, default, **assumptions) +_uniquely_named_symbol = uniquely_named_symbol + + +# XXX: We need type: ignore below because Expr and Boolean are incompatible as +# superclasses. Really Symbol should not be a subclass of Boolean. + + +class Symbol(AtomicExpr, Boolean): # type: ignore + """ + Symbol class is used to create symbolic variables. + + Explanation + =========== + + Symbolic variables are placeholders for mathematical symbols that can represent numbers, constants, or any other mathematical entities and can be used in mathematical expressions and to perform symbolic computations. + + Assumptions: + + commutative = True + positive = True + real = True + imaginary = True + complex = True + complete list of more assumptions- :ref:`predicates` + + You can override the default assumptions in the constructor. + + Examples + ======== + + >>> from sympy import Symbol + >>> x = Symbol("x", positive=True) + >>> x.is_positive + True + >>> x.is_negative + False + + passing in greek letters: + + >>> from sympy import Symbol + >>> alpha = Symbol('alpha') + >>> alpha #doctest: +SKIP + α + + Trailing digits are automatically treated like subscripts of what precedes them in the name. + General format to add subscript to a symbol : + `` = Symbol('_')`` + + >>> from sympy import Symbol + >>> alpha_i = Symbol('alpha_i') + >>> alpha_i #doctest: +SKIP + αᵢ + + Parameters + ========== + + AtomicExpr: variable name + Boolean: Assumption with a boolean value(True or False) + """ + + is_comparable = False + + __slots__ = ('name', '_assumptions_orig', '_assumptions0') + + name: str + + is_Symbol = True + is_symbol = True + + @property + def kind(self): + if self.is_commutative: + return NumberKind + return UndefinedKind + + @property + def _diff_wrt(self): + """Allow derivatives wrt Symbols. + + Examples + ======== + + >>> from sympy import Symbol + >>> x = Symbol('x') + >>> x._diff_wrt + True + """ + return True + + @staticmethod + def _sanitize(assumptions, obj=None): + """Remove None, convert values to bool, check commutativity *in place*. + """ + + # be strict about commutativity: cannot be None + is_commutative = fuzzy_bool(assumptions.get('commutative', True)) + if is_commutative is None: + whose = '%s ' % obj.__name__ if obj else '' + raise ValueError( + '%scommutativity must be True or False.' % whose) + + # sanitize other assumptions so 1 -> True and 0 -> False + for key in list(assumptions.keys()): + v = assumptions[key] + if v is None: + assumptions.pop(key) + continue + assumptions[key] = bool(v) + + def _merge(self, assumptions): + base = self.assumptions0 + for k in set(assumptions) & set(base): + if assumptions[k] != base[k]: + raise ValueError(filldedent(''' + non-matching assumptions for %s: existing value + is %s and new value is %s''' % ( + k, base[k], assumptions[k]))) + base.update(assumptions) + return base + + def __new__(cls, name, **assumptions): + """Symbols are identified by name and assumptions:: + + >>> from sympy import Symbol + >>> Symbol("x") == Symbol("x") + True + >>> Symbol("x", real=True) == Symbol("x", real=False) + False + + """ + cls._sanitize(assumptions, cls) + return Symbol.__xnew_cached_(cls, name, **assumptions) + + + @staticmethod + @cacheit + def _canonical_assumptions(**assumptions): + # This is retained purely so that srepr can include commutative=True if + # that was explicitly specified but not if it was not. Ideally srepr + # should not distinguish these cases because the symbols otherwise + # compare equal and are considered equivalent. + # + # See https://github.com/sympy/sympy/issues/8873 + # + assumptions_orig = assumptions.copy() + + # The only assumption that is assumed by default is commutative=True: + assumptions.setdefault('commutative', True) + + assumptions_kb = StdFactKB(assumptions) + assumptions0 = dict(assumptions_kb) + + return assumptions_kb, assumptions_orig, assumptions0 + + @staticmethod + def __xnew__(cls, name, **assumptions): # never cached (e.g. dummy) + if not isinstance(name, str): + raise TypeError("name should be a string, not %s" % repr(type(name))) + + + obj = Expr.__new__(cls) + obj.name = name + + assumptions_kb, assumptions_orig, assumptions0 = Symbol._canonical_assumptions(**assumptions) + + obj._assumptions = assumptions_kb + obj._assumptions_orig = assumptions_orig + obj._assumptions0 = tuple(sorted(assumptions0.items())) + + # The three assumptions dicts are all a little different: + # + # >>> from sympy import Symbol + # >>> x = Symbol('x', finite=True) + # >>> x.is_positive # query an assumption + # >>> x._assumptions + # {'finite': True, 'infinite': False, 'commutative': True, 'positive': None} + # >>> x._assumptions0 + # {'finite': True, 'infinite': False, 'commutative': True} + # >>> x._assumptions_orig + # {'finite': True} + # + # Two symbols with the same name are equal if their _assumptions0 are + # the same. Arguably it should be _assumptions_orig that is being + # compared because that is more transparent to the user (it is + # what was passed to the constructor modulo changes made by _sanitize). + + return obj + + @staticmethod + @cacheit + def __xnew_cached_(cls, name, **assumptions): # symbols are always cached + return Symbol.__xnew__(cls, name, **assumptions) + + def __getnewargs_ex__(self): + return ((self.name,), self._assumptions_orig) + + # NOTE: __setstate__ is not needed for pickles created by __getnewargs_ex__ + # but was used before Symbol was changed to use __getnewargs_ex__ in v1.9. + # Pickles created in previous SymPy versions will still need __setstate__ + # so that they can be unpickled in SymPy > v1.9. + + def __setstate__(self, state): + for name, value in state.items(): + setattr(self, name, value) + + def _hashable_content(self): + return (self.name,) + self._assumptions0 + + def _eval_subs(self, old, new): + if old.is_Pow: + from sympy.core.power import Pow + return Pow(self, S.One, evaluate=False)._eval_subs(old, new) + + def _eval_refine(self, assumptions): + return self + + @property + def assumptions0(self): + return dict(self._assumptions0) + + @cacheit + def sort_key(self, order=None): + return self.class_key(), (1, (self.name,)), S.One.sort_key(), S.One + + def as_dummy(self): + # only put commutativity in explicitly if it is False + return Dummy(self.name) if self.is_commutative is not False \ + else Dummy(self.name, commutative=self.is_commutative) + + def as_real_imag(self, deep=True, **hints): + if hints.get('ignore') == self: + return None + else: + from sympy.functions.elementary.complexes import im, re + return (re(self), im(self)) + + def is_constant(self, *wrt, **flags): + if not wrt: + return False + return self not in wrt + + @property + def free_symbols(self): + return {self} + + binary_symbols = free_symbols # in this case, not always + + def as_set(self): + return S.UniversalSet + + +class Dummy(Symbol): + """Dummy symbols are each unique, even if they have the same name: + + Examples + ======== + + >>> from sympy import Dummy + >>> Dummy("x") == Dummy("x") + False + + If a name is not supplied then a string value of an internal count will be + used. This is useful when a temporary variable is needed and the name + of the variable used in the expression is not important. + + >>> Dummy() #doctest: +SKIP + _Dummy_10 + + """ + + # In the rare event that a Dummy object needs to be recreated, both the + # `name` and `dummy_index` should be passed. This is used by `srepr` for + # example: + # >>> d1 = Dummy() + # >>> d2 = eval(srepr(d1)) + # >>> d2 == d1 + # True + # + # If a new session is started between `srepr` and `eval`, there is a very + # small chance that `d2` will be equal to a previously-created Dummy. + + _count = 0 + _prng = random.Random() + _base_dummy_index = _prng.randint(10**6, 9*10**6) + + __slots__ = ('dummy_index',) + + is_Dummy = True + + def __new__(cls, name=None, dummy_index=None, **assumptions): + if dummy_index is not None: + assert name is not None, "If you specify a dummy_index, you must also provide a name" + + if name is None: + name = "Dummy_" + str(Dummy._count) + + if dummy_index is None: + dummy_index = Dummy._base_dummy_index + Dummy._count + Dummy._count += 1 + + cls._sanitize(assumptions, cls) + obj = Symbol.__xnew__(cls, name, **assumptions) + + obj.dummy_index = dummy_index + + return obj + + def __getnewargs_ex__(self): + return ((self.name, self.dummy_index), self._assumptions_orig) + + @cacheit + def sort_key(self, order=None): + return self.class_key(), ( + 2, (self.name, self.dummy_index)), S.One.sort_key(), S.One + + def _hashable_content(self): + return Symbol._hashable_content(self) + (self.dummy_index,) + + +class Wild(Symbol): + """ + A Wild symbol matches anything, or anything + without whatever is explicitly excluded. + + Parameters + ========== + + name : str + Name of the Wild instance. + + exclude : iterable, optional + Instances in ``exclude`` will not be matched. + + properties : iterable of functions, optional + Functions, each taking an expressions as input + and returns a ``bool``. All functions in ``properties`` + need to return ``True`` in order for the Wild instance + to match the expression. + + Examples + ======== + + >>> from sympy import Wild, WildFunction, cos, pi + >>> from sympy.abc import x, y, z + >>> a = Wild('a') + >>> x.match(a) + {a_: x} + >>> pi.match(a) + {a_: pi} + >>> (3*x**2).match(a*x) + {a_: 3*x} + >>> cos(x).match(a) + {a_: cos(x)} + >>> b = Wild('b', exclude=[x]) + >>> (3*x**2).match(b*x) + >>> b.match(a) + {a_: b_} + >>> A = WildFunction('A') + >>> A.match(a) + {a_: A_} + + Tips + ==== + + When using Wild, be sure to use the exclude + keyword to make the pattern more precise. + Without the exclude pattern, you may get matches + that are technically correct, but not what you + wanted. For example, using the above without + exclude: + + >>> from sympy import symbols + >>> a, b = symbols('a b', cls=Wild) + >>> (2 + 3*y).match(a*x + b*y) + {a_: 2/x, b_: 3} + + This is technically correct, because + (2/x)*x + 3*y == 2 + 3*y, but you probably + wanted it to not match at all. The issue is that + you really did not want a and b to include x and y, + and the exclude parameter lets you specify exactly + this. With the exclude parameter, the pattern will + not match. + + >>> a = Wild('a', exclude=[x, y]) + >>> b = Wild('b', exclude=[x, y]) + >>> (2 + 3*y).match(a*x + b*y) + + Exclude also helps remove ambiguity from matches. + + >>> E = 2*x**3*y*z + >>> a, b = symbols('a b', cls=Wild) + >>> E.match(a*b) + {a_: 2*y*z, b_: x**3} + >>> a = Wild('a', exclude=[x, y]) + >>> E.match(a*b) + {a_: z, b_: 2*x**3*y} + >>> a = Wild('a', exclude=[x, y, z]) + >>> E.match(a*b) + {a_: 2, b_: x**3*y*z} + + Wild also accepts a ``properties`` parameter: + + >>> a = Wild('a', properties=[lambda k: k.is_Integer]) + >>> E.match(a*b) + {a_: 2, b_: x**3*y*z} + + """ + is_Wild = True + + __slots__ = ('exclude', 'properties') + + def __new__(cls, name, exclude=(), properties=(), **assumptions): + exclude = tuple([sympify(x) for x in exclude]) + properties = tuple(properties) + cls._sanitize(assumptions, cls) + return Wild.__xnew__(cls, name, exclude, properties, **assumptions) + + def __getnewargs__(self): + return (self.name, self.exclude, self.properties) + + @staticmethod + @cacheit + def __xnew__(cls, name, exclude, properties, **assumptions): + obj = Symbol.__xnew__(cls, name, **assumptions) + obj.exclude = exclude + obj.properties = properties + return obj + + def _hashable_content(self): + return super()._hashable_content() + (self.exclude, self.properties) + + # TODO add check against another Wild + def matches(self, expr, repl_dict=None, old=False): + if any(expr.has(x) for x in self.exclude): + return None + if not all(f(expr) for f in self.properties): + return None + if repl_dict is None: + repl_dict = {} + else: + repl_dict = repl_dict.copy() + repl_dict[self] = expr + return repl_dict + + +_range = _re.compile('([0-9]*:[0-9]+|[a-zA-Z]?:[a-zA-Z])') + + +def symbols(names, *, cls=Symbol, **args) -> Any: + r""" + Transform strings into instances of :class:`Symbol` class. + + :func:`symbols` function returns a sequence of symbols with names taken + from ``names`` argument, which can be a comma or whitespace delimited + string, or a sequence of strings:: + + >>> from sympy import symbols, Function + + >>> x, y, z = symbols('x,y,z') + >>> a, b, c = symbols('a b c') + + The type of output is dependent on the properties of input arguments:: + + >>> symbols('x') + x + >>> symbols('x,') + (x,) + >>> symbols('x,y') + (x, y) + >>> symbols(('a', 'b', 'c')) + (a, b, c) + >>> symbols(['a', 'b', 'c']) + [a, b, c] + >>> symbols({'a', 'b', 'c'}) + {a, b, c} + + If an iterable container is needed for a single symbol, set the ``seq`` + argument to ``True`` or terminate the symbol name with a comma:: + + >>> symbols('x', seq=True) + (x,) + + To reduce typing, range syntax is supported to create indexed symbols. + Ranges are indicated by a colon and the type of range is determined by + the character to the right of the colon. If the character is a digit + then all contiguous digits to the left are taken as the nonnegative + starting value (or 0 if there is no digit left of the colon) and all + contiguous digits to the right are taken as 1 greater than the ending + value:: + + >>> symbols('x:10') + (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) + + >>> symbols('x5:10') + (x5, x6, x7, x8, x9) + >>> symbols('x5(:2)') + (x50, x51) + + >>> symbols('x5:10,y:5') + (x5, x6, x7, x8, x9, y0, y1, y2, y3, y4) + + >>> symbols(('x5:10', 'y:5')) + ((x5, x6, x7, x8, x9), (y0, y1, y2, y3, y4)) + + If the character to the right of the colon is a letter, then the single + letter to the left (or 'a' if there is none) is taken as the start + and all characters in the lexicographic range *through* the letter to + the right are used as the range:: + + >>> symbols('x:z') + (x, y, z) + >>> symbols('x:c') # null range + () + >>> symbols('x(:c)') + (xa, xb, xc) + + >>> symbols(':c') + (a, b, c) + + >>> symbols('a:d, x:z') + (a, b, c, d, x, y, z) + + >>> symbols(('a:d', 'x:z')) + ((a, b, c, d), (x, y, z)) + + Multiple ranges are supported; contiguous numerical ranges should be + separated by parentheses to disambiguate the ending number of one + range from the starting number of the next:: + + >>> symbols('x:2(1:3)') + (x01, x02, x11, x12) + >>> symbols(':3:2') # parsing is from left to right + (00, 01, 10, 11, 20, 21) + + Only one pair of parentheses surrounding ranges are removed, so to + include parentheses around ranges, double them. And to include spaces, + commas, or colons, escape them with a backslash:: + + >>> symbols('x((a:b))') + (x(a), x(b)) + >>> symbols(r'x(:1\,:2)') # or r'x((:1)\,(:2))' + (x(0,0), x(0,1)) + + All newly created symbols have assumptions set according to ``args``:: + + >>> a = symbols('a', integer=True) + >>> a.is_integer + True + + >>> x, y, z = symbols('x,y,z', real=True) + >>> x.is_real and y.is_real and z.is_real + True + + Despite its name, :func:`symbols` can create symbol-like objects like + instances of Function or Wild classes. To achieve this, set ``cls`` + keyword argument to the desired type:: + + >>> symbols('f,g,h', cls=Function) + (f, g, h) + + >>> type(_[0]) + + + """ + result = [] + + if isinstance(names, str): + marker = 0 + splitters = r'\,', r'\:', r'\ ' + literals: list[tuple[str, str]] = [] + for splitter in splitters: + if splitter in names: + while chr(marker) in names: + marker += 1 + lit_char = chr(marker) + marker += 1 + names = names.replace(splitter, lit_char) + literals.append((lit_char, splitter[1:])) + def literal(s): + if literals: + for c, l in literals: + s = s.replace(c, l) + return s + + names = names.strip() + as_seq = names.endswith(',') + if as_seq: + names = names[:-1].rstrip() + if not names: + raise ValueError('no symbols given') + + # split on commas + names = [n.strip() for n in names.split(',')] + if not all(n for n in names): + raise ValueError('missing symbol between commas') + # split on spaces + for i in range(len(names) - 1, -1, -1): + names[i: i + 1] = names[i].split() + + seq = args.pop('seq', as_seq) + + for name in names: + if not name: + raise ValueError('missing symbol') + + if ':' not in name: + symbol = cls(literal(name), **args) + result.append(symbol) + continue + + split: list[str] = _range.split(name) + split_list: list[list[str]] = [] + # remove 1 layer of bounding parentheses around ranges + for i in range(len(split) - 1): + if i and ':' in split[i] and split[i] != ':' and \ + split[i - 1].endswith('(') and \ + split[i + 1].startswith(')'): + split[i - 1] = split[i - 1][:-1] + split[i + 1] = split[i + 1][1:] + for s in split: + if ':' in s: + if s.endswith(':'): + raise ValueError('missing end range') + a, b = s.split(':') + if b[-1] in string.digits: + a_i = 0 if not a else int(a) + b_i = int(b) + split_list.append([str(c) for c in range(a_i, b_i)]) + else: + a = a or 'a' + split_list.append([string.ascii_letters[c] for c in range( + string.ascii_letters.index(a), + string.ascii_letters.index(b) + 1)]) # inclusive + if not split_list[-1]: + break + else: + split_list.append([s]) + else: + seq = True + if len(split_list) == 1: + names = split_list[0] + else: + names = [''.join(s) for s in product(*split_list)] + if literals: + result.extend([cls(literal(s), **args) for s in names]) + else: + result.extend([cls(s, **args) for s in names]) + + if not seq and len(result) <= 1: + if not result: + return () + return result[0] + + return tuple(result) + else: + for name in names: + result.append(symbols(name, cls=cls, **args)) + + return type(names)(result) + + +def var(names, **args): + """ + Create symbols and inject them into the global namespace. + + Explanation + =========== + + This calls :func:`symbols` with the same arguments and puts the results + into the *global* namespace. It's recommended not to use :func:`var` in + library code, where :func:`symbols` has to be used:: + + Examples + ======== + + >>> from sympy import var + + >>> var('x') + x + >>> x # noqa: F821 + x + + >>> var('a,ab,abc') + (a, ab, abc) + >>> abc # noqa: F821 + abc + + >>> var('x,y', real=True) + (x, y) + >>> x.is_real and y.is_real # noqa: F821 + True + + See :func:`symbols` documentation for more details on what kinds of + arguments can be passed to :func:`var`. + + """ + def traverse(symbols, frame): + """Recursively inject symbols to the global namespace. """ + for symbol in symbols: + if isinstance(symbol, Basic): + frame.f_globals[symbol.name] = symbol + elif isinstance(symbol, FunctionClass): + frame.f_globals[symbol.__name__] = symbol + else: + traverse(symbol, frame) + + from inspect import currentframe + frame = currentframe().f_back + + try: + syms = symbols(names, **args) + + if syms is not None: + if isinstance(syms, Basic): + frame.f_globals[syms.name] = syms + elif isinstance(syms, FunctionClass): + frame.f_globals[syms.__name__] = syms + else: + traverse(syms, frame) + finally: + del frame # break cyclic dependencies as stated in inspect docs + + return syms + +def disambiguate(*iter): + """ + Return a Tuple containing the passed expressions with symbols + that appear the same when printed replaced with numerically + subscripted symbols, and all Dummy symbols replaced with Symbols. + + Parameters + ========== + + iter: list of symbols or expressions. + + Examples + ======== + + >>> from sympy.core.symbol import disambiguate + >>> from sympy import Dummy, Symbol, Tuple + >>> from sympy.abc import y + + >>> tup = Symbol('_x'), Dummy('x'), Dummy('x') + >>> disambiguate(*tup) + (x_2, x, x_1) + + >>> eqs = Tuple(Symbol('x')/y, Dummy('x')/y) + >>> disambiguate(*eqs) + (x_1/y, x/y) + + >>> ix = Symbol('x', integer=True) + >>> vx = Symbol('x') + >>> disambiguate(vx + ix) + (x + x_1,) + + To make your own mapping of symbols to use, pass only the free symbols + of the expressions and create a dictionary: + + >>> free = eqs.free_symbols + >>> mapping = dict(zip(free, disambiguate(*free))) + >>> eqs.xreplace(mapping) + (x_1/y, x/y) + + """ + new_iter = Tuple(*iter) + key = lambda x:tuple(sorted(x.assumptions0.items())) + syms = ordered(new_iter.free_symbols, keys=key) + mapping = {} + for s in syms: + mapping.setdefault(str(s).lstrip('_'), []).append(s) + reps = {} + for k in mapping: + # the first or only symbol doesn't get subscripted but make + # sure that it's a Symbol, not a Dummy + mapk0 = Symbol("%s" % (k), **mapping[k][0].assumptions0) + if mapping[k][0] != mapk0: + reps[mapping[k][0]] = mapk0 + # the others get subscripts (and are made into Symbols) + skip = 0 + for i in range(1, len(mapping[k])): + while True: + name = "%s_%i" % (k, i + skip) + if name not in mapping: + break + skip += 1 + ki = mapping[k][i] + reps[ki] = Symbol(name, **ki.assumptions0) + return new_iter.xreplace(reps) diff --git a/venv/lib/python3.11/site-packages/sympy/core/sympify.py b/venv/lib/python3.11/site-packages/sympy/core/sympify.py new file mode 100644 index 0000000000000000000000000000000000000000..df30fbb85d5f160540312de4eef1d0e6702fc974 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/sympify.py @@ -0,0 +1,646 @@ +"""sympify -- convert objects SymPy internal format""" + +from __future__ import annotations + +from typing import Any, Callable, overload, TYPE_CHECKING, TypeVar + +import mpmath.libmp as mlib + +from inspect import getmro +import string +from sympy.core.random import choice + +from .parameters import global_parameters + +from sympy.utilities.iterables import iterable + + +if TYPE_CHECKING: + + from sympy.core.basic import Basic + from sympy.core.expr import Expr + from sympy.core.numbers import Integer, Float + + Tbasic = TypeVar('Tbasic', bound=Basic) + + +class SympifyError(ValueError): + def __init__(self, expr, base_exc=None): + self.expr = expr + self.base_exc = base_exc + + def __str__(self): + if self.base_exc is None: + return "SympifyError: %r" % (self.expr,) + + return ("Sympify of expression '%s' failed, because of exception being " + "raised:\n%s: %s" % (self.expr, self.base_exc.__class__.__name__, + str(self.base_exc))) + + +converter: dict[type[Any], Callable[[Any], Basic]] = {} + +#holds the conversions defined in SymPy itself, i.e. non-user defined conversions +_sympy_converter: dict[type[Any], Callable[[Any], Basic]] = {} + +#alias for clearer use in the library +_external_converter = converter + +class CantSympify: + """ + Mix in this trait to a class to disallow sympification of its instances. + + Examples + ======== + + >>> from sympy import sympify + >>> from sympy.core.sympify import CantSympify + + >>> class Something(dict): + ... pass + ... + >>> sympify(Something()) + {} + + >>> class Something(dict, CantSympify): + ... pass + ... + >>> sympify(Something()) + Traceback (most recent call last): + ... + SympifyError: SympifyError: {} + + """ + + __slots__ = () + + +def _is_numpy_instance(a): + """ + Checks if an object is an instance of a type from the numpy module. + """ + # This check avoids unnecessarily importing NumPy. We check the whole + # __mro__ in case any base type is a numpy type. + return any(type_.__module__ == 'numpy' + for type_ in type(a).__mro__) + + +def _convert_numpy_types(a, **sympify_args): + """ + Converts a numpy datatype input to an appropriate SymPy type. + """ + import numpy as np + if not isinstance(a, np.floating): + if np.iscomplex(a): + return _sympy_converter[complex](a.item()) + else: + return sympify(a.item(), **sympify_args) + else: + from .numbers import Float + prec = np.finfo(a).nmant + 1 + # E.g. double precision means prec=53 but nmant=52 + # Leading bit of mantissa is always 1, so is not stored + if np.isposinf(a): + return Float('inf') + elif np.isneginf(a): + return Float('-inf') + else: + p, q = a.as_integer_ratio() + a = mlib.from_rational(p, q, prec) + return Float(a, precision=prec) + + +@overload +def sympify(a: int, *, strict: bool = False) -> Integer: ... # type: ignore +@overload +def sympify(a: float, *, strict: bool = False) -> Float: ... +@overload +def sympify(a: Expr | complex, *, strict: bool = False) -> Expr: ... +@overload +def sympify(a: Tbasic, *, strict: bool = False) -> Tbasic: ... +@overload +def sympify(a: Any, *, strict: bool = False) -> Basic: ... + +def sympify(a, locals=None, convert_xor=True, strict=False, rational=False, + evaluate=None): + """ + Converts an arbitrary expression to a type that can be used inside SymPy. + + Explanation + =========== + + It will convert Python ints into instances of :class:`~.Integer`, floats + into instances of :class:`~.Float`, etc. It is also able to coerce + symbolic expressions which inherit from :class:`~.Basic`. This can be + useful in cooperation with SAGE. + + .. warning:: + Note that this function uses ``eval``, and thus shouldn't be used on + unsanitized input. + + If the argument is already a type that SymPy understands, it will do + nothing but return that value. This can be used at the beginning of a + function to ensure you are working with the correct type. + + Examples + ======== + + >>> from sympy import sympify + + >>> sympify(2).is_integer + True + >>> sympify(2).is_real + True + + >>> sympify(2.0).is_real + True + >>> sympify("2.0").is_real + True + >>> sympify("2e-45").is_real + True + + If the expression could not be converted, a SympifyError is raised. + + >>> sympify("x***2") + Traceback (most recent call last): + ... + SympifyError: SympifyError: "could not parse 'x***2'" + + When attempting to parse non-Python syntax using ``sympify``, it raises a + ``SympifyError``: + + >>> sympify("2x+1") + Traceback (most recent call last): + ... + SympifyError: Sympify of expression 'could not parse '2x+1'' failed + + To parse non-Python syntax, use ``parse_expr`` from ``sympy.parsing.sympy_parser``. + + >>> from sympy.parsing.sympy_parser import parse_expr + >>> parse_expr("2x+1", transformations="all") + 2*x + 1 + + For more details about ``transformations``: see :func:`~sympy.parsing.sympy_parser.parse_expr` + + Locals + ------ + + The sympification happens with access to everything that is loaded + by ``from sympy import *``; anything used in a string that is not + defined by that import will be converted to a symbol. In the following, + the ``bitcount`` function is treated as a symbol and the ``O`` is + interpreted as the :class:`~.Order` object (used with series) and it raises + an error when used improperly: + + >>> s = 'bitcount(42)' + >>> sympify(s) + bitcount(42) + >>> sympify("O(x)") + O(x) + >>> sympify("O + 1") + Traceback (most recent call last): + ... + TypeError: unbound method... + + In order to have ``bitcount`` be recognized it can be imported into a + namespace dictionary and passed as locals: + + >>> ns = {} + >>> exec('from sympy.core.evalf import bitcount', ns) + >>> sympify(s, locals=ns) + 6 + + In order to have the ``O`` interpreted as a Symbol, identify it as such + in the namespace dictionary. This can be done in a variety of ways; all + three of the following are possibilities: + + >>> from sympy import Symbol + >>> ns["O"] = Symbol("O") # method 1 + >>> exec('from sympy.abc import O', ns) # method 2 + >>> ns.update(dict(O=Symbol("O"))) # method 3 + >>> sympify("O + 1", locals=ns) + O + 1 + + If you want *all* single-letter and Greek-letter variables to be symbols + then you can use the clashing-symbols dictionaries that have been defined + there as private variables: ``_clash1`` (single-letter variables), + ``_clash2`` (the multi-letter Greek names) or ``_clash`` (both single and + multi-letter names that are defined in ``abc``). + + >>> from sympy.abc import _clash1 + >>> set(_clash1) # if this fails, see issue #23903 + {'E', 'I', 'N', 'O', 'Q', 'S'} + >>> sympify('I & Q', _clash1) + I & Q + + Strict + ------ + + If the option ``strict`` is set to ``True``, only the types for which an + explicit conversion has been defined are converted. In the other + cases, a SympifyError is raised. + + >>> print(sympify(None)) + None + >>> sympify(None, strict=True) + Traceback (most recent call last): + ... + SympifyError: SympifyError: None + + .. deprecated:: 1.6 + + ``sympify(obj)`` automatically falls back to ``str(obj)`` when all + other conversion methods fail, but this is deprecated. ``strict=True`` + will disable this deprecated behavior. See + :ref:`deprecated-sympify-string-fallback`. + + Evaluation + ---------- + + If the option ``evaluate`` is set to ``False``, then arithmetic and + operators will be converted into their SymPy equivalents and the + ``evaluate=False`` option will be added. Nested ``Add`` or ``Mul`` will + be denested first. This is done via an AST transformation that replaces + operators with their SymPy equivalents, so if an operand redefines any + of those operations, the redefined operators will not be used. If + argument a is not a string, the mathematical expression is evaluated + before being passed to sympify, so adding ``evaluate=False`` will still + return the evaluated result of expression. + + >>> sympify('2**2 / 3 + 5') + 19/3 + >>> sympify('2**2 / 3 + 5', evaluate=False) + 2**2/3 + 5 + >>> sympify('4/2+7', evaluate=True) + 9 + >>> sympify('4/2+7', evaluate=False) + 4/2 + 7 + >>> sympify(4/2+7, evaluate=False) + 9.00000000000000 + + Extending + --------- + + To extend ``sympify`` to convert custom objects (not derived from ``Basic``), + just define a ``_sympy_`` method to your class. You can do that even to + classes that you do not own by subclassing or adding the method at runtime. + + >>> from sympy import Matrix + >>> class MyList1(object): + ... def __iter__(self): + ... yield 1 + ... yield 2 + ... return + ... def __getitem__(self, i): return list(self)[i] + ... def _sympy_(self): return Matrix(self) + >>> sympify(MyList1()) + Matrix([ + [1], + [2]]) + + If you do not have control over the class definition you could also use the + ``converter`` global dictionary. The key is the class and the value is a + function that takes a single argument and returns the desired SymPy + object, e.g. ``converter[MyList] = lambda x: Matrix(x)``. + + >>> class MyList2(object): # XXX Do not do this if you control the class! + ... def __iter__(self): # Use _sympy_! + ... yield 1 + ... yield 2 + ... return + ... def __getitem__(self, i): return list(self)[i] + >>> from sympy.core.sympify import converter + >>> converter[MyList2] = lambda x: Matrix(x) + >>> sympify(MyList2()) + Matrix([ + [1], + [2]]) + + Notes + ===== + + The keywords ``rational`` and ``convert_xor`` are only used + when the input is a string. + + convert_xor + ----------- + + >>> sympify('x^y',convert_xor=True) + x**y + >>> sympify('x^y',convert_xor=False) + x ^ y + + rational + -------- + + >>> sympify('0.1',rational=False) + 0.1 + >>> sympify('0.1',rational=True) + 1/10 + + Sometimes autosimplification during sympification results in expressions + that are very different in structure than what was entered. Until such + autosimplification is no longer done, the ``kernS`` function might be of + some use. In the example below you can see how an expression reduces to + $-1$ by autosimplification, but does not do so when ``kernS`` is used. + + >>> from sympy.core.sympify import kernS + >>> from sympy.abc import x + >>> -2*(-(-x + 1/x)/(x*(x - 1/x)**2) - 1/(x*(x - 1/x))) - 1 + -1 + >>> s = '-2*(-(-x + 1/x)/(x*(x - 1/x)**2) - 1/(x*(x - 1/x))) - 1' + >>> sympify(s) + -1 + >>> kernS(s) + -2*(-(-x + 1/x)/(x*(x - 1/x)**2) - 1/(x*(x - 1/x))) - 1 + + Parameters + ========== + + a : + - any object defined in SymPy + - standard numeric Python types: ``int``, ``long``, ``float``, ``Decimal`` + - strings (like ``"0.09"``, ``"2e-19"`` or ``'sin(x)'``) + - booleans, including ``None`` (will leave ``None`` unchanged) + - dicts, lists, sets or tuples containing any of the above + + convert_xor : bool, optional + If true, treats ``^`` as exponentiation. + If False, treats ``^`` as XOR itself. + Used only when input is a string. + + locals : any object defined in SymPy, optional + In order to have strings be recognized it can be imported + into a namespace dictionary and passed as locals. + + strict : bool, optional + If the option strict is set to ``True``, only the types for which + an explicit conversion has been defined are converted. In the + other cases, a SympifyError is raised. + + rational : bool, optional + If ``True``, converts floats into :class:`~.Rational`. + If ``False``, it lets floats remain as it is. + Used only when input is a string. + + evaluate : bool, optional + If False, then arithmetic and operators will be converted into + their SymPy equivalents. If True the expression will be evaluated + and the result will be returned. + + """ + # XXX: If a is a Basic subclass rather than instance (e.g. sin rather than + # sin(x)) then a.__sympy__ will be the property. Only on the instance will + # a.__sympy__ give the *value* of the property (True). Since sympify(sin) + # was used for a long time we allow it to pass. However if strict=True as + # is the case in internal calls to _sympify then we only allow + # is_sympy=True. + # + # https://github.com/sympy/sympy/issues/20124 + is_sympy = getattr(a, '__sympy__', None) + if is_sympy is True: + return a + elif is_sympy is not None: + if not strict: + return a + else: + raise SympifyError(a) + + if isinstance(a, CantSympify): + raise SympifyError(a) + + cls = getattr(a, "__class__", None) + + #Check if there exists a converter for any of the types in the mro + for superclass in getmro(cls): + #First check for user defined converters + conv = _external_converter.get(superclass) + if conv is None: + #if none exists, check for SymPy defined converters + conv = _sympy_converter.get(superclass) + if conv is not None: + return conv(a) + + if cls is type(None): + if strict: + raise SympifyError(a) + else: + return a + + if evaluate is None: + evaluate = global_parameters.evaluate + + # Support for basic numpy datatypes + if _is_numpy_instance(a): + import numpy as np + if np.isscalar(a): + return _convert_numpy_types(a, locals=locals, + convert_xor=convert_xor, strict=strict, rational=rational, + evaluate=evaluate) + + _sympy_ = getattr(a, "_sympy_", None) + if _sympy_ is not None: + return a._sympy_() + + if not strict: + # Put numpy array conversion _before_ float/int, see + # . + flat = getattr(a, "flat", None) + if flat is not None: + shape = getattr(a, "shape", None) + if shape is not None: + from sympy.tensor.array import Array + return Array(a.flat, a.shape) # works with e.g. NumPy arrays + + if not isinstance(a, str): + if _is_numpy_instance(a): + import numpy as np + assert not isinstance(a, np.number) + if isinstance(a, np.ndarray): + # Scalar arrays (those with zero dimensions) have sympify + # called on the scalar element. + if a.ndim == 0: + try: + return sympify(a.item(), + locals=locals, + convert_xor=convert_xor, + strict=strict, + rational=rational, + evaluate=evaluate) + except SympifyError: + pass + elif hasattr(a, '__float__'): + # float and int can coerce size-one numpy arrays to their lone + # element. See issue https://github.com/numpy/numpy/issues/10404. + return sympify(float(a)) + elif hasattr(a, '__int__'): + return sympify(int(a)) + + if strict: + raise SympifyError(a) + + if iterable(a): + try: + return type(a)([sympify(x, locals=locals, convert_xor=convert_xor, + rational=rational, evaluate=evaluate) for x in a]) + except TypeError: + # Not all iterables are rebuildable with their type. + pass + + if not isinstance(a, str): + raise SympifyError('cannot sympify object of type %r' % type(a)) + + from sympy.parsing.sympy_parser import (parse_expr, TokenError, + standard_transformations) + from sympy.parsing.sympy_parser import convert_xor as t_convert_xor + from sympy.parsing.sympy_parser import rationalize as t_rationalize + + transformations = standard_transformations + + if rational: + transformations += (t_rationalize,) + if convert_xor: + transformations += (t_convert_xor,) + + try: + a = a.replace('\n', '') + expr = parse_expr(a, local_dict=locals, transformations=transformations, evaluate=evaluate) + except (TokenError, SyntaxError) as exc: + raise SympifyError('could not parse %r' % a, exc) + + return expr + + +def _sympify(a): + """ + Short version of :func:`~.sympify` for internal usage for ``__add__`` and + ``__eq__`` methods where it is ok to allow some things (like Python + integers and floats) in the expression. This excludes things (like strings) + that are unwise to allow into such an expression. + + >>> from sympy import Integer + >>> Integer(1) == 1 + True + + >>> Integer(1) == '1' + False + + >>> from sympy.abc import x + >>> x + 1 + x + 1 + + >>> x + '1' + Traceback (most recent call last): + ... + TypeError: unsupported operand type(s) for +: 'Symbol' and 'str' + + see: sympify + + """ + return sympify(a, strict=True) + + +def kernS(s): + """Use a hack to try keep autosimplification from distributing a + a number into an Add; this modification does not + prevent the 2-arg Mul from becoming an Add, however. + + Examples + ======== + + >>> from sympy.core.sympify import kernS + >>> from sympy.abc import x, y + + The 2-arg Mul distributes a number (or minus sign) across the terms + of an expression, but kernS will prevent that: + + >>> 2*(x + y), -(x + 1) + (2*x + 2*y, -x - 1) + >>> kernS('2*(x + y)') + 2*(x + y) + >>> kernS('-(x + 1)') + -(x + 1) + + If use of the hack fails, the un-hacked string will be passed to sympify... + and you get what you get. + + XXX This hack should not be necessary once issue 4596 has been resolved. + """ + hit = False + quoted = '"' in s or "'" in s + if '(' in s and not quoted: + if s.count('(') != s.count(")"): + raise SympifyError('unmatched left parenthesis') + + # strip all space from s + s = ''.join(s.split()) + olds = s + # now use space to represent a symbol that + # will + # step 1. turn potential 2-arg Muls into 3-arg versions + # 1a. *( -> * *( + s = s.replace('*(', '* *(') + # 1b. close up exponentials + s = s.replace('** *', '**') + # 2. handle the implied multiplication of a negated + # parenthesized expression in two steps + # 2a: -(...) --> -( *(...) + target = '-( *(' + s = s.replace('-(', target) + # 2b: double the matching closing parenthesis + # -( *(...) --> -( *(...)) + i = nest = 0 + assert target.endswith('(') # assumption below + while True: + j = s.find(target, i) + if j == -1: + break + j += len(target) - 1 + for j in range(j, len(s)): + if s[j] == "(": + nest += 1 + elif s[j] == ")": + nest -= 1 + if nest == 0: + break + s = s[:j] + ")" + s[j:] + i = j + 2 # the first char after 2nd ) + if ' ' in s: + # get a unique kern + kern = '_' + while kern in s: + kern += choice(string.ascii_letters + string.digits) + s = s.replace(' ', kern) + hit = kern in s + else: + hit = False + + for i in range(2): + try: + expr = sympify(s) + break + except TypeError: # the kern might cause unknown errors... + if hit: + s = olds # maybe it didn't like the kern; use un-kerned s + hit = False + continue + expr = sympify(s) # let original error raise + + if not hit: + return expr + + from .symbol import Symbol + rep = {Symbol(kern): 1} + def _clear(expr): + if isinstance(expr, (list, tuple, set)): + return type(expr)([_clear(e) for e in expr]) + if hasattr(expr, 'subs'): + return expr.subs(rep, hack2=True) + return expr + expr = _clear(expr) + # hope that kern is not there anymore + return expr + + +# Avoid circular import +from .basic import Basic diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/core/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_args.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_args.py new file mode 100644 index 0000000000000000000000000000000000000000..75b326146e1cc645a27e26ba13d44c92d56d5efb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_args.py @@ -0,0 +1,5517 @@ +"""Test whether all elements of cls.args are instances of Basic. """ + +# NOTE: keep tests sorted by (module, class name) key. If a class can't +# be instantiated, add it here anyway with @SKIP("abstract class) (see +# e.g. Function). + +import os +import re +from pathlib import Path + +from sympy.assumptions.ask import Q +from sympy.core.basic import Basic +from sympy.core.function import (Function, Lambda) +from sympy.core.numbers import (Rational, oo, pi) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin + +from sympy.testing.pytest import SKIP, warns_deprecated_sympy + +a, b, c, x, y, z, s = symbols('a,b,c,x,y,z,s') + + +whitelist = [ + "sympy.assumptions.predicates", # tested by test_predicates() + "sympy.assumptions.relation.equality", # tested by test_predicates() +] + +def test_all_classes_are_tested(): + this = os.path.split(__file__)[0] + path = os.path.join(this, os.pardir, os.pardir) + sympy_path = os.path.abspath(path) + prefix = os.path.split(sympy_path)[0] + os.sep + + re_cls = re.compile(r"^class ([A-Za-z][A-Za-z0-9_]*)\s*\(", re.MULTILINE) + + modules = {} + + for root, dirs, files in os.walk(sympy_path): + module = root.replace(prefix, "").replace(os.sep, ".") + + for file in files: + if file.startswith(("_", "test_", "bench_")): + continue + if not file.endswith(".py"): + continue + + text = Path(os.path.join(root, file)).read_text(encoding='utf-8') + + submodule = module + '.' + file[:-3] + + if any(submodule.startswith(wpath) for wpath in whitelist): + continue + + names = re_cls.findall(text) + + if not names: + continue + + try: + mod = __import__(submodule, fromlist=names) + except ImportError: + continue + + def is_Basic(name): + cls = getattr(mod, name) + if hasattr(cls, '_sympy_deprecated_func'): + cls = cls._sympy_deprecated_func + if not isinstance(cls, type): + # check instance of singleton class with same name + cls = type(cls) + return issubclass(cls, Basic) + + names = list(filter(is_Basic, names)) + + if names: + modules[submodule] = names + + ns = globals() + failed = [] + + for module, names in modules.items(): + mod = module.replace('.', '__') + + for name in names: + test = 'test_' + mod + '__' + name + + if test not in ns: + failed.append(module + '.' + name) + + assert not failed, "Missing classes: %s. Please add tests for these to sympy/core/tests/test_args.py." % ", ".join(failed) + + +def _test_args(obj): + all_basic = all(isinstance(arg, Basic) for arg in obj.args) + # Ideally obj.func(*obj.args) would always recreate the object, but for + # now, we only require it for objects with non-empty .args + recreatable = not obj.args or obj.func(*obj.args) == obj + return all_basic and recreatable + + +def test_sympy__algebras__quaternion__Quaternion(): + from sympy.algebras.quaternion import Quaternion + assert _test_args(Quaternion(x, 1, 2, 3)) + + +def test_sympy__assumptions__assume__AppliedPredicate(): + from sympy.assumptions.assume import AppliedPredicate, Predicate + assert _test_args(AppliedPredicate(Predicate("test"), 2)) + assert _test_args(Q.is_true(True)) + +@SKIP("abstract class") +def test_sympy__assumptions__assume__Predicate(): + pass + +def test_predicates(): + predicates = [ + getattr(Q, attr) + for attr in Q.__class__.__dict__ + if not attr.startswith('__')] + for p in predicates: + assert _test_args(p) + +def test_sympy__assumptions__assume__UndefinedPredicate(): + from sympy.assumptions.assume import Predicate + assert _test_args(Predicate("test")) + +@SKIP('abstract class') +def test_sympy__assumptions__relation__binrel__BinaryRelation(): + pass + +def test_sympy__assumptions__relation__binrel__AppliedBinaryRelation(): + assert _test_args(Q.eq(1, 2)) + +def test_sympy__assumptions__wrapper__AssumptionsWrapper(): + from sympy.assumptions.wrapper import AssumptionsWrapper + assert _test_args(AssumptionsWrapper(x, Q.positive(x))) + +@SKIP("abstract Class") +def test_sympy__codegen__ast__CodegenAST(): + from sympy.codegen.ast import CodegenAST + assert _test_args(CodegenAST()) + +@SKIP("abstract Class") +def test_sympy__codegen__ast__AssignmentBase(): + from sympy.codegen.ast import AssignmentBase + assert _test_args(AssignmentBase(x, 1)) + +@SKIP("abstract Class") +def test_sympy__codegen__ast__AugmentedAssignment(): + from sympy.codegen.ast import AugmentedAssignment + assert _test_args(AugmentedAssignment(x, 1)) + +def test_sympy__codegen__ast__AddAugmentedAssignment(): + from sympy.codegen.ast import AddAugmentedAssignment + assert _test_args(AddAugmentedAssignment(x, 1)) + +def test_sympy__codegen__ast__SubAugmentedAssignment(): + from sympy.codegen.ast import SubAugmentedAssignment + assert _test_args(SubAugmentedAssignment(x, 1)) + +def test_sympy__codegen__ast__MulAugmentedAssignment(): + from sympy.codegen.ast import MulAugmentedAssignment + assert _test_args(MulAugmentedAssignment(x, 1)) + +def test_sympy__codegen__ast__DivAugmentedAssignment(): + from sympy.codegen.ast import DivAugmentedAssignment + assert _test_args(DivAugmentedAssignment(x, 1)) + +def test_sympy__codegen__ast__ModAugmentedAssignment(): + from sympy.codegen.ast import ModAugmentedAssignment + assert _test_args(ModAugmentedAssignment(x, 1)) + +def test_sympy__codegen__ast__CodeBlock(): + from sympy.codegen.ast import CodeBlock, Assignment + assert _test_args(CodeBlock(Assignment(x, 1), Assignment(y, 2))) + +def test_sympy__codegen__ast__For(): + from sympy.codegen.ast import For, CodeBlock, AddAugmentedAssignment + from sympy.sets import Range + assert _test_args(For(x, Range(10), CodeBlock(AddAugmentedAssignment(y, 1)))) + + +def test_sympy__codegen__ast__Token(): + from sympy.codegen.ast import Token + assert _test_args(Token()) + + +def test_sympy__codegen__ast__ContinueToken(): + from sympy.codegen.ast import ContinueToken + assert _test_args(ContinueToken()) + +def test_sympy__codegen__ast__BreakToken(): + from sympy.codegen.ast import BreakToken + assert _test_args(BreakToken()) + +def test_sympy__codegen__ast__NoneToken(): + from sympy.codegen.ast import NoneToken + assert _test_args(NoneToken()) + +def test_sympy__codegen__ast__String(): + from sympy.codegen.ast import String + assert _test_args(String('foobar')) + +def test_sympy__codegen__ast__QuotedString(): + from sympy.codegen.ast import QuotedString + assert _test_args(QuotedString('foobar')) + +def test_sympy__codegen__ast__Comment(): + from sympy.codegen.ast import Comment + assert _test_args(Comment('this is a comment')) + +def test_sympy__codegen__ast__Node(): + from sympy.codegen.ast import Node + assert _test_args(Node()) + assert _test_args(Node(attrs={1, 2, 3})) + + +def test_sympy__codegen__ast__Type(): + from sympy.codegen.ast import Type + assert _test_args(Type('float128')) + + +def test_sympy__codegen__ast__IntBaseType(): + from sympy.codegen.ast import IntBaseType + assert _test_args(IntBaseType('bigint')) + + +def test_sympy__codegen__ast___SizedIntType(): + from sympy.codegen.ast import _SizedIntType + assert _test_args(_SizedIntType('int128', 128)) + + +def test_sympy__codegen__ast__SignedIntType(): + from sympy.codegen.ast import SignedIntType + assert _test_args(SignedIntType('int128_with_sign', 128)) + + +def test_sympy__codegen__ast__UnsignedIntType(): + from sympy.codegen.ast import UnsignedIntType + assert _test_args(UnsignedIntType('unt128', 128)) + + +def test_sympy__codegen__ast__FloatBaseType(): + from sympy.codegen.ast import FloatBaseType + assert _test_args(FloatBaseType('positive_real')) + + +def test_sympy__codegen__ast__FloatType(): + from sympy.codegen.ast import FloatType + assert _test_args(FloatType('float242', 242, nmant=142, nexp=99)) + + +def test_sympy__codegen__ast__ComplexBaseType(): + from sympy.codegen.ast import ComplexBaseType + assert _test_args(ComplexBaseType('positive_cmplx')) + +def test_sympy__codegen__ast__ComplexType(): + from sympy.codegen.ast import ComplexType + assert _test_args(ComplexType('complex42', 42, nmant=15, nexp=5)) + + +def test_sympy__codegen__ast__Attribute(): + from sympy.codegen.ast import Attribute + assert _test_args(Attribute('noexcept')) + + +def test_sympy__codegen__ast__Variable(): + from sympy.codegen.ast import Variable, Type, value_const + assert _test_args(Variable(x)) + assert _test_args(Variable(y, Type('float32'), {value_const})) + assert _test_args(Variable(z, type=Type('float64'))) + + +def test_sympy__codegen__ast__Pointer(): + from sympy.codegen.ast import Pointer, Type, pointer_const + assert _test_args(Pointer(x)) + assert _test_args(Pointer(y, type=Type('float32'))) + assert _test_args(Pointer(z, Type('float64'), {pointer_const})) + + +def test_sympy__codegen__ast__Declaration(): + from sympy.codegen.ast import Declaration, Variable, Type + vx = Variable(x, type=Type('float')) + assert _test_args(Declaration(vx)) + + +def test_sympy__codegen__ast__While(): + from sympy.codegen.ast import While, AddAugmentedAssignment + assert _test_args(While(abs(x) < 1, [AddAugmentedAssignment(x, -1)])) + + +def test_sympy__codegen__ast__Scope(): + from sympy.codegen.ast import Scope, AddAugmentedAssignment + assert _test_args(Scope([AddAugmentedAssignment(x, -1)])) + + +def test_sympy__codegen__ast__Stream(): + from sympy.codegen.ast import Stream + assert _test_args(Stream('stdin')) + +def test_sympy__codegen__ast__Print(): + from sympy.codegen.ast import Print + assert _test_args(Print([x, y])) + assert _test_args(Print([x, y], "%d %d")) + + +def test_sympy__codegen__ast__FunctionPrototype(): + from sympy.codegen.ast import FunctionPrototype, real, Declaration, Variable + inp_x = Declaration(Variable(x, type=real)) + assert _test_args(FunctionPrototype(real, 'pwer', [inp_x])) + + +def test_sympy__codegen__ast__FunctionDefinition(): + from sympy.codegen.ast import FunctionDefinition, real, Declaration, Variable, Assignment + inp_x = Declaration(Variable(x, type=real)) + assert _test_args(FunctionDefinition(real, 'pwer', [inp_x], [Assignment(x, x**2)])) + + +def test_sympy__codegen__ast__Raise(): + from sympy.codegen.ast import Raise + assert _test_args(Raise(x)) + + +def test_sympy__codegen__ast__Return(): + from sympy.codegen.ast import Return + assert _test_args(Return(x)) + + +def test_sympy__codegen__ast__RuntimeError_(): + from sympy.codegen.ast import RuntimeError_ + assert _test_args(RuntimeError_('"message"')) + + +def test_sympy__codegen__ast__FunctionCall(): + from sympy.codegen.ast import FunctionCall + assert _test_args(FunctionCall('pwer', [x])) + + +def test_sympy__codegen__ast__Element(): + from sympy.codegen.ast import Element + assert _test_args(Element('x', range(3))) + + +def test_sympy__codegen__cnodes__CommaOperator(): + from sympy.codegen.cnodes import CommaOperator + assert _test_args(CommaOperator(1, 2)) + + +def test_sympy__codegen__cnodes__goto(): + from sympy.codegen.cnodes import goto + assert _test_args(goto('early_exit')) + + +def test_sympy__codegen__cnodes__Label(): + from sympy.codegen.cnodes import Label + assert _test_args(Label('early_exit')) + + +def test_sympy__codegen__cnodes__PreDecrement(): + from sympy.codegen.cnodes import PreDecrement + assert _test_args(PreDecrement(x)) + + +def test_sympy__codegen__cnodes__PostDecrement(): + from sympy.codegen.cnodes import PostDecrement + assert _test_args(PostDecrement(x)) + + +def test_sympy__codegen__cnodes__PreIncrement(): + from sympy.codegen.cnodes import PreIncrement + assert _test_args(PreIncrement(x)) + + +def test_sympy__codegen__cnodes__PostIncrement(): + from sympy.codegen.cnodes import PostIncrement + assert _test_args(PostIncrement(x)) + + +def test_sympy__codegen__cnodes__struct(): + from sympy.codegen.ast import real, Variable + from sympy.codegen.cnodes import struct + assert _test_args(struct(declarations=[ + Variable(x, type=real), + Variable(y, type=real) + ])) + + +def test_sympy__codegen__cnodes__union(): + from sympy.codegen.ast import float32, int32, Variable + from sympy.codegen.cnodes import union + assert _test_args(union(declarations=[ + Variable(x, type=float32), + Variable(y, type=int32) + ])) + + +def test_sympy__codegen__cxxnodes__using(): + from sympy.codegen.cxxnodes import using + assert _test_args(using('std::vector')) + assert _test_args(using('std::vector', 'vec')) + + +def test_sympy__codegen__fnodes__Program(): + from sympy.codegen.fnodes import Program + assert _test_args(Program('foobar', [])) + +def test_sympy__codegen__fnodes__Module(): + from sympy.codegen.fnodes import Module + assert _test_args(Module('foobar', [], [])) + + +def test_sympy__codegen__fnodes__Subroutine(): + from sympy.codegen.fnodes import Subroutine + x = symbols('x', real=True) + assert _test_args(Subroutine('foo', [x], [])) + + +def test_sympy__codegen__fnodes__GoTo(): + from sympy.codegen.fnodes import GoTo + assert _test_args(GoTo([10])) + assert _test_args(GoTo([10, 20], x > 1)) + + +def test_sympy__codegen__fnodes__FortranReturn(): + from sympy.codegen.fnodes import FortranReturn + assert _test_args(FortranReturn(10)) + + +def test_sympy__codegen__fnodes__Extent(): + from sympy.codegen.fnodes import Extent + assert _test_args(Extent()) + assert _test_args(Extent(None)) + assert _test_args(Extent(':')) + assert _test_args(Extent(-3, 4)) + assert _test_args(Extent(x, y)) + + +def test_sympy__codegen__fnodes__use_rename(): + from sympy.codegen.fnodes import use_rename + assert _test_args(use_rename('loc', 'glob')) + + +def test_sympy__codegen__fnodes__use(): + from sympy.codegen.fnodes import use + assert _test_args(use('modfoo', only='bar')) + + +def test_sympy__codegen__fnodes__SubroutineCall(): + from sympy.codegen.fnodes import SubroutineCall + assert _test_args(SubroutineCall('foo', ['bar', 'baz'])) + + +def test_sympy__codegen__fnodes__Do(): + from sympy.codegen.fnodes import Do + assert _test_args(Do([], 'i', 1, 42)) + + +def test_sympy__codegen__fnodes__ImpliedDoLoop(): + from sympy.codegen.fnodes import ImpliedDoLoop + assert _test_args(ImpliedDoLoop('i', 'i', 1, 42)) + + +def test_sympy__codegen__fnodes__ArrayConstructor(): + from sympy.codegen.fnodes import ArrayConstructor + assert _test_args(ArrayConstructor([1, 2, 3])) + from sympy.codegen.fnodes import ImpliedDoLoop + idl = ImpliedDoLoop('i', 'i', 1, 42) + assert _test_args(ArrayConstructor([1, idl, 3])) + + +def test_sympy__codegen__fnodes__sum_(): + from sympy.codegen.fnodes import sum_ + assert _test_args(sum_('arr')) + + +def test_sympy__codegen__fnodes__product_(): + from sympy.codegen.fnodes import product_ + assert _test_args(product_('arr')) + + +def test_sympy__codegen__numpy_nodes__logaddexp(): + from sympy.codegen.numpy_nodes import logaddexp + assert _test_args(logaddexp(x, y)) + + +def test_sympy__codegen__numpy_nodes__logaddexp2(): + from sympy.codegen.numpy_nodes import logaddexp2 + assert _test_args(logaddexp2(x, y)) + + +def test_sympy__codegen__numpy_nodes__amin(): + from sympy.codegen.numpy_nodes import amin + assert _test_args(amin(x)) + + +def test_sympy__codegen__numpy_nodes__amax(): + from sympy.codegen.numpy_nodes import amax + assert _test_args(amax(x)) + + +def test_sympy__codegen__numpy_nodes__minimum(): + from sympy.codegen.numpy_nodes import minimum + assert _test_args(minimum(x, y, z)) + + +def test_sympy__codegen__numpy_nodes__maximum(): + from sympy.codegen.numpy_nodes import maximum + assert _test_args(maximum(x, y, z)) + + +def test_sympy__codegen__pynodes__List(): + from sympy.codegen.pynodes import List + assert _test_args(List(1, 2, 3)) + + +def test_sympy__codegen__pynodes__NumExprEvaluate(): + from sympy.codegen.pynodes import NumExprEvaluate + assert _test_args(NumExprEvaluate(x)) + + +def test_sympy__codegen__scipy_nodes__cosm1(): + from sympy.codegen.scipy_nodes import cosm1 + assert _test_args(cosm1(x)) + + +def test_sympy__codegen__scipy_nodes__powm1(): + from sympy.codegen.scipy_nodes import powm1 + assert _test_args(powm1(x, y)) + + +def test_sympy__codegen__abstract_nodes__List(): + from sympy.codegen.abstract_nodes import List + assert _test_args(List(1, 2, 3)) + +def test_sympy__combinatorics__graycode__GrayCode(): + from sympy.combinatorics.graycode import GrayCode + # an integer is given and returned from GrayCode as the arg + assert _test_args(GrayCode(3, start='100')) + assert _test_args(GrayCode(3, rank=1)) + + +def test_sympy__combinatorics__permutations__Permutation(): + from sympy.combinatorics.permutations import Permutation + assert _test_args(Permutation([0, 1, 2, 3])) + +def test_sympy__combinatorics__permutations__AppliedPermutation(): + from sympy.combinatorics.permutations import Permutation + from sympy.combinatorics.permutations import AppliedPermutation + p = Permutation([0, 1, 2, 3]) + assert _test_args(AppliedPermutation(p, x)) + +def test_sympy__combinatorics__perm_groups__PermutationGroup(): + from sympy.combinatorics.permutations import Permutation + from sympy.combinatorics.perm_groups import PermutationGroup + assert _test_args(PermutationGroup([Permutation([0, 1])])) + + +def test_sympy__combinatorics__polyhedron__Polyhedron(): + from sympy.combinatorics.permutations import Permutation + from sympy.combinatorics.polyhedron import Polyhedron + from sympy.abc import w, x, y, z + pgroup = [Permutation([[0, 1, 2], [3]]), + Permutation([[0, 1, 3], [2]]), + Permutation([[0, 2, 3], [1]]), + Permutation([[1, 2, 3], [0]]), + Permutation([[0, 1], [2, 3]]), + Permutation([[0, 2], [1, 3]]), + Permutation([[0, 3], [1, 2]]), + Permutation([[0, 1, 2, 3]])] + corners = [w, x, y, z] + faces = [(w, x, y), (w, y, z), (w, z, x), (x, y, z)] + assert _test_args(Polyhedron(corners, faces, pgroup)) + + +def test_sympy__combinatorics__prufer__Prufer(): + from sympy.combinatorics.prufer import Prufer + assert _test_args(Prufer([[0, 1], [0, 2], [0, 3]], 4)) + + +def test_sympy__combinatorics__partitions__Partition(): + from sympy.combinatorics.partitions import Partition + assert _test_args(Partition([1])) + + +def test_sympy__combinatorics__partitions__IntegerPartition(): + from sympy.combinatorics.partitions import IntegerPartition + assert _test_args(IntegerPartition([1])) + + +def test_sympy__concrete__products__Product(): + from sympy.concrete.products import Product + assert _test_args(Product(x, (x, 0, 10))) + assert _test_args(Product(x, (x, 0, y), (y, 0, 10))) + + +@SKIP("abstract Class") +def test_sympy__concrete__expr_with_limits__ExprWithLimits(): + from sympy.concrete.expr_with_limits import ExprWithLimits + assert _test_args(ExprWithLimits(x, (x, 0, 10))) + assert _test_args(ExprWithLimits(x*y, (x, 0, 10.),(y,1.,3))) + + +@SKIP("abstract Class") +def test_sympy__concrete__expr_with_limits__AddWithLimits(): + from sympy.concrete.expr_with_limits import AddWithLimits + assert _test_args(AddWithLimits(x, (x, 0, 10))) + assert _test_args(AddWithLimits(x*y, (x, 0, 10),(y,1,3))) + + +@SKIP("abstract Class") +def test_sympy__concrete__expr_with_intlimits__ExprWithIntLimits(): + from sympy.concrete.expr_with_intlimits import ExprWithIntLimits + assert _test_args(ExprWithIntLimits(x, (x, 0, 10))) + assert _test_args(ExprWithIntLimits(x*y, (x, 0, 10),(y,1,3))) + + +def test_sympy__concrete__summations__Sum(): + from sympy.concrete.summations import Sum + assert _test_args(Sum(x, (x, 0, 10))) + assert _test_args(Sum(x, (x, 0, y), (y, 0, 10))) + + +def test_sympy__core__add__Add(): + from sympy.core.add import Add + assert _test_args(Add(x, y, z, 2)) + + +def test_sympy__core__basic__Atom(): + from sympy.core.basic import Atom + assert _test_args(Atom()) + + +def test_sympy__core__basic__Basic(): + from sympy.core.basic import Basic + assert _test_args(Basic()) + + +def test_sympy__core__containers__Dict(): + from sympy.core.containers import Dict + assert _test_args(Dict({x: y, y: z})) + + +def test_sympy__core__containers__Tuple(): + from sympy.core.containers import Tuple + assert _test_args(Tuple(x, y, z, 2)) + + +def test_sympy__core__expr__AtomicExpr(): + from sympy.core.expr import AtomicExpr + assert _test_args(AtomicExpr()) + + +def test_sympy__core__expr__Expr(): + from sympy.core.expr import Expr + assert _test_args(Expr()) + + +def test_sympy__core__expr__UnevaluatedExpr(): + from sympy.core.expr import UnevaluatedExpr + from sympy.abc import x + assert _test_args(UnevaluatedExpr(x)) + + +def test_sympy__core__function__Application(): + from sympy.core.function import Application + assert _test_args(Application(1, 2, 3)) + + +def test_sympy__core__function__AppliedUndef(): + from sympy.core.function import AppliedUndef + assert _test_args(AppliedUndef(1, 2, 3)) + + +def test_sympy__core__function__DefinedFunction(): + from sympy.core.function import DefinedFunction + assert _test_args(DefinedFunction(1, 2, 3)) + + +def test_sympy__core__function__Derivative(): + from sympy.core.function import Derivative + assert _test_args(Derivative(2, x, y, 3)) + + +@SKIP("abstract class") +def test_sympy__core__function__Function(): + pass + + +def test_sympy__core__function__Lambda(): + assert _test_args(Lambda((x, y), x + y + z)) + + +def test_sympy__core__function__Subs(): + from sympy.core.function import Subs + assert _test_args(Subs(x + y, x, 2)) + + +def test_sympy__core__function__WildFunction(): + from sympy.core.function import WildFunction + assert _test_args(WildFunction('f')) + + +def test_sympy__core__mod__Mod(): + from sympy.core.mod import Mod + assert _test_args(Mod(x, 2)) + + +def test_sympy__core__mul__Mul(): + from sympy.core.mul import Mul + assert _test_args(Mul(2, x, y, z)) + + +def test_sympy__core__numbers__Catalan(): + from sympy.core.numbers import Catalan + assert _test_args(Catalan()) + + +def test_sympy__core__numbers__ComplexInfinity(): + from sympy.core.numbers import ComplexInfinity + assert _test_args(ComplexInfinity()) + + +def test_sympy__core__numbers__EulerGamma(): + from sympy.core.numbers import EulerGamma + assert _test_args(EulerGamma()) + + +def test_sympy__core__numbers__Exp1(): + from sympy.core.numbers import Exp1 + assert _test_args(Exp1()) + + +def test_sympy__core__numbers__Float(): + from sympy.core.numbers import Float + assert _test_args(Float(1.23)) + + +def test_sympy__core__numbers__GoldenRatio(): + from sympy.core.numbers import GoldenRatio + assert _test_args(GoldenRatio()) + + +def test_sympy__core__numbers__TribonacciConstant(): + from sympy.core.numbers import TribonacciConstant + assert _test_args(TribonacciConstant()) + + +def test_sympy__core__numbers__Half(): + from sympy.core.numbers import Half + assert _test_args(Half()) + + +def test_sympy__core__numbers__ImaginaryUnit(): + from sympy.core.numbers import ImaginaryUnit + assert _test_args(ImaginaryUnit()) + + +def test_sympy__core__numbers__Infinity(): + from sympy.core.numbers import Infinity + assert _test_args(Infinity()) + + +def test_sympy__core__numbers__Integer(): + from sympy.core.numbers import Integer + assert _test_args(Integer(7)) + + +@SKIP("abstract class") +def test_sympy__core__numbers__IntegerConstant(): + pass + + +def test_sympy__core__numbers__NaN(): + from sympy.core.numbers import NaN + assert _test_args(NaN()) + + +def test_sympy__core__numbers__NegativeInfinity(): + from sympy.core.numbers import NegativeInfinity + assert _test_args(NegativeInfinity()) + + +def test_sympy__core__numbers__NegativeOne(): + from sympy.core.numbers import NegativeOne + assert _test_args(NegativeOne()) + + +def test_sympy__core__numbers__Number(): + from sympy.core.numbers import Number + assert _test_args(Number(1, 7)) + + +def test_sympy__core__numbers__NumberSymbol(): + from sympy.core.numbers import NumberSymbol + assert _test_args(NumberSymbol()) + + +def test_sympy__core__numbers__One(): + from sympy.core.numbers import One + assert _test_args(One()) + + +def test_sympy__core__numbers__Pi(): + from sympy.core.numbers import Pi + assert _test_args(Pi()) + + +def test_sympy__core__numbers__Rational(): + from sympy.core.numbers import Rational + assert _test_args(Rational(1, 7)) + + +@SKIP("abstract class") +def test_sympy__core__numbers__RationalConstant(): + pass + + +def test_sympy__core__numbers__Zero(): + from sympy.core.numbers import Zero + assert _test_args(Zero()) + + +@SKIP("abstract class") +def test_sympy__core__operations__AssocOp(): + pass + + +@SKIP("abstract class") +def test_sympy__core__operations__LatticeOp(): + pass + + +def test_sympy__core__power__Pow(): + from sympy.core.power import Pow + assert _test_args(Pow(x, 2)) + + +def test_sympy__core__relational__Equality(): + from sympy.core.relational import Equality + assert _test_args(Equality(x, 2)) + + +def test_sympy__core__relational__GreaterThan(): + from sympy.core.relational import GreaterThan + assert _test_args(GreaterThan(x, 2)) + + +def test_sympy__core__relational__LessThan(): + from sympy.core.relational import LessThan + assert _test_args(LessThan(x, 2)) + + +@SKIP("abstract class") +def test_sympy__core__relational__Relational(): + pass + + +def test_sympy__core__relational__StrictGreaterThan(): + from sympy.core.relational import StrictGreaterThan + assert _test_args(StrictGreaterThan(x, 2)) + + +def test_sympy__core__relational__StrictLessThan(): + from sympy.core.relational import StrictLessThan + assert _test_args(StrictLessThan(x, 2)) + + +def test_sympy__core__relational__Unequality(): + from sympy.core.relational import Unequality + assert _test_args(Unequality(x, 2)) + + +def test_sympy__sandbox__indexed_integrals__IndexedIntegral(): + from sympy.tensor import IndexedBase, Idx + from sympy.sandbox.indexed_integrals import IndexedIntegral + A = IndexedBase('A') + i, j = symbols('i j', integer=True) + a1, a2 = symbols('a1:3', cls=Idx) + assert _test_args(IndexedIntegral(A[a1], A[a2])) + assert _test_args(IndexedIntegral(A[i], A[j])) + + +def test_sympy__calculus__accumulationbounds__AccumulationBounds(): + from sympy.calculus.accumulationbounds import AccumulationBounds + assert _test_args(AccumulationBounds(0, 1)) + + +def test_sympy__sets__ordinals__OmegaPower(): + from sympy.sets.ordinals import OmegaPower + assert _test_args(OmegaPower(1, 1)) + +def test_sympy__sets__ordinals__Ordinal(): + from sympy.sets.ordinals import Ordinal, OmegaPower + assert _test_args(Ordinal(OmegaPower(2, 1))) + +def test_sympy__sets__ordinals__OrdinalOmega(): + from sympy.sets.ordinals import OrdinalOmega + assert _test_args(OrdinalOmega()) + +def test_sympy__sets__ordinals__OrdinalZero(): + from sympy.sets.ordinals import OrdinalZero + assert _test_args(OrdinalZero()) + + +def test_sympy__sets__powerset__PowerSet(): + from sympy.sets.powerset import PowerSet + from sympy.core.singleton import S + assert _test_args(PowerSet(S.EmptySet)) + + +def test_sympy__sets__sets__EmptySet(): + from sympy.sets.sets import EmptySet + assert _test_args(EmptySet()) + + +def test_sympy__sets__sets__UniversalSet(): + from sympy.sets.sets import UniversalSet + assert _test_args(UniversalSet()) + + +def test_sympy__sets__sets__FiniteSet(): + from sympy.sets.sets import FiniteSet + assert _test_args(FiniteSet(x, y, z)) + + +def test_sympy__sets__sets__Interval(): + from sympy.sets.sets import Interval + assert _test_args(Interval(0, 1)) + + +def test_sympy__sets__sets__ProductSet(): + from sympy.sets.sets import ProductSet, Interval + assert _test_args(ProductSet(Interval(0, 1), Interval(0, 1))) + + +@SKIP("does it make sense to test this?") +def test_sympy__sets__sets__Set(): + from sympy.sets.sets import Set + assert _test_args(Set()) + + +def test_sympy__sets__sets__Intersection(): + from sympy.sets.sets import Intersection, Interval + from sympy.core.symbol import Symbol + x = Symbol('x') + y = Symbol('y') + S = Intersection(Interval(0, x), Interval(y, 1)) + assert isinstance(S, Intersection) + assert _test_args(S) + + +def test_sympy__sets__sets__Union(): + from sympy.sets.sets import Union, Interval + assert _test_args(Union(Interval(0, 1), Interval(2, 3))) + + +def test_sympy__sets__sets__Complement(): + from sympy.sets.sets import Complement, Interval + assert _test_args(Complement(Interval(0, 2), Interval(0, 1))) + + +def test_sympy__sets__sets__SymmetricDifference(): + from sympy.sets.sets import FiniteSet, SymmetricDifference + assert _test_args(SymmetricDifference(FiniteSet(1, 2, 3), \ + FiniteSet(2, 3, 4))) + +def test_sympy__sets__sets__DisjointUnion(): + from sympy.sets.sets import FiniteSet, DisjointUnion + assert _test_args(DisjointUnion(FiniteSet(1, 2, 3), \ + FiniteSet(2, 3, 4))) + + +def test_sympy__physics__quantum__trace__Tr(): + from sympy.physics.quantum.trace import Tr + a, b = symbols('a b', commutative=False) + assert _test_args(Tr(a + b)) + + +def test_sympy__sets__setexpr__SetExpr(): + from sympy.sets.setexpr import SetExpr + from sympy.sets.sets import Interval + assert _test_args(SetExpr(Interval(0, 1))) + + +def test_sympy__sets__fancysets__Rationals(): + from sympy.sets.fancysets import Rationals + assert _test_args(Rationals()) + + +def test_sympy__sets__fancysets__Naturals(): + from sympy.sets.fancysets import Naturals + assert _test_args(Naturals()) + + +def test_sympy__sets__fancysets__Naturals0(): + from sympy.sets.fancysets import Naturals0 + assert _test_args(Naturals0()) + + +def test_sympy__sets__fancysets__Integers(): + from sympy.sets.fancysets import Integers + assert _test_args(Integers()) + + +def test_sympy__sets__fancysets__Reals(): + from sympy.sets.fancysets import Reals + assert _test_args(Reals()) + + +def test_sympy__sets__fancysets__Complexes(): + from sympy.sets.fancysets import Complexes + assert _test_args(Complexes()) + + +def test_sympy__sets__fancysets__ComplexRegion(): + from sympy.sets.fancysets import ComplexRegion + from sympy.core.singleton import S + from sympy.sets import Interval + a = Interval(0, 1) + b = Interval(2, 3) + theta = Interval(0, 2*S.Pi) + assert _test_args(ComplexRegion(a*b)) + assert _test_args(ComplexRegion(a*theta, polar=True)) + + +def test_sympy__sets__fancysets__CartesianComplexRegion(): + from sympy.sets.fancysets import CartesianComplexRegion + from sympy.sets import Interval + a = Interval(0, 1) + b = Interval(2, 3) + assert _test_args(CartesianComplexRegion(a*b)) + + +def test_sympy__sets__fancysets__PolarComplexRegion(): + from sympy.sets.fancysets import PolarComplexRegion + from sympy.core.singleton import S + from sympy.sets import Interval + a = Interval(0, 1) + theta = Interval(0, 2*S.Pi) + assert _test_args(PolarComplexRegion(a*theta)) + + +def test_sympy__sets__fancysets__ImageSet(): + from sympy.sets.fancysets import ImageSet + from sympy.core.singleton import S + from sympy.core.symbol import Symbol + x = Symbol('x') + assert _test_args(ImageSet(Lambda(x, x**2), S.Naturals)) + + +def test_sympy__sets__fancysets__Range(): + from sympy.sets.fancysets import Range + assert _test_args(Range(1, 5, 1)) + + +def test_sympy__sets__conditionset__ConditionSet(): + from sympy.sets.conditionset import ConditionSet + from sympy.core.singleton import S + from sympy.core.symbol import Symbol + x = Symbol('x') + assert _test_args(ConditionSet(x, Eq(x**2, 1), S.Reals)) + + +def test_sympy__sets__contains__Contains(): + from sympy.sets.fancysets import Range + from sympy.sets.contains import Contains + assert _test_args(Contains(x, Range(0, 10, 2))) + + +# STATS + + +from sympy.stats.crv_types import NormalDistribution +nd = NormalDistribution(0, 1) +from sympy.stats.frv_types import DieDistribution +die = DieDistribution(6) + + +def test_sympy__stats__crv__ContinuousDomain(): + from sympy.sets.sets import Interval + from sympy.stats.crv import ContinuousDomain + assert _test_args(ContinuousDomain({x}, Interval(-oo, oo))) + + +def test_sympy__stats__crv__SingleContinuousDomain(): + from sympy.sets.sets import Interval + from sympy.stats.crv import SingleContinuousDomain + assert _test_args(SingleContinuousDomain(x, Interval(-oo, oo))) + + +def test_sympy__stats__crv__ProductContinuousDomain(): + from sympy.sets.sets import Interval + from sympy.stats.crv import SingleContinuousDomain, ProductContinuousDomain + D = SingleContinuousDomain(x, Interval(-oo, oo)) + E = SingleContinuousDomain(y, Interval(0, oo)) + assert _test_args(ProductContinuousDomain(D, E)) + + +def test_sympy__stats__crv__ConditionalContinuousDomain(): + from sympy.sets.sets import Interval + from sympy.stats.crv import (SingleContinuousDomain, + ConditionalContinuousDomain) + D = SingleContinuousDomain(x, Interval(-oo, oo)) + assert _test_args(ConditionalContinuousDomain(D, x > 0)) + + +def test_sympy__stats__crv__ContinuousPSpace(): + from sympy.sets.sets import Interval + from sympy.stats.crv import ContinuousPSpace, SingleContinuousDomain + D = SingleContinuousDomain(x, Interval(-oo, oo)) + assert _test_args(ContinuousPSpace(D, nd)) + + +def test_sympy__stats__crv__SingleContinuousPSpace(): + from sympy.stats.crv import SingleContinuousPSpace + assert _test_args(SingleContinuousPSpace(x, nd)) + +@SKIP("abstract class") +def test_sympy__stats__rv__Distribution(): + pass + +@SKIP("abstract class") +def test_sympy__stats__crv__SingleContinuousDistribution(): + pass + + +def test_sympy__stats__drv__SingleDiscreteDomain(): + from sympy.stats.drv import SingleDiscreteDomain + assert _test_args(SingleDiscreteDomain(x, S.Naturals)) + + +def test_sympy__stats__drv__ProductDiscreteDomain(): + from sympy.stats.drv import SingleDiscreteDomain, ProductDiscreteDomain + X = SingleDiscreteDomain(x, S.Naturals) + Y = SingleDiscreteDomain(y, S.Integers) + assert _test_args(ProductDiscreteDomain(X, Y)) + + +def test_sympy__stats__drv__SingleDiscretePSpace(): + from sympy.stats.drv import SingleDiscretePSpace + from sympy.stats.drv_types import PoissonDistribution + assert _test_args(SingleDiscretePSpace(x, PoissonDistribution(1))) + +def test_sympy__stats__drv__DiscretePSpace(): + from sympy.stats.drv import DiscretePSpace, SingleDiscreteDomain + density = Lambda(x, 2**(-x)) + domain = SingleDiscreteDomain(x, S.Naturals) + assert _test_args(DiscretePSpace(domain, density)) + +def test_sympy__stats__drv__ConditionalDiscreteDomain(): + from sympy.stats.drv import ConditionalDiscreteDomain, SingleDiscreteDomain + X = SingleDiscreteDomain(x, S.Naturals0) + assert _test_args(ConditionalDiscreteDomain(X, x > 2)) + +def test_sympy__stats__joint_rv__JointPSpace(): + from sympy.stats.joint_rv import JointPSpace, JointDistribution + assert _test_args(JointPSpace('X', JointDistribution(1))) + +def test_sympy__stats__joint_rv__JointRandomSymbol(): + from sympy.stats.joint_rv import JointRandomSymbol + assert _test_args(JointRandomSymbol(x)) + +def test_sympy__stats__joint_rv_types__JointDistributionHandmade(): + from sympy.tensor.indexed import Indexed + from sympy.stats.joint_rv_types import JointDistributionHandmade + x1, x2 = (Indexed('x', i) for i in (1, 2)) + assert _test_args(JointDistributionHandmade(x1 + x2, S.Reals**2)) + + +def test_sympy__stats__joint_rv__MarginalDistribution(): + from sympy.stats.rv import RandomSymbol + from sympy.stats.joint_rv import MarginalDistribution + r = RandomSymbol(S('r')) + assert _test_args(MarginalDistribution(r, (r,))) + + +def test_sympy__stats__compound_rv__CompoundDistribution(): + from sympy.stats.compound_rv import CompoundDistribution + from sympy.stats.drv_types import PoissonDistribution, Poisson + r = Poisson('r', 10) + assert _test_args(CompoundDistribution(PoissonDistribution(r))) + + +def test_sympy__stats__compound_rv__CompoundPSpace(): + from sympy.stats.compound_rv import CompoundPSpace, CompoundDistribution + from sympy.stats.drv_types import PoissonDistribution, Poisson + r = Poisson('r', 5) + C = CompoundDistribution(PoissonDistribution(r)) + assert _test_args(CompoundPSpace('C', C)) + + +@SKIP("abstract class") +def test_sympy__stats__drv__SingleDiscreteDistribution(): + pass + +@SKIP("abstract class") +def test_sympy__stats__drv__DiscreteDistribution(): + pass + +@SKIP("abstract class") +def test_sympy__stats__drv__DiscreteDomain(): + pass + + +def test_sympy__stats__rv__RandomDomain(): + from sympy.stats.rv import RandomDomain + from sympy.sets.sets import FiniteSet + assert _test_args(RandomDomain(FiniteSet(x), FiniteSet(1, 2, 3))) + + +def test_sympy__stats__rv__SingleDomain(): + from sympy.stats.rv import SingleDomain + from sympy.sets.sets import FiniteSet + assert _test_args(SingleDomain(x, FiniteSet(1, 2, 3))) + + +def test_sympy__stats__rv__ConditionalDomain(): + from sympy.stats.rv import ConditionalDomain, RandomDomain + from sympy.sets.sets import FiniteSet + D = RandomDomain(FiniteSet(x), FiniteSet(1, 2)) + assert _test_args(ConditionalDomain(D, x > 1)) + +def test_sympy__stats__rv__MatrixDomain(): + from sympy.stats.rv import MatrixDomain + from sympy.matrices import MatrixSet + from sympy.core.singleton import S + assert _test_args(MatrixDomain(x, MatrixSet(2, 2, S.Reals))) + +def test_sympy__stats__rv__PSpace(): + from sympy.stats.rv import PSpace, RandomDomain + from sympy.sets.sets import FiniteSet + D = RandomDomain(FiniteSet(x), FiniteSet(1, 2, 3, 4, 5, 6)) + assert _test_args(PSpace(D, die)) + + +@SKIP("abstract Class") +def test_sympy__stats__rv__SinglePSpace(): + pass + + +def test_sympy__stats__rv__RandomSymbol(): + from sympy.stats.rv import RandomSymbol + from sympy.stats.crv import SingleContinuousPSpace + A = SingleContinuousPSpace(x, nd) + assert _test_args(RandomSymbol(x, A)) + + +@SKIP("abstract Class") +def test_sympy__stats__rv__ProductPSpace(): + pass + + +def test_sympy__stats__rv__IndependentProductPSpace(): + from sympy.stats.rv import IndependentProductPSpace + from sympy.stats.crv import SingleContinuousPSpace + A = SingleContinuousPSpace(x, nd) + B = SingleContinuousPSpace(y, nd) + assert _test_args(IndependentProductPSpace(A, B)) + + +def test_sympy__stats__rv__ProductDomain(): + from sympy.sets.sets import Interval + from sympy.stats.rv import ProductDomain, SingleDomain + D = SingleDomain(x, Interval(-oo, oo)) + E = SingleDomain(y, Interval(0, oo)) + assert _test_args(ProductDomain(D, E)) + + +def test_sympy__stats__symbolic_probability__Probability(): + from sympy.stats.symbolic_probability import Probability + from sympy.stats import Normal + X = Normal('X', 0, 1) + assert _test_args(Probability(X > 0)) + + +def test_sympy__stats__symbolic_probability__Expectation(): + from sympy.stats.symbolic_probability import Expectation + from sympy.stats import Normal + X = Normal('X', 0, 1) + assert _test_args(Expectation(X > 0)) + + +def test_sympy__stats__symbolic_probability__Covariance(): + from sympy.stats.symbolic_probability import Covariance + from sympy.stats import Normal + X = Normal('X', 0, 1) + Y = Normal('Y', 0, 3) + assert _test_args(Covariance(X, Y)) + + +def test_sympy__stats__symbolic_probability__Variance(): + from sympy.stats.symbolic_probability import Variance + from sympy.stats import Normal + X = Normal('X', 0, 1) + assert _test_args(Variance(X)) + + +def test_sympy__stats__symbolic_probability__Moment(): + from sympy.stats.symbolic_probability import Moment + from sympy.stats import Normal + X = Normal('X', 0, 1) + assert _test_args(Moment(X, 3, 2, X > 3)) + + +def test_sympy__stats__symbolic_probability__CentralMoment(): + from sympy.stats.symbolic_probability import CentralMoment + from sympy.stats import Normal + X = Normal('X', 0, 1) + assert _test_args(CentralMoment(X, 2, X > 1)) + + +def test_sympy__stats__frv_types__DiscreteUniformDistribution(): + from sympy.stats.frv_types import DiscreteUniformDistribution + from sympy.core.containers import Tuple + assert _test_args(DiscreteUniformDistribution(Tuple(*list(range(6))))) + + +def test_sympy__stats__frv_types__DieDistribution(): + assert _test_args(die) + + +def test_sympy__stats__frv_types__BernoulliDistribution(): + from sympy.stats.frv_types import BernoulliDistribution + assert _test_args(BernoulliDistribution(S.Half, 0, 1)) + + +def test_sympy__stats__frv_types__BinomialDistribution(): + from sympy.stats.frv_types import BinomialDistribution + assert _test_args(BinomialDistribution(5, S.Half, 1, 0)) + +def test_sympy__stats__frv_types__BetaBinomialDistribution(): + from sympy.stats.frv_types import BetaBinomialDistribution + assert _test_args(BetaBinomialDistribution(5, 1, 1)) + + +def test_sympy__stats__frv_types__HypergeometricDistribution(): + from sympy.stats.frv_types import HypergeometricDistribution + assert _test_args(HypergeometricDistribution(10, 5, 3)) + + +def test_sympy__stats__frv_types__RademacherDistribution(): + from sympy.stats.frv_types import RademacherDistribution + assert _test_args(RademacherDistribution()) + +def test_sympy__stats__frv_types__IdealSolitonDistribution(): + from sympy.stats.frv_types import IdealSolitonDistribution + assert _test_args(IdealSolitonDistribution(10)) + +def test_sympy__stats__frv_types__RobustSolitonDistribution(): + from sympy.stats.frv_types import RobustSolitonDistribution + assert _test_args(RobustSolitonDistribution(1000, 0.5, 0.1)) + +def test_sympy__stats__frv__FiniteDomain(): + from sympy.stats.frv import FiniteDomain + assert _test_args(FiniteDomain({(x, 1), (x, 2)})) # x can be 1 or 2 + + +def test_sympy__stats__frv__SingleFiniteDomain(): + from sympy.stats.frv import SingleFiniteDomain + assert _test_args(SingleFiniteDomain(x, {1, 2})) # x can be 1 or 2 + + +def test_sympy__stats__frv__ProductFiniteDomain(): + from sympy.stats.frv import SingleFiniteDomain, ProductFiniteDomain + xd = SingleFiniteDomain(x, {1, 2}) + yd = SingleFiniteDomain(y, {1, 2}) + assert _test_args(ProductFiniteDomain(xd, yd)) + + +def test_sympy__stats__frv__ConditionalFiniteDomain(): + from sympy.stats.frv import SingleFiniteDomain, ConditionalFiniteDomain + xd = SingleFiniteDomain(x, {1, 2}) + assert _test_args(ConditionalFiniteDomain(xd, x > 1)) + + +def test_sympy__stats__frv__FinitePSpace(): + from sympy.stats.frv import FinitePSpace, SingleFiniteDomain + xd = SingleFiniteDomain(x, {1, 2, 3, 4, 5, 6}) + assert _test_args(FinitePSpace(xd, {(x, 1): S.Half, (x, 2): S.Half})) + + xd = SingleFiniteDomain(x, {1, 2}) + assert _test_args(FinitePSpace(xd, {(x, 1): S.Half, (x, 2): S.Half})) + + +def test_sympy__stats__frv__SingleFinitePSpace(): + from sympy.stats.frv import SingleFinitePSpace + from sympy.core.symbol import Symbol + + assert _test_args(SingleFinitePSpace(Symbol('x'), die)) + + +def test_sympy__stats__frv__ProductFinitePSpace(): + from sympy.stats.frv import SingleFinitePSpace, ProductFinitePSpace + from sympy.core.symbol import Symbol + xp = SingleFinitePSpace(Symbol('x'), die) + yp = SingleFinitePSpace(Symbol('y'), die) + assert _test_args(ProductFinitePSpace(xp, yp)) + +@SKIP("abstract class") +def test_sympy__stats__frv__SingleFiniteDistribution(): + pass + +@SKIP("abstract class") +def test_sympy__stats__crv__ContinuousDistribution(): + pass + + +def test_sympy__stats__frv_types__FiniteDistributionHandmade(): + from sympy.stats.frv_types import FiniteDistributionHandmade + from sympy.core.containers import Dict + assert _test_args(FiniteDistributionHandmade(Dict({1: 1}))) + + +def test_sympy__stats__crv_types__ContinuousDistributionHandmade(): + from sympy.stats.crv_types import ContinuousDistributionHandmade + from sympy.core.function import Lambda + from sympy.sets.sets import Interval + from sympy.abc import x + assert _test_args(ContinuousDistributionHandmade(Lambda(x, 2*x), + Interval(0, 1))) + + +def test_sympy__stats__drv_types__DiscreteDistributionHandmade(): + from sympy.stats.drv_types import DiscreteDistributionHandmade + from sympy.core.function import Lambda + from sympy.sets.sets import FiniteSet + from sympy.abc import x + assert _test_args(DiscreteDistributionHandmade(Lambda(x, Rational(1, 10)), + FiniteSet(*range(10)))) + + +def test_sympy__stats__rv__Density(): + from sympy.stats.rv import Density + from sympy.stats.crv_types import Normal + assert _test_args(Density(Normal('x', 0, 1))) + + +def test_sympy__stats__crv_types__ArcsinDistribution(): + from sympy.stats.crv_types import ArcsinDistribution + assert _test_args(ArcsinDistribution(0, 1)) + + +def test_sympy__stats__crv_types__BeniniDistribution(): + from sympy.stats.crv_types import BeniniDistribution + assert _test_args(BeniniDistribution(1, 1, 1)) + + +def test_sympy__stats__crv_types__BetaDistribution(): + from sympy.stats.crv_types import BetaDistribution + assert _test_args(BetaDistribution(1, 1)) + +def test_sympy__stats__crv_types__BetaNoncentralDistribution(): + from sympy.stats.crv_types import BetaNoncentralDistribution + assert _test_args(BetaNoncentralDistribution(1, 1, 1)) + + +def test_sympy__stats__crv_types__BetaPrimeDistribution(): + from sympy.stats.crv_types import BetaPrimeDistribution + assert _test_args(BetaPrimeDistribution(1, 1)) + +def test_sympy__stats__crv_types__BoundedParetoDistribution(): + from sympy.stats.crv_types import BoundedParetoDistribution + assert _test_args(BoundedParetoDistribution(1, 1, 2)) + +def test_sympy__stats__crv_types__CauchyDistribution(): + from sympy.stats.crv_types import CauchyDistribution + assert _test_args(CauchyDistribution(0, 1)) + + +def test_sympy__stats__crv_types__ChiDistribution(): + from sympy.stats.crv_types import ChiDistribution + assert _test_args(ChiDistribution(1)) + + +def test_sympy__stats__crv_types__ChiNoncentralDistribution(): + from sympy.stats.crv_types import ChiNoncentralDistribution + assert _test_args(ChiNoncentralDistribution(1,1)) + + +def test_sympy__stats__crv_types__ChiSquaredDistribution(): + from sympy.stats.crv_types import ChiSquaredDistribution + assert _test_args(ChiSquaredDistribution(1)) + + +def test_sympy__stats__crv_types__DagumDistribution(): + from sympy.stats.crv_types import DagumDistribution + assert _test_args(DagumDistribution(1, 1, 1)) + + +def test_sympy__stats__crv_types__DavisDistribution(): + from sympy.stats.crv_types import DavisDistribution + assert _test_args(DavisDistribution(1, 1, 1)) + + +def test_sympy__stats__crv_types__ExGaussianDistribution(): + from sympy.stats.crv_types import ExGaussianDistribution + assert _test_args(ExGaussianDistribution(1, 1, 1)) + + +def test_sympy__stats__crv_types__ExponentialDistribution(): + from sympy.stats.crv_types import ExponentialDistribution + assert _test_args(ExponentialDistribution(1)) + + +def test_sympy__stats__crv_types__ExponentialPowerDistribution(): + from sympy.stats.crv_types import ExponentialPowerDistribution + assert _test_args(ExponentialPowerDistribution(0, 1, 1)) + + +def test_sympy__stats__crv_types__FDistributionDistribution(): + from sympy.stats.crv_types import FDistributionDistribution + assert _test_args(FDistributionDistribution(1, 1)) + + +def test_sympy__stats__crv_types__FisherZDistribution(): + from sympy.stats.crv_types import FisherZDistribution + assert _test_args(FisherZDistribution(1, 1)) + + +def test_sympy__stats__crv_types__FrechetDistribution(): + from sympy.stats.crv_types import FrechetDistribution + assert _test_args(FrechetDistribution(1, 1, 1)) + + +def test_sympy__stats__crv_types__GammaInverseDistribution(): + from sympy.stats.crv_types import GammaInverseDistribution + assert _test_args(GammaInverseDistribution(1, 1)) + + +def test_sympy__stats__crv_types__GammaDistribution(): + from sympy.stats.crv_types import GammaDistribution + assert _test_args(GammaDistribution(1, 1)) + +def test_sympy__stats__crv_types__GumbelDistribution(): + from sympy.stats.crv_types import GumbelDistribution + assert _test_args(GumbelDistribution(1, 1, False)) + +def test_sympy__stats__crv_types__GompertzDistribution(): + from sympy.stats.crv_types import GompertzDistribution + assert _test_args(GompertzDistribution(1, 1)) + +def test_sympy__stats__crv_types__KumaraswamyDistribution(): + from sympy.stats.crv_types import KumaraswamyDistribution + assert _test_args(KumaraswamyDistribution(1, 1)) + +def test_sympy__stats__crv_types__LaplaceDistribution(): + from sympy.stats.crv_types import LaplaceDistribution + assert _test_args(LaplaceDistribution(0, 1)) + +def test_sympy__stats__crv_types__LevyDistribution(): + from sympy.stats.crv_types import LevyDistribution + assert _test_args(LevyDistribution(0, 1)) + +def test_sympy__stats__crv_types__LogCauchyDistribution(): + from sympy.stats.crv_types import LogCauchyDistribution + assert _test_args(LogCauchyDistribution(0, 1)) + +def test_sympy__stats__crv_types__LogisticDistribution(): + from sympy.stats.crv_types import LogisticDistribution + assert _test_args(LogisticDistribution(0, 1)) + +def test_sympy__stats__crv_types__LogLogisticDistribution(): + from sympy.stats.crv_types import LogLogisticDistribution + assert _test_args(LogLogisticDistribution(1, 1)) + +def test_sympy__stats__crv_types__LogitNormalDistribution(): + from sympy.stats.crv_types import LogitNormalDistribution + assert _test_args(LogitNormalDistribution(0, 1)) + +def test_sympy__stats__crv_types__LogNormalDistribution(): + from sympy.stats.crv_types import LogNormalDistribution + assert _test_args(LogNormalDistribution(0, 1)) + +def test_sympy__stats__crv_types__LomaxDistribution(): + from sympy.stats.crv_types import LomaxDistribution + assert _test_args(LomaxDistribution(1, 2)) + +def test_sympy__stats__crv_types__MaxwellDistribution(): + from sympy.stats.crv_types import MaxwellDistribution + assert _test_args(MaxwellDistribution(1)) + +def test_sympy__stats__crv_types__MoyalDistribution(): + from sympy.stats.crv_types import MoyalDistribution + assert _test_args(MoyalDistribution(1,2)) + +def test_sympy__stats__crv_types__NakagamiDistribution(): + from sympy.stats.crv_types import NakagamiDistribution + assert _test_args(NakagamiDistribution(1, 1)) + + +def test_sympy__stats__crv_types__NormalDistribution(): + from sympy.stats.crv_types import NormalDistribution + assert _test_args(NormalDistribution(0, 1)) + +def test_sympy__stats__crv_types__GaussianInverseDistribution(): + from sympy.stats.crv_types import GaussianInverseDistribution + assert _test_args(GaussianInverseDistribution(1, 1)) + + +def test_sympy__stats__crv_types__ParetoDistribution(): + from sympy.stats.crv_types import ParetoDistribution + assert _test_args(ParetoDistribution(1, 1)) + +def test_sympy__stats__crv_types__PowerFunctionDistribution(): + from sympy.stats.crv_types import PowerFunctionDistribution + assert _test_args(PowerFunctionDistribution(2,0,1)) + +def test_sympy__stats__crv_types__QuadraticUDistribution(): + from sympy.stats.crv_types import QuadraticUDistribution + assert _test_args(QuadraticUDistribution(1, 2)) + +def test_sympy__stats__crv_types__RaisedCosineDistribution(): + from sympy.stats.crv_types import RaisedCosineDistribution + assert _test_args(RaisedCosineDistribution(1, 1)) + +def test_sympy__stats__crv_types__RayleighDistribution(): + from sympy.stats.crv_types import RayleighDistribution + assert _test_args(RayleighDistribution(1)) + +def test_sympy__stats__crv_types__ReciprocalDistribution(): + from sympy.stats.crv_types import ReciprocalDistribution + assert _test_args(ReciprocalDistribution(5, 30)) + +def test_sympy__stats__crv_types__ShiftedGompertzDistribution(): + from sympy.stats.crv_types import ShiftedGompertzDistribution + assert _test_args(ShiftedGompertzDistribution(1, 1)) + +def test_sympy__stats__crv_types__StudentTDistribution(): + from sympy.stats.crv_types import StudentTDistribution + assert _test_args(StudentTDistribution(1)) + +def test_sympy__stats__crv_types__TrapezoidalDistribution(): + from sympy.stats.crv_types import TrapezoidalDistribution + assert _test_args(TrapezoidalDistribution(1, 2, 3, 4)) + +def test_sympy__stats__crv_types__TriangularDistribution(): + from sympy.stats.crv_types import TriangularDistribution + assert _test_args(TriangularDistribution(-1, 0, 1)) + + +def test_sympy__stats__crv_types__UniformDistribution(): + from sympy.stats.crv_types import UniformDistribution + assert _test_args(UniformDistribution(0, 1)) + + +def test_sympy__stats__crv_types__UniformSumDistribution(): + from sympy.stats.crv_types import UniformSumDistribution + assert _test_args(UniformSumDistribution(1)) + + +def test_sympy__stats__crv_types__VonMisesDistribution(): + from sympy.stats.crv_types import VonMisesDistribution + assert _test_args(VonMisesDistribution(1, 1)) + + +def test_sympy__stats__crv_types__WeibullDistribution(): + from sympy.stats.crv_types import WeibullDistribution + assert _test_args(WeibullDistribution(1, 1)) + + +def test_sympy__stats__crv_types__WignerSemicircleDistribution(): + from sympy.stats.crv_types import WignerSemicircleDistribution + assert _test_args(WignerSemicircleDistribution(1)) + + +def test_sympy__stats__drv_types__GeometricDistribution(): + from sympy.stats.drv_types import GeometricDistribution + assert _test_args(GeometricDistribution(.5)) + +def test_sympy__stats__drv_types__HermiteDistribution(): + from sympy.stats.drv_types import HermiteDistribution + assert _test_args(HermiteDistribution(1, 2)) + +def test_sympy__stats__drv_types__LogarithmicDistribution(): + from sympy.stats.drv_types import LogarithmicDistribution + assert _test_args(LogarithmicDistribution(.5)) + + +def test_sympy__stats__drv_types__NegativeBinomialDistribution(): + from sympy.stats.drv_types import NegativeBinomialDistribution + assert _test_args(NegativeBinomialDistribution(.5, .5)) + +def test_sympy__stats__drv_types__FlorySchulzDistribution(): + from sympy.stats.drv_types import FlorySchulzDistribution + assert _test_args(FlorySchulzDistribution(.5)) + +def test_sympy__stats__drv_types__PoissonDistribution(): + from sympy.stats.drv_types import PoissonDistribution + assert _test_args(PoissonDistribution(1)) + + +def test_sympy__stats__drv_types__SkellamDistribution(): + from sympy.stats.drv_types import SkellamDistribution + assert _test_args(SkellamDistribution(1, 1)) + + +def test_sympy__stats__drv_types__YuleSimonDistribution(): + from sympy.stats.drv_types import YuleSimonDistribution + assert _test_args(YuleSimonDistribution(.5)) + + +def test_sympy__stats__drv_types__ZetaDistribution(): + from sympy.stats.drv_types import ZetaDistribution + assert _test_args(ZetaDistribution(1.5)) + + +def test_sympy__stats__joint_rv__JointDistribution(): + from sympy.stats.joint_rv import JointDistribution + assert _test_args(JointDistribution(1, 2, 3, 4)) + + +def test_sympy__stats__joint_rv_types__MultivariateNormalDistribution(): + from sympy.stats.joint_rv_types import MultivariateNormalDistribution + assert _test_args( + MultivariateNormalDistribution([0, 1], [[1, 0],[0, 1]])) + +def test_sympy__stats__joint_rv_types__MultivariateLaplaceDistribution(): + from sympy.stats.joint_rv_types import MultivariateLaplaceDistribution + assert _test_args(MultivariateLaplaceDistribution([0, 1], [[1, 0],[0, 1]])) + + +def test_sympy__stats__joint_rv_types__MultivariateTDistribution(): + from sympy.stats.joint_rv_types import MultivariateTDistribution + assert _test_args(MultivariateTDistribution([0, 1], [[1, 0],[0, 1]], 1)) + + +def test_sympy__stats__joint_rv_types__NormalGammaDistribution(): + from sympy.stats.joint_rv_types import NormalGammaDistribution + assert _test_args(NormalGammaDistribution(1, 2, 3, 4)) + +def test_sympy__stats__joint_rv_types__GeneralizedMultivariateLogGammaDistribution(): + from sympy.stats.joint_rv_types import GeneralizedMultivariateLogGammaDistribution + v, l, mu = (4, [1, 2, 3, 4], [1, 2, 3, 4]) + assert _test_args(GeneralizedMultivariateLogGammaDistribution(S.Half, v, l, mu)) + +def test_sympy__stats__joint_rv_types__MultivariateBetaDistribution(): + from sympy.stats.joint_rv_types import MultivariateBetaDistribution + assert _test_args(MultivariateBetaDistribution([1, 2, 3])) + +def test_sympy__stats__joint_rv_types__MultivariateEwensDistribution(): + from sympy.stats.joint_rv_types import MultivariateEwensDistribution + assert _test_args(MultivariateEwensDistribution(5, 1)) + +def test_sympy__stats__joint_rv_types__MultinomialDistribution(): + from sympy.stats.joint_rv_types import MultinomialDistribution + assert _test_args(MultinomialDistribution(5, [0.5, 0.1, 0.3])) + +def test_sympy__stats__joint_rv_types__NegativeMultinomialDistribution(): + from sympy.stats.joint_rv_types import NegativeMultinomialDistribution + assert _test_args(NegativeMultinomialDistribution(5, [0.5, 0.1, 0.3])) + +def test_sympy__stats__rv__RandomIndexedSymbol(): + from sympy.stats.rv import RandomIndexedSymbol, pspace + from sympy.stats.stochastic_process_types import DiscreteMarkovChain + X = DiscreteMarkovChain("X") + assert _test_args(RandomIndexedSymbol(X[0].symbol, pspace(X[0]))) + +def test_sympy__stats__rv__RandomMatrixSymbol(): + from sympy.stats.rv import RandomMatrixSymbol + from sympy.stats.random_matrix import RandomMatrixPSpace + pspace = RandomMatrixPSpace('P') + assert _test_args(RandomMatrixSymbol('M', 3, 3, pspace)) + +def test_sympy__stats__stochastic_process__StochasticPSpace(): + from sympy.stats.stochastic_process import StochasticPSpace + from sympy.stats.stochastic_process_types import StochasticProcess + from sympy.stats.frv_types import BernoulliDistribution + assert _test_args(StochasticPSpace("Y", StochasticProcess("Y", [1, 2, 3]), BernoulliDistribution(S.Half, 1, 0))) + +def test_sympy__stats__stochastic_process_types__StochasticProcess(): + from sympy.stats.stochastic_process_types import StochasticProcess + assert _test_args(StochasticProcess("Y", [1, 2, 3])) + +def test_sympy__stats__stochastic_process_types__MarkovProcess(): + from sympy.stats.stochastic_process_types import MarkovProcess + assert _test_args(MarkovProcess("Y", [1, 2, 3])) + +def test_sympy__stats__stochastic_process_types__DiscreteTimeStochasticProcess(): + from sympy.stats.stochastic_process_types import DiscreteTimeStochasticProcess + assert _test_args(DiscreteTimeStochasticProcess("Y", [1, 2, 3])) + +def test_sympy__stats__stochastic_process_types__ContinuousTimeStochasticProcess(): + from sympy.stats.stochastic_process_types import ContinuousTimeStochasticProcess + assert _test_args(ContinuousTimeStochasticProcess("Y", [1, 2, 3])) + +def test_sympy__stats__stochastic_process_types__TransitionMatrixOf(): + from sympy.stats.stochastic_process_types import TransitionMatrixOf, DiscreteMarkovChain + from sympy.matrices.expressions.matexpr import MatrixSymbol + DMC = DiscreteMarkovChain("Y") + assert _test_args(TransitionMatrixOf(DMC, MatrixSymbol('T', 3, 3))) + +def test_sympy__stats__stochastic_process_types__GeneratorMatrixOf(): + from sympy.stats.stochastic_process_types import GeneratorMatrixOf, ContinuousMarkovChain + from sympy.matrices.expressions.matexpr import MatrixSymbol + DMC = ContinuousMarkovChain("Y") + assert _test_args(GeneratorMatrixOf(DMC, MatrixSymbol('T', 3, 3))) + +def test_sympy__stats__stochastic_process_types__StochasticStateSpaceOf(): + from sympy.stats.stochastic_process_types import StochasticStateSpaceOf, DiscreteMarkovChain + DMC = DiscreteMarkovChain("Y") + assert _test_args(StochasticStateSpaceOf(DMC, [0, 1, 2])) + +def test_sympy__stats__stochastic_process_types__DiscreteMarkovChain(): + from sympy.stats.stochastic_process_types import DiscreteMarkovChain + from sympy.matrices.expressions.matexpr import MatrixSymbol + assert _test_args(DiscreteMarkovChain("Y", [0, 1, 2], MatrixSymbol('T', 3, 3))) + +def test_sympy__stats__stochastic_process_types__ContinuousMarkovChain(): + from sympy.stats.stochastic_process_types import ContinuousMarkovChain + from sympy.matrices.expressions.matexpr import MatrixSymbol + assert _test_args(ContinuousMarkovChain("Y", [0, 1, 2], MatrixSymbol('T', 3, 3))) + +def test_sympy__stats__stochastic_process_types__BernoulliProcess(): + from sympy.stats.stochastic_process_types import BernoulliProcess + assert _test_args(BernoulliProcess("B", 0.5, 1, 0)) + +def test_sympy__stats__stochastic_process_types__CountingProcess(): + from sympy.stats.stochastic_process_types import CountingProcess + assert _test_args(CountingProcess("C")) + +def test_sympy__stats__stochastic_process_types__PoissonProcess(): + from sympy.stats.stochastic_process_types import PoissonProcess + assert _test_args(PoissonProcess("X", 2)) + +def test_sympy__stats__stochastic_process_types__WienerProcess(): + from sympy.stats.stochastic_process_types import WienerProcess + assert _test_args(WienerProcess("X")) + +def test_sympy__stats__stochastic_process_types__GammaProcess(): + from sympy.stats.stochastic_process_types import GammaProcess + assert _test_args(GammaProcess("X", 1, 2)) + +def test_sympy__stats__random_matrix__RandomMatrixPSpace(): + from sympy.stats.random_matrix import RandomMatrixPSpace + from sympy.stats.random_matrix_models import RandomMatrixEnsembleModel + model = RandomMatrixEnsembleModel('R', 3) + assert _test_args(RandomMatrixPSpace('P', model=model)) + +def test_sympy__stats__random_matrix_models__RandomMatrixEnsembleModel(): + from sympy.stats.random_matrix_models import RandomMatrixEnsembleModel + assert _test_args(RandomMatrixEnsembleModel('R', 3)) + +def test_sympy__stats__random_matrix_models__GaussianEnsembleModel(): + from sympy.stats.random_matrix_models import GaussianEnsembleModel + assert _test_args(GaussianEnsembleModel('G', 3)) + +def test_sympy__stats__random_matrix_models__GaussianUnitaryEnsembleModel(): + from sympy.stats.random_matrix_models import GaussianUnitaryEnsembleModel + assert _test_args(GaussianUnitaryEnsembleModel('U', 3)) + +def test_sympy__stats__random_matrix_models__GaussianOrthogonalEnsembleModel(): + from sympy.stats.random_matrix_models import GaussianOrthogonalEnsembleModel + assert _test_args(GaussianOrthogonalEnsembleModel('U', 3)) + +def test_sympy__stats__random_matrix_models__GaussianSymplecticEnsembleModel(): + from sympy.stats.random_matrix_models import GaussianSymplecticEnsembleModel + assert _test_args(GaussianSymplecticEnsembleModel('U', 3)) + +def test_sympy__stats__random_matrix_models__CircularEnsembleModel(): + from sympy.stats.random_matrix_models import CircularEnsembleModel + assert _test_args(CircularEnsembleModel('C', 3)) + +def test_sympy__stats__random_matrix_models__CircularUnitaryEnsembleModel(): + from sympy.stats.random_matrix_models import CircularUnitaryEnsembleModel + assert _test_args(CircularUnitaryEnsembleModel('U', 3)) + +def test_sympy__stats__random_matrix_models__CircularOrthogonalEnsembleModel(): + from sympy.stats.random_matrix_models import CircularOrthogonalEnsembleModel + assert _test_args(CircularOrthogonalEnsembleModel('O', 3)) + +def test_sympy__stats__random_matrix_models__CircularSymplecticEnsembleModel(): + from sympy.stats.random_matrix_models import CircularSymplecticEnsembleModel + assert _test_args(CircularSymplecticEnsembleModel('S', 3)) + +def test_sympy__stats__symbolic_multivariate_probability__ExpectationMatrix(): + from sympy.stats import ExpectationMatrix + from sympy.stats.rv import RandomMatrixSymbol + assert _test_args(ExpectationMatrix(RandomMatrixSymbol('R', 2, 1))) + +def test_sympy__stats__symbolic_multivariate_probability__VarianceMatrix(): + from sympy.stats import VarianceMatrix + from sympy.stats.rv import RandomMatrixSymbol + assert _test_args(VarianceMatrix(RandomMatrixSymbol('R', 3, 1))) + +def test_sympy__stats__symbolic_multivariate_probability__CrossCovarianceMatrix(): + from sympy.stats import CrossCovarianceMatrix + from sympy.stats.rv import RandomMatrixSymbol + assert _test_args(CrossCovarianceMatrix(RandomMatrixSymbol('R', 3, 1), + RandomMatrixSymbol('X', 3, 1))) + +def test_sympy__stats__matrix_distributions__MatrixPSpace(): + from sympy.stats.matrix_distributions import MatrixDistribution, MatrixPSpace + from sympy.matrices.dense import Matrix + M = MatrixDistribution(1, Matrix([[1, 0], [0, 1]])) + assert _test_args(MatrixPSpace('M', M, 2, 2)) + +def test_sympy__stats__matrix_distributions__MatrixDistribution(): + from sympy.stats.matrix_distributions import MatrixDistribution + from sympy.matrices.dense import Matrix + assert _test_args(MatrixDistribution(1, Matrix([[1, 0], [0, 1]]))) + +def test_sympy__stats__matrix_distributions__MatrixGammaDistribution(): + from sympy.stats.matrix_distributions import MatrixGammaDistribution + from sympy.matrices.dense import Matrix + assert _test_args(MatrixGammaDistribution(3, 4, Matrix([[1, 0], [0, 1]]))) + +def test_sympy__stats__matrix_distributions__WishartDistribution(): + from sympy.stats.matrix_distributions import WishartDistribution + from sympy.matrices.dense import Matrix + assert _test_args(WishartDistribution(3, Matrix([[1, 0], [0, 1]]))) + +def test_sympy__stats__matrix_distributions__MatrixNormalDistribution(): + from sympy.stats.matrix_distributions import MatrixNormalDistribution + from sympy.matrices.expressions.matexpr import MatrixSymbol + L = MatrixSymbol('L', 1, 2) + S1 = MatrixSymbol('S1', 1, 1) + S2 = MatrixSymbol('S2', 2, 2) + assert _test_args(MatrixNormalDistribution(L, S1, S2)) + +def test_sympy__stats__matrix_distributions__MatrixStudentTDistribution(): + from sympy.stats.matrix_distributions import MatrixStudentTDistribution + from sympy.matrices.expressions.matexpr import MatrixSymbol + v = symbols('v', positive=True) + Omega = MatrixSymbol('Omega', 3, 3) + Sigma = MatrixSymbol('Sigma', 1, 1) + Location = MatrixSymbol('Location', 1, 3) + assert _test_args(MatrixStudentTDistribution(v, Location, Omega, Sigma)) + +def test_sympy__utilities__matchpy_connector__WildDot(): + from sympy.utilities.matchpy_connector import WildDot + assert _test_args(WildDot("w_")) + + +def test_sympy__utilities__matchpy_connector__WildPlus(): + from sympy.utilities.matchpy_connector import WildPlus + assert _test_args(WildPlus("w__")) + + +def test_sympy__utilities__matchpy_connector__WildStar(): + from sympy.utilities.matchpy_connector import WildStar + assert _test_args(WildStar("w___")) + + +def test_sympy__core__symbol__Str(): + from sympy.core.symbol import Str + assert _test_args(Str('t')) + +def test_sympy__core__symbol__Dummy(): + from sympy.core.symbol import Dummy + assert _test_args(Dummy('t')) + + +def test_sympy__core__symbol__Symbol(): + from sympy.core.symbol import Symbol + assert _test_args(Symbol('t')) + + +def test_sympy__core__symbol__Wild(): + from sympy.core.symbol import Wild + assert _test_args(Wild('x', exclude=[x])) + + +@SKIP("abstract class") +def test_sympy__functions__combinatorial__factorials__CombinatorialFunction(): + pass + + +def test_sympy__functions__combinatorial__factorials__FallingFactorial(): + from sympy.functions.combinatorial.factorials import FallingFactorial + assert _test_args(FallingFactorial(2, x)) + + +def test_sympy__functions__combinatorial__factorials__MultiFactorial(): + from sympy.functions.combinatorial.factorials import MultiFactorial + assert _test_args(MultiFactorial(x)) + + +def test_sympy__functions__combinatorial__factorials__RisingFactorial(): + from sympy.functions.combinatorial.factorials import RisingFactorial + assert _test_args(RisingFactorial(2, x)) + + +def test_sympy__functions__combinatorial__factorials__binomial(): + from sympy.functions.combinatorial.factorials import binomial + assert _test_args(binomial(2, x)) + + +def test_sympy__functions__combinatorial__factorials__subfactorial(): + from sympy.functions.combinatorial.factorials import subfactorial + assert _test_args(subfactorial(x)) + + +def test_sympy__functions__combinatorial__factorials__factorial(): + from sympy.functions.combinatorial.factorials import factorial + assert _test_args(factorial(x)) + + +def test_sympy__functions__combinatorial__factorials__factorial2(): + from sympy.functions.combinatorial.factorials import factorial2 + assert _test_args(factorial2(x)) + + +def test_sympy__functions__combinatorial__numbers__bell(): + from sympy.functions.combinatorial.numbers import bell + assert _test_args(bell(x, y)) + + +def test_sympy__functions__combinatorial__numbers__bernoulli(): + from sympy.functions.combinatorial.numbers import bernoulli + assert _test_args(bernoulli(x)) + + +def test_sympy__functions__combinatorial__numbers__catalan(): + from sympy.functions.combinatorial.numbers import catalan + assert _test_args(catalan(x)) + + +def test_sympy__functions__combinatorial__numbers__genocchi(): + from sympy.functions.combinatorial.numbers import genocchi + assert _test_args(genocchi(x)) + + +def test_sympy__functions__combinatorial__numbers__euler(): + from sympy.functions.combinatorial.numbers import euler + assert _test_args(euler(x)) + + +def test_sympy__functions__combinatorial__numbers__andre(): + from sympy.functions.combinatorial.numbers import andre + assert _test_args(andre(x)) + + +def test_sympy__functions__combinatorial__numbers__carmichael(): + from sympy.functions.combinatorial.numbers import carmichael + assert _test_args(carmichael(x)) + + +def test_sympy__functions__combinatorial__numbers__divisor_sigma(): + from sympy.functions.combinatorial.numbers import divisor_sigma + k = symbols('k', integer=True) + n = symbols('n', integer=True) + t = divisor_sigma(n, k) + assert _test_args(t) + + +def test_sympy__functions__combinatorial__numbers__fibonacci(): + from sympy.functions.combinatorial.numbers import fibonacci + assert _test_args(fibonacci(x)) + + +def test_sympy__functions__combinatorial__numbers__jacobi_symbol(): + from sympy.functions.combinatorial.numbers import jacobi_symbol + assert _test_args(jacobi_symbol(2, 3)) + + +def test_sympy__functions__combinatorial__numbers__kronecker_symbol(): + from sympy.functions.combinatorial.numbers import kronecker_symbol + assert _test_args(kronecker_symbol(2, 3)) + + +def test_sympy__functions__combinatorial__numbers__legendre_symbol(): + from sympy.functions.combinatorial.numbers import legendre_symbol + assert _test_args(legendre_symbol(2, 3)) + + +def test_sympy__functions__combinatorial__numbers__mobius(): + from sympy.functions.combinatorial.numbers import mobius + assert _test_args(mobius(2)) + + +def test_sympy__functions__combinatorial__numbers__motzkin(): + from sympy.functions.combinatorial.numbers import motzkin + assert _test_args(motzkin(5)) + + +def test_sympy__functions__combinatorial__numbers__partition(): + from sympy.core.symbol import Symbol + from sympy.functions.combinatorial.numbers import partition + assert _test_args(partition(Symbol('a', integer=True))) + + +def test_sympy__functions__combinatorial__numbers__primenu(): + from sympy.functions.combinatorial.numbers import primenu + n = symbols('n', integer=True) + t = primenu(n) + assert _test_args(t) + + +def test_sympy__functions__combinatorial__numbers__primeomega(): + from sympy.functions.combinatorial.numbers import primeomega + n = symbols('n', integer=True) + t = primeomega(n) + assert _test_args(t) + + +def test_sympy__functions__combinatorial__numbers__primepi(): + from sympy.functions.combinatorial.numbers import primepi + n = symbols('n') + t = primepi(n) + assert _test_args(t) + + +def test_sympy__functions__combinatorial__numbers__reduced_totient(): + from sympy.functions.combinatorial.numbers import reduced_totient + k = symbols('k', integer=True) + t = reduced_totient(k) + assert _test_args(t) + + +def test_sympy__functions__combinatorial__numbers__totient(): + from sympy.functions.combinatorial.numbers import totient + k = symbols('k', integer=True) + t = totient(k) + assert _test_args(t) + + +def test_sympy__functions__combinatorial__numbers__tribonacci(): + from sympy.functions.combinatorial.numbers import tribonacci + assert _test_args(tribonacci(x)) + + +def test_sympy__functions__combinatorial__numbers__udivisor_sigma(): + from sympy.functions.combinatorial.numbers import udivisor_sigma + k = symbols('k', integer=True) + n = symbols('n', integer=True) + t = udivisor_sigma(n, k) + assert _test_args(t) + + +def test_sympy__functions__combinatorial__numbers__harmonic(): + from sympy.functions.combinatorial.numbers import harmonic + assert _test_args(harmonic(x, 2)) + + +def test_sympy__functions__combinatorial__numbers__lucas(): + from sympy.functions.combinatorial.numbers import lucas + assert _test_args(lucas(x)) + + +def test_sympy__functions__elementary__complexes__Abs(): + from sympy.functions.elementary.complexes import Abs + assert _test_args(Abs(x)) + + +def test_sympy__functions__elementary__complexes__adjoint(): + from sympy.functions.elementary.complexes import adjoint + assert _test_args(adjoint(x)) + + +def test_sympy__functions__elementary__complexes__arg(): + from sympy.functions.elementary.complexes import arg + assert _test_args(arg(x)) + + +def test_sympy__functions__elementary__complexes__conjugate(): + from sympy.functions.elementary.complexes import conjugate + assert _test_args(conjugate(x)) + + +def test_sympy__functions__elementary__complexes__im(): + from sympy.functions.elementary.complexes import im + assert _test_args(im(x)) + + +def test_sympy__functions__elementary__complexes__re(): + from sympy.functions.elementary.complexes import re + assert _test_args(re(x)) + + +def test_sympy__functions__elementary__complexes__sign(): + from sympy.functions.elementary.complexes import sign + assert _test_args(sign(x)) + + +def test_sympy__functions__elementary__complexes__polar_lift(): + from sympy.functions.elementary.complexes import polar_lift + assert _test_args(polar_lift(x)) + + +def test_sympy__functions__elementary__complexes__periodic_argument(): + from sympy.functions.elementary.complexes import periodic_argument + assert _test_args(periodic_argument(x, y)) + + +def test_sympy__functions__elementary__complexes__principal_branch(): + from sympy.functions.elementary.complexes import principal_branch + assert _test_args(principal_branch(x, y)) + + +def test_sympy__functions__elementary__complexes__transpose(): + from sympy.functions.elementary.complexes import transpose + assert _test_args(transpose(x)) + + +def test_sympy__functions__elementary__exponential__LambertW(): + from sympy.functions.elementary.exponential import LambertW + assert _test_args(LambertW(2)) + + +@SKIP("abstract class") +def test_sympy__functions__elementary__exponential__ExpBase(): + pass + + +def test_sympy__functions__elementary__exponential__exp(): + from sympy.functions.elementary.exponential import exp + assert _test_args(exp(2)) + + +def test_sympy__functions__elementary__exponential__exp_polar(): + from sympy.functions.elementary.exponential import exp_polar + assert _test_args(exp_polar(2)) + + +def test_sympy__functions__elementary__exponential__log(): + from sympy.functions.elementary.exponential import log + assert _test_args(log(2)) + + +@SKIP("abstract class") +def test_sympy__functions__elementary__hyperbolic__HyperbolicFunction(): + pass + + +@SKIP("abstract class") +def test_sympy__functions__elementary__hyperbolic__ReciprocalHyperbolicFunction(): + pass + + +@SKIP("abstract class") +def test_sympy__functions__elementary__hyperbolic__InverseHyperbolicFunction(): + pass + + +def test_sympy__functions__elementary__hyperbolic__acosh(): + from sympy.functions.elementary.hyperbolic import acosh + assert _test_args(acosh(2)) + + +def test_sympy__functions__elementary__hyperbolic__acoth(): + from sympy.functions.elementary.hyperbolic import acoth + assert _test_args(acoth(2)) + + +def test_sympy__functions__elementary__hyperbolic__asinh(): + from sympy.functions.elementary.hyperbolic import asinh + assert _test_args(asinh(2)) + + +def test_sympy__functions__elementary__hyperbolic__atanh(): + from sympy.functions.elementary.hyperbolic import atanh + assert _test_args(atanh(2)) + + +def test_sympy__functions__elementary__hyperbolic__asech(): + from sympy.functions.elementary.hyperbolic import asech + assert _test_args(asech(x)) + +def test_sympy__functions__elementary__hyperbolic__acsch(): + from sympy.functions.elementary.hyperbolic import acsch + assert _test_args(acsch(x)) + +def test_sympy__functions__elementary__hyperbolic__cosh(): + from sympy.functions.elementary.hyperbolic import cosh + assert _test_args(cosh(2)) + + +def test_sympy__functions__elementary__hyperbolic__coth(): + from sympy.functions.elementary.hyperbolic import coth + assert _test_args(coth(2)) + + +def test_sympy__functions__elementary__hyperbolic__csch(): + from sympy.functions.elementary.hyperbolic import csch + assert _test_args(csch(2)) + + +def test_sympy__functions__elementary__hyperbolic__sech(): + from sympy.functions.elementary.hyperbolic import sech + assert _test_args(sech(2)) + + +def test_sympy__functions__elementary__hyperbolic__sinh(): + from sympy.functions.elementary.hyperbolic import sinh + assert _test_args(sinh(2)) + + +def test_sympy__functions__elementary__hyperbolic__tanh(): + from sympy.functions.elementary.hyperbolic import tanh + assert _test_args(tanh(2)) + + +@SKIP("abstract class") +def test_sympy__functions__elementary__integers__RoundFunction(): + pass + + +def test_sympy__functions__elementary__integers__ceiling(): + from sympy.functions.elementary.integers import ceiling + assert _test_args(ceiling(x)) + + +def test_sympy__functions__elementary__integers__floor(): + from sympy.functions.elementary.integers import floor + assert _test_args(floor(x)) + + +def test_sympy__functions__elementary__integers__frac(): + from sympy.functions.elementary.integers import frac + assert _test_args(frac(x)) + + +def test_sympy__functions__elementary__miscellaneous__IdentityFunction(): + from sympy.functions.elementary.miscellaneous import IdentityFunction + assert _test_args(IdentityFunction()) + + +def test_sympy__functions__elementary__miscellaneous__Max(): + from sympy.functions.elementary.miscellaneous import Max + assert _test_args(Max(x, 2)) + + +def test_sympy__functions__elementary__miscellaneous__Min(): + from sympy.functions.elementary.miscellaneous import Min + assert _test_args(Min(x, 2)) + + +@SKIP("abstract class") +def test_sympy__functions__elementary__miscellaneous__MinMaxBase(): + pass + + +def test_sympy__functions__elementary__miscellaneous__Rem(): + from sympy.functions.elementary.miscellaneous import Rem + assert _test_args(Rem(x, 2)) + + +def test_sympy__functions__elementary__piecewise__ExprCondPair(): + from sympy.functions.elementary.piecewise import ExprCondPair + assert _test_args(ExprCondPair(1, True)) + + +def test_sympy__functions__elementary__piecewise__Piecewise(): + from sympy.functions.elementary.piecewise import Piecewise + assert _test_args(Piecewise((1, x >= 0), (0, True))) + + +@SKIP("abstract class") +def test_sympy__functions__elementary__trigonometric__TrigonometricFunction(): + pass + +@SKIP("abstract class") +def test_sympy__functions__elementary__trigonometric__ReciprocalTrigonometricFunction(): + pass + +@SKIP("abstract class") +def test_sympy__functions__elementary__trigonometric__InverseTrigonometricFunction(): + pass + +def test_sympy__functions__elementary__trigonometric__acos(): + from sympy.functions.elementary.trigonometric import acos + assert _test_args(acos(2)) + + +def test_sympy__functions__elementary__trigonometric__acot(): + from sympy.functions.elementary.trigonometric import acot + assert _test_args(acot(2)) + + +def test_sympy__functions__elementary__trigonometric__asin(): + from sympy.functions.elementary.trigonometric import asin + assert _test_args(asin(2)) + + +def test_sympy__functions__elementary__trigonometric__asec(): + from sympy.functions.elementary.trigonometric import asec + assert _test_args(asec(x)) + + +def test_sympy__functions__elementary__trigonometric__acsc(): + from sympy.functions.elementary.trigonometric import acsc + assert _test_args(acsc(x)) + + +def test_sympy__functions__elementary__trigonometric__atan(): + from sympy.functions.elementary.trigonometric import atan + assert _test_args(atan(2)) + + +def test_sympy__functions__elementary__trigonometric__atan2(): + from sympy.functions.elementary.trigonometric import atan2 + assert _test_args(atan2(2, 3)) + + +def test_sympy__functions__elementary__trigonometric__cos(): + from sympy.functions.elementary.trigonometric import cos + assert _test_args(cos(2)) + + +def test_sympy__functions__elementary__trigonometric__csc(): + from sympy.functions.elementary.trigonometric import csc + assert _test_args(csc(2)) + + +def test_sympy__functions__elementary__trigonometric__cot(): + from sympy.functions.elementary.trigonometric import cot + assert _test_args(cot(2)) + + +def test_sympy__functions__elementary__trigonometric__sin(): + assert _test_args(sin(2)) + + +def test_sympy__functions__elementary__trigonometric__sinc(): + from sympy.functions.elementary.trigonometric import sinc + assert _test_args(sinc(2)) + + +def test_sympy__functions__elementary__trigonometric__sec(): + from sympy.functions.elementary.trigonometric import sec + assert _test_args(sec(2)) + + +def test_sympy__functions__elementary__trigonometric__tan(): + from sympy.functions.elementary.trigonometric import tan + assert _test_args(tan(2)) + + +@SKIP("abstract class") +def test_sympy__functions__special__bessel__BesselBase(): + pass + + +@SKIP("abstract class") +def test_sympy__functions__special__bessel__SphericalBesselBase(): + pass + + +@SKIP("abstract class") +def test_sympy__functions__special__bessel__SphericalHankelBase(): + pass + + +def test_sympy__functions__special__bessel__besseli(): + from sympy.functions.special.bessel import besseli + assert _test_args(besseli(x, 1)) + + +def test_sympy__functions__special__bessel__besselj(): + from sympy.functions.special.bessel import besselj + assert _test_args(besselj(x, 1)) + + +def test_sympy__functions__special__bessel__besselk(): + from sympy.functions.special.bessel import besselk + assert _test_args(besselk(x, 1)) + + +def test_sympy__functions__special__bessel__bessely(): + from sympy.functions.special.bessel import bessely + assert _test_args(bessely(x, 1)) + + +def test_sympy__functions__special__bessel__hankel1(): + from sympy.functions.special.bessel import hankel1 + assert _test_args(hankel1(x, 1)) + + +def test_sympy__functions__special__bessel__hankel2(): + from sympy.functions.special.bessel import hankel2 + assert _test_args(hankel2(x, 1)) + + +def test_sympy__functions__special__bessel__jn(): + from sympy.functions.special.bessel import jn + assert _test_args(jn(0, x)) + + +def test_sympy__functions__special__bessel__yn(): + from sympy.functions.special.bessel import yn + assert _test_args(yn(0, x)) + + +def test_sympy__functions__special__bessel__hn1(): + from sympy.functions.special.bessel import hn1 + assert _test_args(hn1(0, x)) + + +def test_sympy__functions__special__bessel__hn2(): + from sympy.functions.special.bessel import hn2 + assert _test_args(hn2(0, x)) + + +def test_sympy__functions__special__bessel__AiryBase(): + pass + + +def test_sympy__functions__special__bessel__airyai(): + from sympy.functions.special.bessel import airyai + assert _test_args(airyai(2)) + + +def test_sympy__functions__special__bessel__airybi(): + from sympy.functions.special.bessel import airybi + assert _test_args(airybi(2)) + + +def test_sympy__functions__special__bessel__airyaiprime(): + from sympy.functions.special.bessel import airyaiprime + assert _test_args(airyaiprime(2)) + + +def test_sympy__functions__special__bessel__airybiprime(): + from sympy.functions.special.bessel import airybiprime + assert _test_args(airybiprime(2)) + + +def test_sympy__functions__special__bessel__marcumq(): + from sympy.functions.special.bessel import marcumq + assert _test_args(marcumq(x, y, z)) + + +def test_sympy__functions__special__elliptic_integrals__elliptic_k(): + from sympy.functions.special.elliptic_integrals import elliptic_k as K + assert _test_args(K(x)) + + +def test_sympy__functions__special__elliptic_integrals__elliptic_f(): + from sympy.functions.special.elliptic_integrals import elliptic_f as F + assert _test_args(F(x, y)) + + +def test_sympy__functions__special__elliptic_integrals__elliptic_e(): + from sympy.functions.special.elliptic_integrals import elliptic_e as E + assert _test_args(E(x)) + assert _test_args(E(x, y)) + + +def test_sympy__functions__special__elliptic_integrals__elliptic_pi(): + from sympy.functions.special.elliptic_integrals import elliptic_pi as P + assert _test_args(P(x, y)) + assert _test_args(P(x, y, z)) + + +def test_sympy__functions__special__delta_functions__DiracDelta(): + from sympy.functions.special.delta_functions import DiracDelta + assert _test_args(DiracDelta(x, 1)) + + +def test_sympy__functions__special__singularity_functions__SingularityFunction(): + from sympy.functions.special.singularity_functions import SingularityFunction + assert _test_args(SingularityFunction(x, y, z)) + + +def test_sympy__functions__special__delta_functions__Heaviside(): + from sympy.functions.special.delta_functions import Heaviside + assert _test_args(Heaviside(x)) + + +def test_sympy__functions__special__error_functions__erf(): + from sympy.functions.special.error_functions import erf + assert _test_args(erf(2)) + +def test_sympy__functions__special__error_functions__erfc(): + from sympy.functions.special.error_functions import erfc + assert _test_args(erfc(2)) + +def test_sympy__functions__special__error_functions__erfi(): + from sympy.functions.special.error_functions import erfi + assert _test_args(erfi(2)) + +def test_sympy__functions__special__error_functions__erf2(): + from sympy.functions.special.error_functions import erf2 + assert _test_args(erf2(2, 3)) + +def test_sympy__functions__special__error_functions__erfinv(): + from sympy.functions.special.error_functions import erfinv + assert _test_args(erfinv(2)) + +def test_sympy__functions__special__error_functions__erfcinv(): + from sympy.functions.special.error_functions import erfcinv + assert _test_args(erfcinv(2)) + +def test_sympy__functions__special__error_functions__erf2inv(): + from sympy.functions.special.error_functions import erf2inv + assert _test_args(erf2inv(2, 3)) + +@SKIP("abstract class") +def test_sympy__functions__special__error_functions__FresnelIntegral(): + pass + + +def test_sympy__functions__special__error_functions__fresnels(): + from sympy.functions.special.error_functions import fresnels + assert _test_args(fresnels(2)) + + +def test_sympy__functions__special__error_functions__fresnelc(): + from sympy.functions.special.error_functions import fresnelc + assert _test_args(fresnelc(2)) + + +def test_sympy__functions__special__error_functions__erfs(): + from sympy.functions.special.error_functions import _erfs + assert _test_args(_erfs(2)) + + +def test_sympy__functions__special__error_functions__Ei(): + from sympy.functions.special.error_functions import Ei + assert _test_args(Ei(2)) + + +def test_sympy__functions__special__error_functions__li(): + from sympy.functions.special.error_functions import li + assert _test_args(li(2)) + + +def test_sympy__functions__special__error_functions__Li(): + from sympy.functions.special.error_functions import Li + assert _test_args(Li(5)) + + +@SKIP("abstract class") +def test_sympy__functions__special__error_functions__TrigonometricIntegral(): + pass + + +def test_sympy__functions__special__error_functions__Si(): + from sympy.functions.special.error_functions import Si + assert _test_args(Si(2)) + + +def test_sympy__functions__special__error_functions__Ci(): + from sympy.functions.special.error_functions import Ci + assert _test_args(Ci(2)) + + +def test_sympy__functions__special__error_functions__Shi(): + from sympy.functions.special.error_functions import Shi + assert _test_args(Shi(2)) + + +def test_sympy__functions__special__error_functions__Chi(): + from sympy.functions.special.error_functions import Chi + assert _test_args(Chi(2)) + + +def test_sympy__functions__special__error_functions__expint(): + from sympy.functions.special.error_functions import expint + assert _test_args(expint(y, x)) + + +def test_sympy__functions__special__gamma_functions__gamma(): + from sympy.functions.special.gamma_functions import gamma + assert _test_args(gamma(x)) + + +def test_sympy__functions__special__gamma_functions__loggamma(): + from sympy.functions.special.gamma_functions import loggamma + assert _test_args(loggamma(x)) + + +def test_sympy__functions__special__gamma_functions__lowergamma(): + from sympy.functions.special.gamma_functions import lowergamma + assert _test_args(lowergamma(x, 2)) + + +def test_sympy__functions__special__gamma_functions__polygamma(): + from sympy.functions.special.gamma_functions import polygamma + assert _test_args(polygamma(x, 2)) + +def test_sympy__functions__special__gamma_functions__digamma(): + from sympy.functions.special.gamma_functions import digamma + assert _test_args(digamma(x)) + +def test_sympy__functions__special__gamma_functions__trigamma(): + from sympy.functions.special.gamma_functions import trigamma + assert _test_args(trigamma(x)) + +def test_sympy__functions__special__gamma_functions__uppergamma(): + from sympy.functions.special.gamma_functions import uppergamma + assert _test_args(uppergamma(x, 2)) + +def test_sympy__functions__special__gamma_functions__multigamma(): + from sympy.functions.special.gamma_functions import multigamma + assert _test_args(multigamma(x, 1)) + + +def test_sympy__functions__special__beta_functions__beta(): + from sympy.functions.special.beta_functions import beta + assert _test_args(beta(x)) + assert _test_args(beta(x, x)) + +def test_sympy__functions__special__beta_functions__betainc(): + from sympy.functions.special.beta_functions import betainc + assert _test_args(betainc(a, b, x, y)) + +def test_sympy__functions__special__beta_functions__betainc_regularized(): + from sympy.functions.special.beta_functions import betainc_regularized + assert _test_args(betainc_regularized(a, b, x, y)) + + +def test_sympy__functions__special__mathieu_functions__MathieuBase(): + pass + + +def test_sympy__functions__special__mathieu_functions__mathieus(): + from sympy.functions.special.mathieu_functions import mathieus + assert _test_args(mathieus(1, 1, 1)) + + +def test_sympy__functions__special__mathieu_functions__mathieuc(): + from sympy.functions.special.mathieu_functions import mathieuc + assert _test_args(mathieuc(1, 1, 1)) + + +def test_sympy__functions__special__mathieu_functions__mathieusprime(): + from sympy.functions.special.mathieu_functions import mathieusprime + assert _test_args(mathieusprime(1, 1, 1)) + + +def test_sympy__functions__special__mathieu_functions__mathieucprime(): + from sympy.functions.special.mathieu_functions import mathieucprime + assert _test_args(mathieucprime(1, 1, 1)) + + +@SKIP("abstract class") +def test_sympy__functions__special__hyper__TupleParametersBase(): + pass + + +@SKIP("abstract class") +def test_sympy__functions__special__hyper__TupleArg(): + pass + + +def test_sympy__functions__special__hyper__hyper(): + from sympy.functions.special.hyper import hyper + assert _test_args(hyper([1, 2, 3], [4, 5], x)) + + +def test_sympy__functions__special__hyper__meijerg(): + from sympy.functions.special.hyper import meijerg + assert _test_args(meijerg([1, 2, 3], [4, 5], [6], [], x)) + + +@SKIP("abstract class") +def test_sympy__functions__special__hyper__HyperRep(): + pass + + +def test_sympy__functions__special__hyper__HyperRep_power1(): + from sympy.functions.special.hyper import HyperRep_power1 + assert _test_args(HyperRep_power1(x, y)) + + +def test_sympy__functions__special__hyper__HyperRep_power2(): + from sympy.functions.special.hyper import HyperRep_power2 + assert _test_args(HyperRep_power2(x, y)) + + +def test_sympy__functions__special__hyper__HyperRep_log1(): + from sympy.functions.special.hyper import HyperRep_log1 + assert _test_args(HyperRep_log1(x)) + + +def test_sympy__functions__special__hyper__HyperRep_atanh(): + from sympy.functions.special.hyper import HyperRep_atanh + assert _test_args(HyperRep_atanh(x)) + + +def test_sympy__functions__special__hyper__HyperRep_asin1(): + from sympy.functions.special.hyper import HyperRep_asin1 + assert _test_args(HyperRep_asin1(x)) + + +def test_sympy__functions__special__hyper__HyperRep_asin2(): + from sympy.functions.special.hyper import HyperRep_asin2 + assert _test_args(HyperRep_asin2(x)) + + +def test_sympy__functions__special__hyper__HyperRep_sqrts1(): + from sympy.functions.special.hyper import HyperRep_sqrts1 + assert _test_args(HyperRep_sqrts1(x, y)) + + +def test_sympy__functions__special__hyper__HyperRep_sqrts2(): + from sympy.functions.special.hyper import HyperRep_sqrts2 + assert _test_args(HyperRep_sqrts2(x, y)) + + +def test_sympy__functions__special__hyper__HyperRep_log2(): + from sympy.functions.special.hyper import HyperRep_log2 + assert _test_args(HyperRep_log2(x)) + + +def test_sympy__functions__special__hyper__HyperRep_cosasin(): + from sympy.functions.special.hyper import HyperRep_cosasin + assert _test_args(HyperRep_cosasin(x, y)) + + +def test_sympy__functions__special__hyper__HyperRep_sinasin(): + from sympy.functions.special.hyper import HyperRep_sinasin + assert _test_args(HyperRep_sinasin(x, y)) + +def test_sympy__functions__special__hyper__appellf1(): + from sympy.functions.special.hyper import appellf1 + a, b1, b2, c, x, y = symbols('a b1 b2 c x y') + assert _test_args(appellf1(a, b1, b2, c, x, y)) + +@SKIP("abstract class") +def test_sympy__functions__special__polynomials__OrthogonalPolynomial(): + pass + + +def test_sympy__functions__special__polynomials__jacobi(): + from sympy.functions.special.polynomials import jacobi + assert _test_args(jacobi(x, y, 2, 2)) + + +def test_sympy__functions__special__polynomials__gegenbauer(): + from sympy.functions.special.polynomials import gegenbauer + assert _test_args(gegenbauer(x, 2, 2)) + + +def test_sympy__functions__special__polynomials__chebyshevt(): + from sympy.functions.special.polynomials import chebyshevt + assert _test_args(chebyshevt(x, 2)) + + +def test_sympy__functions__special__polynomials__chebyshevt_root(): + from sympy.functions.special.polynomials import chebyshevt_root + assert _test_args(chebyshevt_root(3, 2)) + + +def test_sympy__functions__special__polynomials__chebyshevu(): + from sympy.functions.special.polynomials import chebyshevu + assert _test_args(chebyshevu(x, 2)) + + +def test_sympy__functions__special__polynomials__chebyshevu_root(): + from sympy.functions.special.polynomials import chebyshevu_root + assert _test_args(chebyshevu_root(3, 2)) + + +def test_sympy__functions__special__polynomials__hermite(): + from sympy.functions.special.polynomials import hermite + assert _test_args(hermite(x, 2)) + + +def test_sympy__functions__special__polynomials__hermite_prob(): + from sympy.functions.special.polynomials import hermite_prob + assert _test_args(hermite_prob(x, 2)) + + +def test_sympy__functions__special__polynomials__legendre(): + from sympy.functions.special.polynomials import legendre + assert _test_args(legendre(x, 2)) + + +def test_sympy__functions__special__polynomials__assoc_legendre(): + from sympy.functions.special.polynomials import assoc_legendre + assert _test_args(assoc_legendre(x, 0, y)) + + +def test_sympy__functions__special__polynomials__laguerre(): + from sympy.functions.special.polynomials import laguerre + assert _test_args(laguerre(x, 2)) + + +def test_sympy__functions__special__polynomials__assoc_laguerre(): + from sympy.functions.special.polynomials import assoc_laguerre + assert _test_args(assoc_laguerre(x, 0, y)) + + +def test_sympy__functions__special__spherical_harmonics__Ynm(): + from sympy.functions.special.spherical_harmonics import Ynm + assert _test_args(Ynm(1, 1, x, y)) + + +def test_sympy__functions__special__spherical_harmonics__Znm(): + from sympy.functions.special.spherical_harmonics import Znm + assert _test_args(Znm(x, y, 1, 1)) + + +def test_sympy__functions__special__tensor_functions__LeviCivita(): + from sympy.functions.special.tensor_functions import LeviCivita + assert _test_args(LeviCivita(x, y, 2)) + + +def test_sympy__functions__special__tensor_functions__KroneckerDelta(): + from sympy.functions.special.tensor_functions import KroneckerDelta + assert _test_args(KroneckerDelta(x, y)) + + +def test_sympy__functions__special__zeta_functions__dirichlet_eta(): + from sympy.functions.special.zeta_functions import dirichlet_eta + assert _test_args(dirichlet_eta(x)) + + +def test_sympy__functions__special__zeta_functions__riemann_xi(): + from sympy.functions.special.zeta_functions import riemann_xi + assert _test_args(riemann_xi(x)) + + +def test_sympy__functions__special__zeta_functions__zeta(): + from sympy.functions.special.zeta_functions import zeta + assert _test_args(zeta(101)) + + +def test_sympy__functions__special__zeta_functions__lerchphi(): + from sympy.functions.special.zeta_functions import lerchphi + assert _test_args(lerchphi(x, y, z)) + + +def test_sympy__functions__special__zeta_functions__polylog(): + from sympy.functions.special.zeta_functions import polylog + assert _test_args(polylog(x, y)) + + +def test_sympy__functions__special__zeta_functions__stieltjes(): + from sympy.functions.special.zeta_functions import stieltjes + assert _test_args(stieltjes(x, y)) + + +def test_sympy__integrals__integrals__Integral(): + from sympy.integrals.integrals import Integral + assert _test_args(Integral(2, (x, 0, 1))) + + +def test_sympy__integrals__risch__NonElementaryIntegral(): + from sympy.integrals.risch import NonElementaryIntegral + assert _test_args(NonElementaryIntegral(exp(-x**2), x)) + + +@SKIP("abstract class") +def test_sympy__integrals__transforms__IntegralTransform(): + pass + + +def test_sympy__integrals__transforms__MellinTransform(): + from sympy.integrals.transforms import MellinTransform + assert _test_args(MellinTransform(2, x, y)) + + +def test_sympy__integrals__transforms__InverseMellinTransform(): + from sympy.integrals.transforms import InverseMellinTransform + assert _test_args(InverseMellinTransform(2, x, y, 0, 1)) + + +def test_sympy__integrals__laplace__LaplaceTransform(): + from sympy.integrals.laplace import LaplaceTransform + assert _test_args(LaplaceTransform(2, x, y)) + + +def test_sympy__integrals__laplace__InverseLaplaceTransform(): + from sympy.integrals.laplace import InverseLaplaceTransform + assert _test_args(InverseLaplaceTransform(2, x, y, 0)) + + +@SKIP("abstract class") +def test_sympy__integrals__transforms__FourierTypeTransform(): + pass + + +def test_sympy__integrals__transforms__InverseFourierTransform(): + from sympy.integrals.transforms import InverseFourierTransform + assert _test_args(InverseFourierTransform(2, x, y)) + + +def test_sympy__integrals__transforms__FourierTransform(): + from sympy.integrals.transforms import FourierTransform + assert _test_args(FourierTransform(2, x, y)) + + +@SKIP("abstract class") +def test_sympy__integrals__transforms__SineCosineTypeTransform(): + pass + + +def test_sympy__integrals__transforms__InverseSineTransform(): + from sympy.integrals.transforms import InverseSineTransform + assert _test_args(InverseSineTransform(2, x, y)) + + +def test_sympy__integrals__transforms__SineTransform(): + from sympy.integrals.transforms import SineTransform + assert _test_args(SineTransform(2, x, y)) + + +def test_sympy__integrals__transforms__InverseCosineTransform(): + from sympy.integrals.transforms import InverseCosineTransform + assert _test_args(InverseCosineTransform(2, x, y)) + + +def test_sympy__integrals__transforms__CosineTransform(): + from sympy.integrals.transforms import CosineTransform + assert _test_args(CosineTransform(2, x, y)) + + +@SKIP("abstract class") +def test_sympy__integrals__transforms__HankelTypeTransform(): + pass + + +def test_sympy__integrals__transforms__InverseHankelTransform(): + from sympy.integrals.transforms import InverseHankelTransform + assert _test_args(InverseHankelTransform(2, x, y, 0)) + + +def test_sympy__integrals__transforms__HankelTransform(): + from sympy.integrals.transforms import HankelTransform + assert _test_args(HankelTransform(2, x, y, 0)) + + +def test_sympy__liealgebras__cartan_type__Standard_Cartan(): + from sympy.liealgebras.cartan_type import Standard_Cartan + assert _test_args(Standard_Cartan("A", 2)) + +def test_sympy__liealgebras__weyl_group__WeylGroup(): + from sympy.liealgebras.weyl_group import WeylGroup + assert _test_args(WeylGroup("B4")) + +def test_sympy__liealgebras__root_system__RootSystem(): + from sympy.liealgebras.root_system import RootSystem + assert _test_args(RootSystem("A2")) + +def test_sympy__liealgebras__type_a__TypeA(): + from sympy.liealgebras.type_a import TypeA + assert _test_args(TypeA(2)) + +def test_sympy__liealgebras__type_b__TypeB(): + from sympy.liealgebras.type_b import TypeB + assert _test_args(TypeB(4)) + +def test_sympy__liealgebras__type_c__TypeC(): + from sympy.liealgebras.type_c import TypeC + assert _test_args(TypeC(4)) + +def test_sympy__liealgebras__type_d__TypeD(): + from sympy.liealgebras.type_d import TypeD + assert _test_args(TypeD(4)) + +def test_sympy__liealgebras__type_e__TypeE(): + from sympy.liealgebras.type_e import TypeE + assert _test_args(TypeE(6)) + +def test_sympy__liealgebras__type_f__TypeF(): + from sympy.liealgebras.type_f import TypeF + assert _test_args(TypeF(4)) + +def test_sympy__liealgebras__type_g__TypeG(): + from sympy.liealgebras.type_g import TypeG + assert _test_args(TypeG(2)) + + +def test_sympy__logic__boolalg__And(): + from sympy.logic.boolalg import And + assert _test_args(And(x, y, 1)) + + +@SKIP("abstract class") +def test_sympy__logic__boolalg__Boolean(): + pass + + +def test_sympy__logic__boolalg__BooleanFunction(): + from sympy.logic.boolalg import BooleanFunction + assert _test_args(BooleanFunction(1, 2, 3)) + +@SKIP("abstract class") +def test_sympy__logic__boolalg__BooleanAtom(): + pass + +def test_sympy__logic__boolalg__BooleanTrue(): + from sympy.logic.boolalg import true + assert _test_args(true) + +def test_sympy__logic__boolalg__BooleanFalse(): + from sympy.logic.boolalg import false + assert _test_args(false) + +def test_sympy__logic__boolalg__Equivalent(): + from sympy.logic.boolalg import Equivalent + assert _test_args(Equivalent(x, 2)) + + +def test_sympy__logic__boolalg__ITE(): + from sympy.logic.boolalg import ITE + assert _test_args(ITE(x, y, 1)) + + +def test_sympy__logic__boolalg__Implies(): + from sympy.logic.boolalg import Implies + assert _test_args(Implies(x, y)) + + +def test_sympy__logic__boolalg__Nand(): + from sympy.logic.boolalg import Nand + assert _test_args(Nand(x, y, 1)) + + +def test_sympy__logic__boolalg__Nor(): + from sympy.logic.boolalg import Nor + assert _test_args(Nor(x, y)) + + +def test_sympy__logic__boolalg__Not(): + from sympy.logic.boolalg import Not + assert _test_args(Not(x)) + + +def test_sympy__logic__boolalg__Or(): + from sympy.logic.boolalg import Or + assert _test_args(Or(x, y)) + + +def test_sympy__logic__boolalg__Xor(): + from sympy.logic.boolalg import Xor + assert _test_args(Xor(x, y, 2)) + +def test_sympy__logic__boolalg__Xnor(): + from sympy.logic.boolalg import Xnor + assert _test_args(Xnor(x, y, 2)) + +def test_sympy__logic__boolalg__Exclusive(): + from sympy.logic.boolalg import Exclusive + assert _test_args(Exclusive(x, y, z)) + + +def test_sympy__matrices__matrixbase__DeferredVector(): + from sympy.matrices.matrixbase import DeferredVector + assert _test_args(DeferredVector("X")) + + +@SKIP("abstract class") +def test_sympy__matrices__expressions__matexpr__MatrixBase(): + pass + + +@SKIP("abstract class") +def test_sympy__matrices__immutable__ImmutableRepMatrix(): + pass + + +def test_sympy__matrices__immutable__ImmutableDenseMatrix(): + from sympy.matrices.immutable import ImmutableDenseMatrix + m = ImmutableDenseMatrix([[1, 2], [3, 4]]) + assert _test_args(m) + assert _test_args(Basic(*list(m))) + m = ImmutableDenseMatrix(1, 1, [1]) + assert _test_args(m) + assert _test_args(Basic(*list(m))) + m = ImmutableDenseMatrix(2, 2, lambda i, j: 1) + assert m[0, 0] is S.One + m = ImmutableDenseMatrix(2, 2, lambda i, j: 1/(1 + i) + 1/(1 + j)) + assert m[1, 1] is S.One # true div. will give 1.0 if i,j not sympified + assert _test_args(m) + assert _test_args(Basic(*list(m))) + + +def test_sympy__matrices__immutable__ImmutableSparseMatrix(): + from sympy.matrices.immutable import ImmutableSparseMatrix + m = ImmutableSparseMatrix([[1, 2], [3, 4]]) + assert _test_args(m) + assert _test_args(Basic(*list(m))) + m = ImmutableSparseMatrix(1, 1, {(0, 0): 1}) + assert _test_args(m) + assert _test_args(Basic(*list(m))) + m = ImmutableSparseMatrix(1, 1, [1]) + assert _test_args(m) + assert _test_args(Basic(*list(m))) + m = ImmutableSparseMatrix(2, 2, lambda i, j: 1) + assert m[0, 0] is S.One + m = ImmutableSparseMatrix(2, 2, lambda i, j: 1/(1 + i) + 1/(1 + j)) + assert m[1, 1] is S.One # true div. will give 1.0 if i,j not sympified + assert _test_args(m) + assert _test_args(Basic(*list(m))) + + +def test_sympy__matrices__expressions__slice__MatrixSlice(): + from sympy.matrices.expressions.slice import MatrixSlice + from sympy.matrices.expressions import MatrixSymbol + X = MatrixSymbol('X', 4, 4) + assert _test_args(MatrixSlice(X, (0, 2), (0, 2))) + + +def test_sympy__matrices__expressions__applyfunc__ElementwiseApplyFunction(): + from sympy.matrices.expressions.applyfunc import ElementwiseApplyFunction + from sympy.matrices.expressions import MatrixSymbol + X = MatrixSymbol("X", x, x) + func = Lambda(x, x**2) + assert _test_args(ElementwiseApplyFunction(func, X)) + + +def test_sympy__matrices__expressions__blockmatrix__BlockDiagMatrix(): + from sympy.matrices.expressions.blockmatrix import BlockDiagMatrix + from sympy.matrices.expressions import MatrixSymbol + X = MatrixSymbol('X', x, x) + Y = MatrixSymbol('Y', y, y) + assert _test_args(BlockDiagMatrix(X, Y)) + + +def test_sympy__matrices__expressions__blockmatrix__BlockMatrix(): + from sympy.matrices.expressions.blockmatrix import BlockMatrix + from sympy.matrices.expressions import MatrixSymbol, ZeroMatrix + X = MatrixSymbol('X', x, x) + Y = MatrixSymbol('Y', y, y) + Z = MatrixSymbol('Z', x, y) + O = ZeroMatrix(y, x) + assert _test_args(BlockMatrix([[X, Z], [O, Y]])) + + +def test_sympy__matrices__expressions__inverse__Inverse(): + from sympy.matrices.expressions.inverse import Inverse + from sympy.matrices.expressions import MatrixSymbol + assert _test_args(Inverse(MatrixSymbol('A', 3, 3))) + + +def test_sympy__matrices__expressions__matadd__MatAdd(): + from sympy.matrices.expressions.matadd import MatAdd + from sympy.matrices.expressions import MatrixSymbol + X = MatrixSymbol('X', x, y) + Y = MatrixSymbol('Y', x, y) + assert _test_args(MatAdd(X, Y)) + + +@SKIP("abstract class") +def test_sympy__matrices__expressions__matexpr__MatrixExpr(): + pass + +def test_sympy__matrices__expressions__matexpr__MatrixElement(): + from sympy.matrices.expressions.matexpr import MatrixSymbol, MatrixElement + from sympy.core.singleton import S + assert _test_args(MatrixElement(MatrixSymbol('A', 3, 5), S(2), S(3))) + +def test_sympy__matrices__expressions__matexpr__MatrixSymbol(): + from sympy.matrices.expressions.matexpr import MatrixSymbol + assert _test_args(MatrixSymbol('A', 3, 5)) + + +def test_sympy__matrices__expressions__special__OneMatrix(): + from sympy.matrices.expressions.special import OneMatrix + assert _test_args(OneMatrix(3, 5)) + + +def test_sympy__matrices__expressions__special__ZeroMatrix(): + from sympy.matrices.expressions.special import ZeroMatrix + assert _test_args(ZeroMatrix(3, 5)) + + +def test_sympy__matrices__expressions__special__GenericZeroMatrix(): + from sympy.matrices.expressions.special import GenericZeroMatrix + assert _test_args(GenericZeroMatrix()) + + +def test_sympy__matrices__expressions__special__Identity(): + from sympy.matrices.expressions.special import Identity + assert _test_args(Identity(3)) + + +def test_sympy__matrices__expressions__special__GenericIdentity(): + from sympy.matrices.expressions.special import GenericIdentity + assert _test_args(GenericIdentity()) + + +def test_sympy__matrices__expressions__sets__MatrixSet(): + from sympy.matrices.expressions.sets import MatrixSet + from sympy.core.singleton import S + assert _test_args(MatrixSet(2, 2, S.Reals)) + +def test_sympy__matrices__expressions__matmul__MatMul(): + from sympy.matrices.expressions.matmul import MatMul + from sympy.matrices.expressions import MatrixSymbol + X = MatrixSymbol('X', x, y) + Y = MatrixSymbol('Y', y, x) + assert _test_args(MatMul(X, Y)) + + +def test_sympy__matrices__expressions__dotproduct__DotProduct(): + from sympy.matrices.expressions.dotproduct import DotProduct + from sympy.matrices.expressions import MatrixSymbol + X = MatrixSymbol('X', x, 1) + Y = MatrixSymbol('Y', x, 1) + assert _test_args(DotProduct(X, Y)) + +def test_sympy__matrices__expressions__diagonal__DiagonalMatrix(): + from sympy.matrices.expressions.diagonal import DiagonalMatrix + from sympy.matrices.expressions import MatrixSymbol + x = MatrixSymbol('x', 10, 1) + assert _test_args(DiagonalMatrix(x)) + +def test_sympy__matrices__expressions__diagonal__DiagonalOf(): + from sympy.matrices.expressions.diagonal import DiagonalOf + from sympy.matrices.expressions import MatrixSymbol + X = MatrixSymbol('x', 10, 10) + assert _test_args(DiagonalOf(X)) + +def test_sympy__matrices__expressions__diagonal__DiagMatrix(): + from sympy.matrices.expressions.diagonal import DiagMatrix + from sympy.matrices.expressions import MatrixSymbol + x = MatrixSymbol('x', 10, 1) + assert _test_args(DiagMatrix(x)) + +def test_sympy__matrices__expressions__hadamard__HadamardProduct(): + from sympy.matrices.expressions.hadamard import HadamardProduct + from sympy.matrices.expressions import MatrixSymbol + X = MatrixSymbol('X', x, y) + Y = MatrixSymbol('Y', x, y) + assert _test_args(HadamardProduct(X, Y)) + +def test_sympy__matrices__expressions__hadamard__HadamardPower(): + from sympy.matrices.expressions.hadamard import HadamardPower + from sympy.matrices.expressions import MatrixSymbol + from sympy.core.symbol import Symbol + X = MatrixSymbol('X', x, y) + n = Symbol("n") + assert _test_args(HadamardPower(X, n)) + +def test_sympy__matrices__expressions__kronecker__KroneckerProduct(): + from sympy.matrices.expressions.kronecker import KroneckerProduct + from sympy.matrices.expressions import MatrixSymbol + X = MatrixSymbol('X', x, y) + Y = MatrixSymbol('Y', x, y) + assert _test_args(KroneckerProduct(X, Y)) + + +def test_sympy__matrices__expressions__matpow__MatPow(): + from sympy.matrices.expressions.matpow import MatPow + from sympy.matrices.expressions import MatrixSymbol + X = MatrixSymbol('X', x, x) + assert _test_args(MatPow(X, 2)) + + +def test_sympy__matrices__expressions__transpose__Transpose(): + from sympy.matrices.expressions.transpose import Transpose + from sympy.matrices.expressions import MatrixSymbol + assert _test_args(Transpose(MatrixSymbol('A', 3, 5))) + + +def test_sympy__matrices__expressions__adjoint__Adjoint(): + from sympy.matrices.expressions.adjoint import Adjoint + from sympy.matrices.expressions import MatrixSymbol + assert _test_args(Adjoint(MatrixSymbol('A', 3, 5))) + + +def test_sympy__matrices__expressions__trace__Trace(): + from sympy.matrices.expressions.trace import Trace + from sympy.matrices.expressions import MatrixSymbol + assert _test_args(Trace(MatrixSymbol('A', 3, 3))) + +def test_sympy__matrices__expressions__determinant__Determinant(): + from sympy.matrices.expressions.determinant import Determinant + from sympy.matrices.expressions import MatrixSymbol + assert _test_args(Determinant(MatrixSymbol('A', 3, 3))) + +def test_sympy__matrices__expressions__determinant__Permanent(): + from sympy.matrices.expressions.determinant import Permanent + from sympy.matrices.expressions import MatrixSymbol + assert _test_args(Permanent(MatrixSymbol('A', 3, 4))) + +def test_sympy__matrices__expressions__funcmatrix__FunctionMatrix(): + from sympy.matrices.expressions.funcmatrix import FunctionMatrix + from sympy.core.symbol import symbols + i, j = symbols('i,j') + assert _test_args(FunctionMatrix(3, 3, Lambda((i, j), i - j) )) + +def test_sympy__matrices__expressions__fourier__DFT(): + from sympy.matrices.expressions.fourier import DFT + from sympy.core.singleton import S + assert _test_args(DFT(S(2))) + +def test_sympy__matrices__expressions__fourier__IDFT(): + from sympy.matrices.expressions.fourier import IDFT + from sympy.core.singleton import S + assert _test_args(IDFT(S(2))) + +from sympy.matrices.expressions import MatrixSymbol +X = MatrixSymbol('X', 10, 10) + +def test_sympy__matrices__expressions__factorizations__LofLU(): + from sympy.matrices.expressions.factorizations import LofLU + assert _test_args(LofLU(X)) + +def test_sympy__matrices__expressions__factorizations__UofLU(): + from sympy.matrices.expressions.factorizations import UofLU + assert _test_args(UofLU(X)) + +def test_sympy__matrices__expressions__factorizations__QofQR(): + from sympy.matrices.expressions.factorizations import QofQR + assert _test_args(QofQR(X)) + +def test_sympy__matrices__expressions__factorizations__RofQR(): + from sympy.matrices.expressions.factorizations import RofQR + assert _test_args(RofQR(X)) + +def test_sympy__matrices__expressions__factorizations__LofCholesky(): + from sympy.matrices.expressions.factorizations import LofCholesky + assert _test_args(LofCholesky(X)) + +def test_sympy__matrices__expressions__factorizations__UofCholesky(): + from sympy.matrices.expressions.factorizations import UofCholesky + assert _test_args(UofCholesky(X)) + +def test_sympy__matrices__expressions__factorizations__EigenVectors(): + from sympy.matrices.expressions.factorizations import EigenVectors + assert _test_args(EigenVectors(X)) + +def test_sympy__matrices__expressions__factorizations__EigenValues(): + from sympy.matrices.expressions.factorizations import EigenValues + assert _test_args(EigenValues(X)) + +def test_sympy__matrices__expressions__factorizations__UofSVD(): + from sympy.matrices.expressions.factorizations import UofSVD + assert _test_args(UofSVD(X)) + +def test_sympy__matrices__expressions__factorizations__VofSVD(): + from sympy.matrices.expressions.factorizations import VofSVD + assert _test_args(VofSVD(X)) + +def test_sympy__matrices__expressions__factorizations__SofSVD(): + from sympy.matrices.expressions.factorizations import SofSVD + assert _test_args(SofSVD(X)) + +@SKIP("abstract class") +def test_sympy__matrices__expressions__factorizations__Factorization(): + pass + +def test_sympy__matrices__expressions__permutation__PermutationMatrix(): + from sympy.combinatorics import Permutation + from sympy.matrices.expressions.permutation import PermutationMatrix + assert _test_args(PermutationMatrix(Permutation([2, 0, 1]))) + +def test_sympy__matrices__expressions__permutation__MatrixPermute(): + from sympy.combinatorics import Permutation + from sympy.matrices.expressions.matexpr import MatrixSymbol + from sympy.matrices.expressions.permutation import MatrixPermute + A = MatrixSymbol('A', 3, 3) + assert _test_args(MatrixPermute(A, Permutation([2, 0, 1]))) + +def test_sympy__matrices__expressions__companion__CompanionMatrix(): + from sympy.core.symbol import Symbol + from sympy.matrices.expressions.companion import CompanionMatrix + from sympy.polys.polytools import Poly + + x = Symbol('x') + p = Poly([1, 2, 3], x) + assert _test_args(CompanionMatrix(p)) + +def test_sympy__physics__vector__frame__CoordinateSym(): + from sympy.physics.vector import CoordinateSym + from sympy.physics.vector import ReferenceFrame + assert _test_args(CoordinateSym('R_x', ReferenceFrame('R'), 0)) + + +@SKIP("abstract class") +def test_sympy__physics__biomechanics__curve__CharacteristicCurveFunction(): + pass + + +def test_sympy__physics__biomechanics__curve__TendonForceLengthDeGroote2016(): + from sympy.physics.biomechanics import TendonForceLengthDeGroote2016 + l_T_tilde, c0, c1, c2, c3 = symbols('l_T_tilde, c0, c1, c2, c3') + assert _test_args(TendonForceLengthDeGroote2016(l_T_tilde, c0, c1, c2, c3)) + + +def test_sympy__physics__biomechanics__curve__TendonForceLengthInverseDeGroote2016(): + from sympy.physics.biomechanics import TendonForceLengthInverseDeGroote2016 + fl_T, c0, c1, c2, c3 = symbols('fl_T, c0, c1, c2, c3') + assert _test_args(TendonForceLengthInverseDeGroote2016(fl_T, c0, c1, c2, c3)) + + +def test_sympy__physics__biomechanics__curve__FiberForceLengthPassiveDeGroote2016(): + from sympy.physics.biomechanics import FiberForceLengthPassiveDeGroote2016 + l_M_tilde, c0, c1 = symbols('l_M_tilde, c0, c1') + assert _test_args(FiberForceLengthPassiveDeGroote2016(l_M_tilde, c0, c1)) + + +def test_sympy__physics__biomechanics__curve__FiberForceLengthPassiveInverseDeGroote2016(): + from sympy.physics.biomechanics import FiberForceLengthPassiveInverseDeGroote2016 + fl_M_pas, c0, c1 = symbols('fl_M_pas, c0, c1') + assert _test_args(FiberForceLengthPassiveInverseDeGroote2016(fl_M_pas, c0, c1)) + + +def test_sympy__physics__biomechanics__curve__FiberForceLengthActiveDeGroote2016(): + from sympy.physics.biomechanics import FiberForceLengthActiveDeGroote2016 + l_M_tilde, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11 = symbols('l_M_tilde, c0:12') + assert _test_args(FiberForceLengthActiveDeGroote2016(l_M_tilde, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11)) + + +def test_sympy__physics__biomechanics__curve__FiberForceVelocityDeGroote2016(): + from sympy.physics.biomechanics import FiberForceVelocityDeGroote2016 + v_M_tilde, c0, c1, c2, c3 = symbols('v_M_tilde, c0, c1, c2, c3') + assert _test_args(FiberForceVelocityDeGroote2016(v_M_tilde, c0, c1, c2, c3)) + + +def test_sympy__physics__biomechanics__curve__FiberForceVelocityInverseDeGroote2016(): + from sympy.physics.biomechanics import FiberForceVelocityInverseDeGroote2016 + fv_M, c0, c1, c2, c3 = symbols('fv_M, c0, c1, c2, c3') + assert _test_args(FiberForceVelocityInverseDeGroote2016(fv_M, c0, c1, c2, c3)) + + +def test_sympy__physics__paulialgebra__Pauli(): + from sympy.physics.paulialgebra import Pauli + assert _test_args(Pauli(1)) + + +def test_sympy__physics__quantum__anticommutator__AntiCommutator(): + from sympy.physics.quantum.anticommutator import AntiCommutator + assert _test_args(AntiCommutator(x, y)) + + +def test_sympy__physics__quantum__cartesian__PositionBra3D(): + from sympy.physics.quantum.cartesian import PositionBra3D + assert _test_args(PositionBra3D(x, y, z)) + + +def test_sympy__physics__quantum__cartesian__PositionKet3D(): + from sympy.physics.quantum.cartesian import PositionKet3D + assert _test_args(PositionKet3D(x, y, z)) + + +def test_sympy__physics__quantum__cartesian__PositionState3D(): + from sympy.physics.quantum.cartesian import PositionState3D + assert _test_args(PositionState3D(x, y, z)) + + +def test_sympy__physics__quantum__cartesian__PxBra(): + from sympy.physics.quantum.cartesian import PxBra + assert _test_args(PxBra(x, y, z)) + + +def test_sympy__physics__quantum__cartesian__PxKet(): + from sympy.physics.quantum.cartesian import PxKet + assert _test_args(PxKet(x, y, z)) + + +def test_sympy__physics__quantum__cartesian__PxOp(): + from sympy.physics.quantum.cartesian import PxOp + assert _test_args(PxOp(x, y, z)) + + +def test_sympy__physics__quantum__cartesian__XBra(): + from sympy.physics.quantum.cartesian import XBra + assert _test_args(XBra(x)) + + +def test_sympy__physics__quantum__cartesian__XKet(): + from sympy.physics.quantum.cartesian import XKet + assert _test_args(XKet(x)) + + +def test_sympy__physics__quantum__cartesian__XOp(): + from sympy.physics.quantum.cartesian import XOp + assert _test_args(XOp(x)) + + +def test_sympy__physics__quantum__cartesian__YOp(): + from sympy.physics.quantum.cartesian import YOp + assert _test_args(YOp(x)) + + +def test_sympy__physics__quantum__cartesian__ZOp(): + from sympy.physics.quantum.cartesian import ZOp + assert _test_args(ZOp(x)) + + +def test_sympy__physics__quantum__cg__CG(): + from sympy.physics.quantum.cg import CG + from sympy.core.singleton import S + assert _test_args(CG(Rational(3, 2), Rational(3, 2), S.Half, Rational(-1, 2), 1, 1)) + + +def test_sympy__physics__quantum__cg__Wigner3j(): + from sympy.physics.quantum.cg import Wigner3j + assert _test_args(Wigner3j(6, 0, 4, 0, 2, 0)) + + +def test_sympy__physics__quantum__cg__Wigner6j(): + from sympy.physics.quantum.cg import Wigner6j + assert _test_args(Wigner6j(1, 2, 3, 2, 1, 2)) + + +def test_sympy__physics__quantum__cg__Wigner9j(): + from sympy.physics.quantum.cg import Wigner9j + assert _test_args(Wigner9j(2, 1, 1, Rational(3, 2), S.Half, 1, S.Half, S.Half, 0)) + +def test_sympy__physics__quantum__circuitplot__Mz(): + from sympy.physics.quantum.circuitplot import Mz + assert _test_args(Mz(0)) + +def test_sympy__physics__quantum__circuitplot__Mx(): + from sympy.physics.quantum.circuitplot import Mx + assert _test_args(Mx(0)) + +def test_sympy__physics__quantum__commutator__Commutator(): + from sympy.physics.quantum.commutator import Commutator + A, B = symbols('A,B', commutative=False) + assert _test_args(Commutator(A, B)) + + +def test_sympy__physics__quantum__constants__HBar(): + from sympy.physics.quantum.constants import HBar + assert _test_args(HBar()) + + +def test_sympy__physics__quantum__dagger__Dagger(): + from sympy.physics.quantum.dagger import Dagger + from sympy.physics.quantum.state import Ket + assert _test_args(Dagger(Dagger(Ket('psi')))) + + +def test_sympy__physics__quantum__gate__CGate(): + from sympy.physics.quantum.gate import CGate, Gate + assert _test_args(CGate((0, 1), Gate(2))) + + +def test_sympy__physics__quantum__gate__CGateS(): + from sympy.physics.quantum.gate import CGateS, Gate + assert _test_args(CGateS((0, 1), Gate(2))) + + +def test_sympy__physics__quantum__gate__CNotGate(): + from sympy.physics.quantum.gate import CNotGate + assert _test_args(CNotGate(0, 1)) + + +def test_sympy__physics__quantum__gate__Gate(): + from sympy.physics.quantum.gate import Gate + assert _test_args(Gate(0)) + + +def test_sympy__physics__quantum__gate__HadamardGate(): + from sympy.physics.quantum.gate import HadamardGate + assert _test_args(HadamardGate(0)) + + +def test_sympy__physics__quantum__gate__IdentityGate(): + from sympy.physics.quantum.gate import IdentityGate + assert _test_args(IdentityGate(0)) + + +def test_sympy__physics__quantum__gate__OneQubitGate(): + from sympy.physics.quantum.gate import OneQubitGate + assert _test_args(OneQubitGate(0)) + + +def test_sympy__physics__quantum__gate__PhaseGate(): + from sympy.physics.quantum.gate import PhaseGate + assert _test_args(PhaseGate(0)) + + +def test_sympy__physics__quantum__gate__SwapGate(): + from sympy.physics.quantum.gate import SwapGate + assert _test_args(SwapGate(0, 1)) + + +def test_sympy__physics__quantum__gate__TGate(): + from sympy.physics.quantum.gate import TGate + assert _test_args(TGate(0)) + + +def test_sympy__physics__quantum__gate__TwoQubitGate(): + from sympy.physics.quantum.gate import TwoQubitGate + assert _test_args(TwoQubitGate(0)) + + +def test_sympy__physics__quantum__gate__UGate(): + from sympy.physics.quantum.gate import UGate + from sympy.matrices.immutable import ImmutableDenseMatrix + from sympy.core.containers import Tuple + from sympy.core.numbers import Integer + assert _test_args( + UGate(Tuple(Integer(1)), ImmutableDenseMatrix([[1, 0], [0, 2]]))) + + +def test_sympy__physics__quantum__gate__XGate(): + from sympy.physics.quantum.gate import XGate + assert _test_args(XGate(0)) + + +def test_sympy__physics__quantum__gate__YGate(): + from sympy.physics.quantum.gate import YGate + assert _test_args(YGate(0)) + + +def test_sympy__physics__quantum__gate__ZGate(): + from sympy.physics.quantum.gate import ZGate + assert _test_args(ZGate(0)) + + +def test_sympy__physics__quantum__grover__OracleGateFunction(): + from sympy.physics.quantum.grover import OracleGateFunction + @OracleGateFunction + def f(qubit): + return + assert _test_args(f) + +def test_sympy__physics__quantum__grover__OracleGate(): + from sympy.physics.quantum.grover import OracleGate + def f(qubit): + return + assert _test_args(OracleGate(1,f)) + + +def test_sympy__physics__quantum__grover__WGate(): + from sympy.physics.quantum.grover import WGate + assert _test_args(WGate(1)) + + +def test_sympy__physics__quantum__hilbert__ComplexSpace(): + from sympy.physics.quantum.hilbert import ComplexSpace + assert _test_args(ComplexSpace(x)) + + +def test_sympy__physics__quantum__hilbert__DirectSumHilbertSpace(): + from sympy.physics.quantum.hilbert import DirectSumHilbertSpace, ComplexSpace, FockSpace + c = ComplexSpace(2) + f = FockSpace() + assert _test_args(DirectSumHilbertSpace(c, f)) + + +def test_sympy__physics__quantum__hilbert__FockSpace(): + from sympy.physics.quantum.hilbert import FockSpace + assert _test_args(FockSpace()) + + +def test_sympy__physics__quantum__hilbert__HilbertSpace(): + from sympy.physics.quantum.hilbert import HilbertSpace + assert _test_args(HilbertSpace()) + + +def test_sympy__physics__quantum__hilbert__L2(): + from sympy.physics.quantum.hilbert import L2 + from sympy.core.numbers import oo + from sympy.sets.sets import Interval + assert _test_args(L2(Interval(0, oo))) + + +def test_sympy__physics__quantum__hilbert__TensorPowerHilbertSpace(): + from sympy.physics.quantum.hilbert import TensorPowerHilbertSpace, FockSpace + f = FockSpace() + assert _test_args(TensorPowerHilbertSpace(f, 2)) + + +def test_sympy__physics__quantum__hilbert__TensorProductHilbertSpace(): + from sympy.physics.quantum.hilbert import TensorProductHilbertSpace, FockSpace, ComplexSpace + c = ComplexSpace(2) + f = FockSpace() + assert _test_args(TensorProductHilbertSpace(f, c)) + + +def test_sympy__physics__quantum__innerproduct__InnerProduct(): + from sympy.physics.quantum import Bra, Ket, InnerProduct + b = Bra('b') + k = Ket('k') + assert _test_args(InnerProduct(b, k)) + + +def test_sympy__physics__quantum__operator__DifferentialOperator(): + from sympy.physics.quantum.operator import DifferentialOperator + from sympy.core.function import (Derivative, Function) + f = Function('f') + assert _test_args(DifferentialOperator(1/x*Derivative(f(x), x), f(x))) + + +def test_sympy__physics__quantum__operator__HermitianOperator(): + from sympy.physics.quantum.operator import HermitianOperator + assert _test_args(HermitianOperator('H')) + + +def test_sympy__physics__quantum__operator__IdentityOperator(): + with warns_deprecated_sympy(): + from sympy.physics.quantum.operator import IdentityOperator + assert _test_args(IdentityOperator(5)) + + +def test_sympy__physics__quantum__operator__Operator(): + from sympy.physics.quantum.operator import Operator + assert _test_args(Operator('A')) + + +def test_sympy__physics__quantum__operator__OuterProduct(): + from sympy.physics.quantum.operator import OuterProduct + from sympy.physics.quantum import Ket, Bra + b = Bra('b') + k = Ket('k') + assert _test_args(OuterProduct(k, b)) + + +def test_sympy__physics__quantum__operator__UnitaryOperator(): + from sympy.physics.quantum.operator import UnitaryOperator + assert _test_args(UnitaryOperator('U')) + + +def test_sympy__physics__quantum__piab__PIABBra(): + from sympy.physics.quantum.piab import PIABBra + assert _test_args(PIABBra('B')) + + +def test_sympy__physics__quantum__boson__BosonOp(): + from sympy.physics.quantum.boson import BosonOp + assert _test_args(BosonOp('a')) + assert _test_args(BosonOp('a', False)) + + +def test_sympy__physics__quantum__boson__BosonFockKet(): + from sympy.physics.quantum.boson import BosonFockKet + assert _test_args(BosonFockKet(1)) + + +def test_sympy__physics__quantum__boson__BosonFockBra(): + from sympy.physics.quantum.boson import BosonFockBra + assert _test_args(BosonFockBra(1)) + + +def test_sympy__physics__quantum__boson__BosonCoherentKet(): + from sympy.physics.quantum.boson import BosonCoherentKet + assert _test_args(BosonCoherentKet(1)) + + +def test_sympy__physics__quantum__boson__BosonCoherentBra(): + from sympy.physics.quantum.boson import BosonCoherentBra + assert _test_args(BosonCoherentBra(1)) + + +def test_sympy__physics__quantum__fermion__FermionOp(): + from sympy.physics.quantum.fermion import FermionOp + assert _test_args(FermionOp('c')) + assert _test_args(FermionOp('c', False)) + + +def test_sympy__physics__quantum__fermion__FermionFockKet(): + from sympy.physics.quantum.fermion import FermionFockKet + assert _test_args(FermionFockKet(1)) + + +def test_sympy__physics__quantum__fermion__FermionFockBra(): + from sympy.physics.quantum.fermion import FermionFockBra + assert _test_args(FermionFockBra(1)) + + +def test_sympy__physics__quantum__pauli__SigmaOpBase(): + from sympy.physics.quantum.pauli import SigmaOpBase + assert _test_args(SigmaOpBase()) + + +def test_sympy__physics__quantum__pauli__SigmaX(): + from sympy.physics.quantum.pauli import SigmaX + assert _test_args(SigmaX()) + + +def test_sympy__physics__quantum__pauli__SigmaY(): + from sympy.physics.quantum.pauli import SigmaY + assert _test_args(SigmaY()) + + +def test_sympy__physics__quantum__pauli__SigmaZ(): + from sympy.physics.quantum.pauli import SigmaZ + assert _test_args(SigmaZ()) + + +def test_sympy__physics__quantum__pauli__SigmaMinus(): + from sympy.physics.quantum.pauli import SigmaMinus + assert _test_args(SigmaMinus()) + + +def test_sympy__physics__quantum__pauli__SigmaPlus(): + from sympy.physics.quantum.pauli import SigmaPlus + assert _test_args(SigmaPlus()) + + +def test_sympy__physics__quantum__pauli__SigmaZKet(): + from sympy.physics.quantum.pauli import SigmaZKet + assert _test_args(SigmaZKet(0)) + + +def test_sympy__physics__quantum__pauli__SigmaZBra(): + from sympy.physics.quantum.pauli import SigmaZBra + assert _test_args(SigmaZBra(0)) + + +def test_sympy__physics__quantum__piab__PIABHamiltonian(): + from sympy.physics.quantum.piab import PIABHamiltonian + assert _test_args(PIABHamiltonian('P')) + + +def test_sympy__physics__quantum__piab__PIABKet(): + from sympy.physics.quantum.piab import PIABKet + assert _test_args(PIABKet('K')) + + +def test_sympy__physics__quantum__qexpr__QExpr(): + from sympy.physics.quantum.qexpr import QExpr + assert _test_args(QExpr(0)) + + +def test_sympy__physics__quantum__qft__Fourier(): + from sympy.physics.quantum.qft import Fourier + assert _test_args(Fourier(0, 1)) + + +def test_sympy__physics__quantum__qft__IQFT(): + from sympy.physics.quantum.qft import IQFT + assert _test_args(IQFT(0, 1)) + + +def test_sympy__physics__quantum__qft__QFT(): + from sympy.physics.quantum.qft import QFT + assert _test_args(QFT(0, 1)) + + +def test_sympy__physics__quantum__qft__RkGate(): + from sympy.physics.quantum.qft import RkGate + assert _test_args(RkGate(0, 1)) + + +def test_sympy__physics__quantum__qubit__IntQubit(): + from sympy.physics.quantum.qubit import IntQubit + assert _test_args(IntQubit(0)) + + +def test_sympy__physics__quantum__qubit__IntQubitBra(): + from sympy.physics.quantum.qubit import IntQubitBra + assert _test_args(IntQubitBra(0)) + + +def test_sympy__physics__quantum__qubit__IntQubitState(): + from sympy.physics.quantum.qubit import IntQubitState, QubitState + assert _test_args(IntQubitState(QubitState(0, 1))) + + +def test_sympy__physics__quantum__qubit__Qubit(): + from sympy.physics.quantum.qubit import Qubit + assert _test_args(Qubit(0, 0, 0)) + + +def test_sympy__physics__quantum__qubit__QubitBra(): + from sympy.physics.quantum.qubit import QubitBra + assert _test_args(QubitBra('1', 0)) + + +def test_sympy__physics__quantum__qubit__QubitState(): + from sympy.physics.quantum.qubit import QubitState + assert _test_args(QubitState(0, 1)) + + +def test_sympy__physics__quantum__density__Density(): + from sympy.physics.quantum.density import Density + from sympy.physics.quantum.state import Ket + assert _test_args(Density([Ket(0), 0.5], [Ket(1), 0.5])) + + +@SKIP("TODO: sympy.physics.quantum.shor: Cmod Not Implemented") +def test_sympy__physics__quantum__shor__CMod(): + from sympy.physics.quantum.shor import CMod + assert _test_args(CMod()) + + +def test_sympy__physics__quantum__spin__CoupledSpinState(): + from sympy.physics.quantum.spin import CoupledSpinState + assert _test_args(CoupledSpinState(1, 0, (1, 1))) + assert _test_args(CoupledSpinState(1, 0, (1, S.Half, S.Half))) + assert _test_args(CoupledSpinState( + 1, 0, (1, S.Half, S.Half), ((2, 3, S.Half), (1, 2, 1)) )) + j, m, j1, j2, j3, j12, x = symbols('j m j1:4 j12 x') + assert CoupledSpinState( + j, m, (j1, j2, j3)).subs(j2, x) == CoupledSpinState(j, m, (j1, x, j3)) + assert CoupledSpinState(j, m, (j1, j2, j3), ((1, 3, j12), (1, 2, j)) ).subs(j12, x) == \ + CoupledSpinState(j, m, (j1, j2, j3), ((1, 3, x), (1, 2, j)) ) + + +def test_sympy__physics__quantum__spin__J2Op(): + from sympy.physics.quantum.spin import J2Op + assert _test_args(J2Op('J')) + + +def test_sympy__physics__quantum__spin__JminusOp(): + from sympy.physics.quantum.spin import JminusOp + assert _test_args(JminusOp('J')) + + +def test_sympy__physics__quantum__spin__JplusOp(): + from sympy.physics.quantum.spin import JplusOp + assert _test_args(JplusOp('J')) + + +def test_sympy__physics__quantum__spin__JxBra(): + from sympy.physics.quantum.spin import JxBra + assert _test_args(JxBra(1, 0)) + + +def test_sympy__physics__quantum__spin__JxBraCoupled(): + from sympy.physics.quantum.spin import JxBraCoupled + assert _test_args(JxBraCoupled(1, 0, (1, 1))) + + +def test_sympy__physics__quantum__spin__JxKet(): + from sympy.physics.quantum.spin import JxKet + assert _test_args(JxKet(1, 0)) + + +def test_sympy__physics__quantum__spin__JxKetCoupled(): + from sympy.physics.quantum.spin import JxKetCoupled + assert _test_args(JxKetCoupled(1, 0, (1, 1))) + + +def test_sympy__physics__quantum__spin__JxOp(): + from sympy.physics.quantum.spin import JxOp + assert _test_args(JxOp('J')) + + +def test_sympy__physics__quantum__spin__JyBra(): + from sympy.physics.quantum.spin import JyBra + assert _test_args(JyBra(1, 0)) + + +def test_sympy__physics__quantum__spin__JyBraCoupled(): + from sympy.physics.quantum.spin import JyBraCoupled + assert _test_args(JyBraCoupled(1, 0, (1, 1))) + + +def test_sympy__physics__quantum__spin__JyKet(): + from sympy.physics.quantum.spin import JyKet + assert _test_args(JyKet(1, 0)) + + +def test_sympy__physics__quantum__spin__JyKetCoupled(): + from sympy.physics.quantum.spin import JyKetCoupled + assert _test_args(JyKetCoupled(1, 0, (1, 1))) + + +def test_sympy__physics__quantum__spin__JyOp(): + from sympy.physics.quantum.spin import JyOp + assert _test_args(JyOp('J')) + + +def test_sympy__physics__quantum__spin__JzBra(): + from sympy.physics.quantum.spin import JzBra + assert _test_args(JzBra(1, 0)) + + +def test_sympy__physics__quantum__spin__JzBraCoupled(): + from sympy.physics.quantum.spin import JzBraCoupled + assert _test_args(JzBraCoupled(1, 0, (1, 1))) + + +def test_sympy__physics__quantum__spin__JzKet(): + from sympy.physics.quantum.spin import JzKet + assert _test_args(JzKet(1, 0)) + + +def test_sympy__physics__quantum__spin__JzKetCoupled(): + from sympy.physics.quantum.spin import JzKetCoupled + assert _test_args(JzKetCoupled(1, 0, (1, 1))) + + +def test_sympy__physics__quantum__spin__JzOp(): + from sympy.physics.quantum.spin import JzOp + assert _test_args(JzOp('J')) + + +def test_sympy__physics__quantum__spin__Rotation(): + from sympy.physics.quantum.spin import Rotation + assert _test_args(Rotation(pi, 0, pi/2)) + + +def test_sympy__physics__quantum__spin__SpinState(): + from sympy.physics.quantum.spin import SpinState + assert _test_args(SpinState(1, 0)) + + +def test_sympy__physics__quantum__spin__WignerD(): + from sympy.physics.quantum.spin import WignerD + assert _test_args(WignerD(0, 1, 2, 3, 4, 5)) + + +def test_sympy__physics__quantum__state__Bra(): + from sympy.physics.quantum.state import Bra + assert _test_args(Bra(0)) + + +def test_sympy__physics__quantum__state__BraBase(): + from sympy.physics.quantum.state import BraBase + assert _test_args(BraBase(0)) + + +def test_sympy__physics__quantum__state__Ket(): + from sympy.physics.quantum.state import Ket + assert _test_args(Ket(0)) + + +def test_sympy__physics__quantum__state__KetBase(): + from sympy.physics.quantum.state import KetBase + assert _test_args(KetBase(0)) + + +def test_sympy__physics__quantum__state__State(): + from sympy.physics.quantum.state import State + assert _test_args(State(0)) + + +def test_sympy__physics__quantum__state__StateBase(): + from sympy.physics.quantum.state import StateBase + assert _test_args(StateBase(0)) + + +def test_sympy__physics__quantum__state__OrthogonalBra(): + from sympy.physics.quantum.state import OrthogonalBra + assert _test_args(OrthogonalBra(0)) + + +def test_sympy__physics__quantum__state__OrthogonalKet(): + from sympy.physics.quantum.state import OrthogonalKet + assert _test_args(OrthogonalKet(0)) + + +def test_sympy__physics__quantum__state__OrthogonalState(): + from sympy.physics.quantum.state import OrthogonalState + assert _test_args(OrthogonalState(0)) + + +def test_sympy__physics__quantum__state__TimeDepBra(): + from sympy.physics.quantum.state import TimeDepBra + assert _test_args(TimeDepBra('psi', 't')) + + +def test_sympy__physics__quantum__state__TimeDepKet(): + from sympy.physics.quantum.state import TimeDepKet + assert _test_args(TimeDepKet('psi', 't')) + + +def test_sympy__physics__quantum__state__TimeDepState(): + from sympy.physics.quantum.state import TimeDepState + assert _test_args(TimeDepState('psi', 't')) + + +def test_sympy__physics__quantum__state__Wavefunction(): + from sympy.physics.quantum.state import Wavefunction + from sympy.functions import sin + from sympy.functions.elementary.piecewise import Piecewise + n = 1 + L = 1 + g = Piecewise((0, x < 0), (0, x > L), (sqrt(2//L)*sin(n*pi*x/L), True)) + assert _test_args(Wavefunction(g, x)) + + +def test_sympy__physics__quantum__tensorproduct__TensorProduct(): + from sympy.physics.quantum.tensorproduct import TensorProduct + x, y = symbols("x y", commutative=False) + assert _test_args(TensorProduct(x, y)) + + +def test_sympy__physics__quantum__identitysearch__GateIdentity(): + from sympy.physics.quantum.gate import X + from sympy.physics.quantum.identitysearch import GateIdentity + assert _test_args(GateIdentity(X(0), X(0))) + + +def test_sympy__physics__quantum__sho1d__SHOOp(): + from sympy.physics.quantum.sho1d import SHOOp + assert _test_args(SHOOp('a')) + + +def test_sympy__physics__quantum__sho1d__RaisingOp(): + from sympy.physics.quantum.sho1d import RaisingOp + assert _test_args(RaisingOp('a')) + + +def test_sympy__physics__quantum__sho1d__LoweringOp(): + from sympy.physics.quantum.sho1d import LoweringOp + assert _test_args(LoweringOp('a')) + + +def test_sympy__physics__quantum__sho1d__NumberOp(): + from sympy.physics.quantum.sho1d import NumberOp + assert _test_args(NumberOp('N')) + + +def test_sympy__physics__quantum__sho1d__Hamiltonian(): + from sympy.physics.quantum.sho1d import Hamiltonian + assert _test_args(Hamiltonian('H')) + + +def test_sympy__physics__quantum__sho1d__SHOState(): + from sympy.physics.quantum.sho1d import SHOState + assert _test_args(SHOState(0)) + + +def test_sympy__physics__quantum__sho1d__SHOKet(): + from sympy.physics.quantum.sho1d import SHOKet + assert _test_args(SHOKet(0)) + + +def test_sympy__physics__quantum__sho1d__SHOBra(): + from sympy.physics.quantum.sho1d import SHOBra + assert _test_args(SHOBra(0)) + + +def test_sympy__physics__secondquant__AnnihilateBoson(): + from sympy.physics.secondquant import AnnihilateBoson + assert _test_args(AnnihilateBoson(0)) + + +def test_sympy__physics__secondquant__AnnihilateFermion(): + from sympy.physics.secondquant import AnnihilateFermion + assert _test_args(AnnihilateFermion(0)) + + +@SKIP("abstract class") +def test_sympy__physics__secondquant__Annihilator(): + pass + + +def test_sympy__physics__secondquant__AntiSymmetricTensor(): + from sympy.physics.secondquant import AntiSymmetricTensor + i, j = symbols('i j', below_fermi=True) + a, b = symbols('a b', above_fermi=True) + assert _test_args(AntiSymmetricTensor('v', (a, i), (b, j))) + + +def test_sympy__physics__secondquant__BosonState(): + from sympy.physics.secondquant import BosonState + assert _test_args(BosonState((0, 1))) + + +@SKIP("abstract class") +def test_sympy__physics__secondquant__BosonicOperator(): + pass + + +def test_sympy__physics__secondquant__Commutator(): + from sympy.physics.secondquant import Commutator + x, y = symbols('x y', commutative=False) + assert _test_args(Commutator(x, y)) + + +def test_sympy__physics__secondquant__CreateBoson(): + from sympy.physics.secondquant import CreateBoson + assert _test_args(CreateBoson(0)) + + +def test_sympy__physics__secondquant__CreateFermion(): + from sympy.physics.secondquant import CreateFermion + assert _test_args(CreateFermion(0)) + + +@SKIP("abstract class") +def test_sympy__physics__secondquant__Creator(): + pass + + +def test_sympy__physics__secondquant__Dagger(): + from sympy.physics.secondquant import Dagger + assert _test_args(Dagger(x)) + + +def test_sympy__physics__secondquant__FermionState(): + from sympy.physics.secondquant import FermionState + assert _test_args(FermionState((0, 1))) + + +def test_sympy__physics__secondquant__FermionicOperator(): + from sympy.physics.secondquant import FermionicOperator + assert _test_args(FermionicOperator(0)) + + +def test_sympy__physics__secondquant__FockState(): + from sympy.physics.secondquant import FockState + assert _test_args(FockState((0, 1))) + + +def test_sympy__physics__secondquant__FockStateBosonBra(): + from sympy.physics.secondquant import FockStateBosonBra + assert _test_args(FockStateBosonBra((0, 1))) + + +def test_sympy__physics__secondquant__FockStateBosonKet(): + from sympy.physics.secondquant import FockStateBosonKet + assert _test_args(FockStateBosonKet((0, 1))) + + +def test_sympy__physics__secondquant__FockStateBra(): + from sympy.physics.secondquant import FockStateBra + assert _test_args(FockStateBra((0, 1))) + + +def test_sympy__physics__secondquant__FockStateFermionBra(): + from sympy.physics.secondquant import FockStateFermionBra + assert _test_args(FockStateFermionBra((0, 1))) + + +def test_sympy__physics__secondquant__FockStateFermionKet(): + from sympy.physics.secondquant import FockStateFermionKet + assert _test_args(FockStateFermionKet((0, 1))) + + +def test_sympy__physics__secondquant__FockStateKet(): + from sympy.physics.secondquant import FockStateKet + assert _test_args(FockStateKet((0, 1))) + + +def test_sympy__physics__secondquant__InnerProduct(): + from sympy.physics.secondquant import InnerProduct + from sympy.physics.secondquant import FockStateKet, FockStateBra + assert _test_args(InnerProduct(FockStateBra((0, 1)), FockStateKet((0, 1)))) + + +def test_sympy__physics__secondquant__NO(): + from sympy.physics.secondquant import NO, F, Fd + assert _test_args(NO(Fd(x)*F(y))) + + +def test_sympy__physics__secondquant__PermutationOperator(): + from sympy.physics.secondquant import PermutationOperator + assert _test_args(PermutationOperator(0, 1)) + + +def test_sympy__physics__secondquant__SqOperator(): + from sympy.physics.secondquant import SqOperator + assert _test_args(SqOperator(0)) + + +def test_sympy__physics__secondquant__TensorSymbol(): + from sympy.physics.secondquant import TensorSymbol + assert _test_args(TensorSymbol(x)) + + +def test_sympy__physics__control__lti__LinearTimeInvariant(): + # Direct instances of LinearTimeInvariant class are not allowed. + # func(*args) tests for its derived classes (TransferFunction, + # Series, Parallel and TransferFunctionMatrix) should pass. + pass + + +def test_sympy__physics__control__lti__SISOLinearTimeInvariant(): + # Direct instances of SISOLinearTimeInvariant class are not allowed. + pass + + +def test_sympy__physics__control__lti__MIMOLinearTimeInvariant(): + # Direct instances of MIMOLinearTimeInvariant class are not allowed. + pass + + +def test_sympy__physics__control__lti__TransferFunction(): + from sympy.physics.control.lti import TransferFunction + assert _test_args(TransferFunction(2, 3, x)) + + +def _test_args_PIDController(obj): + from sympy.physics.control.lti import PIDController + if isinstance(obj, PIDController): + kp, ki, kd, tf = obj.kp, obj.ki, obj.kd, obj.tf + recreated_pid = PIDController(kp, ki, kd, tf, s) + return recreated_pid == obj + return False + + +def test_sympy__physics__control__lti__PIDController(): + from sympy.physics.control.lti import PIDController + kp, ki, kd, tf = 1, 0.1, 0.01, 0 + assert _test_args_PIDController(PIDController(kp, ki, kd, tf, s)) + + +def test_sympy__physics__control__lti__Series(): + from sympy.physics.control import Series, TransferFunction + tf1 = TransferFunction(x**2 - y**3, y - z, x) + tf2 = TransferFunction(y - x, z + y, x) + assert _test_args(Series(tf1, tf2)) + + +def test_sympy__physics__control__lti__MIMOSeries(): + from sympy.physics.control import MIMOSeries, TransferFunction, TransferFunctionMatrix + tf1 = TransferFunction(x**2 - y**3, y - z, x) + tf2 = TransferFunction(y - x, z + y, x) + tfm_1 = TransferFunctionMatrix([[tf2, tf1]]) + tfm_2 = TransferFunctionMatrix([[tf1, tf2], [tf2, tf1]]) + tfm_3 = TransferFunctionMatrix([[tf1], [tf2]]) + assert _test_args(MIMOSeries(tfm_3, tfm_2, tfm_1)) + + +def test_sympy__physics__control__lti__Parallel(): + from sympy.physics.control import Parallel, TransferFunction + tf1 = TransferFunction(x**2 - y**3, y - z, x) + tf2 = TransferFunction(y - x, z + y, x) + assert _test_args(Parallel(tf1, tf2)) + + +def test_sympy__physics__control__lti__MIMOParallel(): + from sympy.physics.control import MIMOParallel, TransferFunction, TransferFunctionMatrix + tf1 = TransferFunction(x**2 - y**3, y - z, x) + tf2 = TransferFunction(y - x, z + y, x) + tfm_1 = TransferFunctionMatrix([[tf1, tf2], [tf2, tf1]]) + tfm_2 = TransferFunctionMatrix([[tf2, tf1], [tf1, tf2]]) + assert _test_args(MIMOParallel(tfm_1, tfm_2)) + + +def test_sympy__physics__control__lti__Feedback(): + from sympy.physics.control import TransferFunction, Feedback + tf1 = TransferFunction(x**2 - y**3, y - z, x) + tf2 = TransferFunction(y - x, z + y, x) + assert _test_args(Feedback(tf1, tf2)) + assert _test_args(Feedback(tf1, tf2, 1)) + + +def test_sympy__physics__control__lti__MIMOFeedback(): + from sympy.physics.control import TransferFunction, MIMOFeedback, TransferFunctionMatrix + tf1 = TransferFunction(x**2 - y**3, y - z, x) + tf2 = TransferFunction(y - x, z + y, x) + tfm_1 = TransferFunctionMatrix([[tf2, tf1], [tf1, tf2]]) + tfm_2 = TransferFunctionMatrix([[tf1, tf2], [tf2, tf1]]) + assert _test_args(MIMOFeedback(tfm_1, tfm_2)) + assert _test_args(MIMOFeedback(tfm_1, tfm_2, 1)) + + +def test_sympy__physics__control__lti__TransferFunctionMatrix(): + from sympy.physics.control import TransferFunction, TransferFunctionMatrix + tf1 = TransferFunction(x**2 - y**3, y - z, x) + tf2 = TransferFunction(y - x, z + y, x) + assert _test_args(TransferFunctionMatrix([[tf1, tf2]])) + + +def test_sympy__physics__control__lti__StateSpace(): + from sympy.matrices.dense import Matrix + from sympy.physics.control import StateSpace + A = Matrix([[-5, -1], [3, -1]]) + B = Matrix([2, 5]) + C = Matrix([[1, 2]]) + D = Matrix([0]) + assert _test_args(StateSpace(A, B, C, D)) + + +def test_sympy__physics__units__dimensions__Dimension(): + from sympy.physics.units.dimensions import Dimension + assert _test_args(Dimension("length", "L")) + + +def test_sympy__physics__units__dimensions__DimensionSystem(): + from sympy.physics.units.dimensions import DimensionSystem + from sympy.physics.units.definitions.dimension_definitions import length, time, velocity + assert _test_args(DimensionSystem((length, time), (velocity,))) + + +def test_sympy__physics__units__quantities__Quantity(): + from sympy.physics.units.quantities import Quantity + assert _test_args(Quantity("dam")) + + +def test_sympy__physics__units__quantities__PhysicalConstant(): + from sympy.physics.units.quantities import PhysicalConstant + assert _test_args(PhysicalConstant("foo")) + + +def test_sympy__physics__units__prefixes__Prefix(): + from sympy.physics.units.prefixes import Prefix + assert _test_args(Prefix('kilo', 'k', 3)) + + +def test_sympy__core__numbers__AlgebraicNumber(): + from sympy.core.numbers import AlgebraicNumber + assert _test_args(AlgebraicNumber(sqrt(2), [1, 2, 3])) + + +def test_sympy__polys__polytools__GroebnerBasis(): + from sympy.polys.polytools import GroebnerBasis + assert _test_args(GroebnerBasis([x, y, z], x, y, z)) + + +def test_sympy__polys__polytools__Poly(): + from sympy.polys.polytools import Poly + assert _test_args(Poly(2, x, y)) + + +def test_sympy__polys__polytools__PurePoly(): + from sympy.polys.polytools import PurePoly + assert _test_args(PurePoly(2, x, y)) + + +@SKIP('abstract class') +def test_sympy__polys__rootoftools__RootOf(): + pass + + +def test_sympy__polys__rootoftools__ComplexRootOf(): + from sympy.polys.rootoftools import ComplexRootOf + assert _test_args(ComplexRootOf(x**3 + x + 1, 0)) + + +def test_sympy__polys__rootoftools__RootSum(): + from sympy.polys.rootoftools import RootSum + assert _test_args(RootSum(x**3 + x + 1, sin)) + + +def test_sympy__series__limits__Limit(): + from sympy.series.limits import Limit + assert _test_args(Limit(x, x, 0, dir='-')) + + +def test_sympy__series__order__Order(): + from sympy.series.order import Order + assert _test_args(Order(1, x, y)) + + +@SKIP('Abstract Class') +def test_sympy__series__sequences__SeqBase(): + pass + + +def test_sympy__series__sequences__EmptySequence(): + # Need to import the instance from series not the class from + # series.sequence + from sympy.series import EmptySequence + assert _test_args(EmptySequence) + + +@SKIP('Abstract Class') +def test_sympy__series__sequences__SeqExpr(): + pass + + +def test_sympy__series__sequences__SeqPer(): + from sympy.series.sequences import SeqPer + assert _test_args(SeqPer((1, 2, 3), (0, 10))) + + +def test_sympy__series__sequences__SeqFormula(): + from sympy.series.sequences import SeqFormula + assert _test_args(SeqFormula(x**2, (0, 10))) + + +def test_sympy__series__sequences__RecursiveSeq(): + from sympy.series.sequences import RecursiveSeq + y = Function("y") + n = symbols("n") + assert _test_args(RecursiveSeq(y(n - 1) + y(n - 2), y(n), n, (0, 1))) + assert _test_args(RecursiveSeq(y(n - 1) + y(n - 2), y(n), n)) + + +def test_sympy__series__sequences__SeqExprOp(): + from sympy.series.sequences import SeqExprOp, sequence + s1 = sequence((1, 2, 3)) + s2 = sequence(x**2) + assert _test_args(SeqExprOp(s1, s2)) + + +def test_sympy__series__sequences__SeqAdd(): + from sympy.series.sequences import SeqAdd, sequence + s1 = sequence((1, 2, 3)) + s2 = sequence(x**2) + assert _test_args(SeqAdd(s1, s2)) + + +def test_sympy__series__sequences__SeqMul(): + from sympy.series.sequences import SeqMul, sequence + s1 = sequence((1, 2, 3)) + s2 = sequence(x**2) + assert _test_args(SeqMul(s1, s2)) + + +@SKIP('Abstract Class') +def test_sympy__series__series_class__SeriesBase(): + pass + + +def test_sympy__series__fourier__FourierSeries(): + from sympy.series.fourier import fourier_series + assert _test_args(fourier_series(x, (x, -pi, pi))) + +def test_sympy__series__fourier__FiniteFourierSeries(): + from sympy.series.fourier import fourier_series + assert _test_args(fourier_series(sin(pi*x), (x, -1, 1))) + + +def test_sympy__series__formal__FormalPowerSeries(): + from sympy.series.formal import fps + assert _test_args(fps(log(1 + x), x)) + + +def test_sympy__series__formal__Coeff(): + from sympy.series.formal import fps + assert _test_args(fps(x**2 + x + 1, x)) + + +@SKIP('Abstract Class') +def test_sympy__series__formal__FiniteFormalPowerSeries(): + pass + + +def test_sympy__series__formal__FormalPowerSeriesProduct(): + from sympy.series.formal import fps + f1, f2 = fps(sin(x)), fps(exp(x)) + assert _test_args(f1.product(f2, x)) + + +def test_sympy__series__formal__FormalPowerSeriesCompose(): + from sympy.series.formal import fps + f1, f2 = fps(exp(x)), fps(sin(x)) + assert _test_args(f1.compose(f2, x)) + + +def test_sympy__series__formal__FormalPowerSeriesInverse(): + from sympy.series.formal import fps + f1 = fps(exp(x)) + assert _test_args(f1.inverse(x)) + + +def test_sympy__simplify__hyperexpand__Hyper_Function(): + from sympy.simplify.hyperexpand import Hyper_Function + assert _test_args(Hyper_Function([2], [1])) + + +def test_sympy__simplify__hyperexpand__G_Function(): + from sympy.simplify.hyperexpand import G_Function + assert _test_args(G_Function([2], [1], [], [])) + + +@SKIP("abstract class") +def test_sympy__tensor__array__ndim_array__ImmutableNDimArray(): + pass + + +def test_sympy__tensor__array__dense_ndim_array__ImmutableDenseNDimArray(): + from sympy.tensor.array.dense_ndim_array import ImmutableDenseNDimArray + densarr = ImmutableDenseNDimArray(range(10, 34), (2, 3, 4)) + assert _test_args(densarr) + + +def test_sympy__tensor__array__sparse_ndim_array__ImmutableSparseNDimArray(): + from sympy.tensor.array.sparse_ndim_array import ImmutableSparseNDimArray + sparr = ImmutableSparseNDimArray(range(10, 34), (2, 3, 4)) + assert _test_args(sparr) + + +def test_sympy__tensor__array__array_comprehension__ArrayComprehension(): + from sympy.tensor.array.array_comprehension import ArrayComprehension + arrcom = ArrayComprehension(x, (x, 1, 5)) + assert _test_args(arrcom) + +def test_sympy__tensor__array__array_comprehension__ArrayComprehensionMap(): + from sympy.tensor.array.array_comprehension import ArrayComprehensionMap + arrcomma = ArrayComprehensionMap(lambda: 0, (x, 1, 5)) + assert _test_args(arrcomma) + + +def test_sympy__tensor__array__array_derivatives__ArrayDerivative(): + from sympy.tensor.array.array_derivatives import ArrayDerivative + A = MatrixSymbol("A", 2, 2) + arrder = ArrayDerivative(A, A, evaluate=False) + assert _test_args(arrder) + +def test_sympy__tensor__array__expressions__array_expressions__ArraySymbol(): + from sympy.tensor.array.expressions.array_expressions import ArraySymbol + m, n, k = symbols("m n k") + array = ArraySymbol("A", (m, n, k, 2)) + assert _test_args(array) + +def test_sympy__tensor__array__expressions__array_expressions__ArrayElement(): + from sympy.tensor.array.expressions.array_expressions import ArrayElement + m, n, k = symbols("m n k") + ae = ArrayElement("A", (m, n, k, 2)) + assert _test_args(ae) + +def test_sympy__tensor__array__expressions__array_expressions__ZeroArray(): + from sympy.tensor.array.expressions.array_expressions import ZeroArray + m, n, k = symbols("m n k") + za = ZeroArray(m, n, k, 2) + assert _test_args(za) + +def test_sympy__tensor__array__expressions__array_expressions__OneArray(): + from sympy.tensor.array.expressions.array_expressions import OneArray + m, n, k = symbols("m n k") + za = OneArray(m, n, k, 2) + assert _test_args(za) + +def test_sympy__tensor__functions__TensorProduct(): + from sympy.tensor.functions import TensorProduct + A = MatrixSymbol('A', 3, 3) + B = MatrixSymbol('B', 3, 3) + tp = TensorProduct(A, B) + assert _test_args(tp) + + +def test_sympy__tensor__indexed__Idx(): + from sympy.tensor.indexed import Idx + assert _test_args(Idx('test')) + assert _test_args(Idx('test', (0, 10))) + assert _test_args(Idx('test', 2)) + assert _test_args(Idx('test', x)) + + +def test_sympy__tensor__indexed__Indexed(): + from sympy.tensor.indexed import Indexed, Idx + assert _test_args(Indexed('A', Idx('i'), Idx('j'))) + + +def test_sympy__tensor__indexed__IndexedBase(): + from sympy.tensor.indexed import IndexedBase + assert _test_args(IndexedBase('A', shape=(x, y))) + assert _test_args(IndexedBase('A', 1)) + assert _test_args(IndexedBase('A')[0, 1]) + + +def test_sympy__tensor__tensor__TensorIndexType(): + from sympy.tensor.tensor import TensorIndexType + assert _test_args(TensorIndexType('Lorentz')) + + +@SKIP("deprecated class") +def test_sympy__tensor__tensor__TensorType(): + pass + + +def test_sympy__tensor__tensor__TensorSymmetry(): + from sympy.tensor.tensor import TensorSymmetry, get_symmetric_group_sgs + assert _test_args(TensorSymmetry(get_symmetric_group_sgs(2))) + + +def test_sympy__tensor__tensor__TensorHead(): + from sympy.tensor.tensor import TensorIndexType, TensorSymmetry, get_symmetric_group_sgs, TensorHead + Lorentz = TensorIndexType('Lorentz', dummy_name='L') + sym = TensorSymmetry(get_symmetric_group_sgs(1)) + assert _test_args(TensorHead('p', [Lorentz], sym, 0)) + + +def test_sympy__tensor__tensor__TensorIndex(): + from sympy.tensor.tensor import TensorIndexType, TensorIndex + Lorentz = TensorIndexType('Lorentz', dummy_name='L') + assert _test_args(TensorIndex('i', Lorentz)) + +@SKIP("abstract class") +def test_sympy__tensor__tensor__TensExpr(): + pass + +def test_sympy__tensor__tensor__TensAdd(): + from sympy.tensor.tensor import TensorIndexType, TensorSymmetry, get_symmetric_group_sgs, tensor_indices, TensAdd, tensor_heads + Lorentz = TensorIndexType('Lorentz', dummy_name='L') + a, b = tensor_indices('a,b', Lorentz) + sym = TensorSymmetry(get_symmetric_group_sgs(1)) + p, q = tensor_heads('p,q', [Lorentz], sym) + t1 = p(a) + t2 = q(a) + assert _test_args(TensAdd(t1, t2)) + + +def test_sympy__tensor__tensor__Tensor(): + from sympy.tensor.tensor import TensorIndexType, TensorSymmetry, get_symmetric_group_sgs, tensor_indices, TensorHead + Lorentz = TensorIndexType('Lorentz', dummy_name='L') + a, b = tensor_indices('a,b', Lorentz) + sym = TensorSymmetry(get_symmetric_group_sgs(1)) + p = TensorHead('p', [Lorentz], sym) + assert _test_args(p(a)) + + +def test_sympy__tensor__tensor__TensMul(): + from sympy.tensor.tensor import TensorIndexType, TensorSymmetry, get_symmetric_group_sgs, tensor_indices, tensor_heads + Lorentz = TensorIndexType('Lorentz', dummy_name='L') + a, b = tensor_indices('a,b', Lorentz) + sym = TensorSymmetry(get_symmetric_group_sgs(1)) + p, q = tensor_heads('p, q', [Lorentz], sym) + assert _test_args(3*p(a)*q(b)) + + +def test_sympy__tensor__tensor__TensorElement(): + from sympy.tensor.tensor import TensorIndexType, TensorHead, TensorElement + L = TensorIndexType("L") + A = TensorHead("A", [L, L]) + telem = TensorElement(A(x, y), {x: 1}) + assert _test_args(telem) + +def test_sympy__tensor__tensor__WildTensor(): + from sympy.tensor.tensor import TensorIndexType, WildTensorHead, TensorIndex + Lorentz = TensorIndexType('Lorentz', dummy_name='L') + a = TensorIndex('a', Lorentz) + p = WildTensorHead('p') + assert _test_args(p(a)) + +def test_sympy__tensor__tensor__WildTensorHead(): + from sympy.tensor.tensor import WildTensorHead + assert _test_args(WildTensorHead('p')) + +def test_sympy__tensor__tensor__WildTensorIndex(): + from sympy.tensor.tensor import TensorIndexType, WildTensorIndex + Lorentz = TensorIndexType('Lorentz', dummy_name='L') + assert _test_args(WildTensorIndex('i', Lorentz)) + +def test_sympy__tensor__toperators__PartialDerivative(): + from sympy.tensor.tensor import TensorIndexType, tensor_indices, TensorHead + from sympy.tensor.toperators import PartialDerivative + Lorentz = TensorIndexType('Lorentz', dummy_name='L') + a, b = tensor_indices('a,b', Lorentz) + A = TensorHead("A", [Lorentz]) + assert _test_args(PartialDerivative(A(a), A(b))) + + +def test_as_coeff_add(): + assert (7, (3*x, 4*x**2)) == (7 + 3*x + 4*x**2).as_coeff_add() + + +def test_sympy__geometry__curve__Curve(): + from sympy.geometry.curve import Curve + assert _test_args(Curve((x, 1), (x, 0, 1))) + + +def test_sympy__geometry__point__Point(): + from sympy.geometry.point import Point + assert _test_args(Point(0, 1)) + + +def test_sympy__geometry__point__Point2D(): + from sympy.geometry.point import Point2D + assert _test_args(Point2D(0, 1)) + + +def test_sympy__geometry__point__Point3D(): + from sympy.geometry.point import Point3D + assert _test_args(Point3D(0, 1, 2)) + + +def test_sympy__geometry__ellipse__Ellipse(): + from sympy.geometry.ellipse import Ellipse + assert _test_args(Ellipse((0, 1), 2, 3)) + + +def test_sympy__geometry__ellipse__Circle(): + from sympy.geometry.ellipse import Circle + assert _test_args(Circle((0, 1), 2)) + + +def test_sympy__geometry__parabola__Parabola(): + from sympy.geometry.parabola import Parabola + from sympy.geometry.line import Line + assert _test_args(Parabola((0, 0), Line((2, 3), (4, 3)))) + + +@SKIP("abstract class") +def test_sympy__geometry__line__LinearEntity(): + pass + + +def test_sympy__geometry__line__Line(): + from sympy.geometry.line import Line + assert _test_args(Line((0, 1), (2, 3))) + + +def test_sympy__geometry__line__Ray(): + from sympy.geometry.line import Ray + assert _test_args(Ray((0, 1), (2, 3))) + + +def test_sympy__geometry__line__Segment(): + from sympy.geometry.line import Segment + assert _test_args(Segment((0, 1), (2, 3))) + +@SKIP("abstract class") +def test_sympy__geometry__line__LinearEntity2D(): + pass + + +def test_sympy__geometry__line__Line2D(): + from sympy.geometry.line import Line2D + assert _test_args(Line2D((0, 1), (2, 3))) + + +def test_sympy__geometry__line__Ray2D(): + from sympy.geometry.line import Ray2D + assert _test_args(Ray2D((0, 1), (2, 3))) + + +def test_sympy__geometry__line__Segment2D(): + from sympy.geometry.line import Segment2D + assert _test_args(Segment2D((0, 1), (2, 3))) + + +@SKIP("abstract class") +def test_sympy__geometry__line__LinearEntity3D(): + pass + + +def test_sympy__geometry__line__Line3D(): + from sympy.geometry.line import Line3D + assert _test_args(Line3D((0, 1, 1), (2, 3, 4))) + + +def test_sympy__geometry__line__Segment3D(): + from sympy.geometry.line import Segment3D + assert _test_args(Segment3D((0, 1, 1), (2, 3, 4))) + + +def test_sympy__geometry__line__Ray3D(): + from sympy.geometry.line import Ray3D + assert _test_args(Ray3D((0, 1, 1), (2, 3, 4))) + + +def test_sympy__geometry__plane__Plane(): + from sympy.geometry.plane import Plane + assert _test_args(Plane((1, 1, 1), (-3, 4, -2), (1, 2, 3))) + + +def test_sympy__geometry__polygon__Polygon(): + from sympy.geometry.polygon import Polygon + assert _test_args(Polygon((0, 1), (2, 3), (4, 5), (6, 7))) + + +def test_sympy__geometry__polygon__RegularPolygon(): + from sympy.geometry.polygon import RegularPolygon + assert _test_args(RegularPolygon((0, 1), 2, 3, 4)) + + +def test_sympy__geometry__polygon__Triangle(): + from sympy.geometry.polygon import Triangle + assert _test_args(Triangle((0, 1), (2, 3), (4, 5))) + + +def test_sympy__geometry__entity__GeometryEntity(): + from sympy.geometry.entity import GeometryEntity + from sympy.geometry.point import Point + assert _test_args(GeometryEntity(Point(1, 0), 1, [1, 2])) + +@SKIP("abstract class") +def test_sympy__geometry__entity__GeometrySet(): + pass + +def test_sympy__diffgeom__diffgeom__Manifold(): + from sympy.diffgeom import Manifold + assert _test_args(Manifold('name', 3)) + + +def test_sympy__diffgeom__diffgeom__Patch(): + from sympy.diffgeom import Manifold, Patch + assert _test_args(Patch('name', Manifold('name', 3))) + + +def test_sympy__diffgeom__diffgeom__CoordSystem(): + from sympy.diffgeom import Manifold, Patch, CoordSystem + assert _test_args(CoordSystem('name', Patch('name', Manifold('name', 3)))) + assert _test_args(CoordSystem('name', Patch('name', Manifold('name', 3)), [a, b, c])) + + +def test_sympy__diffgeom__diffgeom__CoordinateSymbol(): + from sympy.diffgeom import Manifold, Patch, CoordSystem, CoordinateSymbol + assert _test_args(CoordinateSymbol(CoordSystem('name', Patch('name', Manifold('name', 3)), [a, b, c]), 0)) + + +def test_sympy__diffgeom__diffgeom__Point(): + from sympy.diffgeom import Manifold, Patch, CoordSystem, Point + assert _test_args(Point( + CoordSystem('name', Patch('name', Manifold('name', 3)), [a, b, c]), [x, y])) + + +def test_sympy__diffgeom__diffgeom__BaseScalarField(): + from sympy.diffgeom import Manifold, Patch, CoordSystem, BaseScalarField + cs = CoordSystem('name', Patch('name', Manifold('name', 3)), [a, b, c]) + assert _test_args(BaseScalarField(cs, 0)) + + +def test_sympy__diffgeom__diffgeom__BaseVectorField(): + from sympy.diffgeom import Manifold, Patch, CoordSystem, BaseVectorField + cs = CoordSystem('name', Patch('name', Manifold('name', 3)), [a, b, c]) + assert _test_args(BaseVectorField(cs, 0)) + + +def test_sympy__diffgeom__diffgeom__Differential(): + from sympy.diffgeom import Manifold, Patch, CoordSystem, BaseScalarField, Differential + cs = CoordSystem('name', Patch('name', Manifold('name', 3)), [a, b, c]) + assert _test_args(Differential(BaseScalarField(cs, 0))) + + +def test_sympy__diffgeom__diffgeom__Commutator(): + from sympy.diffgeom import Manifold, Patch, CoordSystem, BaseVectorField, Commutator + cs = CoordSystem('name', Patch('name', Manifold('name', 3)), [a, b, c]) + cs1 = CoordSystem('name1', Patch('name', Manifold('name', 3)), [a, b, c]) + v = BaseVectorField(cs, 0) + v1 = BaseVectorField(cs1, 0) + assert _test_args(Commutator(v, v1)) + + +def test_sympy__diffgeom__diffgeom__TensorProduct(): + from sympy.diffgeom import Manifold, Patch, CoordSystem, BaseScalarField, Differential, TensorProduct + cs = CoordSystem('name', Patch('name', Manifold('name', 3)), [a, b, c]) + d = Differential(BaseScalarField(cs, 0)) + assert _test_args(TensorProduct(d, d)) + + +def test_sympy__diffgeom__diffgeom__WedgeProduct(): + from sympy.diffgeom import Manifold, Patch, CoordSystem, BaseScalarField, Differential, WedgeProduct + cs = CoordSystem('name', Patch('name', Manifold('name', 3)), [a, b, c]) + d = Differential(BaseScalarField(cs, 0)) + d1 = Differential(BaseScalarField(cs, 1)) + assert _test_args(WedgeProduct(d, d1)) + + +def test_sympy__diffgeom__diffgeom__LieDerivative(): + from sympy.diffgeom import Manifold, Patch, CoordSystem, BaseScalarField, Differential, BaseVectorField, LieDerivative + cs = CoordSystem('name', Patch('name', Manifold('name', 3)), [a, b, c]) + d = Differential(BaseScalarField(cs, 0)) + v = BaseVectorField(cs, 0) + assert _test_args(LieDerivative(v, d)) + + +def test_sympy__diffgeom__diffgeom__BaseCovarDerivativeOp(): + from sympy.diffgeom import Manifold, Patch, CoordSystem, BaseCovarDerivativeOp + cs = CoordSystem('name', Patch('name', Manifold('name', 3)), [a, b, c]) + assert _test_args(BaseCovarDerivativeOp(cs, 0, [[[0, ]*3, ]*3, ]*3)) + + +def test_sympy__diffgeom__diffgeom__CovarDerivativeOp(): + from sympy.diffgeom import Manifold, Patch, CoordSystem, BaseVectorField, CovarDerivativeOp + cs = CoordSystem('name', Patch('name', Manifold('name', 3)), [a, b, c]) + v = BaseVectorField(cs, 0) + _test_args(CovarDerivativeOp(v, [[[0, ]*3, ]*3, ]*3)) + + +def test_sympy__categories__baseclasses__Class(): + from sympy.categories.baseclasses import Class + assert _test_args(Class()) + + +def test_sympy__categories__baseclasses__Object(): + from sympy.categories import Object + assert _test_args(Object("A")) + + +@SKIP("abstract class") +def test_sympy__categories__baseclasses__Morphism(): + pass + + +def test_sympy__categories__baseclasses__IdentityMorphism(): + from sympy.categories import Object, IdentityMorphism + assert _test_args(IdentityMorphism(Object("A"))) + + +def test_sympy__categories__baseclasses__NamedMorphism(): + from sympy.categories import Object, NamedMorphism + assert _test_args(NamedMorphism(Object("A"), Object("B"), "f")) + + +def test_sympy__categories__baseclasses__CompositeMorphism(): + from sympy.categories import Object, NamedMorphism, CompositeMorphism + A = Object("A") + B = Object("B") + C = Object("C") + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + assert _test_args(CompositeMorphism(f, g)) + + +def test_sympy__categories__baseclasses__Diagram(): + from sympy.categories import Object, NamedMorphism, Diagram + A = Object("A") + B = Object("B") + f = NamedMorphism(A, B, "f") + d = Diagram([f]) + assert _test_args(d) + + +def test_sympy__categories__baseclasses__Category(): + from sympy.categories import Object, NamedMorphism, Diagram, Category + A = Object("A") + B = Object("B") + C = Object("C") + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + d1 = Diagram([f, g]) + d2 = Diagram([f]) + K = Category("K", commutative_diagrams=[d1, d2]) + assert _test_args(K) + + +def test_sympy__physics__optics__waves__TWave(): + from sympy.physics.optics import TWave + A, f, phi = symbols('A, f, phi') + assert _test_args(TWave(A, f, phi)) + + +def test_sympy__physics__optics__gaussopt__BeamParameter(): + from sympy.physics.optics import BeamParameter + assert _test_args(BeamParameter(530e-9, 1, w=1e-3, n=1)) + + +def test_sympy__physics__optics__medium__Medium(): + from sympy.physics.optics import Medium + assert _test_args(Medium('m')) + + +def test_sympy__physics__optics__medium__MediumN(): + from sympy.physics.optics.medium import Medium + assert _test_args(Medium('m', n=2)) + + +def test_sympy__physics__optics__medium__MediumPP(): + from sympy.physics.optics.medium import Medium + assert _test_args(Medium('m', permittivity=2, permeability=2)) + + +def test_sympy__tensor__array__expressions__array_expressions__ArrayContraction(): + from sympy.tensor.array.expressions.array_expressions import ArrayContraction + from sympy.tensor.indexed import IndexedBase + A = symbols("A", cls=IndexedBase) + assert _test_args(ArrayContraction(A, (0, 1))) + + +def test_sympy__tensor__array__expressions__array_expressions__ArrayDiagonal(): + from sympy.tensor.array.expressions.array_expressions import ArrayDiagonal + from sympy.tensor.indexed import IndexedBase + A = symbols("A", cls=IndexedBase) + assert _test_args(ArrayDiagonal(A, (0, 1))) + + +def test_sympy__tensor__array__expressions__array_expressions__ArrayTensorProduct(): + from sympy.tensor.array.expressions.array_expressions import ArrayTensorProduct + from sympy.tensor.indexed import IndexedBase + A, B = symbols("A B", cls=IndexedBase) + assert _test_args(ArrayTensorProduct(A, B)) + + +def test_sympy__tensor__array__expressions__array_expressions__ArrayAdd(): + from sympy.tensor.array.expressions.array_expressions import ArrayAdd + from sympy.tensor.indexed import IndexedBase + A, B = symbols("A B", cls=IndexedBase) + assert _test_args(ArrayAdd(A, B)) + + +def test_sympy__tensor__array__expressions__array_expressions__PermuteDims(): + from sympy.tensor.array.expressions.array_expressions import PermuteDims + A = MatrixSymbol("A", 4, 4) + assert _test_args(PermuteDims(A, (1, 0))) + + +def test_sympy__tensor__array__expressions__array_expressions__ArrayElementwiseApplyFunc(): + from sympy.tensor.array.expressions.array_expressions import ArraySymbol, ArrayElementwiseApplyFunc + A = ArraySymbol("A", (4,)) + assert _test_args(ArrayElementwiseApplyFunc(exp, A)) + + +def test_sympy__tensor__array__expressions__array_expressions__Reshape(): + from sympy.tensor.array.expressions.array_expressions import ArraySymbol, Reshape + A = ArraySymbol("A", (4,)) + assert _test_args(Reshape(A, (2, 2))) + + +def test_sympy__codegen__ast__Assignment(): + from sympy.codegen.ast import Assignment + assert _test_args(Assignment(x, y)) + + +def test_sympy__codegen__cfunctions__expm1(): + from sympy.codegen.cfunctions import expm1 + assert _test_args(expm1(x)) + + +def test_sympy__codegen__cfunctions__log1p(): + from sympy.codegen.cfunctions import log1p + assert _test_args(log1p(x)) + + +def test_sympy__codegen__cfunctions__exp2(): + from sympy.codegen.cfunctions import exp2 + assert _test_args(exp2(x)) + + +def test_sympy__codegen__cfunctions__log2(): + from sympy.codegen.cfunctions import log2 + assert _test_args(log2(x)) + + +def test_sympy__codegen__cfunctions__fma(): + from sympy.codegen.cfunctions import fma + assert _test_args(fma(x, y, z)) + + +def test_sympy__codegen__cfunctions__log10(): + from sympy.codegen.cfunctions import log10 + assert _test_args(log10(x)) + + +def test_sympy__codegen__cfunctions__Sqrt(): + from sympy.codegen.cfunctions import Sqrt + assert _test_args(Sqrt(x)) + +def test_sympy__codegen__cfunctions__Cbrt(): + from sympy.codegen.cfunctions import Cbrt + assert _test_args(Cbrt(x)) + +def test_sympy__codegen__cfunctions__hypot(): + from sympy.codegen.cfunctions import hypot + assert _test_args(hypot(x, y)) + + +def test_sympy__codegen__cfunctions__isnan(): + from sympy.codegen.cfunctions import isnan + assert _test_args(isnan(x)) + + +def test_sympy__codegen__cfunctions__isinf(): + from sympy.codegen.cfunctions import isinf + assert _test_args(isinf(x)) + + +def test_sympy__codegen__fnodes__FFunction(): + from sympy.codegen.fnodes import FFunction + assert _test_args(FFunction('f')) + + +def test_sympy__codegen__fnodes__F95Function(): + from sympy.codegen.fnodes import F95Function + assert _test_args(F95Function('f')) + + +def test_sympy__codegen__fnodes__isign(): + from sympy.codegen.fnodes import isign + assert _test_args(isign(1, x)) + + +def test_sympy__codegen__fnodes__dsign(): + from sympy.codegen.fnodes import dsign + assert _test_args(dsign(1, x)) + + +def test_sympy__codegen__fnodes__cmplx(): + from sympy.codegen.fnodes import cmplx + assert _test_args(cmplx(x, y)) + + +def test_sympy__codegen__fnodes__kind(): + from sympy.codegen.fnodes import kind + assert _test_args(kind(x)) + + +def test_sympy__codegen__fnodes__merge(): + from sympy.codegen.fnodes import merge + assert _test_args(merge(1, 2, Eq(x, 0))) + + +def test_sympy__codegen__fnodes___literal(): + from sympy.codegen.fnodes import _literal + assert _test_args(_literal(1)) + + +def test_sympy__codegen__fnodes__literal_sp(): + from sympy.codegen.fnodes import literal_sp + assert _test_args(literal_sp(1)) + + +def test_sympy__codegen__fnodes__literal_dp(): + from sympy.codegen.fnodes import literal_dp + assert _test_args(literal_dp(1)) + + +def test_sympy__codegen__matrix_nodes__MatrixSolve(): + from sympy.matrices import MatrixSymbol + from sympy.codegen.matrix_nodes import MatrixSolve + A = MatrixSymbol('A', 3, 3) + v = MatrixSymbol('x', 3, 1) + assert _test_args(MatrixSolve(A, v)) + + +def test_sympy__printing__rust__TypeCast(): + from sympy.printing.rust import TypeCast + from sympy.codegen.ast import real + assert _test_args(TypeCast(x, real)) + + +def test_sympy__printing__rust__float_floor(): + from sympy.printing.rust import float_floor + assert _test_args(float_floor(x)) + + +def test_sympy__printing__rust__float_ceiling(): + from sympy.printing.rust import float_ceiling + assert _test_args(float_ceiling(x)) + + +def test_sympy__vector__coordsysrect__CoordSys3D(): + from sympy.vector.coordsysrect import CoordSys3D + assert _test_args(CoordSys3D('C')) + + +def test_sympy__vector__point__Point(): + from sympy.vector.point import Point + assert _test_args(Point('P')) + + +def test_sympy__vector__basisdependent__BasisDependent(): + #from sympy.vector.basisdependent import BasisDependent + #These classes have been created to maintain an OOP hierarchy + #for Vectors and Dyadics. Are NOT meant to be initialized + pass + + +def test_sympy__vector__basisdependent__BasisDependentMul(): + #from sympy.vector.basisdependent import BasisDependentMul + #These classes have been created to maintain an OOP hierarchy + #for Vectors and Dyadics. Are NOT meant to be initialized + pass + + +def test_sympy__vector__basisdependent__BasisDependentAdd(): + #from sympy.vector.basisdependent import BasisDependentAdd + #These classes have been created to maintain an OOP hierarchy + #for Vectors and Dyadics. Are NOT meant to be initialized + pass + + +def test_sympy__vector__basisdependent__BasisDependentZero(): + #from sympy.vector.basisdependent import BasisDependentZero + #These classes have been created to maintain an OOP hierarchy + #for Vectors and Dyadics. Are NOT meant to be initialized + pass + + +def test_sympy__vector__vector__BaseVector(): + from sympy.vector.vector import BaseVector + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + assert _test_args(BaseVector(0, C, ' ', ' ')) + + +def test_sympy__vector__vector__VectorAdd(): + from sympy.vector.vector import VectorAdd, VectorMul + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + from sympy.abc import a, b, c, x, y, z + v1 = a*C.i + b*C.j + c*C.k + v2 = x*C.i + y*C.j + z*C.k + assert _test_args(VectorAdd(v1, v2)) + assert _test_args(VectorMul(x, v1)) + + +def test_sympy__vector__vector__VectorMul(): + from sympy.vector.vector import VectorMul + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + from sympy.abc import a + assert _test_args(VectorMul(a, C.i)) + + +def test_sympy__vector__vector__VectorZero(): + from sympy.vector.vector import VectorZero + assert _test_args(VectorZero()) + + +def test_sympy__vector__vector__Vector(): + #from sympy.vector.vector import Vector + #Vector is never to be initialized using args + pass + + +def test_sympy__vector__vector__Cross(): + from sympy.vector.vector import Cross + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + _test_args(Cross(C.i, C.j)) + + +def test_sympy__vector__vector__Dot(): + from sympy.vector.vector import Dot + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + _test_args(Dot(C.i, C.j)) + + +def test_sympy__vector__dyadic__Dyadic(): + #from sympy.vector.dyadic import Dyadic + #Dyadic is never to be initialized using args + pass + + +def test_sympy__vector__dyadic__BaseDyadic(): + from sympy.vector.dyadic import BaseDyadic + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + assert _test_args(BaseDyadic(C.i, C.j)) + + +def test_sympy__vector__dyadic__DyadicMul(): + from sympy.vector.dyadic import BaseDyadic, DyadicMul + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + assert _test_args(DyadicMul(3, BaseDyadic(C.i, C.j))) + + +def test_sympy__vector__dyadic__DyadicAdd(): + from sympy.vector.dyadic import BaseDyadic, DyadicAdd + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + assert _test_args(2 * DyadicAdd(BaseDyadic(C.i, C.i), + BaseDyadic(C.i, C.j))) + + +def test_sympy__vector__dyadic__DyadicZero(): + from sympy.vector.dyadic import DyadicZero + assert _test_args(DyadicZero()) + + +def test_sympy__vector__deloperator__Del(): + from sympy.vector.deloperator import Del + assert _test_args(Del()) + + +def test_sympy__vector__implicitregion__ImplicitRegion(): + from sympy.vector.implicitregion import ImplicitRegion + from sympy.abc import x, y + assert _test_args(ImplicitRegion((x, y), y**3 - 4*x)) + + +def test_sympy__vector__integrals__ParametricIntegral(): + from sympy.vector.integrals import ParametricIntegral + from sympy.vector.parametricregion import ParametricRegion + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + assert _test_args(ParametricIntegral(C.y*C.i - 10*C.j,\ + ParametricRegion((x, y), (x, 1, 3), (y, -2, 2)))) + +def test_sympy__vector__operators__Curl(): + from sympy.vector.operators import Curl + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + assert _test_args(Curl(C.i)) + + +def test_sympy__vector__operators__Laplacian(): + from sympy.vector.operators import Laplacian + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + assert _test_args(Laplacian(C.i)) + + +def test_sympy__vector__operators__Divergence(): + from sympy.vector.operators import Divergence + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + assert _test_args(Divergence(C.i)) + + +def test_sympy__vector__operators__Gradient(): + from sympy.vector.operators import Gradient + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + assert _test_args(Gradient(C.x)) + + +def test_sympy__vector__orienters__Orienter(): + #from sympy.vector.orienters import Orienter + #Not to be initialized + pass + + +def test_sympy__vector__orienters__ThreeAngleOrienter(): + #from sympy.vector.orienters import ThreeAngleOrienter + #Not to be initialized + pass + + +def test_sympy__vector__orienters__AxisOrienter(): + from sympy.vector.orienters import AxisOrienter + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + assert _test_args(AxisOrienter(x, C.i)) + + +def test_sympy__vector__orienters__BodyOrienter(): + from sympy.vector.orienters import BodyOrienter + assert _test_args(BodyOrienter(x, y, z, '123')) + + +def test_sympy__vector__orienters__SpaceOrienter(): + from sympy.vector.orienters import SpaceOrienter + assert _test_args(SpaceOrienter(x, y, z, '123')) + + +def test_sympy__vector__orienters__QuaternionOrienter(): + from sympy.vector.orienters import QuaternionOrienter + a, b, c, d = symbols('a b c d') + assert _test_args(QuaternionOrienter(a, b, c, d)) + + +def test_sympy__vector__parametricregion__ParametricRegion(): + from sympy.abc import t + from sympy.vector.parametricregion import ParametricRegion + assert _test_args(ParametricRegion((t, t**3), (t, 0, 2))) + + +def test_sympy__vector__scalar__BaseScalar(): + from sympy.vector.scalar import BaseScalar + from sympy.vector.coordsysrect import CoordSys3D + C = CoordSys3D('C') + assert _test_args(BaseScalar(0, C, ' ', ' ')) + + +def test_sympy__physics__wigner__Wigner3j(): + from sympy.physics.wigner import Wigner3j + assert _test_args(Wigner3j(0, 0, 0, 0, 0, 0)) + + +def test_sympy__combinatorics__schur_number__SchurNumber(): + from sympy.combinatorics.schur_number import SchurNumber + assert _test_args(SchurNumber(x)) + + +def test_sympy__combinatorics__perm_groups__SymmetricPermutationGroup(): + from sympy.combinatorics.perm_groups import SymmetricPermutationGroup + assert _test_args(SymmetricPermutationGroup(5)) + + +def test_sympy__combinatorics__perm_groups__Coset(): + from sympy.combinatorics.permutations import Permutation + from sympy.combinatorics.perm_groups import PermutationGroup, Coset + a = Permutation(1, 2) + b = Permutation(0, 1) + G = PermutationGroup([a, b]) + assert _test_args(Coset(a, G)) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_arit.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_arit.py new file mode 100644 index 0000000000000000000000000000000000000000..251fc4c4234cbd6e82adc9a24ccea536ed6a37b7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_arit.py @@ -0,0 +1,2489 @@ +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.mod import Mod +from sympy.core.mul import Mul +from sympy.core.numbers import (Float, I, Integer, Rational, comp, nan, + oo, pi, zoo) +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.complexes import (im, re, sign) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import (Max, sqrt) +from sympy.functions.elementary.trigonometric import (atan, cos, sin) +from sympy.integrals.integrals import Integral +from sympy.polys.polytools import Poly +from sympy.sets.sets import FiniteSet + +from sympy.core.parameters import distribute, evaluate +from sympy.core.expr import unchanged +from sympy.utilities.iterables import permutations +from sympy.testing.pytest import XFAIL, raises, warns +from sympy.utilities.exceptions import SymPyDeprecationWarning +from sympy.core.random import verify_numerically +from sympy.functions.elementary.trigonometric import asin + +from itertools import product + +a, c, x, y, z = symbols('a,c,x,y,z') +b = Symbol("b", positive=True) + + +def same_and_same_prec(a, b): + # stricter matching for Floats + return a == b and a._prec == b._prec + + +def test_bug1(): + assert re(x) != x + x.series(x, 0, 1) + assert re(x) != x + + +def test_Symbol(): + e = a*b + assert e == a*b + assert a*b*b == a*b**2 + assert a*b*b + c == c + a*b**2 + assert a*b*b - c == -c + a*b**2 + + x = Symbol('x', complex=True, real=False) + assert x.is_imaginary is None # could be I or 1 + I + x = Symbol('x', complex=True, imaginary=False) + assert x.is_real is None # could be 1 or 1 + I + x = Symbol('x', real=True) + assert x.is_complex + x = Symbol('x', imaginary=True) + assert x.is_complex + x = Symbol('x', real=False, imaginary=False) + assert x.is_complex is None # might be a non-number + + +def test_arit0(): + p = Rational(5) + e = a*b + assert e == a*b + e = a*b + b*a + assert e == 2*a*b + e = a*b + b*a + a*b + p*b*a + assert e == 8*a*b + e = a*b + b*a + a*b + p*b*a + a + assert e == a + 8*a*b + e = a + a + assert e == 2*a + e = a + b + a + assert e == b + 2*a + e = a + b*b + a + b*b + assert e == 2*a + 2*b**2 + e = a + Rational(2) + b*b + a + b*b + p + assert e == 7 + 2*a + 2*b**2 + e = (a + b*b + a + b*b)*p + assert e == 5*(2*a + 2*b**2) + e = (a*b*c + c*b*a + b*a*c)*p + assert e == 15*a*b*c + e = (a*b*c + c*b*a + b*a*c)*p - Rational(15)*a*b*c + assert e == Rational(0) + e = Rational(50)*(a - a) + assert e == Rational(0) + e = b*a - b - a*b + b + assert e == Rational(0) + e = a*b + c**p + assert e == a*b + c**5 + e = a/b + assert e == a*b**(-1) + e = a*2*2 + assert e == 4*a + e = 2 + a*2/2 + assert e == 2 + a + e = 2 - a - 2 + assert e == -a + e = 2*a*2 + assert e == 4*a + e = 2/a/2 + assert e == a**(-1) + e = 2**a**2 + assert e == 2**(a**2) + e = -(1 + a) + assert e == -1 - a + e = S.Half*(1 + a) + assert e == S.Half + a/2 + + +def test_div(): + e = a/b + assert e == a*b**(-1) + e = a/b + c/2 + assert e == a*b**(-1) + Rational(1)/2*c + e = (1 - b)/(b - 1) + assert e == (1 + -b)*((-1) + b)**(-1) + + +def test_pow_arit(): + n1 = Rational(1) + n2 = Rational(2) + n5 = Rational(5) + e = a*a + assert e == a**2 + e = a*a*a + assert e == a**3 + e = a*a*a*a**Rational(6) + assert e == a**9 + e = a*a*a*a**Rational(6) - a**Rational(9) + assert e == Rational(0) + e = a**(b - b) + assert e == Rational(1) + e = (a + Rational(1) - a)**b + assert e == Rational(1) + + e = (a + b + c)**n2 + assert e == (a + b + c)**2 + assert e.expand() == 2*b*c + 2*a*c + 2*a*b + a**2 + c**2 + b**2 + + e = (a + b)**n2 + assert e == (a + b)**2 + assert e.expand() == 2*a*b + a**2 + b**2 + + e = (a + b)**(n1/n2) + assert e == sqrt(a + b) + assert e.expand() == sqrt(a + b) + + n = n5**(n1/n2) + assert n == sqrt(5) + e = n*a*b - n*b*a + assert e == Rational(0) + e = n*a*b + n*b*a + assert e == 2*a*b*sqrt(5) + assert e.diff(a) == 2*b*sqrt(5) + assert e.diff(a) == 2*b*sqrt(5) + e = a/b**2 + assert e == a*b**(-2) + + assert sqrt(2*(1 + sqrt(2))) == (2*(1 + 2**S.Half))**S.Half + + x = Symbol('x') + y = Symbol('y') + + assert ((x*y)**3).expand() == y**3 * x**3 + assert ((x*y)**-3).expand() == y**-3 * x**-3 + + assert (x**5*(3*x)**(3)).expand() == 27 * x**8 + assert (x**5*(-3*x)**(3)).expand() == -27 * x**8 + assert (x**5*(3*x)**(-3)).expand() == x**2 * Rational(1, 27) + assert (x**5*(-3*x)**(-3)).expand() == x**2 * Rational(-1, 27) + + # expand_power_exp + _x = Symbol('x', zero=False) + _y = Symbol('y', zero=False) + assert (_x**(y**(x + exp(x + y)) + z)).expand(deep=False) == \ + _x**z*_x**(y**(x + exp(x + y))) + assert (_x**(_y**(x + exp(x + y)) + z)).expand() == \ + _x**z*_x**(_y**x*_y**(exp(x)*exp(y))) + + n = Symbol('n', even=False) + k = Symbol('k', even=True) + o = Symbol('o', odd=True) + + assert unchanged(Pow, -1, x) + assert unchanged(Pow, -1, n) + assert (-2)**k == 2**k + assert (-1)**k == 1 + assert (-1)**o == -1 + + +def test_pow2(): + # x**(2*y) is always (x**y)**2 but is only (x**2)**y if + # x.is_positive or y.is_integer + # let x = 1 to see why the following are not true. + assert (-x)**Rational(2, 3) != x**Rational(2, 3) + assert (-x)**Rational(5, 7) != -x**Rational(5, 7) + assert ((-x)**2)**Rational(1, 3) != ((-x)**Rational(1, 3))**2 + assert sqrt(x**2) != x + + +def test_pow3(): + assert sqrt(2)**3 == 2 * sqrt(2) + assert sqrt(2)**3 == sqrt(8) + + +def test_mod_pow(): + for s, t, u, v in [(4, 13, 497, 445), (4, -3, 497, 365), + (3.2, 2.1, 1.9, 0.1031015682350942), (S(3)/2, 5, S(5)/6, S(3)/32)]: + assert pow(S(s), t, u) == v + assert pow(S(s), S(t), u) == v + assert pow(S(s), t, S(u)) == v + assert pow(S(s), S(t), S(u)) == v + assert pow(S(2), S(10000000000), S(3)) == 1 + assert pow(x, y, z) == x**y%z + raises(TypeError, lambda: pow(S(4), "13", 497)) + raises(TypeError, lambda: pow(S(4), 13, "497")) + + +def test_pow_E(): + assert 2**(y/log(2)) == S.Exp1**y + assert 2**(y/log(2)/3) == S.Exp1**(y/3) + assert 3**(1/log(-3)) != S.Exp1 + assert (3 + 2*I)**(1/(log(-3 - 2*I) + I*pi)) == S.Exp1 + assert (4 + 2*I)**(1/(log(-4 - 2*I) + I*pi)) == S.Exp1 + assert (3 + 2*I)**(1/(log(-3 - 2*I, 3)/2 + I*pi/log(3)/2)) == 9 + assert (3 + 2*I)**(1/(log(3 + 2*I, 3)/2)) == 9 + # every time tests are run they will affirm with a different random + # value that this identity holds + while 1: + b = x._random() + r, i = b.as_real_imag() + if i: + break + assert verify_numerically(b**(1/(log(-b) + sign(i)*I*pi).n()), S.Exp1) + + +def test_pow_issue_3516(): + assert 4**Rational(1, 4) == sqrt(2) + + +def test_pow_im(): + for m in (-2, -1, 2): + for d in (3, 4, 5): + b = m*I + for i in range(1, 4*d + 1): + e = Rational(i, d) + assert (b**e - b.n()**e.n()).n(2, chop=1e-10) == 0 + + e = Rational(7, 3) + assert (2*x*I)**e == 4*2**Rational(1, 3)*(I*x)**e # same as Wolfram Alpha + im = symbols('im', imaginary=True) + assert (2*im*I)**e == 4*2**Rational(1, 3)*(I*im)**e + + args = [I, I, I, I, 2] + e = Rational(1, 3) + ans = 2**e + assert Mul(*args, evaluate=False)**e == ans + assert Mul(*args)**e == ans + args = [I, I, I, 2] + e = Rational(1, 3) + ans = 2**e*(-I)**e + assert Mul(*args, evaluate=False)**e == ans + assert Mul(*args)**e == ans + args.append(-3) + ans = (6*I)**e + assert Mul(*args, evaluate=False)**e == ans + assert Mul(*args)**e == ans + args.append(-1) + ans = (-6*I)**e + assert Mul(*args, evaluate=False)**e == ans + assert Mul(*args)**e == ans + + args = [I, I, 2] + e = Rational(1, 3) + ans = (-2)**e + assert Mul(*args, evaluate=False)**e == ans + assert Mul(*args)**e == ans + args.append(-3) + ans = (6)**e + assert Mul(*args, evaluate=False)**e == ans + assert Mul(*args)**e == ans + args.append(-1) + ans = (-6)**e + assert Mul(*args, evaluate=False)**e == ans + assert Mul(*args)**e == ans + assert Mul(Pow(-1, Rational(3, 2), evaluate=False), I, I) == I + assert Mul(I*Pow(I, S.Half, evaluate=False)) == sqrt(I)*I + + +def test_real_mul(): + assert Float(0) * pi * x == 0 + assert set((Float(1) * pi * x).args) == {Float(1), pi, x} + + +def test_ncmul(): + A = Symbol("A", commutative=False) + B = Symbol("B", commutative=False) + C = Symbol("C", commutative=False) + assert A*B != B*A + assert A*B*C != C*B*A + assert A*b*B*3*C == 3*b*A*B*C + assert A*b*B*3*C != 3*b*B*A*C + assert A*b*B*3*C == 3*A*B*C*b + + assert A + B == B + A + assert (A + B)*C != C*(A + B) + + assert C*(A + B)*C != C*C*(A + B) + + assert A*A == A**2 + assert (A + B)*(A + B) == (A + B)**2 + + assert A**-1 * A == 1 + assert A/A == 1 + assert A/(A**2) == 1/A + + assert A/(1 + A) == A/(1 + A) + + assert set((A + B + 2*(A + B)).args) == \ + {A, B, 2*(A + B)} + + +def test_mul_add_identity(): + m = Mul(1, 2) + assert isinstance(m, Rational) and m.p == 2 and m.q == 1 + m = Mul(1, 2, evaluate=False) + assert isinstance(m, Mul) and m.args == (1, 2) + m = Mul(0, 1) + assert m is S.Zero + m = Mul(0, 1, evaluate=False) + assert isinstance(m, Mul) and m.args == (0, 1) + m = Add(0, 1) + assert m is S.One + m = Add(0, 1, evaluate=False) + assert isinstance(m, Add) and m.args == (0, 1) + + +def test_ncpow(): + x = Symbol('x', commutative=False) + y = Symbol('y', commutative=False) + z = Symbol('z', commutative=False) + a = Symbol('a') + b = Symbol('b') + c = Symbol('c') + + assert (x**2)*(y**2) != (y**2)*(x**2) + assert (x**-2)*y != y*(x**2) + assert 2**x*2**y != 2**(x + y) + assert 2**x*2**y*2**z != 2**(x + y + z) + assert 2**x*2**(2*x) == 2**(3*x) + assert 2**x*2**(2*x)*2**x == 2**(4*x) + assert exp(x)*exp(y) != exp(y)*exp(x) + assert exp(x)*exp(y)*exp(z) != exp(y)*exp(x)*exp(z) + assert exp(x)*exp(y)*exp(z) != exp(x + y + z) + assert x**a*x**b != x**(a + b) + assert x**a*x**b*x**c != x**(a + b + c) + assert x**3*x**4 == x**7 + assert x**3*x**4*x**2 == x**9 + assert x**a*x**(4*a) == x**(5*a) + assert x**a*x**(4*a)*x**a == x**(6*a) + + +def test_powerbug(): + x = Symbol("x") + assert x**1 != (-x)**1 + assert x**2 == (-x)**2 + assert x**3 != (-x)**3 + assert x**4 == (-x)**4 + assert x**5 != (-x)**5 + assert x**6 == (-x)**6 + + assert x**128 == (-x)**128 + assert x**129 != (-x)**129 + + assert (2*x)**2 == (-2*x)**2 + + +def test_Mul_doesnt_expand_exp(): + x = Symbol('x') + y = Symbol('y') + assert unchanged(Mul, exp(x), exp(y)) + assert unchanged(Mul, 2**x, 2**y) + assert x**2*x**3 == x**5 + assert 2**x*3**x == 6**x + assert x**(y)*x**(2*y) == x**(3*y) + assert sqrt(2)*sqrt(2) == 2 + assert 2**x*2**(2*x) == 2**(3*x) + assert sqrt(2)*2**Rational(1, 4)*5**Rational(3, 4) == 10**Rational(3, 4) + assert (x**(-log(5)/log(3))*x)/(x*x**( - log(5)/log(3))) == sympify(1) + + +def test_Mul_is_integer(): + k = Symbol('k', integer=True) + n = Symbol('n', integer=True) + nr = Symbol('nr', rational=False) + ir = Symbol('ir', irrational=True) + nz = Symbol('nz', integer=True, zero=False) + e = Symbol('e', even=True) + o = Symbol('o', odd=True) + i2 = Symbol('2', prime=True, even=True) + + assert (k/3).is_integer is None + assert (nz/3).is_integer is None + assert (nr/3).is_integer is False + assert (ir/3).is_integer is False + assert (x*k*n).is_integer is None + assert (e/2).is_integer is True + assert (e**2/2).is_integer is True + assert (2/k).is_integer is None + assert (2/k**2).is_integer is None + assert ((-1)**k*n).is_integer is True + assert (3*k*e/2).is_integer is True + assert (2*k*e/3).is_integer is None + assert (e/o).is_integer is None + assert (o/e).is_integer is False + assert (o/i2).is_integer is False + assert Mul(k, 1/k, evaluate=False).is_integer is None + assert Mul(2., S.Half, evaluate=False).is_integer is None + assert (2*sqrt(k)).is_integer is None + assert (2*k**n).is_integer is None + + s = 2**2**2**Pow(2, 1000, evaluate=False) + m = Mul(s, s, evaluate=False) + assert m.is_integer + + # broken in 1.6 and before, see #20161 + xq = Symbol('xq', rational=True) + yq = Symbol('yq', rational=True) + assert (xq*yq).is_integer is None + e_20161 = Mul(-1,Mul(1,Pow(2,-1,evaluate=False),evaluate=False),evaluate=False) + assert e_20161.is_integer is not True # expand(e_20161) -> -1/2, but no need to see that in the assumption without evaluation + + +def test_Add_Mul_is_integer(): + x = Symbol('x') + + k = Symbol('k', integer=True) + n = Symbol('n', integer=True) + nk = Symbol('nk', integer=False) + nr = Symbol('nr', rational=False) + nz = Symbol('nz', integer=True, zero=False) + + assert (-nk).is_integer is None + assert (-nr).is_integer is False + assert (2*k).is_integer is True + assert (-k).is_integer is True + + assert (k + nk).is_integer is False + assert (k + n).is_integer is True + assert (k + x).is_integer is None + assert (k + n*x).is_integer is None + assert (k + n/3).is_integer is None + assert (k + nz/3).is_integer is None + assert (k + nr/3).is_integer is False + + assert ((1 + sqrt(3))*(-sqrt(3) + 1)).is_integer is not False + assert (1 + (1 + sqrt(3))*(-sqrt(3) + 1)).is_integer is not False + + +def test_Add_Mul_is_finite(): + x = Symbol('x', extended_real=True, finite=False) + + assert sin(x).is_finite is True + assert (x*sin(x)).is_finite is None + assert (x*atan(x)).is_finite is False + assert (1024*sin(x)).is_finite is True + assert (sin(x)*exp(x)).is_finite is None + assert (sin(x)*cos(x)).is_finite is True + assert (x*sin(x)*exp(x)).is_finite is None + + assert (sin(x) - 67).is_finite is True + assert (sin(x) + exp(x)).is_finite is not True + assert (1 + x).is_finite is False + assert (1 + x**2 + (1 + x)*(1 - x)).is_finite is None + assert (sqrt(2)*(1 + x)).is_finite is False + assert (sqrt(2)*(1 + x)*(1 - x)).is_finite is False + + +def test_Mul_is_even_odd(): + x = Symbol('x', integer=True) + y = Symbol('y', integer=True) + + k = Symbol('k', odd=True) + n = Symbol('n', odd=True) + m = Symbol('m', even=True) + + assert (2*x).is_even is True + assert (2*x).is_odd is False + + assert (3*x).is_even is None + assert (3*x).is_odd is None + + assert (k/3).is_integer is None + assert (k/3).is_even is None + assert (k/3).is_odd is None + + assert (2*n).is_even is True + assert (2*n).is_odd is False + + assert (2*m).is_even is True + assert (2*m).is_odd is False + + assert (-n).is_even is False + assert (-n).is_odd is True + + assert (k*n).is_even is False + assert (k*n).is_odd is True + + assert (k*m).is_even is True + assert (k*m).is_odd is False + + assert (k*n*m).is_even is True + assert (k*n*m).is_odd is False + + assert (k*m*x).is_even is True + assert (k*m*x).is_odd is False + + # issue 6791: + assert (x/2).is_integer is None + assert (k/2).is_integer is False + assert (m/2).is_integer is True + + assert (x*y).is_even is None + assert (x*x).is_even is None + assert (x*(x + k)).is_even is True + assert (x*(x + m)).is_even is None + + assert (x*y).is_odd is None + assert (x*x).is_odd is None + assert (x*(x + k)).is_odd is False + assert (x*(x + m)).is_odd is None + + # issue 8648 + assert (m**2/2).is_even + assert (m**2/3).is_even is False + assert (2/m**2).is_odd is False + assert (2/m).is_odd is None + + +@XFAIL +def test_evenness_in_ternary_integer_product_with_odd(): + # Tests that oddness inference is independent of term ordering. + # Term ordering at the point of testing depends on SymPy's symbol order, so + # we try to force a different order by modifying symbol names. + x = Symbol('x', integer=True) + y = Symbol('y', integer=True) + k = Symbol('k', odd=True) + assert (x*y*(y + k)).is_even is True + assert (y*x*(x + k)).is_even is True + + +def test_evenness_in_ternary_integer_product_with_even(): + x = Symbol('x', integer=True) + y = Symbol('y', integer=True) + m = Symbol('m', even=True) + assert (x*y*(y + m)).is_even is None + + +@XFAIL +def test_oddness_in_ternary_integer_product_with_odd(): + # Tests that oddness inference is independent of term ordering. + # Term ordering at the point of testing depends on SymPy's symbol order, so + # we try to force a different order by modifying symbol names. + x = Symbol('x', integer=True) + y = Symbol('y', integer=True) + k = Symbol('k', odd=True) + assert (x*y*(y + k)).is_odd is False + assert (y*x*(x + k)).is_odd is False + + +def test_oddness_in_ternary_integer_product_with_even(): + x = Symbol('x', integer=True) + y = Symbol('y', integer=True) + m = Symbol('m', even=True) + assert (x*y*(y + m)).is_odd is None + + +def test_Mul_is_rational(): + x = Symbol('x') + n = Symbol('n', integer=True) + m = Symbol('m', integer=True, nonzero=True) + + assert (n/m).is_rational is True + assert (x/pi).is_rational is None + assert (x/n).is_rational is None + assert (m/pi).is_rational is False + + r = Symbol('r', rational=True) + assert (pi*r).is_rational is None + + # issue 8008 + z = Symbol('z', zero=True) + i = Symbol('i', imaginary=True) + assert (z*i).is_rational is True + bi = Symbol('i', imaginary=True, finite=True) + assert (z*bi).is_zero is True + + +def test_Add_is_rational(): + x = Symbol('x') + n = Symbol('n', rational=True) + m = Symbol('m', rational=True) + + assert (n + m).is_rational is True + assert (x + pi).is_rational is None + assert (x + n).is_rational is None + assert (n + pi).is_rational is False + + +def test_Add_is_even_odd(): + x = Symbol('x', integer=True) + + k = Symbol('k', odd=True) + n = Symbol('n', odd=True) + m = Symbol('m', even=True) + + assert (k + 7).is_even is True + assert (k + 7).is_odd is False + + assert (-k + 7).is_even is True + assert (-k + 7).is_odd is False + + assert (k - 12).is_even is False + assert (k - 12).is_odd is True + + assert (-k - 12).is_even is False + assert (-k - 12).is_odd is True + + assert (k + n).is_even is True + assert (k + n).is_odd is False + + assert (k + m).is_even is False + assert (k + m).is_odd is True + + assert (k + n + m).is_even is True + assert (k + n + m).is_odd is False + + assert (k + n + x + m).is_even is None + assert (k + n + x + m).is_odd is None + + +def test_Mul_is_negative_positive(): + x = Symbol('x', real=True) + y = Symbol('y', extended_real=False, complex=True) + z = Symbol('z', zero=True) + + e = 2*z + assert e.is_Mul and e.is_positive is False and e.is_negative is False + + neg = Symbol('neg', negative=True) + pos = Symbol('pos', positive=True) + nneg = Symbol('nneg', nonnegative=True) + npos = Symbol('npos', nonpositive=True) + + assert neg.is_negative is True + assert (-neg).is_negative is False + assert (2*neg).is_negative is True + + assert (2*pos)._eval_is_extended_negative() is False + assert (2*pos).is_negative is False + + assert pos.is_negative is False + assert (-pos).is_negative is True + assert (2*pos).is_negative is False + + assert (pos*neg).is_negative is True + assert (2*pos*neg).is_negative is True + assert (-pos*neg).is_negative is False + assert (pos*neg*y).is_negative is False # y.is_real=F; !real -> !neg + + assert nneg.is_negative is False + assert (-nneg).is_negative is None + assert (2*nneg).is_negative is False + + assert npos.is_negative is None + assert (-npos).is_negative is False + assert (2*npos).is_negative is None + + assert (nneg*npos).is_negative is None + + assert (neg*nneg).is_negative is None + assert (neg*npos).is_negative is False + + assert (pos*nneg).is_negative is False + assert (pos*npos).is_negative is None + + assert (npos*neg*nneg).is_negative is False + assert (npos*pos*nneg).is_negative is None + + assert (-npos*neg*nneg).is_negative is None + assert (-npos*pos*nneg).is_negative is False + + assert (17*npos*neg*nneg).is_negative is False + assert (17*npos*pos*nneg).is_negative is None + + assert (neg*npos*pos*nneg).is_negative is False + + assert (x*neg).is_negative is None + assert (nneg*npos*pos*x*neg).is_negative is None + + assert neg.is_positive is False + assert (-neg).is_positive is True + assert (2*neg).is_positive is False + + assert pos.is_positive is True + assert (-pos).is_positive is False + assert (2*pos).is_positive is True + + assert (pos*neg).is_positive is False + assert (2*pos*neg).is_positive is False + assert (-pos*neg).is_positive is True + assert (-pos*neg*y).is_positive is False # y.is_real=F; !real -> !neg + + assert nneg.is_positive is None + assert (-nneg).is_positive is False + assert (2*nneg).is_positive is None + + assert npos.is_positive is False + assert (-npos).is_positive is None + assert (2*npos).is_positive is False + + assert (nneg*npos).is_positive is False + + assert (neg*nneg).is_positive is False + assert (neg*npos).is_positive is None + + assert (pos*nneg).is_positive is None + assert (pos*npos).is_positive is False + + assert (npos*neg*nneg).is_positive is None + assert (npos*pos*nneg).is_positive is False + + assert (-npos*neg*nneg).is_positive is False + assert (-npos*pos*nneg).is_positive is None + + assert (17*npos*neg*nneg).is_positive is None + assert (17*npos*pos*nneg).is_positive is False + + assert (neg*npos*pos*nneg).is_positive is None + + assert (x*neg).is_positive is None + assert (nneg*npos*pos*x*neg).is_positive is None + + +def test_Mul_is_negative_positive_2(): + a = Symbol('a', nonnegative=True) + b = Symbol('b', nonnegative=True) + c = Symbol('c', nonpositive=True) + d = Symbol('d', nonpositive=True) + + assert (a*b).is_nonnegative is True + assert (a*b).is_negative is False + assert (a*b).is_zero is None + assert (a*b).is_positive is None + + assert (c*d).is_nonnegative is True + assert (c*d).is_negative is False + assert (c*d).is_zero is None + assert (c*d).is_positive is None + + assert (a*c).is_nonpositive is True + assert (a*c).is_positive is False + assert (a*c).is_zero is None + assert (a*c).is_negative is None + + +def test_Mul_is_nonpositive_nonnegative(): + x = Symbol('x', real=True) + + k = Symbol('k', negative=True) + n = Symbol('n', positive=True) + u = Symbol('u', nonnegative=True) + v = Symbol('v', nonpositive=True) + + assert k.is_nonpositive is True + assert (-k).is_nonpositive is False + assert (2*k).is_nonpositive is True + + assert n.is_nonpositive is False + assert (-n).is_nonpositive is True + assert (2*n).is_nonpositive is False + + assert (n*k).is_nonpositive is True + assert (2*n*k).is_nonpositive is True + assert (-n*k).is_nonpositive is False + + assert u.is_nonpositive is None + assert (-u).is_nonpositive is True + assert (2*u).is_nonpositive is None + + assert v.is_nonpositive is True + assert (-v).is_nonpositive is None + assert (2*v).is_nonpositive is True + + assert (u*v).is_nonpositive is True + + assert (k*u).is_nonpositive is True + assert (k*v).is_nonpositive is None + + assert (n*u).is_nonpositive is None + assert (n*v).is_nonpositive is True + + assert (v*k*u).is_nonpositive is None + assert (v*n*u).is_nonpositive is True + + assert (-v*k*u).is_nonpositive is True + assert (-v*n*u).is_nonpositive is None + + assert (17*v*k*u).is_nonpositive is None + assert (17*v*n*u).is_nonpositive is True + + assert (k*v*n*u).is_nonpositive is None + + assert (x*k).is_nonpositive is None + assert (u*v*n*x*k).is_nonpositive is None + + assert k.is_nonnegative is False + assert (-k).is_nonnegative is True + assert (2*k).is_nonnegative is False + + assert n.is_nonnegative is True + assert (-n).is_nonnegative is False + assert (2*n).is_nonnegative is True + + assert (n*k).is_nonnegative is False + assert (2*n*k).is_nonnegative is False + assert (-n*k).is_nonnegative is True + + assert u.is_nonnegative is True + assert (-u).is_nonnegative is None + assert (2*u).is_nonnegative is True + + assert v.is_nonnegative is None + assert (-v).is_nonnegative is True + assert (2*v).is_nonnegative is None + + assert (u*v).is_nonnegative is None + + assert (k*u).is_nonnegative is None + assert (k*v).is_nonnegative is True + + assert (n*u).is_nonnegative is True + assert (n*v).is_nonnegative is None + + assert (v*k*u).is_nonnegative is True + assert (v*n*u).is_nonnegative is None + + assert (-v*k*u).is_nonnegative is None + assert (-v*n*u).is_nonnegative is True + + assert (17*v*k*u).is_nonnegative is True + assert (17*v*n*u).is_nonnegative is None + + assert (k*v*n*u).is_nonnegative is True + + assert (x*k).is_nonnegative is None + assert (u*v*n*x*k).is_nonnegative is None + + +def test_Add_is_negative_positive(): + x = Symbol('x', real=True) + + k = Symbol('k', negative=True) + n = Symbol('n', positive=True) + u = Symbol('u', nonnegative=True) + v = Symbol('v', nonpositive=True) + + assert (k - 2).is_negative is True + assert (k + 17).is_negative is None + assert (-k - 5).is_negative is None + assert (-k + 123).is_negative is False + + assert (k - n).is_negative is True + assert (k + n).is_negative is None + assert (-k - n).is_negative is None + assert (-k + n).is_negative is False + + assert (k - n - 2).is_negative is True + assert (k + n + 17).is_negative is None + assert (-k - n - 5).is_negative is None + assert (-k + n + 123).is_negative is False + + assert (-2*k + 123*n + 17).is_negative is False + + assert (k + u).is_negative is None + assert (k + v).is_negative is True + assert (n + u).is_negative is False + assert (n + v).is_negative is None + + assert (u - v).is_negative is False + assert (u + v).is_negative is None + assert (-u - v).is_negative is None + assert (-u + v).is_negative is None + + assert (u - v + n + 2).is_negative is False + assert (u + v + n + 2).is_negative is None + assert (-u - v + n + 2).is_negative is None + assert (-u + v + n + 2).is_negative is None + + assert (k + x).is_negative is None + assert (k + x - n).is_negative is None + + assert (k - 2).is_positive is False + assert (k + 17).is_positive is None + assert (-k - 5).is_positive is None + assert (-k + 123).is_positive is True + + assert (k - n).is_positive is False + assert (k + n).is_positive is None + assert (-k - n).is_positive is None + assert (-k + n).is_positive is True + + assert (k - n - 2).is_positive is False + assert (k + n + 17).is_positive is None + assert (-k - n - 5).is_positive is None + assert (-k + n + 123).is_positive is True + + assert (-2*k + 123*n + 17).is_positive is True + + assert (k + u).is_positive is None + assert (k + v).is_positive is False + assert (n + u).is_positive is True + assert (n + v).is_positive is None + + assert (u - v).is_positive is None + assert (u + v).is_positive is None + assert (-u - v).is_positive is None + assert (-u + v).is_positive is False + + assert (u - v - n - 2).is_positive is None + assert (u + v - n - 2).is_positive is None + assert (-u - v - n - 2).is_positive is None + assert (-u + v - n - 2).is_positive is False + + assert (n + x).is_positive is None + assert (n + x - k).is_positive is None + + z = (-3 - sqrt(5) + (-sqrt(10)/2 - sqrt(2)/2)**2) + assert z.is_zero + z = sqrt(1 + sqrt(3)) + sqrt(3 + 3*sqrt(3)) - sqrt(10 + 6*sqrt(3)) + assert z.is_zero + + +def test_Add_is_nonpositive_nonnegative(): + x = Symbol('x', real=True) + + k = Symbol('k', negative=True) + n = Symbol('n', positive=True) + u = Symbol('u', nonnegative=True) + v = Symbol('v', nonpositive=True) + + assert (u - 2).is_nonpositive is None + assert (u + 17).is_nonpositive is False + assert (-u - 5).is_nonpositive is True + assert (-u + 123).is_nonpositive is None + + assert (u - v).is_nonpositive is None + assert (u + v).is_nonpositive is None + assert (-u - v).is_nonpositive is None + assert (-u + v).is_nonpositive is True + + assert (u - v - 2).is_nonpositive is None + assert (u + v + 17).is_nonpositive is None + assert (-u - v - 5).is_nonpositive is None + assert (-u + v - 123).is_nonpositive is True + + assert (-2*u + 123*v - 17).is_nonpositive is True + + assert (k + u).is_nonpositive is None + assert (k + v).is_nonpositive is True + assert (n + u).is_nonpositive is False + assert (n + v).is_nonpositive is None + + assert (k - n).is_nonpositive is True + assert (k + n).is_nonpositive is None + assert (-k - n).is_nonpositive is None + assert (-k + n).is_nonpositive is False + + assert (k - n + u + 2).is_nonpositive is None + assert (k + n + u + 2).is_nonpositive is None + assert (-k - n + u + 2).is_nonpositive is None + assert (-k + n + u + 2).is_nonpositive is False + + assert (u + x).is_nonpositive is None + assert (v - x - n).is_nonpositive is None + + assert (u - 2).is_nonnegative is None + assert (u + 17).is_nonnegative is True + assert (-u - 5).is_nonnegative is False + assert (-u + 123).is_nonnegative is None + + assert (u - v).is_nonnegative is True + assert (u + v).is_nonnegative is None + assert (-u - v).is_nonnegative is None + assert (-u + v).is_nonnegative is None + + assert (u - v + 2).is_nonnegative is True + assert (u + v + 17).is_nonnegative is None + assert (-u - v - 5).is_nonnegative is None + assert (-u + v - 123).is_nonnegative is False + + assert (2*u - 123*v + 17).is_nonnegative is True + + assert (k + u).is_nonnegative is None + assert (k + v).is_nonnegative is False + assert (n + u).is_nonnegative is True + assert (n + v).is_nonnegative is None + + assert (k - n).is_nonnegative is False + assert (k + n).is_nonnegative is None + assert (-k - n).is_nonnegative is None + assert (-k + n).is_nonnegative is True + + assert (k - n - u - 2).is_nonnegative is False + assert (k + n - u - 2).is_nonnegative is None + assert (-k - n - u - 2).is_nonnegative is None + assert (-k + n - u - 2).is_nonnegative is None + + assert (u - x).is_nonnegative is None + assert (v + x + n).is_nonnegative is None + + +def test_Pow_is_integer(): + x = Symbol('x') + + k = Symbol('k', integer=True) + n = Symbol('n', integer=True, nonnegative=True) + m = Symbol('m', integer=True, positive=True) + + assert (k**2).is_integer is True + assert (k**(-2)).is_integer is None + assert ((m + 1)**(-2)).is_integer is False + assert (m**(-1)).is_integer is None # issue 8580 + + assert (2**k).is_integer is None + assert (2**(-k)).is_integer is None + + assert (2**n).is_integer is True + assert (2**(-n)).is_integer is None + + assert (2**m).is_integer is True + assert (2**(-m)).is_integer is False + + assert (x**2).is_integer is None + assert (2**x).is_integer is None + + assert (k**n).is_integer is True + assert (k**(-n)).is_integer is None + + assert (k**x).is_integer is None + assert (x**k).is_integer is None + + assert (k**(n*m)).is_integer is True + assert (k**(-n*m)).is_integer is None + + assert sqrt(3).is_integer is False + assert sqrt(.3).is_integer is False + assert Pow(3, 2, evaluate=False).is_integer is True + assert Pow(3, 0, evaluate=False).is_integer is True + assert Pow(3, -2, evaluate=False).is_integer is False + assert Pow(S.Half, 3, evaluate=False).is_integer is False + # decided by re-evaluating + assert Pow(3, S.Half, evaluate=False).is_integer is False + assert Pow(3, S.Half, evaluate=False).is_integer is False + assert Pow(4, S.Half, evaluate=False).is_integer is True + assert Pow(S.Half, -2, evaluate=False).is_integer is True + + assert ((-1)**k).is_integer + + # issue 8641 + x = Symbol('x', real=True, integer=False) + assert (x**2).is_integer is None + + # issue 10458 + x = Symbol('x', positive=True) + assert (1/(x + 1)).is_integer is False + assert (1/(-x - 1)).is_integer is False + assert (-1/(x + 1)).is_integer is False + # issue 23287 + assert (x**2/2).is_integer is None + + # issue 8648-like + k = Symbol('k', even=True) + assert (k**3/2).is_integer + assert (k**3/8).is_integer + assert (k**3/16).is_integer is None + assert (2/k).is_integer is None + assert (2/k**2).is_integer is False + o = Symbol('o', odd=True) + assert (k/o).is_integer is None + o = Symbol('o', odd=True, prime=True) + assert (k/o).is_integer is False + + +def test_Pow_is_real(): + x = Symbol('x', real=True) + y = Symbol('y', positive=True) + + assert (x**2).is_real is True + assert (x**3).is_real is True + assert (x**x).is_real is None + assert (y**x).is_real is True + + assert (x**Rational(1, 3)).is_real is None + assert (y**Rational(1, 3)).is_real is True + + assert sqrt(-1 - sqrt(2)).is_real is False + + i = Symbol('i', imaginary=True) + assert (i**i).is_real is None + assert (I**i).is_extended_real is True + assert ((-I)**i).is_extended_real is True + assert (2**i).is_real is None # (2**(pi/log(2) * I)) is real, 2**I is not + assert (2**I).is_real is False + assert (2**-I).is_real is False + assert (i**2).is_extended_real is True + assert (i**3).is_extended_real is False + assert (i**x).is_real is None # could be (-I)**(2/3) + e = Symbol('e', even=True) + o = Symbol('o', odd=True) + k = Symbol('k', integer=True) + assert (i**e).is_extended_real is True + assert (i**o).is_extended_real is False + assert (i**k).is_real is None + assert (i**(4*k)).is_extended_real is True + + x = Symbol("x", nonnegative=True) + y = Symbol("y", nonnegative=True) + assert im(x**y).expand(complex=True) is S.Zero + assert (x**y).is_real is True + i = Symbol('i', imaginary=True) + assert (exp(i)**I).is_extended_real is True + assert log(exp(i)).is_imaginary is None # i could be 2*pi*I + c = Symbol('c', complex=True) + assert log(c).is_real is None # c could be 0 or 2, too + assert log(exp(c)).is_real is None # log(0), log(E), ... + n = Symbol('n', negative=False) + assert log(n).is_real is None + n = Symbol('n', nonnegative=True) + assert log(n).is_real is None + + assert sqrt(-I).is_real is False # issue 7843 + + i = Symbol('i', integer=True) + assert (1/(i-1)).is_real is None + assert (1/(i-1)).is_extended_real is None + + # test issue 20715 + from sympy.core.parameters import evaluate + x = S(-1) + with evaluate(False): + assert x.is_negative is True + + f = Pow(x, -1) + with evaluate(False): + assert f.is_imaginary is False + + +def test_real_Pow(): + k = Symbol('k', integer=True, nonzero=True) + assert (k**(I*pi/log(k))).is_real + + +def test_Pow_is_finite(): + xe = Symbol('xe', extended_real=True) + xr = Symbol('xr', real=True) + p = Symbol('p', positive=True) + n = Symbol('n', negative=True) + i = Symbol('i', integer=True) + + assert (xe**2).is_finite is None # xe could be oo + assert (xr**2).is_finite is True + + assert (xe**xe).is_finite is None + assert (xr**xe).is_finite is None + assert (xe**xr).is_finite is None + # FIXME: The line below should be True rather than None + # assert (xr**xr).is_finite is True + assert (xr**xr).is_finite is None + + assert (p**xe).is_finite is None + assert (p**xr).is_finite is True + + assert (n**xe).is_finite is None + assert (n**xr).is_finite is True + + assert (sin(xe)**2).is_finite is True + assert (sin(xr)**2).is_finite is True + + assert (sin(xe)**xe).is_finite is None # xe, xr could be -pi + assert (sin(xr)**xr).is_finite is None + + # FIXME: Should the line below be True rather than None? + assert (sin(xe)**exp(xe)).is_finite is None + assert (sin(xr)**exp(xr)).is_finite is True + + assert (1/sin(xe)).is_finite is None # if zero, no, otherwise yes + assert (1/sin(xr)).is_finite is None + + assert (1/exp(xe)).is_finite is None # xe could be -oo + assert (1/exp(xr)).is_finite is True + + assert (1/S.Pi).is_finite is True + + assert (1/(i-1)).is_finite is None + + +def test_Pow_is_even_odd(): + x = Symbol('x') + + k = Symbol('k', even=True) + n = Symbol('n', odd=True) + m = Symbol('m', integer=True, nonnegative=True) + p = Symbol('p', integer=True, positive=True) + + assert ((-1)**n).is_odd + assert ((-1)**k).is_odd + assert ((-1)**(m - p)).is_odd + + assert (k**2).is_even is True + assert (n**2).is_even is False + assert (2**k).is_even is None + assert (x**2).is_even is None + + assert (k**m).is_even is None + assert (n**m).is_even is False + + assert (k**p).is_even is True + assert (n**p).is_even is False + + assert (m**k).is_even is None + assert (p**k).is_even is None + + assert (m**n).is_even is None + assert (p**n).is_even is None + + assert (k**x).is_even is None + assert (n**x).is_even is None + + assert (k**2).is_odd is False + assert (n**2).is_odd is True + assert (3**k).is_odd is None + + assert (k**m).is_odd is None + assert (n**m).is_odd is True + + assert (k**p).is_odd is False + assert (n**p).is_odd is True + + assert (m**k).is_odd is None + assert (p**k).is_odd is None + + assert (m**n).is_odd is None + assert (p**n).is_odd is None + + assert (k**x).is_odd is None + assert (n**x).is_odd is None + + +def test_Pow_is_negative_positive(): + r = Symbol('r', real=True) + + k = Symbol('k', integer=True, positive=True) + n = Symbol('n', even=True) + m = Symbol('m', odd=True) + + x = Symbol('x') + + assert (2**r).is_positive is True + assert ((-2)**r).is_positive is None + assert ((-2)**n).is_positive is True + assert ((-2)**m).is_positive is False + + assert (k**2).is_positive is True + assert (k**(-2)).is_positive is True + + assert (k**r).is_positive is True + assert ((-k)**r).is_positive is None + assert ((-k)**n).is_positive is True + assert ((-k)**m).is_positive is False + + assert (2**r).is_negative is False + assert ((-2)**r).is_negative is None + assert ((-2)**n).is_negative is False + assert ((-2)**m).is_negative is True + + assert (k**2).is_negative is False + assert (k**(-2)).is_negative is False + + assert (k**r).is_negative is False + assert ((-k)**r).is_negative is None + assert ((-k)**n).is_negative is False + assert ((-k)**m).is_negative is True + + assert (2**x).is_positive is None + assert (2**x).is_negative is None + + +def test_Pow_is_zero(): + z = Symbol('z', zero=True) + e = z**2 + assert e.is_zero + assert e.is_positive is False + assert e.is_negative is False + + assert Pow(0, 0, evaluate=False).is_zero is False + assert Pow(0, 3, evaluate=False).is_zero + assert Pow(0, oo, evaluate=False).is_zero + assert Pow(0, -3, evaluate=False).is_zero is False + assert Pow(0, -oo, evaluate=False).is_zero is False + assert Pow(2, 2, evaluate=False).is_zero is False + + a = Symbol('a', zero=False) + assert Pow(a, 3).is_zero is False # issue 7965 + + assert Pow(2, oo, evaluate=False).is_zero is False + assert Pow(2, -oo, evaluate=False).is_zero + assert Pow(S.Half, oo, evaluate=False).is_zero + assert Pow(S.Half, -oo, evaluate=False).is_zero is False + + # All combinations of real/complex base/exponent + h = S.Half + T = True + F = False + N = None + + pow_iszero = [ + ['**', 0, h, 1, 2, -h, -1,-2,-2*I,-I/2,I/2,1+I,oo,-oo,zoo], + [ 0, F, T, T, T, F, F, F, F, F, F, N, T, F, N], + [ h, F, F, F, F, F, F, F, F, F, F, F, T, F, N], + [ 1, F, F, F, F, F, F, F, F, F, F, F, F, F, N], + [ 2, F, F, F, F, F, F, F, F, F, F, F, F, T, N], + [ -h, F, F, F, F, F, F, F, F, F, F, F, T, F, N], + [ -1, F, F, F, F, F, F, F, F, F, F, F, F, F, N], + [ -2, F, F, F, F, F, F, F, F, F, F, F, F, T, N], + [-2*I, F, F, F, F, F, F, F, F, F, F, F, F, T, N], + [-I/2, F, F, F, F, F, F, F, F, F, F, F, T, F, N], + [ I/2, F, F, F, F, F, F, F, F, F, F, F, T, F, N], + [ 1+I, F, F, F, F, F, F, F, F, F, F, F, F, T, N], + [ oo, F, F, F, F, T, T, T, F, F, F, F, F, T, N], + [ -oo, F, F, F, F, T, T, T, F, F, F, F, F, T, N], + [ zoo, F, F, F, F, T, T, T, N, N, N, N, F, T, N] + ] + + def test_table(table): + n = len(table[0]) + for row in range(1, n): + base = table[row][0] + for col in range(1, n): + exp = table[0][col] + is_zero = table[row][col] + # The actual test here: + assert Pow(base, exp, evaluate=False).is_zero is is_zero + + test_table(pow_iszero) + + # A zero symbol... + zo, zo2 = symbols('zo, zo2', zero=True) + + # All combinations of finite symbols + zf, zf2 = symbols('zf, zf2', finite=True) + wf, wf2 = symbols('wf, wf2', nonzero=True) + xf, xf2 = symbols('xf, xf2', real=True) + yf, yf2 = symbols('yf, yf2', nonzero=True) + af, af2 = symbols('af, af2', positive=True) + bf, bf2 = symbols('bf, bf2', nonnegative=True) + cf, cf2 = symbols('cf, cf2', negative=True) + df, df2 = symbols('df, df2', nonpositive=True) + + # Without finiteness: + zi, zi2 = symbols('zi, zi2') + wi, wi2 = symbols('wi, wi2', zero=False) + xi, xi2 = symbols('xi, xi2', extended_real=True) + yi, yi2 = symbols('yi, yi2', zero=False, extended_real=True) + ai, ai2 = symbols('ai, ai2', extended_positive=True) + bi, bi2 = symbols('bi, bi2', extended_nonnegative=True) + ci, ci2 = symbols('ci, ci2', extended_negative=True) + di, di2 = symbols('di, di2', extended_nonpositive=True) + + pow_iszero_sym = [ + ['**',zo,wf,yf,af,cf,zf,xf,bf,df,zi,wi,xi,yi,ai,bi,ci,di], + [ zo2, F, N, N, T, F, N, N, N, F, N, N, N, N, T, N, F, F], + [ wf2, F, F, F, F, F, F, F, F, F, N, N, N, N, N, N, N, N], + [ yf2, F, F, F, F, F, F, F, F, F, N, N, N, N, N, N, N, N], + [ af2, F, F, F, F, F, F, F, F, F, N, N, N, N, N, N, N, N], + [ cf2, F, F, F, F, F, F, F, F, F, N, N, N, N, N, N, N, N], + [ zf2, N, N, N, N, F, N, N, N, N, N, N, N, N, N, N, N, N], + [ xf2, N, N, N, N, F, N, N, N, N, N, N, N, N, N, N, N, N], + [ bf2, N, N, N, N, F, N, N, N, N, N, N, N, N, N, N, N, N], + [ df2, N, N, N, N, F, N, N, N, N, N, N, N, N, N, N, N, N], + [ zi2, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N], + [ wi2, F, N, N, F, N, N, N, F, N, N, N, N, N, N, N, N, N], + [ xi2, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N], + [ yi2, F, N, N, F, N, N, N, F, N, N, N, N, N, N, N, N, N], + [ ai2, F, N, N, F, N, N, N, F, N, N, N, N, N, N, N, N, N], + [ bi2, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N], + [ ci2, F, N, N, F, N, N, N, F, N, N, N, N, N, N, N, N, N], + [ di2, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N] + ] + + test_table(pow_iszero_sym) + + # In some cases (x**x).is_zero is different from (x**y).is_zero even if y + # has the same assumptions as x. + assert (zo ** zo).is_zero is False + assert (wf ** wf).is_zero is False + assert (yf ** yf).is_zero is False + assert (af ** af).is_zero is False + assert (cf ** cf).is_zero is False + assert (zf ** zf).is_zero is None + assert (xf ** xf).is_zero is None + assert (bf ** bf).is_zero is False # None in table + assert (df ** df).is_zero is None + assert (zi ** zi).is_zero is None + assert (wi ** wi).is_zero is None + assert (xi ** xi).is_zero is None + assert (yi ** yi).is_zero is None + assert (ai ** ai).is_zero is False # None in table + assert (bi ** bi).is_zero is False # None in table + assert (ci ** ci).is_zero is None + assert (di ** di).is_zero is None + + +def test_Pow_is_nonpositive_nonnegative(): + x = Symbol('x', real=True) + + k = Symbol('k', integer=True, nonnegative=True) + l = Symbol('l', integer=True, positive=True) + n = Symbol('n', even=True) + m = Symbol('m', odd=True) + + assert (x**(4*k)).is_nonnegative is True + assert (2**x).is_nonnegative is True + assert ((-2)**x).is_nonnegative is None + assert ((-2)**n).is_nonnegative is True + assert ((-2)**m).is_nonnegative is False + + assert (k**2).is_nonnegative is True + assert (k**(-2)).is_nonnegative is None + assert (k**k).is_nonnegative is True + + assert (k**x).is_nonnegative is None # NOTE (0**x).is_real = U + assert (l**x).is_nonnegative is True + assert (l**x).is_positive is True + assert ((-k)**x).is_nonnegative is None + + assert ((-k)**m).is_nonnegative is None + + assert (2**x).is_nonpositive is False + assert ((-2)**x).is_nonpositive is None + assert ((-2)**n).is_nonpositive is False + assert ((-2)**m).is_nonpositive is True + + assert (k**2).is_nonpositive is None + assert (k**(-2)).is_nonpositive is None + + assert (k**x).is_nonpositive is None + assert ((-k)**x).is_nonpositive is None + assert ((-k)**n).is_nonpositive is None + + + assert (x**2).is_nonnegative is True + i = symbols('i', imaginary=True) + assert (i**2).is_nonpositive is True + assert (i**4).is_nonpositive is False + assert (i**3).is_nonpositive is False + assert (I**i).is_nonnegative is True + assert (exp(I)**i).is_nonnegative is True + + assert ((-l)**n).is_nonnegative is True + assert ((-l)**m).is_nonpositive is True + assert ((-k)**n).is_nonnegative is None + assert ((-k)**m).is_nonpositive is None + + +def test_Mul_is_imaginary_real(): + r = Symbol('r', real=True) + p = Symbol('p', positive=True) + i1 = Symbol('i1', imaginary=True) + i2 = Symbol('i2', imaginary=True) + x = Symbol('x') + + assert I.is_imaginary is True + assert I.is_real is False + assert (-I).is_imaginary is True + assert (-I).is_real is False + assert (3*I).is_imaginary is True + assert (3*I).is_real is False + assert (I*I).is_imaginary is False + assert (I*I).is_real is True + + e = (p + p*I) + j = Symbol('j', integer=True, zero=False) + assert (e**j).is_real is None + assert (e**(2*j)).is_real is None + assert (e**j).is_imaginary is None + assert (e**(2*j)).is_imaginary is None + + assert (e**-1).is_imaginary is False + assert (e**2).is_imaginary + assert (e**3).is_imaginary is False + assert (e**4).is_imaginary is False + assert (e**5).is_imaginary is False + assert (e**-1).is_real is False + assert (e**2).is_real is False + assert (e**3).is_real is False + assert (e**4).is_real is True + assert (e**5).is_real is False + assert (e**3).is_complex + + assert (r*i1).is_imaginary is None + assert (r*i1).is_real is None + + assert (x*i1).is_imaginary is None + assert (x*i1).is_real is None + + assert (i1*i2).is_imaginary is False + assert (i1*i2).is_real is True + + assert (r*i1*i2).is_imaginary is False + assert (r*i1*i2).is_real is True + + # Github's issue 5874: + nr = Symbol('nr', real=False, complex=True) # e.g. I or 1 + I + a = Symbol('a', real=True, nonzero=True) + b = Symbol('b', real=True) + assert (i1*nr).is_real is None + assert (a*nr).is_real is False + assert (b*nr).is_real is None + + ni = Symbol('ni', imaginary=False, complex=True) # e.g. 2 or 1 + I + a = Symbol('a', real=True, nonzero=True) + b = Symbol('b', real=True) + assert (i1*ni).is_real is False + assert (a*ni).is_real is None + assert (b*ni).is_real is None + + +def test_Mul_hermitian_antihermitian(): + xz, yz = symbols('xz, yz', zero=True, antihermitian=True) + xf, yf = symbols('xf, yf', hermitian=False, antihermitian=False, finite=True) + xh, yh = symbols('xh, yh', hermitian=True, antihermitian=False, nonzero=True) + xa, ya = symbols('xa, ya', hermitian=False, antihermitian=True, zero=False, finite=True) + assert (xz*xh).is_hermitian is True + assert (xz*xh).is_antihermitian is True + assert (xz*xa).is_hermitian is True + assert (xz*xa).is_antihermitian is True + assert (xf*yf).is_hermitian is None + assert (xf*yf).is_antihermitian is None + assert (xh*yh).is_hermitian is True + assert (xh*yh).is_antihermitian is False + assert (xh*ya).is_hermitian is False + assert (xh*ya).is_antihermitian is True + assert (xa*ya).is_hermitian is True + assert (xa*ya).is_antihermitian is False + + a = Symbol('a', hermitian=True, zero=False) + b = Symbol('b', hermitian=True) + c = Symbol('c', hermitian=False) + d = Symbol('d', antihermitian=True) + e1 = Mul(a, b, c, evaluate=False) + e2 = Mul(b, a, c, evaluate=False) + e3 = Mul(a, b, c, d, evaluate=False) + e4 = Mul(b, a, c, d, evaluate=False) + e5 = Mul(a, c, evaluate=False) + e6 = Mul(a, c, d, evaluate=False) + assert e1.is_hermitian is None + assert e2.is_hermitian is None + assert e1.is_antihermitian is None + assert e2.is_antihermitian is None + assert e3.is_antihermitian is None + assert e4.is_antihermitian is None + assert e5.is_antihermitian is None + assert e6.is_antihermitian is None + + +def test_Add_is_comparable(): + assert (x + y).is_comparable is False + assert (x + 1).is_comparable is False + assert (Rational(1, 3) - sqrt(8)).is_comparable is True + + +def test_Mul_is_comparable(): + assert (x*y).is_comparable is False + assert (x*2).is_comparable is False + assert (sqrt(2)*Rational(1, 3)).is_comparable is True + + +def test_Pow_is_comparable(): + assert (x**y).is_comparable is False + assert (x**2).is_comparable is False + assert (sqrt(Rational(1, 3))).is_comparable is True + + +def test_Add_is_positive_2(): + e = Rational(1, 3) - sqrt(8) + assert e.is_positive is False + assert e.is_negative is True + + e = pi - 1 + assert e.is_positive is True + assert e.is_negative is False + + +def test_Add_is_irrational(): + i = Symbol('i', irrational=True) + + assert i.is_irrational is True + assert i.is_rational is False + + assert (i + 1).is_irrational is True + assert (i + 1).is_rational is False + + +def test_Mul_is_irrational(): + expr = Mul(1, 2, 3, evaluate=False) + assert expr.is_irrational is False + expr = Mul(1, I, I, evaluate=False) + assert expr.is_rational is None # I * I = -1 but *no evaluation allowed* + # sqrt(2) * I * I = -sqrt(2) is irrational but + # this can't be determined without evaluating the + # expression and the eval_is routines shouldn't do that + expr = Mul(sqrt(2), I, I, evaluate=False) + assert expr.is_irrational is None + + +def test_issue_3531(): + # https://github.com/sympy/sympy/issues/3531 + # https://github.com/sympy/sympy/pull/18116 + class MightyNumeric(tuple): + __slots__ = () + + def __rtruediv__(self, other): + return "something" + + assert sympify(1)/MightyNumeric((1, 2)) == "something" + + +def test_issue_3531b(): + class Foo: + def __init__(self): + self.field = 1.0 + + def __mul__(self, other): + self.field = self.field * other + + def __rmul__(self, other): + self.field = other * self.field + f = Foo() + x = Symbol("x") + assert f*x == x*f + + +def test_bug3(): + a = Symbol("a") + b = Symbol("b", positive=True) + e = 2*a + b + f = b + 2*a + assert e == f + + +def test_suppressed_evaluation(): + a = Add(0, 3, 2, evaluate=False) + b = Mul(1, 3, 2, evaluate=False) + c = Pow(3, 2, evaluate=False) + assert a != 6 + assert a.func is Add + assert a.args == (0, 3, 2) + assert b != 6 + assert b.func is Mul + assert b.args == (1, 3, 2) + assert c != 9 + assert c.func is Pow + assert c.args == (3, 2) + + +def test_AssocOp_doit(): + a = Add(x,x, evaluate=False) + b = Mul(y,y, evaluate=False) + c = Add(b,b, evaluate=False) + d = Mul(a,a, evaluate=False) + assert c.doit(deep=False).func == Mul + assert c.doit(deep=False).args == (2,y,y) + assert c.doit().func == Mul + assert c.doit().args == (2, Pow(y,2)) + assert d.doit(deep=False).func == Pow + assert d.doit(deep=False).args == (a, 2*S.One) + assert d.doit().func == Mul + assert d.doit().args == (4*S.One, Pow(x,2)) + + +def test_Add_Mul_Expr_args(): + nonexpr = [Basic(), Poly(x, x), FiniteSet(x)] + for typ in [Add, Mul]: + for obj in nonexpr: + # The cache can mess with the stacklevel check + with warns(SymPyDeprecationWarning, test_stacklevel=False): + typ(obj, 1) + + +def test_Add_as_coeff_mul(): + # issue 5524. These should all be (1, self) + assert (x + 1).as_coeff_mul() == (1, (x + 1,)) + assert (x + 2).as_coeff_mul() == (1, (x + 2,)) + assert (x + 3).as_coeff_mul() == (1, (x + 3,)) + + assert (x - 1).as_coeff_mul() == (1, (x - 1,)) + assert (x - 2).as_coeff_mul() == (1, (x - 2,)) + assert (x - 3).as_coeff_mul() == (1, (x - 3,)) + + n = Symbol('n', integer=True) + assert (n + 1).as_coeff_mul() == (1, (n + 1,)) + assert (n + 2).as_coeff_mul() == (1, (n + 2,)) + assert (n + 3).as_coeff_mul() == (1, (n + 3,)) + + assert (n - 1).as_coeff_mul() == (1, (n - 1,)) + assert (n - 2).as_coeff_mul() == (1, (n - 2,)) + assert (n - 3).as_coeff_mul() == (1, (n - 3,)) + + +def test_Pow_as_coeff_mul_doesnt_expand(): + assert exp(x + y).as_coeff_mul() == (1, (exp(x + y),)) + assert exp(x + exp(x + y)) != exp(x + exp(x)*exp(y)) + +def test_issue_24751(): + expr = Add(-2, -3, evaluate=False) + expr1 = Add(-1, expr, evaluate=False) + assert int(expr1) == int((-3 - 2) - 1) + + +def test_issue_3514_18626(): + assert sqrt(S.Half) * sqrt(6) == 2 * sqrt(3)/2 + assert S.Half*sqrt(6)*sqrt(2) == sqrt(3) + assert sqrt(6)/2*sqrt(2) == sqrt(3) + assert sqrt(6)*sqrt(2)/2 == sqrt(3) + assert sqrt(8)**Rational(2, 3) == 2 + + +def test_make_args(): + assert Add.make_args(x) == (x,) + assert Mul.make_args(x) == (x,) + + assert Add.make_args(x*y*z) == (x*y*z,) + assert Mul.make_args(x*y*z) == (x*y*z).args + + assert Add.make_args(x + y + z) == (x + y + z).args + assert Mul.make_args(x + y + z) == (x + y + z,) + + assert Add.make_args((x + y)**z) == ((x + y)**z,) + assert Mul.make_args((x + y)**z) == ((x + y)**z,) + + +def test_issue_5126(): + assert (-2)**x*(-3)**x != 6**x + i = Symbol('i', integer=1) + assert (-2)**i*(-3)**i == 6**i + + +def test_Rational_as_content_primitive(): + c, p = S.One, S.Zero + assert (c*p).as_content_primitive() == (c, p) + c, p = S.Half, S.One + assert (c*p).as_content_primitive() == (c, p) + + +def test_Add_as_content_primitive(): + assert (x + 2).as_content_primitive() == (1, x + 2) + + assert (3*x + 2).as_content_primitive() == (1, 3*x + 2) + assert (3*x + 3).as_content_primitive() == (3, x + 1) + assert (3*x + 6).as_content_primitive() == (3, x + 2) + + assert (3*x + 2*y).as_content_primitive() == (1, 3*x + 2*y) + assert (3*x + 3*y).as_content_primitive() == (3, x + y) + assert (3*x + 6*y).as_content_primitive() == (3, x + 2*y) + + assert (3/x + 2*x*y*z**2).as_content_primitive() == (1, 3/x + 2*x*y*z**2) + assert (3/x + 3*x*y*z**2).as_content_primitive() == (3, 1/x + x*y*z**2) + assert (3/x + 6*x*y*z**2).as_content_primitive() == (3, 1/x + 2*x*y*z**2) + + assert (2*x/3 + 4*y/9).as_content_primitive() == \ + (Rational(2, 9), 3*x + 2*y) + assert (2*x/3 + 2.5*y).as_content_primitive() == \ + (Rational(1, 3), 2*x + 7.5*y) + + # the coefficient may sort to a position other than 0 + p = 3 + x + y + assert (2*p).expand().as_content_primitive() == (2, p) + assert (2.0*p).expand().as_content_primitive() == (1, 2.*p) + p *= -1 + assert (2*p).expand().as_content_primitive() == (2, p) + + +def test_Mul_as_content_primitive(): + assert (2*x).as_content_primitive() == (2, x) + assert (x*(2 + 2*x)).as_content_primitive() == (2, x*(1 + x)) + assert (x*(2 + 2*y)*(3*x + 3)**2).as_content_primitive() == \ + (18, x*(1 + y)*(x + 1)**2) + assert ((2 + 2*x)**2*(3 + 6*x) + S.Half).as_content_primitive() == \ + (S.Half, 24*(x + 1)**2*(2*x + 1) + 1) + + +def test_Pow_as_content_primitive(): + assert (x**y).as_content_primitive() == (1, x**y) + assert ((2*x + 2)**y).as_content_primitive() == \ + (1, (Mul(2, (x + 1), evaluate=False))**y) + assert ((2*x + 2)**3).as_content_primitive() == (8, (x + 1)**3) + + +def test_issue_5460(): + u = Mul(2, (1 + x), evaluate=False) + assert (2 + u).args == (2, u) + + +def test_product_irrational(): + assert (I*pi).is_irrational is False + # The following used to be deduced from the above bug: + assert (I*pi).is_positive is False + + +def test_issue_5919(): + assert (x/(y*(1 + y))).expand() == x/(y**2 + y) + + +def test_Mod(): + assert Mod(x, 1).func is Mod + assert pi % pi is S.Zero + assert Mod(5, 3) == 2 + assert Mod(-5, 3) == 1 + assert Mod(5, -3) == -1 + assert Mod(-5, -3) == -2 + assert type(Mod(3.2, 2, evaluate=False)) == Mod + assert 5 % x == Mod(5, x) + assert x % 5 == Mod(x, 5) + assert x % y == Mod(x, y) + assert (x % y).subs({x: 5, y: 3}) == 2 + assert Mod(nan, 1) is nan + assert Mod(1, nan) is nan + assert Mod(nan, nan) is nan + + assert Mod(0, x) == 0 + with raises(ZeroDivisionError): + Mod(x, 0) + + k = Symbol('k', integer=True) + m = Symbol('m', integer=True, positive=True) + assert (x**m % x).func is Mod + assert (k**(-m) % k).func is Mod + assert k**m % k == 0 + assert (-2*k)**m % k == 0 + + # Float handling + point3 = Float(3.3) % 1 + assert (x - 3.3) % 1 == Mod(1.*x + 1 - point3, 1) + assert Mod(-3.3, 1) == 1 - point3 + assert Mod(0.7, 1) == Float(0.7) + e = Mod(1.3, 1) + assert comp(e, .3) and e.is_Float + e = Mod(1.3, .7) + assert comp(e, .6) and e.is_Float + e = Mod(1.3, Rational(7, 10)) + assert comp(e, .6) and e.is_Float + e = Mod(Rational(13, 10), 0.7) + assert comp(e, .6) and e.is_Float + e = Mod(Rational(13, 10), Rational(7, 10)) + assert comp(e, .6) and e.is_Rational + + # check that sign is right + r2 = sqrt(2) + r3 = sqrt(3) + for i in [-r3, -r2, r2, r3]: + for j in [-r3, -r2, r2, r3]: + assert verify_numerically(i % j, i.n() % j.n()) + for _x in range(4): + for _y in range(9): + reps = [(x, _x), (y, _y)] + assert Mod(3*x + y, 9).subs(reps) == (3*_x + _y) % 9 + + # denesting + t = Symbol('t', real=True) + assert Mod(Mod(x, t), t) == Mod(x, t) + assert Mod(-Mod(x, t), t) == Mod(-x, t) + assert Mod(Mod(x, 2*t), t) == Mod(x, t) + assert Mod(-Mod(x, 2*t), t) == Mod(-x, t) + assert Mod(Mod(x, t), 2*t) == Mod(x, t) + assert Mod(-Mod(x, t), -2*t) == -Mod(x, t) + for i in [-4, -2, 2, 4]: + for j in [-4, -2, 2, 4]: + for k in range(4): + assert Mod(Mod(x, i), j).subs({x: k}) == (k % i) % j + assert Mod(-Mod(x, i), j).subs({x: k}) == -(k % i) % j + + # known difference + assert Mod(5*sqrt(2), sqrt(5)) == 5*sqrt(2) - 3*sqrt(5) + p = symbols('p', positive=True) + assert Mod(2, p + 3) == 2 + assert Mod(-2, p + 3) == p + 1 + assert Mod(2, -p - 3) == -p - 1 + assert Mod(-2, -p - 3) == -2 + assert Mod(p + 5, p + 3) == 2 + assert Mod(-p - 5, p + 3) == p + 1 + assert Mod(p + 5, -p - 3) == -p - 1 + assert Mod(-p - 5, -p - 3) == -2 + assert Mod(p + 1, p - 1).func is Mod + + # issue 27749 + n = symbols('n', integer=True, positive=True) + assert unchanged(Mod, 1, n) + n = symbols('n', prime=True) + assert Mod(1, n) == 1 + + # handling sums + assert (x + 3) % 1 == Mod(x, 1) + assert (x + 3.0) % 1 == Mod(1.*x, 1) + assert (x - S(33)/10) % 1 == Mod(x + S(7)/10, 1) + + a = Mod(.6*x + y, .3*y) + b = Mod(0.1*y + 0.6*x, 0.3*y) + # Test that a, b are equal, with 1e-14 accuracy in coefficients + eps = 1e-14 + assert abs((a.args[0] - b.args[0]).subs({x: 1, y: 1})) < eps + assert abs((a.args[1] - b.args[1]).subs({x: 1, y: 1})) < eps + + assert (x + 1) % x == 1 % x + assert (x + y) % x == y % x + assert (x + y + 2) % x == (y + 2) % x + assert (a + 3*x + 1) % (2*x) == Mod(a + x + 1, 2*x) + assert (12*x + 18*y) % (3*x) == 3*Mod(6*y, x) + + # gcd extraction + assert (-3*x) % (-2*y) == -Mod(3*x, 2*y) + assert (.6*pi) % (.3*x*pi) == 0.3*pi*Mod(2, x) + assert (.6*pi) % (.31*x*pi) == pi*Mod(0.6, 0.31*x) + assert (6*pi) % (.3*x*pi) == 0.3*pi*Mod(20, x) + assert (6*pi) % (.31*x*pi) == pi*Mod(6, 0.31*x) + assert (6*pi) % (.42*x*pi) == pi*Mod(6, 0.42*x) + assert (12*x) % (2*y) == 2*Mod(6*x, y) + assert (12*x) % (3*5*y) == 3*Mod(4*x, 5*y) + assert (12*x) % (15*x*y) == 3*x*Mod(4, 5*y) + assert (-2*pi) % (3*pi) == pi + assert (2*x + 2) % (x + 1) == 0 + assert (x*(x + 1)) % (x + 1) == (x + 1)*Mod(x, 1) + assert Mod(5.0*x, 0.1*y) == 0.1*Mod(50*x, y) + i = Symbol('i', integer=True) + assert (3*i*x) % (2*i*y) == i*Mod(3*x, 2*y) + assert Mod(4*i, 4) == 0 + + # issue 8677 + n = Symbol('n', integer=True, positive=True) + assert factorial(n) % n == 0 + assert factorial(n + 2) % n == 0 + assert (factorial(n + 4) % (n + 5)).func is Mod + + # Wilson's theorem + assert factorial(18042, evaluate=False) % 18043 == 18042 + p = Symbol('n', prime=True) + assert factorial(p - 1) % p == p - 1 + assert factorial(p - 1) % -p == -1 + assert (factorial(3, evaluate=False) % 4).doit() == 2 + n = Symbol('n', composite=True, odd=True) + assert factorial(n - 1) % n == 0 + + # symbolic with known parity + n = Symbol('n', even=True) + assert Mod(n, 2) == 0 + n = Symbol('n', odd=True) + assert Mod(n, 2) == 1 + + # issue 10963 + assert (x**6000%400).args[1] == 400 + + #issue 13543 + assert Mod(Mod(x + 1, 2) + 1, 2) == Mod(x, 2) + + x1 = Symbol('x1', integer=True) + assert Mod(Mod(x1 + 2, 4)*(x1 + 4), 4) == Mod(x1*(x1 + 2), 4) + assert Mod(Mod(x1 + 2, 4)*4, 4) == 0 + + # issue 15493 + i, j = symbols('i j', integer=True, positive=True) + assert Mod(3*i, 2) == Mod(i, 2) + assert Mod(8*i/j, 4) == 4*Mod(2*i/j, 1) + assert Mod(8*i, 4) == 0 + + # rewrite + assert Mod(x, y).rewrite(floor) == x - y*floor(x/y) + assert ((x - Mod(x, y))/y).rewrite(floor) == floor(x/y) + + # issue 21373 + from sympy.functions.elementary.hyperbolic import sinh + from sympy.functions.elementary.piecewise import Piecewise + + x_r, y_r = symbols('x_r y_r', real=True) + assert (Piecewise((x_r, y_r > x_r), (y_r, True)) / z) % 1 + expr = exp(sinh(Piecewise((x_r, y_r > x_r), (y_r, True)) / z)) + expr.subs({1: 1.0}) + sinh(Piecewise((x_r, y_r > x_r), (y_r, True)) * z ** -1.0).is_zero + + # issue 24215 + from sympy.abc import phi + assert Mod(4.0*Mod(phi, 1) , 2) == 2.0*(Mod(2*(Mod(phi, 1)), 1)) + + xi = symbols('x', integer=True) + assert unchanged(Mod, xi, 2) + assert Mod(3*xi, 2) == Mod(xi, 2) + assert unchanged(Mod, 3*x, 2) + + +def test_Mod_Pow(): + # modular exponentiation + assert isinstance(Mod(Pow(2, 2, evaluate=False), 3), Integer) + + assert Mod(Pow(4, 13, evaluate=False), 497) == Mod(Pow(4, 13), 497) + assert Mod(Pow(2, 10000000000, evaluate=False), 3) == 1 + assert Mod(Pow(32131231232, 9**10**6, evaluate=False),10**12) == \ + pow(32131231232,9**10**6,10**12) + assert Mod(Pow(33284959323, 123**999, evaluate=False),11**13) == \ + pow(33284959323,123**999,11**13) + assert Mod(Pow(78789849597, 333**555, evaluate=False),12**9) == \ + pow(78789849597,333**555,12**9) + + # modular nested exponentiation + expr = Pow(2, 2, evaluate=False) + expr = Pow(2, expr, evaluate=False) + assert Mod(expr, 3**10) == 16 + expr = Pow(2, expr, evaluate=False) + assert Mod(expr, 3**10) == 6487 + expr = Pow(2, expr, evaluate=False) + assert Mod(expr, 3**10) == 32191 + expr = Pow(2, expr, evaluate=False) + assert Mod(expr, 3**10) == 18016 + expr = Pow(2, expr, evaluate=False) + assert Mod(expr, 3**10) == 5137 + + expr = Pow(2, 2, evaluate=False) + expr = Pow(expr, 2, evaluate=False) + assert Mod(expr, 3**10) == 16 + expr = Pow(expr, 2, evaluate=False) + assert Mod(expr, 3**10) == 256 + expr = Pow(expr, 2, evaluate=False) + assert Mod(expr, 3**10) == 6487 + expr = Pow(expr, 2, evaluate=False) + assert Mod(expr, 3**10) == 38281 + expr = Pow(expr, 2, evaluate=False) + assert Mod(expr, 3**10) == 15928 + + expr = Pow(2, 2, evaluate=False) + expr = Pow(expr, expr, evaluate=False) + assert Mod(expr, 3**10) == 256 + expr = Pow(expr, expr, evaluate=False) + assert Mod(expr, 3**10) == 9229 + expr = Pow(expr, expr, evaluate=False) + assert Mod(expr, 3**10) == 25708 + expr = Pow(expr, expr, evaluate=False) + assert Mod(expr, 3**10) == 26608 + expr = Pow(expr, expr, evaluate=False) + # XXX This used to fail in a nondeterministic way because of overflow + # error. + assert Mod(expr, 3**10) == 1966 + + +def test_Mod_is_integer(): + p = Symbol('p', integer=True) + q1 = Symbol('q1', integer=True) + q2 = Symbol('q2', integer=True, nonzero=True) + assert Mod(x, y).is_integer is None + assert Mod(p, q1).is_integer is None + assert Mod(x, q2).is_integer is None + assert Mod(p, q2).is_integer + + +def test_Mod_is_nonposneg(): + n = Symbol('n', integer=True) + k = Symbol('k', integer=True, positive=True) + assert (n%3).is_nonnegative + assert Mod(n, -3).is_nonpositive + assert Mod(n, k).is_nonnegative + assert Mod(n, -k).is_nonpositive + assert Mod(k, n).is_nonnegative is None + + +def test_issue_6001(): + A = Symbol("A", commutative=False) + eq = A + A**2 + # it doesn't matter whether it's True or False; they should + # just all be the same + assert ( + eq.is_commutative == + (eq + 1).is_commutative == + (A + 1).is_commutative) + + B = Symbol("B", commutative=False) + # Although commutative terms could cancel we return True + # meaning "there are non-commutative symbols; aftersubstitution + # that definition can change, e.g. (A*B).subs(B,A**-1) -> 1 + assert (sqrt(2)*A).is_commutative is False + assert (sqrt(2)*A*B).is_commutative is False + + +def test_polar(): + from sympy.functions.elementary.complexes import polar_lift + p = Symbol('p', polar=True) + x = Symbol('x') + assert p.is_polar + assert x.is_polar is None + assert S.One.is_polar is None + assert (p**x).is_polar is True + assert (x**p).is_polar is None + assert ((2*p)**x).is_polar is True + assert (2*p).is_polar is True + assert (-2*p).is_polar is not True + assert (polar_lift(-2)*p).is_polar is True + + q = Symbol('q', polar=True) + assert (p*q)**2 == p**2 * q**2 + assert (2*q)**2 == 4 * q**2 + assert ((p*q)**x).expand() == p**x * q**x + + +def test_issue_6040(): + a, b = Pow(1, 2, evaluate=False), S.One + assert a != b + assert b != a + assert not (a == b) + assert not (b == a) + + +def test_issue_6082(): + # Comparison is symmetric + assert Basic.compare(Max(x, 1), Max(x, 2)) == \ + - Basic.compare(Max(x, 2), Max(x, 1)) + # Equal expressions compare equal + assert Basic.compare(Max(x, 1), Max(x, 1)) == 0 + # Basic subtypes (such as Max) compare different than standard types + assert Basic.compare(Max(1, x), frozenset((1, x))) != 0 + + +def test_issue_6077(): + assert x**2.0/x == x**1.0 + assert x/x**2.0 == x**-1.0 + assert x*x**2.0 == x**3.0 + assert x**1.5*x**2.5 == x**4.0 + + assert 2**(2.0*x)/2**x == 2**(1.0*x) + assert 2**x/2**(2.0*x) == 2**(-1.0*x) + assert 2**x*2**(2.0*x) == 2**(3.0*x) + assert 2**(1.5*x)*2**(2.5*x) == 2**(4.0*x) + + +def test_mul_flatten_oo(): + p = symbols('p', positive=True) + n, m = symbols('n,m', negative=True) + x_im = symbols('x_im', imaginary=True) + assert n*oo is -oo + assert n*m*oo is oo + assert p*oo is oo + assert x_im*oo != I*oo # i could be +/- 3*I -> +/-oo + + +def test_add_flatten(): + # see https://github.com/sympy/sympy/issues/2633#issuecomment-29545524 + a = oo + I*oo + b = oo - I*oo + assert a + b is nan + assert a - b is nan + # FIXME: This evaluates as: + # >>> 1/a + # 0*(oo + oo*I) + # which should not simplify to 0. Should be fixed in Pow.eval + #assert (1/a).simplify() == (1/b).simplify() == 0 + + a = Pow(2, 3, evaluate=False) + assert a + a == 16 + + +def test_issue_5160_6087_6089_6090(): + # issue 6087 + assert ((-2*x*y**y)**3.2).n(2) == (2**3.2*(-x*y**y)**3.2).n(2) + # issue 6089 + A, B, C = symbols('A,B,C', commutative=False) + assert (2.*B*C)**3 == 8.0*(B*C)**3 + assert (-2.*B*C)**3 == -8.0*(B*C)**3 + assert (-2*B*C)**2 == 4*(B*C)**2 + # issue 5160 + assert sqrt(-1.0*x) == 1.0*sqrt(-x) + assert sqrt(1.0*x) == 1.0*sqrt(x) + # issue 6090 + assert (-2*x*y*A*B)**2 == 4*x**2*y**2*(A*B)**2 + + +def test_float_int_round(): + assert int(float(sqrt(10))) == int(sqrt(10)) + assert int(pi**1000) % 10 == 2 + assert int(Float('1.123456789012345678901234567890e20', '')) == \ + int(112345678901234567890) + assert int(Float('1.123456789012345678901234567890e25', '')) == \ + int(11234567890123456789012345) + # decimal forces float so it's not an exact integer ending in 000000 + assert int(Float('1.123456789012345678901234567890e35', '')) == \ + 112345678901234567890123456789000192 + assert int(Float('123456789012345678901234567890e5', '')) == \ + 12345678901234567890123456789000000 + assert Integer(Float('1.123456789012345678901234567890e20', '')) == \ + 112345678901234567890 + assert Integer(Float('1.123456789012345678901234567890e25', '')) == \ + 11234567890123456789012345 + # decimal forces float so it's not an exact integer ending in 000000 + assert Integer(Float('1.123456789012345678901234567890e35', '')) == \ + 112345678901234567890123456789000192 + assert Integer(Float('123456789012345678901234567890e5', '')) == \ + 12345678901234567890123456789000000 + assert same_and_same_prec(Float('123000e-2',''), Float('1230.00', '')) + assert same_and_same_prec(Float('123000e2',''), Float('12300000', '')) + + assert int(1 + Rational('.9999999999999999999999999')) == 1 + assert int(pi/1e20) == 0 + assert int(1 + pi/1e20) == 1 + assert int(Add(1.2, -2, evaluate=False)) == int(1.2 - 2) + assert int(Add(1.2, +2, evaluate=False)) == int(1.2 + 2) + assert int(Add(1 + Float('.99999999999999999', ''), evaluate=False)) == 1 + raises(TypeError, lambda: float(x)) + raises(TypeError, lambda: float(sqrt(-1))) + + assert int(12345678901234567890 + cos(1)**2 + sin(1)**2) == \ + 12345678901234567891 + + +def test_issue_6611a(): + assert Mul.flatten([3**Rational(1, 3), + Pow(-Rational(1, 9), Rational(2, 3), evaluate=False)]) == \ + ([Rational(1, 3), (-1)**Rational(2, 3)], [], None) + + +def test_denest_add_mul(): + # when working with evaluated expressions make sure they denest + eq = x + 1 + eq = Add(eq, 2, evaluate=False) + eq = Add(eq, 2, evaluate=False) + assert Add(*eq.args) == x + 5 + eq = x*2 + eq = Mul(eq, 2, evaluate=False) + eq = Mul(eq, 2, evaluate=False) + assert Mul(*eq.args) == 8*x + # but don't let them denest unnecessarily + eq = Mul(-2, x - 2, evaluate=False) + assert 2*eq == Mul(-4, x - 2, evaluate=False) + assert -eq == Mul(2, x - 2, evaluate=False) + + +def test_mul_coeff(): + # It is important that all Numbers be removed from the seq; + # This can be tricky when powers combine to produce those numbers + p = exp(I*pi/3) + assert p**2*x*p*y*p*x*p**2 == x**2*y + + +def test_mul_zero_detection(): + nz = Dummy(real=True, zero=False) + r = Dummy(extended_real=True) + c = Dummy(real=False, complex=True) + c2 = Dummy(real=False, complex=True) + i = Dummy(imaginary=True) + e = nz*r*c + assert e.is_imaginary is None + assert e.is_extended_real is None + e = nz*c + assert e.is_imaginary is None + assert e.is_extended_real is False + e = nz*i*c + assert e.is_imaginary is False + assert e.is_extended_real is None + # check for more than one complex; it is important to use + # uniquely named Symbols to ensure that two factors appear + # e.g. if the symbols have the same name they just become + # a single factor, a power. + e = nz*i*c*c2 + assert e.is_imaginary is None + assert e.is_extended_real is None + + # _eval_is_extended_real and _eval_is_zero both employ trapping of the + # zero value so args should be tested in both directions and + # TO AVOID GETTING THE CACHED RESULT, Dummy MUST BE USED + + # real is unknown + def test(z, b, e): + if z.is_zero and b.is_finite: + assert e.is_extended_real and e.is_zero + else: + assert e.is_extended_real is None + if b.is_finite: + if z.is_zero: + assert e.is_zero + else: + assert e.is_zero is None + elif b.is_finite is False: + if z.is_zero is None: + assert e.is_zero is None + else: + assert e.is_zero is False + + + for iz, ib in product(*[[True, False, None]]*2): + z = Dummy('z', nonzero=iz) + b = Dummy('f', finite=ib) + e = Mul(z, b, evaluate=False) + test(z, b, e) + z = Dummy('nz', nonzero=iz) + b = Dummy('f', finite=ib) + e = Mul(b, z, evaluate=False) + test(z, b, e) + + # real is True + def test(z, b, e): + if z.is_zero and not b.is_finite: + assert e.is_extended_real is None + else: + assert e.is_extended_real is True + + for iz, ib in product(*[[True, False, None]]*2): + z = Dummy('z', nonzero=iz, extended_real=True) + b = Dummy('b', finite=ib, extended_real=True) + e = Mul(z, b, evaluate=False) + test(z, b, e) + z = Dummy('z', nonzero=iz, extended_real=True) + b = Dummy('b', finite=ib, extended_real=True) + e = Mul(b, z, evaluate=False) + test(z, b, e) + + +def test_Mul_with_zero_infinite(): + zer = Dummy(zero=True) + inf = Dummy(finite=False) + + e = Mul(zer, inf, evaluate=False) + assert e.is_extended_positive is None + assert e.is_hermitian is None + + e = Mul(inf, zer, evaluate=False) + assert e.is_extended_positive is None + assert e.is_hermitian is None + + +def test_Mul_does_not_cancel_infinities(): + a, b = symbols('a b') + assert ((zoo + 3*a)/(3*a + zoo)) is nan + assert ((b - oo)/(b - oo)) is nan + # issue 13904 + expr = (1/(a+b) + 1/(a-b))/(1/(a+b) - 1/(a-b)) + assert expr.subs(b, a) is nan + + +def test_Mul_does_not_distribute_infinity(): + a, b = symbols('a b') + assert ((1 + I)*oo).is_Mul + assert ((a + b)*(-oo)).is_Mul + assert ((a + 1)*zoo).is_Mul + assert ((1 + I)*oo).is_finite is False + z = (1 + I)*oo + assert ((1 - I)*z).expand() is oo + + +def test_Mul_does_not_let_0_trump_inf(): + assert Mul(*[0, a + zoo]) is S.NaN + assert Mul(*[0, a + oo]) is S.NaN + assert Mul(*[0, a + Integral(1/x**2, (x, 1, oo))]) is S.Zero + # Integral is treated like an unknown like 0*x -> 0 + assert Mul(*[0, a + Integral(x, (x, 1, oo))]) is S.Zero + + +def test_issue_8247_8354(): + from sympy.functions.elementary.trigonometric import tan + z = sqrt(1 + sqrt(3)) + sqrt(3 + 3*sqrt(3)) - sqrt(10 + 6*sqrt(3)) + assert z.is_positive is False # it's 0 + z = S('''-2**(1/3)*(3*sqrt(93) + 29)**2 - 4*(3*sqrt(93) + 29)**(4/3) + + 12*sqrt(93)*(3*sqrt(93) + 29)**(1/3) + 116*(3*sqrt(93) + 29)**(1/3) + + 174*2**(1/3)*sqrt(93) + 1678*2**(1/3)''') + assert z.is_positive is False # it's 0 + z = 2*(-3*tan(19*pi/90) + sqrt(3))*cos(11*pi/90)*cos(19*pi/90) - \ + sqrt(3)*(-3 + 4*cos(19*pi/90)**2) + assert z.is_positive is not True # it's zero and it shouldn't hang + z = S('''9*(3*sqrt(93) + 29)**(2/3)*((3*sqrt(93) + + 29)**(1/3)*(-2**(2/3)*(3*sqrt(93) + 29)**(1/3) - 2) - 2*2**(1/3))**3 + + 72*(3*sqrt(93) + 29)**(2/3)*(81*sqrt(93) + 783) + (162*sqrt(93) + + 1566)*((3*sqrt(93) + 29)**(1/3)*(-2**(2/3)*(3*sqrt(93) + 29)**(1/3) - + 2) - 2*2**(1/3))**2''') + assert z.is_positive is False # it's 0 (and a single _mexpand isn't enough) + + +def test_Add_is_zero(): + x, y = symbols('x y', zero=True) + assert (x + y).is_zero + + # Issue 15873 + e = -2*I + (1 + I)**2 + assert e.is_zero is None + + +def test_issue_14392(): + assert (sin(zoo)**2).as_real_imag() == (nan, nan) + + +def test_divmod(): + assert divmod(x, y) == (x//y, x % y) + assert divmod(x, 3) == (x//3, x % 3) + assert divmod(3, x) == (3//x, 3 % x) + + +def test__neg__(): + assert -(x*y) == -x*y + assert -(-x*y) == x*y + assert -(1.*x) == -1.*x + assert -(-1.*x) == 1.*x + assert -(2.*x) == -2.*x + assert -(-2.*x) == 2.*x + with distribute(False): + eq = -(x + y) + assert eq.is_Mul and eq.args == (-1, x + y) + with evaluate(False): + eq = -(x + y) + assert eq.is_Mul and eq.args == (-1, x + y) + + +def test_issue_18507(): + assert Mul(zoo, zoo, 0) is nan + + +def test_issue_17130(): + e = Add(b, -b, I, -I, evaluate=False) + assert e.is_zero is None # ideally this would be True + + +def test_issue_21034(): + e = -I*log((re(asin(5)) + I*im(asin(5)))/sqrt(re(asin(5))**2 + im(asin(5))**2))/pi + assert e.round(2) + + +def test_issue_22021(): + from sympy.calculus.accumulationbounds import AccumBounds + # these objects are special cases in Mul + from sympy.tensor.tensor import TensorIndexType, tensor_indices, tensor_heads + L = TensorIndexType("L") + i = tensor_indices("i", L) + A, B = tensor_heads("A B", [L]) + e = A(i) + B(i) + assert -e == -1*e + e = zoo + x + assert -e == -1*e + a = AccumBounds(1, 2) + e = a + x + assert -e == -1*e + for args in permutations((zoo, a, x)): + e = Add(*args, evaluate=False) + assert -e == -1*e + assert 2*Add(1, x, x, evaluate=False) == 4*x + 2 + + +def test_issue_22244(): + assert -(zoo*x) == zoo*x + + +def test_issue_22453(): + from sympy.utilities.iterables import cartes + e = Symbol('e', extended_positive=True) + for a, b in cartes(*[[oo, -oo, 3]]*2): + if a == b == 3: + continue + i = a + I*b + assert i**(1 + e) is S.ComplexInfinity + assert i**-e is S.Zero + assert unchanged(Pow, i, e) + assert 1/(oo + I*oo) is S.Zero + r, i = [Dummy(infinite=True, extended_real=True) for _ in range(2)] + assert 1/(r + I*i) is S.Zero + assert 1/(3 + I*i) is S.Zero + assert 1/(r + I*3) is S.Zero + + +def test_issue_22613(): + assert (0**(x - 2)).as_content_primitive() == (1, 0**(x - 2)) + assert (0**(x + 2)).as_content_primitive() == (1, 0**(x + 2)) + + +def test_issue_25176(): + assert sqrt(-4*3**(S(3)/4)*I/3) == 2*3**(S(7)/8)*sqrt(-I)/3 diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_assumptions.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_assumptions.py new file mode 100644 index 0000000000000000000000000000000000000000..574e90178fb489fe99c99ea0c72df57ceec4b249 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_assumptions.py @@ -0,0 +1,1335 @@ +from sympy.core.mod import Mod +from sympy.core.numbers import (I, oo, pi) +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (asin, sin) +from sympy.simplify.simplify import simplify +from sympy.core import Symbol, S, Rational, Integer, Dummy, Wild, Pow +from sympy.core.assumptions import (assumptions, check_assumptions, + failing_assumptions, common_assumptions, _generate_assumption_rules, + _load_pre_generated_assumption_rules) +from sympy.core.facts import InconsistentAssumptions +from sympy.core.random import seed +from sympy.combinatorics import Permutation +from sympy.combinatorics.perm_groups import PermutationGroup + +from sympy.testing.pytest import raises, XFAIL + + +def test_symbol_unset(): + x = Symbol('x', real=True, integer=True) + assert x.is_real is True + assert x.is_integer is True + assert x.is_imaginary is False + assert x.is_noninteger is False + assert x.is_number is False + + +def test_zero(): + z = Integer(0) + assert z.is_commutative is True + assert z.is_integer is True + assert z.is_rational is True + assert z.is_algebraic is True + assert z.is_transcendental is False + assert z.is_real is True + assert z.is_complex is True + assert z.is_noninteger is False + assert z.is_irrational is False + assert z.is_imaginary is False + assert z.is_positive is False + assert z.is_negative is False + assert z.is_nonpositive is True + assert z.is_nonnegative is True + assert z.is_even is True + assert z.is_odd is False + assert z.is_finite is True + assert z.is_infinite is False + assert z.is_comparable is True + assert z.is_prime is False + assert z.is_composite is False + assert z.is_number is True + + +def test_one(): + z = Integer(1) + assert z.is_commutative is True + assert z.is_integer is True + assert z.is_rational is True + assert z.is_algebraic is True + assert z.is_transcendental is False + assert z.is_real is True + assert z.is_complex is True + assert z.is_noninteger is False + assert z.is_irrational is False + assert z.is_imaginary is False + assert z.is_positive is True + assert z.is_negative is False + assert z.is_nonpositive is False + assert z.is_nonnegative is True + assert z.is_even is False + assert z.is_odd is True + assert z.is_finite is True + assert z.is_infinite is False + assert z.is_comparable is True + assert z.is_prime is False + assert z.is_number is True + assert z.is_composite is False # issue 8807 + + +def test_negativeone(): + z = Integer(-1) + assert z.is_commutative is True + assert z.is_integer is True + assert z.is_rational is True + assert z.is_algebraic is True + assert z.is_transcendental is False + assert z.is_real is True + assert z.is_complex is True + assert z.is_noninteger is False + assert z.is_irrational is False + assert z.is_imaginary is False + assert z.is_positive is False + assert z.is_negative is True + assert z.is_nonpositive is True + assert z.is_nonnegative is False + assert z.is_even is False + assert z.is_odd is True + assert z.is_finite is True + assert z.is_infinite is False + assert z.is_comparable is True + assert z.is_prime is False + assert z.is_composite is False + assert z.is_number is True + + +def test_infinity(): + oo = S.Infinity + + assert oo.is_commutative is True + assert oo.is_integer is False + assert oo.is_rational is False + assert oo.is_algebraic is False + assert oo.is_transcendental is False + assert oo.is_extended_real is True + assert oo.is_real is False + assert oo.is_complex is False + assert oo.is_noninteger is True + assert oo.is_irrational is False + assert oo.is_imaginary is False + assert oo.is_nonzero is False + assert oo.is_positive is False + assert oo.is_negative is False + assert oo.is_nonpositive is False + assert oo.is_nonnegative is False + assert oo.is_extended_nonzero is True + assert oo.is_extended_positive is True + assert oo.is_extended_negative is False + assert oo.is_extended_nonpositive is False + assert oo.is_extended_nonnegative is True + assert oo.is_even is False + assert oo.is_odd is False + assert oo.is_finite is False + assert oo.is_infinite is True + assert oo.is_comparable is True + assert oo.is_prime is False + assert oo.is_composite is False + assert oo.is_number is True + + +def test_neg_infinity(): + mm = S.NegativeInfinity + + assert mm.is_commutative is True + assert mm.is_integer is False + assert mm.is_rational is False + assert mm.is_algebraic is False + assert mm.is_transcendental is False + assert mm.is_extended_real is True + assert mm.is_real is False + assert mm.is_complex is False + assert mm.is_noninteger is True + assert mm.is_irrational is False + assert mm.is_imaginary is False + assert mm.is_nonzero is False + assert mm.is_positive is False + assert mm.is_negative is False + assert mm.is_nonpositive is False + assert mm.is_nonnegative is False + assert mm.is_extended_nonzero is True + assert mm.is_extended_positive is False + assert mm.is_extended_negative is True + assert mm.is_extended_nonpositive is True + assert mm.is_extended_nonnegative is False + assert mm.is_even is False + assert mm.is_odd is False + assert mm.is_finite is False + assert mm.is_infinite is True + assert mm.is_comparable is True + assert mm.is_prime is False + assert mm.is_composite is False + assert mm.is_number is True + + +def test_zoo(): + zoo = S.ComplexInfinity + assert zoo.is_complex is False + assert zoo.is_real is False + assert zoo.is_prime is False + + +def test_nan(): + nan = S.NaN + + assert nan.is_commutative is True + assert nan.is_integer is None + assert nan.is_rational is None + assert nan.is_algebraic is None + assert nan.is_transcendental is None + assert nan.is_real is None + assert nan.is_complex is None + assert nan.is_noninteger is None + assert nan.is_irrational is None + assert nan.is_imaginary is None + assert nan.is_positive is None + assert nan.is_negative is None + assert nan.is_nonpositive is None + assert nan.is_nonnegative is None + assert nan.is_even is None + assert nan.is_odd is None + assert nan.is_finite is None + assert nan.is_infinite is None + assert nan.is_comparable is False + assert nan.is_prime is None + assert nan.is_composite is None + assert nan.is_number is True + + +def test_pos_rational(): + r = Rational(3, 4) + assert r.is_commutative is True + assert r.is_integer is False + assert r.is_rational is True + assert r.is_algebraic is True + assert r.is_transcendental is False + assert r.is_real is True + assert r.is_complex is True + assert r.is_noninteger is True + assert r.is_irrational is False + assert r.is_imaginary is False + assert r.is_positive is True + assert r.is_negative is False + assert r.is_nonpositive is False + assert r.is_nonnegative is True + assert r.is_even is False + assert r.is_odd is False + assert r.is_finite is True + assert r.is_infinite is False + assert r.is_comparable is True + assert r.is_prime is False + assert r.is_composite is False + + r = Rational(1, 4) + assert r.is_nonpositive is False + assert r.is_positive is True + assert r.is_negative is False + assert r.is_nonnegative is True + r = Rational(5, 4) + assert r.is_negative is False + assert r.is_positive is True + assert r.is_nonpositive is False + assert r.is_nonnegative is True + r = Rational(5, 3) + assert r.is_nonnegative is True + assert r.is_positive is True + assert r.is_negative is False + assert r.is_nonpositive is False + + +def test_neg_rational(): + r = Rational(-3, 4) + assert r.is_positive is False + assert r.is_nonpositive is True + assert r.is_negative is True + assert r.is_nonnegative is False + r = Rational(-1, 4) + assert r.is_nonpositive is True + assert r.is_positive is False + assert r.is_negative is True + assert r.is_nonnegative is False + r = Rational(-5, 4) + assert r.is_negative is True + assert r.is_positive is False + assert r.is_nonpositive is True + assert r.is_nonnegative is False + r = Rational(-5, 3) + assert r.is_nonnegative is False + assert r.is_positive is False + assert r.is_negative is True + assert r.is_nonpositive is True + + +def test_pi(): + z = S.Pi + assert z.is_commutative is True + assert z.is_integer is False + assert z.is_rational is False + assert z.is_algebraic is False + assert z.is_transcendental is True + assert z.is_real is True + assert z.is_complex is True + assert z.is_noninteger is True + assert z.is_irrational is True + assert z.is_imaginary is False + assert z.is_positive is True + assert z.is_negative is False + assert z.is_nonpositive is False + assert z.is_nonnegative is True + assert z.is_even is False + assert z.is_odd is False + assert z.is_finite is True + assert z.is_infinite is False + assert z.is_comparable is True + assert z.is_prime is False + assert z.is_composite is False + + +def test_E(): + z = S.Exp1 + assert z.is_commutative is True + assert z.is_integer is False + assert z.is_rational is False + assert z.is_algebraic is False + assert z.is_transcendental is True + assert z.is_real is True + assert z.is_complex is True + assert z.is_noninteger is True + assert z.is_irrational is True + assert z.is_imaginary is False + assert z.is_positive is True + assert z.is_negative is False + assert z.is_nonpositive is False + assert z.is_nonnegative is True + assert z.is_even is False + assert z.is_odd is False + assert z.is_finite is True + assert z.is_infinite is False + assert z.is_comparable is True + assert z.is_prime is False + assert z.is_composite is False + + +def test_I(): + z = S.ImaginaryUnit + assert z.is_commutative is True + assert z.is_integer is False + assert z.is_rational is False + assert z.is_algebraic is True + assert z.is_transcendental is False + assert z.is_real is False + assert z.is_complex is True + assert z.is_noninteger is False + assert z.is_irrational is False + assert z.is_imaginary is True + assert z.is_positive is False + assert z.is_negative is False + assert z.is_nonpositive is False + assert z.is_nonnegative is False + assert z.is_even is False + assert z.is_odd is False + assert z.is_finite is True + assert z.is_infinite is False + assert z.is_comparable is False + assert z.is_prime is False + assert z.is_composite is False + + +def test_symbol_real_false(): + # issue 3848 + a = Symbol('a', real=False) + + assert a.is_real is False + assert a.is_integer is False + assert a.is_zero is False + + assert a.is_negative is False + assert a.is_positive is False + assert a.is_nonnegative is False + assert a.is_nonpositive is False + assert a.is_nonzero is False + + assert a.is_extended_negative is None + assert a.is_extended_positive is None + assert a.is_extended_nonnegative is None + assert a.is_extended_nonpositive is None + assert a.is_extended_nonzero is None + + +def test_symbol_extended_real_false(): + # issue 3848 + a = Symbol('a', extended_real=False) + + assert a.is_real is False + assert a.is_integer is False + assert a.is_zero is False + + assert a.is_negative is False + assert a.is_positive is False + assert a.is_nonnegative is False + assert a.is_nonpositive is False + assert a.is_nonzero is False + + assert a.is_extended_negative is False + assert a.is_extended_positive is False + assert a.is_extended_nonnegative is False + assert a.is_extended_nonpositive is False + assert a.is_extended_nonzero is False + + +def test_symbol_imaginary(): + a = Symbol('a', imaginary=True) + + assert a.is_real is False + assert a.is_integer is False + assert a.is_negative is False + assert a.is_positive is False + assert a.is_nonnegative is False + assert a.is_nonpositive is False + assert a.is_zero is False + assert a.is_nonzero is False # since nonzero -> real + + +def test_symbol_zero(): + x = Symbol('x', zero=True) + assert x.is_positive is False + assert x.is_nonpositive + assert x.is_negative is False + assert x.is_nonnegative + assert x.is_zero is True + # TODO Change to x.is_nonzero is None + # See https://github.com/sympy/sympy/pull/9583 + assert x.is_nonzero is False + assert x.is_finite is True + + +def test_symbol_positive(): + x = Symbol('x', positive=True) + assert x.is_positive is True + assert x.is_nonpositive is False + assert x.is_negative is False + assert x.is_nonnegative is True + assert x.is_zero is False + assert x.is_nonzero is True + + +def test_neg_symbol_positive(): + x = -Symbol('x', positive=True) + assert x.is_positive is False + assert x.is_nonpositive is True + assert x.is_negative is True + assert x.is_nonnegative is False + assert x.is_zero is False + assert x.is_nonzero is True + + +def test_symbol_nonpositive(): + x = Symbol('x', nonpositive=True) + assert x.is_positive is False + assert x.is_nonpositive is True + assert x.is_negative is None + assert x.is_nonnegative is None + assert x.is_zero is None + assert x.is_nonzero is None + + +def test_neg_symbol_nonpositive(): + x = -Symbol('x', nonpositive=True) + assert x.is_positive is None + assert x.is_nonpositive is None + assert x.is_negative is False + assert x.is_nonnegative is True + assert x.is_zero is None + assert x.is_nonzero is None + + +def test_symbol_falsepositive(): + x = Symbol('x', positive=False) + assert x.is_positive is False + assert x.is_nonpositive is None + assert x.is_negative is None + assert x.is_nonnegative is None + assert x.is_zero is None + assert x.is_nonzero is None + + +def test_symbol_falsepositive_mul(): + # To test pull request 9379 + # Explicit handling of arg.is_positive=False was added to Mul._eval_is_positive + x = 2*Symbol('x', positive=False) + assert x.is_positive is False # This was None before + assert x.is_nonpositive is None + assert x.is_negative is None + assert x.is_nonnegative is None + assert x.is_zero is None + assert x.is_nonzero is None + + +@XFAIL +def test_symbol_infinitereal_mul(): + ix = Symbol('ix', infinite=True, extended_real=True) + assert (-ix).is_extended_positive is None + + +def test_neg_symbol_falsepositive(): + x = -Symbol('x', positive=False) + assert x.is_positive is None + assert x.is_nonpositive is None + assert x.is_negative is False + assert x.is_nonnegative is None + assert x.is_zero is None + assert x.is_nonzero is None + + +def test_neg_symbol_falsenegative(): + # To test pull request 9379 + # Explicit handling of arg.is_negative=False was added to Mul._eval_is_positive + x = -Symbol('x', negative=False) + assert x.is_positive is False # This was None before + assert x.is_nonpositive is None + assert x.is_negative is None + assert x.is_nonnegative is None + assert x.is_zero is None + assert x.is_nonzero is None + + +def test_symbol_falsepositive_real(): + x = Symbol('x', positive=False, real=True) + assert x.is_positive is False + assert x.is_nonpositive is True + assert x.is_negative is None + assert x.is_nonnegative is None + assert x.is_zero is None + assert x.is_nonzero is None + + +def test_neg_symbol_falsepositive_real(): + x = -Symbol('x', positive=False, real=True) + assert x.is_positive is None + assert x.is_nonpositive is None + assert x.is_negative is False + assert x.is_nonnegative is True + assert x.is_zero is None + assert x.is_nonzero is None + + +def test_symbol_falsenonnegative(): + x = Symbol('x', nonnegative=False) + assert x.is_positive is False + assert x.is_nonpositive is None + assert x.is_negative is None + assert x.is_nonnegative is False + assert x.is_zero is False + assert x.is_nonzero is None + + +@XFAIL +def test_neg_symbol_falsenonnegative(): + x = -Symbol('x', nonnegative=False) + assert x.is_positive is None + assert x.is_nonpositive is False # this currently returns None + assert x.is_negative is False # this currently returns None + assert x.is_nonnegative is None + assert x.is_zero is False # this currently returns None + assert x.is_nonzero is True # this currently returns None + + +def test_symbol_falsenonnegative_real(): + x = Symbol('x', nonnegative=False, real=True) + assert x.is_positive is False + assert x.is_nonpositive is True + assert x.is_negative is True + assert x.is_nonnegative is False + assert x.is_zero is False + assert x.is_nonzero is True + + +def test_neg_symbol_falsenonnegative_real(): + x = -Symbol('x', nonnegative=False, real=True) + assert x.is_positive is True + assert x.is_nonpositive is False + assert x.is_negative is False + assert x.is_nonnegative is True + assert x.is_zero is False + assert x.is_nonzero is True + + +def test_prime(): + assert S.NegativeOne.is_prime is False + assert S(-2).is_prime is False + assert S(-4).is_prime is False + assert S.Zero.is_prime is False + assert S.One.is_prime is False + assert S(2).is_prime is True + assert S(17).is_prime is True + assert S(4).is_prime is False + + +def test_composite(): + assert S.NegativeOne.is_composite is False + assert S(-2).is_composite is False + assert S(-4).is_composite is False + assert S.Zero.is_composite is False + assert S(2).is_composite is False + assert S(17).is_composite is False + assert S(4).is_composite is True + x = Dummy(integer=True, positive=True, prime=False) + assert x.is_composite is None # x could be 1 + assert (x + 1).is_composite is None + x = Dummy(positive=True, even=True, prime=False) + assert x.is_integer is True + assert x.is_composite is True + + +def test_prime_symbol(): + x = Symbol('x', prime=True) + assert x.is_prime is True + assert x.is_integer is True + assert x.is_positive is True + assert x.is_negative is False + assert x.is_nonpositive is False + assert x.is_nonnegative is True + + x = Symbol('x', prime=False) + assert x.is_prime is False + assert x.is_integer is None + assert x.is_positive is None + assert x.is_negative is None + assert x.is_nonpositive is None + assert x.is_nonnegative is None + + +def test_symbol_noncommutative(): + x = Symbol('x', commutative=True) + assert x.is_complex is None + + x = Symbol('x', commutative=False) + assert x.is_integer is False + assert x.is_rational is False + assert x.is_algebraic is False + assert x.is_irrational is False + assert x.is_real is False + assert x.is_complex is False + + +def test_other_symbol(): + x = Symbol('x', integer=True) + assert x.is_integer is True + assert x.is_real is True + assert x.is_finite is True + + x = Symbol('x', integer=True, nonnegative=True) + assert x.is_integer is True + assert x.is_nonnegative is True + assert x.is_negative is False + assert x.is_positive is None + assert x.is_finite is True + + x = Symbol('x', integer=True, nonpositive=True) + assert x.is_integer is True + assert x.is_nonpositive is True + assert x.is_positive is False + assert x.is_negative is None + assert x.is_finite is True + + x = Symbol('x', odd=True) + assert x.is_odd is True + assert x.is_even is False + assert x.is_integer is True + assert x.is_finite is True + + x = Symbol('x', odd=False) + assert x.is_odd is False + assert x.is_even is None + assert x.is_integer is None + assert x.is_finite is None + + x = Symbol('x', even=True) + assert x.is_even is True + assert x.is_odd is False + assert x.is_integer is True + assert x.is_finite is True + + x = Symbol('x', even=False) + assert x.is_even is False + assert x.is_odd is None + assert x.is_integer is None + assert x.is_finite is None + + x = Symbol('x', integer=True, nonnegative=True) + assert x.is_integer is True + assert x.is_nonnegative is True + assert x.is_finite is True + + x = Symbol('x', integer=True, nonpositive=True) + assert x.is_integer is True + assert x.is_nonpositive is True + assert x.is_finite is True + + x = Symbol('x', rational=True) + assert x.is_real is True + assert x.is_finite is True + + x = Symbol('x', rational=False) + assert x.is_real is None + assert x.is_finite is None + + x = Symbol('x', irrational=True) + assert x.is_real is True + assert x.is_finite is True + + x = Symbol('x', irrational=False) + assert x.is_real is None + assert x.is_finite is None + + with raises(AttributeError): + x.is_real = False + + x = Symbol('x', algebraic=True) + assert x.is_transcendental is False + x = Symbol('x', transcendental=True) + assert x.is_algebraic is False + assert x.is_rational is False + assert x.is_integer is False + + +def test_evaluate_false(): + # Previously this failed because the assumptions query would make new + # expressions and some of the evaluation logic would fail under + # evaluate(False). + from sympy.core.parameters import evaluate + from sympy.abc import x, h + f = 2**x**7 + with evaluate(False): + fh = f.xreplace({x: x+h}) + assert fh.exp.is_rational is None + + +def test_issue_3825(): + """catch: hash instability""" + x = Symbol("x") + y = Symbol("y") + a1 = x + y + a2 = y + x + a2.is_comparable + + h1 = hash(a1) + h2 = hash(a2) + assert h1 == h2 + + +def test_issue_4822(): + z = (-1)**Rational(1, 3)*(1 - I*sqrt(3)) + assert z.is_real in [True, None] + + +def test_hash_vs_typeinfo(): + """seemingly different typeinfo, but in fact equal""" + + # the following two are semantically equal + x1 = Symbol('x', even=True) + x2 = Symbol('x', integer=True, odd=False) + + assert hash(x1) == hash(x2) + assert x1 == x2 + + +def test_hash_vs_typeinfo_2(): + """different typeinfo should mean !eq""" + # the following two are semantically different + x = Symbol('x') + x1 = Symbol('x', even=True) + + assert x != x1 + assert hash(x) != hash(x1) # This might fail with very low probability + + +def test_hash_vs_eq(): + """catch: different hash for equal objects""" + a = 1 + S.Pi # important: do not fold it into a Number instance + ha = hash(a) # it should be Add/Mul/... to trigger the bug + + a.is_positive # this uses .evalf() and deduces it is positive + assert a.is_positive is True + + # be sure that hash stayed the same + assert ha == hash(a) + + # now b should be the same expression + b = a.expand(trig=True) + hb = hash(b) + + assert a == b + assert ha == hb + + +def test_Add_is_pos_neg(): + # these cover lines not covered by the rest of tests in core + n = Symbol('n', extended_negative=True, infinite=True) + nn = Symbol('n', extended_nonnegative=True, infinite=True) + np = Symbol('n', extended_nonpositive=True, infinite=True) + p = Symbol('p', extended_positive=True, infinite=True) + r = Dummy(extended_real=True, finite=False) + x = Symbol('x') + xf = Symbol('xf', finite=True) + assert (n + p).is_extended_positive is None + assert (n + x).is_extended_positive is None + assert (p + x).is_extended_positive is None + assert (n + p).is_extended_negative is None + assert (n + x).is_extended_negative is None + assert (p + x).is_extended_negative is None + + assert (n + xf).is_extended_positive is False + assert (p + xf).is_extended_positive is True + assert (n + xf).is_extended_negative is True + assert (p + xf).is_extended_negative is False + + assert (x - S.Infinity).is_extended_negative is None # issue 7798 + # issue 8046, 16.2 + assert (p + nn).is_extended_positive + assert (n + np).is_extended_negative + assert (p + r).is_extended_positive is None + + +def test_Add_is_imaginary(): + nn = Dummy(nonnegative=True) + assert (I*nn + I).is_imaginary # issue 8046, 17 + + +def test_Add_is_algebraic(): + a = Symbol('a', algebraic=True) + b = Symbol('a', algebraic=True) + na = Symbol('na', algebraic=False) + nb = Symbol('nb', algebraic=False) + x = Symbol('x') + assert (a + b).is_algebraic + assert (na + nb).is_algebraic is None + assert (a + na).is_algebraic is False + assert (a + x).is_algebraic is None + assert (na + x).is_algebraic is None + + +def test_Mul_is_algebraic(): + a = Symbol('a', algebraic=True) + b = Symbol('b', algebraic=True) + na = Symbol('na', algebraic=False) + an = Symbol('an', algebraic=True, nonzero=True) + nb = Symbol('nb', algebraic=False) + x = Symbol('x') + assert (a*b).is_algebraic is True + assert (na*nb).is_algebraic is None + assert (a*na).is_algebraic is None + assert (an*na).is_algebraic is False + assert (a*x).is_algebraic is None + assert (na*x).is_algebraic is None + + +def test_Pow_is_algebraic(): + e = Symbol('e', algebraic=True) + + assert Pow(1, e, evaluate=False).is_algebraic + assert Pow(0, e, evaluate=False).is_algebraic + + a = Symbol('a', algebraic=True) + azf = Symbol('azf', algebraic=True, zero=False) + na = Symbol('na', algebraic=False) + ia = Symbol('ia', algebraic=True, irrational=True) + ib = Symbol('ib', algebraic=True, irrational=True) + r = Symbol('r', rational=True) + x = Symbol('x') + assert (a**2).is_algebraic is True + assert (a**r).is_algebraic is None + assert (azf**r).is_algebraic is True + assert (a**x).is_algebraic is None + assert (na**r).is_algebraic is None + assert (ia**r).is_algebraic is True + assert (ia**ib).is_algebraic is False + + assert (a**e).is_algebraic is None + + # Gelfond-Schneider constant: + assert Pow(2, sqrt(2), evaluate=False).is_algebraic is False + + assert Pow(S.GoldenRatio, sqrt(3), evaluate=False).is_algebraic is False + + # issue 8649 + t = Symbol('t', real=True, transcendental=True) + n = Symbol('n', integer=True) + assert (t**n).is_algebraic is None + assert (t**n).is_integer is None + + assert (pi**3).is_algebraic is False + r = Symbol('r', zero=True) + assert (pi**r).is_algebraic is True + + +def test_Mul_is_prime_composite(): + x = Symbol('x', positive=True, integer=True) + y = Symbol('y', positive=True, integer=True) + assert (x*y).is_prime is None + assert ( (x+1)*(y+1) ).is_prime is False + assert ( (x+1)*(y+1) ).is_composite is True + + x = Symbol('x', positive=True) + assert ( (x+1)*(y+1) ).is_prime is None + assert ( (x+1)*(y+1) ).is_composite is None + + +def test_Pow_is_pos_neg(): + z = Symbol('z', real=True) + w = Symbol('w', nonpositive=True) + + assert (S.NegativeOne**S(2)).is_positive is True + assert (S.One**z).is_positive is True + assert (S.NegativeOne**S(3)).is_positive is False + assert (S.Zero**S.Zero).is_positive is True # 0**0 is 1 + assert (w**S(3)).is_positive is False + assert (w**S(2)).is_positive is None + assert (I**2).is_positive is False + assert (I**4).is_positive is True + + # tests emerging from #16332 issue + p = Symbol('p', zero=True) + q = Symbol('q', zero=False, real=True) + j = Symbol('j', zero=False, even=True) + x = Symbol('x', zero=True) + y = Symbol('y', zero=True) + assert (p**q).is_positive is False + assert (p**q).is_negative is False + assert (p**j).is_positive is False + assert (x**y).is_positive is True # 0**0 + assert (x**y).is_negative is False + + +def test_Pow_is_prime_composite(): + x = Symbol('x', positive=True, integer=True) + y = Symbol('y', positive=True, integer=True) + assert (x**y).is_prime is None + assert ( x**(y+1) ).is_prime is False + assert ( x**(y+1) ).is_composite is None + assert ( (x+1)**(y+1) ).is_composite is True + assert ( (-x-1)**(2*y) ).is_composite is True + + x = Symbol('x', positive=True) + assert (x**y).is_prime is None + + +def test_Mul_is_infinite(): + x = Symbol('x') + f = Symbol('f', finite=True) + i = Symbol('i', infinite=True) + z = Dummy(zero=True) + nzf = Dummy(finite=True, zero=False) + from sympy.core.mul import Mul + assert (x*f).is_finite is None + assert (x*i).is_finite is None + assert (f*i).is_finite is None + assert (x*f*i).is_finite is None + assert (z*i).is_finite is None + assert (nzf*i).is_finite is False + assert (z*f).is_finite is True + assert Mul(0, f, evaluate=False).is_finite is True + assert Mul(0, i, evaluate=False).is_finite is None + + assert (x*f).is_infinite is None + assert (x*i).is_infinite is None + assert (f*i).is_infinite is None + assert (x*f*i).is_infinite is None + assert (z*i).is_infinite is S.NaN.is_infinite + assert (nzf*i).is_infinite is True + assert (z*f).is_infinite is False + assert Mul(0, f, evaluate=False).is_infinite is False + assert Mul(0, i, evaluate=False).is_infinite is S.NaN.is_infinite + + +def test_Add_is_infinite(): + x = Symbol('x') + f = Symbol('f', finite=True) + i = Symbol('i', infinite=True) + i2 = Symbol('i2', infinite=True) + z = Dummy(zero=True) + nzf = Dummy(finite=True, zero=False) + from sympy.core.add import Add + assert (x+f).is_finite is None + assert (x+i).is_finite is None + assert (f+i).is_finite is False + assert (x+f+i).is_finite is None + assert (z+i).is_finite is False + assert (nzf+i).is_finite is False + assert (z+f).is_finite is True + assert (i+i2).is_finite is None + assert Add(0, f, evaluate=False).is_finite is True + assert Add(0, i, evaluate=False).is_finite is False + + assert (x+f).is_infinite is None + assert (x+i).is_infinite is None + assert (f+i).is_infinite is True + assert (x+f+i).is_infinite is None + assert (z+i).is_infinite is True + assert (nzf+i).is_infinite is True + assert (z+f).is_infinite is False + assert (i+i2).is_infinite is None + assert Add(0, f, evaluate=False).is_infinite is False + assert Add(0, i, evaluate=False).is_infinite is True + + +def test_special_is_rational(): + i = Symbol('i', integer=True) + i2 = Symbol('i2', integer=True) + ni = Symbol('ni', integer=True, nonzero=True) + r = Symbol('r', rational=True) + rn = Symbol('r', rational=True, nonzero=True) + nr = Symbol('nr', irrational=True) + x = Symbol('x') + assert sqrt(3).is_rational is False + assert (3 + sqrt(3)).is_rational is False + assert (3*sqrt(3)).is_rational is False + assert exp(3).is_rational is False + assert exp(ni).is_rational is False + assert exp(rn).is_rational is False + assert exp(x).is_rational is None + assert exp(log(3), evaluate=False).is_rational is True + assert log(exp(3), evaluate=False).is_rational is True + assert log(3).is_rational is False + assert log(ni + 1).is_rational is False + assert log(rn + 1).is_rational is False + assert log(x).is_rational is None + assert (sqrt(3) + sqrt(5)).is_rational is None + assert (sqrt(3) + S.Pi).is_rational is False + assert (x**i).is_rational is None + assert (i**i).is_rational is True + assert (i**i2).is_rational is None + assert (r**i).is_rational is None + assert (r**r).is_rational is None + assert (r**x).is_rational is None + assert (nr**i).is_rational is None # issue 8598 + assert (nr**Symbol('z', zero=True)).is_rational + assert sin(1).is_rational is False + assert sin(ni).is_rational is False + assert sin(rn).is_rational is False + assert sin(x).is_rational is None + assert asin(r).is_rational is False + assert sin(asin(3), evaluate=False).is_rational is True + + +@XFAIL +def test_issue_6275(): + x = Symbol('x') + # both zero or both Muls...but neither "change would be very appreciated. + # This is similar to x/x => 1 even though if x = 0, it is really nan. + assert isinstance(x*0, type(0*S.Infinity)) + if 0*S.Infinity is S.NaN: + b = Symbol('b', finite=None) + assert (b*0).is_zero is None + + +def test_sanitize_assumptions(): + # issue 6666 + for cls in (Symbol, Dummy, Wild): + x = cls('x', real=1, positive=0) + assert x.is_real is True + assert x.is_positive is False + assert cls('', real=True, positive=None).is_positive is None + raises(ValueError, lambda: cls('', commutative=None)) + raises(ValueError, lambda: Symbol._sanitize({"commutative": None})) + + +def test_special_assumptions(): + e = -3 - sqrt(5) + (-sqrt(10)/2 - sqrt(2)/2)**2 + assert simplify(e < 0) is S.false + assert simplify(e > 0) is S.false + assert (e == 0) is False # it's not a literal 0 + assert e.equals(0) is True + + +def test_inconsistent(): + # cf. issues 5795 and 5545 + raises(InconsistentAssumptions, lambda: Symbol('x', real=True, + commutative=False)) + + +def test_issue_6631(): + assert ((-1)**(I)).is_real is True + assert ((-1)**(I*2)).is_real is True + assert ((-1)**(I/2)).is_real is True + assert ((-1)**(I*S.Pi)).is_real is True + assert (I**(I + 2)).is_real is True + + +def test_issue_2730(): + assert (1/(1 + I)).is_real is False + + +def test_issue_4149(): + assert (3 + I).is_complex + assert (3 + I).is_imaginary is False + assert (3*I + S.Pi*I).is_imaginary + # as Zero.is_imaginary is False, see issue 7649 + y = Symbol('y', real=True) + assert (3*I + S.Pi*I + y*I).is_imaginary is None + p = Symbol('p', positive=True) + assert (3*I + S.Pi*I + p*I).is_imaginary + n = Symbol('n', negative=True) + assert (-3*I - S.Pi*I + n*I).is_imaginary + + i = Symbol('i', imaginary=True) + assert ([(i**a).is_imaginary for a in range(4)] == + [False, True, False, True]) + + # tests from the PR #7887: + e = S("-sqrt(3)*I/2 + 0.866025403784439*I") + assert e.is_real is False + assert e.is_imaginary + + +def test_issue_2920(): + n = Symbol('n', negative=True) + assert sqrt(n).is_imaginary + + +def test_issue_7899(): + x = Symbol('x', real=True) + assert (I*x).is_real is None + assert ((x - I)*(x - 1)).is_zero is None + assert ((x - I)*(x - 1)).is_real is None + + +@XFAIL +def test_issue_7993(): + x = Dummy(integer=True) + y = Dummy(noninteger=True) + assert (x - y).is_zero is False + + +def test_issue_8075(): + raises(InconsistentAssumptions, lambda: Dummy(zero=True, finite=False)) + raises(InconsistentAssumptions, lambda: Dummy(zero=True, infinite=True)) + + +def test_issue_8642(): + x = Symbol('x', real=True, integer=False) + assert (x*2).is_integer is None, (x*2).is_integer + + +def test_issues_8632_8633_8638_8675_8992(): + p = Dummy(integer=True, positive=True) + nn = Dummy(integer=True, nonnegative=True) + assert (p - S.Half).is_positive + assert (p - 1).is_nonnegative + assert (nn + 1).is_positive + assert (-p + 1).is_nonpositive + assert (-nn - 1).is_negative + prime = Dummy(prime=True) + assert (prime - 2).is_nonnegative + assert (prime - 3).is_nonnegative is None + even = Dummy(positive=True, even=True) + assert (even - 2).is_nonnegative + + p = Dummy(positive=True) + assert (p/(p + 1) - 1).is_negative + assert ((p + 2)**3 - S.Half).is_positive + n = Dummy(negative=True) + assert (n - 3).is_nonpositive + + +def test_issue_9115_9150(): + n = Dummy('n', integer=True, nonnegative=True) + assert (factorial(n) >= 1) == True + assert (factorial(n) < 1) == False + + assert factorial(n + 1).is_even is None + assert factorial(n + 2).is_even is True + assert factorial(n + 2) >= 2 + + +def test_issue_9165(): + z = Symbol('z', zero=True) + f = Symbol('f', finite=False) + assert 0/z is S.NaN + assert 0*(1/z) is S.NaN + assert 0*f is S.NaN + + +def test_issue_10024(): + x = Dummy('x') + assert Mod(x, 2*pi).is_zero is None + + +def test_issue_10302(): + x = Symbol('x') + r = Symbol('r', real=True) + u = -(3*2**pi)**(1/pi) + 2*3**(1/pi) + i = u + u*I + + assert i.is_real is None # w/o simplification this should fail + assert (u + i).is_zero is None + assert (1 + i).is_zero is False + + a = Dummy('a', zero=True) + assert (a + I).is_zero is False + assert (a + r*I).is_zero is None + assert (a + I).is_imaginary + assert (a + x + I).is_imaginary is None + assert (a + r*I + I).is_imaginary is None + + +def test_complex_reciprocal_imaginary(): + assert (1 / (4 + 3*I)).is_imaginary is False + + +def test_issue_16313(): + x = Symbol('x', extended_real=False) + k = Symbol('k', real=True) + l = Symbol('l', real=True, zero=False) + assert (-x).is_real is False + assert (k*x).is_real is None # k can be zero also + assert (l*x).is_real is False + assert (l*x*x).is_real is None # since x*x can be a real number + assert (-x).is_positive is False + + +def test_issue_16579(): + # extended_real -> finite | infinite + x = Symbol('x', extended_real=True, infinite=False) + y = Symbol('y', extended_real=True, finite=False) + assert x.is_finite is True + assert y.is_infinite is True + + # With PR 16978, complex now implies finite + c = Symbol('c', complex=True) + assert c.is_finite is True + raises(InconsistentAssumptions, lambda: Dummy(complex=True, finite=False)) + + # Now infinite == !finite + nf = Symbol('nf', finite=False) + assert nf.is_infinite is True + + +def test_issue_17556(): + z = I*oo + assert z.is_imaginary is False + assert z.is_finite is False + + +def test_issue_21651(): + k = Symbol('k', positive=True, integer=True) + exp = 2*2**(-k) + assert exp.is_integer is None + + +def test_assumptions_copy(): + assert assumptions(Symbol('x'), {"commutative": True} + ) == {'commutative': True} + assert assumptions(Symbol('x'), ['integer']) == {} + assert assumptions(Symbol('x'), ['commutative'] + ) == {'commutative': True} + assert assumptions(Symbol('x')) == {'commutative': True} + assert assumptions(1)['positive'] + assert assumptions(3 + I) == { + 'algebraic': True, + 'commutative': True, + 'complex': True, + 'composite': False, + 'even': False, + 'extended_negative': False, + 'extended_nonnegative': False, + 'extended_nonpositive': False, + 'extended_nonzero': False, + 'extended_positive': False, + 'extended_real': False, + 'finite': True, + 'imaginary': False, + 'infinite': False, + 'integer': False, + 'irrational': False, + 'negative': False, + 'noninteger': False, + 'nonnegative': False, + 'nonpositive': False, + 'nonzero': False, + 'odd': False, + 'positive': False, + 'prime': False, + 'rational': False, + 'real': False, + 'transcendental': False, + 'zero': False} + + +def test_check_assumptions(): + assert check_assumptions(1, 0) is False + x = Symbol('x', positive=True) + assert check_assumptions(1, x) is True + assert check_assumptions(1, 1) is True + assert check_assumptions(-1, 1) is False + i = Symbol('i', integer=True) + # don't know if i is positive (or prime, etc...) + assert check_assumptions(i, 1) is None + assert check_assumptions(Dummy(integer=None), integer=True) is None + assert check_assumptions(Dummy(integer=None), integer=False) is None + assert check_assumptions(Dummy(integer=False), integer=True) is False + assert check_assumptions(Dummy(integer=True), integer=False) is False + # no T/F assumptions to check + assert check_assumptions(Dummy(integer=False), integer=None) is True + raises(ValueError, lambda: check_assumptions(2*x, x, positive=True)) + + +def test_failing_assumptions(): + x = Symbol('x', positive=True) + y = Symbol('y') + assert failing_assumptions(6*x + y, **x.assumptions0) == \ + {'real': None, 'imaginary': None, 'complex': None, 'hermitian': None, + 'positive': None, 'nonpositive': None, 'nonnegative': None, 'nonzero': None, + 'negative': None, 'zero': None, 'extended_real': None, 'finite': None, + 'infinite': None, 'extended_negative': None, 'extended_nonnegative': None, + 'extended_nonpositive': None, 'extended_nonzero': None, + 'extended_positive': None } + + +def test_common_assumptions(): + assert common_assumptions([0, 1, 2] + ) == {'algebraic': True, 'irrational': False, 'hermitian': + True, 'extended_real': True, 'real': True, 'extended_negative': + False, 'extended_nonnegative': True, 'integer': True, + 'rational': True, 'imaginary': False, 'complex': True, + 'commutative': True,'noninteger': False, 'composite': False, + 'infinite': False, 'nonnegative': True, 'finite': True, + 'transcendental': False,'negative': False} + assert common_assumptions([0, 1, 2], 'positive integer'.split() + ) == {'integer': True} + assert common_assumptions([0, 1, 2], []) == {} + assert common_assumptions([], ['integer']) == {} + assert common_assumptions([0], ['integer']) == {'integer': True} + +def test_pre_generated_assumption_rules_are_valid(): + # check the pre-generated assumptions match freshly generated assumptions + # if this check fails, consider updating the assumptions + # see sympy.core.assumptions._generate_assumption_rules + pre_generated_assumptions =_load_pre_generated_assumption_rules() + generated_assumptions =_generate_assumption_rules() + assert pre_generated_assumptions._to_python() == generated_assumptions._to_python(), "pre-generated assumptions are invalid, see sympy.core.assumptions._generate_assumption_rules" + + +def test_ask_shuffle(): + grp = PermutationGroup(Permutation(1, 0, 2), Permutation(2, 1, 3)) + + seed(123) + first = grp.random() + seed(123) + simplify(I) + second = grp.random() + seed(123) + simplify(-I) + third = grp.random() + + assert first == second == third diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_basic.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_basic.py new file mode 100644 index 0000000000000000000000000000000000000000..3a7adbb5dcf0d70089ff79028afa943b24ee0c42 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_basic.py @@ -0,0 +1,343 @@ +"""This tests sympy/core/basic.py with (ideally) no reference to subclasses +of Basic or Atom.""" +import collections +from typing import TypeVar, Generic + +from sympy.assumptions.ask import Q +from sympy.core.basic import (Basic, Atom, as_Basic, + _atomic, _aresame) +from sympy.core.containers import Tuple +from sympy.core.function import Function, Lambda +from sympy.core.numbers import I, pi, Float +from sympy.core.singleton import S +from sympy.core.symbol import symbols, Symbol, Dummy +from sympy.concrete.summations import Sum +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.functions.special.gamma_functions import gamma +from sympy.integrals.integrals import Integral +from sympy.functions.elementary.exponential import exp +from sympy.testing.pytest import raises, warns_deprecated_sympy +from sympy.functions.elementary.complexes import Abs, sign +from sympy.functions.elementary.piecewise import Piecewise +from sympy.core.relational import Eq + +b1 = Basic() +b2 = Basic(b1) +b3 = Basic(b2) +b21 = Basic(b2, b1) +T = TypeVar('T') + + +def test__aresame(): + assert not _aresame(Basic(Tuple()), Basic()) + for i, j in [(S(2), S(2.)), (1., Float(1))]: + for do in range(2): + assert not _aresame(Basic(i), Basic(j)) + assert not _aresame(i, j) + i, j = j, i + + +def test_structure(): + assert b21.args == (b2, b1) + assert b21.func(*b21.args) == b21 + assert bool(b1) + + +def test_immutable(): + assert not hasattr(b1, '__dict__') + with raises(AttributeError): + b1.x = 1 + + +def test_equality(): + instances = [b1, b2, b3, b21, Basic(b1, b1, b1), Basic] + for i, b_i in enumerate(instances): + for j, b_j in enumerate(instances): + assert (b_i == b_j) == (i == j) + assert (b_i != b_j) == (i != j) + + assert Basic() != [] + assert not(Basic() == []) + assert Basic() != 0 + assert not(Basic() == 0) + + class Foo: + """ + Class that is unaware of Basic, and relies on both classes returning + the NotImplemented singleton for equivalence to evaluate to False. + + """ + + b = Basic() + foo = Foo() + + assert b != foo + assert foo != b + assert not b == foo + assert not foo == b + + class Bar: + """ + Class that considers itself equal to any instance of Basic, and relies + on Basic returning the NotImplemented singleton in order to achieve + a symmetric equivalence relation. + + """ + def __eq__(self, other): + if isinstance(other, Basic): + return True + return NotImplemented + + def __ne__(self, other): + return not self == other + + bar = Bar() + + assert b == bar + assert bar == b + assert not b != bar + assert not bar != b + + +def test_matches_basic(): + instances = [Basic(b1, b1, b2), Basic(b1, b2, b1), Basic(b2, b1, b1), + Basic(b1, b2), Basic(b2, b1), b2, b1] + for i, b_i in enumerate(instances): + for j, b_j in enumerate(instances): + if i == j: + assert b_i.matches(b_j) == {} + else: + assert b_i.matches(b_j) is None + assert b1.match(b1) == {} + + +def test_has(): + assert b21.has(b1) + assert b21.has(b3, b1) + assert b21.has(Basic) + assert not b1.has(b21, b3) + assert not b21.has() + assert not b21.has(str) + assert not Symbol("x").has("x") + + +def test_subs(): + assert b21.subs(b2, b1) == Basic(b1, b1) + assert b21.subs(b2, b21) == Basic(b21, b1) + assert b3.subs(b2, b1) == b2 + + assert b21.subs([(b2, b1), (b1, b2)]) == Basic(b2, b2) + + assert b21.subs({b1: b2, b2: b1}) == Basic(b2, b2) + assert b21.subs(collections.ChainMap({b1: b2}, {b2: b1})) == Basic(b2, b2) + assert b21.subs(collections.OrderedDict([(b2, b1), (b1, b2)])) == Basic(b2, b2) + + raises(ValueError, lambda: b21.subs('bad arg')) + raises(TypeError, lambda: b21.subs(b1, b2, b3)) + # dict(b1=foo) creates a string 'b1' but leaves foo unchanged; subs + # will convert the first to a symbol but will raise an error if foo + # cannot be sympified; sympification is strict if foo is not string + raises(TypeError, lambda: b21.subs(b1='bad arg')) + + assert Symbol("text").subs({"text": b1}) == b1 + assert Symbol("s").subs({"s": 1}) == 1 + + +def test_subs_with_unicode_symbols(): + expr = Symbol('var1') + replaced = expr.subs('var1', 'x') + assert replaced.name == 'x' + + replaced = expr.subs('var1', 'x') + assert replaced.name == 'x' + + +def test_atoms(): + assert b21.atoms() == {Basic()} + + +def test_free_symbols_empty(): + assert b21.free_symbols == set() + + +def test_doit(): + assert b21.doit() == b21 + assert b21.doit(deep=False) == b21 + + +def test_S(): + assert repr(S) == 'S' + + +def test_xreplace(): + assert b21.xreplace({b2: b1}) == Basic(b1, b1) + assert b21.xreplace({b2: b21}) == Basic(b21, b1) + assert b3.xreplace({b2: b1}) == b2 + assert Basic(b1, b2).xreplace({b1: b2, b2: b1}) == Basic(b2, b1) + assert Atom(b1).xreplace({b1: b2}) == Atom(b1) + assert Atom(b1).xreplace({Atom(b1): b2}) == b2 + raises(TypeError, lambda: b1.xreplace()) + raises(TypeError, lambda: b1.xreplace([b1, b2])) + for f in (exp, Function('f')): + assert f.xreplace({}) == f + assert f.xreplace({}, hack2=True) == f + assert f.xreplace({f: b1}) == b1 + assert f.xreplace({f: b1}, hack2=True) == b1 + + +def test_sorted_args(): + x = symbols('x') + assert b21._sorted_args == b21.args + raises(AttributeError, lambda: x._sorted_args) + +def test_call(): + x, y = symbols('x y') + # See the long history of this in issues 5026 and 5105. + + raises(TypeError, lambda: sin(x)({ x : 1, sin(x) : 2})) + raises(TypeError, lambda: sin(x)(1)) + + # No effect as there are no callables + assert sin(x).rcall(1) == sin(x) + assert (1 + sin(x)).rcall(1) == 1 + sin(x) + + # Effect in the presence of callables + l = Lambda(x, 2*x) + assert (l + x).rcall(y) == 2*y + x + assert (x**l).rcall(2) == x**4 + # TODO UndefinedFunction does not subclass Expr + #f = Function('f') + #assert (2*f)(x) == 2*f(x) + + assert (Q.real & Q.positive).rcall(x) == Q.real(x) & Q.positive(x) + + +def test_rewrite(): + x, y, z = symbols('x y z') + a, b = symbols('a b') + f1 = sin(x) + cos(x) + assert f1.rewrite(cos,exp) == exp(I*x)/2 + sin(x) + exp(-I*x)/2 + assert f1.rewrite([cos],sin) == sin(x) + sin(x + pi/2, evaluate=False) + f2 = sin(x) + cos(y)/gamma(z) + assert f2.rewrite(sin,exp) == -I*(exp(I*x) - exp(-I*x))/2 + cos(y)/gamma(z) + + assert f1.rewrite() == f1 + +def test_literal_evalf_is_number_is_zero_is_comparable(): + x = symbols('x') + f = Function('f') + + # issue 5033 + assert f.is_number is False + # issue 6646 + assert f(1).is_number is False + i = Integral(0, (x, x, x)) + # expressions that are symbolically 0 can be difficult to prove + # so in case there is some easy way to know if something is 0 + # it should appear in the is_zero property for that object; + # if is_zero is true evalf should always be able to compute that + # zero + assert i.n() == 0 + assert i.is_zero + assert i.is_number is False + assert i.evalf(2, strict=False) == 0 + + # issue 10268 + n = sin(1)**2 + cos(1)**2 - 1 + assert n.is_comparable is False + assert n.n(2).is_comparable is False + assert n.n(2).n(2).is_comparable + + +def test_as_Basic(): + assert as_Basic(1) is S.One + assert as_Basic(()) == Tuple() + raises(TypeError, lambda: as_Basic([])) + + +def test_atomic(): + g, h = map(Function, 'gh') + x = symbols('x') + assert _atomic(g(x + h(x))) == {g(x + h(x))} + assert _atomic(g(x + h(x)), recursive=True) == {h(x), x, g(x + h(x))} + assert _atomic(1) == set() + assert _atomic(Basic(S(1), S(2))) == set() + + +def test_as_dummy(): + u, v, x, y, z, _0, _1 = symbols('u v x y z _0 _1') + assert Lambda(x, x + 1).as_dummy() == Lambda(_0, _0 + 1) + assert Lambda(x, x + _0).as_dummy() == Lambda(_1, _0 + _1) + eq = (1 + Sum(x, (x, 1, x))) + ans = 1 + Sum(_0, (_0, 1, x)) + once = eq.as_dummy() + assert once == ans + twice = once.as_dummy() + assert twice == ans + assert Integral(x + _0, (x, x + 1), (_0, 1, 2) + ).as_dummy() == Integral(_0 + _1, (_0, x + 1), (_1, 1, 2)) + for T in (Symbol, Dummy): + d = T('x', real=True) + D = d.as_dummy() + assert D != d and D.func == Dummy and D.is_real is None + assert Dummy().as_dummy().is_commutative + assert Dummy(commutative=False).as_dummy().is_commutative is False + + +def test_canonical_variables(): + x, i0, i1 = symbols('x _:2') + assert Integral(x, (x, x + 1)).canonical_variables == {x: i0} + assert Integral(x, (x, x + 1), (i0, 1, 2)).canonical_variables == { + x: i0, i0: i1} + assert Integral(x, (x, x + i0)).canonical_variables == {x: i1} + + +def test_replace_exceptions(): + from sympy.core.symbol import Wild + x, y = symbols('x y') + e = (x**2 + x*y) + raises(TypeError, lambda: e.replace(sin, 2)) + b = Wild('b') + c = Wild('c') + raises(TypeError, lambda: e.replace(b*c, c.is_real)) + raises(TypeError, lambda: e.replace(b.is_real, 1)) + raises(TypeError, lambda: e.replace(lambda d: d.is_Number, 1)) + + +def test_ManagedProperties(): + # ManagedProperties is now deprecated. Here we do our best to check that if + # someone is using it then it does work in the way that it previously did + # but gives a deprecation warning. + from sympy.core.assumptions import ManagedProperties + + myclasses = [] + + class MyMeta(ManagedProperties): + def __init__(cls, *args, **kwargs): + myclasses.append('executed') + super().__init__(*args, **kwargs) + + code = """ +class MySubclass(Basic, metaclass=MyMeta): + pass +""" + with warns_deprecated_sympy(): + exec(code) + + assert myclasses == ['executed'] + + +def test_generic(): + # https://github.com/sympy/sympy/issues/25399 + class A(Symbol, Generic[T]): + pass + + class B(A[T]): + pass + + +def test_rewrite_abs(): + # https://github.com/sympy/sympy/issues/27323 + x = Symbol('x') + assert sign(x).rewrite(abs) == sign(x).rewrite(Abs) + assert sign(x).rewrite(abs) == Piecewise((0, Eq(x, 0)), (x / Abs(x), True)) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_cache.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_cache.py new file mode 100644 index 0000000000000000000000000000000000000000..9124fca70718299252929a9923f335dde25256eb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_cache.py @@ -0,0 +1,91 @@ +import sys +from sympy.core.cache import cacheit, cached_property, lazy_function +from sympy.testing.pytest import raises + +def test_cacheit_doc(): + @cacheit + def testfn(): + "test docstring" + pass + + assert testfn.__doc__ == "test docstring" + assert testfn.__name__ == "testfn" + +def test_cacheit_unhashable(): + @cacheit + def testit(x): + return x + + assert testit(1) == 1 + assert testit(1) == 1 + a = {} + assert testit(a) == {} + a[1] = 2 + assert testit(a) == {1: 2} + +def test_cachit_exception(): + # Make sure the cache doesn't call functions multiple times when they + # raise TypeError + + a = [] + + @cacheit + def testf(x): + a.append(0) + raise TypeError + + raises(TypeError, lambda: testf(1)) + assert len(a) == 1 + + a.clear() + # Unhashable type + raises(TypeError, lambda: testf([])) + assert len(a) == 1 + + @cacheit + def testf2(x): + a.append(0) + raise TypeError("Error") + + a.clear() + raises(TypeError, lambda: testf2(1)) + assert len(a) == 1 + + a.clear() + # Unhashable type + raises(TypeError, lambda: testf2([])) + assert len(a) == 1 + +def test_cached_property(): + class A: + def __init__(self, value): + self.value = value + self.calls = 0 + + @cached_property + def prop(self): + self.calls = self.calls + 1 + return self.value + + a = A(2) + assert a.calls == 0 + assert a.prop == 2 + assert a.calls == 1 + assert a.prop == 2 + assert a.calls == 1 + b = A(None) + assert b.prop == None + + +def test_lazy_function(): + module_name='xmlrpc.client' + function_name = 'gzip_decode' + lazy = lazy_function(module_name, function_name) + assert lazy(b'') == b'' + assert module_name in sys.modules + assert function_name in str(lazy) + repr_lazy = repr(lazy) + assert 'LazyFunction' in repr_lazy + assert function_name in repr_lazy + + lazy = lazy_function('sympy.core.cache', 'cheap') diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_compatibility.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_compatibility.py new file mode 100644 index 0000000000000000000000000000000000000000..31d2bed07b21aa2fa489273dca9edfc9993cfd86 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_compatibility.py @@ -0,0 +1,6 @@ +from sympy.testing.pytest import warns_deprecated_sympy + +def test_compatibility_submodule(): + # Test the sympy.core.compatibility deprecation warning + with warns_deprecated_sympy(): + import sympy.core.compatibility # noqa:F401 diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_complex.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_complex.py new file mode 100644 index 0000000000000000000000000000000000000000..a607e0bdb4db859336aa30aa61f43bfb57d5df88 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_complex.py @@ -0,0 +1,226 @@ +from sympy.core.function import expand_complex +from sympy.core.numbers import (I, Integer, Rational, pi) +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import (Abs, conjugate, im, re, sign) +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.hyperbolic import (cosh, coth, sinh, tanh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, cot, sin, tan) + +def test_complex(): + a = Symbol("a", real=True) + b = Symbol("b", real=True) + e = (a + I*b)*(a - I*b) + assert e.expand() == a**2 + b**2 + assert sqrt(I) == Pow(I, S.Half) + + +def test_conjugate(): + a = Symbol("a", real=True) + b = Symbol("b", real=True) + c = Symbol("c", imaginary=True) + d = Symbol("d", imaginary=True) + x = Symbol('x') + z = a + I*b + c + I*d + zc = a - I*b - c + I*d + assert conjugate(z) == zc + assert conjugate(exp(z)) == exp(zc) + assert conjugate(exp(I*x)) == exp(-I*conjugate(x)) + assert conjugate(z**5) == zc**5 + assert conjugate(abs(x)) == abs(x) + assert conjugate(sign(z)) == sign(zc) + assert conjugate(sin(z)) == sin(zc) + assert conjugate(cos(z)) == cos(zc) + assert conjugate(tan(z)) == tan(zc) + assert conjugate(cot(z)) == cot(zc) + assert conjugate(sinh(z)) == sinh(zc) + assert conjugate(cosh(z)) == cosh(zc) + assert conjugate(tanh(z)) == tanh(zc) + assert conjugate(coth(z)) == coth(zc) + + +def test_abs1(): + a = Symbol("a", real=True) + b = Symbol("b", real=True) + assert abs(a) == Abs(a) + assert abs(-a) == abs(a) + assert abs(a + I*b) == sqrt(a**2 + b**2) + + +def test_abs2(): + a = Symbol("a", real=False) + b = Symbol("b", real=False) + assert abs(a) != a + assert abs(-a) != a + assert abs(a + I*b) != sqrt(a**2 + b**2) + + +def test_evalc(): + x = Symbol("x", real=True) + y = Symbol("y", real=True) + z = Symbol("z") + assert ((x + I*y)**2).expand(complex=True) == x**2 + 2*I*x*y - y**2 + assert expand_complex(z**(2*I)) == (re((re(z) + I*im(z))**(2*I)) + + I*im((re(z) + I*im(z))**(2*I))) + assert expand_complex( + z**(2*I), deep=False) == I*im(z**(2*I)) + re(z**(2*I)) + + assert exp(I*x) != cos(x) + I*sin(x) + assert exp(I*x).expand(complex=True) == cos(x) + I*sin(x) + assert exp(I*x + y).expand(complex=True) == exp(y)*cos(x) + I*sin(x)*exp(y) + + assert sin(I*x).expand(complex=True) == I * sinh(x) + assert sin(x + I*y).expand(complex=True) == sin(x)*cosh(y) + \ + I * sinh(y) * cos(x) + + assert cos(I*x).expand(complex=True) == cosh(x) + assert cos(x + I*y).expand(complex=True) == cos(x)*cosh(y) - \ + I * sinh(y) * sin(x) + + assert tan(I*x).expand(complex=True) == tanh(x) * I + assert tan(x + I*y).expand(complex=True) == ( + sin(2*x)/(cos(2*x) + cosh(2*y)) + + I*sinh(2*y)/(cos(2*x) + cosh(2*y))) + + assert sinh(I*x).expand(complex=True) == I * sin(x) + assert sinh(x + I*y).expand(complex=True) == sinh(x)*cos(y) + \ + I * sin(y) * cosh(x) + + assert cosh(I*x).expand(complex=True) == cos(x) + assert cosh(x + I*y).expand(complex=True) == cosh(x)*cos(y) + \ + I * sin(y) * sinh(x) + + assert tanh(I*x).expand(complex=True) == tan(x) * I + assert tanh(x + I*y).expand(complex=True) == ( + (sinh(x)*cosh(x) + I*cos(y)*sin(y)) / + (sinh(x)**2 + cos(y)**2)).expand() + + +def test_pythoncomplex(): + x = Symbol("x") + assert 4j*x != 4*x*I + assert 4j*x == 4.0*x*I + assert 4.1j*x != 4*x*I + + +def test_rootcomplex(): + R = Rational + assert ((+1 + I)**R(1, 2)).expand( + complex=True) == 2**R(1, 4)*cos( pi/8) + 2**R(1, 4)*sin( pi/8)*I + assert ((-1 - I)**R(1, 2)).expand( + complex=True) == 2**R(1, 4)*cos(3*pi/8) - 2**R(1, 4)*sin(3*pi/8)*I + assert (sqrt(-10)*I).as_real_imag() == (-sqrt(10), 0) + + +def test_expand_inverse(): + assert (1/(1 + I)).expand(complex=True) == (1 - I)/2 + assert ((1 + 2*I)**(-2)).expand(complex=True) == (-3 - 4*I)/25 + assert ((1 + I)**(-8)).expand(complex=True) == Rational(1, 16) + + +def test_expand_complex(): + assert ((2 + 3*I)**10).expand(complex=True) == -341525 - 145668*I + # the following two tests are to ensure the SymPy uses an efficient + # algorithm for calculating powers of complex numbers. They should execute + # in something like 0.01s. + assert ((2 + 3*I)**1000).expand(complex=True) == \ + -81079464736246615951519029367296227340216902563389546989376269312984127074385455204551402940331021387412262494620336565547972162814110386834027871072723273110439771695255662375718498785908345629702081336606863762777939617745464755635193139022811989314881997210583159045854968310911252660312523907616129080027594310008539817935736331124833163907518549408018652090650537035647520296539436440394920287688149200763245475036722326561143851304795139005599209239350981457301460233967137708519975586996623552182807311159141501424576682074392689622074945519232029999 + \ + 46938745946789557590804551905243206242164799136976022474337918748798900569942573265747576032611189047943842446167719177749107138603040963603119861476016947257034472364028585381714774667326478071264878108114128915685688115488744955550920239128462489496563930809677159214598114273887061533057125164518549173898349061972857446844052995037423459472376202251620778517659247970283904820245958198842631651569984310559418135975795868314764489884749573052997832686979294085577689571149679540256349988338406458116270429842222666345146926395233040564229555893248370000*I + assert ((2 + 3*I/4)**1000).expand(complex=True) == \ + Integer(1)*37079892761199059751745775382463070250205990218394308874593455293485167797989691280095867197640410033222367257278387021789651672598831503296531725827158233077451476545928116965316544607115843772405184272449644892857783761260737279675075819921259597776770965829089907990486964515784097181964312256560561065607846661496055417619388874421218472707497847700629822858068783288579581649321248495739224020822198695759609598745114438265083593711851665996586461937988748911532242908776883696631067311443171682974330675406616373422505939887984366289623091300746049101284856530270685577940283077888955692921951247230006346681086274961362500646889925803654263491848309446197554307105991537357310209426736453173441104334496173618419659521888945605315751089087820455852582920963561495787655250624781448951403353654348109893478206364632640344111022531861683064175862889459084900614967785405977231549003280842218501570429860550379522498497412180001/114813069527425452423283320117768198402231770208869520047764273682576626139237031385665948631650626991844596463898746277344711896086305533142593135616665318539129989145312280000688779148240044871428926990063486244781615463646388363947317026040466353970904996558162398808944629605623311649536164221970332681344168908984458505602379484807914058900934776500429002716706625830522008132236281291761267883317206598995396418127021779858404042159853183251540889433902091920554957783589672039160081957216630582755380425583726015528348786419432054508915275783882625175435528800822842770817965453762184851149029376 + \ + I*421638390580169706973991429333213477486930178424989246669892530737775352519112934278994501272111385966211392610029433824534634841747911783746811994443436271013377059560245191441549885048056920190833693041257216263519792201852046825443439142932464031501882145407459174948712992271510309541474392303461939389368955986650538525895866713074543004916049550090364398070215427272240155060576252568700906004691224321432509053286859100920489253598392100207663785243368195857086816912514025693453058403158416856847185079684216151337200057494966741268925263085619240941610301610538225414050394612058339070756009433535451561664522479191267503989904464718368605684297071150902631208673621618217106272361061676184840810762902463998065947687814692402219182668782278472952758690939877465065070481351343206840649517150634973307937551168752642148704904383991876969408056379195860410677814566225456558230131911142229028179902418223009651437985670625/1793954211366022694113801876840128100034871409513586250746316776290259783425578615401030447369541046747571819748417910583511123376348523955353017744010395602173906080395504375010762174191250701116076984219741972574712741619474818186676828531882286780795390571221287481389759837587864244524002565968286448146002639202882164150037179450123657170327105882819203167448541028601906377066191895183769810676831353109303069033234715310287563158747705988305326397404720186258671215368588625611876280581509852855552819149745718992630449787803625851701801184123166018366180137512856918294030710215034138299203584 + assert ((2 + 3*I)**-1000).expand(complex=True) == \ + Integer(1)*-81079464736246615951519029367296227340216902563389546989376269312984127074385455204551402940331021387412262494620336565547972162814110386834027871072723273110439771695255662375718498785908345629702081336606863762777939617745464755635193139022811989314881997210583159045854968310911252660312523907616129080027594310008539817935736331124833163907518549408018652090650537035647520296539436440394920287688149200763245475036722326561143851304795139005599209239350981457301460233967137708519975586996623552182807311159141501424576682074392689622074945519232029999/8777125472973511649630750050295188683351430110097915876250894978429797369155961290321829625004920141758416719066805645579710744290541337680113772670040386863849283653078324415471816788604945889094925784900885812724984087843737442111926413818245854362613018058774368703971604921858023116665586358870612944209398056562604561248859926344335598822815885851096698226775053153403320782439987679978321289537645645163767251396759519805603090332694449553371530571613352311006350058217982509738362083094920649452123351717366337410243853659113315547584871655479914439219520157174729130746351059075207407866012574386726064196992865627149566238044625779078186624347183905913357718850537058578084932880569701242598663149911276357125355850792073635533676541250531086757377369962506979378337216411188347761901006460813413505861461267545723590468627854202034450569581626648934062198718362303420281555886394558137408159453103395918783625713213314350531051312551733021627153081075080140680608080529736975658786227362251632725009435866547613598753584705455955419696609282059191031962604169242974038517575645939316377801594539335940001 - Integer(1)*46938745946789557590804551905243206242164799136976022474337918748798900569942573265747576032611189047943842446167719177749107138603040963603119861476016947257034472364028585381714774667326478071264878108114128915685688115488744955550920239128462489496563930809677159214598114273887061533057125164518549173898349061972857446844052995037423459472376202251620778517659247970283904820245958198842631651569984310559418135975795868314764489884749573052997832686979294085577689571149679540256349988338406458116270429842222666345146926395233040564229555893248370000*I/8777125472973511649630750050295188683351430110097915876250894978429797369155961290321829625004920141758416719066805645579710744290541337680113772670040386863849283653078324415471816788604945889094925784900885812724984087843737442111926413818245854362613018058774368703971604921858023116665586358870612944209398056562604561248859926344335598822815885851096698226775053153403320782439987679978321289537645645163767251396759519805603090332694449553371530571613352311006350058217982509738362083094920649452123351717366337410243853659113315547584871655479914439219520157174729130746351059075207407866012574386726064196992865627149566238044625779078186624347183905913357718850537058578084932880569701242598663149911276357125355850792073635533676541250531086757377369962506979378337216411188347761901006460813413505861461267545723590468627854202034450569581626648934062198718362303420281555886394558137408159453103395918783625713213314350531051312551733021627153081075080140680608080529736975658786227362251632725009435866547613598753584705455955419696609282059191031962604169242974038517575645939316377801594539335940001 + assert ((2 + 3*I/4)**-1000).expand(complex=True) == \ + Integer(1)*4257256305661027385394552848555894604806501409793288342610746813288539790051927148781268212212078237301273165351052934681382567968787279534591114913777456610214738290619922068269909423637926549603264174216950025398244509039145410016404821694746262142525173737175066432954496592560621330313807235750500564940782099283410261748370262433487444897446779072067625787246390824312580440138770014838135245148574339248259670887549732495841810961088930810608893772914812838358159009303794863047635845688453859317690488124382253918725010358589723156019888846606295866740117645571396817375322724096486161308083462637370825829567578309445855481578518239186117686659177284332344643124760453112513611749309168470605289172320376911472635805822082051716625171429727162039621902266619821870482519063133136820085579315127038372190224739238686708451840610064871885616258831386810233957438253532027049148030157164346719204500373766157143311767338973363806106967439378604898250533766359989107510507493549529158818602327525235240510049484816090584478644771183158342479140194633579061295740839490629457435283873180259847394582069479062820225159699506175855369539201399183443253793905149785994830358114153241481884290274629611529758663543080724574566578220908907477622643689220814376054314972190402285121776593824615083669045183404206291739005554569305329760211752815718335731118664756831942466773261465213581616104242113894521054475516019456867271362053692785300826523328020796670205463390909136593859765912483565093461468865534470710132881677639651348709376/2103100954337624833663208713697737151593634525061637972297915388685604042449504336765884978184588688426595940401280828953096857809292320006227881797146858511436638446932833617514351442216409828605662238790280753075176269765767010004889778647709740770757817960711900340755635772183674511158570690702969774966791073165467918123298694584729211212414462628433370481195120564586361368504153395406845170075275051749019600057116719726628746724489572189061061036426955163696859127711110719502594479795200686212257570291758725259007379710596548777812659422174199194837355646482046783616494013289495563083118517507178847555801163089723056310287760875135196081975602765511153122381201303871673391366630940702817360340900568748719988954847590748960761446218262344767250783946365392689256634180417145926390656439421745644011831124277463643383712803287985472471755648426749842410972650924240795946699346613614779460399530274263580007672855851663196114585312432954432654691485867618908420370875753749297487803461900447407917655296784879220450937110470920633595689721819488638484547259978337741496090602390463594556401615298457456112485536498177883358587125449801777718900375736758266215245325999241624148841915093787519330809347240990363802360596034171167818310322276373120180985148650099673289383722502488957717848531612020897298448601714154586319660314294591620415272119454982220034319689607295960162971300417552364254983071768070124456169427638371140064235083443242844616326538396503937972586505546495649094344512270582463639152160238137952390380581401171977159154009407415523525096743009110916334144716516647041176989758534635251844947906038080852185583742296318878233394998111078843229681030277039104786225656992262073797524057992347971177720807155842376332851559276430280477639539393920006008737472164850104411971830120295750221200029811143140323763349636629725073624360001 - Integer(1)*3098214262599218784594285246258841485430681674561917573155883806818465520660668045042109232930382494608383663464454841313154390741655282039877410154577448327874989496074260116195788919037407420625081798124301494353693248757853222257918294662198297114746822817460991242508743651430439120439020484502408313310689912381846149597061657483084652685283853595100434135149479564507015504022249330340259111426799121454516345905101620532787348293877485702600390665276070250119465888154331218827342488849948540687659846652377277250614246402784754153678374932540789808703029043827352976139228402417432199779415751301480406673762521987999573209628597459357964214510139892316208670927074795773830798600837815329291912002136924506221066071242281626618211060464126372574400100990746934953437169840312584285942093951405864225230033279614235191326102697164613004299868695519642598882914862568516635347204441042798206770888274175592401790040170576311989738272102077819127459014286741435419468254146418098278519775722104890854275995510700298782146199325790002255362719776098816136732897323406228294203133323296591166026338391813696715894870956511298793595675308998014158717167429941371979636895553724830981754579086664608880698350866487717403917070872269853194118364230971216854931998642990452908852258008095741042117326241406479532880476938937997238098399302185675832474590293188864060116934035867037219176916416481757918864533515526389079998129329045569609325290897577497835388451456680707076072624629697883854217331728051953671643278797380171857920000*I/2103100954337624833663208713697737151593634525061637972297915388685604042449504336765884978184588688426595940401280828953096857809292320006227881797146858511436638446932833617514351442216409828605662238790280753075176269765767010004889778647709740770757817960711900340755635772183674511158570690702969774966791073165467918123298694584729211212414462628433370481195120564586361368504153395406845170075275051749019600057116719726628746724489572189061061036426955163696859127711110719502594479795200686212257570291758725259007379710596548777812659422174199194837355646482046783616494013289495563083118517507178847555801163089723056310287760875135196081975602765511153122381201303871673391366630940702817360340900568748719988954847590748960761446218262344767250783946365392689256634180417145926390656439421745644011831124277463643383712803287985472471755648426749842410972650924240795946699346613614779460399530274263580007672855851663196114585312432954432654691485867618908420370875753749297487803461900447407917655296784879220450937110470920633595689721819488638484547259978337741496090602390463594556401615298457456112485536498177883358587125449801777718900375736758266215245325999241624148841915093787519330809347240990363802360596034171167818310322276373120180985148650099673289383722502488957717848531612020897298448601714154586319660314294591620415272119454982220034319689607295960162971300417552364254983071768070124456169427638371140064235083443242844616326538396503937972586505546495649094344512270582463639152160238137952390380581401171977159154009407415523525096743009110916334144716516647041176989758534635251844947906038080852185583742296318878233394998111078843229681030277039104786225656992262073797524057992347971177720807155842376332851559276430280477639539393920006008737472164850104411971830120295750221200029811143140323763349636629725073624360001 + + a = Symbol('a', real=True) + b = Symbol('b', real=True) + assert exp(a*(2 + I*b)).expand(complex=True) == \ + I*exp(2*a)*sin(a*b) + exp(2*a)*cos(a*b) + + +def test_expand(): + f = (16 - 2*sqrt(29))**2 + assert f.expand() == 372 - 64*sqrt(29) + f = (Integer(1)/2 + I/2)**10 + assert f.expand() == I/32 + f = (Integer(1)/2 + I)**10 + assert f.expand() == Integer(237)/1024 - 779*I/256 + + +def test_re_im1652(): + x = Symbol('x') + assert re(x) == re(conjugate(x)) + assert im(x) == - im(conjugate(x)) + assert im(x)*re(conjugate(x)) + im(conjugate(x)) * re(x) == 0 + + +def test_issue_5084(): + x = Symbol('x') + assert ((x + x*I)/(1 + I)).as_real_imag() == (re((x + I*x)/(1 + I) + ), im((x + I*x)/(1 + I))) + + +def test_issue_5236(): + assert (cos(1 + I)**3).as_real_imag() == (-3*sin(1)**2*sinh(1)**2*cos(1)*cosh(1) + + cos(1)**3*cosh(1)**3, -3*cos(1)**2*cosh(1)**2*sin(1)*sinh(1) + sin(1)**3*sinh(1)**3) + + +def test_real_imag(): + x, y, z = symbols('x, y, z') + X, Y, Z = symbols('X, Y, Z', commutative=False) + a = Symbol('a', real=True) + assert (2*a*x).as_real_imag() == (2*a*re(x), 2*a*im(x)) + + # issue 5395: + assert (x*x.conjugate()).as_real_imag() == (Abs(x)**2, 0) + assert im(x*x.conjugate()) == 0 + assert im(x*y.conjugate()*z*y) == im(x*z)*Abs(y)**2 + assert im(x*y.conjugate()*x*y) == im(x**2)*Abs(y)**2 + assert im(Z*y.conjugate()*X*y) == im(Z*X)*Abs(y)**2 + assert im(X*X.conjugate()) == im(X*X.conjugate(), evaluate=False) + assert (sin(x)*sin(x).conjugate()).as_real_imag() == \ + (Abs(sin(x))**2, 0) + + # issue 6573: + assert (x**2).as_real_imag() == (re(x)**2 - im(x)**2, 2*re(x)*im(x)) + + # issue 6428: + r = Symbol('r', real=True) + i = Symbol('i', imaginary=True) + assert (i*r*x).as_real_imag() == (I*i*r*im(x), -I*i*r*re(x)) + assert (i*r*x*(y + 2)).as_real_imag() == ( + I*i*r*(re(y) + 2)*im(x) + I*i*r*re(x)*im(y), + -I*i*r*(re(y) + 2)*re(x) + I*i*r*im(x)*im(y)) + + # issue 7106: + assert ((1 + I)/(1 - I)).as_real_imag() == (0, 1) + assert ((1 + 2*I)*(1 + 3*I)).as_real_imag() == (-5, 5) + + +def test_pow_issue_1724(): + e = ((S.NegativeOne)**(S.One/3)) + assert e.conjugate().n() == e.n().conjugate() + e = S('-2/3 - (-29/54 + sqrt(93)/18)**(1/3) - 1/(9*(-29/54 + sqrt(93)/18)**(1/3))') + assert e.conjugate().n() == e.n().conjugate() + e = 2**I + assert e.conjugate().n() == e.n().conjugate() + + +def test_issue_5429(): + assert sqrt(I).conjugate() != sqrt(I) + +def test_issue_4124(): + from sympy.core.numbers import oo + assert expand_complex(I*oo) == oo*I + +def test_issue_11518(): + x = Symbol("x", real=True) + y = Symbol("y", real=True) + r = sqrt(x**2 + y**2) + assert conjugate(r) == r + s = abs(x + I * y) + assert conjugate(s) == r diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_constructor_postprocessor.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_constructor_postprocessor.py new file mode 100644 index 0000000000000000000000000000000000000000..c199e24eddf8ef7c2a14e38d1ad2dc95e4acc0cc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_constructor_postprocessor.py @@ -0,0 +1,87 @@ +from sympy.core.basic import Basic +from sympy.core.mul import Mul +from sympy.core.symbol import (Symbol, symbols) + +from sympy.testing.pytest import XFAIL + +class SymbolInMulOnce(Symbol): + # Test class for a symbol that can only appear once in a `Mul` expression. + pass + + +Basic._constructor_postprocessor_mapping[SymbolInMulOnce] = { + "Mul": [lambda x: x], + "Pow": [lambda x: x.base if isinstance(x.base, SymbolInMulOnce) else x], + "Add": [lambda x: x], +} + + +def _postprocess_SymbolRemovesOtherSymbols(expr): + args = tuple(i for i in expr.args if not isinstance(i, Symbol) or isinstance(i, SymbolRemovesOtherSymbols)) + if args == expr.args: + return expr + return Mul.fromiter(args) + + +class SymbolRemovesOtherSymbols(Symbol): + # Test class for a symbol that removes other symbols in `Mul`. + pass + +Basic._constructor_postprocessor_mapping[SymbolRemovesOtherSymbols] = { + "Mul": [_postprocess_SymbolRemovesOtherSymbols], +} + +class SubclassSymbolInMulOnce(SymbolInMulOnce): + pass + +class SubclassSymbolRemovesOtherSymbols(SymbolRemovesOtherSymbols): + pass + + +def test_constructor_postprocessors1(): + x = SymbolInMulOnce("x") + y = SymbolInMulOnce("y") + assert isinstance(3*x, Mul) + assert (3*x).args == (3, x) + assert x*x == x + assert 3*x*x == 3*x + assert 2*x*x + x == 3*x + assert x**3*y*y == x*y + assert x**5 + y*x**3 == x + x*y + + w = SymbolRemovesOtherSymbols("w") + assert x*w == w + assert (3*w).args == (3, w) + assert set((w + x).args) == {x, w} + +def test_constructor_postprocessors2(): + x = SubclassSymbolInMulOnce("x") + y = SubclassSymbolInMulOnce("y") + assert isinstance(3*x, Mul) + assert (3*x).args == (3, x) + assert x*x == x + assert 3*x*x == 3*x + assert 2*x*x + x == 3*x + assert x**3*y*y == x*y + assert x**5 + y*x**3 == x + x*y + + w = SubclassSymbolRemovesOtherSymbols("w") + assert x*w == w + assert (3*w).args == (3, w) + assert set((w + x).args) == {x, w} + + +@XFAIL +def test_subexpression_postprocessors(): + # The postprocessors used to work with subexpressions, but the + # functionality was removed. See #15948. + a = symbols("a") + x = SymbolInMulOnce("x") + w = SymbolRemovesOtherSymbols("w") + assert 3*a*w**2 == 3*w**2 + assert 3*a*x**3*w**2 == 3*w**2 + + x = SubclassSymbolInMulOnce("x") + w = SubclassSymbolRemovesOtherSymbols("w") + assert 3*a*w**2 == 3*w**2 + assert 3*a*x**3*w**2 == 3*w**2 diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_containers.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_containers.py new file mode 100644 index 0000000000000000000000000000000000000000..23357b9f667fffc82d93b2b1adb42b495114c67e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_containers.py @@ -0,0 +1,217 @@ +from collections import defaultdict + +from sympy.core.basic import Basic +from sympy.core.containers import (Dict, Tuple) +from sympy.core.numbers import Integer +from sympy.core.kind import NumberKind +from sympy.matrices.kind import MatrixKind +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.core.sympify import sympify +from sympy.matrices.dense import Matrix +from sympy.sets.sets import FiniteSet +from sympy.core.containers import tuple_wrapper, TupleKind +from sympy.core.expr import unchanged +from sympy.core.function import Function, Lambda +from sympy.core.relational import Eq +from sympy.testing.pytest import raises +from sympy.utilities.iterables import is_sequence, iterable + +from sympy.abc import x, y + + +def test_Tuple(): + t = (1, 2, 3, 4) + st = Tuple(*t) + assert set(sympify(t)) == set(st) + assert len(t) == len(st) + assert set(sympify(t[:2])) == set(st[:2]) + assert isinstance(st[:], Tuple) + assert st == Tuple(1, 2, 3, 4) + assert st.func(*st.args) == st + p, q, r, s = symbols('p q r s') + t2 = (p, q, r, s) + st2 = Tuple(*t2) + assert st2.atoms() == set(t2) + assert st == st2.subs({p: 1, q: 2, r: 3, s: 4}) + # issue 5505 + assert all(isinstance(arg, Basic) for arg in st.args) + assert Tuple(p, 1).subs(p, 0) == Tuple(0, 1) + assert Tuple(p, Tuple(p, 1)).subs(p, 0) == Tuple(0, Tuple(0, 1)) + + assert Tuple(t2) == Tuple(Tuple(*t2)) + assert Tuple.fromiter(t2) == Tuple(*t2) + assert Tuple.fromiter(x for x in range(4)) == Tuple(0, 1, 2, 3) + assert st2.fromiter(st2.args) == st2 + + +def test_Tuple_contains(): + t1, t2 = Tuple(1), Tuple(2) + assert t1 in Tuple(1, 2, 3, t1, Tuple(t2)) + assert t2 not in Tuple(1, 2, 3, t1, Tuple(t2)) + + +def test_Tuple_concatenation(): + assert Tuple(1, 2) + Tuple(3, 4) == Tuple(1, 2, 3, 4) + assert (1, 2) + Tuple(3, 4) == Tuple(1, 2, 3, 4) + assert Tuple(1, 2) + (3, 4) == Tuple(1, 2, 3, 4) + raises(TypeError, lambda: Tuple(1, 2) + 3) + raises(TypeError, lambda: 1 + Tuple(2, 3)) + + #the Tuple case in __radd__ is only reached when a subclass is involved + class Tuple2(Tuple): + def __radd__(self, other): + return Tuple.__radd__(self, other + other) + assert Tuple(1, 2) + Tuple2(3, 4) == Tuple(1, 2, 1, 2, 3, 4) + assert Tuple2(1, 2) + Tuple(3, 4) == Tuple(1, 2, 3, 4) + + +def test_Tuple_equality(): + assert not isinstance(Tuple(1, 2), tuple) + assert (Tuple(1, 2) == (1, 2)) is True + assert (Tuple(1, 2) != (1, 2)) is False + assert (Tuple(1, 2) == (1, 3)) is False + assert (Tuple(1, 2) != (1, 3)) is True + assert (Tuple(1, 2) == Tuple(1, 2)) is True + assert (Tuple(1, 2) != Tuple(1, 2)) is False + assert (Tuple(1, 2) == Tuple(1, 3)) is False + assert (Tuple(1, 2) != Tuple(1, 3)) is True + + +def test_Tuple_Eq(): + assert Eq(Tuple(), Tuple()) is S.true + assert Eq(Tuple(1), 1) is S.false + assert Eq(Tuple(1, 2), Tuple(1)) is S.false + assert Eq(Tuple(1), Tuple(1)) is S.true + assert Eq(Tuple(1, 2), Tuple(1, 3)) is S.false + assert Eq(Tuple(1, 2), Tuple(1, 2)) is S.true + assert unchanged(Eq, Tuple(1, x), Tuple(1, 2)) + assert Eq(Tuple(1, x), Tuple(1, 2)).subs(x, 2) is S.true + assert unchanged(Eq, Tuple(1, 2), x) + f = Function('f') + assert unchanged(Eq, Tuple(1), f(x)) + assert Eq(Tuple(1), f(x)).subs(x, 1).subs(f, Lambda(y, (y,))) is S.true + + +def test_Tuple_comparision(): + assert (Tuple(1, 3) >= Tuple(-10, 30)) is S.true + assert (Tuple(1, 3) <= Tuple(-10, 30)) is S.false + assert (Tuple(1, 3) >= Tuple(1, 3)) is S.true + assert (Tuple(1, 3) <= Tuple(1, 3)) is S.true + + +def test_Tuple_tuple_count(): + assert Tuple(0, 1, 2, 3).tuple_count(4) == 0 + assert Tuple(0, 4, 1, 2, 3).tuple_count(4) == 1 + assert Tuple(0, 4, 1, 4, 2, 3).tuple_count(4) == 2 + assert Tuple(0, 4, 1, 4, 2, 4, 3).tuple_count(4) == 3 + + +def test_Tuple_index(): + assert Tuple(4, 0, 1, 2, 3).index(4) == 0 + assert Tuple(0, 4, 1, 2, 3).index(4) == 1 + assert Tuple(0, 1, 4, 2, 3).index(4) == 2 + assert Tuple(0, 1, 2, 4, 3).index(4) == 3 + assert Tuple(0, 1, 2, 3, 4).index(4) == 4 + + raises(ValueError, lambda: Tuple(0, 1, 2, 3).index(4)) + raises(ValueError, lambda: Tuple(4, 0, 1, 2, 3).index(4, 1)) + raises(ValueError, lambda: Tuple(0, 1, 2, 3, 4).index(4, 1, 4)) + + +def test_Tuple_mul(): + assert Tuple(1, 2, 3)*2 == Tuple(1, 2, 3, 1, 2, 3) + assert 2*Tuple(1, 2, 3) == Tuple(1, 2, 3, 1, 2, 3) + assert Tuple(1, 2, 3)*Integer(2) == Tuple(1, 2, 3, 1, 2, 3) + assert Integer(2)*Tuple(1, 2, 3) == Tuple(1, 2, 3, 1, 2, 3) + + raises(TypeError, lambda: Tuple(1, 2, 3)*S.Half) + raises(TypeError, lambda: S.Half*Tuple(1, 2, 3)) + + +def test_tuple_wrapper(): + + @tuple_wrapper + def wrap_tuples_and_return(*t): + return t + + p = symbols('p') + assert wrap_tuples_and_return(p, 1) == (p, 1) + assert wrap_tuples_and_return((p, 1)) == (Tuple(p, 1),) + assert wrap_tuples_and_return(1, (p, 2), 3) == (1, Tuple(p, 2), 3) + + +def test_iterable_is_sequence(): + ordered = [[], (), Tuple(), Matrix([[]])] + unordered = [set()] + not_sympy_iterable = [{}, '', ''] + assert all(is_sequence(i) for i in ordered) + assert all(not is_sequence(i) for i in unordered) + assert all(iterable(i) for i in ordered + unordered) + assert all(not iterable(i) for i in not_sympy_iterable) + assert all(iterable(i, exclude=None) for i in not_sympy_iterable) + + +def test_TupleKind(): + kind = TupleKind(NumberKind, MatrixKind(NumberKind)) + assert Tuple(1, Matrix([1, 2])).kind is kind + assert Tuple(1, 2).kind is TupleKind(NumberKind, NumberKind) + assert Tuple(1, 2).kind.element_kind == (NumberKind, NumberKind) + + +def test_Dict(): + x, y, z = symbols('x y z') + d = Dict({x: 1, y: 2, z: 3}) + assert d[x] == 1 + assert d[y] == 2 + raises(KeyError, lambda: d[2]) + raises(KeyError, lambda: d['2']) + assert len(d) == 3 + assert set(d.keys()) == {x, y, z} + assert set(d.values()) == {S.One, S(2), S(3)} + assert d.get(5, 'default') == 'default' + assert d.get('5', 'default') == 'default' + assert x in d and z in d and 5 not in d and '5' not in d + assert d.has(x) and d.has(1) # SymPy Basic .has method + + # Test input types + # input - a Python dict + # input - items as args - SymPy style + assert (Dict({x: 1, y: 2, z: 3}) == + Dict((x, 1), (y, 2), (z, 3))) + + raises(TypeError, lambda: Dict(((x, 1), (y, 2), (z, 3)))) + with raises(NotImplementedError): + d[5] = 6 # assert immutability + + assert set( + d.items()) == {Tuple(x, S.One), Tuple(y, S(2)), Tuple(z, S(3))} + assert set(d) == {x, y, z} + assert str(d) == '{x: 1, y: 2, z: 3}' + assert d.__repr__() == '{x: 1, y: 2, z: 3}' + + # Test creating a Dict from a Dict. + d = Dict({x: 1, y: 2, z: 3}) + assert d == Dict(d) + + # Test for supporting defaultdict + d = defaultdict(int) + assert d[x] == 0 + assert d[y] == 0 + assert d[z] == 0 + assert Dict(d) + d = Dict(d) + assert len(d) == 3 + assert set(d.keys()) == {x, y, z} + assert set(d.values()) == {S.Zero, S.Zero, S.Zero} + + +def test_issue_5788(): + args = [(1, 2), (2, 1)] + for o in [Dict, Tuple, FiniteSet]: + # __eq__ and arg handling + if o != Tuple: + assert o(*args) == o(*reversed(args)) + pair = [o(*args), o(*reversed(args))] + assert sorted(pair) == sorted(pair) + assert set(o(*args)) # doesn't fail diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_count_ops.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_count_ops.py new file mode 100644 index 0000000000000000000000000000000000000000..bc95004ef5ba4421927289a049a9197d208c30d0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_count_ops.py @@ -0,0 +1,155 @@ +from sympy.concrete.summations import Sum +from sympy.core.basic import Basic +from sympy.core.function import (Derivative, Function, count_ops) +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.relational import (Eq, Rel) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.integrals.integrals import Integral +from sympy.logic.boolalg import (And, Equivalent, ITE, Implies, Nand, + Nor, Not, Or, Xor) +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.core.containers import Tuple + +x, y, z = symbols('x,y,z') +a, b, c = symbols('a,b,c') + +def test_count_ops_non_visual(): + def count(val): + return count_ops(val, visual=False) + assert count(x) == 0 + assert count(x) is not S.Zero + assert count(x + y) == 1 + assert count(x + y) is not S.One + assert count(x + y*x + 2*y) == 4 + assert count({x + y: x}) == 1 + assert count({x + y: S(2) + x}) is not S.One + assert count(x < y) == 1 + assert count(Or(x,y)) == 1 + assert count(And(x,y)) == 1 + assert count(Not(x)) == 1 + assert count(Nor(x,y)) == 2 + assert count(Nand(x,y)) == 2 + assert count(Xor(x,y)) == 1 + assert count(Implies(x,y)) == 1 + assert count(Equivalent(x,y)) == 1 + assert count(ITE(x,y,z)) == 1 + assert count(ITE(True,x,y)) == 0 + + +def test_count_ops_visual(): + ADD, MUL, POW, SIN, COS, EXP, AND, D, G, M = symbols( + 'Add Mul Pow sin cos exp And Derivative Integral Sum'.upper()) + DIV, SUB, NEG = symbols('DIV SUB NEG') + LT, LE, GT, GE, EQ, NE = symbols('LT LE GT GE EQ NE') + NOT, OR, AND, XOR, IMPLIES, EQUIVALENT, _ITE, BASIC, TUPLE = symbols( + 'Not Or And Xor Implies Equivalent ITE Basic Tuple'.upper()) + + def count(val): + return count_ops(val, visual=True) + + assert count(7) is S.Zero + assert count(S(7)) is S.Zero + assert count(-1) == NEG + assert count(-2) == NEG + assert count(S(2)/3) == DIV + assert count(Rational(2, 3)) == DIV + assert count(pi/3) == DIV + assert count(-pi/3) == DIV + NEG + assert count(I - 1) == SUB + assert count(1 - I) == SUB + assert count(1 - 2*I) == SUB + MUL + + assert count(x) is S.Zero + assert count(-x) == NEG + assert count(-2*x/3) == NEG + DIV + MUL + assert count(Rational(-2, 3)*x) == NEG + DIV + MUL + assert count(1/x) == DIV + assert count(1/(x*y)) == DIV + MUL + assert count(-1/x) == NEG + DIV + assert count(-2/x) == NEG + DIV + assert count(x/y) == DIV + assert count(-x/y) == NEG + DIV + + assert count(x**2) == POW + assert count(-x**2) == POW + NEG + assert count(-2*x**2) == POW + MUL + NEG + + assert count(x + pi/3) == ADD + DIV + assert count(x + S.One/3) == ADD + DIV + assert count(x + Rational(1, 3)) == ADD + DIV + assert count(x + y) == ADD + assert count(x - y) == SUB + assert count(y - x) == SUB + assert count(-1/(x - y)) == DIV + NEG + SUB + assert count(-1/(y - x)) == DIV + NEG + SUB + assert count(1 + x**y) == ADD + POW + assert count(1 + x + y) == 2*ADD + assert count(1 + x + y + z) == 3*ADD + assert count(1 + x**y + 2*x*y + y**2) == 3*ADD + 2*POW + 2*MUL + assert count(2*z + y + x + 1) == 3*ADD + MUL + assert count(2*z + y**17 + x + 1) == 3*ADD + MUL + POW + assert count(2*z + y**17 + x + sin(x)) == 3*ADD + POW + MUL + SIN + assert count(2*z + y**17 + x + sin(x**2)) == 3*ADD + MUL + 2*POW + SIN + assert count(2*z + y**17 + x + sin( + x**2) + exp(cos(x))) == 4*ADD + MUL + 2*POW + EXP + COS + SIN + + assert count(Derivative(x, x)) == D + assert count(Integral(x, x) + 2*x/(1 + x)) == G + DIV + MUL + 2*ADD + assert count(Sum(x, (x, 1, x + 1)) + 2*x/(1 + x)) == M + DIV + MUL + 3*ADD + assert count(Basic()) is S.Zero + + assert count({x + 1: sin(x)}) == ADD + SIN + assert count([x + 1, sin(x) + y, None]) == ADD + SIN + ADD + assert count({x + 1: sin(x), y: cos(x) + 1}) == SIN + COS + 2*ADD + assert count({}) is S.Zero + assert count([x + 1, sin(x)*y, None]) == SIN + ADD + MUL + assert count([]) is S.Zero + + assert count(Basic()) == 0 + assert count(Basic(Basic(),Basic(x,x+y))) == ADD + 2*BASIC + assert count(Basic(x, x + y)) == ADD + BASIC + assert [count(Rel(x, y, op)) for op in '< <= > >= == <> !='.split() + ] == [LT, LE, GT, GE, EQ, NE, NE] + assert count(Or(x, y)) == OR + assert count(And(x, y)) == AND + assert count(Or(x, Or(y, And(z, a)))) == AND + OR + assert count(Nor(x, y)) == NOT + OR + assert count(Nand(x, y)) == NOT + AND + assert count(Xor(x, y)) == XOR + assert count(Implies(x, y)) == IMPLIES + assert count(Equivalent(x, y)) == EQUIVALENT + assert count(ITE(x, y, z)) == _ITE + assert count([Or(x, y), And(x, y), Basic(x + y)] + ) == ADD + AND + BASIC + OR + + assert count(Basic(Tuple(x))) == BASIC + TUPLE + #It checks that TUPLE is counted as an operation. + + assert count(Eq(x + y, S(2))) == ADD + EQ + + +def test_issue_9324(): + def count(val): + return count_ops(val, visual=False) + + M = MatrixSymbol('M', 10, 10) + assert count(M[0, 0]) == 0 + assert count(2 * M[0, 0] + M[5, 7]) == 2 + P = MatrixSymbol('P', 3, 3) + Q = MatrixSymbol('Q', 3, 3) + assert count(P + Q) == 1 + m = Symbol('m', integer=True) + n = Symbol('n', integer=True) + M = MatrixSymbol('M', m + n, m * m) + assert count(M[0, 1]) == 2 + + +def test_issue_21532(): + f = Function('f') + g = Function('g') + FUNC_F, FUNC_G = symbols('FUNC_F, FUNC_G') + assert f(x).count_ops(visual=True) == FUNC_F + assert g(x).count_ops(visual=True) == FUNC_G diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_diff.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_diff.py new file mode 100644 index 0000000000000000000000000000000000000000..effc9cd91d2e7b6f8f8e5fd04bb667ed71c0ffaf --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_diff.py @@ -0,0 +1,160 @@ +from sympy.concrete.summations import Sum +from sympy.core.expr import Expr +from sympy.core.function import (Derivative, Function, diff, Subs) +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.complexes import (im, re) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import Max +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (cos, cot, sin, tan) +from sympy.tensor.array.ndim_array import NDimArray +from sympy.testing.pytest import raises +from sympy.abc import a, b, c, x, y, z + +def test_diff(): + assert Rational(1, 3).diff(x) is S.Zero + assert I.diff(x) is S.Zero + assert pi.diff(x) is S.Zero + assert x.diff(x, 0) == x + assert (x**2).diff(x, 2, x) == 0 + assert (x**2).diff((x, 2), x) == 0 + assert (x**2).diff((x, 1), x) == 2 + assert (x**2).diff((x, 1), (x, 1)) == 2 + assert (x**2).diff((x, 2)) == 2 + assert (x**2).diff(x, y, 0) == 2*x + assert (x**2).diff(x, (y, 0)) == 2*x + assert (x**2).diff(x, y) == 0 + raises(ValueError, lambda: x.diff(1, x)) + + p = Rational(5) + e = a*b + b**p + assert e.diff(a) == b + assert e.diff(b) == a + 5*b**4 + assert e.diff(b).diff(a) == Rational(1) + e = a*(b + c) + assert e.diff(a) == b + c + assert e.diff(b) == a + assert e.diff(b).diff(a) == Rational(1) + e = c**p + assert e.diff(c, 6) == Rational(0) + assert e.diff(c, 5) == Rational(120) + e = c**Rational(2) + assert e.diff(c) == 2*c + e = a*b*c + assert e.diff(c) == a*b + + +def test_diff2(): + n3 = Rational(3) + n2 = Rational(2) + n6 = Rational(6) + + e = n3*(-n2 + x**n2)*cos(x) + x*(-n6 + x**n2)*sin(x) + assert e == 3*(-2 + x**2)*cos(x) + x*(-6 + x**2)*sin(x) + assert e.diff(x).expand() == x**3*cos(x) + + e = (x + 1)**3 + assert e.diff(x) == 3*(x + 1)**2 + e = x*(x + 1)**3 + assert e.diff(x) == (x + 1)**3 + 3*x*(x + 1)**2 + e = 2*exp(x*x)*x + assert e.diff(x) == 2*exp(x**2) + 4*x**2*exp(x**2) + + +def test_diff3(): + p = Rational(5) + e = a*b + sin(b**p) + assert e == a*b + sin(b**5) + assert e.diff(a) == b + assert e.diff(b) == a + 5*b**4*cos(b**5) + e = tan(c) + assert e == tan(c) + assert e.diff(c) in [cos(c)**(-2), 1 + sin(c)**2/cos(c)**2, 1 + tan(c)**2] + e = c*log(c) - c + assert e == -c + c*log(c) + assert e.diff(c) == log(c) + e = log(sin(c)) + assert e == log(sin(c)) + assert e.diff(c) in [sin(c)**(-1)*cos(c), cot(c)] + e = (Rational(2)**a/log(Rational(2))) + assert e == 2**a*log(Rational(2))**(-1) + assert e.diff(a) == 2**a + + +def test_diff_no_eval_derivative(): + class My(Expr): + def __new__(cls, x): + return Expr.__new__(cls, x) + + # My doesn't have its own _eval_derivative method + assert My(x).diff(x).func is Derivative + assert My(x).diff(x, 3).func is Derivative + assert re(x).diff(x, 2) == Derivative(re(x), (x, 2)) # issue 15518 + assert diff(NDimArray([re(x), im(x)]), (x, 2)) == NDimArray( + [Derivative(re(x), (x, 2)), Derivative(im(x), (x, 2))]) + # it doesn't have y so it shouldn't need a method for this case + assert My(x).diff(y) == 0 + + +def test_speed(): + # this should return in 0.0s. If it takes forever, it's wrong. + assert x.diff(x, 10**8) == 0 + + +def test_deriv_noncommutative(): + A = Symbol("A", commutative=False) + f = Function("f") + assert A*f(x)*A == f(x)*A**2 + assert A*f(x).diff(x)*A == f(x).diff(x) * A**2 + + +def test_diff_nth_derivative(): + f = Function("f") + n = Symbol("n", integer=True) + + expr = diff(sin(x), (x, n)) + expr2 = diff(f(x), (x, 2)) + expr3 = diff(f(x), (x, n)) + + assert expr.subs(sin(x), cos(-x)) == Derivative(cos(-x), (x, n)) + assert expr.subs(n, 1).doit() == cos(x) + assert expr.subs(n, 2).doit() == -sin(x) + + assert expr2.subs(Derivative(f(x), x), y) == Derivative(y, x) + # Currently not supported (cannot determine if `n > 1`): + #assert expr3.subs(Derivative(f(x), x), y) == Derivative(y, (x, n-1)) + assert expr3 == Derivative(f(x), (x, n)) + + assert diff(x, (x, n)) == Piecewise((x, Eq(n, 0)), (1, Eq(n, 1)), (0, True)) + assert diff(2*x, (x, n)).dummy_eq( + Sum(Piecewise((2*x*factorial(n)/(factorial(y)*factorial(-y + n)), + Eq(y, 0) & Eq(Max(0, -y + n), 0)), + (2*factorial(n)/(factorial(y)*factorial(-y + n)), Eq(y, 0) & Eq(Max(0, + -y + n), 1)), (0, True)), (y, 0, n))) + # TODO: assert diff(x**2, (x, n)) == x**(2-n)*ff(2, n) + exprm = x*sin(x) + mul_diff = diff(exprm, (x, n)) + assert isinstance(mul_diff, Sum) + for i in range(5): + assert mul_diff.subs(n, i).doit() == exprm.diff((x, i)).expand() + + exprm2 = 2*y*x*sin(x)*cos(x)*log(x)*exp(x) + dex = exprm2.diff((x, n)) + assert isinstance(dex, Sum) + for i in range(7): + assert dex.subs(n, i).doit().expand() == \ + exprm2.diff((x, i)).expand() + + assert (cos(x)*sin(y)).diff([[x, y, z]]) == NDimArray([ + -sin(x)*sin(y), cos(x)*cos(y), 0]) + + +def test_issue_16160(): + assert Derivative(x**3, (x, x)).subs(x, 2) == Subs( + Derivative(x**3, (x, 2)), x, 2) + assert Derivative(1 + x**3, (x, x)).subs(x, 0 + ) == Derivative(1 + y**3, (y, 0)).subs(y, 0) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_equal.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_equal.py new file mode 100644 index 0000000000000000000000000000000000000000..82213b757cda5fbd80310e387bdf00cc1c9c25fe --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_equal.py @@ -0,0 +1,89 @@ +from sympy.core.numbers import Rational +from sympy.core.symbol import (Dummy, Symbol) +from sympy.functions.elementary.exponential import exp + + +def test_equal(): + b = Symbol("b") + a = Symbol("a") + e1 = a + b + e2 = 2*a*b + e3 = a**3*b**2 + e4 = a*b + b*a + assert not e1 == e2 + assert not e1 == e2 + assert e1 != e2 + assert e2 == e4 + assert e2 != e3 + assert not e2 == e3 + + x = Symbol("x") + e1 = exp(x + 1/x) + y = Symbol("x") + e2 = exp(y + 1/y) + assert e1 == e2 + assert not e1 != e2 + y = Symbol("y") + e2 = exp(y + 1/y) + assert not e1 == e2 + assert e1 != e2 + + e5 = Rational(3) + 2*x - x - x + assert e5 == 3 + assert 3 == e5 + assert e5 != 4 + assert 4 != e5 + assert e5 != 3 + x + assert 3 + x != e5 + + +def test_expevalbug(): + x = Symbol("x") + e1 = exp(1*x) + e3 = exp(x) + assert e1 == e3 + + +def test_cmp_bug1(): + class T: + pass + + t = T() + x = Symbol("x") + + assert not (x == t) + assert (x != t) + + +def test_cmp_bug2(): + class T: + pass + + t = T() + + assert not (Symbol == t) + assert (Symbol != t) + + +def test_cmp_issue_4357(): + """ Check that Basic subclasses can be compared with sympifiable objects. + + https://github.com/sympy/sympy/issues/4357 + """ + assert not (Symbol == 1) + assert (Symbol != 1) + assert not (Symbol == 'x') + assert (Symbol != 'x') + + +def test_dummy_eq(): + x = Symbol('x') + y = Symbol('y') + + u = Dummy('u') + + assert (u**2 + 1).dummy_eq(x**2 + 1) is True + assert ((u**2 + 1) == (x**2 + 1)) is False + + assert (u**2 + y).dummy_eq(x**2 + y, x) is True + assert (u**2 + y).dummy_eq(x**2 + y, y) is False diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_eval.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_eval.py new file mode 100644 index 0000000000000000000000000000000000000000..9c1633f77b50483afee21c6d9fca232b1279d2b9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_eval.py @@ -0,0 +1,95 @@ +from sympy.core.function import Function +from sympy.core.numbers import (I, Rational) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, tan) +from sympy.testing.pytest import XFAIL + + +def test_add_eval(): + a = Symbol("a") + b = Symbol("b") + c = Rational(1) + p = Rational(5) + assert a*b + c + p == a*b + 6 + assert c + a + p == a + 6 + assert c + a - p == a + (-4) + assert a + a == 2*a + assert a + p + a == 2*a + 5 + assert c + p == Rational(6) + assert b + a - b == a + + +def test_addmul_eval(): + a = Symbol("a") + b = Symbol("b") + c = Rational(1) + p = Rational(5) + assert c + a + b*c + a - p == 2*a + b + (-4) + assert a*2 + p + a == a*2 + 5 + a + assert a*2 + p + a == 3*a + 5 + assert a*2 + a == 3*a + + +def test_pow_eval(): + # XXX Pow does not fully support conversion of negative numbers + # to their complex equivalent + + assert sqrt(-1) == I + + assert sqrt(-4) == 2*I + assert sqrt( 4) == 2 + assert (8)**Rational(1, 3) == 2 + assert (-8)**Rational(1, 3) == 2*((-1)**Rational(1, 3)) + + assert sqrt(-2) == I*sqrt(2) + assert (-1)**Rational(1, 3) != I + assert (-10)**Rational(1, 3) != I*((10)**Rational(1, 3)) + assert (-2)**Rational(1, 4) != (2)**Rational(1, 4) + + assert 64**Rational(1, 3) == 4 + assert 64**Rational(2, 3) == 16 + assert 24/sqrt(64) == 3 + assert (-27)**Rational(1, 3) == 3*(-1)**Rational(1, 3) + + assert (cos(2) / tan(2))**2 == (cos(2) / tan(2))**2 + + +@XFAIL +def test_pow_eval_X1(): + assert (-1)**Rational(1, 3) == S.Half + S.Half*I*sqrt(3) + + +def test_mulpow_eval(): + x = Symbol('x') + assert sqrt(50)/(sqrt(2)*x) == 5/x + assert sqrt(27)/sqrt(3) == 3 + + +def test_evalpow_bug(): + x = Symbol("x") + assert 1/(1/x) == x + assert 1/(-1/x) == -x + + +def test_symbol_expand(): + x = Symbol('x') + y = Symbol('y') + + f = x**4*y**4 + assert f == x**4*y**4 + assert f == f.expand() + + g = (x*y)**4 + assert g == f + assert g.expand() == f + assert g.expand() == g.expand().expand() + + +def test_function(): + f, l = map(Function, 'fl') + x = Symbol('x') + assert exp(l(x))*l(x)/exp(l(x)) == l(x) + assert exp(f(x))*f(x)/exp(f(x)) == f(x) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_evalf.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_evalf.py new file mode 100644 index 0000000000000000000000000000000000000000..2c3c26a2d265da9ea2daa73a9eea3091b2af1999 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_evalf.py @@ -0,0 +1,738 @@ +import math + +from sympy.concrete.products import (Product, product) +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.evalf import N +from sympy.core.function import (Function, nfloat) +from sympy.core.mul import Mul +from sympy.core import (GoldenRatio) +from sympy.core.numbers import (AlgebraicNumber, E, Float, I, Rational, + oo, zoo, nan, pi) +from sympy.core.power import Pow +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.combinatorial.numbers import fibonacci +from sympy.functions.elementary.complexes import (Abs, re, im) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import (acosh, cosh) +from sympy.functions.elementary.integers import (ceiling, floor) +from sympy.functions.elementary.miscellaneous import (Max, sqrt) +from sympy.functions.elementary.trigonometric import (acos, atan, cos, sin, tan) +from sympy.integrals.integrals import (Integral, integrate) +from sympy.polys.polytools import factor +from sympy.polys.rootoftools import CRootOf +from sympy.polys.specialpolys import cyclotomic_poly +from sympy.printing import srepr +from sympy.printing.str import sstr +from sympy.simplify.simplify import simplify +from sympy.core.numbers import comp +from sympy.core.evalf import (complex_accuracy, PrecisionExhausted, + scaled_zero, get_integer_part, as_mpmath, evalf, _evalf_with_bounded_error) +from mpmath import inf, ninf, make_mpc +from mpmath.libmp.libmpf import from_float, fzero +from sympy.core.expr import unchanged +from sympy.testing.pytest import raises, XFAIL +from sympy.abc import n, x, y + + +def NS(e, n=15, **options): + return sstr(sympify(e).evalf(n, **options), full_prec=True) + + +def test_evalf_helpers(): + from mpmath.libmp import finf + assert complex_accuracy((from_float(2.0), None, 35, None)) == 35 + assert complex_accuracy((from_float(2.0), from_float(10.0), 35, 100)) == 37 + assert complex_accuracy( + (from_float(2.0), from_float(1000.0), 35, 100)) == 43 + assert complex_accuracy((from_float(2.0), from_float(10.0), 100, 35)) == 35 + assert complex_accuracy( + (from_float(2.0), from_float(1000.0), 100, 35)) == 35 + assert complex_accuracy(finf) == math.inf + assert complex_accuracy(zoo) == math.inf + raises(ValueError, lambda: get_integer_part(zoo, 1, {})) + + +def test_evalf_basic(): + assert NS('pi', 15) == '3.14159265358979' + assert NS('2/3', 10) == '0.6666666667' + assert NS('355/113-pi', 6) == '2.66764e-7' + assert NS('16*atan(1/5)-4*atan(1/239)', 15) == '3.14159265358979' + + +def test_cancellation(): + assert NS(Add(pi, Rational(1, 10**1000), -pi, evaluate=False), 15, + maxn=1200) == '1.00000000000000e-1000' + + +def test_evalf_powers(): + assert NS('pi**(10**20)', 10) == '1.339148777e+49714987269413385435' + assert NS(pi**(10**100), 10) == ('4.946362032e+4971498726941338543512682882' + '9089887365167832438044244613405349992494711208' + '95526746555473864642912223') + assert NS('2**(1/10**50)', 15) == '1.00000000000000' + assert NS('2**(1/10**50)-1', 15) == '6.93147180559945e-51' + +# Evaluation of Rump's ill-conditioned polynomial + + +def test_evalf_rump(): + a = 1335*y**6/4 + x**2*(11*x**2*y**2 - y**6 - 121*y**4 - 2) + 11*y**8/2 + x/(2*y) + assert NS(a, 15, subs={x: 77617, y: 33096}) == '-0.827396059946821' + + +def test_evalf_complex(): + assert NS('2*sqrt(pi)*I', 10) == '3.544907702*I' + assert NS('3+3*I', 15) == '3.00000000000000 + 3.00000000000000*I' + assert NS('E+pi*I', 15) == '2.71828182845905 + 3.14159265358979*I' + assert NS('pi * (3+4*I)', 15) == '9.42477796076938 + 12.5663706143592*I' + assert NS('I*(2+I)', 15) == '-1.00000000000000 + 2.00000000000000*I' + + +@XFAIL +def test_evalf_complex_bug(): + assert NS('(pi+E*I)*(E+pi*I)', 15) in ('0.e-15 + 17.25866050002*I', + '0.e-17 + 17.25866050002*I', '-0.e-17 + 17.25866050002*I') + + +def test_evalf_complex_powers(): + assert NS('(E+pi*I)**100000000000000000') == \ + '-3.58896782867793e+61850354284995199 + 4.58581754997159e+61850354284995199*I' + # XXX: rewrite if a+a*I simplification introduced in SymPy + #assert NS('(pi + pi*I)**2') in ('0.e-15 + 19.7392088021787*I', '0.e-16 + 19.7392088021787*I') + assert NS('(pi + pi*I)**2', chop=True) == '19.7392088021787*I' + assert NS( + '(pi + 1/10**8 + pi*I)**2') == '6.2831853e-8 + 19.7392088650106*I' + assert NS('(pi + 1/10**12 + pi*I)**2') == '6.283e-12 + 19.7392088021850*I' + assert NS('(pi + pi*I)**4', chop=True) == '-389.636364136010' + assert NS( + '(pi + 1/10**8 + pi*I)**4') == '-389.636366616512 + 2.4805021e-6*I' + assert NS('(pi + 1/10**12 + pi*I)**4') == '-389.636364136258 + 2.481e-10*I' + assert NS( + '(10000*pi + 10000*pi*I)**4', chop=True) == '-3.89636364136010e+18' + + +@XFAIL +def test_evalf_complex_powers_bug(): + assert NS('(pi + pi*I)**4') == '-389.63636413601 + 0.e-14*I' + + +def test_evalf_exponentiation(): + assert NS(sqrt(-pi)) == '1.77245385090552*I' + assert NS(Pow(pi*I, Rational( + 1, 2), evaluate=False)) == '1.25331413731550 + 1.25331413731550*I' + assert NS(pi**I) == '0.413292116101594 + 0.910598499212615*I' + assert NS(pi**(E + I/3)) == '20.8438653991931 + 8.36343473930031*I' + assert NS((pi + I/3)**(E + I/3)) == '17.2442906093590 + 13.6839376767037*I' + assert NS(exp(pi)) == '23.1406926327793' + assert NS(exp(pi + E*I)) == '-21.0981542849657 + 9.50576358282422*I' + assert NS(pi**pi) == '36.4621596072079' + assert NS((-pi)**pi) == '-32.9138577418939 - 15.6897116534332*I' + assert NS((-pi)**(-pi)) == '-0.0247567717232697 + 0.0118013091280262*I' + +# An example from Smith, "Multiple Precision Complex Arithmetic and Functions" + + +def test_evalf_complex_cancellation(): + A = Rational('63287/100000') + B = Rational('52498/100000') + C = Rational('69301/100000') + D = Rational('83542/100000') + F = Rational('2231321613/2500000000') + # XXX: the number of returned mantissa digits in the real part could + # change with the implementation. What matters is that the returned digits are + # correct; those that are showing now are correct. + # >>> ((A+B*I)*(C+D*I)).expand() + # 64471/10000000000 + 2231321613*I/2500000000 + # >>> 2231321613*4 + # 8925286452L + assert NS((A + B*I)*(C + D*I), 6) == '6.44710e-6 + 0.892529*I' + assert NS((A + B*I)*(C + D*I), 10) == '6.447100000e-6 + 0.8925286452*I' + assert NS((A + B*I)*( + C + D*I) - F*I, 5) in ('6.4471e-6 + 0.e-14*I', '6.4471e-6 - 0.e-14*I') + + +def test_evalf_logs(): + assert NS("log(3+pi*I)", 15) == '1.46877619736226 + 0.808448792630022*I' + assert NS("log(pi*I)", 15) == '1.14472988584940 + 1.57079632679490*I' + assert NS('log(-1 + 0.00001)', 2) == '-1.0e-5 + 3.1*I' + assert NS('log(100, 10, evaluate=False)', 15) == '2.00000000000000' + assert NS('-2*I*log(-(-1)**(S(1)/9))', 15) == '-5.58505360638185' + + +def test_evalf_trig(): + assert NS('sin(1)', 15) == '0.841470984807897' + assert NS('cos(1)', 15) == '0.540302305868140' + assert NS('tan(1)', 15) == '1.55740772465490' + assert NS('sin(10**-6)', 15) == '9.99999999999833e-7' + assert NS('cos(10**-6)', 15) == '0.999999999999500' + assert NS('tan(10**-6)', 15) == '1.00000000000033e-6' + assert NS('sin(E*10**100)', 15) == '0.409160531722613' + assert NS('tan(I)',15) =='0.761594155955765*I' + assert NS('tan(1000*I)',15)== '1.00000000000000*I' + # Some input near roots + assert NS(sin(exp(pi*sqrt(163))*pi), 15) == '-2.35596641936785e-12' + assert NS(sin(pi*10**100 + Rational(7, 10**5), evaluate=False), 15, maxn=120) == \ + '6.99999999428333e-5' + assert NS(sin(Rational(7, 10**5), evaluate=False), 15) == \ + '6.99999999428333e-5' + +# Check detection of various false identities + + +def test_evalf_near_integers(): + # Binet's formula + f = lambda n: ((1 + sqrt(5))**n)/(2**n * sqrt(5)) + assert NS(f(5000) - fibonacci(5000), 10, maxn=1500) == '5.156009964e-1046' + # Some near-integer identities from + # http://mathworld.wolfram.com/AlmostInteger.html + assert NS('sin(2017*2**(1/5))', 15) == '-1.00000000000000' + assert NS('sin(2017*2**(1/5))', 20) == '-0.99999999999999997857' + assert NS('1+sin(2017*2**(1/5))', 15) == '2.14322287389390e-17' + assert NS('45 - 613*E/37 + 35/991', 15) == '6.03764498766326e-11' + + +def test_evalf_ramanujan(): + assert NS(exp(pi*sqrt(163)) - 640320**3 - 744, 10) == '-7.499274028e-13' + # A related identity + A = 262537412640768744*exp(-pi*sqrt(163)) + B = 196884*exp(-2*pi*sqrt(163)) + C = 103378831900730205293632*exp(-3*pi*sqrt(163)) + assert NS(1 - A - B + C, 10) == '1.613679005e-59' + +# Input that for various reasons have failed at some point + + +def test_evalf_bugs(): + assert NS(sin(1) + exp(-10**10), 10) == NS(sin(1), 10) + assert NS(exp(10**10) + sin(1), 10) == NS(exp(10**10), 10) + assert NS('expand_log(log(1+1/10**50))', 20) == '1.0000000000000000000e-50' + assert NS('log(10**100,10)', 10) == '100.0000000' + assert NS('log(2)', 10) == '0.6931471806' + assert NS( + '(sin(x)-x)/x**3', 15, subs={x: '1/10**50'}) == '-0.166666666666667' + assert NS(sin(1) + Rational( + 1, 10**100)*I, 15) == '0.841470984807897 + 1.00000000000000e-100*I' + assert x.evalf() == x + assert NS((1 + I)**2*I, 6) == '-2.00000' + d = {n: ( + -1)**Rational(6, 7), y: (-1)**Rational(4, 7), x: (-1)**Rational(2, 7)} + assert NS((x*(1 + y*(1 + n))).subs(d).evalf(), 6) == '0.346011 + 0.433884*I' + assert NS(((-I - sqrt(2)*I)**2).evalf()) == '-5.82842712474619' + assert NS((1 + I)**2*I, 15) == '-2.00000000000000' + # issue 4758 (1/2): + assert NS(pi.evalf(69) - pi) == '-4.43863937855894e-71' + # issue 4758 (2/2): With the bug present, this still only fails if the + # terms are in the order given here. This is not generally the case, + # because the order depends on the hashes of the terms. + assert NS(20 - 5008329267844*n**25 - 477638700*n**37 - 19*n, + subs={n: .01}) == '19.8100000000000' + assert NS(((x - 1)*(1 - x)**1000).n() + ) == '(1.00000000000000 - x)**1000*(x - 1.00000000000000)' + assert NS((-x).n()) == '-x' + assert NS((-2*x).n()) == '-2.00000000000000*x' + assert NS((-2*x*y).n()) == '-2.00000000000000*x*y' + assert cos(x).n(subs={x: 1+I}) == cos(x).subs(x, 1+I).n() + # issue 6660. Also NaN != mpmath.nan + # In this order: + # 0*nan, 0/nan, 0*inf, 0/inf + # 0+nan, 0-nan, 0+inf, 0-inf + # >>> n = Some Number + # n*nan, n/nan, n*inf, n/inf + # n+nan, n-nan, n+inf, n-inf + assert (0*E**(oo)).n() is S.NaN + assert (0/E**(oo)).n() is S.Zero + + assert (0+E**(oo)).n() is S.Infinity + assert (0-E**(oo)).n() is S.NegativeInfinity + + assert (5*E**(oo)).n() is S.Infinity + assert (5/E**(oo)).n() is S.Zero + + assert (5+E**(oo)).n() is S.Infinity + assert (5-E**(oo)).n() is S.NegativeInfinity + + #issue 7416 + assert as_mpmath(0.0, 10, {'chop': True}) == 0 + + #issue 5412 + assert ((oo*I).n() == S.Infinity*I) + assert ((oo+oo*I).n() == S.Infinity + S.Infinity*I) + + #issue 11518 + assert NS(2*x**2.5, 5) == '2.0000*x**2.5000' + + #issue 13076 + assert NS(Mul(Max(0, y), x, evaluate=False).evalf()) == 'x*Max(0, y)' + + #issue 18516 + assert NS(log(S(3273390607896141870013189696827599152216642046043064789483291368096133796404674554883270092325904157150886684127560071009217256545885393053328527589376)/36360291795869936842385267079543319118023385026001623040346035832580600191583895484198508262979388783308179702534403855752855931517013066142992430916562025780021771247847643450125342836565813209972590371590152578728008385990139795377610001).evalf(15, chop=True)) == '-oo' + + +def test_evalf_integer_parts(): + a = floor(log(8)/log(2) - exp(-1000), evaluate=False) + b = floor(log(8)/log(2), evaluate=False) + assert a.evalf() == 3.0 + assert b.evalf() == 3.0 + # equals, as a fallback, can still fail but it might succeed as here + assert ceiling(10*(sin(1)**2 + cos(1)**2)) == 10 + + assert int(floor(factorial(50)/E, evaluate=False).evalf(70)) == \ + int(11188719610782480504630258070757734324011354208865721592720336800) + assert int(ceiling(factorial(50)/E, evaluate=False).evalf(70)) == \ + int(11188719610782480504630258070757734324011354208865721592720336801) + assert int(floor(GoldenRatio**999 / sqrt(5) + S.Half) + .evalf(1000)) == fibonacci(999) + assert int(floor(GoldenRatio**1000 / sqrt(5) + S.Half) + .evalf(1000)) == fibonacci(1000) + + assert ceiling(x).evalf(subs={x: 3}) == 3.0 + assert ceiling(x).evalf(subs={x: 3*I}) == 3.0*I + assert ceiling(x).evalf(subs={x: 2 + 3*I}) == 2.0 + 3.0*I + assert ceiling(x).evalf(subs={x: 3.}) == 3.0 + assert ceiling(x).evalf(subs={x: 3.*I}) == 3.0*I + assert ceiling(x).evalf(subs={x: 2. + 3*I}) == 2.0 + 3.0*I + + assert float((floor(1.5, evaluate=False)+1/9).evalf()) == 1 + 1/9 + assert float((floor(0.5, evaluate=False)+20).evalf()) == 20 + + # issue 19991 + n = 1169809367327212570704813632106852886389036911 + r = 744723773141314414542111064094745678855643068 + + assert floor(n / (pi / 2)) == r + assert floor(80782 * sqrt(2)) == 114242 + + # issue 20076 + assert 260515 - floor(260515/pi + 1/2) * pi == atan(tan(260515)) + + assert floor(x).evalf(subs={x: sqrt(2)}) == 1.0 + + +def test_evalf_trig_zero_detection(): + a = sin(160*pi, evaluate=False) + t = a.evalf(maxn=100) + assert abs(t) < 1e-100 + assert t._prec < 2 + assert a.evalf(chop=True) == 0 + raises(PrecisionExhausted, lambda: a.evalf(strict=True)) + + +def test_evalf_sum(): + assert Sum(n,(n,1,2)).evalf() == 3. + assert Sum(n,(n,1,2)).doit().evalf() == 3. + # the next test should return instantly + assert Sum(1/n,(n,1,2)).evalf() == 1.5 + + # issue 8219 + assert Sum(E/factorial(n), (n, 0, oo)).evalf() == (E*E).evalf() + # issue 8254 + assert Sum(2**n*n/factorial(n), (n, 0, oo)).evalf() == (2*E*E).evalf() + # issue 8411 + s = Sum(1/x**2, (x, 100, oo)) + assert s.n() == s.doit().n() + + +def test_evalf_divergent_series(): + raises(ValueError, lambda: Sum(1/n, (n, 1, oo)).evalf()) + raises(ValueError, lambda: Sum(n/(n**2 + 1), (n, 1, oo)).evalf()) + raises(ValueError, lambda: Sum((-1)**n, (n, 1, oo)).evalf()) + raises(ValueError, lambda: Sum((-1)**n, (n, 1, oo)).evalf()) + raises(ValueError, lambda: Sum(n**2, (n, 1, oo)).evalf()) + raises(ValueError, lambda: Sum(2**n, (n, 1, oo)).evalf()) + raises(ValueError, lambda: Sum((-2)**n, (n, 1, oo)).evalf()) + raises(ValueError, lambda: Sum((2*n + 3)/(3*n**2 + 4), (n, 0, oo)).evalf()) + raises(ValueError, lambda: Sum((0.5*n**3)/(n**4 + 1), (n, 0, oo)).evalf()) + + +def test_evalf_product(): + assert Product(n, (n, 1, 10)).evalf() == 3628800. + assert comp(Product(1 - S.Half**2/n**2, (n, 1, oo)).n(5), 0.63662) + assert Product(n, (n, -1, 3)).evalf() == 0 + + +def test_evalf_py_methods(): + assert abs(float(pi + 1) - 4.1415926535897932) < 1e-10 + assert abs(complex(pi + 1) - 4.1415926535897932) < 1e-10 + assert abs( + complex(pi + E*I) - (3.1415926535897931 + 2.7182818284590451j)) < 1e-10 + raises(TypeError, lambda: float(pi + x)) + + +def test_evalf_power_subs_bugs(): + assert (x**2).evalf(subs={x: 0}) == 0 + assert sqrt(x).evalf(subs={x: 0}) == 0 + assert (x**Rational(2, 3)).evalf(subs={x: 0}) == 0 + assert (x**x).evalf(subs={x: 0}) == 1.0 + assert (3**x).evalf(subs={x: 0}) == 1.0 + assert exp(x).evalf(subs={x: 0}) == 1.0 + assert ((2 + I)**x).evalf(subs={x: 0}) == 1.0 + assert (0**x).evalf(subs={x: 0}) == 1.0 + + +def test_evalf_arguments(): + raises(TypeError, lambda: pi.evalf(method="garbage")) + + +def test_implemented_function_evalf(): + from sympy.utilities.lambdify import implemented_function + f = Function('f') + f = implemented_function(f, lambda x: x + 1) + assert str(f(x)) == "f(x)" + assert str(f(2)) == "f(2)" + assert f(2).evalf() == 3.0 + assert f(x).evalf() == f(x) + f = implemented_function(Function('sin'), lambda x: x + 1) + assert f(2).evalf() != sin(2) + del f._imp_ # XXX: due to caching _imp_ would influence all other tests + + +def test_evaluate_false(): + for no in [0, False]: + assert Add(3, 2, evaluate=no).is_Add + assert Mul(3, 2, evaluate=no).is_Mul + assert Pow(3, 2, evaluate=no).is_Pow + assert Pow(y, 2, evaluate=True) - Pow(y, 2, evaluate=True) == 0 + + +def test_evalf_relational(): + assert Eq(x/5, y/10).evalf() == Eq(0.2*x, 0.1*y) + # if this first assertion fails it should be replaced with + # one that doesn't + assert unchanged(Eq, (3 - I)**2/2 + I, 0) + assert Eq((3 - I)**2/2 + I, 0).n() is S.false + assert nfloat(Eq((3 - I)**2 + I, 0)) == S.false + + +def test_issue_5486(): + assert not cos(sqrt(0.5 + I)).n().is_Function + + +def test_issue_5486_bug(): + from sympy.core.expr import Expr + from sympy.core.numbers import I + assert abs(Expr._from_mpmath(I._to_mpmath(15), 15) - I) < 1.0e-15 + + +def test_bugs(): + from sympy.functions.elementary.complexes import (polar_lift, re) + + assert abs(re((1 + I)**2)) < 1e-15 + + # anything that evalf's to 0 will do in place of polar_lift + assert abs(polar_lift(0)).n() == 0 + + +def test_subs(): + assert NS('besseli(-x, y) - besseli(x, y)', subs={x: 3.5, y: 20.0}) == \ + '-4.92535585957223e-10' + assert NS('Piecewise((x, x>0)) + Piecewise((1-x, x>0))', subs={x: 0.1}) == \ + '1.00000000000000' + raises(TypeError, lambda: x.evalf(subs=(x, 1))) + + +def test_issue_4956_5204(): + # issue 4956 + v = S('''(-27*12**(1/3)*sqrt(31)*I + + 27*2**(2/3)*3**(1/3)*sqrt(31)*I)/(-2511*2**(2/3)*3**(1/3) + + (29*18**(1/3) + 9*2**(1/3)*3**(2/3)*sqrt(31)*I + + 87*2**(1/3)*3**(1/6)*I)**2)''') + assert NS(v, 1) == '0.e-118 - 0.e-118*I' + + # issue 5204 + v = S('''-(357587765856 + 18873261792*249**(1/2) + 56619785376*I*83**(1/2) + + 108755765856*I*3**(1/2) + 41281887168*6**(1/3)*(1422 + + 54*249**(1/2))**(1/3) - 1239810624*6**(1/3)*249**(1/2)*(1422 + + 54*249**(1/2))**(1/3) - 3110400000*I*6**(1/3)*83**(1/2)*(1422 + + 54*249**(1/2))**(1/3) + 13478400000*I*3**(1/2)*6**(1/3)*(1422 + + 54*249**(1/2))**(1/3) + 1274950152*6**(2/3)*(1422 + + 54*249**(1/2))**(2/3) + 32347944*6**(2/3)*249**(1/2)*(1422 + + 54*249**(1/2))**(2/3) - 1758790152*I*3**(1/2)*6**(2/3)*(1422 + + 54*249**(1/2))**(2/3) - 304403832*I*6**(2/3)*83**(1/2)*(1422 + + 4*249**(1/2))**(2/3))/(175732658352 + (1106028 + 25596*249**(1/2) + + 76788*I*83**(1/2))**2)''') + assert NS(v, 5) == '0.077284 + 1.1104*I' + assert NS(v, 1) == '0.08 + 1.*I' + + +def test_old_docstring(): + a = (E + pi*I)*(E - pi*I) + assert NS(a) == '17.2586605000200' + assert a.n() == 17.25866050002001 + + +def test_issue_4806(): + assert integrate(atan(x)**2, (x, -1, 1)).evalf().round(1) == Float(0.5, 1) + assert atan(0, evaluate=False).n() == 0 + + +def test_evalf_mul(): + # SymPy should not try to expand this; it should be handled term-wise + # in evalf through mpmath + assert NS(product(1 + sqrt(n)*I, (n, 1, 500)), 1) == '5.e+567 + 2.e+568*I' + + +def test_scaled_zero(): + a, b = (([0], 1, 100, 1), -1) + assert scaled_zero(100) == (a, b) + assert scaled_zero(a) == (0, 1, 100, 1) + a, b = (([1], 1, 100, 1), -1) + assert scaled_zero(100, -1) == (a, b) + assert scaled_zero(a) == (1, 1, 100, 1) + raises(ValueError, lambda: scaled_zero(scaled_zero(100))) + raises(ValueError, lambda: scaled_zero(100, 2)) + raises(ValueError, lambda: scaled_zero(100, 0)) + raises(ValueError, lambda: scaled_zero((1, 5, 1, 3))) + + +def test_chop_value(): + for i in range(-27, 28): + assert (Pow(10, i)*2).n(chop=10**i) and not (Pow(10, i)).n(chop=10**i) + + +def test_infinities(): + assert oo.evalf(chop=True) == inf + assert (-oo).evalf(chop=True) == ninf + + +def test_to_mpmath(): + assert sqrt(3)._to_mpmath(20)._mpf_ == (0, int(908093), -19, 20) + assert S(3.2)._to_mpmath(20)._mpf_ == (0, int(838861), -18, 20) + + +def test_issue_6632_evalf(): + add = (-100000*sqrt(2500000001) + 5000000001) + assert add.n() == 9.999999998e-11 + assert (add*add).n() == 9.999999996e-21 + + +def test_issue_4945(): + from sympy.abc import H + assert (H/0).evalf(subs={H:1}) == zoo + + +def test_evalf_integral(): + # test that workprec has to increase in order to get a result other than 0 + eps = Rational(1, 1000000) + assert Integral(sin(x), (x, -pi, pi + eps)).n(2)._prec == 10 + + +def test_issue_8821_highprec_from_str(): + s = str(pi.evalf(128)) + p = N(s) + assert Abs(sin(p)) < 1e-15 + p = N(s, 64) + assert Abs(sin(p)) < 1e-64 + + +def test_issue_8853(): + p = Symbol('x', even=True, positive=True) + assert floor(-p - S.Half).is_even == False + assert floor(-p + S.Half).is_even == True + assert ceiling(p - S.Half).is_even == True + assert ceiling(p + S.Half).is_even == False + + assert get_integer_part(S.Half, -1, {}, True) == (0, 0) + assert get_integer_part(S.Half, 1, {}, True) == (1, 0) + assert get_integer_part(Rational(-1, 2), -1, {}, True) == (-1, 0) + assert get_integer_part(Rational(-1, 2), 1, {}, True) == (0, 0) + + +def test_issue_17681(): + class identity_func(Function): + + def _eval_evalf(self, *args, **kwargs): + return self.args[0].evalf(*args, **kwargs) + + assert floor(identity_func(S(0))) == 0 + assert get_integer_part(S(0), 1, {}, True) == (0, 0) + + +def test_issue_9326(): + from sympy.core.symbol import Dummy + d1 = Dummy('d') + d2 = Dummy('d') + e = d1 + d2 + assert e.evalf(subs = {d1: 1, d2: 2}) == 3.0 + + +def test_issue_10323(): + assert ceiling(sqrt(2**30 + 1)) == 2**15 + 1 + + +def test_AssocOp_Function(): + # the first arg of Min is not comparable in the imaginary part + raises(ValueError, lambda: S(''' + Min(-sqrt(3)*cos(pi/18)/6 + re(1/((-1/2 - sqrt(3)*I/2)*(1/6 + + sqrt(3)*I/18)**(1/3)))/3 + sin(pi/18)/2 + 2 + I*(-cos(pi/18)/2 - + sqrt(3)*sin(pi/18)/6 + im(1/((-1/2 - sqrt(3)*I/2)*(1/6 + + sqrt(3)*I/18)**(1/3)))/3), re(1/((-1/2 + sqrt(3)*I/2)*(1/6 + + sqrt(3)*I/18)**(1/3)))/3 - sqrt(3)*cos(pi/18)/6 - sin(pi/18)/2 + 2 + + I*(im(1/((-1/2 + sqrt(3)*I/2)*(1/6 + sqrt(3)*I/18)**(1/3)))/3 - + sqrt(3)*sin(pi/18)/6 + cos(pi/18)/2))''')) + # if that is changed so a non-comparable number remains as + # an arg, then the Min/Max instantiation needs to be changed + # to watch out for non-comparable args when making simplifications + # and the following test should be added instead (with e being + # the sympified expression above): + # raises(ValueError, lambda: e._eval_evalf(2)) + + +def test_issue_10395(): + eq = x*Max(0, y) + assert nfloat(eq) == eq + eq = x*Max(y, -1.1) + assert nfloat(eq) == eq + assert Max(y, 4).n() == Max(4.0, y) + + +def test_issue_13098(): + assert floor(log(S('9.'+'9'*20), 10)) == 0 + assert ceiling(log(S('9.'+'9'*20), 10)) == 1 + assert floor(log(20 - S('9.'+'9'*20), 10)) == 1 + assert ceiling(log(20 - S('9.'+'9'*20), 10)) == 2 + + +def test_issue_14601(): + e = 5*x*y/2 - y*(35*(x**3)/2 - 15*x/2) + subst = {x:0.0, y:0.0} + e2 = e.evalf(subs=subst) + assert float(e2) == 0.0 + assert float((x + x*(x**2 + x)).evalf(subs={x: 0.0})) == 0.0 + + +def test_issue_11151(): + z = S.Zero + e = Sum(z, (x, 1, 2)) + assert e != z # it shouldn't evaluate + # when it does evaluate, this is what it should give + assert evalf(e, 15, {}) == \ + evalf(z, 15, {}) == (None, None, 15, None) + # so this shouldn't fail + assert (e/2).n() == 0 + # this was where the issue appeared + expr0 = Sum(x**2 + x, (x, 1, 2)) + expr1 = Sum(0, (x, 1, 2)) + expr2 = expr1/expr0 + assert simplify(factor(expr2) - expr2) == 0 + + +def test_issue_13425(): + assert N('2**.5', 30) == N('sqrt(2)', 30) + assert N('x - x', 30) == 0 + assert abs((N('pi*.1', 22)*10 - pi).n()) < 1e-22 + + +def test_issue_17421(): + assert N(acos(-I + acosh(cosh(cosh(1) + I)))) == 1.0*I + + +def test_issue_20291(): + from sympy.sets import EmptySet, Reals + from sympy.sets.sets import (Complement, FiniteSet, Intersection) + a = Symbol('a') + b = Symbol('b') + A = FiniteSet(a, b) + assert A.evalf(subs={a: 1, b: 2}) == FiniteSet(1.0, 2.0) + B = FiniteSet(a-b, 1) + assert B.evalf(subs={a: 1, b: 2}) == FiniteSet(-1.0, 1.0) + + sol = Complement(Intersection(FiniteSet(-b/2 - sqrt(b**2-4*pi)/2), Reals), FiniteSet(0)) + assert sol.evalf(subs={b: 1}) == EmptySet + + +def test_evalf_with_zoo(): + assert (1/x).evalf(subs={x: 0}) == zoo # issue 8242 + assert (-1/x).evalf(subs={x: 0}) == zoo # PR 16150 + assert (0 ** x).evalf(subs={x: -1}) == zoo # PR 16150 + assert (0 ** x).evalf(subs={x: -1 + I}) == nan + assert Mul(2, Pow(0, -1, evaluate=False), evaluate=False).evalf() == zoo # issue 21147 + assert Mul(x, 1/x, evaluate=False).evalf(subs={x: 0}) == Mul(x, 1/x, evaluate=False).subs(x, 0) == nan + assert Mul(1/x, 1/x, evaluate=False).evalf(subs={x: 0}) == zoo + assert Mul(1/x, Abs(1/x), evaluate=False).evalf(subs={x: 0}) == zoo + assert Abs(zoo, evaluate=False).evalf() == oo + assert re(zoo, evaluate=False).evalf() == nan + assert im(zoo, evaluate=False).evalf() == nan + assert Add(zoo, zoo, evaluate=False).evalf() == nan + assert Add(oo, zoo, evaluate=False).evalf() == nan + assert Pow(zoo, -1, evaluate=False).evalf() == 0 + assert Pow(zoo, Rational(-1, 3), evaluate=False).evalf() == 0 + assert Pow(zoo, Rational(1, 3), evaluate=False).evalf() == zoo + assert Pow(zoo, S.Half, evaluate=False).evalf() == zoo + assert Pow(zoo, 2, evaluate=False).evalf() == zoo + assert Pow(0, zoo, evaluate=False).evalf() == nan + assert log(zoo, evaluate=False).evalf() == zoo + assert zoo.evalf(chop=True) == zoo + assert x.evalf(subs={x: zoo}) == zoo + + +def test_evalf_with_bounded_error(): + cases = [ + # zero + (Rational(0), None, 1), + # zero im part + (pi, None, 10), + # zero real part + (pi*I, None, 10), + # re and im nonzero + (2-3*I, None, 5), + # similar tests again, but using eps instead of m + (Rational(0), Rational(1, 2), None), + (pi, Rational(1, 1000), None), + (pi * I, Rational(1, 1000), None), + (2 - 3 * I, Rational(1, 1000), None), + # very large eps + (2 - 3 * I, Rational(1000), None), + # case where x already small, hence some cancellation in p = m + n - 1 + (Rational(1234, 10**8), Rational(1, 10**12), None), + ] + for x0, eps, m in cases: + a, b, _, _ = evalf(x0, 53, {}) + c, d, _, _ = _evalf_with_bounded_error(x0, eps, m) + if eps is None: + eps = 2**(-m) + z = make_mpc((a or fzero, b or fzero)) + w = make_mpc((c or fzero, d or fzero)) + assert abs(w - z) < eps + + # eps must be positive + raises(ValueError, lambda: _evalf_with_bounded_error(pi, Rational(0))) + raises(ValueError, lambda: _evalf_with_bounded_error(pi, -pi)) + raises(ValueError, lambda: _evalf_with_bounded_error(pi, I)) + + +def test_issue_22849(): + a = -8 + 3 * sqrt(3) + x = AlgebraicNumber(a) + assert evalf(a, 1, {}) == evalf(x, 1, {}) + + +def test_evalf_real_alg_num(): + # This test demonstrates why the entry for `AlgebraicNumber` in + # `sympy.core.evalf._create_evalf_table()` has to use `x.to_root()`, + # instead of `x.as_expr()`. If the latter is used, then `z` will be + # a complex number with `0.e-20` for imaginary part, even though `a5` + # is a real number. + zeta = Symbol('zeta') + a5 = AlgebraicNumber(CRootOf(cyclotomic_poly(5), -1), [-1, -1, 0, 0], alias=zeta) + z = a5.evalf() + assert isinstance(z, Float) + assert not hasattr(z, '_mpc_') + assert hasattr(z, '_mpf_') + + +def test_issue_20733(): + expr = 1/((x - 9)*(x - 8)*(x - 7)*(x - 4)**2*(x - 3)**3*(x - 2)) + assert str(expr.evalf(1, subs={x:1})) == '-4.e-5' + assert str(expr.evalf(2, subs={x:1})) == '-4.1e-5' + assert str(expr.evalf(11, subs={x:1})) == '-4.1335978836e-5' + assert str(expr.evalf(20, subs={x:1})) == '-0.000041335978835978835979' + + expr = Mul(*((x - i) for i in range(2, 1000))) + assert srepr(expr.evalf(2, subs={x: 1})) == "Float('4.0271e+2561', precision=10)" + assert srepr(expr.evalf(10, subs={x: 1})) == "Float('4.02790050126e+2561', precision=37)" + assert srepr(expr.evalf(53, subs={x: 1})) == "Float('4.0279005012722099453824067459760158730668154575647110393e+2561', precision=179)" diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_expand.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_expand.py new file mode 100644 index 0000000000000000000000000000000000000000..e7abb5daacebbe81664b3de3a7ac35a490ab31bc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_expand.py @@ -0,0 +1,364 @@ +from sympy.core.expr import unchanged +from sympy.core.mul import Mul +from sympy.core.numbers import (I, Rational as R, pi) +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.series.order import O +from sympy.simplify.radsimp import expand_numer +from sympy.core.function import (expand, expand_multinomial, + expand_power_base, expand_log) + +from sympy.testing.pytest import raises +from sympy.core.random import verify_numerically + +from sympy.abc import x, y, z + + +def test_expand_no_log(): + assert ( + (1 + log(x**4))**2).expand(log=False) == 1 + 2*log(x**4) + log(x**4)**2 + assert ((1 + log(x**4))*(1 + log(x**3))).expand( + log=False) == 1 + log(x**4) + log(x**3) + log(x**4)*log(x**3) + + +def test_expand_no_multinomial(): + assert ((1 + x)*(1 + (1 + x)**4)).expand(multinomial=False) == \ + 1 + x + (1 + x)**4 + x*(1 + x)**4 + + +def test_expand_negative_integer_powers(): + expr = (x + y)**(-2) + assert expr.expand() == 1 / (2*x*y + x**2 + y**2) + assert expr.expand(multinomial=False) == (x + y)**(-2) + expr = (x + y)**(-3) + assert expr.expand() == 1 / (3*x*x*y + 3*x*y*y + x**3 + y**3) + assert expr.expand(multinomial=False) == (x + y)**(-3) + expr = (x + y)**(2) * (x + y)**(-4) + assert expr.expand() == 1 / (2*x*y + x**2 + y**2) + assert expr.expand(multinomial=False) == (x + y)**(-2) + + +def test_expand_non_commutative(): + A = Symbol('A', commutative=False) + B = Symbol('B', commutative=False) + C = Symbol('C', commutative=False) + a = Symbol('a') + b = Symbol('b') + i = Symbol('i', integer=True) + n = Symbol('n', negative=True) + m = Symbol('m', negative=True) + p = Symbol('p', polar=True) + np = Symbol('p', polar=False) + + assert (C*(A + B)).expand() == C*A + C*B + assert (C*(A + B)).expand() != A*C + B*C + assert ((A + B)**2).expand() == A**2 + A*B + B*A + B**2 + assert ((A + B)**3).expand() == (A**2*B + B**2*A + A*B**2 + B*A**2 + + A**3 + B**3 + A*B*A + B*A*B) + # issue 6219 + assert ((a*A*B*A**-1)**2).expand() == a**2*A*B**2/A + # Note that (a*A*B*A**-1)**2 is automatically converted to a**2*(A*B*A**-1)**2 + assert ((a*A*B*A**-1)**2).expand(deep=False) == a**2*(A*B*A**-1)**2 + assert ((a*A*B*A**-1)**2).expand() == a**2*(A*B**2*A**-1) + assert ((a*A*B*A**-1)**2).expand(force=True) == a**2*A*B**2*A**(-1) + assert ((a*A*B)**2).expand() == a**2*A*B*A*B + assert ((a*A)**2).expand() == a**2*A**2 + assert ((a*A*B)**i).expand() == a**i*(A*B)**i + assert ((a*A*(B*(A*B/A)**2))**i).expand() == a**i*(A*B*A*B**2/A)**i + # issue 6558 + assert (A*B*(A*B)**-1).expand() == 1 + assert ((a*A)**i).expand() == a**i*A**i + assert ((a*A*B*A**-1)**3).expand() == a**3*A*B**3/A + assert ((a*A*B*A*B/A)**3).expand() == \ + a**3*A*B*(A*B**2)*(A*B**2)*A*B*A**(-1) + assert ((a*A*B*A*B/A)**-2).expand() == \ + A*B**-1*A**-1*B**-2*A**-1*B**-1*A**-1/a**2 + assert ((a*b*A*B*A**-1)**i).expand() == a**i*b**i*(A*B/A)**i + assert ((a*(a*b)**i)**i).expand() == a**i*a**(i**2)*b**(i**2) + e = Pow(Mul(a, 1/a, A, B, evaluate=False), S(2), evaluate=False) + assert e.expand() == A*B*A*B + assert sqrt(a*(A*b)**i).expand() == sqrt(a*b**i*A**i) + assert (sqrt(-a)**a).expand() == sqrt(-a)**a + assert expand((-2*n)**(i/3)) == 2**(i/3)*(-n)**(i/3) + assert expand((-2*n*m)**(i/a)) == (-2)**(i/a)*(-n)**(i/a)*(-m)**(i/a) + assert expand((-2*a*p)**b) == 2**b*p**b*(-a)**b + assert expand((-2*a*np)**b) == 2**b*(-a*np)**b + assert expand(sqrt(A*B)) == sqrt(A*B) + assert expand(sqrt(-2*a*b)) == sqrt(2)*sqrt(-a*b) + + +def test_expand_radicals(): + a = (x + y)**R(1, 2) + + assert (a**1).expand() == a + assert (a**3).expand() == x*a + y*a + assert (a**5).expand() == x**2*a + 2*x*y*a + y**2*a + + assert (1/a**1).expand() == 1/a + assert (1/a**3).expand() == 1/(x*a + y*a) + assert (1/a**5).expand() == 1/(x**2*a + 2*x*y*a + y**2*a) + + a = (x + y)**R(1, 3) + + assert (a**1).expand() == a + assert (a**2).expand() == a**2 + assert (a**4).expand() == x*a + y*a + assert (a**5).expand() == x*a**2 + y*a**2 + assert (a**7).expand() == x**2*a + 2*x*y*a + y**2*a + + +def test_expand_modulus(): + assert ((x + y)**11).expand(modulus=11) == x**11 + y**11 + assert ((x + sqrt(2)*y)**11).expand(modulus=11) == x**11 + 10*sqrt(2)*y**11 + assert (x + y/2).expand(modulus=1) == y/2 + + raises(ValueError, lambda: ((x + y)**11).expand(modulus=0)) + raises(ValueError, lambda: ((x + y)**11).expand(modulus=x)) + + +def test_issue_5743(): + assert (x*sqrt( + x + y)*(1 + sqrt(x + y))).expand() == x**2 + x*y + x*sqrt(x + y) + assert (x*sqrt( + x + y)*(1 + x*sqrt(x + y))).expand() == x**3 + x**2*y + x*sqrt(x + y) + + +def test_expand_frac(): + assert expand((x + y)*y/x/(x + 1), frac=True) == \ + (x*y + y**2)/(x**2 + x) + assert expand((x + y)*y/x/(x + 1), numer=True) == \ + (x*y + y**2)/(x*(x + 1)) + assert expand((x + y)*y/x/(x + 1), denom=True) == \ + y*(x + y)/(x**2 + x) + eq = (x + 1)**2/y + assert expand_numer(eq, multinomial=False) == eq + # issue 26329 + eq = (exp(x*z) - exp(y*z))/exp(z*(x + y)) + ans = exp(-y*z) - exp(-x*z) + assert eq.expand(numer=True) != ans + assert eq.expand(numer=True, exact=True) == ans + assert expand_numer(eq) != ans + assert expand_numer(eq, exact=True) == ans + + +def test_issue_6121(): + eq = -I*exp(-3*I*pi/4)/(4*pi**(S(3)/2)*sqrt(x)) + assert eq.expand(complex=True) # does not give oo recursion + eq = -I*exp(-3*I*pi/4)/(4*pi**(R(3, 2))*sqrt(x)) + assert eq.expand(complex=True) # does not give oo recursion + + +def test_expand_power_base(): + assert expand_power_base((x*y*z)**4) == x**4*y**4*z**4 + assert expand_power_base((x*y*z)**x).is_Pow + assert expand_power_base((x*y*z)**x, force=True) == x**x*y**x*z**x + assert expand_power_base((x*(y*z)**2)**3) == x**3*y**6*z**6 + + assert expand_power_base((sin((x*y)**2)*y)**z).is_Pow + assert expand_power_base( + (sin((x*y)**2)*y)**z, force=True) == sin((x*y)**2)**z*y**z + assert expand_power_base( + (sin((x*y)**2)*y)**z, deep=True) == (sin(x**2*y**2)*y)**z + + assert expand_power_base(exp(x)**2) == exp(2*x) + assert expand_power_base((exp(x)*exp(y))**2) == exp(2*x)*exp(2*y) + + assert expand_power_base( + (exp((x*y)**z)*exp(y))**2) == exp(2*(x*y)**z)*exp(2*y) + assert expand_power_base((exp((x*y)**z)*exp( + y))**2, deep=True, force=True) == exp(2*x**z*y**z)*exp(2*y) + + assert expand_power_base((exp(x)*exp(y))**z).is_Pow + assert expand_power_base( + (exp(x)*exp(y))**z, force=True) == exp(x)**z*exp(y)**z + + +def test_expand_arit(): + a = Symbol("a") + b = Symbol("b", positive=True) + c = Symbol("c") + + p = R(5) + e = (a + b)*c + assert e == c*(a + b) + assert (e.expand() - a*c - b*c) == R(0) + e = (a + b)*(a + b) + assert e == (a + b)**2 + assert e.expand() == 2*a*b + a**2 + b**2 + e = (a + b)*(a + b)**R(2) + assert e == (a + b)**3 + assert e.expand() == 3*b*a**2 + 3*a*b**2 + a**3 + b**3 + assert e.expand() == 3*b*a**2 + 3*a*b**2 + a**3 + b**3 + e = (a + b)*(a + c)*(b + c) + assert e == (a + c)*(a + b)*(b + c) + assert e.expand() == 2*a*b*c + b*a**2 + c*a**2 + b*c**2 + a*c**2 + c*b**2 + a*b**2 + e = (a + R(1))**p + assert e == (1 + a)**5 + assert e.expand() == 1 + 5*a + 10*a**2 + 10*a**3 + 5*a**4 + a**5 + e = (a + b + c)*(a + c + p) + assert e == (5 + a + c)*(a + b + c) + assert e.expand() == 5*a + 5*b + 5*c + 2*a*c + b*c + a*b + a**2 + c**2 + x = Symbol("x") + s = exp(x*x) - 1 + e = s.nseries(x, 0, 6)/x**2 + assert e.expand() == 1 + x**2/2 + O(x**4) + + e = (x*(y + z))**(x*(y + z))*(x + y) + assert e.expand(power_exp=False, power_base=False) == x*(x*y + x* + z)**(x*y + x*z) + y*(x*y + x*z)**(x*y + x*z) + assert e.expand(power_exp=False, power_base=False, deep=False) == x* \ + (x*(y + z))**(x*(y + z)) + y*(x*(y + z))**(x*(y + z)) + e = x * (x + (y + 1)**2) + assert e.expand(deep=False) == x**2 + x*(y + 1)**2 + e = (x*(y + z))**z + assert e.expand(power_base=True, mul=True, deep=True) in [x**z*(y + + z)**z, (x*y + x*z)**z] + assert ((2*y)**z).expand() == 2**z*y**z + p = Symbol('p', positive=True) + assert sqrt(-x).expand().is_Pow + assert sqrt(-x).expand(force=True) == I*sqrt(x) + assert ((2*y*p)**z).expand() == 2**z*p**z*y**z + assert ((2*y*p*x)**z).expand() == 2**z*p**z*(x*y)**z + assert ((2*y*p*x)**z).expand(force=True) == 2**z*p**z*x**z*y**z + assert ((2*y*p*-pi)**z).expand() == 2**z*pi**z*p**z*(-y)**z + assert ((2*y*p*-pi*x)**z).expand() == 2**z*pi**z*p**z*(-x*y)**z + n = Symbol('n', negative=True) + m = Symbol('m', negative=True) + assert ((-2*x*y*n)**z).expand() == 2**z*(-n)**z*(x*y)**z + assert ((-2*x*y*n*m)**z).expand() == 2**z*(-m)**z*(-n)**z*(-x*y)**z + # issue 5482 + assert sqrt(-2*x*n) == sqrt(2)*sqrt(-n)*sqrt(x) + # issue 5605 (2) + assert (cos(x + y)**2).expand(trig=True) in [ + (-sin(x)*sin(y) + cos(x)*cos(y))**2, + sin(x)**2*sin(y)**2 - 2*sin(x)*sin(y)*cos(x)*cos(y) + cos(x)**2*cos(y)**2 + ] + + # Check that this isn't too slow + x = Symbol('x') + W = 1 + for i in range(1, 21): + W = W * (x - i) + W = W.expand() + assert W.has(-1672280820*x**15) + +def test_expand_mul(): + # part of issue 20597 + e = Mul(2, 3, evaluate=False) + assert e.expand() == 6 + + e = Mul(2, 3, 1/x, evaluate=False) + assert e.expand() == 6/x + e = Mul(2, R(1, 3), evaluate=False) + assert e.expand() == R(2, 3) + +def test_power_expand(): + """Test for Pow.expand()""" + a = Symbol('a') + b = Symbol('b') + p = (a + b)**2 + assert p.expand() == a**2 + b**2 + 2*a*b + + p = (1 + 2*(1 + a))**2 + assert p.expand() == 9 + 4*(a**2) + 12*a + + p = 2**(a + b) + assert p.expand() == 2**a*2**b + + A = Symbol('A', commutative=False) + B = Symbol('B', commutative=False) + assert (2**(A + B)).expand() == 2**(A + B) + assert (A**(a + b)).expand() != A**(a + b) + + +def test_issues_5919_6830(): + # issue 5919 + n = -1 + 1/x + z = n/x/(-n)**2 - 1/n/x + assert expand(z) == 1/(x**2 - 2*x + 1) - 1/(x - 2 + 1/x) - 1/(-x + 1) + + # issue 6830 + p = (1 + x)**2 + assert expand_multinomial((1 + x*p)**2) == ( + x**2*(x**4 + 4*x**3 + 6*x**2 + 4*x + 1) + 2*x*(x**2 + 2*x + 1) + 1) + assert expand_multinomial((1 + (y + x)*p)**2) == ( + 2*((x + y)*(x**2 + 2*x + 1)) + (x**2 + 2*x*y + y**2)* + (x**4 + 4*x**3 + 6*x**2 + 4*x + 1) + 1) + A = Symbol('A', commutative=False) + p = (1 + A)**2 + assert expand_multinomial((1 + x*p)**2) == ( + x**2*(1 + 4*A + 6*A**2 + 4*A**3 + A**4) + 2*x*(1 + 2*A + A**2) + 1) + assert expand_multinomial((1 + (y + x)*p)**2) == ( + (x + y)*(1 + 2*A + A**2)*2 + (x**2 + 2*x*y + y**2)* + (1 + 4*A + 6*A**2 + 4*A**3 + A**4) + 1) + assert expand_multinomial((1 + (y + x)*p)**3) == ( + (x + y)*(1 + 2*A + A**2)*3 + (x**2 + 2*x*y + y**2)*(1 + 4*A + + 6*A**2 + 4*A**3 + A**4)*3 + (x**3 + 3*x**2*y + 3*x*y**2 + y**3)*(1 + 6*A + + 15*A**2 + 20*A**3 + 15*A**4 + 6*A**5 + A**6) + 1) + # unevaluate powers + eq = (Pow((x + 1)*((A + 1)**2), 2, evaluate=False)) + # - in this case the base is not an Add so no further + # expansion is done + assert expand_multinomial(eq) == \ + (x**2 + 2*x + 1)*(1 + 4*A + 6*A**2 + 4*A**3 + A**4) + # - but here, the expanded base *is* an Add so it gets expanded + eq = (Pow(((A + 1)**2), 2, evaluate=False)) + assert expand_multinomial(eq) == 1 + 4*A + 6*A**2 + 4*A**3 + A**4 + + # coverage + def ok(a, b, n): + e = (a + I*b)**n + return verify_numerically(e, expand_multinomial(e)) + + for a in [2, S.Half]: + for b in [3, R(1, 3)]: + for n in range(2, 6): + assert ok(a, b, n) + + assert expand_multinomial((x + 1 + O(z))**2) == \ + 1 + 2*x + x**2 + O(z) + assert expand_multinomial((x + 1 + O(z))**3) == \ + 1 + 3*x + 3*x**2 + x**3 + O(z) + + assert expand_multinomial(3**(x + y + 3)) == 27*3**(x + y) + +def test_expand_log(): + t = Symbol('t', positive=True) + # after first expansion, -2*log(2) + log(4); then 0 after second + assert expand(log(t**2) - log(t**2/4) - 2*log(2)) == 0 + assert expand_log(log(7*6)/log(6)) == 1 + log(7)/log(6) + b = factorial(10) + assert expand_log(log(7*b**4)/log(b) + ) == 4 + log(7)/log(b) + + +def test_issue_23952(): + assert (x**(y + z)).expand(force=True) == x**y*x**z + one = Symbol('1', integer=True, prime=True, odd=True, positive=True) + two = Symbol('2', integer=True, prime=True, even=True) + e = two - one + for b in (0, x): + # 0**e = 0, 0**-e = zoo; but if expanded then nan + assert unchanged(Pow, b, e) # power_exp + assert unchanged(Pow, b, -e) # power_exp + assert unchanged(Pow, b, y - x) # power_exp + assert unchanged(Pow, b, 3 - x) # multinomial + assert (b**e).expand().is_Pow # power_exp + assert (b**-e).expand().is_Pow # power_exp + assert (b**(y - x)).expand().is_Pow # power_exp + assert (b**(3 - x)).expand().is_Pow # multinomial + nn1 = Symbol('nn1', nonnegative=True) + nn2 = Symbol('nn2', nonnegative=True) + nn3 = Symbol('nn3', nonnegative=True) + assert (x**(nn1 + nn2)).expand() == x**nn1*x**nn2 + assert (x**(-nn1 - nn2)).expand() == x**-nn1*x**-nn2 + assert unchanged(Pow, x, nn1 + nn2 - nn3) + assert unchanged(Pow, x, 1 + nn2 - nn3) + assert unchanged(Pow, x, nn1 - nn2) + assert unchanged(Pow, x, 1 - nn2) + assert unchanged(Pow, x, -1 + nn2) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_expr.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_expr.py new file mode 100644 index 0000000000000000000000000000000000000000..af63823345e2b0564ebb7e9015bfe1b423c9bafa --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_expr.py @@ -0,0 +1,2313 @@ +from sympy.assumptions.refine import refine +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.expr import (ExprBuilder, unchanged, Expr, + UnevaluatedExpr) +from sympy.core.function import (Function, expand, WildFunction, + AppliedUndef, Derivative, diff, Subs) +from sympy.core.mul import Mul, _unevaluated_Mul +from sympy.core.numbers import (NumberSymbol, E, zoo, oo, Float, I, + Rational, nan, Integer, Number, pi, _illegal) +from sympy.core.power import Pow +from sympy.core.relational import Ge, Lt, Gt, Le +from sympy.core.singleton import S +from sympy.core.sorting import default_sort_key +from sympy.core.symbol import Symbol, symbols, Dummy, Wild +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.exponential import exp_polar, exp, log +from sympy.functions.elementary.hyperbolic import sinh, tanh +from sympy.functions.elementary.miscellaneous import sqrt, Max +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import tan, sin, cos +from sympy.functions.special.delta_functions import (Heaviside, + DiracDelta) +from sympy.functions.special.error_functions import Si +from sympy.functions.special.gamma_functions import gamma +from sympy.integrals.integrals import integrate, Integral +from sympy.physics.secondquant import FockState +from sympy.polys.partfrac import apart +from sympy.polys.polytools import factor, cancel, Poly +from sympy.polys.rationaltools import together +from sympy.series.order import O +from sympy.sets.sets import FiniteSet +from sympy.simplify.combsimp import combsimp +from sympy.simplify.gammasimp import gammasimp +from sympy.simplify.powsimp import powsimp +from sympy.simplify.radsimp import collect, radsimp +from sympy.simplify.ratsimp import ratsimp +from sympy.simplify.simplify import simplify, nsimplify +from sympy.simplify.trigsimp import trigsimp +from sympy.tensor.indexed import Indexed +from sympy.physics.units import meter + +from sympy.testing.pytest import raises, XFAIL + +from sympy.abc import a, b, c, n, t, u, x, y, z + + +f, g, h = symbols('f,g,h', cls=Function) + + +class DummyNumber: + """ + Minimal implementation of a number that works with SymPy. + + If one has a Number class (e.g. Sage Integer, or some other custom class) + that one wants to work well with SymPy, one has to implement at least the + methods of this class DummyNumber, resp. its subclasses I5 and F1_1. + + Basically, one just needs to implement either __int__() or __float__() and + then one needs to make sure that the class works with Python integers and + with itself. + """ + + def __radd__(self, a): + if isinstance(a, (int, float)): + return a + self.number + return NotImplemented + + def __add__(self, a): + if isinstance(a, (int, float, DummyNumber)): + return self.number + a + return NotImplemented + + def __rsub__(self, a): + if isinstance(a, (int, float)): + return a - self.number + return NotImplemented + + def __sub__(self, a): + if isinstance(a, (int, float, DummyNumber)): + return self.number - a + return NotImplemented + + def __rmul__(self, a): + if isinstance(a, (int, float)): + return a * self.number + return NotImplemented + + def __mul__(self, a): + if isinstance(a, (int, float, DummyNumber)): + return self.number * a + return NotImplemented + + def __rtruediv__(self, a): + if isinstance(a, (int, float)): + return a / self.number + return NotImplemented + + def __truediv__(self, a): + if isinstance(a, (int, float, DummyNumber)): + return self.number / a + return NotImplemented + + def __rpow__(self, a): + if isinstance(a, (int, float)): + return a ** self.number + return NotImplemented + + def __pow__(self, a): + if isinstance(a, (int, float, DummyNumber)): + return self.number ** a + return NotImplemented + + def __pos__(self): + return self.number + + def __neg__(self): + return - self.number + + +class I5(DummyNumber): + number = 5 + + def __int__(self): + return self.number + + +class F1_1(DummyNumber): + number = 1.1 + + def __float__(self): + return self.number + +i5 = I5() +f1_1 = F1_1() + +# basic SymPy objects +basic_objs = [ + Rational(2), + Float("1.3"), + x, + y, + pow(x, y)*y, +] + +# all supported objects +all_objs = basic_objs + [ + 5, + 5.5, + i5, + f1_1 +] + + +def dotest(s): + for xo in all_objs: + for yo in all_objs: + s(xo, yo) + return True + + +def test_basic(): + def j(a, b): + x = a + x = +a + x = -a + x = a + b + x = a - b + x = a*b + x = a/b + x = a**b + del x + assert dotest(j) + + +def test_ibasic(): + def s(a, b): + x = a + x += b + x = a + x -= b + x = a + x *= b + x = a + x /= b + assert dotest(s) + + +class NonBasic: + '''This class represents an object that knows how to implement binary + operations like +, -, etc with Expr but is not a subclass of Basic itself. + The NonExpr subclass below does subclass Basic but not Expr. + + For both NonBasic and NonExpr it should be possible for them to override + Expr.__add__ etc because Expr.__add__ should be returning NotImplemented + for non Expr classes. Otherwise Expr.__add__ would create meaningless + objects like Add(Integer(1), FiniteSet(2)) and it wouldn't be possible for + other classes to override these operations when interacting with Expr. + ''' + def __add__(self, other): + return SpecialOp('+', self, other) + + def __radd__(self, other): + return SpecialOp('+', other, self) + + def __sub__(self, other): + return SpecialOp('-', self, other) + + def __rsub__(self, other): + return SpecialOp('-', other, self) + + def __mul__(self, other): + return SpecialOp('*', self, other) + + def __rmul__(self, other): + return SpecialOp('*', other, self) + + def __truediv__(self, other): + return SpecialOp('/', self, other) + + def __rtruediv__(self, other): + return SpecialOp('/', other, self) + + def __floordiv__(self, other): + return SpecialOp('//', self, other) + + def __rfloordiv__(self, other): + return SpecialOp('//', other, self) + + def __mod__(self, other): + return SpecialOp('%', self, other) + + def __rmod__(self, other): + return SpecialOp('%', other, self) + + def __divmod__(self, other): + return SpecialOp('divmod', self, other) + + def __rdivmod__(self, other): + return SpecialOp('divmod', other, self) + + def __pow__(self, other): + return SpecialOp('**', self, other) + + def __rpow__(self, other): + return SpecialOp('**', other, self) + + def __lt__(self, other): + return SpecialOp('<', self, other) + + def __gt__(self, other): + return SpecialOp('>', self, other) + + def __le__(self, other): + return SpecialOp('<=', self, other) + + def __ge__(self, other): + return SpecialOp('>=', self, other) + + +class NonExpr(Basic, NonBasic): + '''Like NonBasic above except this is a subclass of Basic but not Expr''' + pass + + +class SpecialOp(): + '''Represents the results of operations with NonBasic and NonExpr''' + def __new__(cls, op, arg1, arg2): + obj = object.__new__(cls) + obj.args = (op, arg1, arg2) + return obj + + +class NonArithmetic(Basic): + '''Represents a Basic subclass that does not support arithmetic operations''' + pass + + +def test_cooperative_operations(): + '''Tests that Expr uses binary operations cooperatively. + + In particular it should be possible for non-Expr classes to override + binary operators like +, - etc when used with Expr instances. This should + work for non-Expr classes whether they are Basic subclasses or not. Also + non-Expr classes that do not define binary operators with Expr should give + TypeError. + ''' + # A bunch of instances of Expr subclasses + exprs = [ + Expr(), + S.Zero, + S.One, + S.Infinity, + S.NegativeInfinity, + S.ComplexInfinity, + S.Half, + Float(0.5), + Integer(2), + Symbol('x'), + Mul(2, Symbol('x')), + Add(2, Symbol('x')), + Pow(2, Symbol('x')), + ] + + for e in exprs: + # Test that these classes can override arithmetic operations in + # combination with various Expr types. + for ne in [NonBasic(), NonExpr()]: + + results = [ + (ne + e, ('+', ne, e)), + (e + ne, ('+', e, ne)), + (ne - e, ('-', ne, e)), + (e - ne, ('-', e, ne)), + (ne * e, ('*', ne, e)), + (e * ne, ('*', e, ne)), + (ne / e, ('/', ne, e)), + (e / ne, ('/', e, ne)), + (ne // e, ('//', ne, e)), + (e // ne, ('//', e, ne)), + (ne % e, ('%', ne, e)), + (e % ne, ('%', e, ne)), + (divmod(ne, e), ('divmod', ne, e)), + (divmod(e, ne), ('divmod', e, ne)), + (ne ** e, ('**', ne, e)), + (e ** ne, ('**', e, ne)), + (e < ne, ('>', ne, e)), + (ne < e, ('<', ne, e)), + (e > ne, ('<', ne, e)), + (ne > e, ('>', ne, e)), + (e <= ne, ('>=', ne, e)), + (ne <= e, ('<=', ne, e)), + (e >= ne, ('<=', ne, e)), + (ne >= e, ('>=', ne, e)), + ] + + for res, args in results: + assert type(res) is SpecialOp and res.args == args + + # These classes do not support binary operators with Expr. Every + # operation should raise in combination with any of the Expr types. + for na in [NonArithmetic(), object()]: + + raises(TypeError, lambda : e + na) + raises(TypeError, lambda : na + e) + raises(TypeError, lambda : e - na) + raises(TypeError, lambda : na - e) + raises(TypeError, lambda : e * na) + raises(TypeError, lambda : na * e) + raises(TypeError, lambda : e / na) + raises(TypeError, lambda : na / e) + raises(TypeError, lambda : e // na) + raises(TypeError, lambda : na // e) + raises(TypeError, lambda : e % na) + raises(TypeError, lambda : na % e) + raises(TypeError, lambda : divmod(e, na)) + raises(TypeError, lambda : divmod(na, e)) + raises(TypeError, lambda : e ** na) + raises(TypeError, lambda : na ** e) + raises(TypeError, lambda : e > na) + raises(TypeError, lambda : na > e) + raises(TypeError, lambda : e < na) + raises(TypeError, lambda : na < e) + raises(TypeError, lambda : e >= na) + raises(TypeError, lambda : na >= e) + raises(TypeError, lambda : e <= na) + raises(TypeError, lambda : na <= e) + + +def test_relational(): + from sympy.core.relational import Lt + assert (pi < 3) is S.false + assert (pi <= 3) is S.false + assert (pi > 3) is S.true + assert (pi >= 3) is S.true + assert (-pi < 3) is S.true + assert (-pi <= 3) is S.true + assert (-pi > 3) is S.false + assert (-pi >= 3) is S.false + r = Symbol('r', real=True) + assert (r - 2 < r - 3) is S.false + assert Lt(x + I, x + I + 2).func == Lt # issue 8288 + + +def test_relational_assumptions(): + m1 = Symbol("m1", nonnegative=False) + m2 = Symbol("m2", positive=False) + m3 = Symbol("m3", nonpositive=False) + m4 = Symbol("m4", negative=False) + assert (m1 < 0) == Lt(m1, 0) + assert (m2 <= 0) == Le(m2, 0) + assert (m3 > 0) == Gt(m3, 0) + assert (m4 >= 0) == Ge(m4, 0) + m1 = Symbol("m1", nonnegative=False, real=True) + m2 = Symbol("m2", positive=False, real=True) + m3 = Symbol("m3", nonpositive=False, real=True) + m4 = Symbol("m4", negative=False, real=True) + assert (m1 < 0) is S.true + assert (m2 <= 0) is S.true + assert (m3 > 0) is S.true + assert (m4 >= 0) is S.true + m1 = Symbol("m1", negative=True) + m2 = Symbol("m2", nonpositive=True) + m3 = Symbol("m3", positive=True) + m4 = Symbol("m4", nonnegative=True) + assert (m1 < 0) is S.true + assert (m2 <= 0) is S.true + assert (m3 > 0) is S.true + assert (m4 >= 0) is S.true + m1 = Symbol("m1", negative=False, real=True) + m2 = Symbol("m2", nonpositive=False, real=True) + m3 = Symbol("m3", positive=False, real=True) + m4 = Symbol("m4", nonnegative=False, real=True) + assert (m1 < 0) is S.false + assert (m2 <= 0) is S.false + assert (m3 > 0) is S.false + assert (m4 >= 0) is S.false + + +# See https://github.com/sympy/sympy/issues/17708 +#def test_relational_noncommutative(): +# from sympy import Lt, Gt, Le, Ge +# A, B = symbols('A,B', commutative=False) +# assert (A < B) == Lt(A, B) +# assert (A <= B) == Le(A, B) +# assert (A > B) == Gt(A, B) +# assert (A >= B) == Ge(A, B) + + +def test_basic_nostr(): + for obj in basic_objs: + raises(TypeError, lambda: obj + '1') + raises(TypeError, lambda: obj - '1') + if obj == 2: + assert obj * '1' == '11' + else: + raises(TypeError, lambda: obj * '1') + raises(TypeError, lambda: obj / '1') + raises(TypeError, lambda: obj ** '1') + + +def test_series_expansion_for_uniform_order(): + assert (1/x + y + x).series(x, 0, 0) == 1/x + O(1, x) + assert (1/x + y + x).series(x, 0, 1) == 1/x + y + O(x) + assert (1/x + 1 + x).series(x, 0, 0) == 1/x + O(1, x) + assert (1/x + 1 + x).series(x, 0, 1) == 1/x + 1 + O(x) + assert (1/x + x).series(x, 0, 0) == 1/x + O(1, x) + assert (1/x + y + y*x + x).series(x, 0, 0) == 1/x + O(1, x) + assert (1/x + y + y*x + x).series(x, 0, 1) == 1/x + y + O(x) + + +def test_leadterm(): + assert (3 + 2*x**(log(3)/log(2) - 1)).leadterm(x) == (3, 0) + + assert (1/x**2 + 1 + x + x**2).leadterm(x)[1] == -2 + assert (1/x + 1 + x + x**2).leadterm(x)[1] == -1 + assert (x**2 + 1/x).leadterm(x)[1] == -1 + assert (1 + x**2).leadterm(x)[1] == 0 + assert (x + 1).leadterm(x)[1] == 0 + assert (x + x**2).leadterm(x)[1] == 1 + assert (x**2).leadterm(x)[1] == 2 + + +def test_as_leading_term(): + assert (3 + 2*x**(log(3)/log(2) - 1)).as_leading_term(x) == 3 + assert (1/x**2 + 1 + x + x**2).as_leading_term(x) == 1/x**2 + assert (1/x + 1 + x + x**2).as_leading_term(x) == 1/x + assert (x**2 + 1/x).as_leading_term(x) == 1/x + assert (1 + x**2).as_leading_term(x) == 1 + assert (x + 1).as_leading_term(x) == 1 + assert (x + x**2).as_leading_term(x) == x + assert (x**2).as_leading_term(x) == x**2 + assert (x + oo).as_leading_term(x) is oo + + raises(ValueError, lambda: (x + 1).as_leading_term(1)) + + # https://github.com/sympy/sympy/issues/21177 + e = -3*x + (x + Rational(3, 2) - sqrt(3)*S.ImaginaryUnit/2)**2\ + - Rational(3, 2) + 3*sqrt(3)*S.ImaginaryUnit/2 + assert e.as_leading_term(x) == -sqrt(3)*I*x + + # https://github.com/sympy/sympy/issues/21245 + e = 1 - x - x**2 + d = (1 + sqrt(5))/2 + assert e.subs(x, y + 1/d).as_leading_term(y) == \ + (-40*y - 16*sqrt(5)*y)/(16 + 8*sqrt(5)) + + # https://github.com/sympy/sympy/issues/26991 + assert sinh(tanh(3/(100*x))).as_leading_term(x, cdir = 1) == sinh(1) + + +def test_leadterm2(): + assert (x*cos(1)*cos(1 + sin(1)) + sin(1 + sin(1))).leadterm(x) == \ + (sin(1 + sin(1)), 0) + + +def test_leadterm3(): + assert (y + z + x).leadterm(x) == (y + z, 0) + + +def test_as_leading_term2(): + assert (x*cos(1)*cos(1 + sin(1)) + sin(1 + sin(1))).as_leading_term(x) == \ + sin(1 + sin(1)) + + +def test_as_leading_term3(): + assert (2 + pi + x).as_leading_term(x) == 2 + pi + assert (2*x + pi*x + x**2).as_leading_term(x) == 2*x + pi*x + + +def test_as_leading_term4(): + # see issue 6843 + n = Symbol('n', integer=True, positive=True) + r = -n**3/(2*n**2 + 4*n + 2) - n**2/(n**2 + 2*n + 1) + \ + n**2/(n + 1) - n/(2*n**2 + 4*n + 2) + n/(n*x + x) + 2*n/(n + 1) - \ + 1 + 1/(n*x + x) + 1/(n + 1) - 1/x + assert r.as_leading_term(x).cancel() == n/2 + + +def test_as_leading_term_stub(): + class foo(Function): + pass + assert foo(1/x).as_leading_term(x) == foo(1/x) + assert foo(1).as_leading_term(x) == foo(1) + raises(NotImplementedError, lambda: foo(x).as_leading_term(x)) + + +def test_as_leading_term_deriv_integral(): + # related to issue 11313 + assert Derivative(x ** 3, x).as_leading_term(x) == 3*x**2 + assert Derivative(x ** 3, y).as_leading_term(x) == 0 + + assert Integral(x ** 3, x).as_leading_term(x) == x**4/4 + assert Integral(x ** 3, y).as_leading_term(x) == y*x**3 + + assert Derivative(exp(x), x).as_leading_term(x) == 1 + assert Derivative(log(x), x).as_leading_term(x) == (1/x).as_leading_term(x) + + +def test_atoms(): + assert x.atoms() == {x} + assert (1 + x).atoms() == {x, S.One} + + assert (1 + 2*cos(x)).atoms(Symbol) == {x} + assert (1 + 2*cos(x)).atoms(Symbol, Number) == {S.One, S(2), x} + + assert (2*(x**(y**x))).atoms() == {S(2), x, y} + + assert S.Half.atoms() == {S.Half} + assert S.Half.atoms(Symbol) == set() + + assert sin(oo).atoms(oo) == set() + + assert Poly(0, x).atoms() == {S.Zero, x} + assert Poly(1, x).atoms() == {S.One, x} + + assert Poly(x, x).atoms() == {x} + assert Poly(x, x, y).atoms() == {x, y} + assert Poly(x + y, x, y).atoms() == {x, y} + assert Poly(x + y, x, y, z).atoms() == {x, y, z} + assert Poly(x + y*t, x, y, z).atoms() == {t, x, y, z} + + assert (I*pi).atoms(NumberSymbol) == {pi} + assert (I*pi).atoms(NumberSymbol, I) == \ + (I*pi).atoms(I, NumberSymbol) == {pi, I} + + assert exp(exp(x)).atoms(exp) == {exp(exp(x)), exp(x)} + assert (1 + x*(2 + y) + exp(3 + z)).atoms(Add) == \ + {1 + x*(2 + y) + exp(3 + z), 2 + y, 3 + z} + + # issue 6132 + e = (f(x) + sin(x) + 2) + assert e.atoms(AppliedUndef) == \ + {f(x)} + assert e.atoms(AppliedUndef, Function) == \ + {f(x), sin(x)} + assert e.atoms(Function) == \ + {f(x), sin(x)} + assert e.atoms(AppliedUndef, Number) == \ + {f(x), S(2)} + assert e.atoms(Function, Number) == \ + {S(2), sin(x), f(x)} + + +def test_is_polynomial(): + k = Symbol('k', nonnegative=True, integer=True) + + assert Rational(2).is_polynomial(x, y, z) is True + assert (S.Pi).is_polynomial(x, y, z) is True + + assert x.is_polynomial(x) is True + assert x.is_polynomial(y) is True + + assert (x**2).is_polynomial(x) is True + assert (x**2).is_polynomial(y) is True + + assert (x**(-2)).is_polynomial(x) is False + assert (x**(-2)).is_polynomial(y) is True + + assert (2**x).is_polynomial(x) is False + assert (2**x).is_polynomial(y) is True + + assert (x**k).is_polynomial(x) is False + assert (x**k).is_polynomial(k) is False + assert (x**x).is_polynomial(x) is False + assert (k**k).is_polynomial(k) is False + assert (k**x).is_polynomial(k) is False + + assert (x**(-k)).is_polynomial(x) is False + assert ((2*x)**k).is_polynomial(x) is False + + assert (x**2 + 3*x - 8).is_polynomial(x) is True + assert (x**2 + 3*x - 8).is_polynomial(y) is True + + assert (x**2 + 3*x - 8).is_polynomial() is True + + assert sqrt(x).is_polynomial(x) is False + assert (sqrt(x)**3).is_polynomial(x) is False + + assert (x**2 + 3*x*sqrt(y) - 8).is_polynomial(x) is True + assert (x**2 + 3*x*sqrt(y) - 8).is_polynomial(y) is False + + assert ((x**2)*(y**2) + x*(y**2) + y*x + exp(2)).is_polynomial() is True + assert ((x**2)*(y**2) + x*(y**2) + y*x + exp(x)).is_polynomial() is False + + assert ( + (x**2)*(y**2) + x*(y**2) + y*x + exp(2)).is_polynomial(x, y) is True + assert ( + (x**2)*(y**2) + x*(y**2) + y*x + exp(x)).is_polynomial(x, y) is False + + assert (1/f(x) + 1).is_polynomial(f(x)) is False + + +def test_is_rational_function(): + assert Integer(1).is_rational_function() is True + assert Integer(1).is_rational_function(x) is True + + assert Rational(17, 54).is_rational_function() is True + assert Rational(17, 54).is_rational_function(x) is True + + assert (12/x).is_rational_function() is True + assert (12/x).is_rational_function(x) is True + + assert (x/y).is_rational_function() is True + assert (x/y).is_rational_function(x) is True + assert (x/y).is_rational_function(x, y) is True + + assert (x**2 + 1/x/y).is_rational_function() is True + assert (x**2 + 1/x/y).is_rational_function(x) is True + assert (x**2 + 1/x/y).is_rational_function(x, y) is True + + assert (sin(y)/x).is_rational_function() is False + assert (sin(y)/x).is_rational_function(y) is False + assert (sin(y)/x).is_rational_function(x) is True + assert (sin(y)/x).is_rational_function(x, y) is False + + for i in _illegal: + assert not i.is_rational_function() + for d in (1, x): + assert not (i/d).is_rational_function() + + +def test_is_meromorphic(): + f = a/x**2 + b + x + c*x**2 + assert f.is_meromorphic(x, 0) is True + assert f.is_meromorphic(x, 1) is True + assert f.is_meromorphic(x, zoo) is True + + g = 3 + 2*x**(log(3)/log(2) - 1) + assert g.is_meromorphic(x, 0) is False + assert g.is_meromorphic(x, 1) is True + assert g.is_meromorphic(x, zoo) is False + + n = Symbol('n', integer=True) + e = sin(1/x)**n*x + assert e.is_meromorphic(x, 0) is False + assert e.is_meromorphic(x, 1) is True + assert e.is_meromorphic(x, zoo) is False + + e = log(x)**pi + assert e.is_meromorphic(x, 0) is False + assert e.is_meromorphic(x, 1) is False + assert e.is_meromorphic(x, 2) is True + assert e.is_meromorphic(x, zoo) is False + + assert (log(x)**a).is_meromorphic(x, 0) is False + assert (log(x)**a).is_meromorphic(x, 1) is False + assert (a**log(x)).is_meromorphic(x, 0) is None + assert (3**log(x)).is_meromorphic(x, 0) is False + assert (3**log(x)).is_meromorphic(x, 1) is True + +def test_is_algebraic_expr(): + assert sqrt(3).is_algebraic_expr(x) is True + assert sqrt(3).is_algebraic_expr() is True + + eq = ((1 + x**2)/(1 - y**2))**(S.One/3) + assert eq.is_algebraic_expr(x) is True + assert eq.is_algebraic_expr(y) is True + + assert (sqrt(x) + y**(S(2)/3)).is_algebraic_expr(x) is True + assert (sqrt(x) + y**(S(2)/3)).is_algebraic_expr(y) is True + assert (sqrt(x) + y**(S(2)/3)).is_algebraic_expr() is True + + assert (cos(y)/sqrt(x)).is_algebraic_expr() is False + assert (cos(y)/sqrt(x)).is_algebraic_expr(x) is True + assert (cos(y)/sqrt(x)).is_algebraic_expr(y) is False + assert (cos(y)/sqrt(x)).is_algebraic_expr(x, y) is False + + +def test_SAGE1(): + #see https://github.com/sympy/sympy/issues/3346 + class MyInt: + def _sympy_(self): + return Integer(5) + m = MyInt() + e = Rational(2)*m + assert e == 10 + + raises(TypeError, lambda: Rational(2)*MyInt) + + +def test_SAGE2(): + class MyInt: + def __int__(self): + return 5 + assert sympify(MyInt()) == 5 + e = Rational(2)*MyInt() + assert e == 10 + + raises(TypeError, lambda: Rational(2)*MyInt) + + +def test_SAGE3(): + class MySymbol: + def __rmul__(self, other): + return ('mys', other, self) + + o = MySymbol() + e = x*o + + assert e == ('mys', x, o) + + +def test_len(): + e = x*y + assert len(e.args) == 2 + e = x + y + z + assert len(e.args) == 3 + + +def test_doit(): + a = Integral(x**2, x) + + assert isinstance(a.doit(), Integral) is False + + assert isinstance(a.doit(integrals=True), Integral) is False + assert isinstance(a.doit(integrals=False), Integral) is True + + assert (2*Integral(x, x)).doit() == x**2 + + +def test_attribute_error(): + raises(AttributeError, lambda: x.cos()) + raises(AttributeError, lambda: x.sin()) + raises(AttributeError, lambda: x.exp()) + + +def test_args(): + assert (x*y).args in ((x, y), (y, x)) + assert (x + y).args in ((x, y), (y, x)) + assert (x*y + 1).args in ((x*y, 1), (1, x*y)) + assert sin(x*y).args == (x*y,) + assert sin(x*y).args[0] == x*y + assert (x**y).args == (x, y) + assert (x**y).args[0] == x + assert (x**y).args[1] == y + + +def test_noncommutative_expand_issue_3757(): + A, B, C = symbols('A,B,C', commutative=False) + assert A*B - B*A != 0 + assert (A*(A + B)*B).expand() == A**2*B + A*B**2 + assert (A*(A + B + C)*B).expand() == A**2*B + A*B**2 + A*C*B + + +def test_as_numer_denom(): + a, b, c = symbols('a, b, c') + + assert nan.as_numer_denom() == (nan, 1) + assert oo.as_numer_denom() == (oo, 1) + assert (-oo).as_numer_denom() == (-oo, 1) + assert zoo.as_numer_denom() == (zoo, 1) + assert (-zoo).as_numer_denom() == (zoo, 1) + + assert x.as_numer_denom() == (x, 1) + assert (1/x).as_numer_denom() == (1, x) + assert (x/y).as_numer_denom() == (x, y) + assert (x/2).as_numer_denom() == (x, 2) + assert (x*y/z).as_numer_denom() == (x*y, z) + assert (x/(y*z)).as_numer_denom() == (x, y*z) + assert S.Half.as_numer_denom() == (1, 2) + assert (1/y**2).as_numer_denom() == (1, y**2) + assert (x/y**2).as_numer_denom() == (x, y**2) + assert ((x**2 + 1)/y).as_numer_denom() == (x**2 + 1, y) + assert (x*(y + 1)/y**7).as_numer_denom() == (x*(y + 1), y**7) + assert (x**-2).as_numer_denom() == (1, x**2) + assert (a/x + b/2/x + c/3/x).as_numer_denom() == \ + (6*a + 3*b + 2*c, 6*x) + assert (a/x + b/2/x + c/3/y).as_numer_denom() == \ + (2*c*x + y*(6*a + 3*b), 6*x*y) + assert (a/x + b/2/x + c/.5/x).as_numer_denom() == \ + (2*a + b + 4.0*c, 2*x) + # this should take no more than a few seconds + assert int(log(Add(*[Dummy()/i/x for i in range(1, 705)] + ).as_numer_denom()[1]/x).n(4)) == 705 + for i in [S.Infinity, S.NegativeInfinity, S.ComplexInfinity]: + assert (i + x/3).as_numer_denom() == \ + (x + i, 3) + assert (S.Infinity + x/3 + y/4).as_numer_denom() == \ + (4*x + 3*y + S.Infinity, 12) + assert (oo*x + zoo*y).as_numer_denom() == \ + (zoo*y + oo*x, 1) + + A, B, C = symbols('A,B,C', commutative=False) + + assert (A*B*C**-1).as_numer_denom() == (A*B*C**-1, 1) + assert (A*B*C**-1/x).as_numer_denom() == (A*B*C**-1, x) + assert (C**-1*A*B).as_numer_denom() == (C**-1*A*B, 1) + assert (C**-1*A*B/x).as_numer_denom() == (C**-1*A*B, x) + assert ((A*B*C)**-1).as_numer_denom() == ((A*B*C)**-1, 1) + assert ((A*B*C)**-1/x).as_numer_denom() == ((A*B*C)**-1, x) + + # the following morphs from Add to Mul during processing + assert Add(0, (x + y)/z/-2, evaluate=False).as_numer_denom( + ) == (-x - y, 2*z) + + +def test_trunc(): + import math + x, y = symbols('x y') + assert math.trunc(2) == 2 + assert math.trunc(4.57) == 4 + assert math.trunc(-5.79) == -5 + assert math.trunc(pi) == 3 + assert math.trunc(log(7)) == 1 + assert math.trunc(exp(5)) == 148 + assert math.trunc(cos(pi)) == -1 + assert math.trunc(sin(5)) == 0 + + raises(TypeError, lambda: math.trunc(x)) + raises(TypeError, lambda: math.trunc(x + y**2)) + raises(TypeError, lambda: math.trunc(oo)) + + +def test_as_independent(): + assert S.Zero.as_independent(x, as_Add=True) == (0, 0) + assert S.Zero.as_independent(x, as_Add=False) == (0, 0) + assert (2*x*sin(x) + y + x).as_independent(x) == (y, x + 2*x*sin(x)) + assert (2*x*sin(x) + y + x).as_independent(y) == (x + 2*x*sin(x), y) + + assert (2*x*sin(x) + y + x).as_independent(x, y) == (0, y + x + 2*x*sin(x)) + + assert (x*sin(x)*cos(y)).as_independent(x) == (cos(y), x*sin(x)) + assert (x*sin(x)*cos(y)).as_independent(y) == (x*sin(x), cos(y)) + + assert (x*sin(x)*cos(y)).as_independent(x, y) == (1, x*sin(x)*cos(y)) + + assert (sin(x)).as_independent(x) == (1, sin(x)) + assert (sin(x)).as_independent(y) == (sin(x), 1) + + assert (2*sin(x)).as_independent(x) == (2, sin(x)) + assert (2*sin(x)).as_independent(y) == (2*sin(x), 1) + + # issue 4903 = 1766b + n1, n2, n3 = symbols('n1 n2 n3', commutative=False) + assert (n1 + n1*n2).as_independent(n2) == (n1, n1*n2) + assert (n2*n1 + n1*n2).as_independent(n2) == (0, n1*n2 + n2*n1) + assert (n1*n2*n1).as_independent(n2) == (n1, n2*n1) + assert (n1*n2*n1).as_independent(n1) == (1, n1*n2*n1) + + assert (3*x).as_independent(x, as_Add=True) == (0, 3*x) + assert (3*x).as_independent(x, as_Add=False) == (3, x) + assert (3 + x).as_independent(x, as_Add=True) == (3, x) + assert (3 + x).as_independent(x, as_Add=False) == (1, 3 + x) + + # issue 5479 + assert (3*x).as_independent(Symbol) == (3, x) + + # issue 5648 + assert (n1*x*y).as_independent(x) == (n1*y, x) + assert ((x + n1)*(x - y)).as_independent(x) == (1, (x + n1)*(x - y)) + assert ((x + n1)*(x - y)).as_independent(y) == (x + n1, x - y) + assert (DiracDelta(x - n1)*DiracDelta(x - y)).as_independent(x) \ + == (1, DiracDelta(x - n1)*DiracDelta(x - y)) + assert (x*y*n1*n2*n3).as_independent(n2) == (x*y*n1, n2*n3) + assert (x*y*n1*n2*n3).as_independent(n1) == (x*y, n1*n2*n3) + assert (x*y*n1*n2*n3).as_independent(n3) == (x*y*n1*n2, n3) + assert (DiracDelta(x - n1)*DiracDelta(y - n1)*DiracDelta(x - n2)).as_independent(y) == \ + (DiracDelta(x - n1)*DiracDelta(x - n2), DiracDelta(y - n1)) + + # issue 5784 + assert (x + Integral(x, (x, 1, 2))).as_independent(x, strict=True) == \ + (Integral(x, (x, 1, 2)), x) + + eq = Add(x, -x, 2, -3, evaluate=False) + assert eq.as_independent(x) == (-1, Add(x, -x, evaluate=False)) + eq = Mul(x, 1/x, 2, -3, evaluate=False) + assert eq.as_independent(x) == (-6, Mul(x, 1/x, evaluate=False)) + + assert (x*y).as_independent(z, as_Add=True) == (x*y, 0) + +@XFAIL +def test_call_2(): + # TODO UndefinedFunction does not subclass Expr + assert (2*f)(x) == 2*f(x) + + +def test_replace(): + e = log(sin(x)) + tan(sin(x**2)) + + assert e.replace(sin, cos) == log(cos(x)) + tan(cos(x**2)) + assert e.replace( + sin, lambda a: sin(2*a)) == log(sin(2*x)) + tan(sin(2*x**2)) + + a = Wild('a') + b = Wild('b') + + assert e.replace(sin(a), cos(a)) == log(cos(x)) + tan(cos(x**2)) + assert e.replace( + sin(a), lambda a: sin(2*a)) == log(sin(2*x)) + tan(sin(2*x**2)) + # test exact + assert (2*x).replace(a*x + b, b - a, exact=True) == 2*x + assert (2*x).replace(a*x + b, b - a) == 2*x + assert (2*x).replace(a*x + b, b - a, exact=False) == 2/x + assert (2*x).replace(a*x + b, lambda a, b: b - a, exact=True) == 2*x + assert (2*x).replace(a*x + b, lambda a, b: b - a) == 2*x + assert (2*x).replace(a*x + b, lambda a, b: b - a, exact=False) == 2/x + + g = 2*sin(x**3) + + assert g.replace( + lambda expr: expr.is_Number, lambda expr: expr**2) == 4*sin(x**9) + + assert cos(x).replace(cos, sin, map=True) == (sin(x), {cos(x): sin(x)}) + assert sin(x).replace(cos, sin) == sin(x) + + cond, func = lambda x: x.is_Mul, lambda x: 2*x + assert (x*y).replace(cond, func, map=True) == (2*x*y, {x*y: 2*x*y}) + assert (x*(1 + x*y)).replace(cond, func, map=True) == \ + (2*x*(2*x*y + 1), {x*(2*x*y + 1): 2*x*(2*x*y + 1), x*y: 2*x*y}) + assert (y*sin(x)).replace(sin, lambda expr: sin(expr)/y, map=True) == \ + (sin(x), {sin(x): sin(x)/y}) + # if not simultaneous then y*sin(x) -> y*sin(x)/y = sin(x) -> sin(x)/y + assert (y*sin(x)).replace(sin, lambda expr: sin(expr)/y, + simultaneous=False) == sin(x)/y + assert (x**2 + O(x**3)).replace(Pow, lambda b, e: b**e/e + ) == x**2/2 + O(x**3) + assert (x**2 + O(x**3)).replace(Pow, lambda b, e: b**e/e, + simultaneous=False) == x**2/2 + O(x**3) + assert (x*(x*y + 3)).replace(lambda x: x.is_Mul, lambda x: 2 + x) == \ + x*(x*y + 5) + 2 + e = (x*y + 1)*(2*x*y + 1) + 1 + assert e.replace(cond, func, map=True) == ( + 2*((2*x*y + 1)*(4*x*y + 1)) + 1, + {2*x*y: 4*x*y, x*y: 2*x*y, (2*x*y + 1)*(4*x*y + 1): + 2*((2*x*y + 1)*(4*x*y + 1))}) + assert x.replace(x, y) == y + assert (x + 1).replace(1, 2) == x + 2 + + # https://groups.google.com/forum/#!topic/sympy/8wCgeC95tz0 + n1, n2, n3 = symbols('n1:4', commutative=False) + assert (n1*f(n2)).replace(f, lambda x: x) == n1*n2 + assert (n3*f(n2)).replace(f, lambda x: x) == n3*n2 + + # issue 16725 + assert S.Zero.replace(Wild('x'), 1) == 1 + # let the user override the default decision of False + assert S.Zero.replace(Wild('x'), 1, exact=True) == 0 + + +def test_replace_integral(): + # https://github.com/sympy/sympy/issues/27142 + q, p, s, t = symbols('q p s t', cls=Wild) + a, b, c, d = symbols('a b c d') + i = Integral(a + b, (b, c, d)) + pattern = Integral(q, (p, s, t)) + assert i.replace(pattern, q) == a + b + + +def test_find(): + expr = (x + y + 2 + sin(3*x)) + + assert expr.find(lambda u: u.is_Integer) == {S(2), S(3)} + assert expr.find(lambda u: u.is_Symbol) == {x, y} + + assert expr.find(lambda u: u.is_Integer, group=True) == {S(2): 1, S(3): 1} + assert expr.find(lambda u: u.is_Symbol, group=True) == {x: 2, y: 1} + + assert expr.find(Integer) == {S(2), S(3)} + assert expr.find(Symbol) == {x, y} + + assert expr.find(Integer, group=True) == {S(2): 1, S(3): 1} + assert expr.find(Symbol, group=True) == {x: 2, y: 1} + + a = Wild('a') + + expr = sin(sin(x)) + sin(x) + cos(x) + x + + assert expr.find(lambda u: type(u) is sin) == {sin(x), sin(sin(x))} + assert expr.find( + lambda u: type(u) is sin, group=True) == {sin(x): 2, sin(sin(x)): 1} + + assert expr.find(sin(a)) == {sin(x), sin(sin(x))} + assert expr.find(sin(a), group=True) == {sin(x): 2, sin(sin(x)): 1} + + assert expr.find(sin) == {sin(x), sin(sin(x))} + assert expr.find(sin, group=True) == {sin(x): 2, sin(sin(x)): 1} + + +def test_count(): + expr = (x + y + 2 + sin(3*x)) + + assert expr.count(lambda u: u.is_Integer) == 2 + assert expr.count(lambda u: u.is_Symbol) == 3 + + assert expr.count(Integer) == 2 + assert expr.count(Symbol) == 3 + assert expr.count(2) == 1 + + a = Wild('a') + + assert expr.count(sin) == 1 + assert expr.count(sin(a)) == 1 + assert expr.count(lambda u: type(u) is sin) == 1 + + assert f(x).count(f(x)) == 1 + assert f(x).diff(x).count(f(x)) == 1 + assert f(x).diff(x).count(x) == 2 + + +def test_has_basics(): + p = Wild('p') + + assert sin(x).has(x) + assert sin(x).has(sin) + assert not sin(x).has(y) + assert not sin(x).has(cos) + assert f(x).has(x) + assert f(x).has(f) + assert not f(x).has(y) + assert not f(x).has(g) + + assert f(x).diff(x).has(x) + assert f(x).diff(x).has(f) + assert f(x).diff(x).has(Derivative) + assert not f(x).diff(x).has(y) + assert not f(x).diff(x).has(g) + assert not f(x).diff(x).has(sin) + + assert (x**2).has(Symbol) + assert not (x**2).has(Wild) + assert (2*p).has(Wild) + + assert not x.has() + + # see issue at https://github.com/sympy/sympy/issues/5190 + assert not S(1).has(Wild) + assert not x.has(Wild) + + +def test_has_multiple(): + f = x**2*y + sin(2**t + log(z)) + + assert f.has(x) + assert f.has(y) + assert f.has(z) + assert f.has(t) + + assert not f.has(u) + + assert f.has(x, y, z, t) + assert f.has(x, y, z, t, u) + + i = Integer(4400) + + assert not i.has(x) + + assert (i*x**i).has(x) + assert not (i*y**i).has(x) + assert (i*y**i).has(x, y) + assert not (i*y**i).has(x, z) + + +def test_has_piecewise(): + f = (x*y + 3/y)**(3 + 2) + p = Piecewise((g(x), x < -1), (1, x <= 1), (f, True)) + + assert p.has(x) + assert p.has(y) + assert not p.has(z) + assert p.has(1) + assert p.has(3) + assert not p.has(4) + assert p.has(f) + assert p.has(g) + assert not p.has(h) + + +def test_has_iterative(): + A, B, C = symbols('A,B,C', commutative=False) + f = x*gamma(x)*sin(x)*exp(x*y)*A*B*C*cos(x*A*B) + + assert f.has(x) + assert f.has(x*y) + assert f.has(x*sin(x)) + assert not f.has(x*sin(y)) + assert f.has(x*A) + assert f.has(x*A*B) + assert not f.has(x*A*C) + assert f.has(x*A*B*C) + assert not f.has(x*A*C*B) + assert f.has(x*sin(x)*A*B*C) + assert not f.has(x*sin(x)*A*C*B) + assert not f.has(x*sin(y)*A*B*C) + assert f.has(x*gamma(x)) + assert not f.has(x + sin(x)) + + assert (x & y & z).has(x & z) + + +def test_has_integrals(): + f = Integral(x**2 + sin(x*y*z), (x, 0, x + y + z)) + + assert f.has(x + y) + assert f.has(x + z) + assert f.has(y + z) + + assert f.has(x*y) + assert f.has(x*z) + assert f.has(y*z) + + assert not f.has(2*x + y) + assert not f.has(2*x*y) + + +def test_has_tuple(): + assert Tuple(x, y).has(x) + assert not Tuple(x, y).has(z) + assert Tuple(f(x), g(x)).has(x) + assert not Tuple(f(x), g(x)).has(y) + assert Tuple(f(x), g(x)).has(f) + assert Tuple(f(x), g(x)).has(f(x)) + # XXX to be deprecated + #assert not Tuple(f, g).has(x) + #assert Tuple(f, g).has(f) + #assert not Tuple(f, g).has(h) + assert Tuple(True).has(True) + assert Tuple(True).has(S.true) + assert not Tuple(True).has(1) + + +def test_has_units(): + from sympy.physics.units import m, s + + assert (x*m/s).has(x) + assert (x*m/s).has(y, z) is False + + +def test_has_polys(): + poly = Poly(x**2 + x*y*sin(z), x, y, t) + + assert poly.has(x) + assert poly.has(x, y, z) + assert poly.has(x, y, z, t) + + +def test_has_physics(): + assert FockState((x, y)).has(x) + + +def test_as_poly_as_expr(): + f = x**2 + 2*x*y + + assert f.as_poly().as_expr() == f + assert f.as_poly(x, y).as_expr() == f + + assert (f + sin(x)).as_poly(x, y) is None + + p = Poly(f, x, y) + + assert p.as_poly() == p + + # https://github.com/sympy/sympy/issues/20610 + assert S(2).as_poly() is None + assert sqrt(2).as_poly(extension=True) is None + + raises(AttributeError, lambda: Tuple(x, x).as_poly(x)) + raises(AttributeError, lambda: Tuple(x ** 2, x, y).as_poly(x)) + + +def test_nonzero(): + assert bool(S.Zero) is False + assert bool(S.One) is True + assert bool(x) is True + assert bool(x + y) is True + assert bool(x - x) is False + assert bool(x*y) is True + assert bool(x*1) is True + assert bool(x*0) is False + + +def test_is_number(): + assert Float(3.14).is_number is True + assert Integer(737).is_number is True + assert Rational(3, 2).is_number is True + assert Rational(8).is_number is True + assert x.is_number is False + assert (2*x).is_number is False + assert (x + y).is_number is False + assert log(2).is_number is True + assert log(x).is_number is False + assert (2 + log(2)).is_number is True + assert (8 + log(2)).is_number is True + assert (2 + log(x)).is_number is False + assert (8 + log(2) + x).is_number is False + assert (1 + x**2/x - x).is_number is True + assert Tuple(Integer(1)).is_number is False + assert Add(2, x).is_number is False + assert Mul(3, 4).is_number is True + assert Pow(log(2), 2).is_number is True + assert oo.is_number is True + g = WildFunction('g') + assert g.is_number is False + assert (2*g).is_number is False + assert (x**2).subs(x, 3).is_number is True + + # test extensibility of .is_number + # on subinstances of Basic + class A(Basic): + pass + a = A() + assert a.is_number is False + + +def test_as_coeff_add(): + assert S(2).as_coeff_add() == (2, ()) + assert S(3.0).as_coeff_add() == (0, (S(3.0),)) + assert S(-3.0).as_coeff_add() == (0, (S(-3.0),)) + assert x.as_coeff_add() == (0, (x,)) + assert (x - 1).as_coeff_add() == (-1, (x,)) + assert (x + 1).as_coeff_add() == (1, (x,)) + assert (x + 2).as_coeff_add() == (2, (x,)) + assert (x + y).as_coeff_add(y) == (x, (y,)) + assert (3*x).as_coeff_add(y) == (3*x, ()) + # don't do expansion + e = (x + y)**2 + assert e.as_coeff_add(y) == (0, (e,)) + + +def test_as_coeff_mul(): + assert S(2).as_coeff_mul() == (2, ()) + assert S(3.0).as_coeff_mul() == (1, (S(3.0),)) + assert S(-3.0).as_coeff_mul() == (-1, (S(3.0),)) + assert S(-3.0).as_coeff_mul(rational=False) == (-S(3.0), ()) + assert x.as_coeff_mul() == (1, (x,)) + assert (-x).as_coeff_mul() == (-1, (x,)) + assert (2*x).as_coeff_mul() == (2, (x,)) + assert (x*y).as_coeff_mul(y) == (x, (y,)) + assert (3 + x).as_coeff_mul() == (1, (3 + x,)) + assert (3 + x).as_coeff_mul(y) == (3 + x, ()) + # don't do expansion + e = exp(x + y) + assert e.as_coeff_mul(y) == (1, (e,)) + e = 2**(x + y) + assert e.as_coeff_mul(y) == (1, (e,)) + assert (1.1*x).as_coeff_mul(rational=False) == (1.1, (x,)) + assert (1.1*x).as_coeff_mul() == (1, (1.1, x)) + assert (-oo*x).as_coeff_mul(rational=True) == (-1, (oo, x)) + + +def test_as_coeff_exponent(): + assert (3*x**4).as_coeff_exponent(x) == (3, 4) + assert (2*x**3).as_coeff_exponent(x) == (2, 3) + assert (4*x**2).as_coeff_exponent(x) == (4, 2) + assert (6*x**1).as_coeff_exponent(x) == (6, 1) + assert (3*x**0).as_coeff_exponent(x) == (3, 0) + assert (2*x**0).as_coeff_exponent(x) == (2, 0) + assert (1*x**0).as_coeff_exponent(x) == (1, 0) + assert (0*x**0).as_coeff_exponent(x) == (0, 0) + assert (-1*x**0).as_coeff_exponent(x) == (-1, 0) + assert (-2*x**0).as_coeff_exponent(x) == (-2, 0) + assert (2*x**3 + pi*x**3).as_coeff_exponent(x) == (2 + pi, 3) + assert (x*log(2)/(2*x + pi*x)).as_coeff_exponent(x) == \ + (log(2)/(2 + pi), 0) + # issue 4784 + D = Derivative + fx = D(f(x), x) + assert fx.as_coeff_exponent(f(x)) == (fx, 0) + + +def test_extractions(): + for base in (2, S.Exp1): + assert Pow(base**x, 3, evaluate=False + ).extract_multiplicatively(base**x) == base**(2*x) + assert (base**(5*x)).extract_multiplicatively( + base**(3*x)) == base**(2*x) + assert ((x*y)**3).extract_multiplicatively(x**2 * y) == x*y**2 + assert ((x*y)**3).extract_multiplicatively(x**4 * y) is None + assert (2*x).extract_multiplicatively(2) == x + assert (2*x).extract_multiplicatively(3) is None + assert (2*x).extract_multiplicatively(-1) is None + assert (S.Half*x).extract_multiplicatively(3) == x/6 + assert (sqrt(x)).extract_multiplicatively(x) is None + assert (sqrt(x)).extract_multiplicatively(1/x) is None + assert x.extract_multiplicatively(-x) is None + assert (-2 - 4*I).extract_multiplicatively(-2) == 1 + 2*I + assert (-2 - 4*I).extract_multiplicatively(3) is None + assert (-2*x - 4*y - 8).extract_multiplicatively(-2) == x + 2*y + 4 + assert (-2*x*y - 4*x**2*y).extract_multiplicatively(-2*y) == 2*x**2 + x + assert (2*x*y + 4*x**2*y).extract_multiplicatively(2*y) == 2*x**2 + x + assert (-4*y**2*x).extract_multiplicatively(-3*y) is None + assert (2*x).extract_multiplicatively(1) == 2*x + assert (-oo).extract_multiplicatively(5) is -oo + assert (oo).extract_multiplicatively(5) is oo + + assert ((x*y)**3).extract_additively(1) is None + assert (x + 1).extract_additively(x) == 1 + assert (x + 1).extract_additively(2*x) is None + assert (x + 1).extract_additively(-x) is None + assert (-x + 1).extract_additively(2*x) is None + assert (2*x + 3).extract_additively(x) == x + 3 + assert (2*x + 3).extract_additively(2) == 2*x + 1 + assert (2*x + 3).extract_additively(3) == 2*x + assert (2*x + 3).extract_additively(-2) is None + assert (2*x + 3).extract_additively(3*x) is None + assert (2*x + 3).extract_additively(2*x) == 3 + assert x.extract_additively(0) == x + assert S(2).extract_additively(x) is None + assert S(2.).extract_additively(2.) is S.Zero + assert S(2.).extract_additively(2) is S.Zero + assert S(2*x + 3).extract_additively(x + 1) == x + 2 + assert S(2*x + 3).extract_additively(y + 1) is None + assert S(2*x - 3).extract_additively(x + 1) is None + assert S(2*x - 3).extract_additively(y + z) is None + assert ((a + 1)*x*4 + y).extract_additively(x).expand() == \ + 4*a*x + 3*x + y + assert ((a + 1)*x*4 + 3*y).extract_additively(x + 2*y).expand() == \ + 4*a*x + 3*x + y + assert (y*(x + 1)).extract_additively(x + 1) is None + assert ((y + 1)*(x + 1) + 3).extract_additively(x + 1) == \ + y*(x + 1) + 3 + assert ((x + y)*(x + 1) + x + y + 3).extract_additively(x + y) == \ + x*(x + y) + 3 + assert (x + y + 2*((x + y)*(x + 1)) + 3).extract_additively((x + y)*(x + 1)) == \ + x + y + (x + 1)*(x + y) + 3 + assert ((y + 1)*(x + 2*y + 1) + 3).extract_additively(y + 1) == \ + (x + 2*y)*(y + 1) + 3 + assert (-x - x*I).extract_additively(-x) == -I*x + # extraction does not leave artificats, now + assert (4*x*(y + 1) + y).extract_additively(x) == x*(4*y + 3) + y + + n = Symbol("n", integer=True) + assert (Integer(-3)).could_extract_minus_sign() is True + assert (-n*x + x).could_extract_minus_sign() != \ + (n*x - x).could_extract_minus_sign() + assert (x - y).could_extract_minus_sign() != \ + (-x + y).could_extract_minus_sign() + assert (1 - x - y).could_extract_minus_sign() is True + assert (1 - x + y).could_extract_minus_sign() is False + assert ((-x - x*y)/y).could_extract_minus_sign() is False + assert ((x + x*y)/(-y)).could_extract_minus_sign() is True + assert ((x + x*y)/y).could_extract_minus_sign() is False + assert ((-x - y)/(x + y)).could_extract_minus_sign() is False + + class sign_invariant(Function, Expr): + nargs = 1 + def __neg__(self): + return self + foo = sign_invariant(x) + assert foo == -foo + assert foo.could_extract_minus_sign() is False + assert (x - y).could_extract_minus_sign() is False + assert (-x + y).could_extract_minus_sign() is True + assert (x - 1).could_extract_minus_sign() is False + assert (1 - x).could_extract_minus_sign() is True + assert (sqrt(2) - 1).could_extract_minus_sign() is True + assert (1 - sqrt(2)).could_extract_minus_sign() is False + # check that result is canonical + eq = (3*x + 15*y).extract_multiplicatively(3) + assert eq.args == eq.func(*eq.args).args + + +def test_nan_extractions(): + for r in (1, 0, I, nan): + assert nan.extract_additively(r) is None + assert nan.extract_multiplicatively(r) is None + + +def test_coeff(): + assert (x + 1).coeff(x + 1) == 1 + assert (3*x).coeff(0) == 0 + assert (z*(1 + x)*x**2).coeff(1 + x) == z*x**2 + assert (1 + 2*x*x**(1 + x)).coeff(x*x**(1 + x)) == 2 + assert (1 + 2*x**(y + z)).coeff(x**(y + z)) == 2 + assert (3 + 2*x + 4*x**2).coeff(1) == 0 + assert (3 + 2*x + 4*x**2).coeff(-1) == 0 + assert (3 + 2*x + 4*x**2).coeff(x) == 2 + assert (3 + 2*x + 4*x**2).coeff(x**2) == 4 + assert (3 + 2*x + 4*x**2).coeff(x**3) == 0 + + assert (-x/8 + x*y).coeff(x) == Rational(-1, 8) + y + assert (-x/8 + x*y).coeff(-x) == S.One/8 + assert (4*x).coeff(2*x) == 0 + assert (2*x).coeff(2*x) == 1 + assert (-oo*x).coeff(x*oo) == -1 + assert (10*x).coeff(x, 0) == 0 + assert (10*x).coeff(10*x, 0) == 0 + + n1, n2 = symbols('n1 n2', commutative=False) + assert (n1*n2).coeff(n1) == 1 + assert (n1*n2).coeff(n2) == n1 + assert (n1*n2 + x*n1).coeff(n1) == 1 # 1*n1*(n2+x) + assert (n2*n1 + x*n1).coeff(n1) == n2 + x + assert (n2*n1 + x*n1**2).coeff(n1) == n2 + assert (n1**x).coeff(n1) == 0 + assert (n1*n2 + n2*n1).coeff(n1) == 0 + assert (2*(n1 + n2)*n2).coeff(n1 + n2, right=1) == n2 + assert (2*(n1 + n2)*n2).coeff(n1 + n2, right=0) == 2 + + assert (2*f(x) + 3*f(x).diff(x)).coeff(f(x)) == 2 + + expr = z*(x + y)**2 + expr2 = z*(x + y)**2 + z*(2*x + 2*y)**2 + assert expr.coeff(z) == (x + y)**2 + assert expr.coeff(x + y) == 0 + assert expr2.coeff(z) == (x + y)**2 + (2*x + 2*y)**2 + + assert (x + y + 3*z).coeff(1) == x + y + assert (-x + 2*y).coeff(-1) == x + assert (x - 2*y).coeff(-1) == 2*y + assert (3 + 2*x + 4*x**2).coeff(1) == 0 + assert (-x - 2*y).coeff(2) == -y + assert (x + sqrt(2)*x).coeff(sqrt(2)) == x + assert (3 + 2*x + 4*x**2).coeff(x) == 2 + assert (3 + 2*x + 4*x**2).coeff(x**2) == 4 + assert (3 + 2*x + 4*x**2).coeff(x**3) == 0 + assert (z*(x + y)**2).coeff((x + y)**2) == z + assert (z*(x + y)**2).coeff(x + y) == 0 + assert (2 + 2*x + (x + 1)*y).coeff(x + 1) == y + + assert (x + 2*y + 3).coeff(1) == x + assert (x + 2*y + 3).coeff(x, 0) == 2*y + 3 + assert (x**2 + 2*y + 3*x).coeff(x**2, 0) == 2*y + 3*x + assert x.coeff(0, 0) == 0 + assert x.coeff(x, 0) == 0 + + n, m, o, l = symbols('n m o l', commutative=False) + assert n.coeff(n) == 1 + assert y.coeff(n) == 0 + assert (3*n).coeff(n) == 3 + assert (2 + n).coeff(x*m) == 0 + assert (2*x*n*m).coeff(x) == 2*n*m + assert (2 + n).coeff(x*m*n + y) == 0 + assert (2*x*n*m).coeff(3*n) == 0 + assert (n*m + m*n*m).coeff(n) == 1 + m + assert (n*m + m*n*m).coeff(n, right=True) == m # = (1 + m)*n*m + assert (n*m + m*n).coeff(n) == 0 + assert (n*m + o*m*n).coeff(m*n) == o + assert (n*m + o*m*n).coeff(m*n, right=True) == 1 + assert (n*m + n*m*n).coeff(n*m, right=True) == 1 + n # = n*m*(n + 1) + + assert (x*y).coeff(z, 0) == x*y + + assert (x*n + y*n + z*m).coeff(n) == x + y + assert (n*m + n*o + o*l).coeff(n, right=True) == m + o + assert (x*n*m*n + y*n*m*o + z*l).coeff(m, right=True) == x*n + y*o + assert (x*n*m*n + x*n*m*o + z*l).coeff(m, right=True) == n + o + assert (x*n*m*n + x*n*m*o + z*l).coeff(m) == x*n + + +def test_coeff2(): + r, kappa = symbols('r, kappa') + psi = Function("psi") + g = 1/r**2 * (2*r*psi(r).diff(r, 1) + r**2 * psi(r).diff(r, 2)) + g = g.expand() + assert g.coeff(psi(r).diff(r)) == 2/r + + +def test_coeff2_0(): + r, kappa = symbols('r, kappa') + psi = Function("psi") + g = 1/r**2 * (2*r*psi(r).diff(r, 1) + r**2 * psi(r).diff(r, 2)) + g = g.expand() + + assert g.coeff(psi(r).diff(r, 2)) == 1 + + +def test_coeff_expand(): + expr = z*(x + y)**2 + expr2 = z*(x + y)**2 + z*(2*x + 2*y)**2 + assert expr.coeff(z) == (x + y)**2 + assert expr2.coeff(z) == (x + y)**2 + (2*x + 2*y)**2 + + +def test_integrate(): + assert x.integrate(x) == x**2/2 + assert x.integrate((x, 0, 1)) == S.Half + + +def test_as_base_exp(): + assert x.as_base_exp() == (x, S.One) + assert (x*y*z).as_base_exp() == (x*y*z, S.One) + assert (x + y + z).as_base_exp() == (x + y + z, S.One) + assert ((x + y)**z).as_base_exp() == (x + y, z) + assert (x**2*y**2).as_base_exp() == (x*y, 2) + assert (x**z*y**z).as_base_exp() == (x**z*y**z, S.One) + + +def test_issue_4963(): + assert hasattr(Mul(x, y), "is_commutative") + assert hasattr(Mul(x, y, evaluate=False), "is_commutative") + assert hasattr(Pow(x, y), "is_commutative") + assert hasattr(Pow(x, y, evaluate=False), "is_commutative") + expr = Mul(Pow(2, 2, evaluate=False), 3, evaluate=False) + 1 + assert hasattr(expr, "is_commutative") + + +def test_action_verbs(): + assert nsimplify(1/(exp(3*pi*x/5) + 1)) == \ + (1/(exp(3*pi*x/5) + 1)).nsimplify() + assert ratsimp(1/x + 1/y) == (1/x + 1/y).ratsimp() + assert trigsimp(log(x), deep=True) == (log(x)).trigsimp(deep=True) + assert radsimp(1/(2 + sqrt(2))) == (1/(2 + sqrt(2))).radsimp() + assert radsimp(1/(a + b*sqrt(c)), symbolic=False) == \ + (1/(a + b*sqrt(c))).radsimp(symbolic=False) + assert powsimp(x**y*x**z*y**z, combine='all') == \ + (x**y*x**z*y**z).powsimp(combine='all') + assert (x**t*y**t).powsimp(force=True) == (x*y)**t + assert simplify(x**y*x**z*y**z) == (x**y*x**z*y**z).simplify() + assert together(1/x + 1/y) == (1/x + 1/y).together() + assert collect(a*x**2 + b*x**2 + a*x - b*x + c, x) == \ + (a*x**2 + b*x**2 + a*x - b*x + c).collect(x) + assert apart(y/(y + 2)/(y + 1), y) == (y/(y + 2)/(y + 1)).apart(y) + assert combsimp(y/(x + 2)/(x + 1)) == (y/(x + 2)/(x + 1)).combsimp() + assert gammasimp(gamma(x)/gamma(x-5)) == (gamma(x)/gamma(x-5)).gammasimp() + assert factor(x**2 + 5*x + 6) == (x**2 + 5*x + 6).factor() + assert refine(sqrt(x**2)) == sqrt(x**2).refine() + assert cancel((x**2 + 5*x + 6)/(x + 2)) == ((x**2 + 5*x + 6)/(x + 2)).cancel() + + +def test_as_powers_dict(): + assert x.as_powers_dict() == {x: 1} + assert (x**y*z).as_powers_dict() == {x: y, z: 1} + assert Mul(2, 2, evaluate=False).as_powers_dict() == {S(2): S(2)} + assert (x*y).as_powers_dict()[z] == 0 + assert (x + y).as_powers_dict()[z] == 0 + + +def test_as_coefficients_dict(): + check = [S.One, x, y, x*y, 1] + assert [Add(3*x, 2*x, y, 3).as_coefficients_dict()[i] for i in check] == \ + [3, 5, 1, 0, 3] + assert [Add(3*x, 2*x, y, 3, evaluate=False).as_coefficients_dict()[i] + for i in check] == [3, 5, 1, 0, 3] + assert [(3*x*y).as_coefficients_dict()[i] for i in check] == \ + [0, 0, 0, 3, 0] + assert [(3.0*x*y).as_coefficients_dict()[i] for i in check] == \ + [0, 0, 0, 3.0, 0] + assert (3.0*x*y).as_coefficients_dict()[3.0*x*y] == 0 + eq = x*(x + 1)*a + x*b + c/x + assert eq.as_coefficients_dict(x) == {x: b, 1/x: c, + x*(x + 1): a} + assert eq.expand().as_coefficients_dict(x) == {x**2: a, x: a + b, 1/x: c} + assert x.as_coefficients_dict() == {x: S.One} + + +def test_args_cnc(): + A = symbols('A', commutative=False) + assert (x + A).args_cnc() == \ + [[], [x + A]] + assert (x + a).args_cnc() == \ + [[a + x], []] + assert (x*a).args_cnc() == \ + [[a, x], []] + assert (x*y*A*(A + 1)).args_cnc(cset=True) == \ + [{x, y}, [A, 1 + A]] + assert Mul(x, x, evaluate=False).args_cnc(cset=True, warn=False) == \ + [{x}, []] + assert Mul(x, x**2, evaluate=False).args_cnc(cset=True, warn=False) == \ + [{x, x**2}, []] + raises(ValueError, lambda: Mul(x, x, evaluate=False).args_cnc(cset=True)) + assert Mul(x, y, x, evaluate=False).args_cnc() == \ + [[x, y, x], []] + # always split -1 from leading number + assert (-1.*x).args_cnc() == [[-1, 1.0, x], []] + + +def test_new_rawargs(): + n = Symbol('n', commutative=False) + a = x + n + assert a.is_commutative is False + assert a._new_rawargs(x).is_commutative + assert a._new_rawargs(x, y).is_commutative + assert a._new_rawargs(x, n).is_commutative is False + assert a._new_rawargs(x, y, n).is_commutative is False + m = x*n + assert m.is_commutative is False + assert m._new_rawargs(x).is_commutative + assert m._new_rawargs(n).is_commutative is False + assert m._new_rawargs(x, y).is_commutative + assert m._new_rawargs(x, n).is_commutative is False + assert m._new_rawargs(x, y, n).is_commutative is False + + assert m._new_rawargs(x, n, reeval=False).is_commutative is False + assert m._new_rawargs(S.One) is S.One + + +def test_issue_5226(): + assert Add(evaluate=False) == 0 + assert Mul(evaluate=False) == 1 + assert Mul(x + y, evaluate=False).is_Add + + +def test_free_symbols(): + # free_symbols should return the free symbols of an object + assert S.One.free_symbols == set() + assert x.free_symbols == {x} + assert Integral(x, (x, 1, y)).free_symbols == {y} + assert (-Integral(x, (x, 1, y))).free_symbols == {y} + assert meter.free_symbols == set() + assert (meter**x).free_symbols == {x} + + +def test_has_free(): + assert x.has_free(x) + assert not x.has_free(y) + assert (x + y).has_free(x) + assert (x + y).has_free(*(x, z)) + assert f(x).has_free(x) + assert f(x).has_free(f(x)) + assert Integral(f(x), (f(x), 1, y)).has_free(y) + assert not Integral(f(x), (f(x), 1, y)).has_free(x) + assert not Integral(f(x), (f(x), 1, y)).has_free(f(x)) + # simple extraction + assert (x + 1 + y).has_free(x + 1) + assert not (x + 2 + y).has_free(x + 1) + assert (2 + 3*x*y).has_free(3*x) + raises(TypeError, lambda: x.has_free({x, y})) + s = FiniteSet(1, 2) + assert Piecewise((s, x > 3), (4, True)).has_free(s) + assert not Piecewise((1, x > 3), (4, True)).has_free(s) + # can't make set of these, but fallback will handle + raises(TypeError, lambda: x.has_free(y, [])) + + +def test_has_xfree(): + assert (x + 1).has_xfree({x}) + assert ((x + 1)**2).has_xfree({x + 1}) + assert not (x + y + 1).has_xfree({x + 1}) + raises(TypeError, lambda: x.has_xfree(x)) + raises(TypeError, lambda: x.has_xfree([x])) + + +def test_issue_5300(): + x = Symbol('x', commutative=False) + assert x*sqrt(2)/sqrt(6) == x*sqrt(3)/3 + + +def test_floordiv(): + from sympy.functions.elementary.integers import floor + assert x // y == floor(x / y) + + +def test_as_coeff_Mul(): + assert Integer(3).as_coeff_Mul() == (Integer(3), Integer(1)) + assert Rational(3, 4).as_coeff_Mul() == (Rational(3, 4), Integer(1)) + assert Float(5.0).as_coeff_Mul() == (Float(5.0), Integer(1)) + assert Float(0.0).as_coeff_Mul() == (Float(0.0), Integer(1)) + + assert (Integer(3)*x).as_coeff_Mul() == (Integer(3), x) + assert (Rational(3, 4)*x).as_coeff_Mul() == (Rational(3, 4), x) + assert (Float(5.0)*x).as_coeff_Mul() == (Float(5.0), x) + + assert (Integer(3)*x*y).as_coeff_Mul() == (Integer(3), x*y) + assert (Rational(3, 4)*x*y).as_coeff_Mul() == (Rational(3, 4), x*y) + assert (Float(5.0)*x*y).as_coeff_Mul() == (Float(5.0), x*y) + + assert (x).as_coeff_Mul() == (S.One, x) + assert (x*y).as_coeff_Mul() == (S.One, x*y) + assert (-oo*x).as_coeff_Mul(rational=True) == (-1, oo*x) + + +def test_as_coeff_Add(): + assert Integer(3).as_coeff_Add() == (Integer(3), Integer(0)) + assert Rational(3, 4).as_coeff_Add() == (Rational(3, 4), Integer(0)) + assert Float(5.0).as_coeff_Add() == (Float(5.0), Integer(0)) + + assert (Integer(3) + x).as_coeff_Add() == (Integer(3), x) + assert (Rational(3, 4) + x).as_coeff_Add() == (Rational(3, 4), x) + assert (Float(5.0) + x).as_coeff_Add() == (Float(5.0), x) + assert (Float(5.0) + x).as_coeff_Add(rational=True) == (0, Float(5.0) + x) + + assert (Integer(3) + x + y).as_coeff_Add() == (Integer(3), x + y) + assert (Rational(3, 4) + x + y).as_coeff_Add() == (Rational(3, 4), x + y) + assert (Float(5.0) + x + y).as_coeff_Add() == (Float(5.0), x + y) + + assert (x).as_coeff_Add() == (S.Zero, x) + assert (x*y).as_coeff_Add() == (S.Zero, x*y) + + +def test_expr_sorting(): + + exprs = [1/x**2, 1/x, sqrt(sqrt(x)), sqrt(x), x, sqrt(x)**3, x**2] + assert sorted(exprs, key=default_sort_key) == exprs + + exprs = [x, 2*x, 2*x**2, 2*x**3, x**n, 2*x**n, sin(x), sin(x)**n, + sin(x**2), cos(x), cos(x**2), tan(x)] + assert sorted(exprs, key=default_sort_key) == exprs + + exprs = [x + 1, x**2 + x + 1, x**3 + x**2 + x + 1] + assert sorted(exprs, key=default_sort_key) == exprs + + exprs = [S(4), x - 3*I/2, x + 3*I/2, x - 4*I + 1, x + 4*I + 1] + assert sorted(exprs, key=default_sort_key) == exprs + + exprs = [f(1), f(2), f(3), f(1, 2, 3), g(1), g(2), g(3), g(1, 2, 3)] + assert sorted(exprs, key=default_sort_key) == exprs + + exprs = [f(x), g(x), exp(x), sin(x), cos(x), factorial(x)] + assert sorted(exprs, key=default_sort_key) == exprs + + exprs = [Tuple(x, y), Tuple(x, z), Tuple(x, y, z)] + assert sorted(exprs, key=default_sort_key) == exprs + + exprs = [[3], [1, 2]] + assert sorted(exprs, key=default_sort_key) == exprs + + exprs = [[1, 2], [2, 3]] + assert sorted(exprs, key=default_sort_key) == exprs + + exprs = [[1, 2], [1, 2, 3]] + assert sorted(exprs, key=default_sort_key) == exprs + + exprs = [{x: -y}, {x: y}] + assert sorted(exprs, key=default_sort_key) == exprs + + exprs = [{1}, {1, 2}] + assert sorted(exprs, key=default_sort_key) == exprs + + a, b = exprs = [Dummy('x'), Dummy('x')] + assert sorted([b, a], key=default_sort_key) == exprs + + +def test_as_ordered_factors(): + + assert x.as_ordered_factors() == [x] + assert (2*x*x**n*sin(x)*cos(x)).as_ordered_factors() \ + == [Integer(2), x, x**n, sin(x), cos(x)] + + args = [f(1), f(2), f(3), f(1, 2, 3), g(1), g(2), g(3), g(1, 2, 3)] + expr = Mul(*args) + + assert expr.as_ordered_factors() == args + + A, B = symbols('A,B', commutative=False) + + assert (A*B).as_ordered_factors() == [A, B] + assert (B*A).as_ordered_factors() == [B, A] + + +def test_as_ordered_terms(): + + assert x.as_ordered_terms() == [x] + assert (sin(x)**2*cos(x) + sin(x)*cos(x)**2 + 1).as_ordered_terms() \ + == [sin(x)**2*cos(x), sin(x)*cos(x)**2, 1] + + args = [f(1), f(2), f(3), f(1, 2, 3), g(1), g(2), g(3), g(1, 2, 3)] + expr = Add(*args) + + assert expr.as_ordered_terms() == args + + assert (1 + 4*sqrt(3)*pi*x).as_ordered_terms() == [4*pi*x*sqrt(3), 1] + + assert ( 2 + 3*I).as_ordered_terms() == [2, 3*I] + assert (-2 + 3*I).as_ordered_terms() == [-2, 3*I] + assert ( 2 - 3*I).as_ordered_terms() == [2, -3*I] + assert (-2 - 3*I).as_ordered_terms() == [-2, -3*I] + + assert ( 4 + 3*I).as_ordered_terms() == [4, 3*I] + assert (-4 + 3*I).as_ordered_terms() == [-4, 3*I] + assert ( 4 - 3*I).as_ordered_terms() == [4, -3*I] + assert (-4 - 3*I).as_ordered_terms() == [-4, -3*I] + + e = x**2*y**2 + x*y**4 + y + 2 + + assert e.as_ordered_terms(order="lex") == [x**2*y**2, x*y**4, y, 2] + assert e.as_ordered_terms(order="grlex") == [x*y**4, x**2*y**2, y, 2] + assert e.as_ordered_terms(order="rev-lex") == [2, y, x*y**4, x**2*y**2] + assert e.as_ordered_terms(order="rev-grlex") == [2, y, x**2*y**2, x*y**4] + + k = symbols('k') + assert k.as_ordered_terms(data=True) == ([(k, ((1.0, 0.0), (1,), ()))], [k]) + + +def test_sort_key_atomic_expr(): + from sympy.physics.units import m, s + assert sorted([-m, s], key=lambda arg: arg.sort_key()) == [-m, s] + + +def test_eval_interval(): + assert exp(x)._eval_interval(*Tuple(x, 0, 1)) == exp(1) - exp(0) + + # issue 4199 + a = x/y + raises(NotImplementedError, lambda: a._eval_interval(x, S.Zero, oo)._eval_interval(y, oo, S.Zero)) + raises(NotImplementedError, lambda: a._eval_interval(x, S.Zero, oo)._eval_interval(y, S.Zero, oo)) + a = x - y + raises(NotImplementedError, lambda: a._eval_interval(x, S.One, oo)._eval_interval(y, oo, S.One)) + raises(ValueError, lambda: x._eval_interval(x, None, None)) + a = -y*Heaviside(x - y) + assert a._eval_interval(x, -oo, oo) == -y + assert a._eval_interval(x, oo, -oo) == y + + +def test_eval_interval_zoo(): + # Test that limit is used when zoo is returned + assert Si(1/x)._eval_interval(x, S.Zero, S.One) == -pi/2 + Si(1) + + +def test_primitive(): + assert (3*(x + 1)**2).primitive() == (3, (x + 1)**2) + assert (6*x + 2).primitive() == (2, 3*x + 1) + assert (x/2 + 3).primitive() == (S.Half, x + 6) + eq = (6*x + 2)*(x/2 + 3) + assert eq.primitive()[0] == 1 + eq = (2 + 2*x)**2 + assert eq.primitive()[0] == 1 + assert (4.0*x).primitive() == (1, 4.0*x) + assert (4.0*x + y/2).primitive() == (S.Half, 8.0*x + y) + assert (-2*x).primitive() == (2, -x) + assert Add(5*z/7, 0.5*x, 3*y/2, evaluate=False).primitive() == \ + (S.One/14, 7.0*x + 21*y + 10*z) + for i in [S.Infinity, S.NegativeInfinity, S.ComplexInfinity]: + assert (i + x/3).primitive() == \ + (S.One/3, i + x) + assert (S.Infinity + 2*x/3 + 4*y/7).primitive() == \ + (S.One/21, 14*x + 12*y + oo) + assert S.Zero.primitive() == (S.One, S.Zero) + + +def test_issue_5843(): + a = 1 + x + assert (2*a).extract_multiplicatively(a) == 2 + assert (4*a).extract_multiplicatively(2*a) == 2 + assert ((3*a)*(2*a)).extract_multiplicatively(a) == 6*a + + +def test_is_constant(): + from sympy.solvers.solvers import checksol + assert Sum(x, (x, 1, 10)).is_constant() is True + assert Sum(x, (x, 1, n)).is_constant() is False + assert Sum(x, (x, 1, n)).is_constant(y) is True + assert Sum(x, (x, 1, n)).is_constant(n) is False + assert Sum(x, (x, 1, n)).is_constant(x) is True + eq = a*cos(x)**2 + a*sin(x)**2 - a + assert eq.is_constant() is True + assert eq.subs({x: pi, a: 2}) == eq.subs({x: pi, a: 3}) == 0 + assert x.is_constant() is False + assert x.is_constant(y) is True + assert log(x/y).is_constant() is False + + assert checksol(x, x, Sum(x, (x, 1, n))) is False + assert checksol(x, x, Sum(x, (x, 1, n))) is False + assert f(1).is_constant + assert checksol(x, x, f(x)) is False + + assert Pow(x, S.Zero, evaluate=False).is_constant() is True # == 1 + assert Pow(S.Zero, x, evaluate=False).is_constant() is False # == 0 or 1 + assert (2**x).is_constant() is False + assert Pow(S(2), S(3), evaluate=False).is_constant() is True + + z1, z2 = symbols('z1 z2', zero=True) + assert (z1 + 2*z2).is_constant() is True + + assert meter.is_constant() is True + assert (3*meter).is_constant() is True + assert (x*meter).is_constant() is False + + +def test_equals(): + assert (-3 - sqrt(5) + (-sqrt(10)/2 - sqrt(2)/2)**2).equals(0) + assert (x**2 - 1).equals((x + 1)*(x - 1)) + assert (cos(x)**2 + sin(x)**2).equals(1) + assert (a*cos(x)**2 + a*sin(x)**2).equals(a) + r = sqrt(2) + assert (-1/(r + r*x) + 1/r/(1 + x)).equals(0) + assert factorial(x + 1).equals((x + 1)*factorial(x)) + assert sqrt(3).equals(2*sqrt(3)) is False + assert (sqrt(5)*sqrt(3)).equals(sqrt(3)) is False + assert (sqrt(5) + sqrt(3)).equals(0) is False + assert (sqrt(5) + pi).equals(0) is False + assert meter.equals(0) is False + assert (3*meter**2).equals(0) is False + eq = -(-1)**(S(3)/4)*6**(S.One/4) + (-6)**(S.One/4)*I + if eq != 0: # if canonicalization makes this zero, skip the test + assert eq.equals(0) + assert sqrt(x).equals(0) is False + + # from integrate(x*sqrt(1 + 2*x), x); + # diff is zero only when assumptions allow + i = 2*sqrt(2)*x**(S(5)/2)*(1 + 1/(2*x))**(S(5)/2)/5 + \ + 2*sqrt(2)*x**(S(3)/2)*(1 + 1/(2*x))**(S(5)/2)/(-6 - 3/x) + ans = sqrt(2*x + 1)*(6*x**2 + x - 1)/15 + diff = i - ans + assert diff.equals(0) is None # should be False, but previously this was False due to wrong intermediate result + assert diff.subs(x, Rational(-1, 2)/2) == 7*sqrt(2)/120 + # there are regions for x for which the expression is True, for + # example, when x < -1/2 or x > 0 the expression is zero + p = Symbol('p', positive=True) + assert diff.subs(x, p).equals(0) is True + assert diff.subs(x, -1).equals(0) is True + + # prove via minimal_polynomial or self-consistency + eq = sqrt(1 + sqrt(3)) + sqrt(3 + 3*sqrt(3)) - sqrt(10 + 6*sqrt(3)) + assert eq.equals(0) + q = 3**Rational(1, 3) + 3 + p = expand(q**3)**Rational(1, 3) + assert (p - q).equals(0) + + # issue 6829 + # eq = q*x + q/4 + x**4 + x**3 + 2*x**2 - S.One/3 + # z = eq.subs(x, solve(eq, x)[0]) + q = symbols('q') + z = (q*(-sqrt(-2*(-(q - S(7)/8)**S(2)/8 - S(2197)/13824)**(S.One/3) - + S(13)/12)/2 - sqrt((2*q - S(7)/4)/sqrt(-2*(-(q - S(7)/8)**S(2)/8 - + S(2197)/13824)**(S.One/3) - S(13)/12) + 2*(-(q - S(7)/8)**S(2)/8 - + S(2197)/13824)**(S.One/3) - S(13)/6)/2 - S.One/4) + q/4 + (-sqrt(-2*(-(q + - S(7)/8)**S(2)/8 - S(2197)/13824)**(S.One/3) - S(13)/12)/2 - sqrt((2*q + - S(7)/4)/sqrt(-2*(-(q - S(7)/8)**S(2)/8 - S(2197)/13824)**(S.One/3) - + S(13)/12) + 2*(-(q - S(7)/8)**S(2)/8 - S(2197)/13824)**(S.One/3) - + S(13)/6)/2 - S.One/4)**4 + (-sqrt(-2*(-(q - S(7)/8)**S(2)/8 - + S(2197)/13824)**(S.One/3) - S(13)/12)/2 - sqrt((2*q - + S(7)/4)/sqrt(-2*(-(q - S(7)/8)**S(2)/8 - S(2197)/13824)**(S.One/3) - + S(13)/12) + 2*(-(q - S(7)/8)**S(2)/8 - S(2197)/13824)**(S.One/3) - + S(13)/6)/2 - S.One/4)**3 + 2*(-sqrt(-2*(-(q - S(7)/8)**S(2)/8 - + S(2197)/13824)**(S.One/3) - S(13)/12)/2 - sqrt((2*q - + S(7)/4)/sqrt(-2*(-(q - S(7)/8)**S(2)/8 - S(2197)/13824)**(S.One/3) - + S(13)/12) + 2*(-(q - S(7)/8)**S(2)/8 - S(2197)/13824)**(S.One/3) - + S(13)/6)/2 - S.One/4)**2 - Rational(1, 3)) + assert z.equals(0) + + +def test_random(): + from sympy.functions.combinatorial.numbers import lucas + from sympy.simplify.simplify import posify + assert posify(x)[0]._random() is not None + assert lucas(n)._random(2, -2, 0, -1, 1) is None + + # issue 8662 + assert Piecewise((Max(x, y), z))._random() is None + + +def test_round(): + assert str(Float('0.1249999').round(2)) == '0.12' + d20 = 12345678901234567890 + ans = S(d20).round(2) + assert ans.is_Integer and ans == d20 + ans = S(d20).round(-2) + assert ans.is_Integer and ans == 12345678901234567900 + assert str(S('1/7').round(4)) == '0.1429' + assert str(S('.[12345]').round(4)) == '0.1235' + assert str(S('.1349').round(2)) == '0.13' + n = S(12345) + ans = n.round() + assert ans.is_Integer + assert ans == n + ans = n.round(1) + assert ans.is_Integer + assert ans == n + ans = n.round(4) + assert ans.is_Integer + assert ans == n + assert n.round(-1) == 12340 + + r = Float(str(n)).round(-4) + assert r == 10000.0 + + assert n.round(-5) == 0 + + assert str((pi + sqrt(2)).round(2)) == '4.56' + assert (10*(pi + sqrt(2))).round(-1) == 50.0 + raises(TypeError, lambda: round(x + 2, 2)) + assert str(S(2.3).round(1)) == '2.3' + # rounding in SymPy (as in Decimal) should be + # exact for the given precision; we check here + # that when a 5 follows the last digit that + # the rounded digit will be even. + for i in range(-99, 100): + # construct a decimal that ends in 5, e.g. 123 -> 0.1235 + s = str(abs(i)) + p = len(s) # we are going to round to the last digit of i + n = '0.%s5' % s # put a 5 after i's digits + j = p + 2 # 2 for '0.' + if i < 0: # 1 for '-' + j += 1 + n = '-' + n + v = str(Float(n).round(p))[:j] # pertinent digits + if v.endswith('.'): + continue # it ends with 0 which is even + L = int(v[-1]) # last digit + assert L % 2 == 0, (n, '->', v) + + assert (Float(.3, 3) + 2*pi).round() == 7 + assert (Float(.3, 3) + 2*pi*100).round() == 629 + assert (pi + 2*E*I).round() == 3 + 5*I + # don't let request for extra precision give more than + # what is known (in this case, only 3 digits) + assert str((Float(.03, 3) + 2*pi/100).round(5)) == '0.0928' + assert str((Float(.03, 3) + 2*pi/100).round(4)) == '0.0928' + + assert S.Zero.round() == 0 + + a = (Add(1, Float('1.' + '9'*27, ''), evaluate=False)) + assert a.round(10) == Float('3.000000000000000000000000000', '') + assert a.round(25) == Float('3.000000000000000000000000000', '') + assert a.round(26) == Float('3.000000000000000000000000000', '') + assert a.round(27) == Float('2.999999999999999999999999999', '') + assert a.round(30) == Float('2.999999999999999999999999999', '') + #assert a.round(10) == Float('3.0000000000', '') + #assert a.round(25) == Float('3.0000000000000000000000000', '') + #assert a.round(26) == Float('3.00000000000000000000000000', '') + #assert a.round(27) == Float('2.999999999999999999999999999', '') + #assert a.round(30) == Float('2.999999999999999999999999999', '') + + # XXX: Should round set the precision of the result? + # The previous version of the tests above is this but they only pass + # because Floats with unequal precision compare equal: + # + # assert a.round(10) == Float('3.0000000000', '') + # assert a.round(25) == Float('3.0000000000000000000000000', '') + # assert a.round(26) == Float('3.00000000000000000000000000', '') + # assert a.round(27) == Float('2.999999999999999999999999999', '') + # assert a.round(30) == Float('2.999999999999999999999999999', '') + + raises(TypeError, lambda: x.round()) + raises(TypeError, lambda: f(1).round()) + + # exact magnitude of 10 + assert str(S.One.round()) == '1' + assert str(S(100).round()) == '100' + + # applied to real and imaginary portions + assert (2*pi + E*I).round() == 6 + 3*I + assert (2*pi + I/10).round() == 6 + assert (pi/10 + 2*I).round() == 2*I + # the lhs re and im parts are Float with dps of 2 + # and those on the right have dps of 15 so they won't compare + # equal unless we use string or compare components (which will + # then coerce the floats to the same precision) or re-create + # the floats + assert str((pi/10 + E*I).round(2)) == '0.31 + 2.72*I' + assert str((pi/10 + E*I).round(2).as_real_imag()) == '(0.31, 2.72)' + assert str((pi/10 + E*I).round(2)) == '0.31 + 2.72*I' + + # issue 6914 + assert (I**(I + 3)).round(3) == Float('-0.208', '')*I + + # issue 8720 + assert S(-123.6).round() == -124 + assert S(-1.5).round() == -2 + assert S(-100.5).round() == -100 + assert S(-1.5 - 10.5*I).round() == -2 - 10*I + + # issue 7961 + assert str(S(0.006).round(2)) == '0.01' + assert str(S(0.00106).round(4)) == '0.0011' + + # issue 8147 + assert S.NaN.round() is S.NaN + assert S.Infinity.round() is S.Infinity + assert S.NegativeInfinity.round() is S.NegativeInfinity + assert S.ComplexInfinity.round() is S.ComplexInfinity + + # check that types match + for i in range(2): + fi = float(i) + # 2 args + assert all(type(round(i, p)) is int for p in (-1, 0, 1)) + assert all(S(i).round(p).is_Integer for p in (-1, 0, 1)) + assert all(type(round(fi, p)) is float for p in (-1, 0, 1)) + assert all(S(fi).round(p).is_Float for p in (-1, 0, 1)) + # 1 arg (p is None) + assert type(round(i)) is int + assert S(i).round().is_Integer + assert type(round(fi)) is int + assert S(fi).round().is_Integer + + # issue 25698 + n = 6000002 + assert int(n*(log(n) + log(log(n)))) == 110130079 + one = cos(2)**2 + sin(2)**2 + eq = exp(one*I*pi) + qr, qi = eq.as_real_imag() + assert qi.round(2) == 0.0 + assert eq.round(2) == -1.0 + eq = one - 1/S(10**120) + assert S.true not in (eq > 1, eq < 1) + assert int(eq) == int(.9) == 0 + assert int(-eq) == int(-.9) == 0 + + +def test_held_expression_UnevaluatedExpr(): + x = symbols("x") + he = UnevaluatedExpr(1/x) + e1 = x*he + + assert isinstance(e1, Mul) + assert e1.args == (x, he) + assert e1.doit() == 1 + assert UnevaluatedExpr(Derivative(x, x)).doit(deep=False + ) == Derivative(x, x) + assert UnevaluatedExpr(Derivative(x, x)).doit() == 1 + + xx = Mul(x, x, evaluate=False) + assert xx != x**2 + + ue2 = UnevaluatedExpr(xx) + assert isinstance(ue2, UnevaluatedExpr) + assert ue2.args == (xx,) + assert ue2.doit() == x**2 + assert ue2.doit(deep=False) == xx + + x2 = UnevaluatedExpr(2)*2 + assert type(x2) is Mul + assert x2.args == (2, UnevaluatedExpr(2)) + +def test_round_exception_nostr(): + # Don't use the string form of the expression in the round exception, as + # it's too slow + s = Symbol('bad') + try: + s.round() + except TypeError as e: + assert 'bad' not in str(e) + else: + # Did not raise + raise AssertionError("Did not raise") + + +def test_extract_branch_factor(): + assert exp_polar(2.0*I*pi).extract_branch_factor() == (1, 1) + + +def test_identity_removal(): + assert Add.make_args(x + 0) == (x,) + assert Mul.make_args(x*1) == (x,) + + +def test_float_0(): + assert Float(0.0) + 1 == Float(1.0) + + +@XFAIL +def test_float_0_fail(): + assert Float(0.0)*x == Float(0.0) + assert (x + Float(0.0)).is_Add + + +def test_issue_6325(): + ans = (b**2 + z**2 - (b*(a + b*t) + z*(c + t*z))**2/( + (a + b*t)**2 + (c + t*z)**2))/sqrt((a + b*t)**2 + (c + t*z)**2) + e = sqrt((a + b*t)**2 + (c + z*t)**2) + assert diff(e, t, 2) == ans + assert e.diff(t, 2) == ans + assert diff(e, t, 2, simplify=False) != ans + + +def test_issue_7426(): + f1 = a % c + f2 = x % z + assert f1.equals(f2) is None + + +def test_issue_11122(): + x = Symbol('x', extended_positive=False) + assert unchanged(Gt, x, 0) # (x > 0) + # (x > 0) should remain unevaluated after PR #16956 + + x = Symbol('x', positive=False, real=True) + assert (x > 0) is S.false + + +def test_issue_10651(): + x = Symbol('x', real=True) + e1 = (-1 + x)/(1 - x) + e3 = (4*x**2 - 4)/((1 - x)*(1 + x)) + e4 = 1/(cos(x)**2) - (tan(x))**2 + x = Symbol('x', positive=True) + e5 = (1 + x)/x + assert e1.is_constant() is None + assert e3.is_constant() is None + assert e4.is_constant() is None + assert e5.is_constant() is False + + +def test_issue_10161(): + x = symbols('x', real=True) + assert x*abs(x)*abs(x) == x**3 + + +def test_issue_10755(): + x = symbols('x') + raises(TypeError, lambda: int(log(x))) + raises(TypeError, lambda: log(x).round(2)) + + +def test_issue_11877(): + x = symbols('x') + assert integrate(log(S.Half - x), (x, 0, S.Half)) == Rational(-1, 2) -log(2)/2 + + +def test_normal(): + x = symbols('x') + e = Mul(S.Half, 1 + x, evaluate=False) + assert e.normal() == e + + +def test_expr(): + x = symbols('x') + raises(TypeError, lambda: tan(x).series(x, 2, oo, "+")) + + +def test_ExprBuilder(): + eb = ExprBuilder(Mul) + eb.args.extend([x, x]) + assert eb.build() == x**2 + + +def test_issue_22020(): + from sympy.parsing.sympy_parser import parse_expr + x = parse_expr("log((2*V/3-V)/C)/-(R+r)*C") + y = parse_expr("log((2*V/3-V)/C)/-(R+r)*2") + assert x.equals(y) is False + + +def test_non_string_equality(): + # Expressions should not compare equal to strings + x = symbols('x') + one = sympify(1) + assert (x == 'x') is False + assert (x != 'x') is True + assert (one == '1') is False + assert (one != '1') is True + assert (x + 1 == 'x + 1') is False + assert (x + 1 != 'x + 1') is True + + # Make sure == doesn't try to convert the resulting expression to a string + # (e.g., by calling sympify() instead of _sympify()) + + class BadRepr: + def __repr__(self): + raise RuntimeError + + assert (x == BadRepr()) is False + assert (x != BadRepr()) is True + + +def test_21494(): + from sympy.testing.pytest import warns_deprecated_sympy + + with warns_deprecated_sympy(): + assert x.expr_free_symbols == {x} + + with warns_deprecated_sympy(): + assert Basic().expr_free_symbols == set() + + with warns_deprecated_sympy(): + assert S(2).expr_free_symbols == {S(2)} + + with warns_deprecated_sympy(): + assert Indexed("A", x).expr_free_symbols == {Indexed("A", x)} + + with warns_deprecated_sympy(): + assert Subs(x, x, 0).expr_free_symbols == set() + + +def test_Expr__eq__iterable_handling(): + assert x != range(3) + + +def test_format(): + assert '{:1.2f}'.format(S.Zero) == '0.00' + assert '{:+3.0f}'.format(S(3)) == ' +3' + assert '{:23.20f}'.format(pi) == ' 3.14159265358979323846' + assert '{:50.48f}'.format(exp(sin(1))) == '2.319776824715853173956590377503266813254904772376' + + +def test_issue_24045(): + assert powsimp(exp(a)/((c*a - c*b)*(Float(1.0)*c*a - Float(1.0)*c*b))) # doesn't raise + + +def test__unevaluated_Mul(): + A, B = symbols('A B', commutative=False) + assert _unevaluated_Mul(x, A, B, S(2), A).args == (2, x, A, B, A) + assert _unevaluated_Mul(-x*A*B, S(2), A).args == (-2, x, A, B, A) + + +def test_Float_zero_division_error(): + # issue 27165 + assert Float('1.7567e-1417').round(15) == Float(0) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_exprtools.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_exprtools.py new file mode 100644 index 0000000000000000000000000000000000000000..b550db1606866fb76442980ea2139aaf61219525 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_exprtools.py @@ -0,0 +1,493 @@ +"""Tests for tools for manipulating of large commutative expressions. """ + +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.containers import (Dict, Tuple) +from sympy.core.function import Function +from sympy.core.mul import Mul +from sympy.core.numbers import (I, Rational, oo) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import (root, sqrt) +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.integrals.integrals import Integral +from sympy.series.order import O +from sympy.sets.sets import Interval +from sympy.simplify.radsimp import collect +from sympy.simplify.simplify import simplify +from sympy.core.exprtools import (decompose_power, Factors, Term, _gcd_terms, + gcd_terms, factor_terms, factor_nc, _mask_nc, + _monotonic_sign) +from sympy.core.mul import _keep_coeff as _keep_coeff +from sympy.simplify.cse_opts import sub_pre +from sympy.testing.pytest import raises + +from sympy.abc import a, b, t, x, y, z + + +def test_decompose_power(): + assert decompose_power(x) == (x, 1) + assert decompose_power(x**2) == (x, 2) + assert decompose_power(x**(2*y)) == (x**y, 2) + assert decompose_power(x**(2*y/3)) == (x**(y/3), 2) + assert decompose_power(x**(y*Rational(2, 3))) == (x**(y/3), 2) + + +def test_Factors(): + assert Factors() == Factors({}) == Factors(S.One) + assert Factors().as_expr() is S.One + assert Factors({x: 2, y: 3, sin(x): 4}).as_expr() == x**2*y**3*sin(x)**4 + assert Factors(S.Infinity) == Factors({oo: 1}) + assert Factors(S.NegativeInfinity) == Factors({oo: 1, -1: 1}) + # issue #18059: + assert Factors((x**2)**S.Half).as_expr() == (x**2)**S.Half + + a = Factors({x: 5, y: 3, z: 7}) + b = Factors({ y: 4, z: 3, t: 10}) + + assert a.mul(b) == a*b == Factors({x: 5, y: 7, z: 10, t: 10}) + + assert a.div(b) == divmod(a, b) == \ + (Factors({x: 5, z: 4}), Factors({y: 1, t: 10})) + assert a.quo(b) == a/b == Factors({x: 5, z: 4}) + assert a.rem(b) == a % b == Factors({y: 1, t: 10}) + + assert a.pow(3) == a**3 == Factors({x: 15, y: 9, z: 21}) + assert b.pow(3) == b**3 == Factors({y: 12, z: 9, t: 30}) + + assert a.gcd(b) == Factors({y: 3, z: 3}) + assert a.lcm(b) == Factors({x: 5, y: 4, z: 7, t: 10}) + + a = Factors({x: 4, y: 7, t: 7}) + b = Factors({z: 1, t: 3}) + + assert a.normal(b) == (Factors({x: 4, y: 7, t: 4}), Factors({z: 1})) + + assert Factors(sqrt(2)*x).as_expr() == sqrt(2)*x + + assert Factors(-I)*I == Factors() + assert Factors({S.NegativeOne: S(3)})*Factors({S.NegativeOne: S.One, I: S(5)}) == \ + Factors(I) + assert Factors(sqrt(I)*I) == Factors(I**(S(3)/2)) == Factors({I: S(3)/2}) + assert Factors({I: S(3)/2}).as_expr() == I**(S(3)/2) + + assert Factors(S(2)**x).div(S(3)**x) == \ + (Factors({S(2): x}), Factors({S(3): x})) + assert Factors(2**(2*x + 2)).div(S(8)) == \ + (Factors({S(2): 2*x + 2}), Factors({S(8): S.One})) + + # coverage + # /!\ things break if this is not True + assert Factors({S.NegativeOne: Rational(3, 2)}) == Factors({I: S.One, S.NegativeOne: S.One}) + assert Factors({I: S.One, S.NegativeOne: Rational(1, 3)}).as_expr() == I*(-1)**Rational(1, 3) + + assert Factors(-1.) == Factors({S.NegativeOne: S.One, S(1.): 1}) + assert Factors(-2.) == Factors({S.NegativeOne: S.One, S(2.): 1}) + assert Factors((-2.)**x) == Factors({S(-2.): x}) + assert Factors(S(-2)) == Factors({S.NegativeOne: S.One, S(2): 1}) + assert Factors(S.Half) == Factors({S(2): -S.One}) + assert Factors(Rational(3, 2)) == Factors({S(3): S.One, S(2): S.NegativeOne}) + assert Factors({I: S.One}) == Factors(I) + assert Factors({-1.0: 2, I: 1}) == Factors({S(1.0): 1, I: 1}) + assert Factors({S.NegativeOne: Rational(-3, 2)}).as_expr() == I + A = symbols('A', commutative=False) + assert Factors(2*A**2) == Factors({S(2): 1, A**2: 1}) + assert Factors(I) == Factors({I: S.One}) + assert Factors(x).normal(S(2)) == (Factors(x), Factors(S(2))) + assert Factors(x).normal(S.Zero) == (Factors(), Factors(S.Zero)) + raises(ZeroDivisionError, lambda: Factors(x).div(S.Zero)) + assert Factors(x).mul(S(2)) == Factors(2*x) + assert Factors(x).mul(S.Zero).is_zero + assert Factors(x).mul(1/x).is_one + assert Factors(x**sqrt(2)**3).as_expr() == x**(2*sqrt(2)) + assert Factors(x)**Factors(S(2)) == Factors(x**2) + assert Factors(x).gcd(S.Zero) == Factors(x) + assert Factors(x).lcm(S.Zero).is_zero + assert Factors(S.Zero).div(x) == (Factors(S.Zero), Factors()) + assert Factors(x).div(x) == (Factors(), Factors()) + assert Factors({x: .2})/Factors({x: .2}) == Factors() + assert Factors(x) != Factors() + assert Factors(S.Zero).normal(x) == (Factors(S.Zero), Factors()) + n, d = x**(2 + y), x**2 + f = Factors(n) + assert f.div(d) == f.normal(d) == (Factors(x**y), Factors()) + assert f.gcd(d) == Factors() + d = x**y + assert f.div(d) == f.normal(d) == (Factors(x**2), Factors()) + assert f.gcd(d) == Factors(d) + n = d = 2**x + f = Factors(n) + assert f.div(d) == f.normal(d) == (Factors(), Factors()) + assert f.gcd(d) == Factors(d) + n, d = 2**x, 2**y + f = Factors(n) + assert f.div(d) == f.normal(d) == (Factors({S(2): x}), Factors({S(2): y})) + assert f.gcd(d) == Factors() + + # extraction of constant only + n = x**(x + 3) + assert Factors(n).normal(x**-3) == (Factors({x: x + 6}), Factors({})) + assert Factors(n).normal(x**3) == (Factors({x: x}), Factors({})) + assert Factors(n).normal(x**4) == (Factors({x: x}), Factors({x: 1})) + assert Factors(n).normal(x**(y - 3)) == \ + (Factors({x: x + 6}), Factors({x: y})) + assert Factors(n).normal(x**(y + 3)) == (Factors({x: x}), Factors({x: y})) + assert Factors(n).normal(x**(y + 4)) == \ + (Factors({x: x}), Factors({x: y + 1})) + + assert Factors(n).div(x**-3) == (Factors({x: x + 6}), Factors({})) + assert Factors(n).div(x**3) == (Factors({x: x}), Factors({})) + assert Factors(n).div(x**4) == (Factors({x: x}), Factors({x: 1})) + assert Factors(n).div(x**(y - 3)) == \ + (Factors({x: x + 6}), Factors({x: y})) + assert Factors(n).div(x**(y + 3)) == (Factors({x: x}), Factors({x: y})) + assert Factors(n).div(x**(y + 4)) == \ + (Factors({x: x}), Factors({x: y + 1})) + + assert Factors(3 * x / 2) == Factors({3: 1, 2: -1, x: 1}) + assert Factors(x * x / y) == Factors({x: 2, y: -1}) + assert Factors(27 * x / y**9) == Factors({27: 1, x: 1, y: -9}) + + +def test_Term(): + a = Term(4*x*y**2/z/t**3) + b = Term(2*x**3*y**5/t**3) + + assert a == Term(4, Factors({x: 1, y: 2}), Factors({z: 1, t: 3})) + assert b == Term(2, Factors({x: 3, y: 5}), Factors({t: 3})) + + assert a.as_expr() == 4*x*y**2/z/t**3 + assert b.as_expr() == 2*x**3*y**5/t**3 + + assert a.inv() == \ + Term(S.One/4, Factors({z: 1, t: 3}), Factors({x: 1, y: 2})) + assert b.inv() == Term(S.Half, Factors({t: 3}), Factors({x: 3, y: 5})) + + assert a.mul(b) == a*b == \ + Term(8, Factors({x: 4, y: 7}), Factors({z: 1, t: 6})) + assert a.quo(b) == a/b == Term(2, Factors({}), Factors({x: 2, y: 3, z: 1})) + + assert a.pow(3) == a**3 == \ + Term(64, Factors({x: 3, y: 6}), Factors({z: 3, t: 9})) + assert b.pow(3) == b**3 == Term(8, Factors({x: 9, y: 15}), Factors({t: 9})) + + assert a.pow(-3) == a**(-3) == \ + Term(S.One/64, Factors({z: 3, t: 9}), Factors({x: 3, y: 6})) + assert b.pow(-3) == b**(-3) == \ + Term(S.One/8, Factors({t: 9}), Factors({x: 9, y: 15})) + + assert a.gcd(b) == Term(2, Factors({x: 1, y: 2}), Factors({t: 3})) + assert a.lcm(b) == Term(4, Factors({x: 3, y: 5}), Factors({z: 1, t: 3})) + + a = Term(4*x*y**2/z/t**3) + b = Term(2*x**3*y**5*t**7) + + assert a.mul(b) == Term(8, Factors({x: 4, y: 7, t: 4}), Factors({z: 1})) + + assert Term((2*x + 2)**3) == Term(8, Factors({x + 1: 3}), Factors({})) + assert Term((2*x + 2)*(3*x + 6)**2) == \ + Term(18, Factors({x + 1: 1, x + 2: 2}), Factors({})) + + +def test_gcd_terms(): + f = 2*(x + 1)*(x + 4)/(5*x**2 + 5) + (2*x + 2)*(x + 5)/(x**2 + 1)/5 + \ + (2*x + 2)*(x + 6)/(5*x**2 + 5) + + assert _gcd_terms(f) == ((Rational(6, 5))*((1 + x)/(1 + x**2)), 5 + x, 1) + assert _gcd_terms(Add.make_args(f)) == \ + ((Rational(6, 5))*((1 + x)/(1 + x**2)), 5 + x, 1) + + newf = (Rational(6, 5))*((1 + x)*(5 + x)/(1 + x**2)) + assert gcd_terms(f) == newf + args = Add.make_args(f) + # non-Basic sequences of terms treated as terms of Add + assert gcd_terms(list(args)) == newf + assert gcd_terms(tuple(args)) == newf + assert gcd_terms(set(args)) == newf + # but a Basic sequence is treated as a container + assert gcd_terms(Tuple(*args)) != newf + assert gcd_terms(Basic(Tuple(S(1), 3*y + 3*x*y), Tuple(S(1), S(3)))) == \ + Basic(Tuple(S(1), 3*y*(x + 1)), Tuple(S(1), S(3))) + # but we shouldn't change keys of a dictionary or some may be lost + assert gcd_terms(Dict((x*(1 + y), S(2)), (x + x*y, y + x*y))) == \ + Dict({x*(y + 1): S(2), x + x*y: y*(1 + x)}) + + assert gcd_terms((2*x + 2)**3 + (2*x + 2)**2) == 4*(x + 1)**2*(2*x + 3) + + assert gcd_terms(0) == 0 + assert gcd_terms(1) == 1 + assert gcd_terms(x) == x + assert gcd_terms(2 + 2*x) == Mul(2, 1 + x, evaluate=False) + arg = x*(2*x + 4*y) + garg = 2*x*(x + 2*y) + assert gcd_terms(arg) == garg + assert gcd_terms(sin(arg)) == sin(garg) + + # issue 6139-like + alpha, alpha1, alpha2, alpha3 = symbols('alpha:4') + a = alpha**2 - alpha*x**2 + alpha + x**3 - x*(alpha + 1) + rep = (alpha, (1 + sqrt(5))/2 + alpha1*x + alpha2*x**2 + alpha3*x**3) + s = (a/(x - alpha)).subs(*rep).series(x, 0, 1) + assert simplify(collect(s, x)) == -sqrt(5)/2 - Rational(3, 2) + O(x) + + # issue 5917 + assert _gcd_terms([S.Zero, S.Zero]) == (0, 0, 1) + assert _gcd_terms([2*x + 4]) == (2, x + 2, 1) + + eq = x/(x + 1/x) + assert gcd_terms(eq, fraction=False) == eq + eq = x/2/y + 1/x/y + assert gcd_terms(eq, fraction=True, clear=True) == \ + (x**2 + 2)/(2*x*y) + assert gcd_terms(eq, fraction=True, clear=False) == \ + (x**2/2 + 1)/(x*y) + assert gcd_terms(eq, fraction=False, clear=True) == \ + (x + 2/x)/(2*y) + assert gcd_terms(eq, fraction=False, clear=False) == \ + (x/2 + 1/x)/y + + +def test_factor_terms(): + A = Symbol('A', commutative=False) + assert factor_terms(9*(x + x*y + 1) + (3*x + 3)**(2 + 2*x)) == \ + 9*x*y + 9*x + _keep_coeff(S(3), x + 1)**_keep_coeff(S(2), x + 1) + 9 + assert factor_terms(9*(x + x*y + 1) + (3)**(2 + 2*x)) == \ + _keep_coeff(S(9), 3**(2*x) + x*y + x + 1) + assert factor_terms(3**(2 + 2*x) + a*3**(2 + 2*x)) == \ + 9*3**(2*x)*(a + 1) + assert factor_terms(x + x*A) == \ + x*(1 + A) + assert factor_terms(sin(x + x*A)) == \ + sin(x*(1 + A)) + assert factor_terms((3*x + 3)**((2 + 2*x)/3)) == \ + _keep_coeff(S(3), x + 1)**_keep_coeff(Rational(2, 3), x + 1) + assert factor_terms(x + (x*y + x)**(3*x + 3)) == \ + x + (x*(y + 1))**_keep_coeff(S(3), x + 1) + assert factor_terms(a*(x + x*y) + b*(x*2 + y*x*2)) == \ + x*(a + 2*b)*(y + 1) + i = Integral(x, (x, 0, oo)) + assert factor_terms(i) == i + + assert factor_terms(x/2 + y) == x/2 + y + # fraction doesn't apply to integer denominators + assert factor_terms(x/2 + y, fraction=True) == x/2 + y + # clear *does* apply to the integer denominators + assert factor_terms(x/2 + y, clear=True) == Mul(S.Half, x + 2*y, evaluate=False) + + # check radical extraction + eq = sqrt(2) + sqrt(10) + assert factor_terms(eq) == eq + assert factor_terms(eq, radical=True) == sqrt(2)*(1 + sqrt(5)) + eq = root(-6, 3) + root(6, 3) + assert factor_terms(eq, radical=True) == 6**(S.One/3)*(1 + (-1)**(S.One/3)) + + eq = [x + x*y] + ans = [x*(y + 1)] + for c in [list, tuple, set]: + assert factor_terms(c(eq)) == c(ans) + assert factor_terms(Tuple(x + x*y)) == Tuple(x*(y + 1)) + assert factor_terms(Interval(0, 1)) == Interval(0, 1) + e = 1/sqrt(a/2 + 1) + assert factor_terms(e, clear=False) == 1/sqrt(a/2 + 1) + assert factor_terms(e, clear=True) == sqrt(2)/sqrt(a + 2) + + eq = x/(x + 1/x) + 1/(x**2 + 1) + assert factor_terms(eq, fraction=False) == eq + assert factor_terms(eq, fraction=True) == 1 + + assert factor_terms((1/(x**3 + x**2) + 2/x**2)*y) == \ + y*(2 + 1/(x + 1))/x**2 + + # if not True, then processesing for this in factor_terms is not necessary + assert gcd_terms(-x - y) == -x - y + assert factor_terms(-x - y) == Mul(-1, x + y, evaluate=False) + + # if not True, then "special" processesing in factor_terms is not necessary + assert gcd_terms(exp(Mul(-1, x + 1))) == exp(-x - 1) + e = exp(-x - 2) + x + assert factor_terms(e) == exp(Mul(-1, x + 2, evaluate=False)) + x + assert factor_terms(e, sign=False) == e + assert factor_terms(exp(-4*x - 2) - x) == -x + exp(Mul(-2, 2*x + 1, evaluate=False)) + + # sum/integral tests + for F in (Sum, Integral): + assert factor_terms(F(x, (y, 1, 10))) == x * F(1, (y, 1, 10)) + assert factor_terms(F(x, (y, 1, 10)) + x) == x * (1 + F(1, (y, 1, 10))) + assert factor_terms(F(x*y + x*y**2, (y, 1, 10))) == x*F(y*(y + 1), (y, 1, 10)) + + # expressions involving Pow terms with base 0 + assert factor_terms(0**(x - 2) - 1) == 0**(x - 2) - 1 + assert factor_terms(0**(x + 2) - 1) == 0**(x + 2) - 1 + assert factor_terms((0**(x + 2) - 1).subs(x,-2)) == 0 + + +def test_xreplace(): + e = Mul(2, 1 + x, evaluate=False) + assert e.xreplace({}) == e + assert e.xreplace({y: x}) == e + + +def test_factor_nc(): + x, y = symbols('x,y') + k = symbols('k', integer=True) + n, m, o = symbols('n,m,o', commutative=False) + + # mul and multinomial expansion is needed + from sympy.core.function import _mexpand + e = x*(1 + y)**2 + assert _mexpand(e) == x + x*2*y + x*y**2 + + def factor_nc_test(e): + ex = _mexpand(e) + assert ex.is_Add + f = factor_nc(ex) + assert not f.is_Add and _mexpand(f) == ex + + factor_nc_test(x*(1 + y)) + factor_nc_test(n*(x + 1)) + factor_nc_test(n*(x + m)) + factor_nc_test((x + m)*n) + factor_nc_test(n*m*(x*o + n*o*m)*n) + s = Sum(x, (x, 1, 2)) + factor_nc_test(x*(1 + s)) + factor_nc_test(x*(1 + s)*s) + factor_nc_test(x*(1 + sin(s))) + factor_nc_test((1 + n)**2) + + factor_nc_test((x + n)*(x + m)*(x + y)) + factor_nc_test(x*(n*m + 1)) + factor_nc_test(x*(n*m + x)) + factor_nc_test(x*(x*n*m + 1)) + factor_nc_test(n*(m/x + o)) + factor_nc_test(m*(n + o/2)) + factor_nc_test(x*n*(x*m + 1)) + factor_nc_test(x*(m*n + x*n*m)) + factor_nc_test(n*(1 - m)*n**2) + + factor_nc_test((n + m)**2) + factor_nc_test((n - m)*(n + m)**2) + factor_nc_test((n + m)**2*(n - m)) + factor_nc_test((m - n)*(n + m)**2*(n - m)) + + assert factor_nc(n*(n + n*m)) == n**2*(1 + m) + assert factor_nc(m*(m*n + n*m*n**2)) == m*(m + n*m*n)*n + eq = m*sin(n) - sin(n)*m + assert factor_nc(eq) == eq + + # for coverage: + from sympy.physics.secondquant import Commutator + from sympy.polys.polytools import factor + eq = 1 + x*Commutator(m, n) + assert factor_nc(eq) == eq + eq = x*Commutator(m, n) + x*Commutator(m, o)*Commutator(m, n) + assert factor(eq) == x*(1 + Commutator(m, o))*Commutator(m, n) + + # issue 6534 + assert (2*n + 2*m).factor() == 2*(n + m) + + # issue 6701 + _n = symbols('nz', zero=False, commutative=False) + assert factor_nc(_n**k + _n**(k + 1)) == _n**k*(1 + _n) + assert factor_nc((m*n)**k + (m*n)**(k + 1)) == (1 + m*n)*(m*n)**k + + # issue 6918 + assert factor_nc(-n*(2*x**2 + 2*x)) == -2*n*x*(x + 1) + + +def test_issue_6360(): + a, b = symbols("a b") + apb = a + b + eq = apb + apb**2*(-2*a - 2*b) + assert factor_terms(sub_pre(eq)) == a + b - 2*(a + b)**3 + + +def test_issue_7903(): + a = symbols(r'a', real=True) + t = exp(I*cos(a)) + exp(-I*sin(a)) + assert t.simplify() + +def test_issue_8263(): + F, G = symbols('F, G', commutative=False, cls=Function) + x, y = symbols('x, y') + expr, dummies, _ = _mask_nc(F(x)*G(y) - G(y)*F(x)) + for v in dummies.values(): + assert not v.is_commutative + assert not expr.is_zero + +def test_monotonic_sign(): + F = _monotonic_sign + x = symbols('x') + assert F(x) is None + assert F(-x) is None + assert F(Dummy(prime=True)) == 2 + assert F(Dummy(prime=True, odd=True)) == 3 + assert F(Dummy(composite=True)) == 4 + assert F(Dummy(composite=True, odd=True)) == 9 + assert F(Dummy(positive=True, integer=True)) == 1 + assert F(Dummy(positive=True, even=True)) == 2 + assert F(Dummy(positive=True, even=True, prime=False)) == 4 + assert F(Dummy(negative=True, integer=True)) == -1 + assert F(Dummy(negative=True, even=True)) == -2 + assert F(Dummy(zero=True)) == 0 + assert F(Dummy(nonnegative=True)) == 0 + assert F(Dummy(nonpositive=True)) == 0 + + assert F(Dummy(positive=True) + 1).is_positive + assert F(Dummy(positive=True, integer=True) - 1).is_nonnegative + assert F(Dummy(positive=True) - 1) is None + assert F(Dummy(negative=True) + 1) is None + assert F(Dummy(negative=True, integer=True) - 1).is_nonpositive + assert F(Dummy(negative=True) - 1).is_negative + assert F(-Dummy(positive=True) + 1) is None + assert F(-Dummy(positive=True, integer=True) - 1).is_negative + assert F(-Dummy(positive=True) - 1).is_negative + assert F(-Dummy(negative=True) + 1).is_positive + assert F(-Dummy(negative=True, integer=True) - 1).is_nonnegative + assert F(-Dummy(negative=True) - 1) is None + x = Dummy(negative=True) + assert F(x**3).is_nonpositive + assert F(x**3 + log(2)*x - 1).is_negative + x = Dummy(positive=True) + assert F(-x**3).is_nonpositive + + p = Dummy(positive=True) + assert F(1/p).is_positive + assert F(p/(p + 1)).is_positive + p = Dummy(nonnegative=True) + assert F(p/(p + 1)).is_nonnegative + p = Dummy(positive=True) + assert F(-1/p).is_negative + p = Dummy(nonpositive=True) + assert F(p/(-p + 1)).is_nonpositive + + p = Dummy(positive=True, integer=True) + q = Dummy(positive=True, integer=True) + assert F(-2/p/q).is_negative + assert F(-2/(p - 1)/q) is None + + assert F((p - 1)*q + 1).is_positive + assert F(-(p - 1)*q - 1).is_negative + +def test_issue_17256(): + from sympy.sets.fancysets import Range + x = Symbol('x') + s1 = Sum(x + 1, (x, 1, 9)) + s2 = Sum(x + 1, (x, Range(1, 10))) + a = Symbol('a') + r1 = s1.xreplace({x:a}) + r2 = s2.xreplace({x:a}) + + assert r1.doit() == r2.doit() + s1 = Sum(x + 1, (x, 0, 9)) + s2 = Sum(x + 1, (x, Range(10))) + a = Symbol('a') + r1 = s1.xreplace({x:a}) + r2 = s2.xreplace({x:a}) + assert r1 == r2 + +def test_issue_21623(): + from sympy.matrices.expressions.matexpr import MatrixSymbol + M = MatrixSymbol('X', 2, 2) + assert gcd_terms(M[0,0], 1) == M[0,0] diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_facts.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_facts.py new file mode 100644 index 0000000000000000000000000000000000000000..7ca04877d0bdaf8124258ea1d25a10bcfa0f5f3a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_facts.py @@ -0,0 +1,312 @@ +from sympy.core.facts import (deduce_alpha_implications, + apply_beta_to_alpha_route, rules_2prereq, FactRules, FactKB) +from sympy.core.logic import And, Not +from sympy.testing.pytest import raises + +T = True +F = False +U = None + + +def test_deduce_alpha_implications(): + def D(i): + I = deduce_alpha_implications(i) + P = rules_2prereq({ + (k, True): {(v, True) for v in S} for k, S in I.items()}) + return I, P + + # transitivity + I, P = D([('a', 'b'), ('b', 'c')]) + assert I == {'a': {'b', 'c'}, 'b': {'c'}, Not('b'): + {Not('a')}, Not('c'): {Not('a'), Not('b')}} + assert P == {'a': {'b', 'c'}, 'b': {'a', 'c'}, 'c': {'a', 'b'}} + + # Duplicate entry + I, P = D([('a', 'b'), ('b', 'c'), ('b', 'c')]) + assert I == {'a': {'b', 'c'}, 'b': {'c'}, Not('b'): {Not('a')}, Not('c'): {Not('a'), Not('b')}} + assert P == {'a': {'b', 'c'}, 'b': {'a', 'c'}, 'c': {'a', 'b'}} + + # see if it is tolerant to cycles + assert D([('a', 'a'), ('a', 'a')]) == ({}, {}) + assert D([('a', 'b'), ('b', 'a')]) == ( + {'a': {'b'}, 'b': {'a'}, Not('a'): {Not('b')}, Not('b'): {Not('a')}}, + {'a': {'b'}, 'b': {'a'}}) + + # see if it catches inconsistency + raises(ValueError, lambda: D([('a', Not('a'))])) + raises(ValueError, lambda: D([('a', 'b'), ('b', Not('a'))])) + raises(ValueError, lambda: D([('a', 'b'), ('b', 'c'), ('b', 'na'), + ('na', Not('a'))])) + + # see if it handles implications with negations + I, P = D([('a', Not('b')), ('c', 'b')]) + assert I == {'a': {Not('b'), Not('c')}, 'b': {Not('a')}, 'c': {'b', Not('a')}, Not('b'): {Not('c')}} + assert P == {'a': {'b', 'c'}, 'b': {'a', 'c'}, 'c': {'a', 'b'}} + I, P = D([(Not('a'), 'b'), ('a', 'c')]) + assert I == {'a': {'c'}, Not('a'): {'b'}, Not('b'): {'a', + 'c'}, Not('c'): {Not('a'), 'b'},} + assert P == {'a': {'b', 'c'}, 'b': {'a', 'c'}, 'c': {'a', 'b'}} + + + # Long deductions + I, P = D([('a', 'b'), ('b', 'c'), ('c', 'd'), ('d', 'e')]) + assert I == {'a': {'b', 'c', 'd', 'e'}, 'b': {'c', 'd', 'e'}, + 'c': {'d', 'e'}, 'd': {'e'}, Not('b'): {Not('a')}, + Not('c'): {Not('a'), Not('b')}, Not('d'): {Not('a'), Not('b'), + Not('c')}, Not('e'): {Not('a'), Not('b'), Not('c'), Not('d')}} + assert P == {'a': {'b', 'c', 'd', 'e'}, 'b': {'a', 'c', 'd', + 'e'}, 'c': {'a', 'b', 'd', 'e'}, 'd': {'a', 'b', 'c', 'e'}, + 'e': {'a', 'b', 'c', 'd'}} + + # something related to real-world + I, P = D([('rat', 'real'), ('int', 'rat')]) + + assert I == {'int': {'rat', 'real'}, 'rat': {'real'}, + Not('real'): {Not('rat'), Not('int')}, Not('rat'): {Not('int')}} + assert P == {'rat': {'int', 'real'}, 'real': {'int', 'rat'}, + 'int': {'rat', 'real'}} + + +# TODO move me to appropriate place +def test_apply_beta_to_alpha_route(): + APPLY = apply_beta_to_alpha_route + + # indicates empty alpha-chain with attached beta-rule #bidx + def Q(bidx): + return (set(), [bidx]) + + # x -> a &(a,b) -> x -- x -> a + A = {'x': {'a'}} + B = [(And('a', 'b'), 'x')] + assert APPLY(A, B) == {'x': ({'a'}, []), 'a': Q(0), 'b': Q(0)} + + # x -> a &(a,!x) -> b -- x -> a + A = {'x': {'a'}} + B = [(And('a', Not('x')), 'b')] + assert APPLY(A, B) == {'x': ({'a'}, []), Not('x'): Q(0), 'a': Q(0)} + + # x -> a b &(a,b) -> c -- x -> a b c + A = {'x': {'a', 'b'}} + B = [(And('a', 'b'), 'c')] + assert APPLY(A, B) == \ + {'x': ({'a', 'b', 'c'}, []), 'a': Q(0), 'b': Q(0)} + + # x -> a &(a,b) -> y -- x -> a [#0] + A = {'x': {'a'}} + B = [(And('a', 'b'), 'y')] + assert APPLY(A, B) == {'x': ({'a'}, [0]), 'a': Q(0), 'b': Q(0)} + + # x -> a b c &(a,b) -> c -- x -> a b c + A = {'x': {'a', 'b', 'c'}} + B = [(And('a', 'b'), 'c')] + assert APPLY(A, B) == \ + {'x': ({'a', 'b', 'c'}, []), 'a': Q(0), 'b': Q(0)} + + # x -> a b &(a,b,c) -> y -- x -> a b [#0] + A = {'x': {'a', 'b'}} + B = [(And('a', 'b', 'c'), 'y')] + assert APPLY(A, B) == \ + {'x': ({'a', 'b'}, [0]), 'a': Q(0), 'b': Q(0), 'c': Q(0)} + + # x -> a b &(a,b) -> c -- x -> a b c d + # c -> d c -> d + A = {'x': {'a', 'b'}, 'c': {'d'}} + B = [(And('a', 'b'), 'c')] + assert APPLY(A, B) == {'x': ({'a', 'b', 'c', 'd'}, []), + 'c': ({'d'}, []), 'a': Q(0), 'b': Q(0)} + + # x -> a b &(a,b) -> c -- x -> a b c d e + # c -> d &(c,d) -> e c -> d e + A = {'x': {'a', 'b'}, 'c': {'d'}} + B = [(And('a', 'b'), 'c'), (And('c', 'd'), 'e')] + assert APPLY(A, B) == {'x': ({'a', 'b', 'c', 'd', 'e'}, []), + 'c': ({'d', 'e'}, []), 'a': Q(0), 'b': Q(0), 'd': Q(1)} + + # x -> a b &(a,y) -> z -- x -> a b y z + # &(a,b) -> y + A = {'x': {'a', 'b'}} + B = [(And('a', 'y'), 'z'), (And('a', 'b'), 'y')] + assert APPLY(A, B) == {'x': ({'a', 'b', 'y', 'z'}, []), + 'a': (set(), [0, 1]), 'y': Q(0), 'b': Q(1)} + + # x -> a b &(a,!b) -> c -- x -> a b + A = {'x': {'a', 'b'}} + B = [(And('a', Not('b')), 'c')] + assert APPLY(A, B) == \ + {'x': ({'a', 'b'}, []), 'a': Q(0), Not('b'): Q(0)} + + # !x -> !a !b &(!a,b) -> c -- !x -> !a !b + A = {Not('x'): {Not('a'), Not('b')}} + B = [(And(Not('a'), 'b'), 'c')] + assert APPLY(A, B) == \ + {Not('x'): ({Not('a'), Not('b')}, []), Not('a'): Q(0), 'b': Q(0)} + + # x -> a b &(b,c) -> !a -- x -> a b + A = {'x': {'a', 'b'}} + B = [(And('b', 'c'), Not('a'))] + assert APPLY(A, B) == {'x': ({'a', 'b'}, []), 'b': Q(0), 'c': Q(0)} + + # x -> a b &(a, b) -> c -- x -> a b c p + # c -> p a + A = {'x': {'a', 'b'}, 'c': {'p', 'a'}} + B = [(And('a', 'b'), 'c')] + assert APPLY(A, B) == {'x': ({'a', 'b', 'c', 'p'}, []), + 'c': ({'p', 'a'}, []), 'a': Q(0), 'b': Q(0)} + + +def test_FactRules_parse(): + f = FactRules('a -> b') + assert f.prereq == {'b': {'a'}, 'a': {'b'}} + + f = FactRules('a -> !b') + assert f.prereq == {'b': {'a'}, 'a': {'b'}} + + f = FactRules('!a -> b') + assert f.prereq == {'b': {'a'}, 'a': {'b'}} + + f = FactRules('!a -> !b') + assert f.prereq == {'b': {'a'}, 'a': {'b'}} + + f = FactRules('!z == nz') + assert f.prereq == {'z': {'nz'}, 'nz': {'z'}} + + +def test_FactRules_parse2(): + raises(ValueError, lambda: FactRules('a -> !a')) + + +def test_FactRules_deduce(): + f = FactRules(['a -> b', 'b -> c', 'b -> d', 'c -> e']) + + def D(facts): + kb = FactKB(f) + kb.deduce_all_facts(facts) + return kb + + assert D({'a': T}) == {'a': T, 'b': T, 'c': T, 'd': T, 'e': T} + assert D({'b': T}) == { 'b': T, 'c': T, 'd': T, 'e': T} + assert D({'c': T}) == { 'c': T, 'e': T} + assert D({'d': T}) == { 'd': T } + assert D({'e': T}) == { 'e': T} + + assert D({'a': F}) == {'a': F } + assert D({'b': F}) == {'a': F, 'b': F } + assert D({'c': F}) == {'a': F, 'b': F, 'c': F } + assert D({'d': F}) == {'a': F, 'b': F, 'd': F } + + assert D({'a': U}) == {'a': U} # XXX ok? + + +def test_FactRules_deduce2(): + # pos/neg/zero, but the rules are not sufficient to derive all relations + f = FactRules(['pos -> !neg', 'pos -> !z']) + + def D(facts): + kb = FactKB(f) + kb.deduce_all_facts(facts) + return kb + + assert D({'pos': T}) == {'pos': T, 'neg': F, 'z': F} + assert D({'pos': F}) == {'pos': F } + assert D({'neg': T}) == {'pos': F, 'neg': T } + assert D({'neg': F}) == { 'neg': F } + assert D({'z': T}) == {'pos': F, 'z': T} + assert D({'z': F}) == { 'z': F} + + # pos/neg/zero. rules are sufficient to derive all relations + f = FactRules(['pos -> !neg', 'neg -> !pos', 'pos -> !z', 'neg -> !z']) + + assert D({'pos': T}) == {'pos': T, 'neg': F, 'z': F} + assert D({'pos': F}) == {'pos': F } + assert D({'neg': T}) == {'pos': F, 'neg': T, 'z': F} + assert D({'neg': F}) == { 'neg': F } + assert D({'z': T}) == {'pos': F, 'neg': F, 'z': T} + assert D({'z': F}) == { 'z': F} + + +def test_FactRules_deduce_multiple(): + # deduction that involves _several_ starting points + f = FactRules(['real == pos | npos']) + + def D(facts): + kb = FactKB(f) + kb.deduce_all_facts(facts) + return kb + + assert D({'real': T}) == {'real': T} + assert D({'real': F}) == {'real': F, 'pos': F, 'npos': F} + assert D({'pos': T}) == {'real': T, 'pos': T} + assert D({'npos': T}) == {'real': T, 'npos': T} + + # --- key tests below --- + assert D({'pos': F, 'npos': F}) == {'real': F, 'pos': F, 'npos': F} + assert D({'real': T, 'pos': F}) == {'real': T, 'pos': F, 'npos': T} + assert D({'real': T, 'npos': F}) == {'real': T, 'pos': T, 'npos': F} + + assert D({'pos': T, 'npos': F}) == {'real': T, 'pos': T, 'npos': F} + assert D({'pos': F, 'npos': T}) == {'real': T, 'pos': F, 'npos': T} + + +def test_FactRules_deduce_multiple2(): + f = FactRules(['real == neg | zero | pos']) + + def D(facts): + kb = FactKB(f) + kb.deduce_all_facts(facts) + return kb + + assert D({'real': T}) == {'real': T} + assert D({'real': F}) == {'real': F, 'neg': F, 'zero': F, 'pos': F} + assert D({'neg': T}) == {'real': T, 'neg': T} + assert D({'zero': T}) == {'real': T, 'zero': T} + assert D({'pos': T}) == {'real': T, 'pos': T} + + # --- key tests below --- + assert D({'neg': F, 'zero': F, 'pos': F}) == {'real': F, 'neg': F, + 'zero': F, 'pos': F} + assert D({'real': T, 'neg': F}) == {'real': T, 'neg': F} + assert D({'real': T, 'zero': F}) == {'real': T, 'zero': F} + assert D({'real': T, 'pos': F}) == {'real': T, 'pos': F} + + assert D({'real': T, 'zero': F, 'pos': F}) == {'real': T, + 'neg': T, 'zero': F, 'pos': F} + assert D({'real': T, 'neg': F, 'pos': F}) == {'real': T, + 'neg': F, 'zero': T, 'pos': F} + assert D({'real': T, 'neg': F, 'zero': F }) == {'real': T, + 'neg': F, 'zero': F, 'pos': T} + + assert D({'neg': T, 'zero': F, 'pos': F}) == {'real': T, 'neg': T, + 'zero': F, 'pos': F} + assert D({'neg': F, 'zero': T, 'pos': F}) == {'real': T, 'neg': F, + 'zero': T, 'pos': F} + assert D({'neg': F, 'zero': F, 'pos': T}) == {'real': T, 'neg': F, + 'zero': F, 'pos': T} + + +def test_FactRules_deduce_base(): + # deduction that starts from base + + f = FactRules(['real == neg | zero | pos', + 'neg -> real & !zero & !pos', + 'pos -> real & !zero & !neg']) + base = FactKB(f) + + base.deduce_all_facts({'real': T, 'neg': F}) + assert base == {'real': T, 'neg': F} + + base.deduce_all_facts({'zero': F}) + assert base == {'real': T, 'neg': F, 'zero': F, 'pos': T} + + +def test_FactRules_deduce_staticext(): + # verify that static beta-extensions deduction takes place + f = FactRules(['real == neg | zero | pos', + 'neg -> real & !zero & !pos', + 'pos -> real & !zero & !neg', + 'nneg == real & !neg', + 'npos == real & !pos']) + + assert ('npos', True) in f.full_implications[('neg', True)] + assert ('nneg', True) in f.full_implications[('pos', True)] + assert ('nneg', True) in f.full_implications[('zero', True)] + assert ('npos', True) in f.full_implications[('zero', True)] diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_function.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_function.py new file mode 100644 index 0000000000000000000000000000000000000000..a69c6b81b786ab0f0592367eaf402c2165a615dc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_function.py @@ -0,0 +1,1459 @@ +from sympy.concrete.summations import Sum +from sympy.core.basic import Basic, _aresame +from sympy.core.cache import clear_cache +from sympy.core.containers import Dict, Tuple +from sympy.core.expr import Expr, unchanged +from sympy.core.function import (Subs, Function, diff, Lambda, expand, + nfloat, Derivative) +from sympy.core.numbers import E, Float, zoo, Rational, pi, I, oo, nan +from sympy.core.power import Pow +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import symbols, Dummy, Symbol +from sympy.functions.elementary.complexes import im, re +from sympy.functions.elementary.exponential import log, exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import sin, cos, acos +from sympy.functions.special.error_functions import expint +from sympy.functions.special.gamma_functions import loggamma, polygamma +from sympy.matrices.dense import Matrix +from sympy.printing.str import sstr +from sympy.series.order import O +from sympy.tensor.indexed import Indexed +from sympy.core.function import (PoleError, _mexpand, arity, + BadSignatureError, BadArgumentsError) +from sympy.core.parameters import _exp_is_pow +from sympy.core.sympify import sympify, SympifyError +from sympy.matrices import MutableMatrix, ImmutableMatrix +from sympy.sets.sets import FiniteSet +from sympy.solvers.solveset import solveset +from sympy.tensor.array import NDimArray +from sympy.utilities.iterables import subsets, variations +from sympy.testing.pytest import XFAIL, raises, warns_deprecated_sympy, _both_exp_pow + +from sympy.abc import t, w, x, y, z +f, g, h = symbols('f g h', cls=Function) +_xi_1, _xi_2, _xi_3 = [Dummy() for i in range(3)] + +def test_f_expand_complex(): + x = Symbol('x', real=True) + + assert f(x).expand(complex=True) == I*im(f(x)) + re(f(x)) + assert exp(x).expand(complex=True) == exp(x) + assert exp(I*x).expand(complex=True) == cos(x) + I*sin(x) + assert exp(z).expand(complex=True) == cos(im(z))*exp(re(z)) + \ + I*sin(im(z))*exp(re(z)) + + +def test_bug1(): + e = sqrt(-log(w)) + assert e.subs(log(w), -x) == sqrt(x) + + e = sqrt(-5*log(w)) + assert e.subs(log(w), -x) == sqrt(5*x) + + +def test_general_function(): + nu = Function('nu') + + e = nu(x) + edx = e.diff(x) + edy = e.diff(y) + edxdx = e.diff(x).diff(x) + edxdy = e.diff(x).diff(y) + assert e == nu(x) + assert edx != nu(x) + assert edx == diff(nu(x), x) + assert edy == 0 + assert edxdx == diff(diff(nu(x), x), x) + assert edxdy == 0 + +def test_general_function_nullary(): + nu = Function('nu') + + e = nu() + edx = e.diff(x) + edxdx = e.diff(x).diff(x) + assert e == nu() + assert edx != nu() + assert edx == 0 + assert edxdx == 0 + + +def test_derivative_subs_bug(): + e = diff(g(x), x) + assert e.subs(g(x), f(x)) != e + assert e.subs(g(x), f(x)) == Derivative(f(x), x) + assert e.subs(g(x), -f(x)) == Derivative(-f(x), x) + + assert e.subs(x, y) == Derivative(g(y), y) + + +def test_derivative_subs_self_bug(): + d = diff(f(x), x) + + assert d.subs(d, y) == y + + +def test_derivative_linearity(): + assert diff(-f(x), x) == -diff(f(x), x) + assert diff(8*f(x), x) == 8*diff(f(x), x) + assert diff(8*f(x), x) != 7*diff(f(x), x) + assert diff(8*f(x)*x, x) == 8*f(x) + 8*x*diff(f(x), x) + assert diff(8*f(x)*y*x, x).expand() == 8*y*f(x) + 8*y*x*diff(f(x), x) + + +def test_derivative_evaluate(): + assert Derivative(sin(x), x) != diff(sin(x), x) + assert Derivative(sin(x), x).doit() == diff(sin(x), x) + + assert Derivative(Derivative(f(x), x), x) == diff(f(x), x, x) + assert Derivative(sin(x), x, 0) == sin(x) + assert Derivative(sin(x), (x, y), (x, -y)) == sin(x) + + +def test_diff_symbols(): + assert diff(f(x, y, z), x, y, z) == Derivative(f(x, y, z), x, y, z) + assert diff(f(x, y, z), x, x, x) == Derivative(f(x, y, z), x, x, x) == Derivative(f(x, y, z), (x, 3)) + assert diff(f(x, y, z), x, 3) == Derivative(f(x, y, z), x, 3) + + # issue 5028 + assert [diff(-z + x/y, sym) for sym in (z, x, y)] == [-1, 1/y, -x/y**2] + assert diff(f(x, y, z), x, y, z, 2) == Derivative(f(x, y, z), x, y, z, z) + assert diff(f(x, y, z), x, y, z, 2, evaluate=False) == \ + Derivative(f(x, y, z), x, y, z, z) + assert Derivative(f(x, y, z), x, y, z)._eval_derivative(z) == \ + Derivative(f(x, y, z), x, y, z, z) + assert Derivative(Derivative(f(x, y, z), x), y)._eval_derivative(z) == \ + Derivative(f(x, y, z), x, y, z) + + raises(TypeError, lambda: cos(x).diff((x, y)).variables) + assert cos(x).diff((x, y))._wrt_variables == [x] + + # issue 23222 + assert sympify("a*x+b").diff("x") == sympify("a") + +def test_Function(): + class myfunc(Function): + @classmethod + def eval(cls): # zero args + return + + assert myfunc.nargs == FiniteSet(0) + assert myfunc().nargs == FiniteSet(0) + raises(TypeError, lambda: myfunc(x).nargs) + + class myfunc(Function): + @classmethod + def eval(cls, x): # one arg + return + + assert myfunc.nargs == FiniteSet(1) + assert myfunc(x).nargs == FiniteSet(1) + raises(TypeError, lambda: myfunc(x, y).nargs) + + class myfunc(Function): + @classmethod + def eval(cls, *x): # star args + return + + assert myfunc.nargs == S.Naturals0 + assert myfunc(x).nargs == S.Naturals0 + + +def test_nargs(): + f = Function('f') + assert f.nargs == S.Naturals0 + assert f(1).nargs == S.Naturals0 + assert Function('f', nargs=2)(1, 2).nargs == FiniteSet(2) + assert sin.nargs == FiniteSet(1) + assert sin(2).nargs == FiniteSet(1) + assert log.nargs == FiniteSet(1, 2) + assert log(2).nargs == FiniteSet(1, 2) + assert Function('f', nargs=2).nargs == FiniteSet(2) + assert Function('f', nargs=0).nargs == FiniteSet(0) + assert Function('f', nargs=(0, 1)).nargs == FiniteSet(0, 1) + assert Function('f', nargs=None).nargs == S.Naturals0 + raises(ValueError, lambda: Function('f', nargs=())) + +def test_nargs_inheritance(): + class f1(Function): + nargs = 2 + class f2(f1): + pass + class f3(f2): + pass + class f4(f3): + nargs = 1,2 + class f5(f4): + pass + class f6(f5): + pass + class f7(f6): + nargs=None + class f8(f7): + pass + class f9(f8): + pass + class f10(f9): + nargs = 1 + class f11(f10): + pass + assert f1.nargs == FiniteSet(2) + assert f2.nargs == FiniteSet(2) + assert f3.nargs == FiniteSet(2) + assert f4.nargs == FiniteSet(1, 2) + assert f5.nargs == FiniteSet(1, 2) + assert f6.nargs == FiniteSet(1, 2) + assert f7.nargs == S.Naturals0 + assert f8.nargs == S.Naturals0 + assert f9.nargs == S.Naturals0 + assert f10.nargs == FiniteSet(1) + assert f11.nargs == FiniteSet(1) + +def test_arity(): + f = lambda x, y: 1 + assert arity(f) == 2 + def f(x, y, z=None): + pass + assert arity(f) == (2, 3) + assert arity(lambda *x: x) is None + assert arity(log) == (1, 2) + + +def test_Lambda(): + e = Lambda(x, x**2) + assert e(4) == 16 + assert e(x) == x**2 + assert e(y) == y**2 + + assert Lambda((), 42)() == 42 + assert unchanged(Lambda, (), 42) + assert Lambda((), 42) != Lambda((), 43) + assert Lambda((), f(x))() == f(x) + assert Lambda((), 42).nargs == FiniteSet(0) + + assert unchanged(Lambda, (x,), x**2) + assert Lambda(x, x**2) == Lambda((x,), x**2) + assert Lambda(x, x**2) != Lambda(x, x**2 + 1) + assert Lambda((x, y), x**y) != Lambda((y, x), y**x) + assert Lambda((x, y), x**y) != Lambda((x, y), y**x) + + assert Lambda((x, y), x**y)(x, y) == x**y + assert Lambda((x, y), x**y)(3, 3) == 3**3 + assert Lambda((x, y), x**y)(x, 3) == x**3 + assert Lambda((x, y), x**y)(3, y) == 3**y + assert Lambda(x, f(x))(x) == f(x) + assert Lambda(x, x**2)(e(x)) == x**4 + assert e(e(x)) == x**4 + + x1, x2 = (Indexed('x', i) for i in (1, 2)) + assert Lambda((x1, x2), x1 + x2)(x, y) == x + y + + assert Lambda((x, y), x + y).nargs == FiniteSet(2) + + p = x, y, z, t + assert Lambda(p, t*(x + y + z))(*p) == t * (x + y + z) + + eq = Lambda(x, 2*x) + Lambda(y, 2*y) + assert eq != 2*Lambda(x, 2*x) + assert eq.as_dummy() == 2*Lambda(x, 2*x).as_dummy() + assert Lambda(x, 2*x) not in [ Lambda(x, x) ] + raises(BadSignatureError, lambda: Lambda(1, x)) + assert Lambda(x, 1)(1) is S.One + + raises(BadSignatureError, lambda: Lambda((x, x), x + 2)) + raises(BadSignatureError, lambda: Lambda(((x, x), y), x)) + raises(BadSignatureError, lambda: Lambda(((y, x), x), x)) + raises(BadSignatureError, lambda: Lambda(((y, 1), 2), x)) + + with warns_deprecated_sympy(): + assert Lambda([x, y], x+y) == Lambda((x, y), x+y) + + flam = Lambda(((x, y),), x + y) + assert flam((2, 3)) == 5 + flam = Lambda(((x, y), z), x + y + z) + assert flam((2, 3), 1) == 6 + flam = Lambda((((x, y), z),), x + y + z) + assert flam(((2, 3), 1)) == 6 + raises(BadArgumentsError, lambda: flam(1, 2, 3)) + flam = Lambda( (x,), (x, x)) + assert flam(1,) == (1, 1) + assert flam((1,)) == ((1,), (1,)) + flam = Lambda( ((x,),), (x, x)) + raises(BadArgumentsError, lambda: flam(1)) + assert flam((1,)) == (1, 1) + + # Previously TypeError was raised so this is potentially needed for + # backwards compatibility. + assert issubclass(BadSignatureError, TypeError) + assert issubclass(BadArgumentsError, TypeError) + + # These are tested to see they don't raise: + hash(Lambda(x, 2*x)) + hash(Lambda(x, x)) # IdentityFunction subclass + + +def test_IdentityFunction(): + assert Lambda(x, x) is Lambda(y, y) is S.IdentityFunction + assert Lambda(x, 2*x) is not S.IdentityFunction + assert Lambda((x, y), x) is not S.IdentityFunction + + +def test_Lambda_symbols(): + assert Lambda(x, 2*x).free_symbols == set() + assert Lambda(x, x*y).free_symbols == {y} + assert Lambda((), 42).free_symbols == set() + assert Lambda((), x*y).free_symbols == {x,y} + + +def test_functionclas_symbols(): + assert f.free_symbols == set() + + +def test_Lambda_arguments(): + raises(TypeError, lambda: Lambda(x, 2*x)(x, y)) + raises(TypeError, lambda: Lambda((x, y), x + y)(x)) + raises(TypeError, lambda: Lambda((), 42)(x)) + + +def test_Lambda_equality(): + assert Lambda((x, y), 2*x) == Lambda((x, y), 2*x) + # these, of course, should never be equal + assert Lambda(x, 2*x) != Lambda((x, y), 2*x) + assert Lambda(x, 2*x) != 2*x + # But it is tempting to want expressions that differ only + # in bound symbols to compare the same. But this is not what + # Python's `==` is intended to do; two objects that compare + # as equal means that they are indistibguishable and cache to the + # same value. We wouldn't want to expression that are + # mathematically the same but written in different variables to be + # interchanged else what is the point of allowing for different + # variable names? + assert Lambda(x, 2*x) != Lambda(y, 2*y) + + +def test_Subs(): + assert Subs(1, (), ()) is S.One + # check null subs influence on hashing + assert Subs(x, y, z) != Subs(x, y, 1) + # neutral subs works + assert Subs(x, x, 1).subs(x, y).has(y) + # self mapping var/point + assert Subs(Derivative(f(x), (x, 2)), x, x).doit() == f(x).diff(x, x) + assert Subs(x, x, 0).has(x) # it's a structural answer + assert not Subs(x, x, 0).free_symbols + assert Subs(Subs(x + y, x, 2), y, 1) == Subs(x + y, (x, y), (2, 1)) + assert Subs(x, (x,), (0,)) == Subs(x, x, 0) + assert Subs(x, x, 0) == Subs(y, y, 0) + assert Subs(x, x, 0).subs(x, 1) == Subs(x, x, 0) + assert Subs(y, x, 0).subs(y, 1) == Subs(1, x, 0) + assert Subs(f(x), x, 0).doit() == f(0) + assert Subs(f(x**2), x**2, 0).doit() == f(0) + assert Subs(f(x, y, z), (x, y, z), (0, 1, 1)) != \ + Subs(f(x, y, z), (x, y, z), (0, 0, 1)) + assert Subs(x, y, 2).subs(x, y).doit() == 2 + assert Subs(f(x, y), (x, y, z), (0, 1, 1)) != \ + Subs(f(x, y) + z, (x, y, z), (0, 1, 0)) + assert Subs(f(x, y), (x, y), (0, 1)).doit() == f(0, 1) + assert Subs(Subs(f(x, y), x, 0), y, 1).doit() == f(0, 1) + raises(ValueError, lambda: Subs(f(x, y), (x, y), (0, 0, 1))) + raises(ValueError, lambda: Subs(f(x, y), (x, x, y), (0, 0, 1))) + + assert len(Subs(f(x, y), (x, y), (0, 1)).variables) == 2 + assert Subs(f(x, y), (x, y), (0, 1)).point == Tuple(0, 1) + + assert Subs(f(x), x, 0) == Subs(f(y), y, 0) + assert Subs(f(x, y), (x, y), (0, 1)) == Subs(f(x, y), (y, x), (1, 0)) + assert Subs(f(x)*y, (x, y), (0, 1)) == Subs(f(y)*x, (y, x), (0, 1)) + assert Subs(f(x)*y, (x, y), (1, 1)) == Subs(f(y)*x, (x, y), (1, 1)) + + assert Subs(f(x), x, 0).subs(x, 1).doit() == f(0) + assert Subs(f(x), x, y).subs(y, 0) == Subs(f(x), x, 0) + assert Subs(y*f(x), x, y).subs(y, 2) == Subs(2*f(x), x, 2) + assert (2 * Subs(f(x), x, 0)).subs(Subs(f(x), x, 0), y) == 2*y + + assert Subs(f(x), x, 0).free_symbols == set() + assert Subs(f(x, y), x, z).free_symbols == {y, z} + + assert Subs(f(x).diff(x), x, 0).doit(), Subs(f(x).diff(x), x, 0) + assert Subs(1 + f(x).diff(x), x, 0).doit(), 1 + Subs(f(x).diff(x), x, 0) + assert Subs(y*f(x, y).diff(x), (x, y), (0, 2)).doit() == \ + 2*Subs(Derivative(f(x, 2), x), x, 0) + assert Subs(y**2*f(x), x, 0).diff(y) == 2*y*f(0) + + e = Subs(y**2*f(x), x, y) + assert e.diff(y) == e.doit().diff(y) == y**2*Derivative(f(y), y) + 2*y*f(y) + + assert Subs(f(x), x, 0) + Subs(f(x), x, 0) == 2*Subs(f(x), x, 0) + e1 = Subs(z*f(x), x, 1) + e2 = Subs(z*f(y), y, 1) + assert e1 + e2 == 2*e1 + assert e1.__hash__() == e2.__hash__() + assert Subs(z*f(x + 1), x, 1) not in [ e1, e2 ] + assert Derivative(f(x), x).subs(x, g(x)) == Derivative(f(g(x)), g(x)) + assert Derivative(f(x), x).subs(x, x + y) == Subs(Derivative(f(x), x), + x, x + y) + assert Subs(f(x)*cos(y) + z, (x, y), (0, pi/3)).n(2) == \ + Subs(f(x)*cos(y) + z, (x, y), (0, pi/3)).evalf(2) == \ + z + Rational('1/2').n(2)*f(0) + + assert f(x).diff(x).subs(x, 0).subs(x, y) == f(x).diff(x).subs(x, 0) + assert (x*f(x).diff(x).subs(x, 0)).subs(x, y) == y*f(x).diff(x).subs(x, 0) + assert Subs(Derivative(g(x)**2, g(x), x), g(x), exp(x) + ).doit() == 2*exp(x) + assert Subs(Derivative(g(x)**2, g(x), x), g(x), exp(x) + ).doit(deep=False) == 2*Derivative(exp(x), x) + assert Derivative(f(x, g(x)), x).doit() == Derivative( + f(x, g(x)), g(x))*Derivative(g(x), x) + Subs(Derivative( + f(y, g(x)), y), y, x) + +def test_doitdoit(): + done = Derivative(f(x, g(x)), x, g(x)).doit() + assert done == done.doit() + + +@XFAIL +def test_Subs2(): + # this reflects a limitation of subs(), probably won't fix + assert Subs(f(x), x**2, x).doit() == f(sqrt(x)) + + +def test_expand_function(): + assert expand(x + y) == x + y + assert expand(x + y, complex=True) == I*im(x) + I*im(y) + re(x) + re(y) + assert expand((x + y)**11, modulus=11) == x**11 + y**11 + + +def test_function_comparable(): + assert sin(x).is_comparable is False + assert cos(x).is_comparable is False + + assert sin(Float('0.1')).is_comparable is True + assert cos(Float('0.1')).is_comparable is True + + assert sin(E).is_comparable is True + assert cos(E).is_comparable is True + + assert sin(Rational(1, 3)).is_comparable is True + assert cos(Rational(1, 3)).is_comparable is True + + +def test_function_comparable_infinities(): + assert sin(oo).is_comparable is False + assert sin(-oo).is_comparable is False + assert sin(zoo).is_comparable is False + assert sin(nan).is_comparable is False + + +def test_deriv1(): + # These all require derivatives evaluated at a point (issue 4719) to work. + # See issue 4624 + assert f(2*x).diff(x) == 2*Subs(Derivative(f(x), x), x, 2*x) + assert (f(x)**3).diff(x) == 3*f(x)**2*f(x).diff(x) + assert (f(2*x)**3).diff(x) == 6*f(2*x)**2*Subs( + Derivative(f(x), x), x, 2*x) + + assert f(2 + x).diff(x) == Subs(Derivative(f(x), x), x, x + 2) + assert f(2 + 3*x).diff(x) == 3*Subs( + Derivative(f(x), x), x, 3*x + 2) + assert f(3*sin(x)).diff(x) == 3*cos(x)*Subs( + Derivative(f(x), x), x, 3*sin(x)) + + # See issue 8510 + assert f(x, x + z).diff(x) == ( + Subs(Derivative(f(y, x + z), y), y, x) + + Subs(Derivative(f(x, y), y), y, x + z)) + assert f(x, x**2).diff(x) == ( + 2*x*Subs(Derivative(f(x, y), y), y, x**2) + + Subs(Derivative(f(y, x**2), y), y, x)) + # but Subs is not always necessary + assert f(x, g(y)).diff(g(y)) == Derivative(f(x, g(y)), g(y)) + + +def test_deriv2(): + assert (x**3).diff(x) == 3*x**2 + assert (x**3).diff(x, evaluate=False) != 3*x**2 + assert (x**3).diff(x, evaluate=False) == Derivative(x**3, x) + + assert diff(x**3, x) == 3*x**2 + assert diff(x**3, x, evaluate=False) != 3*x**2 + assert diff(x**3, x, evaluate=False) == Derivative(x**3, x) + + +def test_func_deriv(): + assert f(x).diff(x) == Derivative(f(x), x) + # issue 4534 + assert f(x, y).diff(x, y) - f(x, y).diff(y, x) == 0 + assert Derivative(f(x, y), x, y).args[1:] == ((x, 1), (y, 1)) + assert Derivative(f(x, y), y, x).args[1:] == ((y, 1), (x, 1)) + assert (Derivative(f(x, y), x, y) - Derivative(f(x, y), y, x)).doit() == 0 + + +def test_suppressed_evaluation(): + a = sin(0, evaluate=False) + assert a != 0 + assert a.func is sin + assert a.args == (0,) + + +def test_function_evalf(): + def eq(a, b, eps): + return abs(a - b) < eps + assert eq(sin(1).evalf(15), Float("0.841470984807897"), 1e-13) + assert eq( + sin(2).evalf(25), Float("0.9092974268256816953960199", 25), 1e-23) + assert eq(sin(1 + I).evalf( + 15), Float("1.29845758141598") + Float("0.634963914784736")*I, 1e-13) + assert eq(exp(1 + I).evalf(15), Float( + "1.46869393991588") + Float("2.28735528717884239")*I, 1e-13) + assert eq(exp(-0.5 + 1.5*I).evalf(15), Float( + "0.0429042815937374") + Float("0.605011292285002")*I, 1e-13) + assert eq(log(pi + sqrt(2)*I).evalf( + 15), Float("1.23699044022052") + Float("0.422985442737893")*I, 1e-13) + assert eq(cos(100).evalf(15), Float("0.86231887228768"), 1e-13) + + +def test_extensibility_eval(): + class MyFunc(Function): + @classmethod + def eval(cls, *args): + return (0, 0, 0) + assert MyFunc(0) == (0, 0, 0) + + +@_both_exp_pow +def test_function_non_commutative(): + x = Symbol('x', commutative=False) + assert f(x).is_commutative is False + assert sin(x).is_commutative is False + assert exp(x).is_commutative is False + assert log(x).is_commutative is False + assert f(x).is_complex is False + assert sin(x).is_complex is False + assert exp(x).is_complex is False + assert log(x).is_complex is False + + +def test_function_complex(): + x = Symbol('x', complex=True) + xzf = Symbol('x', complex=True, zero=False) + assert f(x).is_commutative is True + assert sin(x).is_commutative is True + assert exp(x).is_commutative is True + assert log(x).is_commutative is True + assert f(x).is_complex is None + assert sin(x).is_complex is True + assert exp(x).is_complex is True + assert log(x).is_complex is None + assert log(xzf).is_complex is True + + +def test_function__eval_nseries(): + n = Symbol('n') + + assert sin(x)._eval_nseries(x, 2, None) == x + O(x**2) + assert sin(x + 1)._eval_nseries(x, 2, None) == x*cos(1) + sin(1) + O(x**2) + assert sin(pi*(1 - x))._eval_nseries(x, 2, None) == pi*x + O(x**2) + assert acos(1 - x**2)._eval_nseries(x, 2, None) == sqrt(2)*sqrt(x**2) + O(x**2) + assert polygamma(n, x + 1)._eval_nseries(x, 2, None) == \ + polygamma(n, 1) + polygamma(n + 1, 1)*x + O(x**2) + raises(PoleError, lambda: sin(1/x)._eval_nseries(x, 2, None)) + assert acos(1 - x)._eval_nseries(x, 2, None) == sqrt(2)*sqrt(x) + sqrt(2)*x**(S(3)/2)/12 + O(x**2) + assert acos(1 + x)._eval_nseries(x, 2, None) == sqrt(2)*sqrt(-x) + sqrt(2)*(-x)**(S(3)/2)/12 + O(x**2) + assert loggamma(1/x)._eval_nseries(x, 0, None) == \ + log(x)/2 - log(x)/x - 1/x + O(1, x) + assert loggamma(log(1/x)).nseries(x, n=1, logx=y) == loggamma(-y) + + # issue 6725: + assert expint(Rational(3, 2), -x)._eval_nseries(x, 5, None) == \ + 2 - 2*x - x**2/3 - x**3/15 - x**4/84 - 2*I*sqrt(pi)*sqrt(x) + O(x**5) + assert sin(sqrt(x))._eval_nseries(x, 3, None) == \ + sqrt(x) - x**Rational(3, 2)/6 + x**Rational(5, 2)/120 + O(x**3) + + # issue 19065: + s1 = f(x,y).series(y, n=2) + assert {i.name for i in s1.atoms(Symbol)} == {'x', 'xi', 'y'} + xi = Symbol('xi') + s2 = f(xi, y).series(y, n=2) + assert {i.name for i in s2.atoms(Symbol)} == {'xi', 'xi0', 'y'} + +def test_doit(): + n = Symbol('n', integer=True) + f = Sum(2 * n * x, (n, 1, 3)) + d = Derivative(f, x) + assert d.doit() == 12 + assert d.doit(deep=False) == Sum(2*n, (n, 1, 3)) + + +def test_evalf_default(): + from sympy.functions.special.gamma_functions import polygamma + assert type(sin(4.0)) == Float + assert type(re(sin(I + 1.0))) == Float + assert type(im(sin(I + 1.0))) == Float + assert type(sin(4)) == sin + assert type(polygamma(2.0, 4.0)) == Float + assert type(sin(Rational(1, 4))) == sin + + +def test_issue_5399(): + args = [x, y, S(2), S.Half] + + def ok(a): + """Return True if the input args for diff are ok""" + if not a: + return False + if a[0].is_Symbol is False: + return False + s_at = [i for i in range(len(a)) if a[i].is_Symbol] + n_at = [i for i in range(len(a)) if not a[i].is_Symbol] + # every symbol is followed by symbol or int + # every number is followed by a symbol + return (all(a[i + 1].is_Symbol or a[i + 1].is_Integer + for i in s_at if i + 1 < len(a)) and + all(a[i + 1].is_Symbol + for i in n_at if i + 1 < len(a))) + eq = x**10*y**8 + for a in subsets(args): + for v in variations(a, len(a)): + if ok(v): + eq.diff(*v) # does not raise + else: + raises(ValueError, lambda: eq.diff(*v)) + + +def test_derivative_numerically(): + z0 = x._random() + assert abs(Derivative(sin(x), x).doit_numerically(z0) - cos(z0)) < 1e-15 + + +def test_fdiff_argument_index_error(): + from sympy.core.function import ArgumentIndexError + + class myfunc(Function): + nargs = 1 # define since there is no eval routine + + def fdiff(self, idx): + raise ArgumentIndexError + mf = myfunc(x) + assert mf.diff(x) == Derivative(mf, x) + raises(TypeError, lambda: myfunc(x, x)) + + +def test_deriv_wrt_function(): + x = f(t) + xd = diff(x, t) + xdd = diff(xd, t) + y = g(t) + yd = diff(y, t) + + assert diff(x, t) == xd + assert diff(2 * x + 4, t) == 2 * xd + assert diff(2 * x + 4 + y, t) == 2 * xd + yd + assert diff(2 * x + 4 + y * x, t) == 2 * xd + x * yd + xd * y + assert diff(2 * x + 4 + y * x, x) == 2 + y + assert (diff(4 * x**2 + 3 * x + x * y, t) == 3 * xd + x * yd + xd * y + + 8 * x * xd) + assert (diff(4 * x**2 + 3 * xd + x * y, t) == 3 * xdd + x * yd + xd * y + + 8 * x * xd) + assert diff(4 * x**2 + 3 * xd + x * y, xd) == 3 + assert diff(4 * x**2 + 3 * xd + x * y, xdd) == 0 + assert diff(sin(x), t) == xd * cos(x) + assert diff(exp(x), t) == xd * exp(x) + assert diff(sqrt(x), t) == xd / (2 * sqrt(x)) + + +def test_diff_wrt_value(): + assert Expr()._diff_wrt is False + assert x._diff_wrt is True + assert f(x)._diff_wrt is True + assert Derivative(f(x), x)._diff_wrt is True + assert Derivative(x**2, x)._diff_wrt is False + + +def test_diff_wrt(): + fx = f(x) + dfx = diff(f(x), x) + ddfx = diff(f(x), x, x) + + assert diff(sin(fx) + fx**2, fx) == cos(fx) + 2*fx + assert diff(sin(dfx) + dfx**2, dfx) == cos(dfx) + 2*dfx + assert diff(sin(ddfx) + ddfx**2, ddfx) == cos(ddfx) + 2*ddfx + assert diff(fx**2, dfx) == 0 + assert diff(fx**2, ddfx) == 0 + assert diff(dfx**2, fx) == 0 + assert diff(dfx**2, ddfx) == 0 + assert diff(ddfx**2, dfx) == 0 + + assert diff(fx*dfx*ddfx, fx) == dfx*ddfx + assert diff(fx*dfx*ddfx, dfx) == fx*ddfx + assert diff(fx*dfx*ddfx, ddfx) == fx*dfx + + assert diff(f(x), x).diff(f(x)) == 0 + assert (sin(f(x)) - cos(diff(f(x), x))).diff(f(x)) == cos(f(x)) + + assert diff(sin(fx), fx, x) == diff(sin(fx), x, fx) + + # Chain rule cases + assert f(g(x)).diff(x) == \ + Derivative(g(x), x)*Derivative(f(g(x)), g(x)) + assert diff(f(g(x), h(y)), x) == \ + Derivative(g(x), x)*Derivative(f(g(x), h(y)), g(x)) + assert diff(f(g(x), h(x)), x) == ( + Derivative(f(g(x), h(x)), g(x))*Derivative(g(x), x) + + Derivative(f(g(x), h(x)), h(x))*Derivative(h(x), x)) + assert f( + sin(x)).diff(x) == cos(x)*Subs(Derivative(f(x), x), x, sin(x)) + + assert diff(f(g(x)), g(x)) == Derivative(f(g(x)), g(x)) + + +def test_diff_wrt_func_subs(): + assert f(g(x)).diff(x).subs(g, Lambda(x, 2*x)).doit() == f(2*x).diff(x) + + +def test_subs_in_derivative(): + expr = sin(x*exp(y)) + u = Function('u') + v = Function('v') + assert Derivative(expr, y).subs(expr, y) == Derivative(y, y) + assert Derivative(expr, y).subs(y, x).doit() == \ + Derivative(expr, y).doit().subs(y, x) + assert Derivative(f(x, y), y).subs(y, x) == Subs(Derivative(f(x, y), y), y, x) + assert Derivative(f(x, y), y).subs(x, y) == Subs(Derivative(f(x, y), y), x, y) + assert Derivative(f(x, y), y).subs(y, g(x, y)) == Subs(Derivative(f(x, y), y), y, g(x, y)).doit() + assert Derivative(f(x, y), y).subs(x, g(x, y)) == Subs(Derivative(f(x, y), y), x, g(x, y)) + assert Derivative(f(x, y), g(y)).subs(x, g(x, y)) == Derivative(f(g(x, y), y), g(y)) + assert Derivative(f(u(x), h(y)), h(y)).subs(h(y), g(x, y)) == \ + Subs(Derivative(f(u(x), h(y)), h(y)), h(y), g(x, y)).doit() + assert Derivative(f(x, y), y).subs(y, z) == Derivative(f(x, z), z) + assert Derivative(f(x, y), y).subs(y, g(y)) == Derivative(f(x, g(y)), g(y)) + assert Derivative(f(g(x), h(y)), h(y)).subs(h(y), u(y)) == \ + Derivative(f(g(x), u(y)), u(y)) + assert Derivative(f(x, f(x, x)), f(x, x)).subs( + f, Lambda((x, y), x + y)) == Subs( + Derivative(z + x, z), z, 2*x) + assert Subs(Derivative(f(f(x)), x), f, cos).doit() == sin(x)*sin(cos(x)) + assert Subs(Derivative(f(f(x)), f(x)), f, cos).doit() == -sin(cos(x)) + # Issue 13791. No comparison (it's a long formula) but this used to raise an exception. + assert isinstance(v(x, y, u(x, y)).diff(y).diff(x).diff(y), Expr) + # This is also related to issues 13791 and 13795; issue 15190 + F = Lambda((x, y), exp(2*x + 3*y)) + abstract = f(x, f(x, x)).diff(x, 2) + concrete = F(x, F(x, x)).diff(x, 2) + assert (abstract.subs(f, F).doit() - concrete).simplify() == 0 + # don't introduce a new symbol if not necessary + assert x in f(x).diff(x).subs(x, 0).atoms() + # case (4) + assert Derivative(f(x,f(x,y)), x, y).subs(x, g(y) + ) == Subs(Derivative(f(x, f(x, y)), x, y), x, g(y)) + + assert Derivative(f(x, x), x).subs(x, 0 + ) == Subs(Derivative(f(x, x), x), x, 0) + # issue 15194 + assert Derivative(f(y, g(x)), (x, z)).subs(z, x + ) == Derivative(f(y, g(x)), (x, x)) + + df = f(x).diff(x) + assert df.subs(df, 1) is S.One + assert df.diff(df) is S.One + dxy = Derivative(f(x, y), x, y) + dyx = Derivative(f(x, y), y, x) + assert dxy.subs(Derivative(f(x, y), y, x), 1) is S.One + assert dxy.diff(dyx) is S.One + assert Derivative(f(x, y), x, 2, y, 3).subs( + dyx, g(x, y)) == Derivative(g(x, y), x, 1, y, 2) + assert Derivative(f(x, x - y), y).subs(x, x + y) == Subs( + Derivative(f(x, x - y), y), x, x + y) + + +def test_diff_wrt_not_allowed(): + # issue 7027 included + for wrt in ( + cos(x), re(x), x**2, x*y, 1 + x, + Derivative(cos(x), x), Derivative(f(f(x)), x)): + raises(ValueError, lambda: diff(f(x), wrt)) + # if we don't differentiate wrt then don't raise error + assert diff(exp(x*y), x*y, 0) == exp(x*y) + + +def test_diff_wrt_intlike(): + class Two: + def __int__(self): + return 2 + + assert cos(x).diff(x, Two()) == -cos(x) + + +def test_klein_gordon_lagrangian(): + m = Symbol('m') + phi = f(x, t) + + L = -(diff(phi, t)**2 - diff(phi, x)**2 - m**2*phi**2)/2 + eqna = Eq( + diff(L, phi) - diff(L, diff(phi, x), x) - diff(L, diff(phi, t), t), 0) + eqnb = Eq(diff(phi, t, t) - diff(phi, x, x) + m**2*phi, 0) + assert eqna == eqnb + + +def test_sho_lagrangian(): + m = Symbol('m') + k = Symbol('k') + x = f(t) + + L = m*diff(x, t)**2/2 - k*x**2/2 + eqna = Eq(diff(L, x), diff(L, diff(x, t), t)) + eqnb = Eq(-k*x, m*diff(x, t, t)) + assert eqna == eqnb + + assert diff(L, x, t) == diff(L, t, x) + assert diff(L, diff(x, t), t) == m*diff(x, t, 2) + assert diff(L, t, diff(x, t)) == -k*x + m*diff(x, t, 2) + + +def test_straight_line(): + F = f(x) + Fd = F.diff(x) + L = sqrt(1 + Fd**2) + assert diff(L, F) == 0 + assert diff(L, Fd) == Fd/sqrt(1 + Fd**2) + + +def test_sort_variable(): + vsort = Derivative._sort_variable_count + def vsort0(*v, reverse=False): + return [i[0] for i in vsort([(i, 0) for i in ( + reversed(v) if reverse else v)])] + + for R in range(2): + assert vsort0(y, x, reverse=R) == [x, y] + assert vsort0(f(x), x, reverse=R) == [x, f(x)] + assert vsort0(f(y), f(x), reverse=R) == [f(x), f(y)] + assert vsort0(g(x), f(y), reverse=R) == [f(y), g(x)] + assert vsort0(f(x, y), f(x), reverse=R) == [f(x), f(x, y)] + fx = f(x).diff(x) + assert vsort0(fx, y, reverse=R) == [y, fx] + fy = f(y).diff(y) + assert vsort0(fy, fx, reverse=R) == [fx, fy] + fxx = fx.diff(x) + assert vsort0(fxx, fx, reverse=R) == [fx, fxx] + assert vsort0(Basic(x), f(x), reverse=R) == [f(x), Basic(x)] + assert vsort0(Basic(y), Basic(x), reverse=R) == [Basic(x), Basic(y)] + assert vsort0(Basic(y, z), Basic(x), reverse=R) == [ + Basic(x), Basic(y, z)] + assert vsort0(fx, x, reverse=R) == [ + x, fx] if R else [fx, x] + assert vsort0(Basic(x), x, reverse=R) == [ + x, Basic(x)] if R else [Basic(x), x] + assert vsort0(Basic(f(x)), f(x), reverse=R) == [ + f(x), Basic(f(x))] if R else [Basic(f(x)), f(x)] + assert vsort0(Basic(x, z), Basic(x), reverse=R) == [ + Basic(x), Basic(x, z)] if R else [Basic(x, z), Basic(x)] + assert vsort([]) == [] + assert _aresame(vsort([(x, 1)]), [Tuple(x, 1)]) + assert vsort([(x, y), (x, z)]) == [(x, y + z)] + assert vsort([(y, 1), (x, 1 + y)]) == [(x, 1 + y), (y, 1)] + # coverage complete; legacy tests below + assert vsort([(x, 3), (y, 2), (z, 1)]) == [(x, 3), (y, 2), (z, 1)] + assert vsort([(h(x), 1), (g(x), 1), (f(x), 1)]) == [ + (f(x), 1), (g(x), 1), (h(x), 1)] + assert vsort([(z, 1), (y, 2), (x, 3), (h(x), 1), (g(x), 1), + (f(x), 1)]) == [(x, 3), (y, 2), (z, 1), (f(x), 1), (g(x), 1), + (h(x), 1)] + assert vsort([(x, 1), (f(x), 1), (y, 1), (f(y), 1)]) == [(x, 1), + (y, 1), (f(x), 1), (f(y), 1)] + assert vsort([(y, 1), (x, 2), (g(x), 1), (f(x), 1), (z, 1), + (h(x), 1), (y, 2), (x, 1)]) == [(x, 3), (y, 3), (z, 1), + (f(x), 1), (g(x), 1), (h(x), 1)] + assert vsort([(z, 1), (y, 1), (f(x), 1), (x, 1), (f(x), 1), + (g(x), 1)]) == [(x, 1), (y, 1), (z, 1), (f(x), 2), (g(x), 1)] + assert vsort([(z, 1), (y, 2), (f(x), 1), (x, 2), (f(x), 2), + (g(x), 1), (z, 2), (z, 1), (y, 1), (x, 1)]) == [(x, 3), (y, 3), + (z, 4), (f(x), 3), (g(x), 1)] + assert vsort(((y, 2), (x, 1), (y, 1), (x, 1))) == [(x, 2), (y, 3)] + assert isinstance(vsort([(x, 3), (y, 2), (z, 1)])[0], Tuple) + assert vsort([(x, 1), (f(x), 1), (x, 1)]) == [(x, 2), (f(x), 1)] + assert vsort([(y, 2), (x, 3), (z, 1)]) == [(x, 3), (y, 2), (z, 1)] + assert vsort([(h(y), 1), (g(x), 1), (f(x), 1)]) == [ + (f(x), 1), (g(x), 1), (h(y), 1)] + assert vsort([(x, 1), (y, 1), (x, 1)]) == [(x, 2), (y, 1)] + assert vsort([(f(x), 1), (f(y), 1), (f(x), 1)]) == [ + (f(x), 2), (f(y), 1)] + dfx = f(x).diff(x) + self = [(dfx, 1), (x, 1)] + assert vsort(self) == self + assert vsort([ + (dfx, 1), (y, 1), (f(x), 1), (x, 1), (f(y), 1), (x, 1)]) == [ + (y, 1), (f(x), 1), (f(y), 1), (dfx, 1), (x, 2)] + dfy = f(y).diff(y) + assert vsort([(dfy, 1), (dfx, 1)]) == [(dfx, 1), (dfy, 1)] + d2fx = dfx.diff(x) + assert vsort([(d2fx, 1), (dfx, 1)]) == [(dfx, 1), (d2fx, 1)] + + +def test_multiple_derivative(): + # Issue #15007 + assert f(x, y).diff(y, y, x, y, x + ) == Derivative(f(x, y), (x, 2), (y, 3)) + + +def test_unhandled(): + class MyExpr(Expr): + def _eval_derivative(self, s): + if not s.name.startswith('xi'): + return self + else: + return None + + eq = MyExpr(f(x), y, z) + assert diff(eq, x, y, f(x), z) == Derivative(eq, f(x)) + assert diff(eq, f(x), x) == Derivative(eq, f(x)) + assert f(x, y).diff(x,(y, z)) == Derivative(f(x, y), x, (y, z)) + assert f(x, y).diff(x,(y, 0)) == Derivative(f(x, y), x) + + +def test_nfloat(): + from sympy.core.basic import _aresame + from sympy.polys.rootoftools import rootof + + x = Symbol("x") + eq = x**Rational(4, 3) + 4*x**(S.One/3)/3 + assert _aresame(nfloat(eq), x**Rational(4, 3) + (4.0/3)*x**(S.One/3)) + assert _aresame(nfloat(eq, exponent=True), x**(4.0/3) + (4.0/3)*x**(1.0/3)) + eq = x**Rational(4, 3) + 4*x**(x/3)/3 + assert _aresame(nfloat(eq), x**Rational(4, 3) + (4.0/3)*x**(x/3)) + big = 12345678901234567890 + # specify precision to match value used in nfloat + Float_big = Float(big, 15) + assert _aresame(nfloat(big), Float_big) + assert _aresame(nfloat(big*x), Float_big*x) + assert _aresame(nfloat(x**big, exponent=True), x**Float_big) + assert nfloat(cos(x + sqrt(2))) == cos(x + nfloat(sqrt(2))) + + # issue 6342 + f = S('x*lamda + lamda**3*(x/2 + 1/2) + lamda**2 + 1/4') + assert not any(a.free_symbols for a in solveset(f.subs(x, -0.139))) + + # issue 6632 + assert nfloat(-100000*sqrt(2500000001) + 5000000001) == \ + 9.99999999800000e-11 + + # issue 7122 + eq = cos(3*x**4 + y)*rootof(x**5 + 3*x**3 + 1, 0) + assert str(nfloat(eq, exponent=False, n=1)) == '-0.7*cos(3.0*x**4 + y)' + + # issue 10933 + for ti in (dict, Dict): + d = ti({S.Half: S.Half}) + n = nfloat(d) + assert isinstance(n, ti) + assert _aresame(list(n.items()).pop(), (S.Half, Float(.5))) + for ti in (dict, Dict): + d = ti({S.Half: S.Half}) + n = nfloat(d, dkeys=True) + assert isinstance(n, ti) + assert _aresame(list(n.items()).pop(), (Float(.5), Float(.5))) + d = [S.Half] + n = nfloat(d) + assert type(n) is list + assert _aresame(n[0], Float(.5)) + assert _aresame(nfloat(Eq(x, S.Half)).rhs, Float(.5)) + assert _aresame(nfloat(S(True)), S(True)) + assert _aresame(nfloat(Tuple(S.Half))[0], Float(.5)) + assert nfloat(Eq((3 - I)**2/2 + I, 0)) == S.false + # pass along kwargs + assert nfloat([{S.Half: x}], dkeys=True) == [{Float(0.5): x}] + + # Issue 17706 + A = MutableMatrix([[1, 2], [3, 4]]) + B = MutableMatrix( + [[Float('1.0', precision=53), Float('2.0', precision=53)], + [Float('3.0', precision=53), Float('4.0', precision=53)]]) + assert _aresame(nfloat(A), B) + A = ImmutableMatrix([[1, 2], [3, 4]]) + B = ImmutableMatrix( + [[Float('1.0', precision=53), Float('2.0', precision=53)], + [Float('3.0', precision=53), Float('4.0', precision=53)]]) + assert _aresame(nfloat(A), B) + + # issue 22524 + f = Function('f') + assert not nfloat(f(2)).atoms(Float) + + +def test_issue_7068(): + from sympy.abc import a, b + f = Function('f') + y1 = Dummy('y') + y2 = Dummy('y') + func1 = f(a + y1 * b) + func2 = f(a + y2 * b) + func1_y = func1.diff(y1) + func2_y = func2.diff(y2) + assert func1_y != func2_y + z1 = Subs(f(a), a, y1) + z2 = Subs(f(a), a, y2) + assert z1 != z2 + + +def test_issue_7231(): + from sympy.abc import a + ans1 = f(x).series(x, a) + res = (f(a) + (-a + x)*Subs(Derivative(f(y), y), y, a) + + (-a + x)**2*Subs(Derivative(f(y), y, y), y, a)/2 + + (-a + x)**3*Subs(Derivative(f(y), y, y, y), + y, a)/6 + + (-a + x)**4*Subs(Derivative(f(y), y, y, y, y), + y, a)/24 + + (-a + x)**5*Subs(Derivative(f(y), y, y, y, y, y), + y, a)/120 + O((-a + x)**6, (x, a))) + assert res == ans1 + ans2 = f(x).series(x, a) + assert res == ans2 + + +def test_issue_7687(): + from sympy.core.function import Function + from sympy.abc import x + f = Function('f')(x) + ff = Function('f')(x) + match_with_cache = ff.matches(f) + assert isinstance(f, type(ff)) + clear_cache() + ff = Function('f')(x) + assert isinstance(f, type(ff)) + assert match_with_cache == ff.matches(f) + + +def test_issue_7688(): + from sympy.core.function import Function, UndefinedFunction + + f = Function('f') # actually an UndefinedFunction + clear_cache() + class A(UndefinedFunction): + pass + a = A('f') + assert isinstance(a, type(f)) + + +def test_mexpand(): + from sympy.abc import x + assert _mexpand(None) is None + assert _mexpand(1) is S.One + assert _mexpand(x*(x + 1)**2) == (x*(x + 1)**2).expand() + + +def test_issue_8469(): + # This should not take forever to run + N = 40 + def g(w, theta): + return 1/(1+exp(w-theta)) + + ws = symbols(['w%i'%i for i in range(N)]) + import functools + expr = functools.reduce(g, ws) + assert isinstance(expr, Pow) + + +def test_issue_12996(): + # foo=True imitates the sort of arguments that Derivative can get + # from Integral when it passes doit to the expression + assert Derivative(im(x), x).doit(foo=True) == Derivative(im(x), x) + + +def test_should_evalf(): + # This should not take forever to run (see #8506) + assert isinstance(sin((1.0 + 1.0*I)**10000 + 1), sin) + + +def test_Derivative_as_finite_difference(): + # Central 1st derivative at gridpoint + x, h = symbols('x h', real=True) + dfdx = f(x).diff(x) + assert (dfdx.as_finite_difference([x-2, x-1, x, x+1, x+2]) - + (S.One/12*(f(x-2)-f(x+2)) + Rational(2, 3)*(f(x+1)-f(x-1)))).simplify() == 0 + + # Central 1st derivative "half-way" + assert (dfdx.as_finite_difference() - + (f(x + S.Half)-f(x - S.Half))).simplify() == 0 + assert (dfdx.as_finite_difference(h) - + (f(x + h/S(2))-f(x - h/S(2)))/h).simplify() == 0 + assert (dfdx.as_finite_difference([x - 3*h, x-h, x+h, x + 3*h]) - + (S(9)/(8*2*h)*(f(x+h) - f(x-h)) + + S.One/(24*2*h)*(f(x - 3*h) - f(x + 3*h)))).simplify() == 0 + + # One sided 1st derivative at gridpoint + assert (dfdx.as_finite_difference([0, 1, 2], 0) - + (Rational(-3, 2)*f(0) + 2*f(1) - f(2)/2)).simplify() == 0 + assert (dfdx.as_finite_difference([x, x+h], x) - + (f(x+h) - f(x))/h).simplify() == 0 + assert (dfdx.as_finite_difference([x-h, x, x+h], x-h) - + (-S(3)/(2*h)*f(x-h) + 2/h*f(x) - + S.One/(2*h)*f(x+h))).simplify() == 0 + + # One sided 1st derivative "half-way" + assert (dfdx.as_finite_difference([x-h, x+h, x + 3*h, x + 5*h, x + 7*h]) + - 1/(2*h)*(-S(11)/(12)*f(x-h) + S(17)/(24)*f(x+h) + + Rational(3, 8)*f(x + 3*h) - Rational(5, 24)*f(x + 5*h) + + S.One/24*f(x + 7*h))).simplify() == 0 + + d2fdx2 = f(x).diff(x, 2) + # Central 2nd derivative at gridpoint + assert (d2fdx2.as_finite_difference([x-h, x, x+h]) - + h**-2 * (f(x-h) + f(x+h) - 2*f(x))).simplify() == 0 + + assert (d2fdx2.as_finite_difference([x - 2*h, x-h, x, x+h, x + 2*h]) - + h**-2 * (Rational(-1, 12)*(f(x - 2*h) + f(x + 2*h)) + + Rational(4, 3)*(f(x+h) + f(x-h)) - Rational(5, 2)*f(x))).simplify() == 0 + + # Central 2nd derivative "half-way" + assert (d2fdx2.as_finite_difference([x - 3*h, x-h, x+h, x + 3*h]) - + (2*h)**-2 * (S.Half*(f(x - 3*h) + f(x + 3*h)) - + S.Half*(f(x+h) + f(x-h)))).simplify() == 0 + + # One sided 2nd derivative at gridpoint + assert (d2fdx2.as_finite_difference([x, x+h, x + 2*h, x + 3*h]) - + h**-2 * (2*f(x) - 5*f(x+h) + + 4*f(x+2*h) - f(x+3*h))).simplify() == 0 + + # One sided 2nd derivative at "half-way" + assert (d2fdx2.as_finite_difference([x-h, x+h, x + 3*h, x + 5*h]) - + (2*h)**-2 * (Rational(3, 2)*f(x-h) - Rational(7, 2)*f(x+h) + Rational(5, 2)*f(x + 3*h) - + S.Half*f(x + 5*h))).simplify() == 0 + + d3fdx3 = f(x).diff(x, 3) + # Central 3rd derivative at gridpoint + assert (d3fdx3.as_finite_difference() - + (-f(x - Rational(3, 2)) + 3*f(x - S.Half) - + 3*f(x + S.Half) + f(x + Rational(3, 2)))).simplify() == 0 + + assert (d3fdx3.as_finite_difference( + [x - 3*h, x - 2*h, x-h, x, x+h, x + 2*h, x + 3*h]) - + h**-3 * (S.One/8*(f(x - 3*h) - f(x + 3*h)) - f(x - 2*h) + + f(x + 2*h) + Rational(13, 8)*(f(x-h) - f(x+h)))).simplify() == 0 + + # Central 3rd derivative at "half-way" + assert (d3fdx3.as_finite_difference([x - 3*h, x-h, x+h, x + 3*h]) - + (2*h)**-3 * (f(x + 3*h)-f(x - 3*h) + + 3*(f(x-h)-f(x+h)))).simplify() == 0 + + # One sided 3rd derivative at gridpoint + assert (d3fdx3.as_finite_difference([x, x+h, x + 2*h, x + 3*h]) - + h**-3 * (f(x + 3*h)-f(x) + 3*(f(x+h)-f(x + 2*h)))).simplify() == 0 + + # One sided 3rd derivative at "half-way" + assert (d3fdx3.as_finite_difference([x-h, x+h, x + 3*h, x + 5*h]) - + (2*h)**-3 * (f(x + 5*h)-f(x-h) + + 3*(f(x+h)-f(x + 3*h)))).simplify() == 0 + + # issue 11007 + y = Symbol('y', real=True) + d2fdxdy = f(x, y).diff(x, y) + + ref0 = Derivative(f(x + S.Half, y), y) - Derivative(f(x - S.Half, y), y) + assert (d2fdxdy.as_finite_difference(wrt=x) - ref0).simplify() == 0 + + half = S.Half + xm, xp, ym, yp = x-half, x+half, y-half, y+half + ref2 = f(xm, ym) + f(xp, yp) - f(xp, ym) - f(xm, yp) + assert (d2fdxdy.as_finite_difference() - ref2).simplify() == 0 + + +def test_issue_11159(): + # Tests Application._eval_subs + with _exp_is_pow(False): + expr1 = E + expr0 = expr1 * expr1 + expr1 = expr0.subs(expr1,expr0) + assert expr0 == expr1 + with _exp_is_pow(True): + expr1 = E + expr0 = expr1 * expr1 + expr2 = expr0.subs(expr1, expr0) + assert expr2 == E ** 4 + + +def test_issue_12005(): + e1 = Subs(Derivative(f(x), x), x, x) + assert e1.diff(x) == Derivative(f(x), x, x) + e2 = Subs(Derivative(f(x), x), x, x**2 + 1) + assert e2.diff(x) == 2*x*Subs(Derivative(f(x), x, x), x, x**2 + 1) + e3 = Subs(Derivative(f(x) + y**2 - y, y), y, y**2) + assert e3.diff(y) == 4*y + e4 = Subs(Derivative(f(x + y), y), y, (x**2)) + assert e4.diff(y) is S.Zero + e5 = Subs(Derivative(f(x), x), (y, z), (y, z)) + assert e5.diff(x) == Derivative(f(x), x, x) + assert f(g(x)).diff(g(x), g(x)) == Derivative(f(g(x)), g(x), g(x)) + + +def test_issue_13843(): + x = symbols('x') + f = Function('f') + m, n = symbols('m n', integer=True) + assert Derivative(Derivative(f(x), (x, m)), (x, n)) == Derivative(f(x), (x, m + n)) + assert Derivative(Derivative(f(x), (x, m+5)), (x, n+3)) == Derivative(f(x), (x, m + n + 8)) + + assert Derivative(f(x), (x, n)).doit() == Derivative(f(x), (x, n)) + + +def test_order_could_be_zero(): + x, y = symbols('x, y') + n = symbols('n', integer=True, nonnegative=True) + m = symbols('m', integer=True, positive=True) + assert diff(y, (x, n)) == Piecewise((y, Eq(n, 0)), (0, True)) + assert diff(y, (x, n + 1)) is S.Zero + assert diff(y, (x, m)) is S.Zero + + +def test_undefined_function_eq(): + f = Function('f') + f2 = Function('f') + g = Function('g') + f_real = Function('f', is_real=True) + + # This test may only be meaningful if the cache is turned off + assert f == f2 + assert hash(f) == hash(f2) + assert f == f + + assert f != g + + assert f != f_real + + +def test_function_assumptions(): + x = Symbol('x') + f = Function('f') + f_real = Function('f', real=True) + f_real1 = Function('f', real=1) + f_real_inherit = Function(Symbol('f', real=True)) + + assert f_real == f_real1 # assumptions are sanitized + assert f != f_real + assert f(x) != f_real(x) + + assert f(x).is_real is None + assert f_real(x).is_real is True + assert f_real_inherit(x).is_real is True and f_real_inherit.name == 'f' + + # Can also do it this way, but it won't be equal to f_real because of the + # way UndefinedFunction.__new__ works. Any non-recognized assumptions + # are just added literally as something which is used in the hash + f_real2 = Function('f', is_real=True) + assert f_real2(x).is_real is True + + +def test_undef_fcn_float_issue_6938(): + f = Function('ceil') + assert not f(0.3).is_number + f = Function('sin') + assert not f(0.3).is_number + assert not f(pi).evalf().is_number + x = Symbol('x') + assert not f(x).evalf(subs={x:1.2}).is_number + + +def test_undefined_function_eval(): + # Issue 15170. Make sure UndefinedFunction with eval defined works + # properly. + + fdiff = lambda self, argindex=1: cos(self.args[argindex - 1]) + eval = classmethod(lambda cls, t: None) + _imp_ = classmethod(lambda cls, t: sin(t)) + + temp = Function('temp', fdiff=fdiff, eval=eval, _imp_=_imp_) + + expr = temp(t) + assert sympify(expr) == expr + assert type(sympify(expr)).fdiff.__name__ == "" + assert expr.diff(t) == cos(t) + + +def test_issue_15241(): + F = f(x) + Fx = F.diff(x) + assert (F + x*Fx).diff(x, Fx) == 2 + assert (F + x*Fx).diff(Fx, x) == 1 + assert (x*F + x*Fx*F).diff(F, x) == x*Fx.diff(x) + Fx + 1 + assert (x*F + x*Fx*F).diff(x, F) == x*Fx.diff(x) + Fx + 1 + y = f(x) + G = f(y) + Gy = G.diff(y) + assert (G + y*Gy).diff(y, Gy) == 2 + assert (G + y*Gy).diff(Gy, y) == 1 + assert (y*G + y*Gy*G).diff(G, y) == y*Gy.diff(y) + Gy + 1 + assert (y*G + y*Gy*G).diff(y, G) == y*Gy.diff(y) + Gy + 1 + + +def test_issue_15226(): + assert Subs(Derivative(f(y), x, y), y, g(x)).doit() != 0 + + +def test_issue_7027(): + for wrt in (cos(x), re(x), Derivative(cos(x), x)): + raises(ValueError, lambda: diff(f(x), wrt)) + + +def test_derivative_quick_exit(): + assert f(x).diff(y) == 0 + assert f(x).diff(y, f(x)) == 0 + assert f(x).diff(x, f(y)) == 0 + assert f(f(x)).diff(x, f(x), f(y)) == 0 + assert f(f(x)).diff(x, f(x), y) == 0 + assert f(x).diff(g(x)) == 0 + assert f(x).diff(x, f(x).diff(x)) == 1 + df = f(x).diff(x) + assert f(x).diff(df) == 0 + dg = g(x).diff(x) + assert dg.diff(df).doit() == 0 + + +def test_issue_15084_13166(): + eq = f(x, g(x)) + assert eq.diff((g(x), y)) == Derivative(f(x, g(x)), (g(x), y)) + # issue 13166 + assert eq.diff(x, 2).doit() == ( + (Derivative(f(x, g(x)), (g(x), 2))*Derivative(g(x), x) + + Subs(Derivative(f(x, _xi_2), _xi_2, x), _xi_2, g(x)))*Derivative(g(x), + x) + Derivative(f(x, g(x)), g(x))*Derivative(g(x), (x, 2)) + + Derivative(g(x), x)*Subs(Derivative(f(_xi_1, g(x)), _xi_1, g(x)), + _xi_1, x) + Subs(Derivative(f(_xi_1, g(x)), (_xi_1, 2)), _xi_1, x)) + # issue 6681 + assert diff(f(x, t, g(x, t)), x).doit() == ( + Derivative(f(x, t, g(x, t)), g(x, t))*Derivative(g(x, t), x) + + Subs(Derivative(f(_xi_1, t, g(x, t)), _xi_1), _xi_1, x)) + # make sure the order doesn't matter when using diff + assert eq.diff(x, g(x)) == eq.diff(g(x), x) + + +def test_negative_counts(): + # issue 13873 + raises(ValueError, lambda: sin(x).diff(x, -1)) + + +def test_Derivative__new__(): + raises(TypeError, lambda: f(x).diff((x, 2), 0)) + assert f(x, y).diff([(x, y), 0]) == f(x, y) + assert f(x, y).diff([(x, y), 1]) == NDimArray([ + Derivative(f(x, y), x), Derivative(f(x, y), y)]) + assert f(x,y).diff(y, (x, z), y, x) == Derivative( + f(x, y), (x, z + 1), (y, 2)) + assert Matrix([x]).diff(x, 2) == Matrix([0]) # is_zero exit + + +def test_issue_14719_10150(): + class V(Expr): + _diff_wrt = True + is_scalar = False + assert V().diff(V()) == Derivative(V(), V()) + assert (2*V()).diff(V()) == 2*Derivative(V(), V()) + class X(Expr): + _diff_wrt = True + assert X().diff(X()) == 1 + assert (2*X()).diff(X()) == 2 + + +def test_noncommutative_issue_15131(): + x = Symbol('x', commutative=False) + t = Symbol('t', commutative=False) + fx = Function('Fx', commutative=False)(x) + ft = Function('Ft', commutative=False)(t) + A = Symbol('A', commutative=False) + eq = fx * A * ft + eqdt = eq.diff(t) + assert eqdt.args[-1] == ft.diff(t) + + +def test_Subs_Derivative(): + a = Derivative(f(g(x), h(x)), g(x), h(x),x) + b = Derivative(Derivative(f(g(x), h(x)), g(x), h(x)),x) + c = f(g(x), h(x)).diff(g(x), h(x), x) + d = f(g(x), h(x)).diff(g(x), h(x)).diff(x) + e = Derivative(f(g(x), h(x)), x) + eqs = (a, b, c, d, e) + subs = lambda arg: arg.subs(f, Lambda((x, y), exp(x + y)) + ).subs(g(x), 1/x).subs(h(x), x**3) + ans = 3*x**2*exp(1/x)*exp(x**3) - exp(1/x)*exp(x**3)/x**2 + assert all(subs(i).doit().expand() == ans for i in eqs) + assert all(subs(i.doit()).doit().expand() == ans for i in eqs) + +def test_issue_15360(): + f = Function('f') + assert f.name == 'f' + + +def test_issue_15947(): + assert f._diff_wrt is False + raises(TypeError, lambda: f(f)) + raises(TypeError, lambda: f(x).diff(f)) + + +def test_Derivative_free_symbols(): + f = Function('f') + n = Symbol('n', integer=True, positive=True) + assert diff(f(x), (x, n)).free_symbols == {n, x} + + +def test_issue_20683(): + x = Symbol('x') + y = Symbol('y') + z = Symbol('z') + y = Derivative(z, x).subs(x,0) + assert y.doit() == 0 + y = Derivative(8, x).subs(x,0) + assert y.doit() == 0 + + +def test_issue_10503(): + f = exp(x**3)*cos(x**6) + assert f.series(x, 0, 14) == 1 + x**3 + x**6/2 + x**9/6 - 11*x**12/24 + O(x**14) + + +def test_issue_17382(): + # copied from sympy/core/tests/test_evalf.py + def NS(e, n=15, **options): + return sstr(sympify(e).evalf(n, **options), full_prec=True) + + x = Symbol('x') + expr = solveset(2 * cos(x) * cos(2 * x) - 1, x, S.Reals) + expected = "Union(" \ + "ImageSet(Lambda(_n, 6.28318530717959*_n + 5.79812359592087), Integers), " \ + "ImageSet(Lambda(_n, 6.28318530717959*_n + 0.485061711258717), Integers))" + assert NS(expr) == expected + +def test_eval_sympified(): + # Check both arguments and return types from eval are sympified + + class F(Function): + @classmethod + def eval(cls, x): + assert x is S.One + return 1 + + assert F(1) is S.One + + # String arguments are not allowed + class F2(Function): + @classmethod + def eval(cls, x): + if x == 0: + return '1' + + raises(SympifyError, lambda: F2(0)) + F2(1) # Doesn't raise + + # TODO: Disable string inputs (https://github.com/sympy/sympy/issues/11003) + # raises(SympifyError, lambda: F2('2')) + +def test_eval_classmethod_check(): + with raises(TypeError): + class F(Function): + def eval(self, x): + pass + + +def test_issue_27163(): + # https://github.com/sympy/sympy/issues/27163 + raises(TypeError, lambda: Derivative(f, t)) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_kind.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_kind.py new file mode 100644 index 0000000000000000000000000000000000000000..cbfdffb9304b49488756752ca198fd4067087437 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_kind.py @@ -0,0 +1,57 @@ +from sympy.core.add import Add +from sympy.core.kind import NumberKind, UndefinedKind +from sympy.core.mul import Mul +from sympy.core.numbers import pi, zoo, I, AlgebraicNumber +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.integrals.integrals import Integral +from sympy.core.function import Derivative +from sympy.matrices import (Matrix, SparseMatrix, ImmutableMatrix, + ImmutableSparseMatrix, MatrixSymbol, MatrixKind, MatMul) + +comm_x = Symbol('x') +noncomm_x = Symbol('x', commutative=False) + +def test_NumberKind(): + assert S.One.kind is NumberKind + assert pi.kind is NumberKind + assert S.NaN.kind is NumberKind + assert zoo.kind is NumberKind + assert I.kind is NumberKind + assert AlgebraicNumber(1).kind is NumberKind + +def test_Add_kind(): + assert Add(2, 3, evaluate=False).kind is NumberKind + assert Add(2,comm_x).kind is NumberKind + assert Add(2,noncomm_x).kind is UndefinedKind + +def test_mul_kind(): + assert Mul(2,comm_x, evaluate=False).kind is NumberKind + assert Mul(2,3, evaluate=False).kind is NumberKind + assert Mul(noncomm_x,2, evaluate=False).kind is UndefinedKind + assert Mul(2,noncomm_x, evaluate=False).kind is UndefinedKind + +def test_Symbol_kind(): + assert comm_x.kind is NumberKind + assert noncomm_x.kind is UndefinedKind + +def test_Integral_kind(): + A = MatrixSymbol('A', 2,2) + assert Integral(comm_x, comm_x).kind is NumberKind + assert Integral(A, comm_x).kind is MatrixKind(NumberKind) + +def test_Derivative_kind(): + A = MatrixSymbol('A', 2,2) + assert Derivative(comm_x, comm_x).kind is NumberKind + assert Derivative(A, comm_x).kind is MatrixKind(NumberKind) + +def test_Matrix_kind(): + classes = (Matrix, SparseMatrix, ImmutableMatrix, ImmutableSparseMatrix) + for cls in classes: + m = cls.zeros(3, 2) + assert m.kind is MatrixKind(NumberKind) + +def test_MatMul_kind(): + M = Matrix([[1,2],[3,4]]) + assert MatMul(2, M).kind is MatrixKind(NumberKind) + assert MatMul(comm_x, M).kind is MatrixKind(NumberKind) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_logic.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_logic.py new file mode 100644 index 0000000000000000000000000000000000000000..df5647f32ea7c4e326eb4e3aec6a7b2987f32aee --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_logic.py @@ -0,0 +1,198 @@ +from sympy.core.logic import (fuzzy_not, Logic, And, Or, Not, fuzzy_and, + fuzzy_or, _fuzzy_group, _torf, fuzzy_nand, fuzzy_xor) +from sympy.testing.pytest import raises + +from itertools import product + +T = True +F = False +U = None + + + +def test_torf(): + v = [T, F, U] + for i in product(*[v]*3): + assert _torf(i) is (True if all(j for j in i) else + (False if all(j is False for j in i) else None)) + + +def test_fuzzy_group(): + v = [T, F, U] + for i in product(*[v]*3): + assert _fuzzy_group(i) is (None if None in i else + (True if all(j for j in i) else False)) + assert _fuzzy_group(i, quick_exit=True) is \ + (None if (i.count(False) > 1) else + (None if None in i else (True if all(j for j in i) else False))) + it = (True if (i == 0) else None for i in range(2)) + assert _torf(it) is None + it = (True if (i == 1) else None for i in range(2)) + assert _torf(it) is None + + +def test_fuzzy_not(): + assert fuzzy_not(T) == F + assert fuzzy_not(F) == T + assert fuzzy_not(U) == U + + +def test_fuzzy_and(): + assert fuzzy_and([T, T]) == T + assert fuzzy_and([T, F]) == F + assert fuzzy_and([T, U]) == U + assert fuzzy_and([F, F]) == F + assert fuzzy_and([F, U]) == F + assert fuzzy_and([U, U]) == U + assert [fuzzy_and([w]) for w in [U, T, F]] == [U, T, F] + assert fuzzy_and([T, F, U]) == F + assert fuzzy_and([]) == T + raises(TypeError, lambda: fuzzy_and()) + + +def test_fuzzy_or(): + assert fuzzy_or([T, T]) == T + assert fuzzy_or([T, F]) == T + assert fuzzy_or([T, U]) == T + assert fuzzy_or([F, F]) == F + assert fuzzy_or([F, U]) == U + assert fuzzy_or([U, U]) == U + assert [fuzzy_or([w]) for w in [U, T, F]] == [U, T, F] + assert fuzzy_or([T, F, U]) == T + assert fuzzy_or([]) == F + raises(TypeError, lambda: fuzzy_or()) + + +def test_logic_cmp(): + l1 = And('a', Not('b')) + l2 = And('a', Not('b')) + + assert hash(l1) == hash(l2) + assert (l1 == l2) == T + assert (l1 != l2) == F + + assert And('a', 'b', 'c') == And('b', 'a', 'c') + assert And('a', 'b', 'c') == And('c', 'b', 'a') + assert And('a', 'b', 'c') == And('c', 'a', 'b') + + assert Not('a') < Not('b') + assert (Not('b') < Not('a')) is False + assert (Not('a') < 2) is False + + +def test_logic_onearg(): + assert And() is True + assert Or() is False + + assert And(T) == T + assert And(F) == F + assert Or(T) == T + assert Or(F) == F + + assert And('a') == 'a' + assert Or('a') == 'a' + + +def test_logic_xnotx(): + assert And('a', Not('a')) == F + assert Or('a', Not('a')) == T + + +def test_logic_eval_TF(): + assert And(F, F) == F + assert And(F, T) == F + assert And(T, F) == F + assert And(T, T) == T + + assert Or(F, F) == F + assert Or(F, T) == T + assert Or(T, F) == T + assert Or(T, T) == T + + assert And('a', T) == 'a' + assert And('a', F) == F + assert Or('a', T) == T + assert Or('a', F) == 'a' + + +def test_logic_combine_args(): + assert And('a', 'b', 'a') == And('a', 'b') + assert Or('a', 'b', 'a') == Or('a', 'b') + + assert And(And('a', 'b'), And('c', 'd')) == And('a', 'b', 'c', 'd') + assert Or(Or('a', 'b'), Or('c', 'd')) == Or('a', 'b', 'c', 'd') + + assert Or('t', And('n', 'p', 'r'), And('n', 'r'), And('n', 'p', 'r'), 't', + And('n', 'r')) == Or('t', And('n', 'p', 'r'), And('n', 'r')) + + +def test_logic_expand(): + t = And(Or('a', 'b'), 'c') + assert t.expand() == Or(And('a', 'c'), And('b', 'c')) + + t = And(Or('a', Not('b')), 'b') + assert t.expand() == And('a', 'b') + + t = And(Or('a', 'b'), Or('c', 'd')) + assert t.expand() == \ + Or(And('a', 'c'), And('a', 'd'), And('b', 'c'), And('b', 'd')) + + +def test_logic_fromstring(): + S = Logic.fromstring + + assert S('a') == 'a' + assert S('!a') == Not('a') + assert S('a & b') == And('a', 'b') + assert S('a | b') == Or('a', 'b') + assert S('a | b & c') == And(Or('a', 'b'), 'c') + assert S('a & b | c') == Or(And('a', 'b'), 'c') + assert S('a & b & c') == And('a', 'b', 'c') + assert S('a | b | c') == Or('a', 'b', 'c') + + raises(ValueError, lambda: S('| a')) + raises(ValueError, lambda: S('& a')) + raises(ValueError, lambda: S('a | | b')) + raises(ValueError, lambda: S('a | & b')) + raises(ValueError, lambda: S('a & & b')) + raises(ValueError, lambda: S('a |')) + raises(ValueError, lambda: S('a|b')) + raises(ValueError, lambda: S('!')) + raises(ValueError, lambda: S('! a')) + raises(ValueError, lambda: S('!(a + 1)')) + raises(ValueError, lambda: S('')) + + +def test_logic_not(): + assert Not('a') != '!a' + assert Not('!a') != 'a' + assert Not(True) == False + assert Not(False) == True + + # NOTE: we may want to change default Not behaviour and put this + # functionality into some method. + assert Not(And('a', 'b')) == Or(Not('a'), Not('b')) + assert Not(Or('a', 'b')) == And(Not('a'), Not('b')) + + raises(ValueError, lambda: Not(1)) + + +def test_formatting(): + S = Logic.fromstring + raises(ValueError, lambda: S('a&b')) + raises(ValueError, lambda: S('a|b')) + raises(ValueError, lambda: S('! a')) + + +def test_fuzzy_xor(): + assert fuzzy_xor((None,)) is None + assert fuzzy_xor((None, True)) is None + assert fuzzy_xor((None, False)) is None + assert fuzzy_xor((True, False)) is True + assert fuzzy_xor((True, True)) is False + assert fuzzy_xor((True, True, False)) is False + assert fuzzy_xor((True, True, False, True)) is True + +def test_fuzzy_nand(): + for args in [(1, 0), (1, 1), (0, 0)]: + assert fuzzy_nand(args) == fuzzy_not(fuzzy_and(args)) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_match.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_match.py new file mode 100644 index 0000000000000000000000000000000000000000..b44012bebc4a5f3ec413c236dca1dec71da78cf5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_match.py @@ -0,0 +1,766 @@ +from sympy import abc +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.function import (Derivative, Function, diff) +from sympy.core.mul import Mul +from sympy.core.numbers import (Float, I, Integer, Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, Wild, symbols) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.functions.special.hyper import meijerg +from sympy.polys.polytools import Poly +from sympy.simplify.radsimp import collect +from sympy.simplify.simplify import signsimp + +from sympy.testing.pytest import XFAIL + + +def test_symbol(): + x = Symbol('x') + a, b, c, p, q = map(Wild, 'abcpq') + + e = x + assert e.match(x) == {} + assert e.matches(x) == {} + assert e.match(a) == {a: x} + + e = Rational(5) + assert e.match(c) == {c: 5} + assert e.match(e) == {} + assert e.match(e + 1) is None + + +def test_add(): + x, y, a, b, c = map(Symbol, 'xyabc') + p, q, r = map(Wild, 'pqr') + + e = a + b + assert e.match(p + b) == {p: a} + assert e.match(p + a) == {p: b} + + e = 1 + b + assert e.match(p + b) == {p: 1} + + e = a + b + c + assert e.match(a + p + c) == {p: b} + assert e.match(b + p + c) == {p: a} + + e = a + b + c + x + assert e.match(a + p + x + c) == {p: b} + assert e.match(b + p + c + x) == {p: a} + assert e.match(b) is None + assert e.match(b + p) == {p: a + c + x} + assert e.match(a + p + c) == {p: b + x} + assert e.match(b + p + c) == {p: a + x} + + e = 4*x + 5 + assert e.match(4*x + p) == {p: 5} + assert e.match(3*x + p) == {p: x + 5} + assert e.match(p*x + 5) == {p: 4} + + +def test_power(): + x, y, a, b, c = map(Symbol, 'xyabc') + p, q, r = map(Wild, 'pqr') + + e = (x + y)**a + assert e.match(p**q) == {p: x + y, q: a} + assert e.match(p**p) is None + + e = (x + y)**(x + y) + assert e.match(p**p) == {p: x + y} + assert e.match(p**q) == {p: x + y, q: x + y} + + e = (2*x)**2 + assert e.match(p*q**r) == {p: 4, q: x, r: 2} + + e = Integer(1) + assert e.match(x**p) == {p: 0} + + +def test_match_exclude(): + x = Symbol('x') + y = Symbol('y') + p = Wild("p") + q = Wild("q") + r = Wild("r") + + e = Rational(6) + assert e.match(2*p) == {p: 3} + + e = 3/(4*x + 5) + assert e.match(3/(p*x + q)) == {p: 4, q: 5} + + e = 3/(4*x + 5) + assert e.match(p/(q*x + r)) == {p: 3, q: 4, r: 5} + + e = 2/(x + 1) + assert e.match(p/(q*x + r)) == {p: 2, q: 1, r: 1} + + e = 1/(x + 1) + assert e.match(p/(q*x + r)) == {p: 1, q: 1, r: 1} + + e = 4*x + 5 + assert e.match(p*x + q) == {p: 4, q: 5} + + e = 4*x + 5*y + 6 + assert e.match(p*x + q*y + r) == {p: 4, q: 5, r: 6} + + a = Wild('a', exclude=[x]) + + e = 3*x + assert e.match(p*x) == {p: 3} + assert e.match(a*x) == {a: 3} + + e = 3*x**2 + assert e.match(p*x) == {p: 3*x} + assert e.match(a*x) is None + + e = 3*x + 3 + 6/x + assert e.match(p*x**2 + p*x + 2*p) == {p: 3/x} + assert e.match(a*x**2 + a*x + 2*a) is None + + +def test_mul(): + x, y, a, b, c = map(Symbol, 'xyabc') + p, q = map(Wild, 'pq') + + e = 4*x + assert e.match(p*x) == {p: 4} + assert e.match(p*y) is None + assert e.match(e + p*y) == {p: 0} + + e = a*x*b*c + assert e.match(p*x) == {p: a*b*c} + assert e.match(c*p*x) == {p: a*b} + + e = (a + b)*(a + c) + assert e.match((p + b)*(p + c)) == {p: a} + + e = x + assert e.match(p*x) == {p: 1} + + e = exp(x) + assert e.match(x**p*exp(x*q)) == {p: 0, q: 1} + + e = I*Poly(x, x) + assert e.match(I*p) == {p: x} + + +def test_mul_noncommutative(): + x, y = symbols('x y') + A, B, C = symbols('A B C', commutative=False) + u, v = symbols('u v', cls=Wild) + w, z = symbols('w z', cls=Wild, commutative=False) + + assert (u*v).matches(x) in ({v: x, u: 1}, {u: x, v: 1}) + assert (u*v).matches(x*y) in ({v: y, u: x}, {u: y, v: x}) + assert (u*v).matches(A) is None + assert (u*v).matches(A*B) is None + assert (u*v).matches(x*A) is None + assert (u*v).matches(x*y*A) is None + assert (u*v).matches(x*A*B) is None + assert (u*v).matches(x*y*A*B) is None + + assert (v*w).matches(x) is None + assert (v*w).matches(x*y) is None + assert (v*w).matches(A) == {w: A, v: 1} + assert (v*w).matches(A*B) == {w: A*B, v: 1} + assert (v*w).matches(x*A) == {w: A, v: x} + assert (v*w).matches(x*y*A) == {w: A, v: x*y} + assert (v*w).matches(x*A*B) == {w: A*B, v: x} + assert (v*w).matches(x*y*A*B) == {w: A*B, v: x*y} + + assert (v*w).matches(-x) is None + assert (v*w).matches(-x*y) is None + assert (v*w).matches(-A) == {w: A, v: -1} + assert (v*w).matches(-A*B) == {w: A*B, v: -1} + assert (v*w).matches(-x*A) == {w: A, v: -x} + assert (v*w).matches(-x*y*A) == {w: A, v: -x*y} + assert (v*w).matches(-x*A*B) == {w: A*B, v: -x} + assert (v*w).matches(-x*y*A*B) == {w: A*B, v: -x*y} + + assert (w*z).matches(x) is None + assert (w*z).matches(x*y) is None + assert (w*z).matches(A) is None + assert (w*z).matches(A*B) == {w: A, z: B} + assert (w*z).matches(B*A) == {w: B, z: A} + assert (w*z).matches(A*B*C) in [{w: A, z: B*C}, {w: A*B, z: C}] + assert (w*z).matches(x*A) is None + assert (w*z).matches(x*y*A) is None + assert (w*z).matches(x*A*B) is None + assert (w*z).matches(x*y*A*B) is None + + assert (w*A).matches(A) is None + assert (A*w*B).matches(A*B) is None + + assert (u*w*z).matches(x) is None + assert (u*w*z).matches(x*y) is None + assert (u*w*z).matches(A) is None + assert (u*w*z).matches(A*B) == {u: 1, w: A, z: B} + assert (u*w*z).matches(B*A) == {u: 1, w: B, z: A} + assert (u*w*z).matches(x*A) is None + assert (u*w*z).matches(x*y*A) is None + assert (u*w*z).matches(x*A*B) == {u: x, w: A, z: B} + assert (u*w*z).matches(x*B*A) == {u: x, w: B, z: A} + assert (u*w*z).matches(x*y*A*B) == {u: x*y, w: A, z: B} + assert (u*w*z).matches(x*y*B*A) == {u: x*y, w: B, z: A} + + assert (u*A).matches(x*A) == {u: x} + assert (u*A).matches(x*A*B) is None + assert (u*B).matches(x*A) is None + assert (u*A*B).matches(x*A*B) == {u: x} + assert (u*A*B).matches(x*B*A) is None + assert (u*A*B).matches(x*A) is None + + assert (u*w*A).matches(x*A*B) is None + assert (u*w*B).matches(x*A*B) == {u: x, w: A} + + assert (u*v*A*B).matches(x*A*B) in [{u: x, v: 1}, {v: x, u: 1}] + assert (u*v*A*B).matches(x*B*A) is None + assert (u*v*A*B).matches(u*v*A*C) is None + + +def test_mul_noncommutative_mismatch(): + A, B, C = symbols('A B C', commutative=False) + w = symbols('w', cls=Wild, commutative=False) + + assert (w*B*w).matches(A*B*A) == {w: A} + assert (w*B*w).matches(A*C*B*A*C) == {w: A*C} + assert (w*B*w).matches(A*C*B*A*B) is None + assert (w*B*w).matches(A*B*C) is None + assert (w*w*C).matches(A*B*C) is None + + +def test_mul_noncommutative_pow(): + A, B, C = symbols('A B C', commutative=False) + w = symbols('w', cls=Wild, commutative=False) + + assert (A*B*w).matches(A*B**2) == {w: B} + assert (A*(B**2)*w*(B**3)).matches(A*B**8) == {w: B**3} + assert (A*B*w*C).matches(A*(B**4)*C) == {w: B**3} + + assert (A*B*(w**(-1))).matches(A*B*(C**(-1))) == {w: C} + assert (A*(B*w)**(-1)*C).matches(A*(B*C)**(-1)*C) == {w: C} + + assert ((w**2)*B*C).matches((A**2)*B*C) == {w: A} + assert ((w**2)*B*(w**3)).matches((A**2)*B*(A**3)) == {w: A} + assert ((w**2)*B*(w**4)).matches((A**2)*B*(A**2)) is None + +def test_complex(): + a, b, c = map(Symbol, 'abc') + x, y = map(Wild, 'xy') + + assert (1 + I).match(x + I) == {x: 1} + assert (a + I).match(x + I) == {x: a} + assert (2*I).match(x*I) == {x: 2} + assert (a*I).match(x*I) == {x: a} + assert (a*I).match(x*y) == {x: I, y: a} + assert (2*I).match(x*y) == {x: 2, y: I} + assert (a + b*I).match(x + y*I) == {x: a, y: b} + + +def test_functions(): + from sympy.core.function import WildFunction + x = Symbol('x') + g = WildFunction('g') + p = Wild('p') + q = Wild('q') + + f = cos(5*x) + notf = x + assert f.match(p*cos(q*x)) == {p: 1, q: 5} + assert f.match(p*g) == {p: 1, g: cos(5*x)} + assert notf.match(g) is None + + +@XFAIL +def test_functions_X1(): + from sympy.core.function import WildFunction + x = Symbol('x') + g = WildFunction('g') + p = Wild('p') + q = Wild('q') + + f = cos(5*x) + assert f.match(p*g(q*x)) == {p: 1, g: cos, q: 5} + + +def test_interface(): + x, y = map(Symbol, 'xy') + p, q = map(Wild, 'pq') + + assert (x + 1).match(p + 1) == {p: x} + assert (x*3).match(p*3) == {p: x} + assert (x**3).match(p**3) == {p: x} + assert (x*cos(y)).match(p*cos(q)) == {p: x, q: y} + + assert (x*y).match(p*q) in [{p:x, q:y}, {p:y, q:x}] + assert (x + y).match(p + q) in [{p:x, q:y}, {p:y, q:x}] + assert (x*y + 1).match(p*q) in [{p:1, q:1 + x*y}, {p:1 + x*y, q:1}] + + +def test_derivative1(): + x, y = map(Symbol, 'xy') + p, q = map(Wild, 'pq') + + f = Function('f', nargs=1) + fd = Derivative(f(x), x) + + assert fd.match(p) == {p: fd} + assert (fd + 1).match(p + 1) == {p: fd} + assert (fd).match(fd) == {} + assert (3*fd).match(p*fd) is not None + assert (3*fd - 1).match(p*fd + q) == {p: 3, q: -1} + + +def test_derivative_bug1(): + f = Function("f") + x = Symbol("x") + a = Wild("a", exclude=[f, x]) + b = Wild("b", exclude=[f]) + pattern = a * Derivative(f(x), x, x) + b + expr = Derivative(f(x), x) + x**2 + d1 = {b: x**2} + d2 = pattern.xreplace(d1).matches(expr, d1) + assert d2 is None + + +def test_derivative2(): + f = Function("f") + x = Symbol("x") + a = Wild("a", exclude=[f, x]) + b = Wild("b", exclude=[f]) + e = Derivative(f(x), x) + assert e.match(Derivative(f(x), x)) == {} + assert e.match(Derivative(f(x), x, x)) is None + e = Derivative(f(x), x, x) + assert e.match(Derivative(f(x), x)) is None + assert e.match(Derivative(f(x), x, x)) == {} + e = Derivative(f(x), x) + x**2 + assert e.match(a*Derivative(f(x), x) + b) == {a: 1, b: x**2} + assert e.match(a*Derivative(f(x), x, x) + b) is None + e = Derivative(f(x), x, x) + x**2 + assert e.match(a*Derivative(f(x), x) + b) is None + assert e.match(a*Derivative(f(x), x, x) + b) == {a: 1, b: x**2} + + +def test_match_deriv_bug1(): + n = Function('n') + l = Function('l') + + x = Symbol('x') + p = Wild('p') + + e = diff(l(x), x)/x - diff(diff(n(x), x), x)/2 - \ + diff(n(x), x)**2/4 + diff(n(x), x)*diff(l(x), x)/4 + e = e.subs(n(x), -l(x)).doit() + t = x*exp(-l(x)) + t2 = t.diff(x, x)/t + assert e.match( (p*t2).expand() ) == {p: Rational(-1, 2)} + + +def test_match_bug2(): + x, y = map(Symbol, 'xy') + p, q, r = map(Wild, 'pqr') + res = (x + y).match(p + q + r) + assert (p + q + r).subs(res) == x + y + + +def test_match_bug3(): + x, a, b = map(Symbol, 'xab') + p = Wild('p') + assert (b*x*exp(a*x)).match(x*exp(p*x)) is None + + +def test_match_bug4(): + x = Symbol('x') + p = Wild('p') + e = x + assert e.match(-p*x) == {p: -1} + + +def test_match_bug5(): + x = Symbol('x') + p = Wild('p') + e = -x + assert e.match(-p*x) == {p: 1} + + +def test_match_bug6(): + x = Symbol('x') + p = Wild('p') + e = x + assert e.match(3*p*x) == {p: Rational(1)/3} + + +def test_match_polynomial(): + x = Symbol('x') + a = Wild('a', exclude=[x]) + b = Wild('b', exclude=[x]) + c = Wild('c', exclude=[x]) + d = Wild('d', exclude=[x]) + + eq = 4*x**3 + 3*x**2 + 2*x + 1 + pattern = a*x**3 + b*x**2 + c*x + d + assert eq.match(pattern) == {a: 4, b: 3, c: 2, d: 1} + assert (eq - 3*x**2).match(pattern) == {a: 4, b: 0, c: 2, d: 1} + assert (x + sqrt(2) + 3).match(a + b*x + c*x**2) == \ + {b: 1, a: sqrt(2) + 3, c: 0} + + +def test_exclude(): + x, y, a = map(Symbol, 'xya') + p = Wild('p', exclude=[1, x]) + q = Wild('q') + r = Wild('r', exclude=[sin, y]) + + assert sin(x).match(r) is None + assert cos(y).match(r) is None + + e = 3*x**2 + y*x + a + assert e.match(p*x**2 + q*x + r) == {p: 3, q: y, r: a} + + e = x + 1 + assert e.match(x + p) is None + assert e.match(p + 1) is None + assert e.match(x + 1 + p) == {p: 0} + + e = cos(x) + 5*sin(y) + assert e.match(r) is None + assert e.match(cos(y) + r) is None + assert e.match(r + p*sin(q)) == {r: cos(x), p: 5, q: y} + + +def test_floats(): + a, b = map(Wild, 'ab') + + e = cos(0.12345, evaluate=False)**2 + r = e.match(a*cos(b)**2) + assert r == {a: 1, b: Float(0.12345)} + + +def test_Derivative_bug1(): + f = Function("f") + x = abc.x + a = Wild("a", exclude=[f(x)]) + b = Wild("b", exclude=[f(x)]) + eq = f(x).diff(x) + assert eq.match(a*Derivative(f(x), x) + b) == {a: 1, b: 0} + + +def test_match_wild_wild(): + p = Wild('p') + q = Wild('q') + r = Wild('r') + + assert p.match(q + r) in [ {q: p, r: 0}, {q: 0, r: p} ] + assert p.match(q*r) in [ {q: p, r: 1}, {q: 1, r: p} ] + + p = Wild('p') + q = Wild('q', exclude=[p]) + r = Wild('r') + + assert p.match(q + r) == {q: 0, r: p} + assert p.match(q*r) == {q: 1, r: p} + + p = Wild('p') + q = Wild('q', exclude=[p]) + r = Wild('r', exclude=[p]) + + assert p.match(q + r) is None + assert p.match(q*r) is None + + +def test__combine_inverse(): + x, y = symbols("x y") + assert Mul._combine_inverse(x*I*y, x*I) == y + assert Mul._combine_inverse(x*x**(1 + y), x**(1 + y)) == x + assert Mul._combine_inverse(x*I*y, y*I) == x + assert Mul._combine_inverse(oo*I*y, y*I) is oo + assert Mul._combine_inverse(oo*I*y, oo*I) == y + assert Mul._combine_inverse(oo*I*y, oo*I) == y + assert Mul._combine_inverse(oo*y, -oo) == -y + assert Mul._combine_inverse(-oo*y, oo) == -y + assert Mul._combine_inverse((1-exp(x/y)),(exp(x/y)-1)) == -1 + assert Add._combine_inverse(oo, oo) is S.Zero + assert Add._combine_inverse(oo*I, oo*I) is S.Zero + assert Add._combine_inverse(x*oo, x*oo) is S.Zero + assert Add._combine_inverse(-x*oo, -x*oo) is S.Zero + assert Add._combine_inverse((x - oo)*(x + oo), -oo) + + +def test_issue_3773(): + x = symbols('x') + z, phi, r = symbols('z phi r') + c, A, B, N = symbols('c A B N', cls=Wild) + l = Wild('l', exclude=(0,)) + + eq = z * sin(2*phi) * r**7 + matcher = c * sin(phi*N)**l * r**A * log(r)**B + + assert eq.match(matcher) == {c: z, l: 1, N: 2, A: 7, B: 0} + assert (-eq).match(matcher) == {c: -z, l: 1, N: 2, A: 7, B: 0} + assert (x*eq).match(matcher) == {c: x*z, l: 1, N: 2, A: 7, B: 0} + assert (-7*x*eq).match(matcher) == {c: -7*x*z, l: 1, N: 2, A: 7, B: 0} + + matcher = c*sin(phi*N)**l * r**A + + assert eq.match(matcher) == {c: z, l: 1, N: 2, A: 7} + assert (-eq).match(matcher) == {c: -z, l: 1, N: 2, A: 7} + assert (x*eq).match(matcher) == {c: x*z, l: 1, N: 2, A: 7} + assert (-7*x*eq).match(matcher) == {c: -7*x*z, l: 1, N: 2, A: 7} + + +def test_issue_3883(): + from sympy.abc import gamma, mu, x + f = (-gamma * (x - mu)**2 - log(gamma) + log(2*pi))/2 + a, b, c = symbols('a b c', cls=Wild, exclude=(gamma,)) + + assert f.match(a * log(gamma) + b * gamma + c) == \ + {a: Rational(-1, 2), b: -(-mu + x)**2/2, c: log(2*pi)/2} + assert f.expand().collect(gamma).match(a * log(gamma) + b * gamma + c) == \ + {a: Rational(-1, 2), b: (-(x - mu)**2/2).expand(), c: (log(2*pi)/2).expand()} + g1 = Wild('g1', exclude=[gamma]) + g2 = Wild('g2', exclude=[gamma]) + g3 = Wild('g3', exclude=[gamma]) + assert f.expand().match(g1 * log(gamma) + g2 * gamma + g3) == \ + {g3: log(2)/2 + log(pi)/2, g1: Rational(-1, 2), g2: -mu**2/2 + mu*x - x**2/2} + + +def test_issue_4418(): + x = Symbol('x') + a, b, c = symbols('a b c', cls=Wild, exclude=(x,)) + f, g = symbols('f g', cls=Function) + + eq = diff(g(x)*f(x).diff(x), x) + + assert eq.match( + g(x).diff(x)*f(x).diff(x) + g(x)*f(x).diff(x, x) + c) == {c: 0} + assert eq.match(a*g(x).diff( + x)*f(x).diff(x) + b*g(x)*f(x).diff(x, x) + c) == {a: 1, b: 1, c: 0} + + +def test_issue_4700(): + f = Function('f') + x = Symbol('x') + a, b = symbols('a b', cls=Wild, exclude=(f(x),)) + + p = a*f(x) + b + eq1 = sin(x) + eq2 = f(x) + sin(x) + eq3 = f(x) + x + sin(x) + eq4 = x + sin(x) + + assert eq1.match(p) == {a: 0, b: sin(x)} + assert eq2.match(p) == {a: 1, b: sin(x)} + assert eq3.match(p) == {a: 1, b: x + sin(x)} + assert eq4.match(p) == {a: 0, b: x + sin(x)} + + +def test_issue_5168(): + a, b, c = symbols('a b c', cls=Wild) + x = Symbol('x') + f = Function('f') + + assert x.match(a) == {a: x} + assert x.match(a*f(x)**c) == {a: x, c: 0} + assert x.match(a*b) == {a: 1, b: x} + assert x.match(a*b*f(x)**c) == {a: 1, b: x, c: 0} + + assert (-x).match(a) == {a: -x} + assert (-x).match(a*f(x)**c) == {a: -x, c: 0} + assert (-x).match(a*b) == {a: -1, b: x} + assert (-x).match(a*b*f(x)**c) == {a: -1, b: x, c: 0} + + assert (2*x).match(a) == {a: 2*x} + assert (2*x).match(a*f(x)**c) == {a: 2*x, c: 0} + assert (2*x).match(a*b) == {a: 2, b: x} + assert (2*x).match(a*b*f(x)**c) == {a: 2, b: x, c: 0} + + assert (-2*x).match(a) == {a: -2*x} + assert (-2*x).match(a*f(x)**c) == {a: -2*x, c: 0} + assert (-2*x).match(a*b) == {a: -2, b: x} + assert (-2*x).match(a*b*f(x)**c) == {a: -2, b: x, c: 0} + + +def test_issue_4559(): + x = Symbol('x') + e = Symbol('e') + w = Wild('w', exclude=[x]) + y = Wild('y') + + # this is as it should be + + assert (3/x).match(w/y) == {w: 3, y: x} + assert (3*x).match(w*y) == {w: 3, y: x} + assert (x/3).match(y/w) == {w: 3, y: x} + assert (3*x).match(y/w) == {w: S.One/3, y: x} + assert (3*x).match(y/w) == {w: Rational(1, 3), y: x} + + # these could be allowed to fail + + assert (x/3).match(w/y) == {w: S.One/3, y: 1/x} + assert (3*x).match(w/y) == {w: 3, y: 1/x} + assert (3/x).match(w*y) == {w: 3, y: 1/x} + + # Note that solve will give + # multiple roots but match only gives one: + # + # >>> solve(x**r-y**2,y) + # [-x**(r/2), x**(r/2)] + + r = Symbol('r', rational=True) + assert (x**r).match(y**2) == {y: x**(r/2)} + assert (x**e).match(y**2) == {y: sqrt(x**e)} + + # since (x**i = y) -> x = y**(1/i) where i is an integer + # the following should also be valid as long as y is not + # zero when i is negative. + + a = Wild('a') + + e = S.Zero + assert e.match(a) == {a: e} + assert e.match(1/a) is None + assert e.match(a**.3) is None + + e = S(3) + assert e.match(1/a) == {a: 1/e} + assert e.match(1/a**2) == {a: 1/sqrt(e)} + e = pi + assert e.match(1/a) == {a: 1/e} + assert e.match(1/a**2) == {a: 1/sqrt(e)} + assert (-e).match(sqrt(a)) is None + assert (-e).match(a**2) == {a: I*sqrt(pi)} + +# The pattern matcher doesn't know how to handle (x - a)**2 == (a - x)**2. To +# avoid ambiguity in actual applications, don't put a coefficient (including a +# minus sign) in front of a wild. +@XFAIL +def test_issue_4883(): + a = Wild('a') + x = Symbol('x') + + e = [i**2 for i in (x - 2, 2 - x)] + p = [i**2 for i in (x - a, a- x)] + for eq in e: + for pat in p: + assert eq.match(pat) == {a: 2} + + +def test_issue_4319(): + x, y = symbols('x y') + + p = -x*(S.One/8 - y) + ans = {S.Zero, y - S.One/8} + + def ok(pat): + assert set(p.match(pat).values()) == ans + + ok(Wild("coeff", exclude=[x])*x + Wild("rest")) + ok(Wild("w", exclude=[x])*x + Wild("rest")) + ok(Wild("coeff", exclude=[x])*x + Wild("rest")) + ok(Wild("w", exclude=[x])*x + Wild("rest")) + ok(Wild("e", exclude=[x])*x + Wild("rest")) + ok(Wild("ress", exclude=[x])*x + Wild("rest")) + ok(Wild("resu", exclude=[x])*x + Wild("rest")) + + +def test_issue_3778(): + p, c, q = symbols('p c q', cls=Wild) + x = Symbol('x') + + assert (sin(x)**2).match(sin(p)*sin(q)*c) == {q: x, c: 1, p: x} + assert (2*sin(x)).match(sin(p) + sin(q) + c) == {q: x, c: 0, p: x} + + +def test_issue_6103(): + x = Symbol('x') + a = Wild('a') + assert (-I*x*oo).match(I*a*oo) == {a: -x} + + +def test_issue_3539(): + a = Wild('a') + x = Symbol('x') + assert (x - 2).match(a - x) is None + assert (6/x).match(a*x) is None + assert (6/x**2).match(a/x) == {a: 6/x} + +def test_gh_issue_2711(): + x = Symbol('x') + f = meijerg(((), ()), ((0,), ()), x) + a = Wild('a') + b = Wild('b') + + assert f.find(a) == {(S.Zero,), ((), ()), ((S.Zero,), ()), x, S.Zero, + (), meijerg(((), ()), ((S.Zero,), ()), x)} + assert f.find(a + b) == \ + {meijerg(((), ()), ((S.Zero,), ()), x), x, S.Zero} + assert f.find(a**2) == {meijerg(((), ()), ((S.Zero,), ()), x), x} + + +def test_issue_17354(): + from sympy.core.symbol import (Wild, symbols) + x, y = symbols("x y", real=True) + a, b = symbols("a b", cls=Wild) + assert ((0 <= x).reversed | (y <= x)).match((1/a <= b) | (a <= b)) is None + + +def test_match_issue_17397(): + f = Function("f") + x = Symbol("x") + a3 = Wild('a3', exclude=[f(x), f(x).diff(x), f(x).diff(x, 2)]) + b3 = Wild('b3', exclude=[f(x), f(x).diff(x), f(x).diff(x, 2)]) + c3 = Wild('c3', exclude=[f(x), f(x).diff(x), f(x).diff(x, 2)]) + deq = a3*(f(x).diff(x, 2)) + b3*f(x).diff(x) + c3*f(x) + + eq = (x-2)**2*(f(x).diff(x, 2)) + (x-2)*(f(x).diff(x)) + ((x-2)**2 - 4)*f(x) + r = collect(eq, [f(x).diff(x, 2), f(x).diff(x), f(x)]).match(deq) + assert r == {a3: (x - 2)**2, c3: (x - 2)**2 - 4, b3: x - 2} + + eq =x*f(x) + x*Derivative(f(x), (x, 2)) - 4*f(x) + Derivative(f(x), x) \ + - 4*Derivative(f(x), (x, 2)) - 2*Derivative(f(x), x)/x + 4*Derivative(f(x), (x, 2))/x + r = collect(eq, [f(x).diff(x, 2), f(x).diff(x), f(x)]).match(deq) + assert r == {a3: x - 4 + 4/x, b3: 1 - 2/x, c3: x - 4} + + +def test_match_issue_21942(): + a, r, w = symbols('a, r, w', nonnegative=True) + p = symbols('p', positive=True) + g_ = Wild('g') + pattern = g_ ** (1 / (1 - p)) + eq = (a * r ** (1 - p) + w ** (1 - p) * (1 - a)) ** (1 / (1 - p)) + m = {g_: a * r ** (1 - p) + w ** (1 - p) * (1 - a)} + assert pattern.matches(eq) == m + assert (-pattern).matches(-eq) == m + assert pattern.matches(signsimp(eq)) is None + + +def test_match_terms(): + X, Y = map(Wild, "XY") + x, y, z = symbols('x y z') + assert (5*y - x).match(5*X - Y) == {X: y, Y: x} + # 15907 + assert (x + (y - 1)*z).match(x + X*z) == {X: y - 1} + # 20747 + assert (x - log(x/y)*(1-exp(x/y))).match(x - log(X/y)*(1-exp(x/y))) == {X: x} + + +def test_match_bound(): + V, W = map(Wild, "VW") + x, y = symbols('x y') + assert Sum(x, (x, 1, 2)).match(Sum(y, (y, 1, W))) is None + assert Sum(x, (x, 1, 2)).match(Sum(V, (V, 1, W))) == {W: 2, V:x} + assert Sum(x, (x, 1, 2)).match(Sum(V, (V, 1, 2))) == {V:x} + + +def test_issue_22462(): + x, f = symbols('x'), Function('f') + n, Q = symbols('n Q', cls=Wild) + pattern = -Q*f(x)**n + eq = 5*f(x)**2 + assert pattern.matches(eq) == {n: 2, Q: -5} diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_multidimensional.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_multidimensional.py new file mode 100644 index 0000000000000000000000000000000000000000..765c78adf8dbed2ead43721ca4ab9510dbeeb282 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_multidimensional.py @@ -0,0 +1,24 @@ +from sympy.core.function import (Derivative, Function, diff) +from sympy.core.symbol import symbols +from sympy.functions.elementary.trigonometric import sin +from sympy.core.multidimensional import vectorize +x, y, z = symbols('x y z') +f, g, h = list(map(Function, 'fgh')) + + +def test_vectorize(): + @vectorize(0) + def vsin(x): + return sin(x) + + assert vsin([1, x, y]) == [sin(1), sin(x), sin(y)] + + @vectorize(0, 1) + def vdiff(f, y): + return diff(f, y) + + assert vdiff([f(x, y, z), g(x, y, z), h(x, y, z)], [x, y, z]) == \ + [[Derivative(f(x, y, z), x), Derivative(f(x, y, z), y), + Derivative(f(x, y, z), z)], [Derivative(g(x, y, z), x), + Derivative(g(x, y, z), y), Derivative(g(x, y, z), z)], + [Derivative(h(x, y, z), x), Derivative(h(x, y, z), y), Derivative(h(x, y, z), z)]] diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_noncommutative.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_noncommutative.py new file mode 100644 index 0000000000000000000000000000000000000000..b3d3a3cec2ef64aa500aad08b438c90cc8987581 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_noncommutative.py @@ -0,0 +1,140 @@ +"""Tests for noncommutative symbols and expressions.""" + +from sympy.core.function import expand +from sympy.core.numbers import I +from sympy.core.symbol import symbols +from sympy.functions.elementary.complexes import (adjoint, conjugate, transpose) +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.polys.polytools import (cancel, factor) +from sympy.simplify.combsimp import combsimp +from sympy.simplify.gammasimp import gammasimp +from sympy.simplify.radsimp import (collect, radsimp, rcollect) +from sympy.simplify.ratsimp import ratsimp +from sympy.simplify.simplify import (posify, simplify) +from sympy.simplify.trigsimp import trigsimp +from sympy.abc import x, y, z +from sympy.testing.pytest import XFAIL + +A, B, C = symbols("A B C", commutative=False) +X = symbols("X", commutative=False, hermitian=True) +Y = symbols("Y", commutative=False, antihermitian=True) + + +def test_adjoint(): + assert adjoint(A).is_commutative is False + assert adjoint(A*A) == adjoint(A)**2 + assert adjoint(A*B) == adjoint(B)*adjoint(A) + assert adjoint(A*B**2) == adjoint(B)**2*adjoint(A) + assert adjoint(A*B - B*A) == adjoint(B)*adjoint(A) - adjoint(A)*adjoint(B) + assert adjoint(A + I*B) == adjoint(A) - I*adjoint(B) + + assert adjoint(X) == X + assert adjoint(-I*X) == I*X + assert adjoint(Y) == -Y + assert adjoint(-I*Y) == -I*Y + + assert adjoint(X) == conjugate(transpose(X)) + assert adjoint(Y) == conjugate(transpose(Y)) + assert adjoint(X) == transpose(conjugate(X)) + assert adjoint(Y) == transpose(conjugate(Y)) + + +def test_cancel(): + assert cancel(A*B - B*A) == A*B - B*A + assert cancel(A*B*(x - 1)) == A*B*(x - 1) + assert cancel(A*B*(x**2 - 1)/(x + 1)) == A*B*(x - 1) + assert cancel(A*B*(x**2 - 1)/(x + 1) - B*A*(x - 1)) == A*B*(x - 1) + (1 - x)*B*A + + +@XFAIL +def test_collect(): + assert collect(A*B - B*A, A) == A*B - B*A + assert collect(A*B - B*A, B) == A*B - B*A + assert collect(A*B - B*A, x) == A*B - B*A + + +def test_combsimp(): + assert combsimp(A*B - B*A) == A*B - B*A + + +def test_gammasimp(): + assert gammasimp(A*B - B*A) == A*B - B*A + + +def test_conjugate(): + assert conjugate(A).is_commutative is False + assert (A*A).conjugate() == conjugate(A)**2 + assert (A*B).conjugate() == conjugate(A)*conjugate(B) + assert (A*B**2).conjugate() == conjugate(A)*conjugate(B)**2 + assert (A*B - B*A).conjugate() == \ + conjugate(A)*conjugate(B) - conjugate(B)*conjugate(A) + assert (A*B).conjugate() - (B*A).conjugate() == \ + conjugate(A)*conjugate(B) - conjugate(B)*conjugate(A) + assert (A + I*B).conjugate() == conjugate(A) - I*conjugate(B) + + +def test_expand(): + assert expand((A*B)**2) == A*B*A*B + assert expand(A*B - B*A) == A*B - B*A + assert expand((A*B/A)**2) == A*B*B/A + assert expand(B*A*(A + B)*B) == B*A**2*B + B*A*B**2 + assert expand(B*A*(A + C)*B) == B*A**2*B + B*A*C*B + + +def test_factor(): + assert factor(A*B - B*A) == A*B - B*A + + +def test_posify(): + assert posify(A)[0].is_commutative is False + for q in (A*B/A, (A*B/A)**2, (A*B)**2, A*B - B*A): + p = posify(q) + assert p[0].subs(p[1]) == q + + +def test_radsimp(): + assert radsimp(A*B - B*A) == A*B - B*A + + +@XFAIL +def test_ratsimp(): + assert ratsimp(A*B - B*A) == A*B - B*A + + +@XFAIL +def test_rcollect(): + assert rcollect(A*B - B*A, A) == A*B - B*A + assert rcollect(A*B - B*A, B) == A*B - B*A + assert rcollect(A*B - B*A, x) == A*B - B*A + + +def test_simplify(): + assert simplify(A*B - B*A) == A*B - B*A + + +def test_subs(): + assert (x*y*A).subs(x*y, z) == A*z + assert (x*A*B).subs(x*A, C) == C*B + assert (x*A*x*x).subs(x**2*A, C) == x*C + assert (x*A*x*B).subs(x**2*A, C) == C*B + assert (A**2*B**2).subs(A*B**2, C) == A*C + assert (A*A*A + A*B*A).subs(A*A*A, C) == C + A*B*A + + +def test_transpose(): + assert transpose(A).is_commutative is False + assert transpose(A*A) == transpose(A)**2 + assert transpose(A*B) == transpose(B)*transpose(A) + assert transpose(A*B**2) == transpose(B)**2*transpose(A) + assert transpose(A*B - B*A) == \ + transpose(B)*transpose(A) - transpose(A)*transpose(B) + assert transpose(A + I*B) == transpose(A) + I*transpose(B) + + assert transpose(X) == conjugate(X) + assert transpose(-I*X) == -I*conjugate(X) + assert transpose(Y) == -conjugate(Y) + assert transpose(-I*Y) == I*conjugate(Y) + + +def test_trigsimp(): + assert trigsimp(A*sin(x)**2 + A*cos(x)**2) == A diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_numbers.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_numbers.py new file mode 100644 index 0000000000000000000000000000000000000000..10d14b6ac09fb0ab102c37cb99405440bd0bbffb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_numbers.py @@ -0,0 +1,2335 @@ +import numbers as nums +import decimal +from sympy.concrete.summations import Sum +from sympy.core import (EulerGamma, Catalan, TribonacciConstant, + GoldenRatio) +from sympy.core.containers import Tuple +from sympy.core.expr import unchanged +from sympy.core.logic import fuzzy_not +from sympy.core.mul import Mul +from sympy.core.numbers import (mpf_norm, seterr, + Integer, I, pi, comp, Rational, E, nan, + oo, AlgebraicNumber, Number, Float, zoo, equal_valued, + int_valued, all_close) +from sympy.core.intfunc import (igcd, igcdex, igcd2, igcd_lehmer, + ilcm, integer_nthroot, isqrt, integer_log, mod_inverse) +from sympy.core.power import Pow +from sympy.core.relational import Ge, Gt, Le, Lt +from sympy.core.singleton import S +from sympy.core.symbol import Dummy, Symbol +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.integers import floor +from sympy.functions.combinatorial.numbers import fibonacci +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.miscellaneous import sqrt, cbrt +from sympy.functions.elementary.trigonometric import cos, sin +from sympy.polys.domains.realfield import RealField +from sympy.printing.latex import latex +from sympy.printing.repr import srepr +from sympy.simplify import simplify +from sympy.polys.domains.groundtypes import PythonRational +from sympy.utilities.decorator import conserve_mpmath_dps +from sympy.utilities.iterables import permutations +from sympy.testing.pytest import (XFAIL, raises, _both_exp_pow, + warns_deprecated_sympy) +from sympy import Add + +from mpmath import mpf +import mpmath +from sympy.core import numbers +t = Symbol('t', real=False) + +_ninf = float(-oo) +_inf = float(oo) + + +def same_and_same_prec(a, b): + # stricter matching for Floats + return a == b and a._prec == b._prec + + +def test_seterr(): + seterr(divide=True) + raises(ValueError, lambda: S.Zero/S.Zero) + seterr(divide=False) + assert S.Zero / S.Zero is S.NaN + + +def test_mod(): + x = S.Half + y = Rational(3, 4) + z = Rational(5, 18043) + + assert x % x == 0 + assert x % y == S.Half + assert x % z == Rational(3, 36086) + assert y % x == Rational(1, 4) + assert y % y == 0 + assert y % z == Rational(9, 72172) + assert z % x == Rational(5, 18043) + assert z % y == Rational(5, 18043) + assert z % z == 0 + + a = Float(2.6) + + assert (a % .2) == 0.0 + assert (a % 2).round(15) == 0.6 + assert (a % 0.5).round(15) == 0.1 + + p = Symbol('p', infinite=True) + + assert oo % oo is nan + assert zoo % oo is nan + assert 5 % oo is nan + assert p % 5 is nan + + # In these two tests, if the precision of m does + # not match the precision of the ans, then it is + # likely that the change made now gives an answer + # with degraded accuracy. + r = Rational(500, 41) + f = Float('.36', 3) + m = r % f + ans = Float(r % Rational(f), 3) + assert m == ans and m._prec == ans._prec + f = Float('8.36', 3) + m = f % r + ans = Float(Rational(f) % r, 3) + assert m == ans and m._prec == ans._prec + + s = S.Zero + + assert s % float(1) == 0.0 + + # No rounding required since these numbers can be represented + # exactly. + assert Rational(3, 4) % Float(1.1) == 0.75 + assert Float(1.5) % Rational(5, 4) == 0.25 + assert Rational(5, 4).__rmod__(Float('1.5')) == 0.25 + assert Float('1.5').__rmod__(Float('2.75')) == Float('1.25') + assert 2.75 % Float('1.5') == Float('1.25') + + a = Integer(7) + b = Integer(4) + + assert type(a % b) == Integer + assert a % b == Integer(3) + assert Integer(1) % Rational(2, 3) == Rational(1, 3) + assert Rational(7, 5) % Integer(1) == Rational(2, 5) + assert Integer(2) % 1.5 == 0.5 + + assert Integer(3).__rmod__(Integer(10)) == Integer(1) + assert Integer(10) % 4 == Integer(2) + assert 15 % Integer(4) == Integer(3) + + +def test_divmod(): + x = Symbol("x") + assert divmod(S(12), S(8)) == Tuple(1, 4) + assert divmod(-S(12), S(8)) == Tuple(-2, 4) + assert divmod(S.Zero, S.One) == Tuple(0, 0) + raises(ZeroDivisionError, lambda: divmod(S.Zero, S.Zero)) + raises(ZeroDivisionError, lambda: divmod(S.One, S.Zero)) + assert divmod(S(12), 8) == Tuple(1, 4) + assert divmod(12, S(8)) == Tuple(1, 4) + assert S(1024)//x == 1024//x == floor(1024/x) + + assert divmod(S("2"), S("3/2")) == Tuple(S("1"), S("1/2")) + assert divmod(S("3/2"), S("2")) == Tuple(S("0"), S("3/2")) + assert divmod(S("2"), S("3.5")) == Tuple(S("0"), S("2.")) + assert divmod(S("3.5"), S("2")) == Tuple(S("1"), S("1.5")) + assert divmod(S("2"), S("1/3")) == Tuple(S("6"), S("0")) + assert divmod(S("1/3"), S("2")) == Tuple(S("0"), S("1/3")) + assert divmod(S("2"), S("1/10")) == Tuple(S("20"), S("0")) + assert divmod(S("2"), S(".1"))[0] == 19 + assert divmod(S("0.1"), S("2")) == Tuple(S("0"), S("0.1")) + assert divmod(S("2"), 2) == Tuple(S("1"), S("0")) + assert divmod(2, S("2")) == Tuple(S("1"), S("0")) + assert divmod(S("2"), 1.5) == Tuple(S("1"), S("0.5")) + assert divmod(1.5, S("2")) == Tuple(S("0"), S("1.5")) + assert divmod(0.3, S("2")) == Tuple(S("0"), S("0.3")) + assert divmod(S("3/2"), S("3.5")) == Tuple(S("0"), S(3/2)) + assert divmod(S("3.5"), S("3/2")) == Tuple(S("2"), S("0.5")) + assert divmod(S("3/2"), S("1/3")) == Tuple(S("4"), S("1/6")) + assert divmod(S("1/3"), S("3/2")) == Tuple(S("0"), S("1/3")) + assert divmod(S("3/2"), S("0.1"))[0] == 14 + assert divmod(S("0.1"), S("3/2")) == Tuple(S("0"), S("0.1")) + assert divmod(S("3/2"), 2) == Tuple(S("0"), S("3/2")) + assert divmod(2, S("3/2")) == Tuple(S("1"), S("1/2")) + assert divmod(S("3/2"), 1.5) == Tuple(S("1"), S("0.")) + assert divmod(1.5, S("3/2")) == Tuple(S("1"), S("0.")) + assert divmod(S("3/2"), 0.3) == Tuple(S("5"), S("0.")) + assert divmod(0.3, S("3/2")) == Tuple(S("0"), S("0.3")) + assert divmod(S("1/3"), S("3.5")) == (0, 1/3) + assert divmod(S("3.5"), S("0.1")) == Tuple(S("35"), S("0.")) + assert divmod(S("0.1"), S("3.5")) == Tuple(S("0"), S("0.1")) + assert divmod(S("3.5"), 2) == Tuple(S("1"), S("1.5")) + assert divmod(2, S("3.5")) == Tuple(S("0"), S("2.")) + assert divmod(S("3.5"), 1.5) == Tuple(S("2"), S("0.5")) + assert divmod(1.5, S("3.5")) == Tuple(S("0"), S("1.5")) + assert divmod(0.3, S("3.5")) == Tuple(S("0"), S("0.3")) + assert divmod(S("0.1"), S("1/3")) == Tuple(S("0"), S("0.1")) + assert divmod(S("1/3"), 2) == Tuple(S("0"), S("1/3")) + assert divmod(2, S("1/3")) == Tuple(S("6"), S("0")) + assert divmod(S("1/3"), 1.5) == (0, 1/3) + assert divmod(0.3, S("1/3")) == (0, 0.3) + assert divmod(S("0.1"), 2) == (0, 0.1) + assert divmod(2, S("0.1"))[0] == 19 + assert divmod(S("0.1"), 1.5) == (0, 0.1) + assert divmod(1.5, S("0.1")) == Tuple(S("15"), S("0.")) + assert divmod(S("0.1"), 0.3) == Tuple(S("0"), S("0.1")) + + assert str(divmod(S("2"), 0.3)) == '(6, 0.2)' + assert str(divmod(S("3.5"), S("1/3"))) == '(10, 0.166666666666667)' + assert str(divmod(S("3.5"), 0.3)) == '(11, 0.2)' + assert str(divmod(S("1/3"), S("0.1"))) == '(3, 0.0333333333333333)' + assert str(divmod(1.5, S("1/3"))) == '(4, 0.166666666666667)' + assert str(divmod(S("1/3"), 0.3)) == '(1, 0.0333333333333333)' + assert str(divmod(0.3, S("0.1"))) == '(2, 0.1)' + + assert divmod(-3, S(2)) == (-2, 1) + assert divmod(S(-3), S(2)) == (-2, 1) + assert divmod(S(-3), 2) == (-2, 1) + + assert divmod(oo, 1) == (S.NaN, S.NaN) + assert divmod(S.NaN, 1) == (S.NaN, S.NaN) + assert divmod(1, S.NaN) == (S.NaN, S.NaN) + ans = [(-1, oo), (-1, oo), (0, 0), (0, 1), (0, 2)] + OO = float('inf') + ANS = [tuple(map(float, i)) for i in ans] + assert [divmod(i, oo) for i in range(-2, 3)] == ans + ans = [(0, -2), (0, -1), (0, 0), (-1, -oo), (-1, -oo)] + ANS = [tuple(map(float, i)) for i in ans] + assert [divmod(i, -oo) for i in range(-2, 3)] == ans + assert [divmod(i, -OO) for i in range(-2, 3)] == ANS + + # sympy's divmod gives an Integer for the quotient rather than a float + dmod = lambda a, b: tuple([j if i else int(j) for i, j in enumerate(divmod(a, b))]) + for a in (4, 4., 4.25, 0, 0., -4, -4. -4.25): + for b in (2, 2., 2.5, -2, -2., -2.5): + assert divmod(S(a), S(b)) == dmod(a, b) + + +def test_igcd(): + assert igcd(0, 0) == 0 + assert igcd(0, 1) == 1 + assert igcd(1, 0) == 1 + assert igcd(0, 7) == 7 + assert igcd(7, 0) == 7 + assert igcd(7, 1) == 1 + assert igcd(1, 7) == 1 + assert igcd(-1, 0) == 1 + assert igcd(0, -1) == 1 + assert igcd(-1, -1) == 1 + assert igcd(-1, 7) == 1 + assert igcd(7, -1) == 1 + assert igcd(8, 2) == 2 + assert igcd(4, 8) == 4 + assert igcd(8, 16) == 8 + assert igcd(7, -3) == 1 + assert igcd(-7, 3) == 1 + assert igcd(-7, -3) == 1 + assert igcd(*[10, 20, 30]) == 10 + raises(TypeError, lambda: igcd()) + raises(TypeError, lambda: igcd(2)) + raises(ValueError, lambda: igcd(0, None)) + raises(ValueError, lambda: igcd(1, 2.2)) + for args in permutations((45.1, 1, 30)): + raises(ValueError, lambda: igcd(*args)) + for args in permutations((1, 2, None)): + raises(ValueError, lambda: igcd(*args)) + + +def test_igcd_lehmer(): + a, b = fibonacci(10001), fibonacci(10000) + # len(str(a)) == 2090 + # small divisors, long Euclidean sequence + assert igcd_lehmer(a, b) == 1 + c = fibonacci(100) + assert igcd_lehmer(a*c, b*c) == c + # big divisor + assert igcd_lehmer(a, 10**1000) == 1 + # swapping argument + assert igcd_lehmer(1, 2) == igcd_lehmer(2, 1) + + +def test_igcd2(): + # short loop + assert igcd2(2**100 - 1, 2**99 - 1) == 1 + # Lehmer's algorithm + a, b = int(fibonacci(10001)), int(fibonacci(10000)) + assert igcd2(a, b) == 1 + + +def test_ilcm(): + assert ilcm(0, 0) == 0 + assert ilcm(1, 0) == 0 + assert ilcm(0, 1) == 0 + assert ilcm(1, 1) == 1 + assert ilcm(2, 1) == 2 + assert ilcm(8, 2) == 8 + assert ilcm(8, 6) == 24 + assert ilcm(8, 7) == 56 + assert ilcm(*[10, 20, 30]) == 60 + raises(ValueError, lambda: ilcm(8.1, 7)) + raises(ValueError, lambda: ilcm(8, 7.1)) + raises(TypeError, lambda: ilcm(8)) + + +def test_igcdex(): + assert igcdex(2, 3) == (-1, 1, 1) + assert igcdex(10, 12) == (-1, 1, 2) + assert igcdex(100, 2004) == (-20, 1, 4) + assert igcdex(0, 0) == (0, 0, 0) + assert igcdex(1, 0) == (1, 0, 1) + + +def _strictly_equal(a, b): + return (a.p, a.q, type(a.p), type(a.q)) == \ + (b.p, b.q, type(b.p), type(b.q)) + + +def _test_rational_new(cls): + """ + Tests that are common between Integer and Rational. + """ + assert cls(0) is S.Zero + assert cls(1) is S.One + assert cls(-1) is S.NegativeOne + # These look odd, but are similar to int(): + assert cls('1') is S.One + assert cls('-1') is S.NegativeOne + + i = Integer(10) + assert _strictly_equal(i, cls('10')) + assert _strictly_equal(i, cls('10')) + assert _strictly_equal(i, cls(int(10))) + assert _strictly_equal(i, cls(i)) + + raises(TypeError, lambda: cls(Symbol('x'))) + + +def test_Integer_new(): + """ + Test for Integer constructor + """ + _test_rational_new(Integer) + + assert _strictly_equal(Integer(0.9), S.Zero) + assert _strictly_equal(Integer(10.5), Integer(10)) + raises(ValueError, lambda: Integer("10.5")) + assert Integer(Rational('1.' + '9'*20)) == 1 + + +def test_Rational_new(): + """" + Test for Rational constructor + """ + _test_rational_new(Rational) + + n1 = S.Half + assert n1 == Rational(Integer(1), 2) + assert n1 == Rational(Integer(1), Integer(2)) + assert n1 == Rational(1, Integer(2)) + assert n1 == Rational(S.Half) + assert 1 == Rational(n1, n1) + assert Rational(3, 2) == Rational(S.Half, Rational(1, 3)) + assert Rational(3, 1) == Rational(1, Rational(1, 3)) + n3_4 = Rational(3, 4) + assert Rational('3/4') == n3_4 + assert -Rational('-3/4') == n3_4 + assert Rational('.76').limit_denominator(4) == n3_4 + assert Rational(19, 25).limit_denominator(4) == n3_4 + assert Rational('19/25').limit_denominator(4) == n3_4 + assert Rational(1.0, 3) == Rational(1, 3) + assert Rational(1, 3.0) == Rational(1, 3) + assert Rational(Float(0.5)) == S.Half + assert Rational('1e2/1e-2') == Rational(10000) + assert Rational('1 234') == Rational(1234) + assert Rational('1/1 234') == Rational(1, 1234) + assert Rational(-1, 0) is S.ComplexInfinity + assert Rational(1, 0) is S.ComplexInfinity + # Make sure Rational doesn't lose precision on Floats + assert Rational(pi.evalf(100)).evalf(100) == pi.evalf(100) + raises(TypeError, lambda: Rational('3**3')) + raises(TypeError, lambda: Rational('1/2 + 2/3')) + + # handle fractions.Fraction instances + try: + import fractions + assert Rational(fractions.Fraction(1, 2)) == S.Half + except ImportError: + pass + + assert Rational(PythonRational(2, 6)) == Rational(1, 3) + + with warns_deprecated_sympy(): + assert Rational(2, 4, gcd=1).q == 4 + with warns_deprecated_sympy(): + n = Rational(2, -4, gcd=1) + assert n.q == 4 + assert n.p == -2 + + assert Rational.from_coprime_ints(3, 5) == Rational(3, 5) + + +def test_issue_24543(): + for p in ('1.5', 1.5, 2): + for q in ('1.5', 1.5, 2): + assert Rational(p, q).as_numer_denom() == Rational('%s/%s'%(p,q)).as_numer_denom() + + assert Rational('0.5', '100') == Rational(1, 200) + + +def test_Number_new(): + """" + Test for Number constructor + """ + # Expected behavior on numbers and strings + assert Number(1) is S.One + assert Number(2).__class__ is Integer + assert Number(-622).__class__ is Integer + assert Number(5, 3).__class__ is Rational + assert Number(5.3).__class__ is Float + assert Number('1') is S.One + assert Number('2').__class__ is Integer + assert Number('-622').__class__ is Integer + assert Number('5/3').__class__ is Rational + assert Number('5.3').__class__ is Float + raises(ValueError, lambda: Number('cos')) + raises(TypeError, lambda: Number(cos)) + a = Rational(3, 5) + assert Number(a) is a # Check idempotence on Numbers + u = ['inf', '-inf', 'nan', 'iNF', '+inf'] + v = [oo, -oo, nan, oo, oo] + for i, a in zip(u, v): + assert Number(i) is a, (i, Number(i), a) + + +def test_Number_cmp(): + n1 = Number(1) + n2 = Number(2) + n3 = Number(-3) + + assert n1 < n2 + assert n1 <= n2 + assert n3 < n1 + assert n2 > n3 + assert n2 >= n3 + + raises(TypeError, lambda: n1 < S.NaN) + raises(TypeError, lambda: n1 <= S.NaN) + raises(TypeError, lambda: n1 > S.NaN) + raises(TypeError, lambda: n1 >= S.NaN) + + +def test_Rational_cmp(): + n1 = Rational(1, 4) + n2 = Rational(1, 3) + n3 = Rational(2, 4) + n4 = Rational(2, -4) + n5 = Rational(0) + n6 = Rational(1) + n7 = Rational(3) + n8 = Rational(-3) + + assert n8 < n5 + assert n5 < n6 + assert n6 < n7 + assert n8 < n7 + assert n7 > n8 + assert (n1 + 1)**n2 < 2 + assert ((n1 + n6)/n7) < 1 + + assert n4 < n3 + assert n2 < n3 + assert n1 < n2 + assert n3 > n1 + assert not n3 < n1 + assert not (Rational(-1) > 0) + assert Rational(-1) < 0 + + raises(TypeError, lambda: n1 < S.NaN) + raises(TypeError, lambda: n1 <= S.NaN) + raises(TypeError, lambda: n1 > S.NaN) + raises(TypeError, lambda: n1 >= S.NaN) + + +def test_Float(): + def eq(a, b): + t = Float("1.0E-15") + return (-t < a - b < t) + + equal_pairs = [ + (0, 0.0), # This is just how Python works... + (0, S.Zero), + (0.0, Float(0)), + ] + unequal_pairs = [ + (0.0, S.Zero), + (0, Float(0)), + (S.Zero, Float(0)), + ] + for p1, p2 in equal_pairs: + assert (p1 == p2) is True + assert (p1 != p2) is False + assert (p2 == p1) is True + assert (p2 != p1) is False + for p1, p2 in unequal_pairs: + assert (p1 == p2) is False + assert (p1 != p2) is True + assert (p2 == p1) is False + assert (p2 != p1) is True + + assert S.Zero.is_zero + + a = Float(2) ** Float(3) + assert eq(a.evalf(), Float(8)) + assert eq((pi ** -1).evalf(), Float("0.31830988618379067")) + a = Float(2) ** Float(4) + assert eq(a.evalf(), Float(16)) + assert (S(.3) == S(.5)) is False + + mpf = (0, 5404319552844595, -52, 53) + x_str = Float((0, '13333333333333', -52, 53)) + x_0xstr = Float((0, '0x13333333333333', -52, 53)) + x2_str = Float((0, '26666666666666', -53, 54)) + x_hex = Float((0, int(0x13333333333333), -52, 53)) + x_dec = Float(mpf) + assert x_str == x_0xstr == x_hex == x_dec == Float(1.2) + # x2_str was entered slightly malformed in that the mantissa + # was even -- it should be odd and the even part should be + # included with the exponent, but this is resolved by normalization + # ONLY IF REQUIREMENTS of mpf_norm are met: the bitcount must + # be exact: double the mantissa ==> increase bc by 1 + assert Float(1.2)._mpf_ == mpf + assert x2_str._mpf_ == mpf + + assert Float((0, int(0), -123, -1)) is S.NaN + assert Float((0, int(0), -456, -2)) is S.Infinity + assert Float((1, int(0), -789, -3)) is S.NegativeInfinity + # if you don't give the full signature, it's not special + assert Float((0, int(0), -123)) == Float(0) + assert Float((0, int(0), -456)) == Float(0) + assert Float((1, int(0), -789)) == Float(0) + + raises(ValueError, lambda: Float((0, 7, 1, 3), '')) + + assert Float('0.0').is_finite is True + assert Float('0.0').is_negative is False + assert Float('0.0').is_positive is False + assert Float('0.0').is_infinite is False + assert Float('0.0').is_zero is True + + # rationality properties + # if the integer test fails then the use of intlike + # should be removed from gamma_functions.py + assert Float(1).is_integer is None + assert Float(1).is_rational is None + assert Float(1).is_irrational is None + assert sqrt(2).n(15).is_rational is None + assert sqrt(2).n(15).is_irrational is None + + # do not automatically evalf + def teq(a): + assert (a.evalf() == a) is False + assert (a.evalf() != a) is True + assert (a == a.evalf()) is False + assert (a != a.evalf()) is True + + teq(pi) + teq(2*pi) + teq(cos(0.1, evaluate=False)) + + # long integer + i = 12345678901234567890 + assert same_and_same_prec(Float(12, ''), Float('12', '')) + assert same_and_same_prec(Float(Integer(i), ''), Float(i, '')) + assert same_and_same_prec(Float(i, ''), Float(str(i), 20)) + assert same_and_same_prec(Float(str(i)), Float(i, '')) + assert same_and_same_prec(Float(i), Float(i, '')) + + # inexact floats (repeating binary = denom not multiple of 2) + # cannot have precision greater than 15 + assert Float(.125, 22)._prec == 76 + assert Float(2.0, 22)._prec == 76 + # only default prec is equal, even for exactly representable float + assert Float(.125, 22) != .125 + #assert Float(2.0, 22) == 2 + assert float(Float('.12500000000000001', '')) == .125 + raises(ValueError, lambda: Float(.12500000000000001, '')) + + # allow spaces + assert Float('123 456.123 456') == Float('123456.123456') + assert Integer('123 456') == Integer('123456') + assert Rational('123 456.123 456') == Rational('123456.123456') + assert Float(' .3e2') == Float('0.3e2') + # but treat them as strictly ass underscore between digits: only 1 + raises(ValueError, lambda: Float('1 2')) + + # allow underscore between digits + assert Float('1_23.4_56') == Float('123.456') + # assert Float('1_23.4_5_6', 12) == Float('123.456', 12) + # ...but not in all cases (per Py 3.6) + raises(ValueError, lambda: Float('1_')) + raises(ValueError, lambda: Float('1__2')) + raises(ValueError, lambda: Float('_1')) + raises(ValueError, lambda: Float('_inf')) + + # allow auto precision detection + assert Float('.1', '') == Float(.1, 1) + assert Float('.125', '') == Float(.125, 3) + assert Float('.100', '') == Float(.1, 3) + assert Float('2.0', '') == Float('2', 2) + + raises(ValueError, lambda: Float("12.3d-4", "")) + raises(ValueError, lambda: Float(12.3, "")) + raises(ValueError, lambda: Float('.')) + raises(ValueError, lambda: Float('-.')) + + zero = Float('0.0') + assert Float('-0') == zero + assert Float('.0') == zero + assert Float('-.0') == zero + assert Float('-0.0') == zero + assert Float(0.0) == zero + assert Float(0) == zero + assert Float(0, '') == Float('0', '') + assert Float(1) == Float(1.0) + assert Float(S.Zero) == zero + assert Float(S.One) == Float(1.0) + + assert Float(decimal.Decimal('0.1'), 3) == Float('.1', 3) + assert Float(decimal.Decimal('nan')) is S.NaN + assert Float(decimal.Decimal('Infinity')) is S.Infinity + assert Float(decimal.Decimal('-Infinity')) is S.NegativeInfinity + + assert '{:.3f}'.format(Float(4.236622)) == '4.237' + assert '{:.35f}'.format(Float(pi.n(40), 40)) == \ + '3.14159265358979323846264338327950288' + + # unicode + assert Float('0.73908513321516064100000000') == \ + Float('0.73908513321516064100000000') + assert Float('0.73908513321516064100000000', 28) == \ + Float('0.73908513321516064100000000', 28) + + # binary precision + # Decimal value 0.1 cannot be expressed precisely as a base 2 fraction + a = Float(S.One/10, dps=15) + b = Float(S.One/10, dps=16) + p = Float(S.One/10, precision=53) + q = Float(S.One/10, precision=54) + assert a._mpf_ == p._mpf_ + assert not a._mpf_ == q._mpf_ + assert not b._mpf_ == q._mpf_ + + # Precision specifying errors + raises(ValueError, lambda: Float("1.23", dps=3, precision=10)) + raises(ValueError, lambda: Float("1.23", dps="", precision=10)) + raises(ValueError, lambda: Float("1.23", dps=3, precision="")) + raises(ValueError, lambda: Float("1.23", dps="", precision="")) + + # from NumberSymbol + assert same_and_same_prec(Float(pi, 32), pi.evalf(32)) + assert same_and_same_prec(Float(Catalan), Catalan.evalf()) + + # oo and nan + u = ['inf', '-inf', 'nan', 'iNF', '+inf'] + v = [oo, -oo, nan, oo, oo] + for i, a in zip(u, v): + assert Float(i) is a + + +def test_zero_not_false(): + # https://github.com/sympy/sympy/issues/20796 + assert (S(0.0) == S.false) is False + assert (S.false == S(0.0)) is False + assert (S(0) == S.false) is False + assert (S.false == S(0)) is False + + +@conserve_mpmath_dps +def test_float_mpf(): + import mpmath + mpmath.mp.dps = 100 + mp_pi = mpmath.pi() + + assert Float(mp_pi, 100) == Float(mp_pi._mpf_, 100) == pi.evalf(100) + + mpmath.mp.dps = 15 + + assert Float(mp_pi, 100) == Float(mp_pi._mpf_, 100) == pi.evalf(100) + + +def test_Float_RealElement(): + repi = RealField(dps=100)(pi.evalf(100)) + # We still have to pass the precision because Float doesn't know what + # RealElement is, but make sure it keeps full precision from the result. + assert Float(repi, 100) == pi.evalf(100) + + +def test_Float_default_to_highprec_from_str(): + s = str(pi.evalf(128)) + assert same_and_same_prec(Float(s), Float(s, '')) + + +def test_Float_eval(): + a = Float(3.2) + assert (a**2).is_Float + + +def test_Float_issue_2107(): + a = Float(0.1, 10) + b = Float("0.1", 10) + + assert a - a == 0 + assert a + (-a) == 0 + assert S.Zero + a - a == 0 + assert S.Zero + a + (-a) == 0 + + assert b - b == 0 + assert b + (-b) == 0 + assert S.Zero + b - b == 0 + assert S.Zero + b + (-b) == 0 + + +def test_issue_14289(): + from sympy.polys.numberfields import to_number_field + + a = 1 - sqrt(2) + b = to_number_field(a) + assert b.as_expr() == a + assert b.minpoly(a).expand() == 0 + + +def test_Float_from_tuple(): + a = Float((0, '1L', 0, 1)) + b = Float((0, '1', 0, 1)) + assert a == b + + +def test_Infinity(): + assert oo != 1 + assert 1*oo is oo + assert 1 != oo + assert oo != -oo + assert oo != Symbol("x")**3 + assert oo + 1 is oo + assert 2 + oo is oo + assert 3*oo + 2 is oo + assert S.Half**oo == 0 + assert S.Half**(-oo) is oo + assert -oo*3 is -oo + assert oo + oo is oo + assert -oo + oo*(-5) is -oo + assert 1/oo == 0 + assert 1/(-oo) == 0 + assert 8/oo == 0 + assert oo % 2 is nan + assert 2 % oo is nan + assert oo/oo is nan + assert oo/-oo is nan + assert -oo/oo is nan + assert -oo/-oo is nan + assert oo - oo is nan + assert oo - -oo is oo + assert -oo - oo is -oo + assert -oo - -oo is nan + assert oo + -oo is nan + assert -oo + oo is nan + assert oo + oo is oo + assert -oo + oo is nan + assert oo + -oo is nan + assert -oo + -oo is -oo + assert oo*oo is oo + assert -oo*oo is -oo + assert oo*-oo is -oo + assert -oo*-oo is oo + assert oo/0 is oo + assert -oo/0 is -oo + assert 0/oo == 0 + assert 0/-oo == 0 + assert oo*0 is nan + assert -oo*0 is nan + assert 0*oo is nan + assert 0*-oo is nan + assert oo + 0 is oo + assert -oo + 0 is -oo + assert 0 + oo is oo + assert 0 + -oo is -oo + assert oo - 0 is oo + assert -oo - 0 is -oo + assert 0 - oo is -oo + assert 0 - -oo is oo + assert oo/2 is oo + assert -oo/2 is -oo + assert oo/-2 is -oo + assert -oo/-2 is oo + assert oo*2 is oo + assert -oo*2 is -oo + assert oo*-2 is -oo + assert 2/oo == 0 + assert 2/-oo == 0 + assert -2/oo == 0 + assert -2/-oo == 0 + assert 2*oo is oo + assert 2*-oo is -oo + assert -2*oo is -oo + assert -2*-oo is oo + assert 2 + oo is oo + assert 2 - oo is -oo + assert -2 + oo is oo + assert -2 - oo is -oo + assert 2 + -oo is -oo + assert 2 - -oo is oo + assert -2 + -oo is -oo + assert -2 - -oo is oo + assert S(2) + oo is oo + assert S(2) - oo is -oo + assert oo/I == -oo*I + assert -oo/I == oo*I + assert oo*float(1) == _inf and (oo*float(1)) is oo + assert -oo*float(1) == _ninf and (-oo*float(1)) is -oo + assert oo/float(1) == _inf and (oo/float(1)) is oo + assert -oo/float(1) == _ninf and (-oo/float(1)) is -oo + assert oo*float(-1) == _ninf and (oo*float(-1)) is -oo + assert -oo*float(-1) == _inf and (-oo*float(-1)) is oo + assert oo/float(-1) == _ninf and (oo/float(-1)) is -oo + assert -oo/float(-1) == _inf and (-oo/float(-1)) is oo + assert oo + float(1) == _inf and (oo + float(1)) is oo + assert -oo + float(1) == _ninf and (-oo + float(1)) is -oo + assert oo - float(1) == _inf and (oo - float(1)) is oo + assert -oo - float(1) == _ninf and (-oo - float(1)) is -oo + assert float(1)*oo == _inf and (float(1)*oo) is oo + assert float(1)*-oo == _ninf and (float(1)*-oo) is -oo + assert float(1)/oo == 0 + assert float(1)/-oo == 0 + assert float(-1)*oo == _ninf and (float(-1)*oo) is -oo + assert float(-1)*-oo == _inf and (float(-1)*-oo) is oo + assert float(-1)/oo == 0 + assert float(-1)/-oo == 0 + assert float(1) + oo is oo + assert float(1) + -oo is -oo + assert float(1) - oo is -oo + assert float(1) - -oo is oo + assert oo == float(oo) + assert (oo != float(oo)) is False + assert type(float(oo)) is float + assert -oo == float(-oo) + assert (-oo != float(-oo)) is False + assert type(float(-oo)) is float + + assert Float('nan') is nan + assert nan*1.0 is nan + assert -1.0*nan is nan + assert nan*oo is nan + assert nan*-oo is nan + assert nan/oo is nan + assert nan/-oo is nan + assert nan + oo is nan + assert nan + -oo is nan + assert nan - oo is nan + assert nan - -oo is nan + assert -oo * S.Zero is nan + + assert oo*nan is nan + assert -oo*nan is nan + assert oo/nan is nan + assert -oo/nan is nan + assert oo + nan is nan + assert -oo + nan is nan + assert oo - nan is nan + assert -oo - nan is nan + assert S.Zero * oo is nan + assert oo.is_Rational is False + assert isinstance(oo, Rational) is False + + assert S.One/oo == 0 + assert -S.One/oo == 0 + assert S.One/-oo == 0 + assert -S.One/-oo == 0 + assert S.One*oo is oo + assert -S.One*oo is -oo + assert S.One*-oo is -oo + assert -S.One*-oo is oo + assert S.One/nan is nan + assert S.One - -oo is oo + assert S.One + nan is nan + assert S.One - nan is nan + assert nan - S.One is nan + assert nan/S.One is nan + assert -oo - S.One is -oo + + +def test_Infinity_2(): + x = Symbol('x') + assert oo*x != oo + assert oo*(pi - 1) is oo + assert oo*(1 - pi) is -oo + + assert (-oo)*x != -oo + assert (-oo)*(pi - 1) is -oo + assert (-oo)*(1 - pi) is oo + + assert (-1)**S.NaN is S.NaN + assert oo - _inf is S.NaN + assert oo + _ninf is S.NaN + assert oo*0 is S.NaN + assert oo/_inf is S.NaN + assert oo/_ninf is S.NaN + assert oo**S.NaN is S.NaN + assert -oo + _inf is S.NaN + assert -oo - _ninf is S.NaN + assert -oo*S.NaN is S.NaN + assert -oo*0 is S.NaN + assert -oo/_inf is S.NaN + assert -oo/_ninf is S.NaN + assert -oo/S.NaN is S.NaN + assert abs(-oo) is oo + assert all((-oo)**i is S.NaN for i in (oo, -oo, S.NaN)) + assert (-oo)**3 is -oo + assert (-oo)**2 is oo + assert abs(S.ComplexInfinity) is oo + + +def test_Mul_Infinity_Zero(): + assert Float(0)*_inf is nan + assert Float(0)*_ninf is nan + assert Float(0)*_inf is nan + assert Float(0)*_ninf is nan + assert _inf*Float(0) is nan + assert _ninf*Float(0) is nan + assert _inf*Float(0) is nan + assert _ninf*Float(0) is nan + + +def test_Div_By_Zero(): + assert 1/S.Zero is zoo + assert 1/Float(0) is zoo + assert 0/S.Zero is nan + assert 0/Float(0) is nan + assert S.Zero/0 is nan + assert Float(0)/0 is nan + assert -1/S.Zero is zoo + assert -1/Float(0) is zoo + + +@_both_exp_pow +def test_Infinity_inequations(): + assert oo > pi + assert not (oo < pi) + assert exp(-3) < oo + + assert _inf > pi + assert not (_inf < pi) + assert exp(-3) < _inf + + raises(TypeError, lambda: oo < I) + raises(TypeError, lambda: oo <= I) + raises(TypeError, lambda: oo > I) + raises(TypeError, lambda: oo >= I) + raises(TypeError, lambda: -oo < I) + raises(TypeError, lambda: -oo <= I) + raises(TypeError, lambda: -oo > I) + raises(TypeError, lambda: -oo >= I) + + raises(TypeError, lambda: I < oo) + raises(TypeError, lambda: I <= oo) + raises(TypeError, lambda: I > oo) + raises(TypeError, lambda: I >= oo) + raises(TypeError, lambda: I < -oo) + raises(TypeError, lambda: I <= -oo) + raises(TypeError, lambda: I > -oo) + raises(TypeError, lambda: I >= -oo) + + assert oo > -oo and oo >= -oo + assert (oo < -oo) == False and (oo <= -oo) == False + assert -oo < oo and -oo <= oo + assert (-oo > oo) == False and (-oo >= oo) == False + + assert (oo < oo) == False # issue 7775 + assert (oo > oo) == False + assert (-oo > -oo) == False and (-oo < -oo) == False + assert oo >= oo and oo <= oo and -oo >= -oo and -oo <= -oo + assert (-oo < -_inf) == False + assert (oo > _inf) == False + assert -oo >= -_inf + assert oo <= _inf + + x = Symbol('x') + b = Symbol('b', finite=True, real=True) + assert (x < oo) == Lt(x, oo) # issue 7775 + assert b < oo and b > -oo and b <= oo and b >= -oo + assert oo > b and oo >= b and (oo < b) == False and (oo <= b) == False + assert (-oo > b) == False and (-oo >= b) == False and -oo < b and -oo <= b + assert (oo < x) == Lt(oo, x) and (oo > x) == Gt(oo, x) + assert (oo <= x) == Le(oo, x) and (oo >= x) == Ge(oo, x) + assert (-oo < x) == Lt(-oo, x) and (-oo > x) == Gt(-oo, x) + assert (-oo <= x) == Le(-oo, x) and (-oo >= x) == Ge(-oo, x) + + +def test_NaN(): + assert nan is nan + assert nan != 1 + assert 1*nan is nan + assert 1 != nan + assert -nan is nan + assert oo != Symbol("x")**3 + assert 2 + nan is nan + assert 3*nan + 2 is nan + assert -nan*3 is nan + assert nan + nan is nan + assert -nan + nan*(-5) is nan + assert 8/nan is nan + raises(TypeError, lambda: nan > 0) + raises(TypeError, lambda: nan < 0) + raises(TypeError, lambda: nan >= 0) + raises(TypeError, lambda: nan <= 0) + raises(TypeError, lambda: 0 < nan) + raises(TypeError, lambda: 0 > nan) + raises(TypeError, lambda: 0 <= nan) + raises(TypeError, lambda: 0 >= nan) + assert nan**0 == 1 # as per IEEE 754 + assert 1**nan is nan # IEEE 754 is not the best choice for symbolic work + # test Pow._eval_power's handling of NaN + assert Pow(nan, 0, evaluate=False)**2 == 1 + for n in (1, 1., S.One, S.NegativeOne, Float(1)): + assert n + nan is nan + assert n - nan is nan + assert nan + n is nan + assert nan - n is nan + assert n/nan is nan + assert nan/n is nan + + +def test_special_numbers(): + assert isinstance(S.NaN, Number) is True + assert isinstance(S.Infinity, Number) is True + assert isinstance(S.NegativeInfinity, Number) is True + + assert S.NaN.is_number is True + assert S.Infinity.is_number is True + assert S.NegativeInfinity.is_number is True + assert S.ComplexInfinity.is_number is True + + assert isinstance(S.NaN, Rational) is False + assert isinstance(S.Infinity, Rational) is False + assert isinstance(S.NegativeInfinity, Rational) is False + + assert S.NaN.is_rational is not True + assert S.Infinity.is_rational is not True + assert S.NegativeInfinity.is_rational is not True + + +def test_powers(): + assert integer_nthroot(1, 2) == (1, True) + assert integer_nthroot(1, 5) == (1, True) + assert integer_nthroot(2, 1) == (2, True) + assert integer_nthroot(2, 2) == (1, False) + assert integer_nthroot(2, 5) == (1, False) + assert integer_nthroot(4, 2) == (2, True) + assert integer_nthroot(123**25, 25) == (123, True) + assert integer_nthroot(123**25 + 1, 25) == (123, False) + assert integer_nthroot(123**25 - 1, 25) == (122, False) + assert integer_nthroot(1, 1) == (1, True) + assert integer_nthroot(0, 1) == (0, True) + assert integer_nthroot(0, 3) == (0, True) + assert integer_nthroot(10000, 1) == (10000, True) + assert integer_nthroot(4, 2) == (2, True) + assert integer_nthroot(16, 2) == (4, True) + assert integer_nthroot(26, 2) == (5, False) + assert integer_nthroot(1234567**7, 7) == (1234567, True) + assert integer_nthroot(1234567**7 + 1, 7) == (1234567, False) + assert integer_nthroot(1234567**7 - 1, 7) == (1234566, False) + b = 25**1000 + assert integer_nthroot(b, 1000) == (25, True) + assert integer_nthroot(b + 1, 1000) == (25, False) + assert integer_nthroot(b - 1, 1000) == (24, False) + c = 10**400 + c2 = c**2 + assert integer_nthroot(c2, 2) == (c, True) + assert integer_nthroot(c2 + 1, 2) == (c, False) + assert integer_nthroot(c2 - 1, 2) == (c - 1, False) + assert integer_nthroot(2, 10**10) == (1, False) + + p, r = integer_nthroot(int(factorial(10000)), 100) + assert p % (10**10) == 5322420655 + assert not r + + # Test that this is fast + assert integer_nthroot(2, 10**10) == (1, False) + + # output should be int if possible + assert type(integer_nthroot(2**61, 2)[0]) is int + + +def test_integer_nthroot_overflow(): + assert integer_nthroot(10**(50*50), 50) == (10**50, True) + assert integer_nthroot(10**100000, 10000) == (10**10, True) + + +def test_integer_log(): + raises(ValueError, lambda: integer_log(2, 1)) + raises(ValueError, lambda: integer_log(0, 2)) + raises(ValueError, lambda: integer_log(1.1, 2)) + raises(ValueError, lambda: integer_log(1, 2.2)) + + assert integer_log(1, 2) == (0, True) + assert integer_log(1, 3) == (0, True) + assert integer_log(2, 3) == (0, False) + assert integer_log(3, 3) == (1, True) + assert integer_log(3*2, 3) == (1, False) + assert integer_log(3**2, 3) == (2, True) + assert integer_log(3*4, 3) == (2, False) + assert integer_log(3**3, 3) == (3, True) + assert integer_log(27, 5) == (2, False) + assert integer_log(2, 3) == (0, False) + assert integer_log(-4, 2) == (2, False) + assert integer_log(-16, 4) == (0, False) + assert integer_log(-4, -2) == (2, False) + assert integer_log(4, -2) == (2, True) + assert integer_log(-8, -2) == (3, True) + assert integer_log(8, -2) == (3, False) + assert integer_log(-9, 3) == (0, False) + assert integer_log(-9, -3) == (2, False) + assert integer_log(9, -3) == (2, True) + assert integer_log(-27, -3) == (3, True) + assert integer_log(27, -3) == (3, False) + + +def test_isqrt(): + from math import sqrt as _sqrt + limit = 4503599761588223 + assert int(_sqrt(limit)) == integer_nthroot(limit, 2)[0] + assert int(_sqrt(limit + 1)) != integer_nthroot(limit + 1, 2)[0] + assert isqrt(limit + 1) == integer_nthroot(limit + 1, 2)[0] + assert isqrt(limit + S.Half) == integer_nthroot(limit, 2)[0] + assert isqrt(limit + 1 + S.Half) == integer_nthroot(limit + 1, 2)[0] + assert isqrt(limit + 2 + S.Half) == integer_nthroot(limit + 2, 2)[0] + + # Regression tests for https://github.com/sympy/sympy/issues/17034 + assert isqrt(4503599761588224) == 67108864 + assert isqrt(9999999999999999) == 99999999 + + # Other corner cases, especially involving non-integers. + raises(ValueError, lambda: isqrt(-1)) + raises(ValueError, lambda: isqrt(-10**1000)) + raises(ValueError, lambda: isqrt(Rational(-1, 2))) + + tiny = Rational(1, 10**1000) + raises(ValueError, lambda: isqrt(-tiny)) + assert isqrt(1-tiny) == 0 + assert isqrt(4503599761588224-tiny) == 67108864 + assert isqrt(10**100 - tiny) == 10**50 - 1 + + +def test_powers_Integer(): + """Test Integer._eval_power""" + # check infinity + assert S.One ** S.Infinity is S.NaN + assert S.NegativeOne** S.Infinity is S.NaN + assert S(2) ** S.Infinity is S.Infinity + assert S(-2)** S.Infinity == zoo + assert S(0) ** S.Infinity is S.Zero + + # check Nan + assert S.One ** S.NaN is S.NaN + assert S.NegativeOne ** S.NaN is S.NaN + + # check for exact roots + assert S.NegativeOne ** Rational(6, 5) == - (-1)**(S.One/5) + assert sqrt(S(4)) == 2 + assert sqrt(S(-4)) == I * 2 + assert S(16) ** Rational(1, 4) == 2 + assert S(-16) ** Rational(1, 4) == 2 * (-1)**Rational(1, 4) + assert S(9) ** Rational(3, 2) == 27 + assert S(-9) ** Rational(3, 2) == -27*I + assert S(27) ** Rational(2, 3) == 9 + assert S(-27) ** Rational(2, 3) == 9 * (S.NegativeOne ** Rational(2, 3)) + assert (-2) ** Rational(-2, 1) == Rational(1, 4) + + # not exact roots + assert sqrt(-3) == I*sqrt(3) + assert (3) ** (Rational(3, 2)) == 3 * sqrt(3) + assert (-3) ** (Rational(3, 2)) == - 3 * sqrt(-3) + assert (-3) ** (Rational(5, 2)) == 9 * I * sqrt(3) + assert (-3) ** (Rational(7, 2)) == - I * 27 * sqrt(3) + assert (2) ** (Rational(3, 2)) == 2 * sqrt(2) + assert (2) ** (Rational(-3, 2)) == sqrt(2) / 4 + assert (81) ** (Rational(2, 3)) == 9 * (S(3) ** (Rational(2, 3))) + assert (-81) ** (Rational(2, 3)) == 9 * (S(-3) ** (Rational(2, 3))) + assert (-3) ** Rational(-7, 3) == \ + -(-1)**Rational(2, 3)*3**Rational(2, 3)/27 + assert (-3) ** Rational(-2, 3) == \ + -(-1)**Rational(1, 3)*3**Rational(1, 3)/3 + + # join roots + assert sqrt(6) + sqrt(24) == 3*sqrt(6) + assert sqrt(2) * sqrt(3) == sqrt(6) + + # separate symbols & constansts + x = Symbol("x") + assert sqrt(49 * x) == 7 * sqrt(x) + assert sqrt((3 - sqrt(pi)) ** 2) == 3 - sqrt(pi) + + # check that it is fast for big numbers + assert (2**64 + 1) ** Rational(4, 3) + assert (2**64 + 1) ** Rational(17, 25) + + # negative rational power and negative base + assert (-3) ** Rational(-7, 3) == \ + -(-1)**Rational(2, 3)*3**Rational(2, 3)/27 + assert (-3) ** Rational(-2, 3) == \ + -(-1)**Rational(1, 3)*3**Rational(1, 3)/3 + assert (-2) ** Rational(-10, 3) == \ + (-1)**Rational(2, 3)*2**Rational(2, 3)/16 + assert abs(Pow(-2, Rational(-10, 3)).n() - + Pow(-2, Rational(-10, 3), evaluate=False).n()) < 1e-16 + + # negative base and rational power with some simplification + assert (-8) ** Rational(2, 5) == \ + 2*(-1)**Rational(2, 5)*2**Rational(1, 5) + assert (-4) ** Rational(9, 5) == \ + -8*(-1)**Rational(4, 5)*2**Rational(3, 5) + + assert S(1234).factors() == {617: 1, 2: 1} + assert Rational(2*3, 3*5*7).factors() == {2: 1, 5: -1, 7: -1} + + # test that eval_power factors numbers bigger than + # the current limit in factor_trial_division (2**15) + from sympy.ntheory.generate import nextprime + n = nextprime(2**15) + assert sqrt(n**2) == n + assert sqrt(n**3) == n*sqrt(n) + assert sqrt(4*n) == 2*sqrt(n) + + # check that factors of base with powers sharing gcd with power are removed + assert (2**4*3)**Rational(1, 6) == 2**Rational(2, 3)*3**Rational(1, 6) + assert (2**4*3)**Rational(5, 6) == 8*2**Rational(1, 3)*3**Rational(5, 6) + + # check that bases sharing a gcd are exptracted + assert 2**Rational(1, 3)*3**Rational(1, 4)*6**Rational(1, 5) == \ + 2**Rational(8, 15)*3**Rational(9, 20) + assert sqrt(8)*24**Rational(1, 3)*6**Rational(1, 5) == \ + 4*2**Rational(7, 10)*3**Rational(8, 15) + assert sqrt(8)*(-24)**Rational(1, 3)*(-6)**Rational(1, 5) == \ + 4*(-3)**Rational(8, 15)*2**Rational(7, 10) + assert 2**Rational(1, 3)*2**Rational(8, 9) == 2*2**Rational(2, 9) + assert 2**Rational(2, 3)*6**Rational(1, 3) == 2*3**Rational(1, 3) + assert 2**Rational(2, 3)*6**Rational(8, 9) == \ + 2*2**Rational(5, 9)*3**Rational(8, 9) + assert (-2)**Rational(2, S(3))*(-4)**Rational(1, S(3)) == -2*2**Rational(1, 3) + assert 3*Pow(3, 2, evaluate=False) == 3**3 + assert 3*Pow(3, Rational(-1, 3), evaluate=False) == 3**Rational(2, 3) + assert (-2)**Rational(1, 3)*(-3)**Rational(1, 4)*(-5)**Rational(5, 6) == \ + -(-1)**Rational(5, 12)*2**Rational(1, 3)*3**Rational(1, 4) * \ + 5**Rational(5, 6) + + assert Integer(-2)**Symbol('', even=True) == \ + Integer(2)**Symbol('', even=True) + assert (-1)**Float(.5) == 1.0*I + + +def test_powers_Rational(): + """Test Rational._eval_power""" + # check infinity + assert S.Half ** S.Infinity == 0 + assert Rational(3, 2) ** S.Infinity is S.Infinity + assert Rational(-1, 2) ** S.Infinity == 0 + assert Rational(-3, 2) ** S.Infinity == zoo + + # check Nan + assert Rational(3, 4) ** S.NaN is S.NaN + assert Rational(-2, 3) ** S.NaN is S.NaN + + # exact roots on numerator + assert sqrt(Rational(4, 3)) == 2 * sqrt(3) / 3 + assert Rational(4, 3) ** Rational(3, 2) == 8 * sqrt(3) / 9 + assert sqrt(Rational(-4, 3)) == I * 2 * sqrt(3) / 3 + assert Rational(-4, 3) ** Rational(3, 2) == - I * 8 * sqrt(3) / 9 + assert Rational(27, 2) ** Rational(1, 3) == 3 * (2 ** Rational(2, 3)) / 2 + assert Rational(5**3, 8**3) ** Rational(4, 3) == Rational(5**4, 8**4) + + # exact root on denominator + assert sqrt(Rational(1, 4)) == S.Half + assert sqrt(Rational(1, -4)) == I * S.Half + assert sqrt(Rational(3, 4)) == sqrt(3) / 2 + assert sqrt(Rational(3, -4)) == I * sqrt(3) / 2 + assert Rational(5, 27) ** Rational(1, 3) == (5 ** Rational(1, 3)) / 3 + + # not exact roots + assert sqrt(S.Half) == sqrt(2) / 2 + assert sqrt(Rational(-4, 7)) == I * sqrt(Rational(4, 7)) + assert Rational(-3, 2)**Rational(-7, 3) == \ + -4*(-1)**Rational(2, 3)*2**Rational(1, 3)*3**Rational(2, 3)/27 + assert Rational(-3, 2)**Rational(-2, 3) == \ + -(-1)**Rational(1, 3)*2**Rational(2, 3)*3**Rational(1, 3)/3 + assert Rational(-3, 2)**Rational(-10, 3) == \ + 8*(-1)**Rational(2, 3)*2**Rational(1, 3)*3**Rational(2, 3)/81 + assert abs(Pow(Rational(-2, 3), Rational(-7, 4)).n() - + Pow(Rational(-2, 3), Rational(-7, 4), evaluate=False).n()) < 1e-16 + + # negative integer power and negative rational base + assert Rational(-2, 3) ** Rational(-2, 1) == Rational(9, 4) + + a = Rational(1, 10) + assert a**Float(a, 2) == Float(a, 2)**Float(a, 2) + assert Rational(-2, 3)**Symbol('', even=True) == \ + Rational(2, 3)**Symbol('', even=True) + + +def test_powers_Float(): + assert str((S('-1/10')**S('3/10')).n()) == str(Float(-.1)**(.3)) + + +def test_lshift_Integer(): + assert Integer(0) << Integer(2) == Integer(0) + assert Integer(0) << 2 == Integer(0) + assert 0 << Integer(2) == Integer(0) + + assert Integer(0b11) << Integer(0) == Integer(0b11) + assert Integer(0b11) << 0 == Integer(0b11) + assert 0b11 << Integer(0) == Integer(0b11) + + assert Integer(0b11) << Integer(2) == Integer(0b11 << 2) + assert Integer(0b11) << 2 == Integer(0b11 << 2) + assert 0b11 << Integer(2) == Integer(0b11 << 2) + + assert Integer(-0b11) << Integer(2) == Integer(-0b11 << 2) + assert Integer(-0b11) << 2 == Integer(-0b11 << 2) + assert -0b11 << Integer(2) == Integer(-0b11 << 2) + + raises(TypeError, lambda: Integer(2) << 0.0) + raises(TypeError, lambda: 0.0 << Integer(2)) + raises(ValueError, lambda: Integer(1) << Integer(-1)) + + +def test_rshift_Integer(): + assert Integer(0) >> Integer(2) == Integer(0) + assert Integer(0) >> 2 == Integer(0) + assert 0 >> Integer(2) == Integer(0) + + assert Integer(0b11) >> Integer(0) == Integer(0b11) + assert Integer(0b11) >> 0 == Integer(0b11) + assert 0b11 >> Integer(0) == Integer(0b11) + + assert Integer(0b11) >> Integer(2) == Integer(0) + assert Integer(0b11) >> 2 == Integer(0) + assert 0b11 >> Integer(2) == Integer(0) + + assert Integer(-0b11) >> Integer(2) == Integer(-1) + assert Integer(-0b11) >> 2 == Integer(-1) + assert -0b11 >> Integer(2) == Integer(-1) + + assert Integer(0b1100) >> Integer(2) == Integer(0b1100 >> 2) + assert Integer(0b1100) >> 2 == Integer(0b1100 >> 2) + assert 0b1100 >> Integer(2) == Integer(0b1100 >> 2) + + assert Integer(-0b1100) >> Integer(2) == Integer(-0b1100 >> 2) + assert Integer(-0b1100) >> 2 == Integer(-0b1100 >> 2) + assert -0b1100 >> Integer(2) == Integer(-0b1100 >> 2) + + raises(TypeError, lambda: Integer(0b10) >> 0.0) + raises(TypeError, lambda: 0.0 >> Integer(2)) + raises(ValueError, lambda: Integer(1) >> Integer(-1)) + + +def test_and_Integer(): + assert Integer(0b01010101) & Integer(0b10101010) == Integer(0) + assert Integer(0b01010101) & 0b10101010 == Integer(0) + assert 0b01010101 & Integer(0b10101010) == Integer(0) + + assert Integer(0b01010101) & Integer(0b11011011) == Integer(0b01010001) + assert Integer(0b01010101) & 0b11011011 == Integer(0b01010001) + assert 0b01010101 & Integer(0b11011011) == Integer(0b01010001) + + assert -Integer(0b01010101) & Integer(0b11011011) == Integer(-0b01010101 & 0b11011011) + assert Integer(-0b01010101) & 0b11011011 == Integer(-0b01010101 & 0b11011011) + assert -0b01010101 & Integer(0b11011011) == Integer(-0b01010101 & 0b11011011) + + assert Integer(0b01010101) & -Integer(0b11011011) == Integer(0b01010101 & -0b11011011) + assert Integer(0b01010101) & -0b11011011 == Integer(0b01010101 & -0b11011011) + assert 0b01010101 & Integer(-0b11011011) == Integer(0b01010101 & -0b11011011) + + raises(TypeError, lambda: Integer(2) & 0.0) + raises(TypeError, lambda: 0.0 & Integer(2)) + + +def test_xor_Integer(): + assert Integer(0b01010101) ^ Integer(0b11111111) == Integer(0b10101010) + assert Integer(0b01010101) ^ 0b11111111 == Integer(0b10101010) + assert 0b01010101 ^ Integer(0b11111111) == Integer(0b10101010) + + assert Integer(0b01010101) ^ Integer(0b11011011) == Integer(0b10001110) + assert Integer(0b01010101) ^ 0b11011011 == Integer(0b10001110) + assert 0b01010101 ^ Integer(0b11011011) == Integer(0b10001110) + + assert -Integer(0b01010101) ^ Integer(0b11011011) == Integer(-0b01010101 ^ 0b11011011) + assert Integer(-0b01010101) ^ 0b11011011 == Integer(-0b01010101 ^ 0b11011011) + assert -0b01010101 ^ Integer(0b11011011) == Integer(-0b01010101 ^ 0b11011011) + + assert Integer(0b01010101) ^ -Integer(0b11011011) == Integer(0b01010101 ^ -0b11011011) + assert Integer(0b01010101) ^ -0b11011011 == Integer(0b01010101 ^ -0b11011011) + assert 0b01010101 ^ Integer(-0b11011011) == Integer(0b01010101 ^ -0b11011011) + + raises(TypeError, lambda: Integer(2) ^ 0.0) + raises(TypeError, lambda: 0.0 ^ Integer(2)) + + +def test_or_Integer(): + assert Integer(0b01010101) | Integer(0b10101010) == Integer(0b11111111) + assert Integer(0b01010101) | 0b10101010 == Integer(0b11111111) + assert 0b01010101 | Integer(0b10101010) == Integer(0b11111111) + + assert Integer(0b01010101) | Integer(0b11011011) == Integer(0b11011111) + assert Integer(0b01010101) | 0b11011011 == Integer(0b11011111) + assert 0b01010101 | Integer(0b11011011) == Integer(0b11011111) + + assert -Integer(0b01010101) | Integer(0b11011011) == Integer(-0b01010101 | 0b11011011) + assert Integer(-0b01010101) | 0b11011011 == Integer(-0b01010101 | 0b11011011) + assert -0b01010101 | Integer(0b11011011) == Integer(-0b01010101 | 0b11011011) + + assert Integer(0b01010101) | -Integer(0b11011011) == Integer(0b01010101 | -0b11011011) + assert Integer(0b01010101) | -0b11011011 == Integer(0b01010101 | -0b11011011) + assert 0b01010101 | Integer(-0b11011011) == Integer(0b01010101 | -0b11011011) + + raises(TypeError, lambda: Integer(2) | 0.0) + raises(TypeError, lambda: 0.0 | Integer(2)) + + +def test_invert_Integer(): + assert ~Integer(0b01010101) == Integer(-0b01010110) + assert ~Integer(0b01010101) == Integer(~0b01010101) + assert ~(~Integer(0b01010101)) == Integer(0b01010101) + + +def test_abs1(): + assert Rational(1, 6) != Rational(-1, 6) + assert abs(Rational(1, 6)) == abs(Rational(-1, 6)) + + +def test_accept_int(): + assert not Float(4) == 4 + assert Float(4) != 4 + assert Float(4) == 4.0 + + +def test_dont_accept_str(): + assert Float("0.2") != "0.2" + assert not (Float("0.2") == "0.2") + + +def test_int(): + a = Rational(5) + assert int(a) == 5 + a = Rational(9, 10) + assert int(a) == int(-a) == 0 + assert 1/(-1)**Rational(2, 3) == -(-1)**Rational(1, 3) + # issue 10368 + a = Rational(32442016954, 78058255275) + assert type(int(a)) is type(int(-a)) is int + + +def test_int_NumberSymbols(): + assert int(Catalan) == 0 + assert int(EulerGamma) == 0 + assert int(pi) == 3 + assert int(E) == 2 + assert int(GoldenRatio) == 1 + assert int(TribonacciConstant) == 1 + for i in [Catalan, E, EulerGamma, GoldenRatio, TribonacciConstant, pi]: + a, b = i.approximation_interval(Integer) + ia = int(i) + assert ia == a + assert isinstance(ia, int) + assert b == a + 1 + assert a.is_Integer and b.is_Integer + + +def test_real_bug(): + x = Symbol("x") + assert str(2.0*x*x) in ["(2.0*x)*x", "2.0*x**2", "2.00000000000000*x**2"] + assert str(2.1*x*x) != "(2.0*x)*x" + + +def test_bug_sqrt(): + assert ((sqrt(Rational(2)) + 1)*(sqrt(Rational(2)) - 1)).expand() == 1 + + +def test_pi_Pi(): + "Test that pi (instance) is imported, but Pi (class) is not" + from sympy import pi # noqa + with raises(ImportError): + from sympy import Pi # noqa + + +def test_no_len(): + # there should be no len for numbers + raises(TypeError, lambda: len(Rational(2))) + raises(TypeError, lambda: len(Rational(2, 3))) + raises(TypeError, lambda: len(Integer(2))) + + +def test_issue_3321(): + assert sqrt(Rational(1, 5)) == Rational(1, 5)**S.Half + assert 5 * sqrt(Rational(1, 5)) == sqrt(5) + + +def test_issue_3692(): + assert ((-1)**Rational(1, 6)).expand(complex=True) == I/2 + sqrt(3)/2 + assert ((-5)**Rational(1, 6)).expand(complex=True) == \ + 5**Rational(1, 6)*I/2 + 5**Rational(1, 6)*sqrt(3)/2 + assert ((-64)**Rational(1, 6)).expand(complex=True) == I + sqrt(3) + + +def test_issue_3423(): + x = Symbol("x") + assert sqrt(x - 1).as_base_exp() == (x - 1, S.Half) + assert sqrt(x - 1) != I*sqrt(1 - x) + + +def test_issue_3449(): + x = Symbol("x") + assert sqrt(x - 1).subs(x, 5) == 2 + + +def test_issue_13890(): + x = Symbol("x") + e = (-x/4 - S.One/12)**x - 1 + f = simplify(e) + a = Rational(9, 5) + assert abs(e.subs(x,a).evalf() - f.subs(x,a).evalf()) < 1e-15 + + +def test_Integer_factors(): + def F(i): + return Integer(i).factors() + + assert F(1) == {} + assert F(2) == {2: 1} + assert F(3) == {3: 1} + assert F(4) == {2: 2} + assert F(5) == {5: 1} + assert F(6) == {2: 1, 3: 1} + assert F(7) == {7: 1} + assert F(8) == {2: 3} + assert F(9) == {3: 2} + assert F(10) == {2: 1, 5: 1} + assert F(11) == {11: 1} + assert F(12) == {2: 2, 3: 1} + assert F(13) == {13: 1} + assert F(14) == {2: 1, 7: 1} + assert F(15) == {3: 1, 5: 1} + assert F(16) == {2: 4} + assert F(17) == {17: 1} + assert F(18) == {2: 1, 3: 2} + assert F(19) == {19: 1} + assert F(20) == {2: 2, 5: 1} + assert F(21) == {3: 1, 7: 1} + assert F(22) == {2: 1, 11: 1} + assert F(23) == {23: 1} + assert F(24) == {2: 3, 3: 1} + assert F(25) == {5: 2} + assert F(26) == {2: 1, 13: 1} + assert F(27) == {3: 3} + assert F(28) == {2: 2, 7: 1} + assert F(29) == {29: 1} + assert F(30) == {2: 1, 3: 1, 5: 1} + assert F(31) == {31: 1} + assert F(32) == {2: 5} + assert F(33) == {3: 1, 11: 1} + assert F(34) == {2: 1, 17: 1} + assert F(35) == {5: 1, 7: 1} + assert F(36) == {2: 2, 3: 2} + assert F(37) == {37: 1} + assert F(38) == {2: 1, 19: 1} + assert F(39) == {3: 1, 13: 1} + assert F(40) == {2: 3, 5: 1} + assert F(41) == {41: 1} + assert F(42) == {2: 1, 3: 1, 7: 1} + assert F(43) == {43: 1} + assert F(44) == {2: 2, 11: 1} + assert F(45) == {3: 2, 5: 1} + assert F(46) == {2: 1, 23: 1} + assert F(47) == {47: 1} + assert F(48) == {2: 4, 3: 1} + assert F(49) == {7: 2} + assert F(50) == {2: 1, 5: 2} + assert F(51) == {3: 1, 17: 1} + + +def test_Rational_factors(): + def F(p, q, visual=None): + return Rational(p, q).factors(visual=visual) + + assert F(2, 3) == {2: 1, 3: -1} + assert F(2, 9) == {2: 1, 3: -2} + assert F(2, 15) == {2: 1, 3: -1, 5: -1} + assert F(6, 10) == {3: 1, 5: -1} + + +def test_issue_4107(): + assert pi*(E + 10) + pi*(-E - 10) != 0 + assert pi*(E + 10**10) + pi*(-E - 10**10) != 0 + assert pi*(E + 10**20) + pi*(-E - 10**20) != 0 + assert pi*(E + 10**80) + pi*(-E - 10**80) != 0 + + assert (pi*(E + 10) + pi*(-E - 10)).expand() == 0 + assert (pi*(E + 10**10) + pi*(-E - 10**10)).expand() == 0 + assert (pi*(E + 10**20) + pi*(-E - 10**20)).expand() == 0 + assert (pi*(E + 10**80) + pi*(-E - 10**80)).expand() == 0 + + +def test_IntegerInteger(): + a = Integer(4) + b = Integer(a) + + assert a == b + + +def test_Rational_gcd_lcm_cofactors(): + assert Integer(4).gcd(2) == Integer(2) + assert Integer(4).lcm(2) == Integer(4) + assert Integer(4).gcd(Integer(2)) == Integer(2) + assert Integer(4).lcm(Integer(2)) == Integer(4) + a, b = 720**99911, 480**12342 + assert Integer(a).lcm(b) == a*b/Integer(a).gcd(b) + + assert Integer(4).gcd(3) == Integer(1) + assert Integer(4).lcm(3) == Integer(12) + assert Integer(4).gcd(Integer(3)) == Integer(1) + assert Integer(4).lcm(Integer(3)) == Integer(12) + + assert Rational(4, 3).gcd(2) == Rational(2, 3) + assert Rational(4, 3).lcm(2) == Integer(4) + assert Rational(4, 3).gcd(Integer(2)) == Rational(2, 3) + assert Rational(4, 3).lcm(Integer(2)) == Integer(4) + + assert Integer(4).gcd(Rational(2, 9)) == Rational(2, 9) + assert Integer(4).lcm(Rational(2, 9)) == Integer(4) + + assert Rational(4, 3).gcd(Rational(2, 9)) == Rational(2, 9) + assert Rational(4, 3).lcm(Rational(2, 9)) == Rational(4, 3) + assert Rational(4, 5).gcd(Rational(2, 9)) == Rational(2, 45) + assert Rational(4, 5).lcm(Rational(2, 9)) == Integer(4) + assert Rational(5, 9).lcm(Rational(3, 7)) == Rational(Integer(5).lcm(3),Integer(9).gcd(7)) + + assert Integer(4).cofactors(2) == (Integer(2), Integer(2), Integer(1)) + assert Integer(4).cofactors(Integer(2)) == \ + (Integer(2), Integer(2), Integer(1)) + + assert Integer(4).gcd(Float(2.0)) == Float(1.0) + assert Integer(4).lcm(Float(2.0)) == Float(8.0) + assert Integer(4).cofactors(Float(2.0)) == (Float(1.0), Float(4.0), Float(2.0)) + + assert S.Half.gcd(Float(2.0)) == Float(1.0) + assert S.Half.lcm(Float(2.0)) == Float(1.0) + assert S.Half.cofactors(Float(2.0)) == \ + (Float(1.0), Float(0.5), Float(2.0)) + + +def test_Float_gcd_lcm_cofactors(): + assert Float(2.0).gcd(Integer(4)) == Float(1.0) + assert Float(2.0).lcm(Integer(4)) == Float(8.0) + assert Float(2.0).cofactors(Integer(4)) == (Float(1.0), Float(2.0), Float(4.0)) + + assert Float(2.0).gcd(S.Half) == Float(1.0) + assert Float(2.0).lcm(S.Half) == Float(1.0) + assert Float(2.0).cofactors(S.Half) == \ + (Float(1.0), Float(2.0), Float(0.5)) + + +def test_issue_4611(): + assert abs(pi._evalf(50) - 3.14159265358979) < 1e-10 + assert abs(E._evalf(50) - 2.71828182845905) < 1e-10 + assert abs(Catalan._evalf(50) - 0.915965594177219) < 1e-10 + assert abs(EulerGamma._evalf(50) - 0.577215664901533) < 1e-10 + assert abs(GoldenRatio._evalf(50) - 1.61803398874989) < 1e-10 + assert abs(TribonacciConstant._evalf(50) - 1.83928675521416) < 1e-10 + + x = Symbol("x") + assert (pi + x).evalf() == pi.evalf() + x + assert (E + x).evalf() == E.evalf() + x + assert (Catalan + x).evalf() == Catalan.evalf() + x + assert (EulerGamma + x).evalf() == EulerGamma.evalf() + x + assert (GoldenRatio + x).evalf() == GoldenRatio.evalf() + x + assert (TribonacciConstant + x).evalf() == TribonacciConstant.evalf() + x + + +@conserve_mpmath_dps +def test_conversion_to_mpmath(): + assert mpmath.mpmathify(Integer(1)) == mpmath.mpf(1) + assert mpmath.mpmathify(S.Half) == mpmath.mpf(0.5) + assert mpmath.mpmathify(Float('1.23', 15)) == mpmath.mpf('1.23') + + assert mpmath.mpmathify(I) == mpmath.mpc(1j) + + assert mpmath.mpmathify(1 + 2*I) == mpmath.mpc(1 + 2j) + assert mpmath.mpmathify(1.0 + 2*I) == mpmath.mpc(1 + 2j) + assert mpmath.mpmathify(1 + 2.0*I) == mpmath.mpc(1 + 2j) + assert mpmath.mpmathify(1.0 + 2.0*I) == mpmath.mpc(1 + 2j) + assert mpmath.mpmathify(S.Half + S.Half*I) == mpmath.mpc(0.5 + 0.5j) + + assert mpmath.mpmathify(2*I) == mpmath.mpc(2j) + assert mpmath.mpmathify(2.0*I) == mpmath.mpc(2j) + assert mpmath.mpmathify(S.Half*I) == mpmath.mpc(0.5j) + + mpmath.mp.dps = 100 + assert mpmath.mpmathify(pi.evalf(100) + pi.evalf(100)*I) == mpmath.pi + mpmath.pi*mpmath.j + assert mpmath.mpmathify(pi.evalf(100)*I) == mpmath.pi*mpmath.j + + +def test_relational(): + # real + x = S(.1) + assert (x != cos) is True + assert (x == cos) is False + + # rational + x = Rational(1, 3) + assert (x != cos) is True + assert (x == cos) is False + + # integer defers to rational so these tests are omitted + + # number symbol + x = pi + assert (x != cos) is True + assert (x == cos) is False + + +def test_Integer_as_index(): + assert 'hello'[Integer(2):] == 'llo' + + +def test_Rational_int(): + assert int( Rational(7, 5)) == 1 + assert int( S.Half) == 0 + assert int(Rational(-1, 2)) == 0 + assert int(-Rational(7, 5)) == -1 + + +def test_zoo(): + b = Symbol('b', finite=True) + nz = Symbol('nz', nonzero=True) + p = Symbol('p', positive=True) + n = Symbol('n', negative=True) + im = Symbol('i', imaginary=True) + c = Symbol('c', complex=True) + pb = Symbol('pb', positive=True) + nb = Symbol('nb', negative=True) + imb = Symbol('ib', imaginary=True, finite=True) + for i in [I, S.Infinity, S.NegativeInfinity, S.Zero, S.One, S.Pi, S.Half, S(3), log(3), + b, nz, p, n, im, pb, nb, imb, c]: + if i.is_finite and (i.is_real or i.is_imaginary): + assert i + zoo is zoo + assert i - zoo is zoo + assert zoo + i is zoo + assert zoo - i is zoo + elif i.is_finite is not False: + assert (i + zoo).is_Add + assert (i - zoo).is_Add + assert (zoo + i).is_Add + assert (zoo - i).is_Add + else: + assert (i + zoo) is S.NaN + assert (i - zoo) is S.NaN + assert (zoo + i) is S.NaN + assert (zoo - i) is S.NaN + + if fuzzy_not(i.is_zero) and (i.is_extended_real or i.is_imaginary): + assert i*zoo is zoo + assert zoo*i is zoo + elif i.is_zero: + assert i*zoo is S.NaN + assert zoo*i is S.NaN + else: + assert (i*zoo).is_Mul + assert (zoo*i).is_Mul + + if fuzzy_not((1/i).is_zero) and (i.is_real or i.is_imaginary): + assert zoo/i is zoo + elif (1/i).is_zero: + assert zoo/i is S.NaN + elif i.is_zero: + assert zoo/i is zoo + else: + assert (zoo/i).is_Mul + + assert (I*oo).is_Mul # allow directed infinity + assert zoo + zoo is S.NaN + assert zoo * zoo is zoo + assert zoo - zoo is S.NaN + assert zoo/zoo is S.NaN + assert zoo**zoo is S.NaN + assert zoo**0 is S.One + assert zoo**2 is zoo + assert 1/zoo is S.Zero + + assert Mul.flatten([S.NegativeOne, oo, S(0)]) == ([S.NaN], [], None) + + +def test_issue_4122(): + x = Symbol('x', nonpositive=True) + assert oo + x is oo + x = Symbol('x', extended_nonpositive=True) + assert (oo + x).is_Add + x = Symbol('x', finite=True) + assert (oo + x).is_Add # x could be imaginary + x = Symbol('x', nonnegative=True) + assert oo + x is oo + x = Symbol('x', extended_nonnegative=True) + assert oo + x is oo + x = Symbol('x', finite=True, real=True) + assert oo + x is oo + + # similarly for negative infinity + x = Symbol('x', nonnegative=True) + assert -oo + x is -oo + x = Symbol('x', extended_nonnegative=True) + assert (-oo + x).is_Add + x = Symbol('x', finite=True) + assert (-oo + x).is_Add + x = Symbol('x', nonpositive=True) + assert -oo + x is -oo + x = Symbol('x', extended_nonpositive=True) + assert -oo + x is -oo + x = Symbol('x', finite=True, real=True) + assert -oo + x is -oo + + +def test_GoldenRatio_expand(): + assert GoldenRatio.expand(func=True) == S.Half + sqrt(5)/2 + + +def test_TribonacciConstant_expand(): + assert TribonacciConstant.expand(func=True) == \ + (1 + cbrt(19 - 3*sqrt(33)) + cbrt(19 + 3*sqrt(33))) / 3 + + +def test_as_content_primitive(): + assert S.Zero.as_content_primitive() == (1, 0) + assert S.Half.as_content_primitive() == (S.Half, 1) + assert (Rational(-1, 2)).as_content_primitive() == (S.Half, -1) + assert S(3).as_content_primitive() == (3, 1) + assert S(3.1).as_content_primitive() == (1, 3.1) + + +def test_hashing_sympy_integers(): + # Test for issue 5072 + assert {Integer(3)} == {int(3)} + assert hash(Integer(4)) == hash(int(4)) + + +def test_rounding_issue_4172(): + assert int((E**100).round()) == \ + 26881171418161354484126255515800135873611119 + assert int((pi**100).round()) == \ + 51878483143196131920862615246303013562686760680406 + assert int((Rational(1)/EulerGamma**100).round()) == \ + 734833795660954410469466 + + +@XFAIL +def test_mpmath_issues(): + from mpmath.libmp.libmpf import _normalize + import mpmath.libmp as mlib + rnd = mlib.round_nearest + mpf = (0, int(0), -123, -1, 53, rnd) # nan + assert _normalize(mpf, 53) != (0, int(0), 0, 0) + mpf = (0, int(0), -456, -2, 53, rnd) # +inf + assert _normalize(mpf, 53) != (0, int(0), 0, 0) + mpf = (1, int(0), -789, -3, 53, rnd) # -inf + assert _normalize(mpf, 53) != (0, int(0), 0, 0) + + from mpmath.libmp.libmpf import fnan + assert mlib.mpf_eq(fnan, fnan) + + +def test_Catalan_EulerGamma_prec(): + n = GoldenRatio + f = Float(n.n(), 5) + assert f._mpf_ == (0, int(212079), -17, 18) + assert f._prec == 20 + assert n._as_mpf_val(20) == f._mpf_ + + n = EulerGamma + f = Float(n.n(), 5) + assert f._mpf_ == (0, int(302627), -19, 19) + assert f._prec == 20 + assert n._as_mpf_val(20) == f._mpf_ + + +def test_Catalan_rewrite(): + k = Dummy('k', integer=True, nonnegative=True) + assert Catalan.rewrite(Sum).dummy_eq( + Sum((-1)**k/(2*k + 1)**2, (k, 0, oo))) + assert Catalan.rewrite() == Catalan + + +def test_bool_eq(): + assert 0 == False + assert S(0) == False + assert S(0) != S.false + assert 1 == True + assert S.One == True + assert S.One != S.true + + +def test_Float_eq(): + # Floats with different precision should not compare equal + assert Float(.5, 10) != Float(.5, 11) != Float(.5, 1) + # but floats that aren't exact in base-2 still + # don't compare the same because they have different + # underlying mpf values + assert Float(.12, 3) != Float(.12, 4) + assert Float(.12, 3) != .12 + assert 0.12 != Float(.12, 3) + assert Float('.12', 22) != .12 + # issue 11707 + # but Float/Rational -- except for 0 -- + # are exact so Rational(x) = Float(y) only if + # Rational(x) == Rational(Float(y)) + assert Float('1.1') != Rational(11, 10) + assert Rational(11, 10) != Float('1.1') + # coverage + assert not Float(3) == 2 + assert not Float(3) == Float(2) + assert not Float(3) == 3 + assert not Float(2**2) == S.Half + assert Float(2**2) == 4.0 + assert not Float(2**-2) == 1 + assert Float(2**-1) == 0.5 + assert not Float(2*3) == 3 + assert not Float(2*3) == 0.5 + assert Float(2*3) == 6.0 + assert not Float(2*3) == 6 + assert not Float(2*3) == 8 + assert not Float(.75) == Rational(3, 4) + assert Float(.75) == 0.75 + assert Float(5/18) == 5/18 + # 4473 + assert Float(2.) != 3 + assert not Float((0,1,-3)) == S.One/8 + assert Float((0,1,-3)) == 1/8 + assert Float((0,1,-3)) != S.One/9 + # 16196 + assert not 2 == Float(2) # unlike Python + assert t**2 != t**2.0 + + +def test_issue_6640(): + from mpmath.libmp.libmpf import finf, fninf + # fnan is not included because Float no longer returns fnan, + # but otherwise, the same sort of test could apply + assert Float(finf).is_zero is False + assert Float(fninf).is_zero is False + assert bool(Float(0)) is False + + +def test_issue_6349(): + assert Float('23.e3', '')._prec == 10 + assert Float('23e3', '')._prec == 20 + assert Float('23000', '')._prec == 20 + assert Float('-23000', '')._prec == 20 + + +def test_mpf_norm(): + assert mpf_norm((1, 0, 1, 0), 10) == mpf('0')._mpf_ + assert Float._new((1, 0, 1, 0), 10)._mpf_ == mpf('0')._mpf_ + + +def test_latex(): + assert latex(pi) == r"\pi" + assert latex(E) == r"e" + assert latex(GoldenRatio) == r"\phi" + assert latex(TribonacciConstant) == r"\text{TribonacciConstant}" + assert latex(EulerGamma) == r"\gamma" + assert latex(oo) == r"\infty" + assert latex(-oo) == r"-\infty" + assert latex(zoo) == r"\tilde{\infty}" + assert latex(nan) == r"\text{NaN}" + assert latex(I) == r"i" + + +def test_issue_7742(): + assert -oo % 1 is nan + + +def test_simplify_AlgebraicNumber(): + A = AlgebraicNumber + e = 3**(S.One/6)*(3 + (135 + 78*sqrt(3))**Rational(2, 3))/(45 + 26*sqrt(3))**(S.One/3) + assert simplify(A(e)) == A(12) # wester test_C20 + + e = (41 + 29*sqrt(2))**(S.One/5) + assert simplify(A(e)) == A(1 + sqrt(2)) # wester test_C21 + + e = (3 + 4*I)**Rational(3, 2) + assert simplify(A(e)) == A(2 + 11*I) # issue 4401 + + +def test_Float_idempotence(): + x = Float('1.23', '') + y = Float(x) + z = Float(x, 15) + assert same_and_same_prec(y, x) + assert not same_and_same_prec(z, x) + x = Float(10**20) + y = Float(x) + z = Float(x, 15) + assert same_and_same_prec(y, x) + assert not same_and_same_prec(z, x) + + +def test_comp1(): + # sqrt(2) = 1.414213 5623730950... + a = sqrt(2).n(7) + assert comp(a, 1.4142129) is False + assert comp(a, 1.4142130) + # ... + assert comp(a, 1.4142141) + assert comp(a, 1.4142142) is False + assert comp(sqrt(2).n(2), '1.4') + assert comp(sqrt(2).n(2), Float(1.4, 2), '') + assert comp(sqrt(2).n(2), 1.4, '') + assert comp(sqrt(2).n(2), Float(1.4, 3), '') is False + assert comp(sqrt(2) + sqrt(3)*I, 1.4 + 1.7*I, .1) + assert not comp(sqrt(2) + sqrt(3)*I, (1.5 + 1.7*I)*0.89, .1) + assert comp(sqrt(2) + sqrt(3)*I, (1.5 + 1.7*I)*0.90, .1) + assert comp(sqrt(2) + sqrt(3)*I, (1.5 + 1.7*I)*1.07, .1) + assert not comp(sqrt(2) + sqrt(3)*I, (1.5 + 1.7*I)*1.08, .1) + assert [(i, j) + for i in range(130, 150) + for j in range(170, 180) + if comp((sqrt(2)+ I*sqrt(3)).n(3), i/100. + I*j/100.)] == [ + (141, 173), (142, 173)] + raises(ValueError, lambda: comp(t, '1')) + raises(ValueError, lambda: comp(t, 1)) + assert comp(0, 0.0) + assert comp(.5, S.Half) + assert comp(2 + sqrt(2), 2.0 + sqrt(2)) + assert not comp(0, 1) + assert not comp(2, sqrt(2)) + assert not comp(2 + I, 2.0 + sqrt(2)) + assert not comp(2.0 + sqrt(2), 2 + I) + assert not comp(2.0 + sqrt(2), sqrt(3)) + assert comp(1/pi.n(4), 0.3183, 1e-5) + assert not comp(1/pi.n(4), 0.3183, 8e-6) + + +def test_issue_9491(): + assert oo**zoo is nan + + +def test_issue_10063(): + assert 2**Float(3) == Float(8) + + +def test_issue_10020(): + assert oo**I is S.NaN + assert oo**(1 + I) is S.ComplexInfinity + assert oo**(-1 + I) is S.Zero + assert (-oo)**I is S.NaN + assert (-oo)**(-1 + I) is S.Zero + assert oo**t == Pow(oo, t, evaluate=False) + assert (-oo)**t == Pow(-oo, t, evaluate=False) + + +def test_invert_numbers(): + assert S(2).invert(5) == 3 + assert S(2).invert(Rational(5, 2)) == S.Half + assert S(2).invert(5.) == S.Half + assert S(2).invert(S(5)) == 3 + assert S(2.).invert(5) == 0.5 + assert S(sqrt(2)).invert(5) == 1/sqrt(2) + assert S(sqrt(2)).invert(sqrt(3)) == 1/sqrt(2) + + +def test_mod_inverse(): + assert mod_inverse(3, 11) == 4 + assert mod_inverse(5, 11) == 9 + assert mod_inverse(21124921, 521512) == 7713 + assert mod_inverse(124215421, 5125) == 2981 + assert mod_inverse(214, 12515) == 1579 + assert mod_inverse(5823991, 3299) == 1442 + assert mod_inverse(123, 44) == 39 + assert mod_inverse(2, 5) == 3 + assert mod_inverse(-2, 5) == 2 + assert mod_inverse(2, -5) == -2 + assert mod_inverse(-2, -5) == -3 + assert mod_inverse(-3, -7) == -5 + x = Symbol('x') + assert S(2).invert(x) == S.Half + raises(TypeError, lambda: mod_inverse(2, x)) + raises(ValueError, lambda: mod_inverse(2, S.Half)) + raises(ValueError, lambda: mod_inverse(2, cos(1)**2 + sin(1)**2)) + + +def test_golden_ratio_rewrite_as_sqrt(): + assert GoldenRatio.rewrite(sqrt) == S.Half + sqrt(5)*S.Half + + +def test_tribonacci_constant_rewrite_as_sqrt(): + assert TribonacciConstant.rewrite(sqrt) == \ + (1 + cbrt(19 - 3*sqrt(33)) + cbrt(19 + 3*sqrt(33))) / 3 + + +def test_comparisons_with_unknown_type(): + class Foo: + """ + Class that is unaware of Basic, and relies on both classes returning + the NotImplemented singleton for equivalence to evaluate to False. + + """ + + ni, nf, nr = Integer(3), Float(1.0), Rational(1, 3) + foo = Foo() + + for n in ni, nf, nr, oo, -oo, zoo, nan: + assert n != foo + assert foo != n + assert not n == foo + assert not foo == n + raises(TypeError, lambda: n < foo) + raises(TypeError, lambda: foo > n) + raises(TypeError, lambda: n > foo) + raises(TypeError, lambda: foo < n) + raises(TypeError, lambda: n <= foo) + raises(TypeError, lambda: foo >= n) + raises(TypeError, lambda: n >= foo) + raises(TypeError, lambda: foo <= n) + + class Bar: + """ + Class that considers itself equal to any instance of Number except + infinities and nans, and relies on SymPy types returning the + NotImplemented singleton for symmetric equality relations. + + """ + def __eq__(self, other): + if other in (oo, -oo, zoo, nan): + return False + if isinstance(other, Number): + return True + return NotImplemented + + def __ne__(self, other): + return not self == other + + bar = Bar() + + for n in ni, nf, nr: + assert n == bar + assert bar == n + assert not n != bar + assert not bar != n + + for n in oo, -oo, zoo, nan: + assert n != bar + assert bar != n + assert not n == bar + assert not bar == n + + for n in ni, nf, nr, oo, -oo, zoo, nan: + raises(TypeError, lambda: n < bar) + raises(TypeError, lambda: bar > n) + raises(TypeError, lambda: n > bar) + raises(TypeError, lambda: bar < n) + raises(TypeError, lambda: n <= bar) + raises(TypeError, lambda: bar >= n) + raises(TypeError, lambda: n >= bar) + raises(TypeError, lambda: bar <= n) + + +def test_NumberSymbol_comparison(): + from sympy.core.tests.test_relational import rel_check + rpi = Rational('905502432259640373/288230376151711744') + fpi = Float(float(pi)) + assert rel_check(rpi, fpi) + + +def test_Integer_precision(): + # Make sure Integer inputs for keyword args work + assert Float('1.0', dps=Integer(15))._prec == 53 + assert Float('1.0', precision=Integer(15))._prec == 15 + assert type(Float('1.0', precision=Integer(15))._prec) == int + assert sympify(srepr(Float('1.0', precision=15))) == Float('1.0', precision=15) + + +def test_numpy_to_float(): + from sympy.testing.pytest import skip + from sympy.external import import_module + np = import_module('numpy') + if not np: + skip('numpy not installed. Abort numpy tests.') + + def check_prec_and_relerr(npval, ratval): + prec = np.finfo(npval).nmant + 1 + x = Float(npval) + assert x._prec == prec + y = Float(ratval, precision=prec) + assert abs((x - y)/y) < 2**(-(prec + 1)) + + check_prec_and_relerr(np.float16(2.0/3), Rational(2, 3)) + check_prec_and_relerr(np.float32(2.0/3), Rational(2, 3)) + check_prec_and_relerr(np.float64(2.0/3), Rational(2, 3)) + # extended precision, on some arch/compilers: + x = np.longdouble(2)/3 + check_prec_and_relerr(x, Rational(2, 3)) + y = Float(x, precision=10) + assert same_and_same_prec(y, Float(Rational(2, 3), precision=10)) + + raises(TypeError, lambda: Float(np.complex64(1+2j))) + raises(TypeError, lambda: Float(np.complex128(1+2j))) + + +def test_Integer_ceiling_floor(): + a = Integer(4) + + assert a.floor() == a + assert a.ceiling() == a + + +def test_ComplexInfinity(): + assert zoo.floor() is zoo + assert zoo.ceiling() is zoo + assert zoo**zoo is S.NaN + + +def test_Infinity_floor_ceiling_power(): + assert oo.floor() is oo + assert oo.ceiling() is oo + assert oo**S.NaN is S.NaN + assert oo**zoo is S.NaN + + +def test_One_power(): + assert S.One**12 is S.One + assert S.NegativeOne**S.NaN is S.NaN + + +def test_NegativeInfinity(): + assert (-oo).floor() is -oo + assert (-oo).ceiling() is -oo + assert (-oo)**11 is -oo + assert (-oo)**12 is oo + + +def test_issue_6133(): + raises(TypeError, lambda: (-oo < None)) + raises(TypeError, lambda: (S(-2) < None)) + raises(TypeError, lambda: (oo < None)) + raises(TypeError, lambda: (oo > None)) + raises(TypeError, lambda: (S(2) < None)) + + +def test_abc(): + x = numbers.Float(5) + assert(isinstance(x, nums.Number)) + assert(isinstance(x, numbers.Number)) + assert(isinstance(x, nums.Real)) + y = numbers.Rational(1, 3) + assert(isinstance(y, nums.Number)) + assert(y.numerator == 1) + assert(y.denominator == 3) + assert(isinstance(y, nums.Rational)) + z = numbers.Integer(3) + assert(isinstance(z, nums.Number)) + assert(isinstance(z, numbers.Number)) + assert(isinstance(z, nums.Rational)) + assert(isinstance(z, numbers.Rational)) + assert(isinstance(z, nums.Integral)) + + +def test_floordiv(): + assert S(2)//S.Half == 4 + + +def test_negation(): + assert -S.Zero is S.Zero + assert -Float(0) is not S.Zero and -Float(0) == 0.0 + + +def test_exponentiation_of_0(): + x = Symbol('x') + assert 0**-x == zoo**x + assert unchanged(Pow, 0, x) + x = Symbol('x', zero=True) + assert 0**-x == S.One + assert 0**x == S.One + + +def test_int_valued(): + x = Symbol('x') + assert int_valued(x) == False + assert int_valued(S.Half) == False + assert int_valued(S.One) == True + assert int_valued(Float(1)) == True + assert int_valued(Float(1.1)) == False + assert int_valued(pi) == False + + +def test_equal_valued(): + x = Symbol('x') + + equal_values = [ + [1, 1.0, S(1), S(1.0), S(1).n(5)], + [2, 2.0, S(2), S(2.0), S(2).n(5)], + [-1, -1.0, -S(1), -S(1.0), -S(1).n(5)], + [0.5, S(0.5), S(1)/2], + [-0.5, -S(0.5), -S(1)/2], + [0, 0.0, S(0), S(0.0), S(0).n()], + [pi], [pi.n()], # <-- not equal + [S(1)/10], [0.1, S(0.1)], # <-- not equal + [S(0.1).n(5)], + [oo], + [cos(x/2)], [cos(0.5*x)], # <-- no recursion + ] + + for m, values_m in enumerate(equal_values): + for value_i in values_m: + + # All values in same list equal + for value_j in values_m: + assert equal_valued(value_i, value_j) is True + + # Not equal to anything in any other list: + for n, values_n in enumerate(equal_values): + if n == m: + continue + for value_j in values_n: + assert equal_valued(value_i, value_j) is False + + +def test_all_close(): + x = Symbol('x') + y = Symbol('y') + z = Symbol('z') + assert all_close(2, 2) is True + assert all_close(2, 2.0000) is True + assert all_close(2, 2.0001) is False + assert all_close(1/3, 1/3.0001) is False + assert all_close(1/3, 1/3.0001, 1e-3, 1e-3) is True + assert all_close(1/3, Rational(1, 3)) is True + assert all_close(0.1*exp(0.2*x), exp(x/5)/10) is True + # The expressions should be structurally the same modulo identity: + assert all_close(1.4142135623730951, sqrt(2)) is False + assert all_close(1.4142135623730951, sqrt(2).evalf()) is True + assert all_close(x + 1e-20, x) is True + # We should be able to match terms of an Add/Mul in any order + assert all_close(Add(1, 2, evaluate=False), Add(2, 1, evaluate=False)) + # coverage + assert not all_close(2*x, 3*x) + assert all_close(2*x, 3*x, 1) + assert not all_close(2*x, 3*x, 0, 0.5) + assert all_close(2*x, 3*x, 0, 1) + assert not all_close(y*x, z*x) + assert all_close(2*x*exp(1.0*x), 2.0*x*exp(x)) + assert not all_close(2*x*exp(1.0*x), 2.0*x*exp(2.*x)) + assert all_close(x + 2.*y, 1.*x + 2*y) + assert all_close(x + exp(2.*x)*y, 1.*x + exp(2*x)*y) + assert not all_close(x + exp(2.*x)*y, 1.*x + 2*exp(2*x)*y) + assert not all_close(x + exp(2.*x)*y, 1.*x + exp(3*x)*y) + assert not all_close(x + 2.*y, 1.*x + 3*y) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_operations.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_operations.py new file mode 100644 index 0000000000000000000000000000000000000000..c60d691ef00ee9601ada04ef68e2db37794a81ad --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_operations.py @@ -0,0 +1,110 @@ +from sympy.core.expr import Expr +from sympy.core.numbers import Integer +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.core.operations import AssocOp, LatticeOp +from sympy.testing.pytest import raises +from sympy.core.sympify import SympifyError +from sympy.core.add import Add, add +from sympy.core.mul import Mul, mul + +# create the simplest possible Lattice class + + +class join(LatticeOp): + zero = Integer(0) + identity = Integer(1) + + +def test_lattice_simple(): + assert join(join(2, 3), 4) == join(2, join(3, 4)) + assert join(2, 3) == join(3, 2) + assert join(0, 2) == 0 + assert join(1, 2) == 2 + assert join(2, 2) == 2 + + assert join(join(2, 3), 4) == join(2, 3, 4) + assert join() == 1 + assert join(4) == 4 + assert join(1, 4, 2, 3, 1, 3, 2) == join(2, 3, 4) + + +def test_lattice_shortcircuit(): + raises(SympifyError, lambda: join(object)) + assert join(0, object) == 0 + + +def test_lattice_print(): + assert str(join(5, 4, 3, 2)) == 'join(2, 3, 4, 5)' + + +def test_lattice_make_args(): + assert join.make_args(join(2, 3, 4)) == {S(2), S(3), S(4)} + assert join.make_args(0) == {0} + assert list(join.make_args(0))[0] is S.Zero + assert Add.make_args(0)[0] is S.Zero + + +def test_issue_14025(): + a, b, c, d = symbols('a,b,c,d', commutative=False) + assert Mul(a, b, c).has(c*b) == False + assert Mul(a, b, c).has(b*c) == True + assert Mul(a, b, c, d).has(b*c*d) == True + + +def test_AssocOp_flatten(): + a, b, c, d = symbols('a,b,c,d') + + class MyAssoc(AssocOp): + identity = S.One + + assert MyAssoc(a, MyAssoc(b, c)).args == \ + MyAssoc(MyAssoc(a, b), c).args == \ + MyAssoc(MyAssoc(a, b, c)).args == \ + MyAssoc(a, b, c).args == \ + (a, b, c) + u = MyAssoc(b, c) + v = MyAssoc(u, d, evaluate=False) + assert v.args == (u, d) + # like Add, any unevaluated outer call will flatten inner args + assert MyAssoc(a, v).args == (a, b, c, d) + + +def test_add_dispatcher(): + + class NewBase(Expr): + @property + def _add_handler(self): + return NewAdd + class NewAdd(NewBase, Add): + pass + add.register_handlerclass((Add, NewAdd), NewAdd) + + a, b = Symbol('a'), NewBase() + + # Add called as fallback + assert add(1, 2) == Add(1, 2) + assert add(a, a) == Add(a, a) + + # selection by registered priority + assert add(a,b,a) == NewAdd(2*a, b) + + +def test_mul_dispatcher(): + + class NewBase(Expr): + @property + def _mul_handler(self): + return NewMul + class NewMul(NewBase, Mul): + pass + mul.register_handlerclass((Mul, NewMul), NewMul) + + a, b = Symbol('a'), NewBase() + + # Mul called as fallback + assert mul(1, 2) == Mul(1, 2) + assert mul(a, a) == Mul(a, a) + + # selection by registered priority + assert mul(a,b,a) == NewMul(a**2, b) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_parameters.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_parameters.py new file mode 100644 index 0000000000000000000000000000000000000000..21e03d717872a9a8165ceeebf7ef58e9842702c0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_parameters.py @@ -0,0 +1,126 @@ +from sympy.abc import x, y +from sympy.core.parameters import evaluate +from sympy.core import Mul, Add, Pow, S +from sympy.core.numbers import oo +from sympy.functions.elementary.miscellaneous import sqrt + +def test_add(): + with evaluate(False): + p = oo - oo + assert isinstance(p, Add) and p.args == (oo, -oo) + p = 5 - oo + assert isinstance(p, Add) and p.args == (-oo, 5) + p = oo - 5 + assert isinstance(p, Add) and p.args == (oo, -5) + p = oo + 5 + assert isinstance(p, Add) and p.args == (oo, 5) + p = 5 + oo + assert isinstance(p, Add) and p.args == (oo, 5) + p = -oo + 5 + assert isinstance(p, Add) and p.args == (-oo, 5) + p = -5 - oo + assert isinstance(p, Add) and p.args == (-oo, -5) + + with evaluate(False): + expr = x + x + assert isinstance(expr, Add) + assert expr.args == (x, x) + + with evaluate(True): + assert (x + x).args == (2, x) + + assert (x + x).args == (x, x) + + assert isinstance(x + x, Mul) + + with evaluate(False): + assert S.One + 1 == Add(1, 1) + assert 1 + S.One == Add(1, 1) + + assert S(4) - 3 == Add(4, -3) + assert -3 + S(4) == Add(4, -3) + + assert S(2) * 4 == Mul(2, 4) + assert 4 * S(2) == Mul(2, 4) + + assert S(6) / 3 == Mul(6, Pow(3, -1)) + assert S.One / 3 * 6 == Mul(S.One / 3, 6) + + assert 9 ** S(2) == Pow(9, 2) + assert S(2) ** 9 == Pow(2, 9) + + assert S(2) / 2 == Mul(2, Pow(2, -1)) + assert S.One / 2 * 2 == Mul(S.One / 2, 2) + + assert S(2) / 3 + 1 == Add(S(2) / 3, 1) + assert 1 + S(2) / 3 == Add(1, S(2) / 3) + + assert S(4) / 7 - 3 == Add(S(4) / 7, -3) + assert -3 + S(4) / 7 == Add(-3, S(4) / 7) + + assert S(2) / 4 * 4 == Mul(S(2) / 4, 4) + assert 4 * (S(2) / 4) == Mul(4, S(2) / 4) + + assert S(6) / 3 == Mul(6, Pow(3, -1)) + assert S.One / 3 * 6 == Mul(S.One / 3, 6) + + assert S.One / 3 + sqrt(3) == Add(S.One / 3, sqrt(3)) + assert sqrt(3) + S.One / 3 == Add(sqrt(3), S.One / 3) + + assert S.One / 2 * 10.333 == Mul(S.One / 2, 10.333) + assert 10.333 * (S.One / 2) == Mul(10.333, S.One / 2) + + assert sqrt(2) * sqrt(2) == Mul(sqrt(2), sqrt(2)) + + assert S.One / 2 + x == Add(S.One / 2, x) + assert x + S.One / 2 == Add(x, S.One / 2) + + assert S.One / x * x == Mul(S.One / x, x) + assert x * (S.One / x) == Mul(x, Pow(x, -1)) + + assert S.One / 3 == Pow(3, -1) + assert S.One / x == Pow(x, -1) + assert 1 / S(3) == Pow(3, -1) + assert 1 / x == Pow(x, -1) + +def test_nested(): + with evaluate(False): + expr = (x + x) + (y + y) + assert expr.args == ((x + x), (y + y)) + assert expr.args[0].args == (x, x) + +def test_reentrantcy(): + with evaluate(False): + expr = x + x + assert expr.args == (x, x) + with evaluate(True): + expr = x + x + assert expr.args == (2, x) + expr = x + x + assert expr.args == (x, x) + +def test_reusability(): + f = evaluate(False) + + with f: + expr = x + x + assert expr.args == (x, x) + + expr = x + x + assert expr.args == (2, x) + + with f: + expr = x + x + assert expr.args == (x, x) + + # Assure reentrancy with reusability + ctx = evaluate(False) + with ctx: + expr = x + x + assert expr.args == (x, x) + with ctx: + expr = x + x + assert expr.args == (x, x) + + expr = x + x + assert expr.args == (2, x) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_power.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_power.py new file mode 100644 index 0000000000000000000000000000000000000000..80ae48c525c20da6153deffbc9feadef81acf527 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_power.py @@ -0,0 +1,670 @@ +from sympy.core import ( + Basic, Rational, Symbol, S, Float, Integer, Mul, Number, Pow, + Expr, I, nan, pi, symbols, oo, zoo, N) +from sympy.core.parameters import global_parameters +from sympy.core.tests.test_evalf import NS +from sympy.core.function import expand_multinomial +from sympy.functions.elementary.miscellaneous import sqrt, cbrt +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.special.error_functions import erf +from sympy.functions.elementary.trigonometric import ( + sin, cos, tan, sec, csc, atan) +from sympy.functions.elementary.hyperbolic import cosh, sinh, tanh +from sympy.polys import Poly +from sympy.series.order import O +from sympy.sets import FiniteSet +from sympy.core.power import power +from sympy.core.intfunc import integer_nthroot +from sympy.testing.pytest import warns, _both_exp_pow +from sympy.utilities.exceptions import SymPyDeprecationWarning +from sympy.abc import a, b, c, x, y +from sympy.core.numbers import all_close + +def test_rational(): + a = Rational(1, 5) + + r = sqrt(5)/5 + assert sqrt(a) == r + assert 2*sqrt(a) == 2*r + + r = a*a**S.Half + assert a**Rational(3, 2) == r + assert 2*a**Rational(3, 2) == 2*r + + r = a**5*a**Rational(2, 3) + assert a**Rational(17, 3) == r + assert 2 * a**Rational(17, 3) == 2*r + + +def test_large_rational(): + e = (Rational(123712**12 - 1, 7) + Rational(1, 7))**Rational(1, 3) + assert e == 234232585392159195136 * (Rational(1, 7)**Rational(1, 3)) + + +def test_negative_real(): + def feq(a, b): + return abs(a - b) < 1E-10 + + assert feq(S.One / Float(-0.5), -Integer(2)) + + +def test_expand(): + assert (2**(-1 - x)).expand() == S.Half*2**(-x) + + +def test_issue_3449(): + #test if powers are simplified correctly + #see also issue 3995 + assert ((x**Rational(1, 3))**Rational(2)) == x**Rational(2, 3) + assert ( + (x**Rational(3))**Rational(2, 5)) == (x**Rational(3))**Rational(2, 5) + + a = Symbol('a', real=True) + b = Symbol('b', real=True) + assert (a**2)**b == (abs(a)**b)**2 + assert sqrt(1/a) != 1/sqrt(a) # e.g. for a = -1 + assert (a**3)**Rational(1, 3) != a + assert (x**a)**b != x**(a*b) # e.g. x = -1, a=2, b=1/2 + assert (x**.5)**b == x**(.5*b) + assert (x**.5)**.5 == x**.25 + assert (x**2.5)**.5 != x**1.25 # e.g. for x = 5*I + + k = Symbol('k', integer=True) + m = Symbol('m', integer=True) + assert (x**k)**m == x**(k*m) + assert Number(5)**Rational(2, 3) == Number(25)**Rational(1, 3) + + assert (x**.5)**2 == x**1.0 + assert (x**2)**k == (x**k)**2 == x**(2*k) + + a = Symbol('a', positive=True) + assert (a**3)**Rational(2, 5) == a**Rational(6, 5) + assert (a**2)**b == (a**b)**2 + assert (a**Rational(2, 3))**x == a**(x*Rational(2, 3)) != (a**x)**Rational(2, 3) + + +def test_issue_3866(): + assert --sqrt(sqrt(5) - 1) == sqrt(sqrt(5) - 1) + + +def test_negative_one(): + x = Symbol('x', complex=True) + y = Symbol('y', complex=True) + assert 1/x**y == x**(-y) + + +def test_issue_4362(): + neg = Symbol('neg', negative=True) + nonneg = Symbol('nonneg', nonnegative=True) + any = Symbol('any') + num, den = sqrt(1/neg).as_numer_denom() + assert num == sqrt(-1) + assert den == sqrt(-neg) + num, den = sqrt(1/nonneg).as_numer_denom() + assert num == 1 + assert den == sqrt(nonneg) + num, den = sqrt(1/any).as_numer_denom() + assert num == sqrt(1/any) + assert den == 1 + + def eqn(num, den, pow): + return (num/den)**pow + npos = 1 + nneg = -1 + dpos = 2 - sqrt(3) + dneg = 1 - sqrt(3) + assert dpos > 0 and dneg < 0 and npos > 0 and nneg < 0 + # pos or neg integer + eq = eqn(npos, dpos, 2) + assert eq.is_Pow and eq.as_numer_denom() == (1, dpos**2) + eq = eqn(npos, dneg, 2) + assert eq.is_Pow and eq.as_numer_denom() == (1, dneg**2) + eq = eqn(nneg, dpos, 2) + assert eq.is_Pow and eq.as_numer_denom() == (1, dpos**2) + eq = eqn(nneg, dneg, 2) + assert eq.is_Pow and eq.as_numer_denom() == (1, dneg**2) + eq = eqn(npos, dpos, -2) + assert eq.is_Pow and eq.as_numer_denom() == (dpos**2, 1) + eq = eqn(npos, dneg, -2) + assert eq.is_Pow and eq.as_numer_denom() == (dneg**2, 1) + eq = eqn(nneg, dpos, -2) + assert eq.is_Pow and eq.as_numer_denom() == (dpos**2, 1) + eq = eqn(nneg, dneg, -2) + assert eq.is_Pow and eq.as_numer_denom() == (dneg**2, 1) + # pos or neg rational + pow = S.Half + eq = eqn(npos, dpos, pow) + assert eq.is_Pow and eq.as_numer_denom() == (npos**pow, dpos**pow) + eq = eqn(npos, dneg, pow) + assert eq.is_Pow is False and eq.as_numer_denom() == ((-npos)**pow, (-dneg)**pow) + eq = eqn(nneg, dpos, pow) + assert not eq.is_Pow or eq.as_numer_denom() == (nneg**pow, dpos**pow) + eq = eqn(nneg, dneg, pow) + assert eq.is_Pow and eq.as_numer_denom() == ((-nneg)**pow, (-dneg)**pow) + eq = eqn(npos, dpos, -pow) + assert eq.is_Pow and eq.as_numer_denom() == (dpos**pow, npos**pow) + eq = eqn(npos, dneg, -pow) + assert eq.is_Pow is False and eq.as_numer_denom() == (-(-npos)**pow*(-dneg)**pow, npos) + eq = eqn(nneg, dpos, -pow) + assert not eq.is_Pow or eq.as_numer_denom() == (dpos**pow, nneg**pow) + eq = eqn(nneg, dneg, -pow) + assert eq.is_Pow and eq.as_numer_denom() == ((-dneg)**pow, (-nneg)**pow) + # unknown exponent + pow = 2*any + eq = eqn(npos, dpos, pow) + assert eq.is_Pow and eq.as_numer_denom() == (npos**pow, dpos**pow) + eq = eqn(npos, dneg, pow) + assert eq.is_Pow and eq.as_numer_denom() == ((-npos)**pow, (-dneg)**pow) + eq = eqn(nneg, dpos, pow) + assert eq.is_Pow and eq.as_numer_denom() == (nneg**pow, dpos**pow) + eq = eqn(nneg, dneg, pow) + assert eq.is_Pow and eq.as_numer_denom() == ((-nneg)**pow, (-dneg)**pow) + eq = eqn(npos, dpos, -pow) + assert eq.as_numer_denom() == (dpos**pow, npos**pow) + eq = eqn(npos, dneg, -pow) + assert eq.is_Pow and eq.as_numer_denom() == ((-dneg)**pow, (-npos)**pow) + eq = eqn(nneg, dpos, -pow) + assert eq.is_Pow and eq.as_numer_denom() == (dpos**pow, nneg**pow) + eq = eqn(nneg, dneg, -pow) + assert eq.is_Pow and eq.as_numer_denom() == ((-dneg)**pow, (-nneg)**pow) + + assert ((1/(1 + x/3))**(-S.One)).as_numer_denom() == (3 + x, 3) + notp = Symbol('notp', positive=False) # not positive does not imply real + b = ((1 + x/notp)**-2) + assert (b**(-y)).as_numer_denom() == (1, b**y) + assert (b**(-S.One)).as_numer_denom() == ((notp + x)**2, notp**2) + nonp = Symbol('nonp', nonpositive=True) + assert (((1 + x/nonp)**-2)**(-S.One)).as_numer_denom() == ((-nonp - + x)**2, nonp**2) + + n = Symbol('n', negative=True) + assert (x**n).as_numer_denom() == (1, x**-n) + assert sqrt(1/n).as_numer_denom() == (S.ImaginaryUnit, sqrt(-n)) + n = Symbol('0 or neg', nonpositive=True) + # if x and n are split up without negating each term and n is negative + # then the answer might be wrong; if n is 0 it won't matter since + # 1/oo and 1/zoo are both zero as is sqrt(0)/sqrt(-x) unless x is also + # zero (in which case the negative sign doesn't matter): + # 1/sqrt(1/-1) = -I but sqrt(-1)/sqrt(1) = I + assert (1/sqrt(x/n)).as_numer_denom() == (sqrt(-n), sqrt(-x)) + c = Symbol('c', complex=True) + e = sqrt(1/c) + assert e.as_numer_denom() == (e, 1) + i = Symbol('i', integer=True) + assert ((1 + x/y)**i).as_numer_denom() == ((x + y)**i, y**i) + + +def test_Pow_Expr_args(): + bases = [Basic(), Poly(x, x), FiniteSet(x)] + for base in bases: + # The cache can mess with the stacklevel test + with warns(SymPyDeprecationWarning, test_stacklevel=False): + Pow(base, S.One) + + +def test_Pow_signs(): + """Cf. issues 4595 and 5250""" + n = Symbol('n', even=True) + assert (3 - y)**2 != (y - 3)**2 + assert (3 - y)**n != (y - 3)**n + assert (-3 + y - x)**2 != (3 - y + x)**2 + assert (y - 3)**3 != -(3 - y)**3 + + +def test_power_with_noncommutative_mul_as_base(): + x = Symbol('x', commutative=False) + y = Symbol('y', commutative=False) + assert not (x*y)**3 == x**3*y**3 + assert (2*x*y)**3 == 8*(x*y)**3 + + +@_both_exp_pow +def test_power_rewrite_exp(): + assert (I**I).rewrite(exp) == exp(-pi/2) + + expr = (2 + 3*I)**(4 + 5*I) + assert expr.rewrite(exp) == exp((4 + 5*I)*(log(sqrt(13)) + I*atan(Rational(3, 2)))) + assert expr.rewrite(exp).expand() == \ + 169*exp(5*I*log(13)/2)*exp(4*I*atan(Rational(3, 2)))*exp(-5*atan(Rational(3, 2))) + + assert ((6 + 7*I)**5).rewrite(exp) == 7225*sqrt(85)*exp(5*I*atan(Rational(7, 6))) + + expr = 5**(6 + 7*I) + assert expr.rewrite(exp) == exp((6 + 7*I)*log(5)) + assert expr.rewrite(exp).expand() == 15625*exp(7*I*log(5)) + + assert Pow(123, 789, evaluate=False).rewrite(exp) == 123**789 + assert (1**I).rewrite(exp) == 1**I + assert (0**I).rewrite(exp) == 0**I + + expr = (-2)**(2 + 5*I) + assert expr.rewrite(exp) == exp((2 + 5*I)*(log(2) + I*pi)) + assert expr.rewrite(exp).expand() == 4*exp(-5*pi)*exp(5*I*log(2)) + + assert ((-2)**S(-5)).rewrite(exp) == (-2)**S(-5) + + x, y = symbols('x y') + assert (x**y).rewrite(exp) == exp(y*log(x)) + if global_parameters.exp_is_pow: + assert (7**x).rewrite(exp) == Pow(S.Exp1, x*log(7), evaluate=False) + else: + assert (7**x).rewrite(exp) == exp(x*log(7), evaluate=False) + assert ((2 + 3*I)**x).rewrite(exp) == exp(x*(log(sqrt(13)) + I*atan(Rational(3, 2)))) + assert (y**(5 + 6*I)).rewrite(exp) == exp(log(y)*(5 + 6*I)) + + assert all((1/func(x)).rewrite(exp) == 1/(func(x).rewrite(exp)) for func in + (sin, cos, tan, sec, csc, sinh, cosh, tanh)) + + +def test_zero(): + assert 0**x != 0 + assert 0**(2*x) == 0**x + assert 0**(1.0*x) == 0**x + assert 0**(2.0*x) == 0**x + assert (0**(2 - x)).as_base_exp() == (0, 2 - x) + assert 0**(x - 2) != S.Infinity**(2 - x) + assert 0**(2*x*y) == 0**(x*y) + assert 0**(-2*x*y) == S.ComplexInfinity**(x*y) + assert Float(0)**2 is not S.Zero + assert Float(0)**2 == 0.0 + assert Float(0)**-2 is zoo + assert Float(0)**oo is S.Zero + + #Test issue 19572 + assert 0 ** -oo is zoo + assert power(0, -oo) is zoo + assert Float(0)**-oo is zoo + +def test_pow_as_base_exp(): + assert (S.Infinity**(2 - x)).as_base_exp() == (S.Infinity, 2 - x) + assert (S.Infinity**(x - 2)).as_base_exp() == (S.Infinity, x - 2) + p = S.Half**x + assert p.base, p.exp == p.as_base_exp() == (S(2), -x) + p = (S(3)/2)**x + assert p.base, p.exp == p.as_base_exp() == (3*S.Half, x) + p = (S(2)/3)**x + assert p.as_base_exp() == (S(2)/3, x) + assert p.base, p.exp == (S(2)/3, x) + # issue 8344: + assert Pow(1, 2, evaluate=False).as_base_exp() == (S.One, S(2)) + + +def test_nseries(): + assert sqrt(I*x - 1)._eval_nseries(x, 4, None, 1) == I + x/2 + I*x**2/8 - x**3/16 + O(x**4) + assert sqrt(I*x - 1)._eval_nseries(x, 4, None, -1) == -I - x/2 - I*x**2/8 + x**3/16 + O(x**4) + assert cbrt(I*x - 1)._eval_nseries(x, 4, None, 1) == (-1)**(S(1)/3) - (-1)**(S(5)/6)*x/3 + \ + (-1)**(S(1)/3)*x**2/9 + 5*(-1)**(S(5)/6)*x**3/81 + O(x**4) + assert cbrt(I*x - 1)._eval_nseries(x, 4, None, -1) == -(-1)**(S(2)/3) - (-1)**(S(1)/6)*x/3 - \ + (-1)**(S(2)/3)*x**2/9 + 5*(-1)**(S(1)/6)*x**3/81 + O(x**4) + assert (1 / (exp(-1/x) + 1/x))._eval_nseries(x, 2, None) == x + O(x**2) + # test issue 23752 + assert sqrt(-I*x**2 + x - 3)._eval_nseries(x, 4, None, 1) == -sqrt(3)*I + sqrt(3)*I*x/6 - \ + sqrt(3)*I*x**2*(-S(1)/72 + I/6) - sqrt(3)*I*x**3*(-S(1)/432 + I/36) + O(x**4) + assert sqrt(-I*x**2 + x - 3)._eval_nseries(x, 4, None, -1) == -sqrt(3)*I + sqrt(3)*I*x/6 - \ + sqrt(3)*I*x**2*(-S(1)/72 + I/6) - sqrt(3)*I*x**3*(-S(1)/432 + I/36) + O(x**4) + assert cbrt(-I*x**2 + x - 3)._eval_nseries(x, 4, None, 1) == -(-1)**(S(2)/3)*3**(S(1)/3) + \ + (-1)**(S(2)/3)*3**(S(1)/3)*x/9 - (-1)**(S(2)/3)*3**(S(1)/3)*x**2*(-S(1)/81 + I/9) - \ + (-1)**(S(2)/3)*3**(S(1)/3)*x**3*(-S(5)/2187 + 2*I/81) + O(x**4) + assert cbrt(-I*x**2 + x - 3)._eval_nseries(x, 4, None, -1) == -(-1)**(S(2)/3)*3**(S(1)/3) + \ + (-1)**(S(2)/3)*3**(S(1)/3)*x/9 - (-1)**(S(2)/3)*3**(S(1)/3)*x**2*(-S(1)/81 + I/9) - \ + (-1)**(S(2)/3)*3**(S(1)/3)*x**3*(-S(5)/2187 + 2*I/81) + O(x**4) + + +def test_issue_6100_12942_4473(): + assert x**1.0 != x + assert x != x**1.0 + assert True != x**1.0 + assert x**1.0 is not True + assert x is not True + assert x*y != (x*y)**1.0 + # Pow != Symbol + assert (x**1.0)**1.0 != x + assert (x**1.0)**2.0 != x**2 + b = Expr() + assert Pow(b, 1.0, evaluate=False) != b + # if the following gets distributed as a Mul (x**1.0*y**1.0 then + # __eq__ methods could be added to Symbol and Pow to detect the + # power-of-1.0 case. + assert ((x*y)**1.0).func is Pow + + +def test_issue_6208(): + from sympy.functions.elementary.miscellaneous import root + assert sqrt(33**(I*9/10)) == -33**(I*9/20) + assert root((6*I)**(2*I), 3).as_base_exp()[1] == Rational(1, 3) # != 2*I/3 + assert root((6*I)**(I/3), 3).as_base_exp()[1] == I/9 + assert sqrt(exp(3*I)) == exp(3*I/2) + assert sqrt(-sqrt(3)*(1 + 2*I)) == sqrt(sqrt(3))*sqrt(-1 - 2*I) + assert sqrt(exp(5*I)) == -exp(5*I/2) + assert root(exp(5*I), 3).exp == Rational(1, 3) + + +def test_issue_6990(): + assert (sqrt(a + b*x + x**2)).series(x, 0, 3).removeO() == \ + sqrt(a)*x**2*(1/(2*a) - b**2/(8*a**2)) + sqrt(a) + b*x/(2*sqrt(a)) + + +def test_issue_6068(): + assert sqrt(sin(x)).series(x, 0, 7) == \ + sqrt(x) - x**Rational(5, 2)/12 + x**Rational(9, 2)/1440 - \ + x**Rational(13, 2)/24192 + O(x**7) + assert sqrt(sin(x)).series(x, 0, 9) == \ + sqrt(x) - x**Rational(5, 2)/12 + x**Rational(9, 2)/1440 - \ + x**Rational(13, 2)/24192 - 67*x**Rational(17, 2)/29030400 + O(x**9) + assert sqrt(sin(x**3)).series(x, 0, 19) == \ + x**Rational(3, 2) - x**Rational(15, 2)/12 + x**Rational(27, 2)/1440 + O(x**19) + assert sqrt(sin(x**3)).series(x, 0, 20) == \ + x**Rational(3, 2) - x**Rational(15, 2)/12 + x**Rational(27, 2)/1440 - \ + x**Rational(39, 2)/24192 + O(x**20) + + +def test_issue_6782(): + assert sqrt(sin(x**3)).series(x, 0, 7) == x**Rational(3, 2) + O(x**7) + assert sqrt(sin(x**4)).series(x, 0, 3) == x**2 + O(x**3) + + +def test_issue_6653(): + assert (1 / sqrt(1 + sin(x**2))).series(x, 0, 3) == 1 - x**2/2 + O(x**3) + + +def test_issue_6429(): + f = (c**2 + x)**(0.5) + assert f.series(x, x0=0, n=1) == (c**2)**0.5 + O(x) + assert f.taylor_term(0, x) == (c**2)**0.5 + assert f.taylor_term(1, x) == 0.5*x*(c**2)**(-0.5) + assert f.taylor_term(2, x) == -0.125*x**2*(c**2)**(-1.5) + + +def test_issue_7638(): + f = pi/log(sqrt(2)) + assert ((1 + I)**(I*f/2))**0.3 == (1 + I)**(0.15*I*f) + # if 1/3 -> 1.0/3 this should fail since it cannot be shown that the + # sign will be +/-1; for the previous "small arg" case, it didn't matter + # that this could not be proved + assert (1 + I)**(4*I*f) == ((1 + I)**(12*I*f))**Rational(1, 3) + + assert (((1 + I)**(I*(1 + 7*f)))**Rational(1, 3)).exp == Rational(1, 3) + r = symbols('r', real=True) + assert sqrt(r**2) == abs(r) + assert cbrt(r**3) != r + assert sqrt(Pow(2*I, 5*S.Half)) != (2*I)**Rational(5, 4) + p = symbols('p', positive=True) + assert cbrt(p**2) == p**Rational(2, 3) + assert NS(((0.2 + 0.7*I)**(0.7 + 1.0*I))**(0.5 - 0.1*I), 1) == '0.4 + 0.2*I' + assert sqrt(1/(1 + I)) == sqrt(1 - I)/sqrt(2) # or 1/sqrt(1 + I) + e = 1/(1 - sqrt(2)) + assert sqrt(e) == I/sqrt(-1 + sqrt(2)) + assert e**Rational(-1, 2) == -I*sqrt(-1 + sqrt(2)) + assert sqrt((cos(1)**2 + sin(1)**2 - 1)**(3 + I)).exp in [S.Half, + Rational(3, 2) + I/2] + assert sqrt(r**Rational(4, 3)) != r**Rational(2, 3) + assert sqrt((p + I)**Rational(4, 3)) == (p + I)**Rational(2, 3) + + for q in 1+I, 1-I: + assert sqrt(q**2) == q + for q in -1+I, -1-I: + assert sqrt(q**2) == -q + + assert sqrt((p + r*I)**2) != p + r*I + e = (1 + I/5) + assert sqrt(e**5) == e**(5*S.Half) + assert sqrt(e**6) == e**3 + assert sqrt((1 + I*r)**6) != (1 + I*r)**3 + + +def test_issue_8582(): + assert 1**oo is nan + assert 1**(-oo) is nan + assert 1**zoo is nan + assert 1**(oo + I) is nan + assert 1**(1 + I*oo) is nan + assert 1**(oo + I*oo) is nan + + +def test_issue_8650(): + n = Symbol('n', integer=True, nonnegative=True) + assert (n**n).is_positive is True + x = 5*n + 5 + assert (x**(5*(n + 1))).is_positive is True + + +def test_issue_13914(): + b = Symbol('b') + assert (-1)**zoo is nan + assert 2**zoo is nan + assert (S.Half)**(1 + zoo) is nan + assert I**(zoo + I) is nan + assert b**(I + zoo) is nan + + +def test_better_sqrt(): + n = Symbol('n', integer=True, nonnegative=True) + assert sqrt(3 + 4*I) == 2 + I + assert sqrt(3 - 4*I) == 2 - I + assert sqrt(-3 - 4*I) == 1 - 2*I + assert sqrt(-3 + 4*I) == 1 + 2*I + assert sqrt(32 + 24*I) == 6 + 2*I + assert sqrt(32 - 24*I) == 6 - 2*I + assert sqrt(-32 - 24*I) == 2 - 6*I + assert sqrt(-32 + 24*I) == 2 + 6*I + + # triple (3, 4, 5): + # parity of 3 matches parity of 5 and + # den, 4, is a square + assert sqrt((3 + 4*I)/4) == 1 + I/2 + # triple (8, 15, 17) + # parity of 8 doesn't match parity of 17 but + # den/2, 8/2, is a square + assert sqrt((8 + 15*I)/8) == (5 + 3*I)/4 + # handle the denominator + assert sqrt((3 - 4*I)/25) == (2 - I)/5 + assert sqrt((3 - 4*I)/26) == (2 - I)/sqrt(26) + # mul + # issue #12739 + assert sqrt((3 + 4*I)/(3 - 4*I)) == (3 + 4*I)/5 + assert sqrt(2/(3 + 4*I)) == sqrt(2)/5*(2 - I) + assert sqrt(n/(3 + 4*I)).subs(n, 2) == sqrt(2)/5*(2 - I) + assert sqrt(-2/(3 + 4*I)) == sqrt(2)/5*(1 + 2*I) + assert sqrt(-n/(3 + 4*I)).subs(n, 2) == sqrt(2)/5*(1 + 2*I) + # power + assert sqrt(1/(3 + I*4)) == (2 - I)/5 + assert sqrt(1/(3 - I)) == sqrt(10)*sqrt(3 + I)/10 + # symbolic + i = symbols('i', imaginary=True) + assert sqrt(3/i) == Mul(sqrt(3), 1/sqrt(i), evaluate=False) + # multiples of 1/2; don't make this too automatic + assert sqrt(3 + 4*I)**3 == (2 + I)**3 + assert Pow(3 + 4*I, Rational(3, 2)) == 2 + 11*I + assert Pow(6 + 8*I, Rational(3, 2)) == 2*sqrt(2)*(2 + 11*I) + n, d = (3 + 4*I), (3 - 4*I)**3 + a = n/d + assert a.args == (1/d, n) + eq = sqrt(a) + assert eq.args == (a, S.Half) + assert expand_multinomial(eq) == sqrt((-117 + 44*I)*(3 + 4*I))/125 + assert eq.expand() == (7 - 24*I)/125 + + # issue 12775 + # pos im part + assert sqrt(2*I) == (1 + I) + assert sqrt(2*9*I) == Mul(3, 1 + I, evaluate=False) + assert Pow(2*I, 3*S.Half) == (1 + I)**3 + # neg im part + assert sqrt(-I/2) == Mul(S.Half, 1 - I, evaluate=False) + # fractional im part + assert Pow(Rational(-9, 2)*I, Rational(3, 2)) == 27*(1 - I)**3/8 + + +def test_issue_2993(): + assert str((2.3*x - 4)**0.3) == '(2.3*x - 4)**0.3' + assert str((2.3*x + 4)**0.3) == '(2.3*x + 4)**0.3' + assert str((-2.3*x + 4)**0.3) == '(4 - 2.3*x)**0.3' + assert str((-2.3*x - 4)**0.3) == '(-2.3*x - 4)**0.3' + assert str((2.3*x - 2)**0.3) == '(2.3*x - 2)**0.3' + assert str((-2.3*x - 2)**0.3) == '(-2.3*x - 2)**0.3' + assert str((-2.3*x + 2)**0.3) == '(2 - 2.3*x)**0.3' + assert str((2.3*x + 2)**0.3) == '(2.3*x + 2)**0.3' + assert str((2.3*x - 4)**Rational(1, 3)) == '(2.3*x - 4)**(1/3)' + eq = (2.3*x + 4) + assert str(eq**2) == '(2.3*x + 4)**2' + assert (1/eq).args == (eq, -1) # don't change trivial power + # issue 17735 + q=.5*exp(x) - .5*exp(-x) + 0.1 + assert int((q**2).subs(x, 1)) == 1 + # issue 17756 + y = Symbol('y') + assert len(sqrt(x/(x + y)**2 + Float('0.008', 30)).subs(y, pi.n(25)).atoms(Float)) == 2 + # issue 17756 + a, b, c, d, e, f, g = symbols('a:g') + expr = sqrt(1 + a*(c**4 + g*d - 2*g*e - f*(-g + d))**2/ + (c**3*b**2*(d - 3*e + 2*f)**2))/2 + r = [ + (a, N('0.0170992456333788667034850458615', 30)), + (b, N('0.0966594956075474769169134801223', 30)), + (c, N('0.390911862903463913632151616184', 30)), + (d, N('0.152812084558656566271750185933', 30)), + (e, N('0.137562344465103337106561623432', 30)), + (f, N('0.174259178881496659302933610355', 30)), + (g, N('0.220745448491223779615401870086', 30))] + tru = expr.n(30, subs=dict(r)) + seq = expr.subs(r) + # although `tru` is the right way to evaluate + # expr with numerical values, `seq` will have + # significant loss of precision if extraction of + # the largest coefficient of a power's base's terms + # is done improperly + assert seq == tru + +def test_issue_17450(): + assert (erf(cosh(1)**7)**I).is_real is None + assert (erf(cosh(1)**7)**I).is_imaginary is False + assert (Pow(exp(1+sqrt(2)), ((1-sqrt(2))*I*pi), evaluate=False)).is_real is None + assert ((-10)**(10*I*pi/3)).is_real is False + assert ((-5)**(4*I*pi)).is_real is False + + +def test_issue_18190(): + assert sqrt(1 / tan(1 + I)) == 1 / sqrt(tan(1 + I)) + + +def test_issue_14815(): + x = Symbol('x', real=True) + assert sqrt(x).is_extended_negative is False + x = Symbol('x', real=False) + assert sqrt(x).is_extended_negative is None + x = Symbol('x', complex=True) + assert sqrt(x).is_extended_negative is False + x = Symbol('x', extended_real=True) + assert sqrt(x).is_extended_negative is False + assert sqrt(zoo, evaluate=False).is_extended_negative is False + assert sqrt(nan, evaluate=False).is_extended_negative is None + + +def test_issue_18509(): + x = Symbol('x', prime=True) + assert x**oo is oo + assert (1/x)**oo is S.Zero + assert (-1/x)**oo is S.Zero + assert (-x)**oo is zoo + assert (-oo)**(-1 + I) is S.Zero + assert (-oo)**(1 + I) is zoo + assert (oo)**(-1 + I) is S.Zero + assert (oo)**(1 + I) is zoo + + +def test_issue_18762(): + e, p = symbols('e p') + g0 = sqrt(1 + e**2 - 2*e*cos(p)) + assert len(g0.series(e, 1, 3).args) == 4 + + +def test_issue_21860(): + e = 3*2**Rational(66666666667,200000000000)*3**Rational(16666666667,50000000000)*x**Rational(66666666667, 200000000000) + ans = Mul(Rational(3, 2), + Pow(Integer(2), Rational(33333333333, 100000000000)), + Pow(Integer(3), Rational(26666666667, 40000000000))) + assert e.xreplace({x: Rational(3,8)}) == ans + + +def test_issue_21647(): + e = log((Integer(567)/500)**(811*(Integer(567)/500)**x/100)) + ans = log(Mul(Rational(64701150190720499096094005280169087619821081527, + 76293945312500000000000000000000000000000000000), + Pow(Integer(2), Rational(396204892125479941, 781250000000000000)), + Pow(Integer(3), Rational(385045107874520059, 390625000000000000)), + Pow(Integer(5), Rational(407364676376439823, 1562500000000000000)), + Pow(Integer(7), Rational(385045107874520059, 1562500000000000000)))) + assert e.xreplace({x: 6}) == ans + + +def test_issue_21762(): + e = (x**2 + 6)**(Integer(33333333333333333)/50000000000000000) + ans = Mul(Rational(5, 4), + Pow(Integer(2), Rational(16666666666666667, 25000000000000000)), + Pow(Integer(5), Rational(8333333333333333, 25000000000000000))) + assert e.xreplace({x: S.Half}) == ans + + +def test_issue_14704(): + a = 144**144 + x, xexact = integer_nthroot(a,a) + assert x == 1 and xexact is False + + +def test_rational_powers_larger_than_one(): + assert Rational(2, 3)**Rational(3, 2) == 2*sqrt(6)/9 + assert Rational(1, 6)**Rational(9, 4) == 6**Rational(3, 4)/216 + assert Rational(3, 7)**Rational(7, 3) == 9*3**Rational(1, 3)*7**Rational(2, 3)/343 + + +def test_power_dispatcher(): + + class NewBase(Expr): + pass + class NewPow(NewBase, Pow): + pass + a, b = Symbol('a'), NewBase() + + @power.register(Expr, NewBase) + @power.register(NewBase, Expr) + @power.register(NewBase, NewBase) + def _(a, b): + return NewPow(a, b) + + # Pow called as fallback + assert power(2, 3) == 8*S.One + assert power(a, 2) == Pow(a, 2) + assert power(a, a) == Pow(a, a) + + # NewPow called by dispatch + assert power(a, b) == NewPow(a, b) + assert power(b, a) == NewPow(b, a) + assert power(b, b) == NewPow(b, b) + + +def test_powers_of_I(): + assert [sqrt(I)**i for i in range(13)] == [ + 1, sqrt(I), I, sqrt(I)**3, -1, -sqrt(I), -I, -sqrt(I)**3, + 1, sqrt(I), I, sqrt(I)**3, -1] + assert sqrt(I)**(S(9)/2) == -I**(S(1)/4) + + +def test_issue_23918(): + b = S(2)/3 + assert (b**x).as_base_exp() == (b, x) + + +def test_issue_26546(): + x = Symbol('x', real=True) + assert x.is_extended_real is True + assert sqrt(x+I).is_extended_real is False + assert Pow(x+I, S.Half).is_extended_real is False + assert Pow(x+I, Rational(1,2)).is_extended_real is False + assert Pow(x+I, Rational(1,13)).is_extended_real is False + assert Pow(x+I, Rational(2,3)).is_extended_real is None + + +def test_issue_25165(): + e1 = (1/sqrt(( - x + 1)**2 + (x - 0.23)**4)).series(x, 0, 2) + e2 = 0.998603724830355 + 1.02004923189934*x + O(x**2) + assert all_close(e1, e2) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_priority.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_priority.py new file mode 100644 index 0000000000000000000000000000000000000000..276e653100f886243e07b866b699b8da53cdaf88 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_priority.py @@ -0,0 +1,145 @@ +from sympy.core.decorators import call_highest_priority +from sympy.core.expr import Expr +from sympy.core.mod import Mod +from sympy.core.numbers import Integer +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.integers import floor + + +class Higher(Integer): + ''' + Integer of value 1 and _op_priority 20 + + Operations handled by this class return 1 and reverse operations return 2 + ''' + + _op_priority = 20.0 + result: Expr = S.One + + def __new__(cls): + obj = Expr.__new__(cls) + obj.p = 1 + return obj + + @call_highest_priority('__rmul__') + def __mul__(self, other): + return self.result + + @call_highest_priority('__mul__') + def __rmul__(self, other): + return 2*self.result + + @call_highest_priority('__radd__') + def __add__(self, other): + return self.result + + @call_highest_priority('__add__') + def __radd__(self, other): + return 2*self.result + + @call_highest_priority('__rsub__') + def __sub__(self, other): + return self.result + + @call_highest_priority('__sub__') + def __rsub__(self, other): + return 2*self.result + + @call_highest_priority('__rpow__') + def __pow__(self, other): + return self.result + + @call_highest_priority('__pow__') + def __rpow__(self, other): + return 2*self.result + + @call_highest_priority('__rtruediv__') + def __truediv__(self, other): + return self.result + + @call_highest_priority('__truediv__') + def __rtruediv__(self, other): + return 2*self.result + + @call_highest_priority('__rmod__') + def __mod__(self, other): + return self.result + + @call_highest_priority('__mod__') + def __rmod__(self, other): + return 2*self.result + + @call_highest_priority('__rfloordiv__') + def __floordiv__(self, other): + return self.result + + @call_highest_priority('__floordiv__') + def __rfloordiv__(self, other): + return 2*self.result + + +class Lower(Higher): + ''' + Integer of value -1 and _op_priority 5 + + Operations handled by this class return -1 and reverse operations return -2 + ''' + + _op_priority = 5.0 + result: Expr = S.NegativeOne + + def __new__(cls): + obj = Expr.__new__(cls) + obj.p = -1 + return obj + + +x = Symbol('x') +h = Higher() +l = Lower() + + +def test_mul(): + assert h*l == h*x == 1 + assert l*h == x*h == 2 + assert x*l == l*x == -x + + +def test_add(): + assert h + l == h + x == 1 + assert l + h == x + h == 2 + assert x + l == l + x == x - 1 + + +def test_sub(): + assert h - l == h - x == 1 + assert l - h == x - h == 2 + assert x - l == -(l - x) == x + 1 + + +def test_pow(): + assert h**l == h**x == 1 + assert l**h == x**h == 2 + assert (x**l).args == (1/x).args and (x**l).is_Pow + assert (l**x).args == ((-1)**x).args and (l**x).is_Pow + + +def test_div(): + assert h/l == h/x == 1 + assert l/h == x/h == 2 + assert x/l == 1/(l/x) == -x + + +def test_mod(): + assert h%l == h%x == 1 + assert l%h == x%h == 2 + assert x%l == Mod(x, -1) + assert l%x == Mod(-1, x) + + +def test_floordiv(): + assert h//l == h//x == 1 + assert l//h == x//h == 2 + assert x//l == floor(-x) + assert l//x == floor(-1/x) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_random.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_random.py new file mode 100644 index 0000000000000000000000000000000000000000..01c677126285eb66349253368b94b3270fb97793 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_random.py @@ -0,0 +1,61 @@ +import random +from sympy.core.random import random as rand, seed, shuffle, _assumptions_shuffle +from sympy.core.symbol import Symbol, symbols +from sympy.functions.elementary.trigonometric import sin, acos +from sympy.abc import x + + +def test_random(): + random.seed(42) + a = random.random() + random.seed(42) + Symbol('z').is_finite + b = random.random() + assert a == b + + got = set() + for i in range(2): + random.seed(28) + m0, m1 = symbols('m_0 m_1', real=True) + _ = acos(-m0/m1) + got.add(random.uniform(0,1)) + assert len(got) == 1 + + random.seed(10) + y = 0 + for i in range(4): + y += sin(random.uniform(-10,10) * x) + random.seed(10) + z = 0 + for i in range(4): + z += sin(random.uniform(-10,10) * x) + assert y == z + + +def test_seed(): + assert rand() < 1 + seed(1) + a = rand() + b = rand() + seed(1) + c = rand() + d = rand() + assert a == c + if not c == d: + assert a != b + else: + assert a == b + + abc = 'abc' + first = list(abc) + second = list(abc) + third = list(abc) + + seed(123) + shuffle(first) + + seed(123) + shuffle(second) + _assumptions_shuffle(third) + + assert first == second == third diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_relational.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_relational.py new file mode 100644 index 0000000000000000000000000000000000000000..60c026fd5f5b8cee2e90e00582047cc7763bb8a4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_relational.py @@ -0,0 +1,1271 @@ +from sympy.core.logic import fuzzy_and +from sympy.core.sympify import _sympify +from sympy.multipledispatch import dispatch +from sympy.testing.pytest import XFAIL, raises +from sympy.assumptions.ask import Q +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.expr import Expr, unchanged +from sympy.core.function import Function +from sympy.core.mul import Mul +from sympy.core.numbers import (Float, I, Rational, nan, oo, pi, zoo) +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import sign, Abs +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.exponential import (exp, exp_polar, log) +from sympy.functions.elementary.integers import (ceiling, floor) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.logic.boolalg import (And, Implies, Not, Or, Xor) +from sympy.sets import Reals +from sympy.simplify.simplify import simplify +from sympy.simplify.trigsimp import trigsimp +from sympy.core.relational import (Relational, Equality, Unequality, + GreaterThan, LessThan, StrictGreaterThan, + StrictLessThan, Rel, Eq, Lt, Le, + Gt, Ge, Ne, is_le, is_gt, is_ge, is_lt, is_eq, is_neq) +from sympy.sets.sets import Interval, FiniteSet + +from itertools import combinations + +x, y, z, t = symbols('x,y,z,t') + + +def rel_check(a, b): + from sympy.testing.pytest import raises + assert a.is_number and b.is_number + for do in range(len({type(a), type(b)})): + if S.NaN in (a, b): + v = [(a == b), (a != b)] + assert len(set(v)) == 1 and v[0] == False + assert not (a != b) and not (a == b) + assert raises(TypeError, lambda: a < b) + assert raises(TypeError, lambda: a <= b) + assert raises(TypeError, lambda: a > b) + assert raises(TypeError, lambda: a >= b) + else: + E = [(a == b), (a != b)] + assert len(set(E)) == 2 + v = [ + (a < b), (a <= b), (a > b), (a >= b)] + i = [ + [True, True, False, False], + [False, True, False, True], # <-- i == 1 + [False, False, True, True]].index(v) + if i == 1: + assert E[0] or (a.is_Float != b.is_Float) # ugh + else: + assert E[1] + a, b = b, a + return True + + +def test_rel_ne(): + assert Relational(x, y, '!=') == Ne(x, y) + + # issue 6116 + p = Symbol('p', positive=True) + assert Ne(p, 0) is S.true + + +def test_rel_subs(): + e = Relational(x, y, '==') + e = e.subs(x, z) + + assert isinstance(e, Equality) + assert e.lhs == z + assert e.rhs == y + + e = Relational(x, y, '>=') + e = e.subs(x, z) + + assert isinstance(e, GreaterThan) + assert e.lhs == z + assert e.rhs == y + + e = Relational(x, y, '<=') + e = e.subs(x, z) + + assert isinstance(e, LessThan) + assert e.lhs == z + assert e.rhs == y + + e = Relational(x, y, '>') + e = e.subs(x, z) + + assert isinstance(e, StrictGreaterThan) + assert e.lhs == z + assert e.rhs == y + + e = Relational(x, y, '<') + e = e.subs(x, z) + + assert isinstance(e, StrictLessThan) + assert e.lhs == z + assert e.rhs == y + + e = Eq(x, 0) + assert e.subs(x, 0) is S.true + assert e.subs(x, 1) is S.false + + +def test_wrappers(): + e = x + x**2 + + res = Relational(y, e, '==') + assert Rel(y, x + x**2, '==') == res + assert Eq(y, x + x**2) == res + + res = Relational(y, e, '<') + assert Lt(y, x + x**2) == res + + res = Relational(y, e, '<=') + assert Le(y, x + x**2) == res + + res = Relational(y, e, '>') + assert Gt(y, x + x**2) == res + + res = Relational(y, e, '>=') + assert Ge(y, x + x**2) == res + + res = Relational(y, e, '!=') + assert Ne(y, x + x**2) == res + + +def test_Eq_Ne(): + + assert Eq(x, x) # issue 5719 + + # issue 6116 + p = Symbol('p', positive=True) + assert Eq(p, 0) is S.false + + # issue 13348; 19048 + # SymPy is strict about 0 and 1 not being + # interpreted as Booleans + assert Eq(True, 1) is S.false + assert Eq(False, 0) is S.false + assert Eq(~x, 0) is S.false + assert Eq(~x, 1) is S.false + assert Ne(True, 1) is S.true + assert Ne(False, 0) is S.true + assert Ne(~x, 0) is S.true + assert Ne(~x, 1) is S.true + + assert Eq((), 1) is S.false + assert Ne((), 1) is S.true + + +def test_as_poly(): + from sympy.polys.polytools import Poly + # Only Eq should have an as_poly method: + assert Eq(x, 1).as_poly() == Poly(x - 1, x, domain='ZZ') + raises(AttributeError, lambda: Ne(x, 1).as_poly()) + raises(AttributeError, lambda: Ge(x, 1).as_poly()) + raises(AttributeError, lambda: Gt(x, 1).as_poly()) + raises(AttributeError, lambda: Le(x, 1).as_poly()) + raises(AttributeError, lambda: Lt(x, 1).as_poly()) + + +def test_rel_Infinity(): + # NOTE: All of these are actually handled by sympy.core.Number, and do + # not create Relational objects. + assert (oo > oo) is S.false + assert (oo > -oo) is S.true + assert (oo > 1) is S.true + assert (oo < oo) is S.false + assert (oo < -oo) is S.false + assert (oo < 1) is S.false + assert (oo >= oo) is S.true + assert (oo >= -oo) is S.true + assert (oo >= 1) is S.true + assert (oo <= oo) is S.true + assert (oo <= -oo) is S.false + assert (oo <= 1) is S.false + assert (-oo > oo) is S.false + assert (-oo > -oo) is S.false + assert (-oo > 1) is S.false + assert (-oo < oo) is S.true + assert (-oo < -oo) is S.false + assert (-oo < 1) is S.true + assert (-oo >= oo) is S.false + assert (-oo >= -oo) is S.true + assert (-oo >= 1) is S.false + assert (-oo <= oo) is S.true + assert (-oo <= -oo) is S.true + assert (-oo <= 1) is S.true + + +def test_infinite_symbol_inequalities(): + x = Symbol('x', extended_positive=True, infinite=True) + y = Symbol('y', extended_positive=True, infinite=True) + z = Symbol('z', extended_negative=True, infinite=True) + w = Symbol('w', extended_negative=True, infinite=True) + + inf_set = (x, y, oo) + ninf_set = (z, w, -oo) + + for inf1 in inf_set: + assert (inf1 < 1) is S.false + assert (inf1 > 1) is S.true + assert (inf1 <= 1) is S.false + assert (inf1 >= 1) is S.true + + for inf2 in inf_set: + assert (inf1 < inf2) is S.false + assert (inf1 > inf2) is S.false + assert (inf1 <= inf2) is S.true + assert (inf1 >= inf2) is S.true + + for ninf1 in ninf_set: + assert (inf1 < ninf1) is S.false + assert (inf1 > ninf1) is S.true + assert (inf1 <= ninf1) is S.false + assert (inf1 >= ninf1) is S.true + assert (ninf1 < inf1) is S.true + assert (ninf1 > inf1) is S.false + assert (ninf1 <= inf1) is S.true + assert (ninf1 >= inf1) is S.false + + for ninf1 in ninf_set: + assert (ninf1 < 1) is S.true + assert (ninf1 > 1) is S.false + assert (ninf1 <= 1) is S.true + assert (ninf1 >= 1) is S.false + + for ninf2 in ninf_set: + assert (ninf1 < ninf2) is S.false + assert (ninf1 > ninf2) is S.false + assert (ninf1 <= ninf2) is S.true + assert (ninf1 >= ninf2) is S.true + + +def test_bool(): + assert Eq(0, 0) is S.true + assert Eq(1, 0) is S.false + assert Ne(0, 0) is S.false + assert Ne(1, 0) is S.true + assert Lt(0, 1) is S.true + assert Lt(1, 0) is S.false + assert Le(0, 1) is S.true + assert Le(1, 0) is S.false + assert Le(0, 0) is S.true + assert Gt(1, 0) is S.true + assert Gt(0, 1) is S.false + assert Ge(1, 0) is S.true + assert Ge(0, 1) is S.false + assert Ge(1, 1) is S.true + assert Eq(I, 2) is S.false + assert Ne(I, 2) is S.true + raises(TypeError, lambda: Gt(I, 2)) + raises(TypeError, lambda: Ge(I, 2)) + raises(TypeError, lambda: Lt(I, 2)) + raises(TypeError, lambda: Le(I, 2)) + a = Float('.000000000000000000001', '') + b = Float('.0000000000000000000001', '') + assert Eq(pi + a, pi + b) is S.false + + +def test_rich_cmp(): + assert (x < y) == Lt(x, y) + assert (x <= y) == Le(x, y) + assert (x > y) == Gt(x, y) + assert (x >= y) == Ge(x, y) + + +def test_doit(): + from sympy.core.symbol import Symbol + p = Symbol('p', positive=True) + n = Symbol('n', negative=True) + np = Symbol('np', nonpositive=True) + nn = Symbol('nn', nonnegative=True) + + assert Gt(p, 0).doit() is S.true + assert Gt(p, 1).doit() == Gt(p, 1) + assert Ge(p, 0).doit() is S.true + assert Le(p, 0).doit() is S.false + assert Lt(n, 0).doit() is S.true + assert Le(np, 0).doit() is S.true + assert Gt(nn, 0).doit() == Gt(nn, 0) + assert Lt(nn, 0).doit() is S.false + + assert Eq(x, 0).doit() == Eq(x, 0) + + +def test_new_relational(): + x = Symbol('x') + + assert Eq(x, 0) == Relational(x, 0) # None ==> Equality + assert Eq(x, 0) == Relational(x, 0, '==') + assert Eq(x, 0) == Relational(x, 0, 'eq') + assert Eq(x, 0) == Equality(x, 0) + + assert Eq(x, 0) != Relational(x, 1) # None ==> Equality + assert Eq(x, 0) != Relational(x, 1, '==') + assert Eq(x, 0) != Relational(x, 1, 'eq') + assert Eq(x, 0) != Equality(x, 1) + + assert Eq(x, -1) == Relational(x, -1) # None ==> Equality + assert Eq(x, -1) == Relational(x, -1, '==') + assert Eq(x, -1) == Relational(x, -1, 'eq') + assert Eq(x, -1) == Equality(x, -1) + assert Eq(x, -1) != Relational(x, 1) # None ==> Equality + assert Eq(x, -1) != Relational(x, 1, '==') + assert Eq(x, -1) != Relational(x, 1, 'eq') + assert Eq(x, -1) != Equality(x, 1) + + assert Ne(x, 0) == Relational(x, 0, '!=') + assert Ne(x, 0) == Relational(x, 0, '<>') + assert Ne(x, 0) == Relational(x, 0, 'ne') + assert Ne(x, 0) == Unequality(x, 0) + assert Ne(x, 0) != Relational(x, 1, '!=') + assert Ne(x, 0) != Relational(x, 1, '<>') + assert Ne(x, 0) != Relational(x, 1, 'ne') + assert Ne(x, 0) != Unequality(x, 1) + + assert Ge(x, 0) == Relational(x, 0, '>=') + assert Ge(x, 0) == Relational(x, 0, 'ge') + assert Ge(x, 0) == GreaterThan(x, 0) + assert Ge(x, 1) != Relational(x, 0, '>=') + assert Ge(x, 1) != Relational(x, 0, 'ge') + assert Ge(x, 1) != GreaterThan(x, 0) + assert (x >= 1) == Relational(x, 1, '>=') + assert (x >= 1) == Relational(x, 1, 'ge') + assert (x >= 1) == GreaterThan(x, 1) + assert (x >= 0) != Relational(x, 1, '>=') + assert (x >= 0) != Relational(x, 1, 'ge') + assert (x >= 0) != GreaterThan(x, 1) + + assert Le(x, 0) == Relational(x, 0, '<=') + assert Le(x, 0) == Relational(x, 0, 'le') + assert Le(x, 0) == LessThan(x, 0) + assert Le(x, 1) != Relational(x, 0, '<=') + assert Le(x, 1) != Relational(x, 0, 'le') + assert Le(x, 1) != LessThan(x, 0) + assert (x <= 1) == Relational(x, 1, '<=') + assert (x <= 1) == Relational(x, 1, 'le') + assert (x <= 1) == LessThan(x, 1) + assert (x <= 0) != Relational(x, 1, '<=') + assert (x <= 0) != Relational(x, 1, 'le') + assert (x <= 0) != LessThan(x, 1) + + assert Gt(x, 0) == Relational(x, 0, '>') + assert Gt(x, 0) == Relational(x, 0, 'gt') + assert Gt(x, 0) == StrictGreaterThan(x, 0) + assert Gt(x, 1) != Relational(x, 0, '>') + assert Gt(x, 1) != Relational(x, 0, 'gt') + assert Gt(x, 1) != StrictGreaterThan(x, 0) + assert (x > 1) == Relational(x, 1, '>') + assert (x > 1) == Relational(x, 1, 'gt') + assert (x > 1) == StrictGreaterThan(x, 1) + assert (x > 0) != Relational(x, 1, '>') + assert (x > 0) != Relational(x, 1, 'gt') + assert (x > 0) != StrictGreaterThan(x, 1) + + assert Lt(x, 0) == Relational(x, 0, '<') + assert Lt(x, 0) == Relational(x, 0, 'lt') + assert Lt(x, 0) == StrictLessThan(x, 0) + assert Lt(x, 1) != Relational(x, 0, '<') + assert Lt(x, 1) != Relational(x, 0, 'lt') + assert Lt(x, 1) != StrictLessThan(x, 0) + assert (x < 1) == Relational(x, 1, '<') + assert (x < 1) == Relational(x, 1, 'lt') + assert (x < 1) == StrictLessThan(x, 1) + assert (x < 0) != Relational(x, 1, '<') + assert (x < 0) != Relational(x, 1, 'lt') + assert (x < 0) != StrictLessThan(x, 1) + + # finally, some fuzz testing + from sympy.core.random import randint + for i in range(100): + while 1: + strtype, length = (chr, 65535) if randint(0, 1) else (chr, 255) + relation_type = strtype(randint(0, length)) + if randint(0, 1): + relation_type += strtype(randint(0, length)) + if relation_type not in ('==', 'eq', '!=', '<>', 'ne', '>=', 'ge', + '<=', 'le', '>', 'gt', '<', 'lt', ':=', + '+=', '-=', '*=', '/=', '%='): + break + + raises(ValueError, lambda: Relational(x, 1, relation_type)) + assert all(Relational(x, 0, op).rel_op == '==' for op in ('eq', '==')) + assert all(Relational(x, 0, op).rel_op == '!=' + for op in ('ne', '<>', '!=')) + assert all(Relational(x, 0, op).rel_op == '>' for op in ('gt', '>')) + assert all(Relational(x, 0, op).rel_op == '<' for op in ('lt', '<')) + assert all(Relational(x, 0, op).rel_op == '>=' for op in ('ge', '>=')) + assert all(Relational(x, 0, op).rel_op == '<=' for op in ('le', '<=')) + + +def test_relational_arithmetic(): + for cls in [Eq, Ne, Le, Lt, Ge, Gt]: + rel = cls(x, y) + raises(TypeError, lambda: 0+rel) + raises(TypeError, lambda: 1*rel) + raises(TypeError, lambda: 1**rel) + raises(TypeError, lambda: rel**1) + raises(TypeError, lambda: Add(0, rel)) + raises(TypeError, lambda: Mul(1, rel)) + raises(TypeError, lambda: Pow(1, rel)) + raises(TypeError, lambda: Pow(rel, 1)) + + +def test_relational_bool_output(): + # https://github.com/sympy/sympy/issues/5931 + raises(TypeError, lambda: bool(x > 3)) + raises(TypeError, lambda: bool(x >= 3)) + raises(TypeError, lambda: bool(x < 3)) + raises(TypeError, lambda: bool(x <= 3)) + raises(TypeError, lambda: bool(Eq(x, 3))) + raises(TypeError, lambda: bool(Ne(x, 3))) + + +def test_relational_logic_symbols(): + # See issue 6204 + assert (x < y) & (z < t) == And(x < y, z < t) + assert (x < y) | (z < t) == Or(x < y, z < t) + assert ~(x < y) == Not(x < y) + assert (x < y) >> (z < t) == Implies(x < y, z < t) + assert (x < y) << (z < t) == Implies(z < t, x < y) + assert (x < y) ^ (z < t) == Xor(x < y, z < t) + + assert isinstance((x < y) & (z < t), And) + assert isinstance((x < y) | (z < t), Or) + assert isinstance(~(x < y), GreaterThan) + assert isinstance((x < y) >> (z < t), Implies) + assert isinstance((x < y) << (z < t), Implies) + assert isinstance((x < y) ^ (z < t), (Or, Xor)) + + +def test_univariate_relational_as_set(): + assert (x > 0).as_set() == Interval(0, oo, True, True) + assert (x >= 0).as_set() == Interval(0, oo) + assert (x < 0).as_set() == Interval(-oo, 0, True, True) + assert (x <= 0).as_set() == Interval(-oo, 0) + assert Eq(x, 0).as_set() == FiniteSet(0) + assert Ne(x, 0).as_set() == Interval(-oo, 0, True, True) + \ + Interval(0, oo, True, True) + + assert (x**2 >= 4).as_set() == Interval(-oo, -2) + Interval(2, oo) + + +@XFAIL +def test_multivariate_relational_as_set(): + assert (x*y >= 0).as_set() == Interval(0, oo)*Interval(0, oo) + \ + Interval(-oo, 0)*Interval(-oo, 0) + + +def test_Not(): + assert Not(Equality(x, y)) == Unequality(x, y) + assert Not(Unequality(x, y)) == Equality(x, y) + assert Not(StrictGreaterThan(x, y)) == LessThan(x, y) + assert Not(StrictLessThan(x, y)) == GreaterThan(x, y) + assert Not(GreaterThan(x, y)) == StrictLessThan(x, y) + assert Not(LessThan(x, y)) == StrictGreaterThan(x, y) + + +def test_evaluate(): + assert str(Eq(x, x, evaluate=False)) == 'Eq(x, x)' + assert Eq(x, x, evaluate=False).doit() == S.true + assert str(Ne(x, x, evaluate=False)) == 'Ne(x, x)' + assert Ne(x, x, evaluate=False).doit() == S.false + + assert str(Ge(x, x, evaluate=False)) == 'x >= x' + assert str(Le(x, x, evaluate=False)) == 'x <= x' + assert str(Gt(x, x, evaluate=False)) == 'x > x' + assert str(Lt(x, x, evaluate=False)) == 'x < x' + + +def assert_all_ineq_raise_TypeError(a, b): + raises(TypeError, lambda: a > b) + raises(TypeError, lambda: a >= b) + raises(TypeError, lambda: a < b) + raises(TypeError, lambda: a <= b) + raises(TypeError, lambda: b > a) + raises(TypeError, lambda: b >= a) + raises(TypeError, lambda: b < a) + raises(TypeError, lambda: b <= a) + + +def assert_all_ineq_give_class_Inequality(a, b): + """All inequality operations on `a` and `b` result in class Inequality.""" + from sympy.core.relational import _Inequality as Inequality + assert isinstance(a > b, Inequality) + assert isinstance(a >= b, Inequality) + assert isinstance(a < b, Inequality) + assert isinstance(a <= b, Inequality) + assert isinstance(b > a, Inequality) + assert isinstance(b >= a, Inequality) + assert isinstance(b < a, Inequality) + assert isinstance(b <= a, Inequality) + + +def test_imaginary_compare_raises_TypeError(): + # See issue #5724 + assert_all_ineq_raise_TypeError(I, x) + + +def test_complex_compare_not_real(): + # two cases which are not real + y = Symbol('y', imaginary=True) + z = Symbol('z', complex=True, extended_real=False) + for w in (y, z): + assert_all_ineq_raise_TypeError(2, w) + # some cases which should remain un-evaluated + t = Symbol('t') + x = Symbol('x', real=True) + z = Symbol('z', complex=True) + for w in (x, z, t): + assert_all_ineq_give_class_Inequality(2, w) + + +def test_imaginary_and_inf_compare_raises_TypeError(): + # See pull request #7835 + y = Symbol('y', imaginary=True) + assert_all_ineq_raise_TypeError(oo, y) + assert_all_ineq_raise_TypeError(-oo, y) + + +def test_complex_pure_imag_not_ordered(): + raises(TypeError, lambda: 2*I < 3*I) + + # more generally + x = Symbol('x', real=True, nonzero=True) + y = Symbol('y', imaginary=True) + z = Symbol('z', complex=True) + assert_all_ineq_raise_TypeError(I, y) + + t = I*x # an imaginary number, should raise errors + assert_all_ineq_raise_TypeError(2, t) + + t = -I*y # a real number, so no errors + assert_all_ineq_give_class_Inequality(2, t) + + t = I*z # unknown, should be unevaluated + assert_all_ineq_give_class_Inequality(2, t) + + +def test_x_minus_y_not_same_as_x_lt_y(): + """ + A consequence of pull request #7792 is that `x - y < 0` and `x < y` + are not synonymous. + """ + x = I + 2 + y = I + 3 + raises(TypeError, lambda: x < y) + assert x - y < 0 + + ineq = Lt(x, y, evaluate=False) + raises(TypeError, lambda: ineq.doit()) + assert ineq.lhs - ineq.rhs < 0 + + t = Symbol('t', imaginary=True) + x = 2 + t + y = 3 + t + ineq = Lt(x, y, evaluate=False) + raises(TypeError, lambda: ineq.doit()) + assert ineq.lhs - ineq.rhs < 0 + + # this one should give error either way + x = I + 2 + y = 2*I + 3 + raises(TypeError, lambda: x < y) + raises(TypeError, lambda: x - y < 0) + + +def test_nan_equality_exceptions(): + # See issue #7774 + import random + assert Equality(nan, nan) is S.false + assert Unequality(nan, nan) is S.true + + # See issue #7773 + A = (x, S.Zero, S.One/3, pi, oo, -oo) + assert Equality(nan, random.choice(A)) is S.false + assert Equality(random.choice(A), nan) is S.false + assert Unequality(nan, random.choice(A)) is S.true + assert Unequality(random.choice(A), nan) is S.true + + +def test_nan_inequality_raise_errors(): + # See discussion in pull request #7776. We test inequalities with + # a set including examples of various classes. + for q in (x, S.Zero, S(10), S.One/3, pi, S(1.3), oo, -oo, nan): + assert_all_ineq_raise_TypeError(q, nan) + + +def test_nan_complex_inequalities(): + # Comparisons of NaN with non-real raise errors, we're not too + # fussy whether its the NaN error or complex error. + for r in (I, zoo, Symbol('z', imaginary=True)): + assert_all_ineq_raise_TypeError(r, nan) + + +def test_complex_infinity_inequalities(): + raises(TypeError, lambda: zoo > 0) + raises(TypeError, lambda: zoo >= 0) + raises(TypeError, lambda: zoo < 0) + raises(TypeError, lambda: zoo <= 0) + + +def test_inequalities_symbol_name_same(): + """Using the operator and functional forms should give same results.""" + # We test all combinations from a set + # FIXME: could replace with random selection after test passes + A = (x, y, S.Zero, S.One/3, pi, oo, -oo) + for a in A: + for b in A: + assert Gt(a, b) == (a > b) + assert Lt(a, b) == (a < b) + assert Ge(a, b) == (a >= b) + assert Le(a, b) == (a <= b) + + for b in (y, S.Zero, S.One/3, pi, oo, -oo): + assert Gt(x, b, evaluate=False) == (x > b) + assert Lt(x, b, evaluate=False) == (x < b) + assert Ge(x, b, evaluate=False) == (x >= b) + assert Le(x, b, evaluate=False) == (x <= b) + + for b in (y, S.Zero, S.One/3, pi, oo, -oo): + assert Gt(b, x, evaluate=False) == (b > x) + assert Lt(b, x, evaluate=False) == (b < x) + assert Ge(b, x, evaluate=False) == (b >= x) + assert Le(b, x, evaluate=False) == (b <= x) + + +def test_inequalities_symbol_name_same_complex(): + """Using the operator and functional forms should give same results. + With complex non-real numbers, both should raise errors. + """ + # FIXME: could replace with random selection after test passes + for a in (x, S.Zero, S.One/3, pi, oo, Rational(1, 3)): + raises(TypeError, lambda: Gt(a, I)) + raises(TypeError, lambda: a > I) + raises(TypeError, lambda: Lt(a, I)) + raises(TypeError, lambda: a < I) + raises(TypeError, lambda: Ge(a, I)) + raises(TypeError, lambda: a >= I) + raises(TypeError, lambda: Le(a, I)) + raises(TypeError, lambda: a <= I) + + +def test_inequalities_cant_sympify_other(): + # see issue 7833 + from operator import gt, lt, ge, le + + bar = "foo" + + for a in (x, S.Zero, S.One/3, pi, I, zoo, oo, -oo, nan, Rational(1, 3)): + for op in (lt, gt, le, ge): + raises(TypeError, lambda: op(a, bar)) + + +def test_ineq_avoid_wild_symbol_flip(): + # see issue #7951, we try to avoid this internally, e.g., by using + # __lt__ instead of "<". + from sympy.core.symbol import Wild + p = symbols('p', cls=Wild) + # x > p might flip, but Gt should not: + assert Gt(x, p) == Gt(x, p, evaluate=False) + # Previously failed as 'p > x': + e = Lt(x, y).subs({y: p}) + assert e == Lt(x, p, evaluate=False) + # Previously failed as 'p <= x': + e = Ge(x, p).doit() + assert e == Ge(x, p, evaluate=False) + + +def test_issue_8245(): + a = S("6506833320952669167898688709329/5070602400912917605986812821504") + assert rel_check(a, a.n(10)) + assert rel_check(a, a.n(20)) + assert rel_check(a, a.n()) + # prec of 31 is enough to fully capture a as mpf + assert Float(a, 31) == Float(str(a.p), '')/Float(str(a.q), '') + for i in range(31): + r = Rational(Float(a, i)) + f = Float(r) + assert (f < a) == (Rational(f) < a) + # test sign handling + assert (-f < -a) == (Rational(-f) < -a) + # test equivalence handling + isa = Float(a.p,'')/Float(a.q,'') + assert isa <= a + assert not isa < a + assert isa >= a + assert not isa > a + assert isa > 0 + + a = sqrt(2) + r = Rational(str(a.n(30))) + assert rel_check(a, r) + + a = sqrt(2) + r = Rational(str(a.n(29))) + assert rel_check(a, r) + + assert Eq(log(cos(2)**2 + sin(2)**2), 0) is S.true + + +def test_issue_8449(): + p = Symbol('p', nonnegative=True) + assert Lt(-oo, p) + assert Ge(-oo, p) is S.false + assert Gt(oo, -p) + assert Le(oo, -p) is S.false + + +def test_simplify_relational(): + assert simplify(x*(y + 1) - x*y - x + 1 < x) == (x > 1) + assert simplify(x*(y + 1) - x*y - x - 1 < x) == (x > -1) + assert simplify(x < x*(y + 1) - x*y - x + 1) == (x < 1) + q, r = symbols("q r") + assert (((-q + r) - (q - r)) <= 0).simplify() == (q >= r) + root2 = sqrt(2) + equation = ((root2 * (-q + r) - root2 * (q - r)) <= 0).simplify() + assert equation == (q >= r) + r = S.One < x + # canonical operations are not the same as simplification, + # so if there is no simplification, canonicalization will + # be done unless the measure forbids it + assert simplify(r) == r.canonical + assert simplify(r, ratio=0) != r.canonical + # this is not a random test; in _eval_simplify + # this will simplify to S.false and that is the + # reason for the 'if r.is_Relational' in Relational's + # _eval_simplify routine + assert simplify(-(2**(pi*Rational(3, 2)) + 6**pi)**(1/pi) + + 2*(2**(pi/2) + 3**pi)**(1/pi) < 0) is S.false + # canonical at least + assert Eq(y, x).simplify() == Eq(x, y) + assert Eq(x - 1, 0).simplify() == Eq(x, 1) + assert Eq(x - 1, x).simplify() == S.false + assert Eq(2*x - 1, x).simplify() == Eq(x, 1) + assert Eq(2*x, 4).simplify() == Eq(x, 2) + z = cos(1)**2 + sin(1)**2 - 1 # z.is_zero is None + assert Eq(z*x, 0).simplify() == S.true + + assert Ne(y, x).simplify() == Ne(x, y) + assert Ne(x - 1, 0).simplify() == Ne(x, 1) + assert Ne(x - 1, x).simplify() == S.true + assert Ne(2*x - 1, x).simplify() == Ne(x, 1) + assert Ne(2*x, 4).simplify() == Ne(x, 2) + assert Ne(z*x, 0).simplify() == S.false + + # No real-valued assumptions + assert Ge(y, x).simplify() == Le(x, y) + assert Ge(x - 1, 0).simplify() == Ge(x, 1) + assert Ge(x - 1, x).simplify() == S.false + assert Ge(2*x - 1, x).simplify() == Ge(x, 1) + assert Ge(2*x, 4).simplify() == Ge(x, 2) + assert Ge(z*x, 0).simplify() == S.true + assert Ge(x, -2).simplify() == Ge(x, -2) + assert Ge(-x, -2).simplify() == Le(x, 2) + assert Ge(x, 2).simplify() == Ge(x, 2) + assert Ge(-x, 2).simplify() == Le(x, -2) + + assert Le(y, x).simplify() == Ge(x, y) + assert Le(x - 1, 0).simplify() == Le(x, 1) + assert Le(x - 1, x).simplify() == S.true + assert Le(2*x - 1, x).simplify() == Le(x, 1) + assert Le(2*x, 4).simplify() == Le(x, 2) + assert Le(z*x, 0).simplify() == S.true + assert Le(x, -2).simplify() == Le(x, -2) + assert Le(-x, -2).simplify() == Ge(x, 2) + assert Le(x, 2).simplify() == Le(x, 2) + assert Le(-x, 2).simplify() == Ge(x, -2) + + assert Gt(y, x).simplify() == Lt(x, y) + assert Gt(x - 1, 0).simplify() == Gt(x, 1) + assert Gt(x - 1, x).simplify() == S.false + assert Gt(2*x - 1, x).simplify() == Gt(x, 1) + assert Gt(2*x, 4).simplify() == Gt(x, 2) + assert Gt(z*x, 0).simplify() == S.false + assert Gt(x, -2).simplify() == Gt(x, -2) + assert Gt(-x, -2).simplify() == Lt(x, 2) + assert Gt(x, 2).simplify() == Gt(x, 2) + assert Gt(-x, 2).simplify() == Lt(x, -2) + + assert Lt(y, x).simplify() == Gt(x, y) + assert Lt(x - 1, 0).simplify() == Lt(x, 1) + assert Lt(x - 1, x).simplify() == S.true + assert Lt(2*x - 1, x).simplify() == Lt(x, 1) + assert Lt(2*x, 4).simplify() == Lt(x, 2) + assert Lt(z*x, 0).simplify() == S.false + assert Lt(x, -2).simplify() == Lt(x, -2) + assert Lt(-x, -2).simplify() == Gt(x, 2) + assert Lt(x, 2).simplify() == Lt(x, 2) + assert Lt(-x, 2).simplify() == Gt(x, -2) + + # Test particular branches of _eval_simplify + m = exp(1) - exp_polar(1) + assert simplify(m*x > 1) is S.false + # These two test the same branch + assert simplify(m*x + 2*m*y > 1) is S.false + assert simplify(m*x + y > 1 + y) is S.false + + +def test_equals(): + w, x, y, z = symbols('w:z') + f = Function('f') + assert Eq(x, 1).equals(Eq(x*(y + 1) - x*y - x + 1, x)) + assert Eq(x, y).equals(x < y, True) == False + assert Eq(x, f(1)).equals(Eq(x, f(2)), True) == f(1) - f(2) + assert Eq(f(1), y).equals(Eq(f(2), y), True) == f(1) - f(2) + assert Eq(x, f(1)).equals(Eq(f(2), x), True) == f(1) - f(2) + assert Eq(f(1), x).equals(Eq(x, f(2)), True) == f(1) - f(2) + assert Eq(w, x).equals(Eq(y, z), True) == False + assert Eq(f(1), f(2)).equals(Eq(f(3), f(4)), True) == f(1) - f(3) + assert (x < y).equals(y > x, True) == True + assert (x < y).equals(y >= x, True) == False + assert (x < y).equals(z < y, True) == False + assert (x < y).equals(x < z, True) == False + assert (x < f(1)).equals(x < f(2), True) == f(1) - f(2) + assert (f(1) < x).equals(f(2) < x, True) == f(1) - f(2) + + +def test_reversed(): + assert (x < y).reversed == (y > x) + assert (x <= y).reversed == (y >= x) + assert Eq(x, y, evaluate=False).reversed == Eq(y, x, evaluate=False) + assert Ne(x, y, evaluate=False).reversed == Ne(y, x, evaluate=False) + assert (x >= y).reversed == (y <= x) + assert (x > y).reversed == (y < x) + + +def test_canonical(): + c = [i.canonical for i in ( + x + y < z, + x + 2 > 3, + x < 2, + S(2) > x, + x**2 > -x/y, + Gt(3, 2, evaluate=False) + )] + assert [i.canonical for i in c] == c + assert [i.reversed.canonical for i in c] == c + assert not any(i.lhs.is_Number and not i.rhs.is_Number for i in c) + + c = [i.reversed.func(i.rhs, i.lhs, evaluate=False).canonical for i in c] + assert [i.canonical for i in c] == c + assert [i.reversed.canonical for i in c] == c + assert not any(i.lhs.is_Number and not i.rhs.is_Number for i in c) + assert Eq(y < x, x > y).canonical is S.true + + +@XFAIL +def test_issue_8444_nonworkingtests(): + x = symbols('x', real=True) + assert (x <= oo) == (x >= -oo) == True + + x = symbols('x') + assert x >= floor(x) + assert (x < floor(x)) == False + assert x <= ceiling(x) + assert (x > ceiling(x)) == False + + +def test_issue_8444_workingtests(): + x = symbols('x') + assert Gt(x, floor(x)) == Gt(x, floor(x), evaluate=False) + assert Ge(x, floor(x)) == Ge(x, floor(x), evaluate=False) + assert Lt(x, ceiling(x)) == Lt(x, ceiling(x), evaluate=False) + assert Le(x, ceiling(x)) == Le(x, ceiling(x), evaluate=False) + i = symbols('i', integer=True) + assert (i > floor(i)) == False + assert (i < ceiling(i)) == False + + +def test_issue_10304(): + d = cos(1)**2 + sin(1)**2 - 1 + assert d.is_comparable is False # if this fails, find a new d + e = 1 + d*I + assert simplify(Eq(e, 0)) is S.false + + +def test_issue_18412(): + d = (Rational(1, 6) + z / 4 / y) + assert Eq(x, pi * y**3 * d).replace(y**3, z) == Eq(x, pi * z * d) + + +def test_issue_10401(): + x = symbols('x') + fin = symbols('inf', finite=True) + inf = symbols('inf', infinite=True) + inf2 = symbols('inf2', infinite=True) + infx = symbols('infx', infinite=True, extended_real=True) + # Used in the commented tests below: + #infx2 = symbols('infx2', infinite=True, extended_real=True) + infnx = symbols('inf~x', infinite=True, extended_real=False) + infnx2 = symbols('inf~x2', infinite=True, extended_real=False) + infp = symbols('infp', infinite=True, extended_positive=True) + infp1 = symbols('infp1', infinite=True, extended_positive=True) + infn = symbols('infn', infinite=True, extended_negative=True) + zero = symbols('z', zero=True) + nonzero = symbols('nz', zero=False, finite=True) + + assert Eq(1/(1/x + 1), 1).func is Eq + assert Eq(1/(1/x + 1), 1).subs(x, S.ComplexInfinity) is S.true + assert Eq(1/(1/fin + 1), 1) is S.false + + T, F = S.true, S.false + assert Eq(fin, inf) is F + assert Eq(inf, inf2) not in (T, F) and inf != inf2 + assert Eq(1 + inf, 2 + inf2) not in (T, F) and inf != inf2 + assert Eq(infp, infp1) is T + assert Eq(infp, infn) is F + assert Eq(1 + I*oo, I*oo) is F + assert Eq(I*oo, 1 + I*oo) is F + assert Eq(1 + I*oo, 2 + I*oo) is F + assert Eq(1 + I*oo, 2 + I*infx) is F + assert Eq(1 + I*oo, 2 + infx) is F + # FIXME: The test below fails because (-infx).is_extended_positive is True + # (should be None) + #assert Eq(1 + I*infx, 1 + I*infx2) not in (T, F) and infx != infx2 + # + assert Eq(zoo, sqrt(2) + I*oo) is F + assert Eq(zoo, oo) is F + r = Symbol('r', real=True) + i = Symbol('i', imaginary=True) + assert Eq(i*I, r) not in (T, F) + assert Eq(infx, infnx) is F + assert Eq(infnx, infnx2) not in (T, F) and infnx != infnx2 + assert Eq(zoo, oo) is F + assert Eq(inf/inf2, 0) is F + assert Eq(inf/fin, 0) is F + assert Eq(fin/inf, 0) is T + assert Eq(zero/nonzero, 0) is T and ((zero/nonzero) != 0) + # The commented out test below is incorrect because: + assert zoo == -zoo + assert Eq(zoo, -zoo) is T + assert Eq(oo, -oo) is F + assert Eq(inf, -inf) not in (T, F) + + assert Eq(fin/(fin + 1), 1) is S.false + + o = symbols('o', odd=True) + assert Eq(o, 2*o) is S.false + + p = symbols('p', positive=True) + assert Eq(p/(p - 1), 1) is F + + +def test_issue_10633(): + assert Eq(True, False) == False + assert Eq(False, True) == False + assert Eq(True, True) == True + assert Eq(False, False) == True + + +def test_issue_10927(): + x = symbols('x') + assert str(Eq(x, oo)) == 'Eq(x, oo)' + assert str(Eq(x, -oo)) == 'Eq(x, -oo)' + + +def test_issues_13081_12583_12534(): + # 13081 + r = Rational('905502432259640373/288230376151711744') + assert (r < pi) is S.false + assert (r > pi) is S.true + # 12583 + v = sqrt(2) + u = sqrt(v) + 2/sqrt(10 - 8/sqrt(2 - v) + 4*v*(1/sqrt(2 - v) - 1)) + assert (u >= 0) is S.true + # 12534; Rational vs NumberSymbol + # here are some precisions for which Rational forms + # at a lower and higher precision bracket the value of pi + # e.g. for p = 20: + # Rational(pi.n(p + 1)).n(25) = 3.14159265358979323846 2834 + # pi.n(25) = 3.14159265358979323846 2643 + # Rational(pi.n(p )).n(25) = 3.14159265358979323846 1987 + assert [p for p in range(20, 50) if + (Rational(pi.n(p)) < pi) and + (pi < Rational(pi.n(p + 1)))] == [20, 24, 27, 33, 37, 43, 48] + # pick one such precision and affirm that the reversed operation + # gives the opposite result, i.e. if x < y is true then x > y + # must be false + for i in (20, 21): + v = pi.n(i) + assert rel_check(Rational(v), pi) + assert rel_check(v, pi) + assert rel_check(pi.n(20), pi.n(21)) + # Float vs Rational + # the rational form is less than the floating representation + # at the same precision + assert [i for i in range(15, 50) if Rational(pi.n(i)) > pi.n(i)] == [] + # this should be the same if we reverse the relational + assert [i for i in range(15, 50) if pi.n(i) < Rational(pi.n(i))] == [] + +def test_issue_18188(): + from sympy.sets.conditionset import ConditionSet + result1 = Eq(x*cos(x) - 3*sin(x), 0) + assert result1.as_set() == ConditionSet(x, Eq(x*cos(x) - 3*sin(x), 0), Reals) + + result2 = Eq(x**2 + sqrt(x*2) + sin(x), 0) + assert result2.as_set() == ConditionSet(x, Eq(sqrt(2)*sqrt(x) + x**2 + sin(x), 0), Reals) + +def test_binary_symbols(): + ans = {x} + for f in Eq, Ne: + for t in S.true, S.false: + eq = f(x, S.true) + assert eq.binary_symbols == ans + assert eq.reversed.binary_symbols == ans + assert f(x, 1).binary_symbols == set() + + +def test_rel_args(): + # can't have Boolean args; this is automatic for True/False + # with Python 3 and we confirm that SymPy does the same + # for true/false + for op in ['<', '<=', '>', '>=']: + for b in (S.true, x < 1, And(x, y)): + for v in (0.1, 1, 2**32, t, S.One): + raises(TypeError, lambda: Relational(b, v, op)) + + +def test_nothing_happens_to_Eq_condition_during_simplify(): + # issue 25701 + r = symbols('r', real=True) + assert Eq(2*sign(r + 3)/(5*Abs(r + 3)**Rational(3, 5)), 0 + ).simplify() == Eq(Piecewise( + (0, Eq(r, -3)), ((r + 3)/(5*Abs((r + 3)**Rational(8, 5)))*2, True)), 0) + + +def test_issue_15847(): + a = Ne(x*(x + y), x**2 + x*y) + assert simplify(a) == False + + +def test_negated_property(): + eq = Eq(x, y) + assert eq.negated == Ne(x, y) + + eq = Ne(x, y) + assert eq.negated == Eq(x, y) + + eq = Ge(x + y, y - x) + assert eq.negated == Lt(x + y, y - x) + + for f in (Eq, Ne, Ge, Gt, Le, Lt): + assert f(x, y).negated.negated == f(x, y) + + +def test_reversedsign_property(): + eq = Eq(x, y) + assert eq.reversedsign == Eq(-x, -y) + + eq = Ne(x, y) + assert eq.reversedsign == Ne(-x, -y) + + eq = Ge(x + y, y - x) + assert eq.reversedsign == Le(-x - y, x - y) + + for f in (Eq, Ne, Ge, Gt, Le, Lt): + assert f(x, y).reversedsign.reversedsign == f(x, y) + + for f in (Eq, Ne, Ge, Gt, Le, Lt): + assert f(-x, y).reversedsign.reversedsign == f(-x, y) + + for f in (Eq, Ne, Ge, Gt, Le, Lt): + assert f(x, -y).reversedsign.reversedsign == f(x, -y) + + for f in (Eq, Ne, Ge, Gt, Le, Lt): + assert f(-x, -y).reversedsign.reversedsign == f(-x, -y) + + +def test_reversed_reversedsign_property(): + for f in (Eq, Ne, Ge, Gt, Le, Lt): + assert f(x, y).reversed.reversedsign == f(x, y).reversedsign.reversed + + for f in (Eq, Ne, Ge, Gt, Le, Lt): + assert f(-x, y).reversed.reversedsign == f(-x, y).reversedsign.reversed + + for f in (Eq, Ne, Ge, Gt, Le, Lt): + assert f(x, -y).reversed.reversedsign == f(x, -y).reversedsign.reversed + + for f in (Eq, Ne, Ge, Gt, Le, Lt): + assert f(-x, -y).reversed.reversedsign == \ + f(-x, -y).reversedsign.reversed + + +def test_improved_canonical(): + def test_different_forms(listofforms): + for form1, form2 in combinations(listofforms, 2): + assert form1.canonical == form2.canonical + + def generate_forms(expr): + return [expr, expr.reversed, expr.reversedsign, + expr.reversed.reversedsign] + + test_different_forms(generate_forms(x > -y)) + test_different_forms(generate_forms(x >= -y)) + test_different_forms(generate_forms(Eq(x, -y))) + test_different_forms(generate_forms(Ne(x, -y))) + test_different_forms(generate_forms(pi < x)) + test_different_forms(generate_forms(pi - 5*y < -x + 2*y**2 - 7)) + + assert (pi >= x).canonical == (x <= pi) + + +def test_set_equality_canonical(): + a, b, c = symbols('a b c') + + A = Eq(FiniteSet(a, b, c), FiniteSet(1, 2, 3)) + B = Ne(FiniteSet(a, b, c), FiniteSet(4, 5, 6)) + + assert A.canonical == A.reversed + assert B.canonical == B.reversed + + +def test_trigsimp(): + # issue 16736 + s, c = sin(2*x), cos(2*x) + eq = Eq(s, c) + assert trigsimp(eq) == eq # no rearrangement of sides + # simplification of sides might result in + # an unevaluated Eq + changed = trigsimp(Eq(s + c, sqrt(2))) + assert isinstance(changed, Eq) + assert changed.subs(x, pi/8) is S.true + # or an evaluated one + assert trigsimp(Eq(cos(x)**2 + sin(x)**2, 1)) is S.true + + +def test_polynomial_relation_simplification(): + assert Ge(3*x*(x + 1) + 4, 3*x).simplify() in [Ge(x**2, -Rational(4,3)), Le(-x**2, Rational(4, 3))] + assert Le(-(3*x*(x + 1) + 4), -3*x).simplify() in [Ge(x**2, -Rational(4,3)), Le(-x**2, Rational(4, 3))] + assert ((x**2+3)*(x**2-1)+3*x >= 2*x**2).simplify() in [(x**4 + 3*x >= 3), (-x**4 - 3*x <= -3)] + + +def test_multivariate_linear_function_simplification(): + assert Ge(x + y, x - y).simplify() == Ge(y, 0) + assert Le(-x + y, -x - y).simplify() == Le(y, 0) + assert Eq(2*x + y, 2*x + y - 3).simplify() == False + assert (2*x + y > 2*x + y - 3).simplify() == True + assert (2*x + y < 2*x + y - 3).simplify() == False + assert (2*x + y < 2*x + y + 3).simplify() == True + a, b, c, d, e, f, g = symbols('a b c d e f g') + assert Lt(a + b + c + 2*d, 3*d - f + g). simplify() == Lt(a, -b - c + d - f + g) + + +def test_nonpolymonial_relations(): + assert Eq(cos(x), 0).simplify() == Eq(cos(x), 0) + +def test_18778(): + raises(TypeError, lambda: is_le(Basic(), Basic())) + raises(TypeError, lambda: is_gt(Basic(), Basic())) + raises(TypeError, lambda: is_ge(Basic(), Basic())) + raises(TypeError, lambda: is_lt(Basic(), Basic())) + +def test_EvalEq(): + """ + + This test exists to ensure backwards compatibility. + The method to use is _eval_is_eq + """ + from sympy.core.expr import Expr + + class PowTest(Expr): + def __new__(cls, base, exp): + return Basic.__new__(PowTest, _sympify(base), _sympify(exp)) + + def _eval_Eq(lhs, rhs): + if type(lhs) == PowTest and type(rhs) == PowTest: + return lhs.args[0] == rhs.args[0] and lhs.args[1] == rhs.args[1] + + assert is_eq(PowTest(3, 4), PowTest(3,4)) + assert is_eq(PowTest(3, 4), _sympify(4)) is None + assert is_neq(PowTest(3, 4), PowTest(3,7)) + + +def test_is_eq(): + # test assumptions + assert is_eq(x, y, Q.infinite(x) & Q.finite(y)) is False + assert is_eq(x, y, Q.infinite(x) & Q.infinite(y) & Q.extended_real(x) & ~Q.extended_real(y)) is False + assert is_eq(x, y, Q.infinite(x) & Q.infinite(y) & Q.extended_positive(x) & Q.extended_negative(y)) is False + + assert is_eq(x+I, y+I, Q.infinite(x) & Q.finite(y)) is False + assert is_eq(1+x*I, 1+y*I, Q.infinite(x) & Q.finite(y)) is False + + assert is_eq(x, S(0), assumptions=Q.zero(x)) + assert is_eq(x, S(0), assumptions=~Q.zero(x)) is False + assert is_eq(x, S(0), assumptions=Q.nonzero(x)) is False + assert is_neq(x, S(0), assumptions=Q.zero(x)) is False + assert is_neq(x, S(0), assumptions=~Q.zero(x)) + assert is_neq(x, S(0), assumptions=Q.nonzero(x)) + + # test registration + class PowTest(Expr): + def __new__(cls, base, exp): + return Basic.__new__(cls, _sympify(base), _sympify(exp)) + + @dispatch(PowTest, PowTest) + def _eval_is_eq(lhs, rhs): + if type(lhs) == PowTest and type(rhs) == PowTest: + return fuzzy_and([is_eq(lhs.args[0], rhs.args[0]), is_eq(lhs.args[1], rhs.args[1])]) + + assert is_eq(PowTest(3, 4), PowTest(3,4)) + assert is_eq(PowTest(3, 4), _sympify(4)) is None + assert is_neq(PowTest(3, 4), PowTest(3,7)) + + +def test_is_ge_le(): + # test assumptions + assert is_ge(x, S(0), Q.nonnegative(x)) is True + assert is_ge(x, S(0), Q.negative(x)) is False + + # test registration + class PowTest(Expr): + def __new__(cls, base, exp): + return Basic.__new__(cls, _sympify(base), _sympify(exp)) + + @dispatch(PowTest, PowTest) + def _eval_is_ge(lhs, rhs): + if type(lhs) == PowTest and type(rhs) == PowTest: + return fuzzy_and([is_ge(lhs.args[0], rhs.args[0]), is_ge(lhs.args[1], rhs.args[1])]) + + assert is_ge(PowTest(3, 9), PowTest(3,2)) + assert is_gt(PowTest(3, 9), PowTest(3,2)) + assert is_le(PowTest(3, 2), PowTest(3,9)) + assert is_lt(PowTest(3, 2), PowTest(3,9)) + + +def test_weak_strict(): + for func in (Eq, Ne): + eq = func(x, 1) + assert eq.strict == eq.weak == eq + eq = Gt(x, 1) + assert eq.weak == Ge(x, 1) + assert eq.strict == eq + eq = Lt(x, 1) + assert eq.weak == Le(x, 1) + assert eq.strict == eq + eq = Ge(x, 1) + assert eq.strict == Gt(x, 1) + assert eq.weak == eq + eq = Le(x, 1) + assert eq.strict == Lt(x, 1) + assert eq.weak == eq + + +def test_issue_23731(): + i = symbols('i', integer=True) + assert unchanged(Eq, i, 1.0) + assert unchanged(Eq, i/2, 0.5) + ni = symbols('ni', integer=False) + assert Eq(ni, 1) == False + assert unchanged(Eq, ni, .1) + assert Eq(ni, 1.0) == False + nr = symbols('nr', rational=False) + assert Eq(nr, .1) == False + + +def test_rewrite_Add(): + from sympy.testing.pytest import warns_deprecated_sympy + with warns_deprecated_sympy(): + assert Eq(x, y).rewrite(Add) == x - y diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_rules.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_rules.py new file mode 100644 index 0000000000000000000000000000000000000000..31cb88b52db21f39653033b4567526e992be99f0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_rules.py @@ -0,0 +1,14 @@ +from sympy.core.rules import Transform + +from sympy.testing.pytest import raises + + +def test_Transform(): + add1 = Transform(lambda x: x + 1, lambda x: x % 2 == 1) + assert add1[1] == 2 + assert (1 in add1) is True + assert add1.get(1) == 2 + + raises(KeyError, lambda: add1[2]) + assert (2 in add1) is False + assert add1.get(2) is None diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_singleton.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_singleton.py new file mode 100644 index 0000000000000000000000000000000000000000..893713f27d74b884391ad800d186eafe5337ab1c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_singleton.py @@ -0,0 +1,76 @@ +from sympy.core.basic import Basic +from sympy.core.numbers import Rational +from sympy.core.singleton import S, Singleton + +def test_Singleton(): + + class MySingleton(Basic, metaclass=Singleton): + pass + + MySingleton() # force instantiation + assert MySingleton() is not Basic() + assert MySingleton() is MySingleton() + assert S.MySingleton is MySingleton() + + class MySingleton_sub(MySingleton): + pass + + MySingleton_sub() + assert MySingleton_sub() is not MySingleton() + assert MySingleton_sub() is MySingleton_sub() + +def test_singleton_redefinition(): + class TestSingleton(Basic, metaclass=Singleton): + pass + + assert TestSingleton() is S.TestSingleton + + class TestSingleton(Basic, metaclass=Singleton): + pass + + assert TestSingleton() is S.TestSingleton + +def test_names_in_namespace(): + # Every singleton name should be accessible from the 'from sympy import *' + # namespace in addition to the S object. However, it does not need to be + # by the same name (e.g., oo instead of S.Infinity). + + # As a general rule, things should only be added to the singleton registry + # if they are used often enough that code can benefit either from the + # performance benefit of being able to use 'is' (this only matters in very + # tight loops), or from the memory savings of having exactly one instance + # (this matters for the numbers singletons, but very little else). The + # singleton registry is already a bit overpopulated, and things cannot be + # removed from it without breaking backwards compatibility. So if you got + # here by adding something new to the singletons, ask yourself if it + # really needs to be singletonized. Note that SymPy classes compare to one + # another just fine, so Class() == Class() will give True even if each + # Class() returns a new instance. Having unique instances is only + # necessary for the above noted performance gains. It should not be needed + # for any behavioral purposes. + + # If you determine that something really should be a singleton, it must be + # accessible to sympify() without using 'S' (hence this test). Also, its + # str printer should print a form that does not use S. This is because + # sympify() disables attribute lookups by default for safety purposes. + d = {} + exec('from sympy import *', d) + + for name in dir(S) + list(S._classes_to_install): + if name.startswith('_'): + continue + if name == 'register': + continue + if isinstance(getattr(S, name), Rational): + continue + if getattr(S, name).__module__.startswith('sympy.physics'): + continue + if name in ['MySingleton', 'MySingleton_sub', 'TestSingleton']: + # From the tests above + continue + if name == 'NegativeInfinity': + # Accessible by -oo + continue + + # Use is here to ensure it is the exact same object + assert any(getattr(S, name) is i for i in d.values()), name diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_sorting.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_sorting.py new file mode 100644 index 0000000000000000000000000000000000000000..a18dbfb624552cf2fa11bb7f3c3a9e865caeb0c4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_sorting.py @@ -0,0 +1,28 @@ +from sympy.core.sorting import default_sort_key, ordered +from sympy.testing.pytest import raises + +from sympy.abc import x + + +def test_default_sort_key(): + func = lambda x: x + assert sorted([func, x, func], key=default_sort_key) == [func, func, x] + + class C: + def __repr__(self): + return 'x.y' + func = C() + assert sorted([x, func], key=default_sort_key) == [func, x] + + +def test_ordered(): + # Issue 7210 - this had been failing with python2/3 problems + assert (list(ordered([{1:3, 2:4, 9:10}, {1:3}])) == \ + [{1: 3}, {1: 3, 2: 4, 9: 10}]) + # warnings should not be raised for identical items + l = [1, 1] + assert list(ordered(l, warn=True)) == l + l = [[1], [2], [1]] + assert list(ordered(l, warn=True)) == [[1], [1], [2]] + raises(ValueError, lambda: list(ordered(['a', 'ab'], keys=[lambda x: x[0]], + default=False, warn=True))) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_subs.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_subs.py new file mode 100644 index 0000000000000000000000000000000000000000..0803a4b1b5e93b8a35f43516ccef3ab9a16f08ec --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_subs.py @@ -0,0 +1,895 @@ +from sympy.calculus.accumulationbounds import AccumBounds +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.containers import (Dict, Tuple) +from sympy.core.function import (Derivative, Function, Lambda, Subs) +from sympy.core.mul import Mul +from sympy.core.numbers import (Float, I, Integer, Rational, oo, pi, zoo) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, Wild, symbols) +from sympy.core.sympify import SympifyError +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (atan2, cos, cot, sin, tan) +from sympy.matrices.dense import (Matrix, zeros) +from sympy.matrices.expressions.special import ZeroMatrix +from sympy.polys.polytools import factor +from sympy.polys.rootoftools import RootOf +from sympy.simplify.cse_main import cse +from sympy.simplify.simplify import nsimplify +from sympy.core.basic import _aresame +from sympy.testing.pytest import XFAIL, raises +from sympy.abc import a, x, y, z, t + + +def test_subs(): + n3 = Rational(3) + e = x + e = e.subs(x, n3) + assert e == Rational(3) + + e = 2*x + assert e == 2*x + e = e.subs(x, n3) + assert e == Rational(6) + + +def test_subs_Matrix(): + z = zeros(2) + z1 = ZeroMatrix(2, 2) + assert (x*y).subs({x:z, y:0}) in [z, z1] + assert (x*y).subs({y:z, x:0}) == 0 + assert (x*y).subs({y:z, x:0}, simultaneous=True) in [z, z1] + assert (x + y).subs({x: z, y: z}, simultaneous=True) in [z, z1] + assert (x + y).subs({x: z, y: z}) in [z, z1] + + # Issue #15528 + assert Mul(Matrix([[3]]), x).subs(x, 2.0) == Matrix([[6.0]]) + # Does not raise a TypeError, see comment on the MatAdd postprocessor + assert Add(Matrix([[3]]), x).subs(x, 2.0) == Add(Matrix([[3]]), 2.0) + + +def test_subs_AccumBounds(): + e = x + e = e.subs(x, AccumBounds(1, 3)) + assert e == AccumBounds(1, 3) + + e = 2*x + e = e.subs(x, AccumBounds(1, 3)) + assert e == AccumBounds(2, 6) + + e = x + x**2 + e = e.subs(x, AccumBounds(-1, 1)) + assert e == AccumBounds(-1, 2) + + +def test_trigonometric(): + n3 = Rational(3) + e = (sin(x)**2).diff(x) + assert e == 2*sin(x)*cos(x) + e = e.subs(x, n3) + assert e == 2*cos(n3)*sin(n3) + + e = (sin(x)**2).diff(x) + assert e == 2*sin(x)*cos(x) + e = e.subs(sin(x), cos(x)) + assert e == 2*cos(x)**2 + + assert exp(pi).subs(exp, sin) == 0 + assert cos(exp(pi)).subs(exp, sin) == 1 + + i = Symbol('i', integer=True) + zoo = S.ComplexInfinity + assert tan(x).subs(x, pi/2) is zoo + assert cot(x).subs(x, pi) is zoo + assert cot(i*x).subs(x, pi) is zoo + assert tan(i*x).subs(x, pi/2) == tan(i*pi/2) + assert tan(i*x).subs(x, pi/2).subs(i, 1) is zoo + o = Symbol('o', odd=True) + assert tan(o*x).subs(x, pi/2) == tan(o*pi/2) + + +def test_powers(): + assert sqrt(1 - sqrt(x)).subs(x, 4) == I + assert (sqrt(1 - x**2)**3).subs(x, 2) == - 3*I*sqrt(3) + assert (x**Rational(1, 3)).subs(x, 27) == 3 + assert (x**Rational(1, 3)).subs(x, -27) == 3*(-1)**Rational(1, 3) + assert ((-x)**Rational(1, 3)).subs(x, 27) == 3*(-1)**Rational(1, 3) + n = Symbol('n', negative=True) + assert (x**n).subs(x, 0) is S.ComplexInfinity + assert exp(-1).subs(S.Exp1, 0) is S.ComplexInfinity + assert (x**(4.0*y)).subs(x**(2.0*y), n) == n**2.0 + assert (2**(x + 2)).subs(2, 3) == 3**(x + 3) + + +def test_logexppow(): # no eval() + x = Symbol('x', real=True) + w = Symbol('w') + e = (3**(1 + x) + 2**(1 + x))/(3**x + 2**x) + assert e.subs(2**x, w) != e + assert e.subs(exp(x*log(Rational(2))), w) != e + + +def test_bug(): + x1 = Symbol('x1') + x2 = Symbol('x2') + y = x1*x2 + assert y.subs(x1, Float(3.0)) == Float(3.0)*x2 + + +def test_subbug1(): + # see that they don't fail + (x**x).subs(x, 1) + (x**x).subs(x, 1.0) + + +def test_subbug2(): + # Ensure this does not cause infinite recursion + assert Float(7.7).epsilon_eq(abs(x).subs(x, -7.7)) + + +def test_dict_set(): + a, b, c = map(Wild, 'abc') + + f = 3*cos(4*x) + r = f.match(a*cos(b*x)) + assert r == {a: 3, b: 4} + e = a/b*sin(b*x) + assert e.subs(r) == r[a]/r[b]*sin(r[b]*x) + assert e.subs(r) == 3*sin(4*x) / 4 + s = set(r.items()) + assert e.subs(s) == r[a]/r[b]*sin(r[b]*x) + assert e.subs(s) == 3*sin(4*x) / 4 + + assert e.subs(r) == r[a]/r[b]*sin(r[b]*x) + assert e.subs(r) == 3*sin(4*x) / 4 + assert x.subs(Dict((x, 1))) == 1 + + +def test_dict_ambigous(): # see issue 3566 + f = x*exp(x) + g = z*exp(z) + + df = {x: y, exp(x): y} + dg = {z: y, exp(z): y} + + assert f.subs(df) == y**2 + assert g.subs(dg) == y**2 + + # and this is how order can affect the result + assert f.subs(x, y).subs(exp(x), y) == y*exp(y) + assert f.subs(exp(x), y).subs(x, y) == y**2 + + # length of args and count_ops are the same so + # default_sort_key resolves ordering...if one + # doesn't want this result then an unordered + # sequence should not be used. + e = 1 + x*y + assert e.subs({x: y, y: 2}) == 5 + # here, there are no obviously clashing keys or values + # but the results depend on the order + assert exp(x/2 + y).subs({exp(y + 1): 2, x: 2}) == exp(y + 1) + + +def test_deriv_sub_bug3(): + f = Function('f') + pat = Derivative(f(x), x, x) + assert pat.subs(y, y**2) == Derivative(f(x), x, x) + assert pat.subs(y, y**2) != Derivative(f(x), x) + + +def test_equality_subs1(): + f = Function('f') + eq = Eq(f(x)**2, x) + res = Eq(Integer(16), x) + assert eq.subs(f(x), 4) == res + + +def test_equality_subs2(): + f = Function('f') + eq = Eq(f(x)**2, 16) + assert bool(eq.subs(f(x), 3)) is False + assert bool(eq.subs(f(x), 4)) is True + + +def test_issue_3742(): + e = sqrt(x)*exp(y) + assert e.subs(sqrt(x), 1) == exp(y) + + +def test_subs_dict1(): + assert (1 + x*y).subs(x, pi) == 1 + pi*y + assert (1 + x*y).subs({x: pi, y: 2}) == 1 + 2*pi + + c2, c3, q1p, q2p, c1, s1, s2, s3 = symbols('c2 c3 q1p q2p c1 s1 s2 s3') + test = (c2**2*q2p*c3 + c1**2*s2**2*q2p*c3 + s1**2*s2**2*q2p*c3 + - c1**2*q1p*c2*s3 - s1**2*q1p*c2*s3) + assert (test.subs({c1**2: 1 - s1**2, c2**2: 1 - s2**2, c3**3: 1 - s3**2}) + == c3*q2p*(1 - s2**2) + c3*q2p*s2**2*(1 - s1**2) + - c2*q1p*s3*(1 - s1**2) + c3*q2p*s1**2*s2**2 - c2*q1p*s3*s1**2) + + +def test_mul(): + x, y, z, a, b, c = symbols('x y z a b c') + A, B, C = symbols('A B C', commutative=0) + assert (x*y*z).subs(z*x, y) == y**2 + assert (z*x).subs(1/x, z) == 1 + assert (x*y/z).subs(1/z, a) == a*x*y + assert (x*y/z).subs(x/z, a) == a*y + assert (x*y/z).subs(y/z, a) == a*x + assert (x*y/z).subs(x/z, 1/a) == y/a + assert (x*y/z).subs(x, 1/a) == y/(z*a) + assert (2*x*y).subs(5*x*y, z) != z*Rational(2, 5) + assert (x*y*A).subs(x*y, a) == a*A + assert (x**2*y**(x*Rational(3, 2))).subs(x*y**(x/2), 2) == 4*y**(x/2) + assert (x*exp(x*2)).subs(x*exp(x), 2) == 2*exp(x) + assert ((x**(2*y))**3).subs(x**y, 2) == 64 + assert (x*A*B).subs(x*A, y) == y*B + assert (x*y*(1 + x)*(1 + x*y)).subs(x*y, 2) == 6*(1 + x) + assert ((1 + A*B)*A*B).subs(A*B, x*A*B) + assert (x*a/z).subs(x/z, A) == a*A + assert (x**3*A).subs(x**2*A, a) == a*x + assert (x**2*A*B).subs(x**2*B, a) == a*A + assert (x**2*A*B).subs(x**2*A, a) == a*B + assert (b*A**3/(a**3*c**3)).subs(a**4*c**3*A**3/b**4, z) == \ + b*A**3/(a**3*c**3) + assert (6*x).subs(2*x, y) == 3*y + assert (y*exp(x*Rational(3, 2))).subs(y*exp(x), 2) == 2*exp(x/2) + assert (y*exp(x*Rational(3, 2))).subs(y*exp(x), 2) == 2*exp(x/2) + assert (A**2*B*A**2*B*A**2).subs(A*B*A, C) == A*C**2*A + assert (x*A**3).subs(x*A, y) == y*A**2 + assert (x**2*A**3).subs(x*A, y) == y**2*A + assert (x*A**3).subs(x*A, B) == B*A**2 + assert (x*A*B*A*exp(x*A*B)).subs(x*A, B) == B**2*A*exp(B*B) + assert (x**2*A*B*A*exp(x*A*B)).subs(x*A, B) == B**3*exp(B**2) + assert (x**3*A*exp(x*A*B)*A*exp(x*A*B)).subs(x*A, B) == \ + x*B*exp(B**2)*B*exp(B**2) + assert (x*A*B*C*A*B).subs(x*A*B, C) == C**2*A*B + assert (-I*a*b).subs(a*b, 2) == -2*I + + # issue 6361 + assert (-8*I*a).subs(-2*a, 1) == 4*I + assert (-I*a).subs(-a, 1) == I + + # issue 6441 + assert (4*x**2).subs(2*x, y) == y**2 + assert (2*4*x**2).subs(2*x, y) == 2*y**2 + assert (-x**3/9).subs(-x/3, z) == -z**2*x + assert (-x**3/9).subs(x/3, z) == -z**2*x + assert (-2*x**3/9).subs(x/3, z) == -2*x*z**2 + assert (-2*x**3/9).subs(-x/3, z) == -2*x*z**2 + assert (-2*x**3/9).subs(-2*x, z) == z*x**2/9 + assert (-2*x**3/9).subs(2*x, z) == -z*x**2/9 + assert (2*(3*x/5/7)**2).subs(3*x/5, z) == 2*(Rational(1, 7))**2*z**2 + assert (4*x).subs(-2*x, z) == 4*x # try keep subs literal + + +def test_subs_simple(): + a = symbols('a', commutative=True) + x = symbols('x', commutative=False) + + assert (2*a).subs(1, 3) == 2*a + assert (2*a).subs(2, 3) == 3*a + assert (2*a).subs(a, 3) == 6 + assert sin(2).subs(1, 3) == sin(2) + assert sin(2).subs(2, 3) == sin(3) + assert sin(a).subs(a, 3) == sin(3) + + assert (2*x).subs(1, 3) == 2*x + assert (2*x).subs(2, 3) == 3*x + assert (2*x).subs(x, 3) == 6 + assert sin(x).subs(x, 3) == sin(3) + + +def test_subs_constants(): + a, b = symbols('a b', commutative=True) + x, y = symbols('x y', commutative=False) + + assert (a*b).subs(2*a, 1) == a*b + assert (1.5*a*b).subs(a, 1) == 1.5*b + assert (2*a*b).subs(2*a, 1) == b + assert (2*a*b).subs(4*a, 1) == 2*a*b + + assert (x*y).subs(2*x, 1) == x*y + assert (1.5*x*y).subs(x, 1) == 1.5*y + assert (2*x*y).subs(2*x, 1) == y + assert (2*x*y).subs(4*x, 1) == 2*x*y + + +def test_subs_commutative(): + a, b, c, d, K = symbols('a b c d K', commutative=True) + + assert (a*b).subs(a*b, K) == K + assert (a*b*a*b).subs(a*b, K) == K**2 + assert (a*a*b*b).subs(a*b, K) == K**2 + assert (a*b*c*d).subs(a*b*c, K) == d*K + assert (a*b**c).subs(a, K) == K*b**c + assert (a*b**c).subs(b, K) == a*K**c + assert (a*b**c).subs(c, K) == a*b**K + assert (a*b*c*b*a).subs(a*b, K) == c*K**2 + assert (a**3*b**2*a).subs(a*b, K) == a**2*K**2 + + +def test_subs_noncommutative(): + w, x, y, z, L = symbols('w x y z L', commutative=False) + alpha = symbols('alpha', commutative=True) + someint = symbols('someint', commutative=True, integer=True) + + assert (x*y).subs(x*y, L) == L + assert (w*y*x).subs(x*y, L) == w*y*x + assert (w*x*y*z).subs(x*y, L) == w*L*z + assert (x*y*x*y).subs(x*y, L) == L**2 + assert (x*x*y).subs(x*y, L) == x*L + assert (x*x*y*y).subs(x*y, L) == x*L*y + assert (w*x*y).subs(x*y*z, L) == w*x*y + assert (x*y**z).subs(x, L) == L*y**z + assert (x*y**z).subs(y, L) == x*L**z + assert (x*y**z).subs(z, L) == x*y**L + assert (w*x*y*z*x*y).subs(x*y*z, L) == w*L*x*y + assert (w*x*y*y*w*x*x*y*x*y*y*x*y).subs(x*y, L) == w*L*y*w*x*L**2*y*L + + # Check fractional power substitutions. It should not do + # substitutions that choose a value for noncommutative log, + # or inverses that don't already appear in the expressions. + assert (x*x*x).subs(x*x, L) == L*x + assert (x*x*x*y*x*x*x*x).subs(x*x, L) == L*x*y*L**2 + for p in range(1, 5): + for k in range(10): + assert (y * x**k).subs(x**p, L) == y * L**(k//p) * x**(k % p) + assert (x**Rational(3, 2)).subs(x**S.Half, L) == x**Rational(3, 2) + assert (x**S.Half).subs(x**S.Half, L) == L + assert (x**Rational(-1, 2)).subs(x**S.Half, L) == x**Rational(-1, 2) + assert (x**Rational(-1, 2)).subs(x**Rational(-1, 2), L) == L + + assert (x**(2*someint)).subs(x**someint, L) == L**2 + assert (x**(2*someint + 3)).subs(x**someint, L) == L**2*x**3 + assert (x**(3*someint + 3)).subs(x**someint, L) == L**3*x**3 + assert (x**(3*someint)).subs(x**(2*someint), L) == L * x**someint + assert (x**(4*someint)).subs(x**(2*someint), L) == L**2 + assert (x**(4*someint + 1)).subs(x**(2*someint), L) == L**2 * x + assert (x**(4*someint)).subs(x**(3*someint), L) == L * x**someint + assert (x**(4*someint + 1)).subs(x**(3*someint), L) == L * x**(someint + 1) + + assert (x**(2*alpha)).subs(x**alpha, L) == x**(2*alpha) + assert (x**(2*alpha + 2)).subs(x**2, L) == x**(2*alpha + 2) + assert ((2*z)**alpha).subs(z**alpha, y) == (2*z)**alpha + assert (x**(2*someint*alpha)).subs(x**someint, L) == x**(2*someint*alpha) + assert (x**(2*someint + alpha)).subs(x**someint, L) == x**(2*someint + alpha) + + # This could in principle be substituted, but is not currently + # because it requires recognizing that someint**2 is divisible by + # someint. + assert (x**(someint**2 + 3)).subs(x**someint, L) == x**(someint**2 + 3) + + # alpha**z := exp(log(alpha) z) is usually well-defined + assert (4**z).subs(2**z, y) == y**2 + + # Negative powers + assert (x**(-1)).subs(x**3, L) == x**(-1) + assert (x**(-2)).subs(x**3, L) == x**(-2) + assert (x**(-3)).subs(x**3, L) == L**(-1) + assert (x**(-4)).subs(x**3, L) == L**(-1) * x**(-1) + assert (x**(-5)).subs(x**3, L) == L**(-1) * x**(-2) + + assert (x**(-1)).subs(x**(-3), L) == x**(-1) + assert (x**(-2)).subs(x**(-3), L) == x**(-2) + assert (x**(-3)).subs(x**(-3), L) == L + assert (x**(-4)).subs(x**(-3), L) == L * x**(-1) + assert (x**(-5)).subs(x**(-3), L) == L * x**(-2) + + assert (x**1).subs(x**(-3), L) == x + assert (x**2).subs(x**(-3), L) == x**2 + assert (x**3).subs(x**(-3), L) == L**(-1) + assert (x**4).subs(x**(-3), L) == L**(-1) * x + assert (x**5).subs(x**(-3), L) == L**(-1) * x**2 + + +def test_subs_basic_funcs(): + a, b, c, d, K = symbols('a b c d K', commutative=True) + w, x, y, z, L = symbols('w x y z L', commutative=False) + + assert (x + y).subs(x + y, L) == L + assert (x - y).subs(x - y, L) == L + assert (x/y).subs(x, L) == L/y + assert (x**y).subs(x, L) == L**y + assert (x**y).subs(y, L) == x**L + assert ((a - c)/b).subs(b, K) == (a - c)/K + assert (exp(x*y - z)).subs(x*y, L) == exp(L - z) + assert (a*exp(x*y - w*z) + b*exp(x*y + w*z)).subs(z, 0) == \ + a*exp(x*y) + b*exp(x*y) + assert ((a - b)/(c*d - a*b)).subs(c*d - a*b, K) == (a - b)/K + assert (w*exp(a*b - c)*x*y/4).subs(x*y, L) == w*exp(a*b - c)*L/4 + + +def test_subs_wild(): + R, S, T, U = symbols('R S T U', cls=Wild) + + assert (R*S).subs(R*S, T) == T + assert (S*R).subs(R*S, T) == T + assert (R + S).subs(R + S, T) == T + assert (R**S).subs(R, T) == T**S + assert (R**S).subs(S, T) == R**T + assert (R*S**T).subs(R, U) == U*S**T + assert (R*S**T).subs(S, U) == R*U**T + assert (R*S**T).subs(T, U) == R*S**U + + +def test_subs_mixed(): + a, b, c, d, K = symbols('a b c d K', commutative=True) + w, x, y, z, L = symbols('w x y z L', commutative=False) + R, S, T, U = symbols('R S T U', cls=Wild) + + assert (a*x*y).subs(x*y, L) == a*L + assert (a*b*x*y*x).subs(x*y, L) == a*b*L*x + assert (R*x*y*exp(x*y)).subs(x*y, L) == R*L*exp(L) + assert (a*x*y*y*x - x*y*z*exp(a*b)).subs(x*y, L) == a*L*y*x - L*z*exp(a*b) + e = c*y*x*y*x**(R*S - a*b) - T*(a*R*b*S) + assert e.subs(x*y, L).subs(a*b, K).subs(R*S, U) == \ + c*y*L*x**(U - K) - T*(U*K) + + +def test_division(): + a, b, c = symbols('a b c', commutative=True) + x, y, z = symbols('x y z', commutative=True) + + assert (1/a).subs(a, c) == 1/c + assert (1/a**2).subs(a, c) == 1/c**2 + assert (1/a**2).subs(a, -2) == Rational(1, 4) + assert (-(1/a**2)).subs(a, -2) == Rational(-1, 4) + + assert (1/x).subs(x, z) == 1/z + assert (1/x**2).subs(x, z) == 1/z**2 + assert (1/x**2).subs(x, -2) == Rational(1, 4) + assert (-(1/x**2)).subs(x, -2) == Rational(-1, 4) + + #issue 5360 + assert (1/x).subs(x, 0) == 1/S.Zero + + +def test_add(): + a, b, c, d, x, y, t = symbols('a b c d x y t') + + assert (a**2 - b - c).subs(a**2 - b, d) in [d - c, a**2 - b - c] + assert (a**2 - c).subs(a**2 - c, d) == d + assert (a**2 - b - c).subs(a**2 - c, d) in [d - b, a**2 - b - c] + assert (a**2 - x - c).subs(a**2 - c, d) in [d - x, a**2 - x - c] + assert (a**2 - b - sqrt(a)).subs(a**2 - sqrt(a), c) == c - b + assert (a + b + exp(a + b)).subs(a + b, c) == c + exp(c) + assert (c + b + exp(c + b)).subs(c + b, a) == a + exp(a) + assert (a + b + c + d).subs(b + c, x) == a + d + x + assert (a + b + c + d).subs(-b - c, x) == a + d - x + assert ((x + 1)*y).subs(x + 1, t) == t*y + assert ((-x - 1)*y).subs(x + 1, t) == -t*y + assert ((x - 1)*y).subs(x + 1, t) == y*(t - 2) + assert ((-x + 1)*y).subs(x + 1, t) == y*(-t + 2) + + # this should work every time: + e = a**2 - b - c + assert e.subs(Add(*e.args[:2]), d) == d + e.args[2] + assert e.subs(a**2 - c, d) == d - b + + # the fallback should recognize when a change has + # been made; while .1 == Rational(1, 10) they are not the same + # and the change should be made + assert (0.1 + a).subs(0.1, Rational(1, 10)) == Rational(1, 10) + a + + e = (-x*(-y + 1) - y*(y - 1)) + ans = (-x*(x) - y*(-x)).expand() + assert e.subs(-y + 1, x) == ans + + #Test issue 18747 + assert (exp(x) + cos(x)).subs(x, oo) == oo + assert Add(*[AccumBounds(-1, 1), oo]) == oo + assert Add(*[oo, AccumBounds(-1, 1)]) == oo + + +def test_subs_issue_4009(): + assert (I*Symbol('a')).subs(1, 2) == I*Symbol('a') + + +def test_functions_subs(): + f, g = symbols('f g', cls=Function) + l = Lambda((x, y), sin(x) + y) + assert (g(y, x) + cos(x)).subs(g, l) == sin(y) + x + cos(x) + assert (f(x)**2).subs(f, sin) == sin(x)**2 + assert (f(x, y)).subs(f, log) == log(x, y) + assert (f(x, y)).subs(f, sin) == f(x, y) + assert (sin(x) + atan2(x, y)).subs([[atan2, f], [sin, g]]) == \ + f(x, y) + g(x) + assert (g(f(x + y, x))).subs([[f, l], [g, exp]]) == exp(x + sin(x + y)) + + +def test_derivative_subs(): + f = Function('f') + g = Function('g') + assert Derivative(f(x), x).subs(f(x), y) != 0 + # need xreplace to put the function back, see #13803 + assert Derivative(f(x), x).subs(f(x), y).xreplace({y: f(x)}) == \ + Derivative(f(x), x) + # issues 5085, 5037 + assert cse(Derivative(f(x), x) + f(x))[1][0].has(Derivative) + assert cse(Derivative(f(x, y), x) + + Derivative(f(x, y), y))[1][0].has(Derivative) + eq = Derivative(g(x), g(x)) + assert eq.subs(g, f) == Derivative(f(x), f(x)) + assert eq.subs(g(x), f(x)) == Derivative(f(x), f(x)) + assert eq.subs(g, cos) == Subs(Derivative(y, y), y, cos(x)) + + +def test_derivative_subs2(): + f_func, g_func = symbols('f g', cls=Function) + f, g = f_func(x, y, z), g_func(x, y, z) + assert Derivative(f, x, y).subs(Derivative(f, x, y), g) == g + assert Derivative(f, y, x).subs(Derivative(f, x, y), g) == g + assert Derivative(f, x, y).subs(Derivative(f, x), g) == Derivative(g, y) + assert Derivative(f, x, y).subs(Derivative(f, y), g) == Derivative(g, x) + assert (Derivative(f, x, y, z).subs( + Derivative(f, x, z), g) == Derivative(g, y)) + assert (Derivative(f, x, y, z).subs( + Derivative(f, z, y), g) == Derivative(g, x)) + assert (Derivative(f, x, y, z).subs( + Derivative(f, z, y, x), g) == g) + + # Issue 9135 + assert (Derivative(f, x, x, y).subs( + Derivative(f, y, y), g) == Derivative(f, x, x, y)) + assert (Derivative(f, x, y, y, z).subs( + Derivative(f, x, y, y, y), g) == Derivative(f, x, y, y, z)) + + assert Derivative(f, x, y).subs(Derivative(f_func(x), x, y), g) == Derivative(f, x, y) + + +def test_derivative_subs3(): + dex = Derivative(exp(x), x) + assert Derivative(dex, x).subs(dex, exp(x)) == dex + assert dex.subs(exp(x), dex) == Derivative(exp(x), x, x) + + +def test_issue_5284(): + A, B = symbols('A B', commutative=False) + assert (x*A).subs(x**2*A, B) == x*A + assert (A**2).subs(A**3, B) == A**2 + assert (A**6).subs(A**3, B) == B**2 + + +def test_subs_iter(): + assert x.subs(reversed([[x, y]])) == y + it = iter([[x, y]]) + assert x.subs(it) == y + assert x.subs(Tuple((x, y))) == y + + +def test_subs_dict(): + a, b, c, d, e = symbols('a b c d e') + + assert (2*x + y + z).subs({"x": 1, "y": 2}) == 4 + z + + l = [(sin(x), 2), (x, 1)] + assert (sin(x)).subs(l) == \ + (sin(x)).subs(dict(l)) == 2 + assert sin(x).subs(reversed(l)) == sin(1) + + expr = sin(2*x) + sqrt(sin(2*x))*cos(2*x)*sin(exp(x)*x) + reps = {sin(2*x): c, + sqrt(sin(2*x)): a, + cos(2*x): b, + exp(x): e, + x: d,} + assert expr.subs(reps) == c + a*b*sin(d*e) + + l = [(x, 3), (y, x**2)] + assert (x + y).subs(l) == 3 + x**2 + assert (x + y).subs(reversed(l)) == 12 + + # If changes are made to convert lists into dictionaries and do + # a dictionary-lookup replacement, these tests will help to catch + # some logical errors that might occur + l = [(y, z + 2), (1 + z, 5), (z, 2)] + assert (y - 1 + 3*x).subs(l) == 5 + 3*x + l = [(y, z + 2), (z, 3)] + assert (y - 2).subs(l) == 3 + + +def test_no_arith_subs_on_floats(): + assert (x + 3).subs(x + 3, a) == a + assert (x + 3).subs(x + 2, a) == a + 1 + + assert (x + y + 3).subs(x + 3, a) == a + y + assert (x + y + 3).subs(x + 2, a) == a + y + 1 + + assert (x + 3.0).subs(x + 3.0, a) == a + assert (x + 3.0).subs(x + 2.0, a) == x + 3.0 + + assert (x + y + 3.0).subs(x + 3.0, a) == a + y + assert (x + y + 3.0).subs(x + 2.0, a) == x + y + 3.0 + + +def test_issue_5651(): + a, b, c, K = symbols('a b c K', commutative=True) + assert (a/(b*c)).subs(b*c, K) == a/K + assert (a/(b**2*c**3)).subs(b*c, K) == a/(c*K**2) + assert (1/(x*y)).subs(x*y, 2) == S.Half + assert ((1 + x*y)/(x*y)).subs(x*y, 1) == 2 + assert (x*y*z).subs(x*y, 2) == 2*z + assert ((1 + x*y)/(x*y)/z).subs(x*y, 1) == 2/z + + +def test_issue_6075(): + assert Tuple(1, True).subs(1, 2) == Tuple(2, True) + + +def test_issue_6079(): + # since x + 2.0 == x + 2 we can't do a simple equality test + assert _aresame((x + 2.0).subs(2, 3), x + 2.0) + assert _aresame((x + 2.0).subs(2.0, 3), x + 3) + assert not _aresame(x + 2, x + 2.0) + assert not _aresame(Basic(cos(x), S(1)), Basic(cos(x), S(1.))) + assert _aresame(cos, cos) + assert not _aresame(1, S.One) + assert not _aresame(x, symbols('x', positive=True)) + + +def test_issue_4680(): + N = Symbol('N') + assert N.subs({"N": 3}) == 3 + + +def test_issue_6158(): + assert (x - 1).subs(1, y) == x - y + assert (x - 1).subs(-1, y) == x + y + assert (x - oo).subs(oo, y) == x - y + assert (x - oo).subs(-oo, y) == x + y + + +def test_Function_subs(): + f, g, h, i = symbols('f g h i', cls=Function) + p = Piecewise((g(f(x, y)), x < -1), (g(x), x <= 1)) + assert p.subs(g, h) == Piecewise((h(f(x, y)), x < -1), (h(x), x <= 1)) + assert (f(y) + g(x)).subs({f: h, g: i}) == i(x) + h(y) + + +def test_simultaneous_subs(): + reps = {x: 0, y: 0} + assert (x/y).subs(reps) != (y/x).subs(reps) + assert (x/y).subs(reps, simultaneous=True) == \ + (y/x).subs(reps, simultaneous=True) + reps = reps.items() + assert (x/y).subs(reps) != (y/x).subs(reps) + assert (x/y).subs(reps, simultaneous=True) == \ + (y/x).subs(reps, simultaneous=True) + assert Derivative(x, y, z).subs(reps, simultaneous=True) == \ + Subs(Derivative(0, y, z), y, 0) + + +def test_issue_6419_6421(): + assert (1/(1 + x/y)).subs(x/y, x) == 1/(1 + x) + assert (-2*I).subs(2*I, x) == -x + assert (-I*x).subs(I*x, x) == -x + assert (-3*I*y**4).subs(3*I*y**2, x) == -x*y**2 + + +def test_issue_6559(): + assert (-12*x + y).subs(-x, 1) == 12 + y + # though this involves cse it generated a failure in Mul._eval_subs + x0, x1 = symbols('x0 x1') + e = -log(-12*sqrt(2) + 17)/24 - log(-2*sqrt(2) + 3)/12 + sqrt(2)/3 + # XXX modify cse so x1 is eliminated and x0 = -sqrt(2)? + assert cse(e) == ( + [(x0, sqrt(2))], [x0/3 - log(-12*x0 + 17)/24 - log(-2*x0 + 3)/12]) + + +def test_issue_5261(): + x = symbols('x', real=True) + e = I*x + assert exp(e).subs(exp(x), y) == y**I + assert (2**e).subs(2**x, y) == y**I + eq = (-2)**e + assert eq.subs((-2)**x, y) == eq + + +def test_issue_6923(): + assert (-2*x*sqrt(2)).subs(2*x, y) == -sqrt(2)*y + + +def test_2arg_hack(): + N = Symbol('N', commutative=False) + ans = Mul(2, y + 1, evaluate=False) + assert (2*x*(y + 1)).subs(x, 1, hack2=True) == ans + assert (2*(y + 1 + N)).subs(N, 0, hack2=True) == ans + + +@XFAIL +def test_mul2(): + """When this fails, remove things labelled "2-arg hack" + 1) remove special handling in the fallback of subs that + was added in the same commit as this test + 2) remove the special handling in Mul.flatten + """ + assert (2*(x + 1)).is_Mul + + +def test_noncommutative_subs(): + x,y = symbols('x,y', commutative=False) + assert (x*y*x).subs([(x, x*y), (y, x)], simultaneous=True) == (x*y*x**2*y) + + +def test_issue_2877(): + f = Float(2.0) + assert (x + f).subs({f: 2}) == x + 2 + + def r(a, b, c): + return factor(a*x**2 + b*x + c) + e = r(5.0/6, 10, 5) + assert nsimplify(e) == 5*x**2/6 + 10*x + 5 + + +def test_issue_5910(): + t = Symbol('t') + assert (1/(1 - t)).subs(t, 1) is zoo + n = t + d = t - 1 + assert (n/d).subs(t, 1) is zoo + assert (-n/-d).subs(t, 1) is zoo + + +def test_issue_5217(): + s = Symbol('s') + z = (1 - 2*x*x) + w = (1 + 2*x*x) + q = 2*x*x*2*y*y + sub = {2*x*x: s} + assert w.subs(sub) == 1 + s + assert z.subs(sub) == 1 - s + assert q == 4*x**2*y**2 + assert q.subs(sub) == 2*y**2*s + + +def test_issue_10829(): + assert (4**x).subs(2**x, y) == y**2 + assert (9**x).subs(3**x, y) == y**2 + + +def test_pow_eval_subs_no_cache(): + # Tests pull request 9376 is working + from sympy.core.cache import clear_cache + + s = 1/sqrt(x**2) + # This bug only appeared when the cache was turned off. + # We need to approximate running this test without the cache. + # This creates approximately the same situation. + clear_cache() + + # This used to fail with a wrong result. + # It incorrectly returned 1/sqrt(x**2) before this pull request. + result = s.subs(sqrt(x**2), y) + assert result == 1/y + + +def test_RootOf_issue_10092(): + x = Symbol('x', real=True) + eq = x**3 - 17*x**2 + 81*x - 118 + r = RootOf(eq, 0) + assert (x < r).subs(x, r) is S.false + + +def test_issue_8886(): + from sympy.physics.mechanics import ReferenceFrame as R + # if something can't be sympified we assume that it + # doesn't play well with SymPy and disallow the + # substitution + v = R('A').x + raises(SympifyError, lambda: x.subs(x, v)) + raises(SympifyError, lambda: v.subs(v, x)) + assert v.__eq__(x) is False + + +def test_issue_12657(): + # treat -oo like the atom that it is + reps = [(-oo, 1), (oo, 2)] + assert (x < -oo).subs(reps) == (x < 1) + assert (x < -oo).subs(list(reversed(reps))) == (x < 1) + reps = [(-oo, 2), (oo, 1)] + assert (x < oo).subs(reps) == (x < 1) + assert (x < oo).subs(list(reversed(reps))) == (x < 1) + + +def test_recurse_Application_args(): + F = Lambda((x, y), exp(2*x + 3*y)) + f = Function('f') + A = f(x, f(x, x)) + C = F(x, F(x, x)) + assert A.subs(f, F) == A.replace(f, F) == C + + +def test_Subs_subs(): + assert Subs(x*y, x, x).subs(x, y) == Subs(x*y, x, y) + assert Subs(x*y, x, x + 1).subs(x, y) == \ + Subs(x*y, x, y + 1) + assert Subs(x*y, y, x + 1).subs(x, y) == \ + Subs(y**2, y, y + 1) + a = Subs(x*y*z, (y, x, z), (x + 1, x + z, x)) + b = Subs(x*y*z, (y, x, z), (x + 1, y + z, y)) + assert a.subs(x, y) == b and \ + a.doit().subs(x, y) == a.subs(x, y).doit() + f = Function('f') + g = Function('g') + assert Subs(2*f(x, y) + g(x), f(x, y), 1).subs(y, 2) == Subs( + 2*f(x, y) + g(x), (f(x, y), y), (1, 2)) + + +def test_issue_13333(): + eq = 1/x + assert eq.subs({"x": '1/2'}) == 2 + assert eq.subs({"x": '(1/2)'}) == 2 + + +def test_issue_15234(): + x, y = symbols('x y', real=True) + p = 6*x**5 + x**4 - 4*x**3 + 4*x**2 - 2*x + 3 + p_subbed = 6*x**5 - 4*x**3 - 2*x + y**4 + 4*y**2 + 3 + assert p.subs([(x**i, y**i) for i in [2, 4]]) == p_subbed + x, y = symbols('x y', complex=True) + p = 6*x**5 + x**4 - 4*x**3 + 4*x**2 - 2*x + 3 + p_subbed = 6*x**5 - 4*x**3 - 2*x + y**4 + 4*y**2 + 3 + assert p.subs([(x**i, y**i) for i in [2, 4]]) == p_subbed + + +def test_issue_6976(): + x, y = symbols('x y') + assert (sqrt(x)**3 + sqrt(x) + x + x**2).subs(sqrt(x), y) == \ + y**4 + y**3 + y**2 + y + assert (x**4 + x**3 + x**2 + x + sqrt(x)).subs(x**2, y) == \ + sqrt(x) + x**3 + x + y**2 + y + assert x.subs(x**3, y) == x + assert x.subs(x**Rational(1, 3), y) == y**3 + + # More substitutions are possible with nonnegative symbols + x, y = symbols('x y', nonnegative=True) + assert (x**4 + x**3 + x**2 + x + sqrt(x)).subs(x**2, y) == \ + y**Rational(1, 4) + y**Rational(3, 2) + sqrt(y) + y**2 + y + assert x.subs(x**3, y) == y**Rational(1, 3) + + +def test_issue_11746(): + assert (1/x).subs(x**2, 1) == 1/x + assert (1/(x**3)).subs(x**2, 1) == x**(-3) + assert (1/(x**4)).subs(x**2, 1) == 1 + assert (1/(x**3)).subs(x**4, 1) == x**(-3) + assert (1/(y**5)).subs(x**5, 1) == y**(-5) + + +def test_issue_17823(): + from sympy.physics.mechanics import dynamicsymbols + q1, q2 = dynamicsymbols('q1, q2') + expr = q1.diff().diff()**2*q1 + q1.diff()*q2.diff() + reps={q1: a, q1.diff(): a*x*y, q1.diff().diff(): z} + assert expr.subs(reps) == a*x*y*Derivative(q2, t) + a*z**2 + + +def test_issue_19326(): + x, y = [i(t) for i in map(Function, 'xy')] + assert (x*y).subs({x: 1 + x, y: x}) == (1 + x)*x + + +def test_issue_19558(): + e = (7*x*cos(x) - 12*log(x)**3)*(-log(x)**4 + 2*sin(x) + 1)**2/ \ + (2*(x*cos(x) - 2*log(x)**3)*(3*log(x)**4 - 7*sin(x) + 3)**2) + + assert e.subs(x, oo) == AccumBounds(-oo, oo) + assert (sin(x) + cos(x)).subs(x, oo) == AccumBounds(-2, 2) + + +def test_issue_22033(): + xr = Symbol('xr', real=True) + e = (1/xr) + assert e.subs(xr**2, y) == e + + +def test_guard_against_indeterminate_evaluation(): + eq = x**y + assert eq.subs([(x, 1), (y, oo)]) == 1 # because 1**y == 1 + assert eq.subs([(y, oo), (x, 1)]) is S.NaN + assert eq.subs({x: 1, y: oo}) is S.NaN + assert eq.subs([(x, 1), (y, oo)], simultaneous=True) is S.NaN diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_symbol.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_symbol.py new file mode 100644 index 0000000000000000000000000000000000000000..acf27700825c4822456207afe95108480505ce2c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_symbol.py @@ -0,0 +1,421 @@ +import threading + +from sympy.core.function import Function, UndefinedFunction +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.relational import (GreaterThan, LessThan, StrictGreaterThan, StrictLessThan) +from sympy.core.symbol import (Dummy, Symbol, Wild, symbols) +from sympy.core.sympify import sympify # can't import as S yet +from sympy.core.symbol import uniquely_named_symbol, _symbol, Str + +from sympy.testing.pytest import raises, skip_under_pyodide +from sympy.core.symbol import disambiguate + + +def test_Str(): + a1 = Str('a') + a2 = Str('a') + b = Str('b') + assert a1 == a2 != b + raises(TypeError, lambda: Str()) + + +def test_Symbol(): + a = Symbol("a") + x1 = Symbol("x") + x2 = Symbol("x") + xdummy1 = Dummy("x") + xdummy2 = Dummy("x") + + assert a != x1 + assert a != x2 + assert x1 == x2 + assert x1 != xdummy1 + assert xdummy1 != xdummy2 + + assert Symbol("x") == Symbol("x") + assert Dummy("x") != Dummy("x") + d = symbols('d', cls=Dummy) + assert isinstance(d, Dummy) + c, d = symbols('c,d', cls=Dummy) + assert isinstance(c, Dummy) + assert isinstance(d, Dummy) + raises(TypeError, lambda: Symbol()) + + +def test_Dummy(): + assert Dummy() != Dummy() + + +def test_Dummy_force_dummy_index(): + raises(AssertionError, lambda: Dummy(dummy_index=1)) + assert Dummy('d', dummy_index=2) == Dummy('d', dummy_index=2) + assert Dummy('d1', dummy_index=2) != Dummy('d2', dummy_index=2) + d1 = Dummy('d', dummy_index=3) + d2 = Dummy('d') + # might fail if d1 were created with dummy_index >= 10**6 + assert d1 != d2 + d3 = Dummy('d', dummy_index=3) + assert d1 == d3 + assert Dummy()._count == Dummy('d', dummy_index=3)._count + + +def test_lt_gt(): + S = sympify + x, y = Symbol('x'), Symbol('y') + + assert (x >= y) == GreaterThan(x, y) + assert (x >= 0) == GreaterThan(x, 0) + assert (x <= y) == LessThan(x, y) + assert (x <= 0) == LessThan(x, 0) + + assert (0 <= x) == GreaterThan(x, 0) + assert (0 >= x) == LessThan(x, 0) + assert (S(0) >= x) == GreaterThan(0, x) + assert (S(0) <= x) == LessThan(0, x) + + assert (x > y) == StrictGreaterThan(x, y) + assert (x > 0) == StrictGreaterThan(x, 0) + assert (x < y) == StrictLessThan(x, y) + assert (x < 0) == StrictLessThan(x, 0) + + assert (0 < x) == StrictGreaterThan(x, 0) + assert (0 > x) == StrictLessThan(x, 0) + assert (S(0) > x) == StrictGreaterThan(0, x) + assert (S(0) < x) == StrictLessThan(0, x) + + e = x**2 + 4*x + 1 + assert (e >= 0) == GreaterThan(e, 0) + assert (0 <= e) == GreaterThan(e, 0) + assert (e > 0) == StrictGreaterThan(e, 0) + assert (0 < e) == StrictGreaterThan(e, 0) + + assert (e <= 0) == LessThan(e, 0) + assert (0 >= e) == LessThan(e, 0) + assert (e < 0) == StrictLessThan(e, 0) + assert (0 > e) == StrictLessThan(e, 0) + + assert (S(0) >= e) == GreaterThan(0, e) + assert (S(0) <= e) == LessThan(0, e) + assert (S(0) < e) == StrictLessThan(0, e) + assert (S(0) > e) == StrictGreaterThan(0, e) + + +def test_no_len(): + # there should be no len for numbers + x = Symbol('x') + raises(TypeError, lambda: len(x)) + + +def test_ineq_unequal(): + S = sympify + x, y, z = symbols('x,y,z') + + e = ( + S(-1) >= x, S(-1) >= y, S(-1) >= z, + S(-1) > x, S(-1) > y, S(-1) > z, + S(-1) <= x, S(-1) <= y, S(-1) <= z, + S(-1) < x, S(-1) < y, S(-1) < z, + S(0) >= x, S(0) >= y, S(0) >= z, + S(0) > x, S(0) > y, S(0) > z, + S(0) <= x, S(0) <= y, S(0) <= z, + S(0) < x, S(0) < y, S(0) < z, + S('3/7') >= x, S('3/7') >= y, S('3/7') >= z, + S('3/7') > x, S('3/7') > y, S('3/7') > z, + S('3/7') <= x, S('3/7') <= y, S('3/7') <= z, + S('3/7') < x, S('3/7') < y, S('3/7') < z, + S(1.5) >= x, S(1.5) >= y, S(1.5) >= z, + S(1.5) > x, S(1.5) > y, S(1.5) > z, + S(1.5) <= x, S(1.5) <= y, S(1.5) <= z, + S(1.5) < x, S(1.5) < y, S(1.5) < z, + S(2) >= x, S(2) >= y, S(2) >= z, + S(2) > x, S(2) > y, S(2) > z, + S(2) <= x, S(2) <= y, S(2) <= z, + S(2) < x, S(2) < y, S(2) < z, + x >= -1, y >= -1, z >= -1, + x > -1, y > -1, z > -1, + x <= -1, y <= -1, z <= -1, + x < -1, y < -1, z < -1, + x >= 0, y >= 0, z >= 0, + x > 0, y > 0, z > 0, + x <= 0, y <= 0, z <= 0, + x < 0, y < 0, z < 0, + x >= 1.5, y >= 1.5, z >= 1.5, + x > 1.5, y > 1.5, z > 1.5, + x <= 1.5, y <= 1.5, z <= 1.5, + x < 1.5, y < 1.5, z < 1.5, + x >= 2, y >= 2, z >= 2, + x > 2, y > 2, z > 2, + x <= 2, y <= 2, z <= 2, + x < 2, y < 2, z < 2, + + x >= y, x >= z, y >= x, y >= z, z >= x, z >= y, + x > y, x > z, y > x, y > z, z > x, z > y, + x <= y, x <= z, y <= x, y <= z, z <= x, z <= y, + x < y, x < z, y < x, y < z, z < x, z < y, + + x - pi >= y + z, y - pi >= x + z, z - pi >= x + y, + x - pi > y + z, y - pi > x + z, z - pi > x + y, + x - pi <= y + z, y - pi <= x + z, z - pi <= x + y, + x - pi < y + z, y - pi < x + z, z - pi < x + y, + True, False + ) + + left_e = e[:-1] + for i, e1 in enumerate( left_e ): + for e2 in e[i + 1:]: + assert e1 != e2 + + +def test_Wild_properties(): + S = sympify + # these tests only include Atoms + x = Symbol("x") + y = Symbol("y") + p = Symbol("p", positive=True) + k = Symbol("k", integer=True) + n = Symbol("n", integer=True, positive=True) + + given_patterns = [ x, y, p, k, -k, n, -n, S(-3), S(3), + pi, Rational(3, 2), I ] + + integerp = lambda k: k.is_integer + positivep = lambda k: k.is_positive + symbolp = lambda k: k.is_Symbol + realp = lambda k: k.is_extended_real + + S = Wild("S", properties=[symbolp]) + R = Wild("R", properties=[realp]) + Y = Wild("Y", exclude=[x, p, k, n]) + P = Wild("P", properties=[positivep]) + K = Wild("K", properties=[integerp]) + N = Wild("N", properties=[positivep, integerp]) + + given_wildcards = [ S, R, Y, P, K, N ] + + goodmatch = { + S: (x, y, p, k, n), + R: (p, k, -k, n, -n, -3, 3, pi, Rational(3, 2)), + Y: (y, -3, 3, pi, Rational(3, 2), I ), + P: (p, n, 3, pi, Rational(3, 2)), + K: (k, -k, n, -n, -3, 3), + N: (n, 3)} + + for A in given_wildcards: + for pat in given_patterns: + d = pat.match(A) + if pat in goodmatch[A]: + assert d[A] in goodmatch[A] + else: + assert d is None + + +def test_symbols(): + x = Symbol('x') + y = Symbol('y') + z = Symbol('z') + + assert symbols('x') == x + assert symbols('x ') == x + assert symbols(' x ') == x + assert symbols('x,') == (x,) + assert symbols('x, ') == (x,) + assert symbols('x ,') == (x,) + + assert symbols('x , y') == (x, y) + + assert symbols('x,y,z') == (x, y, z) + assert symbols('x y z') == (x, y, z) + + assert symbols('x,y,z,') == (x, y, z) + assert symbols('x y z ') == (x, y, z) + + xyz = Symbol('xyz') + abc = Symbol('abc') + + assert symbols('xyz') == xyz + assert symbols('xyz,') == (xyz,) + assert symbols('xyz,abc') == (xyz, abc) + + assert symbols(('xyz',)) == (xyz,) + assert symbols(('xyz,',)) == ((xyz,),) + assert symbols(('x,y,z,',)) == ((x, y, z),) + assert symbols(('xyz', 'abc')) == (xyz, abc) + assert symbols(('xyz,abc',)) == ((xyz, abc),) + assert symbols(('xyz,abc', 'x,y,z')) == ((xyz, abc), (x, y, z)) + + assert symbols(('x', 'y', 'z')) == (x, y, z) + assert symbols(['x', 'y', 'z']) == [x, y, z] + assert symbols({'x', 'y', 'z'}) == {x, y, z} + + raises(ValueError, lambda: symbols('')) + raises(ValueError, lambda: symbols(',')) + raises(ValueError, lambda: symbols('x,,y,,z')) + raises(ValueError, lambda: symbols(('x', '', 'y', '', 'z'))) + + a, b = symbols('x,y', real=True) + assert a.is_real and b.is_real + + x0 = Symbol('x0') + x1 = Symbol('x1') + x2 = Symbol('x2') + + y0 = Symbol('y0') + y1 = Symbol('y1') + + assert symbols('x0:0') == () + assert symbols('x0:1') == (x0,) + assert symbols('x0:2') == (x0, x1) + assert symbols('x0:3') == (x0, x1, x2) + + assert symbols('x:0') == () + assert symbols('x:1') == (x0,) + assert symbols('x:2') == (x0, x1) + assert symbols('x:3') == (x0, x1, x2) + + assert symbols('x1:1') == () + assert symbols('x1:2') == (x1,) + assert symbols('x1:3') == (x1, x2) + + assert symbols('x1:3,x,y,z') == (x1, x2, x, y, z) + + assert symbols('x:3,y:2') == (x0, x1, x2, y0, y1) + assert symbols(('x:3', 'y:2')) == ((x0, x1, x2), (y0, y1)) + + a = Symbol('a') + b = Symbol('b') + c = Symbol('c') + d = Symbol('d') + + assert symbols('x:z') == (x, y, z) + assert symbols('a:d,x:z') == (a, b, c, d, x, y, z) + assert symbols(('a:d', 'x:z')) == ((a, b, c, d), (x, y, z)) + + aa = Symbol('aa') + ab = Symbol('ab') + ac = Symbol('ac') + ad = Symbol('ad') + + assert symbols('aa:d') == (aa, ab, ac, ad) + assert symbols('aa:d,x:z') == (aa, ab, ac, ad, x, y, z) + assert symbols(('aa:d','x:z')) == ((aa, ab, ac, ad), (x, y, z)) + + assert type(symbols(('q:2', 'u:2'), cls=Function)[0][0]) == UndefinedFunction # issue 23532 + + # issue 6675 + def sym(s): + return str(symbols(s)) + assert sym('a0:4') == '(a0, a1, a2, a3)' + assert sym('a2:4,b1:3') == '(a2, a3, b1, b2)' + assert sym('a1(2:4)') == '(a12, a13)' + assert sym('a0:2.0:2') == '(a0.0, a0.1, a1.0, a1.1)' + assert sym('aa:cz') == '(aaz, abz, acz)' + assert sym('aa:c0:2') == '(aa0, aa1, ab0, ab1, ac0, ac1)' + assert sym('aa:ba:b') == '(aaa, aab, aba, abb)' + assert sym('a:3b') == '(a0b, a1b, a2b)' + assert sym('a-1:3b') == '(a-1b, a-2b)' + assert sym(r'a:2\,:2' + chr(0)) == '(a0,0%s, a0,1%s, a1,0%s, a1,1%s)' % ( + (chr(0),)*4) + assert sym('x(:a:3)') == '(x(a0), x(a1), x(a2))' + assert sym('x(:c):1') == '(xa0, xb0, xc0)' + assert sym('x((:a)):3') == '(x(a)0, x(a)1, x(a)2)' + assert sym('x(:a:3') == '(x(a0, x(a1, x(a2)' + assert sym(':2') == '(0, 1)' + assert sym(':b') == '(a, b)' + assert sym(':b:2') == '(a0, a1, b0, b1)' + assert sym(':2:2') == '(00, 01, 10, 11)' + assert sym(':b:b') == '(aa, ab, ba, bb)' + + raises(ValueError, lambda: symbols(':')) + raises(ValueError, lambda: symbols('a:')) + raises(ValueError, lambda: symbols('::')) + raises(ValueError, lambda: symbols('a::')) + raises(ValueError, lambda: symbols(':a:')) + raises(ValueError, lambda: symbols('::a')) + + +def test_symbols_become_functions_issue_3539(): + from sympy.abc import alpha, phi, beta, t + raises(TypeError, lambda: beta(2)) + raises(TypeError, lambda: beta(2.5)) + raises(TypeError, lambda: phi(2.5)) + raises(TypeError, lambda: alpha(2.5)) + raises(TypeError, lambda: phi(t)) + + +def test_unicode(): + xu = Symbol('x') + x = Symbol('x') + assert x == xu + + raises(TypeError, lambda: Symbol(1)) + + +def test_uniquely_named_symbol_and_Symbol(): + F = uniquely_named_symbol + x = Symbol('x') + assert F(x) == x + assert F('x') == x + assert str(F('x', x)) == 'x0' + assert str(F('x', (x + 1, 1/x))) == 'x0' + _x = Symbol('x', real=True) + assert F(('x', _x)) == _x + assert F((x, _x)) == _x + assert F('x', real=True).is_real + y = Symbol('y') + assert F(('x', y), real=True).is_real + r = Symbol('x', real=True) + assert F(('x', r)).is_real + assert F(('x', r), real=False).is_real + assert F('x1', Symbol('x1'), + compare=lambda i: str(i).rstrip('1')).name == 'x0' + assert F('x1', Symbol('x1'), + modify=lambda i: i + '_').name == 'x1_' + assert _symbol(x, _x) == x + + +def test_disambiguate(): + x, y, y_1, _x, x_1, x_2 = symbols('x y y_1 _x x_1 x_2') + t1 = Dummy('y'), _x, Dummy('x'), Dummy('x') + t2 = Dummy('x'), Dummy('x') + t3 = Dummy('x'), Dummy('y') + t4 = x, Dummy('x') + t5 = Symbol('x', integer=True), x, Symbol('x_1') + + assert disambiguate(*t1) == (y, x_2, x, x_1) + assert disambiguate(*t2) == (x, x_1) + assert disambiguate(*t3) == (x, y) + assert disambiguate(*t4) == (x_1, x) + assert disambiguate(*t5) == (t5[0], x_2, x_1) + assert disambiguate(*t5)[0] != x # assumptions are retained + + t6 = _x, Dummy('x')/y + t7 = y*Dummy('y'), y + + assert disambiguate(*t6) == (x_1, x/y) + assert disambiguate(*t7) == (y*y_1, y_1) + assert disambiguate(Dummy('x_1'), Dummy('x_1') + ) == (x_1, Symbol('x_1_1')) + + +@skip_under_pyodide("Cannot create threads under pyodide.") +def test_issue_gh_16734(): + # https://github.com/sympy/sympy/issues/16734 + + syms = list(symbols('x, y')) + + def thread1(): + for n in range(1000): + syms[0], syms[1] = symbols(f'x{n}, y{n}') + syms[0].is_positive # Check an assumption in this thread. + syms[0] = None + + def thread2(): + while syms[0] is not None: + # Compare the symbol in this thread. + result = (syms[0] == syms[1]) # noqa + + # Previously this would be very likely to raise an exception: + thread = threading.Thread(target=thread1) + thread.start() + thread2() + thread.join() diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_sympify.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_sympify.py new file mode 100644 index 0000000000000000000000000000000000000000..40be30c25d5826ceadde6d176c160a0090967659 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_sympify.py @@ -0,0 +1,892 @@ +from sympy.core.add import Add +from sympy.core.containers import Tuple +from sympy.core.function import (Function, Lambda) +from sympy.core.mul import Mul +from sympy.core.numbers import (Float, I, Integer, Rational, pi, oo) +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.logic.boolalg import (false, Or, true, Xor) +from sympy.matrices.dense import Matrix +from sympy.parsing.sympy_parser import null +from sympy.polys.polytools import Poly +from sympy.printing.repr import srepr +from sympy.sets.fancysets import Range +from sympy.sets.sets import Interval +from sympy.abc import x, y +from sympy.core.sympify import (sympify, _sympify, SympifyError, kernS, + CantSympify, converter) +from sympy.core.decorators import _sympifyit +from sympy.external import import_module +from sympy.testing.pytest import raises, XFAIL, skip +from sympy.utilities.decorator import conserve_mpmath_dps +from sympy.geometry import Point, Line +from sympy.functions.combinatorial.factorials import factorial, factorial2 +from sympy.abc import _clash, _clash1, _clash2 +from sympy.external.gmpy import gmpy as _gmpy, flint as _flint +from sympy.sets import FiniteSet, EmptySet +from sympy.tensor.array.dense_ndim_array import ImmutableDenseNDimArray + +import mpmath +from collections import defaultdict, OrderedDict + + +numpy = import_module('numpy') + + +def test_issue_3538(): + v = sympify("exp(x)") + assert v == exp(x) + assert type(v) == type(exp(x)) + assert str(type(v)) == str(type(exp(x))) + + +def test_sympify1(): + assert sympify("x") == Symbol("x") + assert sympify(" x") == Symbol("x") + assert sympify(" x ") == Symbol("x") + # issue 4877 + assert sympify('--.5') == 0.5 + assert sympify('-1/2') == -S.Half + assert sympify('-+--.5') == -0.5 + assert sympify('-.[3]') == Rational(-1, 3) + assert sympify('.[3]') == Rational(1, 3) + assert sympify('+.[3]') == Rational(1, 3) + assert sympify('+0.[3]*10**-2') == Rational(1, 300) + assert sympify('.[052631578947368421]') == Rational(1, 19) + assert sympify('.0[526315789473684210]') == Rational(1, 19) + assert sympify('.034[56]') == Rational(1711, 49500) + # options to make reals into rationals + assert sympify('1.22[345]', rational=True) == \ + 1 + Rational(22, 100) + Rational(345, 99900) + assert sympify('2/2.6', rational=True) == Rational(10, 13) + assert sympify('2.6/2', rational=True) == Rational(13, 10) + assert sympify('2.6e2/17', rational=True) == Rational(260, 17) + assert sympify('2.6e+2/17', rational=True) == Rational(260, 17) + assert sympify('2.6e-2/17', rational=True) == Rational(26, 17000) + assert sympify('2.1+3/4', rational=True) == \ + Rational(21, 10) + Rational(3, 4) + assert sympify('2.234456', rational=True) == Rational(279307, 125000) + assert sympify('2.234456e23', rational=True) == 223445600000000000000000 + assert sympify('2.234456e-23', rational=True) == \ + Rational(279307, 12500000000000000000000000000) + assert sympify('-2.234456e-23', rational=True) == \ + Rational(-279307, 12500000000000000000000000000) + assert sympify('12345678901/17', rational=True) == \ + Rational(12345678901, 17) + assert sympify('1/.3 + x', rational=True) == Rational(10, 3) + x + # make sure longs in fractions work + assert sympify('222222222222/11111111111') == \ + Rational(222222222222, 11111111111) + # ... even if they come from repetend notation + assert sympify('1/.2[123456789012]') == Rational(333333333333, 70781892967) + # ... or from high precision reals + assert sympify('.1234567890123456', rational=True) == \ + Rational(19290123283179, 156250000000000) + + +def test_sympify_Fraction(): + try: + import fractions + except ImportError: + pass + else: + value = sympify(fractions.Fraction(101, 127)) + assert value == Rational(101, 127) and type(value) is Rational + + +def test_sympify_gmpy(): + if _gmpy is not None: + import gmpy2 + + value = sympify(gmpy2.mpz(1000001)) + assert value == Integer(1000001) and type(value) is Integer + + value = sympify(gmpy2.mpq(101, 127)) + assert value == Rational(101, 127) and type(value) is Rational + + +def test_sympify_flint(): + if _flint is not None: + import flint + + value = sympify(flint.fmpz(1000001)) + assert value == Integer(1000001) and type(value) is Integer + + value = sympify(flint.fmpq(101, 127)) + assert value == Rational(101, 127) and type(value) is Rational + + +@conserve_mpmath_dps +def test_sympify_mpmath(): + value = sympify(mpmath.mpf(1.0)) + assert value == Float(1.0) and type(value) is Float + + mpmath.mp.dps = 12 + assert sympify( + mpmath.pi).epsilon_eq(Float("3.14159265359"), Float("1e-12")) == True + assert sympify( + mpmath.pi).epsilon_eq(Float("3.14159265359"), Float("1e-13")) == False + + mpmath.mp.dps = 6 + assert sympify( + mpmath.pi).epsilon_eq(Float("3.14159"), Float("1e-5")) == True + assert sympify( + mpmath.pi).epsilon_eq(Float("3.14159"), Float("1e-6")) == False + + mpmath.mp.dps = 15 + assert sympify(mpmath.mpc(1.0 + 2.0j)) == Float(1.0) + Float(2.0)*I + + +def test_sympify2(): + class A: + def _sympy_(self): + return Symbol("x")**3 + + a = A() + + assert _sympify(a) == x**3 + assert sympify(a) == x**3 + assert a == x**3 + + +def test_sympify3(): + assert sympify("x**3") == x**3 + assert sympify("x^3") == x**3 + assert sympify("1/2") == Integer(1)/2 + + raises(SympifyError, lambda: _sympify('x**3')) + raises(SympifyError, lambda: _sympify('1/2')) + + +def test_sympify_keywords(): + raises(SympifyError, lambda: sympify('if')) + raises(SympifyError, lambda: sympify('for')) + raises(SympifyError, lambda: sympify('while')) + raises(SympifyError, lambda: sympify('lambda')) + + +def test_sympify_float(): + assert sympify("1e-64") != 0 + assert sympify("1e-20000") != 0 + + +def test_sympify_bool(): + assert sympify(True) is true + assert sympify(False) is false + + +def test_sympify_iterables(): + ans = [Rational(3, 10), Rational(1, 5)] + assert sympify(['.3', '.2'], rational=True) == ans + assert sympify({"x": 0, "y": 1}) == {x: 0, y: 1} + assert sympify(['1', '2', ['3', '4']]) == [S(1), S(2), [S(3), S(4)]] + + +@XFAIL +def test_issue_16772(): + # because there is a converter for tuple, the + # args are only sympified without the flags being passed + # along; list, on the other hand, is not converted + # with a converter so its args are traversed later + ans = [Rational(3, 10), Rational(1, 5)] + assert sympify(('.3', '.2'), rational=True) == Tuple(*ans) + + +def test_issue_16859(): + class no(float, CantSympify): + pass + raises(SympifyError, lambda: sympify(no(1.2))) + + +def test_sympify4(): + class A: + def _sympy_(self): + return Symbol("x") + + a = A() + + assert _sympify(a)**3 == x**3 + assert sympify(a)**3 == x**3 + assert a == x + + +def test_sympify_text(): + assert sympify('some') == Symbol('some') + assert sympify('core') == Symbol('core') + + assert sympify('True') is True + assert sympify('False') is False + + assert sympify('Poly') == Poly + assert sympify('sin') == sin + + +def test_sympify_function(): + assert sympify('factor(x**2-1, x)') == -(1 - x)*(x + 1) + assert sympify('sin(pi/2)*cos(pi)') == -Integer(1) + + +def test_sympify_poly(): + p = Poly(x**2 + x + 1, x) + + assert _sympify(p) is p + assert sympify(p) is p + + +def test_sympify_factorial(): + assert sympify('x!') == factorial(x) + assert sympify('(x+1)!') == factorial(x + 1) + assert sympify('(1 + y*(x + 1))!') == factorial(1 + y*(x + 1)) + assert sympify('(1 + y*(x + 1)!)^2') == (1 + y*factorial(x + 1))**2 + assert sympify('y*x!') == y*factorial(x) + assert sympify('x!!') == factorial2(x) + assert sympify('(x+1)!!') == factorial2(x + 1) + assert sympify('(1 + y*(x + 1))!!') == factorial2(1 + y*(x + 1)) + assert sympify('(1 + y*(x + 1)!!)^2') == (1 + y*factorial2(x + 1))**2 + assert sympify('y*x!!') == y*factorial2(x) + assert sympify('factorial2(x)!') == factorial(factorial2(x)) + + raises(SympifyError, lambda: sympify("+!!")) + raises(SympifyError, lambda: sympify(")!!")) + raises(SympifyError, lambda: sympify("!")) + raises(SympifyError, lambda: sympify("(!)")) + raises(SympifyError, lambda: sympify("x!!!")) + + +def test_issue_3595(): + assert sympify("a_") == Symbol("a_") + assert sympify("_a") == Symbol("_a") + + +def test_lambda(): + x = Symbol('x') + assert sympify('lambda: 1') == Lambda((), 1) + assert sympify('lambda x: x') == Lambda(x, x) + assert sympify('lambda x: 2*x') == Lambda(x, 2*x) + assert sympify('lambda x, y: 2*x+y') == Lambda((x, y), 2*x + y) + + +def test_lambda_raises(): + raises(SympifyError, lambda: sympify("lambda *args: args")) # args argument error + raises(SympifyError, lambda: sympify("lambda **kwargs: kwargs[0]")) # kwargs argument error + raises(SympifyError, lambda: sympify("lambda x = 1: x")) # Keyword argument error + with raises(SympifyError): + _sympify('lambda: 1') + + +def test_sympify_raises(): + raises(SympifyError, lambda: sympify("fx)")) + + class A: + def __str__(self): + return 'x' + + raises(SympifyError, lambda: sympify(A())) + + +def test__sympify(): + x = Symbol('x') + f = Function('f') + + # positive _sympify + assert _sympify(x) is x + assert _sympify(1) == Integer(1) + assert _sympify(0.5) == Float("0.5") + assert _sympify(1 + 1j) == 1.0 + I*1.0 + + # Function f is not Basic and can't sympify to Basic. We allow it to pass + # with sympify but not with _sympify. + # https://github.com/sympy/sympy/issues/20124 + assert sympify(f) is f + raises(SympifyError, lambda: _sympify(f)) + + class A: + def _sympy_(self): + return Integer(5) + + a = A() + assert _sympify(a) == Integer(5) + + # negative _sympify + raises(SympifyError, lambda: _sympify('1')) + raises(SympifyError, lambda: _sympify([1, 2, 3])) + + +def test_sympifyit(): + x = Symbol('x') + y = Symbol('y') + + @_sympifyit('b', NotImplemented) + def add(a, b): + return a + b + + assert add(x, 1) == x + 1 + assert add(x, 0.5) == x + Float('0.5') + assert add(x, y) == x + y + + assert add(x, '1') == NotImplemented + + @_sympifyit('b') + def add_raises(a, b): + return a + b + + assert add_raises(x, 1) == x + 1 + assert add_raises(x, 0.5) == x + Float('0.5') + assert add_raises(x, y) == x + y + + raises(SympifyError, lambda: add_raises(x, '1')) + + +def test_int_float(): + class F1_1: + def __float__(self): + return 1.1 + + class F1_1b: + """ + This class is still a float, even though it also implements __int__(). + """ + def __float__(self): + return 1.1 + + def __int__(self): + return 1 + + class F1_1c: + """ + This class is still a float, because it implements _sympy_() + """ + def __float__(self): + return 1.1 + + def __int__(self): + return 1 + + def _sympy_(self): + return Float(1.1) + + class I5: + def __int__(self): + return 5 + + class I5b: + """ + This class implements both __int__() and __float__(), so it will be + treated as Float in SymPy. One could change this behavior, by using + float(a) == int(a), but deciding that integer-valued floats represent + exact numbers is arbitrary and often not correct, so we do not do it. + If, in the future, we decide to do it anyway, the tests for I5b need to + be changed. + """ + def __float__(self): + return 5.0 + + def __int__(self): + return 5 + + class I5c: + """ + This class implements both __int__() and __float__(), but also + a _sympy_() method, so it will be Integer. + """ + def __float__(self): + return 5.0 + + def __int__(self): + return 5 + + def _sympy_(self): + return Integer(5) + + i5 = I5() + i5b = I5b() + i5c = I5c() + f1_1 = F1_1() + f1_1b = F1_1b() + f1_1c = F1_1c() + assert sympify(i5) == 5 + assert isinstance(sympify(i5), Integer) + assert sympify(i5b) == 5.0 + assert isinstance(sympify(i5b), Float) + assert sympify(i5c) == 5 + assert isinstance(sympify(i5c), Integer) + assert abs(sympify(f1_1) - 1.1) < 1e-5 + assert abs(sympify(f1_1b) - 1.1) < 1e-5 + assert abs(sympify(f1_1c) - 1.1) < 1e-5 + + assert _sympify(i5) == 5 + assert isinstance(_sympify(i5), Integer) + assert _sympify(i5b) == 5.0 + assert isinstance(_sympify(i5b), Float) + assert _sympify(i5c) == 5 + assert isinstance(_sympify(i5c), Integer) + assert abs(_sympify(f1_1) - 1.1) < 1e-5 + assert abs(_sympify(f1_1b) - 1.1) < 1e-5 + assert abs(_sympify(f1_1c) - 1.1) < 1e-5 + + +def test_evaluate_false(): + cases = { + '2 + 3': Add(2, 3, evaluate=False), + '2**2 / 3': Mul(Pow(2, 2, evaluate=False), Pow(3, -1, evaluate=False), evaluate=False), + '2 + 3 * 5': Add(2, Mul(3, 5, evaluate=False), evaluate=False), + '2 - 3 * 5': Add(2, Mul(-1, Mul(3, 5,evaluate=False), evaluate=False), evaluate=False), + '1 / 3': Mul(1, Pow(3, -1, evaluate=False), evaluate=False), + 'True | False': Or(True, False, evaluate=False), + '1 + 2 + 3 + 5*3 + integrate(x)': Add(1, 2, 3, Mul(5, 3, evaluate=False), x**2/2, evaluate=False), + '2 * 4 * 6 + 8': Add(Mul(2, 4, 6, evaluate=False), 8, evaluate=False), + '2 - 8 / 4': Add(2, Mul(-1, Mul(8, Pow(4, -1, evaluate=False), evaluate=False), evaluate=False), evaluate=False), + '2 - 2**2': Add(2, Mul(-1, Pow(2, 2, evaluate=False), evaluate=False), evaluate=False), + } + for case, result in cases.items(): + assert sympify(case, evaluate=False) == result + + +def test_issue_4133(): + a = sympify('Integer(4)') + + assert a == Integer(4) + assert a.is_Integer + + +def test_issue_3982(): + a = [3, 2.0] + assert sympify(a) == [Integer(3), Float(2.0)] + assert sympify(tuple(a)) == Tuple(Integer(3), Float(2.0)) + assert sympify(set(a)) == FiniteSet(Integer(3), Float(2.0)) + + +def test_S_sympify(): + assert S(1)/2 == sympify(1)/2 == S.Half + assert (-2)**(S(1)/2) == sqrt(2)*I + + +def test_issue_4788(): + assert srepr(S(1.0 + 0J)) == srepr(S(1.0)) == srepr(Float(1.0)) + + +def test_issue_4798_None(): + assert S(None) is None + + +def test_issue_3218(): + assert sympify("x+\ny") == x + y + +def test_issue_19399(): + if not numpy: + skip("numpy not installed.") + + a = numpy.array(Rational(1, 2)) + b = Rational(1, 3) + assert (a * b, type(a * b)) == (b * a, type(b * a)) + + +def test_issue_4988_builtins(): + C = Symbol('C') + vars = {'C': C} + exp1 = sympify('C') + assert exp1 == C # Make sure it did not get mixed up with sympy.C + + exp2 = sympify('C', vars) + assert exp2 == C # Make sure it did not get mixed up with sympy.C + + +def test_geometry(): + p = sympify(Point(0, 1)) + assert p == Point(0, 1) and isinstance(p, Point) + L = sympify(Line(p, (1, 0))) + assert L == Line((0, 1), (1, 0)) and isinstance(L, Line) + + +def test_kernS(): + s = '-1 - 2*(-(-x + 1/x)/(x*(x - 1/x)**2) - 1/(x*(x - 1/x)))' + # when 1497 is fixed, this no longer should pass: the expression + # should be unchanged + assert -1 - 2*(-(-x + 1/x)/(x*(x - 1/x)**2) - 1/(x*(x - 1/x))) == -1 + # sympification should not allow the constant to enter a Mul + # or else the structure can change dramatically + ss = kernS(s) + assert ss != -1 and ss.simplify() == -1 + s = '-1 - 2*(-(-x + 1/x)/(x*(x - 1/x)**2) - 1/(x*(x - 1/x)))'.replace( + 'x', '_kern') + ss = kernS(s) + assert ss != -1 and ss.simplify() == -1 + # issue 6687 + assert (kernS('Interval(-1,-2 - 4*(-3))') + == Interval(-1, Add(-2, Mul(12, 1, evaluate=False), evaluate=False))) + assert kernS('_kern') == Symbol('_kern') + assert kernS('E**-(x)') == exp(-x) + e = 2*(x + y)*y + assert kernS(['2*(x + y)*y', ('2*(x + y)*y',)]) == [e, (e,)] + assert kernS('-(2*sin(x)**2 + 2*sin(x)*cos(x))*y/2') == \ + -y*(2*sin(x)**2 + 2*sin(x)*cos(x))/2 + # issue 15132 + assert kernS('(1 - x)/(1 - x*(1-y))') == kernS('(1-x)/(1-(1-y)*x)') + assert kernS('(1-2**-(4+1)*(1-y)*x)') == (1 - x*(1 - y)/32) + assert kernS('(1-2**(4+1)*(1-y)*x)') == (1 - 32*x*(1 - y)) + assert kernS('(1-2.*(1-y)*x)') == 1 - 2.*x*(1 - y) + one = kernS('x - (x - 1)') + assert one != 1 and one.expand() == 1 + assert kernS("(2*x)/(x-1)") == 2*x/(x-1) + + +def test_issue_6540_6552(): + assert S('[[1/3,2], (2/5,)]') == [[Rational(1, 3), 2], (Rational(2, 5),)] + assert S('[[2/6,2], (2/4,)]') == [[Rational(1, 3), 2], (S.Half,)] + assert S('[[[2*(1)]]]') == [[[2]]] + assert S('Matrix([2*(1)])') == Matrix([2]) + + +def test_issue_6046(): + assert str(S("Q & C", locals=_clash1)) == 'C & Q' + assert str(S('pi(x)', locals=_clash2)) == 'pi(x)' + locals = {} + exec("from sympy.abc import Q, C", locals) + assert str(S('C&Q', locals)) == 'C & Q' + # clash can act as Symbol or Function + assert str(S('pi(C, Q)', locals=_clash)) == 'pi(C, Q)' + assert len(S('pi + x', locals=_clash2).free_symbols) == 2 + # but not both + raises(TypeError, lambda: S('pi + pi(x)', locals=_clash2)) + assert all(set(i.values()) == {null} for i in ( + _clash, _clash1, _clash2)) + + +def test_issue_8821_highprec_from_str(): + s = str(pi.evalf(128)) + p = sympify(s) + assert Abs(sin(p)) < 1e-127 + + +def test_issue_10295(): + if not numpy: + skip("numpy not installed.") + + A = numpy.array([[1, 3, -1], + [0, 1, 7]]) + sA = S(A) + assert sA.shape == (2, 3) + for (ri, ci), val in numpy.ndenumerate(A): + assert sA[ri, ci] == val + + B = numpy.array([-7, x, 3*y**2]) + sB = S(B) + assert sB.shape == (3,) + assert B[0] == sB[0] == -7 + assert B[1] == sB[1] == x + assert B[2] == sB[2] == 3*y**2 + + C = numpy.arange(0, 24) + C.resize(2,3,4) + sC = S(C) + assert sC[0, 0, 0].is_integer + assert sC[0, 0, 0] == 0 + + a1 = numpy.array([1, 2, 3]) + a2 = numpy.array(list(range(24))) + a2.resize(2, 4, 3) + assert sympify(a1) == ImmutableDenseNDimArray([1, 2, 3]) + assert sympify(a2) == ImmutableDenseNDimArray(list(range(24)), (2, 4, 3)) + + +def test_Range(): + # Only works in Python 3 where range returns a range type + assert sympify(range(10)) == Range(10) + assert _sympify(range(10)) == Range(10) + + +def test_sympify_set(): + n = Symbol('n') + assert sympify({n}) == FiniteSet(n) + assert sympify(set()) == EmptySet + + +def test_sympify_numpy(): + if not numpy: + skip('numpy not installed. Abort numpy tests.') + np = numpy + + def equal(x, y): + return x == y and type(x) == type(y) + + assert sympify(np.bool_(1)) is S(True) + try: + assert equal( + sympify(np.int_(1234567891234567891)), S(1234567891234567891)) + assert equal( + sympify(np.intp(1234567891234567891)), S(1234567891234567891)) + except OverflowError: + # May fail on 32-bit systems: Python int too large to convert to C long + pass + assert equal(sympify(np.intc(1234567891)), S(1234567891)) + assert equal(sympify(np.int8(-123)), S(-123)) + assert equal(sympify(np.int16(-12345)), S(-12345)) + assert equal(sympify(np.int32(-1234567891)), S(-1234567891)) + assert equal( + sympify(np.int64(-1234567891234567891)), S(-1234567891234567891)) + assert equal(sympify(np.uint8(123)), S(123)) + assert equal(sympify(np.uint16(12345)), S(12345)) + assert equal(sympify(np.uint32(1234567891)), S(1234567891)) + assert equal( + sympify(np.uint64(1234567891234567891)), S(1234567891234567891)) + assert equal(sympify(np.float32(1.123456)), Float(1.123456, precision=24)) + assert equal(sympify(np.float64(1.1234567891234)), + Float(1.1234567891234, precision=53)) + + # The exact precision of np.longdouble, npfloat128 and other extended + # precision dtypes is platform dependent. + ldprec = np.finfo(np.longdouble(1)).nmant + 1 + assert equal(sympify(np.longdouble(1.123456789)), + Float(1.123456789, precision=ldprec)) + + assert equal(sympify(np.complex64(1 + 2j)), S(1.0 + 2.0*I)) + assert equal(sympify(np.complex128(1 + 2j)), S(1.0 + 2.0*I)) + + lcprec = np.finfo(np.clongdouble(1)).nmant + 1 + assert equal(sympify(np.clongdouble(1 + 2j)), + Float(1.0, precision=lcprec) + Float(2.0, precision=lcprec)*I) + + #float96 does not exist on all platforms + if hasattr(np, 'float96'): + f96prec = np.finfo(np.float96(1)).nmant + 1 + assert equal(sympify(np.float96(1.123456789)), + Float(1.123456789, precision=f96prec)) + + #float128 does not exist on all platforms + if hasattr(np, 'float128'): + f128prec = np.finfo(np.float128(1)).nmant + 1 + assert equal(sympify(np.float128(1.123456789123)), + Float(1.123456789123, precision=f128prec)) + + +@XFAIL +def test_sympify_rational_numbers_set(): + ans = [Rational(3, 10), Rational(1, 5)] + assert sympify({'.3', '.2'}, rational=True) == FiniteSet(*ans) + + +def test_sympify_mro(): + """Tests the resolution order for classes that implement _sympy_""" + class a: + def _sympy_(self): + return Integer(1) + class b(a): + def _sympy_(self): + return Integer(2) + class c(a): + pass + + assert sympify(a()) == Integer(1) + assert sympify(b()) == Integer(2) + assert sympify(c()) == Integer(1) + + +def test_sympify_converter(): + """Tests the resolution order for classes in converter""" + class a: + pass + class b(a): + pass + class c(a): + pass + + converter[a] = lambda x: Integer(1) + converter[b] = lambda x: Integer(2) + + assert sympify(a()) == Integer(1) + assert sympify(b()) == Integer(2) + assert sympify(c()) == Integer(1) + + class MyInteger(Integer): + pass + + if int in converter: + int_converter = converter[int] + else: + int_converter = None + + try: + converter[int] = MyInteger + assert sympify(1) == MyInteger(1) + finally: + if int_converter is None: + del converter[int] + else: + converter[int] = int_converter + + +def test_issue_13924(): + if not numpy: + skip("numpy not installed.") + + a = sympify(numpy.array([1])) + assert isinstance(a, ImmutableDenseNDimArray) + assert a[0] == 1 + + +def test_numpy_sympify_args(): + # Issue 15098. Make sure sympify args work with numpy types (like numpy.str_) + if not numpy: + skip("numpy not installed.") + + a = sympify(numpy.str_('a')) + assert type(a) is Symbol + assert a == Symbol('a') + + class CustomSymbol(Symbol): + pass + + a = sympify(numpy.str_('a'), {"Symbol": CustomSymbol}) + assert isinstance(a, CustomSymbol) + + a = sympify(numpy.str_('x^y')) + assert a == x**y + a = sympify(numpy.str_('x^y'), convert_xor=False) + assert a == Xor(x, y) + + raises(SympifyError, lambda: sympify(numpy.str_('x'), strict=True)) + + a = sympify(numpy.str_('1.1')) + assert isinstance(a, Float) + assert a == 1.1 + + a = sympify(numpy.str_('1.1'), rational=True) + assert isinstance(a, Rational) + assert a == Rational(11, 10) + + a = sympify(numpy.str_('x + x')) + assert isinstance(a, Mul) + assert a == 2*x + + a = sympify(numpy.str_('x + x'), evaluate=False) + assert isinstance(a, Add) + assert a == Add(x, x, evaluate=False) + + +def test_issue_5939(): + a = Symbol('a') + b = Symbol('b') + assert sympify('''a+\nb''') == a + b + + +def test_issue_16759(): + d = sympify({.5: 1}) + assert S.Half not in d + assert Float(.5) in d + assert d[.5] is S.One + d = sympify(OrderedDict({.5: 1})) + assert S.Half not in d + assert Float(.5) in d + assert d[.5] is S.One + d = sympify(defaultdict(int, {.5: 1})) + assert S.Half not in d + assert Float(.5) in d + assert d[.5] is S.One + + +def test_issue_17811(): + a = Function('a') + assert sympify('a(x)*5', evaluate=False) == Mul(a(x), 5, evaluate=False) + + +def test_issue_8439(): + assert sympify(float('inf')) == oo + assert x + float('inf') == x + oo + assert S(float('inf')) == oo + + +def test_issue_14706(): + if not numpy: + skip("numpy not installed.") + + z1 = numpy.zeros((1, 1), dtype=numpy.float64) + z2 = numpy.zeros((2, 2), dtype=numpy.float64) + z3 = numpy.zeros((), dtype=numpy.float64) + + y1 = numpy.ones((1, 1), dtype=numpy.float64) + y2 = numpy.ones((2, 2), dtype=numpy.float64) + y3 = numpy.ones((), dtype=numpy.float64) + + assert numpy.all(x + z1 == numpy.full((1, 1), x)) + assert numpy.all(x + z2 == numpy.full((2, 2), x)) + assert numpy.all(z1 + x == numpy.full((1, 1), x)) + assert numpy.all(z2 + x == numpy.full((2, 2), x)) + for z in [z3, + numpy.int64(0), + numpy.float64(0), + numpy.complex64(0)]: + assert x + z == x + assert z + x == x + assert isinstance(x + z, Symbol) + assert isinstance(z + x, Symbol) + + # If these tests fail, then it means that numpy has finally + # fixed the issue of scalar conversion for rank>0 arrays + # which is mentioned in numpy/numpy#10404. In that case, + # some changes have to be made in sympify.py. + # Note: For future reference, for anyone who takes up this + # issue when numpy has finally fixed their side of the problem, + # the changes for this temporary fix were introduced in PR 18651 + assert numpy.all(x + y1 == numpy.full((1, 1), x + 1.0)) + assert numpy.all(x + y2 == numpy.full((2, 2), x + 1.0)) + assert numpy.all(y1 + x == numpy.full((1, 1), x + 1.0)) + assert numpy.all(y2 + x == numpy.full((2, 2), x + 1.0)) + for y_ in [y3, + numpy.int64(1), + numpy.float64(1), + numpy.complex64(1)]: + assert x + y_ == y_ + x + assert isinstance(x + y_, Add) + assert isinstance(y_ + x, Add) + + assert x + numpy.array(x) == 2 * x + assert x + numpy.array([x]) == numpy.array([2*x], dtype=object) + + assert sympify(numpy.array([1])) == ImmutableDenseNDimArray([1], 1) + assert sympify(numpy.array([[[1]]])) == ImmutableDenseNDimArray([1], (1, 1, 1)) + assert sympify(z1) == ImmutableDenseNDimArray([0.0], (1, 1)) + assert sympify(z2) == ImmutableDenseNDimArray([0.0, 0.0, 0.0, 0.0], (2, 2)) + assert sympify(z3) == ImmutableDenseNDimArray([0.0], ()) + assert sympify(z3, strict=True) == 0.0 + + raises(SympifyError, lambda: sympify(numpy.array([1]), strict=True)) + raises(SympifyError, lambda: sympify(z1, strict=True)) + raises(SympifyError, lambda: sympify(z2, strict=True)) + + +def test_issue_21536(): + #test to check evaluate=False in case of iterable input + u = sympify("x+3*x+2", evaluate=False) + v = sympify("2*x+4*x+2+4", evaluate=False) + + assert u.is_Add and set(u.args) == {x, 3*x, 2} + assert v.is_Add and set(v.args) == {2*x, 4*x, 2, 4} + assert sympify(["x+3*x+2", "2*x+4*x+2+4"], evaluate=False) == [u, v] + + #test to check evaluate=True in case of iterable input + u = sympify("x+3*x+2", evaluate=True) + v = sympify("2*x+4*x+2+4", evaluate=True) + + assert u.is_Add and set(u.args) == {4*x, 2} + assert v.is_Add and set(v.args) == {6*x, 6} + assert sympify(["x+3*x+2", "2*x+4*x+2+4"], evaluate=True) == [u, v] + + #test to check evaluate with no input in case of iterable input + u = sympify("x+3*x+2") + v = sympify("2*x+4*x+2+4") + + assert u.is_Add and set(u.args) == {4*x, 2} + assert v.is_Add and set(v.args) == {6*x, 6} + assert sympify(["x+3*x+2", "2*x+4*x+2+4"]) == [u, v] + +def test_issue_27284(): + if not numpy: + skip("numpy not installed.") + + assert Float(numpy.float32(float('inf'))) == S.Infinity + assert Float(numpy.float32(float('-inf'))) == S.NegativeInfinity diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_traversal.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_traversal.py new file mode 100644 index 0000000000000000000000000000000000000000..8bf067283eaba5d4a073a73feb07aac199055a7f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_traversal.py @@ -0,0 +1,119 @@ +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.sorting import default_sort_key +from sympy.core.symbol import symbols +from sympy.core.singleton import S +from sympy.core.function import expand, Function +from sympy.core.numbers import I +from sympy.integrals.integrals import Integral +from sympy.polys.polytools import factor +from sympy.core.traversal import preorder_traversal, use, postorder_traversal, iterargs, iterfreeargs +from sympy.functions.elementary.piecewise import ExprCondPair, Piecewise +from sympy.testing.pytest import warns_deprecated_sympy +from sympy.utilities.iterables import capture + +b1 = Basic() +b2 = Basic(b1) +b3 = Basic(b2) +b21 = Basic(b2, b1) + + +def test_preorder_traversal(): + expr = Basic(b21, b3) + assert list( + preorder_traversal(expr)) == [expr, b21, b2, b1, b1, b3, b2, b1] + assert list(preorder_traversal(('abc', ('d', 'ef')))) == [ + ('abc', ('d', 'ef')), 'abc', ('d', 'ef'), 'd', 'ef'] + + result = [] + pt = preorder_traversal(expr) + for i in pt: + result.append(i) + if i == b2: + pt.skip() + assert result == [expr, b21, b2, b1, b3, b2] + + w, x, y, z = symbols('w:z') + expr = z + w*(x + y) + assert list(preorder_traversal([expr], keys=default_sort_key)) == \ + [[w*(x + y) + z], w*(x + y) + z, z, w*(x + y), w, x + y, x, y] + assert list(preorder_traversal((x + y)*z, keys=True)) == \ + [z*(x + y), z, x + y, x, y] + + +def test_use(): + x, y = symbols('x y') + + assert use(0, expand) == 0 + + f = (x + y)**2*x + 1 + + assert use(f, expand, level=0) == x**3 + 2*x**2*y + x*y**2 + + 1 + assert use(f, expand, level=1) == x**3 + 2*x**2*y + x*y**2 + + 1 + assert use(f, expand, level=2) == 1 + x*(2*x*y + x**2 + y**2) + assert use(f, expand, level=3) == (x + y)**2*x + 1 + + f = (x**2 + 1)**2 - 1 + kwargs = {'gaussian': True} + + assert use(f, factor, level=0, kwargs=kwargs) == x**2*(x**2 + 2) + assert use(f, factor, level=1, kwargs=kwargs) == (x + I)**2*(x - I)**2 - 1 + assert use(f, factor, level=2, kwargs=kwargs) == (x + I)**2*(x - I)**2 - 1 + assert use(f, factor, level=3, kwargs=kwargs) == (x**2 + 1)**2 - 1 + + +def test_postorder_traversal(): + x, y, z, w = symbols('x y z w') + expr = z + w*(x + y) + expected = [z, w, x, y, x + y, w*(x + y), w*(x + y) + z] + assert list(postorder_traversal(expr, keys=default_sort_key)) == expected + assert list(postorder_traversal(expr, keys=True)) == expected + + expr = Piecewise((x, x < 1), (x**2, True)) + expected = [ + x, 1, x, x < 1, ExprCondPair(x, x < 1), + 2, x, x**2, S.true, + ExprCondPair(x**2, True), Piecewise((x, x < 1), (x**2, True)) + ] + assert list(postorder_traversal(expr, keys=default_sort_key)) == expected + assert list(postorder_traversal( + [expr], keys=default_sort_key)) == expected + [[expr]] + + assert list(postorder_traversal(Integral(x**2, (x, 0, 1)), + keys=default_sort_key)) == [ + 2, x, x**2, 0, 1, x, Tuple(x, 0, 1), + Integral(x**2, Tuple(x, 0, 1)) + ] + assert list(postorder_traversal(('abc', ('d', 'ef')))) == [ + 'abc', 'd', 'ef', ('d', 'ef'), ('abc', ('d', 'ef'))] + + +def test_iterargs(): + f = Function('f') + x = symbols('x') + assert list(iterfreeargs(Integral(f(x), (f(x), 1)))) == [ + Integral(f(x), (f(x), 1)), 1] + assert list(iterargs(Integral(f(x), (f(x), 1)))) == [ + Integral(f(x), (f(x), 1)), f(x), (f(x), 1), x, f(x), 1, x] + +def test_deprecated_imports(): + x = symbols('x') + + with warns_deprecated_sympy(): + from sympy.core.basic import preorder_traversal + preorder_traversal(x) + with warns_deprecated_sympy(): + from sympy.simplify.simplify import bottom_up + bottom_up(x, lambda x: x) + with warns_deprecated_sympy(): + from sympy.simplify.simplify import walk + walk(x, lambda x: x) + with warns_deprecated_sympy(): + from sympy.simplify.traversaltools import use + use(x, lambda x: x) + with warns_deprecated_sympy(): + from sympy.utilities.iterables import postorder_traversal + postorder_traversal(x) + with warns_deprecated_sympy(): + from sympy.utilities.iterables import interactive_traversal + capture(lambda: interactive_traversal(x)) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_truediv.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_truediv.py new file mode 100644 index 0000000000000000000000000000000000000000..1fcf9e1ab754d05a3b47e7ec0c2be5ea9929da02 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_truediv.py @@ -0,0 +1,54 @@ +#this module tests that SymPy works with true division turned on + +from sympy.core.numbers import (Float, Rational) +from sympy.core.symbol import Symbol + + +def test_truediv(): + assert 1/2 != 0 + assert Rational(1)/2 != 0 + + +def dotest(s): + x = Symbol("x") + y = Symbol("y") + l = [ + Rational(2), + Float("1.3"), + x, + y, + pow(x, y)*y, + 5, + 5.5 + ] + for x in l: + for y in l: + s(x, y) + return True + + +def test_basic(): + def s(a, b): + x = a + x = +a + x = -a + x = a + b + x = a - b + x = a*b + x = a/b + x = a**b + del x + assert dotest(s) + + +def test_ibasic(): + def s(a, b): + x = a + x += b + x = a + x -= b + x = a + x *= b + x = a + x /= b + assert dotest(s) diff --git a/venv/lib/python3.11/site-packages/sympy/core/tests/test_var.py b/venv/lib/python3.11/site-packages/sympy/core/tests/test_var.py new file mode 100644 index 0000000000000000000000000000000000000000..a02709464c9878082fecaf70fa47067ac8838ac6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/tests/test_var.py @@ -0,0 +1,62 @@ +from sympy.core.function import (Function, FunctionClass) +from sympy.core.symbol import (Symbol, var) +from sympy.testing.pytest import raises + +def test_var(): + ns = {"var": var, "raises": raises} + eval("var('a')", ns) + assert ns["a"] == Symbol("a") + + eval("var('b bb cc zz _x')", ns) + assert ns["b"] == Symbol("b") + assert ns["bb"] == Symbol("bb") + assert ns["cc"] == Symbol("cc") + assert ns["zz"] == Symbol("zz") + assert ns["_x"] == Symbol("_x") + + v = eval("var(['d', 'e', 'fg'])", ns) + assert ns['d'] == Symbol('d') + assert ns['e'] == Symbol('e') + assert ns['fg'] == Symbol('fg') + +# check return value + assert v != ['d', 'e', 'fg'] + assert v == [Symbol('d'), Symbol('e'), Symbol('fg')] + + +def test_var_return(): + ns = {"var": var, "raises": raises} + "raises(ValueError, lambda: var(''))" + v2 = eval("var('q')", ns) + v3 = eval("var('q p')", ns) + + assert v2 == Symbol('q') + assert v3 == (Symbol('q'), Symbol('p')) + + +def test_var_accepts_comma(): + ns = {"var": var} + v1 = eval("var('x y z')", ns) + v2 = eval("var('x,y,z')", ns) + v3 = eval("var('x,y z')", ns) + + assert v1 == v2 + assert v1 == v3 + + +def test_var_keywords(): + ns = {"var": var} + eval("var('x y', real=True)", ns) + assert ns['x'].is_real and ns['y'].is_real + + +def test_var_cls(): + ns = {"var": var, "Function": Function} + eval("var('f', cls=Function)", ns) + + assert isinstance(ns['f'], FunctionClass) + + eval("var('g,h', cls=Function)", ns) + + assert isinstance(ns['g'], FunctionClass) + assert isinstance(ns['h'], FunctionClass) diff --git a/venv/lib/python3.11/site-packages/sympy/core/trace.py b/venv/lib/python3.11/site-packages/sympy/core/trace.py new file mode 100644 index 0000000000000000000000000000000000000000..58326ce1fdd5038f0b5805afe7c453314a22cb6a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/trace.py @@ -0,0 +1,12 @@ +from sympy.utilities.exceptions import sympy_deprecation_warning + +sympy_deprecation_warning( + """ + sympy.core.trace is deprecated. Use sympy.physics.quantum.trace + instead. + """, + deprecated_since_version="1.10", + active_deprecations_target="sympy-core-trace-deprecated", +) + +from sympy.physics.quantum.trace import Tr # noqa:F401 diff --git a/venv/lib/python3.11/site-packages/sympy/core/traversal.py b/venv/lib/python3.11/site-packages/sympy/core/traversal.py new file mode 100644 index 0000000000000000000000000000000000000000..e4e000ef44bf636b9adc700964a7ee4c2372a019 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/core/traversal.py @@ -0,0 +1,304 @@ +from __future__ import annotations + +from typing import Iterator + +from .basic import Basic +from .sorting import ordered +from .sympify import sympify +from sympy.utilities.iterables import iterable + + + +def iterargs(expr): + """Yield the args of a Basic object in a breadth-first traversal. + Depth-traversal stops if `arg.args` is either empty or is not + an iterable. + + Examples + ======== + + >>> from sympy import Integral, Function + >>> from sympy.abc import x + >>> f = Function('f') + >>> from sympy.core.traversal import iterargs + >>> list(iterargs(Integral(f(x), (f(x), 1)))) + [Integral(f(x), (f(x), 1)), f(x), (f(x), 1), x, f(x), 1, x] + + See Also + ======== + iterfreeargs, preorder_traversal + """ + args = [expr] + for i in args: + yield i + args.extend(i.args) + + +def iterfreeargs(expr, _first=True): + """Yield the args of a Basic object in a breadth-first traversal. + Depth-traversal stops if `arg.args` is either empty or is not + an iterable. The bound objects of an expression will be returned + as canonical variables. + + Examples + ======== + + >>> from sympy import Integral, Function + >>> from sympy.abc import x + >>> f = Function('f') + >>> from sympy.core.traversal import iterfreeargs + >>> list(iterfreeargs(Integral(f(x), (f(x), 1)))) + [Integral(f(x), (f(x), 1)), 1] + + See Also + ======== + iterargs, preorder_traversal + """ + args = [expr] + for i in args: + yield i + if _first and hasattr(i, 'bound_symbols'): + void = i.canonical_variables.values() + for i in iterfreeargs(i.as_dummy(), _first=False): + if not i.has(*void): + yield i + args.extend(i.args) + + +class preorder_traversal: + """ + Do a pre-order traversal of a tree. + + This iterator recursively yields nodes that it has visited in a pre-order + fashion. That is, it yields the current node then descends through the + tree breadth-first to yield all of a node's children's pre-order + traversal. + + + For an expression, the order of the traversal depends on the order of + .args, which in many cases can be arbitrary. + + Parameters + ========== + node : SymPy expression + The expression to traverse. + keys : (default None) sort key(s) + The key(s) used to sort args of Basic objects. When None, args of Basic + objects are processed in arbitrary order. If key is defined, it will + be passed along to ordered() as the only key(s) to use to sort the + arguments; if ``key`` is simply True then the default keys of ordered + will be used. + + Yields + ====== + subtree : SymPy expression + All of the subtrees in the tree. + + Examples + ======== + + >>> from sympy import preorder_traversal, symbols + >>> x, y, z = symbols('x y z') + + The nodes are returned in the order that they are encountered unless key + is given; simply passing key=True will guarantee that the traversal is + unique. + + >>> list(preorder_traversal((x + y)*z, keys=None)) # doctest: +SKIP + [z*(x + y), z, x + y, y, x] + >>> list(preorder_traversal((x + y)*z, keys=True)) + [z*(x + y), z, x + y, x, y] + + """ + def __init__(self, node, keys=None): + self._skip_flag = False + self._pt = self._preorder_traversal(node, keys) + + def _preorder_traversal(self, node, keys): + yield node + if self._skip_flag: + self._skip_flag = False + return + if isinstance(node, Basic): + if not keys and hasattr(node, '_argset'): + # LatticeOp keeps args as a set. We should use this if we + # don't care about the order, to prevent unnecessary sorting. + args = node._argset + else: + args = node.args + if keys: + if keys != True: + args = ordered(args, keys, default=False) + else: + args = ordered(args) + for arg in args: + yield from self._preorder_traversal(arg, keys) + elif iterable(node): + for item in node: + yield from self._preorder_traversal(item, keys) + + def skip(self): + """ + Skip yielding current node's (last yielded node's) subtrees. + + Examples + ======== + + >>> from sympy import preorder_traversal, symbols + >>> x, y, z = symbols('x y z') + >>> pt = preorder_traversal((x + y*z)*z) + >>> for i in pt: + ... print(i) + ... if i == x + y*z: + ... pt.skip() + z*(x + y*z) + z + x + y*z + """ + self._skip_flag = True + + def __next__(self): + return next(self._pt) + + def __iter__(self) -> Iterator[Basic]: + return self + + +def use(expr, func, level=0, args=(), kwargs={}): + """ + Use ``func`` to transform ``expr`` at the given level. + + Examples + ======== + + >>> from sympy import use, expand + >>> from sympy.abc import x, y + + >>> f = (x + y)**2*x + 1 + + >>> use(f, expand, level=2) + x*(x**2 + 2*x*y + y**2) + 1 + >>> expand(f) + x**3 + 2*x**2*y + x*y**2 + 1 + + """ + def _use(expr, level): + if not level: + return func(expr, *args, **kwargs) + else: + if expr.is_Atom: + return expr + else: + level -= 1 + _args = [_use(arg, level) for arg in expr.args] + return expr.__class__(*_args) + + return _use(sympify(expr), level) + + +def walk(e, *target): + """Iterate through the args that are the given types (target) and + return a list of the args that were traversed; arguments + that are not of the specified types are not traversed. + + Examples + ======== + + >>> from sympy.core.traversal import walk + >>> from sympy import Min, Max + >>> from sympy.abc import x, y, z + >>> list(walk(Min(x, Max(y, Min(1, z))), Min)) + [Min(x, Max(y, Min(1, z)))] + >>> list(walk(Min(x, Max(y, Min(1, z))), Min, Max)) + [Min(x, Max(y, Min(1, z))), Max(y, Min(1, z)), Min(1, z)] + + See Also + ======== + + bottom_up + """ + if isinstance(e, target): + yield e + for i in e.args: + yield from walk(i, *target) + + +def bottom_up(rv, F, atoms=False, nonbasic=False): + """Apply ``F`` to all expressions in an expression tree from the + bottom up. If ``atoms`` is True, apply ``F`` even if there are no args; + if ``nonbasic`` is True, try to apply ``F`` to non-Basic objects. + """ + args = getattr(rv, 'args', None) + if args is not None: + if args: + args = tuple([bottom_up(a, F, atoms, nonbasic) for a in args]) + if args != rv.args: + rv = rv.func(*args) + rv = F(rv) + elif atoms: + rv = F(rv) + else: + if nonbasic: + try: + rv = F(rv) + except TypeError: + pass + + return rv + + +def postorder_traversal(node, keys=None): + """ + Do a postorder traversal of a tree. + + This generator recursively yields nodes that it has visited in a postorder + fashion. That is, it descends through the tree depth-first to yield all of + a node's children's postorder traversal before yielding the node itself. + + Parameters + ========== + + node : SymPy expression + The expression to traverse. + keys : (default None) sort key(s) + The key(s) used to sort args of Basic objects. When None, args of Basic + objects are processed in arbitrary order. If key is defined, it will + be passed along to ordered() as the only key(s) to use to sort the + arguments; if ``key`` is simply True then the default keys of + ``ordered`` will be used (node count and default_sort_key). + + Yields + ====== + subtree : SymPy expression + All of the subtrees in the tree. + + Examples + ======== + + >>> from sympy import postorder_traversal + >>> from sympy.abc import w, x, y, z + + The nodes are returned in the order that they are encountered unless key + is given; simply passing key=True will guarantee that the traversal is + unique. + + >>> list(postorder_traversal(w + (x + y)*z)) # doctest: +SKIP + [z, y, x, x + y, z*(x + y), w, w + z*(x + y)] + >>> list(postorder_traversal(w + (x + y)*z, keys=True)) + [w, z, x, y, x + y, z*(x + y), w + z*(x + y)] + + + """ + if isinstance(node, Basic): + args = node.args + if keys: + if keys != True: + args = ordered(args, keys, default=False) + else: + args = ordered(args) + for arg in args: + yield from postorder_traversal(arg, keys) + elif iterable(node): + for item in node: + yield from postorder_traversal(item, keys) + yield node diff --git a/venv/lib/python3.11/site-packages/sympy/crypto/__init__.py b/venv/lib/python3.11/site-packages/sympy/crypto/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2b27b4b036e5f2ed93a1ea88cd7d7144eb5615d4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/crypto/__init__.py @@ -0,0 +1,35 @@ +from sympy.crypto.crypto import (cycle_list, + encipher_shift, encipher_affine, encipher_substitution, + check_and_join, encipher_vigenere, decipher_vigenere, bifid5_square, + bifid6_square, encipher_hill, decipher_hill, + encipher_bifid5, encipher_bifid6, decipher_bifid5, + decipher_bifid6, encipher_kid_rsa, decipher_kid_rsa, + kid_rsa_private_key, kid_rsa_public_key, decipher_rsa, rsa_private_key, + rsa_public_key, encipher_rsa, lfsr_connection_polynomial, + lfsr_autocorrelation, lfsr_sequence, encode_morse, decode_morse, + elgamal_private_key, elgamal_public_key, decipher_elgamal, + encipher_elgamal, dh_private_key, dh_public_key, dh_shared_key, + padded_key, encipher_bifid, decipher_bifid, bifid_square, bifid5, + bifid6, bifid10, decipher_gm, encipher_gm, gm_public_key, + gm_private_key, bg_private_key, bg_public_key, encipher_bg, decipher_bg, + encipher_rot13, decipher_rot13, encipher_atbash, decipher_atbash, + encipher_railfence, decipher_railfence) + +__all__ = [ + 'cycle_list', 'encipher_shift', 'encipher_affine', + 'encipher_substitution', 'check_and_join', 'encipher_vigenere', + 'decipher_vigenere', 'bifid5_square', 'bifid6_square', 'encipher_hill', + 'decipher_hill', 'encipher_bifid5', 'encipher_bifid6', 'decipher_bifid5', + 'decipher_bifid6', 'encipher_kid_rsa', 'decipher_kid_rsa', + 'kid_rsa_private_key', 'kid_rsa_public_key', 'decipher_rsa', + 'rsa_private_key', 'rsa_public_key', 'encipher_rsa', + 'lfsr_connection_polynomial', 'lfsr_autocorrelation', 'lfsr_sequence', + 'encode_morse', 'decode_morse', 'elgamal_private_key', + 'elgamal_public_key', 'decipher_elgamal', 'encipher_elgamal', + 'dh_private_key', 'dh_public_key', 'dh_shared_key', 'padded_key', + 'encipher_bifid', 'decipher_bifid', 'bifid_square', 'bifid5', 'bifid6', + 'bifid10', 'decipher_gm', 'encipher_gm', 'gm_public_key', + 'gm_private_key', 'bg_private_key', 'bg_public_key', 'encipher_bg', + 'decipher_bg', 'encipher_rot13', 'decipher_rot13', 'encipher_atbash', + 'decipher_atbash', 'encipher_railfence', 'decipher_railfence', +] diff --git a/venv/lib/python3.11/site-packages/sympy/crypto/crypto.py b/venv/lib/python3.11/site-packages/sympy/crypto/crypto.py new file mode 100644 index 0000000000000000000000000000000000000000..2c298e4ac08616dbe7d607a9d56d33b7fe9d5e2d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/crypto/crypto.py @@ -0,0 +1,3368 @@ +""" +This file contains some classical ciphers and routines +implementing a linear-feedback shift register (LFSR) +and the Diffie-Hellman key exchange. + +.. warning:: + + This module is intended for educational purposes only. Do not use the + functions in this module for real cryptographic applications. If you wish + to encrypt real data, we recommend using something like the `cryptography + `_ module. + +""" + +from string import whitespace, ascii_uppercase as uppercase, printable +from functools import reduce +import string +import warnings + +from itertools import cycle + +from sympy.external.gmpy import GROUND_TYPES +from sympy.core import Symbol +from sympy.core.numbers import Rational +from sympy.core.random import _randrange, _randint +from sympy.external.gmpy import gcd, invert +from sympy.functions.combinatorial.numbers import (totient as _euler, + reduced_totient as _carmichael) +from sympy.matrices import Matrix +from sympy.ntheory import isprime, primitive_root, factorint +from sympy.ntheory.generate import nextprime +from sympy.ntheory.modular import crt +from sympy.polys.domains import FF +from sympy.polys.polytools import Poly +from sympy.utilities.misc import as_int, filldedent, translate +from sympy.utilities.iterables import uniq, multiset +from sympy.utilities.decorator import doctest_depends_on + + +if GROUND_TYPES == 'flint': + __doctest_skip__ = ['lfsr_sequence'] + + +class NonInvertibleCipherWarning(RuntimeWarning): + """A warning raised if the cipher is not invertible.""" + def __init__(self, msg): + self.fullMessage = msg + + def __str__(self): + return '\n\t' + self.fullMessage + + def warn(self, stacklevel=3): + warnings.warn(self, stacklevel=stacklevel) + + +def AZ(s=None): + """Return the letters of ``s`` in uppercase. In case more than + one string is passed, each of them will be processed and a list + of upper case strings will be returned. + + Examples + ======== + + >>> from sympy.crypto.crypto import AZ + >>> AZ('Hello, world!') + 'HELLOWORLD' + >>> AZ('Hello, world!'.split()) + ['HELLO', 'WORLD'] + + See Also + ======== + + check_and_join + + """ + if not s: + return uppercase + t = isinstance(s, str) + if t: + s = [s] + rv = [check_and_join(i.upper().split(), uppercase, filter=True) + for i in s] + if t: + return rv[0] + return rv + +bifid5 = AZ().replace('J', '') +bifid6 = AZ() + string.digits +bifid10 = printable + + +def padded_key(key, symbols): + """Return a string of the distinct characters of ``symbols`` with + those of ``key`` appearing first. A ValueError is raised if + a) there are duplicate characters in ``symbols`` or + b) there are characters in ``key`` that are not in ``symbols``. + + Examples + ======== + + >>> from sympy.crypto.crypto import padded_key + >>> padded_key('PUPPY', 'OPQRSTUVWXY') + 'PUYOQRSTVWX' + >>> padded_key('RSA', 'ARTIST') + Traceback (most recent call last): + ... + ValueError: duplicate characters in symbols: T + + """ + syms = list(uniq(symbols)) + if len(syms) != len(symbols): + extra = ''.join(sorted({ + i for i in symbols if symbols.count(i) > 1})) + raise ValueError('duplicate characters in symbols: %s' % extra) + extra = set(key) - set(syms) + if extra: + raise ValueError( + 'characters in key but not symbols: %s' % ''.join( + sorted(extra))) + key0 = ''.join(list(uniq(key))) + # remove from syms characters in key0 + return key0 + translate(''.join(syms), None, key0) + + +def check_and_join(phrase, symbols=None, filter=None): + """ + Joins characters of ``phrase`` and if ``symbols`` is given, raises + an error if any character in ``phrase`` is not in ``symbols``. + + Parameters + ========== + + phrase + String or list of strings to be returned as a string. + + symbols + Iterable of characters allowed in ``phrase``. + + If ``symbols`` is ``None``, no checking is performed. + + Examples + ======== + + >>> from sympy.crypto.crypto import check_and_join + >>> check_and_join('a phrase') + 'a phrase' + >>> check_and_join('a phrase'.upper().split()) + 'APHRASE' + >>> check_and_join('a phrase!'.upper().split(), 'ARE', filter=True) + 'ARAE' + >>> check_and_join('a phrase!'.upper().split(), 'ARE') + Traceback (most recent call last): + ... + ValueError: characters in phrase but not symbols: "!HPS" + + """ + rv = ''.join(''.join(phrase)) + if symbols is not None: + symbols = check_and_join(symbols) + missing = ''.join(sorted(set(rv) - set(symbols))) + if missing: + if not filter: + raise ValueError( + 'characters in phrase but not symbols: "%s"' % missing) + rv = translate(rv, None, missing) + return rv + + +def _prep(msg, key, alp, default=None): + if not alp: + if not default: + alp = AZ() + msg = AZ(msg) + key = AZ(key) + else: + alp = default + else: + alp = ''.join(alp) + key = check_and_join(key, alp, filter=True) + msg = check_and_join(msg, alp, filter=True) + return msg, key, alp + + +def cycle_list(k, n): + """ + Returns the elements of the list ``range(n)`` shifted to the + left by ``k`` (so the list starts with ``k`` (mod ``n``)). + + Examples + ======== + + >>> from sympy.crypto.crypto import cycle_list + >>> cycle_list(3, 10) + [3, 4, 5, 6, 7, 8, 9, 0, 1, 2] + + """ + k = k % n + return list(range(k, n)) + list(range(k)) + + +######## shift cipher examples ############ + + +def encipher_shift(msg, key, symbols=None): + """ + Performs shift cipher encryption on plaintext msg, and returns the + ciphertext. + + Parameters + ========== + + key : int + The secret key. + + msg : str + Plaintext of upper-case letters. + + Returns + ======= + + str + Ciphertext of upper-case letters. + + Examples + ======== + + >>> from sympy.crypto.crypto import encipher_shift, decipher_shift + >>> msg = "GONAVYBEATARMY" + >>> ct = encipher_shift(msg, 1); ct + 'HPOBWZCFBUBSNZ' + + To decipher the shifted text, change the sign of the key: + + >>> encipher_shift(ct, -1) + 'GONAVYBEATARMY' + + There is also a convenience function that does this with the + original key: + + >>> decipher_shift(ct, 1) + 'GONAVYBEATARMY' + + Notes + ===== + + ALGORITHM: + + STEPS: + 0. Number the letters of the alphabet from 0, ..., N + 1. Compute from the string ``msg`` a list ``L1`` of + corresponding integers. + 2. Compute from the list ``L1`` a new list ``L2``, given by + adding ``(k mod 26)`` to each element in ``L1``. + 3. Compute from the list ``L2`` a string ``ct`` of + corresponding letters. + + The shift cipher is also called the Caesar cipher, after + Julius Caesar, who, according to Suetonius, used it with a + shift of three to protect messages of military significance. + Caesar's nephew Augustus reportedly used a similar cipher, but + with a right shift of 1. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Caesar_cipher + .. [2] https://mathworld.wolfram.com/CaesarsMethod.html + + See Also + ======== + + decipher_shift + + """ + msg, _, A = _prep(msg, '', symbols) + shift = len(A) - key % len(A) + key = A[shift:] + A[:shift] + return translate(msg, key, A) + + +def decipher_shift(msg, key, symbols=None): + """ + Return the text by shifting the characters of ``msg`` to the + left by the amount given by ``key``. + + Examples + ======== + + >>> from sympy.crypto.crypto import encipher_shift, decipher_shift + >>> msg = "GONAVYBEATARMY" + >>> ct = encipher_shift(msg, 1); ct + 'HPOBWZCFBUBSNZ' + + To decipher the shifted text, change the sign of the key: + + >>> encipher_shift(ct, -1) + 'GONAVYBEATARMY' + + Or use this function with the original key: + + >>> decipher_shift(ct, 1) + 'GONAVYBEATARMY' + + """ + return encipher_shift(msg, -key, symbols) + +def encipher_rot13(msg, symbols=None): + """ + Performs the ROT13 encryption on a given plaintext ``msg``. + + Explanation + =========== + + ROT13 is a substitution cipher which substitutes each letter + in the plaintext message for the letter furthest away from it + in the English alphabet. + + Equivalently, it is just a Caeser (shift) cipher with a shift + key of 13 (midway point of the alphabet). + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/ROT13 + + See Also + ======== + + decipher_rot13 + encipher_shift + + """ + return encipher_shift(msg, 13, symbols) + +def decipher_rot13(msg, symbols=None): + """ + Performs the ROT13 decryption on a given plaintext ``msg``. + + Explanation + ============ + + ``decipher_rot13`` is equivalent to ``encipher_rot13`` as both + ``decipher_shift`` with a key of 13 and ``encipher_shift`` key with a + key of 13 will return the same results. Nonetheless, + ``decipher_rot13`` has nonetheless been explicitly defined here for + consistency. + + Examples + ======== + + >>> from sympy.crypto.crypto import encipher_rot13, decipher_rot13 + >>> msg = 'GONAVYBEATARMY' + >>> ciphertext = encipher_rot13(msg);ciphertext + 'TBANILORNGNEZL' + >>> decipher_rot13(ciphertext) + 'GONAVYBEATARMY' + >>> encipher_rot13(msg) == decipher_rot13(msg) + True + >>> msg == decipher_rot13(ciphertext) + True + + """ + return decipher_shift(msg, 13, symbols) + +######## affine cipher examples ############ + + +def encipher_affine(msg, key, symbols=None, _inverse=False): + r""" + Performs the affine cipher encryption on plaintext ``msg``, and + returns the ciphertext. + + Explanation + =========== + + Encryption is based on the map `x \rightarrow ax+b` (mod `N`) + where ``N`` is the number of characters in the alphabet. + Decryption is based on the map `x \rightarrow cx+d` (mod `N`), + where `c = a^{-1}` (mod `N`) and `d = -a^{-1}b` (mod `N`). + In particular, for the map to be invertible, we need + `\mathrm{gcd}(a, N) = 1` and an error will be raised if this is + not true. + + Parameters + ========== + + msg : str + Characters that appear in ``symbols``. + + a, b : int, int + A pair integers, with ``gcd(a, N) = 1`` (the secret key). + + symbols + String of characters (default = uppercase letters). + + When no symbols are given, ``msg`` is converted to upper case + letters and all other characters are ignored. + + Returns + ======= + + ct + String of characters (the ciphertext message) + + Notes + ===== + + ALGORITHM: + + STEPS: + 0. Number the letters of the alphabet from 0, ..., N + 1. Compute from the string ``msg`` a list ``L1`` of + corresponding integers. + 2. Compute from the list ``L1`` a new list ``L2``, given by + replacing ``x`` by ``a*x + b (mod N)``, for each element + ``x`` in ``L1``. + 3. Compute from the list ``L2`` a string ``ct`` of + corresponding letters. + + This is a straightforward generalization of the shift cipher with + the added complexity of requiring 2 characters to be deciphered in + order to recover the key. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Affine_cipher + + See Also + ======== + + decipher_affine + + """ + msg, _, A = _prep(msg, '', symbols) + N = len(A) + a, b = key + assert gcd(a, N) == 1 + if _inverse: + c = invert(a, N) + d = -b*c + a, b = c, d + B = ''.join([A[(a*i + b) % N] for i in range(N)]) + return translate(msg, A, B) + + +def decipher_affine(msg, key, symbols=None): + r""" + Return the deciphered text that was made from the mapping, + `x \rightarrow ax+b` (mod `N`), where ``N`` is the + number of characters in the alphabet. Deciphering is done by + reciphering with a new key: `x \rightarrow cx+d` (mod `N`), + where `c = a^{-1}` (mod `N`) and `d = -a^{-1}b` (mod `N`). + + Examples + ======== + + >>> from sympy.crypto.crypto import encipher_affine, decipher_affine + >>> msg = "GO NAVY BEAT ARMY" + >>> key = (3, 1) + >>> encipher_affine(msg, key) + 'TROBMVENBGBALV' + >>> decipher_affine(_, key) + 'GONAVYBEATARMY' + + See Also + ======== + + encipher_affine + + """ + return encipher_affine(msg, key, symbols, _inverse=True) + + +def encipher_atbash(msg, symbols=None): + r""" + Enciphers a given ``msg`` into its Atbash ciphertext and returns it. + + Explanation + =========== + + Atbash is a substitution cipher originally used to encrypt the Hebrew + alphabet. Atbash works on the principle of mapping each alphabet to its + reverse / counterpart (i.e. a would map to z, b to y etc.) + + Atbash is functionally equivalent to the affine cipher with ``a = 25`` + and ``b = 25`` + + See Also + ======== + + decipher_atbash + + """ + return encipher_affine(msg, (25, 25), symbols) + + +def decipher_atbash(msg, symbols=None): + r""" + Deciphers a given ``msg`` using Atbash cipher and returns it. + + Explanation + =========== + + ``decipher_atbash`` is functionally equivalent to ``encipher_atbash``. + However, it has still been added as a separate function to maintain + consistency. + + Examples + ======== + + >>> from sympy.crypto.crypto import encipher_atbash, decipher_atbash + >>> msg = 'GONAVYBEATARMY' + >>> encipher_atbash(msg) + 'TLMZEBYVZGZINB' + >>> decipher_atbash(msg) + 'TLMZEBYVZGZINB' + >>> encipher_atbash(msg) == decipher_atbash(msg) + True + >>> msg == encipher_atbash(encipher_atbash(msg)) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Atbash + + See Also + ======== + + encipher_atbash + + """ + return decipher_affine(msg, (25, 25), symbols) + +#################### substitution cipher ########################### + + +def encipher_substitution(msg, old, new=None): + r""" + Returns the ciphertext obtained by replacing each character that + appears in ``old`` with the corresponding character in ``new``. + If ``old`` is a mapping, then new is ignored and the replacements + defined by ``old`` are used. + + Explanation + =========== + + This is a more general than the affine cipher in that the key can + only be recovered by determining the mapping for each symbol. + Though in practice, once a few symbols are recognized the mappings + for other characters can be quickly guessed. + + Examples + ======== + + >>> from sympy.crypto.crypto import encipher_substitution, AZ + >>> old = 'OEYAG' + >>> new = '034^6' + >>> msg = AZ("go navy! beat army!") + >>> ct = encipher_substitution(msg, old, new); ct + '60N^V4B3^T^RM4' + + To decrypt a substitution, reverse the last two arguments: + + >>> encipher_substitution(ct, new, old) + 'GONAVYBEATARMY' + + In the special case where ``old`` and ``new`` are a permutation of + order 2 (representing a transposition of characters) their order + is immaterial: + + >>> old = 'NAVY' + >>> new = 'ANYV' + >>> encipher = lambda x: encipher_substitution(x, old, new) + >>> encipher('NAVY') + 'ANYV' + >>> encipher(_) + 'NAVY' + + The substitution cipher, in general, is a method + whereby "units" (not necessarily single characters) of plaintext + are replaced with ciphertext according to a regular system. + + >>> ords = dict(zip('abc', ['\\%i' % ord(i) for i in 'abc'])) + >>> print(encipher_substitution('abc', ords)) + \97\98\99 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Substitution_cipher + + """ + return translate(msg, old, new) + + +###################################################################### +#################### Vigenere cipher examples ######################## +###################################################################### + +def encipher_vigenere(msg, key, symbols=None): + """ + Performs the Vigenere cipher encryption on plaintext ``msg``, and + returns the ciphertext. + + Examples + ======== + + >>> from sympy.crypto.crypto import encipher_vigenere, AZ + >>> key = "encrypt" + >>> msg = "meet me on monday" + >>> encipher_vigenere(msg, key) + 'QRGKKTHRZQEBPR' + + Section 1 of the Kryptos sculpture at the CIA headquarters + uses this cipher and also changes the order of the + alphabet [2]_. Here is the first line of that section of + the sculpture: + + >>> from sympy.crypto.crypto import decipher_vigenere, padded_key + >>> alp = padded_key('KRYPTOS', AZ()) + >>> key = 'PALIMPSEST' + >>> msg = 'EMUFPHZLRFAXYUSDJKZLDKRNSHGNFIVJ' + >>> decipher_vigenere(msg, key, alp) + 'BETWEENSUBTLESHADINGANDTHEABSENC' + + Explanation + =========== + + The Vigenere cipher is named after Blaise de Vigenere, a sixteenth + century diplomat and cryptographer, by a historical accident. + Vigenere actually invented a different and more complicated cipher. + The so-called *Vigenere cipher* was actually invented + by Giovan Batista Belaso in 1553. + + This cipher was used in the 1800's, for example, during the American + Civil War. The Confederacy used a brass cipher disk to implement the + Vigenere cipher (now on display in the NSA Museum in Fort + Meade) [1]_. + + The Vigenere cipher is a generalization of the shift cipher. + Whereas the shift cipher shifts each letter by the same amount + (that amount being the key of the shift cipher) the Vigenere + cipher shifts a letter by an amount determined by the key (which is + a word or phrase known only to the sender and receiver). + + For example, if the key was a single letter, such as "C", then the + so-called Vigenere cipher is actually a shift cipher with a + shift of `2` (since "C" is the 2nd letter of the alphabet, if + you start counting at `0`). If the key was a word with two + letters, such as "CA", then the so-called Vigenere cipher will + shift letters in even positions by `2` and letters in odd positions + are left alone (shifted by `0`, since "A" is the 0th letter, if + you start counting at `0`). + + + ALGORITHM: + + INPUT: + + ``msg``: string of characters that appear in ``symbols`` + (the plaintext) + + ``key``: a string of characters that appear in ``symbols`` + (the secret key) + + ``symbols``: a string of letters defining the alphabet + + + OUTPUT: + + ``ct``: string of characters (the ciphertext message) + + STEPS: + 0. Number the letters of the alphabet from 0, ..., N + 1. Compute from the string ``key`` a list ``L1`` of + corresponding integers. Let ``n1 = len(L1)``. + 2. Compute from the string ``msg`` a list ``L2`` of + corresponding integers. Let ``n2 = len(L2)``. + 3. Break ``L2`` up sequentially into sublists of size + ``n1``; the last sublist may be smaller than ``n1`` + 4. For each of these sublists ``L`` of ``L2``, compute a + new list ``C`` given by ``C[i] = L[i] + L1[i] (mod N)`` + to the ``i``-th element in the sublist, for each ``i``. + 5. Assemble these lists ``C`` by concatenation into a new + list of length ``n2``. + 6. Compute from the new list a string ``ct`` of + corresponding letters. + + Once it is known that the key is, say, `n` characters long, + frequency analysis can be applied to every `n`-th letter of + the ciphertext to determine the plaintext. This method is + called *Kasiski examination* (although it was first discovered + by Babbage). If they key is as long as the message and is + comprised of randomly selected characters -- a one-time pad -- the + message is theoretically unbreakable. + + The cipher Vigenere actually discovered is an "auto-key" cipher + described as follows. + + ALGORITHM: + + INPUT: + + ``key``: a string of letters (the secret key) + + ``msg``: string of letters (the plaintext message) + + OUTPUT: + + ``ct``: string of upper-case letters (the ciphertext message) + + STEPS: + 0. Number the letters of the alphabet from 0, ..., N + 1. Compute from the string ``msg`` a list ``L2`` of + corresponding integers. Let ``n2 = len(L2)``. + 2. Let ``n1`` be the length of the key. Append to the + string ``key`` the first ``n2 - n1`` characters of + the plaintext message. Compute from this string (also of + length ``n2``) a list ``L1`` of integers corresponding + to the letter numbers in the first step. + 3. Compute a new list ``C`` given by + ``C[i] = L1[i] + L2[i] (mod N)``. + 4. Compute from the new list a string ``ct`` of letters + corresponding to the new integers. + + To decipher the auto-key ciphertext, the key is used to decipher + the first ``n1`` characters and then those characters become the + key to decipher the next ``n1`` characters, etc...: + + >>> m = AZ('go navy, beat army! yes you can'); m + 'GONAVYBEATARMYYESYOUCAN' + >>> key = AZ('gold bug'); n1 = len(key); n2 = len(m) + >>> auto_key = key + m[:n2 - n1]; auto_key + 'GOLDBUGGONAVYBEATARMYYE' + >>> ct = encipher_vigenere(m, auto_key); ct + 'MCYDWSHKOGAMKZCELYFGAYR' + >>> n1 = len(key) + >>> pt = [] + >>> while ct: + ... part, ct = ct[:n1], ct[n1:] + ... pt.append(decipher_vigenere(part, key)) + ... key = pt[-1] + ... + >>> ''.join(pt) == m + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Vigenere_cipher + .. [2] https://web.archive.org/web/20071116100808/https://filebox.vt.edu/users/batman/kryptos.html + (short URL: https://goo.gl/ijr22d) + + """ + msg, key, A = _prep(msg, key, symbols) + map = {c: i for i, c in enumerate(A)} + key = [map[c] for c in key] + N = len(map) + k = len(key) + rv = [] + for i, m in enumerate(msg): + rv.append(A[(map[m] + key[i % k]) % N]) + rv = ''.join(rv) + return rv + + +def decipher_vigenere(msg, key, symbols=None): + """ + Decode using the Vigenere cipher. + + Examples + ======== + + >>> from sympy.crypto.crypto import decipher_vigenere + >>> key = "encrypt" + >>> ct = "QRGK kt HRZQE BPR" + >>> decipher_vigenere(ct, key) + 'MEETMEONMONDAY' + + """ + msg, key, A = _prep(msg, key, symbols) + map = {c: i for i, c in enumerate(A)} + N = len(A) # normally, 26 + K = [map[c] for c in key] + n = len(K) + C = [map[c] for c in msg] + rv = ''.join([A[(-K[i % n] + c) % N] for i, c in enumerate(C)]) + return rv + + +#################### Hill cipher ######################## + + +def encipher_hill(msg, key, symbols=None, pad="Q"): + r""" + Return the Hill cipher encryption of ``msg``. + + Explanation + =========== + + The Hill cipher [1]_, invented by Lester S. Hill in the 1920's [2]_, + was the first polygraphic cipher in which it was practical + (though barely) to operate on more than three symbols at once. + The following discussion assumes an elementary knowledge of + matrices. + + First, each letter is first encoded as a number starting with 0. + Suppose your message `msg` consists of `n` capital letters, with no + spaces. This may be regarded an `n`-tuple M of elements of + `Z_{26}` (if the letters are those of the English alphabet). A key + in the Hill cipher is a `k x k` matrix `K`, all of whose entries + are in `Z_{26}`, such that the matrix `K` is invertible (i.e., the + linear transformation `K: Z_{N}^k \rightarrow Z_{N}^k` + is one-to-one). + + + Parameters + ========== + + msg + Plaintext message of `n` upper-case letters. + + key + A `k \times k` invertible matrix `K`, all of whose entries are + in `Z_{26}` (or whatever number of symbols are being used). + + pad + Character (default "Q") to use to make length of text be a + multiple of ``k``. + + Returns + ======= + + ct + Ciphertext of upper-case letters. + + Notes + ===== + + ALGORITHM: + + STEPS: + 0. Number the letters of the alphabet from 0, ..., N + 1. Compute from the string ``msg`` a list ``L`` of + corresponding integers. Let ``n = len(L)``. + 2. Break the list ``L`` up into ``t = ceiling(n/k)`` + sublists ``L_1``, ..., ``L_t`` of size ``k`` (with + the last list "padded" to ensure its size is + ``k``). + 3. Compute new list ``C_1``, ..., ``C_t`` given by + ``C[i] = K*L_i`` (arithmetic is done mod N), for each + ``i``. + 4. Concatenate these into a list ``C = C_1 + ... + C_t``. + 5. Compute from ``C`` a string ``ct`` of corresponding + letters. This has length ``k*t``. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hill_cipher + .. [2] Lester S. Hill, Cryptography in an Algebraic Alphabet, + The American Mathematical Monthly Vol.36, June-July 1929, + pp.306-312. + + See Also + ======== + + decipher_hill + + """ + assert key.is_square + assert len(pad) == 1 + msg, pad, A = _prep(msg, pad, symbols) + map = {c: i for i, c in enumerate(A)} + P = [map[c] for c in msg] + N = len(A) + k = key.cols + n = len(P) + m, r = divmod(n, k) + if r: + P = P + [map[pad]]*(k - r) + m += 1 + rv = ''.join([A[c % N] for j in range(m) for c in + list(key*Matrix(k, 1, [P[i] + for i in range(k*j, k*(j + 1))]))]) + return rv + + +def decipher_hill(msg, key, symbols=None): + """ + Deciphering is the same as enciphering but using the inverse of the + key matrix. + + Examples + ======== + + >>> from sympy.crypto.crypto import encipher_hill, decipher_hill + >>> from sympy import Matrix + + >>> key = Matrix([[1, 2], [3, 5]]) + >>> encipher_hill("meet me on monday", key) + 'UEQDUEODOCTCWQ' + >>> decipher_hill(_, key) + 'MEETMEONMONDAY' + + When the length of the plaintext (stripped of invalid characters) + is not a multiple of the key dimension, extra characters will + appear at the end of the enciphered and deciphered text. In order to + decipher the text, those characters must be included in the text to + be deciphered. In the following, the key has a dimension of 4 but + the text is 2 short of being a multiple of 4 so two characters will + be added. + + >>> key = Matrix([[1, 1, 1, 2], [0, 1, 1, 0], + ... [2, 2, 3, 4], [1, 1, 0, 1]]) + >>> msg = "ST" + >>> encipher_hill(msg, key) + 'HJEB' + >>> decipher_hill(_, key) + 'STQQ' + >>> encipher_hill(msg, key, pad="Z") + 'ISPK' + >>> decipher_hill(_, key) + 'STZZ' + + If the last two characters of the ciphertext were ignored in + either case, the wrong plaintext would be recovered: + + >>> decipher_hill("HD", key) + 'ORMV' + >>> decipher_hill("IS", key) + 'UIKY' + + See Also + ======== + + encipher_hill + + """ + assert key.is_square + msg, _, A = _prep(msg, '', symbols) + map = {c: i for i, c in enumerate(A)} + C = [map[c] for c in msg] + N = len(A) + k = key.cols + n = len(C) + m, r = divmod(n, k) + if r: + C = C + [0]*(k - r) + m += 1 + key_inv = key.inv_mod(N) + rv = ''.join([A[p % N] for j in range(m) for p in + list(key_inv*Matrix( + k, 1, [C[i] for i in range(k*j, k*(j + 1))]))]) + return rv + + +#################### Bifid cipher ######################## + + +def encipher_bifid(msg, key, symbols=None): + r""" + Performs the Bifid cipher encryption on plaintext ``msg``, and + returns the ciphertext. + + This is the version of the Bifid cipher that uses an `n \times n` + Polybius square. + + Parameters + ========== + + msg + Plaintext string. + + key + Short string for key. + + Duplicate characters are ignored and then it is padded with the + characters in ``symbols`` that were not in the short key. + + symbols + `n \times n` characters defining the alphabet. + + (default is string.printable) + + Returns + ======= + + ciphertext + Ciphertext using Bifid5 cipher without spaces. + + See Also + ======== + + decipher_bifid, encipher_bifid5, encipher_bifid6 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Bifid_cipher + + """ + msg, key, A = _prep(msg, key, symbols, bifid10) + long_key = ''.join(uniq(key)) or A + + n = len(A)**.5 + if n != int(n): + raise ValueError( + 'Length of alphabet (%s) is not a square number.' % len(A)) + N = int(n) + if len(long_key) < N**2: + long_key = list(long_key) + [x for x in A if x not in long_key] + + # the fractionalization + row_col = {ch: divmod(i, N) for i, ch in enumerate(long_key)} + r, c = zip(*[row_col[x] for x in msg]) + rc = r + c + ch = {i: ch for ch, i in row_col.items()} + rv = ''.join(ch[i] for i in zip(rc[::2], rc[1::2])) + return rv + + +def decipher_bifid(msg, key, symbols=None): + r""" + Performs the Bifid cipher decryption on ciphertext ``msg``, and + returns the plaintext. + + This is the version of the Bifid cipher that uses the `n \times n` + Polybius square. + + Parameters + ========== + + msg + Ciphertext string. + + key + Short string for key. + + Duplicate characters are ignored and then it is padded with the + characters in symbols that were not in the short key. + + symbols + `n \times n` characters defining the alphabet. + + (default=string.printable, a `10 \times 10` matrix) + + Returns + ======= + + deciphered + Deciphered text. + + Examples + ======== + + >>> from sympy.crypto.crypto import ( + ... encipher_bifid, decipher_bifid, AZ) + + Do an encryption using the bifid5 alphabet: + + >>> alp = AZ().replace('J', '') + >>> ct = AZ("meet me on monday!") + >>> key = AZ("gold bug") + >>> encipher_bifid(ct, key, alp) + 'IEILHHFSTSFQYE' + + When entering the text or ciphertext, spaces are ignored so it + can be formatted as desired. Re-entering the ciphertext from the + preceding, putting 4 characters per line and padding with an extra + J, does not cause problems for the deciphering: + + >>> decipher_bifid(''' + ... IEILH + ... HFSTS + ... FQYEJ''', key, alp) + 'MEETMEONMONDAY' + + When no alphabet is given, all 100 printable characters will be + used: + + >>> key = '' + >>> encipher_bifid('hello world!', key) + 'bmtwmg-bIo*w' + >>> decipher_bifid(_, key) + 'hello world!' + + If the key is changed, a different encryption is obtained: + + >>> key = 'gold bug' + >>> encipher_bifid('hello world!', 'gold_bug') + 'hg2sfuei7t}w' + + And if the key used to decrypt the message is not exact, the + original text will not be perfectly obtained: + + >>> decipher_bifid(_, 'gold pug') + 'heldo~wor6d!' + + """ + msg, _, A = _prep(msg, '', symbols, bifid10) + long_key = ''.join(uniq(key)) or A + + n = len(A)**.5 + if n != int(n): + raise ValueError( + 'Length of alphabet (%s) is not a square number.' % len(A)) + N = int(n) + if len(long_key) < N**2: + long_key = list(long_key) + [x for x in A if x not in long_key] + + # the reverse fractionalization + row_col = { + ch: divmod(i, N) for i, ch in enumerate(long_key)} + rc = [i for c in msg for i in row_col[c]] + n = len(msg) + rc = zip(*(rc[:n], rc[n:])) + ch = {i: ch for ch, i in row_col.items()} + rv = ''.join(ch[i] for i in rc) + return rv + + +def bifid_square(key): + """Return characters of ``key`` arranged in a square. + + Examples + ======== + + >>> from sympy.crypto.crypto import ( + ... bifid_square, AZ, padded_key, bifid5) + >>> bifid_square(AZ().replace('J', '')) + Matrix([ + [A, B, C, D, E], + [F, G, H, I, K], + [L, M, N, O, P], + [Q, R, S, T, U], + [V, W, X, Y, Z]]) + + >>> bifid_square(padded_key(AZ('gold bug!'), bifid5)) + Matrix([ + [G, O, L, D, B], + [U, A, C, E, F], + [H, I, K, M, N], + [P, Q, R, S, T], + [V, W, X, Y, Z]]) + + See Also + ======== + + padded_key + + """ + A = ''.join(uniq(''.join(key))) + n = len(A)**.5 + if n != int(n): + raise ValueError( + 'Length of alphabet (%s) is not a square number.' % len(A)) + n = int(n) + f = lambda i, j: Symbol(A[n*i + j]) + rv = Matrix(n, n, f) + return rv + + +def encipher_bifid5(msg, key): + r""" + Performs the Bifid cipher encryption on plaintext ``msg``, and + returns the ciphertext. + + Explanation + =========== + + This is the version of the Bifid cipher that uses the `5 \times 5` + Polybius square. The letter "J" is ignored so it must be replaced + with something else (traditionally an "I") before encryption. + + ALGORITHM: (5x5 case) + + STEPS: + 0. Create the `5 \times 5` Polybius square ``S`` associated + to ``key`` as follows: + + a) moving from left-to-right, top-to-bottom, + place the letters of the key into a `5 \times 5` + matrix, + b) if the key has less than 25 letters, add the + letters of the alphabet not in the key until the + `5 \times 5` square is filled. + + 1. Create a list ``P`` of pairs of numbers which are the + coordinates in the Polybius square of the letters in + ``msg``. + 2. Let ``L1`` be the list of all first coordinates of ``P`` + (length of ``L1 = n``), let ``L2`` be the list of all + second coordinates of ``P`` (so the length of ``L2`` + is also ``n``). + 3. Let ``L`` be the concatenation of ``L1`` and ``L2`` + (length ``L = 2*n``), except that consecutive numbers + are paired ``(L[2*i], L[2*i + 1])``. You can regard + ``L`` as a list of pairs of length ``n``. + 4. Let ``C`` be the list of all letters which are of the + form ``S[i, j]``, for all ``(i, j)`` in ``L``. As a + string, this is the ciphertext of ``msg``. + + Parameters + ========== + + msg : str + Plaintext string. + + Converted to upper case and filtered of anything but all letters + except J. + + key + Short string for key; non-alphabetic letters, J and duplicated + characters are ignored and then, if the length is less than 25 + characters, it is padded with other letters of the alphabet + (in alphabetical order). + + Returns + ======= + + ct + Ciphertext (all caps, no spaces). + + Examples + ======== + + >>> from sympy.crypto.crypto import ( + ... encipher_bifid5, decipher_bifid5) + + "J" will be omitted unless it is replaced with something else: + + >>> round_trip = lambda m, k: \ + ... decipher_bifid5(encipher_bifid5(m, k), k) + >>> key = 'a' + >>> msg = "JOSIE" + >>> round_trip(msg, key) + 'OSIE' + >>> round_trip(msg.replace("J", "I"), key) + 'IOSIE' + >>> j = "QIQ" + >>> round_trip(msg.replace("J", j), key).replace(j, "J") + 'JOSIE' + + + Notes + ===== + + The Bifid cipher was invented around 1901 by Felix Delastelle. + It is a *fractional substitution* cipher, where letters are + replaced by pairs of symbols from a smaller alphabet. The + cipher uses a `5 \times 5` square filled with some ordering of the + alphabet, except that "J" is replaced with "I" (this is a so-called + Polybius square; there is a `6 \times 6` analog if you add back in + "J" and also append onto the usual 26 letter alphabet, the digits + 0, 1, ..., 9). + According to Helen Gaines' book *Cryptanalysis*, this type of cipher + was used in the field by the German Army during World War I. + + See Also + ======== + + decipher_bifid5, encipher_bifid + + """ + msg, key, _ = _prep(msg.upper(), key.upper(), None, bifid5) + key = padded_key(key, bifid5) + return encipher_bifid(msg, '', key) + + +def decipher_bifid5(msg, key): + r""" + Return the Bifid cipher decryption of ``msg``. + + Explanation + =========== + + This is the version of the Bifid cipher that uses the `5 \times 5` + Polybius square; the letter "J" is ignored unless a ``key`` of + length 25 is used. + + Parameters + ========== + + msg + Ciphertext string. + + key + Short string for key; duplicated characters are ignored and if + the length is less then 25 characters, it will be padded with + other letters from the alphabet omitting "J". + Non-alphabetic characters are ignored. + + Returns + ======= + + plaintext + Plaintext from Bifid5 cipher (all caps, no spaces). + + Examples + ======== + + >>> from sympy.crypto.crypto import encipher_bifid5, decipher_bifid5 + >>> key = "gold bug" + >>> encipher_bifid5('meet me on friday', key) + 'IEILEHFSTSFXEE' + >>> encipher_bifid5('meet me on monday', key) + 'IEILHHFSTSFQYE' + >>> decipher_bifid5(_, key) + 'MEETMEONMONDAY' + + """ + msg, key, _ = _prep(msg.upper(), key.upper(), None, bifid5) + key = padded_key(key, bifid5) + return decipher_bifid(msg, '', key) + + +def bifid5_square(key=None): + r""" + 5x5 Polybius square. + + Produce the Polybius square for the `5 \times 5` Bifid cipher. + + Examples + ======== + + >>> from sympy.crypto.crypto import bifid5_square + >>> bifid5_square("gold bug") + Matrix([ + [G, O, L, D, B], + [U, A, C, E, F], + [H, I, K, M, N], + [P, Q, R, S, T], + [V, W, X, Y, Z]]) + + """ + if not key: + key = bifid5 + else: + _, key, _ = _prep('', key.upper(), None, bifid5) + key = padded_key(key, bifid5) + return bifid_square(key) + + +def encipher_bifid6(msg, key): + r""" + Performs the Bifid cipher encryption on plaintext ``msg``, and + returns the ciphertext. + + This is the version of the Bifid cipher that uses the `6 \times 6` + Polybius square. + + Parameters + ========== + + msg + Plaintext string (digits okay). + + key + Short string for key (digits okay). + + If ``key`` is less than 36 characters long, the square will be + filled with letters A through Z and digits 0 through 9. + + Returns + ======= + + ciphertext + Ciphertext from Bifid cipher (all caps, no spaces). + + See Also + ======== + + decipher_bifid6, encipher_bifid + + """ + msg, key, _ = _prep(msg.upper(), key.upper(), None, bifid6) + key = padded_key(key, bifid6) + return encipher_bifid(msg, '', key) + + +def decipher_bifid6(msg, key): + r""" + Performs the Bifid cipher decryption on ciphertext ``msg``, and + returns the plaintext. + + This is the version of the Bifid cipher that uses the `6 \times 6` + Polybius square. + + Parameters + ========== + + msg + Ciphertext string (digits okay); converted to upper case + + key + Short string for key (digits okay). + + If ``key`` is less than 36 characters long, the square will be + filled with letters A through Z and digits 0 through 9. + All letters are converted to uppercase. + + Returns + ======= + + plaintext + Plaintext from Bifid cipher (all caps, no spaces). + + Examples + ======== + + >>> from sympy.crypto.crypto import encipher_bifid6, decipher_bifid6 + >>> key = "gold bug" + >>> encipher_bifid6('meet me on monday at 8am', key) + 'KFKLJJHF5MMMKTFRGPL' + >>> decipher_bifid6(_, key) + 'MEETMEONMONDAYAT8AM' + + """ + msg, key, _ = _prep(msg.upper(), key.upper(), None, bifid6) + key = padded_key(key, bifid6) + return decipher_bifid(msg, '', key) + + +def bifid6_square(key=None): + r""" + 6x6 Polybius square. + + Produces the Polybius square for the `6 \times 6` Bifid cipher. + Assumes alphabet of symbols is "A", ..., "Z", "0", ..., "9". + + Examples + ======== + + >>> from sympy.crypto.crypto import bifid6_square + >>> key = "gold bug" + >>> bifid6_square(key) + Matrix([ + [G, O, L, D, B, U], + [A, C, E, F, H, I], + [J, K, M, N, P, Q], + [R, S, T, V, W, X], + [Y, Z, 0, 1, 2, 3], + [4, 5, 6, 7, 8, 9]]) + + """ + if not key: + key = bifid6 + else: + _, key, _ = _prep('', key.upper(), None, bifid6) + key = padded_key(key, bifid6) + return bifid_square(key) + + +#################### RSA ############################# + +def _decipher_rsa_crt(i, d, factors): + """Decipher RSA using chinese remainder theorem from the information + of the relatively-prime factors of the modulus. + + Parameters + ========== + + i : integer + Ciphertext + + d : integer + The exponent component. + + factors : list of relatively-prime integers + The integers given must be coprime and the product must equal + the modulus component of the original RSA key. + + Examples + ======== + + How to decrypt RSA with CRT: + + >>> from sympy.crypto.crypto import rsa_public_key, rsa_private_key + >>> primes = [61, 53] + >>> e = 17 + >>> args = primes + [e] + >>> puk = rsa_public_key(*args) + >>> prk = rsa_private_key(*args) + + >>> from sympy.crypto.crypto import encipher_rsa, _decipher_rsa_crt + >>> msg = 65 + >>> crt_primes = primes + >>> encrypted = encipher_rsa(msg, puk) + >>> decrypted = _decipher_rsa_crt(encrypted, prk[1], primes) + >>> decrypted + 65 + """ + moduluses = [pow(i, d, p) for p in factors] + + result = crt(factors, moduluses) + if not result: + raise ValueError("CRT failed") + return result[0] + + +def _rsa_key(*args, public=True, private=True, totient='Euler', index=None, multipower=None): + r"""A private subroutine to generate RSA key + + Parameters + ========== + + public, private : bool, optional + Flag to generate either a public key, a private key. + + totient : 'Euler' or 'Carmichael' + Different notation used for totient. + + multipower : bool, optional + Flag to bypass warning for multipower RSA. + """ + + if len(args) < 2: + return False + + if totient not in ('Euler', 'Carmichael'): + raise ValueError( + "The argument totient={} should either be " \ + "'Euler', 'Carmichalel'." \ + .format(totient)) + + if totient == 'Euler': + _totient = _euler + else: + _totient = _carmichael + + if index is not None: + index = as_int(index) + if totient != 'Carmichael': + raise ValueError( + "Setting the 'index' keyword argument requires totient" + "notation to be specified as 'Carmichael'.") + + primes, e = args[:-1], args[-1] + + if not all(isprime(p) for p in primes): + new_primes = [] + for i in primes: + new_primes.extend(factorint(i, multiple=True)) + primes = new_primes + + n = reduce(lambda i, j: i*j, primes) + + tally = multiset(primes) + if all(v == 1 for v in tally.values()): + phi = int(_totient(tally)) + + else: + if not multipower: + NonInvertibleCipherWarning( + 'Non-distinctive primes found in the factors {}. ' + 'The cipher may not be decryptable for some numbers ' + 'in the complete residue system Z[{}], but the cipher ' + 'can still be valid if you restrict the domain to be ' + 'the reduced residue system Z*[{}]. You can pass ' + 'the flag multipower=True if you want to suppress this ' + 'warning.' + .format(primes, n, n) + # stacklevel=4 because most users will call a function that + # calls this function + ).warn(stacklevel=4) + phi = int(_totient(tally)) + + if gcd(e, phi) == 1: + if public and not private: + if isinstance(index, int): + e = e % phi + e += index * phi + return n, e + + if private and not public: + d = invert(e, phi) + if isinstance(index, int): + d += index * phi + return n, d + + return False + + +def rsa_public_key(*args, **kwargs): + r"""Return the RSA *public key* pair, `(n, e)` + + Parameters + ========== + + args : naturals + If specified as `p, q, e` where `p` and `q` are distinct primes + and `e` is a desired public exponent of the RSA, `n = p q` and + `e` will be verified against the totient + `\phi(n)` (Euler totient) or `\lambda(n)` (Carmichael totient) + to be `\gcd(e, \phi(n)) = 1` or `\gcd(e, \lambda(n)) = 1`. + + If specified as `p_1, p_2, \dots, p_n, e` where + `p_1, p_2, \dots, p_n` are specified as primes, + and `e` is specified as a desired public exponent of the RSA, + it will be able to form a multi-prime RSA, which is a more + generalized form of the popular 2-prime RSA. + + It can also be possible to form a single-prime RSA by specifying + the argument as `p, e`, which can be considered a trivial case + of a multiprime RSA. + + Furthermore, it can be possible to form a multi-power RSA by + specifying two or more pairs of the primes to be same. + However, unlike the two-distinct prime RSA or multi-prime + RSA, not every numbers in the complete residue system + (`\mathbb{Z}_n`) will be decryptable since the mapping + `\mathbb{Z}_{n} \rightarrow \mathbb{Z}_{n}` + will not be bijective. + (Only except for the trivial case when + `e = 1` + or more generally, + + .. math:: + e \in \left \{ 1 + k \lambda(n) + \mid k \in \mathbb{Z} \land k \geq 0 \right \} + + when RSA reduces to the identity.) + However, the RSA can still be decryptable for the numbers in the + reduced residue system (`\mathbb{Z}_n^{\times}`), since the + mapping + `\mathbb{Z}_{n}^{\times} \rightarrow \mathbb{Z}_{n}^{\times}` + can still be bijective. + + If you pass a non-prime integer to the arguments + `p_1, p_2, \dots, p_n`, the particular number will be + prime-factored and it will become either a multi-prime RSA or a + multi-power RSA in its canonical form, depending on whether the + product equals its radical or not. + `p_1 p_2 \dots p_n = \text{rad}(p_1 p_2 \dots p_n)` + + totient : bool, optional + If ``'Euler'``, it uses Euler's totient `\phi(n)` which is + :meth:`sympy.functions.combinatorial.numbers.totient` in SymPy. + + If ``'Carmichael'``, it uses Carmichael's totient `\lambda(n)` + which is :meth:`sympy.functions.combinatorial.numbers.reduced_totient` in SymPy. + + Unlike private key generation, this is a trivial keyword for + public key generation because + `\gcd(e, \phi(n)) = 1 \iff \gcd(e, \lambda(n)) = 1`. + + index : nonnegative integer, optional + Returns an arbitrary solution of a RSA public key at the index + specified at `0, 1, 2, \dots`. This parameter needs to be + specified along with ``totient='Carmichael'``. + + Similarly to the non-uniquenss of a RSA private key as described + in the ``index`` parameter documentation in + :meth:`rsa_private_key`, RSA public key is also not unique and + there is an infinite number of RSA public exponents which + can behave in the same manner. + + From any given RSA public exponent `e`, there are can be an + another RSA public exponent `e + k \lambda(n)` where `k` is an + integer, `\lambda` is a Carmichael's totient function. + + However, considering only the positive cases, there can be + a principal solution of a RSA public exponent `e_0` in + `0 < e_0 < \lambda(n)`, and all the other solutions + can be canonicalzed in a form of `e_0 + k \lambda(n)`. + + ``index`` specifies the `k` notation to yield any possible value + an RSA public key can have. + + An example of computing any arbitrary RSA public key: + + >>> from sympy.crypto.crypto import rsa_public_key + >>> rsa_public_key(61, 53, 17, totient='Carmichael', index=0) + (3233, 17) + >>> rsa_public_key(61, 53, 17, totient='Carmichael', index=1) + (3233, 797) + >>> rsa_public_key(61, 53, 17, totient='Carmichael', index=2) + (3233, 1577) + + multipower : bool, optional + Any pair of non-distinct primes found in the RSA specification + will restrict the domain of the cryptosystem, as noted in the + explanation of the parameter ``args``. + + SymPy RSA key generator may give a warning before dispatching it + as a multi-power RSA, however, you can disable the warning if + you pass ``True`` to this keyword. + + Returns + ======= + + (n, e) : int, int + `n` is a product of any arbitrary number of primes given as + the argument. + + `e` is relatively prime (coprime) to the Euler totient + `\phi(n)`. + + False + Returned if less than two arguments are given, or `e` is + not relatively prime to the modulus. + + Examples + ======== + + >>> from sympy.crypto.crypto import rsa_public_key + + A public key of a two-prime RSA: + + >>> p, q, e = 3, 5, 7 + >>> rsa_public_key(p, q, e) + (15, 7) + >>> rsa_public_key(p, q, 30) + False + + A public key of a multiprime RSA: + + >>> primes = [2, 3, 5, 7, 11, 13] + >>> e = 7 + >>> args = primes + [e] + >>> rsa_public_key(*args) + (30030, 7) + + Notes + ===== + + Although the RSA can be generalized over any modulus `n`, using + two large primes had became the most popular specification because a + product of two large primes is usually the hardest to factor + relatively to the digits of `n` can have. + + However, it may need further understanding of the time complexities + of each prime-factoring algorithms to verify the claim. + + See Also + ======== + + rsa_private_key + encipher_rsa + decipher_rsa + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29 + + .. [2] https://cacr.uwaterloo.ca/techreports/2006/cacr2006-16.pdf + + .. [3] https://link.springer.com/content/pdf/10.1007/BFb0055738.pdf + + .. [4] https://www.itiis.org/digital-library/manuscript/1381 + """ + return _rsa_key(*args, public=True, private=False, **kwargs) + + +def rsa_private_key(*args, **kwargs): + r"""Return the RSA *private key* pair, `(n, d)` + + Parameters + ========== + + args : naturals + The keyword is identical to the ``args`` in + :meth:`rsa_public_key`. + + totient : bool, optional + If ``'Euler'``, it uses Euler's totient convention `\phi(n)` + which is :meth:`sympy.functions.combinatorial.numbers.totient` in SymPy. + + If ``'Carmichael'``, it uses Carmichael's totient convention + `\lambda(n)` which is + :meth:`sympy.functions.combinatorial.numbers.reduced_totient` in SymPy. + + There can be some output differences for private key generation + as examples below. + + Example using Euler's totient: + + >>> from sympy.crypto.crypto import rsa_private_key + >>> rsa_private_key(61, 53, 17, totient='Euler') + (3233, 2753) + + Example using Carmichael's totient: + + >>> from sympy.crypto.crypto import rsa_private_key + >>> rsa_private_key(61, 53, 17, totient='Carmichael') + (3233, 413) + + index : nonnegative integer, optional + Returns an arbitrary solution of a RSA private key at the index + specified at `0, 1, 2, \dots`. This parameter needs to be + specified along with ``totient='Carmichael'``. + + RSA private exponent is a non-unique solution of + `e d \mod \lambda(n) = 1` and it is possible in any form of + `d + k \lambda(n)`, where `d` is an another + already-computed private exponent, and `\lambda` is a + Carmichael's totient function, and `k` is any integer. + + However, considering only the positive cases, there can be + a principal solution of a RSA private exponent `d_0` in + `0 < d_0 < \lambda(n)`, and all the other solutions + can be canonicalzed in a form of `d_0 + k \lambda(n)`. + + ``index`` specifies the `k` notation to yield any possible value + an RSA private key can have. + + An example of computing any arbitrary RSA private key: + + >>> from sympy.crypto.crypto import rsa_private_key + >>> rsa_private_key(61, 53, 17, totient='Carmichael', index=0) + (3233, 413) + >>> rsa_private_key(61, 53, 17, totient='Carmichael', index=1) + (3233, 1193) + >>> rsa_private_key(61, 53, 17, totient='Carmichael', index=2) + (3233, 1973) + + multipower : bool, optional + The keyword is identical to the ``multipower`` in + :meth:`rsa_public_key`. + + Returns + ======= + + (n, d) : int, int + `n` is a product of any arbitrary number of primes given as + the argument. + + `d` is the inverse of `e` (mod `\phi(n)`) where `e` is the + exponent given, and `\phi` is a Euler totient. + + False + Returned if less than two arguments are given, or `e` is + not relatively prime to the totient of the modulus. + + Examples + ======== + + >>> from sympy.crypto.crypto import rsa_private_key + + A private key of a two-prime RSA: + + >>> p, q, e = 3, 5, 7 + >>> rsa_private_key(p, q, e) + (15, 7) + >>> rsa_private_key(p, q, 30) + False + + A private key of a multiprime RSA: + + >>> primes = [2, 3, 5, 7, 11, 13] + >>> e = 7 + >>> args = primes + [e] + >>> rsa_private_key(*args) + (30030, 823) + + See Also + ======== + + rsa_public_key + encipher_rsa + decipher_rsa + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29 + + .. [2] https://cacr.uwaterloo.ca/techreports/2006/cacr2006-16.pdf + + .. [3] https://link.springer.com/content/pdf/10.1007/BFb0055738.pdf + + .. [4] https://www.itiis.org/digital-library/manuscript/1381 + """ + return _rsa_key(*args, public=False, private=True, **kwargs) + + +def _encipher_decipher_rsa(i, key, factors=None): + n, d = key + if not factors: + return pow(i, d, n) + + def _is_coprime_set(l): + is_coprime_set = True + for i in range(len(l)): + for j in range(i+1, len(l)): + if gcd(l[i], l[j]) != 1: + is_coprime_set = False + break + return is_coprime_set + + prod = reduce(lambda i, j: i*j, factors) + if prod == n and _is_coprime_set(factors): + return _decipher_rsa_crt(i, d, factors) + return _encipher_decipher_rsa(i, key, factors=None) + + +def encipher_rsa(i, key, factors=None): + r"""Encrypt the plaintext with RSA. + + Parameters + ========== + + i : integer + The plaintext to be encrypted for. + + key : (n, e) where n, e are integers + `n` is the modulus of the key and `e` is the exponent of the + key. The encryption is computed by `i^e \bmod n`. + + The key can either be a public key or a private key, however, + the message encrypted by a public key can only be decrypted by + a private key, and vice versa, as RSA is an asymmetric + cryptography system. + + factors : list of coprime integers + This is identical to the keyword ``factors`` in + :meth:`decipher_rsa`. + + Notes + ===== + + Some specifications may make the RSA not cryptographically + meaningful. + + For example, `0`, `1` will remain always same after taking any + number of exponentiation, thus, should be avoided. + + Furthermore, if `i^e < n`, `i` may easily be figured out by taking + `e` th root. + + And also, specifying the exponent as `1` or in more generalized form + as `1 + k \lambda(n)` where `k` is an nonnegative integer, + `\lambda` is a carmichael totient, the RSA becomes an identity + mapping. + + Examples + ======== + + >>> from sympy.crypto.crypto import encipher_rsa + >>> from sympy.crypto.crypto import rsa_public_key, rsa_private_key + + Public Key Encryption: + + >>> p, q, e = 3, 5, 7 + >>> puk = rsa_public_key(p, q, e) + >>> msg = 12 + >>> encipher_rsa(msg, puk) + 3 + + Private Key Encryption: + + >>> p, q, e = 3, 5, 7 + >>> prk = rsa_private_key(p, q, e) + >>> msg = 12 + >>> encipher_rsa(msg, prk) + 3 + + Encryption using chinese remainder theorem: + + >>> encipher_rsa(msg, prk, factors=[p, q]) + 3 + """ + return _encipher_decipher_rsa(i, key, factors=factors) + + +def decipher_rsa(i, key, factors=None): + r"""Decrypt the ciphertext with RSA. + + Parameters + ========== + + i : integer + The ciphertext to be decrypted for. + + key : (n, d) where n, d are integers + `n` is the modulus of the key and `d` is the exponent of the + key. The decryption is computed by `i^d \bmod n`. + + The key can either be a public key or a private key, however, + the message encrypted by a public key can only be decrypted by + a private key, and vice versa, as RSA is an asymmetric + cryptography system. + + factors : list of coprime integers + As the modulus `n` created from RSA key generation is composed + of arbitrary prime factors + `n = {p_1}^{k_1}{p_2}^{k_2}\dots{p_n}^{k_n}` where + `p_1, p_2, \dots, p_n` are distinct primes and + `k_1, k_2, \dots, k_n` are positive integers, chinese remainder + theorem can be used to compute `i^d \bmod n` from the + fragmented modulo operations like + + .. math:: + i^d \bmod {p_1}^{k_1}, i^d \bmod {p_2}^{k_2}, \dots, + i^d \bmod {p_n}^{k_n} + + or like + + .. math:: + i^d \bmod {p_1}^{k_1}{p_2}^{k_2}, + i^d \bmod {p_3}^{k_3}, \dots , + i^d \bmod {p_n}^{k_n} + + as long as every moduli does not share any common divisor each + other. + + The raw primes used in generating the RSA key pair can be a good + option. + + Note that the speed advantage of using this is only viable for + very large cases (Like 2048-bit RSA keys) since the + overhead of using pure Python implementation of + :meth:`sympy.ntheory.modular.crt` may overcompensate the + theoretical speed advantage. + + Notes + ===== + + See the ``Notes`` section in the documentation of + :meth:`encipher_rsa` + + Examples + ======== + + >>> from sympy.crypto.crypto import decipher_rsa, encipher_rsa + >>> from sympy.crypto.crypto import rsa_public_key, rsa_private_key + + Public Key Encryption and Decryption: + + >>> p, q, e = 3, 5, 7 + >>> prk = rsa_private_key(p, q, e) + >>> puk = rsa_public_key(p, q, e) + >>> msg = 12 + >>> new_msg = encipher_rsa(msg, prk) + >>> new_msg + 3 + >>> decipher_rsa(new_msg, puk) + 12 + + Private Key Encryption and Decryption: + + >>> p, q, e = 3, 5, 7 + >>> prk = rsa_private_key(p, q, e) + >>> puk = rsa_public_key(p, q, e) + >>> msg = 12 + >>> new_msg = encipher_rsa(msg, puk) + >>> new_msg + 3 + >>> decipher_rsa(new_msg, prk) + 12 + + Decryption using chinese remainder theorem: + + >>> decipher_rsa(new_msg, prk, factors=[p, q]) + 12 + + See Also + ======== + + encipher_rsa + """ + return _encipher_decipher_rsa(i, key, factors=factors) + + +#################### kid krypto (kid RSA) ############################# + + +def kid_rsa_public_key(a, b, A, B): + r""" + Kid RSA is a version of RSA useful to teach grade school children + since it does not involve exponentiation. + + Explanation + =========== + + Alice wants to talk to Bob. Bob generates keys as follows. + Key generation: + + * Select positive integers `a, b, A, B` at random. + * Compute `M = a b - 1`, `e = A M + a`, `d = B M + b`, + `n = (e d - 1)//M`. + * The *public key* is `(n, e)`. Bob sends these to Alice. + * The *private key* is `(n, d)`, which Bob keeps secret. + + Encryption: If `p` is the plaintext message then the + ciphertext is `c = p e \pmod n`. + + Decryption: If `c` is the ciphertext message then the + plaintext is `p = c d \pmod n`. + + Examples + ======== + + >>> from sympy.crypto.crypto import kid_rsa_public_key + >>> a, b, A, B = 3, 4, 5, 6 + >>> kid_rsa_public_key(a, b, A, B) + (369, 58) + + """ + M = a*b - 1 + e = A*M + a + d = B*M + b + n = (e*d - 1)//M + return n, e + + +def kid_rsa_private_key(a, b, A, B): + """ + Compute `M = a b - 1`, `e = A M + a`, `d = B M + b`, + `n = (e d - 1) / M`. The *private key* is `d`, which Bob + keeps secret. + + Examples + ======== + + >>> from sympy.crypto.crypto import kid_rsa_private_key + >>> a, b, A, B = 3, 4, 5, 6 + >>> kid_rsa_private_key(a, b, A, B) + (369, 70) + + """ + M = a*b - 1 + e = A*M + a + d = B*M + b + n = (e*d - 1)//M + return n, d + + +def encipher_kid_rsa(msg, key): + """ + Here ``msg`` is the plaintext and ``key`` is the public key. + + Examples + ======== + + >>> from sympy.crypto.crypto import ( + ... encipher_kid_rsa, kid_rsa_public_key) + >>> msg = 200 + >>> a, b, A, B = 3, 4, 5, 6 + >>> key = kid_rsa_public_key(a, b, A, B) + >>> encipher_kid_rsa(msg, key) + 161 + + """ + n, e = key + return (msg*e) % n + + +def decipher_kid_rsa(msg, key): + """ + Here ``msg`` is the plaintext and ``key`` is the private key. + + Examples + ======== + + >>> from sympy.crypto.crypto import ( + ... kid_rsa_public_key, kid_rsa_private_key, + ... decipher_kid_rsa, encipher_kid_rsa) + >>> a, b, A, B = 3, 4, 5, 6 + >>> d = kid_rsa_private_key(a, b, A, B) + >>> msg = 200 + >>> pub = kid_rsa_public_key(a, b, A, B) + >>> pri = kid_rsa_private_key(a, b, A, B) + >>> ct = encipher_kid_rsa(msg, pub) + >>> decipher_kid_rsa(ct, pri) + 200 + + """ + n, d = key + return (msg*d) % n + + +#################### Morse Code ###################################### + +morse_char = { + ".-": "A", "-...": "B", + "-.-.": "C", "-..": "D", + ".": "E", "..-.": "F", + "--.": "G", "....": "H", + "..": "I", ".---": "J", + "-.-": "K", ".-..": "L", + "--": "M", "-.": "N", + "---": "O", ".--.": "P", + "--.-": "Q", ".-.": "R", + "...": "S", "-": "T", + "..-": "U", "...-": "V", + ".--": "W", "-..-": "X", + "-.--": "Y", "--..": "Z", + "-----": "0", ".----": "1", + "..---": "2", "...--": "3", + "....-": "4", ".....": "5", + "-....": "6", "--...": "7", + "---..": "8", "----.": "9", + ".-.-.-": ".", "--..--": ",", + "---...": ":", "-.-.-.": ";", + "..--..": "?", "-....-": "-", + "..--.-": "_", "-.--.": "(", + "-.--.-": ")", ".----.": "'", + "-...-": "=", ".-.-.": "+", + "-..-.": "/", ".--.-.": "@", + "...-..-": "$", "-.-.--": "!"} +char_morse = {v: k for k, v in morse_char.items()} + + +def encode_morse(msg, sep='|', mapping=None): + """ + Encodes a plaintext into popular Morse Code with letters + separated by ``sep`` and words by a double ``sep``. + + Examples + ======== + + >>> from sympy.crypto.crypto import encode_morse + >>> msg = 'ATTACK RIGHT FLANK' + >>> encode_morse(msg) + '.-|-|-|.-|-.-.|-.-||.-.|..|--.|....|-||..-.|.-..|.-|-.|-.-' + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Morse_code + + """ + + mapping = mapping or char_morse + assert sep not in mapping + word_sep = 2*sep + mapping[" "] = word_sep + suffix = msg and msg[-1] in whitespace + + # normalize whitespace + msg = (' ' if word_sep else '').join(msg.split()) + # omit unmapped chars + chars = set(''.join(msg.split())) + ok = set(mapping.keys()) + msg = translate(msg, None, ''.join(chars - ok)) + + morsestring = [] + words = msg.split() + for word in words: + morseword = [] + for letter in word: + morseletter = mapping[letter] + morseword.append(morseletter) + + word = sep.join(morseword) + morsestring.append(word) + + return word_sep.join(morsestring) + (word_sep if suffix else '') + + +def decode_morse(msg, sep='|', mapping=None): + """ + Decodes a Morse Code with letters separated by ``sep`` + (default is '|') and words by `word_sep` (default is '||) + into plaintext. + + Examples + ======== + + >>> from sympy.crypto.crypto import decode_morse + >>> mc = '--|---|...-|.||.|.-|...|-' + >>> decode_morse(mc) + 'MOVE EAST' + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Morse_code + + """ + + mapping = mapping or morse_char + word_sep = 2*sep + characterstring = [] + words = msg.strip(word_sep).split(word_sep) + for word in words: + letters = word.split(sep) + chars = [mapping[c] for c in letters] + word = ''.join(chars) + characterstring.append(word) + rv = " ".join(characterstring) + return rv + + +#################### LFSRs ########################################## + + +@doctest_depends_on(ground_types=['python', 'gmpy']) +def lfsr_sequence(key, fill, n): + r""" + This function creates an LFSR sequence. + + Parameters + ========== + + key : list + A list of finite field elements, `[c_0, c_1, \ldots, c_k].` + + fill : list + The list of the initial terms of the LFSR sequence, + `[x_0, x_1, \ldots, x_k].` + + n + Number of terms of the sequence that the function returns. + + Returns + ======= + + L + The LFSR sequence defined by + `x_{n+1} = c_k x_n + \ldots + c_0 x_{n-k}`, for + `n \leq k`. + + Notes + ===== + + S. Golomb [G]_ gives a list of three statistical properties a + sequence of numbers `a = \{a_n\}_{n=1}^\infty`, + `a_n \in \{0,1\}`, should display to be considered + "random". Define the autocorrelation of `a` to be + + .. math:: + + C(k) = C(k,a) = \lim_{N\rightarrow \infty} {1\over N}\sum_{n=1}^N (-1)^{a_n + a_{n+k}}. + + In the case where `a` is periodic with period + `P` then this reduces to + + .. math:: + + C(k) = {1\over P}\sum_{n=1}^P (-1)^{a_n + a_{n+k}}. + + Assume `a` is periodic with period `P`. + + - balance: + + .. math:: + + \left|\sum_{n=1}^P(-1)^{a_n}\right| \leq 1. + + - low autocorrelation: + + .. math:: + + C(k) = \left\{ \begin{array}{cc} 1,& k = 0,\\ \epsilon, & k \ne 0. \end{array} \right. + + (For sequences satisfying these first two properties, it is known + that `\epsilon = -1/P` must hold.) + + - proportional runs property: In each period, half the runs have + length `1`, one-fourth have length `2`, etc. + Moreover, there are as many runs of `1`'s as there are of + `0`'s. + + Examples + ======== + + >>> from sympy.crypto.crypto import lfsr_sequence + >>> from sympy.polys.domains import FF + >>> F = FF(2) + >>> fill = [F(1), F(1), F(0), F(1)] + >>> key = [F(1), F(0), F(0), F(1)] + >>> lfsr_sequence(key, fill, 10) + [1 mod 2, 1 mod 2, 0 mod 2, 1 mod 2, 0 mod 2, + 1 mod 2, 1 mod 2, 0 mod 2, 0 mod 2, 1 mod 2] + + References + ========== + + .. [G] Solomon Golomb, Shift register sequences, Aegean Park Press, + Laguna Hills, Ca, 1967 + + """ + if not isinstance(key, list): + raise TypeError("key must be a list") + if not isinstance(fill, list): + raise TypeError("fill must be a list") + p = key[0].modulus() + F = FF(p) + s = fill + k = len(fill) + L = [] + for i in range(n): + s0 = s[:] + L.append(s[0]) + s = s[1:k] + x = sum(int(key[i]*s0[i]) for i in range(k)) + s.append(F(x)) + return L # use [int(x) for x in L] for int version + + +def lfsr_autocorrelation(L, P, k): + """ + This function computes the LFSR autocorrelation function. + + Parameters + ========== + + L + A periodic sequence of elements of `GF(2)`. + L must have length larger than P. + + P + The period of L. + + k : int + An integer `k` (`0 < k < P`). + + Returns + ======= + + autocorrelation + The k-th value of the autocorrelation of the LFSR L. + + Examples + ======== + + >>> from sympy.crypto.crypto import ( + ... lfsr_sequence, lfsr_autocorrelation) + >>> from sympy.polys.domains import FF + >>> F = FF(2) + >>> fill = [F(1), F(1), F(0), F(1)] + >>> key = [F(1), F(0), F(0), F(1)] + >>> s = lfsr_sequence(key, fill, 20) + >>> lfsr_autocorrelation(s, 15, 7) + -1/15 + >>> lfsr_autocorrelation(s, 15, 0) + 1 + + """ + if not isinstance(L, list): + raise TypeError("L (=%s) must be a list" % L) + P = int(P) + k = int(k) + L0 = L[:P] # slices makes a copy + L1 = L0 + L0[:k] + L2 = [(-1)**(int(L1[i]) + int(L1[i + k])) for i in range(P)] + tot = sum(L2) + return Rational(tot, P) + + +def lfsr_connection_polynomial(s): + """ + This function computes the LFSR connection polynomial. + + Parameters + ========== + + s + A sequence of elements of even length, with entries in a finite + field. + + Returns + ======= + + C(x) + The connection polynomial of a minimal LFSR yielding s. + + This implements the algorithm in section 3 of J. L. Massey's + article [M]_. + + Examples + ======== + + >>> from sympy.crypto.crypto import ( + ... lfsr_sequence, lfsr_connection_polynomial) + >>> from sympy.polys.domains import FF + >>> F = FF(2) + >>> fill = [F(1), F(1), F(0), F(1)] + >>> key = [F(1), F(0), F(0), F(1)] + >>> s = lfsr_sequence(key, fill, 20) + >>> lfsr_connection_polynomial(s) + x**4 + x + 1 + >>> fill = [F(1), F(0), F(0), F(1)] + >>> key = [F(1), F(1), F(0), F(1)] + >>> s = lfsr_sequence(key, fill, 20) + >>> lfsr_connection_polynomial(s) + x**3 + 1 + >>> fill = [F(1), F(0), F(1)] + >>> key = [F(1), F(1), F(0)] + >>> s = lfsr_sequence(key, fill, 20) + >>> lfsr_connection_polynomial(s) + x**3 + x**2 + 1 + >>> fill = [F(1), F(0), F(1)] + >>> key = [F(1), F(0), F(1)] + >>> s = lfsr_sequence(key, fill, 20) + >>> lfsr_connection_polynomial(s) + x**3 + x + 1 + + References + ========== + + .. [M] James L. Massey, "Shift-Register Synthesis and BCH Decoding." + IEEE Trans. on Information Theory, vol. 15(1), pp. 122-127, + Jan 1969. + + """ + # Initialization: + p = s[0].modulus() + x = Symbol("x") + C = 1*x**0 + B = 1*x**0 + m = 1 + b = 1*x**0 + L = 0 + N = 0 + while N < len(s): + if L > 0: + dC = Poly(C).degree() + r = min(L + 1, dC + 1) + coeffsC = [C.subs(x, 0)] + [C.coeff(x**i) + for i in range(1, dC + 1)] + d = (int(s[N]) + sum(coeffsC[i]*int(s[N - i]) + for i in range(1, r))) % p + if L == 0: + d = int(s[N])*x**0 + if d == 0: + m += 1 + N += 1 + if d > 0: + if 2*L > N: + C = (C - d*((b**(p - 2)) % p)*x**m*B).expand() + m += 1 + N += 1 + else: + T = C + C = (C - d*((b**(p - 2)) % p)*x**m*B).expand() + L = N + 1 - L + m = 1 + b = d + B = T + N += 1 + dC = Poly(C).degree() + coeffsC = [C.subs(x, 0)] + [C.coeff(x**i) for i in range(1, dC + 1)] + return sum(coeffsC[i] % p*x**i for i in range(dC + 1) + if coeffsC[i] is not None) + + +#################### ElGamal ############################# + + +def elgamal_private_key(digit=10, seed=None): + r""" + Return three number tuple as private key. + + Explanation + =========== + + Elgamal encryption is based on the mathematical problem + called the Discrete Logarithm Problem (DLP). For example, + + `a^{b} \equiv c \pmod p` + + In general, if ``a`` and ``b`` are known, ``ct`` is easily + calculated. If ``b`` is unknown, it is hard to use + ``a`` and ``ct`` to get ``b``. + + Parameters + ========== + + digit : int + Minimum number of binary digits for key. + + Returns + ======= + + tuple : (p, r, d) + p = prime number. + + r = primitive root. + + d = random number. + + Notes + ===== + + For testing purposes, the ``seed`` parameter may be set to control + the output of this routine. See sympy.core.random._randrange. + + Examples + ======== + + >>> from sympy.crypto.crypto import elgamal_private_key + >>> from sympy.ntheory import is_primitive_root, isprime + >>> a, b, _ = elgamal_private_key() + >>> isprime(a) + True + >>> is_primitive_root(b, a) + True + + """ + randrange = _randrange(seed) + p = nextprime(2**digit) + return p, primitive_root(p), randrange(2, p) + + +def elgamal_public_key(key): + r""" + Return three number tuple as public key. + + Parameters + ========== + + key : (p, r, e) + Tuple generated by ``elgamal_private_key``. + + Returns + ======= + + tuple : (p, r, e) + `e = r**d \bmod p` + + `d` is a random number in private key. + + Examples + ======== + + >>> from sympy.crypto.crypto import elgamal_public_key + >>> elgamal_public_key((1031, 14, 636)) + (1031, 14, 212) + + """ + p, r, e = key + return p, r, pow(r, e, p) + + +def encipher_elgamal(i, key, seed=None): + r""" + Encrypt message with public key. + + Explanation + =========== + + ``i`` is a plaintext message expressed as an integer. + ``key`` is public key (p, r, e). In order to encrypt + a message, a random number ``a`` in ``range(2, p)`` + is generated and the encrypted message is returned as + `c_{1}` and `c_{2}` where: + + `c_{1} \equiv r^{a} \pmod p` + + `c_{2} \equiv m e^{a} \pmod p` + + Parameters + ========== + + msg + int of encoded message. + + key + Public key. + + Returns + ======= + + tuple : (c1, c2) + Encipher into two number. + + Notes + ===== + + For testing purposes, the ``seed`` parameter may be set to control + the output of this routine. See sympy.core.random._randrange. + + Examples + ======== + + >>> from sympy.crypto.crypto import encipher_elgamal, elgamal_private_key, elgamal_public_key + >>> pri = elgamal_private_key(5, seed=[3]); pri + (37, 2, 3) + >>> pub = elgamal_public_key(pri); pub + (37, 2, 8) + >>> msg = 36 + >>> encipher_elgamal(msg, pub, seed=[3]) + (8, 6) + + """ + p, r, e = key + if i < 0 or i >= p: + raise ValueError( + 'Message (%s) should be in range(%s)' % (i, p)) + randrange = _randrange(seed) + a = randrange(2, p) + return pow(r, a, p), i*pow(e, a, p) % p + + +def decipher_elgamal(msg, key): + r""" + Decrypt message with private key. + + `msg = (c_{1}, c_{2})` + + `key = (p, r, d)` + + According to extended Eucliden theorem, + `u c_{1}^{d} + p n = 1` + + `u \equiv 1/{{c_{1}}^d} \pmod p` + + `u c_{2} \equiv \frac{1}{c_{1}^d} c_{2} \equiv \frac{1}{r^{ad}} c_{2} \pmod p` + + `\frac{1}{r^{ad}} m e^a \equiv \frac{1}{r^{ad}} m {r^{d a}} \equiv m \pmod p` + + Examples + ======== + + >>> from sympy.crypto.crypto import decipher_elgamal + >>> from sympy.crypto.crypto import encipher_elgamal + >>> from sympy.crypto.crypto import elgamal_private_key + >>> from sympy.crypto.crypto import elgamal_public_key + + >>> pri = elgamal_private_key(5, seed=[3]) + >>> pub = elgamal_public_key(pri); pub + (37, 2, 8) + >>> msg = 17 + >>> decipher_elgamal(encipher_elgamal(msg, pub), pri) == msg + True + + """ + p, _, d = key + c1, c2 = msg + u = pow(c1, -d, p) + return u * c2 % p + + +################ Diffie-Hellman Key Exchange ######################### + +def dh_private_key(digit=10, seed=None): + r""" + Return three integer tuple as private key. + + Explanation + =========== + + Diffie-Hellman key exchange is based on the mathematical problem + called the Discrete Logarithm Problem (see ElGamal). + + Diffie-Hellman key exchange is divided into the following steps: + + * Alice and Bob agree on a base that consist of a prime ``p`` + and a primitive root of ``p`` called ``g`` + * Alice choses a number ``a`` and Bob choses a number ``b`` where + ``a`` and ``b`` are random numbers in range `[2, p)`. These are + their private keys. + * Alice then publicly sends Bob `g^{a} \pmod p` while Bob sends + Alice `g^{b} \pmod p` + * They both raise the received value to their secretly chosen + number (``a`` or ``b``) and now have both as their shared key + `g^{ab} \pmod p` + + Parameters + ========== + + digit + Minimum number of binary digits required in key. + + Returns + ======= + + tuple : (p, g, a) + p = prime number. + + g = primitive root of p. + + a = random number from 2 through p - 1. + + Notes + ===== + + For testing purposes, the ``seed`` parameter may be set to control + the output of this routine. See sympy.core.random._randrange. + + Examples + ======== + + >>> from sympy.crypto.crypto import dh_private_key + >>> from sympy.ntheory import isprime, is_primitive_root + >>> p, g, _ = dh_private_key() + >>> isprime(p) + True + >>> is_primitive_root(g, p) + True + >>> p, g, _ = dh_private_key(5) + >>> isprime(p) + True + >>> is_primitive_root(g, p) + True + + """ + p = nextprime(2**digit) + g = primitive_root(p) + randrange = _randrange(seed) + a = randrange(2, p) + return p, g, a + + +def dh_public_key(key): + r""" + Return three number tuple as public key. + + This is the tuple that Alice sends to Bob. + + Parameters + ========== + + key : (p, g, a) + A tuple generated by ``dh_private_key``. + + Returns + ======= + + tuple : int, int, int + A tuple of `(p, g, g^a \mod p)` with `p`, `g` and `a` given as + parameters.s + + Examples + ======== + + >>> from sympy.crypto.crypto import dh_private_key, dh_public_key + >>> p, g, a = dh_private_key(); + >>> _p, _g, x = dh_public_key((p, g, a)) + >>> p == _p and g == _g + True + >>> x == pow(g, a, p) + True + + """ + p, g, a = key + return p, g, pow(g, a, p) + + +def dh_shared_key(key, b): + """ + Return an integer that is the shared key. + + This is what Bob and Alice can both calculate using the public + keys they received from each other and their private keys. + + Parameters + ========== + + key : (p, g, x) + Tuple `(p, g, x)` generated by ``dh_public_key``. + + b + Random number in the range of `2` to `p - 1` + (Chosen by second key exchange member (Bob)). + + Returns + ======= + + int + A shared key. + + Examples + ======== + + >>> from sympy.crypto.crypto import ( + ... dh_private_key, dh_public_key, dh_shared_key) + >>> prk = dh_private_key(); + >>> p, g, x = dh_public_key(prk); + >>> sk = dh_shared_key((p, g, x), 1000) + >>> sk == pow(x, 1000, p) + True + + """ + p, _, x = key + if 1 >= b or b >= p: + raise ValueError(filldedent(''' + Value of b should be greater 1 and less + than prime %s.''' % p)) + + return pow(x, b, p) + + +################ Goldwasser-Micali Encryption ######################### + + +def _legendre(a, p): + """ + Returns the legendre symbol of a and p + assuming that p is a prime. + + i.e. 1 if a is a quadratic residue mod p + -1 if a is not a quadratic residue mod p + 0 if a is divisible by p + + Parameters + ========== + + a : int + The number to test. + + p : prime + The prime to test ``a`` against. + + Returns + ======= + + int + Legendre symbol (a / p). + + """ + sig = pow(a, (p - 1)//2, p) + if sig == 1: + return 1 + elif sig == 0: + return 0 + else: + return -1 + + +def _random_coprime_stream(n, seed=None): + randrange = _randrange(seed) + while True: + y = randrange(n) + if gcd(y, n) == 1: + yield y + + +def gm_private_key(p, q, a=None): + r""" + Check if ``p`` and ``q`` can be used as private keys for + the Goldwasser-Micali encryption. The method works + roughly as follows. + + Explanation + =========== + + #. Pick two large primes $p$ and $q$. + #. Call their product $N$. + #. Given a message as an integer $i$, write $i$ in its bit representation $b_0, \dots, b_n$. + #. For each $k$, + + if $b_k = 0$: + let $a_k$ be a random square + (quadratic residue) modulo $p q$ + such that ``jacobi_symbol(a, p*q) = 1`` + if $b_k = 1$: + let $a_k$ be a random non-square + (non-quadratic residue) modulo $p q$ + such that ``jacobi_symbol(a, p*q) = 1`` + + returns $\left[a_1, a_2, \dots\right]$ + + $b_k$ can be recovered by checking whether or not + $a_k$ is a residue. And from the $b_k$'s, the message + can be reconstructed. + + The idea is that, while ``jacobi_symbol(a, p*q)`` + can be easily computed (and when it is equal to $-1$ will + tell you that $a$ is not a square mod $p q$), quadratic + residuosity modulo a composite number is hard to compute + without knowing its factorization. + + Moreover, approximately half the numbers coprime to $p q$ have + :func:`~.jacobi_symbol` equal to $1$ . And among those, approximately half + are residues and approximately half are not. This maximizes the + entropy of the code. + + Parameters + ========== + + p, q, a + Initialization variables. + + Returns + ======= + + tuple : (p, q) + The input value ``p`` and ``q``. + + Raises + ====== + + ValueError + If ``p`` and ``q`` are not distinct odd primes. + + """ + if p == q: + raise ValueError("expected distinct primes, " + "got two copies of %i" % p) + elif not isprime(p) or not isprime(q): + raise ValueError("first two arguments must be prime, " + "got %i of %i" % (p, q)) + elif p == 2 or q == 2: + raise ValueError("first two arguments must not be even, " + "got %i of %i" % (p, q)) + return p, q + + +def gm_public_key(p, q, a=None, seed=None): + """ + Compute public keys for ``p`` and ``q``. + Note that in Goldwasser-Micali Encryption, + public keys are randomly selected. + + Parameters + ========== + + p, q, a : int, int, int + Initialization variables. + + Returns + ======= + + tuple : (a, N) + ``a`` is the input ``a`` if it is not ``None`` otherwise + some random integer coprime to ``p`` and ``q``. + + ``N`` is the product of ``p`` and ``q``. + + """ + + p, q = gm_private_key(p, q) + N = p * q + + if a is None: + randrange = _randrange(seed) + while True: + a = randrange(N) + if _legendre(a, p) == _legendre(a, q) == -1: + break + else: + if _legendre(a, p) != -1 or _legendre(a, q) != -1: + return False + return (a, N) + + +def encipher_gm(i, key, seed=None): + """ + Encrypt integer 'i' using public_key 'key' + Note that gm uses random encryption. + + Parameters + ========== + + i : int + The message to encrypt. + + key : (a, N) + The public key. + + Returns + ======= + + list : list of int + The randomized encrypted message. + + """ + if i < 0: + raise ValueError( + "message must be a non-negative " + "integer: got %d instead" % i) + a, N = key + bits = [] + while i > 0: + bits.append(i % 2) + i //= 2 + + gen = _random_coprime_stream(N, seed) + rev = reversed(bits) + encode = lambda b: next(gen)**2*pow(a, b) % N + return [ encode(b) for b in rev ] + + + +def decipher_gm(message, key): + """ + Decrypt message 'message' using public_key 'key'. + + Parameters + ========== + + message : list of int + The randomized encrypted message. + + key : (p, q) + The private key. + + Returns + ======= + + int + The encrypted message. + + """ + p, q = key + res = lambda m, p: _legendre(m, p) > 0 + bits = [res(m, p) * res(m, q) for m in message] + m = 0 + for b in bits: + m <<= 1 + m += not b + return m + + + +########### RailFence Cipher ############# + +def encipher_railfence(message,rails): + """ + Performs Railfence Encryption on plaintext and returns ciphertext + + Examples + ======== + + >>> from sympy.crypto.crypto import encipher_railfence + >>> message = "hello world" + >>> encipher_railfence(message,3) + 'horel ollwd' + + Parameters + ========== + + message : string, the message to encrypt. + rails : int, the number of rails. + + Returns + ======= + + The Encrypted string message. + + References + ========== + .. [1] https://en.wikipedia.org/wiki/Rail_fence_cipher + + """ + r = list(range(rails)) + p = cycle(r + r[-2:0:-1]) + return ''.join(sorted(message, key=lambda i: next(p))) + + +def decipher_railfence(ciphertext,rails): + """ + Decrypt the message using the given rails + + Examples + ======== + + >>> from sympy.crypto.crypto import decipher_railfence + >>> decipher_railfence("horel ollwd",3) + 'hello world' + + Parameters + ========== + + message : string, the message to encrypt. + rails : int, the number of rails. + + Returns + ======= + + The Decrypted string message. + + """ + r = list(range(rails)) + p = cycle(r + r[-2:0:-1]) + + idx = sorted(range(len(ciphertext)), key=lambda i: next(p)) + res = [''] * len(ciphertext) + for i, c in zip(idx, ciphertext): + res[i] = c + return ''.join(res) + + +################ Blum-Goldwasser cryptosystem ######################### + +def bg_private_key(p, q): + """ + Check if p and q can be used as private keys for + the Blum-Goldwasser cryptosystem. + + Explanation + =========== + + The three necessary checks for p and q to pass + so that they can be used as private keys: + + 1. p and q must both be prime + 2. p and q must be distinct + 3. p and q must be congruent to 3 mod 4 + + Parameters + ========== + + p, q + The keys to be checked. + + Returns + ======= + + p, q + Input values. + + Raises + ====== + + ValueError + If p and q do not pass the above conditions. + + """ + + if not isprime(p) or not isprime(q): + raise ValueError("the two arguments must be prime, " + "got %i and %i" %(p, q)) + elif p == q: + raise ValueError("the two arguments must be distinct, " + "got two copies of %i. " %p) + elif (p - 3) % 4 != 0 or (q - 3) % 4 != 0: + raise ValueError("the two arguments must be congruent to 3 mod 4, " + "got %i and %i" %(p, q)) + return p, q + +def bg_public_key(p, q): + """ + Calculates public keys from private keys. + + Explanation + =========== + + The function first checks the validity of + private keys passed as arguments and + then returns their product. + + Parameters + ========== + + p, q + The private keys. + + Returns + ======= + + N + The public key. + + """ + p, q = bg_private_key(p, q) + N = p * q + return N + +def encipher_bg(i, key, seed=None): + """ + Encrypts the message using public key and seed. + + Explanation + =========== + + ALGORITHM: + 1. Encodes i as a string of L bits, m. + 2. Select a random element r, where 1 < r < key, and computes + x = r^2 mod key. + 3. Use BBS pseudo-random number generator to generate L random bits, b, + using the initial seed as x. + 4. Encrypted message, c_i = m_i XOR b_i, 1 <= i <= L. + 5. x_L = x^(2^L) mod key. + 6. Return (c, x_L) + + Parameters + ========== + + i + Message, a non-negative integer + + key + The public key + + Returns + ======= + + Tuple + (encrypted_message, x_L) + + Raises + ====== + + ValueError + If i is negative. + + """ + + if i < 0: + raise ValueError( + "message must be a non-negative " + "integer: got %d instead" % i) + + enc_msg = [] + while i > 0: + enc_msg.append(i % 2) + i //= 2 + enc_msg.reverse() + L = len(enc_msg) + + r = _randint(seed)(2, key - 1) + x = r**2 % key + x_L = pow(int(x), int(2**L), int(key)) + + rand_bits = [] + for _ in range(L): + rand_bits.append(x % 2) + x = x**2 % key + + encrypt_msg = [m ^ b for (m, b) in zip(enc_msg, rand_bits)] + + return (encrypt_msg, x_L) + +def decipher_bg(message, key): + """ + Decrypts the message using private keys. + + Explanation + =========== + + ALGORITHM: + 1. Let, c be the encrypted message, y the second number received, + and p and q be the private keys. + 2. Compute, r_p = y^((p+1)/4 ^ L) mod p and + r_q = y^((q+1)/4 ^ L) mod q. + 3. Compute x_0 = (q(q^-1 mod p)r_p + p(p^-1 mod q)r_q) mod N. + 4. From, recompute the bits using the BBS generator, as in the + encryption algorithm. + 5. Compute original message by XORing c and b. + + Parameters + ========== + + message + Tuple of encrypted message and a non-negative integer. + + key + Tuple of private keys. + + Returns + ======= + + orig_msg + The original message + + """ + + p, q = key + encrypt_msg, y = message + public_key = p * q + L = len(encrypt_msg) + p_t = ((p + 1)/4)**L + q_t = ((q + 1)/4)**L + r_p = pow(int(y), int(p_t), int(p)) + r_q = pow(int(y), int(q_t), int(q)) + + x = (q * invert(q, p) * r_p + p * invert(p, q) * r_q) % public_key + + orig_bits = [] + for _ in range(L): + orig_bits.append(x % 2) + x = x**2 % public_key + + orig_msg = 0 + for (m, b) in zip(encrypt_msg, orig_bits): + orig_msg = orig_msg * 2 + orig_msg += (m ^ b) + + return orig_msg diff --git a/venv/lib/python3.11/site-packages/sympy/crypto/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/crypto/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/crypto/tests/test_crypto.py b/venv/lib/python3.11/site-packages/sympy/crypto/tests/test_crypto.py new file mode 100644 index 0000000000000000000000000000000000000000..c671138f9a61325f6e65cc7cafddc7cd46f19229 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/crypto/tests/test_crypto.py @@ -0,0 +1,562 @@ +from sympy.core import symbols +from sympy.crypto.crypto import (cycle_list, + encipher_shift, encipher_affine, encipher_substitution, + check_and_join, encipher_vigenere, decipher_vigenere, + encipher_hill, decipher_hill, encipher_bifid5, encipher_bifid6, + bifid5_square, bifid6_square, bifid5, bifid6, + decipher_bifid5, decipher_bifid6, encipher_kid_rsa, + decipher_kid_rsa, kid_rsa_private_key, kid_rsa_public_key, + decipher_rsa, rsa_private_key, rsa_public_key, encipher_rsa, + lfsr_connection_polynomial, lfsr_autocorrelation, lfsr_sequence, + encode_morse, decode_morse, elgamal_private_key, elgamal_public_key, + encipher_elgamal, decipher_elgamal, dh_private_key, dh_public_key, + dh_shared_key, decipher_shift, decipher_affine, encipher_bifid, + decipher_bifid, bifid_square, padded_key, uniq, decipher_gm, + encipher_gm, gm_public_key, gm_private_key, encipher_bg, decipher_bg, + bg_private_key, bg_public_key, encipher_rot13, decipher_rot13, + encipher_atbash, decipher_atbash, NonInvertibleCipherWarning, + encipher_railfence, decipher_railfence) +from sympy.external.gmpy import gcd +from sympy.matrices import Matrix +from sympy.ntheory import isprime, is_primitive_root +from sympy.polys.domains import FF + +from sympy.testing.pytest import raises, warns + +from sympy.core.random import randrange + +def test_encipher_railfence(): + assert encipher_railfence("hello world",2) == "hlowrdel ol" + assert encipher_railfence("hello world",3) == "horel ollwd" + assert encipher_railfence("hello world",4) == "hwe olordll" + +def test_decipher_railfence(): + assert decipher_railfence("hlowrdel ol",2) == "hello world" + assert decipher_railfence("horel ollwd",3) == "hello world" + assert decipher_railfence("hwe olordll",4) == "hello world" + + +def test_cycle_list(): + assert cycle_list(3, 4) == [3, 0, 1, 2] + assert cycle_list(-1, 4) == [3, 0, 1, 2] + assert cycle_list(1, 4) == [1, 2, 3, 0] + + +def test_encipher_shift(): + assert encipher_shift("ABC", 0) == "ABC" + assert encipher_shift("ABC", 1) == "BCD" + assert encipher_shift("ABC", -1) == "ZAB" + assert decipher_shift("ZAB", -1) == "ABC" + +def test_encipher_rot13(): + assert encipher_rot13("ABC") == "NOP" + assert encipher_rot13("NOP") == "ABC" + assert decipher_rot13("ABC") == "NOP" + assert decipher_rot13("NOP") == "ABC" + + +def test_encipher_affine(): + assert encipher_affine("ABC", (1, 0)) == "ABC" + assert encipher_affine("ABC", (1, 1)) == "BCD" + assert encipher_affine("ABC", (-1, 0)) == "AZY" + assert encipher_affine("ABC", (-1, 1), symbols="ABCD") == "BAD" + assert encipher_affine("123", (-1, 1), symbols="1234") == "214" + assert encipher_affine("ABC", (3, 16)) == "QTW" + assert decipher_affine("QTW", (3, 16)) == "ABC" + +def test_encipher_atbash(): + assert encipher_atbash("ABC") == "ZYX" + assert encipher_atbash("ZYX") == "ABC" + assert decipher_atbash("ABC") == "ZYX" + assert decipher_atbash("ZYX") == "ABC" + +def test_encipher_substitution(): + assert encipher_substitution("ABC", "BAC", "ABC") == "BAC" + assert encipher_substitution("123", "1243", "1234") == "124" + + +def test_check_and_join(): + assert check_and_join("abc") == "abc" + assert check_and_join(uniq("aaabc")) == "abc" + assert check_and_join("ab c".split()) == "abc" + assert check_and_join("abc", "a", filter=True) == "a" + raises(ValueError, lambda: check_and_join('ab', 'a')) + + +def test_encipher_vigenere(): + assert encipher_vigenere("ABC", "ABC") == "ACE" + assert encipher_vigenere("ABC", "ABC", symbols="ABCD") == "ACA" + assert encipher_vigenere("ABC", "AB", symbols="ABCD") == "ACC" + assert encipher_vigenere("AB", "ABC", symbols="ABCD") == "AC" + assert encipher_vigenere("A", "ABC", symbols="ABCD") == "A" + + +def test_decipher_vigenere(): + assert decipher_vigenere("ABC", "ABC") == "AAA" + assert decipher_vigenere("ABC", "ABC", symbols="ABCD") == "AAA" + assert decipher_vigenere("ABC", "AB", symbols="ABCD") == "AAC" + assert decipher_vigenere("AB", "ABC", symbols="ABCD") == "AA" + assert decipher_vigenere("A", "ABC", symbols="ABCD") == "A" + + +def test_encipher_hill(): + A = Matrix(2, 2, [1, 2, 3, 5]) + assert encipher_hill("ABCD", A) == "CFIV" + A = Matrix(2, 2, [1, 0, 0, 1]) + assert encipher_hill("ABCD", A) == "ABCD" + assert encipher_hill("ABCD", A, symbols="ABCD") == "ABCD" + A = Matrix(2, 2, [1, 2, 3, 5]) + assert encipher_hill("ABCD", A, symbols="ABCD") == "CBAB" + assert encipher_hill("AB", A, symbols="ABCD") == "CB" + # message length, n, does not need to be a multiple of k; + # it is padded + assert encipher_hill("ABA", A) == "CFGC" + assert encipher_hill("ABA", A, pad="Z") == "CFYV" + + +def test_decipher_hill(): + A = Matrix(2, 2, [1, 2, 3, 5]) + assert decipher_hill("CFIV", A) == "ABCD" + A = Matrix(2, 2, [1, 0, 0, 1]) + assert decipher_hill("ABCD", A) == "ABCD" + assert decipher_hill("ABCD", A, symbols="ABCD") == "ABCD" + A = Matrix(2, 2, [1, 2, 3, 5]) + assert decipher_hill("CBAB", A, symbols="ABCD") == "ABCD" + assert decipher_hill("CB", A, symbols="ABCD") == "AB" + # n does not need to be a multiple of k + assert decipher_hill("CFA", A) == "ABAA" + + +def test_encipher_bifid5(): + assert encipher_bifid5("AB", "AB") == "AB" + assert encipher_bifid5("AB", "CD") == "CO" + assert encipher_bifid5("ab", "c") == "CH" + assert encipher_bifid5("a bc", "b") == "BAC" + + +def test_bifid5_square(): + A = bifid5 + f = lambda i, j: symbols(A[5*i + j]) + M = Matrix(5, 5, f) + assert bifid5_square("") == M + + +def test_decipher_bifid5(): + assert decipher_bifid5("AB", "AB") == "AB" + assert decipher_bifid5("CO", "CD") == "AB" + assert decipher_bifid5("ch", "c") == "AB" + assert decipher_bifid5("b ac", "b") == "ABC" + + +def test_encipher_bifid6(): + assert encipher_bifid6("AB", "AB") == "AB" + assert encipher_bifid6("AB", "CD") == "CP" + assert encipher_bifid6("ab", "c") == "CI" + assert encipher_bifid6("a bc", "b") == "BAC" + + +def test_decipher_bifid6(): + assert decipher_bifid6("AB", "AB") == "AB" + assert decipher_bifid6("CP", "CD") == "AB" + assert decipher_bifid6("ci", "c") == "AB" + assert decipher_bifid6("b ac", "b") == "ABC" + + +def test_bifid6_square(): + A = bifid6 + f = lambda i, j: symbols(A[6*i + j]) + M = Matrix(6, 6, f) + assert bifid6_square("") == M + + +def test_rsa_public_key(): + assert rsa_public_key(2, 3, 1) == (6, 1) + assert rsa_public_key(5, 3, 3) == (15, 3) + + with warns(NonInvertibleCipherWarning): + assert rsa_public_key(2, 2, 1) == (4, 1) + assert rsa_public_key(8, 8, 8) is False + + +def test_rsa_private_key(): + assert rsa_private_key(2, 3, 1) == (6, 1) + assert rsa_private_key(5, 3, 3) == (15, 3) + assert rsa_private_key(23,29,5) == (667,493) + + with warns(NonInvertibleCipherWarning): + assert rsa_private_key(2, 2, 1) == (4, 1) + assert rsa_private_key(8, 8, 8) is False + + +def test_rsa_large_key(): + # Sample from + # http://www.herongyang.com/Cryptography/JCE-Public-Key-RSA-Private-Public-Key-Pair-Sample.html + p = int('101565610013301240713207239558950144682174355406589305284428666'\ + '903702505233009') + q = int('894687191887545488935455605955948413812376003053143521429242133'\ + '12069293984003') + e = int('65537') + d = int('893650581832704239530398858744759129594796235440844479456143566'\ + '6999402846577625762582824202269399672579058991442587406384754958587'\ + '400493169361356902030209') + assert rsa_public_key(p, q, e) == (p*q, e) + assert rsa_private_key(p, q, e) == (p*q, d) + + +def test_encipher_rsa(): + puk = rsa_public_key(2, 3, 1) + assert encipher_rsa(2, puk) == 2 + puk = rsa_public_key(5, 3, 3) + assert encipher_rsa(2, puk) == 8 + + with warns(NonInvertibleCipherWarning): + puk = rsa_public_key(2, 2, 1) + assert encipher_rsa(2, puk) == 2 + + +def test_decipher_rsa(): + prk = rsa_private_key(2, 3, 1) + assert decipher_rsa(2, prk) == 2 + prk = rsa_private_key(5, 3, 3) + assert decipher_rsa(8, prk) == 2 + + with warns(NonInvertibleCipherWarning): + prk = rsa_private_key(2, 2, 1) + assert decipher_rsa(2, prk) == 2 + + +def test_mutltiprime_rsa_full_example(): + # Test example from + # https://iopscience.iop.org/article/10.1088/1742-6596/995/1/012030 + puk = rsa_public_key(2, 3, 5, 7, 11, 13, 7) + prk = rsa_private_key(2, 3, 5, 7, 11, 13, 7) + assert puk == (30030, 7) + assert prk == (30030, 823) + + msg = 10 + encrypted = encipher_rsa(2 * msg - 15, puk) + assert encrypted == 18065 + decrypted = (decipher_rsa(encrypted, prk) + 15) / 2 + assert decrypted == msg + + # Test example from + # https://www.scirp.org/pdf/JCC_2018032215502008.pdf + puk1 = rsa_public_key(53, 41, 43, 47, 41) + prk1 = rsa_private_key(53, 41, 43, 47, 41) + puk2 = rsa_public_key(53, 41, 43, 47, 97) + prk2 = rsa_private_key(53, 41, 43, 47, 97) + + assert puk1 == (4391633, 41) + assert prk1 == (4391633, 294041) + assert puk2 == (4391633, 97) + assert prk2 == (4391633, 455713) + + msg = 12321 + encrypted = encipher_rsa(encipher_rsa(msg, puk1), puk2) + assert encrypted == 1081588 + decrypted = decipher_rsa(decipher_rsa(encrypted, prk2), prk1) + assert decrypted == msg + + +def test_rsa_crt_extreme(): + p = int( + '10177157607154245068023861503693082120906487143725062283406501' \ + '54082258226204046999838297167140821364638180697194879500245557' \ + '65445186962893346463841419427008800341257468600224049986260471' \ + '92257248163014468841725476918639415726709736077813632961290911' \ + '0256421232977833028677441206049309220354796014376698325101693') + + q = int( + '28752342353095132872290181526607275886182793241660805077850801' \ + '75689512797754286972952273553128181861830576836289738668745250' \ + '34028199691128870676414118458442900035778874482624765513861643' \ + '27966696316822188398336199002306588703902894100476186823849595' \ + '103239410527279605442148285816149368667083114802852804976893') + + r = int( + '17698229259868825776879500736350186838850961935956310134378261' \ + '89771862186717463067541369694816245225291921138038800171125596' \ + '07315449521981157084370187887650624061033066022458512942411841' \ + '18747893789972315277160085086164119879536041875335384844820566' \ + '0287479617671726408053319619892052000850883994343378882717849') + + s = int( + '68925428438585431029269182233502611027091755064643742383515623' \ + '64321310582896893395529367074942808353187138794422745718419645' \ + '28291231865157212604266903677599180789896916456120289112752835' \ + '98502265889669730331688206825220074713977607415178738015831030' \ + '364290585369150502819743827343552098197095520550865360159439' + ) + + t = int( + '69035483433453632820551311892368908779778144568711455301541094' \ + '31487047642322695357696860925747923189635033183069823820910521' \ + '71172909106797748883261493224162414050106920442445896819806600' \ + '15448444826108008217972129130625571421904893252804729877353352' \ + '739420480574842850202181462656251626522910618936534699566291' + ) + + e = 65537 + puk = rsa_public_key(p, q, r, s, t, e) + prk = rsa_private_key(p, q, r, s, t, e) + + plaintext = 1000 + ciphertext_1 = encipher_rsa(plaintext, puk) + ciphertext_2 = encipher_rsa(plaintext, puk, [p, q, r, s, t]) + assert ciphertext_1 == ciphertext_2 + assert decipher_rsa(ciphertext_1, prk) == \ + decipher_rsa(ciphertext_1, prk, [p, q, r, s, t]) + + +def test_rsa_exhaustive(): + p, q = 61, 53 + e = 17 + puk = rsa_public_key(p, q, e, totient='Carmichael') + prk = rsa_private_key(p, q, e, totient='Carmichael') + + for msg in range(puk[0]): + encrypted = encipher_rsa(msg, puk) + decrypted = decipher_rsa(encrypted, prk) + try: + assert decrypted == msg + except AssertionError: + raise AssertionError( + "The RSA is not correctly decrypted " \ + "(Original : {}, Encrypted : {}, Decrypted : {})" \ + .format(msg, encrypted, decrypted) + ) + + +def test_rsa_multiprime_exhanstive(): + primes = [3, 5, 7, 11] + e = 7 + args = primes + [e] + puk = rsa_public_key(*args, totient='Carmichael') + prk = rsa_private_key(*args, totient='Carmichael') + n = puk[0] + + for msg in range(n): + encrypted = encipher_rsa(msg, puk) + decrypted = decipher_rsa(encrypted, prk) + try: + assert decrypted == msg + except AssertionError: + raise AssertionError( + "The RSA is not correctly decrypted " \ + "(Original : {}, Encrypted : {}, Decrypted : {})" \ + .format(msg, encrypted, decrypted) + ) + + +def test_rsa_multipower_exhanstive(): + primes = [5, 5, 7] + e = 7 + args = primes + [e] + puk = rsa_public_key(*args, multipower=True) + prk = rsa_private_key(*args, multipower=True) + n = puk[0] + + for msg in range(n): + if gcd(msg, n) != 1: + continue + + encrypted = encipher_rsa(msg, puk) + decrypted = decipher_rsa(encrypted, prk) + try: + assert decrypted == msg + except AssertionError: + raise AssertionError( + "The RSA is not correctly decrypted " \ + "(Original : {}, Encrypted : {}, Decrypted : {})" \ + .format(msg, encrypted, decrypted) + ) + + +def test_kid_rsa_public_key(): + assert kid_rsa_public_key(1, 2, 1, 1) == (5, 2) + assert kid_rsa_public_key(1, 2, 2, 1) == (8, 3) + assert kid_rsa_public_key(1, 2, 1, 2) == (7, 2) + + +def test_kid_rsa_private_key(): + assert kid_rsa_private_key(1, 2, 1, 1) == (5, 3) + assert kid_rsa_private_key(1, 2, 2, 1) == (8, 3) + assert kid_rsa_private_key(1, 2, 1, 2) == (7, 4) + + +def test_encipher_kid_rsa(): + assert encipher_kid_rsa(1, (5, 2)) == 2 + assert encipher_kid_rsa(1, (8, 3)) == 3 + assert encipher_kid_rsa(1, (7, 2)) == 2 + + +def test_decipher_kid_rsa(): + assert decipher_kid_rsa(2, (5, 3)) == 1 + assert decipher_kid_rsa(3, (8, 3)) == 1 + assert decipher_kid_rsa(2, (7, 4)) == 1 + + +def test_encode_morse(): + assert encode_morse('ABC') == '.-|-...|-.-.' + assert encode_morse('SMS ') == '...|--|...||' + assert encode_morse('SMS\n') == '...|--|...||' + assert encode_morse('') == '' + assert encode_morse(' ') == '||' + assert encode_morse(' ', sep='`') == '``' + assert encode_morse(' ', sep='``') == '````' + assert encode_morse('!@#$%^&*()_+') == '-.-.--|.--.-.|...-..-|-.--.|-.--.-|..--.-|.-.-.' + assert encode_morse('12345') == '.----|..---|...--|....-|.....' + assert encode_morse('67890') == '-....|--...|---..|----.|-----' + + +def test_decode_morse(): + assert decode_morse('-.-|.|-.--') == 'KEY' + assert decode_morse('.-.|..-|-.||') == 'RUN' + raises(KeyError, lambda: decode_morse('.....----')) + + +def test_lfsr_sequence(): + raises(TypeError, lambda: lfsr_sequence(1, [1], 1)) + raises(TypeError, lambda: lfsr_sequence([1], 1, 1)) + F = FF(2) + assert lfsr_sequence([F(1)], [F(1)], 2) == [F(1), F(1)] + assert lfsr_sequence([F(0)], [F(1)], 2) == [F(1), F(0)] + F = FF(3) + assert lfsr_sequence([F(1)], [F(1)], 2) == [F(1), F(1)] + assert lfsr_sequence([F(0)], [F(2)], 2) == [F(2), F(0)] + assert lfsr_sequence([F(1)], [F(2)], 2) == [F(2), F(2)] + + +def test_lfsr_autocorrelation(): + raises(TypeError, lambda: lfsr_autocorrelation(1, 2, 3)) + F = FF(2) + s = lfsr_sequence([F(1), F(0)], [F(0), F(1)], 5) + assert lfsr_autocorrelation(s, 2, 0) == 1 + assert lfsr_autocorrelation(s, 2, 1) == -1 + + +def test_lfsr_connection_polynomial(): + F = FF(2) + x = symbols("x") + s = lfsr_sequence([F(1), F(0)], [F(0), F(1)], 5) + assert lfsr_connection_polynomial(s) == x**2 + 1 + s = lfsr_sequence([F(1), F(1)], [F(0), F(1)], 5) + assert lfsr_connection_polynomial(s) == x**2 + x + 1 + + +def test_elgamal_private_key(): + a, b, _ = elgamal_private_key(digit=100) + assert isprime(a) + assert is_primitive_root(b, a) + assert len(bin(a)) >= 102 + + +def test_elgamal(): + dk = elgamal_private_key(5) + ek = elgamal_public_key(dk) + P = ek[0] + assert P - 1 == decipher_elgamal(encipher_elgamal(P - 1, ek), dk) + raises(ValueError, lambda: encipher_elgamal(P, dk)) + raises(ValueError, lambda: encipher_elgamal(-1, dk)) + + +def test_dh_private_key(): + p, g, _ = dh_private_key(digit = 100) + assert isprime(p) + assert is_primitive_root(g, p) + assert len(bin(p)) >= 102 + + +def test_dh_public_key(): + p1, g1, a = dh_private_key(digit = 100) + p2, g2, ga = dh_public_key((p1, g1, a)) + assert p1 == p2 + assert g1 == g2 + assert ga == pow(g1, a, p1) + + +def test_dh_shared_key(): + prk = dh_private_key(digit = 100) + p, _, ga = dh_public_key(prk) + b = randrange(2, p) + sk = dh_shared_key((p, _, ga), b) + assert sk == pow(ga, b, p) + raises(ValueError, lambda: dh_shared_key((1031, 14, 565), 2000)) + + +def test_padded_key(): + assert padded_key('b', 'ab') == 'ba' + raises(ValueError, lambda: padded_key('ab', 'ace')) + raises(ValueError, lambda: padded_key('ab', 'abba')) + + +def test_bifid(): + raises(ValueError, lambda: encipher_bifid('abc', 'b', 'abcde')) + assert encipher_bifid('abc', 'b', 'abcd') == 'bdb' + raises(ValueError, lambda: decipher_bifid('bdb', 'b', 'abcde')) + assert encipher_bifid('bdb', 'b', 'abcd') == 'abc' + raises(ValueError, lambda: bifid_square('abcde')) + assert bifid5_square("B") == \ + bifid5_square('BACDEFGHIKLMNOPQRSTUVWXYZ') + assert bifid6_square('B0') == \ + bifid6_square('B0ACDEFGHIJKLMNOPQRSTUVWXYZ123456789') + + +def test_encipher_decipher_gm(): + ps = [131, 137, 139, 149, 151, 157, 163, 167, + 173, 179, 181, 191, 193, 197, 199] + qs = [89, 97, 101, 103, 107, 109, 113, 127, + 131, 137, 139, 149, 151, 157, 47] + messages = [ + 0, 32855, 34303, 14805, 1280, 75859, 38368, + 724, 60356, 51675, 76697, 61854, 18661, + ] + for p, q in zip(ps, qs): + pri = gm_private_key(p, q) + for msg in messages: + pub = gm_public_key(p, q) + enc = encipher_gm(msg, pub) + dec = decipher_gm(enc, pri) + assert dec == msg + + +def test_gm_private_key(): + raises(ValueError, lambda: gm_public_key(13, 15)) + raises(ValueError, lambda: gm_public_key(0, 0)) + raises(ValueError, lambda: gm_public_key(0, 5)) + assert 17, 19 == gm_public_key(17, 19) + + +def test_gm_public_key(): + assert 323 == gm_public_key(17, 19)[1] + assert 15 == gm_public_key(3, 5)[1] + raises(ValueError, lambda: gm_public_key(15, 19)) + +def test_encipher_decipher_bg(): + ps = [67, 7, 71, 103, 11, 43, 107, 47, + 79, 19, 83, 23, 59, 127, 31] + qs = qs = [7, 71, 103, 11, 43, 107, 47, + 79, 19, 83, 23, 59, 127, 31, 67] + messages = [ + 0, 328, 343, 148, 1280, 758, 383, + 724, 603, 516, 766, 618, 186, + ] + + for p, q in zip(ps, qs): + pri = bg_private_key(p, q) + for msg in messages: + pub = bg_public_key(p, q) + enc = encipher_bg(msg, pub) + dec = decipher_bg(enc, pri) + assert dec == msg + +def test_bg_private_key(): + raises(ValueError, lambda: bg_private_key(8, 16)) + raises(ValueError, lambda: bg_private_key(8, 8)) + raises(ValueError, lambda: bg_private_key(13, 17)) + assert 23, 31 == bg_private_key(23, 31) + +def test_bg_public_key(): + assert 5293 == bg_public_key(67, 79) + assert 713 == bg_public_key(23, 31) + raises(ValueError, lambda: bg_private_key(13, 17)) diff --git a/venv/lib/python3.11/site-packages/sympy/diffgeom/__init__.py b/venv/lib/python3.11/site-packages/sympy/diffgeom/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8846a99510601c9675103e21ef5a0a1e839fdd11 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/diffgeom/__init__.py @@ -0,0 +1,19 @@ +from .diffgeom import ( + BaseCovarDerivativeOp, BaseScalarField, BaseVectorField, Commutator, + contravariant_order, CoordSystem, CoordinateSymbol, + CovarDerivativeOp, covariant_order, Differential, intcurve_diffequ, + intcurve_series, LieDerivative, Manifold, metric_to_Christoffel_1st, + metric_to_Christoffel_2nd, metric_to_Ricci_components, + metric_to_Riemann_components, Patch, Point, TensorProduct, twoform_to_matrix, + vectors_in_basis, WedgeProduct, +) + +__all__ = [ + 'BaseCovarDerivativeOp', 'BaseScalarField', 'BaseVectorField', 'Commutator', + 'contravariant_order', 'CoordSystem', 'CoordinateSymbol', + 'CovarDerivativeOp', 'covariant_order', 'Differential', 'intcurve_diffequ', + 'intcurve_series', 'LieDerivative', 'Manifold', 'metric_to_Christoffel_1st', + 'metric_to_Christoffel_2nd', 'metric_to_Ricci_components', + 'metric_to_Riemann_components', 'Patch', 'Point', 'TensorProduct', + 'twoform_to_matrix', 'vectors_in_basis', 'WedgeProduct', +] diff --git a/venv/lib/python3.11/site-packages/sympy/diffgeom/diffgeom.py b/venv/lib/python3.11/site-packages/sympy/diffgeom/diffgeom.py new file mode 100644 index 0000000000000000000000000000000000000000..a95f83122d6de0b7015b9a3ad0573cbfd97a7ef3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/diffgeom/diffgeom.py @@ -0,0 +1,2270 @@ +from __future__ import annotations +from typing import Any + +from functools import reduce +from itertools import permutations + +from sympy.combinatorics import Permutation +from sympy.core import ( + Basic, Expr, Function, diff, + Pow, Mul, Add, Lambda, S, Tuple, Dict +) +from sympy.core.cache import cacheit + +from sympy.core.symbol import Symbol, Dummy +from sympy.core.symbol import Str +from sympy.core.sympify import _sympify +from sympy.functions import factorial +from sympy.matrices import ImmutableDenseMatrix as Matrix +from sympy.solvers import solve + +from sympy.utilities.exceptions import (sympy_deprecation_warning, + SymPyDeprecationWarning, + ignore_warnings) + + +# TODO you are a bit excessive in the use of Dummies +# TODO dummy point, literal field +# TODO too often one needs to call doit or simplify on the output, check the +# tests and find out why +from sympy.tensor.array import ImmutableDenseNDimArray + + +class Manifold(Basic): + """ + A mathematical manifold. + + Explanation + =========== + + A manifold is a topological space that locally resembles + Euclidean space near each point [1]. + This class does not provide any means to study the topological + characteristics of the manifold that it represents, though. + + Parameters + ========== + + name : str + The name of the manifold. + + dim : int + The dimension of the manifold. + + Examples + ======== + + >>> from sympy.diffgeom import Manifold + >>> m = Manifold('M', 2) + >>> m + M + >>> m.dim + 2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Manifold + """ + + def __new__(cls, name, dim, **kwargs): + if not isinstance(name, Str): + name = Str(name) + dim = _sympify(dim) + obj = super().__new__(cls, name, dim) + + obj.patches = _deprecated_list( + """ + Manifold.patches is deprecated. The Manifold object is now + immutable. Instead use a separate list to keep track of the + patches. + """, []) + return obj + + @property + def name(self): + return self.args[0] + + @property + def dim(self): + return self.args[1] + + +class Patch(Basic): + """ + A patch on a manifold. + + Explanation + =========== + + Coordinate patch, or patch in short, is a simply-connected open set around + a point in the manifold [1]. On a manifold one can have many patches that + do not always include the whole manifold. On these patches coordinate + charts can be defined that permit the parameterization of any point on the + patch in terms of a tuple of real numbers (the coordinates). + + This class does not provide any means to study the topological + characteristics of the patch that it represents. + + Parameters + ========== + + name : str + The name of the patch. + + manifold : Manifold + The manifold on which the patch is defined. + + Examples + ======== + + >>> from sympy.diffgeom import Manifold, Patch + >>> m = Manifold('M', 2) + >>> p = Patch('P', m) + >>> p + P + >>> p.dim + 2 + + References + ========== + + .. [1] G. Sussman, J. Wisdom, W. Farr, Functional Differential Geometry + (2013) + + """ + def __new__(cls, name, manifold, **kwargs): + if not isinstance(name, Str): + name = Str(name) + obj = super().__new__(cls, name, manifold) + + obj.manifold.patches.append(obj) # deprecated + obj.coord_systems = _deprecated_list( + """ + Patch.coord_systms is deprecated. The Patch class is now + immutable. Instead use a separate list to keep track of coordinate + systems. + """, []) + return obj + + @property + def name(self): + return self.args[0] + + @property + def manifold(self): + return self.args[1] + + @property + def dim(self): + return self.manifold.dim + + +class CoordSystem(Basic): + """ + A coordinate system defined on the patch. + + Explanation + =========== + + Coordinate system is a system that uses one or more coordinates to uniquely + determine the position of the points or other geometric elements on a + manifold [1]. + + By passing ``Symbols`` to *symbols* parameter, user can define the name and + assumptions of coordinate symbols of the coordinate system. If not passed, + these symbols are generated automatically and are assumed to be real valued. + + By passing *relations* parameter, user can define the transform relations of + coordinate systems. Inverse transformation and indirect transformation can + be found automatically. If this parameter is not passed, coordinate + transformation cannot be done. + + Parameters + ========== + + name : str + The name of the coordinate system. + + patch : Patch + The patch where the coordinate system is defined. + + symbols : list of Symbols, optional + Defines the names and assumptions of coordinate symbols. + + relations : dict, optional + Key is a tuple of two strings, who are the names of the systems where + the coordinates transform from and transform to. + Value is a tuple of the symbols before transformation and a tuple of + the expressions after transformation. + + Examples + ======== + + We define two-dimensional Cartesian coordinate system and polar coordinate + system. + + >>> from sympy import symbols, pi, sqrt, atan2, cos, sin + >>> from sympy.diffgeom import Manifold, Patch, CoordSystem + >>> m = Manifold('M', 2) + >>> p = Patch('P', m) + >>> x, y = symbols('x y', real=True) + >>> r, theta = symbols('r theta', nonnegative=True) + >>> relation_dict = { + ... ('Car2D', 'Pol'): [(x, y), (sqrt(x**2 + y**2), atan2(y, x))], + ... ('Pol', 'Car2D'): [(r, theta), (r*cos(theta), r*sin(theta))] + ... } + >>> Car2D = CoordSystem('Car2D', p, (x, y), relation_dict) + >>> Pol = CoordSystem('Pol', p, (r, theta), relation_dict) + + ``symbols`` property returns ``CoordinateSymbol`` instances. These symbols + are not same with the symbols used to construct the coordinate system. + + >>> Car2D + Car2D + >>> Car2D.dim + 2 + >>> Car2D.symbols + (x, y) + >>> _[0].func + + + ``transformation()`` method returns the transformation function from + one coordinate system to another. ``transform()`` method returns the + transformed coordinates. + + >>> Car2D.transformation(Pol) + Lambda((x, y), Matrix([ + [sqrt(x**2 + y**2)], + [ atan2(y, x)]])) + >>> Car2D.transform(Pol) + Matrix([ + [sqrt(x**2 + y**2)], + [ atan2(y, x)]]) + >>> Car2D.transform(Pol, [1, 2]) + Matrix([ + [sqrt(5)], + [atan(2)]]) + + ``jacobian()`` method returns the Jacobian matrix of coordinate + transformation between two systems. ``jacobian_determinant()`` method + returns the Jacobian determinant of coordinate transformation between two + systems. + + >>> Pol.jacobian(Car2D) + Matrix([ + [cos(theta), -r*sin(theta)], + [sin(theta), r*cos(theta)]]) + >>> Pol.jacobian(Car2D, [1, pi/2]) + Matrix([ + [0, -1], + [1, 0]]) + >>> Car2D.jacobian_determinant(Pol) + 1/sqrt(x**2 + y**2) + >>> Car2D.jacobian_determinant(Pol, [1,0]) + 1 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Coordinate_system + + """ + def __new__(cls, name, patch, symbols=None, relations={}, **kwargs): + if not isinstance(name, Str): + name = Str(name) + + # canonicallize the symbols + if symbols is None: + names = kwargs.get('names', None) + if names is None: + symbols = Tuple( + *[Symbol('%s_%s' % (name.name, i), real=True) + for i in range(patch.dim)] + ) + else: + sympy_deprecation_warning( + f""" +The 'names' argument to CoordSystem is deprecated. Use 'symbols' instead. That +is, replace + + CoordSystem(..., names={names}) + +with + + CoordSystem(..., symbols=[{', '.join(["Symbol(" + repr(n) + ", real=True)" for n in names])}]) + """, + deprecated_since_version="1.7", + active_deprecations_target="deprecated-diffgeom-mutable", + ) + symbols = Tuple( + *[Symbol(n, real=True) for n in names] + ) + else: + syms = [] + for s in symbols: + if isinstance(s, Symbol): + syms.append(Symbol(s.name, **s._assumptions.generator)) + elif isinstance(s, str): + sympy_deprecation_warning( + f""" + +Passing a string as the coordinate symbol name to CoordSystem is deprecated. +Pass a Symbol with the appropriate name and assumptions instead. + +That is, replace {s} with Symbol({s!r}, real=True). + """, + + deprecated_since_version="1.7", + active_deprecations_target="deprecated-diffgeom-mutable", + ) + syms.append(Symbol(s, real=True)) + symbols = Tuple(*syms) + + # canonicallize the relations + rel_temp = {} + for k,v in relations.items(): + s1, s2 = k + if not isinstance(s1, Str): + s1 = Str(s1) + if not isinstance(s2, Str): + s2 = Str(s2) + key = Tuple(s1, s2) + + # Old version used Lambda as a value. + if isinstance(v, Lambda): + v = (tuple(v.signature), tuple(v.expr)) + else: + v = (tuple(v[0]), tuple(v[1])) + rel_temp[key] = v + relations = Dict(rel_temp) + + # construct the object + obj = super().__new__(cls, name, patch, symbols, relations) + + # Add deprecated attributes + obj.transforms = _deprecated_dict( + """ + CoordSystem.transforms is deprecated. The CoordSystem class is now + immutable. Use the 'relations' keyword argument to the + CoordSystems() constructor to specify relations. + """, {}) + obj._names = [str(n) for n in symbols] + obj.patch.coord_systems.append(obj) # deprecated + obj._dummies = [Dummy(str(n)) for n in symbols] # deprecated + obj._dummy = Dummy() + + return obj + + @property + def name(self): + return self.args[0] + + @property + def patch(self): + return self.args[1] + + @property + def manifold(self): + return self.patch.manifold + + @property + def symbols(self): + return tuple(CoordinateSymbol(self, i, **s._assumptions.generator) + for i,s in enumerate(self.args[2])) + + @property + def relations(self): + return self.args[3] + + @property + def dim(self): + return self.patch.dim + + ########################################################################## + # Finding transformation relation + ########################################################################## + + def transformation(self, sys): + """ + Return coordinate transformation function from *self* to *sys*. + + Parameters + ========== + + sys : CoordSystem + + Returns + ======= + + sympy.Lambda + + Examples + ======== + + >>> from sympy.diffgeom.rn import R2_r, R2_p + >>> R2_r.transformation(R2_p) + Lambda((x, y), Matrix([ + [sqrt(x**2 + y**2)], + [ atan2(y, x)]])) + + """ + signature = self.args[2] + + key = Tuple(self.name, sys.name) + if self == sys: + expr = Matrix(self.symbols) + elif key in self.relations: + expr = Matrix(self.relations[key][1]) + elif key[::-1] in self.relations: + expr = Matrix(self._inverse_transformation(sys, self)) + else: + expr = Matrix(self._indirect_transformation(self, sys)) + return Lambda(signature, expr) + + @staticmethod + def _solve_inverse(sym1, sym2, exprs, sys1_name, sys2_name): + ret = solve( + [t[0] - t[1] for t in zip(sym2, exprs)], + list(sym1), dict=True) + + if len(ret) == 0: + temp = "Cannot solve inverse relation from {} to {}." + raise NotImplementedError(temp.format(sys1_name, sys2_name)) + elif len(ret) > 1: + temp = "Obtained multiple inverse relation from {} to {}." + raise ValueError(temp.format(sys1_name, sys2_name)) + + return ret[0] + + @classmethod + def _inverse_transformation(cls, sys1, sys2): + # Find the transformation relation from sys2 to sys1 + forward = sys1.transform(sys2) + inv_results = cls._solve_inverse(sys1.symbols, sys2.symbols, forward, + sys1.name, sys2.name) + signature = tuple(sys1.symbols) + return [inv_results[s] for s in signature] + + @classmethod + @cacheit + def _indirect_transformation(cls, sys1, sys2): + # Find the transformation relation between two indirectly connected + # coordinate systems + rel = sys1.relations + path = cls._dijkstra(sys1, sys2) + + transforms = [] + for s1, s2 in zip(path, path[1:]): + if (s1, s2) in rel: + transforms.append(rel[(s1, s2)]) + else: + sym2, inv_exprs = rel[(s2, s1)] + sym1 = tuple(Dummy() for i in sym2) + ret = cls._solve_inverse(sym2, sym1, inv_exprs, s2, s1) + ret = tuple(ret[s] for s in sym2) + transforms.append((sym1, ret)) + syms = sys1.args[2] + exprs = syms + for newsyms, newexprs in transforms: + exprs = tuple(e.subs(zip(newsyms, exprs)) for e in newexprs) + return exprs + + @staticmethod + def _dijkstra(sys1, sys2): + # Use Dijkstra algorithm to find the shortest path between two indirectly-connected + # coordinate systems + # return value is the list of the names of the systems. + relations = sys1.relations + graph = {} + for s1, s2 in relations.keys(): + if s1 not in graph: + graph[s1] = {s2} + else: + graph[s1].add(s2) + if s2 not in graph: + graph[s2] = {s1} + else: + graph[s2].add(s1) + + path_dict = {sys:[0, [], 0] for sys in graph} # minimum distance, path, times of visited + + def visit(sys): + path_dict[sys][2] = 1 + for newsys in graph[sys]: + distance = path_dict[sys][0] + 1 + if path_dict[newsys][0] >= distance or not path_dict[newsys][1]: + path_dict[newsys][0] = distance + path_dict[newsys][1] = list(path_dict[sys][1]) + path_dict[newsys][1].append(sys) + + visit(sys1.name) + + while True: + min_distance = max(path_dict.values(), key=lambda x:x[0])[0] + newsys = None + for sys, lst in path_dict.items(): + if 0 < lst[0] <= min_distance and not lst[2]: + min_distance = lst[0] + newsys = sys + if newsys is None: + break + visit(newsys) + + result = path_dict[sys2.name][1] + result.append(sys2.name) + + if result == [sys2.name]: + raise KeyError("Two coordinate systems are not connected.") + return result + + def connect_to(self, to_sys, from_coords, to_exprs, inverse=True, fill_in_gaps=False): + sympy_deprecation_warning( + """ + The CoordSystem.connect_to() method is deprecated. Instead, + generate a new instance of CoordSystem with the 'relations' + keyword argument (CoordSystem classes are now immutable). + """, + deprecated_since_version="1.7", + active_deprecations_target="deprecated-diffgeom-mutable", + ) + + from_coords, to_exprs = dummyfy(from_coords, to_exprs) + self.transforms[to_sys] = Matrix(from_coords), Matrix(to_exprs) + + if inverse: + to_sys.transforms[self] = self._inv_transf(from_coords, to_exprs) + + if fill_in_gaps: + self._fill_gaps_in_transformations() + + @staticmethod + def _inv_transf(from_coords, to_exprs): + # Will be removed when connect_to is removed + inv_from = [i.as_dummy() for i in from_coords] + inv_to = solve( + [t[0] - t[1] for t in zip(inv_from, to_exprs)], + list(from_coords), dict=True)[0] + inv_to = [inv_to[fc] for fc in from_coords] + return Matrix(inv_from), Matrix(inv_to) + + @staticmethod + def _fill_gaps_in_transformations(): + # Will be removed when connect_to is removed + raise NotImplementedError + + ########################################################################## + # Coordinate transformations + ########################################################################## + + def transform(self, sys, coordinates=None): + """ + Return the result of coordinate transformation from *self* to *sys*. + If coordinates are not given, coordinate symbols of *self* are used. + + Parameters + ========== + + sys : CoordSystem + + coordinates : Any iterable, optional. + + Returns + ======= + + sympy.ImmutableDenseMatrix containing CoordinateSymbol + + Examples + ======== + + >>> from sympy.diffgeom.rn import R2_r, R2_p + >>> R2_r.transform(R2_p) + Matrix([ + [sqrt(x**2 + y**2)], + [ atan2(y, x)]]) + >>> R2_r.transform(R2_p, [0, 1]) + Matrix([ + [ 1], + [pi/2]]) + + """ + if coordinates is None: + coordinates = self.symbols + if self != sys: + transf = self.transformation(sys) + coordinates = transf(*coordinates) + else: + coordinates = Matrix(coordinates) + return coordinates + + def coord_tuple_transform_to(self, to_sys, coords): + """Transform ``coords`` to coord system ``to_sys``.""" + sympy_deprecation_warning( + """ + The CoordSystem.coord_tuple_transform_to() method is deprecated. + Use the CoordSystem.transform() method instead. + """, + deprecated_since_version="1.7", + active_deprecations_target="deprecated-diffgeom-mutable", + ) + + coords = Matrix(coords) + if self != to_sys: + with ignore_warnings(SymPyDeprecationWarning): + transf = self.transforms[to_sys] + coords = transf[1].subs(list(zip(transf[0], coords))) + return coords + + def jacobian(self, sys, coordinates=None): + """ + Return the jacobian matrix of a transformation on given coordinates. + If coordinates are not given, coordinate symbols of *self* are used. + + Parameters + ========== + + sys : CoordSystem + + coordinates : Any iterable, optional. + + Returns + ======= + + sympy.ImmutableDenseMatrix + + Examples + ======== + + >>> from sympy.diffgeom.rn import R2_r, R2_p + >>> R2_p.jacobian(R2_r) + Matrix([ + [cos(theta), -rho*sin(theta)], + [sin(theta), rho*cos(theta)]]) + >>> R2_p.jacobian(R2_r, [1, 0]) + Matrix([ + [1, 0], + [0, 1]]) + + """ + result = self.transform(sys).jacobian(self.symbols) + if coordinates is not None: + result = result.subs(list(zip(self.symbols, coordinates))) + return result + jacobian_matrix = jacobian + + def jacobian_determinant(self, sys, coordinates=None): + """ + Return the jacobian determinant of a transformation on given + coordinates. If coordinates are not given, coordinate symbols of *self* + are used. + + Parameters + ========== + + sys : CoordSystem + + coordinates : Any iterable, optional. + + Returns + ======= + + sympy.Expr + + Examples + ======== + + >>> from sympy.diffgeom.rn import R2_r, R2_p + >>> R2_r.jacobian_determinant(R2_p) + 1/sqrt(x**2 + y**2) + >>> R2_r.jacobian_determinant(R2_p, [1, 0]) + 1 + + """ + return self.jacobian(sys, coordinates).det() + + + ########################################################################## + # Points + ########################################################################## + + def point(self, coords): + """Create a ``Point`` with coordinates given in this coord system.""" + return Point(self, coords) + + def point_to_coords(self, point): + """Calculate the coordinates of a point in this coord system.""" + return point.coords(self) + + ########################################################################## + # Base fields. + ########################################################################## + + def base_scalar(self, coord_index): + """Return ``BaseScalarField`` that takes a point and returns one of the coordinates.""" + return BaseScalarField(self, coord_index) + coord_function = base_scalar + + def base_scalars(self): + """Returns a list of all coordinate functions. + For more details see the ``base_scalar`` method of this class.""" + return [self.base_scalar(i) for i in range(self.dim)] + coord_functions = base_scalars + + def base_vector(self, coord_index): + """Return a basis vector field. + The basis vector field for this coordinate system. It is also an + operator on scalar fields.""" + return BaseVectorField(self, coord_index) + + def base_vectors(self): + """Returns a list of all base vectors. + For more details see the ``base_vector`` method of this class.""" + return [self.base_vector(i) for i in range(self.dim)] + + def base_oneform(self, coord_index): + """Return a basis 1-form field. + The basis one-form field for this coordinate system. It is also an + operator on vector fields.""" + return Differential(self.coord_function(coord_index)) + + def base_oneforms(self): + """Returns a list of all base oneforms. + For more details see the ``base_oneform`` method of this class.""" + return [self.base_oneform(i) for i in range(self.dim)] + + +class CoordinateSymbol(Symbol): + """A symbol which denotes an abstract value of i-th coordinate of + the coordinate system with given context. + + Explanation + =========== + + Each coordinates in coordinate system are represented by unique symbol, + such as x, y, z in Cartesian coordinate system. + + You may not construct this class directly. Instead, use `symbols` method + of CoordSystem. + + Parameters + ========== + + coord_sys : CoordSystem + + index : integer + + Examples + ======== + + >>> from sympy import symbols, Lambda, Matrix, sqrt, atan2, cos, sin + >>> from sympy.diffgeom import Manifold, Patch, CoordSystem + >>> m = Manifold('M', 2) + >>> p = Patch('P', m) + >>> x, y = symbols('x y', real=True) + >>> r, theta = symbols('r theta', nonnegative=True) + >>> relation_dict = { + ... ('Car2D', 'Pol'): Lambda((x, y), Matrix([sqrt(x**2 + y**2), atan2(y, x)])), + ... ('Pol', 'Car2D'): Lambda((r, theta), Matrix([r*cos(theta), r*sin(theta)])) + ... } + >>> Car2D = CoordSystem('Car2D', p, [x, y], relation_dict) + >>> Pol = CoordSystem('Pol', p, [r, theta], relation_dict) + >>> x, y = Car2D.symbols + + ``CoordinateSymbol`` contains its coordinate symbol and index. + + >>> x.name + 'x' + >>> x.coord_sys == Car2D + True + >>> x.index + 0 + >>> x.is_real + True + + You can transform ``CoordinateSymbol`` into other coordinate system using + ``rewrite()`` method. + + >>> x.rewrite(Pol) + r*cos(theta) + >>> sqrt(x**2 + y**2).rewrite(Pol).simplify() + r + + """ + def __new__(cls, coord_sys, index, **assumptions): + name = coord_sys.args[2][index].name + obj = super().__new__(cls, name, **assumptions) + obj.coord_sys = coord_sys + obj.index = index + return obj + + def __getnewargs__(self): + return (self.coord_sys, self.index) + + def _hashable_content(self): + return ( + self.coord_sys, self.index + ) + tuple(sorted(self.assumptions0.items())) + + def _eval_rewrite(self, rule, args, **hints): + if isinstance(rule, CoordSystem): + return rule.transform(self.coord_sys)[self.index] + return super()._eval_rewrite(rule, args, **hints) + + +class Point(Basic): + """Point defined in a coordinate system. + + Explanation + =========== + + Mathematically, point is defined in the manifold and does not have any coordinates + by itself. Coordinate system is what imbues the coordinates to the point by coordinate + chart. However, due to the difficulty of realizing such logic, you must supply + a coordinate system and coordinates to define a Point here. + + The usage of this object after its definition is independent of the + coordinate system that was used in order to define it, however due to + limitations in the simplification routines you can arrive at complicated + expressions if you use inappropriate coordinate systems. + + Parameters + ========== + + coord_sys : CoordSystem + + coords : list + The coordinates of the point. + + Examples + ======== + + >>> from sympy import pi + >>> from sympy.diffgeom import Point + >>> from sympy.diffgeom.rn import R2, R2_r, R2_p + >>> rho, theta = R2_p.symbols + + >>> p = Point(R2_p, [rho, 3*pi/4]) + + >>> p.manifold == R2 + True + + >>> p.coords() + Matrix([ + [ rho], + [3*pi/4]]) + >>> p.coords(R2_r) + Matrix([ + [-sqrt(2)*rho/2], + [ sqrt(2)*rho/2]]) + + """ + + def __new__(cls, coord_sys, coords, **kwargs): + coords = Matrix(coords) + obj = super().__new__(cls, coord_sys, coords) + obj._coord_sys = coord_sys + obj._coords = coords + return obj + + @property + def patch(self): + return self._coord_sys.patch + + @property + def manifold(self): + return self._coord_sys.manifold + + @property + def dim(self): + return self.manifold.dim + + def coords(self, sys=None): + """ + Coordinates of the point in given coordinate system. If coordinate system + is not passed, it returns the coordinates in the coordinate system in which + the point was defined. + """ + if sys is None: + return self._coords + else: + return self._coord_sys.transform(sys, self._coords) + + @property + def free_symbols(self): + return self._coords.free_symbols + + +class BaseScalarField(Expr): + """Base scalar field over a manifold for a given coordinate system. + + Explanation + =========== + + A scalar field takes a point as an argument and returns a scalar. + A base scalar field of a coordinate system takes a point and returns one of + the coordinates of that point in the coordinate system in question. + + To define a scalar field you need to choose the coordinate system and the + index of the coordinate. + + The use of the scalar field after its definition is independent of the + coordinate system in which it was defined, however due to limitations in + the simplification routines you may arrive at more complicated + expression if you use unappropriate coordinate systems. + You can build complicated scalar fields by just building up SymPy + expressions containing ``BaseScalarField`` instances. + + Parameters + ========== + + coord_sys : CoordSystem + + index : integer + + Examples + ======== + + >>> from sympy import Function, pi + >>> from sympy.diffgeom import BaseScalarField + >>> from sympy.diffgeom.rn import R2_r, R2_p + >>> rho, _ = R2_p.symbols + >>> point = R2_p.point([rho, 0]) + >>> fx, fy = R2_r.base_scalars() + >>> ftheta = BaseScalarField(R2_r, 1) + + >>> fx(point) + rho + >>> fy(point) + 0 + + >>> (fx**2+fy**2).rcall(point) + rho**2 + + >>> g = Function('g') + >>> fg = g(ftheta-pi) + >>> fg.rcall(point) + g(-pi) + + """ + + is_commutative = True + + def __new__(cls, coord_sys, index, **kwargs): + index = _sympify(index) + obj = super().__new__(cls, coord_sys, index) + obj._coord_sys = coord_sys + obj._index = index + return obj + + @property + def coord_sys(self): + return self.args[0] + + @property + def index(self): + return self.args[1] + + @property + def patch(self): + return self.coord_sys.patch + + @property + def manifold(self): + return self.coord_sys.manifold + + @property + def dim(self): + return self.manifold.dim + + def __call__(self, *args): + """Evaluating the field at a point or doing nothing. + If the argument is a ``Point`` instance, the field is evaluated at that + point. The field is returned itself if the argument is any other + object. It is so in order to have working recursive calling mechanics + for all fields (check the ``__call__`` method of ``Expr``). + """ + point = args[0] + if len(args) != 1 or not isinstance(point, Point): + return self + coords = point.coords(self._coord_sys) + # XXX Calling doit is necessary with all the Subs expressions + # XXX Calling simplify is necessary with all the trig expressions + return simplify(coords[self._index]).doit() + + # XXX Workaround for limitations on the content of args + free_symbols: set[Any] = set() + + +class BaseVectorField(Expr): + r"""Base vector field over a manifold for a given coordinate system. + + Explanation + =========== + + A vector field is an operator taking a scalar field and returning a + directional derivative (which is also a scalar field). + A base vector field is the same type of operator, however the derivation is + specifically done with respect to a chosen coordinate. + + To define a base vector field you need to choose the coordinate system and + the index of the coordinate. + + The use of the vector field after its definition is independent of the + coordinate system in which it was defined, however due to limitations in the + simplification routines you may arrive at more complicated expression if you + use unappropriate coordinate systems. + + Parameters + ========== + coord_sys : CoordSystem + + index : integer + + Examples + ======== + + >>> from sympy import Function + >>> from sympy.diffgeom.rn import R2_p, R2_r + >>> from sympy.diffgeom import BaseVectorField + >>> from sympy import pprint + + >>> x, y = R2_r.symbols + >>> rho, theta = R2_p.symbols + >>> fx, fy = R2_r.base_scalars() + >>> point_p = R2_p.point([rho, theta]) + >>> point_r = R2_r.point([x, y]) + + >>> g = Function('g') + >>> s_field = g(fx, fy) + + >>> v = BaseVectorField(R2_r, 1) + >>> pprint(v(s_field)) + / d \| + |---(g(x, xi))|| + \dxi /|xi=y + >>> pprint(v(s_field).rcall(point_r).doit()) + d + --(g(x, y)) + dy + >>> pprint(v(s_field).rcall(point_p)) + / d \| + |---(g(rho*cos(theta), xi))|| + \dxi /|xi=rho*sin(theta) + + """ + + is_commutative = False + + def __new__(cls, coord_sys, index, **kwargs): + index = _sympify(index) + obj = super().__new__(cls, coord_sys, index) + obj._coord_sys = coord_sys + obj._index = index + return obj + + @property + def coord_sys(self): + return self.args[0] + + @property + def index(self): + return self.args[1] + + @property + def patch(self): + return self.coord_sys.patch + + @property + def manifold(self): + return self.coord_sys.manifold + + @property + def dim(self): + return self.manifold.dim + + def __call__(self, scalar_field): + """Apply on a scalar field. + The action of a vector field on a scalar field is a directional + differentiation. + If the argument is not a scalar field an error is raised. + """ + if covariant_order(scalar_field) or contravariant_order(scalar_field): + raise ValueError('Only scalar fields can be supplied as arguments to vector fields.') + + if scalar_field is None: + return self + + base_scalars = list(scalar_field.atoms(BaseScalarField)) + + # First step: e_x(x+r**2) -> e_x(x) + 2*r*e_x(r) + d_var = self._coord_sys._dummy + # TODO: you need a real dummy function for the next line + d_funcs = [Function('_#_%s' % i)(d_var) for i, + b in enumerate(base_scalars)] + d_result = scalar_field.subs(list(zip(base_scalars, d_funcs))) + d_result = d_result.diff(d_var) + + # Second step: e_x(x) -> 1 and e_x(r) -> cos(atan2(x, y)) + coords = self._coord_sys.symbols + d_funcs_deriv = [f.diff(d_var) for f in d_funcs] + d_funcs_deriv_sub = [] + for b in base_scalars: + jac = self._coord_sys.jacobian(b._coord_sys, coords) + d_funcs_deriv_sub.append(jac[b._index, self._index]) + d_result = d_result.subs(list(zip(d_funcs_deriv, d_funcs_deriv_sub))) + + # Remove the dummies + result = d_result.subs(list(zip(d_funcs, base_scalars))) + result = result.subs(list(zip(coords, self._coord_sys.coord_functions()))) + return result.doit() + + +def _find_coords(expr): + # Finds CoordinateSystems existing in expr + fields = expr.atoms(BaseScalarField, BaseVectorField) + return {f._coord_sys for f in fields} + + +class Commutator(Expr): + r"""Commutator of two vector fields. + + Explanation + =========== + + The commutator of two vector fields `v_1` and `v_2` is defined as the + vector field `[v_1, v_2]` that evaluated on each scalar field `f` is equal + to `v_1(v_2(f)) - v_2(v_1(f))`. + + Examples + ======== + + + >>> from sympy.diffgeom.rn import R2_p, R2_r + >>> from sympy.diffgeom import Commutator + >>> from sympy import simplify + + >>> fx, fy = R2_r.base_scalars() + >>> e_x, e_y = R2_r.base_vectors() + >>> e_r = R2_p.base_vector(0) + + >>> c_xy = Commutator(e_x, e_y) + >>> c_xr = Commutator(e_x, e_r) + >>> c_xy + 0 + + Unfortunately, the current code is not able to compute everything: + + >>> c_xr + Commutator(e_x, e_rho) + >>> simplify(c_xr(fy**2)) + -2*cos(theta)*y**2/(x**2 + y**2) + + """ + def __new__(cls, v1, v2): + if (covariant_order(v1) or contravariant_order(v1) != 1 + or covariant_order(v2) or contravariant_order(v2) != 1): + raise ValueError( + 'Only commutators of vector fields are supported.') + if v1 == v2: + return S.Zero + coord_sys = set().union(*[_find_coords(v) for v in (v1, v2)]) + if len(coord_sys) == 1: + # Only one coordinate systems is used, hence it is easy enough to + # actually evaluate the commutator. + if all(isinstance(v, BaseVectorField) for v in (v1, v2)): + return S.Zero + bases_1, bases_2 = [list(v.atoms(BaseVectorField)) + for v in (v1, v2)] + coeffs_1 = [v1.expand().coeff(b) for b in bases_1] + coeffs_2 = [v2.expand().coeff(b) for b in bases_2] + res = 0 + for c1, b1 in zip(coeffs_1, bases_1): + for c2, b2 in zip(coeffs_2, bases_2): + res += c1*b1(c2)*b2 - c2*b2(c1)*b1 + return res + else: + obj = super().__new__(cls, v1, v2) + obj._v1 = v1 # deprecated assignment + obj._v2 = v2 # deprecated assignment + return obj + + @property + def v1(self): + return self.args[0] + + @property + def v2(self): + return self.args[1] + + def __call__(self, scalar_field): + """Apply on a scalar field. + If the argument is not a scalar field an error is raised. + """ + return self.v1(self.v2(scalar_field)) - self.v2(self.v1(scalar_field)) + + +class Differential(Expr): + r"""Return the differential (exterior derivative) of a form field. + + Explanation + =========== + + The differential of a form (i.e. the exterior derivative) has a complicated + definition in the general case. + The differential `df` of the 0-form `f` is defined for any vector field `v` + as `df(v) = v(f)`. + + Examples + ======== + + >>> from sympy import Function + >>> from sympy.diffgeom.rn import R2_r + >>> from sympy.diffgeom import Differential + >>> from sympy import pprint + + >>> fx, fy = R2_r.base_scalars() + >>> e_x, e_y = R2_r.base_vectors() + >>> g = Function('g') + >>> s_field = g(fx, fy) + >>> dg = Differential(s_field) + + >>> dg + d(g(x, y)) + >>> pprint(dg(e_x)) + / d \| + |---(g(xi, y))|| + \dxi /|xi=x + >>> pprint(dg(e_y)) + / d \| + |---(g(x, xi))|| + \dxi /|xi=y + + Applying the exterior derivative operator twice always results in: + + >>> Differential(dg) + 0 + """ + + is_commutative = False + + def __new__(cls, form_field): + if contravariant_order(form_field): + raise ValueError( + 'A vector field was supplied as an argument to Differential.') + if isinstance(form_field, Differential): + return S.Zero + else: + obj = super().__new__(cls, form_field) + obj._form_field = form_field # deprecated assignment + return obj + + @property + def form_field(self): + return self.args[0] + + def __call__(self, *vector_fields): + """Apply on a list of vector_fields. + + Explanation + =========== + + If the number of vector fields supplied is not equal to 1 + the order of + the form field inside the differential the result is undefined. + + For 1-forms (i.e. differentials of scalar fields) the evaluation is + done as `df(v)=v(f)`. However if `v` is ``None`` instead of a vector + field, the differential is returned unchanged. This is done in order to + permit partial contractions for higher forms. + + In the general case the evaluation is done by applying the form field + inside the differential on a list with one less elements than the number + of elements in the original list. Lowering the number of vector fields + is achieved through replacing each pair of fields by their + commutator. + + If the arguments are not vectors or ``None``s an error is raised. + """ + if any((contravariant_order(a) != 1 or covariant_order(a)) and a is not None + for a in vector_fields): + raise ValueError('The arguments supplied to Differential should be vector fields or Nones.') + k = len(vector_fields) + if k == 1: + if vector_fields[0]: + return vector_fields[0].rcall(self._form_field) + return self + else: + # For higher form it is more complicated: + # Invariant formula: + # https://en.wikipedia.org/wiki/Exterior_derivative#Invariant_formula + # df(v1, ... vn) = +/- vi(f(v1..no i..vn)) + # +/- f([vi,vj],v1..no i, no j..vn) + f = self._form_field + v = vector_fields + ret = 0 + for i in range(k): + t = v[i].rcall(f.rcall(*v[:i] + v[i + 1:])) + ret += (-1)**i*t + for j in range(i + 1, k): + c = Commutator(v[i], v[j]) + if c: # TODO this is ugly - the Commutator can be Zero and + # this causes the next line to fail + t = f.rcall(*(c,) + v[:i] + v[i + 1:j] + v[j + 1:]) + ret += (-1)**(i + j)*t + return ret + + +class TensorProduct(Expr): + """Tensor product of forms. + + Explanation + =========== + + The tensor product permits the creation of multilinear functionals (i.e. + higher order tensors) out of lower order fields (e.g. 1-forms and vector + fields). However, the higher tensors thus created lack the interesting + features provided by the other type of product, the wedge product, namely + they are not antisymmetric and hence are not form fields. + + Examples + ======== + + >>> from sympy.diffgeom.rn import R2_r + >>> from sympy.diffgeom import TensorProduct + + >>> fx, fy = R2_r.base_scalars() + >>> e_x, e_y = R2_r.base_vectors() + >>> dx, dy = R2_r.base_oneforms() + + >>> TensorProduct(dx, dy)(e_x, e_y) + 1 + >>> TensorProduct(dx, dy)(e_y, e_x) + 0 + >>> TensorProduct(dx, fx*dy)(fx*e_x, e_y) + x**2 + >>> TensorProduct(e_x, e_y)(fx**2, fy**2) + 4*x*y + >>> TensorProduct(e_y, dx)(fy) + dx + + You can nest tensor products. + + >>> tp1 = TensorProduct(dx, dy) + >>> TensorProduct(tp1, dx)(e_x, e_y, e_x) + 1 + + You can make partial contraction for instance when 'raising an index'. + Putting ``None`` in the second argument of ``rcall`` means that the + respective position in the tensor product is left as it is. + + >>> TP = TensorProduct + >>> metric = TP(dx, dx) + 3*TP(dy, dy) + >>> metric.rcall(e_y, None) + 3*dy + + Or automatically pad the args with ``None`` without specifying them. + + >>> metric.rcall(e_y) + 3*dy + + """ + def __new__(cls, *args): + scalar = Mul(*[m for m in args if covariant_order(m) + contravariant_order(m) == 0]) + multifields = [m for m in args if covariant_order(m) + contravariant_order(m)] + if multifields: + if len(multifields) == 1: + return scalar*multifields[0] + return scalar*super().__new__(cls, *multifields) + else: + return scalar + + def __call__(self, *fields): + """Apply on a list of fields. + + If the number of input fields supplied is not equal to the order of + the tensor product field, the list of arguments is padded with ``None``'s. + + The list of arguments is divided in sublists depending on the order of + the forms inside the tensor product. The sublists are provided as + arguments to these forms and the resulting expressions are given to the + constructor of ``TensorProduct``. + + """ + tot_order = covariant_order(self) + contravariant_order(self) + tot_args = len(fields) + if tot_args != tot_order: + fields = list(fields) + [None]*(tot_order - tot_args) + orders = [covariant_order(f) + contravariant_order(f) for f in self._args] + indices = [sum(orders[:i + 1]) for i in range(len(orders) - 1)] + fields = [fields[i:j] for i, j in zip([0] + indices, indices + [None])] + multipliers = [t[0].rcall(*t[1]) for t in zip(self._args, fields)] + return TensorProduct(*multipliers) + + +class WedgeProduct(TensorProduct): + """Wedge product of forms. + + Explanation + =========== + + In the context of integration only completely antisymmetric forms make + sense. The wedge product permits the creation of such forms. + + Examples + ======== + + >>> from sympy.diffgeom.rn import R2_r + >>> from sympy.diffgeom import WedgeProduct + + >>> fx, fy = R2_r.base_scalars() + >>> e_x, e_y = R2_r.base_vectors() + >>> dx, dy = R2_r.base_oneforms() + + >>> WedgeProduct(dx, dy)(e_x, e_y) + 1 + >>> WedgeProduct(dx, dy)(e_y, e_x) + -1 + >>> WedgeProduct(dx, fx*dy)(fx*e_x, e_y) + x**2 + >>> WedgeProduct(e_x, e_y)(fy, None) + -e_x + + You can nest wedge products. + + >>> wp1 = WedgeProduct(dx, dy) + >>> WedgeProduct(wp1, dx)(e_x, e_y, e_x) + 0 + + """ + # TODO the calculation of signatures is slow + # TODO you do not need all these permutations (neither the prefactor) + def __call__(self, *fields): + """Apply on a list of vector_fields. + The expression is rewritten internally in terms of tensor products and evaluated.""" + orders = (covariant_order(e) + contravariant_order(e) for e in self.args) + mul = 1/Mul(*(factorial(o) for o in orders)) + perms = permutations(fields) + perms_par = (Permutation( + p).signature() for p in permutations(range(len(fields)))) + tensor_prod = TensorProduct(*self.args) + return mul*Add(*[tensor_prod(*p[0])*p[1] for p in zip(perms, perms_par)]) + + +class LieDerivative(Expr): + """Lie derivative with respect to a vector field. + + Explanation + =========== + + The transport operator that defines the Lie derivative is the pushforward of + the field to be derived along the integral curve of the field with respect + to which one derives. + + Examples + ======== + + >>> from sympy.diffgeom.rn import R2_r, R2_p + >>> from sympy.diffgeom import (LieDerivative, TensorProduct) + + >>> fx, fy = R2_r.base_scalars() + >>> e_x, e_y = R2_r.base_vectors() + >>> e_rho, e_theta = R2_p.base_vectors() + >>> dx, dy = R2_r.base_oneforms() + + >>> LieDerivative(e_x, fy) + 0 + >>> LieDerivative(e_x, fx) + 1 + >>> LieDerivative(e_x, e_x) + 0 + + The Lie derivative of a tensor field by another tensor field is equal to + their commutator: + + >>> LieDerivative(e_x, e_rho) + Commutator(e_x, e_rho) + >>> LieDerivative(e_x + e_y, fx) + 1 + + >>> tp = TensorProduct(dx, dy) + >>> LieDerivative(e_x, tp) + LieDerivative(e_x, TensorProduct(dx, dy)) + >>> LieDerivative(e_x, tp) + LieDerivative(e_x, TensorProduct(dx, dy)) + + """ + def __new__(cls, v_field, expr): + expr_form_ord = covariant_order(expr) + if contravariant_order(v_field) != 1 or covariant_order(v_field): + raise ValueError('Lie derivatives are defined only with respect to' + ' vector fields. The supplied argument was not a ' + 'vector field.') + if expr_form_ord > 0: + obj = super().__new__(cls, v_field, expr) + # deprecated assignments + obj._v_field = v_field + obj._expr = expr + return obj + if expr.atoms(BaseVectorField): + return Commutator(v_field, expr) + else: + return v_field.rcall(expr) + + @property + def v_field(self): + return self.args[0] + + @property + def expr(self): + return self.args[1] + + def __call__(self, *args): + v = self.v_field + expr = self.expr + lead_term = v(expr(*args)) + rest = Add(*[Mul(*args[:i] + (Commutator(v, args[i]),) + args[i + 1:]) + for i in range(len(args))]) + return lead_term - rest + + +class BaseCovarDerivativeOp(Expr): + """Covariant derivative operator with respect to a base vector. + + Examples + ======== + + >>> from sympy.diffgeom.rn import R2_r + >>> from sympy.diffgeom import BaseCovarDerivativeOp + >>> from sympy.diffgeom import metric_to_Christoffel_2nd, TensorProduct + + >>> TP = TensorProduct + >>> fx, fy = R2_r.base_scalars() + >>> e_x, e_y = R2_r.base_vectors() + >>> dx, dy = R2_r.base_oneforms() + + >>> ch = metric_to_Christoffel_2nd(TP(dx, dx) + TP(dy, dy)) + >>> ch + [[[0, 0], [0, 0]], [[0, 0], [0, 0]]] + >>> cvd = BaseCovarDerivativeOp(R2_r, 0, ch) + >>> cvd(fx) + 1 + >>> cvd(fx*e_x) + e_x + """ + + def __new__(cls, coord_sys, index, christoffel): + index = _sympify(index) + christoffel = ImmutableDenseNDimArray(christoffel) + obj = super().__new__(cls, coord_sys, index, christoffel) + # deprecated assignments + obj._coord_sys = coord_sys + obj._index = index + obj._christoffel = christoffel + return obj + + @property + def coord_sys(self): + return self.args[0] + + @property + def index(self): + return self.args[1] + + @property + def christoffel(self): + return self.args[2] + + def __call__(self, field): + """Apply on a scalar field. + + The action of a vector field on a scalar field is a directional + differentiation. + If the argument is not a scalar field the behaviour is undefined. + """ + if covariant_order(field) != 0: + raise NotImplementedError() + + field = vectors_in_basis(field, self._coord_sys) + + wrt_vector = self._coord_sys.base_vector(self._index) + wrt_scalar = self._coord_sys.coord_function(self._index) + vectors = list(field.atoms(BaseVectorField)) + + # First step: replace all vectors with something susceptible to + # derivation and do the derivation + # TODO: you need a real dummy function for the next line + d_funcs = [Function('_#_%s' % i)(wrt_scalar) for i, + b in enumerate(vectors)] + d_result = field.subs(list(zip(vectors, d_funcs))) + d_result = wrt_vector(d_result) + + # Second step: backsubstitute the vectors in + d_result = d_result.subs(list(zip(d_funcs, vectors))) + + # Third step: evaluate the derivatives of the vectors + derivs = [] + for v in vectors: + d = Add(*[(self._christoffel[k, wrt_vector._index, v._index] + *v._coord_sys.base_vector(k)) + for k in range(v._coord_sys.dim)]) + derivs.append(d) + to_subs = [wrt_vector(d) for d in d_funcs] + # XXX: This substitution can fail when there are Dummy symbols and the + # cache is disabled: https://github.com/sympy/sympy/issues/17794 + result = d_result.subs(list(zip(to_subs, derivs))) + + # Remove the dummies + result = result.subs(list(zip(d_funcs, vectors))) + return result.doit() + + +class CovarDerivativeOp(Expr): + """Covariant derivative operator. + + Examples + ======== + + >>> from sympy.diffgeom.rn import R2_r + >>> from sympy.diffgeom import CovarDerivativeOp + >>> from sympy.diffgeom import metric_to_Christoffel_2nd, TensorProduct + >>> TP = TensorProduct + >>> fx, fy = R2_r.base_scalars() + >>> e_x, e_y = R2_r.base_vectors() + >>> dx, dy = R2_r.base_oneforms() + >>> ch = metric_to_Christoffel_2nd(TP(dx, dx) + TP(dy, dy)) + + >>> ch + [[[0, 0], [0, 0]], [[0, 0], [0, 0]]] + >>> cvd = CovarDerivativeOp(fx*e_x, ch) + >>> cvd(fx) + x + >>> cvd(fx*e_x) + x*e_x + + """ + + def __new__(cls, wrt, christoffel): + if len({v._coord_sys for v in wrt.atoms(BaseVectorField)}) > 1: + raise NotImplementedError() + if contravariant_order(wrt) != 1 or covariant_order(wrt): + raise ValueError('Covariant derivatives are defined only with ' + 'respect to vector fields. The supplied argument ' + 'was not a vector field.') + christoffel = ImmutableDenseNDimArray(christoffel) + obj = super().__new__(cls, wrt, christoffel) + # deprecated assignments + obj._wrt = wrt + obj._christoffel = christoffel + return obj + + @property + def wrt(self): + return self.args[0] + + @property + def christoffel(self): + return self.args[1] + + def __call__(self, field): + vectors = list(self._wrt.atoms(BaseVectorField)) + base_ops = [BaseCovarDerivativeOp(v._coord_sys, v._index, self._christoffel) + for v in vectors] + return self._wrt.subs(list(zip(vectors, base_ops))).rcall(field) + + +############################################################################### +# Integral curves on vector fields +############################################################################### +def intcurve_series(vector_field, param, start_point, n=6, coord_sys=None, coeffs=False): + r"""Return the series expansion for an integral curve of the field. + + Explanation + =========== + + Integral curve is a function `\gamma` taking a parameter in `R` to a point + in the manifold. It verifies the equation: + + `V(f)\big(\gamma(t)\big) = \frac{d}{dt}f\big(\gamma(t)\big)` + + where the given ``vector_field`` is denoted as `V`. This holds for any + value `t` for the parameter and any scalar field `f`. + + This equation can also be decomposed of a basis of coordinate functions + `V(f_i)\big(\gamma(t)\big) = \frac{d}{dt}f_i\big(\gamma(t)\big) \quad \forall i` + + This function returns a series expansion of `\gamma(t)` in terms of the + coordinate system ``coord_sys``. The equations and expansions are necessarily + done in coordinate-system-dependent way as there is no other way to + represent movement between points on the manifold (i.e. there is no such + thing as a difference of points for a general manifold). + + Parameters + ========== + vector_field + the vector field for which an integral curve will be given + + param + the argument of the function `\gamma` from R to the curve + + start_point + the point which corresponds to `\gamma(0)` + + n + the order to which to expand + + coord_sys + the coordinate system in which to expand + coeffs (default False) - if True return a list of elements of the expansion + + Examples + ======== + + Use the predefined R2 manifold: + + >>> from sympy.abc import t, x, y + >>> from sympy.diffgeom.rn import R2_p, R2_r + >>> from sympy.diffgeom import intcurve_series + + Specify a starting point and a vector field: + + >>> start_point = R2_r.point([x, y]) + >>> vector_field = R2_r.e_x + + Calculate the series: + + >>> intcurve_series(vector_field, t, start_point, n=3) + Matrix([ + [t + x], + [ y]]) + + Or get the elements of the expansion in a list: + + >>> series = intcurve_series(vector_field, t, start_point, n=3, coeffs=True) + >>> series[0] + Matrix([ + [x], + [y]]) + >>> series[1] + Matrix([ + [t], + [0]]) + >>> series[2] + Matrix([ + [0], + [0]]) + + The series in the polar coordinate system: + + >>> series = intcurve_series(vector_field, t, start_point, + ... n=3, coord_sys=R2_p, coeffs=True) + >>> series[0] + Matrix([ + [sqrt(x**2 + y**2)], + [ atan2(y, x)]]) + >>> series[1] + Matrix([ + [t*x/sqrt(x**2 + y**2)], + [ -t*y/(x**2 + y**2)]]) + >>> series[2] + Matrix([ + [t**2*(-x**2/(x**2 + y**2)**(3/2) + 1/sqrt(x**2 + y**2))/2], + [ t**2*x*y/(x**2 + y**2)**2]]) + + See Also + ======== + + intcurve_diffequ + + """ + if contravariant_order(vector_field) != 1 or covariant_order(vector_field): + raise ValueError('The supplied field was not a vector field.') + + def iter_vfield(scalar_field, i): + """Return ``vector_field`` called `i` times on ``scalar_field``.""" + return reduce(lambda s, v: v.rcall(s), [vector_field, ]*i, scalar_field) + + def taylor_terms_per_coord(coord_function): + """Return the series for one of the coordinates.""" + return [param**i*iter_vfield(coord_function, i).rcall(start_point)/factorial(i) + for i in range(n)] + coord_sys = coord_sys if coord_sys else start_point._coord_sys + coord_functions = coord_sys.coord_functions() + taylor_terms = [taylor_terms_per_coord(f) for f in coord_functions] + if coeffs: + return [Matrix(t) for t in zip(*taylor_terms)] + else: + return Matrix([sum(c) for c in taylor_terms]) + + +def intcurve_diffequ(vector_field, param, start_point, coord_sys=None): + r"""Return the differential equation for an integral curve of the field. + + Explanation + =========== + + Integral curve is a function `\gamma` taking a parameter in `R` to a point + in the manifold. It verifies the equation: + + `V(f)\big(\gamma(t)\big) = \frac{d}{dt}f\big(\gamma(t)\big)` + + where the given ``vector_field`` is denoted as `V`. This holds for any + value `t` for the parameter and any scalar field `f`. + + This function returns the differential equation of `\gamma(t)` in terms of the + coordinate system ``coord_sys``. The equations and expansions are necessarily + done in coordinate-system-dependent way as there is no other way to + represent movement between points on the manifold (i.e. there is no such + thing as a difference of points for a general manifold). + + Parameters + ========== + + vector_field + the vector field for which an integral curve will be given + + param + the argument of the function `\gamma` from R to the curve + + start_point + the point which corresponds to `\gamma(0)` + + coord_sys + the coordinate system in which to give the equations + + Returns + ======= + + a tuple of (equations, initial conditions) + + Examples + ======== + + Use the predefined R2 manifold: + + >>> from sympy.abc import t + >>> from sympy.diffgeom.rn import R2, R2_p, R2_r + >>> from sympy.diffgeom import intcurve_diffequ + + Specify a starting point and a vector field: + + >>> start_point = R2_r.point([0, 1]) + >>> vector_field = -R2.y*R2.e_x + R2.x*R2.e_y + + Get the equation: + + >>> equations, init_cond = intcurve_diffequ(vector_field, t, start_point) + >>> equations + [f_1(t) + Derivative(f_0(t), t), -f_0(t) + Derivative(f_1(t), t)] + >>> init_cond + [f_0(0), f_1(0) - 1] + + The series in the polar coordinate system: + + >>> equations, init_cond = intcurve_diffequ(vector_field, t, start_point, R2_p) + >>> equations + [Derivative(f_0(t), t), Derivative(f_1(t), t) - 1] + >>> init_cond + [f_0(0) - 1, f_1(0) - pi/2] + + See Also + ======== + + intcurve_series + + """ + if contravariant_order(vector_field) != 1 or covariant_order(vector_field): + raise ValueError('The supplied field was not a vector field.') + coord_sys = coord_sys if coord_sys else start_point._coord_sys + gammas = [Function('f_%d' % i)(param) for i in range( + start_point._coord_sys.dim)] + arbitrary_p = Point(coord_sys, gammas) + coord_functions = coord_sys.coord_functions() + equations = [simplify(diff(cf.rcall(arbitrary_p), param) - vector_field.rcall(cf).rcall(arbitrary_p)) + for cf in coord_functions] + init_cond = [simplify(cf.rcall(arbitrary_p).subs(param, 0) - cf.rcall(start_point)) + for cf in coord_functions] + return equations, init_cond + + +############################################################################### +# Helpers +############################################################################### +def dummyfy(args, exprs): + # TODO Is this a good idea? + d_args = Matrix([s.as_dummy() for s in args]) + reps = dict(zip(args, d_args)) + d_exprs = Matrix([_sympify(expr).subs(reps) for expr in exprs]) + return d_args, d_exprs + +############################################################################### +# Helpers +############################################################################### +def contravariant_order(expr, _strict=False): + """Return the contravariant order of an expression. + + Examples + ======== + + >>> from sympy.diffgeom import contravariant_order + >>> from sympy.diffgeom.rn import R2 + >>> from sympy.abc import a + + >>> contravariant_order(a) + 0 + >>> contravariant_order(a*R2.x + 2) + 0 + >>> contravariant_order(a*R2.x*R2.e_y + R2.e_x) + 1 + + """ + # TODO move some of this to class methods. + # TODO rewrite using the .as_blah_blah methods + if isinstance(expr, Add): + orders = [contravariant_order(e) for e in expr.args] + if len(set(orders)) != 1: + raise ValueError('Misformed expression containing contravariant fields of varying order.') + return orders[0] + elif isinstance(expr, Mul): + orders = [contravariant_order(e) for e in expr.args] + not_zero = [o for o in orders if o != 0] + if len(not_zero) > 1: + raise ValueError('Misformed expression containing multiplication between vectors.') + return 0 if not not_zero else not_zero[0] + elif isinstance(expr, Pow): + if covariant_order(expr.base) or covariant_order(expr.exp): + raise ValueError( + 'Misformed expression containing a power of a vector.') + return 0 + elif isinstance(expr, BaseVectorField): + return 1 + elif isinstance(expr, TensorProduct): + return sum(contravariant_order(a) for a in expr.args) + elif not _strict or expr.atoms(BaseScalarField): + return 0 + else: # If it does not contain anything related to the diffgeom module and it is _strict + return -1 + + +def covariant_order(expr, _strict=False): + """Return the covariant order of an expression. + + Examples + ======== + + >>> from sympy.diffgeom import covariant_order + >>> from sympy.diffgeom.rn import R2 + >>> from sympy.abc import a + + >>> covariant_order(a) + 0 + >>> covariant_order(a*R2.x + 2) + 0 + >>> covariant_order(a*R2.x*R2.dy + R2.dx) + 1 + + """ + # TODO move some of this to class methods. + # TODO rewrite using the .as_blah_blah methods + if isinstance(expr, Add): + orders = [covariant_order(e) for e in expr.args] + if len(set(orders)) != 1: + raise ValueError('Misformed expression containing form fields of varying order.') + return orders[0] + elif isinstance(expr, Mul): + orders = [covariant_order(e) for e in expr.args] + not_zero = [o for o in orders if o != 0] + if len(not_zero) > 1: + raise ValueError('Misformed expression containing multiplication between forms.') + return 0 if not not_zero else not_zero[0] + elif isinstance(expr, Pow): + if covariant_order(expr.base) or covariant_order(expr.exp): + raise ValueError( + 'Misformed expression containing a power of a form.') + return 0 + elif isinstance(expr, Differential): + return covariant_order(*expr.args) + 1 + elif isinstance(expr, TensorProduct): + return sum(covariant_order(a) for a in expr.args) + elif not _strict or expr.atoms(BaseScalarField): + return 0 + else: # If it does not contain anything related to the diffgeom module and it is _strict + return -1 + + +############################################################################### +# Coordinate transformation functions +############################################################################### +def vectors_in_basis(expr, to_sys): + """Transform all base vectors in base vectors of a specified coord basis. + While the new base vectors are in the new coordinate system basis, any + coefficients are kept in the old system. + + Examples + ======== + + >>> from sympy.diffgeom import vectors_in_basis + >>> from sympy.diffgeom.rn import R2_r, R2_p + + >>> vectors_in_basis(R2_r.e_x, R2_p) + -y*e_theta/(x**2 + y**2) + x*e_rho/sqrt(x**2 + y**2) + >>> vectors_in_basis(R2_p.e_r, R2_r) + sin(theta)*e_y + cos(theta)*e_x + + """ + vectors = list(expr.atoms(BaseVectorField)) + new_vectors = [] + for v in vectors: + cs = v._coord_sys + jac = cs.jacobian(to_sys, cs.coord_functions()) + new = (jac.T*Matrix(to_sys.base_vectors()))[v._index] + new_vectors.append(new) + return expr.subs(list(zip(vectors, new_vectors))) + + +############################################################################### +# Coordinate-dependent functions +############################################################################### +def twoform_to_matrix(expr): + """Return the matrix representing the twoform. + + For the twoform `w` return the matrix `M` such that `M[i,j]=w(e_i, e_j)`, + where `e_i` is the i-th base vector field for the coordinate system in + which the expression of `w` is given. + + Examples + ======== + + >>> from sympy.diffgeom.rn import R2 + >>> from sympy.diffgeom import twoform_to_matrix, TensorProduct + >>> TP = TensorProduct + + >>> twoform_to_matrix(TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy)) + Matrix([ + [1, 0], + [0, 1]]) + >>> twoform_to_matrix(R2.x*TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy)) + Matrix([ + [x, 0], + [0, 1]]) + >>> twoform_to_matrix(TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy) - TP(R2.dx, R2.dy)/2) + Matrix([ + [ 1, 0], + [-1/2, 1]]) + + """ + if covariant_order(expr) != 2 or contravariant_order(expr): + raise ValueError('The input expression is not a two-form.') + coord_sys = _find_coords(expr) + if len(coord_sys) != 1: + raise ValueError('The input expression concerns more than one ' + 'coordinate systems, hence there is no unambiguous ' + 'way to choose a coordinate system for the matrix.') + coord_sys = coord_sys.pop() + vectors = coord_sys.base_vectors() + expr = expr.expand() + matrix_content = [[expr.rcall(v1, v2) for v1 in vectors] + for v2 in vectors] + return Matrix(matrix_content) + + +def metric_to_Christoffel_1st(expr): + """Return the nested list of Christoffel symbols for the given metric. + This returns the Christoffel symbol of first kind that represents the + Levi-Civita connection for the given metric. + + Examples + ======== + + >>> from sympy.diffgeom.rn import R2 + >>> from sympy.diffgeom import metric_to_Christoffel_1st, TensorProduct + >>> TP = TensorProduct + + >>> metric_to_Christoffel_1st(TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy)) + [[[0, 0], [0, 0]], [[0, 0], [0, 0]]] + >>> metric_to_Christoffel_1st(R2.x*TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy)) + [[[1/2, 0], [0, 0]], [[0, 0], [0, 0]]] + + """ + matrix = twoform_to_matrix(expr) + if not matrix.is_symmetric(): + raise ValueError( + 'The two-form representing the metric is not symmetric.') + coord_sys = _find_coords(expr).pop() + deriv_matrices = [matrix.applyfunc(d) for d in coord_sys.base_vectors()] + indices = list(range(coord_sys.dim)) + christoffel = [[[(deriv_matrices[k][i, j] + deriv_matrices[j][i, k] - deriv_matrices[i][j, k])/2 + for k in indices] + for j in indices] + for i in indices] + return ImmutableDenseNDimArray(christoffel) + + +def metric_to_Christoffel_2nd(expr): + """Return the nested list of Christoffel symbols for the given metric. + This returns the Christoffel symbol of second kind that represents the + Levi-Civita connection for the given metric. + + Examples + ======== + + >>> from sympy.diffgeom.rn import R2 + >>> from sympy.diffgeom import metric_to_Christoffel_2nd, TensorProduct + >>> TP = TensorProduct + + >>> metric_to_Christoffel_2nd(TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy)) + [[[0, 0], [0, 0]], [[0, 0], [0, 0]]] + >>> metric_to_Christoffel_2nd(R2.x*TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy)) + [[[1/(2*x), 0], [0, 0]], [[0, 0], [0, 0]]] + + """ + ch_1st = metric_to_Christoffel_1st(expr) + coord_sys = _find_coords(expr).pop() + indices = list(range(coord_sys.dim)) + # XXX workaround, inverting a matrix does not work if it contains non + # symbols + #matrix = twoform_to_matrix(expr).inv() + matrix = twoform_to_matrix(expr) + s_fields = set() + for e in matrix: + s_fields.update(e.atoms(BaseScalarField)) + s_fields = list(s_fields) + dums = coord_sys.symbols + matrix = matrix.subs(list(zip(s_fields, dums))).inv().subs(list(zip(dums, s_fields))) + # XXX end of workaround + christoffel = [[[Add(*[matrix[i, l]*ch_1st[l, j, k] for l in indices]) + for k in indices] + for j in indices] + for i in indices] + return ImmutableDenseNDimArray(christoffel) + + +def metric_to_Riemann_components(expr): + """Return the components of the Riemann tensor expressed in a given basis. + + Given a metric it calculates the components of the Riemann tensor in the + canonical basis of the coordinate system in which the metric expression is + given. + + Examples + ======== + + >>> from sympy import exp + >>> from sympy.diffgeom.rn import R2 + >>> from sympy.diffgeom import metric_to_Riemann_components, TensorProduct + >>> TP = TensorProduct + + >>> metric_to_Riemann_components(TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy)) + [[[[0, 0], [0, 0]], [[0, 0], [0, 0]]], [[[0, 0], [0, 0]], [[0, 0], [0, 0]]]] + >>> non_trivial_metric = exp(2*R2.r)*TP(R2.dr, R2.dr) + \ + R2.r**2*TP(R2.dtheta, R2.dtheta) + >>> non_trivial_metric + exp(2*rho)*TensorProduct(drho, drho) + rho**2*TensorProduct(dtheta, dtheta) + >>> riemann = metric_to_Riemann_components(non_trivial_metric) + >>> riemann[0, :, :, :] + [[[0, 0], [0, 0]], [[0, exp(-2*rho)*rho], [-exp(-2*rho)*rho, 0]]] + >>> riemann[1, :, :, :] + [[[0, -1/rho], [1/rho, 0]], [[0, 0], [0, 0]]] + + """ + ch_2nd = metric_to_Christoffel_2nd(expr) + coord_sys = _find_coords(expr).pop() + indices = list(range(coord_sys.dim)) + deriv_ch = [[[[d(ch_2nd[i, j, k]) + for d in coord_sys.base_vectors()] + for k in indices] + for j in indices] + for i in indices] + riemann_a = [[[[deriv_ch[rho][sig][nu][mu] - deriv_ch[rho][sig][mu][nu] + for nu in indices] + for mu in indices] + for sig in indices] + for rho in indices] + riemann_b = [[[[Add(*[ch_2nd[rho, l, mu]*ch_2nd[l, sig, nu] - ch_2nd[rho, l, nu]*ch_2nd[l, sig, mu] for l in indices]) + for nu in indices] + for mu in indices] + for sig in indices] + for rho in indices] + riemann = [[[[riemann_a[rho][sig][mu][nu] + riemann_b[rho][sig][mu][nu] + for nu in indices] + for mu in indices] + for sig in indices] + for rho in indices] + return ImmutableDenseNDimArray(riemann) + + +def metric_to_Ricci_components(expr): + + """Return the components of the Ricci tensor expressed in a given basis. + + Given a metric it calculates the components of the Ricci tensor in the + canonical basis of the coordinate system in which the metric expression is + given. + + Examples + ======== + + >>> from sympy import exp + >>> from sympy.diffgeom.rn import R2 + >>> from sympy.diffgeom import metric_to_Ricci_components, TensorProduct + >>> TP = TensorProduct + + >>> metric_to_Ricci_components(TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy)) + [[0, 0], [0, 0]] + >>> non_trivial_metric = exp(2*R2.r)*TP(R2.dr, R2.dr) + \ + R2.r**2*TP(R2.dtheta, R2.dtheta) + >>> non_trivial_metric + exp(2*rho)*TensorProduct(drho, drho) + rho**2*TensorProduct(dtheta, dtheta) + >>> metric_to_Ricci_components(non_trivial_metric) + [[1/rho, 0], [0, exp(-2*rho)*rho]] + + """ + riemann = metric_to_Riemann_components(expr) + coord_sys = _find_coords(expr).pop() + indices = list(range(coord_sys.dim)) + ricci = [[Add(*[riemann[k, i, k, j] for k in indices]) + for j in indices] + for i in indices] + return ImmutableDenseNDimArray(ricci) + +############################################################################### +# Classes for deprecation +############################################################################### + +class _deprecated_container: + # This class gives deprecation warning. + # When deprecated features are completely deleted, this should be removed as well. + # See https://github.com/sympy/sympy/pull/19368 + def __init__(self, message, data): + super().__init__(data) + self.message = message + + def warn(self): + sympy_deprecation_warning( + self.message, + deprecated_since_version="1.7", + active_deprecations_target="deprecated-diffgeom-mutable", + stacklevel=4 + ) + + def __iter__(self): + self.warn() + return super().__iter__() + + def __getitem__(self, key): + self.warn() + return super().__getitem__(key) + + def __contains__(self, key): + self.warn() + return super().__contains__(key) + + +class _deprecated_list(_deprecated_container, list): + pass + + +class _deprecated_dict(_deprecated_container, dict): + pass + + +# Import at end to avoid cyclic imports +from sympy.simplify.simplify import simplify diff --git a/venv/lib/python3.11/site-packages/sympy/diffgeom/rn.py b/venv/lib/python3.11/site-packages/sympy/diffgeom/rn.py new file mode 100644 index 0000000000000000000000000000000000000000..897c7e82bc804d260612f79c820af92632f3b281 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/diffgeom/rn.py @@ -0,0 +1,143 @@ +"""Predefined R^n manifolds together with common coord. systems. + +Coordinate systems are predefined as well as the transformation laws between +them. + +Coordinate functions can be accessed as attributes of the manifold (eg `R2.x`), +as attributes of the coordinate systems (eg `R2_r.x` and `R2_p.theta`), or by +using the usual `coord_sys.coord_function(index, name)` interface. +""" + +from typing import Any +import warnings + +from sympy.core.symbol import (Dummy, symbols) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (acos, atan2, cos, sin) +from .diffgeom import Manifold, Patch, CoordSystem + +__all__ = [ + 'R2', 'R2_origin', 'relations_2d', 'R2_r', 'R2_p', + 'R3', 'R3_origin', 'relations_3d', 'R3_r', 'R3_c', 'R3_s' +] + +############################################################################### +# R2 +############################################################################### +R2: Any = Manifold('R^2', 2) + +R2_origin: Any = Patch('origin', R2) + +x, y = symbols('x y', real=True) +r, theta = symbols('rho theta', nonnegative=True) + +relations_2d = { + ('rectangular', 'polar'): [(x, y), (sqrt(x**2 + y**2), atan2(y, x))], + ('polar', 'rectangular'): [(r, theta), (r*cos(theta), r*sin(theta))], +} + +R2_r: Any = CoordSystem('rectangular', R2_origin, (x, y), relations_2d) +R2_p: Any = CoordSystem('polar', R2_origin, (r, theta), relations_2d) + +# support deprecated feature +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + x, y, r, theta = symbols('x y r theta', cls=Dummy) + R2_r.connect_to(R2_p, [x, y], + [sqrt(x**2 + y**2), atan2(y, x)], + inverse=False, fill_in_gaps=False) + R2_p.connect_to(R2_r, [r, theta], + [r*cos(theta), r*sin(theta)], + inverse=False, fill_in_gaps=False) + +# Defining the basis coordinate functions and adding shortcuts for them to the +# manifold and the patch. +R2.x, R2.y = R2_origin.x, R2_origin.y = R2_r.x, R2_r.y = R2_r.coord_functions() +R2.r, R2.theta = R2_origin.r, R2_origin.theta = R2_p.r, R2_p.theta = R2_p.coord_functions() + +# Defining the basis vector fields and adding shortcuts for them to the +# manifold and the patch. +R2.e_x, R2.e_y = R2_origin.e_x, R2_origin.e_y = R2_r.e_x, R2_r.e_y = R2_r.base_vectors() +R2.e_r, R2.e_theta = R2_origin.e_r, R2_origin.e_theta = R2_p.e_r, R2_p.e_theta = R2_p.base_vectors() + +# Defining the basis oneform fields and adding shortcuts for them to the +# manifold and the patch. +R2.dx, R2.dy = R2_origin.dx, R2_origin.dy = R2_r.dx, R2_r.dy = R2_r.base_oneforms() +R2.dr, R2.dtheta = R2_origin.dr, R2_origin.dtheta = R2_p.dr, R2_p.dtheta = R2_p.base_oneforms() + +############################################################################### +# R3 +############################################################################### +R3: Any = Manifold('R^3', 3) + +R3_origin: Any = Patch('origin', R3) + +x, y, z = symbols('x y z', real=True) +rho, psi, r, theta, phi = symbols('rho psi r theta phi', nonnegative=True) + +relations_3d = { + ('rectangular', 'cylindrical'): [(x, y, z), + (sqrt(x**2 + y**2), atan2(y, x), z)], + ('cylindrical', 'rectangular'): [(rho, psi, z), + (rho*cos(psi), rho*sin(psi), z)], + ('rectangular', 'spherical'): [(x, y, z), + (sqrt(x**2 + y**2 + z**2), + acos(z/sqrt(x**2 + y**2 + z**2)), + atan2(y, x))], + ('spherical', 'rectangular'): [(r, theta, phi), + (r*sin(theta)*cos(phi), + r*sin(theta)*sin(phi), + r*cos(theta))], + ('cylindrical', 'spherical'): [(rho, psi, z), + (sqrt(rho**2 + z**2), + acos(z/sqrt(rho**2 + z**2)), + psi)], + ('spherical', 'cylindrical'): [(r, theta, phi), + (r*sin(theta), phi, r*cos(theta))], +} + +R3_r: Any = CoordSystem('rectangular', R3_origin, (x, y, z), relations_3d) +R3_c: Any = CoordSystem('cylindrical', R3_origin, (rho, psi, z), relations_3d) +R3_s: Any = CoordSystem('spherical', R3_origin, (r, theta, phi), relations_3d) + +# support deprecated feature +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + x, y, z, rho, psi, r, theta, phi = symbols('x y z rho psi r theta phi', cls=Dummy) + R3_r.connect_to(R3_c, [x, y, z], + [sqrt(x**2 + y**2), atan2(y, x), z], + inverse=False, fill_in_gaps=False) + R3_c.connect_to(R3_r, [rho, psi, z], + [rho*cos(psi), rho*sin(psi), z], + inverse=False, fill_in_gaps=False) + ## rectangular <-> spherical + R3_r.connect_to(R3_s, [x, y, z], + [sqrt(x**2 + y**2 + z**2), acos(z/ + sqrt(x**2 + y**2 + z**2)), atan2(y, x)], + inverse=False, fill_in_gaps=False) + R3_s.connect_to(R3_r, [r, theta, phi], + [r*sin(theta)*cos(phi), r*sin( + theta)*sin(phi), r*cos(theta)], + inverse=False, fill_in_gaps=False) + ## cylindrical <-> spherical + R3_c.connect_to(R3_s, [rho, psi, z], + [sqrt(rho**2 + z**2), acos(z/sqrt(rho**2 + z**2)), psi], + inverse=False, fill_in_gaps=False) + R3_s.connect_to(R3_c, [r, theta, phi], + [r*sin(theta), phi, r*cos(theta)], + inverse=False, fill_in_gaps=False) + +# Defining the basis coordinate functions. +R3_r.x, R3_r.y, R3_r.z = R3_r.coord_functions() +R3_c.rho, R3_c.psi, R3_c.z = R3_c.coord_functions() +R3_s.r, R3_s.theta, R3_s.phi = R3_s.coord_functions() + +# Defining the basis vector fields. +R3_r.e_x, R3_r.e_y, R3_r.e_z = R3_r.base_vectors() +R3_c.e_rho, R3_c.e_psi, R3_c.e_z = R3_c.base_vectors() +R3_s.e_r, R3_s.e_theta, R3_s.e_phi = R3_s.base_vectors() + +# Defining the basis oneform fields. +R3_r.dx, R3_r.dy, R3_r.dz = R3_r.base_oneforms() +R3_c.drho, R3_c.dpsi, R3_c.dz = R3_c.base_oneforms() +R3_s.dr, R3_s.dtheta, R3_s.dphi = R3_s.base_oneforms() diff --git a/venv/lib/python3.11/site-packages/sympy/diffgeom/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/diffgeom/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/diffgeom/tests/test_class_structure.py b/venv/lib/python3.11/site-packages/sympy/diffgeom/tests/test_class_structure.py new file mode 100644 index 0000000000000000000000000000000000000000..c649fd9fcb9acdf1f410a021966c6e0fee62cc2b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/diffgeom/tests/test_class_structure.py @@ -0,0 +1,33 @@ +from sympy.diffgeom import Manifold, Patch, CoordSystem, Point +from sympy.core.function import Function +from sympy.core.symbol import symbols +from sympy.testing.pytest import warns_deprecated_sympy + +m = Manifold('m', 2) +p = Patch('p', m) +a, b = symbols('a b') +cs = CoordSystem('cs', p, [a, b]) +x, y = symbols('x y') +f = Function('f') +s1, s2 = cs.coord_functions() +v1, v2 = cs.base_vectors() +f1, f2 = cs.base_oneforms() + +def test_point(): + point = Point(cs, [x, y]) + assert point != Point(cs, [2, y]) + #TODO assert point.subs(x, 2) == Point(cs, [2, y]) + #TODO assert point.free_symbols == set([x, y]) + +def test_subs(): + assert s1.subs(s1, s2) == s2 + assert v1.subs(v1, v2) == v2 + assert f1.subs(f1, f2) == f2 + assert (x*f(s1) + y).subs(s1, s2) == x*f(s2) + y + assert (f(s1)*v1).subs(v1, v2) == f(s1)*v2 + assert (y*f(s1)*f1).subs(f1, f2) == y*f(s1)*f2 + +def test_deprecated(): + with warns_deprecated_sympy(): + cs_wname = CoordSystem('cs', p, ['a', 'b']) + assert cs_wname == cs_wname.func(*cs_wname.args) diff --git a/venv/lib/python3.11/site-packages/sympy/diffgeom/tests/test_diffgeom.py b/venv/lib/python3.11/site-packages/sympy/diffgeom/tests/test_diffgeom.py new file mode 100644 index 0000000000000000000000000000000000000000..7c3c9265785896b8f4ffa3a2b41816ca90579758 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/diffgeom/tests/test_diffgeom.py @@ -0,0 +1,342 @@ +from sympy.core import Lambda, Symbol, symbols +from sympy.diffgeom.rn import R2, R2_p, R2_r, R3_r, R3_c, R3_s, R2_origin +from sympy.diffgeom import (Manifold, Patch, CoordSystem, Commutator, Differential, TensorProduct, + WedgeProduct, BaseCovarDerivativeOp, CovarDerivativeOp, LieDerivative, + covariant_order, contravariant_order, twoform_to_matrix, metric_to_Christoffel_1st, + metric_to_Christoffel_2nd, metric_to_Riemann_components, + metric_to_Ricci_components, intcurve_diffequ, intcurve_series) +from sympy.simplify import trigsimp, simplify +from sympy.functions import sqrt, atan2, sin +from sympy.matrices import Matrix +from sympy.testing.pytest import raises, nocache_fail +from sympy.testing.pytest import warns_deprecated_sympy + +TP = TensorProduct + + +def test_coordsys_transform(): + # test inverse transforms + p, q, r, s = symbols('p q r s') + rel = {('first', 'second'): [(p, q), (q, -p)]} + R2_pq = CoordSystem('first', R2_origin, [p, q], rel) + R2_rs = CoordSystem('second', R2_origin, [r, s], rel) + r, s = R2_rs.symbols + assert R2_rs.transform(R2_pq) == Matrix([[-s], [r]]) + + # inverse transform impossible case + a, b = symbols('a b', positive=True) + rel = {('first', 'second'): [(a,), (-a,)]} + R2_a = CoordSystem('first', R2_origin, [a], rel) + R2_b = CoordSystem('second', R2_origin, [b], rel) + # This transformation is uninvertible because there is no positive a, b satisfying a = -b + with raises(NotImplementedError): + R2_b.transform(R2_a) + + # inverse transform ambiguous case + c, d = symbols('c d') + rel = {('first', 'second'): [(c,), (c**2,)]} + R2_c = CoordSystem('first', R2_origin, [c], rel) + R2_d = CoordSystem('second', R2_origin, [d], rel) + # The transform method should throw if it finds multiple inverses for a coordinate transformation. + with raises(ValueError): + R2_d.transform(R2_c) + + # test indirect transformation + a, b, c, d, e, f = symbols('a, b, c, d, e, f') + rel = {('C1', 'C2'): [(a, b), (2*a, 3*b)], + ('C2', 'C3'): [(c, d), (3*c, 2*d)]} + C1 = CoordSystem('C1', R2_origin, (a, b), rel) + C2 = CoordSystem('C2', R2_origin, (c, d), rel) + C3 = CoordSystem('C3', R2_origin, (e, f), rel) + a, b = C1.symbols + c, d = C2.symbols + e, f = C3.symbols + assert C2.transform(C1) == Matrix([c/2, d/3]) + assert C1.transform(C3) == Matrix([6*a, 6*b]) + assert C3.transform(C1) == Matrix([e/6, f/6]) + assert C3.transform(C2) == Matrix([e/3, f/2]) + + a, b, c, d, e, f = symbols('a, b, c, d, e, f') + rel = {('C1', 'C2'): [(a, b), (2*a, 3*b + 1)], + ('C3', 'C2'): [(e, f), (-e - 2, 2*f)]} + C1 = CoordSystem('C1', R2_origin, (a, b), rel) + C2 = CoordSystem('C2', R2_origin, (c, d), rel) + C3 = CoordSystem('C3', R2_origin, (e, f), rel) + a, b = C1.symbols + c, d = C2.symbols + e, f = C3.symbols + assert C2.transform(C1) == Matrix([c/2, (d - 1)/3]) + assert C1.transform(C3) == Matrix([-2*a - 2, (3*b + 1)/2]) + assert C3.transform(C1) == Matrix([-e/2 - 1, (2*f - 1)/3]) + assert C3.transform(C2) == Matrix([-e - 2, 2*f]) + + # old signature uses Lambda + a, b, c, d, e, f = symbols('a, b, c, d, e, f') + rel = {('C1', 'C2'): Lambda((a, b), (2*a, 3*b + 1)), + ('C3', 'C2'): Lambda((e, f), (-e - 2, 2*f))} + C1 = CoordSystem('C1', R2_origin, (a, b), rel) + C2 = CoordSystem('C2', R2_origin, (c, d), rel) + C3 = CoordSystem('C3', R2_origin, (e, f), rel) + a, b = C1.symbols + c, d = C2.symbols + e, f = C3.symbols + assert C2.transform(C1) == Matrix([c/2, (d - 1)/3]) + assert C1.transform(C3) == Matrix([-2*a - 2, (3*b + 1)/2]) + assert C3.transform(C1) == Matrix([-e/2 - 1, (2*f - 1)/3]) + assert C3.transform(C2) == Matrix([-e - 2, 2*f]) + + +def test_R2(): + x0, y0, r0, theta0 = symbols('x0, y0, r0, theta0', real=True) + point_r = R2_r.point([x0, y0]) + point_p = R2_p.point([r0, theta0]) + + # r**2 = x**2 + y**2 + assert (R2.r**2 - R2.x**2 - R2.y**2).rcall(point_r) == 0 + assert trigsimp( (R2.r**2 - R2.x**2 - R2.y**2).rcall(point_p) ) == 0 + assert trigsimp(R2.e_r(R2.x**2 + R2.y**2).rcall(point_p).doit()) == 2*r0 + + # polar->rect->polar == Id + a, b = symbols('a b', positive=True) + m = Matrix([[a], [b]]) + + #TODO assert m == R2_r.transform(R2_p, R2_p.transform(R2_r, [a, b])).applyfunc(simplify) + assert m == R2_p.transform(R2_r, R2_r.transform(R2_p, m)).applyfunc(simplify) + + # deprecated method + with warns_deprecated_sympy(): + assert m == R2_p.coord_tuple_transform_to( + R2_r, R2_r.coord_tuple_transform_to(R2_p, m)).applyfunc(simplify) + + +def test_R3(): + a, b, c = symbols('a b c', positive=True) + m = Matrix([[a], [b], [c]]) + + assert m == R3_c.transform(R3_r, R3_r.transform(R3_c, m)).applyfunc(simplify) + #TODO assert m == R3_r.transform(R3_c, R3_c.transform(R3_r, m)).applyfunc(simplify) + assert m == R3_s.transform( + R3_r, R3_r.transform(R3_s, m)).applyfunc(simplify) + #TODO assert m == R3_r.transform(R3_s, R3_s.transform(R3_r, m)).applyfunc(simplify) + assert m == R3_s.transform( + R3_c, R3_c.transform(R3_s, m)).applyfunc(simplify) + #TODO assert m == R3_c.transform(R3_s, R3_s.transform(R3_c, m)).applyfunc(simplify) + + with warns_deprecated_sympy(): + assert m == R3_c.coord_tuple_transform_to( + R3_r, R3_r.coord_tuple_transform_to(R3_c, m)).applyfunc(simplify) + #TODO assert m == R3_r.coord_tuple_transform_to(R3_c, R3_c.coord_tuple_transform_to(R3_r, m)).applyfunc(simplify) + assert m == R3_s.coord_tuple_transform_to( + R3_r, R3_r.coord_tuple_transform_to(R3_s, m)).applyfunc(simplify) + #TODO assert m == R3_r.coord_tuple_transform_to(R3_s, R3_s.coord_tuple_transform_to(R3_r, m)).applyfunc(simplify) + assert m == R3_s.coord_tuple_transform_to( + R3_c, R3_c.coord_tuple_transform_to(R3_s, m)).applyfunc(simplify) + #TODO assert m == R3_c.coord_tuple_transform_to(R3_s, R3_s.coord_tuple_transform_to(R3_c, m)).applyfunc(simplify) + + +def test_CoordinateSymbol(): + x, y = R2_r.symbols + r, theta = R2_p.symbols + assert y.rewrite(R2_p) == r*sin(theta) + + +def test_point(): + x, y = symbols('x, y') + p = R2_r.point([x, y]) + assert p.free_symbols == {x, y} + assert p.coords(R2_r) == p.coords() == Matrix([x, y]) + assert p.coords(R2_p) == Matrix([sqrt(x**2 + y**2), atan2(y, x)]) + + +def test_commutator(): + assert Commutator(R2.e_x, R2.e_y) == 0 + assert Commutator(R2.x*R2.e_x, R2.x*R2.e_x) == 0 + assert Commutator(R2.x*R2.e_x, R2.x*R2.e_y) == R2.x*R2.e_y + c = Commutator(R2.e_x, R2.e_r) + assert c(R2.x) == R2.y*(R2.x**2 + R2.y**2)**(-1)*sin(R2.theta) + + +def test_differential(): + xdy = R2.x*R2.dy + dxdy = Differential(xdy) + assert xdy.rcall(None) == xdy + assert dxdy(R2.e_x, R2.e_y) == 1 + assert dxdy(R2.e_x, R2.x*R2.e_y) == R2.x + assert Differential(dxdy) == 0 + + +def test_products(): + assert TensorProduct( + R2.dx, R2.dy)(R2.e_x, R2.e_y) == R2.dx(R2.e_x)*R2.dy(R2.e_y) == 1 + assert TensorProduct(R2.dx, R2.dy)(None, R2.e_y) == R2.dx + assert TensorProduct(R2.dx, R2.dy)(R2.e_x, None) == R2.dy + assert TensorProduct(R2.dx, R2.dy)(R2.e_x) == R2.dy + assert TensorProduct(R2.x, R2.dx) == R2.x*R2.dx + assert TensorProduct( + R2.e_x, R2.e_y)(R2.x, R2.y) == R2.e_x(R2.x) * R2.e_y(R2.y) == 1 + assert TensorProduct(R2.e_x, R2.e_y)(None, R2.y) == R2.e_x + assert TensorProduct(R2.e_x, R2.e_y)(R2.x, None) == R2.e_y + assert TensorProduct(R2.e_x, R2.e_y)(R2.x) == R2.e_y + assert TensorProduct(R2.x, R2.e_x) == R2.x * R2.e_x + assert TensorProduct( + R2.dx, R2.e_y)(R2.e_x, R2.y) == R2.dx(R2.e_x) * R2.e_y(R2.y) == 1 + assert TensorProduct(R2.dx, R2.e_y)(None, R2.y) == R2.dx + assert TensorProduct(R2.dx, R2.e_y)(R2.e_x, None) == R2.e_y + assert TensorProduct(R2.dx, R2.e_y)(R2.e_x) == R2.e_y + assert TensorProduct(R2.x, R2.e_x) == R2.x * R2.e_x + assert TensorProduct( + R2.e_x, R2.dy)(R2.x, R2.e_y) == R2.e_x(R2.x) * R2.dy(R2.e_y) == 1 + assert TensorProduct(R2.e_x, R2.dy)(None, R2.e_y) == R2.e_x + assert TensorProduct(R2.e_x, R2.dy)(R2.x, None) == R2.dy + assert TensorProduct(R2.e_x, R2.dy)(R2.x) == R2.dy + assert TensorProduct(R2.e_y,R2.e_x)(R2.x**2 + R2.y**2,R2.x**2 + R2.y**2) == 4*R2.x*R2.y + + assert WedgeProduct(R2.dx, R2.dy)(R2.e_x, R2.e_y) == 1 + assert WedgeProduct(R2.e_x, R2.e_y)(R2.x, R2.y) == 1 + + +def test_lie_derivative(): + assert LieDerivative(R2.e_x, R2.y) == R2.e_x(R2.y) == 0 + assert LieDerivative(R2.e_x, R2.x) == R2.e_x(R2.x) == 1 + assert LieDerivative(R2.e_x, R2.e_x) == Commutator(R2.e_x, R2.e_x) == 0 + assert LieDerivative(R2.e_x, R2.e_r) == Commutator(R2.e_x, R2.e_r) + assert LieDerivative(R2.e_x + R2.e_y, R2.x) == 1 + assert LieDerivative( + R2.e_x, TensorProduct(R2.dx, R2.dy))(R2.e_x, R2.e_y) == 0 + + +@nocache_fail +def test_covar_deriv(): + ch = metric_to_Christoffel_2nd(TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy)) + cvd = BaseCovarDerivativeOp(R2_r, 0, ch) + assert cvd(R2.x) == 1 + # This line fails if the cache is disabled: + assert cvd(R2.x*R2.e_x) == R2.e_x + cvd = CovarDerivativeOp(R2.x*R2.e_x, ch) + assert cvd(R2.x) == R2.x + assert cvd(R2.x*R2.e_x) == R2.x*R2.e_x + + +def test_intcurve_diffequ(): + t = symbols('t') + start_point = R2_r.point([1, 0]) + vector_field = -R2.y*R2.e_x + R2.x*R2.e_y + equations, init_cond = intcurve_diffequ(vector_field, t, start_point) + assert str(equations) == '[f_1(t) + Derivative(f_0(t), t), -f_0(t) + Derivative(f_1(t), t)]' + assert str(init_cond) == '[f_0(0) - 1, f_1(0)]' + equations, init_cond = intcurve_diffequ(vector_field, t, start_point, R2_p) + assert str( + equations) == '[Derivative(f_0(t), t), Derivative(f_1(t), t) - 1]' + assert str(init_cond) == '[f_0(0) - 1, f_1(0)]' + + +def test_helpers_and_coordinate_dependent(): + one_form = R2.dr + R2.dx + two_form = Differential(R2.x*R2.dr + R2.r*R2.dx) + three_form = Differential( + R2.y*two_form) + Differential(R2.x*Differential(R2.r*R2.dr)) + metric = TensorProduct(R2.dx, R2.dx) + TensorProduct(R2.dy, R2.dy) + metric_ambig = TensorProduct(R2.dx, R2.dx) + TensorProduct(R2.dr, R2.dr) + misform_a = TensorProduct(R2.dr, R2.dr) + R2.dr + misform_b = R2.dr**4 + misform_c = R2.dx*R2.dy + twoform_not_sym = TensorProduct(R2.dx, R2.dx) + TensorProduct(R2.dx, R2.dy) + twoform_not_TP = WedgeProduct(R2.dx, R2.dy) + + one_vector = R2.e_x + R2.e_y + two_vector = TensorProduct(R2.e_x, R2.e_y) + three_vector = TensorProduct(R2.e_x, R2.e_y, R2.e_x) + two_wp = WedgeProduct(R2.e_x,R2.e_y) + + assert covariant_order(one_form) == 1 + assert covariant_order(two_form) == 2 + assert covariant_order(three_form) == 3 + assert covariant_order(two_form + metric) == 2 + assert covariant_order(two_form + metric_ambig) == 2 + assert covariant_order(two_form + twoform_not_sym) == 2 + assert covariant_order(two_form + twoform_not_TP) == 2 + + assert contravariant_order(one_vector) == 1 + assert contravariant_order(two_vector) == 2 + assert contravariant_order(three_vector) == 3 + assert contravariant_order(two_vector + two_wp) == 2 + + raises(ValueError, lambda: covariant_order(misform_a)) + raises(ValueError, lambda: covariant_order(misform_b)) + raises(ValueError, lambda: covariant_order(misform_c)) + + assert twoform_to_matrix(metric) == Matrix([[1, 0], [0, 1]]) + assert twoform_to_matrix(twoform_not_sym) == Matrix([[1, 0], [1, 0]]) + assert twoform_to_matrix(twoform_not_TP) == Matrix([[0, -1], [1, 0]]) + + raises(ValueError, lambda: twoform_to_matrix(one_form)) + raises(ValueError, lambda: twoform_to_matrix(three_form)) + raises(ValueError, lambda: twoform_to_matrix(metric_ambig)) + + raises(ValueError, lambda: metric_to_Christoffel_1st(twoform_not_sym)) + raises(ValueError, lambda: metric_to_Christoffel_2nd(twoform_not_sym)) + raises(ValueError, lambda: metric_to_Riemann_components(twoform_not_sym)) + raises(ValueError, lambda: metric_to_Ricci_components(twoform_not_sym)) + + +def test_correct_arguments(): + raises(ValueError, lambda: R2.e_x(R2.e_x)) + raises(ValueError, lambda: R2.e_x(R2.dx)) + + raises(ValueError, lambda: Commutator(R2.e_x, R2.x)) + raises(ValueError, lambda: Commutator(R2.dx, R2.e_x)) + + raises(ValueError, lambda: Differential(Differential(R2.e_x))) + + raises(ValueError, lambda: R2.dx(R2.x)) + + raises(ValueError, lambda: LieDerivative(R2.dx, R2.dx)) + raises(ValueError, lambda: LieDerivative(R2.x, R2.dx)) + + raises(ValueError, lambda: CovarDerivativeOp(R2.dx, [])) + raises(ValueError, lambda: CovarDerivativeOp(R2.x, [])) + + a = Symbol('a') + raises(ValueError, lambda: intcurve_series(R2.dx, a, R2_r.point([1, 2]))) + raises(ValueError, lambda: intcurve_series(R2.x, a, R2_r.point([1, 2]))) + + raises(ValueError, lambda: intcurve_diffequ(R2.dx, a, R2_r.point([1, 2]))) + raises(ValueError, lambda: intcurve_diffequ(R2.x, a, R2_r.point([1, 2]))) + + raises(ValueError, lambda: contravariant_order(R2.e_x + R2.dx)) + raises(ValueError, lambda: covariant_order(R2.e_x + R2.dx)) + + raises(ValueError, lambda: contravariant_order(R2.e_x*R2.e_y)) + raises(ValueError, lambda: covariant_order(R2.dx*R2.dy)) + +def test_simplify(): + x, y = R2_r.coord_functions() + dx, dy = R2_r.base_oneforms() + ex, ey = R2_r.base_vectors() + assert simplify(x) == x + assert simplify(x*y) == x*y + assert simplify(dx*dy) == dx*dy + assert simplify(ex*ey) == ex*ey + assert ((1-x)*dx)/(1-x)**2 == dx/(1-x) + + +def test_issue_17917(): + X = R2.x*R2.e_x - R2.y*R2.e_y + Y = (R2.x**2 + R2.y**2)*R2.e_x - R2.x*R2.y*R2.e_y + assert LieDerivative(X, Y).expand() == ( + R2.x**2*R2.e_x - 3*R2.y**2*R2.e_x - R2.x*R2.y*R2.e_y) + +def test_deprecations(): + m = Manifold('M', 2) + p = Patch('P', m) + with warns_deprecated_sympy(): + CoordSystem('Car2d', p, names=['x', 'y']) + + with warns_deprecated_sympy(): + c = CoordSystem('Car2d', p, ['x', 'y']) + + with warns_deprecated_sympy(): + list(m.patches) + + with warns_deprecated_sympy(): + list(c.transforms) diff --git a/venv/lib/python3.11/site-packages/sympy/diffgeom/tests/test_function_diffgeom_book.py b/venv/lib/python3.11/site-packages/sympy/diffgeom/tests/test_function_diffgeom_book.py new file mode 100644 index 0000000000000000000000000000000000000000..44d9623bc34ab73c7d575d9d9fd5b6d84f8e4a94 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/diffgeom/tests/test_function_diffgeom_book.py @@ -0,0 +1,145 @@ +from sympy.diffgeom.rn import R2, R2_p, R2_r, R3_r +from sympy.diffgeom import intcurve_series, Differential, WedgeProduct +from sympy.core import symbols, Function, Derivative +from sympy.simplify import trigsimp, simplify +from sympy.functions import sqrt, atan2, sin, cos +from sympy.matrices import Matrix + +# Most of the functionality is covered in the +# test_functional_diffgeom_ch* tests which are based on the +# example from the paper of Sussman and Wisdom. +# If they do not cover something, additional tests are added in other test +# functions. + +# From "Functional Differential Geometry" as of 2011 +# by Sussman and Wisdom. + + +def test_functional_diffgeom_ch2(): + x0, y0, r0, theta0 = symbols('x0, y0, r0, theta0', real=True) + x, y = symbols('x, y', real=True) + f = Function('f') + + assert (R2_p.point_to_coords(R2_r.point([x0, y0])) == + Matrix([sqrt(x0**2 + y0**2), atan2(y0, x0)])) + assert (R2_r.point_to_coords(R2_p.point([r0, theta0])) == + Matrix([r0*cos(theta0), r0*sin(theta0)])) + + assert R2_p.jacobian(R2_r, [r0, theta0]) == Matrix( + [[cos(theta0), -r0*sin(theta0)], [sin(theta0), r0*cos(theta0)]]) + + field = f(R2.x, R2.y) + p1_in_rect = R2_r.point([x0, y0]) + p1_in_polar = R2_p.point([sqrt(x0**2 + y0**2), atan2(y0, x0)]) + assert field.rcall(p1_in_rect) == f(x0, y0) + assert field.rcall(p1_in_polar) == f(x0, y0) + + p_r = R2_r.point([x0, y0]) + p_p = R2_p.point([r0, theta0]) + assert R2.x(p_r) == x0 + assert R2.x(p_p) == r0*cos(theta0) + assert R2.r(p_p) == r0 + assert R2.r(p_r) == sqrt(x0**2 + y0**2) + assert R2.theta(p_r) == atan2(y0, x0) + + h = R2.x*R2.r**2 + R2.y**3 + assert h.rcall(p_r) == x0*(x0**2 + y0**2) + y0**3 + assert h.rcall(p_p) == r0**3*sin(theta0)**3 + r0**3*cos(theta0) + + +def test_functional_diffgeom_ch3(): + x0, y0 = symbols('x0, y0', real=True) + x, y, t = symbols('x, y, t', real=True) + f = Function('f') + b1 = Function('b1') + b2 = Function('b2') + p_r = R2_r.point([x0, y0]) + + s_field = f(R2.x, R2.y) + v_field = b1(R2.x)*R2.e_x + b2(R2.y)*R2.e_y + assert v_field.rcall(s_field).rcall(p_r).doit() == b1( + x0)*Derivative(f(x0, y0), x0) + b2(y0)*Derivative(f(x0, y0), y0) + + assert R2.e_x(R2.r**2).rcall(p_r) == 2*x0 + v = R2.e_x + 2*R2.e_y + s = R2.r**2 + 3*R2.x + assert v.rcall(s).rcall(p_r).doit() == 2*x0 + 4*y0 + 3 + + circ = -R2.y*R2.e_x + R2.x*R2.e_y + series = intcurve_series(circ, t, R2_r.point([1, 0]), coeffs=True) + series_x, series_y = zip(*series) + assert all( + term == cos(t).taylor_term(i, t) for i, term in enumerate(series_x)) + assert all( + term == sin(t).taylor_term(i, t) for i, term in enumerate(series_y)) + + +def test_functional_diffgeom_ch4(): + x0, y0, theta0 = symbols('x0, y0, theta0', real=True) + x, y, r, theta = symbols('x, y, r, theta', real=True) + r0 = symbols('r0', positive=True) + f = Function('f') + b1 = Function('b1') + b2 = Function('b2') + p_r = R2_r.point([x0, y0]) + p_p = R2_p.point([r0, theta0]) + + f_field = b1(R2.x, R2.y)*R2.dx + b2(R2.x, R2.y)*R2.dy + assert f_field.rcall(R2.e_x).rcall(p_r) == b1(x0, y0) + assert f_field.rcall(R2.e_y).rcall(p_r) == b2(x0, y0) + + s_field_r = f(R2.x, R2.y) + df = Differential(s_field_r) + assert df(R2.e_x).rcall(p_r).doit() == Derivative(f(x0, y0), x0) + assert df(R2.e_y).rcall(p_r).doit() == Derivative(f(x0, y0), y0) + + s_field_p = f(R2.r, R2.theta) + df = Differential(s_field_p) + assert trigsimp(df(R2.e_x).rcall(p_p).doit()) == ( + cos(theta0)*Derivative(f(r0, theta0), r0) - + sin(theta0)*Derivative(f(r0, theta0), theta0)/r0) + assert trigsimp(df(R2.e_y).rcall(p_p).doit()) == ( + sin(theta0)*Derivative(f(r0, theta0), r0) + + cos(theta0)*Derivative(f(r0, theta0), theta0)/r0) + + assert R2.dx(R2.e_x).rcall(p_r) == 1 + assert R2.dx(R2.e_x) == 1 + assert R2.dx(R2.e_y).rcall(p_r) == 0 + assert R2.dx(R2.e_y) == 0 + + circ = -R2.y*R2.e_x + R2.x*R2.e_y + assert R2.dx(circ).rcall(p_r).doit() == -y0 + assert R2.dy(circ).rcall(p_r) == x0 + assert R2.dr(circ).rcall(p_r) == 0 + assert simplify(R2.dtheta(circ).rcall(p_r)) == 1 + + assert (circ - R2.e_theta).rcall(s_field_r).rcall(p_r) == 0 + + +def test_functional_diffgeom_ch6(): + u0, u1, u2, v0, v1, v2, w0, w1, w2 = symbols('u0:3, v0:3, w0:3', real=True) + + u = u0*R2.e_x + u1*R2.e_y + v = v0*R2.e_x + v1*R2.e_y + wp = WedgeProduct(R2.dx, R2.dy) + assert wp(u, v) == u0*v1 - u1*v0 + + u = u0*R3_r.e_x + u1*R3_r.e_y + u2*R3_r.e_z + v = v0*R3_r.e_x + v1*R3_r.e_y + v2*R3_r.e_z + w = w0*R3_r.e_x + w1*R3_r.e_y + w2*R3_r.e_z + wp = WedgeProduct(R3_r.dx, R3_r.dy, R3_r.dz) + assert wp( + u, v, w) == Matrix(3, 3, [u0, u1, u2, v0, v1, v2, w0, w1, w2]).det() + + a, b, c = symbols('a, b, c', cls=Function) + a_f = a(R3_r.x, R3_r.y, R3_r.z) + b_f = b(R3_r.x, R3_r.y, R3_r.z) + c_f = c(R3_r.x, R3_r.y, R3_r.z) + theta = a_f*R3_r.dx + b_f*R3_r.dy + c_f*R3_r.dz + dtheta = Differential(theta) + da = Differential(a_f) + db = Differential(b_f) + dc = Differential(c_f) + expr = dtheta - WedgeProduct( + da, R3_r.dx) - WedgeProduct(db, R3_r.dy) - WedgeProduct(dc, R3_r.dz) + assert expr.rcall(R3_r.e_x, R3_r.e_y) == 0 diff --git a/venv/lib/python3.11/site-packages/sympy/diffgeom/tests/test_hyperbolic_space.py b/venv/lib/python3.11/site-packages/sympy/diffgeom/tests/test_hyperbolic_space.py new file mode 100644 index 0000000000000000000000000000000000000000..48ddc7f8065f2b69bcd8eca4726a21c5901514ec --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/diffgeom/tests/test_hyperbolic_space.py @@ -0,0 +1,91 @@ +r''' +unit test describing the hyperbolic half-plane with the Poincare metric. This +is a basic model of hyperbolic geometry on the (positive) half-space + +{(x,y) \in R^2 | y > 0} + +with the Riemannian metric + +ds^2 = (dx^2 + dy^2)/y^2 + +It has constant negative scalar curvature = -2 + +https://en.wikipedia.org/wiki/Poincare_half-plane_model +''' +from sympy.matrices.dense import diag +from sympy.diffgeom import (twoform_to_matrix, + metric_to_Christoffel_1st, metric_to_Christoffel_2nd, + metric_to_Riemann_components, metric_to_Ricci_components) +import sympy.diffgeom.rn +from sympy.tensor.array import ImmutableDenseNDimArray + + +def test_H2(): + TP = sympy.diffgeom.TensorProduct + R2 = sympy.diffgeom.rn.R2 + y = R2.y + dy = R2.dy + dx = R2.dx + g = (TP(dx, dx) + TP(dy, dy))*y**(-2) + automat = twoform_to_matrix(g) + mat = diag(y**(-2), y**(-2)) + assert mat == automat + + gamma1 = metric_to_Christoffel_1st(g) + assert gamma1[0, 0, 0] == 0 + assert gamma1[0, 0, 1] == -y**(-3) + assert gamma1[0, 1, 0] == -y**(-3) + assert gamma1[0, 1, 1] == 0 + + assert gamma1[1, 1, 1] == -y**(-3) + assert gamma1[1, 1, 0] == 0 + assert gamma1[1, 0, 1] == 0 + assert gamma1[1, 0, 0] == y**(-3) + + gamma2 = metric_to_Christoffel_2nd(g) + assert gamma2[0, 0, 0] == 0 + assert gamma2[0, 0, 1] == -y**(-1) + assert gamma2[0, 1, 0] == -y**(-1) + assert gamma2[0, 1, 1] == 0 + + assert gamma2[1, 1, 1] == -y**(-1) + assert gamma2[1, 1, 0] == 0 + assert gamma2[1, 0, 1] == 0 + assert gamma2[1, 0, 0] == y**(-1) + + Rm = metric_to_Riemann_components(g) + assert Rm[0, 0, 0, 0] == 0 + assert Rm[0, 0, 0, 1] == 0 + assert Rm[0, 0, 1, 0] == 0 + assert Rm[0, 0, 1, 1] == 0 + + assert Rm[0, 1, 0, 0] == 0 + assert Rm[0, 1, 0, 1] == -y**(-2) + assert Rm[0, 1, 1, 0] == y**(-2) + assert Rm[0, 1, 1, 1] == 0 + + assert Rm[1, 0, 0, 0] == 0 + assert Rm[1, 0, 0, 1] == y**(-2) + assert Rm[1, 0, 1, 0] == -y**(-2) + assert Rm[1, 0, 1, 1] == 0 + + assert Rm[1, 1, 0, 0] == 0 + assert Rm[1, 1, 0, 1] == 0 + assert Rm[1, 1, 1, 0] == 0 + assert Rm[1, 1, 1, 1] == 0 + + Ric = metric_to_Ricci_components(g) + assert Ric[0, 0] == -y**(-2) + assert Ric[0, 1] == 0 + assert Ric[1, 0] == 0 + assert Ric[0, 0] == -y**(-2) + + assert Ric == ImmutableDenseNDimArray([-y**(-2), 0, 0, -y**(-2)], (2, 2)) + + ## scalar curvature is -2 + #TODO - it would be nice to have index contraction built-in + R = (Ric[0, 0] + Ric[1, 1])*y**2 + assert R == -2 + + ## Gauss curvature is -1 + assert R/2 == -1 diff --git a/venv/lib/python3.11/site-packages/sympy/discrete/__init__.py b/venv/lib/python3.11/site-packages/sympy/discrete/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..968c4caa0d4562b71285f414bfb70f43d0b35111 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/discrete/__init__.py @@ -0,0 +1,20 @@ +"""This module contains functions which operate on discrete sequences. + +Transforms - ``fft``, ``ifft``, ``ntt``, ``intt``, ``fwht``, ``ifwht``, + ``mobius_transform``, ``inverse_mobius_transform`` + +Convolutions - ``convolution``, ``convolution_fft``, ``convolution_ntt``, + ``convolution_fwht``, ``convolution_subset``, + ``covering_product``, ``intersecting_product`` +""" + +from .transforms import (fft, ifft, ntt, intt, fwht, ifwht, + mobius_transform, inverse_mobius_transform) +from .convolutions import convolution, covering_product, intersecting_product + +__all__ = [ + 'fft', 'ifft', 'ntt', 'intt', 'fwht', 'ifwht', 'mobius_transform', + 'inverse_mobius_transform', + + 'convolution', 'covering_product', 'intersecting_product', +] diff --git a/venv/lib/python3.11/site-packages/sympy/discrete/convolutions.py b/venv/lib/python3.11/site-packages/sympy/discrete/convolutions.py new file mode 100644 index 0000000000000000000000000000000000000000..ac9a3dbbb26b8b117ea1ee99cf7ebabbd21322cc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/discrete/convolutions.py @@ -0,0 +1,597 @@ +""" +Convolution (using **FFT**, **NTT**, **FWHT**), Subset Convolution, +Covering Product, Intersecting Product +""" + +from sympy.core import S, sympify, Rational +from sympy.core.function import expand_mul +from sympy.discrete.transforms import ( + fft, ifft, ntt, intt, fwht, ifwht, + mobius_transform, inverse_mobius_transform) +from sympy.external.gmpy import MPZ, lcm +from sympy.utilities.iterables import iterable +from sympy.utilities.misc import as_int + + +def convolution(a, b, cycle=0, dps=None, prime=None, dyadic=None, subset=None): + """ + Performs convolution by determining the type of desired + convolution using hints. + + Exactly one of ``dps``, ``prime``, ``dyadic``, ``subset`` arguments + should be specified explicitly for identifying the type of convolution, + and the argument ``cycle`` can be specified optionally. + + For the default arguments, linear convolution is performed using **FFT**. + + Parameters + ========== + + a, b : iterables + The sequences for which convolution is performed. + cycle : Integer + Specifies the length for doing cyclic convolution. + dps : Integer + Specifies the number of decimal digits for precision for + performing **FFT** on the sequence. + prime : Integer + Prime modulus of the form `(m 2^k + 1)` to be used for + performing **NTT** on the sequence. + dyadic : bool + Identifies the convolution type as dyadic (*bitwise-XOR*) + convolution, which is performed using **FWHT**. + subset : bool + Identifies the convolution type as subset convolution. + + Examples + ======== + + >>> from sympy import convolution, symbols, S, I + >>> u, v, w, x, y, z = symbols('u v w x y z') + + >>> convolution([1 + 2*I, 4 + 3*I], [S(5)/4, 6], dps=3) + [1.25 + 2.5*I, 11.0 + 15.8*I, 24.0 + 18.0*I] + >>> convolution([1, 2, 3], [4, 5, 6], cycle=3) + [31, 31, 28] + + >>> convolution([111, 777], [888, 444], prime=19*2**10 + 1) + [1283, 19351, 14219] + >>> convolution([111, 777], [888, 444], prime=19*2**10 + 1, cycle=2) + [15502, 19351] + + >>> convolution([u, v], [x, y, z], dyadic=True) + [u*x + v*y, u*y + v*x, u*z, v*z] + >>> convolution([u, v], [x, y, z], dyadic=True, cycle=2) + [u*x + u*z + v*y, u*y + v*x + v*z] + + >>> convolution([u, v, w], [x, y, z], subset=True) + [u*x, u*y + v*x, u*z + w*x, v*z + w*y] + >>> convolution([u, v, w], [x, y, z], subset=True, cycle=3) + [u*x + v*z + w*y, u*y + v*x, u*z + w*x] + + """ + + c = as_int(cycle) + if c < 0: + raise ValueError("The length for cyclic convolution " + "must be non-negative") + + dyadic = True if dyadic else None + subset = True if subset else None + if sum(x is not None for x in (prime, dps, dyadic, subset)) > 1: + raise TypeError("Ambiguity in determining the type of convolution") + + if prime is not None: + ls = convolution_ntt(a, b, prime=prime) + return ls if not c else [sum(ls[i::c]) % prime for i in range(c)] + + if dyadic: + ls = convolution_fwht(a, b) + elif subset: + ls = convolution_subset(a, b) + else: + def loop(a): + dens = [] + for i in a: + if isinstance(i, Rational) and i.q - 1: + dens.append(i.q) + elif not isinstance(i, int): + return + if dens: + l = lcm(*dens) + return [i*l if type(i) is int else i.p*(l//i.q) for i in a], l + # no lcm of den to deal with + return a, 1 + ls = None + da = loop(a) + if da is not None: + db = loop(b) + if db is not None: + (ia, ma), (ib, mb) = da, db + den = ma*mb + ls = convolution_int(ia, ib) + if den != 1: + ls = [Rational(i, den) for i in ls] + if ls is None: + ls = convolution_fft(a, b, dps) + + return ls if not c else [sum(ls[i::c]) for i in range(c)] + + +#----------------------------------------------------------------------------# +# # +# Convolution for Complex domain # +# # +#----------------------------------------------------------------------------# + +def convolution_fft(a, b, dps=None): + """ + Performs linear convolution using Fast Fourier Transform. + + Parameters + ========== + + a, b : iterables + The sequences for which convolution is performed. + dps : Integer + Specifies the number of decimal digits for precision. + + Examples + ======== + + >>> from sympy import S, I + >>> from sympy.discrete.convolutions import convolution_fft + + >>> convolution_fft([2, 3], [4, 5]) + [8, 22, 15] + >>> convolution_fft([2, 5], [6, 7, 3]) + [12, 44, 41, 15] + >>> convolution_fft([1 + 2*I, 4 + 3*I], [S(5)/4, 6]) + [5/4 + 5*I/2, 11 + 63*I/4, 24 + 18*I] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Convolution_theorem + .. [2] https://en.wikipedia.org/wiki/Discrete_Fourier_transform_(general%29 + + """ + + a, b = a[:], b[:] + n = m = len(a) + len(b) - 1 # convolution size + + if n > 0 and n&(n - 1): # not a power of 2 + n = 2**n.bit_length() + + # padding with zeros + a += [S.Zero]*(n - len(a)) + b += [S.Zero]*(n - len(b)) + + a, b = fft(a, dps), fft(b, dps) + a = [expand_mul(x*y) for x, y in zip(a, b)] + a = ifft(a, dps)[:m] + + return a + + +#----------------------------------------------------------------------------# +# # +# Convolution for GF(p) # +# # +#----------------------------------------------------------------------------# + +def convolution_ntt(a, b, prime): + """ + Performs linear convolution using Number Theoretic Transform. + + Parameters + ========== + + a, b : iterables + The sequences for which convolution is performed. + prime : Integer + Prime modulus of the form `(m 2^k + 1)` to be used for performing + **NTT** on the sequence. + + Examples + ======== + + >>> from sympy.discrete.convolutions import convolution_ntt + >>> convolution_ntt([2, 3], [4, 5], prime=19*2**10 + 1) + [8, 22, 15] + >>> convolution_ntt([2, 5], [6, 7, 3], prime=19*2**10 + 1) + [12, 44, 41, 15] + >>> convolution_ntt([333, 555], [222, 666], prime=19*2**10 + 1) + [15555, 14219, 19404] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Convolution_theorem + .. [2] https://en.wikipedia.org/wiki/Discrete_Fourier_transform_(general%29 + + """ + + a, b, p = a[:], b[:], as_int(prime) + n = m = len(a) + len(b) - 1 # convolution size + + if n > 0 and n&(n - 1): # not a power of 2 + n = 2**n.bit_length() + + # padding with zeros + a += [0]*(n - len(a)) + b += [0]*(n - len(b)) + + a, b = ntt(a, p), ntt(b, p) + a = [x*y % p for x, y in zip(a, b)] + a = intt(a, p)[:m] + + return a + + +#----------------------------------------------------------------------------# +# # +# Convolution for 2**n-group # +# # +#----------------------------------------------------------------------------# + +def convolution_fwht(a, b): + """ + Performs dyadic (*bitwise-XOR*) convolution using Fast Walsh Hadamard + Transform. + + The convolution is automatically padded to the right with zeros, as the + *radix-2 FWHT* requires the number of sample points to be a power of 2. + + Parameters + ========== + + a, b : iterables + The sequences for which convolution is performed. + + Examples + ======== + + >>> from sympy import symbols, S, I + >>> from sympy.discrete.convolutions import convolution_fwht + + >>> u, v, x, y = symbols('u v x y') + >>> convolution_fwht([u, v], [x, y]) + [u*x + v*y, u*y + v*x] + + >>> convolution_fwht([2, 3], [4, 5]) + [23, 22] + >>> convolution_fwht([2, 5 + 4*I, 7], [6*I, 7, 3 + 4*I]) + [56 + 68*I, -10 + 30*I, 6 + 50*I, 48 + 32*I] + + >>> convolution_fwht([S(33)/7, S(55)/6, S(7)/4], [S(2)/3, 5]) + [2057/42, 1870/63, 7/6, 35/4] + + References + ========== + + .. [1] https://www.radioeng.cz/fulltexts/2002/02_03_40_42.pdf + .. [2] https://en.wikipedia.org/wiki/Hadamard_transform + + """ + + if not a or not b: + return [] + + a, b = a[:], b[:] + n = max(len(a), len(b)) + + if n&(n - 1): # not a power of 2 + n = 2**n.bit_length() + + # padding with zeros + a += [S.Zero]*(n - len(a)) + b += [S.Zero]*(n - len(b)) + + a, b = fwht(a), fwht(b) + a = [expand_mul(x*y) for x, y in zip(a, b)] + a = ifwht(a) + + return a + + +#----------------------------------------------------------------------------# +# # +# Subset Convolution # +# # +#----------------------------------------------------------------------------# + +def convolution_subset(a, b): + """ + Performs Subset Convolution of given sequences. + + The indices of each argument, considered as bit strings, correspond to + subsets of a finite set. + + The sequence is automatically padded to the right with zeros, as the + definition of subset based on bitmasks (indices) requires the size of + sequence to be a power of 2. + + Parameters + ========== + + a, b : iterables + The sequences for which convolution is performed. + + Examples + ======== + + >>> from sympy import symbols, S + >>> from sympy.discrete.convolutions import convolution_subset + >>> u, v, x, y, z = symbols('u v x y z') + + >>> convolution_subset([u, v], [x, y]) + [u*x, u*y + v*x] + >>> convolution_subset([u, v, x], [y, z]) + [u*y, u*z + v*y, x*y, x*z] + + >>> convolution_subset([1, S(2)/3], [3, 4]) + [3, 6] + >>> convolution_subset([1, 3, S(5)/7], [7]) + [7, 21, 5, 0] + + References + ========== + + .. [1] https://people.csail.mit.edu/rrw/presentations/subset-conv.pdf + + """ + + if not a or not b: + return [] + + if not iterable(a) or not iterable(b): + raise TypeError("Expected a sequence of coefficients for convolution") + + a = [sympify(arg) for arg in a] + b = [sympify(arg) for arg in b] + n = max(len(a), len(b)) + + if n&(n - 1): # not a power of 2 + n = 2**n.bit_length() + + # padding with zeros + a += [S.Zero]*(n - len(a)) + b += [S.Zero]*(n - len(b)) + + c = [S.Zero]*n + + for mask in range(n): + smask = mask + while smask > 0: + c[mask] += expand_mul(a[smask] * b[mask^smask]) + smask = (smask - 1)&mask + + c[mask] += expand_mul(a[smask] * b[mask^smask]) + + return c + + +#----------------------------------------------------------------------------# +# # +# Covering Product # +# # +#----------------------------------------------------------------------------# + +def covering_product(a, b): + """ + Returns the covering product of given sequences. + + The indices of each argument, considered as bit strings, correspond to + subsets of a finite set. + + The covering product of given sequences is a sequence which contains + the sum of products of the elements of the given sequences grouped by + the *bitwise-OR* of the corresponding indices. + + The sequence is automatically padded to the right with zeros, as the + definition of subset based on bitmasks (indices) requires the size of + sequence to be a power of 2. + + Parameters + ========== + + a, b : iterables + The sequences for which covering product is to be obtained. + + Examples + ======== + + >>> from sympy import symbols, S, I, covering_product + >>> u, v, x, y, z = symbols('u v x y z') + + >>> covering_product([u, v], [x, y]) + [u*x, u*y + v*x + v*y] + >>> covering_product([u, v, x], [y, z]) + [u*y, u*z + v*y + v*z, x*y, x*z] + + >>> covering_product([1, S(2)/3], [3, 4 + 5*I]) + [3, 26/3 + 25*I/3] + >>> covering_product([1, 3, S(5)/7], [7, 8]) + [7, 53, 5, 40/7] + + References + ========== + + .. [1] https://people.csail.mit.edu/rrw/presentations/subset-conv.pdf + + """ + + if not a or not b: + return [] + + a, b = a[:], b[:] + n = max(len(a), len(b)) + + if n&(n - 1): # not a power of 2 + n = 2**n.bit_length() + + # padding with zeros + a += [S.Zero]*(n - len(a)) + b += [S.Zero]*(n - len(b)) + + a, b = mobius_transform(a), mobius_transform(b) + a = [expand_mul(x*y) for x, y in zip(a, b)] + a = inverse_mobius_transform(a) + + return a + + +#----------------------------------------------------------------------------# +# # +# Intersecting Product # +# # +#----------------------------------------------------------------------------# + +def intersecting_product(a, b): + """ + Returns the intersecting product of given sequences. + + The indices of each argument, considered as bit strings, correspond to + subsets of a finite set. + + The intersecting product of given sequences is the sequence which + contains the sum of products of the elements of the given sequences + grouped by the *bitwise-AND* of the corresponding indices. + + The sequence is automatically padded to the right with zeros, as the + definition of subset based on bitmasks (indices) requires the size of + sequence to be a power of 2. + + Parameters + ========== + + a, b : iterables + The sequences for which intersecting product is to be obtained. + + Examples + ======== + + >>> from sympy import symbols, S, I, intersecting_product + >>> u, v, x, y, z = symbols('u v x y z') + + >>> intersecting_product([u, v], [x, y]) + [u*x + u*y + v*x, v*y] + >>> intersecting_product([u, v, x], [y, z]) + [u*y + u*z + v*y + x*y + x*z, v*z, 0, 0] + + >>> intersecting_product([1, S(2)/3], [3, 4 + 5*I]) + [9 + 5*I, 8/3 + 10*I/3] + >>> intersecting_product([1, 3, S(5)/7], [7, 8]) + [327/7, 24, 0, 0] + + References + ========== + + .. [1] https://people.csail.mit.edu/rrw/presentations/subset-conv.pdf + + """ + + if not a or not b: + return [] + + a, b = a[:], b[:] + n = max(len(a), len(b)) + + if n&(n - 1): # not a power of 2 + n = 2**n.bit_length() + + # padding with zeros + a += [S.Zero]*(n - len(a)) + b += [S.Zero]*(n - len(b)) + + a, b = mobius_transform(a, subset=False), mobius_transform(b, subset=False) + a = [expand_mul(x*y) for x, y in zip(a, b)] + a = inverse_mobius_transform(a, subset=False) + + return a + + +#----------------------------------------------------------------------------# +# # +# Integer Convolutions # +# # +#----------------------------------------------------------------------------# + +def convolution_int(a, b): + """Return the convolution of two sequences as a list. + + The iterables must consist solely of integers. + + Parameters + ========== + + a, b : Sequence + The sequences for which convolution is performed. + + Explanation + =========== + + This function performs the convolution of ``a`` and ``b`` by packing + each into a single integer, multiplying them together, and then + unpacking the result from the product. The intuition behind this is + that if we evaluate some polynomial [1]: + + .. math :: + 1156x^6 + 3808x^5 + 8440x^4 + 14856x^3 + 16164x^2 + 14040x + 8100 + + at say $x = 10^5$ we obtain $1156038080844014856161641404008100$. + Note we can read of the coefficients for each term every five digits. + If the $x$ we chose to evaluate at is large enough, the same will hold + for the product. + + The idea now is since big integer multiplication in libraries such + as GMP is highly optimised, this will be reasonably fast. + + Examples + ======== + + >>> from sympy.discrete.convolutions import convolution_int + + >>> convolution_int([2, 3], [4, 5]) + [8, 22, 15] + >>> convolution_int([1, 1, -1], [1, 1]) + [1, 2, 0, -1] + + References + ========== + + .. [1] Fateman, Richard J. + Can you save time in multiplying polynomials by encoding them as integers? + University of California, Berkeley, California (2004). + https://people.eecs.berkeley.edu/~fateman/papers/polysbyGMP.pdf + """ + # An upper bound on the largest coefficient in p(x)q(x) is given by (1 + min(dp, dq))N(p)N(q) + # where dp = deg(p), dq = deg(q), N(f) denotes the coefficient of largest modulus in f [1] + B = max(abs(c) for c in a)*max(abs(c) for c in b)*(1 + min(len(a) - 1, len(b) - 1)) + x, power = MPZ(1), 0 + while x <= (2*B): # multiply by two for negative coefficients, see [1] + x <<= 1 + power += 1 + + def to_integer(poly): + n, mul = MPZ(0), 0 + for c in reversed(poly): + if c and not mul: mul = -1 if c < 0 else 1 + n <<= power + n += mul*int(c) + return mul, n + + # Perform packing and multiplication + (a_mul, a_packed), (b_mul, b_packed) = to_integer(a), to_integer(b) + result = a_packed * b_packed + + # Perform unpacking + mul = a_mul * b_mul + mask, half, borrow, poly = x - 1, x >> 1, 0, [] + while result or borrow: + coeff = (result & mask) + borrow + result >>= power + borrow = coeff >= half + poly.append(mul * int(coeff if coeff < half else coeff - x)) + return poly or [0] diff --git a/venv/lib/python3.11/site-packages/sympy/discrete/recurrences.py b/venv/lib/python3.11/site-packages/sympy/discrete/recurrences.py new file mode 100644 index 0000000000000000000000000000000000000000..0b0ed80d304161cf9ca298321aedc094c8cae1b3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/discrete/recurrences.py @@ -0,0 +1,166 @@ +""" +Recurrences +""" + +from sympy.core import S, sympify +from sympy.utilities.iterables import iterable +from sympy.utilities.misc import as_int + + +def linrec(coeffs, init, n): + r""" + Evaluation of univariate linear recurrences of homogeneous type + having coefficients independent of the recurrence variable. + + Parameters + ========== + + coeffs : iterable + Coefficients of the recurrence + init : iterable + Initial values of the recurrence + n : Integer + Point of evaluation for the recurrence + + Notes + ===== + + Let `y(n)` be the recurrence of given type, ``c`` be the sequence + of coefficients, ``b`` be the sequence of initial/base values of the + recurrence and ``k`` (equal to ``len(c)``) be the order of recurrence. + Then, + + .. math :: y(n) = \begin{cases} b_n & 0 \le n < k \\ + c_0 y(n-1) + c_1 y(n-2) + \cdots + c_{k-1} y(n-k) & n \ge k + \end{cases} + + Let `x_0, x_1, \ldots, x_n` be a sequence and consider the transformation + that maps each polynomial `f(x)` to `T(f(x))` where each power `x^i` is + replaced by the corresponding value `x_i`. The sequence is then a solution + of the recurrence if and only if `T(x^i p(x)) = 0` for each `i \ge 0` where + `p(x) = x^k - c_0 x^(k-1) - \cdots - c_{k-1}` is the characteristic + polynomial. + + Then `T(f(x)p(x)) = 0` for each polynomial `f(x)` (as it is a linear + combination of powers `x^i`). Now, if `x^n` is congruent to + `g(x) = a_0 x^0 + a_1 x^1 + \cdots + a_{k-1} x^{k-1}` modulo `p(x)`, then + `T(x^n) = x_n` is equal to + `T(g(x)) = a_0 x_0 + a_1 x_1 + \cdots + a_{k-1} x_{k-1}`. + + Computation of `x^n`, + given `x^k = c_0 x^{k-1} + c_1 x^{k-2} + \cdots + c_{k-1}` + is performed using exponentiation by squaring (refer to [1_]) with + an additional reduction step performed to retain only first `k` powers + of `x` in the representation of `x^n`. + + Examples + ======== + + >>> from sympy.discrete.recurrences import linrec + >>> from sympy.abc import x, y, z + + >>> linrec(coeffs=[1, 1], init=[0, 1], n=10) + 55 + + >>> linrec(coeffs=[1, 1], init=[x, y], n=10) + 34*x + 55*y + + >>> linrec(coeffs=[x, y], init=[0, 1], n=5) + x**2*y + x*(x**3 + 2*x*y) + y**2 + + >>> linrec(coeffs=[1, 2, 3, 0, 0, 4], init=[x, y, z], n=16) + 13576*x + 5676*y + 2356*z + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Exponentiation_by_squaring + .. [2] https://en.wikipedia.org/w/index.php?title=Modular_exponentiation§ion=6#Matrices + + See Also + ======== + + sympy.polys.agca.extensions.ExtensionElement.__pow__ + + """ + + if not coeffs: + return S.Zero + + if not iterable(coeffs): + raise TypeError("Expected a sequence of coefficients for" + " the recurrence") + + if not iterable(init): + raise TypeError("Expected a sequence of values for the initialization" + " of the recurrence") + + n = as_int(n) + if n < 0: + raise ValueError("Point of evaluation of recurrence must be a " + "non-negative integer") + + c = [sympify(arg) for arg in coeffs] + b = [sympify(arg) for arg in init] + k = len(c) + + if len(b) > k: + raise TypeError("Count of initial values should not exceed the " + "order of the recurrence") + else: + b += [S.Zero]*(k - len(b)) # remaining initial values default to zero + + if n < k: + return b[n] + terms = [u*v for u, v in zip(linrec_coeffs(c, n), b)] + return sum(terms[:-1], terms[-1]) + + +def linrec_coeffs(c, n): + r""" + Compute the coefficients of n'th term in linear recursion + sequence defined by c. + + `x^k = c_0 x^{k-1} + c_1 x^{k-2} + \cdots + c_{k-1}`. + + It computes the coefficients by using binary exponentiation. + This function is used by `linrec` and `_eval_pow_by_cayley`. + + Parameters + ========== + + c = coefficients of the divisor polynomial + n = exponent of x, so dividend is x^n + + """ + + k = len(c) + + def _square_and_reduce(u, offset): + # squares `(u_0 + u_1 x + u_2 x^2 + \cdots + u_{k-1} x^k)` (and + # multiplies by `x` if offset is 1) and reduces the above result of + # length upto `2k` to `k` using the characteristic equation of the + # recurrence given by, `x^k = c_0 x^{k-1} + c_1 x^{k-2} + \cdots + c_{k-1}` + + w = [S.Zero]*(2*len(u) - 1 + offset) + for i, p in enumerate(u): + for j, q in enumerate(u): + w[offset + i + j] += p*q + + for j in range(len(w) - 1, k - 1, -1): + for i in range(k): + w[j - i - 1] += w[j]*c[i] + + return w[:k] + + def _final_coeffs(n): + # computes the final coefficient list - `cf` corresponding to the + # point at which recurrence is to be evalauted - `n`, such that, + # `y(n) = cf_0 y(k-1) + cf_1 y(k-2) + \cdots + cf_{k-1} y(0)` + + if n < k: + return [S.Zero]*n + [S.One] + [S.Zero]*(k - n - 1) + else: + return _square_and_reduce(_final_coeffs(n // 2), n % 2) + + return _final_coeffs(n) diff --git a/venv/lib/python3.11/site-packages/sympy/discrete/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/discrete/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/discrete/tests/test_convolutions.py b/venv/lib/python3.11/site-packages/sympy/discrete/tests/test_convolutions.py new file mode 100644 index 0000000000000000000000000000000000000000..96e5fc801ac63f95c01eb18d48143ae3a1ac6222 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/discrete/tests/test_convolutions.py @@ -0,0 +1,392 @@ +from sympy.core.numbers import (E, Rational, pi) +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.core import S, symbols, I +from sympy.discrete.convolutions import ( + convolution, convolution_fft, convolution_ntt, convolution_fwht, + convolution_subset, covering_product, intersecting_product, + convolution_int) +from sympy.testing.pytest import raises +from sympy.abc import x, y + +def test_convolution(): + # fft + a = [1, Rational(5, 3), sqrt(3), Rational(7, 5)] + b = [9, 5, 5, 4, 3, 2] + c = [3, 5, 3, 7, 8] + d = [1422, 6572, 3213, 5552] + e = [-1, Rational(5, 3), Rational(7, 5)] + + assert convolution(a, b) == convolution_fft(a, b) + assert convolution(a, b, dps=9) == convolution_fft(a, b, dps=9) + assert convolution(a, d, dps=7) == convolution_fft(d, a, dps=7) + assert convolution(a, d[1:], dps=3) == convolution_fft(d[1:], a, dps=3) + + # prime moduli of the form (m*2**k + 1), sequence length + # should be a divisor of 2**k + p = 7*17*2**23 + 1 + q = 19*2**10 + 1 + + # ntt + assert convolution(d, b, prime=q) == convolution_ntt(b, d, prime=q) + assert convolution(c, b, prime=p) == convolution_ntt(b, c, prime=p) + assert convolution(d, c, prime=p) == convolution_ntt(c, d, prime=p) + raises(TypeError, lambda: convolution(b, d, dps=5, prime=q)) + raises(TypeError, lambda: convolution(b, d, dps=6, prime=q)) + + # fwht + assert convolution(a, b, dyadic=True) == convolution_fwht(a, b) + assert convolution(a, b, dyadic=False) == convolution(a, b) + raises(TypeError, lambda: convolution(b, d, dps=2, dyadic=True)) + raises(TypeError, lambda: convolution(b, d, prime=p, dyadic=True)) + raises(TypeError, lambda: convolution(a, b, dps=2, dyadic=True)) + raises(TypeError, lambda: convolution(b, c, prime=p, dyadic=True)) + + # subset + assert convolution(a, b, subset=True) == convolution_subset(a, b) == \ + convolution(a, b, subset=True, dyadic=False) == \ + convolution(a, b, subset=True) + assert convolution(a, b, subset=False) == convolution(a, b) + raises(TypeError, lambda: convolution(a, b, subset=True, dyadic=True)) + raises(TypeError, lambda: convolution(c, d, subset=True, dps=6)) + raises(TypeError, lambda: convolution(a, c, subset=True, prime=q)) + + # integer + assert convolution([0], [0]) == convolution_int([0], [0]) + assert convolution(b, c) == convolution_int(b, c) + + # rational + assert convolution([Rational(1,2)], [Rational(1,2)]) == [Rational(1, 4)] + assert convolution(b, e) == [-9, 10, Rational(239, 15), Rational(34, 3), + Rational(32, 3), Rational(43, 5), Rational(113, 15), + Rational(14, 5)] + + +def test_cyclic_convolution(): + # fft + a = [1, Rational(5, 3), sqrt(3), Rational(7, 5)] + b = [9, 5, 5, 4, 3, 2] + + assert convolution([1, 2, 3], [4, 5, 6], cycle=0) == \ + convolution([1, 2, 3], [4, 5, 6], cycle=5) == \ + convolution([1, 2, 3], [4, 5, 6]) + + assert convolution([1, 2, 3], [4, 5, 6], cycle=3) == [31, 31, 28] + + a = [Rational(1, 3), Rational(7, 3), Rational(5, 9), Rational(2, 7), Rational(5, 8)] + b = [Rational(3, 5), Rational(4, 7), Rational(7, 8), Rational(8, 9)] + + assert convolution(a, b, cycle=0) == \ + convolution(a, b, cycle=len(a) + len(b) - 1) + + assert convolution(a, b, cycle=4) == [Rational(87277, 26460), Rational(30521, 11340), + Rational(11125, 4032), Rational(3653, 1080)] + + assert convolution(a, b, cycle=6) == [Rational(20177, 20160), Rational(676, 315), Rational(47, 24), + Rational(3053, 1080), Rational(16397, 5292), Rational(2497, 2268)] + + assert convolution(a, b, cycle=9) == \ + convolution(a, b, cycle=0) + [S.Zero] + + # ntt + a = [2313, 5323532, S(3232), 42142, 42242421] + b = [S(33456), 56757, 45754, 432423] + + assert convolution(a, b, prime=19*2**10 + 1, cycle=0) == \ + convolution(a, b, prime=19*2**10 + 1, cycle=8) == \ + convolution(a, b, prime=19*2**10 + 1) + + assert convolution(a, b, prime=19*2**10 + 1, cycle=5) == [96, 17146, 2664, + 15534, 3517] + + assert convolution(a, b, prime=19*2**10 + 1, cycle=7) == [4643, 3458, 1260, + 15534, 3517, 16314, 13688] + + assert convolution(a, b, prime=19*2**10 + 1, cycle=9) == \ + convolution(a, b, prime=19*2**10 + 1) + [0] + + # fwht + u, v, w, x, y = symbols('u v w x y') + p, q, r, s, t = symbols('p q r s t') + c = [u, v, w, x, y] + d = [p, q, r, s, t] + + assert convolution(a, b, dyadic=True, cycle=3) == \ + [2499522285783, 19861417974796, 4702176579021] + + assert convolution(a, b, dyadic=True, cycle=5) == [2718149225143, + 2114320852171, 20571217906407, 246166418903, 1413262436976] + + assert convolution(c, d, dyadic=True, cycle=4) == \ + [p*u + p*y + q*v + r*w + s*x + t*u + t*y, + p*v + q*u + q*y + r*x + s*w + t*v, + p*w + q*x + r*u + r*y + s*v + t*w, + p*x + q*w + r*v + s*u + s*y + t*x] + + assert convolution(c, d, dyadic=True, cycle=6) == \ + [p*u + q*v + r*w + r*y + s*x + t*w + t*y, + p*v + q*u + r*x + s*w + s*y + t*x, + p*w + q*x + r*u + s*v, + p*x + q*w + r*v + s*u, + p*y + t*u, + q*y + t*v] + + # subset + assert convolution(a, b, subset=True, cycle=7) == [18266671799811, + 178235365533, 213958794, 246166418903, 1413262436976, + 2397553088697, 1932759730434] + + assert convolution(a[1:], b, subset=True, cycle=4) == \ + [178104086592, 302255835516, 244982785880, 3717819845434] + + assert convolution(a, b[:-1], subset=True, cycle=6) == [1932837114162, + 178235365533, 213958794, 245166224504, 1413262436976, 2397553088697] + + assert convolution(c, d, subset=True, cycle=3) == \ + [p*u + p*x + q*w + r*v + r*y + s*u + t*w, + p*v + p*y + q*u + s*y + t*u + t*x, + p*w + q*y + r*u + t*v] + + assert convolution(c, d, subset=True, cycle=5) == \ + [p*u + q*y + t*v, + p*v + q*u + r*y + t*w, + p*w + r*u + s*y + t*x, + p*x + q*w + r*v + s*u, + p*y + t*u] + + raises(ValueError, lambda: convolution([1, 2, 3], [4, 5, 6], cycle=-1)) + + +def test_convolution_fft(): + assert all(convolution_fft([], x, dps=y) == [] for x in ([], [1]) for y in (None, 3)) + assert convolution_fft([1, 2, 3], [4, 5, 6]) == [4, 13, 28, 27, 18] + assert convolution_fft([1], [5, 6, 7]) == [5, 6, 7] + assert convolution_fft([1, 3], [5, 6, 7]) == [5, 21, 25, 21] + + assert convolution_fft([1 + 2*I], [2 + 3*I]) == [-4 + 7*I] + assert convolution_fft([1 + 2*I, 3 + 4*I, 5 + 3*I/5], [Rational(2, 5) + 4*I/7]) == \ + [Rational(-26, 35) + I*48/35, Rational(-38, 35) + I*116/35, Rational(58, 35) + I*542/175] + + assert convolution_fft([Rational(3, 4), Rational(5, 6)], [Rational(7, 8), Rational(1, 3), Rational(2, 5)]) == \ + [Rational(21, 32), Rational(47, 48), Rational(26, 45), Rational(1, 3)] + + assert convolution_fft([Rational(1, 9), Rational(2, 3), Rational(3, 5)], [Rational(2, 5), Rational(3, 7), Rational(4, 9)]) == \ + [Rational(2, 45), Rational(11, 35), Rational(8152, 14175), Rational(523, 945), Rational(4, 15)] + + assert convolution_fft([pi, E, sqrt(2)], [sqrt(3), 1/pi, 1/E]) == \ + [sqrt(3)*pi, 1 + sqrt(3)*E, E/pi + pi*exp(-1) + sqrt(6), + sqrt(2)/pi + 1, sqrt(2)*exp(-1)] + + assert convolution_fft([2321, 33123], [5321, 6321, 71323]) == \ + [12350041, 190918524, 374911166, 2362431729] + + assert convolution_fft([312313, 31278232], [32139631, 319631]) == \ + [10037624576503, 1005370659728895, 9997492572392] + + raises(TypeError, lambda: convolution_fft(x, y)) + raises(ValueError, lambda: convolution_fft([x, y], [y, x])) + + +def test_convolution_ntt(): + # prime moduli of the form (m*2**k + 1), sequence length + # should be a divisor of 2**k + p = 7*17*2**23 + 1 + q = 19*2**10 + 1 + r = 2*500000003 + 1 # only for sequences of length 1 or 2 + # s = 2*3*5*7 # composite modulus + + assert all(convolution_ntt([], x, prime=y) == [] for x in ([], [1]) for y in (p, q, r)) + assert convolution_ntt([2], [3], r) == [6] + assert convolution_ntt([2, 3], [4], r) == [8, 12] + + assert convolution_ntt([32121, 42144, 4214, 4241], [32132, 3232, 87242], p) == [33867619, + 459741727, 79180879, 831885249, 381344700, 369993322] + assert convolution_ntt([121913, 3171831, 31888131, 12], [17882, 21292, 29921, 312], q) == \ + [8158, 3065, 3682, 7090, 1239, 2232, 3744] + + assert convolution_ntt([12, 19, 21, 98, 67], [2, 6, 7, 8, 9], p) == \ + convolution_ntt([12, 19, 21, 98, 67], [2, 6, 7, 8, 9], q) + assert convolution_ntt([12, 19, 21, 98, 67], [21, 76, 17, 78, 69], p) == \ + convolution_ntt([12, 19, 21, 98, 67], [21, 76, 17, 78, 69], q) + + raises(ValueError, lambda: convolution_ntt([2, 3], [4, 5], r)) + raises(ValueError, lambda: convolution_ntt([x, y], [y, x], q)) + raises(TypeError, lambda: convolution_ntt(x, y, p)) + + +def test_convolution_fwht(): + assert convolution_fwht([], []) == [] + assert convolution_fwht([], [1]) == [] + assert convolution_fwht([1, 2, 3], [4, 5, 6]) == [32, 13, 18, 27] + + assert convolution_fwht([Rational(5, 7), Rational(6, 8), Rational(7, 3)], [2, 4, Rational(6, 7)]) == \ + [Rational(45, 7), Rational(61, 14), Rational(776, 147), Rational(419, 42)] + + a = [1, Rational(5, 3), sqrt(3), Rational(7, 5), 4 + 5*I] + b = [94, 51, 53, 45, 31, 27, 13] + c = [3 + 4*I, 5 + 7*I, 3, Rational(7, 6), 8] + + assert convolution_fwht(a, b) == [53*sqrt(3) + 366 + 155*I, + 45*sqrt(3) + Rational(5848, 15) + 135*I, + 94*sqrt(3) + Rational(1257, 5) + 65*I, + 51*sqrt(3) + Rational(3974, 15), + 13*sqrt(3) + 452 + 470*I, + Rational(4513, 15) + 255*I, + 31*sqrt(3) + Rational(1314, 5) + 265*I, + 27*sqrt(3) + Rational(3676, 15) + 225*I] + + assert convolution_fwht(b, c) == [Rational(1993, 2) + 733*I, Rational(6215, 6) + 862*I, + Rational(1659, 2) + 527*I, Rational(1988, 3) + 551*I, 1019 + 313*I, Rational(3955, 6) + 325*I, + Rational(1175, 2) + 52*I, Rational(3253, 6) + 91*I] + + assert convolution_fwht(a[3:], c) == [Rational(-54, 5) + I*293/5, -1 + I*204/5, + Rational(133, 15) + I*35/6, Rational(409, 30) + 15*I, Rational(56, 5), 32 + 40*I, 0, 0] + + u, v, w, x, y, z = symbols('u v w x y z') + + assert convolution_fwht([u, v], [x, y]) == [u*x + v*y, u*y + v*x] + + assert convolution_fwht([u, v, w], [x, y]) == \ + [u*x + v*y, u*y + v*x, w*x, w*y] + + assert convolution_fwht([u, v, w], [x, y, z]) == \ + [u*x + v*y + w*z, u*y + v*x, u*z + w*x, v*z + w*y] + + raises(TypeError, lambda: convolution_fwht(x, y)) + raises(TypeError, lambda: convolution_fwht(x*y, u + v)) + + +def test_convolution_subset(): + assert convolution_subset([], []) == [] + assert convolution_subset([], [Rational(1, 3)]) == [] + assert convolution_subset([6 + I*3/7], [Rational(2, 3)]) == [4 + I*2/7] + + a = [1, Rational(5, 3), sqrt(3), 4 + 5*I] + b = [64, 71, 55, 47, 33, 29, 15] + c = [3 + I*2/3, 5 + 7*I, 7, Rational(7, 5), 9] + + assert convolution_subset(a, b) == [64, Rational(533, 3), 55 + 64*sqrt(3), + 71*sqrt(3) + Rational(1184, 3) + 320*I, 33, 84, + 15 + 33*sqrt(3), 29*sqrt(3) + 157 + 165*I] + + assert convolution_subset(b, c) == [192 + I*128/3, 533 + I*1486/3, + 613 + I*110/3, Rational(5013, 5) + I*1249/3, + 675 + 22*I, 891 + I*751/3, + 771 + 10*I, Rational(3736, 5) + 105*I] + + assert convolution_subset(a, c) == convolution_subset(c, a) + assert convolution_subset(a[:2], b) == \ + [64, Rational(533, 3), 55, Rational(416, 3), 33, 84, 15, 25] + + assert convolution_subset(a[:2], c) == \ + [3 + I*2/3, 10 + I*73/9, 7, Rational(196, 15), 9, 15, 0, 0] + + u, v, w, x, y, z = symbols('u v w x y z') + + assert convolution_subset([u, v, w], [x, y]) == [u*x, u*y + v*x, w*x, w*y] + assert convolution_subset([u, v, w, x], [y, z]) == \ + [u*y, u*z + v*y, w*y, w*z + x*y] + + assert convolution_subset([u, v], [x, y, z]) == \ + convolution_subset([x, y, z], [u, v]) + + raises(TypeError, lambda: convolution_subset(x, z)) + raises(TypeError, lambda: convolution_subset(Rational(7, 3), u)) + + +def test_covering_product(): + assert covering_product([], []) == [] + assert covering_product([], [Rational(1, 3)]) == [] + assert covering_product([6 + I*3/7], [Rational(2, 3)]) == [4 + I*2/7] + + a = [1, Rational(5, 8), sqrt(7), 4 + 9*I] + b = [66, 81, 95, 49, 37, 89, 17] + c = [3 + I*2/3, 51 + 72*I, 7, Rational(7, 15), 91] + + assert covering_product(a, b) == [66, Rational(1383, 8), 95 + 161*sqrt(7), + 130*sqrt(7) + 1303 + 2619*I, 37, + Rational(671, 4), 17 + 54*sqrt(7), + 89*sqrt(7) + Rational(4661, 8) + 1287*I] + + assert covering_product(b, c) == [198 + 44*I, 7740 + 10638*I, + 1412 + I*190/3, Rational(42684, 5) + I*31202/3, + 9484 + I*74/3, 22163 + I*27394/3, + 10621 + I*34/3, Rational(90236, 15) + 1224*I] + + assert covering_product(a, c) == covering_product(c, a) + assert covering_product(b, c[:-1]) == [198 + 44*I, 7740 + 10638*I, + 1412 + I*190/3, Rational(42684, 5) + I*31202/3, + 111 + I*74/3, 6693 + I*27394/3, + 429 + I*34/3, Rational(23351, 15) + 1224*I] + + assert covering_product(a, c[:-1]) == [3 + I*2/3, + Rational(339, 4) + I*1409/12, 7 + 10*sqrt(7) + 2*sqrt(7)*I/3, + -403 + 772*sqrt(7)/15 + 72*sqrt(7)*I + I*12658/15] + + u, v, w, x, y, z = symbols('u v w x y z') + + assert covering_product([u, v, w], [x, y]) == \ + [u*x, u*y + v*x + v*y, w*x, w*y] + + assert covering_product([u, v, w, x], [y, z]) == \ + [u*y, u*z + v*y + v*z, w*y, w*z + x*y + x*z] + + assert covering_product([u, v], [x, y, z]) == \ + covering_product([x, y, z], [u, v]) + + raises(TypeError, lambda: covering_product(x, z)) + raises(TypeError, lambda: covering_product(Rational(7, 3), u)) + + +def test_intersecting_product(): + assert intersecting_product([], []) == [] + assert intersecting_product([], [Rational(1, 3)]) == [] + assert intersecting_product([6 + I*3/7], [Rational(2, 3)]) == [4 + I*2/7] + + a = [1, sqrt(5), Rational(3, 8) + 5*I, 4 + 7*I] + b = [67, 51, 65, 48, 36, 79, 27] + c = [3 + I*2/5, 5 + 9*I, 7, Rational(7, 19), 13] + + assert intersecting_product(a, b) == [195*sqrt(5) + Rational(6979, 8) + 1886*I, + 178*sqrt(5) + 520 + 910*I, Rational(841, 2) + 1344*I, + 192 + 336*I, 0, 0, 0, 0] + + assert intersecting_product(b, c) == [Rational(128553, 19) + I*9521/5, + Rational(17820, 19) + 1602*I, Rational(19264, 19), Rational(336, 19), 1846, 0, 0, 0] + + assert intersecting_product(a, c) == intersecting_product(c, a) + assert intersecting_product(b[1:], c[:-1]) == [Rational(64788, 19) + I*8622/5, + Rational(12804, 19) + 1152*I, Rational(11508, 19), Rational(252, 19), 0, 0, 0, 0] + + assert intersecting_product(a, c[:-2]) == \ + [Rational(-99, 5) + 10*sqrt(5) + 2*sqrt(5)*I/5 + I*3021/40, + -43 + 5*sqrt(5) + 9*sqrt(5)*I + 71*I, Rational(245, 8) + 84*I, 0] + + u, v, w, x, y, z = symbols('u v w x y z') + + assert intersecting_product([u, v, w], [x, y]) == \ + [u*x + u*y + v*x + w*x + w*y, v*y, 0, 0] + + assert intersecting_product([u, v, w, x], [y, z]) == \ + [u*y + u*z + v*y + w*y + w*z + x*y, v*z + x*z, 0, 0] + + assert intersecting_product([u, v], [x, y, z]) == \ + intersecting_product([x, y, z], [u, v]) + + raises(TypeError, lambda: intersecting_product(x, z)) + raises(TypeError, lambda: intersecting_product(u, Rational(8, 3))) + + +def test_convolution_int(): + assert convolution_int([1], [1]) == [1] + assert convolution_int([1, 1], [0]) == [0] + assert convolution_int([1, 2, 3], [4, 5, 6]) == [4, 13, 28, 27, 18] + assert convolution_int([1], [5, 6, 7]) == [5, 6, 7] + assert convolution_int([1, 3], [5, 6, 7]) == [5, 21, 25, 21] + assert convolution_int([10, -5, 1, 3], [-5, 6, 7]) == [-50, 85, 35, -44, 25, 21] + assert convolution_int([0, 1, 0, -1], [1, 0, -1, 0]) == [0, 1, 0, -2, 0, 1] + assert convolution_int( + [-341, -5, 1, 3, -71, -99, 43, 87], + [5, 6, 7, 12, 345, 21, -78, -7, -89] + ) == [-1705, -2071, -2412, -4106, -118035, -9774, 25998, 2981, 5509, + -34317, 19228, 38870, 5485, 1724, -4436, -7743] diff --git a/venv/lib/python3.11/site-packages/sympy/discrete/tests/test_recurrences.py b/venv/lib/python3.11/site-packages/sympy/discrete/tests/test_recurrences.py new file mode 100644 index 0000000000000000000000000000000000000000..2c2186ca525b6680350a03edbe44ca88f8f95c3c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/discrete/tests/test_recurrences.py @@ -0,0 +1,59 @@ +from sympy.core.numbers import Rational +from sympy.functions.combinatorial.numbers import fibonacci +from sympy.core import S, symbols +from sympy.testing.pytest import raises +from sympy.discrete.recurrences import linrec + +def test_linrec(): + assert linrec(coeffs=[1, 1], init=[1, 1], n=20) == 10946 + assert linrec(coeffs=[1, 2, 3, 4, 5], init=[1, 1, 0, 2], n=10) == 1040 + assert linrec(coeffs=[0, 0, 11, 13], init=[23, 27], n=25) == 59628567384 + assert linrec(coeffs=[0, 0, 1, 1, 2], init=[1, 5, 3], n=15) == 165 + assert linrec(coeffs=[11, 13, 15, 17], init=[1, 2, 3, 4], n=70) == \ + 56889923441670659718376223533331214868804815612050381493741233489928913241 + assert linrec(coeffs=[0]*55 + [1, 1, 2, 3], init=[0]*50 + [1, 2, 3], n=4000) == \ + 702633573874937994980598979769135096432444135301118916539 + + assert linrec(coeffs=[11, 13, 15, 17], init=[1, 2, 3, 4], n=10**4) + assert linrec(coeffs=[11, 13, 15, 17], init=[1, 2, 3, 4], n=10**5) + + assert all(linrec(coeffs=[1, 1], init=[0, 1], n=n) == fibonacci(n) + for n in range(95, 115)) + + assert all(linrec(coeffs=[1, 1], init=[1, 1], n=n) == fibonacci(n + 1) + for n in range(595, 615)) + + a = [S.Half, Rational(3, 4), Rational(5, 6), 7, Rational(8, 9), Rational(3, 5)] + b = [1, 2, 8, Rational(5, 7), Rational(3, 7), Rational(2, 9), 6] + x, y, z = symbols('x y z') + + assert linrec(coeffs=a[:5], init=b[:4], n=80) == \ + Rational(1726244235456268979436592226626304376013002142588105090705187189, + 1960143456748895967474334873705475211264) + + assert linrec(coeffs=a[:4], init=b[:4], n=50) == \ + Rational(368949940033050147080268092104304441, 504857282956046106624) + + assert linrec(coeffs=a[3:], init=b[:3], n=35) == \ + Rational(97409272177295731943657945116791049305244422833125109, + 814315512679031689453125) + + assert linrec(coeffs=[0]*60 + [Rational(2, 3), Rational(4, 5)], init=b, n=3000) == \ + Rational(26777668739896791448594650497024, 48084516708184142230517578125) + + raises(TypeError, lambda: linrec(coeffs=[11, 13, 15, 17], init=[1, 2, 3, 4, 5], n=1)) + raises(TypeError, lambda: linrec(coeffs=a[:4], init=b[:5], n=10000)) + raises(ValueError, lambda: linrec(coeffs=a[:4], init=b[:4], n=-10000)) + raises(TypeError, lambda: linrec(x, b, n=10000)) + raises(TypeError, lambda: linrec(a, y, n=10000)) + + assert linrec(coeffs=[x, y, z], init=[1, 1, 1], n=4) == \ + x**2 + x*y + x*z + y + z + assert linrec(coeffs=[1, 2, 1], init=[x, y, z], n=20) == \ + 269542*x + 664575*y + 578949*z + assert linrec(coeffs=[0, 3, 1, 2], init=[x, y], n=30) == \ + 58516436*x + 56372788*y + assert linrec(coeffs=[0]*50 + [1, 2, 3], init=[x, y, z], n=1000) == \ + 11477135884896*x + 25999077948732*y + 41975630244216*z + assert linrec(coeffs=[], init=[1, 1], n=20) == 0 + assert linrec(coeffs=[x, y, z], init=[1, 2, 3], n=2) == 3 diff --git a/venv/lib/python3.11/site-packages/sympy/discrete/tests/test_transforms.py b/venv/lib/python3.11/site-packages/sympy/discrete/tests/test_transforms.py new file mode 100644 index 0000000000000000000000000000000000000000..385514be4cdec2f19cf3a750bdbe0f4f6e21cc6e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/discrete/tests/test_transforms.py @@ -0,0 +1,154 @@ +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.core import S, Symbol, symbols, I, Rational +from sympy.discrete import (fft, ifft, ntt, intt, fwht, ifwht, + mobius_transform, inverse_mobius_transform) +from sympy.testing.pytest import raises + + +def test_fft_ifft(): + assert all(tf(ls) == ls for tf in (fft, ifft) + for ls in ([], [Rational(5, 3)])) + + ls = list(range(6)) + fls = [15, -7*sqrt(2)/2 - 4 - sqrt(2)*I/2 + 2*I, 2 + 3*I, + -4 + 7*sqrt(2)/2 - 2*I - sqrt(2)*I/2, -3, + -4 + 7*sqrt(2)/2 + sqrt(2)*I/2 + 2*I, + 2 - 3*I, -7*sqrt(2)/2 - 4 - 2*I + sqrt(2)*I/2] + + assert fft(ls) == fls + assert ifft(fls) == ls + [S.Zero]*2 + + ls = [1 + 2*I, 3 + 4*I, 5 + 6*I] + ifls = [Rational(9, 4) + 3*I, I*Rational(-7, 4), Rational(3, 4) + I, -2 - I/4] + + assert ifft(ls) == ifls + assert fft(ifls) == ls + [S.Zero] + + x = Symbol('x', real=True) + raises(TypeError, lambda: fft(x)) + raises(ValueError, lambda: ifft([x, 2*x, 3*x**2, 4*x**3])) + + +def test_ntt_intt(): + # prime moduli of the form (m*2**k + 1), sequence length + # should be a divisor of 2**k + p = 7*17*2**23 + 1 + q = 2*500000003 + 1 # only for sequences of length 1 or 2 + r = 2*3*5*7 # composite modulus + + assert all(tf(ls, p) == ls for tf in (ntt, intt) + for ls in ([], [5])) + + ls = list(range(6)) + nls = [15, 801133602, 738493201, 334102277, 998244350, 849020224, + 259751156, 12232587] + + assert ntt(ls, p) == nls + assert intt(nls, p) == ls + [0]*2 + + ls = [1 + 2*I, 3 + 4*I, 5 + 6*I] + x = Symbol('x', integer=True) + + raises(TypeError, lambda: ntt(x, p)) + raises(ValueError, lambda: intt([x, 2*x, 3*x**2, 4*x**3], p)) + raises(ValueError, lambda: intt(ls, p)) + raises(ValueError, lambda: ntt([1.2, 2.1, 3.5], p)) + raises(ValueError, lambda: ntt([3, 5, 6], q)) + raises(ValueError, lambda: ntt([4, 5, 7], r)) + raises(ValueError, lambda: ntt([1.0, 2.0, 3.0], p)) + + +def test_fwht_ifwht(): + assert all(tf(ls) == ls for tf in (fwht, ifwht) \ + for ls in ([], [Rational(7, 4)])) + + ls = [213, 321, 43235, 5325, 312, 53] + fls = [49459, 38061, -47661, -37759, 48729, 37543, -48391, -38277] + + assert fwht(ls) == fls + assert ifwht(fls) == ls + [S.Zero]*2 + + ls = [S.Half + 2*I, Rational(3, 7) + 4*I, Rational(5, 6) + 6*I, Rational(7, 3), Rational(9, 4)] + ifls = [Rational(533, 672) + I*3/2, Rational(23, 224) + I/2, Rational(1, 672), Rational(107, 224) - I, + Rational(155, 672) + I*3/2, Rational(-103, 224) + I/2, Rational(-377, 672), Rational(-19, 224) - I] + + assert ifwht(ls) == ifls + assert fwht(ifls) == ls + [S.Zero]*3 + + x, y = symbols('x y') + + raises(TypeError, lambda: fwht(x)) + + ls = [x, 2*x, 3*x**2, 4*x**3] + ifls = [x**3 + 3*x**2/4 + x*Rational(3, 4), + -x**3 + 3*x**2/4 - x/4, + -x**3 - 3*x**2/4 + x*Rational(3, 4), + x**3 - 3*x**2/4 - x/4] + + assert ifwht(ls) == ifls + assert fwht(ifls) == ls + + ls = [x, y, x**2, y**2, x*y] + fls = [x**2 + x*y + x + y**2 + y, + x**2 + x*y + x - y**2 - y, + -x**2 + x*y + x - y**2 + y, + -x**2 + x*y + x + y**2 - y, + x**2 - x*y + x + y**2 + y, + x**2 - x*y + x - y**2 - y, + -x**2 - x*y + x - y**2 + y, + -x**2 - x*y + x + y**2 - y] + + assert fwht(ls) == fls + assert ifwht(fls) == ls + [S.Zero]*3 + + ls = list(range(6)) + + assert fwht(ls) == [x*8 for x in ifwht(ls)] + + +def test_mobius_transform(): + assert all(tf(ls, subset=subset) == ls + for ls in ([], [Rational(7, 4)]) for subset in (True, False) + for tf in (mobius_transform, inverse_mobius_transform)) + + w, x, y, z = symbols('w x y z') + + assert mobius_transform([x, y]) == [x, x + y] + assert inverse_mobius_transform([x, x + y]) == [x, y] + assert mobius_transform([x, y], subset=False) == [x + y, y] + assert inverse_mobius_transform([x + y, y], subset=False) == [x, y] + + assert mobius_transform([w, x, y, z]) == [w, w + x, w + y, w + x + y + z] + assert inverse_mobius_transform([w, w + x, w + y, w + x + y + z]) == \ + [w, x, y, z] + assert mobius_transform([w, x, y, z], subset=False) == \ + [w + x + y + z, x + z, y + z, z] + assert inverse_mobius_transform([w + x + y + z, x + z, y + z, z], subset=False) == \ + [w, x, y, z] + + ls = [Rational(2, 3), Rational(6, 7), Rational(5, 8), 9, Rational(5, 3) + 7*I] + mls = [Rational(2, 3), Rational(32, 21), Rational(31, 24), Rational(1873, 168), + Rational(7, 3) + 7*I, Rational(67, 21) + 7*I, Rational(71, 24) + 7*I, + Rational(2153, 168) + 7*I] + + assert mobius_transform(ls) == mls + assert inverse_mobius_transform(mls) == ls + [S.Zero]*3 + + mls = [Rational(2153, 168) + 7*I, Rational(69, 7), Rational(77, 8), 9, Rational(5, 3) + 7*I, 0, 0, 0] + + assert mobius_transform(ls, subset=False) == mls + assert inverse_mobius_transform(mls, subset=False) == ls + [S.Zero]*3 + + ls = ls[:-1] + mls = [Rational(2, 3), Rational(32, 21), Rational(31, 24), Rational(1873, 168)] + + assert mobius_transform(ls) == mls + assert inverse_mobius_transform(mls) == ls + + mls = [Rational(1873, 168), Rational(69, 7), Rational(77, 8), 9] + + assert mobius_transform(ls, subset=False) == mls + assert inverse_mobius_transform(mls, subset=False) == ls + + raises(TypeError, lambda: mobius_transform(x, subset=True)) + raises(TypeError, lambda: inverse_mobius_transform(y, subset=False)) diff --git a/venv/lib/python3.11/site-packages/sympy/discrete/transforms.py b/venv/lib/python3.11/site-packages/sympy/discrete/transforms.py new file mode 100644 index 0000000000000000000000000000000000000000..cb3550837021a4cf99e38c6b15f89ce8bb69b25a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/discrete/transforms.py @@ -0,0 +1,425 @@ +""" +Discrete Fourier Transform, Number Theoretic Transform, +Walsh Hadamard Transform, Mobius Transform +""" + +from sympy.core import S, Symbol, sympify +from sympy.core.function import expand_mul +from sympy.core.numbers import pi, I +from sympy.functions.elementary.trigonometric import sin, cos +from sympy.ntheory import isprime, primitive_root +from sympy.utilities.iterables import ibin, iterable +from sympy.utilities.misc import as_int + + +#----------------------------------------------------------------------------# +# # +# Discrete Fourier Transform # +# # +#----------------------------------------------------------------------------# + +def _fourier_transform(seq, dps, inverse=False): + """Utility function for the Discrete Fourier Transform""" + + if not iterable(seq): + raise TypeError("Expected a sequence of numeric coefficients " + "for Fourier Transform") + + a = [sympify(arg) for arg in seq] + if any(x.has(Symbol) for x in a): + raise ValueError("Expected non-symbolic coefficients") + + n = len(a) + if n < 2: + return a + + b = n.bit_length() - 1 + if n&(n - 1): # not a power of 2 + b += 1 + n = 2**b + + a += [S.Zero]*(n - len(a)) + for i in range(1, n): + j = int(ibin(i, b, str=True)[::-1], 2) + if i < j: + a[i], a[j] = a[j], a[i] + + ang = -2*pi/n if inverse else 2*pi/n + + if dps is not None: + ang = ang.evalf(dps + 2) + + w = [cos(ang*i) + I*sin(ang*i) for i in range(n // 2)] + + h = 2 + while h <= n: + hf, ut = h // 2, n // h + for i in range(0, n, h): + for j in range(hf): + u, v = a[i + j], expand_mul(a[i + j + hf]*w[ut * j]) + a[i + j], a[i + j + hf] = u + v, u - v + h *= 2 + + if inverse: + a = [(x/n).evalf(dps) for x in a] if dps is not None \ + else [x/n for x in a] + + return a + + +def fft(seq, dps=None): + r""" + Performs the Discrete Fourier Transform (**DFT**) in the complex domain. + + The sequence is automatically padded to the right with zeros, as the + *radix-2 FFT* requires the number of sample points to be a power of 2. + + This method should be used with default arguments only for short sequences + as the complexity of expressions increases with the size of the sequence. + + Parameters + ========== + + seq : iterable + The sequence on which **DFT** is to be applied. + dps : Integer + Specifies the number of decimal digits for precision. + + Examples + ======== + + >>> from sympy import fft, ifft + + >>> fft([1, 2, 3, 4]) + [10, -2 - 2*I, -2, -2 + 2*I] + >>> ifft(_) + [1, 2, 3, 4] + + >>> ifft([1, 2, 3, 4]) + [5/2, -1/2 + I/2, -1/2, -1/2 - I/2] + >>> fft(_) + [1, 2, 3, 4] + + >>> ifft([1, 7, 3, 4], dps=15) + [3.75, -0.5 - 0.75*I, -1.75, -0.5 + 0.75*I] + >>> fft(_) + [1.0, 7.0, 3.0, 4.0] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm + .. [2] https://mathworld.wolfram.com/FastFourierTransform.html + + """ + + return _fourier_transform(seq, dps=dps) + + +def ifft(seq, dps=None): + return _fourier_transform(seq, dps=dps, inverse=True) + +ifft.__doc__ = fft.__doc__ + + +#----------------------------------------------------------------------------# +# # +# Number Theoretic Transform # +# # +#----------------------------------------------------------------------------# + +def _number_theoretic_transform(seq, prime, inverse=False): + """Utility function for the Number Theoretic Transform""" + + if not iterable(seq): + raise TypeError("Expected a sequence of integer coefficients " + "for Number Theoretic Transform") + + p = as_int(prime) + if not isprime(p): + raise ValueError("Expected prime modulus for " + "Number Theoretic Transform") + + a = [as_int(x) % p for x in seq] + + n = len(a) + if n < 1: + return a + + b = n.bit_length() - 1 + if n&(n - 1): + b += 1 + n = 2**b + + if (p - 1) % n: + raise ValueError("Expected prime modulus of the form (m*2**k + 1)") + + a += [0]*(n - len(a)) + for i in range(1, n): + j = int(ibin(i, b, str=True)[::-1], 2) + if i < j: + a[i], a[j] = a[j], a[i] + + pr = primitive_root(p) + + rt = pow(pr, (p - 1) // n, p) + if inverse: + rt = pow(rt, p - 2, p) + + w = [1]*(n // 2) + for i in range(1, n // 2): + w[i] = w[i - 1]*rt % p + + h = 2 + while h <= n: + hf, ut = h // 2, n // h + for i in range(0, n, h): + for j in range(hf): + u, v = a[i + j], a[i + j + hf]*w[ut * j] + a[i + j], a[i + j + hf] = (u + v) % p, (u - v) % p + h *= 2 + + if inverse: + rv = pow(n, p - 2, p) + a = [x*rv % p for x in a] + + return a + + +def ntt(seq, prime): + r""" + Performs the Number Theoretic Transform (**NTT**), which specializes the + Discrete Fourier Transform (**DFT**) over quotient ring `Z/pZ` for prime + `p` instead of complex numbers `C`. + + The sequence is automatically padded to the right with zeros, as the + *radix-2 NTT* requires the number of sample points to be a power of 2. + + Parameters + ========== + + seq : iterable + The sequence on which **DFT** is to be applied. + prime : Integer + Prime modulus of the form `(m 2^k + 1)` to be used for performing + **NTT** on the sequence. + + Examples + ======== + + >>> from sympy import ntt, intt + >>> ntt([1, 2, 3, 4], prime=3*2**8 + 1) + [10, 643, 767, 122] + >>> intt(_, 3*2**8 + 1) + [1, 2, 3, 4] + >>> intt([1, 2, 3, 4], prime=3*2**8 + 1) + [387, 415, 384, 353] + >>> ntt(_, prime=3*2**8 + 1) + [1, 2, 3, 4] + + References + ========== + + .. [1] http://www.apfloat.org/ntt.html + .. [2] https://mathworld.wolfram.com/NumberTheoreticTransform.html + .. [3] https://en.wikipedia.org/wiki/Discrete_Fourier_transform_(general%29 + + """ + + return _number_theoretic_transform(seq, prime=prime) + + +def intt(seq, prime): + return _number_theoretic_transform(seq, prime=prime, inverse=True) + +intt.__doc__ = ntt.__doc__ + + +#----------------------------------------------------------------------------# +# # +# Walsh Hadamard Transform # +# # +#----------------------------------------------------------------------------# + +def _walsh_hadamard_transform(seq, inverse=False): + """Utility function for the Walsh Hadamard Transform""" + + if not iterable(seq): + raise TypeError("Expected a sequence of coefficients " + "for Walsh Hadamard Transform") + + a = [sympify(arg) for arg in seq] + n = len(a) + if n < 2: + return a + + if n&(n - 1): + n = 2**n.bit_length() + + a += [S.Zero]*(n - len(a)) + h = 2 + while h <= n: + hf = h // 2 + for i in range(0, n, h): + for j in range(hf): + u, v = a[i + j], a[i + j + hf] + a[i + j], a[i + j + hf] = u + v, u - v + h *= 2 + + if inverse: + a = [x/n for x in a] + + return a + + +def fwht(seq): + r""" + Performs the Walsh Hadamard Transform (**WHT**), and uses Hadamard + ordering for the sequence. + + The sequence is automatically padded to the right with zeros, as the + *radix-2 FWHT* requires the number of sample points to be a power of 2. + + Parameters + ========== + + seq : iterable + The sequence on which WHT is to be applied. + + Examples + ======== + + >>> from sympy import fwht, ifwht + >>> fwht([4, 2, 2, 0, 0, 2, -2, 0]) + [8, 0, 8, 0, 8, 8, 0, 0] + >>> ifwht(_) + [4, 2, 2, 0, 0, 2, -2, 0] + + >>> ifwht([19, -1, 11, -9, -7, 13, -15, 5]) + [2, 0, 4, 0, 3, 10, 0, 0] + >>> fwht(_) + [19, -1, 11, -9, -7, 13, -15, 5] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hadamard_transform + .. [2] https://en.wikipedia.org/wiki/Fast_Walsh%E2%80%93Hadamard_transform + + """ + + return _walsh_hadamard_transform(seq) + + +def ifwht(seq): + return _walsh_hadamard_transform(seq, inverse=True) + +ifwht.__doc__ = fwht.__doc__ + + +#----------------------------------------------------------------------------# +# # +# Mobius Transform for Subset Lattice # +# # +#----------------------------------------------------------------------------# + +def _mobius_transform(seq, sgn, subset): + r"""Utility function for performing Mobius Transform using + Yate's Dynamic Programming method""" + + if not iterable(seq): + raise TypeError("Expected a sequence of coefficients") + + a = [sympify(arg) for arg in seq] + + n = len(a) + if n < 2: + return a + + if n&(n - 1): + n = 2**n.bit_length() + + a += [S.Zero]*(n - len(a)) + + if subset: + i = 1 + while i < n: + for j in range(n): + if j & i: + a[j] += sgn*a[j ^ i] + i *= 2 + + else: + i = 1 + while i < n: + for j in range(n): + if j & i: + continue + a[j] += sgn*a[j ^ i] + i *= 2 + + return a + + +def mobius_transform(seq, subset=True): + r""" + Performs the Mobius Transform for subset lattice with indices of + sequence as bitmasks. + + The indices of each argument, considered as bit strings, correspond + to subsets of a finite set. + + The sequence is automatically padded to the right with zeros, as the + definition of subset/superset based on bitmasks (indices) requires + the size of sequence to be a power of 2. + + Parameters + ========== + + seq : iterable + The sequence on which Mobius Transform is to be applied. + subset : bool + Specifies if Mobius Transform is applied by enumerating subsets + or supersets of the given set. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy import mobius_transform, inverse_mobius_transform + >>> x, y, z = symbols('x y z') + + >>> mobius_transform([x, y, z]) + [x, x + y, x + z, x + y + z] + >>> inverse_mobius_transform(_) + [x, y, z, 0] + + >>> mobius_transform([x, y, z], subset=False) + [x + y + z, y, z, 0] + >>> inverse_mobius_transform(_, subset=False) + [x, y, z, 0] + + >>> mobius_transform([1, 2, 3, 4]) + [1, 3, 4, 10] + >>> inverse_mobius_transform(_) + [1, 2, 3, 4] + >>> mobius_transform([1, 2, 3, 4], subset=False) + [10, 6, 7, 4] + >>> inverse_mobius_transform(_, subset=False) + [1, 2, 3, 4] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/M%C3%B6bius_inversion_formula + .. [2] https://people.csail.mit.edu/rrw/presentations/subset-conv.pdf + .. [3] https://arxiv.org/pdf/1211.0189.pdf + + """ + + return _mobius_transform(seq, sgn=+1, subset=subset) + +def inverse_mobius_transform(seq, subset=True): + return _mobius_transform(seq, sgn=-1, subset=subset) + +inverse_mobius_transform.__doc__ = mobius_transform.__doc__ diff --git a/venv/lib/python3.11/site-packages/sympy/external/__init__.py b/venv/lib/python3.11/site-packages/sympy/external/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..549b4b96cdce0ee4d31960e89cb9dc26af0e105d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/external/__init__.py @@ -0,0 +1,20 @@ +""" +Unified place for determining if external dependencies are installed or not. + +You should import all external modules using the import_module() function. + +For example + +>>> from sympy.external import import_module +>>> numpy = import_module('numpy') + +If the resulting library is not installed, or if the installed version +is less than a given minimum version, the function will return None. +Otherwise, it will return the library. See the docstring of +import_module() for more information. + +""" + +from sympy.external.importtools import import_module + +__all__ = ['import_module'] diff --git a/venv/lib/python3.11/site-packages/sympy/external/gmpy.py b/venv/lib/python3.11/site-packages/sympy/external/gmpy.py new file mode 100644 index 0000000000000000000000000000000000000000..d26942864bf4786e72198d3640d488857b3313f4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/external/gmpy.py @@ -0,0 +1,342 @@ +from __future__ import annotations +import os +from ctypes import c_long, sizeof +from functools import reduce +from typing import Type +from warnings import warn + +from sympy.external import import_module + +from .pythonmpq import PythonMPQ + +from .ntheory import ( + bit_scan1 as python_bit_scan1, + bit_scan0 as python_bit_scan0, + remove as python_remove, + factorial as python_factorial, + sqrt as python_sqrt, + sqrtrem as python_sqrtrem, + gcd as python_gcd, + lcm as python_lcm, + gcdext as python_gcdext, + is_square as python_is_square, + invert as python_invert, + legendre as python_legendre, + jacobi as python_jacobi, + kronecker as python_kronecker, + iroot as python_iroot, + is_fermat_prp as python_is_fermat_prp, + is_euler_prp as python_is_euler_prp, + is_strong_prp as python_is_strong_prp, + is_fibonacci_prp as python_is_fibonacci_prp, + is_lucas_prp as python_is_lucas_prp, + is_selfridge_prp as python_is_selfridge_prp, + is_strong_lucas_prp as python_is_strong_lucas_prp, + is_strong_selfridge_prp as python_is_strong_selfridge_prp, + is_bpsw_prp as python_is_bpsw_prp, + is_strong_bpsw_prp as python_is_strong_bpsw_prp, +) + + +__all__ = [ + # GROUND_TYPES is either 'gmpy' or 'python' depending on which is used. If + # gmpy is installed then it will be used unless the environment variable + # SYMPY_GROUND_TYPES is set to something other than 'auto', 'gmpy', or + # 'gmpy2'. + 'GROUND_TYPES', + + # If HAS_GMPY is 0, no supported version of gmpy is available. Otherwise, + # HAS_GMPY will be 2 for gmpy2 if GROUND_TYPES is 'gmpy'. It used to be + # possible for HAS_GMPY to be 1 for gmpy but gmpy is no longer supported. + 'HAS_GMPY', + + # SYMPY_INTS is a tuple containing the base types for valid integer types. + # This is either (int,) or (int, type(mpz(0))) depending on GROUND_TYPES. + 'SYMPY_INTS', + + # MPQ is either gmpy.mpq or the Python equivalent from + # sympy.external.pythonmpq + 'MPQ', + + # MPZ is either gmpy.mpz or int. + 'MPZ', + + 'bit_scan1', + 'bit_scan0', + 'remove', + 'factorial', + 'sqrt', + 'is_square', + 'sqrtrem', + 'gcd', + 'lcm', + 'gcdext', + 'invert', + 'legendre', + 'jacobi', + 'kronecker', + 'iroot', + 'is_fermat_prp', + 'is_euler_prp', + 'is_strong_prp', + 'is_fibonacci_prp', + 'is_lucas_prp', + 'is_selfridge_prp', + 'is_strong_lucas_prp', + 'is_strong_selfridge_prp', + 'is_bpsw_prp', + 'is_strong_bpsw_prp', +] + + +# +# Tested python-flint version. Future versions might work but we will only use +# them if explicitly requested by SYMPY_GROUND_TYPES=flint. +# +_PYTHON_FLINT_VERSION_NEEDED = ["0.6", "0.7", "0.8", "0.9", "0.10"] + + +def _flint_version_okay(flint_version): + major, minor = flint_version.split('.')[:2] + flint_ver = f'{major}.{minor}' + return flint_ver in _PYTHON_FLINT_VERSION_NEEDED + +# +# We will only use gmpy2 >= 2.0.0 +# +_GMPY2_MIN_VERSION = '2.0.0' + + +def _get_flint(sympy_ground_types): + if sympy_ground_types not in ('auto', 'flint'): + return None + + try: + import flint + # Earlier versions of python-flint may not have __version__. + from flint import __version__ as _flint_version + except ImportError: + if sympy_ground_types == 'flint': + warn("SYMPY_GROUND_TYPES was set to flint but python-flint is not " + "installed. Falling back to other ground types.") + return None + + if _flint_version_okay(_flint_version): + return flint + elif sympy_ground_types == 'auto': + return None + else: + warn(f"Using python-flint {_flint_version} because SYMPY_GROUND_TYPES " + f"is set to flint but this version of SymPy is only tested " + f"with python-flint versions {_PYTHON_FLINT_VERSION_NEEDED}.") + return flint + + +def _get_gmpy2(sympy_ground_types): + if sympy_ground_types not in ('auto', 'gmpy', 'gmpy2'): + return None + + gmpy = import_module('gmpy2', min_module_version=_GMPY2_MIN_VERSION, + module_version_attr='version', module_version_attr_call_args=()) + + if sympy_ground_types != 'auto' and gmpy is None: + warn("gmpy2 library is not installed, switching to 'python' ground types") + + return gmpy + + +# +# SYMPY_GROUND_TYPES can be flint, gmpy, gmpy2, python or auto (default) +# +_SYMPY_GROUND_TYPES = os.environ.get('SYMPY_GROUND_TYPES', 'auto').lower() +_flint = None +_gmpy = None + +# +# First handle auto-detection of flint/gmpy2. We will prefer flint if available +# or otherwise gmpy2 if available and then lastly the python types. +# +if _SYMPY_GROUND_TYPES in ('auto', 'flint'): + _flint = _get_flint(_SYMPY_GROUND_TYPES) + if _flint is not None: + _SYMPY_GROUND_TYPES = 'flint' + else: + _SYMPY_GROUND_TYPES = 'auto' + +if _SYMPY_GROUND_TYPES in ('auto', 'gmpy', 'gmpy2'): + _gmpy = _get_gmpy2(_SYMPY_GROUND_TYPES) + if _gmpy is not None: + _SYMPY_GROUND_TYPES = 'gmpy' + else: + _SYMPY_GROUND_TYPES = 'python' + +if _SYMPY_GROUND_TYPES not in ('flint', 'gmpy', 'python'): + warn("SYMPY_GROUND_TYPES environment variable unrecognised. " + "Should be 'auto', 'flint', 'gmpy', 'gmpy2' or 'python'.") + _SYMPY_GROUND_TYPES = 'python' + +# +# At this point _SYMPY_GROUND_TYPES is either flint, gmpy or python. The blocks +# below define the values exported by this module in each case. +# + +# +# In gmpy2 and flint, there are functions that take a long (or unsigned long) +# argument. That is, it is not possible to input a value larger than that. +# +LONG_MAX = (1 << (8*sizeof(c_long) - 1)) - 1 + +# +# Type checkers are confused by what SYMPY_INTS is. There may be a better type +# hint for this like Type[Integral] or something. +# +SYMPY_INTS: tuple[Type, ...] + +if _SYMPY_GROUND_TYPES == 'gmpy': + + assert _gmpy is not None + + flint = None + gmpy = _gmpy + + HAS_GMPY = 2 + GROUND_TYPES = 'gmpy' + SYMPY_INTS = (int, type(gmpy.mpz(0))) + MPZ = gmpy.mpz + MPQ = gmpy.mpq + + bit_scan1 = gmpy.bit_scan1 + bit_scan0 = gmpy.bit_scan0 + remove = gmpy.remove + factorial = gmpy.fac + sqrt = gmpy.isqrt + is_square = gmpy.is_square + sqrtrem = gmpy.isqrt_rem + gcd = gmpy.gcd + lcm = gmpy.lcm + gcdext = gmpy.gcdext + invert = gmpy.invert + legendre = gmpy.legendre + jacobi = gmpy.jacobi + kronecker = gmpy.kronecker + + def iroot(x, n): + # In the latest gmpy2, the threshold for n is ULONG_MAX, + # but adjust to the older one. + if n <= LONG_MAX: + return gmpy.iroot(x, n) + return python_iroot(x, n) + + is_fermat_prp = gmpy.is_fermat_prp + is_euler_prp = gmpy.is_euler_prp + is_strong_prp = gmpy.is_strong_prp + is_fibonacci_prp = gmpy.is_fibonacci_prp + is_lucas_prp = gmpy.is_lucas_prp + is_selfridge_prp = gmpy.is_selfridge_prp + is_strong_lucas_prp = gmpy.is_strong_lucas_prp + is_strong_selfridge_prp = gmpy.is_strong_selfridge_prp + is_bpsw_prp = gmpy.is_bpsw_prp + is_strong_bpsw_prp = gmpy.is_strong_bpsw_prp + +elif _SYMPY_GROUND_TYPES == 'flint': + + assert _flint is not None + + flint = _flint + gmpy = None + + HAS_GMPY = 0 + GROUND_TYPES = 'flint' + SYMPY_INTS = (int, flint.fmpz) # type: ignore + MPZ = flint.fmpz # type: ignore + MPQ = flint.fmpq # type: ignore + + bit_scan1 = python_bit_scan1 + bit_scan0 = python_bit_scan0 + remove = python_remove + factorial = python_factorial + + def sqrt(x): + return flint.fmpz(x).isqrt() + + def is_square(x): + if x < 0: + return False + return flint.fmpz(x).sqrtrem()[1] == 0 + + def sqrtrem(x): + return flint.fmpz(x).sqrtrem() + + def gcd(*args): + return reduce(flint.fmpz.gcd, args, flint.fmpz(0)) + + def lcm(*args): + return reduce(flint.fmpz.lcm, args, flint.fmpz(1)) + + gcdext = python_gcdext + invert = python_invert + legendre = python_legendre + + def jacobi(x, y): + if y <= 0 or not y % 2: + raise ValueError("y should be an odd positive integer") + return flint.fmpz(x).jacobi(y) + + kronecker = python_kronecker + + def iroot(x, n): + if n <= LONG_MAX: + y = flint.fmpz(x).root(n) + return y, y**n == x + return python_iroot(x, n) + + is_fermat_prp = python_is_fermat_prp + is_euler_prp = python_is_euler_prp + is_strong_prp = python_is_strong_prp + is_fibonacci_prp = python_is_fibonacci_prp + is_lucas_prp = python_is_lucas_prp + is_selfridge_prp = python_is_selfridge_prp + is_strong_lucas_prp = python_is_strong_lucas_prp + is_strong_selfridge_prp = python_is_strong_selfridge_prp + is_bpsw_prp = python_is_bpsw_prp + is_strong_bpsw_prp = python_is_strong_bpsw_prp + +elif _SYMPY_GROUND_TYPES == 'python': + + flint = None + gmpy = None + + HAS_GMPY = 0 + GROUND_TYPES = 'python' + SYMPY_INTS = (int,) + MPZ = int + MPQ = PythonMPQ + + bit_scan1 = python_bit_scan1 + bit_scan0 = python_bit_scan0 + remove = python_remove + factorial = python_factorial + sqrt = python_sqrt + is_square = python_is_square + sqrtrem = python_sqrtrem + gcd = python_gcd + lcm = python_lcm + gcdext = python_gcdext + invert = python_invert + legendre = python_legendre + jacobi = python_jacobi + kronecker = python_kronecker + iroot = python_iroot + is_fermat_prp = python_is_fermat_prp + is_euler_prp = python_is_euler_prp + is_strong_prp = python_is_strong_prp + is_fibonacci_prp = python_is_fibonacci_prp + is_lucas_prp = python_is_lucas_prp + is_selfridge_prp = python_is_selfridge_prp + is_strong_lucas_prp = python_is_strong_lucas_prp + is_strong_selfridge_prp = python_is_strong_selfridge_prp + is_bpsw_prp = python_is_bpsw_prp + is_strong_bpsw_prp = python_is_strong_bpsw_prp + +else: + assert False diff --git a/venv/lib/python3.11/site-packages/sympy/external/importtools.py b/venv/lib/python3.11/site-packages/sympy/external/importtools.py new file mode 100644 index 0000000000000000000000000000000000000000..5008b3dd4634d3cee10744a0a92b1204051f07cc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/external/importtools.py @@ -0,0 +1,187 @@ +"""Tools to assist importing optional external modules.""" + +import sys +import re + +# Override these in the module to change the default warning behavior. +# For example, you might set both to False before running the tests so that +# warnings are not printed to the console, or set both to True for debugging. + +WARN_NOT_INSTALLED = None # Default is False +WARN_OLD_VERSION = None # Default is True + + +def __sympy_debug(): + # helper function from sympy/__init__.py + # We don't just import SYMPY_DEBUG from that file because we don't want to + # import all of SymPy just to use this module. + import os + debug_str = os.getenv('SYMPY_DEBUG', 'False') + if debug_str in ('True', 'False'): + return eval(debug_str) + else: + raise RuntimeError("unrecognized value for SYMPY_DEBUG: %s" % + debug_str) + +if __sympy_debug(): + WARN_OLD_VERSION = True + WARN_NOT_INSTALLED = True + + +_component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) + +def version_tuple(vstring): + # Parse a version string to a tuple e.g. '1.2' -> (1, 2) + # Simplified from distutils.version.LooseVersion which was deprecated in + # Python 3.10. + components = [] + for x in _component_re.split(vstring): + if x and x != '.': + try: + x = int(x) + except ValueError: + pass + components.append(x) + return tuple(components) + + +def import_module(module, min_module_version=None, min_python_version=None, + warn_not_installed=None, warn_old_version=None, + module_version_attr='__version__', module_version_attr_call_args=None, + import_kwargs={}, catch=()): + """ + Import and return a module if it is installed. + + If the module is not installed, it returns None. + + A minimum version for the module can be given as the keyword argument + min_module_version. This should be comparable against the module version. + By default, module.__version__ is used to get the module version. To + override this, set the module_version_attr keyword argument. If the + attribute of the module to get the version should be called (e.g., + module.version()), then set module_version_attr_call_args to the args such + that module.module_version_attr(*module_version_attr_call_args) returns the + module's version. + + If the module version is less than min_module_version using the Python < + comparison, None will be returned, even if the module is installed. You can + use this to keep from importing an incompatible older version of a module. + + You can also specify a minimum Python version by using the + min_python_version keyword argument. This should be comparable against + sys.version_info. + + If the keyword argument warn_not_installed is set to True, the function will + emit a UserWarning when the module is not installed. + + If the keyword argument warn_old_version is set to True, the function will + emit a UserWarning when the library is installed, but cannot be imported + because of the min_module_version or min_python_version options. + + Note that because of the way warnings are handled, a warning will be + emitted for each module only once. You can change the default warning + behavior by overriding the values of WARN_NOT_INSTALLED and WARN_OLD_VERSION + in sympy.external.importtools. By default, WARN_NOT_INSTALLED is False and + WARN_OLD_VERSION is True. + + This function uses __import__() to import the module. To pass additional + options to __import__(), use the import_kwargs keyword argument. For + example, to import a submodule A.B, you must pass a nonempty fromlist option + to __import__. See the docstring of __import__(). + + This catches ImportError to determine if the module is not installed. To + catch additional errors, pass them as a tuple to the catch keyword + argument. + + Examples + ======== + + >>> from sympy.external import import_module + + >>> numpy = import_module('numpy') + + >>> numpy = import_module('numpy', min_python_version=(2, 7), + ... warn_old_version=False) + + >>> numpy = import_module('numpy', min_module_version='1.5', + ... warn_old_version=False) # numpy.__version__ is a string + + >>> # gmpy does not have __version__, but it does have gmpy.version() + + >>> gmpy = import_module('gmpy', min_module_version='1.14', + ... module_version_attr='version', module_version_attr_call_args=(), + ... warn_old_version=False) + + >>> # To import a submodule, you must pass a nonempty fromlist to + >>> # __import__(). The values do not matter. + >>> p3 = import_module('mpl_toolkits.mplot3d', + ... import_kwargs={'fromlist':['something']}) + + >>> # matplotlib.pyplot can raise RuntimeError when the display cannot be opened + >>> matplotlib = import_module('matplotlib', + ... import_kwargs={'fromlist':['pyplot']}, catch=(RuntimeError,)) + + """ + # keyword argument overrides default, and global variable overrides + # keyword argument. + warn_old_version = (WARN_OLD_VERSION if WARN_OLD_VERSION is not None + else warn_old_version or True) + warn_not_installed = (WARN_NOT_INSTALLED if WARN_NOT_INSTALLED is not None + else warn_not_installed or False) + + import warnings + + # Check Python first so we don't waste time importing a module we can't use + if min_python_version: + if sys.version_info < min_python_version: + if warn_old_version: + warnings.warn("Python version is too old to use %s " + "(%s or newer required)" % ( + module, '.'.join(map(str, min_python_version))), + UserWarning, stacklevel=2) + return + + try: + mod = __import__(module, **import_kwargs) + + ## there's something funny about imports with matplotlib and py3k. doing + ## from matplotlib import collections + ## gives python's stdlib collections module. explicitly re-importing + ## the module fixes this. + from_list = import_kwargs.get('fromlist', ()) + for submod in from_list: + if submod == 'collections' and mod.__name__ == 'matplotlib': + __import__(module + '.' + submod) + except ImportError: + if warn_not_installed: + warnings.warn("%s module is not installed" % module, UserWarning, + stacklevel=2) + return + except catch as e: + if warn_not_installed: + warnings.warn( + "%s module could not be used (%s)" % (module, repr(e)), + stacklevel=2) + return + + if min_module_version: + modversion = getattr(mod, module_version_attr) + if module_version_attr_call_args is not None: + modversion = modversion(*module_version_attr_call_args) + if version_tuple(modversion) < version_tuple(min_module_version): + if warn_old_version: + # Attempt to create a pretty string version of the version + if isinstance(min_module_version, str): + verstr = min_module_version + elif isinstance(min_module_version, (tuple, list)): + verstr = '.'.join(map(str, min_module_version)) + else: + # Either don't know what this is. Hopefully + # it's something that has a nice str version, like an int. + verstr = str(min_module_version) + warnings.warn("%s version is too old to use " + "(%s or newer required)" % (module, verstr), + UserWarning, stacklevel=2) + return + + return mod diff --git a/venv/lib/python3.11/site-packages/sympy/external/ntheory.py b/venv/lib/python3.11/site-packages/sympy/external/ntheory.py new file mode 100644 index 0000000000000000000000000000000000000000..a0c9bf813cf02b311f9a12ee7fbc4932ed551f3b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/external/ntheory.py @@ -0,0 +1,618 @@ +# sympy.external.ntheory +# +# This module provides pure Python implementations of some number theory +# functions that are alternately used from gmpy2 if it is installed. + +import math + +import mpmath.libmp as mlib + + +_small_trailing = [0] * 256 +for j in range(1, 8): + _small_trailing[1 << j :: 1 << (j + 1)] = [j] * (1 << (7 - j)) + + +def bit_scan1(x, n=0): + if not x: + return + x = abs(x >> n) + low_byte = x & 0xFF + if low_byte: + return _small_trailing[low_byte] + n + + t = 8 + n + x >>= 8 + # 2**m is quick for z up through 2**30 + z = x.bit_length() - 1 + if x == 1 << z: + return z + t + + if z < 300: + # fixed 8-byte reduction + while not x & 0xFF: + x >>= 8 + t += 8 + else: + # binary reduction important when there might be a large + # number of trailing 0s + p = z >> 1 + while not x & 0xFF: + while x & ((1 << p) - 1): + p >>= 1 + x >>= p + t += p + return t + _small_trailing[x & 0xFF] + + +def bit_scan0(x, n=0): + return bit_scan1(x + (1 << n), n) + + +def remove(x, f): + if f < 2: + raise ValueError("factor must be > 1") + if x == 0: + return 0, 0 + if f == 2: + b = bit_scan1(x) + return x >> b, b + m = 0 + y, rem = divmod(x, f) + while not rem: + x = y + m += 1 + if m > 5: + pow_list = [f**2] + while pow_list: + _f = pow_list[-1] + y, rem = divmod(x, _f) + if not rem: + m += 1 << len(pow_list) + x = y + pow_list.append(_f**2) + else: + pow_list.pop() + y, rem = divmod(x, f) + return x, m + + +def factorial(x): + """Return x!.""" + return int(mlib.ifac(int(x))) + + +def sqrt(x): + """Integer square root of x.""" + return int(mlib.isqrt(int(x))) + + +def sqrtrem(x): + """Integer square root of x and remainder.""" + s, r = mlib.sqrtrem(int(x)) + return (int(s), int(r)) + + +gcd = math.gcd +lcm = math.lcm + + +def _sign(n): + if n < 0: + return -1, -n + return 1, n + + +def gcdext(a, b): + if not a or not b: + g = abs(a) or abs(b) + if not g: + return (0, 0, 0) + return (g, a // g, b // g) + + x_sign, a = _sign(a) + y_sign, b = _sign(b) + x, r = 1, 0 + y, s = 0, 1 + + while b: + q, c = divmod(a, b) + a, b = b, c + x, r = r, x - q*r + y, s = s, y - q*s + + return (a, x * x_sign, y * y_sign) + + +def is_square(x): + """Return True if x is a square number.""" + if x < 0: + return False + + # Note that the possible values of y**2 % n for a given n are limited. + # For example, when n=4, y**2 % n can only take 0 or 1. + # In other words, if x % 4 is 2 or 3, then x is not a square number. + # Mathematically, it determines if it belongs to the set {y**2 % n}, + # but implementationally, it can be realized as a logical conjunction + # with an n-bit integer. + # see https://mersenneforum.org/showpost.php?p=110896 + # def magic(n): + # s = {y**2 % n for y in range(n)} + # s = set(range(n)) - s + # return sum(1 << bit for bit in s) + # >>> print(hex(magic(128))) + # 0xfdfdfdedfdfdfdecfdfdfdedfdfcfdec + # >>> print(hex(magic(99))) + # 0x5f6f9ffb6fb7ddfcb75befdec + # >>> print(hex(magic(91))) + # 0x6fd1bfcfed5f3679d3ebdec + # >>> print(hex(magic(85))) + # 0xdef9ae771ffe3b9d67dec + if 0xfdfdfdedfdfdfdecfdfdfdedfdfcfdec & (1 << (x & 127)): + return False # e.g. 2, 3 + m = x % 765765 # 765765 = 99 * 91 * 85 + if 0x5f6f9ffb6fb7ddfcb75befdec & (1 << (m % 99)): + return False # e.g. 17, 68 + if 0x6fd1bfcfed5f3679d3ebdec & (1 << (m % 91)): + return False # e.g. 97, 388 + if 0xdef9ae771ffe3b9d67dec & (1 << (m % 85)): + return False # e.g. 793, 1408 + return mlib.sqrtrem(int(x))[1] == 0 + + +def invert(x, m): + """Modular inverse of x modulo m. + + Returns y such that x*y == 1 mod m. + + Uses ``math.pow`` but reproduces the behaviour of ``gmpy2.invert`` + which raises ZeroDivisionError if no inverse exists. + """ + try: + return pow(x, -1, m) + except ValueError: + raise ZeroDivisionError("invert() no inverse exists") + + +def legendre(x, y): + """Legendre symbol (x / y). + + Following the implementation of gmpy2, + the error is raised only when y is an even number. + """ + if y <= 0 or not y % 2: + raise ValueError("y should be an odd prime") + x %= y + if not x: + return 0 + if pow(x, (y - 1) // 2, y) == 1: + return 1 + return -1 + + +def jacobi(x, y): + """Jacobi symbol (x / y).""" + if y <= 0 or not y % 2: + raise ValueError("y should be an odd positive integer") + x %= y + if not x: + return int(y == 1) + if y == 1 or x == 1: + return 1 + if gcd(x, y) != 1: + return 0 + j = 1 + while x != 0: + while x % 2 == 0 and x > 0: + x >>= 1 + if y % 8 in [3, 5]: + j = -j + x, y = y, x + if x % 4 == y % 4 == 3: + j = -j + x %= y + return j + + +def kronecker(x, y): + """Kronecker symbol (x / y).""" + if gcd(x, y) != 1: + return 0 + if y == 0: + return 1 + sign = -1 if y < 0 and x < 0 else 1 + y = abs(y) + s = bit_scan1(y) + y >>= s + if s % 2 and x % 8 in [3, 5]: + sign = -sign + return sign * jacobi(x, y) + + +def iroot(y, n): + if y < 0: + raise ValueError("y must be nonnegative") + if n < 1: + raise ValueError("n must be positive") + if y in (0, 1): + return y, True + if n == 1: + return y, True + if n == 2: + x, rem = mlib.sqrtrem(y) + return int(x), not rem + if n >= y.bit_length(): + return 1, False + # Get initial estimate for Newton's method. Care must be taken to + # avoid overflow + try: + guess = int(y**(1./n) + 0.5) + except OverflowError: + exp = math.log2(y)/n + if exp > 53: + shift = int(exp - 53) + guess = int(2.0**(exp - shift) + 1) << shift + else: + guess = int(2.0**exp) + if guess > 2**50: + # Newton iteration + xprev, x = -1, guess + while 1: + t = x**(n - 1) + xprev, x = x, ((n - 1)*x + y//t)//n + if abs(x - xprev) < 2: + break + else: + x = guess + # Compensate + t = x**n + while t < y: + x += 1 + t = x**n + while t > y: + x -= 1 + t = x**n + return x, t == y + + +def is_fermat_prp(n, a): + if a < 2: + raise ValueError("is_fermat_prp() requires 'a' greater than or equal to 2") + if n < 1: + raise ValueError("is_fermat_prp() requires 'n' be greater than 0") + if n == 1: + return False + if n % 2 == 0: + return n == 2 + a %= n + if gcd(n, a) != 1: + raise ValueError("is_fermat_prp() requires gcd(n,a) == 1") + return pow(a, n - 1, n) == 1 + + +def is_euler_prp(n, a): + if a < 2: + raise ValueError("is_euler_prp() requires 'a' greater than or equal to 2") + if n < 1: + raise ValueError("is_euler_prp() requires 'n' be greater than 0") + if n == 1: + return False + if n % 2 == 0: + return n == 2 + a %= n + if gcd(n, a) != 1: + raise ValueError("is_euler_prp() requires gcd(n,a) == 1") + return pow(a, n >> 1, n) == jacobi(a, n) % n + + +def _is_strong_prp(n, a): + s = bit_scan1(n - 1) + a = pow(a, n >> s, n) + if a == 1 or a == n - 1: + return True + for _ in range(s - 1): + a = pow(a, 2, n) + if a == n - 1: + return True + if a == 1: + return False + return False + + +def is_strong_prp(n, a): + if a < 2: + raise ValueError("is_strong_prp() requires 'a' greater than or equal to 2") + if n < 1: + raise ValueError("is_strong_prp() requires 'n' be greater than 0") + if n == 1: + return False + if n % 2 == 0: + return n == 2 + a %= n + if gcd(n, a) != 1: + raise ValueError("is_strong_prp() requires gcd(n,a) == 1") + return _is_strong_prp(n, a) + + +def _lucas_sequence(n, P, Q, k): + r"""Return the modular Lucas sequence (U_k, V_k, Q_k). + + Explanation + =========== + + Given a Lucas sequence defined by P, Q, returns the kth values for + U and V, along with Q^k, all modulo n. This is intended for use with + possibly very large values of n and k, where the combinatorial functions + would be completely unusable. + + .. math :: + U_k = \begin{cases} + 0 & \text{if } k = 0\\ + 1 & \text{if } k = 1\\ + PU_{k-1} - QU_{k-2} & \text{if } k > 1 + \end{cases}\\ + V_k = \begin{cases} + 2 & \text{if } k = 0\\ + P & \text{if } k = 1\\ + PV_{k-1} - QV_{k-2} & \text{if } k > 1 + \end{cases} + + The modular Lucas sequences are used in numerous places in number theory, + especially in the Lucas compositeness tests and the various n + 1 proofs. + + Parameters + ========== + + n : int + n is an odd number greater than or equal to 3 + P : int + Q : int + D determined by D = P**2 - 4*Q is non-zero + k : int + k is a nonnegative integer + + Returns + ======= + + U, V, Qk : (int, int, int) + `(U_k \bmod{n}, V_k \bmod{n}, Q^k \bmod{n})` + + Examples + ======== + + >>> from sympy.external.ntheory import _lucas_sequence + >>> N = 10**2000 + 4561 + >>> sol = U, V, Qk = _lucas_sequence(N, 3, 1, N//2); sol + (0, 2, 1) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Lucas_sequence + + """ + if k == 0: + return (0, 2, 1) + D = P**2 - 4*Q + U = 1 + V = P + Qk = Q % n + if Q == 1: + # Optimization for extra strong tests. + for b in bin(k)[3:]: + U = (U*V) % n + V = (V*V - 2) % n + if b == "1": + U, V = U*P + V, V*P + U*D + if U & 1: + U += n + if V & 1: + V += n + U, V = U >> 1, V >> 1 + elif P == 1 and Q == -1: + # Small optimization for 50% of Selfridge parameters. + for b in bin(k)[3:]: + U = (U*V) % n + if Qk == 1: + V = (V*V - 2) % n + else: + V = (V*V + 2) % n + Qk = 1 + if b == "1": + # new_U = (U + V) // 2 + # new_V = (5*U + V) // 2 = 2*U + new_U + U, V = U + V, U << 1 + if U & 1: + U += n + U >>= 1 + V += U + Qk = -1 + Qk %= n + elif P == 1: + for b in bin(k)[3:]: + U = (U*V) % n + V = (V*V - 2*Qk) % n + Qk *= Qk + if b == "1": + # new_U = (U + V) // 2 + # new_V = new_U - 2*Q*U + U, V = U + V, (Q*U) << 1 + if U & 1: + U += n + U >>= 1 + V = U - V + Qk *= Q + Qk %= n + else: + # The general case with any P and Q. + for b in bin(k)[3:]: + U = (U*V) % n + V = (V*V - 2*Qk) % n + Qk *= Qk + if b == "1": + U, V = U*P + V, V*P + U*D + if U & 1: + U += n + if V & 1: + V += n + U, V = U >> 1, V >> 1 + Qk *= Q + Qk %= n + return (U % n, V % n, Qk) + + +def is_fibonacci_prp(n, p, q): + d = p**2 - 4*q + if d == 0 or p <= 0 or q not in [1, -1]: + raise ValueError("invalid values for p,q in is_fibonacci_prp()") + if n < 1: + raise ValueError("is_fibonacci_prp() requires 'n' be greater than 0") + if n == 1: + return False + if n % 2 == 0: + return n == 2 + return _lucas_sequence(n, p, q, n)[1] == p % n + + +def is_lucas_prp(n, p, q): + d = p**2 - 4*q + if d == 0: + raise ValueError("invalid values for p,q in is_lucas_prp()") + if n < 1: + raise ValueError("is_lucas_prp() requires 'n' be greater than 0") + if n == 1: + return False + if n % 2 == 0: + return n == 2 + if gcd(n, q*d) not in [1, n]: + raise ValueError("is_lucas_prp() requires gcd(n,2*q*D) == 1") + return _lucas_sequence(n, p, q, n - jacobi(d, n))[0] == 0 + + +def _is_selfridge_prp(n): + """Lucas compositeness test with the Selfridge parameters for n. + + Explanation + =========== + + The Lucas compositeness test checks whether n is a prime number. + The test can be run with arbitrary parameters ``P`` and ``Q``, which also change the performance of the test. + So, which parameters are most effective for running the Lucas compositeness test? + As an algorithm for determining ``P`` and ``Q``, Selfridge proposed method A [1]_ page 1401 + (Since two methods were proposed, referred to simply as A and B in the paper, + we will refer to one of them as "method A"). + + method A fixes ``P = 1``. Then, ``D`` defined by ``D = P**2 - 4Q`` is varied from 5, -7, 9, -11, 13, and so on, + with the first ``D`` being ``jacobi(D, n) == -1``. Once ``D`` is determined, + ``Q`` is determined to be ``(P**2 - D)//4``. + + References + ========== + + .. [1] Robert Baillie, Samuel S. Wagstaff, Lucas Pseudoprimes, + Math. Comp. Vol 35, Number 152 (1980), pp. 1391-1417, + https://doi.org/10.1090%2FS0025-5718-1980-0583518-6 + http://mpqs.free.fr/LucasPseudoprimes.pdf + + """ + for D in range(5, 1_000_000, 2): + if D & 2: # if D % 4 == 3 + D = -D + j = jacobi(D, n) + if j == -1: + return _lucas_sequence(n, 1, (1-D) // 4, n + 1)[0] == 0 + if j == 0 and D % n: + return False + # When j == -1 is hard to find, suspect a square number + if D == 13 and is_square(n): + return False + raise ValueError("appropriate value for D cannot be found in is_selfridge_prp()") + + +def is_selfridge_prp(n): + if n < 1: + raise ValueError("is_selfridge_prp() requires 'n' be greater than 0") + if n == 1: + return False + if n % 2 == 0: + return n == 2 + return _is_selfridge_prp(n) + + +def is_strong_lucas_prp(n, p, q): + D = p**2 - 4*q + if D == 0: + raise ValueError("invalid values for p,q in is_strong_lucas_prp()") + if n < 1: + raise ValueError("is_selfridge_prp() requires 'n' be greater than 0") + if n == 1: + return False + if n % 2 == 0: + return n == 2 + if gcd(n, q*D) not in [1, n]: + raise ValueError("is_strong_lucas_prp() requires gcd(n,2*q*D) == 1") + j = jacobi(D, n) + s = bit_scan1(n - j) + U, V, Qk = _lucas_sequence(n, p, q, (n - j) >> s) + if U == 0 or V == 0: + return True + for _ in range(s - 1): + V = (V*V - 2*Qk) % n + if V == 0: + return True + Qk = pow(Qk, 2, n) + return False + + +def _is_strong_selfridge_prp(n): + for D in range(5, 1_000_000, 2): + if D & 2: # if D % 4 == 3 + D = -D + j = jacobi(D, n) + if j == -1: + s = bit_scan1(n + 1) + U, V, Qk = _lucas_sequence(n, 1, (1-D) // 4, (n + 1) >> s) + if U == 0 or V == 0: + return True + for _ in range(s - 1): + V = (V*V - 2*Qk) % n + if V == 0: + return True + Qk = pow(Qk, 2, n) + return False + if j == 0 and D % n: + return False + # When j == -1 is hard to find, suspect a square number + if D == 13 and is_square(n): + return False + raise ValueError("appropriate value for D cannot be found in is_strong_selfridge_prp()") + + +def is_strong_selfridge_prp(n): + if n < 1: + raise ValueError("is_strong_selfridge_prp() requires 'n' be greater than 0") + if n == 1: + return False + if n % 2 == 0: + return n == 2 + return _is_strong_selfridge_prp(n) + + +def is_bpsw_prp(n): + if n < 1: + raise ValueError("is_bpsw_prp() requires 'n' be greater than 0") + if n == 1: + return False + if n % 2 == 0: + return n == 2 + return _is_strong_prp(n, 2) and _is_selfridge_prp(n) + + +def is_strong_bpsw_prp(n): + if n < 1: + raise ValueError("is_strong_bpsw_prp() requires 'n' be greater than 0") + if n == 1: + return False + if n % 2 == 0: + return n == 2 + return _is_strong_prp(n, 2) and _is_strong_selfridge_prp(n) diff --git a/venv/lib/python3.11/site-packages/sympy/external/pythonmpq.py b/venv/lib/python3.11/site-packages/sympy/external/pythonmpq.py new file mode 100644 index 0000000000000000000000000000000000000000..4f2d102974e04e139c00a39057976b5a5bf90776 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/external/pythonmpq.py @@ -0,0 +1,341 @@ +""" +PythonMPQ: Rational number type based on Python integers. + +This class is intended as a pure Python fallback for when gmpy2 is not +installed. If gmpy2 is installed then its mpq type will be used instead. The +mpq type is around 20x faster. We could just use the stdlib Fraction class +here but that is slower: + + from fractions import Fraction + from sympy.external.pythonmpq import PythonMPQ + nums = range(1000) + dens = range(5, 1005) + rats = [Fraction(n, d) for n, d in zip(nums, dens)] + sum(rats) # <--- 24 milliseconds + rats = [PythonMPQ(n, d) for n, d in zip(nums, dens)] + sum(rats) # <--- 7 milliseconds + +Both mpq and Fraction have some awkward features like the behaviour of +division with // and %: + + >>> from fractions import Fraction + >>> Fraction(2, 3) % Fraction(1, 4) + 1/6 + +For the QQ domain we do not want this behaviour because there should be no +remainder when dividing rational numbers. SymPy does not make use of this +aspect of mpq when gmpy2 is installed. Since this class is a fallback for that +case we do not bother implementing e.g. __mod__ so that we can be sure we +are not using it when gmpy2 is installed either. +""" + +from __future__ import annotations +import operator +from math import gcd +from decimal import Decimal +from fractions import Fraction +import sys +from typing import Type + + +# Used for __hash__ +_PyHASH_MODULUS = sys.hash_info.modulus +_PyHASH_INF = sys.hash_info.inf + + +class PythonMPQ: + """Rational number implementation that is intended to be compatible with + gmpy2's mpq. + + Also slightly faster than fractions.Fraction. + + PythonMPQ should be treated as immutable although no effort is made to + prevent mutation (since that might slow down calculations). + """ + __slots__ = ('numerator', 'denominator') + + def __new__(cls, numerator, denominator=None): + """Construct PythonMPQ with gcd computation and checks""" + if denominator is not None: + # + # PythonMPQ(n, d): require n and d to be int and d != 0 + # + if isinstance(numerator, int) and isinstance(denominator, int): + # This is the slow part: + divisor = gcd(numerator, denominator) + numerator //= divisor + denominator //= divisor + return cls._new_check(numerator, denominator) + else: + # + # PythonMPQ(q) + # + # Here q can be PythonMPQ, int, Decimal, float, Fraction or str + # + if isinstance(numerator, int): + return cls._new(numerator, 1) + elif isinstance(numerator, PythonMPQ): + return cls._new(numerator.numerator, numerator.denominator) + + # Let Fraction handle Decimal/float conversion and str parsing + if isinstance(numerator, (Decimal, float, str)): + numerator = Fraction(numerator) + if isinstance(numerator, Fraction): + return cls._new(numerator.numerator, numerator.denominator) + # + # Reject everything else. This is more strict than mpq which allows + # things like mpq(Fraction, Fraction) or mpq(Decimal, any). The mpq + # behaviour is somewhat inconsistent so we choose to accept only a + # more strict subset of what mpq allows. + # + raise TypeError("PythonMPQ() requires numeric or string argument") + + @classmethod + def _new_check(cls, numerator, denominator): + """Construct PythonMPQ, check divide by zero and canonicalize signs""" + if not denominator: + raise ZeroDivisionError(f'Zero divisor {numerator}/{denominator}') + elif denominator < 0: + numerator = -numerator + denominator = -denominator + return cls._new(numerator, denominator) + + @classmethod + def _new(cls, numerator, denominator): + """Construct PythonMPQ efficiently (no checks)""" + obj = super().__new__(cls) + obj.numerator = numerator + obj.denominator = denominator + return obj + + def __int__(self): + """Convert to int (truncates towards zero)""" + p, q = self.numerator, self.denominator + if p < 0: + return -(-p//q) + return p//q + + def __float__(self): + """Convert to float (approximately)""" + return self.numerator / self.denominator + + def __bool__(self): + """True/False if nonzero/zero""" + return bool(self.numerator) + + def __eq__(self, other): + """Compare equal with PythonMPQ, int, float, Decimal or Fraction""" + if isinstance(other, PythonMPQ): + return (self.numerator == other.numerator + and self.denominator == other.denominator) + elif isinstance(other, self._compatible_types): + return self.__eq__(PythonMPQ(other)) + else: + return NotImplemented + + def __hash__(self): + """hash - same as mpq/Fraction""" + try: + dinv = pow(self.denominator, -1, _PyHASH_MODULUS) + except ValueError: + hash_ = _PyHASH_INF + else: + hash_ = hash(hash(abs(self.numerator)) * dinv) + result = hash_ if self.numerator >= 0 else -hash_ + return -2 if result == -1 else result + + def __reduce__(self): + """Deconstruct for pickling""" + return type(self), (self.numerator, self.denominator) + + def __str__(self): + """Convert to string""" + if self.denominator != 1: + return f"{self.numerator}/{self.denominator}" + else: + return f"{self.numerator}" + + def __repr__(self): + """Convert to string""" + return f"MPQ({self.numerator},{self.denominator})" + + def _cmp(self, other, op): + """Helper for lt/le/gt/ge""" + if not isinstance(other, self._compatible_types): + return NotImplemented + lhs = self.numerator * other.denominator + rhs = other.numerator * self.denominator + return op(lhs, rhs) + + def __lt__(self, other): + """self < other""" + return self._cmp(other, operator.lt) + + def __le__(self, other): + """self <= other""" + return self._cmp(other, operator.le) + + def __gt__(self, other): + """self > other""" + return self._cmp(other, operator.gt) + + def __ge__(self, other): + """self >= other""" + return self._cmp(other, operator.ge) + + def __abs__(self): + """abs(q)""" + return self._new(abs(self.numerator), self.denominator) + + def __pos__(self): + """+q""" + return self + + def __neg__(self): + """-q""" + return self._new(-self.numerator, self.denominator) + + def __add__(self, other): + """q1 + q2""" + if isinstance(other, PythonMPQ): + # + # This is much faster than the naive method used in the stdlib + # fractions module. Not sure where this method comes from + # though... + # + # Compare timings for something like: + # nums = range(1000) + # rats = [PythonMPQ(n, d) for n, d in zip(nums[:-5], nums[5:])] + # sum(rats) # <-- time this + # + ap, aq = self.numerator, self.denominator + bp, bq = other.numerator, other.denominator + g = gcd(aq, bq) + if g == 1: + p = ap*bq + aq*bp + q = bq*aq + else: + q1, q2 = aq//g, bq//g + p, q = ap*q2 + bp*q1, q1*q2 + g2 = gcd(p, g) + p, q = (p // g2), q * (g // g2) + + elif isinstance(other, int): + p = self.numerator + self.denominator * other + q = self.denominator + else: + return NotImplemented + + return self._new(p, q) + + def __radd__(self, other): + """z1 + q2""" + if isinstance(other, int): + p = self.numerator + self.denominator * other + q = self.denominator + return self._new(p, q) + else: + return NotImplemented + + def __sub__(self ,other): + """q1 - q2""" + if isinstance(other, PythonMPQ): + ap, aq = self.numerator, self.denominator + bp, bq = other.numerator, other.denominator + g = gcd(aq, bq) + if g == 1: + p = ap*bq - aq*bp + q = bq*aq + else: + q1, q2 = aq//g, bq//g + p, q = ap*q2 - bp*q1, q1*q2 + g2 = gcd(p, g) + p, q = (p // g2), q * (g // g2) + elif isinstance(other, int): + p = self.numerator - self.denominator*other + q = self.denominator + else: + return NotImplemented + + return self._new(p, q) + + def __rsub__(self, other): + """z1 - q2""" + if isinstance(other, int): + p = self.denominator * other - self.numerator + q = self.denominator + return self._new(p, q) + else: + return NotImplemented + + def __mul__(self, other): + """q1 * q2""" + if isinstance(other, PythonMPQ): + ap, aq = self.numerator, self.denominator + bp, bq = other.numerator, other.denominator + x1 = gcd(ap, bq) + x2 = gcd(bp, aq) + p, q = ((ap//x1)*(bp//x2), (aq//x2)*(bq//x1)) + elif isinstance(other, int): + x = gcd(other, self.denominator) + p = self.numerator*(other//x) + q = self.denominator//x + else: + return NotImplemented + + return self._new(p, q) + + def __rmul__(self, other): + """z1 * q2""" + if isinstance(other, int): + x = gcd(self.denominator, other) + p = self.numerator*(other//x) + q = self.denominator//x + return self._new(p, q) + else: + return NotImplemented + + def __pow__(self, exp): + """q ** z""" + p, q = self.numerator, self.denominator + + if exp < 0: + p, q, exp = q, p, -exp + + return self._new_check(p**exp, q**exp) + + def __truediv__(self, other): + """q1 / q2""" + if isinstance(other, PythonMPQ): + ap, aq = self.numerator, self.denominator + bp, bq = other.numerator, other.denominator + x1 = gcd(ap, bp) + x2 = gcd(bq, aq) + p, q = ((ap//x1)*(bq//x2), (aq//x2)*(bp//x1)) + elif isinstance(other, int): + x = gcd(other, self.numerator) + p = self.numerator//x + q = self.denominator*(other//x) + else: + return NotImplemented + + return self._new_check(p, q) + + def __rtruediv__(self, other): + """z / q""" + if isinstance(other, int): + x = gcd(self.numerator, other) + p = self.denominator*(other//x) + q = self.numerator//x + return self._new_check(p, q) + else: + return NotImplemented + + _compatible_types: tuple[Type, ...] = () + +# +# These are the types that PythonMPQ will interoperate with for operations +# and comparisons such as ==, + etc. We define this down here so that we can +# include PythonMPQ in the list as well. +# +PythonMPQ._compatible_types = (PythonMPQ, int, Decimal, Fraction) diff --git a/venv/lib/python3.11/site-packages/sympy/external/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/external/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/external/tests/test_autowrap.py b/venv/lib/python3.11/site-packages/sympy/external/tests/test_autowrap.py new file mode 100644 index 0000000000000000000000000000000000000000..d469b552995b7625f786f3296089e41f42da75cb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/external/tests/test_autowrap.py @@ -0,0 +1,313 @@ +import sympy +import tempfile +import os +from pathlib import Path +from sympy.core.mod import Mod +from sympy.core.relational import Eq +from sympy.core.symbol import symbols +from sympy.external import import_module +from sympy.tensor import IndexedBase, Idx +from sympy.utilities.autowrap import autowrap, ufuncify, CodeWrapError +from sympy.testing.pytest import skip + +numpy = import_module('numpy', min_module_version='1.6.1') +Cython = import_module('Cython', min_module_version='0.15.1') +f2py = import_module('numpy.f2py', import_kwargs={'fromlist': ['f2py']}) + +f2pyworks = False +if f2py: + try: + autowrap(symbols('x'), 'f95', 'f2py') + except (CodeWrapError, ImportError, OSError): + f2pyworks = False + else: + f2pyworks = True + +a, b, c = symbols('a b c') +n, m, d = symbols('n m d', integer=True) +A, B, C = symbols('A B C', cls=IndexedBase) +i = Idx('i', m) +j = Idx('j', n) +k = Idx('k', d) + + +def has_module(module): + """ + Return True if module exists, otherwise run skip(). + + module should be a string. + """ + # To give a string of the module name to skip(), this function takes a + # string. So we don't waste time running import_module() more than once, + # just map the three modules tested here in this dict. + modnames = {'numpy': numpy, 'Cython': Cython, 'f2py': f2py} + + if modnames[module]: + if module == 'f2py' and not f2pyworks: + skip("Couldn't run f2py.") + return True + skip("Couldn't import %s." % module) + +# +# test runners used by several language-backend combinations +# + +def runtest_autowrap_twice(language, backend): + f = autowrap((((a + b)/c)**5).expand(), language, backend) + g = autowrap((((a + b)/c)**4).expand(), language, backend) + + # check that autowrap updates the module name. Else, g gives the same as f + assert f(1, -2, 1) == -1.0 + assert g(1, -2, 1) == 1.0 + + +def runtest_autowrap_trace(language, backend): + has_module('numpy') + trace = autowrap(A[i, i], language, backend) + assert trace(numpy.eye(100)) == 100 + + +def runtest_autowrap_matrix_vector(language, backend): + has_module('numpy') + x, y = symbols('x y', cls=IndexedBase) + expr = Eq(y[i], A[i, j]*x[j]) + mv = autowrap(expr, language, backend) + + # compare with numpy's dot product + M = numpy.random.rand(10, 20) + x = numpy.random.rand(20) + y = numpy.dot(M, x) + assert numpy.sum(numpy.abs(y - mv(M, x))) < 1e-13 + + +def runtest_autowrap_matrix_matrix(language, backend): + has_module('numpy') + expr = Eq(C[i, j], A[i, k]*B[k, j]) + matmat = autowrap(expr, language, backend) + + # compare with numpy's dot product + M1 = numpy.random.rand(10, 20) + M2 = numpy.random.rand(20, 15) + M3 = numpy.dot(M1, M2) + assert numpy.sum(numpy.abs(M3 - matmat(M1, M2))) < 1e-13 + + +def runtest_ufuncify(language, backend): + has_module('numpy') + a, b, c = symbols('a b c') + fabc = ufuncify([a, b, c], a*b + c, backend=backend) + facb = ufuncify([a, c, b], a*b + c, backend=backend) + grid = numpy.linspace(-2, 2, 50) + b = numpy.linspace(-5, 4, 50) + c = numpy.linspace(-1, 1, 50) + expected = grid*b + c + numpy.testing.assert_allclose(fabc(grid, b, c), expected) + numpy.testing.assert_allclose(facb(grid, c, b), expected) + + +def runtest_issue_10274(language, backend): + expr = (a - b + c)**(13) + tmp = tempfile.mkdtemp() + f = autowrap(expr, language, backend, tempdir=tmp, + helpers=('helper', a - b + c, (a, b, c))) + assert f(1, 1, 1) == 1 + + for file in os.listdir(tmp): + if not (file.startswith("wrapped_code_") and file.endswith(".c")): + continue + + with open(tmp + '/' + file) as fil: + lines = fil.readlines() + assert lines[0] == "/******************************************************************************\n" + assert "Code generated with SymPy " + sympy.__version__ in lines[1] + assert lines[2:] == [ + " * *\n", + " * See http://www.sympy.org/ for more information. *\n", + " * *\n", + " * This file is part of 'autowrap' *\n", + " ******************************************************************************/\n", + "#include " + '"' + file[:-1]+ 'h"' + "\n", + "#include \n", + "\n", + "double helper(double a, double b, double c) {\n", + "\n", + " double helper_result;\n", + " helper_result = a - b + c;\n", + " return helper_result;\n", + "\n", + "}\n", + "\n", + "double autofunc(double a, double b, double c) {\n", + "\n", + " double autofunc_result;\n", + " autofunc_result = pow(helper(a, b, c), 13);\n", + " return autofunc_result;\n", + "\n", + "}\n", + ] + + +def runtest_issue_15337(language, backend): + has_module('numpy') + # NOTE : autowrap was originally designed to only accept an iterable for + # the kwarg "helpers", but in issue 10274 the user mistakenly thought that + # if there was only a single helper it did not need to be passed via an + # iterable that wrapped the helper tuple. There were no tests for this + # behavior so when the code was changed to accept a single tuple it broke + # the original behavior. These tests below ensure that both now work. + a, b, c, d, e = symbols('a, b, c, d, e') + expr = (a - b + c - d + e)**13 + exp_res = (1. - 2. + 3. - 4. + 5.)**13 + + f = autowrap(expr, language, backend, args=(a, b, c, d, e), + helpers=('f1', a - b + c, (a, b, c))) + numpy.testing.assert_allclose(f(1, 2, 3, 4, 5), exp_res) + + f = autowrap(expr, language, backend, args=(a, b, c, d, e), + helpers=(('f1', a - b, (a, b)), ('f2', c - d, (c, d)))) + numpy.testing.assert_allclose(f(1, 2, 3, 4, 5), exp_res) + + +def test_issue_15230(): + has_module('f2py') + + x, y = symbols('x, y') + expr = Mod(x, 3.0) - Mod(y, -2.0) + f = autowrap(expr, args=[x, y], language='F95') + exp_res = float(expr.xreplace({x: 3.5, y: 2.7}).evalf()) + assert abs(f(3.5, 2.7) - exp_res) < 1e-14 + + x, y = symbols('x, y', integer=True) + expr = Mod(x, 3) - Mod(y, -2) + f = autowrap(expr, args=[x, y], language='F95') + assert f(3, 2) == expr.xreplace({x: 3, y: 2}) + +# +# tests of language-backend combinations +# + +# f2py + + +def test_wrap_twice_f95_f2py(): + has_module('f2py') + runtest_autowrap_twice('f95', 'f2py') + + +def test_autowrap_trace_f95_f2py(): + has_module('f2py') + runtest_autowrap_trace('f95', 'f2py') + + +def test_autowrap_matrix_vector_f95_f2py(): + has_module('f2py') + runtest_autowrap_matrix_vector('f95', 'f2py') + + +def test_autowrap_matrix_matrix_f95_f2py(): + has_module('f2py') + runtest_autowrap_matrix_matrix('f95', 'f2py') + + +def test_ufuncify_f95_f2py(): + has_module('f2py') + runtest_ufuncify('f95', 'f2py') + + +def test_issue_15337_f95_f2py(): + has_module('f2py') + runtest_issue_15337('f95', 'f2py') + +# Cython + + +def test_wrap_twice_c_cython(): + has_module('Cython') + runtest_autowrap_twice('C', 'cython') + + +def test_autowrap_trace_C_Cython(): + has_module('Cython') + runtest_autowrap_trace('C99', 'cython') + + +def test_autowrap_matrix_vector_C_cython(): + has_module('Cython') + runtest_autowrap_matrix_vector('C99', 'cython') + + +def test_autowrap_matrix_matrix_C_cython(): + has_module('Cython') + runtest_autowrap_matrix_matrix('C99', 'cython') + + +def test_ufuncify_C_Cython(): + has_module('Cython') + runtest_ufuncify('C99', 'cython') + + +def test_issue_10274_C_cython(): + has_module('Cython') + runtest_issue_10274('C89', 'cython') + + +def test_issue_15337_C_cython(): + has_module('Cython') + runtest_issue_15337('C89', 'cython') + + +def test_autowrap_custom_printer(): + has_module('Cython') + + from sympy.core.numbers import pi + from sympy.utilities.codegen import C99CodeGen + from sympy.printing.c import C99CodePrinter + + class PiPrinter(C99CodePrinter): + def _print_Pi(self, expr): + return "S_PI" + + printer = PiPrinter() + gen = C99CodeGen(printer=printer) + gen.preprocessor_statements.append('#include "shortpi.h"') + + expr = pi * a + + expected = ( + '#include "%s"\n' + '#include \n' + '#include "shortpi.h"\n' + '\n' + 'double autofunc(double a) {\n' + '\n' + ' double autofunc_result;\n' + ' autofunc_result = S_PI*a;\n' + ' return autofunc_result;\n' + '\n' + '}\n' + ) + + tmpdir = tempfile.mkdtemp() + # write a trivial header file to use in the generated code + Path(os.path.join(tmpdir, 'shortpi.h')).write_text('#define S_PI 3.14') + + func = autowrap(expr, backend='cython', tempdir=tmpdir, code_gen=gen) + + assert func(4.2) == 3.14 * 4.2 + + # check that the generated code is correct + for filename in os.listdir(tmpdir): + if filename.startswith('wrapped_code') and filename.endswith('.c'): + with open(os.path.join(tmpdir, filename)) as f: + lines = f.readlines() + expected = expected % filename.replace('.c', '.h') + assert ''.join(lines[7:]) == expected + + +# Numpy + +def test_ufuncify_numpy(): + # This test doesn't use Cython, but if Cython works, then there is a valid + # C compiler, which is needed. + has_module('Cython') + runtest_ufuncify('C99', 'numpy') diff --git a/venv/lib/python3.11/site-packages/sympy/external/tests/test_codegen.py b/venv/lib/python3.11/site-packages/sympy/external/tests/test_codegen.py new file mode 100644 index 0000000000000000000000000000000000000000..8a4fe28300b86fb0b38d98fcf2fcbbe514cf720f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/external/tests/test_codegen.py @@ -0,0 +1,375 @@ +# This tests the compilation and execution of the source code generated with +# utilities.codegen. The compilation takes place in a temporary directory that +# is removed after the test. By default the test directory is always removed, +# but this behavior can be changed by setting the environment variable +# SYMPY_TEST_CLEAN_TEMP to: +# export SYMPY_TEST_CLEAN_TEMP=always : the default behavior. +# export SYMPY_TEST_CLEAN_TEMP=success : only remove the directories of working tests. +# export SYMPY_TEST_CLEAN_TEMP=never : never remove the directories with the test code. +# When a directory is not removed, the necessary information is printed on +# screen to find the files that belong to the (failed) tests. If a test does +# not fail, py.test captures all the output and you will not see the directories +# corresponding to the successful tests. Use the --nocapture option to see all +# the output. + +# All tests below have a counterpart in utilities/test/test_codegen.py. In the +# latter file, the resulting code is compared with predefined strings, without +# compilation or execution. + +# All the generated Fortran code should conform with the Fortran 95 standard, +# and all the generated C code should be ANSI C, which facilitates the +# incorporation in various projects. The tests below assume that the binary cc +# is somewhere in the path and that it can compile ANSI C code. + +from sympy.abc import x, y, z +from sympy.testing.pytest import IS_WASM, skip +from sympy.utilities.codegen import codegen, make_routine, get_code_generator +import sys +import os +import tempfile +import subprocess +from pathlib import Path + + +# templates for the main program that will test the generated code. + +main_template = {} +main_template['F95'] = """ +program main + include "codegen.h" + integer :: result; + result = 0 + + %(statements)s + + call exit(result) +end program +""" + +main_template['C89'] = """ +#include "codegen.h" +#include +#include + +int main() { + int result = 0; + + %(statements)s + + return result; +} +""" +main_template['C99'] = main_template['C89'] +# templates for the numerical tests + +numerical_test_template = {} +numerical_test_template['C89'] = """ + if (fabs(%(call)s)>%(threshold)s) { + printf("Numerical validation failed: %(call)s=%%e threshold=%(threshold)s\\n", %(call)s); + result = -1; + } +""" +numerical_test_template['C99'] = numerical_test_template['C89'] + +numerical_test_template['F95'] = """ + if (abs(%(call)s)>%(threshold)s) then + write(6,"('Numerical validation failed:')") + write(6,"('%(call)s=',e15.5,'threshold=',e15.5)") %(call)s, %(threshold)s + result = -1; + end if +""" +# command sequences for supported compilers + +compile_commands = {} +compile_commands['cc'] = [ + "cc -c codegen.c -o codegen.o", + "cc -c main.c -o main.o", + "cc main.o codegen.o -lm -o test.exe" +] + +compile_commands['gfortran'] = [ + "gfortran -c codegen.f90 -o codegen.o", + "gfortran -ffree-line-length-none -c main.f90 -o main.o", + "gfortran main.o codegen.o -o test.exe" +] + +compile_commands['g95'] = [ + "g95 -c codegen.f90 -o codegen.o", + "g95 -ffree-line-length-huge -c main.f90 -o main.o", + "g95 main.o codegen.o -o test.exe" +] + +compile_commands['ifort'] = [ + "ifort -c codegen.f90 -o codegen.o", + "ifort -c main.f90 -o main.o", + "ifort main.o codegen.o -o test.exe" +] + +combinations_lang_compiler = [ + ('C89', 'cc'), + ('C99', 'cc'), + ('F95', 'ifort'), + ('F95', 'gfortran'), + ('F95', 'g95') +] + +def try_run(commands): + """Run a series of commands and only return True if all ran fine.""" + if IS_WASM: + return False + with open(os.devnull, 'w') as null: + for command in commands: + retcode = subprocess.call(command, stdout=null, shell=True, + stderr=subprocess.STDOUT) + if retcode != 0: + return False + return True + + +def run_test(label, routines, numerical_tests, language, commands, friendly=True): + """A driver for the codegen tests. + + This driver assumes that a compiler ifort is present in the PATH and that + ifort is (at least) a Fortran 90 compiler. The generated code is written in + a temporary directory, together with a main program that validates the + generated code. The test passes when the compilation and the validation + run correctly. + """ + + # Check input arguments before touching the file system + language = language.upper() + assert language in main_template + assert language in numerical_test_template + + # Check that environment variable makes sense + clean = os.getenv('SYMPY_TEST_CLEAN_TEMP', 'always').lower() + if clean not in ('always', 'success', 'never'): + raise ValueError("SYMPY_TEST_CLEAN_TEMP must be one of the following: 'always', 'success' or 'never'.") + + # Do all the magic to compile, run and validate the test code + # 1) prepare the temporary working directory, switch to that dir + work = tempfile.mkdtemp("_sympy_%s_test" % language, "%s_" % label) + oldwork = os.getcwd() + os.chdir(work) + + # 2) write the generated code + if friendly: + # interpret the routines as a name_expr list and call the friendly + # function codegen + codegen(routines, language, "codegen", to_files=True) + else: + code_gen = get_code_generator(language, "codegen") + code_gen.write(routines, "codegen", to_files=True) + + # 3) write a simple main program that links to the generated code, and that + # includes the numerical tests + test_strings = [] + for fn_name, args, expected, threshold in numerical_tests: + call_string = "%s(%s)-(%s)" % ( + fn_name, ",".join(str(arg) for arg in args), expected) + if language == "F95": + call_string = fortranize_double_constants(call_string) + threshold = fortranize_double_constants(str(threshold)) + test_strings.append(numerical_test_template[language] % { + "call": call_string, + "threshold": threshold, + }) + + if language == "F95": + f_name = "main.f90" + elif language.startswith("C"): + f_name = "main.c" + else: + raise NotImplementedError( + "FIXME: filename extension unknown for language: %s" % language) + + Path(f_name).write_text( + main_template[language] % {'statements': "".join(test_strings)}) + + # 4) Compile and link + compiled = try_run(commands) + + # 5) Run if compiled + if compiled: + executed = try_run(["./test.exe"]) + else: + executed = False + + # 6) Clean up stuff + if clean == 'always' or (clean == 'success' and compiled and executed): + def safe_remove(filename): + if os.path.isfile(filename): + os.remove(filename) + safe_remove("codegen.f90") + safe_remove("codegen.c") + safe_remove("codegen.h") + safe_remove("codegen.o") + safe_remove("main.f90") + safe_remove("main.c") + safe_remove("main.o") + safe_remove("test.exe") + os.chdir(oldwork) + os.rmdir(work) + else: + print("TEST NOT REMOVED: %s" % work, file=sys.stderr) + os.chdir(oldwork) + + # 7) Do the assertions in the end + assert compiled, "failed to compile %s code with:\n%s" % ( + language, "\n".join(commands)) + assert executed, "failed to execute %s code from:\n%s" % ( + language, "\n".join(commands)) + + +def fortranize_double_constants(code_string): + """ + Replaces every literal float with literal doubles + """ + import re + pattern_exp = re.compile(r'\d+(\.)?\d*[eE]-?\d+') + pattern_float = re.compile(r'\d+\.\d*(?!\d*d)') + + def subs_exp(matchobj): + return re.sub('[eE]', 'd', matchobj.group(0)) + + def subs_float(matchobj): + return "%sd0" % matchobj.group(0) + + code_string = pattern_exp.sub(subs_exp, code_string) + code_string = pattern_float.sub(subs_float, code_string) + + return code_string + + +def is_feasible(language, commands): + # This test should always work, otherwise the compiler is not present. + routine = make_routine("test", x) + numerical_tests = [ + ("test", ( 1.0,), 1.0, 1e-15), + ("test", (-1.0,), -1.0, 1e-15), + ] + try: + run_test("is_feasible", [routine], numerical_tests, language, commands, + friendly=False) + return True + except AssertionError: + return False + +valid_lang_commands = [] +invalid_lang_compilers = [] +for lang, compiler in combinations_lang_compiler: + commands = compile_commands[compiler] + if is_feasible(lang, commands): + valid_lang_commands.append((lang, commands)) + else: + invalid_lang_compilers.append((lang, compiler)) + +# We test all language-compiler combinations, just to report what is skipped + +def test_C89_cc(): + if ("C89", 'cc') in invalid_lang_compilers: + skip("`cc' command didn't work as expected (C89)") + + +def test_C99_cc(): + if ("C99", 'cc') in invalid_lang_compilers: + skip("`cc' command didn't work as expected (C99)") + + +def test_F95_ifort(): + if ("F95", 'ifort') in invalid_lang_compilers: + skip("`ifort' command didn't work as expected") + + +def test_F95_gfortran(): + if ("F95", 'gfortran') in invalid_lang_compilers: + skip("`gfortran' command didn't work as expected") + + +def test_F95_g95(): + if ("F95", 'g95') in invalid_lang_compilers: + skip("`g95' command didn't work as expected") + +# Here comes the actual tests + + +def test_basic_codegen(): + numerical_tests = [ + ("test", (1.0, 6.0, 3.0), 21.0, 1e-15), + ("test", (-1.0, 2.0, -2.5), -2.5, 1e-15), + ] + name_expr = [("test", (x + y)*z)] + for lang, commands in valid_lang_commands: + run_test("basic_codegen", name_expr, numerical_tests, lang, commands) + + +def test_intrinsic_math1_codegen(): + # not included: log10 + from sympy.core.evalf import N + from sympy.functions import ln + from sympy.functions.elementary.exponential import log + from sympy.functions.elementary.hyperbolic import (cosh, sinh, tanh) + from sympy.functions.elementary.integers import (ceiling, floor) + from sympy.functions.elementary.miscellaneous import sqrt + from sympy.functions.elementary.trigonometric import (acos, asin, atan, cos, sin, tan) + name_expr = [ + ("test_fabs", abs(x)), + ("test_acos", acos(x)), + ("test_asin", asin(x)), + ("test_atan", atan(x)), + ("test_cos", cos(x)), + ("test_cosh", cosh(x)), + ("test_log", log(x)), + ("test_ln", ln(x)), + ("test_sin", sin(x)), + ("test_sinh", sinh(x)), + ("test_sqrt", sqrt(x)), + ("test_tan", tan(x)), + ("test_tanh", tanh(x)), + ] + numerical_tests = [] + for name, expr in name_expr: + for xval in 0.2, 0.5, 0.8: + expected = N(expr.subs(x, xval)) + numerical_tests.append((name, (xval,), expected, 1e-14)) + for lang, commands in valid_lang_commands: + if lang.startswith("C"): + name_expr_C = [("test_floor", floor(x)), ("test_ceil", ceiling(x))] + else: + name_expr_C = [] + run_test("intrinsic_math1", name_expr + name_expr_C, + numerical_tests, lang, commands) + + +def test_instrinsic_math2_codegen(): + # not included: frexp, ldexp, modf, fmod + from sympy.core.evalf import N + from sympy.functions.elementary.trigonometric import atan2 + name_expr = [ + ("test_atan2", atan2(x, y)), + ("test_pow", x**y), + ] + numerical_tests = [] + for name, expr in name_expr: + for xval, yval in (0.2, 1.3), (0.5, -0.2), (0.8, 0.8): + expected = N(expr.subs(x, xval).subs(y, yval)) + numerical_tests.append((name, (xval, yval), expected, 1e-14)) + for lang, commands in valid_lang_commands: + run_test("intrinsic_math2", name_expr, numerical_tests, lang, commands) + + +def test_complicated_codegen(): + from sympy.core.evalf import N + from sympy.functions.elementary.trigonometric import (cos, sin, tan) + name_expr = [ + ("test1", ((sin(x) + cos(y) + tan(z))**7).expand()), + ("test2", cos(cos(cos(cos(cos(cos(cos(cos(x + y + z))))))))), + ] + numerical_tests = [] + for name, expr in name_expr: + for xval, yval, zval in (0.2, 1.3, -0.3), (0.5, -0.2, 0.0), (0.8, 2.1, 0.8): + expected = N(expr.subs(x, xval).subs(y, yval).subs(z, zval)) + numerical_tests.append((name, (xval, yval, zval), expected, 1e-12)) + for lang, commands in valid_lang_commands: + run_test( + "complicated_codegen", name_expr, numerical_tests, lang, commands) diff --git a/venv/lib/python3.11/site-packages/sympy/external/tests/test_gmpy.py b/venv/lib/python3.11/site-packages/sympy/external/tests/test_gmpy.py new file mode 100644 index 0000000000000000000000000000000000000000..d88f9da0c6c26c15f529ce485fff5b72342170ea --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/external/tests/test_gmpy.py @@ -0,0 +1,12 @@ +from sympy.external.gmpy import LONG_MAX, iroot +from sympy.testing.pytest import raises + + +def test_iroot(): + assert iroot(2, LONG_MAX) == (1, False) + assert iroot(2, LONG_MAX + 1) == (1, False) + for x in range(3): + assert iroot(x, 1) == (x, True) + raises(ValueError, lambda: iroot(-1, 1)) + raises(ValueError, lambda: iroot(0, 0)) + raises(ValueError, lambda: iroot(0, -1)) diff --git a/venv/lib/python3.11/site-packages/sympy/external/tests/test_importtools.py b/venv/lib/python3.11/site-packages/sympy/external/tests/test_importtools.py new file mode 100644 index 0000000000000000000000000000000000000000..0b954070c179282ed2bcf5735d802c5f22a3a261 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/external/tests/test_importtools.py @@ -0,0 +1,40 @@ +from sympy.external import import_module +from sympy.testing.pytest import warns + +# fixes issue that arose in addressing issue 6533 +def test_no_stdlib_collections(): + ''' + make sure we get the right collections when it is not part of a + larger list + ''' + import collections + matplotlib = import_module('matplotlib', + import_kwargs={'fromlist': ['cm', 'collections']}, + min_module_version='1.1.0', catch=(RuntimeError,)) + if matplotlib: + assert collections != matplotlib.collections + +def test_no_stdlib_collections2(): + ''' + make sure we get the right collections when it is not part of a + larger list + ''' + import collections + matplotlib = import_module('matplotlib', + import_kwargs={'fromlist': ['collections']}, + min_module_version='1.1.0', catch=(RuntimeError,)) + if matplotlib: + assert collections != matplotlib.collections + +def test_no_stdlib_collections3(): + '''make sure we get the right collections with no catch''' + import collections + matplotlib = import_module('matplotlib', + import_kwargs={'fromlist': ['cm', 'collections']}, + min_module_version='1.1.0') + if matplotlib: + assert collections != matplotlib.collections + +def test_min_module_version_python3_basestring_error(): + with warns(UserWarning): + import_module('mpmath', min_module_version='1000.0.1') diff --git a/venv/lib/python3.11/site-packages/sympy/external/tests/test_ntheory.py b/venv/lib/python3.11/site-packages/sympy/external/tests/test_ntheory.py new file mode 100644 index 0000000000000000000000000000000000000000..00824481ad27aa9071ea5801fb3bde75cacbc3c8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/external/tests/test_ntheory.py @@ -0,0 +1,307 @@ +from itertools import permutations + +from sympy.external.ntheory import (bit_scan1, remove, bit_scan0, is_fermat_prp, + is_euler_prp, is_strong_prp, gcdext, _lucas_sequence, + is_fibonacci_prp, is_lucas_prp, is_selfridge_prp, + is_strong_lucas_prp, is_strong_selfridge_prp, + is_bpsw_prp, is_strong_bpsw_prp) +from sympy.testing.pytest import raises + + +def test_bit_scan1(): + assert bit_scan1(0) is None + assert bit_scan1(1) == 0 + assert bit_scan1(-1) == 0 + assert bit_scan1(2) == 1 + assert bit_scan1(7) == 0 + assert bit_scan1(-7) == 0 + for i in range(100): + assert bit_scan1(1 << i) == i + assert bit_scan1((1 << i) * 31337) == i + for i in range(500): + n = (1 << 500) + (1 << i) + assert bit_scan1(n) == i + assert bit_scan1(1 << 1000001) == 1000001 + assert bit_scan1((1 << 273956)*7**37) == 273956 + # issue 12709 + for i in range(1, 10): + big = 1 << i + assert bit_scan1(-big) == bit_scan1(big) + + +def test_bit_scan0(): + assert bit_scan0(-1) is None + assert bit_scan0(0) == 0 + assert bit_scan0(1) == 1 + assert bit_scan0(-2) == 0 + + +def test_remove(): + raises(ValueError, lambda: remove(1, 1)) + assert remove(0, 3) == (0, 0) + for f in range(2, 10): + for y in range(2, 1000): + for z in [1, 17, 101, 1009]: + assert remove(z*f**y, f) == (z, y) + + +def test_gcdext(): + assert gcdext(0, 0) == (0, 0, 0) + assert gcdext(3, 0) == (3, 1, 0) + assert gcdext(0, 4) == (4, 0, 1) + + for n in range(1, 10): + assert gcdext(n, 1) == gcdext(-n, 1) == (1, 0, 1) + assert gcdext(n, -1) == gcdext(-n, -1) == (1, 0, -1) + assert gcdext(n, n) == gcdext(-n, n) == (n, 0, 1) + assert gcdext(n, -n) == gcdext(-n, -n) == (n, 0, -1) + + for n in range(2, 10): + assert gcdext(1, n) == gcdext(1, -n) == (1, 1, 0) + assert gcdext(-1, n) == gcdext(-1, -n) == (1, -1, 0) + + for a, b in permutations([2**5, 3, 5, 7**2, 11], 2): + g, x, y = gcdext(a, b) + assert g == a*x + b*y == 1 + + +def test_is_fermat_prp(): + # invalid input + raises(ValueError, lambda: is_fermat_prp(0, 10)) + raises(ValueError, lambda: is_fermat_prp(5, 1)) + + # n = 1 + assert not is_fermat_prp(1, 3) + + # n is prime + assert is_fermat_prp(2, 4) + assert is_fermat_prp(3, 2) + assert is_fermat_prp(11, 3) + assert is_fermat_prp(2**31-1, 5) + + # A001567 + pseudorpime = [341, 561, 645, 1105, 1387, 1729, 1905, 2047, + 2465, 2701, 2821, 3277, 4033, 4369, 4371, 4681] + for n in pseudorpime: + assert is_fermat_prp(n, 2) + + # A020136 + pseudorpime = [15, 85, 91, 341, 435, 451, 561, 645, 703, 1105, + 1247, 1271, 1387, 1581, 1695, 1729, 1891, 1905] + for n in pseudorpime: + assert is_fermat_prp(n, 4) + + +def test_is_euler_prp(): + # invalid input + raises(ValueError, lambda: is_euler_prp(0, 10)) + raises(ValueError, lambda: is_euler_prp(5, 1)) + + # n = 1 + assert not is_euler_prp(1, 3) + + # n is prime + assert is_euler_prp(2, 4) + assert is_euler_prp(3, 2) + assert is_euler_prp(11, 3) + assert is_euler_prp(2**31-1, 5) + + # A047713 + pseudorpime = [561, 1105, 1729, 1905, 2047, 2465, 3277, 4033, + 4681, 6601, 8321, 8481, 10585, 12801, 15841] + for n in pseudorpime: + assert is_euler_prp(n, 2) + + # A048950 + pseudorpime = [121, 703, 1729, 1891, 2821, 3281, 7381, 8401, + 8911, 10585, 12403, 15457, 15841, 16531, 18721] + for n in pseudorpime: + assert is_euler_prp(n, 3) + + +def test_is_strong_prp(): + # invalid input + raises(ValueError, lambda: is_strong_prp(0, 10)) + raises(ValueError, lambda: is_strong_prp(5, 1)) + + # n = 1 + assert not is_strong_prp(1, 3) + + # n is prime + assert is_strong_prp(2, 4) + assert is_strong_prp(3, 2) + assert is_strong_prp(11, 3) + assert is_strong_prp(2**31-1, 5) + + # A001262 + pseudorpime = [2047, 3277, 4033, 4681, 8321, 15841, 29341, + 42799, 49141, 52633, 65281, 74665, 80581] + for n in pseudorpime: + assert is_strong_prp(n, 2) + + # A020229 + pseudorpime = [121, 703, 1891, 3281, 8401, 8911, 10585, 12403, + 16531, 18721, 19345, 23521, 31621, 44287, 47197] + for n in pseudorpime: + assert is_strong_prp(n, 3) + + +def test_lucas_sequence(): + def lucas_u(P, Q, length): + array = [0] * length + array[1] = 1 + for k in range(2, length): + array[k] = P * array[k - 1] - Q * array[k - 2] + return array + + def lucas_v(P, Q, length): + array = [0] * length + array[0] = 2 + array[1] = P + for k in range(2, length): + array[k] = P * array[k - 1] - Q * array[k - 2] + return array + + length = 20 + for P in range(-10, 10): + for Q in range(-10, 10): + D = P**2 - 4*Q + if D == 0: + continue + us = lucas_u(P, Q, length) + vs = lucas_v(P, Q, length) + for n in range(3, 100, 2): + for k in range(length): + U, V, Qk = _lucas_sequence(n, P, Q, k) + assert U == us[k] % n + assert V == vs[k] % n + assert pow(Q, k, n) == Qk + + +def test_is_fibonacci_prp(): + # invalid input + raises(ValueError, lambda: is_fibonacci_prp(3, 2, 1)) + raises(ValueError, lambda: is_fibonacci_prp(3, -5, 1)) + raises(ValueError, lambda: is_fibonacci_prp(3, 5, 2)) + raises(ValueError, lambda: is_fibonacci_prp(0, 5, -1)) + + # n = 1 + assert not is_fibonacci_prp(1, 3, 1) + + # n is prime + assert is_fibonacci_prp(2, 5, 1) + assert is_fibonacci_prp(3, 6, -1) + assert is_fibonacci_prp(11, 7, 1) + assert is_fibonacci_prp(2**31-1, 8, -1) + + # A005845 + pseudorpime = [705, 2465, 2737, 3745, 4181, 5777, 6721, + 10877, 13201, 15251, 24465, 29281, 34561] + for n in pseudorpime: + assert is_fibonacci_prp(n, 1, -1) + + +def test_is_lucas_prp(): + # invalid input + raises(ValueError, lambda: is_lucas_prp(3, 2, 1)) + raises(ValueError, lambda: is_lucas_prp(0, 5, -1)) + raises(ValueError, lambda: is_lucas_prp(15, 3, 1)) + + # n = 1 + assert not is_lucas_prp(1, 3, 1) + + # n is prime + assert is_lucas_prp(2, 5, 2) + assert is_lucas_prp(3, 6, -1) + assert is_lucas_prp(11, 7, 5) + assert is_lucas_prp(2**31-1, 8, -3) + + # A081264 + pseudorpime = [323, 377, 1891, 3827, 4181, 5777, 6601, 6721, + 8149, 10877, 11663, 13201, 13981, 15251, 17119] + for n in pseudorpime: + assert is_lucas_prp(n, 1, -1) + + +def test_is_selfridge_prp(): + # invalid input + raises(ValueError, lambda: is_selfridge_prp(0)) + + # n = 1 + assert not is_selfridge_prp(1) + + # n is prime + assert is_selfridge_prp(2) + assert is_selfridge_prp(3) + assert is_selfridge_prp(11) + assert is_selfridge_prp(2**31-1) + + # A217120 + pseudorpime = [323, 377, 1159, 1829, 3827, 5459, 5777, 9071, + 9179, 10877, 11419, 11663, 13919, 14839, 16109] + for n in pseudorpime: + assert is_selfridge_prp(n) + + +def test_is_strong_lucas_prp(): + # invalid input + raises(ValueError, lambda: is_strong_lucas_prp(3, 2, 1)) + raises(ValueError, lambda: is_strong_lucas_prp(0, 5, -1)) + raises(ValueError, lambda: is_strong_lucas_prp(15, 3, 1)) + + # n = 1 + assert not is_strong_lucas_prp(1, 3, 1) + + # n is prime + assert is_strong_lucas_prp(2, 5, 2) + assert is_strong_lucas_prp(3, 6, -1) + assert is_strong_lucas_prp(11, 7, 5) + assert is_strong_lucas_prp(2**31-1, 8, -3) + + +def test_is_strong_selfridge_prp(): + # invalid input + raises(ValueError, lambda: is_strong_selfridge_prp(0)) + + # n = 1 + assert not is_strong_selfridge_prp(1) + + # n is prime + assert is_strong_selfridge_prp(2) + assert is_strong_selfridge_prp(3) + assert is_strong_selfridge_prp(11) + assert is_strong_selfridge_prp(2**31-1) + + # A217255 + pseudorpime = [5459, 5777, 10877, 16109, 18971, 22499, 24569, + 25199, 40309, 58519, 75077, 97439, 100127, 113573] + for n in pseudorpime: + assert is_strong_selfridge_prp(n) + + +def test_is_bpsw_prp(): + # invalid input + raises(ValueError, lambda: is_bpsw_prp(0)) + + # n = 1 + assert not is_bpsw_prp(1) + + # n is prime + assert is_bpsw_prp(2) + assert is_bpsw_prp(3) + assert is_bpsw_prp(11) + assert is_bpsw_prp(2**31-1) + + +def test_is_strong_bpsw_prp(): + # invalid input + raises(ValueError, lambda: is_strong_bpsw_prp(0)) + + # n = 1 + assert not is_strong_bpsw_prp(1) + + # n is prime + assert is_strong_bpsw_prp(2) + assert is_strong_bpsw_prp(3) + assert is_strong_bpsw_prp(11) + assert is_strong_bpsw_prp(2**31-1) diff --git a/venv/lib/python3.11/site-packages/sympy/external/tests/test_numpy.py b/venv/lib/python3.11/site-packages/sympy/external/tests/test_numpy.py new file mode 100644 index 0000000000000000000000000000000000000000..cd456d0d6cc49138c29d7ab28ee02694448d578f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/external/tests/test_numpy.py @@ -0,0 +1,335 @@ +# This testfile tests SymPy <-> NumPy compatibility + +# Don't test any SymPy features here. Just pure interaction with NumPy. +# Always write regular SymPy tests for anything, that can be tested in pure +# Python (without numpy). Here we test everything, that a user may need when +# using SymPy with NumPy +from sympy.external.importtools import version_tuple +from sympy.external import import_module + +numpy = import_module('numpy') +if numpy: + array, matrix, ndarray = numpy.array, numpy.matrix, numpy.ndarray +else: + #bin/test will not execute any tests now + disabled = True + + +from sympy.core.numbers import (Float, Integer, Rational) +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.trigonometric import sin +from sympy.matrices.dense import (Matrix, list2numpy, matrix2numpy, symarray) +from sympy.utilities.lambdify import lambdify +import sympy + +import mpmath +from sympy.abc import x, y, z +from sympy.utilities.decorator import conserve_mpmath_dps +from sympy.utilities.exceptions import ignore_warnings +from sympy.testing.pytest import raises + + +# first, systematically check, that all operations are implemented and don't +# raise an exception + + +def test_systematic_basic(): + def s(sympy_object, numpy_array): + _ = [sympy_object + numpy_array, + numpy_array + sympy_object, + sympy_object - numpy_array, + numpy_array - sympy_object, + sympy_object * numpy_array, + numpy_array * sympy_object, + sympy_object / numpy_array, + numpy_array / sympy_object, + sympy_object ** numpy_array, + numpy_array ** sympy_object] + x = Symbol("x") + y = Symbol("y") + sympy_objs = [ + Rational(2, 3), + Float("1.3"), + x, + y, + pow(x, y)*y, + Integer(5), + Float(5.5), + ] + numpy_objs = [ + array([1]), + array([3, 8, -1]), + array([x, x**2, Rational(5)]), + array([x/y*sin(y), 5, Rational(5)]), + ] + for x in sympy_objs: + for y in numpy_objs: + s(x, y) + + +# now some random tests, that test particular problems and that also +# check that the results of the operations are correct + +def test_basics(): + one = Rational(1) + zero = Rational(0) + assert array(1) == array(one) + assert array([one]) == array([one]) + assert array([x]) == array([x]) + assert array(x) == array(Symbol("x")) + assert array(one + x) == array(1 + x) + + X = array([one, zero, zero]) + assert (X == array([one, zero, zero])).all() + assert (X == array([one, 0, 0])).all() + + +def test_arrays(): + one = Rational(1) + zero = Rational(0) + X = array([one, zero, zero]) + Y = one*X + X = array([Symbol("a") + Rational(1, 2)]) + Y = X + X + assert Y == array([1 + 2*Symbol("a")]) + Y = Y + 1 + assert Y == array([2 + 2*Symbol("a")]) + Y = X - X + assert Y == array([0]) + + +def test_conversion1(): + a = list2numpy([x**2, x]) + #looks like an array? + assert isinstance(a, ndarray) + assert a[0] == x**2 + assert a[1] == x + assert len(a) == 2 + #yes, it's the array + + +def test_conversion2(): + a = 2*list2numpy([x**2, x]) + b = list2numpy([2*x**2, 2*x]) + assert (a == b).all() + + one = Rational(1) + zero = Rational(0) + X = list2numpy([one, zero, zero]) + Y = one*X + X = list2numpy([Symbol("a") + Rational(1, 2)]) + Y = X + X + assert Y == array([1 + 2*Symbol("a")]) + Y = Y + 1 + assert Y == array([2 + 2*Symbol("a")]) + Y = X - X + assert Y == array([0]) + + +def test_list2numpy(): + assert (array([x**2, x]) == list2numpy([x**2, x])).all() + + +def test_Matrix1(): + m = Matrix([[x, x**2], [5, 2/x]]) + assert (array(m.subs(x, 2)) == array([[2, 4], [5, 1]])).all() + m = Matrix([[sin(x), x**2], [5, 2/x]]) + assert (array(m.subs(x, 2)) == array([[sin(2), 4], [5, 1]])).all() + + +def test_Matrix2(): + m = Matrix([[x, x**2], [5, 2/x]]) + with ignore_warnings(PendingDeprecationWarning): + assert (matrix(m.subs(x, 2)) == matrix([[2, 4], [5, 1]])).all() + m = Matrix([[sin(x), x**2], [5, 2/x]]) + with ignore_warnings(PendingDeprecationWarning): + assert (matrix(m.subs(x, 2)) == matrix([[sin(2), 4], [5, 1]])).all() + + +def test_Matrix3(): + a = array([[2, 4], [5, 1]]) + assert Matrix(a) == Matrix([[2, 4], [5, 1]]) + assert Matrix(a) != Matrix([[2, 4], [5, 2]]) + a = array([[sin(2), 4], [5, 1]]) + assert Matrix(a) == Matrix([[sin(2), 4], [5, 1]]) + assert Matrix(a) != Matrix([[sin(0), 4], [5, 1]]) + + +def test_Matrix4(): + with ignore_warnings(PendingDeprecationWarning): + a = matrix([[2, 4], [5, 1]]) + assert Matrix(a) == Matrix([[2, 4], [5, 1]]) + assert Matrix(a) != Matrix([[2, 4], [5, 2]]) + with ignore_warnings(PendingDeprecationWarning): + a = matrix([[sin(2), 4], [5, 1]]) + assert Matrix(a) == Matrix([[sin(2), 4], [5, 1]]) + assert Matrix(a) != Matrix([[sin(0), 4], [5, 1]]) + + +def test_Matrix_sum(): + M = Matrix([[1, 2, 3], [x, y, x], [2*y, -50, z*x]]) + with ignore_warnings(PendingDeprecationWarning): + m = matrix([[2, 3, 4], [x, 5, 6], [x, y, z**2]]) + assert M + m == Matrix([[3, 5, 7], [2*x, y + 5, x + 6], [2*y + x, y - 50, z*x + z**2]]) + assert m + M == Matrix([[3, 5, 7], [2*x, y + 5, x + 6], [2*y + x, y - 50, z*x + z**2]]) + assert M + m == M.add(m) + + +def test_Matrix_mul(): + M = Matrix([[1, 2, 3], [x, y, x]]) + with ignore_warnings(PendingDeprecationWarning): + m = matrix([[2, 4], [x, 6], [x, z**2]]) + assert M*m == Matrix([ + [ 2 + 5*x, 16 + 3*z**2], + [2*x + x*y + x**2, 4*x + 6*y + x*z**2], + ]) + + assert m*M == Matrix([ + [ 2 + 4*x, 4 + 4*y, 6 + 4*x], + [ 7*x, 2*x + 6*y, 9*x], + [x + x*z**2, 2*x + y*z**2, 3*x + x*z**2], + ]) + a = array([2]) + assert a[0] * M == 2 * M + assert M * a[0] == 2 * M + + +def test_Matrix_array(): + class matarray: + def __array__(self, dtype=object, copy=None): + if copy is not None and not copy: + raise TypeError("Cannot implement copy=False when converting Matrix to ndarray") + from numpy import array + return array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + matarr = matarray() + assert Matrix(matarr) == Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + + +def test_matrix2numpy(): + a = matrix2numpy(Matrix([[1, x**2], [3*sin(x), 0]])) + assert isinstance(a, ndarray) + assert a.shape == (2, 2) + assert a[0, 0] == 1 + assert a[0, 1] == x**2 + assert a[1, 0] == 3*sin(x) + assert a[1, 1] == 0 + + +def test_matrix2numpy_conversion(): + a = Matrix([[1, 2, sin(x)], [x**2, x, Rational(1, 2)]]) + b = array([[1, 2, sin(x)], [x**2, x, Rational(1, 2)]]) + assert (matrix2numpy(a) == b).all() + assert matrix2numpy(a).dtype == numpy.dtype('object') + + c = matrix2numpy(Matrix([[1, 2], [10, 20]]), dtype='int8') + d = matrix2numpy(Matrix([[1, 2], [10, 20]]), dtype='float64') + assert c.dtype == numpy.dtype('int8') + assert d.dtype == numpy.dtype('float64') + + +def test_issue_3728(): + assert (Rational(1, 2)*array([2*x, 0]) == array([x, 0])).all() + assert (Rational(1, 2) + array( + [2*x, 0]) == array([2*x + Rational(1, 2), Rational(1, 2)])).all() + assert (Float("0.5")*array([2*x, 0]) == array([Float("1.0")*x, 0])).all() + assert (Float("0.5") + array( + [2*x, 0]) == array([2*x + Float("0.5"), Float("0.5")])).all() + + +@conserve_mpmath_dps +def test_lambdify(): + mpmath.mp.dps = 16 + sin02 = mpmath.mpf("0.198669330795061215459412627") + f = lambdify(x, sin(x), "numpy") + prec = 1e-15 + assert -prec < f(0.2) - sin02 < prec + + # if this succeeds, it can't be a numpy function + + if version_tuple(numpy.__version__) >= version_tuple('1.17'): + with raises(TypeError): + f(x) + else: + with raises(AttributeError): + f(x) + + +def test_lambdify_matrix(): + f = lambdify(x, Matrix([[x, 2*x], [1, 2]]), [{'ImmutableMatrix': numpy.array}, "numpy"]) + assert (f(1) == array([[1, 2], [1, 2]])).all() + + +def test_lambdify_matrix_multi_input(): + M = sympy.Matrix([[x**2, x*y, x*z], + [y*x, y**2, y*z], + [z*x, z*y, z**2]]) + f = lambdify((x, y, z), M, [{'ImmutableMatrix': numpy.array}, "numpy"]) + + xh, yh, zh = 1.0, 2.0, 3.0 + expected = array([[xh**2, xh*yh, xh*zh], + [yh*xh, yh**2, yh*zh], + [zh*xh, zh*yh, zh**2]]) + actual = f(xh, yh, zh) + assert numpy.allclose(actual, expected) + + +def test_lambdify_matrix_vec_input(): + X = sympy.DeferredVector('X') + M = Matrix([ + [X[0]**2, X[0]*X[1], X[0]*X[2]], + [X[1]*X[0], X[1]**2, X[1]*X[2]], + [X[2]*X[0], X[2]*X[1], X[2]**2]]) + f = lambdify(X, M, [{'ImmutableMatrix': numpy.array}, "numpy"]) + + Xh = array([1.0, 2.0, 3.0]) + expected = array([[Xh[0]**2, Xh[0]*Xh[1], Xh[0]*Xh[2]], + [Xh[1]*Xh[0], Xh[1]**2, Xh[1]*Xh[2]], + [Xh[2]*Xh[0], Xh[2]*Xh[1], Xh[2]**2]]) + actual = f(Xh) + assert numpy.allclose(actual, expected) + + +def test_lambdify_transl(): + from sympy.utilities.lambdify import NUMPY_TRANSLATIONS + for sym, mat in NUMPY_TRANSLATIONS.items(): + assert sym in sympy.__dict__ + assert mat in numpy.__dict__ + + +def test_symarray(): + """Test creation of numpy arrays of SymPy symbols.""" + + import numpy as np + import numpy.testing as npt + + syms = symbols('_0,_1,_2') + s1 = symarray("", 3) + s2 = symarray("", 3) + npt.assert_array_equal(s1, np.array(syms, dtype=object)) + assert s1[0] == s2[0] + + a = symarray('a', 3) + b = symarray('b', 3) + assert not(a[0] == b[0]) + + asyms = symbols('a_0,a_1,a_2') + npt.assert_array_equal(a, np.array(asyms, dtype=object)) + + # Multidimensional checks + a2d = symarray('a', (2, 3)) + assert a2d.shape == (2, 3) + a00, a12 = symbols('a_0_0,a_1_2') + assert a2d[0, 0] == a00 + assert a2d[1, 2] == a12 + + a3d = symarray('a', (2, 3, 2)) + assert a3d.shape == (2, 3, 2) + a000, a120, a121 = symbols('a_0_0_0,a_1_2_0,a_1_2_1') + assert a3d[0, 0, 0] == a000 + assert a3d[1, 2, 0] == a120 + assert a3d[1, 2, 1] == a121 + + +def test_vectorize(): + assert (numpy.vectorize( + sin)([1, 2, 3]) == numpy.array([sin(1), sin(2), sin(3)])).all() diff --git a/venv/lib/python3.11/site-packages/sympy/external/tests/test_pythonmpq.py b/venv/lib/python3.11/site-packages/sympy/external/tests/test_pythonmpq.py new file mode 100644 index 0000000000000000000000000000000000000000..137cfdf5c858544f0811ae666f000cfb368787a0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/external/tests/test_pythonmpq.py @@ -0,0 +1,176 @@ +""" +test_pythonmpq.py + +Test the PythonMPQ class for consistency with gmpy2's mpq type. If gmpy2 is +installed run the same tests for both. +""" +from fractions import Fraction +from decimal import Decimal +import pickle +from typing import Callable, List, Tuple, Type + +from sympy.testing.pytest import raises + +from sympy.external.pythonmpq import PythonMPQ + +# +# If gmpy2 is installed then run the tests for both mpq and PythonMPQ. +# That should ensure consistency between the implementation here and mpq. +# +rational_types: List[Tuple[Callable, Type, Callable, Type]] +rational_types = [(PythonMPQ, PythonMPQ, int, int)] +try: + from gmpy2 import mpq, mpz + rational_types.append((mpq, type(mpq(1)), mpz, type(mpz(1)))) +except ImportError: + pass + + +def test_PythonMPQ(): + # + # Test PythonMPQ and also mpq if gmpy/gmpy2 is installed. + # + for Q, TQ, Z, TZ in rational_types: + + def check_Q(q): + assert isinstance(q, TQ) + assert isinstance(q.numerator, TZ) + assert isinstance(q.denominator, TZ) + return q.numerator, q.denominator + + # Check construction from different types + assert check_Q(Q(3)) == (3, 1) + assert check_Q(Q(3, 5)) == (3, 5) + assert check_Q(Q(Q(3, 5))) == (3, 5) + assert check_Q(Q(0.5)) == (1, 2) + assert check_Q(Q('0.5')) == (1, 2) + assert check_Q(Q(Fraction(3, 5))) == (3, 5) + + # https://github.com/aleaxit/gmpy/issues/327 + if Q is PythonMPQ: + assert check_Q(Q(Decimal('0.6'))) == (3, 5) + + # Invalid types + raises(TypeError, lambda: Q([])) + raises(TypeError, lambda: Q([], [])) + + # Check normalisation of signs + assert check_Q(Q(2, 3)) == (2, 3) + assert check_Q(Q(-2, 3)) == (-2, 3) + assert check_Q(Q(2, -3)) == (-2, 3) + assert check_Q(Q(-2, -3)) == (2, 3) + + # Check gcd calculation + assert check_Q(Q(12, 8)) == (3, 2) + + # __int__/__float__ + assert int(Q(5, 3)) == 1 + assert int(Q(-5, 3)) == -1 + assert float(Q(5, 2)) == 2.5 + assert float(Q(-5, 2)) == -2.5 + + # __str__/__repr__ + assert str(Q(2, 1)) == "2" + assert str(Q(1, 2)) == "1/2" + if Q is PythonMPQ: + assert repr(Q(2, 1)) == "MPQ(2,1)" + assert repr(Q(1, 2)) == "MPQ(1,2)" + else: + assert repr(Q(2, 1)) == "mpq(2,1)" + assert repr(Q(1, 2)) == "mpq(1,2)" + + # __bool__ + assert bool(Q(1, 2)) is True + assert bool(Q(0)) is False + + # __eq__/__ne__ + assert (Q(2, 3) == Q(2, 3)) is True + assert (Q(2, 3) == Q(2, 5)) is False + assert (Q(2, 3) != Q(2, 3)) is False + assert (Q(2, 3) != Q(2, 5)) is True + + # __hash__ + assert hash(Q(3, 5)) == hash(Fraction(3, 5)) + + # __reduce__ + q = Q(2, 3) + assert pickle.loads(pickle.dumps(q)) == q + + # __ge__/__gt__/__le__/__lt__ + assert (Q(1, 3) < Q(2, 3)) is True + assert (Q(2, 3) < Q(2, 3)) is False + assert (Q(2, 3) < Q(1, 3)) is False + assert (Q(-2, 3) < Q(1, 3)) is True + assert (Q(1, 3) < Q(-2, 3)) is False + + assert (Q(1, 3) <= Q(2, 3)) is True + assert (Q(2, 3) <= Q(2, 3)) is True + assert (Q(2, 3) <= Q(1, 3)) is False + assert (Q(-2, 3) <= Q(1, 3)) is True + assert (Q(1, 3) <= Q(-2, 3)) is False + + assert (Q(1, 3) > Q(2, 3)) is False + assert (Q(2, 3) > Q(2, 3)) is False + assert (Q(2, 3) > Q(1, 3)) is True + assert (Q(-2, 3) > Q(1, 3)) is False + assert (Q(1, 3) > Q(-2, 3)) is True + + assert (Q(1, 3) >= Q(2, 3)) is False + assert (Q(2, 3) >= Q(2, 3)) is True + assert (Q(2, 3) >= Q(1, 3)) is True + assert (Q(-2, 3) >= Q(1, 3)) is False + assert (Q(1, 3) >= Q(-2, 3)) is True + + # __abs__/__pos__/__neg__ + assert abs(Q(2, 3)) == abs(Q(-2, 3)) == Q(2, 3) + assert +Q(2, 3) == Q(2, 3) + assert -Q(2, 3) == Q(-2, 3) + + # __add__/__radd__ + assert Q(2, 3) + Q(5, 7) == Q(29, 21) + assert Q(2, 3) + 1 == Q(5, 3) + assert 1 + Q(2, 3) == Q(5, 3) + raises(TypeError, lambda: [] + Q(1)) + raises(TypeError, lambda: Q(1) + []) + + # __sub__/__rsub__ + assert Q(2, 3) - Q(5, 7) == Q(-1, 21) + assert Q(2, 3) - 1 == Q(-1, 3) + assert 1 - Q(2, 3) == Q(1, 3) + raises(TypeError, lambda: [] - Q(1)) + raises(TypeError, lambda: Q(1) - []) + + # __mul__/__rmul__ + assert Q(2, 3) * Q(5, 7) == Q(10, 21) + assert Q(2, 3) * 1 == Q(2, 3) + assert 1 * Q(2, 3) == Q(2, 3) + raises(TypeError, lambda: [] * Q(1)) + raises(TypeError, lambda: Q(1) * []) + + # __pow__/__rpow__ + assert Q(2, 3) ** 2 == Q(4, 9) + assert Q(2, 3) ** 1 == Q(2, 3) + assert Q(-2, 3) ** 2 == Q(4, 9) + assert Q(-2, 3) ** -1 == Q(-3, 2) + if Q is PythonMPQ: + raises(TypeError, lambda: 1 ** Q(2, 3)) + raises(TypeError, lambda: Q(1, 4) ** Q(1, 2)) + raises(TypeError, lambda: [] ** Q(1)) + raises(TypeError, lambda: Q(1) ** []) + + # __div__/__rdiv__ + assert Q(2, 3) / Q(5, 7) == Q(14, 15) + assert Q(2, 3) / 1 == Q(2, 3) + assert 1 / Q(2, 3) == Q(3, 2) + raises(TypeError, lambda: [] / Q(1)) + raises(TypeError, lambda: Q(1) / []) + raises(ZeroDivisionError, lambda: Q(1, 2) / Q(0)) + + # __divmod__ + if Q is PythonMPQ: + raises(TypeError, lambda: Q(2, 3) // Q(1, 3)) + raises(TypeError, lambda: Q(2, 3) % Q(1, 3)) + raises(TypeError, lambda: 1 // Q(1, 3)) + raises(TypeError, lambda: 1 % Q(1, 3)) + raises(TypeError, lambda: Q(2, 3) // 1) + raises(TypeError, lambda: Q(2, 3) % 1) diff --git a/venv/lib/python3.11/site-packages/sympy/external/tests/test_scipy.py b/venv/lib/python3.11/site-packages/sympy/external/tests/test_scipy.py new file mode 100644 index 0000000000000000000000000000000000000000..3746d1a311eb68bb1af16e18ab152c7236b42bb5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/external/tests/test_scipy.py @@ -0,0 +1,35 @@ +# This testfile tests SymPy <-> SciPy compatibility + +# Don't test any SymPy features here. Just pure interaction with SciPy. +# Always write regular SymPy tests for anything, that can be tested in pure +# Python (without scipy). Here we test everything, that a user may need when +# using SymPy with SciPy + +from sympy.external import import_module + +scipy = import_module('scipy') +if not scipy: + #bin/test will not execute any tests now + disabled = True + +from sympy.functions.special.bessel import jn_zeros + + +def eq(a, b, tol=1e-6): + for x, y in zip(a, b): + if not (abs(x - y) < tol): + return False + return True + + +def test_jn_zeros(): + assert eq(jn_zeros(0, 4, method="scipy"), + [3.141592, 6.283185, 9.424777, 12.566370]) + assert eq(jn_zeros(1, 4, method="scipy"), + [4.493409, 7.725251, 10.904121, 14.066193]) + assert eq(jn_zeros(2, 4, method="scipy"), + [5.763459, 9.095011, 12.322940, 15.514603]) + assert eq(jn_zeros(3, 4, method="scipy"), + [6.987932, 10.417118, 13.698023, 16.923621]) + assert eq(jn_zeros(4, 4, method="scipy"), + [8.182561, 11.704907, 15.039664, 18.301255]) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/__init__.py b/venv/lib/python3.11/site-packages/sympy/functions/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ed93b2a11754aa26af5eef3932d177374b3ddfd6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/__init__.py @@ -0,0 +1,115 @@ +"""A functions module, includes all the standard functions. + +Combinatorial - factorial, fibonacci, harmonic, bernoulli... +Elementary - hyperbolic, trigonometric, exponential, floor and ceiling, sqrt... +Special - gamma, zeta,spherical harmonics... +""" + +from sympy.functions.combinatorial.factorials import (factorial, factorial2, + rf, ff, binomial, RisingFactorial, FallingFactorial, subfactorial) +from sympy.functions.combinatorial.numbers import (carmichael, fibonacci, lucas, tribonacci, + harmonic, bernoulli, bell, euler, catalan, genocchi, andre, partition, divisor_sigma, + udivisor_sigma, legendre_symbol, jacobi_symbol, kronecker_symbol, mobius, + primenu, primeomega, totient, reduced_totient, primepi, motzkin) +from sympy.functions.elementary.miscellaneous import (sqrt, root, Min, Max, + Id, real_root, cbrt, Rem) +from sympy.functions.elementary.complexes import (re, im, sign, Abs, + conjugate, arg, polar_lift, periodic_argument, unbranched_argument, + principal_branch, transpose, adjoint, polarify, unpolarify) +from sympy.functions.elementary.trigonometric import (sin, cos, tan, + sec, csc, cot, sinc, asin, acos, atan, asec, acsc, acot, atan2) +from sympy.functions.elementary.exponential import (exp_polar, exp, log, + LambertW) +from sympy.functions.elementary.hyperbolic import (sinh, cosh, tanh, coth, + sech, csch, asinh, acosh, atanh, acoth, asech, acsch) +from sympy.functions.elementary.integers import floor, ceiling, frac +from sympy.functions.elementary.piecewise import (Piecewise, piecewise_fold, + piecewise_exclusive) +from sympy.functions.special.error_functions import (erf, erfc, erfi, erf2, + erfinv, erfcinv, erf2inv, Ei, expint, E1, li, Li, Si, Ci, Shi, Chi, + fresnels, fresnelc) +from sympy.functions.special.gamma_functions import (gamma, lowergamma, + uppergamma, polygamma, loggamma, digamma, trigamma, multigamma) +from sympy.functions.special.zeta_functions import (dirichlet_eta, zeta, + lerchphi, polylog, stieltjes, riemann_xi) +from sympy.functions.special.tensor_functions import (Eijk, LeviCivita, + KroneckerDelta) +from sympy.functions.special.singularity_functions import SingularityFunction +from sympy.functions.special.delta_functions import DiracDelta, Heaviside +from sympy.functions.special.bsplines import bspline_basis, bspline_basis_set, interpolating_spline +from sympy.functions.special.bessel import (besselj, bessely, besseli, besselk, + hankel1, hankel2, jn, yn, jn_zeros, hn1, hn2, airyai, airybi, airyaiprime, airybiprime, marcumq) +from sympy.functions.special.hyper import hyper, meijerg, appellf1 +from sympy.functions.special.polynomials import (legendre, assoc_legendre, + hermite, hermite_prob, chebyshevt, chebyshevu, chebyshevu_root, + chebyshevt_root, laguerre, assoc_laguerre, gegenbauer, jacobi, jacobi_normalized) +from sympy.functions.special.spherical_harmonics import Ynm, Ynm_c, Znm +from sympy.functions.special.elliptic_integrals import (elliptic_k, + elliptic_f, elliptic_e, elliptic_pi) +from sympy.functions.special.beta_functions import beta, betainc, betainc_regularized +from sympy.functions.special.mathieu_functions import (mathieus, mathieuc, + mathieusprime, mathieucprime) +ln = log + +__all__ = [ + 'factorial', 'factorial2', 'rf', 'ff', 'binomial', 'RisingFactorial', + 'FallingFactorial', 'subfactorial', + + 'carmichael', 'fibonacci', 'lucas', 'motzkin', 'tribonacci', 'harmonic', + 'bernoulli', 'bell', 'euler', 'catalan', 'genocchi', 'andre', 'partition', + 'divisor_sigma', 'udivisor_sigma', 'legendre_symbol', 'jacobi_symbol', 'kronecker_symbol', + 'mobius', 'primenu', 'primeomega', 'totient', 'reduced_totient', 'primepi', + + 'sqrt', 'root', 'Min', 'Max', 'Id', 'real_root', 'cbrt', 'Rem', + + 're', 'im', 'sign', 'Abs', 'conjugate', 'arg', 'polar_lift', + 'periodic_argument', 'unbranched_argument', 'principal_branch', + 'transpose', 'adjoint', 'polarify', 'unpolarify', + + 'sin', 'cos', 'tan', 'sec', 'csc', 'cot', 'sinc', 'asin', 'acos', 'atan', + 'asec', 'acsc', 'acot', 'atan2', + + 'exp_polar', 'exp', 'ln', 'log', 'LambertW', + + 'sinh', 'cosh', 'tanh', 'coth', 'sech', 'csch', 'asinh', 'acosh', 'atanh', + 'acoth', 'asech', 'acsch', + + 'floor', 'ceiling', 'frac', + + 'Piecewise', 'piecewise_fold', 'piecewise_exclusive', + + 'erf', 'erfc', 'erfi', 'erf2', 'erfinv', 'erfcinv', 'erf2inv', 'Ei', + 'expint', 'E1', 'li', 'Li', 'Si', 'Ci', 'Shi', 'Chi', 'fresnels', + 'fresnelc', + + 'gamma', 'lowergamma', 'uppergamma', 'polygamma', 'loggamma', 'digamma', + 'trigamma', 'multigamma', + + 'dirichlet_eta', 'zeta', 'lerchphi', 'polylog', 'stieltjes', 'riemann_xi', + + 'Eijk', 'LeviCivita', 'KroneckerDelta', + + 'SingularityFunction', + + 'DiracDelta', 'Heaviside', + + 'bspline_basis', 'bspline_basis_set', 'interpolating_spline', + + 'besselj', 'bessely', 'besseli', 'besselk', 'hankel1', 'hankel2', 'jn', + 'yn', 'jn_zeros', 'hn1', 'hn2', 'airyai', 'airybi', 'airyaiprime', + 'airybiprime', 'marcumq', + + 'hyper', 'meijerg', 'appellf1', + + 'legendre', 'assoc_legendre', 'hermite', 'hermite_prob', 'chebyshevt', + 'chebyshevu', 'chebyshevu_root', 'chebyshevt_root', 'laguerre', + 'assoc_laguerre', 'gegenbauer', 'jacobi', 'jacobi_normalized', + + 'Ynm', 'Ynm_c', 'Znm', + + 'elliptic_k', 'elliptic_f', 'elliptic_e', 'elliptic_pi', + + 'beta', 'betainc', 'betainc_regularized', + + 'mathieus', 'mathieuc', 'mathieusprime', 'mathieucprime', +] diff --git a/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/__init__.py b/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..584b3c8d46b5c7600d85efc7db46d7aa190397f8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/__init__.py @@ -0,0 +1 @@ +# Stub __init__.py for sympy.functions.combinatorial diff --git a/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/factorials.py b/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/factorials.py new file mode 100644 index 0000000000000000000000000000000000000000..0c6d2f09524debca29d3040b28a019127a244b33 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/factorials.py @@ -0,0 +1,1133 @@ +from __future__ import annotations +from functools import reduce + +from sympy.core import S, sympify, Dummy, Mod +from sympy.core.cache import cacheit +from sympy.core.function import DefinedFunction, ArgumentIndexError, PoleError +from sympy.core.logic import fuzzy_and +from sympy.core.numbers import Integer, pi, I +from sympy.core.relational import Eq +from sympy.external.gmpy import gmpy as _gmpy +from sympy.ntheory import sieve +from sympy.ntheory.residue_ntheory import binomial_mod +from sympy.polys.polytools import Poly + +from math import factorial as _factorial, prod, sqrt as _sqrt + +class CombinatorialFunction(DefinedFunction): + """Base class for combinatorial functions. """ + + def _eval_simplify(self, **kwargs): + from sympy.simplify.combsimp import combsimp + # combinatorial function with non-integer arguments is + # automatically passed to gammasimp + expr = combsimp(self) + measure = kwargs['measure'] + if measure(expr) <= kwargs['ratio']*measure(self): + return expr + return self + + +############################################################################### +######################## FACTORIAL and MULTI-FACTORIAL ######################## +############################################################################### + + +class factorial(CombinatorialFunction): + r"""Implementation of factorial function over nonnegative integers. + By convention (consistent with the gamma function and the binomial + coefficients), factorial of a negative integer is complex infinity. + + The factorial is very important in combinatorics where it gives + the number of ways in which `n` objects can be permuted. It also + arises in calculus, probability, number theory, etc. + + There is strict relation of factorial with gamma function. In + fact `n! = gamma(n+1)` for nonnegative integers. Rewrite of this + kind is very useful in case of combinatorial simplification. + + Computation of the factorial is done using two algorithms. For + small arguments a precomputed look up table is used. However for bigger + input algorithm Prime-Swing is used. It is the fastest algorithm + known and computes `n!` via prime factorization of special class + of numbers, called here the 'Swing Numbers'. + + Examples + ======== + + >>> from sympy import Symbol, factorial, S + >>> n = Symbol('n', integer=True) + + >>> factorial(0) + 1 + + >>> factorial(7) + 5040 + + >>> factorial(-2) + zoo + + >>> factorial(n) + factorial(n) + + >>> factorial(2*n) + factorial(2*n) + + >>> factorial(S(1)/2) + factorial(1/2) + + See Also + ======== + + factorial2, RisingFactorial, FallingFactorial + """ + + def fdiff(self, argindex=1): + from sympy.functions.special.gamma_functions import (gamma, polygamma) + if argindex == 1: + return gamma(self.args[0] + 1)*polygamma(0, self.args[0] + 1) + else: + raise ArgumentIndexError(self, argindex) + + _small_swing = [ + 1, 1, 1, 3, 3, 15, 5, 35, 35, 315, 63, 693, 231, 3003, 429, 6435, 6435, 109395, + 12155, 230945, 46189, 969969, 88179, 2028117, 676039, 16900975, 1300075, + 35102025, 5014575, 145422675, 9694845, 300540195, 300540195 + ] + + _small_factorials: list[int] = [] + + @classmethod + def _swing(cls, n): + if n < 33: + return cls._small_swing[n] + else: + N, primes = int(_sqrt(n)), [] + + for prime in sieve.primerange(3, N + 1): + p, q = 1, n + + while True: + q //= prime + + if q > 0: + if q & 1 == 1: + p *= prime + else: + break + + if p > 1: + primes.append(p) + + for prime in sieve.primerange(N + 1, n//3 + 1): + if (n // prime) & 1 == 1: + primes.append(prime) + + L_product = prod(sieve.primerange(n//2 + 1, n + 1)) + R_product = prod(primes) + + return L_product*R_product + + @classmethod + def _recursive(cls, n): + if n < 2: + return 1 + else: + return (cls._recursive(n//2)**2)*cls._swing(n) + + @classmethod + def eval(cls, n): + n = sympify(n) + + if n.is_Number: + if n.is_zero: + return S.One + elif n is S.Infinity: + return S.Infinity + elif n.is_Integer: + if n.is_negative: + return S.ComplexInfinity + else: + n = n.p + + if n < 20: + if not cls._small_factorials: + result = 1 + for i in range(1, 20): + result *= i + cls._small_factorials.append(result) + result = cls._small_factorials[n-1] + + # GMPY factorial is faster, use it when available + # + # XXX: There is a sympy.external.gmpy.factorial function + # which provides gmpy.fac if available or the flint version + # if flint is used. It could be used here to avoid the + # conditional logic but it needs to be checked whether the + # pure Python fallback used there is as fast as the + # fallback used here (perhaps the fallback here should be + # moved to sympy.external.ntheory). + elif _gmpy is not None: + result = _gmpy.fac(n) + + else: + bits = bin(n).count('1') + result = cls._recursive(n)*2**(n - bits) + + return Integer(result) + + def _facmod(self, n, q): + res, N = 1, int(_sqrt(n)) + + # Exponent of prime p in n! is e_p(n) = [n/p] + [n/p**2] + ... + # for p > sqrt(n), e_p(n) < sqrt(n), the primes with [n/p] = m, + # occur consecutively and are grouped together in pw[m] for + # simultaneous exponentiation at a later stage + pw = [1]*N + + m = 2 # to initialize the if condition below + for prime in sieve.primerange(2, n + 1): + if m > 1: + m, y = 0, n // prime + while y: + m += y + y //= prime + if m < N: + pw[m] = pw[m]*prime % q + else: + res = res*pow(prime, m, q) % q + + for ex, bs in enumerate(pw): + if ex == 0 or bs == 1: + continue + if bs == 0: + return 0 + res = res*pow(bs, ex, q) % q + + return res + + def _eval_Mod(self, q): + n = self.args[0] + if n.is_integer and n.is_nonnegative and q.is_integer: + aq = abs(q) + d = aq - n + if d.is_nonpositive: + return S.Zero + else: + isprime = aq.is_prime + if d == 1: + # Apply Wilson's theorem (if a natural number n > 1 + # is a prime number, then (n-1)! = -1 mod n) and + # its inverse (if n > 4 is a composite number, then + # (n-1)! = 0 mod n) + if isprime: + return -1 % q + elif isprime is False and (aq - 6).is_nonnegative: + return S.Zero + elif n.is_Integer and q.is_Integer: + n, d, aq = map(int, (n, d, aq)) + if isprime and (d - 1 < n): + fc = self._facmod(d - 1, aq) + fc = pow(fc, aq - 2, aq) + if d%2: + fc = -fc + else: + fc = self._facmod(n, aq) + + return fc % q + + def _eval_rewrite_as_gamma(self, n, piecewise=True, **kwargs): + from sympy.functions.special.gamma_functions import gamma + return gamma(n + 1) + + def _eval_rewrite_as_Product(self, n, **kwargs): + from sympy.concrete.products import Product + if n.is_nonnegative and n.is_integer: + i = Dummy('i', integer=True) + return Product(i, (i, 1, n)) + + def _eval_is_integer(self): + if self.args[0].is_integer and self.args[0].is_nonnegative: + return True + + def _eval_is_positive(self): + if self.args[0].is_integer and self.args[0].is_nonnegative: + return True + + def _eval_is_even(self): + x = self.args[0] + if x.is_integer and x.is_nonnegative: + return (x - 2).is_nonnegative + + def _eval_is_composite(self): + x = self.args[0] + if x.is_integer and x.is_nonnegative: + return (x - 3).is_nonnegative + + def _eval_is_real(self): + x = self.args[0] + if x.is_nonnegative or x.is_noninteger: + return True + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0].as_leading_term(x) + arg0 = arg.subs(x, 0) + if arg0.is_zero: + return S.One + elif not arg0.is_infinite: + return self.func(arg) + raise PoleError("Cannot expand %s around 0" % (self)) + +class MultiFactorial(CombinatorialFunction): + pass + + +class subfactorial(CombinatorialFunction): + r"""The subfactorial counts the derangements of $n$ items and is + defined for non-negative integers as: + + .. math:: !n = \begin{cases} 1 & n = 0 \\ 0 & n = 1 \\ + (n-1)(!(n-1) + !(n-2)) & n > 1 \end{cases} + + It can also be written as ``int(round(n!/exp(1)))`` but the + recursive definition with caching is implemented for this function. + + An interesting analytic expression is the following [2]_ + + .. math:: !x = \Gamma(x + 1, -1)/e + + which is valid for non-negative integers `x`. The above formula + is not very useful in case of non-integers. `\Gamma(x + 1, -1)` is + single-valued only for integral arguments `x`, elsewhere on the positive + real axis it has an infinite number of branches none of which are real. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Subfactorial + .. [2] https://mathworld.wolfram.com/Subfactorial.html + + Examples + ======== + + >>> from sympy import subfactorial + >>> from sympy.abc import n + >>> subfactorial(n + 1) + subfactorial(n + 1) + >>> subfactorial(5) + 44 + + See Also + ======== + + factorial, uppergamma, + sympy.utilities.iterables.generate_derangements + """ + + @classmethod + @cacheit + def _eval(self, n): + if not n: + return S.One + elif n == 1: + return S.Zero + else: + z1, z2 = 1, 0 + for i in range(2, n + 1): + z1, z2 = z2, (i - 1)*(z2 + z1) + return z2 + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg.is_Integer and arg.is_nonnegative: + return cls._eval(arg) + elif arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.Infinity + + def _eval_is_even(self): + if self.args[0].is_odd and self.args[0].is_nonnegative: + return True + + def _eval_is_integer(self): + if self.args[0].is_integer and self.args[0].is_nonnegative: + return True + + def _eval_rewrite_as_factorial(self, arg, **kwargs): + from sympy.concrete.summations import summation + i = Dummy('i') + f = S.NegativeOne**i / factorial(i) + return factorial(arg) * summation(f, (i, 0, arg)) + + def _eval_rewrite_as_gamma(self, arg, piecewise=True, **kwargs): + from sympy.functions.elementary.exponential import exp + from sympy.functions.special.gamma_functions import (gamma, lowergamma) + return (S.NegativeOne**(arg + 1)*exp(-I*pi*arg)*lowergamma(arg + 1, -1) + + gamma(arg + 1))*exp(-1) + + def _eval_rewrite_as_uppergamma(self, arg, **kwargs): + from sympy.functions.special.gamma_functions import uppergamma + return uppergamma(arg + 1, -1)/S.Exp1 + + def _eval_is_nonnegative(self): + if self.args[0].is_integer and self.args[0].is_nonnegative: + return True + + def _eval_is_odd(self): + if self.args[0].is_even and self.args[0].is_nonnegative: + return True + + +class factorial2(CombinatorialFunction): + r"""The double factorial `n!!`, not to be confused with `(n!)!` + + The double factorial is defined for nonnegative integers and for odd + negative integers as: + + .. math:: n!! = \begin{cases} 1 & n = 0 \\ + n(n-2)(n-4) \cdots 1 & n\ \text{positive odd} \\ + n(n-2)(n-4) \cdots 2 & n\ \text{positive even} \\ + (n+2)!!/(n+2) & n\ \text{negative odd} \end{cases} + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Double_factorial + + Examples + ======== + + >>> from sympy import factorial2, var + >>> n = var('n') + >>> n + n + >>> factorial2(n + 1) + factorial2(n + 1) + >>> factorial2(5) + 15 + >>> factorial2(-1) + 1 + >>> factorial2(-5) + 1/3 + + See Also + ======== + + factorial, RisingFactorial, FallingFactorial + """ + + @classmethod + def eval(cls, arg): + # TODO: extend this to complex numbers? + + if arg.is_Number: + if not arg.is_Integer: + raise ValueError("argument must be nonnegative integer " + "or negative odd integer") + + # This implementation is faster than the recursive one + # It also avoids "maximum recursion depth exceeded" runtime error + if arg.is_nonnegative: + if arg.is_even: + k = arg / 2 + return 2**k * factorial(k) + return factorial(arg) / factorial2(arg - 1) + + + if arg.is_odd: + return arg*(S.NegativeOne)**((1 - arg)/2) / factorial2(-arg) + raise ValueError("argument must be nonnegative integer " + "or negative odd integer") + + + def _eval_is_even(self): + # Double factorial is even for every positive even input + n = self.args[0] + if n.is_integer: + if n.is_odd: + return False + if n.is_even: + if n.is_positive: + return True + if n.is_zero: + return False + + def _eval_is_integer(self): + # Double factorial is an integer for every nonnegative input, and for + # -1 and -3 + n = self.args[0] + if n.is_integer: + if (n + 1).is_nonnegative: + return True + if n.is_odd: + return (n + 3).is_nonnegative + + def _eval_is_odd(self): + # Double factorial is odd for every odd input not smaller than -3, and + # for 0 + n = self.args[0] + if n.is_odd: + return (n + 3).is_nonnegative + if n.is_even: + if n.is_positive: + return False + if n.is_zero: + return True + + def _eval_is_positive(self): + # Double factorial is positive for every nonnegative input, and for + # every odd negative input which is of the form -1-4k for an + # nonnegative integer k + n = self.args[0] + if n.is_integer: + if (n + 1).is_nonnegative: + return True + if n.is_odd: + return ((n + 1) / 2).is_even + + def _eval_rewrite_as_gamma(self, n, piecewise=True, **kwargs): + from sympy.functions.elementary.miscellaneous import sqrt + from sympy.functions.elementary.piecewise import Piecewise + from sympy.functions.special.gamma_functions import gamma + return 2**(n/2)*gamma(n/2 + 1) * Piecewise((1, Eq(Mod(n, 2), 0)), + (sqrt(2/pi), Eq(Mod(n, 2), 1))) + + +############################################################################### +######################## RISING and FALLING FACTORIALS ######################## +############################################################################### + + +class RisingFactorial(CombinatorialFunction): + r""" + Rising factorial (also called Pochhammer symbol [1]_) is a double valued + function arising in concrete mathematics, hypergeometric functions + and series expansions. It is defined by: + + .. math:: \texttt{rf(y, k)} = (x)^k = x \cdot (x+1) \cdots (x+k-1) + + where `x` can be arbitrary expression and `k` is an integer. For + more information check "Concrete mathematics" by Graham, pp. 66 + or visit https://mathworld.wolfram.com/RisingFactorial.html page. + + When `x` is a `~.Poly` instance of degree $\ge 1$ with a single variable, + `(x)^k = x(y) \cdot x(y+1) \cdots x(y+k-1)`, where `y` is the + variable of `x`. This is as described in [2]_. + + Examples + ======== + + >>> from sympy import rf, Poly + >>> from sympy.abc import x + >>> rf(x, 0) + 1 + >>> rf(1, 5) + 120 + >>> rf(x, 5) == x*(1 + x)*(2 + x)*(3 + x)*(4 + x) + True + >>> rf(Poly(x**3, x), 2) + Poly(x**6 + 3*x**5 + 3*x**4 + x**3, x, domain='ZZ') + + Rewriting is complicated unless the relationship between + the arguments is known, but rising factorial can + be rewritten in terms of gamma, factorial, binomial, + and falling factorial. + + >>> from sympy import Symbol, factorial, ff, binomial, gamma + >>> n = Symbol('n', integer=True, positive=True) + >>> R = rf(n, n + 2) + >>> for i in (rf, ff, factorial, binomial, gamma): + ... R.rewrite(i) + ... + RisingFactorial(n, n + 2) + FallingFactorial(2*n + 1, n + 2) + factorial(2*n + 1)/factorial(n - 1) + binomial(2*n + 1, n + 2)*factorial(n + 2) + gamma(2*n + 2)/gamma(n) + + See Also + ======== + + factorial, factorial2, FallingFactorial + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Pochhammer_symbol + .. [2] Peter Paule, "Greatest Factorial Factorization and Symbolic + Summation", Journal of Symbolic Computation, vol. 20, pp. 235-268, + 1995. + + """ + + @classmethod + def eval(cls, x, k): + x = sympify(x) + k = sympify(k) + + if x is S.NaN or k is S.NaN: + return S.NaN + elif x is S.One: + return factorial(k) + elif k.is_Integer: + if k.is_zero: + return S.One + else: + if k.is_positive: + if x is S.Infinity: + return S.Infinity + elif x is S.NegativeInfinity: + if k.is_odd: + return S.NegativeInfinity + else: + return S.Infinity + else: + if isinstance(x, Poly): + gens = x.gens + if len(gens)!= 1: + raise ValueError("rf only defined for " + "polynomials on one generator") + else: + return reduce(lambda r, i: + r*(x.shift(i)), + range(int(k)), 1) + else: + return reduce(lambda r, i: r*(x + i), + range(int(k)), 1) + + else: + if x is S.Infinity: + return S.Infinity + elif x is S.NegativeInfinity: + return S.Infinity + else: + if isinstance(x, Poly): + gens = x.gens + if len(gens)!= 1: + raise ValueError("rf only defined for " + "polynomials on one generator") + else: + return 1/reduce(lambda r, i: + r*(x.shift(-i)), + range(1, abs(int(k)) + 1), 1) + else: + return 1/reduce(lambda r, i: + r*(x - i), + range(1, abs(int(k)) + 1), 1) + + if k.is_integer == False: + if x.is_integer and x.is_negative: + return S.Zero + + def _eval_rewrite_as_gamma(self, x, k, piecewise=True, **kwargs): + from sympy.functions.elementary.piecewise import Piecewise + from sympy.functions.special.gamma_functions import gamma + if not piecewise: + if (x <= 0) == True: + return S.NegativeOne**k*gamma(1 - x) / gamma(-k - x + 1) + return gamma(x + k) / gamma(x) + return Piecewise( + (gamma(x + k) / gamma(x), x > 0), + (S.NegativeOne**k*gamma(1 - x) / gamma(-k - x + 1), True)) + + def _eval_rewrite_as_FallingFactorial(self, x, k, **kwargs): + return FallingFactorial(x + k - 1, k) + + def _eval_rewrite_as_factorial(self, x, k, **kwargs): + from sympy.functions.elementary.piecewise import Piecewise + if x.is_integer and k.is_integer: + return Piecewise( + (factorial(k + x - 1)/factorial(x - 1), x > 0), + (S.NegativeOne**k*factorial(-x)/factorial(-k - x), True)) + + def _eval_rewrite_as_binomial(self, x, k, **kwargs): + if k.is_integer: + return factorial(k) * binomial(x + k - 1, k) + + def _eval_rewrite_as_tractable(self, x, k, limitvar=None, **kwargs): + from sympy.functions.special.gamma_functions import gamma + if limitvar: + k_lim = k.subs(limitvar, S.Infinity) + if k_lim is S.Infinity: + return (gamma(x + k).rewrite('tractable', deep=True) / gamma(x)) + elif k_lim is S.NegativeInfinity: + return (S.NegativeOne**k*gamma(1 - x) / gamma(-k - x + 1).rewrite('tractable', deep=True)) + return self.rewrite(gamma).rewrite('tractable', deep=True) + + def _eval_is_integer(self): + return fuzzy_and((self.args[0].is_integer, self.args[1].is_integer, + self.args[1].is_nonnegative)) + + +class FallingFactorial(CombinatorialFunction): + r""" + Falling factorial (related to rising factorial) is a double valued + function arising in concrete mathematics, hypergeometric functions + and series expansions. It is defined by + + .. math:: \texttt{ff(x, k)} = (x)_k = x \cdot (x-1) \cdots (x-k+1) + + where `x` can be arbitrary expression and `k` is an integer. For + more information check "Concrete mathematics" by Graham, pp. 66 + or [1]_. + + When `x` is a `~.Poly` instance of degree $\ge 1$ with single variable, + `(x)_k = x(y) \cdot x(y-1) \cdots x(y-k+1)`, where `y` is the + variable of `x`. This is as described in + + >>> from sympy import ff, Poly, Symbol + >>> from sympy.abc import x + >>> n = Symbol('n', integer=True) + + >>> ff(x, 0) + 1 + >>> ff(5, 5) + 120 + >>> ff(x, 5) == x*(x - 1)*(x - 2)*(x - 3)*(x - 4) + True + >>> ff(Poly(x**2, x), 2) + Poly(x**4 - 2*x**3 + x**2, x, domain='ZZ') + >>> ff(n, n) + factorial(n) + + Rewriting is complicated unless the relationship between + the arguments is known, but falling factorial can + be rewritten in terms of gamma, factorial and binomial + and rising factorial. + + >>> from sympy import factorial, rf, gamma, binomial, Symbol + >>> n = Symbol('n', integer=True, positive=True) + >>> F = ff(n, n - 2) + >>> for i in (rf, ff, factorial, binomial, gamma): + ... F.rewrite(i) + ... + RisingFactorial(3, n - 2) + FallingFactorial(n, n - 2) + factorial(n)/2 + binomial(n, n - 2)*factorial(n - 2) + gamma(n + 1)/2 + + See Also + ======== + + factorial, factorial2, RisingFactorial + + References + ========== + + .. [1] https://mathworld.wolfram.com/FallingFactorial.html + .. [2] Peter Paule, "Greatest Factorial Factorization and Symbolic + Summation", Journal of Symbolic Computation, vol. 20, pp. 235-268, + 1995. + + """ + + @classmethod + def eval(cls, x, k): + x = sympify(x) + k = sympify(k) + + if x is S.NaN or k is S.NaN: + return S.NaN + elif k.is_integer and x == k: + return factorial(x) + elif k.is_Integer: + if k.is_zero: + return S.One + else: + if k.is_positive: + if x is S.Infinity: + return S.Infinity + elif x is S.NegativeInfinity: + if k.is_odd: + return S.NegativeInfinity + else: + return S.Infinity + else: + if isinstance(x, Poly): + gens = x.gens + if len(gens)!= 1: + raise ValueError("ff only defined for " + "polynomials on one generator") + else: + return reduce(lambda r, i: + r*(x.shift(-i)), + range(int(k)), 1) + else: + return reduce(lambda r, i: r*(x - i), + range(int(k)), 1) + else: + if x is S.Infinity: + return S.Infinity + elif x is S.NegativeInfinity: + return S.Infinity + else: + if isinstance(x, Poly): + gens = x.gens + if len(gens)!= 1: + raise ValueError("rf only defined for " + "polynomials on one generator") + else: + return 1/reduce(lambda r, i: + r*(x.shift(i)), + range(1, abs(int(k)) + 1), 1) + else: + return 1/reduce(lambda r, i: r*(x + i), + range(1, abs(int(k)) + 1), 1) + + def _eval_rewrite_as_gamma(self, x, k, piecewise=True, **kwargs): + from sympy.functions.elementary.piecewise import Piecewise + from sympy.functions.special.gamma_functions import gamma + if not piecewise: + if (x < 0) == True: + return S.NegativeOne**k*gamma(k - x) / gamma(-x) + return gamma(x + 1) / gamma(x - k + 1) + return Piecewise( + (gamma(x + 1) / gamma(x - k + 1), x >= 0), + (S.NegativeOne**k*gamma(k - x) / gamma(-x), True)) + + def _eval_rewrite_as_RisingFactorial(self, x, k, **kwargs): + return rf(x - k + 1, k) + + def _eval_rewrite_as_binomial(self, x, k, **kwargs): + if k.is_integer: + return factorial(k) * binomial(x, k) + + def _eval_rewrite_as_factorial(self, x, k, **kwargs): + from sympy.functions.elementary.piecewise import Piecewise + if x.is_integer and k.is_integer: + return Piecewise( + (factorial(x)/factorial(-k + x), x >= 0), + (S.NegativeOne**k*factorial(k - x - 1)/factorial(-x - 1), True)) + + def _eval_rewrite_as_tractable(self, x, k, limitvar=None, **kwargs): + from sympy.functions.special.gamma_functions import gamma + if limitvar: + k_lim = k.subs(limitvar, S.Infinity) + if k_lim is S.Infinity: + return (S.NegativeOne**k*gamma(k - x).rewrite('tractable', deep=True) / gamma(-x)) + elif k_lim is S.NegativeInfinity: + return (gamma(x + 1) / gamma(x - k + 1).rewrite('tractable', deep=True)) + return self.rewrite(gamma).rewrite('tractable', deep=True) + + def _eval_is_integer(self): + return fuzzy_and((self.args[0].is_integer, self.args[1].is_integer, + self.args[1].is_nonnegative)) + + +rf = RisingFactorial +ff = FallingFactorial + +############################################################################### +########################### BINOMIAL COEFFICIENTS ############################# +############################################################################### + + +class binomial(CombinatorialFunction): + r"""Implementation of the binomial coefficient. It can be defined + in two ways depending on its desired interpretation: + + .. math:: \binom{n}{k} = \frac{n!}{k!(n-k)!}\ \text{or}\ + \binom{n}{k} = \frac{(n)_k}{k!} + + First, in a strict combinatorial sense it defines the + number of ways we can choose `k` elements from a set of + `n` elements. In this case both arguments are nonnegative + integers and binomial is computed using an efficient + algorithm based on prime factorization. + + The other definition is generalization for arbitrary `n`, + however `k` must also be nonnegative. This case is very + useful when evaluating summations. + + For the sake of convenience, for negative integer `k` this function + will return zero no matter the other argument. + + To expand the binomial when `n` is a symbol, use either + ``expand_func()`` or ``expand(func=True)``. The former will keep + the polynomial in factored form while the latter will expand the + polynomial itself. See examples for details. + + Examples + ======== + + >>> from sympy import Symbol, Rational, binomial, expand_func + >>> n = Symbol('n', integer=True, positive=True) + + >>> binomial(15, 8) + 6435 + + >>> binomial(n, -1) + 0 + + Rows of Pascal's triangle can be generated with the binomial function: + + >>> for N in range(8): + ... print([binomial(N, i) for i in range(N + 1)]) + ... + [1] + [1, 1] + [1, 2, 1] + [1, 3, 3, 1] + [1, 4, 6, 4, 1] + [1, 5, 10, 10, 5, 1] + [1, 6, 15, 20, 15, 6, 1] + [1, 7, 21, 35, 35, 21, 7, 1] + + As can a given diagonal, e.g. the 4th diagonal: + + >>> N = -4 + >>> [binomial(N, i) for i in range(1 - N)] + [1, -4, 10, -20, 35] + + >>> binomial(Rational(5, 4), 3) + -5/128 + >>> binomial(Rational(-5, 4), 3) + -195/128 + + >>> binomial(n, 3) + binomial(n, 3) + + >>> binomial(n, 3).expand(func=True) + n**3/6 - n**2/2 + n/3 + + >>> expand_func(binomial(n, 3)) + n*(n - 2)*(n - 1)/6 + + In many cases, we can also compute binomial coefficients modulo a + prime p quickly using Lucas' Theorem [2]_, though we need to include + `evaluate=False` to postpone evaluation: + + >>> from sympy import Mod + >>> Mod(binomial(156675, 4433, evaluate=False), 10**5 + 3) + 28625 + + Using a generalisation of Lucas's Theorem given by Granville [3]_, + we can extend this to arbitrary n: + + >>> Mod(binomial(10**18, 10**12, evaluate=False), (10**5 + 3)**2) + 3744312326 + + References + ========== + + .. [1] https://www.johndcook.com/blog/binomial_coefficients/ + .. [2] https://en.wikipedia.org/wiki/Lucas%27s_theorem + .. [3] Binomial coefficients modulo prime powers, Andrew Granville, + Available: https://web.archive.org/web/20170202003812/http://www.dms.umontreal.ca/~andrew/PDF/BinCoeff.pdf + """ + + def fdiff(self, argindex=1): + from sympy.functions.special.gamma_functions import polygamma + if argindex == 1: + # https://functions.wolfram.com/GammaBetaErf/Binomial/20/01/01/ + n, k = self.args + return binomial(n, k)*(polygamma(0, n + 1) - \ + polygamma(0, n - k + 1)) + elif argindex == 2: + # https://functions.wolfram.com/GammaBetaErf/Binomial/20/01/02/ + n, k = self.args + return binomial(n, k)*(polygamma(0, n - k + 1) - \ + polygamma(0, k + 1)) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def _eval(self, n, k): + # n.is_Number and k.is_Integer and k != 1 and n != k + + if k.is_Integer: + if n.is_Integer and n >= 0: + n, k = int(n), int(k) + + if k > n: + return S.Zero + elif k > n // 2: + k = n - k + + # XXX: This conditional logic should be moved to + # sympy.external.gmpy and the pure Python version of bincoef + # should be moved to sympy.external.ntheory. + if _gmpy is not None: + return Integer(_gmpy.bincoef(n, k)) + + d, result = n - k, 1 + for i in range(1, k + 1): + d += 1 + result = result * d // i + return Integer(result) + else: + d, result = n - k, 1 + for i in range(1, k + 1): + d += 1 + result *= d + return result / _factorial(k) + + @classmethod + def eval(cls, n, k): + n, k = map(sympify, (n, k)) + d = n - k + n_nonneg, n_isint = n.is_nonnegative, n.is_integer + if k.is_zero or ((n_nonneg or n_isint is False) + and d.is_zero): + return S.One + if (k - 1).is_zero or ((n_nonneg or n_isint is False) + and (d - 1).is_zero): + return n + if k.is_integer: + if k.is_negative or (n_nonneg and n_isint and d.is_negative): + return S.Zero + elif n.is_number: + res = cls._eval(n, k) + return res.expand(basic=True) if res else res + elif n_nonneg is False and n_isint: + # a special case when binomial evaluates to complex infinity + return S.ComplexInfinity + elif k.is_number: + from sympy.functions.special.gamma_functions import gamma + return gamma(n + 1)/(gamma(k + 1)*gamma(n - k + 1)) + + def _eval_Mod(self, q): + n, k = self.args + + if any(x.is_integer is False for x in (n, k, q)): + raise ValueError("Integers expected for binomial Mod") + + if all(x.is_Integer for x in (n, k, q)): + n, k = map(int, (n, k)) + aq, res = abs(q), 1 + + # handle negative integers k or n + if k < 0: + return S.Zero + if n < 0: + n = -n + k - 1 + res = -1 if k%2 else 1 + + # non negative integers k and n + if k > n: + return S.Zero + + isprime = aq.is_prime + aq = int(aq) + if isprime: + if aq < n: + # use Lucas Theorem + N, K = n, k + while N or K: + res = res*binomial(N % aq, K % aq) % aq + N, K = N // aq, K // aq + + else: + # use Factorial Modulo + d = n - k + if k > d: + k, d = d, k + kf = 1 + for i in range(2, k + 1): + kf = kf*i % aq + df = kf + for i in range(k + 1, d + 1): + df = df*i % aq + res *= df + for i in range(d + 1, n + 1): + res = res*i % aq + + res *= pow(kf*df % aq, aq - 2, aq) + res %= aq + + elif _sqrt(q) < k and q != 1: + res = binomial_mod(n, k, q) + + else: + # Binomial Factorization is performed by calculating the + # exponents of primes <= n in `n! /(k! (n - k)!)`, + # for non-negative integers n and k. As the exponent of + # prime in n! is e_p(n) = [n/p] + [n/p**2] + ... + # the exponent of prime in binomial(n, k) would be + # e_p(n) - e_p(k) - e_p(n - k) + M = int(_sqrt(n)) + for prime in sieve.primerange(2, n + 1): + if prime > n - k: + res = res*prime % aq + elif prime > n // 2: + continue + elif prime > M: + if n % prime < k % prime: + res = res*prime % aq + else: + N, K = n, k + exp = a = 0 + + while N > 0: + a = int((N % prime) < (K % prime + a)) + N, K = N // prime, K // prime + exp += a + + if exp > 0: + res *= pow(prime, exp, aq) + res %= aq + + return S(res % q) + + def _eval_expand_func(self, **hints): + """ + Function to expand binomial(n, k) when m is positive integer + Also, + n is self.args[0] and k is self.args[1] while using binomial(n, k) + """ + n = self.args[0] + if n.is_Number: + return binomial(*self.args) + + k = self.args[1] + if (n-k).is_Integer: + k = n - k + + if k.is_Integer: + if k.is_zero: + return S.One + elif k.is_negative: + return S.Zero + else: + n, result = self.args[0], 1 + for i in range(1, k + 1): + result *= n - k + i + return result / _factorial(k) + else: + return binomial(*self.args) + + def _eval_rewrite_as_factorial(self, n, k, **kwargs): + return factorial(n)/(factorial(k)*factorial(n - k)) + + def _eval_rewrite_as_gamma(self, n, k, piecewise=True, **kwargs): + from sympy.functions.special.gamma_functions import gamma + return gamma(n + 1)/(gamma(k + 1)*gamma(n - k + 1)) + + def _eval_rewrite_as_tractable(self, n, k, limitvar=None, **kwargs): + return self._eval_rewrite_as_gamma(n, k).rewrite('tractable') + + def _eval_rewrite_as_FallingFactorial(self, n, k, **kwargs): + if k.is_integer: + return ff(n, k) / factorial(k) + + def _eval_is_integer(self): + n, k = self.args + if n.is_integer and k.is_integer: + return True + elif k.is_integer is False: + return False + + def _eval_is_nonnegative(self): + n, k = self.args + if n.is_integer and k.is_integer: + if n.is_nonnegative or k.is_negative or k.is_even: + return True + elif k.is_even is False: + return False + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.functions.special.gamma_functions import gamma + return self.rewrite(gamma)._eval_as_leading_term(x, logx=logx, cdir=cdir) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/numbers.py b/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/numbers.py new file mode 100644 index 0000000000000000000000000000000000000000..c0dfc518d4a6784712341edaa5731145469a8d1e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/numbers.py @@ -0,0 +1,3196 @@ +""" +This module implements some special functions that commonly appear in +combinatorial contexts (e.g. in power series); in particular, +sequences of rational numbers such as Bernoulli and Fibonacci numbers. + +Factorials, binomial coefficients and related functions are located in +the separate 'factorials' module. +""" +from __future__ import annotations +from math import prod +from collections import defaultdict +from typing import Callable + +from sympy.core import S, Symbol, Add, Dummy +from sympy.core.cache import cacheit +from sympy.core.containers import Dict +from sympy.core.expr import Expr +from sympy.core.function import ArgumentIndexError, DefinedFunction, expand_mul +from sympy.core.logic import fuzzy_not +from sympy.core.mul import Mul +from sympy.core.numbers import E, I, pi, oo, Rational, Integer +from sympy.core.relational import Eq, is_le, is_gt, is_lt +from sympy.external.gmpy import SYMPY_INTS, remove, lcm, legendre, jacobi, kronecker +from sympy.functions.combinatorial.factorials import (binomial, + factorial, subfactorial) +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.piecewise import Piecewise +from sympy.ntheory.factor_ import (factorint, _divisor_sigma, is_carmichael, + find_carmichael_numbers_in_range, find_first_n_carmichaels) +from sympy.ntheory.generate import _primepi +from sympy.ntheory.partitions_ import _partition, _partition_rec +from sympy.ntheory.primetest import isprime, is_square +from sympy.polys.appellseqs import bernoulli_poly, euler_poly, genocchi_poly +from sympy.polys.polytools import cancel +from sympy.utilities.enumerative import MultisetPartitionTraverser +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.utilities.iterables import multiset, multiset_derangements, iterable +from sympy.utilities.memoization import recurrence_memo +from sympy.utilities.misc import as_int + +from mpmath import mp, workprec +from mpmath.libmp import ifib as _ifib + + +def _product(a, b): + return prod(range(a, b + 1)) + + +# Dummy symbol used for computing polynomial sequences +_sym = Symbol('x') + + +#----------------------------------------------------------------------------# +# # +# Carmichael numbers # +# # +#----------------------------------------------------------------------------# + +class carmichael(DefinedFunction): + r""" + Carmichael Numbers: + + Certain cryptographic algorithms make use of big prime numbers. + However, checking whether a big number is prime is not so easy. + Randomized prime number checking tests exist that offer a high degree of + confidence of accurate determination at low cost, such as the Fermat test. + + Let 'a' be a random number between $2$ and $n - 1$, where $n$ is the + number whose primality we are testing. Then, $n$ is probably prime if it + satisfies the modular arithmetic congruence relation: + + .. math :: a^{n-1} = 1 \pmod{n} + + (where mod refers to the modulo operation) + + If a number passes the Fermat test several times, then it is prime with a + high probability. + + Unfortunately, certain composite numbers (non-primes) still pass the Fermat + test with every number smaller than themselves. + These numbers are called Carmichael numbers. + + A Carmichael number will pass a Fermat primality test to every base $b$ + relatively prime to the number, even though it is not actually prime. + This makes tests based on Fermat's Little Theorem less effective than + strong probable prime tests such as the Baillie-PSW primality test and + the Miller-Rabin primality test. + + Examples + ======== + + >>> from sympy.ntheory.factor_ import find_first_n_carmichaels, find_carmichael_numbers_in_range + >>> find_first_n_carmichaels(5) + [561, 1105, 1729, 2465, 2821] + >>> find_carmichael_numbers_in_range(0, 562) + [561] + >>> find_carmichael_numbers_in_range(0,1000) + [561] + >>> find_carmichael_numbers_in_range(0,2000) + [561, 1105, 1729] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Carmichael_number + .. [2] https://en.wikipedia.org/wiki/Fermat_primality_test + .. [3] https://www.jstor.org/stable/23248683?seq=1#metadata_info_tab_contents + """ + + @staticmethod + def is_perfect_square(n): + sympy_deprecation_warning( + """ +is_perfect_square is just a wrapper around sympy.ntheory.primetest.is_square +so use that directly instead. + """, + deprecated_since_version="1.11", + active_deprecations_target='deprecated-carmichael-static-methods', + ) + return is_square(n) + + @staticmethod + def divides(p, n): + sympy_deprecation_warning( + """ + divides can be replaced by directly testing n % p == 0. + """, + deprecated_since_version="1.11", + active_deprecations_target='deprecated-carmichael-static-methods', + ) + return n % p == 0 + + @staticmethod + def is_prime(n): + sympy_deprecation_warning( + """ +is_prime is just a wrapper around sympy.ntheory.primetest.isprime so use that +directly instead. + """, + deprecated_since_version="1.11", + active_deprecations_target='deprecated-carmichael-static-methods', + ) + return isprime(n) + + @staticmethod + def is_carmichael(n): + sympy_deprecation_warning( + """ +is_carmichael is just a wrapper around sympy.ntheory.factor_.is_carmichael so use that +directly instead. + """, + deprecated_since_version="1.13", + active_deprecations_target='deprecated-ntheory-symbolic-functions', + ) + return is_carmichael(n) + + @staticmethod + def find_carmichael_numbers_in_range(x, y): + sympy_deprecation_warning( + """ +find_carmichael_numbers_in_range is just a wrapper around sympy.ntheory.factor_.find_carmichael_numbers_in_range so use that +directly instead. + """, + deprecated_since_version="1.13", + active_deprecations_target='deprecated-ntheory-symbolic-functions', + ) + return find_carmichael_numbers_in_range(x, y) + + @staticmethod + def find_first_n_carmichaels(n): + sympy_deprecation_warning( + """ +find_first_n_carmichaels is just a wrapper around sympy.ntheory.factor_.find_first_n_carmichaels so use that +directly instead. + """, + deprecated_since_version="1.13", + active_deprecations_target='deprecated-ntheory-symbolic-functions', + ) + return find_first_n_carmichaels(n) + + +#----------------------------------------------------------------------------# +# # +# Fibonacci numbers # +# # +#----------------------------------------------------------------------------# + + +class fibonacci(DefinedFunction): + r""" + Fibonacci numbers / Fibonacci polynomials + + The Fibonacci numbers are the integer sequence defined by the + initial terms `F_0 = 0`, `F_1 = 1` and the two-term recurrence + relation `F_n = F_{n-1} + F_{n-2}`. This definition + extended to arbitrary real and complex arguments using + the formula + + .. math :: F_z = \frac{\phi^z - \cos(\pi z) \phi^{-z}}{\sqrt 5} + + The Fibonacci polynomials are defined by `F_1(x) = 1`, + `F_2(x) = x`, and `F_n(x) = x*F_{n-1}(x) + F_{n-2}(x)` for `n > 2`. + For all positive integers `n`, `F_n(1) = F_n`. + + * ``fibonacci(n)`` gives the `n^{th}` Fibonacci number, `F_n` + * ``fibonacci(n, x)`` gives the `n^{th}` Fibonacci polynomial in `x`, `F_n(x)` + + Examples + ======== + + >>> from sympy import fibonacci, Symbol + + >>> [fibonacci(x) for x in range(11)] + [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55] + >>> fibonacci(5, Symbol('t')) + t**4 + 3*t**2 + 1 + + See Also + ======== + + bell, bernoulli, catalan, euler, harmonic, lucas, genocchi, partition, tribonacci + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Fibonacci_number + .. [2] https://mathworld.wolfram.com/FibonacciNumber.html + + """ + + @staticmethod + def _fib(n): + return _ifib(n) + + @staticmethod + @recurrence_memo([None, S.One, _sym]) + def _fibpoly(n, prev): + return (prev[-2] + _sym*prev[-1]).expand() + + @classmethod + def eval(cls, n, sym=None): + if n is S.Infinity: + return S.Infinity + + if n.is_Integer: + if sym is None: + n = int(n) + if n < 0: + return S.NegativeOne**(n + 1) * fibonacci(-n) + else: + return Integer(cls._fib(n)) + else: + if n < 1: + raise ValueError("Fibonacci polynomials are defined " + "only for positive integer indices.") + return cls._fibpoly(n).subs(_sym, sym) + + def _eval_rewrite_as_tractable(self, n, **kwargs): + from sympy.functions import sqrt, cos + return (S.GoldenRatio**n - cos(S.Pi*n)/S.GoldenRatio**n)/sqrt(5) + + def _eval_rewrite_as_sqrt(self, n, **kwargs): + from sympy.functions.elementary.miscellaneous import sqrt + return 2**(-n)*sqrt(5)*((1 + sqrt(5))**n - (-sqrt(5) + 1)**n) / 5 + + def _eval_rewrite_as_GoldenRatio(self,n, **kwargs): + return (S.GoldenRatio**n - 1/(-S.GoldenRatio)**n)/(2*S.GoldenRatio-1) + + +#----------------------------------------------------------------------------# +# # +# Lucas numbers # +# # +#----------------------------------------------------------------------------# + + +class lucas(DefinedFunction): + """ + Lucas numbers + + Lucas numbers satisfy a recurrence relation similar to that of + the Fibonacci sequence, in which each term is the sum of the + preceding two. They are generated by choosing the initial + values `L_0 = 2` and `L_1 = 1`. + + * ``lucas(n)`` gives the `n^{th}` Lucas number + + Examples + ======== + + >>> from sympy import lucas + + >>> [lucas(x) for x in range(11)] + [2, 1, 3, 4, 7, 11, 18, 29, 47, 76, 123] + + See Also + ======== + + bell, bernoulli, catalan, euler, fibonacci, harmonic, genocchi, partition, tribonacci + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Lucas_number + .. [2] https://mathworld.wolfram.com/LucasNumber.html + + """ + + @classmethod + def eval(cls, n): + if n is S.Infinity: + return S.Infinity + + if n.is_Integer: + return fibonacci(n + 1) + fibonacci(n - 1) + + def _eval_rewrite_as_sqrt(self, n, **kwargs): + from sympy.functions.elementary.miscellaneous import sqrt + return 2**(-n)*((1 + sqrt(5))**n + (-sqrt(5) + 1)**n) + + +#----------------------------------------------------------------------------# +# # +# Tribonacci numbers # +# # +#----------------------------------------------------------------------------# + + +class tribonacci(DefinedFunction): + r""" + Tribonacci numbers / Tribonacci polynomials + + The Tribonacci numbers are the integer sequence defined by the + initial terms `T_0 = 0`, `T_1 = 1`, `T_2 = 1` and the three-term + recurrence relation `T_n = T_{n-1} + T_{n-2} + T_{n-3}`. + + The Tribonacci polynomials are defined by `T_0(x) = 0`, `T_1(x) = 1`, + `T_2(x) = x^2`, and `T_n(x) = x^2 T_{n-1}(x) + x T_{n-2}(x) + T_{n-3}(x)` + for `n > 2`. For all positive integers `n`, `T_n(1) = T_n`. + + * ``tribonacci(n)`` gives the `n^{th}` Tribonacci number, `T_n` + * ``tribonacci(n, x)`` gives the `n^{th}` Tribonacci polynomial in `x`, `T_n(x)` + + Examples + ======== + + >>> from sympy import tribonacci, Symbol + + >>> [tribonacci(x) for x in range(11)] + [0, 1, 1, 2, 4, 7, 13, 24, 44, 81, 149] + >>> tribonacci(5, Symbol('t')) + t**8 + 3*t**5 + 3*t**2 + + See Also + ======== + + bell, bernoulli, catalan, euler, fibonacci, harmonic, lucas, genocchi, partition + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Generalizations_of_Fibonacci_numbers#Tribonacci_numbers + .. [2] https://mathworld.wolfram.com/TribonacciNumber.html + .. [3] https://oeis.org/A000073 + + """ + + @staticmethod + @recurrence_memo([S.Zero, S.One, S.One]) + def _trib(n, prev): + return (prev[-3] + prev[-2] + prev[-1]) + + @staticmethod + @recurrence_memo([S.Zero, S.One, _sym**2]) + def _tribpoly(n, prev): + return (prev[-3] + _sym*prev[-2] + _sym**2*prev[-1]).expand() + + @classmethod + def eval(cls, n, sym=None): + if n is S.Infinity: + return S.Infinity + + if n.is_Integer: + n = int(n) + if n < 0: + raise ValueError("Tribonacci polynomials are defined " + "only for non-negative integer indices.") + if sym is None: + return Integer(cls._trib(n)) + else: + return cls._tribpoly(n).subs(_sym, sym) + + def _eval_rewrite_as_sqrt(self, n, **kwargs): + from sympy.functions.elementary.miscellaneous import cbrt, sqrt + w = (-1 + S.ImaginaryUnit * sqrt(3)) / 2 + a = (1 + cbrt(19 + 3*sqrt(33)) + cbrt(19 - 3*sqrt(33))) / 3 + b = (1 + w*cbrt(19 + 3*sqrt(33)) + w**2*cbrt(19 - 3*sqrt(33))) / 3 + c = (1 + w**2*cbrt(19 + 3*sqrt(33)) + w*cbrt(19 - 3*sqrt(33))) / 3 + Tn = (a**(n + 1)/((a - b)*(a - c)) + + b**(n + 1)/((b - a)*(b - c)) + + c**(n + 1)/((c - a)*(c - b))) + return Tn + + def _eval_rewrite_as_TribonacciConstant(self, n, **kwargs): + from sympy.functions.elementary.integers import floor + from sympy.functions.elementary.miscellaneous import cbrt, sqrt + b = cbrt(586 + 102*sqrt(33)) + Tn = 3 * b * S.TribonacciConstant**n / (b**2 - 2*b + 4) + return floor(Tn + S.Half) + + +#----------------------------------------------------------------------------# +# # +# Bernoulli numbers # +# # +#----------------------------------------------------------------------------# + + +class bernoulli(DefinedFunction): + r""" + Bernoulli numbers / Bernoulli polynomials / Bernoulli function + + The Bernoulli numbers are a sequence of rational numbers + defined by `B_0 = 1` and the recursive relation (`n > 0`): + + .. math :: n+1 = \sum_{k=0}^n \binom{n+1}{k} B_k + + They are also commonly defined by their exponential generating + function, which is `\frac{x}{1 - e^{-x}}`. For odd indices > 1, + the Bernoulli numbers are zero. + + The Bernoulli polynomials satisfy the analogous formula: + + .. math :: B_n(x) = \sum_{k=0}^n (-1)^k \binom{n}{k} B_k x^{n-k} + + Bernoulli numbers and Bernoulli polynomials are related as + `B_n(1) = B_n`. + + The generalized Bernoulli function `\operatorname{B}(s, a)` + is defined for any complex `s` and `a`, except where `a` is a + nonpositive integer and `s` is not a nonnegative integer. It is + an entire function of `s` for fixed `a`, related to the Hurwitz + zeta function by + + .. math:: \operatorname{B}(s, a) = \begin{cases} + -s \zeta(1-s, a) & s \ne 0 \\ 1 & s = 0 \end{cases} + + When `s` is a nonnegative integer this function reduces to the + Bernoulli polynomials: `\operatorname{B}(n, x) = B_n(x)`. When + `a` is omitted it is assumed to be 1, yielding the (ordinary) + Bernoulli function which interpolates the Bernoulli numbers and is + related to the Riemann zeta function. + + We compute Bernoulli numbers using Ramanujan's formula: + + .. math :: B_n = \frac{A(n) - S(n)}{\binom{n+3}{n}} + + where: + + .. math :: A(n) = \begin{cases} \frac{n+3}{3} & + n \equiv 0\ \text{or}\ 2 \pmod{6} \\ + -\frac{n+3}{6} & n \equiv 4 \pmod{6} \end{cases} + + and: + + .. math :: S(n) = \sum_{k=1}^{[n/6]} \binom{n+3}{n-6k} B_{n-6k} + + This formula is similar to the sum given in the definition, but + cuts `\frac{2}{3}` of the terms. For Bernoulli polynomials, we use + Appell sequences. + + For `n` a nonnegative integer and `s`, `a`, `x` arbitrary complex numbers, + + * ``bernoulli(n)`` gives the nth Bernoulli number, `B_n` + * ``bernoulli(s)`` gives the Bernoulli function `\operatorname{B}(s)` + * ``bernoulli(n, x)`` gives the nth Bernoulli polynomial in `x`, `B_n(x)` + * ``bernoulli(s, a)`` gives the generalized Bernoulli function + `\operatorname{B}(s, a)` + + .. versionchanged:: 1.12 + ``bernoulli(1)`` gives `+\frac{1}{2}` instead of `-\frac{1}{2}`. + This choice of value confers several theoretical advantages [5]_, + including the extension to complex parameters described above + which this function now implements. The previous behavior, defined + only for nonnegative integers `n`, can be obtained with + ``(-1)**n*bernoulli(n)``. + + Examples + ======== + + >>> from sympy import bernoulli + >>> from sympy.abc import x + >>> [bernoulli(n) for n in range(11)] + [1, 1/2, 1/6, 0, -1/30, 0, 1/42, 0, -1/30, 0, 5/66] + >>> bernoulli(1000001) + 0 + >>> bernoulli(3, x) + x**3 - 3*x**2/2 + x/2 + + See Also + ======== + + andre, bell, catalan, euler, fibonacci, harmonic, lucas, genocchi, + partition, tribonacci, sympy.polys.appellseqs.bernoulli_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Bernoulli_number + .. [2] https://en.wikipedia.org/wiki/Bernoulli_polynomial + .. [3] https://mathworld.wolfram.com/BernoulliNumber.html + .. [4] https://mathworld.wolfram.com/BernoulliPolynomial.html + .. [5] Peter Luschny, "The Bernoulli Manifesto", + https://luschny.de/math/zeta/The-Bernoulli-Manifesto.html + .. [6] Peter Luschny, "An introduction to the Bernoulli function", + https://arxiv.org/abs/2009.06743 + + """ + + args: tuple[Integer] + + # Calculates B_n for positive even n + @staticmethod + def _calc_bernoulli(n): + s = 0 + a = int(binomial(n + 3, n - 6)) + for j in range(1, n//6 + 1): + s += a * bernoulli(n - 6*j) + # Avoid computing each binomial coefficient from scratch + a *= _product(n - 6 - 6*j + 1, n - 6*j) + a //= _product(6*j + 4, 6*j + 9) + if n % 6 == 4: + s = -Rational(n + 3, 6) - s + else: + s = Rational(n + 3, 3) - s + return s / binomial(n + 3, n) + + # We implement a specialized memoization scheme to handle each + # case modulo 6 separately + _cache = {0: S.One, 1: Rational(1, 2), 2: Rational(1, 6), 4: Rational(-1, 30)} + _highest = {0: 0, 1: 1, 2: 2, 4: 4} + + @classmethod + def eval(cls, n, x=None): + if x is S.One: + return cls(n) + elif n.is_zero: + return S.One + elif n.is_integer is False or n.is_nonnegative is False: + if x is not None and x.is_Integer and x.is_nonpositive: + return S.NaN + return + # Bernoulli numbers + elif x is None: + if n is S.One: + return S.Half + elif n.is_odd and (n-1).is_positive: + return S.Zero + elif n.is_Number: + n = int(n) + # Use mpmath for enormous Bernoulli numbers + if n > 500: + p, q = mp.bernfrac(n) + return Rational(int(p), int(q)) + case = n % 6 + highest_cached = cls._highest[case] + if n <= highest_cached: + return cls._cache[n] + # To avoid excessive recursion when, say, bernoulli(1000) is + # requested, calculate and cache the entire sequence ... B_988, + # B_994, B_1000 in increasing order + for i in range(highest_cached + 6, n + 6, 6): + b = cls._calc_bernoulli(i) + cls._cache[i] = b + cls._highest[case] = i + return b + # Bernoulli polynomials + elif n.is_Number: + return bernoulli_poly(n, x) + + def _eval_rewrite_as_zeta(self, n, x=1, **kwargs): + from sympy.functions.special.zeta_functions import zeta + return Piecewise((1, Eq(n, 0)), (-n * zeta(1-n, x), True)) + + def _eval_evalf(self, prec): + if not all(x.is_number for x in self.args): + return + n = self.args[0]._to_mpmath(prec) + x = (self.args[1] if len(self.args) > 1 else S.One)._to_mpmath(prec) + with workprec(prec): + if n == 0: + res = mp.mpf(1) + elif n == 1: + res = x - mp.mpf(0.5) + elif mp.isint(n) and n >= 0: + res = mp.bernoulli(n) if x == 1 else mp.bernpoly(n, x) + else: + res = -n * mp.zeta(1-n, x) + return Expr._from_mpmath(res, prec) + + +#----------------------------------------------------------------------------# +# # +# Bell numbers # +# # +#----------------------------------------------------------------------------# + + +class bell(DefinedFunction): + r""" + Bell numbers / Bell polynomials + + The Bell numbers satisfy `B_0 = 1` and + + .. math:: B_n = \sum_{k=0}^{n-1} \binom{n-1}{k} B_k. + + They are also given by: + + .. math:: B_n = \frac{1}{e} \sum_{k=0}^{\infty} \frac{k^n}{k!}. + + The Bell polynomials are given by `B_0(x) = 1` and + + .. math:: B_n(x) = x \sum_{k=1}^{n-1} \binom{n-1}{k-1} B_{k-1}(x). + + The second kind of Bell polynomials (are sometimes called "partial" Bell + polynomials or incomplete Bell polynomials) are defined as + + .. math:: B_{n,k}(x_1, x_2,\dotsc x_{n-k+1}) = + \sum_{j_1+j_2+j_2+\dotsb=k \atop j_1+2j_2+3j_2+\dotsb=n} + \frac{n!}{j_1!j_2!\dotsb j_{n-k+1}!} + \left(\frac{x_1}{1!} \right)^{j_1} + \left(\frac{x_2}{2!} \right)^{j_2} \dotsb + \left(\frac{x_{n-k+1}}{(n-k+1)!} \right) ^{j_{n-k+1}}. + + * ``bell(n)`` gives the `n^{th}` Bell number, `B_n`. + * ``bell(n, x)`` gives the `n^{th}` Bell polynomial, `B_n(x)`. + * ``bell(n, k, (x1, x2, ...))`` gives Bell polynomials of the second kind, + `B_{n,k}(x_1, x_2, \dotsc, x_{n-k+1})`. + + Notes + ===== + + Not to be confused with Bernoulli numbers and Bernoulli polynomials, + which use the same notation. + + Examples + ======== + + >>> from sympy import bell, Symbol, symbols + + >>> [bell(n) for n in range(11)] + [1, 1, 2, 5, 15, 52, 203, 877, 4140, 21147, 115975] + >>> bell(30) + 846749014511809332450147 + >>> bell(4, Symbol('t')) + t**4 + 6*t**3 + 7*t**2 + t + >>> bell(6, 2, symbols('x:6')[1:]) + 6*x1*x5 + 15*x2*x4 + 10*x3**2 + + See Also + ======== + + bernoulli, catalan, euler, fibonacci, harmonic, lucas, genocchi, partition, tribonacci + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Bell_number + .. [2] https://mathworld.wolfram.com/BellNumber.html + .. [3] https://mathworld.wolfram.com/BellPolynomial.html + + """ + + @staticmethod + @recurrence_memo([1, 1]) + def _bell(n, prev): + s = 1 + a = 1 + for k in range(1, n): + a = a * (n - k) // k + s += a * prev[k] + return s + + @staticmethod + @recurrence_memo([S.One, _sym]) + def _bell_poly(n, prev): + s = 1 + a = 1 + for k in range(2, n + 1): + a = a * (n - k + 1) // (k - 1) + s += a * prev[k - 1] + return expand_mul(_sym * s) + + @staticmethod + def _bell_incomplete_poly(n, k, symbols): + r""" + The second kind of Bell polynomials (incomplete Bell polynomials). + + Calculated by recurrence formula: + + .. math:: B_{n,k}(x_1, x_2, \dotsc, x_{n-k+1}) = + \sum_{m=1}^{n-k+1} + \x_m \binom{n-1}{m-1} B_{n-m,k-1}(x_1, x_2, \dotsc, x_{n-m-k}) + + where + `B_{0,0} = 1;` + `B_{n,0} = 0; for n \ge 1` + `B_{0,k} = 0; for k \ge 1` + + """ + if (n == 0) and (k == 0): + return S.One + elif (n == 0) or (k == 0): + return S.Zero + s = S.Zero + a = S.One + for m in range(1, n - k + 2): + s += a * bell._bell_incomplete_poly( + n - m, k - 1, symbols) * symbols[m - 1] + a = a * (n - m) / m + return expand_mul(s) + + @classmethod + def eval(cls, n, k_sym=None, symbols=None): + if n is S.Infinity: + if k_sym is None: + return S.Infinity + else: + raise ValueError("Bell polynomial is not defined") + + if n.is_negative or n.is_integer is False: + raise ValueError("a non-negative integer expected") + + if n.is_Integer and n.is_nonnegative: + if k_sym is None: + return Integer(cls._bell(int(n))) + elif symbols is None: + return cls._bell_poly(int(n)).subs(_sym, k_sym) + else: + r = cls._bell_incomplete_poly(int(n), int(k_sym), symbols) + return r + + def _eval_rewrite_as_Sum(self, n, k_sym=None, symbols=None, **kwargs): + from sympy.concrete.summations import Sum + if (k_sym is not None) or (symbols is not None): + return self + + # Dobinski's formula + if not n.is_nonnegative: + return self + k = Dummy('k', integer=True, nonnegative=True) + return 1 / E * Sum(k**n / factorial(k), (k, 0, S.Infinity)) + + +#----------------------------------------------------------------------------# +# # +# Harmonic numbers # +# # +#----------------------------------------------------------------------------# + + +class harmonic(DefinedFunction): + r""" + Harmonic numbers + + The nth harmonic number is given by `\operatorname{H}_{n} = + 1 + \frac{1}{2} + \frac{1}{3} + \ldots + \frac{1}{n}`. + + More generally: + + .. math:: \operatorname{H}_{n,m} = \sum_{k=1}^{n} \frac{1}{k^m} + + As `n \rightarrow \infty`, `\operatorname{H}_{n,m} \rightarrow \zeta(m)`, + the Riemann zeta function. + + * ``harmonic(n)`` gives the nth harmonic number, `\operatorname{H}_n` + + * ``harmonic(n, m)`` gives the nth generalized harmonic number + of order `m`, `\operatorname{H}_{n,m}`, where + ``harmonic(n) == harmonic(n, 1)`` + + This function can be extended to complex `n` and `m` where `n` is not a + negative integer or `m` is a nonpositive integer as + + .. math:: \operatorname{H}_{n,m} = \begin{cases} \zeta(m) - \zeta(m, n+1) + & m \ne 1 \\ \psi(n+1) + \gamma & m = 1 \end{cases} + + Examples + ======== + + >>> from sympy import harmonic, oo + + >>> [harmonic(n) for n in range(6)] + [0, 1, 3/2, 11/6, 25/12, 137/60] + >>> [harmonic(n, 2) for n in range(6)] + [0, 1, 5/4, 49/36, 205/144, 5269/3600] + >>> harmonic(oo, 2) + pi**2/6 + + >>> from sympy import Symbol, Sum + >>> n = Symbol("n") + + >>> harmonic(n).rewrite(Sum) + Sum(1/_k, (_k, 1, n)) + + We can evaluate harmonic numbers for all integral and positive + rational arguments: + + >>> from sympy import S, expand_func, simplify + >>> harmonic(8) + 761/280 + >>> harmonic(11) + 83711/27720 + + >>> H = harmonic(1/S(3)) + >>> H + harmonic(1/3) + >>> He = expand_func(H) + >>> He + -log(6) - sqrt(3)*pi/6 + 2*Sum(log(sin(_k*pi/3))*cos(2*_k*pi/3), (_k, 1, 1)) + + 3*Sum(1/(3*_k + 1), (_k, 0, 0)) + >>> He.doit() + -log(6) - sqrt(3)*pi/6 - log(sqrt(3)/2) + 3 + >>> H = harmonic(25/S(7)) + >>> He = simplify(expand_func(H).doit()) + >>> He + log(sin(2*pi/7)**(2*cos(16*pi/7))/(14*sin(pi/7)**(2*cos(pi/7))*cos(pi/14)**(2*sin(pi/14)))) + pi*tan(pi/14)/2 + 30247/9900 + >>> He.n(40) + 1.983697455232980674869851942390639915940 + >>> harmonic(25/S(7)).n(40) + 1.983697455232980674869851942390639915940 + + We can rewrite harmonic numbers in terms of polygamma functions: + + >>> from sympy import digamma, polygamma + >>> m = Symbol("m", integer=True, positive=True) + + >>> harmonic(n).rewrite(digamma) + polygamma(0, n + 1) + EulerGamma + + >>> harmonic(n).rewrite(polygamma) + polygamma(0, n + 1) + EulerGamma + + >>> harmonic(n,3).rewrite(polygamma) + polygamma(2, n + 1)/2 + zeta(3) + + >>> simplify(harmonic(n,m).rewrite(polygamma)) + Piecewise((polygamma(0, n + 1) + EulerGamma, Eq(m, 1)), + (-(-1)**m*polygamma(m - 1, n + 1)/factorial(m - 1) + zeta(m), True)) + + Integer offsets in the argument can be pulled out: + + >>> from sympy import expand_func + + >>> expand_func(harmonic(n+4)) + harmonic(n) + 1/(n + 4) + 1/(n + 3) + 1/(n + 2) + 1/(n + 1) + + >>> expand_func(harmonic(n-4)) + harmonic(n) - 1/(n - 1) - 1/(n - 2) - 1/(n - 3) - 1/n + + Some limits can be computed as well: + + >>> from sympy import limit, oo + + >>> limit(harmonic(n), n, oo) + oo + + >>> limit(harmonic(n, 2), n, oo) + pi**2/6 + + >>> limit(harmonic(n, 3), n, oo) + zeta(3) + + For `m > 1`, `H_{n,m}` tends to `\zeta(m)` in the limit of infinite `n`: + + >>> m = Symbol("m", positive=True) + >>> limit(harmonic(n, m+1), n, oo) + zeta(m + 1) + + See Also + ======== + + bell, bernoulli, catalan, euler, fibonacci, lucas, genocchi, partition, tribonacci + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Harmonic_number + .. [2] https://functions.wolfram.com/GammaBetaErf/HarmonicNumber/ + .. [3] https://functions.wolfram.com/GammaBetaErf/HarmonicNumber2/ + + """ + + # This prevents redundant recalculations and speeds up harmonic number computations. + harmonic_cache: dict[Integer, Callable[[int], Rational]] = {} + + @classmethod + def eval(cls, n, m=None): + from sympy.functions.special.zeta_functions import zeta + if m is S.One: + return cls(n) + if m is None: + m = S.One + if n.is_zero: + return S.Zero + elif m.is_zero: + return n + elif n is S.Infinity: + if m.is_negative: + return S.NaN + elif is_le(m, S.One): + return S.Infinity + elif is_gt(m, S.One): + return zeta(m) + elif m.is_Integer and m.is_nonpositive: + return (bernoulli(1-m, n+1) - bernoulli(1-m)) / (1-m) + elif n.is_Integer: + if n.is_negative and (m.is_integer is False or m.is_nonpositive is False): + return S.ComplexInfinity if m is S.One else S.NaN + if n.is_nonnegative: + if m.is_Integer: + if m not in cls.harmonic_cache: + @recurrence_memo([0]) + def f(n, prev): + return prev[-1] + S.One / n**m + cls.harmonic_cache[m] = f + return cls.harmonic_cache[m](int(n)) + return Add(*(k**(-m) for k in range(1, int(n) + 1))) + + def _eval_rewrite_as_polygamma(self, n, m=S.One, **kwargs): + from sympy.functions.special.gamma_functions import gamma, polygamma + if m.is_integer and m.is_positive: + return Piecewise((polygamma(0, n+1) + S.EulerGamma, Eq(m, 1)), + (S.NegativeOne**m * (polygamma(m-1, 1) - polygamma(m-1, n+1)) / + gamma(m), True)) + + def _eval_rewrite_as_digamma(self, n, m=1, **kwargs): + from sympy.functions.special.gamma_functions import polygamma + return self.rewrite(polygamma) + + def _eval_rewrite_as_trigamma(self, n, m=1, **kwargs): + from sympy.functions.special.gamma_functions import polygamma + return self.rewrite(polygamma) + + def _eval_rewrite_as_Sum(self, n, m=None, **kwargs): + from sympy.concrete.summations import Sum + k = Dummy("k", integer=True) + if m is None: + m = S.One + return Sum(k**(-m), (k, 1, n)) + + def _eval_rewrite_as_zeta(self, n, m=S.One, **kwargs): + from sympy.functions.special.zeta_functions import zeta + from sympy.functions.special.gamma_functions import digamma + return Piecewise((digamma(n + 1) + S.EulerGamma, Eq(m, 1)), + (zeta(m) - zeta(m, n+1), True)) + + def _eval_expand_func(self, **hints): + from sympy.concrete.summations import Sum + n = self.args[0] + m = self.args[1] if len(self.args) == 2 else 1 + + if m == S.One: + if n.is_Add: + off = n.args[0] + nnew = n - off + if off.is_Integer and off.is_positive: + result = [S.One/(nnew + i) for i in range(off, 0, -1)] + [harmonic(nnew)] + return Add(*result) + elif off.is_Integer and off.is_negative: + result = [-S.One/(nnew + i) for i in range(0, off, -1)] + [harmonic(nnew)] + return Add(*result) + + if n.is_Rational: + # Expansions for harmonic numbers at general rational arguments (u + p/q) + # Split n as u + p/q with p < q + p, q = n.as_numer_denom() + u = p // q + p = p - u * q + if u.is_nonnegative and p.is_positive and q.is_positive and p < q: + from sympy.functions.elementary.exponential import log + from sympy.functions.elementary.integers import floor + from sympy.functions.elementary.trigonometric import sin, cos, cot + k = Dummy("k") + t1 = q * Sum(1 / (q * k + p), (k, 0, u)) + t2 = 2 * Sum(cos((2 * pi * p * k) / S(q)) * + log(sin((pi * k) / S(q))), + (k, 1, floor((q - 1) / S(2)))) + t3 = (pi / 2) * cot((pi * p) / q) + log(2 * q) + return t1 + t2 - t3 + + return self + + def _eval_rewrite_as_tractable(self, n, m=1, limitvar=None, **kwargs): + from sympy.functions.special.zeta_functions import zeta + from sympy.functions.special.gamma_functions import polygamma + pg = self.rewrite(polygamma) + if not isinstance(pg, harmonic): + return pg.rewrite("tractable", deep=True) + arg = m - S.One + if arg.is_nonzero: + return (zeta(m) - zeta(m, n+1)).rewrite("tractable", deep=True) + + def _eval_evalf(self, prec): + if not all(x.is_number for x in self.args): + return + n = self.args[0]._to_mpmath(prec) + m = (self.args[1] if len(self.args) > 1 else S.One)._to_mpmath(prec) + if mp.isint(n) and n < 0: + return S.NaN + with workprec(prec): + if m == 1: + res = mp.harmonic(n) + else: + res = mp.zeta(m) - mp.zeta(m, n+1) + return Expr._from_mpmath(res, prec) + + def fdiff(self, argindex=1): + from sympy.functions.special.zeta_functions import zeta + if len(self.args) == 2: + n, m = self.args + else: + n, m = self.args + (1,) + if argindex == 1: + return m * zeta(m+1, n+1) + else: + raise ArgumentIndexError + + +#----------------------------------------------------------------------------# +# # +# Euler numbers # +# # +#----------------------------------------------------------------------------# + + +class euler(DefinedFunction): + r""" + Euler numbers / Euler polynomials / Euler function + + The Euler numbers are given by: + + .. math:: E_{2n} = I \sum_{k=1}^{2n+1} \sum_{j=0}^k \binom{k}{j} + \frac{(-1)^j (k-2j)^{2n+1}}{2^k I^k k} + + .. math:: E_{2n+1} = 0 + + Euler numbers and Euler polynomials are related by + + .. math:: E_n = 2^n E_n\left(\frac{1}{2}\right). + + We compute symbolic Euler polynomials using Appell sequences, + but numerical evaluation of the Euler polynomial is computed + more efficiently (and more accurately) using the mpmath library. + + The Euler polynomials are special cases of the generalized Euler function, + related to the Genocchi function as + + .. math:: \operatorname{E}(s, a) = -\frac{\operatorname{G}(s+1, a)}{s+1} + + with the limit of `\psi\left(\frac{a+1}{2}\right) - \psi\left(\frac{a}{2}\right)` + being taken when `s = -1`. The (ordinary) Euler function interpolating + the Euler numbers is then obtained as + `\operatorname{E}(s) = 2^s \operatorname{E}\left(s, \frac{1}{2}\right)`. + + * ``euler(n)`` gives the nth Euler number `E_n`. + * ``euler(s)`` gives the Euler function `\operatorname{E}(s)`. + * ``euler(n, x)`` gives the nth Euler polynomial `E_n(x)`. + * ``euler(s, a)`` gives the generalized Euler function `\operatorname{E}(s, a)`. + + Examples + ======== + + >>> from sympy import euler, Symbol, S + >>> [euler(n) for n in range(10)] + [1, 0, -1, 0, 5, 0, -61, 0, 1385, 0] + >>> [2**n*euler(n,1) for n in range(10)] + [1, 1, 0, -2, 0, 16, 0, -272, 0, 7936] + >>> n = Symbol("n") + >>> euler(n + 2*n) + euler(3*n) + + >>> x = Symbol("x") + >>> euler(n, x) + euler(n, x) + + >>> euler(0, x) + 1 + >>> euler(1, x) + x - 1/2 + >>> euler(2, x) + x**2 - x + >>> euler(3, x) + x**3 - 3*x**2/2 + 1/4 + >>> euler(4, x) + x**4 - 2*x**3 + x + + >>> euler(12, S.Half) + 2702765/4096 + >>> euler(12) + 2702765 + + See Also + ======== + + andre, bell, bernoulli, catalan, fibonacci, harmonic, lucas, genocchi, + partition, tribonacci, sympy.polys.appellseqs.euler_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Euler_numbers + .. [2] https://mathworld.wolfram.com/EulerNumber.html + .. [3] https://en.wikipedia.org/wiki/Alternating_permutation + .. [4] https://mathworld.wolfram.com/AlternatingPermutation.html + + """ + + @classmethod + def eval(cls, n, x=None): + if n.is_zero: + return S.One + elif n is S.NegativeOne: + if x is None: + return S.Pi/2 + from sympy.functions.special.gamma_functions import digamma + return digamma((x+1)/2) - digamma(x/2) + elif n.is_integer is False or n.is_nonnegative is False: + return + # Euler numbers + elif x is None: + if n.is_odd and n.is_positive: + return S.Zero + elif n.is_Number: + from mpmath import mp + n = n._to_mpmath(mp.prec) + res = mp.eulernum(n, exact=True) + return Integer(res) + # Euler polynomials + elif n.is_Number: + from sympy.core.evalf import pure_complex + n = int(n) + reim = pure_complex(x, or_real=True) + if reim and all(a.is_Float or a.is_Integer for a in reim) \ + and any(a.is_Float for a in reim): + from mpmath import mp + prec = min([a._prec for a in reim if a.is_Float]) + with workprec(prec): + res = mp.eulerpoly(n, x) + return Expr._from_mpmath(res, prec) + return euler_poly(n, x) + + def _eval_rewrite_as_Sum(self, n, x=None, **kwargs): + from sympy.concrete.summations import Sum + if x is None and n.is_even: + k = Dummy("k", integer=True) + j = Dummy("j", integer=True) + n = n / 2 + Em = (S.ImaginaryUnit * Sum(Sum(binomial(k, j) * (S.NegativeOne**j * + (k - 2*j)**(2*n + 1)) / + (2**k*S.ImaginaryUnit**k * k), (j, 0, k)), (k, 1, 2*n + 1))) + return Em + if x: + k = Dummy("k", integer=True) + return Sum(binomial(n, k)*euler(k)/2**k*(x - S.Half)**(n - k), (k, 0, n)) + + def _eval_rewrite_as_genocchi(self, n, x=None, **kwargs): + if x is None: + return Piecewise((S.Pi/2, Eq(n, -1)), + (-2**n * genocchi(n+1, S.Half) / (n+1), True)) + from sympy.functions.special.gamma_functions import digamma + return Piecewise((digamma((x+1)/2) - digamma(x/2), Eq(n, -1)), + (-genocchi(n+1, x) / (n+1), True)) + + def _eval_evalf(self, prec): + if not all(i.is_number for i in self.args): + return + from mpmath import mp + m, x = (self.args[0], None) if len(self.args) == 1 else self.args + m = m._to_mpmath(prec) + if x is not None: + x = x._to_mpmath(prec) + with workprec(prec): + if mp.isint(m) and m >= 0: + res = mp.eulernum(m) if x is None else mp.eulerpoly(m, x) + else: + if m == -1: + res = mp.pi if x is None else mp.digamma((x+1)/2) - mp.digamma(x/2) + else: + y = 0.5 if x is None else x + res = 2 * (mp.zeta(-m, y) - 2**(m+1) * mp.zeta(-m, (y+1)/2)) + if x is None: + res *= 2**m + return Expr._from_mpmath(res, prec) + + +#----------------------------------------------------------------------------# +# # +# Catalan numbers # +# # +#----------------------------------------------------------------------------# + + +class catalan(DefinedFunction): + r""" + Catalan numbers + + The `n^{th}` catalan number is given by: + + .. math :: C_n = \frac{1}{n+1} \binom{2n}{n} + + * ``catalan(n)`` gives the `n^{th}` Catalan number, `C_n` + + Examples + ======== + + >>> from sympy import (Symbol, binomial, gamma, hyper, + ... catalan, diff, combsimp, Rational, I) + + >>> [catalan(i) for i in range(1,10)] + [1, 2, 5, 14, 42, 132, 429, 1430, 4862] + + >>> n = Symbol("n", integer=True) + + >>> catalan(n) + catalan(n) + + Catalan numbers can be transformed into several other, identical + expressions involving other mathematical functions + + >>> catalan(n).rewrite(binomial) + binomial(2*n, n)/(n + 1) + + >>> catalan(n).rewrite(gamma) + 4**n*gamma(n + 1/2)/(sqrt(pi)*gamma(n + 2)) + + >>> catalan(n).rewrite(hyper) + hyper((-n, 1 - n), (2,), 1) + + For some non-integer values of n we can get closed form + expressions by rewriting in terms of gamma functions: + + >>> catalan(Rational(1, 2)).rewrite(gamma) + 8/(3*pi) + + We can differentiate the Catalan numbers C(n) interpreted as a + continuous real function in n: + + >>> diff(catalan(n), n) + (polygamma(0, n + 1/2) - polygamma(0, n + 2) + log(4))*catalan(n) + + As a more advanced example consider the following ratio + between consecutive numbers: + + >>> combsimp((catalan(n + 1)/catalan(n)).rewrite(binomial)) + 2*(2*n + 1)/(n + 2) + + The Catalan numbers can be generalized to complex numbers: + + >>> catalan(I).rewrite(gamma) + 4**I*gamma(1/2 + I)/(sqrt(pi)*gamma(2 + I)) + + and evaluated with arbitrary precision: + + >>> catalan(I).evalf(20) + 0.39764993382373624267 - 0.020884341620842555705*I + + See Also + ======== + + andre, bell, bernoulli, euler, fibonacci, harmonic, lucas, genocchi, + partition, tribonacci, sympy.functions.combinatorial.factorials.binomial + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Catalan_number + .. [2] https://mathworld.wolfram.com/CatalanNumber.html + .. [3] https://functions.wolfram.com/GammaBetaErf/CatalanNumber/ + .. [4] http://geometer.org/mathcircles/catalan.pdf + + """ + + @classmethod + def eval(cls, n): + from sympy.functions.special.gamma_functions import gamma + if (n.is_Integer and n.is_nonnegative) or \ + (n.is_noninteger and n.is_negative): + return 4**n*gamma(n + S.Half)/(gamma(S.Half)*gamma(n + 2)) + + if (n.is_integer and n.is_negative): + if (n + 1).is_negative: + return S.Zero + if (n + 1).is_zero: + return Rational(-1, 2) + + def fdiff(self, argindex=1): + from sympy.functions.elementary.exponential import log + from sympy.functions.special.gamma_functions import polygamma + n = self.args[0] + return catalan(n)*(polygamma(0, n + S.Half) - polygamma(0, n + 2) + log(4)) + + def _eval_rewrite_as_binomial(self, n, **kwargs): + return binomial(2*n, n)/(n + 1) + + def _eval_rewrite_as_factorial(self, n, **kwargs): + return factorial(2*n) / (factorial(n+1) * factorial(n)) + + def _eval_rewrite_as_gamma(self, n, piecewise=True, **kwargs): + from sympy.functions.special.gamma_functions import gamma + # The gamma function allows to generalize Catalan numbers to complex n + return 4**n*gamma(n + S.Half)/(gamma(S.Half)*gamma(n + 2)) + + def _eval_rewrite_as_hyper(self, n, **kwargs): + from sympy.functions.special.hyper import hyper + return hyper([1 - n, -n], [2], 1) + + def _eval_rewrite_as_Product(self, n, **kwargs): + from sympy.concrete.products import Product + if not (n.is_integer and n.is_nonnegative): + return self + k = Dummy('k', integer=True, positive=True) + return Product((n + k) / k, (k, 2, n)) + + def _eval_is_integer(self): + if self.args[0].is_integer and self.args[0].is_nonnegative: + return True + + def _eval_is_positive(self): + if self.args[0].is_nonnegative: + return True + + def _eval_is_composite(self): + if self.args[0].is_integer and (self.args[0] - 3).is_positive: + return True + + def _eval_evalf(self, prec): + from sympy.functions.special.gamma_functions import gamma + if self.args[0].is_number: + return self.rewrite(gamma)._eval_evalf(prec) + + +#----------------------------------------------------------------------------# +# # +# Genocchi numbers # +# # +#----------------------------------------------------------------------------# + + +class genocchi(DefinedFunction): + r""" + Genocchi numbers / Genocchi polynomials / Genocchi function + + The Genocchi numbers are a sequence of integers `G_n` that satisfy the + relation: + + .. math:: \frac{-2t}{1 + e^{-t}} = \sum_{n=0}^\infty \frac{G_n t^n}{n!} + + They are related to the Bernoulli numbers by + + .. math:: G_n = 2 (1 - 2^n) B_n + + and generalize like the Bernoulli numbers to the Genocchi polynomials and + function as + + .. math:: \operatorname{G}(s, a) = 2 \left(\operatorname{B}(s, a) - + 2^s \operatorname{B}\left(s, \frac{a+1}{2}\right)\right) + + .. versionchanged:: 1.12 + ``genocchi(1)`` gives `-1` instead of `1`. + + Examples + ======== + + >>> from sympy import genocchi, Symbol + >>> [genocchi(n) for n in range(9)] + [0, -1, -1, 0, 1, 0, -3, 0, 17] + >>> n = Symbol('n', integer=True, positive=True) + >>> genocchi(2*n + 1) + 0 + >>> x = Symbol('x') + >>> genocchi(4, x) + -4*x**3 + 6*x**2 - 1 + + See Also + ======== + + bell, bernoulli, catalan, euler, fibonacci, harmonic, lucas, partition, tribonacci + sympy.polys.appellseqs.genocchi_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Genocchi_number + .. [2] https://mathworld.wolfram.com/GenocchiNumber.html + .. [3] Peter Luschny, "An introduction to the Bernoulli function", + https://arxiv.org/abs/2009.06743 + + """ + + @classmethod + def eval(cls, n, x=None): + if x is S.One: + return cls(n) + elif n.is_integer is False or n.is_nonnegative is False: + return + # Genocchi numbers + elif x is None: + if n.is_odd and (n-1).is_positive: + return S.Zero + elif n.is_Number: + return 2 * (1-S(2)**n) * bernoulli(n) + # Genocchi polynomials + elif n.is_Number: + return genocchi_poly(n, x) + + def _eval_rewrite_as_bernoulli(self, n, x=1, **kwargs): + if x == 1 and n.is_integer and n.is_nonnegative: + return 2 * (1-S(2)**n) * bernoulli(n) + return 2 * (bernoulli(n, x) - 2**n * bernoulli(n, (x+1) / 2)) + + def _eval_rewrite_as_dirichlet_eta(self, n, x=1, **kwargs): + from sympy.functions.special.zeta_functions import dirichlet_eta + return -2*n * dirichlet_eta(1-n, x) + + def _eval_is_integer(self): + if len(self.args) > 1 and self.args[1] != 1: + return + n = self.args[0] + if n.is_integer and n.is_nonnegative: + return True + + def _eval_is_negative(self): + if len(self.args) > 1 and self.args[1] != 1: + return + n = self.args[0] + if n.is_integer and n.is_nonnegative: + if n.is_odd: + return fuzzy_not((n-1).is_positive) + return (n/2).is_odd + + def _eval_is_positive(self): + if len(self.args) > 1 and self.args[1] != 1: + return + n = self.args[0] + if n.is_integer and n.is_nonnegative: + if n.is_zero or n.is_odd: + return False + return (n/2).is_even + + def _eval_is_even(self): + if len(self.args) > 1 and self.args[1] != 1: + return + n = self.args[0] + if n.is_integer and n.is_nonnegative: + if n.is_even: + return n.is_zero + return (n-1).is_positive + + def _eval_is_odd(self): + if len(self.args) > 1 and self.args[1] != 1: + return + n = self.args[0] + if n.is_integer and n.is_nonnegative: + if n.is_even: + return fuzzy_not(n.is_zero) + return fuzzy_not((n-1).is_positive) + + def _eval_is_prime(self): + if len(self.args) > 1 and self.args[1] != 1: + return + n = self.args[0] + # only G_6 = -3 and G_8 = 17 are prime, + # but SymPy does not consider negatives as prime + # so only n=8 is tested + return (n-8).is_zero + + def _eval_evalf(self, prec): + if all(i.is_number for i in self.args): + return self.rewrite(bernoulli)._eval_evalf(prec) + + +#----------------------------------------------------------------------------# +# # +# Andre numbers # +# # +#----------------------------------------------------------------------------# + + +class andre(DefinedFunction): + r""" + Andre numbers / Andre function + + The Andre number `\mathcal{A}_n` is Luschny's name for half the number of + *alternating permutations* on `n` elements, where a permutation is alternating + if adjacent elements alternately compare "greater" and "smaller" going from + left to right. For example, `2 < 3 > 1 < 4` is an alternating permutation. + + This sequence is A000111 in the OEIS, which assigns the names *up/down numbers* + and *Euler zigzag numbers*. It satisfies a recurrence relation similar to that + for the Catalan numbers, with `\mathcal{A}_0 = 1` and + + .. math:: 2 \mathcal{A}_{n+1} = \sum_{k=0}^n \binom{n}{k} \mathcal{A}_k \mathcal{A}_{n-k} + + The Bernoulli and Euler numbers are signed transformations of the odd- and + even-indexed elements of this sequence respectively: + + .. math :: \operatorname{B}_{2k} = \frac{2k \mathcal{A}_{2k-1}}{(-4)^k - (-16)^k} + + .. math :: \operatorname{E}_{2k} = (-1)^k \mathcal{A}_{2k} + + Like the Bernoulli and Euler numbers, the Andre numbers are interpolated by the + entire Andre function: + + .. math :: \mathcal{A}(s) = (-i)^{s+1} \operatorname{Li}_{-s}(i) + + i^{s+1} \operatorname{Li}_{-s}(-i) = \\ \frac{2 \Gamma(s+1)}{(2\pi)^{s+1}} + (\zeta(s+1, 1/4) - \zeta(s+1, 3/4) \cos{\pi s}) + + Examples + ======== + + >>> from sympy import andre, euler, bernoulli + >>> [andre(n) for n in range(11)] + [1, 1, 1, 2, 5, 16, 61, 272, 1385, 7936, 50521] + >>> [(-1)**k * andre(2*k) for k in range(7)] + [1, -1, 5, -61, 1385, -50521, 2702765] + >>> [euler(2*k) for k in range(7)] + [1, -1, 5, -61, 1385, -50521, 2702765] + >>> [andre(2*k-1) * (2*k) / ((-4)**k - (-16)**k) for k in range(1, 8)] + [1/6, -1/30, 1/42, -1/30, 5/66, -691/2730, 7/6] + >>> [bernoulli(2*k) for k in range(1, 8)] + [1/6, -1/30, 1/42, -1/30, 5/66, -691/2730, 7/6] + + See Also + ======== + + bernoulli, catalan, euler, sympy.polys.appellseqs.andre_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Alternating_permutation + .. [2] https://mathworld.wolfram.com/EulerZigzagNumber.html + .. [3] Peter Luschny, "An introduction to the Bernoulli function", + https://arxiv.org/abs/2009.06743 + """ + + @classmethod + def eval(cls, n): + if n is S.NaN: + return S.NaN + elif n is S.Infinity: + return S.Infinity + if n.is_zero: + return S.One + elif n == -1: + return -log(2) + elif n == -2: + return -2*S.Catalan + elif n.is_Integer: + if n.is_nonnegative and n.is_even: + return abs(euler(n)) + elif n.is_odd: + from sympy.functions.special.zeta_functions import zeta + m = -n-1 + return I**m * Rational(1-2**m, 4**m) * zeta(-n) + + def _eval_rewrite_as_zeta(self, s, **kwargs): + from sympy.functions.elementary.trigonometric import cos + from sympy.functions.special.gamma_functions import gamma + from sympy.functions.special.zeta_functions import zeta + return 2 * gamma(s+1) / (2*pi)**(s+1) * \ + (zeta(s+1, S.One/4) - cos(pi*s) * zeta(s+1, S(3)/4)) + + def _eval_rewrite_as_polylog(self, s, **kwargs): + from sympy.functions.special.zeta_functions import polylog + return (-I)**(s+1) * polylog(-s, I) + I**(s+1) * polylog(-s, -I) + + def _eval_is_integer(self): + n = self.args[0] + if n.is_integer and n.is_nonnegative: + return True + + def _eval_is_positive(self): + if self.args[0].is_nonnegative: + return True + + def _eval_evalf(self, prec): + if not self.args[0].is_number: + return + s = self.args[0]._to_mpmath(prec+12) + with workprec(prec+12): + sp, cp = mp.sinpi(s/2), mp.cospi(s/2) + res = 2*mp.dirichlet(-s, (-sp, cp, sp, -cp)) + return Expr._from_mpmath(res, prec) + + +#----------------------------------------------------------------------------# +# # +# Partition numbers # +# # +#----------------------------------------------------------------------------# + +class partition(DefinedFunction): + r""" + Partition numbers + + The Partition numbers are a sequence of integers `p_n` that represent the + number of distinct ways of representing `n` as a sum of natural numbers + (with order irrelevant). The generating function for `p_n` is given by: + + .. math:: \sum_{n=0}^\infty p_n x^n = \prod_{k=1}^\infty (1 - x^k)^{-1} + + Examples + ======== + + >>> from sympy import partition, Symbol + >>> [partition(n) for n in range(9)] + [1, 1, 2, 3, 5, 7, 11, 15, 22] + >>> n = Symbol('n', integer=True, negative=True) + >>> partition(n) + 0 + + See Also + ======== + + bell, bernoulli, catalan, euler, fibonacci, harmonic, lucas, genocchi, tribonacci + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Partition_(number_theory%29 + .. [2] https://en.wikipedia.org/wiki/Pentagonal_number_theorem + + """ + is_integer = True + is_nonnegative = True + + @classmethod + def eval(cls, n): + if n.is_integer is False: + raise TypeError("n should be an integer") + if n.is_negative is True: + return S.Zero + if n.is_zero is True or n is S.One: + return S.One + if n.is_Integer is True: + return S(_partition(as_int(n))) + + def _eval_is_positive(self): + if self.args[0].is_nonnegative is True: + return True + + def _eval_Mod(self, q): + # Ramanujan's congruences + n = self.args[0] + for p, rem in [(5, 4), (7, 5), (11, 6)]: + if q == p and n % q == rem: + return S.Zero + + +class divisor_sigma(DefinedFunction): + r""" + Calculate the divisor function `\sigma_k(n)` for positive integer n + + ``divisor_sigma(n, k)`` is equal to ``sum([x**k for x in divisors(n)])`` + + If n's prime factorization is: + + .. math :: + n = \prod_{i=1}^\omega p_i^{m_i}, + + then + + .. math :: + \sigma_k(n) = \prod_{i=1}^\omega (1+p_i^k+p_i^{2k}+\cdots + + p_i^{m_ik}). + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import divisor_sigma + >>> divisor_sigma(18, 0) + 6 + >>> divisor_sigma(39, 1) + 56 + >>> divisor_sigma(12, 2) + 210 + >>> divisor_sigma(37) + 38 + + See Also + ======== + + sympy.ntheory.factor_.divisor_count, totient, sympy.ntheory.factor_.divisors, sympy.ntheory.factor_.factorint + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Divisor_function + + """ + is_integer = True + is_positive = True + + @classmethod + def eval(cls, n, k=S.One): + if n.is_integer is False: + raise TypeError("n should be an integer") + if n.is_positive is False: + raise ValueError("n should be a positive integer") + if k.is_integer is False: + raise TypeError("k should be an integer") + if k.is_nonnegative is False: + raise ValueError("k should be a nonnegative integer") + if n.is_prime is True: + return 1 + n**k + if n is S.One: + return S.One + if n.is_Integer is True: + if k.is_zero is True: + return Mul(*[e + 1 for e in factorint(n).values()]) + if k.is_Integer is True: + return S(_divisor_sigma(as_int(n), as_int(k))) + if k.is_zero is False: + return Mul(*[cancel((p**(k*(e + 1)) - 1) / (p**k - 1)) for p, e in factorint(n).items()]) + + +class udivisor_sigma(DefinedFunction): + r""" + Calculate the unitary divisor function `\sigma_k^*(n)` for positive integer n + + ``udivisor_sigma(n, k)`` is equal to ``sum([x**k for x in udivisors(n)])`` + + If n's prime factorization is: + + .. math :: + n = \prod_{i=1}^\omega p_i^{m_i}, + + then + + .. math :: + \sigma_k^*(n) = \prod_{i=1}^\omega (1+ p_i^{m_ik}). + + Parameters + ========== + + k : power of divisors in the sum + + for k = 0, 1: + ``udivisor_sigma(n, 0)`` is equal to ``udivisor_count(n)`` + ``udivisor_sigma(n, 1)`` is equal to ``sum(udivisors(n))`` + + Default for k is 1. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import udivisor_sigma + >>> udivisor_sigma(18, 0) + 4 + >>> udivisor_sigma(74, 1) + 114 + >>> udivisor_sigma(36, 3) + 47450 + >>> udivisor_sigma(111) + 152 + + See Also + ======== + + sympy.ntheory.factor_.divisor_count, totient, sympy.ntheory.factor_.divisors, + sympy.ntheory.factor_.udivisors, sympy.ntheory.factor_.udivisor_count, divisor_sigma, + sympy.ntheory.factor_.factorint + + References + ========== + + .. [1] https://mathworld.wolfram.com/UnitaryDivisorFunction.html + + """ + is_integer = True + is_positive = True + + @classmethod + def eval(cls, n, k=S.One): + if n.is_integer is False: + raise TypeError("n should be an integer") + if n.is_positive is False: + raise ValueError("n should be a positive integer") + if k.is_integer is False: + raise TypeError("k should be an integer") + if k.is_nonnegative is False: + raise ValueError("k should be a nonnegative integer") + if n.is_prime is True: + return 1 + n**k + if n.is_Integer: + return Mul(*[1+p**(k*e) for p, e in factorint(n).items()]) + + +class legendre_symbol(DefinedFunction): + r""" + Returns the Legendre symbol `(a / p)`. + + For an integer ``a`` and an odd prime ``p``, the Legendre symbol is + defined as + + .. math :: + \genfrac(){}{}{a}{p} = \begin{cases} + 0 & \text{if } p \text{ divides } a\\ + 1 & \text{if } a \text{ is a quadratic residue modulo } p\\ + -1 & \text{if } a \text{ is a quadratic nonresidue modulo } p + \end{cases} + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import legendre_symbol + >>> [legendre_symbol(i, 7) for i in range(7)] + [0, 1, 1, -1, 1, -1, -1] + >>> sorted(set([i**2 % 7 for i in range(7)])) + [0, 1, 2, 4] + + See Also + ======== + + sympy.ntheory.residue_ntheory.is_quad_residue, jacobi_symbol + + """ + is_integer = True + is_prime = False + + @classmethod + def eval(cls, a, p): + if a.is_integer is False: + raise TypeError("a should be an integer") + if p.is_integer is False: + raise TypeError("p should be an integer") + if p.is_prime is False or p.is_odd is False: + raise ValueError("p should be an odd prime integer") + if (a % p).is_zero is True: + return S.Zero + if a is S.One: + return S.One + if a.is_Integer is True and p.is_Integer is True: + return S(legendre(as_int(a), as_int(p))) + + +class jacobi_symbol(DefinedFunction): + r""" + Returns the Jacobi symbol `(m / n)`. + + For any integer ``m`` and any positive odd integer ``n`` the Jacobi symbol + is defined as the product of the Legendre symbols corresponding to the + prime factors of ``n``: + + .. math :: + \genfrac(){}{}{m}{n} = + \genfrac(){}{}{m}{p^{1}}^{\alpha_1} + \genfrac(){}{}{m}{p^{2}}^{\alpha_2} + ... + \genfrac(){}{}{m}{p^{k}}^{\alpha_k} + \text{ where } n = + p_1^{\alpha_1} + p_2^{\alpha_2} + ... + p_k^{\alpha_k} + + Like the Legendre symbol, if the Jacobi symbol `\genfrac(){}{}{m}{n} = -1` + then ``m`` is a quadratic nonresidue modulo ``n``. + + But, unlike the Legendre symbol, if the Jacobi symbol + `\genfrac(){}{}{m}{n} = 1` then ``m`` may or may not be a quadratic residue + modulo ``n``. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import jacobi_symbol, legendre_symbol + >>> from sympy import S + >>> jacobi_symbol(45, 77) + -1 + >>> jacobi_symbol(60, 121) + 1 + + The relationship between the ``jacobi_symbol`` and ``legendre_symbol`` can + be demonstrated as follows: + + >>> L = legendre_symbol + >>> S(45).factors() + {3: 2, 5: 1} + >>> jacobi_symbol(7, 45) == L(7, 3)**2 * L(7, 5)**1 + True + + See Also + ======== + + sympy.ntheory.residue_ntheory.is_quad_residue, legendre_symbol + + """ + is_integer = True + is_prime = False + + @classmethod + def eval(cls, m, n): + if m.is_integer is False: + raise TypeError("m should be an integer") + if n.is_integer is False: + raise TypeError("n should be an integer") + if n.is_positive is False or n.is_odd is False: + raise ValueError("n should be an odd positive integer") + if m is S.One or n is S.One: + return S.One + if (m % n).is_zero is True: + return S.Zero + if m.is_Integer is True and n.is_Integer is True: + return S(jacobi(as_int(m), as_int(n))) + + +class kronecker_symbol(DefinedFunction): + r""" + Returns the Kronecker symbol `(a / n)`. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import kronecker_symbol + >>> kronecker_symbol(45, 77) + -1 + >>> kronecker_symbol(13, -120) + 1 + + See Also + ======== + + jacobi_symbol, legendre_symbol + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Kronecker_symbol + + """ + is_integer = True + is_prime = False + + @classmethod + def eval(cls, a, n): + if a.is_integer is False: + raise TypeError("a should be an integer") + if n.is_integer is False: + raise TypeError("n should be an integer") + if a is S.One or n is S.One: + return S.One + if a.is_Integer is True and n.is_Integer is True: + return S(kronecker(as_int(a), as_int(n))) + + +class mobius(DefinedFunction): + """ + Mobius function maps natural number to {-1, 0, 1} + + It is defined as follows: + 1) `1` if `n = 1`. + 2) `0` if `n` has a squared prime factor. + 3) `(-1)^k` if `n` is a square-free positive integer with `k` + number of prime factors. + + It is an important multiplicative function in number theory + and combinatorics. It has applications in mathematical series, + algebraic number theory and also physics (Fermion operator has very + concrete realization with Mobius Function model). + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import mobius + >>> mobius(13*7) + 1 + >>> mobius(1) + 1 + >>> mobius(13*7*5) + -1 + >>> mobius(13**2) + 0 + + Even in the case of a symbol, if it clearly contains a squared prime factor, it will be zero. + + >>> from sympy import Symbol + >>> n = Symbol("n", integer=True, positive=True) + >>> mobius(4*n) + 0 + >>> mobius(n**2) + 0 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/M%C3%B6bius_function + .. [2] Thomas Koshy "Elementary Number Theory with Applications" + .. [3] https://oeis.org/A008683 + + """ + is_integer = True + is_prime = False + + @classmethod + def eval(cls, n): + if n.is_integer is False: + raise TypeError("n should be an integer") + if n.is_positive is False: + raise ValueError("n should be a positive integer") + if n.is_prime is True: + return S.NegativeOne + if n is S.One: + return S.One + result = None + for m, e in (_.as_base_exp() for _ in Mul.make_args(n)): + if m.is_integer is True and m.is_positive is True and \ + e.is_integer is True and e.is_positive is True: + lt = is_lt(S.One, e) # 1 < e + if lt is True: + result = S.Zero + elif m.is_Integer is True: + factors = factorint(m) + if any(v > 1 for v in factors.values()): + result = S.Zero + elif lt is False: + s = S.NegativeOne if len(factors) % 2 else S.One + if result is None: + result = s + else: + result *= s + else: + return + return result + + +class primenu(DefinedFunction): + r""" + Calculate the number of distinct prime factors for a positive integer n. + + If n's prime factorization is: + + .. math :: + n = \prod_{i=1}^k p_i^{m_i}, + + then ``primenu(n)`` or `\nu(n)` is: + + .. math :: + \nu(n) = k. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import primenu + >>> primenu(1) + 0 + >>> primenu(30) + 3 + + See Also + ======== + + sympy.ntheory.factor_.factorint + + References + ========== + + .. [1] https://mathworld.wolfram.com/PrimeFactor.html + .. [2] https://oeis.org/A001221 + + """ + is_integer = True + is_nonnegative = True + + @classmethod + def eval(cls, n): + if n.is_integer is False: + raise TypeError("n should be an integer") + if n.is_positive is False: + raise ValueError("n should be a positive integer") + if n.is_prime is True: + return S.One + if n is S.One: + return S.Zero + if n.is_Integer is True: + return S(len(factorint(n))) + + +class primeomega(DefinedFunction): + r""" + Calculate the number of prime factors counting multiplicities for a + positive integer n. + + If n's prime factorization is: + + .. math :: + n = \prod_{i=1}^k p_i^{m_i}, + + then ``primeomega(n)`` or `\Omega(n)` is: + + .. math :: + \Omega(n) = \sum_{i=1}^k m_i. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import primeomega + >>> primeomega(1) + 0 + >>> primeomega(20) + 3 + + See Also + ======== + + sympy.ntheory.factor_.factorint + + References + ========== + + .. [1] https://mathworld.wolfram.com/PrimeFactor.html + .. [2] https://oeis.org/A001222 + + """ + is_integer = True + is_nonnegative = True + + @classmethod + def eval(cls, n): + if n.is_integer is False: + raise TypeError("n should be an integer") + if n.is_positive is False: + raise ValueError("n should be a positive integer") + if n.is_prime is True: + return S.One + if n is S.One: + return S.Zero + if n.is_Integer is True: + return S(sum(factorint(n).values())) + + +class totient(DefinedFunction): + r""" + Calculate the Euler totient function phi(n) + + ``totient(n)`` or `\phi(n)` is the number of positive integers `\leq` n + that are relatively prime to n. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import totient + >>> totient(1) + 1 + >>> totient(25) + 20 + >>> totient(45) == totient(5)*totient(9) + True + + See Also + ======== + + sympy.ntheory.factor_.divisor_count + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Euler%27s_totient_function + .. [2] https://mathworld.wolfram.com/TotientFunction.html + .. [3] https://oeis.org/A000010 + + """ + is_integer = True + is_positive = True + + @classmethod + def eval(cls, n): + if n.is_integer is False: + raise TypeError("n should be an integer") + if n.is_positive is False: + raise ValueError("n should be a positive integer") + if n is S.One: + return S.One + if n.is_prime is True: + return n - 1 + if isinstance(n, Dict): + return S(prod(p**(k-1)*(p-1) for p, k in n.items())) + if n.is_Integer is True: + return S(prod(p**(k-1)*(p-1) for p, k in factorint(n).items())) + + +class reduced_totient(DefinedFunction): + r""" + Calculate the Carmichael reduced totient function lambda(n) + + ``reduced_totient(n)`` or `\lambda(n)` is the smallest m > 0 such that + `k^m \equiv 1 \mod n` for all k relatively prime to n. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import reduced_totient + >>> reduced_totient(1) + 1 + >>> reduced_totient(8) + 2 + >>> reduced_totient(30) + 4 + + See Also + ======== + + totient + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Carmichael_function + .. [2] https://mathworld.wolfram.com/CarmichaelFunction.html + .. [3] https://oeis.org/A002322 + + """ + is_integer = True + is_positive = True + + @classmethod + def eval(cls, n): + if n.is_integer is False: + raise TypeError("n should be an integer") + if n.is_positive is False: + raise ValueError("n should be a positive integer") + if n is S.One: + return S.One + if n.is_prime is True: + return n - 1 + if isinstance(n, Dict): + t = 1 + if 2 in n: + t = (1 << (n[2] - 2)) if 2 < n[2] else n[2] + return S(lcm(int(t), *(int(p-1)*int(p)**int(k-1) for p, k in n.items() if p != 2))) + if n.is_Integer is True: + n, t = remove(int(n), 2) + if not t: + t = 1 + elif 2 < t: + t = 1 << (t - 2) + return S(lcm(t, *((p-1)*p**(k-1) for p, k in factorint(n).items()))) + + +class primepi(DefinedFunction): + r""" Represents the prime counting function pi(n) = the number + of prime numbers less than or equal to n. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import primepi + >>> from sympy import prime, prevprime, isprime + >>> primepi(25) + 9 + + So there are 9 primes less than or equal to 25. Is 25 prime? + + >>> isprime(25) + False + + It is not. So the first prime less than 25 must be the + 9th prime: + + >>> prevprime(25) == prime(9) + True + + See Also + ======== + + sympy.ntheory.primetest.isprime : Test if n is prime + sympy.ntheory.generate.primerange : Generate all primes in a given range + sympy.ntheory.generate.prime : Return the nth prime + + References + ========== + + .. [1] https://oeis.org/A000720 + + """ + is_integer = True + is_nonnegative = True + + @classmethod + def eval(cls, n): + if n is S.Infinity: + return S.Infinity + if n is S.NegativeInfinity: + return S.Zero + if n.is_real is False: + raise TypeError("n should be a real") + if is_lt(n, S(2)) is True: + return S.Zero + try: + n = int(n) + except TypeError: + return + return S(_primepi(n)) + + +####################################################################### +### +### Functions for enumerating partitions, permutations and combinations +### +####################################################################### + + +class _MultisetHistogram(tuple): + __slots__ = () + + +_N = -1 +_ITEMS = -2 +_M = slice(None, _ITEMS) + + +def _multiset_histogram(n): + """Return tuple used in permutation and combination counting. Input + is a dictionary giving items with counts as values or a sequence of + items (which need not be sorted). + + The data is stored in a class deriving from tuple so it is easily + recognized and so it can be converted easily to a list. + """ + if isinstance(n, dict): # item: count + if not all(isinstance(v, int) and v >= 0 for v in n.values()): + raise ValueError + tot = sum(n.values()) + items = sum(1 for k in n if n[k] > 0) + return _MultisetHistogram([n[k] for k in n if n[k] > 0] + [items, tot]) + else: + n = list(n) + s = set(n) + lens = len(s) + lenn = len(n) + if lens == lenn: + n = [1]*lenn + [lenn, lenn] + return _MultisetHistogram(n) + m = dict(zip(s, range(lens))) + d = dict(zip(range(lens), (0,)*lens)) + for i in n: + d[m[i]] += 1 + return _multiset_histogram(d) + + +def nP(n, k=None, replacement=False): + """Return the number of permutations of ``n`` items taken ``k`` at a time. + + Possible values for ``n``: + + integer - set of length ``n`` + + sequence - converted to a multiset internally + + multiset - {element: multiplicity} + + If ``k`` is None then the total of all permutations of length 0 + through the number of items represented by ``n`` will be returned. + + If ``replacement`` is True then a given item can appear more than once + in the ``k`` items. (For example, for 'ab' permutations of 2 would + include 'aa', 'ab', 'ba' and 'bb'.) The multiplicity of elements in + ``n`` is ignored when ``replacement`` is True but the total number + of elements is considered since no element can appear more times than + the number of elements in ``n``. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import nP + >>> from sympy.utilities.iterables import multiset_permutations, multiset + >>> nP(3, 2) + 6 + >>> nP('abc', 2) == nP(multiset('abc'), 2) == 6 + True + >>> nP('aab', 2) + 3 + >>> nP([1, 2, 2], 2) + 3 + >>> [nP(3, i) for i in range(4)] + [1, 3, 6, 6] + >>> nP(3) == sum(_) + True + + When ``replacement`` is True, each item can have multiplicity + equal to the length represented by ``n``: + + >>> nP('aabc', replacement=True) + 121 + >>> [len(list(multiset_permutations('aaaabbbbcccc', i))) for i in range(5)] + [1, 3, 9, 27, 81] + >>> sum(_) + 121 + + See Also + ======== + sympy.utilities.iterables.multiset_permutations + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Permutation + + """ + try: + n = as_int(n) + except ValueError: + return Integer(_nP(_multiset_histogram(n), k, replacement)) + return Integer(_nP(n, k, replacement)) + + +@cacheit +def _nP(n, k=None, replacement=False): + + if k == 0: + return 1 + if isinstance(n, SYMPY_INTS): # n different items + # assert n >= 0 + if k is None: + return sum(_nP(n, i, replacement) for i in range(n + 1)) + elif replacement: + return n**k + elif k > n: + return 0 + elif k == n: + return factorial(k) + elif k == 1: + return n + else: + # assert k >= 0 + return _product(n - k + 1, n) + elif isinstance(n, _MultisetHistogram): + if k is None: + return sum(_nP(n, i, replacement) for i in range(n[_N] + 1)) + elif replacement: + return n[_ITEMS]**k + elif k == n[_N]: + return factorial(k)/prod([factorial(i) for i in n[_M] if i > 1]) + elif k > n[_N]: + return 0 + elif k == 1: + return n[_ITEMS] + else: + # assert k >= 0 + tot = 0 + n = list(n) + for i in range(len(n[_M])): + if not n[i]: + continue + n[_N] -= 1 + if n[i] == 1: + n[i] = 0 + n[_ITEMS] -= 1 + tot += _nP(_MultisetHistogram(n), k - 1) + n[_ITEMS] += 1 + n[i] = 1 + else: + n[i] -= 1 + tot += _nP(_MultisetHistogram(n), k - 1) + n[i] += 1 + n[_N] += 1 + return tot + + +@cacheit +def _AOP_product(n): + """for n = (m1, m2, .., mk) return the coefficients of the polynomial, + prod(sum(x**i for i in range(nj + 1)) for nj in n); i.e. the coefficients + of the product of AOPs (all-one polynomials) or order given in n. The + resulting coefficient corresponding to x**r is the number of r-length + combinations of sum(n) elements with multiplicities given in n. + The coefficients are given as a default dictionary (so if a query is made + for a key that is not present, 0 will be returned). + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import _AOP_product + >>> from sympy.abc import x + >>> n = (2, 2, 3) # e.g. aabbccc + >>> prod = ((x**2 + x + 1)*(x**2 + x + 1)*(x**3 + x**2 + x + 1)).expand() + >>> c = _AOP_product(n); dict(c) + {0: 1, 1: 3, 2: 6, 3: 8, 4: 8, 5: 6, 6: 3, 7: 1} + >>> [c[i] for i in range(8)] == [prod.coeff(x, i) for i in range(8)] + True + + The generating poly used here is the same as that listed in + https://tinyurl.com/cep849r, but in a refactored form. + + """ + + n = list(n) + ord = sum(n) + need = (ord + 2)//2 + rv = [1]*(n.pop() + 1) + rv.extend((0,) * (need - len(rv))) + rv = rv[:need] + while n: + ni = n.pop() + N = ni + 1 + was = rv[:] + for i in range(1, min(N, len(rv))): + rv[i] += rv[i - 1] + for i in range(N, need): + rv[i] += rv[i - 1] - was[i - N] + rev = list(reversed(rv)) + if ord % 2: + rv = rv + rev + else: + rv[-1:] = rev + d = defaultdict(int) + for i, r in enumerate(rv): + d[i] = r + return d + + +def nC(n, k=None, replacement=False): + """Return the number of combinations of ``n`` items taken ``k`` at a time. + + Possible values for ``n``: + + integer - set of length ``n`` + + sequence - converted to a multiset internally + + multiset - {element: multiplicity} + + If ``k`` is None then the total of all combinations of length 0 + through the number of items represented in ``n`` will be returned. + + If ``replacement`` is True then a given item can appear more than once + in the ``k`` items. (For example, for 'ab' sets of 2 would include 'aa', + 'ab', and 'bb'.) The multiplicity of elements in ``n`` is ignored when + ``replacement`` is True but the total number of elements is considered + since no element can appear more times than the number of elements in + ``n``. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import nC + >>> from sympy.utilities.iterables import multiset_combinations + >>> nC(3, 2) + 3 + >>> nC('abc', 2) + 3 + >>> nC('aab', 2) + 2 + + When ``replacement`` is True, each item can have multiplicity + equal to the length represented by ``n``: + + >>> nC('aabc', replacement=True) + 35 + >>> [len(list(multiset_combinations('aaaabbbbcccc', i))) for i in range(5)] + [1, 3, 6, 10, 15] + >>> sum(_) + 35 + + If there are ``k`` items with multiplicities ``m_1, m_2, ..., m_k`` + then the total of all combinations of length 0 through ``k`` is the + product, ``(m_1 + 1)*(m_2 + 1)*...*(m_k + 1)``. When the multiplicity + of each item is 1 (i.e., k unique items) then there are 2**k + combinations. For example, if there are 4 unique items, the total number + of combinations is 16: + + >>> sum(nC(4, i) for i in range(5)) + 16 + + See Also + ======== + + sympy.utilities.iterables.multiset_combinations + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Combination + .. [2] https://tinyurl.com/cep849r + + """ + + if isinstance(n, SYMPY_INTS): + if k is None: + if not replacement: + return 2**n + return sum(nC(n, i, replacement) for i in range(n + 1)) + if k < 0: + raise ValueError("k cannot be negative") + if replacement: + return binomial(n + k - 1, k) + return binomial(n, k) + if isinstance(n, _MultisetHistogram): + N = n[_N] + if k is None: + if not replacement: + return prod(m + 1 for m in n[_M]) + return sum(nC(n, i, replacement) for i in range(N + 1)) + elif replacement: + return nC(n[_ITEMS], k, replacement) + # assert k >= 0 + elif k in (1, N - 1): + return n[_ITEMS] + elif k in (0, N): + return 1 + return _AOP_product(tuple(n[_M]))[k] + else: + return nC(_multiset_histogram(n), k, replacement) + + +def _eval_stirling1(n, k): + if n == k == 0: + return S.One + if 0 in (n, k): + return S.Zero + + # some special values + if n == k: + return S.One + elif k == n - 1: + return binomial(n, 2) + elif k == n - 2: + return (3*n - 1)*binomial(n, 3)/4 + elif k == n - 3: + return binomial(n, 2)*binomial(n, 4) + + return _stirling1(n, k) + + +@cacheit +def _stirling1(n, k): + row = [0, 1]+[0]*(k-1) # for n = 1 + for i in range(2, n+1): + for j in range(min(k,i), 0, -1): + row[j] = (i-1) * row[j] + row[j-1] + return Integer(row[k]) + + +def _eval_stirling2(n, k): + if n == k == 0: + return S.One + if 0 in (n, k): + return S.Zero + + # some special values + if n == k: + return S.One + elif k == n - 1: + return binomial(n, 2) + elif k == 1: + return S.One + elif k == 2: + return Integer(2**(n - 1) - 1) + + return _stirling2(n, k) + + +@cacheit +def _stirling2(n, k): + row = [0, 1]+[0]*(k-1) # for n = 1 + for i in range(2, n+1): + for j in range(min(k,i), 0, -1): + row[j] = j * row[j] + row[j-1] + return Integer(row[k]) + + +def stirling(n, k, d=None, kind=2, signed=False): + r"""Return Stirling number $S(n, k)$ of the first or second (default) kind. + + The sum of all Stirling numbers of the second kind for $k = 1$ + through $n$ is ``bell(n)``. The recurrence relationship for these numbers + is: + + .. math :: {0 \brace 0} = 1; {n \brace 0} = {0 \brace k} = 0; + + .. math :: {{n+1} \brace k} = j {n \brace k} + {n \brace {k-1}} + + where $j$ is: + $n$ for Stirling numbers of the first kind, + $-n$ for signed Stirling numbers of the first kind, + $k$ for Stirling numbers of the second kind. + + The first kind of Stirling number counts the number of permutations of + ``n`` distinct items that have ``k`` cycles; the second kind counts the + ways in which ``n`` distinct items can be partitioned into ``k`` parts. + If ``d`` is given, the "reduced Stirling number of the second kind" is + returned: $S^{d}(n, k) = S(n - d + 1, k - d + 1)$ with $n \ge k \ge d$. + (This counts the ways to partition $n$ consecutive integers into $k$ + groups with no pairwise difference less than $d$. See example below.) + + To obtain the signed Stirling numbers of the first kind, use keyword + ``signed=True``. Using this keyword automatically sets ``kind`` to 1. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import stirling, bell + >>> from sympy.combinatorics import Permutation + >>> from sympy.utilities.iterables import multiset_partitions, permutations + + First kind (unsigned by default): + + >>> [stirling(6, i, kind=1) for i in range(7)] + [0, 120, 274, 225, 85, 15, 1] + >>> perms = list(permutations(range(4))) + >>> [sum(Permutation(p).cycles == i for p in perms) for i in range(5)] + [0, 6, 11, 6, 1] + >>> [stirling(4, i, kind=1) for i in range(5)] + [0, 6, 11, 6, 1] + + First kind (signed): + + >>> [stirling(4, i, signed=True) for i in range(5)] + [0, -6, 11, -6, 1] + + Second kind: + + >>> [stirling(10, i) for i in range(12)] + [0, 1, 511, 9330, 34105, 42525, 22827, 5880, 750, 45, 1, 0] + >>> sum(_) == bell(10) + True + >>> len(list(multiset_partitions(range(4), 2))) == stirling(4, 2) + True + + Reduced second kind: + + >>> from sympy import subsets, oo + >>> def delta(p): + ... if len(p) == 1: + ... return oo + ... return min(abs(i[0] - i[1]) for i in subsets(p, 2)) + >>> parts = multiset_partitions(range(5), 3) + >>> d = 2 + >>> sum(1 for p in parts if all(delta(i) >= d for i in p)) + 7 + >>> stirling(5, 3, 2) + 7 + + See Also + ======== + sympy.utilities.iterables.multiset_partitions + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Stirling_numbers_of_the_first_kind + .. [2] https://en.wikipedia.org/wiki/Stirling_numbers_of_the_second_kind + + """ + # TODO: make this a class like bell() + + n = as_int(n) + k = as_int(k) + if n < 0: + raise ValueError('n must be nonnegative') + if k > n: + return S.Zero + if d: + # assert k >= d + # kind is ignored -- only kind=2 is supported + return _eval_stirling2(n - d + 1, k - d + 1) + elif signed: + # kind is ignored -- only kind=1 is supported + return S.NegativeOne**(n - k)*_eval_stirling1(n, k) + + if kind == 1: + return _eval_stirling1(n, k) + elif kind == 2: + return _eval_stirling2(n, k) + else: + raise ValueError('kind must be 1 or 2, not %s' % k) + + +@cacheit +def _nT(n, k): + """Return the partitions of ``n`` items into ``k`` parts. This + is used by ``nT`` for the case when ``n`` is an integer.""" + # really quick exits + if k > n or k < 0: + return 0 + if k in (1, n): + return 1 + if k == 0: + return 0 + # exits that could be done below but this is quicker + if k == 2: + return n//2 + d = n - k + if d <= 3: + return d + # quick exit + if 3*k >= n: # or, equivalently, 2*k >= d + # all the information needed in this case + # will be in the cache needed to calculate + # partition(d), so... + # update cache + tot = _partition_rec(d) + # and correct for values not needed + if d - k > 0: + tot -= sum(_partition_rec.fetch_item(slice(d - k))) + return tot + # regular exit + # nT(n, k) = Sum(nT(n - k, m), (m, 1, k)); + # calculate needed nT(i, j) values + p = [1]*d + for i in range(2, k + 1): + for m in range(i + 1, d): + p[m] += p[m - i] + d -= 1 + # if p[0] were appended to the end of p then the last + # k values of p are the nT(n, j) values for 0 < j < k in reverse + # order p[-1] = nT(n, 1), p[-2] = nT(n, 2), etc.... Instead of + # putting the 1 from p[0] there, however, it is simply added to + # the sum below which is valid for 1 < k <= n//2 + return (1 + sum(p[1 - k:])) + + +def nT(n, k=None): + """Return the number of ``k``-sized partitions of ``n`` items. + + Possible values for ``n``: + + integer - ``n`` identical items + + sequence - converted to a multiset internally + + multiset - {element: multiplicity} + + Note: the convention for ``nT`` is different than that of ``nC`` and + ``nP`` in that + here an integer indicates ``n`` *identical* items instead of a set of + length ``n``; this is in keeping with the ``partitions`` function which + treats its integer-``n`` input like a list of ``n`` 1s. One can use + ``range(n)`` for ``n`` to indicate ``n`` distinct items. + + If ``k`` is None then the total number of ways to partition the elements + represented in ``n`` will be returned. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import nT + + Partitions of the given multiset: + + >>> [nT('aabbc', i) for i in range(1, 7)] + [1, 8, 11, 5, 1, 0] + >>> nT('aabbc') == sum(_) + True + + >>> [nT("mississippi", i) for i in range(1, 12)] + [1, 74, 609, 1521, 1768, 1224, 579, 197, 50, 9, 1] + + Partitions when all items are identical: + + >>> [nT(5, i) for i in range(1, 6)] + [1, 2, 2, 1, 1] + >>> nT('1'*5) == sum(_) + True + + When all items are different: + + >>> [nT(range(5), i) for i in range(1, 6)] + [1, 15, 25, 10, 1] + >>> nT(range(5)) == sum(_) + True + + Partitions of an integer expressed as a sum of positive integers: + + >>> from sympy import partition + >>> partition(4) + 5 + >>> nT(4, 1) + nT(4, 2) + nT(4, 3) + nT(4, 4) + 5 + >>> nT('1'*4) + 5 + + See Also + ======== + sympy.utilities.iterables.partitions + sympy.utilities.iterables.multiset_partitions + sympy.functions.combinatorial.numbers.partition + + References + ========== + + .. [1] https://web.archive.org/web/20210507012732/https://teaching.csse.uwa.edu.au/units/CITS7209/partition.pdf + + """ + + if isinstance(n, SYMPY_INTS): + # n identical items + if k is None: + return partition(n) + if isinstance(k, SYMPY_INTS): + n = as_int(n) + k = as_int(k) + return Integer(_nT(n, k)) + if not isinstance(n, _MultisetHistogram): + try: + # if n contains hashable items there is some + # quick handling that can be done + u = len(set(n)) + if u <= 1: + return nT(len(n), k) + elif u == len(n): + n = range(u) + raise TypeError + except TypeError: + n = _multiset_histogram(n) + N = n[_N] + if k is None and N == 1: + return 1 + if k in (1, N): + return 1 + if k == 2 or N == 2 and k is None: + m, r = divmod(N, 2) + rv = sum(nC(n, i) for i in range(1, m + 1)) + if not r: + rv -= nC(n, m)//2 + if k is None: + rv += 1 # for k == 1 + return rv + if N == n[_ITEMS]: + # all distinct + if k is None: + return bell(N) + return stirling(N, k) + m = MultisetPartitionTraverser() + if k is None: + return m.count_partitions(n[_M]) + # MultisetPartitionTraverser does not have a range-limited count + # method, so need to enumerate and count + tot = 0 + for discard in m.enum_range(n[_M], k-1, k): + tot += 1 + return tot + + +#-----------------------------------------------------------------------------# +# # +# Motzkin numbers # +# # +#-----------------------------------------------------------------------------# + + +class motzkin(DefinedFunction): + """ + The nth Motzkin number is the number + of ways of drawing non-intersecting chords + between n points on a circle (not necessarily touching + every point by a chord). The Motzkin numbers are named + after Theodore Motzkin and have diverse applications + in geometry, combinatorics and number theory. + + Motzkin numbers are the integer sequence defined by the + initial terms `M_0 = 1`, `M_1 = 1` and the two-term recurrence relation + `M_n = \frac{2*n + 1}{n + 2} * M_{n-1} + \frac{3n - 3}{n + 2} * M_{n-2}`. + + + Examples + ======== + + >>> from sympy import motzkin + + >>> motzkin.is_motzkin(5) + False + >>> motzkin.find_motzkin_numbers_in_range(2,300) + [2, 4, 9, 21, 51, 127] + >>> motzkin.find_motzkin_numbers_in_range(2,900) + [2, 4, 9, 21, 51, 127, 323, 835] + >>> motzkin.find_first_n_motzkins(10) + [1, 1, 2, 4, 9, 21, 51, 127, 323, 835] + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Motzkin_number + .. [2] https://mathworld.wolfram.com/MotzkinNumber.html + + """ + + @staticmethod + def is_motzkin(n): + try: + n = as_int(n) + except ValueError: + return False + if n > 0: + if n in (1, 2): + return True + + tn1 = 1 + tn = 2 + i = 3 + while tn < n: + a = ((2*i + 1)*tn + (3*i - 3)*tn1)/(i + 2) + i += 1 + tn1 = tn + tn = a + + if tn == n: + return True + else: + return False + + else: + return False + + @staticmethod + def find_motzkin_numbers_in_range(x, y): + if 0 <= x <= y: + motzkins = [] + if x <= 1 <= y: + motzkins.append(1) + tn1 = 1 + tn = 2 + i = 3 + while tn <= y: + if tn >= x: + motzkins.append(tn) + a = ((2*i + 1)*tn + (3*i - 3)*tn1)/(i + 2) + i += 1 + tn1 = tn + tn = int(a) + + return motzkins + + else: + raise ValueError('The provided range is not valid. This condition should satisfy x <= y') + + @staticmethod + def find_first_n_motzkins(n): + try: + n = as_int(n) + except ValueError: + raise ValueError('The provided number must be a positive integer') + if n < 0: + raise ValueError('The provided number must be a positive integer') + motzkins = [1] + if n >= 1: + motzkins.append(1) + tn1 = 1 + tn = 2 + i = 3 + while i <= n: + motzkins.append(tn) + a = ((2*i + 1)*tn + (3*i - 3)*tn1)/(i + 2) + i += 1 + tn1 = tn + tn = int(a) + + return motzkins + + @staticmethod + @recurrence_memo([S.One, S.One]) + def _motzkin(n, prev): + return ((2*n + 1)*prev[-1] + (3*n - 3)*prev[-2]) // (n + 2) + + @classmethod + def eval(cls, n): + try: + n = as_int(n) + except ValueError: + raise ValueError('The provided number must be a positive integer') + if n < 0: + raise ValueError('The provided number must be a positive integer') + return Integer(cls._motzkin(n - 1)) + + +def nD(i=None, brute=None, *, n=None, m=None): + """return the number of derangements for: ``n`` unique items, ``i`` + items (as a sequence or multiset), or multiplicities, ``m`` given + as a sequence or multiset. + + Examples + ======== + + >>> from sympy.utilities.iterables import generate_derangements as enum + >>> from sympy.functions.combinatorial.numbers import nD + + A derangement ``d`` of sequence ``s`` has all ``d[i] != s[i]``: + + >>> set([''.join(i) for i in enum('abc')]) + {'bca', 'cab'} + >>> nD('abc') + 2 + + Input as iterable or dictionary (multiset form) is accepted: + + >>> assert nD([1, 2, 2, 3, 3, 3]) == nD({1: 1, 2: 2, 3: 3}) + + By default, a brute-force enumeration and count of multiset permutations + is only done if there are fewer than 9 elements. There may be cases when + there is high multiplicity with few unique elements that will benefit + from a brute-force enumeration, too. For this reason, the `brute` + keyword (default None) is provided. When False, the brute-force + enumeration will never be used. When True, it will always be used. + + >>> nD('1111222233', brute=True) + 44 + + For convenience, one may specify ``n`` distinct items using the + ``n`` keyword: + + >>> assert nD(n=3) == nD('abc') == 2 + + Since the number of derangments depends on the multiplicity of the + elements and not the elements themselves, it may be more convenient + to give a list or multiset of multiplicities using keyword ``m``: + + >>> assert nD('abc') == nD(m=(1,1,1)) == nD(m={1:3}) == 2 + + """ + from sympy.integrals.integrals import integrate + from sympy.functions.special.polynomials import laguerre + from sympy.abc import x + def ok(x): + if not isinstance(x, SYMPY_INTS): + raise TypeError('expecting integer values') + if x < 0: + raise ValueError('value must not be negative') + return True + + if (i, n, m).count(None) != 2: + raise ValueError('enter only 1 of i, n, or m') + if i is not None: + if isinstance(i, SYMPY_INTS): + raise TypeError('items must be a list or dictionary') + if not i: + return S.Zero + if type(i) is not dict: + s = list(i) + ms = multiset(s) + elif type(i) is dict: + all(ok(_) for _ in i.values()) + ms = {k: v for k, v in i.items() if v} + s = None + if not ms: + return S.Zero + N = sum(ms.values()) + counts = multiset(ms.values()) + nkey = len(ms) + elif n is not None: + ok(n) + if not n: + return S.Zero + return subfactorial(n) + elif m is not None: + if isinstance(m, dict): + all(ok(i) and ok(j) for i, j in m.items()) + counts = {k: v for k, v in m.items() if k*v} + elif iterable(m) or isinstance(m, str): + m = list(m) + all(ok(i) for i in m) + counts = multiset([i for i in m if i]) + else: + raise TypeError('expecting iterable') + if not counts: + return S.Zero + N = sum(k*v for k, v in counts.items()) + nkey = sum(counts.values()) + s = None + big = int(max(counts)) + if big == 1: # no repetition + return subfactorial(nkey) + nval = len(counts) + if big*2 > N: + return S.Zero + if big*2 == N: + if nkey == 2 and nval == 1: + return S.One # aaabbb + if nkey - 1 == big: # one element repeated + return factorial(big) # e.g. abc part of abcddd + if N < 9 and brute is None or brute: + # for all possibilities, this was found to be faster + if s is None: + s = [] + i = 0 + for m, v in counts.items(): + for j in range(v): + s.extend([i]*m) + i += 1 + return Integer(sum(1 for i in multiset_derangements(s))) + from sympy.functions.elementary.exponential import exp + return Integer(abs(integrate(exp(-x)*Mul(*[ + laguerre(i, x)**m for i, m in counts.items()]), (x, 0, oo)))) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/tests/test_comb_factorials.py b/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/tests/test_comb_factorials.py new file mode 100644 index 0000000000000000000000000000000000000000..6e3986c56736cccec0b3370007e047a1f38f06d1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/tests/test_comb_factorials.py @@ -0,0 +1,653 @@ +from sympy.concrete.products import Product +from sympy.core.function import expand_func +from sympy.core.mod import Mod +from sympy.core.mul import Mul +from sympy.core import EulerGamma +from sympy.core.numbers import (Float, I, Rational, nan, oo, pi, zoo) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.functions.combinatorial.factorials import (ff, rf, binomial, factorial, factorial2) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.gamma_functions import (gamma, polygamma) +from sympy.polys.polytools import Poly +from sympy.series.order import O +from sympy.simplify.simplify import simplify +from sympy.core.expr import unchanged +from sympy.core.function import ArgumentIndexError +from sympy.functions.combinatorial.factorials import subfactorial +from sympy.functions.special.gamma_functions import uppergamma +from sympy.testing.pytest import XFAIL, raises, slow + +#Solves and Fixes Issue #10388 - This is the updated test for the same solved issue + +def test_rf_eval_apply(): + x, y = symbols('x,y') + n, k = symbols('n k', integer=True) + m = Symbol('m', integer=True, nonnegative=True) + + assert rf(nan, y) is nan + assert rf(x, nan) is nan + + assert unchanged(rf, x, y) + + assert rf(oo, 0) == 1 + assert rf(-oo, 0) == 1 + + assert rf(oo, 6) is oo + assert rf(-oo, 7) is -oo + assert rf(-oo, 6) is oo + + assert rf(oo, -6) is oo + assert rf(-oo, -7) is oo + + assert rf(-1, pi) == 0 + assert rf(-5, 1 + I) == 0 + + assert unchanged(rf, -3, k) + assert unchanged(rf, x, Symbol('k', integer=False)) + assert rf(-3, Symbol('k', integer=False)) == 0 + assert rf(Symbol('x', negative=True, integer=True), Symbol('k', integer=False)) == 0 + + assert rf(x, 0) == 1 + assert rf(x, 1) == x + assert rf(x, 2) == x*(x + 1) + assert rf(x, 3) == x*(x + 1)*(x + 2) + assert rf(x, 5) == x*(x + 1)*(x + 2)*(x + 3)*(x + 4) + + assert rf(x, -1) == 1/(x - 1) + assert rf(x, -2) == 1/((x - 1)*(x - 2)) + assert rf(x, -3) == 1/((x - 1)*(x - 2)*(x - 3)) + + assert rf(1, 100) == factorial(100) + + assert rf(x**2 + 3*x, 2) == (x**2 + 3*x)*(x**2 + 3*x + 1) + assert isinstance(rf(x**2 + 3*x, 2), Mul) + assert rf(x**3 + x, -2) == 1/((x**3 + x - 1)*(x**3 + x - 2)) + + assert rf(Poly(x**2 + 3*x, x), 2) == Poly(x**4 + 8*x**3 + 19*x**2 + 12*x, x) + assert isinstance(rf(Poly(x**2 + 3*x, x), 2), Poly) + raises(ValueError, lambda: rf(Poly(x**2 + 3*x, x, y), 2)) + assert rf(Poly(x**3 + x, x), -2) == 1/(x**6 - 9*x**5 + 35*x**4 - 75*x**3 + 94*x**2 - 66*x + 20) + raises(ValueError, lambda: rf(Poly(x**3 + x, x, y), -2)) + + assert rf(x, m).is_integer is None + assert rf(n, k).is_integer is None + assert rf(n, m).is_integer is True + assert rf(n, k + pi).is_integer is False + assert rf(n, m + pi).is_integer is False + assert rf(pi, m).is_integer is False + + def check(x, k, o, n): + a, b = Dummy(), Dummy() + r = lambda x, k: o(a, b).rewrite(n).subs({a:x,b:k}) + for i in range(-5,5): + for j in range(-5,5): + assert o(i, j) == r(i, j), (o, n, i, j) + check(x, k, rf, ff) + check(x, k, rf, binomial) + check(n, k, rf, factorial) + check(x, y, rf, factorial) + check(x, y, rf, binomial) + + assert rf(x, k).rewrite(ff) == ff(x + k - 1, k) + assert rf(x, k).rewrite(gamma) == Piecewise( + (gamma(k + x)/gamma(x), x > 0), + ((-1)**k*gamma(1 - x)/gamma(-k - x + 1), True)) + assert rf(5, k).rewrite(gamma) == gamma(k + 5)/24 + assert rf(x, k).rewrite(binomial) == factorial(k)*binomial(x + k - 1, k) + assert rf(n, k).rewrite(factorial) == Piecewise( + (factorial(k + n - 1)/factorial(n - 1), n > 0), + ((-1)**k*factorial(-n)/factorial(-k - n), True)) + assert rf(5, k).rewrite(factorial) == factorial(k + 4)/24 + assert rf(x, y).rewrite(factorial) == rf(x, y) + assert rf(x, y).rewrite(binomial) == rf(x, y) + + import random + from mpmath import rf as mpmath_rf + for i in range(100): + x = -500 + 500 * random.random() + k = -500 + 500 * random.random() + assert (abs(mpmath_rf(x, k) - rf(x, k)) < 10**(-15)) + + +def test_ff_eval_apply(): + x, y = symbols('x,y') + n, k = symbols('n k', integer=True) + m = Symbol('m', integer=True, nonnegative=True) + + assert ff(nan, y) is nan + assert ff(x, nan) is nan + + assert unchanged(ff, x, y) + + assert ff(oo, 0) == 1 + assert ff(-oo, 0) == 1 + + assert ff(oo, 6) is oo + assert ff(-oo, 7) is -oo + assert ff(-oo, 6) is oo + + assert ff(oo, -6) is oo + assert ff(-oo, -7) is oo + + assert ff(x, 0) == 1 + assert ff(x, 1) == x + assert ff(x, 2) == x*(x - 1) + assert ff(x, 3) == x*(x - 1)*(x - 2) + assert ff(x, 5) == x*(x - 1)*(x - 2)*(x - 3)*(x - 4) + + assert ff(x, -1) == 1/(x + 1) + assert ff(x, -2) == 1/((x + 1)*(x + 2)) + assert ff(x, -3) == 1/((x + 1)*(x + 2)*(x + 3)) + + assert ff(100, 100) == factorial(100) + + assert ff(2*x**2 - 5*x, 2) == (2*x**2 - 5*x)*(2*x**2 - 5*x - 1) + assert isinstance(ff(2*x**2 - 5*x, 2), Mul) + assert ff(x**2 + 3*x, -2) == 1/((x**2 + 3*x + 1)*(x**2 + 3*x + 2)) + + assert ff(Poly(2*x**2 - 5*x, x), 2) == Poly(4*x**4 - 28*x**3 + 59*x**2 - 35*x, x) + assert isinstance(ff(Poly(2*x**2 - 5*x, x), 2), Poly) + raises(ValueError, lambda: ff(Poly(2*x**2 - 5*x, x, y), 2)) + assert ff(Poly(x**2 + 3*x, x), -2) == 1/(x**4 + 12*x**3 + 49*x**2 + 78*x + 40) + raises(ValueError, lambda: ff(Poly(x**2 + 3*x, x, y), -2)) + + + assert ff(x, m).is_integer is None + assert ff(n, k).is_integer is None + assert ff(n, m).is_integer is True + assert ff(n, k + pi).is_integer is False + assert ff(n, m + pi).is_integer is False + assert ff(pi, m).is_integer is False + + assert isinstance(ff(x, x), ff) + assert ff(n, n) == factorial(n) + + def check(x, k, o, n): + a, b = Dummy(), Dummy() + r = lambda x, k: o(a, b).rewrite(n).subs({a:x,b:k}) + for i in range(-5,5): + for j in range(-5,5): + assert o(i, j) == r(i, j), (o, n) + check(x, k, ff, rf) + check(x, k, ff, gamma) + check(n, k, ff, factorial) + check(x, k, ff, binomial) + check(x, y, ff, factorial) + check(x, y, ff, binomial) + + assert ff(x, k).rewrite(rf) == rf(x - k + 1, k) + assert ff(x, k).rewrite(gamma) == Piecewise( + (gamma(x + 1)/gamma(-k + x + 1), x >= 0), + ((-1)**k*gamma(k - x)/gamma(-x), True)) + assert ff(5, k).rewrite(gamma) == 120/gamma(6 - k) + assert ff(n, k).rewrite(factorial) == Piecewise( + (factorial(n)/factorial(-k + n), n >= 0), + ((-1)**k*factorial(k - n - 1)/factorial(-n - 1), True)) + assert ff(5, k).rewrite(factorial) == 120/factorial(5 - k) + assert ff(x, k).rewrite(binomial) == factorial(k) * binomial(x, k) + assert ff(x, y).rewrite(factorial) == ff(x, y) + assert ff(x, y).rewrite(binomial) == ff(x, y) + + import random + from mpmath import ff as mpmath_ff + for i in range(100): + x = -500 + 500 * random.random() + k = -500 + 500 * random.random() + a = mpmath_ff(x, k) + b = ff(x, k) + assert (abs(a - b) < abs(a) * 10**(-15)) + + +def test_rf_ff_eval_hiprec(): + maple = Float('6.9109401292234329956525265438452') + us = ff(18, Rational(2, 3)).evalf(32) + assert abs(us - maple)/us < 1e-31 + + maple = Float('6.8261540131125511557924466355367') + us = rf(18, Rational(2, 3)).evalf(32) + assert abs(us - maple)/us < 1e-31 + + maple = Float('34.007346127440197150854651814225') + us = rf(Float('4.4', 32), Float('2.2', 32)) + assert abs(us - maple)/us < 1e-31 + + +def test_rf_lambdify_mpmath(): + from sympy.utilities.lambdify import lambdify + x, y = symbols('x,y') + f = lambdify((x,y), rf(x, y), 'mpmath') + maple = Float('34.007346127440197') + us = f(4.4, 2.2) + assert abs(us - maple)/us < 1e-15 + + +def test_factorial(): + x = Symbol('x') + n = Symbol('n', integer=True) + k = Symbol('k', integer=True, nonnegative=True) + r = Symbol('r', integer=False) + s = Symbol('s', integer=False, negative=True) + t = Symbol('t', nonnegative=True) + u = Symbol('u', noninteger=True) + + assert factorial(-2) is zoo + assert factorial(0) == 1 + assert factorial(7) == 5040 + assert factorial(19) == 121645100408832000 + assert factorial(31) == 8222838654177922817725562880000000 + assert factorial(n).func == factorial + assert factorial(2*n).func == factorial + + assert factorial(x).is_integer is None + assert factorial(n).is_integer is None + assert factorial(k).is_integer + assert factorial(r).is_integer is None + + assert factorial(n).is_positive is None + assert factorial(k).is_positive + + assert factorial(x).is_real is None + assert factorial(n).is_real is None + assert factorial(k).is_real is True + assert factorial(r).is_real is None + assert factorial(s).is_real is True + assert factorial(t).is_real is True + assert factorial(u).is_real is True + + assert factorial(x).is_composite is None + assert factorial(n).is_composite is None + assert factorial(k).is_composite is None + assert factorial(k + 3).is_composite is True + assert factorial(r).is_composite is None + assert factorial(s).is_composite is None + assert factorial(t).is_composite is None + assert factorial(u).is_composite is None + + assert factorial(oo) is oo + + +def test_factorial_Mod(): + pr = Symbol('pr', prime=True) + p, q = 10**9 + 9, 10**9 + 33 # prime modulo + r, s = 10**7 + 5, 33333333 # composite modulo + assert Mod(factorial(pr - 1), pr) == pr - 1 + assert Mod(factorial(pr - 1), -pr) == -1 + assert Mod(factorial(r - 1, evaluate=False), r) == 0 + assert Mod(factorial(s - 1, evaluate=False), s) == 0 + assert Mod(factorial(p - 1, evaluate=False), p) == p - 1 + assert Mod(factorial(q - 1, evaluate=False), q) == q - 1 + assert Mod(factorial(p - 50, evaluate=False), p) == 854928834 + assert Mod(factorial(q - 1800, evaluate=False), q) == 905504050 + assert Mod(factorial(153, evaluate=False), r) == Mod(factorial(153), r) + assert Mod(factorial(255, evaluate=False), s) == Mod(factorial(255), s) + assert Mod(factorial(4, evaluate=False), 3) == S.Zero + assert Mod(factorial(5, evaluate=False), 6) == S.Zero + + +def test_factorial_diff(): + n = Symbol('n', integer=True) + + assert factorial(n).diff(n) == \ + gamma(1 + n)*polygamma(0, 1 + n) + assert factorial(n**2).diff(n) == \ + 2*n*gamma(1 + n**2)*polygamma(0, 1 + n**2) + raises(ArgumentIndexError, lambda: factorial(n**2).fdiff(2)) + + +def test_factorial_series(): + n = Symbol('n', integer=True) + + assert factorial(n).series(n, 0, 3) == \ + 1 - n*EulerGamma + n**2*(EulerGamma**2/2 + pi**2/12) + O(n**3) + + +def test_factorial_rewrite(): + n = Symbol('n', integer=True) + k = Symbol('k', integer=True, nonnegative=True) + + assert factorial(n).rewrite(gamma) == gamma(n + 1) + _i = Dummy('i') + assert factorial(k).rewrite(Product).dummy_eq(Product(_i, (_i, 1, k))) + assert factorial(n).rewrite(Product) == factorial(n) + + +def test_factorial2(): + n = Symbol('n', integer=True) + + assert factorial2(-1) == 1 + assert factorial2(0) == 1 + assert factorial2(7) == 105 + assert factorial2(8) == 384 + + # The following is exhaustive + tt = Symbol('tt', integer=True, nonnegative=True) + tte = Symbol('tte', even=True, nonnegative=True) + tpe = Symbol('tpe', even=True, positive=True) + tto = Symbol('tto', odd=True, nonnegative=True) + tf = Symbol('tf', integer=True, nonnegative=False) + tfe = Symbol('tfe', even=True, nonnegative=False) + tfo = Symbol('tfo', odd=True, nonnegative=False) + ft = Symbol('ft', integer=False, nonnegative=True) + ff = Symbol('ff', integer=False, nonnegative=False) + fn = Symbol('fn', integer=False) + nt = Symbol('nt', nonnegative=True) + nf = Symbol('nf', nonnegative=False) + nn = Symbol('nn') + z = Symbol('z', zero=True) + #Solves and Fixes Issue #10388 - This is the updated test for the same solved issue + raises(ValueError, lambda: factorial2(oo)) + raises(ValueError, lambda: factorial2(Rational(5, 2))) + raises(ValueError, lambda: factorial2(-4)) + assert factorial2(n).is_integer is None + assert factorial2(tt - 1).is_integer + assert factorial2(tte - 1).is_integer + assert factorial2(tpe - 3).is_integer + assert factorial2(tto - 4).is_integer + assert factorial2(tto - 2).is_integer + assert factorial2(tf).is_integer is None + assert factorial2(tfe).is_integer is None + assert factorial2(tfo).is_integer is None + assert factorial2(ft).is_integer is None + assert factorial2(ff).is_integer is None + assert factorial2(fn).is_integer is None + assert factorial2(nt).is_integer is None + assert factorial2(nf).is_integer is None + assert factorial2(nn).is_integer is None + + assert factorial2(n).is_positive is None + assert factorial2(tt - 1).is_positive is True + assert factorial2(tte - 1).is_positive is True + assert factorial2(tpe - 3).is_positive is True + assert factorial2(tpe - 1).is_positive is True + assert factorial2(tto - 2).is_positive is True + assert factorial2(tto - 1).is_positive is True + assert factorial2(tf).is_positive is None + assert factorial2(tfe).is_positive is None + assert factorial2(tfo).is_positive is None + assert factorial2(ft).is_positive is None + assert factorial2(ff).is_positive is None + assert factorial2(fn).is_positive is None + assert factorial2(nt).is_positive is None + assert factorial2(nf).is_positive is None + assert factorial2(nn).is_positive is None + + assert factorial2(tt).is_even is None + assert factorial2(tt).is_odd is None + assert factorial2(tte).is_even is None + assert factorial2(tte).is_odd is None + assert factorial2(tte + 2).is_even is True + assert factorial2(tpe).is_even is True + assert factorial2(tpe).is_odd is False + assert factorial2(tto).is_odd is True + assert factorial2(tf).is_even is None + assert factorial2(tf).is_odd is None + assert factorial2(tfe).is_even is None + assert factorial2(tfe).is_odd is None + assert factorial2(tfo).is_even is False + assert factorial2(tfo).is_odd is None + assert factorial2(z).is_even is False + assert factorial2(z).is_odd is True + + +def test_factorial2_rewrite(): + n = Symbol('n', integer=True) + assert factorial2(n).rewrite(gamma) == \ + 2**(n/2)*Piecewise((1, Eq(Mod(n, 2), 0)), (sqrt(2)/sqrt(pi), Eq(Mod(n, 2), 1)))*gamma(n/2 + 1) + assert factorial2(2*n).rewrite(gamma) == 2**n*gamma(n + 1) + assert factorial2(2*n + 1).rewrite(gamma) == \ + sqrt(2)*2**(n + S.Half)*gamma(n + Rational(3, 2))/sqrt(pi) + + +def test_binomial(): + x = Symbol('x') + n = Symbol('n', integer=True) + nz = Symbol('nz', integer=True, nonzero=True) + k = Symbol('k', integer=True) + kp = Symbol('kp', integer=True, positive=True) + kn = Symbol('kn', integer=True, negative=True) + u = Symbol('u', negative=True) + v = Symbol('v', nonnegative=True) + p = Symbol('p', positive=True) + z = Symbol('z', zero=True) + nt = Symbol('nt', integer=False) + kt = Symbol('kt', integer=False) + a = Symbol('a', integer=True, nonnegative=True) + b = Symbol('b', integer=True, nonnegative=True) + + assert binomial(0, 0) == 1 + assert binomial(1, 1) == 1 + assert binomial(10, 10) == 1 + assert binomial(n, z) == 1 + assert binomial(1, 2) == 0 + assert binomial(-1, 2) == 1 + assert binomial(1, -1) == 0 + assert binomial(-1, 1) == -1 + assert binomial(-1, -1) == 0 + assert binomial(S.Half, S.Half) == 1 + assert binomial(-10, 1) == -10 + assert binomial(-10, 7) == -11440 + assert binomial(n, -1) == 0 # holds for all integers (negative, zero, positive) + assert binomial(kp, -1) == 0 + assert binomial(nz, 0) == 1 + assert expand_func(binomial(n, 1)) == n + assert expand_func(binomial(n, 2)) == n*(n - 1)/2 + assert expand_func(binomial(n, n - 2)) == n*(n - 1)/2 + assert expand_func(binomial(n, n - 1)) == n + assert binomial(n, 3).func == binomial + assert binomial(n, 3).expand(func=True) == n**3/6 - n**2/2 + n/3 + assert expand_func(binomial(n, 3)) == n*(n - 2)*(n - 1)/6 + assert binomial(n, n).func == binomial # e.g. (-1, -1) == 0, (2, 2) == 1 + assert binomial(n, n + 1).func == binomial # e.g. (-1, 0) == 1 + assert binomial(kp, kp + 1) == 0 + assert binomial(kn, kn) == 0 # issue #14529 + assert binomial(n, u).func == binomial + assert binomial(kp, u).func == binomial + assert binomial(n, p).func == binomial + assert binomial(n, k).func == binomial + assert binomial(n, n + p).func == binomial + assert binomial(kp, kp + p).func == binomial + + assert expand_func(binomial(n, n - 3)) == n*(n - 2)*(n - 1)/6 + + assert binomial(n, k).is_integer + assert binomial(nt, k).is_integer is None + assert binomial(x, nt).is_integer is False + + assert binomial(gamma(25), 6) == 79232165267303928292058750056084441948572511312165380965440075720159859792344339983120618959044048198214221915637090855535036339620413440000 + assert binomial(1324, 47) == 906266255662694632984994480774946083064699457235920708992926525848438478406790323869952 + assert binomial(1735, 43) == 190910140420204130794758005450919715396159959034348676124678207874195064798202216379800 + assert binomial(2512, 53) == 213894469313832631145798303740098720367984955243020898718979538096223399813295457822575338958939834177325304000 + assert binomial(3383, 52) == 27922807788818096863529701501764372757272890613101645521813434902890007725667814813832027795881839396839287659777235 + assert binomial(4321, 51) == 124595639629264868916081001263541480185227731958274383287107643816863897851139048158022599533438936036467601690983780576 + + assert binomial(a, b).is_nonnegative is True + assert binomial(-1, 2, evaluate=False).is_nonnegative is True + assert binomial(10, 5, evaluate=False).is_nonnegative is True + assert binomial(10, -3, evaluate=False).is_nonnegative is True + assert binomial(-10, -3, evaluate=False).is_nonnegative is True + assert binomial(-10, 2, evaluate=False).is_nonnegative is True + assert binomial(-10, 1, evaluate=False).is_nonnegative is False + assert binomial(-10, 7, evaluate=False).is_nonnegative is False + + # issue #14625 + for _ in (pi, -pi, nt, v, a): + assert binomial(_, _) == 1 + assert binomial(_, _ - 1) == _ + assert isinstance(binomial(u, u), binomial) + assert isinstance(binomial(u, u - 1), binomial) + assert isinstance(binomial(x, x), binomial) + assert isinstance(binomial(x, x - 1), binomial) + + #issue #18802 + assert expand_func(binomial(x + 1, x)) == x + 1 + assert expand_func(binomial(x, x - 1)) == x + assert expand_func(binomial(x + 1, x - 1)) == x*(x + 1)/2 + assert expand_func(binomial(x**2 + 1, x**2)) == x**2 + 1 + + # issue #13980 and #13981 + assert binomial(-7, -5) == 0 + assert binomial(-23, -12) == 0 + assert binomial(Rational(13, 2), -10) == 0 + assert binomial(-49, -51) == 0 + + assert binomial(19, Rational(-7, 2)) == S(-68719476736)/(911337863661225*pi) + assert binomial(0, Rational(3, 2)) == S(-2)/(3*pi) + assert binomial(-3, Rational(-7, 2)) is zoo + assert binomial(kn, kt) is zoo + + assert binomial(nt, kt).func == binomial + assert binomial(nt, Rational(15, 6)) == 8*gamma(nt + 1)/(15*sqrt(pi)*gamma(nt - Rational(3, 2))) + assert binomial(Rational(20, 3), Rational(-10, 8)) == gamma(Rational(23, 3))/(gamma(Rational(-1, 4))*gamma(Rational(107, 12))) + assert binomial(Rational(19, 2), Rational(-7, 2)) == Rational(-1615, 8388608) + assert binomial(Rational(-13, 5), Rational(-7, 8)) == gamma(Rational(-8, 5))/(gamma(Rational(-29, 40))*gamma(Rational(1, 8))) + assert binomial(Rational(-19, 8), Rational(-13, 5)) == gamma(Rational(-11, 8))/(gamma(Rational(-8, 5))*gamma(Rational(49, 40))) + + # binomial for complexes + assert binomial(I, Rational(-89, 8)) == gamma(1 + I)/(gamma(Rational(-81, 8))*gamma(Rational(97, 8) + I)) + assert binomial(I, 2*I) == gamma(1 + I)/(gamma(1 - I)*gamma(1 + 2*I)) + assert binomial(-7, I) is zoo + assert binomial(Rational(-7, 6), I) == gamma(Rational(-1, 6))/(gamma(Rational(-1, 6) - I)*gamma(1 + I)) + assert binomial((1+2*I), (1+3*I)) == gamma(2 + 2*I)/(gamma(1 - I)*gamma(2 + 3*I)) + assert binomial(I, 5) == Rational(1, 3) - I/S(12) + assert binomial((2*I + 3), 7) == -13*I/S(63) + assert isinstance(binomial(I, n), binomial) + assert expand_func(binomial(3, 2, evaluate=False)) == 3 + assert expand_func(binomial(n, 0, evaluate=False)) == 1 + assert expand_func(binomial(n, -2, evaluate=False)) == 0 + assert expand_func(binomial(n, k)) == binomial(n, k) + + +def test_binomial_Mod(): + p, q = 10**5 + 3, 10**9 + 33 # prime modulo + r = 10**7 + 5 # composite modulo + + # A few tests to get coverage + # Lucas Theorem + assert Mod(binomial(156675, 4433, evaluate=False), p) == Mod(binomial(156675, 4433), p) + + # factorial Mod + assert Mod(binomial(1234, 432, evaluate=False), q) == Mod(binomial(1234, 432), q) + + # binomial factorize + assert Mod(binomial(253, 113, evaluate=False), r) == Mod(binomial(253, 113), r) + + # using Granville's generalisation of Lucas' Theorem + assert Mod(binomial(10**18, 10**12, evaluate=False), p*p) == 3744312326 + + +@slow +def test_binomial_Mod_slow(): + p, q = 10**5 + 3, 10**9 + 33 # prime modulo + r, s = 10**7 + 5, 33333333 # composite modulo + + n, k, m = symbols('n k m') + assert (binomial(n, k) % q).subs({n: s, k: p}) == Mod(binomial(s, p), q) + assert (binomial(n, k) % m).subs({n: 8, k: 5, m: 13}) == 4 + assert (binomial(9, k) % 7).subs(k, 2) == 1 + + # Lucas Theorem + assert Mod(binomial(123456, 43253, evaluate=False), p) == Mod(binomial(123456, 43253), p) + assert Mod(binomial(-178911, 237, evaluate=False), p) == Mod(-binomial(178911 + 237 - 1, 237), p) + assert Mod(binomial(-178911, 238, evaluate=False), p) == Mod(binomial(178911 + 238 - 1, 238), p) + + # factorial Mod + assert Mod(binomial(9734, 451, evaluate=False), q) == Mod(binomial(9734, 451), q) + assert Mod(binomial(-10733, 4459, evaluate=False), q) == Mod(binomial(-10733, 4459), q) + assert Mod(binomial(-15733, 4458, evaluate=False), q) == Mod(binomial(-15733, 4458), q) + assert Mod(binomial(23, -38, evaluate=False), q) is S.Zero + assert Mod(binomial(23, 38, evaluate=False), q) is S.Zero + + # binomial factorize + assert Mod(binomial(753, 119, evaluate=False), r) == Mod(binomial(753, 119), r) + assert Mod(binomial(3781, 948, evaluate=False), s) == Mod(binomial(3781, 948), s) + assert Mod(binomial(25773, 1793, evaluate=False), s) == Mod(binomial(25773, 1793), s) + assert Mod(binomial(-753, 118, evaluate=False), r) == Mod(binomial(-753, 118), r) + assert Mod(binomial(-25773, 1793, evaluate=False), s) == Mod(binomial(-25773, 1793), s) + + +def test_binomial_diff(): + n = Symbol('n', integer=True) + k = Symbol('k', integer=True) + + assert binomial(n, k).diff(n) == \ + (-polygamma(0, 1 + n - k) + polygamma(0, 1 + n))*binomial(n, k) + assert binomial(n**2, k**3).diff(n) == \ + 2*n*(-polygamma( + 0, 1 + n**2 - k**3) + polygamma(0, 1 + n**2))*binomial(n**2, k**3) + + assert binomial(n, k).diff(k) == \ + (-polygamma(0, 1 + k) + polygamma(0, 1 + n - k))*binomial(n, k) + assert binomial(n**2, k**3).diff(k) == \ + 3*k**2*(-polygamma( + 0, 1 + k**3) + polygamma(0, 1 + n**2 - k**3))*binomial(n**2, k**3) + raises(ArgumentIndexError, lambda: binomial(n, k).fdiff(3)) + + +def test_binomial_rewrite(): + n = Symbol('n', integer=True) + k = Symbol('k', integer=True) + x = Symbol('x') + + assert binomial(n, k).rewrite( + factorial) == factorial(n)/(factorial(k)*factorial(n - k)) + assert binomial( + n, k).rewrite(gamma) == gamma(n + 1)/(gamma(k + 1)*gamma(n - k + 1)) + assert binomial(n, k).rewrite(ff) == ff(n, k) / factorial(k) + assert binomial(n, x).rewrite(ff) == binomial(n, x) + + +@XFAIL +def test_factorial_simplify_fail(): + # simplify(factorial(x + 1).diff(x) - ((x + 1)*factorial(x)).diff(x))) == 0 + from sympy.abc import x + assert simplify(x*polygamma(0, x + 1) - x*polygamma(0, x + 2) + + polygamma(0, x + 1) - polygamma(0, x + 2) + 1) == 0 + + +def test_subfactorial(): + assert all(subfactorial(i) == ans for i, ans in enumerate( + [1, 0, 1, 2, 9, 44, 265, 1854, 14833, 133496])) + assert subfactorial(oo) is oo + assert subfactorial(nan) is nan + assert subfactorial(23) == 9510425471055777937262 + assert unchanged(subfactorial, 2.2) + + x = Symbol('x') + assert subfactorial(x).rewrite(uppergamma) == uppergamma(x + 1, -1)/S.Exp1 + + tt = Symbol('tt', integer=True, nonnegative=True) + tf = Symbol('tf', integer=True, nonnegative=False) + tn = Symbol('tf', integer=True) + ft = Symbol('ft', integer=False, nonnegative=True) + ff = Symbol('ff', integer=False, nonnegative=False) + fn = Symbol('ff', integer=False) + nt = Symbol('nt', nonnegative=True) + nf = Symbol('nf', nonnegative=False) + nn = Symbol('nf') + te = Symbol('te', even=True, nonnegative=True) + to = Symbol('to', odd=True, nonnegative=True) + assert subfactorial(tt).is_integer + assert subfactorial(tf).is_integer is None + assert subfactorial(tn).is_integer is None + assert subfactorial(ft).is_integer is None + assert subfactorial(ff).is_integer is None + assert subfactorial(fn).is_integer is None + assert subfactorial(nt).is_integer is None + assert subfactorial(nf).is_integer is None + assert subfactorial(nn).is_integer is None + assert subfactorial(tt).is_nonnegative + assert subfactorial(tf).is_nonnegative is None + assert subfactorial(tn).is_nonnegative is None + assert subfactorial(ft).is_nonnegative is None + assert subfactorial(ff).is_nonnegative is None + assert subfactorial(fn).is_nonnegative is None + assert subfactorial(nt).is_nonnegative is None + assert subfactorial(nf).is_nonnegative is None + assert subfactorial(nn).is_nonnegative is None + assert subfactorial(tt).is_even is None + assert subfactorial(tt).is_odd is None + assert subfactorial(te).is_odd is True + assert subfactorial(to).is_even is True diff --git a/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/tests/test_comb_numbers.py b/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/tests/test_comb_numbers.py new file mode 100644 index 0000000000000000000000000000000000000000..83a7de89ed8e4fcc433d29f41fc87b9d0d397539 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/combinatorial/tests/test_comb_numbers.py @@ -0,0 +1,1250 @@ +import string + +from sympy.concrete.products import Product +from sympy.concrete.summations import Sum +from sympy.core.function import (diff, expand_func) +from sympy.core import (EulerGamma, TribonacciConstant) +from sympy.core.numbers import (Float, I, Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.functions.combinatorial.numbers import carmichael +from sympy.functions.elementary.complexes import (im, re) +from sympy.functions.elementary.integers import floor +from sympy.polys.polytools import cancel +from sympy.series.limits import limit, Limit +from sympy.series.order import O +from sympy.functions import ( + bernoulli, harmonic, bell, fibonacci, tribonacci, lucas, euler, catalan, + genocchi, andre, partition, divisor_sigma, udivisor_sigma, legendre_symbol, + jacobi_symbol, kronecker_symbol, mobius, + primenu, primeomega, totient, reduced_totient, primepi, + motzkin, binomial, gamma, sqrt, cbrt, hyper, log, digamma, + trigamma, polygamma, factorial, sin, cos, cot, polylog, zeta, dirichlet_eta) +from sympy.functions.combinatorial.numbers import _nT +from sympy.ntheory.factor_ import factorint + +from sympy.core.expr import unchanged +from sympy.core.numbers import GoldenRatio, Integer + +from sympy.testing.pytest import raises, nocache_fail, warns_deprecated_sympy +from sympy.abc import x + + +def test_carmichael(): + with warns_deprecated_sympy(): + assert carmichael.is_prime(2821) == False + + +def test_bernoulli(): + assert bernoulli(0) == 1 + assert bernoulli(1) == Rational(1, 2) + assert bernoulli(2) == Rational(1, 6) + assert bernoulli(3) == 0 + assert bernoulli(4) == Rational(-1, 30) + assert bernoulli(5) == 0 + assert bernoulli(6) == Rational(1, 42) + assert bernoulli(7) == 0 + assert bernoulli(8) == Rational(-1, 30) + assert bernoulli(10) == Rational(5, 66) + assert bernoulli(1000001) == 0 + + assert bernoulli(0, x) == 1 + assert bernoulli(1, x) == x - S.Half + assert bernoulli(2, x) == x**2 - x + Rational(1, 6) + assert bernoulli(3, x) == x**3 - (3*x**2)/2 + x/2 + + # Should be fast; computed with mpmath + b = bernoulli(1000) + assert b.p % 10**10 == 7950421099 + assert b.q == 342999030 + + b = bernoulli(10**6, evaluate=False).evalf() + assert str(b) == '-2.23799235765713e+4767529' + + # Issue #8527 + l = Symbol('l', integer=True) + m = Symbol('m', integer=True, nonnegative=True) + n = Symbol('n', integer=True, positive=True) + assert isinstance(bernoulli(2 * l + 1), bernoulli) + assert isinstance(bernoulli(2 * m + 1), bernoulli) + assert bernoulli(2 * n + 1) == 0 + + assert bernoulli(x, 1) == bernoulli(x) + + assert str(bernoulli(0.0, 2.3).evalf(n=10)) == '1.000000000' + assert str(bernoulli(1.0).evalf(n=10)) == '0.5000000000' + assert str(bernoulli(1.2).evalf(n=10)) == '0.4195995367' + assert str(bernoulli(1.2, 0.8).evalf(n=10)) == '0.2144830348' + assert str(bernoulli(1.2, -0.8).evalf(n=10)) == '-1.158865646 - 0.6745558744*I' + assert str(bernoulli(3.0, 1j).evalf(n=10)) == '1.5 - 0.5*I' + assert str(bernoulli(I).evalf(n=10)) == '0.9268485643 - 0.5821580598*I' + assert str(bernoulli(I, I).evalf(n=10)) == '0.1267792071 + 0.01947413152*I' + assert bernoulli(x).evalf() == bernoulli(x) + + +def test_bernoulli_rewrite(): + from sympy.functions.elementary.piecewise import Piecewise + n = Symbol('n', integer=True, nonnegative=True) + + assert bernoulli(-1).rewrite(zeta) == pi**2/6 + assert bernoulli(-2).rewrite(zeta) == 2*zeta(3) + assert not bernoulli(n, -3).rewrite(zeta).has(harmonic) + assert bernoulli(-4, x).rewrite(zeta) == 4*zeta(5, x) + assert isinstance(bernoulli(n, x).rewrite(zeta), Piecewise) + assert bernoulli(n+1, x).rewrite(zeta) == -(n+1) * zeta(-n, x) + + +def test_fibonacci(): + assert [fibonacci(n) for n in range(-3, 5)] == [2, -1, 1, 0, 1, 1, 2, 3] + assert fibonacci(100) == 354224848179261915075 + assert [lucas(n) for n in range(-3, 5)] == [-4, 3, -1, 2, 1, 3, 4, 7] + assert lucas(100) == 792070839848372253127 + + assert fibonacci(1, x) == 1 + assert fibonacci(2, x) == x + assert fibonacci(3, x) == x**2 + 1 + assert fibonacci(4, x) == x**3 + 2*x + + # issue #8800 + n = Dummy('n') + assert fibonacci(n).limit(n, S.Infinity) is S.Infinity + assert lucas(n).limit(n, S.Infinity) is S.Infinity + + assert fibonacci(n).rewrite(sqrt) == \ + 2**(-n)*sqrt(5)*((1 + sqrt(5))**n - (-sqrt(5) + 1)**n) / 5 + assert fibonacci(n).rewrite(sqrt).subs(n, 10).expand() == fibonacci(10) + assert fibonacci(n).rewrite(GoldenRatio).subs(n,10).evalf() == \ + Float(fibonacci(10)) + assert lucas(n).rewrite(sqrt) == \ + (fibonacci(n-1).rewrite(sqrt) + fibonacci(n+1).rewrite(sqrt)).simplify() + assert lucas(n).rewrite(sqrt).subs(n, 10).expand() == lucas(10) + raises(ValueError, lambda: fibonacci(-3, x)) + + +def test_tribonacci(): + assert [tribonacci(n) for n in range(8)] == [0, 1, 1, 2, 4, 7, 13, 24] + assert tribonacci(100) == 98079530178586034536500564 + + assert tribonacci(0, x) == 0 + assert tribonacci(1, x) == 1 + assert tribonacci(2, x) == x**2 + assert tribonacci(3, x) == x**4 + x + assert tribonacci(4, x) == x**6 + 2*x**3 + 1 + assert tribonacci(5, x) == x**8 + 3*x**5 + 3*x**2 + + n = Dummy('n') + assert tribonacci(n).limit(n, S.Infinity) is S.Infinity + + w = (-1 + S.ImaginaryUnit * sqrt(3)) / 2 + a = (1 + cbrt(19 + 3*sqrt(33)) + cbrt(19 - 3*sqrt(33))) / 3 + b = (1 + w*cbrt(19 + 3*sqrt(33)) + w**2*cbrt(19 - 3*sqrt(33))) / 3 + c = (1 + w**2*cbrt(19 + 3*sqrt(33)) + w*cbrt(19 - 3*sqrt(33))) / 3 + assert tribonacci(n).rewrite(sqrt) == \ + (a**(n + 1)/((a - b)*(a - c)) + + b**(n + 1)/((b - a)*(b - c)) + + c**(n + 1)/((c - a)*(c - b))) + assert tribonacci(n).rewrite(sqrt).subs(n, 4).simplify() == tribonacci(4) + assert tribonacci(n).rewrite(GoldenRatio).subs(n,10).evalf() == \ + Float(tribonacci(10)) + assert tribonacci(n).rewrite(TribonacciConstant) == floor( + 3*TribonacciConstant**n*(102*sqrt(33) + 586)**Rational(1, 3)/ + (-2*(102*sqrt(33) + 586)**Rational(1, 3) + 4 + (102*sqrt(33) + + 586)**Rational(2, 3)) + S.Half) + raises(ValueError, lambda: tribonacci(-1, x)) + + +@nocache_fail +def test_bell(): + assert [bell(n) for n in range(8)] == [1, 1, 2, 5, 15, 52, 203, 877] + + assert bell(0, x) == 1 + assert bell(1, x) == x + assert bell(2, x) == x**2 + x + assert bell(5, x) == x**5 + 10*x**4 + 25*x**3 + 15*x**2 + x + assert bell(oo) is S.Infinity + raises(ValueError, lambda: bell(oo, x)) + + raises(ValueError, lambda: bell(-1)) + raises(ValueError, lambda: bell(S.Half)) + + X = symbols('x:6') + # X = (x0, x1, .. x5) + # at the same time: X[1] = x1, X[2] = x2 for standard readablity. + # but we must supply zero-based indexed object X[1:] = (x1, .. x5) + + assert bell(6, 2, X[1:]) == 6*X[5]*X[1] + 15*X[4]*X[2] + 10*X[3]**2 + assert bell( + 6, 3, X[1:]) == 15*X[4]*X[1]**2 + 60*X[3]*X[2]*X[1] + 15*X[2]**3 + + X = (1, 10, 100, 1000, 10000) + assert bell(6, 2, X) == (6 + 15 + 10)*10000 + + X = (1, 2, 3, 3, 5) + assert bell(6, 2, X) == 6*5 + 15*3*2 + 10*3**2 + + X = (1, 2, 3, 5) + assert bell(6, 3, X) == 15*5 + 60*3*2 + 15*2**3 + + # Dobinski's formula + n = Symbol('n', integer=True, nonnegative=True) + # For large numbers, this is too slow + # For nonintegers, there are significant precision errors + for i in [0, 2, 3, 7, 13, 42, 55]: + # Running without the cache this is either very slow or goes into an + # infinite loop. + assert bell(i).evalf() == bell(n).rewrite(Sum).evalf(subs={n: i}) + + m = Symbol("m") + assert bell(m).rewrite(Sum) == bell(m) + assert bell(n, m).rewrite(Sum) == bell(n, m) + # issue 9184 + n = Dummy('n') + assert bell(n).limit(n, S.Infinity) is S.Infinity + + +def test_harmonic(): + n = Symbol("n") + m = Symbol("m") + + assert harmonic(n, 0) == n + assert harmonic(n).evalf() == harmonic(n) + assert harmonic(n, 1) == harmonic(n) + assert harmonic(1, n) == 1 + + assert harmonic(0, 1) == 0 + assert harmonic(1, 1) == 1 + assert harmonic(2, 1) == Rational(3, 2) + assert harmonic(3, 1) == Rational(11, 6) + assert harmonic(4, 1) == Rational(25, 12) + assert harmonic(0, 2) == 0 + assert harmonic(1, 2) == 1 + assert harmonic(2, 2) == Rational(5, 4) + assert harmonic(3, 2) == Rational(49, 36) + assert harmonic(4, 2) == Rational(205, 144) + assert harmonic(0, 3) == 0 + assert harmonic(1, 3) == 1 + assert harmonic(2, 3) == Rational(9, 8) + assert harmonic(3, 3) == Rational(251, 216) + assert harmonic(4, 3) == Rational(2035, 1728) + + assert harmonic(oo, -1) is S.NaN + assert harmonic(oo, 0) is oo + assert harmonic(oo, S.Half) is oo + assert harmonic(oo, 1) is oo + assert harmonic(oo, 2) == (pi**2)/6 + assert harmonic(oo, 3) == zeta(3) + assert harmonic(oo, Dummy(negative=True)) is S.NaN + ip = Dummy(integer=True, positive=True) + if (1/ip <= 1) is True: #---------------------------------+ + assert None, 'delete this if-block and the next line' #| + ip = Dummy(even=True, positive=True) #--------------------+ + assert harmonic(oo, 1/ip) is oo + assert harmonic(oo, 1 + ip) is zeta(1 + ip) + + assert harmonic(0, m) == 0 + assert harmonic(-1, -1) == 0 + assert harmonic(-1, 0) == -1 + assert harmonic(-1, 1) is S.ComplexInfinity + assert harmonic(-1, 2) is S.NaN + assert harmonic(-3, -2) == -5 + assert harmonic(-3, -3) == 9 + + +def test_harmonic_rational(): + ne = S(6) + no = S(5) + pe = S(8) + po = S(9) + qe = S(10) + qo = S(13) + + Heee = harmonic(ne + pe/qe) + Aeee = (-log(10) + 2*(Rational(-1, 4) + sqrt(5)/4)*log(sqrt(-sqrt(5)/8 + Rational(5, 8))) + + 2*(-sqrt(5)/4 - Rational(1, 4))*log(sqrt(sqrt(5)/8 + Rational(5, 8))) + + pi*sqrt(2*sqrt(5)/5 + 1)/2 + Rational(13944145, 4720968)) + + Heeo = harmonic(ne + pe/qo) + Aeeo = (-log(26) + 2*log(sin(pi*Rational(3, 13)))*cos(pi*Rational(4, 13)) + 2*log(sin(pi*Rational(2, 13)))*cos(pi*Rational(32, 13)) + + 2*log(sin(pi*Rational(5, 13)))*cos(pi*Rational(80, 13)) - 2*log(sin(pi*Rational(6, 13)))*cos(pi*Rational(5, 13)) + - 2*log(sin(pi*Rational(4, 13)))*cos(pi/13) + pi*cot(pi*Rational(5, 13))/2 - 2*log(sin(pi/13))*cos(pi*Rational(3, 13)) + + Rational(2422020029, 702257080)) + + Heoe = harmonic(ne + po/qe) + Aeoe = (-log(20) + 2*(Rational(1, 4) + sqrt(5)/4)*log(Rational(-1, 4) + sqrt(5)/4) + + 2*(Rational(-1, 4) + sqrt(5)/4)*log(sqrt(-sqrt(5)/8 + Rational(5, 8))) + + 2*(-sqrt(5)/4 - Rational(1, 4))*log(sqrt(sqrt(5)/8 + Rational(5, 8))) + + 2*(-sqrt(5)/4 + Rational(1, 4))*log(Rational(1, 4) + sqrt(5)/4) + + Rational(11818877030, 4286604231) + pi*sqrt(2*sqrt(5) + 5)/2) + + Heoo = harmonic(ne + po/qo) + Aeoo = (-log(26) + 2*log(sin(pi*Rational(3, 13)))*cos(pi*Rational(54, 13)) + 2*log(sin(pi*Rational(4, 13)))*cos(pi*Rational(6, 13)) + + 2*log(sin(pi*Rational(6, 13)))*cos(pi*Rational(108, 13)) - 2*log(sin(pi*Rational(5, 13)))*cos(pi/13) + - 2*log(sin(pi/13))*cos(pi*Rational(5, 13)) + pi*cot(pi*Rational(4, 13))/2 + - 2*log(sin(pi*Rational(2, 13)))*cos(pi*Rational(3, 13)) + Rational(11669332571, 3628714320)) + + Hoee = harmonic(no + pe/qe) + Aoee = (-log(10) + 2*(Rational(-1, 4) + sqrt(5)/4)*log(sqrt(-sqrt(5)/8 + Rational(5, 8))) + + 2*(-sqrt(5)/4 - Rational(1, 4))*log(sqrt(sqrt(5)/8 + Rational(5, 8))) + + pi*sqrt(2*sqrt(5)/5 + 1)/2 + Rational(779405, 277704)) + + Hoeo = harmonic(no + pe/qo) + Aoeo = (-log(26) + 2*log(sin(pi*Rational(3, 13)))*cos(pi*Rational(4, 13)) + 2*log(sin(pi*Rational(2, 13)))*cos(pi*Rational(32, 13)) + + 2*log(sin(pi*Rational(5, 13)))*cos(pi*Rational(80, 13)) - 2*log(sin(pi*Rational(6, 13)))*cos(pi*Rational(5, 13)) + - 2*log(sin(pi*Rational(4, 13)))*cos(pi/13) + pi*cot(pi*Rational(5, 13))/2 + - 2*log(sin(pi/13))*cos(pi*Rational(3, 13)) + Rational(53857323, 16331560)) + + Hooe = harmonic(no + po/qe) + Aooe = (-log(20) + 2*(Rational(1, 4) + sqrt(5)/4)*log(Rational(-1, 4) + sqrt(5)/4) + + 2*(Rational(-1, 4) + sqrt(5)/4)*log(sqrt(-sqrt(5)/8 + Rational(5, 8))) + + 2*(-sqrt(5)/4 - Rational(1, 4))*log(sqrt(sqrt(5)/8 + Rational(5, 8))) + + 2*(-sqrt(5)/4 + Rational(1, 4))*log(Rational(1, 4) + sqrt(5)/4) + + Rational(486853480, 186374097) + pi*sqrt(2*sqrt(5) + 5)/2) + + Hooo = harmonic(no + po/qo) + Aooo = (-log(26) + 2*log(sin(pi*Rational(3, 13)))*cos(pi*Rational(54, 13)) + 2*log(sin(pi*Rational(4, 13)))*cos(pi*Rational(6, 13)) + + 2*log(sin(pi*Rational(6, 13)))*cos(pi*Rational(108, 13)) - 2*log(sin(pi*Rational(5, 13)))*cos(pi/13) + - 2*log(sin(pi/13))*cos(pi*Rational(5, 13)) + pi*cot(pi*Rational(4, 13))/2 + - 2*log(sin(pi*Rational(2, 13)))*cos(3*pi/13) + Rational(383693479, 125128080)) + + H = [Heee, Heeo, Heoe, Heoo, Hoee, Hoeo, Hooe, Hooo] + A = [Aeee, Aeeo, Aeoe, Aeoo, Aoee, Aoeo, Aooe, Aooo] + for h, a in zip(H, A): + e = expand_func(h).doit() + assert cancel(e/a) == 1 + assert abs(h.n() - a.n()) < 1e-12 + + +def test_harmonic_evalf(): + assert str(harmonic(1.5).evalf(n=10)) == '1.280372306' + assert str(harmonic(1.5, 2).evalf(n=10)) == '1.154576311' # issue 7443 + assert str(harmonic(4.0, -3).evalf(n=10)) == '100.0000000' + assert str(harmonic(7.0, 1.0).evalf(n=10)) == '2.592857143' + assert str(harmonic(1, pi).evalf(n=10)) == '1.000000000' + assert str(harmonic(2, pi).evalf(n=10)) == '1.113314732' + assert str(harmonic(1000.0, pi).evalf(n=10)) == '1.176241563' + assert str(harmonic(I).evalf(n=10)) == '0.6718659855 + 1.076674047*I' + assert str(harmonic(I, I).evalf(n=10)) == '-0.3970915266 + 1.9629689*I' + + assert harmonic(-1.0, 1).evalf() is S.NaN + assert harmonic(-2.0, 2.0).evalf() is S.NaN + +def test_harmonic_rewrite(): + from sympy.functions.elementary.piecewise import Piecewise + n = Symbol("n") + m = Symbol("m", integer=True, positive=True) + x1 = Symbol("x1", positive=True) + x2 = Symbol("x2", negative=True) + + assert harmonic(n).rewrite(digamma) == polygamma(0, n + 1) + EulerGamma + assert harmonic(n).rewrite(trigamma) == polygamma(0, n + 1) + EulerGamma + assert harmonic(n).rewrite(polygamma) == polygamma(0, n + 1) + EulerGamma + + assert harmonic(n,3).rewrite(polygamma) == polygamma(2, n + 1)/2 - polygamma(2, 1)/2 + assert isinstance(harmonic(n,m).rewrite(polygamma), Piecewise) + + assert expand_func(harmonic(n+4)) == harmonic(n) + 1/(n + 4) + 1/(n + 3) + 1/(n + 2) + 1/(n + 1) + assert expand_func(harmonic(n-4)) == harmonic(n) - 1/(n - 1) - 1/(n - 2) - 1/(n - 3) - 1/n + + assert harmonic(n, m).rewrite("tractable") == harmonic(n, m).rewrite(polygamma) + assert harmonic(n, x1).rewrite("tractable") == harmonic(n, x1) + assert harmonic(n, x1 + 1).rewrite("tractable") == zeta(x1 + 1) - zeta(x1 + 1, n + 1) + assert harmonic(n, x2).rewrite("tractable") == zeta(x2) - zeta(x2, n + 1) + + _k = Dummy("k") + assert harmonic(n).rewrite(Sum).dummy_eq(Sum(1/_k, (_k, 1, n))) + assert harmonic(n, m).rewrite(Sum).dummy_eq(Sum(_k**(-m), (_k, 1, n))) + + +def test_harmonic_calculus(): + y = Symbol("y", positive=True) + z = Symbol("z", negative=True) + assert harmonic(x, 1).limit(x, 0) == 0 + assert harmonic(x, y).limit(x, 0) == 0 + assert harmonic(x, 1).series(x, y, 2) == \ + harmonic(y) + (x - y)*zeta(2, y + 1) + O((x - y)**2, (x, y)) + assert limit(harmonic(x, y), x, oo) == harmonic(oo, y) + assert limit(harmonic(x, y + 1), x, oo) == zeta(y + 1) + assert limit(harmonic(x, y - 1), x, oo) == harmonic(oo, y - 1) + assert limit(harmonic(x, z), x, oo) == Limit(harmonic(x, z), x, oo, dir='-') + assert limit(harmonic(x, z + 1), x, oo) == oo + assert limit(harmonic(x, z + 2), x, oo) == harmonic(oo, z + 2) + assert limit(harmonic(x, z - 1), x, oo) == Limit(harmonic(x, z - 1), x, oo, dir='-') + + +def test_euler(): + assert euler(0) == 1 + assert euler(1) == 0 + assert euler(2) == -1 + assert euler(3) == 0 + assert euler(4) == 5 + assert euler(6) == -61 + assert euler(8) == 1385 + + assert euler(20, evaluate=False) != 370371188237525 + + n = Symbol('n', integer=True) + assert euler(n) != -1 + assert euler(n).subs(n, 2) == -1 + + assert euler(-1) == S.Pi / 2 + assert euler(-1, 1) == 2*log(2) + assert euler(-2).evalf() == (2*S.Catalan).evalf() + assert euler(-3).evalf() == (S.Pi**3 / 16).evalf() + assert str(euler(2.3).evalf(n=10)) == '-1.052850274' + assert str(euler(1.2, 3.4).evalf(n=10)) == '3.575613489' + assert str(euler(I).evalf(n=10)) == '1.248446443 - 0.7675445124*I' + assert str(euler(I, I).evalf(n=10)) == '0.04812930469 + 0.01052411008*I' + + assert euler(20).evalf() == 370371188237525.0 + assert euler(20, evaluate=False).evalf() == 370371188237525.0 + + assert euler(n).rewrite(Sum) == euler(n) + n = Symbol('n', integer=True, nonnegative=True) + assert euler(2*n + 1).rewrite(Sum) == 0 + _j = Dummy('j') + _k = Dummy('k') + assert euler(2*n).rewrite(Sum).dummy_eq( + I*Sum((-1)**_j*2**(-_k)*I**(-_k)*(-2*_j + _k)**(2*n + 1)* + binomial(_k, _j)/_k, (_j, 0, _k), (_k, 1, 2*n + 1))) + + +def test_euler_odd(): + n = Symbol('n', odd=True, positive=True) + assert euler(n) == 0 + n = Symbol('n', odd=True) + assert euler(n) != 0 + + +def test_euler_polynomials(): + assert euler(0, x) == 1 + assert euler(1, x) == x - S.Half + assert euler(2, x) == x**2 - x + assert euler(3, x) == x**3 - (3*x**2)/2 + Rational(1, 4) + m = Symbol('m') + assert isinstance(euler(m, x), euler) + from sympy.core.numbers import Float + A = Float('-0.46237208575048694923364757452876131e8') # from Maple + B = euler(19, S.Pi).evalf(32) + assert abs((A - B)/A) < 1e-31 + z = Float(0.1) + Float(0.2)*I + expected = Float(-3126.54721663773 ) + Float(565.736261497056) * I + assert abs(euler(13, z) - expected) < 1e-10 + + +def test_euler_polynomial_rewrite(): + m = Symbol('m') + A = euler(m, x).rewrite('Sum') + assert A.subs({m:3, x:5}).doit() == euler(3, 5) + + +def test_catalan(): + n = Symbol('n', integer=True) + m = Symbol('m', integer=True, positive=True) + k = Symbol('k', integer=True, nonnegative=True) + p = Symbol('p', nonnegative=True) + + catalans = [1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786] + for i, c in enumerate(catalans): + assert catalan(i) == c + assert catalan(n).rewrite(factorial).subs(n, i) == c + assert catalan(n).rewrite(Product).subs(n, i).doit() == c + + assert unchanged(catalan, x) + assert catalan(2*x).rewrite(binomial) == binomial(4*x, 2*x)/(2*x + 1) + assert catalan(S.Half).rewrite(gamma) == 8/(3*pi) + assert catalan(S.Half).rewrite(factorial).rewrite(gamma) ==\ + 8 / (3 * pi) + assert catalan(3*x).rewrite(gamma) == 4**( + 3*x)*gamma(3*x + S.Half)/(sqrt(pi)*gamma(3*x + 2)) + assert catalan(x).rewrite(hyper) == hyper((-x + 1, -x), (2,), 1) + + assert catalan(n).rewrite(factorial) == factorial(2*n) / (factorial(n + 1) + * factorial(n)) + assert isinstance(catalan(n).rewrite(Product), catalan) + assert isinstance(catalan(m).rewrite(Product), Product) + + assert diff(catalan(x), x) == (polygamma( + 0, x + S.Half) - polygamma(0, x + 2) + log(4))*catalan(x) + + assert catalan(x).evalf() == catalan(x) + c = catalan(S.Half).evalf() + assert str(c) == '0.848826363156775' + c = catalan(I).evalf(3) + assert str((re(c), im(c))) == '(0.398, -0.0209)' + + # Assumptions + assert catalan(p).is_positive is True + assert catalan(k).is_integer is True + assert catalan(m+3).is_composite is True + + +def test_genocchi(): + genocchis = [0, -1, -1, 0, 1, 0, -3, 0, 17] + for n, g in enumerate(genocchis): + assert genocchi(n) == g + + m = Symbol('m', integer=True) + n = Symbol('n', integer=True, positive=True) + assert unchanged(genocchi, m) + assert genocchi(2*n + 1) == 0 + gn = 2 * (1 - 2**n) * bernoulli(n) + assert genocchi(n).rewrite(bernoulli).factor() == gn.factor() + gnx = 2 * (bernoulli(n, x) - 2**n * bernoulli(n, (x+1) / 2)) + assert genocchi(n, x).rewrite(bernoulli).factor() == gnx.factor() + assert genocchi(2 * n).is_odd + assert genocchi(2 * n).is_even is False + assert genocchi(2 * n + 1).is_even + assert genocchi(n).is_integer + assert genocchi(4 * n).is_positive + # these are the only 2 prime Genocchi numbers + assert genocchi(6, evaluate=False).is_prime == S(-3).is_prime + assert genocchi(8, evaluate=False).is_prime + assert genocchi(4 * n + 2).is_negative + assert genocchi(4 * n + 1).is_negative is False + assert genocchi(4 * n - 2).is_negative + + g0 = genocchi(0, evaluate=False) + assert g0.is_positive is False + assert g0.is_negative is False + assert g0.is_even is True + assert g0.is_odd is False + + assert genocchi(0, x) == 0 + assert genocchi(1, x) == -1 + assert genocchi(2, x) == 1 - 2*x + assert genocchi(3, x) == 3*x - 3*x**2 + assert genocchi(4, x) == -1 + 6*x**2 - 4*x**3 + y = Symbol("y") + assert genocchi(5, (x+y)**100) == -5*(x+y)**400 + 10*(x+y)**300 - 5*(x+y)**100 + + assert str(genocchi(5.0, 4.0).evalf(n=10)) == '-660.0000000' + assert str(genocchi(Rational(5, 4)).evalf(n=10)) == '-1.104286457' + assert str(genocchi(-2).evalf(n=10)) == '3.606170709' + assert str(genocchi(1.3, 3.7).evalf(n=10)) == '-1.847375373' + assert str(genocchi(I, 1.0).evalf(n=10)) == '-0.3161917278 - 1.45311955*I' + + n = Symbol('n') + assert genocchi(n, x).rewrite(dirichlet_eta) == -2*n * dirichlet_eta(1-n, x) + + +def test_andre(): + nums = [1, 1, 1, 2, 5, 16, 61, 272, 1385, 7936, 50521] + for n, a in enumerate(nums): + assert andre(n) == a + assert andre(S.Infinity) == S.Infinity + assert andre(-1) == -log(2) + assert andre(-2) == -2*S.Catalan + assert andre(-3) == 3*zeta(3)/16 + assert andre(-5) == -15*zeta(5)/256 + # In fact andre(-2*n) is related to the Dirichlet *beta* function + # at 2*n, but SymPy doesn't implement that (or general L-functions) + assert unchanged(andre, -4) + + n = Symbol('n', integer=True, nonnegative=True) + assert unchanged(andre, n) + assert andre(n).is_integer is True + assert andre(n).is_positive is True + + assert str(andre(10, evaluate=False).evalf(n=10)) == '50521.00000' + assert str(andre(-1, evaluate=False).evalf(n=10)) == '-0.6931471806' + assert str(andre(-2, evaluate=False).evalf(n=10)) == '-1.831931188' + assert str(andre(-4, evaluate=False).evalf(n=10)) == '1.977889103' + assert str(andre(I, evaluate=False).evalf(n=10)) == '2.378417833 + 0.6343322845*I' + + assert andre(x).rewrite(polylog) == \ + (-I)**(x+1) * polylog(-x, I) + I**(x+1) * polylog(-x, -I) + assert andre(x).rewrite(zeta) == \ + 2 * gamma(x+1) / (2*pi)**(x+1) * \ + (zeta(x+1, Rational(1,4)) - cos(pi*x) * zeta(x+1, Rational(3,4))) + + +@nocache_fail +def test_partition(): + partition_nums = [1, 1, 2, 3, 5, 7, 11, 15, 22] + for n, p in enumerate(partition_nums): + assert partition(n) == p + + x = Symbol('x') + y = Symbol('y', real=True) + m = Symbol('m', integer=True) + n = Symbol('n', integer=True, negative=True) + p = Symbol('p', integer=True, nonnegative=True) + assert partition(m).is_integer + assert not partition(m).is_negative + assert partition(m).is_nonnegative + assert partition(n).is_zero + assert partition(p).is_positive + assert partition(x).subs(x, 7) == 15 + assert partition(y).subs(y, 8) == 22 + raises(TypeError, lambda: partition(Rational(5, 4))) + assert partition(9, evaluate=False) % 5 == 0 + assert partition(5*m + 4) % 5 == 0 + assert partition(47, evaluate=False) % 7 == 0 + assert partition(7*m + 5) % 7 == 0 + assert partition(50, evaluate=False) % 11 == 0 + assert partition(11*m + 6) % 11 == 0 + + +def test_divisor_sigma(): + # error + m = Symbol('m', integer=False) + raises(TypeError, lambda: divisor_sigma(m)) + raises(TypeError, lambda: divisor_sigma(4.5)) + raises(TypeError, lambda: divisor_sigma(1, m)) + raises(TypeError, lambda: divisor_sigma(1, 4.5)) + m = Symbol('m', positive=False) + raises(ValueError, lambda: divisor_sigma(m)) + raises(ValueError, lambda: divisor_sigma(0)) + m = Symbol('m', negative=True) + raises(ValueError, lambda: divisor_sigma(1, m)) + raises(ValueError, lambda: divisor_sigma(1, -1)) + + # special case + p = Symbol('p', prime=True) + k = Symbol('k', integer=True) + assert divisor_sigma(p, 1) == p + 1 + assert divisor_sigma(p, k) == p**k + 1 + + # property + n = Symbol('n', integer=True, positive=True) + assert divisor_sigma(n).is_integer is True + assert divisor_sigma(n).is_positive is True + + # symbolic + k = Symbol('k', integer=True, zero=False) + assert divisor_sigma(4, k) == 2**(2*k) + 2**k + 1 + assert divisor_sigma(6, k) == (2**k + 1) * (3**k + 1) + + # Integer + assert divisor_sigma(23450) == 50592 + assert divisor_sigma(23450, 0) == 24 + assert divisor_sigma(23450, 1) == 50592 + assert divisor_sigma(23450, 2) == 730747500 + assert divisor_sigma(23450, 3) == 14666785333344 + + +def test_udivisor_sigma(): + # error + m = Symbol('m', integer=False) + raises(TypeError, lambda: udivisor_sigma(m)) + raises(TypeError, lambda: udivisor_sigma(4.5)) + raises(TypeError, lambda: udivisor_sigma(1, m)) + raises(TypeError, lambda: udivisor_sigma(1, 4.5)) + m = Symbol('m', positive=False) + raises(ValueError, lambda: udivisor_sigma(m)) + raises(ValueError, lambda: udivisor_sigma(0)) + m = Symbol('m', negative=True) + raises(ValueError, lambda: udivisor_sigma(1, m)) + raises(ValueError, lambda: udivisor_sigma(1, -1)) + + # special case + p = Symbol('p', prime=True) + k = Symbol('k', integer=True) + assert udivisor_sigma(p, 1) == p + 1 + assert udivisor_sigma(p, k) == p**k + 1 + + # property + n = Symbol('n', integer=True, positive=True) + assert udivisor_sigma(n).is_integer is True + assert udivisor_sigma(n).is_positive is True + + # Integer + A034444 = [1, 2, 2, 2, 2, 4, 2, 2, 2, 4, 2, 4, 2, 4, 4, 2, 2, 4, 2, 4, + 4, 4, 2, 4, 2, 4, 2, 4, 2, 8, 2, 2, 4, 4, 4, 4, 2, 4, 4, 4, + 2, 8, 2, 4, 4, 4, 2, 4, 2, 4, 4, 4, 2, 4, 4, 4, 4, 4, 2, 8] + for n, val in enumerate(A034444, 1): + assert udivisor_sigma(n, 0) == val + A034448 = [1, 3, 4, 5, 6, 12, 8, 9, 10, 18, 12, 20, 14, 24, 24, 17, 18, + 30, 20, 30, 32, 36, 24, 36, 26, 42, 28, 40, 30, 72, 32, 33, + 48, 54, 48, 50, 38, 60, 56, 54, 42, 96, 44, 60, 60, 72, 48] + for n, val in enumerate(A034448, 1): + assert udivisor_sigma(n, 1) == val + A034676 = [1, 5, 10, 17, 26, 50, 50, 65, 82, 130, 122, 170, 170, 250, + 260, 257, 290, 410, 362, 442, 500, 610, 530, 650, 626, 850, + 730, 850, 842, 1300, 962, 1025, 1220, 1450, 1300, 1394, 1370] + for n, val in enumerate(A034676, 1): + assert udivisor_sigma(n, 2) == val + + +def test_legendre_symbol(): + # error + m = Symbol('m', integer=False) + raises(TypeError, lambda: legendre_symbol(m, 3)) + raises(TypeError, lambda: legendre_symbol(4.5, 3)) + raises(TypeError, lambda: legendre_symbol(1, m)) + raises(TypeError, lambda: legendre_symbol(1, 4.5)) + m = Symbol('m', prime=False) + raises(ValueError, lambda: legendre_symbol(1, m)) + raises(ValueError, lambda: legendre_symbol(1, 6)) + m = Symbol('m', odd=False) + raises(ValueError, lambda: legendre_symbol(1, m)) + raises(ValueError, lambda: legendre_symbol(1, 2)) + + # special case + p = Symbol('p', prime=True) + k = Symbol('k', integer=True) + assert legendre_symbol(p*k, p) == 0 + assert legendre_symbol(1, p) == 1 + + # property + n = Symbol('n') + m = Symbol('m') + assert legendre_symbol(m, n).is_integer is True + assert legendre_symbol(m, n).is_prime is False + + # Integer + assert legendre_symbol(5, 11) == 1 + assert legendre_symbol(25, 41) == 1 + assert legendre_symbol(67, 101) == -1 + assert legendre_symbol(0, 13) == 0 + assert legendre_symbol(9, 3) == 0 + + +def test_jacobi_symbol(): + # error + m = Symbol('m', integer=False) + raises(TypeError, lambda: jacobi_symbol(m, 3)) + raises(TypeError, lambda: jacobi_symbol(4.5, 3)) + raises(TypeError, lambda: jacobi_symbol(1, m)) + raises(TypeError, lambda: jacobi_symbol(1, 4.5)) + m = Symbol('m', positive=False) + raises(ValueError, lambda: jacobi_symbol(1, m)) + raises(ValueError, lambda: jacobi_symbol(1, -6)) + m = Symbol('m', odd=False) + raises(ValueError, lambda: jacobi_symbol(1, m)) + raises(ValueError, lambda: jacobi_symbol(1, 2)) + + # special case + p = Symbol('p', integer=True) + k = Symbol('k', integer=True) + assert jacobi_symbol(p*k, p) == 0 + assert jacobi_symbol(1, p) == 1 + assert jacobi_symbol(1, 1) == 1 + assert jacobi_symbol(0, 1) == 1 + + # property + n = Symbol('n') + m = Symbol('m') + assert jacobi_symbol(m, n).is_integer is True + assert jacobi_symbol(m, n).is_prime is False + + # Integer + assert jacobi_symbol(25, 41) == 1 + assert jacobi_symbol(-23, 83) == -1 + assert jacobi_symbol(3, 9) == 0 + assert jacobi_symbol(42, 97) == -1 + assert jacobi_symbol(3, 5) == -1 + assert jacobi_symbol(7, 9) == 1 + assert jacobi_symbol(0, 3) == 0 + assert jacobi_symbol(0, 1) == 1 + assert jacobi_symbol(2, 1) == 1 + assert jacobi_symbol(1, 3) == 1 + + +def test_kronecker_symbol(): + # error + m = Symbol('m', integer=False) + raises(TypeError, lambda: kronecker_symbol(m, 3)) + raises(TypeError, lambda: kronecker_symbol(4.5, 3)) + raises(TypeError, lambda: kronecker_symbol(1, m)) + raises(TypeError, lambda: kronecker_symbol(1, 4.5)) + + # special case + p = Symbol('p', integer=True) + assert kronecker_symbol(1, p) == 1 + assert kronecker_symbol(1, 1) == 1 + assert kronecker_symbol(0, 1) == 1 + + # property + n = Symbol('n') + m = Symbol('m') + assert kronecker_symbol(m, n).is_integer is True + assert kronecker_symbol(m, n).is_prime is False + + # Integer + for n in range(3, 10, 2): + for a in range(-n, n): + val = kronecker_symbol(a, n) + assert val == jacobi_symbol(a, n) + minus = kronecker_symbol(a, -n) + if a < 0: + assert -minus == val + else: + assert minus == val + even = kronecker_symbol(a, 2 * n) + if a % 2 == 0: + assert even == 0 + elif a % 8 in [1, 7]: + assert even == val + else: + assert -even == val + assert kronecker_symbol(1, 0) == kronecker_symbol(-1, 0) == 1 + assert kronecker_symbol(0, 0) == 0 + + +def test_mobius(): + # error + m = Symbol('m', integer=False) + raises(TypeError, lambda: mobius(m)) + raises(TypeError, lambda: mobius(4.5)) + m = Symbol('m', positive=False) + raises(ValueError, lambda: mobius(m)) + raises(ValueError, lambda: mobius(-3)) + + # special case + p = Symbol('p', prime=True) + assert mobius(p) == -1 + + # property + n = Symbol('n', integer=True, positive=True) + assert mobius(n).is_integer is True + assert mobius(n).is_prime is False + + # symbolic + n = Symbol('n', integer=True, positive=True) + k = Symbol('k', integer=True, positive=True) + assert mobius(n**2) == 0 + assert mobius(4*n) == 0 + assert isinstance(mobius(n**k), mobius) + assert mobius(n**(k+1)) == 0 + assert isinstance(mobius(3**k), mobius) + assert mobius(3**(k+1)) == 0 + m = Symbol('m') + assert isinstance(mobius(4*m), mobius) + + # Integer + assert mobius(13*7) == 1 + assert mobius(1) == 1 + assert mobius(13*7*5) == -1 + assert mobius(13**2) == 0 + A008683 = [1, -1, -1, 0, -1, 1, -1, 0, 0, 1, -1, 0, -1, 1, 1, 0, -1, 0, + -1, 0, 1, 1, -1, 0, 0, 1, 0, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, + 1, 1, 0, -1, -1, -1, 0, 0, 1, -1, 0, 0, 0, 1, 0, -1, 0, 1, 0] + for n, val in enumerate(A008683, 1): + assert mobius(n) == val + + +def test_primenu(): + # error + m = Symbol('m', integer=False) + raises(TypeError, lambda: primenu(m)) + raises(TypeError, lambda: primenu(4.5)) + m = Symbol('m', positive=False) + raises(ValueError, lambda: primenu(m)) + raises(ValueError, lambda: primenu(0)) + + # special case + p = Symbol('p', prime=True) + assert primenu(p) == 1 + + # property + n = Symbol('n', integer=True, positive=True) + assert primenu(n).is_integer is True + assert primenu(n).is_nonnegative is True + + # Integer + assert primenu(7*13) == 2 + assert primenu(2*17*19) == 3 + assert primenu(2**3 * 17 * 19**2) == 3 + A001221 = [0, 1, 1, 1, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 2, 1, 1, 2, + 1, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 1, 2, 2, 2, 2] + for n, val in enumerate(A001221, 1): + assert primenu(n) == val + + +def test_primeomega(): + # error + m = Symbol('m', integer=False) + raises(TypeError, lambda: primeomega(m)) + raises(TypeError, lambda: primeomega(4.5)) + m = Symbol('m', positive=False) + raises(ValueError, lambda: primeomega(m)) + raises(ValueError, lambda: primeomega(0)) + + # special case + p = Symbol('p', prime=True) + assert primeomega(p) == 1 + + # property + n = Symbol('n', integer=True, positive=True) + assert primeomega(n).is_integer is True + assert primeomega(n).is_nonnegative is True + + # Integer + assert primeomega(7*13) == 2 + assert primeomega(2*17*19) == 3 + assert primeomega(2**3 * 17 * 19**2) == 6 + A001222 = [0, 1, 1, 2, 1, 2, 1, 3, 2, 2, 1, 3, 1, 2, 2, 4, 1, 3, + 1, 3, 2, 2, 1, 4, 2, 2, 3, 3, 1, 3, 1, 5, 2, 2, 2, 4] + for n, val in enumerate(A001222, 1): + assert primeomega(n) == val + + +def test_totient(): + # error + m = Symbol('m', integer=False) + raises(TypeError, lambda: totient(m)) + raises(TypeError, lambda: totient(4.5)) + m = Symbol('m', positive=False) + raises(ValueError, lambda: totient(m)) + raises(ValueError, lambda: totient(0)) + + # special case + p = Symbol('p', prime=True) + assert totient(p) == p - 1 + + # property + n = Symbol('n', integer=True, positive=True) + assert totient(n).is_integer is True + assert totient(n).is_positive is True + + # Integer + assert totient(7*13) == totient(factorint(7*13)) == (7-1)*(13-1) + assert totient(2*17*19) == totient(factorint(2*17*19)) == (17-1)*(19-1) + assert totient(2**3 * 17 * 19**2) == totient({2: 3, 17: 1, 19: 2}) == 2**2 * (17-1) * 19*(19-1) + A000010 = [1, 1, 2, 2, 4, 2, 6, 4, 6, 4, 10, 4, 12, 6, 8, 8, 16, + 6, 18, 8, 12, 10, 22, 8, 20, 12, 18, 12, 28, 8, 30, 16, + 20, 16, 24, 12, 36, 18, 24, 16, 40, 12, 42, 20, 24, 22] + for n, val in enumerate(A000010, 1): + assert totient(n) == val + + +def test_reduced_totient(): + # error + m = Symbol('m', integer=False) + raises(TypeError, lambda: reduced_totient(m)) + raises(TypeError, lambda: reduced_totient(4.5)) + m = Symbol('m', positive=False) + raises(ValueError, lambda: reduced_totient(m)) + raises(ValueError, lambda: reduced_totient(0)) + + # special case + p = Symbol('p', prime=True) + assert reduced_totient(p) == p - 1 + + # property + n = Symbol('n', integer=True, positive=True) + assert reduced_totient(n).is_integer is True + assert reduced_totient(n).is_positive is True + + # Integer + assert reduced_totient(7*13) == reduced_totient(factorint(7*13)) == 12 + assert reduced_totient(2*17*19) == reduced_totient(factorint(2*17*19)) == 144 + assert reduced_totient(2**2 * 11) == reduced_totient({2: 2, 11: 1}) == 10 + assert reduced_totient(2**3 * 17 * 19**2) == reduced_totient({2: 3, 17: 1, 19: 2}) == 2736 + A002322 = [1, 1, 2, 2, 4, 2, 6, 2, 6, 4, 10, 2, 12, 6, 4, 4, 16, 6, + 18, 4, 6, 10, 22, 2, 20, 12, 18, 6, 28, 4, 30, 8, 10, 16, + 12, 6, 36, 18, 12, 4, 40, 6, 42, 10, 12, 22, 46, 4, 42] + for n, val in enumerate(A002322, 1): + assert reduced_totient(n) == val + + +def test_primepi(): + # error + z = Symbol('z', real=False) + raises(TypeError, lambda: primepi(z)) + raises(TypeError, lambda: primepi(I)) + + # property + n = Symbol('n', integer=True, positive=True) + assert primepi(n).is_integer is True + assert primepi(n).is_nonnegative is True + + # infinity + assert primepi(oo) == oo + assert primepi(-oo) == 0 + + # symbol + x = Symbol('x') + assert isinstance(primepi(x), primepi) + + # Integer + assert primepi(0) == 0 + A000720 = [0, 1, 2, 2, 3, 3, 4, 4, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 8, + 8, 8, 8, 9, 9, 9, 9, 9, 9, 10, 10, 11, 11, 11, 11, 11, 11, + 12, 12, 12, 12, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15] + for n, val in enumerate(A000720, 1): + assert primepi(n) == primepi(n + 0.5) == val + + +def test__nT(): + assert [_nT(i, j) for i in range(5) for j in range(i + 2)] == [ + 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 2, 1, 1, 0] + check = [_nT(10, i) for i in range(11)] + assert check == [0, 1, 5, 8, 9, 7, 5, 3, 2, 1, 1] + assert all(type(i) is int for i in check) + assert _nT(10, 5) == 7 + assert _nT(100, 98) == 2 + assert _nT(100, 100) == 1 + assert _nT(10, 3) == 8 + + +def test_nC_nP_nT(): + from sympy.utilities.iterables import ( + multiset_permutations, multiset_combinations, multiset_partitions, + partitions, subsets, permutations) + from sympy.functions.combinatorial.numbers import ( + nP, nC, nT, stirling, _stirling1, _stirling2, _multiset_histogram, _AOP_product) + + from sympy.combinatorics.permutations import Permutation + from sympy.core.random import choice + + c = string.ascii_lowercase + for i in range(100): + s = ''.join(choice(c) for i in range(7)) + u = len(s) == len(set(s)) + try: + tot = 0 + for i in range(8): + check = nP(s, i) + tot += check + assert len(list(multiset_permutations(s, i))) == check + if u: + assert nP(len(s), i) == check + assert nP(s) == tot + except AssertionError: + print(s, i, 'failed perm test') + raise ValueError() + + for i in range(100): + s = ''.join(choice(c) for i in range(7)) + u = len(s) == len(set(s)) + try: + tot = 0 + for i in range(8): + check = nC(s, i) + tot += check + assert len(list(multiset_combinations(s, i))) == check + if u: + assert nC(len(s), i) == check + assert nC(s) == tot + if u: + assert nC(len(s)) == tot + except AssertionError: + print(s, i, 'failed combo test') + raise ValueError() + + for i in range(1, 10): + tot = 0 + for j in range(1, i + 2): + check = nT(i, j) + assert check.is_Integer + tot += check + assert sum(1 for p in partitions(i, j, size=True) if p[0] == j) == check + assert nT(i) == tot + + for i in range(1, 10): + tot = 0 + for j in range(1, i + 2): + check = nT(range(i), j) + tot += check + assert len(list(multiset_partitions(list(range(i)), j))) == check + assert nT(range(i)) == tot + + for i in range(100): + s = ''.join(choice(c) for i in range(7)) + u = len(s) == len(set(s)) + try: + tot = 0 + for i in range(1, 8): + check = nT(s, i) + tot += check + assert len(list(multiset_partitions(s, i))) == check + if u: + assert nT(range(len(s)), i) == check + if u: + assert nT(range(len(s))) == tot + assert nT(s) == tot + except AssertionError: + print(s, i, 'failed partition test') + raise ValueError() + + # tests for Stirling numbers of the first kind that are not tested in the + # above + assert [stirling(9, i, kind=1) for i in range(11)] == [ + 0, 40320, 109584, 118124, 67284, 22449, 4536, 546, 36, 1, 0] + perms = list(permutations(range(4))) + assert [sum(1 for p in perms if Permutation(p).cycles == i) + for i in range(5)] == [0, 6, 11, 6, 1] == [ + stirling(4, i, kind=1) for i in range(5)] + # http://oeis.org/A008275 + assert [stirling(n, k, signed=1) + for n in range(10) for k in range(1, n + 1)] == [ + 1, -1, + 1, 2, -3, + 1, -6, 11, -6, + 1, 24, -50, 35, -10, + 1, -120, 274, -225, 85, -15, + 1, 720, -1764, 1624, -735, 175, -21, + 1, -5040, 13068, -13132, 6769, -1960, 322, -28, + 1, 40320, -109584, 118124, -67284, 22449, -4536, 546, -36, 1] + # https://en.wikipedia.org/wiki/Stirling_numbers_of_the_first_kind + assert [stirling(n, k, kind=1) + for n in range(10) for k in range(n+1)] == [ + 1, + 0, 1, + 0, 1, 1, + 0, 2, 3, 1, + 0, 6, 11, 6, 1, + 0, 24, 50, 35, 10, 1, + 0, 120, 274, 225, 85, 15, 1, + 0, 720, 1764, 1624, 735, 175, 21, 1, + 0, 5040, 13068, 13132, 6769, 1960, 322, 28, 1, + 0, 40320, 109584, 118124, 67284, 22449, 4536, 546, 36, 1] + # https://en.wikipedia.org/wiki/Stirling_numbers_of_the_second_kind + assert [stirling(n, k, kind=2) + for n in range(10) for k in range(n+1)] == [ + 1, + 0, 1, + 0, 1, 1, + 0, 1, 3, 1, + 0, 1, 7, 6, 1, + 0, 1, 15, 25, 10, 1, + 0, 1, 31, 90, 65, 15, 1, + 0, 1, 63, 301, 350, 140, 21, 1, + 0, 1, 127, 966, 1701, 1050, 266, 28, 1, + 0, 1, 255, 3025, 7770, 6951, 2646, 462, 36, 1] + assert stirling(3, 4, kind=1) == stirling(3, 4, kind=1) == 0 + raises(ValueError, lambda: stirling(-2, 2)) + + # Assertion that the return type is SymPy Integer. + assert isinstance(_stirling1(6, 3), Integer) + assert isinstance(_stirling2(6, 3), Integer) + + def delta(p): + if len(p) == 1: + return oo + return min(abs(i[0] - i[1]) for i in subsets(p, 2)) + parts = multiset_partitions(range(5), 3) + d = 2 + assert (sum(1 for p in parts if all(delta(i) >= d for i in p)) == + stirling(5, 3, d=d) == 7) + + # other coverage tests + assert nC('abb', 2) == nC('aab', 2) == 2 + assert nP(3, 3, replacement=True) == nP('aabc', 3, replacement=True) == 27 + assert nP(3, 4) == 0 + assert nP('aabc', 5) == 0 + assert nC(4, 2, replacement=True) == nC('abcdd', 2, replacement=True) == \ + len(list(multiset_combinations('aabbccdd', 2))) == 10 + assert nC('abcdd') == sum(nC('abcdd', i) for i in range(6)) == 24 + assert nC(list('abcdd'), 4) == 4 + assert nT('aaaa') == nT(4) == len(list(partitions(4))) == 5 + assert nT('aaab') == len(list(multiset_partitions('aaab'))) == 7 + assert nC('aabb'*3, 3) == 4 # aaa, bbb, abb, baa + assert dict(_AOP_product((4,1,1,1))) == { + 0: 1, 1: 4, 2: 7, 3: 8, 4: 8, 5: 7, 6: 4, 7: 1} + # the following was the first t that showed a problem in a previous form of + # the function, so it's not as random as it may appear + t = (3, 9, 4, 6, 6, 5, 5, 2, 10, 4) + assert sum(_AOP_product(t)[i] for i in range(55)) == 58212000 + raises(ValueError, lambda: _multiset_histogram({1:'a'})) + + +def test_PR_14617(): + from sympy.functions.combinatorial.numbers import nT + for n in (0, []): + for k in (-1, 0, 1): + if k == 0: + assert nT(n, k) == 1 + else: + assert nT(n, k) == 0 + + +def test_issue_8496(): + n = Symbol("n") + k = Symbol("k") + + raises(TypeError, lambda: catalan(n, k)) + + +def test_issue_8601(): + n = Symbol('n', integer=True, negative=True) + + assert catalan(n - 1) is S.Zero + assert catalan(Rational(-1, 2)) is S.ComplexInfinity + assert catalan(-S.One) == Rational(-1, 2) + c1 = catalan(-5.6).evalf() + assert str(c1) == '6.93334070531408e-5' + c2 = catalan(-35.4).evalf() + assert str(c2) == '-4.14189164517449e-24' + + +def test_motzkin(): + assert motzkin.is_motzkin(4) == True + assert motzkin.is_motzkin(9) == True + assert motzkin.is_motzkin(10) == False + assert motzkin.find_motzkin_numbers_in_range(10,200) == [21, 51, 127] + assert motzkin.find_motzkin_numbers_in_range(10,400) == [21, 51, 127, 323] + assert motzkin.find_motzkin_numbers_in_range(10,1600) == [21, 51, 127, 323, 835] + assert motzkin.find_first_n_motzkins(5) == [1, 1, 2, 4, 9] + assert motzkin.find_first_n_motzkins(7) == [1, 1, 2, 4, 9, 21, 51] + assert motzkin.find_first_n_motzkins(10) == [1, 1, 2, 4, 9, 21, 51, 127, 323, 835] + raises(ValueError, lambda: motzkin.eval(77.58)) + raises(ValueError, lambda: motzkin.eval(-8)) + raises(ValueError, lambda: motzkin.find_motzkin_numbers_in_range(-2,7)) + raises(ValueError, lambda: motzkin.find_motzkin_numbers_in_range(13,7)) + raises(ValueError, lambda: motzkin.find_first_n_motzkins(112.8)) + + +def test_nD_derangements(): + from sympy.utilities.iterables import (partitions, multiset, + multiset_derangements, multiset_permutations) + from sympy.functions.combinatorial.numbers import nD + + got = [] + for i in partitions(8, k=4): + s = [] + it = 0 + for k, v in i.items(): + for i in range(v): + s.extend([it]*k) + it += 1 + ms = multiset(s) + c1 = sum(1 for i in multiset_permutations(s) if + all(i != j for i, j in zip(i, s))) + assert c1 == nD(ms) == nD(ms, 0) == nD(ms, 1) + v = [tuple(i) for i in multiset_derangements(s)] + c2 = len(v) + assert c2 == len(set(v)) + assert c1 == c2 + got.append(c1) + assert got == [1, 4, 6, 12, 24, 24, 61, 126, 315, 780, 297, 772, + 2033, 5430, 14833] + + assert nD('1112233456', brute=True) == nD('1112233456') == 16356 + assert nD('') == nD([]) == nD({}) == 0 + assert nD({1: 0}) == 0 + raises(ValueError, lambda: nD({1: -1})) + assert nD('112') == 0 + assert nD(i='112') == 0 + assert [nD(n=i) for i in range(6)] == [0, 0, 1, 2, 9, 44] + assert nD((i for i in range(4))) == nD('0123') == 9 + assert nD(m=(i for i in range(4))) == 3 + assert nD(m={0: 1, 1: 1, 2: 1, 3: 1}) == 3 + assert nD(m=[0, 1, 2, 3]) == 3 + raises(TypeError, lambda: nD(m=0)) + raises(TypeError, lambda: nD(-1)) + assert nD({-1: 1, -2: 1}) == 1 + assert nD(m={0: 3}) == 0 + raises(ValueError, lambda: nD(i='123', n=3)) + raises(ValueError, lambda: nD(i='123', m=(1,2))) + raises(ValueError, lambda: nD(n=0, m=(1,2))) + raises(ValueError, lambda: nD({1: -1})) + raises(ValueError, lambda: nD(m={-1: 1, 2: 1})) + raises(ValueError, lambda: nD(m={1: -1, 2: 1})) + raises(ValueError, lambda: nD(m=[-1, 2])) + raises(TypeError, lambda: nD({1: x})) + raises(TypeError, lambda: nD(m={1: x})) + raises(TypeError, lambda: nD(m={x: 1})) + + +def test_deprecated_ntheory_symbolic_functions(): + from sympy.testing.pytest import warns_deprecated_sympy + + with warns_deprecated_sympy(): + assert not carmichael.is_carmichael(3) + with warns_deprecated_sympy(): + assert carmichael.find_carmichael_numbers_in_range(10, 20) == [] + with warns_deprecated_sympy(): + assert carmichael.find_first_n_carmichaels(1) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/__init__.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..78034e72ef2ed722c3ae685a87cf4df618a982b0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/__init__.py @@ -0,0 +1 @@ +# Stub __init__.py for sympy.functions.elementary diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/_trigonometric_special.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/_trigonometric_special.py new file mode 100644 index 0000000000000000000000000000000000000000..fdf8c9d06241b46e791afe76836ea33e6d4fb1c8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/_trigonometric_special.py @@ -0,0 +1,261 @@ +r"""A module for special angle formulas for trigonometric functions + +TODO +==== + +This module should be developed in the future to contain direct square root +representation of + +.. math + F(\frac{n}{m} \pi) + +for every + +- $m \in \{ 3, 5, 17, 257, 65537 \}$ +- $n \in \mathbb{N}$, $0 \le n < m$ +- $F \in \{\sin, \cos, \tan, \csc, \sec, \cot\}$ + +Without multi-step rewrites +(e.g. $\tan \to \cos/\sin \to \cos/\sqrt \to \ sqrt$) +or using chebyshev identities +(e.g. $\cos \to \cos + \cos^2 + \cdots \to \sqrt{} + \sqrt{}^2 + \cdots $), +which are trivial to implement in sympy, +and had used to give overly complicated expressions. + +The reference can be found below, if anyone may need help implementing them. + +References +========== + +.. [*] Gottlieb, Christian. (1999). The Simple and straightforward construction + of the regular 257-gon. The Mathematical Intelligencer. 21. 31-37. + 10.1007/BF03024829. +.. [*] https://resources.wolframcloud.com/FunctionRepository/resources/Cos2PiOverFermatPrime +""" +from __future__ import annotations +from typing import Callable +from functools import reduce +from sympy.core.expr import Expr +from sympy.core.singleton import S +from sympy.core.intfunc import igcdex +from sympy.core.numbers import Integer +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.core.cache import cacheit + + +def migcdex(*x: int) -> tuple[tuple[int, ...], int]: + r"""Compute extended gcd for multiple integers. + + Explanation + =========== + + Given the integers $x_1, \cdots, x_n$ and + an extended gcd for multiple arguments are defined as a solution + $(y_1, \cdots, y_n), g$ for the diophantine equation + $x_1 y_1 + \cdots + x_n y_n = g$ such that + $g = \gcd(x_1, \cdots, x_n)$. + + Examples + ======== + + >>> from sympy.functions.elementary._trigonometric_special import migcdex + >>> migcdex() + ((), 0) + >>> migcdex(4) + ((1,), 4) + >>> migcdex(4, 6) + ((-1, 1), 2) + >>> migcdex(6, 10, 15) + ((1, 1, -1), 1) + """ + if not x: + return (), 0 + + if len(x) == 1: + return (1,), x[0] + + if len(x) == 2: + u, v, h = igcdex(x[0], x[1]) + return (u, v), h + + y, g = migcdex(*x[1:]) + u, v, h = igcdex(x[0], g) + return (u, *(v * i for i in y)), h + + +def ipartfrac(*denoms: int) -> tuple[int, ...]: + r"""Compute the partial fraction decomposition. + + Explanation + =========== + + Given a rational number $\frac{1}{q_1 \cdots q_n}$ where all + $q_1, \cdots, q_n$ are pairwise coprime, + + A partial fraction decomposition is defined as + + .. math:: + \frac{1}{q_1 \cdots q_n} = \frac{p_1}{q_1} + \cdots + \frac{p_n}{q_n} + + And it can be derived from solving the following diophantine equation for + the $p_1, \cdots, p_n$ + + .. math:: + 1 = p_1 \prod_{i \ne 1}q_i + \cdots + p_n \prod_{i \ne n}q_i + + Where $q_1, \cdots, q_n$ being pairwise coprime implies + $\gcd(\prod_{i \ne 1}q_i, \cdots, \prod_{i \ne n}q_i) = 1$, + which guarantees the existence of the solution. + + It is sufficient to compute partial fraction decomposition only + for numerator $1$ because partial fraction decomposition for any + $\frac{n}{q_1 \cdots q_n}$ can be easily computed by multiplying + the result by $n$ afterwards. + + Parameters + ========== + + denoms : int + The pairwise coprime integer denominators $q_i$ which defines the + rational number $\frac{1}{q_1 \cdots q_n}$ + + Returns + ======= + + tuple[int, ...] + The list of numerators which semantically corresponds to $p_i$ of the + partial fraction decomposition + $\frac{1}{q_1 \cdots q_n} = \frac{p_1}{q_1} + \cdots + \frac{p_n}{q_n}$ + + Examples + ======== + + >>> from sympy import Rational, Mul + >>> from sympy.functions.elementary._trigonometric_special import ipartfrac + + >>> denoms = 2, 3, 5 + >>> numers = ipartfrac(2, 3, 5) + >>> numers + (1, 7, -14) + + >>> Rational(1, Mul(*denoms)) + 1/30 + >>> out = 0 + >>> for n, d in zip(numers, denoms): + ... out += Rational(n, d) + >>> out + 1/30 + """ + if not denoms: + return () + + def mul(x: int, y: int) -> int: + return x * y + + denom = reduce(mul, denoms) + a = [denom // x for x in denoms] + h, _ = migcdex(*a) + return h + + +def fermat_coords(n: int) -> list[int] | None: + """If n can be factored in terms of Fermat primes with + multiplicity of each being 1, return those primes, else + None + """ + primes = [] + for p in [3, 5, 17, 257, 65537]: + quotient, remainder = divmod(n, p) + if remainder == 0: + n = quotient + primes.append(p) + if n == 1: + return primes + return None + + +@cacheit +def cos_3() -> Expr: + r"""Computes $\cos \frac{\pi}{3}$ in square roots""" + return S.Half + + +@cacheit +def cos_5() -> Expr: + r"""Computes $\cos \frac{\pi}{5}$ in square roots""" + return (sqrt(5) + 1) / 4 + + +@cacheit +def cos_17() -> Expr: + r"""Computes $\cos \frac{\pi}{17}$ in square roots""" + return sqrt( + (15 + sqrt(17)) / 32 + sqrt(2) * (sqrt(17 - sqrt(17)) + + sqrt(sqrt(2) * (-8 * sqrt(17 + sqrt(17)) - (1 - sqrt(17)) + * sqrt(17 - sqrt(17))) + 6 * sqrt(17) + 34)) / 32) + + +@cacheit +def cos_257() -> Expr: + r"""Computes $\cos \frac{\pi}{257}$ in square roots + + References + ========== + + .. [*] https://math.stackexchange.com/questions/516142/how-does-cos2-pi-257-look-like-in-real-radicals + .. [*] https://r-knott.surrey.ac.uk/Fibonacci/simpleTrig.html + """ + def f1(a: Expr, b: Expr) -> tuple[Expr, Expr]: + return (a + sqrt(a**2 + b)) / 2, (a - sqrt(a**2 + b)) / 2 + + def f2(a: Expr, b: Expr) -> Expr: + return (a - sqrt(a**2 + b))/2 + + t1, t2 = f1(S.NegativeOne, Integer(256)) + z1, z3 = f1(t1, Integer(64)) + z2, z4 = f1(t2, Integer(64)) + y1, y5 = f1(z1, 4*(5 + t1 + 2*z1)) + y6, y2 = f1(z2, 4*(5 + t2 + 2*z2)) + y3, y7 = f1(z3, 4*(5 + t1 + 2*z3)) + y8, y4 = f1(z4, 4*(5 + t2 + 2*z4)) + x1, x9 = f1(y1, -4*(t1 + y1 + y3 + 2*y6)) + x2, x10 = f1(y2, -4*(t2 + y2 + y4 + 2*y7)) + x3, x11 = f1(y3, -4*(t1 + y3 + y5 + 2*y8)) + x4, x12 = f1(y4, -4*(t2 + y4 + y6 + 2*y1)) + x5, x13 = f1(y5, -4*(t1 + y5 + y7 + 2*y2)) + x6, x14 = f1(y6, -4*(t2 + y6 + y8 + 2*y3)) + x15, x7 = f1(y7, -4*(t1 + y7 + y1 + 2*y4)) + x8, x16 = f1(y8, -4*(t2 + y8 + y2 + 2*y5)) + v1 = f2(x1, -4*(x1 + x2 + x3 + x6)) + v2 = f2(x2, -4*(x2 + x3 + x4 + x7)) + v3 = f2(x8, -4*(x8 + x9 + x10 + x13)) + v4 = f2(x9, -4*(x9 + x10 + x11 + x14)) + v5 = f2(x10, -4*(x10 + x11 + x12 + x15)) + v6 = f2(x16, -4*(x16 + x1 + x2 + x5)) + u1 = -f2(-v1, -4*(v2 + v3)) + u2 = -f2(-v4, -4*(v5 + v6)) + w1 = -2*f2(-u1, -4*u2) + return sqrt(sqrt(2)*sqrt(w1 + 4)/8 + S.Half) + + +def cos_table() -> dict[int, Callable[[], Expr]]: + r"""Lazily evaluated table for $\cos \frac{\pi}{n}$ in square roots for + $n \in \{3, 5, 17, 257, 65537\}$. + + Notes + ===== + + 65537 is the only other known Fermat prime and it is nearly impossible to + build in the current SymPy due to performance issues. + + References + ========== + + https://r-knott.surrey.ac.uk/Fibonacci/simpleTrig.html + """ + return { + 3: cos_3, + 5: cos_5, + 17: cos_17, + 257: cos_257 + } diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/benchmarks/__init__.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/benchmarks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/benchmarks/bench_exp.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/benchmarks/bench_exp.py new file mode 100644 index 0000000000000000000000000000000000000000..fa18d29f87bcd249baec1d278a030fa7a133c3ba --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/benchmarks/bench_exp.py @@ -0,0 +1,11 @@ +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import exp + +x, y = symbols('x,y') + +e = exp(2*x) +q = exp(3*x) + + +def timeit_exp_subs(): + e.subs(q, y) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/complexes.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/complexes.py new file mode 100644 index 0000000000000000000000000000000000000000..dd837e4e242057050370f38c4b4e9c26aa5d06c9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/complexes.py @@ -0,0 +1,1492 @@ +from __future__ import annotations + +from sympy.core import S, Add, Mul, sympify, Symbol, Dummy, Basic +from sympy.core.expr import Expr +from sympy.core.exprtools import factor_terms +from sympy.core.function import (DefinedFunction, Derivative, ArgumentIndexError, + AppliedUndef, expand_mul, PoleError) +from sympy.core.logic import fuzzy_not, fuzzy_or +from sympy.core.numbers import pi, I, oo +from sympy.core.power import Pow +from sympy.core.relational import Eq +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise + +############################################################################### +######################### REAL and IMAGINARY PARTS ############################ +############################################################################### + + +class re(DefinedFunction): + """ + Returns real part of expression. This function performs only + elementary analysis and so it will fail to decompose properly + more complicated expressions. If completely simplified result + is needed then use ``Basic.as_real_imag()`` or perform complex + expansion on instance of this function. + + Examples + ======== + + >>> from sympy import re, im, I, E, symbols + >>> x, y = symbols('x y', real=True) + >>> re(2*E) + 2*E + >>> re(2*I + 17) + 17 + >>> re(2*I) + 0 + >>> re(im(x) + x*I + 2) + 2 + >>> re(5 + I + 2) + 7 + + Parameters + ========== + + arg : Expr + Real or complex expression. + + Returns + ======= + + expr : Expr + Real part of expression. + + See Also + ======== + + im + """ + + args: tuple[Expr] + + is_extended_real = True + unbranched = True # implicitly works on the projection to C + _singularities = True # non-holomorphic + + @classmethod + def eval(cls, arg): + if arg is S.NaN: + return S.NaN + elif arg is S.ComplexInfinity: + return S.NaN + elif arg.is_extended_real: + return arg + elif arg.is_imaginary or (I*arg).is_extended_real: + return S.Zero + elif arg.is_Matrix: + return arg.as_real_imag()[0] + elif arg.is_Function and isinstance(arg, conjugate): + return re(arg.args[0]) + else: + + included, reverted, excluded = [], [], [] + args = Add.make_args(arg) + for term in args: + coeff = term.as_coefficient(I) + + if coeff is not None: + if not coeff.is_extended_real: + reverted.append(coeff) + elif not term.has(I) and term.is_extended_real: + excluded.append(term) + else: + # Try to do some advanced expansion. If + # impossible, don't try to do re(arg) again + # (because this is what we are trying to do now). + real_imag = term.as_real_imag(ignore=arg) + if real_imag: + excluded.append(real_imag[0]) + else: + included.append(term) + + if len(args) != len(included): + a, b, c = (Add(*xs) for xs in [included, reverted, excluded]) + + return cls(a) - im(b) + c + + def as_real_imag(self, deep=True, **hints): + """ + Returns the real number with a zero imaginary part. + + """ + return (self, S.Zero) + + def _eval_derivative(self, x): + if x.is_extended_real or self.args[0].is_extended_real: + return re(Derivative(self.args[0], x, evaluate=True)) + if x.is_imaginary or self.args[0].is_imaginary: + return -I \ + * im(Derivative(self.args[0], x, evaluate=True)) + + def _eval_rewrite_as_im(self, arg, **kwargs): + return self.args[0] - I*im(self.args[0]) + + def _eval_is_algebraic(self): + return self.args[0].is_algebraic + + def _eval_is_zero(self): + # is_imaginary implies nonzero + return fuzzy_or([self.args[0].is_imaginary, self.args[0].is_zero]) + + def _eval_is_finite(self): + if self.args[0].is_finite: + return True + + def _eval_is_complex(self): + if self.args[0].is_finite: + return True + + +class im(DefinedFunction): + """ + Returns imaginary part of expression. This function performs only + elementary analysis and so it will fail to decompose properly more + complicated expressions. If completely simplified result is needed then + use ``Basic.as_real_imag()`` or perform complex expansion on instance of + this function. + + Examples + ======== + + >>> from sympy import re, im, E, I + >>> from sympy.abc import x, y + >>> im(2*E) + 0 + >>> im(2*I + 17) + 2 + >>> im(x*I) + re(x) + >>> im(re(x) + y) + im(y) + >>> im(2 + 3*I) + 3 + + Parameters + ========== + + arg : Expr + Real or complex expression. + + Returns + ======= + + expr : Expr + Imaginary part of expression. + + See Also + ======== + + re + """ + + args: tuple[Expr] + + is_extended_real = True + unbranched = True # implicitly works on the projection to C + _singularities = True # non-holomorphic + + @classmethod + def eval(cls, arg): + if arg is S.NaN: + return S.NaN + elif arg is S.ComplexInfinity: + return S.NaN + elif arg.is_extended_real: + return S.Zero + elif arg.is_imaginary or (I*arg).is_extended_real: + return -I * arg + elif arg.is_Matrix: + return arg.as_real_imag()[1] + elif arg.is_Function and isinstance(arg, conjugate): + return -im(arg.args[0]) + else: + included, reverted, excluded = [], [], [] + args = Add.make_args(arg) + for term in args: + coeff = term.as_coefficient(I) + + if coeff is not None: + if not coeff.is_extended_real: + reverted.append(coeff) + else: + excluded.append(coeff) + elif term.has(I) or not term.is_extended_real: + # Try to do some advanced expansion. If + # impossible, don't try to do im(arg) again + # (because this is what we are trying to do now). + real_imag = term.as_real_imag(ignore=arg) + if real_imag: + excluded.append(real_imag[1]) + else: + included.append(term) + + if len(args) != len(included): + a, b, c = (Add(*xs) for xs in [included, reverted, excluded]) + + return cls(a) + re(b) + c + + def as_real_imag(self, deep=True, **hints): + """ + Return the imaginary part with a zero real part. + + """ + return (self, S.Zero) + + def _eval_derivative(self, x): + if x.is_extended_real or self.args[0].is_extended_real: + return im(Derivative(self.args[0], x, evaluate=True)) + if x.is_imaginary or self.args[0].is_imaginary: + return -I \ + * re(Derivative(self.args[0], x, evaluate=True)) + + def _eval_rewrite_as_re(self, arg, **kwargs): + return -I*(self.args[0] - re(self.args[0])) + + def _eval_is_algebraic(self): + return self.args[0].is_algebraic + + def _eval_is_zero(self): + return self.args[0].is_extended_real + + def _eval_is_finite(self): + if self.args[0].is_finite: + return True + + def _eval_is_complex(self): + if self.args[0].is_finite: + return True + +############################################################################### +############### SIGN, ABSOLUTE VALUE, ARGUMENT and CONJUGATION ################ +############################################################################### + +class sign(DefinedFunction): + """ + Returns the complex sign of an expression: + + Explanation + =========== + + If the expression is real the sign will be: + + * $1$ if expression is positive + * $0$ if expression is equal to zero + * $-1$ if expression is negative + + If the expression is imaginary the sign will be: + + * $I$ if im(expression) is positive + * $-I$ if im(expression) is negative + + Otherwise an unevaluated expression will be returned. When evaluated, the + result (in general) will be ``cos(arg(expr)) + I*sin(arg(expr))``. + + Examples + ======== + + >>> from sympy import sign, I + + >>> sign(-1) + -1 + >>> sign(0) + 0 + >>> sign(-3*I) + -I + >>> sign(1 + I) + sign(1 + I) + >>> _.evalf() + 0.707106781186548 + 0.707106781186548*I + + Parameters + ========== + + arg : Expr + Real or imaginary expression. + + Returns + ======= + + expr : Expr + Complex sign of expression. + + See Also + ======== + + Abs, conjugate + """ + + is_complex = True + _singularities = True + + def doit(self, **hints): + s = super().doit() + if s == self and self.args[0].is_zero is False: + return self.args[0] / Abs(self.args[0]) + return s + + @classmethod + def eval(cls, arg): + # handle what we can + if arg.is_Mul: + c, args = arg.as_coeff_mul() + unk = [] + s = sign(c) + for a in args: + if a.is_extended_negative: + s = -s + elif a.is_extended_positive: + pass + else: + if a.is_imaginary: + ai = im(a) + if ai.is_comparable: # i.e. a = I*real + s *= I + if ai.is_extended_negative: + # can't use sign(ai) here since ai might not be + # a Number + s = -s + else: + unk.append(a) + else: + unk.append(a) + if c is S.One and len(unk) == len(args): + return None + return s * cls(arg._new_rawargs(*unk)) + if arg is S.NaN: + return S.NaN + if arg.is_zero: # it may be an Expr that is zero + return S.Zero + if arg.is_extended_positive: + return S.One + if arg.is_extended_negative: + return S.NegativeOne + if arg.is_Function: + if isinstance(arg, sign): + return arg + if arg.is_imaginary: + if arg.is_Pow and arg.exp is S.Half: + # we catch this because non-trivial sqrt args are not expanded + # e.g. sqrt(1-sqrt(2)) --x--> to I*sqrt(sqrt(2) - 1) + return I + arg2 = -I * arg + if arg2.is_extended_positive: + return I + if arg2.is_extended_negative: + return -I + + def _eval_Abs(self): + if fuzzy_not(self.args[0].is_zero): + return S.One + + def _eval_conjugate(self): + return sign(conjugate(self.args[0])) + + def _eval_derivative(self, x): + if self.args[0].is_extended_real: + from sympy.functions.special.delta_functions import DiracDelta + return 2 * Derivative(self.args[0], x, evaluate=True) \ + * DiracDelta(self.args[0]) + elif self.args[0].is_imaginary: + from sympy.functions.special.delta_functions import DiracDelta + return 2 * Derivative(self.args[0], x, evaluate=True) \ + * DiracDelta(-I * self.args[0]) + + def _eval_is_nonnegative(self): + if self.args[0].is_nonnegative: + return True + + def _eval_is_nonpositive(self): + if self.args[0].is_nonpositive: + return True + + def _eval_is_imaginary(self): + return self.args[0].is_imaginary + + def _eval_is_integer(self): + return self.args[0].is_extended_real + + def _eval_is_zero(self): + return self.args[0].is_zero + + def _eval_power(self, other): + if ( + fuzzy_not(self.args[0].is_zero) and + other.is_integer and + other.is_even + ): + return S.One + + def _eval_nseries(self, x, n, logx, cdir=0): + arg0 = self.args[0] + x0 = arg0.subs(x, 0) + if x0 != 0: + return self.func(x0) + if cdir != 0: + cdir = arg0.dir(x, cdir) + return -S.One if re(cdir) < 0 else S.One + + def _eval_rewrite_as_Piecewise(self, arg, **kwargs): + if arg.is_extended_real: + return Piecewise((1, arg > 0), (-1, arg < 0), (0, True)) + + def _eval_rewrite_as_Heaviside(self, arg, **kwargs): + from sympy.functions.special.delta_functions import Heaviside + if arg.is_extended_real: + return Heaviside(arg) * 2 - 1 + + def _eval_rewrite_as_Abs(self, arg, **kwargs): + return Piecewise((0, Eq(arg, 0)), (arg / Abs(arg), True)) + + def _eval_simplify(self, **kwargs): + return self.func(factor_terms(self.args[0])) # XXX include doit? + + +class Abs(DefinedFunction): + """ + Return the absolute value of the argument. + + Explanation + =========== + + This is an extension of the built-in function ``abs()`` to accept symbolic + values. If you pass a SymPy expression to the built-in ``abs()``, it will + pass it automatically to ``Abs()``. + + Examples + ======== + + >>> from sympy import Abs, Symbol, S, I + >>> Abs(-1) + 1 + >>> x = Symbol('x', real=True) + >>> Abs(-x) + Abs(x) + >>> Abs(x**2) + x**2 + >>> abs(-x) # The Python built-in + Abs(x) + >>> Abs(3*x + 2*I) + sqrt(9*x**2 + 4) + >>> Abs(8*I) + 8 + + Note that the Python built-in will return either an Expr or int depending on + the argument:: + + >>> type(abs(-1)) + <... 'int'> + >>> type(abs(S.NegativeOne)) + + + Abs will always return a SymPy object. + + Parameters + ========== + + arg : Expr + Real or complex expression. + + Returns + ======= + + expr : Expr + Absolute value returned can be an expression or integer depending on + input arg. + + See Also + ======== + + sign, conjugate + """ + + args: tuple[Expr] + + is_extended_real = True + is_extended_negative = False + is_extended_nonnegative = True + unbranched = True + _singularities = True # non-holomorphic + + def fdiff(self, argindex=1): + """ + Get the first derivative of the argument to Abs(). + + """ + if argindex == 1: + return sign(self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, arg): + from sympy.simplify.simplify import signsimp + + if hasattr(arg, '_eval_Abs'): + obj = arg._eval_Abs() + if obj is not None: + return obj + if not isinstance(arg, Expr): + raise TypeError("Bad argument type for Abs(): %s" % type(arg)) + + # handle what we can + arg = signsimp(arg, evaluate=False) + n, d = arg.as_numer_denom() + if d.free_symbols and not n.free_symbols: + return cls(n)/cls(d) + + if arg.is_Mul: + known = [] + unk = [] + for t in arg.args: + if t.is_Pow and t.exp.is_integer and t.exp.is_negative: + bnew = cls(t.base) + if isinstance(bnew, cls): + unk.append(t) + else: + known.append(Pow(bnew, t.exp)) + else: + tnew = cls(t) + if isinstance(tnew, cls): + unk.append(t) + else: + known.append(tnew) + known = Mul(*known) + unk = cls(Mul(*unk), evaluate=False) if unk else S.One + return known*unk + if arg is S.NaN: + return S.NaN + if arg is S.ComplexInfinity: + return oo + from sympy.functions.elementary.exponential import exp, log + + if arg.is_Pow: + base, exponent = arg.as_base_exp() + if base.is_extended_real: + if exponent.is_integer: + if exponent.is_even: + return arg + if base is S.NegativeOne: + return S.One + return Abs(base)**exponent + if base.is_extended_nonnegative: + return base**re(exponent) + if base.is_extended_negative: + return (-base)**re(exponent)*exp(-pi*im(exponent)) + return + elif not base.has(Symbol): # complex base + # express base**exponent as exp(exponent*log(base)) + a, b = log(base).as_real_imag() + z = a + I*b + return exp(re(exponent*z)) + if isinstance(arg, exp): + return exp(re(arg.args[0])) + if isinstance(arg, AppliedUndef): + if arg.is_positive: + return arg + elif arg.is_negative: + return -arg + return + if arg.is_Add and arg.has(oo, S.NegativeInfinity): + if any(a.is_infinite for a in arg.as_real_imag()): + return oo + if arg.is_zero: + return S.Zero + if arg.is_extended_nonnegative: + return arg + if arg.is_extended_nonpositive: + return -arg + if arg.is_imaginary: + arg2 = -I * arg + if arg2.is_extended_nonnegative: + return arg2 + if arg.is_extended_real: + return + # reject result if all new conjugates are just wrappers around + # an expression that was already in the arg + conj = signsimp(arg.conjugate(), evaluate=False) + new_conj = conj.atoms(conjugate) - arg.atoms(conjugate) + if new_conj and all(arg.has(i.args[0]) for i in new_conj): + return + if arg != conj and arg != -conj: + ignore = arg.atoms(Abs) + abs_free_arg = arg.xreplace({i: Dummy(real=True) for i in ignore}) + unk = [a for a in abs_free_arg.free_symbols if a.is_extended_real is None] + if not unk or not all(conj.has(conjugate(u)) for u in unk): + return sqrt(expand_mul(arg*conj)) + + def _eval_is_real(self): + if self.args[0].is_finite: + return True + + def _eval_is_integer(self): + if self.args[0].is_extended_real: + return self.args[0].is_integer + + def _eval_is_extended_nonzero(self): + return fuzzy_not(self._args[0].is_zero) + + def _eval_is_zero(self): + return self._args[0].is_zero + + def _eval_is_extended_positive(self): + return fuzzy_not(self._args[0].is_zero) + + def _eval_is_rational(self): + if self.args[0].is_extended_real: + return self.args[0].is_rational + + def _eval_is_even(self): + if self.args[0].is_extended_real: + return self.args[0].is_even + + def _eval_is_odd(self): + if self.args[0].is_extended_real: + return self.args[0].is_odd + + def _eval_is_algebraic(self): + return self.args[0].is_algebraic + + def _eval_power(self, exponent): + if self.args[0].is_extended_real and exponent.is_integer: + if exponent.is_even: + return self.args[0]**exponent + elif exponent is not S.NegativeOne and exponent.is_Integer: + return self.args[0]**(exponent - 1)*self + return + + def _eval_nseries(self, x, n, logx, cdir=0): + from sympy.functions.elementary.exponential import log + direction = self.args[0].leadterm(x)[0] + if direction.has(log(x)): + direction = direction.subs(log(x), logx) + s = self.args[0]._eval_nseries(x, n=n, logx=logx) + return (sign(direction)*s).expand() + + def _eval_derivative(self, x): + if self.args[0].is_extended_real or self.args[0].is_imaginary: + return Derivative(self.args[0], x, evaluate=True) \ + * sign(conjugate(self.args[0])) + rv = (re(self.args[0]) * Derivative(re(self.args[0]), x, + evaluate=True) + im(self.args[0]) * Derivative(im(self.args[0]), + x, evaluate=True)) / Abs(self.args[0]) + return rv.rewrite(sign) + + def _eval_rewrite_as_Heaviside(self, arg, **kwargs): + # Note this only holds for real arg (since Heaviside is not defined + # for complex arguments). + from sympy.functions.special.delta_functions import Heaviside + if arg.is_extended_real: + return arg*(Heaviside(arg) - Heaviside(-arg)) + + def _eval_rewrite_as_Piecewise(self, arg, **kwargs): + if arg.is_extended_real: + return Piecewise((arg, arg >= 0), (-arg, True)) + elif arg.is_imaginary: + return Piecewise((I*arg, I*arg >= 0), (-I*arg, True)) + + def _eval_rewrite_as_sign(self, arg, **kwargs): + return arg/sign(arg) + + def _eval_rewrite_as_conjugate(self, arg, **kwargs): + return sqrt(arg*conjugate(arg)) + + +class arg(DefinedFunction): + r""" + Returns the argument (in radians) of a complex number. The argument is + evaluated in consistent convention with ``atan2`` where the branch-cut is + taken along the negative real axis and ``arg(z)`` is in the interval + $(-\pi,\pi]$. For a positive number, the argument is always 0; the + argument of a negative number is $\pi$; and the argument of 0 + is undefined and returns ``nan``. So the ``arg`` function will never nest + greater than 3 levels since at the 4th application, the result must be + nan; for a real number, nan is returned on the 3rd application. + + Examples + ======== + + >>> from sympy import arg, I, sqrt, Dummy + >>> from sympy.abc import x + >>> arg(2.0) + 0 + >>> arg(I) + pi/2 + >>> arg(sqrt(2) + I*sqrt(2)) + pi/4 + >>> arg(sqrt(3)/2 + I/2) + pi/6 + >>> arg(4 + 3*I) + atan(3/4) + >>> arg(0.8 + 0.6*I) + 0.643501108793284 + >>> arg(arg(arg(arg(x)))) + nan + >>> real = Dummy(real=True) + >>> arg(arg(arg(real))) + nan + + Parameters + ========== + + arg : Expr + Real or complex expression. + + Returns + ======= + + value : Expr + Returns arc tangent of arg measured in radians. + + """ + + is_extended_real = True + is_real = True + is_finite = True + _singularities = True # non-holomorphic + + @classmethod + def eval(cls, arg): + a = arg + for i in range(3): + if isinstance(a, cls): + a = a.args[0] + else: + if i == 2 and a.is_extended_real: + return S.NaN + break + else: + return S.NaN + from sympy.functions.elementary.exponential import exp, exp_polar + if isinstance(arg, exp_polar): + return periodic_argument(arg, oo) + elif isinstance(arg, exp): + i_ = im(arg.args[0]) + if i_.is_comparable: + i_ %= 2*S.Pi + if i_ > S.Pi: + i_ -= 2*S.Pi + return i_ + + if not arg.is_Atom: + c, arg_ = factor_terms(arg).as_coeff_Mul() + if arg_.is_Mul: + arg_ = Mul(*[a if (sign(a) not in (-1, 1)) else + sign(a) for a in arg_.args]) + arg_ = sign(c)*arg_ + else: + arg_ = arg + if any(i.is_extended_positive is None for i in arg_.atoms(AppliedUndef)): + return + from sympy.functions.elementary.trigonometric import atan2 + x, y = arg_.as_real_imag() + rv = atan2(y, x) + if rv.is_number: + return rv + if arg_ != arg: + return cls(arg_, evaluate=False) + + def _eval_derivative(self, t): + x, y = self.args[0].as_real_imag() + return (x * Derivative(y, t, evaluate=True) - y * + Derivative(x, t, evaluate=True)) / (x**2 + y**2) + + def _eval_rewrite_as_atan2(self, arg, **kwargs): + from sympy.functions.elementary.trigonometric import atan2 + x, y = self.args[0].as_real_imag() + return atan2(y, x) + + def _eval_as_leading_term(self, x, logx, cdir): + arg0 = self.args[0] + t = Dummy('t', positive=True) + if cdir == 0: + cdir = 1 + z = arg0.subs(x, cdir*t) + if z.is_positive: + return S.Zero + elif z.is_negative: + return S.Pi + else: + raise PoleError("Cannot expand %s around 0" % (self)) + + def _eval_nseries(self, x, n, logx, cdir=0): + from sympy.series.order import Order + if n <= 0: + return Order(1) + return self._eval_as_leading_term(x, logx=logx, cdir=cdir) + + +class conjugate(DefinedFunction): + """ + Returns the *complex conjugate* [1]_ of an argument. + In mathematics, the complex conjugate of a complex number + is given by changing the sign of the imaginary part. + + Thus, the conjugate of the complex number + :math:`a + ib` (where $a$ and $b$ are real numbers) is :math:`a - ib` + + Examples + ======== + + >>> from sympy import conjugate, I + >>> conjugate(2) + 2 + >>> conjugate(I) + -I + >>> conjugate(3 + 2*I) + 3 - 2*I + >>> conjugate(5 - I) + 5 + I + + Parameters + ========== + + arg : Expr + Real or complex expression. + + Returns + ======= + + arg : Expr + Complex conjugate of arg as real, imaginary or mixed expression. + + See Also + ======== + + sign, Abs + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Complex_conjugation + """ + _singularities = True # non-holomorphic + + @classmethod + def eval(cls, arg): + obj = arg._eval_conjugate() + if obj is not None: + return obj + + def inverse(self): + return conjugate + + def _eval_Abs(self): + return Abs(self.args[0], evaluate=True) + + def _eval_adjoint(self): + return transpose(self.args[0]) + + def _eval_conjugate(self): + return self.args[0] + + def _eval_derivative(self, x): + if x.is_real: + return conjugate(Derivative(self.args[0], x, evaluate=True)) + elif x.is_imaginary: + return -conjugate(Derivative(self.args[0], x, evaluate=True)) + + def _eval_transpose(self): + return adjoint(self.args[0]) + + def _eval_is_algebraic(self): + return self.args[0].is_algebraic + + +class transpose(DefinedFunction): + """ + Linear map transposition. + + Examples + ======== + + >>> from sympy import transpose, Matrix, MatrixSymbol + >>> A = MatrixSymbol('A', 25, 9) + >>> transpose(A) + A.T + >>> B = MatrixSymbol('B', 9, 22) + >>> transpose(B) + B.T + >>> transpose(A*B) + B.T*A.T + >>> M = Matrix([[4, 5], [2, 1], [90, 12]]) + >>> M + Matrix([ + [ 4, 5], + [ 2, 1], + [90, 12]]) + >>> transpose(M) + Matrix([ + [4, 2, 90], + [5, 1, 12]]) + + Parameters + ========== + + arg : Matrix + Matrix or matrix expression to take the transpose of. + + Returns + ======= + + value : Matrix + Transpose of arg. + + """ + + @classmethod + def eval(cls, arg): + obj = arg._eval_transpose() + if obj is not None: + return obj + + def _eval_adjoint(self): + return conjugate(self.args[0]) + + def _eval_conjugate(self): + return adjoint(self.args[0]) + + def _eval_transpose(self): + return self.args[0] + + +class adjoint(DefinedFunction): + """ + Conjugate transpose or Hermite conjugation. + + Examples + ======== + + >>> from sympy import adjoint, MatrixSymbol + >>> A = MatrixSymbol('A', 10, 5) + >>> adjoint(A) + Adjoint(A) + + Parameters + ========== + + arg : Matrix + Matrix or matrix expression to take the adjoint of. + + Returns + ======= + + value : Matrix + Represents the conjugate transpose or Hermite + conjugation of arg. + + """ + + @classmethod + def eval(cls, arg): + obj = arg._eval_adjoint() + if obj is not None: + return obj + obj = arg._eval_transpose() + if obj is not None: + return conjugate(obj) + + def _eval_adjoint(self): + return self.args[0] + + def _eval_conjugate(self): + return transpose(self.args[0]) + + def _eval_transpose(self): + return conjugate(self.args[0]) + + def _latex(self, printer, exp=None, *args): + arg = printer._print(self.args[0]) + tex = r'%s^{\dagger}' % arg + if exp: + tex = r'\left(%s\right)^{%s}' % (tex, exp) + return tex + + def _pretty(self, printer, *args): + from sympy.printing.pretty.stringpict import prettyForm + pform = printer._print(self.args[0], *args) + if printer._use_unicode: + pform = pform**prettyForm('\N{DAGGER}') + else: + pform = pform**prettyForm('+') + return pform + +############################################################################### +############### HANDLING OF POLAR NUMBERS ##################################### +############################################################################### + + +class polar_lift(DefinedFunction): + """ + Lift argument to the Riemann surface of the logarithm, using the + standard branch. + + Examples + ======== + + >>> from sympy import Symbol, polar_lift, I + >>> p = Symbol('p', polar=True) + >>> x = Symbol('x') + >>> polar_lift(4) + 4*exp_polar(0) + >>> polar_lift(-4) + 4*exp_polar(I*pi) + >>> polar_lift(-I) + exp_polar(-I*pi/2) + >>> polar_lift(I + 2) + polar_lift(2 + I) + + >>> polar_lift(4*x) + 4*polar_lift(x) + >>> polar_lift(4*p) + 4*p + + Parameters + ========== + + arg : Expr + Real or complex expression. + + See Also + ======== + + sympy.functions.elementary.exponential.exp_polar + periodic_argument + """ + + is_polar = True + is_comparable = False # Cannot be evalf'd. + + @classmethod + def eval(cls, arg): + from sympy.functions.elementary.complexes import arg as argument + if arg.is_number: + ar = argument(arg) + # In general we want to affirm that something is known, + # e.g. `not ar.has(argument) and not ar.has(atan)` + # but for now we will just be more restrictive and + # see that it has evaluated to one of the known values. + if ar in (0, pi/2, -pi/2, pi): + from sympy.functions.elementary.exponential import exp_polar + return exp_polar(I*ar)*abs(arg) + + if arg.is_Mul: + args = arg.args + else: + args = [arg] + included = [] + excluded = [] + positive = [] + for arg in args: + if arg.is_polar: + included += [arg] + elif arg.is_positive: + positive += [arg] + else: + excluded += [arg] + if len(excluded) < len(args): + if excluded: + return Mul(*(included + positive))*polar_lift(Mul(*excluded)) + elif included: + return Mul(*(included + positive)) + else: + from sympy.functions.elementary.exponential import exp_polar + return Mul(*positive)*exp_polar(0) + + def _eval_evalf(self, prec): + """ Careful! any evalf of polar numbers is flaky """ + return self.args[0]._eval_evalf(prec) + + def _eval_Abs(self): + return Abs(self.args[0], evaluate=True) + + +class periodic_argument(DefinedFunction): + r""" + Represent the argument on a quotient of the Riemann surface of the + logarithm. That is, given a period $P$, always return a value in + $(-P/2, P/2]$, by using $\exp(PI) = 1$. + + Examples + ======== + + >>> from sympy import exp_polar, periodic_argument + >>> from sympy import I, pi + >>> periodic_argument(exp_polar(10*I*pi), 2*pi) + 0 + >>> periodic_argument(exp_polar(5*I*pi), 4*pi) + pi + >>> from sympy import exp_polar, periodic_argument + >>> from sympy import I, pi + >>> periodic_argument(exp_polar(5*I*pi), 2*pi) + pi + >>> periodic_argument(exp_polar(5*I*pi), 3*pi) + -pi + >>> periodic_argument(exp_polar(5*I*pi), pi) + 0 + + Parameters + ========== + + ar : Expr + A polar number. + + period : Expr + The period $P$. + + See Also + ======== + + sympy.functions.elementary.exponential.exp_polar + polar_lift : Lift argument to the Riemann surface of the logarithm + principal_branch + """ + + @classmethod + def _getunbranched(cls, ar): + from sympy.functions.elementary.exponential import exp_polar, log + if ar.is_Mul: + args = ar.args + else: + args = [ar] + unbranched = 0 + for a in args: + if not a.is_polar: + unbranched += arg(a) + elif isinstance(a, exp_polar): + unbranched += a.exp.as_real_imag()[1] + elif a.is_Pow: + re, im = a.exp.as_real_imag() + unbranched += re*unbranched_argument( + a.base) + im*log(abs(a.base)) + elif isinstance(a, polar_lift): + unbranched += arg(a.args[0]) + else: + return None + return unbranched + + @classmethod + def eval(cls, ar, period): + # Our strategy is to evaluate the argument on the Riemann surface of the + # logarithm, and then reduce. + # NOTE evidently this means it is a rather bad idea to use this with + # period != 2*pi and non-polar numbers. + if not period.is_extended_positive: + return None + if period == oo and isinstance(ar, principal_branch): + return periodic_argument(*ar.args) + if isinstance(ar, polar_lift) and period >= 2*pi: + return periodic_argument(ar.args[0], period) + if ar.is_Mul: + newargs = [x for x in ar.args if not x.is_positive] + if len(newargs) != len(ar.args): + return periodic_argument(Mul(*newargs), period) + unbranched = cls._getunbranched(ar) + if unbranched is None: + return None + from sympy.functions.elementary.trigonometric import atan, atan2 + if unbranched.has(periodic_argument, atan2, atan): + return None + if period == oo: + return unbranched + if period != oo: + from sympy.functions.elementary.integers import ceiling + n = ceiling(unbranched/period - S.Half)*period + if not n.has(ceiling): + return unbranched - n + + def _eval_evalf(self, prec): + z, period = self.args + if period == oo: + unbranched = periodic_argument._getunbranched(z) + if unbranched is None: + return self + return unbranched._eval_evalf(prec) + ub = periodic_argument(z, oo)._eval_evalf(prec) + from sympy.functions.elementary.integers import ceiling + return (ub - ceiling(ub/period - S.Half)*period)._eval_evalf(prec) + + +def unbranched_argument(arg): + ''' + Returns periodic argument of arg with period as infinity. + + Examples + ======== + + >>> from sympy import exp_polar, unbranched_argument + >>> from sympy import I, pi + >>> unbranched_argument(exp_polar(15*I*pi)) + 15*pi + >>> unbranched_argument(exp_polar(7*I*pi)) + 7*pi + + See also + ======== + + periodic_argument + ''' + return periodic_argument(arg, oo) + + +class principal_branch(DefinedFunction): + """ + Represent a polar number reduced to its principal branch on a quotient + of the Riemann surface of the logarithm. + + Explanation + =========== + + This is a function of two arguments. The first argument is a polar + number `z`, and the second one a positive real number or infinity, `p`. + The result is ``z mod exp_polar(I*p)``. + + Examples + ======== + + >>> from sympy import exp_polar, principal_branch, oo, I, pi + >>> from sympy.abc import z + >>> principal_branch(z, oo) + z + >>> principal_branch(exp_polar(2*pi*I)*3, 2*pi) + 3*exp_polar(0) + >>> principal_branch(exp_polar(2*pi*I)*3*z, 2*pi) + 3*principal_branch(z, 2*pi) + + Parameters + ========== + + x : Expr + A polar number. + + period : Expr + Positive real number or infinity. + + See Also + ======== + + sympy.functions.elementary.exponential.exp_polar + polar_lift : Lift argument to the Riemann surface of the logarithm + periodic_argument + """ + + is_polar = True + is_comparable = False # cannot always be evalf'd + + @classmethod + def eval(self, x, period): + from sympy.functions.elementary.exponential import exp_polar + if isinstance(x, polar_lift): + return principal_branch(x.args[0], period) + if period == oo: + return x + ub = periodic_argument(x, oo) + barg = periodic_argument(x, period) + if ub != barg and not ub.has(periodic_argument) \ + and not barg.has(periodic_argument): + pl = polar_lift(x) + + def mr(expr): + if not isinstance(expr, Symbol): + return polar_lift(expr) + return expr + pl = pl.replace(polar_lift, mr) + # Recompute unbranched argument + ub = periodic_argument(pl, oo) + if not pl.has(polar_lift): + if ub != barg: + res = exp_polar(I*(barg - ub))*pl + else: + res = pl + if not res.is_polar and not res.has(exp_polar): + res *= exp_polar(0) + return res + + if not x.free_symbols: + c, m = x, () + else: + c, m = x.as_coeff_mul(*x.free_symbols) + others = [] + for y in m: + if y.is_positive: + c *= y + else: + others += [y] + m = tuple(others) + arg = periodic_argument(c, period) + if arg.has(periodic_argument): + return None + if arg.is_number and (unbranched_argument(c) != arg or + (arg == 0 and m != () and c != 1)): + if arg == 0: + return abs(c)*principal_branch(Mul(*m), period) + return principal_branch(exp_polar(I*arg)*Mul(*m), period)*abs(c) + if arg.is_number and ((abs(arg) < period/2) == True or arg == period/2) \ + and m == (): + return exp_polar(arg*I)*abs(c) + + def _eval_evalf(self, prec): + z, period = self.args + p = periodic_argument(z, period)._eval_evalf(prec) + if abs(p) > pi or p == -pi: + return self # Cannot evalf for this argument. + from sympy.functions.elementary.exponential import exp + return (abs(z)*exp(I*p))._eval_evalf(prec) + + +def _polarify(eq, lift, pause=False): + from sympy.integrals.integrals import Integral + if eq.is_polar: + return eq + if eq.is_number and not pause: + return polar_lift(eq) + if isinstance(eq, Symbol) and not pause and lift: + return polar_lift(eq) + elif eq.is_Atom: + return eq + elif eq.is_Add: + r = eq.func(*[_polarify(arg, lift, pause=True) for arg in eq.args]) + if lift: + return polar_lift(r) + return r + elif eq.is_Pow and eq.base == S.Exp1: + return eq.func(S.Exp1, _polarify(eq.exp, lift, pause=False)) + elif eq.is_Function: + return eq.func(*[_polarify(arg, lift, pause=False) for arg in eq.args]) + elif isinstance(eq, Integral): + # Don't lift the integration variable + func = _polarify(eq.function, lift, pause=pause) + limits = [] + for limit in eq.args[1:]: + var = _polarify(limit[0], lift=False, pause=pause) + rest = _polarify(limit[1:], lift=lift, pause=pause) + limits.append((var,) + rest) + return Integral(*((func,) + tuple(limits))) + else: + return eq.func(*[_polarify(arg, lift, pause=pause) + if isinstance(arg, Expr) else arg for arg in eq.args]) + + +def polarify(eq, subs=True, lift=False): + """ + Turn all numbers in eq into their polar equivalents (under the standard + choice of argument). + + Note that no attempt is made to guess a formal convention of adding + polar numbers, expressions like $1 + x$ will generally not be altered. + + Note also that this function does not promote ``exp(x)`` to ``exp_polar(x)``. + + If ``subs`` is ``True``, all symbols which are not already polar will be + substituted for polar dummies; in this case the function behaves much + like :func:`~.posify`. + + If ``lift`` is ``True``, both addition statements and non-polar symbols are + changed to their ``polar_lift()``ed versions. + Note that ``lift=True`` implies ``subs=False``. + + Examples + ======== + + >>> from sympy import polarify, sin, I + >>> from sympy.abc import x, y + >>> expr = (-x)**y + >>> expr.expand() + (-x)**y + >>> polarify(expr) + ((_x*exp_polar(I*pi))**_y, {_x: x, _y: y}) + >>> polarify(expr)[0].expand() + _x**_y*exp_polar(_y*I*pi) + >>> polarify(x, lift=True) + polar_lift(x) + >>> polarify(x*(1+y), lift=True) + polar_lift(x)*polar_lift(y + 1) + + Adds are treated carefully: + + >>> polarify(1 + sin((1 + I)*x)) + (sin(_x*polar_lift(1 + I)) + 1, {_x: x}) + """ + if lift: + subs = False + eq = _polarify(sympify(eq), lift) + if not subs: + return eq + reps = {s: Dummy(s.name, polar=True) for s in eq.free_symbols} + eq = eq.subs(reps) + return eq, {r: s for s, r in reps.items()} + + +def _unpolarify(eq, exponents_only, pause=False): + if not isinstance(eq, Basic) or eq.is_Atom: + return eq + + if not pause: + from sympy.functions.elementary.exponential import exp, exp_polar + if isinstance(eq, exp_polar): + return exp(_unpolarify(eq.exp, exponents_only)) + if isinstance(eq, principal_branch) and eq.args[1] == 2*pi: + return _unpolarify(eq.args[0], exponents_only) + if ( + eq.is_Add or eq.is_Mul or eq.is_Boolean or + eq.is_Relational and ( + eq.rel_op in ('==', '!=') and 0 in eq.args or + eq.rel_op not in ('==', '!=')) + ): + return eq.func(*[_unpolarify(x, exponents_only) for x in eq.args]) + if isinstance(eq, polar_lift): + return _unpolarify(eq.args[0], exponents_only) + + if eq.is_Pow: + expo = _unpolarify(eq.exp, exponents_only) + base = _unpolarify(eq.base, exponents_only, + not (expo.is_integer and not pause)) + return base**expo + + if eq.is_Function and getattr(eq.func, 'unbranched', False): + return eq.func(*[_unpolarify(x, exponents_only, exponents_only) + for x in eq.args]) + + return eq.func(*[_unpolarify(x, exponents_only, True) for x in eq.args]) + + +def unpolarify(eq, subs=None, exponents_only=False): + """ + If `p` denotes the projection from the Riemann surface of the logarithm to + the complex line, return a simplified version `eq'` of `eq` such that + `p(eq') = p(eq)`. + Also apply the substitution subs in the end. (This is a convenience, since + ``unpolarify``, in a certain sense, undoes :func:`polarify`.) + + Examples + ======== + + >>> from sympy import unpolarify, polar_lift, sin, I + >>> unpolarify(polar_lift(I + 2)) + 2 + I + >>> unpolarify(sin(polar_lift(I + 7))) + sin(7 + I) + """ + if isinstance(eq, bool): + return eq + + eq = sympify(eq) + if subs is not None: + return unpolarify(eq.subs(subs)) + changed = True + pause = False + if exponents_only: + pause = True + while changed: + changed = False + res = _unpolarify(eq, exponents_only, pause) + if res != eq: + changed = True + eq = res + if isinstance(res, bool): + return res + # Finally, replacing Exp(0) by 1 is always correct. + # So is polar_lift(0) -> 0. + from sympy.functions.elementary.exponential import exp_polar + return res.subs({exp_polar(0): 1, polar_lift(0): 0}) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/exponential.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/exponential.py new file mode 100644 index 0000000000000000000000000000000000000000..2bb0333cb34a35a96248c12a4640e848986f2feb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/exponential.py @@ -0,0 +1,1286 @@ +from __future__ import annotations +from itertools import product + +from sympy.core.add import Add +from sympy.core.cache import cacheit +from sympy.core.expr import Expr +from sympy.core.function import (DefinedFunction, ArgumentIndexError, expand_log, + expand_mul, FunctionClass, PoleError, expand_multinomial, expand_complex) +from sympy.core.logic import fuzzy_and, fuzzy_not, fuzzy_or +from sympy.core.mul import Mul +from sympy.core.numbers import Integer, Rational, pi, I +from sympy.core.parameters import global_parameters +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import Wild, Dummy +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.complexes import arg, unpolarify, im, re, Abs +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.ntheory import multiplicity, perfect_power +from sympy.ntheory.factor_ import factorint + +# NOTE IMPORTANT +# The series expansion code in this file is an important part of the gruntz +# algorithm for determining limits. _eval_nseries has to return a generalized +# power series with coefficients in C(log(x), log). +# In more detail, the result of _eval_nseries(self, x, n) must be +# c_0*x**e_0 + ... (finitely many terms) +# where e_i are numbers (not necessarily integers) and c_i involve only +# numbers, the function log, and log(x). [This also means it must not contain +# log(x(1+p)), this *has* to be expanded to log(x)+log(1+p) if x.is_positive and +# p.is_positive.] + + +class ExpBase(DefinedFunction): + + unbranched = True + _singularities = (S.ComplexInfinity,) + + @property + def kind(self): + return self.exp.kind + + def inverse(self, argindex=1): + """ + Returns the inverse function of ``exp(x)``. + """ + return log + + def as_numer_denom(self): + """ + Returns this with a positive exponent as a 2-tuple (a fraction). + + Examples + ======== + + >>> from sympy import exp + >>> from sympy.abc import x + >>> exp(-x).as_numer_denom() + (1, exp(x)) + >>> exp(x).as_numer_denom() + (exp(x), 1) + """ + # this should be the same as Pow.as_numer_denom wrt + # exponent handling + if not self.is_commutative: + return self, S.One + exp = self.exp + neg_exp = exp.is_negative + if not neg_exp and not (-exp).is_negative: + neg_exp = exp.could_extract_minus_sign() + if neg_exp: + return S.One, self.func(-exp) + return self, S.One + + @property + def exp(self): + """ + Returns the exponent of the function. + """ + return self.args[0] + + def as_base_exp(self): + """ + Returns the 2-tuple (base, exponent). + """ + return self.func(1), Mul(*self.args) + + def _eval_adjoint(self): + return self.func(self.exp.adjoint()) + + def _eval_conjugate(self): + return self.func(self.exp.conjugate()) + + def _eval_transpose(self): + return self.func(self.exp.transpose()) + + def _eval_is_finite(self): + arg = self.exp + if arg.is_infinite: + if arg.is_extended_negative: + return True + if arg.is_extended_positive: + return False + if arg.is_finite: + return True + + def _eval_is_rational(self): + s = self.func(*self.args) + if s.func == self.func: + z = s.exp.is_zero + if z: + return True + elif s.exp.is_rational and fuzzy_not(z): + return False + else: + return s.is_rational + + def _eval_is_zero(self): + return self.exp is S.NegativeInfinity + + def _eval_power(self, other): + """exp(arg)**e -> exp(arg*e) if assumptions allow it. + """ + b, e = self.as_base_exp() + return Pow._eval_power(Pow(b, e, evaluate=False), other) + + def _eval_expand_power_exp(self, **hints): + from sympy.concrete.products import Product + from sympy.concrete.summations import Sum + arg = self.args[0] + if arg.is_Add and arg.is_commutative: + return Mul.fromiter(self.func(x) for x in arg.args) + elif isinstance(arg, Sum) and arg.is_commutative: + return Product(self.func(arg.function), *arg.limits) + return self.func(arg) + + +class exp_polar(ExpBase): + r""" + Represent a *polar number* (see g-function Sphinx documentation). + + Explanation + =========== + + ``exp_polar`` represents the function + `Exp: \mathbb{C} \rightarrow \mathcal{S}`, sending the complex number + `z = a + bi` to the polar number `r = exp(a), \theta = b`. It is one of + the main functions to construct polar numbers. + + Examples + ======== + + >>> from sympy import exp_polar, pi, I, exp + + The main difference is that polar numbers do not "wrap around" at `2 \pi`: + + >>> exp(2*pi*I) + 1 + >>> exp_polar(2*pi*I) + exp_polar(2*I*pi) + + apart from that they behave mostly like classical complex numbers: + + >>> exp_polar(2)*exp_polar(3) + exp_polar(5) + + See Also + ======== + + sympy.simplify.powsimp.powsimp + polar_lift + periodic_argument + principal_branch + """ + + is_polar = True + is_comparable = False # cannot be evalf'd + + def _eval_Abs(self): # Abs is never a polar number + return exp(re(self.args[0])) + + def _eval_evalf(self, prec): + """ Careful! any evalf of polar numbers is flaky """ + i = im(self.args[0]) + try: + bad = (i <= -pi or i > pi) + except TypeError: + bad = True + if bad: + return self # cannot evalf for this argument + res = exp(self.args[0])._eval_evalf(prec) + if i > 0 and im(res) < 0: + # i ~ pi, but exp(I*i) evaluated to argument slightly bigger than pi + return re(res) + return res + + def _eval_power(self, other): + return self.func(self.args[0]*other) + + def _eval_is_extended_real(self): + if self.args[0].is_extended_real: + return True + + def as_base_exp(self): + # XXX exp_polar(0) is special! + if self.args[0] == 0: + return self, S.One + return ExpBase.as_base_exp(self) + + +class ExpMeta(FunctionClass): + def __instancecheck__(cls, instance): + if exp in instance.__class__.__mro__: + return True + return isinstance(instance, Pow) and instance.base is S.Exp1 + + +class exp(ExpBase, metaclass=ExpMeta): + """ + The exponential function, :math:`e^x`. + + Examples + ======== + + >>> from sympy import exp, I, pi + >>> from sympy.abc import x + >>> exp(x) + exp(x) + >>> exp(x).diff(x) + exp(x) + >>> exp(I*pi) + -1 + + Parameters + ========== + + arg : Expr + + See Also + ======== + + log + """ + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex == 1: + return self + else: + raise ArgumentIndexError(self, argindex) + + def _eval_refine(self, assumptions): + from sympy.assumptions import ask, Q + arg = self.args[0] + if arg.is_Mul: + Ioo = I*S.Infinity + if arg in [Ioo, -Ioo]: + return S.NaN + + coeff = arg.as_coefficient(pi*I) + if coeff: + if ask(Q.integer(2*coeff)): + if ask(Q.even(coeff)): + return S.One + elif ask(Q.odd(coeff)): + return S.NegativeOne + elif ask(Q.even(coeff + S.Half)): + return -I + elif ask(Q.odd(coeff + S.Half)): + return I + + @classmethod + def eval(cls, arg): + from sympy.calculus import AccumBounds + from sympy.matrices.matrixbase import MatrixBase + from sympy.sets.setexpr import SetExpr + from sympy.simplify.simplify import logcombine + if isinstance(arg, MatrixBase): + return arg.exp() + elif global_parameters.exp_is_pow: + return Pow(S.Exp1, arg) + elif arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg.is_zero: + return S.One + elif arg is S.One: + return S.Exp1 + elif arg is S.Infinity: + return S.Infinity + elif arg is S.NegativeInfinity: + return S.Zero + elif arg is S.ComplexInfinity: + return S.NaN + elif isinstance(arg, log): + return arg.args[0] + elif isinstance(arg, AccumBounds): + return AccumBounds(exp(arg.min), exp(arg.max)) + elif isinstance(arg, SetExpr): + return arg._eval_func(cls) + elif arg.is_Mul: + coeff = arg.as_coefficient(pi*I) + if coeff: + if (2*coeff).is_integer: + if coeff.is_even: + return S.One + elif coeff.is_odd: + return S.NegativeOne + elif (coeff + S.Half).is_even: + return -I + elif (coeff + S.Half).is_odd: + return I + elif coeff.is_Rational: + ncoeff = coeff % 2 # restrict to [0, 2pi) + if ncoeff > 1: # restrict to (-pi, pi] + ncoeff -= 2 + if ncoeff != coeff: + return cls(ncoeff*pi*I) + + # Warning: code in risch.py will be very sensitive to changes + # in this (see DifferentialExtension). + + # look for a single log factor + + coeff, terms = arg.as_coeff_Mul() + + # but it can't be multiplied by oo + if coeff in [S.NegativeInfinity, S.Infinity]: + if terms.is_number: + if coeff is S.NegativeInfinity: + terms = -terms + if re(terms).is_zero and terms is not S.Zero: + return S.NaN + if re(terms).is_positive and im(terms) is not S.Zero: + return S.ComplexInfinity + if re(terms).is_negative: + return S.Zero + return None + + coeffs, log_term = [coeff], None + for term in Mul.make_args(terms): + term_ = logcombine(term) + if isinstance(term_, log): + if log_term is None: + log_term = term_.args[0] + else: + return None + elif term.is_comparable: + coeffs.append(term) + else: + return None + + return log_term**Mul(*coeffs) if log_term else None + + elif arg.is_Add: + out = [] + add = [] + argchanged = False + for a in arg.args: + if a is S.One: + add.append(a) + continue + newa = cls(a) + if isinstance(newa, cls): + if newa.args[0] != a: + add.append(newa.args[0]) + argchanged = True + else: + add.append(a) + else: + out.append(newa) + if out or argchanged: + return Mul(*out)*cls(Add(*add), evaluate=False) + + if arg.is_zero: + return S.One + + @property + def base(self): + """ + Returns the base of the exponential function. + """ + return S.Exp1 + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + """ + Calculates the next term in the Taylor series expansion. + """ + if n < 0: + return S.Zero + if n == 0: + return S.One + x = sympify(x) + if previous_terms: + p = previous_terms[-1] + if p is not None: + return p * x / n + return x**n/factorial(n) + + def as_real_imag(self, deep=True, **hints): + """ + Returns this function as a 2-tuple representing a complex number. + + Examples + ======== + + >>> from sympy import exp, I + >>> from sympy.abc import x + >>> exp(x).as_real_imag() + (exp(re(x))*cos(im(x)), exp(re(x))*sin(im(x))) + >>> exp(1).as_real_imag() + (E, 0) + >>> exp(I).as_real_imag() + (cos(1), sin(1)) + >>> exp(1+I).as_real_imag() + (E*cos(1), E*sin(1)) + + See Also + ======== + + sympy.functions.elementary.complexes.re + sympy.functions.elementary.complexes.im + """ + from sympy.functions.elementary.trigonometric import cos, sin + re, im = self.args[0].as_real_imag() + if deep: + re = re.expand(deep, **hints) + im = im.expand(deep, **hints) + cos, sin = cos(im), sin(im) + return (exp(re)*cos, exp(re)*sin) + + def _eval_subs(self, old, new): + # keep processing of power-like args centralized in Pow + if old.is_Pow: # handle (exp(3*log(x))).subs(x**2, z) -> z**(3/2) + old = exp(old.exp*log(old.base)) + elif old is S.Exp1 and new.is_Function: + old = exp + if isinstance(old, exp) or old is S.Exp1: + f = lambda a: Pow(*a.as_base_exp(), evaluate=False) if ( + a.is_Pow or isinstance(a, exp)) else a + return Pow._eval_subs(f(self), f(old), new) + + if old is exp and not new.is_Function: + return new**self.exp._subs(old, new) + return super()._eval_subs(old, new) + + def _eval_is_extended_real(self): + if self.args[0].is_extended_real: + return True + elif self.args[0].is_imaginary: + arg2 = -S(2) * I * self.args[0] / pi + return arg2.is_even + + def _eval_is_complex(self): + def complex_extended_negative(arg): + yield arg.is_complex + yield arg.is_extended_negative + return fuzzy_or(complex_extended_negative(self.args[0])) + + def _eval_is_algebraic(self): + if (self.exp / pi / I).is_rational: + return True + if fuzzy_not(self.exp.is_zero): + if self.exp.is_algebraic: + return False + elif (self.exp / pi).is_rational: + return False + + def _eval_is_extended_positive(self): + if self.exp.is_extended_real: + return self.args[0] is not S.NegativeInfinity + elif self.exp.is_imaginary: + arg2 = -I * self.args[0] / pi + return arg2.is_even + + def _eval_nseries(self, x, n, logx, cdir=0): + # NOTE Please see the comment at the beginning of this file, labelled + # IMPORTANT. + from sympy.functions.elementary.complexes import sign + from sympy.functions.elementary.integers import ceiling + from sympy.series.limits import limit + from sympy.series.order import Order + from sympy.simplify.powsimp import powsimp + arg = self.exp + arg_series = arg._eval_nseries(x, n=n, logx=logx) + if arg_series.is_Order: + return 1 + arg_series + arg0 = limit(arg_series.removeO(), x, 0) + if arg0 is S.NegativeInfinity: + return Order(x**n, x) + if arg0 is S.Infinity: + return self + if arg0.is_infinite: + raise PoleError("Cannot expand %s around 0" % (self)) + # checking for indecisiveness/ sign terms in arg0 + if any(isinstance(arg, sign) for arg in arg0.args): + return self + t = Dummy("t") + nterms = n + try: + cf = Order(arg.as_leading_term(x, logx=logx), x).getn() + except (NotImplementedError, PoleError): + cf = 0 + if cf and cf > 0: + nterms = ceiling(n/cf) + exp_series = exp(t)._taylor(t, nterms) + r = exp(arg0)*exp_series.subs(t, arg_series - arg0) + rep = {logx: log(x)} if logx is not None else {} + if r.subs(rep) == self: + return r + if cf and cf > 1: + r += Order((arg_series - arg0)**n, x)/x**((cf-1)*n) + else: + r += Order((arg_series - arg0)**n, x) + r = r.expand() + r = powsimp(r, deep=True, combine='exp') + # powsimp may introduce unexpanded (-1)**Rational; see PR #17201 + simplerat = lambda x: x.is_Rational and x.q in [3, 4, 6] + w = Wild('w', properties=[simplerat]) + r = r.replace(S.NegativeOne**w, expand_complex(S.NegativeOne**w)) + return r + + def _taylor(self, x, n): + l = [] + g = None + for i in range(n): + g = self.taylor_term(i, self.args[0], g) + g = g.nseries(x, n=n) + l.append(g.removeO()) + return Add(*l) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.calculus.util import AccumBounds + arg = self.args[0].cancel().as_leading_term(x, logx=logx) + arg0 = arg.subs(x, 0) + if arg is S.NaN: + return S.NaN + if isinstance(arg0, AccumBounds): + # This check addresses a corner case involving AccumBounds. + # if isinstance(arg, AccumBounds) is True, then arg0 can either be 0, + # AccumBounds(-oo, 0) or AccumBounds(-oo, oo). + # Check out function: test_issue_18473() in test_exponential.py and + # test_limits.py for more information. + if re(cdir) < S.Zero: + return exp(-arg0) + return exp(arg0) + if arg0 is S.NaN: + arg0 = arg.limit(x, 0) + if arg0.is_infinite is False: + return exp(arg0) + raise PoleError("Cannot expand %s around 0" % (self)) + + def _eval_rewrite_as_sin(self, arg, **kwargs): + from sympy.functions.elementary.trigonometric import sin + return sin(I*arg + pi/2) - I*sin(I*arg) + + def _eval_rewrite_as_cos(self, arg, **kwargs): + from sympy.functions.elementary.trigonometric import cos + return cos(I*arg) + I*cos(I*arg + pi/2) + + def _eval_rewrite_as_tanh(self, arg, **kwargs): + from sympy.functions.elementary.hyperbolic import tanh + return (1 + tanh(arg/2))/(1 - tanh(arg/2)) + + def _eval_rewrite_as_sqrt(self, arg, **kwargs): + from sympy.functions.elementary.trigonometric import sin, cos + if arg.is_Mul: + coeff = arg.coeff(pi*I) + if coeff and coeff.is_number: + cosine, sine = cos(pi*coeff), sin(pi*coeff) + if not isinstance(cosine, cos) and not isinstance (sine, sin): + return cosine + I*sine + + def _eval_rewrite_as_Pow(self, arg, **kwargs): + if arg.is_Mul: + logs = [a for a in arg.args if isinstance(a, log) and len(a.args) == 1] + if logs: + return Pow(logs[0].args[0], arg.coeff(logs[0])) + + +def match_real_imag(expr): + r""" + Try to match expr with $a + Ib$ for real $a$ and $b$. + + ``match_real_imag`` returns a tuple containing the real and imaginary + parts of expr or ``(None, None)`` if direct matching is not possible. Contrary + to :func:`~.re`, :func:`~.im``, and ``as_real_imag()``, this helper will not force things + by returning expressions themselves containing ``re()`` or ``im()`` and it + does not expand its argument either. + + """ + r_, i_ = expr.as_independent(I, as_Add=True) + if i_ == 0 and r_.is_real: + return (r_, i_) + i_ = i_.as_coefficient(I) + if i_ and i_.is_real and r_.is_real: + return (r_, i_) + else: + return (None, None) # simpler to check for than None + + +class log(DefinedFunction): + r""" + The natural logarithm function `\ln(x)` or `\log(x)`. + + Explanation + =========== + + Logarithms are taken with the natural base, `e`. To get + a logarithm of a different base ``b``, use ``log(x, b)``, + which is essentially short-hand for ``log(x)/log(b)``. + + ``log`` represents the principal branch of the natural + logarithm. As such it has a branch cut along the negative + real axis and returns values having a complex argument in + `(-\pi, \pi]`. + + Examples + ======== + + >>> from sympy import log, sqrt, S, I + >>> log(8, 2) + 3 + >>> log(S(8)/3, 2) + -log(3)/log(2) + 3 + >>> log(-1 + I*sqrt(3)) + log(2) + 2*I*pi/3 + + See Also + ======== + + exp + + """ + + args: tuple[Expr] + + _singularities = (S.Zero, S.ComplexInfinity) + + def fdiff(self, argindex=1): + """ + Returns the first derivative of the function. + """ + if argindex == 1: + return 1/self.args[0] + else: + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + r""" + Returns `e^x`, the inverse function of `\log(x)`. + """ + return exp + + @classmethod + def eval(cls, arg, base=None): + from sympy.calculus import AccumBounds + from sympy.sets.setexpr import SetExpr + + arg = sympify(arg) + + if base is not None: + base = sympify(base) + if base == 1: + if arg == 1: + return S.NaN + else: + return S.ComplexInfinity + try: + # handle extraction of powers of the base now + # or else expand_log in Mul would have to handle this + n = multiplicity(base, arg) + if n: + return n + log(arg / base**n) / log(base) + else: + return log(arg)/log(base) + except ValueError: + pass + if base is not S.Exp1: + return cls(arg)/cls(base) + else: + return cls(arg) + + if arg.is_Number: + if arg.is_zero: + return S.ComplexInfinity + elif arg is S.One: + return S.Zero + elif arg is S.Infinity: + return S.Infinity + elif arg is S.NegativeInfinity: + return S.Infinity + elif arg is S.NaN: + return S.NaN + elif arg.is_Rational and arg.p == 1: + return -cls(arg.q) + + if arg.is_Pow and arg.base is S.Exp1 and arg.exp.is_extended_real: + return arg.exp + if isinstance(arg, exp) and arg.exp.is_extended_real: + return arg.exp + elif isinstance(arg, exp) and arg.exp.is_number: + r_, i_ = match_real_imag(arg.exp) + if i_ and i_.is_comparable: + i_ %= 2*pi + if i_ > pi: + i_ -= 2*pi + return r_ + expand_mul(i_ * I, deep=False) + elif isinstance(arg, exp_polar): + return unpolarify(arg.exp) + elif isinstance(arg, AccumBounds): + if arg.min.is_positive: + return AccumBounds(log(arg.min), log(arg.max)) + elif arg.min.is_zero: + return AccumBounds(S.NegativeInfinity, log(arg.max)) + else: + return S.NaN + elif isinstance(arg, SetExpr): + return arg._eval_func(cls) + + if arg.is_number: + if arg.is_negative: + return pi * I + cls(-arg) + elif arg is S.ComplexInfinity: + return S.ComplexInfinity + elif arg is S.Exp1: + return S.One + + if arg.is_zero: + return S.ComplexInfinity + + # don't autoexpand Pow or Mul (see the issue 3351): + if not arg.is_Add: + coeff = arg.as_coefficient(I) + + if coeff is not None: + if coeff is S.Infinity: + return S.Infinity + elif coeff is S.NegativeInfinity: + return S.Infinity + elif coeff.is_Rational: + if coeff.is_nonnegative: + return pi * I * S.Half + cls(coeff) + else: + return -pi * I * S.Half + cls(-coeff) + + if arg.is_number and arg.is_algebraic: + # Match arg = coeff*(r_ + i_*I) with coeff>0, r_ and i_ real. + coeff, arg_ = arg.as_independent(I, as_Add=False) + if coeff.is_negative: + coeff *= -1 + arg_ *= -1 + arg_ = expand_mul(arg_, deep=False) + r_, i_ = arg_.as_independent(I, as_Add=True) + i_ = i_.as_coefficient(I) + if coeff.is_real and i_ and i_.is_real and r_.is_real: + if r_.is_zero: + if i_.is_positive: + return pi * I * S.Half + cls(coeff * i_) + elif i_.is_negative: + return -pi * I * S.Half + cls(coeff * -i_) + else: + from sympy.simplify import ratsimp + # Check for arguments involving rational multiples of pi + t = (i_/r_).cancel() + t1 = (-t).cancel() + atan_table = _log_atan_table() + if t in atan_table: + modulus = ratsimp(coeff * Abs(arg_)) + if r_.is_positive: + return cls(modulus) + I * atan_table[t] + else: + return cls(modulus) + I * (atan_table[t] - pi) + elif t1 in atan_table: + modulus = ratsimp(coeff * Abs(arg_)) + if r_.is_positive: + return cls(modulus) + I * (-atan_table[t1]) + else: + return cls(modulus) + I * (pi - atan_table[t1]) + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): # of log(1+x) + r""" + Returns the next term in the Taylor series expansion of `\log(1+x)`. + """ + from sympy.simplify.powsimp import powsimp + if n < 0: + return S.Zero + x = sympify(x) + if n == 0: + return x + if previous_terms: + p = previous_terms[-1] + if p is not None: + return powsimp((-n) * p * x / (n + 1), deep=True, combine='exp') + return (1 - 2*(n % 2)) * x**(n + 1)/(n + 1) + + def _eval_expand_log(self, deep=True, **hints): + from sympy.concrete import Sum, Product + force = hints.get('force', False) + factor = hints.get('factor', False) + if (len(self.args) == 2): + return expand_log(self.func(*self.args), deep=deep, force=force) + arg = self.args[0] + if arg.is_Integer: + # remove perfect powers + p = perfect_power(arg) + logarg = None + coeff = 1 + if p is not False: + arg, coeff = p + logarg = self.func(arg) + # expand as product of its prime factors if factor=True + if factor: + p = factorint(arg) + if arg not in p.keys(): + logarg = sum(n*log(val) for val, n in p.items()) + if logarg is not None: + return coeff*logarg + elif arg.is_Rational: + return log(arg.p) - log(arg.q) + elif arg.is_Mul: + expr = [] + nonpos = [] + for x in arg.args: + if force or x.is_positive or x.is_polar: + a = self.func(x) + if isinstance(a, log): + expr.append(self.func(x)._eval_expand_log(**hints)) + else: + expr.append(a) + elif x.is_negative: + a = self.func(-x) + expr.append(a) + nonpos.append(S.NegativeOne) + else: + nonpos.append(x) + return Add(*expr) + log(Mul(*nonpos)) + elif arg.is_Pow or isinstance(arg, exp): + if force or (arg.exp.is_extended_real and (arg.base.is_positive or ((arg.exp+1) + .is_positive and (arg.exp-1).is_nonpositive))) or arg.base.is_polar: + b = arg.base + e = arg.exp + a = self.func(b) + if isinstance(a, log): + return unpolarify(e) * a._eval_expand_log(**hints) + else: + return unpolarify(e) * a + elif isinstance(arg, Product): + if force or arg.function.is_positive: + return Sum(log(arg.function), *arg.limits) + + return self.func(arg) + + def _eval_simplify(self, **kwargs): + from sympy.simplify.simplify import expand_log, simplify, inversecombine + if len(self.args) == 2: # it's unevaluated + return simplify(self.func(*self.args), **kwargs) + + expr = self.func(simplify(self.args[0], **kwargs)) + if kwargs['inverse']: + expr = inversecombine(expr) + expr = expand_log(expr, deep=True) + return min([expr, self], key=kwargs['measure']) + + def as_real_imag(self, deep=True, **hints): + """ + Returns this function as a complex coordinate. + + Examples + ======== + + >>> from sympy import I, log + >>> from sympy.abc import x + >>> log(x).as_real_imag() + (log(Abs(x)), arg(x)) + >>> log(I).as_real_imag() + (0, pi/2) + >>> log(1 + I).as_real_imag() + (log(sqrt(2)), pi/4) + >>> log(I*x).as_real_imag() + (log(Abs(x)), arg(I*x)) + + """ + sarg = self.args[0] + if deep: + sarg = self.args[0].expand(deep, **hints) + sarg_abs = Abs(sarg) + if sarg_abs == sarg: + return self, S.Zero + sarg_arg = arg(sarg) + if hints.get('log', False): # Expand the log + hints['complex'] = False + return (log(sarg_abs).expand(deep, **hints), sarg_arg) + else: + return log(sarg_abs), sarg_arg + + def _eval_is_rational(self): + s = self.func(*self.args) + if s.func == self.func: + if (self.args[0] - 1).is_zero: + return True + if s.args[0].is_rational and fuzzy_not((self.args[0] - 1).is_zero): + return False + else: + return s.is_rational + + def _eval_is_algebraic(self): + s = self.func(*self.args) + if s.func == self.func: + if (self.args[0] - 1).is_zero: + return True + elif fuzzy_not((self.args[0] - 1).is_zero): + if self.args[0].is_algebraic: + return False + else: + return s.is_algebraic + + def _eval_is_extended_real(self): + return self.args[0].is_extended_positive + + def _eval_is_complex(self): + z = self.args[0] + return fuzzy_and([z.is_complex, fuzzy_not(z.is_zero)]) + + def _eval_is_finite(self): + arg = self.args[0] + if arg.is_zero: + return False + return arg.is_finite + + def _eval_is_extended_positive(self): + return (self.args[0] - 1).is_extended_positive + + def _eval_is_zero(self): + return (self.args[0] - 1).is_zero + + def _eval_is_extended_nonnegative(self): + return (self.args[0] - 1).is_extended_nonnegative + + def _eval_nseries(self, x, n, logx, cdir=0): + # NOTE Please see the comment at the beginning of this file, labelled + # IMPORTANT. + from sympy.series.order import Order + from sympy.simplify.simplify import logcombine + from sympy.core.symbol import Dummy + + if self.args[0] == x: + return log(x) if logx is None else logx + arg = self.args[0] + t = Dummy('t', positive=True) + if cdir == 0: + cdir = 1 + z = arg.subs(x, cdir*t) + + k, l = Wild("k"), Wild("l") + r = z.match(k*t**l) + if r is not None: + k, l = r[k], r[l] + if l != 0 and not l.has(t) and not k.has(t): + r = l*log(x) if logx is None else l*logx + r += log(k) - l*log(cdir) # XXX true regardless of assumptions? + return r + + def coeff_exp(term, x): + coeff, exp = S.One, S.Zero + for factor in Mul.make_args(term): + if factor.has(x): + base, exp = factor.as_base_exp() + if base != x: + try: + return term.leadterm(x) + except ValueError: + return term, S.Zero + else: + coeff *= factor + return coeff, exp + + # TODO new and probably slow + try: + a, b = z.leadterm(t, logx=logx, cdir=1) + except (ValueError, NotImplementedError, PoleError): + s = z._eval_nseries(t, n=n, logx=logx, cdir=1) + while s.is_Order: + n += 1 + s = z._eval_nseries(t, n=n, logx=logx, cdir=1) + try: + a, b = s.removeO().leadterm(t, cdir=1) + except ValueError: + a, b = s.removeO().as_leading_term(t, cdir=1), S.Zero + + p = (z/(a*t**b) - 1).cancel()._eval_nseries(t, n=n, logx=logx, cdir=1) + if p.has(exp): + p = logcombine(p) + if isinstance(p, Order): + n = p.getn() + _, d = coeff_exp(p, t) + logx = log(x) if logx is None else logx + + if not d.is_positive: + res = log(a) - b*log(cdir) + b*logx + _res = res + logflags = {"deep": True, "log": True, "mul": False, "power_exp": False, + "power_base": False, "multinomial": False, "basic": False, "force": True, + "factor": False} + expr = self.expand(**logflags) + if (not a.could_extract_minus_sign() and + logx.could_extract_minus_sign()): + _res = _res.subs(-logx, -log(x)).expand(**logflags) + else: + _res = _res.subs(logx, log(x)).expand(**logflags) + if _res == expr: + return res + return res + Order(x**n, x) + + def mul(d1, d2): + res = {} + for e1, e2 in product(d1, d2): + ex = e1 + e2 + if ex < n: + res[ex] = res.get(ex, S.Zero) + d1[e1]*d2[e2] + return res + + pterms = {} + + for term in Add.make_args(p.removeO()): + co1, e1 = coeff_exp(term, t) + pterms[e1] = pterms.get(e1, S.Zero) + co1 + + k = S.One + terms = {} + pk = pterms + + while k*d < n: + coeff = -S.NegativeOne**k/k + for ex in pk: + terms[ex] = terms.get(ex, S.Zero) + coeff*pk[ex] + pk = mul(pk, pterms) + k += S.One + + res = log(a) - b*log(cdir) + b*logx + for ex in terms: + res += terms[ex].cancel()*t**(ex) + + if a.is_negative and im(z) != 0: + from sympy.functions.special.delta_functions import Heaviside + for i, term in enumerate(z.lseries(t)): + if not term.is_real or i == 5: + break + if i < 5: + coeff, _ = term.as_coeff_exponent(t) + res += -2*I*pi*Heaviside(-im(coeff), 0) + + res = res.subs(t, x/cdir) + return res + Order(x**n, x) + + def _eval_as_leading_term(self, x, logx, cdir): + # NOTE + # Refer https://github.com/sympy/sympy/pull/23592 for more information + # on each of the following steps involved in this method. + arg0 = self.args[0].together() + + # STEP 1 + t = Dummy('t', positive=True) + if cdir == 0: + cdir = 1 + z = arg0.subs(x, cdir*t) + + # STEP 2 + try: + c, e = z.leadterm(t, logx=logx, cdir=1) + except ValueError: + arg = arg0.as_leading_term(x, logx=logx, cdir=cdir) + return log(arg) + if c.has(t): + c = c.subs(t, x/cdir) + if e != 0: + raise PoleError("Cannot expand %s around 0" % (self)) + return log(c) + + # STEP 3 + if c == S.One and e == S.Zero: + return (arg0 - S.One).as_leading_term(x, logx=logx) + + # STEP 4 + res = log(c) - e*log(cdir) + logx = log(x) if logx is None else logx + res += e*logx + + # STEP 5 + if c.is_negative and im(z) != 0: + from sympy.functions.special.delta_functions import Heaviside + for i, term in enumerate(z.lseries(t)): + if not term.is_real or i == 5: + break + if i < 5: + coeff, _ = term.as_coeff_exponent(t) + res += -2*I*pi*Heaviside(-im(coeff), 0) + return res + + +class LambertW(DefinedFunction): + r""" + The Lambert W function $W(z)$ is defined as the inverse + function of $w \exp(w)$ [1]_. + + Explanation + =========== + + In other words, the value of $W(z)$ is such that $z = W(z) \exp(W(z))$ + for any complex number $z$. The Lambert W function is a multivalued + function with infinitely many branches $W_k(z)$, indexed by + $k \in \mathbb{Z}$. Each branch gives a different solution $w$ + of the equation $z = w \exp(w)$. + + The Lambert W function has two partially real branches: the + principal branch ($k = 0$) is real for real $z > -1/e$, and the + $k = -1$ branch is real for $-1/e < z < 0$. All branches except + $k = 0$ have a logarithmic singularity at $z = 0$. + + Examples + ======== + + >>> from sympy import LambertW + >>> LambertW(1.2) + 0.635564016364870 + >>> LambertW(1.2, -1).n() + -1.34747534407696 - 4.41624341514535*I + >>> LambertW(-1).is_real + False + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Lambert_W_function + """ + _singularities = (-Pow(S.Exp1, -1, evaluate=False), S.ComplexInfinity) + + @classmethod + def eval(cls, x, k=None): + if k == S.Zero: + return cls(x) + elif k is None: + k = S.Zero + + if k.is_zero: + if x.is_zero: + return S.Zero + if x is S.Exp1: + return S.One + if x == -1/S.Exp1: + return S.NegativeOne + if x == -log(2)/2: + return -log(2) + if x == 2*log(2): + return log(2) + if x == -pi/2: + return I*pi/2 + if x == exp(1 + S.Exp1): + return S.Exp1 + if x is S.Infinity: + return S.Infinity + + if fuzzy_not(k.is_zero): + if x.is_zero: + return S.NegativeInfinity + if k is S.NegativeOne: + if x == -pi/2: + return -I*pi/2 + elif x == -1/S.Exp1: + return S.NegativeOne + elif x == -2*exp(-2): + return -Integer(2) + + def fdiff(self, argindex=1): + """ + Return the first derivative of this function. + """ + x = self.args[0] + + if len(self.args) == 1: + if argindex == 1: + return LambertW(x)/(x*(1 + LambertW(x))) + else: + k = self.args[1] + if argindex == 1: + return LambertW(x, k)/(x*(1 + LambertW(x, k))) + + raise ArgumentIndexError(self, argindex) + + def _eval_is_extended_real(self): + x = self.args[0] + if len(self.args) == 1: + k = S.Zero + else: + k = self.args[1] + if k.is_zero: + if (x + 1/S.Exp1).is_positive: + return True + elif (x + 1/S.Exp1).is_nonpositive: + return False + elif (k + 1).is_zero: + if x.is_negative and (x + 1/S.Exp1).is_positive: + return True + elif x.is_nonpositive or (x + 1/S.Exp1).is_nonnegative: + return False + elif fuzzy_not(k.is_zero) and fuzzy_not((k + 1).is_zero): + if x.is_extended_real: + return False + + def _eval_is_finite(self): + return self.args[0].is_finite + + def _eval_is_algebraic(self): + s = self.func(*self.args) + if s.func == self.func: + if fuzzy_not(self.args[0].is_zero) and self.args[0].is_algebraic: + return False + else: + return s.is_algebraic + + def _eval_as_leading_term(self, x, logx, cdir): + if len(self.args) == 1: + arg = self.args[0] + arg0 = arg.subs(x, 0).cancel() + if not arg0.is_zero: + return self.func(arg0) + return arg.as_leading_term(x) + + def _eval_nseries(self, x, n, logx, cdir=0): + if len(self.args) == 1: + from sympy.functions.elementary.integers import ceiling + from sympy.series.order import Order + arg = self.args[0].nseries(x, n=n, logx=logx) + lt = arg.as_leading_term(x, logx=logx) + lte = 1 + if lt.is_Pow: + lte = lt.exp + if ceiling(n/lte) >= 1: + s = Add(*[(-S.One)**(k - 1)*Integer(k)**(k - 2)/ + factorial(k - 1)*arg**k for k in range(1, ceiling(n/lte))]) + s = expand_multinomial(s) + else: + s = S.Zero + + return s + Order(x**n, x) + return super()._eval_nseries(x, n, logx) + + def _eval_is_zero(self): + x = self.args[0] + if len(self.args) == 1: + return x.is_zero + else: + return fuzzy_and([x.is_zero, self.args[1].is_zero]) + + +@cacheit +def _log_atan_table(): + return { + # first quadrant only + sqrt(3): pi / 3, + 1: pi / 4, + sqrt(5 - 2 * sqrt(5)): pi / 5, + sqrt(2) * sqrt(5 - sqrt(5)) / (1 + sqrt(5)): pi / 5, + sqrt(5 + 2 * sqrt(5)): pi * Rational(2, 5), + sqrt(2) * sqrt(sqrt(5) + 5) / (-1 + sqrt(5)): pi * Rational(2, 5), + sqrt(3) / 3: pi / 6, + sqrt(2) - 1: pi / 8, + sqrt(2 - sqrt(2)) / sqrt(sqrt(2) + 2): pi / 8, + sqrt(2) + 1: pi * Rational(3, 8), + sqrt(sqrt(2) + 2) / sqrt(2 - sqrt(2)): pi * Rational(3, 8), + sqrt(1 - 2 * sqrt(5) / 5): pi / 10, + (-sqrt(2) + sqrt(10)) / (2 * sqrt(sqrt(5) + 5)): pi / 10, + sqrt(1 + 2 * sqrt(5) / 5): pi * Rational(3, 10), + (sqrt(2) + sqrt(10)) / (2 * sqrt(5 - sqrt(5))): pi * Rational(3, 10), + 2 - sqrt(3): pi / 12, + (-1 + sqrt(3)) / (1 + sqrt(3)): pi / 12, + 2 + sqrt(3): pi * Rational(5, 12), + (1 + sqrt(3)) / (-1 + sqrt(3)): pi * Rational(5, 12) + } diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/hyperbolic.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/hyperbolic.py new file mode 100644 index 0000000000000000000000000000000000000000..1031d035373bb641d26e61a395e6048906285bfe --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/hyperbolic.py @@ -0,0 +1,2285 @@ +from sympy.core import S, sympify, cacheit +from sympy.core.add import Add +from sympy.core.function import DefinedFunction, ArgumentIndexError +from sympy.core.logic import fuzzy_or, fuzzy_and, fuzzy_not, FuzzyBool +from sympy.core.numbers import I, pi, Rational +from sympy.core.symbol import Dummy +from sympy.functions.combinatorial.factorials import (binomial, factorial, + RisingFactorial) +from sympy.functions.combinatorial.numbers import bernoulli, euler, nC +from sympy.functions.elementary.complexes import Abs, im, re +from sympy.functions.elementary.exponential import exp, log, match_real_imag +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import ( + acos, acot, asin, atan, cos, cot, csc, sec, sin, tan, + _imaginary_unit_as_coefficient) +from sympy.polys.specialpolys import symmetric_poly + + +def _rewrite_hyperbolics_as_exp(expr): + return expr.xreplace({h: h.rewrite(exp) + for h in expr.atoms(HyperbolicFunction)}) + + +@cacheit +def _acosh_table(): + return { + I: log(I*(1 + sqrt(2))), + -I: log(-I*(1 + sqrt(2))), + S.Half: pi/3, + Rational(-1, 2): pi*Rational(2, 3), + sqrt(2)/2: pi/4, + -sqrt(2)/2: pi*Rational(3, 4), + 1/sqrt(2): pi/4, + -1/sqrt(2): pi*Rational(3, 4), + sqrt(3)/2: pi/6, + -sqrt(3)/2: pi*Rational(5, 6), + (sqrt(3) - 1)/sqrt(2**3): pi*Rational(5, 12), + -(sqrt(3) - 1)/sqrt(2**3): pi*Rational(7, 12), + sqrt(2 + sqrt(2))/2: pi/8, + -sqrt(2 + sqrt(2))/2: pi*Rational(7, 8), + sqrt(2 - sqrt(2))/2: pi*Rational(3, 8), + -sqrt(2 - sqrt(2))/2: pi*Rational(5, 8), + (1 + sqrt(3))/(2*sqrt(2)): pi/12, + -(1 + sqrt(3))/(2*sqrt(2)): pi*Rational(11, 12), + (sqrt(5) + 1)/4: pi/5, + -(sqrt(5) + 1)/4: pi*Rational(4, 5) + } + + +@cacheit +def _acsch_table(): + return { + I: -pi / 2, + I*(sqrt(2) + sqrt(6)): -pi / 12, + I*(1 + sqrt(5)): -pi / 10, + I*2 / sqrt(2 - sqrt(2)): -pi / 8, + I*2: -pi / 6, + I*sqrt(2 + 2/sqrt(5)): -pi / 5, + I*sqrt(2): -pi / 4, + I*(sqrt(5)-1): -3*pi / 10, + I*2 / sqrt(3): -pi / 3, + I*2 / sqrt(2 + sqrt(2)): -3*pi / 8, + I*sqrt(2 - 2/sqrt(5)): -2*pi / 5, + I*(sqrt(6) - sqrt(2)): -5*pi / 12, + S(2): -I*log((1+sqrt(5))/2), + } + + +@cacheit +def _asech_table(): + return { + I: - (pi*I / 2) + log(1 + sqrt(2)), + -I: (pi*I / 2) + log(1 + sqrt(2)), + (sqrt(6) - sqrt(2)): pi / 12, + (sqrt(2) - sqrt(6)): 11*pi / 12, + sqrt(2 - 2/sqrt(5)): pi / 10, + -sqrt(2 - 2/sqrt(5)): 9*pi / 10, + 2 / sqrt(2 + sqrt(2)): pi / 8, + -2 / sqrt(2 + sqrt(2)): 7*pi / 8, + 2 / sqrt(3): pi / 6, + -2 / sqrt(3): 5*pi / 6, + (sqrt(5) - 1): pi / 5, + (1 - sqrt(5)): 4*pi / 5, + sqrt(2): pi / 4, + -sqrt(2): 3*pi / 4, + sqrt(2 + 2/sqrt(5)): 3*pi / 10, + -sqrt(2 + 2/sqrt(5)): 7*pi / 10, + S(2): pi / 3, + -S(2): 2*pi / 3, + sqrt(2*(2 + sqrt(2))): 3*pi / 8, + -sqrt(2*(2 + sqrt(2))): 5*pi / 8, + (1 + sqrt(5)): 2*pi / 5, + (-1 - sqrt(5)): 3*pi / 5, + (sqrt(6) + sqrt(2)): 5*pi / 12, + (-sqrt(6) - sqrt(2)): 7*pi / 12, + I*S.Infinity: -pi*I / 2, + I*S.NegativeInfinity: pi*I / 2, + } + +############################################################################### +########################### HYPERBOLIC FUNCTIONS ############################## +############################################################################### + + +class HyperbolicFunction(DefinedFunction): + """ + Base class for hyperbolic functions. + + See Also + ======== + + sinh, cosh, tanh, coth + """ + + unbranched = True + + +def _peeloff_ipi(arg): + r""" + Split ARG into two parts, a "rest" and a multiple of $I\pi$. + This assumes ARG to be an ``Add``. + The multiple of $I\pi$ returned in the second position is always a ``Rational``. + + Examples + ======== + + >>> from sympy.functions.elementary.hyperbolic import _peeloff_ipi as peel + >>> from sympy import pi, I + >>> from sympy.abc import x, y + >>> peel(x + I*pi/2) + (x, 1/2) + >>> peel(x + I*2*pi/3 + I*pi*y) + (x + I*pi*y + I*pi/6, 1/2) + """ + ipi = pi*I + for a in Add.make_args(arg): + if a == ipi: + K = S.One + break + elif a.is_Mul: + K, p = a.as_two_terms() + if p == ipi and K.is_Rational: + break + else: + return arg, S.Zero + + m1 = (K % S.Half) + m2 = K - m1 + return arg - m2*ipi, m2 + + +class sinh(HyperbolicFunction): + r""" + ``sinh(x)`` is the hyperbolic sine of ``x``. + + The hyperbolic sine function is $\frac{e^x - e^{-x}}{2}$. + + Examples + ======== + + >>> from sympy import sinh + >>> from sympy.abc import x + >>> sinh(x) + sinh(x) + + See Also + ======== + + cosh, tanh, asinh + """ + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function. + """ + if argindex == 1: + return cosh(self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return asinh + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.Infinity + elif arg is S.NegativeInfinity: + return S.NegativeInfinity + elif arg.is_zero: + return S.Zero + elif arg.is_negative: + return -cls(-arg) + else: + if arg is S.ComplexInfinity: + return S.NaN + + i_coeff = _imaginary_unit_as_coefficient(arg) + + if i_coeff is not None: + return I * sin(i_coeff) + else: + if arg.could_extract_minus_sign(): + return -cls(-arg) + + if arg.is_Add: + x, m = _peeloff_ipi(arg) + if m: + m = m*pi*I + return sinh(m)*cosh(x) + cosh(m)*sinh(x) + + if arg.is_zero: + return S.Zero + + if arg.func == asinh: + return arg.args[0] + + if arg.func == acosh: + x = arg.args[0] + return sqrt(x - 1) * sqrt(x + 1) + + if arg.func == atanh: + x = arg.args[0] + return x/sqrt(1 - x**2) + + if arg.func == acoth: + x = arg.args[0] + return 1/(sqrt(x - 1) * sqrt(x + 1)) + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + """ + Returns the next term in the Taylor series expansion. + """ + if n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + + if len(previous_terms) > 2: + p = previous_terms[-2] + return p * x**2 / (n*(n - 1)) + else: + return x**(n) / factorial(n) + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def as_real_imag(self, deep=True, **hints): + """ + Returns this function as a complex coordinate. + """ + if self.args[0].is_extended_real: + if deep: + hints['complex'] = False + return (self.expand(deep, **hints), S.Zero) + else: + return (self, S.Zero) + if deep: + re, im = self.args[0].expand(deep, **hints).as_real_imag() + else: + re, im = self.args[0].as_real_imag() + return (sinh(re)*cos(im), cosh(re)*sin(im)) + + def _eval_expand_complex(self, deep=True, **hints): + re_part, im_part = self.as_real_imag(deep=deep, **hints) + return re_part + im_part*I + + def _eval_expand_trig(self, deep=True, **hints): + if deep: + arg = self.args[0].expand(deep, **hints) + else: + arg = self.args[0] + x = None + if arg.is_Add: # TODO, implement more if deep stuff here + x, y = arg.as_two_terms() + else: + coeff, terms = arg.as_coeff_Mul(rational=True) + if coeff is not S.One and coeff.is_Integer and terms is not S.One: + x = terms + y = (coeff - 1)*x + if x is not None: + return (sinh(x)*cosh(y) + sinh(y)*cosh(x)).expand(trig=True) + return sinh(arg) + + def _eval_rewrite_as_tractable(self, arg, limitvar=None, **kwargs): + return (exp(arg) - exp(-arg)) / 2 + + def _eval_rewrite_as_exp(self, arg, **kwargs): + return (exp(arg) - exp(-arg)) / 2 + + def _eval_rewrite_as_sin(self, arg, **kwargs): + return -I * sin(I * arg) + + def _eval_rewrite_as_csc(self, arg, **kwargs): + return -I / csc(I * arg) + + def _eval_rewrite_as_cosh(self, arg, **kwargs): + return -I*cosh(arg + pi*I/2) + + def _eval_rewrite_as_tanh(self, arg, **kwargs): + tanh_half = tanh(S.Half*arg) + return 2*tanh_half/(1 - tanh_half**2) + + def _eval_rewrite_as_coth(self, arg, **kwargs): + coth_half = coth(S.Half*arg) + return 2*coth_half/(coth_half**2 - 1) + + def _eval_rewrite_as_csch(self, arg, **kwargs): + return 1 / csch(arg) + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0].as_leading_term(x, logx=logx, cdir=cdir) + arg0 = arg.subs(x, 0) + + if arg0 is S.NaN: + arg0 = arg.limit(x, 0, dir='-' if cdir.is_negative else '+') + if arg0.is_zero: + return arg + elif arg0.is_finite: + return self.func(arg0) + else: + return self + + def _eval_is_real(self): + arg = self.args[0] + if arg.is_real: + return True + + # if `im` is of the form n*pi + # else, check if it is a number + re, im = arg.as_real_imag() + return (im%pi).is_zero + + def _eval_is_extended_real(self): + if self.args[0].is_extended_real: + return True + + def _eval_is_positive(self): + if self.args[0].is_extended_real: + return self.args[0].is_positive + + def _eval_is_negative(self): + if self.args[0].is_extended_real: + return self.args[0].is_negative + + def _eval_is_finite(self): + arg = self.args[0] + return arg.is_finite + + def _eval_is_zero(self): + rest, ipi_mult = _peeloff_ipi(self.args[0]) + if rest.is_zero: + return ipi_mult.is_integer + + +class cosh(HyperbolicFunction): + r""" + ``cosh(x)`` is the hyperbolic cosine of ``x``. + + The hyperbolic cosine function is $\frac{e^x + e^{-x}}{2}$. + + Examples + ======== + + >>> from sympy import cosh + >>> from sympy.abc import x + >>> cosh(x) + cosh(x) + + See Also + ======== + + sinh, tanh, acosh + """ + + def fdiff(self, argindex=1): + if argindex == 1: + return sinh(self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, arg): + from sympy.functions.elementary.trigonometric import cos + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.Infinity + elif arg is S.NegativeInfinity: + return S.Infinity + elif arg.is_zero: + return S.One + elif arg.is_negative: + return cls(-arg) + else: + if arg is S.ComplexInfinity: + return S.NaN + + i_coeff = _imaginary_unit_as_coefficient(arg) + + if i_coeff is not None: + return cos(i_coeff) + else: + if arg.could_extract_minus_sign(): + return cls(-arg) + + if arg.is_Add: + x, m = _peeloff_ipi(arg) + if m: + m = m*pi*I + return cosh(m)*cosh(x) + sinh(m)*sinh(x) + + if arg.is_zero: + return S.One + + if arg.func == asinh: + return sqrt(1 + arg.args[0]**2) + + if arg.func == acosh: + return arg.args[0] + + if arg.func == atanh: + return 1/sqrt(1 - arg.args[0]**2) + + if arg.func == acoth: + x = arg.args[0] + return x/(sqrt(x - 1) * sqrt(x + 1)) + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0 or n % 2 == 1: + return S.Zero + else: + x = sympify(x) + + if len(previous_terms) > 2: + p = previous_terms[-2] + return p * x**2 / (n*(n - 1)) + else: + return x**(n)/factorial(n) + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def as_real_imag(self, deep=True, **hints): + if self.args[0].is_extended_real: + if deep: + hints['complex'] = False + return (self.expand(deep, **hints), S.Zero) + else: + return (self, S.Zero) + if deep: + re, im = self.args[0].expand(deep, **hints).as_real_imag() + else: + re, im = self.args[0].as_real_imag() + + return (cosh(re)*cos(im), sinh(re)*sin(im)) + + def _eval_expand_complex(self, deep=True, **hints): + re_part, im_part = self.as_real_imag(deep=deep, **hints) + return re_part + im_part*I + + def _eval_expand_trig(self, deep=True, **hints): + if deep: + arg = self.args[0].expand(deep, **hints) + else: + arg = self.args[0] + x = None + if arg.is_Add: # TODO, implement more if deep stuff here + x, y = arg.as_two_terms() + else: + coeff, terms = arg.as_coeff_Mul(rational=True) + if coeff is not S.One and coeff.is_Integer and terms is not S.One: + x = terms + y = (coeff - 1)*x + if x is not None: + return (cosh(x)*cosh(y) + sinh(x)*sinh(y)).expand(trig=True) + return cosh(arg) + + def _eval_rewrite_as_tractable(self, arg, limitvar=None, **kwargs): + return (exp(arg) + exp(-arg)) / 2 + + def _eval_rewrite_as_exp(self, arg, **kwargs): + return (exp(arg) + exp(-arg)) / 2 + + def _eval_rewrite_as_cos(self, arg, **kwargs): + return cos(I * arg, evaluate=False) + + def _eval_rewrite_as_sec(self, arg, **kwargs): + return 1 / sec(I * arg, evaluate=False) + + def _eval_rewrite_as_sinh(self, arg, **kwargs): + return -I*sinh(arg + pi*I/2, evaluate=False) + + def _eval_rewrite_as_tanh(self, arg, **kwargs): + tanh_half = tanh(S.Half*arg)**2 + return (1 + tanh_half)/(1 - tanh_half) + + def _eval_rewrite_as_coth(self, arg, **kwargs): + coth_half = coth(S.Half*arg)**2 + return (coth_half + 1)/(coth_half - 1) + + def _eval_rewrite_as_sech(self, arg, **kwargs): + return 1 / sech(arg) + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0].as_leading_term(x, logx=logx, cdir=cdir) + arg0 = arg.subs(x, 0) + + if arg0 is S.NaN: + arg0 = arg.limit(x, 0, dir='-' if cdir.is_negative else '+') + if arg0.is_zero: + return S.One + elif arg0.is_finite: + return self.func(arg0) + else: + return self + + def _eval_is_real(self): + arg = self.args[0] + + # `cosh(x)` is real for real OR purely imaginary `x` + if arg.is_real or arg.is_imaginary: + return True + + # cosh(a+ib) = cos(b)*cosh(a) + i*sin(b)*sinh(a) + # the imaginary part can be an expression like n*pi + # if not, check if the imaginary part is a number + re, im = arg.as_real_imag() + return (im%pi).is_zero + + def _eval_is_positive(self): + # cosh(x+I*y) = cos(y)*cosh(x) + I*sin(y)*sinh(x) + # cosh(z) is positive iff it is real and the real part is positive. + # So we need sin(y)*sinh(x) = 0 which gives x=0 or y=n*pi + # Case 1 (y=n*pi): cosh(z) = (-1)**n * cosh(x) -> positive for n even + # Case 2 (x=0): cosh(z) = cos(y) -> positive when cos(y) is positive + z = self.args[0] + + x, y = z.as_real_imag() + ymod = y % (2*pi) + + yzero = ymod.is_zero + # shortcut if ymod is zero + if yzero: + return True + + xzero = x.is_zero + # shortcut x is not zero + if xzero is False: + return yzero + + return fuzzy_or([ + # Case 1: + yzero, + # Case 2: + fuzzy_and([ + xzero, + fuzzy_or([ymod < pi/2, ymod > 3*pi/2]) + ]) + ]) + + + def _eval_is_nonnegative(self): + z = self.args[0] + + x, y = z.as_real_imag() + ymod = y % (2*pi) + + yzero = ymod.is_zero + # shortcut if ymod is zero + if yzero: + return True + + xzero = x.is_zero + # shortcut x is not zero + if xzero is False: + return yzero + + return fuzzy_or([ + # Case 1: + yzero, + # Case 2: + fuzzy_and([ + xzero, + fuzzy_or([ymod <= pi/2, ymod >= 3*pi/2]) + ]) + ]) + + def _eval_is_finite(self): + arg = self.args[0] + return arg.is_finite + + def _eval_is_zero(self): + rest, ipi_mult = _peeloff_ipi(self.args[0]) + if ipi_mult and rest.is_zero: + return (ipi_mult - S.Half).is_integer + + +class tanh(HyperbolicFunction): + r""" + ``tanh(x)`` is the hyperbolic tangent of ``x``. + + The hyperbolic tangent function is $\frac{\sinh(x)}{\cosh(x)}$. + + Examples + ======== + + >>> from sympy import tanh + >>> from sympy.abc import x + >>> tanh(x) + tanh(x) + + See Also + ======== + + sinh, cosh, atanh + """ + + def fdiff(self, argindex=1): + if argindex == 1: + return S.One - tanh(self.args[0])**2 + else: + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return atanh + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.One + elif arg is S.NegativeInfinity: + return S.NegativeOne + elif arg.is_zero: + return S.Zero + elif arg.is_negative: + return -cls(-arg) + else: + if arg is S.ComplexInfinity: + return S.NaN + + i_coeff = _imaginary_unit_as_coefficient(arg) + + if i_coeff is not None: + if i_coeff.could_extract_minus_sign(): + return -I * tan(-i_coeff) + return I * tan(i_coeff) + else: + if arg.could_extract_minus_sign(): + return -cls(-arg) + + if arg.is_Add: + x, m = _peeloff_ipi(arg) + if m: + tanhm = tanh(m*pi*I) + if tanhm is S.ComplexInfinity: + return coth(x) + else: # tanhm == 0 + return tanh(x) + + if arg.is_zero: + return S.Zero + + if arg.func == asinh: + x = arg.args[0] + return x/sqrt(1 + x**2) + + if arg.func == acosh: + x = arg.args[0] + return sqrt(x - 1) * sqrt(x + 1) / x + + if arg.func == atanh: + return arg.args[0] + + if arg.func == acoth: + return 1/arg.args[0] + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + + a = 2**(n + 1) + + B = bernoulli(n + 1) + F = factorial(n + 1) + + return a*(a - 1) * B/F * x**n + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def as_real_imag(self, deep=True, **hints): + if self.args[0].is_extended_real: + if deep: + hints['complex'] = False + return (self.expand(deep, **hints), S.Zero) + else: + return (self, S.Zero) + if deep: + re, im = self.args[0].expand(deep, **hints).as_real_imag() + else: + re, im = self.args[0].as_real_imag() + denom = sinh(re)**2 + cos(im)**2 + return (sinh(re)*cosh(re)/denom, sin(im)*cos(im)/denom) + + def _eval_expand_trig(self, **hints): + arg = self.args[0] + if arg.is_Add: + n = len(arg.args) + TX = [tanh(x, evaluate=False)._eval_expand_trig() + for x in arg.args] + p = [0, 0] # [den, num] + for i in range(n + 1): + p[i % 2] += symmetric_poly(i, TX) + return p[1]/p[0] + elif arg.is_Mul: + coeff, terms = arg.as_coeff_Mul() + if coeff.is_Integer and coeff > 1: + T = tanh(terms) + n = [nC(range(coeff), k)*T**k for k in range(1, coeff + 1, 2)] + d = [nC(range(coeff), k)*T**k for k in range(0, coeff + 1, 2)] + return Add(*n)/Add(*d) + return tanh(arg) + + def _eval_rewrite_as_tractable(self, arg, limitvar=None, **kwargs): + neg_exp, pos_exp = exp(-arg), exp(arg) + return (pos_exp - neg_exp)/(pos_exp + neg_exp) + + def _eval_rewrite_as_exp(self, arg, **kwargs): + neg_exp, pos_exp = exp(-arg), exp(arg) + return (pos_exp - neg_exp)/(pos_exp + neg_exp) + + def _eval_rewrite_as_tan(self, arg, **kwargs): + return -I * tan(I * arg, evaluate=False) + + def _eval_rewrite_as_cot(self, arg, **kwargs): + return -I / cot(I * arg, evaluate=False) + + def _eval_rewrite_as_sinh(self, arg, **kwargs): + return I*sinh(arg)/sinh(pi*I/2 - arg, evaluate=False) + + def _eval_rewrite_as_cosh(self, arg, **kwargs): + return I*cosh(pi*I/2 - arg, evaluate=False)/cosh(arg) + + def _eval_rewrite_as_coth(self, arg, **kwargs): + return 1/coth(arg) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.series.order import Order + arg = self.args[0].as_leading_term(x) + + if x in arg.free_symbols and Order(1, x).contains(arg): + return arg + else: + return self.func(arg) + + def _eval_is_real(self): + arg = self.args[0] + if arg.is_real: + return True + + re, im = arg.as_real_imag() + + # if denom = 0, tanh(arg) = zoo + if re == 0 and im % pi == pi/2: + return None + + # check if im is of the form n*pi/2 to make sin(2*im) = 0 + # if not, im could be a number, return False in that case + return (im % (pi/2)).is_zero + + def _eval_is_extended_real(self): + if self.args[0].is_extended_real: + return True + + def _eval_is_positive(self): + if self.args[0].is_extended_real: + return self.args[0].is_positive + + def _eval_is_negative(self): + if self.args[0].is_extended_real: + return self.args[0].is_negative + + def _eval_is_finite(self): + arg = self.args[0] + + re, im = arg.as_real_imag() + denom = cos(im)**2 + sinh(re)**2 + if denom == 0: + return False + elif denom.is_number: + return True + if arg.is_extended_real: + return True + + def _eval_is_zero(self): + arg = self.args[0] + if arg.is_zero: + return True + + +class coth(HyperbolicFunction): + r""" + ``coth(x)`` is the hyperbolic cotangent of ``x``. + + The hyperbolic cotangent function is $\frac{\cosh(x)}{\sinh(x)}$. + + Examples + ======== + + >>> from sympy import coth + >>> from sympy.abc import x + >>> coth(x) + coth(x) + + See Also + ======== + + sinh, cosh, acoth + """ + + def fdiff(self, argindex=1): + if argindex == 1: + return -1/sinh(self.args[0])**2 + else: + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return acoth + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.One + elif arg is S.NegativeInfinity: + return S.NegativeOne + elif arg.is_zero: + return S.ComplexInfinity + elif arg.is_negative: + return -cls(-arg) + else: + if arg is S.ComplexInfinity: + return S.NaN + + i_coeff = _imaginary_unit_as_coefficient(arg) + + if i_coeff is not None: + if i_coeff.could_extract_minus_sign(): + return I * cot(-i_coeff) + return -I * cot(i_coeff) + else: + if arg.could_extract_minus_sign(): + return -cls(-arg) + + if arg.is_Add: + x, m = _peeloff_ipi(arg) + if m: + cothm = coth(m*pi*I) + if cothm is S.ComplexInfinity: + return coth(x) + else: # cothm == 0 + return tanh(x) + + if arg.is_zero: + return S.ComplexInfinity + + if arg.func == asinh: + x = arg.args[0] + return sqrt(1 + x**2)/x + + if arg.func == acosh: + x = arg.args[0] + return x/(sqrt(x - 1) * sqrt(x + 1)) + + if arg.func == atanh: + return 1/arg.args[0] + + if arg.func == acoth: + return arg.args[0] + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n == 0: + return 1 / sympify(x) + elif n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + + B = bernoulli(n + 1) + F = factorial(n + 1) + + return 2**(n + 1) * B/F * x**n + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def as_real_imag(self, deep=True, **hints): + from sympy.functions.elementary.trigonometric import (cos, sin) + if self.args[0].is_extended_real: + if deep: + hints['complex'] = False + return (self.expand(deep, **hints), S.Zero) + else: + return (self, S.Zero) + if deep: + re, im = self.args[0].expand(deep, **hints).as_real_imag() + else: + re, im = self.args[0].as_real_imag() + denom = sinh(re)**2 + sin(im)**2 + return (sinh(re)*cosh(re)/denom, -sin(im)*cos(im)/denom) + + def _eval_rewrite_as_tractable(self, arg, limitvar=None, **kwargs): + neg_exp, pos_exp = exp(-arg), exp(arg) + return (pos_exp + neg_exp)/(pos_exp - neg_exp) + + def _eval_rewrite_as_exp(self, arg, **kwargs): + neg_exp, pos_exp = exp(-arg), exp(arg) + return (pos_exp + neg_exp)/(pos_exp - neg_exp) + + def _eval_rewrite_as_sinh(self, arg, **kwargs): + return -I*sinh(pi*I/2 - arg, evaluate=False)/sinh(arg) + + def _eval_rewrite_as_cosh(self, arg, **kwargs): + return -I*cosh(arg)/cosh(pi*I/2 - arg, evaluate=False) + + def _eval_rewrite_as_tanh(self, arg, **kwargs): + return 1/tanh(arg) + + def _eval_is_positive(self): + if self.args[0].is_extended_real: + return self.args[0].is_positive + + def _eval_is_negative(self): + if self.args[0].is_extended_real: + return self.args[0].is_negative + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.series.order import Order + arg = self.args[0].as_leading_term(x) + + if x in arg.free_symbols and Order(1, x).contains(arg): + return 1/arg + else: + return self.func(arg) + + def _eval_expand_trig(self, **hints): + arg = self.args[0] + if arg.is_Add: + CX = [coth(x, evaluate=False)._eval_expand_trig() for x in arg.args] + p = [[], []] + n = len(arg.args) + for i in range(n, -1, -1): + p[(n - i) % 2].append(symmetric_poly(i, CX)) + return Add(*p[0])/Add(*p[1]) + elif arg.is_Mul: + coeff, x = arg.as_coeff_Mul(rational=True) + if coeff.is_Integer and coeff > 1: + c = coth(x, evaluate=False) + p = [[], []] + for i in range(coeff, -1, -1): + p[(coeff - i) % 2].append(binomial(coeff, i)*c**i) + return Add(*p[0])/Add(*p[1]) + return coth(arg) + + +class ReciprocalHyperbolicFunction(HyperbolicFunction): + """Base class for reciprocal functions of hyperbolic functions. """ + + #To be defined in class + _reciprocal_of = None + _is_even: FuzzyBool = None + _is_odd: FuzzyBool = None + + @classmethod + def eval(cls, arg): + if arg.could_extract_minus_sign(): + if cls._is_even: + return cls(-arg) + if cls._is_odd: + return -cls(-arg) + + t = cls._reciprocal_of.eval(arg) + if hasattr(arg, 'inverse') and arg.inverse() == cls: + return arg.args[0] + return 1/t if t is not None else t + + def _call_reciprocal(self, method_name, *args, **kwargs): + # Calls method_name on _reciprocal_of + o = self._reciprocal_of(self.args[0]) + return getattr(o, method_name)(*args, **kwargs) + + def _calculate_reciprocal(self, method_name, *args, **kwargs): + # If calling method_name on _reciprocal_of returns a value != None + # then return the reciprocal of that value + t = self._call_reciprocal(method_name, *args, **kwargs) + return 1/t if t is not None else t + + def _rewrite_reciprocal(self, method_name, arg): + # Special handling for rewrite functions. If reciprocal rewrite returns + # unmodified expression, then return None + t = self._call_reciprocal(method_name, arg) + if t is not None and t != self._reciprocal_of(arg): + return 1/t + + def _eval_rewrite_as_exp(self, arg, **kwargs): + return self._rewrite_reciprocal("_eval_rewrite_as_exp", arg) + + def _eval_rewrite_as_tractable(self, arg, limitvar=None, **kwargs): + return self._rewrite_reciprocal("_eval_rewrite_as_tractable", arg) + + def _eval_rewrite_as_tanh(self, arg, **kwargs): + return self._rewrite_reciprocal("_eval_rewrite_as_tanh", arg) + + def _eval_rewrite_as_coth(self, arg, **kwargs): + return self._rewrite_reciprocal("_eval_rewrite_as_coth", arg) + + def as_real_imag(self, deep = True, **hints): + return (1 / self._reciprocal_of(self.args[0])).as_real_imag(deep, **hints) + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def _eval_expand_complex(self, deep=True, **hints): + re_part, im_part = self.as_real_imag(deep=True, **hints) + return re_part + I*im_part + + def _eval_expand_trig(self, **hints): + return self._calculate_reciprocal("_eval_expand_trig", **hints) + + def _eval_as_leading_term(self, x, logx, cdir): + return (1/self._reciprocal_of(self.args[0]))._eval_as_leading_term(x, logx=logx, cdir=cdir) + + def _eval_is_extended_real(self): + return self._reciprocal_of(self.args[0]).is_extended_real + + def _eval_is_finite(self): + return (1/self._reciprocal_of(self.args[0])).is_finite + + +class csch(ReciprocalHyperbolicFunction): + r""" + ``csch(x)`` is the hyperbolic cosecant of ``x``. + + The hyperbolic cosecant function is $\frac{2}{e^x - e^{-x}}$ + + Examples + ======== + + >>> from sympy import csch + >>> from sympy.abc import x + >>> csch(x) + csch(x) + + See Also + ======== + + sinh, cosh, tanh, sech, asinh, acosh + """ + + _reciprocal_of = sinh + _is_odd = True + + def fdiff(self, argindex=1): + """ + Returns the first derivative of this function + """ + if argindex == 1: + return -coth(self.args[0]) * csch(self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + """ + Returns the next term in the Taylor series expansion + """ + if n == 0: + return 1/sympify(x) + elif n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + + B = bernoulli(n + 1) + F = factorial(n + 1) + + return 2 * (1 - 2**n) * B/F * x**n + + def _eval_rewrite_as_sin(self, arg, **kwargs): + return I / sin(I * arg, evaluate=False) + + def _eval_rewrite_as_csc(self, arg, **kwargs): + return I * csc(I * arg, evaluate=False) + + def _eval_rewrite_as_cosh(self, arg, **kwargs): + return I / cosh(arg + I * pi / 2, evaluate=False) + + def _eval_rewrite_as_sinh(self, arg, **kwargs): + return 1 / sinh(arg) + + def _eval_is_positive(self): + if self.args[0].is_extended_real: + return self.args[0].is_positive + + def _eval_is_negative(self): + if self.args[0].is_extended_real: + return self.args[0].is_negative + + +class sech(ReciprocalHyperbolicFunction): + r""" + ``sech(x)`` is the hyperbolic secant of ``x``. + + The hyperbolic secant function is $\frac{2}{e^x + e^{-x}}$ + + Examples + ======== + + >>> from sympy import sech + >>> from sympy.abc import x + >>> sech(x) + sech(x) + + See Also + ======== + + sinh, cosh, tanh, coth, csch, asinh, acosh + """ + + _reciprocal_of = cosh + _is_even = True + + def fdiff(self, argindex=1): + if argindex == 1: + return - tanh(self.args[0])*sech(self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0 or n % 2 == 1: + return S.Zero + else: + x = sympify(x) + return euler(n) / factorial(n) * x**(n) + + def _eval_rewrite_as_cos(self, arg, **kwargs): + return 1 / cos(I * arg, evaluate=False) + + def _eval_rewrite_as_sec(self, arg, **kwargs): + return sec(I * arg, evaluate=False) + + def _eval_rewrite_as_sinh(self, arg, **kwargs): + return I / sinh(arg + I * pi /2, evaluate=False) + + def _eval_rewrite_as_cosh(self, arg, **kwargs): + return 1 / cosh(arg) + + def _eval_is_positive(self): + if self.args[0].is_extended_real: + return True + + +############################################################################### +############################# HYPERBOLIC INVERSES ############################# +############################################################################### + +class InverseHyperbolicFunction(DefinedFunction): + """Base class for inverse hyperbolic functions.""" + + pass + + +class asinh(InverseHyperbolicFunction): + """ + ``asinh(x)`` is the inverse hyperbolic sine of ``x``. + + The inverse hyperbolic sine function. + + Examples + ======== + + >>> from sympy import asinh + >>> from sympy.abc import x + >>> asinh(x).diff(x) + 1/sqrt(x**2 + 1) + >>> asinh(1) + log(1 + sqrt(2)) + + See Also + ======== + + acosh, atanh, sinh + """ + + def fdiff(self, argindex=1): + if argindex == 1: + return 1/sqrt(self.args[0]**2 + 1) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.Infinity + elif arg is S.NegativeInfinity: + return S.NegativeInfinity + elif arg.is_zero: + return S.Zero + elif arg is S.One: + return log(sqrt(2) + 1) + elif arg is S.NegativeOne: + return log(sqrt(2) - 1) + elif arg.is_negative: + return -cls(-arg) + else: + if arg is S.ComplexInfinity: + return S.ComplexInfinity + + if arg.is_zero: + return S.Zero + + i_coeff = _imaginary_unit_as_coefficient(arg) + + if i_coeff is not None: + return I * asin(i_coeff) + else: + if arg.could_extract_minus_sign(): + return -cls(-arg) + + if isinstance(arg, sinh) and arg.args[0].is_number: + z = arg.args[0] + if z.is_real: + return z + r, i = match_real_imag(z) + if r is not None and i is not None: + f = floor((i + pi/2)/pi) + m = z - I*pi*f + even = f.is_even + if even is True: + return m + elif even is False: + return -m + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + if len(previous_terms) >= 2 and n > 2: + p = previous_terms[-2] + return -p * (n - 2)**2/(n*(n - 1)) * x**2 + else: + k = (n - 1) // 2 + R = RisingFactorial(S.Half, k) + F = factorial(k) + return S.NegativeOne**k * R / F * x**n / n + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + if x0.is_zero: + return arg.as_leading_term(x) + + if x0 is S.NaN: + expr = self.func(arg.as_leading_term(x)) + if expr.is_finite: + return expr + else: + return self + + # Handling branch points + if x0 in (-I, I, S.ComplexInfinity): + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir) + # Handling points lying on branch cuts (-I*oo, -I) U (I, I*oo) + if (1 + x0**2).is_negative: + ndir = arg.dir(x, cdir if cdir else 1) + if re(ndir).is_positive: + if im(x0).is_negative: + return -self.func(x0) - I*pi + elif re(ndir).is_negative: + if im(x0).is_positive: + return -self.func(x0) + I*pi + else: + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir) + return self.func(x0) + + def _eval_nseries(self, x, n, logx, cdir=0): # asinh + arg = self.args[0] + arg0 = arg.subs(x, 0) + + # Handling branch points + if arg0 in (I, -I): + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + + res = super()._eval_nseries(x, n=n, logx=logx) + if arg0 is S.ComplexInfinity: + return res + + # Handling points lying on branch cuts (-I*oo, -I) U (I, I*oo) + if (1 + arg0**2).is_negative: + ndir = arg.dir(x, cdir if cdir else 1) + if re(ndir).is_positive: + if im(arg0).is_negative: + return -res - I*pi + elif re(ndir).is_negative: + if im(arg0).is_positive: + return -res + I*pi + else: + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + return res + + def _eval_rewrite_as_log(self, x, **kwargs): + return log(x + sqrt(x**2 + 1)) + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + def _eval_rewrite_as_atanh(self, x, **kwargs): + return atanh(x/sqrt(1 + x**2)) + + def _eval_rewrite_as_acosh(self, x, **kwargs): + ix = I*x + return I*(sqrt(1 - ix)/sqrt(ix - 1) * acosh(ix) - pi/2) + + def _eval_rewrite_as_asin(self, x, **kwargs): + return -I * asin(I * x, evaluate=False) + + def _eval_rewrite_as_acos(self, x, **kwargs): + return I * acos(I * x, evaluate=False) - I*pi/2 + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return sinh + + def _eval_is_zero(self): + return self.args[0].is_zero + + def _eval_is_extended_real(self): + return self.args[0].is_extended_real + + def _eval_is_finite(self): + return self.args[0].is_finite + + +class acosh(InverseHyperbolicFunction): + """ + ``acosh(x)`` is the inverse hyperbolic cosine of ``x``. + + The inverse hyperbolic cosine function. + + Examples + ======== + + >>> from sympy import acosh + >>> from sympy.abc import x + >>> acosh(x).diff(x) + 1/(sqrt(x - 1)*sqrt(x + 1)) + >>> acosh(1) + 0 + + See Also + ======== + + asinh, atanh, cosh + """ + + def fdiff(self, argindex=1): + if argindex == 1: + arg = self.args[0] + return 1/(sqrt(arg - 1)*sqrt(arg + 1)) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.Infinity + elif arg is S.NegativeInfinity: + return S.Infinity + elif arg.is_zero: + return pi*I / 2 + elif arg is S.One: + return S.Zero + elif arg is S.NegativeOne: + return pi*I + + if arg.is_number: + cst_table = _acosh_table() + + if arg in cst_table: + if arg.is_extended_real: + return cst_table[arg]*I + return cst_table[arg] + + if arg is S.ComplexInfinity: + return S.ComplexInfinity + if arg == I*S.Infinity: + return S.Infinity + I*pi/2 + if arg == -I*S.Infinity: + return S.Infinity - I*pi/2 + + if arg.is_zero: + return pi*I*S.Half + + if isinstance(arg, cosh) and arg.args[0].is_number: + z = arg.args[0] + if z.is_real: + return Abs(z) + r, i = match_real_imag(z) + if r is not None and i is not None: + f = floor(i/pi) + m = z - I*pi*f + even = f.is_even + if even is True: + if r.is_nonnegative: + return m + elif r.is_negative: + return -m + elif even is False: + m -= I*pi + if r.is_nonpositive: + return -m + elif r.is_positive: + return m + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n == 0: + return I*pi/2 + elif n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + if len(previous_terms) >= 2 and n > 2: + p = previous_terms[-2] + return p * (n - 2)**2/(n*(n - 1)) * x**2 + else: + k = (n - 1) // 2 + R = RisingFactorial(S.Half, k) + F = factorial(k) + return -R / F * I * x**n / n + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + # Handling branch points + if x0 in (-S.One, S.Zero, S.One, S.ComplexInfinity): + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir) + + if x0 is S.NaN: + expr = self.func(arg.as_leading_term(x)) + if expr.is_finite: + return expr + else: + return self + + # Handling points lying on branch cuts (-oo, 1) + if (x0 - 1).is_negative: + ndir = arg.dir(x, cdir if cdir else 1) + if im(ndir).is_negative: + if (x0 + 1).is_negative: + return self.func(x0) - 2*I*pi + return -self.func(x0) + elif not im(ndir).is_positive: + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir) + return self.func(x0) + + def _eval_nseries(self, x, n, logx, cdir=0): # acosh + arg = self.args[0] + arg0 = arg.subs(x, 0) + + # Handling branch points + if arg0 in (S.One, S.NegativeOne): + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + + res = super()._eval_nseries(x, n=n, logx=logx) + if arg0 is S.ComplexInfinity: + return res + + # Handling points lying on branch cuts (-oo, 1) + if (arg0 - 1).is_negative: + ndir = arg.dir(x, cdir if cdir else 1) + if im(ndir).is_negative: + if (arg0 + 1).is_negative: + return res - 2*I*pi + return -res + elif not im(ndir).is_positive: + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + return res + + def _eval_rewrite_as_log(self, x, **kwargs): + return log(x + sqrt(x + 1) * sqrt(x - 1)) + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + def _eval_rewrite_as_acos(self, x, **kwargs): + return sqrt(x - 1)/sqrt(1 - x) * acos(x) + + def _eval_rewrite_as_asin(self, x, **kwargs): + return sqrt(x - 1)/sqrt(1 - x) * (pi/2 - asin(x)) + + def _eval_rewrite_as_asinh(self, x, **kwargs): + return sqrt(x - 1)/sqrt(1 - x) * (pi/2 + I*asinh(I*x, evaluate=False)) + + def _eval_rewrite_as_atanh(self, x, **kwargs): + sxm1 = sqrt(x - 1) + s1mx = sqrt(1 - x) + sx2m1 = sqrt(x**2 - 1) + return (pi/2*sxm1/s1mx*(1 - x * sqrt(1/x**2)) + + sxm1*sqrt(x + 1)/sx2m1 * atanh(sx2m1/x)) + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return cosh + + def _eval_is_zero(self): + if (self.args[0] - 1).is_zero: + return True + + def _eval_is_extended_real(self): + return fuzzy_and([self.args[0].is_extended_real, (self.args[0] - 1).is_extended_nonnegative]) + + def _eval_is_finite(self): + return self.args[0].is_finite + + +class atanh(InverseHyperbolicFunction): + """ + ``atanh(x)`` is the inverse hyperbolic tangent of ``x``. + + The inverse hyperbolic tangent function. + + Examples + ======== + + >>> from sympy import atanh + >>> from sympy.abc import x + >>> atanh(x).diff(x) + 1/(1 - x**2) + + See Also + ======== + + asinh, acosh, tanh + """ + + def fdiff(self, argindex=1): + if argindex == 1: + return 1/(1 - self.args[0]**2) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg.is_zero: + return S.Zero + elif arg is S.One: + return S.Infinity + elif arg is S.NegativeOne: + return S.NegativeInfinity + elif arg is S.Infinity: + return -I * atan(arg) + elif arg is S.NegativeInfinity: + return I * atan(-arg) + elif arg.is_negative: + return -cls(-arg) + else: + if arg is S.ComplexInfinity: + from sympy.calculus.accumulationbounds import AccumBounds + return I*AccumBounds(-pi/2, pi/2) + + i_coeff = _imaginary_unit_as_coefficient(arg) + + if i_coeff is not None: + return I * atan(i_coeff) + else: + if arg.could_extract_minus_sign(): + return -cls(-arg) + + if arg.is_zero: + return S.Zero + + if isinstance(arg, tanh) and arg.args[0].is_number: + z = arg.args[0] + if z.is_real: + return z + r, i = match_real_imag(z) + if r is not None and i is not None: + f = floor(2*i/pi) + even = f.is_even + m = z - I*f*pi/2 + if even is True: + return m + elif even is False: + return m - I*pi/2 + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + return x**n / n + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + if x0.is_zero: + return arg.as_leading_term(x) + if x0 is S.NaN: + expr = self.func(arg.as_leading_term(x)) + if expr.is_finite: + return expr + else: + return self + + # Handling branch points + if x0 in (-S.One, S.One, S.ComplexInfinity): + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir) + # Handling points lying on branch cuts (-oo, -1] U [1, oo) + if (1 - x0**2).is_negative: + ndir = arg.dir(x, cdir if cdir else 1) + if im(ndir).is_negative: + if x0.is_negative: + return self.func(x0) - I*pi + elif im(ndir).is_positive: + if x0.is_positive: + return self.func(x0) + I*pi + else: + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir) + return self.func(x0) + + def _eval_nseries(self, x, n, logx, cdir=0): # atanh + arg = self.args[0] + arg0 = arg.subs(x, 0) + + # Handling branch points + if arg0 in (S.One, S.NegativeOne): + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + + res = super()._eval_nseries(x, n=n, logx=logx) + if arg0 is S.ComplexInfinity: + return res + + # Handling points lying on branch cuts (-oo, -1] U [1, oo) + if (1 - arg0**2).is_negative: + ndir = arg.dir(x, cdir if cdir else 1) + if im(ndir).is_negative: + if arg0.is_negative: + return res - I*pi + elif im(ndir).is_positive: + if arg0.is_positive: + return res + I*pi + else: + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + return res + + def _eval_rewrite_as_log(self, x, **kwargs): + return (log(1 + x) - log(1 - x)) / 2 + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + def _eval_rewrite_as_asinh(self, x, **kwargs): + f = sqrt(1/(x**2 - 1)) + return (pi*x/(2*sqrt(-x**2)) - + sqrt(-x)*sqrt(1 - x**2)/sqrt(x)*f*asinh(f)) + + def _eval_is_zero(self): + if self.args[0].is_zero: + return True + + def _eval_is_extended_real(self): + return fuzzy_and([self.args[0].is_extended_real, (1 - self.args[0]).is_nonnegative, (self.args[0] + 1).is_nonnegative]) + + def _eval_is_finite(self): + return fuzzy_not(fuzzy_or([(self.args[0] - 1).is_zero, (self.args[0] + 1).is_zero])) + + def _eval_is_imaginary(self): + return self.args[0].is_imaginary + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return tanh + + +class acoth(InverseHyperbolicFunction): + """ + ``acoth(x)`` is the inverse hyperbolic cotangent of ``x``. + + The inverse hyperbolic cotangent function. + + Examples + ======== + + >>> from sympy import acoth + >>> from sympy.abc import x + >>> acoth(x).diff(x) + 1/(1 - x**2) + + See Also + ======== + + asinh, acosh, coth + """ + + def fdiff(self, argindex=1): + if argindex == 1: + return 1/(1 - self.args[0]**2) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.Zero + elif arg is S.NegativeInfinity: + return S.Zero + elif arg.is_zero: + return pi*I / 2 + elif arg is S.One: + return S.Infinity + elif arg is S.NegativeOne: + return S.NegativeInfinity + elif arg.is_negative: + return -cls(-arg) + else: + if arg is S.ComplexInfinity: + return S.Zero + + i_coeff = _imaginary_unit_as_coefficient(arg) + + if i_coeff is not None: + return -I * acot(i_coeff) + else: + if arg.could_extract_minus_sign(): + return -cls(-arg) + + if arg.is_zero: + return pi*I*S.Half + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n == 0: + return -I*pi/2 + elif n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + return x**n / n + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + if x0 is S.ComplexInfinity: + return (1/arg).as_leading_term(x) + if x0 is S.NaN: + expr = self.func(arg.as_leading_term(x)) + if expr.is_finite: + return expr + else: + return self + + # Handling branch points + if x0 in (-S.One, S.One, S.Zero): + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir) + # Handling points lying on branch cuts [-1, 1] + if x0.is_real and (1 - x0**2).is_positive: + ndir = arg.dir(x, cdir if cdir else 1) + if im(ndir).is_negative: + if x0.is_positive: + return self.func(x0) + I*pi + elif im(ndir).is_positive: + if x0.is_negative: + return self.func(x0) - I*pi + else: + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir) + return self.func(x0) + + def _eval_nseries(self, x, n, logx, cdir=0): # acoth + arg = self.args[0] + arg0 = arg.subs(x, 0) + + # Handling branch points + if arg0 in (S.One, S.NegativeOne): + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + + res = super()._eval_nseries(x, n=n, logx=logx) + if arg0 is S.ComplexInfinity: + return res + + # Handling points lying on branch cuts [-1, 1] + if arg0.is_real and (1 - arg0**2).is_positive: + ndir = arg.dir(x, cdir if cdir else 1) + if im(ndir).is_negative: + if arg0.is_positive: + return res + I*pi + elif im(ndir).is_positive: + if arg0.is_negative: + return res - I*pi + else: + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + return res + + def _eval_rewrite_as_log(self, x, **kwargs): + return (log(1 + 1/x) - log(1 - 1/x)) / 2 + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + def _eval_rewrite_as_atanh(self, x, **kwargs): + return atanh(1/x) + + def _eval_rewrite_as_asinh(self, x, **kwargs): + return (pi*I/2*(sqrt((x - 1)/x)*sqrt(x/(x - 1)) - sqrt(1 + 1/x)*sqrt(x/(x + 1))) + + x*sqrt(1/x**2)*asinh(sqrt(1/(x**2 - 1)))) + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return coth + + def _eval_is_extended_real(self): + return fuzzy_and([self.args[0].is_extended_real, fuzzy_or([(self.args[0] - 1).is_extended_nonnegative, (self.args[0] + 1).is_extended_nonpositive])]) + + def _eval_is_finite(self): + return fuzzy_not(fuzzy_or([(self.args[0] - 1).is_zero, (self.args[0] + 1).is_zero])) + + +class asech(InverseHyperbolicFunction): + """ + ``asech(x)`` is the inverse hyperbolic secant of ``x``. + + The inverse hyperbolic secant function. + + Examples + ======== + + >>> from sympy import asech, sqrt, S + >>> from sympy.abc import x + >>> asech(x).diff(x) + -1/(x*sqrt(1 - x**2)) + >>> asech(1).diff(x) + 0 + >>> asech(1) + 0 + >>> asech(S(2)) + I*pi/3 + >>> asech(-sqrt(2)) + 3*I*pi/4 + >>> asech((sqrt(6) - sqrt(2))) + I*pi/12 + + See Also + ======== + + asinh, atanh, cosh, acoth + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hyperbolic_function + .. [2] https://dlmf.nist.gov/4.37 + .. [3] https://functions.wolfram.com/ElementaryFunctions/ArcSech/ + + """ + + def fdiff(self, argindex=1): + if argindex == 1: + z = self.args[0] + return -1/(z*sqrt(1 - z**2)) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return pi*I / 2 + elif arg is S.NegativeInfinity: + return pi*I / 2 + elif arg.is_zero: + return S.Infinity + elif arg is S.One: + return S.Zero + elif arg is S.NegativeOne: + return pi*I + + if arg.is_number: + cst_table = _asech_table() + + if arg in cst_table: + if arg.is_extended_real: + return cst_table[arg]*I + return cst_table[arg] + + if arg is S.ComplexInfinity: + from sympy.calculus.accumulationbounds import AccumBounds + return I*AccumBounds(-pi/2, pi/2) + + if arg.is_zero: + return S.Infinity + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n == 0: + return log(2 / x) + elif n < 0 or n % 2 == 1: + return S.Zero + else: + x = sympify(x) + if len(previous_terms) > 2 and n > 2: + p = previous_terms[-2] + return p * ((n - 1)*(n-2)) * x**2/(4 * (n//2)**2) + else: + k = n // 2 + R = RisingFactorial(S.Half, k) * n + F = factorial(k) * n // 2 * n // 2 + return -1 * R / F * x**n / 4 + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + # Handling branch points + if x0 in (-S.One, S.Zero, S.One, S.ComplexInfinity): + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir) + + if x0 is S.NaN: + expr = self.func(arg.as_leading_term(x)) + if expr.is_finite: + return expr + else: + return self + + # Handling points lying on branch cuts (-oo, 0] U (1, oo) + if x0.is_negative or (1 - x0).is_negative: + ndir = arg.dir(x, cdir if cdir else 1) + if im(ndir).is_positive: + if x0.is_positive or (x0 + 1).is_negative: + return -self.func(x0) + return self.func(x0) - 2*I*pi + elif not im(ndir).is_negative: + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir) + return self.func(x0) + + def _eval_nseries(self, x, n, logx, cdir=0): # asech + from sympy.series.order import O + arg = self.args[0] + arg0 = arg.subs(x, 0) + + # Handling branch points + if arg0 is S.One: + t = Dummy('t', positive=True) + ser = asech(S.One - t**2).rewrite(log).nseries(t, 0, 2*n) + arg1 = S.One - self.args[0] + f = arg1.as_leading_term(x) + g = (arg1 - f)/ f + if not g.is_meromorphic(x, 0): # cannot be expanded + return O(1) if n == 0 else O(sqrt(x)) + res1 = sqrt(S.One + g)._eval_nseries(x, n=n, logx=logx) + res = (res1.removeO()*sqrt(f)).expand() + return ser.removeO().subs(t, res).expand().powsimp() + O(x**n, x) + + if arg0 is S.NegativeOne: + t = Dummy('t', positive=True) + ser = asech(S.NegativeOne + t**2).rewrite(log).nseries(t, 0, 2*n) + arg1 = S.One + self.args[0] + f = arg1.as_leading_term(x) + g = (arg1 - f)/ f + if not g.is_meromorphic(x, 0): # cannot be expanded + return O(1) if n == 0 else I*pi + O(sqrt(x)) + res1 = sqrt(S.One + g)._eval_nseries(x, n=n, logx=logx) + res = (res1.removeO()*sqrt(f)).expand() + return ser.removeO().subs(t, res).expand().powsimp() + O(x**n, x) + + res = super()._eval_nseries(x, n=n, logx=logx) + if arg0 is S.ComplexInfinity: + return res + + # Handling points lying on branch cuts (-oo, 0] U (1, oo) + if arg0.is_negative or (1 - arg0).is_negative: + ndir = arg.dir(x, cdir if cdir else 1) + if im(ndir).is_positive: + if arg0.is_positive or (arg0 + 1).is_negative: + return -res + return res - 2*I*pi + elif not im(ndir).is_negative: + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + return res + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return sech + + def _eval_rewrite_as_log(self, arg, **kwargs): + return log(1/arg + sqrt(1/arg - 1) * sqrt(1/arg + 1)) + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + def _eval_rewrite_as_acosh(self, arg, **kwargs): + return acosh(1/arg) + + def _eval_rewrite_as_asinh(self, arg, **kwargs): + return sqrt(1/arg - 1)/sqrt(1 - 1/arg)*(I*asinh(I/arg, evaluate=False) + + pi*S.Half) + + def _eval_rewrite_as_atanh(self, x, **kwargs): + return (I*pi*(1 - sqrt(x)*sqrt(1/x) - I/2*sqrt(-x)/sqrt(x) - I/2*sqrt(x**2)/sqrt(-x**2)) + + sqrt(1/(x + 1))*sqrt(x + 1)*atanh(sqrt(1 - x**2))) + + def _eval_rewrite_as_acsch(self, x, **kwargs): + return sqrt(1/x - 1)/sqrt(1 - 1/x)*(pi/2 - I*acsch(I*x, evaluate=False)) + + def _eval_is_extended_real(self): + return fuzzy_and([self.args[0].is_extended_real, self.args[0].is_nonnegative, (1 - self.args[0]).is_nonnegative]) + + def _eval_is_finite(self): + return fuzzy_not(self.args[0].is_zero) + + +class acsch(InverseHyperbolicFunction): + """ + ``acsch(x)`` is the inverse hyperbolic cosecant of ``x``. + + The inverse hyperbolic cosecant function. + + Examples + ======== + + >>> from sympy import acsch, sqrt, I + >>> from sympy.abc import x + >>> acsch(x).diff(x) + -1/(x**2*sqrt(1 + x**(-2))) + >>> acsch(1).diff(x) + 0 + >>> acsch(1) + log(1 + sqrt(2)) + >>> acsch(I) + -I*pi/2 + >>> acsch(-2*I) + I*pi/6 + >>> acsch(I*(sqrt(6) - sqrt(2))) + -5*I*pi/12 + + See Also + ======== + + asinh + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hyperbolic_function + .. [2] https://dlmf.nist.gov/4.37 + .. [3] https://functions.wolfram.com/ElementaryFunctions/ArcCsch/ + + """ + + def fdiff(self, argindex=1): + if argindex == 1: + z = self.args[0] + return -1/(z**2*sqrt(1 + 1/z**2)) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.Zero + elif arg is S.NegativeInfinity: + return S.Zero + elif arg.is_zero: + return S.ComplexInfinity + elif arg is S.One: + return log(1 + sqrt(2)) + elif arg is S.NegativeOne: + return - log(1 + sqrt(2)) + + if arg.is_number: + cst_table = _acsch_table() + + if arg in cst_table: + return cst_table[arg]*I + + if arg is S.ComplexInfinity: + return S.Zero + + if arg.is_infinite: + return S.Zero + + if arg.is_zero: + return S.ComplexInfinity + + if arg.could_extract_minus_sign(): + return -cls(-arg) + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n == 0: + return log(2 / x) + elif n < 0 or n % 2 == 1: + return S.Zero + else: + x = sympify(x) + if len(previous_terms) > 2 and n > 2: + p = previous_terms[-2] + return -p * ((n - 1)*(n-2)) * x**2/(4 * (n//2)**2) + else: + k = n // 2 + R = RisingFactorial(S.Half, k) * n + F = factorial(k) * n // 2 * n // 2 + return S.NegativeOne**(k +1) * R / F * x**n / 4 + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + # Handling branch points + if x0 in (-I, I, S.Zero): + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir) + + if x0 is S.NaN: + expr = self.func(arg.as_leading_term(x)) + if expr.is_finite: + return expr + else: + return self + + if x0 is S.ComplexInfinity: + return (1/arg).as_leading_term(x) + # Handling points lying on branch cuts (-I, I) + if x0.is_imaginary and (1 + x0**2).is_positive: + ndir = arg.dir(x, cdir if cdir else 1) + if re(ndir).is_positive: + if im(x0).is_positive: + return -self.func(x0) - I*pi + elif re(ndir).is_negative: + if im(x0).is_negative: + return -self.func(x0) + I*pi + else: + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir) + return self.func(x0) + + def _eval_nseries(self, x, n, logx, cdir=0): # acsch + from sympy.series.order import O + arg = self.args[0] + arg0 = arg.subs(x, 0) + + # Handling branch points + if arg0 is I: + t = Dummy('t', positive=True) + ser = acsch(I + t**2).rewrite(log).nseries(t, 0, 2*n) + arg1 = -I + self.args[0] + f = arg1.as_leading_term(x) + g = (arg1 - f)/ f + if not g.is_meromorphic(x, 0): # cannot be expanded + return O(1) if n == 0 else -I*pi/2 + O(sqrt(x)) + res1 = sqrt(S.One + g)._eval_nseries(x, n=n, logx=logx) + res = (res1.removeO()*sqrt(f)).expand() + res = ser.removeO().subs(t, res).expand().powsimp() + O(x**n, x) + return res + + if arg0 == S.NegativeOne*I: + t = Dummy('t', positive=True) + ser = acsch(-I + t**2).rewrite(log).nseries(t, 0, 2*n) + arg1 = I + self.args[0] + f = arg1.as_leading_term(x) + g = (arg1 - f)/ f + if not g.is_meromorphic(x, 0): # cannot be expanded + return O(1) if n == 0 else I*pi/2 + O(sqrt(x)) + res1 = sqrt(S.One + g)._eval_nseries(x, n=n, logx=logx) + res = (res1.removeO()*sqrt(f)).expand() + return ser.removeO().subs(t, res).expand().powsimp() + O(x**n, x) + + res = super()._eval_nseries(x, n=n, logx=logx) + if arg0 is S.ComplexInfinity: + return res + + # Handling points lying on branch cuts (-I, I) + if arg0.is_imaginary and (1 + arg0**2).is_positive: + ndir = self.args[0].dir(x, cdir if cdir else 1) + if re(ndir).is_positive: + if im(arg0).is_positive: + return -res - I*pi + elif re(ndir).is_negative: + if im(arg0).is_negative: + return -res + I*pi + else: + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + return res + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return csch + + def _eval_rewrite_as_log(self, arg, **kwargs): + return log(1/arg + sqrt(1/arg**2 + 1)) + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + def _eval_rewrite_as_asinh(self, arg, **kwargs): + return asinh(1/arg) + + def _eval_rewrite_as_acosh(self, arg, **kwargs): + return I*(sqrt(1 - I/arg)/sqrt(I/arg - 1)* + acosh(I/arg, evaluate=False) - pi*S.Half) + + def _eval_rewrite_as_atanh(self, arg, **kwargs): + arg2 = arg**2 + arg2p1 = arg2 + 1 + return sqrt(-arg2)/arg*(pi*S.Half - + sqrt(-arg2p1**2)/arg2p1*atanh(sqrt(arg2p1))) + + def _eval_is_zero(self): + return self.args[0].is_infinite + + def _eval_is_extended_real(self): + return self.args[0].is_extended_real + + def _eval_is_finite(self): + return fuzzy_not(self.args[0].is_zero) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/integers.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/integers.py new file mode 100644 index 0000000000000000000000000000000000000000..d0b58d32399144c39133855475d70c01b70b1a3f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/integers.py @@ -0,0 +1,710 @@ +from __future__ import annotations + +from sympy.core.basic import Basic +from sympy.core.expr import Expr + +from sympy.core import Add, S +from sympy.core.evalf import get_integer_part, PrecisionExhausted +from sympy.core.function import DefinedFunction +from sympy.core.logic import fuzzy_or, fuzzy_and +from sympy.core.numbers import Integer, int_valued +from sympy.core.relational import Gt, Lt, Ge, Le, Relational, is_eq, is_le, is_lt +from sympy.core.sympify import _sympify +from sympy.functions.elementary.complexes import im, re +from sympy.multipledispatch import dispatch + +############################################################################### +######################### FLOOR and CEILING FUNCTIONS ######################### +############################################################################### + + +class RoundFunction(DefinedFunction): + """Abstract base class for rounding functions.""" + + args: tuple[Expr] + + @classmethod + def eval(cls, arg): + if (v := cls._eval_number(arg)) is not None: + return v + if (v := cls._eval_const_number(arg)) is not None: + return v + + if arg.is_integer or arg.is_finite is False: + return arg + if arg.is_imaginary or (S.ImaginaryUnit*arg).is_real: + i = im(arg) + if not i.has(S.ImaginaryUnit): + return cls(i)*S.ImaginaryUnit + return cls(arg, evaluate=False) + + # Integral, numerical, symbolic part + ipart = npart = spart = S.Zero + + # Extract integral (or complex integral) terms + intof = lambda x: int(x) if int_valued(x) else ( + x if x.is_integer else None) + for t in Add.make_args(arg): + if t.is_imaginary and (i := intof(im(t))) is not None: + ipart += i*S.ImaginaryUnit + elif (i := intof(t)) is not None: + ipart += i + elif t.is_number: + npart += t + else: + spart += t + + if not (npart or spart): + return ipart + + # Evaluate npart numerically if independent of spart + if npart and ( + not spart or + npart.is_real and (spart.is_imaginary or (S.ImaginaryUnit*spart).is_real) or + npart.is_imaginary and spart.is_real): + try: + r, i = get_integer_part( + npart, cls._dir, {}, return_ints=True) + ipart += Integer(r) + Integer(i)*S.ImaginaryUnit + npart = S.Zero + except (PrecisionExhausted, NotImplementedError): + pass + + spart += npart + if not spart: + return ipart + elif spart.is_imaginary or (S.ImaginaryUnit*spart).is_real: + return ipart + cls(im(spart), evaluate=False)*S.ImaginaryUnit + elif isinstance(spart, (floor, ceiling)): + return ipart + spart + else: + return ipart + cls(spart, evaluate=False) + + @classmethod + def _eval_number(cls, arg): + raise NotImplementedError() + + def _eval_is_finite(self): + return self.args[0].is_finite + + def _eval_is_real(self): + return self.args[0].is_real + + def _eval_is_integer(self): + return self.args[0].is_real + + +class floor(RoundFunction): + """ + Floor is a univariate function which returns the largest integer + value not greater than its argument. This implementation + generalizes floor to complex numbers by taking the floor of the + real and imaginary parts separately. + + Examples + ======== + + >>> from sympy import floor, E, I, S, Float, Rational + >>> floor(17) + 17 + >>> floor(Rational(23, 10)) + 2 + >>> floor(2*E) + 5 + >>> floor(-Float(0.567)) + -1 + >>> floor(-I/2) + -I + >>> floor(S(5)/2 + 5*I/2) + 2 + 2*I + + See Also + ======== + + sympy.functions.elementary.integers.ceiling + + References + ========== + + .. [1] "Concrete mathematics" by Graham, pp. 87 + .. [2] https://mathworld.wolfram.com/FloorFunction.html + + """ + _dir = -1 + + @classmethod + def _eval_number(cls, arg): + if arg.is_Number: + return arg.floor() + if any(isinstance(i, j) + for i in (arg, -arg) for j in (floor, ceiling)): + return arg + if arg.is_NumberSymbol: + return arg.approximation_interval(Integer)[0] + + @classmethod + def _eval_const_number(cls, arg): + if arg.is_real: + if arg.is_zero: + return S.Zero + if arg.is_positive: + num, den = arg.as_numer_denom() + s = den.is_negative + if s is None: + return None + if s: + num, den = -num, -den + # 0 <= num/den < 1 -> 0 + if is_lt(num, den): + return S.Zero + # 1 <= num/den < 2 -> 1 + if fuzzy_and([is_le(den, num), is_lt(num, 2*den)]): + return S.One + if arg.is_negative: + num, den = arg.as_numer_denom() + s = den.is_negative + if s is None: + return None + if s: + num, den = -num, -den + # -1 <= num/den < 0 -> -1 + if is_le(-den, num): + return S.NegativeOne + # -2 <= num/den < -1 -> -2 + if fuzzy_and([is_le(-2*den, num), is_lt(num, -den)]): + return Integer(-2) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.calculus.accumulationbounds import AccumBounds + arg = self.args[0] + arg0 = arg.subs(x, 0) + r = self.subs(x, 0) + if arg0 is S.NaN or isinstance(arg0, AccumBounds): + arg0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + r = floor(arg0) + if arg0.is_finite: + if arg0 == r: + ndir = arg.dir(x, cdir=cdir if cdir != 0 else 1) + if ndir.is_negative: + return r - 1 + elif ndir.is_positive: + return r + else: + raise NotImplementedError("Not sure of sign of %s" % ndir) + else: + return r + return arg.as_leading_term(x, logx=logx, cdir=cdir) + + def _eval_nseries(self, x, n, logx, cdir=0): + arg = self.args[0] + arg0 = arg.subs(x, 0) + r = self.subs(x, 0) + if arg0 is S.NaN: + arg0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + r = floor(arg0) + if arg0.is_infinite: + from sympy.calculus.accumulationbounds import AccumBounds + from sympy.series.order import Order + s = arg._eval_nseries(x, n, logx, cdir) + o = Order(1, (x, 0)) if n <= 0 else AccumBounds(-1, 0) + return s + o + if arg0 == r: + ndir = arg.dir(x, cdir=cdir if cdir != 0 else 1) + if ndir.is_negative: + return r - 1 + elif ndir.is_positive: + return r + else: + raise NotImplementedError("Not sure of sign of %s" % ndir) + else: + return r + + def _eval_is_negative(self): + return self.args[0].is_negative + + def _eval_is_nonnegative(self): + return self.args[0].is_nonnegative + + def _eval_rewrite_as_ceiling(self, arg, **kwargs): + return -ceiling(-arg) + + def _eval_rewrite_as_frac(self, arg, **kwargs): + return arg - frac(arg) + + def __le__(self, other): + other = S(other) + if self.args[0].is_real: + if other.is_integer: + return self.args[0] < other + 1 + if other.is_number and other.is_real: + return self.args[0] < ceiling(other) + if self.args[0] == other and other.is_real: + return S.true + if other is S.Infinity and self.is_finite: + return S.true + + return Le(self, other, evaluate=False) + + def __ge__(self, other): + other = S(other) + if self.args[0].is_real: + if other.is_integer: + return self.args[0] >= other + if other.is_number and other.is_real: + return self.args[0] >= ceiling(other) + if self.args[0] == other and other.is_real and other.is_noninteger: + return S.false + if other is S.NegativeInfinity and self.is_finite: + return S.true + + return Ge(self, other, evaluate=False) + + def __gt__(self, other): + other = S(other) + if self.args[0].is_real: + if other.is_integer: + return self.args[0] >= other + 1 + if other.is_number and other.is_real: + return self.args[0] >= ceiling(other) + if self.args[0] == other and other.is_real: + return S.false + if other is S.NegativeInfinity and self.is_finite: + return S.true + + return Gt(self, other, evaluate=False) + + def __lt__(self, other): + other = S(other) + if self.args[0].is_real: + if other.is_integer: + return self.args[0] < other + if other.is_number and other.is_real: + return self.args[0] < ceiling(other) + if self.args[0] == other and other.is_real and other.is_noninteger: + return S.true + if other is S.Infinity and self.is_finite: + return S.true + + return Lt(self, other, evaluate=False) + + +@dispatch(floor, Expr) +def _eval_is_eq(lhs, rhs): # noqa:F811 + return is_eq(lhs.rewrite(ceiling), rhs) or \ + is_eq(lhs.rewrite(frac),rhs) + + +class ceiling(RoundFunction): + """ + Ceiling is a univariate function which returns the smallest integer + value not less than its argument. This implementation + generalizes ceiling to complex numbers by taking the ceiling of the + real and imaginary parts separately. + + Examples + ======== + + >>> from sympy import ceiling, E, I, S, Float, Rational + >>> ceiling(17) + 17 + >>> ceiling(Rational(23, 10)) + 3 + >>> ceiling(2*E) + 6 + >>> ceiling(-Float(0.567)) + 0 + >>> ceiling(I/2) + I + >>> ceiling(S(5)/2 + 5*I/2) + 3 + 3*I + + See Also + ======== + + sympy.functions.elementary.integers.floor + + References + ========== + + .. [1] "Concrete mathematics" by Graham, pp. 87 + .. [2] https://mathworld.wolfram.com/CeilingFunction.html + + """ + _dir = 1 + + @classmethod + def _eval_number(cls, arg): + if arg.is_Number: + return arg.ceiling() + if any(isinstance(i, j) + for i in (arg, -arg) for j in (floor, ceiling)): + return arg + if arg.is_NumberSymbol: + return arg.approximation_interval(Integer)[1] + + @classmethod + def _eval_const_number(cls, arg): + if arg.is_real: + if arg.is_zero: + return S.Zero + if arg.is_positive: + num, den = arg.as_numer_denom() + s = den.is_negative + if s is None: + return None + if s: + num, den = -num, -den + # 0 < num/den <= 1 -> 1 + if is_le(num, den): + return S.One + # 1 < num/den <= 2 -> 2 + if fuzzy_and([is_lt(den, num), is_le(num, 2*den)]): + return Integer(2) + if arg.is_negative: + num, den = arg.as_numer_denom() + s = den.is_negative + if s is None: + return None + if s: + num, den = -num, -den + # -1 < num/den <= 0 -> 0 + if is_lt(-den, num): + return S.Zero + # -2 < num/den <= -1 -> -1 + if fuzzy_and([is_lt(-2*den, num), is_le(num, -den)]): + return S.NegativeOne + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.calculus.accumulationbounds import AccumBounds + arg = self.args[0] + arg0 = arg.subs(x, 0) + r = self.subs(x, 0) + if arg0 is S.NaN or isinstance(arg0, AccumBounds): + arg0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + r = ceiling(arg0) + if arg0.is_finite: + if arg0 == r: + ndir = arg.dir(x, cdir=cdir if cdir != 0 else 1) + if ndir.is_negative: + return r + elif ndir.is_positive: + return r + 1 + else: + raise NotImplementedError("Not sure of sign of %s" % ndir) + else: + return r + return arg.as_leading_term(x, logx=logx, cdir=cdir) + + def _eval_nseries(self, x, n, logx, cdir=0): + arg = self.args[0] + arg0 = arg.subs(x, 0) + r = self.subs(x, 0) + if arg0 is S.NaN: + arg0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + r = ceiling(arg0) + if arg0.is_infinite: + from sympy.calculus.accumulationbounds import AccumBounds + from sympy.series.order import Order + s = arg._eval_nseries(x, n, logx, cdir) + o = Order(1, (x, 0)) if n <= 0 else AccumBounds(0, 1) + return s + o + if arg0 == r: + ndir = arg.dir(x, cdir=cdir if cdir != 0 else 1) + if ndir.is_negative: + return r + elif ndir.is_positive: + return r + 1 + else: + raise NotImplementedError("Not sure of sign of %s" % ndir) + else: + return r + + def _eval_rewrite_as_floor(self, arg, **kwargs): + return -floor(-arg) + + def _eval_rewrite_as_frac(self, arg, **kwargs): + return arg + frac(-arg) + + def _eval_is_positive(self): + return self.args[0].is_positive + + def _eval_is_nonpositive(self): + return self.args[0].is_nonpositive + + def __lt__(self, other): + other = S(other) + if self.args[0].is_real: + if other.is_integer: + return self.args[0] <= other - 1 + if other.is_number and other.is_real: + return self.args[0] <= floor(other) + if self.args[0] == other and other.is_real: + return S.false + if other is S.Infinity and self.is_finite: + return S.true + + return Lt(self, other, evaluate=False) + + def __gt__(self, other): + other = S(other) + if self.args[0].is_real: + if other.is_integer: + return self.args[0] > other + if other.is_number and other.is_real: + return self.args[0] > floor(other) + if self.args[0] == other and other.is_real and other.is_noninteger: + return S.true + if other is S.NegativeInfinity and self.is_finite: + return S.true + + return Gt(self, other, evaluate=False) + + def __ge__(self, other): + other = S(other) + if self.args[0].is_real: + if other.is_integer: + return self.args[0] > other - 1 + if other.is_number and other.is_real: + return self.args[0] > floor(other) + if self.args[0] == other and other.is_real: + return S.true + if other is S.NegativeInfinity and self.is_finite: + return S.true + + return Ge(self, other, evaluate=False) + + def __le__(self, other): + other = S(other) + if self.args[0].is_real: + if other.is_integer: + return self.args[0] <= other + if other.is_number and other.is_real: + return self.args[0] <= floor(other) + if self.args[0] == other and other.is_real and other.is_noninteger: + return S.false + if other is S.Infinity and self.is_finite: + return S.true + + return Le(self, other, evaluate=False) + + +@dispatch(ceiling, Basic) # type:ignore +def _eval_is_eq(lhs, rhs): # noqa:F811 + return is_eq(lhs.rewrite(floor), rhs) or is_eq(lhs.rewrite(frac),rhs) + + +class frac(DefinedFunction): + r"""Represents the fractional part of x + + For real numbers it is defined [1]_ as + + .. math:: + x - \left\lfloor{x}\right\rfloor + + Examples + ======== + + >>> from sympy import Symbol, frac, Rational, floor, I + >>> frac(Rational(4, 3)) + 1/3 + >>> frac(-Rational(4, 3)) + 2/3 + + returns zero for integer arguments + + >>> n = Symbol('n', integer=True) + >>> frac(n) + 0 + + rewrite as floor + + >>> x = Symbol('x') + >>> frac(x).rewrite(floor) + x - floor(x) + + for complex arguments + + >>> r = Symbol('r', real=True) + >>> t = Symbol('t', real=True) + >>> frac(t + I*r) + I*frac(r) + frac(t) + + See Also + ======== + + sympy.functions.elementary.integers.floor + sympy.functions.elementary.integers.ceiling + + References + =========== + + .. [1] https://en.wikipedia.org/wiki/Fractional_part + .. [2] https://mathworld.wolfram.com/FractionalPart.html + + """ + @classmethod + def eval(cls, arg): + from sympy.calculus.accumulationbounds import AccumBounds + + def _eval(arg): + if arg in (S.Infinity, S.NegativeInfinity): + return AccumBounds(0, 1) + if arg.is_integer: + return S.Zero + if arg.is_number: + if arg is S.NaN: + return S.NaN + elif arg is S.ComplexInfinity: + return S.NaN + else: + return arg - floor(arg) + return cls(arg, evaluate=False) + + real, imag = S.Zero, S.Zero + for t in Add.make_args(arg): + # Two checks are needed for complex arguments + # see issue-7649 for details + if t.is_imaginary or (S.ImaginaryUnit*t).is_real: + i = im(t) + if not i.has(S.ImaginaryUnit): + imag += i + else: + real += t + else: + real += t + + real = _eval(real) + imag = _eval(imag) + return real + S.ImaginaryUnit*imag + + def _eval_rewrite_as_floor(self, arg, **kwargs): + return arg - floor(arg) + + def _eval_rewrite_as_ceiling(self, arg, **kwargs): + return arg + ceiling(-arg) + + def _eval_is_finite(self): + return True + + def _eval_is_real(self): + return self.args[0].is_extended_real + + def _eval_is_imaginary(self): + return self.args[0].is_imaginary + + def _eval_is_integer(self): + return self.args[0].is_integer + + def _eval_is_zero(self): + return fuzzy_or([self.args[0].is_zero, self.args[0].is_integer]) + + def _eval_is_negative(self): + return False + + def __ge__(self, other): + if self.is_extended_real: + other = _sympify(other) + # Check if other <= 0 + if other.is_extended_nonpositive: + return S.true + # Check if other >= 1 + res = self._value_one_or_more(other) + if res is not None: + return not(res) + return Ge(self, other, evaluate=False) + + def __gt__(self, other): + if self.is_extended_real: + other = _sympify(other) + # Check if other < 0 + res = self._value_one_or_more(other) + if res is not None: + return not(res) + # Check if other >= 1 + if other.is_extended_negative: + return S.true + return Gt(self, other, evaluate=False) + + def __le__(self, other): + if self.is_extended_real: + other = _sympify(other) + # Check if other < 0 + if other.is_extended_negative: + return S.false + # Check if other >= 1 + res = self._value_one_or_more(other) + if res is not None: + return res + return Le(self, other, evaluate=False) + + def __lt__(self, other): + if self.is_extended_real: + other = _sympify(other) + # Check if other <= 0 + if other.is_extended_nonpositive: + return S.false + # Check if other >= 1 + res = self._value_one_or_more(other) + if res is not None: + return res + return Lt(self, other, evaluate=False) + + def _value_one_or_more(self, other): + if other.is_extended_real: + if other.is_number: + res = other >= 1 + if res and not isinstance(res, Relational): + return S.true + if other.is_integer and other.is_positive: + return S.true + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.calculus.accumulationbounds import AccumBounds + arg = self.args[0] + arg0 = arg.subs(x, 0) + r = self.subs(x, 0) + + if arg0.is_finite: + if r.is_zero: + ndir = arg.dir(x, cdir=cdir) + if ndir.is_negative: + return S.One + return (arg - arg0).as_leading_term(x, logx=logx, cdir=cdir) + else: + return r + elif arg0 in (S.ComplexInfinity, S.Infinity, S.NegativeInfinity): + return AccumBounds(0, 1) + return arg.as_leading_term(x, logx=logx, cdir=cdir) + + def _eval_nseries(self, x, n, logx, cdir=0): + from sympy.series.order import Order + arg = self.args[0] + arg0 = arg.subs(x, 0) + r = self.subs(x, 0) + + if arg0.is_infinite: + from sympy.calculus.accumulationbounds import AccumBounds + o = Order(1, (x, 0)) if n <= 0 else AccumBounds(0, 1) + Order(x**n, (x, 0)) + return o + else: + res = (arg - arg0)._eval_nseries(x, n, logx=logx, cdir=cdir) + if r.is_zero: + ndir = arg.dir(x, cdir=cdir) + res += S.One if ndir.is_negative else S.Zero + else: + res += r + return res + + +@dispatch(frac, Basic) # type:ignore +def _eval_is_eq(lhs, rhs): # noqa:F811 + if (lhs.rewrite(floor) == rhs) or \ + (lhs.rewrite(ceiling) == rhs): + return True + # Check if other < 0 + if rhs.is_extended_negative: + return False + # Check if other >= 1 + res = lhs._value_one_or_more(rhs) + if res is not None: + return False diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/miscellaneous.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/miscellaneous.py new file mode 100644 index 0000000000000000000000000000000000000000..c7f3016bc7ea0d5c4ad778cf9922c941acb7fc44 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/miscellaneous.py @@ -0,0 +1,915 @@ +from sympy.core import S, sympify, NumberKind +from sympy.utilities.iterables import sift +from sympy.core.add import Add +from sympy.core.containers import Tuple +from sympy.core.operations import LatticeOp, ShortCircuit +from sympy.core.function import (Application, Lambda, + ArgumentIndexError, DefinedFunction) +from sympy.core.expr import Expr +from sympy.core.exprtools import factor_terms +from sympy.core.mod import Mod +from sympy.core.mul import Mul +from sympy.core.numbers import Rational +from sympy.core.power import Pow +from sympy.core.relational import Eq, Relational +from sympy.core.singleton import Singleton +from sympy.core.sorting import ordered +from sympy.core.symbol import Dummy +from sympy.core.rules import Transform +from sympy.core.logic import fuzzy_and, fuzzy_or, _torf +from sympy.core.traversal import walk +from sympy.core.numbers import Integer +from sympy.logic.boolalg import And, Or + + +def _minmax_as_Piecewise(op, *args): + # helper for Min/Max rewrite as Piecewise + from sympy.functions.elementary.piecewise import Piecewise + ec = [] + for i, a in enumerate(args): + c = [Relational(a, args[j], op) for j in range(i + 1, len(args))] + ec.append((a, And(*c))) + return Piecewise(*ec) + + +class IdentityFunction(Lambda, metaclass=Singleton): + """ + The identity function + + Examples + ======== + + >>> from sympy import Id, Symbol + >>> x = Symbol('x') + >>> Id(x) + x + + """ + + _symbol = Dummy('x') + + @property + def signature(self): + return Tuple(self._symbol) + + @property + def expr(self): + return self._symbol + + +Id = S.IdentityFunction + +############################################################################### +############################# ROOT and SQUARE ROOT FUNCTION ################### +############################################################################### + + +def sqrt(arg, evaluate=None): + """Returns the principal square root. + + Parameters + ========== + + evaluate : bool, optional + The parameter determines if the expression should be evaluated. + If ``None``, its value is taken from + ``global_parameters.evaluate``. + + Examples + ======== + + >>> from sympy import sqrt, Symbol, S + >>> x = Symbol('x') + + >>> sqrt(x) + sqrt(x) + + >>> sqrt(x)**2 + x + + Note that sqrt(x**2) does not simplify to x. + + >>> sqrt(x**2) + sqrt(x**2) + + This is because the two are not equal to each other in general. + For example, consider x == -1: + + >>> from sympy import Eq + >>> Eq(sqrt(x**2), x).subs(x, -1) + False + + This is because sqrt computes the principal square root, so the square may + put the argument in a different branch. This identity does hold if x is + positive: + + >>> y = Symbol('y', positive=True) + >>> sqrt(y**2) + y + + You can force this simplification by using the powdenest() function with + the force option set to True: + + >>> from sympy import powdenest + >>> sqrt(x**2) + sqrt(x**2) + >>> powdenest(sqrt(x**2), force=True) + x + + To get both branches of the square root you can use the rootof function: + + >>> from sympy import rootof + + >>> [rootof(x**2-3,i) for i in (0,1)] + [-sqrt(3), sqrt(3)] + + Although ``sqrt`` is printed, there is no ``sqrt`` function so looking for + ``sqrt`` in an expression will fail: + + >>> from sympy.utilities.misc import func_name + >>> func_name(sqrt(x)) + 'Pow' + >>> sqrt(x).has(sqrt) + False + + To find ``sqrt`` look for ``Pow`` with an exponent of ``1/2``: + + >>> (x + 1/sqrt(x)).find(lambda i: i.is_Pow and abs(i.exp) is S.Half) + {1/sqrt(x)} + + See Also + ======== + + sympy.polys.rootoftools.rootof, root, real_root + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Square_root + .. [2] https://en.wikipedia.org/wiki/Principal_value + """ + # arg = sympify(arg) is handled by Pow + return Pow(arg, S.Half, evaluate=evaluate) + + +def cbrt(arg, evaluate=None): + """Returns the principal cube root. + + Parameters + ========== + + evaluate : bool, optional + The parameter determines if the expression should be evaluated. + If ``None``, its value is taken from + ``global_parameters.evaluate``. + + Examples + ======== + + >>> from sympy import cbrt, Symbol + >>> x = Symbol('x') + + >>> cbrt(x) + x**(1/3) + + >>> cbrt(x)**3 + x + + Note that cbrt(x**3) does not simplify to x. + + >>> cbrt(x**3) + (x**3)**(1/3) + + This is because the two are not equal to each other in general. + For example, consider `x == -1`: + + >>> from sympy import Eq + >>> Eq(cbrt(x**3), x).subs(x, -1) + False + + This is because cbrt computes the principal cube root, this + identity does hold if `x` is positive: + + >>> y = Symbol('y', positive=True) + >>> cbrt(y**3) + y + + See Also + ======== + + sympy.polys.rootoftools.rootof, root, real_root + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Cube_root + .. [2] https://en.wikipedia.org/wiki/Principal_value + + """ + return Pow(arg, Rational(1, 3), evaluate=evaluate) + + +def root(arg, n, k=0, evaluate=None): + r"""Returns the *k*-th *n*-th root of ``arg``. + + Parameters + ========== + + k : int, optional + Should be an integer in $\{0, 1, ..., n-1\}$. + Defaults to the principal root if $0$. + + evaluate : bool, optional + The parameter determines if the expression should be evaluated. + If ``None``, its value is taken from + ``global_parameters.evaluate``. + + Examples + ======== + + >>> from sympy import root, Rational + >>> from sympy.abc import x, n + + >>> root(x, 2) + sqrt(x) + + >>> root(x, 3) + x**(1/3) + + >>> root(x, n) + x**(1/n) + + >>> root(x, -Rational(2, 3)) + x**(-3/2) + + To get the k-th n-th root, specify k: + + >>> root(-2, 3, 2) + -(-1)**(2/3)*2**(1/3) + + To get all n n-th roots you can use the rootof function. + The following examples show the roots of unity for n + equal 2, 3 and 4: + + >>> from sympy import rootof + + >>> [rootof(x**2 - 1, i) for i in range(2)] + [-1, 1] + + >>> [rootof(x**3 - 1,i) for i in range(3)] + [1, -1/2 - sqrt(3)*I/2, -1/2 + sqrt(3)*I/2] + + >>> [rootof(x**4 - 1,i) for i in range(4)] + [-1, 1, -I, I] + + SymPy, like other symbolic algebra systems, returns the + complex root of negative numbers. This is the principal + root and differs from the text-book result that one might + be expecting. For example, the cube root of -8 does not + come back as -2: + + >>> root(-8, 3) + 2*(-1)**(1/3) + + The real_root function can be used to either make the principal + result real (or simply to return the real root directly): + + >>> from sympy import real_root + >>> real_root(_) + -2 + >>> real_root(-32, 5) + -2 + + Alternatively, the n//2-th n-th root of a negative number can be + computed with root: + + >>> root(-32, 5, 5//2) + -2 + + See Also + ======== + + sympy.polys.rootoftools.rootof + sympy.core.intfunc.integer_nthroot + sqrt, real_root + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Square_root + .. [2] https://en.wikipedia.org/wiki/Real_root + .. [3] https://en.wikipedia.org/wiki/Root_of_unity + .. [4] https://en.wikipedia.org/wiki/Principal_value + .. [5] https://mathworld.wolfram.com/CubeRoot.html + + """ + n = sympify(n) + if k: + return Mul(Pow(arg, S.One/n, evaluate=evaluate), S.NegativeOne**(2*k/n), evaluate=evaluate) + return Pow(arg, 1/n, evaluate=evaluate) + + +def real_root(arg, n=None, evaluate=None): + r"""Return the real *n*'th-root of *arg* if possible. + + Parameters + ========== + + n : int or None, optional + If *n* is ``None``, then all instances of + $(-n)^{1/\text{odd}}$ will be changed to $-n^{1/\text{odd}}$. + This will only create a real root of a principal root. + The presence of other factors may cause the result to not be + real. + + evaluate : bool, optional + The parameter determines if the expression should be evaluated. + If ``None``, its value is taken from + ``global_parameters.evaluate``. + + Examples + ======== + + >>> from sympy import root, real_root + + >>> real_root(-8, 3) + -2 + >>> root(-8, 3) + 2*(-1)**(1/3) + >>> real_root(_) + -2 + + If one creates a non-principal root and applies real_root, the + result will not be real (so use with caution): + + >>> root(-8, 3, 2) + -2*(-1)**(2/3) + >>> real_root(_) + -2*(-1)**(2/3) + + See Also + ======== + + sympy.polys.rootoftools.rootof + sympy.core.intfunc.integer_nthroot + root, sqrt + """ + from sympy.functions.elementary.complexes import Abs, im, sign + from sympy.functions.elementary.piecewise import Piecewise + if n is not None: + return Piecewise( + (root(arg, n, evaluate=evaluate), Or(Eq(n, S.One), Eq(n, S.NegativeOne))), + (Mul(sign(arg), root(Abs(arg), n, evaluate=evaluate), evaluate=evaluate), + And(Eq(im(arg), S.Zero), Eq(Mod(n, 2), S.One))), + (root(arg, n, evaluate=evaluate), True)) + rv = sympify(arg) + n1pow = Transform(lambda x: -(-x.base)**x.exp, + lambda x: + x.is_Pow and + x.base.is_negative and + x.exp.is_Rational and + x.exp.p == 1 and x.exp.q % 2) + return rv.xreplace(n1pow) + +############################################################################### +############################# MINIMUM and MAXIMUM ############################# +############################################################################### + + +class MinMaxBase(Expr, LatticeOp): + def __new__(cls, *args, **assumptions): + from sympy.core.parameters import global_parameters + evaluate = assumptions.pop('evaluate', global_parameters.evaluate) + args = (sympify(arg) for arg in args) + + # first standard filter, for cls.zero and cls.identity + # also reshape Max(a, Max(b, c)) to Max(a, b, c) + + if evaluate: + try: + args = frozenset(cls._new_args_filter(args)) + except ShortCircuit: + return cls.zero + # remove redundant args that are easily identified + args = cls._collapse_arguments(args, **assumptions) + # find local zeros + args = cls._find_localzeros(args, **assumptions) + args = frozenset(args) + + if not args: + return cls.identity + + if len(args) == 1: + return list(args).pop() + + # base creation + obj = Expr.__new__(cls, *ordered(args), **assumptions) + obj._argset = args + return obj + + @classmethod + def _collapse_arguments(cls, args, **assumptions): + """Remove redundant args. + + Examples + ======== + + >>> from sympy import Min, Max + >>> from sympy.abc import a, b, c, d, e + + Any arg in parent that appears in any + parent-like function in any of the flat args + of parent can be removed from that sub-arg: + + >>> Min(a, Max(b, Min(a, c, d))) + Min(a, Max(b, Min(c, d))) + + If the arg of parent appears in an opposite-than parent + function in any of the flat args of parent that function + can be replaced with the arg: + + >>> Min(a, Max(b, Min(c, d, Max(a, e)))) + Min(a, Max(b, Min(a, c, d))) + """ + if not args: + return args + args = list(ordered(args)) + if cls == Min: + other = Max + else: + other = Min + + # find global comparable max of Max and min of Min if a new + # value is being introduced in these args at position 0 of + # the ordered args + if args[0].is_number: + sifted = mins, maxs = [], [] + for i in args: + for v in walk(i, Min, Max): + if v.args[0].is_comparable: + sifted[isinstance(v, Max)].append(v) + small = Min.identity + for i in mins: + v = i.args[0] + if v.is_number and (v < small) == True: + small = v + big = Max.identity + for i in maxs: + v = i.args[0] + if v.is_number and (v > big) == True: + big = v + # at the point when this function is called from __new__, + # there may be more than one numeric arg present since + # local zeros have not been handled yet, so look through + # more than the first arg + if cls == Min: + for arg in args: + if not arg.is_number: + break + if (arg < small) == True: + small = arg + elif cls == Max: + for arg in args: + if not arg.is_number: + break + if (arg > big) == True: + big = arg + T = None + if cls == Min: + if small != Min.identity: + other = Max + T = small + elif big != Max.identity: + other = Min + T = big + if T is not None: + # remove numerical redundancy + for i in range(len(args)): + a = args[i] + if isinstance(a, other): + a0 = a.args[0] + if ((a0 > T) if other == Max else (a0 < T)) == True: + args[i] = cls.identity + + # remove redundant symbolic args + def do(ai, a): + if not isinstance(ai, (Min, Max)): + return ai + cond = a in ai.args + if not cond: + return ai.func(*[do(i, a) for i in ai.args], + evaluate=False) + if isinstance(ai, cls): + return ai.func(*[do(i, a) for i in ai.args if i != a], + evaluate=False) + return a + for i, a in enumerate(args): + args[i + 1:] = [do(ai, a) for ai in args[i + 1:]] + + # factor out common elements as for + # Min(Max(x, y), Max(x, z)) -> Max(x, Min(y, z)) + # and vice versa when swapping Min/Max -- do this only for the + # easy case where all functions contain something in common; + # trying to find some optimal subset of args to modify takes + # too long + + def factor_minmax(args): + is_other = lambda arg: isinstance(arg, other) + other_args, remaining_args = sift(args, is_other, binary=True) + if not other_args: + return args + + # Min(Max(x, y, z), Max(x, y, u, v)) -> {x,y}, ({z}, {u,v}) + arg_sets = [set(arg.args) for arg in other_args] + common = set.intersection(*arg_sets) + if not common: + return args + + new_other_args = list(common) + arg_sets_diff = [arg_set - common for arg_set in arg_sets] + + # If any set is empty after removing common then all can be + # discarded e.g. Min(Max(a, b, c), Max(a, b)) -> Max(a, b) + if all(arg_sets_diff): + other_args_diff = [other(*s, evaluate=False) for s in arg_sets_diff] + new_other_args.append(cls(*other_args_diff, evaluate=False)) + + other_args_factored = other(*new_other_args, evaluate=False) + return remaining_args + [other_args_factored] + + if len(args) > 1: + args = factor_minmax(args) + + return args + + @classmethod + def _new_args_filter(cls, arg_sequence): + """ + Generator filtering args. + + first standard filter, for cls.zero and cls.identity. + Also reshape ``Max(a, Max(b, c))`` to ``Max(a, b, c)``, + and check arguments for comparability + """ + for arg in arg_sequence: + # pre-filter, checking comparability of arguments + if not isinstance(arg, Expr) or arg.is_extended_real is False or ( + arg.is_number and + not arg.is_comparable): + raise ValueError("The argument '%s' is not comparable." % arg) + + if arg == cls.zero: + raise ShortCircuit(arg) + elif arg == cls.identity: + continue + elif arg.func == cls: + yield from arg.args + else: + yield arg + + @classmethod + def _find_localzeros(cls, values, **options): + """ + Sequentially allocate values to localzeros. + + When a value is identified as being more extreme than another member it + replaces that member; if this is never true, then the value is simply + appended to the localzeros. + """ + localzeros = set() + for v in values: + is_newzero = True + localzeros_ = list(localzeros) + for z in localzeros_: + if id(v) == id(z): + is_newzero = False + else: + con = cls._is_connected(v, z) + if con: + is_newzero = False + if con is True or con == cls: + localzeros.remove(z) + localzeros.update([v]) + if is_newzero: + localzeros.update([v]) + return localzeros + + @classmethod + def _is_connected(cls, x, y): + """ + Check if x and y are connected somehow. + """ + for i in range(2): + if x == y: + return True + t, f = Max, Min + for op in "><": + for j in range(2): + try: + if op == ">": + v = x >= y + else: + v = x <= y + except TypeError: + return False # non-real arg + if not v.is_Relational: + return t if v else f + t, f = f, t + x, y = y, x + x, y = y, x # run next pass with reversed order relative to start + # simplification can be expensive, so be conservative + # in what is attempted + x = factor_terms(x - y) + y = S.Zero + + return False + + def _eval_derivative(self, s): + # f(x).diff(s) -> x.diff(s) * f.fdiff(1)(s) + i = 0 + l = [] + for a in self.args: + i += 1 + da = a.diff(s) + if da.is_zero: + continue + try: + df = self.fdiff(i) + except ArgumentIndexError: + df = super().fdiff(i) + l.append(df * da) + return Add(*l) + + def _eval_rewrite_as_Abs(self, *args, **kwargs): + from sympy.functions.elementary.complexes import Abs + s = (args[0] + self.func(*args[1:]))/2 + d = abs(args[0] - self.func(*args[1:]))/2 + return (s + d if isinstance(self, Max) else s - d).rewrite(Abs) + + def evalf(self, n=15, **options): + return self.func(*[a.evalf(n, **options) for a in self.args]) + + def n(self, *args, **kwargs): + return self.evalf(*args, **kwargs) + + _eval_is_algebraic = lambda s: _torf(i.is_algebraic for i in s.args) + _eval_is_antihermitian = lambda s: _torf(i.is_antihermitian for i in s.args) + _eval_is_commutative = lambda s: _torf(i.is_commutative for i in s.args) + _eval_is_complex = lambda s: _torf(i.is_complex for i in s.args) + _eval_is_composite = lambda s: _torf(i.is_composite for i in s.args) + _eval_is_even = lambda s: _torf(i.is_even for i in s.args) + _eval_is_finite = lambda s: _torf(i.is_finite for i in s.args) + _eval_is_hermitian = lambda s: _torf(i.is_hermitian for i in s.args) + _eval_is_imaginary = lambda s: _torf(i.is_imaginary for i in s.args) + _eval_is_infinite = lambda s: _torf(i.is_infinite for i in s.args) + _eval_is_integer = lambda s: _torf(i.is_integer for i in s.args) + _eval_is_irrational = lambda s: _torf(i.is_irrational for i in s.args) + _eval_is_negative = lambda s: _torf(i.is_negative for i in s.args) + _eval_is_noninteger = lambda s: _torf(i.is_noninteger for i in s.args) + _eval_is_nonnegative = lambda s: _torf(i.is_nonnegative for i in s.args) + _eval_is_nonpositive = lambda s: _torf(i.is_nonpositive for i in s.args) + _eval_is_nonzero = lambda s: _torf(i.is_nonzero for i in s.args) + _eval_is_odd = lambda s: _torf(i.is_odd for i in s.args) + _eval_is_polar = lambda s: _torf(i.is_polar for i in s.args) + _eval_is_positive = lambda s: _torf(i.is_positive for i in s.args) + _eval_is_prime = lambda s: _torf(i.is_prime for i in s.args) + _eval_is_rational = lambda s: _torf(i.is_rational for i in s.args) + _eval_is_real = lambda s: _torf(i.is_real for i in s.args) + _eval_is_extended_real = lambda s: _torf(i.is_extended_real for i in s.args) + _eval_is_transcendental = lambda s: _torf(i.is_transcendental for i in s.args) + _eval_is_zero = lambda s: _torf(i.is_zero for i in s.args) + + +class Max(MinMaxBase, Application): + r""" + Return, if possible, the maximum value of the list. + + When number of arguments is equal one, then + return this argument. + + When number of arguments is equal two, then + return, if possible, the value from (a, b) that is $\ge$ the other. + + In common case, when the length of list greater than 2, the task + is more complicated. Return only the arguments, which are greater + than others, if it is possible to determine directional relation. + + If is not possible to determine such a relation, return a partially + evaluated result. + + Assumptions are used to make the decision too. + + Also, only comparable arguments are permitted. + + It is named ``Max`` and not ``max`` to avoid conflicts + with the built-in function ``max``. + + + Examples + ======== + + >>> from sympy import Max, Symbol, oo + >>> from sympy.abc import x, y, z + >>> p = Symbol('p', positive=True) + >>> n = Symbol('n', negative=True) + + >>> Max(x, -2) + Max(-2, x) + >>> Max(x, -2).subs(x, 3) + 3 + >>> Max(p, -2) + p + >>> Max(x, y) + Max(x, y) + >>> Max(x, y) == Max(y, x) + True + >>> Max(x, Max(y, z)) + Max(x, y, z) + >>> Max(n, 8, p, 7, -oo) + Max(8, p) + >>> Max (1, x, oo) + oo + + * Algorithm + + The task can be considered as searching of supremums in the + directed complete partial orders [1]_. + + The source values are sequentially allocated by the isolated subsets + in which supremums are searched and result as Max arguments. + + If the resulted supremum is single, then it is returned. + + The isolated subsets are the sets of values which are only the comparable + with each other in the current set. E.g. natural numbers are comparable with + each other, but not comparable with the `x` symbol. Another example: the + symbol `x` with negative assumption is comparable with a natural number. + + Also there are "least" elements, which are comparable with all others, + and have a zero property (maximum or minimum for all elements). + For example, in case of $\infty$, the allocation operation is terminated + and only this value is returned. + + Assumption: + - if $A > B > C$ then $A > C$ + - if $A = B$ then $B$ can be removed + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Directed_complete_partial_order + .. [2] https://en.wikipedia.org/wiki/Lattice_%28order%29 + + See Also + ======== + + Min : find minimum values + """ + zero = S.Infinity + identity = S.NegativeInfinity + + def fdiff( self, argindex ): + from sympy.functions.special.delta_functions import Heaviside + n = len(self.args) + if 0 < argindex and argindex <= n: + argindex -= 1 + if n == 2: + return Heaviside(self.args[argindex] - self.args[1 - argindex]) + newargs = tuple([self.args[i] for i in range(n) if i != argindex]) + return Heaviside(self.args[argindex] - Max(*newargs)) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Heaviside(self, *args, **kwargs): + from sympy.functions.special.delta_functions import Heaviside + return Add(*[j*Mul(*[Heaviside(j - i) for i in args if i!=j]) \ + for j in args]) + + def _eval_rewrite_as_Piecewise(self, *args, **kwargs): + return _minmax_as_Piecewise('>=', *args) + + def _eval_is_positive(self): + return fuzzy_or(a.is_positive for a in self.args) + + def _eval_is_nonnegative(self): + return fuzzy_or(a.is_nonnegative for a in self.args) + + def _eval_is_negative(self): + return fuzzy_and(a.is_negative for a in self.args) + + +class Min(MinMaxBase, Application): + """ + Return, if possible, the minimum value of the list. + It is named ``Min`` and not ``min`` to avoid conflicts + with the built-in function ``min``. + + Examples + ======== + + >>> from sympy import Min, Symbol, oo + >>> from sympy.abc import x, y + >>> p = Symbol('p', positive=True) + >>> n = Symbol('n', negative=True) + + >>> Min(x, -2) + Min(-2, x) + >>> Min(x, -2).subs(x, 3) + -2 + >>> Min(p, -3) + -3 + >>> Min(x, y) + Min(x, y) + >>> Min(n, 8, p, -7, p, oo) + Min(-7, n) + + See Also + ======== + + Max : find maximum values + """ + zero = S.NegativeInfinity + identity = S.Infinity + + def fdiff( self, argindex ): + from sympy.functions.special.delta_functions import Heaviside + n = len(self.args) + if 0 < argindex and argindex <= n: + argindex -= 1 + if n == 2: + return Heaviside( self.args[1-argindex] - self.args[argindex] ) + newargs = tuple([ self.args[i] for i in range(n) if i != argindex]) + return Heaviside( Min(*newargs) - self.args[argindex] ) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Heaviside(self, *args, **kwargs): + from sympy.functions.special.delta_functions import Heaviside + return Add(*[j*Mul(*[Heaviside(i-j) for i in args if i!=j]) \ + for j in args]) + + def _eval_rewrite_as_Piecewise(self, *args, **kwargs): + return _minmax_as_Piecewise('<=', *args) + + def _eval_is_positive(self): + return fuzzy_and(a.is_positive for a in self.args) + + def _eval_is_nonnegative(self): + return fuzzy_and(a.is_nonnegative for a in self.args) + + def _eval_is_negative(self): + return fuzzy_or(a.is_negative for a in self.args) + + +class Rem(DefinedFunction): + """Returns the remainder when ``p`` is divided by ``q`` where ``p`` is finite + and ``q`` is not equal to zero. The result, ``p - int(p/q)*q``, has the same sign + as the divisor. + + Parameters + ========== + + p : Expr + Dividend. + + q : Expr + Divisor. + + Notes + ===== + + ``Rem`` corresponds to the ``%`` operator in C. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import Rem + >>> Rem(x**3, y) + Rem(x**3, y) + >>> Rem(x**3, y).subs({x: -5, y: 3}) + -2 + + See Also + ======== + + Mod + """ + kind = NumberKind + + @classmethod + def eval(cls, p, q): + """Return the function remainder if both p, q are numbers and q is not + zero. + """ + + if q.is_zero: + raise ZeroDivisionError("Division by zero") + if p is S.NaN or q is S.NaN or p.is_finite is False or q.is_finite is False: + return S.NaN + if p is S.Zero or p in (q, -q) or (p.is_integer and q == 1): + return S.Zero + + if q.is_Number: + if p.is_Number: + return p - Integer(p/q)*q diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/piecewise.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/piecewise.py new file mode 100644 index 0000000000000000000000000000000000000000..fe4a4d4f57e2c3af170dac994e11782b9ed54b8f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/piecewise.py @@ -0,0 +1,1517 @@ +from sympy.core import S, diff, Tuple, Dummy, Mul +from sympy.core.basic import Basic, as_Basic +from sympy.core.function import DefinedFunction +from sympy.core.numbers import Rational, NumberSymbol, _illegal +from sympy.core.parameters import global_parameters +from sympy.core.relational import (Lt, Gt, Eq, Ne, Relational, + _canonical, _canonical_coeff) +from sympy.core.sorting import ordered +from sympy.functions.elementary.miscellaneous import Max, Min +from sympy.logic.boolalg import (And, Boolean, distribute_and_over_or, Not, + true, false, Or, ITE, simplify_logic, to_cnf, distribute_or_over_and) +from sympy.utilities.iterables import uniq, sift, common_prefix +from sympy.utilities.misc import filldedent, func_name + +from itertools import product + +Undefined = S.NaN # Piecewise() + +class ExprCondPair(Tuple): + """Represents an expression, condition pair.""" + + def __new__(cls, expr, cond): + expr = as_Basic(expr) + if cond == True: + return Tuple.__new__(cls, expr, true) + elif cond == False: + return Tuple.__new__(cls, expr, false) + elif isinstance(cond, Basic) and cond.has(Piecewise): + cond = piecewise_fold(cond) + if isinstance(cond, Piecewise): + cond = cond.rewrite(ITE) + + if not isinstance(cond, Boolean): + raise TypeError(filldedent(''' + Second argument must be a Boolean, + not `%s`''' % func_name(cond))) + return Tuple.__new__(cls, expr, cond) + + @property + def expr(self): + """ + Returns the expression of this pair. + """ + return self.args[0] + + @property + def cond(self): + """ + Returns the condition of this pair. + """ + return self.args[1] + + @property + def is_commutative(self): + return self.expr.is_commutative + + def __iter__(self): + yield self.expr + yield self.cond + + def _eval_simplify(self, **kwargs): + return self.func(*[a.simplify(**kwargs) for a in self.args]) + + +class Piecewise(DefinedFunction): + """ + Represents a piecewise function. + + Usage: + + Piecewise( (expr,cond), (expr,cond), ... ) + - Each argument is a 2-tuple defining an expression and condition + - The conds are evaluated in turn returning the first that is True. + If any of the evaluated conds are not explicitly False, + e.g. ``x < 1``, the function is returned in symbolic form. + - If the function is evaluated at a place where all conditions are False, + nan will be returned. + - Pairs where the cond is explicitly False, will be removed and no pair + appearing after a True condition will ever be retained. If a single + pair with a True condition remains, it will be returned, even when + evaluation is False. + + Examples + ======== + + >>> from sympy import Piecewise, log, piecewise_fold + >>> from sympy.abc import x, y + >>> f = x**2 + >>> g = log(x) + >>> p = Piecewise((0, x < -1), (f, x <= 1), (g, True)) + >>> p.subs(x,1) + 1 + >>> p.subs(x,5) + log(5) + + Booleans can contain Piecewise elements: + + >>> cond = (x < y).subs(x, Piecewise((2, x < 0), (3, True))); cond + Piecewise((2, x < 0), (3, True)) < y + + The folded version of this results in a Piecewise whose + expressions are Booleans: + + >>> folded_cond = piecewise_fold(cond); folded_cond + Piecewise((2 < y, x < 0), (3 < y, True)) + + When a Boolean containing Piecewise (like cond) or a Piecewise + with Boolean expressions (like folded_cond) is used as a condition, + it is converted to an equivalent :class:`~.ITE` object: + + >>> Piecewise((1, folded_cond)) + Piecewise((1, ITE(x < 0, y > 2, y > 3))) + + When a condition is an ``ITE``, it will be converted to a simplified + Boolean expression: + + >>> piecewise_fold(_) + Piecewise((1, ((x >= 0) | (y > 2)) & ((y > 3) | (x < 0)))) + + See Also + ======== + + piecewise_fold + piecewise_exclusive + ITE + """ + + nargs = None + is_Piecewise = True + + def __new__(cls, *args, **options): + if len(args) == 0: + raise TypeError("At least one (expr, cond) pair expected.") + # (Try to) sympify args first + newargs = [] + for ec in args: + # ec could be a ExprCondPair or a tuple + pair = ExprCondPair(*getattr(ec, 'args', ec)) + cond = pair.cond + if cond is false: + continue + newargs.append(pair) + if cond is true: + break + + eval = options.pop('evaluate', global_parameters.evaluate) + if eval: + r = cls.eval(*newargs) + if r is not None: + return r + elif len(newargs) == 1 and newargs[0].cond == True: + return newargs[0].expr + + return Basic.__new__(cls, *newargs, **options) + + @classmethod + def eval(cls, *_args): + """Either return a modified version of the args or, if no + modifications were made, return None. + + Modifications that are made here: + + 1. relationals are made canonical + 2. any False conditions are dropped + 3. any repeat of a previous condition is ignored + 4. any args past one with a true condition are dropped + + If there are no args left, nan will be returned. + If there is a single arg with a True condition, its + corresponding expression will be returned. + + EXAMPLES + ======== + + >>> from sympy import Piecewise + >>> from sympy.abc import x + >>> cond = -x < -1 + >>> args = [(1, cond), (4, cond), (3, False), (2, True), (5, x < 1)] + >>> Piecewise(*args, evaluate=False) + Piecewise((1, -x < -1), (4, -x < -1), (2, True)) + >>> Piecewise(*args) + Piecewise((1, x > 1), (2, True)) + """ + if not _args: + return Undefined + + if len(_args) == 1 and _args[0][-1] == True: + return _args[0][0] + + newargs = _piecewise_collapse_arguments(_args) + + # some conditions may have been redundant + missing = len(newargs) != len(_args) + # some conditions may have changed + same = all(a == b for a, b in zip(newargs, _args)) + # if either change happened we return the expr with the + # updated args + if not newargs: + raise ValueError(filldedent(''' + There are no conditions (or none that + are not trivially false) to define an + expression.''')) + if missing or not same: + return cls(*newargs) + + def doit(self, **hints): + """ + Evaluate this piecewise function. + """ + newargs = [] + for e, c in self.args: + if hints.get('deep', True): + if isinstance(e, Basic): + newe = e.doit(**hints) + if newe != self: + e = newe + if isinstance(c, Basic): + c = c.doit(**hints) + newargs.append((e, c)) + return self.func(*newargs) + + def _eval_simplify(self, **kwargs): + return piecewise_simplify(self, **kwargs) + + def _eval_as_leading_term(self, x, logx, cdir): + for e, c in self.args: + if c == True or c.subs(x, 0) == True: + return e.as_leading_term(x) + + def _eval_adjoint(self): + return self.func(*[(e.adjoint(), c) for e, c in self.args]) + + def _eval_conjugate(self): + return self.func(*[(e.conjugate(), c) for e, c in self.args]) + + def _eval_derivative(self, x): + return self.func(*[(diff(e, x), c) for e, c in self.args]) + + def _eval_evalf(self, prec): + return self.func(*[(e._evalf(prec), c) for e, c in self.args]) + + def _eval_is_meromorphic(self, x, a): + # Conditions often implicitly assume that the argument is real. + # Hence, there needs to be some check for as_set. + if not a.is_real: + return None + + # Then, scan ExprCondPairs in the given order to find a piece that would contain a, + # possibly as a boundary point. + for e, c in self.args: + cond = c.subs(x, a) + + if cond.is_Relational: + return None + if a in c.as_set().boundary: + return None + # Apply expression if a is an interior point of the domain of e. + if cond: + return e._eval_is_meromorphic(x, a) + + def piecewise_integrate(self, x, **kwargs): + """Return the Piecewise with each expression being + replaced with its antiderivative. To obtain a continuous + antiderivative, use the :func:`~.integrate` function or method. + + Examples + ======== + + >>> from sympy import Piecewise + >>> from sympy.abc import x + >>> p = Piecewise((0, x < 0), (1, x < 1), (2, True)) + >>> p.piecewise_integrate(x) + Piecewise((0, x < 0), (x, x < 1), (2*x, True)) + + Note that this does not give a continuous function, e.g. + at x = 1 the 3rd condition applies and the antiderivative + there is 2*x so the value of the antiderivative is 2: + + >>> anti = _ + >>> anti.subs(x, 1) + 2 + + The continuous derivative accounts for the integral *up to* + the point of interest, however: + + >>> p.integrate(x) + Piecewise((0, x < 0), (x, x < 1), (2*x - 1, True)) + >>> _.subs(x, 1) + 1 + + See Also + ======== + Piecewise._eval_integral + """ + from sympy.integrals import integrate + return self.func(*[(integrate(e, x, **kwargs), c) for e, c in self.args]) + + def _handle_irel(self, x, handler): + """Return either None (if the conditions of self depend only on x) else + a Piecewise expression whose expressions (handled by the handler that + was passed) are paired with the governing x-independent relationals, + e.g. Piecewise((A, a(x) & b(y)), (B, c(x) | c(y)) -> + Piecewise( + (handler(Piecewise((A, a(x) & True), (B, c(x) | True)), b(y) & c(y)), + (handler(Piecewise((A, a(x) & True), (B, c(x) | False)), b(y)), + (handler(Piecewise((A, a(x) & False), (B, c(x) | True)), c(y)), + (handler(Piecewise((A, a(x) & False), (B, c(x) | False)), True)) + """ + # identify governing relationals + rel = self.atoms(Relational) + irel = list(ordered([r for r in rel if x not in r.free_symbols + and r not in (S.true, S.false)])) + if irel: + args = {} + exprinorder = [] + for truth in product((1, 0), repeat=len(irel)): + reps = dict(zip(irel, truth)) + # only store the true conditions since the false are implied + # when they appear lower in the Piecewise args + if 1 not in truth: + cond = None # flag this one so it doesn't get combined + else: + andargs = Tuple(*[i for i in reps if reps[i]]) + free = list(andargs.free_symbols) + if len(free) == 1: + from sympy.solvers.inequalities import ( + reduce_inequalities, _solve_inequality) + try: + t = reduce_inequalities(andargs, free[0]) + # ValueError when there are potentially + # nonvanishing imaginary parts + except (ValueError, NotImplementedError): + # at least isolate free symbol on left + t = And(*[_solve_inequality( + a, free[0], linear=True) + for a in andargs]) + else: + t = And(*andargs) + if t is S.false: + continue # an impossible combination + cond = t + expr = handler(self.xreplace(reps)) + if isinstance(expr, self.func) and len(expr.args) == 1: + expr, econd = expr.args[0] + cond = And(econd, True if cond is None else cond) + # the ec pairs are being collected since all possibilities + # are being enumerated, but don't put the last one in since + # its expr might match a previous expression and it + # must appear last in the args + if cond is not None: + args.setdefault(expr, []).append(cond) + # but since we only store the true conditions we must maintain + # the order so that the expression with the most true values + # comes first + exprinorder.append(expr) + # convert collected conditions as args of Or + for k in args: + args[k] = Or(*args[k]) + # take them in the order obtained + args = [(e, args[e]) for e in uniq(exprinorder)] + # add in the last arg + args.append((expr, True)) + return Piecewise(*args) + + def _eval_integral(self, x, _first=True, **kwargs): + """Return the indefinite integral of the + Piecewise such that subsequent substitution of x with a + value will give the value of the integral (not including + the constant of integration) up to that point. To only + integrate the individual parts of Piecewise, use the + ``piecewise_integrate`` method. + + Examples + ======== + + >>> from sympy import Piecewise + >>> from sympy.abc import x + >>> p = Piecewise((0, x < 0), (1, x < 1), (2, True)) + >>> p.integrate(x) + Piecewise((0, x < 0), (x, x < 1), (2*x - 1, True)) + >>> p.piecewise_integrate(x) + Piecewise((0, x < 0), (x, x < 1), (2*x, True)) + + See Also + ======== + Piecewise.piecewise_integrate + """ + from sympy.integrals.integrals import integrate + + if _first: + def handler(ipw): + if isinstance(ipw, self.func): + return ipw._eval_integral(x, _first=False, **kwargs) + else: + return ipw.integrate(x, **kwargs) + irv = self._handle_irel(x, handler) + if irv is not None: + return irv + + # handle a Piecewise from -oo to oo with and no x-independent relationals + # ----------------------------------------------------------------------- + ok, abei = self._intervals(x) + if not ok: + from sympy.integrals.integrals import Integral + return Integral(self, x) # unevaluated + + pieces = [(a, b) for a, b, _, _ in abei] + oo = S.Infinity + done = [(-oo, oo, -1)] + for k, p in enumerate(pieces): + if p == (-oo, oo): + # all undone intervals will get this key + for j, (a, b, i) in enumerate(done): + if i == -1: + done[j] = a, b, k + break # nothing else to consider + N = len(done) - 1 + for j, (a, b, i) in enumerate(reversed(done)): + if i == -1: + j = N - j + done[j: j + 1] = _clip(p, (a, b), k) + done = [(a, b, i) for a, b, i in done if a != b] + + # append an arg if there is a hole so a reference to + # argument -1 will give Undefined + if any(i == -1 for (a, b, i) in done): + abei.append((-oo, oo, Undefined, -1)) + + # return the sum of the intervals + args = [] + sum = None + for a, b, i in done: + anti = integrate(abei[i][-2], x, **kwargs) + if sum is None: + sum = anti + else: + sum = sum.subs(x, a) + e = anti._eval_interval(x, a, x) + if sum.has(*_illegal) or e.has(*_illegal): + sum = anti + else: + sum += e + # see if we know whether b is contained in original + # condition + if b is S.Infinity: + cond = True + elif self.args[abei[i][-1]].cond.subs(x, b) == False: + cond = (x < b) + else: + cond = (x <= b) + args.append((sum, cond)) + return Piecewise(*args) + + def _eval_interval(self, sym, a, b, _first=True): + """Evaluates the function along the sym in a given interval [a, b]""" + # FIXME: Currently complex intervals are not supported. A possible + # replacement algorithm, discussed in issue 5227, can be found in the + # following papers; + # http://portal.acm.org/citation.cfm?id=281649 + # http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.70.4127&rep=rep1&type=pdf + + if a is None or b is None: + # In this case, it is just simple substitution + return super()._eval_interval(sym, a, b) + else: + x, lo, hi = map(as_Basic, (sym, a, b)) + + if _first: # get only x-dependent relationals + def handler(ipw): + if isinstance(ipw, self.func): + return ipw._eval_interval(x, lo, hi, _first=None) + else: + return ipw._eval_interval(x, lo, hi) + irv = self._handle_irel(x, handler) + if irv is not None: + return irv + + if (lo < hi) is S.false or ( + lo is S.Infinity or hi is S.NegativeInfinity): + rv = self._eval_interval(x, hi, lo, _first=False) + if isinstance(rv, Piecewise): + rv = Piecewise(*[(-e, c) for e, c in rv.args]) + else: + rv = -rv + return rv + + if (lo < hi) is S.true or ( + hi is S.Infinity or lo is S.NegativeInfinity): + pass + else: + _a = Dummy('lo') + _b = Dummy('hi') + a = lo if lo.is_comparable else _a + b = hi if hi.is_comparable else _b + pos = self._eval_interval(x, a, b, _first=False) + if a == _a and b == _b: + # it's purely symbolic so just swap lo and hi and + # change the sign to get the value for when lo > hi + neg, pos = (-pos.xreplace({_a: hi, _b: lo}), + pos.xreplace({_a: lo, _b: hi})) + else: + # at least one of the bounds was comparable, so allow + # _eval_interval to use that information when computing + # the interval with lo and hi reversed + neg, pos = (-self._eval_interval(x, hi, lo, _first=False), + pos.xreplace({_a: lo, _b: hi})) + + # allow simplification based on ordering of lo and hi + p = Dummy('', positive=True) + if lo.is_Symbol: + pos = pos.xreplace({lo: hi - p}).xreplace({p: hi - lo}) + neg = neg.xreplace({lo: hi + p}).xreplace({p: lo - hi}) + elif hi.is_Symbol: + pos = pos.xreplace({hi: lo + p}).xreplace({p: hi - lo}) + neg = neg.xreplace({hi: lo - p}).xreplace({p: lo - hi}) + # evaluate limits that may have unevaluate Min/Max + touch = lambda _: _.replace( + lambda x: isinstance(x, (Min, Max)), + lambda x: x.func(*x.args)) + neg = touch(neg) + pos = touch(pos) + # assemble return expression; make the first condition be Lt + # b/c then the first expression will look the same whether + # the lo or hi limit is symbolic + if a == _a: # the lower limit was symbolic + rv = Piecewise( + (pos, + lo < hi), + (neg, + True)) + else: + rv = Piecewise( + (neg, + hi < lo), + (pos, + True)) + + if rv == Undefined: + raise ValueError("Can't integrate across undefined region.") + if any(isinstance(i, Piecewise) for i in (pos, neg)): + rv = piecewise_fold(rv) + return rv + + # handle a Piecewise with lo <= hi and no x-independent relationals + # ----------------------------------------------------------------- + ok, abei = self._intervals(x) + if not ok: + from sympy.integrals.integrals import Integral + # not being able to do the interval of f(x) can + # be stated as not being able to do the integral + # of f'(x) over the same range + return Integral(self.diff(x), (x, lo, hi)) # unevaluated + + pieces = [(a, b) for a, b, _, _ in abei] + done = [(lo, hi, -1)] + oo = S.Infinity + for k, p in enumerate(pieces): + if p[:2] == (-oo, oo): + # all undone intervals will get this key + for j, (a, b, i) in enumerate(done): + if i == -1: + done[j] = a, b, k + break # nothing else to consider + N = len(done) - 1 + for j, (a, b, i) in enumerate(reversed(done)): + if i == -1: + j = N - j + done[j: j + 1] = _clip(p, (a, b), k) + done = [(a, b, i) for a, b, i in done if a != b] + + # return the sum of the intervals + sum = S.Zero + upto = None + for a, b, i in done: + if i == -1: + if upto is None: + return Undefined + # TODO simplify hi <= upto + return Piecewise((sum, hi <= upto), (Undefined, True)) + sum += abei[i][-2]._eval_interval(x, a, b) + upto = b + return sum + + def _intervals(self, sym, err_on_Eq=False): + r"""Return a bool and a message (when bool is False), else a + list of unique tuples, (a, b, e, i), where a and b + are the lower and upper bounds in which the expression e of + argument i in self is defined and $a < b$ (when involving + numbers) or $a \le b$ when involving symbols. + + If there are any relationals not involving sym, or any + relational cannot be solved for sym, the bool will be False + a message be given as the second return value. The calling + routine should have removed such relationals before calling + this routine. + + The evaluated conditions will be returned as ranges. + Discontinuous ranges will be returned separately with + identical expressions. The first condition that evaluates to + True will be returned as the last tuple with a, b = -oo, oo. + """ + from sympy.solvers.inequalities import _solve_inequality + + assert isinstance(self, Piecewise) + + def nonsymfail(cond): + return False, filldedent(''' + A condition not involving + %s appeared: %s''' % (sym, cond)) + + def _solve_relational(r): + if sym not in r.free_symbols: + return nonsymfail(r) + try: + rv = _solve_inequality(r, sym) + except NotImplementedError: + return False, 'Unable to solve relational %s for %s.' % (r, sym) + if isinstance(rv, Relational): + free = rv.args[1].free_symbols + if rv.args[0] != sym or sym in free: + return False, 'Unable to solve relational %s for %s.' % (r, sym) + if rv.rel_op == '==': + # this equality has been affirmed to have the form + # Eq(sym, rhs) where rhs is sym-free; it represents + # a zero-width interval which will be ignored + # whether it is an isolated condition or contained + # within an And or an Or + rv = S.false + elif rv.rel_op == '!=': + try: + rv = Or(sym < rv.rhs, sym > rv.rhs) + except TypeError: + # e.g. x != I ==> all real x satisfy + rv = S.true + elif rv == (S.NegativeInfinity < sym) & (sym < S.Infinity): + rv = S.true + return True, rv + + args = list(self.args) + # make self canonical wrt Relationals + keys = self.atoms(Relational) + reps = {} + for r in keys: + ok, s = _solve_relational(r) + if ok != True: + return False, ok + reps[r] = s + # process args individually so if any evaluate, their position + # in the original Piecewise will be known + args = [i.xreplace(reps) for i in self.args] + + # precondition args + expr_cond = [] + default = idefault = None + for i, (expr, cond) in enumerate(args): + if cond is S.false: + continue + if cond is S.true: + default = expr + idefault = i + break + if isinstance(cond, Eq): + # unanticipated condition, but it is here in case a + # replacement caused an Eq to appear + if err_on_Eq: + return False, 'encountered Eq condition: %s' % cond + continue # zero width interval + + cond = to_cnf(cond) + if isinstance(cond, And): + cond = distribute_or_over_and(cond) + + if isinstance(cond, Or): + expr_cond.extend( + [(i, expr, o) for o in cond.args + if not isinstance(o, Eq)]) + elif cond is not S.false: + expr_cond.append((i, expr, cond)) + elif cond is S.true: + default = expr + idefault = i + break + + # determine intervals represented by conditions + int_expr = [] + for iarg, expr, cond in expr_cond: + if isinstance(cond, And): + lower = S.NegativeInfinity + upper = S.Infinity + exclude = [] + for cond2 in cond.args: + if not isinstance(cond2, Relational): + return False, 'expecting only Relationals' + if isinstance(cond2, Eq): + lower = upper # ignore + if err_on_Eq: + return False, 'encountered secondary Eq condition' + break + elif isinstance(cond2, Ne): + l, r = cond2.args + if l == sym: + exclude.append(r) + elif r == sym: + exclude.append(l) + else: + return nonsymfail(cond2) + continue + elif cond2.lts == sym: + upper = Min(cond2.gts, upper) + elif cond2.gts == sym: + lower = Max(cond2.lts, lower) + else: + return nonsymfail(cond2) # should never get here + if exclude: + exclude = list(ordered(exclude)) + newcond = [] + for i, e in enumerate(exclude): + if e < lower == True or e > upper == True: + continue + if not newcond: + newcond.append((None, lower)) # add a primer + newcond.append((newcond[-1][1], e)) + newcond.append((newcond[-1][1], upper)) + newcond.pop(0) # remove the primer + expr_cond.extend([(iarg, expr, And(i[0] < sym, sym < i[1])) for i in newcond]) + continue + elif isinstance(cond, Relational) and cond.rel_op != '!=': + lower, upper = cond.lts, cond.gts # part 1: initialize with givens + if cond.lts == sym: # part 1a: expand the side ... + lower = S.NegativeInfinity # e.g. x <= 0 ---> -oo <= 0 + elif cond.gts == sym: # part 1a: ... that can be expanded + upper = S.Infinity # e.g. x >= 0 ---> oo >= 0 + else: + return nonsymfail(cond) + else: + return False, 'unrecognized condition: %s' % cond + + upper = Max(lower, upper) + if err_on_Eq and lower == upper: + return False, 'encountered Eq condition' + if (lower >= upper) is not S.true: + int_expr.append((lower, upper, expr, iarg)) + + if default is not None: + int_expr.append( + (S.NegativeInfinity, S.Infinity, default, idefault)) + + return True, list(uniq(int_expr)) + + def _eval_nseries(self, x, n, logx, cdir=0): + args = [(ec.expr._eval_nseries(x, n, logx), ec.cond) for ec in self.args] + return self.func(*args) + + def _eval_power(self, s): + return self.func(*[(e**s, c) for e, c in self.args]) + + def _eval_subs(self, old, new): + # this is strictly not necessary, but we can keep track + # of whether True or False conditions arise and be + # somewhat more efficient by avoiding other substitutions + # and avoiding invalid conditions that appear after a + # True condition + args = list(self.args) + args_exist = False + for i, (e, c) in enumerate(args): + c = c._subs(old, new) + if c != False: + args_exist = True + e = e._subs(old, new) + args[i] = (e, c) + if c == True: + break + if not args_exist: + args = ((Undefined, True),) + return self.func(*args) + + def _eval_transpose(self): + return self.func(*[(e.transpose(), c) for e, c in self.args]) + + def _eval_template_is_attr(self, is_attr): + b = None + for expr, _ in self.args: + a = getattr(expr, is_attr) + if a is None: + return + if b is None: + b = a + elif b is not a: + return + return b + + _eval_is_finite = lambda self: self._eval_template_is_attr( + 'is_finite') + _eval_is_complex = lambda self: self._eval_template_is_attr('is_complex') + _eval_is_even = lambda self: self._eval_template_is_attr('is_even') + _eval_is_imaginary = lambda self: self._eval_template_is_attr( + 'is_imaginary') + _eval_is_integer = lambda self: self._eval_template_is_attr('is_integer') + _eval_is_irrational = lambda self: self._eval_template_is_attr( + 'is_irrational') + _eval_is_negative = lambda self: self._eval_template_is_attr('is_negative') + _eval_is_nonnegative = lambda self: self._eval_template_is_attr( + 'is_nonnegative') + _eval_is_nonpositive = lambda self: self._eval_template_is_attr( + 'is_nonpositive') + _eval_is_nonzero = lambda self: self._eval_template_is_attr( + 'is_nonzero') + _eval_is_odd = lambda self: self._eval_template_is_attr('is_odd') + _eval_is_polar = lambda self: self._eval_template_is_attr('is_polar') + _eval_is_positive = lambda self: self._eval_template_is_attr('is_positive') + _eval_is_extended_real = lambda self: self._eval_template_is_attr( + 'is_extended_real') + _eval_is_extended_positive = lambda self: self._eval_template_is_attr( + 'is_extended_positive') + _eval_is_extended_negative = lambda self: self._eval_template_is_attr( + 'is_extended_negative') + _eval_is_extended_nonzero = lambda self: self._eval_template_is_attr( + 'is_extended_nonzero') + _eval_is_extended_nonpositive = lambda self: self._eval_template_is_attr( + 'is_extended_nonpositive') + _eval_is_extended_nonnegative = lambda self: self._eval_template_is_attr( + 'is_extended_nonnegative') + _eval_is_real = lambda self: self._eval_template_is_attr('is_real') + _eval_is_zero = lambda self: self._eval_template_is_attr( + 'is_zero') + + @classmethod + def __eval_cond(cls, cond): + """Return the truth value of the condition.""" + if cond == True: + return True + if isinstance(cond, Eq): + try: + diff = cond.lhs - cond.rhs + if diff.is_commutative: + return diff.is_zero + except TypeError: + pass + + def as_expr_set_pairs(self, domain=None): + """Return tuples for each argument of self that give + the expression and the interval in which it is valid + which is contained within the given domain. + If a condition cannot be converted to a set, an error + will be raised. The variable of the conditions is + assumed to be real; sets of real values are returned. + + Examples + ======== + + >>> from sympy import Piecewise, Interval + >>> from sympy.abc import x + >>> p = Piecewise( + ... (1, x < 2), + ... (2,(x > 0) & (x < 4)), + ... (3, True)) + >>> p.as_expr_set_pairs() + [(1, Interval.open(-oo, 2)), + (2, Interval.Ropen(2, 4)), + (3, Interval(4, oo))] + >>> p.as_expr_set_pairs(Interval(0, 3)) + [(1, Interval.Ropen(0, 2)), + (2, Interval(2, 3))] + """ + if domain is None: + domain = S.Reals + exp_sets = [] + U = domain + complex = not domain.is_subset(S.Reals) + cond_free = set() + for expr, cond in self.args: + cond_free |= cond.free_symbols + if len(cond_free) > 1: + raise NotImplementedError(filldedent(''' + multivariate conditions are not handled.''')) + if complex: + for i in cond.atoms(Relational): + if not isinstance(i, (Eq, Ne)): + raise ValueError(filldedent(''' + Inequalities in the complex domain are + not supported. Try the real domain by + setting domain=S.Reals''')) + cond_int = U.intersect(cond.as_set()) + U = U - cond_int + if cond_int != S.EmptySet: + exp_sets.append((expr, cond_int)) + return exp_sets + + def _eval_rewrite_as_ITE(self, *args, **kwargs): + byfree = {} + args = list(args) + default = any(c == True for b, c in args) + for i, (b, c) in enumerate(args): + if not isinstance(b, Boolean) and b != True: + raise TypeError(filldedent(''' + Expecting Boolean or bool but got `%s` + ''' % func_name(b))) + if c == True: + break + # loop over independent conditions for this b + for c in c.args if isinstance(c, Or) else [c]: + free = c.free_symbols + x = free.pop() + try: + byfree[x] = byfree.setdefault( + x, S.EmptySet).union(c.as_set()) + except NotImplementedError: + if not default: + raise NotImplementedError(filldedent(''' + A method to determine whether a multivariate + conditional is consistent with a complete coverage + of all variables has not been implemented so the + rewrite is being stopped after encountering `%s`. + This error would not occur if a default expression + like `(foo, True)` were given. + ''' % c)) + if byfree[x] in (S.UniversalSet, S.Reals): + # collapse the ith condition to True and break + args[i] = list(args[i]) + c = args[i][1] = True + break + if c == True: + break + if c != True: + raise ValueError(filldedent(''' + Conditions must cover all reals or a final default + condition `(foo, True)` must be given. + ''')) + last, _ = args[i] # ignore all past ith arg + for a, c in reversed(args[:i]): + last = ITE(c, a, last) + return _canonical(last) + + def _eval_rewrite_as_KroneckerDelta(self, *args, **kwargs): + from sympy.functions.special.tensor_functions import KroneckerDelta + + rules = { + And: [False, False], + Or: [True, True], + Not: [True, False], + Eq: [None, None], + Ne: [None, None] + } + + class UnrecognizedCondition(Exception): + pass + + def rewrite(cond): + if isinstance(cond, Eq): + return KroneckerDelta(*cond.args) + if isinstance(cond, Ne): + return 1 - KroneckerDelta(*cond.args) + + cls, args = type(cond), cond.args + if cls not in rules: + raise UnrecognizedCondition(cls) + + b1, b2 = rules[cls] + k = Mul(*[1 - rewrite(c) for c in args]) if b1 else Mul(*[rewrite(c) for c in args]) + + if b2: + return 1 - k + return k + + conditions = [] + true_value = None + for value, cond in args: + if type(cond) in rules: + conditions.append((value, cond)) + elif cond is S.true: + if true_value is None: + true_value = value + else: + return + + if true_value is not None: + result = true_value + + for value, cond in conditions[::-1]: + try: + k = rewrite(cond) + result = k * value + (1 - k) * result + except UnrecognizedCondition: + return + + return result + + +def piecewise_fold(expr, evaluate=True): + """ + Takes an expression containing a piecewise function and returns the + expression in piecewise form. In addition, any ITE conditions are + rewritten in negation normal form and simplified. + + The final Piecewise is evaluated (default) but if the raw form + is desired, send ``evaluate=False``; if trivial evaluation is + desired, send ``evaluate=None`` and duplicate conditions and + processing of True and False will be handled. + + Examples + ======== + + >>> from sympy import Piecewise, piecewise_fold, S + >>> from sympy.abc import x + >>> p = Piecewise((x, x < 1), (1, S(1) <= x)) + >>> piecewise_fold(x*p) + Piecewise((x**2, x < 1), (x, True)) + + See Also + ======== + + Piecewise + piecewise_exclusive + """ + if not isinstance(expr, Basic) or not expr.has(Piecewise): + return expr + + new_args = [] + if isinstance(expr, (ExprCondPair, Piecewise)): + for e, c in expr.args: + if not isinstance(e, Piecewise): + e = piecewise_fold(e) + # we don't keep Piecewise in condition because + # it has to be checked to see that it's complete + # and we convert it to ITE at that time + assert not c.has(Piecewise) # pragma: no cover + if isinstance(c, ITE): + c = c.to_nnf() + c = simplify_logic(c, form='cnf') + if isinstance(e, Piecewise): + new_args.extend([(piecewise_fold(ei), And(ci, c)) + for ei, ci in e.args]) + else: + new_args.append((e, c)) + else: + # Given + # P1 = Piecewise((e11, c1), (e12, c2), A) + # P2 = Piecewise((e21, c1), (e22, c2), B) + # ... + # the folding of f(P1, P2) is trivially + # Piecewise( + # (f(e11, e21), c1), + # (f(e12, e22), c2), + # (f(Piecewise(A), Piecewise(B)), True)) + # Certain objects end up rewriting themselves as thus, so + # we do that grouping before the more generic folding. + # The following applies this idea when f = Add or f = Mul + # (and the expression is commutative). + if expr.is_Add or expr.is_Mul and expr.is_commutative: + p, args = sift(expr.args, lambda x: x.is_Piecewise, binary=True) + pc = sift(p, lambda x: tuple([c for e,c in x.args])) + for c in list(ordered(pc)): + if len(pc[c]) > 1: + pargs = [list(i.args) for i in pc[c]] + # the first one is the same; there may be more + com = common_prefix(*[ + [i.cond for i in j] for j in pargs]) + n = len(com) + collected = [] + for i in range(n): + collected.append(( + expr.func(*[ai[i].expr for ai in pargs]), + com[i])) + remains = [] + for a in pargs: + if n == len(a): # no more args + continue + if a[n].cond == True: # no longer Piecewise + remains.append(a[n].expr) + else: # restore the remaining Piecewise + remains.append( + Piecewise(*a[n:], evaluate=False)) + if remains: + collected.append((expr.func(*remains), True)) + args.append(Piecewise(*collected, evaluate=False)) + continue + args.extend(pc[c]) + else: + args = expr.args + # fold + folded = list(map(piecewise_fold, args)) + for ec in product(*[ + (i.args if isinstance(i, Piecewise) else + [(i, true)]) for i in folded]): + e, c = zip(*ec) + new_args.append((expr.func(*e), And(*c))) + + if evaluate is None: + # don't return duplicate conditions, otherwise don't evaluate + new_args = list(reversed([(e, c) for c, e in { + c: e for e, c in reversed(new_args)}.items()])) + rv = Piecewise(*new_args, evaluate=evaluate) + if evaluate is None and len(rv.args) == 1 and rv.args[0].cond == True: + return rv.args[0].expr + if any(s.expr.has(Piecewise) for p in rv.atoms(Piecewise) for s in p.args): + return piecewise_fold(rv) + return rv + + +def _clip(A, B, k): + """Return interval B as intervals that are covered by A (keyed + to k) and all other intervals of B not covered by A keyed to -1. + + The reference point of each interval is the rhs; if the lhs is + greater than the rhs then an interval of zero width interval will + result, e.g. (4, 1) is treated like (1, 1). + + Examples + ======== + + >>> from sympy.functions.elementary.piecewise import _clip + >>> from sympy import Tuple + >>> A = Tuple(1, 3) + >>> B = Tuple(2, 4) + >>> _clip(A, B, 0) + [(2, 3, 0), (3, 4, -1)] + + Interpretation: interval portion (2, 3) of interval (2, 4) is + covered by interval (1, 3) and is keyed to 0 as requested; + interval (3, 4) was not covered by (1, 3) and is keyed to -1. + """ + a, b = B + c, d = A + c, d = Min(Max(c, a), b), Min(Max(d, a), b) + a = Min(a, b) + p = [] + if a != c: + p.append((a, c, -1)) + else: + pass + if c != d: + p.append((c, d, k)) + else: + pass + if b != d: + if d == c and p and p[-1][-1] == -1: + p[-1] = p[-1][0], b, -1 + else: + p.append((d, b, -1)) + else: + pass + + return p + + +def piecewise_simplify_arguments(expr, **kwargs): + from sympy.simplify.simplify import simplify + + # simplify conditions + f1 = expr.args[0].cond.free_symbols + args = None + if len(f1) == 1 and not expr.atoms(Eq): + x = f1.pop() + # this won't return intervals involving Eq + # and it won't handle symbols treated as + # booleans + ok, abe_ = expr._intervals(x, err_on_Eq=True) + def include(c, x, a): + "return True if c.subs(x, a) is True, else False" + try: + return c.subs(x, a) == True + except TypeError: + return False + if ok: + args = [] + covered = S.EmptySet + from sympy.sets.sets import Interval + for a, b, e, i in abe_: + c = expr.args[i].cond + incl_a = include(c, x, a) + incl_b = include(c, x, b) + iv = Interval(a, b, not incl_a, not incl_b) + cset = iv - covered + if not cset: + continue + try: + a = cset.inf + except NotImplementedError: + pass # continue with the given `a` + else: + incl_a = include(c, x, a) + if incl_a and incl_b: + if a.is_infinite and b.is_infinite: + c = S.true + elif b.is_infinite: + c = (x > a) if a in covered else (x >= a) + elif a.is_infinite: + c = (x <= b) + elif a in covered: + c = And(a < x, x <= b) + else: + c = And(a <= x, x <= b) + elif incl_a: + if a.is_infinite: + c = (x < b) + elif a in covered: + c = And(a < x, x < b) + else: + c = And(a <= x, x < b) + elif incl_b: + if b.is_infinite: + c = (x > a) + else: + c = And(a < x, x <= b) + else: + if a in covered: + c = (x < b) + else: + c = And(a < x, x < b) + covered |= iv + if a is S.NegativeInfinity and incl_a: + covered |= {S.NegativeInfinity} + if b is S.Infinity and incl_b: + covered |= {S.Infinity} + args.append((e, c)) + if not S.Reals.is_subset(covered): + args.append((Undefined, True)) + if args is None: + args = list(expr.args) + for i in range(len(args)): + e, c = args[i] + if isinstance(c, Basic): + c = simplify(c, **kwargs) + args[i] = (e, c) + + # simplify expressions + doit = kwargs.pop('doit', None) + for i in range(len(args)): + e, c = args[i] + if isinstance(e, Basic): + # Skip doit to avoid growth at every call for some integrals + # and sums, see sympy/sympy#17165 + newe = simplify(e, doit=False, **kwargs) + if newe != e: + e = newe + args[i] = (e, c) + + # restore kwargs flag + if doit is not None: + kwargs['doit'] = doit + + return Piecewise(*args) + + +def _piecewise_collapse_arguments(_args): + newargs = [] # the unevaluated conditions + current_cond = set() # the conditions up to a given e, c pair + for expr, cond in _args: + cond = cond.replace( + lambda _: _.is_Relational, _canonical_coeff) + # Check here if expr is a Piecewise and collapse if one of + # the conds in expr matches cond. This allows the collapsing + # of Piecewise((Piecewise((x,x<0)),x<0)) to Piecewise((x,x<0)). + # This is important when using piecewise_fold to simplify + # multiple Piecewise instances having the same conds. + # Eventually, this code should be able to collapse Piecewise's + # having different intervals, but this will probably require + # using the new assumptions. + if isinstance(expr, Piecewise): + unmatching = [] + for i, (e, c) in enumerate(expr.args): + if c in current_cond: + # this would already have triggered + continue + if c == cond: + if c != True: + # nothing past this condition will ever + # trigger and only those args before this + # that didn't match a previous condition + # could possibly trigger + if unmatching: + expr = Piecewise(*( + unmatching + [(e, c)])) + else: + expr = e + break + else: + unmatching.append((e, c)) + + # check for condition repeats + got = False + # -- if an And contains a condition that was + # already encountered, then the And will be + # False: if the previous condition was False + # then the And will be False and if the previous + # condition is True then then we wouldn't get to + # this point. In either case, we can skip this condition. + for i in ([cond] + + (list(cond.args) if isinstance(cond, And) else + [])): + if i in current_cond: + got = True + break + if got: + continue + + # -- if not(c) is already in current_cond then c is + # a redundant condition in an And. This does not + # apply to Or, however: (e1, c), (e2, Or(~c, d)) + # is not (e1, c), (e2, d) because if c and d are + # both False this would give no results when the + # true answer should be (e2, True) + if isinstance(cond, And): + nonredundant = [] + for c in cond.args: + if isinstance(c, Relational): + if c.negated.canonical in current_cond: + continue + # if a strict inequality appears after + # a non-strict one, then the condition is + # redundant + if isinstance(c, (Lt, Gt)) and ( + c.weak in current_cond): + cond = False + break + nonredundant.append(c) + else: + cond = cond.func(*nonredundant) + elif isinstance(cond, Relational): + if cond.negated.canonical in current_cond: + cond = S.true + + current_cond.add(cond) + + # collect successive e,c pairs when exprs or cond match + if newargs: + if newargs[-1].expr == expr: + orcond = Or(cond, newargs[-1].cond) + if isinstance(orcond, (And, Or)): + orcond = distribute_and_over_or(orcond) + newargs[-1] = ExprCondPair(expr, orcond) + continue + elif newargs[-1].cond == cond: + continue + newargs.append(ExprCondPair(expr, cond)) + return newargs + + +_blessed = lambda e: getattr(e.lhs, '_diff_wrt', False) and ( + getattr(e.rhs, '_diff_wrt', None) or + isinstance(e.rhs, (Rational, NumberSymbol))) + + +def piecewise_simplify(expr, **kwargs): + expr = piecewise_simplify_arguments(expr, **kwargs) + if not isinstance(expr, Piecewise): + return expr + args = list(expr.args) + + args = _piecewise_simplify_eq_and(args) + args = _piecewise_simplify_equal_to_next_segment(args) + return Piecewise(*args) + + +def _piecewise_simplify_equal_to_next_segment(args): + """ + See if expressions valid for an Equal expression happens to evaluate + to the same function as in the next piecewise segment, see: + https://github.com/sympy/sympy/issues/8458 + """ + prevexpr = None + for i, (expr, cond) in reversed(list(enumerate(args))): + if prevexpr is not None: + if isinstance(cond, And): + eqs, other = sift(cond.args, + lambda i: isinstance(i, Eq), binary=True) + elif isinstance(cond, Eq): + eqs, other = [cond], [] + else: + eqs = other = [] + _prevexpr = prevexpr + _expr = expr + if eqs and not other: + eqs = list(ordered(eqs)) + for e in eqs: + # allow 2 args to collapse into 1 for any e + # otherwise limit simplification to only simple-arg + # Eq instances + if len(args) == 2 or _blessed(e): + _prevexpr = _prevexpr.subs(*e.args) + _expr = _expr.subs(*e.args) + # Did it evaluate to the same? + if _prevexpr == _expr: + # Set the expression for the Not equal section to the same + # as the next. These will be merged when creating the new + # Piecewise + args[i] = args[i].func(args[i + 1][0], cond) + else: + # Update the expression that we compare against + prevexpr = expr + else: + prevexpr = expr + return args + + +def _piecewise_simplify_eq_and(args): + """ + Try to simplify conditions and the expression for + equalities that are part of the condition, e.g. + Piecewise((n, And(Eq(n,0), Eq(n + m, 0))), (1, True)) + -> Piecewise((0, And(Eq(n, 0), Eq(m, 0))), (1, True)) + """ + for i, (expr, cond) in enumerate(args): + if isinstance(cond, And): + eqs, other = sift(cond.args, + lambda i: isinstance(i, Eq), binary=True) + elif isinstance(cond, Eq): + eqs, other = [cond], [] + else: + eqs = other = [] + if eqs: + eqs = list(ordered(eqs)) + for j, e in enumerate(eqs): + # these blessed lhs objects behave like Symbols + # and the rhs are simple replacements for the "symbols" + if _blessed(e): + expr = expr.subs(*e.args) + eqs[j + 1:] = [ei.subs(*e.args) for ei in eqs[j + 1:]] + other = [ei.subs(*e.args) for ei in other] + cond = And(*(eqs + other)) + args[i] = args[i].func(expr, cond) + return args + + +def piecewise_exclusive(expr, *, skip_nan=False, deep=True): + """ + Rewrite :class:`Piecewise` with mutually exclusive conditions. + + Explanation + =========== + + SymPy represents the conditions of a :class:`Piecewise` in an + "if-elif"-fashion, allowing more than one condition to be simultaneously + True. The interpretation is that the first condition that is True is the + case that holds. While this is a useful representation computationally it + is not how a piecewise formula is typically shown in a mathematical text. + The :func:`piecewise_exclusive` function can be used to rewrite any + :class:`Piecewise` with more typical mutually exclusive conditions. + + Note that further manipulation of the resulting :class:`Piecewise`, e.g. + simplifying it, will most likely make it non-exclusive. Hence, this is + primarily a function to be used in conjunction with printing the Piecewise + or if one would like to reorder the expression-condition pairs. + + If it is not possible to determine that all possibilities are covered by + the different cases of the :class:`Piecewise` then a final + :class:`~sympy.core.numbers.NaN` case will be included explicitly. This + can be prevented by passing ``skip_nan=True``. + + Examples + ======== + + >>> from sympy import piecewise_exclusive, Symbol, Piecewise, S + >>> x = Symbol('x', real=True) + >>> p = Piecewise((0, x < 0), (S.Half, x <= 0), (1, True)) + >>> piecewise_exclusive(p) + Piecewise((0, x < 0), (1/2, Eq(x, 0)), (1, x > 0)) + >>> piecewise_exclusive(Piecewise((2, x > 1))) + Piecewise((2, x > 1), (nan, x <= 1)) + >>> piecewise_exclusive(Piecewise((2, x > 1)), skip_nan=True) + Piecewise((2, x > 1)) + + Parameters + ========== + + expr: a SymPy expression. + Any :class:`Piecewise` in the expression will be rewritten. + skip_nan: ``bool`` (default ``False``) + If ``skip_nan`` is set to ``True`` then a final + :class:`~sympy.core.numbers.NaN` case will not be included. + deep: ``bool`` (default ``True``) + If ``deep`` is ``True`` then :func:`piecewise_exclusive` will rewrite + any :class:`Piecewise` subexpressions in ``expr`` rather than just + rewriting ``expr`` itself. + + Returns + ======= + + An expression equivalent to ``expr`` but where all :class:`Piecewise` have + been rewritten with mutually exclusive conditions. + + See Also + ======== + + Piecewise + piecewise_fold + """ + + def make_exclusive(*pwargs): + + cumcond = false + newargs = [] + + # Handle the first n-1 cases + for expr_i, cond_i in pwargs[:-1]: + cancond = And(cond_i, Not(cumcond)).simplify() + cumcond = Or(cond_i, cumcond).simplify() + newargs.append((expr_i, cancond)) + + # For the nth case defer simplification of cumcond + expr_n, cond_n = pwargs[-1] + cancond_n = And(cond_n, Not(cumcond)).simplify() + newargs.append((expr_n, cancond_n)) + + if not skip_nan: + cumcond = Or(cond_n, cumcond).simplify() + if cumcond is not true: + newargs.append((Undefined, Not(cumcond).simplify())) + + return Piecewise(*newargs, evaluate=False) + + if deep: + return expr.replace(Piecewise, make_exclusive) + elif isinstance(expr, Piecewise): + return make_exclusive(*expr.args) + else: + return expr diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_complexes.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_complexes.py new file mode 100644 index 0000000000000000000000000000000000000000..699c0fef966c99147b713aaa80710b7b8cf21c73 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_complexes.py @@ -0,0 +1,1030 @@ +from sympy.core.function import (Derivative, Function, Lambda, expand, PoleError) +from sympy.core.numbers import (E, I, Rational, comp, nan, oo, pi, zoo) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import (Abs, adjoint, arg, conjugate, im, re, sign, transpose) +from sympy.functions.elementary.exponential import (exp, exp_polar, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (acos, atan, atan2, cos, sin) +from sympy.functions.elementary.hyperbolic import sinh +from sympy.functions.special.delta_functions import (DiracDelta, Heaviside) +from sympy.integrals.integrals import Integral +from sympy.matrices.dense import Matrix +from sympy.matrices.expressions.funcmatrix import FunctionMatrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.immutable import (ImmutableMatrix, ImmutableSparseMatrix) +from sympy.matrices import SparseMatrix +from sympy.sets.sets import Interval +from sympy.core.expr import unchanged +from sympy.core.function import ArgumentIndexError +from sympy.series.order import Order +from sympy.testing.pytest import XFAIL, raises, _both_exp_pow + + +def N_equals(a, b): + """Check whether two complex numbers are numerically close""" + return comp(a.n(), b.n(), 1.e-6) + + +def test_re(): + x, y = symbols('x,y') + a, b = symbols('a,b', real=True) + + r = Symbol('r', real=True) + i = Symbol('i', imaginary=True) + + assert re(nan) is nan + + assert re(oo) is oo + assert re(-oo) is -oo + + assert re(0) == 0 + + assert re(1) == 1 + assert re(-1) == -1 + + assert re(E) == E + assert re(-E) == -E + + assert unchanged(re, x) + assert re(x*I) == -im(x) + assert re(r*I) == 0 + assert re(r) == r + assert re(i*I) == I * i + assert re(i) == 0 + + assert re(x + y) == re(x) + re(y) + assert re(x + r) == re(x) + r + + assert re(re(x)) == re(x) + + assert re(2 + I) == 2 + assert re(x + I) == re(x) + + assert re(x + y*I) == re(x) - im(y) + assert re(x + r*I) == re(x) + + assert re(log(2*I)) == log(2) + + assert re((2 + I)**2).expand(complex=True) == 3 + + assert re(conjugate(x)) == re(x) + assert conjugate(re(x)) == re(x) + + assert re(x).as_real_imag() == (re(x), 0) + + assert re(i*r*x).diff(r) == re(i*x) + assert re(i*r*x).diff(i) == I*r*im(x) + + assert re( + sqrt(a + b*I)) == (a**2 + b**2)**Rational(1, 4)*cos(atan2(b, a)/2) + assert re(a * (2 + b*I)) == 2*a + + assert re((1 + sqrt(a + b*I))/2) == \ + (a**2 + b**2)**Rational(1, 4)*cos(atan2(b, a)/2)/2 + S.Half + + assert re(x).rewrite(im) == x - S.ImaginaryUnit*im(x) + assert (x + re(y)).rewrite(re, im) == x + y - S.ImaginaryUnit*im(y) + + a = Symbol('a', algebraic=True) + t = Symbol('t', transcendental=True) + x = Symbol('x') + assert re(a).is_algebraic + assert re(x).is_algebraic is None + assert re(t).is_algebraic is False + + assert re(S.ComplexInfinity) is S.NaN + + n, m, l = symbols('n m l') + A = MatrixSymbol('A',n,m) + assert re(A) == (S.Half) * (A + conjugate(A)) + + A = Matrix([[1 + 4*I,2],[0, -3*I]]) + assert re(A) == Matrix([[1, 2],[0, 0]]) + + A = ImmutableMatrix([[1 + 3*I, 3-2*I],[0, 2*I]]) + assert re(A) == ImmutableMatrix([[1, 3],[0, 0]]) + + X = SparseMatrix([[2*j + i*I for i in range(5)] for j in range(5)]) + assert re(X) - Matrix([[0, 0, 0, 0, 0], + [2, 2, 2, 2, 2], + [4, 4, 4, 4, 4], + [6, 6, 6, 6, 6], + [8, 8, 8, 8, 8]]) == Matrix.zeros(5) + + assert im(X) - Matrix([[0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4]]) == Matrix.zeros(5) + + X = FunctionMatrix(3, 3, Lambda((n, m), n + m*I)) + assert re(X) == Matrix([[0, 0, 0], [1, 1, 1], [2, 2, 2]]) + + +def test_im(): + x, y = symbols('x,y') + a, b = symbols('a,b', real=True) + + r = Symbol('r', real=True) + i = Symbol('i', imaginary=True) + + assert im(nan) is nan + + assert im(oo*I) is oo + assert im(-oo*I) is -oo + + assert im(0) == 0 + + assert im(1) == 0 + assert im(-1) == 0 + + assert im(E*I) == E + assert im(-E*I) == -E + + assert unchanged(im, x) + assert im(x*I) == re(x) + assert im(r*I) == r + assert im(r) == 0 + assert im(i*I) == 0 + assert im(i) == -I * i + + assert im(x + y) == im(x) + im(y) + assert im(x + r) == im(x) + assert im(x + r*I) == im(x) + r + + assert im(im(x)*I) == im(x) + + assert im(2 + I) == 1 + assert im(x + I) == im(x) + 1 + + assert im(x + y*I) == im(x) + re(y) + assert im(x + r*I) == im(x) + r + + assert im(log(2*I)) == pi/2 + + assert im((2 + I)**2).expand(complex=True) == 4 + + assert im(conjugate(x)) == -im(x) + assert conjugate(im(x)) == im(x) + + assert im(x).as_real_imag() == (im(x), 0) + + assert im(i*r*x).diff(r) == im(i*x) + assert im(i*r*x).diff(i) == -I * re(r*x) + + assert im( + sqrt(a + b*I)) == (a**2 + b**2)**Rational(1, 4)*sin(atan2(b, a)/2) + assert im(a * (2 + b*I)) == a*b + + assert im((1 + sqrt(a + b*I))/2) == \ + (a**2 + b**2)**Rational(1, 4)*sin(atan2(b, a)/2)/2 + + assert im(x).rewrite(re) == -S.ImaginaryUnit * (x - re(x)) + assert (x + im(y)).rewrite(im, re) == x - S.ImaginaryUnit * (y - re(y)) + + a = Symbol('a', algebraic=True) + t = Symbol('t', transcendental=True) + x = Symbol('x') + assert re(a).is_algebraic + assert re(x).is_algebraic is None + assert re(t).is_algebraic is False + + assert im(S.ComplexInfinity) is S.NaN + + n, m, l = symbols('n m l') + A = MatrixSymbol('A',n,m) + + assert im(A) == (S.One/(2*I)) * (A - conjugate(A)) + + A = Matrix([[1 + 4*I, 2],[0, -3*I]]) + assert im(A) == Matrix([[4, 0],[0, -3]]) + + A = ImmutableMatrix([[1 + 3*I, 3-2*I],[0, 2*I]]) + assert im(A) == ImmutableMatrix([[3, -2],[0, 2]]) + + X = ImmutableSparseMatrix( + [[i*I + i for i in range(5)] for i in range(5)]) + Y = SparseMatrix([list(range(5)) for i in range(5)]) + assert im(X).as_immutable() == Y + + X = FunctionMatrix(3, 3, Lambda((n, m), n + m*I)) + assert im(X) == Matrix([[0, 1, 2], [0, 1, 2], [0, 1, 2]]) + +def test_sign(): + assert sign(1.2) == 1 + assert sign(-1.2) == -1 + assert sign(3*I) == I + assert sign(-3*I) == -I + assert sign(0) == 0 + assert sign(0, evaluate=False).doit() == 0 + assert sign(oo, evaluate=False).doit() == 1 + assert sign(nan) is nan + assert sign(2 + 2*I).doit() == sqrt(2)*(2 + 2*I)/4 + assert sign(2 + 3*I).simplify() == sign(2 + 3*I) + assert sign(2 + 2*I).simplify() == sign(1 + I) + assert sign(im(sqrt(1 - sqrt(3)))) == 1 + assert sign(sqrt(1 - sqrt(3))) == I + + x = Symbol('x') + assert sign(x).is_finite is True + assert sign(x).is_complex is True + assert sign(x).is_imaginary is None + assert sign(x).is_integer is None + assert sign(x).is_real is None + assert sign(x).is_zero is None + assert sign(x).doit() == sign(x) + assert sign(1.2*x) == sign(x) + assert sign(2*x) == sign(x) + assert sign(I*x) == I*sign(x) + assert sign(-2*I*x) == -I*sign(x) + assert sign(conjugate(x)) == conjugate(sign(x)) + + p = Symbol('p', positive=True) + n = Symbol('n', negative=True) + m = Symbol('m', negative=True) + assert sign(2*p*x) == sign(x) + assert sign(n*x) == -sign(x) + assert sign(n*m*x) == sign(x) + + x = Symbol('x', imaginary=True) + assert sign(x).is_imaginary is True + assert sign(x).is_integer is False + assert sign(x).is_real is False + assert sign(x).is_zero is False + assert sign(x).diff(x) == 2*DiracDelta(-I*x) + assert sign(x).doit() == x / Abs(x) + assert conjugate(sign(x)) == -sign(x) + + x = Symbol('x', real=True) + assert sign(x).is_imaginary is False + assert sign(x).is_integer is True + assert sign(x).is_real is True + assert sign(x).is_zero is None + assert sign(x).diff(x) == 2*DiracDelta(x) + assert sign(x).doit() == sign(x) + assert conjugate(sign(x)) == sign(x) + + x = Symbol('x', nonzero=True) + assert sign(x).is_imaginary is False + assert sign(x).is_integer is True + assert sign(x).is_real is True + assert sign(x).is_zero is False + assert sign(x).doit() == x / Abs(x) + assert sign(Abs(x)) == 1 + assert Abs(sign(x)) == 1 + + x = Symbol('x', positive=True) + assert sign(x).is_imaginary is False + assert sign(x).is_integer is True + assert sign(x).is_real is True + assert sign(x).is_zero is False + assert sign(x).doit() == x / Abs(x) + assert sign(Abs(x)) == 1 + assert Abs(sign(x)) == 1 + + x = 0 + assert sign(x).is_imaginary is False + assert sign(x).is_integer is True + assert sign(x).is_real is True + assert sign(x).is_zero is True + assert sign(x).doit() == 0 + assert sign(Abs(x)) == 0 + assert Abs(sign(x)) == 0 + + nz = Symbol('nz', nonzero=True, integer=True) + assert sign(nz).is_imaginary is False + assert sign(nz).is_integer is True + assert sign(nz).is_real is True + assert sign(nz).is_zero is False + assert sign(nz)**2 == 1 + assert (sign(nz)**3).args == (sign(nz), 3) + + assert sign(Symbol('x', nonnegative=True)).is_nonnegative + assert sign(Symbol('x', nonnegative=True)).is_nonpositive is None + assert sign(Symbol('x', nonpositive=True)).is_nonnegative is None + assert sign(Symbol('x', nonpositive=True)).is_nonpositive + assert sign(Symbol('x', real=True)).is_nonnegative is None + assert sign(Symbol('x', real=True)).is_nonpositive is None + assert sign(Symbol('x', real=True, zero=False)).is_nonpositive is None + + x, y = Symbol('x', real=True), Symbol('y') + f = Function('f') + assert sign(x).rewrite(Piecewise) == \ + Piecewise((1, x > 0), (-1, x < 0), (0, True)) + assert sign(y).rewrite(Piecewise) == sign(y) + assert sign(x).rewrite(Heaviside) == 2*Heaviside(x, H0=S(1)/2) - 1 + assert sign(y).rewrite(Heaviside) == sign(y) + assert sign(y).rewrite(Abs) == Piecewise((0, Eq(y, 0)), (y/Abs(y), True)) + assert sign(f(y)).rewrite(Abs) == Piecewise((0, Eq(f(y), 0)), (f(y)/Abs(f(y)), True)) + + # evaluate what can be evaluated + assert sign(exp_polar(I*pi)*pi) is S.NegativeOne + + eq = -sqrt(10 + 6*sqrt(3)) + sqrt(1 + sqrt(3)) + sqrt(3 + 3*sqrt(3)) + # if there is a fast way to know when and when you cannot prove an + # expression like this is zero then the equality to zero is ok + assert sign(eq).func is sign or sign(eq) == 0 + # but sometimes it's hard to do this so it's better not to load + # abs down with tests that will be very slow + q = 1 + sqrt(2) - 2*sqrt(3) + 1331*sqrt(6) + p = expand(q**3)**Rational(1, 3) + d = p - q + assert sign(d).func is sign or sign(d) == 0 + + +def test_as_real_imag(): + n = pi**1000 + # the special code for working out the real + # and complex parts of a power with Integer exponent + # should not run if there is no imaginary part, hence + # this should not hang + assert n.as_real_imag() == (n, 0) + + # issue 6261 + x = Symbol('x') + assert sqrt(x).as_real_imag() == \ + ((re(x)**2 + im(x)**2)**Rational(1, 4)*cos(atan2(im(x), re(x))/2), + (re(x)**2 + im(x)**2)**Rational(1, 4)*sin(atan2(im(x), re(x))/2)) + + # issue 3853 + a, b = symbols('a,b', real=True) + assert ((1 + sqrt(a + b*I))/2).as_real_imag() == \ + ( + (a**2 + b**2)**Rational( + 1, 4)*cos(atan2(b, a)/2)/2 + S.Half, + (a**2 + b**2)**Rational(1, 4)*sin(atan2(b, a)/2)/2) + + assert sqrt(a**2).as_real_imag() == (sqrt(a**2), 0) + i = symbols('i', imaginary=True) + assert sqrt(i**2).as_real_imag() == (0, abs(i)) + + assert ((1 + I)/(1 - I)).as_real_imag() == (0, 1) + assert ((1 + I)**3/(1 - I)).as_real_imag() == (-2, 0) + + +@XFAIL +def test_sign_issue_3068(): + n = pi**1000 + i = int(n) + x = Symbol('x') + assert (n - i).round() == 1 # doesn't hang + assert sign(n - i) == 1 + # perhaps it's not possible to get the sign right when + # only 1 digit is being requested for this situation; + # 2 digits works + assert (n - x).n(1, subs={x: i}) > 0 + assert (n - x).n(2, subs={x: i}) > 0 + + +def test_Abs(): + raises(TypeError, lambda: Abs(Interval(2, 3))) # issue 8717 + + x, y = symbols('x,y') + assert sign(sign(x)) == sign(x) + assert sign(x*y).func is sign + assert Abs(0) == 0 + assert Abs(1) == 1 + assert Abs(-1) == 1 + assert Abs(I) == 1 + assert Abs(-I) == 1 + assert Abs(nan) is nan + assert Abs(zoo) is oo + assert Abs(I * pi) == pi + assert Abs(-I * pi) == pi + assert Abs(I * x) == Abs(x) + assert Abs(-I * x) == Abs(x) + assert Abs(-2*x) == 2*Abs(x) + assert Abs(-2.0*x) == 2.0*Abs(x) + assert Abs(2*pi*x*y) == 2*pi*Abs(x*y) + assert Abs(conjugate(x)) == Abs(x) + assert conjugate(Abs(x)) == Abs(x) + assert Abs(x).expand(complex=True) == sqrt(re(x)**2 + im(x)**2) + + a = Symbol('a', positive=True) + assert Abs(2*pi*x*a) == 2*pi*a*Abs(x) + assert Abs(2*pi*I*x*a) == 2*pi*a*Abs(x) + + x = Symbol('x', real=True) + n = Symbol('n', integer=True) + assert Abs((-1)**n) == 1 + assert x**(2*n) == Abs(x)**(2*n) + assert Abs(x).diff(x) == sign(x) + assert abs(x) == Abs(x) # Python built-in + assert Abs(x)**3 == x**2*Abs(x) + assert Abs(x)**4 == x**4 + assert ( + Abs(x)**(3*n)).args == (Abs(x), 3*n) # leave symbolic odd unchanged + assert (1/Abs(x)).args == (Abs(x), -1) + assert 1/Abs(x)**3 == 1/(x**2*Abs(x)) + assert Abs(x)**-3 == Abs(x)/(x**4) + assert Abs(x**3) == x**2*Abs(x) + assert Abs(I**I) == exp(-pi/2) + assert Abs((4 + 5*I)**(6 + 7*I)) == 68921*exp(-7*atan(Rational(5, 4))) + y = Symbol('y', real=True) + assert Abs(I**y) == 1 + y = Symbol('y') + assert Abs(I**y) == exp(-pi*im(y)/2) + + x = Symbol('x', imaginary=True) + assert Abs(x).diff(x) == -sign(x) + + eq = -sqrt(10 + 6*sqrt(3)) + sqrt(1 + sqrt(3)) + sqrt(3 + 3*sqrt(3)) + # if there is a fast way to know when you can and when you cannot prove an + # expression like this is zero then the equality to zero is ok + assert abs(eq).func is Abs or abs(eq) == 0 + # but sometimes it's hard to do this so it's better not to load + # abs down with tests that will be very slow + q = 1 + sqrt(2) - 2*sqrt(3) + 1331*sqrt(6) + p = expand(q**3)**Rational(1, 3) + d = p - q + assert abs(d).func is Abs or abs(d) == 0 + + assert Abs(4*exp(pi*I/4)) == 4 + assert Abs(3**(2 + I)) == 9 + assert Abs((-3)**(1 - I)) == 3*exp(pi) + + assert Abs(oo) is oo + assert Abs(-oo) is oo + assert Abs(oo + I) is oo + assert Abs(oo + I*oo) is oo + + a = Symbol('a', algebraic=True) + t = Symbol('t', transcendental=True) + x = Symbol('x') + assert re(a).is_algebraic + assert re(x).is_algebraic is None + assert re(t).is_algebraic is False + assert Abs(x).fdiff() == sign(x) + raises(ArgumentIndexError, lambda: Abs(x).fdiff(2)) + + # doesn't have recursion error + arg = sqrt(acos(1 - I)*acos(1 + I)) + assert abs(arg) == arg + + # special handling to put Abs in denom + assert abs(1/x) == 1/Abs(x) + e = abs(2/x**2) + assert e.is_Mul and e == 2/Abs(x**2) + assert unchanged(Abs, y/x) + assert unchanged(Abs, x/(x + 1)) + assert unchanged(Abs, x*y) + p = Symbol('p', positive=True) + assert abs(x/p) == abs(x)/p + + # coverage + assert unchanged(Abs, Symbol('x', real=True)**y) + # issue 19627 + f = Function('f', positive=True) + assert sqrt(f(x)**2) == f(x) + # issue 21625 + assert unchanged(Abs, S("im(acos(-i + acosh(-g + i)))")) + + +def test_Abs_rewrite(): + x = Symbol('x', real=True) + a = Abs(x).rewrite(Heaviside).expand() + assert a == x*Heaviside(x) - x*Heaviside(-x) + for i in [-2, -1, 0, 1, 2]: + assert a.subs(x, i) == abs(i) + y = Symbol('y') + assert Abs(y).rewrite(Heaviside) == Abs(y) + + x, y = Symbol('x', real=True), Symbol('y') + assert Abs(x).rewrite(Piecewise) == Piecewise((x, x >= 0), (-x, True)) + assert Abs(y).rewrite(Piecewise) == Abs(y) + assert Abs(y).rewrite(sign) == y/sign(y) + + i = Symbol('i', imaginary=True) + assert abs(i).rewrite(Piecewise) == Piecewise((I*i, I*i >= 0), (-I*i, True)) + + + assert Abs(y).rewrite(conjugate) == sqrt(y*conjugate(y)) + assert Abs(i).rewrite(conjugate) == sqrt(-i**2) # == -I*i + + y = Symbol('y', extended_real=True) + assert (Abs(exp(-I*x)-exp(-I*y))**2).rewrite(conjugate) == \ + -exp(I*x)*exp(-I*y) + 2 - exp(-I*x)*exp(I*y) + + +def test_Abs_real(): + # test some properties of abs that only apply + # to real numbers + x = Symbol('x', complex=True) + assert sqrt(x**2) != Abs(x) + assert Abs(x**2) != x**2 + + x = Symbol('x', real=True) + assert sqrt(x**2) == Abs(x) + assert Abs(x**2) == x**2 + + # if the symbol is zero, the following will still apply + nn = Symbol('nn', nonnegative=True, real=True) + np = Symbol('np', nonpositive=True, real=True) + assert Abs(nn) == nn + assert Abs(np) == -np + + +def test_Abs_properties(): + x = Symbol('x') + assert Abs(x).is_real is None + assert Abs(x).is_extended_real is True + assert Abs(x).is_rational is None + assert Abs(x).is_positive is None + assert Abs(x).is_nonnegative is None + assert Abs(x).is_extended_positive is None + assert Abs(x).is_extended_nonnegative is True + + f = Symbol('x', finite=True) + assert Abs(f).is_real is True + assert Abs(f).is_extended_real is True + assert Abs(f).is_rational is None + assert Abs(f).is_positive is None + assert Abs(f).is_nonnegative is True + assert Abs(f).is_extended_positive is None + assert Abs(f).is_extended_nonnegative is True + + z = Symbol('z', complex=True, zero=False) + assert Abs(z).is_real is True # since complex implies finite + assert Abs(z).is_extended_real is True + assert Abs(z).is_rational is None + assert Abs(z).is_positive is True + assert Abs(z).is_extended_positive is True + assert Abs(z).is_zero is False + + p = Symbol('p', positive=True) + assert Abs(p).is_real is True + assert Abs(p).is_extended_real is True + assert Abs(p).is_rational is None + assert Abs(p).is_positive is True + assert Abs(p).is_zero is False + + q = Symbol('q', rational=True) + assert Abs(q).is_real is True + assert Abs(q).is_rational is True + assert Abs(q).is_integer is None + assert Abs(q).is_positive is None + assert Abs(q).is_nonnegative is True + + i = Symbol('i', integer=True) + assert Abs(i).is_real is True + assert Abs(i).is_integer is True + assert Abs(i).is_positive is None + assert Abs(i).is_nonnegative is True + + e = Symbol('n', even=True) + ne = Symbol('ne', real=True, even=False) + assert Abs(e).is_even is True + assert Abs(ne).is_even is False + assert Abs(i).is_even is None + + o = Symbol('n', odd=True) + no = Symbol('no', real=True, odd=False) + assert Abs(o).is_odd is True + assert Abs(no).is_odd is False + assert Abs(i).is_odd is None + + +def test_abs(): + # this tests that abs calls Abs; don't rename to + # test_Abs since that test is already above + a = Symbol('a', positive=True) + assert abs(I*(1 + a)**2) == (1 + a)**2 + + +def test_arg(): + assert arg(0) is nan + assert arg(1) == 0 + assert arg(-1) == pi + assert arg(I) == pi/2 + assert arg(-I) == -pi/2 + assert arg(1 + I) == pi/4 + assert arg(-1 + I) == pi*Rational(3, 4) + assert arg(1 - I) == -pi/4 + assert arg(exp_polar(4*pi*I)) == 4*pi + assert arg(exp_polar(-7*pi*I)) == -7*pi + assert arg(exp_polar(5 - 3*pi*I/4)) == pi*Rational(-3, 4) + + assert arg(exp(I*pi/7)) == pi/7 # issue 17300 + assert arg(exp(16*I)) == 16 - 6*pi + assert arg(exp(13*I*pi/12)) == -11*pi/12 + assert arg(exp(123 - 5*I)) == -5 + 2*pi + assert arg(exp(sin(1 + 3*I))) == -2*pi + cos(1)*sinh(3) + r = Symbol('r', real=True) + assert arg(exp(r - 2*I)) == -2 + + f = Function('f') + assert not arg(f(0) + I*f(1)).atoms(re) + + # check nesting + x = Symbol('x') + assert arg(arg(arg(x))) is not S.NaN + assert arg(arg(arg(arg(x)))) is S.NaN + r = Symbol('r', extended_real=True) + assert arg(arg(r)) is not S.NaN + assert arg(arg(arg(r))) is S.NaN + + p = Function('p', extended_positive=True) + assert arg(p(x)) == 0 + assert arg((3 + I)*p(x)) == arg(3 + I) + + p = Symbol('p', positive=True) + assert arg(p) == 0 + assert arg(p*I) == pi/2 + + n = Symbol('n', negative=True) + assert arg(n) == pi + assert arg(n*I) == -pi/2 + + x = Symbol('x') + assert conjugate(arg(x)) == arg(x) + + e = p + I*p**2 + assert arg(e) == arg(1 + p*I) + # make sure sign doesn't swap + e = -2*p + 4*I*p**2 + assert arg(e) == arg(-1 + 2*p*I) + # make sure sign isn't lost + x = symbols('x', real=True) # could be zero + e = x + I*x + assert arg(e) == arg(x*(1 + I)) + assert arg(e/p) == arg(x*(1 + I)) + e = p*cos(p) + I*log(p)*exp(p) + assert arg(e).args[0] == e + # keep it simple -- let the user do more advanced cancellation + e = (p + 1) + I*(p**2 - 1) + assert arg(e).args[0] == e + + f = Function('f') + e = 2*x*(f(0) - 1) - 2*x*f(0) + assert arg(e) == arg(-2*x) + assert arg(f(0)).func == arg and arg(f(0)).args == (f(0),) + + +def test_arg_rewrite(): + assert arg(1 + I) == atan2(1, 1) + + x = Symbol('x', real=True) + y = Symbol('y', real=True) + assert arg(x + I*y).rewrite(atan2) == atan2(y, x) + + +def test_arg_leading_term_and_series(): + x = Symbol('x') + assert arg(x).as_leading_term(x, cdir = 1) == 0 + assert arg(x).as_leading_term(x, cdir = -1) == pi + raises(PoleError, lambda: arg(x + I).as_leading_term(x, cdir = 1)) + raises(PoleError, lambda: arg(2*x).as_leading_term(x, cdir = I)) + + assert arg(x).nseries(x) == 0 + assert arg(x).nseries(x, n=0) == Order(1) + + +def test_adjoint(): + a = Symbol('a', antihermitian=True) + b = Symbol('b', hermitian=True) + assert adjoint(a) == -a + assert adjoint(I*a) == I*a + assert adjoint(b) == b + assert adjoint(I*b) == -I*b + assert adjoint(a*b) == -b*a + assert adjoint(I*a*b) == I*b*a + + x, y = symbols('x y') + assert adjoint(adjoint(x)) == x + assert adjoint(x + y) == conjugate(x) + conjugate(y) + assert adjoint(x - y) == conjugate(x) - conjugate(y) + assert adjoint(x * y) == conjugate(x) * conjugate(y) + assert adjoint(x / y) == conjugate(x) / conjugate(y) + assert adjoint(-x) == -conjugate(x) + + x, y = symbols('x y', commutative=False) + assert adjoint(adjoint(x)) == x + assert adjoint(x + y) == adjoint(x) + adjoint(y) + assert adjoint(x - y) == adjoint(x) - adjoint(y) + assert adjoint(x * y) == adjoint(y) * adjoint(x) + assert adjoint(x / y) == 1 / adjoint(y) * adjoint(x) + assert adjoint(-x) == -adjoint(x) + + +def test_conjugate(): + a = Symbol('a', real=True) + b = Symbol('b', imaginary=True) + assert conjugate(a) == a + assert conjugate(I*a) == -I*a + assert conjugate(b) == -b + assert conjugate(I*b) == I*b + assert conjugate(a*b) == -a*b + assert conjugate(I*a*b) == I*a*b + + x, y = symbols('x y') + assert conjugate(conjugate(x)) == x + assert conjugate(x).inverse() == conjugate + assert conjugate(x + y) == conjugate(x) + conjugate(y) + assert conjugate(x - y) == conjugate(x) - conjugate(y) + assert conjugate(x * y) == conjugate(x) * conjugate(y) + assert conjugate(x / y) == conjugate(x) / conjugate(y) + assert conjugate(-x) == -conjugate(x) + + a = Symbol('a', algebraic=True) + t = Symbol('t', transcendental=True) + assert re(a).is_algebraic + assert re(x).is_algebraic is None + assert re(t).is_algebraic is False + + +def test_conjugate_transpose(): + x = Symbol('x', commutative=False) + assert conjugate(transpose(x)) == adjoint(x) + assert transpose(conjugate(x)) == adjoint(x) + assert adjoint(transpose(x)) == conjugate(x) + assert transpose(adjoint(x)) == conjugate(x) + assert adjoint(conjugate(x)) == transpose(x) + assert conjugate(adjoint(x)) == transpose(x) + + x = Symbol('x') + assert conjugate(x) == adjoint(x) + assert transpose(x) == x + + +def test_transpose(): + a = Symbol('a', complex=True) + assert transpose(a) == a + assert transpose(I*a) == I*a + + x, y = symbols('x y') + assert transpose(transpose(x)) == x + assert transpose(x + y) == x + y + assert transpose(x - y) == x - y + assert transpose(x * y) == x * y + assert transpose(x / y) == x / y + assert transpose(-x) == -x + + x, y = symbols('x y', commutative=False) + assert transpose(transpose(x)) == x + assert transpose(x + y) == transpose(x) + transpose(y) + assert transpose(x - y) == transpose(x) - transpose(y) + assert transpose(x * y) == transpose(y) * transpose(x) + assert transpose(x / y) == 1 / transpose(y) * transpose(x) + assert transpose(-x) == -transpose(x) + + +@_both_exp_pow +def test_polarify(): + from sympy.functions.elementary.complexes import (polar_lift, polarify) + x = Symbol('x') + z = Symbol('z', polar=True) + f = Function('f') + ES = {} + + assert polarify(-1) == (polar_lift(-1), ES) + assert polarify(1 + I) == (polar_lift(1 + I), ES) + + assert polarify(exp(x), subs=False) == exp(x) + assert polarify(1 + x, subs=False) == 1 + x + assert polarify(f(I) + x, subs=False) == f(polar_lift(I)) + x + + assert polarify(x, lift=True) == polar_lift(x) + assert polarify(z, lift=True) == z + assert polarify(f(x), lift=True) == f(polar_lift(x)) + assert polarify(1 + x, lift=True) == polar_lift(1 + x) + assert polarify(1 + f(x), lift=True) == polar_lift(1 + f(polar_lift(x))) + + newex, subs = polarify(f(x) + z) + assert newex.subs(subs) == f(x) + z + + mu = Symbol("mu") + sigma = Symbol("sigma", positive=True) + + # Make sure polarify(lift=True) doesn't try to lift the integration + # variable + assert polarify( + Integral(sqrt(2)*x*exp(-(-mu + x)**2/(2*sigma**2))/(2*sqrt(pi)*sigma), + (x, -oo, oo)), lift=True) == Integral(sqrt(2)*(sigma*exp_polar(0))**exp_polar(I*pi)* + exp((sigma*exp_polar(0))**(2*exp_polar(I*pi))*exp_polar(I*pi)*polar_lift(-mu + x)** + (2*exp_polar(0))/2)*exp_polar(0)*polar_lift(x)/(2*sqrt(pi)), (x, -oo, oo)) + + +def test_unpolarify(): + from sympy.functions.elementary.complexes import (polar_lift, principal_branch, unpolarify) + from sympy.core.relational import Ne + from sympy.functions.elementary.hyperbolic import tanh + from sympy.functions.special.error_functions import erf + from sympy.functions.special.gamma_functions import (gamma, uppergamma) + from sympy.abc import x + p = exp_polar(7*I) + 1 + u = exp(7*I) + 1 + + assert unpolarify(1) == 1 + assert unpolarify(p) == u + assert unpolarify(p**2) == u**2 + assert unpolarify(p**x) == p**x + assert unpolarify(p*x) == u*x + assert unpolarify(p + x) == u + x + assert unpolarify(sqrt(sin(p))) == sqrt(sin(u)) + + # Test reduction to principal branch 2*pi. + t = principal_branch(x, 2*pi) + assert unpolarify(t) == x + assert unpolarify(sqrt(t)) == sqrt(t) + + # Test exponents_only. + assert unpolarify(p**p, exponents_only=True) == p**u + assert unpolarify(uppergamma(x, p**p)) == uppergamma(x, p**u) + + # Test functions. + assert unpolarify(sin(p)) == sin(u) + assert unpolarify(tanh(p)) == tanh(u) + assert unpolarify(gamma(p)) == gamma(u) + assert unpolarify(erf(p)) == erf(u) + assert unpolarify(uppergamma(x, p)) == uppergamma(x, p) + + assert unpolarify(uppergamma(sin(p), sin(p + exp_polar(0)))) == \ + uppergamma(sin(u), sin(u + 1)) + assert unpolarify(uppergamma(polar_lift(0), 2*exp_polar(0))) == \ + uppergamma(0, 2) + + assert unpolarify(Eq(p, 0)) == Eq(u, 0) + assert unpolarify(Ne(p, 0)) == Ne(u, 0) + assert unpolarify(polar_lift(x) > 0) == (x > 0) + + # Test bools + assert unpolarify(True) is True + + +def test_issue_4035(): + x = Symbol('x') + assert Abs(x).expand(trig=True) == Abs(x) + assert sign(x).expand(trig=True) == sign(x) + assert arg(x).expand(trig=True) == arg(x) + + +def test_issue_3206(): + x = Symbol('x') + assert Abs(Abs(x)) == Abs(x) + + +def test_issue_4754_derivative_conjugate(): + x = Symbol('x', real=True) + y = Symbol('y', imaginary=True) + f = Function('f') + assert (f(x).conjugate()).diff(x) == (f(x).diff(x)).conjugate() + assert (f(y).conjugate()).diff(y) == -(f(y).diff(y)).conjugate() + + +def test_derivatives_issue_4757(): + x = Symbol('x', real=True) + y = Symbol('y', imaginary=True) + f = Function('f') + assert re(f(x)).diff(x) == re(f(x).diff(x)) + assert im(f(x)).diff(x) == im(f(x).diff(x)) + assert re(f(y)).diff(y) == -I*im(f(y).diff(y)) + assert im(f(y)).diff(y) == -I*re(f(y).diff(y)) + assert Abs(f(x)).diff(x).subs(f(x), 1 + I*x).doit() == x/sqrt(1 + x**2) + assert arg(f(x)).diff(x).subs(f(x), 1 + I*x**2).doit() == 2*x/(1 + x**4) + assert Abs(f(y)).diff(y).subs(f(y), 1 + y).doit() == -y/sqrt(1 - y**2) + assert arg(f(y)).diff(y).subs(f(y), I + y**2).doit() == 2*y/(1 + y**4) + + +def test_issue_11413(): + from sympy.simplify.simplify import simplify + v0 = Symbol('v0') + v1 = Symbol('v1') + v2 = Symbol('v2') + V = Matrix([[v0],[v1],[v2]]) + U = V.normalized() + assert U == Matrix([ + [v0/sqrt(Abs(v0)**2 + Abs(v1)**2 + Abs(v2)**2)], + [v1/sqrt(Abs(v0)**2 + Abs(v1)**2 + Abs(v2)**2)], + [v2/sqrt(Abs(v0)**2 + Abs(v1)**2 + Abs(v2)**2)]]) + U.norm = sqrt(v0**2/(v0**2 + v1**2 + v2**2) + v1**2/(v0**2 + v1**2 + v2**2) + v2**2/(v0**2 + v1**2 + v2**2)) + assert simplify(U.norm) == 1 + + +def test_periodic_argument(): + from sympy.functions.elementary.complexes import (periodic_argument, polar_lift, principal_branch, unbranched_argument) + x = Symbol('x') + p = Symbol('p', positive=True) + + assert unbranched_argument(2 + I) == periodic_argument(2 + I, oo) + assert unbranched_argument(1 + x) == periodic_argument(1 + x, oo) + assert N_equals(unbranched_argument((1 + I)**2), pi/2) + assert N_equals(unbranched_argument((1 - I)**2), -pi/2) + assert N_equals(periodic_argument((1 + I)**2, 3*pi), pi/2) + assert N_equals(periodic_argument((1 - I)**2, 3*pi), -pi/2) + + assert unbranched_argument(principal_branch(x, pi)) == \ + periodic_argument(x, pi) + + assert unbranched_argument(polar_lift(2 + I)) == unbranched_argument(2 + I) + assert periodic_argument(polar_lift(2 + I), 2*pi) == \ + periodic_argument(2 + I, 2*pi) + assert periodic_argument(polar_lift(2 + I), 3*pi) == \ + periodic_argument(2 + I, 3*pi) + assert periodic_argument(polar_lift(2 + I), pi) == \ + periodic_argument(polar_lift(2 + I), pi) + + assert unbranched_argument(polar_lift(1 + I)) == pi/4 + assert periodic_argument(2*p, p) == periodic_argument(p, p) + assert periodic_argument(pi*p, p) == periodic_argument(p, p) + + assert Abs(polar_lift(1 + I)) == Abs(1 + I) + + +@XFAIL +def test_principal_branch_fail(): + # TODO XXX why does abs(x)._eval_evalf() not fall back to global evalf? + from sympy.functions.elementary.complexes import principal_branch + assert N_equals(principal_branch((1 + I)**2, pi/2), 0) + + +def test_principal_branch(): + from sympy.functions.elementary.complexes import (polar_lift, principal_branch) + p = Symbol('p', positive=True) + x = Symbol('x') + neg = Symbol('x', negative=True) + + assert principal_branch(polar_lift(x), p) == principal_branch(x, p) + assert principal_branch(polar_lift(2 + I), p) == principal_branch(2 + I, p) + assert principal_branch(2*x, p) == 2*principal_branch(x, p) + assert principal_branch(1, pi) == exp_polar(0) + assert principal_branch(-1, 2*pi) == exp_polar(I*pi) + assert principal_branch(-1, pi) == exp_polar(0) + assert principal_branch(exp_polar(3*pi*I)*x, 2*pi) == \ + principal_branch(exp_polar(I*pi)*x, 2*pi) + assert principal_branch(neg*exp_polar(pi*I), 2*pi) == neg*exp_polar(-I*pi) + # related to issue #14692 + assert principal_branch(exp_polar(-I*pi/2)/polar_lift(neg), 2*pi) == \ + exp_polar(-I*pi/2)/neg + + assert N_equals(principal_branch((1 + I)**2, 2*pi), 2*I) + assert N_equals(principal_branch((1 + I)**2, 3*pi), 2*I) + assert N_equals(principal_branch((1 + I)**2, 1*pi), 2*I) + + # test argument sanitization + assert principal_branch(x, I).func is principal_branch + assert principal_branch(x, -4).func is principal_branch + assert principal_branch(x, -oo).func is principal_branch + assert principal_branch(x, zoo).func is principal_branch + + +@XFAIL +def test_issue_6167_6151(): + n = pi**1000 + i = int(n) + assert sign(n - i) == 1 + assert abs(n - i) == n - i + x = Symbol('x') + eps = pi**-1500 + big = pi**1000 + one = cos(x)**2 + sin(x)**2 + e = big*one - big + eps + from sympy.simplify.simplify import simplify + assert sign(simplify(e)) == 1 + for xi in (111, 11, 1, Rational(1, 10)): + assert sign(e.subs(x, xi)) == 1 + + +def test_issue_14216(): + from sympy.functions.elementary.complexes import unpolarify + A = MatrixSymbol("A", 2, 2) + assert unpolarify(A[0, 0]) == A[0, 0] + assert unpolarify(A[0, 0]*A[1, 0]) == A[0, 0]*A[1, 0] + + +def test_issue_14238(): + # doesn't cause recursion error + r = Symbol('r', real=True) + assert Abs(r + Piecewise((0, r > 0), (1 - r, True))) + + +def test_issue_22189(): + x = Symbol('x') + for a in (sqrt(7 - 2*x) - 2, 1 - x): + assert Abs(a) - Abs(-a) == 0, a + + +def test_zero_assumptions(): + nr = Symbol('nonreal', real=False, finite=True) + ni = Symbol('nonimaginary', imaginary=False) + # imaginary implies not zero + nzni = Symbol('nonzerononimaginary', zero=False, imaginary=False) + + assert re(nr).is_zero is None + assert im(nr).is_zero is False + + assert re(ni).is_zero is None + assert im(ni).is_zero is None + + assert re(nzni).is_zero is False + assert im(nzni).is_zero is None + + +@_both_exp_pow +def test_issue_15893(): + f = Function('f', real=True) + x = Symbol('x', real=True) + eq = Derivative(Abs(f(x)), f(x)) + assert eq.doit() == sign(f(x)) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_exponential.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_exponential.py new file mode 100644 index 0000000000000000000000000000000000000000..ee8c311d01e98d7fd6831ad754e854fae409aa0c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_exponential.py @@ -0,0 +1,810 @@ +from sympy.assumptions.refine import refine +from sympy.calculus.accumulationbounds import AccumBounds +from sympy.concrete.products import Product +from sympy.concrete.summations import Sum +from sympy.core.function import expand_log +from sympy.core.numbers import (E, Float, I, Rational, nan, oo, pi, zoo) +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import (adjoint, conjugate, re, sign, transpose) +from sympy.functions.elementary.exponential import (LambertW, exp, exp_polar, log) +from sympy.functions.elementary.hyperbolic import (cosh, sinh, tanh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin, tan) +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.polys.polytools import gcd +from sympy.series.order import O +from sympy.simplify.simplify import simplify +from sympy.core.parameters import global_parameters +from sympy.functions.elementary.exponential import match_real_imag +from sympy.abc import x, y, z +from sympy.core.expr import unchanged +from sympy.core.function import ArgumentIndexError +from sympy.testing.pytest import raises, XFAIL, _both_exp_pow + + +@_both_exp_pow +def test_exp_values(): + if global_parameters.exp_is_pow: + assert type(exp(x)) is Pow + else: + assert type(exp(x)) is exp + + k = Symbol('k', integer=True) + + assert exp(nan) is nan + + assert exp(oo) is oo + assert exp(-oo) == 0 + + assert exp(0) == 1 + assert exp(1) == E + assert exp(-1 + x).as_base_exp() == (S.Exp1, x - 1) + assert exp(1 + x).as_base_exp() == (S.Exp1, x + 1) + + assert exp(pi*I/2) == I + assert exp(pi*I) == -1 + assert exp(pi*I*Rational(3, 2)) == -I + assert exp(2*pi*I) == 1 + + assert refine(exp(pi*I*2*k)) == 1 + assert refine(exp(pi*I*2*(k + S.Half))) == -1 + assert refine(exp(pi*I*2*(k + Rational(1, 4)))) == I + assert refine(exp(pi*I*2*(k + Rational(3, 4)))) == -I + + assert exp(log(x)) == x + assert exp(2*log(x)) == x**2 + assert exp(pi*log(x)) == x**pi + + assert exp(17*log(x) + E*log(y)) == x**17 * y**E + + assert exp(x*log(x)) != x**x + assert exp(sin(x)*log(x)) != x + + assert exp(3*log(x) + oo*x) == exp(oo*x) * x**3 + assert exp(4*log(x)*log(y) + 3*log(x)) == x**3 * exp(4*log(x)*log(y)) + + assert exp(-oo, evaluate=False).is_finite is True + assert exp(oo, evaluate=False).is_finite is False + + +@_both_exp_pow +def test_exp_period(): + assert exp(I*pi*Rational(9, 4)) == exp(I*pi/4) + assert exp(I*pi*Rational(46, 18)) == exp(I*pi*Rational(5, 9)) + assert exp(I*pi*Rational(25, 7)) == exp(I*pi*Rational(-3, 7)) + assert exp(I*pi*Rational(-19, 3)) == exp(-I*pi/3) + assert exp(I*pi*Rational(37, 8)) - exp(I*pi*Rational(-11, 8)) == 0 + assert exp(I*pi*Rational(-5, 3)) / exp(I*pi*Rational(11, 5)) * exp(I*pi*Rational(148, 15)) == 1 + + assert exp(2 - I*pi*Rational(17, 5)) == exp(2 + I*pi*Rational(3, 5)) + assert exp(log(3) + I*pi*Rational(29, 9)) == 3 * exp(I*pi*Rational(-7, 9)) + + n = Symbol('n', integer=True) + e = Symbol('e', even=True) + assert exp(e*I*pi) == 1 + assert exp((e + 1)*I*pi) == -1 + assert exp((1 + 4*n)*I*pi/2) == I + assert exp((-1 + 4*n)*I*pi/2) == -I + + +@_both_exp_pow +def test_exp_log(): + x = Symbol("x", real=True) + assert log(exp(x)) == x + assert exp(log(x)) == x + + if not global_parameters.exp_is_pow: + assert log(x).inverse() == exp + assert exp(x).inverse() == log + + y = Symbol("y", polar=True) + assert log(exp_polar(z)) == z + assert exp(log(y)) == y + + +@_both_exp_pow +def test_exp_expand(): + e = exp(log(Rational(2))*(1 + x) - log(Rational(2))*x) + assert e.expand() == 2 + assert exp(x + y) != exp(x)*exp(y) + assert exp(x + y).expand() == exp(x)*exp(y) + + +@_both_exp_pow +def test_exp__as_base_exp(): + assert exp(x).as_base_exp() == (E, x) + assert exp(2*x).as_base_exp() == (E, 2*x) + assert exp(x*y).as_base_exp() == (E, x*y) + assert exp(-x).as_base_exp() == (E, -x) + + # Pow( *expr.as_base_exp() ) == expr invariant should hold + assert E**x == exp(x) + assert E**(2*x) == exp(2*x) + assert E**(x*y) == exp(x*y) + + assert exp(x).base is S.Exp1 + assert exp(x).exp == x + + +@_both_exp_pow +def test_exp_infinity(): + assert exp(I*y) != nan + assert refine(exp(I*oo)) is nan + assert refine(exp(-I*oo)) is nan + assert exp(y*I*oo) != nan + assert exp(zoo) is nan + x = Symbol('x', extended_real=True, finite=False) + assert exp(x).is_complex is None + + +@_both_exp_pow +def test_exp_subs(): + x = Symbol('x') + e = (exp(3*log(x), evaluate=False)) # evaluates to x**3 + assert e.subs(x**3, y**3) == e + assert e.subs(x**2, 5) == e + assert (x**3).subs(x**2, y) != y**Rational(3, 2) + assert exp(exp(x) + exp(x**2)).subs(exp(exp(x)), y) == y * exp(exp(x**2)) + assert exp(x).subs(E, y) == y**x + x = symbols('x', real=True) + assert exp(5*x).subs(exp(7*x), y) == y**Rational(5, 7) + assert exp(2*x + 7).subs(exp(3*x), y) == y**Rational(2, 3) * exp(7) + x = symbols('x', positive=True) + assert exp(3*log(x)).subs(x**2, y) == y**Rational(3, 2) + # differentiate between E and exp + assert exp(exp(x + E)).subs(exp, 3) == 3**(3**(x + E)) + assert exp(exp(x + E)).subs(exp, sin) == sin(sin(x + E)) + assert exp(exp(x + E)).subs(E, 3) == 3**(3**(x + 3)) + assert exp(3).subs(E, sin) == sin(3) + + +def test_exp_adjoint(): + x = Symbol('x', commutative=False) + assert adjoint(exp(x)) == exp(adjoint(x)) + + +def test_exp_conjugate(): + assert conjugate(exp(x)) == exp(conjugate(x)) + + +@_both_exp_pow +def test_exp_transpose(): + assert transpose(exp(x)) == exp(transpose(x)) + + +@_both_exp_pow +def test_exp_rewrite(): + assert exp(x).rewrite(sin) == sinh(x) + cosh(x) + assert exp(x*I).rewrite(cos) == cos(x) + I*sin(x) + assert exp(1).rewrite(cos) == sinh(1) + cosh(1) + assert exp(1).rewrite(sin) == sinh(1) + cosh(1) + assert exp(1).rewrite(sin) == sinh(1) + cosh(1) + assert exp(x).rewrite(tanh) == (1 + tanh(x/2))/(1 - tanh(x/2)) + assert exp(pi*I/4).rewrite(sqrt) == sqrt(2)/2 + sqrt(2)*I/2 + assert exp(pi*I/3).rewrite(sqrt) == S.Half + sqrt(3)*I/2 + if not global_parameters.exp_is_pow: + assert exp(x*log(y)).rewrite(Pow) == y**x + assert exp(log(x)*log(y)).rewrite(Pow) in [x**log(y), y**log(x)] + assert exp(log(log(x))*y).rewrite(Pow) == log(x)**y + + n = Symbol('n', integer=True) + + assert Sum((exp(pi*I/2)/2)**n, (n, 0, oo)).rewrite(sqrt).doit() == Rational(4, 5) + I*2/5 + assert Sum((exp(pi*I/4)/2)**n, (n, 0, oo)).rewrite(sqrt).doit() == 1/(1 - sqrt(2)*(1 + I)/4) + assert (Sum((exp(pi*I/3)/2)**n, (n, 0, oo)).rewrite(sqrt).doit().cancel() + == 4*I/(sqrt(3) + 3*I)) + + +@_both_exp_pow +def test_exp_leading_term(): + assert exp(x).as_leading_term(x) == 1 + assert exp(2 + x).as_leading_term(x) == exp(2) + assert exp((2*x + 3) / (x+1)).as_leading_term(x) == exp(3) + + # The following tests are commented, since now SymPy returns the + # original function when the leading term in the series expansion does + # not exist. + # raises(NotImplementedError, lambda: exp(1/x).as_leading_term(x)) + # raises(NotImplementedError, lambda: exp((x + 1) / x**2).as_leading_term(x)) + # raises(NotImplementedError, lambda: exp(x + 1/x).as_leading_term(x)) + + +@_both_exp_pow +def test_exp_taylor_term(): + x = symbols('x') + assert exp(x).taylor_term(1, x) == x + assert exp(x).taylor_term(3, x) == x**3/6 + assert exp(x).taylor_term(4, x) == x**4/24 + assert exp(x).taylor_term(-1, x) is S.Zero + + +def test_exp_MatrixSymbol(): + A = MatrixSymbol("A", 2, 2) + assert exp(A).has(exp) + + +def test_exp_fdiff(): + x = Symbol('x') + raises(ArgumentIndexError, lambda: exp(x).fdiff(2)) + + +def test_log_values(): + assert log(nan) is nan + + assert log(oo) is oo + assert log(-oo) is oo + + assert log(zoo) is zoo + assert log(-zoo) is zoo + + assert log(0) is zoo + + assert log(1) == 0 + assert log(-1) == I*pi + + assert log(E) == 1 + assert log(-E).expand() == 1 + I*pi + + assert unchanged(log, pi) + assert log(-pi).expand() == log(pi) + I*pi + + assert unchanged(log, 17) + assert log(-17) == log(17) + I*pi + + assert log(I) == I*pi/2 + assert log(-I) == -I*pi/2 + + assert log(17*I) == I*pi/2 + log(17) + assert log(-17*I).expand() == -I*pi/2 + log(17) + + assert log(oo*I) is oo + assert log(-oo*I) is oo + assert log(0, 2) is zoo + assert log(0, 5) is zoo + + assert exp(-log(3))**(-1) == 3 + + assert log(S.Half) == -log(2) + assert log(2*3).func is log + assert log(2*3**2).func is log + + +def test_match_real_imag(): + x, y = symbols('x,y', real=True) + i = Symbol('i', imaginary=True) + assert match_real_imag(S.One) == (1, 0) + assert match_real_imag(I) == (0, 1) + assert match_real_imag(3 - 5*I) == (3, -5) + assert match_real_imag(-sqrt(3) + S.Half*I) == (-sqrt(3), S.Half) + assert match_real_imag(x + y*I) == (x, y) + assert match_real_imag(x*I + y*I) == (0, x + y) + assert match_real_imag((x + y)*I) == (0, x + y) + assert match_real_imag(Rational(-2, 3)*i*I) == (None, None) + assert match_real_imag(1 - 2*i) == (None, None) + assert match_real_imag(sqrt(2)*(3 - 5*I)) == (None, None) + + +def test_log_exact(): + # check for pi/2, pi/3, pi/4, pi/6, pi/8, pi/12; pi/5, pi/10: + for n in range(-23, 24): + if gcd(n, 24) != 1: + assert log(exp(n*I*pi/24).rewrite(sqrt)) == n*I*pi/24 + for n in range(-9, 10): + assert log(exp(n*I*pi/10).rewrite(sqrt)) == n*I*pi/10 + + assert log(S.Half - I*sqrt(3)/2) == -I*pi/3 + assert log(Rational(-1, 2) + I*sqrt(3)/2) == I*pi*Rational(2, 3) + assert log(-sqrt(2)/2 - I*sqrt(2)/2) == -I*pi*Rational(3, 4) + assert log(-sqrt(3)/2 - I*S.Half) == -I*pi*Rational(5, 6) + + assert log(Rational(-1, 4) + sqrt(5)/4 - I*sqrt(sqrt(5)/8 + Rational(5, 8))) == -I*pi*Rational(2, 5) + assert log(sqrt(Rational(5, 8) - sqrt(5)/8) + I*(Rational(1, 4) + sqrt(5)/4)) == I*pi*Rational(3, 10) + assert log(-sqrt(sqrt(2)/4 + S.Half) + I*sqrt(S.Half - sqrt(2)/4)) == I*pi*Rational(7, 8) + assert log(-sqrt(6)/4 - sqrt(2)/4 + I*(-sqrt(6)/4 + sqrt(2)/4)) == -I*pi*Rational(11, 12) + + assert log(-1 + I*sqrt(3)) == log(2) + I*pi*Rational(2, 3) + assert log(5 + 5*I) == log(5*sqrt(2)) + I*pi/4 + assert log(sqrt(-12)) == log(2*sqrt(3)) + I*pi/2 + assert log(-sqrt(6) + sqrt(2) - I*sqrt(6) - I*sqrt(2)) == log(4) - I*pi*Rational(7, 12) + assert log(-sqrt(6-3*sqrt(2)) - I*sqrt(6+3*sqrt(2))) == log(2*sqrt(3)) - I*pi*Rational(5, 8) + assert log(1 + I*sqrt(2-sqrt(2))/sqrt(2+sqrt(2))) == log(2/sqrt(sqrt(2) + 2)) + I*pi/8 + assert log(cos(pi*Rational(7, 12)) + I*sin(pi*Rational(7, 12))) == I*pi*Rational(7, 12) + assert log(cos(pi*Rational(6, 5)) + I*sin(pi*Rational(6, 5))) == I*pi*Rational(-4, 5) + + assert log(5*(1 + I)/sqrt(2)) == log(5) + I*pi/4 + assert log(sqrt(2)*(-sqrt(3) + 1 - sqrt(3)*I - I)) == log(4) - I*pi*Rational(7, 12) + assert log(-sqrt(2)*(1 - I*sqrt(3))) == log(2*sqrt(2)) + I*pi*Rational(2, 3) + assert log(sqrt(3)*I*(-sqrt(6 - 3*sqrt(2)) - I*sqrt(3*sqrt(2) + 6))) == log(6) - I*pi/8 + + zero = (1 + sqrt(2))**2 - 3 - 2*sqrt(2) + assert log(zero - I*sqrt(3)) == log(sqrt(3)) - I*pi/2 + assert unchanged(log, zero + I*zero) or log(zero + zero*I) is zoo + + # bail quickly if no obvious simplification is possible: + assert unchanged(log, (sqrt(2)-1/sqrt(sqrt(3)+I))**1000) + # beware of non-real coefficients + assert unchanged(log, sqrt(2-sqrt(5))*(1 + I)) + + +def test_log_base(): + assert log(1, 2) == 0 + assert log(2, 2) == 1 + assert log(3, 2) == log(3)/log(2) + assert log(6, 2) == 1 + log(3)/log(2) + assert log(6, 3) == 1 + log(2)/log(3) + assert log(2**3, 2) == 3 + assert log(3**3, 3) == 3 + assert log(5, 1) is zoo + assert log(1, 1) is nan + assert log(Rational(2, 3), 10) == log(Rational(2, 3))/log(10) + assert log(Rational(2, 3), Rational(1, 3)) == -log(2)/log(3) + 1 + assert log(Rational(2, 3), Rational(2, 5)) == \ + log(Rational(2, 3))/log(Rational(2, 5)) + # issue 17148 + assert log(Rational(8, 3), 2) == -log(3)/log(2) + 3 + + +def test_log_symbolic(): + assert log(x, exp(1)) == log(x) + assert log(exp(x)) != x + + assert log(x, exp(1)) == log(x) + assert log(x*y) != log(x) + log(y) + assert log(x/y).expand() != log(x) - log(y) + assert log(x/y).expand(force=True) == log(x) - log(y) + assert log(x**y).expand() != y*log(x) + assert log(x**y).expand(force=True) == y*log(x) + + assert log(x, 2) == log(x)/log(2) + assert log(E, 2) == 1/log(2) + + p, q = symbols('p,q', positive=True) + r = Symbol('r', real=True) + + assert log(p**2) != 2*log(p) + assert log(p**2).expand() == 2*log(p) + assert log(x**2).expand() != 2*log(x) + assert log(p**q) != q*log(p) + assert log(exp(p)) == p + assert log(p*q) != log(p) + log(q) + assert log(p*q).expand() == log(p) + log(q) + + assert log(-sqrt(3)) == log(sqrt(3)) + I*pi + assert log(-exp(p)) != p + I*pi + assert log(-exp(x)).expand() != x + I*pi + assert log(-exp(r)).expand() == r + I*pi + + assert log(x**y) != y*log(x) + + assert (log(x**-5)**-1).expand() != -1/log(x)/5 + assert (log(p**-5)**-1).expand() == -1/log(p)/5 + assert log(-x).func is log and log(-x).args[0] == -x + assert log(-p).func is log and log(-p).args[0] == -p + + +def test_log_exp(): + assert log(exp(4*I*pi)) == 0 # exp evaluates + assert log(exp(-5*I*pi)) == I*pi # exp evaluates + assert log(exp(I*pi*Rational(19, 4))) == I*pi*Rational(3, 4) + assert log(exp(I*pi*Rational(25, 7))) == I*pi*Rational(-3, 7) + assert log(exp(-5*I)) == -5*I + 2*I*pi + + +@_both_exp_pow +def test_exp_assumptions(): + r = Symbol('r', real=True) + i = Symbol('i', imaginary=True) + for e in exp, exp_polar: + assert e(x).is_real is None + assert e(x).is_imaginary is None + assert e(i).is_real is None + assert e(i).is_imaginary is None + assert e(r).is_real is True + assert e(r).is_imaginary is False + assert e(re(x)).is_extended_real is True + assert e(re(x)).is_imaginary is False + + assert Pow(E, I*pi, evaluate=False).is_imaginary == False + assert Pow(E, 2*I*pi, evaluate=False).is_imaginary == False + assert Pow(E, I*pi/2, evaluate=False).is_imaginary == True + assert Pow(E, I*pi/3, evaluate=False).is_imaginary is None + + assert exp(0, evaluate=False).is_algebraic + + a = Symbol('a', algebraic=True) + an = Symbol('an', algebraic=True, nonzero=True) + r = Symbol('r', rational=True) + rn = Symbol('rn', rational=True, nonzero=True) + assert exp(a).is_algebraic is None + assert exp(an).is_algebraic is False + assert exp(pi*r).is_algebraic is None + assert exp(pi*rn).is_algebraic is False + + assert exp(0, evaluate=False).is_algebraic is True + assert exp(I*pi/3, evaluate=False).is_algebraic is True + assert exp(I*pi*r, evaluate=False).is_algebraic is True + + +@_both_exp_pow +def test_exp_AccumBounds(): + assert exp(AccumBounds(1, 2)) == AccumBounds(E, E**2) + + +def test_log_assumptions(): + p = symbols('p', positive=True) + n = symbols('n', negative=True) + z = symbols('z', zero=True) + x = symbols('x', infinite=True, extended_positive=True) + + assert log(z).is_positive is False + assert log(x).is_extended_positive is True + assert log(2) > 0 + assert log(1, evaluate=False).is_zero + assert log(1 + z).is_zero + assert log(p).is_zero is None + assert log(n).is_zero is False + assert log(0.5).is_negative is True + assert log(exp(p) + 1).is_positive + + assert log(1, evaluate=False).is_algebraic + assert log(42, evaluate=False).is_algebraic is False + + assert log(1 + z).is_rational + + +def test_log_hashing(): + assert x != log(log(x)) + assert hash(x) != hash(log(log(x))) + assert log(x) != log(log(log(x))) + + e = 1/log(log(x) + log(log(x))) + assert e.base.func is log + e = 1/log(log(x) + log(log(log(x)))) + assert e.base.func is log + + e = log(log(x)) + assert e.func is log + assert x.func is not log + assert hash(log(log(x))) != hash(x) + assert e != x + + +def test_log_sign(): + assert sign(log(2)) == 1 + + +def test_log_expand_complex(): + assert log(1 + I).expand(complex=True) == log(2)/2 + I*pi/4 + assert log(1 - sqrt(2)).expand(complex=True) == log(sqrt(2) - 1) + I*pi + + +def test_log_apply_evalf(): + value = (log(3)/log(2) - 1).evalf() + assert value.epsilon_eq(Float("0.58496250072115618145373")) + + +def test_log_leading_term(): + p = Symbol('p') + + # Test for STEP 3 + assert log(1 + x + x**2).as_leading_term(x, cdir=1) == x + # Test for STEP 4 + assert log(2*x).as_leading_term(x, cdir=1) == log(x) + log(2) + assert log(2*x).as_leading_term(x, cdir=-1) == log(x) + log(2) + assert log(-2*x).as_leading_term(x, cdir=1, logx=p) == p + log(2) + I*pi + assert log(-2*x).as_leading_term(x, cdir=-1, logx=p) == p + log(2) - I*pi + # Test for STEP 5 + assert log(-2*x + (3 - I)*x**2).as_leading_term(x, cdir=1) == log(x) + log(2) - I*pi + assert log(-2*x + (3 - I)*x**2).as_leading_term(x, cdir=-1) == log(x) + log(2) - I*pi + assert log(2*x + (3 - I)*x**2).as_leading_term(x, cdir=1) == log(x) + log(2) + assert log(2*x + (3 - I)*x**2).as_leading_term(x, cdir=-1) == log(x) + log(2) - 2*I*pi + assert log(-1 + x - I*x**2 + I*x**3).as_leading_term(x, cdir=1) == -I*pi + assert log(-1 + x - I*x**2 + I*x**3).as_leading_term(x, cdir=-1) == -I*pi + assert log(-1/(1 - x)).as_leading_term(x, cdir=1) == I*pi + assert log(-1/(1 - x)).as_leading_term(x, cdir=-1) == I*pi + + +def test_log_nseries(): + p = Symbol('p') + assert log(1/x)._eval_nseries(x, 4, logx=-p, cdir=1) == p + assert log(1/x)._eval_nseries(x, 4, logx=-p, cdir=-1) == p + 2*I*pi + assert log(x - 1)._eval_nseries(x, 4, None, I) == I*pi - x - x**2/2 - x**3/3 + O(x**4) + assert log(x - 1)._eval_nseries(x, 4, None, -I) == -I*pi - x - x**2/2 - x**3/3 + O(x**4) + assert log(I*x + I*x**3 - 1)._eval_nseries(x, 3, None, 1) == I*pi - I*x + x**2/2 + O(x**3) + assert log(I*x + I*x**3 - 1)._eval_nseries(x, 3, None, -1) == -I*pi - I*x + x**2/2 + O(x**3) + assert log(I*x**2 + I*x**3 - 1)._eval_nseries(x, 3, None, 1) == I*pi - I*x**2 + O(x**3) + assert log(I*x**2 + I*x**3 - 1)._eval_nseries(x, 3, None, -1) == I*pi - I*x**2 + O(x**3) + assert log(2*x + (3 - I)*x**2)._eval_nseries(x, 3, None, 1) == log(2) + log(x) + \ + x*(S(3)/2 - I/2) + x**2*(-1 + 3*I/4) + O(x**3) + assert log(2*x + (3 - I)*x**2)._eval_nseries(x, 3, None, -1) == -2*I*pi + log(2) + \ + log(x) - x*(-S(3)/2 + I/2) + x**2*(-1 + 3*I/4) + O(x**3) + assert log(-2*x + (3 - I)*x**2)._eval_nseries(x, 3, None, 1) == -I*pi + log(2) + log(x) + \ + x*(-S(3)/2 + I/2) + x**2*(-1 + 3*I/4) + O(x**3) + assert log(-2*x + (3 - I)*x**2)._eval_nseries(x, 3, None, -1) == -I*pi + log(2) + log(x) - \ + x*(S(3)/2 - I/2) + x**2*(-1 + 3*I/4) + O(x**3) + assert log(sqrt(-I*x**2 - 3)*sqrt(-I*x**2 - 1) - 2)._eval_nseries(x, 3, None, 1) == -I*pi + \ + log(sqrt(3) + 2) + 2*sqrt(3)*I*x**2/(3*sqrt(3) + 6) + O(x**3) + assert log(-1/(1 - x))._eval_nseries(x, 3, None, 1) == I*pi + x + x**2/2 + O(x**3) + assert log(-1/(1 - x))._eval_nseries(x, 3, None, -1) == I*pi + x + x**2/2 + O(x**3) + + +def test_log_series(): + # Note Series at infinities other than oo/-oo were introduced as a part of + # pull request 23798. Refer https://github.com/sympy/sympy/pull/23798 for + # more information. + expr1 = log(1 + x) + expr2 = log(x + sqrt(x**2 + 1)) + + assert expr1.series(x, x0=I*oo, n=4) == 1/(3*x**3) - 1/(2*x**2) + 1/x + \ + I*pi/2 - log(I/x) + O(x**(-4), (x, oo*I)) + assert expr1.series(x, x0=-I*oo, n=4) == 1/(3*x**3) - 1/(2*x**2) + 1/x - \ + I*pi/2 - log(-I/x) + O(x**(-4), (x, -oo*I)) + assert expr2.series(x, x0=I*oo, n=4) == 1/(4*x**2) + I*pi/2 + log(2) - \ + log(I/x) + O(x**(-4), (x, oo*I)) + assert expr2.series(x, x0=-I*oo, n=4) == -1/(4*x**2) - I*pi/2 - log(2) + \ + log(-I/x) + O(x**(-4), (x, -oo*I)) + + +def test_log_expand(): + w = Symbol("w", positive=True) + e = log(w**(log(5)/log(3))) + assert e.expand() == log(5)/log(3) * log(w) + x, y, z = symbols('x,y,z', positive=True) + assert log(x*(y + z)).expand(mul=False) == log(x) + log(y + z) + assert log(log(x**2)*log(y*z)).expand() in [log(2*log(x)*log(y) + + 2*log(x)*log(z)), log(log(x)*log(z) + log(y)*log(x)) + log(2), + log((log(y) + log(z))*log(x)) + log(2)] + assert log(x**log(x**2)).expand(deep=False) == log(x)*log(x**2) + assert log(x**log(x**2)).expand() == 2*log(x)**2 + x, y = symbols('x,y') + assert log(x*y).expand(force=True) == log(x) + log(y) + assert log(x**y).expand(force=True) == y*log(x) + assert log(exp(x)).expand(force=True) == x + + # there's generally no need to expand out logs since this requires + # factoring and if simplification is sought, it's cheaper to put + # logs together than it is to take them apart. + assert log(2*3**2).expand() != 2*log(3) + log(2) + + +@XFAIL +def test_log_expand_fail(): + x, y, z = symbols('x,y,z', positive=True) + assert (log(x*(y + z))*(x + y)).expand(mul=True, log=True) == y*log( + x) + y*log(y + z) + z*log(x) + z*log(y + z) + + +def test_log_simplify(): + x = Symbol("x", positive=True) + assert log(x**2).expand() == 2*log(x) + assert expand_log(log(x**(2 + log(2)))) == (2 + log(2))*log(x) + + z = Symbol('z') + assert log(sqrt(z)).expand() == log(z)/2 + assert expand_log(log(z**(log(2) - 1))) == (log(2) - 1)*log(z) + assert log(z**(-1)).expand() != -log(z) + assert log(z**(x/(x+1))).expand() == x*log(z)/(x + 1) + + +def test_log_AccumBounds(): + assert log(AccumBounds(1, E)) == AccumBounds(0, 1) + assert log(AccumBounds(0, E)) == AccumBounds(-oo, 1) + assert log(AccumBounds(-1, E)) == S.NaN + assert log(AccumBounds(0, oo)) == AccumBounds(-oo, oo) + assert log(AccumBounds(-oo, 0)) == S.NaN + assert log(AccumBounds(-oo, oo)) == S.NaN + + +@_both_exp_pow +def test_lambertw(): + k = Symbol('k') + + assert LambertW(x, 0) == LambertW(x) + assert LambertW(x, 0, evaluate=False) != LambertW(x) + assert LambertW(0) == 0 + assert LambertW(E) == 1 + assert LambertW(-1/E) == -1 + assert LambertW(-log(2)/2) == -log(2) + assert LambertW(oo) is oo + assert LambertW(0, 1) is -oo + assert LambertW(0, 42) is -oo + assert LambertW(-pi/2, -1) == -I*pi/2 + assert LambertW(-1/E, -1) == -1 + assert LambertW(-2*exp(-2), -1) == -2 + assert LambertW(2*log(2)) == log(2) + assert LambertW(-pi/2) == I*pi/2 + assert LambertW(exp(1 + E)) == E + + assert LambertW(x**2).diff(x) == 2*LambertW(x**2)/x/(1 + LambertW(x**2)) + assert LambertW(x, k).diff(x) == LambertW(x, k)/x/(1 + LambertW(x, k)) + + assert LambertW(sqrt(2)).evalf(30).epsilon_eq( + Float("0.701338383413663009202120278965", 30), 1e-29) + assert re(LambertW(2, -1)).evalf().epsilon_eq(Float("-0.834310366631110")) + + assert LambertW(-1).is_real is False # issue 5215 + assert LambertW(2, evaluate=False).is_real + p = Symbol('p', positive=True) + assert LambertW(p, evaluate=False).is_real + assert LambertW(p - 1, evaluate=False).is_real is None + assert LambertW(-p - 2/S.Exp1, evaluate=False).is_real is False + assert LambertW(S.Half, -1, evaluate=False).is_real is False + assert LambertW(Rational(-1, 10), -1, evaluate=False).is_real + assert LambertW(-10, -1, evaluate=False).is_real is False + assert LambertW(-2, 2, evaluate=False).is_real is False + + assert LambertW(0, evaluate=False).is_algebraic + na = Symbol('na', nonzero=True, algebraic=True) + assert LambertW(na).is_algebraic is False + assert LambertW(p).is_zero is False + n = Symbol('n', negative=True) + assert LambertW(n).is_zero is False + + +def test_issue_5673(): + e = LambertW(-1) + assert e.is_comparable is False + assert e.is_positive is not True + e2 = 1 - 1/(1 - exp(-1000)) + assert e2.is_positive is not True + e3 = -2 + exp(exp(LambertW(log(2)))*LambertW(log(2))) + assert e3.is_nonzero is not True + + +def test_log_fdiff(): + x = Symbol('x') + raises(ArgumentIndexError, lambda: log(x).fdiff(2)) + + +def test_log_taylor_term(): + x = symbols('x') + assert log(x).taylor_term(0, x) == x + assert log(x).taylor_term(1, x) == -x**2/2 + assert log(x).taylor_term(4, x) == x**5/5 + assert log(x).taylor_term(-1, x) is S.Zero + + +def test_exp_expand_NC(): + A, B, C = symbols('A,B,C', commutative=False) + + assert exp(A + B).expand() == exp(A + B) + assert exp(A + B + C).expand() == exp(A + B + C) + assert exp(x + y).expand() == exp(x)*exp(y) + assert exp(x + y + z).expand() == exp(x)*exp(y)*exp(z) + + +@_both_exp_pow +def test_as_numer_denom(): + n = symbols('n', negative=True) + assert exp(x).as_numer_denom() == (exp(x), 1) + assert exp(-x).as_numer_denom() == (1, exp(x)) + assert exp(-2*x).as_numer_denom() == (1, exp(2*x)) + assert exp(-2).as_numer_denom() == (1, exp(2)) + assert exp(n).as_numer_denom() == (1, exp(-n)) + assert exp(-n).as_numer_denom() == (exp(-n), 1) + assert exp(-I*x).as_numer_denom() == (1, exp(I*x)) + assert exp(-I*n).as_numer_denom() == (1, exp(I*n)) + assert exp(-n).as_numer_denom() == (exp(-n), 1) + # Check noncommutativity + a = symbols('a', commutative=False) + assert exp(-a).as_numer_denom() == (exp(-a), 1) + + +@_both_exp_pow +def test_polar(): + x, y = symbols('x y', polar=True) + + assert abs(exp_polar(I*4)) == 1 + assert abs(exp_polar(0)) == 1 + assert abs(exp_polar(2 + 3*I)) == exp(2) + assert exp_polar(I*10).n() == exp_polar(I*10) + + assert log(exp_polar(z)) == z + assert log(x*y).expand() == log(x) + log(y) + assert log(x**z).expand() == z*log(x) + + assert exp_polar(3).exp == 3 + + # Compare exp(1.0*pi*I). + assert (exp_polar(1.0*pi*I).n(n=5)).as_real_imag()[1] >= 0 + + assert exp_polar(0).is_rational is True # issue 8008 + + +def test_exp_summation(): + w = symbols("w") + m, n, i, j = symbols("m n i j") + expr = exp(Sum(w*i, (i, 0, n), (j, 0, m))) + assert expr.expand() == Product(exp(w*i), (i, 0, n), (j, 0, m)) + + +def test_log_product(): + from sympy.abc import n, m + + i, j = symbols('i,j', positive=True, integer=True) + x, y = symbols('x,y', positive=True) + z = symbols('z', real=True) + w = symbols('w') + + expr = log(Product(x**i, (i, 1, n))) + assert simplify(expr) == expr + assert expr.expand() == Sum(i*log(x), (i, 1, n)) + expr = log(Product(x**i*y**j, (i, 1, n), (j, 1, m))) + assert simplify(expr) == expr + assert expr.expand() == Sum(i*log(x) + j*log(y), (i, 1, n), (j, 1, m)) + + expr = log(Product(-2, (n, 0, 4))) + assert simplify(expr) == expr + assert expr.expand() == expr + assert expr.expand(force=True) == Sum(log(-2), (n, 0, 4)) + + expr = log(Product(exp(z*i), (i, 0, n))) + assert expr.expand() == Sum(z*i, (i, 0, n)) + + expr = log(Product(exp(w*i), (i, 0, n))) + assert expr.expand() == expr + assert expr.expand(force=True) == Sum(w*i, (i, 0, n)) + + expr = log(Product(i**2*abs(j), (i, 1, n), (j, 1, m))) + assert expr.expand() == Sum(2*log(i) + log(j), (i, 1, n), (j, 1, m)) + + +@XFAIL +def test_log_product_simplify_to_sum(): + from sympy.abc import n, m + i, j = symbols('i,j', positive=True, integer=True) + x, y = symbols('x,y', positive=True) + assert simplify(log(Product(x**i, (i, 1, n)))) == Sum(i*log(x), (i, 1, n)) + assert simplify(log(Product(x**i*y**j, (i, 1, n), (j, 1, m)))) == \ + Sum(i*log(x) + j*log(y), (i, 1, n), (j, 1, m)) + + +def test_issue_8866(): + assert simplify(log(x, 10, evaluate=False)) == simplify(log(x, 10)) + assert expand_log(log(x, 10, evaluate=False)) == expand_log(log(x, 10)) + + y = Symbol('y', positive=True) + l1 = log(exp(y), exp(10)) + b1 = log(exp(y), exp(5)) + l2 = log(exp(y), exp(10), evaluate=False) + b2 = log(exp(y), exp(5), evaluate=False) + assert simplify(log(l1, b1)) == simplify(log(l2, b2)) + assert expand_log(log(l1, b1)) == expand_log(log(l2, b2)) + + +def test_log_expand_factor(): + assert (log(18)/log(3) - 2).expand(factor=True) == log(2)/log(3) + assert (log(12)/log(2)).expand(factor=True) == log(3)/log(2) + 2 + assert (log(15)/log(3)).expand(factor=True) == 1 + log(5)/log(3) + assert (log(2)/(-log(12) + log(24))).expand(factor=True) == 1 + + assert expand_log(log(12), factor=True) == log(3) + 2*log(2) + assert expand_log(log(21)/log(7), factor=False) == log(3)/log(7) + 1 + assert expand_log(log(45)/log(5) + log(20), factor=False) == \ + 1 + 2*log(3)/log(5) + log(20) + assert expand_log(log(45)/log(5) + log(26), factor=True) == \ + log(2) + log(13) + (log(5) + 2*log(3))/log(5) + + +def test_issue_9116(): + n = Symbol('n', positive=True, integer=True) + assert log(n).is_nonnegative is True + + +def test_issue_18473(): + assert exp(x*log(cos(1/x))).as_leading_term(x) == S.NaN + assert exp(x*log(tan(1/x))).as_leading_term(x) == S.NaN + assert log(cos(1/x)).as_leading_term(x) == S.NaN + assert log(tan(1/x)).as_leading_term(x) == S.NaN + assert log(cos(1/x) + 2).as_leading_term(x) == AccumBounds(0, log(3)) + assert exp(x*log(cos(1/x) + 2)).as_leading_term(x) == 1 + assert log(cos(1/x) - 2).as_leading_term(x) == S.NaN + assert exp(x*log(cos(1/x) - 2)).as_leading_term(x) == S.NaN + assert log(cos(1/x) + 1).as_leading_term(x) == AccumBounds(-oo, log(2)) + assert exp(x*log(cos(1/x) + 1)).as_leading_term(x) == AccumBounds(0, 1) + assert log(sin(1/x)**2).as_leading_term(x) == AccumBounds(-oo, 0) + assert exp(x*log(sin(1/x)**2)).as_leading_term(x) == AccumBounds(0, 1) + assert log(tan(1/x)**2).as_leading_term(x) == AccumBounds(-oo, oo) + assert exp(2*x*(log(tan(1/x)**2))).as_leading_term(x) == AccumBounds(0, oo) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_hyperbolic.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_hyperbolic.py new file mode 100644 index 0000000000000000000000000000000000000000..1ad9f1d51598b9d605b0472e254c5a710d4ed4f5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_hyperbolic.py @@ -0,0 +1,1553 @@ +from sympy.calculus.accumulationbounds import AccumBounds +from sympy.core.function import (expand_mul, expand_trig) +from sympy.core.numbers import (E, I, Integer, Rational, nan, oo, pi, zoo) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import (im, re) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import (acosh, acoth, acsch, asech, asinh, atanh, cosh, coth, csch, sech, sinh, tanh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (acos, asin, cos, cot, sec, sin, tan) +from sympy.series.order import O + +from sympy.core.expr import unchanged +from sympy.core.function import ArgumentIndexError, PoleError +from sympy.testing.pytest import raises + + +def test_sinh(): + x, y = symbols('x,y') + + k = Symbol('k', integer=True) + + assert sinh(nan) is nan + assert sinh(zoo) is nan + + assert sinh(oo) is oo + assert sinh(-oo) is -oo + + assert sinh(0) == 0 + + assert unchanged(sinh, 1) + assert sinh(-1) == -sinh(1) + + assert unchanged(sinh, x) + assert sinh(-x) == -sinh(x) + + assert unchanged(sinh, pi) + assert sinh(-pi) == -sinh(pi) + + assert unchanged(sinh, 2**1024 * E) + assert sinh(-2**1024 * E) == -sinh(2**1024 * E) + + assert sinh(pi*I) == 0 + assert sinh(-pi*I) == 0 + assert sinh(2*pi*I) == 0 + assert sinh(-2*pi*I) == 0 + assert sinh(-3*10**73*pi*I) == 0 + assert sinh(7*10**103*pi*I) == 0 + + assert sinh(pi*I/2) == I + assert sinh(-pi*I/2) == -I + assert sinh(pi*I*Rational(5, 2)) == I + assert sinh(pi*I*Rational(7, 2)) == -I + + assert sinh(pi*I/3) == S.Half*sqrt(3)*I + assert sinh(pi*I*Rational(-2, 3)) == Rational(-1, 2)*sqrt(3)*I + + assert sinh(pi*I/4) == S.Half*sqrt(2)*I + assert sinh(-pi*I/4) == Rational(-1, 2)*sqrt(2)*I + assert sinh(pi*I*Rational(17, 4)) == S.Half*sqrt(2)*I + assert sinh(pi*I*Rational(-3, 4)) == Rational(-1, 2)*sqrt(2)*I + + assert sinh(pi*I/6) == S.Half*I + assert sinh(-pi*I/6) == Rational(-1, 2)*I + assert sinh(pi*I*Rational(7, 6)) == Rational(-1, 2)*I + assert sinh(pi*I*Rational(-5, 6)) == Rational(-1, 2)*I + + assert sinh(pi*I/105) == sin(pi/105)*I + assert sinh(-pi*I/105) == -sin(pi/105)*I + + assert unchanged(sinh, 2 + 3*I) + + assert sinh(x*I) == sin(x)*I + + assert sinh(k*pi*I) == 0 + assert sinh(17*k*pi*I) == 0 + + assert sinh(k*pi*I/2) == sin(k*pi/2)*I + + assert sinh(x).as_real_imag(deep=False) == (cos(im(x))*sinh(re(x)), + sin(im(x))*cosh(re(x))) + x = Symbol('x', extended_real=True) + assert sinh(x).as_real_imag(deep=False) == (sinh(x), 0) + + x = Symbol('x', real=True) + assert sinh(I*x).is_finite is True + assert sinh(x).is_real is True + assert sinh(I).is_real is False + p = Symbol('p', positive=True) + assert sinh(p).is_zero is False + assert sinh(0, evaluate=False).is_zero is True + assert sinh(2*pi*I, evaluate=False).is_zero is True + + +def test_sinh_series(): + x = Symbol('x') + assert sinh(x).series(x, 0, 10) == \ + x + x**3/6 + x**5/120 + x**7/5040 + x**9/362880 + O(x**10) + + +def test_sinh_fdiff(): + x = Symbol('x') + raises(ArgumentIndexError, lambda: sinh(x).fdiff(2)) + + +def test_cosh(): + x, y = symbols('x,y') + + k = Symbol('k', integer=True) + + assert cosh(nan) is nan + assert cosh(zoo) is nan + + assert cosh(oo) is oo + assert cosh(-oo) is oo + + assert cosh(0) == 1 + + assert unchanged(cosh, 1) + assert cosh(-1) == cosh(1) + + assert unchanged(cosh, x) + assert cosh(-x) == cosh(x) + + assert cosh(pi*I) == cos(pi) + assert cosh(-pi*I) == cos(pi) + + assert unchanged(cosh, 2**1024 * E) + assert cosh(-2**1024 * E) == cosh(2**1024 * E) + + assert cosh(pi*I/2) == 0 + assert cosh(-pi*I/2) == 0 + assert cosh((-3*10**73 + 1)*pi*I/2) == 0 + assert cosh((7*10**103 + 1)*pi*I/2) == 0 + + assert cosh(pi*I) == -1 + assert cosh(-pi*I) == -1 + assert cosh(5*pi*I) == -1 + assert cosh(8*pi*I) == 1 + + assert cosh(pi*I/3) == S.Half + assert cosh(pi*I*Rational(-2, 3)) == Rational(-1, 2) + + assert cosh(pi*I/4) == S.Half*sqrt(2) + assert cosh(-pi*I/4) == S.Half*sqrt(2) + assert cosh(pi*I*Rational(11, 4)) == Rational(-1, 2)*sqrt(2) + assert cosh(pi*I*Rational(-3, 4)) == Rational(-1, 2)*sqrt(2) + + assert cosh(pi*I/6) == S.Half*sqrt(3) + assert cosh(-pi*I/6) == S.Half*sqrt(3) + assert cosh(pi*I*Rational(7, 6)) == Rational(-1, 2)*sqrt(3) + assert cosh(pi*I*Rational(-5, 6)) == Rational(-1, 2)*sqrt(3) + + assert cosh(pi*I/105) == cos(pi/105) + assert cosh(-pi*I/105) == cos(pi/105) + + assert unchanged(cosh, 2 + 3*I) + + assert cosh(x*I) == cos(x) + + assert cosh(k*pi*I) == cos(k*pi) + assert cosh(17*k*pi*I) == cos(17*k*pi) + + assert unchanged(cosh, k*pi) + + assert cosh(x).as_real_imag(deep=False) == (cos(im(x))*cosh(re(x)), + sin(im(x))*sinh(re(x))) + x = Symbol('x', extended_real=True) + assert cosh(x).as_real_imag(deep=False) == (cosh(x), 0) + + x = Symbol('x', real=True) + assert cosh(I*x).is_finite is True + assert cosh(I*x).is_real is True + assert cosh(I*2 + 1).is_real is False + assert cosh(5*I*S.Pi/2, evaluate=False).is_zero is True + assert cosh(x).is_zero is False + + +def test_cosh_series(): + x = Symbol('x') + assert cosh(x).series(x, 0, 10) == \ + 1 + x**2/2 + x**4/24 + x**6/720 + x**8/40320 + O(x**10) + + +def test_cosh_fdiff(): + x = Symbol('x') + raises(ArgumentIndexError, lambda: cosh(x).fdiff(2)) + + +def test_tanh(): + x, y = symbols('x,y') + + k = Symbol('k', integer=True) + + assert tanh(nan) is nan + assert tanh(zoo) is nan + + assert tanh(oo) == 1 + assert tanh(-oo) == -1 + + assert tanh(0) == 0 + + assert unchanged(tanh, 1) + assert tanh(-1) == -tanh(1) + + assert unchanged(tanh, x) + assert tanh(-x) == -tanh(x) + + assert unchanged(tanh, pi) + assert tanh(-pi) == -tanh(pi) + + assert unchanged(tanh, 2**1024 * E) + assert tanh(-2**1024 * E) == -tanh(2**1024 * E) + + assert tanh(pi*I) == 0 + assert tanh(-pi*I) == 0 + assert tanh(2*pi*I) == 0 + assert tanh(-2*pi*I) == 0 + assert tanh(-3*10**73*pi*I) == 0 + assert tanh(7*10**103*pi*I) == 0 + + assert tanh(pi*I/2) is zoo + assert tanh(-pi*I/2) is zoo + assert tanh(pi*I*Rational(5, 2)) is zoo + assert tanh(pi*I*Rational(7, 2)) is zoo + + assert tanh(pi*I/3) == sqrt(3)*I + assert tanh(pi*I*Rational(-2, 3)) == sqrt(3)*I + + assert tanh(pi*I/4) == I + assert tanh(-pi*I/4) == -I + assert tanh(pi*I*Rational(17, 4)) == I + assert tanh(pi*I*Rational(-3, 4)) == I + + assert tanh(pi*I/6) == I/sqrt(3) + assert tanh(-pi*I/6) == -I/sqrt(3) + assert tanh(pi*I*Rational(7, 6)) == I/sqrt(3) + assert tanh(pi*I*Rational(-5, 6)) == I/sqrt(3) + + assert tanh(pi*I/105) == tan(pi/105)*I + assert tanh(-pi*I/105) == -tan(pi/105)*I + + assert unchanged(tanh, 2 + 3*I) + + assert tanh(x*I) == tan(x)*I + + assert tanh(k*pi*I) == 0 + assert tanh(17*k*pi*I) == 0 + + assert tanh(k*pi*I/2) == tan(k*pi/2)*I + + assert tanh(x).as_real_imag(deep=False) == (sinh(re(x))*cosh(re(x))/(cos(im(x))**2 + + sinh(re(x))**2), + sin(im(x))*cos(im(x))/(cos(im(x))**2 + sinh(re(x))**2)) + x = Symbol('x', extended_real=True) + assert tanh(x).as_real_imag(deep=False) == (tanh(x), 0) + assert tanh(I*pi/3 + 1).is_real is False + assert tanh(x).is_real is True + assert tanh(I*pi*x/2).is_real is None + + +def test_tanh_series(): + x = Symbol('x') + assert tanh(x).series(x, 0, 10) == \ + x - x**3/3 + 2*x**5/15 - 17*x**7/315 + 62*x**9/2835 + O(x**10) + + +def test_tanh_fdiff(): + x = Symbol('x') + raises(ArgumentIndexError, lambda: tanh(x).fdiff(2)) + + +def test_coth(): + x, y = symbols('x,y') + + k = Symbol('k', integer=True) + + assert coth(nan) is nan + assert coth(zoo) is nan + + assert coth(oo) == 1 + assert coth(-oo) == -1 + + assert coth(0) is zoo + assert unchanged(coth, 1) + assert coth(-1) == -coth(1) + + assert unchanged(coth, x) + assert coth(-x) == -coth(x) + + assert coth(pi*I) == -I*cot(pi) + assert coth(-pi*I) == cot(pi)*I + + assert unchanged(coth, 2**1024 * E) + assert coth(-2**1024 * E) == -coth(2**1024 * E) + + assert coth(pi*I) == -I*cot(pi) + assert coth(-pi*I) == I*cot(pi) + assert coth(2*pi*I) == -I*cot(2*pi) + assert coth(-2*pi*I) == I*cot(2*pi) + assert coth(-3*10**73*pi*I) == I*cot(3*10**73*pi) + assert coth(7*10**103*pi*I) == -I*cot(7*10**103*pi) + + assert coth(pi*I/2) == 0 + assert coth(-pi*I/2) == 0 + assert coth(pi*I*Rational(5, 2)) == 0 + assert coth(pi*I*Rational(7, 2)) == 0 + + assert coth(pi*I/3) == -I/sqrt(3) + assert coth(pi*I*Rational(-2, 3)) == -I/sqrt(3) + + assert coth(pi*I/4) == -I + assert coth(-pi*I/4) == I + assert coth(pi*I*Rational(17, 4)) == -I + assert coth(pi*I*Rational(-3, 4)) == -I + + assert coth(pi*I/6) == -sqrt(3)*I + assert coth(-pi*I/6) == sqrt(3)*I + assert coth(pi*I*Rational(7, 6)) == -sqrt(3)*I + assert coth(pi*I*Rational(-5, 6)) == -sqrt(3)*I + + assert coth(pi*I/105) == -cot(pi/105)*I + assert coth(-pi*I/105) == cot(pi/105)*I + + assert unchanged(coth, 2 + 3*I) + + assert coth(x*I) == -cot(x)*I + + assert coth(k*pi*I) == -cot(k*pi)*I + assert coth(17*k*pi*I) == -cot(17*k*pi)*I + + assert coth(k*pi*I) == -cot(k*pi)*I + + assert coth(log(tan(2))) == coth(log(-tan(2))) + assert coth(1 + I*pi/2) == tanh(1) + + assert coth(x).as_real_imag(deep=False) == (sinh(re(x))*cosh(re(x))/(sin(im(x))**2 + + sinh(re(x))**2), + -sin(im(x))*cos(im(x))/(sin(im(x))**2 + sinh(re(x))**2)) + x = Symbol('x', extended_real=True) + assert coth(x).as_real_imag(deep=False) == (coth(x), 0) + + assert expand_trig(coth(2*x)) == (coth(x)**2 + 1)/(2*coth(x)) + assert expand_trig(coth(3*x)) == (coth(x)**3 + 3*coth(x))/(1 + 3*coth(x)**2) + + assert expand_trig(coth(x + y)) == (1 + coth(x)*coth(y))/(coth(x) + coth(y)) + + +def test_coth_series(): + x = Symbol('x') + assert coth(x).series(x, 0, 8) == \ + 1/x + x/3 - x**3/45 + 2*x**5/945 - x**7/4725 + O(x**8) + + +def test_coth_fdiff(): + x = Symbol('x') + raises(ArgumentIndexError, lambda: coth(x).fdiff(2)) + + +def test_csch(): + x, y = symbols('x,y') + + k = Symbol('k', integer=True) + n = Symbol('n', positive=True) + + assert csch(nan) is nan + assert csch(zoo) is nan + + assert csch(oo) == 0 + assert csch(-oo) == 0 + + assert csch(0) is zoo + + assert csch(-1) == -csch(1) + + assert csch(-x) == -csch(x) + assert csch(-pi) == -csch(pi) + assert csch(-2**1024 * E) == -csch(2**1024 * E) + + assert csch(pi*I) is zoo + assert csch(-pi*I) is zoo + assert csch(2*pi*I) is zoo + assert csch(-2*pi*I) is zoo + assert csch(-3*10**73*pi*I) is zoo + assert csch(7*10**103*pi*I) is zoo + + assert csch(pi*I/2) == -I + assert csch(-pi*I/2) == I + assert csch(pi*I*Rational(5, 2)) == -I + assert csch(pi*I*Rational(7, 2)) == I + + assert csch(pi*I/3) == -2/sqrt(3)*I + assert csch(pi*I*Rational(-2, 3)) == 2/sqrt(3)*I + + assert csch(pi*I/4) == -sqrt(2)*I + assert csch(-pi*I/4) == sqrt(2)*I + assert csch(pi*I*Rational(7, 4)) == sqrt(2)*I + assert csch(pi*I*Rational(-3, 4)) == sqrt(2)*I + + assert csch(pi*I/6) == -2*I + assert csch(-pi*I/6) == 2*I + assert csch(pi*I*Rational(7, 6)) == 2*I + assert csch(pi*I*Rational(-7, 6)) == -2*I + assert csch(pi*I*Rational(-5, 6)) == 2*I + + assert csch(pi*I/105) == -1/sin(pi/105)*I + assert csch(-pi*I/105) == 1/sin(pi/105)*I + + assert csch(x*I) == -1/sin(x)*I + + assert csch(k*pi*I) is zoo + assert csch(17*k*pi*I) is zoo + + assert csch(k*pi*I/2) == -1/sin(k*pi/2)*I + + assert csch(n).is_real is True + + assert expand_trig(csch(x + y)) == 1/(sinh(x)*cosh(y) + cosh(x)*sinh(y)) + + +def test_csch_series(): + x = Symbol('x') + assert csch(x).series(x, 0, 10) == \ + 1/ x - x/6 + 7*x**3/360 - 31*x**5/15120 + 127*x**7/604800 \ + - 73*x**9/3421440 + O(x**10) + + +def test_csch_fdiff(): + x = Symbol('x') + raises(ArgumentIndexError, lambda: csch(x).fdiff(2)) + + +def test_sech(): + x, y = symbols('x, y') + + k = Symbol('k', integer=True) + n = Symbol('n', positive=True) + + assert sech(nan) is nan + assert sech(zoo) is nan + + assert sech(oo) == 0 + assert sech(-oo) == 0 + + assert sech(0) == 1 + + assert sech(-1) == sech(1) + assert sech(-x) == sech(x) + + assert sech(pi*I) == sec(pi) + + assert sech(-pi*I) == sec(pi) + assert sech(-2**1024 * E) == sech(2**1024 * E) + + assert sech(pi*I/2) is zoo + assert sech(-pi*I/2) is zoo + assert sech((-3*10**73 + 1)*pi*I/2) is zoo + assert sech((7*10**103 + 1)*pi*I/2) is zoo + + assert sech(pi*I) == -1 + assert sech(-pi*I) == -1 + assert sech(5*pi*I) == -1 + assert sech(8*pi*I) == 1 + + assert sech(pi*I/3) == 2 + assert sech(pi*I*Rational(-2, 3)) == -2 + + assert sech(pi*I/4) == sqrt(2) + assert sech(-pi*I/4) == sqrt(2) + assert sech(pi*I*Rational(5, 4)) == -sqrt(2) + assert sech(pi*I*Rational(-5, 4)) == -sqrt(2) + + assert sech(pi*I/6) == 2/sqrt(3) + assert sech(-pi*I/6) == 2/sqrt(3) + assert sech(pi*I*Rational(7, 6)) == -2/sqrt(3) + assert sech(pi*I*Rational(-5, 6)) == -2/sqrt(3) + + assert sech(pi*I/105) == 1/cos(pi/105) + assert sech(-pi*I/105) == 1/cos(pi/105) + + assert sech(x*I) == 1/cos(x) + + assert sech(k*pi*I) == 1/cos(k*pi) + assert sech(17*k*pi*I) == 1/cos(17*k*pi) + + assert sech(n).is_real is True + + assert expand_trig(sech(x + y)) == 1/(cosh(x)*cosh(y) + sinh(x)*sinh(y)) + + +def test_sech_series(): + x = Symbol('x') + assert sech(x).series(x, 0, 10) == \ + 1 - x**2/2 + 5*x**4/24 - 61*x**6/720 + 277*x**8/8064 + O(x**10) + + +def test_sech_fdiff(): + x = Symbol('x') + raises(ArgumentIndexError, lambda: sech(x).fdiff(2)) + + +def test_asinh(): + x, y = symbols('x,y') + assert unchanged(asinh, x) + assert asinh(-x) == -asinh(x) + + # at specific points + assert asinh(nan) is nan + assert asinh( 0) == 0 + assert asinh(+1) == log(sqrt(2) + 1) + + assert asinh(-1) == log(sqrt(2) - 1) + assert asinh(I) == pi*I/2 + assert asinh(-I) == -pi*I/2 + assert asinh(I/2) == pi*I/6 + assert asinh(-I/2) == -pi*I/6 + + # at infinites + assert asinh(oo) is oo + assert asinh(-oo) is -oo + + assert asinh(I*oo) is oo + assert asinh(-I *oo) is -oo + + assert asinh(zoo) is zoo + + # properties + assert asinh(I *(sqrt(3) - 1)/(2**Rational(3, 2))) == pi*I/12 + assert asinh(-I *(sqrt(3) - 1)/(2**Rational(3, 2))) == -pi*I/12 + + assert asinh(I*(sqrt(5) - 1)/4) == pi*I/10 + assert asinh(-I*(sqrt(5) - 1)/4) == -pi*I/10 + + assert asinh(I*(sqrt(5) + 1)/4) == pi*I*Rational(3, 10) + assert asinh(-I*(sqrt(5) + 1)/4) == pi*I*Rational(-3, 10) + + # reality + assert asinh(S(2)).is_real is True + assert asinh(S(2)).is_finite is True + assert asinh(S(-2)).is_real is True + assert asinh(S(oo)).is_extended_real is True + assert asinh(-S(oo)).is_real is False + assert (asinh(2) - oo) == -oo + assert asinh(symbols('y', real=True)).is_real is True + + # Symmetry + assert asinh(Rational(-1, 2)) == -asinh(S.Half) + + # inverse composition + assert unchanged(asinh, sinh(Symbol('v1'))) + + assert asinh(sinh(0, evaluate=False)) == 0 + assert asinh(sinh(-3, evaluate=False)) == -3 + assert asinh(sinh(2, evaluate=False)) == 2 + assert asinh(sinh(I, evaluate=False)) == I + assert asinh(sinh(-I, evaluate=False)) == -I + assert asinh(sinh(5*I, evaluate=False)) == -2*I*pi + 5*I + assert asinh(sinh(15 + 11*I)) == 15 - 4*I*pi + 11*I + assert asinh(sinh(-73 + 97*I)) == 73 - 97*I + 31*I*pi + assert asinh(sinh(-7 - 23*I)) == 7 - 7*I*pi + 23*I + assert asinh(sinh(13 - 3*I)) == -13 - I*pi + 3*I + p = Symbol('p', positive=True) + assert asinh(p).is_zero is False + assert asinh(sinh(0, evaluate=False), evaluate=False).is_zero is True + + +def test_asinh_rewrite(): + x = Symbol('x') + assert asinh(x).rewrite(log) == log(x + sqrt(x**2 + 1)) + assert asinh(x).rewrite(atanh) == atanh(x/sqrt(1 + x**2)) + assert asinh(x).rewrite(asin) == -I*asin(I*x, evaluate=False) + assert asinh(x*(1 + I)).rewrite(asin) == -I*asin(I*x*(1+I)) + assert asinh(x).rewrite(acos) == I*acos(I*x, evaluate=False) - I*pi/2 + + +def test_asinh_leading_term(): + x = Symbol('x') + assert asinh(x).as_leading_term(x, cdir=1) == x + # Tests concerning branch points + assert asinh(x + I).as_leading_term(x, cdir=1) == I*pi/2 + assert asinh(x - I).as_leading_term(x, cdir=1) == -I*pi/2 + assert asinh(1/x).as_leading_term(x, cdir=1) == -log(x) + log(2) + assert asinh(1/x).as_leading_term(x, cdir=-1) == log(x) - log(2) - I*pi + # Tests concerning points lying on branch cuts + assert asinh(x + 2*I).as_leading_term(x, cdir=1) == I*asin(2) + assert asinh(x + 2*I).as_leading_term(x, cdir=-1) == -I*asin(2) + I*pi + assert asinh(x - 2*I).as_leading_term(x, cdir=1) == -I*pi + I*asin(2) + assert asinh(x - 2*I).as_leading_term(x, cdir=-1) == -I*asin(2) + # Tests concerning re(ndir) == 0 + assert asinh(2*I + I*x - x**2).as_leading_term(x, cdir=1) == log(2 - sqrt(3)) + I*pi/2 + assert asinh(2*I + I*x - x**2).as_leading_term(x, cdir=-1) == log(2 - sqrt(3)) + I*pi/2 + + +def test_asinh_series(): + x = Symbol('x') + assert asinh(x).series(x, 0, 8) == \ + x - x**3/6 + 3*x**5/40 - 5*x**7/112 + O(x**8) + t5 = asinh(x).taylor_term(5, x) + assert t5 == 3*x**5/40 + assert asinh(x).taylor_term(7, x, t5, 0) == -5*x**7/112 + + +def test_asinh_nseries(): + x = Symbol('x') + # Tests concerning branch points + assert asinh(x + I)._eval_nseries(x, 4, None) == I*pi/2 - \ + sqrt(2)*sqrt(I)*I*sqrt(x) + sqrt(2)*sqrt(I)*x**(S(3)/2)/12 + 3*sqrt(2)*sqrt(I)*I*x**(S(5)/2)/160 - \ + 5*sqrt(2)*sqrt(I)*x**(S(7)/2)/896 + O(x**4) + assert asinh(x - I)._eval_nseries(x, 4, None) == -I*pi/2 + \ + sqrt(2)*I*sqrt(x)*sqrt(-I) + sqrt(2)*x**(S(3)/2)*sqrt(-I)/12 - \ + 3*sqrt(2)*I*x**(S(5)/2)*sqrt(-I)/160 - 5*sqrt(2)*x**(S(7)/2)*sqrt(-I)/896 + O(x**4) + # Tests concerning points lying on branch cuts + assert asinh(x + 2*I)._eval_nseries(x, 4, None, cdir=1) == I*asin(2) - \ + sqrt(3)*I*x/3 + sqrt(3)*x**2/9 + sqrt(3)*I*x**3/18 + O(x**4) + assert asinh(x + 2*I)._eval_nseries(x, 4, None, cdir=-1) == I*pi - I*asin(2) + \ + sqrt(3)*I*x/3 - sqrt(3)*x**2/9 - sqrt(3)*I*x**3/18 + O(x**4) + assert asinh(x - 2*I)._eval_nseries(x, 4, None, cdir=1) == I*asin(2) - I*pi + \ + sqrt(3)*I*x/3 + sqrt(3)*x**2/9 - sqrt(3)*I*x**3/18 + O(x**4) + assert asinh(x - 2*I)._eval_nseries(x, 4, None, cdir=-1) == -I*asin(2) - \ + sqrt(3)*I*x/3 - sqrt(3)*x**2/9 + sqrt(3)*I*x**3/18 + O(x**4) + # Tests concerning re(ndir) == 0 + assert asinh(2*I + I*x - x**2)._eval_nseries(x, 4, None) == I*pi/2 + log(2 - sqrt(3)) + \ + x*(-3 + 2*sqrt(3))/(-6 + 3*sqrt(3)) + x**2*(12 - 36*I + sqrt(3)*(-7 + 21*I))/(-63 + \ + 36*sqrt(3)) + x**3*(-168 + sqrt(3)*(97 - 388*I) + 672*I)/(-1746 + 1008*sqrt(3)) + O(x**4) + + +def test_asinh_fdiff(): + x = Symbol('x') + raises(ArgumentIndexError, lambda: asinh(x).fdiff(2)) + + +def test_acosh(): + x = Symbol('x') + + assert unchanged(acosh, -x) + + #at specific points + assert acosh(1) == 0 + assert acosh(-1) == pi*I + assert acosh(0) == I*pi/2 + assert acosh(S.Half) == I*pi/3 + assert acosh(Rational(-1, 2)) == pi*I*Rational(2, 3) + assert acosh(nan) is nan + + # at infinites + assert acosh(oo) is oo + assert acosh(-oo) is oo + + assert acosh(I*oo) == oo + I*pi/2 + assert acosh(-I*oo) == oo - I*pi/2 + + assert acosh(zoo) is zoo + + assert acosh(I) == log(I*(1 + sqrt(2))) + assert acosh(-I) == log(-I*(1 + sqrt(2))) + assert acosh((sqrt(3) - 1)/(2*sqrt(2))) == pi*I*Rational(5, 12) + assert acosh(-(sqrt(3) - 1)/(2*sqrt(2))) == pi*I*Rational(7, 12) + assert acosh(sqrt(2)/2) == I*pi/4 + assert acosh(-sqrt(2)/2) == I*pi*Rational(3, 4) + assert acosh(sqrt(3)/2) == I*pi/6 + assert acosh(-sqrt(3)/2) == I*pi*Rational(5, 6) + assert acosh(sqrt(2 + sqrt(2))/2) == I*pi/8 + assert acosh(-sqrt(2 + sqrt(2))/2) == I*pi*Rational(7, 8) + assert acosh(sqrt(2 - sqrt(2))/2) == I*pi*Rational(3, 8) + assert acosh(-sqrt(2 - sqrt(2))/2) == I*pi*Rational(5, 8) + assert acosh((1 + sqrt(3))/(2*sqrt(2))) == I*pi/12 + assert acosh(-(1 + sqrt(3))/(2*sqrt(2))) == I*pi*Rational(11, 12) + assert acosh((sqrt(5) + 1)/4) == I*pi/5 + assert acosh(-(sqrt(5) + 1)/4) == I*pi*Rational(4, 5) + + assert str(acosh(5*I).n(6)) == '2.31244 + 1.5708*I' + assert str(acosh(-5*I).n(6)) == '2.31244 - 1.5708*I' + + # inverse composition + assert unchanged(acosh, Symbol('v1')) + + assert acosh(cosh(-3, evaluate=False)) == 3 + assert acosh(cosh(3, evaluate=False)) == 3 + assert acosh(cosh(0, evaluate=False)) == 0 + assert acosh(cosh(I, evaluate=False)) == I + assert acosh(cosh(-I, evaluate=False)) == I + assert acosh(cosh(7*I, evaluate=False)) == -2*I*pi + 7*I + assert acosh(cosh(1 + I)) == 1 + I + assert acosh(cosh(3 - 3*I)) == 3 - 3*I + assert acosh(cosh(-3 + 2*I)) == 3 - 2*I + assert acosh(cosh(-5 - 17*I)) == 5 - 6*I*pi + 17*I + assert acosh(cosh(-21 + 11*I)) == 21 - 11*I + 4*I*pi + assert acosh(cosh(cosh(1) + I)) == cosh(1) + I + assert acosh(1, evaluate=False).is_zero is True + + # Reality + assert acosh(S(2)).is_real is True + assert acosh(S(2)).is_extended_real is True + assert acosh(oo).is_extended_real is True + assert acosh(S(2)).is_finite is True + assert acosh(S(1) / 5).is_real is False + assert (acosh(2) - oo) == -oo + assert acosh(symbols('y', real=True)).is_real is None + + +def test_acosh_rewrite(): + x = Symbol('x') + assert acosh(x).rewrite(log) == log(x + sqrt(x - 1)*sqrt(x + 1)) + assert acosh(x).rewrite(asin) == sqrt(x - 1)*(-asin(x) + pi/2)/sqrt(1 - x) + assert acosh(x).rewrite(asinh) == sqrt(x - 1)*(I*asinh(I*x, evaluate=False) + pi/2)/sqrt(1 - x) + assert acosh(x).rewrite(atanh) == \ + (sqrt(x - 1)*sqrt(x + 1)*atanh(sqrt(x**2 - 1)/x)/sqrt(x**2 - 1) + + pi*sqrt(x - 1)*(-x*sqrt(x**(-2)) + 1)/(2*sqrt(1 - x))) + x = Symbol('x', positive=True) + assert acosh(x).rewrite(atanh) == \ + sqrt(x - 1)*sqrt(x + 1)*atanh(sqrt(x**2 - 1)/x)/sqrt(x**2 - 1) + + +def test_acosh_leading_term(): + x = Symbol('x') + # Tests concerning branch points + assert acosh(x).as_leading_term(x) == I*pi/2 + assert acosh(x + 1).as_leading_term(x) == sqrt(2)*sqrt(x) + assert acosh(x - 1).as_leading_term(x) == I*pi + assert acosh(1/x).as_leading_term(x, cdir=1) == -log(x) + log(2) + assert acosh(1/x).as_leading_term(x, cdir=-1) == -log(x) + log(2) + 2*I*pi + # Tests concerning points lying on branch cuts + assert acosh(I*x - 2).as_leading_term(x, cdir=1) == acosh(-2) + assert acosh(-I*x - 2).as_leading_term(x, cdir=1) == -2*I*pi + acosh(-2) + assert acosh(x**2 - I*x + S(1)/3).as_leading_term(x, cdir=1) == -acosh(S(1)/3) + assert acosh(x**2 - I*x + S(1)/3).as_leading_term(x, cdir=-1) == acosh(S(1)/3) + assert acosh(1/(I*x - 3)).as_leading_term(x, cdir=1) == -acosh(-S(1)/3) + assert acosh(1/(I*x - 3)).as_leading_term(x, cdir=-1) == acosh(-S(1)/3) + # Tests concerning im(ndir) == 0 + assert acosh(-I*x**2 + x - 2).as_leading_term(x, cdir=1) == log(sqrt(3) + 2) - I*pi + assert acosh(-I*x**2 + x - 2).as_leading_term(x, cdir=-1) == log(sqrt(3) + 2) - I*pi + + +def test_acosh_series(): + x = Symbol('x') + assert acosh(x).series(x, 0, 8) == \ + -I*x + pi*I/2 - I*x**3/6 - 3*I*x**5/40 - 5*I*x**7/112 + O(x**8) + t5 = acosh(x).taylor_term(5, x) + assert t5 == - 3*I*x**5/40 + assert acosh(x).taylor_term(7, x, t5, 0) == - 5*I*x**7/112 + + +def test_acosh_nseries(): + x = Symbol('x') + # Tests concerning branch points + assert acosh(x + 1)._eval_nseries(x, 4, None) == sqrt(2)*sqrt(x) - \ + sqrt(2)*x**(S(3)/2)/12 + 3*sqrt(2)*x**(S(5)/2)/160 - 5*sqrt(2)*x**(S(7)/2)/896 + O(x**4) + # Tests concerning points lying on branch cuts + assert acosh(x - 1)._eval_nseries(x, 4, None) == I*pi - \ + sqrt(2)*I*sqrt(x) - sqrt(2)*I*x**(S(3)/2)/12 - 3*sqrt(2)*I*x**(S(5)/2)/160 - \ + 5*sqrt(2)*I*x**(S(7)/2)/896 + O(x**4) + assert acosh(I*x - 2)._eval_nseries(x, 4, None, cdir=1) == acosh(-2) - \ + sqrt(3)*I*x/3 + sqrt(3)*x**2/9 + sqrt(3)*I*x**3/18 + O(x**4) + assert acosh(-I*x - 2)._eval_nseries(x, 4, None, cdir=1) == acosh(-2) - \ + 2*I*pi + sqrt(3)*I*x/3 + sqrt(3)*x**2/9 - sqrt(3)*I*x**3/18 + O(x**4) + assert acosh(1/(I*x - 3))._eval_nseries(x, 4, None, cdir=1) == -acosh(-S(1)/3) + \ + sqrt(2)*x/12 + 17*sqrt(2)*I*x**2/576 - 443*sqrt(2)*x**3/41472 + O(x**4) + assert acosh(1/(I*x - 3))._eval_nseries(x, 4, None, cdir=-1) == acosh(-S(1)/3) - \ + sqrt(2)*x/12 - 17*sqrt(2)*I*x**2/576 + 443*sqrt(2)*x**3/41472 + O(x**4) + # Tests concerning im(ndir) == 0 + assert acosh(-I*x**2 + x - 2)._eval_nseries(x, 4, None) == -I*pi + log(sqrt(3) + 2) + \ + x*(-2*sqrt(3) - 3)/(3*sqrt(3) + 6) + x**2*(-12 + 36*I + sqrt(3)*(-7 + 21*I))/(36*sqrt(3) + \ + 63) + x**3*(-168 + 672*I + sqrt(3)*(-97 + 388*I))/(1008*sqrt(3) + 1746) + O(x**4) + + +def test_acosh_fdiff(): + x = Symbol('x') + raises(ArgumentIndexError, lambda: acosh(x).fdiff(2)) + + +def test_asech(): + x = Symbol('x') + + assert unchanged(asech, -x) + + # values at fixed points + assert asech(1) == 0 + assert asech(-1) == pi*I + assert asech(0) is oo + assert asech(2) == I*pi/3 + assert asech(-2) == 2*I*pi / 3 + assert asech(nan) is nan + + # at infinites + assert asech(oo) == I*pi/2 + assert asech(-oo) == I*pi/2 + assert asech(zoo) == I*AccumBounds(-pi/2, pi/2) + + assert asech(I) == log(1 + sqrt(2)) - I*pi/2 + assert asech(-I) == log(1 + sqrt(2)) + I*pi/2 + assert asech(sqrt(2) - sqrt(6)) == 11*I*pi / 12 + assert asech(sqrt(2 - 2/sqrt(5))) == I*pi / 10 + assert asech(-sqrt(2 - 2/sqrt(5))) == 9*I*pi / 10 + assert asech(2 / sqrt(2 + sqrt(2))) == I*pi / 8 + assert asech(-2 / sqrt(2 + sqrt(2))) == 7*I*pi / 8 + assert asech(sqrt(5) - 1) == I*pi / 5 + assert asech(1 - sqrt(5)) == 4*I*pi / 5 + assert asech(-sqrt(2*(2 + sqrt(2)))) == 5*I*pi / 8 + + # properties + # asech(x) == acosh(1/x) + assert asech(sqrt(2)) == acosh(1/sqrt(2)) + assert asech(2/sqrt(3)) == acosh(sqrt(3)/2) + assert asech(2/sqrt(2 + sqrt(2))) == acosh(sqrt(2 + sqrt(2))/2) + assert asech(2) == acosh(S.Half) + + # reality + assert asech(S(2)).is_real is False + assert asech(-S(1) / 3).is_real is False + assert asech(S(2) / 3).is_finite is True + assert asech(S(0)).is_real is False + assert asech(S(0)).is_extended_real is True + assert asech(symbols('y', real=True)).is_real is None + + # asech(x) == I*acos(1/x) + # (Note: the exact formula is asech(x) == +/- I*acos(1/x)) + assert asech(-sqrt(2)) == I*acos(-1/sqrt(2)) + assert asech(-2/sqrt(3)) == I*acos(-sqrt(3)/2) + assert asech(-S(2)) == I*acos(Rational(-1, 2)) + assert asech(-2/sqrt(2)) == I*acos(-sqrt(2)/2) + + # sech(asech(x)) / x == 1 + assert expand_mul(sech(asech(sqrt(6) - sqrt(2))) / (sqrt(6) - sqrt(2))) == 1 + assert expand_mul(sech(asech(sqrt(6) + sqrt(2))) / (sqrt(6) + sqrt(2))) == 1 + assert (sech(asech(sqrt(2 + 2/sqrt(5)))) / (sqrt(2 + 2/sqrt(5)))).simplify() == 1 + assert (sech(asech(-sqrt(2 + 2/sqrt(5)))) / (-sqrt(2 + 2/sqrt(5)))).simplify() == 1 + assert (sech(asech(sqrt(2*(2 + sqrt(2))))) / (sqrt(2*(2 + sqrt(2))))).simplify() == 1 + assert expand_mul(sech(asech(1 + sqrt(5))) / (1 + sqrt(5))) == 1 + assert expand_mul(sech(asech(-1 - sqrt(5))) / (-1 - sqrt(5))) == 1 + assert expand_mul(sech(asech(-sqrt(6) - sqrt(2))) / (-sqrt(6) - sqrt(2))) == 1 + + # numerical evaluation + assert str(asech(5*I).n(6)) == '0.19869 - 1.5708*I' + assert str(asech(-5*I).n(6)) == '0.19869 + 1.5708*I' + + +def test_asech_leading_term(): + x = Symbol('x') + # Tests concerning branch points + assert asech(x).as_leading_term(x, cdir=1) == -log(x) + log(2) + assert asech(x).as_leading_term(x, cdir=-1) == -log(x) + log(2) + 2*I*pi + assert asech(x + 1).as_leading_term(x, cdir=1) == sqrt(2)*I*sqrt(x) + assert asech(1/x).as_leading_term(x, cdir=1) == I*pi/2 + # Tests concerning points lying on branch cuts + assert asech(x - 1).as_leading_term(x, cdir=1) == I*pi + assert asech(I*x + 3).as_leading_term(x, cdir=1) == -asech(3) + assert asech(-I*x + 3).as_leading_term(x, cdir=1) == asech(3) + assert asech(I*x - 3).as_leading_term(x, cdir=1) == -asech(-3) + assert asech(-I*x - 3).as_leading_term(x, cdir=1) == asech(-3) + assert asech(I*x - S(1)/3).as_leading_term(x, cdir=1) == -2*I*pi + asech(-S(1)/3) + assert asech(I*x - S(1)/3).as_leading_term(x, cdir=-1) == asech(-S(1)/3) + # Tests concerning im(ndir) == 0 + assert asech(-I*x**2 + x - 3).as_leading_term(x, cdir=1) == log(-S(1)/3 + 2*sqrt(2)*I/3) + assert asech(-I*x**2 + x - 3).as_leading_term(x, cdir=-1) == log(-S(1)/3 + 2*sqrt(2)*I/3) + + +def test_asech_series(): + x = Symbol('x') + assert asech(x).series(x, 0, 9, cdir=1) == log(2) - log(x) - x**2/4 - 3*x**4/32 \ + - 5*x**6/96 - 35*x**8/1024 + O(x**9) + assert asech(x).series(x, 0, 9, cdir=-1) == I*pi + log(2) - log(-x) - x**2/4 - \ + 3*x**4/32 - 5*x**6/96 - 35*x**8/1024 + O(x**9) + t6 = asech(x).taylor_term(6, x) + assert t6 == -5*x**6/96 + assert asech(x).taylor_term(8, x, t6, 0) == -35*x**8/1024 + + +def test_asech_nseries(): + x = Symbol('x') + # Tests concerning branch points + assert asech(x + 1)._eval_nseries(x, 4, None) == sqrt(2)*sqrt(-x) + 5*sqrt(2)*(-x)**(S(3)/2)/12 + \ + 43*sqrt(2)*(-x)**(S(5)/2)/160 + 177*sqrt(2)*(-x)**(S(7)/2)/896 + O(x**4) + # Tests concerning points lying on branch cuts + assert asech(x - 1)._eval_nseries(x, 4, None) == I*pi + sqrt(2)*sqrt(x) + \ + 5*sqrt(2)*x**(S(3)/2)/12 + 43*sqrt(2)*x**(S(5)/2)/160 + 177*sqrt(2)*x**(S(7)/2)/896 + O(x**4) + assert asech(I*x + 3)._eval_nseries(x, 4, None) == -asech(3) + sqrt(2)*x/12 - \ + 17*sqrt(2)*I*x**2/576 - 443*sqrt(2)*x**3/41472 + O(x**4) + assert asech(-I*x + 3)._eval_nseries(x, 4, None) == asech(3) + sqrt(2)*x/12 + \ + 17*sqrt(2)*I*x**2/576 - 443*sqrt(2)*x**3/41472 + O(x**4) + assert asech(I*x - 3)._eval_nseries(x, 4, None) == -asech(-3) - sqrt(2)*x/12 - \ + 17*sqrt(2)*I*x**2/576 + 443*sqrt(2)*x**3/41472 + O(x**4) + assert asech(-I*x - 3)._eval_nseries(x, 4, None) == asech(-3) - sqrt(2)*x/12 + \ + 17*sqrt(2)*I*x**2/576 + 443*sqrt(2)*x**3/41472 + O(x**4) + # Tests concerning im(ndir) == 0 + assert asech(-I*x**2 + x - 2)._eval_nseries(x, 3, None) == 2*I*pi/3 + \ + x*(-sqrt(3) + 3*I)/(6*sqrt(3) + 6*I) + x**2*(36 + sqrt(3)*(7 - 12*I) + 21*I)/(72*sqrt(3) - \ + 72*I) + O(x**3) + + +def test_asech_rewrite(): + x = Symbol('x') + assert asech(x).rewrite(log) == log(1/x + sqrt(1/x - 1) * sqrt(1/x + 1)) + assert asech(x).rewrite(acosh) == acosh(1/x) + assert asech(x).rewrite(asinh) == sqrt(-1 + 1/x)*(I*asinh(I/x, evaluate=False) + pi/2)/sqrt(1 - 1/x) + assert asech(x).rewrite(atanh) == \ + sqrt(x + 1)*sqrt(1/(x + 1))*atanh(sqrt(1 - x**2)) + I*pi*(-sqrt(x)*sqrt(1/x) + 1 - I*sqrt(x**2)/(2*sqrt(-x**2)) - I*sqrt(-x)/(2*sqrt(x))) + + +def test_asech_fdiff(): + x = Symbol('x') + raises(ArgumentIndexError, lambda: asech(x).fdiff(2)) + + +def test_acsch(): + x = Symbol('x') + + assert unchanged(acsch, x) + assert acsch(-x) == -acsch(x) + + # values at fixed points + assert acsch(1) == log(1 + sqrt(2)) + assert acsch(-1) == - log(1 + sqrt(2)) + assert acsch(0) is zoo + assert acsch(2) == log((1+sqrt(5))/2) + assert acsch(-2) == - log((1+sqrt(5))/2) + + assert acsch(I) == - I*pi/2 + assert acsch(-I) == I*pi/2 + assert acsch(-I*(sqrt(6) + sqrt(2))) == I*pi / 12 + assert acsch(I*(sqrt(2) + sqrt(6))) == -I*pi / 12 + assert acsch(-I*(1 + sqrt(5))) == I*pi / 10 + assert acsch(I*(1 + sqrt(5))) == -I*pi / 10 + assert acsch(-I*2 / sqrt(2 - sqrt(2))) == I*pi / 8 + assert acsch(I*2 / sqrt(2 - sqrt(2))) == -I*pi / 8 + assert acsch(-I*2) == I*pi / 6 + assert acsch(I*2) == -I*pi / 6 + assert acsch(-I*sqrt(2 + 2/sqrt(5))) == I*pi / 5 + assert acsch(I*sqrt(2 + 2/sqrt(5))) == -I*pi / 5 + assert acsch(-I*sqrt(2)) == I*pi / 4 + assert acsch(I*sqrt(2)) == -I*pi / 4 + assert acsch(-I*(sqrt(5)-1)) == 3*I*pi / 10 + assert acsch(I*(sqrt(5)-1)) == -3*I*pi / 10 + assert acsch(-I*2 / sqrt(3)) == I*pi / 3 + assert acsch(I*2 / sqrt(3)) == -I*pi / 3 + assert acsch(-I*2 / sqrt(2 + sqrt(2))) == 3*I*pi / 8 + assert acsch(I*2 / sqrt(2 + sqrt(2))) == -3*I*pi / 8 + assert acsch(-I*sqrt(2 - 2/sqrt(5))) == 2*I*pi / 5 + assert acsch(I*sqrt(2 - 2/sqrt(5))) == -2*I*pi / 5 + assert acsch(-I*(sqrt(6) - sqrt(2))) == 5*I*pi / 12 + assert acsch(I*(sqrt(6) - sqrt(2))) == -5*I*pi / 12 + assert acsch(nan) is nan + + # properties + # acsch(x) == asinh(1/x) + assert acsch(-I*sqrt(2)) == asinh(I/sqrt(2)) + assert acsch(-I*2 / sqrt(3)) == asinh(I*sqrt(3) / 2) + + # reality + assert acsch(S(2)).is_real is True + assert acsch(S(2)).is_finite is True + assert acsch(S(-2)).is_real is True + assert acsch(S(oo)).is_extended_real is True + assert acsch(-S(oo)).is_real is True + assert (acsch(2) - oo) == -oo + assert acsch(symbols('y', extended_real=True)).is_extended_real is True + + # acsch(x) == -I*asin(I/x) + assert acsch(-I*sqrt(2)) == -I*asin(-1/sqrt(2)) + assert acsch(-I*2 / sqrt(3)) == -I*asin(-sqrt(3)/2) + + # csch(acsch(x)) / x == 1 + assert expand_mul(csch(acsch(-I*(sqrt(6) + sqrt(2)))) / (-I*(sqrt(6) + sqrt(2)))) == 1 + assert expand_mul(csch(acsch(I*(1 + sqrt(5)))) / (I*(1 + sqrt(5)))) == 1 + assert (csch(acsch(I*sqrt(2 - 2/sqrt(5)))) / (I*sqrt(2 - 2/sqrt(5)))).simplify() == 1 + assert (csch(acsch(-I*sqrt(2 - 2/sqrt(5)))) / (-I*sqrt(2 - 2/sqrt(5)))).simplify() == 1 + + # numerical evaluation + assert str(acsch(5*I+1).n(6)) == '0.0391819 - 0.193363*I' + assert str(acsch(-5*I+1).n(6)) == '0.0391819 + 0.193363*I' + + +def test_acsch_infinities(): + assert acsch(oo) == 0 + assert acsch(-oo) == 0 + assert acsch(zoo) == 0 + + +def test_acsch_leading_term(): + x = Symbol('x') + assert acsch(1/x).as_leading_term(x) == x + # Tests concerning branch points + assert acsch(x + I).as_leading_term(x) == -I*pi/2 + assert acsch(x - I).as_leading_term(x) == I*pi/2 + # Tests concerning points lying on branch cuts + assert acsch(x).as_leading_term(x, cdir=1) == -log(x) + log(2) + assert acsch(x).as_leading_term(x, cdir=-1) == log(x) - log(2) - I*pi + assert acsch(x + I/2).as_leading_term(x, cdir=1) == -I*pi - acsch(I/2) + assert acsch(x + I/2).as_leading_term(x, cdir=-1) == acsch(I/2) + assert acsch(x - I/2).as_leading_term(x, cdir=1) == -acsch(I/2) + assert acsch(x - I/2).as_leading_term(x, cdir=-1) == acsch(I/2) + I*pi + # Tests concerning re(ndir) == 0 + assert acsch(I/2 + I*x - x**2).as_leading_term(x, cdir=1) == log(2 - sqrt(3)) - I*pi/2 + assert acsch(I/2 + I*x - x**2).as_leading_term(x, cdir=-1) == log(2 - sqrt(3)) - I*pi/2 + + +def test_acsch_series(): + x = Symbol('x') + assert acsch(x).series(x, 0, 9) == log(2) - log(x) + x**2/4 - 3*x**4/32 \ + + 5*x**6/96 - 35*x**8/1024 + O(x**9) + t4 = acsch(x).taylor_term(4, x) + assert t4 == -3*x**4/32 + assert acsch(x).taylor_term(6, x, t4, 0) == 5*x**6/96 + + +def test_acsch_nseries(): + x = Symbol('x') + # Tests concerning branch points + assert acsch(x + I)._eval_nseries(x, 4, None) == -I*pi/2 + \ + sqrt(2)*I*sqrt(x)*sqrt(-I) - 5*x**(S(3)/2)*(1 - I)/12 - \ + 43*sqrt(2)*I*x**(S(5)/2)*sqrt(-I)/160 + 177*x**(S(7)/2)*(1 - I)/896 + O(x**4) + assert acsch(x - I)._eval_nseries(x, 4, None) == I*pi/2 - \ + sqrt(2)*sqrt(I)*I*sqrt(x) - 5*x**(S(3)/2)*(1 + I)/12 + \ + 43*sqrt(2)*sqrt(I)*I*x**(S(5)/2)/160 + 177*x**(S(7)/2)*(1 + I)/896 + O(x**4) + # Tests concerning points lying on branch cuts + assert acsch(x + I/2)._eval_nseries(x, 4, None, cdir=1) == -acsch(I/2) - \ + I*pi + 4*sqrt(3)*I*x/3 - 8*sqrt(3)*x**2/9 - 16*sqrt(3)*I*x**3/9 + O(x**4) + assert acsch(x + I/2)._eval_nseries(x, 4, None, cdir=-1) == acsch(I/2) - \ + 4*sqrt(3)*I*x/3 + 8*sqrt(3)*x**2/9 + 16*sqrt(3)*I*x**3/9 + O(x**4) + assert acsch(x - I/2)._eval_nseries(x, 4, None, cdir=1) == -acsch(I/2) - \ + 4*sqrt(3)*I*x/3 - 8*sqrt(3)*x**2/9 + 16*sqrt(3)*I*x**3/9 + O(x**4) + assert acsch(x - I/2)._eval_nseries(x, 4, None, cdir=-1) == I*pi + \ + acsch(I/2) + 4*sqrt(3)*I*x/3 + 8*sqrt(3)*x**2/9 - 16*sqrt(3)*I*x**3/9 + O(x**4) + # Tests concerning re(ndir) == 0 + assert acsch(I/2 + I*x - x**2)._eval_nseries(x, 4, None) == -I*pi/2 + \ + log(2 - sqrt(3)) + x*(12 - 8*sqrt(3))/(-6 + 3*sqrt(3)) + x**2*(-96 + \ + sqrt(3)*(56 - 84*I) + 144*I)/(-63 + 36*sqrt(3)) + x**3*(2688 - 2688*I + \ + sqrt(3)*(-1552 + 1552*I))/(-873 + 504*sqrt(3)) + O(x**4) + + +def test_acsch_rewrite(): + x = Symbol('x') + assert acsch(x).rewrite(log) == log(1/x + sqrt(1/x**2 + 1)) + assert acsch(x).rewrite(asinh) == asinh(1/x) + assert acsch(x).rewrite(atanh) == (sqrt(-x**2)*(-sqrt(-(x**2 + 1)**2) + *atanh(sqrt(x**2 + 1))/(x**2 + 1) + + pi/2)/x) + + +def test_acsch_fdiff(): + x = Symbol('x') + raises(ArgumentIndexError, lambda: acsch(x).fdiff(2)) + + +def test_atanh(): + x = Symbol('x') + + # at specific points + assert atanh(0) == 0 + assert atanh(I) == I*pi/4 + assert atanh(-I) == -I*pi/4 + assert atanh(1) is oo + assert atanh(-1) is -oo + assert atanh(nan) is nan + + # at infinites + assert atanh(oo) == -I*pi/2 + assert atanh(-oo) == I*pi/2 + + assert atanh(I*oo) == I*pi/2 + assert atanh(-I*oo) == -I*pi/2 + + assert atanh(zoo) == I*AccumBounds(-pi/2, pi/2) + + # properties + assert atanh(-x) == -atanh(x) + + # reality + assert atanh(S(2)).is_real is False + assert atanh(S(-1)/5).is_real is True + assert atanh(symbols('y', extended_real=True)).is_real is None + assert atanh(S(1)).is_real is False + assert atanh(S(1)).is_extended_real is True + assert atanh(S(-1)).is_real is False + + # special values + assert atanh(I/sqrt(3)) == I*pi/6 + assert atanh(-I/sqrt(3)) == -I*pi/6 + assert atanh(I*sqrt(3)) == I*pi/3 + assert atanh(-I*sqrt(3)) == -I*pi/3 + assert atanh(I*(1 + sqrt(2))) == pi*I*Rational(3, 8) + assert atanh(I*(sqrt(2) - 1)) == pi*I/8 + assert atanh(I*(1 - sqrt(2))) == -pi*I/8 + assert atanh(-I*(1 + sqrt(2))) == pi*I*Rational(-3, 8) + assert atanh(I*sqrt(5 + 2*sqrt(5))) == I*pi*Rational(2, 5) + assert atanh(-I*sqrt(5 + 2*sqrt(5))) == I*pi*Rational(-2, 5) + assert atanh(I*(2 - sqrt(3))) == pi*I/12 + assert atanh(I*(sqrt(3) - 2)) == -pi*I/12 + assert atanh(oo) == -I*pi/2 + + # Symmetry + assert atanh(Rational(-1, 2)) == -atanh(S.Half) + + # inverse composition + assert unchanged(atanh, tanh(Symbol('v1'))) + + assert atanh(tanh(-5, evaluate=False)) == -5 + assert atanh(tanh(0, evaluate=False)) == 0 + assert atanh(tanh(7, evaluate=False)) == 7 + assert atanh(tanh(I, evaluate=False)) == I + assert atanh(tanh(-I, evaluate=False)) == -I + assert atanh(tanh(-11*I, evaluate=False)) == -11*I + 4*I*pi + assert atanh(tanh(3 + I)) == 3 + I + assert atanh(tanh(4 + 5*I)) == 4 - 2*I*pi + 5*I + assert atanh(tanh(pi/2)) == pi/2 + assert atanh(tanh(pi)) == pi + assert atanh(tanh(-3 + 7*I)) == -3 - 2*I*pi + 7*I + assert atanh(tanh(9 - I*2/3)) == 9 - I*2/3 + assert atanh(tanh(-32 - 123*I)) == -32 - 123*I + 39*I*pi + + +def test_atanh_rewrite(): + x = Symbol('x') + assert atanh(x).rewrite(log) == (log(1 + x) - log(1 - x)) / 2 + assert atanh(x).rewrite(asinh) == \ + pi*x/(2*sqrt(-x**2)) - sqrt(-x)*sqrt(1 - x**2)*sqrt(1/(x**2 - 1))*asinh(sqrt(1/(x**2 - 1)))/sqrt(x) + + +def test_atanh_leading_term(): + x = Symbol('x') + assert atanh(x).as_leading_term(x) == x + # Tests concerning branch points + assert atanh(x + 1).as_leading_term(x, cdir=1) == -log(x)/2 + log(2)/2 - I*pi/2 + assert atanh(x + 1).as_leading_term(x, cdir=-1) == -log(x)/2 + log(2)/2 + I*pi/2 + assert atanh(x - 1).as_leading_term(x, cdir=1) == log(x)/2 - log(2)/2 + assert atanh(x - 1).as_leading_term(x, cdir=-1) == log(x)/2 - log(2)/2 + assert atanh(1/x).as_leading_term(x, cdir=1) == -I*pi/2 + assert atanh(1/x).as_leading_term(x, cdir=-1) == I*pi/2 + # Tests concerning points lying on branch cuts + assert atanh(I*x + 2).as_leading_term(x, cdir=1) == atanh(2) + I*pi + assert atanh(-I*x + 2).as_leading_term(x, cdir=1) == atanh(2) + assert atanh(I*x - 2).as_leading_term(x, cdir=1) == -atanh(2) + assert atanh(-I*x - 2).as_leading_term(x, cdir=1) == -I*pi - atanh(2) + # Tests concerning im(ndir) == 0 + assert atanh(-I*x**2 + x - 2).as_leading_term(x, cdir=1) == -log(3)/2 - I*pi/2 + assert atanh(-I*x**2 + x - 2).as_leading_term(x, cdir=-1) == -log(3)/2 - I*pi/2 + + +def test_atanh_series(): + x = Symbol('x') + assert atanh(x).series(x, 0, 10) == \ + x + x**3/3 + x**5/5 + x**7/7 + x**9/9 + O(x**10) + + +def test_atanh_nseries(): + x = Symbol('x') + # Tests concerning branch points + assert atanh(x + 1)._eval_nseries(x, 4, None, cdir=1) == -I*pi/2 + log(2)/2 - \ + log(x)/2 + x/4 - x**2/16 + x**3/48 + O(x**4) + assert atanh(x + 1)._eval_nseries(x, 4, None, cdir=-1) == I*pi/2 + log(2)/2 - \ + log(x)/2 + x/4 - x**2/16 + x**3/48 + O(x**4) + assert atanh(x - 1)._eval_nseries(x, 4, None, cdir=1) == -log(2)/2 + log(x)/2 + \ + x/4 + x**2/16 + x**3/48 + O(x**4) + assert atanh(x - 1)._eval_nseries(x, 4, None, cdir=-1) == -log(2)/2 + log(x)/2 + \ + x/4 + x**2/16 + x**3/48 + O(x**4) + # Tests concerning points lying on branch cuts + assert atanh(I*x + 2)._eval_nseries(x, 4, None, cdir=1) == I*pi + atanh(2) - \ + I*x/3 - 2*x**2/9 + 13*I*x**3/81 + O(x**4) + assert atanh(I*x + 2)._eval_nseries(x, 4, None, cdir=-1) == atanh(2) - I*x/3 - \ + 2*x**2/9 + 13*I*x**3/81 + O(x**4) + assert atanh(I*x - 2)._eval_nseries(x, 4, None, cdir=1) == -atanh(2) - I*x/3 + \ + 2*x**2/9 + 13*I*x**3/81 + O(x**4) + assert atanh(I*x - 2)._eval_nseries(x, 4, None, cdir=-1) == -atanh(2) - I*pi - \ + I*x/3 + 2*x**2/9 + 13*I*x**3/81 + O(x**4) + # Tests concerning im(ndir) == 0 + assert atanh(-I*x**2 + x - 2)._eval_nseries(x, 4, None) == -I*pi/2 - log(3)/2 - x/3 + \ + x**2*(-S(1)/4 + I/2) + x**2*(S(1)/36 - I/6) + x**3*(-S(1)/6 + I/2) + x**3*(S(1)/162 - I/18) + O(x**4) + + +def test_atanh_fdiff(): + x = Symbol('x') + raises(ArgumentIndexError, lambda: atanh(x).fdiff(2)) + + +def test_acoth(): + x = Symbol('x') + + #at specific points + assert acoth(0) == I*pi/2 + assert acoth(I) == -I*pi/4 + assert acoth(-I) == I*pi/4 + assert acoth(1) is oo + assert acoth(-1) is -oo + assert acoth(nan) is nan + + # at infinites + assert acoth(oo) == 0 + assert acoth(-oo) == 0 + assert acoth(I*oo) == 0 + assert acoth(-I*oo) == 0 + assert acoth(zoo) == 0 + + #properties + assert acoth(-x) == -acoth(x) + + assert acoth(I/sqrt(3)) == -I*pi/3 + assert acoth(-I/sqrt(3)) == I*pi/3 + assert acoth(I*sqrt(3)) == -I*pi/6 + assert acoth(-I*sqrt(3)) == I*pi/6 + assert acoth(I*(1 + sqrt(2))) == -pi*I/8 + assert acoth(-I*(sqrt(2) + 1)) == pi*I/8 + assert acoth(I*(1 - sqrt(2))) == pi*I*Rational(3, 8) + assert acoth(I*(sqrt(2) - 1)) == pi*I*Rational(-3, 8) + assert acoth(I*sqrt(5 + 2*sqrt(5))) == -I*pi/10 + assert acoth(-I*sqrt(5 + 2*sqrt(5))) == I*pi/10 + assert acoth(I*(2 + sqrt(3))) == -pi*I/12 + assert acoth(-I*(2 + sqrt(3))) == pi*I/12 + assert acoth(I*(2 - sqrt(3))) == pi*I*Rational(-5, 12) + assert acoth(I*(sqrt(3) - 2)) == pi*I*Rational(5, 12) + + # reality + assert acoth(S(2)).is_real is True + assert acoth(S(2)).is_finite is True + assert acoth(S(2)).is_extended_real is True + assert acoth(S(-2)).is_real is True + assert acoth(S(1)).is_real is False + assert acoth(S(1)).is_extended_real is True + assert acoth(S(-1)).is_real is False + assert acoth(symbols('y', real=True)).is_real is None + + # Symmetry + assert acoth(Rational(-1, 2)) == -acoth(S.Half) + + +def test_acoth_rewrite(): + x = Symbol('x') + assert acoth(x).rewrite(log) == (log(1 + 1/x) - log(1 - 1/x)) / 2 + assert acoth(x).rewrite(atanh) == atanh(1/x) + assert acoth(x).rewrite(asinh) == \ + x*sqrt(x**(-2))*asinh(sqrt(1/(x**2 - 1))) + I*pi*(sqrt((x - 1)/x)*sqrt(x/(x - 1)) - sqrt(x/(x + 1))*sqrt(1 + 1/x))/2 + + +def test_acoth_leading_term(): + x = Symbol('x') + # Tests concerning branch points + assert acoth(x + 1).as_leading_term(x, cdir=1) == -log(x)/2 + log(2)/2 + assert acoth(x + 1).as_leading_term(x, cdir=-1) == -log(x)/2 + log(2)/2 + assert acoth(x - 1).as_leading_term(x, cdir=1) == log(x)/2 - log(2)/2 + I*pi/2 + assert acoth(x - 1).as_leading_term(x, cdir=-1) == log(x)/2 - log(2)/2 - I*pi/2 + # Tests concerning points lying on branch cuts + assert acoth(x).as_leading_term(x, cdir=-1) == I*pi/2 + assert acoth(x).as_leading_term(x, cdir=1) == -I*pi/2 + assert acoth(I*x + 1/2).as_leading_term(x, cdir=1) == acoth(1/2) + assert acoth(-I*x + 1/2).as_leading_term(x, cdir=1) == acoth(1/2) + I*pi + assert acoth(I*x - 1/2).as_leading_term(x, cdir=1) == -I*pi - acoth(1/2) + assert acoth(-I*x - 1/2).as_leading_term(x, cdir=1) == -acoth(1/2) + # Tests concerning im(ndir) == 0 + assert acoth(-I*x**2 - x - S(1)/2).as_leading_term(x, cdir=1) == -log(3)/2 + I*pi/2 + assert acoth(-I*x**2 - x - S(1)/2).as_leading_term(x, cdir=-1) == -log(3)/2 + I*pi/2 + + +def test_acoth_series(): + x = Symbol('x') + assert acoth(x).series(x, 0, 10) == \ + -I*pi/2 + x + x**3/3 + x**5/5 + x**7/7 + x**9/9 + O(x**10) + + +def test_acoth_nseries(): + x = Symbol('x') + # Tests concerning branch points + assert acoth(x + 1)._eval_nseries(x, 4, None) == log(2)/2 - log(x)/2 + x/4 - \ + x**2/16 + x**3/48 + O(x**4) + assert acoth(x - 1)._eval_nseries(x, 4, None, cdir=1) == I*pi/2 - log(2)/2 + \ + log(x)/2 + x/4 + x**2/16 + x**3/48 + O(x**4) + assert acoth(x - 1)._eval_nseries(x, 4, None, cdir=-1) == -I*pi/2 - log(2)/2 + \ + log(x)/2 + x/4 + x**2/16 + x**3/48 + O(x**4) + # Tests concerning points lying on branch cuts + assert acoth(I*x + S(1)/2)._eval_nseries(x, 4, None, cdir=1) == acoth(S(1)/2) + \ + 4*I*x/3 - 8*x**2/9 - 112*I*x**3/81 + O(x**4) + assert acoth(I*x + S(1)/2)._eval_nseries(x, 4, None, cdir=-1) == I*pi + \ + acoth(S(1)/2) + 4*I*x/3 - 8*x**2/9 - 112*I*x**3/81 + O(x**4) + assert acoth(I*x - S(1)/2)._eval_nseries(x, 4, None, cdir=1) == -acoth(S(1)/2) - \ + I*pi + 4*I*x/3 + 8*x**2/9 - 112*I*x**3/81 + O(x**4) + assert acoth(I*x - S(1)/2)._eval_nseries(x, 4, None, cdir=-1) == -acoth(S(1)/2) + \ + 4*I*x/3 + 8*x**2/9 - 112*I*x**3/81 + O(x**4) + # Tests concerning im(ndir) == 0 + assert acoth(-I*x**2 - x - S(1)/2)._eval_nseries(x, 4, None) == I*pi/2 - log(3)/2 - \ + 4*x/3 + x**2*(-S(8)/9 + 2*I/3) - 2*I*x**2 + x**3*(S(104)/81 - 16*I/9) - 8*x**3/3 + O(x**4) + + +def test_acoth_fdiff(): + x = Symbol('x') + raises(ArgumentIndexError, lambda: acoth(x).fdiff(2)) + + +def test_inverses(): + x = Symbol('x') + assert sinh(x).inverse() == asinh + raises(AttributeError, lambda: cosh(x).inverse()) + assert tanh(x).inverse() == atanh + assert coth(x).inverse() == acoth + assert asinh(x).inverse() == sinh + assert acosh(x).inverse() == cosh + assert atanh(x).inverse() == tanh + assert acoth(x).inverse() == coth + assert asech(x).inverse() == sech + assert acsch(x).inverse() == csch + + +def test_leading_term(): + x = Symbol('x') + assert cosh(x).as_leading_term(x) == 1 + assert coth(x).as_leading_term(x) == 1/x + for func in [sinh, tanh]: + assert func(x).as_leading_term(x) == x + for func in [sinh, cosh, tanh, coth]: + for ar in (1/x, S.Half): + eq = func(ar) + assert eq.as_leading_term(x) == eq + for func in [csch, sech]: + eq = func(S.Half) + assert eq.as_leading_term(x) == eq + + +def test_complex(): + a, b = symbols('a,b', real=True) + z = a + b*I + for func in [sinh, cosh, tanh, coth, sech, csch]: + assert func(z).conjugate() == func(a - b*I) + for deep in [True, False]: + assert sinh(z).expand( + complex=True, deep=deep) == sinh(a)*cos(b) + I*cosh(a)*sin(b) + assert cosh(z).expand( + complex=True, deep=deep) == cosh(a)*cos(b) + I*sinh(a)*sin(b) + assert tanh(z).expand(complex=True, deep=deep) == sinh(a)*cosh( + a)/(cos(b)**2 + sinh(a)**2) + I*sin(b)*cos(b)/(cos(b)**2 + sinh(a)**2) + assert coth(z).expand(complex=True, deep=deep) == sinh(a)*cosh( + a)/(sin(b)**2 + sinh(a)**2) - I*sin(b)*cos(b)/(sin(b)**2 + sinh(a)**2) + assert csch(z).expand(complex=True, deep=deep) == cos(b) * sinh(a) / (sin(b)**2\ + *cosh(a)**2 + cos(b)**2 * sinh(a)**2) - I*sin(b) * cosh(a) / (sin(b)**2\ + *cosh(a)**2 + cos(b)**2 * sinh(a)**2) + assert sech(z).expand(complex=True, deep=deep) == cos(b) * cosh(a) / (sin(b)**2\ + *sinh(a)**2 + cos(b)**2 * cosh(a)**2) - I*sin(b) * sinh(a) / (sin(b)**2\ + *sinh(a)**2 + cos(b)**2 * cosh(a)**2) + + +def test_complex_2899(): + a, b = symbols('a,b', real=True) + for deep in [True, False]: + for func in [sinh, cosh, tanh, coth]: + assert func(a).expand(complex=True, deep=deep) == func(a) + + +def test_simplifications(): + x = Symbol('x') + assert sinh(asinh(x)) == x + assert sinh(acosh(x)) == sqrt(x - 1) * sqrt(x + 1) + assert sinh(atanh(x)) == x/sqrt(1 - x**2) + assert sinh(acoth(x)) == 1/(sqrt(x - 1) * sqrt(x + 1)) + + assert cosh(asinh(x)) == sqrt(1 + x**2) + assert cosh(acosh(x)) == x + assert cosh(atanh(x)) == 1/sqrt(1 - x**2) + assert cosh(acoth(x)) == x/(sqrt(x - 1) * sqrt(x + 1)) + + assert tanh(asinh(x)) == x/sqrt(1 + x**2) + assert tanh(acosh(x)) == sqrt(x - 1) * sqrt(x + 1) / x + assert tanh(atanh(x)) == x + assert tanh(acoth(x)) == 1/x + + assert coth(asinh(x)) == sqrt(1 + x**2)/x + assert coth(acosh(x)) == x/(sqrt(x - 1) * sqrt(x + 1)) + assert coth(atanh(x)) == 1/x + assert coth(acoth(x)) == x + + assert csch(asinh(x)) == 1/x + assert csch(acosh(x)) == 1/(sqrt(x - 1) * sqrt(x + 1)) + assert csch(atanh(x)) == sqrt(1 - x**2)/x + assert csch(acoth(x)) == sqrt(x - 1) * sqrt(x + 1) + + assert sech(asinh(x)) == 1/sqrt(1 + x**2) + assert sech(acosh(x)) == 1/x + assert sech(atanh(x)) == sqrt(1 - x**2) + assert sech(acoth(x)) == sqrt(x - 1) * sqrt(x + 1)/x + + +def test_issue_4136(): + assert cosh(asinh(Integer(3)/2)) == sqrt(Integer(13)/4) + + +def test_sinh_rewrite(): + x = Symbol('x') + assert sinh(x).rewrite(exp) == (exp(x) - exp(-x))/2 \ + == sinh(x).rewrite('tractable') + assert sinh(x).rewrite(cosh) == -I*cosh(x + I*pi/2) + tanh_half = tanh(S.Half*x) + assert sinh(x).rewrite(tanh) == 2*tanh_half/(1 - tanh_half**2) + coth_half = coth(S.Half*x) + assert sinh(x).rewrite(coth) == 2*coth_half/(coth_half**2 - 1) + + +def test_cosh_rewrite(): + x = Symbol('x') + assert cosh(x).rewrite(exp) == (exp(x) + exp(-x))/2 \ + == cosh(x).rewrite('tractable') + assert cosh(x).rewrite(sinh) == -I*sinh(x + I*pi/2, evaluate=False) + tanh_half = tanh(S.Half*x)**2 + assert cosh(x).rewrite(tanh) == (1 + tanh_half)/(1 - tanh_half) + coth_half = coth(S.Half*x)**2 + assert cosh(x).rewrite(coth) == (coth_half + 1)/(coth_half - 1) + + +def test_tanh_rewrite(): + x = Symbol('x') + assert tanh(x).rewrite(exp) == (exp(x) - exp(-x))/(exp(x) + exp(-x)) \ + == tanh(x).rewrite('tractable') + assert tanh(x).rewrite(sinh) == I*sinh(x)/sinh(I*pi/2 - x, evaluate=False) + assert tanh(x).rewrite(cosh) == I*cosh(I*pi/2 - x, evaluate=False)/cosh(x) + assert tanh(x).rewrite(coth) == 1/coth(x) + + +def test_coth_rewrite(): + x = Symbol('x') + assert coth(x).rewrite(exp) == (exp(x) + exp(-x))/(exp(x) - exp(-x)) \ + == coth(x).rewrite('tractable') + assert coth(x).rewrite(sinh) == -I*sinh(I*pi/2 - x, evaluate=False)/sinh(x) + assert coth(x).rewrite(cosh) == -I*cosh(x)/cosh(I*pi/2 - x, evaluate=False) + assert coth(x).rewrite(tanh) == 1/tanh(x) + + +def test_csch_rewrite(): + x = Symbol('x') + assert csch(x).rewrite(exp) == 1 / (exp(x)/2 - exp(-x)/2) \ + == csch(x).rewrite('tractable') + assert csch(x).rewrite(cosh) == I/cosh(x + I*pi/2, evaluate=False) + tanh_half = tanh(S.Half*x) + assert csch(x).rewrite(tanh) == (1 - tanh_half**2)/(2*tanh_half) + coth_half = coth(S.Half*x) + assert csch(x).rewrite(coth) == (coth_half**2 - 1)/(2*coth_half) + + +def test_sech_rewrite(): + x = Symbol('x') + assert sech(x).rewrite(exp) == 1 / (exp(x)/2 + exp(-x)/2) \ + == sech(x).rewrite('tractable') + assert sech(x).rewrite(sinh) == I/sinh(x + I*pi/2, evaluate=False) + tanh_half = tanh(S.Half*x)**2 + assert sech(x).rewrite(tanh) == (1 - tanh_half)/(1 + tanh_half) + coth_half = coth(S.Half*x)**2 + assert sech(x).rewrite(coth) == (coth_half - 1)/(coth_half + 1) + + +def test_derivs(): + x = Symbol('x') + assert coth(x).diff(x) == -sinh(x)**(-2) + assert sinh(x).diff(x) == cosh(x) + assert cosh(x).diff(x) == sinh(x) + assert tanh(x).diff(x) == -tanh(x)**2 + 1 + assert csch(x).diff(x) == -coth(x)*csch(x) + assert sech(x).diff(x) == -tanh(x)*sech(x) + assert acoth(x).diff(x) == 1/(-x**2 + 1) + assert asinh(x).diff(x) == 1/sqrt(x**2 + 1) + assert acosh(x).diff(x) == 1/(sqrt(x - 1)*sqrt(x + 1)) + assert acosh(x).diff(x) == acosh(x).rewrite(log).diff(x).together() + assert atanh(x).diff(x) == 1/(-x**2 + 1) + assert asech(x).diff(x) == -1/(x*sqrt(1 - x**2)) + assert acsch(x).diff(x) == -1/(x**2*sqrt(1 + x**(-2))) + + +def test_sinh_expansion(): + x, y = symbols('x,y') + assert sinh(x+y).expand(trig=True) == sinh(x)*cosh(y) + cosh(x)*sinh(y) + assert sinh(2*x).expand(trig=True) == 2*sinh(x)*cosh(x) + assert sinh(3*x).expand(trig=True).expand() == \ + sinh(x)**3 + 3*sinh(x)*cosh(x)**2 + + +def test_cosh_expansion(): + x, y = symbols('x,y') + assert cosh(x+y).expand(trig=True) == cosh(x)*cosh(y) + sinh(x)*sinh(y) + assert cosh(2*x).expand(trig=True) == cosh(x)**2 + sinh(x)**2 + assert cosh(3*x).expand(trig=True).expand() == \ + 3*sinh(x)**2*cosh(x) + cosh(x)**3 + +def test_cosh_positive(): + # See issue 11721 + # cosh(x) is positive for real values of x + k = symbols('k', real=True) + n = symbols('n', integer=True) + + assert cosh(k, evaluate=False).is_positive is True + assert cosh(k + 2*n*pi*I, evaluate=False).is_positive is True + assert cosh(I*pi/4, evaluate=False).is_positive is True + assert cosh(3*I*pi/4, evaluate=False).is_positive is False + +def test_cosh_nonnegative(): + k = symbols('k', real=True) + n = symbols('n', integer=True) + + assert cosh(k, evaluate=False).is_nonnegative is True + assert cosh(k + 2*n*pi*I, evaluate=False).is_nonnegative is True + assert cosh(I*pi/4, evaluate=False).is_nonnegative is True + assert cosh(3*I*pi/4, evaluate=False).is_nonnegative is False + assert cosh(S.Zero, evaluate=False).is_nonnegative is True + +def test_real_assumptions(): + z = Symbol('z', real=False) + assert sinh(z).is_real is None + assert cosh(z).is_real is None + assert tanh(z).is_real is None + assert sech(z).is_real is None + assert csch(z).is_real is None + assert coth(z).is_real is None + +def test_sign_assumptions(): + p = Symbol('p', positive=True) + n = Symbol('n', negative=True) + assert sinh(n).is_negative is True + assert sinh(p).is_positive is True + assert cosh(n).is_positive is True + assert cosh(p).is_positive is True + assert tanh(n).is_negative is True + assert tanh(p).is_positive is True + assert csch(n).is_negative is True + assert csch(p).is_positive is True + assert sech(n).is_positive is True + assert sech(p).is_positive is True + assert coth(n).is_negative is True + assert coth(p).is_positive is True + + +def test_issue_25847(): + x = Symbol('x') + + #atanh + assert atanh(sin(x)/x).as_leading_term(x) == atanh(sin(x)/x) + raises(PoleError, lambda: atanh(exp(1/x)).as_leading_term(x)) + + #asinh + assert asinh(sin(x)/x).as_leading_term(x) == log(1 + sqrt(2)) + raises(PoleError, lambda: asinh(exp(1/x)).as_leading_term(x)) + + #acosh + assert acosh(sin(x)/x).as_leading_term(x) == 0 + raises(PoleError, lambda: acosh(exp(1/x)).as_leading_term(x)) + + #acoth + assert acoth(sin(x)/x).as_leading_term(x) == acoth(sin(x)/x) + raises(PoleError, lambda: acoth(exp(1/x)).as_leading_term(x)) + + #asech + assert asech(sinh(x)/x).as_leading_term(x) == 0 + raises(PoleError, lambda: asech(exp(1/x)).as_leading_term(x)) + + #acsch + assert acsch(sin(x)/x).as_leading_term(x) == log(1 + sqrt(2)) + raises(PoleError, lambda: acsch(exp(1/x)).as_leading_term(x)) + + +def test_issue_25175(): + x = Symbol('x') + g1 = 2*acosh(1 + 2*x/3) - acosh(S(5)/3 - S(8)/3/(x + 4)) + g2 = 2*log(sqrt((x + 4)/3)*(sqrt(x + 3)+sqrt(x))**2/(2*sqrt(x + 3) + sqrt(x))) + assert (g1 - g2).series(x) == O(x**6) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_integers.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_integers.py new file mode 100644 index 0000000000000000000000000000000000000000..a48ad2ac24c4a857d57b2f24e3308ac90078a9b1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_integers.py @@ -0,0 +1,688 @@ +from sympy.calculus.accumulationbounds import AccumBounds +from sympy.core.numbers import (E, Float, I, Rational, Integer, nan, oo, pi, zoo) +from sympy.core.relational import (Eq, Ge, Gt, Le, Lt, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.integers import (ceiling, floor, frac) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin, cos, tan, asin +from sympy.polys.rootoftools import RootOf, CRootOf +from sympy import Integers +from sympy.sets.sets import Interval +from sympy.sets.fancysets import ImageSet +from sympy.core.function import Lambda + +from sympy.core.expr import unchanged +from sympy.testing.pytest import XFAIL, raises + +x = Symbol('x') +i = Symbol('i', imaginary=True) +y = Symbol('y', real=True) +k, n = symbols('k,n', integer=True) +b = Symbol('b', real=True, noninteger=True) +m = Symbol('m', positive=True) + + +def test_floor(): + + assert floor(nan) is nan + + assert floor(oo) is oo + assert floor(-oo) is -oo + assert floor(zoo) is zoo + + assert floor(0) == 0 + + assert floor(1) == 1 + assert floor(-1) == -1 + + assert floor(I*log(asin(5)/abs(asin(5)))) == 0 + assert floor(-I*log(asin(7)/abs(asin(7)))) == -2 + + assert floor(E) == 2 + assert floor(-E) == -3 + + assert floor(2*E) == 5 + assert floor(-2*E) == -6 + + assert floor(pi) == 3 + assert floor(-pi) == -4 + + assert floor(S.Half) == 0 + assert floor(Rational(-1, 2)) == -1 + + assert floor(Rational(7, 3)) == 2 + assert floor(Rational(-7, 3)) == -3 + assert floor(-Rational(7, 3)) == -3 + + assert floor(Float(17.0)) == 17 + assert floor(-Float(17.0)) == -17 + + assert floor(Float(7.69)) == 7 + assert floor(-Float(7.69)) == -8 + + assert floor(1/(m+1)) == S.Zero + assert floor((m+2)/(m+1)) == S.One + assert floor(-1/(m+1)) == S.NegativeOne + assert floor((m+2)/(-m-1)) == Integer(-2) + + assert floor(I) == I + assert floor(-I) == -I + e = floor(i) + assert e.func is floor and e.args[0] == i + + assert floor(oo*I) == oo*I + assert floor(-oo*I) == -oo*I + assert floor(exp(I*pi/4)*oo) == exp(I*pi/4)*oo + + assert floor(2*I) == 2*I + assert floor(-2*I) == -2*I + + assert floor(I/2) == 0 + assert floor(-I/2) == -I + + assert floor(E + 17) == 19 + assert floor(pi + 2) == 5 + + assert floor(E + pi) == 5 + assert floor(I + pi) == 3 + I + + assert floor(floor(pi)) == 3 + assert floor(floor(y)) == floor(y) + assert floor(floor(x)) == floor(x) + + assert unchanged(floor, x) + assert unchanged(floor, 2*x) + assert unchanged(floor, k*x) + + assert floor(k) == k + assert floor(2*k) == 2*k + assert floor(k*n) == k*n + + assert unchanged(floor, k/2) + + assert unchanged(floor, x + y) + + assert floor(x + 3) == floor(x) + 3 + assert floor(x + k) == floor(x) + k + + assert floor(y + 3) == floor(y) + 3 + assert floor(y + k) == floor(y) + k + + assert floor(3 + I*y + pi) == 6 + floor(y)*I + + assert floor(k + n) == k + n + + assert unchanged(floor, x*I) + assert floor(k*I) == k*I + + assert floor(Rational(23, 10) - E*I) == 2 - 3*I + + assert floor(sin(1)) == 0 + assert floor(sin(-1)) == -1 + + assert floor(exp(2)) == 7 + + assert floor(log(8)/log(2)) != 2 + assert int(floor(log(8)/log(2)).evalf(chop=True)) == 3 + + assert floor(factorial(50)/exp(1)) == \ + 11188719610782480504630258070757734324011354208865721592720336800 + + assert (floor(y) < y).is_Relational + assert (floor(y) <= y) == True + assert (floor(y) > y) == False + assert (floor(y) >= y).is_Relational + assert (floor(x) <= x).is_Relational # x could be non-real + assert (floor(x) > x).is_Relational + assert (floor(x) <= y).is_Relational # arg is not same as rhs + assert (floor(x) > y).is_Relational + assert (floor(y) <= oo) == True + assert (floor(y) < oo) == True + assert (floor(y) >= -oo) == True + assert (floor(y) > -oo) == True + assert (floor(b) < b) == True + assert (floor(b) <= b) == True + assert (floor(b) > b) == False + assert (floor(b) >= b) == False + + assert floor(y).rewrite(frac) == y - frac(y) + assert floor(y).rewrite(ceiling) == -ceiling(-y) + assert floor(y).rewrite(frac).subs(y, -pi) == floor(-pi) + assert floor(y).rewrite(frac).subs(y, E) == floor(E) + assert floor(y).rewrite(ceiling).subs(y, E) == -ceiling(-E) + assert floor(y).rewrite(ceiling).subs(y, -pi) == -ceiling(pi) + + assert Eq(floor(y), y - frac(y)) + assert Eq(floor(y), -ceiling(-y)) + + neg = Symbol('neg', negative=True) + nn = Symbol('nn', nonnegative=True) + pos = Symbol('pos', positive=True) + np = Symbol('np', nonpositive=True) + + assert (floor(neg) < 0) == True + assert (floor(neg) <= 0) == True + assert (floor(neg) > 0) == False + assert (floor(neg) >= 0) == False + assert (floor(neg) <= -1) == True + assert (floor(neg) >= -3) == (neg >= -3) + assert (floor(neg) < 5) == (neg < 5) + + assert (floor(nn) < 0) == False + assert (floor(nn) >= 0) == True + + assert (floor(pos) < 0) == False + assert (floor(pos) <= 0) == (pos < 1) + assert (floor(pos) > 0) == (pos >= 1) + assert (floor(pos) >= 0) == True + assert (floor(pos) >= 3) == (pos >= 3) + + assert (floor(np) <= 0) == True + assert (floor(np) > 0) == False + + assert floor(neg).is_negative == True + assert floor(neg).is_nonnegative == False + assert floor(nn).is_negative == False + assert floor(nn).is_nonnegative == True + assert floor(pos).is_negative == False + assert floor(pos).is_nonnegative == True + assert floor(np).is_negative is None + assert floor(np).is_nonnegative is None + + assert (floor(7, evaluate=False) >= 7) == True + assert (floor(7, evaluate=False) > 7) == False + assert (floor(7, evaluate=False) <= 7) == True + assert (floor(7, evaluate=False) < 7) == False + + assert (floor(7, evaluate=False) >= 6) == True + assert (floor(7, evaluate=False) > 6) == True + assert (floor(7, evaluate=False) <= 6) == False + assert (floor(7, evaluate=False) < 6) == False + + assert (floor(7, evaluate=False) >= 8) == False + assert (floor(7, evaluate=False) > 8) == False + assert (floor(7, evaluate=False) <= 8) == True + assert (floor(7, evaluate=False) < 8) == True + + assert (floor(x) <= 5.5) == Le(floor(x), 5.5, evaluate=False) + assert (floor(x) >= -3.2) == Ge(floor(x), -3.2, evaluate=False) + assert (floor(x) < 2.9) == Lt(floor(x), 2.9, evaluate=False) + assert (floor(x) > -1.7) == Gt(floor(x), -1.7, evaluate=False) + + assert (floor(y) <= 5.5) == (y < 6) + assert (floor(y) >= -3.2) == (y >= -3) + assert (floor(y) < 2.9) == (y < 3) + assert (floor(y) > -1.7) == (y >= -1) + + assert (floor(y) <= n) == (y < n + 1) + assert (floor(y) >= n) == (y >= n) + assert (floor(y) < n) == (y < n) + assert (floor(y) > n) == (y >= n + 1) + + assert floor(RootOf(x**3 - 27*x, 2)) == 5 + + +def test_ceiling(): + + assert ceiling(nan) is nan + + assert ceiling(oo) is oo + assert ceiling(-oo) is -oo + assert ceiling(zoo) is zoo + + assert ceiling(0) == 0 + + assert ceiling(1) == 1 + assert ceiling(-1) == -1 + + assert ceiling(I*log(asin(5)/abs(asin(5)))) == 1 + assert ceiling(-I*log(asin(7)/abs(asin(7)))) == -1 + + assert ceiling(E) == 3 + assert ceiling(-E) == -2 + + assert ceiling(2*E) == 6 + assert ceiling(-2*E) == -5 + + assert ceiling(pi) == 4 + assert ceiling(-pi) == -3 + + assert ceiling(S.Half) == 1 + assert ceiling(Rational(-1, 2)) == 0 + + assert ceiling(Rational(7, 3)) == 3 + assert ceiling(-Rational(7, 3)) == -2 + + assert ceiling(Float(17.0)) == 17 + assert ceiling(-Float(17.0)) == -17 + + assert ceiling(Float(7.69)) == 8 + assert ceiling(-Float(7.69)) == -7 + + assert ceiling(1/(m+1)) == S.One + assert ceiling((m+2)/(m+1)) == Integer(2) + assert ceiling(-1/(m+1)) == S.Zero + assert ceiling((m+2)/(-m-1)) == S.NegativeOne + + assert ceiling(I) == I + assert ceiling(-I) == -I + e = ceiling(i) + assert e.func is ceiling and e.args[0] == i + + assert ceiling(oo*I) == oo*I + assert ceiling(-oo*I) == -oo*I + assert ceiling(exp(I*pi/4)*oo) == exp(I*pi/4)*oo + + assert ceiling(2*I) == 2*I + assert ceiling(-2*I) == -2*I + + assert ceiling(I/2) == I + assert ceiling(-I/2) == 0 + + assert ceiling(E + 17) == 20 + assert ceiling(pi + 2) == 6 + + assert ceiling(E + pi) == 6 + assert ceiling(I + pi) == I + 4 + + assert ceiling(ceiling(pi)) == 4 + assert ceiling(ceiling(y)) == ceiling(y) + assert ceiling(ceiling(x)) == ceiling(x) + + assert unchanged(ceiling, x) + assert unchanged(ceiling, 2*x) + assert unchanged(ceiling, k*x) + + assert ceiling(k) == k + assert ceiling(2*k) == 2*k + assert ceiling(k*n) == k*n + + assert unchanged(ceiling, k/2) + + assert unchanged(ceiling, x + y) + + assert ceiling(x + 3) == ceiling(x) + 3 + assert ceiling(x + 3.0) == ceiling(x) + 3 + assert ceiling(x + 3.0*I) == ceiling(x) + 3*I + assert ceiling(x + k) == ceiling(x) + k + + assert ceiling(y + 3) == ceiling(y) + 3 + assert ceiling(y + k) == ceiling(y) + k + + assert ceiling(3 + pi + y*I) == 7 + ceiling(y)*I + + assert ceiling(k + n) == k + n + + assert unchanged(ceiling, x*I) + assert ceiling(k*I) == k*I + + assert ceiling(Rational(23, 10) - E*I) == 3 - 2*I + + assert ceiling(sin(1)) == 1 + assert ceiling(sin(-1)) == 0 + + assert ceiling(exp(2)) == 8 + + assert ceiling(-log(8)/log(2)) != -2 + assert int(ceiling(-log(8)/log(2)).evalf(chop=True)) == -3 + + assert ceiling(factorial(50)/exp(1)) == \ + 11188719610782480504630258070757734324011354208865721592720336801 + + assert (ceiling(y) >= y) == True + assert (ceiling(y) > y).is_Relational + assert (ceiling(y) < y) == False + assert (ceiling(y) <= y).is_Relational + assert (ceiling(x) >= x).is_Relational # x could be non-real + assert (ceiling(x) < x).is_Relational + assert (ceiling(x) >= y).is_Relational # arg is not same as rhs + assert (ceiling(x) < y).is_Relational + assert (ceiling(y) >= -oo) == True + assert (ceiling(y) > -oo) == True + assert (ceiling(y) <= oo) == True + assert (ceiling(y) < oo) == True + assert (ceiling(b) < b) == False + assert (ceiling(b) <= b) == False + assert (ceiling(b) > b) == True + assert (ceiling(b) >= b) == True + + assert ceiling(y).rewrite(floor) == -floor(-y) + assert ceiling(y).rewrite(frac) == y + frac(-y) + assert ceiling(y).rewrite(floor).subs(y, -pi) == -floor(pi) + assert ceiling(y).rewrite(floor).subs(y, E) == -floor(-E) + assert ceiling(y).rewrite(frac).subs(y, pi) == ceiling(pi) + assert ceiling(y).rewrite(frac).subs(y, -E) == ceiling(-E) + + assert Eq(ceiling(y), y + frac(-y)) + assert Eq(ceiling(y), -floor(-y)) + + neg = Symbol('neg', negative=True) + nn = Symbol('nn', nonnegative=True) + pos = Symbol('pos', positive=True) + np = Symbol('np', nonpositive=True) + + assert (ceiling(neg) <= 0) == True + assert (ceiling(neg) < 0) == (neg <= -1) + assert (ceiling(neg) > 0) == False + assert (ceiling(neg) >= 0) == (neg > -1) + assert (ceiling(neg) > -3) == (neg > -3) + assert (ceiling(neg) <= 10) == (neg <= 10) + + assert (ceiling(nn) < 0) == False + assert (ceiling(nn) >= 0) == True + + assert (ceiling(pos) < 0) == False + assert (ceiling(pos) <= 0) == False + assert (ceiling(pos) > 0) == True + assert (ceiling(pos) >= 0) == True + assert (ceiling(pos) >= 1) == True + assert (ceiling(pos) > 5) == (pos > 5) + + assert (ceiling(np) <= 0) == True + assert (ceiling(np) > 0) == False + + assert ceiling(neg).is_positive == False + assert ceiling(neg).is_nonpositive == True + assert ceiling(nn).is_positive is None + assert ceiling(nn).is_nonpositive is None + assert ceiling(pos).is_positive == True + assert ceiling(pos).is_nonpositive == False + assert ceiling(np).is_positive == False + assert ceiling(np).is_nonpositive == True + + assert (ceiling(7, evaluate=False) >= 7) == True + assert (ceiling(7, evaluate=False) > 7) == False + assert (ceiling(7, evaluate=False) <= 7) == True + assert (ceiling(7, evaluate=False) < 7) == False + + assert (ceiling(7, evaluate=False) >= 6) == True + assert (ceiling(7, evaluate=False) > 6) == True + assert (ceiling(7, evaluate=False) <= 6) == False + assert (ceiling(7, evaluate=False) < 6) == False + + assert (ceiling(7, evaluate=False) >= 8) == False + assert (ceiling(7, evaluate=False) > 8) == False + assert (ceiling(7, evaluate=False) <= 8) == True + assert (ceiling(7, evaluate=False) < 8) == True + + assert (ceiling(x) <= 5.5) == Le(ceiling(x), 5.5, evaluate=False) + assert (ceiling(x) >= -3.2) == Ge(ceiling(x), -3.2, evaluate=False) + assert (ceiling(x) < 2.9) == Lt(ceiling(x), 2.9, evaluate=False) + assert (ceiling(x) > -1.7) == Gt(ceiling(x), -1.7, evaluate=False) + + assert (ceiling(y) <= 5.5) == (y <= 5) + assert (ceiling(y) >= -3.2) == (y > -4) + assert (ceiling(y) < 2.9) == (y <= 2) + assert (ceiling(y) > -1.7) == (y > -2) + + assert (ceiling(y) <= n) == (y <= n) + assert (ceiling(y) >= n) == (y > n - 1) + assert (ceiling(y) < n) == (y <= n - 1) + assert (ceiling(y) > n) == (y > n) + + assert ceiling(RootOf(x**3 - 27*x, 2)) == 6 + s = ImageSet(Lambda(n, n + (CRootOf(x**5 - x**2 + 1, 0))), Integers) + f = CRootOf(x**5 - x**2 + 1, 0) + s = ImageSet(Lambda(n, n + f), Integers) + assert s.intersect(Interval(-10, 10)) == {i + f for i in range(-9, 11)} + + +def test_frac(): + assert isinstance(frac(x), frac) + assert frac(oo) == AccumBounds(0, 1) + assert frac(-oo) == AccumBounds(0, 1) + assert frac(zoo) is nan + + assert frac(n) == 0 + assert frac(nan) is nan + assert frac(Rational(4, 3)) == Rational(1, 3) + assert frac(-Rational(4, 3)) == Rational(2, 3) + assert frac(Rational(-4, 3)) == Rational(2, 3) + + r = Symbol('r', real=True) + assert frac(I*r) == I*frac(r) + assert frac(1 + I*r) == I*frac(r) + assert frac(0.5 + I*r) == 0.5 + I*frac(r) + assert frac(n + I*r) == I*frac(r) + assert frac(n + I*k) == 0 + assert unchanged(frac, x + I*x) + assert frac(x + I*n) == frac(x) + + assert frac(x).rewrite(floor) == x - floor(x) + assert frac(x).rewrite(ceiling) == x + ceiling(-x) + assert frac(y).rewrite(floor).subs(y, pi) == frac(pi) + assert frac(y).rewrite(floor).subs(y, -E) == frac(-E) + assert frac(y).rewrite(ceiling).subs(y, -pi) == frac(-pi) + assert frac(y).rewrite(ceiling).subs(y, E) == frac(E) + + assert Eq(frac(y), y - floor(y)) + assert Eq(frac(y), y + ceiling(-y)) + + r = Symbol('r', real=True) + p_i = Symbol('p_i', integer=True, positive=True) + n_i = Symbol('p_i', integer=True, negative=True) + np_i = Symbol('np_i', integer=True, nonpositive=True) + nn_i = Symbol('nn_i', integer=True, nonnegative=True) + p_r = Symbol('p_r', positive=True) + n_r = Symbol('n_r', negative=True) + np_r = Symbol('np_r', real=True, nonpositive=True) + nn_r = Symbol('nn_r', real=True, nonnegative=True) + + # Real frac argument, integer rhs + assert frac(r) <= p_i + assert not frac(r) <= n_i + assert (frac(r) <= np_i).has(Le) + assert (frac(r) <= nn_i).has(Le) + assert frac(r) < p_i + assert not frac(r) < n_i + assert not frac(r) < np_i + assert (frac(r) < nn_i).has(Lt) + assert not frac(r) >= p_i + assert frac(r) >= n_i + assert frac(r) >= np_i + assert (frac(r) >= nn_i).has(Ge) + assert not frac(r) > p_i + assert frac(r) > n_i + assert (frac(r) > np_i).has(Gt) + assert (frac(r) > nn_i).has(Gt) + + assert not Eq(frac(r), p_i) + assert not Eq(frac(r), n_i) + assert Eq(frac(r), np_i).has(Eq) + assert Eq(frac(r), nn_i).has(Eq) + + assert Ne(frac(r), p_i) + assert Ne(frac(r), n_i) + assert Ne(frac(r), np_i).has(Ne) + assert Ne(frac(r), nn_i).has(Ne) + + + # Real frac argument, real rhs + assert (frac(r) <= p_r).has(Le) + assert not frac(r) <= n_r + assert (frac(r) <= np_r).has(Le) + assert (frac(r) <= nn_r).has(Le) + assert (frac(r) < p_r).has(Lt) + assert not frac(r) < n_r + assert not frac(r) < np_r + assert (frac(r) < nn_r).has(Lt) + assert (frac(r) >= p_r).has(Ge) + assert frac(r) >= n_r + assert frac(r) >= np_r + assert (frac(r) >= nn_r).has(Ge) + assert (frac(r) > p_r).has(Gt) + assert frac(r) > n_r + assert (frac(r) > np_r).has(Gt) + assert (frac(r) > nn_r).has(Gt) + + assert not Eq(frac(r), n_r) + assert Eq(frac(r), p_r).has(Eq) + assert Eq(frac(r), np_r).has(Eq) + assert Eq(frac(r), nn_r).has(Eq) + + assert Ne(frac(r), p_r).has(Ne) + assert Ne(frac(r), n_r) + assert Ne(frac(r), np_r).has(Ne) + assert Ne(frac(r), nn_r).has(Ne) + + # Real frac argument, +/- oo rhs + assert frac(r) < oo + assert frac(r) <= oo + assert not frac(r) > oo + assert not frac(r) >= oo + + assert not frac(r) < -oo + assert not frac(r) <= -oo + assert frac(r) > -oo + assert frac(r) >= -oo + + assert frac(r) < 1 + assert frac(r) <= 1 + assert not frac(r) > 1 + assert not frac(r) >= 1 + + assert not frac(r) < 0 + assert (frac(r) <= 0).has(Le) + assert (frac(r) > 0).has(Gt) + assert frac(r) >= 0 + + # Some test for numbers + assert frac(r) <= sqrt(2) + assert (frac(r) <= sqrt(3) - sqrt(2)).has(Le) + assert not frac(r) <= sqrt(2) - sqrt(3) + assert not frac(r) >= sqrt(2) + assert (frac(r) >= sqrt(3) - sqrt(2)).has(Ge) + assert frac(r) >= sqrt(2) - sqrt(3) + + assert not Eq(frac(r), sqrt(2)) + assert Eq(frac(r), sqrt(3) - sqrt(2)).has(Eq) + assert not Eq(frac(r), sqrt(2) - sqrt(3)) + assert Ne(frac(r), sqrt(2)) + assert Ne(frac(r), sqrt(3) - sqrt(2)).has(Ne) + assert Ne(frac(r), sqrt(2) - sqrt(3)) + + assert frac(p_i, evaluate=False).is_zero + assert frac(p_i, evaluate=False).is_finite + assert frac(p_i, evaluate=False).is_integer + assert frac(p_i, evaluate=False).is_real + assert frac(r).is_finite + assert frac(r).is_real + assert frac(r).is_zero is None + assert frac(r).is_integer is None + + assert frac(oo).is_finite + assert frac(oo).is_real + + +def test_series(): + x, y = symbols('x,y') + assert floor(x).nseries(x, y, 100) == floor(y) + assert ceiling(x).nseries(x, y, 100) == ceiling(y) + assert floor(x).nseries(x, pi, 100) == 3 + assert ceiling(x).nseries(x, pi, 100) == 4 + assert floor(x).nseries(x, 0, 100) == 0 + assert ceiling(x).nseries(x, 0, 100) == 1 + assert floor(-x).nseries(x, 0, 100) == -1 + assert ceiling(-x).nseries(x, 0, 100) == 0 + + +def test_issue_14355(): + # This test checks the leading term and series for the floor and ceil + # function when arg0 evaluates to S.NaN. + assert floor((x**3 + x)/(x**2 - x)).as_leading_term(x, cdir = 1) == -2 + assert floor((x**3 + x)/(x**2 - x)).as_leading_term(x, cdir = -1) == -1 + assert floor((cos(x) - 1)/x).as_leading_term(x, cdir = 1) == -1 + assert floor((cos(x) - 1)/x).as_leading_term(x, cdir = -1) == 0 + assert floor(sin(x)/x).as_leading_term(x, cdir = 1) == 0 + assert floor(sin(x)/x).as_leading_term(x, cdir = -1) == 0 + assert floor(-tan(x)/x).as_leading_term(x, cdir = 1) == -2 + assert floor(-tan(x)/x).as_leading_term(x, cdir = -1) == -2 + assert floor(sin(x)/x/3).as_leading_term(x, cdir = 1) == 0 + assert floor(sin(x)/x/3).as_leading_term(x, cdir = -1) == 0 + assert ceiling((x**3 + x)/(x**2 - x)).as_leading_term(x, cdir = 1) == -1 + assert ceiling((x**3 + x)/(x**2 - x)).as_leading_term(x, cdir = -1) == 0 + assert ceiling((cos(x) - 1)/x).as_leading_term(x, cdir = 1) == 0 + assert ceiling((cos(x) - 1)/x).as_leading_term(x, cdir = -1) == 1 + assert ceiling(sin(x)/x).as_leading_term(x, cdir = 1) == 1 + assert ceiling(sin(x)/x).as_leading_term(x, cdir = -1) == 1 + assert ceiling(-tan(x)/x).as_leading_term(x, cdir = 1) == -1 + assert ceiling(-tan(x)/x).as_leading_term(x, cdir = 1) == -1 + assert ceiling(sin(x)/x/3).as_leading_term(x, cdir = 1) == 1 + assert ceiling(sin(x)/x/3).as_leading_term(x, cdir = -1) == 1 + # test for series + assert floor(sin(x)/x).series(x, 0, 100, cdir = 1) == 0 + assert floor(sin(x)/x).series(x, 0, 100, cdir = 1) == 0 + assert floor((x**3 + x)/(x**2 - x)).series(x, 0, 100, cdir = 1) == -2 + assert floor((x**3 + x)/(x**2 - x)).series(x, 0, 100, cdir = -1) == -1 + assert ceiling(sin(x)/x).series(x, 0, 100, cdir = 1) == 1 + assert ceiling(sin(x)/x).series(x, 0, 100, cdir = -1) == 1 + assert ceiling((x**3 + x)/(x**2 - x)).series(x, 0, 100, cdir = 1) == -1 + assert ceiling((x**3 + x)/(x**2 - x)).series(x, 0, 100, cdir = -1) == 0 + + +def test_frac_leading_term(): + assert frac(x).as_leading_term(x) == x + assert frac(x).as_leading_term(x, cdir = 1) == x + assert frac(x).as_leading_term(x, cdir = -1) == 1 + assert frac(x + S.Half).as_leading_term(x, cdir = 1) == S.Half + assert frac(x + S.Half).as_leading_term(x, cdir = -1) == S.Half + assert frac(-2*x + 1).as_leading_term(x, cdir = 1) == S.One + assert frac(-2*x + 1).as_leading_term(x, cdir = -1) == -2*x + assert frac(sin(x) + 5).as_leading_term(x, cdir = 1) == x + assert frac(sin(x) + 5).as_leading_term(x, cdir = -1) == S.One + assert frac(sin(x**2) + 5).as_leading_term(x, cdir = 1) == x**2 + assert frac(sin(x**2) + 5).as_leading_term(x, cdir = -1) == x**2 + + +@XFAIL +def test_issue_4149(): + assert floor(3 + pi*I + y*I) == 3 + floor(pi + y)*I + assert floor(3*I + pi*I + y*I) == floor(3 + pi + y)*I + assert floor(3 + E + pi*I + y*I) == 5 + floor(pi + y)*I + + +def test_issue_21651(): + k = Symbol('k', positive=True, integer=True) + exp = 2*2**(-k) + assert isinstance(floor(exp), floor) + + +def test_issue_11207(): + assert floor(floor(x)) == floor(x) + assert floor(ceiling(x)) == ceiling(x) + assert ceiling(floor(x)) == floor(x) + assert ceiling(ceiling(x)) == ceiling(x) + + +def test_nested_floor_ceiling(): + assert floor(-floor(ceiling(x**3)/y)) == -floor(ceiling(x**3)/y) + assert ceiling(-floor(ceiling(x**3)/y)) == -floor(ceiling(x**3)/y) + assert floor(ceiling(-floor(x**Rational(7, 2)/y))) == -floor(x**Rational(7, 2)/y) + assert -ceiling(-ceiling(floor(x)/y)) == ceiling(floor(x)/y) + +def test_issue_18689(): + assert floor(floor(floor(x)) + 3) == floor(x) + 3 + assert ceiling(ceiling(ceiling(x)) + 1) == ceiling(x) + 1 + assert ceiling(ceiling(floor(x)) + 3) == floor(x) + 3 + +def test_issue_18421(): + assert floor(float(0)) is S.Zero + assert ceiling(float(0)) is S.Zero + +def test_issue_25230(): + a = Symbol('a', real = True) + b = Symbol('b', positive = True) + c = Symbol('c', negative = True) + raises(NotImplementedError, lambda: floor(x/a).as_leading_term(x, cdir = 1)) + raises(NotImplementedError, lambda: ceiling(x/a).as_leading_term(x, cdir = 1)) + assert floor(x/b).as_leading_term(x, cdir = 1) == 0 + assert floor(x/b).as_leading_term(x, cdir = -1) == -1 + assert floor(x/c).as_leading_term(x, cdir = 1) == -1 + assert floor(x/c).as_leading_term(x, cdir = -1) == 0 + assert ceiling(x/b).as_leading_term(x, cdir = 1) == 1 + assert ceiling(x/b).as_leading_term(x, cdir = -1) == 0 + assert ceiling(x/c).as_leading_term(x, cdir = 1) == 0 + assert ceiling(x/c).as_leading_term(x, cdir = -1) == 1 diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_interface.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_interface.py new file mode 100644 index 0000000000000000000000000000000000000000..6ae2f78b50bea24c64079066076971e315660d69 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_interface.py @@ -0,0 +1,82 @@ +# This test file tests the SymPy function interface, that people use to create +# their own new functions. It should be as easy as possible. +# +# We test that it works with both Function and DefinedFunction. New code should +# use DefinedFunction because it has better type inference. Old code still +# using Function should continue to work though. +from sympy.core.function import Function, DefinedFunction +from sympy.core.sympify import sympify +from sympy.functions.elementary.hyperbolic import tanh +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.series.limits import limit +from sympy.abc import x + + +def test_function_series1(): + """Create our new "sin" function.""" + + for F in [Function, DefinedFunction]: + + class my_function(F): + + def fdiff(self, argindex=1): + return cos(self.args[0]) + + @classmethod + def eval(cls, arg): + arg = sympify(arg) + if arg == 0: + return sympify(0) + + #Test that the taylor series is correct + assert my_function(x).series(x, 0, 10) == sin(x).series(x, 0, 10) + assert limit(my_function(x)/x, x, 0) == 1 + + +def test_function_series2(): + """Create our new "cos" function.""" + + for F in [Function, DefinedFunction]: + + class my_function2(F): + + def fdiff(self, argindex=1): + return -sin(self.args[0]) + + @classmethod + def eval(cls, arg): + arg = sympify(arg) + if arg == 0: + return sympify(1) + + #Test that the taylor series is correct + assert my_function2(x).series(x, 0, 10) == cos(x).series(x, 0, 10) + + +def test_function_series3(): + """ + Test our easy "tanh" function. + + This test tests two things: + * that the Function interface works as expected and it's easy to use + * that the general algorithm for the series expansion works even when the + derivative is defined recursively in terms of the original function, + since tanh(x).diff(x) == 1-tanh(x)**2 + """ + + for F in [Function, DefinedFunction]: + + class mytanh(F): + + def fdiff(self, argindex=1): + return 1 - mytanh(self.args[0])**2 + + @classmethod + def eval(cls, arg): + arg = sympify(arg) + if arg == 0: + return sympify(0) + + e = tanh(x) + f = mytanh(x) + assert e.series(x, 0, 6) == f.series(x, 0, 6) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_miscellaneous.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_miscellaneous.py new file mode 100644 index 0000000000000000000000000000000000000000..374c4fb50eaae54a9884015c124c245385e1761e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_miscellaneous.py @@ -0,0 +1,504 @@ +import itertools as it + +from sympy.core.expr import unchanged +from sympy.core.function import Function +from sympy.core.numbers import I, oo, Rational +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.external import import_module +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.integers import floor, ceiling +from sympy.functions.elementary.miscellaneous import (sqrt, cbrt, root, Min, + Max, real_root, Rem) +from sympy.functions.elementary.trigonometric import cos, sin +from sympy.functions.special.delta_functions import Heaviside + +from sympy.utilities.lambdify import lambdify +from sympy.testing.pytest import raises, skip, ignore_warnings + +def test_Min(): + from sympy.abc import x, y, z + n = Symbol('n', negative=True) + n_ = Symbol('n_', negative=True) + nn = Symbol('nn', nonnegative=True) + nn_ = Symbol('nn_', nonnegative=True) + p = Symbol('p', positive=True) + p_ = Symbol('p_', positive=True) + np = Symbol('np', nonpositive=True) + np_ = Symbol('np_', nonpositive=True) + r = Symbol('r', real=True) + + assert Min(5, 4) == 4 + assert Min(-oo, -oo) is -oo + assert Min(-oo, n) is -oo + assert Min(n, -oo) is -oo + assert Min(-oo, np) is -oo + assert Min(np, -oo) is -oo + assert Min(-oo, 0) is -oo + assert Min(0, -oo) is -oo + assert Min(-oo, nn) is -oo + assert Min(nn, -oo) is -oo + assert Min(-oo, p) is -oo + assert Min(p, -oo) is -oo + assert Min(-oo, oo) is -oo + assert Min(oo, -oo) is -oo + assert Min(n, n) == n + assert unchanged(Min, n, np) + assert Min(np, n) == Min(n, np) + assert Min(n, 0) == n + assert Min(0, n) == n + assert Min(n, nn) == n + assert Min(nn, n) == n + assert Min(n, p) == n + assert Min(p, n) == n + assert Min(n, oo) == n + assert Min(oo, n) == n + assert Min(np, np) == np + assert Min(np, 0) == np + assert Min(0, np) == np + assert Min(np, nn) == np + assert Min(nn, np) == np + assert Min(np, p) == np + assert Min(p, np) == np + assert Min(np, oo) == np + assert Min(oo, np) == np + assert Min(0, 0) == 0 + assert Min(0, nn) == 0 + assert Min(nn, 0) == 0 + assert Min(0, p) == 0 + assert Min(p, 0) == 0 + assert Min(0, oo) == 0 + assert Min(oo, 0) == 0 + assert Min(nn, nn) == nn + assert unchanged(Min, nn, p) + assert Min(p, nn) == Min(nn, p) + assert Min(nn, oo) == nn + assert Min(oo, nn) == nn + assert Min(p, p) == p + assert Min(p, oo) == p + assert Min(oo, p) == p + assert Min(oo, oo) is oo + + assert Min(n, n_).func is Min + assert Min(nn, nn_).func is Min + assert Min(np, np_).func is Min + assert Min(p, p_).func is Min + + # lists + assert Min() is S.Infinity + assert Min(x) == x + assert Min(x, y) == Min(y, x) + assert Min(x, y, z) == Min(z, y, x) + assert Min(x, Min(y, z)) == Min(z, y, x) + assert Min(x, Max(y, -oo)) == Min(x, y) + assert Min(p, oo, n, p, p, p_) == n + assert Min(p_, n_, p) == n_ + assert Min(n, oo, -7, p, p, 2) == Min(n, -7) + assert Min(2, x, p, n, oo, n_, p, 2, -2, -2) == Min(-2, x, n, n_) + assert Min(0, x, 1, y) == Min(0, x, y) + assert Min(1000, 100, -100, x, p, n) == Min(n, x, -100) + assert unchanged(Min, sin(x), cos(x)) + assert Min(sin(x), cos(x)) == Min(cos(x), sin(x)) + assert Min(cos(x), sin(x)).subs(x, 1) == cos(1) + assert Min(cos(x), sin(x)).subs(x, S.Half) == sin(S.Half) + raises(ValueError, lambda: Min(cos(x), sin(x)).subs(x, I)) + raises(ValueError, lambda: Min(I)) + raises(ValueError, lambda: Min(I, x)) + raises(ValueError, lambda: Min(S.ComplexInfinity, x)) + + assert Min(1, x).diff(x) == Heaviside(1 - x) + assert Min(x, 1).diff(x) == Heaviside(1 - x) + assert Min(0, -x, 1 - 2*x).diff(x) == -Heaviside(x + Min(0, -2*x + 1)) \ + - 2*Heaviside(2*x + Min(0, -x) - 1) + + # issue 7619 + f = Function('f') + assert Min(1, 2*Min(f(1), 2)) # doesn't fail + + # issue 7233 + e = Min(0, x) + assert e.n().args == (0, x) + + # issue 8643 + m = Min(n, p_, n_, r) + assert m.is_positive is False + assert m.is_nonnegative is False + assert m.is_negative is True + + m = Min(p, p_) + assert m.is_positive is True + assert m.is_nonnegative is True + assert m.is_negative is False + + m = Min(p, nn_, p_) + assert m.is_positive is None + assert m.is_nonnegative is True + assert m.is_negative is False + + m = Min(nn, p, r) + assert m.is_positive is None + assert m.is_nonnegative is None + assert m.is_negative is None + + +def test_Max(): + from sympy.abc import x, y, z + n = Symbol('n', negative=True) + n_ = Symbol('n_', negative=True) + nn = Symbol('nn', nonnegative=True) + p = Symbol('p', positive=True) + p_ = Symbol('p_', positive=True) + r = Symbol('r', real=True) + + assert Max(5, 4) == 5 + + # lists + + assert Max() is S.NegativeInfinity + assert Max(x) == x + assert Max(x, y) == Max(y, x) + assert Max(x, y, z) == Max(z, y, x) + assert Max(x, Max(y, z)) == Max(z, y, x) + assert Max(x, Min(y, oo)) == Max(x, y) + assert Max(n, -oo, n_, p, 2) == Max(p, 2) + assert Max(n, -oo, n_, p) == p + assert Max(2, x, p, n, -oo, S.NegativeInfinity, n_, p, 2) == Max(2, x, p) + assert Max(0, x, 1, y) == Max(1, x, y) + assert Max(r, r + 1, r - 1) == 1 + r + assert Max(1000, 100, -100, x, p, n) == Max(p, x, 1000) + assert Max(cos(x), sin(x)) == Max(sin(x), cos(x)) + assert Max(cos(x), sin(x)).subs(x, 1) == sin(1) + assert Max(cos(x), sin(x)).subs(x, S.Half) == cos(S.Half) + raises(ValueError, lambda: Max(cos(x), sin(x)).subs(x, I)) + raises(ValueError, lambda: Max(I)) + raises(ValueError, lambda: Max(I, x)) + raises(ValueError, lambda: Max(S.ComplexInfinity, 1)) + assert Max(n, -oo, n_, p, 2) == Max(p, 2) + assert Max(n, -oo, n_, p, 1000) == Max(p, 1000) + + assert Max(1, x).diff(x) == Heaviside(x - 1) + assert Max(x, 1).diff(x) == Heaviside(x - 1) + assert Max(x**2, 1 + x, 1).diff(x) == \ + 2*x*Heaviside(x**2 - Max(1, x + 1)) \ + + Heaviside(x - Max(1, x**2) + 1) + + e = Max(0, x) + assert e.n().args == (0, x) + + # issue 8643 + m = Max(p, p_, n, r) + assert m.is_positive is True + assert m.is_nonnegative is True + assert m.is_negative is False + + m = Max(n, n_) + assert m.is_positive is False + assert m.is_nonnegative is False + assert m.is_negative is True + + m = Max(n, n_, r) + assert m.is_positive is None + assert m.is_nonnegative is None + assert m.is_negative is None + + m = Max(n, nn, r) + assert m.is_positive is None + assert m.is_nonnegative is True + assert m.is_negative is False + + +def test_minmax_assumptions(): + r = Symbol('r', real=True) + a = Symbol('a', real=True, algebraic=True) + t = Symbol('t', real=True, transcendental=True) + q = Symbol('q', rational=True) + p = Symbol('p', irrational=True) + n = Symbol('n', rational=True, integer=False) + i = Symbol('i', integer=True) + o = Symbol('o', odd=True) + e = Symbol('e', even=True) + k = Symbol('k', prime=True) + reals = [r, a, t, q, p, n, i, o, e, k] + + for ext in (Max, Min): + for x, y in it.product(reals, repeat=2): + + # Must be real + assert ext(x, y).is_real + + # Algebraic? + if x.is_algebraic and y.is_algebraic: + assert ext(x, y).is_algebraic + elif x.is_transcendental and y.is_transcendental: + assert ext(x, y).is_transcendental + else: + assert ext(x, y).is_algebraic is None + + # Rational? + if x.is_rational and y.is_rational: + assert ext(x, y).is_rational + elif x.is_irrational and y.is_irrational: + assert ext(x, y).is_irrational + else: + assert ext(x, y).is_rational is None + + # Integer? + if x.is_integer and y.is_integer: + assert ext(x, y).is_integer + elif x.is_noninteger and y.is_noninteger: + assert ext(x, y).is_noninteger + else: + assert ext(x, y).is_integer is None + + # Odd? + if x.is_odd and y.is_odd: + assert ext(x, y).is_odd + elif x.is_odd is False and y.is_odd is False: + assert ext(x, y).is_odd is False + else: + assert ext(x, y).is_odd is None + + # Even? + if x.is_even and y.is_even: + assert ext(x, y).is_even + elif x.is_even is False and y.is_even is False: + assert ext(x, y).is_even is False + else: + assert ext(x, y).is_even is None + + # Prime? + if x.is_prime and y.is_prime: + assert ext(x, y).is_prime + elif x.is_prime is False and y.is_prime is False: + assert ext(x, y).is_prime is False + else: + assert ext(x, y).is_prime is None + + +def test_issue_8413(): + x = Symbol('x', real=True) + # we can't evaluate in general because non-reals are not + # comparable: Min(floor(3.2 + I), 3.2 + I) -> ValueError + assert Min(floor(x), x) == floor(x) + assert Min(ceiling(x), x) == x + assert Max(floor(x), x) == x + assert Max(ceiling(x), x) == ceiling(x) + + +def test_root(): + from sympy.abc import x + n = Symbol('n', integer=True) + k = Symbol('k', integer=True) + + assert root(2, 2) == sqrt(2) + assert root(2, 1) == 2 + assert root(2, 3) == 2**Rational(1, 3) + assert root(2, 3) == cbrt(2) + assert root(2, -5) == 2**Rational(4, 5)/2 + + assert root(-2, 1) == -2 + + assert root(-2, 2) == sqrt(2)*I + assert root(-2, 1) == -2 + + assert root(x, 2) == sqrt(x) + assert root(x, 1) == x + assert root(x, 3) == x**Rational(1, 3) + assert root(x, 3) == cbrt(x) + assert root(x, -5) == x**Rational(-1, 5) + + assert root(x, n) == x**(1/n) + assert root(x, -n) == x**(-1/n) + + assert root(x, n, k) == (-1)**(2*k/n)*x**(1/n) + + +def test_real_root(): + assert real_root(-8, 3) == -2 + assert real_root(-16, 4) == root(-16, 4) + r = root(-7, 4) + assert real_root(r) == r + r1 = root(-1, 3) + r2 = r1**2 + r3 = root(-1, 4) + assert real_root(r1 + r2 + r3) == -1 + r2 + r3 + assert real_root(root(-2, 3)) == -root(2, 3) + assert real_root(-8., 3) == -2.0 + x = Symbol('x') + n = Symbol('n') + g = real_root(x, n) + assert g.subs({"x": -8, "n": 3}) == -2 + assert g.subs({"x": 8, "n": 3}) == 2 + # give principle root if there is no real root -- if this is not desired + # then maybe a Root class is needed to raise an error instead + assert g.subs({"x": I, "n": 3}) == cbrt(I) + assert g.subs({"x": -8, "n": 2}) == sqrt(-8) + assert g.subs({"x": I, "n": 2}) == sqrt(I) + + +def test_issue_11463(): + numpy = import_module('numpy') + if not numpy: + skip("numpy not installed.") + x = Symbol('x') + f = lambdify(x, real_root((log(x/(x-2))), 3), 'numpy') + # numpy.select evaluates all options before considering conditions, + # so it raises a warning about root of negative number which does + # not affect the outcome. This warning is suppressed here + with ignore_warnings(RuntimeWarning): + assert f(numpy.array(-1)) < -1 + + +def test_rewrite_MaxMin_as_Heaviside(): + from sympy.abc import x + assert Max(0, x).rewrite(Heaviside) == x*Heaviside(x) + assert Max(3, x).rewrite(Heaviside) == x*Heaviside(x - 3) + \ + 3*Heaviside(-x + 3) + assert Max(0, x+2, 2*x).rewrite(Heaviside) == \ + 2*x*Heaviside(2*x)*Heaviside(x - 2) + \ + (x + 2)*Heaviside(-x + 2)*Heaviside(x + 2) + + assert Min(0, x).rewrite(Heaviside) == x*Heaviside(-x) + assert Min(3, x).rewrite(Heaviside) == x*Heaviside(-x + 3) + \ + 3*Heaviside(x - 3) + assert Min(x, -x, -2).rewrite(Heaviside) == \ + x*Heaviside(-2*x)*Heaviside(-x - 2) - \ + x*Heaviside(2*x)*Heaviside(x - 2) \ + - 2*Heaviside(-x + 2)*Heaviside(x + 2) + + +def test_rewrite_MaxMin_as_Piecewise(): + from sympy.core.symbol import symbols + from sympy.functions.elementary.piecewise import Piecewise + x, y, z, a, b = symbols('x y z a b', real=True) + vx, vy, va = symbols('vx vy va') + assert Max(a, b).rewrite(Piecewise) == Piecewise((a, a >= b), (b, True)) + assert Max(x, y, z).rewrite(Piecewise) == Piecewise((x, (x >= y) & (x >= z)), (y, y >= z), (z, True)) + assert Max(x, y, a, b).rewrite(Piecewise) == Piecewise((a, (a >= b) & (a >= x) & (a >= y)), + (b, (b >= x) & (b >= y)), (x, x >= y), (y, True)) + assert Min(a, b).rewrite(Piecewise) == Piecewise((a, a <= b), (b, True)) + assert Min(x, y, z).rewrite(Piecewise) == Piecewise((x, (x <= y) & (x <= z)), (y, y <= z), (z, True)) + assert Min(x, y, a, b).rewrite(Piecewise) == Piecewise((a, (a <= b) & (a <= x) & (a <= y)), + (b, (b <= x) & (b <= y)), (x, x <= y), (y, True)) + + # Piecewise rewriting of Min/Max does also takes place for not explicitly real arguments + assert Max(vx, vy).rewrite(Piecewise) == Piecewise((vx, vx >= vy), (vy, True)) + assert Min(va, vx, vy).rewrite(Piecewise) == Piecewise((va, (va <= vx) & (va <= vy)), (vx, vx <= vy), (vy, True)) + + +def test_issue_11099(): + from sympy.abc import x, y + # some fixed value tests + fixed_test_data = {x: -2, y: 3} + assert Min(x, y).evalf(subs=fixed_test_data) == \ + Min(x, y).subs(fixed_test_data).evalf() + assert Max(x, y).evalf(subs=fixed_test_data) == \ + Max(x, y).subs(fixed_test_data).evalf() + # randomly generate some test data + from sympy.core.random import randint + for i in range(20): + random_test_data = {x: randint(-100, 100), y: randint(-100, 100)} + assert Min(x, y).evalf(subs=random_test_data) == \ + Min(x, y).subs(random_test_data).evalf() + assert Max(x, y).evalf(subs=random_test_data) == \ + Max(x, y).subs(random_test_data).evalf() + + +def test_issue_12638(): + from sympy.abc import a, b, c + assert Min(a, b, c, Max(a, b)) == Min(a, b, c) + assert Min(a, b, Max(a, b, c)) == Min(a, b) + assert Min(a, b, Max(a, c)) == Min(a, b) + +def test_issue_21399(): + from sympy.abc import a, b, c + assert Max(Min(a, b), Min(a, b, c)) == Min(a, b) + + +def test_instantiation_evaluation(): + from sympy.abc import v, w, x, y, z + assert Min(1, Max(2, x)) == 1 + assert Max(3, Min(2, x)) == 3 + assert Min(Max(x, y), Max(x, z)) == Max(x, Min(y, z)) + assert set(Min(Max(w, x), Max(y, z)).args) == { + Max(w, x), Max(y, z)} + assert Min(Max(x, y), Max(x, z), w) == Min( + w, Max(x, Min(y, z))) + A, B = Min, Max + for i in range(2): + assert A(x, B(x, y)) == x + assert A(x, B(y, A(x, w, z))) == A(x, B(y, A(w, z))) + A, B = B, A + assert Min(w, Max(x, y), Max(v, x, z)) == Min( + w, Max(x, Min(y, Max(v, z)))) + +def test_rewrite_as_Abs(): + from itertools import permutations + from sympy.functions.elementary.complexes import Abs + from sympy.abc import x, y, z, w + def test(e): + free = e.free_symbols + a = e.rewrite(Abs) + assert not a.has(Min, Max) + for i in permutations(range(len(free))): + reps = dict(zip(free, i)) + assert a.xreplace(reps) == e.xreplace(reps) + test(Min(x, y)) + test(Max(x, y)) + test(Min(x, y, z)) + test(Min(Max(w, x), Max(y, z))) + +def test_issue_14000(): + assert isinstance(sqrt(4, evaluate=False), Pow) == True + assert isinstance(cbrt(3.5, evaluate=False), Pow) == True + assert isinstance(root(16, 4, evaluate=False), Pow) == True + + assert sqrt(4, evaluate=False) == Pow(4, S.Half, evaluate=False) + assert cbrt(3.5, evaluate=False) == Pow(3.5, Rational(1, 3), evaluate=False) + assert root(4, 2, evaluate=False) == Pow(4, S.Half, evaluate=False) + + assert root(16, 4, 2, evaluate=False).has(Pow) == True + assert real_root(-8, 3, evaluate=False).has(Pow) == True + +def test_issue_6899(): + from sympy.core.function import Lambda + x = Symbol('x') + eqn = Lambda(x, x) + assert eqn.func(*eqn.args) == eqn + +def test_Rem(): + from sympy.abc import x, y + assert Rem(5, 3) == 2 + assert Rem(-5, 3) == -2 + assert Rem(5, -3) == 2 + assert Rem(-5, -3) == -2 + assert Rem(x**3, y) == Rem(x**3, y) + assert Rem(Rem(-5, 3) + 3, 3) == 1 + + +def test_minmax_no_evaluate(): + from sympy import evaluate + p = Symbol('p', positive=True) + + assert Max(1, 3) == 3 + assert Max(1, 3).args == () + assert Max(0, p) == p + assert Max(0, p).args == () + assert Min(0, p) == 0 + assert Min(0, p).args == () + + assert Max(1, 3, evaluate=False) != 3 + assert Max(1, 3, evaluate=False).args == (1, 3) + assert Max(0, p, evaluate=False) != p + assert Max(0, p, evaluate=False).args == (0, p) + assert Min(0, p, evaluate=False) != 0 + assert Min(0, p, evaluate=False).args == (0, p) + + with evaluate(False): + assert Max(1, 3) != 3 + assert Max(1, 3).args == (1, 3) + assert Max(0, p) != p + assert Max(0, p).args == (0, p) + assert Min(0, p) != 0 + assert Min(0, p).args == (0, p) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_piecewise.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_piecewise.py new file mode 100644 index 0000000000000000000000000000000000000000..7d0728095578b49480a1334857a1c237012d2534 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_piecewise.py @@ -0,0 +1,1639 @@ +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.expr import unchanged +from sympy.core.function import (Function, diff, expand) +from sympy.core.mul import Mul +from sympy.core.mod import Mod +from sympy.core.numbers import (Float, I, Rational, oo, pi, zoo) +from sympy.core.relational import (Eq, Ge, Gt, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.complexes import (Abs, adjoint, arg, conjugate, im, re, transpose) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import (Max, Min, sqrt) +from sympy.functions.elementary.piecewise import (Piecewise, + piecewise_fold, piecewise_exclusive, Undefined, ExprCondPair) +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.functions.special.delta_functions import (DiracDelta, Heaviside) +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.integrals.integrals import (Integral, integrate) +from sympy.logic.boolalg import (And, ITE, Not, Or) +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.printing import srepr +from sympy.sets.contains import Contains +from sympy.sets.sets import Interval +from sympy.solvers.solvers import solve +from sympy.testing.pytest import raises, slow +from sympy.utilities.lambdify import lambdify + +a, b, c, d, x, y = symbols('a:d, x, y') +z = symbols('z', nonzero=True) + + +def test_piecewise1(): + + # Test canonicalization + assert Piecewise((x, x < 1.)).has(1.0) # doesn't get changed to x < 1 + assert unchanged(Piecewise, ExprCondPair(x, x < 1), ExprCondPair(0, True)) + assert Piecewise((x, x < 1), (0, True)) == Piecewise(ExprCondPair(x, x < 1), + ExprCondPair(0, True)) + assert Piecewise((x, x < 1), (0, True), (1, True)) == \ + Piecewise((x, x < 1), (0, True)) + assert Piecewise((x, x < 1), (0, False), (-1, 1 > 2)) == \ + Piecewise((x, x < 1)) + assert Piecewise((x, x < 1), (0, x < 1), (0, True)) == \ + Piecewise((x, x < 1), (0, True)) + assert Piecewise((x, x < 1), (0, x < 2), (0, True)) == \ + Piecewise((x, x < 1), (0, True)) + assert Piecewise((x, x < 1), (x, x < 2), (0, True)) == \ + Piecewise((x, Or(x < 1, x < 2)), (0, True)) + assert Piecewise((x, x < 1), (x, x < 2), (x, True)) == x + assert Piecewise((x, True)) == x + # Explicitly constructed empty Piecewise not accepted + raises(TypeError, lambda: Piecewise()) + # False condition is never retained + assert Piecewise((2*x, x < 0), (x, False)) == \ + Piecewise((2*x, x < 0), (x, False), evaluate=False) == \ + Piecewise((2*x, x < 0)) + assert Piecewise((x, False)) == Undefined + raises(TypeError, lambda: Piecewise(x)) + assert Piecewise((x, 1)) == x # 1 and 0 are accepted as True/False + raises(TypeError, lambda: Piecewise((x, 2))) + raises(TypeError, lambda: Piecewise((x, x**2))) + raises(TypeError, lambda: Piecewise(([1], True))) + assert Piecewise(((1, 2), True)) == Tuple(1, 2) + cond = (Piecewise((1, x < 0), (2, True)) < y) + assert Piecewise((1, cond) + ) == Piecewise((1, ITE(x < 0, y > 1, y > 2))) + + assert Piecewise((1, x > 0), (2, And(x <= 0, x > -1)) + ) == Piecewise((1, x > 0), (2, x > -1)) + assert Piecewise((1, x <= 0), (2, (x < 0) & (x > -1)) + ) == Piecewise((1, x <= 0)) + + # test for supporting Contains in Piecewise + pwise = Piecewise( + (1, And(x <= 6, x > 1, Contains(x, S.Integers))), + (0, True)) + assert pwise.subs(x, pi) == 0 + assert pwise.subs(x, 2) == 1 + assert pwise.subs(x, 7) == 0 + + # Test subs + p = Piecewise((-1, x < -1), (x**2, x < 0), (log(x), x >= 0)) + p_x2 = Piecewise((-1, x**2 < -1), (x**4, x**2 < 0), (log(x**2), x**2 >= 0)) + assert p.subs(x, x**2) == p_x2 + assert p.subs(x, -5) == -1 + assert p.subs(x, -1) == 1 + assert p.subs(x, 1) == log(1) + + # More subs tests + p2 = Piecewise((1, x < pi), (-1, x < 2*pi), (0, x > 2*pi)) + p3 = Piecewise((1, Eq(x, 0)), (1/x, True)) + p4 = Piecewise((1, Eq(x, 0)), (2, 1/x>2)) + assert p2.subs(x, 2) == 1 + assert p2.subs(x, 4) == -1 + assert p2.subs(x, 10) == 0 + assert p3.subs(x, 0.0) == 1 + assert p4.subs(x, 0.0) == 1 + + + f, g, h = symbols('f,g,h', cls=Function) + pf = Piecewise((f(x), x < -1), (f(x) + h(x) + 2, x <= 1)) + pg = Piecewise((g(x), x < -1), (g(x) + h(x) + 2, x <= 1)) + assert pg.subs(g, f) == pf + + assert Piecewise((1, Eq(x, 0)), (0, True)).subs(x, 0) == 1 + assert Piecewise((1, Eq(x, 0)), (0, True)).subs(x, 1) == 0 + assert Piecewise((1, Eq(x, y)), (0, True)).subs(x, y) == 1 + assert Piecewise((1, Eq(x, z)), (0, True)).subs(x, z) == 1 + assert Piecewise((1, Eq(exp(x), cos(z))), (0, True)).subs(x, z) == \ + Piecewise((1, Eq(exp(z), cos(z))), (0, True)) + + p5 = Piecewise( (0, Eq(cos(x) + y, 0)), (1, True)) + assert p5.subs(y, 0) == Piecewise( (0, Eq(cos(x), 0)), (1, True)) + + assert Piecewise((-1, y < 1), (0, x < 0), (1, Eq(x, 0)), (2, True) + ).subs(x, 1) == Piecewise((-1, y < 1), (2, True)) + assert Piecewise((1, Eq(x**2, -1)), (2, x < 0)).subs(x, I) == 1 + + p6 = Piecewise((x, x > 0)) + n = symbols('n', negative=True) + assert p6.subs(x, n) == Undefined + + # Test evalf + assert p.evalf() == Piecewise((-1.0, x < -1), (x**2, x < 0), (log(x), True)) + assert p.evalf(subs={x: -2}) == -1.0 + assert p.evalf(subs={x: -1}) == 1.0 + assert p.evalf(subs={x: 1}) == log(1) + assert p6.evalf(subs={x: -5}) == Undefined + + # Test doit + f_int = Piecewise((Integral(x, (x, 0, 1)), x < 1)) + assert f_int.doit() == Piecewise( (S.Half, x < 1) ) + + # Test differentiation + f = x + fp = x*p + dp = Piecewise((0, x < -1), (2*x, x < 0), (1/x, x >= 0)) + fp_dx = x*dp + p + assert diff(p, x) == dp + assert diff(f*p, x) == fp_dx + + # Test simple arithmetic + assert x*p == fp + assert x*p + p == p + x*p + assert p + f == f + p + assert p + dp == dp + p + assert p - dp == -(dp - p) + + # Test power + dp2 = Piecewise((0, x < -1), (4*x**2, x < 0), (1/x**2, x >= 0)) + assert dp**2 == dp2 + + # Test _eval_interval + f1 = x*y + 2 + f2 = x*y**2 + 3 + peval = Piecewise((f1, x < 0), (f2, x > 0)) + peval_interval = f1.subs( + x, 0) - f1.subs(x, -1) + f2.subs(x, 1) - f2.subs(x, 0) + assert peval._eval_interval(x, 0, 0) == 0 + assert peval._eval_interval(x, -1, 1) == peval_interval + peval2 = Piecewise((f1, x < 0), (f2, True)) + assert peval2._eval_interval(x, 0, 0) == 0 + assert peval2._eval_interval(x, 1, -1) == -peval_interval + assert peval2._eval_interval(x, -1, -2) == f1.subs(x, -2) - f1.subs(x, -1) + assert peval2._eval_interval(x, -1, 1) == peval_interval + assert peval2._eval_interval(x, None, 0) == peval2.subs(x, 0) + assert peval2._eval_interval(x, -1, None) == -peval2.subs(x, -1) + + # Test integration + assert p.integrate() == Piecewise( + (-x, x < -1), + (x**3/3 + Rational(4, 3), x < 0), + (x*log(x) - x + Rational(4, 3), True)) + p = Piecewise((x, x < 1), (x**2, -1 <= x), (x, 3 < x)) + assert integrate(p, (x, -2, 2)) == Rational(5, 6) + assert integrate(p, (x, 2, -2)) == Rational(-5, 6) + p = Piecewise((0, x < 0), (1, x < 1), (0, x < 2), (1, x < 3), (0, True)) + assert integrate(p, (x, -oo, oo)) == 2 + p = Piecewise((x, x < -10), (x**2, x <= -1), (x, 1 < x)) + assert integrate(p, (x, -2, 2)) == Undefined + + # Test commutativity + assert isinstance(p, Piecewise) and p.is_commutative is True + + +def test_piecewise_free_symbols(): + f = Piecewise((x, a < 0), (y, True)) + assert f.free_symbols == {x, y, a} + + +def test_piecewise_integrate1(): + x, y = symbols('x y', real=True) + + f = Piecewise(((x - 2)**2, x >= 0), (1, True)) + assert integrate(f, (x, -2, 2)) == Rational(14, 3) + + g = Piecewise(((x - 5)**5, x >= 4), (f, True)) + assert integrate(g, (x, -2, 2)) == Rational(14, 3) + assert integrate(g, (x, -2, 5)) == Rational(43, 6) + + assert g == Piecewise(((x - 5)**5, x >= 4), (f, x < 4)) + + g = Piecewise(((x - 5)**5, 2 <= x), (f, x < 2)) + assert integrate(g, (x, -2, 2)) == Rational(14, 3) + assert integrate(g, (x, -2, 5)) == Rational(-701, 6) + + assert g == Piecewise(((x - 5)**5, 2 <= x), (f, True)) + + g = Piecewise(((x - 5)**5, 2 <= x), (2*f, True)) + assert integrate(g, (x, -2, 2)) == Rational(28, 3) + assert integrate(g, (x, -2, 5)) == Rational(-673, 6) + + +def test_piecewise_integrate1b(): + g = Piecewise((1, x > 0), (0, Eq(x, 0)), (-1, x < 0)) + assert integrate(g, (x, -1, 1)) == 0 + + g = Piecewise((1, x - y < 0), (0, True)) + assert integrate(g, (y, -oo, 0)) == -Min(0, x) + assert g.subs(x, -3).integrate((y, -oo, 0)) == 3 + assert integrate(g, (y, 0, -oo)) == Min(0, x) + assert integrate(g, (y, 0, oo)) == -Max(0, x) + oo + assert integrate(g, (y, -oo, 42)) == -Min(42, x) + 42 + assert integrate(g, (y, -oo, oo)) == -x + oo + + g = Piecewise((0, x < 0), (x, x <= 1), (1, True)) + gy1 = g.integrate((x, y, 1)) + g1y = g.integrate((x, 1, y)) + for yy in (-1, S.Half, 2): + assert g.integrate((x, yy, 1)) == gy1.subs(y, yy) + assert g.integrate((x, 1, yy)) == g1y.subs(y, yy) + assert gy1 == Piecewise( + (-Min(1, Max(0, y))**2/2 + S.Half, y < 1), + (-y + 1, True)) + assert g1y == Piecewise( + (Min(1, Max(0, y))**2/2 - S.Half, y < 1), + (y - 1, True)) + + +@slow +def test_piecewise_integrate1ca(): + y = symbols('y', real=True) + g = Piecewise( + (1 - x, Interval(0, 1).contains(x)), + (1 + x, Interval(-1, 0).contains(x)), + (0, True) + ) + gy1 = g.integrate((x, y, 1)) + g1y = g.integrate((x, 1, y)) + + assert g.integrate((x, -2, 1)) == gy1.subs(y, -2) + assert g.integrate((x, 1, -2)) == g1y.subs(y, -2) + assert g.integrate((x, 0, 1)) == gy1.subs(y, 0) + assert g.integrate((x, 1, 0)) == g1y.subs(y, 0) + assert g.integrate((x, 2, 1)) == gy1.subs(y, 2) + assert g.integrate((x, 1, 2)) == g1y.subs(y, 2) + assert piecewise_fold(gy1.rewrite(Piecewise) + ).simplify() == Piecewise( + (1, y <= -1), + (-y**2/2 - y + S.Half, y <= 0), + (y**2/2 - y + S.Half, y < 1), + (0, True)) + assert piecewise_fold(g1y.rewrite(Piecewise) + ).simplify() == Piecewise( + (-1, y <= -1), + (y**2/2 + y - S.Half, y <= 0), + (-y**2/2 + y - S.Half, y < 1), + (0, True)) + assert gy1 == Piecewise( + ( + -Min(1, Max(-1, y))**2/2 - Min(1, Max(-1, y)) + + Min(1, Max(0, y))**2 + S.Half, y < 1), + (0, True) + ) + assert g1y == Piecewise( + ( + Min(1, Max(-1, y))**2/2 + Min(1, Max(-1, y)) - + Min(1, Max(0, y))**2 - S.Half, y < 1), + (0, True)) + + +@slow +def test_piecewise_integrate1cb(): + y = symbols('y', real=True) + g = Piecewise( + (0, Or(x <= -1, x >= 1)), + (1 - x, x > 0), + (1 + x, True) + ) + gy1 = g.integrate((x, y, 1)) + g1y = g.integrate((x, 1, y)) + + assert g.integrate((x, -2, 1)) == gy1.subs(y, -2) + assert g.integrate((x, 1, -2)) == g1y.subs(y, -2) + assert g.integrate((x, 0, 1)) == gy1.subs(y, 0) + assert g.integrate((x, 1, 0)) == g1y.subs(y, 0) + assert g.integrate((x, 2, 1)) == gy1.subs(y, 2) + assert g.integrate((x, 1, 2)) == g1y.subs(y, 2) + + assert piecewise_fold(gy1.rewrite(Piecewise) + ).simplify() == Piecewise( + (1, y <= -1), + (-y**2/2 - y + S.Half, y <= 0), + (y**2/2 - y + S.Half, y < 1), + (0, True)) + assert piecewise_fold(g1y.rewrite(Piecewise) + ).simplify() == Piecewise( + (-1, y <= -1), + (y**2/2 + y - S.Half, y <= 0), + (-y**2/2 + y - S.Half, y < 1), + (0, True)) + + # g1y and gy1 should simplify if the condition that y < 1 + # is applied, e.g. Min(1, Max(-1, y)) --> Max(-1, y) + assert gy1 == Piecewise( + ( + -Min(1, Max(-1, y))**2/2 - Min(1, Max(-1, y)) + + Min(1, Max(0, y))**2 + S.Half, y < 1), + (0, True) + ) + assert g1y == Piecewise( + ( + Min(1, Max(-1, y))**2/2 + Min(1, Max(-1, y)) - + Min(1, Max(0, y))**2 - S.Half, y < 1), + (0, True)) + + +def test_piecewise_integrate2(): + from itertools import permutations + lim = Tuple(x, c, d) + p = Piecewise((1, x < a), (2, x > b), (3, True)) + q = p.integrate(lim) + assert q == Piecewise( + (-c + 2*d - 2*Min(d, Max(a, c)) + Min(d, Max(a, b, c)), c < d), + (-2*c + d + 2*Min(c, Max(a, d)) - Min(c, Max(a, b, d)), True)) + for v in permutations((1, 2, 3, 4)): + r = dict(zip((a, b, c, d), v)) + assert p.subs(r).integrate(lim.subs(r)) == q.subs(r) + + +def test_meijer_bypass(): + # totally bypass meijerg machinery when dealing + # with Piecewise in integrate + assert Piecewise((1, x < 4), (0, True)).integrate((x, oo, 1)) == -3 + + +def test_piecewise_integrate3_inequality_conditions(): + from sympy.utilities.iterables import cartes + lim = (x, 0, 5) + # set below includes two pts below range, 2 pts in range, + # 2 pts above range, and the boundaries + N = (-2, -1, 0, 1, 2, 5, 6, 7) + + p = Piecewise((1, x > a), (2, x > b), (0, True)) + ans = p.integrate(lim) + for i, j in cartes(N, repeat=2): + reps = dict(zip((a, b), (i, j))) + assert ans.subs(reps) == p.subs(reps).integrate(lim) + assert ans.subs(a, 4).subs(b, 1) == 0 + 2*3 + 1 + + p = Piecewise((1, x > a), (2, x < b), (0, True)) + ans = p.integrate(lim) + for i, j in cartes(N, repeat=2): + reps = dict(zip((a, b), (i, j))) + assert ans.subs(reps) == p.subs(reps).integrate(lim) + + # delete old tests that involved c1 and c2 since those + # reduce to the above except that a value of 0 was used + # for two expressions whereas the above uses 3 different + # values + + +@slow +def test_piecewise_integrate4_symbolic_conditions(): + a = Symbol('a', real=True) + b = Symbol('b', real=True) + x = Symbol('x', real=True) + y = Symbol('y', real=True) + p0 = Piecewise((0, Or(x < a, x > b)), (1, True)) + p1 = Piecewise((0, x < a), (0, x > b), (1, True)) + p2 = Piecewise((0, x > b), (0, x < a), (1, True)) + p3 = Piecewise((0, x < a), (1, x < b), (0, True)) + p4 = Piecewise((0, x > b), (1, x > a), (0, True)) + p5 = Piecewise((1, And(a < x, x < b)), (0, True)) + + # check values of a=1, b=3 (and reversed) with values + # of y of 0, 1, 2, 3, 4 + lim = Tuple(x, -oo, y) + for p in (p0, p1, p2, p3, p4, p5): + ans = p.integrate(lim) + for i in range(5): + reps = {a:1, b:3, y:i} + assert ans.subs(reps) == p.subs(reps).integrate(lim.subs(reps)) + reps = {a: 3, b:1, y:i} + assert ans.subs(reps) == p.subs(reps).integrate(lim.subs(reps)) + lim = Tuple(x, y, oo) + for p in (p0, p1, p2, p3, p4, p5): + ans = p.integrate(lim) + for i in range(5): + reps = {a:1, b:3, y:i} + assert ans.subs(reps) == p.subs(reps).integrate(lim.subs(reps)) + reps = {a:3, b:1, y:i} + assert ans.subs(reps) == p.subs(reps).integrate(lim.subs(reps)) + + ans = Piecewise( + (0, x <= Min(a, b)), + (x - Min(a, b), x <= b), + (b - Min(a, b), True)) + for i in (p0, p1, p2, p4): + assert i.integrate(x) == ans + assert p3.integrate(x) == Piecewise( + (0, x < a), + (-a + x, x <= Max(a, b)), + (-a + Max(a, b), True)) + assert p5.integrate(x) == Piecewise( + (0, x <= a), + (-a + x, x <= Max(a, b)), + (-a + Max(a, b), True)) + + p1 = Piecewise((0, x < a), (S.Half, x > b), (1, True)) + p2 = Piecewise((S.Half, x > b), (0, x < a), (1, True)) + p3 = Piecewise((0, x < a), (1, x < b), (S.Half, True)) + p4 = Piecewise((S.Half, x > b), (1, x > a), (0, True)) + p5 = Piecewise((1, And(a < x, x < b)), (S.Half, x > b), (0, True)) + + # check values of a=1, b=3 (and reversed) with values + # of y of 0, 1, 2, 3, 4 + lim = Tuple(x, -oo, y) + for p in (p1, p2, p3, p4, p5): + ans = p.integrate(lim) + for i in range(5): + reps = {a:1, b:3, y:i} + assert ans.subs(reps) == p.subs(reps).integrate(lim.subs(reps)) + reps = {a: 3, b:1, y:i} + assert ans.subs(reps) == p.subs(reps).integrate(lim.subs(reps)) + + +def test_piecewise_integrate5_independent_conditions(): + p = Piecewise((0, Eq(y, 0)), (x*y, True)) + assert integrate(p, (x, 1, 3)) == Piecewise((0, Eq(y, 0)), (4*y, True)) + + +def test_issue_22917(): + p = (Piecewise((0, ITE((x - y > 1) | (2 * x - 2 * y > 1), False, + ITE(x - y > 1, 2 * y - 2 < -1, 2 * x - 2 * y > 1))), + (Piecewise((0, ITE(x - y > 1, True, 2 * x - 2 * y > 1)), + (2 * Piecewise((0, x - y > 1), (y, True)), True)), True)) + + 2 * Piecewise((1, ITE((x - y > 1) | (2 * x - 2 * y > 1), False, + ITE(x - y > 1, 2 * y - 2 < -1, 2 * x - 2 * y > 1))), + (Piecewise((1, ITE(x - y > 1, True, 2 * x - 2 * y > 1)), + (2 * Piecewise((1, x - y > 1), (x, True)), True)), True))) + assert piecewise_fold(p) == Piecewise((2, (x - y > S.Half) | (x - y > 1)), + (2*y + 4, x - y > 1), + (4*x + 2*y, True)) + assert piecewise_fold(p > 1).rewrite(ITE) == ITE((x - y > S.Half) | (x - y > 1), True, + ITE(x - y > 1, 2*y + 4 > 1, 4*x + 2*y > 1)) + + +def test_piecewise_simplify(): + p = Piecewise(((x**2 + 1)/x**2, Eq(x*(1 + x) - x**2, 0)), + ((-1)**x*(-1), True)) + assert p.simplify() == \ + Piecewise((zoo, Eq(x, 0)), ((-1)**(x + 1), True)) + # simplify when there are Eq in conditions + assert Piecewise( + (a, And(Eq(a, 0), Eq(a + b, 0))), (1, True)).simplify( + ) == Piecewise( + (0, And(Eq(a, 0), Eq(b, 0))), (1, True)) + assert Piecewise((2*x*factorial(a)/(factorial(y)*factorial(-y + a)), + Eq(y, 0) & Eq(-y + a, 0)), (2*factorial(a)/(factorial(y)*factorial(-y + + a)), Eq(y, 0) & Eq(-y + a, 1)), (0, True)).simplify( + ) == Piecewise( + (2*x, And(Eq(a, 0), Eq(y, 0))), + (2, And(Eq(a, 1), Eq(y, 0))), + (0, True)) + args = (2, And(Eq(x, 2), Ge(y, 0))), (x, True) + assert Piecewise(*args).simplify() == Piecewise(*args) + args = (1, Eq(x, 0)), (sin(x)/x, True) + assert Piecewise(*args).simplify() == Piecewise(*args) + assert Piecewise((2 + y, And(Eq(x, 2), Eq(y, 0))), (x, True) + ).simplify() == x + # check that x or f(x) are recognized as being Symbol-like for lhs + args = Tuple((1, Eq(x, 0)), (sin(x) + 1 + x, True)) + ans = x + sin(x) + 1 + f = Function('f') + assert Piecewise(*args).simplify() == ans + assert Piecewise(*args.subs(x, f(x))).simplify() == ans.subs(x, f(x)) + + # issue 18634 + d = Symbol("d", integer=True) + n = Symbol("n", integer=True) + t = Symbol("t", positive=True) + expr = Piecewise((-d + 2*n, Eq(1/t, 1)), (t**(1 - 4*n)*t**(4*n - 1)*(-d + 2*n), True)) + assert expr.simplify() == -d + 2*n + + # issue 22747 + p = Piecewise((0, (t < -2) & (t < -1) & (t < 0)), ((t/2 + 1)*(t + + 1)*(t + 2), (t < -1) & (t < 0)), ((S.Half - t/2)*(1 - t)*(t + 1), + (t < -2) & (t < -1) & (t < 1)), ((t + 1)*(-t*(t/2 + 1) + (S.Half + - t/2)*(1 - t)), (t < -2) & (t < -1) & (t < 0) & (t < 1)), ((t + + 1)*((S.Half - t/2)*(1 - t) + (t/2 + 1)*(t + 2)), (t < -1) & (t < + 1)), ((t + 1)*(-t*(t/2 + 1) + (S.Half - t/2)*(1 - t)), (t < -1) & + (t < 0) & (t < 1)), (0, (t < -2) & (t < -1)), ((t/2 + 1)*(t + + 1)*(t + 2), t < -1), ((t + 1)*(-t*(t/2 + 1) + (S.Half - t/2)*(t + + 1)), (t < 0) & ((t < -2) | (t < 0))), ((S.Half - t/2)*(1 - t)*(t + + 1), (t < 1) & ((t < -2) | (t < 1))), (0, True)) + Piecewise((0, + (t < -1) & (t < 0) & (t < 1)), ((1 - t)*(t/2 + S.Half)*(t + 1), + (t < 0) & (t < 1)), ((1 - t)*(1 - t/2)*(2 - t), (t < -1) & (t < + 0) & (t < 2)), ((1 - t)*((1 - t)*(t/2 + S.Half) + (1 - t/2)*(2 - + t)), (t < -1) & (t < 0) & (t < 1) & (t < 2)), ((1 - t)*((1 - + t/2)*(2 - t) + (t/2 + S.Half)*(t + 1)), (t < 0) & (t < 2)), ((1 - + t)*((1 - t)*(t/2 + S.Half) + (1 - t/2)*(2 - t)), (t < 0) & (t < + 1) & (t < 2)), (0, (t < -1) & (t < 0)), ((1 - t)*(t/2 + + S.Half)*(t + 1), t < 0), ((1 - t)*(t*(1 - t/2) + (1 - t)*(t/2 + + S.Half)), (t < 1) & ((t < -1) | (t < 1))), ((1 - t)*(1 - t/2)*(2 + - t), (t < 2) & ((t < -1) | (t < 2))), (0, True)) + assert p.simplify() == Piecewise( + (0, t < -2), ((t + 1)*(t + 2)**2/2, t < -1), (-3*t**3/2 + - 5*t**2/2 + 1, t < 0), (3*t**3/2 - 5*t**2/2 + 1, t < 1), ((1 - + t)*(t - 2)**2/2, t < 2), (0, True)) + + # coverage + nan = Undefined + assert Piecewise((1, x > 3), (2, x < 2), (3, x > 1)).simplify( + ) == Piecewise((1, x > 3), (2, x < 2), (3, True)) + assert Piecewise((1, x < 2), (2, x < 1), (3, True)).simplify( + ) == Piecewise((1, x < 2), (3, True)) + assert Piecewise((1, x > 2)).simplify() == Piecewise((1, x > 2), + (nan, True)) + assert Piecewise((1, (x >= 2) & (x < oo)) + ).simplify() == Piecewise((1, (x >= 2) & (x < oo)), (nan, True)) + assert Piecewise((1, x < 2), (2, (x > 1) & (x < 3)), (3, True) + ). simplify() == Piecewise((1, x < 2), (2, x < 3), (3, True)) + assert Piecewise((1, x < 2), (2, (x <= 3) & (x > 1)), (3, True) + ).simplify() == Piecewise((1, x < 2), (2, x <= 3), (3, True)) + assert Piecewise((1, x < 2), (2, (x > 2) & (x < 3)), (3, True) + ).simplify() == Piecewise((1, x < 2), (2, (x > 2) & (x < 3)), + (3, True)) + assert Piecewise((1, x < 2), (2, (x >= 1) & (x <= 3)), (3, True) + ).simplify() == Piecewise((1, x < 2), (2, x <= 3), (3, True)) + assert Piecewise((1, x < 1), (2, (x >= 2) & (x <= 3)), (3, True) + ).simplify() == Piecewise((1, x < 1), (2, (x >= 2) & (x <= 3)), + (3, True)) + # https://github.com/sympy/sympy/issues/25603 + assert Piecewise((log(x), (x <= 5) & (x > 3)), (x, True) + ).simplify() == Piecewise((log(x), (x <= 5) & (x > 3)), (x, True)) + + assert Piecewise((1, (x >= 1) & (x < 3)), (2, (x > 2) & (x < 4)) + ).simplify() == Piecewise((1, (x >= 1) & (x < 3)), ( + 2, (x >= 3) & (x < 4)), (nan, True)) + assert Piecewise((1, (x >= 1) & (x <= 3)), (2, (x > 2) & (x < 4)) + ).simplify() == Piecewise((1, (x >= 1) & (x <= 3)), ( + 2, (x > 3) & (x < 4)), (nan, True)) + + # involves a symbolic range so cset.inf fails + L = Symbol('L', nonnegative=True) + p = Piecewise((nan, x <= 0), (0, (x >= 0) & (L > x) & (L - x <= 0)), + (x - L/2, (L > x) & (L - x <= 0)), + (L/2 - x, (x >= 0) & (L > x)), + (0, L > x), (nan, True)) + assert p.simplify() == Piecewise( + (nan, x <= 0), (L/2 - x, L > x), (nan, True)) + assert p.subs(L, y).simplify() == Piecewise( + (nan, x <= 0), (-x + y/2, x < Max(0, y)), (0, x < y), (nan, True)) + + +def test_piecewise_solve(): + abs2 = Piecewise((-x, x <= 0), (x, x > 0)) + f = abs2.subs(x, x - 2) + assert solve(f, x) == [2] + assert solve(f - 1, x) == [1, 3] + + f = Piecewise(((x - 2)**2, x >= 0), (1, True)) + assert solve(f, x) == [2] + + g = Piecewise(((x - 5)**5, x >= 4), (f, True)) + assert solve(g, x) == [2, 5] + + g = Piecewise(((x - 5)**5, x >= 4), (f, x < 4)) + assert solve(g, x) == [2, 5] + + g = Piecewise(((x - 5)**5, x >= 2), (f, x < 2)) + assert solve(g, x) == [5] + + g = Piecewise(((x - 5)**5, x >= 2), (f, True)) + assert solve(g, x) == [5] + + g = Piecewise(((x - 5)**5, x >= 2), (f, True), (10, False)) + assert solve(g, x) == [5] + + g = Piecewise(((x - 5)**5, x >= 2), + (-x + 2, x - 2 <= 0), (x - 2, x - 2 > 0)) + assert solve(g, x) == [5] + + # if no symbol is given the piecewise detection must still work + assert solve(Piecewise((x - 2, x > 2), (2 - x, True)) - 3) == [-1, 5] + + f = Piecewise(((x - 2)**2, x >= 0), (0, True)) + raises(NotImplementedError, lambda: solve(f, x)) + + def nona(ans): + return list(filter(lambda x: x is not S.NaN, ans)) + p = Piecewise((x**2 - 4, x < y), (x - 2, True)) + ans = solve(p, x) + assert nona([i.subs(y, -2) for i in ans]) == [2] + assert nona([i.subs(y, 2) for i in ans]) == [-2, 2] + assert nona([i.subs(y, 3) for i in ans]) == [-2, 2] + assert ans == [ + Piecewise((-2, y > -2), (S.NaN, True)), + Piecewise((2, y <= 2), (S.NaN, True)), + Piecewise((2, y > 2), (S.NaN, True))] + + # issue 6060 + absxm3 = Piecewise( + (x - 3, 0 <= x - 3), + (3 - x, 0 > x - 3) + ) + assert solve(absxm3 - y, x) == [ + Piecewise((-y + 3, -y < 0), (S.NaN, True)), + Piecewise((y + 3, y >= 0), (S.NaN, True))] + p = Symbol('p', positive=True) + assert solve(absxm3 - p, x) == [-p + 3, p + 3] + + # issue 6989 + f = Function('f') + assert solve(Eq(-f(x), Piecewise((1, x > 0), (0, True))), f(x)) == \ + [Piecewise((-1, x > 0), (0, True))] + + # issue 8587 + f = Piecewise((2*x**2, And(0 < x, x < 1)), (2, True)) + assert solve(f - 1) == [1/sqrt(2)] + + +def test_piecewise_fold(): + p = Piecewise((x, x < 1), (1, 1 <= x)) + + assert piecewise_fold(x*p) == Piecewise((x**2, x < 1), (x, 1 <= x)) + assert piecewise_fold(p + p) == Piecewise((2*x, x < 1), (2, 1 <= x)) + assert piecewise_fold(Piecewise((1, x < 0), (2, True)) + + Piecewise((10, x < 0), (-10, True))) == \ + Piecewise((11, x < 0), (-8, True)) + + p1 = Piecewise((0, x < 0), (x, x <= 1), (0, True)) + p2 = Piecewise((0, x < 0), (1 - x, x <= 1), (0, True)) + + p = 4*p1 + 2*p2 + assert integrate( + piecewise_fold(p), (x, -oo, oo)) == integrate(2*x + 2, (x, 0, 1)) + + assert piecewise_fold( + Piecewise((1, y <= 0), (-Piecewise((2, y >= 0)), True) + )) == Piecewise((1, y <= 0), (-2, y >= 0)) + + assert piecewise_fold(Piecewise((x, ITE(x > 0, y < 1, y > 1))) + ) == Piecewise((x, ((x <= 0) | (y < 1)) & ((x > 0) | (y > 1)))) + + a, b = (Piecewise((2, Eq(x, 0)), (0, True)), + Piecewise((x, Eq(-x + y, 0)), (1, Eq(-x + y, 1)), (0, True))) + assert piecewise_fold(Mul(a, b, evaluate=False) + ) == piecewise_fold(Mul(b, a, evaluate=False)) + + +def test_piecewise_fold_piecewise_in_cond(): + p1 = Piecewise((cos(x), x < 0), (0, True)) + p2 = Piecewise((0, Eq(p1, 0)), (p1 / Abs(p1), True)) + assert p2.subs(x, -pi/2) == 0 + assert p2.subs(x, 1) == 0 + assert p2.subs(x, -pi/4) == 1 + p4 = Piecewise((0, Eq(p1, 0)), (1,True)) + ans = piecewise_fold(p4) + for i in range(-1, 1): + assert ans.subs(x, i) == p4.subs(x, i) + + r1 = 1 < Piecewise((1, x < 1), (3, True)) + ans = piecewise_fold(r1) + for i in range(2): + assert ans.subs(x, i) == r1.subs(x, i) + + p5 = Piecewise((1, x < 0), (3, True)) + p6 = Piecewise((1, x < 1), (3, True)) + p7 = Piecewise((1, p5 < p6), (0, True)) + ans = piecewise_fold(p7) + for i in range(-1, 2): + assert ans.subs(x, i) == p7.subs(x, i) + + +def test_piecewise_fold_piecewise_in_cond_2(): + p1 = Piecewise((cos(x), x < 0), (0, True)) + p2 = Piecewise((0, Eq(p1, 0)), (1 / p1, True)) + p3 = Piecewise( + (0, (x >= 0) | Eq(cos(x), 0)), + (1/cos(x), x < 0), + (zoo, True)) # redundant b/c all x are already covered + assert(piecewise_fold(p2) == p3) + + +def test_piecewise_fold_expand(): + p1 = Piecewise((1, Interval(0, 1, False, True).contains(x)), (0, True)) + + p2 = piecewise_fold(expand((1 - x)*p1)) + cond = ((x >= 0) & (x < 1)) + assert piecewise_fold(expand((1 - x)*p1), evaluate=False + ) == Piecewise((1 - x, cond), (-x, cond), (1, cond), (0, True), evaluate=False) + assert piecewise_fold(expand((1 - x)*p1), evaluate=None + ) == Piecewise((1 - x, cond), (0, True)) + assert p2 == Piecewise((1 - x, cond), (0, True)) + assert p2 == expand(piecewise_fold((1 - x)*p1)) + + +def test_piecewise_duplicate(): + p = Piecewise((x, x < -10), (x**2, x <= -1), (x, 1 < x)) + assert p == Piecewise(*p.args) + + +def test_doit(): + p1 = Piecewise((x, x < 1), (x**2, -1 <= x), (x, 3 < x)) + p2 = Piecewise((x, x < 1), (Integral(2 * x), -1 <= x), (x, 3 < x)) + assert p2.doit() == p1 + assert p2.doit(deep=False) == p2 + # issue 17165 + p1 = Sum(y**x, (x, -1, oo)).doit() + assert p1.doit() == p1 + + +def test_piecewise_interval(): + p1 = Piecewise((x, Interval(0, 1).contains(x)), (0, True)) + assert p1.subs(x, -0.5) == 0 + assert p1.subs(x, 0.5) == 0.5 + assert p1.diff(x) == Piecewise((1, Interval(0, 1).contains(x)), (0, True)) + assert integrate(p1, x) == Piecewise( + (0, x <= 0), + (x**2/2, x <= 1), + (S.Half, True)) + + +def test_piecewise_exclusive(): + p = Piecewise((0, x < 0), (S.Half, x <= 0), (1, True)) + assert piecewise_exclusive(p) == Piecewise((0, x < 0), (S.Half, Eq(x, 0)), + (1, x > 0), evaluate=False) + assert piecewise_exclusive(p + 2) == Piecewise((0, x < 0), (S.Half, Eq(x, 0)), + (1, x > 0), evaluate=False) + 2 + assert piecewise_exclusive(Piecewise((1, y <= 0), + (-Piecewise((2, y >= 0)), True))) == \ + Piecewise((1, y <= 0), + (-Piecewise((2, y >= 0), + (S.NaN, y < 0), evaluate=False), y > 0), evaluate=False) + assert piecewise_exclusive(Piecewise((1, x > y))) == Piecewise((1, x > y), + (S.NaN, x <= y), + evaluate=False) + assert piecewise_exclusive(Piecewise((1, x > y)), + skip_nan=True) == Piecewise((1, x > y)) + + xr, yr = symbols('xr, yr', real=True) + + p1 = Piecewise((1, xr < 0), (2, True), evaluate=False) + p1x = Piecewise((1, xr < 0), (2, xr >= 0), evaluate=False) + + p2 = Piecewise((p1, yr < 0), (3, True), evaluate=False) + p2x = Piecewise((p1, yr < 0), (3, yr >= 0), evaluate=False) + p2xx = Piecewise((p1x, yr < 0), (3, yr >= 0), evaluate=False) + + assert piecewise_exclusive(p2) == p2xx + assert piecewise_exclusive(p2, deep=False) == p2x + + +def test_piecewise_collapse(): + assert Piecewise((x, True)) == x + a = x < 1 + assert Piecewise((x, a), (x + 1, a)) == Piecewise((x, a)) + assert Piecewise((x, a), (x + 1, a.reversed)) == Piecewise((x, a)) + b = x < 5 + def canonical(i): + if isinstance(i, Piecewise): + return Piecewise(*i.args) + return i + for args in [ + ((1, a), (Piecewise((2, a), (3, b)), b)), + ((1, a), (Piecewise((2, a), (3, b.reversed)), b)), + ((1, a), (Piecewise((2, a), (3, b)), b), (4, True)), + ((1, a), (Piecewise((2, a), (3, b), (4, True)), b)), + ((1, a), (Piecewise((2, a), (3, b), (4, True)), b), (5, True))]: + for i in (0, 2, 10): + assert canonical( + Piecewise(*args, evaluate=False).subs(x, i) + ) == canonical(Piecewise(*args).subs(x, i)) + r1, r2, r3, r4 = symbols('r1:5') + a = x < r1 + b = x < r2 + c = x < r3 + d = x < r4 + assert Piecewise((1, a), (Piecewise( + (2, a), (3, b), (4, c)), b), (5, c) + ) == Piecewise((1, a), (3, b), (5, c)) + assert Piecewise((1, a), (Piecewise( + (2, a), (3, b), (4, c), (6, True)), c), (5, d) + ) == Piecewise((1, a), (Piecewise( + (3, b), (4, c)), c), (5, d)) + assert Piecewise((1, Or(a, d)), (Piecewise( + (2, d), (3, b), (4, c)), b), (5, c) + ) == Piecewise((1, Or(a, d)), (Piecewise( + (2, d), (3, b)), b), (5, c)) + assert Piecewise((1, c), (2, ~c), (3, S.true) + ) == Piecewise((1, c), (2, S.true)) + assert Piecewise((1, c), (2, And(~c, b)), (3,True) + ) == Piecewise((1, c), (2, b), (3, True)) + assert Piecewise((1, c), (2, Or(~c, b)), (3,True) + ).subs(dict(zip((r1, r2, r3, r4, x), (1, 2, 3, 4, 3.5)))) == 2 + assert Piecewise((1, c), (2, ~c)) == Piecewise((1, c), (2, True)) + + +def test_piecewise_lambdify(): + p = Piecewise( + (x**2, x < 0), + (x, Interval(0, 1, False, True).contains(x)), + (2 - x, x >= 1), + (0, True) + ) + + f = lambdify(x, p) + assert f(-2.0) == 4.0 + assert f(0.0) == 0.0 + assert f(0.5) == 0.5 + assert f(2.0) == 0.0 + + +def test_piecewise_series(): + from sympy.series.order import O + p1 = Piecewise((sin(x), x < 0), (cos(x), x > 0)) + p2 = Piecewise((x + O(x**2), x < 0), (1 + O(x**2), x > 0)) + assert p1.nseries(x, n=2) == p2 + + +def test_piecewise_as_leading_term(): + p1 = Piecewise((1/x, x > 1), (0, True)) + p2 = Piecewise((x, x > 1), (0, True)) + p3 = Piecewise((1/x, x > 1), (x, True)) + p4 = Piecewise((x, x > 1), (1/x, True)) + p5 = Piecewise((1/x, x > 1), (x, True)) + p6 = Piecewise((1/x, x < 1), (x, True)) + p7 = Piecewise((x, x < 1), (1/x, True)) + p8 = Piecewise((x, x > 1), (1/x, True)) + assert p1.as_leading_term(x) == 0 + assert p2.as_leading_term(x) == 0 + assert p3.as_leading_term(x) == x + assert p4.as_leading_term(x) == 1/x + assert p5.as_leading_term(x) == x + assert p6.as_leading_term(x) == 1/x + assert p7.as_leading_term(x) == x + assert p8.as_leading_term(x) == 1/x + + +def test_piecewise_complex(): + p1 = Piecewise((2, x < 0), (1, 0 <= x)) + p2 = Piecewise((2*I, x < 0), (I, 0 <= x)) + p3 = Piecewise((I*x, x > 1), (1 + I, True)) + p4 = Piecewise((-I*conjugate(x), x > 1), (1 - I, True)) + + assert conjugate(p1) == p1 + assert conjugate(p2) == piecewise_fold(-p2) + assert conjugate(p3) == p4 + + assert p1.is_imaginary is False + assert p1.is_real is True + assert p2.is_imaginary is True + assert p2.is_real is False + assert p3.is_imaginary is None + assert p3.is_real is None + + assert p1.as_real_imag() == (p1, 0) + assert p2.as_real_imag() == (0, -I*p2) + + +def test_conjugate_transpose(): + A, B = symbols("A B", commutative=False) + p = Piecewise((A*B**2, x > 0), (A**2*B, True)) + assert p.adjoint() == \ + Piecewise((adjoint(A*B**2), x > 0), (adjoint(A**2*B), True)) + assert p.conjugate() == \ + Piecewise((conjugate(A*B**2), x > 0), (conjugate(A**2*B), True)) + assert p.transpose() == \ + Piecewise((transpose(A*B**2), x > 0), (transpose(A**2*B), True)) + + +def test_piecewise_evaluate(): + assert Piecewise((x, True)) == x + assert Piecewise((x, True), evaluate=True) == x + assert Piecewise((1, Eq(1, x))).args == ((1, Eq(x, 1)),) + assert Piecewise((1, Eq(1, x)), evaluate=False).args == ( + (1, Eq(1, x)),) + # like the additive and multiplicative identities that + # cannot be kept in Add/Mul, we also do not keep a single True + p = Piecewise((x, True), evaluate=False) + assert p == x + + +def test_as_expr_set_pairs(): + assert Piecewise((x, x > 0), (-x, x <= 0)).as_expr_set_pairs() == \ + [(x, Interval(0, oo, True, True)), (-x, Interval(-oo, 0))] + + assert Piecewise(((x - 2)**2, x >= 0), (0, True)).as_expr_set_pairs() == \ + [((x - 2)**2, Interval(0, oo)), (0, Interval(-oo, 0, True, True))] + + +def test_S_srepr_is_identity(): + p = Piecewise((10, Eq(x, 0)), (12, True)) + q = S(srepr(p)) + assert p == q + + +def test_issue_12587(): + # sort holes into intervals + p = Piecewise((1, x > 4), (2, Not((x <= 3) & (x > -1))), (3, True)) + assert p.integrate((x, -5, 5)) == 23 + p = Piecewise((1, x > 1), (2, x < y), (3, True)) + lim = x, -3, 3 + ans = p.integrate(lim) + for i in range(-1, 3): + assert ans.subs(y, i) == p.subs(y, i).integrate(lim) + + +def test_issue_11045(): + assert integrate(1/(x*sqrt(x**2 - 1)), (x, 1, 2)) == pi/3 + + # handle And with Or arguments + assert Piecewise((1, And(Or(x < 1, x > 3), x < 2)), (0, True) + ).integrate((x, 0, 3)) == 1 + + # hidden false + assert Piecewise((1, x > 1), (2, x > x + 1), (3, True) + ).integrate((x, 0, 3)) == 5 + # targetcond is Eq + assert Piecewise((1, x > 1), (2, Eq(1, x)), (3, True) + ).integrate((x, 0, 4)) == 6 + # And has Relational needing to be solved + assert Piecewise((1, And(2*x > x + 1, x < 2)), (0, True) + ).integrate((x, 0, 3)) == 1 + # Or has Relational needing to be solved + assert Piecewise((1, Or(2*x > x + 2, x < 1)), (0, True) + ).integrate((x, 0, 3)) == 2 + # ignore hidden false (handled in canonicalization) + assert Piecewise((1, x > 1), (2, x > x + 1), (3, True) + ).integrate((x, 0, 3)) == 5 + # watch for hidden True Piecewise + assert Piecewise((2, Eq(1 - x, x*(1/x - 1))), (0, True) + ).integrate((x, 0, 3)) == 6 + + # overlapping conditions of targetcond are recognized and ignored; + # the condition x > 3 will be pre-empted by the first condition + assert Piecewise((1, Or(x < 1, x > 2)), (2, x > 3), (3, True) + ).integrate((x, 0, 4)) == 6 + + # convert Ne to Or + assert Piecewise((1, Ne(x, 0)), (2, True) + ).integrate((x, -1, 1)) == 2 + + # no default but well defined + assert Piecewise((x, (x > 1) & (x < 3)), (1, (x < 4)) + ).integrate((x, 1, 4)) == 5 + + p = Piecewise((x, (x > 1) & (x < 3)), (1, (x < 4))) + nan = Undefined + i = p.integrate((x, 1, y)) + assert i == Piecewise( + (y - 1, y < 1), + (Min(3, y)**2/2 - Min(3, y) + Min(4, y) - S.Half, + y <= Min(4, y)), + (nan, True)) + assert p.integrate((x, 1, -1)) == i.subs(y, -1) + assert p.integrate((x, 1, 4)) == 5 + assert p.integrate((x, 1, 5)) is nan + + # handle Not + p = Piecewise((1, x > 1), (2, Not(And(x > 1, x< 3))), (3, True)) + assert p.integrate((x, 0, 3)) == 4 + + # handle updating of int_expr when there is overlap + p = Piecewise( + (1, And(5 > x, x > 1)), + (2, Or(x < 3, x > 7)), + (4, x < 8)) + assert p.integrate((x, 0, 10)) == 20 + + # And with Eq arg handling + assert Piecewise((1, x < 1), (2, And(Eq(x, 3), x > 1)) + ).integrate((x, 0, 3)) is S.NaN + assert Piecewise((1, x < 1), (2, And(Eq(x, 3), x > 1)), (3, True) + ).integrate((x, 0, 3)) == 7 + assert Piecewise((1, x < 0), (2, And(Eq(x, 3), x < 1)), (3, True) + ).integrate((x, -1, 1)) == 4 + # middle condition doesn't matter: it's a zero width interval + assert Piecewise((1, x < 1), (2, Eq(x, 3) & (y < x)), (3, True) + ).integrate((x, 0, 3)) == 7 + + +def test_holes(): + nan = Undefined + assert Piecewise((1, x < 2)).integrate(x) == Piecewise( + (x, x < 2), (nan, True)) + assert Piecewise((1, And(x > 1, x < 2))).integrate(x) == Piecewise( + (nan, x < 1), (x, x < 2), (nan, True)) + assert Piecewise((1, And(x > 1, x < 2))).integrate((x, 0, 3)) is nan + assert Piecewise((1, And(x > 0, x < 4))).integrate((x, 1, 3)) == 2 + + # this also tests that the integrate method is used on non-Piecwise + # arguments in _eval_integral + A, B = symbols("A B") + a, b = symbols('a b', real=True) + assert Piecewise((A, And(x < 0, a < 1)), (B, Or(x < 1, a > 2)) + ).integrate(x) == Piecewise( + (B*x, (a > 2)), + (Piecewise((A*x, x < 0), (B*x, x < 1), (nan, True)), a < 1), + (Piecewise((B*x, x < 1), (nan, True)), True)) + + +def test_issue_11922(): + def f(x): + return Piecewise((0, x < -1), (1 - x**2, x < 1), (0, True)) + autocorr = lambda k: ( + f(x) * f(x + k)).integrate((x, -1, 1)) + assert autocorr(1.9) > 0 + k = symbols('k') + good_autocorr = lambda k: ( + (1 - x**2) * f(x + k)).integrate((x, -1, 1)) + a = good_autocorr(k) + assert a.subs(k, 3) == 0 + k = symbols('k', positive=True) + a = good_autocorr(k) + assert a.subs(k, 3) == 0 + assert Piecewise((0, x < 1), (10, (x >= 1)) + ).integrate() == Piecewise((0, x < 1), (10*x - 10, True)) + + +def test_issue_5227(): + f = 0.0032513612725229*Piecewise((0, x < -80.8461538461539), + (-0.0160799238820171*x + 1.33215984776403, x < 2), + (Piecewise((0.3, x > 123), (0.7, True)) + + Piecewise((0.4, x > 2), (0.6, True)), x <= + 123), (-0.00817409766454352*x + 2.10541401273885, x < + 380.571428571429), (0, True)) + i = integrate(f, (x, -oo, oo)) + assert i == Integral(f, (x, -oo, oo)).doit() + assert str(i) == '1.00195081676351' + assert Piecewise((1, x - y < 0), (0, True) + ).integrate(y) == Piecewise((0, y <= x), (-x + y, True)) + + +def test_issue_10137(): + a = Symbol('a', real=True) + b = Symbol('b', real=True) + x = Symbol('x', real=True) + y = Symbol('y', real=True) + p0 = Piecewise((0, Or(x < a, x > b)), (1, True)) + p1 = Piecewise((0, Or(a > x, b < x)), (1, True)) + assert integrate(p0, (x, y, oo)) == integrate(p1, (x, y, oo)) + p3 = Piecewise((1, And(0 < x, x < a)), (0, True)) + p4 = Piecewise((1, And(a > x, x > 0)), (0, True)) + ip3 = integrate(p3, x) + assert ip3 == Piecewise( + (0, x <= 0), + (x, x <= Max(0, a)), + (Max(0, a), True)) + ip4 = integrate(p4, x) + assert ip4 == ip3 + assert p3.integrate((x, 2, 4)) == Min(4, Max(2, a)) - 2 + assert p4.integrate((x, 2, 4)) == Min(4, Max(2, a)) - 2 + + +def test_stackoverflow_43852159(): + f = lambda x: Piecewise((1, (x >= -1) & (x <= 1)), (0, True)) + Conv = lambda x: integrate(f(x - y)*f(y), (y, -oo, +oo)) + cx = Conv(x) + assert cx.subs(x, -1.5) == cx.subs(x, 1.5) + assert cx.subs(x, 3) == 0 + assert piecewise_fold(f(x - y)*f(y)) == Piecewise( + (1, (y >= -1) & (y <= 1) & (x - y >= -1) & (x - y <= 1)), + (0, True)) + + +def test_issue_12557(): + ''' + # 3200 seconds to compute the fourier part of issue + import sympy as sym + x,y,z,t = sym.symbols('x y z t') + k = sym.symbols("k", integer=True) + fourier = sym.fourier_series(sym.cos(k*x)*sym.sqrt(x**2), + (x, -sym.pi, sym.pi)) + assert fourier == FourierSeries( + sqrt(x**2)*cos(k*x), (x, -pi, pi), (Piecewise((pi**2, + Eq(k, 0)), (2*(-1)**k/k**2 - 2/k**2, True))/(2*pi), + SeqFormula(Piecewise((pi**2, (Eq(_n, 0) & Eq(k, 0)) | (Eq(_n, 0) & + Eq(_n, k) & Eq(k, 0)) | (Eq(_n, 0) & Eq(k, 0) & Eq(_n, -k)) | (Eq(_n, + 0) & Eq(_n, k) & Eq(k, 0) & Eq(_n, -k))), (pi**2/2, Eq(_n, k) | Eq(_n, + -k) | (Eq(_n, 0) & Eq(_n, k)) | (Eq(_n, k) & Eq(k, 0)) | (Eq(_n, 0) & + Eq(_n, -k)) | (Eq(_n, k) & Eq(_n, -k)) | (Eq(k, 0) & Eq(_n, -k)) | + (Eq(_n, 0) & Eq(_n, k) & Eq(_n, -k)) | (Eq(_n, k) & Eq(k, 0) & Eq(_n, + -k))), ((-1)**k*pi**2*_n**3*sin(pi*_n)/(pi*_n**4 - 2*pi*_n**2*k**2 + + pi*k**4) - (-1)**k*pi**2*_n**3*sin(pi*_n)/(-pi*_n**4 + 2*pi*_n**2*k**2 + - pi*k**4) + (-1)**k*pi*_n**2*cos(pi*_n)/(pi*_n**4 - 2*pi*_n**2*k**2 + + pi*k**4) - (-1)**k*pi*_n**2*cos(pi*_n)/(-pi*_n**4 + 2*pi*_n**2*k**2 - + pi*k**4) - (-1)**k*pi**2*_n*k**2*sin(pi*_n)/(pi*_n**4 - + 2*pi*_n**2*k**2 + pi*k**4) + + (-1)**k*pi**2*_n*k**2*sin(pi*_n)/(-pi*_n**4 + 2*pi*_n**2*k**2 - + pi*k**4) + (-1)**k*pi*k**2*cos(pi*_n)/(pi*_n**4 - 2*pi*_n**2*k**2 + + pi*k**4) - (-1)**k*pi*k**2*cos(pi*_n)/(-pi*_n**4 + 2*pi*_n**2*k**2 - + pi*k**4) - (2*_n**2 + 2*k**2)/(_n**4 - 2*_n**2*k**2 + k**4), + True))*cos(_n*x)/pi, (_n, 1, oo)), SeqFormula(0, (_k, 1, oo)))) + ''' + x = symbols("x", real=True) + k = symbols('k', integer=True, finite=True) + abs2 = lambda x: Piecewise((-x, x <= 0), (x, x > 0)) + assert integrate(abs2(x), (x, -pi, pi)) == pi**2 + func = cos(k*x)*sqrt(x**2) + assert integrate(func, (x, -pi, pi)) == Piecewise( + (2*(-1)**k/k**2 - 2/k**2, Ne(k, 0)), (pi**2, True)) + +def test_issue_6900(): + from itertools import permutations + t0, t1, T, t = symbols('t0, t1 T t') + f = Piecewise((0, t < t0), (x, And(t0 <= t, t < t1)), (0, t >= t1)) + g = f.integrate(t) + assert g == Piecewise( + (0, t <= t0), + (t*x - t0*x, t <= Max(t0, t1)), + (-t0*x + x*Max(t0, t1), True)) + for i in permutations(range(2)): + reps = dict(zip((t0,t1), i)) + for tt in range(-1,3): + assert (g.xreplace(reps).subs(t,tt) == + f.xreplace(reps).integrate(t).subs(t,tt)) + lim = Tuple(t, t0, T) + g = f.integrate(lim) + ans = Piecewise( + (-t0*x + x*Min(T, Max(t0, t1)), T > t0), + (0, True)) + for i in permutations(range(3)): + reps = dict(zip((t0,t1,T), i)) + tru = f.xreplace(reps).integrate(lim.xreplace(reps)) + assert tru == ans.xreplace(reps) + assert g == ans + + +def test_issue_10122(): + assert solve(abs(x) + abs(x - 1) - 1 > 0, x + ) == Or(And(-oo < x, x < S.Zero), And(S.One < x, x < oo)) + + +def test_issue_4313(): + u = Piecewise((0, x <= 0), (1, x >= a), (x/a, True)) + e = (u - u.subs(x, y))**2/(x - y)**2 + M = Max(0, a) + assert integrate(e, x).expand() == Piecewise( + (Piecewise( + (0, x <= 0), + (-y**2/(a**2*x - a**2*y) + x/a**2 - 2*y*log(-y)/a**2 + + 2*y*log(x - y)/a**2 - y/a**2, x <= M), + (-y**2/(-a**2*y + a**2*M) + 1/(-y + M) - + 1/(x - y) - 2*y*log(-y)/a**2 + 2*y*log(-y + + M)/a**2 - y/a**2 + M/a**2, True)), + ((a <= y) & (y <= 0)) | ((y <= 0) & (y > -oo))), + (Piecewise( + (-1/(x - y), x <= 0), + (-a**2/(a**2*x - a**2*y) + 2*a*y/(a**2*x - a**2*y) - + y**2/(a**2*x - a**2*y) + 2*log(-y)/a - 2*log(x - y)/a + + 2/a + x/a**2 - 2*y*log(-y)/a**2 + 2*y*log(x - y)/a**2 - + y/a**2, x <= M), + (-a**2/(-a**2*y + a**2*M) + 2*a*y/(-a**2*y + + a**2*M) - y**2/(-a**2*y + a**2*M) + + 2*log(-y)/a - 2*log(-y + M)/a + 2/a - + 2*y*log(-y)/a**2 + 2*y*log(-y + M)/a**2 - + y/a**2 + M/a**2, True)), + a <= y), + (Piecewise( + (-y**2/(a**2*x - a**2*y), x <= 0), + (x/a**2 + y/a**2, x <= M), + (a**2/(-a**2*y + a**2*M) - + a**2/(a**2*x - a**2*y) - 2*a*y/(-a**2*y + a**2*M) + + 2*a*y/(a**2*x - a**2*y) + y**2/(-a**2*y + a**2*M) - + y**2/(a**2*x - a**2*y) + y/a**2 + M/a**2, True)), + True)) + + +def test__intervals(): + assert Piecewise((x + 2, Eq(x, 3)))._intervals(x) == (True, []) + assert Piecewise( + (1, x > x + 1), + (Piecewise((1, x < x + 1)), 2*x < 2*x + 1), + (1, True))._intervals(x) == (True, [(-oo, oo, 1, 1)]) + assert Piecewise((1, Ne(x, I)), (0, True))._intervals(x) == (True, + [(-oo, oo, 1, 0)]) + assert Piecewise((-cos(x), sin(x) >= 0), (cos(x), True) + )._intervals(x) == (True, + [(0, pi, -cos(x), 0), (-oo, oo, cos(x), 1)]) + # the following tests that duplicates are removed and that non-Eq + # generated zero-width intervals are removed + assert Piecewise((1, Abs(x**(-2)) > 1), (0, True) + )._intervals(x) == (True, + [(-1, 0, 1, 0), (0, 1, 1, 0), (-oo, oo, 0, 1)]) + + +def test_containment(): + a, b, c, d, e = [1, 2, 3, 4, 5] + p = (Piecewise((d, x > 1), (e, True))* + Piecewise((a, Abs(x - 1) < 1), (b, Abs(x - 2) < 2), (c, True))) + assert p.integrate(x).diff(x) == Piecewise( + (c*e, x <= 0), + (a*e, x <= 1), + (a*d, x < 2), # this is what we want to get right + (b*d, x < 4), + (c*d, True)) + + +def test_piecewise_with_DiracDelta(): + d1 = DiracDelta(x - 1) + assert integrate(d1, (x, -oo, oo)) == 1 + assert integrate(d1, (x, 0, 2)) == 1 + assert Piecewise((d1, Eq(x, 2)), (0, True)).integrate(x) == 0 + assert Piecewise((d1, x < 2), (0, True)).integrate(x) == Piecewise( + (Heaviside(x - 1), x < 2), (1, True)) + # TODO raise error if function is discontinuous at limit of + # integration, e.g. integrate(d1, (x, -2, 1)) or Piecewise( + # (d1, Eq(x, 1) + + +def test_issue_10258(): + assert Piecewise((0, x < 1), (1, True)).is_zero is None + assert Piecewise((-1, x < 1), (1, True)).is_zero is False + a = Symbol('a', zero=True) + assert Piecewise((0, x < 1), (a, True)).is_zero + assert Piecewise((1, x < 1), (a, x < 3)).is_zero is None + a = Symbol('a') + assert Piecewise((0, x < 1), (a, True)).is_zero is None + assert Piecewise((0, x < 1), (1, True)).is_nonzero is None + assert Piecewise((1, x < 1), (2, True)).is_nonzero + assert Piecewise((0, x < 1), (oo, True)).is_finite is None + assert Piecewise((0, x < 1), (1, True)).is_finite + b = Basic() + assert Piecewise((b, x < 1)).is_finite is None + + # 10258 + c = Piecewise((1, x < 0), (2, True)) < 3 + assert c != True + assert piecewise_fold(c) == True + + +def test_issue_10087(): + a, b = Piecewise((x, x > 1), (2, True)), Piecewise((x, x > 3), (3, True)) + m = a*b + f = piecewise_fold(m) + for i in (0, 2, 4): + assert m.subs(x, i) == f.subs(x, i) + m = a + b + f = piecewise_fold(m) + for i in (0, 2, 4): + assert m.subs(x, i) == f.subs(x, i) + + +def test_issue_8919(): + c = symbols('c:5') + x = symbols("x") + f1 = Piecewise((c[1], x < 1), (c[2], True)) + f2 = Piecewise((c[3], x < Rational(1, 3)), (c[4], True)) + assert integrate(f1*f2, (x, 0, 2) + ) == c[1]*c[3]/3 + 2*c[1]*c[4]/3 + c[2]*c[4] + f1 = Piecewise((0, x < 1), (2, True)) + f2 = Piecewise((3, x < 2), (0, True)) + assert integrate(f1*f2, (x, 0, 3)) == 6 + + y = symbols("y", positive=True) + a, b, c, x, z = symbols("a,b,c,x,z", real=True) + I = Integral(Piecewise( + (0, (x >= y) | (x < 0) | (b > c)), + (a, True)), (x, 0, z)) + ans = I.doit() + assert ans == Piecewise((0, b > c), (a*Min(y, z) - a*Min(0, z), True)) + for cond in (True, False): + for yy in range(1, 3): + for zz in range(-yy, 0, yy): + reps = [(b > c, cond), (y, yy), (z, zz)] + assert ans.subs(reps) == I.subs(reps).doit() + + +def test_unevaluated_integrals(): + f = Function('f') + p = Piecewise((1, Eq(f(x) - 1, 0)), (2, x - 10 < 0), (0, True)) + assert p.integrate(x) == Integral(p, x) + assert p.integrate((x, 0, 5)) == Integral(p, (x, 0, 5)) + # test it by replacing f(x) with x%2 which will not + # affect the answer: the integrand is essentially 2 over + # the domain of integration + assert Integral(p, (x, 0, 5)).subs(f(x), x%2).n() == 10.0 + + # this is a test of using _solve_inequality when + # solve_univariate_inequality fails + assert p.integrate(y) == Piecewise( + (y, Eq(f(x), 1) | ((x < 10) & Eq(f(x), 1))), + (2*y, (x > -oo) & (x < 10)), (0, True)) + + +def test_conditions_as_alternate_booleans(): + a, b, c = symbols('a:c') + assert Piecewise((x, Piecewise((y < 1, x > 0), (y > 1, True))) + ) == Piecewise((x, ITE(x > 0, y < 1, y > 1))) + + +def test_Piecewise_rewrite_as_ITE(): + a, b, c, d = symbols('a:d') + + def _ITE(*args): + return Piecewise(*args).rewrite(ITE) + + assert _ITE((a, x < 1), (b, x >= 1)) == ITE(x < 1, a, b) + assert _ITE((a, x < 1), (b, x < oo)) == ITE(x < 1, a, b) + assert _ITE((a, x < 1), (b, Or(y < 1, x < oo)), (c, y > 0) + ) == ITE(x < 1, a, b) + assert _ITE((a, x < 1), (b, True)) == ITE(x < 1, a, b) + assert _ITE((a, x < 1), (b, x < 2), (c, True) + ) == ITE(x < 1, a, ITE(x < 2, b, c)) + assert _ITE((a, x < 1), (b, y < 2), (c, True) + ) == ITE(x < 1, a, ITE(y < 2, b, c)) + assert _ITE((a, x < 1), (b, x < oo), (c, y < 1) + ) == ITE(x < 1, a, b) + assert _ITE((a, x < 1), (c, y < 1), (b, x < oo), (d, True) + ) == ITE(x < 1, a, ITE(y < 1, c, b)) + assert _ITE((a, x < 0), (b, Or(x < oo, y < 1)) + ) == ITE(x < 0, a, b) + raises(TypeError, lambda: _ITE((x + 1, x < 1), (x, True))) + # if `a` in the following were replaced with y then the coverage + # is complete but something other than as_set would need to be + # used to detect this + raises(NotImplementedError, lambda: _ITE((x, x < y), (y, x >= a))) + raises(ValueError, lambda: _ITE((a, x < 2), (b, x > 3))) + + +def test_Piecewise_replace_relational_27538(): + x, y = symbols('x, y') + p1 = Piecewise( + (0, Eq(x, True)), + (1, True), + ) + p2 = p1.xreplace({x: y < 1}) + assert p2.subs(y, 0) == 0 + assert p2.subs(y, 1) == 1 + + +def test_issue_14052(): + assert integrate(abs(sin(x)), (x, 0, 2*pi)) == 4 + + +def test_issue_14240(): + assert piecewise_fold( + Piecewise((1, a), (2, b), (4, True)) + + Piecewise((8, a), (16, True)) + ) == Piecewise((9, a), (18, b), (20, True)) + assert piecewise_fold( + Piecewise((2, a), (3, b), (5, True)) * + Piecewise((7, a), (11, True)) + ) == Piecewise((14, a), (33, b), (55, True)) + # these will hang if naive folding is used + assert piecewise_fold(Add(*[ + Piecewise((i, a), (0, True)) for i in range(40)]) + ) == Piecewise((780, a), (0, True)) + assert piecewise_fold(Mul(*[ + Piecewise((i, a), (0, True)) for i in range(1, 41)]) + ) == Piecewise((factorial(40), a), (0, True)) + + +def test_issue_14787(): + x = Symbol('x') + f = Piecewise((x, x < 1), ((S(58) / 7), True)) + assert str(f.evalf()) == "Piecewise((x, x < 1), (8.28571428571429, True))" + +def test_issue_21481(): + b, e = symbols('b e') + C = Piecewise( + (2, + ((b > 1) & (e > 0)) | + ((b > 0) & (b < 1) & (e < 0)) | + ((e >= 2) & (b < -1) & Eq(Mod(e, 2), 0)) | + ((e <= -2) & (b > -1) & (b < 0) & Eq(Mod(e, 2), 0))), + (S.Half, + ((b > 1) & (e < 0)) | + ((b > 0) & (e > 0) & (b < 1)) | + ((e <= -2) & (b < -1) & Eq(Mod(e, 2), 0)) | + ((e >= 2) & (b > -1) & (b < 0) & Eq(Mod(e, 2), 0))), + (-S.Half, + Eq(Mod(e, 2), 1) & + (((e <= -1) & (b < -1)) | ((e >= 1) & (b > -1) & (b < 0)))), + (-2, + ((e >= 1) & (b < -1) & Eq(Mod(e, 2), 1)) | + ((e <= -1) & (b > -1) & (b < 0) & Eq(Mod(e, 2), 1))) + ) + A = Piecewise( + (1, Eq(b, 1) | Eq(e, 0) | (Eq(b, -1) & Eq(Mod(e, 2), 0))), + (0, Eq(b, 0) & (e > 0)), + (-1, Eq(b, -1) & Eq(Mod(e, 2), 1)), + (C, Eq(im(b), 0) & Eq(im(e), 0)) + ) + + B = piecewise_fold(A) + sa = A.simplify() + sb = B.simplify() + v = (-2, -1, -S.Half, 0, S.Half, 1, 2) + for i in v: + for j in v: + r = {b:i, e:j} + ok = [k.xreplace(r) for k in (A, B, sa, sb)] + assert len(set(ok)) == 1 + + +def test_issue_8458(): + x, y = symbols('x y') + # Original issue + p1 = Piecewise((0, Eq(x, 0)), (sin(x), True)) + assert p1.simplify() == sin(x) + # Slightly larger variant + p2 = Piecewise((x, Eq(x, 0)), (4*x + (y-2)**4, Eq(x, 0) & Eq(x+y, 2)), (sin(x), True)) + assert p2.simplify() == sin(x) + # Test for problem highlighted during review + p3 = Piecewise((x+1, Eq(x, -1)), (4*x + (y-2)**4, Eq(x, 0) & Eq(x+y, 2)), (sin(x), True)) + assert p3.simplify() == Piecewise((0, Eq(x, -1)), (sin(x), True)) + + +def test_issue_16417(): + z = Symbol('z') + assert unchanged(Piecewise, (1, Or(Eq(im(z), 0), Gt(re(z), 0))), (2, True)) + + x = Symbol('x') + assert unchanged(Piecewise, (S.Pi, re(x) < 0), + (0, Or(re(x) > 0, Ne(im(x), 0))), + (S.NaN, True)) + r = Symbol('r', real=True) + p = Piecewise((S.Pi, re(r) < 0), + (0, Or(re(r) > 0, Ne(im(r), 0))), + (S.NaN, True)) + assert p == Piecewise((S.Pi, r < 0), + (0, r > 0), + (S.NaN, True), evaluate=False) + # Does not work since imaginary != 0... + #i = Symbol('i', imaginary=True) + #p = Piecewise((S.Pi, re(i) < 0), + # (0, Or(re(i) > 0, Ne(im(i), 0))), + # (S.NaN, True)) + #assert p == Piecewise((0, Ne(im(i), 0)), + # (S.NaN, True), evaluate=False) + i = I*r + p = Piecewise((S.Pi, re(i) < 0), + (0, Or(re(i) > 0, Ne(im(i), 0))), + (S.NaN, True)) + assert p == Piecewise((0, Ne(im(i), 0)), + (S.NaN, True), evaluate=False) + assert p == Piecewise((0, Ne(r, 0)), + (S.NaN, True), evaluate=False) + + +def test_eval_rewrite_as_KroneckerDelta(): + x, y, z, n, t, m = symbols('x y z n t m') + K = KroneckerDelta + f = lambda p: expand(p.rewrite(K)) + + p1 = Piecewise((0, Eq(x, y)), (1, True)) + assert f(p1) == 1 - K(x, y) + + p2 = Piecewise((x, Eq(y,0)), (z, Eq(t,0)), (n, True)) + assert f(p2) == n*K(0, t)*K(0, y) - n*K(0, t) - n*K(0, y) + n + \ + x*K(0, y) - z*K(0, t)*K(0, y) + z*K(0, t) + + p3 = Piecewise((1, Ne(x, y)), (0, True)) + assert f(p3) == 1 - K(x, y) + + p4 = Piecewise((1, Eq(x, 3)), (4, True), (5, True)) + assert f(p4) == 4 - 3*K(3, x) + + p5 = Piecewise((3, Ne(x, 2)), (4, Eq(y, 2)), (5, True)) + assert f(p5) == -K(2, x)*K(2, y) + 2*K(2, x) + 3 + + p6 = Piecewise((0, Ne(x, 1) & Ne(y, 4)), (1, True)) + assert f(p6) == -K(1, x)*K(4, y) + K(1, x) + K(4, y) + + p7 = Piecewise((2, Eq(y, 3) & Ne(x, 2)), (1, True)) + assert f(p7) == -K(2, x)*K(3, y) + K(3, y) + 1 + + p8 = Piecewise((4, Eq(x, 3) & Ne(y, 2)), (1, True)) + assert f(p8) == -3*K(2, y)*K(3, x) + 3*K(3, x) + 1 + + p9 = Piecewise((6, Eq(x, 4) & Eq(y, 1)), (1, True)) + assert f(p9) == 5 * K(1, y) * K(4, x) + 1 + + p10 = Piecewise((4, Ne(x, -4) | Ne(y, 1)), (1, True)) + assert f(p10) == -3 * K(-4, x) * K(1, y) + 4 + + p11 = Piecewise((1, Eq(y, 2) | Ne(x, -3)), (2, True)) + assert f(p11) == -K(-3, x)*K(2, y) + K(-3, x) + 1 + + p12 = Piecewise((-1, Eq(x, 1) | Ne(y, 3)), (1, True)) + assert f(p12) == -2*K(1, x)*K(3, y) + 2*K(3, y) - 1 + + p13 = Piecewise((3, Eq(x, 2) | Eq(y, 4)), (1, True)) + assert f(p13) == -2*K(2, x)*K(4, y) + 2*K(2, x) + 2*K(4, y) + 1 + + p14 = Piecewise((1, Ne(x, 0) | Ne(y, 1)), (3, True)) + assert f(p14) == 2 * K(0, x) * K(1, y) + 1 + + p15 = Piecewise((2, Eq(x, 3) | Ne(y, 2)), (3, Eq(x, 4) & Eq(y, 5)), (1, True)) + assert f(p15) == -2*K(2, y)*K(3, x)*K(4, x)*K(5, y) + K(2, y)*K(3, x) + \ + 2*K(2, y)*K(4, x)*K(5, y) - K(2, y) + 2 + + p16 = Piecewise((0, Ne(m, n)), (1, True))*Piecewise((0, Ne(n, t)), (1, True))\ + *Piecewise((0, Ne(n, x)), (1, True)) - Piecewise((0, Ne(t, x)), (1, True)) + assert f(p16) == K(m, n)*K(n, t)*K(n, x) - K(t, x) + + p17 = Piecewise((0, Ne(t, x) & (Ne(m, n) | Ne(n, t) | Ne(n, x))), + (1, Ne(t, x)), (-1, Ne(m, n) | Ne(n, t) | Ne(n, x)), (0, True)) + assert f(p17) == K(m, n)*K(n, t)*K(n, x) - K(t, x) + + p18 = Piecewise((-4, Eq(y, 1) | (Eq(x, -5) & Eq(x, z))), (4, True)) + assert f(p18) == 8*K(-5, x)*K(1, y)*K(x, z) - 8*K(-5, x)*K(x, z) - 8*K(1, y) + 4 + + p19 = Piecewise((0, x > 2), (1, True)) + assert f(p19) == p19 + + p20 = Piecewise((0, And(x < 2, x > -5)), (1, True)) + assert f(p20) == p20 + + p21 = Piecewise((0, Or(x > 1, x < 0)), (1, True)) + assert f(p21) == p21 + + p22 = Piecewise((0, ~((Eq(y, -1) | Ne(x, 0)) & (Ne(x, 1) | Ne(y, -1)))), (1, True)) + assert f(p22) == K(-1, y)*K(0, x) - K(-1, y)*K(1, x) - K(0, x) + 1 + + +@slow +def test_identical_conds_issue(): + from sympy.stats import Uniform, density + u1 = Uniform('u1', 0, 1) + u2 = Uniform('u2', 0, 1) + # Result is quite big, so not really important here (and should ideally be + # simpler). Should not give an exception though. + density(u1 + u2) + + +def test_issue_7370(): + f = Piecewise((1, x <= 2400)) + v = integrate(f, (x, 0, Float("252.4", 30))) + assert str(v) == '252.400000000000000000000000000' + + +def test_issue_14933(): + x = Symbol('x') + y = Symbol('y') + + inp = MatrixSymbol('inp', 1, 1) + rep_dict = {y: inp[0, 0], x: inp[0, 0]} + + p = Piecewise((1, ITE(y > 0, x < 0, True))) + assert p.xreplace(rep_dict) == Piecewise((1, ITE(inp[0, 0] > 0, inp[0, 0] < 0, True))) + + +def test_issue_16715(): + raises(NotImplementedError, lambda: Piecewise((x, x<0), (0, y>1)).as_expr_set_pairs()) + + +def test_issue_20360(): + t, tau = symbols("t tau", real=True) + n = symbols("n", integer=True) + lam = pi * (n - S.Half) + eq = integrate(exp(lam * tau), (tau, 0, t)) + assert eq.simplify() == (2*exp(pi*t*(2*n - 1)/2) - 2)/(pi*(2*n - 1)) + + +def test_piecewise_eval(): + # XXX these tests might need modification if this + # simplification is moved out of eval and into + # boolalg or Piecewise simplification functions + f = lambda x: x.args[0].cond + # unsimplified + assert f(Piecewise((x, (x > -oo) & (x < 3))) + ) == ((x > -oo) & (x < 3)) + assert f(Piecewise((x, (x > -oo) & (x < oo))) + ) == ((x > -oo) & (x < oo)) + assert f(Piecewise((x, (x > -3) & (x < 3))) + ) == ((x > -3) & (x < 3)) + assert f(Piecewise((x, (x > -3) & (x < oo))) + ) == ((x > -3) & (x < oo)) + assert f(Piecewise((x, (x <= 3) & (x > -oo))) + ) == ((x <= 3) & (x > -oo)) + assert f(Piecewise((x, (x <= 3) & (x > -3))) + ) == ((x <= 3) & (x > -3)) + assert f(Piecewise((x, (x >= -3) & (x < 3))) + ) == ((x >= -3) & (x < 3)) + assert f(Piecewise((x, (x >= -3) & (x < oo))) + ) == ((x >= -3) & (x < oo)) + assert f(Piecewise((x, (x >= -3) & (x <= 3))) + ) == ((x >= -3) & (x <= 3)) + # could simplify by keeping only the first + # arg of result + assert f(Piecewise((x, (x <= oo) & (x > -oo))) + ) == (x > -oo) & (x <= oo) + assert f(Piecewise((x, (x <= oo) & (x > -3))) + ) == (x > -3) & (x <= oo) + assert f(Piecewise((x, (x >= -oo) & (x < 3))) + ) == (x < 3) & (x >= -oo) + assert f(Piecewise((x, (x >= -oo) & (x < oo))) + ) == (x < oo) & (x >= -oo) + assert f(Piecewise((x, (x >= -oo) & (x <= 3))) + ) == (x <= 3) & (x >= -oo) + assert f(Piecewise((x, (x >= -oo) & (x <= oo))) + ) == (x <= oo) & (x >= -oo) # but cannot be True unless x is real + assert f(Piecewise((x, (x >= -3) & (x <= oo))) + ) == (x >= -3) & (x <= oo) + assert f(Piecewise((x, (Abs(arg(a)) <= 1) | (Abs(arg(a)) < 1))) + ) == (Abs(arg(a)) <= 1) | (Abs(arg(a)) < 1) + + +def test_issue_22533(): + x = Symbol('x', real=True) + f = Piecewise((-1 / x, x <= 0), (1 / x, True)) + assert integrate(f, x) == Piecewise((-log(x), x <= 0), (log(x), True)) + + +def test_issue_24072(): + assert Piecewise((1, x > 1), (2, x <= 1), (3, x <= 1) + ) == Piecewise((1, x > 1), (2, True)) + + +def test_piecewise__eval_is_meromorphic(): + """ Issue 24127: Tests eval_is_meromorphic auxiliary method """ + x = symbols('x', real=True) + f = Piecewise((1, x < 0), (sqrt(1 - x), True)) + assert f.is_meromorphic(x, I) is None + assert f.is_meromorphic(x, -1) == True + assert f.is_meromorphic(x, 0) == None + assert f.is_meromorphic(x, 1) == False + assert f.is_meromorphic(x, 2) == True + assert f.is_meromorphic(x, Symbol('a')) == None + assert f.is_meromorphic(x, Symbol('a', real=True)) == None diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_trigonometric.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_trigonometric.py new file mode 100644 index 0000000000000000000000000000000000000000..815f424093aac72ee3a078d8ce62e5c195a625dc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/tests/test_trigonometric.py @@ -0,0 +1,2236 @@ +from sympy.calculus.accumulationbounds import AccumBounds +from sympy.core.add import Add +from sympy.core.function import (Lambda, diff) +from sympy.core.mod import Mod +from sympy.core.mul import Mul +from sympy.core.numbers import (E, Float, I, Rational, nan, oo, pi, zoo) +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import (arg, conjugate, im, re) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import (acoth, asinh, atanh, cosh, coth, sinh, tanh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (acos, acot, acsc, asec, asin, atan, atan2, + cos, cot, csc, sec, sin, sinc, tan) +from sympy.functions.special.bessel import (besselj, jn) +from sympy.functions.special.delta_functions import Heaviside +from sympy.matrices.dense import Matrix +from sympy.polys.polytools import (cancel, gcd) +from sympy.series.limits import limit +from sympy.series.order import O +from sympy.series.series import series +from sympy.sets.fancysets import ImageSet +from sympy.sets.sets import (FiniteSet, Interval) +from sympy.simplify.simplify import simplify +from sympy.core.expr import unchanged +from sympy.core.function import ArgumentIndexError, PoleError +from sympy.core.relational import Ne, Eq +from sympy.functions.elementary.piecewise import Piecewise +from sympy.sets.setexpr import SetExpr +from sympy.testing.pytest import XFAIL, slow, raises + + +x, y, z = symbols('x y z') +r = Symbol('r', real=True) +k, m = symbols('k m', integer=True) +p = Symbol('p', positive=True) +n = Symbol('n', negative=True) +np = Symbol('p', nonpositive=True) +nn = Symbol('n', nonnegative=True) +nz = Symbol('nz', nonzero=True) +ep = Symbol('ep', extended_positive=True) +en = Symbol('en', extended_negative=True) +enp = Symbol('ep', extended_nonpositive=True) +enn = Symbol('en', extended_nonnegative=True) +enz = Symbol('enz', extended_nonzero=True) +a = Symbol('a', algebraic=True) +na = Symbol('na', nonzero=True, algebraic=True) + + +def test_sin(): + x, y = symbols('x y') + z = symbols('z', imaginary=True) + + assert sin.nargs == FiniteSet(1) + assert sin(nan) is nan + assert sin(zoo) is nan + + assert sin(oo) == AccumBounds(-1, 1) + assert sin(oo) - sin(oo) == AccumBounds(-2, 2) + assert sin(oo*I) == oo*I + assert sin(-oo*I) == -oo*I + assert 0*sin(oo) is S.Zero + assert 0/sin(oo) is S.Zero + assert 0 + sin(oo) == AccumBounds(-1, 1) + assert 5 + sin(oo) == AccumBounds(4, 6) + + assert sin(0) == 0 + + assert sin(z*I) == I*sinh(z) + assert sin(asin(x)) == x + assert sin(atan(x)) == x / sqrt(1 + x**2) + assert sin(acos(x)) == sqrt(1 - x**2) + assert sin(acot(x)) == 1 / (sqrt(1 + 1 / x**2) * x) + assert sin(acsc(x)) == 1 / x + assert sin(asec(x)) == sqrt(1 - 1 / x**2) + assert sin(atan2(y, x)) == y / sqrt(x**2 + y**2) + + assert sin(pi*I) == sinh(pi)*I + assert sin(-pi*I) == -sinh(pi)*I + assert sin(-2*I) == -sinh(2)*I + + assert sin(pi) == 0 + assert sin(-pi) == 0 + assert sin(2*pi) == 0 + assert sin(-2*pi) == 0 + assert sin(-3*10**73*pi) == 0 + assert sin(7*10**103*pi) == 0 + + assert sin(pi/2) == 1 + assert sin(-pi/2) == -1 + assert sin(pi*Rational(5, 2)) == 1 + assert sin(pi*Rational(7, 2)) == -1 + + ne = symbols('ne', integer=True, even=False) + e = symbols('e', even=True) + assert sin(pi*ne/2) == (-1)**(ne/2 - S.Half) + assert sin(pi*k/2).func == sin + assert sin(pi*e/2) == 0 + assert sin(pi*k) == 0 + assert sin(pi*k).subs(k, 3) == sin(pi*k/2).subs(k, 6) # issue 8298 + + assert sin(pi/3) == S.Half*sqrt(3) + assert sin(pi*Rational(-2, 3)) == Rational(-1, 2)*sqrt(3) + + assert sin(pi/4) == S.Half*sqrt(2) + assert sin(-pi/4) == Rational(-1, 2)*sqrt(2) + assert sin(pi*Rational(17, 4)) == S.Half*sqrt(2) + assert sin(pi*Rational(-3, 4)) == Rational(-1, 2)*sqrt(2) + + assert sin(pi/6) == S.Half + assert sin(-pi/6) == Rational(-1, 2) + assert sin(pi*Rational(7, 6)) == Rational(-1, 2) + assert sin(pi*Rational(-5, 6)) == Rational(-1, 2) + + assert sin(pi*Rational(1, 5)) == sqrt((5 - sqrt(5)) / 8) + assert sin(pi*Rational(2, 5)) == sqrt((5 + sqrt(5)) / 8) + assert sin(pi*Rational(3, 5)) == sin(pi*Rational(2, 5)) + assert sin(pi*Rational(4, 5)) == sin(pi*Rational(1, 5)) + assert sin(pi*Rational(6, 5)) == -sin(pi*Rational(1, 5)) + assert sin(pi*Rational(8, 5)) == -sin(pi*Rational(2, 5)) + + assert sin(pi*Rational(-1273, 5)) == -sin(pi*Rational(2, 5)) + + assert sin(pi/8) == sqrt((2 - sqrt(2))/4) + + assert sin(pi/10) == Rational(-1, 4) + sqrt(5)/4 + + assert sin(pi/12) == -sqrt(2)/4 + sqrt(6)/4 + assert sin(pi*Rational(5, 12)) == sqrt(2)/4 + sqrt(6)/4 + assert sin(pi*Rational(-7, 12)) == -sqrt(2)/4 - sqrt(6)/4 + assert sin(pi*Rational(-11, 12)) == sqrt(2)/4 - sqrt(6)/4 + + assert sin(pi*Rational(104, 105)) == sin(pi/105) + assert sin(pi*Rational(106, 105)) == -sin(pi/105) + + assert sin(pi*Rational(-104, 105)) == -sin(pi/105) + assert sin(pi*Rational(-106, 105)) == sin(pi/105) + + assert sin(x*I) == sinh(x)*I + + assert sin(k*pi) == 0 + assert sin(17*k*pi) == 0 + assert sin(2*k*pi + 4) == sin(4) + assert sin(2*k*pi + m*pi + 1) == (-1)**(m + 2*k)*sin(1) + + assert sin(k*pi*I) == sinh(k*pi)*I + + assert sin(r).is_real is True + + assert sin(0, evaluate=False).is_algebraic + assert sin(a).is_algebraic is None + assert sin(na).is_algebraic is False + q = Symbol('q', rational=True) + assert sin(pi*q).is_algebraic + qn = Symbol('qn', rational=True, nonzero=True) + assert sin(qn).is_rational is False + assert sin(q).is_rational is None # issue 8653 + + assert isinstance(sin( re(x) - im(y)), sin) is True + assert isinstance(sin(-re(x) + im(y)), sin) is False + + assert sin(SetExpr(Interval(0, 1))) == SetExpr(ImageSet(Lambda(x, sin(x)), + Interval(0, 1))) + + for d in list(range(1, 22)) + [60, 85]: + for n in range(d*2 + 1): + x = n*pi/d + e = abs( float(sin(x)) - sin(float(x)) ) + assert e < 1e-12 + + assert sin(0, evaluate=False).is_zero is True + assert sin(k*pi, evaluate=False).is_zero is True + + assert sin(Add(1, -1, evaluate=False), evaluate=False).is_zero is True + + +def test_sin_cos(): + for d in [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 24, 30, 40, 60, 120]: # list is not exhaustive... + for n in range(-2*d, d*2): + x = n*pi/d + assert sin(x + pi/2) == cos(x), "fails for %d*pi/%d" % (n, d) + assert sin(x - pi/2) == -cos(x), "fails for %d*pi/%d" % (n, d) + assert sin(x) == cos(x - pi/2), "fails for %d*pi/%d" % (n, d) + assert -sin(x) == cos(x + pi/2), "fails for %d*pi/%d" % (n, d) + + +def test_sin_series(): + assert sin(x).series(x, 0, 9) == \ + x - x**3/6 + x**5/120 - x**7/5040 + O(x**9) + + +def test_sin_rewrite(): + assert sin(x).rewrite(exp) == -I*(exp(I*x) - exp(-I*x))/2 + assert sin(x).rewrite(tan) == 2*tan(x/2)/(1 + tan(x/2)**2) + assert sin(x).rewrite(cot) == \ + Piecewise((0, Eq(im(x), 0) & Eq(Mod(x, pi), 0)), + (2*cot(x/2)/(cot(x/2)**2 + 1), True)) + assert sin(sinh(x)).rewrite( + exp).subs(x, 3).n() == sin(x).rewrite(exp).subs(x, sinh(3)).n() + assert sin(cosh(x)).rewrite( + exp).subs(x, 3).n() == sin(x).rewrite(exp).subs(x, cosh(3)).n() + assert sin(tanh(x)).rewrite( + exp).subs(x, 3).n() == sin(x).rewrite(exp).subs(x, tanh(3)).n() + assert sin(coth(x)).rewrite( + exp).subs(x, 3).n() == sin(x).rewrite(exp).subs(x, coth(3)).n() + assert sin(sin(x)).rewrite( + exp).subs(x, 3).n() == sin(x).rewrite(exp).subs(x, sin(3)).n() + assert sin(cos(x)).rewrite( + exp).subs(x, 3).n() == sin(x).rewrite(exp).subs(x, cos(3)).n() + assert sin(tan(x)).rewrite( + exp).subs(x, 3).n() == sin(x).rewrite(exp).subs(x, tan(3)).n() + assert sin(cot(x)).rewrite( + exp).subs(x, 3).n() == sin(x).rewrite(exp).subs(x, cot(3)).n() + assert sin(log(x)).rewrite(Pow) == I*x**-I / 2 - I*x**I /2 + assert sin(x).rewrite(csc) == 1/csc(x) + assert sin(x).rewrite(cos) == cos(x - pi / 2, evaluate=False) + assert sin(x).rewrite(sec) == 1 / sec(x - pi / 2, evaluate=False) + assert sin(cos(x)).rewrite(Pow) == sin(cos(x)) + assert sin(x).rewrite(besselj) == sqrt(pi*x/2)*besselj(S.Half, x) + assert sin(x).rewrite(besselj).subs(x, 0) == sin(0) + + +def _test_extrig(f, i, e): + from sympy.core.function import expand_trig + assert unchanged(f, i) + assert expand_trig(f(i)) == f(i) + # testing directly instead of with .expand(trig=True) + # because the other expansions undo the unevaluated Mul + assert expand_trig(f(Mul(i, 1, evaluate=False))) == e + assert abs(f(i) - e).n() < 1e-10 + + +def test_sin_expansion(): + # Note: these formulas are not unique. The ones here come from the + # Chebyshev formulas. + assert sin(x + y).expand(trig=True) == sin(x)*cos(y) + cos(x)*sin(y) + assert sin(x - y).expand(trig=True) == sin(x)*cos(y) - cos(x)*sin(y) + assert sin(y - x).expand(trig=True) == cos(x)*sin(y) - sin(x)*cos(y) + assert sin(2*x).expand(trig=True) == 2*sin(x)*cos(x) + assert sin(3*x).expand(trig=True) == -4*sin(x)**3 + 3*sin(x) + assert sin(4*x).expand(trig=True) == -8*sin(x)**3*cos(x) + 4*sin(x)*cos(x) + assert sin(2*pi/17).expand(trig=True) == sin(2*pi/17, evaluate=False) + assert sin(x+pi/17).expand(trig=True) == sin(pi/17)*cos(x) + cos(pi/17)*sin(x) + _test_extrig(sin, 2, 2*sin(1)*cos(1)) + _test_extrig(sin, 3, -4*sin(1)**3 + 3*sin(1)) + + +def test_sin_AccumBounds(): + assert sin(AccumBounds(-oo, oo)) == AccumBounds(-1, 1) + assert sin(AccumBounds(0, oo)) == AccumBounds(-1, 1) + assert sin(AccumBounds(-oo, 0)) == AccumBounds(-1, 1) + assert sin(AccumBounds(0, 2*S.Pi)) == AccumBounds(-1, 1) + assert sin(AccumBounds(0, S.Pi*Rational(3, 4))) == AccumBounds(0, 1) + assert sin(AccumBounds(S.Pi*Rational(3, 4), S.Pi*Rational(7, 4))) == AccumBounds(-1, sin(S.Pi*Rational(3, 4))) + assert sin(AccumBounds(S.Pi/4, S.Pi/3)) == AccumBounds(sin(S.Pi/4), sin(S.Pi/3)) + assert sin(AccumBounds(S.Pi*Rational(3, 4), S.Pi*Rational(5, 6))) == AccumBounds(sin(S.Pi*Rational(5, 6)), sin(S.Pi*Rational(3, 4))) + + +def test_sin_fdiff(): + assert sin(x).fdiff() == cos(x) + raises(ArgumentIndexError, lambda: sin(x).fdiff(2)) + + +def test_trig_symmetry(): + assert sin(-x) == -sin(x) + assert cos(-x) == cos(x) + assert tan(-x) == -tan(x) + assert cot(-x) == -cot(x) + assert sin(x + pi) == -sin(x) + assert sin(x + 2*pi) == sin(x) + assert sin(x + 3*pi) == -sin(x) + assert sin(x + 4*pi) == sin(x) + assert sin(x - 5*pi) == -sin(x) + assert cos(x + pi) == -cos(x) + assert cos(x + 2*pi) == cos(x) + assert cos(x + 3*pi) == -cos(x) + assert cos(x + 4*pi) == cos(x) + assert cos(x - 5*pi) == -cos(x) + assert tan(x + pi) == tan(x) + assert tan(x - 3*pi) == tan(x) + assert cot(x + pi) == cot(x) + assert cot(x - 3*pi) == cot(x) + assert sin(pi/2 - x) == cos(x) + assert sin(pi*Rational(3, 2) - x) == -cos(x) + assert sin(pi*Rational(5, 2) - x) == cos(x) + assert cos(pi/2 - x) == sin(x) + assert cos(pi*Rational(3, 2) - x) == -sin(x) + assert cos(pi*Rational(5, 2) - x) == sin(x) + assert tan(pi/2 - x) == cot(x) + assert tan(pi*Rational(3, 2) - x) == cot(x) + assert tan(pi*Rational(5, 2) - x) == cot(x) + assert cot(pi/2 - x) == tan(x) + assert cot(pi*Rational(3, 2) - x) == tan(x) + assert cot(pi*Rational(5, 2) - x) == tan(x) + assert sin(pi/2 + x) == cos(x) + assert cos(pi/2 + x) == -sin(x) + assert tan(pi/2 + x) == -cot(x) + assert cot(pi/2 + x) == -tan(x) + + +def test_cos(): + x, y = symbols('x y') + + assert cos.nargs == FiniteSet(1) + assert cos(nan) is nan + + assert cos(oo) == AccumBounds(-1, 1) + assert cos(oo) - cos(oo) == AccumBounds(-2, 2) + assert cos(oo*I) is oo + assert cos(-oo*I) is oo + assert cos(zoo) is nan + + assert cos(0) == 1 + + assert cos(acos(x)) == x + assert cos(atan(x)) == 1 / sqrt(1 + x**2) + assert cos(asin(x)) == sqrt(1 - x**2) + assert cos(acot(x)) == 1 / sqrt(1 + 1 / x**2) + assert cos(acsc(x)) == sqrt(1 - 1 / x**2) + assert cos(asec(x)) == 1 / x + assert cos(atan2(y, x)) == x / sqrt(x**2 + y**2) + + assert cos(pi*I) == cosh(pi) + assert cos(-pi*I) == cosh(pi) + assert cos(-2*I) == cosh(2) + + assert cos(pi/2) == 0 + assert cos(-pi/2) == 0 + assert cos(pi/2) == 0 + assert cos(-pi/2) == 0 + assert cos((-3*10**73 + 1)*pi/2) == 0 + assert cos((7*10**103 + 1)*pi/2) == 0 + + n = symbols('n', integer=True, even=False) + e = symbols('e', even=True) + assert cos(pi*n/2) == 0 + assert cos(pi*e/2) == (-1)**(e/2) + + assert cos(pi) == -1 + assert cos(-pi) == -1 + assert cos(2*pi) == 1 + assert cos(5*pi) == -1 + assert cos(8*pi) == 1 + + assert cos(pi/3) == S.Half + assert cos(pi*Rational(-2, 3)) == Rational(-1, 2) + + assert cos(pi/4) == S.Half*sqrt(2) + assert cos(-pi/4) == S.Half*sqrt(2) + assert cos(pi*Rational(11, 4)) == Rational(-1, 2)*sqrt(2) + assert cos(pi*Rational(-3, 4)) == Rational(-1, 2)*sqrt(2) + + assert cos(pi/6) == S.Half*sqrt(3) + assert cos(-pi/6) == S.Half*sqrt(3) + assert cos(pi*Rational(7, 6)) == Rational(-1, 2)*sqrt(3) + assert cos(pi*Rational(-5, 6)) == Rational(-1, 2)*sqrt(3) + + assert cos(pi*Rational(1, 5)) == (sqrt(5) + 1)/4 + assert cos(pi*Rational(2, 5)) == (sqrt(5) - 1)/4 + assert cos(pi*Rational(3, 5)) == -cos(pi*Rational(2, 5)) + assert cos(pi*Rational(4, 5)) == -cos(pi*Rational(1, 5)) + assert cos(pi*Rational(6, 5)) == -cos(pi*Rational(1, 5)) + assert cos(pi*Rational(8, 5)) == cos(pi*Rational(2, 5)) + + assert cos(pi*Rational(-1273, 5)) == -cos(pi*Rational(2, 5)) + + assert cos(pi/8) == sqrt((2 + sqrt(2))/4) + + assert cos(pi/12) == sqrt(2)/4 + sqrt(6)/4 + assert cos(pi*Rational(5, 12)) == -sqrt(2)/4 + sqrt(6)/4 + assert cos(pi*Rational(7, 12)) == sqrt(2)/4 - sqrt(6)/4 + assert cos(pi*Rational(11, 12)) == -sqrt(2)/4 - sqrt(6)/4 + + assert cos(pi*Rational(104, 105)) == -cos(pi/105) + assert cos(pi*Rational(106, 105)) == -cos(pi/105) + + assert cos(pi*Rational(-104, 105)) == -cos(pi/105) + assert cos(pi*Rational(-106, 105)) == -cos(pi/105) + + assert cos(x*I) == cosh(x) + assert cos(k*pi*I) == cosh(k*pi) + + assert cos(r).is_real is True + + assert cos(0, evaluate=False).is_algebraic + assert cos(a).is_algebraic is None + assert cos(na).is_algebraic is False + q = Symbol('q', rational=True) + assert cos(pi*q).is_algebraic + assert cos(pi*Rational(2, 7)).is_algebraic + + assert cos(k*pi) == (-1)**k + assert cos(2*k*pi) == 1 + assert cos(0, evaluate=False).is_zero is False + assert cos(Rational(1, 2)).is_zero is False + # The following test will return None as the result, but really it should + # be True even if it is not always possible to resolve an assumptions query. + assert cos(asin(-1, evaluate=False), evaluate=False).is_zero is None + for d in list(range(1, 22)) + [60, 85]: + for n in range(2*d + 1): + x = n*pi/d + e = abs( float(cos(x)) - cos(float(x)) ) + assert e < 1e-12 + + +def test_issue_6190(): + c = Float('123456789012345678901234567890.25', '') + for cls in [sin, cos, tan, cot]: + assert cls(c*pi) == cls(pi/4) + assert cls(4.125*pi) == cls(pi/8) + assert cls(4.7*pi) == cls((4.7 % 2)*pi) + + +def test_cos_series(): + assert cos(x).series(x, 0, 9) == \ + 1 - x**2/2 + x**4/24 - x**6/720 + x**8/40320 + O(x**9) + + +def test_cos_rewrite(): + assert cos(x).rewrite(exp) == exp(I*x)/2 + exp(-I*x)/2 + assert cos(x).rewrite(tan) == (1 - tan(x/2)**2)/(1 + tan(x/2)**2) + assert cos(x).rewrite(cot) == \ + Piecewise((1, Eq(im(x), 0) & Eq(Mod(x, 2*pi), 0)), + ((cot(x/2)**2 - 1)/(cot(x/2)**2 + 1), True)) + assert cos(sinh(x)).rewrite( + exp).subs(x, 3).n() == cos(x).rewrite(exp).subs(x, sinh(3)).n() + assert cos(cosh(x)).rewrite( + exp).subs(x, 3).n() == cos(x).rewrite(exp).subs(x, cosh(3)).n() + assert cos(tanh(x)).rewrite( + exp).subs(x, 3).n() == cos(x).rewrite(exp).subs(x, tanh(3)).n() + assert cos(coth(x)).rewrite( + exp).subs(x, 3).n() == cos(x).rewrite(exp).subs(x, coth(3)).n() + assert cos(sin(x)).rewrite( + exp).subs(x, 3).n() == cos(x).rewrite(exp).subs(x, sin(3)).n() + assert cos(cos(x)).rewrite( + exp).subs(x, 3).n() == cos(x).rewrite(exp).subs(x, cos(3)).n() + assert cos(tan(x)).rewrite( + exp).subs(x, 3).n() == cos(x).rewrite(exp).subs(x, tan(3)).n() + assert cos(cot(x)).rewrite( + exp).subs(x, 3).n() == cos(x).rewrite(exp).subs(x, cot(3)).n() + assert cos(log(x)).rewrite(Pow) == x**I/2 + x**-I/2 + assert cos(x).rewrite(sec) == 1/sec(x) + assert cos(x).rewrite(sin) == sin(x + pi/2, evaluate=False) + assert cos(x).rewrite(csc) == 1/csc(-x + pi/2, evaluate=False) + assert cos(sin(x)).rewrite(Pow) == cos(sin(x)) + assert cos(x).rewrite(besselj) == Piecewise( + (sqrt(pi*x/2)*besselj(-S.Half, x), Ne(x, 0)), + (1, True) + ) + assert cos(x).rewrite(besselj).subs(x, 0) == cos(0) + + +def test_cos_expansion(): + assert cos(x + y).expand(trig=True) == cos(x)*cos(y) - sin(x)*sin(y) + assert cos(x - y).expand(trig=True) == cos(x)*cos(y) + sin(x)*sin(y) + assert cos(y - x).expand(trig=True) == cos(x)*cos(y) + sin(x)*sin(y) + assert cos(2*x).expand(trig=True) == 2*cos(x)**2 - 1 + assert cos(3*x).expand(trig=True) == 4*cos(x)**3 - 3*cos(x) + assert cos(4*x).expand(trig=True) == 8*cos(x)**4 - 8*cos(x)**2 + 1 + assert cos(2*pi/17).expand(trig=True) == cos(2*pi/17, evaluate=False) + assert cos(x+pi/17).expand(trig=True) == cos(pi/17)*cos(x) - sin(pi/17)*sin(x) + _test_extrig(cos, 2, 2*cos(1)**2 - 1) + _test_extrig(cos, 3, 4*cos(1)**3 - 3*cos(1)) + + +def test_cos_AccumBounds(): + assert cos(AccumBounds(-oo, oo)) == AccumBounds(-1, 1) + assert cos(AccumBounds(0, oo)) == AccumBounds(-1, 1) + assert cos(AccumBounds(-oo, 0)) == AccumBounds(-1, 1) + assert cos(AccumBounds(0, 2*S.Pi)) == AccumBounds(-1, 1) + assert cos(AccumBounds(-S.Pi/3, S.Pi/4)) == AccumBounds(cos(-S.Pi/3), 1) + assert cos(AccumBounds(S.Pi*Rational(3, 4), S.Pi*Rational(5, 4))) == AccumBounds(-1, cos(S.Pi*Rational(3, 4))) + assert cos(AccumBounds(S.Pi*Rational(5, 4), S.Pi*Rational(4, 3))) == AccumBounds(cos(S.Pi*Rational(5, 4)), cos(S.Pi*Rational(4, 3))) + assert cos(AccumBounds(S.Pi/4, S.Pi/3)) == AccumBounds(cos(S.Pi/3), cos(S.Pi/4)) + + +def test_cos_fdiff(): + assert cos(x).fdiff() == -sin(x) + raises(ArgumentIndexError, lambda: cos(x).fdiff(2)) + + +def test_tan(): + assert tan(nan) is nan + + assert tan(zoo) is nan + assert tan(oo) == AccumBounds(-oo, oo) + assert tan(oo) - tan(oo) == AccumBounds(-oo, oo) + assert tan.nargs == FiniteSet(1) + assert tan(oo*I) == I + assert tan(-oo*I) == -I + + assert tan(0) == 0 + + assert tan(atan(x)) == x + assert tan(asin(x)) == x / sqrt(1 - x**2) + assert tan(acos(x)) == sqrt(1 - x**2) / x + assert tan(acot(x)) == 1 / x + assert tan(acsc(x)) == 1 / (sqrt(1 - 1 / x**2) * x) + assert tan(asec(x)) == sqrt(1 - 1 / x**2) * x + assert tan(atan2(y, x)) == y/x + + assert tan(pi*I) == tanh(pi)*I + assert tan(-pi*I) == -tanh(pi)*I + assert tan(-2*I) == -tanh(2)*I + + assert tan(pi) == 0 + assert tan(-pi) == 0 + assert tan(2*pi) == 0 + assert tan(-2*pi) == 0 + assert tan(-3*10**73*pi) == 0 + + assert tan(pi/2) is zoo + assert tan(pi*Rational(3, 2)) is zoo + + assert tan(pi/3) == sqrt(3) + assert tan(pi*Rational(-2, 3)) == sqrt(3) + + assert tan(pi/4) is S.One + assert tan(-pi/4) is S.NegativeOne + assert tan(pi*Rational(17, 4)) is S.One + assert tan(pi*Rational(-3, 4)) is S.One + + assert tan(pi/5) == sqrt(5 - 2*sqrt(5)) + assert tan(pi*Rational(2, 5)) == sqrt(5 + 2*sqrt(5)) + assert tan(pi*Rational(18, 5)) == -sqrt(5 + 2*sqrt(5)) + assert tan(pi*Rational(-16, 5)) == -sqrt(5 - 2*sqrt(5)) + + assert tan(pi/6) == 1/sqrt(3) + assert tan(-pi/6) == -1/sqrt(3) + assert tan(pi*Rational(7, 6)) == 1/sqrt(3) + assert tan(pi*Rational(-5, 6)) == 1/sqrt(3) + + assert tan(pi/8) == -1 + sqrt(2) + assert tan(pi*Rational(3, 8)) == 1 + sqrt(2) # issue 15959 + assert tan(pi*Rational(5, 8)) == -1 - sqrt(2) + assert tan(pi*Rational(7, 8)) == 1 - sqrt(2) + + assert tan(pi/10) == sqrt(1 - 2*sqrt(5)/5) + assert tan(pi*Rational(3, 10)) == sqrt(1 + 2*sqrt(5)/5) + assert tan(pi*Rational(17, 10)) == -sqrt(1 + 2*sqrt(5)/5) + assert tan(pi*Rational(-31, 10)) == -sqrt(1 - 2*sqrt(5)/5) + + assert tan(pi/12) == -sqrt(3) + 2 + assert tan(pi*Rational(5, 12)) == sqrt(3) + 2 + assert tan(pi*Rational(7, 12)) == -sqrt(3) - 2 + assert tan(pi*Rational(11, 12)) == sqrt(3) - 2 + + assert tan(pi/24).radsimp() == -2 - sqrt(3) + sqrt(2) + sqrt(6) + assert tan(pi*Rational(5, 24)).radsimp() == -2 + sqrt(3) - sqrt(2) + sqrt(6) + assert tan(pi*Rational(7, 24)).radsimp() == 2 - sqrt(3) - sqrt(2) + sqrt(6) + assert tan(pi*Rational(11, 24)).radsimp() == 2 + sqrt(3) + sqrt(2) + sqrt(6) + assert tan(pi*Rational(13, 24)).radsimp() == -2 - sqrt(3) - sqrt(2) - sqrt(6) + assert tan(pi*Rational(17, 24)).radsimp() == -2 + sqrt(3) + sqrt(2) - sqrt(6) + assert tan(pi*Rational(19, 24)).radsimp() == 2 - sqrt(3) + sqrt(2) - sqrt(6) + assert tan(pi*Rational(23, 24)).radsimp() == 2 + sqrt(3) - sqrt(2) - sqrt(6) + + assert tan(x*I) == tanh(x)*I + + assert tan(k*pi) == 0 + assert tan(17*k*pi) == 0 + + assert tan(k*pi*I) == tanh(k*pi)*I + + assert tan(r).is_real is None + assert tan(r).is_extended_real is True + + assert tan(0, evaluate=False).is_algebraic + assert tan(a).is_algebraic is None + assert tan(na).is_algebraic is False + + assert tan(pi*Rational(10, 7)) == tan(pi*Rational(3, 7)) + assert tan(pi*Rational(11, 7)) == -tan(pi*Rational(3, 7)) + assert tan(pi*Rational(-11, 7)) == tan(pi*Rational(3, 7)) + + assert tan(pi*Rational(15, 14)) == tan(pi/14) + assert tan(pi*Rational(-15, 14)) == -tan(pi/14) + + assert tan(r).is_finite is None + assert tan(I*r).is_finite is True + + # https://github.com/sympy/sympy/issues/21177 + f = tan(pi*(x + S(3)/2))/(3*x) + assert f.as_leading_term(x) == -1/(3*pi*x**2) + + +def test_tan_series(): + assert tan(x).series(x, 0, 9) == \ + x + x**3/3 + 2*x**5/15 + 17*x**7/315 + O(x**9) + + +def test_tan_rewrite(): + neg_exp, pos_exp = exp(-x*I), exp(x*I) + assert tan(x).rewrite(exp) == I*(neg_exp - pos_exp)/(neg_exp + pos_exp) + assert tan(x).rewrite(sin) == 2*sin(x)**2/sin(2*x) + assert tan(x).rewrite(cos) == cos(x - S.Pi/2, evaluate=False)/cos(x) + assert tan(x).rewrite(cot) == 1/cot(x) + assert tan(sinh(x)).rewrite(exp).subs(x, 3).n() == tan(x).rewrite(exp).subs(x, sinh(3)).n() + assert tan(cosh(x)).rewrite(exp).subs(x, 3).n() == tan(x).rewrite(exp).subs(x, cosh(3)).n() + assert tan(tanh(x)).rewrite(exp).subs(x, 3).n() == tan(x).rewrite(exp).subs(x, tanh(3)).n() + assert tan(coth(x)).rewrite(exp).subs(x, 3).n() == tan(x).rewrite(exp).subs(x, coth(3)).n() + assert tan(sin(x)).rewrite(exp).subs(x, 3).n() == tan(x).rewrite(exp).subs(x, sin(3)).n() + assert tan(cos(x)).rewrite(exp).subs(x, 3).n() == tan(x).rewrite(exp).subs(x, cos(3)).n() + assert tan(tan(x)).rewrite(exp).subs(x, 3).n() == tan(x).rewrite(exp).subs(x, tan(3)).n() + assert tan(cot(x)).rewrite(exp).subs(x, 3).n() == tan(x).rewrite(exp).subs(x, cot(3)).n() + assert tan(log(x)).rewrite(Pow) == I*(x**-I - x**I)/(x**-I + x**I) + assert tan(x).rewrite(sec) == sec(x)/sec(x - pi/2, evaluate=False) + assert tan(x).rewrite(csc) == csc(-x + pi/2, evaluate=False)/csc(x) + assert tan(sin(x)).rewrite(Pow) == tan(sin(x)) + assert tan(pi*Rational(2, 5), evaluate=False).rewrite(sqrt) == sqrt(sqrt(5)/8 + + Rational(5, 8))/(Rational(-1, 4) + sqrt(5)/4) + assert tan(x).rewrite(besselj) == besselj(S.Half, x)/besselj(-S.Half, x) + assert tan(x).rewrite(besselj).subs(x, 0) == tan(0) + + +@slow +def test_tan_rewrite_slow(): + assert 0 == (cos(pi/34)*tan(pi/34) - sin(pi/34)).rewrite(pow) + assert 0 == (cos(pi/17)*tan(pi/17) - sin(pi/17)).rewrite(pow) + assert tan(pi/19).rewrite(pow) == tan(pi/19) + assert tan(pi*Rational(8, 19)).rewrite(sqrt) == tan(pi*Rational(8, 19)) + assert tan(pi*Rational(2, 5), evaluate=False).rewrite(sqrt) == sqrt(sqrt(5)/8 + + Rational(5, 8))/(Rational(-1, 4) + sqrt(5)/4) + + +def test_tan_subs(): + assert tan(x).subs(tan(x), y) == y + assert tan(x).subs(x, y) == tan(y) + assert tan(x).subs(x, S.Pi/2) is zoo + assert tan(x).subs(x, S.Pi*Rational(3, 2)) is zoo + + +def test_tan_expansion(): + assert tan(x + y).expand(trig=True) == ((tan(x) + tan(y))/(1 - tan(x)*tan(y))).expand() + assert tan(x - y).expand(trig=True) == ((tan(x) - tan(y))/(1 + tan(x)*tan(y))).expand() + assert tan(x + y + z).expand(trig=True) == ( + (tan(x) + tan(y) + tan(z) - tan(x)*tan(y)*tan(z))/ + (1 - tan(x)*tan(y) - tan(x)*tan(z) - tan(y)*tan(z))).expand() + assert 0 == tan(2*x).expand(trig=True).rewrite(tan).subs([(tan(x), Rational(1, 7))])*24 - 7 + assert 0 == tan(3*x).expand(trig=True).rewrite(tan).subs([(tan(x), Rational(1, 5))])*55 - 37 + assert 0 == tan(4*x - pi/4).expand(trig=True).rewrite(tan).subs([(tan(x), Rational(1, 5))])*239 - 1 + _test_extrig(tan, 2, 2*tan(1)/(1 - tan(1)**2)) + _test_extrig(tan, 3, (-tan(1)**3 + 3*tan(1))/(1 - 3*tan(1)**2)) + + +def test_tan_AccumBounds(): + assert tan(AccumBounds(-oo, oo)) == AccumBounds(-oo, oo) + assert tan(AccumBounds(S.Pi/3, S.Pi*Rational(2, 3))) == AccumBounds(-oo, oo) + assert tan(AccumBounds(S.Pi/6, S.Pi/3)) == AccumBounds(tan(S.Pi/6), tan(S.Pi/3)) + + +def test_tan_fdiff(): + assert tan(x).fdiff() == tan(x)**2 + 1 + raises(ArgumentIndexError, lambda: tan(x).fdiff(2)) + + +def test_cot(): + assert cot(nan) is nan + + assert cot.nargs == FiniteSet(1) + assert cot(oo*I) == -I + assert cot(-oo*I) == I + assert cot(zoo) is nan + + assert cot(0) is zoo + assert cot(2*pi) is zoo + + assert cot(acot(x)) == x + assert cot(atan(x)) == 1 / x + assert cot(asin(x)) == sqrt(1 - x**2) / x + assert cot(acos(x)) == x / sqrt(1 - x**2) + assert cot(acsc(x)) == sqrt(1 - 1 / x**2) * x + assert cot(asec(x)) == 1 / (sqrt(1 - 1 / x**2) * x) + assert cot(atan2(y, x)) == x/y + + assert cot(pi*I) == -coth(pi)*I + assert cot(-pi*I) == coth(pi)*I + assert cot(-2*I) == coth(2)*I + + assert cot(pi) == cot(2*pi) == cot(3*pi) + assert cot(-pi) == cot(-2*pi) == cot(-3*pi) + + assert cot(pi/2) == 0 + assert cot(-pi/2) == 0 + assert cot(pi*Rational(5, 2)) == 0 + assert cot(pi*Rational(7, 2)) == 0 + + assert cot(pi/3) == 1/sqrt(3) + assert cot(pi*Rational(-2, 3)) == 1/sqrt(3) + + assert cot(pi/4) is S.One + assert cot(-pi/4) is S.NegativeOne + assert cot(pi*Rational(17, 4)) is S.One + assert cot(pi*Rational(-3, 4)) is S.One + + assert cot(pi/6) == sqrt(3) + assert cot(-pi/6) == -sqrt(3) + assert cot(pi*Rational(7, 6)) == sqrt(3) + assert cot(pi*Rational(-5, 6)) == sqrt(3) + + assert cot(pi/8) == 1 + sqrt(2) + assert cot(pi*Rational(3, 8)) == -1 + sqrt(2) + assert cot(pi*Rational(5, 8)) == 1 - sqrt(2) + assert cot(pi*Rational(7, 8)) == -1 - sqrt(2) + + assert cot(pi/12) == sqrt(3) + 2 + assert cot(pi*Rational(5, 12)) == -sqrt(3) + 2 + assert cot(pi*Rational(7, 12)) == sqrt(3) - 2 + assert cot(pi*Rational(11, 12)) == -sqrt(3) - 2 + + assert cot(pi/24).radsimp() == sqrt(2) + sqrt(3) + 2 + sqrt(6) + assert cot(pi*Rational(5, 24)).radsimp() == -sqrt(2) - sqrt(3) + 2 + sqrt(6) + assert cot(pi*Rational(7, 24)).radsimp() == -sqrt(2) + sqrt(3) - 2 + sqrt(6) + assert cot(pi*Rational(11, 24)).radsimp() == sqrt(2) - sqrt(3) - 2 + sqrt(6) + assert cot(pi*Rational(13, 24)).radsimp() == -sqrt(2) + sqrt(3) + 2 - sqrt(6) + assert cot(pi*Rational(17, 24)).radsimp() == sqrt(2) - sqrt(3) + 2 - sqrt(6) + assert cot(pi*Rational(19, 24)).radsimp() == sqrt(2) + sqrt(3) - 2 - sqrt(6) + assert cot(pi*Rational(23, 24)).radsimp() == -sqrt(2) - sqrt(3) - 2 - sqrt(6) + + assert cot(x*I) == -coth(x)*I + assert cot(k*pi*I) == -coth(k*pi)*I + + assert cot(r).is_real is None + assert cot(r).is_extended_real is True + + assert cot(a).is_algebraic is None + assert cot(na).is_algebraic is False + + assert cot(pi*Rational(10, 7)) == cot(pi*Rational(3, 7)) + assert cot(pi*Rational(11, 7)) == -cot(pi*Rational(3, 7)) + assert cot(pi*Rational(-11, 7)) == cot(pi*Rational(3, 7)) + + assert cot(pi*Rational(39, 34)) == cot(pi*Rational(5, 34)) + assert cot(pi*Rational(-41, 34)) == -cot(pi*Rational(7, 34)) + + assert cot(x).is_finite is None + assert cot(r).is_finite is None + i = Symbol('i', imaginary=True) + assert cot(i).is_finite is True + + assert cot(x).subs(x, 3*pi) is zoo + + # https://github.com/sympy/sympy/issues/21177 + f = cot(pi*(x + 4))/(3*x) + assert f.as_leading_term(x) == 1/(3*pi*x**2) + + +def test_tan_cot_sin_cos_evalf(): + assert abs((tan(pi*Rational(8, 15))*cos(pi*Rational(8, 15))/sin(pi*Rational(8, 15)) - 1).evalf()) < 1e-14 + assert abs((cot(pi*Rational(4, 15))*sin(pi*Rational(4, 15))/cos(pi*Rational(4, 15)) - 1).evalf()) < 1e-14 + +@XFAIL +def test_tan_cot_sin_cos_ratsimp(): + assert 1 == (tan(pi*Rational(8, 15))*cos(pi*Rational(8, 15))/sin(pi*Rational(8, 15))).ratsimp() + assert 1 == (cot(pi*Rational(4, 15))*sin(pi*Rational(4, 15))/cos(pi*Rational(4, 15))).ratsimp() + + +def test_cot_series(): + assert cot(x).series(x, 0, 9) == \ + 1/x - x/3 - x**3/45 - 2*x**5/945 - x**7/4725 + O(x**9) + # issue 6210 + assert cot(x**4 + x**5).series(x, 0, 1) == \ + x**(-4) - 1/x**3 + x**(-2) - 1/x + 1 + O(x) + assert cot(pi*(1-x)).series(x, 0, 3) == -1/(pi*x) + pi*x/3 + O(x**3) + assert cot(x).taylor_term(0, x) == 1/x + assert cot(x).taylor_term(2, x) is S.Zero + assert cot(x).taylor_term(3, x) == -x**3/45 + + +def test_cot_rewrite(): + neg_exp, pos_exp = exp(-x*I), exp(x*I) + assert cot(x).rewrite(exp) == I*(pos_exp + neg_exp)/(pos_exp - neg_exp) + assert cot(x).rewrite(sin) == sin(2*x)/(2*(sin(x)**2)) + assert cot(x).rewrite(cos) == cos(x)/cos(x - pi/2, evaluate=False) + assert cot(x).rewrite(tan) == 1/tan(x) + def check(func): + z = cot(func(x)).rewrite(exp) - cot(x).rewrite(exp).subs(x, func(x)) + assert z.rewrite(exp).expand() == 0 + check(sinh) + check(cosh) + check(tanh) + check(coth) + check(sin) + check(cos) + check(tan) + assert cot(log(x)).rewrite(Pow) == -I*(x**-I + x**I)/(x**-I - x**I) + assert cot(x).rewrite(sec) == sec(x - pi / 2, evaluate=False) / sec(x) + assert cot(x).rewrite(csc) == csc(x) / csc(- x + pi / 2, evaluate=False) + assert cot(sin(x)).rewrite(Pow) == cot(sin(x)) + assert cot(pi*Rational(2, 5), evaluate=False).rewrite(sqrt) == (Rational(-1, 4) + sqrt(5)/4)/\ + sqrt(sqrt(5)/8 + Rational(5, 8)) + assert cot(x).rewrite(besselj) == besselj(-S.Half, x)/besselj(S.Half, x) + assert cot(x).rewrite(besselj).subs(x, 0) == cot(0) + + +@slow +def test_cot_rewrite_slow(): + assert cot(pi*Rational(4, 34)).rewrite(pow).ratsimp() == \ + (cos(pi*Rational(4, 34))/sin(pi*Rational(4, 34))).rewrite(pow).ratsimp() + assert cot(pi*Rational(4, 17)).rewrite(pow) == \ + (cos(pi*Rational(4, 17))/sin(pi*Rational(4, 17))).rewrite(pow) + assert cot(pi/19).rewrite(pow) == cot(pi/19) + assert cot(pi/19).rewrite(sqrt) == cot(pi/19) + assert cot(pi*Rational(2, 5), evaluate=False).rewrite(sqrt) == \ + (Rational(-1, 4) + sqrt(5)/4) / sqrt(sqrt(5)/8 + Rational(5, 8)) + + +def test_cot_subs(): + assert cot(x).subs(cot(x), y) == y + assert cot(x).subs(x, y) == cot(y) + assert cot(x).subs(x, 0) is zoo + assert cot(x).subs(x, S.Pi) is zoo + + +def test_cot_expansion(): + assert cot(x + y).expand(trig=True).together() == ( + (cot(x)*cot(y) - 1)/(cot(x) + cot(y))) + assert cot(x - y).expand(trig=True).together() == ( + cot(x)*cot(-y) - 1)/(cot(x) + cot(-y)) + assert cot(x + y + z).expand(trig=True).together() == ( + (cot(x)*cot(y)*cot(z) - cot(x) - cot(y) - cot(z))/ + (-1 + cot(x)*cot(y) + cot(x)*cot(z) + cot(y)*cot(z))) + assert cot(3*x).expand(trig=True).together() == ( + (cot(x)**2 - 3)*cot(x)/(3*cot(x)**2 - 1)) + assert cot(2*x).expand(trig=True) == cot(x)/2 - 1/(2*cot(x)) + assert cot(3*x).expand(trig=True).together() == ( + cot(x)**2 - 3)*cot(x)/(3*cot(x)**2 - 1) + assert cot(4*x - pi/4).expand(trig=True).cancel() == ( + -tan(x)**4 + 4*tan(x)**3 + 6*tan(x)**2 - 4*tan(x) - 1 + )/(tan(x)**4 + 4*tan(x)**3 - 6*tan(x)**2 - 4*tan(x) + 1) + _test_extrig(cot, 2, (-1 + cot(1)**2)/(2*cot(1))) + _test_extrig(cot, 3, (-3*cot(1) + cot(1)**3)/(-1 + 3*cot(1)**2)) + + +def test_cot_AccumBounds(): + assert cot(AccumBounds(-oo, oo)) == AccumBounds(-oo, oo) + assert cot(AccumBounds(-S.Pi/3, S.Pi/3)) == AccumBounds(-oo, oo) + assert cot(AccumBounds(S.Pi/6, S.Pi/3)) == AccumBounds(cot(S.Pi/3), cot(S.Pi/6)) + + +def test_cot_fdiff(): + assert cot(x).fdiff() == -cot(x)**2 - 1 + raises(ArgumentIndexError, lambda: cot(x).fdiff(2)) + + +def test_sinc(): + assert isinstance(sinc(x), sinc) + + s = Symbol('s', zero=True) + assert sinc(s) is S.One + assert sinc(S.Infinity) is S.Zero + assert sinc(S.NegativeInfinity) is S.Zero + assert sinc(S.NaN) is S.NaN + assert sinc(S.ComplexInfinity) is S.NaN + + n = Symbol('n', integer=True, nonzero=True) + assert sinc(n*pi) is S.Zero + assert sinc(-n*pi) is S.Zero + assert sinc(pi/2) == 2 / pi + assert sinc(-pi/2) == 2 / pi + assert sinc(pi*Rational(5, 2)) == 2 / (5*pi) + assert sinc(pi*Rational(7, 2)) == -2 / (7*pi) + + assert sinc(-x) == sinc(x) + + assert sinc(x).diff(x) == cos(x)/x - sin(x)/x**2 + assert sinc(x).diff(x) == (sin(x)/x).diff(x) + assert sinc(x).diff(x, x) == (-sin(x) - 2*cos(x)/x + 2*sin(x)/x**2)/x + assert sinc(x).diff(x, x) == (sin(x)/x).diff(x, x) + assert limit(sinc(x).diff(x), x, 0) == 0 + assert limit(sinc(x).diff(x, x), x, 0) == -S(1)/3 + + # https://github.com/sympy/sympy/issues/11402 + # + # assert sinc(x).diff(x) == Piecewise(((x*cos(x) - sin(x)) / x**2, Ne(x, 0)), (0, True)) + # + # assert sinc(x).diff(x).equals(sinc(x).rewrite(sin).diff(x)) + # + # assert sinc(x).diff(x).subs(x, 0) is S.Zero + + assert sinc(x).series() == 1 - x**2/6 + x**4/120 + O(x**6) + + assert sinc(x).rewrite(jn) == jn(0, x) + assert sinc(x).rewrite(sin) == Piecewise((sin(x)/x, Ne(x, 0)), (1, True)) + assert sinc(pi, evaluate=False).is_zero is True + assert sinc(0, evaluate=False).is_zero is False + assert sinc(n*pi, evaluate=False).is_zero is True + assert sinc(x).is_zero is None + xr = Symbol('xr', real=True, nonzero=True) + assert sinc(x).is_real is None + assert sinc(xr).is_real is True + assert sinc(I*xr).is_real is True + assert sinc(I*100).is_real is True + assert sinc(x).is_finite is None + assert sinc(xr).is_finite is True + + +def test_asin(): + assert asin(nan) is nan + + assert asin.nargs == FiniteSet(1) + assert asin(oo) == -I*oo + assert asin(-oo) == I*oo + assert asin(zoo) is zoo + + # Note: asin(-x) = - asin(x) + assert asin(0) == 0 + assert asin(1) == pi/2 + assert asin(-1) == -pi/2 + assert asin(sqrt(3)/2) == pi/3 + assert asin(-sqrt(3)/2) == -pi/3 + assert asin(sqrt(2)/2) == pi/4 + assert asin(-sqrt(2)/2) == -pi/4 + assert asin(sqrt((5 - sqrt(5))/8)) == pi/5 + assert asin(-sqrt((5 - sqrt(5))/8)) == -pi/5 + assert asin(S.Half) == pi/6 + assert asin(Rational(-1, 2)) == -pi/6 + assert asin((sqrt(2 - sqrt(2)))/2) == pi/8 + assert asin(-(sqrt(2 - sqrt(2)))/2) == -pi/8 + assert asin((sqrt(5) - 1)/4) == pi/10 + assert asin(-(sqrt(5) - 1)/4) == -pi/10 + assert asin((sqrt(3) - 1)/sqrt(2**3)) == pi/12 + assert asin(-(sqrt(3) - 1)/sqrt(2**3)) == -pi/12 + + # check round-trip for exact values: + for d in [5, 6, 8, 10, 12]: + for n in range(-(d//2), d//2 + 1): + if gcd(n, d) == 1: + assert asin(sin(n*pi/d)) == n*pi/d + + assert asin(x).diff(x) == 1/sqrt(1 - x**2) + + assert asin(0.2, evaluate=False).is_real is True + assert asin(-2).is_real is False + assert asin(r).is_real is None + + assert asin(-2*I) == -I*asinh(2) + + assert asin(Rational(1, 7), evaluate=False).is_positive is True + assert asin(Rational(-1, 7), evaluate=False).is_positive is False + assert asin(p).is_positive is None + assert asin(sin(Rational(7, 2))) == Rational(-7, 2) + pi + assert asin(sin(Rational(-7, 4))) == Rational(7, 4) - pi + assert unchanged(asin, cos(x)) + + +def test_asin_series(): + assert asin(x).series(x, 0, 9) == \ + x + x**3/6 + 3*x**5/40 + 5*x**7/112 + O(x**9) + t5 = asin(x).taylor_term(5, x) + assert t5 == 3*x**5/40 + assert asin(x).taylor_term(7, x, t5, 0) == 5*x**7/112 + + +def test_asin_leading_term(): + assert asin(x).as_leading_term(x) == x + # Tests concerning branch points + assert asin(x + 1).as_leading_term(x) == pi/2 + assert asin(x - 1).as_leading_term(x) == -pi/2 + assert asin(1/x).as_leading_term(x, cdir=1) == I*log(x) + pi/2 - I*log(2) + assert asin(1/x).as_leading_term(x, cdir=-1) == -I*log(x) - 3*pi/2 + I*log(2) + # Tests concerning points lying on branch cuts + assert asin(I*x + 2).as_leading_term(x, cdir=1) == pi - asin(2) + assert asin(-I*x + 2).as_leading_term(x, cdir=1) == asin(2) + assert asin(I*x - 2).as_leading_term(x, cdir=1) == -asin(2) + assert asin(-I*x - 2).as_leading_term(x, cdir=1) == -pi + asin(2) + # Tests concerning im(ndir) == 0 + assert asin(-I*x**2 + x - 2).as_leading_term(x, cdir=1) == -pi/2 + I*log(2 - sqrt(3)) + assert asin(-I*x**2 + x - 2).as_leading_term(x, cdir=-1) == -pi/2 + I*log(2 - sqrt(3)) + + +def test_asin_rewrite(): + assert asin(x).rewrite(log) == -I*log(I*x + sqrt(1 - x**2)) + assert asin(x).rewrite(atan) == 2*atan(x/(1 + sqrt(1 - x**2))) + assert asin(x).rewrite(acos) == S.Pi/2 - acos(x) + assert asin(x).rewrite(acot) == 2*acot((sqrt(-x**2 + 1) + 1)/x) + assert asin(x).rewrite(asec) == -asec(1/x) + pi/2 + assert asin(x).rewrite(acsc) == acsc(1/x) + + +def test_asin_fdiff(): + assert asin(x).fdiff() == 1/sqrt(1 - x**2) + raises(ArgumentIndexError, lambda: asin(x).fdiff(2)) + + +def test_acos(): + assert acos(nan) is nan + assert acos(zoo) is zoo + + assert acos.nargs == FiniteSet(1) + assert acos(oo) == I*oo + assert acos(-oo) == -I*oo + + # Note: acos(-x) = pi - acos(x) + assert acos(0) == pi/2 + assert acos(S.Half) == pi/3 + assert acos(Rational(-1, 2)) == pi*Rational(2, 3) + assert acos(1) == 0 + assert acos(-1) == pi + assert acos(sqrt(2)/2) == pi/4 + assert acos(-sqrt(2)/2) == pi*Rational(3, 4) + + # check round-trip for exact values: + for d in range(5, 13): + for num in range(d): + if gcd(num, d) == 1: + assert acos(cos(num*pi/d)) == num*pi/d + assert acos(-cos(num*pi/d)) == pi - num*pi/d + assert acos(sin(num*pi/d)) == pi/2 - asin(sin(num*pi/d)) + assert acos(-sin(num*pi/d)) == pi/2 - asin(-sin(num*pi/d)) + + assert acos(2*I) == pi/2 - asin(2*I) + + assert acos(x).diff(x) == -1/sqrt(1 - x**2) + + assert acos(0.2).is_real is True + assert acos(-2).is_real is False + assert acos(r).is_real is None + + assert acos(Rational(1, 7), evaluate=False).is_positive is True + assert acos(Rational(-1, 7), evaluate=False).is_positive is True + assert acos(Rational(3, 2), evaluate=False).is_positive is False + assert acos(p).is_positive is None + + assert acos(2 + p).conjugate() != acos(10 + p) + assert acos(-3 + n).conjugate() != acos(-3 + n) + assert acos(Rational(1, 3)).conjugate() == acos(Rational(1, 3)) + assert acos(Rational(-1, 3)).conjugate() == acos(Rational(-1, 3)) + assert acos(p + n*I).conjugate() == acos(p - n*I) + assert acos(z).conjugate() != acos(conjugate(z)) + + +def test_acos_leading_term(): + assert acos(x).as_leading_term(x) == pi/2 + # Tests concerning branch points + assert acos(x + 1).as_leading_term(x) == sqrt(2)*sqrt(-x) + assert acos(x - 1).as_leading_term(x) == pi + assert acos(1/x).as_leading_term(x, cdir=1) == -I*log(x) + I*log(2) + assert acos(1/x).as_leading_term(x, cdir=-1) == I*log(x) + 2*pi - I*log(2) + # Tests concerning points lying on branch cuts + assert acos(I*x + 2).as_leading_term(x, cdir=1) == -acos(2) + assert acos(-I*x + 2).as_leading_term(x, cdir=1) == acos(2) + assert acos(I*x - 2).as_leading_term(x, cdir=1) == acos(-2) + assert acos(-I*x - 2).as_leading_term(x, cdir=1) == 2*pi - acos(-2) + # Tests concerning im(ndir) == 0 + assert acos(-I*x**2 + x - 2).as_leading_term(x, cdir=1) == pi + I*log(sqrt(3) + 2) + assert acos(-I*x**2 + x - 2).as_leading_term(x, cdir=-1) == pi + I*log(sqrt(3) + 2) + + +def test_acos_series(): + assert acos(x).series(x, 0, 8) == \ + pi/2 - x - x**3/6 - 3*x**5/40 - 5*x**7/112 + O(x**8) + assert acos(x).series(x, 0, 8) == pi/2 - asin(x).series(x, 0, 8) + t5 = acos(x).taylor_term(5, x) + assert t5 == -3*x**5/40 + assert acos(x).taylor_term(7, x, t5, 0) == -5*x**7/112 + assert acos(x).taylor_term(0, x) == pi/2 + assert acos(x).taylor_term(2, x) is S.Zero + + +def test_acos_rewrite(): + assert acos(x).rewrite(log) == pi/2 + I*log(I*x + sqrt(1 - x**2)) + assert acos(x).rewrite(atan) == pi*(-x*sqrt(x**(-2)) + 1)/2 + atan(sqrt(1 - x**2)/x) + assert acos(0).rewrite(atan) == S.Pi/2 + assert acos(0.5).rewrite(atan) == acos(0.5).rewrite(log) + assert acos(x).rewrite(asin) == S.Pi/2 - asin(x) + assert acos(x).rewrite(acot) == -2*acot((sqrt(-x**2 + 1) + 1)/x) + pi/2 + assert acos(x).rewrite(asec) == asec(1/x) + assert acos(x).rewrite(acsc) == -acsc(1/x) + pi/2 + + +def test_acos_fdiff(): + assert acos(x).fdiff() == -1/sqrt(1 - x**2) + raises(ArgumentIndexError, lambda: acos(x).fdiff(2)) + + +def test_atan(): + assert atan(nan) is nan + + assert atan.nargs == FiniteSet(1) + assert atan(oo) == pi/2 + assert atan(-oo) == -pi/2 + assert atan(zoo) == AccumBounds(-pi/2, pi/2) + + assert atan(0) == 0 + assert atan(1) == pi/4 + assert atan(sqrt(3)) == pi/3 + assert atan(-(1 + sqrt(2))) == pi*Rational(-3, 8) + assert atan(sqrt(5 - 2 * sqrt(5))) == pi/5 + assert atan(-sqrt(1 - 2 * sqrt(5)/ 5)) == -pi/10 + assert atan(sqrt(1 + 2 * sqrt(5) / 5)) == pi*Rational(3, 10) + assert atan(-2 + sqrt(3)) == -pi/12 + assert atan(2 + sqrt(3)) == pi*Rational(5, 12) + assert atan(-2 - sqrt(3)) == pi*Rational(-5, 12) + + # check round-trip for exact values: + for d in [5, 6, 8, 10, 12]: + for num in range(-(d//2), d//2 + 1): + if gcd(num, d) == 1: + assert atan(tan(num*pi/d)) == num*pi/d + + assert atan(oo) == pi/2 + assert atan(x).diff(x) == 1/(1 + x**2) + + assert atan(r).is_real is True + + assert atan(-2*I) == -I*atanh(2) + assert unchanged(atan, cot(x)) + assert atan(cot(Rational(1, 4))) == Rational(-1, 4) + pi/2 + assert acot(Rational(1, 4)).is_rational is False + + for s in (x, p, n, np, nn, nz, ep, en, enp, enn, enz): + if s.is_real or s.is_extended_real is None: + assert s.is_nonzero is atan(s).is_nonzero + assert s.is_positive is atan(s).is_positive + assert s.is_negative is atan(s).is_negative + assert s.is_nonpositive is atan(s).is_nonpositive + assert s.is_nonnegative is atan(s).is_nonnegative + else: + assert s.is_extended_nonzero is atan(s).is_nonzero + assert s.is_extended_positive is atan(s).is_positive + assert s.is_extended_negative is atan(s).is_negative + assert s.is_extended_nonpositive is atan(s).is_nonpositive + assert s.is_extended_nonnegative is atan(s).is_nonnegative + assert s.is_extended_nonzero is atan(s).is_extended_nonzero + assert s.is_extended_positive is atan(s).is_extended_positive + assert s.is_extended_negative is atan(s).is_extended_negative + assert s.is_extended_nonpositive is atan(s).is_extended_nonpositive + assert s.is_extended_nonnegative is atan(s).is_extended_nonnegative + + +def test_atan_rewrite(): + assert atan(x).rewrite(log) == I*(log(1 - I*x)-log(1 + I*x))/2 + assert atan(x).rewrite(asin) == (-asin(1/sqrt(x**2 + 1)) + pi/2)*sqrt(x**2)/x + assert atan(x).rewrite(acos) == sqrt(x**2)*acos(1/sqrt(x**2 + 1))/x + assert atan(x).rewrite(acot) == acot(1/x) + assert atan(x).rewrite(asec) == sqrt(x**2)*asec(sqrt(x**2 + 1))/x + assert atan(x).rewrite(acsc) == (-acsc(sqrt(x**2 + 1)) + pi/2)*sqrt(x**2)/x + + assert atan(-5*I).evalf() == atan(x).rewrite(log).evalf(subs={x:-5*I}) + assert atan(5*I).evalf() == atan(x).rewrite(log).evalf(subs={x:5*I}) + + +def test_atan_fdiff(): + assert atan(x).fdiff() == 1/(x**2 + 1) + raises(ArgumentIndexError, lambda: atan(x).fdiff(2)) + + +def test_atan_leading_term(): + assert atan(x).as_leading_term(x) == x + assert atan(1/x).as_leading_term(x, cdir=1) == pi/2 + assert atan(1/x).as_leading_term(x, cdir=-1) == -pi/2 + # Tests concerning branch points + assert atan(x + I).as_leading_term(x, cdir=1) == -I*log(x)/2 + pi/4 + I*log(2)/2 + assert atan(x + I).as_leading_term(x, cdir=-1) == -I*log(x)/2 - 3*pi/4 + I*log(2)/2 + assert atan(x - I).as_leading_term(x, cdir=1) == I*log(x)/2 + pi/4 - I*log(2)/2 + assert atan(x - I).as_leading_term(x, cdir=-1) == I*log(x)/2 + pi/4 - I*log(2)/2 + # Tests concerning points lying on branch cuts + assert atan(x + 2*I).as_leading_term(x, cdir=1) == I*atanh(2) + assert atan(x + 2*I).as_leading_term(x, cdir=-1) == -pi + I*atanh(2) + assert atan(x - 2*I).as_leading_term(x, cdir=1) == pi - I*atanh(2) + assert atan(x - 2*I).as_leading_term(x, cdir=-1) == -I*atanh(2) + # Tests concerning re(ndir) == 0 + assert atan(2*I - I*x - x**2).as_leading_term(x, cdir=1) == -pi/2 + I*log(3)/2 + assert atan(2*I - I*x - x**2).as_leading_term(x, cdir=-1) == -pi/2 + I*log(3)/2 + + +def test_atan2(): + assert atan2.nargs == FiniteSet(2) + assert atan2(0, 0) is S.NaN + assert atan2(0, 1) == 0 + assert atan2(1, 1) == pi/4 + assert atan2(1, 0) == pi/2 + assert atan2(1, -1) == pi*Rational(3, 4) + assert atan2(0, -1) == pi + assert atan2(-1, -1) == pi*Rational(-3, 4) + assert atan2(-1, 0) == -pi/2 + assert atan2(-1, 1) == -pi/4 + i = symbols('i', imaginary=True) + r = symbols('r', real=True) + eq = atan2(r, i) + ans = -I*log((i + I*r)/sqrt(i**2 + r**2)) + reps = ((r, 2), (i, I)) + assert eq.subs(reps) == ans.subs(reps) + + x = Symbol('x', negative=True) + y = Symbol('y', negative=True) + assert atan2(y, x) == atan(y/x) - pi + y = Symbol('y', nonnegative=True) + assert atan2(y, x) == atan(y/x) + pi + y = Symbol('y') + assert atan2(y, x) == atan2(y, x, evaluate=False) + + u = Symbol("u", positive=True) + assert atan2(0, u) == 0 + u = Symbol("u", negative=True) + assert atan2(0, u) == pi + + assert atan2(y, oo) == 0 + assert atan2(y, -oo)== 2*pi*Heaviside(re(y), S.Half) - pi + + assert atan2(y, x).rewrite(log) == -I*log((x + I*y)/sqrt(x**2 + y**2)) + assert atan2(0, 0) is S.NaN + + ex = atan2(y, x) - arg(x + I*y) + assert ex.subs({x:2, y:3}).rewrite(arg) == 0 + assert ex.subs({x:2, y:3*I}).rewrite(arg) == -pi - I*log(sqrt(5)*I/5) + assert ex.subs({x:2*I, y:3}).rewrite(arg) == -pi/2 - I*log(sqrt(5)*I) + assert ex.subs({x:2*I, y:3*I}).rewrite(arg) == -pi + atan(Rational(2, 3)) + atan(Rational(3, 2)) + i = symbols('i', imaginary=True) + r = symbols('r', real=True) + e = atan2(i, r) + rewrite = e.rewrite(arg) + reps = {i: I, r: -2} + assert rewrite == -I*log(abs(I*i + r)/sqrt(abs(i**2 + r**2))) + arg((I*i + r)/sqrt(i**2 + r**2)) + assert (e - rewrite).subs(reps).equals(0) + + assert atan2(0, x).rewrite(atan) == Piecewise((pi, re(x) < 0), + (0, Ne(x, 0)), + (nan, True)) + assert atan2(0, r).rewrite(atan) == Piecewise((pi, r < 0), (0, Ne(r, 0)), (S.NaN, True)) + assert atan2(0, i),rewrite(atan) == 0 + assert atan2(0, r + i).rewrite(atan) == Piecewise((pi, r < 0), (0, True)) + + assert atan2(y, x).rewrite(atan) == Piecewise( + (2*atan(y/(x + sqrt(x**2 + y**2))), Ne(y, 0)), + (pi, re(x) < 0), + (0, (re(x) > 0) | Ne(im(x), 0)), + (nan, True)) + assert conjugate(atan2(x, y)) == atan2(conjugate(x), conjugate(y)) + + assert diff(atan2(y, x), x) == -y/(x**2 + y**2) + assert diff(atan2(y, x), y) == x/(x**2 + y**2) + + assert simplify(diff(atan2(y, x).rewrite(log), x)) == -y/(x**2 + y**2) + assert simplify(diff(atan2(y, x).rewrite(log), y)) == x/(x**2 + y**2) + + assert str(atan2(1, 2).evalf(5)) == '0.46365' + raises(ArgumentIndexError, lambda: atan2(x, y).fdiff(3)) + +def test_issue_17461(): + class A(Symbol): + is_extended_real = True + + def _eval_evalf(self, prec): + return Float(5.0) + + x = A('X') + y = A('Y') + assert abs(atan2(x, y).evalf() - 0.785398163397448) <= 1e-10 + +def test_acot(): + assert acot(nan) is nan + + assert acot.nargs == FiniteSet(1) + assert acot(-oo) == 0 + assert acot(oo) == 0 + assert acot(zoo) == 0 + assert acot(1) == pi/4 + assert acot(0) == pi/2 + assert acot(sqrt(3)/3) == pi/3 + assert acot(1/sqrt(3)) == pi/3 + assert acot(-1/sqrt(3)) == -pi/3 + assert acot(x).diff(x) == -1/(1 + x**2) + + assert acot(r).is_extended_real is True + + assert acot(I*pi) == -I*acoth(pi) + assert acot(-2*I) == I*acoth(2) + assert acot(x).is_positive is None + assert acot(n).is_positive is False + assert acot(p).is_positive is True + assert acot(I).is_positive is False + assert acot(Rational(1, 4)).is_rational is False + assert unchanged(acot, cot(x)) + assert unchanged(acot, tan(x)) + assert acot(cot(Rational(1, 4))) == Rational(1, 4) + assert acot(tan(Rational(-1, 4))) == Rational(1, 4) - pi/2 + + +def test_acot_rewrite(): + assert acot(x).rewrite(log) == I*(log(1 - I/x)-log(1 + I/x))/2 + assert acot(x).rewrite(asin) == x*(-asin(sqrt(-x**2)/sqrt(-x**2 - 1)) + pi/2)*sqrt(x**(-2)) + assert acot(x).rewrite(acos) == x*sqrt(x**(-2))*acos(sqrt(-x**2)/sqrt(-x**2 - 1)) + assert acot(x).rewrite(atan) == atan(1/x) + assert acot(x).rewrite(asec) == x*sqrt(x**(-2))*asec(sqrt((x**2 + 1)/x**2)) + assert acot(x).rewrite(acsc) == x*(-acsc(sqrt((x**2 + 1)/x**2)) + pi/2)*sqrt(x**(-2)) + + assert acot(-I/5).evalf() == acot(x).rewrite(log).evalf(subs={x:-I/5}) + assert acot(I/5).evalf() == acot(x).rewrite(log).evalf(subs={x:I/5}) + + +def test_acot_fdiff(): + assert acot(x).fdiff() == -1/(x**2 + 1) + raises(ArgumentIndexError, lambda: acot(x).fdiff(2)) + +def test_acot_leading_term(): + assert acot(1/x).as_leading_term(x) == x + # Tests concerning branch points + assert acot(x + I).as_leading_term(x, cdir=1) == I*log(x)/2 + pi/4 - I*log(2)/2 + assert acot(x + I).as_leading_term(x, cdir=-1) == I*log(x)/2 + pi/4 - I*log(2)/2 + assert acot(x - I).as_leading_term(x, cdir=1) == -I*log(x)/2 + pi/4 + I*log(2)/2 + assert acot(x - I).as_leading_term(x, cdir=-1) == -I*log(x)/2 - 3*pi/4 + I*log(2)/2 + # Tests concerning points lying on branch cuts + assert acot(x).as_leading_term(x, cdir=1) == pi/2 + assert acot(x).as_leading_term(x, cdir=-1) == -pi/2 + assert acot(x + I/2).as_leading_term(x, cdir=1) == pi - I*acoth(S(1)/2) + assert acot(x + I/2).as_leading_term(x, cdir=-1) == -I*acoth(S(1)/2) + assert acot(x - I/2).as_leading_term(x, cdir=1) == I*acoth(S(1)/2) + assert acot(x - I/2).as_leading_term(x, cdir=-1) == -pi + I*acoth(S(1)/2) + # Tests concerning re(ndir) == 0 + assert acot(I/2 - I*x - x**2).as_leading_term(x, cdir=1) == -pi/2 - I*log(3)/2 + assert acot(I/2 - I*x - x**2).as_leading_term(x, cdir=-1) == -pi/2 - I*log(3)/2 + + +def test_attributes(): + assert sin(x).args == (x,) + + +def test_sincos_rewrite(): + assert sin(pi/2 - x) == cos(x) + assert sin(pi - x) == sin(x) + assert cos(pi/2 - x) == sin(x) + assert cos(pi - x) == -cos(x) + + +def _check_even_rewrite(func, arg): + """Checks that the expr has been rewritten using f(-x) -> f(x) + arg : -x + """ + return func(arg).args[0] == -arg + + +def _check_odd_rewrite(func, arg): + """Checks that the expr has been rewritten using f(-x) -> -f(x) + arg : -x + """ + return func(arg).func.is_Mul + + +def _check_no_rewrite(func, arg): + """Checks that the expr is not rewritten""" + return func(arg).args[0] == arg + + +def test_evenodd_rewrite(): + a = cos(2) # negative + b = sin(1) # positive + even = [cos] + odd = [sin, tan, cot, asin, atan, acot] + with_minus = [-1, -2**1024 * E, -pi/105, -x*y, -x - y] + for func in even: + for expr in with_minus: + assert _check_even_rewrite(func, expr) + assert _check_no_rewrite(func, a*b) + assert func( + x - y) == func(y - x) # it doesn't matter which form is canonical + for func in odd: + for expr in with_minus: + assert _check_odd_rewrite(func, expr) + assert _check_no_rewrite(func, a*b) + assert func( + x - y) == -func(y - x) # it doesn't matter which form is canonical + + +def test_as_leading_term_issue_5272(): + assert sin(x).as_leading_term(x) == x + assert cos(x).as_leading_term(x) == 1 + assert tan(x).as_leading_term(x) == x + assert cot(x).as_leading_term(x) == 1/x + + +def test_leading_terms(): + assert sin(1/x).as_leading_term(x) == AccumBounds(-1, 1) + assert sin(S.Half).as_leading_term(x) == sin(S.Half) + assert cos(1/x).as_leading_term(x) == AccumBounds(-1, 1) + assert cos(S.Half).as_leading_term(x) == cos(S.Half) + assert sec(1/x).as_leading_term(x) == AccumBounds(S.NegativeInfinity, S.Infinity) + assert csc(1/x).as_leading_term(x) == AccumBounds(S.NegativeInfinity, S.Infinity) + assert tan(1/x).as_leading_term(x) == AccumBounds(S.NegativeInfinity, S.Infinity) + assert cot(1/x).as_leading_term(x) == AccumBounds(S.NegativeInfinity, S.Infinity) + + # https://github.com/sympy/sympy/issues/21038 + f = sin(pi*(x + 4))/(3*x) + assert f.as_leading_term(x) == pi/3 + + +def test_atan2_expansion(): + assert cancel(atan2(x**2, x + 1).diff(x) - atan(x**2/(x + 1)).diff(x)) == 0 + assert cancel(atan(y/x).series(y, 0, 5) - atan2(y, x).series(y, 0, 5) + + atan2(0, x) - atan(0)) == O(y**5) + assert cancel(atan(y/x).series(x, 1, 4) - atan2(y, x).series(x, 1, 4) + + atan2(y, 1) - atan(y)) == O((x - 1)**4, (x, 1)) + assert cancel(atan((y + x)/x).series(x, 1, 3) - atan2(y + x, x).series(x, 1, 3) + + atan2(1 + y, 1) - atan(1 + y)) == O((x - 1)**3, (x, 1)) + assert Matrix([atan2(y, x)]).jacobian([y, x]) == \ + Matrix([[x/(y**2 + x**2), -y/(y**2 + x**2)]]) + + +def test_aseries(): + def t(n, v, d, e): + assert abs( + n(1/v).evalf() - n(1/x).series(x, dir=d).removeO().subs(x, v)) < e + t(atan, 0.1, '+', 1e-5) + t(atan, -0.1, '-', 1e-5) + t(acot, 0.1, '+', 1e-5) + t(acot, -0.1, '-', 1e-5) + + +def test_issue_4420(): + i = Symbol('i', integer=True) + e = Symbol('e', even=True) + o = Symbol('o', odd=True) + + # unknown parity for variable + assert cos(4*i*pi) == 1 + assert sin(4*i*pi) == 0 + assert tan(4*i*pi) == 0 + assert cot(4*i*pi) is zoo + + assert cos(3*i*pi) == cos(pi*i) # +/-1 + assert sin(3*i*pi) == 0 + assert tan(3*i*pi) == 0 + assert cot(3*i*pi) is zoo + + assert cos(4.0*i*pi) == 1 + assert sin(4.0*i*pi) == 0 + assert tan(4.0*i*pi) == 0 + assert cot(4.0*i*pi) is zoo + + assert cos(3.0*i*pi) == cos(pi*i) # +/-1 + assert sin(3.0*i*pi) == 0 + assert tan(3.0*i*pi) == 0 + assert cot(3.0*i*pi) is zoo + + assert cos(4.5*i*pi) == cos(0.5*pi*i) + assert sin(4.5*i*pi) == sin(0.5*pi*i) + assert tan(4.5*i*pi) == tan(0.5*pi*i) + assert cot(4.5*i*pi) == cot(0.5*pi*i) + + # parity of variable is known + assert cos(4*e*pi) == 1 + assert sin(4*e*pi) == 0 + assert tan(4*e*pi) == 0 + assert cot(4*e*pi) is zoo + + assert cos(3*e*pi) == 1 + assert sin(3*e*pi) == 0 + assert tan(3*e*pi) == 0 + assert cot(3*e*pi) is zoo + + assert cos(4.0*e*pi) == 1 + assert sin(4.0*e*pi) == 0 + assert tan(4.0*e*pi) == 0 + assert cot(4.0*e*pi) is zoo + + assert cos(3.0*e*pi) == 1 + assert sin(3.0*e*pi) == 0 + assert tan(3.0*e*pi) == 0 + assert cot(3.0*e*pi) is zoo + + assert cos(4.5*e*pi) == cos(0.5*pi*e) + assert sin(4.5*e*pi) == sin(0.5*pi*e) + assert tan(4.5*e*pi) == tan(0.5*pi*e) + assert cot(4.5*e*pi) == cot(0.5*pi*e) + + assert cos(4*o*pi) == 1 + assert sin(4*o*pi) == 0 + assert tan(4*o*pi) == 0 + assert cot(4*o*pi) is zoo + + assert cos(3*o*pi) == -1 + assert sin(3*o*pi) == 0 + assert tan(3*o*pi) == 0 + assert cot(3*o*pi) is zoo + + assert cos(4.0*o*pi) == 1 + assert sin(4.0*o*pi) == 0 + assert tan(4.0*o*pi) == 0 + assert cot(4.0*o*pi) is zoo + + assert cos(3.0*o*pi) == -1 + assert sin(3.0*o*pi) == 0 + assert tan(3.0*o*pi) == 0 + assert cot(3.0*o*pi) is zoo + + assert cos(4.5*o*pi) == cos(0.5*pi*o) + assert sin(4.5*o*pi) == sin(0.5*pi*o) + assert tan(4.5*o*pi) == tan(0.5*pi*o) + assert cot(4.5*o*pi) == cot(0.5*pi*o) + + # x could be imaginary + assert cos(4*x*pi) == cos(4*pi*x) + assert sin(4*x*pi) == sin(4*pi*x) + assert tan(4*x*pi) == tan(4*pi*x) + assert cot(4*x*pi) == cot(4*pi*x) + + assert cos(3*x*pi) == cos(3*pi*x) + assert sin(3*x*pi) == sin(3*pi*x) + assert tan(3*x*pi) == tan(3*pi*x) + assert cot(3*x*pi) == cot(3*pi*x) + + assert cos(4.0*x*pi) == cos(4.0*pi*x) + assert sin(4.0*x*pi) == sin(4.0*pi*x) + assert tan(4.0*x*pi) == tan(4.0*pi*x) + assert cot(4.0*x*pi) == cot(4.0*pi*x) + + assert cos(3.0*x*pi) == cos(3.0*pi*x) + assert sin(3.0*x*pi) == sin(3.0*pi*x) + assert tan(3.0*x*pi) == tan(3.0*pi*x) + assert cot(3.0*x*pi) == cot(3.0*pi*x) + + assert cos(4.5*x*pi) == cos(4.5*pi*x) + assert sin(4.5*x*pi) == sin(4.5*pi*x) + assert tan(4.5*x*pi) == tan(4.5*pi*x) + assert cot(4.5*x*pi) == cot(4.5*pi*x) + + +def test_inverses(): + raises(AttributeError, lambda: sin(x).inverse()) + raises(AttributeError, lambda: cos(x).inverse()) + assert tan(x).inverse() == atan + assert cot(x).inverse() == acot + raises(AttributeError, lambda: csc(x).inverse()) + raises(AttributeError, lambda: sec(x).inverse()) + assert asin(x).inverse() == sin + assert acos(x).inverse() == cos + assert atan(x).inverse() == tan + assert acot(x).inverse() == cot + + +def test_real_imag(): + a, b = symbols('a b', real=True) + z = a + b*I + for deep in [True, False]: + assert sin( + z).as_real_imag(deep=deep) == (sin(a)*cosh(b), cos(a)*sinh(b)) + assert cos( + z).as_real_imag(deep=deep) == (cos(a)*cosh(b), -sin(a)*sinh(b)) + assert tan(z).as_real_imag(deep=deep) == (sin(2*a)/(cos(2*a) + + cosh(2*b)), sinh(2*b)/(cos(2*a) + cosh(2*b))) + assert cot(z).as_real_imag(deep=deep) == (-sin(2*a)/(cos(2*a) - + cosh(2*b)), sinh(2*b)/(cos(2*a) - cosh(2*b))) + assert sin(a).as_real_imag(deep=deep) == (sin(a), 0) + assert cos(a).as_real_imag(deep=deep) == (cos(a), 0) + assert tan(a).as_real_imag(deep=deep) == (tan(a), 0) + assert cot(a).as_real_imag(deep=deep) == (cot(a), 0) + + +@slow +def test_sincos_rewrite_sqrt(): + # equivalent to testing rewrite(pow) + for p in [1, 3, 5, 17]: + for t in [1, 8]: + n = t*p + # The vertices `exp(i*pi/n)` of a regular `n`-gon can + # be expressed by means of nested square roots if and + # only if `n` is a product of Fermat primes, `p`, and + # powers of 2, `t'. The code aims to check all vertices + # not belonging to an `m`-gon for `m < n`(`gcd(i, n) == 1`). + # For large `n` this makes the test too slow, therefore + # the vertices are limited to those of index `i < 10`. + for i in range(1, min((n + 1)//2 + 1, 10)): + if 1 == gcd(i, n): + x = i*pi/n + s1 = sin(x).rewrite(sqrt) + c1 = cos(x).rewrite(sqrt) + assert not s1.has(cos, sin), "fails for %d*pi/%d" % (i, n) + assert not c1.has(cos, sin), "fails for %d*pi/%d" % (i, n) + assert 1e-3 > abs(sin(x.evalf(5)) - s1.evalf(2)), "fails for %d*pi/%d" % (i, n) + assert 1e-3 > abs(cos(x.evalf(5)) - c1.evalf(2)), "fails for %d*pi/%d" % (i, n) + assert cos(pi/14).rewrite(sqrt) == sqrt(cos(pi/7)/2 + S.Half) + assert cos(pi*Rational(-15, 2)/11, evaluate=False).rewrite( + sqrt) == -sqrt(-cos(pi*Rational(4, 11))/2 + S.Half) + assert cos(Mul(2, pi, S.Half, evaluate=False), evaluate=False).rewrite( + sqrt) == -1 + e = cos(pi/3/17) # don't use pi/15 since that is caught at instantiation + a = ( + -3*sqrt(-sqrt(17) + 17)*sqrt(sqrt(17) + 17)/64 - + 3*sqrt(34)*sqrt(sqrt(17) + 17)/128 - sqrt(sqrt(17) + + 17)*sqrt(-8*sqrt(2)*sqrt(sqrt(17) + 17) - sqrt(2)*sqrt(-sqrt(17) + 17) + + sqrt(34)*sqrt(-sqrt(17) + 17) + 6*sqrt(17) + 34)/64 - sqrt(-sqrt(17) + + 17)*sqrt(-8*sqrt(2)*sqrt(sqrt(17) + 17) - sqrt(2)*sqrt(-sqrt(17) + + 17) + sqrt(34)*sqrt(-sqrt(17) + 17) + 6*sqrt(17) + 34)/128 - Rational(1, 32) + + sqrt(2)*sqrt(-8*sqrt(2)*sqrt(sqrt(17) + 17) - sqrt(2)*sqrt(-sqrt(17) + + 17) + sqrt(34)*sqrt(-sqrt(17) + 17) + 6*sqrt(17) + 34)/64 + + 3*sqrt(2)*sqrt(sqrt(17) + 17)/128 + sqrt(34)*sqrt(-sqrt(17) + 17)/128 + + 13*sqrt(2)*sqrt(-sqrt(17) + 17)/128 + sqrt(17)*sqrt(-sqrt(17) + + 17)*sqrt(-8*sqrt(2)*sqrt(sqrt(17) + 17) - sqrt(2)*sqrt(-sqrt(17) + 17) + + sqrt(34)*sqrt(-sqrt(17) + 17) + 6*sqrt(17) + 34)/128 + 5*sqrt(17)/32 + + sqrt(3)*sqrt(-sqrt(2)*sqrt(sqrt(17) + 17)*sqrt(sqrt(17)/32 + + sqrt(2)*sqrt(-sqrt(17) + 17)/32 + + sqrt(2)*sqrt(-8*sqrt(2)*sqrt(sqrt(17) + 17) - sqrt(2)*sqrt(-sqrt(17) + + 17) + sqrt(34)*sqrt(-sqrt(17) + 17) + 6*sqrt(17) + 34)/32 + Rational(15, 32))/8 - + 5*sqrt(2)*sqrt(sqrt(17)/32 + sqrt(2)*sqrt(-sqrt(17) + 17)/32 + + sqrt(2)*sqrt(-8*sqrt(2)*sqrt(sqrt(17) + 17) - sqrt(2)*sqrt(-sqrt(17) + + 17) + sqrt(34)*sqrt(-sqrt(17) + 17) + 6*sqrt(17) + 34)/32 + + Rational(15, 32))*sqrt(-8*sqrt(2)*sqrt(sqrt(17) + 17) - sqrt(2)*sqrt(-sqrt(17) + + 17) + sqrt(34)*sqrt(-sqrt(17) + 17) + 6*sqrt(17) + 34)/64 - + 3*sqrt(2)*sqrt(-sqrt(17) + 17)*sqrt(sqrt(17)/32 + + sqrt(2)*sqrt(-sqrt(17) + 17)/32 + + sqrt(2)*sqrt(-8*sqrt(2)*sqrt(sqrt(17) + 17) - sqrt(2)*sqrt(-sqrt(17) + + 17) + sqrt(34)*sqrt(-sqrt(17) + 17) + 6*sqrt(17) + 34)/32 + Rational(15, 32))/32 + + sqrt(34)*sqrt(sqrt(17)/32 + sqrt(2)*sqrt(-sqrt(17) + 17)/32 + + sqrt(2)*sqrt(-8*sqrt(2)*sqrt(sqrt(17) + 17) - sqrt(2)*sqrt(-sqrt(17) + + 17) + sqrt(34)*sqrt(-sqrt(17) + 17) + 6*sqrt(17) + 34)/32 + + Rational(15, 32))*sqrt(-8*sqrt(2)*sqrt(sqrt(17) + 17) - sqrt(2)*sqrt(-sqrt(17) + + 17) + sqrt(34)*sqrt(-sqrt(17) + 17) + 6*sqrt(17) + 34)/64 + + sqrt(sqrt(17)/32 + sqrt(2)*sqrt(-sqrt(17) + 17)/32 + + sqrt(2)*sqrt(-8*sqrt(2)*sqrt(sqrt(17) + 17) - sqrt(2)*sqrt(-sqrt(17) + + 17) + sqrt(34)*sqrt(-sqrt(17) + 17) + 6*sqrt(17) + 34)/32 + Rational(15, 32))/2 + + S.Half + sqrt(-sqrt(17) + 17)*sqrt(sqrt(17)/32 + sqrt(2)*sqrt(-sqrt(17) + + 17)/32 + sqrt(2)*sqrt(-8*sqrt(2)*sqrt(sqrt(17) + 17) - + sqrt(2)*sqrt(-sqrt(17) + 17) + sqrt(34)*sqrt(-sqrt(17) + 17) + + 6*sqrt(17) + 34)/32 + Rational(15, 32))*sqrt(-8*sqrt(2)*sqrt(sqrt(17) + 17) - + sqrt(2)*sqrt(-sqrt(17) + 17) + sqrt(34)*sqrt(-sqrt(17) + 17) + + 6*sqrt(17) + 34)/32 + sqrt(34)*sqrt(-sqrt(17) + 17)*sqrt(sqrt(17)/32 + + sqrt(2)*sqrt(-sqrt(17) + 17)/32 + + sqrt(2)*sqrt(-8*sqrt(2)*sqrt(sqrt(17) + 17) - sqrt(2)*sqrt(-sqrt(17) + + 17) + sqrt(34)*sqrt(-sqrt(17) + 17) + 6*sqrt(17) + 34)/32 + + Rational(15, 32))/32)/2) + assert e.rewrite(sqrt) == a + assert e.n() == a.n() + # coverage of fermatCoords: multiplicity > 1; the following could be + # different but that portion of the code should be tested in some way + assert cos(pi/9/17).rewrite(sqrt) == \ + sin(pi/9)*sin(pi*Rational(2, 17)) + cos(pi/9)*cos(pi*Rational(2, 17)) + + +@slow +def test_sincos_rewrite_sqrt_257(): + assert cos(pi/257).rewrite(sqrt).evalf(64) == cos(pi/257).evalf(64) + + +@slow +def test_tancot_rewrite_sqrt(): + # equivalent to testing rewrite(pow) + for p in [1, 3, 5, 17]: + for t in [1, 8]: + n = t*p + for i in range(1, min((n + 1)//2 + 1, 10)): + if 1 == gcd(i, n): + x = i*pi/n + if 2*i != n and 3*i != 2*n: + t1 = tan(x).rewrite(sqrt) + assert not t1.has(cot, tan), "fails for %d*pi/%d" % (i, n) + assert 1e-3 > abs( tan(x.evalf(7)) - t1.evalf(4) ), "fails for %d*pi/%d" % (i, n) + if i != 0 and i != n: + c1 = cot(x).rewrite(sqrt) + assert not c1.has(cot, tan), "fails for %d*pi/%d" % (i, n) + assert 1e-3 > abs( cot(x.evalf(7)) - c1.evalf(4) ), "fails for %d*pi/%d" % (i, n) + + +def test_sec(): + x = symbols('x', real=True) + z = symbols('z') + + assert sec.nargs == FiniteSet(1) + + assert sec(zoo) is nan + assert sec(0) == 1 + assert sec(pi) == -1 + assert sec(pi/2) is zoo + assert sec(-pi/2) is zoo + assert sec(pi/6) == 2*sqrt(3)/3 + assert sec(pi/3) == 2 + assert sec(pi*Rational(5, 2)) is zoo + assert sec(pi*Rational(9, 7)) == -sec(pi*Rational(2, 7)) + assert sec(pi*Rational(3, 4)) == -sqrt(2) # issue 8421 + assert sec(I) == 1/cosh(1) + assert sec(x*I) == 1/cosh(x) + assert sec(-x) == sec(x) + + assert sec(asec(x)) == x + + assert sec(z).conjugate() == sec(conjugate(z)) + + assert (sec(z).as_real_imag() == + (cos(re(z))*cosh(im(z))/(sin(re(z))**2*sinh(im(z))**2 + + cos(re(z))**2*cosh(im(z))**2), + sin(re(z))*sinh(im(z))/(sin(re(z))**2*sinh(im(z))**2 + + cos(re(z))**2*cosh(im(z))**2))) + + assert sec(x).expand(trig=True) == 1/cos(x) + assert sec(2*x).expand(trig=True) == 1/(2*cos(x)**2 - 1) + + assert sec(x).is_extended_real == True + assert sec(z).is_real == None + + assert sec(a).is_algebraic is None + assert sec(na).is_algebraic is False + + assert sec(x).as_leading_term() == sec(x) + + assert sec(0, evaluate=False).is_finite == True + assert sec(x).is_finite == None + assert sec(pi/2, evaluate=False).is_finite == False + + assert series(sec(x), x, x0=0, n=6) == 1 + x**2/2 + 5*x**4/24 + O(x**6) + + # https://github.com/sympy/sympy/issues/7166 + assert series(sqrt(sec(x))) == 1 + x**2/4 + 7*x**4/96 + O(x**6) + + # https://github.com/sympy/sympy/issues/7167 + assert (series(sqrt(sec(x)), x, x0=pi*3/2, n=4) == + 1/sqrt(x - pi*Rational(3, 2)) + (x - pi*Rational(3, 2))**Rational(3, 2)/12 + + (x - pi*Rational(3, 2))**Rational(7, 2)/160 + O((x - pi*Rational(3, 2))**4, (x, pi*Rational(3, 2)))) + + assert sec(x).diff(x) == tan(x)*sec(x) + + # Taylor Term checks + assert sec(z).taylor_term(4, z) == 5*z**4/24 + assert sec(z).taylor_term(6, z) == 61*z**6/720 + assert sec(z).taylor_term(5, z) == 0 + + +def test_sec_rewrite(): + assert sec(x).rewrite(exp) == 1/(exp(I*x)/2 + exp(-I*x)/2) + assert sec(x).rewrite(cos) == 1/cos(x) + assert sec(x).rewrite(tan) == (tan(x/2)**2 + 1)/(-tan(x/2)**2 + 1) + assert sec(x).rewrite(pow) == sec(x) + assert sec(x).rewrite(sqrt) == sec(x) + assert sec(z).rewrite(cot) == (cot(z/2)**2 + 1)/(cot(z/2)**2 - 1) + assert sec(x).rewrite(sin) == 1 / sin(x + pi / 2, evaluate=False) + assert sec(x).rewrite(tan) == (tan(x / 2)**2 + 1) / (-tan(x / 2)**2 + 1) + assert sec(x).rewrite(csc) == csc(-x + pi/2, evaluate=False) + assert sec(x).rewrite(besselj) == Piecewise( + (sqrt(2)/(sqrt(pi*x)*besselj(-S.Half, x)), Ne(x, 0)), + (1, True) + ) + assert sec(x).rewrite(besselj).subs(x, 0) == sec(0) + + +def test_sec_fdiff(): + assert sec(x).fdiff() == tan(x)*sec(x) + raises(ArgumentIndexError, lambda: sec(x).fdiff(2)) + + +def test_csc(): + x = symbols('x', real=True) + z = symbols('z') + + # https://github.com/sympy/sympy/issues/6707 + cosecant = csc('x') + alternate = 1/sin('x') + assert cosecant.equals(alternate) == True + assert alternate.equals(cosecant) == True + + assert csc.nargs == FiniteSet(1) + + assert csc(0) is zoo + assert csc(pi) is zoo + assert csc(zoo) is nan + + assert csc(pi/2) == 1 + assert csc(-pi/2) == -1 + assert csc(pi/6) == 2 + assert csc(pi/3) == 2*sqrt(3)/3 + assert csc(pi*Rational(5, 2)) == 1 + assert csc(pi*Rational(9, 7)) == -csc(pi*Rational(2, 7)) + assert csc(pi*Rational(3, 4)) == sqrt(2) # issue 8421 + assert csc(I) == -I/sinh(1) + assert csc(x*I) == -I/sinh(x) + assert csc(-x) == -csc(x) + + assert csc(acsc(x)) == x + + assert csc(z).conjugate() == csc(conjugate(z)) + + assert (csc(z).as_real_imag() == + (sin(re(z))*cosh(im(z))/(sin(re(z))**2*cosh(im(z))**2 + + cos(re(z))**2*sinh(im(z))**2), + -cos(re(z))*sinh(im(z))/(sin(re(z))**2*cosh(im(z))**2 + + cos(re(z))**2*sinh(im(z))**2))) + + assert csc(x).expand(trig=True) == 1/sin(x) + assert csc(2*x).expand(trig=True) == 1/(2*sin(x)*cos(x)) + + assert csc(x).is_extended_real == True + assert csc(z).is_real == None + + assert csc(a).is_algebraic is None + assert csc(na).is_algebraic is False + + assert csc(x).as_leading_term() == csc(x) + + assert csc(0, evaluate=False).is_finite == False + assert csc(x).is_finite == None + assert csc(pi/2, evaluate=False).is_finite == True + + assert series(csc(x), x, x0=pi/2, n=6) == \ + 1 + (x - pi/2)**2/2 + 5*(x - pi/2)**4/24 + O((x - pi/2)**6, (x, pi/2)) + assert series(csc(x), x, x0=0, n=6) == \ + 1/x + x/6 + 7*x**3/360 + 31*x**5/15120 + O(x**6) + + assert csc(x).diff(x) == -cot(x)*csc(x) + + assert csc(x).taylor_term(2, x) == 0 + assert csc(x).taylor_term(3, x) == 7*x**3/360 + assert csc(x).taylor_term(5, x) == 31*x**5/15120 + raises(ArgumentIndexError, lambda: csc(x).fdiff(2)) + + +def test_asec(): + z = Symbol('z', zero=True) + assert asec(z) is zoo + assert asec(nan) is nan + assert asec(1) == 0 + assert asec(-1) == pi + assert asec(oo) == pi/2 + assert asec(-oo) == pi/2 + assert asec(zoo) == pi/2 + + assert asec(sec(pi*Rational(13, 4))) == pi*Rational(3, 4) + assert asec(1 + sqrt(5)) == pi*Rational(2, 5) + assert asec(2/sqrt(3)) == pi/6 + assert asec(sqrt(4 - 2*sqrt(2))) == pi/8 + assert asec(-sqrt(4 + 2*sqrt(2))) == pi*Rational(5, 8) + assert asec(sqrt(2 + 2*sqrt(5)/5)) == pi*Rational(3, 10) + assert asec(-sqrt(2 + 2*sqrt(5)/5)) == pi*Rational(7, 10) + assert asec(sqrt(2) - sqrt(6)) == pi*Rational(11, 12) + + for d in [3, 4, 6]: + for num in range(d): + if gcd(num, d) == 1: + assert asec(sec(num*pi/d)) == num*pi/d + assert asec(-sec(num*pi/d)) == pi - num*pi/d + assert asec(csc(num*pi/d)) == pi/2 - acsc(csc(num*pi/d)) + assert asec(-csc(num*pi/d)) == pi/2 - acsc(-csc(num*pi/d)) + + assert asec(x).diff(x) == 1/(x**2*sqrt(1 - 1/x**2)) + + assert asec(x).rewrite(log) == I*log(sqrt(1 - 1/x**2) + I/x) + pi/2 + assert asec(x).rewrite(asin) == -asin(1/x) + pi/2 + assert asec(x).rewrite(acos) == acos(1/x) + assert asec(x).rewrite(atan) == \ + pi*(1 - sqrt(x**2)/x)/2 + sqrt(x**2)*atan(sqrt(x**2 - 1))/x + assert asec(x).rewrite(acot) == \ + pi*(1 - sqrt(x**2)/x)/2 + sqrt(x**2)*acot(1/sqrt(x**2 - 1))/x + assert asec(x).rewrite(acsc) == -acsc(x) + pi/2 + raises(ArgumentIndexError, lambda: asec(x).fdiff(2)) + + +def test_asec_is_real(): + assert asec(S.Half).is_real is False + n = Symbol('n', positive=True, integer=True) + assert asec(n).is_extended_real is True + assert asec(x).is_real is None + assert asec(r).is_real is None + t = Symbol('t', real=False, finite=True) + assert asec(t).is_real is False + + +def test_asec_leading_term(): + assert asec(1/x).as_leading_term(x) == pi/2 + # Tests concerning branch points + assert asec(x + 1).as_leading_term(x) == sqrt(2)*sqrt(x) + assert asec(x - 1).as_leading_term(x) == pi + # Tests concerning points lying on branch cuts + assert asec(x).as_leading_term(x, cdir=1) == -I*log(x) + I*log(2) + assert asec(x).as_leading_term(x, cdir=-1) == I*log(x) + 2*pi - I*log(2) + assert asec(I*x + 1/2).as_leading_term(x, cdir=1) == asec(1/2) + assert asec(-I*x + 1/2).as_leading_term(x, cdir=1) == -asec(1/2) + assert asec(I*x - 1/2).as_leading_term(x, cdir=1) == 2*pi - asec(-1/2) + assert asec(-I*x - 1/2).as_leading_term(x, cdir=1) == asec(-1/2) + # Tests concerning im(ndir) == 0 + assert asec(-I*x**2 + x - S(1)/2).as_leading_term(x, cdir=1) == pi + I*log(2 - sqrt(3)) + assert asec(-I*x**2 + x - S(1)/2).as_leading_term(x, cdir=-1) == pi + I*log(2 - sqrt(3)) + + +def test_asec_series(): + assert asec(x).series(x, 0, 9) == \ + I*log(2) - I*log(x) - I*x**2/4 - 3*I*x**4/32 \ + - 5*I*x**6/96 - 35*I*x**8/1024 + O(x**9) + t4 = asec(x).taylor_term(4, x) + assert t4 == -3*I*x**4/32 + assert asec(x).taylor_term(6, x, t4, 0) == -5*I*x**6/96 + + +def test_acsc(): + assert acsc(nan) is nan + assert acsc(1) == pi/2 + assert acsc(-1) == -pi/2 + assert acsc(oo) == 0 + assert acsc(-oo) == 0 + assert acsc(zoo) == 0 + assert acsc(0) is zoo + + assert acsc(csc(3)) == -3 + pi + assert acsc(csc(4)) == -4 + pi + assert acsc(csc(6)) == 6 - 2*pi + assert unchanged(acsc, csc(x)) + assert unchanged(acsc, sec(x)) + + assert acsc(2/sqrt(3)) == pi/3 + assert acsc(csc(pi*Rational(13, 4))) == -pi/4 + assert acsc(sqrt(2 + 2*sqrt(5)/5)) == pi/5 + assert acsc(-sqrt(2 + 2*sqrt(5)/5)) == -pi/5 + assert acsc(-2) == -pi/6 + assert acsc(-sqrt(4 + 2*sqrt(2))) == -pi/8 + assert acsc(sqrt(4 - 2*sqrt(2))) == pi*Rational(3, 8) + assert acsc(1 + sqrt(5)) == pi/10 + assert acsc(sqrt(2) - sqrt(6)) == pi*Rational(-5, 12) + + assert acsc(x).diff(x) == -1/(x**2*sqrt(1 - 1/x**2)) + + assert acsc(x).rewrite(log) == -I*log(sqrt(1 - 1/x**2) + I/x) + assert acsc(x).rewrite(asin) == asin(1/x) + assert acsc(x).rewrite(acos) == -acos(1/x) + pi/2 + assert acsc(x).rewrite(atan) == \ + (-atan(sqrt(x**2 - 1)) + pi/2)*sqrt(x**2)/x + assert acsc(x).rewrite(acot) == (-acot(1/sqrt(x**2 - 1)) + pi/2)*sqrt(x**2)/x + assert acsc(x).rewrite(asec) == -asec(x) + pi/2 + raises(ArgumentIndexError, lambda: acsc(x).fdiff(2)) + + +def test_csc_rewrite(): + assert csc(x).rewrite(pow) == csc(x) + assert csc(x).rewrite(sqrt) == csc(x) + + assert csc(x).rewrite(exp) == 2*I/(exp(I*x) - exp(-I*x)) + assert csc(x).rewrite(sin) == 1/sin(x) + assert csc(x).rewrite(tan) == (tan(x/2)**2 + 1)/(2*tan(x/2)) + assert csc(x).rewrite(cot) == (cot(x/2)**2 + 1)/(2*cot(x/2)) + assert csc(x).rewrite(cos) == 1/cos(x - pi/2, evaluate=False) + assert csc(x).rewrite(sec) == sec(-x + pi/2, evaluate=False) + + # issue 17349 + assert csc(1 - exp(-besselj(I, I))).rewrite(cos) == \ + -1/cos(-pi/2 - 1 + cos(I*besselj(I, I)) + + I*cos(-pi/2 + I*besselj(I, I), evaluate=False), evaluate=False) + assert csc(x).rewrite(besselj) == sqrt(2)/(sqrt(pi*x)*besselj(S.Half, x)) + assert csc(x).rewrite(besselj).subs(x, 0) == csc(0) + + +def test_acsc_leading_term(): + assert acsc(1/x).as_leading_term(x) == x + # Tests concerning branch points + assert acsc(x + 1).as_leading_term(x) == pi/2 + assert acsc(x - 1).as_leading_term(x) == -pi/2 + # Tests concerning points lying on branch cuts + assert acsc(x).as_leading_term(x, cdir=1) == I*log(x) + pi/2 - I*log(2) + assert acsc(x).as_leading_term(x, cdir=-1) == -I*log(x) - 3*pi/2 + I*log(2) + assert acsc(I*x + 1/2).as_leading_term(x, cdir=1) == acsc(1/2) + assert acsc(-I*x + 1/2).as_leading_term(x, cdir=1) == pi - acsc(1/2) + assert acsc(I*x - 1/2).as_leading_term(x, cdir=1) == -pi - acsc(-1/2) + assert acsc(-I*x - 1/2).as_leading_term(x, cdir=1) == -acsc(1/2) + # Tests concerning im(ndir) == 0 + assert acsc(-I*x**2 + x - S(1)/2).as_leading_term(x, cdir=1) == -pi/2 + I*log(sqrt(3) + 2) + assert acsc(-I*x**2 + x - S(1)/2).as_leading_term(x, cdir=-1) == -pi/2 + I*log(sqrt(3) + 2) + + +def test_acsc_series(): + assert acsc(x).series(x, 0, 9) == \ + -I*log(2) + pi/2 + I*log(x) + I*x**2/4 \ + + 3*I*x**4/32 + 5*I*x**6/96 + 35*I*x**8/1024 + O(x**9) + t6 = acsc(x).taylor_term(6, x) + assert t6 == 5*I*x**6/96 + assert acsc(x).taylor_term(8, x, t6, 0) == 35*I*x**8/1024 + + +def test_asin_nseries(): + assert asin(x + 2)._eval_nseries(x, 4, None, I) == -asin(2) + pi + \ + sqrt(3)*I*x/3 - sqrt(3)*I*x**2/9 + sqrt(3)*I*x**3/18 + O(x**4) + assert asin(x + 2)._eval_nseries(x, 4, None, -I) == asin(2) - \ + sqrt(3)*I*x/3 + sqrt(3)*I*x**2/9 - sqrt(3)*I*x**3/18 + O(x**4) + assert asin(x - 2)._eval_nseries(x, 4, None, I) == -asin(2) - \ + sqrt(3)*I*x/3 - sqrt(3)*I*x**2/9 - sqrt(3)*I*x**3/18 + O(x**4) + assert asin(x - 2)._eval_nseries(x, 4, None, -I) == asin(2) - pi + \ + sqrt(3)*I*x/3 + sqrt(3)*I*x**2/9 + sqrt(3)*I*x**3/18 + O(x**4) + # testing nseries for asin at branch points + assert asin(1 + x)._eval_nseries(x, 3, None) == pi/2 - sqrt(2)*sqrt(-x) - \ + sqrt(2)*(-x)**(S(3)/2)/12 - 3*sqrt(2)*(-x)**(S(5)/2)/160 + O(x**3) + assert asin(-1 + x)._eval_nseries(x, 3, None) == -pi/2 + sqrt(2)*sqrt(x) + \ + sqrt(2)*x**(S(3)/2)/12 + 3*sqrt(2)*x**(S(5)/2)/160 + O(x**3) + assert asin(exp(x))._eval_nseries(x, 3, None) == pi/2 - sqrt(2)*sqrt(-x) + \ + sqrt(2)*(-x)**(S(3)/2)/6 - sqrt(2)*(-x)**(S(5)/2)/120 + O(x**3) + assert asin(-exp(x))._eval_nseries(x, 3, None) == -pi/2 + sqrt(2)*sqrt(-x) - \ + sqrt(2)*(-x)**(S(3)/2)/6 + sqrt(2)*(-x)**(S(5)/2)/120 + O(x**3) + + +def test_acos_nseries(): + assert acos(x + 2)._eval_nseries(x, 4, None, I) == -acos(2) - sqrt(3)*I*x/3 + \ + sqrt(3)*I*x**2/9 - sqrt(3)*I*x**3/18 + O(x**4) + assert acos(x + 2)._eval_nseries(x, 4, None, -I) == acos(2) + sqrt(3)*I*x/3 - \ + sqrt(3)*I*x**2/9 + sqrt(3)*I*x**3/18 + O(x**4) + assert acos(x - 2)._eval_nseries(x, 4, None, I) == acos(-2) + sqrt(3)*I*x/3 + \ + sqrt(3)*I*x**2/9 + sqrt(3)*I*x**3/18 + O(x**4) + assert acos(x - 2)._eval_nseries(x, 4, None, -I) == -acos(-2) + 2*pi - \ + sqrt(3)*I*x/3 - sqrt(3)*I*x**2/9 - sqrt(3)*I*x**3/18 + O(x**4) + # testing nseries for acos at branch points + assert acos(1 + x)._eval_nseries(x, 3, None) == sqrt(2)*sqrt(-x) + \ + sqrt(2)*(-x)**(S(3)/2)/12 + 3*sqrt(2)*(-x)**(S(5)/2)/160 + O(x**3) + assert acos(-1 + x)._eval_nseries(x, 3, None) == pi - sqrt(2)*sqrt(x) - \ + sqrt(2)*x**(S(3)/2)/12 - 3*sqrt(2)*x**(S(5)/2)/160 + O(x**3) + assert acos(exp(x))._eval_nseries(x, 3, None) == sqrt(2)*sqrt(-x) - \ + sqrt(2)*(-x)**(S(3)/2)/6 + sqrt(2)*(-x)**(S(5)/2)/120 + O(x**3) + assert acos(-exp(x))._eval_nseries(x, 3, None) == pi - sqrt(2)*sqrt(-x) + \ + sqrt(2)*(-x)**(S(3)/2)/6 - sqrt(2)*(-x)**(S(5)/2)/120 + O(x**3) + + +def test_atan_nseries(): + assert atan(x + 2*I)._eval_nseries(x, 4, None, 1) == I*atanh(2) - x/3 - \ + 2*I*x**2/9 + 13*x**3/81 + O(x**4) + assert atan(x + 2*I)._eval_nseries(x, 4, None, -1) == I*atanh(2) - pi - \ + x/3 - 2*I*x**2/9 + 13*x**3/81 + O(x**4) + assert atan(x - 2*I)._eval_nseries(x, 4, None, 1) == -I*atanh(2) + pi - \ + x/3 + 2*I*x**2/9 + 13*x**3/81 + O(x**4) + assert atan(x - 2*I)._eval_nseries(x, 4, None, -1) == -I*atanh(2) - x/3 + \ + 2*I*x**2/9 + 13*x**3/81 + O(x**4) + assert atan(1/x)._eval_nseries(x, 2, None, 1) == pi/2 - x + O(x**2) + assert atan(1/x)._eval_nseries(x, 2, None, -1) == -pi/2 - x + O(x**2) + # testing nseries for atan at branch points + assert atan(x + I)._eval_nseries(x, 4, None) == I*log(2)/2 + pi/4 - \ + I*log(x)/2 + x/4 + I*x**2/16 - x**3/48 + O(x**4) + assert atan(x - I)._eval_nseries(x, 4, None) == -I*log(2)/2 + pi/4 + \ + I*log(x)/2 + x/4 - I*x**2/16 - x**3/48 + O(x**4) + + +def test_acot_nseries(): + assert acot(x + S(1)/2*I)._eval_nseries(x, 4, None, 1) == -I*acoth(S(1)/2) + \ + pi - 4*x/3 + 8*I*x**2/9 + 112*x**3/81 + O(x**4) + assert acot(x + S(1)/2*I)._eval_nseries(x, 4, None, -1) == -I*acoth(S(1)/2) - \ + 4*x/3 + 8*I*x**2/9 + 112*x**3/81 + O(x**4) + assert acot(x - S(1)/2*I)._eval_nseries(x, 4, None, 1) == I*acoth(S(1)/2) - \ + 4*x/3 - 8*I*x**2/9 + 112*x**3/81 + O(x**4) + assert acot(x - S(1)/2*I)._eval_nseries(x, 4, None, -1) == I*acoth(S(1)/2) - \ + pi - 4*x/3 - 8*I*x**2/9 + 112*x**3/81 + O(x**4) + assert acot(x)._eval_nseries(x, 2, None, 1) == pi/2 - x + O(x**2) + assert acot(x)._eval_nseries(x, 2, None, -1) == -pi/2 - x + O(x**2) + # testing nseries for acot at branch points + assert acot(x + I)._eval_nseries(x, 4, None) == -I*log(2)/2 + pi/4 + \ + I*log(x)/2 - x/4 - I*x**2/16 + x**3/48 + O(x**4) + assert acot(x - I)._eval_nseries(x, 4, None) == I*log(2)/2 + pi/4 - \ + I*log(x)/2 - x/4 + I*x**2/16 + x**3/48 + O(x**4) + + +def test_asec_nseries(): + assert asec(x + S(1)/2)._eval_nseries(x, 4, None, I) == asec(S(1)/2) - \ + 4*sqrt(3)*I*x/3 + 8*sqrt(3)*I*x**2/9 - 16*sqrt(3)*I*x**3/9 + O(x**4) + assert asec(x + S(1)/2)._eval_nseries(x, 4, None, -I) == -asec(S(1)/2) + \ + 4*sqrt(3)*I*x/3 - 8*sqrt(3)*I*x**2/9 + 16*sqrt(3)*I*x**3/9 + O(x**4) + assert asec(x - S(1)/2)._eval_nseries(x, 4, None, I) == -asec(-S(1)/2) + \ + 2*pi + 4*sqrt(3)*I*x/3 + 8*sqrt(3)*I*x**2/9 + 16*sqrt(3)*I*x**3/9 + O(x**4) + assert asec(x - S(1)/2)._eval_nseries(x, 4, None, -I) == asec(-S(1)/2) - \ + 4*sqrt(3)*I*x/3 - 8*sqrt(3)*I*x**2/9 - 16*sqrt(3)*I*x**3/9 + O(x**4) + # testing nseries for asec at branch points + assert asec(1 + x)._eval_nseries(x, 3, None) == sqrt(2)*sqrt(x) - \ + 5*sqrt(2)*x**(S(3)/2)/12 + 43*sqrt(2)*x**(S(5)/2)/160 + O(x**3) + assert asec(-1 + x)._eval_nseries(x, 3, None) == pi - sqrt(2)*sqrt(-x) + \ + 5*sqrt(2)*(-x)**(S(3)/2)/12 - 43*sqrt(2)*(-x)**(S(5)/2)/160 + O(x**3) + assert asec(exp(x))._eval_nseries(x, 3, None) == sqrt(2)*sqrt(x) - \ + sqrt(2)*x**(S(3)/2)/6 + sqrt(2)*x**(S(5)/2)/120 + O(x**3) + assert asec(-exp(x))._eval_nseries(x, 3, None) == pi - sqrt(2)*sqrt(x) + \ + sqrt(2)*x**(S(3)/2)/6 - sqrt(2)*x**(S(5)/2)/120 + O(x**3) + + +def test_acsc_nseries(): + assert acsc(x + S(1)/2)._eval_nseries(x, 4, None, I) == acsc(S(1)/2) + \ + 4*sqrt(3)*I*x/3 - 8*sqrt(3)*I*x**2/9 + 16*sqrt(3)*I*x**3/9 + O(x**4) + assert acsc(x + S(1)/2)._eval_nseries(x, 4, None, -I) == -acsc(S(1)/2) + \ + pi - 4*sqrt(3)*I*x/3 + 8*sqrt(3)*I*x**2/9 - 16*sqrt(3)*I*x**3/9 + O(x**4) + assert acsc(x - S(1)/2)._eval_nseries(x, 4, None, I) == acsc(S(1)/2) - pi -\ + 4*sqrt(3)*I*x/3 - 8*sqrt(3)*I*x**2/9 - 16*sqrt(3)*I*x**3/9 + O(x**4) + assert acsc(x - S(1)/2)._eval_nseries(x, 4, None, -I) == -acsc(S(1)/2) + \ + 4*sqrt(3)*I*x/3 + 8*sqrt(3)*I*x**2/9 + 16*sqrt(3)*I*x**3/9 + O(x**4) + # testing nseries for acsc at branch points + assert acsc(1 + x)._eval_nseries(x, 3, None) == pi/2 - sqrt(2)*sqrt(x) + \ + 5*sqrt(2)*x**(S(3)/2)/12 - 43*sqrt(2)*x**(S(5)/2)/160 + O(x**3) + assert acsc(-1 + x)._eval_nseries(x, 3, None) == -pi/2 + sqrt(2)*sqrt(-x) - \ + 5*sqrt(2)*(-x)**(S(3)/2)/12 + 43*sqrt(2)*(-x)**(S(5)/2)/160 + O(x**3) + assert acsc(exp(x))._eval_nseries(x, 3, None) == pi/2 - sqrt(2)*sqrt(x) + \ + sqrt(2)*x**(S(3)/2)/6 - sqrt(2)*x**(S(5)/2)/120 + O(x**3) + assert acsc(-exp(x))._eval_nseries(x, 3, None) == -pi/2 + sqrt(2)*sqrt(x) - \ + sqrt(2)*x**(S(3)/2)/6 + sqrt(2)*x**(S(5)/2)/120 + O(x**3) + + +def test_issue_8653(): + n = Symbol('n', integer=True) + assert sin(n).is_irrational is None + assert cos(n).is_irrational is None + assert tan(n).is_irrational is None + + +def test_issue_9157(): + n = Symbol('n', integer=True, positive=True) + assert atan(n - 1).is_nonnegative is True + + +def test_trig_period(): + x, y = symbols('x, y') + + assert sin(x).period() == 2*pi + assert cos(x).period() == 2*pi + assert tan(x).period() == pi + assert cot(x).period() == pi + assert sec(x).period() == 2*pi + assert csc(x).period() == 2*pi + assert sin(2*x).period() == pi + assert cot(4*x - 6).period() == pi/4 + assert cos((-3)*x).period() == pi*Rational(2, 3) + assert cos(x*y).period(x) == 2*pi/abs(y) + assert sin(3*x*y + 2*pi).period(y) == 2*pi/abs(3*x) + assert tan(3*x).period(y) is S.Zero + raises(NotImplementedError, lambda: sin(x**2).period(x)) + + +def test_issue_7171(): + assert sin(x).rewrite(sqrt) == sin(x) + assert sin(x).rewrite(pow) == sin(x) + + +def test_issue_11864(): + w, k = symbols('w, k', real=True) + F = Piecewise((1, Eq(2*pi*k, 0)), (sin(pi*k)/(pi*k), True)) + soln = Piecewise((1, Eq(2*pi*k, 0)), (sinc(pi*k), True)) + assert F.rewrite(sinc) == soln + +def test_real_assumptions(): + z = Symbol('z', real=False, finite=True) + assert sin(z).is_real is None + assert cos(z).is_real is None + assert tan(z).is_real is False + assert sec(z).is_real is None + assert csc(z).is_real is None + assert cot(z).is_real is False + assert asin(p).is_real is None + assert asin(n).is_real is None + assert asec(p).is_real is None + assert asec(n).is_real is None + assert acos(p).is_real is None + assert acos(n).is_real is None + assert acsc(p).is_real is None + assert acsc(n).is_real is None + assert atan(p).is_positive is True + assert atan(n).is_negative is True + assert acot(p).is_positive is True + assert acot(n).is_negative is True + +def test_issue_14320(): + assert asin(sin(2)) == -2 + pi and (-pi/2 <= -2 + pi <= pi/2) and sin(2) == sin(-2 + pi) + assert asin(cos(2)) == -2 + pi/2 and (-pi/2 <= -2 + pi/2 <= pi/2) and cos(2) == sin(-2 + pi/2) + assert acos(sin(2)) == -pi/2 + 2 and (0 <= -pi/2 + 2 <= pi) and sin(2) == cos(-pi/2 + 2) + assert acos(cos(20)) == -6*pi + 20 and (0 <= -6*pi + 20 <= pi) and cos(20) == cos(-6*pi + 20) + assert acos(cos(30)) == -30 + 10*pi and (0 <= -30 + 10*pi <= pi) and cos(30) == cos(-30 + 10*pi) + + assert atan(tan(17)) == -5*pi + 17 and (-pi/2 < -5*pi + 17 < pi/2) and tan(17) == tan(-5*pi + 17) + assert atan(tan(15)) == -5*pi + 15 and (-pi/2 < -5*pi + 15 < pi/2) and tan(15) == tan(-5*pi + 15) + assert atan(cot(12)) == -12 + pi*Rational(7, 2) and (-pi/2 < -12 + pi*Rational(7, 2) < pi/2) and cot(12) == tan(-12 + pi*Rational(7, 2)) + assert acot(cot(15)) == -5*pi + 15 and (-pi/2 < -5*pi + 15 <= pi/2) and cot(15) == cot(-5*pi + 15) + assert acot(tan(19)) == -19 + pi*Rational(13, 2) and (-pi/2 < -19 + pi*Rational(13, 2) <= pi/2) and tan(19) == cot(-19 + pi*Rational(13, 2)) + + assert asec(sec(11)) == -11 + 4*pi and (0 <= -11 + 4*pi <= pi) and cos(11) == cos(-11 + 4*pi) + assert asec(csc(13)) == -13 + pi*Rational(9, 2) and (0 <= -13 + pi*Rational(9, 2) <= pi) and sin(13) == cos(-13 + pi*Rational(9, 2)) + assert acsc(csc(14)) == -4*pi + 14 and (-pi/2 <= -4*pi + 14 <= pi/2) and sin(14) == sin(-4*pi + 14) + assert acsc(sec(10)) == pi*Rational(-7, 2) + 10 and (-pi/2 <= pi*Rational(-7, 2) + 10 <= pi/2) and cos(10) == sin(pi*Rational(-7, 2) + 10) + +def test_issue_14543(): + assert sec(2*pi + 11) == sec(11) + assert sec(2*pi - 11) == sec(11) + assert sec(pi + 11) == -sec(11) + assert sec(pi - 11) == -sec(11) + + assert csc(2*pi + 17) == csc(17) + assert csc(2*pi - 17) == -csc(17) + assert csc(pi + 17) == -csc(17) + assert csc(pi - 17) == csc(17) + + x = Symbol('x') + assert csc(pi/2 + x) == sec(x) + assert csc(pi/2 - x) == sec(x) + assert csc(pi*Rational(3, 2) + x) == -sec(x) + assert csc(pi*Rational(3, 2) - x) == -sec(x) + + assert sec(pi/2 - x) == csc(x) + assert sec(pi/2 + x) == -csc(x) + assert sec(pi*Rational(3, 2) + x) == csc(x) + assert sec(pi*Rational(3, 2) - x) == -csc(x) + + +def test_as_real_imag(): + # This is for https://github.com/sympy/sympy/issues/17142 + # If it start failing again in irrelevant builds or in the master + # please open up the issue again. + expr = atan(I/(I + I*tan(1))) + assert expr.as_real_imag() == (expr, 0) + + +def test_issue_18746(): + e3 = cos(S.Pi*(x/4 + 1/4)) + assert e3.period() == 8 + + +def test_issue_25833(): + assert limit(atan(x**2), x, oo) == pi/2 + assert limit(atan(x**2 - 1), x, oo) == pi/2 + assert limit(atan(log(2**x)/log(2*x)), x, oo) == pi/2 + + +def test_issue_25847(): + #atan + assert atan(sin(x)/x).as_leading_term(x) == pi/4 + raises(PoleError, lambda: atan(exp(1/x)).as_leading_term(x)) + + #asin + assert asin(sin(x)/x).as_leading_term(x) == pi/2 + raises(PoleError, lambda: asin(exp(1/x)).as_leading_term(x)) + + #acos + assert acos(sin(x)/x).as_leading_term(x) == 0 + raises(PoleError, lambda: acos(exp(1/x)).as_leading_term(x)) + + #acot + assert acot(sin(x)/x).as_leading_term(x) == pi/4 + raises(PoleError, lambda: acot(exp(1/x)).as_leading_term(x)) + + #asec + assert asec(sin(x)/x).as_leading_term(x) == 0 + raises(PoleError, lambda: asec(exp(1/x)).as_leading_term(x)) + + #acsc + assert acsc(sin(x)/x).as_leading_term(x) == pi/2 + raises(PoleError, lambda: acsc(exp(1/x)).as_leading_term(x)) + +def test_issue_23843(): + #atan + assert atan(x + I).series(x, oo) == -16/(5*x**5) - 2*I/x**4 + 4/(3*x**3) + I/x**2 - 1/x + pi/2 + O(x**(-6), (x, oo)) + assert atan(x + I).series(x, -oo) == -16/(5*x**5) - 2*I/x**4 + 4/(3*x**3) + I/x**2 - 1/x - pi/2 + O(x**(-6), (x, -oo)) + assert atan(x - I).series(x, oo) == -16/(5*x**5) + 2*I/x**4 + 4/(3*x**3) - I/x**2 - 1/x + pi/2 + O(x**(-6), (x, oo)) + assert atan(x - I).series(x, -oo) == -16/(5*x**5) + 2*I/x**4 + 4/(3*x**3) - I/x**2 - 1/x - pi/2 + O(x**(-6), (x, -oo)) + + #acot + assert acot(x + I).series(x, oo) == 16/(5*x**5) + 2*I/x**4 - 4/(3*x**3) - I/x**2 + 1/x + O(x**(-6), (x, oo)) + assert acot(x + I).series(x, -oo) == 16/(5*x**5) + 2*I/x**4 - 4/(3*x**3) - I/x**2 + 1/x + O(x**(-6), (x, -oo)) + assert acot(x - I).series(x, oo) == 16/(5*x**5) - 2*I/x**4 - 4/(3*x**3) + I/x**2 + 1/x + O(x**(-6), (x, oo)) + assert acot(x - I).series(x, -oo) == 16/(5*x**5) - 2*I/x**4 - 4/(3*x**3) + I/x**2 + 1/x + O(x**(-6), (x, -oo)) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/elementary/trigonometric.py b/venv/lib/python3.11/site-packages/sympy/functions/elementary/trigonometric.py new file mode 100644 index 0000000000000000000000000000000000000000..24e5db81f17a215f5b344291f0e9bf4752e5317d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/elementary/trigonometric.py @@ -0,0 +1,3627 @@ +from __future__ import annotations +from sympy.core.add import Add +from sympy.core.cache import cacheit +from sympy.core.expr import Expr +from sympy.core.function import DefinedFunction, ArgumentIndexError, PoleError, expand_mul +from sympy.core.logic import fuzzy_not, fuzzy_or, FuzzyBool, fuzzy_and +from sympy.core.mod import Mod +from sympy.core.numbers import Rational, pi, Integer, Float, equal_valued +from sympy.core.relational import Ne, Eq +from sympy.core.singleton import S +from sympy.core.symbol import Symbol, Dummy +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import factorial, RisingFactorial +from sympy.functions.combinatorial.numbers import bernoulli, euler +from sympy.functions.elementary.complexes import arg as arg_f, im, re +from sympy.functions.elementary.exponential import log, exp +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import sqrt, Min, Max +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary._trigonometric_special import ( + cos_table, ipartfrac, fermat_coords) +from sympy.logic.boolalg import And +from sympy.ntheory import factorint +from sympy.polys.specialpolys import symmetric_poly +from sympy.utilities.iterables import numbered_symbols + + +############################################################################### +########################## UTILITIES ########################################## +############################################################################### + + +def _imaginary_unit_as_coefficient(arg): + """ Helper to extract symbolic coefficient for imaginary unit """ + if isinstance(arg, Float): + return None + else: + return arg.as_coefficient(S.ImaginaryUnit) + +############################################################################### +########################## TRIGONOMETRIC FUNCTIONS ############################ +############################################################################### + + +class TrigonometricFunction(DefinedFunction): + """Base class for trigonometric functions. """ + + unbranched = True + _singularities = (S.ComplexInfinity,) + + def _eval_is_rational(self): + s = self.func(*self.args) + if s.func == self.func: + if s.args[0].is_rational and fuzzy_not(s.args[0].is_zero): + return False + else: + return s.is_rational + + def _eval_is_algebraic(self): + s = self.func(*self.args) + if s.func == self.func: + if fuzzy_not(self.args[0].is_zero) and self.args[0].is_algebraic: + return False + pi_coeff = _pi_coeff(self.args[0]) + if pi_coeff is not None and pi_coeff.is_rational: + return True + else: + return s.is_algebraic + + def _eval_expand_complex(self, deep=True, **hints): + re_part, im_part = self.as_real_imag(deep=deep, **hints) + return re_part + im_part*S.ImaginaryUnit + + def _as_real_imag(self, deep=True, **hints): + if self.args[0].is_extended_real: + if deep: + hints['complex'] = False + return (self.args[0].expand(deep, **hints), S.Zero) + else: + return (self.args[0], S.Zero) + if deep: + re, im = self.args[0].expand(deep, **hints).as_real_imag() + else: + re, im = self.args[0].as_real_imag() + return (re, im) + + def _period(self, general_period, symbol=None): + f = expand_mul(self.args[0]) + if symbol is None: + symbol = tuple(f.free_symbols)[0] + + if not f.has(symbol): + return S.Zero + + if f == symbol: + return general_period + + if symbol in f.free_symbols: + if f.is_Mul: + g, h = f.as_independent(symbol) + if h == symbol: + return general_period/abs(g) + + if f.is_Add: + a, h = f.as_independent(symbol) + g, h = h.as_independent(symbol, as_Add=False) + if h == symbol: + return general_period/abs(g) + + raise NotImplementedError("Use the periodicity function instead.") + + +@cacheit +def _table2(): + # If nested sqrt's are worse than un-evaluation + # you can require q to be in (1, 2, 3, 4, 6, 12) + # q <= 12, q=15, q=20, q=24, q=30, q=40, q=60, q=120 return + # expressions with 2 or fewer sqrt nestings. + return { + 12: (3, 4), + 20: (4, 5), + 30: (5, 6), + 15: (6, 10), + 24: (6, 8), + 40: (8, 10), + 60: (20, 30), + 120: (40, 60) + } + + +def _peeloff_pi(arg): + r""" + Split ARG into two parts, a "rest" and a multiple of $\pi$. + This assumes ARG to be an Add. + The multiple of $\pi$ returned in the second position is always a Rational. + + Examples + ======== + + >>> from sympy.functions.elementary.trigonometric import _peeloff_pi + >>> from sympy import pi + >>> from sympy.abc import x, y + >>> _peeloff_pi(x + pi/2) + (x, 1/2) + >>> _peeloff_pi(x + 2*pi/3 + pi*y) + (x + pi*y + pi/6, 1/2) + + """ + pi_coeff = S.Zero + rest_terms = [] + for a in Add.make_args(arg): + K = a.coeff(pi) + if K and K.is_rational: + pi_coeff += K + else: + rest_terms.append(a) + + if pi_coeff is S.Zero: + return arg, S.Zero + + m1 = (pi_coeff % S.Half) + m2 = pi_coeff - m1 + if m2.is_integer or ((2*m2).is_integer and m2.is_even is False): + return Add(*(rest_terms + [m1*pi])), m2 + return arg, S.Zero + + +def _pi_coeff(arg: Expr, cycles: int = 1) -> Expr | None: + r""" + When arg is a Number times $\pi$ (e.g. $3\pi/2$) then return the Number + normalized to be in the range $[0, 2]$, else `None`. + + When an even multiple of $\pi$ is encountered, if it is multiplying + something with known parity then the multiple is returned as 0 otherwise + as 2. + + Examples + ======== + + >>> from sympy.functions.elementary.trigonometric import _pi_coeff + >>> from sympy import pi, Dummy + >>> from sympy.abc import x + >>> _pi_coeff(3*x*pi) + 3*x + >>> _pi_coeff(11*pi/7) + 11/7 + >>> _pi_coeff(-11*pi/7) + 3/7 + >>> _pi_coeff(4*pi) + 0 + >>> _pi_coeff(5*pi) + 1 + >>> _pi_coeff(5.0*pi) + 1 + >>> _pi_coeff(5.5*pi) + 3/2 + >>> _pi_coeff(2 + pi) + + >>> _pi_coeff(2*Dummy(integer=True)*pi) + 2 + >>> _pi_coeff(2*Dummy(even=True)*pi) + 0 + + """ + if arg is pi: + return S.One + elif not arg: + return S.Zero + elif arg.is_Mul: + cx = arg.coeff(pi) + if cx: + c, x = cx.as_coeff_Mul() # pi is not included as coeff + if c.is_Float: + # recast exact binary fractions to Rationals + f = abs(c) % 1 + if f != 0: + p = -int(round(log(f, 2).evalf())) + m = 2**p + cm = c*m + i = int(cm) + if equal_valued(i, cm): + c = Rational(i, m) + cx = c*x + else: + c = Rational(int(c)) + cx = c*x + if x.is_integer: + c2 = c % 2 + if c2 == 1: + return x + elif not c2: + if x.is_even is not None: # known parity + return S.Zero + return Integer(2) + else: + return c2*x + return cx + elif arg.is_zero: + return S.Zero + return None + + +class sin(TrigonometricFunction): + r""" + The sine function. + + Returns the sine of x (measured in radians). + + Explanation + =========== + + This function will evaluate automatically in the + case $x/\pi$ is some rational number [4]_. For example, + if $x$ is a multiple of $\pi$, $\pi/2$, $\pi/3$, $\pi/4$, and $\pi/6$. + + Examples + ======== + + >>> from sympy import sin, pi + >>> from sympy.abc import x + >>> sin(x**2).diff(x) + 2*x*cos(x**2) + >>> sin(1).diff(x) + 0 + >>> sin(pi) + 0 + >>> sin(pi/2) + 1 + >>> sin(pi/6) + 1/2 + >>> sin(pi/12) + -sqrt(2)/4 + sqrt(6)/4 + + + See Also + ======== + + csc, cos, sec, tan, cot + asin, acsc, acos, asec, atan, acot, atan2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Trigonometric_functions + .. [2] https://dlmf.nist.gov/4.14 + .. [3] https://functions.wolfram.com/ElementaryFunctions/Sin + .. [4] https://mathworld.wolfram.com/TrigonometryAngles.html + + """ + + def period(self, symbol=None): + return self._period(2*pi, symbol) + + def fdiff(self, argindex=1): + if argindex == 1: + return cos(self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, arg): + from sympy.calculus.accumulationbounds import AccumBounds + from sympy.sets.setexpr import SetExpr + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg.is_zero: + return S.Zero + elif arg in (S.Infinity, S.NegativeInfinity): + return AccumBounds(-1, 1) + + if arg is S.ComplexInfinity: + return S.NaN + + if isinstance(arg, AccumBounds): + from sympy.sets.sets import FiniteSet + min, max = arg.min, arg.max + d = floor(min/(2*pi)) + if min is not S.NegativeInfinity: + min = min - d*2*pi + if max is not S.Infinity: + max = max - d*2*pi + if AccumBounds(min, max).intersection(FiniteSet(pi/2, pi*Rational(5, 2))) \ + is not S.EmptySet and \ + AccumBounds(min, max).intersection(FiniteSet(pi*Rational(3, 2), + pi*Rational(7, 2))) is not S.EmptySet: + return AccumBounds(-1, 1) + elif AccumBounds(min, max).intersection(FiniteSet(pi/2, pi*Rational(5, 2))) \ + is not S.EmptySet: + return AccumBounds(Min(sin(min), sin(max)), 1) + elif AccumBounds(min, max).intersection(FiniteSet(pi*Rational(3, 2), pi*Rational(8, 2))) \ + is not S.EmptySet: + return AccumBounds(-1, Max(sin(min), sin(max))) + else: + return AccumBounds(Min(sin(min), sin(max)), + Max(sin(min), sin(max))) + elif isinstance(arg, SetExpr): + return arg._eval_func(cls) + + if arg.could_extract_minus_sign(): + return -cls(-arg) + + i_coeff = _imaginary_unit_as_coefficient(arg) + if i_coeff is not None: + from sympy.functions.elementary.hyperbolic import sinh + return S.ImaginaryUnit*sinh(i_coeff) + + pi_coeff = _pi_coeff(arg) + if pi_coeff is not None: + if pi_coeff.is_integer: + return S.Zero + + if (2*pi_coeff).is_integer: + # is_even-case handled above as then pi_coeff.is_integer, + # so check if known to be not even + if pi_coeff.is_even is False: + return S.NegativeOne**(pi_coeff - S.Half) + + if not pi_coeff.is_Rational: + narg = pi_coeff*pi + if narg != arg: + return cls(narg) + return None + + # https://github.com/sympy/sympy/issues/6048 + # transform a sine to a cosine, to avoid redundant code + if pi_coeff.is_Rational: + x = pi_coeff % 2 + if x > 1: + return -cls((x % 1)*pi) + if 2*x > 1: + return cls((1 - x)*pi) + narg = ((pi_coeff + Rational(3, 2)) % 2)*pi + result = cos(narg) + if not isinstance(result, cos): + return result + if pi_coeff*pi != arg: + return cls(pi_coeff*pi) + return None + + if arg.is_Add: + x, m = _peeloff_pi(arg) + if m: + m = m*pi + return sin(m)*cos(x) + cos(m)*sin(x) + + if arg.is_zero: + return S.Zero + + if isinstance(arg, asin): + return arg.args[0] + + if isinstance(arg, atan): + x = arg.args[0] + return x/sqrt(1 + x**2) + + if isinstance(arg, atan2): + y, x = arg.args + return y/sqrt(x**2 + y**2) + + if isinstance(arg, acos): + x = arg.args[0] + return sqrt(1 - x**2) + + if isinstance(arg, acot): + x = arg.args[0] + return 1/(sqrt(1 + 1/x**2)*x) + + if isinstance(arg, acsc): + x = arg.args[0] + return 1/x + + if isinstance(arg, asec): + x = arg.args[0] + return sqrt(1 - 1/x**2) + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + + if len(previous_terms) > 2: + p = previous_terms[-2] + return -p*x**2/(n*(n - 1)) + else: + return S.NegativeOne**(n//2)*x**n/factorial(n) + + def _eval_nseries(self, x, n, logx, cdir=0): + arg = self.args[0] + if logx is not None: + arg = arg.subs(log(x), logx) + if arg.subs(x, 0).has(S.NaN, S.ComplexInfinity): + raise PoleError("Cannot expand %s around 0" % (self)) + return super()._eval_nseries(x, n=n, logx=logx, cdir=cdir) + + def _eval_rewrite_as_exp(self, arg, **kwargs): + from sympy.functions.elementary.hyperbolic import HyperbolicFunction + I = S.ImaginaryUnit + if isinstance(arg, (TrigonometricFunction, HyperbolicFunction)): + arg = arg.func(arg.args[0]).rewrite(exp) + return (exp(arg*I) - exp(-arg*I))/(2*I) + + def _eval_rewrite_as_Pow(self, arg, **kwargs): + if isinstance(arg, log): + I = S.ImaginaryUnit + x = arg.args[0] + return I*x**-I/2 - I*x**I /2 + + def _eval_rewrite_as_cos(self, arg, **kwargs): + return cos(arg - pi/2, evaluate=False) + + def _eval_rewrite_as_tan(self, arg, **kwargs): + tan_half = tan(S.Half*arg) + return 2*tan_half/(1 + tan_half**2) + + def _eval_rewrite_as_sincos(self, arg, **kwargs): + return sin(arg)*cos(arg)/cos(arg) + + def _eval_rewrite_as_cot(self, arg, **kwargs): + cot_half = cot(S.Half*arg) + return Piecewise((0, And(Eq(im(arg), 0), Eq(Mod(arg, pi), 0))), + (2*cot_half/(1 + cot_half**2), True)) + + def _eval_rewrite_as_pow(self, arg, **kwargs): + return self.rewrite(cos, **kwargs).rewrite(pow, **kwargs) + + def _eval_rewrite_as_sqrt(self, arg, **kwargs): + return self.rewrite(cos, **kwargs).rewrite(sqrt, **kwargs) + + def _eval_rewrite_as_csc(self, arg, **kwargs): + return 1/csc(arg) + + def _eval_rewrite_as_sec(self, arg, **kwargs): + return 1/sec(arg - pi/2, evaluate=False) + + def _eval_rewrite_as_sinc(self, arg, **kwargs): + return arg*sinc(arg) + + def _eval_rewrite_as_besselj(self, arg, **kwargs): + from sympy.functions.special.bessel import besselj + return sqrt(pi*arg/2)*besselj(S.Half, arg) + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def as_real_imag(self, deep=True, **hints): + from sympy.functions.elementary.hyperbolic import cosh, sinh + re, im = self._as_real_imag(deep=deep, **hints) + return (sin(re)*cosh(im), cos(re)*sinh(im)) + + def _eval_expand_trig(self, **hints): + from sympy.functions.special.polynomials import chebyshevt, chebyshevu + arg = self.args[0] + x = None + if arg.is_Add: # TODO, implement more if deep stuff here + # TODO: Do this more efficiently for more than two terms + x, y = arg.as_two_terms() + sx = sin(x, evaluate=False)._eval_expand_trig() + sy = sin(y, evaluate=False)._eval_expand_trig() + cx = cos(x, evaluate=False)._eval_expand_trig() + cy = cos(y, evaluate=False)._eval_expand_trig() + return sx*cy + sy*cx + elif arg.is_Mul: + n, x = arg.as_coeff_Mul(rational=True) + if n.is_Integer: # n will be positive because of .eval + # canonicalization + + # See https://mathworld.wolfram.com/Multiple-AngleFormulas.html + if n.is_odd: + return S.NegativeOne**((n - 1)/2)*chebyshevt(n, sin(x)) + else: + return expand_mul(S.NegativeOne**(n/2 - 1)*cos(x)* + chebyshevu(n - 1, sin(x)), deep=False) + return sin(arg) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.calculus.accumulationbounds import AccumBounds + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + n = x0/pi + if n.is_integer: + lt = (arg - n*pi).as_leading_term(x) + return (S.NegativeOne**n)*lt + if x0 is S.ComplexInfinity: + x0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + if x0 in [S.Infinity, S.NegativeInfinity]: + return AccumBounds(-1, 1) + return self.func(x0) if x0.is_finite else self + + def _eval_is_extended_real(self): + if self.args[0].is_extended_real: + return True + + def _eval_is_finite(self): + arg = self.args[0] + if arg.is_extended_real: + return True + + def _eval_is_zero(self): + rest, pi_mult = _peeloff_pi(self.args[0]) + if rest.is_zero: + return pi_mult.is_integer + + def _eval_is_complex(self): + if self.args[0].is_extended_real \ + or self.args[0].is_complex: + return True + + +class cos(TrigonometricFunction): + """ + The cosine function. + + Returns the cosine of x (measured in radians). + + Explanation + =========== + + See :func:`sin` for notes about automatic evaluation. + + Examples + ======== + + >>> from sympy import cos, pi + >>> from sympy.abc import x + >>> cos(x**2).diff(x) + -2*x*sin(x**2) + >>> cos(1).diff(x) + 0 + >>> cos(pi) + -1 + >>> cos(pi/2) + 0 + >>> cos(2*pi/3) + -1/2 + >>> cos(pi/12) + sqrt(2)/4 + sqrt(6)/4 + + See Also + ======== + + sin, csc, sec, tan, cot + asin, acsc, acos, asec, atan, acot, atan2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Trigonometric_functions + .. [2] https://dlmf.nist.gov/4.14 + .. [3] https://functions.wolfram.com/ElementaryFunctions/Cos + + """ + + def period(self, symbol=None): + return self._period(2*pi, symbol) + + def fdiff(self, argindex=1): + if argindex == 1: + return -sin(self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, arg): + from sympy.functions.special.polynomials import chebyshevt + from sympy.calculus.accumulationbounds import AccumBounds + from sympy.sets.setexpr import SetExpr + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg.is_zero: + return S.One + elif arg in (S.Infinity, S.NegativeInfinity): + # In this case it is better to return AccumBounds(-1, 1) + # rather than returning S.NaN, since AccumBounds(-1, 1) + # preserves the information that sin(oo) is between + # -1 and 1, where S.NaN does not do that. + return AccumBounds(-1, 1) + + if arg is S.ComplexInfinity: + return S.NaN + + if isinstance(arg, AccumBounds): + return sin(arg + pi/2) + elif isinstance(arg, SetExpr): + return arg._eval_func(cls) + + if arg.is_extended_real and arg.is_finite is False: + return AccumBounds(-1, 1) + + if arg.could_extract_minus_sign(): + return cls(-arg) + + i_coeff = _imaginary_unit_as_coefficient(arg) + if i_coeff is not None: + from sympy.functions.elementary.hyperbolic import cosh + return cosh(i_coeff) + + pi_coeff = _pi_coeff(arg) + if pi_coeff is not None: + if pi_coeff.is_integer: + return (S.NegativeOne)**pi_coeff + + if (2*pi_coeff).is_integer: + # is_even-case handled above as then pi_coeff.is_integer, + # so check if known to be not even + if pi_coeff.is_even is False: + return S.Zero + + if not pi_coeff.is_Rational: + narg = pi_coeff*pi + if narg != arg: + return cls(narg) + return None + + # cosine formula ##################### + # https://github.com/sympy/sympy/issues/6048 + # explicit calculations are performed for + # cos(k pi/n) for n = 8,10,12,15,20,24,30,40,60,120 + # Some other exact values like cos(k pi/240) can be + # calculated using a partial-fraction decomposition + # by calling cos( X ).rewrite(sqrt) + if pi_coeff.is_Rational: + q = pi_coeff.q + p = pi_coeff.p % (2*q) + if p > q: + narg = (pi_coeff - 1)*pi + return -cls(narg) + if 2*p > q: + narg = (1 - pi_coeff)*pi + return -cls(narg) + + # If nested sqrt's are worse than un-evaluation + # you can require q to be in (1, 2, 3, 4, 6, 12) + # q <= 12, q=15, q=20, q=24, q=30, q=40, q=60, q=120 return + # expressions with 2 or fewer sqrt nestings. + table2 = _table2() + if q in table2: + a, b = table2[q] + a, b = p*pi/a, p*pi/b + nvala, nvalb = cls(a), cls(b) + if None in (nvala, nvalb): + return None + return nvala*nvalb + cls(pi/2 - a)*cls(pi/2 - b) + + if q > 12: + return None + + cst_table_some = { + 3: S.Half, + 5: (sqrt(5) + 1) / 4, + } + if q in cst_table_some: + cts = cst_table_some[pi_coeff.q] + return chebyshevt(pi_coeff.p, cts).expand() + + if 0 == q % 2: + narg = (pi_coeff*2)*pi + nval = cls(narg) + if None == nval: + return None + x = (2*pi_coeff + 1)/2 + sign_cos = (-1)**((-1 if x < 0 else 1)*int(abs(x))) + return sign_cos*sqrt( (1 + nval)/2 ) + return None + + if arg.is_Add: + x, m = _peeloff_pi(arg) + if m: + m = m*pi + return cos(m)*cos(x) - sin(m)*sin(x) + + if arg.is_zero: + return S.One + + if isinstance(arg, acos): + return arg.args[0] + + if isinstance(arg, atan): + x = arg.args[0] + return 1/sqrt(1 + x**2) + + if isinstance(arg, atan2): + y, x = arg.args + return x/sqrt(x**2 + y**2) + + if isinstance(arg, asin): + x = arg.args[0] + return sqrt(1 - x ** 2) + + if isinstance(arg, acot): + x = arg.args[0] + return 1/sqrt(1 + 1/x**2) + + if isinstance(arg, acsc): + x = arg.args[0] + return sqrt(1 - 1/x**2) + + if isinstance(arg, asec): + x = arg.args[0] + return 1/x + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0 or n % 2 == 1: + return S.Zero + else: + x = sympify(x) + + if len(previous_terms) > 2: + p = previous_terms[-2] + return -p*x**2/(n*(n - 1)) + else: + return S.NegativeOne**(n//2)*x**n/factorial(n) + + def _eval_nseries(self, x, n, logx, cdir=0): + arg = self.args[0] + if logx is not None: + arg = arg.subs(log(x), logx) + if arg.subs(x, 0).has(S.NaN, S.ComplexInfinity): + raise PoleError("Cannot expand %s around 0" % (self)) + return super()._eval_nseries(x, n=n, logx=logx, cdir=cdir) + + def _eval_rewrite_as_exp(self, arg, **kwargs): + I = S.ImaginaryUnit + from sympy.functions.elementary.hyperbolic import HyperbolicFunction + if isinstance(arg, (TrigonometricFunction, HyperbolicFunction)): + arg = arg.func(arg.args[0]).rewrite(exp, **kwargs) + return (exp(arg*I) + exp(-arg*I))/2 + + def _eval_rewrite_as_Pow(self, arg, **kwargs): + if isinstance(arg, log): + I = S.ImaginaryUnit + x = arg.args[0] + return x**I/2 + x**-I/2 + + def _eval_rewrite_as_sin(self, arg, **kwargs): + return sin(arg + pi/2, evaluate=False) + + def _eval_rewrite_as_tan(self, arg, **kwargs): + tan_half = tan(S.Half*arg)**2 + return (1 - tan_half)/(1 + tan_half) + + def _eval_rewrite_as_sincos(self, arg, **kwargs): + return sin(arg)*cos(arg)/sin(arg) + + def _eval_rewrite_as_cot(self, arg, **kwargs): + cot_half = cot(S.Half*arg)**2 + return Piecewise((1, And(Eq(im(arg), 0), Eq(Mod(arg, 2*pi), 0))), + ((cot_half - 1)/(cot_half + 1), True)) + + def _eval_rewrite_as_pow(self, arg, **kwargs): + return self._eval_rewrite_as_sqrt(arg, **kwargs) + + def _eval_rewrite_as_sqrt(self, arg: Expr, **kwargs): + from sympy.functions.special.polynomials import chebyshevt + + pi_coeff = _pi_coeff(arg) + if pi_coeff is None: + return None + + if isinstance(pi_coeff, Integer): + return None + + if not isinstance(pi_coeff, Rational): + return None + + cst_table_some = cos_table() + + if pi_coeff.q in cst_table_some: + rv = chebyshevt(pi_coeff.p, cst_table_some[pi_coeff.q]()) + if pi_coeff.q < 257: + rv = rv.expand() + return rv + + if not pi_coeff.q % 2: # recursively remove factors of 2 + pico2 = pi_coeff * 2 + nval = cos(pico2 * pi).rewrite(sqrt, **kwargs) + x = (pico2 + 1) / 2 + sign_cos = -1 if int(x) % 2 else 1 + return sign_cos * sqrt((1 + nval) / 2) + + FC = fermat_coords(pi_coeff.q) + if FC: + denoms = FC + else: + denoms = [b**e for b, e in factorint(pi_coeff.q).items()] + + apart = ipartfrac(*denoms) + decomp = (pi_coeff.p * Rational(n, d) for n, d in zip(apart, denoms)) + X = [(x[1], x[0]*pi) for x in zip(decomp, numbered_symbols('z'))] + pcls = cos(sum(x[0] for x in X))._eval_expand_trig().subs(X) + + if not FC or len(FC) == 1: + return pcls + return pcls.rewrite(sqrt, **kwargs) + + def _eval_rewrite_as_sec(self, arg, **kwargs): + return 1/sec(arg) + + def _eval_rewrite_as_csc(self, arg, **kwargs): + return 1/sec(arg).rewrite(csc, **kwargs) + + def _eval_rewrite_as_besselj(self, arg, **kwargs): + from sympy.functions.special.bessel import besselj + return Piecewise( + (sqrt(pi*arg/2)*besselj(-S.Half, arg), Ne(arg, 0)), + (1, True) + ) + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def as_real_imag(self, deep=True, **hints): + from sympy.functions.elementary.hyperbolic import cosh, sinh + re, im = self._as_real_imag(deep=deep, **hints) + return (cos(re)*cosh(im), -sin(re)*sinh(im)) + + def _eval_expand_trig(self, **hints): + from sympy.functions.special.polynomials import chebyshevt + arg = self.args[0] + x = None + if arg.is_Add: # TODO: Do this more efficiently for more than two terms + x, y = arg.as_two_terms() + sx = sin(x, evaluate=False)._eval_expand_trig() + sy = sin(y, evaluate=False)._eval_expand_trig() + cx = cos(x, evaluate=False)._eval_expand_trig() + cy = cos(y, evaluate=False)._eval_expand_trig() + return cx*cy - sx*sy + elif arg.is_Mul: + coeff, terms = arg.as_coeff_Mul(rational=True) + if coeff.is_Integer: + return chebyshevt(coeff, cos(terms)) + return cos(arg) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.calculus.accumulationbounds import AccumBounds + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + n = (x0 + pi/2)/pi + if n.is_integer: + lt = (arg - n*pi + pi/2).as_leading_term(x) + return (S.NegativeOne**n)*lt + if x0 is S.ComplexInfinity: + x0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + if x0 in [S.Infinity, S.NegativeInfinity]: + return AccumBounds(-1, 1) + return self.func(x0) if x0.is_finite else self + + def _eval_is_extended_real(self): + if self.args[0].is_extended_real: + return True + + def _eval_is_finite(self): + arg = self.args[0] + + if arg.is_extended_real: + return True + + def _eval_is_complex(self): + if self.args[0].is_extended_real \ + or self.args[0].is_complex: + return True + + def _eval_is_zero(self): + rest, pi_mult = _peeloff_pi(self.args[0]) + if rest.is_zero and pi_mult: + return (pi_mult - S.Half).is_integer + + +class tan(TrigonometricFunction): + """ + The tangent function. + + Returns the tangent of x (measured in radians). + + Explanation + =========== + + See :class:`sin` for notes about automatic evaluation. + + Examples + ======== + + >>> from sympy import tan, pi + >>> from sympy.abc import x + >>> tan(x**2).diff(x) + 2*x*(tan(x**2)**2 + 1) + >>> tan(1).diff(x) + 0 + >>> tan(pi/8).expand() + -1 + sqrt(2) + + See Also + ======== + + sin, csc, cos, sec, cot + asin, acsc, acos, asec, atan, acot, atan2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Trigonometric_functions + .. [2] https://dlmf.nist.gov/4.14 + .. [3] https://functions.wolfram.com/ElementaryFunctions/Tan + + """ + + def period(self, symbol=None): + return self._period(pi, symbol) + + def fdiff(self, argindex=1): + if argindex == 1: + return S.One + self**2 + else: + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return atan + + @classmethod + def eval(cls, arg): + from sympy.calculus.accumulationbounds import AccumBounds + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg.is_zero: + return S.Zero + elif arg in (S.Infinity, S.NegativeInfinity): + return AccumBounds(S.NegativeInfinity, S.Infinity) + + if arg is S.ComplexInfinity: + return S.NaN + + if isinstance(arg, AccumBounds): + min, max = arg.min, arg.max + d = floor(min/pi) + if min is not S.NegativeInfinity: + min = min - d*pi + if max is not S.Infinity: + max = max - d*pi + from sympy.sets.sets import FiniteSet + if AccumBounds(min, max).intersection(FiniteSet(pi/2, pi*Rational(3, 2))): + return AccumBounds(S.NegativeInfinity, S.Infinity) + else: + return AccumBounds(tan(min), tan(max)) + + if arg.could_extract_minus_sign(): + return -cls(-arg) + + i_coeff = _imaginary_unit_as_coefficient(arg) + if i_coeff is not None: + from sympy.functions.elementary.hyperbolic import tanh + return S.ImaginaryUnit*tanh(i_coeff) + + pi_coeff = _pi_coeff(arg, 2) + if pi_coeff is not None: + if pi_coeff.is_integer: + return S.Zero + + if not pi_coeff.is_Rational: + narg = pi_coeff*pi + if narg != arg: + return cls(narg) + return None + + if pi_coeff.is_Rational: + q = pi_coeff.q + p = pi_coeff.p % q + # ensure simplified results are returned for n*pi/5, n*pi/10 + table10 = { + 1: sqrt(1 - 2*sqrt(5)/5), + 2: sqrt(5 - 2*sqrt(5)), + 3: sqrt(1 + 2*sqrt(5)/5), + 4: sqrt(5 + 2*sqrt(5)) + } + if q in (5, 10): + n = 10*p/q + if n > 5: + n = 10 - n + return -table10[n] + else: + return table10[n] + if not pi_coeff.q % 2: + narg = pi_coeff*pi*2 + cresult, sresult = cos(narg), cos(narg - pi/2) + if not isinstance(cresult, cos) \ + and not isinstance(sresult, cos): + if sresult == 0: + return S.ComplexInfinity + return 1/sresult - cresult/sresult + + table2 = _table2() + if q in table2: + a, b = table2[q] + nvala, nvalb = cls(p*pi/a), cls(p*pi/b) + if None in (nvala, nvalb): + return None + return (nvala - nvalb)/(1 + nvala*nvalb) + narg = ((pi_coeff + S.Half) % 1 - S.Half)*pi + # see cos() to specify which expressions should be + # expanded automatically in terms of radicals + cresult, sresult = cos(narg), cos(narg - pi/2) + if not isinstance(cresult, cos) \ + and not isinstance(sresult, cos): + if cresult == 0: + return S.ComplexInfinity + return (sresult/cresult) + if narg != arg: + return cls(narg) + + if arg.is_Add: + x, m = _peeloff_pi(arg) + if m: + tanm = tan(m*pi) + if tanm is S.ComplexInfinity: + return -cot(x) + else: # tanm == 0 + return tan(x) + + if arg.is_zero: + return S.Zero + + if isinstance(arg, atan): + return arg.args[0] + + if isinstance(arg, atan2): + y, x = arg.args + return y/x + + if isinstance(arg, asin): + x = arg.args[0] + return x/sqrt(1 - x**2) + + if isinstance(arg, acos): + x = arg.args[0] + return sqrt(1 - x**2)/x + + if isinstance(arg, acot): + x = arg.args[0] + return 1/x + + if isinstance(arg, acsc): + x = arg.args[0] + return 1/(sqrt(1 - 1/x**2)*x) + + if isinstance(arg, asec): + x = arg.args[0] + return sqrt(1 - 1/x**2)*x + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + + a, b = ((n - 1)//2), 2**(n + 1) + + B = bernoulli(n + 1) + F = factorial(n + 1) + + return S.NegativeOne**a*b*(b - 1)*B/F*x**n + + def _eval_nseries(self, x, n, logx, cdir=0): + i = self.args[0].limit(x, 0)*2/pi + if i and i.is_Integer: + return self.rewrite(cos)._eval_nseries(x, n=n, logx=logx) + return super()._eval_nseries(x, n=n, logx=logx) + + def _eval_rewrite_as_Pow(self, arg, **kwargs): + if isinstance(arg, log): + I = S.ImaginaryUnit + x = arg.args[0] + return I*(x**-I - x**I)/(x**-I + x**I) + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def as_real_imag(self, deep=True, **hints): + re, im = self._as_real_imag(deep=deep, **hints) + if im: + from sympy.functions.elementary.hyperbolic import cosh, sinh + denom = cos(2*re) + cosh(2*im) + return (sin(2*re)/denom, sinh(2*im)/denom) + else: + return (self.func(re), S.Zero) + + def _eval_expand_trig(self, **hints): + arg = self.args[0] + x = None + if arg.is_Add: + n = len(arg.args) + TX = [] + for x in arg.args: + tx = tan(x, evaluate=False)._eval_expand_trig() + TX.append(tx) + + Yg = numbered_symbols('Y') + Y = [ next(Yg) for i in range(n) ] + + p = [0, 0] + for i in range(n + 1): + p[1 - i % 2] += symmetric_poly(i, Y)*(-1)**((i % 4)//2) + return (p[0]/p[1]).subs(list(zip(Y, TX))) + + elif arg.is_Mul: + coeff, terms = arg.as_coeff_Mul(rational=True) + if coeff.is_Integer and coeff > 1: + I = S.ImaginaryUnit + z = Symbol('dummy', real=True) + P = ((1 + I*z)**coeff).expand() + return (im(P)/re(P)).subs([(z, tan(terms))]) + return tan(arg) + + def _eval_rewrite_as_exp(self, arg, **kwargs): + I = S.ImaginaryUnit + from sympy.functions.elementary.hyperbolic import HyperbolicFunction + if isinstance(arg, (TrigonometricFunction, HyperbolicFunction)): + arg = arg.func(arg.args[0]).rewrite(exp) + neg_exp, pos_exp = exp(-arg*I), exp(arg*I) + return I*(neg_exp - pos_exp)/(neg_exp + pos_exp) + + def _eval_rewrite_as_sin(self, x, **kwargs): + return 2*sin(x)**2/sin(2*x) + + def _eval_rewrite_as_cos(self, x, **kwargs): + return cos(x - pi/2, evaluate=False)/cos(x) + + def _eval_rewrite_as_sincos(self, arg, **kwargs): + return sin(arg)/cos(arg) + + def _eval_rewrite_as_cot(self, arg, **kwargs): + return 1/cot(arg) + + def _eval_rewrite_as_sec(self, arg, **kwargs): + sin_in_sec_form = sin(arg).rewrite(sec, **kwargs) + cos_in_sec_form = cos(arg).rewrite(sec, **kwargs) + return sin_in_sec_form/cos_in_sec_form + + def _eval_rewrite_as_csc(self, arg, **kwargs): + sin_in_csc_form = sin(arg).rewrite(csc, **kwargs) + cos_in_csc_form = cos(arg).rewrite(csc, **kwargs) + return sin_in_csc_form/cos_in_csc_form + + def _eval_rewrite_as_pow(self, arg, **kwargs): + y = self.rewrite(cos, **kwargs).rewrite(pow, **kwargs) + if y.has(cos): + return None + return y + + def _eval_rewrite_as_sqrt(self, arg, **kwargs): + y = self.rewrite(cos, **kwargs).rewrite(sqrt, **kwargs) + if y.has(cos): + return None + return y + + def _eval_rewrite_as_besselj(self, arg, **kwargs): + from sympy.functions.special.bessel import besselj + return besselj(S.Half, arg)/besselj(-S.Half, arg) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.calculus.accumulationbounds import AccumBounds + from sympy.functions.elementary.complexes import re + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + n = 2*x0/pi + if n.is_integer: + lt = (arg - n*pi/2).as_leading_term(x) + return lt if n.is_even else -1/lt + if x0 is S.ComplexInfinity: + x0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + if x0 in (S.Infinity, S.NegativeInfinity): + return AccumBounds(S.NegativeInfinity, S.Infinity) + return self.func(x0) if x0.is_finite else self + + def _eval_is_extended_real(self): + # FIXME: currently tan(pi/2) return zoo + return self.args[0].is_extended_real + + def _eval_is_real(self): + arg = self.args[0] + if arg.is_real and (arg/pi - S.Half).is_integer is False: + return True + + def _eval_is_finite(self): + arg = self.args[0] + + if arg.is_real and (arg/pi - S.Half).is_integer is False: + return True + + if arg.is_imaginary: + return True + + def _eval_is_zero(self): + rest, pi_mult = _peeloff_pi(self.args[0]) + if rest.is_zero: + return pi_mult.is_integer + + def _eval_is_complex(self): + arg = self.args[0] + + if arg.is_real and (arg/pi - S.Half).is_integer is False: + return True + + +class cot(TrigonometricFunction): + """ + The cotangent function. + + Returns the cotangent of x (measured in radians). + + Explanation + =========== + + See :class:`sin` for notes about automatic evaluation. + + Examples + ======== + + >>> from sympy import cot, pi + >>> from sympy.abc import x + >>> cot(x**2).diff(x) + 2*x*(-cot(x**2)**2 - 1) + >>> cot(1).diff(x) + 0 + >>> cot(pi/12) + sqrt(3) + 2 + + See Also + ======== + + sin, csc, cos, sec, tan + asin, acsc, acos, asec, atan, acot, atan2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Trigonometric_functions + .. [2] https://dlmf.nist.gov/4.14 + .. [3] https://functions.wolfram.com/ElementaryFunctions/Cot + + """ + + def period(self, symbol=None): + return self._period(pi, symbol) + + def fdiff(self, argindex=1): + if argindex == 1: + return S.NegativeOne - self**2 + else: + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return acot + + @classmethod + def eval(cls, arg): + from sympy.calculus.accumulationbounds import AccumBounds + if arg.is_Number: + if arg is S.NaN: + return S.NaN + if arg.is_zero: + return S.ComplexInfinity + elif arg in (S.Infinity, S.NegativeInfinity): + return AccumBounds(S.NegativeInfinity, S.Infinity) + + if arg is S.ComplexInfinity: + return S.NaN + + if isinstance(arg, AccumBounds): + return -tan(arg + pi/2) + + if arg.could_extract_minus_sign(): + return -cls(-arg) + + i_coeff = _imaginary_unit_as_coefficient(arg) + if i_coeff is not None: + from sympy.functions.elementary.hyperbolic import coth + return -S.ImaginaryUnit*coth(i_coeff) + + pi_coeff = _pi_coeff(arg, 2) + if pi_coeff is not None: + if pi_coeff.is_integer: + return S.ComplexInfinity + + if not pi_coeff.is_Rational: + narg = pi_coeff*pi + if narg != arg: + return cls(narg) + return None + + if pi_coeff.is_Rational: + if pi_coeff.q in (5, 10): + return tan(pi/2 - arg) + if pi_coeff.q > 2 and not pi_coeff.q % 2: + narg = pi_coeff*pi*2 + cresult, sresult = cos(narg), cos(narg - pi/2) + if not isinstance(cresult, cos) \ + and not isinstance(sresult, cos): + return 1/sresult + cresult/sresult + q = pi_coeff.q + p = pi_coeff.p % q + table2 = _table2() + if q in table2: + a, b = table2[q] + nvala, nvalb = cls(p*pi/a), cls(p*pi/b) + if None in (nvala, nvalb): + return None + return (1 + nvala*nvalb)/(nvalb - nvala) + narg = (((pi_coeff + S.Half) % 1) - S.Half)*pi + # see cos() to specify which expressions should be + # expanded automatically in terms of radicals + cresult, sresult = cos(narg), cos(narg - pi/2) + if not isinstance(cresult, cos) \ + and not isinstance(sresult, cos): + if sresult == 0: + return S.ComplexInfinity + return cresult/sresult + if narg != arg: + return cls(narg) + + if arg.is_Add: + x, m = _peeloff_pi(arg) + if m: + cotm = cot(m*pi) + if cotm is S.ComplexInfinity: + return cot(x) + else: # cotm == 0 + return -tan(x) + + if arg.is_zero: + return S.ComplexInfinity + + if isinstance(arg, acot): + return arg.args[0] + + if isinstance(arg, atan): + x = arg.args[0] + return 1/x + + if isinstance(arg, atan2): + y, x = arg.args + return x/y + + if isinstance(arg, asin): + x = arg.args[0] + return sqrt(1 - x**2)/x + + if isinstance(arg, acos): + x = arg.args[0] + return x/sqrt(1 - x**2) + + if isinstance(arg, acsc): + x = arg.args[0] + return sqrt(1 - 1/x**2)*x + + if isinstance(arg, asec): + x = arg.args[0] + return 1/(sqrt(1 - 1/x**2)*x) + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n == 0: + return 1/sympify(x) + elif n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + + B = bernoulli(n + 1) + F = factorial(n + 1) + + return S.NegativeOne**((n + 1)//2)*2**(n + 1)*B/F*x**n + + def _eval_nseries(self, x, n, logx, cdir=0): + i = self.args[0].limit(x, 0)/pi + if i and i.is_Integer: + return self.rewrite(cos)._eval_nseries(x, n=n, logx=logx) + return self.rewrite(tan)._eval_nseries(x, n=n, logx=logx) + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def as_real_imag(self, deep=True, **hints): + re, im = self._as_real_imag(deep=deep, **hints) + if im: + from sympy.functions.elementary.hyperbolic import cosh, sinh + denom = cos(2*re) - cosh(2*im) + return (-sin(2*re)/denom, sinh(2*im)/denom) + else: + return (self.func(re), S.Zero) + + def _eval_rewrite_as_exp(self, arg, **kwargs): + from sympy.functions.elementary.hyperbolic import HyperbolicFunction + I = S.ImaginaryUnit + if isinstance(arg, (TrigonometricFunction, HyperbolicFunction)): + arg = arg.func(arg.args[0]).rewrite(exp, **kwargs) + neg_exp, pos_exp = exp(-arg*I), exp(arg*I) + return I*(pos_exp + neg_exp)/(pos_exp - neg_exp) + + def _eval_rewrite_as_Pow(self, arg, **kwargs): + if isinstance(arg, log): + I = S.ImaginaryUnit + x = arg.args[0] + return -I*(x**-I + x**I)/(x**-I - x**I) + + def _eval_rewrite_as_sin(self, x, **kwargs): + return sin(2*x)/(2*(sin(x)**2)) + + def _eval_rewrite_as_cos(self, x, **kwargs): + return cos(x)/cos(x - pi/2, evaluate=False) + + def _eval_rewrite_as_sincos(self, arg, **kwargs): + return cos(arg)/sin(arg) + + def _eval_rewrite_as_tan(self, arg, **kwargs): + return 1/tan(arg) + + def _eval_rewrite_as_sec(self, arg, **kwargs): + cos_in_sec_form = cos(arg).rewrite(sec, **kwargs) + sin_in_sec_form = sin(arg).rewrite(sec, **kwargs) + return cos_in_sec_form/sin_in_sec_form + + def _eval_rewrite_as_csc(self, arg, **kwargs): + cos_in_csc_form = cos(arg).rewrite(csc, **kwargs) + sin_in_csc_form = sin(arg).rewrite(csc, **kwargs) + return cos_in_csc_form/sin_in_csc_form + + def _eval_rewrite_as_pow(self, arg, **kwargs): + y = self.rewrite(cos, **kwargs).rewrite(pow, **kwargs) + if y.has(cos): + return None + return y + + def _eval_rewrite_as_sqrt(self, arg, **kwargs): + y = self.rewrite(cos, **kwargs).rewrite(sqrt, **kwargs) + if y.has(cos): + return None + return y + + def _eval_rewrite_as_besselj(self, arg, **kwargs): + from sympy.functions.special.bessel import besselj + return besselj(-S.Half, arg)/besselj(S.Half, arg) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.calculus.accumulationbounds import AccumBounds + from sympy.functions.elementary.complexes import re + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + n = 2*x0/pi + if n.is_integer: + lt = (arg - n*pi/2).as_leading_term(x) + return 1/lt if n.is_even else -lt + if x0 is S.ComplexInfinity: + x0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + if x0 in (S.Infinity, S.NegativeInfinity): + return AccumBounds(S.NegativeInfinity, S.Infinity) + return self.func(x0) if x0.is_finite else self + + def _eval_is_extended_real(self): + return self.args[0].is_extended_real + + def _eval_expand_trig(self, **hints): + arg = self.args[0] + x = None + if arg.is_Add: + n = len(arg.args) + CX = [] + for x in arg.args: + cx = cot(x, evaluate=False)._eval_expand_trig() + CX.append(cx) + + Yg = numbered_symbols('Y') + Y = [ next(Yg) for i in range(n) ] + + p = [0, 0] + for i in range(n, -1, -1): + p[(n - i) % 2] += symmetric_poly(i, Y)*(-1)**(((n - i) % 4)//2) + return (p[0]/p[1]).subs(list(zip(Y, CX))) + elif arg.is_Mul: + coeff, terms = arg.as_coeff_Mul(rational=True) + if coeff.is_Integer and coeff > 1: + I = S.ImaginaryUnit + z = Symbol('dummy', real=True) + P = ((z + I)**coeff).expand() + return (re(P)/im(P)).subs([(z, cot(terms))]) + return cot(arg) # XXX sec and csc return 1/cos and 1/sin + + def _eval_is_finite(self): + arg = self.args[0] + if arg.is_real and (arg/pi).is_integer is False: + return True + if arg.is_imaginary: + return True + + def _eval_is_real(self): + arg = self.args[0] + if arg.is_real and (arg/pi).is_integer is False: + return True + + def _eval_is_complex(self): + arg = self.args[0] + if arg.is_real and (arg/pi).is_integer is False: + return True + + def _eval_is_zero(self): + rest, pimult = _peeloff_pi(self.args[0]) + if pimult and rest.is_zero: + return (pimult - S.Half).is_integer + + def _eval_subs(self, old, new): + arg = self.args[0] + argnew = arg.subs(old, new) + if arg != argnew and (argnew/pi).is_integer: + return S.ComplexInfinity + return cot(argnew) + + +class ReciprocalTrigonometricFunction(TrigonometricFunction): + """Base class for reciprocal functions of trigonometric functions. """ + + _reciprocal_of = None # mandatory, to be defined in subclass + _singularities = (S.ComplexInfinity,) + + # _is_even and _is_odd are used for correct evaluation of csc(-x), sec(-x) + # TODO refactor into TrigonometricFunction common parts of + # trigonometric functions eval() like even/odd, func(x+2*k*pi), etc. + + # optional, to be defined in subclasses: + _is_even: FuzzyBool = None + _is_odd: FuzzyBool = None + + @classmethod + def eval(cls, arg): + if arg.could_extract_minus_sign(): + if cls._is_even: + return cls(-arg) + if cls._is_odd: + return -cls(-arg) + + pi_coeff = _pi_coeff(arg) + if (pi_coeff is not None + and not (2*pi_coeff).is_integer + and pi_coeff.is_Rational): + q = pi_coeff.q + p = pi_coeff.p % (2*q) + if p > q: + narg = (pi_coeff - 1)*pi + return -cls(narg) + if 2*p > q: + narg = (1 - pi_coeff)*pi + if cls._is_odd: + return cls(narg) + elif cls._is_even: + return -cls(narg) + + if hasattr(arg, 'inverse') and arg.inverse() == cls: + return arg.args[0] + + t = cls._reciprocal_of.eval(arg) + if t is None: + return t + elif any(isinstance(i, cos) for i in (t, -t)): + return (1/t).rewrite(sec) + elif any(isinstance(i, sin) for i in (t, -t)): + return (1/t).rewrite(csc) + else: + return 1/t + + def _call_reciprocal(self, method_name, *args, **kwargs): + # Calls method_name on _reciprocal_of + o = self._reciprocal_of(self.args[0]) + return getattr(o, method_name)(*args, **kwargs) + + def _calculate_reciprocal(self, method_name, *args, **kwargs): + # If calling method_name on _reciprocal_of returns a value != None + # then return the reciprocal of that value + t = self._call_reciprocal(method_name, *args, **kwargs) + return 1/t if t is not None else t + + def _rewrite_reciprocal(self, method_name, arg): + # Special handling for rewrite functions. If reciprocal rewrite returns + # unmodified expression, then return None + t = self._call_reciprocal(method_name, arg) + if t is not None and t != self._reciprocal_of(arg): + return 1/t + + def _period(self, symbol): + f = expand_mul(self.args[0]) + return self._reciprocal_of(f).period(symbol) + + def fdiff(self, argindex=1): + return -self._calculate_reciprocal("fdiff", argindex)/self**2 + + def _eval_rewrite_as_exp(self, arg, **kwargs): + return self._rewrite_reciprocal("_eval_rewrite_as_exp", arg) + + def _eval_rewrite_as_Pow(self, arg, **kwargs): + return self._rewrite_reciprocal("_eval_rewrite_as_Pow", arg) + + def _eval_rewrite_as_sin(self, arg, **kwargs): + return self._rewrite_reciprocal("_eval_rewrite_as_sin", arg) + + def _eval_rewrite_as_cos(self, arg, **kwargs): + return self._rewrite_reciprocal("_eval_rewrite_as_cos", arg) + + def _eval_rewrite_as_tan(self, arg, **kwargs): + return self._rewrite_reciprocal("_eval_rewrite_as_tan", arg) + + def _eval_rewrite_as_pow(self, arg, **kwargs): + return self._rewrite_reciprocal("_eval_rewrite_as_pow", arg) + + def _eval_rewrite_as_sqrt(self, arg, **kwargs): + return self._rewrite_reciprocal("_eval_rewrite_as_sqrt", arg) + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def as_real_imag(self, deep=True, **hints): + return (1/self._reciprocal_of(self.args[0])).as_real_imag(deep, + **hints) + + def _eval_expand_trig(self, **hints): + return self._calculate_reciprocal("_eval_expand_trig", **hints) + + def _eval_is_extended_real(self): + return self._reciprocal_of(self.args[0])._eval_is_extended_real() + + def _eval_as_leading_term(self, x, logx, cdir): + return (1/self._reciprocal_of(self.args[0]))._eval_as_leading_term(x, logx=logx, cdir=cdir) + + def _eval_is_finite(self): + return (1/self._reciprocal_of(self.args[0])).is_finite + + def _eval_nseries(self, x, n, logx, cdir=0): + return (1/self._reciprocal_of(self.args[0]))._eval_nseries(x, n, logx) + + +class sec(ReciprocalTrigonometricFunction): + """ + The secant function. + + Returns the secant of x (measured in radians). + + Explanation + =========== + + See :class:`sin` for notes about automatic evaluation. + + Examples + ======== + + >>> from sympy import sec + >>> from sympy.abc import x + >>> sec(x**2).diff(x) + 2*x*tan(x**2)*sec(x**2) + >>> sec(1).diff(x) + 0 + + See Also + ======== + + sin, csc, cos, tan, cot + asin, acsc, acos, asec, atan, acot, atan2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Trigonometric_functions + .. [2] https://dlmf.nist.gov/4.14 + .. [3] https://functions.wolfram.com/ElementaryFunctions/Sec + + """ + + _reciprocal_of = cos + _is_even = True + + def period(self, symbol=None): + return self._period(symbol) + + def _eval_rewrite_as_cot(self, arg, **kwargs): + cot_half_sq = cot(arg/2)**2 + return (cot_half_sq + 1)/(cot_half_sq - 1) + + def _eval_rewrite_as_cos(self, arg, **kwargs): + return (1/cos(arg)) + + def _eval_rewrite_as_sincos(self, arg, **kwargs): + return sin(arg)/(cos(arg)*sin(arg)) + + def _eval_rewrite_as_sin(self, arg, **kwargs): + return (1/cos(arg).rewrite(sin, **kwargs)) + + def _eval_rewrite_as_tan(self, arg, **kwargs): + return (1/cos(arg).rewrite(tan, **kwargs)) + + def _eval_rewrite_as_csc(self, arg, **kwargs): + return csc(pi/2 - arg, evaluate=False) + + def fdiff(self, argindex=1): + if argindex == 1: + return tan(self.args[0])*sec(self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_besselj(self, arg, **kwargs): + from sympy.functions.special.bessel import besselj + return Piecewise( + (1/(sqrt(pi*arg)/(sqrt(2))*besselj(-S.Half, arg)), Ne(arg, 0)), + (1, True) + ) + + def _eval_is_complex(self): + arg = self.args[0] + + if arg.is_complex and (arg/pi - S.Half).is_integer is False: + return True + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + # Reference Formula: + # https://functions.wolfram.com/ElementaryFunctions/Sec/06/01/02/01/ + if n < 0 or n % 2 == 1: + return S.Zero + else: + x = sympify(x) + k = n//2 + return S.NegativeOne**k*euler(2*k)/factorial(2*k)*x**(2*k) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.calculus.accumulationbounds import AccumBounds + from sympy.functions.elementary.complexes import re + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + n = (x0 + pi/2)/pi + if n.is_integer: + lt = (arg - n*pi + pi/2).as_leading_term(x) + return (S.NegativeOne**n)/lt + if x0 is S.ComplexInfinity: + x0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + if x0 in (S.Infinity, S.NegativeInfinity): + return AccumBounds(S.NegativeInfinity, S.Infinity) + return self.func(x0) if x0.is_finite else self + + +class csc(ReciprocalTrigonometricFunction): + """ + The cosecant function. + + Returns the cosecant of x (measured in radians). + + Explanation + =========== + + See :func:`sin` for notes about automatic evaluation. + + Examples + ======== + + >>> from sympy import csc + >>> from sympy.abc import x + >>> csc(x**2).diff(x) + -2*x*cot(x**2)*csc(x**2) + >>> csc(1).diff(x) + 0 + + See Also + ======== + + sin, cos, sec, tan, cot + asin, acsc, acos, asec, atan, acot, atan2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Trigonometric_functions + .. [2] https://dlmf.nist.gov/4.14 + .. [3] https://functions.wolfram.com/ElementaryFunctions/Csc + + """ + + _reciprocal_of = sin + _is_odd = True + + def period(self, symbol=None): + return self._period(symbol) + + def _eval_rewrite_as_sin(self, arg, **kwargs): + return (1/sin(arg)) + + def _eval_rewrite_as_sincos(self, arg, **kwargs): + return cos(arg)/(sin(arg)*cos(arg)) + + def _eval_rewrite_as_cot(self, arg, **kwargs): + cot_half = cot(arg/2) + return (1 + cot_half**2)/(2*cot_half) + + def _eval_rewrite_as_cos(self, arg, **kwargs): + return 1/sin(arg).rewrite(cos, **kwargs) + + def _eval_rewrite_as_sec(self, arg, **kwargs): + return sec(pi/2 - arg, evaluate=False) + + def _eval_rewrite_as_tan(self, arg, **kwargs): + return (1/sin(arg).rewrite(tan, **kwargs)) + + def _eval_rewrite_as_besselj(self, arg, **kwargs): + from sympy.functions.special.bessel import besselj + return sqrt(2/pi)*(1/(sqrt(arg)*besselj(S.Half, arg))) + + def fdiff(self, argindex=1): + if argindex == 1: + return -cot(self.args[0])*csc(self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_is_complex(self): + arg = self.args[0] + if arg.is_real and (arg/pi).is_integer is False: + return True + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n == 0: + return 1/sympify(x) + elif n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + k = n//2 + 1 + return (S.NegativeOne**(k - 1)*2*(2**(2*k - 1) - 1)* + bernoulli(2*k)*x**(2*k - 1)/factorial(2*k)) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.calculus.accumulationbounds import AccumBounds + from sympy.functions.elementary.complexes import re + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + n = x0/pi + if n.is_integer: + lt = (arg - n*pi).as_leading_term(x) + return (S.NegativeOne**n)/lt + if x0 is S.ComplexInfinity: + x0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + if x0 in (S.Infinity, S.NegativeInfinity): + return AccumBounds(S.NegativeInfinity, S.Infinity) + return self.func(x0) if x0.is_finite else self + + +class sinc(DefinedFunction): + r""" + Represents an unnormalized sinc function: + + .. math:: + + \operatorname{sinc}(x) = + \begin{cases} + \frac{\sin x}{x} & \qquad x \neq 0 \\ + 1 & \qquad x = 0 + \end{cases} + + Examples + ======== + + >>> from sympy import sinc, oo, jn + >>> from sympy.abc import x + >>> sinc(x) + sinc(x) + + * Automated Evaluation + + >>> sinc(0) + 1 + >>> sinc(oo) + 0 + + * Differentiation + + >>> sinc(x).diff() + cos(x)/x - sin(x)/x**2 + + * Series Expansion + + >>> sinc(x).series() + 1 - x**2/6 + x**4/120 + O(x**6) + + * As zero'th order spherical Bessel Function + + >>> sinc(x).rewrite(jn) + jn(0, x) + + See also + ======== + + sin + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Sinc_function + + """ + _singularities = (S.ComplexInfinity,) + + def fdiff(self, argindex=1): + x = self.args[0] + if argindex == 1: + # We would like to return the Piecewise here, but Piecewise.diff + # currently can't handle removable singularities, meaning things + # like sinc(x).diff(x, 2) give the wrong answer at x = 0. See + # https://github.com/sympy/sympy/issues/11402. + # + # return Piecewise(((x*cos(x) - sin(x))/x**2, Ne(x, S.Zero)), (S.Zero, S.true)) + return cos(x)/x - sin(x)/x**2 + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, arg): + if arg.is_zero: + return S.One + if arg.is_Number: + if arg in [S.Infinity, S.NegativeInfinity]: + return S.Zero + elif arg is S.NaN: + return S.NaN + + if arg is S.ComplexInfinity: + return S.NaN + + if arg.could_extract_minus_sign(): + return cls(-arg) + + pi_coeff = _pi_coeff(arg) + if pi_coeff is not None: + if pi_coeff.is_integer: + if fuzzy_not(arg.is_zero): + return S.Zero + elif (2*pi_coeff).is_integer: + return S.NegativeOne**(pi_coeff - S.Half)/arg + + def _eval_nseries(self, x, n, logx, cdir=0): + x = self.args[0] + return (sin(x)/x)._eval_nseries(x, n, logx) + + def _eval_rewrite_as_jn(self, arg, **kwargs): + from sympy.functions.special.bessel import jn + return jn(0, arg) + + def _eval_rewrite_as_sin(self, arg, **kwargs): + return Piecewise((sin(arg)/arg, Ne(arg, S.Zero)), (S.One, S.true)) + + def _eval_is_zero(self): + if self.args[0].is_infinite: + return True + rest, pi_mult = _peeloff_pi(self.args[0]) + if rest.is_zero: + return fuzzy_and([pi_mult.is_integer, pi_mult.is_nonzero]) + if rest.is_Number and pi_mult.is_integer: + return False + + def _eval_is_real(self): + if self.args[0].is_extended_real or self.args[0].is_imaginary: + return True + + _eval_is_finite = _eval_is_real + + +############################################################################### +########################### TRIGONOMETRIC INVERSES ############################ +############################################################################### + + +class InverseTrigonometricFunction(DefinedFunction): + """Base class for inverse trigonometric functions.""" + _singularities: tuple[Expr, ...] = (S.One, S.NegativeOne, S.Zero, S.ComplexInfinity) + + @staticmethod + @cacheit + def _asin_table(): + # Only keys with could_extract_minus_sign() == False + # are actually needed. + return { + sqrt(3)/2: pi/3, + sqrt(2)/2: pi/4, + 1/sqrt(2): pi/4, + sqrt((5 - sqrt(5))/8): pi/5, + sqrt(2)*sqrt(5 - sqrt(5))/4: pi/5, + sqrt((5 + sqrt(5))/8): pi*Rational(2, 5), + sqrt(2)*sqrt(5 + sqrt(5))/4: pi*Rational(2, 5), + S.Half: pi/6, + sqrt(2 - sqrt(2))/2: pi/8, + sqrt(S.Half - sqrt(2)/4): pi/8, + sqrt(2 + sqrt(2))/2: pi*Rational(3, 8), + sqrt(S.Half + sqrt(2)/4): pi*Rational(3, 8), + (sqrt(5) - 1)/4: pi/10, + (1 - sqrt(5))/4: -pi/10, + (sqrt(5) + 1)/4: pi*Rational(3, 10), + sqrt(6)/4 - sqrt(2)/4: pi/12, + -sqrt(6)/4 + sqrt(2)/4: -pi/12, + (sqrt(3) - 1)/sqrt(8): pi/12, + (1 - sqrt(3))/sqrt(8): -pi/12, + sqrt(6)/4 + sqrt(2)/4: pi*Rational(5, 12), + (1 + sqrt(3))/sqrt(8): pi*Rational(5, 12) + } + + + @staticmethod + @cacheit + def _atan_table(): + # Only keys with could_extract_minus_sign() == False + # are actually needed. + return { + sqrt(3)/3: pi/6, + 1/sqrt(3): pi/6, + sqrt(3): pi/3, + sqrt(2) - 1: pi/8, + 1 - sqrt(2): -pi/8, + 1 + sqrt(2): pi*Rational(3, 8), + sqrt(5 - 2*sqrt(5)): pi/5, + sqrt(5 + 2*sqrt(5)): pi*Rational(2, 5), + sqrt(1 - 2*sqrt(5)/5): pi/10, + sqrt(1 + 2*sqrt(5)/5): pi*Rational(3, 10), + 2 - sqrt(3): pi/12, + -2 + sqrt(3): -pi/12, + 2 + sqrt(3): pi*Rational(5, 12) + } + + @staticmethod + @cacheit + def _acsc_table(): + # Keys for which could_extract_minus_sign() + # will obviously return True are omitted. + return { + 2*sqrt(3)/3: pi/3, + sqrt(2): pi/4, + sqrt(2 + 2*sqrt(5)/5): pi/5, + 1/sqrt(Rational(5, 8) - sqrt(5)/8): pi/5, + sqrt(2 - 2*sqrt(5)/5): pi*Rational(2, 5), + 1/sqrt(Rational(5, 8) + sqrt(5)/8): pi*Rational(2, 5), + 2: pi/6, + sqrt(4 + 2*sqrt(2)): pi/8, + 2/sqrt(2 - sqrt(2)): pi/8, + sqrt(4 - 2*sqrt(2)): pi*Rational(3, 8), + 2/sqrt(2 + sqrt(2)): pi*Rational(3, 8), + 1 + sqrt(5): pi/10, + sqrt(5) - 1: pi*Rational(3, 10), + -(sqrt(5) - 1): pi*Rational(-3, 10), + sqrt(6) + sqrt(2): pi/12, + sqrt(6) - sqrt(2): pi*Rational(5, 12), + -(sqrt(6) - sqrt(2)): pi*Rational(-5, 12) + } + + +class asin(InverseTrigonometricFunction): + r""" + The inverse sine function. + + Returns the arcsine of x in radians. + + Explanation + =========== + + ``asin(x)`` will evaluate automatically in the cases + $x \in \{\infty, -\infty, 0, 1, -1\}$ and for some instances when the + result is a rational multiple of $\pi$ (see the ``eval`` class method). + + A purely imaginary argument will lead to an asinh expression. + + Examples + ======== + + >>> from sympy import asin, oo + >>> asin(1) + pi/2 + >>> asin(-1) + -pi/2 + >>> asin(-oo) + oo*I + >>> asin(oo) + -oo*I + + See Also + ======== + + sin, csc, cos, sec, tan, cot + acsc, acos, asec, atan, acot, atan2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Inverse_trigonometric_functions + .. [2] https://dlmf.nist.gov/4.23 + .. [3] https://functions.wolfram.com/ElementaryFunctions/ArcSin + + """ + + def fdiff(self, argindex=1): + if argindex == 1: + return 1/sqrt(1 - self.args[0]**2) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_is_rational(self): + s = self.func(*self.args) + if s.func == self.func: + if s.args[0].is_rational: + return False + else: + return s.is_rational + + def _eval_is_positive(self): + return self._eval_is_extended_real() and self.args[0].is_positive + + def _eval_is_negative(self): + return self._eval_is_extended_real() and self.args[0].is_negative + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.NegativeInfinity*S.ImaginaryUnit + elif arg is S.NegativeInfinity: + return S.Infinity*S.ImaginaryUnit + elif arg.is_zero: + return S.Zero + elif arg is S.One: + return pi/2 + elif arg is S.NegativeOne: + return -pi/2 + + if arg is S.ComplexInfinity: + return S.ComplexInfinity + + if arg.could_extract_minus_sign(): + return -cls(-arg) + + if arg.is_number: + asin_table = cls._asin_table() + if arg in asin_table: + return asin_table[arg] + + i_coeff = _imaginary_unit_as_coefficient(arg) + if i_coeff is not None: + from sympy.functions.elementary.hyperbolic import asinh + return S.ImaginaryUnit*asinh(i_coeff) + + if arg.is_zero: + return S.Zero + + if isinstance(arg, sin): + ang = arg.args[0] + if ang.is_comparable: + ang %= 2*pi # restrict to [0,2*pi) + if ang > pi: # restrict to (-pi,pi] + ang = pi - ang + + # restrict to [-pi/2,pi/2] + if ang > pi/2: + ang = pi - ang + if ang < -pi/2: + ang = -pi - ang + + return ang + + if isinstance(arg, cos): # acos(x) + asin(x) = pi/2 + ang = arg.args[0] + if ang.is_comparable: + return pi/2 - acos(arg) + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + if len(previous_terms) >= 2 and n > 2: + p = previous_terms[-2] + return p*(n - 2)**2/(n*(n - 1))*x**2 + else: + k = (n - 1) // 2 + R = RisingFactorial(S.Half, k) + F = factorial(k) + return R/F*x**n/n + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + if x0 is S.NaN: + return self.func(arg.as_leading_term(x)) + if x0.is_zero: + return arg.as_leading_term(x) + + # Handling branch points + if x0 in (-S.One, S.One, S.ComplexInfinity): + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir).expand() + # Handling points lying on branch cuts (-oo, -1) U (1, oo) + if (1 - x0**2).is_negative: + ndir = arg.dir(x, cdir if cdir else 1) + if im(ndir).is_negative: + if x0.is_negative: + return -pi - self.func(x0) + elif im(ndir).is_positive: + if x0.is_positive: + return pi - self.func(x0) + else: + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir).expand() + return self.func(x0) + + def _eval_nseries(self, x, n, logx, cdir=0): # asin + from sympy.series.order import O + arg0 = self.args[0].subs(x, 0) + # Handling branch points + if arg0 is S.One: + t = Dummy('t', positive=True) + ser = asin(S.One - t**2).rewrite(log).nseries(t, 0, 2*n) + arg1 = S.One - self.args[0] + f = arg1.as_leading_term(x) + g = (arg1 - f)/ f + if not g.is_meromorphic(x, 0): # cannot be expanded + return O(1) if n == 0 else pi/2 + O(sqrt(x)) + res1 = sqrt(S.One + g)._eval_nseries(x, n=n, logx=logx) + res = (res1.removeO()*sqrt(f)).expand() + return ser.removeO().subs(t, res).expand().powsimp() + O(x**n, x) + + if arg0 is S.NegativeOne: + t = Dummy('t', positive=True) + ser = asin(S.NegativeOne + t**2).rewrite(log).nseries(t, 0, 2*n) + arg1 = S.One + self.args[0] + f = arg1.as_leading_term(x) + g = (arg1 - f)/ f + if not g.is_meromorphic(x, 0): # cannot be expanded + return O(1) if n == 0 else -pi/2 + O(sqrt(x)) + res1 = sqrt(S.One + g)._eval_nseries(x, n=n, logx=logx) + res = (res1.removeO()*sqrt(f)).expand() + return ser.removeO().subs(t, res).expand().powsimp() + O(x**n, x) + + res = super()._eval_nseries(x, n=n, logx=logx) + if arg0 is S.ComplexInfinity: + return res + # Handling points lying on branch cuts (-oo, -1) U (1, oo) + if (1 - arg0**2).is_negative: + ndir = self.args[0].dir(x, cdir if cdir else 1) + if im(ndir).is_negative: + if arg0.is_negative: + return -pi - res + elif im(ndir).is_positive: + if arg0.is_positive: + return pi - res + else: + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + return res + + def _eval_rewrite_as_acos(self, x, **kwargs): + return pi/2 - acos(x) + + def _eval_rewrite_as_atan(self, x, **kwargs): + return 2*atan(x/(1 + sqrt(1 - x**2))) + + def _eval_rewrite_as_log(self, x, **kwargs): + return -S.ImaginaryUnit*log(S.ImaginaryUnit*x + sqrt(1 - x**2)) + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + def _eval_rewrite_as_acot(self, arg, **kwargs): + return 2*acot((1 + sqrt(1 - arg**2))/arg) + + def _eval_rewrite_as_asec(self, arg, **kwargs): + return pi/2 - asec(1/arg) + + def _eval_rewrite_as_acsc(self, arg, **kwargs): + return acsc(1/arg) + + def _eval_is_extended_real(self): + x = self.args[0] + return x.is_extended_real and (1 - abs(x)).is_nonnegative + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return sin + + +class acos(InverseTrigonometricFunction): + r""" + The inverse cosine function. + + Explanation + =========== + + Returns the arc cosine of x (measured in radians). + + ``acos(x)`` will evaluate automatically in the cases + $x \in \{\infty, -\infty, 0, 1, -1\}$ and for some instances when + the result is a rational multiple of $\pi$ (see the eval class method). + + ``acos(zoo)`` evaluates to ``zoo`` + (see note in :class:`sympy.functions.elementary.trigonometric.asec`) + + A purely imaginary argument will be rewritten to asinh. + + Examples + ======== + + >>> from sympy import acos, oo + >>> acos(1) + 0 + >>> acos(0) + pi/2 + >>> acos(oo) + oo*I + + See Also + ======== + + sin, csc, cos, sec, tan, cot + asin, acsc, asec, atan, acot, atan2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Inverse_trigonometric_functions + .. [2] https://dlmf.nist.gov/4.23 + .. [3] https://functions.wolfram.com/ElementaryFunctions/ArcCos + + """ + + def fdiff(self, argindex=1): + if argindex == 1: + return -1/sqrt(1 - self.args[0]**2) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_is_rational(self): + s = self.func(*self.args) + if s.func == self.func: + if s.args[0].is_rational: + return False + else: + return s.is_rational + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.Infinity*S.ImaginaryUnit + elif arg is S.NegativeInfinity: + return S.NegativeInfinity*S.ImaginaryUnit + elif arg.is_zero: + return pi/2 + elif arg is S.One: + return S.Zero + elif arg is S.NegativeOne: + return pi + + if arg is S.ComplexInfinity: + return S.ComplexInfinity + + if arg.is_number: + asin_table = cls._asin_table() + if arg in asin_table: + return pi/2 - asin_table[arg] + elif -arg in asin_table: + return pi/2 + asin_table[-arg] + + i_coeff = _imaginary_unit_as_coefficient(arg) + if i_coeff is not None: + return pi/2 - asin(arg) + + if arg.is_Mul and len(arg.args) == 2 and arg.args[0] == -1: + narg = arg.args[1] + minus = True + else: + narg = arg + minus = False + + if isinstance(narg, cos): + # acos(cos(x)) = x or acos(-cos(x)) = pi - x + ang = narg.args[0] + if ang.is_comparable: + if minus: + ang = pi - ang + ang %= 2*pi # restrict to [0,2*pi) + if ang > pi: # restrict to [0,pi] + ang = 2*pi - ang + return ang + + if isinstance(narg, sin): # acos(x) + asin(x) = pi/2 + ang = narg.args[0] + if ang.is_comparable: + if minus: + return pi/2 + asin(narg) + return pi/2 - asin(narg) + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n == 0: + return pi/2 + elif n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + if len(previous_terms) >= 2 and n > 2: + p = previous_terms[-2] + return p*(n - 2)**2/(n*(n - 1))*x**2 + else: + k = (n - 1) // 2 + R = RisingFactorial(S.Half, k) + F = factorial(k) + return -R/F*x**n/n + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + if x0 is S.NaN: + return self.func(arg.as_leading_term(x)) + # Handling branch points + if x0 == 1: + return sqrt(2)*sqrt((S.One - arg).as_leading_term(x)) + if x0 in (-S.One, S.ComplexInfinity): + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir) + # Handling points lying on branch cuts (-oo, -1) U (1, oo) + if (1 - x0**2).is_negative: + ndir = arg.dir(x, cdir if cdir else 1) + if im(ndir).is_negative: + if x0.is_negative: + return 2*pi - self.func(x0) + elif im(ndir).is_positive: + if x0.is_positive: + return -self.func(x0) + else: + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir).expand() + return self.func(x0) + + def _eval_is_extended_real(self): + x = self.args[0] + return x.is_extended_real and (1 - abs(x)).is_nonnegative + + def _eval_is_nonnegative(self): + return self._eval_is_extended_real() + + def _eval_nseries(self, x, n, logx, cdir=0): # acos + from sympy.series.order import O + arg0 = self.args[0].subs(x, 0) + # Handling branch points + if arg0 is S.One: + t = Dummy('t', positive=True) + ser = acos(S.One - t**2).rewrite(log).nseries(t, 0, 2*n) + arg1 = S.One - self.args[0] + f = arg1.as_leading_term(x) + g = (arg1 - f)/ f + if not g.is_meromorphic(x, 0): # cannot be expanded + return O(1) if n == 0 else O(sqrt(x)) + res1 = sqrt(S.One + g)._eval_nseries(x, n=n, logx=logx) + res = (res1.removeO()*sqrt(f)).expand() + return ser.removeO().subs(t, res).expand().powsimp() + O(x**n, x) + + if arg0 is S.NegativeOne: + t = Dummy('t', positive=True) + ser = acos(S.NegativeOne + t**2).rewrite(log).nseries(t, 0, 2*n) + arg1 = S.One + self.args[0] + f = arg1.as_leading_term(x) + g = (arg1 - f)/ f + if not g.is_meromorphic(x, 0): # cannot be expanded + return O(1) if n == 0 else pi + O(sqrt(x)) + res1 = sqrt(S.One + g)._eval_nseries(x, n=n, logx=logx) + res = (res1.removeO()*sqrt(f)).expand() + return ser.removeO().subs(t, res).expand().powsimp() + O(x**n, x) + + res = super()._eval_nseries(x, n=n, logx=logx) + if arg0 is S.ComplexInfinity: + return res + # Handling points lying on branch cuts (-oo, -1) U (1, oo) + if (1 - arg0**2).is_negative: + ndir = self.args[0].dir(x, cdir if cdir else 1) + if im(ndir).is_negative: + if arg0.is_negative: + return 2*pi - res + elif im(ndir).is_positive: + if arg0.is_positive: + return -res + else: + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + return res + + def _eval_rewrite_as_log(self, x, **kwargs): + return pi/2 + S.ImaginaryUnit*\ + log(S.ImaginaryUnit*x + sqrt(1 - x**2)) + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + def _eval_rewrite_as_asin(self, x, **kwargs): + return pi/2 - asin(x) + + def _eval_rewrite_as_atan(self, x, **kwargs): + return atan(sqrt(1 - x**2)/x) + (pi/2)*(1 - x*sqrt(1/x**2)) + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return cos + + def _eval_rewrite_as_acot(self, arg, **kwargs): + return pi/2 - 2*acot((1 + sqrt(1 - arg**2))/arg) + + def _eval_rewrite_as_asec(self, arg, **kwargs): + return asec(1/arg) + + def _eval_rewrite_as_acsc(self, arg, **kwargs): + return pi/2 - acsc(1/arg) + + def _eval_conjugate(self): + z = self.args[0] + r = self.func(self.args[0].conjugate()) + if z.is_extended_real is False: + return r + elif z.is_extended_real and (z + 1).is_nonnegative and (z - 1).is_nonpositive: + return r + + +class atan(InverseTrigonometricFunction): + r""" + The inverse tangent function. + + Returns the arc tangent of x (measured in radians). + + Explanation + =========== + + ``atan(x)`` will evaluate automatically in the cases + $x \in \{\infty, -\infty, 0, 1, -1\}$ and for some instances when the + result is a rational multiple of $\pi$ (see the eval class method). + + Examples + ======== + + >>> from sympy import atan, oo + >>> atan(0) + 0 + >>> atan(1) + pi/4 + >>> atan(oo) + pi/2 + + See Also + ======== + + sin, csc, cos, sec, tan, cot + asin, acsc, acos, asec, acot, atan2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Inverse_trigonometric_functions + .. [2] https://dlmf.nist.gov/4.23 + .. [3] https://functions.wolfram.com/ElementaryFunctions/ArcTan + + """ + + args: tuple[Expr] + + _singularities = (S.ImaginaryUnit, -S.ImaginaryUnit) + + def fdiff(self, argindex=1): + if argindex == 1: + return 1/(1 + self.args[0]**2) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_is_rational(self): + s = self.func(*self.args) + if s.func == self.func: + if s.args[0].is_rational: + return False + else: + return s.is_rational + + def _eval_is_positive(self): + return self.args[0].is_extended_positive + + def _eval_is_nonnegative(self): + return self.args[0].is_extended_nonnegative + + def _eval_is_zero(self): + return self.args[0].is_zero + + def _eval_is_real(self): + return self.args[0].is_extended_real + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return pi/2 + elif arg is S.NegativeInfinity: + return -pi/2 + elif arg.is_zero: + return S.Zero + elif arg is S.One: + return pi/4 + elif arg is S.NegativeOne: + return -pi/4 + + if arg is S.ComplexInfinity: + from sympy.calculus.accumulationbounds import AccumBounds + return AccumBounds(-pi/2, pi/2) + + if arg.could_extract_minus_sign(): + return -cls(-arg) + + if arg.is_number: + atan_table = cls._atan_table() + if arg in atan_table: + return atan_table[arg] + + i_coeff = _imaginary_unit_as_coefficient(arg) + if i_coeff is not None: + from sympy.functions.elementary.hyperbolic import atanh + return S.ImaginaryUnit*atanh(i_coeff) + + if arg.is_zero: + return S.Zero + + if isinstance(arg, tan): + ang = arg.args[0] + if ang.is_comparable: + ang %= pi # restrict to [0,pi) + if ang > pi/2: # restrict to [-pi/2,pi/2] + ang -= pi + + return ang + + if isinstance(arg, cot): # atan(x) + acot(x) = pi/2 + ang = arg.args[0] + if ang.is_comparable: + ang = pi/2 - acot(arg) + if ang > pi/2: # restrict to [-pi/2,pi/2] + ang -= pi + return ang + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + return S.NegativeOne**((n - 1)//2)*x**n/n + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + if x0 is S.NaN: + return self.func(arg.as_leading_term(x)) + if x0.is_zero: + return arg.as_leading_term(x) + # Handling branch points + if x0 in (-S.ImaginaryUnit, S.ImaginaryUnit, S.ComplexInfinity): + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir).expand() + # Handling points lying on branch cuts (-I*oo, -I) U (I, I*oo) + if (1 + x0**2).is_negative: + ndir = arg.dir(x, cdir if cdir else 1) + if re(ndir).is_negative: + if im(x0).is_positive: + return self.func(x0) - pi + elif re(ndir).is_positive: + if im(x0).is_negative: + return self.func(x0) + pi + else: + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir).expand() + return self.func(x0) + + def _eval_nseries(self, x, n, logx, cdir=0): # atan + arg0 = self.args[0].subs(x, 0) + + # Handling branch points + if arg0 in (S.ImaginaryUnit, S.NegativeOne*S.ImaginaryUnit): + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + + res = super()._eval_nseries(x, n=n, logx=logx) + ndir = self.args[0].dir(x, cdir if cdir else 1) + if arg0 is S.ComplexInfinity: + if re(ndir) > 0: + return res - pi + return res + # Handling points lying on branch cuts (-I*oo, -I) U (I, I*oo) + if (1 + arg0**2).is_negative: + if re(ndir).is_negative: + if im(arg0).is_positive: + return res - pi + elif re(ndir).is_positive: + if im(arg0).is_negative: + return res + pi + else: + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + return res + + def _eval_rewrite_as_log(self, x, **kwargs): + return S.ImaginaryUnit/2*(log(S.One - S.ImaginaryUnit*x) + - log(S.One + S.ImaginaryUnit*x)) + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + def _eval_aseries(self, n, args0, x, logx): + if args0[0] in [S.Infinity, S.NegativeInfinity]: + return (pi/2 - atan(1/self.args[0]))._eval_nseries(x, n, logx) + else: + return super()._eval_aseries(n, args0, x, logx) + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return tan + + def _eval_rewrite_as_asin(self, arg, **kwargs): + return sqrt(arg**2)/arg*(pi/2 - asin(1/sqrt(1 + arg**2))) + + def _eval_rewrite_as_acos(self, arg, **kwargs): + return sqrt(arg**2)/arg*acos(1/sqrt(1 + arg**2)) + + def _eval_rewrite_as_acot(self, arg, **kwargs): + return acot(1/arg) + + def _eval_rewrite_as_asec(self, arg, **kwargs): + return sqrt(arg**2)/arg*asec(sqrt(1 + arg**2)) + + def _eval_rewrite_as_acsc(self, arg, **kwargs): + return sqrt(arg**2)/arg*(pi/2 - acsc(sqrt(1 + arg**2))) + + +class acot(InverseTrigonometricFunction): + r""" + The inverse cotangent function. + + Returns the arc cotangent of x (measured in radians). + + Explanation + =========== + + ``acot(x)`` will evaluate automatically in the cases + $x \in \{\infty, -\infty, \tilde{\infty}, 0, 1, -1\}$ + and for some instances when the result is a rational multiple of $\pi$ + (see the eval class method). + + A purely imaginary argument will lead to an ``acoth`` expression. + + ``acot(x)`` has a branch cut along $(-i, i)$, hence it is discontinuous + at 0. Its range for real $x$ is $(-\frac{\pi}{2}, \frac{\pi}{2}]$. + + Examples + ======== + + >>> from sympy import acot, sqrt + >>> acot(0) + pi/2 + >>> acot(1) + pi/4 + >>> acot(sqrt(3) - 2) + -5*pi/12 + + See Also + ======== + + sin, csc, cos, sec, tan, cot + asin, acsc, acos, asec, atan, atan2 + + References + ========== + + .. [1] https://dlmf.nist.gov/4.23 + .. [2] https://functions.wolfram.com/ElementaryFunctions/ArcCot + + """ + _singularities = (S.ImaginaryUnit, -S.ImaginaryUnit) + + def fdiff(self, argindex=1): + if argindex == 1: + return -1/(1 + self.args[0]**2) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_is_rational(self): + s = self.func(*self.args) + if s.func == self.func: + if s.args[0].is_rational: + return False + else: + return s.is_rational + + def _eval_is_positive(self): + return self.args[0].is_nonnegative + + def _eval_is_negative(self): + return self.args[0].is_negative + + def _eval_is_extended_real(self): + return self.args[0].is_extended_real + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.Zero + elif arg is S.NegativeInfinity: + return S.Zero + elif arg.is_zero: + return pi/ 2 + elif arg is S.One: + return pi/4 + elif arg is S.NegativeOne: + return -pi/4 + + if arg is S.ComplexInfinity: + return S.Zero + + if arg.could_extract_minus_sign(): + return -cls(-arg) + + if arg.is_number: + atan_table = cls._atan_table() + if arg in atan_table: + ang = pi/2 - atan_table[arg] + if ang > pi/2: # restrict to (-pi/2,pi/2] + ang -= pi + return ang + + i_coeff = _imaginary_unit_as_coefficient(arg) + if i_coeff is not None: + from sympy.functions.elementary.hyperbolic import acoth + return -S.ImaginaryUnit*acoth(i_coeff) + + if arg.is_zero: + return pi*S.Half + + if isinstance(arg, cot): + ang = arg.args[0] + if ang.is_comparable: + ang %= pi # restrict to [0,pi) + if ang > pi/2: # restrict to (-pi/2,pi/2] + ang -= pi + return ang + + if isinstance(arg, tan): # atan(x) + acot(x) = pi/2 + ang = arg.args[0] + if ang.is_comparable: + ang = pi/2 - atan(arg) + if ang > pi/2: # restrict to (-pi/2,pi/2] + ang -= pi + return ang + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n == 0: + return pi/2 # FIX THIS + elif n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + return S.NegativeOne**((n + 1)//2)*x**n/n + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + if x0 is S.NaN: + return self.func(arg.as_leading_term(x)) + if x0 is S.ComplexInfinity: + return (1/arg).as_leading_term(x) + # Handling branch points + if x0 in (-S.ImaginaryUnit, S.ImaginaryUnit, S.Zero): + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir).expand() + # Handling points lying on branch cuts [-I, I] + if x0.is_imaginary and (1 + x0**2).is_positive: + ndir = arg.dir(x, cdir if cdir else 1) + if re(ndir).is_positive: + if im(x0).is_positive: + return self.func(x0) + pi + elif re(ndir).is_negative: + if im(x0).is_negative: + return self.func(x0) - pi + else: + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir).expand() + return self.func(x0) + + def _eval_nseries(self, x, n, logx, cdir=0): # acot + arg0 = self.args[0].subs(x, 0) + + # Handling branch points + if arg0 in (S.ImaginaryUnit, S.NegativeOne*S.ImaginaryUnit): + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + + res = super()._eval_nseries(x, n=n, logx=logx) + if arg0 is S.ComplexInfinity: + return res + ndir = self.args[0].dir(x, cdir if cdir else 1) + if arg0.is_zero: + if re(ndir) < 0: + return res - pi + return res + # Handling points lying on branch cuts [-I, I] + if arg0.is_imaginary and (1 + arg0**2).is_positive: + if re(ndir).is_positive: + if im(arg0).is_positive: + return res + pi + elif re(ndir).is_negative: + if im(arg0).is_negative: + return res - pi + else: + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + return res + + def _eval_aseries(self, n, args0, x, logx): + if args0[0] in [S.Infinity, S.NegativeInfinity]: + return atan(1/self.args[0])._eval_nseries(x, n, logx) + else: + return super()._eval_aseries(n, args0, x, logx) + + def _eval_rewrite_as_log(self, x, **kwargs): + return S.ImaginaryUnit/2*(log(1 - S.ImaginaryUnit/x) + - log(1 + S.ImaginaryUnit/x)) + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return cot + + def _eval_rewrite_as_asin(self, arg, **kwargs): + return (arg*sqrt(1/arg**2)* + (pi/2 - asin(sqrt(-arg**2)/sqrt(-arg**2 - 1)))) + + def _eval_rewrite_as_acos(self, arg, **kwargs): + return arg*sqrt(1/arg**2)*acos(sqrt(-arg**2)/sqrt(-arg**2 - 1)) + + def _eval_rewrite_as_atan(self, arg, **kwargs): + return atan(1/arg) + + def _eval_rewrite_as_asec(self, arg, **kwargs): + return arg*sqrt(1/arg**2)*asec(sqrt((1 + arg**2)/arg**2)) + + def _eval_rewrite_as_acsc(self, arg, **kwargs): + return arg*sqrt(1/arg**2)*(pi/2 - acsc(sqrt((1 + arg**2)/arg**2))) + + +class asec(InverseTrigonometricFunction): + r""" + The inverse secant function. + + Returns the arc secant of x (measured in radians). + + Explanation + =========== + + ``asec(x)`` will evaluate automatically in the cases + $x \in \{\infty, -\infty, 0, 1, -1\}$ and for some instances when the + result is a rational multiple of $\pi$ (see the eval class method). + + ``asec(x)`` has branch cut in the interval $[-1, 1]$. For complex arguments, + it can be defined [4]_ as + + .. math:: + \operatorname{sec^{-1}}(z) = -i\frac{\log\left(\sqrt{1 - z^2} + 1\right)}{z} + + At ``x = 0``, for positive branch cut, the limit evaluates to ``zoo``. For + negative branch cut, the limit + + .. math:: + \lim_{z \to 0}-i\frac{\log\left(-\sqrt{1 - z^2} + 1\right)}{z} + + simplifies to :math:`-i\log\left(z/2 + O\left(z^3\right)\right)` which + ultimately evaluates to ``zoo``. + + As ``acos(x) = asec(1/x)``, a similar argument can be given for + ``acos(x)``. + + Examples + ======== + + >>> from sympy import asec, oo + >>> asec(1) + 0 + >>> asec(-1) + pi + >>> asec(0) + zoo + >>> asec(-oo) + pi/2 + + See Also + ======== + + sin, csc, cos, sec, tan, cot + asin, acsc, acos, atan, acot, atan2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Inverse_trigonometric_functions + .. [2] https://dlmf.nist.gov/4.23 + .. [3] https://functions.wolfram.com/ElementaryFunctions/ArcSec + .. [4] https://reference.wolfram.com/language/ref/ArcSec.html + + """ + + @classmethod + def eval(cls, arg): + if arg.is_zero: + return S.ComplexInfinity + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.One: + return S.Zero + elif arg is S.NegativeOne: + return pi + if arg in [S.Infinity, S.NegativeInfinity, S.ComplexInfinity]: + return pi/2 + + if arg.is_number: + acsc_table = cls._acsc_table() + if arg in acsc_table: + return pi/2 - acsc_table[arg] + elif -arg in acsc_table: + return pi/2 + acsc_table[-arg] + + if arg.is_infinite: + return pi/2 + + if arg.is_Mul and len(arg.args) == 2 and arg.args[0] == -1: + narg = arg.args[1] + minus = True + else: + narg = arg + minus = False + + if isinstance(narg, sec): + # asec(sec(x)) = x or asec(-sec(x)) = pi - x + ang = narg.args[0] + if ang.is_comparable: + if minus: + ang = pi - ang + ang %= 2*pi # restrict to [0,2*pi) + if ang > pi: # restrict to [0,pi] + ang = 2*pi - ang + return ang + + if isinstance(narg, csc): # asec(x) + acsc(x) = pi/2 + ang = narg.args[0] + if ang.is_comparable: + if minus: + pi/2 + acsc(narg) + return pi/2 - acsc(narg) + + def fdiff(self, argindex=1): + if argindex == 1: + return 1/(self.args[0]**2*sqrt(1 - 1/self.args[0]**2)) + else: + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return sec + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n == 0: + return S.ImaginaryUnit*log(2 / x) + elif n < 0 or n % 2 == 1: + return S.Zero + else: + x = sympify(x) + if len(previous_terms) > 2 and n > 2: + p = previous_terms[-2] + return p * ((n - 1)*(n-2)) * x**2/(4 * (n//2)**2) + else: + k = n // 2 + R = RisingFactorial(S.Half, k) * n + F = factorial(k) * n // 2 * n // 2 + return -S.ImaginaryUnit * R / F * x**n / 4 + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + if x0 is S.NaN: + return self.func(arg.as_leading_term(x)) + # Handling branch points + if x0 == 1: + return sqrt(2)*sqrt((arg - S.One).as_leading_term(x)) + if x0 in (-S.One, S.Zero): + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir) + # Handling points lying on branch cuts (-1, 1) + if x0.is_real and (1 - x0**2).is_positive: + ndir = arg.dir(x, cdir if cdir else 1) + if im(ndir).is_negative: + if x0.is_positive: + return -self.func(x0) + elif im(ndir).is_positive: + if x0.is_negative: + return 2*pi - self.func(x0) + else: + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir).expand() + return self.func(x0) + + def _eval_nseries(self, x, n, logx, cdir=0): # asec + from sympy.series.order import O + arg0 = self.args[0].subs(x, 0) + # Handling branch points + if arg0 is S.One: + t = Dummy('t', positive=True) + ser = asec(S.One + t**2).rewrite(log).nseries(t, 0, 2*n) + arg1 = S.NegativeOne + self.args[0] + f = arg1.as_leading_term(x) + g = (arg1 - f)/ f + res1 = sqrt(S.One + g)._eval_nseries(x, n=n, logx=logx) + res = (res1.removeO()*sqrt(f)).expand() + return ser.removeO().subs(t, res).expand().powsimp() + O(x**n, x) + + if arg0 is S.NegativeOne: + t = Dummy('t', positive=True) + ser = asec(S.NegativeOne - t**2).rewrite(log).nseries(t, 0, 2*n) + arg1 = S.NegativeOne - self.args[0] + f = arg1.as_leading_term(x) + g = (arg1 - f)/ f + res1 = sqrt(S.One + g)._eval_nseries(x, n=n, logx=logx) + res = (res1.removeO()*sqrt(f)).expand() + return ser.removeO().subs(t, res).expand().powsimp() + O(x**n, x) + + res = super()._eval_nseries(x, n=n, logx=logx) + if arg0 is S.ComplexInfinity: + return res + # Handling points lying on branch cuts (-1, 1) + if arg0.is_real and (1 - arg0**2).is_positive: + ndir = self.args[0].dir(x, cdir if cdir else 1) + if im(ndir).is_negative: + if arg0.is_positive: + return -res + elif im(ndir).is_positive: + if arg0.is_negative: + return 2*pi - res + else: + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + return res + + def _eval_is_extended_real(self): + x = self.args[0] + if x.is_extended_real is False: + return False + return fuzzy_or(((x - 1).is_nonnegative, (-x - 1).is_nonnegative)) + + def _eval_rewrite_as_log(self, arg, **kwargs): + return pi/2 + S.ImaginaryUnit*log(S.ImaginaryUnit/arg + sqrt(1 - 1/arg**2)) + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + def _eval_rewrite_as_asin(self, arg, **kwargs): + return pi/2 - asin(1/arg) + + def _eval_rewrite_as_acos(self, arg, **kwargs): + return acos(1/arg) + + def _eval_rewrite_as_atan(self, x, **kwargs): + sx2x = sqrt(x**2)/x + return pi/2*(1 - sx2x) + sx2x*atan(sqrt(x**2 - 1)) + + def _eval_rewrite_as_acot(self, x, **kwargs): + sx2x = sqrt(x**2)/x + return pi/2*(1 - sx2x) + sx2x*acot(1/sqrt(x**2 - 1)) + + def _eval_rewrite_as_acsc(self, arg, **kwargs): + return pi/2 - acsc(arg) + + +class acsc(InverseTrigonometricFunction): + r""" + The inverse cosecant function. + + Returns the arc cosecant of x (measured in radians). + + Explanation + =========== + + ``acsc(x)`` will evaluate automatically in the cases + $x \in \{\infty, -\infty, 0, 1, -1\}$` and for some instances when the + result is a rational multiple of $\pi$ (see the ``eval`` class method). + + Examples + ======== + + >>> from sympy import acsc, oo + >>> acsc(1) + pi/2 + >>> acsc(-1) + -pi/2 + >>> acsc(oo) + 0 + >>> acsc(-oo) == acsc(oo) + True + >>> acsc(0) + zoo + + See Also + ======== + + sin, csc, cos, sec, tan, cot + asin, acos, asec, atan, acot, atan2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Inverse_trigonometric_functions + .. [2] https://dlmf.nist.gov/4.23 + .. [3] https://functions.wolfram.com/ElementaryFunctions/ArcCsc + + """ + + @classmethod + def eval(cls, arg): + if arg.is_zero: + return S.ComplexInfinity + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.One: + return pi/2 + elif arg is S.NegativeOne: + return -pi/2 + if arg in [S.Infinity, S.NegativeInfinity, S.ComplexInfinity]: + return S.Zero + + if arg.could_extract_minus_sign(): + return -cls(-arg) + + if arg.is_infinite: + return S.Zero + + if arg.is_number: + acsc_table = cls._acsc_table() + if arg in acsc_table: + return acsc_table[arg] + + if isinstance(arg, csc): + ang = arg.args[0] + if ang.is_comparable: + ang %= 2*pi # restrict to [0,2*pi) + if ang > pi: # restrict to (-pi,pi] + ang = pi - ang + + # restrict to [-pi/2,pi/2] + if ang > pi/2: + ang = pi - ang + if ang < -pi/2: + ang = -pi - ang + + return ang + + if isinstance(arg, sec): # asec(x) + acsc(x) = pi/2 + ang = arg.args[0] + if ang.is_comparable: + return pi/2 - asec(arg) + + def fdiff(self, argindex=1): + if argindex == 1: + return -1/(self.args[0]**2*sqrt(1 - 1/self.args[0]**2)) + else: + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + """ + return csc + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n == 0: + return pi/2 - S.ImaginaryUnit*log(2) + S.ImaginaryUnit*log(x) + elif n < 0 or n % 2 == 1: + return S.Zero + else: + x = sympify(x) + if len(previous_terms) > 2 and n > 2: + p = previous_terms[-2] + return p * ((n - 1)*(n-2)) * x**2/(4 * (n//2)**2) + else: + k = n // 2 + R = RisingFactorial(S.Half, k) * n + F = factorial(k) * n // 2 * n // 2 + return S.ImaginaryUnit * R / F * x**n / 4 + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0] + x0 = arg.subs(x, 0).cancel() + if x0 is S.NaN: + return self.func(arg.as_leading_term(x)) + # Handling branch points + if x0 in (-S.One, S.One, S.Zero): + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir).expand() + if x0 is S.ComplexInfinity: + return (1/arg).as_leading_term(x) + # Handling points lying on branch cuts (-1, 1) + if x0.is_real and (1 - x0**2).is_positive: + ndir = arg.dir(x, cdir if cdir else 1) + if im(ndir).is_negative: + if x0.is_positive: + return pi - self.func(x0) + elif im(ndir).is_positive: + if x0.is_negative: + return -pi - self.func(x0) + else: + return self.rewrite(log)._eval_as_leading_term(x, logx=logx, cdir=cdir).expand() + return self.func(x0) + + def _eval_nseries(self, x, n, logx, cdir=0): # acsc + from sympy.series.order import O + arg0 = self.args[0].subs(x, 0) + # Handling branch points + if arg0 is S.One: + t = Dummy('t', positive=True) + ser = acsc(S.One + t**2).rewrite(log).nseries(t, 0, 2*n) + arg1 = S.NegativeOne + self.args[0] + f = arg1.as_leading_term(x) + g = (arg1 - f)/ f + res1 = sqrt(S.One + g)._eval_nseries(x, n=n, logx=logx) + res = (res1.removeO()*sqrt(f)).expand() + return ser.removeO().subs(t, res).expand().powsimp() + O(x**n, x) + + if arg0 is S.NegativeOne: + t = Dummy('t', positive=True) + ser = acsc(S.NegativeOne - t**2).rewrite(log).nseries(t, 0, 2*n) + arg1 = S.NegativeOne - self.args[0] + f = arg1.as_leading_term(x) + g = (arg1 - f)/ f + res1 = sqrt(S.One + g)._eval_nseries(x, n=n, logx=logx) + res = (res1.removeO()*sqrt(f)).expand() + return ser.removeO().subs(t, res).expand().powsimp() + O(x**n, x) + + res = super()._eval_nseries(x, n=n, logx=logx) + if arg0 is S.ComplexInfinity: + return res + # Handling points lying on branch cuts (-1, 1) + if arg0.is_real and (1 - arg0**2).is_positive: + ndir = self.args[0].dir(x, cdir if cdir else 1) + if im(ndir).is_negative: + if arg0.is_positive: + return pi - res + elif im(ndir).is_positive: + if arg0.is_negative: + return -pi - res + else: + return self.rewrite(log)._eval_nseries(x, n, logx=logx, cdir=cdir) + return res + + def _eval_rewrite_as_log(self, arg, **kwargs): + return -S.ImaginaryUnit*log(S.ImaginaryUnit/arg + sqrt(1 - 1/arg**2)) + + _eval_rewrite_as_tractable = _eval_rewrite_as_log + + def _eval_rewrite_as_asin(self, arg, **kwargs): + return asin(1/arg) + + def _eval_rewrite_as_acos(self, arg, **kwargs): + return pi/2 - acos(1/arg) + + def _eval_rewrite_as_atan(self, x, **kwargs): + return sqrt(x**2)/x*(pi/2 - atan(sqrt(x**2 - 1))) + + def _eval_rewrite_as_acot(self, arg, **kwargs): + return sqrt(arg**2)/arg*(pi/2 - acot(1/sqrt(arg**2 - 1))) + + def _eval_rewrite_as_asec(self, arg, **kwargs): + return pi/2 - asec(arg) + + +class atan2(InverseTrigonometricFunction): + r""" + The function ``atan2(y, x)`` computes `\operatorname{atan}(y/x)` taking + two arguments `y` and `x`. Signs of both `y` and `x` are considered to + determine the appropriate quadrant of `\operatorname{atan}(y/x)`. + The range is `(-\pi, \pi]`. The complete definition reads as follows: + + .. math:: + + \operatorname{atan2}(y, x) = + \begin{cases} + \arctan\left(\frac y x\right) & \qquad x > 0 \\ + \arctan\left(\frac y x\right) + \pi& \qquad y \ge 0, x < 0 \\ + \arctan\left(\frac y x\right) - \pi& \qquad y < 0, x < 0 \\ + +\frac{\pi}{2} & \qquad y > 0, x = 0 \\ + -\frac{\pi}{2} & \qquad y < 0, x = 0 \\ + \text{undefined} & \qquad y = 0, x = 0 + \end{cases} + + Attention: Note the role reversal of both arguments. The `y`-coordinate + is the first argument and the `x`-coordinate the second. + + If either `x` or `y` is complex: + + .. math:: + + \operatorname{atan2}(y, x) = + -i\log\left(\frac{x + iy}{\sqrt{x^2 + y^2}}\right) + + Examples + ======== + + Going counter-clock wise around the origin we find the + following angles: + + >>> from sympy import atan2 + >>> atan2(0, 1) + 0 + >>> atan2(1, 1) + pi/4 + >>> atan2(1, 0) + pi/2 + >>> atan2(1, -1) + 3*pi/4 + >>> atan2(0, -1) + pi + >>> atan2(-1, -1) + -3*pi/4 + >>> atan2(-1, 0) + -pi/2 + >>> atan2(-1, 1) + -pi/4 + + which are all correct. Compare this to the results of the ordinary + `\operatorname{atan}` function for the point `(x, y) = (-1, 1)` + + >>> from sympy import atan, S + >>> atan(S(1)/-1) + -pi/4 + >>> atan2(1, -1) + 3*pi/4 + + where only the `\operatorname{atan2}` function returns what we expect. + We can differentiate the function with respect to both arguments: + + >>> from sympy import diff + >>> from sympy.abc import x, y + >>> diff(atan2(y, x), x) + -y/(x**2 + y**2) + + >>> diff(atan2(y, x), y) + x/(x**2 + y**2) + + We can express the `\operatorname{atan2}` function in terms of + complex logarithms: + + >>> from sympy import log + >>> atan2(y, x).rewrite(log) + -I*log((x + I*y)/sqrt(x**2 + y**2)) + + and in terms of `\operatorname(atan)`: + + >>> from sympy import atan + >>> atan2(y, x).rewrite(atan) + Piecewise((2*atan(y/(x + sqrt(x**2 + y**2))), Ne(y, 0)), (pi, re(x) < 0), (0, Ne(x, 0)), (nan, True)) + + but note that this form is undefined on the negative real axis. + + See Also + ======== + + sin, csc, cos, sec, tan, cot + asin, acsc, acos, asec, atan, acot + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Inverse_trigonometric_functions + .. [2] https://en.wikipedia.org/wiki/Atan2 + .. [3] https://functions.wolfram.com/ElementaryFunctions/ArcTan2 + + """ + + @classmethod + def eval(cls, y, x): + from sympy.functions.special.delta_functions import Heaviside + if x is S.NegativeInfinity: + if y.is_zero: + # Special case y = 0 because we define Heaviside(0) = 1/2 + return pi + return 2*pi*(Heaviside(re(y))) - pi + elif x is S.Infinity: + return S.Zero + elif x.is_imaginary and y.is_imaginary and x.is_number and y.is_number: + x = im(x) + y = im(y) + + if x.is_extended_real and y.is_extended_real: + if x.is_positive: + return atan(y/x) + elif x.is_negative: + if y.is_negative: + return atan(y/x) - pi + elif y.is_nonnegative: + return atan(y/x) + pi + elif x.is_zero: + if y.is_positive: + return pi/2 + elif y.is_negative: + return -pi/2 + elif y.is_zero: + return S.NaN + if y.is_zero: + if x.is_extended_nonzero: + return pi*(S.One - Heaviside(x)) + if x.is_number: + return Piecewise((pi, re(x) < 0), + (0, Ne(x, 0)), + (S.NaN, True)) + if x.is_number and y.is_number: + return -S.ImaginaryUnit*log( + (x + S.ImaginaryUnit*y)/sqrt(x**2 + y**2)) + + def _eval_rewrite_as_log(self, y, x, **kwargs): + return -S.ImaginaryUnit*log((x + S.ImaginaryUnit*y)/sqrt(x**2 + y**2)) + + def _eval_rewrite_as_atan(self, y, x, **kwargs): + return Piecewise((2*atan(y/(x + sqrt(x**2 + y**2))), Ne(y, 0)), + (pi, re(x) < 0), + (0, Ne(x, 0)), + (S.NaN, True)) + + def _eval_rewrite_as_arg(self, y, x, **kwargs): + if x.is_extended_real and y.is_extended_real: + return arg_f(x + y*S.ImaginaryUnit) + n = x + S.ImaginaryUnit*y + d = x**2 + y**2 + return arg_f(n/sqrt(d)) - S.ImaginaryUnit*log(abs(n)/sqrt(abs(d))) + + def _eval_is_extended_real(self): + return self.args[0].is_extended_real and self.args[1].is_extended_real + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate(), self.args[1].conjugate()) + + def fdiff(self, argindex): + y, x = self.args + if argindex == 1: + # Diff wrt y + return x/(x**2 + y**2) + elif argindex == 2: + # Diff wrt x + return -y/(x**2 + y**2) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_evalf(self, prec): + y, x = self.args + if x.is_extended_real and y.is_extended_real: + return super()._eval_evalf(prec) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/__init__.py b/venv/lib/python3.11/site-packages/sympy/functions/special/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ab52ace36a8dfbe73179dbf4419a54f7fa1af5fa --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/__init__.py @@ -0,0 +1 @@ +# Stub __init__.py for the sympy.functions.special package diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/benchmarks/__init__.py b/venv/lib/python3.11/site-packages/sympy/functions/special/benchmarks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/benchmarks/bench_special.py b/venv/lib/python3.11/site-packages/sympy/functions/special/benchmarks/bench_special.py new file mode 100644 index 0000000000000000000000000000000000000000..25d7280c2cf31dcbff08065a78847ed03e0ebb05 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/benchmarks/bench_special.py @@ -0,0 +1,8 @@ +from sympy.core.symbol import symbols +from sympy.functions.special.spherical_harmonics import Ynm + +x, y = symbols('x,y') + + +def timeit_Ynm_xy(): + Ynm(1, 1, x, y) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/bessel.py b/venv/lib/python3.11/site-packages/sympy/functions/special/bessel.py new file mode 100644 index 0000000000000000000000000000000000000000..a24e7dc442d2a5a9bf7047113fd81b36c6b6ba36 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/bessel.py @@ -0,0 +1,2208 @@ +from functools import wraps + +from sympy.core import S +from sympy.core.add import Add +from sympy.core.cache import cacheit +from sympy.core.expr import Expr +from sympy.core.function import DefinedFunction, ArgumentIndexError, _mexpand +from sympy.core.logic import fuzzy_or, fuzzy_not +from sympy.core.numbers import Rational, pi, I +from sympy.core.power import Pow +from sympy.core.symbol import Dummy, uniquely_named_symbol, Wild +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import factorial, RisingFactorial +from sympy.functions.elementary.trigonometric import sin, cos, csc, cot +from sympy.functions.elementary.integers import ceiling +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.miscellaneous import cbrt, sqrt, root +from sympy.functions.elementary.complexes import (Abs, re, im, polar_lift, unpolarify) +from sympy.functions.special.gamma_functions import gamma, digamma, uppergamma +from sympy.functions.special.hyper import hyper +from sympy.polys.orthopolys import spherical_bessel_fn + +from mpmath import mp, workprec + +# TODO +# o Scorer functions G1 and G2 +# o Asymptotic expansions +# These are possible, e.g. for fixed order, but since the bessel type +# functions are oscillatory they are not actually tractable at +# infinity, so this is not particularly useful right now. +# o Nicer series expansions. +# o More rewriting. +# o Add solvers to ode.py (or rather add solvers for the hypergeometric equation). + + +class BesselBase(DefinedFunction): + """ + Abstract base class for Bessel-type functions. + + This class is meant to reduce code duplication. + All Bessel-type functions can 1) be differentiated, with the derivatives + expressed in terms of similar functions, and 2) be rewritten in terms + of other Bessel-type functions. + + Here, Bessel-type functions are assumed to have one complex parameter. + + To use this base class, define class attributes ``_a`` and ``_b`` such that + ``2*F_n' = -_a*F_{n+1} + b*F_{n-1}``. + + """ + + @property + def order(self): + """ The order of the Bessel-type function. """ + return self.args[0] + + @property + def argument(self): + """ The argument of the Bessel-type function. """ + return self.args[1] + + @classmethod + def eval(cls, nu, z): + return + + def fdiff(self, argindex=2): + if argindex != 2: + raise ArgumentIndexError(self, argindex) + return (self._b/2 * self.__class__(self.order - 1, self.argument) - + self._a/2 * self.__class__(self.order + 1, self.argument)) + + def _eval_conjugate(self): + z = self.argument + if z.is_extended_negative is False: + return self.__class__(self.order.conjugate(), z.conjugate()) + + def _eval_is_meromorphic(self, x, a): + nu, z = self.order, self.argument + + if nu.has(x): + return False + if not z._eval_is_meromorphic(x, a): + return None + z0 = z.subs(x, a) + if nu.is_integer: + if isinstance(self, (besselj, besseli, hn1, hn2, jn, yn)) or not nu.is_zero: + return fuzzy_not(z0.is_infinite) + return fuzzy_not(fuzzy_or([z0.is_zero, z0.is_infinite])) + + def _eval_expand_func(self, **hints): + nu, z, f = self.order, self.argument, self.__class__ + if nu.is_real: + if (nu - 1).is_positive: + return (-self._a*self._b*f(nu - 2, z)._eval_expand_func() + + 2*self._a*(nu - 1)*f(nu - 1, z)._eval_expand_func()/z) + elif (nu + 1).is_negative: + return (2*self._b*(nu + 1)*f(nu + 1, z)._eval_expand_func()/z - + self._a*self._b*f(nu + 2, z)._eval_expand_func()) + return self + + def _eval_simplify(self, **kwargs): + from sympy.simplify.simplify import besselsimp + return besselsimp(self) + + +class besselj(BesselBase): + r""" + Bessel function of the first kind. + + Explanation + =========== + + The Bessel $J$ function of order $\nu$ is defined to be the function + satisfying Bessel's differential equation + + .. math :: + z^2 \frac{\mathrm{d}^2 w}{\mathrm{d}z^2} + + z \frac{\mathrm{d}w}{\mathrm{d}z} + (z^2 - \nu^2) w = 0, + + with Laurent expansion + + .. math :: + J_\nu(z) = z^\nu \left(\frac{1}{\Gamma(\nu + 1) 2^\nu} + O(z^2) \right), + + if $\nu$ is not a negative integer. If $\nu=-n \in \mathbb{Z}_{<0}$ + *is* a negative integer, then the definition is + + .. math :: + J_{-n}(z) = (-1)^n J_n(z). + + Examples + ======== + + Create a Bessel function object: + + >>> from sympy import besselj, jn + >>> from sympy.abc import z, n + >>> b = besselj(n, z) + + Differentiate it: + + >>> b.diff(z) + besselj(n - 1, z)/2 - besselj(n + 1, z)/2 + + Rewrite in terms of spherical Bessel functions: + + >>> b.rewrite(jn) + sqrt(2)*sqrt(z)*jn(n - 1/2, z)/sqrt(pi) + + Access the parameter and argument: + + >>> b.order + n + >>> b.argument + z + + See Also + ======== + + bessely, besseli, besselk + + References + ========== + + .. [1] Abramowitz, Milton; Stegun, Irene A., eds. (1965), "Chapter 9", + Handbook of Mathematical Functions with Formulas, Graphs, and + Mathematical Tables + .. [2] Luke, Y. L. (1969), The Special Functions and Their + Approximations, Volume 1 + .. [3] https://en.wikipedia.org/wiki/Bessel_function + .. [4] https://functions.wolfram.com/Bessel-TypeFunctions/BesselJ/ + + """ + + _a = S.One + _b = S.One + + @classmethod + def eval(cls, nu, z): + if z.is_zero: + if nu.is_zero: + return S.One + elif (nu.is_integer and nu.is_zero is False) or re(nu).is_positive: + return S.Zero + elif re(nu).is_negative and not (nu.is_integer is True): + return S.ComplexInfinity + elif nu.is_imaginary: + return S.NaN + if z in (S.Infinity, S.NegativeInfinity): + return S.Zero + + if z.could_extract_minus_sign(): + return (z)**nu*(-z)**(-nu)*besselj(nu, -z) + if nu.is_integer: + if nu.could_extract_minus_sign(): + return S.NegativeOne**(-nu)*besselj(-nu, z) + newz = z.extract_multiplicatively(I) + if newz: # NOTE we don't want to change the function if z==0 + return I**(nu)*besseli(nu, newz) + + # branch handling: + if nu.is_integer: + newz = unpolarify(z) + if newz != z: + return besselj(nu, newz) + else: + newz, n = z.extract_branch_factor() + if n != 0: + return exp(2*n*pi*nu*I)*besselj(nu, newz) + nnu = unpolarify(nu) + if nu != nnu: + return besselj(nnu, z) + + def _eval_rewrite_as_besseli(self, nu, z, **kwargs): + return exp(I*pi*nu/2)*besseli(nu, polar_lift(-I)*z) + + def _eval_rewrite_as_bessely(self, nu, z, **kwargs): + if nu.is_integer is False: + return csc(pi*nu)*bessely(-nu, z) - cot(pi*nu)*bessely(nu, z) + + def _eval_rewrite_as_jn(self, nu, z, **kwargs): + return sqrt(2*z/pi)*jn(nu - S.Half, self.argument) + + def _eval_as_leading_term(self, x, logx, cdir): + nu, z = self.args + try: + arg = z.as_leading_term(x) + except NotImplementedError: + return self + c, e = arg.as_coeff_exponent(x) + + if e.is_positive: + return arg**nu/(2**nu*gamma(nu + 1)) + elif e.is_negative: + cdir = 1 if cdir == 0 else cdir + sign = c*cdir**e + if not sign.is_negative: + # Refer Abramowitz and Stegun 1965, p. 364 for more information on + # asymptotic approximation of besselj function. + return sqrt(2)*cos(z - pi*(2*nu + 1)/4)/sqrt(pi*z) + return self + + return super(besselj, self)._eval_as_leading_term(x, logx=logx, cdir=cdir) + + def _eval_is_extended_real(self): + nu, z = self.args + if nu.is_integer and z.is_extended_real: + return True + + def _eval_nseries(self, x, n, logx, cdir=0): + # Refer https://functions.wolfram.com/Bessel-TypeFunctions/BesselJ/06/01/04/01/01/0003/ + # for more information on nseries expansion of besselj function. + from sympy.series.order import Order + nu, z = self.args + + # In case of powers less than 1, number of terms need to be computed + # separately to avoid repeated callings of _eval_nseries with wrong n + try: + _, exp = z.leadterm(x) + except (ValueError, NotImplementedError): + return self + + if exp.is_positive: + newn = ceiling(n/exp) + o = Order(x**n, x) + r = (z/2)._eval_nseries(x, n, logx, cdir).removeO() + if r is S.Zero: + return o + t = (_mexpand(r**2) + o).removeO() + + term = r**nu/gamma(nu + 1) + s = [term] + for k in range(1, (newn + 1)//2): + term *= -t/(k*(nu + k)) + term = (_mexpand(term) + o).removeO() + s.append(term) + return Add(*s) + o + + return super(besselj, self)._eval_nseries(x, n, logx, cdir) + + +class bessely(BesselBase): + r""" + Bessel function of the second kind. + + Explanation + =========== + + The Bessel $Y$ function of order $\nu$ is defined as + + .. math :: + Y_\nu(z) = \lim_{\mu \to \nu} \frac{J_\mu(z) \cos(\pi \mu) + - J_{-\mu}(z)}{\sin(\pi \mu)}, + + where $J_\mu(z)$ is the Bessel function of the first kind. + + It is a solution to Bessel's equation, and linearly independent from + $J_\nu$. + + Examples + ======== + + >>> from sympy import bessely, yn + >>> from sympy.abc import z, n + >>> b = bessely(n, z) + >>> b.diff(z) + bessely(n - 1, z)/2 - bessely(n + 1, z)/2 + >>> b.rewrite(yn) + sqrt(2)*sqrt(z)*yn(n - 1/2, z)/sqrt(pi) + + See Also + ======== + + besselj, besseli, besselk + + References + ========== + + .. [1] https://functions.wolfram.com/Bessel-TypeFunctions/BesselY/ + + """ + + _a = S.One + _b = S.One + + @classmethod + def eval(cls, nu, z): + if z.is_zero: + if nu.is_zero: + return S.NegativeInfinity + elif re(nu).is_zero is False: + return S.ComplexInfinity + elif re(nu).is_zero: + return S.NaN + if z in (S.Infinity, S.NegativeInfinity): + return S.Zero + if z == I*S.Infinity: + return exp(I*pi*(nu + 1)/2) * S.Infinity + if z == I*S.NegativeInfinity: + return exp(-I*pi*(nu + 1)/2) * S.Infinity + + if nu.is_integer: + if nu.could_extract_minus_sign(): + return S.NegativeOne**(-nu)*bessely(-nu, z) + + def _eval_rewrite_as_besselj(self, nu, z, **kwargs): + if nu.is_integer is False: + return csc(pi*nu)*(cos(pi*nu)*besselj(nu, z) - besselj(-nu, z)) + + def _eval_rewrite_as_besseli(self, nu, z, **kwargs): + aj = self._eval_rewrite_as_besselj(*self.args) + if aj: + return aj.rewrite(besseli) + + def _eval_rewrite_as_yn(self, nu, z, **kwargs): + return sqrt(2*z/pi) * yn(nu - S.Half, self.argument) + + def _eval_as_leading_term(self, x, logx, cdir): + nu, z = self.args + try: + arg = z.as_leading_term(x) + except NotImplementedError: + return self + c, e = arg.as_coeff_exponent(x) + + if e.is_positive: + term_one = ((2/pi)*log(z/2)*besselj(nu, z)) + term_two = -(z/2)**(-nu)*factorial(nu - 1)/pi if (nu).is_positive else S.Zero + term_three = -(z/2)**nu/(pi*factorial(nu))*(digamma(nu + 1) - S.EulerGamma) + arg = Add(*[term_one, term_two, term_three]).as_leading_term(x, logx=logx) + return arg + elif e.is_negative: + cdir = 1 if cdir == 0 else cdir + sign = c*cdir**e + if not sign.is_negative: + # Refer Abramowitz and Stegun 1965, p. 364 for more information on + # asymptotic approximation of bessely function. + return sqrt(2)*(-sin(pi*nu/2 - z + pi/4) + 3*cos(pi*nu/2 - z + pi/4)/(8*z))*sqrt(1/z)/sqrt(pi) + return self + + return super(bessely, self)._eval_as_leading_term(x, logx=logx, cdir=cdir) + + def _eval_is_extended_real(self): + nu, z = self.args + if nu.is_integer and z.is_positive: + return True + + def _eval_nseries(self, x, n, logx, cdir=0): + # Refer https://functions.wolfram.com/Bessel-TypeFunctions/BesselY/06/01/04/01/02/0008/ + # for more information on nseries expansion of bessely function. + from sympy.series.order import Order + nu, z = self.args + + # In case of powers less than 1, number of terms need to be computed + # separately to avoid repeated callings of _eval_nseries with wrong n + try: + _, exp = z.leadterm(x) + except (ValueError, NotImplementedError): + return self + + if exp.is_positive and nu.is_integer: + newn = ceiling(n/exp) + bn = besselj(nu, z) + a = ((2/pi)*log(z/2)*bn)._eval_nseries(x, n, logx, cdir) + + b, c = [], [] + o = Order(x**n, x) + r = (z/2)._eval_nseries(x, n, logx, cdir).removeO() + if r is S.Zero: + return o + t = (_mexpand(r**2) + o).removeO() + + if nu > S.Zero: + term = r**(-nu)*factorial(nu - 1)/pi + b.append(term) + for k in range(1, nu): + denom = (nu - k)*k + if denom == S.Zero: + term *= t/k + else: + term *= t/denom + term = (_mexpand(term) + o).removeO() + b.append(term) + + p = r**nu/(pi*factorial(nu)) + term = p*(digamma(nu + 1) - S.EulerGamma) + c.append(term) + for k in range(1, (newn + 1)//2): + p *= -t/(k*(k + nu)) + p = (_mexpand(p) + o).removeO() + term = p*(digamma(k + nu + 1) + digamma(k + 1)) + c.append(term) + return a - Add(*b) - Add(*c) # Order term comes from a + + return super(bessely, self)._eval_nseries(x, n, logx, cdir) + + +class besseli(BesselBase): + r""" + Modified Bessel function of the first kind. + + Explanation + =========== + + The Bessel $I$ function is a solution to the modified Bessel equation + + .. math :: + z^2 \frac{\mathrm{d}^2 w}{\mathrm{d}z^2} + + z \frac{\mathrm{d}w}{\mathrm{d}z} + (z^2 + \nu^2)^2 w = 0. + + It can be defined as + + .. math :: + I_\nu(z) = i^{-\nu} J_\nu(iz), + + where $J_\nu(z)$ is the Bessel function of the first kind. + + Examples + ======== + + >>> from sympy import besseli + >>> from sympy.abc import z, n + >>> besseli(n, z).diff(z) + besseli(n - 1, z)/2 + besseli(n + 1, z)/2 + + See Also + ======== + + besselj, bessely, besselk + + References + ========== + + .. [1] https://functions.wolfram.com/Bessel-TypeFunctions/BesselI/ + + """ + + _a = -S.One + _b = S.One + + @classmethod + def eval(cls, nu, z): + if z.is_zero: + if nu.is_zero: + return S.One + elif (nu.is_integer and nu.is_zero is False) or re(nu).is_positive: + return S.Zero + elif re(nu).is_negative and not (nu.is_integer is True): + return S.ComplexInfinity + elif nu.is_imaginary: + return S.NaN + if im(z) in (S.Infinity, S.NegativeInfinity): + return S.Zero + if z is S.Infinity: + return S.Infinity + if z is S.NegativeInfinity: + return (-1)**nu*S.Infinity + + if z.could_extract_minus_sign(): + return (z)**nu*(-z)**(-nu)*besseli(nu, -z) + if nu.is_integer: + if nu.could_extract_minus_sign(): + return besseli(-nu, z) + newz = z.extract_multiplicatively(I) + if newz: # NOTE we don't want to change the function if z==0 + return I**(-nu)*besselj(nu, -newz) + + # branch handling: + if nu.is_integer: + newz = unpolarify(z) + if newz != z: + return besseli(nu, newz) + else: + newz, n = z.extract_branch_factor() + if n != 0: + return exp(2*n*pi*nu*I)*besseli(nu, newz) + nnu = unpolarify(nu) + if nu != nnu: + return besseli(nnu, z) + + def _eval_rewrite_as_tractable(self, nu, z, limitvar=None, **kwargs): + if z.is_extended_real: + return exp(z)*_besseli(nu, z) + + def _eval_rewrite_as_besselj(self, nu, z, **kwargs): + return exp(-I*pi*nu/2)*besselj(nu, polar_lift(I)*z) + + def _eval_rewrite_as_bessely(self, nu, z, **kwargs): + aj = self._eval_rewrite_as_besselj(*self.args) + if aj: + return aj.rewrite(bessely) + + def _eval_rewrite_as_jn(self, nu, z, **kwargs): + return self._eval_rewrite_as_besselj(*self.args).rewrite(jn) + + def _eval_is_extended_real(self): + nu, z = self.args + if nu.is_integer and z.is_extended_real: + return True + + def _eval_as_leading_term(self, x, logx, cdir): + nu, z = self.args + try: + arg = z.as_leading_term(x) + except NotImplementedError: + return self + c, e = arg.as_coeff_exponent(x) + + if e.is_positive: + return arg**nu/(2**nu*gamma(nu + 1)) + elif e.is_negative: + cdir = 1 if cdir == 0 else cdir + sign = c*cdir**e + if not sign.is_negative: + # Refer Abramowitz and Stegun 1965, p. 377 for more information on + # asymptotic approximation of besseli function. + return exp(z)/sqrt(2*pi*z) + return self + + return super(besseli, self)._eval_as_leading_term(x, logx=logx, cdir=cdir) + + def _eval_nseries(self, x, n, logx, cdir=0): + # Refer https://functions.wolfram.com/Bessel-TypeFunctions/BesselI/06/01/04/01/01/0003/ + # for more information on nseries expansion of besseli function. + from sympy.series.order import Order + nu, z = self.args + + # In case of powers less than 1, number of terms need to be computed + # separately to avoid repeated callings of _eval_nseries with wrong n + try: + _, exp = z.leadterm(x) + except (ValueError, NotImplementedError): + return self + + if exp.is_positive: + newn = ceiling(n/exp) + o = Order(x**n, x) + r = (z/2)._eval_nseries(x, n, logx, cdir).removeO() + if r is S.Zero: + return o + t = (_mexpand(r**2) + o).removeO() + + term = r**nu/gamma(nu + 1) + s = [term] + for k in range(1, (newn + 1)//2): + term *= t/(k*(nu + k)) + term = (_mexpand(term) + o).removeO() + s.append(term) + return Add(*s) + o + + return super(besseli, self)._eval_nseries(x, n, logx, cdir) + + def _eval_aseries(self, n, args0, x, logx): + from sympy.functions.combinatorial.factorials import RisingFactorial + from sympy.series.order import Order + point = args0[1] + + if point in [S.Infinity, S.NegativeInfinity]: + nu, z = self.args + s = [(RisingFactorial(Rational(2*nu - 1, 2), k)*RisingFactorial(Rational(2*nu + 1, 2), k))/\ + ((2)**(k)*z**(Rational(2*k + 1, 2))*factorial(k)) for k in range(n)] + [Order(1/z**(Rational(2*n + 1, 2)), x)] + return exp(z)/sqrt(2*pi) * (Add(*s)) + + return super()._eval_aseries(n, args0, x, logx) + + +class besselk(BesselBase): + r""" + Modified Bessel function of the second kind. + + Explanation + =========== + + The Bessel $K$ function of order $\nu$ is defined as + + .. math :: + K_\nu(z) = \lim_{\mu \to \nu} \frac{\pi}{2} + \frac{I_{-\mu}(z) -I_\mu(z)}{\sin(\pi \mu)}, + + where $I_\mu(z)$ is the modified Bessel function of the first kind. + + It is a solution of the modified Bessel equation, and linearly independent + from $Y_\nu$. + + Examples + ======== + + >>> from sympy import besselk + >>> from sympy.abc import z, n + >>> besselk(n, z).diff(z) + -besselk(n - 1, z)/2 - besselk(n + 1, z)/2 + + See Also + ======== + + besselj, besseli, bessely + + References + ========== + + .. [1] https://functions.wolfram.com/Bessel-TypeFunctions/BesselK/ + + """ + + _a = S.One + _b = -S.One + + @classmethod + def eval(cls, nu, z): + if z.is_zero: + if nu.is_zero: + return S.Infinity + elif re(nu).is_zero is False: + return S.ComplexInfinity + elif re(nu).is_zero: + return S.NaN + if z in (S.Infinity, I*S.Infinity, I*S.NegativeInfinity): + return S.Zero + + if nu.is_integer: + if nu.could_extract_minus_sign(): + return besselk(-nu, z) + + def _eval_rewrite_as_besseli(self, nu, z, **kwargs): + if nu.is_integer is False: + return pi*csc(pi*nu)*(besseli(-nu, z) - besseli(nu, z))/2 + + def _eval_rewrite_as_besselj(self, nu, z, **kwargs): + ai = self._eval_rewrite_as_besseli(*self.args) + if ai: + return ai.rewrite(besselj) + + def _eval_rewrite_as_bessely(self, nu, z, **kwargs): + aj = self._eval_rewrite_as_besselj(*self.args) + if aj: + return aj.rewrite(bessely) + + def _eval_rewrite_as_yn(self, nu, z, **kwargs): + ay = self._eval_rewrite_as_bessely(*self.args) + if ay: + return ay.rewrite(yn) + + def _eval_is_extended_real(self): + nu, z = self.args + if nu.is_integer and z.is_positive: + return True + + def _eval_rewrite_as_tractable(self, nu, z, limitvar=None, **kwargs): + if z.is_extended_real: + return exp(-z)*_besselk(nu, z) + + def _eval_as_leading_term(self, x, logx, cdir): + nu, z = self.args + try: + arg = z.as_leading_term(x) + except NotImplementedError: + return self + _, e = arg.as_coeff_exponent(x) + + if e.is_positive: + if nu.is_zero: + # Equation 9.6.8 of Abramowitz and Stegun (10th ed, 1972). + term = -log(z) - S.EulerGamma + log(2) + elif nu.is_nonzero: + # Equation 9.6.9 of Abramowitz and Stegun (10th ed, 1972). + term = gamma(Abs(nu))*(z/2)**(-Abs(nu))/2 + else: + raise NotImplementedError(f"Cannot proceed without knowing if {nu} is zero or not.") + + return term.as_leading_term(x, logx=logx) + elif e.is_negative: + # Equation 9.7.2 of Abramowitz and Stegun (10th ed, 1972). + return sqrt(pi)*exp(-arg)/sqrt(2*arg) + else: + return self.func(nu, arg) + + def _eval_nseries(self, x, n, logx, cdir=0): + from sympy.series.order import Order + nu, z = self.args + + try: + _, exp = z.leadterm(x) + except (ValueError, NotImplementedError): + return self + + # In case of powers less than 1, number of terms need to be computed + # separately to avoid repeated callings of _eval_nseries with wrong n + if exp.is_positive: + r = (z/2)._eval_nseries(x, n, logx, cdir).removeO() + if r is S.Zero: + return Order(z**(-nu) + z**nu, x) + + o = Order(x**n, x) + if nu.is_integer: + # Reference: https://functions.wolfram.com/Bessel-TypeFunctions/BesselK/06/01/04/01/02/0008/ (only for integer order) + newn = ceiling(n/exp) + bn = besseli(nu, z) + a = ((-1)**(nu - 1)*log(z/2)*bn)._eval_nseries(x, n, logx, cdir) + + b, c = [], [] + t = _mexpand(r**2) + + if nu > S.Zero: + term = r**(-nu)*factorial(nu - 1)/2 + b.append(term) + for k in range(1, nu): + term *= t/((k - nu)*k) + term = (_mexpand(term) + o).removeO() + b.append(term) + + p = r**nu*(-1)**nu/(2*factorial(nu)) + term = p*(digamma(nu + 1) - S.EulerGamma) + c.append(term) + for k in range(1, (newn + 1)//2): + p *= t/(k*(k + nu)) + p = (_mexpand(p) + o).removeO() + term = p*(digamma(k + nu + 1) + digamma(k + 1)) + c.append(term) + return a + Add(*b) + Add(*c) + o + elif nu.is_noninteger: + # Reference: https://functions.wolfram.com/Bessel-TypeFunctions/BesselK/06/01/04/01/01/0003/ + # (only for non-integer order). + # While the expression in the reference above seems correct + # for non-real order as well, it would need some manipulation + # (not implemented) to be written as a power series in x with + # real exponents [e.g. Dunster 1990. "Bessel functions + # of purely imaginary order, with an application to second-order + # linear differential equations having a large parameter". + # SIAM J. Math. Anal. Vol 21, No. 4, pp 995-1018.]. + newn_a = ceiling((n+nu)/exp) + newn_b = ceiling((n-nu)/exp) + + a, b = [], [] + for k in range((newn_a+1)//2): + term = gamma(nu)*r**(2*k-nu)/(2*RisingFactorial(1-nu, k)*factorial(k)) + a.append(_mexpand(term)) + for k in range((newn_b+1)//2): + term = gamma(-nu)*r**(2*k+nu)/(2*RisingFactorial(nu+1, k)*factorial(k)) + b.append(_mexpand(term)) + return Add(*a) + Add(*b) + o + else: + raise NotImplementedError("besselk expansion is only implemented for real order") + + return super(besselk, self)._eval_nseries(x, n, logx, cdir) + + def _eval_aseries(self, n, args0, x, logx): + from sympy.functions.combinatorial.factorials import RisingFactorial + from sympy.series.order import Order + point = args0[1] + + if point in [S.Infinity, S.NegativeInfinity]: + nu, z = self.args + s = [(RisingFactorial(Rational(2*nu - 1, 2), k)*RisingFactorial(Rational(2*nu + 1, 2), k))/\ + ((-2)**(k)*z**(Rational(2*k + 1, 2))*factorial(k)) for k in range(n)] +[Order(1/z**(Rational(2*n + 1, 2)), x)] + return (exp(-z)*sqrt(pi/2))*Add(*s) + + return super()._eval_aseries(n, args0, x, logx) + + +class hankel1(BesselBase): + r""" + Hankel function of the first kind. + + Explanation + =========== + + This function is defined as + + .. math :: + H_\nu^{(1)} = J_\nu(z) + iY_\nu(z), + + where $J_\nu(z)$ is the Bessel function of the first kind, and + $Y_\nu(z)$ is the Bessel function of the second kind. + + It is a solution to Bessel's equation. + + Examples + ======== + + >>> from sympy import hankel1 + >>> from sympy.abc import z, n + >>> hankel1(n, z).diff(z) + hankel1(n - 1, z)/2 - hankel1(n + 1, z)/2 + + See Also + ======== + + hankel2, besselj, bessely + + References + ========== + + .. [1] https://functions.wolfram.com/Bessel-TypeFunctions/HankelH1/ + + """ + + _a = S.One + _b = S.One + + def _eval_conjugate(self): + z = self.argument + if z.is_extended_negative is False: + return hankel2(self.order.conjugate(), z.conjugate()) + + +class hankel2(BesselBase): + r""" + Hankel function of the second kind. + + Explanation + =========== + + This function is defined as + + .. math :: + H_\nu^{(2)} = J_\nu(z) - iY_\nu(z), + + where $J_\nu(z)$ is the Bessel function of the first kind, and + $Y_\nu(z)$ is the Bessel function of the second kind. + + It is a solution to Bessel's equation, and linearly independent from + $H_\nu^{(1)}$. + + Examples + ======== + + >>> from sympy import hankel2 + >>> from sympy.abc import z, n + >>> hankel2(n, z).diff(z) + hankel2(n - 1, z)/2 - hankel2(n + 1, z)/2 + + See Also + ======== + + hankel1, besselj, bessely + + References + ========== + + .. [1] https://functions.wolfram.com/Bessel-TypeFunctions/HankelH2/ + + """ + + _a = S.One + _b = S.One + + def _eval_conjugate(self): + z = self.argument + if z.is_extended_negative is False: + return hankel1(self.order.conjugate(), z.conjugate()) + + +def assume_integer_order(fn): + @wraps(fn) + def g(self, nu, z): + if nu.is_integer: + return fn(self, nu, z) + return g + + +class SphericalBesselBase(BesselBase): + """ + Base class for spherical Bessel functions. + + These are thin wrappers around ordinary Bessel functions, + since spherical Bessel functions differ from the ordinary + ones just by a slight change in order. + + To use this class, define the ``_eval_evalf()`` and ``_expand()`` methods. + + """ + + def _expand(self, **hints): + """ Expand self into a polynomial. Nu is guaranteed to be Integer. """ + raise NotImplementedError('expansion') + + def _eval_expand_func(self, **hints): + if self.order.is_Integer: + return self._expand(**hints) + return self + + def fdiff(self, argindex=2): + if argindex != 2: + raise ArgumentIndexError(self, argindex) + return self.__class__(self.order - 1, self.argument) - \ + self * (self.order + 1)/self.argument + + +def _jn(n, z): + return (spherical_bessel_fn(n, z)*sin(z) + + S.NegativeOne**(n + 1)*spherical_bessel_fn(-n - 1, z)*cos(z)) + + +def _yn(n, z): + # (-1)**(n + 1) * _jn(-n - 1, z) + return (S.NegativeOne**(n + 1) * spherical_bessel_fn(-n - 1, z)*sin(z) - + spherical_bessel_fn(n, z)*cos(z)) + + +class jn(SphericalBesselBase): + r""" + Spherical Bessel function of the first kind. + + Explanation + =========== + + This function is a solution to the spherical Bessel equation + + .. math :: + z^2 \frac{\mathrm{d}^2 w}{\mathrm{d}z^2} + + 2z \frac{\mathrm{d}w}{\mathrm{d}z} + (z^2 - \nu(\nu + 1)) w = 0. + + It can be defined as + + .. math :: + j_\nu(z) = \sqrt{\frac{\pi}{2z}} J_{\nu + \frac{1}{2}}(z), + + where $J_\nu(z)$ is the Bessel function of the first kind. + + The spherical Bessel functions of integral order are + calculated using the formula: + + .. math:: j_n(z) = f_n(z) \sin{z} + (-1)^{n+1} f_{-n-1}(z) \cos{z}, + + where the coefficients $f_n(z)$ are available as + :func:`sympy.polys.orthopolys.spherical_bessel_fn`. + + Examples + ======== + + >>> from sympy import Symbol, jn, sin, cos, expand_func, besselj, bessely + >>> z = Symbol("z") + >>> nu = Symbol("nu", integer=True) + >>> print(expand_func(jn(0, z))) + sin(z)/z + >>> expand_func(jn(1, z)) == sin(z)/z**2 - cos(z)/z + True + >>> expand_func(jn(3, z)) + (-6/z**2 + 15/z**4)*sin(z) + (1/z - 15/z**3)*cos(z) + >>> jn(nu, z).rewrite(besselj) + sqrt(2)*sqrt(pi)*sqrt(1/z)*besselj(nu + 1/2, z)/2 + >>> jn(nu, z).rewrite(bessely) + (-1)**nu*sqrt(2)*sqrt(pi)*sqrt(1/z)*bessely(-nu - 1/2, z)/2 + >>> jn(2, 5.2+0.3j).evalf(20) + 0.099419756723640344491 - 0.054525080242173562897*I + + See Also + ======== + + besselj, bessely, besselk, yn + + References + ========== + + .. [1] https://dlmf.nist.gov/10.47 + + """ + @classmethod + def eval(cls, nu, z): + if z.is_zero: + if nu.is_zero: + return S.One + elif nu.is_integer: + if nu.is_positive: + return S.Zero + else: + return S.ComplexInfinity + if z in (S.NegativeInfinity, S.Infinity): + return S.Zero + + def _eval_rewrite_as_besselj(self, nu, z, **kwargs): + return sqrt(pi/(2*z)) * besselj(nu + S.Half, z) + + def _eval_rewrite_as_bessely(self, nu, z, **kwargs): + return S.NegativeOne**nu * sqrt(pi/(2*z)) * bessely(-nu - S.Half, z) + + def _eval_rewrite_as_yn(self, nu, z, **kwargs): + return S.NegativeOne**(nu) * yn(-nu - 1, z) + + def _expand(self, **hints): + return _jn(self.order, self.argument) + + def _eval_evalf(self, prec): + if self.order.is_Integer: + return self.rewrite(besselj)._eval_evalf(prec) + + +class yn(SphericalBesselBase): + r""" + Spherical Bessel function of the second kind. + + Explanation + =========== + + This function is another solution to the spherical Bessel equation, and + linearly independent from $j_n$. It can be defined as + + .. math :: + y_\nu(z) = \sqrt{\frac{\pi}{2z}} Y_{\nu + \frac{1}{2}}(z), + + where $Y_\nu(z)$ is the Bessel function of the second kind. + + For integral orders $n$, $y_n$ is calculated using the formula: + + .. math:: y_n(z) = (-1)^{n+1} j_{-n-1}(z) + + Examples + ======== + + >>> from sympy import Symbol, yn, sin, cos, expand_func, besselj, bessely + >>> z = Symbol("z") + >>> nu = Symbol("nu", integer=True) + >>> print(expand_func(yn(0, z))) + -cos(z)/z + >>> expand_func(yn(1, z)) == -cos(z)/z**2-sin(z)/z + True + >>> yn(nu, z).rewrite(besselj) + (-1)**(nu + 1)*sqrt(2)*sqrt(pi)*sqrt(1/z)*besselj(-nu - 1/2, z)/2 + >>> yn(nu, z).rewrite(bessely) + sqrt(2)*sqrt(pi)*sqrt(1/z)*bessely(nu + 1/2, z)/2 + >>> yn(2, 5.2+0.3j).evalf(20) + 0.18525034196069722536 + 0.014895573969924817587*I + + See Also + ======== + + besselj, bessely, besselk, jn + + References + ========== + + .. [1] https://dlmf.nist.gov/10.47 + + """ + @assume_integer_order + def _eval_rewrite_as_besselj(self, nu, z, **kwargs): + return S.NegativeOne**(nu+1) * sqrt(pi/(2*z)) * besselj(-nu - S.Half, z) + + @assume_integer_order + def _eval_rewrite_as_bessely(self, nu, z, **kwargs): + return sqrt(pi/(2*z)) * bessely(nu + S.Half, z) + + def _eval_rewrite_as_jn(self, nu, z, **kwargs): + return S.NegativeOne**(nu + 1) * jn(-nu - 1, z) + + def _expand(self, **hints): + return _yn(self.order, self.argument) + + def _eval_evalf(self, prec): + if self.order.is_Integer: + return self.rewrite(bessely)._eval_evalf(prec) + + +class SphericalHankelBase(SphericalBesselBase): + + @assume_integer_order + def _eval_rewrite_as_besselj(self, nu, z, **kwargs): + # jn +- I*yn + # jn as beeselj: sqrt(pi/(2*z)) * besselj(nu + S.Half, z) + # yn as besselj: (-1)**(nu+1) * sqrt(pi/(2*z)) * besselj(-nu - S.Half, z) + hks = self._hankel_kind_sign + return sqrt(pi/(2*z))*(besselj(nu + S.Half, z) + + hks*I*S.NegativeOne**(nu+1)*besselj(-nu - S.Half, z)) + + @assume_integer_order + def _eval_rewrite_as_bessely(self, nu, z, **kwargs): + # jn +- I*yn + # jn as bessely: (-1)**nu * sqrt(pi/(2*z)) * bessely(-nu - S.Half, z) + # yn as bessely: sqrt(pi/(2*z)) * bessely(nu + S.Half, z) + hks = self._hankel_kind_sign + return sqrt(pi/(2*z))*(S.NegativeOne**nu*bessely(-nu - S.Half, z) + + hks*I*bessely(nu + S.Half, z)) + + def _eval_rewrite_as_yn(self, nu, z, **kwargs): + hks = self._hankel_kind_sign + return jn(nu, z).rewrite(yn) + hks*I*yn(nu, z) + + def _eval_rewrite_as_jn(self, nu, z, **kwargs): + hks = self._hankel_kind_sign + return jn(nu, z) + hks*I*yn(nu, z).rewrite(jn) + + def _eval_expand_func(self, **hints): + if self.order.is_Integer: + return self._expand(**hints) + else: + nu = self.order + z = self.argument + hks = self._hankel_kind_sign + return jn(nu, z) + hks*I*yn(nu, z) + + def _expand(self, **hints): + n = self.order + z = self.argument + hks = self._hankel_kind_sign + + # fully expanded version + # return ((fn(n, z) * sin(z) + + # (-1)**(n + 1) * fn(-n - 1, z) * cos(z)) + # jn + # (hks * I * (-1)**(n + 1) * + # (fn(-n - 1, z) * hk * I * sin(z) + + # (-1)**(-n) * fn(n, z) * I * cos(z))) # +-I*yn + # ) + + return (_jn(n, z) + hks*I*_yn(n, z)).expand() + + def _eval_evalf(self, prec): + if self.order.is_Integer: + return self.rewrite(besselj)._eval_evalf(prec) + + +class hn1(SphericalHankelBase): + r""" + Spherical Hankel function of the first kind. + + Explanation + =========== + + This function is defined as + + .. math:: h_\nu^(1)(z) = j_\nu(z) + i y_\nu(z), + + where $j_\nu(z)$ and $y_\nu(z)$ are the spherical + Bessel function of the first and second kinds. + + For integral orders $n$, $h_n^(1)$ is calculated using the formula: + + .. math:: h_n^(1)(z) = j_{n}(z) + i (-1)^{n+1} j_{-n-1}(z) + + Examples + ======== + + >>> from sympy import Symbol, hn1, hankel1, expand_func, yn, jn + >>> z = Symbol("z") + >>> nu = Symbol("nu", integer=True) + >>> print(expand_func(hn1(nu, z))) + jn(nu, z) + I*yn(nu, z) + >>> print(expand_func(hn1(0, z))) + sin(z)/z - I*cos(z)/z + >>> print(expand_func(hn1(1, z))) + -I*sin(z)/z - cos(z)/z + sin(z)/z**2 - I*cos(z)/z**2 + >>> hn1(nu, z).rewrite(jn) + (-1)**(nu + 1)*I*jn(-nu - 1, z) + jn(nu, z) + >>> hn1(nu, z).rewrite(yn) + (-1)**nu*yn(-nu - 1, z) + I*yn(nu, z) + >>> hn1(nu, z).rewrite(hankel1) + sqrt(2)*sqrt(pi)*sqrt(1/z)*hankel1(nu, z)/2 + + See Also + ======== + + hn2, jn, yn, hankel1, hankel2 + + References + ========== + + .. [1] https://dlmf.nist.gov/10.47 + + """ + + _hankel_kind_sign = S.One + + @assume_integer_order + def _eval_rewrite_as_hankel1(self, nu, z, **kwargs): + return sqrt(pi/(2*z))*hankel1(nu, z) + + +class hn2(SphericalHankelBase): + r""" + Spherical Hankel function of the second kind. + + Explanation + =========== + + This function is defined as + + .. math:: h_\nu^(2)(z) = j_\nu(z) - i y_\nu(z), + + where $j_\nu(z)$ and $y_\nu(z)$ are the spherical + Bessel function of the first and second kinds. + + For integral orders $n$, $h_n^(2)$ is calculated using the formula: + + .. math:: h_n^(2)(z) = j_{n} - i (-1)^{n+1} j_{-n-1}(z) + + Examples + ======== + + >>> from sympy import Symbol, hn2, hankel2, expand_func, jn, yn + >>> z = Symbol("z") + >>> nu = Symbol("nu", integer=True) + >>> print(expand_func(hn2(nu, z))) + jn(nu, z) - I*yn(nu, z) + >>> print(expand_func(hn2(0, z))) + sin(z)/z + I*cos(z)/z + >>> print(expand_func(hn2(1, z))) + I*sin(z)/z - cos(z)/z + sin(z)/z**2 + I*cos(z)/z**2 + >>> hn2(nu, z).rewrite(hankel2) + sqrt(2)*sqrt(pi)*sqrt(1/z)*hankel2(nu, z)/2 + >>> hn2(nu, z).rewrite(jn) + -(-1)**(nu + 1)*I*jn(-nu - 1, z) + jn(nu, z) + >>> hn2(nu, z).rewrite(yn) + (-1)**nu*yn(-nu - 1, z) - I*yn(nu, z) + + See Also + ======== + + hn1, jn, yn, hankel1, hankel2 + + References + ========== + + .. [1] https://dlmf.nist.gov/10.47 + + """ + + _hankel_kind_sign = -S.One + + @assume_integer_order + def _eval_rewrite_as_hankel2(self, nu, z, **kwargs): + return sqrt(pi/(2*z))*hankel2(nu, z) + + +def jn_zeros(n, k, method="sympy", dps=15): + """ + Zeros of the spherical Bessel function of the first kind. + + Explanation + =========== + + This returns an array of zeros of $jn$ up to the $k$-th zero. + + * method = "sympy": uses `mpmath.besseljzero + `_ + * method = "scipy": uses the + `SciPy's sph_jn `_ + and + `newton `_ + to find all + roots, which is faster than computing the zeros using a general + numerical solver, but it requires SciPy and only works with low + precision floating point numbers. (The function used with + method="sympy" is a recent addition to mpmath; before that a general + solver was used.) + + Examples + ======== + + >>> from sympy import jn_zeros + >>> jn_zeros(2, 4, dps=5) + [5.7635, 9.095, 12.323, 15.515] + + See Also + ======== + + jn, yn, besselj, besselk, bessely + + Parameters + ========== + + n : integer + order of Bessel function + + k : integer + number of zeros to return + + + """ + from math import pi as math_pi + + if method == "sympy": + from mpmath import besseljzero + from mpmath.libmp.libmpf import dps_to_prec + prec = dps_to_prec(dps) + return [Expr._from_mpmath(besseljzero(S(n + 0.5)._to_mpmath(prec), + int(l)), prec) + for l in range(1, k + 1)] + elif method == "scipy": + from scipy.optimize import newton + try: + from scipy.special import spherical_jn + f = lambda x: spherical_jn(n, x) + except ImportError: + from scipy.special import sph_jn + f = lambda x: sph_jn(n, x)[0][-1] + else: + raise NotImplementedError("Unknown method.") + + def solver(f, x): + if method == "scipy": + root = newton(f, x) + else: + raise NotImplementedError("Unknown method.") + return root + + # we need to approximate the position of the first root: + root = n + math_pi + # determine the first root exactly: + root = solver(f, root) + roots = [root] + for i in range(k - 1): + # estimate the position of the next root using the last root + pi: + root = solver(f, root + math_pi) + roots.append(root) + return roots + + +class AiryBase(DefinedFunction): + """ + Abstract base class for Airy functions. + + This class is meant to reduce code duplication. + + """ + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def _eval_is_extended_real(self): + return self.args[0].is_extended_real + + def as_real_imag(self, deep=True, **hints): + z = self.args[0] + zc = z.conjugate() + f = self.func + u = (f(z)+f(zc))/2 + v = I*(f(zc)-f(z))/2 + return u, v + + def _eval_expand_complex(self, deep=True, **hints): + re_part, im_part = self.as_real_imag(deep=deep, **hints) + return re_part + im_part*I + + +class airyai(AiryBase): + r""" + The Airy function $\operatorname{Ai}$ of the first kind. + + Explanation + =========== + + The Airy function $\operatorname{Ai}(z)$ is defined to be the function + satisfying Airy's differential equation + + .. math:: + \frac{\mathrm{d}^2 w(z)}{\mathrm{d}z^2} - z w(z) = 0. + + Equivalently, for real $z$ + + .. math:: + \operatorname{Ai}(z) := \frac{1}{\pi} + \int_0^\infty \cos\left(\frac{t^3}{3} + z t\right) \mathrm{d}t. + + Examples + ======== + + Create an Airy function object: + + >>> from sympy import airyai + >>> from sympy.abc import z + + >>> airyai(z) + airyai(z) + + Several special values are known: + + >>> airyai(0) + 3**(1/3)/(3*gamma(2/3)) + >>> from sympy import oo + >>> airyai(oo) + 0 + >>> airyai(-oo) + 0 + + The Airy function obeys the mirror symmetry: + + >>> from sympy import conjugate + >>> conjugate(airyai(z)) + airyai(conjugate(z)) + + Differentiation with respect to $z$ is supported: + + >>> from sympy import diff + >>> diff(airyai(z), z) + airyaiprime(z) + >>> diff(airyai(z), z, 2) + z*airyai(z) + + Series expansion is also supported: + + >>> from sympy import series + >>> series(airyai(z), z, 0, 3) + 3**(5/6)*gamma(1/3)/(6*pi) - 3**(1/6)*z*gamma(2/3)/(2*pi) + O(z**3) + + We can numerically evaluate the Airy function to arbitrary precision + on the whole complex plane: + + >>> airyai(-2).evalf(50) + 0.22740742820168557599192443603787379946077222541710 + + Rewrite $\operatorname{Ai}(z)$ in terms of hypergeometric functions: + + >>> from sympy import hyper + >>> airyai(z).rewrite(hyper) + -3**(2/3)*z*hyper((), (4/3,), z**3/9)/(3*gamma(1/3)) + 3**(1/3)*hyper((), (2/3,), z**3/9)/(3*gamma(2/3)) + + See Also + ======== + + airybi: Airy function of the second kind. + airyaiprime: Derivative of the Airy function of the first kind. + airybiprime: Derivative of the Airy function of the second kind. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Airy_function + .. [2] https://dlmf.nist.gov/9 + .. [3] https://encyclopediaofmath.org/wiki/Airy_functions + .. [4] https://mathworld.wolfram.com/AiryFunctions.html + + """ + + nargs = 1 + unbranched = True + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.Zero + elif arg is S.NegativeInfinity: + return S.Zero + elif arg.is_zero: + return S.One / (3**Rational(2, 3) * gamma(Rational(2, 3))) + if arg.is_zero: + return S.One / (3**Rational(2, 3) * gamma(Rational(2, 3))) + + def fdiff(self, argindex=1): + if argindex == 1: + return airyaiprime(self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0: + return S.Zero + else: + x = sympify(x) + if len(previous_terms) > 1: + p = previous_terms[-1] + return ((cbrt(3)*x)**(-n)*(cbrt(3)*x)**(n + 1)*sin(pi*(n*Rational(2, 3) + Rational(4, 3)))*factorial(n) * + gamma(n/3 + Rational(2, 3))/(sin(pi*(n*Rational(2, 3) + Rational(2, 3)))*factorial(n + 1)*gamma(n/3 + Rational(1, 3))) * p) + else: + return (S.One/(3**Rational(2, 3)*pi) * gamma((n+S.One)/S(3)) * sin(Rational(2, 3)*pi*(n+S.One)) / + factorial(n) * (cbrt(3)*x)**n) + + def _eval_rewrite_as_besselj(self, z, **kwargs): + ot = Rational(1, 3) + tt = Rational(2, 3) + a = Pow(-z, Rational(3, 2)) + if re(z).is_negative: + return ot*sqrt(-z) * (besselj(-ot, tt*a) + besselj(ot, tt*a)) + + def _eval_rewrite_as_besseli(self, z, **kwargs): + ot = Rational(1, 3) + tt = Rational(2, 3) + a = Pow(z, Rational(3, 2)) + if re(z).is_positive: + return ot*sqrt(z) * (besseli(-ot, tt*a) - besseli(ot, tt*a)) + else: + return ot*(Pow(a, ot)*besseli(-ot, tt*a) - z*Pow(a, -ot)*besseli(ot, tt*a)) + + def _eval_rewrite_as_hyper(self, z, **kwargs): + pf1 = S.One / (3**Rational(2, 3)*gamma(Rational(2, 3))) + pf2 = z / (root(3, 3)*gamma(Rational(1, 3))) + return pf1 * hyper([], [Rational(2, 3)], z**3/9) - pf2 * hyper([], [Rational(4, 3)], z**3/9) + + def _eval_expand_func(self, **hints): + arg = self.args[0] + symbs = arg.free_symbols + + if len(symbs) == 1: + z = symbs.pop() + c = Wild("c", exclude=[z]) + d = Wild("d", exclude=[z]) + m = Wild("m", exclude=[z]) + n = Wild("n", exclude=[z]) + M = arg.match(c*(d*z**n)**m) + if M is not None: + m = M[m] + # The transformation is given by 03.05.16.0001.01 + # https://functions.wolfram.com/Bessel-TypeFunctions/AiryAi/16/01/01/0001/ + if (3*m).is_integer: + c = M[c] + d = M[d] + n = M[n] + pf = (d * z**n)**m / (d**m * z**(m*n)) + newarg = c * d**m * z**(m*n) + return S.Half * ((pf + S.One)*airyai(newarg) - (pf - S.One)/sqrt(3)*airybi(newarg)) + + +class airybi(AiryBase): + r""" + The Airy function $\operatorname{Bi}$ of the second kind. + + Explanation + =========== + + The Airy function $\operatorname{Bi}(z)$ is defined to be the function + satisfying Airy's differential equation + + .. math:: + \frac{\mathrm{d}^2 w(z)}{\mathrm{d}z^2} - z w(z) = 0. + + Equivalently, for real $z$ + + .. math:: + \operatorname{Bi}(z) := \frac{1}{\pi} + \int_0^\infty + \exp\left(-\frac{t^3}{3} + z t\right) + + \sin\left(\frac{t^3}{3} + z t\right) \mathrm{d}t. + + Examples + ======== + + Create an Airy function object: + + >>> from sympy import airybi + >>> from sympy.abc import z + + >>> airybi(z) + airybi(z) + + Several special values are known: + + >>> airybi(0) + 3**(5/6)/(3*gamma(2/3)) + >>> from sympy import oo + >>> airybi(oo) + oo + >>> airybi(-oo) + 0 + + The Airy function obeys the mirror symmetry: + + >>> from sympy import conjugate + >>> conjugate(airybi(z)) + airybi(conjugate(z)) + + Differentiation with respect to $z$ is supported: + + >>> from sympy import diff + >>> diff(airybi(z), z) + airybiprime(z) + >>> diff(airybi(z), z, 2) + z*airybi(z) + + Series expansion is also supported: + + >>> from sympy import series + >>> series(airybi(z), z, 0, 3) + 3**(1/3)*gamma(1/3)/(2*pi) + 3**(2/3)*z*gamma(2/3)/(2*pi) + O(z**3) + + We can numerically evaluate the Airy function to arbitrary precision + on the whole complex plane: + + >>> airybi(-2).evalf(50) + -0.41230258795639848808323405461146104203453483447240 + + Rewrite $\operatorname{Bi}(z)$ in terms of hypergeometric functions: + + >>> from sympy import hyper + >>> airybi(z).rewrite(hyper) + 3**(1/6)*z*hyper((), (4/3,), z**3/9)/gamma(1/3) + 3**(5/6)*hyper((), (2/3,), z**3/9)/(3*gamma(2/3)) + + See Also + ======== + + airyai: Airy function of the first kind. + airyaiprime: Derivative of the Airy function of the first kind. + airybiprime: Derivative of the Airy function of the second kind. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Airy_function + .. [2] https://dlmf.nist.gov/9 + .. [3] https://encyclopediaofmath.org/wiki/Airy_functions + .. [4] https://mathworld.wolfram.com/AiryFunctions.html + + """ + + nargs = 1 + unbranched = True + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.Infinity + elif arg is S.NegativeInfinity: + return S.Zero + elif arg.is_zero: + return S.One / (3**Rational(1, 6) * gamma(Rational(2, 3))) + + if arg.is_zero: + return S.One / (3**Rational(1, 6) * gamma(Rational(2, 3))) + + def fdiff(self, argindex=1): + if argindex == 1: + return airybiprime(self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0: + return S.Zero + else: + x = sympify(x) + if len(previous_terms) > 1: + p = previous_terms[-1] + return (cbrt(3)*x * Abs(sin(Rational(2, 3)*pi*(n + S.One))) * factorial((n - S.One)/S(3)) / + ((n + S.One) * Abs(cos(Rational(2, 3)*pi*(n + S.Half))) * factorial((n - 2)/S(3))) * p) + else: + return (S.One/(root(3, 6)*pi) * gamma((n + S.One)/S(3)) * Abs(sin(Rational(2, 3)*pi*(n + S.One))) / + factorial(n) * (cbrt(3)*x)**n) + + def _eval_rewrite_as_besselj(self, z, **kwargs): + ot = Rational(1, 3) + tt = Rational(2, 3) + a = Pow(-z, Rational(3, 2)) + if re(z).is_negative: + return sqrt(-z/3) * (besselj(-ot, tt*a) - besselj(ot, tt*a)) + + def _eval_rewrite_as_besseli(self, z, **kwargs): + ot = Rational(1, 3) + tt = Rational(2, 3) + a = Pow(z, Rational(3, 2)) + if re(z).is_positive: + return sqrt(z)/sqrt(3) * (besseli(-ot, tt*a) + besseli(ot, tt*a)) + else: + b = Pow(a, ot) + c = Pow(a, -ot) + return sqrt(ot)*(b*besseli(-ot, tt*a) + z*c*besseli(ot, tt*a)) + + def _eval_rewrite_as_hyper(self, z, **kwargs): + pf1 = S.One / (root(3, 6)*gamma(Rational(2, 3))) + pf2 = z*root(3, 6) / gamma(Rational(1, 3)) + return pf1 * hyper([], [Rational(2, 3)], z**3/9) + pf2 * hyper([], [Rational(4, 3)], z**3/9) + + def _eval_expand_func(self, **hints): + arg = self.args[0] + symbs = arg.free_symbols + + if len(symbs) == 1: + z = symbs.pop() + c = Wild("c", exclude=[z]) + d = Wild("d", exclude=[z]) + m = Wild("m", exclude=[z]) + n = Wild("n", exclude=[z]) + M = arg.match(c*(d*z**n)**m) + if M is not None: + m = M[m] + # The transformation is given by 03.06.16.0001.01 + # https://functions.wolfram.com/Bessel-TypeFunctions/AiryBi/16/01/01/0001/ + if (3*m).is_integer: + c = M[c] + d = M[d] + n = M[n] + pf = (d * z**n)**m / (d**m * z**(m*n)) + newarg = c * d**m * z**(m*n) + return S.Half * (sqrt(3)*(S.One - pf)*airyai(newarg) + (S.One + pf)*airybi(newarg)) + + +class airyaiprime(AiryBase): + r""" + The derivative $\operatorname{Ai}^\prime$ of the Airy function of the first + kind. + + Explanation + =========== + + The Airy function $\operatorname{Ai}^\prime(z)$ is defined to be the + function + + .. math:: + \operatorname{Ai}^\prime(z) := \frac{\mathrm{d} \operatorname{Ai}(z)}{\mathrm{d} z}. + + Examples + ======== + + Create an Airy function object: + + >>> from sympy import airyaiprime + >>> from sympy.abc import z + + >>> airyaiprime(z) + airyaiprime(z) + + Several special values are known: + + >>> airyaiprime(0) + -3**(2/3)/(3*gamma(1/3)) + >>> from sympy import oo + >>> airyaiprime(oo) + 0 + + The Airy function obeys the mirror symmetry: + + >>> from sympy import conjugate + >>> conjugate(airyaiprime(z)) + airyaiprime(conjugate(z)) + + Differentiation with respect to $z$ is supported: + + >>> from sympy import diff + >>> diff(airyaiprime(z), z) + z*airyai(z) + >>> diff(airyaiprime(z), z, 2) + z*airyaiprime(z) + airyai(z) + + Series expansion is also supported: + + >>> from sympy import series + >>> series(airyaiprime(z), z, 0, 3) + -3**(2/3)/(3*gamma(1/3)) + 3**(1/3)*z**2/(6*gamma(2/3)) + O(z**3) + + We can numerically evaluate the Airy function to arbitrary precision + on the whole complex plane: + + >>> airyaiprime(-2).evalf(50) + 0.61825902074169104140626429133247528291577794512415 + + Rewrite $\operatorname{Ai}^\prime(z)$ in terms of hypergeometric functions: + + >>> from sympy import hyper + >>> airyaiprime(z).rewrite(hyper) + 3**(1/3)*z**2*hyper((), (5/3,), z**3/9)/(6*gamma(2/3)) - 3**(2/3)*hyper((), (1/3,), z**3/9)/(3*gamma(1/3)) + + See Also + ======== + + airyai: Airy function of the first kind. + airybi: Airy function of the second kind. + airybiprime: Derivative of the Airy function of the second kind. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Airy_function + .. [2] https://dlmf.nist.gov/9 + .. [3] https://encyclopediaofmath.org/wiki/Airy_functions + .. [4] https://mathworld.wolfram.com/AiryFunctions.html + + """ + + nargs = 1 + unbranched = True + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.Zero + + if arg.is_zero: + return S.NegativeOne / (3**Rational(1, 3) * gamma(Rational(1, 3))) + + def fdiff(self, argindex=1): + if argindex == 1: + return self.args[0]*airyai(self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_evalf(self, prec): + z = self.args[0]._to_mpmath(prec) + with workprec(prec): + res = mp.airyai(z, derivative=1) + return Expr._from_mpmath(res, prec) + + def _eval_rewrite_as_besselj(self, z, **kwargs): + tt = Rational(2, 3) + a = Pow(-z, Rational(3, 2)) + if re(z).is_negative: + return z/3 * (besselj(-tt, tt*a) - besselj(tt, tt*a)) + + def _eval_rewrite_as_besseli(self, z, **kwargs): + ot = Rational(1, 3) + tt = Rational(2, 3) + a = tt * Pow(z, Rational(3, 2)) + if re(z).is_positive: + return z/3 * (besseli(tt, a) - besseli(-tt, a)) + else: + a = Pow(z, Rational(3, 2)) + b = Pow(a, tt) + c = Pow(a, -tt) + return ot * (z**2*c*besseli(tt, tt*a) - b*besseli(-ot, tt*a)) + + def _eval_rewrite_as_hyper(self, z, **kwargs): + pf1 = z**2 / (2*3**Rational(2, 3)*gamma(Rational(2, 3))) + pf2 = 1 / (root(3, 3)*gamma(Rational(1, 3))) + return pf1 * hyper([], [Rational(5, 3)], z**3/9) - pf2 * hyper([], [Rational(1, 3)], z**3/9) + + def _eval_expand_func(self, **hints): + arg = self.args[0] + symbs = arg.free_symbols + + if len(symbs) == 1: + z = symbs.pop() + c = Wild("c", exclude=[z]) + d = Wild("d", exclude=[z]) + m = Wild("m", exclude=[z]) + n = Wild("n", exclude=[z]) + M = arg.match(c*(d*z**n)**m) + if M is not None: + m = M[m] + # The transformation is in principle + # given by 03.07.16.0001.01 but note + # that there is an error in this formula. + # https://functions.wolfram.com/Bessel-TypeFunctions/AiryAiPrime/16/01/01/0001/ + if (3*m).is_integer: + c = M[c] + d = M[d] + n = M[n] + pf = (d**m * z**(n*m)) / (d * z**n)**m + newarg = c * d**m * z**(n*m) + return S.Half * ((pf + S.One)*airyaiprime(newarg) + (pf - S.One)/sqrt(3)*airybiprime(newarg)) + + +class airybiprime(AiryBase): + r""" + The derivative $\operatorname{Bi}^\prime$ of the Airy function of the first + kind. + + Explanation + =========== + + The Airy function $\operatorname{Bi}^\prime(z)$ is defined to be the + function + + .. math:: + \operatorname{Bi}^\prime(z) := \frac{\mathrm{d} \operatorname{Bi}(z)}{\mathrm{d} z}. + + Examples + ======== + + Create an Airy function object: + + >>> from sympy import airybiprime + >>> from sympy.abc import z + + >>> airybiprime(z) + airybiprime(z) + + Several special values are known: + + >>> airybiprime(0) + 3**(1/6)/gamma(1/3) + >>> from sympy import oo + >>> airybiprime(oo) + oo + >>> airybiprime(-oo) + 0 + + The Airy function obeys the mirror symmetry: + + >>> from sympy import conjugate + >>> conjugate(airybiprime(z)) + airybiprime(conjugate(z)) + + Differentiation with respect to $z$ is supported: + + >>> from sympy import diff + >>> diff(airybiprime(z), z) + z*airybi(z) + >>> diff(airybiprime(z), z, 2) + z*airybiprime(z) + airybi(z) + + Series expansion is also supported: + + >>> from sympy import series + >>> series(airybiprime(z), z, 0, 3) + 3**(1/6)/gamma(1/3) + 3**(5/6)*z**2/(6*gamma(2/3)) + O(z**3) + + We can numerically evaluate the Airy function to arbitrary precision + on the whole complex plane: + + >>> airybiprime(-2).evalf(50) + 0.27879516692116952268509756941098324140300059345163 + + Rewrite $\operatorname{Bi}^\prime(z)$ in terms of hypergeometric functions: + + >>> from sympy import hyper + >>> airybiprime(z).rewrite(hyper) + 3**(5/6)*z**2*hyper((), (5/3,), z**3/9)/(6*gamma(2/3)) + 3**(1/6)*hyper((), (1/3,), z**3/9)/gamma(1/3) + + See Also + ======== + + airyai: Airy function of the first kind. + airybi: Airy function of the second kind. + airyaiprime: Derivative of the Airy function of the first kind. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Airy_function + .. [2] https://dlmf.nist.gov/9 + .. [3] https://encyclopediaofmath.org/wiki/Airy_functions + .. [4] https://mathworld.wolfram.com/AiryFunctions.html + + """ + + nargs = 1 + unbranched = True + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.Infinity + elif arg is S.NegativeInfinity: + return S.Zero + elif arg.is_zero: + return 3**Rational(1, 6) / gamma(Rational(1, 3)) + + if arg.is_zero: + return 3**Rational(1, 6) / gamma(Rational(1, 3)) + + + def fdiff(self, argindex=1): + if argindex == 1: + return self.args[0]*airybi(self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_evalf(self, prec): + z = self.args[0]._to_mpmath(prec) + with workprec(prec): + res = mp.airybi(z, derivative=1) + return Expr._from_mpmath(res, prec) + + def _eval_rewrite_as_besselj(self, z, **kwargs): + tt = Rational(2, 3) + a = tt * Pow(-z, Rational(3, 2)) + if re(z).is_negative: + return -z/sqrt(3) * (besselj(-tt, a) + besselj(tt, a)) + + def _eval_rewrite_as_besseli(self, z, **kwargs): + ot = Rational(1, 3) + tt = Rational(2, 3) + a = tt * Pow(z, Rational(3, 2)) + if re(z).is_positive: + return z/sqrt(3) * (besseli(-tt, a) + besseli(tt, a)) + else: + a = Pow(z, Rational(3, 2)) + b = Pow(a, tt) + c = Pow(a, -tt) + return sqrt(ot) * (b*besseli(-tt, tt*a) + z**2*c*besseli(tt, tt*a)) + + def _eval_rewrite_as_hyper(self, z, **kwargs): + pf1 = z**2 / (2*root(3, 6)*gamma(Rational(2, 3))) + pf2 = root(3, 6) / gamma(Rational(1, 3)) + return pf1 * hyper([], [Rational(5, 3)], z**3/9) + pf2 * hyper([], [Rational(1, 3)], z**3/9) + + def _eval_expand_func(self, **hints): + arg = self.args[0] + symbs = arg.free_symbols + + if len(symbs) == 1: + z = symbs.pop() + c = Wild("c", exclude=[z]) + d = Wild("d", exclude=[z]) + m = Wild("m", exclude=[z]) + n = Wild("n", exclude=[z]) + M = arg.match(c*(d*z**n)**m) + if M is not None: + m = M[m] + # The transformation is in principle + # given by 03.08.16.0001.01 but note + # that there is an error in this formula. + # https://functions.wolfram.com/Bessel-TypeFunctions/AiryBiPrime/16/01/01/0001/ + if (3*m).is_integer: + c = M[c] + d = M[d] + n = M[n] + pf = (d**m * z**(n*m)) / (d * z**n)**m + newarg = c * d**m * z**(n*m) + return S.Half * (sqrt(3)*(pf - S.One)*airyaiprime(newarg) + (pf + S.One)*airybiprime(newarg)) + + +class marcumq(DefinedFunction): + r""" + The Marcum Q-function. + + Explanation + =========== + + The Marcum Q-function is defined by the meromorphic continuation of + + .. math:: + Q_m(a, b) = a^{- m + 1} \int_{b}^{\infty} x^{m} e^{- \frac{a^{2}}{2} - \frac{x^{2}}{2}} I_{m - 1}\left(a x\right)\, dx + + Examples + ======== + + >>> from sympy import marcumq + >>> from sympy.abc import m, a, b + >>> marcumq(m, a, b) + marcumq(m, a, b) + + Special values: + + >>> marcumq(m, 0, b) + uppergamma(m, b**2/2)/gamma(m) + >>> marcumq(0, 0, 0) + 0 + >>> marcumq(0, a, 0) + 1 - exp(-a**2/2) + >>> marcumq(1, a, a) + 1/2 + exp(-a**2)*besseli(0, a**2)/2 + >>> marcumq(2, a, a) + 1/2 + exp(-a**2)*besseli(0, a**2)/2 + exp(-a**2)*besseli(1, a**2) + + Differentiation with respect to $a$ and $b$ is supported: + + >>> from sympy import diff + >>> diff(marcumq(m, a, b), a) + a*(-marcumq(m, a, b) + marcumq(m + 1, a, b)) + >>> diff(marcumq(m, a, b), b) + -a**(1 - m)*b**m*exp(-a**2/2 - b**2/2)*besseli(m - 1, a*b) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Marcum_Q-function + .. [2] https://mathworld.wolfram.com/MarcumQ-Function.html + + """ + + @classmethod + def eval(cls, m, a, b): + if a is S.Zero: + if m is S.Zero and b is S.Zero: + return S.Zero + return uppergamma(m, b**2 * S.Half) / gamma(m) + + if m is S.Zero and b is S.Zero: + return 1 - 1 / exp(a**2 * S.Half) + + if a == b: + if m is S.One: + return (1 + exp(-a**2) * besseli(0, a**2))*S.Half + if m == 2: + return S.Half + S.Half * exp(-a**2) * besseli(0, a**2) + exp(-a**2) * besseli(1, a**2) + + if a.is_zero: + if m.is_zero and b.is_zero: + return S.Zero + return uppergamma(m, b**2*S.Half) / gamma(m) + + if m.is_zero and b.is_zero: + return 1 - 1 / exp(a**2*S.Half) + + def fdiff(self, argindex=2): + m, a, b = self.args + if argindex == 2: + return a * (-marcumq(m, a, b) + marcumq(1+m, a, b)) + elif argindex == 3: + return (-b**m / a**(m-1)) * exp(-(a**2 + b**2)/2) * besseli(m-1, a*b) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Integral(self, m, a, b, **kwargs): + from sympy.integrals.integrals import Integral + x = kwargs.get('x', Dummy(uniquely_named_symbol('x').name)) + return a ** (1 - m) * \ + Integral(x**m * exp(-(x**2 + a**2)/2) * besseli(m-1, a*x), [x, b, S.Infinity]) + + def _eval_rewrite_as_Sum(self, m, a, b, **kwargs): + from sympy.concrete.summations import Sum + k = kwargs.get('k', Dummy('k')) + return exp(-(a**2 + b**2) / 2) * Sum((a/b)**k * besseli(k, a*b), [k, 1-m, S.Infinity]) + + def _eval_rewrite_as_besseli(self, m, a, b, **kwargs): + if a == b: + if m == 1: + return (1 + exp(-a**2) * besseli(0, a**2)) / 2 + if m.is_Integer and m >= 2: + s = sum(besseli(i, a**2) for i in range(1, m)) + return S.Half + exp(-a**2) * besseli(0, a**2) / 2 + exp(-a**2) * s + + def _eval_is_zero(self): + if all(arg.is_zero for arg in self.args): + return True + +class _besseli(DefinedFunction): + """ + Helper function to make the $\\mathrm{besseli}(nu, z)$ + function tractable for the Gruntz algorithm. + + """ + + def _eval_aseries(self, n, args0, x, logx): + from sympy.functions.combinatorial.factorials import RisingFactorial + from sympy.series.order import Order + point = args0[1] + + if point in [S.Infinity, S.NegativeInfinity]: + nu, z = self.args + l = [((RisingFactorial(Rational(2*nu - 1, 2), k)*RisingFactorial( + Rational(2*nu + 1, 2), k))/((2)**(k)*z**(Rational(2*k + 1, 2))*factorial(k))) for k in range(n)] + return sqrt(pi/(2))*(Add(*l)) + Order(1/z**(Rational(2*n + 1, 2)), x) + + return super()._eval_aseries(n, args0, x, logx) + + def _eval_rewrite_as_intractable(self, nu, z, **kwargs): + return exp(-z)*besseli(nu, z) + + def _eval_nseries(self, x, n, logx, cdir=0): + x0 = self.args[0].limit(x, 0) + if x0.is_zero: + f = self._eval_rewrite_as_intractable(*self.args) + return f._eval_nseries(x, n, logx) + return super()._eval_nseries(x, n, logx) + + +class _besselk(DefinedFunction): + """ + Helper function to make the $\\mathrm{besselk}(nu, z)$ + function tractable for the Gruntz algorithm. + + """ + + def _eval_aseries(self, n, args0, x, logx): + from sympy.functions.combinatorial.factorials import RisingFactorial + from sympy.series.order import Order + point = args0[1] + + if point in [S.Infinity, S.NegativeInfinity]: + nu, z = self.args + l = [((RisingFactorial(Rational(2*nu - 1, 2), k)*RisingFactorial( + Rational(2*nu + 1, 2), k))/((-2)**(k)*z**(Rational(2*k + 1, 2))*factorial(k))) for k in range(n)] + return sqrt(pi/(2))*(Add(*l)) + Order(1/z**(Rational(2*n + 1, 2)), x) + + return super()._eval_aseries(n, args0, x, logx) + + def _eval_rewrite_as_intractable(self,nu, z, **kwargs): + return exp(z)*besselk(nu, z) + + def _eval_nseries(self, x, n, logx, cdir=0): + x0 = self.args[0].limit(x, 0) + if x0.is_zero: + f = self._eval_rewrite_as_intractable(*self.args) + return f._eval_nseries(x, n, logx) + return super()._eval_nseries(x, n, logx) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/beta_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/beta_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..337ae6c71fe5a38cc0fcd819e9d489ebbbd1946b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/beta_functions.py @@ -0,0 +1,389 @@ +from sympy.core import S +from sympy.core.function import DefinedFunction, ArgumentIndexError +from sympy.core.symbol import Dummy, uniquely_named_symbol +from sympy.functions.special.gamma_functions import gamma, digamma +from sympy.functions.combinatorial.numbers import catalan +from sympy.functions.elementary.complexes import conjugate + +# See mpmath #569 and SymPy #20569 +def betainc_mpmath_fix(a, b, x1, x2, reg=0): + from mpmath import betainc, mpf + if x1 == x2: + return mpf(0) + else: + return betainc(a, b, x1, x2, reg) + +############################################################################### +############################ COMPLETE BETA FUNCTION ########################## +############################################################################### + +class beta(DefinedFunction): + r""" + The beta integral is called the Eulerian integral of the first kind by + Legendre: + + .. math:: + \mathrm{B}(x,y) \int^{1}_{0} t^{x-1} (1-t)^{y-1} \mathrm{d}t. + + Explanation + =========== + + The Beta function or Euler's first integral is closely associated + with the gamma function. The Beta function is often used in probability + theory and mathematical statistics. It satisfies properties like: + + .. math:: + \mathrm{B}(a,1) = \frac{1}{a} \\ + \mathrm{B}(a,b) = \mathrm{B}(b,a) \\ + \mathrm{B}(a,b) = \frac{\Gamma(a) \Gamma(b)}{\Gamma(a+b)} + + Therefore for integral values of $a$ and $b$: + + .. math:: + \mathrm{B} = \frac{(a-1)! (b-1)!}{(a+b-1)!} + + A special case of the Beta function when `x = y` is the + Central Beta function. It satisfies properties like: + + .. math:: + \mathrm{B}(x) = 2^{1 - 2x}\mathrm{B}(x, \frac{1}{2}) + \mathrm{B}(x) = 2^{1 - 2x} cos(\pi x) \mathrm{B}(\frac{1}{2} - x, x) + \mathrm{B}(x) = \int_{0}^{1} \frac{t^x}{(1 + t)^{2x}} dt + \mathrm{B}(x) = \frac{2}{x} \prod_{n = 1}^{\infty} \frac{n(n + 2x)}{(n + x)^2} + + Examples + ======== + + >>> from sympy import I, pi + >>> from sympy.abc import x, y + + The Beta function obeys the mirror symmetry: + + >>> from sympy import beta, conjugate + >>> conjugate(beta(x, y)) + beta(conjugate(x), conjugate(y)) + + Differentiation with respect to both $x$ and $y$ is supported: + + >>> from sympy import beta, diff + >>> diff(beta(x, y), x) + (polygamma(0, x) - polygamma(0, x + y))*beta(x, y) + + >>> diff(beta(x, y), y) + (polygamma(0, y) - polygamma(0, x + y))*beta(x, y) + + >>> diff(beta(x), x) + 2*(polygamma(0, x) - polygamma(0, 2*x))*beta(x, x) + + We can numerically evaluate the Beta function to + arbitrary precision for any complex numbers x and y: + + >>> from sympy import beta + >>> beta(pi).evalf(40) + 0.02671848900111377452242355235388489324562 + + >>> beta(1 + I).evalf(20) + -0.2112723729365330143 - 0.7655283165378005676*I + + See Also + ======== + + gamma: Gamma function. + uppergamma: Upper incomplete gamma function. + lowergamma: Lower incomplete gamma function. + polygamma: Polygamma function. + loggamma: Log Gamma function. + digamma: Digamma function. + trigamma: Trigamma function. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Beta_function + .. [2] https://mathworld.wolfram.com/BetaFunction.html + .. [3] https://dlmf.nist.gov/5.12 + + """ + unbranched = True + + def fdiff(self, argindex): + x, y = self.args + if argindex == 1: + # Diff wrt x + return beta(x, y)*(digamma(x) - digamma(x + y)) + elif argindex == 2: + # Diff wrt y + return beta(x, y)*(digamma(y) - digamma(x + y)) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, x, y=None): + if y is None: + return beta(x, x) + if x.is_Number and y.is_Number: + return beta(x, y, evaluate=False).doit() + + def doit(self, **hints): + x = xold = self.args[0] + # Deal with unevaluated single argument beta + single_argument = len(self.args) == 1 + y = yold = self.args[0] if single_argument else self.args[1] + if hints.get('deep', True): + x = x.doit(**hints) + y = y.doit(**hints) + if y.is_zero or x.is_zero: + return S.ComplexInfinity + if y is S.One: + return 1/x + if x is S.One: + return 1/y + if y == x + 1: + return 1/(x*y*catalan(x)) + s = x + y + if (s.is_integer and s.is_negative and x.is_integer is False and + y.is_integer is False): + return S.Zero + if x == xold and y == yold and not single_argument: + return self + return beta(x, y) + + def _eval_expand_func(self, **hints): + x, y = self.args + return gamma(x)*gamma(y) / gamma(x + y) + + def _eval_is_real(self): + return self.args[0].is_real and self.args[1].is_real + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate(), self.args[1].conjugate()) + + def _eval_rewrite_as_gamma(self, x, y, piecewise=True, **kwargs): + return self._eval_expand_func(**kwargs) + + def _eval_rewrite_as_Integral(self, x, y, **kwargs): + from sympy.integrals.integrals import Integral + t = Dummy(uniquely_named_symbol('t', [x, y]).name) + return Integral(t**(x - 1)*(1 - t)**(y - 1), (t, 0, 1)) + +############################################################################### +########################## INCOMPLETE BETA FUNCTION ########################### +############################################################################### + +class betainc(DefinedFunction): + r""" + The Generalized Incomplete Beta function is defined as + + .. math:: + \mathrm{B}_{(x_1, x_2)}(a, b) = \int_{x_1}^{x_2} t^{a - 1} (1 - t)^{b - 1} dt + + The Incomplete Beta function is a special case + of the Generalized Incomplete Beta function : + + .. math:: \mathrm{B}_z (a, b) = \mathrm{B}_{(0, z)}(a, b) + + The Incomplete Beta function satisfies : + + .. math:: \mathrm{B}_z (a, b) = (-1)^a \mathrm{B}_{\frac{z}{z - 1}} (a, 1 - a - b) + + The Beta function is a special case of the Incomplete Beta function : + + .. math:: \mathrm{B}(a, b) = \mathrm{B}_{1}(a, b) + + Examples + ======== + + >>> from sympy import betainc, symbols, conjugate + >>> a, b, x, x1, x2 = symbols('a b x x1 x2') + + The Generalized Incomplete Beta function is given by: + + >>> betainc(a, b, x1, x2) + betainc(a, b, x1, x2) + + The Incomplete Beta function can be obtained as follows: + + >>> betainc(a, b, 0, x) + betainc(a, b, 0, x) + + The Incomplete Beta function obeys the mirror symmetry: + + >>> conjugate(betainc(a, b, x1, x2)) + betainc(conjugate(a), conjugate(b), conjugate(x1), conjugate(x2)) + + We can numerically evaluate the Incomplete Beta function to + arbitrary precision for any complex numbers a, b, x1 and x2: + + >>> from sympy import betainc, I + >>> betainc(2, 3, 4, 5).evalf(10) + 56.08333333 + >>> betainc(0.75, 1 - 4*I, 0, 2 + 3*I).evalf(25) + 0.2241657956955709603655887 + 0.3619619242700451992411724*I + + The Generalized Incomplete Beta function can be expressed + in terms of the Generalized Hypergeometric function. + + >>> from sympy import hyper + >>> betainc(a, b, x1, x2).rewrite(hyper) + (-x1**a*hyper((a, 1 - b), (a + 1,), x1) + x2**a*hyper((a, 1 - b), (a + 1,), x2))/a + + See Also + ======== + + beta: Beta function + hyper: Generalized Hypergeometric function + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Beta_function#Incomplete_beta_function + .. [2] https://dlmf.nist.gov/8.17 + .. [3] https://functions.wolfram.com/GammaBetaErf/Beta4/ + .. [4] https://functions.wolfram.com/GammaBetaErf/BetaRegularized4/02/ + + """ + nargs = 4 + unbranched = True + + def fdiff(self, argindex): + a, b, x1, x2 = self.args + if argindex == 3: + # Diff wrt x1 + return -(1 - x1)**(b - 1)*x1**(a - 1) + elif argindex == 4: + # Diff wrt x2 + return (1 - x2)**(b - 1)*x2**(a - 1) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_mpmath(self): + return betainc_mpmath_fix, self.args + + def _eval_is_real(self): + if all(arg.is_real for arg in self.args): + return True + + def _eval_conjugate(self): + return self.func(*map(conjugate, self.args)) + + def _eval_rewrite_as_Integral(self, a, b, x1, x2, **kwargs): + from sympy.integrals.integrals import Integral + t = Dummy(uniquely_named_symbol('t', [a, b, x1, x2]).name) + return Integral(t**(a - 1)*(1 - t)**(b - 1), (t, x1, x2)) + + def _eval_rewrite_as_hyper(self, a, b, x1, x2, **kwargs): + from sympy.functions.special.hyper import hyper + return (x2**a * hyper((a, 1 - b), (a + 1,), x2) - x1**a * hyper((a, 1 - b), (a + 1,), x1)) / a + +############################################################################### +#################### REGULARIZED INCOMPLETE BETA FUNCTION ##################### +############################################################################### + +class betainc_regularized(DefinedFunction): + r""" + The Generalized Regularized Incomplete Beta function is given by + + .. math:: + \mathrm{I}_{(x_1, x_2)}(a, b) = \frac{\mathrm{B}_{(x_1, x_2)}(a, b)}{\mathrm{B}(a, b)} + + The Regularized Incomplete Beta function is a special case + of the Generalized Regularized Incomplete Beta function : + + .. math:: \mathrm{I}_z (a, b) = \mathrm{I}_{(0, z)}(a, b) + + The Regularized Incomplete Beta function is the cumulative distribution + function of the beta distribution. + + Examples + ======== + + >>> from sympy import betainc_regularized, symbols, conjugate + >>> a, b, x, x1, x2 = symbols('a b x x1 x2') + + The Generalized Regularized Incomplete Beta + function is given by: + + >>> betainc_regularized(a, b, x1, x2) + betainc_regularized(a, b, x1, x2) + + The Regularized Incomplete Beta function + can be obtained as follows: + + >>> betainc_regularized(a, b, 0, x) + betainc_regularized(a, b, 0, x) + + The Regularized Incomplete Beta function + obeys the mirror symmetry: + + >>> conjugate(betainc_regularized(a, b, x1, x2)) + betainc_regularized(conjugate(a), conjugate(b), conjugate(x1), conjugate(x2)) + + We can numerically evaluate the Regularized Incomplete Beta function + to arbitrary precision for any complex numbers a, b, x1 and x2: + + >>> from sympy import betainc_regularized, pi, E + >>> betainc_regularized(1, 2, 0, 0.25).evalf(10) + 0.4375000000 + >>> betainc_regularized(pi, E, 0, 1).evalf(5) + 1.00000 + + The Generalized Regularized Incomplete Beta function can be + expressed in terms of the Generalized Hypergeometric function. + + >>> from sympy import hyper + >>> betainc_regularized(a, b, x1, x2).rewrite(hyper) + (-x1**a*hyper((a, 1 - b), (a + 1,), x1) + x2**a*hyper((a, 1 - b), (a + 1,), x2))/(a*beta(a, b)) + + See Also + ======== + + beta: Beta function + hyper: Generalized Hypergeometric function + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Beta_function#Incomplete_beta_function + .. [2] https://dlmf.nist.gov/8.17 + .. [3] https://functions.wolfram.com/GammaBetaErf/Beta4/ + .. [4] https://functions.wolfram.com/GammaBetaErf/BetaRegularized4/02/ + + """ + nargs = 4 + unbranched = True + + def __new__(cls, a, b, x1, x2): + return super().__new__(cls, a, b, x1, x2) + + def _eval_mpmath(self): + return betainc_mpmath_fix, (*self.args, S(1)) + + def fdiff(self, argindex): + a, b, x1, x2 = self.args + if argindex == 3: + # Diff wrt x1 + return -(1 - x1)**(b - 1)*x1**(a - 1) / beta(a, b) + elif argindex == 4: + # Diff wrt x2 + return (1 - x2)**(b - 1)*x2**(a - 1) / beta(a, b) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_is_real(self): + if all(arg.is_real for arg in self.args): + return True + + def _eval_conjugate(self): + return self.func(*map(conjugate, self.args)) + + def _eval_rewrite_as_Integral(self, a, b, x1, x2, **kwargs): + from sympy.integrals.integrals import Integral + t = Dummy(uniquely_named_symbol('t', [a, b, x1, x2]).name) + integrand = t**(a - 1)*(1 - t)**(b - 1) + expr = Integral(integrand, (t, x1, x2)) + return expr / Integral(integrand, (t, 0, 1)) + + def _eval_rewrite_as_hyper(self, a, b, x1, x2, **kwargs): + from sympy.functions.special.hyper import hyper + expr = (x2**a * hyper((a, 1 - b), (a + 1,), x2) - x1**a * hyper((a, 1 - b), (a + 1,), x1)) / a + return expr / beta(a, b) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/bsplines.py b/venv/lib/python3.11/site-packages/sympy/functions/special/bsplines.py new file mode 100644 index 0000000000000000000000000000000000000000..50c9141e841288aa02457c466fc6573f9a20d09f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/bsplines.py @@ -0,0 +1,348 @@ +from sympy.core import S, sympify +from sympy.core.symbol import (Dummy, symbols) +from sympy.functions import Piecewise, piecewise_fold +from sympy.logic.boolalg import And +from sympy.sets.sets import Interval + +from functools import lru_cache + + +def _ivl(cond, x): + """return the interval corresponding to the condition + + Conditions in spline's Piecewise give the range over + which an expression is valid like (lo <= x) & (x <= hi). + This function returns (lo, hi). + """ + if isinstance(cond, And) and len(cond.args) == 2: + a, b = cond.args + if a.lts == x: + a, b = b, a + return a.lts, b.gts + raise TypeError('unexpected cond type: %s' % cond) + + +def _add_splines(c, b1, d, b2, x): + """Construct c*b1 + d*b2.""" + + if S.Zero in (b1, c): + rv = piecewise_fold(d * b2) + elif S.Zero in (b2, d): + rv = piecewise_fold(c * b1) + else: + new_args = [] + # Just combining the Piecewise without any fancy optimization + p1 = piecewise_fold(c * b1) + p2 = piecewise_fold(d * b2) + + # Search all Piecewise arguments except (0, True) + p2args = list(p2.args[:-1]) + + # This merging algorithm assumes the conditions in + # p1 and p2 are sorted + for arg in p1.args[:-1]: + expr = arg.expr + cond = arg.cond + + lower = _ivl(cond, x)[0] + + # Check p2 for matching conditions that can be merged + for i, arg2 in enumerate(p2args): + expr2 = arg2.expr + cond2 = arg2.cond + + lower_2, upper_2 = _ivl(cond2, x) + if cond2 == cond: + # Conditions match, join expressions + expr += expr2 + # Remove matching element + del p2args[i] + # No need to check the rest + break + elif lower_2 < lower and upper_2 <= lower: + # Check if arg2 condition smaller than arg1, + # add to new_args by itself (no match expected + # in p1) + new_args.append(arg2) + del p2args[i] + break + + # Checked all, add expr and cond + new_args.append((expr, cond)) + + # Add remaining items from p2args + new_args.extend(p2args) + + # Add final (0, True) + new_args.append((0, True)) + + rv = Piecewise(*new_args, evaluate=False) + + return rv.expand() + + +@lru_cache(maxsize=128) +def bspline_basis(d, knots, n, x): + """ + The $n$-th B-spline at $x$ of degree $d$ with knots. + + Explanation + =========== + + B-Splines are piecewise polynomials of degree $d$. They are defined on a + set of knots, which is a sequence of integers or floats. + + Examples + ======== + + The 0th degree splines have a value of 1 on a single interval: + + >>> from sympy import bspline_basis + >>> from sympy.abc import x + >>> d = 0 + >>> knots = tuple(range(5)) + >>> bspline_basis(d, knots, 0, x) + Piecewise((1, (x >= 0) & (x <= 1)), (0, True)) + + For a given ``(d, knots)`` there are ``len(knots)-d-1`` B-splines + defined, that are indexed by ``n`` (starting at 0). + + Here is an example of a cubic B-spline: + + >>> bspline_basis(3, tuple(range(5)), 0, x) + Piecewise((x**3/6, (x >= 0) & (x <= 1)), + (-x**3/2 + 2*x**2 - 2*x + 2/3, + (x >= 1) & (x <= 2)), + (x**3/2 - 4*x**2 + 10*x - 22/3, + (x >= 2) & (x <= 3)), + (-x**3/6 + 2*x**2 - 8*x + 32/3, + (x >= 3) & (x <= 4)), + (0, True)) + + By repeating knot points, you can introduce discontinuities in the + B-splines and their derivatives: + + >>> d = 1 + >>> knots = (0, 0, 2, 3, 4) + >>> bspline_basis(d, knots, 0, x) + Piecewise((1 - x/2, (x >= 0) & (x <= 2)), (0, True)) + + It is quite time consuming to construct and evaluate B-splines. If + you need to evaluate a B-spline many times, it is best to lambdify them + first: + + >>> from sympy import lambdify + >>> d = 3 + >>> knots = tuple(range(10)) + >>> b0 = bspline_basis(d, knots, 0, x) + >>> f = lambdify(x, b0) + >>> y = f(0.5) + + Parameters + ========== + + d : integer + degree of bspline + + knots : list of integer values + list of knots points of bspline + + n : integer + $n$-th B-spline + + x : symbol + + See Also + ======== + + bspline_basis_set + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/B-spline + + """ + # make sure x has no assumptions so conditions don't evaluate + xvar = x + x = Dummy() + + knots = tuple(sympify(k) for k in knots) + d = int(d) + n = int(n) + n_knots = len(knots) + n_intervals = n_knots - 1 + if n + d + 1 > n_intervals: + raise ValueError("n + d + 1 must not exceed len(knots) - 1") + if d == 0: + result = Piecewise( + (S.One, Interval(knots[n], knots[n + 1]).contains(x)), (0, True) + ) + elif d > 0: + denom = knots[n + d + 1] - knots[n + 1] + if denom != S.Zero: + B = (knots[n + d + 1] - x) / denom + b2 = bspline_basis(d - 1, knots, n + 1, x) + else: + b2 = B = S.Zero + + denom = knots[n + d] - knots[n] + if denom != S.Zero: + A = (x - knots[n]) / denom + b1 = bspline_basis(d - 1, knots, n, x) + else: + b1 = A = S.Zero + + result = _add_splines(A, b1, B, b2, x) + else: + raise ValueError("degree must be non-negative: %r" % n) + + # return result with user-given x + return result.xreplace({x: xvar}) + + +def bspline_basis_set(d, knots, x): + """ + Return the ``len(knots)-d-1`` B-splines at *x* of degree *d* + with *knots*. + + Explanation + =========== + + This function returns a list of piecewise polynomials that are the + ``len(knots)-d-1`` B-splines of degree *d* for the given knots. + This function calls ``bspline_basis(d, knots, n, x)`` for different + values of *n*. + + Examples + ======== + + >>> from sympy import bspline_basis_set + >>> from sympy.abc import x + >>> d = 2 + >>> knots = range(5) + >>> splines = bspline_basis_set(d, knots, x) + >>> splines + [Piecewise((x**2/2, (x >= 0) & (x <= 1)), + (-x**2 + 3*x - 3/2, (x >= 1) & (x <= 2)), + (x**2/2 - 3*x + 9/2, (x >= 2) & (x <= 3)), + (0, True)), + Piecewise((x**2/2 - x + 1/2, (x >= 1) & (x <= 2)), + (-x**2 + 5*x - 11/2, (x >= 2) & (x <= 3)), + (x**2/2 - 4*x + 8, (x >= 3) & (x <= 4)), + (0, True))] + + Parameters + ========== + + d : integer + degree of bspline + + knots : list of integers + list of knots points of bspline + + x : symbol + + See Also + ======== + + bspline_basis + + """ + n_splines = len(knots) - d - 1 + return [bspline_basis(d, tuple(knots), i, x) for i in range(n_splines)] + + +def interpolating_spline(d, x, X, Y): + """ + Return spline of degree *d*, passing through the given *X* + and *Y* values. + + Explanation + =========== + + This function returns a piecewise function such that each part is + a polynomial of degree not greater than *d*. The value of *d* + must be 1 or greater and the values of *X* must be strictly + increasing. + + Examples + ======== + + >>> from sympy import interpolating_spline + >>> from sympy.abc import x + >>> interpolating_spline(1, x, [1, 2, 4, 7], [3, 6, 5, 7]) + Piecewise((3*x, (x >= 1) & (x <= 2)), + (7 - x/2, (x >= 2) & (x <= 4)), + (2*x/3 + 7/3, (x >= 4) & (x <= 7))) + >>> interpolating_spline(3, x, [-2, 0, 1, 3, 4], [4, 2, 1, 1, 3]) + Piecewise((7*x**3/117 + 7*x**2/117 - 131*x/117 + 2, (x >= -2) & (x <= 1)), + (10*x**3/117 - 2*x**2/117 - 122*x/117 + 77/39, (x >= 1) & (x <= 4))) + + Parameters + ========== + + d : integer + Degree of Bspline strictly greater than equal to one + + x : symbol + + X : list of strictly increasing real values + list of X coordinates through which the spline passes + + Y : list of real values + list of corresponding Y coordinates through which the spline passes + + See Also + ======== + + bspline_basis_set, interpolating_poly + + """ + from sympy.solvers.solveset import linsolve + from sympy.matrices.dense import Matrix + + # Input sanitization + d = sympify(d) + if not (d.is_Integer and d.is_positive): + raise ValueError("Spline degree must be a positive integer, not %s." % d) + if len(X) != len(Y): + raise ValueError("Number of X and Y coordinates must be the same.") + if len(X) < d + 1: + raise ValueError("Degree must be less than the number of control points.") + if not all(a < b for a, b in zip(X, X[1:])): + raise ValueError("The x-coordinates must be strictly increasing.") + X = [sympify(i) for i in X] + + # Evaluating knots value + if d.is_odd: + j = (d + 1) // 2 + interior_knots = X[j:-j] + else: + j = d // 2 + interior_knots = [ + (a + b)/2 for a, b in zip(X[j : -j - 1], X[j + 1 : -j]) + ] + + knots = [X[0]] * (d + 1) + list(interior_knots) + [X[-1]] * (d + 1) + + basis = bspline_basis_set(d, knots, x) + + A = [[b.subs(x, v) for b in basis] for v in X] + + coeff = linsolve((Matrix(A), Matrix(Y)), symbols("c0:{}".format(len(X)), cls=Dummy)) + coeff = list(coeff)[0] + intervals = {c for b in basis for (e, c) in b.args if c != True} + + # Sorting the intervals + # ival contains the end-points of each interval + intervals = sorted(intervals, key=lambda c: _ivl(c, x)) + + basis_dicts = [{c: e for (e, c) in b.args} for b in basis] + spline = [] + for i in intervals: + piece = sum( + [c * d.get(i, S.Zero) for (c, d) in zip(coeff, basis_dicts)], S.Zero + ) + spline.append((piece, i)) + return Piecewise(*spline) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/delta_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/delta_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..698d8c0c3ba5e68c2f084e83e7a9ec070ce83307 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/delta_functions.py @@ -0,0 +1,664 @@ +from sympy.core import S, diff +from sympy.core.function import DefinedFunction, ArgumentIndexError +from sympy.core.logic import fuzzy_not +from sympy.core.relational import Eq, Ne +from sympy.functions.elementary.complexes import im, sign +from sympy.functions.elementary.piecewise import Piecewise +from sympy.polys.polyerrors import PolynomialError +from sympy.polys.polyroots import roots +from sympy.utilities.misc import filldedent + + +############################################################################### +################################ DELTA FUNCTION ############################### +############################################################################### + + +class DiracDelta(DefinedFunction): + r""" + The DiracDelta function and its derivatives. + + Explanation + =========== + + DiracDelta is not an ordinary function. It can be rigorously defined either + as a distribution or as a measure. + + DiracDelta only makes sense in definite integrals, and in particular, + integrals of the form ``Integral(f(x)*DiracDelta(x - x0), (x, a, b))``, + where it equals ``f(x0)`` if ``a <= x0 <= b`` and ``0`` otherwise. Formally, + DiracDelta acts in some ways like a function that is ``0`` everywhere except + at ``0``, but in many ways it also does not. It can often be useful to treat + DiracDelta in formal ways, building up and manipulating expressions with + delta functions (which may eventually be integrated), but care must be taken + to not treat it as a real function. SymPy's ``oo`` is similar. It only + truly makes sense formally in certain contexts (such as integration limits), + but SymPy allows its use everywhere, and it tries to be consistent with + operations on it (like ``1/oo``), but it is easy to get into trouble and get + wrong results if ``oo`` is treated too much like a number. Similarly, if + DiracDelta is treated too much like a function, it is easy to get wrong or + nonsensical results. + + DiracDelta function has the following properties: + + 1) $\frac{d}{d x} \theta(x) = \delta(x)$ + 2) $\int_{-\infty}^\infty \delta(x - a)f(x)\, dx = f(a)$ and $\int_{a- + \epsilon}^{a+\epsilon} \delta(x - a)f(x)\, dx = f(a)$ + 3) $\delta(x) = 0$ for all $x \neq 0$ + 4) $\delta(g(x)) = \sum_i \frac{\delta(x - x_i)}{\|g'(x_i)\|}$ where $x_i$ + are the roots of $g$ + 5) $\delta(-x) = \delta(x)$ + + Derivatives of ``k``-th order of DiracDelta have the following properties: + + 6) $\delta(x, k) = 0$ for all $x \neq 0$ + 7) $\delta(-x, k) = -\delta(x, k)$ for odd $k$ + 8) $\delta(-x, k) = \delta(x, k)$ for even $k$ + + Examples + ======== + + >>> from sympy import DiracDelta, diff, pi + >>> from sympy.abc import x, y + + >>> DiracDelta(x) + DiracDelta(x) + >>> DiracDelta(1) + 0 + >>> DiracDelta(-1) + 0 + >>> DiracDelta(pi) + 0 + >>> DiracDelta(x - 4).subs(x, 4) + DiracDelta(0) + >>> diff(DiracDelta(x)) + DiracDelta(x, 1) + >>> diff(DiracDelta(x - 1), x, 2) + DiracDelta(x - 1, 2) + >>> diff(DiracDelta(x**2 - 1), x, 2) + 2*(2*x**2*DiracDelta(x**2 - 1, 2) + DiracDelta(x**2 - 1, 1)) + >>> DiracDelta(3*x).is_simple(x) + True + >>> DiracDelta(x**2).is_simple(x) + False + >>> DiracDelta((x**2 - 1)*y).expand(diracdelta=True, wrt=x) + DiracDelta(x - 1)/(2*Abs(y)) + DiracDelta(x + 1)/(2*Abs(y)) + + See Also + ======== + + Heaviside + sympy.simplify.simplify.simplify, is_simple + sympy.functions.special.tensor_functions.KroneckerDelta + + References + ========== + + .. [1] https://mathworld.wolfram.com/DeltaFunction.html + + """ + + is_real = True + + def fdiff(self, argindex=1): + """ + Returns the first derivative of a DiracDelta Function. + + Explanation + =========== + + The difference between ``diff()`` and ``fdiff()`` is: ``diff()`` is the + user-level function and ``fdiff()`` is an object method. ``fdiff()`` is + a convenience method available in the ``Function`` class. It returns + the derivative of the function without considering the chain rule. + ``diff(function, x)`` calls ``Function._eval_derivative`` which in turn + calls ``fdiff()`` internally to compute the derivative of the function. + + Examples + ======== + + >>> from sympy import DiracDelta, diff + >>> from sympy.abc import x + + >>> DiracDelta(x).fdiff() + DiracDelta(x, 1) + + >>> DiracDelta(x, 1).fdiff() + DiracDelta(x, 2) + + >>> DiracDelta(x**2 - 1).fdiff() + DiracDelta(x**2 - 1, 1) + + >>> diff(DiracDelta(x, 1)).fdiff() + DiracDelta(x, 3) + + Parameters + ========== + + argindex : integer + degree of derivative + + """ + if argindex == 1: + #I didn't know if there is a better way to handle default arguments + k = 0 + if len(self.args) > 1: + k = self.args[1] + return self.func(self.args[0], k + 1) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, arg, k=S.Zero): + """ + Returns a simplified form or a value of DiracDelta depending on the + argument passed by the DiracDelta object. + + Explanation + =========== + + The ``eval()`` method is automatically called when the ``DiracDelta`` + class is about to be instantiated and it returns either some simplified + instance or the unevaluated instance depending on the argument passed. + In other words, ``eval()`` method is not needed to be called explicitly, + it is being called and evaluated once the object is called. + + Examples + ======== + + >>> from sympy import DiracDelta, S + >>> from sympy.abc import x + + >>> DiracDelta(x) + DiracDelta(x) + + >>> DiracDelta(-x, 1) + -DiracDelta(x, 1) + + >>> DiracDelta(1) + 0 + + >>> DiracDelta(5, 1) + 0 + + >>> DiracDelta(0) + DiracDelta(0) + + >>> DiracDelta(-1) + 0 + + >>> DiracDelta(S.NaN) + nan + + >>> DiracDelta(x - 100).subs(x, 5) + 0 + + >>> DiracDelta(x - 100).subs(x, 100) + DiracDelta(0) + + Parameters + ========== + + k : integer + order of derivative + + arg : argument passed to DiracDelta + + """ + if not k.is_Integer or k.is_negative: + raise ValueError("Error: the second argument of DiracDelta must be \ + a non-negative integer, %s given instead." % (k,)) + if arg is S.NaN: + return S.NaN + if arg.is_nonzero: + return S.Zero + if fuzzy_not(im(arg).is_zero): + raise ValueError(filldedent(''' + Function defined only for Real Values. + Complex part: %s found in %s .''' % ( + repr(im(arg)), repr(arg)))) + c, nc = arg.args_cnc() + if c and c[0] is S.NegativeOne: + # keep this fast and simple instead of using + # could_extract_minus_sign + if k.is_odd: + return -cls(-arg, k) + elif k.is_even: + return cls(-arg, k) if k else cls(-arg) + elif k.is_zero: + return cls(arg, evaluate=False) + + def _eval_expand_diracdelta(self, **hints): + """ + Compute a simplified representation of the function using + property number 4. Pass ``wrt`` as a hint to expand the expression + with respect to a particular variable. + + Explanation + =========== + + ``wrt`` is: + + - a variable with respect to which a DiracDelta expression will + get expanded. + + Examples + ======== + + >>> from sympy import DiracDelta + >>> from sympy.abc import x, y + + >>> DiracDelta(x*y).expand(diracdelta=True, wrt=x) + DiracDelta(x)/Abs(y) + >>> DiracDelta(x*y).expand(diracdelta=True, wrt=y) + DiracDelta(y)/Abs(x) + + >>> DiracDelta(x**2 + x - 2).expand(diracdelta=True, wrt=x) + DiracDelta(x - 1)/3 + DiracDelta(x + 2)/3 + + See Also + ======== + + is_simple, Diracdelta + + """ + wrt = hints.get('wrt', None) + if wrt is None: + free = self.free_symbols + if len(free) == 1: + wrt = free.pop() + else: + raise TypeError(filldedent(''' + When there is more than 1 free symbol or variable in the expression, + the 'wrt' keyword is required as a hint to expand when using the + DiracDelta hint.''')) + + if not self.args[0].has(wrt) or (len(self.args) > 1 and self.args[1] != 0 ): + return self + try: + argroots = roots(self.args[0], wrt) + result = 0 + valid = True + darg = abs(diff(self.args[0], wrt)) + for r, m in argroots.items(): + if r.is_real is not False and m == 1: + result += self.func(wrt - r)/darg.subs(wrt, r) + else: + # don't handle non-real and if m != 1 then + # a polynomial will have a zero in the derivative (darg) + # at r + valid = False + break + if valid: + return result + except PolynomialError: + pass + return self + + def is_simple(self, x): + """ + Tells whether the argument(args[0]) of DiracDelta is a linear + expression in *x*. + + Examples + ======== + + >>> from sympy import DiracDelta, cos + >>> from sympy.abc import x, y + + >>> DiracDelta(x*y).is_simple(x) + True + >>> DiracDelta(x*y).is_simple(y) + True + + >>> DiracDelta(x**2 + x - 2).is_simple(x) + False + + >>> DiracDelta(cos(x)).is_simple(x) + False + + Parameters + ========== + + x : can be a symbol + + See Also + ======== + + sympy.simplify.simplify.simplify, DiracDelta + + """ + p = self.args[0].as_poly(x) + if p: + return p.degree() == 1 + return False + + def _eval_rewrite_as_Piecewise(self, *args, **kwargs): + """ + Represents DiracDelta in a piecewise form. + + Examples + ======== + + >>> from sympy import DiracDelta, Piecewise, Symbol + >>> x = Symbol('x') + + >>> DiracDelta(x).rewrite(Piecewise) + Piecewise((DiracDelta(0), Eq(x, 0)), (0, True)) + + >>> DiracDelta(x - 5).rewrite(Piecewise) + Piecewise((DiracDelta(0), Eq(x, 5)), (0, True)) + + >>> DiracDelta(x**2 - 5).rewrite(Piecewise) + Piecewise((DiracDelta(0), Eq(x**2, 5)), (0, True)) + + >>> DiracDelta(x - 5, 4).rewrite(Piecewise) + DiracDelta(x - 5, 4) + + """ + if len(args) == 1: + return Piecewise((DiracDelta(0), Eq(args[0], 0)), (0, True)) + + def _eval_rewrite_as_SingularityFunction(self, *args, **kwargs): + """ + Returns the DiracDelta expression written in the form of Singularity + Functions. + + """ + from sympy.solvers import solve + from sympy.functions.special.singularity_functions import SingularityFunction + if self == DiracDelta(0): + return SingularityFunction(0, 0, -1) + if self == DiracDelta(0, 1): + return SingularityFunction(0, 0, -2) + free = self.free_symbols + if len(free) == 1: + x = (free.pop()) + if len(args) == 1: + return SingularityFunction(x, solve(args[0], x)[0], -1) + return SingularityFunction(x, solve(args[0], x)[0], -args[1] - 1) + else: + # I don't know how to handle the case for DiracDelta expressions + # having arguments with more than one variable. + raise TypeError(filldedent(''' + rewrite(SingularityFunction) does not support + arguments with more that one variable.''')) + + +############################################################################### +############################## HEAVISIDE FUNCTION ############################# +############################################################################### + + +class Heaviside(DefinedFunction): + r""" + Heaviside step function. + + Explanation + =========== + + The Heaviside step function has the following properties: + + 1) $\frac{d}{d x} \theta(x) = \delta(x)$ + 2) $\theta(x) = \begin{cases} 0 & \text{for}\: x < 0 \\ \frac{1}{2} & + \text{for}\: x = 0 \\1 & \text{for}\: x > 0 \end{cases}$ + 3) $\frac{d}{d x} \max(x, 0) = \theta(x)$ + + Heaviside(x) is printed as $\theta(x)$ with the SymPy LaTeX printer. + + The value at 0 is set differently in different fields. SymPy uses 1/2, + which is a convention from electronics and signal processing, and is + consistent with solving improper integrals by Fourier transform and + convolution. + + To specify a different value of Heaviside at ``x=0``, a second argument + can be given. Using ``Heaviside(x, nan)`` gives an expression that will + evaluate to nan for x=0. + + .. versionchanged:: 1.9 ``Heaviside(0)`` now returns 1/2 (before: undefined) + + Examples + ======== + + >>> from sympy import Heaviside, nan + >>> from sympy.abc import x + >>> Heaviside(9) + 1 + >>> Heaviside(-9) + 0 + >>> Heaviside(0) + 1/2 + >>> Heaviside(0, nan) + nan + >>> (Heaviside(x) + 1).replace(Heaviside(x), Heaviside(x, 1)) + Heaviside(x, 1) + 1 + + See Also + ======== + + DiracDelta + + References + ========== + + .. [1] https://mathworld.wolfram.com/HeavisideStepFunction.html + .. [2] https://dlmf.nist.gov/1.16#iv + + """ + + is_real = True + + def fdiff(self, argindex=1): + """ + Returns the first derivative of a Heaviside Function. + + Examples + ======== + + >>> from sympy import Heaviside, diff + >>> from sympy.abc import x + + >>> Heaviside(x).fdiff() + DiracDelta(x) + + >>> Heaviside(x**2 - 1).fdiff() + DiracDelta(x**2 - 1) + + >>> diff(Heaviside(x)).fdiff() + DiracDelta(x, 1) + + Parameters + ========== + + argindex : integer + order of derivative + + """ + if argindex == 1: + return DiracDelta(self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + def __new__(cls, arg, H0=S.Half, **options): + if isinstance(H0, Heaviside) and len(H0.args) == 1: + H0 = S.Half + return super(cls, cls).__new__(cls, arg, H0, **options) + + @property + def pargs(self): + """Args without default S.Half""" + args = self.args + if args[1] is S.Half: + args = args[:1] + return args + + @classmethod + def eval(cls, arg, H0=S.Half): + """ + Returns a simplified form or a value of Heaviside depending on the + argument passed by the Heaviside object. + + Explanation + =========== + + The ``eval()`` method is automatically called when the ``Heaviside`` + class is about to be instantiated and it returns either some simplified + instance or the unevaluated instance depending on the argument passed. + In other words, ``eval()`` method is not needed to be called explicitly, + it is being called and evaluated once the object is called. + + Examples + ======== + + >>> from sympy import Heaviside, S + >>> from sympy.abc import x + + >>> Heaviside(x) + Heaviside(x) + + >>> Heaviside(19) + 1 + + >>> Heaviside(0) + 1/2 + + >>> Heaviside(0, 1) + 1 + + >>> Heaviside(-5) + 0 + + >>> Heaviside(S.NaN) + nan + + >>> Heaviside(x - 100).subs(x, 5) + 0 + + >>> Heaviside(x - 100).subs(x, 105) + 1 + + Parameters + ========== + + arg : argument passed by Heaviside object + + H0 : value of Heaviside(0) + + """ + if arg.is_extended_negative: + return S.Zero + elif arg.is_extended_positive: + return S.One + elif arg.is_zero: + return H0 + elif arg is S.NaN: + return S.NaN + elif fuzzy_not(im(arg).is_zero): + raise ValueError("Function defined only for Real Values. Complex part: %s found in %s ." % (repr(im(arg)), repr(arg)) ) + + def _eval_rewrite_as_Piecewise(self, arg, H0=None, **kwargs): + """ + Represents Heaviside in a Piecewise form. + + Examples + ======== + + >>> from sympy import Heaviside, Piecewise, Symbol, nan + >>> x = Symbol('x') + + >>> Heaviside(x).rewrite(Piecewise) + Piecewise((0, x < 0), (1/2, Eq(x, 0)), (1, True)) + + >>> Heaviside(x,nan).rewrite(Piecewise) + Piecewise((0, x < 0), (nan, Eq(x, 0)), (1, True)) + + >>> Heaviside(x - 5).rewrite(Piecewise) + Piecewise((0, x < 5), (1/2, Eq(x, 5)), (1, True)) + + >>> Heaviside(x**2 - 1).rewrite(Piecewise) + Piecewise((0, x**2 < 1), (1/2, Eq(x**2, 1)), (1, True)) + + """ + if H0 == 0: + return Piecewise((0, arg <= 0), (1, True)) + if H0 == 1: + return Piecewise((0, arg < 0), (1, True)) + return Piecewise((0, arg < 0), (H0, Eq(arg, 0)), (1, True)) + + def _eval_rewrite_as_sign(self, arg, H0=S.Half, **kwargs): + """ + Represents the Heaviside function in the form of sign function. + + Explanation + =========== + + The value of Heaviside(0) must be 1/2 for rewriting as sign to be + strictly equivalent. For easier usage, we also allow this rewriting + when Heaviside(0) is undefined. + + Examples + ======== + + >>> from sympy import Heaviside, Symbol, sign, nan + >>> x = Symbol('x', real=True) + >>> y = Symbol('y') + + >>> Heaviside(x).rewrite(sign) + sign(x)/2 + 1/2 + + >>> Heaviside(x, 0).rewrite(sign) + Piecewise((sign(x)/2 + 1/2, Ne(x, 0)), (0, True)) + + >>> Heaviside(x, nan).rewrite(sign) + Piecewise((sign(x)/2 + 1/2, Ne(x, 0)), (nan, True)) + + >>> Heaviside(x - 2).rewrite(sign) + sign(x - 2)/2 + 1/2 + + >>> Heaviside(x**2 - 2*x + 1).rewrite(sign) + sign(x**2 - 2*x + 1)/2 + 1/2 + + >>> Heaviside(y).rewrite(sign) + Heaviside(y) + + >>> Heaviside(y**2 - 2*y + 1).rewrite(sign) + Heaviside(y**2 - 2*y + 1) + + See Also + ======== + + sign + + """ + if arg.is_extended_real: + pw1 = Piecewise( + ((sign(arg) + 1)/2, Ne(arg, 0)), + (Heaviside(0, H0=H0), True)) + pw2 = Piecewise( + ((sign(arg) + 1)/2, Eq(Heaviside(0, H0=H0), S.Half)), + (pw1, True)) + return pw2 + + def _eval_rewrite_as_SingularityFunction(self, args, H0=S.Half, **kwargs): + """ + Returns the Heaviside expression written in the form of Singularity + Functions. + + """ + from sympy.solvers import solve + from sympy.functions.special.singularity_functions import SingularityFunction + if self == Heaviside(0): + return SingularityFunction(0, 0, 0) + free = self.free_symbols + if len(free) == 1: + x = (free.pop()) + return SingularityFunction(x, solve(args, x)[0], 0) + # TODO + # ((x - 5)**3*Heaviside(x - 5)).rewrite(SingularityFunction) should output + # SingularityFunction(x, 5, 0) instead of (x - 5)**3*SingularityFunction(x, 5, 0) + else: + # I don't know how to handle the case for Heaviside expressions + # having arguments with more than one variable. + raise TypeError(filldedent(''' + rewrite(SingularityFunction) does not + support arguments with more that one variable.''')) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/elliptic_integrals.py b/venv/lib/python3.11/site-packages/sympy/functions/special/elliptic_integrals.py new file mode 100644 index 0000000000000000000000000000000000000000..a94e343c0106891db44668ae05c33e84ecd05d0b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/elliptic_integrals.py @@ -0,0 +1,445 @@ +""" Elliptic Integrals. """ + +from sympy.core import S, pi, I, Rational +from sympy.core.function import DefinedFunction, ArgumentIndexError +from sympy.core.symbol import Dummy,uniquely_named_symbol +from sympy.functions.elementary.complexes import sign +from sympy.functions.elementary.hyperbolic import atanh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin, tan +from sympy.functions.special.gamma_functions import gamma +from sympy.functions.special.hyper import hyper, meijerg + +class elliptic_k(DefinedFunction): + r""" + The complete elliptic integral of the first kind, defined by + + .. math:: K(m) = F\left(\tfrac{\pi}{2}\middle| m\right) + + where $F\left(z\middle| m\right)$ is the Legendre incomplete + elliptic integral of the first kind. + + Explanation + =========== + + The function $K(m)$ is a single-valued function on the complex + plane with branch cut along the interval $(1, \infty)$. + + Note that our notation defines the incomplete elliptic integral + in terms of the parameter $m$ instead of the elliptic modulus + (eccentricity) $k$. + In this case, the parameter $m$ is defined as $m=k^2$. + + Examples + ======== + + >>> from sympy import elliptic_k, I + >>> from sympy.abc import m + >>> elliptic_k(0) + pi/2 + >>> elliptic_k(1.0 + I) + 1.50923695405127 + 0.625146415202697*I + >>> elliptic_k(m).series(n=3) + pi/2 + pi*m/8 + 9*pi*m**2/128 + O(m**3) + + See Also + ======== + + elliptic_f + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Elliptic_integrals + .. [2] https://functions.wolfram.com/EllipticIntegrals/EllipticK + + """ + + @classmethod + def eval(cls, m): + if m.is_zero: + return pi*S.Half + elif m is S.Half: + return 8*pi**Rational(3, 2)/gamma(Rational(-1, 4))**2 + elif m is S.One: + return S.ComplexInfinity + elif m is S.NegativeOne: + return gamma(Rational(1, 4))**2/(4*sqrt(2*pi)) + elif m in (S.Infinity, S.NegativeInfinity, I*S.Infinity, + I*S.NegativeInfinity, S.ComplexInfinity): + return S.Zero + + def fdiff(self, argindex=1): + m = self.args[0] + return (elliptic_e(m) - (1 - m)*elliptic_k(m))/(2*m*(1 - m)) + + def _eval_conjugate(self): + m = self.args[0] + if (m.is_real and (m - 1).is_positive) is False: + return self.func(m.conjugate()) + + def _eval_nseries(self, x, n, logx, cdir=0): + from sympy.simplify import hyperexpand + return hyperexpand(self.rewrite(hyper)._eval_nseries(x, n=n, logx=logx)) + + def _eval_rewrite_as_hyper(self, m, **kwargs): + return pi*S.Half*hyper((S.Half, S.Half), (S.One,), m) + + def _eval_rewrite_as_meijerg(self, m, **kwargs): + return meijerg(((S.Half, S.Half), []), ((S.Zero,), (S.Zero,)), -m)/2 + + def _eval_is_zero(self): + m = self.args[0] + if m.is_infinite: + return True + + def _eval_rewrite_as_Integral(self, *args, **kwargs): + from sympy.integrals.integrals import Integral + t = Dummy(uniquely_named_symbol('t', args).name) + m = self.args[0] + return Integral(1/sqrt(1 - m*sin(t)**2), (t, 0, pi/2)) + + +class elliptic_f(DefinedFunction): + r""" + The Legendre incomplete elliptic integral of the first + kind, defined by + + .. math:: F\left(z\middle| m\right) = + \int_0^z \frac{dt}{\sqrt{1 - m \sin^2 t}} + + Explanation + =========== + + This function reduces to a complete elliptic integral of + the first kind, $K(m)$, when $z = \pi/2$. + + Note that our notation defines the incomplete elliptic integral + in terms of the parameter $m$ instead of the elliptic modulus + (eccentricity) $k$. + In this case, the parameter $m$ is defined as $m=k^2$. + + Examples + ======== + + >>> from sympy import elliptic_f, I + >>> from sympy.abc import z, m + >>> elliptic_f(z, m).series(z) + z + z**5*(3*m**2/40 - m/30) + m*z**3/6 + O(z**6) + >>> elliptic_f(3.0 + I/2, 1.0 + I) + 2.909449841483 + 1.74720545502474*I + + See Also + ======== + + elliptic_k + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Elliptic_integrals + .. [2] https://functions.wolfram.com/EllipticIntegrals/EllipticF + + """ + + @classmethod + def eval(cls, z, m): + if z.is_zero: + return S.Zero + if m.is_zero: + return z + k = 2*z/pi + if k.is_integer: + return k*elliptic_k(m) + elif m in (S.Infinity, S.NegativeInfinity): + return S.Zero + elif z.could_extract_minus_sign(): + return -elliptic_f(-z, m) + + def fdiff(self, argindex=1): + z, m = self.args + fm = sqrt(1 - m*sin(z)**2) + if argindex == 1: + return 1/fm + elif argindex == 2: + return (elliptic_e(z, m)/(2*m*(1 - m)) - elliptic_f(z, m)/(2*m) - + sin(2*z)/(4*(1 - m)*fm)) + raise ArgumentIndexError(self, argindex) + + def _eval_conjugate(self): + z, m = self.args + if (m.is_real and (m - 1).is_positive) is False: + return self.func(z.conjugate(), m.conjugate()) + + def _eval_rewrite_as_Integral(self, *args, **kwargs): + from sympy.integrals.integrals import Integral + t = Dummy(uniquely_named_symbol('t', args).name) + z, m = self.args[0], self.args[1] + return Integral(1/(sqrt(1 - m*sin(t)**2)), (t, 0, z)) + + def _eval_is_zero(self): + z, m = self.args + if z.is_zero: + return True + if m.is_extended_real and m.is_infinite: + return True + + +class elliptic_e(DefinedFunction): + r""" + Called with two arguments $z$ and $m$, evaluates the + incomplete elliptic integral of the second kind, defined by + + .. math:: E\left(z\middle| m\right) = \int_0^z \sqrt{1 - m \sin^2 t} dt + + Called with a single argument $m$, evaluates the Legendre complete + elliptic integral of the second kind + + .. math:: E(m) = E\left(\tfrac{\pi}{2}\middle| m\right) + + Explanation + =========== + + The function $E(m)$ is a single-valued function on the complex + plane with branch cut along the interval $(1, \infty)$. + + Note that our notation defines the incomplete elliptic integral + in terms of the parameter $m$ instead of the elliptic modulus + (eccentricity) $k$. + In this case, the parameter $m$ is defined as $m=k^2$. + + Examples + ======== + + >>> from sympy import elliptic_e, I + >>> from sympy.abc import z, m + >>> elliptic_e(z, m).series(z) + z + z**5*(-m**2/40 + m/30) - m*z**3/6 + O(z**6) + >>> elliptic_e(m).series(n=4) + pi/2 - pi*m/8 - 3*pi*m**2/128 - 5*pi*m**3/512 + O(m**4) + >>> elliptic_e(1 + I, 2 - I/2).n() + 1.55203744279187 + 0.290764986058437*I + >>> elliptic_e(0) + pi/2 + >>> elliptic_e(2.0 - I) + 0.991052601328069 + 0.81879421395609*I + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Elliptic_integrals + .. [2] https://functions.wolfram.com/EllipticIntegrals/EllipticE2 + .. [3] https://functions.wolfram.com/EllipticIntegrals/EllipticE + + """ + + @classmethod + def eval(cls, m, z=None): + if z is not None: + z, m = m, z + k = 2*z/pi + if m.is_zero: + return z + if z.is_zero: + return S.Zero + elif k.is_integer: + return k*elliptic_e(m) + elif m in (S.Infinity, S.NegativeInfinity): + return S.ComplexInfinity + elif z.could_extract_minus_sign(): + return -elliptic_e(-z, m) + else: + if m.is_zero: + return pi/2 + elif m is S.One: + return S.One + elif m is S.Infinity: + return I*S.Infinity + elif m is S.NegativeInfinity: + return S.Infinity + elif m is S.ComplexInfinity: + return S.ComplexInfinity + + def fdiff(self, argindex=1): + if len(self.args) == 2: + z, m = self.args + if argindex == 1: + return sqrt(1 - m*sin(z)**2) + elif argindex == 2: + return (elliptic_e(z, m) - elliptic_f(z, m))/(2*m) + else: + m = self.args[0] + if argindex == 1: + return (elliptic_e(m) - elliptic_k(m))/(2*m) + raise ArgumentIndexError(self, argindex) + + def _eval_conjugate(self): + if len(self.args) == 2: + z, m = self.args + if (m.is_real and (m - 1).is_positive) is False: + return self.func(z.conjugate(), m.conjugate()) + else: + m = self.args[0] + if (m.is_real and (m - 1).is_positive) is False: + return self.func(m.conjugate()) + + def _eval_nseries(self, x, n, logx, cdir=0): + from sympy.simplify import hyperexpand + if len(self.args) == 1: + return hyperexpand(self.rewrite(hyper)._eval_nseries(x, n=n, logx=logx)) + return super()._eval_nseries(x, n=n, logx=logx) + + def _eval_rewrite_as_hyper(self, *args, **kwargs): + if len(args) == 1: + m = args[0] + return (pi/2)*hyper((Rational(-1, 2), S.Half), (S.One,), m) + + def _eval_rewrite_as_meijerg(self, *args, **kwargs): + if len(args) == 1: + m = args[0] + return -meijerg(((S.Half, Rational(3, 2)), []), \ + ((S.Zero,), (S.Zero,)), -m)/4 + + def _eval_rewrite_as_Integral(self, *args, **kwargs): + from sympy.integrals.integrals import Integral + z, m = (pi/2, self.args[0]) if len(self.args) == 1 else self.args + t = Dummy(uniquely_named_symbol('t', args).name) + return Integral(sqrt(1 - m*sin(t)**2), (t, 0, z)) + + +class elliptic_pi(DefinedFunction): + r""" + Called with three arguments $n$, $z$ and $m$, evaluates the + Legendre incomplete elliptic integral of the third kind, defined by + + .. math:: \Pi\left(n; z\middle| m\right) = \int_0^z \frac{dt} + {\left(1 - n \sin^2 t\right) \sqrt{1 - m \sin^2 t}} + + Called with two arguments $n$ and $m$, evaluates the complete + elliptic integral of the third kind: + + .. math:: \Pi\left(n\middle| m\right) = + \Pi\left(n; \tfrac{\pi}{2}\middle| m\right) + + Explanation + =========== + + Note that our notation defines the incomplete elliptic integral + in terms of the parameter $m$ instead of the elliptic modulus + (eccentricity) $k$. + In this case, the parameter $m$ is defined as $m=k^2$. + + Examples + ======== + + >>> from sympy import elliptic_pi, I + >>> from sympy.abc import z, n, m + >>> elliptic_pi(n, z, m).series(z, n=4) + z + z**3*(m/6 + n/3) + O(z**4) + >>> elliptic_pi(0.5 + I, 1.0 - I, 1.2) + 2.50232379629182 - 0.760939574180767*I + >>> elliptic_pi(0, 0) + pi/2 + >>> elliptic_pi(1.0 - I/3, 2.0 + I) + 3.29136443417283 + 0.32555634906645*I + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Elliptic_integrals + .. [2] https://functions.wolfram.com/EllipticIntegrals/EllipticPi3 + .. [3] https://functions.wolfram.com/EllipticIntegrals/EllipticPi + + """ + + @classmethod + def eval(cls, n, m, z=None): + if z is not None: + z, m = m, z + if n.is_zero: + return elliptic_f(z, m) + elif n is S.One: + return (elliptic_f(z, m) + + (sqrt(1 - m*sin(z)**2)*tan(z) - + elliptic_e(z, m))/(1 - m)) + k = 2*z/pi + if k.is_integer: + return k*elliptic_pi(n, m) + elif m.is_zero: + return atanh(sqrt(n - 1)*tan(z))/sqrt(n - 1) + elif n == m: + return (elliptic_f(z, n) - elliptic_pi(1, z, n) + + tan(z)/sqrt(1 - n*sin(z)**2)) + elif n in (S.Infinity, S.NegativeInfinity): + return S.Zero + elif m in (S.Infinity, S.NegativeInfinity): + return S.Zero + elif z.could_extract_minus_sign(): + return -elliptic_pi(n, -z, m) + if n.is_zero: + return elliptic_f(z, m) + if m.is_extended_real and m.is_infinite or \ + n.is_extended_real and n.is_infinite: + return S.Zero + else: + if n.is_zero: + return elliptic_k(m) + elif n is S.One: + return S.ComplexInfinity + elif m.is_zero: + return pi/(2*sqrt(1 - n)) + elif m == S.One: + return S.NegativeInfinity/sign(n - 1) + elif n == m: + return elliptic_e(n)/(1 - n) + elif n in (S.Infinity, S.NegativeInfinity): + return S.Zero + elif m in (S.Infinity, S.NegativeInfinity): + return S.Zero + if n.is_zero: + return elliptic_k(m) + if m.is_extended_real and m.is_infinite or \ + n.is_extended_real and n.is_infinite: + return S.Zero + + def _eval_conjugate(self): + if len(self.args) == 3: + n, z, m = self.args + if (n.is_real and (n - 1).is_positive) is False and \ + (m.is_real and (m - 1).is_positive) is False: + return self.func(n.conjugate(), z.conjugate(), m.conjugate()) + else: + n, m = self.args + return self.func(n.conjugate(), m.conjugate()) + + def fdiff(self, argindex=1): + if len(self.args) == 3: + n, z, m = self.args + fm, fn = sqrt(1 - m*sin(z)**2), 1 - n*sin(z)**2 + if argindex == 1: + return (elliptic_e(z, m) + (m - n)*elliptic_f(z, m)/n + + (n**2 - m)*elliptic_pi(n, z, m)/n - + n*fm*sin(2*z)/(2*fn))/(2*(m - n)*(n - 1)) + elif argindex == 2: + return 1/(fm*fn) + elif argindex == 3: + return (elliptic_e(z, m)/(m - 1) + + elliptic_pi(n, z, m) - + m*sin(2*z)/(2*(m - 1)*fm))/(2*(n - m)) + else: + n, m = self.args + if argindex == 1: + return (elliptic_e(m) + (m - n)*elliptic_k(m)/n + + (n**2 - m)*elliptic_pi(n, m)/n)/(2*(m - n)*(n - 1)) + elif argindex == 2: + return (elliptic_e(m)/(m - 1) + elliptic_pi(n, m))/(2*(n - m)) + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Integral(self, *args, **kwargs): + from sympy.integrals.integrals import Integral + if len(self.args) == 2: + n, m, z = self.args[0], self.args[1], pi/2 + else: + n, z, m = self.args + t = Dummy(uniquely_named_symbol('t', args).name) + return Integral(1/((1 - n*sin(t)**2)*sqrt(1 - m*sin(t)**2)), (t, 0, z)) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/error_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/error_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..a778127d8b80583a892285fadbb8230f72de39f9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/error_functions.py @@ -0,0 +1,2801 @@ +""" This module contains various functions that are special cases + of incomplete gamma functions. It should probably be renamed. """ + +from sympy.core import EulerGamma # Must be imported from core, not core.numbers +from sympy.core.add import Add +from sympy.core.cache import cacheit +from sympy.core.function import DefinedFunction, ArgumentIndexError, expand_mul +from sympy.core.logic import fuzzy_or +from sympy.core.numbers import I, pi, Rational, Integer +from sympy.core.relational import is_eq +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import Dummy, uniquely_named_symbol +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import factorial, factorial2, RisingFactorial +from sympy.functions.elementary.complexes import polar_lift, re, unpolarify +from sympy.functions.elementary.integers import ceiling, floor +from sympy.functions.elementary.miscellaneous import sqrt, root +from sympy.functions.elementary.exponential import exp, log, exp_polar +from sympy.functions.elementary.hyperbolic import cosh, sinh +from sympy.functions.elementary.trigonometric import cos, sin, sinc +from sympy.functions.special.hyper import hyper, meijerg + +# TODO series expansions +# TODO see the "Note:" in Ei + +# Helper function +def real_to_real_as_real_imag(self, deep=True, **hints): + if self.args[0].is_extended_real: + if deep: + hints['complex'] = False + return (self.expand(deep, **hints), S.Zero) + else: + return (self, S.Zero) + if deep: + x, y = self.args[0].expand(deep, **hints).as_real_imag() + else: + x, y = self.args[0].as_real_imag() + re = (self.func(x + I*y) + self.func(x - I*y))/2 + im = (self.func(x + I*y) - self.func(x - I*y))/(2*I) + return (re, im) + + +############################################################################### +################################ ERROR FUNCTION ############################### +############################################################################### + + +class erf(DefinedFunction): + r""" + The Gauss error function. + + Explanation + =========== + + This function is defined as: + + .. math :: + \mathrm{erf}(x) = \frac{2}{\sqrt{\pi}} \int_0^x e^{-t^2} \mathrm{d}t. + + Examples + ======== + + >>> from sympy import I, oo, erf + >>> from sympy.abc import z + + Several special values are known: + + >>> erf(0) + 0 + >>> erf(oo) + 1 + >>> erf(-oo) + -1 + >>> erf(I*oo) + oo*I + >>> erf(-I*oo) + -oo*I + + In general one can pull out factors of -1 and $I$ from the argument: + + >>> erf(-z) + -erf(z) + + The error function obeys the mirror symmetry: + + >>> from sympy import conjugate + >>> conjugate(erf(z)) + erf(conjugate(z)) + + Differentiation with respect to $z$ is supported: + + >>> from sympy import diff + >>> diff(erf(z), z) + 2*exp(-z**2)/sqrt(pi) + + We can numerically evaluate the error function to arbitrary precision + on the whole complex plane: + + >>> erf(4).evalf(30) + 0.999999984582742099719981147840 + + >>> erf(-4*I).evalf(30) + -1296959.73071763923152794095062*I + + See Also + ======== + + erfc: Complementary error function. + erfi: Imaginary error function. + erf2: Two-argument error function. + erfinv: Inverse error function. + erfcinv: Inverse Complementary error function. + erf2inv: Inverse two-argument error function. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Error_function + .. [2] https://dlmf.nist.gov/7 + .. [3] https://mathworld.wolfram.com/Erf.html + .. [4] https://functions.wolfram.com/GammaBetaErf/Erf + + """ + + unbranched = True + + def fdiff(self, argindex=1): + if argindex == 1: + return 2*exp(-self.args[0]**2)/sqrt(pi) + else: + raise ArgumentIndexError(self, argindex) + + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + + """ + return erfinv + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.One + elif arg is S.NegativeInfinity: + return S.NegativeOne + elif arg.is_zero: + return S.Zero + + if isinstance(arg, erfinv): + return arg.args[0] + + if isinstance(arg, erfcinv): + return S.One - arg.args[0] + + if arg.is_zero: + return S.Zero + + # Only happens with unevaluated erf2inv + if isinstance(arg, erf2inv) and arg.args[0].is_zero: + return arg.args[1] + + # Try to pull out factors of I + t = arg.extract_multiplicatively(I) + if t in (S.Infinity, S.NegativeInfinity): + return arg + + # Try to pull out factors of -1 + if arg.could_extract_minus_sign(): + return -cls(-arg) + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + k = floor((n - 1)/S(2)) + if len(previous_terms) > 2: + return -previous_terms[-2] * x**2 * (n - 2)/(n*k) + else: + return 2*S.NegativeOne**k * x**n/(n*factorial(k)*sqrt(pi)) + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def _eval_is_real(self): + if self.args[0].is_extended_real is True: + return True + # There are cases where erf(z) becomes a real number + # even if z is a complex number + + def _eval_is_imaginary(self): + if self.args[0].is_imaginary is True: + return True + + def _eval_is_finite(self): + z = self.args[0] + return fuzzy_or([z.is_finite, z.is_extended_real]) + + def _eval_is_zero(self): + if self.args[0].is_extended_real is True: + return self.args[0].is_zero + + def _eval_is_positive(self): + if self.args[0].is_extended_real is True: + return self.args[0].is_extended_positive + + def _eval_is_negative(self): + if self.args[0].is_extended_real is True: + return self.args[0].is_extended_negative + + def _eval_rewrite_as_uppergamma(self, z, **kwargs): + from sympy.functions.special.gamma_functions import uppergamma + return sqrt(z**2)/z*(S.One - uppergamma(S.Half, z**2)/sqrt(pi)) + + def _eval_rewrite_as_fresnels(self, z, **kwargs): + arg = (S.One - I)*z/sqrt(pi) + return (S.One + I)*(fresnelc(arg) - I*fresnels(arg)) + + def _eval_rewrite_as_fresnelc(self, z, **kwargs): + arg = (S.One - I)*z/sqrt(pi) + return (S.One + I)*(fresnelc(arg) - I*fresnels(arg)) + + def _eval_rewrite_as_meijerg(self, z, **kwargs): + return z/sqrt(pi)*meijerg([S.Half], [], [0], [Rational(-1, 2)], z**2) + + def _eval_rewrite_as_hyper(self, z, **kwargs): + return 2*z/sqrt(pi)*hyper([S.Half], [3*S.Half], -z**2) + + def _eval_rewrite_as_expint(self, z, **kwargs): + return sqrt(z**2)/z - z*expint(S.Half, z**2)/sqrt(pi) + + def _eval_rewrite_as_tractable(self, z, limitvar=None, **kwargs): + from sympy.series.limits import limit + if limitvar: + lim = limit(z, limitvar, S.Infinity) + if lim is S.NegativeInfinity: + return S.NegativeOne + _erfs(-z)*exp(-z**2) + return S.One - _erfs(z)*exp(-z**2) + + def _eval_rewrite_as_erfc(self, z, **kwargs): + return S.One - erfc(z) + + def _eval_rewrite_as_erfi(self, z, **kwargs): + return -I*erfi(I*z) + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0].as_leading_term(x, logx=logx, cdir=cdir) + arg0 = arg.subs(x, 0) + + if arg0 is S.ComplexInfinity: + arg0 = arg.limit(x, 0, dir='-' if cdir == -1 else '+') + if x in arg.free_symbols and arg0.is_zero: + return 2*arg/sqrt(pi) + else: + return self.func(arg0) + + def _eval_aseries(self, n, args0, x, logx): + from sympy.series.order import Order + point = args0[0] + + if point in [S.Infinity, S.NegativeInfinity]: + z = self.args[0] + + try: + _, ex = z.leadterm(x) + except (ValueError, NotImplementedError): + return self + + ex = -ex # as x->1/x for aseries + if ex.is_positive: + newn = ceiling(n/ex) + s = [S.NegativeOne**k * factorial2(2*k - 1) / (z**(2*k + 1) * 2**k) + for k in range(newn)] + [Order(1/z**newn, x)] + return S.One - (exp(-z**2)/sqrt(pi)) * Add(*s) + + return super(erf, self)._eval_aseries(n, args0, x, logx) + + as_real_imag = real_to_real_as_real_imag + + +class erfc(DefinedFunction): + r""" + Complementary Error Function. + + Explanation + =========== + + The function is defined as: + + .. math :: + \mathrm{erfc}(x) = \frac{2}{\sqrt{\pi}} \int_x^\infty e^{-t^2} \mathrm{d}t + + Examples + ======== + + >>> from sympy import I, oo, erfc + >>> from sympy.abc import z + + Several special values are known: + + >>> erfc(0) + 1 + >>> erfc(oo) + 0 + >>> erfc(-oo) + 2 + >>> erfc(I*oo) + -oo*I + >>> erfc(-I*oo) + oo*I + + The error function obeys the mirror symmetry: + + >>> from sympy import conjugate + >>> conjugate(erfc(z)) + erfc(conjugate(z)) + + Differentiation with respect to $z$ is supported: + + >>> from sympy import diff + >>> diff(erfc(z), z) + -2*exp(-z**2)/sqrt(pi) + + It also follows + + >>> erfc(-z) + 2 - erfc(z) + + We can numerically evaluate the complementary error function to arbitrary + precision on the whole complex plane: + + >>> erfc(4).evalf(30) + 0.0000000154172579002800188521596734869 + + >>> erfc(4*I).evalf(30) + 1.0 - 1296959.73071763923152794095062*I + + See Also + ======== + + erf: Gaussian error function. + erfi: Imaginary error function. + erf2: Two-argument error function. + erfinv: Inverse error function. + erfcinv: Inverse Complementary error function. + erf2inv: Inverse two-argument error function. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Error_function + .. [2] https://dlmf.nist.gov/7 + .. [3] https://mathworld.wolfram.com/Erfc.html + .. [4] https://functions.wolfram.com/GammaBetaErf/Erfc + + """ + + unbranched = True + + def fdiff(self, argindex=1): + if argindex == 1: + return -2*exp(-self.args[0]**2)/sqrt(pi) + else: + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + + """ + return erfcinv + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is S.Infinity: + return S.Zero + elif arg.is_zero: + return S.One + + if isinstance(arg, erfinv): + return S.One - arg.args[0] + + if isinstance(arg, erfcinv): + return arg.args[0] + + if arg.is_zero: + return S.One + + # Try to pull out factors of I + t = arg.extract_multiplicatively(I) + if t in (S.Infinity, S.NegativeInfinity): + return -arg + + # Try to pull out factors of -1 + if arg.could_extract_minus_sign(): + return 2 - cls(-arg) + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n == 0: + return S.One + elif n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + k = floor((n - 1)/S(2)) + if len(previous_terms) > 2: + return -previous_terms[-2] * x**2 * (n - 2)/(n*k) + else: + return -2*S.NegativeOne**k * x**n/(n*factorial(k)*sqrt(pi)) + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def _eval_is_real(self): + if self.args[0].is_extended_real is True: + return True + if self.args[0].is_imaginary is True: + return False + + def _eval_rewrite_as_tractable(self, z, limitvar=None, **kwargs): + return self.rewrite(erf).rewrite("tractable", deep=True, limitvar=limitvar) + + def _eval_rewrite_as_erf(self, z, **kwargs): + return S.One - erf(z) + + def _eval_rewrite_as_erfi(self, z, **kwargs): + return S.One + I*erfi(I*z) + + def _eval_rewrite_as_fresnels(self, z, **kwargs): + arg = (S.One - I)*z/sqrt(pi) + return S.One - (S.One + I)*(fresnelc(arg) - I*fresnels(arg)) + + def _eval_rewrite_as_fresnelc(self, z, **kwargs): + arg = (S.One-I)*z/sqrt(pi) + return S.One - (S.One + I)*(fresnelc(arg) - I*fresnels(arg)) + + def _eval_rewrite_as_meijerg(self, z, **kwargs): + return S.One - z/sqrt(pi)*meijerg([S.Half], [], [0], [Rational(-1, 2)], z**2) + + def _eval_rewrite_as_hyper(self, z, **kwargs): + return S.One - 2*z/sqrt(pi)*hyper([S.Half], [3*S.Half], -z**2) + + def _eval_rewrite_as_uppergamma(self, z, **kwargs): + from sympy.functions.special.gamma_functions import uppergamma + return S.One - sqrt(z**2)/z*(S.One - uppergamma(S.Half, z**2)/sqrt(pi)) + + def _eval_rewrite_as_expint(self, z, **kwargs): + return S.One - sqrt(z**2)/z + z*expint(S.Half, z**2)/sqrt(pi) + + def _eval_expand_func(self, **hints): + return self.rewrite(erf) + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0].as_leading_term(x, logx=logx, cdir=cdir) + arg0 = arg.subs(x, 0) + + if arg0 is S.ComplexInfinity: + arg0 = arg.limit(x, 0, dir='-' if cdir == -1 else '+') + if arg0.is_zero: + return S.One + else: + return self.func(arg0) + + as_real_imag = real_to_real_as_real_imag + + def _eval_aseries(self, n, args0, x, logx): + return S.One - erf(*self.args)._eval_aseries(n, args0, x, logx) + + +class erfi(DefinedFunction): + r""" + Imaginary error function. + + Explanation + =========== + + The function erfi is defined as: + + .. math :: + \mathrm{erfi}(x) = \frac{2}{\sqrt{\pi}} \int_0^x e^{t^2} \mathrm{d}t + + Examples + ======== + + >>> from sympy import I, oo, erfi + >>> from sympy.abc import z + + Several special values are known: + + >>> erfi(0) + 0 + >>> erfi(oo) + oo + >>> erfi(-oo) + -oo + >>> erfi(I*oo) + I + >>> erfi(-I*oo) + -I + + In general one can pull out factors of -1 and $I$ from the argument: + + >>> erfi(-z) + -erfi(z) + + >>> from sympy import conjugate + >>> conjugate(erfi(z)) + erfi(conjugate(z)) + + Differentiation with respect to $z$ is supported: + + >>> from sympy import diff + >>> diff(erfi(z), z) + 2*exp(z**2)/sqrt(pi) + + We can numerically evaluate the imaginary error function to arbitrary + precision on the whole complex plane: + + >>> erfi(2).evalf(30) + 18.5648024145755525987042919132 + + >>> erfi(-2*I).evalf(30) + -0.995322265018952734162069256367*I + + See Also + ======== + + erf: Gaussian error function. + erfc: Complementary error function. + erf2: Two-argument error function. + erfinv: Inverse error function. + erfcinv: Inverse Complementary error function. + erf2inv: Inverse two-argument error function. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Error_function + .. [2] https://mathworld.wolfram.com/Erfi.html + .. [3] https://functions.wolfram.com/GammaBetaErf/Erfi + + """ + + unbranched = True + + def fdiff(self, argindex=1): + if argindex == 1: + return 2*exp(self.args[0]**2)/sqrt(pi) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, z): + if z.is_Number: + if z is S.NaN: + return S.NaN + elif z.is_zero: + return S.Zero + elif z is S.Infinity: + return S.Infinity + + if z.is_zero: + return S.Zero + + # Try to pull out factors of -1 + if z.could_extract_minus_sign(): + return -cls(-z) + + # Try to pull out factors of I + nz = z.extract_multiplicatively(I) + if nz is not None: + if nz is S.Infinity: + return I + if isinstance(nz, erfinv): + return I*nz.args[0] + if isinstance(nz, erfcinv): + return I*(S.One - nz.args[0]) + # Only happens with unevaluated erf2inv + if isinstance(nz, erf2inv) and nz.args[0].is_zero: + return I*nz.args[1] + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0 or n % 2 == 0: + return S.Zero + else: + x = sympify(x) + k = floor((n - 1)/S(2)) + if len(previous_terms) > 2: + return previous_terms[-2] * x**2 * (n - 2)/(n*k) + else: + return 2 * x**n/(n*factorial(k)*sqrt(pi)) + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def _eval_is_extended_real(self): + return self.args[0].is_extended_real + + def _eval_is_zero(self): + return self.args[0].is_zero + + def _eval_rewrite_as_tractable(self, z, limitvar=None, **kwargs): + return self.rewrite(erf).rewrite("tractable", deep=True, limitvar=limitvar) + + def _eval_rewrite_as_erf(self, z, **kwargs): + return -I*erf(I*z) + + def _eval_rewrite_as_erfc(self, z, **kwargs): + return I*erfc(I*z) - I + + def _eval_rewrite_as_fresnels(self, z, **kwargs): + arg = (S.One + I)*z/sqrt(pi) + return (S.One - I)*(fresnelc(arg) - I*fresnels(arg)) + + def _eval_rewrite_as_fresnelc(self, z, **kwargs): + arg = (S.One + I)*z/sqrt(pi) + return (S.One - I)*(fresnelc(arg) - I*fresnels(arg)) + + def _eval_rewrite_as_meijerg(self, z, **kwargs): + return z/sqrt(pi)*meijerg([S.Half], [], [0], [Rational(-1, 2)], -z**2) + + def _eval_rewrite_as_hyper(self, z, **kwargs): + return 2*z/sqrt(pi)*hyper([S.Half], [3*S.Half], z**2) + + def _eval_rewrite_as_uppergamma(self, z, **kwargs): + from sympy.functions.special.gamma_functions import uppergamma + return sqrt(-z**2)/z*(uppergamma(S.Half, -z**2)/sqrt(pi) - S.One) + + def _eval_rewrite_as_expint(self, z, **kwargs): + return sqrt(-z**2)/z - z*expint(S.Half, -z**2)/sqrt(pi) + + def _eval_expand_func(self, **hints): + return self.rewrite(erf) + + as_real_imag = real_to_real_as_real_imag + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0].as_leading_term(x, logx=logx, cdir=cdir) + arg0 = arg.subs(x, 0) + + if x in arg.free_symbols and arg0.is_zero: + return 2*arg/sqrt(pi) + elif arg0.is_finite: + return self.func(arg0) + return self.func(arg) + + def _eval_aseries(self, n, args0, x, logx): + from sympy.series.order import Order + point = args0[0] + + if point is S.Infinity: + z = self.args[0] + s = [factorial2(2*k - 1) / (2**k * z**(2*k + 1)) + for k in range(n)] + [Order(1/z**n, x)] + return -I + (exp(z**2)/sqrt(pi)) * Add(*s) + + return super(erfi, self)._eval_aseries(n, args0, x, logx) + + +class erf2(DefinedFunction): + r""" + Two-argument error function. + + Explanation + =========== + + This function is defined as: + + .. math :: + \mathrm{erf2}(x, y) = \frac{2}{\sqrt{\pi}} \int_x^y e^{-t^2} \mathrm{d}t + + Examples + ======== + + >>> from sympy import oo, erf2 + >>> from sympy.abc import x, y + + Several special values are known: + + >>> erf2(0, 0) + 0 + >>> erf2(x, x) + 0 + >>> erf2(x, oo) + 1 - erf(x) + >>> erf2(x, -oo) + -erf(x) - 1 + >>> erf2(oo, y) + erf(y) - 1 + >>> erf2(-oo, y) + erf(y) + 1 + + In general one can pull out factors of -1: + + >>> erf2(-x, -y) + -erf2(x, y) + + The error function obeys the mirror symmetry: + + >>> from sympy import conjugate + >>> conjugate(erf2(x, y)) + erf2(conjugate(x), conjugate(y)) + + Differentiation with respect to $x$, $y$ is supported: + + >>> from sympy import diff + >>> diff(erf2(x, y), x) + -2*exp(-x**2)/sqrt(pi) + >>> diff(erf2(x, y), y) + 2*exp(-y**2)/sqrt(pi) + + See Also + ======== + + erf: Gaussian error function. + erfc: Complementary error function. + erfi: Imaginary error function. + erfinv: Inverse error function. + erfcinv: Inverse Complementary error function. + erf2inv: Inverse two-argument error function. + + References + ========== + + .. [1] https://functions.wolfram.com/GammaBetaErf/Erf2/ + + """ + + + def fdiff(self, argindex): + x, y = self.args + if argindex == 1: + return -2*exp(-x**2)/sqrt(pi) + elif argindex == 2: + return 2*exp(-y**2)/sqrt(pi) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, x, y): + chk = (S.Infinity, S.NegativeInfinity, S.Zero) + if x is S.NaN or y is S.NaN: + return S.NaN + elif x == y: + return S.Zero + elif x in chk or y in chk: + return erf(y) - erf(x) + + if isinstance(y, erf2inv) and y.args[0] == x: + return y.args[1] + + if x.is_zero or y.is_zero or x.is_extended_real and x.is_infinite or \ + y.is_extended_real and y.is_infinite: + return erf(y) - erf(x) + + #Try to pull out -1 factor + sign_x = x.could_extract_minus_sign() + sign_y = y.could_extract_minus_sign() + if (sign_x and sign_y): + return -cls(-x, -y) + elif (sign_x or sign_y): + return erf(y)-erf(x) + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate(), self.args[1].conjugate()) + + def _eval_is_extended_real(self): + return self.args[0].is_extended_real and self.args[1].is_extended_real + + def _eval_rewrite_as_erf(self, x, y, **kwargs): + return erf(y) - erf(x) + + def _eval_rewrite_as_erfc(self, x, y, **kwargs): + return erfc(x) - erfc(y) + + def _eval_rewrite_as_erfi(self, x, y, **kwargs): + return I*(erfi(I*x)-erfi(I*y)) + + def _eval_rewrite_as_fresnels(self, x, y, **kwargs): + return erf(y).rewrite(fresnels) - erf(x).rewrite(fresnels) + + def _eval_rewrite_as_fresnelc(self, x, y, **kwargs): + return erf(y).rewrite(fresnelc) - erf(x).rewrite(fresnelc) + + def _eval_rewrite_as_meijerg(self, x, y, **kwargs): + return erf(y).rewrite(meijerg) - erf(x).rewrite(meijerg) + + def _eval_rewrite_as_hyper(self, x, y, **kwargs): + return erf(y).rewrite(hyper) - erf(x).rewrite(hyper) + + def _eval_rewrite_as_uppergamma(self, x, y, **kwargs): + from sympy.functions.special.gamma_functions import uppergamma + return (sqrt(y**2)/y*(S.One - uppergamma(S.Half, y**2)/sqrt(pi)) - + sqrt(x**2)/x*(S.One - uppergamma(S.Half, x**2)/sqrt(pi))) + + def _eval_rewrite_as_expint(self, x, y, **kwargs): + return erf(y).rewrite(expint) - erf(x).rewrite(expint) + + def _eval_expand_func(self, **hints): + return self.rewrite(erf) + + def _eval_is_zero(self): + return is_eq(*self.args) + +class erfinv(DefinedFunction): + r""" + Inverse Error Function. The erfinv function is defined as: + + .. math :: + \mathrm{erf}(x) = y \quad \Rightarrow \quad \mathrm{erfinv}(y) = x + + Examples + ======== + + >>> from sympy import erfinv + >>> from sympy.abc import x + + Several special values are known: + + >>> erfinv(0) + 0 + >>> erfinv(1) + oo + + Differentiation with respect to $x$ is supported: + + >>> from sympy import diff + >>> diff(erfinv(x), x) + sqrt(pi)*exp(erfinv(x)**2)/2 + + We can numerically evaluate the inverse error function to arbitrary + precision on [-1, 1]: + + >>> erfinv(0.2).evalf(30) + 0.179143454621291692285822705344 + + See Also + ======== + + erf: Gaussian error function. + erfc: Complementary error function. + erfi: Imaginary error function. + erf2: Two-argument error function. + erfcinv: Inverse Complementary error function. + erf2inv: Inverse two-argument error function. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Error_function#Inverse_functions + .. [2] https://functions.wolfram.com/GammaBetaErf/InverseErf/ + + """ + + + def fdiff(self, argindex =1): + if argindex == 1: + return sqrt(pi)*exp(self.func(self.args[0])**2)*S.Half + else : + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + + """ + return erf + + @classmethod + def eval(cls, z): + if z is S.NaN: + return S.NaN + elif z is S.NegativeOne: + return S.NegativeInfinity + elif z.is_zero: + return S.Zero + elif z is S.One: + return S.Infinity + + if isinstance(z, erf) and z.args[0].is_extended_real: + return z.args[0] + + if z.is_zero: + return S.Zero + + # Try to pull out factors of -1 + nz = z.extract_multiplicatively(-1) + if nz is not None and (isinstance(nz, erf) and (nz.args[0]).is_extended_real): + return -nz.args[0] + + def _eval_rewrite_as_erfcinv(self, z, **kwargs): + return erfcinv(1-z) + + def _eval_is_zero(self): + return self.args[0].is_zero + + +class erfcinv (DefinedFunction): + r""" + Inverse Complementary Error Function. The erfcinv function is defined as: + + .. math :: + \mathrm{erfc}(x) = y \quad \Rightarrow \quad \mathrm{erfcinv}(y) = x + + Examples + ======== + + >>> from sympy import erfcinv + >>> from sympy.abc import x + + Several special values are known: + + >>> erfcinv(1) + 0 + >>> erfcinv(0) + oo + + Differentiation with respect to $x$ is supported: + + >>> from sympy import diff + >>> diff(erfcinv(x), x) + -sqrt(pi)*exp(erfcinv(x)**2)/2 + + See Also + ======== + + erf: Gaussian error function. + erfc: Complementary error function. + erfi: Imaginary error function. + erf2: Two-argument error function. + erfinv: Inverse error function. + erf2inv: Inverse two-argument error function. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Error_function#Inverse_functions + .. [2] https://functions.wolfram.com/GammaBetaErf/InverseErfc/ + + """ + + + def fdiff(self, argindex =1): + if argindex == 1: + return -sqrt(pi)*exp(self.func(self.args[0])**2)*S.Half + else: + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """ + Returns the inverse of this function. + + """ + return erfc + + @classmethod + def eval(cls, z): + if z is S.NaN: + return S.NaN + elif z.is_zero: + return S.Infinity + elif z is S.One: + return S.Zero + elif z == 2: + return S.NegativeInfinity + + if z.is_zero: + return S.Infinity + + def _eval_rewrite_as_erfinv(self, z, **kwargs): + return erfinv(1-z) + + def _eval_is_zero(self): + return (self.args[0] - 1).is_zero + + def _eval_is_infinite(self): + z = self.args[0] + return fuzzy_or([z.is_zero, is_eq(z, Integer(2))]) + + +class erf2inv(DefinedFunction): + r""" + Two-argument Inverse error function. The erf2inv function is defined as: + + .. math :: + \mathrm{erf2}(x, w) = y \quad \Rightarrow \quad \mathrm{erf2inv}(x, y) = w + + Examples + ======== + + >>> from sympy import erf2inv, oo + >>> from sympy.abc import x, y + + Several special values are known: + + >>> erf2inv(0, 0) + 0 + >>> erf2inv(1, 0) + 1 + >>> erf2inv(0, 1) + oo + >>> erf2inv(0, y) + erfinv(y) + >>> erf2inv(oo, y) + erfcinv(-y) + + Differentiation with respect to $x$ and $y$ is supported: + + >>> from sympy import diff + >>> diff(erf2inv(x, y), x) + exp(-x**2 + erf2inv(x, y)**2) + >>> diff(erf2inv(x, y), y) + sqrt(pi)*exp(erf2inv(x, y)**2)/2 + + See Also + ======== + + erf: Gaussian error function. + erfc: Complementary error function. + erfi: Imaginary error function. + erf2: Two-argument error function. + erfinv: Inverse error function. + erfcinv: Inverse complementary error function. + + References + ========== + + .. [1] https://functions.wolfram.com/GammaBetaErf/InverseErf2/ + + """ + + + def fdiff(self, argindex): + x, y = self.args + if argindex == 1: + return exp(self.func(x,y)**2-x**2) + elif argindex == 2: + return sqrt(pi)*S.Half*exp(self.func(x,y)**2) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, x, y): + if x is S.NaN or y is S.NaN: + return S.NaN + elif x.is_zero and y.is_zero: + return S.Zero + elif x.is_zero and y is S.One: + return S.Infinity + elif x is S.One and y.is_zero: + return S.One + elif x.is_zero: + return erfinv(y) + elif x is S.Infinity: + return erfcinv(-y) + elif y.is_zero: + return x + elif y is S.Infinity: + return erfinv(x) + + if x.is_zero: + if y.is_zero: + return S.Zero + else: + return erfinv(y) + if y.is_zero: + return x + + def _eval_is_zero(self): + x, y = self.args + if x.is_zero and y.is_zero: + return True + +############################################################################### +#################### EXPONENTIAL INTEGRALS #################################### +############################################################################### + +class Ei(DefinedFunction): + r""" + The classical exponential integral. + + Explanation + =========== + + For use in SymPy, this function is defined as + + .. math:: \operatorname{Ei}(x) = \sum_{n=1}^\infty \frac{x^n}{n\, n!} + + \log(x) + \gamma, + + where $\gamma$ is the Euler-Mascheroni constant. + + If $x$ is a polar number, this defines an analytic function on the + Riemann surface of the logarithm. Otherwise this defines an analytic + function in the cut plane $\mathbb{C} \setminus (-\infty, 0]$. + + **Background** + + The name exponential integral comes from the following statement: + + .. math:: \operatorname{Ei}(x) = \int_{-\infty}^x \frac{e^t}{t} \mathrm{d}t + + If the integral is interpreted as a Cauchy principal value, this statement + holds for $x > 0$ and $\operatorname{Ei}(x)$ as defined above. + + Examples + ======== + + >>> from sympy import Ei, polar_lift, exp_polar, I, pi + >>> from sympy.abc import x + + >>> Ei(-1) + Ei(-1) + + This yields a real value: + + >>> Ei(-1).n(chop=True) + -0.219383934395520 + + On the other hand the analytic continuation is not real: + + >>> Ei(polar_lift(-1)).n(chop=True) + -0.21938393439552 + 3.14159265358979*I + + The exponential integral has a logarithmic branch point at the origin: + + >>> Ei(x*exp_polar(2*I*pi)) + Ei(x) + 2*I*pi + + Differentiation is supported: + + >>> Ei(x).diff(x) + exp(x)/x + + The exponential integral is related to many other special functions. + For example: + + >>> from sympy import expint, Shi + >>> Ei(x).rewrite(expint) + -expint(1, x*exp_polar(I*pi)) - I*pi + >>> Ei(x).rewrite(Shi) + Chi(x) + Shi(x) + + See Also + ======== + + expint: Generalised exponential integral. + E1: Special case of the generalised exponential integral. + li: Logarithmic integral. + Li: Offset logarithmic integral. + Si: Sine integral. + Ci: Cosine integral. + Shi: Hyperbolic sine integral. + Chi: Hyperbolic cosine integral. + uppergamma: Upper incomplete gamma function. + + References + ========== + + .. [1] https://dlmf.nist.gov/6.6 + .. [2] https://en.wikipedia.org/wiki/Exponential_integral + .. [3] Abramowitz & Stegun, section 5: https://web.archive.org/web/20201128173312/http://people.math.sfu.ca/~cbm/aands/page_228.htm + + """ + + + @classmethod + def eval(cls, z): + if z.is_zero: + return S.NegativeInfinity + elif z is S.Infinity: + return S.Infinity + elif z is S.NegativeInfinity: + return S.Zero + + if z.is_zero: + return S.NegativeInfinity + + nz, n = z.extract_branch_factor() + if n: + return Ei(nz) + 2*I*pi*n + + def fdiff(self, argindex=1): + arg = unpolarify(self.args[0]) + if argindex == 1: + return exp(arg)/arg + else: + raise ArgumentIndexError(self, argindex) + + def _eval_evalf(self, prec): + if (self.args[0]/polar_lift(-1)).is_positive: + return super()._eval_evalf(prec) + (I*pi)._eval_evalf(prec) + return super()._eval_evalf(prec) + + def _eval_rewrite_as_uppergamma(self, z, **kwargs): + from sympy.functions.special.gamma_functions import uppergamma + # XXX this does not currently work usefully because uppergamma + # immediately turns into expint + return -uppergamma(0, polar_lift(-1)*z) - I*pi + + def _eval_rewrite_as_expint(self, z, **kwargs): + return -expint(1, polar_lift(-1)*z) - I*pi + + def _eval_rewrite_as_li(self, z, **kwargs): + if isinstance(z, log): + return li(z.args[0]) + # TODO: + # Actually it only holds that: + # Ei(z) = li(exp(z)) + # for -pi < imag(z) <= pi + return li(exp(z)) + + def _eval_rewrite_as_Si(self, z, **kwargs): + if z.is_negative: + return Shi(z) + Chi(z) - I*pi + else: + return Shi(z) + Chi(z) + _eval_rewrite_as_Ci = _eval_rewrite_as_Si + _eval_rewrite_as_Chi = _eval_rewrite_as_Si + _eval_rewrite_as_Shi = _eval_rewrite_as_Si + + def _eval_rewrite_as_tractable(self, z, limitvar=None, **kwargs): + return exp(z) * _eis(z) + + def _eval_rewrite_as_Integral(self, z, **kwargs): + from sympy.integrals.integrals import Integral + t = Dummy(uniquely_named_symbol('t', [z]).name) + return Integral(S.Exp1**t/t, (t, S.NegativeInfinity, z)) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy import re + x0 = self.args[0].limit(x, 0) + arg = self.args[0].as_leading_term(x, cdir=cdir) + cdir = arg.dir(x, cdir) + if x0.is_zero: + c, e = arg.as_coeff_exponent(x) + logx = log(x) if logx is None else logx + return log(c) + e*logx + EulerGamma - ( + I*pi if re(cdir).is_negative else S.Zero) + return super()._eval_as_leading_term(x, logx=logx, cdir=cdir) + + def _eval_nseries(self, x, n, logx, cdir=0): + x0 = self.args[0].limit(x, 0) + if x0.is_zero: + f = self._eval_rewrite_as_Si(*self.args) + return f._eval_nseries(x, n, logx) + return super()._eval_nseries(x, n, logx) + + def _eval_aseries(self, n, args0, x, logx): + from sympy.series.order import Order + point = args0[0] + + if point in (S.Infinity, S.NegativeInfinity): + z = self.args[0] + s = [factorial(k) / (z)**k for k in range(n)] + \ + [Order(1/z**n, x)] + return (exp(z)/z) * Add(*s) + + return super(Ei, self)._eval_aseries(n, args0, x, logx) + + +class expint(DefinedFunction): + r""" + Generalized exponential integral. + + Explanation + =========== + + This function is defined as + + .. math:: \operatorname{E}_\nu(z) = z^{\nu - 1} \Gamma(1 - \nu, z), + + where $\Gamma(1 - \nu, z)$ is the upper incomplete gamma function + (``uppergamma``). + + Hence for $z$ with positive real part we have + + .. math:: \operatorname{E}_\nu(z) + = \int_1^\infty \frac{e^{-zt}}{t^\nu} \mathrm{d}t, + + which explains the name. + + The representation as an incomplete gamma function provides an analytic + continuation for $\operatorname{E}_\nu(z)$. If $\nu$ is a + non-positive integer, the exponential integral is thus an unbranched + function of $z$, otherwise there is a branch point at the origin. + Refer to the incomplete gamma function documentation for details of the + branching behavior. + + Examples + ======== + + >>> from sympy import expint, S + >>> from sympy.abc import nu, z + + Differentiation is supported. Differentiation with respect to $z$ further + explains the name: for integral orders, the exponential integral is an + iterated integral of the exponential function. + + >>> expint(nu, z).diff(z) + -expint(nu - 1, z) + + Differentiation with respect to $\nu$ has no classical expression: + + >>> expint(nu, z).diff(nu) + -z**(nu - 1)*meijerg(((), (1, 1)), ((0, 0, 1 - nu), ()), z) + + At non-postive integer orders, the exponential integral reduces to the + exponential function: + + >>> expint(0, z) + exp(-z)/z + >>> expint(-1, z) + exp(-z)/z + exp(-z)/z**2 + + At half-integers it reduces to error functions: + + >>> expint(S(1)/2, z) + sqrt(pi)*erfc(sqrt(z))/sqrt(z) + + At positive integer orders it can be rewritten in terms of exponentials + and ``expint(1, z)``. Use ``expand_func()`` to do this: + + >>> from sympy import expand_func + >>> expand_func(expint(5, z)) + z**4*expint(1, z)/24 + (-z**3 + z**2 - 2*z + 6)*exp(-z)/24 + + The generalised exponential integral is essentially equivalent to the + incomplete gamma function: + + >>> from sympy import uppergamma + >>> expint(nu, z).rewrite(uppergamma) + z**(nu - 1)*uppergamma(1 - nu, z) + + As such it is branched at the origin: + + >>> from sympy import exp_polar, pi, I + >>> expint(4, z*exp_polar(2*pi*I)) + I*pi*z**3/3 + expint(4, z) + >>> expint(nu, z*exp_polar(2*pi*I)) + z**(nu - 1)*(exp(2*I*pi*nu) - 1)*gamma(1 - nu) + expint(nu, z) + + See Also + ======== + + Ei: Another related function called exponential integral. + E1: The classical case, returns expint(1, z). + li: Logarithmic integral. + Li: Offset logarithmic integral. + Si: Sine integral. + Ci: Cosine integral. + Shi: Hyperbolic sine integral. + Chi: Hyperbolic cosine integral. + uppergamma + + References + ========== + + .. [1] https://dlmf.nist.gov/8.19 + .. [2] https://functions.wolfram.com/GammaBetaErf/ExpIntegralE/ + .. [3] https://en.wikipedia.org/wiki/Exponential_integral + + """ + + + @classmethod + def eval(cls, nu, z): + from sympy.functions.special.gamma_functions import (gamma, uppergamma) + nu2 = unpolarify(nu) + if nu != nu2: + return expint(nu2, z) + if nu.is_Integer and nu <= 0 or (not nu.is_Integer and (2*nu).is_Integer): + return unpolarify(expand_mul(z**(nu - 1)*uppergamma(1 - nu, z))) + + # Extract branching information. This can be deduced from what is + # explained in lowergamma.eval(). + z, n = z.extract_branch_factor() + if n is S.Zero: + return + if nu.is_integer: + if not nu > 0: + return + return expint(nu, z) \ + - 2*pi*I*n*S.NegativeOne**(nu - 1)/factorial(nu - 1)*unpolarify(z)**(nu - 1) + else: + return (exp(2*I*pi*nu*n) - 1)*z**(nu - 1)*gamma(1 - nu) + expint(nu, z) + + def fdiff(self, argindex): + nu, z = self.args + if argindex == 1: + return -z**(nu - 1)*meijerg([], [1, 1], [0, 0, 1 - nu], [], z) + elif argindex == 2: + return -expint(nu - 1, z) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_uppergamma(self, nu, z, **kwargs): + from sympy.functions.special.gamma_functions import uppergamma + return z**(nu - 1)*uppergamma(1 - nu, z) + + def _eval_rewrite_as_Ei(self, nu, z, **kwargs): + if nu == 1: + return -Ei(z*exp_polar(-I*pi)) - I*pi + elif nu.is_Integer and nu > 1: + # DLMF, 8.19.7 + x = -unpolarify(z) + return x**(nu - 1)/factorial(nu - 1)*E1(z).rewrite(Ei) + \ + exp(x)/factorial(nu - 1) * \ + Add(*[factorial(nu - k - 2)*x**k for k in range(nu - 1)]) + else: + return self + + def _eval_expand_func(self, **hints): + return self.rewrite(Ei).rewrite(expint, **hints) + + def _eval_rewrite_as_Si(self, nu, z, **kwargs): + if nu != 1: + return self + return Shi(z) - Chi(z) + _eval_rewrite_as_Ci = _eval_rewrite_as_Si + _eval_rewrite_as_Chi = _eval_rewrite_as_Si + _eval_rewrite_as_Shi = _eval_rewrite_as_Si + + def _eval_nseries(self, x, n, logx, cdir=0): + if not self.args[0].has(x): + nu = self.args[0] + if nu == 1: + f = self._eval_rewrite_as_Si(*self.args) + return f._eval_nseries(x, n, logx) + elif nu.is_Integer and nu > 1: + f = self._eval_rewrite_as_Ei(*self.args) + return f._eval_nseries(x, n, logx) + return super()._eval_nseries(x, n, logx) + + def _eval_aseries(self, n, args0, x, logx): + from sympy.series.order import Order + point = args0[1] + nu = self.args[0] + + if point is S.Infinity: + z = self.args[1] + s = [S.NegativeOne**k * RisingFactorial(nu, k) / z**k for k in range(n)] + [Order(1/z**n, x)] + return (exp(-z)/z) * Add(*s) + + return super(expint, self)._eval_aseries(n, args0, x, logx) + + def _eval_rewrite_as_Integral(self, *args, **kwargs): + from sympy.integrals.integrals import Integral + n, x = self.args + t = Dummy(uniquely_named_symbol('t', args).name) + return Integral(t**-n * exp(-t*x), (t, 1, S.Infinity)) + + +def E1(z): + """ + Classical case of the generalized exponential integral. + + Explanation + =========== + + This is equivalent to ``expint(1, z)``. + + Examples + ======== + + >>> from sympy import E1 + >>> E1(0) + expint(1, 0) + + >>> E1(5) + expint(1, 5) + + See Also + ======== + + Ei: Exponential integral. + expint: Generalised exponential integral. + li: Logarithmic integral. + Li: Offset logarithmic integral. + Si: Sine integral. + Ci: Cosine integral. + Shi: Hyperbolic sine integral. + Chi: Hyperbolic cosine integral. + + """ + return expint(1, z) + + +class li(DefinedFunction): + r""" + The classical logarithmic integral. + + Explanation + =========== + + For use in SymPy, this function is defined as + + .. math:: \operatorname{li}(x) = \int_0^x \frac{1}{\log(t)} \mathrm{d}t \,. + + Examples + ======== + + >>> from sympy import I, oo, li + >>> from sympy.abc import z + + Several special values are known: + + >>> li(0) + 0 + >>> li(1) + -oo + >>> li(oo) + oo + + Differentiation with respect to $z$ is supported: + + >>> from sympy import diff + >>> diff(li(z), z) + 1/log(z) + + Defining the ``li`` function via an integral: + >>> from sympy import integrate + >>> integrate(li(z)) + z*li(z) - Ei(2*log(z)) + + >>> integrate(li(z),z) + z*li(z) - Ei(2*log(z)) + + + The logarithmic integral can also be defined in terms of ``Ei``: + + >>> from sympy import Ei + >>> li(z).rewrite(Ei) + Ei(log(z)) + >>> diff(li(z).rewrite(Ei), z) + 1/log(z) + + We can numerically evaluate the logarithmic integral to arbitrary precision + on the whole complex plane (except the singular points): + + >>> li(2).evalf(30) + 1.04516378011749278484458888919 + + >>> li(2*I).evalf(30) + 1.0652795784357498247001125598 + 3.08346052231061726610939702133*I + + We can even compute Soldner's constant by the help of mpmath: + + >>> from mpmath import findroot + >>> findroot(li, 2) + 1.45136923488338 + + Further transformations include rewriting ``li`` in terms of + the trigonometric integrals ``Si``, ``Ci``, ``Shi`` and ``Chi``: + + >>> from sympy import Si, Ci, Shi, Chi + >>> li(z).rewrite(Si) + -log(I*log(z)) - log(1/log(z))/2 + log(log(z))/2 + Ci(I*log(z)) + Shi(log(z)) + >>> li(z).rewrite(Ci) + -log(I*log(z)) - log(1/log(z))/2 + log(log(z))/2 + Ci(I*log(z)) + Shi(log(z)) + >>> li(z).rewrite(Shi) + -log(1/log(z))/2 + log(log(z))/2 + Chi(log(z)) - Shi(log(z)) + >>> li(z).rewrite(Chi) + -log(1/log(z))/2 + log(log(z))/2 + Chi(log(z)) - Shi(log(z)) + + See Also + ======== + + Li: Offset logarithmic integral. + Ei: Exponential integral. + expint: Generalised exponential integral. + E1: Special case of the generalised exponential integral. + Si: Sine integral. + Ci: Cosine integral. + Shi: Hyperbolic sine integral. + Chi: Hyperbolic cosine integral. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Logarithmic_integral + .. [2] https://mathworld.wolfram.com/LogarithmicIntegral.html + .. [3] https://dlmf.nist.gov/6 + .. [4] https://mathworld.wolfram.com/SoldnersConstant.html + + """ + + + @classmethod + def eval(cls, z): + if z.is_zero: + return S.Zero + elif z is S.One: + return S.NegativeInfinity + elif z is S.Infinity: + return S.Infinity + if z.is_zero: + return S.Zero + + def fdiff(self, argindex=1): + arg = self.args[0] + if argindex == 1: + return S.One / log(arg) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_conjugate(self): + z = self.args[0] + # Exclude values on the branch cut (-oo, 0) + if not z.is_extended_negative: + return self.func(z.conjugate()) + + def _eval_rewrite_as_Li(self, z, **kwargs): + return Li(z) + li(2) + + def _eval_rewrite_as_Ei(self, z, **kwargs): + return Ei(log(z)) + + def _eval_rewrite_as_uppergamma(self, z, **kwargs): + from sympy.functions.special.gamma_functions import uppergamma + return (-uppergamma(0, -log(z)) + + S.Half*(log(log(z)) - log(S.One/log(z))) - log(-log(z))) + + def _eval_rewrite_as_Si(self, z, **kwargs): + return (Ci(I*log(z)) - I*Si(I*log(z)) - + S.Half*(log(S.One/log(z)) - log(log(z))) - log(I*log(z))) + + _eval_rewrite_as_Ci = _eval_rewrite_as_Si + + def _eval_rewrite_as_Shi(self, z, **kwargs): + return (Chi(log(z)) - Shi(log(z)) - S.Half*(log(S.One/log(z)) - log(log(z)))) + + _eval_rewrite_as_Chi = _eval_rewrite_as_Shi + + def _eval_rewrite_as_hyper(self, z, **kwargs): + return (log(z)*hyper((1, 1), (2, 2), log(z)) + + S.Half*(log(log(z)) - log(S.One/log(z))) + EulerGamma) + + def _eval_rewrite_as_meijerg(self, z, **kwargs): + return (-log(-log(z)) - S.Half*(log(S.One/log(z)) - log(log(z))) + - meijerg(((), (1,)), ((0, 0), ()), -log(z))) + + def _eval_rewrite_as_tractable(self, z, limitvar=None, **kwargs): + return z * _eis(log(z)) + + def _eval_nseries(self, x, n, logx, cdir=0): + z = self.args[0] + s = [(log(z))**k / (factorial(k) * k) for k in range(1, n)] + return EulerGamma + log(log(z)) + Add(*s) + + def _eval_is_zero(self): + z = self.args[0] + if z.is_zero: + return True + +class Li(DefinedFunction): + r""" + The offset logarithmic integral. + + Explanation + =========== + + For use in SymPy, this function is defined as + + .. math:: \operatorname{Li}(x) = \operatorname{li}(x) - \operatorname{li}(2) + + Examples + ======== + + >>> from sympy import Li + >>> from sympy.abc import z + + The following special value is known: + + >>> Li(2) + 0 + + Differentiation with respect to $z$ is supported: + + >>> from sympy import diff + >>> diff(Li(z), z) + 1/log(z) + + The shifted logarithmic integral can be written in terms of $li(z)$: + + >>> from sympy import li + >>> Li(z).rewrite(li) + li(z) - li(2) + + We can numerically evaluate the logarithmic integral to arbitrary precision + on the whole complex plane (except the singular points): + + >>> Li(2).evalf(30) + 0 + + >>> Li(4).evalf(30) + 1.92242131492155809316615998938 + + See Also + ======== + + li: Logarithmic integral. + Ei: Exponential integral. + expint: Generalised exponential integral. + E1: Special case of the generalised exponential integral. + Si: Sine integral. + Ci: Cosine integral. + Shi: Hyperbolic sine integral. + Chi: Hyperbolic cosine integral. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Logarithmic_integral + .. [2] https://mathworld.wolfram.com/LogarithmicIntegral.html + .. [3] https://dlmf.nist.gov/6 + + """ + + + @classmethod + def eval(cls, z): + if z is S.Infinity: + return S.Infinity + elif z == S(2): + return S.Zero + + def fdiff(self, argindex=1): + arg = self.args[0] + if argindex == 1: + return S.One / log(arg) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_evalf(self, prec): + return self.rewrite(li).evalf(prec) + + def _eval_rewrite_as_li(self, z, **kwargs): + return li(z) - li(2) + + def _eval_rewrite_as_tractable(self, z, limitvar=None, **kwargs): + return self.rewrite(li).rewrite("tractable", deep=True) + + def _eval_nseries(self, x, n, logx, cdir=0): + f = self._eval_rewrite_as_li(*self.args) + return f._eval_nseries(x, n, logx) + +############################################################################### +#################### TRIGONOMETRIC INTEGRALS ################################## +############################################################################### + +class TrigonometricIntegral(DefinedFunction): + """ Base class for trigonometric integrals. """ + + + @classmethod + def eval(cls, z): + if z is S.Zero: + return cls._atzero + elif z is S.Infinity: + return cls._atinf() + elif z is S.NegativeInfinity: + return cls._atneginf() + + if z.is_zero: + return cls._atzero + + nz = z.extract_multiplicatively(polar_lift(I)) + if nz is None and cls._trigfunc(0) == 0: + nz = z.extract_multiplicatively(I) + if nz is not None: + return cls._Ifactor(nz, 1) + nz = z.extract_multiplicatively(polar_lift(-I)) + if nz is not None: + return cls._Ifactor(nz, -1) + + nz = z.extract_multiplicatively(polar_lift(-1)) + if nz is None and cls._trigfunc(0) == 0: + nz = z.extract_multiplicatively(-1) + if nz is not None: + return cls._minusfactor(nz) + + nz, n = z.extract_branch_factor() + if n == 0 and nz == z: + return + return 2*pi*I*n*cls._trigfunc(0) + cls(nz) + + def fdiff(self, argindex=1): + arg = unpolarify(self.args[0]) + if argindex == 1: + return self._trigfunc(arg)/arg + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Ei(self, z, **kwargs): + return self._eval_rewrite_as_expint(z).rewrite(Ei) + + def _eval_rewrite_as_uppergamma(self, z, **kwargs): + from sympy.functions.special.gamma_functions import uppergamma + return self._eval_rewrite_as_expint(z).rewrite(uppergamma) + + def _eval_nseries(self, x, n, logx, cdir=0): + # NOTE this is fairly inefficient + if self.args[0].subs(x, 0) != 0: + return super()._eval_nseries(x, n, logx) + baseseries = self._trigfunc(x)._eval_nseries(x, n, logx) + if self._trigfunc(0) != 0: + baseseries -= 1 + baseseries = baseseries.replace(Pow, lambda t, n: t**n/n, simultaneous=False) + if self._trigfunc(0) != 0: + baseseries += EulerGamma + log(x) + return baseseries.subs(x, self.args[0])._eval_nseries(x, n, logx) + + +class Si(TrigonometricIntegral): + r""" + Sine integral. + + Explanation + =========== + + This function is defined by + + .. math:: \operatorname{Si}(z) = \int_0^z \frac{\sin{t}}{t} \mathrm{d}t. + + It is an entire function. + + Examples + ======== + + >>> from sympy import Si + >>> from sympy.abc import z + + The sine integral is an antiderivative of $sin(z)/z$: + + >>> Si(z).diff(z) + sin(z)/z + + It is unbranched: + + >>> from sympy import exp_polar, I, pi + >>> Si(z*exp_polar(2*I*pi)) + Si(z) + + Sine integral behaves much like ordinary sine under multiplication by ``I``: + + >>> Si(I*z) + I*Shi(z) + >>> Si(-z) + -Si(z) + + It can also be expressed in terms of exponential integrals, but beware + that the latter is branched: + + >>> from sympy import expint + >>> Si(z).rewrite(expint) + -I*(-expint(1, z*exp_polar(-I*pi/2))/2 + + expint(1, z*exp_polar(I*pi/2))/2) + pi/2 + + It can be rewritten in the form of sinc function (by definition): + + >>> from sympy import sinc + >>> Si(z).rewrite(sinc) + Integral(sinc(_t), (_t, 0, z)) + + See Also + ======== + + Ci: Cosine integral. + Shi: Hyperbolic sine integral. + Chi: Hyperbolic cosine integral. + Ei: Exponential integral. + expint: Generalised exponential integral. + sinc: unnormalized sinc function + E1: Special case of the generalised exponential integral. + li: Logarithmic integral. + Li: Offset logarithmic integral. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Trigonometric_integral + + """ + + _trigfunc = sin + _atzero = S.Zero + + @classmethod + def _atinf(cls): + return pi*S.Half + + @classmethod + def _atneginf(cls): + return -pi*S.Half + + @classmethod + def _minusfactor(cls, z): + return -Si(z) + + @classmethod + def _Ifactor(cls, z, sign): + return I*Shi(z)*sign + + def _eval_rewrite_as_expint(self, z, **kwargs): + # XXX should we polarify z? + return pi/2 + (E1(polar_lift(I)*z) - E1(polar_lift(-I)*z))/2/I + + def _eval_rewrite_as_Integral(self, z, **kwargs): + from sympy.integrals.integrals import Integral + t = Dummy(uniquely_named_symbol('t', [z]).name) + return Integral(sinc(t), (t, 0, z)) + + _eval_rewrite_as_sinc = _eval_rewrite_as_Integral + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0].as_leading_term(x, logx=logx, cdir=cdir) + arg0 = arg.subs(x, 0) + + if arg0 is S.NaN: + arg0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + if arg0.is_zero: + return arg + elif not arg0.is_infinite: + return self.func(arg0) + else: + return self + + def _eval_aseries(self, n, args0, x, logx): + from sympy.series.order import Order + point = args0[0] + + # Expansion at oo + if point is S.Infinity: + z = self.args[0] + p = [S.NegativeOne**k * factorial(2*k) / z**(2*k + 1) + for k in range(n//2 + 1)] + [Order(1/z**n, x)] + q = [S.NegativeOne**k * factorial(2*k + 1) / z**(2*(k + 1)) + for k in range(n//2)] + [Order(1/z**n, x)] + return pi/2 - cos(z)*Add(*p) - sin(z)*Add(*q) + + # All other points are not handled + return super(Si, self)._eval_aseries(n, args0, x, logx) + + def _eval_is_zero(self): + z = self.args[0] + if z.is_zero: + return True + + +class Ci(TrigonometricIntegral): + r""" + Cosine integral. + + Explanation + =========== + + This function is defined for positive $x$ by + + .. math:: \operatorname{Ci}(x) = \gamma + \log{x} + + \int_0^x \frac{\cos{t} - 1}{t} \mathrm{d}t + = -\int_x^\infty \frac{\cos{t}}{t} \mathrm{d}t, + + where $\gamma$ is the Euler-Mascheroni constant. + + We have + + .. math:: \operatorname{Ci}(z) = + -\frac{\operatorname{E}_1\left(e^{i\pi/2} z\right) + + \operatorname{E}_1\left(e^{-i \pi/2} z\right)}{2} + + which holds for all polar $z$ and thus provides an analytic + continuation to the Riemann surface of the logarithm. + + The formula also holds as stated + for $z \in \mathbb{C}$ with $\Re(z) > 0$. + By lifting to the principal branch, we obtain an analytic function on the + cut complex plane. + + Examples + ======== + + >>> from sympy import Ci + >>> from sympy.abc import z + + The cosine integral is a primitive of $\cos(z)/z$: + + >>> Ci(z).diff(z) + cos(z)/z + + It has a logarithmic branch point at the origin: + + >>> from sympy import exp_polar, I, pi + >>> Ci(z*exp_polar(2*I*pi)) + Ci(z) + 2*I*pi + + The cosine integral behaves somewhat like ordinary $\cos$ under + multiplication by $i$: + + >>> from sympy import polar_lift + >>> Ci(polar_lift(I)*z) + Chi(z) + I*pi/2 + >>> Ci(polar_lift(-1)*z) + Ci(z) + I*pi + + It can also be expressed in terms of exponential integrals: + + >>> from sympy import expint + >>> Ci(z).rewrite(expint) + -expint(1, z*exp_polar(-I*pi/2))/2 - expint(1, z*exp_polar(I*pi/2))/2 + + See Also + ======== + + Si: Sine integral. + Shi: Hyperbolic sine integral. + Chi: Hyperbolic cosine integral. + Ei: Exponential integral. + expint: Generalised exponential integral. + E1: Special case of the generalised exponential integral. + li: Logarithmic integral. + Li: Offset logarithmic integral. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Trigonometric_integral + + """ + + _trigfunc = cos + _atzero = S.ComplexInfinity + + @classmethod + def _atinf(cls): + return S.Zero + + @classmethod + def _atneginf(cls): + return I*pi + + @classmethod + def _minusfactor(cls, z): + return Ci(z) + I*pi + + @classmethod + def _Ifactor(cls, z, sign): + return Chi(z) + I*pi/2*sign + + def _eval_rewrite_as_expint(self, z, **kwargs): + return -(E1(polar_lift(I)*z) + E1(polar_lift(-I)*z))/2 + + def _eval_rewrite_as_Integral(self, z, **kwargs): + from sympy.integrals.integrals import Integral + t = Dummy(uniquely_named_symbol('t', [z]).name) + return S.EulerGamma + log(z) - Integral((1-cos(t))/t, (t, 0, z)) + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0].as_leading_term(x, logx=logx, cdir=cdir) + arg0 = arg.subs(x, 0) + + if arg0 is S.NaN: + arg0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + if arg0.is_zero: + c, e = arg.as_coeff_exponent(x) + logx = log(x) if logx is None else logx + return log(c) + e*logx + EulerGamma + elif arg0.is_finite: + return self.func(arg0) + else: + return self + + def _eval_aseries(self, n, args0, x, logx): + from sympy.series.order import Order + point = args0[0] + + if point in (S.Infinity, S.NegativeInfinity): + z = self.args[0] + p = [S.NegativeOne**k * factorial(2*k) / z**(2*k + 1) + for k in range(n//2 + 1)] + [Order(1/z**n, x)] + q = [S.NegativeOne**k * factorial(2*k + 1) / z**(2*(k + 1)) + for k in range(n//2)] + [Order(1/z**n, x)] + result = sin(z)*(Add(*p)) - cos(z)*(Add(*q)) + + if point is S.NegativeInfinity: + result += I*pi + return result + + return super(Ci, self)._eval_aseries(n, args0, x, logx) + +class Shi(TrigonometricIntegral): + r""" + Sinh integral. + + Explanation + =========== + + This function is defined by + + .. math:: \operatorname{Shi}(z) = \int_0^z \frac{\sinh{t}}{t} \mathrm{d}t. + + It is an entire function. + + Examples + ======== + + >>> from sympy import Shi + >>> from sympy.abc import z + + The Sinh integral is a primitive of $\sinh(z)/z$: + + >>> Shi(z).diff(z) + sinh(z)/z + + It is unbranched: + + >>> from sympy import exp_polar, I, pi + >>> Shi(z*exp_polar(2*I*pi)) + Shi(z) + + The $\sinh$ integral behaves much like ordinary $\sinh$ under + multiplication by $i$: + + >>> Shi(I*z) + I*Si(z) + >>> Shi(-z) + -Shi(z) + + It can also be expressed in terms of exponential integrals, but beware + that the latter is branched: + + >>> from sympy import expint + >>> Shi(z).rewrite(expint) + expint(1, z)/2 - expint(1, z*exp_polar(I*pi))/2 - I*pi/2 + + See Also + ======== + + Si: Sine integral. + Ci: Cosine integral. + Chi: Hyperbolic cosine integral. + Ei: Exponential integral. + expint: Generalised exponential integral. + E1: Special case of the generalised exponential integral. + li: Logarithmic integral. + Li: Offset logarithmic integral. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Trigonometric_integral + + """ + + _trigfunc = sinh + _atzero = S.Zero + + @classmethod + def _atinf(cls): + return S.Infinity + + @classmethod + def _atneginf(cls): + return S.NegativeInfinity + + @classmethod + def _minusfactor(cls, z): + return -Shi(z) + + @classmethod + def _Ifactor(cls, z, sign): + return I*Si(z)*sign + + def _eval_rewrite_as_expint(self, z, **kwargs): + # XXX should we polarify z? + return (E1(z) - E1(exp_polar(I*pi)*z))/2 - I*pi/2 + + def _eval_is_zero(self): + z = self.args[0] + if z.is_zero: + return True + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0].as_leading_term(x) + arg0 = arg.subs(x, 0) + + if arg0 is S.NaN: + arg0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + if arg0.is_zero: + return arg + elif not arg0.is_infinite: + return self.func(arg0) + else: + return self + + +class Chi(TrigonometricIntegral): + r""" + Cosh integral. + + Explanation + =========== + + This function is defined for positive $x$ by + + .. math:: \operatorname{Chi}(x) = \gamma + \log{x} + + \int_0^x \frac{\cosh{t} - 1}{t} \mathrm{d}t, + + where $\gamma$ is the Euler-Mascheroni constant. + + We have + + .. math:: \operatorname{Chi}(z) = \operatorname{Ci}\left(e^{i \pi/2}z\right) + - i\frac{\pi}{2}, + + which holds for all polar $z$ and thus provides an analytic + continuation to the Riemann surface of the logarithm. + By lifting to the principal branch we obtain an analytic function on the + cut complex plane. + + Examples + ======== + + >>> from sympy import Chi + >>> from sympy.abc import z + + The $\cosh$ integral is a primitive of $\cosh(z)/z$: + + >>> Chi(z).diff(z) + cosh(z)/z + + It has a logarithmic branch point at the origin: + + >>> from sympy import exp_polar, I, pi + >>> Chi(z*exp_polar(2*I*pi)) + Chi(z) + 2*I*pi + + The $\cosh$ integral behaves somewhat like ordinary $\cosh$ under + multiplication by $i$: + + >>> from sympy import polar_lift + >>> Chi(polar_lift(I)*z) + Ci(z) + I*pi/2 + >>> Chi(polar_lift(-1)*z) + Chi(z) + I*pi + + It can also be expressed in terms of exponential integrals: + + >>> from sympy import expint + >>> Chi(z).rewrite(expint) + -expint(1, z)/2 - expint(1, z*exp_polar(I*pi))/2 - I*pi/2 + + See Also + ======== + + Si: Sine integral. + Ci: Cosine integral. + Shi: Hyperbolic sine integral. + Ei: Exponential integral. + expint: Generalised exponential integral. + E1: Special case of the generalised exponential integral. + li: Logarithmic integral. + Li: Offset logarithmic integral. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Trigonometric_integral + + """ + + _trigfunc = cosh + _atzero = S.ComplexInfinity + + @classmethod + def _atinf(cls): + return S.Infinity + + @classmethod + def _atneginf(cls): + return S.Infinity + + @classmethod + def _minusfactor(cls, z): + return Chi(z) + I*pi + + @classmethod + def _Ifactor(cls, z, sign): + return Ci(z) + I*pi/2*sign + + def _eval_rewrite_as_expint(self, z, **kwargs): + return -I*pi/2 - (E1(z) + E1(exp_polar(I*pi)*z))/2 + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0].as_leading_term(x, logx=logx, cdir=cdir) + arg0 = arg.subs(x, 0) + + if arg0 is S.NaN: + arg0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + if arg0.is_zero: + c, e = arg.as_coeff_exponent(x) + logx = log(x) if logx is None else logx + return log(c) + e*logx + EulerGamma + elif arg0.is_finite: + return self.func(arg0) + else: + return self + + +############################################################################### +#################### FRESNEL INTEGRALS ######################################## +############################################################################### + +class FresnelIntegral(DefinedFunction): + """ Base class for the Fresnel integrals.""" + + unbranched = True + + @classmethod + def eval(cls, z): + # Values at positive infinities signs + # if any were extracted automatically + if z is S.Infinity: + return S.Half + + # Value at zero + if z.is_zero: + return S.Zero + + # Try to pull out factors of -1 and I + prefact = S.One + newarg = z + changed = False + + nz = newarg.extract_multiplicatively(-1) + if nz is not None: + prefact = -prefact + newarg = nz + changed = True + + nz = newarg.extract_multiplicatively(I) + if nz is not None: + prefact = cls._sign*I*prefact + newarg = nz + changed = True + + if changed: + return prefact*cls(newarg) + + def fdiff(self, argindex=1): + if argindex == 1: + return self._trigfunc(S.Half*pi*self.args[0]**2) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_is_extended_real(self): + return self.args[0].is_extended_real + + _eval_is_finite = _eval_is_extended_real + + def _eval_is_zero(self): + return self.args[0].is_zero + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + as_real_imag = real_to_real_as_real_imag + + +class fresnels(FresnelIntegral): + r""" + Fresnel integral S. + + Explanation + =========== + + This function is defined by + + .. math:: \operatorname{S}(z) = \int_0^z \sin{\frac{\pi}{2} t^2} \mathrm{d}t. + + It is an entire function. + + Examples + ======== + + >>> from sympy import I, oo, fresnels + >>> from sympy.abc import z + + Several special values are known: + + >>> fresnels(0) + 0 + >>> fresnels(oo) + 1/2 + >>> fresnels(-oo) + -1/2 + >>> fresnels(I*oo) + -I/2 + >>> fresnels(-I*oo) + I/2 + + In general one can pull out factors of -1 and $i$ from the argument: + + >>> fresnels(-z) + -fresnels(z) + >>> fresnels(I*z) + -I*fresnels(z) + + The Fresnel S integral obeys the mirror symmetry + $\overline{S(z)} = S(\bar{z})$: + + >>> from sympy import conjugate + >>> conjugate(fresnels(z)) + fresnels(conjugate(z)) + + Differentiation with respect to $z$ is supported: + + >>> from sympy import diff + >>> diff(fresnels(z), z) + sin(pi*z**2/2) + + Defining the Fresnel functions via an integral: + + >>> from sympy import integrate, pi, sin, expand_func + >>> integrate(sin(pi*z**2/2), z) + 3*fresnels(z)*gamma(3/4)/(4*gamma(7/4)) + >>> expand_func(integrate(sin(pi*z**2/2), z)) + fresnels(z) + + We can numerically evaluate the Fresnel integral to arbitrary precision + on the whole complex plane: + + >>> fresnels(2).evalf(30) + 0.343415678363698242195300815958 + + >>> fresnels(-2*I).evalf(30) + 0.343415678363698242195300815958*I + + See Also + ======== + + fresnelc: Fresnel cosine integral. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Fresnel_integral + .. [2] https://dlmf.nist.gov/7 + .. [3] https://mathworld.wolfram.com/FresnelIntegrals.html + .. [4] https://functions.wolfram.com/GammaBetaErf/FresnelS + .. [5] The converging factors for the fresnel integrals + by John W. Wrench Jr. and Vicki Alley + + """ + _trigfunc = sin + _sign = -S.One + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0: + return S.Zero + else: + x = sympify(x) + if len(previous_terms) > 1: + p = previous_terms[-1] + return (-pi**2*x**4*(4*n - 1)/(8*n*(2*n + 1)*(4*n + 3))) * p + else: + return x**3 * (-x**4)**n * (S(2)**(-2*n - 1)*pi**(2*n + 1)) / ((4*n + 3)*factorial(2*n + 1)) + + def _eval_rewrite_as_erf(self, z, **kwargs): + return (S.One + I)/4 * (erf((S.One + I)/2*sqrt(pi)*z) - I*erf((S.One - I)/2*sqrt(pi)*z)) + + def _eval_rewrite_as_hyper(self, z, **kwargs): + return pi*z**3/6 * hyper([Rational(3, 4)], [Rational(3, 2), Rational(7, 4)], -pi**2*z**4/16) + + def _eval_rewrite_as_meijerg(self, z, **kwargs): + return (pi*z**Rational(9, 4) / (sqrt(2)*(z**2)**Rational(3, 4)*(-z)**Rational(3, 4)) + * meijerg([], [1], [Rational(3, 4)], [Rational(1, 4), 0], -pi**2*z**4/16)) + + def _eval_rewrite_as_Integral(self, z, **kwargs): + from sympy.integrals.integrals import Integral + t = Dummy(uniquely_named_symbol('t', [z]).name) + return Integral(sin(pi*t**2/2), (t, 0, z)) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.series.order import Order + arg = self.args[0].as_leading_term(x, logx=logx, cdir=cdir) + arg0 = arg.subs(x, 0) + + if arg0 is S.ComplexInfinity: + arg0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + if arg0.is_zero: + return pi*arg**3/6 + elif arg0 in [S.Infinity, S.NegativeInfinity]: + s = 1 if arg0 is S.Infinity else -1 + return s*S.Half + Order(x, x) + else: + return self.func(arg0) + + def _eval_aseries(self, n, args0, x, logx): + from sympy.series.order import Order + point = args0[0] + + # Expansion at oo and -oo + if point in [S.Infinity, -S.Infinity]: + z = self.args[0] + + # expansion of S(x) = S1(x*sqrt(pi/2)), see reference[5] page 1-8 + # as only real infinities are dealt with, sin and cos are O(1) + p = [S.NegativeOne**k * factorial(4*k + 1) / + (2**(2*k + 2) * z**(4*k + 3) * 2**(2*k)*factorial(2*k)) + for k in range(0, n) if 4*k + 3 < n] + q = [1/(2*z)] + [S.NegativeOne**k * factorial(4*k - 1) / + (2**(2*k + 1) * z**(4*k + 1) * 2**(2*k - 1)*factorial(2*k - 1)) + for k in range(1, n) if 4*k + 1 < n] + + p = [-sqrt(2/pi)*t for t in p] + q = [-sqrt(2/pi)*t for t in q] + s = 1 if point is S.Infinity else -1 + # The expansion at oo is 1/2 + some odd powers of z + # To get the expansion at -oo, replace z by -z and flip the sign + # The result -1/2 + the same odd powers of z as before. + return s*S.Half + (sin(z**2)*Add(*p) + cos(z**2)*Add(*q) + ).subs(x, sqrt(2/pi)*x) + Order(1/z**n, x) + + # All other points are not handled + return super()._eval_aseries(n, args0, x, logx) + + +class fresnelc(FresnelIntegral): + r""" + Fresnel integral C. + + Explanation + =========== + + This function is defined by + + .. math:: \operatorname{C}(z) = \int_0^z \cos{\frac{\pi}{2} t^2} \mathrm{d}t. + + It is an entire function. + + Examples + ======== + + >>> from sympy import I, oo, fresnelc + >>> from sympy.abc import z + + Several special values are known: + + >>> fresnelc(0) + 0 + >>> fresnelc(oo) + 1/2 + >>> fresnelc(-oo) + -1/2 + >>> fresnelc(I*oo) + I/2 + >>> fresnelc(-I*oo) + -I/2 + + In general one can pull out factors of -1 and $i$ from the argument: + + >>> fresnelc(-z) + -fresnelc(z) + >>> fresnelc(I*z) + I*fresnelc(z) + + The Fresnel C integral obeys the mirror symmetry + $\overline{C(z)} = C(\bar{z})$: + + >>> from sympy import conjugate + >>> conjugate(fresnelc(z)) + fresnelc(conjugate(z)) + + Differentiation with respect to $z$ is supported: + + >>> from sympy import diff + >>> diff(fresnelc(z), z) + cos(pi*z**2/2) + + Defining the Fresnel functions via an integral: + + >>> from sympy import integrate, pi, cos, expand_func + >>> integrate(cos(pi*z**2/2), z) + fresnelc(z)*gamma(1/4)/(4*gamma(5/4)) + >>> expand_func(integrate(cos(pi*z**2/2), z)) + fresnelc(z) + + We can numerically evaluate the Fresnel integral to arbitrary precision + on the whole complex plane: + + >>> fresnelc(2).evalf(30) + 0.488253406075340754500223503357 + + >>> fresnelc(-2*I).evalf(30) + -0.488253406075340754500223503357*I + + See Also + ======== + + fresnels: Fresnel sine integral. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Fresnel_integral + .. [2] https://dlmf.nist.gov/7 + .. [3] https://mathworld.wolfram.com/FresnelIntegrals.html + .. [4] https://functions.wolfram.com/GammaBetaErf/FresnelC + .. [5] The converging factors for the fresnel integrals + by John W. Wrench Jr. and Vicki Alley + + """ + _trigfunc = cos + _sign = S.One + + @staticmethod + @cacheit + def taylor_term(n, x, *previous_terms): + if n < 0: + return S.Zero + else: + x = sympify(x) + if len(previous_terms) > 1: + p = previous_terms[-1] + return (-pi**2*x**4*(4*n - 3)/(8*n*(2*n - 1)*(4*n + 1))) * p + else: + return x * (-x**4)**n * (S(2)**(-2*n)*pi**(2*n)) / ((4*n + 1)*factorial(2*n)) + + def _eval_rewrite_as_erf(self, z, **kwargs): + return (S.One - I)/4 * (erf((S.One + I)/2*sqrt(pi)*z) + I*erf((S.One - I)/2*sqrt(pi)*z)) + + def _eval_rewrite_as_hyper(self, z, **kwargs): + return z * hyper([Rational(1, 4)], [S.Half, Rational(5, 4)], -pi**2*z**4/16) + + def _eval_rewrite_as_meijerg(self, z, **kwargs): + return (pi*z**Rational(3, 4) / (sqrt(2)*root(z**2, 4)*root(-z, 4)) + * meijerg([], [1], [Rational(1, 4)], [Rational(3, 4), 0], -pi**2*z**4/16)) + + def _eval_rewrite_as_Integral(self, z, **kwargs): + from sympy.integrals.integrals import Integral + t = Dummy(uniquely_named_symbol('t', [z]).name) + return Integral(cos(pi*t**2/2), (t, 0, z)) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.series.order import Order + arg = self.args[0].as_leading_term(x, logx=logx, cdir=cdir) + arg0 = arg.subs(x, 0) + + if arg0 is S.ComplexInfinity: + arg0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + if arg0.is_zero: + return arg + elif arg0 in [S.Infinity, S.NegativeInfinity]: + s = 1 if arg0 is S.Infinity else -1 + return s*S.Half + Order(x, x) + else: + return self.func(arg0) + + def _eval_aseries(self, n, args0, x, logx): + from sympy.series.order import Order + point = args0[0] + + # Expansion at oo + if point in [S.Infinity, -S.Infinity]: + z = self.args[0] + + # expansion of C(x) = C1(x*sqrt(pi/2)), see reference[5] page 1-8 + # as only real infinities are dealt with, sin and cos are O(1) + p = [S.NegativeOne**k * factorial(4*k + 1) / + (2**(2*k + 2) * z**(4*k + 3) * 2**(2*k)*factorial(2*k)) + for k in range(n) if 4*k + 3 < n] + q = [1/(2*z)] + [S.NegativeOne**k * factorial(4*k - 1) / + (2**(2*k + 1) * z**(4*k + 1) * 2**(2*k - 1)*factorial(2*k - 1)) + for k in range(1, n) if 4*k + 1 < n] + + p = [-sqrt(2/pi)*t for t in p] + q = [ sqrt(2/pi)*t for t in q] + s = 1 if point is S.Infinity else -1 + # The expansion at oo is 1/2 + some odd powers of z + # To get the expansion at -oo, replace z by -z and flip the sign + # The result -1/2 + the same odd powers of z as before. + return s*S.Half + (cos(z**2)*Add(*p) + sin(z**2)*Add(*q) + ).subs(x, sqrt(2/pi)*x) + Order(1/z**n, x) + + # All other points are not handled + return super()._eval_aseries(n, args0, x, logx) + + +############################################################################### +#################### HELPER FUNCTIONS ######################################### +############################################################################### + + +class _erfs(DefinedFunction): + """ + Helper function to make the $\\mathrm{erf}(z)$ function + tractable for the Gruntz algorithm. + + """ + @classmethod + def eval(cls, arg): + if arg.is_zero: + return S.One + + def _eval_aseries(self, n, args0, x, logx): + from sympy.series.order import Order + point = args0[0] + + # Expansion at oo + if point is S.Infinity: + z = self.args[0] + l = [1/sqrt(pi) * factorial(2*k)*(-S( + 4))**(-k)/factorial(k) * (1/z)**(2*k + 1) for k in range(n)] + o = Order(1/z**(2*n + 1), x) + # It is very inefficient to first add the order and then do the nseries + return (Add(*l))._eval_nseries(x, n, logx) + o + + # Expansion at I*oo + t = point.extract_multiplicatively(I) + if t is S.Infinity: + z = self.args[0] + # TODO: is the series really correct? + l = [1/sqrt(pi) * factorial(2*k)*(-S( + 4))**(-k)/factorial(k) * (1/z)**(2*k + 1) for k in range(n)] + o = Order(1/z**(2*n + 1), x) + # It is very inefficient to first add the order and then do the nseries + return (Add(*l))._eval_nseries(x, n, logx) + o + + # All other points are not handled + return super()._eval_aseries(n, args0, x, logx) + + def fdiff(self, argindex=1): + if argindex == 1: + z = self.args[0] + return -2/sqrt(pi) + 2*z*_erfs(z) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_intractable(self, z, **kwargs): + return (S.One - erf(z))*exp(z**2) + + +class _eis(DefinedFunction): + """ + Helper function to make the $\\mathrm{Ei}(z)$ and $\\mathrm{li}(z)$ + functions tractable for the Gruntz algorithm. + + """ + + + def _eval_aseries(self, n, args0, x, logx): + from sympy.series.order import Order + if args0[0] not in (S.Infinity, S.NegativeInfinity): + return super()._eval_aseries(n, args0, x, logx) + + z = self.args[0] + l = [factorial(k) * (1/z)**(k + 1) for k in range(n)] + o = Order(1/z**(n + 1), x) + # It is very inefficient to first add the order and then do the nseries + return (Add(*l))._eval_nseries(x, n, logx) + o + + + def fdiff(self, argindex=1): + if argindex == 1: + z = self.args[0] + return S.One / z - _eis(z) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_intractable(self, z, **kwargs): + return exp(-z)*Ei(z) + + def _eval_as_leading_term(self, x, logx, cdir): + x0 = self.args[0].limit(x, 0) + if x0.is_zero: + f = self._eval_rewrite_as_intractable(*self.args) + return f._eval_as_leading_term(x, logx=logx, cdir=cdir) + return super()._eval_as_leading_term(x, logx=logx, cdir=cdir) + + def _eval_nseries(self, x, n, logx, cdir=0): + x0 = self.args[0].limit(x, 0) + if x0.is_zero: + f = self._eval_rewrite_as_intractable(*self.args) + return f._eval_nseries(x, n, logx) + return super()._eval_nseries(x, n, logx) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/gamma_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/gamma_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..73a5a1585154c603e9c510b3c61144039e0e5502 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/gamma_functions.py @@ -0,0 +1,1344 @@ +from math import prod + +from sympy.core import Add, S, Dummy, expand_func +from sympy.core.expr import Expr +from sympy.core.function import DefinedFunction, ArgumentIndexError, PoleError +from sympy.core.logic import fuzzy_and, fuzzy_not +from sympy.core.numbers import Rational, pi, oo, I +from sympy.core.power import Pow +from sympy.functions.special.zeta_functions import zeta +from sympy.functions.special.error_functions import erf, erfc, Ei +from sympy.functions.elementary.complexes import re, unpolarify +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.integers import ceiling, floor +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin, cos, cot +from sympy.functions.combinatorial.numbers import bernoulli, harmonic +from sympy.functions.combinatorial.factorials import factorial, rf, RisingFactorial +from sympy.utilities.misc import as_int + +from mpmath import mp, workprec +from mpmath.libmp.libmpf import prec_to_dps + +def intlike(n): + try: + as_int(n, strict=False) + return True + except ValueError: + return False + +############################################################################### +############################ COMPLETE GAMMA FUNCTION ########################## +############################################################################### + +class gamma(DefinedFunction): + r""" + The gamma function + + .. math:: + \Gamma(x) := \int^{\infty}_{0} t^{x-1} e^{-t} \mathrm{d}t. + + Explanation + =========== + + The ``gamma`` function implements the function which passes through the + values of the factorial function (i.e., $\Gamma(n) = (n - 1)!$ when n is + an integer). More generally, $\Gamma(z)$ is defined in the whole complex + plane except at the negative integers where there are simple poles. + + Examples + ======== + + >>> from sympy import S, I, pi, gamma + >>> from sympy.abc import x + + Several special values are known: + + >>> gamma(1) + 1 + >>> gamma(4) + 6 + >>> gamma(S(3)/2) + sqrt(pi)/2 + + The ``gamma`` function obeys the mirror symmetry: + + >>> from sympy import conjugate + >>> conjugate(gamma(x)) + gamma(conjugate(x)) + + Differentiation with respect to $x$ is supported: + + >>> from sympy import diff + >>> diff(gamma(x), x) + gamma(x)*polygamma(0, x) + + Series expansion is also supported: + + >>> from sympy import series + >>> series(gamma(x), x, 0, 3) + 1/x - EulerGamma + x*(EulerGamma**2/2 + pi**2/12) + x**2*(-EulerGamma*pi**2/12 - zeta(3)/3 - EulerGamma**3/6) + O(x**3) + + We can numerically evaluate the ``gamma`` function to arbitrary precision + on the whole complex plane: + + >>> gamma(pi).evalf(40) + 2.288037795340032417959588909060233922890 + >>> gamma(1+I).evalf(20) + 0.49801566811835604271 - 0.15494982830181068512*I + + See Also + ======== + + lowergamma: Lower incomplete gamma function. + uppergamma: Upper incomplete gamma function. + polygamma: Polygamma function. + loggamma: Log Gamma function. + digamma: Digamma function. + trigamma: Trigamma function. + sympy.functions.special.beta_functions.beta: Euler Beta function. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gamma_function + .. [2] https://dlmf.nist.gov/5 + .. [3] https://mathworld.wolfram.com/GammaFunction.html + .. [4] https://functions.wolfram.com/GammaBetaErf/Gamma/ + + """ + + unbranched = True + _singularities = (S.ComplexInfinity,) + + def fdiff(self, argindex=1): + if argindex == 1: + return self.func(self.args[0])*polygamma(0, self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, arg): + if arg.is_Number: + if arg is S.NaN: + return S.NaN + elif arg is oo: + return oo + elif intlike(arg): + if arg.is_positive: + return factorial(arg - 1) + else: + return S.ComplexInfinity + elif arg.is_Rational: + if arg.q == 2: + n = abs(arg.p) // arg.q + + if arg.is_positive: + k, coeff = n, S.One + else: + n = k = n + 1 + + if n & 1 == 0: + coeff = S.One + else: + coeff = S.NegativeOne + + coeff *= prod(range(3, 2*k, 2)) + + if arg.is_positive: + return coeff*sqrt(pi) / 2**n + else: + return 2**n*sqrt(pi) / coeff + + def _eval_expand_func(self, **hints): + arg = self.args[0] + if arg.is_Rational: + if abs(arg.p) > arg.q: + x = Dummy('x') + n = arg.p // arg.q + p = arg.p - n*arg.q + return self.func(x + n)._eval_expand_func().subs(x, Rational(p, arg.q)) + + if arg.is_Add: + coeff, tail = arg.as_coeff_add() + if coeff and coeff.q != 1: + intpart = floor(coeff) + tail = (coeff - intpart,) + tail + coeff = intpart + tail = arg._new_rawargs(*tail, reeval=False) + return self.func(tail)*RisingFactorial(tail, coeff) + + return self.func(*self.args) + + def _eval_conjugate(self): + return self.func(self.args[0].conjugate()) + + def _eval_is_real(self): + x = self.args[0] + if x.is_nonpositive and x.is_integer: + return False + if intlike(x) and x <= 0: + return False + if x.is_positive or x.is_noninteger: + return True + + def _eval_is_positive(self): + x = self.args[0] + if x.is_positive: + return True + elif x.is_noninteger: + return floor(x).is_even + + def _eval_rewrite_as_tractable(self, z, limitvar=None, **kwargs): + return exp(loggamma(z)) + + def _eval_rewrite_as_factorial(self, z, **kwargs): + return factorial(z - 1) + + def _eval_nseries(self, x, n, logx, cdir=0): + x0 = self.args[0].limit(x, 0) + if not (x0.is_Integer and x0 <= 0): + return super()._eval_nseries(x, n, logx) + t = self.args[0] - x0 + return (self.func(t + 1)/rf(self.args[0], -x0 + 1))._eval_nseries(x, n, logx) + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[0] + x0 = arg.subs(x, 0) + + if x0.is_integer and x0.is_nonpositive: + n = -x0 + res = S.NegativeOne**n/self.func(n + 1) + return res/(arg + n).as_leading_term(x) + elif not x0.is_infinite: + return self.func(x0) + raise PoleError() + + +############################################################################### +################## LOWER and UPPER INCOMPLETE GAMMA FUNCTIONS ################# +############################################################################### + +class lowergamma(DefinedFunction): + r""" + The lower incomplete gamma function. + + Explanation + =========== + + It can be defined as the meromorphic continuation of + + .. math:: + \gamma(s, x) := \int_0^x t^{s-1} e^{-t} \mathrm{d}t = \Gamma(s) - \Gamma(s, x). + + This can be shown to be the same as + + .. math:: + \gamma(s, x) = \frac{x^s}{s} {}_1F_1\left({s \atop s+1} \middle| -x\right), + + where ${}_1F_1$ is the (confluent) hypergeometric function. + + Examples + ======== + + >>> from sympy import lowergamma, S + >>> from sympy.abc import s, x + >>> lowergamma(s, x) + lowergamma(s, x) + >>> lowergamma(3, x) + -2*(x**2/2 + x + 1)*exp(-x) + 2 + >>> lowergamma(-S(1)/2, x) + -2*sqrt(pi)*erf(sqrt(x)) - 2*exp(-x)/sqrt(x) + + See Also + ======== + + gamma: Gamma function. + uppergamma: Upper incomplete gamma function. + polygamma: Polygamma function. + loggamma: Log Gamma function. + digamma: Digamma function. + trigamma: Trigamma function. + sympy.functions.special.beta_functions.beta: Euler Beta function. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Incomplete_gamma_function#Lower_incomplete_gamma_function + .. [2] Abramowitz, Milton; Stegun, Irene A., eds. (1965), Chapter 6, + Section 5, Handbook of Mathematical Functions with Formulas, Graphs, + and Mathematical Tables + .. [3] https://dlmf.nist.gov/8 + .. [4] https://functions.wolfram.com/GammaBetaErf/Gamma2/ + .. [5] https://functions.wolfram.com/GammaBetaErf/Gamma3/ + + """ + + + def fdiff(self, argindex=2): + from sympy.functions.special.hyper import meijerg + if argindex == 2: + a, z = self.args + return exp(-unpolarify(z))*z**(a - 1) + elif argindex == 1: + a, z = self.args + return gamma(a)*digamma(a) - log(z)*uppergamma(a, z) \ + - meijerg([], [1, 1], [0, 0, a], [], z) + + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, a, x): + # For lack of a better place, we use this one to extract branching + # information. The following can be + # found in the literature (c/f references given above), albeit scattered: + # 1) For fixed x != 0, lowergamma(s, x) is an entire function of s + # 2) For fixed positive integers s, lowergamma(s, x) is an entire + # function of x. + # 3) For fixed non-positive integers s, + # lowergamma(s, exp(I*2*pi*n)*x) = + # 2*pi*I*n*(-1)**(-s)/factorial(-s) + lowergamma(s, x) + # (this follows from lowergamma(s, x).diff(x) = x**(s-1)*exp(-x)). + # 4) For fixed non-integral s, + # lowergamma(s, x) = x**s*gamma(s)*lowergamma_unbranched(s, x), + # where lowergamma_unbranched(s, x) is an entire function (in fact + # of both s and x), i.e. + # lowergamma(s, exp(2*I*pi*n)*x) = exp(2*pi*I*n*a)*lowergamma(a, x) + if x is S.Zero: + return S.Zero + nx, n = x.extract_branch_factor() + if a.is_integer and a.is_positive: + nx = unpolarify(x) + if nx != x: + return lowergamma(a, nx) + elif a.is_integer and a.is_nonpositive: + if n != 0: + return 2*pi*I*n*S.NegativeOne**(-a)/factorial(-a) + lowergamma(a, nx) + elif n != 0: + return exp(2*pi*I*n*a)*lowergamma(a, nx) + + # Special values. + if a.is_Number: + if a is S.One: + return S.One - exp(-x) + elif a is S.Half: + return sqrt(pi)*erf(sqrt(x)) + elif a.is_Integer or (2*a).is_Integer: + b = a - 1 + if b.is_positive: + if a.is_integer: + return factorial(b) - exp(-x) * factorial(b) * Add(*[x ** k / factorial(k) for k in range(a)]) + else: + return gamma(a)*(lowergamma(S.Half, x)/sqrt(pi) - exp(-x)*Add(*[x**(k - S.Half)/gamma(S.Half + k) for k in range(1, a + S.Half)])) + + if not a.is_Integer: + return S.NegativeOne**(S.Half - a)*pi*erf(sqrt(x))/gamma(1 - a) + exp(-x)*Add(*[x**(k + a - 1)*gamma(a)/gamma(a + k) for k in range(1, Rational(3, 2) - a)]) + + if x.is_zero: + return S.Zero + + def _eval_evalf(self, prec): + if all(x.is_number for x in self.args): + a = self.args[0]._to_mpmath(prec) + z = self.args[1]._to_mpmath(prec) + with workprec(prec): + res = mp.gammainc(a, 0, z) + return Expr._from_mpmath(res, prec) + else: + return self + + def _eval_conjugate(self): + x = self.args[1] + if x not in (S.Zero, S.NegativeInfinity): + return self.func(self.args[0].conjugate(), x.conjugate()) + + def _eval_is_meromorphic(self, x, a): + # By https://en.wikipedia.org/wiki/Incomplete_gamma_function#Holomorphic_extension, + # lowergamma(s, z) = z**s*gamma(s)*gammastar(s, z), + # where gammastar(s, z) is holomorphic for all s and z. + # Hence the singularities of lowergamma are z = 0 (branch + # point) and nonpositive integer values of s (poles of gamma(s)). + s, z = self.args + args_merom = fuzzy_and([z._eval_is_meromorphic(x, a), + s._eval_is_meromorphic(x, a)]) + if not args_merom: + return args_merom + z0 = z.subs(x, a) + if s.is_integer: + return fuzzy_and([s.is_positive, z0.is_finite]) + s0 = s.subs(x, a) + return fuzzy_and([s0.is_finite, z0.is_finite, fuzzy_not(z0.is_zero)]) + + def _eval_aseries(self, n, args0, x, logx): + from sympy.series.order import O + s, z = self.args + if args0[0] is oo and not z.has(x): + coeff = z**s*exp(-z) + sum_expr = sum(z**k/rf(s, k + 1) for k in range(n - 1)) + o = O(z**s*s**(-n)) + return coeff*sum_expr + o + return super()._eval_aseries(n, args0, x, logx) + + def _eval_rewrite_as_uppergamma(self, s, x, **kwargs): + return gamma(s) - uppergamma(s, x) + + def _eval_rewrite_as_expint(self, s, x, **kwargs): + from sympy.functions.special.error_functions import expint + if s.is_integer and s.is_nonpositive: + return self + return self.rewrite(uppergamma).rewrite(expint) + + def _eval_is_zero(self): + x = self.args[1] + if x.is_zero: + return True + + +class uppergamma(DefinedFunction): + r""" + The upper incomplete gamma function. + + Explanation + =========== + + It can be defined as the meromorphic continuation of + + .. math:: + \Gamma(s, x) := \int_x^\infty t^{s-1} e^{-t} \mathrm{d}t = \Gamma(s) - \gamma(s, x). + + where $\gamma(s, x)$ is the lower incomplete gamma function, + :class:`lowergamma`. This can be shown to be the same as + + .. math:: + \Gamma(s, x) = \Gamma(s) - \frac{x^s}{s} {}_1F_1\left({s \atop s+1} \middle| -x\right), + + where ${}_1F_1$ is the (confluent) hypergeometric function. + + The upper incomplete gamma function is also essentially equivalent to the + generalized exponential integral: + + .. math:: + \operatorname{E}_{n}(x) = \int_{1}^{\infty}{\frac{e^{-xt}}{t^n} \, dt} = x^{n-1}\Gamma(1-n,x). + + Examples + ======== + + >>> from sympy import uppergamma, S + >>> from sympy.abc import s, x + >>> uppergamma(s, x) + uppergamma(s, x) + >>> uppergamma(3, x) + 2*(x**2/2 + x + 1)*exp(-x) + >>> uppergamma(-S(1)/2, x) + -2*sqrt(pi)*erfc(sqrt(x)) + 2*exp(-x)/sqrt(x) + >>> uppergamma(-2, x) + expint(3, x)/x**2 + + See Also + ======== + + gamma: Gamma function. + lowergamma: Lower incomplete gamma function. + polygamma: Polygamma function. + loggamma: Log Gamma function. + digamma: Digamma function. + trigamma: Trigamma function. + sympy.functions.special.beta_functions.beta: Euler Beta function. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Incomplete_gamma_function#Upper_incomplete_gamma_function + .. [2] Abramowitz, Milton; Stegun, Irene A., eds. (1965), Chapter 6, + Section 5, Handbook of Mathematical Functions with Formulas, Graphs, + and Mathematical Tables + .. [3] https://dlmf.nist.gov/8 + .. [4] https://functions.wolfram.com/GammaBetaErf/Gamma2/ + .. [5] https://functions.wolfram.com/GammaBetaErf/Gamma3/ + .. [6] https://en.wikipedia.org/wiki/Exponential_integral#Relation_with_other_functions + + """ + + + def fdiff(self, argindex=2): + from sympy.functions.special.hyper import meijerg + if argindex == 2: + a, z = self.args + return -exp(-unpolarify(z))*z**(a - 1) + elif argindex == 1: + a, z = self.args + return uppergamma(a, z)*log(z) + meijerg([], [1, 1], [0, 0, a], [], z) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_evalf(self, prec): + if all(x.is_number for x in self.args): + a = self.args[0]._to_mpmath(prec) + z = self.args[1]._to_mpmath(prec) + with workprec(prec): + res = mp.gammainc(a, z, mp.inf) + return Expr._from_mpmath(res, prec) + return self + + @classmethod + def eval(cls, a, z): + from sympy.functions.special.error_functions import expint + if z.is_Number: + if z is S.NaN: + return S.NaN + elif z is oo: + return S.Zero + elif z.is_zero: + if re(a).is_positive: + return gamma(a) + + # We extract branching information here. C/f lowergamma. + nx, n = z.extract_branch_factor() + if a.is_integer and a.is_positive: + nx = unpolarify(z) + if z != nx: + return uppergamma(a, nx) + elif a.is_integer and a.is_nonpositive: + if n != 0: + return -2*pi*I*n*S.NegativeOne**(-a)/factorial(-a) + uppergamma(a, nx) + elif n != 0: + return gamma(a)*(1 - exp(2*pi*I*n*a)) + exp(2*pi*I*n*a)*uppergamma(a, nx) + + # Special values. + if a.is_Number: + if a is S.Zero and z.is_positive: + return -Ei(-z) + elif a is S.One: + return exp(-z) + elif a is S.Half: + return sqrt(pi)*erfc(sqrt(z)) + elif a.is_Integer or (2*a).is_Integer: + b = a - 1 + if b.is_positive: + if a.is_integer: + return exp(-z) * factorial(b) * Add(*[z**k / factorial(k) + for k in range(a)]) + else: + return (gamma(a) * erfc(sqrt(z)) + + S.NegativeOne**(a - S(3)/2) * exp(-z) * sqrt(z) + * Add(*[gamma(-S.Half - k) * (-z)**k / gamma(1-a) + for k in range(a - S.Half)])) + elif b.is_Integer: + return expint(-b, z)*unpolarify(z)**(b + 1) + + if not a.is_Integer: + return (S.NegativeOne**(S.Half - a) * pi*erfc(sqrt(z))/gamma(1-a) + - z**a * exp(-z) * Add(*[z**k * gamma(a) / gamma(a+k+1) + for k in range(S.Half - a)])) + + if a.is_zero and z.is_positive: + return -Ei(-z) + + if z.is_zero and re(a).is_positive: + return gamma(a) + + def _eval_conjugate(self): + z = self.args[1] + if z not in (S.Zero, S.NegativeInfinity): + return self.func(self.args[0].conjugate(), z.conjugate()) + + def _eval_is_meromorphic(self, x, a): + return lowergamma._eval_is_meromorphic(self, x, a) + + def _eval_rewrite_as_lowergamma(self, s, x, **kwargs): + return gamma(s) - lowergamma(s, x) + + def _eval_rewrite_as_tractable(self, s, x, **kwargs): + return exp(loggamma(s)) - lowergamma(s, x) + + def _eval_rewrite_as_expint(self, s, x, **kwargs): + from sympy.functions.special.error_functions import expint + return expint(1 - s, x)*x**s + + +############################################################################### +###################### POLYGAMMA and LOGGAMMA FUNCTIONS ####################### +############################################################################### + +class polygamma(DefinedFunction): + r""" + The function ``polygamma(n, z)`` returns ``log(gamma(z)).diff(n + 1)``. + + Explanation + =========== + + It is a meromorphic function on $\mathbb{C}$ and defined as the $(n+1)$-th + derivative of the logarithm of the gamma function: + + .. math:: + \psi^{(n)} (z) := \frac{\mathrm{d}^{n+1}}{\mathrm{d} z^{n+1}} \log\Gamma(z). + + For `n` not a nonnegative integer the generalization by Espinosa and Moll [5]_ + is used: + + .. math:: \psi(s,z) = \frac{\zeta'(s+1, z) + (\gamma + \psi(-s)) \zeta(s+1, z)} + {\Gamma(-s)} + + Examples + ======== + + Several special values are known: + + >>> from sympy import S, polygamma + >>> polygamma(0, 1) + -EulerGamma + >>> polygamma(0, 1/S(2)) + -2*log(2) - EulerGamma + >>> polygamma(0, 1/S(3)) + -log(3) - sqrt(3)*pi/6 - EulerGamma - log(sqrt(3)) + >>> polygamma(0, 1/S(4)) + -pi/2 - log(4) - log(2) - EulerGamma + >>> polygamma(0, 2) + 1 - EulerGamma + >>> polygamma(0, 23) + 19093197/5173168 - EulerGamma + + >>> from sympy import oo, I + >>> polygamma(0, oo) + oo + >>> polygamma(0, -oo) + oo + >>> polygamma(0, I*oo) + oo + >>> polygamma(0, -I*oo) + oo + + Differentiation with respect to $x$ is supported: + + >>> from sympy import Symbol, diff + >>> x = Symbol("x") + >>> diff(polygamma(0, x), x) + polygamma(1, x) + >>> diff(polygamma(0, x), x, 2) + polygamma(2, x) + >>> diff(polygamma(0, x), x, 3) + polygamma(3, x) + >>> diff(polygamma(1, x), x) + polygamma(2, x) + >>> diff(polygamma(1, x), x, 2) + polygamma(3, x) + >>> diff(polygamma(2, x), x) + polygamma(3, x) + >>> diff(polygamma(2, x), x, 2) + polygamma(4, x) + + >>> n = Symbol("n") + >>> diff(polygamma(n, x), x) + polygamma(n + 1, x) + >>> diff(polygamma(n, x), x, 2) + polygamma(n + 2, x) + + We can rewrite ``polygamma`` functions in terms of harmonic numbers: + + >>> from sympy import harmonic + >>> polygamma(0, x).rewrite(harmonic) + harmonic(x - 1) - EulerGamma + >>> polygamma(2, x).rewrite(harmonic) + 2*harmonic(x - 1, 3) - 2*zeta(3) + >>> ni = Symbol("n", integer=True) + >>> polygamma(ni, x).rewrite(harmonic) + (-1)**(n + 1)*(-harmonic(x - 1, n + 1) + zeta(n + 1))*factorial(n) + + See Also + ======== + + gamma: Gamma function. + lowergamma: Lower incomplete gamma function. + uppergamma: Upper incomplete gamma function. + loggamma: Log Gamma function. + digamma: Digamma function. + trigamma: Trigamma function. + sympy.functions.special.beta_functions.beta: Euler Beta function. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Polygamma_function + .. [2] https://mathworld.wolfram.com/PolygammaFunction.html + .. [3] https://functions.wolfram.com/GammaBetaErf/PolyGamma/ + .. [4] https://functions.wolfram.com/GammaBetaErf/PolyGamma2/ + .. [5] O. Espinosa and V. Moll, "A generalized polygamma function", + *Integral Transforms and Special Functions* (2004), 101-115. + + """ + + @classmethod + def eval(cls, n, z): + if n is S.NaN or z is S.NaN: + return S.NaN + elif z is oo: + return oo if n.is_zero else S.Zero + elif z.is_Integer and z.is_nonpositive: + return S.ComplexInfinity + elif n is S.NegativeOne: + return loggamma(z) - log(2*pi) / 2 + elif n.is_zero: + if z is -oo or z.extract_multiplicatively(I) in (oo, -oo): + return oo + elif z.is_Integer: + return harmonic(z-1) - S.EulerGamma + elif z.is_Rational: + # TODO n == 1 also can do some rational z + p, q = z.as_numer_denom() + # only expand for small denominators to avoid creating long expressions + if q <= 6: + return expand_func(polygamma(S.Zero, z, evaluate=False)) + elif n.is_integer and n.is_nonnegative: + nz = unpolarify(z) + if z != nz: + return polygamma(n, nz) + if z.is_Integer: + return S.NegativeOne**(n+1) * factorial(n) * zeta(n+1, z) + elif z is S.Half: + return S.NegativeOne**(n+1) * factorial(n) * (2**(n+1)-1) * zeta(n+1) + + def _eval_is_real(self): + if self.args[0].is_positive and self.args[1].is_positive: + return True + + def _eval_is_complex(self): + z = self.args[1] + is_negative_integer = fuzzy_and([z.is_negative, z.is_integer]) + return fuzzy_and([z.is_complex, fuzzy_not(is_negative_integer)]) + + def _eval_is_positive(self): + n, z = self.args + if n.is_positive: + if n.is_odd and z.is_real: + return True + if n.is_even and z.is_positive: + return False + + def _eval_is_negative(self): + n, z = self.args + if n.is_positive: + if n.is_even and z.is_positive: + return True + if n.is_odd and z.is_real: + return False + + def _eval_expand_func(self, **hints): + n, z = self.args + + if n.is_Integer and n.is_nonnegative: + if z.is_Add: + coeff = z.args[0] + if coeff.is_Integer: + e = -(n + 1) + if coeff > 0: + tail = Add(*[Pow( + z - i, e) for i in range(1, int(coeff) + 1)]) + else: + tail = -Add(*[Pow( + z + i, e) for i in range(int(-coeff))]) + return polygamma(n, z - coeff) + S.NegativeOne**n*factorial(n)*tail + + elif z.is_Mul: + coeff, z = z.as_two_terms() + if coeff.is_Integer and coeff.is_positive: + tail = [polygamma(n, z + Rational( + i, coeff)) for i in range(int(coeff))] + if n == 0: + return Add(*tail)/coeff + log(coeff) + else: + return Add(*tail)/coeff**(n + 1) + z *= coeff + + if n == 0 and z.is_Rational: + p, q = z.as_numer_denom() + + # Reference: + # Values of the polygamma functions at rational arguments, J. Choi, 2007 + part_1 = -S.EulerGamma - pi * cot(p * pi / q) / 2 - log(q) + Add( + *[cos(2 * k * pi * p / q) * log(2 * sin(k * pi / q)) for k in range(1, q)]) + + if z > 0: + n = floor(z) + z0 = z - n + return part_1 + Add(*[1 / (z0 + k) for k in range(n)]) + elif z < 0: + n = floor(1 - z) + z0 = z + n + return part_1 - Add(*[1 / (z0 - 1 - k) for k in range(n)]) + + if n == -1: + return loggamma(z) - log(2*pi) / 2 + if n.is_integer is False or n.is_nonnegative is False: + s = Dummy("s") + dzt = zeta(s, z).diff(s).subs(s, n+1) + return (dzt + (S.EulerGamma + digamma(-n)) * zeta(n+1, z)) / gamma(-n) + + return polygamma(n, z) + + def _eval_rewrite_as_zeta(self, n, z, **kwargs): + if n.is_integer and n.is_positive: + return S.NegativeOne**(n + 1)*factorial(n)*zeta(n + 1, z) + + def _eval_rewrite_as_harmonic(self, n, z, **kwargs): + if n.is_integer: + if n.is_zero: + return harmonic(z - 1) - S.EulerGamma + else: + return S.NegativeOne**(n+1) * factorial(n) * (zeta(n+1) - harmonic(z-1, n+1)) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.series.order import Order + n, z = [a.as_leading_term(x) for a in self.args] + o = Order(z, x) + if n == 0 and o.contains(1/x): + logx = log(x) if logx is None else logx + return o.getn() * logx + else: + return self.func(n, z) + + def fdiff(self, argindex=2): + if argindex == 2: + n, z = self.args[:2] + return polygamma(n + 1, z) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_aseries(self, n, args0, x, logx): + from sympy.series.order import Order + if args0[1] != oo or not \ + (self.args[0].is_Integer and self.args[0].is_nonnegative): + return super()._eval_aseries(n, args0, x, logx) + z = self.args[1] + N = self.args[0] + + if N == 0: + # digamma function series + # Abramowitz & Stegun, p. 259, 6.3.18 + r = log(z) - 1/(2*z) + o = None + if n < 2: + o = Order(1/z, x) + else: + m = ceiling((n + 1)//2) + l = [bernoulli(2*k) / (2*k*z**(2*k)) for k in range(1, m)] + r -= Add(*l) + o = Order(1/z**n, x) + return r._eval_nseries(x, n, logx) + o + else: + # proper polygamma function + # Abramowitz & Stegun, p. 260, 6.4.10 + # We return terms to order higher than O(x**n) on purpose + # -- otherwise we would not be able to return any terms for + # quite a long time! + fac = gamma(N) + e0 = fac + N*fac/(2*z) + m = ceiling((n + 1)//2) + for k in range(1, m): + fac = fac*(2*k + N - 1)*(2*k + N - 2) / ((2*k)*(2*k - 1)) + e0 += bernoulli(2*k)*fac/z**(2*k) + o = Order(1/z**(2*m), x) + if n == 0: + o = Order(1/z, x) + elif n == 1: + o = Order(1/z**2, x) + r = e0._eval_nseries(z, n, logx) + o + return (-1 * (-1/z)**N * r)._eval_nseries(x, n, logx) + + def _eval_evalf(self, prec): + if not all(i.is_number for i in self.args): + return + s = self.args[0]._to_mpmath(prec+12) + z = self.args[1]._to_mpmath(prec+12) + if mp.isint(z) and z <= 0: + return S.ComplexInfinity + with workprec(prec+12): + if mp.isint(s) and s >= 0: + res = mp.polygamma(s, z) + else: + zt = mp.zeta(s+1, z) + dzt = mp.zeta(s+1, z, 1) + res = (dzt + (mp.euler + mp.digamma(-s)) * zt) * mp.rgamma(-s) + return Expr._from_mpmath(res, prec) + + +class loggamma(DefinedFunction): + r""" + The ``loggamma`` function implements the logarithm of the + gamma function (i.e., $\log\Gamma(x)$). + + Examples + ======== + + Several special values are known. For numerical integral + arguments we have: + + >>> from sympy import loggamma + >>> loggamma(-2) + oo + >>> loggamma(0) + oo + >>> loggamma(1) + 0 + >>> loggamma(2) + 0 + >>> loggamma(3) + log(2) + + And for symbolic values: + + >>> from sympy import Symbol + >>> n = Symbol("n", integer=True, positive=True) + >>> loggamma(n) + log(gamma(n)) + >>> loggamma(-n) + oo + + For half-integral values: + + >>> from sympy import S + >>> loggamma(S(5)/2) + log(3*sqrt(pi)/4) + >>> loggamma(n/2) + log(2**(1 - n)*sqrt(pi)*gamma(n)/gamma(n/2 + 1/2)) + + And general rational arguments: + + >>> from sympy import expand_func + >>> L = loggamma(S(16)/3) + >>> expand_func(L).doit() + -5*log(3) + loggamma(1/3) + log(4) + log(7) + log(10) + log(13) + >>> L = loggamma(S(19)/4) + >>> expand_func(L).doit() + -4*log(4) + loggamma(3/4) + log(3) + log(7) + log(11) + log(15) + >>> L = loggamma(S(23)/7) + >>> expand_func(L).doit() + -3*log(7) + log(2) + loggamma(2/7) + log(9) + log(16) + + The ``loggamma`` function has the following limits towards infinity: + + >>> from sympy import oo + >>> loggamma(oo) + oo + >>> loggamma(-oo) + zoo + + The ``loggamma`` function obeys the mirror symmetry + if $x \in \mathbb{C} \setminus \{-\infty, 0\}$: + + >>> from sympy.abc import x + >>> from sympy import conjugate + >>> conjugate(loggamma(x)) + loggamma(conjugate(x)) + + Differentiation with respect to $x$ is supported: + + >>> from sympy import diff + >>> diff(loggamma(x), x) + polygamma(0, x) + + Series expansion is also supported: + + >>> from sympy import series + >>> series(loggamma(x), x, 0, 4).cancel() + -log(x) - EulerGamma*x + pi**2*x**2/12 - x**3*zeta(3)/3 + O(x**4) + + We can numerically evaluate the ``loggamma`` function + to arbitrary precision on the whole complex plane: + + >>> from sympy import I + >>> loggamma(5).evalf(30) + 3.17805383034794561964694160130 + >>> loggamma(I).evalf(20) + -0.65092319930185633889 - 1.8724366472624298171*I + + See Also + ======== + + gamma: Gamma function. + lowergamma: Lower incomplete gamma function. + uppergamma: Upper incomplete gamma function. + polygamma: Polygamma function. + digamma: Digamma function. + trigamma: Trigamma function. + sympy.functions.special.beta_functions.beta: Euler Beta function. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gamma_function + .. [2] https://dlmf.nist.gov/5 + .. [3] https://mathworld.wolfram.com/LogGammaFunction.html + .. [4] https://functions.wolfram.com/GammaBetaErf/LogGamma/ + + """ + @classmethod + def eval(cls, z): + if z.is_integer: + if z.is_nonpositive: + return oo + elif z.is_positive: + return log(gamma(z)) + elif z.is_rational: + p, q = z.as_numer_denom() + # Half-integral values: + if p.is_positive and q == 2: + return log(sqrt(pi) * 2**(1 - p) * gamma(p) / gamma((p + 1)*S.Half)) + + if z is oo: + return oo + elif abs(z) is oo: + return S.ComplexInfinity + if z is S.NaN: + return S.NaN + + def _eval_expand_func(self, **hints): + from sympy.concrete.summations import Sum + z = self.args[0] + + if z.is_Rational: + p, q = z.as_numer_denom() + # General rational arguments (u + p/q) + # Split z as n + p/q with p < q + n = p // q + p = p - n*q + if p.is_positive and q.is_positive and p < q: + k = Dummy("k") + if n.is_positive: + return loggamma(p / q) - n*log(q) + Sum(log((k - 1)*q + p), (k, 1, n)) + elif n.is_negative: + return loggamma(p / q) - n*log(q) + pi*I*n - Sum(log(k*q - p), (k, 1, -n)) + elif n.is_zero: + return loggamma(p / q) + + return self + + def _eval_nseries(self, x, n, logx=None, cdir=0): + x0 = self.args[0].limit(x, 0) + if x0.is_zero: + f = self._eval_rewrite_as_intractable(*self.args) + return f._eval_nseries(x, n, logx) + return super()._eval_nseries(x, n, logx) + + def _eval_aseries(self, n, args0, x, logx): + from sympy.series.order import Order + if args0[0] != oo: + return super()._eval_aseries(n, args0, x, logx) + z = self.args[0] + r = log(z)*(z - S.Half) - z + log(2*pi)/2 + l = [bernoulli(2*k) / (2*k*(2*k - 1)*z**(2*k - 1)) for k in range(1, n)] + o = None + if n == 0: + o = Order(1, x) + else: + o = Order(1/z**n, x) + # It is very inefficient to first add the order and then do the nseries + return (r + Add(*l))._eval_nseries(x, n, logx) + o + + def _eval_rewrite_as_intractable(self, z, **kwargs): + return log(gamma(z)) + + def _eval_is_real(self): + z = self.args[0] + if z.is_positive: + return True + elif z.is_nonpositive: + return False + + def _eval_conjugate(self): + z = self.args[0] + if z not in (S.Zero, S.NegativeInfinity): + return self.func(z.conjugate()) + + def fdiff(self, argindex=1): + if argindex == 1: + return polygamma(0, self.args[0]) + else: + raise ArgumentIndexError(self, argindex) + + +class digamma(DefinedFunction): + r""" + The ``digamma`` function is the first derivative of the ``loggamma`` + function + + .. math:: + \psi(x) := \frac{\mathrm{d}}{\mathrm{d} z} \log\Gamma(z) + = \frac{\Gamma'(z)}{\Gamma(z) }. + + In this case, ``digamma(z) = polygamma(0, z)``. + + Examples + ======== + + >>> from sympy import digamma + >>> digamma(0) + zoo + >>> from sympy import Symbol + >>> z = Symbol('z') + >>> digamma(z) + polygamma(0, z) + + To retain ``digamma`` as it is: + + >>> digamma(0, evaluate=False) + digamma(0) + >>> digamma(z, evaluate=False) + digamma(z) + + See Also + ======== + + gamma: Gamma function. + lowergamma: Lower incomplete gamma function. + uppergamma: Upper incomplete gamma function. + polygamma: Polygamma function. + loggamma: Log Gamma function. + trigamma: Trigamma function. + sympy.functions.special.beta_functions.beta: Euler Beta function. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Digamma_function + .. [2] https://mathworld.wolfram.com/DigammaFunction.html + .. [3] https://functions.wolfram.com/GammaBetaErf/PolyGamma2/ + + """ + def _eval_evalf(self, prec): + z = self.args[0] + nprec = prec_to_dps(prec) + return polygamma(0, z).evalf(n=nprec) + + def fdiff(self, argindex=1): + z = self.args[0] + return polygamma(0, z).fdiff() + + def _eval_is_real(self): + z = self.args[0] + return polygamma(0, z).is_real + + def _eval_is_positive(self): + z = self.args[0] + return polygamma(0, z).is_positive + + def _eval_is_negative(self): + z = self.args[0] + return polygamma(0, z).is_negative + + def _eval_aseries(self, n, args0, x, logx): + as_polygamma = self.rewrite(polygamma) + args0 = [S.Zero,] + args0 + return as_polygamma._eval_aseries(n, args0, x, logx) + + @classmethod + def eval(cls, z): + return polygamma(0, z) + + def _eval_expand_func(self, **hints): + z = self.args[0] + return polygamma(0, z).expand(func=True) + + def _eval_rewrite_as_harmonic(self, z, **kwargs): + return harmonic(z - 1) - S.EulerGamma + + def _eval_rewrite_as_polygamma(self, z, **kwargs): + return polygamma(0, z) + + def _eval_as_leading_term(self, x, logx, cdir): + z = self.args[0] + return polygamma(0, z).as_leading_term(x) + + + +class trigamma(DefinedFunction): + r""" + The ``trigamma`` function is the second derivative of the ``loggamma`` + function + + .. math:: + \psi^{(1)}(z) := \frac{\mathrm{d}^{2}}{\mathrm{d} z^{2}} \log\Gamma(z). + + In this case, ``trigamma(z) = polygamma(1, z)``. + + Examples + ======== + + >>> from sympy import trigamma + >>> trigamma(0) + zoo + >>> from sympy import Symbol + >>> z = Symbol('z') + >>> trigamma(z) + polygamma(1, z) + + To retain ``trigamma`` as it is: + + >>> trigamma(0, evaluate=False) + trigamma(0) + >>> trigamma(z, evaluate=False) + trigamma(z) + + + See Also + ======== + + gamma: Gamma function. + lowergamma: Lower incomplete gamma function. + uppergamma: Upper incomplete gamma function. + polygamma: Polygamma function. + loggamma: Log Gamma function. + digamma: Digamma function. + sympy.functions.special.beta_functions.beta: Euler Beta function. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Trigamma_function + .. [2] https://mathworld.wolfram.com/TrigammaFunction.html + .. [3] https://functions.wolfram.com/GammaBetaErf/PolyGamma2/ + + """ + def _eval_evalf(self, prec): + z = self.args[0] + nprec = prec_to_dps(prec) + return polygamma(1, z).evalf(n=nprec) + + def fdiff(self, argindex=1): + z = self.args[0] + return polygamma(1, z).fdiff() + + def _eval_is_real(self): + z = self.args[0] + return polygamma(1, z).is_real + + def _eval_is_positive(self): + z = self.args[0] + return polygamma(1, z).is_positive + + def _eval_is_negative(self): + z = self.args[0] + return polygamma(1, z).is_negative + + def _eval_aseries(self, n, args0, x, logx): + as_polygamma = self.rewrite(polygamma) + args0 = [S.One,] + args0 + return as_polygamma._eval_aseries(n, args0, x, logx) + + @classmethod + def eval(cls, z): + return polygamma(1, z) + + def _eval_expand_func(self, **hints): + z = self.args[0] + return polygamma(1, z).expand(func=True) + + def _eval_rewrite_as_zeta(self, z, **kwargs): + return zeta(2, z) + + def _eval_rewrite_as_polygamma(self, z, **kwargs): + return polygamma(1, z) + + def _eval_rewrite_as_harmonic(self, z, **kwargs): + return -harmonic(z - 1, 2) + pi**2 / 6 + + def _eval_as_leading_term(self, x, logx, cdir): + z = self.args[0] + return polygamma(1, z).as_leading_term(x) + + +############################################################################### +##################### COMPLETE MULTIVARIATE GAMMA FUNCTION #################### +############################################################################### + + +class multigamma(DefinedFunction): + r""" + The multivariate gamma function is a generalization of the gamma function + + .. math:: + \Gamma_p(z) = \pi^{p(p-1)/4}\prod_{k=1}^p \Gamma[z + (1 - k)/2]. + + In a special case, ``multigamma(x, 1) = gamma(x)``. + + Examples + ======== + + >>> from sympy import S, multigamma + >>> from sympy import Symbol + >>> x = Symbol('x') + >>> p = Symbol('p', positive=True, integer=True) + + >>> multigamma(x, p) + pi**(p*(p - 1)/4)*Product(gamma(-_k/2 + x + 1/2), (_k, 1, p)) + + Several special values are known: + + >>> multigamma(1, 1) + 1 + >>> multigamma(4, 1) + 6 + >>> multigamma(S(3)/2, 1) + sqrt(pi)/2 + + Writing ``multigamma`` in terms of the ``gamma`` function: + + >>> multigamma(x, 1) + gamma(x) + + >>> multigamma(x, 2) + sqrt(pi)*gamma(x)*gamma(x - 1/2) + + >>> multigamma(x, 3) + pi**(3/2)*gamma(x)*gamma(x - 1)*gamma(x - 1/2) + + Parameters + ========== + + p : order or dimension of the multivariate gamma function + + See Also + ======== + + gamma, lowergamma, uppergamma, polygamma, loggamma, digamma, trigamma, + sympy.functions.special.beta_functions.beta + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Multivariate_gamma_function + + """ + unbranched = True + + def fdiff(self, argindex=2): + from sympy.concrete.summations import Sum + if argindex == 2: + x, p = self.args + k = Dummy("k") + return self.func(x, p)*Sum(polygamma(0, x + (1 - k)/2), (k, 1, p)) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, x, p): + from sympy.concrete.products import Product + if p.is_positive is False or p.is_integer is False: + raise ValueError('Order parameter p must be positive integer.') + k = Dummy("k") + return (pi**(p*(p - 1)/4)*Product(gamma(x + (1 - k)/2), + (k, 1, p))).doit() + + def _eval_conjugate(self): + x, p = self.args + return self.func(x.conjugate(), p) + + def _eval_is_real(self): + x, p = self.args + y = 2*x + if y.is_integer and (y <= (p - 1)) is True: + return False + if intlike(y) and (y <= (p - 1)): + return False + if y > (p - 1) or y.is_noninteger: + return True diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/hyper.py b/venv/lib/python3.11/site-packages/sympy/functions/special/hyper.py new file mode 100644 index 0000000000000000000000000000000000000000..3943e140821222a510c609a071b5dbbf08883745 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/hyper.py @@ -0,0 +1,1185 @@ +"""Hypergeometric and Meijer G-functions""" +from collections import Counter + +from sympy.core import S, Mod +from sympy.core.add import Add +from sympy.core.expr import Expr +from sympy.core.function import DefinedFunction, Derivative, ArgumentIndexError + +from sympy.core.containers import Tuple +from sympy.core.mul import Mul +from sympy.core.numbers import I, pi, oo, zoo +from sympy.core.parameters import global_parameters +from sympy.core.relational import Ne +from sympy.core.sorting import default_sort_key +from sympy.core.symbol import Dummy + +from sympy.external.gmpy import lcm +from sympy.functions import (sqrt, exp, log, sin, cos, asin, atan, + sinh, cosh, asinh, acosh, atanh, acoth) +from sympy.functions import factorial, RisingFactorial +from sympy.functions.elementary.complexes import Abs, re, unpolarify +from sympy.functions.elementary.exponential import exp_polar +from sympy.functions.elementary.integers import ceiling +from sympy.functions.elementary.piecewise import Piecewise +from sympy.logic.boolalg import (And, Or) +from sympy import ordered + + +class TupleArg(Tuple): + + # This method is only needed because hyper._eval_as_leading_term falls back + # (via super()) on using Function._eval_as_leading_term, which in turn + # calls as_leading_term on the args of the hyper. Ideally hyper should just + # have an _eval_as_leading_term method that handles all cases and this + # method should be removed because leading terms of tuples don't make + # sense. + def as_leading_term(self, *x, logx=None, cdir=0): + return TupleArg(*[f.as_leading_term(*x, logx=logx, cdir=cdir) for f in self.args]) + + def limit(self, x, xlim, dir='+'): + """ Compute limit x->xlim. + """ + from sympy.series.limits import limit + return TupleArg(*[limit(f, x, xlim, dir) for f in self.args]) + + +# TODO should __new__ accept **options? +# TODO should constructors should check if parameters are sensible? + + +def _prep_tuple(v): + """ + Turn an iterable argument *v* into a tuple and unpolarify, since both + hypergeometric and meijer g-functions are unbranched in their parameters. + + Examples + ======== + + >>> from sympy.functions.special.hyper import _prep_tuple + >>> _prep_tuple([1, 2, 3]) + (1, 2, 3) + >>> _prep_tuple((4, 5)) + (4, 5) + >>> _prep_tuple((7, 8, 9)) + (7, 8, 9) + + """ + return TupleArg(*[unpolarify(x) for x in v]) + + +class TupleParametersBase(DefinedFunction): + """ Base class that takes care of differentiation, when some of + the arguments are actually tuples. """ + # This is not deduced automatically since there are Tuples as arguments. + is_commutative = True + + def _eval_derivative(self, s): + try: + res = 0 + if self.args[0].has(s) or self.args[1].has(s): + for i, p in enumerate(self._diffargs): + m = self._diffargs[i].diff(s) + if m != 0: + res += self.fdiff((1, i))*m + return res + self.fdiff(3)*self.args[2].diff(s) + except (ArgumentIndexError, NotImplementedError): + return Derivative(self, s) + + +class hyper(TupleParametersBase): + r""" + The generalized hypergeometric function is defined by a series where + the ratios of successive terms are a rational function of the summation + index. When convergent, it is continued analytically to the largest + possible domain. + + Explanation + =========== + + The hypergeometric function depends on two vectors of parameters, called + the numerator parameters $a_p$, and the denominator parameters + $b_q$. It also has an argument $z$. The series definition is + + .. math :: + {}_pF_q\left(\begin{matrix} a_1, \cdots, a_p \\ b_1, \cdots, b_q \end{matrix} + \middle| z \right) + = \sum_{n=0}^\infty \frac{(a_1)_n \cdots (a_p)_n}{(b_1)_n \cdots (b_q)_n} + \frac{z^n}{n!}, + + where $(a)_n = (a)(a+1)\cdots(a+n-1)$ denotes the rising factorial. + + If one of the $b_q$ is a non-positive integer then the series is + undefined unless one of the $a_p$ is a larger (i.e., smaller in + magnitude) non-positive integer. If none of the $b_q$ is a + non-positive integer and one of the $a_p$ is a non-positive + integer, then the series reduces to a polynomial. To simplify the + following discussion, we assume that none of the $a_p$ or + $b_q$ is a non-positive integer. For more details, see the + references. + + The series converges for all $z$ if $p \le q$, and thus + defines an entire single-valued function in this case. If $p = + q+1$ the series converges for $|z| < 1$, and can be continued + analytically into a half-plane. If $p > q+1$ the series is + divergent for all $z$. + + Please note the hypergeometric function constructor currently does *not* + check if the parameters actually yield a well-defined function. + + Examples + ======== + + The parameters $a_p$ and $b_q$ can be passed as arbitrary + iterables, for example: + + >>> from sympy import hyper + >>> from sympy.abc import x, n, a + >>> h = hyper((1, 2, 3), [3, 4], x); h + hyper((1, 2), (4,), x) + >>> hyper((3, 1, 2), [3, 4], x, evaluate=False) # don't remove duplicates + hyper((1, 2, 3), (3, 4), x) + + There is also pretty printing (it looks better using Unicode): + + >>> from sympy import pprint + >>> pprint(h, use_unicode=False) + _ + |_ /1, 2 | \ + | | | x| + 2 1 \ 4 | / + + The parameters must always be iterables, even if they are vectors of + length one or zero: + + >>> hyper((1, ), [], x) + hyper((1,), (), x) + + But of course they may be variables (but if they depend on $x$ then you + should not expect much implemented functionality): + + >>> hyper((n, a), (n**2,), x) + hyper((a, n), (n**2,), x) + + The hypergeometric function generalizes many named special functions. + The function ``hyperexpand()`` tries to express a hypergeometric function + using named special functions. For example: + + >>> from sympy import hyperexpand + >>> hyperexpand(hyper([], [], x)) + exp(x) + + You can also use ``expand_func()``: + + >>> from sympy import expand_func + >>> expand_func(x*hyper([1, 1], [2], -x)) + log(x + 1) + + More examples: + + >>> from sympy import S + >>> hyperexpand(hyper([], [S(1)/2], -x**2/4)) + cos(x) + >>> hyperexpand(x*hyper([S(1)/2, S(1)/2], [S(3)/2], x**2)) + asin(x) + + We can also sometimes ``hyperexpand()`` parametric functions: + + >>> from sympy.abc import a + >>> hyperexpand(hyper([-a], [], x)) + (1 - x)**a + + See Also + ======== + + sympy.simplify.hyperexpand + gamma + meijerg + + References + ========== + + .. [1] Luke, Y. L. (1969), The Special Functions and Their Approximations, + Volume 1 + .. [2] https://en.wikipedia.org/wiki/Generalized_hypergeometric_function + + """ + + + def __new__(cls, ap, bq, z, **kwargs): + # TODO should we check convergence conditions? + if kwargs.pop('evaluate', global_parameters.evaluate): + ca = Counter(Tuple(*ap)) + cb = Counter(Tuple(*bq)) + common = ca & cb + arg = ap, bq = [], [] + for i, c in enumerate((ca, cb)): + c -= common + for k in ordered(c): + arg[i].extend([k]*c[k]) + else: + ap = list(ordered(ap)) + bq = list(ordered(bq)) + return super().__new__(cls, _prep_tuple(ap), _prep_tuple(bq), z, **kwargs) + + @classmethod + def eval(cls, ap, bq, z): + if len(ap) <= len(bq) or (len(ap) == len(bq) + 1 and (Abs(z) <= 1) == True): + nz = unpolarify(z) + if z != nz: + return hyper(ap, bq, nz) + + def fdiff(self, argindex=3): + if argindex != 3: + raise ArgumentIndexError(self, argindex) + nap = Tuple(*[a + 1 for a in self.ap]) + nbq = Tuple(*[b + 1 for b in self.bq]) + fac = Mul(*self.ap)/Mul(*self.bq) + return fac*hyper(nap, nbq, self.argument) + + def _eval_expand_func(self, **hints): + from sympy.functions.special.gamma_functions import gamma + from sympy.simplify.hyperexpand import hyperexpand + if len(self.ap) == 2 and len(self.bq) == 1 and self.argument == 1: + a, b = self.ap + c = self.bq[0] + return gamma(c)*gamma(c - a - b)/gamma(c - a)/gamma(c - b) + return hyperexpand(self) + + def _eval_rewrite_as_Sum(self, ap, bq, z, **kwargs): + from sympy.concrete.summations import Sum + n = Dummy("n", integer=True) + rfap = [RisingFactorial(a, n) for a in ap] + rfbq = [RisingFactorial(b, n) for b in bq] + coeff = Mul(*rfap) / Mul(*rfbq) + return Piecewise((Sum(coeff * z**n / factorial(n), (n, 0, oo)), + self.convergence_statement), (self, True)) + + def _eval_as_leading_term(self, x, logx, cdir): + arg = self.args[2] + x0 = arg.subs(x, 0) + if x0 is S.NaN: + x0 = arg.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + + if x0 is S.Zero: + return S.One + return super()._eval_as_leading_term(x, logx=logx, cdir=cdir) + + def _eval_nseries(self, x, n, logx, cdir=0): + + from sympy.series.order import Order + + arg = self.args[2] + x0 = arg.limit(x, 0) + ap = self.args[0] + bq = self.args[1] + + if not (arg == x and x0 == 0): + # It would be better to do something with arg.nseries here, rather + # than falling back on Function._eval_nseries. The code below + # though is not sufficient if arg is something like x/(x+1). + from sympy.simplify.hyperexpand import hyperexpand + return hyperexpand(super()._eval_nseries(x, n, logx)) + + terms = [] + + for i in range(n): + num = Mul(*[RisingFactorial(a, i) for a in ap]) + den = Mul(*[RisingFactorial(b, i) for b in bq]) + terms.append(((num/den) * (arg**i)) / factorial(i)) + + return (Add(*terms) + Order(x**n,x)) + + @property + def argument(self): + """ Argument of the hypergeometric function. """ + return self.args[2] + + @property + def ap(self): + """ Numerator parameters of the hypergeometric function. """ + return Tuple(*self.args[0]) + + @property + def bq(self): + """ Denominator parameters of the hypergeometric function. """ + return Tuple(*self.args[1]) + + @property + def _diffargs(self): + return self.ap + self.bq + + @property + def eta(self): + """ A quantity related to the convergence of the series. """ + return sum(self.ap) - sum(self.bq) + + @property + def radius_of_convergence(self): + """ + Compute the radius of convergence of the defining series. + + Explanation + =========== + + Note that even if this is not ``oo``, the function may still be + evaluated outside of the radius of convergence by analytic + continuation. But if this is zero, then the function is not actually + defined anywhere else. + + Examples + ======== + + >>> from sympy import hyper + >>> from sympy.abc import z + >>> hyper((1, 2), [3], z).radius_of_convergence + 1 + >>> hyper((1, 2, 3), [4], z).radius_of_convergence + 0 + >>> hyper((1, 2), (3, 4), z).radius_of_convergence + oo + + """ + if any(a.is_integer and (a <= 0) == True for a in self.ap + self.bq): + aints = [a for a in self.ap if a.is_Integer and (a <= 0) == True] + bints = [a for a in self.bq if a.is_Integer and (a <= 0) == True] + if len(aints) < len(bints): + return S.Zero + popped = False + for b in bints: + cancelled = False + while aints: + a = aints.pop() + if a >= b: + cancelled = True + break + popped = True + if not cancelled: + return S.Zero + if aints or popped: + # There are still non-positive numerator parameters. + # This is a polynomial. + return oo + if len(self.ap) == len(self.bq) + 1: + return S.One + elif len(self.ap) <= len(self.bq): + return oo + else: + return S.Zero + + @property + def convergence_statement(self): + """ Return a condition on z under which the series converges. """ + R = self.radius_of_convergence + if R == 0: + return False + if R == oo: + return True + # The special functions and their approximations, page 44 + e = self.eta + z = self.argument + c1 = And(re(e) < 0, abs(z) <= 1) + c2 = And(0 <= re(e), re(e) < 1, abs(z) <= 1, Ne(z, 1)) + c3 = And(re(e) >= 1, abs(z) < 1) + return Or(c1, c2, c3) + + def _eval_simplify(self, **kwargs): + from sympy.simplify.hyperexpand import hyperexpand + return hyperexpand(self) + + +class meijerg(TupleParametersBase): + r""" + The Meijer G-function is defined by a Mellin-Barnes type integral that + resembles an inverse Mellin transform. It generalizes the hypergeometric + functions. + + Explanation + =========== + + The Meijer G-function depends on four sets of parameters. There are + "*numerator parameters*" + $a_1, \ldots, a_n$ and $a_{n+1}, \ldots, a_p$, and there are + "*denominator parameters*" + $b_1, \ldots, b_m$ and $b_{m+1}, \ldots, b_q$. + Confusingly, it is traditionally denoted as follows (note the position + of $m$, $n$, $p$, $q$, and how they relate to the lengths of the four + parameter vectors): + + .. math :: + G_{p,q}^{m,n} \left(\begin{matrix}a_1, \cdots, a_n & a_{n+1}, \cdots, a_p \\ + b_1, \cdots, b_m & b_{m+1}, \cdots, b_q + \end{matrix} \middle| z \right). + + However, in SymPy the four parameter vectors are always available + separately (see examples), so that there is no need to keep track of the + decorating sub- and super-scripts on the G symbol. + + The G function is defined as the following integral: + + .. math :: + \frac{1}{2 \pi i} \int_L \frac{\prod_{j=1}^m \Gamma(b_j - s) + \prod_{j=1}^n \Gamma(1 - a_j + s)}{\prod_{j=m+1}^q \Gamma(1- b_j +s) + \prod_{j=n+1}^p \Gamma(a_j - s)} z^s \mathrm{d}s, + + where $\Gamma(z)$ is the gamma function. There are three possible + contours which we will not describe in detail here (see the references). + If the integral converges along more than one of them, the definitions + agree. The contours all separate the poles of $\Gamma(1-a_j+s)$ + from the poles of $\Gamma(b_k-s)$, so in particular the G function + is undefined if $a_j - b_k \in \mathbb{Z}_{>0}$ for some + $j \le n$ and $k \le m$. + + The conditions under which one of the contours yields a convergent integral + are complicated and we do not state them here, see the references. + + Please note currently the Meijer G-function constructor does *not* check any + convergence conditions. + + Examples + ======== + + You can pass the parameters either as four separate vectors: + + >>> from sympy import meijerg, Tuple, pprint + >>> from sympy.abc import x, a + >>> pprint(meijerg((1, 2), (a, 4), (5,), [], x), use_unicode=False) + __1, 2 /1, 2 4, a | \ + /__ | | x| + \_|4, 1 \ 5 | / + + Or as two nested vectors: + + >>> pprint(meijerg([(1, 2), (3, 4)], ([5], Tuple()), x), use_unicode=False) + __1, 2 /1, 2 3, 4 | \ + /__ | | x| + \_|4, 1 \ 5 | / + + As with the hypergeometric function, the parameters may be passed as + arbitrary iterables. Vectors of length zero and one also have to be + passed as iterables. The parameters need not be constants, but if they + depend on the argument then not much implemented functionality should be + expected. + + All the subvectors of parameters are available: + + >>> from sympy import pprint + >>> g = meijerg([1], [2], [3], [4], x) + >>> pprint(g, use_unicode=False) + __1, 1 /1 2 | \ + /__ | | x| + \_|2, 2 \3 4 | / + >>> g.an + (1,) + >>> g.ap + (1, 2) + >>> g.aother + (2,) + >>> g.bm + (3,) + >>> g.bq + (3, 4) + >>> g.bother + (4,) + + The Meijer G-function generalizes the hypergeometric functions. + In some cases it can be expressed in terms of hypergeometric functions, + using Slater's theorem. For example: + + >>> from sympy import hyperexpand + >>> from sympy.abc import a, b, c + >>> hyperexpand(meijerg([a], [], [c], [b], x), allow_hyper=True) + x**c*gamma(-a + c + 1)*hyper((-a + c + 1,), + (-b + c + 1,), -x)/gamma(-b + c + 1) + + Thus the Meijer G-function also subsumes many named functions as special + cases. You can use ``expand_func()`` or ``hyperexpand()`` to (try to) + rewrite a Meijer G-function in terms of named special functions. For + example: + + >>> from sympy import expand_func, S + >>> expand_func(meijerg([[],[]], [[0],[]], -x)) + exp(x) + >>> hyperexpand(meijerg([[],[]], [[S(1)/2],[0]], (x/2)**2)) + sin(x)/sqrt(pi) + + See Also + ======== + + hyper + sympy.simplify.hyperexpand + + References + ========== + + .. [1] Luke, Y. L. (1969), The Special Functions and Their Approximations, + Volume 1 + .. [2] https://en.wikipedia.org/wiki/Meijer_G-function + + """ + + + def __new__(cls, *args, **kwargs): + if len(args) == 5: + args = [(args[0], args[1]), (args[2], args[3]), args[4]] + if len(args) != 3: + raise TypeError("args must be either as, as', bs, bs', z or " + "as, bs, z") + + def tr(p): + if len(p) != 2: + raise TypeError("wrong argument") + p = [list(ordered(i)) for i in p] + return TupleArg(_prep_tuple(p[0]), _prep_tuple(p[1])) + + arg0, arg1 = tr(args[0]), tr(args[1]) + if Tuple(arg0, arg1).has(oo, zoo, -oo): + raise ValueError("G-function parameters must be finite") + if any((a - b).is_Integer and a - b > 0 + for a in arg0[0] for b in arg1[0]): + raise ValueError("no parameter a1, ..., an may differ from " + "any b1, ..., bm by a positive integer") + + # TODO should we check convergence conditions? + return super().__new__(cls, arg0, arg1, args[2], **kwargs) + + def fdiff(self, argindex=3): + if argindex != 3: + return self._diff_wrt_parameter(argindex[1]) + if len(self.an) >= 1: + a = list(self.an) + a[0] -= 1 + G = meijerg(a, self.aother, self.bm, self.bother, self.argument) + return 1/self.argument * ((self.an[0] - 1)*self + G) + elif len(self.bm) >= 1: + b = list(self.bm) + b[0] += 1 + G = meijerg(self.an, self.aother, b, self.bother, self.argument) + return 1/self.argument * (self.bm[0]*self - G) + else: + return S.Zero + + def _diff_wrt_parameter(self, idx): + # Differentiation wrt a parameter can only be done in very special + # cases. In particular, if we want to differentiate with respect to + # `a`, all other gamma factors have to reduce to rational functions. + # + # Let MT denote mellin transform. Suppose T(-s) is the gamma factor + # appearing in the definition of G. Then + # + # MT(log(z)G(z)) = d/ds T(s) = d/da T(s) + ... + # + # Thus d/da G(z) = log(z)G(z) - ... + # The ... can be evaluated as a G function under the above conditions, + # the formula being most easily derived by using + # + # d Gamma(s + n) Gamma(s + n) / 1 1 1 \ + # -- ------------ = ------------ | - + ---- + ... + --------- | + # ds Gamma(s) Gamma(s) \ s s + 1 s + n - 1 / + # + # which follows from the difference equation of the digamma function. + # (There is a similar equation for -n instead of +n). + + # We first figure out how to pair the parameters. + an = list(self.an) + ap = list(self.aother) + bm = list(self.bm) + bq = list(self.bother) + if idx < len(an): + an.pop(idx) + else: + idx -= len(an) + if idx < len(ap): + ap.pop(idx) + else: + idx -= len(ap) + if idx < len(bm): + bm.pop(idx) + else: + bq.pop(idx - len(bm)) + pairs1 = [] + pairs2 = [] + for l1, l2, pairs in [(an, bq, pairs1), (ap, bm, pairs2)]: + while l1: + x = l1.pop() + found = None + for i, y in enumerate(l2): + if not Mod((x - y).simplify(), 1): + found = i + break + if found is None: + raise NotImplementedError('Derivative not expressible ' + 'as G-function?') + y = l2[i] + l2.pop(i) + pairs.append((x, y)) + + # Now build the result. + res = log(self.argument)*self + + for a, b in pairs1: + sign = 1 + n = a - b + base = b + if n < 0: + sign = -1 + n = b - a + base = a + for k in range(n): + res -= sign*meijerg(self.an + (base + k + 1,), self.aother, + self.bm, self.bother + (base + k + 0,), + self.argument) + + for a, b in pairs2: + sign = 1 + n = b - a + base = a + if n < 0: + sign = -1 + n = a - b + base = b + for k in range(n): + res -= sign*meijerg(self.an, self.aother + (base + k + 1,), + self.bm + (base + k + 0,), self.bother, + self.argument) + + return res + + def get_period(self): + """ + Return a number $P$ such that $G(x*exp(I*P)) == G(x)$. + + Examples + ======== + + >>> from sympy import meijerg, pi, S + >>> from sympy.abc import z + + >>> meijerg([1], [], [], [], z).get_period() + 2*pi + >>> meijerg([pi], [], [], [], z).get_period() + oo + >>> meijerg([1, 2], [], [], [], z).get_period() + oo + >>> meijerg([1,1], [2], [1, S(1)/2, S(1)/3], [1], z).get_period() + 12*pi + + """ + # This follows from slater's theorem. + def compute(l): + # first check that no two differ by an integer + for i, b in enumerate(l): + if not b.is_Rational: + return oo + for j in range(i + 1, len(l)): + if not Mod((b - l[j]).simplify(), 1): + return oo + return lcm(*(x.q for x in l)) + beta = compute(self.bm) + alpha = compute(self.an) + p, q = len(self.ap), len(self.bq) + if p == q: + if oo in (alpha, beta): + return oo + return 2*pi*lcm(alpha, beta) + elif p < q: + return 2*pi*beta + else: + return 2*pi*alpha + + def _eval_expand_func(self, **hints): + from sympy.simplify.hyperexpand import hyperexpand + return hyperexpand(self) + + def _eval_evalf(self, prec): + # The default code is insufficient for polar arguments. + # mpmath provides an optional argument "r", which evaluates + # G(z**(1/r)). I am not sure what its intended use is, but we hijack it + # here in the following way: to evaluate at a number z of |argument| + # less than (say) n*pi, we put r=1/n, compute z' = root(z, n) + # (carefully so as not to loose the branch information), and evaluate + # G(z'**(1/r)) = G(z'**n) = G(z). + import mpmath + znum = self.argument._eval_evalf(prec) + if znum.has(exp_polar): + znum, branch = znum.as_coeff_mul(exp_polar) + if len(branch) != 1: + return + branch = branch[0].args[0]/I + else: + branch = S.Zero + n = ceiling(abs(branch/pi)) + 1 + znum = znum**(S.One/n)*exp(I*branch / n) + + # Convert all args to mpf or mpc + try: + [z, r, ap, bq] = [arg._to_mpmath(prec) + for arg in [znum, 1/n, self.args[0], self.args[1]]] + except ValueError: + return + + with mpmath.workprec(prec): + v = mpmath.meijerg(ap, bq, z, r) + + return Expr._from_mpmath(v, prec) + + def _eval_as_leading_term(self, x, logx, cdir): + from sympy.simplify.hyperexpand import hyperexpand + return hyperexpand(self).as_leading_term(x, logx=logx, cdir=cdir) + + def integrand(self, s): + """ Get the defining integrand D(s). """ + from sympy.functions.special.gamma_functions import gamma + return self.argument**s \ + * Mul(*(gamma(b - s) for b in self.bm)) \ + * Mul(*(gamma(1 - a + s) for a in self.an)) \ + / Mul(*(gamma(1 - b + s) for b in self.bother)) \ + / Mul(*(gamma(a - s) for a in self.aother)) + + @property + def argument(self): + """ Argument of the Meijer G-function. """ + return self.args[2] + + @property + def an(self): + """ First set of numerator parameters. """ + return Tuple(*self.args[0][0]) + + @property + def ap(self): + """ Combined numerator parameters. """ + return Tuple(*(self.args[0][0] + self.args[0][1])) + + @property + def aother(self): + """ Second set of numerator parameters. """ + return Tuple(*self.args[0][1]) + + @property + def bm(self): + """ First set of denominator parameters. """ + return Tuple(*self.args[1][0]) + + @property + def bq(self): + """ Combined denominator parameters. """ + return Tuple(*(self.args[1][0] + self.args[1][1])) + + @property + def bother(self): + """ Second set of denominator parameters. """ + return Tuple(*self.args[1][1]) + + @property + def _diffargs(self): + return self.ap + self.bq + + @property + def nu(self): + """ A quantity related to the convergence region of the integral, + c.f. references. """ + return sum(self.bq) - sum(self.ap) + + @property + def delta(self): + """ A quantity related to the convergence region of the integral, + c.f. references. """ + return len(self.bm) + len(self.an) - S(len(self.ap) + len(self.bq))/2 + + @property + def is_number(self): + """ Returns true if expression has numeric data only. """ + return not self.free_symbols + + +class HyperRep(DefinedFunction): + """ + A base class for "hyper representation functions". + + This is used exclusively in ``hyperexpand()``, but fits more logically here. + + pFq is branched at 1 if p == q+1. For use with slater-expansion, we want + define an "analytic continuation" to all polar numbers, which is + continuous on circles and on the ray t*exp_polar(I*pi). Moreover, we want + a "nice" expression for the various cases. + + This base class contains the core logic, concrete derived classes only + supply the actual functions. + + """ + + + @classmethod + def eval(cls, *args): + newargs = tuple(map(unpolarify, args[:-1])) + args[-1:] + if args != newargs: + return cls(*newargs) + + @classmethod + def _expr_small(cls, x): + """ An expression for F(x) which holds for |x| < 1. """ + raise NotImplementedError + + @classmethod + def _expr_small_minus(cls, x): + """ An expression for F(-x) which holds for |x| < 1. """ + raise NotImplementedError + + @classmethod + def _expr_big(cls, x, n): + """ An expression for F(exp_polar(2*I*pi*n)*x), |x| > 1. """ + raise NotImplementedError + + @classmethod + def _expr_big_minus(cls, x, n): + """ An expression for F(exp_polar(2*I*pi*n + pi*I)*x), |x| > 1. """ + raise NotImplementedError + + def _eval_rewrite_as_nonrep(self, *args, **kwargs): + x, n = self.args[-1].extract_branch_factor(allow_half=True) + minus = False + newargs = self.args[:-1] + (x,) + if not n.is_Integer: + minus = True + n -= S.Half + newerargs = newargs + (n,) + if minus: + small = self._expr_small_minus(*newargs) + big = self._expr_big_minus(*newerargs) + else: + small = self._expr_small(*newargs) + big = self._expr_big(*newerargs) + + if big == small: + return small + return Piecewise((big, abs(x) > 1), (small, True)) + + def _eval_rewrite_as_nonrepsmall(self, *args, **kwargs): + x, n = self.args[-1].extract_branch_factor(allow_half=True) + args = self.args[:-1] + (x,) + if not n.is_Integer: + return self._expr_small_minus(*args) + return self._expr_small(*args) + + +class HyperRep_power1(HyperRep): + """ Return a representative for hyper([-a], [], z) == (1 - z)**a. """ + + @classmethod + def _expr_small(cls, a, x): + return (1 - x)**a + + @classmethod + def _expr_small_minus(cls, a, x): + return (1 + x)**a + + @classmethod + def _expr_big(cls, a, x, n): + if a.is_integer: + return cls._expr_small(a, x) + return (x - 1)**a*exp((2*n - 1)*pi*I*a) + + @classmethod + def _expr_big_minus(cls, a, x, n): + if a.is_integer: + return cls._expr_small_minus(a, x) + return (1 + x)**a*exp(2*n*pi*I*a) + + +class HyperRep_power2(HyperRep): + """ Return a representative for hyper([a, a - 1/2], [2*a], z). """ + + @classmethod + def _expr_small(cls, a, x): + return 2**(2*a - 1)*(1 + sqrt(1 - x))**(1 - 2*a) + + @classmethod + def _expr_small_minus(cls, a, x): + return 2**(2*a - 1)*(1 + sqrt(1 + x))**(1 - 2*a) + + @classmethod + def _expr_big(cls, a, x, n): + sgn = -1 + if n.is_odd: + sgn = 1 + n -= 1 + return 2**(2*a - 1)*(1 + sgn*I*sqrt(x - 1))**(1 - 2*a) \ + *exp(-2*n*pi*I*a) + + @classmethod + def _expr_big_minus(cls, a, x, n): + sgn = 1 + if n.is_odd: + sgn = -1 + return sgn*2**(2*a - 1)*(sqrt(1 + x) + sgn)**(1 - 2*a)*exp(-2*pi*I*a*n) + + +class HyperRep_log1(HyperRep): + """ Represent -z*hyper([1, 1], [2], z) == log(1 - z). """ + @classmethod + def _expr_small(cls, x): + return log(1 - x) + + @classmethod + def _expr_small_minus(cls, x): + return log(1 + x) + + @classmethod + def _expr_big(cls, x, n): + return log(x - 1) + (2*n - 1)*pi*I + + @classmethod + def _expr_big_minus(cls, x, n): + return log(1 + x) + 2*n*pi*I + + +class HyperRep_atanh(HyperRep): + """ Represent hyper([1/2, 1], [3/2], z) == atanh(sqrt(z))/sqrt(z). """ + @classmethod + def _expr_small(cls, x): + return atanh(sqrt(x))/sqrt(x) + + def _expr_small_minus(cls, x): + return atan(sqrt(x))/sqrt(x) + + def _expr_big(cls, x, n): + if n.is_even: + return (acoth(sqrt(x)) + I*pi/2)/sqrt(x) + else: + return (acoth(sqrt(x)) - I*pi/2)/sqrt(x) + + def _expr_big_minus(cls, x, n): + if n.is_even: + return atan(sqrt(x))/sqrt(x) + else: + return (atan(sqrt(x)) - pi)/sqrt(x) + + +class HyperRep_asin1(HyperRep): + """ Represent hyper([1/2, 1/2], [3/2], z) == asin(sqrt(z))/sqrt(z). """ + @classmethod + def _expr_small(cls, z): + return asin(sqrt(z))/sqrt(z) + + @classmethod + def _expr_small_minus(cls, z): + return asinh(sqrt(z))/sqrt(z) + + @classmethod + def _expr_big(cls, z, n): + return S.NegativeOne**n*((S.Half - n)*pi/sqrt(z) + I*acosh(sqrt(z))/sqrt(z)) + + @classmethod + def _expr_big_minus(cls, z, n): + return S.NegativeOne**n*(asinh(sqrt(z))/sqrt(z) + n*pi*I/sqrt(z)) + + +class HyperRep_asin2(HyperRep): + """ Represent hyper([1, 1], [3/2], z) == asin(sqrt(z))/sqrt(z)/sqrt(1-z). """ + # TODO this can be nicer + @classmethod + def _expr_small(cls, z): + return HyperRep_asin1._expr_small(z) \ + /HyperRep_power1._expr_small(S.Half, z) + + @classmethod + def _expr_small_minus(cls, z): + return HyperRep_asin1._expr_small_minus(z) \ + /HyperRep_power1._expr_small_minus(S.Half, z) + + @classmethod + def _expr_big(cls, z, n): + return HyperRep_asin1._expr_big(z, n) \ + /HyperRep_power1._expr_big(S.Half, z, n) + + @classmethod + def _expr_big_minus(cls, z, n): + return HyperRep_asin1._expr_big_minus(z, n) \ + /HyperRep_power1._expr_big_minus(S.Half, z, n) + + +class HyperRep_sqrts1(HyperRep): + """ Return a representative for hyper([-a, 1/2 - a], [1/2], z). """ + + @classmethod + def _expr_small(cls, a, z): + return ((1 - sqrt(z))**(2*a) + (1 + sqrt(z))**(2*a))/2 + + @classmethod + def _expr_small_minus(cls, a, z): + return (1 + z)**a*cos(2*a*atan(sqrt(z))) + + @classmethod + def _expr_big(cls, a, z, n): + if n.is_even: + return ((sqrt(z) + 1)**(2*a)*exp(2*pi*I*n*a) + + (sqrt(z) - 1)**(2*a)*exp(2*pi*I*(n - 1)*a))/2 + else: + n -= 1 + return ((sqrt(z) - 1)**(2*a)*exp(2*pi*I*a*(n + 1)) + + (sqrt(z) + 1)**(2*a)*exp(2*pi*I*a*n))/2 + + @classmethod + def _expr_big_minus(cls, a, z, n): + if n.is_even: + return (1 + z)**a*exp(2*pi*I*n*a)*cos(2*a*atan(sqrt(z))) + else: + return (1 + z)**a*exp(2*pi*I*n*a)*cos(2*a*atan(sqrt(z)) - 2*pi*a) + + +class HyperRep_sqrts2(HyperRep): + """ Return a representative for + sqrt(z)/2*[(1-sqrt(z))**2a - (1 + sqrt(z))**2a] + == -2*z/(2*a+1) d/dz hyper([-a - 1/2, -a], [1/2], z)""" + + @classmethod + def _expr_small(cls, a, z): + return sqrt(z)*((1 - sqrt(z))**(2*a) - (1 + sqrt(z))**(2*a))/2 + + @classmethod + def _expr_small_minus(cls, a, z): + return sqrt(z)*(1 + z)**a*sin(2*a*atan(sqrt(z))) + + @classmethod + def _expr_big(cls, a, z, n): + if n.is_even: + return sqrt(z)/2*((sqrt(z) - 1)**(2*a)*exp(2*pi*I*a*(n - 1)) - + (sqrt(z) + 1)**(2*a)*exp(2*pi*I*a*n)) + else: + n -= 1 + return sqrt(z)/2*((sqrt(z) - 1)**(2*a)*exp(2*pi*I*a*(n + 1)) - + (sqrt(z) + 1)**(2*a)*exp(2*pi*I*a*n)) + + def _expr_big_minus(cls, a, z, n): + if n.is_even: + return (1 + z)**a*exp(2*pi*I*n*a)*sqrt(z)*sin(2*a*atan(sqrt(z))) + else: + return (1 + z)**a*exp(2*pi*I*n*a)*sqrt(z) \ + *sin(2*a*atan(sqrt(z)) - 2*pi*a) + + +class HyperRep_log2(HyperRep): + """ Represent log(1/2 + sqrt(1 - z)/2) == -z/4*hyper([3/2, 1, 1], [2, 2], z) """ + + @classmethod + def _expr_small(cls, z): + return log(S.Half + sqrt(1 - z)/2) + + @classmethod + def _expr_small_minus(cls, z): + return log(S.Half + sqrt(1 + z)/2) + + @classmethod + def _expr_big(cls, z, n): + if n.is_even: + return (n - S.Half)*pi*I + log(sqrt(z)/2) + I*asin(1/sqrt(z)) + else: + return (n - S.Half)*pi*I + log(sqrt(z)/2) - I*asin(1/sqrt(z)) + + def _expr_big_minus(cls, z, n): + if n.is_even: + return pi*I*n + log(S.Half + sqrt(1 + z)/2) + else: + return pi*I*n + log(sqrt(1 + z)/2 - S.Half) + + +class HyperRep_cosasin(HyperRep): + """ Represent hyper([a, -a], [1/2], z) == cos(2*a*asin(sqrt(z))). """ + # Note there are many alternative expressions, e.g. as powers of a sum of + # square roots. + + @classmethod + def _expr_small(cls, a, z): + return cos(2*a*asin(sqrt(z))) + + @classmethod + def _expr_small_minus(cls, a, z): + return cosh(2*a*asinh(sqrt(z))) + + @classmethod + def _expr_big(cls, a, z, n): + return cosh(2*a*acosh(sqrt(z)) + a*pi*I*(2*n - 1)) + + @classmethod + def _expr_big_minus(cls, a, z, n): + return cosh(2*a*asinh(sqrt(z)) + 2*a*pi*I*n) + + +class HyperRep_sinasin(HyperRep): + """ Represent 2*a*z*hyper([1 - a, 1 + a], [3/2], z) + == sqrt(z)/sqrt(1-z)*sin(2*a*asin(sqrt(z))) """ + + @classmethod + def _expr_small(cls, a, z): + return sqrt(z)/sqrt(1 - z)*sin(2*a*asin(sqrt(z))) + + @classmethod + def _expr_small_minus(cls, a, z): + return -sqrt(z)/sqrt(1 + z)*sinh(2*a*asinh(sqrt(z))) + + @classmethod + def _expr_big(cls, a, z, n): + return -1/sqrt(1 - 1/z)*sinh(2*a*acosh(sqrt(z)) + a*pi*I*(2*n - 1)) + + @classmethod + def _expr_big_minus(cls, a, z, n): + return -1/sqrt(1 + 1/z)*sinh(2*a*asinh(sqrt(z)) + 2*a*pi*I*n) + +class appellf1(DefinedFunction): + r""" + This is the Appell hypergeometric function of two variables as: + + .. math :: + F_1(a,b_1,b_2,c,x,y) = \sum_{m=0}^{\infty} \sum_{n=0}^{\infty} + \frac{(a)_{m+n} (b_1)_m (b_2)_n}{(c)_{m+n}} + \frac{x^m y^n}{m! n!}. + + Examples + ======== + + >>> from sympy import appellf1, symbols + >>> x, y, a, b1, b2, c = symbols('x y a b1 b2 c') + >>> appellf1(2., 1., 6., 4., 5., 6.) + 0.0063339426292673 + >>> appellf1(12., 12., 6., 4., 0.5, 0.12) + 172870711.659936 + >>> appellf1(40, 2, 6, 4, 15, 60) + appellf1(40, 2, 6, 4, 15, 60) + >>> appellf1(20., 12., 10., 3., 0.5, 0.12) + 15605338197184.4 + >>> appellf1(40, 2, 6, 4, x, y) + appellf1(40, 2, 6, 4, x, y) + >>> appellf1(a, b1, b2, c, x, y) + appellf1(a, b1, b2, c, x, y) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Appell_series + .. [2] https://functions.wolfram.com/HypergeometricFunctions/AppellF1/ + + """ + + @classmethod + def eval(cls, a, b1, b2, c, x, y): + if default_sort_key(b1) > default_sort_key(b2): + b1, b2 = b2, b1 + x, y = y, x + return cls(a, b1, b2, c, x, y) + elif b1 == b2 and default_sort_key(x) > default_sort_key(y): + x, y = y, x + return cls(a, b1, b2, c, x, y) + if x == 0 and y == 0: + return S.One + + def fdiff(self, argindex=5): + a, b1, b2, c, x, y = self.args + if argindex == 5: + return (a*b1/c)*appellf1(a + 1, b1 + 1, b2, c + 1, x, y) + elif argindex == 6: + return (a*b2/c)*appellf1(a + 1, b1, b2 + 1, c + 1, x, y) + elif argindex in (1, 2, 3, 4): + return Derivative(self, self.args[argindex-1]) + else: + raise ArgumentIndexError(self, argindex) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/mathieu_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/mathieu_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..66bccd8d3e6dd357e1e0b93fb5cb5ad4c5f1367f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/mathieu_functions.py @@ -0,0 +1,269 @@ +""" This module contains the Mathieu functions. +""" + +from sympy.core.function import DefinedFunction, ArgumentIndexError +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin, cos + + +class MathieuBase(DefinedFunction): + """ + Abstract base class for Mathieu functions. + + This class is meant to reduce code duplication. + + """ + + unbranched = True + + def _eval_conjugate(self): + a, q, z = self.args + return self.func(a.conjugate(), q.conjugate(), z.conjugate()) + + +class mathieus(MathieuBase): + r""" + The Mathieu Sine function $S(a,q,z)$. + + Explanation + =========== + + This function is one solution of the Mathieu differential equation: + + .. math :: + y(x)^{\prime\prime} + (a - 2 q \cos(2 x)) y(x) = 0 + + The other solution is the Mathieu Cosine function. + + Examples + ======== + + >>> from sympy import diff, mathieus + >>> from sympy.abc import a, q, z + + >>> mathieus(a, q, z) + mathieus(a, q, z) + + >>> mathieus(a, 0, z) + sin(sqrt(a)*z) + + >>> diff(mathieus(a, q, z), z) + mathieusprime(a, q, z) + + See Also + ======== + + mathieuc: Mathieu cosine function. + mathieusprime: Derivative of Mathieu sine function. + mathieucprime: Derivative of Mathieu cosine function. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Mathieu_function + .. [2] https://dlmf.nist.gov/28 + .. [3] https://mathworld.wolfram.com/MathieuFunction.html + .. [4] https://functions.wolfram.com/MathieuandSpheroidalFunctions/MathieuS/ + + """ + + def fdiff(self, argindex=1): + if argindex == 3: + a, q, z = self.args + return mathieusprime(a, q, z) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, a, q, z): + if q.is_Number and q.is_zero: + return sin(sqrt(a)*z) + # Try to pull out factors of -1 + if z.could_extract_minus_sign(): + return -cls(a, q, -z) + + +class mathieuc(MathieuBase): + r""" + The Mathieu Cosine function $C(a,q,z)$. + + Explanation + =========== + + This function is one solution of the Mathieu differential equation: + + .. math :: + y(x)^{\prime\prime} + (a - 2 q \cos(2 x)) y(x) = 0 + + The other solution is the Mathieu Sine function. + + Examples + ======== + + >>> from sympy import diff, mathieuc + >>> from sympy.abc import a, q, z + + >>> mathieuc(a, q, z) + mathieuc(a, q, z) + + >>> mathieuc(a, 0, z) + cos(sqrt(a)*z) + + >>> diff(mathieuc(a, q, z), z) + mathieucprime(a, q, z) + + See Also + ======== + + mathieus: Mathieu sine function + mathieusprime: Derivative of Mathieu sine function + mathieucprime: Derivative of Mathieu cosine function + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Mathieu_function + .. [2] https://dlmf.nist.gov/28 + .. [3] https://mathworld.wolfram.com/MathieuFunction.html + .. [4] https://functions.wolfram.com/MathieuandSpheroidalFunctions/MathieuC/ + + """ + + def fdiff(self, argindex=1): + if argindex == 3: + a, q, z = self.args + return mathieucprime(a, q, z) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, a, q, z): + if q.is_Number and q.is_zero: + return cos(sqrt(a)*z) + # Try to pull out factors of -1 + if z.could_extract_minus_sign(): + return cls(a, q, -z) + + +class mathieusprime(MathieuBase): + r""" + The derivative $S^{\prime}(a,q,z)$ of the Mathieu Sine function. + + Explanation + =========== + + This function is one solution of the Mathieu differential equation: + + .. math :: + y(x)^{\prime\prime} + (a - 2 q \cos(2 x)) y(x) = 0 + + The other solution is the Mathieu Cosine function. + + Examples + ======== + + >>> from sympy import diff, mathieusprime + >>> from sympy.abc import a, q, z + + >>> mathieusprime(a, q, z) + mathieusprime(a, q, z) + + >>> mathieusprime(a, 0, z) + sqrt(a)*cos(sqrt(a)*z) + + >>> diff(mathieusprime(a, q, z), z) + (-a + 2*q*cos(2*z))*mathieus(a, q, z) + + See Also + ======== + + mathieus: Mathieu sine function + mathieuc: Mathieu cosine function + mathieucprime: Derivative of Mathieu cosine function + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Mathieu_function + .. [2] https://dlmf.nist.gov/28 + .. [3] https://mathworld.wolfram.com/MathieuFunction.html + .. [4] https://functions.wolfram.com/MathieuandSpheroidalFunctions/MathieuSPrime/ + + """ + + def fdiff(self, argindex=1): + if argindex == 3: + a, q, z = self.args + return (2*q*cos(2*z) - a)*mathieus(a, q, z) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, a, q, z): + if q.is_Number and q.is_zero: + return sqrt(a)*cos(sqrt(a)*z) + # Try to pull out factors of -1 + if z.could_extract_minus_sign(): + return cls(a, q, -z) + + +class mathieucprime(MathieuBase): + r""" + The derivative $C^{\prime}(a,q,z)$ of the Mathieu Cosine function. + + Explanation + =========== + + This function is one solution of the Mathieu differential equation: + + .. math :: + y(x)^{\prime\prime} + (a - 2 q \cos(2 x)) y(x) = 0 + + The other solution is the Mathieu Sine function. + + Examples + ======== + + >>> from sympy import diff, mathieucprime + >>> from sympy.abc import a, q, z + + >>> mathieucprime(a, q, z) + mathieucprime(a, q, z) + + >>> mathieucprime(a, 0, z) + -sqrt(a)*sin(sqrt(a)*z) + + >>> diff(mathieucprime(a, q, z), z) + (-a + 2*q*cos(2*z))*mathieuc(a, q, z) + + See Also + ======== + + mathieus: Mathieu sine function + mathieuc: Mathieu cosine function + mathieusprime: Derivative of Mathieu sine function + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Mathieu_function + .. [2] https://dlmf.nist.gov/28 + .. [3] https://mathworld.wolfram.com/MathieuFunction.html + .. [4] https://functions.wolfram.com/MathieuandSpheroidalFunctions/MathieuCPrime/ + + """ + + def fdiff(self, argindex=1): + if argindex == 3: + a, q, z = self.args + return (2*q*cos(2*z) - a)*mathieuc(a, q, z) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, a, q, z): + if q.is_Number and q.is_zero: + return -sqrt(a)*sin(sqrt(a)*z) + # Try to pull out factors of -1 + if z.could_extract_minus_sign(): + return -cls(a, q, -z) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/polynomials.py b/venv/lib/python3.11/site-packages/sympy/functions/special/polynomials.py new file mode 100644 index 0000000000000000000000000000000000000000..5816baef600baf957c31a9dddaa5571da86d754a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/polynomials.py @@ -0,0 +1,1447 @@ +""" +This module mainly implements special orthogonal polynomials. + +See also functions.combinatorial.numbers which contains some +combinatorial polynomials. + +""" + +from sympy.core import Rational +from sympy.core.function import DefinedFunction, ArgumentIndexError +from sympy.core.singleton import S +from sympy.core.symbol import Dummy +from sympy.functions.combinatorial.factorials import binomial, factorial, RisingFactorial +from sympy.functions.elementary.complexes import re +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import cos, sec +from sympy.functions.special.gamma_functions import gamma +from sympy.functions.special.hyper import hyper +from sympy.polys.orthopolys import (chebyshevt_poly, chebyshevu_poly, + gegenbauer_poly, hermite_poly, hermite_prob_poly, + jacobi_poly, laguerre_poly, legendre_poly) + +_x = Dummy('x') + + +class OrthogonalPolynomial(DefinedFunction): + """Base class for orthogonal polynomials. + """ + + @classmethod + def _eval_at_order(cls, n, x): + if n.is_integer and n >= 0: + return cls._ortho_poly(int(n), _x).subs(_x, x) + + def _eval_conjugate(self): + return self.func(self.args[0], self.args[1].conjugate()) + +#---------------------------------------------------------------------------- +# Jacobi polynomials +# + + +class jacobi(OrthogonalPolynomial): + r""" + Jacobi polynomial $P_n^{\left(\alpha, \beta\right)}(x)$. + + Explanation + =========== + + ``jacobi(n, alpha, beta, x)`` gives the $n$th Jacobi polynomial + in $x$, $P_n^{\left(\alpha, \beta\right)}(x)$. + + The Jacobi polynomials are orthogonal on $[-1, 1]$ with respect + to the weight $\left(1-x\right)^\alpha \left(1+x\right)^\beta$. + + Examples + ======== + + >>> from sympy import jacobi, S, conjugate, diff + >>> from sympy.abc import a, b, n, x + + >>> jacobi(0, a, b, x) + 1 + >>> jacobi(1, a, b, x) + a/2 - b/2 + x*(a/2 + b/2 + 1) + >>> jacobi(2, a, b, x) + a**2/8 - a*b/4 - a/8 + b**2/8 - b/8 + x**2*(a**2/8 + a*b/4 + 7*a/8 + b**2/8 + 7*b/8 + 3/2) + x*(a**2/4 + 3*a/4 - b**2/4 - 3*b/4) - 1/2 + + >>> jacobi(n, a, b, x) + jacobi(n, a, b, x) + + >>> jacobi(n, a, a, x) + RisingFactorial(a + 1, n)*gegenbauer(n, + a + 1/2, x)/RisingFactorial(2*a + 1, n) + + >>> jacobi(n, 0, 0, x) + legendre(n, x) + + >>> jacobi(n, S(1)/2, S(1)/2, x) + RisingFactorial(3/2, n)*chebyshevu(n, x)/factorial(n + 1) + + >>> jacobi(n, -S(1)/2, -S(1)/2, x) + RisingFactorial(1/2, n)*chebyshevt(n, x)/factorial(n) + + >>> jacobi(n, a, b, -x) + (-1)**n*jacobi(n, b, a, x) + + >>> jacobi(n, a, b, 0) + gamma(a + n + 1)*hyper((-n, -b - n), (a + 1,), -1)/(2**n*factorial(n)*gamma(a + 1)) + >>> jacobi(n, a, b, 1) + RisingFactorial(a + 1, n)/factorial(n) + + >>> conjugate(jacobi(n, a, b, x)) + jacobi(n, conjugate(a), conjugate(b), conjugate(x)) + + >>> diff(jacobi(n,a,b,x), x) + (a/2 + b/2 + n/2 + 1/2)*jacobi(n - 1, a + 1, b + 1, x) + + See Also + ======== + + gegenbauer, + chebyshevt_root, chebyshevu, chebyshevu_root, + legendre, assoc_legendre, + hermite, hermite_prob, + laguerre, assoc_laguerre, + sympy.polys.orthopolys.jacobi_poly, + sympy.polys.orthopolys.gegenbauer_poly + sympy.polys.orthopolys.chebyshevt_poly + sympy.polys.orthopolys.chebyshevu_poly + sympy.polys.orthopolys.hermite_poly + sympy.polys.orthopolys.legendre_poly + sympy.polys.orthopolys.laguerre_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Jacobi_polynomials + .. [2] https://mathworld.wolfram.com/JacobiPolynomial.html + .. [3] https://functions.wolfram.com/Polynomials/JacobiP/ + + """ + + @classmethod + def eval(cls, n, a, b, x): + # Simplify to other polynomials + # P^{a, a}_n(x) + if a == b: + if a == Rational(-1, 2): + return RisingFactorial(S.Half, n) / factorial(n) * chebyshevt(n, x) + elif a.is_zero: + return legendre(n, x) + elif a == S.Half: + return RisingFactorial(3*S.Half, n) / factorial(n + 1) * chebyshevu(n, x) + else: + return RisingFactorial(a + 1, n) / RisingFactorial(2*a + 1, n) * gegenbauer(n, a + S.Half, x) + elif b == -a: + # P^{a, -a}_n(x) + return gamma(n + a + 1) / gamma(n + 1) * (1 + x)**(a/2) / (1 - x)**(a/2) * assoc_legendre(n, -a, x) + + if not n.is_Number: + # Symbolic result P^{a,b}_n(x) + # P^{a,b}_n(-x) ---> (-1)**n * P^{b,a}_n(-x) + if x.could_extract_minus_sign(): + return S.NegativeOne**n * jacobi(n, b, a, -x) + # We can evaluate for some special values of x + if x.is_zero: + return (2**(-n) * gamma(a + n + 1) / (gamma(a + 1) * factorial(n)) * + hyper([-b - n, -n], [a + 1], -1)) + if x == S.One: + return RisingFactorial(a + 1, n) / factorial(n) + elif x is S.Infinity: + if n.is_positive: + # Make sure a+b+2*n \notin Z + if (a + b + 2*n).is_integer: + raise ValueError("Error. a + b + 2*n should not be an integer.") + return RisingFactorial(a + b + n + 1, n) * S.Infinity + else: + # n is a given fixed integer, evaluate into polynomial + return jacobi_poly(n, a, b, x) + + def fdiff(self, argindex=4): + from sympy.concrete.summations import Sum + if argindex == 1: + # Diff wrt n + raise ArgumentIndexError(self, argindex) + elif argindex == 2: + # Diff wrt a + n, a, b, x = self.args + k = Dummy("k") + f1 = 1 / (a + b + n + k + 1) + f2 = ((a + b + 2*k + 1) * RisingFactorial(b + k + 1, n - k) / + ((n - k) * RisingFactorial(a + b + k + 1, n - k))) + return Sum(f1 * (jacobi(n, a, b, x) + f2*jacobi(k, a, b, x)), (k, 0, n - 1)) + elif argindex == 3: + # Diff wrt b + n, a, b, x = self.args + k = Dummy("k") + f1 = 1 / (a + b + n + k + 1) + f2 = (-1)**(n - k) * ((a + b + 2*k + 1) * RisingFactorial(a + k + 1, n - k) / + ((n - k) * RisingFactorial(a + b + k + 1, n - k))) + return Sum(f1 * (jacobi(n, a, b, x) + f2*jacobi(k, a, b, x)), (k, 0, n - 1)) + elif argindex == 4: + # Diff wrt x + n, a, b, x = self.args + return S.Half * (a + b + n + 1) * jacobi(n - 1, a + 1, b + 1, x) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Sum(self, n, a, b, x, **kwargs): + from sympy.concrete.summations import Sum + # Make sure n \in N + if n.is_negative or n.is_integer is False: + raise ValueError("Error: n should be a non-negative integer.") + k = Dummy("k") + kern = (RisingFactorial(-n, k) * RisingFactorial(a + b + n + 1, k) * RisingFactorial(a + k + 1, n - k) / + factorial(k) * ((1 - x)/2)**k) + return 1 / factorial(n) * Sum(kern, (k, 0, n)) + + def _eval_rewrite_as_polynomial(self, n, a, b, x, **kwargs): + # This function is just kept for backwards compatibility + # but should not be used + return self._eval_rewrite_as_Sum(n, a, b, x, **kwargs) + + def _eval_conjugate(self): + n, a, b, x = self.args + return self.func(n, a.conjugate(), b.conjugate(), x.conjugate()) + + +def jacobi_normalized(n, a, b, x): + r""" + Jacobi polynomial $P_n^{\left(\alpha, \beta\right)}(x)$. + + Explanation + =========== + + ``jacobi_normalized(n, alpha, beta, x)`` gives the $n$th + Jacobi polynomial in $x$, $P_n^{\left(\alpha, \beta\right)}(x)$. + + The Jacobi polynomials are orthogonal on $[-1, 1]$ with respect + to the weight $\left(1-x\right)^\alpha \left(1+x\right)^\beta$. + + This functions returns the polynomials normilzed: + + .. math:: + + \int_{-1}^{1} + P_m^{\left(\alpha, \beta\right)}(x) + P_n^{\left(\alpha, \beta\right)}(x) + (1-x)^{\alpha} (1+x)^{\beta} \mathrm{d}x + = \delta_{m,n} + + Examples + ======== + + >>> from sympy import jacobi_normalized + >>> from sympy.abc import n,a,b,x + + >>> jacobi_normalized(n, a, b, x) + jacobi(n, a, b, x)/sqrt(2**(a + b + 1)*gamma(a + n + 1)*gamma(b + n + 1)/((a + b + 2*n + 1)*factorial(n)*gamma(a + b + n + 1))) + + Parameters + ========== + + n : integer degree of polynomial + + a : alpha value + + b : beta value + + x : symbol + + See Also + ======== + + gegenbauer, + chebyshevt_root, chebyshevu, chebyshevu_root, + legendre, assoc_legendre, + hermite, hermite_prob, + laguerre, assoc_laguerre, + sympy.polys.orthopolys.jacobi_poly, + sympy.polys.orthopolys.gegenbauer_poly + sympy.polys.orthopolys.chebyshevt_poly + sympy.polys.orthopolys.chebyshevu_poly + sympy.polys.orthopolys.hermite_poly + sympy.polys.orthopolys.legendre_poly + sympy.polys.orthopolys.laguerre_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Jacobi_polynomials + .. [2] https://mathworld.wolfram.com/JacobiPolynomial.html + .. [3] https://functions.wolfram.com/Polynomials/JacobiP/ + + """ + nfactor = (S(2)**(a + b + 1) * (gamma(n + a + 1) * gamma(n + b + 1)) + / (2*n + a + b + 1) / (factorial(n) * gamma(n + a + b + 1))) + + return jacobi(n, a, b, x) / sqrt(nfactor) + + +#---------------------------------------------------------------------------- +# Gegenbauer polynomials +# + + +class gegenbauer(OrthogonalPolynomial): + r""" + Gegenbauer polynomial $C_n^{\left(\alpha\right)}(x)$. + + Explanation + =========== + + ``gegenbauer(n, alpha, x)`` gives the $n$th Gegenbauer polynomial + in $x$, $C_n^{\left(\alpha\right)}(x)$. + + The Gegenbauer polynomials are orthogonal on $[-1, 1]$ with + respect to the weight $\left(1-x^2\right)^{\alpha-\frac{1}{2}}$. + + Examples + ======== + + >>> from sympy import gegenbauer, conjugate, diff + >>> from sympy.abc import n,a,x + >>> gegenbauer(0, a, x) + 1 + >>> gegenbauer(1, a, x) + 2*a*x + >>> gegenbauer(2, a, x) + -a + x**2*(2*a**2 + 2*a) + >>> gegenbauer(3, a, x) + x**3*(4*a**3/3 + 4*a**2 + 8*a/3) + x*(-2*a**2 - 2*a) + + >>> gegenbauer(n, a, x) + gegenbauer(n, a, x) + >>> gegenbauer(n, a, -x) + (-1)**n*gegenbauer(n, a, x) + + >>> gegenbauer(n, a, 0) + 2**n*sqrt(pi)*gamma(a + n/2)/(gamma(a)*gamma(1/2 - n/2)*gamma(n + 1)) + >>> gegenbauer(n, a, 1) + gamma(2*a + n)/(gamma(2*a)*gamma(n + 1)) + + >>> conjugate(gegenbauer(n, a, x)) + gegenbauer(n, conjugate(a), conjugate(x)) + + >>> diff(gegenbauer(n, a, x), x) + 2*a*gegenbauer(n - 1, a + 1, x) + + See Also + ======== + + jacobi, + chebyshevt_root, chebyshevu, chebyshevu_root, + legendre, assoc_legendre, + hermite, hermite_prob, + laguerre, assoc_laguerre, + sympy.polys.orthopolys.jacobi_poly + sympy.polys.orthopolys.gegenbauer_poly + sympy.polys.orthopolys.chebyshevt_poly + sympy.polys.orthopolys.chebyshevu_poly + sympy.polys.orthopolys.hermite_poly + sympy.polys.orthopolys.hermite_prob_poly + sympy.polys.orthopolys.legendre_poly + sympy.polys.orthopolys.laguerre_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gegenbauer_polynomials + .. [2] https://mathworld.wolfram.com/GegenbauerPolynomial.html + .. [3] https://functions.wolfram.com/Polynomials/GegenbauerC3/ + + """ + + @classmethod + def eval(cls, n, a, x): + # For negative n the polynomials vanish + # See https://functions.wolfram.com/Polynomials/GegenbauerC3/03/01/03/0012/ + if n.is_negative: + return S.Zero + + # Some special values for fixed a + if a == S.Half: + return legendre(n, x) + elif a == S.One: + return chebyshevu(n, x) + elif a == S.NegativeOne: + return S.Zero + + if not n.is_Number: + # Handle this before the general sign extraction rule + if x == S.NegativeOne: + if (re(a) > S.Half) == True: + return S.ComplexInfinity + else: + return (cos(S.Pi*(a+n)) * sec(S.Pi*a) * gamma(2*a+n) / + (gamma(2*a) * gamma(n+1))) + + # Symbolic result C^a_n(x) + # C^a_n(-x) ---> (-1)**n * C^a_n(x) + if x.could_extract_minus_sign(): + return S.NegativeOne**n * gegenbauer(n, a, -x) + # We can evaluate for some special values of x + if x.is_zero: + return (2**n * sqrt(S.Pi) * gamma(a + S.Half*n) / + (gamma((1 - n)/2) * gamma(n + 1) * gamma(a)) ) + if x == S.One: + return gamma(2*a + n) / (gamma(2*a) * gamma(n + 1)) + elif x is S.Infinity: + if n.is_positive: + return RisingFactorial(a, n) * S.Infinity + else: + # n is a given fixed integer, evaluate into polynomial + return gegenbauer_poly(n, a, x) + + def fdiff(self, argindex=3): + from sympy.concrete.summations import Sum + if argindex == 1: + # Diff wrt n + raise ArgumentIndexError(self, argindex) + elif argindex == 2: + # Diff wrt a + n, a, x = self.args + k = Dummy("k") + factor1 = 2 * (1 + (-1)**(n - k)) * (k + a) / ((k + + n + 2*a) * (n - k)) + factor2 = 2*(k + 1) / ((k + 2*a) * (2*k + 2*a + 1)) + \ + 2 / (k + n + 2*a) + kern = factor1*gegenbauer(k, a, x) + factor2*gegenbauer(n, a, x) + return Sum(kern, (k, 0, n - 1)) + elif argindex == 3: + # Diff wrt x + n, a, x = self.args + return 2*a*gegenbauer(n - 1, a + 1, x) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Sum(self, n, a, x, **kwargs): + from sympy.concrete.summations import Sum + k = Dummy("k") + kern = ((-1)**k * RisingFactorial(a, n - k) * (2*x)**(n - 2*k) / + (factorial(k) * factorial(n - 2*k))) + return Sum(kern, (k, 0, floor(n/2))) + + def _eval_rewrite_as_polynomial(self, n, a, x, **kwargs): + # This function is just kept for backwards compatibility + # but should not be used + return self._eval_rewrite_as_Sum(n, a, x, **kwargs) + + def _eval_conjugate(self): + n, a, x = self.args + return self.func(n, a.conjugate(), x.conjugate()) + +#---------------------------------------------------------------------------- +# Chebyshev polynomials of first and second kind +# + + +class chebyshevt(OrthogonalPolynomial): + r""" + Chebyshev polynomial of the first kind, $T_n(x)$. + + Explanation + =========== + + ``chebyshevt(n, x)`` gives the $n$th Chebyshev polynomial (of the first + kind) in $x$, $T_n(x)$. + + The Chebyshev polynomials of the first kind are orthogonal on + $[-1, 1]$ with respect to the weight $\frac{1}{\sqrt{1-x^2}}$. + + Examples + ======== + + >>> from sympy import chebyshevt, diff + >>> from sympy.abc import n,x + >>> chebyshevt(0, x) + 1 + >>> chebyshevt(1, x) + x + >>> chebyshevt(2, x) + 2*x**2 - 1 + + >>> chebyshevt(n, x) + chebyshevt(n, x) + >>> chebyshevt(n, -x) + (-1)**n*chebyshevt(n, x) + >>> chebyshevt(-n, x) + chebyshevt(n, x) + + >>> chebyshevt(n, 0) + cos(pi*n/2) + >>> chebyshevt(n, -1) + (-1)**n + + >>> diff(chebyshevt(n, x), x) + n*chebyshevu(n - 1, x) + + See Also + ======== + + jacobi, gegenbauer, + chebyshevt_root, chebyshevu, chebyshevu_root, + legendre, assoc_legendre, + hermite, hermite_prob, + laguerre, assoc_laguerre, + sympy.polys.orthopolys.jacobi_poly + sympy.polys.orthopolys.gegenbauer_poly + sympy.polys.orthopolys.chebyshevt_poly + sympy.polys.orthopolys.chebyshevu_poly + sympy.polys.orthopolys.hermite_poly + sympy.polys.orthopolys.hermite_prob_poly + sympy.polys.orthopolys.legendre_poly + sympy.polys.orthopolys.laguerre_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Chebyshev_polynomial + .. [2] https://mathworld.wolfram.com/ChebyshevPolynomialoftheFirstKind.html + .. [3] https://mathworld.wolfram.com/ChebyshevPolynomialoftheSecondKind.html + .. [4] https://functions.wolfram.com/Polynomials/ChebyshevT/ + .. [5] https://functions.wolfram.com/Polynomials/ChebyshevU/ + + """ + + _ortho_poly = staticmethod(chebyshevt_poly) + + @classmethod + def eval(cls, n, x): + if not n.is_Number: + # Symbolic result T_n(x) + # T_n(-x) ---> (-1)**n * T_n(x) + if x.could_extract_minus_sign(): + return S.NegativeOne**n * chebyshevt(n, -x) + # T_{-n}(x) ---> T_n(x) + if n.could_extract_minus_sign(): + return chebyshevt(-n, x) + # We can evaluate for some special values of x + if x.is_zero: + return cos(S.Half * S.Pi * n) + if x == S.One: + return S.One + elif x is S.Infinity: + return S.Infinity + else: + # n is a given fixed integer, evaluate into polynomial + if n.is_negative: + # T_{-n}(x) == T_n(x) + return cls._eval_at_order(-n, x) + else: + return cls._eval_at_order(n, x) + + def fdiff(self, argindex=2): + if argindex == 1: + # Diff wrt n + raise ArgumentIndexError(self, argindex) + elif argindex == 2: + # Diff wrt x + n, x = self.args + return n * chebyshevu(n - 1, x) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Sum(self, n, x, **kwargs): + from sympy.concrete.summations import Sum + k = Dummy("k") + kern = binomial(n, 2*k) * (x**2 - 1)**k * x**(n - 2*k) + return Sum(kern, (k, 0, floor(n/2))) + + def _eval_rewrite_as_polynomial(self, n, x, **kwargs): + # This function is just kept for backwards compatibility + # but should not be used + return self._eval_rewrite_as_Sum(n, x, **kwargs) + + +class chebyshevu(OrthogonalPolynomial): + r""" + Chebyshev polynomial of the second kind, $U_n(x)$. + + Explanation + =========== + + ``chebyshevu(n, x)`` gives the $n$th Chebyshev polynomial of the second + kind in x, $U_n(x)$. + + The Chebyshev polynomials of the second kind are orthogonal on + $[-1, 1]$ with respect to the weight $\sqrt{1-x^2}$. + + Examples + ======== + + >>> from sympy import chebyshevu, diff + >>> from sympy.abc import n,x + >>> chebyshevu(0, x) + 1 + >>> chebyshevu(1, x) + 2*x + >>> chebyshevu(2, x) + 4*x**2 - 1 + + >>> chebyshevu(n, x) + chebyshevu(n, x) + >>> chebyshevu(n, -x) + (-1)**n*chebyshevu(n, x) + >>> chebyshevu(-n, x) + -chebyshevu(n - 2, x) + + >>> chebyshevu(n, 0) + cos(pi*n/2) + >>> chebyshevu(n, 1) + n + 1 + + >>> diff(chebyshevu(n, x), x) + (-x*chebyshevu(n, x) + (n + 1)*chebyshevt(n + 1, x))/(x**2 - 1) + + See Also + ======== + + jacobi, gegenbauer, + chebyshevt, chebyshevt_root, chebyshevu_root, + legendre, assoc_legendre, + hermite, hermite_prob, + laguerre, assoc_laguerre, + sympy.polys.orthopolys.jacobi_poly + sympy.polys.orthopolys.gegenbauer_poly + sympy.polys.orthopolys.chebyshevt_poly + sympy.polys.orthopolys.chebyshevu_poly + sympy.polys.orthopolys.hermite_poly + sympy.polys.orthopolys.hermite_prob_poly + sympy.polys.orthopolys.legendre_poly + sympy.polys.orthopolys.laguerre_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Chebyshev_polynomial + .. [2] https://mathworld.wolfram.com/ChebyshevPolynomialoftheFirstKind.html + .. [3] https://mathworld.wolfram.com/ChebyshevPolynomialoftheSecondKind.html + .. [4] https://functions.wolfram.com/Polynomials/ChebyshevT/ + .. [5] https://functions.wolfram.com/Polynomials/ChebyshevU/ + + """ + + _ortho_poly = staticmethod(chebyshevu_poly) + + @classmethod + def eval(cls, n, x): + if not n.is_Number: + # Symbolic result U_n(x) + # U_n(-x) ---> (-1)**n * U_n(x) + if x.could_extract_minus_sign(): + return S.NegativeOne**n * chebyshevu(n, -x) + # U_{-n}(x) ---> -U_{n-2}(x) + if n.could_extract_minus_sign(): + if n == S.NegativeOne: + # n can not be -1 here + return S.Zero + elif not (-n - 2).could_extract_minus_sign(): + return -chebyshevu(-n - 2, x) + # We can evaluate for some special values of x + if x.is_zero: + return cos(S.Half * S.Pi * n) + if x == S.One: + return S.One + n + elif x is S.Infinity: + return S.Infinity + else: + # n is a given fixed integer, evaluate into polynomial + if n.is_negative: + # U_{-n}(x) ---> -U_{n-2}(x) + if n == S.NegativeOne: + return S.Zero + else: + return -cls._eval_at_order(-n - 2, x) + else: + return cls._eval_at_order(n, x) + + def fdiff(self, argindex=2): + if argindex == 1: + # Diff wrt n + raise ArgumentIndexError(self, argindex) + elif argindex == 2: + # Diff wrt x + n, x = self.args + return ((n + 1) * chebyshevt(n + 1, x) - x * chebyshevu(n, x)) / (x**2 - 1) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Sum(self, n, x, **kwargs): + from sympy.concrete.summations import Sum + k = Dummy("k") + kern = S.NegativeOne**k * factorial( + n - k) * (2*x)**(n - 2*k) / (factorial(k) * factorial(n - 2*k)) + return Sum(kern, (k, 0, floor(n/2))) + + def _eval_rewrite_as_polynomial(self, n, x, **kwargs): + # This function is just kept for backwards compatibility + # but should not be used + return self._eval_rewrite_as_Sum(n, x, **kwargs) + + +class chebyshevt_root(DefinedFunction): + r""" + ``chebyshev_root(n, k)`` returns the $k$th root (indexed from zero) of + the $n$th Chebyshev polynomial of the first kind; that is, if + $0 \le k < n$, ``chebyshevt(n, chebyshevt_root(n, k)) == 0``. + + Examples + ======== + + >>> from sympy import chebyshevt, chebyshevt_root + >>> chebyshevt_root(3, 2) + -sqrt(3)/2 + >>> chebyshevt(3, chebyshevt_root(3, 2)) + 0 + + See Also + ======== + + jacobi, gegenbauer, + chebyshevt, chebyshevu, chebyshevu_root, + legendre, assoc_legendre, + hermite, hermite_prob, + laguerre, assoc_laguerre, + sympy.polys.orthopolys.jacobi_poly + sympy.polys.orthopolys.gegenbauer_poly + sympy.polys.orthopolys.chebyshevt_poly + sympy.polys.orthopolys.chebyshevu_poly + sympy.polys.orthopolys.hermite_poly + sympy.polys.orthopolys.hermite_prob_poly + sympy.polys.orthopolys.legendre_poly + sympy.polys.orthopolys.laguerre_poly + """ + + @classmethod + def eval(cls, n, k): + if not ((0 <= k) and (k < n)): + raise ValueError("must have 0 <= k < n, " + "got k = %s and n = %s" % (k, n)) + return cos(S.Pi*(2*k + 1)/(2*n)) + + +class chebyshevu_root(DefinedFunction): + r""" + ``chebyshevu_root(n, k)`` returns the $k$th root (indexed from zero) of the + $n$th Chebyshev polynomial of the second kind; that is, if $0 \le k < n$, + ``chebyshevu(n, chebyshevu_root(n, k)) == 0``. + + Examples + ======== + + >>> from sympy import chebyshevu, chebyshevu_root + >>> chebyshevu_root(3, 2) + -sqrt(2)/2 + >>> chebyshevu(3, chebyshevu_root(3, 2)) + 0 + + See Also + ======== + + chebyshevt, chebyshevt_root, chebyshevu, + legendre, assoc_legendre, + hermite, hermite_prob, + laguerre, assoc_laguerre, + sympy.polys.orthopolys.jacobi_poly + sympy.polys.orthopolys.gegenbauer_poly + sympy.polys.orthopolys.chebyshevt_poly + sympy.polys.orthopolys.chebyshevu_poly + sympy.polys.orthopolys.hermite_poly + sympy.polys.orthopolys.hermite_prob_poly + sympy.polys.orthopolys.legendre_poly + sympy.polys.orthopolys.laguerre_poly + """ + + + @classmethod + def eval(cls, n, k): + if not ((0 <= k) and (k < n)): + raise ValueError("must have 0 <= k < n, " + "got k = %s and n = %s" % (k, n)) + return cos(S.Pi*(k + 1)/(n + 1)) + +#---------------------------------------------------------------------------- +# Legendre polynomials and Associated Legendre polynomials +# + + +class legendre(OrthogonalPolynomial): + r""" + ``legendre(n, x)`` gives the $n$th Legendre polynomial of $x$, $P_n(x)$ + + Explanation + =========== + + The Legendre polynomials are orthogonal on $[-1, 1]$ with respect to + the constant weight 1. They satisfy $P_n(1) = 1$ for all $n$; further, + $P_n$ is odd for odd $n$ and even for even $n$. + + Examples + ======== + + >>> from sympy import legendre, diff + >>> from sympy.abc import x, n + >>> legendre(0, x) + 1 + >>> legendre(1, x) + x + >>> legendre(2, x) + 3*x**2/2 - 1/2 + >>> legendre(n, x) + legendre(n, x) + >>> diff(legendre(n,x), x) + n*(x*legendre(n, x) - legendre(n - 1, x))/(x**2 - 1) + + See Also + ======== + + jacobi, gegenbauer, + chebyshevt, chebyshevt_root, chebyshevu, chebyshevu_root, + assoc_legendre, + hermite, hermite_prob, + laguerre, assoc_laguerre, + sympy.polys.orthopolys.jacobi_poly + sympy.polys.orthopolys.gegenbauer_poly + sympy.polys.orthopolys.chebyshevt_poly + sympy.polys.orthopolys.chebyshevu_poly + sympy.polys.orthopolys.hermite_poly + sympy.polys.orthopolys.hermite_prob_poly + sympy.polys.orthopolys.legendre_poly + sympy.polys.orthopolys.laguerre_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Legendre_polynomial + .. [2] https://mathworld.wolfram.com/LegendrePolynomial.html + .. [3] https://functions.wolfram.com/Polynomials/LegendreP/ + .. [4] https://functions.wolfram.com/Polynomials/LegendreP2/ + + """ + + _ortho_poly = staticmethod(legendre_poly) + + @classmethod + def eval(cls, n, x): + if not n.is_Number: + # Symbolic result L_n(x) + # L_n(-x) ---> (-1)**n * L_n(x) + if x.could_extract_minus_sign(): + return S.NegativeOne**n * legendre(n, -x) + # L_{-n}(x) ---> L_{n-1}(x) + if n.could_extract_minus_sign() and not(-n - 1).could_extract_minus_sign(): + return legendre(-n - S.One, x) + # We can evaluate for some special values of x + if x.is_zero: + return sqrt(S.Pi)/(gamma(S.Half - n/2)*gamma(S.One + n/2)) + elif x == S.One: + return S.One + elif x is S.Infinity: + return S.Infinity + else: + # n is a given fixed integer, evaluate into polynomial; + # L_{-n}(x) ---> L_{n-1}(x) + if n.is_negative: + n = -n - S.One + return cls._eval_at_order(n, x) + + def fdiff(self, argindex=2): + if argindex == 1: + # Diff wrt n + raise ArgumentIndexError(self, argindex) + elif argindex == 2: + # Diff wrt x + # Find better formula, this is unsuitable for x = +/-1 + # https://www.autodiff.org/ad16/Oral/Buecker_Legendre.pdf says + # at x = 1: + # n*(n + 1)/2 , m = 0 + # oo , m = 1 + # -(n-1)*n*(n+1)*(n+2)/4 , m = 2 + # 0 , m = 3, 4, ..., n + # + # at x = -1 + # (-1)**(n+1)*n*(n + 1)/2 , m = 0 + # (-1)**n*oo , m = 1 + # (-1)**n*(n-1)*n*(n+1)*(n+2)/4 , m = 2 + # 0 , m = 3, 4, ..., n + n, x = self.args + return n/(x**2 - 1)*(x*legendre(n, x) - legendre(n - 1, x)) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Sum(self, n, x, **kwargs): + from sympy.concrete.summations import Sum + k = Dummy("k") + kern = S.NegativeOne**k*binomial(n, k)**2*((1 + x)/2)**(n - k)*((1 - x)/2)**k + return Sum(kern, (k, 0, n)) + + def _eval_rewrite_as_polynomial(self, n, x, **kwargs): + # This function is just kept for backwards compatibility + # but should not be used + return self._eval_rewrite_as_Sum(n, x, **kwargs) + + +class assoc_legendre(DefinedFunction): + r""" + ``assoc_legendre(n, m, x)`` gives $P_n^m(x)$, where $n$ and $m$ are + the degree and order or an expression which is related to the nth + order Legendre polynomial, $P_n(x)$ in the following manner: + + .. math:: + P_n^m(x) = (-1)^m (1 - x^2)^{\frac{m}{2}} + \frac{\mathrm{d}^m P_n(x)}{\mathrm{d} x^m} + + Explanation + =========== + + Associated Legendre polynomials are orthogonal on $[-1, 1]$ with: + + - weight $= 1$ for the same $m$ and different $n$. + - weight $= \frac{1}{1-x^2}$ for the same $n$ and different $m$. + + Examples + ======== + + >>> from sympy import assoc_legendre + >>> from sympy.abc import x, m, n + >>> assoc_legendre(0,0, x) + 1 + >>> assoc_legendre(1,0, x) + x + >>> assoc_legendre(1,1, x) + -sqrt(1 - x**2) + >>> assoc_legendre(n,m,x) + assoc_legendre(n, m, x) + + See Also + ======== + + jacobi, gegenbauer, + chebyshevt, chebyshevt_root, chebyshevu, chebyshevu_root, + legendre, + hermite, hermite_prob, + laguerre, assoc_laguerre, + sympy.polys.orthopolys.jacobi_poly + sympy.polys.orthopolys.gegenbauer_poly + sympy.polys.orthopolys.chebyshevt_poly + sympy.polys.orthopolys.chebyshevu_poly + sympy.polys.orthopolys.hermite_poly + sympy.polys.orthopolys.hermite_prob_poly + sympy.polys.orthopolys.legendre_poly + sympy.polys.orthopolys.laguerre_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Associated_Legendre_polynomials + .. [2] https://mathworld.wolfram.com/LegendrePolynomial.html + .. [3] https://functions.wolfram.com/Polynomials/LegendreP/ + .. [4] https://functions.wolfram.com/Polynomials/LegendreP2/ + + """ + + @classmethod + def _eval_at_order(cls, n, m): + P = legendre_poly(n, _x, polys=True).diff((_x, m)) + return S.NegativeOne**m * (1 - _x**2)**Rational(m, 2) * P.as_expr() + + @classmethod + def eval(cls, n, m, x): + if m.could_extract_minus_sign(): + # P^{-m}_n ---> F * P^m_n + return S.NegativeOne**(-m) * (factorial(m + n)/factorial(n - m)) * assoc_legendre(n, -m, x) + if m == 0: + # P^0_n ---> L_n + return legendre(n, x) + if x == 0: + return 2**m*sqrt(S.Pi) / (gamma((1 - m - n)/2)*gamma(1 - (m - n)/2)) + if n.is_Number and m.is_Number and n.is_integer and m.is_integer: + if n.is_negative: + raise ValueError("%s : 1st index must be nonnegative integer (got %r)" % (cls, n)) + if abs(m) > n: + raise ValueError("%s : abs('2nd index') must be <= '1st index' (got %r, %r)" % (cls, n, m)) + return cls._eval_at_order(int(n), abs(int(m))).subs(_x, x) + + def fdiff(self, argindex=3): + if argindex == 1: + # Diff wrt n + raise ArgumentIndexError(self, argindex) + elif argindex == 2: + # Diff wrt m + raise ArgumentIndexError(self, argindex) + elif argindex == 3: + # Diff wrt x + # Find better formula, this is unsuitable for x = 1 + n, m, x = self.args + return 1/(x**2 - 1)*(x*n*assoc_legendre(n, m, x) - (m + n)*assoc_legendre(n - 1, m, x)) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Sum(self, n, m, x, **kwargs): + from sympy.concrete.summations import Sum + k = Dummy("k") + kern = factorial(2*n - 2*k)/(2**n*factorial(n - k)*factorial( + k)*factorial(n - 2*k - m))*S.NegativeOne**k*x**(n - m - 2*k) + return (1 - x**2)**(m/2) * Sum(kern, (k, 0, floor((n - m)*S.Half))) + + def _eval_rewrite_as_polynomial(self, n, m, x, **kwargs): + # This function is just kept for backwards compatibility + # but should not be used + return self._eval_rewrite_as_Sum(n, m, x, **kwargs) + + def _eval_conjugate(self): + n, m, x = self.args + return self.func(n, m.conjugate(), x.conjugate()) + +#---------------------------------------------------------------------------- +# Hermite polynomials +# + + +class hermite(OrthogonalPolynomial): + r""" + ``hermite(n, x)`` gives the $n$th Hermite polynomial in $x$, $H_n(x)$. + + Explanation + =========== + + The Hermite polynomials are orthogonal on $(-\infty, \infty)$ + with respect to the weight $\exp\left(-x^2\right)$. + + Examples + ======== + + >>> from sympy import hermite, diff + >>> from sympy.abc import x, n + >>> hermite(0, x) + 1 + >>> hermite(1, x) + 2*x + >>> hermite(2, x) + 4*x**2 - 2 + >>> hermite(n, x) + hermite(n, x) + >>> diff(hermite(n,x), x) + 2*n*hermite(n - 1, x) + >>> hermite(n, -x) + (-1)**n*hermite(n, x) + + See Also + ======== + + jacobi, gegenbauer, + chebyshevt, chebyshevt_root, chebyshevu, chebyshevu_root, + legendre, assoc_legendre, + hermite_prob, + laguerre, assoc_laguerre, + sympy.polys.orthopolys.jacobi_poly + sympy.polys.orthopolys.gegenbauer_poly + sympy.polys.orthopolys.chebyshevt_poly + sympy.polys.orthopolys.chebyshevu_poly + sympy.polys.orthopolys.hermite_poly + sympy.polys.orthopolys.hermite_prob_poly + sympy.polys.orthopolys.legendre_poly + sympy.polys.orthopolys.laguerre_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hermite_polynomial + .. [2] https://mathworld.wolfram.com/HermitePolynomial.html + .. [3] https://functions.wolfram.com/Polynomials/HermiteH/ + + """ + + _ortho_poly = staticmethod(hermite_poly) + + @classmethod + def eval(cls, n, x): + if not n.is_Number: + # Symbolic result H_n(x) + # H_n(-x) ---> (-1)**n * H_n(x) + if x.could_extract_minus_sign(): + return S.NegativeOne**n * hermite(n, -x) + # We can evaluate for some special values of x + if x.is_zero: + return 2**n * sqrt(S.Pi) / gamma((S.One - n)/2) + elif x is S.Infinity: + return S.Infinity + else: + # n is a given fixed integer, evaluate into polynomial + if n.is_negative: + raise ValueError( + "The index n must be nonnegative integer (got %r)" % n) + else: + return cls._eval_at_order(n, x) + + def fdiff(self, argindex=2): + if argindex == 1: + # Diff wrt n + raise ArgumentIndexError(self, argindex) + elif argindex == 2: + # Diff wrt x + n, x = self.args + return 2*n*hermite(n - 1, x) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Sum(self, n, x, **kwargs): + from sympy.concrete.summations import Sum + k = Dummy("k") + kern = S.NegativeOne**k / (factorial(k)*factorial(n - 2*k)) * (2*x)**(n - 2*k) + return factorial(n)*Sum(kern, (k, 0, floor(n/2))) + + def _eval_rewrite_as_polynomial(self, n, x, **kwargs): + # This function is just kept for backwards compatibility + # but should not be used + return self._eval_rewrite_as_Sum(n, x, **kwargs) + + def _eval_rewrite_as_hermite_prob(self, n, x, **kwargs): + return sqrt(2)**n * hermite_prob(n, x*sqrt(2)) + + +class hermite_prob(OrthogonalPolynomial): + r""" + ``hermite_prob(n, x)`` gives the $n$th probabilist's Hermite polynomial + in $x$, $He_n(x)$. + + Explanation + =========== + + The probabilist's Hermite polynomials are orthogonal on $(-\infty, \infty)$ + with respect to the weight $\exp\left(-\frac{x^2}{2}\right)$. They are monic + polynomials, related to the plain Hermite polynomials (:py:class:`~.hermite`) by + + .. math :: He_n(x) = 2^{-n/2} H_n(x/\sqrt{2}) + + Examples + ======== + + >>> from sympy import hermite_prob, diff, I + >>> from sympy.abc import x, n + >>> hermite_prob(1, x) + x + >>> hermite_prob(5, x) + x**5 - 10*x**3 + 15*x + >>> diff(hermite_prob(n,x), x) + n*hermite_prob(n - 1, x) + >>> hermite_prob(n, -x) + (-1)**n*hermite_prob(n, x) + + The sum of absolute values of coefficients of $He_n(x)$ is the number of + matchings in the complete graph $K_n$ or telephone number, A000085 in the OEIS: + + >>> [hermite_prob(n,I) / I**n for n in range(11)] + [1, 1, 2, 4, 10, 26, 76, 232, 764, 2620, 9496] + + See Also + ======== + + jacobi, gegenbauer, + chebyshevt, chebyshevt_root, chebyshevu, chebyshevu_root, + legendre, assoc_legendre, + hermite, + laguerre, assoc_laguerre, + sympy.polys.orthopolys.jacobi_poly + sympy.polys.orthopolys.gegenbauer_poly + sympy.polys.orthopolys.chebyshevt_poly + sympy.polys.orthopolys.chebyshevu_poly + sympy.polys.orthopolys.hermite_poly + sympy.polys.orthopolys.hermite_prob_poly + sympy.polys.orthopolys.legendre_poly + sympy.polys.orthopolys.laguerre_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hermite_polynomial + .. [2] https://mathworld.wolfram.com/HermitePolynomial.html + """ + + _ortho_poly = staticmethod(hermite_prob_poly) + + @classmethod + def eval(cls, n, x): + if not n.is_Number: + if x.could_extract_minus_sign(): + return S.NegativeOne**n * hermite_prob(n, -x) + if x.is_zero: + return sqrt(S.Pi) / gamma((S.One-n) / 2) + elif x is S.Infinity: + return S.Infinity + else: + if n.is_negative: + ValueError("n must be a nonnegative integer, not %r" % n) + else: + return cls._eval_at_order(n, x) + + def fdiff(self, argindex=2): + if argindex == 2: + n, x = self.args + return n*hermite_prob(n-1, x) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Sum(self, n, x, **kwargs): + from sympy.concrete.summations import Sum + k = Dummy("k") + kern = (-S.Half)**k * x**(n-2*k) / (factorial(k) * factorial(n-2*k)) + return factorial(n)*Sum(kern, (k, 0, floor(n/2))) + + def _eval_rewrite_as_polynomial(self, n, x, **kwargs): + # This function is just kept for backwards compatibility + # but should not be used + return self._eval_rewrite_as_Sum(n, x, **kwargs) + + def _eval_rewrite_as_hermite(self, n, x, **kwargs): + return sqrt(2)**(-n) * hermite(n, x/sqrt(2)) + + +#---------------------------------------------------------------------------- +# Laguerre polynomials +# + + +class laguerre(OrthogonalPolynomial): + r""" + Returns the $n$th Laguerre polynomial in $x$, $L_n(x)$. + + Examples + ======== + + >>> from sympy import laguerre, diff + >>> from sympy.abc import x, n + >>> laguerre(0, x) + 1 + >>> laguerre(1, x) + 1 - x + >>> laguerre(2, x) + x**2/2 - 2*x + 1 + >>> laguerre(3, x) + -x**3/6 + 3*x**2/2 - 3*x + 1 + + >>> laguerre(n, x) + laguerre(n, x) + + >>> diff(laguerre(n, x), x) + -assoc_laguerre(n - 1, 1, x) + + Parameters + ========== + + n : int + Degree of Laguerre polynomial. Must be `n \ge 0`. + + See Also + ======== + + jacobi, gegenbauer, + chebyshevt, chebyshevt_root, chebyshevu, chebyshevu_root, + legendre, assoc_legendre, + hermite, hermite_prob, + assoc_laguerre, + sympy.polys.orthopolys.jacobi_poly + sympy.polys.orthopolys.gegenbauer_poly + sympy.polys.orthopolys.chebyshevt_poly + sympy.polys.orthopolys.chebyshevu_poly + sympy.polys.orthopolys.hermite_poly + sympy.polys.orthopolys.hermite_prob_poly + sympy.polys.orthopolys.legendre_poly + sympy.polys.orthopolys.laguerre_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Laguerre_polynomial + .. [2] https://mathworld.wolfram.com/LaguerrePolynomial.html + .. [3] https://functions.wolfram.com/Polynomials/LaguerreL/ + .. [4] https://functions.wolfram.com/Polynomials/LaguerreL3/ + + """ + + _ortho_poly = staticmethod(laguerre_poly) + + @classmethod + def eval(cls, n, x): + if n.is_integer is False: + raise ValueError("Error: n should be an integer.") + if not n.is_Number: + # Symbolic result L_n(x) + # L_{n}(-x) ---> exp(-x) * L_{-n-1}(x) + # L_{-n}(x) ---> exp(x) * L_{n-1}(-x) + if n.could_extract_minus_sign() and not(-n - 1).could_extract_minus_sign(): + return exp(x)*laguerre(-n - 1, -x) + # We can evaluate for some special values of x + if x.is_zero: + return S.One + elif x is S.NegativeInfinity: + return S.Infinity + elif x is S.Infinity: + return S.NegativeOne**n * S.Infinity + else: + if n.is_negative: + return exp(x)*laguerre(-n - 1, -x) + else: + return cls._eval_at_order(n, x) + + def fdiff(self, argindex=2): + if argindex == 1: + # Diff wrt n + raise ArgumentIndexError(self, argindex) + elif argindex == 2: + # Diff wrt x + n, x = self.args + return -assoc_laguerre(n - 1, 1, x) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Sum(self, n, x, **kwargs): + from sympy.concrete.summations import Sum + # Make sure n \in N_0 + if n.is_negative: + return exp(x) * self._eval_rewrite_as_Sum(-n - 1, -x, **kwargs) + if n.is_integer is False: + raise ValueError("Error: n should be an integer.") + k = Dummy("k") + kern = RisingFactorial(-n, k) / factorial(k)**2 * x**k + return Sum(kern, (k, 0, n)) + + def _eval_rewrite_as_polynomial(self, n, x, **kwargs): + # This function is just kept for backwards compatibility + # but should not be used + return self._eval_rewrite_as_Sum(n, x, **kwargs) + + +class assoc_laguerre(OrthogonalPolynomial): + r""" + Returns the $n$th generalized Laguerre polynomial in $x$, $L_n(x)$. + + Examples + ======== + + >>> from sympy import assoc_laguerre, diff + >>> from sympy.abc import x, n, a + >>> assoc_laguerre(0, a, x) + 1 + >>> assoc_laguerre(1, a, x) + a - x + 1 + >>> assoc_laguerre(2, a, x) + a**2/2 + 3*a/2 + x**2/2 + x*(-a - 2) + 1 + >>> assoc_laguerre(3, a, x) + a**3/6 + a**2 + 11*a/6 - x**3/6 + x**2*(a/2 + 3/2) + + x*(-a**2/2 - 5*a/2 - 3) + 1 + + >>> assoc_laguerre(n, a, 0) + binomial(a + n, a) + + >>> assoc_laguerre(n, a, x) + assoc_laguerre(n, a, x) + + >>> assoc_laguerre(n, 0, x) + laguerre(n, x) + + >>> diff(assoc_laguerre(n, a, x), x) + -assoc_laguerre(n - 1, a + 1, x) + + >>> diff(assoc_laguerre(n, a, x), a) + Sum(assoc_laguerre(_k, a, x)/(-a + n), (_k, 0, n - 1)) + + Parameters + ========== + + n : int + Degree of Laguerre polynomial. Must be `n \ge 0`. + + alpha : Expr + Arbitrary expression. For ``alpha=0`` regular Laguerre + polynomials will be generated. + + See Also + ======== + + jacobi, gegenbauer, + chebyshevt, chebyshevt_root, chebyshevu, chebyshevu_root, + legendre, assoc_legendre, + hermite, hermite_prob, + laguerre, + sympy.polys.orthopolys.jacobi_poly + sympy.polys.orthopolys.gegenbauer_poly + sympy.polys.orthopolys.chebyshevt_poly + sympy.polys.orthopolys.chebyshevu_poly + sympy.polys.orthopolys.hermite_poly + sympy.polys.orthopolys.hermite_prob_poly + sympy.polys.orthopolys.legendre_poly + sympy.polys.orthopolys.laguerre_poly + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Laguerre_polynomial#Generalized_Laguerre_polynomials + .. [2] https://mathworld.wolfram.com/AssociatedLaguerrePolynomial.html + .. [3] https://functions.wolfram.com/Polynomials/LaguerreL/ + .. [4] https://functions.wolfram.com/Polynomials/LaguerreL3/ + + """ + + @classmethod + def eval(cls, n, alpha, x): + # L_{n}^{0}(x) ---> L_{n}(x) + if alpha.is_zero: + return laguerre(n, x) + + if not n.is_Number: + # We can evaluate for some special values of x + if x.is_zero: + return binomial(n + alpha, alpha) + elif x is S.Infinity and n > 0: + return S.NegativeOne**n * S.Infinity + elif x is S.NegativeInfinity and n > 0: + return S.Infinity + else: + # n is a given fixed integer, evaluate into polynomial + if n.is_negative: + raise ValueError( + "The index n must be nonnegative integer (got %r)" % n) + else: + return laguerre_poly(n, x, alpha) + + def fdiff(self, argindex=3): + from sympy.concrete.summations import Sum + if argindex == 1: + # Diff wrt n + raise ArgumentIndexError(self, argindex) + elif argindex == 2: + # Diff wrt alpha + n, alpha, x = self.args + k = Dummy("k") + return Sum(assoc_laguerre(k, alpha, x) / (n - alpha), (k, 0, n - 1)) + elif argindex == 3: + # Diff wrt x + n, alpha, x = self.args + return -assoc_laguerre(n - 1, alpha + 1, x) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_Sum(self, n, alpha, x, **kwargs): + from sympy.concrete.summations import Sum + # Make sure n \in N_0 + if n.is_negative or n.is_integer is False: + raise ValueError("Error: n should be a non-negative integer.") + k = Dummy("k") + kern = RisingFactorial( + -n, k) / (gamma(k + alpha + 1) * factorial(k)) * x**k + return gamma(n + alpha + 1) / factorial(n) * Sum(kern, (k, 0, n)) + + def _eval_rewrite_as_polynomial(self, n, alpha, x, **kwargs): + # This function is just kept for backwards compatibility + # but should not be used + return self._eval_rewrite_as_Sum(n, alpha, x, **kwargs) + + def _eval_conjugate(self): + n, alpha, x = self.args + return self.func(n, alpha.conjugate(), x.conjugate()) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/singularity_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/singularity_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..a69026e6e657b1131880b47cb32202b6825b7158 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/singularity_functions.py @@ -0,0 +1,235 @@ +from sympy.core import S, oo, diff +from sympy.core.function import DefinedFunction, ArgumentIndexError +from sympy.core.logic import fuzzy_not +from sympy.core.relational import Eq +from sympy.functions.elementary.complexes import im +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.delta_functions import Heaviside + +############################################################################### +############################# SINGULARITY FUNCTION ############################ +############################################################################### + + +class SingularityFunction(DefinedFunction): + r""" + Singularity functions are a class of discontinuous functions. + + Explanation + =========== + + Singularity functions take a variable, an offset, and an exponent as + arguments. These functions are represented using Macaulay brackets as: + + SingularityFunction(x, a, n) := ^n + + The singularity function will automatically evaluate to + ``Derivative(DiracDelta(x - a), x, -n - 1)`` if ``n < 0`` + and ``(x - a)**n*Heaviside(x - a, 1)`` if ``n >= 0``. + + Examples + ======== + + >>> from sympy import SingularityFunction, diff, Piecewise, DiracDelta, Heaviside, Symbol + >>> from sympy.abc import x, a, n + >>> SingularityFunction(x, a, n) + SingularityFunction(x, a, n) + >>> y = Symbol('y', positive=True) + >>> n = Symbol('n', nonnegative=True) + >>> SingularityFunction(y, -10, n) + (y + 10)**n + >>> y = Symbol('y', negative=True) + >>> SingularityFunction(y, 10, n) + 0 + >>> SingularityFunction(x, 4, -1).subs(x, 4) + oo + >>> SingularityFunction(x, 10, -2).subs(x, 10) + oo + >>> SingularityFunction(4, 1, 5) + 243 + >>> diff(SingularityFunction(x, 1, 5) + SingularityFunction(x, 1, 4), x) + 4*SingularityFunction(x, 1, 3) + 5*SingularityFunction(x, 1, 4) + >>> diff(SingularityFunction(x, 4, 0), x, 2) + SingularityFunction(x, 4, -2) + >>> SingularityFunction(x, 4, 5).rewrite(Piecewise) + Piecewise(((x - 4)**5, x >= 4), (0, True)) + >>> expr = SingularityFunction(x, a, n) + >>> y = Symbol('y', positive=True) + >>> n = Symbol('n', nonnegative=True) + >>> expr.subs({x: y, a: -10, n: n}) + (y + 10)**n + + The methods ``rewrite(DiracDelta)``, ``rewrite(Heaviside)``, and + ``rewrite('HeavisideDiracDelta')`` returns the same output. One can use any + of these methods according to their choice. + + >>> expr = SingularityFunction(x, 4, 5) + SingularityFunction(x, -3, -1) - SingularityFunction(x, 0, -2) + >>> expr.rewrite(Heaviside) + (x - 4)**5*Heaviside(x - 4, 1) + DiracDelta(x + 3) - DiracDelta(x, 1) + >>> expr.rewrite(DiracDelta) + (x - 4)**5*Heaviside(x - 4, 1) + DiracDelta(x + 3) - DiracDelta(x, 1) + >>> expr.rewrite('HeavisideDiracDelta') + (x - 4)**5*Heaviside(x - 4, 1) + DiracDelta(x + 3) - DiracDelta(x, 1) + + See Also + ======== + + DiracDelta, Heaviside + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Singularity_function + + """ + + is_real = True + + def fdiff(self, argindex=1): + """ + Returns the first derivative of a DiracDelta Function. + + Explanation + =========== + + The difference between ``diff()`` and ``fdiff()`` is: ``diff()`` is the + user-level function and ``fdiff()`` is an object method. ``fdiff()`` is + a convenience method available in the ``Function`` class. It returns + the derivative of the function without considering the chain rule. + ``diff(function, x)`` calls ``Function._eval_derivative`` which in turn + calls ``fdiff()`` internally to compute the derivative of the function. + + """ + + if argindex == 1: + x, a, n = self.args + if n in (S.Zero, S.NegativeOne, S(-2), S(-3)): + return self.func(x, a, n-1) + elif n.is_positive: + return n*self.func(x, a, n-1) + else: + raise ArgumentIndexError(self, argindex) + + @classmethod + def eval(cls, variable, offset, exponent): + """ + Returns a simplified form or a value of Singularity Function depending + on the argument passed by the object. + + Explanation + =========== + + The ``eval()`` method is automatically called when the + ``SingularityFunction`` class is about to be instantiated and it + returns either some simplified instance or the unevaluated instance + depending on the argument passed. In other words, ``eval()`` method is + not needed to be called explicitly, it is being called and evaluated + once the object is called. + + Examples + ======== + + >>> from sympy import SingularityFunction, Symbol, nan + >>> from sympy.abc import x, a, n + >>> SingularityFunction(x, a, n) + SingularityFunction(x, a, n) + >>> SingularityFunction(5, 3, 2) + 4 + >>> SingularityFunction(x, a, nan) + nan + >>> SingularityFunction(x, 3, 0).subs(x, 3) + 1 + >>> SingularityFunction(4, 1, 5) + 243 + >>> x = Symbol('x', positive = True) + >>> a = Symbol('a', negative = True) + >>> n = Symbol('n', nonnegative = True) + >>> SingularityFunction(x, a, n) + (-a + x)**n + >>> x = Symbol('x', negative = True) + >>> a = Symbol('a', positive = True) + >>> SingularityFunction(x, a, n) + 0 + + """ + + x = variable + a = offset + n = exponent + shift = (x - a) + + if fuzzy_not(im(shift).is_zero): + raise ValueError("Singularity Functions are defined only for Real Numbers.") + if fuzzy_not(im(n).is_zero): + raise ValueError("Singularity Functions are not defined for imaginary exponents.") + if shift is S.NaN or n is S.NaN: + return S.NaN + if (n + 4).is_negative: + raise ValueError("Singularity Functions are not defined for exponents less than -4.") + if shift.is_extended_negative: + return S.Zero + if n.is_nonnegative: + if shift.is_zero: # use literal 0 in case of Symbol('z', zero=True) + return S.Zero**n + if shift.is_extended_nonnegative: + return shift**n + if n in (S.NegativeOne, -2, -3, -4): + if shift.is_negative or shift.is_extended_positive: + return S.Zero + if shift.is_zero: + return oo + + def _eval_rewrite_as_Piecewise(self, *args, **kwargs): + ''' + Converts a Singularity Function expression into its Piecewise form. + + ''' + x, a, n = self.args + + if n in (S.NegativeOne, S(-2), S(-3), S(-4)): + return Piecewise((oo, Eq(x - a, 0)), (0, True)) + elif n.is_nonnegative: + return Piecewise(((x - a)**n, x - a >= 0), (0, True)) + + def _eval_rewrite_as_Heaviside(self, *args, **kwargs): + ''' + Rewrites a Singularity Function expression using Heavisides and DiracDeltas. + + ''' + x, a, n = self.args + + if n == -4: + return diff(Heaviside(x - a), x.free_symbols.pop(), 4) + if n == -3: + return diff(Heaviside(x - a), x.free_symbols.pop(), 3) + if n == -2: + return diff(Heaviside(x - a), x.free_symbols.pop(), 2) + if n == -1: + return diff(Heaviside(x - a), x.free_symbols.pop(), 1) + if n.is_nonnegative: + return (x - a)**n*Heaviside(x - a, 1) + + def _eval_as_leading_term(self, x, logx, cdir): + z, a, n = self.args + shift = (z - a).subs(x, 0) + if n < 0: + return S.Zero + elif n.is_zero and shift.is_zero: + return S.Zero if cdir == -1 else S.One + elif shift.is_positive: + return shift**n + return S.Zero + + def _eval_nseries(self, x, n, logx=None, cdir=0): + z, a, n = self.args + shift = (z - a).subs(x, 0) + if n < 0: + return S.Zero + elif n.is_zero and shift.is_zero: + return S.Zero if cdir == -1 else S.One + elif shift.is_positive: + return ((z - a)**n)._eval_nseries(x, n, logx=logx, cdir=cdir) + return S.Zero + + _eval_rewrite_as_DiracDelta = _eval_rewrite_as_Heaviside + _eval_rewrite_as_HeavisideDiracDelta = _eval_rewrite_as_Heaviside diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/spherical_harmonics.py b/venv/lib/python3.11/site-packages/sympy/functions/special/spherical_harmonics.py new file mode 100644 index 0000000000000000000000000000000000000000..541546b75e882b43c41814b5e92bb85ee41628d1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/spherical_harmonics.py @@ -0,0 +1,334 @@ +from sympy.core.expr import Expr +from sympy.core.function import DefinedFunction, ArgumentIndexError +from sympy.core.numbers import I, pi +from sympy.core.singleton import S +from sympy.core.symbol import Dummy +from sympy.functions import assoc_legendre +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.complexes import Abs, conjugate +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin, cos, cot + +_x = Dummy("x") + +class Ynm(DefinedFunction): + r""" + Spherical harmonics defined as + + .. math:: + Y_n^m(\theta, \varphi) := \sqrt{\frac{(2n+1)(n-m)!}{4\pi(n+m)!}} + \exp(i m \varphi) + \mathrm{P}_n^m\left(\cos(\theta)\right) + + Explanation + =========== + + ``Ynm()`` gives the spherical harmonic function of order $n$ and $m$ + in $\theta$ and $\varphi$, $Y_n^m(\theta, \varphi)$. The four + parameters are as follows: $n \geq 0$ an integer and $m$ an integer + such that $-n \leq m \leq n$ holds. The two angles are real-valued + with $\theta \in [0, \pi]$ and $\varphi \in [0, 2\pi]$. + + Examples + ======== + + >>> from sympy import Ynm, Symbol, simplify + >>> from sympy.abc import n,m + >>> theta = Symbol("theta") + >>> phi = Symbol("phi") + + >>> Ynm(n, m, theta, phi) + Ynm(n, m, theta, phi) + + Several symmetries are known, for the order: + + >>> Ynm(n, -m, theta, phi) + (-1)**m*exp(-2*I*m*phi)*Ynm(n, m, theta, phi) + + As well as for the angles: + + >>> Ynm(n, m, -theta, phi) + Ynm(n, m, theta, phi) + + >>> Ynm(n, m, theta, -phi) + exp(-2*I*m*phi)*Ynm(n, m, theta, phi) + + For specific integers $n$ and $m$ we can evaluate the harmonics + to more useful expressions: + + >>> simplify(Ynm(0, 0, theta, phi).expand(func=True)) + 1/(2*sqrt(pi)) + + >>> simplify(Ynm(1, -1, theta, phi).expand(func=True)) + sqrt(6)*exp(-I*phi)*sin(theta)/(4*sqrt(pi)) + + >>> simplify(Ynm(1, 0, theta, phi).expand(func=True)) + sqrt(3)*cos(theta)/(2*sqrt(pi)) + + >>> simplify(Ynm(1, 1, theta, phi).expand(func=True)) + -sqrt(6)*exp(I*phi)*sin(theta)/(4*sqrt(pi)) + + >>> simplify(Ynm(2, -2, theta, phi).expand(func=True)) + sqrt(30)*exp(-2*I*phi)*sin(theta)**2/(8*sqrt(pi)) + + >>> simplify(Ynm(2, -1, theta, phi).expand(func=True)) + sqrt(30)*exp(-I*phi)*sin(2*theta)/(8*sqrt(pi)) + + >>> simplify(Ynm(2, 0, theta, phi).expand(func=True)) + sqrt(5)*(3*cos(theta)**2 - 1)/(4*sqrt(pi)) + + >>> simplify(Ynm(2, 1, theta, phi).expand(func=True)) + -sqrt(30)*exp(I*phi)*sin(2*theta)/(8*sqrt(pi)) + + >>> simplify(Ynm(2, 2, theta, phi).expand(func=True)) + sqrt(30)*exp(2*I*phi)*sin(theta)**2/(8*sqrt(pi)) + + We can differentiate the functions with respect + to both angles: + + >>> from sympy import Ynm, Symbol, diff + >>> from sympy.abc import n,m + >>> theta = Symbol("theta") + >>> phi = Symbol("phi") + + >>> diff(Ynm(n, m, theta, phi), theta) + m*cot(theta)*Ynm(n, m, theta, phi) + sqrt((-m + n)*(m + n + 1))*exp(-I*phi)*Ynm(n, m + 1, theta, phi) + + >>> diff(Ynm(n, m, theta, phi), phi) + I*m*Ynm(n, m, theta, phi) + + Further we can compute the complex conjugation: + + >>> from sympy import Ynm, Symbol, conjugate + >>> from sympy.abc import n,m + >>> theta = Symbol("theta") + >>> phi = Symbol("phi") + + >>> conjugate(Ynm(n, m, theta, phi)) + (-1)**(2*m)*exp(-2*I*m*phi)*Ynm(n, m, theta, phi) + + To get back the well known expressions in spherical + coordinates, we use full expansion: + + >>> from sympy import Ynm, Symbol, expand_func + >>> from sympy.abc import n,m + >>> theta = Symbol("theta") + >>> phi = Symbol("phi") + + >>> expand_func(Ynm(n, m, theta, phi)) + sqrt((2*n + 1)*factorial(-m + n)/factorial(m + n))*exp(I*m*phi)*assoc_legendre(n, m, cos(theta))/(2*sqrt(pi)) + + See Also + ======== + + Ynm_c, Znm + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Spherical_harmonics + .. [2] https://mathworld.wolfram.com/SphericalHarmonic.html + .. [3] https://functions.wolfram.com/Polynomials/SphericalHarmonicY/ + .. [4] https://dlmf.nist.gov/14.30 + + """ + + @classmethod + def eval(cls, n, m, theta, phi): + # Handle negative index m and arguments theta, phi + if m.could_extract_minus_sign(): + m = -m + return S.NegativeOne**m * exp(-2*I*m*phi) * Ynm(n, m, theta, phi) + if theta.could_extract_minus_sign(): + theta = -theta + return Ynm(n, m, theta, phi) + if phi.could_extract_minus_sign(): + phi = -phi + return exp(-2*I*m*phi) * Ynm(n, m, theta, phi) + + # TODO Add more simplififcation here + + def _eval_expand_func(self, **hints): + n, m, theta, phi = self.args + rv = (sqrt((2*n + 1)/(4*pi) * factorial(n - m)/factorial(n + m)) * + exp(I*m*phi) * assoc_legendre(n, m, cos(theta))) + # We can do this because of the range of theta + return rv.subs(sqrt(-cos(theta)**2 + 1), sin(theta)) + + def fdiff(self, argindex=4): + if argindex == 1: + # Diff wrt n + raise ArgumentIndexError(self, argindex) + elif argindex == 2: + # Diff wrt m + raise ArgumentIndexError(self, argindex) + elif argindex == 3: + # Diff wrt theta + n, m, theta, phi = self.args + return (m * cot(theta) * Ynm(n, m, theta, phi) + + sqrt((n - m)*(n + m + 1)) * exp(-I*phi) * Ynm(n, m + 1, theta, phi)) + elif argindex == 4: + # Diff wrt phi + n, m, theta, phi = self.args + return I * m * Ynm(n, m, theta, phi) + else: + raise ArgumentIndexError(self, argindex) + + def _eval_rewrite_as_polynomial(self, n, m, theta, phi, **kwargs): + # TODO: Make sure n \in N + # TODO: Assert |m| <= n ortherwise we should return 0 + return self.expand(func=True) + + def _eval_rewrite_as_sin(self, n, m, theta, phi, **kwargs): + return self.rewrite(cos) + + def _eval_rewrite_as_cos(self, n, m, theta, phi, **kwargs): + # This method can be expensive due to extensive use of simplification! + from sympy.simplify import simplify, trigsimp + # TODO: Make sure n \in N + # TODO: Assert |m| <= n ortherwise we should return 0 + term = simplify(self.expand(func=True)) + # We can do this because of the range of theta + term = term.xreplace({Abs(sin(theta)):sin(theta)}) + return simplify(trigsimp(term)) + + def _eval_conjugate(self): + # TODO: Make sure theta \in R and phi \in R + n, m, theta, phi = self.args + return S.NegativeOne**m * self.func(n, -m, theta, phi) + + def as_real_imag(self, deep=True, **hints): + # TODO: Handle deep and hints + n, m, theta, phi = self.args + re = (sqrt((2*n + 1)/(4*pi) * factorial(n - m)/factorial(n + m)) * + cos(m*phi) * assoc_legendre(n, m, cos(theta))) + im = (sqrt((2*n + 1)/(4*pi) * factorial(n - m)/factorial(n + m)) * + sin(m*phi) * assoc_legendre(n, m, cos(theta))) + return (re, im) + + def _eval_evalf(self, prec): + # Note: works without this function by just calling + # mpmath for Legendre polynomials. But using + # the dedicated function directly is cleaner. + from mpmath import mp, workprec + n = self.args[0]._to_mpmath(prec) + m = self.args[1]._to_mpmath(prec) + theta = self.args[2]._to_mpmath(prec) + phi = self.args[3]._to_mpmath(prec) + with workprec(prec): + res = mp.spherharm(n, m, theta, phi) + return Expr._from_mpmath(res, prec) + + +def Ynm_c(n, m, theta, phi): + r""" + Conjugate spherical harmonics defined as + + .. math:: + \overline{Y_n^m(\theta, \varphi)} := (-1)^m Y_n^{-m}(\theta, \varphi). + + Examples + ======== + + >>> from sympy import Ynm_c, Symbol, simplify + >>> from sympy.abc import n,m + >>> theta = Symbol("theta") + >>> phi = Symbol("phi") + >>> Ynm_c(n, m, theta, phi) + (-1)**(2*m)*exp(-2*I*m*phi)*Ynm(n, m, theta, phi) + >>> Ynm_c(n, m, -theta, phi) + (-1)**(2*m)*exp(-2*I*m*phi)*Ynm(n, m, theta, phi) + + For specific integers $n$ and $m$ we can evaluate the harmonics + to more useful expressions: + + >>> simplify(Ynm_c(0, 0, theta, phi).expand(func=True)) + 1/(2*sqrt(pi)) + >>> simplify(Ynm_c(1, -1, theta, phi).expand(func=True)) + sqrt(6)*exp(I*(-phi + 2*conjugate(phi)))*sin(theta)/(4*sqrt(pi)) + + See Also + ======== + + Ynm, Znm + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Spherical_harmonics + .. [2] https://mathworld.wolfram.com/SphericalHarmonic.html + .. [3] https://functions.wolfram.com/Polynomials/SphericalHarmonicY/ + + """ + return conjugate(Ynm(n, m, theta, phi)) + + +class Znm(DefinedFunction): + r""" + Real spherical harmonics defined as + + .. math:: + + Z_n^m(\theta, \varphi) := + \begin{cases} + \frac{Y_n^m(\theta, \varphi) + \overline{Y_n^m(\theta, \varphi)}}{\sqrt{2}} &\quad m > 0 \\ + Y_n^m(\theta, \varphi) &\quad m = 0 \\ + \frac{Y_n^m(\theta, \varphi) - \overline{Y_n^m(\theta, \varphi)}}{i \sqrt{2}} &\quad m < 0 \\ + \end{cases} + + which gives in simplified form + + .. math:: + + Z_n^m(\theta, \varphi) = + \begin{cases} + \frac{Y_n^m(\theta, \varphi) + (-1)^m Y_n^{-m}(\theta, \varphi)}{\sqrt{2}} &\quad m > 0 \\ + Y_n^m(\theta, \varphi) &\quad m = 0 \\ + \frac{Y_n^m(\theta, \varphi) - (-1)^m Y_n^{-m}(\theta, \varphi)}{i \sqrt{2}} &\quad m < 0 \\ + \end{cases} + + Examples + ======== + + >>> from sympy import Znm, Symbol, simplify + >>> from sympy.abc import n, m + >>> theta = Symbol("theta") + >>> phi = Symbol("phi") + >>> Znm(n, m, theta, phi) + Znm(n, m, theta, phi) + + For specific integers n and m we can evaluate the harmonics + to more useful expressions: + + >>> simplify(Znm(0, 0, theta, phi).expand(func=True)) + 1/(2*sqrt(pi)) + >>> simplify(Znm(1, 1, theta, phi).expand(func=True)) + -sqrt(3)*sin(theta)*cos(phi)/(2*sqrt(pi)) + >>> simplify(Znm(2, 1, theta, phi).expand(func=True)) + -sqrt(15)*sin(2*theta)*cos(phi)/(4*sqrt(pi)) + + See Also + ======== + + Ynm, Ynm_c + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Spherical_harmonics + .. [2] https://mathworld.wolfram.com/SphericalHarmonic.html + .. [3] https://functions.wolfram.com/Polynomials/SphericalHarmonicY/ + + """ + + @classmethod + def eval(cls, n, m, theta, phi): + if m.is_positive: + zz = (Ynm(n, m, theta, phi) + Ynm_c(n, m, theta, phi)) / sqrt(2) + return zz + elif m.is_zero: + return Ynm(n, m, theta, phi) + elif m.is_negative: + zz = (Ynm(n, m, theta, phi) - Ynm_c(n, m, theta, phi)) / (sqrt(2)*I) + return zz diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tensor_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tensor_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..6d996a58cbc8320620c9a1f6e68529c3b5e99aef --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tensor_functions.py @@ -0,0 +1,474 @@ +from math import prod + +from sympy.core import S, Integer +from sympy.core.function import DefinedFunction +from sympy.core.logic import fuzzy_not +from sympy.core.relational import Ne +from sympy.core.sorting import default_sort_key +from sympy.external.gmpy import SYMPY_INTS +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.piecewise import Piecewise +from sympy.utilities.iterables import has_dups + +############################################################################### +###################### Kronecker Delta, Levi-Civita etc. ###################### +############################################################################### + + +def Eijk(*args, **kwargs): + """ + Represent the Levi-Civita symbol. + + This is a compatibility wrapper to ``LeviCivita()``. + + See Also + ======== + + LeviCivita + + """ + return LeviCivita(*args, **kwargs) + + +def eval_levicivita(*args): + """Evaluate Levi-Civita symbol.""" + n = len(args) + return prod( + prod(args[j] - args[i] for j in range(i + 1, n)) + / factorial(i) for i in range(n)) + # converting factorial(i) to int is slightly faster + + +class LeviCivita(DefinedFunction): + """ + Represent the Levi-Civita symbol. + + Explanation + =========== + + For even permutations of indices it returns 1, for odd permutations -1, and + for everything else (a repeated index) it returns 0. + + Thus it represents an alternating pseudotensor. + + Examples + ======== + + >>> from sympy import LeviCivita + >>> from sympy.abc import i, j, k + >>> LeviCivita(1, 2, 3) + 1 + >>> LeviCivita(1, 3, 2) + -1 + >>> LeviCivita(1, 2, 2) + 0 + >>> LeviCivita(i, j, k) + LeviCivita(i, j, k) + >>> LeviCivita(i, j, i) + 0 + + See Also + ======== + + Eijk + + """ + + is_integer = True + + @classmethod + def eval(cls, *args): + if all(isinstance(a, (SYMPY_INTS, Integer)) for a in args): + return eval_levicivita(*args) + if has_dups(args): + return S.Zero + + def doit(self, **hints): + return eval_levicivita(*self.args) + + +class KroneckerDelta(DefinedFunction): + """ + The discrete, or Kronecker, delta function. + + Explanation + =========== + + A function that takes in two integers $i$ and $j$. It returns $0$ if $i$ + and $j$ are not equal, or it returns $1$ if $i$ and $j$ are equal. + + Examples + ======== + + An example with integer indices: + + >>> from sympy import KroneckerDelta + >>> KroneckerDelta(1, 2) + 0 + >>> KroneckerDelta(3, 3) + 1 + + Symbolic indices: + + >>> from sympy.abc import i, j, k + >>> KroneckerDelta(i, j) + KroneckerDelta(i, j) + >>> KroneckerDelta(i, i) + 1 + >>> KroneckerDelta(i, i + 1) + 0 + >>> KroneckerDelta(i, i + 1 + k) + KroneckerDelta(i, i + k + 1) + + Parameters + ========== + + i : Number, Symbol + The first index of the delta function. + j : Number, Symbol + The second index of the delta function. + + See Also + ======== + + eval + DiracDelta + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Kronecker_delta + + """ + + is_integer = True + + @classmethod + def eval(cls, i, j, delta_range=None): + """ + Evaluates the discrete delta function. + + Examples + ======== + + >>> from sympy import KroneckerDelta + >>> from sympy.abc import i, j, k + + >>> KroneckerDelta(i, j) + KroneckerDelta(i, j) + >>> KroneckerDelta(i, i) + 1 + >>> KroneckerDelta(i, i + 1) + 0 + >>> KroneckerDelta(i, i + 1 + k) + KroneckerDelta(i, i + k + 1) + + # indirect doctest + + """ + + if delta_range is not None: + dinf, dsup = delta_range + if (dinf - i > 0) == True: + return S.Zero + if (dinf - j > 0) == True: + return S.Zero + if (dsup - i < 0) == True: + return S.Zero + if (dsup - j < 0) == True: + return S.Zero + + diff = i - j + if diff.is_zero: + return S.One + elif fuzzy_not(diff.is_zero): + return S.Zero + + if i.assumptions0.get("below_fermi") and \ + j.assumptions0.get("above_fermi"): + return S.Zero + if j.assumptions0.get("below_fermi") and \ + i.assumptions0.get("above_fermi"): + return S.Zero + # to make KroneckerDelta canonical + # following lines will check if inputs are in order + # if not, will return KroneckerDelta with correct order + if default_sort_key(j) < default_sort_key(i): + if delta_range: + return cls(j, i, delta_range) + else: + return cls(j, i) + + @property + def delta_range(self): + if len(self.args) > 2: + return self.args[2] + + def _eval_power(self, expt): + if expt.is_positive: + return self + if expt.is_negative and expt is not S.NegativeOne: + return 1/self + + @property + def is_above_fermi(self): + """ + True if Delta can be non-zero above fermi. + + Examples + ======== + + >>> from sympy import KroneckerDelta, Symbol + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + >>> q = Symbol('q') + >>> KroneckerDelta(p, a).is_above_fermi + True + >>> KroneckerDelta(p, i).is_above_fermi + False + >>> KroneckerDelta(p, q).is_above_fermi + True + + See Also + ======== + + is_below_fermi, is_only_below_fermi, is_only_above_fermi + + """ + if self.args[0].assumptions0.get("below_fermi"): + return False + if self.args[1].assumptions0.get("below_fermi"): + return False + return True + + @property + def is_below_fermi(self): + """ + True if Delta can be non-zero below fermi. + + Examples + ======== + + >>> from sympy import KroneckerDelta, Symbol + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + >>> q = Symbol('q') + >>> KroneckerDelta(p, a).is_below_fermi + False + >>> KroneckerDelta(p, i).is_below_fermi + True + >>> KroneckerDelta(p, q).is_below_fermi + True + + See Also + ======== + + is_above_fermi, is_only_above_fermi, is_only_below_fermi + + """ + if self.args[0].assumptions0.get("above_fermi"): + return False + if self.args[1].assumptions0.get("above_fermi"): + return False + return True + + @property + def is_only_above_fermi(self): + """ + True if Delta is restricted to above fermi. + + Examples + ======== + + >>> from sympy import KroneckerDelta, Symbol + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + >>> q = Symbol('q') + >>> KroneckerDelta(p, a).is_only_above_fermi + True + >>> KroneckerDelta(p, q).is_only_above_fermi + False + >>> KroneckerDelta(p, i).is_only_above_fermi + False + + See Also + ======== + + is_above_fermi, is_below_fermi, is_only_below_fermi + + """ + return ( self.args[0].assumptions0.get("above_fermi") + or + self.args[1].assumptions0.get("above_fermi") + ) or False + + @property + def is_only_below_fermi(self): + """ + True if Delta is restricted to below fermi. + + Examples + ======== + + >>> from sympy import KroneckerDelta, Symbol + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + >>> q = Symbol('q') + >>> KroneckerDelta(p, i).is_only_below_fermi + True + >>> KroneckerDelta(p, q).is_only_below_fermi + False + >>> KroneckerDelta(p, a).is_only_below_fermi + False + + See Also + ======== + + is_above_fermi, is_below_fermi, is_only_above_fermi + + """ + return ( self.args[0].assumptions0.get("below_fermi") + or + self.args[1].assumptions0.get("below_fermi") + ) or False + + @property + def indices_contain_equal_information(self): + """ + Returns True if indices are either both above or below fermi. + + Examples + ======== + + >>> from sympy import KroneckerDelta, Symbol + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + >>> q = Symbol('q') + >>> KroneckerDelta(p, q).indices_contain_equal_information + True + >>> KroneckerDelta(p, q+1).indices_contain_equal_information + True + >>> KroneckerDelta(i, p).indices_contain_equal_information + False + + """ + if (self.args[0].assumptions0.get("below_fermi") and + self.args[1].assumptions0.get("below_fermi")): + return True + if (self.args[0].assumptions0.get("above_fermi") + and self.args[1].assumptions0.get("above_fermi")): + return True + + # if both indices are general we are True, else false + return self.is_below_fermi and self.is_above_fermi + + @property + def preferred_index(self): + """ + Returns the index which is preferred to keep in the final expression. + + Explanation + =========== + + The preferred index is the index with more information regarding fermi + level. If indices contain the same information, 'a' is preferred before + 'b'. + + Examples + ======== + + >>> from sympy import KroneckerDelta, Symbol + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> j = Symbol('j', below_fermi=True) + >>> p = Symbol('p') + >>> KroneckerDelta(p, i).preferred_index + i + >>> KroneckerDelta(p, a).preferred_index + a + >>> KroneckerDelta(i, j).preferred_index + i + + See Also + ======== + + killable_index + + """ + if self._get_preferred_index(): + return self.args[1] + else: + return self.args[0] + + @property + def killable_index(self): + """ + Returns the index which is preferred to substitute in the final + expression. + + Explanation + =========== + + The index to substitute is the index with less information regarding + fermi level. If indices contain the same information, 'a' is preferred + before 'b'. + + Examples + ======== + + >>> from sympy import KroneckerDelta, Symbol + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> j = Symbol('j', below_fermi=True) + >>> p = Symbol('p') + >>> KroneckerDelta(p, i).killable_index + p + >>> KroneckerDelta(p, a).killable_index + p + >>> KroneckerDelta(i, j).killable_index + j + + See Also + ======== + + preferred_index + + """ + if self._get_preferred_index(): + return self.args[0] + else: + return self.args[1] + + def _get_preferred_index(self): + """ + Returns the index which is preferred to keep in the final expression. + + The preferred index is the index with more information regarding fermi + level. If indices contain the same information, index 0 is returned. + + """ + if not self.is_above_fermi: + if self.args[0].assumptions0.get("below_fermi"): + return 0 + else: + return 1 + elif not self.is_below_fermi: + if self.args[0].assumptions0.get("above_fermi"): + return 0 + else: + return 1 + else: + return 0 + + @property + def indices(self): + return self.args[0:2] + + def _eval_rewrite_as_Piecewise(self, *args, **kwargs): + i, j = args + return Piecewise((0, Ne(i, j)), (1, True)) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_bessel.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_bessel.py new file mode 100644 index 0000000000000000000000000000000000000000..ccd1ce88ca9dea15f065e7c57d488498b8f79f4e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_bessel.py @@ -0,0 +1,807 @@ +from itertools import product + +from sympy.concrete.summations import Sum +from sympy.core.function import (diff, expand_func) +from sympy.core.numbers import (I, Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import (conjugate, polar_lift) +from sympy.functions.elementary.exponential import (exp, exp_polar, log) +from sympy.functions.elementary.hyperbolic import (cosh, sinh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.functions.special.bessel import (besseli, besselj, besselk, bessely, hankel1, hankel2, hn1, hn2, jn, jn_zeros, yn) +from sympy.functions.special.gamma_functions import (gamma, uppergamma) +from sympy.functions.special.hyper import hyper +from sympy.integrals.integrals import Integral +from sympy.series.order import O +from sympy.series.series import series +from sympy.functions.special.bessel import (airyai, airybi, + airyaiprime, airybiprime, marcumq) +from sympy.core.random import (random_complex_number as randcplx, + verify_numerically as tn, + test_derivative_numerically as td, + _randint) +from sympy.simplify import besselsimp +from sympy.testing.pytest import raises, slow + +from sympy.abc import z, n, k, x + +randint = _randint() + + +def test_bessel_rand(): + for f in [besselj, bessely, besseli, besselk, hankel1, hankel2]: + assert td(f(randcplx(), z), z) + + for f in [jn, yn, hn1, hn2]: + assert td(f(randint(-10, 10), z), z) + + +def test_bessel_twoinputs(): + for f in [besselj, bessely, besseli, besselk, hankel1, hankel2, jn, yn]: + raises(TypeError, lambda: f(1)) + raises(TypeError, lambda: f(1, 2, 3)) + + +def test_besselj_leading_term(): + assert besselj(0, x).as_leading_term(x) == 1 + assert besselj(1, sin(x)).as_leading_term(x) == x/2 + assert besselj(1, 2*sqrt(x)).as_leading_term(x) == sqrt(x) + + # https://github.com/sympy/sympy/issues/21701 + assert (besselj(z, x)/x**z).as_leading_term(x) == 1/(2**z*gamma(z + 1)) + + +def test_bessely_leading_term(): + assert bessely(0, x).as_leading_term(x) == (2*log(x) - 2*log(2) + 2*S.EulerGamma)/pi + assert bessely(1, sin(x)).as_leading_term(x) == -2/(pi*x) + assert bessely(1, 2*sqrt(x)).as_leading_term(x) == -1/(pi*sqrt(x)) + + +def test_besseli_leading_term(): + assert besseli(0, x).as_leading_term(x) == 1 + assert besseli(1, sin(x)).as_leading_term(x) == x/2 + assert besseli(1, 2*sqrt(x)).as_leading_term(x) == sqrt(x) + + +def test_besselk_leading_term(): + assert besselk(0, x).as_leading_term(x) == -log(x) - S.EulerGamma + log(2) + assert besselk(1, sin(x)).as_leading_term(x) == 1/x + assert besselk(1, 2*sqrt(x)).as_leading_term(x) == 1/(2*sqrt(x)) + assert besselk(S(5)/3, x).as_leading_term(x) == 2**(S(2)/3)*gamma(S(5)/3)/x**(S(5)/3) + assert besselk(S(2)/3, x).as_leading_term(x) == besselk(-S(2)/3, x).as_leading_term(x) + assert besselk(1,cos(x)).as_leading_term(x) == besselk(1,1) + assert besselk(3,1/x).as_leading_term(x) == sqrt(pi)*exp(-(1/x))/sqrt(2/x) + assert besselk(3,1/sin(x)).as_leading_term(x) == sqrt(pi)*exp(-(1/x))/sqrt(2/x) + + nz = Symbol("nz", nonzero=True) + assert besselk(nz, x).as_leading_term(x).subs({nz:S(5)/7}) == besselk(S(5)/7, x).series(x).as_leading_term(x) + assert besselk(nz, x).as_leading_term(x).subs({nz:S(-15)/7}) == besselk(S(-15)/7, x).series(x).as_leading_term(x) + assert besselk(nz, x).as_leading_term(x).subs({nz:3}) == besselk(3, x).series(x).as_leading_term(x) + assert besselk(nz, x).as_leading_term(x).subs({nz:-2}) == besselk(-2, x).series(x).as_leading_term(x) + + +def test_besselj_series(): + assert besselj(0, x).series(x) == 1 - x**2/4 + x**4/64 + O(x**6) + assert besselj(0, x**(1.1)).series(x) == 1 + x**4.4/64 - x**2.2/4 + O(x**6) + assert besselj(0, x**2 + x).series(x) == 1 - x**2/4 - x**3/2\ + - 15*x**4/64 + x**5/16 + O(x**6) + assert besselj(0, sqrt(x) + x).series(x, n=4) == 1 - x/4 - 15*x**2/64\ + + 215*x**3/2304 - x**Rational(3, 2)/2 + x**Rational(5, 2)/16\ + + 23*x**Rational(7, 2)/384 + O(x**4) + assert besselj(0, x/(1 - x)).series(x) == 1 - x**2/4 - x**3/2 - 47*x**4/64\ + - 15*x**5/16 + O(x**6) + assert besselj(0, log(1 + x)).series(x) == 1 - x**2/4 + x**3/4\ + - 41*x**4/192 + 17*x**5/96 + O(x**6) + assert besselj(1, sin(x)).series(x) == x/2 - 7*x**3/48 + 73*x**5/1920 + O(x**6) + assert besselj(1, 2*sqrt(x)).series(x) == sqrt(x) - x**Rational(3, 2)/2\ + + x**Rational(5, 2)/12 - x**Rational(7, 2)/144 + x**Rational(9, 2)/2880\ + - x**Rational(11, 2)/86400 + O(x**6) + assert besselj(-2, sin(x)).series(x, n=4) == besselj(2, sin(x)).series(x, n=4) + + +def test_bessely_series(): + const = 2*S.EulerGamma/pi - 2*log(2)/pi + 2*log(x)/pi + assert bessely(0, x).series(x, n=4) == const + x**2*(-log(x)/(2*pi)\ + + (2 - 2*S.EulerGamma)/(4*pi) + log(2)/(2*pi)) + O(x**4*log(x)) + assert bessely(1, x).series(x, n=4) == -2/(pi*x) + x*(log(x)/pi - log(2)/pi - \ + (1 - 2*S.EulerGamma)/(2*pi)) + x**3*(-log(x)/(8*pi) + \ + (S(5)/2 - 2*S.EulerGamma)/(16*pi) + log(2)/(8*pi)) + O(x**4*log(x)) + assert bessely(2, x).series(x, n=4) == -4/(pi*x**2) - 1/pi + x**2*(log(x)/(4*pi) - \ + log(2)/(4*pi) - (S(3)/2 - 2*S.EulerGamma)/(8*pi)) + O(x**4*log(x)) + assert bessely(3, x).series(x, n=4) == -16/(pi*x**3) - 2/(pi*x) - \ + x/(4*pi) + x**3*(log(x)/(24*pi) - log(2)/(24*pi) - \ + (S(11)/6 - 2*S.EulerGamma)/(48*pi)) + O(x**4*log(x)) + assert bessely(0, x**(1.1)).series(x, n=4) == 2*S.EulerGamma/pi\ + - 2*log(2)/pi + 2.2*log(x)/pi + x**2.2*(-0.55*log(x)/pi\ + + (2 - 2*S.EulerGamma)/(4*pi) + log(2)/(2*pi)) + O(x**4*log(x)) + assert bessely(0, x**2 + x).series(x, n=4) == \ + const - (2 - 2*S.EulerGamma)*(-x**3/(2*pi) - x**2/(4*pi)) + 2*x/pi\ + + x**2*(-log(x)/(2*pi) - 1/pi + log(2)/(2*pi))\ + + x**3*(-log(x)/pi + 1/(6*pi) + log(2)/pi) + O(x**4*log(x)) + assert bessely(0, x/(1 - x)).series(x, n=3) == const\ + + 2*x/pi + x**2*(-log(x)/(2*pi) + (2 - 2*S.EulerGamma)/(4*pi)\ + + log(2)/(2*pi) + 1/pi) + O(x**3*log(x)) + assert bessely(0, log(1 + x)).series(x, n=3) == const\ + - x/pi + x**2*(-log(x)/(2*pi) + (2 - 2*S.EulerGamma)/(4*pi)\ + + log(2)/(2*pi) + 5/(12*pi)) + O(x**3*log(x)) + assert bessely(1, sin(x)).series(x, n=4) == -1/(pi*(-x**3/12 + x/2)) - \ + (1 - 2*S.EulerGamma)*(-x**3/12 + x/2)/pi + x*(log(x)/pi - log(2)/pi) + \ + x**3*(-7*log(x)/(24*pi) - 1/(6*pi) + (S(5)/2 - 2*S.EulerGamma)/(16*pi) + + 7*log(2)/(24*pi)) + O(x**4*log(x)) + assert bessely(1, 2*sqrt(x)).series(x, n=3) == -1/(pi*sqrt(x)) + \ + sqrt(x)*(log(x)/pi - (1 - 2*S.EulerGamma)/pi) + x**(S(3)/2)*(-log(x)/(2*pi) + \ + (S(5)/2 - 2*S.EulerGamma)/(2*pi)) + x**(S(5)/2)*(log(x)/(12*pi) - \ + (S(10)/3 - 2*S.EulerGamma)/(12*pi)) + O(x**3*log(x)) + assert bessely(-2, sin(x)).series(x, n=4) == bessely(2, sin(x)).series(x, n=4) + + +def test_besseli_series(): + assert besseli(0, x).series(x) == 1 + x**2/4 + x**4/64 + O(x**6) + assert besseli(0, x**(1.1)).series(x) == 1 + x**4.4/64 + x**2.2/4 + O(x**6) + assert besseli(0, x**2 + x).series(x) == 1 + x**2/4 + x**3/2 + 17*x**4/64 + \ + x**5/16 + O(x**6) + assert besseli(0, sqrt(x) + x).series(x, n=4) == 1 + x/4 + 17*x**2/64 + \ + 217*x**3/2304 + x**(S(3)/2)/2 + x**(S(5)/2)/16 + 25*x**(S(7)/2)/384 + O(x**4) + assert besseli(0, x/(1 - x)).series(x) == 1 + x**2/4 + x**3/2 + 49*x**4/64 + \ + 17*x**5/16 + O(x**6) + assert besseli(0, log(1 + x)).series(x) == 1 + x**2/4 - x**3/4 + 47*x**4/192 - \ + 23*x**5/96 + O(x**6) + assert besseli(1, sin(x)).series(x) == x/2 - x**3/48 - 47*x**5/1920 + O(x**6) + assert besseli(1, 2*sqrt(x)).series(x) == sqrt(x) + x**(S(3)/2)/2 + x**(S(5)/2)/12 + \ + x**(S(7)/2)/144 + x**(S(9)/2)/2880 + x**(S(11)/2)/86400 + O(x**6) + assert besseli(-2, sin(x)).series(x, n=4) == besseli(2, sin(x)).series(x, n=4) + + #test for aseries + assert besseli(0,x).series(x, oo, n=4) == sqrt(2)*(sqrt(1/x) - (1/x)**(S(3)/2)/8 - \ + 3*(1/x)**(S(5)/2)/128 - 15*(1/x)**(S(7)/2)/1024 + O((1/x)**(S(9)/2), (x, oo)))*exp(x)/(2*sqrt(pi)) + assert besseli(0,x).series(x,-oo, n=4) == sqrt(2)*(sqrt(-1/x) - (-1/x)**(S(3)/2)/8 - 3*(-1/x)**(S(5)/2)/128 - \ + 15*(-1/x)**(S(7)/2)/1024 + O((-1/x)**(S(9)/2), (x, -oo)))*exp(-x)/(2*sqrt(pi)) + + +def test_besselk_series(): + const = log(2) - S.EulerGamma - log(x) + assert besselk(0, x).series(x, n=4) == const + \ + x**2*(-log(x)/4 - S.EulerGamma/4 + log(2)/4 + S(1)/4) + O(x**4*log(x)) + assert besselk(1, x).series(x, n=4) == 1/x + x*(log(x)/2 - log(2)/2 - \ + S(1)/4 + S.EulerGamma/2) + x**3*(log(x)/16 - S(5)/64 - log(2)/16 + \ + S.EulerGamma/16) + O(x**4*log(x)) + assert besselk(2, x).series(x, n=4) == 2/x**2 - S(1)/2 + x**2*(-log(x)/8 - \ + S.EulerGamma/8 + log(2)/8 + S(3)/32) + O(x**4*log(x)) + assert besselk(2, x).series(x, n=1) == 2/x**2 - S(1)/2 + O(x) #edge case for series truncation + assert besselk(0, x**(1.1)).series(x, n=4) == log(2) - S.EulerGamma - \ + 1.1*log(x) + x**2.2*(-0.275*log(x) - S.EulerGamma/4 + \ + log(2)/4 + S(1)/4) + O(x**4*log(x)) + assert besselk(0, x**2 + x).series(x, n=4) == const + \ + (2 - 2*S.EulerGamma)*(x**3/4 + x**2/8) - x + x**2*(-log(x)/4 + \ + log(2)/4 + S(1)/2) + x**3*(-log(x)/2 - S(7)/12 + log(2)/2) + O(x**4*log(x)) + assert besselk(0, x/(1 - x)).series(x, n=3) == const - x + x**2*(-log(x)/4 - \ + S(1)/4 - S.EulerGamma/4 + log(2)/4) + O(x**3*log(x)) + assert besselk(0, log(1 + x)).series(x, n=3) == const + x/2 + \ + x**2*(-log(x)/4 - S.EulerGamma/4 + S(1)/24 + log(2)/4) + O(x**3*log(x)) + assert besselk(1, 2*sqrt(x)).series(x, n=3) == 1/(2*sqrt(x)) + \ + sqrt(x)*(log(x)/2 - S(1)/2 + S.EulerGamma) + x**(S(3)/2)*(log(x)/4 - S(5)/8 + \ + S.EulerGamma/2) + x**(S(5)/2)*(log(x)/24 - S(5)/36 + S.EulerGamma/12) + O(x**3*log(x)) + assert besselk(-2, sin(x)).series(x, n=4) == besselk(2, sin(x)).series(x, n=4) + assert besselk(2, x**2).series(x, n=2) == 2/x**4 - S(1)/2 + O(x**2) #edge case for series truncation + assert besselk(2, x**2).series(x, n=6) == 2/x**4 - S(1)/2 + x**4*(-log(x)/4 - S.EulerGamma/8 + log(2)/8 + S(3)/32) + O(x**6*log(x)) + assert (x**2*besselk(2, x)).series(x, n=2) == 2 + O(x**2) + + #test for aseries + assert besselk(0,x).series(x, oo, n=4) == sqrt(2)*sqrt(pi)*(sqrt(1/x) + (1/x)**(S(3)/2)/8 - \ + 3*(1/x)**(S(5)/2)/128 + 15*(1/x)**(S(7)/2)/1024 + O((1/x)**(S(9)/2), (x, oo)))*exp(-x)/2 + assert besselk(0,x).series(x, -oo, n=4) == sqrt(2)*sqrt(pi)*(-I*sqrt(-1/x) + I*(-1/x)**(S(3)/2)/8 + \ + 3*I*(-1/x)**(S(5)/2)/128 + 15*I*(-1/x)**(S(7)/2)/1024 + O((-1/x)**(S(9)/2), (x, -oo)))*exp(-x)/2 + + +def test_besselk_frac_order_series(): + assert besselk(S(5)/3, x).series(x, n=2) == 2**(S(2)/3)*gamma(S(5)/3)/x**(S(5)/3) - \ + 3*gamma(S(5)/3)*x**(S(1)/3)/(4*2**(S(1)/3)) + \ + gamma(-S(5)/3)*x**(S(5)/3)/(4*2**(S(2)/3)) + O(x**2) + assert besselk(S(1)/2, x).series(x, n=2) == sqrt(pi/2)/sqrt(x) - \ + sqrt(pi*x/2) + x**(S(3)/2)*sqrt(pi/2)/2 + O(x**2) + assert besselk(S(1)/2, sqrt(x)).series(x, n=2) == sqrt(pi/2)/x**(S(1)/4) - \ + sqrt(pi/2)*x**(S(1)/4) + sqrt(pi/2)*x**(S(3)/4)/2 - \ + sqrt(pi/2)*x**(S(5)/4)/6 + sqrt(pi/2)*x**(S(7)/4)/24 + O(x**2) + assert besselk(S(1)/2, x**2).series(x, n=2) == sqrt(pi/2)/x \ + - sqrt(pi/2)*x + O(x**2) + assert besselk(-S(1)/2, x).series(x) == besselk(S(1)/2, x).series(x) + assert besselk(-S(7)/6, x).series(x) == besselk(S(7)/6, x).series(x) + + +def test_diff(): + assert besselj(n, z).diff(z) == besselj(n - 1, z)/2 - besselj(n + 1, z)/2 + assert bessely(n, z).diff(z) == bessely(n - 1, z)/2 - bessely(n + 1, z)/2 + assert besseli(n, z).diff(z) == besseli(n - 1, z)/2 + besseli(n + 1, z)/2 + assert besselk(n, z).diff(z) == -besselk(n - 1, z)/2 - besselk(n + 1, z)/2 + assert hankel1(n, z).diff(z) == hankel1(n - 1, z)/2 - hankel1(n + 1, z)/2 + assert hankel2(n, z).diff(z) == hankel2(n - 1, z)/2 - hankel2(n + 1, z)/2 + + +def test_rewrite(): + assert besselj(n, z).rewrite(jn) == sqrt(2*z/pi)*jn(n - S.Half, z) + assert bessely(n, z).rewrite(yn) == sqrt(2*z/pi)*yn(n - S.Half, z) + assert besseli(n, z).rewrite(besselj) == \ + exp(-I*n*pi/2)*besselj(n, polar_lift(I)*z) + assert besselj(n, z).rewrite(besseli) == \ + exp(I*n*pi/2)*besseli(n, polar_lift(-I)*z) + + nu = randcplx() + + assert tn(besselj(nu, z), besselj(nu, z).rewrite(besseli), z) + assert tn(besselj(nu, z), besselj(nu, z).rewrite(bessely), z) + + assert tn(besseli(nu, z), besseli(nu, z).rewrite(besselj), z) + assert tn(besseli(nu, z), besseli(nu, z).rewrite(bessely), z) + + assert tn(bessely(nu, z), bessely(nu, z).rewrite(besselj), z) + assert tn(bessely(nu, z), bessely(nu, z).rewrite(besseli), z) + + assert tn(besselk(nu, z), besselk(nu, z).rewrite(besselj), z) + assert tn(besselk(nu, z), besselk(nu, z).rewrite(besseli), z) + assert tn(besselk(nu, z), besselk(nu, z).rewrite(bessely), z) + + # check that a rewrite was triggered, when the order is set to a generic + # symbol 'nu' + assert yn(nu, z) != yn(nu, z).rewrite(jn) + assert hn1(nu, z) != hn1(nu, z).rewrite(jn) + assert hn2(nu, z) != hn2(nu, z).rewrite(jn) + assert jn(nu, z) != jn(nu, z).rewrite(yn) + assert hn1(nu, z) != hn1(nu, z).rewrite(yn) + assert hn2(nu, z) != hn2(nu, z).rewrite(yn) + + # rewriting spherical bessel functions (SBFs) w.r.t. besselj, bessely is + # not allowed if a generic symbol 'nu' is used as the order of the SBFs + # to avoid inconsistencies (the order of bessel[jy] is allowed to be + # complex-valued, whereas SBFs are defined only for integer orders) + order = nu + for f in (besselj, bessely): + assert hn1(order, z) == hn1(order, z).rewrite(f) + assert hn2(order, z) == hn2(order, z).rewrite(f) + + assert jn(order, z).rewrite(besselj) == sqrt(2)*sqrt(pi)*sqrt(1/z)*besselj(order + S.Half, z)/2 + assert jn(order, z).rewrite(bessely) == (-1)**nu*sqrt(2)*sqrt(pi)*sqrt(1/z)*bessely(-order - S.Half, z)/2 + + # for integral orders rewriting SBFs w.r.t bessel[jy] is allowed + N = Symbol('n', integer=True) + ri = randint(-11, 10) + for order in (ri, N): + for f in (besselj, bessely): + assert yn(order, z) != yn(order, z).rewrite(f) + assert jn(order, z) != jn(order, z).rewrite(f) + assert hn1(order, z) != hn1(order, z).rewrite(f) + assert hn2(order, z) != hn2(order, z).rewrite(f) + + for func, refunc in product((yn, jn, hn1, hn2), + (jn, yn, besselj, bessely)): + assert tn(func(ri, z), func(ri, z).rewrite(refunc), z) + + +def test_expand(): + assert expand_func(besselj(S.Half, z).rewrite(jn)) == \ + sqrt(2)*sin(z)/(sqrt(pi)*sqrt(z)) + assert expand_func(bessely(S.Half, z).rewrite(yn)) == \ + -sqrt(2)*cos(z)/(sqrt(pi)*sqrt(z)) + + # XXX: teach sin/cos to work around arguments like + # x*exp_polar(I*pi*n/2). Then change besselsimp -> expand_func + assert besselsimp(besselj(S.Half, z)) == sqrt(2)*sin(z)/(sqrt(pi)*sqrt(z)) + assert besselsimp(besselj(Rational(-1, 2), z)) == sqrt(2)*cos(z)/(sqrt(pi)*sqrt(z)) + assert besselsimp(besselj(Rational(5, 2), z)) == \ + -sqrt(2)*(z**2*sin(z) + 3*z*cos(z) - 3*sin(z))/(sqrt(pi)*z**Rational(5, 2)) + assert besselsimp(besselj(Rational(-5, 2), z)) == \ + -sqrt(2)*(z**2*cos(z) - 3*z*sin(z) - 3*cos(z))/(sqrt(pi)*z**Rational(5, 2)) + + assert besselsimp(bessely(S.Half, z)) == \ + -(sqrt(2)*cos(z))/(sqrt(pi)*sqrt(z)) + assert besselsimp(bessely(Rational(-1, 2), z)) == sqrt(2)*sin(z)/(sqrt(pi)*sqrt(z)) + assert besselsimp(bessely(Rational(5, 2), z)) == \ + sqrt(2)*(z**2*cos(z) - 3*z*sin(z) - 3*cos(z))/(sqrt(pi)*z**Rational(5, 2)) + assert besselsimp(bessely(Rational(-5, 2), z)) == \ + -sqrt(2)*(z**2*sin(z) + 3*z*cos(z) - 3*sin(z))/(sqrt(pi)*z**Rational(5, 2)) + + assert besselsimp(besseli(S.Half, z)) == sqrt(2)*sinh(z)/(sqrt(pi)*sqrt(z)) + assert besselsimp(besseli(Rational(-1, 2), z)) == \ + sqrt(2)*cosh(z)/(sqrt(pi)*sqrt(z)) + assert besselsimp(besseli(Rational(5, 2), z)) == \ + sqrt(2)*(z**2*sinh(z) - 3*z*cosh(z) + 3*sinh(z))/(sqrt(pi)*z**Rational(5, 2)) + assert besselsimp(besseli(Rational(-5, 2), z)) == \ + sqrt(2)*(z**2*cosh(z) - 3*z*sinh(z) + 3*cosh(z))/(sqrt(pi)*z**Rational(5, 2)) + + assert besselsimp(besselk(S.Half, z)) == \ + besselsimp(besselk(Rational(-1, 2), z)) == sqrt(pi)*exp(-z)/(sqrt(2)*sqrt(z)) + assert besselsimp(besselk(Rational(5, 2), z)) == \ + besselsimp(besselk(Rational(-5, 2), z)) == \ + sqrt(2)*sqrt(pi)*(z**2 + 3*z + 3)*exp(-z)/(2*z**Rational(5, 2)) + + n = Symbol('n', integer=True, positive=True) + + assert expand_func(besseli(n + 2, z)) == \ + besseli(n, z) + (-2*n - 2)*(-2*n*besseli(n, z)/z + besseli(n - 1, z))/z + assert expand_func(besselj(n + 2, z)) == \ + -besselj(n, z) + (2*n + 2)*(2*n*besselj(n, z)/z - besselj(n - 1, z))/z + assert expand_func(besselk(n + 2, z)) == \ + besselk(n, z) + (2*n + 2)*(2*n*besselk(n, z)/z + besselk(n - 1, z))/z + assert expand_func(bessely(n + 2, z)) == \ + -bessely(n, z) + (2*n + 2)*(2*n*bessely(n, z)/z - bessely(n - 1, z))/z + + assert expand_func(besseli(n + S.Half, z).rewrite(jn)) == \ + (sqrt(2)*sqrt(z)*exp(-I*pi*(n + S.Half)/2) * + exp_polar(I*pi/4)*jn(n, z*exp_polar(I*pi/2))/sqrt(pi)) + assert expand_func(besselj(n + S.Half, z).rewrite(jn)) == \ + sqrt(2)*sqrt(z)*jn(n, z)/sqrt(pi) + + r = Symbol('r', real=True) + p = Symbol('p', positive=True) + i = Symbol('i', integer=True) + + for besselx in [besselj, bessely, besseli, besselk]: + assert besselx(i, p).is_extended_real is True + assert besselx(i, x).is_extended_real is None + assert besselx(x, z).is_extended_real is None + + for besselx in [besselj, besseli]: + assert besselx(i, r).is_extended_real is True + for besselx in [bessely, besselk]: + assert besselx(i, r).is_extended_real is None + + for besselx in [besselj, bessely, besseli, besselk]: + assert expand_func(besselx(oo, x)) == besselx(oo, x, evaluate=False) + assert expand_func(besselx(-oo, x)) == besselx(-oo, x, evaluate=False) + + +# Quite varying time, but often really slow +@slow +def test_slow_expand(): + def check(eq, ans): + return tn(eq, ans) and eq == ans + + rn = randcplx(a=1, b=0, d=0, c=2) + + for besselx in [besselj, bessely, besseli, besselk]: + ri = S(2*randint(-11, 10) + 1) / 2 # half integer in [-21/2, 21/2] + assert tn(besselsimp(besselx(ri, z)), besselx(ri, z)) + + assert check(expand_func(besseli(rn, x)), + besseli(rn - 2, x) - 2*(rn - 1)*besseli(rn - 1, x)/x) + assert check(expand_func(besseli(-rn, x)), + besseli(-rn + 2, x) + 2*(-rn + 1)*besseli(-rn + 1, x)/x) + + assert check(expand_func(besselj(rn, x)), + -besselj(rn - 2, x) + 2*(rn - 1)*besselj(rn - 1, x)/x) + assert check(expand_func(besselj(-rn, x)), + -besselj(-rn + 2, x) + 2*(-rn + 1)*besselj(-rn + 1, x)/x) + + assert check(expand_func(besselk(rn, x)), + besselk(rn - 2, x) + 2*(rn - 1)*besselk(rn - 1, x)/x) + assert check(expand_func(besselk(-rn, x)), + besselk(-rn + 2, x) - 2*(-rn + 1)*besselk(-rn + 1, x)/x) + + assert check(expand_func(bessely(rn, x)), + -bessely(rn - 2, x) + 2*(rn - 1)*bessely(rn - 1, x)/x) + assert check(expand_func(bessely(-rn, x)), + -bessely(-rn + 2, x) + 2*(-rn + 1)*bessely(-rn + 1, x)/x) + + +def mjn(n, z): + return expand_func(jn(n, z)) + + +def myn(n, z): + return expand_func(yn(n, z)) + + +def test_jn(): + z = symbols("z") + assert jn(0, 0) == 1 + assert jn(1, 0) == 0 + assert jn(-1, 0) == S.ComplexInfinity + assert jn(z, 0) == jn(z, 0, evaluate=False) + assert jn(0, oo) == 0 + assert jn(0, -oo) == 0 + + assert mjn(0, z) == sin(z)/z + assert mjn(1, z) == sin(z)/z**2 - cos(z)/z + assert mjn(2, z) == (3/z**3 - 1/z)*sin(z) - (3/z**2) * cos(z) + assert mjn(3, z) == (15/z**4 - 6/z**2)*sin(z) + (1/z - 15/z**3)*cos(z) + assert mjn(4, z) == (1/z + 105/z**5 - 45/z**3)*sin(z) + \ + (-105/z**4 + 10/z**2)*cos(z) + assert mjn(5, z) == (945/z**6 - 420/z**4 + 15/z**2)*sin(z) + \ + (-1/z - 945/z**5 + 105/z**3)*cos(z) + assert mjn(6, z) == (-1/z + 10395/z**7 - 4725/z**5 + 210/z**3)*sin(z) + \ + (-10395/z**6 + 1260/z**4 - 21/z**2)*cos(z) + + assert expand_func(jn(n, z)) == jn(n, z) + + # SBFs not defined for complex-valued orders + assert jn(2+3j, 5.2+0.3j).evalf() == jn(2+3j, 5.2+0.3j) + + assert eq([jn(2, 5.2+0.3j).evalf(10)], + [0.09941975672 - 0.05452508024*I]) + + +def test_yn(): + z = symbols("z") + assert myn(0, z) == -cos(z)/z + assert myn(1, z) == -cos(z)/z**2 - sin(z)/z + assert myn(2, z) == -((3/z**3 - 1/z)*cos(z) + (3/z**2)*sin(z)) + assert expand_func(yn(n, z)) == yn(n, z) + + # SBFs not defined for complex-valued orders + assert yn(2+3j, 5.2+0.3j).evalf() == yn(2+3j, 5.2+0.3j) + + assert eq([yn(2, 5.2+0.3j).evalf(10)], + [0.185250342 + 0.01489557397*I]) + + +def test_sympify_yn(): + assert S(15) in myn(3, pi).atoms() + assert myn(3, pi) == 15/pi**4 - 6/pi**2 + + +def eq(a, b, tol=1e-6): + for u, v in zip(a, b): + if not (abs(u - v) < tol): + return False + return True + + +def test_jn_zeros(): + assert eq(jn_zeros(0, 4), [3.141592, 6.283185, 9.424777, 12.566370]) + assert eq(jn_zeros(1, 4), [4.493409, 7.725251, 10.904121, 14.066193]) + assert eq(jn_zeros(2, 4), [5.763459, 9.095011, 12.322940, 15.514603]) + assert eq(jn_zeros(3, 4), [6.987932, 10.417118, 13.698023, 16.923621]) + assert eq(jn_zeros(4, 4), [8.182561, 11.704907, 15.039664, 18.301255]) + + +def test_bessel_eval(): + n, m, k = Symbol('n', integer=True), Symbol('m'), Symbol('k', integer=True, zero=False) + + for f in [besselj, besseli]: + assert f(0, 0) is S.One + assert f(2.1, 0) is S.Zero + assert f(-3, 0) is S.Zero + assert f(-10.2, 0) is S.ComplexInfinity + assert f(1 + 3*I, 0) is S.Zero + assert f(-3 + I, 0) is S.ComplexInfinity + assert f(-2*I, 0) is S.NaN + assert f(n, 0) != S.One and f(n, 0) != S.Zero + assert f(m, 0) != S.One and f(m, 0) != S.Zero + assert f(k, 0) is S.Zero + + assert bessely(0, 0) is S.NegativeInfinity + assert besselk(0, 0) is S.Infinity + for f in [bessely, besselk]: + assert f(1 + I, 0) is S.ComplexInfinity + assert f(I, 0) is S.NaN + + for f in [besselj, bessely]: + assert f(m, S.Infinity) is S.Zero + assert f(m, S.NegativeInfinity) is S.Zero + + for f in [besseli, besselk]: + assert f(m, I*S.Infinity) is S.Zero + assert f(m, I*S.NegativeInfinity) is S.Zero + + for f in [besseli, besselk]: + assert f(-4, z) == f(4, z) + assert f(-3, z) == f(3, z) + assert f(-n, z) == f(n, z) + assert f(-m, z) != f(m, z) + + for f in [besselj, bessely]: + assert f(-4, z) == f(4, z) + assert f(-3, z) == -f(3, z) + assert f(-n, z) == (-1)**n*f(n, z) + assert f(-m, z) != (-1)**m*f(m, z) + + for f in [besselj, besseli]: + assert f(m, -z) == (-z)**m*z**(-m)*f(m, z) + + assert besseli(2, -z) == besseli(2, z) + assert besseli(3, -z) == -besseli(3, z) + + assert besselj(0, -z) == besselj(0, z) + assert besselj(1, -z) == -besselj(1, z) + + assert besseli(0, I*z) == besselj(0, z) + assert besseli(1, I*z) == I*besselj(1, z) + assert besselj(3, I*z) == -I*besseli(3, z) + + +def test_bessel_nan(): + # FIXME: could have these return NaN; for now just fix infinite recursion + for f in [besselj, bessely, besseli, besselk, hankel1, hankel2, yn, jn]: + assert f(1, S.NaN) == f(1, S.NaN, evaluate=False) + + +def test_meromorphic(): + assert besselj(2, x).is_meromorphic(x, 1) == True + assert besselj(2, x).is_meromorphic(x, 0) == True + assert besselj(2, x).is_meromorphic(x, oo) == False + assert besselj(S(2)/3, x).is_meromorphic(x, 1) == True + assert besselj(S(2)/3, x).is_meromorphic(x, 0) == False + assert besselj(S(2)/3, x).is_meromorphic(x, oo) == False + assert besselj(x, 2*x).is_meromorphic(x, 2) == False + assert besselk(0, x).is_meromorphic(x, 1) == True + assert besselk(2, x).is_meromorphic(x, 0) == True + assert besseli(0, x).is_meromorphic(x, 1) == True + assert besseli(2, x).is_meromorphic(x, 0) == True + assert bessely(0, x).is_meromorphic(x, 1) == True + assert bessely(0, x).is_meromorphic(x, 0) == False + assert bessely(2, x).is_meromorphic(x, 0) == True + assert hankel1(3, x**2 + 2*x).is_meromorphic(x, 1) == True + assert hankel1(0, x).is_meromorphic(x, 0) == False + assert hankel2(11, 4).is_meromorphic(x, 5) == True + assert hn1(6, 7*x**3 + 4).is_meromorphic(x, 7) == True + assert hn2(3, 2*x).is_meromorphic(x, 9) == True + assert jn(5, 2*x + 7).is_meromorphic(x, 4) == True + assert yn(8, x**2 + 11).is_meromorphic(x, 6) == True + + +def test_conjugate(): + n = Symbol('n') + z = Symbol('z', extended_real=False) + x = Symbol('x', extended_real=True) + y = Symbol('y', positive=True) + t = Symbol('t', negative=True) + + for f in [besseli, besselj, besselk, bessely, hankel1, hankel2]: + assert f(n, -1).conjugate() != f(conjugate(n), -1) + assert f(n, x).conjugate() != f(conjugate(n), x) + assert f(n, t).conjugate() != f(conjugate(n), t) + + rz = randcplx(b=0.5) + + for f in [besseli, besselj, besselk, bessely]: + assert f(n, 1 + I).conjugate() == f(conjugate(n), 1 - I) + assert f(n, 0).conjugate() == f(conjugate(n), 0) + assert f(n, 1).conjugate() == f(conjugate(n), 1) + assert f(n, z).conjugate() == f(conjugate(n), conjugate(z)) + assert f(n, y).conjugate() == f(conjugate(n), y) + assert tn(f(n, rz).conjugate(), f(conjugate(n), conjugate(rz))) + + assert hankel1(n, 1 + I).conjugate() == hankel2(conjugate(n), 1 - I) + assert hankel1(n, 0).conjugate() == hankel2(conjugate(n), 0) + assert hankel1(n, 1).conjugate() == hankel2(conjugate(n), 1) + assert hankel1(n, y).conjugate() == hankel2(conjugate(n), y) + assert hankel1(n, z).conjugate() == hankel2(conjugate(n), conjugate(z)) + assert tn(hankel1(n, rz).conjugate(), hankel2(conjugate(n), conjugate(rz))) + + assert hankel2(n, 1 + I).conjugate() == hankel1(conjugate(n), 1 - I) + assert hankel2(n, 0).conjugate() == hankel1(conjugate(n), 0) + assert hankel2(n, 1).conjugate() == hankel1(conjugate(n), 1) + assert hankel2(n, y).conjugate() == hankel1(conjugate(n), y) + assert hankel2(n, z).conjugate() == hankel1(conjugate(n), conjugate(z)) + assert tn(hankel2(n, rz).conjugate(), hankel1(conjugate(n), conjugate(rz))) + + +def test_branching(): + assert besselj(polar_lift(k), x) == besselj(k, x) + assert besseli(polar_lift(k), x) == besseli(k, x) + + n = Symbol('n', integer=True) + assert besselj(n, exp_polar(2*pi*I)*x) == besselj(n, x) + assert besselj(n, polar_lift(x)) == besselj(n, x) + assert besseli(n, exp_polar(2*pi*I)*x) == besseli(n, x) + assert besseli(n, polar_lift(x)) == besseli(n, x) + + def tn(func, s): + from sympy.core.random import uniform + c = uniform(1, 5) + expr = func(s, c*exp_polar(I*pi)) - func(s, c*exp_polar(-I*pi)) + eps = 1e-15 + expr2 = func(s + eps, -c + eps*I) - func(s + eps, -c - eps*I) + return abs(expr.n() - expr2.n()).n() < 1e-10 + + nu = Symbol('nu') + assert besselj(nu, exp_polar(2*pi*I)*x) == exp(2*pi*I*nu)*besselj(nu, x) + assert besseli(nu, exp_polar(2*pi*I)*x) == exp(2*pi*I*nu)*besseli(nu, x) + assert tn(besselj, 2) + assert tn(besselj, pi) + assert tn(besselj, I) + assert tn(besseli, 2) + assert tn(besseli, pi) + assert tn(besseli, I) + + +def test_airy_base(): + z = Symbol('z') + x = Symbol('x', real=True) + y = Symbol('y', real=True) + + assert conjugate(airyai(z)) == airyai(conjugate(z)) + assert airyai(x).is_extended_real + + assert airyai(x+I*y).as_real_imag() == ( + airyai(x - I*y)/2 + airyai(x + I*y)/2, + I*(airyai(x - I*y) - airyai(x + I*y))/2) + + +def test_airyai(): + z = Symbol('z', real=False) + t = Symbol('t', negative=True) + p = Symbol('p', positive=True) + + assert isinstance(airyai(z), airyai) + + assert airyai(0) == 3**Rational(1, 3)/(3*gamma(Rational(2, 3))) + assert airyai(oo) == 0 + assert airyai(-oo) == 0 + + assert diff(airyai(z), z) == airyaiprime(z) + + assert series(airyai(z), z, 0, 3) == ( + 3**Rational(5, 6)*gamma(Rational(1, 3))/(6*pi) - 3**Rational(1, 6)*z*gamma(Rational(2, 3))/(2*pi) + O(z**3)) + + assert airyai(z).rewrite(hyper) == ( + -3**Rational(2, 3)*z*hyper((), (Rational(4, 3),), z**3/9)/(3*gamma(Rational(1, 3))) + + 3**Rational(1, 3)*hyper((), (Rational(2, 3),), z**3/9)/(3*gamma(Rational(2, 3)))) + + assert isinstance(airyai(z).rewrite(besselj), airyai) + assert airyai(t).rewrite(besselj) == ( + sqrt(-t)*(besselj(Rational(-1, 3), 2*(-t)**Rational(3, 2)/3) + + besselj(Rational(1, 3), 2*(-t)**Rational(3, 2)/3))/3) + assert airyai(z).rewrite(besseli) == ( + -z*besseli(Rational(1, 3), 2*z**Rational(3, 2)/3)/(3*(z**Rational(3, 2))**Rational(1, 3)) + + (z**Rational(3, 2))**Rational(1, 3)*besseli(Rational(-1, 3), 2*z**Rational(3, 2)/3)/3) + assert airyai(p).rewrite(besseli) == ( + sqrt(p)*(besseli(Rational(-1, 3), 2*p**Rational(3, 2)/3) - + besseli(Rational(1, 3), 2*p**Rational(3, 2)/3))/3) + + assert expand_func(airyai(2*(3*z**5)**Rational(1, 3))) == ( + -sqrt(3)*(-1 + (z**5)**Rational(1, 3)/z**Rational(5, 3))*airybi(2*3**Rational(1, 3)*z**Rational(5, 3))/6 + + (1 + (z**5)**Rational(1, 3)/z**Rational(5, 3))*airyai(2*3**Rational(1, 3)*z**Rational(5, 3))/2) + + +def test_airybi(): + z = Symbol('z', real=False) + t = Symbol('t', negative=True) + p = Symbol('p', positive=True) + + assert isinstance(airybi(z), airybi) + + assert airybi(0) == 3**Rational(5, 6)/(3*gamma(Rational(2, 3))) + assert airybi(oo) is oo + assert airybi(-oo) == 0 + + assert diff(airybi(z), z) == airybiprime(z) + + assert series(airybi(z), z, 0, 3) == ( + 3**Rational(1, 3)*gamma(Rational(1, 3))/(2*pi) + 3**Rational(2, 3)*z*gamma(Rational(2, 3))/(2*pi) + O(z**3)) + + assert airybi(z).rewrite(hyper) == ( + 3**Rational(1, 6)*z*hyper((), (Rational(4, 3),), z**3/9)/gamma(Rational(1, 3)) + + 3**Rational(5, 6)*hyper((), (Rational(2, 3),), z**3/9)/(3*gamma(Rational(2, 3)))) + + assert isinstance(airybi(z).rewrite(besselj), airybi) + assert airyai(t).rewrite(besselj) == ( + sqrt(-t)*(besselj(Rational(-1, 3), 2*(-t)**Rational(3, 2)/3) + + besselj(Rational(1, 3), 2*(-t)**Rational(3, 2)/3))/3) + assert airybi(z).rewrite(besseli) == ( + sqrt(3)*(z*besseli(Rational(1, 3), 2*z**Rational(3, 2)/3)/(z**Rational(3, 2))**Rational(1, 3) + + (z**Rational(3, 2))**Rational(1, 3)*besseli(Rational(-1, 3), 2*z**Rational(3, 2)/3))/3) + assert airybi(p).rewrite(besseli) == ( + sqrt(3)*sqrt(p)*(besseli(Rational(-1, 3), 2*p**Rational(3, 2)/3) + + besseli(Rational(1, 3), 2*p**Rational(3, 2)/3))/3) + + assert expand_func(airybi(2*(3*z**5)**Rational(1, 3))) == ( + sqrt(3)*(1 - (z**5)**Rational(1, 3)/z**Rational(5, 3))*airyai(2*3**Rational(1, 3)*z**Rational(5, 3))/2 + + (1 + (z**5)**Rational(1, 3)/z**Rational(5, 3))*airybi(2*3**Rational(1, 3)*z**Rational(5, 3))/2) + + +def test_airyaiprime(): + z = Symbol('z', real=False) + t = Symbol('t', negative=True) + p = Symbol('p', positive=True) + + assert isinstance(airyaiprime(z), airyaiprime) + + assert airyaiprime(0) == -3**Rational(2, 3)/(3*gamma(Rational(1, 3))) + assert airyaiprime(oo) == 0 + + assert diff(airyaiprime(z), z) == z*airyai(z) + + assert series(airyaiprime(z), z, 0, 3) == ( + -3**Rational(2, 3)/(3*gamma(Rational(1, 3))) + 3**Rational(1, 3)*z**2/(6*gamma(Rational(2, 3))) + O(z**3)) + + assert airyaiprime(z).rewrite(hyper) == ( + 3**Rational(1, 3)*z**2*hyper((), (Rational(5, 3),), z**3/9)/(6*gamma(Rational(2, 3))) - + 3**Rational(2, 3)*hyper((), (Rational(1, 3),), z**3/9)/(3*gamma(Rational(1, 3)))) + + assert isinstance(airyaiprime(z).rewrite(besselj), airyaiprime) + assert airyai(t).rewrite(besselj) == ( + sqrt(-t)*(besselj(Rational(-1, 3), 2*(-t)**Rational(3, 2)/3) + + besselj(Rational(1, 3), 2*(-t)**Rational(3, 2)/3))/3) + assert airyaiprime(z).rewrite(besseli) == ( + z**2*besseli(Rational(2, 3), 2*z**Rational(3, 2)/3)/(3*(z**Rational(3, 2))**Rational(2, 3)) - + (z**Rational(3, 2))**Rational(2, 3)*besseli(Rational(-1, 3), 2*z**Rational(3, 2)/3)/3) + assert airyaiprime(p).rewrite(besseli) == ( + p*(-besseli(Rational(-2, 3), 2*p**Rational(3, 2)/3) + besseli(Rational(2, 3), 2*p**Rational(3, 2)/3))/3) + + assert expand_func(airyaiprime(2*(3*z**5)**Rational(1, 3))) == ( + sqrt(3)*(z**Rational(5, 3)/(z**5)**Rational(1, 3) - 1)*airybiprime(2*3**Rational(1, 3)*z**Rational(5, 3))/6 + + (z**Rational(5, 3)/(z**5)**Rational(1, 3) + 1)*airyaiprime(2*3**Rational(1, 3)*z**Rational(5, 3))/2) + + +def test_airybiprime(): + z = Symbol('z', real=False) + t = Symbol('t', negative=True) + p = Symbol('p', positive=True) + + assert isinstance(airybiprime(z), airybiprime) + + assert airybiprime(0) == 3**Rational(1, 6)/gamma(Rational(1, 3)) + assert airybiprime(oo) is oo + assert airybiprime(-oo) == 0 + + assert diff(airybiprime(z), z) == z*airybi(z) + + assert series(airybiprime(z), z, 0, 3) == ( + 3**Rational(1, 6)/gamma(Rational(1, 3)) + 3**Rational(5, 6)*z**2/(6*gamma(Rational(2, 3))) + O(z**3)) + + assert airybiprime(z).rewrite(hyper) == ( + 3**Rational(5, 6)*z**2*hyper((), (Rational(5, 3),), z**3/9)/(6*gamma(Rational(2, 3))) + + 3**Rational(1, 6)*hyper((), (Rational(1, 3),), z**3/9)/gamma(Rational(1, 3))) + + assert isinstance(airybiprime(z).rewrite(besselj), airybiprime) + assert airyai(t).rewrite(besselj) == ( + sqrt(-t)*(besselj(Rational(-1, 3), 2*(-t)**Rational(3, 2)/3) + + besselj(Rational(1, 3), 2*(-t)**Rational(3, 2)/3))/3) + assert airybiprime(z).rewrite(besseli) == ( + sqrt(3)*(z**2*besseli(Rational(2, 3), 2*z**Rational(3, 2)/3)/(z**Rational(3, 2))**Rational(2, 3) + + (z**Rational(3, 2))**Rational(2, 3)*besseli(Rational(-2, 3), 2*z**Rational(3, 2)/3))/3) + assert airybiprime(p).rewrite(besseli) == ( + sqrt(3)*p*(besseli(Rational(-2, 3), 2*p**Rational(3, 2)/3) + besseli(Rational(2, 3), 2*p**Rational(3, 2)/3))/3) + + assert expand_func(airybiprime(2*(3*z**5)**Rational(1, 3))) == ( + sqrt(3)*(z**Rational(5, 3)/(z**5)**Rational(1, 3) - 1)*airyaiprime(2*3**Rational(1, 3)*z**Rational(5, 3))/2 + + (z**Rational(5, 3)/(z**5)**Rational(1, 3) + 1)*airybiprime(2*3**Rational(1, 3)*z**Rational(5, 3))/2) + + +def test_marcumq(): + m = Symbol('m') + a = Symbol('a') + b = Symbol('b') + + assert marcumq(0, 0, 0) == 0 + assert marcumq(m, 0, b) == uppergamma(m, b**2/2)/gamma(m) + assert marcumq(2, 0, 5) == 27*exp(Rational(-25, 2))/2 + assert marcumq(0, a, 0) == 1 - exp(-a**2/2) + assert marcumq(0, pi, 0) == 1 - exp(-pi**2/2) + assert marcumq(1, a, a) == S.Half + exp(-a**2)*besseli(0, a**2)/2 + assert marcumq(2, a, a) == S.Half + exp(-a**2)*besseli(0, a**2)/2 + exp(-a**2)*besseli(1, a**2) + + assert diff(marcumq(1, a, 3), a) == a*(-marcumq(1, a, 3) + marcumq(2, a, 3)) + assert diff(marcumq(2, 3, b), b) == -b**2*exp(-b**2/2 - Rational(9, 2))*besseli(1, 3*b)/3 + + x = Symbol('x') + assert marcumq(2, 3, 4).rewrite(Integral, x=x) == \ + Integral(x**2*exp(-x**2/2 - Rational(9, 2))*besseli(1, 3*x), (x, 4, oo))/3 + assert eq([marcumq(5, -2, 3).rewrite(Integral).evalf(10)], + [0.7905769565]) + + k = Symbol('k') + assert marcumq(-3, -5, -7).rewrite(Sum, k=k) == \ + exp(-37)*Sum((Rational(5, 7))**k*besseli(k, 35), (k, 4, oo)) + assert eq([marcumq(1, 3, 1).rewrite(Sum).evalf(10)], + [0.9891705502]) + + assert marcumq(1, a, a, evaluate=False).rewrite(besseli) == S.Half + exp(-a**2)*besseli(0, a**2)/2 + assert marcumq(2, a, a, evaluate=False).rewrite(besseli) == S.Half + exp(-a**2)*besseli(0, a**2)/2 + \ + exp(-a**2)*besseli(1, a**2) + assert marcumq(3, a, a).rewrite(besseli) == (besseli(1, a**2) + besseli(2, a**2))*exp(-a**2) + \ + S.Half + exp(-a**2)*besseli(0, a**2)/2 + assert marcumq(5, 8, 8).rewrite(besseli) == exp(-64)*besseli(0, 64)/2 + \ + (besseli(4, 64) + besseli(3, 64) + besseli(2, 64) + besseli(1, 64))*exp(-64) + S.Half + assert marcumq(m, a, a).rewrite(besseli) == marcumq(m, a, a) + + x = Symbol('x', integer=True) + assert marcumq(x, a, a).rewrite(besseli) == marcumq(x, a, a) + + +def test_issue_26134(): + x = Symbol('x') + assert marcumq(2, 3, 4).rewrite(Integral, x=x).dummy_eq( + Integral(x**2*exp(-x**2/2 - Rational(9, 2))*besseli(1, 3*x), (x, 4, oo))/3) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_beta_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_beta_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..b34cb2febf9e2746d869cd878525d2794535aea5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_beta_functions.py @@ -0,0 +1,89 @@ +from sympy.core.function import (diff, expand_func) +from sympy.core.numbers import I, Rational, pi +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, symbols) +from sympy.functions.combinatorial.numbers import catalan +from sympy.functions.elementary.complexes import conjugate +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.special.beta_functions import (beta, betainc, betainc_regularized) +from sympy.functions.special.gamma_functions import gamma, polygamma +from sympy.functions.special.hyper import hyper +from sympy.integrals.integrals import Integral +from sympy.core.function import ArgumentIndexError +from sympy.core.expr import unchanged +from sympy.testing.pytest import raises + + +def test_beta(): + x, y = symbols('x y') + t = Dummy('t') + + assert unchanged(beta, x, y) + assert unchanged(beta, x, x) + + assert beta(5, -3).is_real == True + assert beta(3, y).is_real is None + + assert expand_func(beta(x, y)) == gamma(x)*gamma(y)/gamma(x + y) + assert expand_func(beta(x, y) - beta(y, x)) == 0 # Symmetric + assert expand_func(beta(x, y)) == expand_func(beta(x, y + 1) + beta(x + 1, y)).simplify() + + assert diff(beta(x, y), x) == beta(x, y)*(polygamma(0, x) - polygamma(0, x + y)) + assert diff(beta(x, y), y) == beta(x, y)*(polygamma(0, y) - polygamma(0, x + y)) + + assert conjugate(beta(x, y)) == beta(conjugate(x), conjugate(y)) + + raises(ArgumentIndexError, lambda: beta(x, y).fdiff(3)) + + assert beta(x, y).rewrite(gamma) == gamma(x)*gamma(y)/gamma(x + y) + assert beta(x).rewrite(gamma) == gamma(x)**2/gamma(2*x) + assert beta(x, y).rewrite(Integral).dummy_eq(Integral(t**(x - 1) * (1 - t)**(y - 1), (t, 0, 1))) + assert beta(Rational(-19, 10), Rational(-1, 10)) == S.Zero + assert beta(Rational(-19, 10), Rational(-9, 10)) == \ + 800*2**(S(4)/5)*sqrt(pi)*gamma(S.One/10)/(171*gamma(-S(7)/5)) + assert beta(Rational(19, 10), Rational(29, 10)) == 100/(551*catalan(Rational(19, 10))) + assert beta(1, 0) == S.ComplexInfinity + assert beta(0, 1) == S.ComplexInfinity + assert beta(2, 3) == S.One/12 + assert unchanged(beta, x, x + 1) + assert unchanged(beta, x, 1) + assert unchanged(beta, 1, y) + assert beta(x, x + 1).doit() == 1/(x*(x+1)*catalan(x)) + assert beta(1, y).doit() == 1/y + assert beta(x, 1).doit() == 1/x + assert beta(Rational(-19, 10), Rational(-1, 10), evaluate=False).doit() == S.Zero + assert beta(2) == beta(2, 2) + assert beta(x, evaluate=False) != beta(x, x) + assert beta(x, evaluate=False).doit() == beta(x, x) + + +def test_betainc(): + a, b, x1, x2 = symbols('a b x1 x2') + + assert unchanged(betainc, a, b, x1, x2) + assert unchanged(betainc, a, b, 0, x1) + + assert betainc(1, 2, 0, -5).is_real == True + assert betainc(1, 2, 0, x2).is_real is None + assert conjugate(betainc(I, 2, 3 - I, 1 + 4*I)) == betainc(-I, 2, 3 + I, 1 - 4*I) + + assert betainc(a, b, 0, 1).rewrite(Integral).dummy_eq(beta(a, b).rewrite(Integral)) + assert betainc(1, 2, 0, x2).rewrite(hyper) == x2*hyper((1, -1), (2,), x2) + + assert betainc(1, 2, 3, 3).evalf() == 0 + + +def test_betainc_regularized(): + a, b, x1, x2 = symbols('a b x1 x2') + + assert unchanged(betainc_regularized, a, b, x1, x2) + assert unchanged(betainc_regularized, a, b, 0, x1) + + assert betainc_regularized(3, 5, 0, -1).is_real == True + assert betainc_regularized(3, 5, 0, x2).is_real is None + assert conjugate(betainc_regularized(3*I, 1, 2 + I, 1 + 2*I)) == betainc_regularized(-3*I, 1, 2 - I, 1 - 2*I) + + assert betainc_regularized(a, b, 0, 1).rewrite(Integral) == 1 + assert betainc_regularized(1, 2, x1, x2).rewrite(hyper) == 2*x2*hyper((1, -1), (2,), x2) - 2*x1*hyper((1, -1), (2,), x1) + + assert betainc_regularized(4, 1, 5, 5).evalf() == 0 diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_bsplines.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_bsplines.py new file mode 100644 index 0000000000000000000000000000000000000000..136831b96ba16c95edba12ecd47b6f1566b68427 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_bsplines.py @@ -0,0 +1,167 @@ +from sympy.functions import bspline_basis_set, interpolating_spline +from sympy.core.numbers import Rational +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.piecewise import Piecewise +from sympy.logic.boolalg import And +from sympy.sets.sets import Interval +from sympy.testing.pytest import slow + +x, y = symbols('x,y') + + +def test_basic_degree_0(): + d = 0 + knots = range(5) + splines = bspline_basis_set(d, knots, x) + for i in range(len(splines)): + assert splines[i] == Piecewise((1, Interval(i, i + 1).contains(x)), + (0, True)) + + +def test_basic_degree_1(): + d = 1 + knots = range(5) + splines = bspline_basis_set(d, knots, x) + assert splines[0] == Piecewise((x, Interval(0, 1).contains(x)), + (2 - x, Interval(1, 2).contains(x)), + (0, True)) + assert splines[1] == Piecewise((-1 + x, Interval(1, 2).contains(x)), + (3 - x, Interval(2, 3).contains(x)), + (0, True)) + assert splines[2] == Piecewise((-2 + x, Interval(2, 3).contains(x)), + (4 - x, Interval(3, 4).contains(x)), + (0, True)) + + +def test_basic_degree_2(): + d = 2 + knots = range(5) + splines = bspline_basis_set(d, knots, x) + b0 = Piecewise((x**2/2, Interval(0, 1).contains(x)), + (Rational(-3, 2) + 3*x - x**2, Interval(1, 2).contains(x)), + (Rational(9, 2) - 3*x + x**2/2, Interval(2, 3).contains(x)), + (0, True)) + b1 = Piecewise((S.Half - x + x**2/2, Interval(1, 2).contains(x)), + (Rational(-11, 2) + 5*x - x**2, Interval(2, 3).contains(x)), + (8 - 4*x + x**2/2, Interval(3, 4).contains(x)), + (0, True)) + assert splines[0] == b0 + assert splines[1] == b1 + + +def test_basic_degree_3(): + d = 3 + knots = range(5) + splines = bspline_basis_set(d, knots, x) + b0 = Piecewise( + (x**3/6, Interval(0, 1).contains(x)), + (Rational(2, 3) - 2*x + 2*x**2 - x**3/2, Interval(1, 2).contains(x)), + (Rational(-22, 3) + 10*x - 4*x**2 + x**3/2, Interval(2, 3).contains(x)), + (Rational(32, 3) - 8*x + 2*x**2 - x**3/6, Interval(3, 4).contains(x)), + (0, True) + ) + assert splines[0] == b0 + + +def test_repeated_degree_1(): + d = 1 + knots = [0, 0, 1, 2, 2, 3, 4, 4] + splines = bspline_basis_set(d, knots, x) + assert splines[0] == Piecewise((1 - x, Interval(0, 1).contains(x)), + (0, True)) + assert splines[1] == Piecewise((x, Interval(0, 1).contains(x)), + (2 - x, Interval(1, 2).contains(x)), + (0, True)) + assert splines[2] == Piecewise((-1 + x, Interval(1, 2).contains(x)), + (0, True)) + assert splines[3] == Piecewise((3 - x, Interval(2, 3).contains(x)), + (0, True)) + assert splines[4] == Piecewise((-2 + x, Interval(2, 3).contains(x)), + (4 - x, Interval(3, 4).contains(x)), + (0, True)) + assert splines[5] == Piecewise((-3 + x, Interval(3, 4).contains(x)), + (0, True)) + + +def test_repeated_degree_2(): + d = 2 + knots = [0, 0, 1, 2, 2, 3, 4, 4] + splines = bspline_basis_set(d, knots, x) + + assert splines[0] == Piecewise(((-3*x**2/2 + 2*x), And(x <= 1, x >= 0)), + (x**2/2 - 2*x + 2, And(x <= 2, x >= 1)), + (0, True)) + assert splines[1] == Piecewise((x**2/2, And(x <= 1, x >= 0)), + (-3*x**2/2 + 4*x - 2, And(x <= 2, x >= 1)), + (0, True)) + assert splines[2] == Piecewise((x**2 - 2*x + 1, And(x <= 2, x >= 1)), + (x**2 - 6*x + 9, And(x <= 3, x >= 2)), + (0, True)) + assert splines[3] == Piecewise((-3*x**2/2 + 8*x - 10, And(x <= 3, x >= 2)), + (x**2/2 - 4*x + 8, And(x <= 4, x >= 3)), + (0, True)) + assert splines[4] == Piecewise((x**2/2 - 2*x + 2, And(x <= 3, x >= 2)), + (-3*x**2/2 + 10*x - 16, And(x <= 4, x >= 3)), + (0, True)) + +# Tests for interpolating_spline + + +def test_10_points_degree_1(): + d = 1 + X = [-5, 2, 3, 4, 7, 9, 10, 30, 31, 34] + Y = [-10, -2, 2, 4, 7, 6, 20, 45, 19, 25] + spline = interpolating_spline(d, x, X, Y) + + assert spline == Piecewise((x*Rational(8, 7) - Rational(30, 7), (x >= -5) & (x <= 2)), (4*x - 10, (x >= 2) & (x <= 3)), + (2*x - 4, (x >= 3) & (x <= 4)), (x, (x >= 4) & (x <= 7)), + (-x/2 + Rational(21, 2), (x >= 7) & (x <= 9)), (14*x - 120, (x >= 9) & (x <= 10)), + (x*Rational(5, 4) + Rational(15, 2), (x >= 10) & (x <= 30)), (-26*x + 825, (x >= 30) & (x <= 31)), + (2*x - 43, (x >= 31) & (x <= 34))) + + +def test_3_points_degree_2(): + d = 2 + X = [-3, 10, 19] + Y = [3, -4, 30] + spline = interpolating_spline(d, x, X, Y) + + assert spline == Piecewise((505*x**2/2574 - x*Rational(4921, 2574) - Rational(1931, 429), (x >= -3) & (x <= 19))) + + +def test_5_points_degree_2(): + d = 2 + X = [-3, 2, 4, 5, 10] + Y = [-1, 2, 5, 10, 14] + spline = interpolating_spline(d, x, X, Y) + + assert spline == Piecewise((4*x**2/329 + x*Rational(1007, 1645) + Rational(1196, 1645), (x >= -3) & (x <= 3)), + (2701*x**2/1645 - x*Rational(15079, 1645) + Rational(5065, 329), (x >= 3) & (x <= Rational(9, 2))), + (-1319*x**2/1645 + x*Rational(21101, 1645) - Rational(11216, 329), (x >= Rational(9, 2)) & (x <= 10))) + + +@slow +def test_6_points_degree_3(): + d = 3 + X = [-1, 0, 2, 3, 9, 12] + Y = [-4, 3, 3, 7, 9, 20] + spline = interpolating_spline(d, x, X, Y) + + assert spline == Piecewise((6058*x**3/5301 - 18427*x**2/5301 + x*Rational(12622, 5301) + 3, (x >= -1) & (x <= 2)), + (-8327*x**3/5301 + 67883*x**2/5301 - x*Rational(159998, 5301) + Rational(43661, 1767), (x >= 2) & (x <= 3)), + (5414*x**3/47709 - 1386*x**2/589 + x*Rational(4267, 279) - Rational(12232, 589), (x >= 3) & (x <= 12))) + + +def test_issue_19262(): + Delta = symbols('Delta', positive=True) + knots = [i*Delta for i in range(4)] + basis = bspline_basis_set(1, knots, x) + y = symbols('y', nonnegative=True) + basis2 = bspline_basis_set(1, knots, y) + assert basis[0].subs(x, y) == basis2[0] + assert interpolating_spline(1, x, + [Delta*i for i in [1, 2, 4, 7]], [3, 6, 5, 7] + ) == Piecewise((3*x/Delta, (Delta <= x) & (x <= 2*Delta)), + (7 - x/(2*Delta), (x >= 2*Delta) & (x <= 4*Delta)), + (Rational(7, 3) + 2*x/(3*Delta), (x >= 4*Delta) & (x <= 7*Delta))) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_delta_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_delta_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..d5a39d9e352143cf878cf69fa42f454f58be65c9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_delta_functions.py @@ -0,0 +1,165 @@ +from sympy.core.numbers import (I, nan, oo, pi) +from sympy.core.relational import (Eq, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import (adjoint, conjugate, sign, transpose) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.delta_functions import (DiracDelta, Heaviside) +from sympy.functions.special.singularity_functions import SingularityFunction +from sympy.simplify.simplify import signsimp + + +from sympy.testing.pytest import raises + +from sympy.core.expr import unchanged + +from sympy.core.function import ArgumentIndexError + + +x, y = symbols('x y') +i = symbols('t', nonzero=True) +j = symbols('j', positive=True) +k = symbols('k', negative=True) + +def test_DiracDelta(): + assert DiracDelta(1) == 0 + assert DiracDelta(5.1) == 0 + assert DiracDelta(-pi) == 0 + assert DiracDelta(5, 7) == 0 + assert DiracDelta(x, 0) == DiracDelta(x) + assert DiracDelta(i) == 0 + assert DiracDelta(j) == 0 + assert DiracDelta(k) == 0 + assert DiracDelta(nan) is nan + assert DiracDelta(0).func is DiracDelta + assert DiracDelta(x).func is DiracDelta + # FIXME: this is generally undefined @ x=0 + # But then limit(Delta(c)*Heaviside(x),x,-oo) + # need's to be implemented. + # assert 0*DiracDelta(x) == 0 + + assert adjoint(DiracDelta(x)) == DiracDelta(x) + assert adjoint(DiracDelta(x - y)) == DiracDelta(x - y) + assert conjugate(DiracDelta(x)) == DiracDelta(x) + assert conjugate(DiracDelta(x - y)) == DiracDelta(x - y) + assert transpose(DiracDelta(x)) == DiracDelta(x) + assert transpose(DiracDelta(x - y)) == DiracDelta(x - y) + + assert DiracDelta(x).diff(x) == DiracDelta(x, 1) + assert DiracDelta(x, 1).diff(x) == DiracDelta(x, 2) + + assert DiracDelta(x).is_simple(x) is True + assert DiracDelta(3*x).is_simple(x) is True + assert DiracDelta(x**2).is_simple(x) is False + assert DiracDelta(sqrt(x)).is_simple(x) is False + assert DiracDelta(x).is_simple(y) is False + + assert DiracDelta(x*y).expand(diracdelta=True, wrt=x) == DiracDelta(x)/abs(y) + assert DiracDelta(x*y).expand(diracdelta=True, wrt=y) == DiracDelta(y)/abs(x) + assert DiracDelta(x**2*y).expand(diracdelta=True, wrt=x) == DiracDelta(x**2*y) + assert DiracDelta(y).expand(diracdelta=True, wrt=x) == DiracDelta(y) + assert DiracDelta((x - 1)*(x - 2)*(x - 3)).expand(diracdelta=True, wrt=x) == ( + DiracDelta(x - 3)/2 + DiracDelta(x - 2) + DiracDelta(x - 1)/2) + + assert DiracDelta(2*x) != DiracDelta(x) # scaling property + assert DiracDelta(x) == DiracDelta(-x) # even function + assert DiracDelta(-x, 2) == DiracDelta(x, 2) + assert DiracDelta(-x, 1) == -DiracDelta(x, 1) # odd deriv is odd + assert DiracDelta(-oo*x) == DiracDelta(oo*x) + assert DiracDelta(x - y) != DiracDelta(y - x) + assert signsimp(DiracDelta(x - y) - DiracDelta(y - x)) == 0 + + assert DiracDelta(x*y).expand(diracdelta=True, wrt=x) == DiracDelta(x)/abs(y) + assert DiracDelta(x*y).expand(diracdelta=True, wrt=y) == DiracDelta(y)/abs(x) + assert DiracDelta(x**2*y).expand(diracdelta=True, wrt=x) == DiracDelta(x**2*y) + assert DiracDelta(y).expand(diracdelta=True, wrt=x) == DiracDelta(y) + assert DiracDelta((x - 1)*(x - 2)*(x - 3)).expand(diracdelta=True) == ( + DiracDelta(x - 3)/2 + DiracDelta(x - 2) + DiracDelta(x - 1)/2) + + raises(ArgumentIndexError, lambda: DiracDelta(x).fdiff(2)) + raises(ValueError, lambda: DiracDelta(x, -1)) + raises(ValueError, lambda: DiracDelta(I)) + raises(ValueError, lambda: DiracDelta(2 + 3*I)) + + +def test_heaviside(): + assert Heaviside(-5) == 0 + assert Heaviside(1) == 1 + assert Heaviside(0) == S.Half + + assert Heaviside(0, x) == x + assert unchanged(Heaviside,x, nan) + assert Heaviside(0, nan) == nan + + h0 = Heaviside(x, 0) + h12 = Heaviside(x, S.Half) + h1 = Heaviside(x, 1) + + assert h0.args == h0.pargs == (x, 0) + assert h1.args == h1.pargs == (x, 1) + assert h12.args == (x, S.Half) + assert h12.pargs == (x,) # default 1/2 suppressed + + assert adjoint(Heaviside(x)) == Heaviside(x) + assert adjoint(Heaviside(x - y)) == Heaviside(x - y) + assert conjugate(Heaviside(x)) == Heaviside(x) + assert conjugate(Heaviside(x - y)) == Heaviside(x - y) + assert transpose(Heaviside(x)) == Heaviside(x) + assert transpose(Heaviside(x - y)) == Heaviside(x - y) + + assert Heaviside(x).diff(x) == DiracDelta(x) + assert Heaviside(x + I).is_Function is True + assert Heaviside(I*x).is_Function is True + + raises(ArgumentIndexError, lambda: Heaviside(x).fdiff(2)) + raises(ValueError, lambda: Heaviside(I)) + raises(ValueError, lambda: Heaviside(2 + 3*I)) + + +def test_rewrite(): + x, y = Symbol('x', real=True), Symbol('y') + assert Heaviside(x).rewrite(Piecewise) == ( + Piecewise((0, x < 0), (Heaviside(0), Eq(x, 0)), (1, True))) + assert Heaviside(y).rewrite(Piecewise) == ( + Piecewise((0, y < 0), (Heaviside(0), Eq(y, 0)), (1, True))) + assert Heaviside(x, y).rewrite(Piecewise) == ( + Piecewise((0, x < 0), (y, Eq(x, 0)), (1, True))) + assert Heaviside(x, 0).rewrite(Piecewise) == ( + Piecewise((0, x <= 0), (1, True))) + assert Heaviside(x, 1).rewrite(Piecewise) == ( + Piecewise((0, x < 0), (1, True))) + assert Heaviside(x, nan).rewrite(Piecewise) == ( + Piecewise((0, x < 0), (nan, Eq(x, 0)), (1, True))) + + assert Heaviside(x).rewrite(sign) == \ + Heaviside(x, H0=Heaviside(0)).rewrite(sign) == \ + Piecewise( + (sign(x)/2 + S(1)/2, Eq(Heaviside(0), S(1)/2)), + (Piecewise( + (sign(x)/2 + S(1)/2, Ne(x, 0)), (Heaviside(0), True)), True) + ) + + assert Heaviside(y).rewrite(sign) == Heaviside(y) + assert Heaviside(x, S.Half).rewrite(sign) == (sign(x)+1)/2 + assert Heaviside(x, y).rewrite(sign) == \ + Piecewise( + (sign(x)/2 + S(1)/2, Eq(y, S(1)/2)), + (Piecewise( + (sign(x)/2 + S(1)/2, Ne(x, 0)), (y, True)), True) + ) + + assert DiracDelta(y).rewrite(Piecewise) == Piecewise((DiracDelta(0), Eq(y, 0)), (0, True)) + assert DiracDelta(y, 1).rewrite(Piecewise) == DiracDelta(y, 1) + assert DiracDelta(x - 5).rewrite(Piecewise) == ( + Piecewise((DiracDelta(0), Eq(x - 5, 0)), (0, True))) + + assert (x*DiracDelta(x - 10)).rewrite(SingularityFunction) == x*SingularityFunction(x, 10, -1) + assert 5*x*y*DiracDelta(y, 1).rewrite(SingularityFunction) == 5*x*y*SingularityFunction(y, 0, -2) + assert DiracDelta(0).rewrite(SingularityFunction) == SingularityFunction(0, 0, -1) + assert DiracDelta(0, 1).rewrite(SingularityFunction) == SingularityFunction(0, 0, -2) + + assert Heaviside(x).rewrite(SingularityFunction) == SingularityFunction(x, 0, 0) + assert 5*x*y*Heaviside(y + 1).rewrite(SingularityFunction) == 5*x*y*SingularityFunction(y, -1, 0) + assert ((x - 3)**3*Heaviside(x - 3)).rewrite(SingularityFunction) == (x - 3)**3*SingularityFunction(x, 3, 0) + assert Heaviside(0).rewrite(SingularityFunction) == S.Half diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_elliptic_integrals.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_elliptic_integrals.py new file mode 100644 index 0000000000000000000000000000000000000000..a11e531af32301a00b6fc864064d02f9318929e1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_elliptic_integrals.py @@ -0,0 +1,181 @@ +from sympy.core.numbers import (I, Rational, oo, pi, zoo) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol) +from sympy.functions.elementary.hyperbolic import atanh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (sin, tan) +from sympy.functions.special.gamma_functions import gamma +from sympy.functions.special.hyper import (hyper, meijerg) +from sympy.integrals.integrals import Integral +from sympy.series.order import O +from sympy.functions.special.elliptic_integrals import (elliptic_k as K, + elliptic_f as F, elliptic_e as E, elliptic_pi as P) +from sympy.core.random import (test_derivative_numerically as td, + random_complex_number as randcplx, + verify_numerically as tn) +from sympy.abc import z, m, n + +i = Symbol('i', integer=True) +j = Symbol('k', integer=True, positive=True) +t = Dummy('t') + +def test_K(): + assert K(0) == pi/2 + assert K(S.Half) == 8*pi**Rational(3, 2)/gamma(Rational(-1, 4))**2 + assert K(1) is zoo + assert K(-1) == gamma(Rational(1, 4))**2/(4*sqrt(2*pi)) + assert K(oo) == 0 + assert K(-oo) == 0 + assert K(I*oo) == 0 + assert K(-I*oo) == 0 + assert K(zoo) == 0 + + assert K(z).diff(z) == (E(z) - (1 - z)*K(z))/(2*z*(1 - z)) + assert td(K(z), z) + + zi = Symbol('z', real=False) + assert K(zi).conjugate() == K(zi.conjugate()) + zr = Symbol('z', negative=True) + assert K(zr).conjugate() == K(zr) + + assert K(z).rewrite(hyper) == \ + (pi/2)*hyper((S.Half, S.Half), (S.One,), z) + assert tn(K(z), (pi/2)*hyper((S.Half, S.Half), (S.One,), z)) + assert K(z).rewrite(meijerg) == \ + meijerg(((S.Half, S.Half), []), ((S.Zero,), (S.Zero,)), -z)/2 + assert tn(K(z), meijerg(((S.Half, S.Half), []), ((S.Zero,), (S.Zero,)), -z)/2) + + assert K(z).series(z) == pi/2 + pi*z/8 + 9*pi*z**2/128 + \ + 25*pi*z**3/512 + 1225*pi*z**4/32768 + 3969*pi*z**5/131072 + O(z**6) + + assert K(m).rewrite(Integral).dummy_eq( + Integral(1/sqrt(1 - m*sin(t)**2), (t, 0, pi/2))) + +def test_F(): + assert F(z, 0) == z + assert F(0, m) == 0 + assert F(pi*i/2, m) == i*K(m) + assert F(z, oo) == 0 + assert F(z, -oo) == 0 + + assert F(-z, m) == -F(z, m) + + assert F(z, m).diff(z) == 1/sqrt(1 - m*sin(z)**2) + assert F(z, m).diff(m) == E(z, m)/(2*m*(1 - m)) - F(z, m)/(2*m) - \ + sin(2*z)/(4*(1 - m)*sqrt(1 - m*sin(z)**2)) + r = randcplx() + assert td(F(z, r), z) + assert td(F(r, m), m) + + mi = Symbol('m', real=False) + assert F(z, mi).conjugate() == F(z.conjugate(), mi.conjugate()) + mr = Symbol('m', negative=True) + assert F(z, mr).conjugate() == F(z.conjugate(), mr) + + assert F(z, m).series(z) == \ + z + z**5*(3*m**2/40 - m/30) + m*z**3/6 + O(z**6) + + assert F(z, m).rewrite(Integral).dummy_eq( + Integral(1/sqrt(1 - m*sin(t)**2), (t, 0, z))) + +def test_E(): + assert E(z, 0) == z + assert E(0, m) == 0 + assert E(i*pi/2, m) == i*E(m) + assert E(z, oo) is zoo + assert E(z, -oo) is zoo + assert E(0) == pi/2 + assert E(1) == 1 + assert E(oo) == I*oo + assert E(-oo) is oo + assert E(zoo) is zoo + + assert E(-z, m) == -E(z, m) + + assert E(z, m).diff(z) == sqrt(1 - m*sin(z)**2) + assert E(z, m).diff(m) == (E(z, m) - F(z, m))/(2*m) + assert E(z).diff(z) == (E(z) - K(z))/(2*z) + r = randcplx() + assert td(E(r, m), m) + assert td(E(z, r), z) + assert td(E(z), z) + + mi = Symbol('m', real=False) + assert E(z, mi).conjugate() == E(z.conjugate(), mi.conjugate()) + assert E(mi).conjugate() == E(mi.conjugate()) + mr = Symbol('m', negative=True) + assert E(z, mr).conjugate() == E(z.conjugate(), mr) + assert E(mr).conjugate() == E(mr) + + assert E(z).rewrite(hyper) == (pi/2)*hyper((Rational(-1, 2), S.Half), (S.One,), z) + assert tn(E(z), (pi/2)*hyper((Rational(-1, 2), S.Half), (S.One,), z)) + assert E(z).rewrite(meijerg) == \ + -meijerg(((S.Half, Rational(3, 2)), []), ((S.Zero,), (S.Zero,)), -z)/4 + assert tn(E(z), -meijerg(((S.Half, Rational(3, 2)), []), ((S.Zero,), (S.Zero,)), -z)/4) + + assert E(z, m).series(z) == \ + z + z**5*(-m**2/40 + m/30) - m*z**3/6 + O(z**6) + assert E(z).series(z) == pi/2 - pi*z/8 - 3*pi*z**2/128 - \ + 5*pi*z**3/512 - 175*pi*z**4/32768 - 441*pi*z**5/131072 + O(z**6) + assert E(4*z/(z+1)).series(z) == \ + pi/2 - pi*z/2 + pi*z**2/8 - 3*pi*z**3/8 - 15*pi*z**4/128 - 93*pi*z**5/128 + O(z**6) + + assert E(z, m).rewrite(Integral).dummy_eq( + Integral(sqrt(1 - m*sin(t)**2), (t, 0, z))) + assert E(m).rewrite(Integral).dummy_eq( + Integral(sqrt(1 - m*sin(t)**2), (t, 0, pi/2))) + +def test_P(): + assert P(0, z, m) == F(z, m) + assert P(1, z, m) == F(z, m) + \ + (sqrt(1 - m*sin(z)**2)*tan(z) - E(z, m))/(1 - m) + assert P(n, i*pi/2, m) == i*P(n, m) + assert P(n, z, 0) == atanh(sqrt(n - 1)*tan(z))/sqrt(n - 1) + assert P(n, z, n) == F(z, n) - P(1, z, n) + tan(z)/sqrt(1 - n*sin(z)**2) + assert P(oo, z, m) == 0 + assert P(-oo, z, m) == 0 + assert P(n, z, oo) == 0 + assert P(n, z, -oo) == 0 + assert P(0, m) == K(m) + assert P(1, m) is zoo + assert P(n, 0) == pi/(2*sqrt(1 - n)) + assert P(2, 1) is -oo + assert P(-1, 1) is oo + assert P(n, n) == E(n)/(1 - n) + + assert P(n, -z, m) == -P(n, z, m) + + ni, mi = Symbol('n', real=False), Symbol('m', real=False) + assert P(ni, z, mi).conjugate() == \ + P(ni.conjugate(), z.conjugate(), mi.conjugate()) + nr, mr = Symbol('n', negative=True), \ + Symbol('m', negative=True) + assert P(nr, z, mr).conjugate() == P(nr, z.conjugate(), mr) + assert P(n, m).conjugate() == P(n.conjugate(), m.conjugate()) + + assert P(n, z, m).diff(n) == (E(z, m) + (m - n)*F(z, m)/n + + (n**2 - m)*P(n, z, m)/n - n*sqrt(1 - + m*sin(z)**2)*sin(2*z)/(2*(1 - n*sin(z)**2)))/(2*(m - n)*(n - 1)) + assert P(n, z, m).diff(z) == 1/(sqrt(1 - m*sin(z)**2)*(1 - n*sin(z)**2)) + assert P(n, z, m).diff(m) == (E(z, m)/(m - 1) + P(n, z, m) - + m*sin(2*z)/(2*(m - 1)*sqrt(1 - m*sin(z)**2)))/(2*(n - m)) + assert P(n, m).diff(n) == (E(m) + (m - n)*K(m)/n + + (n**2 - m)*P(n, m)/n)/(2*(m - n)*(n - 1)) + assert P(n, m).diff(m) == (E(m)/(m - 1) + P(n, m))/(2*(n - m)) + + # These tests fail due to + # https://github.com/fredrik-johansson/mpmath/issues/571#issuecomment-777201962 + # https://github.com/sympy/sympy/issues/20933#issuecomment-777080385 + # + # rx, ry = randcplx(), randcplx() + # assert td(P(n, rx, ry), n) + # assert td(P(rx, z, ry), z) + # assert td(P(rx, ry, m), m) + + assert P(n, z, m).series(z) == z + z**3*(m/6 + n/3) + \ + z**5*(3*m**2/40 + m*n/10 - m/30 + n**2/5 - n/15) + O(z**6) + + assert P(n, z, m).rewrite(Integral).dummy_eq( + Integral(1/((1 - n*sin(t)**2)*sqrt(1 - m*sin(t)**2)), (t, 0, z))) + assert P(n, m).rewrite(Integral).dummy_eq( + Integral(1/((1 - n*sin(t)**2)*sqrt(1 - m*sin(t)**2)), (t, 0, pi/2))) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_error_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_error_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..073371d3d584b97936729dc2e39c833ac347559b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_error_functions.py @@ -0,0 +1,860 @@ +from sympy.core.function import (diff, expand, expand_func) +from sympy.core import EulerGamma +from sympy.core.numbers import (E, Float, I, Rational, nan, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols, Dummy) +from sympy.functions.elementary.complexes import (conjugate, im, polar_lift, re) +from sympy.functions.elementary.exponential import (exp, exp_polar, log) +from sympy.functions.elementary.hyperbolic import (cosh, sinh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin, sinc) +from sympy.functions.special.error_functions import (Chi, Ci, E1, Ei, Li, Shi, Si, erf, erf2, erf2inv, erfc, erfcinv, erfi, erfinv, expint, fresnelc, fresnels, li) +from sympy.functions.special.gamma_functions import (gamma, uppergamma) +from sympy.functions.special.hyper import (hyper, meijerg) +from sympy.integrals.integrals import (Integral, integrate) +from sympy.series.gruntz import gruntz +from sympy.series.limits import limit +from sympy.series.order import O +from sympy.core.expr import unchanged +from sympy.core.function import ArgumentIndexError +from sympy.functions.special.error_functions import _erfs, _eis +from sympy.testing.pytest import raises + +x, y, z = symbols('x,y,z') +w = Symbol("w", real=True) +n = Symbol("n", integer=True) +t = Dummy('t') + + +def test_erf(): + assert erf(nan) is nan + + assert erf(oo) == 1 + assert erf(-oo) == -1 + + assert erf(0) is S.Zero + + assert erf(I*oo) == oo*I + assert erf(-I*oo) == -oo*I + + assert erf(-2) == -erf(2) + assert erf(-x*y) == -erf(x*y) + assert erf(-x - y) == -erf(x + y) + + assert erf(erfinv(x)) == x + assert erf(erfcinv(x)) == 1 - x + assert erf(erf2inv(0, x)) == x + assert erf(erf2inv(0, x, evaluate=False)) == x # To cover code in erf + assert erf(erf2inv(0, erf(erfcinv(1 - erf(erfinv(x)))))) == x + + alpha = symbols('alpha', extended_real=True) + assert erf(alpha).is_real is True + assert erf(alpha).is_finite is True + alpha = symbols('alpha', extended_real=False) + assert erf(alpha).is_real is None + assert erf(alpha).is_finite is None + assert erf(alpha).is_zero is None + assert erf(alpha).is_positive is None + assert erf(alpha).is_negative is None + alpha = symbols('alpha', extended_positive=True) + assert erf(alpha).is_positive is True + alpha = symbols('alpha', extended_negative=True) + assert erf(alpha).is_negative is True + assert erf(I).is_real is False + assert erf(0, evaluate=False).is_real + assert erf(0, evaluate=False).is_zero + + assert conjugate(erf(z)) == erf(conjugate(z)) + + assert erf(x).as_leading_term(x) == 2*x/sqrt(pi) + assert erf(x*y).as_leading_term(y) == 2*x*y/sqrt(pi) + assert (erf(x*y)/erf(y)).as_leading_term(y) == x + assert erf(1/x).as_leading_term(x) == S.One + + assert erf(z).rewrite('uppergamma') == sqrt(z**2)*(1 - erfc(sqrt(z**2)))/z + assert erf(z).rewrite('erfc') == S.One - erfc(z) + assert erf(z).rewrite('erfi') == -I*erfi(I*z) + assert erf(z).rewrite('fresnels') == (1 + I)*(fresnelc(z*(1 - I)/sqrt(pi)) - + I*fresnels(z*(1 - I)/sqrt(pi))) + assert erf(z).rewrite('fresnelc') == (1 + I)*(fresnelc(z*(1 - I)/sqrt(pi)) - + I*fresnels(z*(1 - I)/sqrt(pi))) + assert erf(z).rewrite('hyper') == 2*z*hyper([S.Half], [3*S.Half], -z**2)/sqrt(pi) + assert erf(z).rewrite('meijerg') == z*meijerg([S.Half], [], [0], [Rational(-1, 2)], z**2)/sqrt(pi) + assert erf(z).rewrite('expint') == sqrt(z**2)/z - z*expint(S.Half, z**2)/sqrt(S.Pi) + + assert limit(exp(x)*exp(x**2)*(erf(x + 1/exp(x)) - erf(x)), x, oo) == \ + 2/sqrt(pi) + assert limit((1 - erf(z))*exp(z**2)*z, z, oo) == 1/sqrt(pi) + assert limit((1 - erf(x))*exp(x**2)*sqrt(pi)*x, x, oo) == 1 + assert limit(((1 - erf(x))*exp(x**2)*sqrt(pi)*x - 1)*2*x**2, x, oo) == -1 + assert limit(erf(x)/x, x, 0) == 2/sqrt(pi) + assert limit(x**(-4) - sqrt(pi)*erf(x**2) / (2*x**6), x, 0) == S(1)/3 + + assert erf(x).as_real_imag() == \ + (erf(re(x) - I*im(x))/2 + erf(re(x) + I*im(x))/2, + -I*(-erf(re(x) - I*im(x)) + erf(re(x) + I*im(x)))/2) + + assert erf(x).as_real_imag(deep=False) == \ + (erf(re(x) - I*im(x))/2 + erf(re(x) + I*im(x))/2, + -I*(-erf(re(x) - I*im(x)) + erf(re(x) + I*im(x)))/2) + + assert erf(w).as_real_imag() == (erf(w), 0) + assert erf(w).as_real_imag(deep=False) == (erf(w), 0) + # issue 13575 + assert erf(I).as_real_imag() == (0, -I*erf(I)) + + raises(ArgumentIndexError, lambda: erf(x).fdiff(2)) + + assert erf(x).inverse() == erfinv + + +def test_erf_series(): + assert erf(x).series(x, 0, 7) == 2*x/sqrt(pi) - \ + 2*x**3/3/sqrt(pi) + x**5/5/sqrt(pi) + O(x**7) + + assert erf(x).series(x, oo) == \ + -exp(-x**2)*(3/(4*x**5) - 1/(2*x**3) + 1/x + O(x**(-6), (x, oo)))/sqrt(pi) + 1 + assert erf(x**2).series(x, oo, n=8) == \ + (-1/(2*x**6) + x**(-2) + O(x**(-8), (x, oo)))*exp(-x**4)/sqrt(pi)*-1 + 1 + assert erf(sqrt(x)).series(x, oo, n=3) == (sqrt(1/x) - (1/x)**(S(3)/2)/2\ + + 3*(1/x)**(S(5)/2)/4 + O(x**(-3), (x, oo)))*exp(-x)/sqrt(pi)*-1 + 1 + + +def test_erf_evalf(): + assert abs( erf(Float(2.0)) - 0.995322265 ) < 1E-8 # XXX + + +def test__erfs(): + assert _erfs(z).diff(z) == -2/sqrt(S.Pi) + 2*z*_erfs(z) + + assert _erfs(1/z).series(z) == \ + z/sqrt(pi) - z**3/(2*sqrt(pi)) + 3*z**5/(4*sqrt(pi)) + O(z**6) + + assert expand(erf(z).rewrite('tractable').diff(z).rewrite('intractable')) \ + == erf(z).diff(z) + assert _erfs(z).rewrite("intractable") == (-erf(z) + 1)*exp(z**2) + raises(ArgumentIndexError, lambda: _erfs(z).fdiff(2)) + + +def test_erfc(): + assert erfc(nan) is nan + + assert erfc(oo) is S.Zero + assert erfc(-oo) == 2 + + assert erfc(0) == 1 + + assert erfc(I*oo) == -oo*I + assert erfc(-I*oo) == oo*I + + assert erfc(-x) == S(2) - erfc(x) + assert erfc(erfcinv(x)) == x + + alpha = symbols('alpha', extended_real=True) + assert erfc(alpha).is_real is True + alpha = symbols('alpha', extended_real=False) + assert erfc(alpha).is_real is None + assert erfc(I).is_real is False + assert erfc(0, evaluate=False).is_real + assert erfc(0, evaluate=False).is_zero is False + + assert erfc(erfinv(x)) == 1 - x + + assert conjugate(erfc(z)) == erfc(conjugate(z)) + + assert erfc(x).as_leading_term(x) is S.One + assert erfc(1/x).as_leading_term(x) == S.Zero + + assert erfc(z).rewrite('erf') == 1 - erf(z) + assert erfc(z).rewrite('erfi') == 1 + I*erfi(I*z) + assert erfc(z).rewrite('fresnels') == 1 - (1 + I)*(fresnelc(z*(1 - I)/sqrt(pi)) - + I*fresnels(z*(1 - I)/sqrt(pi))) + assert erfc(z).rewrite('fresnelc') == 1 - (1 + I)*(fresnelc(z*(1 - I)/sqrt(pi)) - + I*fresnels(z*(1 - I)/sqrt(pi))) + assert erfc(z).rewrite('hyper') == 1 - 2*z*hyper([S.Half], [3*S.Half], -z**2)/sqrt(pi) + assert erfc(z).rewrite('meijerg') == 1 - z*meijerg([S.Half], [], [0], [Rational(-1, 2)], z**2)/sqrt(pi) + assert erfc(z).rewrite('uppergamma') == 1 - sqrt(z**2)*(1 - erfc(sqrt(z**2)))/z + assert erfc(z).rewrite('expint') == S.One - sqrt(z**2)/z + z*expint(S.Half, z**2)/sqrt(S.Pi) + assert erfc(z).rewrite('tractable') == _erfs(z)*exp(-z**2) + assert expand_func(erf(x) + erfc(x)) is S.One + + assert erfc(x).as_real_imag() == \ + (erfc(re(x) - I*im(x))/2 + erfc(re(x) + I*im(x))/2, + -I*(-erfc(re(x) - I*im(x)) + erfc(re(x) + I*im(x)))/2) + + assert erfc(x).as_real_imag(deep=False) == \ + (erfc(re(x) - I*im(x))/2 + erfc(re(x) + I*im(x))/2, + -I*(-erfc(re(x) - I*im(x)) + erfc(re(x) + I*im(x)))/2) + + assert erfc(w).as_real_imag() == (erfc(w), 0) + assert erfc(w).as_real_imag(deep=False) == (erfc(w), 0) + raises(ArgumentIndexError, lambda: erfc(x).fdiff(2)) + + assert erfc(x).inverse() == erfcinv + + +def test_erfc_series(): + assert erfc(x).series(x, 0, 7) == 1 - 2*x/sqrt(pi) + \ + 2*x**3/3/sqrt(pi) - x**5/5/sqrt(pi) + O(x**7) + + assert erfc(x).series(x, oo) == \ + (3/(4*x**5) - 1/(2*x**3) + 1/x + O(x**(-6), (x, oo)))*exp(-x**2)/sqrt(pi) + + +def test_erfc_evalf(): + assert abs( erfc(Float(2.0)) - 0.00467773 ) < 1E-8 # XXX + + +def test_erfi(): + assert erfi(nan) is nan + + assert erfi(oo) is S.Infinity + assert erfi(-oo) is S.NegativeInfinity + + assert erfi(0) is S.Zero + + assert erfi(I*oo) == I + assert erfi(-I*oo) == -I + + assert erfi(-x) == -erfi(x) + + assert erfi(I*erfinv(x)) == I*x + assert erfi(I*erfcinv(x)) == I*(1 - x) + assert erfi(I*erf2inv(0, x)) == I*x + assert erfi(I*erf2inv(0, x, evaluate=False)) == I*x # To cover code in erfi + + assert erfi(I).is_real is False + assert erfi(0, evaluate=False).is_real + assert erfi(0, evaluate=False).is_zero + + assert conjugate(erfi(z)) == erfi(conjugate(z)) + + assert erfi(x).as_leading_term(x) == 2*x/sqrt(pi) + assert erfi(x*y).as_leading_term(y) == 2*x*y/sqrt(pi) + assert (erfi(x*y)/erfi(y)).as_leading_term(y) == x + assert erfi(1/x).as_leading_term(x) == erfi(1/x) + + assert erfi(z).rewrite('erf') == -I*erf(I*z) + assert erfi(z).rewrite('erfc') == I*erfc(I*z) - I + assert erfi(z).rewrite('fresnels') == (1 - I)*(fresnelc(z*(1 + I)/sqrt(pi)) - + I*fresnels(z*(1 + I)/sqrt(pi))) + assert erfi(z).rewrite('fresnelc') == (1 - I)*(fresnelc(z*(1 + I)/sqrt(pi)) - + I*fresnels(z*(1 + I)/sqrt(pi))) + assert erfi(z).rewrite('hyper') == 2*z*hyper([S.Half], [3*S.Half], z**2)/sqrt(pi) + assert erfi(z).rewrite('meijerg') == z*meijerg([S.Half], [], [0], [Rational(-1, 2)], -z**2)/sqrt(pi) + assert erfi(z).rewrite('uppergamma') == (sqrt(-z**2)/z*(uppergamma(S.Half, + -z**2)/sqrt(S.Pi) - S.One)) + assert erfi(z).rewrite('expint') == sqrt(-z**2)/z - z*expint(S.Half, -z**2)/sqrt(S.Pi) + assert erfi(z).rewrite('tractable') == -I*(-_erfs(I*z)*exp(z**2) + 1) + assert expand_func(erfi(I*z)) == I*erf(z) + + assert erfi(x).as_real_imag() == \ + (erfi(re(x) - I*im(x))/2 + erfi(re(x) + I*im(x))/2, + -I*(-erfi(re(x) - I*im(x)) + erfi(re(x) + I*im(x)))/2) + assert erfi(x).as_real_imag(deep=False) == \ + (erfi(re(x) - I*im(x))/2 + erfi(re(x) + I*im(x))/2, + -I*(-erfi(re(x) - I*im(x)) + erfi(re(x) + I*im(x)))/2) + + assert erfi(w).as_real_imag() == (erfi(w), 0) + assert erfi(w).as_real_imag(deep=False) == (erfi(w), 0) + + raises(ArgumentIndexError, lambda: erfi(x).fdiff(2)) + + +def test_erfi_series(): + assert erfi(x).series(x, 0, 7) == 2*x/sqrt(pi) + \ + 2*x**3/3/sqrt(pi) + x**5/5/sqrt(pi) + O(x**7) + + assert erfi(x).series(x, oo) == \ + (3/(4*x**5) + 1/(2*x**3) + 1/x + O(x**(-6), (x, oo)))*exp(x**2)/sqrt(pi) - I + + +def test_erfi_evalf(): + assert abs( erfi(Float(2.0)) - 18.5648024145756 ) < 1E-13 # XXX + + +def test_erf2(): + + assert erf2(0, 0) is S.Zero + assert erf2(x, x) is S.Zero + assert erf2(nan, 0) is nan + + assert erf2(-oo, y) == erf(y) + 1 + assert erf2( oo, y) == erf(y) - 1 + assert erf2( x, oo) == 1 - erf(x) + assert erf2( x,-oo) == -1 - erf(x) + assert erf2(x, erf2inv(x, y)) == y + + assert erf2(-x, -y) == -erf2(x,y) + assert erf2(-x, y) == erf(y) + erf(x) + assert erf2( x, -y) == -erf(y) - erf(x) + assert erf2(x, y).rewrite('fresnels') == erf(y).rewrite(fresnels)-erf(x).rewrite(fresnels) + assert erf2(x, y).rewrite('fresnelc') == erf(y).rewrite(fresnelc)-erf(x).rewrite(fresnelc) + assert erf2(x, y).rewrite('hyper') == erf(y).rewrite(hyper)-erf(x).rewrite(hyper) + assert erf2(x, y).rewrite('meijerg') == erf(y).rewrite(meijerg)-erf(x).rewrite(meijerg) + assert erf2(x, y).rewrite('uppergamma') == erf(y).rewrite(uppergamma) - erf(x).rewrite(uppergamma) + assert erf2(x, y).rewrite('expint') == erf(y).rewrite(expint)-erf(x).rewrite(expint) + + assert erf2(I, 0).is_real is False + assert erf2(0, 0, evaluate=False).is_real + assert erf2(0, 0, evaluate=False).is_zero + assert erf2(x, x, evaluate=False).is_zero + assert erf2(x, y).is_zero is None + + assert expand_func(erf(x) + erf2(x, y)) == erf(y) + + assert conjugate(erf2(x, y)) == erf2(conjugate(x), conjugate(y)) + + assert erf2(x, y).rewrite('erf') == erf(y) - erf(x) + assert erf2(x, y).rewrite('erfc') == erfc(x) - erfc(y) + assert erf2(x, y).rewrite('erfi') == I*(erfi(I*x) - erfi(I*y)) + + assert erf2(x, y).diff(x) == erf2(x, y).fdiff(1) + assert erf2(x, y).diff(y) == erf2(x, y).fdiff(2) + assert erf2(x, y).diff(x) == -2*exp(-x**2)/sqrt(pi) + assert erf2(x, y).diff(y) == 2*exp(-y**2)/sqrt(pi) + raises(ArgumentIndexError, lambda: erf2(x, y).fdiff(3)) + + assert erf2(x, y).is_extended_real is None + xr, yr = symbols('xr yr', extended_real=True) + assert erf2(xr, yr).is_extended_real is True + + +def test_erfinv(): + assert erfinv(0) is S.Zero + assert erfinv(1) is S.Infinity + assert erfinv(nan) is S.NaN + assert erfinv(-1) is S.NegativeInfinity + + assert erfinv(erf(w)) == w + assert erfinv(erf(-w)) == -w + + assert erfinv(x).diff() == sqrt(pi)*exp(erfinv(x)**2)/2 + raises(ArgumentIndexError, lambda: erfinv(x).fdiff(2)) + + assert erfinv(z).rewrite('erfcinv') == erfcinv(1-z) + assert erfinv(z).inverse() == erf + + +def test_erfinv_evalf(): + assert abs( erfinv(Float(0.2)) - 0.179143454621292 ) < 1E-13 + + +def test_erfcinv(): + assert erfcinv(1) is S.Zero + assert erfcinv(0) is S.Infinity + assert erfcinv(0, evaluate=False).is_infinite is True + assert erfcinv(2, evaluate=False).is_infinite is True + assert erfcinv(nan) is S.NaN + + assert erfcinv(x).diff() == -sqrt(pi)*exp(erfcinv(x)**2)/2 + raises(ArgumentIndexError, lambda: erfcinv(x).fdiff(2)) + + assert erfcinv(z).rewrite('erfinv') == erfinv(1-z) + assert erfcinv(z).inverse() == erfc + + +def test_erf2inv(): + assert erf2inv(0, 0) is S.Zero + assert erf2inv(0, 1) is S.Infinity + assert erf2inv(1, 0) is S.One + assert erf2inv(0, y) == erfinv(y) + assert erf2inv(oo, y) == erfcinv(-y) + assert erf2inv(x, 0) == x + assert erf2inv(x, oo) == erfinv(x) + assert erf2inv(nan, 0) is nan + assert erf2inv(0, nan) is nan + + assert erf2inv(x, y).diff(x) == exp(-x**2 + erf2inv(x, y)**2) + assert erf2inv(x, y).diff(y) == sqrt(pi)*exp(erf2inv(x, y)**2)/2 + raises(ArgumentIndexError, lambda: erf2inv(x, y).fdiff(3)) + + +# NOTE we multiply by exp_polar(I*pi) and need this to be on the principal +# branch, hence take x in the lower half plane (d=0). + + +def mytn(expr1, expr2, expr3, x, d=0): + from sympy.core.random import verify_numerically, random_complex_number + subs = {} + for a in expr1.free_symbols: + if a != x: + subs[a] = random_complex_number() + return expr2 == expr3 and verify_numerically(expr1.subs(subs), + expr2.subs(subs), x, d=d) + + +def mytd(expr1, expr2, x): + from sympy.core.random import test_derivative_numerically, \ + random_complex_number + subs = {} + for a in expr1.free_symbols: + if a != x: + subs[a] = random_complex_number() + return expr1.diff(x) == expr2 and test_derivative_numerically(expr1.subs(subs), x) + + +def tn_branch(func, s=None): + from sympy.core.random import uniform + + def fn(x): + if s is None: + return func(x) + return func(s, x) + c = uniform(1, 5) + expr = fn(c*exp_polar(I*pi)) - fn(c*exp_polar(-I*pi)) + eps = 1e-15 + expr2 = fn(-c + eps*I) - fn(-c - eps*I) + return abs(expr.n() - expr2.n()).n() < 1e-10 + + +def test_ei(): + assert Ei(0) is S.NegativeInfinity + assert Ei(oo) is S.Infinity + assert Ei(-oo) is S.Zero + + assert tn_branch(Ei) + assert mytd(Ei(x), exp(x)/x, x) + assert mytn(Ei(x), Ei(x).rewrite(uppergamma), + -uppergamma(0, x*polar_lift(-1)) - I*pi, x) + assert mytn(Ei(x), Ei(x).rewrite(expint), + -expint(1, x*polar_lift(-1)) - I*pi, x) + assert Ei(x).rewrite(expint).rewrite(Ei) == Ei(x) + assert Ei(x*exp_polar(2*I*pi)) == Ei(x) + 2*I*pi + assert Ei(x*exp_polar(-2*I*pi)) == Ei(x) - 2*I*pi + + assert mytn(Ei(x), Ei(x).rewrite(Shi), Chi(x) + Shi(x), x) + assert mytn(Ei(x*polar_lift(I)), Ei(x*polar_lift(I)).rewrite(Si), + Ci(x) + I*Si(x) + I*pi/2, x) + + assert Ei(log(x)).rewrite(li) == li(x) + assert Ei(2*log(x)).rewrite(li) == li(x**2) + + assert gruntz(Ei(x+exp(-x))*exp(-x)*x, x, oo) == 1 + + assert Ei(x).series(x) == EulerGamma + log(x) + x + x**2/4 + \ + x**3/18 + x**4/96 + x**5/600 + O(x**6) + assert Ei(x).series(x, 1, 3) == Ei(1) + E*(x - 1) + O((x - 1)**3, (x, 1)) + assert Ei(x).series(x, oo) == \ + (120/x**5 + 24/x**4 + 6/x**3 + 2/x**2 + 1/x + 1 + O(x**(-6), (x, oo)))*exp(x)/x + assert Ei(x).series(x, -oo) == \ + (120/x**5 + 24/x**4 + 6/x**3 + 2/x**2 + 1/x + 1 + O(x**(-6), (x, -oo)))*exp(x)/x + assert Ei(-x).series(x, oo) == \ + -((-120/x**5 + 24/x**4 - 6/x**3 + 2/x**2 - 1/x + 1 + O(x**(-6), (x, oo)))*exp(-x)/x) + + assert str(Ei(cos(2)).evalf(n=10)) == '-0.6760647401' + raises(ArgumentIndexError, lambda: Ei(x).fdiff(2)) + + +def test_expint(): + assert mytn(expint(x, y), expint(x, y).rewrite(uppergamma), + y**(x - 1)*uppergamma(1 - x, y), x) + assert mytd( + expint(x, y), -y**(x - 1)*meijerg([], [1, 1], [0, 0, 1 - x], [], y), x) + assert mytd(expint(x, y), -expint(x - 1, y), y) + assert mytn(expint(1, x), expint(1, x).rewrite(Ei), + -Ei(x*polar_lift(-1)) + I*pi, x) + + assert expint(-4, x) == exp(-x)/x + 4*exp(-x)/x**2 + 12*exp(-x)/x**3 \ + + 24*exp(-x)/x**4 + 24*exp(-x)/x**5 + assert expint(Rational(-3, 2), x) == \ + exp(-x)/x + 3*exp(-x)/(2*x**2) + 3*sqrt(pi)*erfc(sqrt(x))/(4*x**S('5/2')) + + assert tn_branch(expint, 1) + assert tn_branch(expint, 2) + assert tn_branch(expint, 3) + assert tn_branch(expint, 1.7) + assert tn_branch(expint, pi) + + assert expint(y, x*exp_polar(2*I*pi)) == \ + x**(y - 1)*(exp(2*I*pi*y) - 1)*gamma(-y + 1) + expint(y, x) + assert expint(y, x*exp_polar(-2*I*pi)) == \ + x**(y - 1)*(exp(-2*I*pi*y) - 1)*gamma(-y + 1) + expint(y, x) + assert expint(2, x*exp_polar(2*I*pi)) == 2*I*pi*x + expint(2, x) + assert expint(2, x*exp_polar(-2*I*pi)) == -2*I*pi*x + expint(2, x) + assert expint(1, x).rewrite(Ei).rewrite(expint) == expint(1, x) + assert expint(x, y).rewrite(Ei) == expint(x, y) + assert expint(x, y).rewrite(Ci) == expint(x, y) + + assert mytn(E1(x), E1(x).rewrite(Shi), Shi(x) - Chi(x), x) + assert mytn(E1(polar_lift(I)*x), E1(polar_lift(I)*x).rewrite(Si), + -Ci(x) + I*Si(x) - I*pi/2, x) + + assert mytn(expint(2, x), expint(2, x).rewrite(Ei).rewrite(expint), + -x*E1(x) + exp(-x), x) + assert mytn(expint(3, x), expint(3, x).rewrite(Ei).rewrite(expint), + x**2*E1(x)/2 + (1 - x)*exp(-x)/2, x) + + assert expint(Rational(3, 2), z).nseries(z) == \ + 2 + 2*z - z**2/3 + z**3/15 - z**4/84 + z**5/540 - \ + 2*sqrt(pi)*sqrt(z) + O(z**6) + + assert E1(z).series(z) == -EulerGamma - log(z) + z - \ + z**2/4 + z**3/18 - z**4/96 + z**5/600 + O(z**6) + + assert expint(4, z).series(z) == Rational(1, 3) - z/2 + z**2/2 + \ + z**3*(log(z)/6 - Rational(11, 36) + EulerGamma/6 - I*pi/6) - z**4/24 + \ + z**5/240 + O(z**6) + + assert expint(n, x).series(x, oo, n=3) == \ + (n*(n + 1)/x**2 - n/x + 1 + O(x**(-3), (x, oo)))*exp(-x)/x + + assert expint(z, y).series(z, 0, 2) == exp(-y)/y - z*meijerg(((), (1, 1)), + ((0, 0, 1), ()), y)/y + O(z**2) + raises(ArgumentIndexError, lambda: expint(x, y).fdiff(3)) + + neg = Symbol('neg', negative=True) + assert Ei(neg).rewrite(Si) == Shi(neg) + Chi(neg) - I*pi + + +def test__eis(): + assert _eis(z).diff(z) == -_eis(z) + 1/z + + assert _eis(1/z).series(z) == \ + z + z**2 + 2*z**3 + 6*z**4 + 24*z**5 + O(z**6) + + assert Ei(z).rewrite('tractable') == exp(z)*_eis(z) + assert li(z).rewrite('tractable') == z*_eis(log(z)) + + assert _eis(z).rewrite('intractable') == exp(-z)*Ei(z) + + assert expand(li(z).rewrite('tractable').diff(z).rewrite('intractable')) \ + == li(z).diff(z) + + assert expand(Ei(z).rewrite('tractable').diff(z).rewrite('intractable')) \ + == Ei(z).diff(z) + + assert _eis(z).series(z, n=3) == EulerGamma + log(z) + z*(-log(z) - \ + EulerGamma + 1) + z**2*(log(z)/2 - Rational(3, 4) + EulerGamma/2)\ + + O(z**3*log(z)) + raises(ArgumentIndexError, lambda: _eis(z).fdiff(2)) + + +def tn_arg(func): + def test(arg, e1, e2): + from sympy.core.random import uniform + v = uniform(1, 5) + v1 = func(arg*x).subs(x, v).n() + v2 = func(e1*v + e2*1e-15).n() + return abs(v1 - v2).n() < 1e-10 + return test(exp_polar(I*pi/2), I, 1) and \ + test(exp_polar(-I*pi/2), -I, 1) and \ + test(exp_polar(I*pi), -1, I) and \ + test(exp_polar(-I*pi), -1, -I) + + +def test_li(): + z = Symbol("z") + zr = Symbol("z", real=True) + zp = Symbol("z", positive=True) + zn = Symbol("z", negative=True) + + assert li(0) is S.Zero + assert li(1) is -oo + assert li(oo) is oo + + assert isinstance(li(z), li) + assert unchanged(li, -zp) + assert unchanged(li, zn) + + assert diff(li(z), z) == 1/log(z) + + assert conjugate(li(z)) == li(conjugate(z)) + assert conjugate(li(-zr)) == li(-zr) + assert unchanged(conjugate, li(-zp)) + assert unchanged(conjugate, li(zn)) + + assert li(z).rewrite(Li) == Li(z) + li(2) + assert li(z).rewrite(Ei) == Ei(log(z)) + assert li(z).rewrite(uppergamma) == (-log(1/log(z))/2 - log(-log(z)) + + log(log(z))/2 - expint(1, -log(z))) + assert li(z).rewrite(Si) == (-log(I*log(z)) - log(1/log(z))/2 + + log(log(z))/2 + Ci(I*log(z)) + Shi(log(z))) + assert li(z).rewrite(Ci) == (-log(I*log(z)) - log(1/log(z))/2 + + log(log(z))/2 + Ci(I*log(z)) + Shi(log(z))) + assert li(z).rewrite(Shi) == (-log(1/log(z))/2 + log(log(z))/2 + + Chi(log(z)) - Shi(log(z))) + assert li(z).rewrite(Chi) == (-log(1/log(z))/2 + log(log(z))/2 + + Chi(log(z)) - Shi(log(z))) + assert li(z).rewrite(hyper) ==(log(z)*hyper((1, 1), (2, 2), log(z)) - + log(1/log(z))/2 + log(log(z))/2 + EulerGamma) + assert li(z).rewrite(meijerg) == (-log(1/log(z))/2 - log(-log(z)) + log(log(z))/2 - + meijerg(((), (1,)), ((0, 0), ()), -log(z))) + + assert gruntz(1/li(z), z, oo) is S.Zero + assert li(z).series(z) == log(z)**5/600 + log(z)**4/96 + log(z)**3/18 + log(z)**2/4 + \ + log(z) + log(log(z)) + EulerGamma + raises(ArgumentIndexError, lambda: li(z).fdiff(2)) + + +def test_Li(): + assert Li(2) is S.Zero + assert Li(oo) is oo + + assert isinstance(Li(z), Li) + + assert diff(Li(z), z) == 1/log(z) + + assert gruntz(1/Li(z), z, oo) is S.Zero + assert Li(z).rewrite(li) == li(z) - li(2) + assert Li(z).series(z) == \ + log(z)**5/600 + log(z)**4/96 + log(z)**3/18 + log(z)**2/4 + log(z) + log(log(z)) - li(2) + EulerGamma + raises(ArgumentIndexError, lambda: Li(z).fdiff(2)) + + +def test_si(): + assert Si(I*x) == I*Shi(x) + assert Shi(I*x) == I*Si(x) + assert Si(-I*x) == -I*Shi(x) + assert Shi(-I*x) == -I*Si(x) + assert Si(-x) == -Si(x) + assert Shi(-x) == -Shi(x) + assert Si(exp_polar(2*pi*I)*x) == Si(x) + assert Si(exp_polar(-2*pi*I)*x) == Si(x) + assert Shi(exp_polar(2*pi*I)*x) == Shi(x) + assert Shi(exp_polar(-2*pi*I)*x) == Shi(x) + + assert Si(oo) == pi/2 + assert Si(-oo) == -pi/2 + assert Shi(oo) is oo + assert Shi(-oo) is -oo + + assert mytd(Si(x), sin(x)/x, x) + assert mytd(Shi(x), sinh(x)/x, x) + + assert mytn(Si(x), Si(x).rewrite(Ei), + -I*(-Ei(x*exp_polar(-I*pi/2))/2 + + Ei(x*exp_polar(I*pi/2))/2 - I*pi) + pi/2, x) + assert mytn(Si(x), Si(x).rewrite(expint), + -I*(-expint(1, x*exp_polar(-I*pi/2))/2 + + expint(1, x*exp_polar(I*pi/2))/2) + pi/2, x) + assert mytn(Shi(x), Shi(x).rewrite(Ei), + Ei(x)/2 - Ei(x*exp_polar(I*pi))/2 + I*pi/2, x) + assert mytn(Shi(x), Shi(x).rewrite(expint), + expint(1, x)/2 - expint(1, x*exp_polar(I*pi))/2 - I*pi/2, x) + + assert tn_arg(Si) + assert tn_arg(Shi) + + assert Si(x)._eval_as_leading_term(x, None, 1) == x + assert Si(2*x)._eval_as_leading_term(x, None, 1) == 2*x + assert Si(sin(x))._eval_as_leading_term(x, None, 1) == x + assert Si(x + 1)._eval_as_leading_term(x, None, 1) == Si(1) + assert Si(1/x)._eval_as_leading_term(x, None, 1) == \ + Si(1/x)._eval_as_leading_term(x, None, -1) == Si(1/x) + + assert Si(x).nseries(x, n=8) == \ + x - x**3/18 + x**5/600 - x**7/35280 + O(x**8) + assert Shi(x).nseries(x, n=8) == \ + x + x**3/18 + x**5/600 + x**7/35280 + O(x**8) + assert Si(sin(x)).nseries(x, n=5) == x - 2*x**3/9 + O(x**5) + assert Si(x).nseries(x, 1, n=3) == \ + Si(1) + (x - 1)*sin(1) + (x - 1)**2*(-sin(1)/2 + cos(1)/2) + O((x - 1)**3, (x, 1)) + + assert Si(x).series(x, oo) == -sin(x)*(-6/x**4 + x**(-2) + O(x**(-6), (x, oo))) - \ + cos(x)*(24/x**5 - 2/x**3 + 1/x + O(x**(-6), (x, oo))) + pi/2 + + t = Symbol('t', Dummy=True) + assert Si(x).rewrite(sinc).dummy_eq(Integral(sinc(t), (t, 0, x))) + + assert limit(Shi(x), x, S.Infinity) == S.Infinity + assert limit(Shi(x), x, S.NegativeInfinity) == S.NegativeInfinity + + +def test_ci(): + m1 = exp_polar(I*pi) + m1_ = exp_polar(-I*pi) + pI = exp_polar(I*pi/2) + mI = exp_polar(-I*pi/2) + + assert Ci(m1*x) == Ci(x) + I*pi + assert Ci(m1_*x) == Ci(x) - I*pi + assert Ci(pI*x) == Chi(x) + I*pi/2 + assert Ci(mI*x) == Chi(x) - I*pi/2 + assert Chi(m1*x) == Chi(x) + I*pi + assert Chi(m1_*x) == Chi(x) - I*pi + assert Chi(pI*x) == Ci(x) + I*pi/2 + assert Chi(mI*x) == Ci(x) - I*pi/2 + assert Ci(exp_polar(2*I*pi)*x) == Ci(x) + 2*I*pi + assert Chi(exp_polar(-2*I*pi)*x) == Chi(x) - 2*I*pi + assert Chi(exp_polar(2*I*pi)*x) == Chi(x) + 2*I*pi + assert Ci(exp_polar(-2*I*pi)*x) == Ci(x) - 2*I*pi + + assert Ci(oo) is S.Zero + assert Ci(-oo) == I*pi + assert Chi(oo) is oo + assert Chi(-oo) is oo + + assert mytd(Ci(x), cos(x)/x, x) + assert mytd(Chi(x), cosh(x)/x, x) + + assert mytn(Ci(x), Ci(x).rewrite(Ei), + Ei(x*exp_polar(-I*pi/2))/2 + Ei(x*exp_polar(I*pi/2))/2, x) + assert mytn(Chi(x), Chi(x).rewrite(Ei), + Ei(x)/2 + Ei(x*exp_polar(I*pi))/2 - I*pi/2, x) + + assert tn_arg(Ci) + assert tn_arg(Chi) + + assert Ci(x).nseries(x, n=4) == \ + EulerGamma + log(x) - x**2/4 + O(x**4) + assert Chi(x).nseries(x, n=4) == \ + EulerGamma + log(x) + x**2/4 + O(x**4) + + assert Ci(x).series(x, oo) == -cos(x)*(-6/x**4 + x**(-2) + O(x**(-6), (x, oo))) + \ + sin(x)*(24/x**5 - 2/x**3 + 1/x + O(x**(-6), (x, oo))) + + assert Ci(x).series(x, -oo) == -cos(x)*(-6/x**4 + x**(-2) + O(x**(-6), (x, -oo))) + \ + sin(x)*(24/x**5 - 2/x**3 + 1/x + O(x**(-6), (x, -oo))) + I*pi + + assert limit(log(x) - Ci(2*x), x, 0) == -log(2) - EulerGamma + assert Ci(x).rewrite(uppergamma) == -expint(1, x*exp_polar(-I*pi/2))/2 -\ + expint(1, x*exp_polar(I*pi/2))/2 + assert Ci(x).rewrite(expint) == -expint(1, x*exp_polar(-I*pi/2))/2 -\ + expint(1, x*exp_polar(I*pi/2))/2 + raises(ArgumentIndexError, lambda: Ci(x).fdiff(2)) + + +def test_fresnel(): + assert fresnels(0) is S.Zero + assert fresnels(oo) is S.Half + assert fresnels(-oo) == Rational(-1, 2) + assert fresnels(I*oo) == -I*S.Half + + assert unchanged(fresnels, z) + assert fresnels(-z) == -fresnels(z) + assert fresnels(I*z) == -I*fresnels(z) + assert fresnels(-I*z) == I*fresnels(z) + + assert conjugate(fresnels(z)) == fresnels(conjugate(z)) + + assert fresnels(z).diff(z) == sin(pi*z**2/2) + + assert fresnels(z).rewrite(erf) == (S.One + I)/4 * ( + erf((S.One + I)/2*sqrt(pi)*z) - I*erf((S.One - I)/2*sqrt(pi)*z)) + + assert fresnels(z).rewrite(hyper) == \ + pi*z**3/6 * hyper([Rational(3, 4)], [Rational(3, 2), Rational(7, 4)], -pi**2*z**4/16) + + assert fresnels(z).series(z, n=15) == \ + pi*z**3/6 - pi**3*z**7/336 + pi**5*z**11/42240 + O(z**15) + + assert fresnels(w).is_extended_real is True + assert fresnels(w).is_finite is True + + assert fresnels(z).is_extended_real is None + assert fresnels(z).is_finite is None + + assert fresnels(z).as_real_imag() == (fresnels(re(z) - I*im(z))/2 + + fresnels(re(z) + I*im(z))/2, + -I*(-fresnels(re(z) - I*im(z)) + fresnels(re(z) + I*im(z)))/2) + + assert fresnels(z).as_real_imag(deep=False) == (fresnels(re(z) - I*im(z))/2 + + fresnels(re(z) + I*im(z))/2, + -I*(-fresnels(re(z) - I*im(z)) + fresnels(re(z) + I*im(z)))/2) + + assert fresnels(w).as_real_imag() == (fresnels(w), 0) + assert fresnels(w).as_real_imag(deep=True) == (fresnels(w), 0) + + assert fresnels(2 + 3*I).as_real_imag() == ( + fresnels(2 + 3*I)/2 + fresnels(2 - 3*I)/2, + -I*(fresnels(2 + 3*I) - fresnels(2 - 3*I))/2 + ) + + assert expand_func(integrate(fresnels(z), z)) == \ + z*fresnels(z) + cos(pi*z**2/2)/pi + + assert fresnels(z).rewrite(meijerg) == sqrt(2)*pi*z**Rational(9, 4) * \ + meijerg(((), (1,)), ((Rational(3, 4),), + (Rational(1, 4), 0)), -pi**2*z**4/16)/(2*(-z)**Rational(3, 4)*(z**2)**Rational(3, 4)) + + assert fresnelc(0) is S.Zero + assert fresnelc(oo) == S.Half + assert fresnelc(-oo) == Rational(-1, 2) + assert fresnelc(I*oo) == I*S.Half + + assert unchanged(fresnelc, z) + assert fresnelc(-z) == -fresnelc(z) + assert fresnelc(I*z) == I*fresnelc(z) + assert fresnelc(-I*z) == -I*fresnelc(z) + + assert conjugate(fresnelc(z)) == fresnelc(conjugate(z)) + + assert fresnelc(z).diff(z) == cos(pi*z**2/2) + + assert fresnelc(z).rewrite(erf) == (S.One - I)/4 * ( + erf((S.One + I)/2*sqrt(pi)*z) + I*erf((S.One - I)/2*sqrt(pi)*z)) + + assert fresnelc(z).rewrite(hyper) == \ + z * hyper([Rational(1, 4)], [S.Half, Rational(5, 4)], -pi**2*z**4/16) + + assert fresnelc(w).is_extended_real is True + + assert fresnelc(z).as_real_imag() == \ + (fresnelc(re(z) - I*im(z))/2 + fresnelc(re(z) + I*im(z))/2, + -I*(-fresnelc(re(z) - I*im(z)) + fresnelc(re(z) + I*im(z)))/2) + + assert fresnelc(z).as_real_imag(deep=False) == \ + (fresnelc(re(z) - I*im(z))/2 + fresnelc(re(z) + I*im(z))/2, + -I*(-fresnelc(re(z) - I*im(z)) + fresnelc(re(z) + I*im(z)))/2) + + assert fresnelc(2 + 3*I).as_real_imag() == ( + fresnelc(2 - 3*I)/2 + fresnelc(2 + 3*I)/2, + -I*(fresnelc(2 + 3*I) - fresnelc(2 - 3*I))/2 + ) + + assert expand_func(integrate(fresnelc(z), z)) == \ + z*fresnelc(z) - sin(pi*z**2/2)/pi + + assert fresnelc(z).rewrite(meijerg) == sqrt(2)*pi*z**Rational(3, 4) * \ + meijerg(((), (1,)), ((Rational(1, 4),), + (Rational(3, 4), 0)), -pi**2*z**4/16)/(2*(-z)**Rational(1, 4)*(z**2)**Rational(1, 4)) + + from sympy.core.random import verify_numerically + + verify_numerically(re(fresnels(z)), fresnels(z).as_real_imag()[0], z) + verify_numerically(im(fresnels(z)), fresnels(z).as_real_imag()[1], z) + verify_numerically(fresnels(z), fresnels(z).rewrite(hyper), z) + verify_numerically(fresnels(z), fresnels(z).rewrite(meijerg), z) + + verify_numerically(re(fresnelc(z)), fresnelc(z).as_real_imag()[0], z) + verify_numerically(im(fresnelc(z)), fresnelc(z).as_real_imag()[1], z) + verify_numerically(fresnelc(z), fresnelc(z).rewrite(hyper), z) + verify_numerically(fresnelc(z), fresnelc(z).rewrite(meijerg), z) + + raises(ArgumentIndexError, lambda: fresnels(z).fdiff(2)) + raises(ArgumentIndexError, lambda: fresnelc(z).fdiff(2)) + + assert fresnels(x).taylor_term(-1, x) is S.Zero + assert fresnelc(x).taylor_term(-1, x) is S.Zero + assert fresnelc(x).taylor_term(1, x) == -pi**2*x**5/40 + + +def test_fresnel_series(): + assert fresnelc(z).series(z, n=15) == \ + z - pi**2*z**5/40 + pi**4*z**9/3456 - pi**6*z**13/599040 + O(z**15) + + # issues 6510, 10102 + fs = (S.Half - sin(pi*z**2/2)/(pi**2*z**3) + + (-1/(pi*z) + 3/(pi**3*z**5))*cos(pi*z**2/2)) + fc = (S.Half - cos(pi*z**2/2)/(pi**2*z**3) + + (1/(pi*z) - 3/(pi**3*z**5))*sin(pi*z**2/2)) + assert fresnels(z).series(z, oo) == fs + O(z**(-6), (z, oo)) + assert fresnelc(z).series(z, oo) == fc + O(z**(-6), (z, oo)) + assert (fresnels(z).series(z, -oo) + fs.subs(z, -z)).expand().is_Order + assert (fresnelc(z).series(z, -oo) + fc.subs(z, -z)).expand().is_Order + assert (fresnels(1/z).series(z) - fs.subs(z, 1/z)).expand().is_Order + assert (fresnelc(1/z).series(z) - fc.subs(z, 1/z)).expand().is_Order + assert ((2*fresnels(3*z)).series(z, oo) - 2*fs.subs(z, 3*z)).expand().is_Order + assert ((3*fresnelc(2*z)).series(z, oo) - 3*fc.subs(z, 2*z)).expand().is_Order + + +def test_integral_rewrites(): #issues 26134, 26144, 26306 + assert expint(n, x).rewrite(Integral).dummy_eq(Integral(t**-n * exp(-t*x), (t, 1, oo))) + assert Si(x).rewrite(Integral).dummy_eq(Integral(sinc(t), (t, 0, x))) + assert Ci(x).rewrite(Integral).dummy_eq(log(x) - Integral((1 - cos(t))/t, (t, 0, x)) + EulerGamma) + assert fresnels(x).rewrite(Integral).dummy_eq(Integral(sin(pi*t**2/2), (t, 0, x))) + assert fresnelc(x).rewrite(Integral).dummy_eq(Integral(cos(pi*t**2/2), (t, 0, x))) + assert Ei(x).rewrite(Integral).dummy_eq(Integral(exp(t)/t, (t, -oo, x))) + assert fresnels(x).diff(x) == fresnels(x).rewrite(Integral).diff(x) + assert fresnelc(x).diff(x) == fresnelc(x).rewrite(Integral).diff(x) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_gamma_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_gamma_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..14c57a31ce2edaa60fd5efc8bcbc95668961fd41 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_gamma_functions.py @@ -0,0 +1,741 @@ +from sympy.core.function import expand_func, Subs +from sympy.core import EulerGamma +from sympy.core.numbers import (I, Rational, nan, oo, pi, zoo) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol) +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.combinatorial.numbers import harmonic +from sympy.functions.elementary.complexes import (Abs, conjugate, im, re) +from sympy.functions.elementary.exponential import (exp, exp_polar, log) +from sympy.functions.elementary.hyperbolic import tanh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin, atan) +from sympy.functions.special.error_functions import (Ei, erf, erfc) +from sympy.functions.special.gamma_functions import (digamma, gamma, loggamma, lowergamma, multigamma, polygamma, trigamma, uppergamma) +from sympy.functions.special.zeta_functions import zeta +from sympy.series.order import O + +from sympy.core.expr import unchanged +from sympy.core.function import ArgumentIndexError +from sympy.testing.pytest import raises +from sympy.core.random import (test_derivative_numerically as td, + random_complex_number as randcplx, + verify_numerically as tn) + +x = Symbol('x') +y = Symbol('y') +n = Symbol('n', integer=True) +w = Symbol('w', real=True) + +def test_gamma(): + assert gamma(nan) is nan + assert gamma(oo) is oo + + assert gamma(-100) is zoo + assert gamma(0) is zoo + assert gamma(-100.0) is zoo + + assert gamma(1) == 1 + assert gamma(2) == 1 + assert gamma(3) == 2 + + assert gamma(102) == factorial(101) + + assert gamma(S.Half) == sqrt(pi) + + assert gamma(Rational(3, 2)) == sqrt(pi)*S.Half + assert gamma(Rational(5, 2)) == sqrt(pi)*Rational(3, 4) + assert gamma(Rational(7, 2)) == sqrt(pi)*Rational(15, 8) + + assert gamma(Rational(-1, 2)) == -2*sqrt(pi) + assert gamma(Rational(-3, 2)) == sqrt(pi)*Rational(4, 3) + assert gamma(Rational(-5, 2)) == sqrt(pi)*Rational(-8, 15) + + assert gamma(Rational(-15, 2)) == sqrt(pi)*Rational(256, 2027025) + + assert gamma(Rational( + -11, 8)).expand(func=True) == Rational(64, 33)*gamma(Rational(5, 8)) + assert gamma(Rational( + -10, 3)).expand(func=True) == Rational(81, 280)*gamma(Rational(2, 3)) + assert gamma(Rational( + 14, 3)).expand(func=True) == Rational(880, 81)*gamma(Rational(2, 3)) + assert gamma(Rational( + 17, 7)).expand(func=True) == Rational(30, 49)*gamma(Rational(3, 7)) + assert gamma(Rational( + 19, 8)).expand(func=True) == Rational(33, 64)*gamma(Rational(3, 8)) + + assert gamma(x).diff(x) == gamma(x)*polygamma(0, x) + + assert gamma(x - 1).expand(func=True) == gamma(x)/(x - 1) + assert gamma(x + 2).expand(func=True, mul=False) == x*(x + 1)*gamma(x) + + assert conjugate(gamma(x)) == gamma(conjugate(x)) + + assert expand_func(gamma(x + Rational(3, 2))) == \ + (x + S.Half)*gamma(x + S.Half) + + assert expand_func(gamma(x - S.Half)) == \ + gamma(S.Half + x)/(x - S.Half) + + # Test a bug: + assert expand_func(gamma(x + Rational(3, 4))) == gamma(x + Rational(3, 4)) + + # XXX: Not sure about these tests. I can fix them by defining e.g. + # exp_polar.is_integer but I'm not sure if that makes sense. + assert gamma(3*exp_polar(I*pi)/4).is_nonnegative is False + assert gamma(3*exp_polar(I*pi)/4).is_extended_nonpositive is True + + y = Symbol('y', nonpositive=True, integer=True) + assert gamma(y).is_real == False + y = Symbol('y', positive=True, noninteger=True) + assert gamma(y).is_real == True + + assert gamma(-1.0, evaluate=False).is_real == False + assert gamma(0, evaluate=False).is_real == False + assert gamma(-2, evaluate=False).is_real == False + + +def test_gamma_rewrite(): + assert gamma(n).rewrite(factorial) == factorial(n - 1) + + +def test_gamma_series(): + assert gamma(x + 1).series(x, 0, 3) == \ + 1 - EulerGamma*x + x**2*(EulerGamma**2/2 + pi**2/12) + O(x**3) + assert gamma(x).series(x, -1, 3) == \ + -1/(x + 1) + EulerGamma - 1 + (x + 1)*(-1 - pi**2/12 - EulerGamma**2/2 + \ + EulerGamma) + (x + 1)**2*(-1 - pi**2/12 - EulerGamma**2/2 + EulerGamma**3/6 - \ + polygamma(2, 1)/6 + EulerGamma*pi**2/12 + EulerGamma) + O((x + 1)**3, (x, -1)) + + +def tn_branch(s, func): + from sympy.core.random import uniform + c = uniform(1, 5) + expr = func(s, c*exp_polar(I*pi)) - func(s, c*exp_polar(-I*pi)) + eps = 1e-15 + expr2 = func(s + eps, -c + eps*I) - func(s + eps, -c - eps*I) + return abs(expr.n() - expr2.n()).n() < 1e-10 + + +def test_lowergamma(): + from sympy.functions.special.error_functions import expint + from sympy.functions.special.hyper import meijerg + assert lowergamma(x, 0) == 0 + assert lowergamma(x, y).diff(y) == y**(x - 1)*exp(-y) + assert td(lowergamma(randcplx(), y), y) + assert td(lowergamma(x, randcplx()), x) + assert lowergamma(x, y).diff(x) == \ + gamma(x)*digamma(x) - uppergamma(x, y)*log(y) \ + - meijerg([], [1, 1], [0, 0, x], [], y) + + assert lowergamma(S.Half, x) == sqrt(pi)*erf(sqrt(x)) + assert not lowergamma(S.Half - 3, x).has(lowergamma) + assert not lowergamma(S.Half + 3, x).has(lowergamma) + assert lowergamma(S.Half, x, evaluate=False).has(lowergamma) + assert tn(lowergamma(S.Half + 3, x, evaluate=False), + lowergamma(S.Half + 3, x), x) + assert tn(lowergamma(S.Half - 3, x, evaluate=False), + lowergamma(S.Half - 3, x), x) + + assert tn_branch(-3, lowergamma) + assert tn_branch(-4, lowergamma) + assert tn_branch(Rational(1, 3), lowergamma) + assert tn_branch(pi, lowergamma) + assert lowergamma(3, exp_polar(4*pi*I)*x) == lowergamma(3, x) + assert lowergamma(y, exp_polar(5*pi*I)*x) == \ + exp(4*I*pi*y)*lowergamma(y, x*exp_polar(pi*I)) + assert lowergamma(-2, exp_polar(5*pi*I)*x) == \ + lowergamma(-2, x*exp_polar(I*pi)) + 2*pi*I + + assert conjugate(lowergamma(x, y)) == lowergamma(conjugate(x), conjugate(y)) + assert conjugate(lowergamma(x, 0)) == 0 + assert unchanged(conjugate, lowergamma(x, -oo)) + + assert lowergamma(0, x)._eval_is_meromorphic(x, 0) == False + assert lowergamma(S(1)/3, x)._eval_is_meromorphic(x, 0) == False + assert lowergamma(1, x, evaluate=False)._eval_is_meromorphic(x, 0) == True + assert lowergamma(x, x)._eval_is_meromorphic(x, 0) == False + assert lowergamma(x + 1, x)._eval_is_meromorphic(x, 0) == False + assert lowergamma(1/x, x)._eval_is_meromorphic(x, 0) == False + assert lowergamma(0, x + 1)._eval_is_meromorphic(x, 0) == False + assert lowergamma(S(1)/3, x + 1)._eval_is_meromorphic(x, 0) == True + assert lowergamma(1, x + 1, evaluate=False)._eval_is_meromorphic(x, 0) == True + assert lowergamma(x, x + 1)._eval_is_meromorphic(x, 0) == True + assert lowergamma(x + 1, x + 1)._eval_is_meromorphic(x, 0) == True + assert lowergamma(1/x, x + 1)._eval_is_meromorphic(x, 0) == False + assert lowergamma(0, 1/x)._eval_is_meromorphic(x, 0) == False + assert lowergamma(S(1)/3, 1/x)._eval_is_meromorphic(x, 0) == False + assert lowergamma(1, 1/x, evaluate=False)._eval_is_meromorphic(x, 0) == False + assert lowergamma(x, 1/x)._eval_is_meromorphic(x, 0) == False + assert lowergamma(x + 1, 1/x)._eval_is_meromorphic(x, 0) == False + assert lowergamma(1/x, 1/x)._eval_is_meromorphic(x, 0) == False + + assert lowergamma(x, 2).series(x, oo, 3) == \ + 2**x*(1 + 2/(x + 1))*exp(-2)/x + O(exp(x*log(2))/x**3, (x, oo)) + + assert lowergamma( + x, y).rewrite(expint) == -y**x*expint(-x + 1, y) + gamma(x) + k = Symbol('k', integer=True) + assert lowergamma( + k, y).rewrite(expint) == -y**k*expint(-k + 1, y) + gamma(k) + k = Symbol('k', integer=True, positive=False) + assert lowergamma(k, y).rewrite(expint) == lowergamma(k, y) + assert lowergamma(x, y).rewrite(uppergamma) == gamma(x) - uppergamma(x, y) + + assert lowergamma(70, 6) == factorial(69) - 69035724522603011058660187038367026272747334489677105069435923032634389419656200387949342530805432320 * exp(-6) + assert (lowergamma(S(77) / 2, 6) - lowergamma(S(77) / 2, 6, evaluate=False)).evalf() < 1e-16 + assert (lowergamma(-S(77) / 2, 6) - lowergamma(-S(77) / 2, 6, evaluate=False)).evalf() < 1e-16 + + +def test_uppergamma(): + from sympy.functions.special.error_functions import expint + from sympy.functions.special.hyper import meijerg + assert uppergamma(4, 0) == 6 + assert uppergamma(x, y).diff(y) == -y**(x - 1)*exp(-y) + assert td(uppergamma(randcplx(), y), y) + assert uppergamma(x, y).diff(x) == \ + uppergamma(x, y)*log(y) + meijerg([], [1, 1], [0, 0, x], [], y) + assert td(uppergamma(x, randcplx()), x) + + p = Symbol('p', positive=True) + assert uppergamma(0, p) == -Ei(-p) + assert uppergamma(p, 0) == gamma(p) + assert uppergamma(S.Half, x) == sqrt(pi)*erfc(sqrt(x)) + assert not uppergamma(S.Half - 3, x).has(uppergamma) + assert not uppergamma(S.Half + 3, x).has(uppergamma) + assert uppergamma(S.Half, x, evaluate=False).has(uppergamma) + assert tn(uppergamma(S.Half + 3, x, evaluate=False), + uppergamma(S.Half + 3, x), x) + assert tn(uppergamma(S.Half - 3, x, evaluate=False), + uppergamma(S.Half - 3, x), x) + + assert unchanged(uppergamma, x, -oo) + assert unchanged(uppergamma, x, 0) + + assert tn_branch(-3, uppergamma) + assert tn_branch(-4, uppergamma) + assert tn_branch(Rational(1, 3), uppergamma) + assert tn_branch(pi, uppergamma) + assert uppergamma(3, exp_polar(4*pi*I)*x) == uppergamma(3, x) + assert uppergamma(y, exp_polar(5*pi*I)*x) == \ + exp(4*I*pi*y)*uppergamma(y, x*exp_polar(pi*I)) + \ + gamma(y)*(1 - exp(4*pi*I*y)) + assert uppergamma(-2, exp_polar(5*pi*I)*x) == \ + uppergamma(-2, x*exp_polar(I*pi)) - 2*pi*I + + assert uppergamma(-2, x) == expint(3, x)/x**2 + + assert conjugate(uppergamma(x, y)) == uppergamma(conjugate(x), conjugate(y)) + assert unchanged(conjugate, uppergamma(x, -oo)) + + assert uppergamma(x, y).rewrite(expint) == y**x*expint(-x + 1, y) + assert uppergamma(x, y).rewrite(lowergamma) == gamma(x) - lowergamma(x, y) + + assert uppergamma(70, 6) == 69035724522603011058660187038367026272747334489677105069435923032634389419656200387949342530805432320*exp(-6) + assert (uppergamma(S(77) / 2, 6) - uppergamma(S(77) / 2, 6, evaluate=False)).evalf() < 1e-16 + assert (uppergamma(-S(77) / 2, 6) - uppergamma(-S(77) / 2, 6, evaluate=False)).evalf() < 1e-16 + + +def test_polygamma(): + assert polygamma(n, nan) is nan + + assert polygamma(0, oo) is oo + assert polygamma(0, -oo) is oo + assert polygamma(0, I*oo) is oo + assert polygamma(0, -I*oo) is oo + assert polygamma(1, oo) == 0 + assert polygamma(5, oo) == 0 + + assert polygamma(0, -9) is zoo + + assert polygamma(0, -9) is zoo + assert polygamma(0, -1) is zoo + assert polygamma(Rational(3, 2), -1) is zoo + + assert polygamma(0, 0) is zoo + + assert polygamma(0, 1) == -EulerGamma + assert polygamma(0, 7) == Rational(49, 20) - EulerGamma + + assert polygamma(1, 1) == pi**2/6 + assert polygamma(1, 2) == pi**2/6 - 1 + assert polygamma(1, 3) == pi**2/6 - Rational(5, 4) + assert polygamma(3, 1) == pi**4 / 15 + assert polygamma(3, 5) == 6*(Rational(-22369, 20736) + pi**4/90) + assert polygamma(5, 1) == 8 * pi**6 / 63 + + assert polygamma(1, S.Half) == pi**2 / 2 + assert polygamma(2, S.Half) == -14*zeta(3) + assert polygamma(11, S.Half) == 176896*pi**12 + + def t(m, n): + x = S(m)/n + r = polygamma(0, x) + if r.has(polygamma): + return False + return abs(polygamma(0, x.n()).n() - r.n()).n() < 1e-10 + assert t(1, 2) + assert t(3, 2) + assert t(-1, 2) + assert t(1, 4) + assert t(-3, 4) + assert t(1, 3) + assert t(4, 3) + assert t(3, 4) + assert t(2, 3) + assert t(123, 5) + + assert polygamma(0, x).rewrite(zeta) == polygamma(0, x) + assert polygamma(1, x).rewrite(zeta) == zeta(2, x) + assert polygamma(2, x).rewrite(zeta) == -2*zeta(3, x) + assert polygamma(I, 2).rewrite(zeta) == polygamma(I, 2) + n1 = Symbol('n1') + n2 = Symbol('n2', real=True) + n3 = Symbol('n3', integer=True) + n4 = Symbol('n4', positive=True) + n5 = Symbol('n5', positive=True, integer=True) + assert polygamma(n1, x).rewrite(zeta) == polygamma(n1, x) + assert polygamma(n2, x).rewrite(zeta) == polygamma(n2, x) + assert polygamma(n3, x).rewrite(zeta) == polygamma(n3, x) + assert polygamma(n4, x).rewrite(zeta) == polygamma(n4, x) + assert polygamma(n5, x).rewrite(zeta) == (-1)**(n5 + 1) * factorial(n5) * zeta(n5 + 1, x) + + assert polygamma(3, 7*x).diff(x) == 7*polygamma(4, 7*x) + + assert polygamma(0, x).rewrite(harmonic) == harmonic(x - 1) - EulerGamma + assert polygamma(2, x).rewrite(harmonic) == 2*harmonic(x - 1, 3) - 2*zeta(3) + ni = Symbol("n", integer=True) + assert polygamma(ni, x).rewrite(harmonic) == (-1)**(ni + 1)*(-harmonic(x - 1, ni + 1) + + zeta(ni + 1))*factorial(ni) + + # Polygamma of non-negative integer order is unbranched: + k = Symbol('n', integer=True, nonnegative=True) + assert polygamma(k, exp_polar(2*I*pi)*x) == polygamma(k, x) + + # but negative integers are branched! + k = Symbol('n', integer=True) + assert polygamma(k, exp_polar(2*I*pi)*x).args == (k, exp_polar(2*I*pi)*x) + + # Polygamma of order -1 is loggamma: + assert polygamma(-1, x) == loggamma(x) - log(2*pi) / 2 + + # But smaller orders are iterated integrals and don't have a special name + assert polygamma(-2, x).func is polygamma + + # Test a bug + assert polygamma(0, -x).expand(func=True) == polygamma(0, -x) + + assert polygamma(2, 2.5).is_positive == False + assert polygamma(2, -2.5).is_positive == False + assert polygamma(3, 2.5).is_positive == True + assert polygamma(3, -2.5).is_positive is True + assert polygamma(-2, -2.5).is_positive is None + assert polygamma(-3, -2.5).is_positive is None + + assert polygamma(2, 2.5).is_negative == True + assert polygamma(3, 2.5).is_negative == False + assert polygamma(3, -2.5).is_negative == False + assert polygamma(2, -2.5).is_negative is True + assert polygamma(-2, -2.5).is_negative is None + assert polygamma(-3, -2.5).is_negative is None + + assert polygamma(I, 2).is_positive is None + assert polygamma(I, 3).is_negative is None + + # issue 17350 + assert (I*polygamma(I, pi)).as_real_imag() == \ + (-im(polygamma(I, pi)), re(polygamma(I, pi))) + assert (tanh(polygamma(I, 1))).rewrite(exp) == \ + (exp(polygamma(I, 1)) - exp(-polygamma(I, 1)))/(exp(polygamma(I, 1)) + exp(-polygamma(I, 1))) + assert (I / polygamma(I, 4)).rewrite(exp) == \ + I*exp(-I*atan(im(polygamma(I, 4))/re(polygamma(I, 4))))/Abs(polygamma(I, 4)) + + # issue 12569 + assert unchanged(im, polygamma(0, I)) + assert polygamma(Symbol('a', positive=True), Symbol('b', positive=True)).is_real is True + assert polygamma(0, I).is_real is None + + assert str(polygamma(pi, 3).evalf(n=10)) == "0.1169314564" + assert str(polygamma(2.3, 1.0).evalf(n=10)) == "-3.003302909" + assert str(polygamma(-1, 1).evalf(n=10)) == "-0.9189385332" # not zero + assert str(polygamma(I, 1).evalf(n=10)) == "-3.109856569 + 1.89089016*I" + assert str(polygamma(1, I).evalf(n=10)) == "-0.5369999034 - 0.7942335428*I" + assert str(polygamma(I, I).evalf(n=10)) == "6.332362889 + 45.92828268*I" + + +def test_polygamma_expand_func(): + assert polygamma(0, x).expand(func=True) == polygamma(0, x) + assert polygamma(0, 2*x).expand(func=True) == \ + polygamma(0, x)/2 + polygamma(0, S.Half + x)/2 + log(2) + assert polygamma(1, 2*x).expand(func=True) == \ + polygamma(1, x)/4 + polygamma(1, S.Half + x)/4 + assert polygamma(2, x).expand(func=True) == \ + polygamma(2, x) + assert polygamma(0, -1 + x).expand(func=True) == \ + polygamma(0, x) - 1/(x - 1) + assert polygamma(0, 1 + x).expand(func=True) == \ + 1/x + polygamma(0, x ) + assert polygamma(0, 2 + x).expand(func=True) == \ + 1/x + 1/(1 + x) + polygamma(0, x) + assert polygamma(0, 3 + x).expand(func=True) == \ + polygamma(0, x) + 1/x + 1/(1 + x) + 1/(2 + x) + assert polygamma(0, 4 + x).expand(func=True) == \ + polygamma(0, x) + 1/x + 1/(1 + x) + 1/(2 + x) + 1/(3 + x) + assert polygamma(1, 1 + x).expand(func=True) == \ + polygamma(1, x) - 1/x**2 + assert polygamma(1, 2 + x).expand(func=True, multinomial=False) == \ + polygamma(1, x) - 1/x**2 - 1/(1 + x)**2 + assert polygamma(1, 3 + x).expand(func=True, multinomial=False) == \ + polygamma(1, x) - 1/x**2 - 1/(1 + x)**2 - 1/(2 + x)**2 + assert polygamma(1, 4 + x).expand(func=True, multinomial=False) == \ + polygamma(1, x) - 1/x**2 - 1/(1 + x)**2 - \ + 1/(2 + x)**2 - 1/(3 + x)**2 + assert polygamma(0, x + y).expand(func=True) == \ + polygamma(0, x + y) + assert polygamma(1, x + y).expand(func=True) == \ + polygamma(1, x + y) + assert polygamma(1, 3 + 4*x + y).expand(func=True, multinomial=False) == \ + polygamma(1, y + 4*x) - 1/(y + 4*x)**2 - \ + 1/(1 + y + 4*x)**2 - 1/(2 + y + 4*x)**2 + assert polygamma(3, 3 + 4*x + y).expand(func=True, multinomial=False) == \ + polygamma(3, y + 4*x) - 6/(y + 4*x)**4 - \ + 6/(1 + y + 4*x)**4 - 6/(2 + y + 4*x)**4 + assert polygamma(3, 4*x + y + 1).expand(func=True, multinomial=False) == \ + polygamma(3, y + 4*x) - 6/(y + 4*x)**4 + e = polygamma(3, 4*x + y + Rational(3, 2)) + assert e.expand(func=True) == e + e = polygamma(3, x + y + Rational(3, 4)) + assert e.expand(func=True, basic=False) == e + + assert polygamma(-1, x, evaluate=False).expand(func=True) == \ + loggamma(x) - log(pi)/2 - log(2)/2 + p2 = polygamma(-2, x).expand(func=True) + x**2/2 - x/2 + S(1)/12 + assert isinstance(p2, Subs) + assert p2.point == (-1,) + + +def test_digamma(): + assert digamma(nan) == nan + + assert digamma(oo) == oo + assert digamma(-oo) == oo + assert digamma(I*oo) == oo + assert digamma(-I*oo) == oo + + assert digamma(-9) == zoo + + assert digamma(-9) == zoo + assert digamma(-1) == zoo + + assert digamma(0) == zoo + + assert digamma(1) == -EulerGamma + assert digamma(7) == Rational(49, 20) - EulerGamma + + def t(m, n): + x = S(m)/n + r = digamma(x) + if r.has(digamma): + return False + return abs(digamma(x.n()).n() - r.n()).n() < 1e-10 + assert t(1, 2) + assert t(3, 2) + assert t(-1, 2) + assert t(1, 4) + assert t(-3, 4) + assert t(1, 3) + assert t(4, 3) + assert t(3, 4) + assert t(2, 3) + assert t(123, 5) + + assert digamma(x).rewrite(zeta) == polygamma(0, x) + + assert digamma(x).rewrite(harmonic) == harmonic(x - 1) - EulerGamma + + assert digamma(I).is_real is None + + assert digamma(x,evaluate=False).fdiff() == polygamma(1, x) + + assert digamma(x,evaluate=False).is_real is None + + assert digamma(x,evaluate=False).is_positive is None + + assert digamma(x,evaluate=False).is_negative is None + + assert digamma(x,evaluate=False).rewrite(polygamma) == polygamma(0, x) + + +def test_digamma_expand_func(): + assert digamma(x).expand(func=True) == polygamma(0, x) + assert digamma(2*x).expand(func=True) == \ + polygamma(0, x)/2 + polygamma(0, Rational(1, 2) + x)/2 + log(2) + assert digamma(-1 + x).expand(func=True) == \ + polygamma(0, x) - 1/(x - 1) + assert digamma(1 + x).expand(func=True) == \ + 1/x + polygamma(0, x ) + assert digamma(2 + x).expand(func=True) == \ + 1/x + 1/(1 + x) + polygamma(0, x) + assert digamma(3 + x).expand(func=True) == \ + polygamma(0, x) + 1/x + 1/(1 + x) + 1/(2 + x) + assert digamma(4 + x).expand(func=True) == \ + polygamma(0, x) + 1/x + 1/(1 + x) + 1/(2 + x) + 1/(3 + x) + assert digamma(x + y).expand(func=True) == \ + polygamma(0, x + y) + +def test_trigamma(): + assert trigamma(nan) == nan + + assert trigamma(oo) == 0 + + assert trigamma(1) == pi**2/6 + assert trigamma(2) == pi**2/6 - 1 + assert trigamma(3) == pi**2/6 - Rational(5, 4) + + assert trigamma(x, evaluate=False).rewrite(zeta) == zeta(2, x) + assert trigamma(x, evaluate=False).rewrite(harmonic) == \ + trigamma(x).rewrite(polygamma).rewrite(harmonic) + + assert trigamma(x,evaluate=False).fdiff() == polygamma(2, x) + + assert trigamma(x,evaluate=False).is_real is None + + assert trigamma(x,evaluate=False).is_positive is None + + assert trigamma(x,evaluate=False).is_negative is None + + assert trigamma(x,evaluate=False).rewrite(polygamma) == polygamma(1, x) + +def test_trigamma_expand_func(): + assert trigamma(2*x).expand(func=True) == \ + polygamma(1, x)/4 + polygamma(1, Rational(1, 2) + x)/4 + assert trigamma(1 + x).expand(func=True) == \ + polygamma(1, x) - 1/x**2 + assert trigamma(2 + x).expand(func=True, multinomial=False) == \ + polygamma(1, x) - 1/x**2 - 1/(1 + x)**2 + assert trigamma(3 + x).expand(func=True, multinomial=False) == \ + polygamma(1, x) - 1/x**2 - 1/(1 + x)**2 - 1/(2 + x)**2 + assert trigamma(4 + x).expand(func=True, multinomial=False) == \ + polygamma(1, x) - 1/x**2 - 1/(1 + x)**2 - \ + 1/(2 + x)**2 - 1/(3 + x)**2 + assert trigamma(x + y).expand(func=True) == \ + polygamma(1, x + y) + assert trigamma(3 + 4*x + y).expand(func=True, multinomial=False) == \ + polygamma(1, y + 4*x) - 1/(y + 4*x)**2 - \ + 1/(1 + y + 4*x)**2 - 1/(2 + y + 4*x)**2 + +def test_loggamma(): + raises(TypeError, lambda: loggamma(2, 3)) + raises(ArgumentIndexError, lambda: loggamma(x).fdiff(2)) + + assert loggamma(-1) is oo + assert loggamma(-2) is oo + assert loggamma(0) is oo + assert loggamma(1) == 0 + assert loggamma(2) == 0 + assert loggamma(3) == log(2) + assert loggamma(4) == log(6) + + n = Symbol("n", integer=True, positive=True) + assert loggamma(n) == log(gamma(n)) + assert loggamma(-n) is oo + assert loggamma(n/2) == log(2**(-n + 1)*sqrt(pi)*gamma(n)/gamma(n/2 + S.Half)) + + assert loggamma(oo) is oo + assert loggamma(-oo) is zoo + assert loggamma(I*oo) is zoo + assert loggamma(-I*oo) is zoo + assert loggamma(zoo) is zoo + assert loggamma(nan) is nan + + L = loggamma(Rational(16, 3)) + E = -5*log(3) + loggamma(Rational(1, 3)) + log(4) + log(7) + log(10) + log(13) + assert expand_func(L).doit() == E + assert L.n() == E.n() + + L = loggamma(Rational(19, 4)) + E = -4*log(4) + loggamma(Rational(3, 4)) + log(3) + log(7) + log(11) + log(15) + assert expand_func(L).doit() == E + assert L.n() == E.n() + + L = loggamma(Rational(23, 7)) + E = -3*log(7) + log(2) + loggamma(Rational(2, 7)) + log(9) + log(16) + assert expand_func(L).doit() == E + assert L.n() == E.n() + + L = loggamma(Rational(19, 4) - 7) + E = -log(9) - log(5) + loggamma(Rational(3, 4)) + 3*log(4) - 3*I*pi + assert expand_func(L).doit() == E + assert L.n() == E.n() + + L = loggamma(Rational(23, 7) - 6) + E = -log(19) - log(12) - log(5) + loggamma(Rational(2, 7)) + 3*log(7) - 3*I*pi + assert expand_func(L).doit() == E + assert L.n() == E.n() + + assert loggamma(x).diff(x) == polygamma(0, x) + s1 = loggamma(1/(x + sin(x)) + cos(x)).nseries(x, n=4) + s2 = (-log(2*x) - 1)/(2*x) - log(x/pi)/2 + (4 - log(2*x))*x/24 + O(x**2) + \ + log(x)*x**2/2 + assert (s1 - s2).expand(force=True).removeO() == 0 + s1 = loggamma(1/x).series(x) + s2 = (1/x - S.Half)*log(1/x) - 1/x + log(2*pi)/2 + \ + x/12 - x**3/360 + x**5/1260 + O(x**7) + assert ((s1 - s2).expand(force=True)).removeO() == 0 + + assert loggamma(x).rewrite('intractable') == log(gamma(x)) + + s1 = loggamma(x).series(x).cancel() + assert s1 == -log(x) - EulerGamma*x + pi**2*x**2/12 + x**3*polygamma(2, 1)/6 + \ + pi**4*x**4/360 + x**5*polygamma(4, 1)/120 + O(x**6) + assert s1 == loggamma(x).rewrite('intractable').series(x).cancel() + + assert conjugate(loggamma(x)) == loggamma(conjugate(x)) + assert conjugate(loggamma(0)) is oo + assert conjugate(loggamma(1)) == loggamma(conjugate(1)) + assert conjugate(loggamma(-oo)) == conjugate(zoo) + + assert loggamma(Symbol('v', positive=True)).is_real is True + assert loggamma(Symbol('v', zero=True)).is_real is False + assert loggamma(Symbol('v', negative=True)).is_real is False + assert loggamma(Symbol('v', nonpositive=True)).is_real is False + assert loggamma(Symbol('v', nonnegative=True)).is_real is None + assert loggamma(Symbol('v', imaginary=True)).is_real is None + assert loggamma(Symbol('v', real=True)).is_real is None + assert loggamma(Symbol('v')).is_real is None + + assert loggamma(S.Half).is_real is True + assert loggamma(0).is_real is False + assert loggamma(Rational(-1, 2)).is_real is False + assert loggamma(I).is_real is None + assert loggamma(2 + 3*I).is_real is None + + def tN(N, M): + assert loggamma(1/x)._eval_nseries(x, n=N).getn() == M + tN(0, 0) + tN(1, 1) + tN(2, 2) + tN(3, 3) + tN(4, 4) + tN(5, 5) + + +def test_polygamma_expansion(): + # A. & S., pa. 259 and 260 + assert polygamma(0, 1/x).nseries(x, n=3) == \ + -log(x) - x/2 - x**2/12 + O(x**3) + assert polygamma(1, 1/x).series(x, n=5) == \ + x + x**2/2 + x**3/6 + O(x**5) + assert polygamma(3, 1/x).nseries(x, n=11) == \ + 2*x**3 + 3*x**4 + 2*x**5 - x**7 + 4*x**9/3 + O(x**11) + + +def test_polygamma_leading_term(): + expr = -log(1/x) + polygamma(0, 1 + 1/x) + S.EulerGamma + assert expr.as_leading_term(x, logx=-y) == S.EulerGamma + + +def test_issue_8657(): + n = Symbol('n', negative=True, integer=True) + m = Symbol('m', integer=True) + o = Symbol('o', positive=True) + p = Symbol('p', negative=True, integer=False) + assert gamma(n).is_real is False + assert gamma(m).is_real is None + assert gamma(o).is_real is True + assert gamma(p).is_real is True + assert gamma(w).is_real is None + + +def test_issue_8524(): + x = Symbol('x', positive=True) + y = Symbol('y', negative=True) + z = Symbol('z', positive=False) + p = Symbol('p', negative=False) + q = Symbol('q', integer=True) + r = Symbol('r', integer=False) + e = Symbol('e', even=True, negative=True) + assert gamma(x).is_positive is True + assert gamma(y).is_positive is None + assert gamma(z).is_positive is None + assert gamma(p).is_positive is None + assert gamma(q).is_positive is None + assert gamma(r).is_positive is None + assert gamma(e + S.Half).is_positive is True + assert gamma(e - S.Half).is_positive is False + +def test_issue_14450(): + assert uppergamma(Rational(3, 8), x).evalf() == uppergamma(Rational(3, 8), x) + assert lowergamma(x, Rational(3, 8)).evalf() == lowergamma(x, Rational(3, 8)) + # some values from Wolfram Alpha for comparison + assert abs(uppergamma(Rational(3, 8), 2).evalf() - 0.07105675881) < 1e-9 + assert abs(lowergamma(Rational(3, 8), 2).evalf() - 2.2993794256) < 1e-9 + +def test_issue_14528(): + k = Symbol('k', integer=True, nonpositive=True) + assert isinstance(gamma(k), gamma) + +def test_multigamma(): + from sympy.concrete.products import Product + p = Symbol('p') + _k = Dummy('_k') + + assert multigamma(x, p).dummy_eq(pi**(p*(p - 1)/4)*\ + Product(gamma(x + (1 - _k)/2), (_k, 1, p))) + + assert conjugate(multigamma(x, p)).dummy_eq(pi**((conjugate(p) - 1)*\ + conjugate(p)/4)*Product(gamma(conjugate(x) + (1-conjugate(_k))/2), (_k, 1, p))) + assert conjugate(multigamma(x, 1)) == gamma(conjugate(x)) + + p = Symbol('p', positive=True) + assert conjugate(multigamma(x, p)).dummy_eq(pi**((p - 1)*p/4)*\ + Product(gamma(conjugate(x) + (1-conjugate(_k))/2), (_k, 1, p))) + + assert multigamma(nan, 1) is nan + assert multigamma(oo, 1).doit() is oo + + assert multigamma(1, 1) == 1 + assert multigamma(2, 1) == 1 + assert multigamma(3, 1) == 2 + + assert multigamma(102, 1) == factorial(101) + assert multigamma(S.Half, 1) == sqrt(pi) + + assert multigamma(1, 2) == pi + assert multigamma(2, 2) == pi/2 + + assert multigamma(1, 3) is zoo + assert multigamma(2, 3) == pi**2/2 + assert multigamma(3, 3) == 3*pi**2/2 + + assert multigamma(x, 1).diff(x) == gamma(x)*polygamma(0, x) + assert multigamma(x, 2).diff(x) == sqrt(pi)*gamma(x)*gamma(x - S.Half)*\ + polygamma(0, x) + sqrt(pi)*gamma(x)*gamma(x - S.Half)*polygamma(0, x - S.Half) + + assert multigamma(x - 1, 1).expand(func=True) == gamma(x)/(x - 1) + assert multigamma(x + 2, 1).expand(func=True, mul=False) == x*(x + 1)*\ + gamma(x) + assert multigamma(x - 1, 2).expand(func=True) == sqrt(pi)*gamma(x)*\ + gamma(x + S.Half)/(x**3 - 3*x**2 + x*Rational(11, 4) - Rational(3, 4)) + assert multigamma(x - 1, 3).expand(func=True) == pi**Rational(3, 2)*gamma(x)**2*\ + gamma(x + S.Half)/(x**5 - 6*x**4 + 55*x**3/4 - 15*x**2 + x*Rational(31, 4) - Rational(3, 2)) + + assert multigamma(n, 1).rewrite(factorial) == factorial(n - 1) + assert multigamma(n, 2).rewrite(factorial) == sqrt(pi)*\ + factorial(n - Rational(3, 2))*factorial(n - 1) + assert multigamma(n, 3).rewrite(factorial) == pi**Rational(3, 2)*\ + factorial(n - 2)*factorial(n - Rational(3, 2))*factorial(n - 1) + + assert multigamma(Rational(-1, 2), 3, evaluate=False).is_real == False + assert multigamma(S.Half, 3, evaluate=False).is_real == False + assert multigamma(0, 1, evaluate=False).is_real == False + assert multigamma(1, 3, evaluate=False).is_real == False + assert multigamma(-1.0, 3, evaluate=False).is_real == False + assert multigamma(0.7, 3, evaluate=False).is_real == True + assert multigamma(3, 3, evaluate=False).is_real == True + +def test_gamma_as_leading_term(): + assert gamma(x).as_leading_term(x) == 1/x + assert gamma(2 + x).as_leading_term(x) == S(1) + assert gamma(cos(x)).as_leading_term(x) == S(1) + assert gamma(sin(x)).as_leading_term(x) == 1/x diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_hyper.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_hyper.py new file mode 100644 index 0000000000000000000000000000000000000000..f1be5b5f0db158ff76173e180ed8d88bd59461b9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_hyper.py @@ -0,0 +1,403 @@ +from sympy.core.containers import Tuple +from sympy.core.function import Derivative +from sympy.core.numbers import (I, Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import cos +from sympy.functions.special.gamma_functions import gamma +from sympy.functions.special.hyper import (appellf1, hyper, meijerg) +from sympy.series.order import O +from sympy.abc import x, z, k +from sympy.series.limits import limit +from sympy.testing.pytest import raises, slow +from sympy.core.random import ( + random_complex_number as randcplx, + verify_numerically as tn, + test_derivative_numerically as td) + + +def test_TupleParametersBase(): + # test that our implementation of the chain rule works + p = hyper((), (), z**2) + assert p.diff(z) == p*2*z + + +def test_hyper(): + raises(TypeError, lambda: hyper(1, 2, z)) + + assert hyper((2, 1), (1,), z) == hyper(Tuple(1, 2), Tuple(1), z) + assert hyper((2, 1, 2), (1, 2, 1, 3), z) == hyper((2,), (1, 3), z) + u = hyper((2, 1, 2), (1, 2, 1, 3), z, evaluate=False) + assert u.ap == Tuple(1, 2, 2) + assert u.bq == Tuple(1, 1, 2, 3) + + h = hyper((1, 2), (3, 4, 5), z) + assert h.ap == Tuple(1, 2) + assert h.bq == Tuple(3, 4, 5) + assert h.argument == z + assert h.is_commutative is True + h = hyper((2, 1), (4, 3, 5), z) + assert h.ap == Tuple(1, 2) + assert h.bq == Tuple(3, 4, 5) + assert h.argument == z + assert h.is_commutative is True + + # just a few checks to make sure that all arguments go where they should + assert tn(hyper(Tuple(), Tuple(), z), exp(z), z) + assert tn(z*hyper((1, 1), Tuple(2), -z), log(1 + z), z) + + # differentiation + h = hyper( + (randcplx(), randcplx(), randcplx()), (randcplx(), randcplx()), z) + assert td(h, z) + + a1, a2, b1, b2, b3 = symbols('a1:3, b1:4') + assert hyper((a1, a2), (b1, b2, b3), z).diff(z) == \ + a1*a2/(b1*b2*b3) * hyper((a1 + 1, a2 + 1), (b1 + 1, b2 + 1, b3 + 1), z) + + # differentiation wrt parameters is not supported + assert hyper([z], [], z).diff(z) == Derivative(hyper([z], [], z), z) + + # hyper is unbranched wrt parameters + from sympy.functions.elementary.complexes import polar_lift + assert hyper([polar_lift(z)], [polar_lift(k)], polar_lift(x)) == \ + hyper([z], [k], polar_lift(x)) + + # hyper does not automatically evaluate anyway, but the test is to make + # sure that the evaluate keyword is accepted + assert hyper((1, 2), (1,), z, evaluate=False).func is hyper + + +def test_expand_func(): + # evaluation at 1 of Gauss' hypergeometric function: + from sympy.abc import a, b, c + from sympy.core.function import expand_func + a1, b1, c1 = randcplx(), randcplx(), randcplx() + 5 + assert expand_func(hyper([a, b], [c], 1)) == \ + gamma(c)*gamma(-a - b + c)/(gamma(-a + c)*gamma(-b + c)) + assert abs(expand_func(hyper([a1, b1], [c1], 1)).n() + - hyper([a1, b1], [c1], 1).n()) < 1e-10 + + # hyperexpand wrapper for hyper: + assert expand_func(hyper([], [], z)) == exp(z) + assert expand_func(hyper([1, 2, 3], [], z)) == hyper([1, 2, 3], [], z) + assert expand_func(meijerg([[1, 1], []], [[1], [0]], z)) == log(z + 1) + assert expand_func(meijerg([[1, 1], []], [[], []], z)) == \ + meijerg([[1, 1], []], [[], []], z) + + +def replace_dummy(expr, sym): + from sympy.core.symbol import Dummy + dum = expr.atoms(Dummy) + if not dum: + return expr + assert len(dum) == 1 + return expr.xreplace({dum.pop(): sym}) + + +def test_hyper_rewrite_sum(): + from sympy.concrete.summations import Sum + from sympy.core.symbol import Dummy + from sympy.functions.combinatorial.factorials import (RisingFactorial, factorial) + _k = Dummy("k") + assert replace_dummy(hyper((1, 2), (1, 3), x).rewrite(Sum), _k) == \ + Sum(x**_k / factorial(_k) * RisingFactorial(2, _k) / + RisingFactorial(3, _k), (_k, 0, oo)) + + assert hyper((1, 2, 3), (-1, 3), z).rewrite(Sum) == \ + hyper((1, 2, 3), (-1, 3), z) + + +def test_radius_of_convergence(): + assert hyper((1, 2), [3], z).radius_of_convergence == 1 + assert hyper((1, 2), [3, 4], z).radius_of_convergence is oo + assert hyper((1, 2, 3), [4], z).radius_of_convergence == 0 + assert hyper((0, 1, 2), [4], z).radius_of_convergence is oo + assert hyper((-1, 1, 2), [-4], z).radius_of_convergence == 0 + assert hyper((-1, -2, 2), [-1], z).radius_of_convergence is oo + assert hyper((-1, 2), [-1, -2], z).radius_of_convergence == 0 + assert hyper([-1, 1, 3], [-2, 2], z).radius_of_convergence == 1 + assert hyper([-1, 1], [-2, 2], z).radius_of_convergence is oo + assert hyper([-1, 1, 3], [-2], z).radius_of_convergence == 0 + assert hyper((-1, 2, 3, 4), [], z).radius_of_convergence is oo + + assert hyper([1, 1], [3], 1).convergence_statement == True + assert hyper([1, 1], [2], 1).convergence_statement == False + assert hyper([1, 1], [2], -1).convergence_statement == True + assert hyper([1, 1], [1], -1).convergence_statement == False + + +def test_meijer(): + raises(TypeError, lambda: meijerg(1, z)) + raises(TypeError, lambda: meijerg(((1,), (2,)), (3,), (4,), z)) + + assert meijerg(((1, 2), (3,)), ((4,), (5,)), z) == \ + meijerg(Tuple(1, 2), Tuple(3), Tuple(4), Tuple(5), z) + + g = meijerg((1, 2), (3, 4, 5), (6, 7, 8, 9), (10, 11, 12, 13, 14), z) + assert g.an == Tuple(1, 2) + assert g.ap == Tuple(1, 2, 3, 4, 5) + assert g.aother == Tuple(3, 4, 5) + assert g.bm == Tuple(6, 7, 8, 9) + assert g.bq == Tuple(6, 7, 8, 9, 10, 11, 12, 13, 14) + assert g.bother == Tuple(10, 11, 12, 13, 14) + assert g.argument == z + assert g.nu == 75 + assert g.delta == -1 + assert g.is_commutative is True + assert g.is_number is False + #issue 13071 + assert meijerg([[],[]], [[S.Half],[0]], 1).is_number is True + + assert meijerg([1, 2], [3], [4], [5], z).delta == S.Half + + # just a few checks to make sure that all arguments go where they should + assert tn(meijerg(Tuple(), Tuple(), Tuple(0), Tuple(), -z), exp(z), z) + assert tn(sqrt(pi)*meijerg(Tuple(), Tuple(), + Tuple(0), Tuple(S.Half), z**2/4), cos(z), z) + assert tn(meijerg(Tuple(1, 1), Tuple(), Tuple(1), Tuple(0), z), + log(1 + z), z) + + # test exceptions + raises(ValueError, lambda: meijerg(((3, 1), (2,)), ((oo,), (2, 0)), x)) + raises(ValueError, lambda: meijerg(((3, 1), (2,)), ((1,), (2, 0)), x)) + + # differentiation + g = meijerg((randcplx(),), (randcplx() + 2*I,), Tuple(), + (randcplx(), randcplx()), z) + assert td(g, z) + + g = meijerg(Tuple(), (randcplx(),), Tuple(), + (randcplx(), randcplx()), z) + assert td(g, z) + + g = meijerg(Tuple(), Tuple(), Tuple(randcplx()), + Tuple(randcplx(), randcplx()), z) + assert td(g, z) + + a1, a2, b1, b2, c1, c2, d1, d2 = symbols('a1:3, b1:3, c1:3, d1:3') + assert meijerg((a1, a2), (b1, b2), (c1, c2), (d1, d2), z).diff(z) == \ + (meijerg((a1 - 1, a2), (b1, b2), (c1, c2), (d1, d2), z) + + (a1 - 1)*meijerg((a1, a2), (b1, b2), (c1, c2), (d1, d2), z))/z + + assert meijerg([z, z], [], [], [], z).diff(z) == \ + Derivative(meijerg([z, z], [], [], [], z), z) + + # meijerg is unbranched wrt parameters + from sympy.functions.elementary.complexes import polar_lift as pl + assert meijerg([pl(a1)], [pl(a2)], [pl(b1)], [pl(b2)], pl(z)) == \ + meijerg([a1], [a2], [b1], [b2], pl(z)) + + # integrand + from sympy.abc import a, b, c, d, s + assert meijerg([a], [b], [c], [d], z).integrand(s) == \ + z**s*gamma(c - s)*gamma(-a + s + 1)/(gamma(b - s)*gamma(-d + s + 1)) + + +def test_meijerg_derivative(): + assert meijerg([], [1, 1], [0, 0, x], [], z).diff(x) == \ + log(z)*meijerg([], [1, 1], [0, 0, x], [], z) \ + + 2*meijerg([], [1, 1, 1], [0, 0, x, 0], [], z) + + y = randcplx() + a = 5 # mpmath chokes with non-real numbers, and Mod1 with floats + assert td(meijerg([x], [], [], [], y), x) + assert td(meijerg([x**2], [], [], [], y), x) + assert td(meijerg([], [x], [], [], y), x) + assert td(meijerg([], [], [x], [], y), x) + assert td(meijerg([], [], [], [x], y), x) + assert td(meijerg([x], [a], [a + 1], [], y), x) + assert td(meijerg([x], [a + 1], [a], [], y), x) + assert td(meijerg([x, a], [], [], [a + 1], y), x) + assert td(meijerg([x, a + 1], [], [], [a], y), x) + b = Rational(3, 2) + assert td(meijerg([a + 2], [b], [b - 3, x], [a], y), x) + + +def test_meijerg_period(): + assert meijerg([], [1], [0], [], x).get_period() == 2*pi + assert meijerg([1], [], [], [0], x).get_period() == 2*pi + assert meijerg([], [], [0], [], x).get_period() == 2*pi # exp(x) + assert meijerg( + [], [], [0], [S.Half], x).get_period() == 2*pi # cos(sqrt(x)) + assert meijerg( + [], [], [S.Half], [0], x).get_period() == 4*pi # sin(sqrt(x)) + assert meijerg([1, 1], [], [1], [0], x).get_period() is oo # log(1 + x) + + +def test_hyper_unpolarify(): + from sympy.functions.elementary.exponential import exp_polar + a = exp_polar(2*pi*I)*x + b = x + assert hyper([], [], a).argument == b + assert hyper([0], [], a).argument == a + assert hyper([0], [0], a).argument == b + assert hyper([0, 1], [0], a).argument == a + assert hyper([0, 1], [0], exp_polar(2*pi*I)).argument == 1 + + +@slow +def test_hyperrep(): + from sympy.functions.special.hyper import (HyperRep, HyperRep_atanh, + HyperRep_power1, HyperRep_power2, HyperRep_log1, HyperRep_asin1, + HyperRep_asin2, HyperRep_sqrts1, HyperRep_sqrts2, HyperRep_log2, + HyperRep_cosasin, HyperRep_sinasin) + # First test the base class works. + from sympy.functions.elementary.exponential import exp_polar + from sympy.functions.elementary.piecewise import Piecewise + a, b, c, d, z = symbols('a b c d z') + + class myrep(HyperRep): + @classmethod + def _expr_small(cls, x): + return a + + @classmethod + def _expr_small_minus(cls, x): + return b + + @classmethod + def _expr_big(cls, x, n): + return c*n + + @classmethod + def _expr_big_minus(cls, x, n): + return d*n + assert myrep(z).rewrite('nonrep') == Piecewise((0, abs(z) > 1), (a, True)) + assert myrep(exp_polar(I*pi)*z).rewrite('nonrep') == \ + Piecewise((0, abs(z) > 1), (b, True)) + assert myrep(exp_polar(2*I*pi)*z).rewrite('nonrep') == \ + Piecewise((c, abs(z) > 1), (a, True)) + assert myrep(exp_polar(3*I*pi)*z).rewrite('nonrep') == \ + Piecewise((d, abs(z) > 1), (b, True)) + assert myrep(exp_polar(4*I*pi)*z).rewrite('nonrep') == \ + Piecewise((2*c, abs(z) > 1), (a, True)) + assert myrep(exp_polar(5*I*pi)*z).rewrite('nonrep') == \ + Piecewise((2*d, abs(z) > 1), (b, True)) + assert myrep(z).rewrite('nonrepsmall') == a + assert myrep(exp_polar(I*pi)*z).rewrite('nonrepsmall') == b + + def t(func, hyp, z): + """ Test that func is a valid representation of hyp. """ + # First test that func agrees with hyp for small z + if not tn(func.rewrite('nonrepsmall'), hyp, z, + a=Rational(-1, 2), b=Rational(-1, 2), c=S.Half, d=S.Half): + return False + # Next check that the two small representations agree. + if not tn( + func.rewrite('nonrepsmall').subs( + z, exp_polar(I*pi)*z).replace(exp_polar, exp), + func.subs(z, exp_polar(I*pi)*z).rewrite('nonrepsmall'), + z, a=Rational(-1, 2), b=Rational(-1, 2), c=S.Half, d=S.Half): + return False + # Next check continuity along exp_polar(I*pi)*t + expr = func.subs(z, exp_polar(I*pi)*z).rewrite('nonrep') + if abs(expr.subs(z, 1 + 1e-15).n() - expr.subs(z, 1 - 1e-15).n()) > 1e-10: + return False + # Finally check continuity of the big reps. + + def dosubs(func, a, b): + rv = func.subs(z, exp_polar(a)*z).rewrite('nonrep') + return rv.subs(z, exp_polar(b)*z).replace(exp_polar, exp) + for n in [0, 1, 2, 3, 4, -1, -2, -3, -4]: + expr1 = dosubs(func, 2*I*pi*n, I*pi/2) + expr2 = dosubs(func, 2*I*pi*n + I*pi, -I*pi/2) + if not tn(expr1, expr2, z): + return False + expr1 = dosubs(func, 2*I*pi*(n + 1), -I*pi/2) + expr2 = dosubs(func, 2*I*pi*n + I*pi, I*pi/2) + if not tn(expr1, expr2, z): + return False + return True + + # Now test the various representatives. + a = Rational(1, 3) + assert t(HyperRep_atanh(z), hyper([S.Half, 1], [Rational(3, 2)], z), z) + assert t(HyperRep_power1(a, z), hyper([-a], [], z), z) + assert t(HyperRep_power2(a, z), hyper([a, a - S.Half], [2*a], z), z) + assert t(HyperRep_log1(z), -z*hyper([1, 1], [2], z), z) + assert t(HyperRep_asin1(z), hyper([S.Half, S.Half], [Rational(3, 2)], z), z) + assert t(HyperRep_asin2(z), hyper([1, 1], [Rational(3, 2)], z), z) + assert t(HyperRep_sqrts1(a, z), hyper([-a, S.Half - a], [S.Half], z), z) + assert t(HyperRep_sqrts2(a, z), + -2*z/(2*a + 1)*hyper([-a - S.Half, -a], [S.Half], z).diff(z), z) + assert t(HyperRep_log2(z), -z/4*hyper([Rational(3, 2), 1, 1], [2, 2], z), z) + assert t(HyperRep_cosasin(a, z), hyper([-a, a], [S.Half], z), z) + assert t(HyperRep_sinasin(a, z), 2*a*z*hyper([1 - a, 1 + a], [Rational(3, 2)], z), z) + + +@slow +def test_meijerg_eval(): + from sympy.functions.elementary.exponential import exp_polar + from sympy.functions.special.bessel import besseli + from sympy.abc import l + a = randcplx() + arg = x*exp_polar(k*pi*I) + expr1 = pi*meijerg([[], [(a + 1)/2]], [[a/2], [-a/2, (a + 1)/2]], arg**2/4) + expr2 = besseli(a, arg) + + # Test that the two expressions agree for all arguments. + for x_ in [0.5, 1.5]: + for k_ in [0.0, 0.1, 0.3, 0.5, 0.8, 1, 5.751, 15.3]: + assert abs((expr1 - expr2).n(subs={x: x_, k: k_})) < 1e-10 + assert abs((expr1 - expr2).n(subs={x: x_, k: -k_})) < 1e-10 + + # Test continuity independently + eps = 1e-13 + expr2 = expr1.subs(k, l) + for x_ in [0.5, 1.5]: + for k_ in [0.5, Rational(1, 3), 0.25, 0.75, Rational(2, 3), 1.0, 1.5]: + assert abs((expr1 - expr2).n( + subs={x: x_, k: k_ + eps, l: k_ - eps})) < 1e-10 + assert abs((expr1 - expr2).n( + subs={x: x_, k: -k_ + eps, l: -k_ - eps})) < 1e-10 + + expr = (meijerg(((0.5,), ()), ((0.5, 0, 0.5), ()), exp_polar(-I*pi)/4) + + meijerg(((0.5,), ()), ((0.5, 0, 0.5), ()), exp_polar(I*pi)/4)) \ + /(2*sqrt(pi)) + assert (expr - pi/exp(1)).n(chop=True) == 0 + + +def test_limits(): + k, x = symbols('k, x') + assert hyper((1,), (Rational(4, 3), Rational(5, 3)), k**2).series(k) == \ + 1 + 9*k**2/20 + 81*k**4/1120 + O(k**6) # issue 6350 + + # https://github.com/sympy/sympy/issues/11465 + assert limit(1/hyper((1, ), (1, ), x), x, 0) == 1 + + +def test_appellf1(): + a, b1, b2, c, x, y = symbols('a b1 b2 c x y') + assert appellf1(a, b2, b1, c, y, x) == appellf1(a, b1, b2, c, x, y) + assert appellf1(a, b1, b1, c, y, x) == appellf1(a, b1, b1, c, x, y) + assert appellf1(a, b1, b2, c, S.Zero, S.Zero) is S.One + + f = appellf1(a, b1, b2, c, S.Zero, S.Zero, evaluate=False) + assert f.func is appellf1 + assert f.doit() is S.One + + +def test_derivative_appellf1(): + from sympy.core.function import diff + a, b1, b2, c, x, y, z = symbols('a b1 b2 c x y z') + assert diff(appellf1(a, b1, b2, c, x, y), x) == a*b1*appellf1(a + 1, b2, b1 + 1, c + 1, y, x)/c + assert diff(appellf1(a, b1, b2, c, x, y), y) == a*b2*appellf1(a + 1, b1, b2 + 1, c + 1, x, y)/c + assert diff(appellf1(a, b1, b2, c, x, y), z) == 0 + assert diff(appellf1(a, b1, b2, c, x, y), a) == Derivative(appellf1(a, b1, b2, c, x, y), a) + + +def test_eval_nseries(): + a1, b1, a2, b2 = symbols('a1 b1 a2 b2') + assert hyper((1,2), (1,2,3), x**2)._eval_nseries(x, 7, None) == \ + 1 + x**2/3 + x**4/24 + x**6/360 + O(x**7) + assert exp(x)._eval_nseries(x,7,None) == \ + hyper((a1, b1), (a1, b1), x)._eval_nseries(x, 7, None) + assert hyper((a1, a2), (b1, b2), x)._eval_nseries(z, 7, None) ==\ + hyper((a1, a2), (b1, b2), x) + O(z**7) + assert hyper((-S(1)/2, S(1)/2), (1,), 4*x/(x + 1)).nseries(x) == \ + 1 - x + x**2/4 - 3*x**3/4 - 15*x**4/64 - 93*x**5/64 + O(x**6) + assert (pi/2*hyper((-S(1)/2, S(1)/2), (1,), 4*x/(x + 1))).nseries(x) == \ + pi/2 - pi*x/2 + pi*x**2/8 - 3*pi*x**3/8 - 15*pi*x**4/128 - 93*pi*x**5/128 + O(x**6) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_mathieu.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_mathieu.py new file mode 100644 index 0000000000000000000000000000000000000000..b9296f0657d920c8d297f820fb3ab8b6a53129ab --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_mathieu.py @@ -0,0 +1,29 @@ +from sympy.core.function import diff +from sympy.functions.elementary.complexes import conjugate +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.functions.special.mathieu_functions import (mathieuc, mathieucprime, mathieus, mathieusprime) + +from sympy.abc import a, q, z + + +def test_mathieus(): + assert isinstance(mathieus(a, q, z), mathieus) + assert mathieus(a, 0, z) == sin(sqrt(a)*z) + assert conjugate(mathieus(a, q, z)) == mathieus(conjugate(a), conjugate(q), conjugate(z)) + assert diff(mathieus(a, q, z), z) == mathieusprime(a, q, z) + +def test_mathieuc(): + assert isinstance(mathieuc(a, q, z), mathieuc) + assert mathieuc(a, 0, z) == cos(sqrt(a)*z) + assert diff(mathieuc(a, q, z), z) == mathieucprime(a, q, z) + +def test_mathieusprime(): + assert isinstance(mathieusprime(a, q, z), mathieusprime) + assert mathieusprime(a, 0, z) == sqrt(a)*cos(sqrt(a)*z) + assert diff(mathieusprime(a, q, z), z) == (-a + 2*q*cos(2*z))*mathieus(a, q, z) + +def test_mathieucprime(): + assert isinstance(mathieucprime(a, q, z), mathieucprime) + assert mathieucprime(a, 0, z) == -sqrt(a)*sin(sqrt(a)*z) + assert diff(mathieucprime(a, q, z), z) == (-a + 2*q*cos(2*z))*mathieuc(a, q, z) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_singularity_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_singularity_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..dbd85cb0c7e5524d4fe1441615879b9776ad1693 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_singularity_functions.py @@ -0,0 +1,129 @@ +from sympy.core.function import (Derivative, diff) +from sympy.core.numbers import (Float, I, nan, oo, pi) +from sympy.core.relational import Eq +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.delta_functions import (DiracDelta, Heaviside) +from sympy.functions.special.singularity_functions import SingularityFunction +from sympy.series.order import O + + +from sympy.core.expr import unchanged +from sympy.core.function import ArgumentIndexError +from sympy.testing.pytest import raises + +x, y, a, n = symbols('x y a n') + + +def test_fdiff(): + assert SingularityFunction(x, 4, 5).fdiff() == 5*SingularityFunction(x, 4, 4) + assert SingularityFunction(x, 4, -1).fdiff() == SingularityFunction(x, 4, -2) + assert SingularityFunction(x, 4, -2).fdiff() == SingularityFunction(x, 4, -3) + assert SingularityFunction(x, 4, -3).fdiff() == SingularityFunction(x, 4, -4) + assert SingularityFunction(x, 4, 0).fdiff() == SingularityFunction(x, 4, -1) + + assert SingularityFunction(y, 6, 2).diff(y) == 2*SingularityFunction(y, 6, 1) + assert SingularityFunction(y, -4, -1).diff(y) == SingularityFunction(y, -4, -2) + assert SingularityFunction(y, 4, 0).diff(y) == SingularityFunction(y, 4, -1) + assert SingularityFunction(y, 4, 0).diff(y, 2) == SingularityFunction(y, 4, -2) + + n = Symbol('n', positive=True) + assert SingularityFunction(x, a, n).fdiff() == n*SingularityFunction(x, a, n - 1) + assert SingularityFunction(y, a, n).diff(y) == n*SingularityFunction(y, a, n - 1) + + expr_in = 4*SingularityFunction(x, a, n) + 3*SingularityFunction(x, a, -1) + -10*SingularityFunction(x, a, 0) + expr_out = n*4*SingularityFunction(x, a, n - 1) + 3*SingularityFunction(x, a, -2) - 10*SingularityFunction(x, a, -1) + assert diff(expr_in, x) == expr_out + + assert SingularityFunction(x, -10, 5).diff(evaluate=False) == ( + Derivative(SingularityFunction(x, -10, 5), x)) + + raises(ArgumentIndexError, lambda: SingularityFunction(x, 4, 5).fdiff(2)) + + +def test_eval(): + assert SingularityFunction(x, a, n).func == SingularityFunction + assert unchanged(SingularityFunction, x, 5, n) + assert SingularityFunction(5, 3, 2) == 4 + assert SingularityFunction(3, 5, 1) == 0 + assert SingularityFunction(3, 3, 0) == 1 + assert SingularityFunction(3, 3, 1) == 0 + assert SingularityFunction(Symbol('z', zero=True), 0, 1) == 0 # like sin(z) == 0 + assert SingularityFunction(4, 4, -1) is oo + assert SingularityFunction(4, 2, -1) == 0 + assert SingularityFunction(4, 7, -1) == 0 + assert SingularityFunction(5, 6, -2) == 0 + assert SingularityFunction(4, 2, -2) == 0 + assert SingularityFunction(4, 4, -2) is oo + assert SingularityFunction(4, 2, -3) == 0 + assert SingularityFunction(8, 8, -3) is oo + assert SingularityFunction(4, 2, -4) == 0 + assert SingularityFunction(8, 8, -4) is oo + assert (SingularityFunction(6.1, 4, 5)).evalf(5) == Float('40.841', '5') + assert SingularityFunction(6.1, pi, 2) == (-pi + 6.1)**2 + assert SingularityFunction(x, a, nan) is nan + assert SingularityFunction(x, nan, 1) is nan + assert SingularityFunction(nan, a, n) is nan + + raises(ValueError, lambda: SingularityFunction(x, a, I)) + raises(ValueError, lambda: SingularityFunction(2*I, I, n)) + raises(ValueError, lambda: SingularityFunction(x, a, -5)) + + +def test_leading_term(): + l = Symbol('l', positive=True) + assert SingularityFunction(x, 3, 2).as_leading_term(x) == 0 + assert SingularityFunction(x, -2, 1).as_leading_term(x) == 2 + assert SingularityFunction(x, 0, 0).as_leading_term(x) == 1 + assert SingularityFunction(x, 0, 0).as_leading_term(x, cdir=-1) == 0 + assert SingularityFunction(x, 0, -1).as_leading_term(x) == 0 + assert SingularityFunction(x, 0, -2).as_leading_term(x) == 0 + assert SingularityFunction(x, 0, -3).as_leading_term(x) == 0 + assert SingularityFunction(x, 0, -4).as_leading_term(x) == 0 + assert (SingularityFunction(x + l, 0, 1)/2\ + - SingularityFunction(x + l, l/2, 1)\ + + SingularityFunction(x + l, l, 1)/2).as_leading_term(x) == -x/2 + + +def test_series(): + l = Symbol('l', positive=True) + assert SingularityFunction(x, -3, 2).series(x) == x**2 + 6*x + 9 + assert SingularityFunction(x, -2, 1).series(x) == x + 2 + assert SingularityFunction(x, 0, 0).series(x) == 1 + assert SingularityFunction(x, 0, 0).series(x, dir='-') == 0 + assert SingularityFunction(x, 0, -1).series(x) == 0 + assert SingularityFunction(x, 0, -2).series(x) == 0 + assert SingularityFunction(x, 0, -3).series(x) == 0 + assert SingularityFunction(x, 0, -4).series(x) == 0 + assert (SingularityFunction(x + l, 0, 1)/2\ + - SingularityFunction(x + l, l/2, 1)\ + + SingularityFunction(x + l, l, 1)/2).nseries(x) == -x/2 + O(x**6) + + +def test_rewrite(): + assert SingularityFunction(x, 4, 5).rewrite(Piecewise) == ( + Piecewise(((x - 4)**5, x - 4 >= 0), (0, True))) + assert SingularityFunction(x, -10, 0).rewrite(Piecewise) == ( + Piecewise((1, x + 10 >= 0), (0, True))) + assert SingularityFunction(x, 2, -1).rewrite(Piecewise) == ( + Piecewise((oo, Eq(x - 2, 0)), (0, True))) + assert SingularityFunction(x, 0, -2).rewrite(Piecewise) == ( + Piecewise((oo, Eq(x, 0)), (0, True))) + + n = Symbol('n', nonnegative=True) + p = SingularityFunction(x, a, n).rewrite(Piecewise) + assert p == ( + Piecewise(((x - a)**n, x - a >= 0), (0, True))) + assert p.subs(x, a).subs(n, 0) == 1 + + expr_in = SingularityFunction(x, 4, 5) + SingularityFunction(x, -3, -1) - SingularityFunction(x, 0, -2) + expr_out = (x - 4)**5*Heaviside(x - 4, 1) + DiracDelta(x + 3) - DiracDelta(x, 1) + assert expr_in.rewrite(Heaviside) == expr_out + assert expr_in.rewrite(DiracDelta) == expr_out + assert expr_in.rewrite('HeavisideDiracDelta') == expr_out + + expr_in = SingularityFunction(x, a, n) + SingularityFunction(x, a, -1) - SingularityFunction(x, a, -2) + expr_out = (x - a)**n*Heaviside(x - a, 1) + DiracDelta(x - a) + DiracDelta(a - x, 1) + assert expr_in.rewrite(Heaviside) == expr_out + assert expr_in.rewrite(DiracDelta) == expr_out + assert expr_in.rewrite('HeavisideDiracDelta') == expr_out diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_spec_polynomials.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_spec_polynomials.py new file mode 100644 index 0000000000000000000000000000000000000000..584ad3cf97df8b9d92da9fc7805ab4296f40671c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_spec_polynomials.py @@ -0,0 +1,475 @@ +from sympy.concrete.summations import Sum +from sympy.core.function import (Derivative, diff) +from sympy.core.numbers import (Rational, oo, pi, zoo) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol) +from sympy.functions.combinatorial.factorials import (RisingFactorial, binomial, factorial) +from sympy.functions.elementary.complexes import conjugate +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import cos +from sympy.functions.special.gamma_functions import gamma +from sympy.functions.special.hyper import hyper +from sympy.functions.special.polynomials import (assoc_laguerre, assoc_legendre, chebyshevt, chebyshevt_root, chebyshevu, chebyshevu_root, gegenbauer, hermite, hermite_prob, jacobi, jacobi_normalized, laguerre, legendre) +from sympy.polys.orthopolys import laguerre_poly +from sympy.polys.polyroots import roots + +from sympy.core.expr import unchanged +from sympy.core.function import ArgumentIndexError +from sympy.testing.pytest import raises + + +x = Symbol('x') + + +def test_jacobi(): + n = Symbol("n") + a = Symbol("a") + b = Symbol("b") + + assert jacobi(0, a, b, x) == 1 + assert jacobi(1, a, b, x) == a/2 - b/2 + x*(a/2 + b/2 + 1) + + assert jacobi(n, a, a, x) == RisingFactorial( + a + 1, n)*gegenbauer(n, a + S.Half, x)/RisingFactorial(2*a + 1, n) + assert jacobi(n, a, -a, x) == ((-1)**a*(-x + 1)**(-a/2)*(x + 1)**(a/2)*assoc_legendre(n, a, x)* + factorial(-a + n)*gamma(a + n + 1)/(factorial(a + n)*gamma(n + 1))) + assert jacobi(n, -b, b, x) == ((-x + 1)**(b/2)*(x + 1)**(-b/2)*assoc_legendre(n, b, x)* + gamma(-b + n + 1)/gamma(n + 1)) + assert jacobi(n, 0, 0, x) == legendre(n, x) + assert jacobi(n, S.Half, S.Half, x) == RisingFactorial( + Rational(3, 2), n)*chebyshevu(n, x)/factorial(n + 1) + assert jacobi(n, Rational(-1, 2), Rational(-1, 2), x) == RisingFactorial( + S.Half, n)*chebyshevt(n, x)/factorial(n) + + X = jacobi(n, a, b, x) + assert isinstance(X, jacobi) + + assert jacobi(n, a, b, -x) == (-1)**n*jacobi(n, b, a, x) + assert jacobi(n, a, b, 0) == 2**(-n)*gamma(a + n + 1)*hyper( + (-b - n, -n), (a + 1,), -1)/(factorial(n)*gamma(a + 1)) + assert jacobi(n, a, b, 1) == RisingFactorial(a + 1, n)/factorial(n) + + m = Symbol("m", positive=True) + assert jacobi(m, a, b, oo) == oo*RisingFactorial(a + b + m + 1, m) + assert unchanged(jacobi, n, a, b, oo) + + assert conjugate(jacobi(m, a, b, x)) == \ + jacobi(m, conjugate(a), conjugate(b), conjugate(x)) + + _k = Dummy('k') + assert diff(jacobi(n, a, b, x), n) == Derivative(jacobi(n, a, b, x), n) + assert diff(jacobi(n, a, b, x), a).dummy_eq(Sum((jacobi(n, a, b, x) + + (2*_k + a + b + 1)*RisingFactorial(_k + b + 1, -_k + n)*jacobi(_k, a, + b, x)/((-_k + n)*RisingFactorial(_k + a + b + 1, -_k + n)))/(_k + a + + b + n + 1), (_k, 0, n - 1))) + assert diff(jacobi(n, a, b, x), b).dummy_eq(Sum(((-1)**(-_k + n)*(2*_k + + a + b + 1)*RisingFactorial(_k + a + 1, -_k + n)*jacobi(_k, a, b, x)/ + ((-_k + n)*RisingFactorial(_k + a + b + 1, -_k + n)) + jacobi(n, a, + b, x))/(_k + a + b + n + 1), (_k, 0, n - 1))) + assert diff(jacobi(n, a, b, x), x) == \ + (a/2 + b/2 + n/2 + S.Half)*jacobi(n - 1, a + 1, b + 1, x) + + assert jacobi_normalized(n, a, b, x) == \ + (jacobi(n, a, b, x)/sqrt(2**(a + b + 1)*gamma(a + n + 1)*gamma(b + n + 1) + /((a + b + 2*n + 1)*factorial(n)*gamma(a + b + n + 1)))) + + raises(ValueError, lambda: jacobi(-2.1, a, b, x)) + raises(ValueError, lambda: jacobi(Dummy(positive=True, integer=True), 1, 2, oo)) + + assert jacobi(n, a, b, x).rewrite(Sum).dummy_eq(Sum((S.Half - x/2) + **_k*RisingFactorial(-n, _k)*RisingFactorial(_k + a + 1, -_k + n)* + RisingFactorial(a + b + n + 1, _k)/factorial(_k), (_k, 0, n))/factorial(n)) + assert jacobi(n, a, b, x).rewrite("polynomial").dummy_eq(Sum((S.Half - x/2) + **_k*RisingFactorial(-n, _k)*RisingFactorial(_k + a + 1, -_k + n)* + RisingFactorial(a + b + n + 1, _k)/factorial(_k), (_k, 0, n))/factorial(n)) + raises(ArgumentIndexError, lambda: jacobi(n, a, b, x).fdiff(5)) + + +def test_gegenbauer(): + n = Symbol("n") + a = Symbol("a") + + assert gegenbauer(0, a, x) == 1 + assert gegenbauer(1, a, x) == 2*a*x + assert gegenbauer(2, a, x) == -a + x**2*(2*a**2 + 2*a) + assert gegenbauer(3, a, x) == \ + x**3*(4*a**3/3 + 4*a**2 + a*Rational(8, 3)) + x*(-2*a**2 - 2*a) + + assert gegenbauer(-1, a, x) == 0 + assert gegenbauer(n, S.Half, x) == legendre(n, x) + assert gegenbauer(n, 1, x) == chebyshevu(n, x) + assert gegenbauer(n, -1, x) == 0 + + X = gegenbauer(n, a, x) + assert isinstance(X, gegenbauer) + + assert gegenbauer(n, a, -x) == (-1)**n*gegenbauer(n, a, x) + assert gegenbauer(n, a, 0) == 2**n*sqrt(pi) * \ + gamma(a + n/2)/(gamma(a)*gamma(-n/2 + S.Half)*gamma(n + 1)) + assert gegenbauer(n, a, 1) == gamma(2*a + n)/(gamma(2*a)*gamma(n + 1)) + + assert gegenbauer(n, Rational(3, 4), -1) is zoo + assert gegenbauer(n, Rational(1, 4), -1) == (sqrt(2)*cos(pi*(n + S.One/4))* + gamma(n + S.Half)/(sqrt(pi)*gamma(n + 1))) + + m = Symbol("m", positive=True) + assert gegenbauer(m, a, oo) == oo*RisingFactorial(a, m) + assert unchanged(gegenbauer, n, a, oo) + + assert conjugate(gegenbauer(n, a, x)) == gegenbauer(n, conjugate(a), conjugate(x)) + + _k = Dummy('k') + + assert diff(gegenbauer(n, a, x), n) == Derivative(gegenbauer(n, a, x), n) + assert diff(gegenbauer(n, a, x), a).dummy_eq(Sum((2*(-1)**(-_k + n) + 2)* + (_k + a)*gegenbauer(_k, a, x)/((-_k + n)*(_k + 2*a + n)) + ((2*_k + + 2)/((_k + 2*a)*(2*_k + 2*a + 1)) + 2/(_k + 2*a + n))*gegenbauer(n, a + , x), (_k, 0, n - 1))) + assert diff(gegenbauer(n, a, x), x) == 2*a*gegenbauer(n - 1, a + 1, x) + + assert gegenbauer(n, a, x).rewrite(Sum).dummy_eq( + Sum((-1)**_k*(2*x)**(-2*_k + n)*RisingFactorial(a, -_k + n) + /(factorial(_k)*factorial(-2*_k + n)), (_k, 0, floor(n/2)))) + assert gegenbauer(n, a, x).rewrite("polynomial").dummy_eq( + Sum((-1)**_k*(2*x)**(-2*_k + n)*RisingFactorial(a, -_k + n) + /(factorial(_k)*factorial(-2*_k + n)), (_k, 0, floor(n/2)))) + + raises(ArgumentIndexError, lambda: gegenbauer(n, a, x).fdiff(4)) + + +def test_legendre(): + assert legendre(0, x) == 1 + assert legendre(1, x) == x + assert legendre(2, x) == ((3*x**2 - 1)/2).expand() + assert legendre(3, x) == ((5*x**3 - 3*x)/2).expand() + assert legendre(4, x) == ((35*x**4 - 30*x**2 + 3)/8).expand() + assert legendre(5, x) == ((63*x**5 - 70*x**3 + 15*x)/8).expand() + assert legendre(6, x) == ((231*x**6 - 315*x**4 + 105*x**2 - 5)/16).expand() + + assert legendre(10, -1) == 1 + assert legendre(11, -1) == -1 + assert legendre(10, 1) == 1 + assert legendre(11, 1) == 1 + assert legendre(10, 0) != 0 + assert legendre(11, 0) == 0 + + assert legendre(-1, x) == 1 + k = Symbol('k') + assert legendre(5 - k, x).subs(k, 2) == ((5*x**3 - 3*x)/2).expand() + + assert roots(legendre(4, x), x) == { + sqrt(Rational(3, 7) - Rational(2, 35)*sqrt(30)): 1, + -sqrt(Rational(3, 7) - Rational(2, 35)*sqrt(30)): 1, + sqrt(Rational(3, 7) + Rational(2, 35)*sqrt(30)): 1, + -sqrt(Rational(3, 7) + Rational(2, 35)*sqrt(30)): 1, + } + + n = Symbol("n") + + X = legendre(n, x) + assert isinstance(X, legendre) + assert unchanged(legendre, n, x) + + assert legendre(n, 0) == sqrt(pi)/(gamma(S.Half - n/2)*gamma(n/2 + 1)) + assert legendre(n, 1) == 1 + assert legendre(n, oo) is oo + assert legendre(-n, x) == legendre(n - 1, x) + assert legendre(n, -x) == (-1)**n*legendre(n, x) + assert unchanged(legendre, -n + k, x) + + assert conjugate(legendre(n, x)) == legendre(n, conjugate(x)) + + assert diff(legendre(n, x), x) == \ + n*(x*legendre(n, x) - legendre(n - 1, x))/(x**2 - 1) + assert diff(legendre(n, x), n) == Derivative(legendre(n, x), n) + + _k = Dummy('k') + assert legendre(n, x).rewrite(Sum).dummy_eq(Sum((-1)**_k*(S.Half - + x/2)**_k*(x/2 + S.Half)**(-_k + n)*binomial(n, _k)**2, (_k, 0, n))) + assert legendre(n, x).rewrite("polynomial").dummy_eq(Sum((-1)**_k*(S.Half - + x/2)**_k*(x/2 + S.Half)**(-_k + n)*binomial(n, _k)**2, (_k, 0, n))) + raises(ArgumentIndexError, lambda: legendre(n, x).fdiff(1)) + raises(ArgumentIndexError, lambda: legendre(n, x).fdiff(3)) + + +def test_assoc_legendre(): + Plm = assoc_legendre + Q = sqrt(1 - x**2) + + assert Plm(0, 0, x) == 1 + assert Plm(1, 0, x) == x + assert Plm(1, 1, x) == -Q + assert Plm(2, 0, x) == (3*x**2 - 1)/2 + assert Plm(2, 1, x) == -3*x*Q + assert Plm(2, 2, x) == 3*Q**2 + assert Plm(3, 0, x) == (5*x**3 - 3*x)/2 + assert Plm(3, 1, x).expand() == (( 3*(1 - 5*x**2)/2 ).expand() * Q).expand() + assert Plm(3, 2, x) == 15*x * Q**2 + assert Plm(3, 3, x) == -15 * Q**3 + + # negative m + assert Plm(1, -1, x) == -Plm(1, 1, x)/2 + assert Plm(2, -2, x) == Plm(2, 2, x)/24 + assert Plm(2, -1, x) == -Plm(2, 1, x)/6 + assert Plm(3, -3, x) == -Plm(3, 3, x)/720 + assert Plm(3, -2, x) == Plm(3, 2, x)/120 + assert Plm(3, -1, x) == -Plm(3, 1, x)/12 + + n = Symbol("n") + m = Symbol("m") + X = Plm(n, m, x) + assert isinstance(X, assoc_legendre) + + assert Plm(n, 0, x) == legendre(n, x) + assert Plm(n, m, 0) == 2**m*sqrt(pi)/(gamma(-m/2 - n/2 + + S.Half)*gamma(-m/2 + n/2 + 1)) + + assert diff(Plm(m, n, x), x) == (m*x*assoc_legendre(m, n, x) - + (m + n)*assoc_legendre(m - 1, n, x))/(x**2 - 1) + + _k = Dummy('k') + assert Plm(m, n, x).rewrite(Sum).dummy_eq( + (1 - x**2)**(n/2)*Sum((-1)**_k*2**(-m)*x**(-2*_k + m - n)*factorial + (-2*_k + 2*m)/(factorial(_k)*factorial(-_k + m)*factorial(-2*_k + m + - n)), (_k, 0, floor(m/2 - n/2)))) + assert Plm(m, n, x).rewrite("polynomial").dummy_eq( + (1 - x**2)**(n/2)*Sum((-1)**_k*2**(-m)*x**(-2*_k + m - n)*factorial + (-2*_k + 2*m)/(factorial(_k)*factorial(-_k + m)*factorial(-2*_k + m + - n)), (_k, 0, floor(m/2 - n/2)))) + assert conjugate(assoc_legendre(n, m, x)) == \ + assoc_legendre(n, conjugate(m), conjugate(x)) + raises(ValueError, lambda: Plm(0, 1, x)) + raises(ValueError, lambda: Plm(-1, 1, x)) + raises(ArgumentIndexError, lambda: Plm(n, m, x).fdiff(1)) + raises(ArgumentIndexError, lambda: Plm(n, m, x).fdiff(2)) + raises(ArgumentIndexError, lambda: Plm(n, m, x).fdiff(4)) + + +def test_chebyshev(): + assert chebyshevt(0, x) == 1 + assert chebyshevt(1, x) == x + assert chebyshevt(2, x) == 2*x**2 - 1 + assert chebyshevt(3, x) == 4*x**3 - 3*x + + for n in range(1, 4): + for k in range(n): + z = chebyshevt_root(n, k) + assert chebyshevt(n, z) == 0 + raises(ValueError, lambda: chebyshevt_root(n, n)) + + for n in range(1, 4): + for k in range(n): + z = chebyshevu_root(n, k) + assert chebyshevu(n, z) == 0 + raises(ValueError, lambda: chebyshevu_root(n, n)) + + n = Symbol("n") + X = chebyshevt(n, x) + assert isinstance(X, chebyshevt) + assert unchanged(chebyshevt, n, x) + assert chebyshevt(n, -x) == (-1)**n*chebyshevt(n, x) + assert chebyshevt(-n, x) == chebyshevt(n, x) + + assert chebyshevt(n, 0) == cos(pi*n/2) + assert chebyshevt(n, 1) == 1 + assert chebyshevt(n, oo) is oo + + assert conjugate(chebyshevt(n, x)) == chebyshevt(n, conjugate(x)) + + assert diff(chebyshevt(n, x), x) == n*chebyshevu(n - 1, x) + + X = chebyshevu(n, x) + assert isinstance(X, chebyshevu) + + y = Symbol('y') + assert chebyshevu(n, -x) == (-1)**n*chebyshevu(n, x) + assert chebyshevu(-n, x) == -chebyshevu(n - 2, x) + assert unchanged(chebyshevu, -n + y, x) + + assert chebyshevu(n, 0) == cos(pi*n/2) + assert chebyshevu(n, 1) == n + 1 + assert chebyshevu(n, oo) is oo + + assert conjugate(chebyshevu(n, x)) == chebyshevu(n, conjugate(x)) + + assert diff(chebyshevu(n, x), x) == \ + (-x*chebyshevu(n, x) + (n + 1)*chebyshevt(n + 1, x))/(x**2 - 1) + + _k = Dummy('k') + assert chebyshevt(n, x).rewrite(Sum).dummy_eq(Sum(x**(-2*_k + n) + *(x**2 - 1)**_k*binomial(n, 2*_k), (_k, 0, floor(n/2)))) + assert chebyshevt(n, x).rewrite("polynomial").dummy_eq(Sum(x**(-2*_k + n) + *(x**2 - 1)**_k*binomial(n, 2*_k), (_k, 0, floor(n/2)))) + assert chebyshevu(n, x).rewrite(Sum).dummy_eq(Sum((-1)**_k*(2*x) + **(-2*_k + n)*factorial(-_k + n)/(factorial(_k)* + factorial(-2*_k + n)), (_k, 0, floor(n/2)))) + assert chebyshevu(n, x).rewrite("polynomial").dummy_eq(Sum((-1)**_k*(2*x) + **(-2*_k + n)*factorial(-_k + n)/(factorial(_k)* + factorial(-2*_k + n)), (_k, 0, floor(n/2)))) + raises(ArgumentIndexError, lambda: chebyshevt(n, x).fdiff(1)) + raises(ArgumentIndexError, lambda: chebyshevt(n, x).fdiff(3)) + raises(ArgumentIndexError, lambda: chebyshevu(n, x).fdiff(1)) + raises(ArgumentIndexError, lambda: chebyshevu(n, x).fdiff(3)) + + +def test_hermite(): + assert hermite(0, x) == 1 + assert hermite(1, x) == 2*x + assert hermite(2, x) == 4*x**2 - 2 + assert hermite(3, x) == 8*x**3 - 12*x + assert hermite(4, x) == 16*x**4 - 48*x**2 + 12 + assert hermite(6, x) == 64*x**6 - 480*x**4 + 720*x**2 - 120 + + n = Symbol("n") + assert unchanged(hermite, n, x) + assert hermite(n, -x) == (-1)**n*hermite(n, x) + assert unchanged(hermite, -n, x) + + assert hermite(n, 0) == 2**n*sqrt(pi)/gamma(S.Half - n/2) + assert hermite(n, oo) is oo + + assert conjugate(hermite(n, x)) == hermite(n, conjugate(x)) + + _k = Dummy('k') + assert hermite(n, x).rewrite(Sum).dummy_eq(factorial(n)*Sum((-1) + **_k*(2*x)**(-2*_k + n)/(factorial(_k)*factorial(-2*_k + n)), (_k, + 0, floor(n/2)))) + assert hermite(n, x).rewrite("polynomial").dummy_eq(factorial(n)*Sum((-1) + **_k*(2*x)**(-2*_k + n)/(factorial(_k)*factorial(-2*_k + n)), (_k, + 0, floor(n/2)))) + + assert diff(hermite(n, x), x) == 2*n*hermite(n - 1, x) + assert diff(hermite(n, x), n) == Derivative(hermite(n, x), n) + raises(ArgumentIndexError, lambda: hermite(n, x).fdiff(3)) + + assert hermite(n, x).rewrite(hermite_prob) == \ + sqrt(2)**n * hermite_prob(n, x*sqrt(2)) + + +def test_hermite_prob(): + assert hermite_prob(0, x) == 1 + assert hermite_prob(1, x) == x + assert hermite_prob(2, x) == x**2 - 1 + assert hermite_prob(3, x) == x**3 - 3*x + assert hermite_prob(4, x) == x**4 - 6*x**2 + 3 + assert hermite_prob(6, x) == x**6 - 15*x**4 + 45*x**2 - 15 + + n = Symbol("n") + assert unchanged(hermite_prob, n, x) + assert hermite_prob(n, -x) == (-1)**n*hermite_prob(n, x) + assert unchanged(hermite_prob, -n, x) + + assert hermite_prob(n, 0) == sqrt(pi)/gamma(S.Half - n/2) + assert hermite_prob(n, oo) is oo + + assert conjugate(hermite_prob(n, x)) == hermite_prob(n, conjugate(x)) + + _k = Dummy('k') + assert hermite_prob(n, x).rewrite(Sum).dummy_eq(factorial(n) * + Sum((-S.Half)**_k * x**(n-2*_k) / (factorial(_k) * factorial(n-2*_k)), + (_k, 0, floor(n/2)))) + assert hermite_prob(n, x).rewrite("polynomial").dummy_eq(factorial(n) * + Sum((-S.Half)**_k * x**(n-2*_k) / (factorial(_k) * factorial(n-2*_k)), + (_k, 0, floor(n/2)))) + + assert diff(hermite_prob(n, x), x) == n*hermite_prob(n-1, x) + assert diff(hermite_prob(n, x), n) == Derivative(hermite_prob(n, x), n) + raises(ArgumentIndexError, lambda: hermite_prob(n, x).fdiff(3)) + + assert hermite_prob(n, x).rewrite(hermite) == \ + sqrt(2)**(-n) * hermite(n, x/sqrt(2)) + + +def test_laguerre(): + n = Symbol("n") + m = Symbol("m", negative=True) + + # Laguerre polynomials: + assert laguerre(0, x) == 1 + assert laguerre(1, x) == -x + 1 + assert laguerre(2, x) == x**2/2 - 2*x + 1 + assert laguerre(3, x) == -x**3/6 + 3*x**2/2 - 3*x + 1 + assert laguerre(-2, x) == (x + 1)*exp(x) + + X = laguerre(n, x) + assert isinstance(X, laguerre) + + assert laguerre(n, 0) == 1 + assert laguerre(n, oo) == (-1)**n*oo + assert laguerre(n, -oo) is oo + + assert conjugate(laguerre(n, x)) == laguerre(n, conjugate(x)) + + _k = Dummy('k') + + assert laguerre(n, x).rewrite(Sum).dummy_eq( + Sum(x**_k*RisingFactorial(-n, _k)/factorial(_k)**2, (_k, 0, n))) + assert laguerre(n, x).rewrite("polynomial").dummy_eq( + Sum(x**_k*RisingFactorial(-n, _k)/factorial(_k)**2, (_k, 0, n))) + assert laguerre(m, x).rewrite(Sum).dummy_eq( + exp(x)*Sum((-x)**_k*RisingFactorial(m + 1, _k)/factorial(_k)**2, + (_k, 0, -m - 1))) + assert laguerre(m, x).rewrite("polynomial").dummy_eq( + exp(x)*Sum((-x)**_k*RisingFactorial(m + 1, _k)/factorial(_k)**2, + (_k, 0, -m - 1))) + + assert diff(laguerre(n, x), x) == -assoc_laguerre(n - 1, 1, x) + + k = Symbol('k') + assert laguerre(-n, x) == exp(x)*laguerre(n - 1, -x) + assert laguerre(-3, x) == exp(x)*laguerre(2, -x) + assert unchanged(laguerre, -n + k, x) + + raises(ValueError, lambda: laguerre(-2.1, x)) + raises(ValueError, lambda: laguerre(Rational(5, 2), x)) + raises(ArgumentIndexError, lambda: laguerre(n, x).fdiff(1)) + raises(ArgumentIndexError, lambda: laguerre(n, x).fdiff(3)) + + +def test_assoc_laguerre(): + n = Symbol("n") + m = Symbol("m") + alpha = Symbol("alpha") + + # generalized Laguerre polynomials: + assert assoc_laguerre(0, alpha, x) == 1 + assert assoc_laguerre(1, alpha, x) == -x + alpha + 1 + assert assoc_laguerre(2, alpha, x).expand() == \ + (x**2/2 - (alpha + 2)*x + (alpha + 2)*(alpha + 1)/2).expand() + assert assoc_laguerre(3, alpha, x).expand() == \ + (-x**3/6 + (alpha + 3)*x**2/2 - (alpha + 2)*(alpha + 3)*x/2 + + (alpha + 1)*(alpha + 2)*(alpha + 3)/6).expand() + + # Test the lowest 10 polynomials with laguerre_poly, to make sure it works: + for i in range(10): + assert assoc_laguerre(i, 0, x).expand() == laguerre_poly(i, x) + + X = assoc_laguerre(n, m, x) + assert isinstance(X, assoc_laguerre) + + assert assoc_laguerre(n, 0, x) == laguerre(n, x) + assert assoc_laguerre(n, alpha, 0) == binomial(alpha + n, alpha) + p = Symbol("p", positive=True) + assert assoc_laguerre(p, alpha, oo) == (-1)**p*oo + assert assoc_laguerre(p, alpha, -oo) is oo + + assert diff(assoc_laguerre(n, alpha, x), x) == \ + -assoc_laguerre(n - 1, alpha + 1, x) + _k = Dummy('k') + assert diff(assoc_laguerre(n, alpha, x), alpha).dummy_eq( + Sum(assoc_laguerre(_k, alpha, x)/(-alpha + n), (_k, 0, n - 1))) + + assert conjugate(assoc_laguerre(n, alpha, x)) == \ + assoc_laguerre(n, conjugate(alpha), conjugate(x)) + + assert assoc_laguerre(n, alpha, x).rewrite(Sum).dummy_eq( + gamma(alpha + n + 1)*Sum(x**_k*RisingFactorial(-n, _k)/ + (factorial(_k)*gamma(_k + alpha + 1)), (_k, 0, n))/factorial(n)) + assert assoc_laguerre(n, alpha, x).rewrite("polynomial").dummy_eq( + gamma(alpha + n + 1)*Sum(x**_k*RisingFactorial(-n, _k)/ + (factorial(_k)*gamma(_k + alpha + 1)), (_k, 0, n))/factorial(n)) + raises(ValueError, lambda: assoc_laguerre(-2.1, alpha, x)) + raises(ArgumentIndexError, lambda: assoc_laguerre(n, alpha, x).fdiff(1)) + raises(ArgumentIndexError, lambda: assoc_laguerre(n, alpha, x).fdiff(4)) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_spherical_harmonics.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_spherical_harmonics.py new file mode 100644 index 0000000000000000000000000000000000000000..2e0d4ffebabb62c13d3fc2996e8ba23866467720 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_spherical_harmonics.py @@ -0,0 +1,66 @@ +from sympy.core.function import diff +from sympy.core.numbers import (I, pi) +from sympy.core.symbol import Symbol +from sympy.functions.elementary.complexes import conjugate +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, cot, sin) +from sympy.functions.special.spherical_harmonics import Ynm, Znm, Ynm_c + + +def test_Ynm(): + # https://en.wikipedia.org/wiki/Spherical_harmonics + th, ph = Symbol("theta", real=True), Symbol("phi", real=True) + from sympy.abc import n,m + + assert Ynm(0, 0, th, ph).expand(func=True) == 1/(2*sqrt(pi)) + assert Ynm(1, -1, th, ph) == -exp(-2*I*ph)*Ynm(1, 1, th, ph) + assert Ynm(1, -1, th, ph).expand(func=True) == sqrt(6)*sin(th)*exp(-I*ph)/(4*sqrt(pi)) + assert Ynm(1, 0, th, ph).expand(func=True) == sqrt(3)*cos(th)/(2*sqrt(pi)) + assert Ynm(1, 1, th, ph).expand(func=True) == -sqrt(6)*sin(th)*exp(I*ph)/(4*sqrt(pi)) + assert Ynm(2, 0, th, ph).expand(func=True) == 3*sqrt(5)*cos(th)**2/(4*sqrt(pi)) - sqrt(5)/(4*sqrt(pi)) + assert Ynm(2, 1, th, ph).expand(func=True) == -sqrt(30)*sin(th)*exp(I*ph)*cos(th)/(4*sqrt(pi)) + assert Ynm(2, -2, th, ph).expand(func=True) == (-sqrt(30)*exp(-2*I*ph)*cos(th)**2/(8*sqrt(pi)) + + sqrt(30)*exp(-2*I*ph)/(8*sqrt(pi))) + assert Ynm(2, 2, th, ph).expand(func=True) == (-sqrt(30)*exp(2*I*ph)*cos(th)**2/(8*sqrt(pi)) + + sqrt(30)*exp(2*I*ph)/(8*sqrt(pi))) + + assert diff(Ynm(n, m, th, ph), th) == (m*cot(th)*Ynm(n, m, th, ph) + + sqrt((-m + n)*(m + n + 1))*exp(-I*ph)*Ynm(n, m + 1, th, ph)) + assert diff(Ynm(n, m, th, ph), ph) == I*m*Ynm(n, m, th, ph) + + assert conjugate(Ynm(n, m, th, ph)) == (-1)**(2*m)*exp(-2*I*m*ph)*Ynm(n, m, th, ph) + + assert Ynm(n, m, -th, ph) == Ynm(n, m, th, ph) + assert Ynm(n, m, th, -ph) == exp(-2*I*m*ph)*Ynm(n, m, th, ph) + assert Ynm(n, -m, th, ph) == (-1)**m*exp(-2*I*m*ph)*Ynm(n, m, th, ph) + + +def test_Ynm_c(): + th, ph = Symbol("theta", real=True), Symbol("phi", real=True) + from sympy.abc import n,m + + assert Ynm_c(n, m, th, ph) == (-1)**(2*m)*exp(-2*I*m*ph)*Ynm(n, m, th, ph) + + +def test_Znm(): + # https://en.wikipedia.org/wiki/Solid_harmonics#List_of_lowest_functions + th, ph = Symbol("theta", real=True), Symbol("phi", real=True) + + assert Znm(0, 0, th, ph) == Ynm(0, 0, th, ph) + assert Znm(1, -1, th, ph) == (-sqrt(2)*I*(Ynm(1, 1, th, ph) + - exp(-2*I*ph)*Ynm(1, 1, th, ph))/2) + assert Znm(1, 0, th, ph) == Ynm(1, 0, th, ph) + assert Znm(1, 1, th, ph) == (sqrt(2)*(Ynm(1, 1, th, ph) + + exp(-2*I*ph)*Ynm(1, 1, th, ph))/2) + assert Znm(0, 0, th, ph).expand(func=True) == 1/(2*sqrt(pi)) + assert Znm(1, -1, th, ph).expand(func=True) == (sqrt(3)*I*sin(th)*exp(I*ph)/(4*sqrt(pi)) + - sqrt(3)*I*sin(th)*exp(-I*ph)/(4*sqrt(pi))) + assert Znm(1, 0, th, ph).expand(func=True) == sqrt(3)*cos(th)/(2*sqrt(pi)) + assert Znm(1, 1, th, ph).expand(func=True) == (-sqrt(3)*sin(th)*exp(I*ph)/(4*sqrt(pi)) + - sqrt(3)*sin(th)*exp(-I*ph)/(4*sqrt(pi))) + assert Znm(2, -1, th, ph).expand(func=True) == (sqrt(15)*I*sin(th)*exp(I*ph)*cos(th)/(4*sqrt(pi)) + - sqrt(15)*I*sin(th)*exp(-I*ph)*cos(th)/(4*sqrt(pi))) + assert Znm(2, 0, th, ph).expand(func=True) == 3*sqrt(5)*cos(th)**2/(4*sqrt(pi)) - sqrt(5)/(4*sqrt(pi)) + assert Znm(2, 1, th, ph).expand(func=True) == (-sqrt(15)*sin(th)*exp(I*ph)*cos(th)/(4*sqrt(pi)) + - sqrt(15)*sin(th)*exp(-I*ph)*cos(th)/(4*sqrt(pi))) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_tensor_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_tensor_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..7d4f31c45ae0a60a6f72dc5551794b2110f5ab99 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_tensor_functions.py @@ -0,0 +1,145 @@ +from sympy.core.relational import Ne +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.functions.elementary.complexes import (adjoint, conjugate, transpose) +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.tensor_functions import (Eijk, KroneckerDelta, LeviCivita) + +from sympy.physics.secondquant import evaluate_deltas, F + +x, y = symbols('x y') + + +def test_levicivita(): + assert Eijk(1, 2, 3) == LeviCivita(1, 2, 3) + assert LeviCivita(1, 2, 3) == 1 + assert LeviCivita(int(1), int(2), int(3)) == 1 + assert LeviCivita(1, 3, 2) == -1 + assert LeviCivita(1, 2, 2) == 0 + i, j, k = symbols('i j k') + assert LeviCivita(i, j, k) == LeviCivita(i, j, k, evaluate=False) + assert LeviCivita(i, j, i) == 0 + assert LeviCivita(1, i, i) == 0 + assert LeviCivita(i, j, k).doit() == (j - i)*(k - i)*(k - j)/2 + assert LeviCivita(1, 2, 3, 1) == 0 + assert LeviCivita(4, 5, 1, 2, 3) == 1 + assert LeviCivita(4, 5, 2, 1, 3) == -1 + + assert LeviCivita(i, j, k).is_integer is True + + assert adjoint(LeviCivita(i, j, k)) == LeviCivita(i, j, k) + assert conjugate(LeviCivita(i, j, k)) == LeviCivita(i, j, k) + assert transpose(LeviCivita(i, j, k)) == LeviCivita(i, j, k) + + +def test_kronecker_delta(): + i, j = symbols('i j') + k = Symbol('k', nonzero=True) + assert KroneckerDelta(1, 1) == 1 + assert KroneckerDelta(1, 2) == 0 + assert KroneckerDelta(k, 0) == 0 + assert KroneckerDelta(x, x) == 1 + assert KroneckerDelta(x**2 - y**2, x**2 - y**2) == 1 + assert KroneckerDelta(i, i) == 1 + assert KroneckerDelta(i, i + 1) == 0 + assert KroneckerDelta(0, 0) == 1 + assert KroneckerDelta(0, 1) == 0 + assert KroneckerDelta(i + k, i) == 0 + assert KroneckerDelta(i + k, i + k) == 1 + assert KroneckerDelta(i + k, i + 1 + k) == 0 + assert KroneckerDelta(i, j).subs({"i": 1, "j": 0}) == 0 + assert KroneckerDelta(i, j).subs({"i": 3, "j": 3}) == 1 + + assert KroneckerDelta(i, j)**0 == 1 + for n in range(1, 10): + assert KroneckerDelta(i, j)**n == KroneckerDelta(i, j) + assert KroneckerDelta(i, j)**-n == 1/KroneckerDelta(i, j) + + assert KroneckerDelta(i, j).is_integer is True + + assert adjoint(KroneckerDelta(i, j)) == KroneckerDelta(i, j) + assert conjugate(KroneckerDelta(i, j)) == KroneckerDelta(i, j) + assert transpose(KroneckerDelta(i, j)) == KroneckerDelta(i, j) + # to test if canonical + assert (KroneckerDelta(i, j) == KroneckerDelta(j, i)) == True + + assert KroneckerDelta(i, j).rewrite(Piecewise) == Piecewise((0, Ne(i, j)), (1, True)) + + # Tests with range: + assert KroneckerDelta(i, j, (0, i)).args == (i, j, (0, i)) + assert KroneckerDelta(i, j, (-j, i)).delta_range == (-j, i) + + # If index is out of range, return zero: + assert KroneckerDelta(i, j, (0, i-1)) == 0 + assert KroneckerDelta(-1, j, (0, i-1)) == 0 + assert KroneckerDelta(j, -1, (0, i-1)) == 0 + assert KroneckerDelta(j, i, (0, i-1)) == 0 + + +def test_kronecker_delta_secondquant(): + """secondquant-specific methods""" + D = KroneckerDelta + i, j, v, w = symbols('i j v w', below_fermi=True, cls=Dummy) + a, b, t, u = symbols('a b t u', above_fermi=True, cls=Dummy) + p, q, r, s = symbols('p q r s', cls=Dummy) + + assert D(i, a) == 0 + assert D(i, t) == 0 + + assert D(i, j).is_above_fermi is False + assert D(a, b).is_above_fermi is True + assert D(p, q).is_above_fermi is True + assert D(i, q).is_above_fermi is False + assert D(q, i).is_above_fermi is False + assert D(q, v).is_above_fermi is False + assert D(a, q).is_above_fermi is True + + assert D(i, j).is_below_fermi is True + assert D(a, b).is_below_fermi is False + assert D(p, q).is_below_fermi is True + assert D(p, j).is_below_fermi is True + assert D(q, b).is_below_fermi is False + + assert D(i, j).is_only_above_fermi is False + assert D(a, b).is_only_above_fermi is True + assert D(p, q).is_only_above_fermi is False + assert D(i, q).is_only_above_fermi is False + assert D(q, i).is_only_above_fermi is False + assert D(a, q).is_only_above_fermi is True + + assert D(i, j).is_only_below_fermi is True + assert D(a, b).is_only_below_fermi is False + assert D(p, q).is_only_below_fermi is False + assert D(p, j).is_only_below_fermi is True + assert D(q, b).is_only_below_fermi is False + + assert not D(i, q).indices_contain_equal_information + assert not D(a, q).indices_contain_equal_information + assert D(p, q).indices_contain_equal_information + assert D(a, b).indices_contain_equal_information + assert D(i, j).indices_contain_equal_information + + assert D(q, b).preferred_index == b + assert D(q, b).killable_index == q + assert D(q, t).preferred_index == t + assert D(q, t).killable_index == q + assert D(q, i).preferred_index == i + assert D(q, i).killable_index == q + assert D(q, v).preferred_index == v + assert D(q, v).killable_index == q + assert D(q, p).preferred_index == p + assert D(q, p).killable_index == q + + EV = evaluate_deltas + assert EV(D(a, q)*F(q)) == F(a) + assert EV(D(i, q)*F(q)) == F(i) + assert EV(D(a, q)*F(a)) == D(a, q)*F(a) + assert EV(D(i, q)*F(i)) == D(i, q)*F(i) + assert EV(D(a, b)*F(a)) == F(b) + assert EV(D(a, b)*F(b)) == F(a) + assert EV(D(i, j)*F(i)) == F(j) + assert EV(D(i, j)*F(j)) == F(i) + assert EV(D(p, q)*F(q)) == F(p) + assert EV(D(p, q)*F(p)) == F(q) + assert EV(D(p, j)*D(p, i)*F(i)) == F(j) + assert EV(D(p, j)*D(p, i)*F(j)) == F(i) + assert EV(D(p, q)*D(p, i))*F(i) == D(q, i)*F(i) diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_zeta_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_zeta_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..c2083b0b6e8cb38fde17fb1ede2a34be6338b1dc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/tests/test_zeta_functions.py @@ -0,0 +1,286 @@ +from sympy.concrete.summations import Sum +from sympy.core.function import expand_func +from sympy.core.numbers import (Float, I, Rational, nan, oo, pi, zoo) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.complexes import (Abs, polar_lift) +from sympy.functions.elementary.exponential import (exp, exp_polar, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.special.zeta_functions import (dirichlet_eta, lerchphi, polylog, riemann_xi, stieltjes, zeta) +from sympy.series.order import O +from sympy.core.function import ArgumentIndexError +from sympy.functions.combinatorial.numbers import bernoulli, factorial, genocchi, harmonic +from sympy.testing.pytest import raises +from sympy.core.random import (test_derivative_numerically as td, + random_complex_number as randcplx, verify_numerically) + +x = Symbol('x') +a = Symbol('a') +b = Symbol('b', negative=True) +z = Symbol('z') +s = Symbol('s') + + +def test_zeta_eval(): + + assert zeta(nan) is nan + assert zeta(x, nan) is nan + + assert zeta(0) == Rational(-1, 2) + assert zeta(0, x) == S.Half - x + assert zeta(0, b) == S.Half - b + + assert zeta(1) is zoo + assert zeta(1, 2) is zoo + assert zeta(1, -7) is zoo + assert zeta(1, x) is zoo + + assert zeta(2, 1) == pi**2/6 + assert zeta(3, 1) == zeta(3) + + assert zeta(2) == pi**2/6 + assert zeta(4) == pi**4/90 + assert zeta(6) == pi**6/945 + + assert zeta(4, 3) == pi**4/90 - Rational(17, 16) + assert zeta(7, 4) == zeta(7) - Rational(282251, 279936) + assert zeta(S.Half, 2).func == zeta + assert expand_func(zeta(S.Half, 2)) == zeta(S.Half) - 1 + assert zeta(x, 3).func == zeta + assert expand_func(zeta(x, 3)) == zeta(x) - 1 - 1/2**x + + assert zeta(2, 0) is nan + assert zeta(3, -1) is nan + assert zeta(4, -2) is nan + + assert zeta(oo) == 1 + + assert zeta(-1) == Rational(-1, 12) + assert zeta(-2) == 0 + assert zeta(-3) == Rational(1, 120) + assert zeta(-4) == 0 + assert zeta(-5) == Rational(-1, 252) + + assert zeta(-1, 3) == Rational(-37, 12) + assert zeta(-1, 7) == Rational(-253, 12) + assert zeta(-1, -4) == Rational(-121, 12) + assert zeta(-1, -9) == Rational(-541, 12) + + assert zeta(-4, 3) == -17 + assert zeta(-4, -8) == 8772 + + assert zeta(0, 1) == Rational(-1, 2) + assert zeta(0, -1) == Rational(3, 2) + + assert zeta(0, 2) == Rational(-3, 2) + assert zeta(0, -2) == Rational(5, 2) + + assert zeta( + 3).evalf(20).epsilon_eq(Float("1.2020569031595942854", 20), 1e-19) + + +def test_zeta_series(): + assert zeta(x, a).series(a, z, 2) == \ + zeta(x, z) - x*(a-z)*zeta(x+1, z) + O((a-z)**2, (a, z)) + + +def test_dirichlet_eta_eval(): + assert dirichlet_eta(0) == S.Half + assert dirichlet_eta(-1) == Rational(1, 4) + assert dirichlet_eta(1) == log(2) + assert dirichlet_eta(1, S.Half).simplify() == pi/2 + assert dirichlet_eta(1, 2) == 1 - log(2) + assert dirichlet_eta(2) == pi**2/12 + assert dirichlet_eta(4) == pi**4*Rational(7, 720) + assert str(dirichlet_eta(I).evalf(n=10)) == '0.5325931818 + 0.2293848577*I' + assert str(dirichlet_eta(I, I).evalf(n=10)) == '3.462349253 + 0.220285771*I' + + +def test_riemann_xi_eval(): + assert riemann_xi(2) == pi/6 + assert riemann_xi(0) == Rational(1, 2) + assert riemann_xi(1) == Rational(1, 2) + assert riemann_xi(3).rewrite(zeta) == 3*zeta(3)/(2*pi) + assert riemann_xi(4) == pi**2/15 + + +def test_rewriting(): + from sympy.functions.elementary.piecewise import Piecewise + assert isinstance(dirichlet_eta(x).rewrite(zeta), Piecewise) + assert isinstance(dirichlet_eta(x).rewrite(genocchi), Piecewise) + assert zeta(x).rewrite(dirichlet_eta) == dirichlet_eta(x)/(1 - 2**(1 - x)) + assert zeta(x).rewrite(dirichlet_eta, a=2) == zeta(x) + assert verify_numerically(dirichlet_eta(x), dirichlet_eta(x).rewrite(zeta), x) + assert verify_numerically(dirichlet_eta(x), dirichlet_eta(x).rewrite(genocchi), x) + assert verify_numerically(zeta(x), zeta(x).rewrite(dirichlet_eta), x) + + assert zeta(x, a).rewrite(lerchphi) == lerchphi(1, x, a) + assert polylog(s, z).rewrite(lerchphi) == lerchphi(z, s, 1)*z + + assert lerchphi(1, x, a).rewrite(zeta) == zeta(x, a) + assert z*lerchphi(z, s, 1).rewrite(polylog) == polylog(s, z) + + +def test_derivatives(): + from sympy.core.function import Derivative + assert zeta(x, a).diff(x) == Derivative(zeta(x, a), x) + assert zeta(x, a).diff(a) == -x*zeta(x + 1, a) + assert lerchphi( + z, s, a).diff(z) == (lerchphi(z, s - 1, a) - a*lerchphi(z, s, a))/z + assert lerchphi(z, s, a).diff(a) == -s*lerchphi(z, s + 1, a) + assert polylog(s, z).diff(z) == polylog(s - 1, z)/z + + b = randcplx() + c = randcplx() + assert td(zeta(b, x), x) + assert td(polylog(b, z), z) + assert td(lerchphi(c, b, x), x) + assert td(lerchphi(x, b, c), x) + raises(ArgumentIndexError, lambda: lerchphi(c, b, x).fdiff(2)) + raises(ArgumentIndexError, lambda: lerchphi(c, b, x).fdiff(4)) + raises(ArgumentIndexError, lambda: polylog(b, z).fdiff(1)) + raises(ArgumentIndexError, lambda: polylog(b, z).fdiff(3)) + + +def myexpand(func, target): + expanded = expand_func(func) + if target is not None: + return expanded == target + if expanded == func: # it didn't expand + return False + + # check to see that the expanded and original evaluate to the same value + subs = {} + for a in func.free_symbols: + subs[a] = randcplx() + return abs(func.subs(subs).n() + - expanded.replace(exp_polar, exp).subs(subs).n()) < 1e-10 + + +def test_polylog_expansion(): + assert polylog(s, 0) == 0 + assert polylog(s, 1) == zeta(s) + assert polylog(s, -1) == -dirichlet_eta(s) + assert polylog(s, exp_polar(I*pi*Rational(4, 3))) == polylog(s, exp(I*pi*Rational(4, 3))) + assert polylog(s, exp_polar(I*pi)/3) == polylog(s, exp(I*pi)/3) + + assert myexpand(polylog(1, z), -log(1 - z)) + assert myexpand(polylog(0, z), z/(1 - z)) + assert myexpand(polylog(-1, z), z/(1 - z)**2) + assert ((1-z)**3 * expand_func(polylog(-2, z))).simplify() == z*(1 + z) + assert myexpand(polylog(-5, z), None) + + +def test_polylog_series(): + assert polylog(1, z).series(z, n=5) == z + z**2/2 + z**3/3 + z**4/4 + O(z**5) + assert polylog(1, sqrt(z)).series(z, n=3) == z/2 + z**2/4 + sqrt(z)\ + + z**(S(3)/2)/3 + z**(S(5)/2)/5 + O(z**3) + + # https://github.com/sympy/sympy/issues/9497 + assert polylog(S(3)/2, -z).series(z, 0, 5) == -z + sqrt(2)*z**2/4\ + - sqrt(3)*z**3/9 + z**4/8 + O(z**5) + + +def test_issue_8404(): + i = Symbol('i', integer=True) + assert Abs(Sum(1/(3*i + 1)**2, (i, 0, S.Infinity)).doit().n(4) + - 1.122) < 0.001 + + +def test_polylog_values(): + assert polylog(2, 2) == pi**2/4 - I*pi*log(2) + assert polylog(2, S.Half) == pi**2/12 - log(2)**2/2 + for z in [S.Half, 2, (sqrt(5)-1)/2, -(sqrt(5)-1)/2, -(sqrt(5)+1)/2, (3-sqrt(5))/2]: + assert Abs(polylog(2, z).evalf() - polylog(2, z, evaluate=False).evalf()) < 1e-15 + z = Symbol("z") + for s in [-1, 0]: + for _ in range(10): + assert verify_numerically(polylog(s, z), polylog(s, z, evaluate=False), + z, a=-3, b=-2, c=S.Half, d=2) + assert verify_numerically(polylog(s, z), polylog(s, z, evaluate=False), + z, a=2, b=-2, c=5, d=2) + + from sympy.integrals.integrals import Integral + assert polylog(0, Integral(1, (x, 0, 1))) == -S.Half + + +def test_lerchphi_expansion(): + assert myexpand(lerchphi(1, s, a), zeta(s, a)) + assert myexpand(lerchphi(z, s, 1), polylog(s, z)/z) + + # direct summation + assert myexpand(lerchphi(z, -1, a), a/(1 - z) + z/(1 - z)**2) + assert myexpand(lerchphi(z, -3, a), None) + # polylog reduction + assert myexpand(lerchphi(z, s, S.Half), + 2**(s - 1)*(polylog(s, sqrt(z))/sqrt(z) + - polylog(s, polar_lift(-1)*sqrt(z))/sqrt(z))) + assert myexpand(lerchphi(z, s, 2), -1/z + polylog(s, z)/z**2) + assert myexpand(lerchphi(z, s, Rational(3, 2)), None) + assert myexpand(lerchphi(z, s, Rational(7, 3)), None) + assert myexpand(lerchphi(z, s, Rational(-1, 3)), None) + assert myexpand(lerchphi(z, s, Rational(-5, 2)), None) + + # hurwitz zeta reduction + assert myexpand(lerchphi(-1, s, a), + 2**(-s)*zeta(s, a/2) - 2**(-s)*zeta(s, (a + 1)/2)) + assert myexpand(lerchphi(I, s, a), None) + assert myexpand(lerchphi(-I, s, a), None) + assert myexpand(lerchphi(exp(I*pi*Rational(2, 5)), s, a), None) + + +def test_stieltjes(): + assert isinstance(stieltjes(x), stieltjes) + assert isinstance(stieltjes(x, a), stieltjes) + + # Zero'th constant EulerGamma + assert stieltjes(0) == S.EulerGamma + assert stieltjes(0, 1) == S.EulerGamma + + # Not defined + assert stieltjes(nan) is nan + assert stieltjes(0, nan) is nan + assert stieltjes(-1) is S.ComplexInfinity + assert stieltjes(1.5) is S.ComplexInfinity + assert stieltjes(z, 0) is S.ComplexInfinity + assert stieltjes(z, -1) is S.ComplexInfinity + + +def test_stieltjes_evalf(): + assert abs(stieltjes(0).evalf() - 0.577215664) < 1E-9 + assert abs(stieltjes(0, 0.5).evalf() - 1.963510026) < 1E-9 + assert abs(stieltjes(1, 2).evalf() + 0.072815845) < 1E-9 + + +def test_issue_10475(): + a = Symbol('a', extended_real=True) + b = Symbol('b', extended_positive=True) + s = Symbol('s', zero=False) + + assert zeta(2 + I).is_finite + assert zeta(1).is_finite is False + assert zeta(x).is_finite is None + assert zeta(x + I).is_finite is None + assert zeta(a).is_finite is None + assert zeta(b).is_finite is None + assert zeta(-b).is_finite is True + assert zeta(b**2 - 2*b + 1).is_finite is None + assert zeta(a + I).is_finite is True + assert zeta(b + 1).is_finite is True + assert zeta(s + 1).is_finite is True + + +def test_issue_14177(): + n = Symbol('n', nonnegative=True, integer=True) + + assert zeta(-n).rewrite(bernoulli) == bernoulli(n+1) / (-n-1) + assert zeta(-n, a).rewrite(bernoulli) == bernoulli(n+1, a) / (-n-1) + z2n = -(2*I*pi)**(2*n)*bernoulli(2*n) / (2*factorial(2*n)) + assert zeta(2*n).rewrite(bernoulli) == z2n + assert expand_func(zeta(s, n+1)) == zeta(s) - harmonic(n, s) + assert expand_func(zeta(-b, -n)) is nan + assert expand_func(zeta(-b, n)) == zeta(-b, n) + + n = Symbol('n') + + assert zeta(2*n) == zeta(2*n) # As sign of z (= 2*n) is not determined diff --git a/venv/lib/python3.11/site-packages/sympy/functions/special/zeta_functions.py b/venv/lib/python3.11/site-packages/sympy/functions/special/zeta_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..8f410f0f1086de91490c714cd3becf11df9ab189 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/functions/special/zeta_functions.py @@ -0,0 +1,786 @@ +""" Riemann zeta and related function. """ + +from sympy.core.add import Add +from sympy.core.cache import cacheit +from sympy.core.function import ArgumentIndexError, expand_mul, DefinedFunction +from sympy.core.logic import fuzzy_not +from sympy.core.numbers import pi, I, Integer +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import Dummy +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.numbers import bernoulli, factorial, genocchi, harmonic +from sympy.functions.elementary.complexes import re, unpolarify, Abs, polar_lift +from sympy.functions.elementary.exponential import log, exp_polar, exp +from sympy.functions.elementary.integers import ceiling, floor +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.polys.polytools import Poly + +############################################################################### +###################### LERCH TRANSCENDENT ##################################### +############################################################################### + + +class lerchphi(DefinedFunction): + r""" + Lerch transcendent (Lerch phi function). + + Explanation + =========== + + For $\operatorname{Re}(a) > 0$, $|z| < 1$ and $s \in \mathbb{C}$, the + Lerch transcendent is defined as + + .. math :: \Phi(z, s, a) = \sum_{n=0}^\infty \frac{z^n}{(n + a)^s}, + + where the standard branch of the argument is used for $n + a$, + and by analytic continuation for other values of the parameters. + + A commonly used related function is the Lerch zeta function, defined by + + .. math:: L(q, s, a) = \Phi(e^{2\pi i q}, s, a). + + **Analytic Continuation and Branching Behavior** + + It can be shown that + + .. math:: \Phi(z, s, a) = z\Phi(z, s, a+1) + a^{-s}. + + This provides the analytic continuation to $\operatorname{Re}(a) \le 0$. + + Assume now $\operatorname{Re}(a) > 0$. The integral representation + + .. math:: \Phi_0(z, s, a) = \int_0^\infty \frac{t^{s-1} e^{-at}}{1 - ze^{-t}} + \frac{\mathrm{d}t}{\Gamma(s)} + + provides an analytic continuation to $\mathbb{C} - [1, \infty)$. + Finally, for $x \in (1, \infty)$ we find + + .. math:: \lim_{\epsilon \to 0^+} \Phi_0(x + i\epsilon, s, a) + -\lim_{\epsilon \to 0^+} \Phi_0(x - i\epsilon, s, a) + = \frac{2\pi i \log^{s-1}{x}}{x^a \Gamma(s)}, + + using the standard branch for both $\log{x}$ and + $\log{\log{x}}$ (a branch of $\log{\log{x}}$ is needed to + evaluate $\log{x}^{s-1}$). + This concludes the analytic continuation. The Lerch transcendent is thus + branched at $z \in \{0, 1, \infty\}$ and + $a \in \mathbb{Z}_{\le 0}$. For fixed $z, a$ outside these + branch points, it is an entire function of $s$. + + Examples + ======== + + The Lerch transcendent is a fairly general function, for this reason it does + not automatically evaluate to simpler functions. Use ``expand_func()`` to + achieve this. + + If $z=1$, the Lerch transcendent reduces to the Hurwitz zeta function: + + >>> from sympy import lerchphi, expand_func + >>> from sympy.abc import z, s, a + >>> expand_func(lerchphi(1, s, a)) + zeta(s, a) + + More generally, if $z$ is a root of unity, the Lerch transcendent + reduces to a sum of Hurwitz zeta functions: + + >>> expand_func(lerchphi(-1, s, a)) + zeta(s, a/2)/2**s - zeta(s, a/2 + 1/2)/2**s + + If $a=1$, the Lerch transcendent reduces to the polylogarithm: + + >>> expand_func(lerchphi(z, s, 1)) + polylog(s, z)/z + + More generally, if $a$ is rational, the Lerch transcendent reduces + to a sum of polylogarithms: + + >>> from sympy import S + >>> expand_func(lerchphi(z, s, S(1)/2)) + 2**(s - 1)*(polylog(s, sqrt(z))/sqrt(z) - + polylog(s, sqrt(z)*exp_polar(I*pi))/sqrt(z)) + >>> expand_func(lerchphi(z, s, S(3)/2)) + -2**s/z + 2**(s - 1)*(polylog(s, sqrt(z))/sqrt(z) - + polylog(s, sqrt(z)*exp_polar(I*pi))/sqrt(z))/z + + The derivatives with respect to $z$ and $a$ can be computed in + closed form: + + >>> lerchphi(z, s, a).diff(z) + (-a*lerchphi(z, s, a) + lerchphi(z, s - 1, a))/z + >>> lerchphi(z, s, a).diff(a) + -s*lerchphi(z, s + 1, a) + + See Also + ======== + + polylog, zeta + + References + ========== + + .. [1] Bateman, H.; Erdelyi, A. (1953), Higher Transcendental Functions, + Vol. I, New York: McGraw-Hill. Section 1.11. + .. [2] https://dlmf.nist.gov/25.14 + .. [3] https://en.wikipedia.org/wiki/Lerch_transcendent + + """ + + def _eval_expand_func(self, **hints): + z, s, a = self.args + if z == 1: + return zeta(s, a) + if s.is_Integer and s <= 0: + t = Dummy('t') + p = Poly((t + a)**(-s), t) + start = 1/(1 - t) + res = S.Zero + for c in reversed(p.all_coeffs()): + res += c*start + start = t*start.diff(t) + return res.subs(t, z) + + if a.is_Rational: + # See section 18 of + # Kelly B. Roach. Hypergeometric Function Representations. + # In: Proceedings of the 1997 International Symposium on Symbolic and + # Algebraic Computation, pages 205-211, New York, 1997. ACM. + # TODO should something be polarified here? + add = S.Zero + mul = S.One + # First reduce a to the interaval (0, 1] + if a > 1: + n = floor(a) + if n == a: + n -= 1 + a -= n + mul = z**(-n) + add = Add(*[-z**(k - n)/(a + k)**s for k in range(n)]) + elif a <= 0: + n = floor(-a) + 1 + a += n + mul = z**n + add = Add(*[z**(n - 1 - k)/(a - k - 1)**s for k in range(n)]) + + m, n = S([a.p, a.q]) + zet = exp_polar(2*pi*I/n) + root = z**(1/n) + up_zet = unpolarify(zet) + addargs = [] + for k in range(n): + p = polylog(s, zet**k*root) + if isinstance(p, polylog): + p = p._eval_expand_func(**hints) + addargs.append(p/(up_zet**k*root)**m) + return add + mul*n**(s - 1)*Add(*addargs) + + # TODO use minpoly instead of ad-hoc methods when issue 5888 is fixed + if isinstance(z, exp) and (z.args[0]/(pi*I)).is_Rational or z in [-1, I, -I]: + # TODO reference? + if z == -1: + p, q = S([1, 2]) + elif z == I: + p, q = S([1, 4]) + elif z == -I: + p, q = S([-1, 4]) + else: + arg = z.args[0]/(2*pi*I) + p, q = S([arg.p, arg.q]) + return Add(*[exp(2*pi*I*k*p/q)/q**s*zeta(s, (k + a)/q) + for k in range(q)]) + + return lerchphi(z, s, a) + + def fdiff(self, argindex=1): + z, s, a = self.args + if argindex == 3: + return -s*lerchphi(z, s + 1, a) + elif argindex == 1: + return (lerchphi(z, s - 1, a) - a*lerchphi(z, s, a))/z + else: + raise ArgumentIndexError + + def _eval_rewrite_helper(self, target): + res = self._eval_expand_func() + if res.has(target): + return res + else: + return self + + def _eval_rewrite_as_zeta(self, z, s, a, **kwargs): + return self._eval_rewrite_helper(zeta) + + def _eval_rewrite_as_polylog(self, z, s, a, **kwargs): + return self._eval_rewrite_helper(polylog) + +############################################################################### +###################### POLYLOGARITHM ########################################## +############################################################################### + + +class polylog(DefinedFunction): + r""" + Polylogarithm function. + + Explanation + =========== + + For $|z| < 1$ and $s \in \mathbb{C}$, the polylogarithm is + defined by + + .. math:: \operatorname{Li}_s(z) = \sum_{n=1}^\infty \frac{z^n}{n^s}, + + where the standard branch of the argument is used for $n$. It admits + an analytic continuation which is branched at $z=1$ (notably not on the + sheet of initial definition), $z=0$ and $z=\infty$. + + The name polylogarithm comes from the fact that for $s=1$, the + polylogarithm is related to the ordinary logarithm (see examples), and that + + .. math:: \operatorname{Li}_{s+1}(z) = + \int_0^z \frac{\operatorname{Li}_s(t)}{t} \mathrm{d}t. + + The polylogarithm is a special case of the Lerch transcendent: + + .. math:: \operatorname{Li}_{s}(z) = z \Phi(z, s, 1). + + Examples + ======== + + For $z \in \{0, 1, -1\}$, the polylogarithm is automatically expressed + using other functions: + + >>> from sympy import polylog + >>> from sympy.abc import s + >>> polylog(s, 0) + 0 + >>> polylog(s, 1) + zeta(s) + >>> polylog(s, -1) + -dirichlet_eta(s) + + If $s$ is a negative integer, $0$ or $1$, the polylogarithm can be + expressed using elementary functions. This can be done using + ``expand_func()``: + + >>> from sympy import expand_func + >>> from sympy.abc import z + >>> expand_func(polylog(1, z)) + -log(1 - z) + >>> expand_func(polylog(0, z)) + z/(1 - z) + + The derivative with respect to $z$ can be computed in closed form: + + >>> polylog(s, z).diff(z) + polylog(s - 1, z)/z + + The polylogarithm can be expressed in terms of the lerch transcendent: + + >>> from sympy import lerchphi + >>> polylog(s, z).rewrite(lerchphi) + z*lerchphi(z, s, 1) + + See Also + ======== + + zeta, lerchphi + + """ + + @classmethod + def eval(cls, s, z): + if z.is_number: + if z is S.One: + return zeta(s) + elif z is S.NegativeOne: + return -dirichlet_eta(s) + elif z is S.Zero: + return S.Zero + elif s == 2: + dilogtable = _dilogtable() + if z in dilogtable: + return dilogtable[z] + + if z.is_zero: + return S.Zero + + # Make an effort to determine if z is 1 to avoid replacing into + # expression with singularity + zone = z.equals(S.One) + + if zone: + return zeta(s) + elif zone is False: + # For s = 0 or -1 use explicit formulas to evaluate, but + # automatically expanding polylog(1, z) to -log(1-z) seems + # undesirable for summation methods based on hypergeometric + # functions + if s is S.Zero: + return z/(1 - z) + elif s is S.NegativeOne: + return z/(1 - z)**2 + if s.is_zero: + return z/(1 - z) + + # polylog is branched, but not over the unit disk + if z.has(exp_polar, polar_lift) and (zone or (Abs(z) <= S.One) == True): + return cls(s, unpolarify(z)) + + def fdiff(self, argindex=1): + s, z = self.args + if argindex == 2: + return polylog(s - 1, z)/z + raise ArgumentIndexError + + def _eval_rewrite_as_lerchphi(self, s, z, **kwargs): + return z*lerchphi(z, s, 1) + + def _eval_expand_func(self, **hints): + s, z = self.args + if s == 1: + return -log(1 - z) + if s.is_Integer and s <= 0: + u = Dummy('u') + start = u/(1 - u) + for _ in range(-s): + start = u*start.diff(u) + return expand_mul(start).subs(u, z) + return polylog(s, z) + + def _eval_is_zero(self): + z = self.args[1] + if z.is_zero: + return True + + def _eval_nseries(self, x, n, logx, cdir=0): + from sympy.series.order import Order + nu, z = self.args + + z0 = z.subs(x, 0) + if z0 is S.NaN: + z0 = z.limit(x, 0, dir='-' if re(cdir).is_negative else '+') + + if z0.is_zero: + # In case of powers less than 1, number of terms need to be computed + # separately to avoid repeated callings of _eval_nseries with wrong n + try: + _, exp = z.leadterm(x) + except (ValueError, NotImplementedError): + return self + + if exp.is_positive: + newn = ceiling(n/exp) + o = Order(x**n, x) + r = z._eval_nseries(x, n, logx, cdir).removeO() + if r is S.Zero: + return o + + term = r + s = [term] + for k in range(2, newn): + term *= r + s.append(term/k**nu) + return Add(*s) + o + + return super(polylog, self)._eval_nseries(x, n, logx, cdir) + +############################################################################### +###################### HURWITZ GENERALIZED ZETA FUNCTION ###################### +############################################################################### + + +class zeta(DefinedFunction): + r""" + Hurwitz zeta function (or Riemann zeta function). + + Explanation + =========== + + For $\operatorname{Re}(a) > 0$ and $\operatorname{Re}(s) > 1$, this + function is defined as + + .. math:: \zeta(s, a) = \sum_{n=0}^\infty \frac{1}{(n + a)^s}, + + where the standard choice of argument for $n + a$ is used. For fixed + $a$ not a nonpositive integer the Hurwitz zeta function admits a + meromorphic continuation to all of $\mathbb{C}$; it is an unbranched + function with a simple pole at $s = 1$. + + The Hurwitz zeta function is a special case of the Lerch transcendent: + + .. math:: \zeta(s, a) = \Phi(1, s, a). + + This formula defines an analytic continuation for all possible values of + $s$ and $a$ (also $\operatorname{Re}(a) < 0$), see the documentation of + :class:`lerchphi` for a description of the branching behavior. + + If no value is passed for $a$ a default value of $a = 1$ is assumed, + yielding the Riemann zeta function. + + Examples + ======== + + For $a = 1$ the Hurwitz zeta function reduces to the famous Riemann + zeta function: + + .. math:: \zeta(s, 1) = \zeta(s) = \sum_{n=1}^\infty \frac{1}{n^s}. + + >>> from sympy import zeta + >>> from sympy.abc import s + >>> zeta(s, 1) + zeta(s) + >>> zeta(s) + zeta(s) + + The Riemann zeta function can also be expressed using the Dirichlet eta + function: + + >>> from sympy import dirichlet_eta + >>> zeta(s).rewrite(dirichlet_eta) + dirichlet_eta(s)/(1 - 2**(1 - s)) + + The Riemann zeta function at nonnegative even and negative integer + values is related to the Bernoulli numbers and polynomials: + + >>> zeta(2) + pi**2/6 + >>> zeta(4) + pi**4/90 + >>> zeta(0) + -1/2 + >>> zeta(-1) + -1/12 + >>> zeta(-4) + 0 + + The specific formulae are: + + .. math:: \zeta(2n) = -\frac{(2\pi i)^{2n} B_{2n}}{2(2n)!} + .. math:: \zeta(-n,a) = -\frac{B_{n+1}(a)}{n+1} + + No closed-form expressions are known at positive odd integers, but + numerical evaluation is possible: + + >>> zeta(3).n() + 1.20205690315959 + + The derivative of $\zeta(s, a)$ with respect to $a$ can be computed: + + >>> from sympy.abc import a + >>> zeta(s, a).diff(a) + -s*zeta(s + 1, a) + + However the derivative with respect to $s$ has no useful closed form + expression: + + >>> zeta(s, a).diff(s) + Derivative(zeta(s, a), s) + + The Hurwitz zeta function can be expressed in terms of the Lerch + transcendent, :class:`~.lerchphi`: + + >>> from sympy import lerchphi + >>> zeta(s, a).rewrite(lerchphi) + lerchphi(1, s, a) + + See Also + ======== + + dirichlet_eta, lerchphi, polylog + + References + ========== + + .. [1] https://dlmf.nist.gov/25.11 + .. [2] https://en.wikipedia.org/wiki/Hurwitz_zeta_function + + """ + + @classmethod + def eval(cls, s, a=None): + if a is S.One: + return cls(s) + elif s is S.NaN or a is S.NaN: + return S.NaN + elif s is S.One: + return S.ComplexInfinity + elif s is S.Infinity: + return S.One + elif a is S.Infinity: + return S.Zero + + sint = s.is_Integer + if a is None: + a = S.One + if sint and s.is_nonpositive: + return bernoulli(1-s, a) / (s-1) + elif a is S.One: + if sint and s.is_even: + return -(2*pi*I)**s * bernoulli(s) / (2*factorial(s)) + elif sint and a.is_Integer and a.is_positive: + return cls(s) - harmonic(a-1, s) + elif a.is_Integer and a.is_nonpositive and \ + (s.is_integer is False or s.is_nonpositive is False): + return S.NaN + + def _eval_rewrite_as_bernoulli(self, s, a=1, **kwargs): + if a == 1 and s.is_integer and s.is_nonnegative and s.is_even: + return -(2*pi*I)**s * bernoulli(s) / (2*factorial(s)) + return bernoulli(1-s, a) / (s-1) + + def _eval_rewrite_as_dirichlet_eta(self, s, a=1, **kwargs): + if a != 1: + return self + s = self.args[0] + return dirichlet_eta(s)/(1 - 2**(1 - s)) + + def _eval_rewrite_as_lerchphi(self, s, a=1, **kwargs): + return lerchphi(1, s, a) + + def _eval_is_finite(self): + return fuzzy_not((self.args[0] - 1).is_zero) + + def _eval_expand_func(self, **hints): + s = self.args[0] + a = self.args[1] if len(self.args) > 1 else S.One + if a.is_integer: + if a.is_positive: + return zeta(s) - harmonic(a-1, s) + if a.is_nonpositive and (s.is_integer is False or + s.is_nonpositive is False): + return S.NaN + return self + + def fdiff(self, argindex=1): + if len(self.args) == 2: + s, a = self.args + else: + s, a = self.args + (1,) + if argindex == 2: + return -s*zeta(s + 1, a) + else: + raise ArgumentIndexError + + def _eval_as_leading_term(self, x, logx, cdir): + if len(self.args) == 2: + s, a = self.args + else: + s, a = self.args + (S.One,) + + try: + c, e = a.leadterm(x) + except NotImplementedError: + return self + + if e.is_negative and not s.is_positive: + raise NotImplementedError + + return super(zeta, self)._eval_as_leading_term(x, logx=logx, cdir=cdir) + + +class dirichlet_eta(DefinedFunction): + r""" + Dirichlet eta function. + + Explanation + =========== + + For $\operatorname{Re}(s) > 0$ and $0 < x \le 1$, this function is defined as + + .. math:: \eta(s, a) = \sum_{n=0}^\infty \frac{(-1)^n}{(n+a)^s}. + + It admits a unique analytic continuation to all of $\mathbb{C}$ for any + fixed $a$ not a nonpositive integer. It is an entire, unbranched function. + + It can be expressed using the Hurwitz zeta function as + + .. math:: \eta(s, a) = \zeta(s,a) - 2^{1-s} \zeta\left(s, \frac{a+1}{2}\right) + + and using the generalized Genocchi function as + + .. math:: \eta(s, a) = \frac{G(1-s, a)}{2(s-1)}. + + In both cases the limiting value of $\log2 - \psi(a) + \psi\left(\frac{a+1}{2}\right)$ + is used when $s = 1$. + + Examples + ======== + + >>> from sympy import dirichlet_eta, zeta + >>> from sympy.abc import s + >>> dirichlet_eta(s).rewrite(zeta) + Piecewise((log(2), Eq(s, 1)), ((1 - 2**(1 - s))*zeta(s), True)) + + See Also + ======== + + zeta + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Dirichlet_eta_function + .. [2] Peter Luschny, "An introduction to the Bernoulli function", + https://arxiv.org/abs/2009.06743 + + """ + + @classmethod + def eval(cls, s, a=None): + if a is S.One: + return cls(s) + if a is None: + if s == 1: + return log(2) + z = zeta(s) + if not z.has(zeta): + return (1 - 2**(1-s)) * z + return + elif s == 1: + from sympy.functions.special.gamma_functions import digamma + return log(2) - digamma(a) + digamma((a+1)/2) + z1 = zeta(s, a) + z2 = zeta(s, (a+1)/2) + if not z1.has(zeta) and not z2.has(zeta): + return z1 - 2**(1-s) * z2 + + def _eval_rewrite_as_zeta(self, s, a=1, **kwargs): + from sympy.functions.special.gamma_functions import digamma + if a == 1: + return Piecewise((log(2), Eq(s, 1)), ((1 - 2**(1-s)) * zeta(s), True)) + return Piecewise((log(2) - digamma(a) + digamma((a+1)/2), Eq(s, 1)), + (zeta(s, a) - 2**(1-s) * zeta(s, (a+1)/2), True)) + + def _eval_rewrite_as_genocchi(self, s, a=S.One, **kwargs): + from sympy.functions.special.gamma_functions import digamma + return Piecewise((log(2) - digamma(a) + digamma((a+1)/2), Eq(s, 1)), + (genocchi(1-s, a) / (2 * (s-1)), True)) + + def _eval_evalf(self, prec): + if all(i.is_number for i in self.args): + return self.rewrite(zeta)._eval_evalf(prec) + + +class riemann_xi(DefinedFunction): + r""" + Riemann Xi function. + + Examples + ======== + + The Riemann Xi function is closely related to the Riemann zeta function. + The zeros of Riemann Xi function are precisely the non-trivial zeros + of the zeta function. + + >>> from sympy import riemann_xi, zeta + >>> from sympy.abc import s + >>> riemann_xi(s).rewrite(zeta) + s*(s - 1)*gamma(s/2)*zeta(s)/(2*pi**(s/2)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Riemann_Xi_function + + """ + + + @classmethod + def eval(cls, s): + from sympy.functions.special.gamma_functions import gamma + z = zeta(s) + if s in (S.Zero, S.One): + return S.Half + + if not isinstance(z, zeta): + return s*(s - 1)*gamma(s/2)*z/(2*pi**(s/2)) + + def _eval_rewrite_as_zeta(self, s, **kwargs): + from sympy.functions.special.gamma_functions import gamma + return s*(s - 1)*gamma(s/2)*zeta(s)/(2*pi**(s/2)) + + +class stieltjes(DefinedFunction): + r""" + Represents Stieltjes constants, $\gamma_{k}$ that occur in + Laurent Series expansion of the Riemann zeta function. + + Examples + ======== + + >>> from sympy import stieltjes + >>> from sympy.abc import n, m + >>> stieltjes(n) + stieltjes(n) + + The zero'th stieltjes constant: + + >>> stieltjes(0) + EulerGamma + >>> stieltjes(0, 1) + EulerGamma + + For generalized stieltjes constants: + + >>> stieltjes(n, m) + stieltjes(n, m) + + Constants are only defined for integers >= 0: + + >>> stieltjes(-1) + zoo + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Stieltjes_constants + + """ + + @classmethod + def eval(cls, n, a=None): + if a is not None: + a = sympify(a) + if a is S.NaN: + return S.NaN + if a.is_Integer and a.is_nonpositive: + return S.ComplexInfinity + + if n.is_Number: + if n is S.NaN: + return S.NaN + elif n < 0: + return S.ComplexInfinity + elif not n.is_Integer: + return S.ComplexInfinity + elif n is S.Zero and a in [None, 1]: + return S.EulerGamma + + if n.is_extended_negative: + return S.ComplexInfinity + + if n.is_zero and a in [None, 1]: + return S.EulerGamma + + if n.is_integer == False: + return S.ComplexInfinity + + +@cacheit +def _dilogtable(): + return { + S.Half: pi**2/12 - log(2)**2/2, + Integer(2) : pi**2/4 - I*pi*log(2), + -(sqrt(5) - 1)/2 : -pi**2/15 + log((sqrt(5)-1)/2)**2/2, + -(sqrt(5) + 1)/2 : -pi**2/10 - log((sqrt(5)+1)/2)**2, + (3 - sqrt(5))/2 : pi**2/15 - log((sqrt(5)-1)/2)**2, + (sqrt(5) - 1)/2 : pi**2/10 - log((sqrt(5)-1)/2)**2, + I : I*S.Catalan - pi**2/48, + -I : -I*S.Catalan - pi**2/48, + 1 - I : pi**2/16 - I*S.Catalan - pi*I/4*log(2), + 1 + I : pi**2/16 + I*S.Catalan + pi*I/4*log(2), + (1 - I)/2 : -log(2)**2/8 + pi*I*log(2)/8 + 5*pi**2/96 - I*S.Catalan + } diff --git a/venv/lib/python3.11/site-packages/sympy/galgebra.py b/venv/lib/python3.11/site-packages/sympy/galgebra.py new file mode 100644 index 0000000000000000000000000000000000000000..be812bfcbcece834c86d7b55eb846286acd0ac08 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/galgebra.py @@ -0,0 +1 @@ +raise ImportError("""As of SymPy 1.0 the galgebra module is maintained separately at https://github.com/pygae/galgebra""") diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/__init__.py b/venv/lib/python3.11/site-packages/sympy/geometry/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bb85d4ff5d53eb44a039a95cfc2fff687322cc76 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/__init__.py @@ -0,0 +1,45 @@ +""" +A geometry module for the SymPy library. This module contains all of the +entities and functions needed to construct basic geometrical data and to +perform simple informational queries. + +Usage: +====== + +Examples +======== + +""" +from sympy.geometry.point import Point, Point2D, Point3D +from sympy.geometry.line import Line, Ray, Segment, Line2D, Segment2D, Ray2D, \ + Line3D, Segment3D, Ray3D +from sympy.geometry.plane import Plane +from sympy.geometry.ellipse import Ellipse, Circle +from sympy.geometry.polygon import Polygon, RegularPolygon, Triangle, rad, deg +from sympy.geometry.util import are_similar, centroid, convex_hull, idiff, \ + intersection, closest_points, farthest_points +from sympy.geometry.exceptions import GeometryError +from sympy.geometry.curve import Curve +from sympy.geometry.parabola import Parabola + +__all__ = [ + 'Point', 'Point2D', 'Point3D', + + 'Line', 'Ray', 'Segment', 'Line2D', 'Segment2D', 'Ray2D', 'Line3D', + 'Segment3D', 'Ray3D', + + 'Plane', + + 'Ellipse', 'Circle', + + 'Polygon', 'RegularPolygon', 'Triangle', 'rad', 'deg', + + 'are_similar', 'centroid', 'convex_hull', 'idiff', 'intersection', + 'closest_points', 'farthest_points', + + 'GeometryError', + + 'Curve', + + 'Parabola', +] diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/curve.py b/venv/lib/python3.11/site-packages/sympy/geometry/curve.py new file mode 100644 index 0000000000000000000000000000000000000000..c074f22cad79b1261ad44be4ccface972cdd3b82 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/curve.py @@ -0,0 +1,424 @@ +"""Curves in 2-dimensional Euclidean space. + +Contains +======== +Curve + +""" + +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.core import diff +from sympy.core.containers import Tuple +from sympy.core.symbol import _symbol +from sympy.geometry.entity import GeometryEntity, GeometrySet +from sympy.geometry.point import Point +from sympy.integrals import integrate +from sympy.matrices import Matrix, rot_axis3 +from sympy.utilities.iterables import is_sequence + +from mpmath.libmp.libmpf import prec_to_dps + + +class Curve(GeometrySet): + """A curve in space. + + A curve is defined by parametric functions for the coordinates, a + parameter and the lower and upper bounds for the parameter value. + + Parameters + ========== + + function : list of functions + limits : 3-tuple + Function parameter and lower and upper bounds. + + Attributes + ========== + + functions + parameter + limits + + Raises + ====== + + ValueError + When `functions` are specified incorrectly. + When `limits` are specified incorrectly. + + Examples + ======== + + >>> from sympy import Curve, sin, cos, interpolate + >>> from sympy.abc import t, a + >>> C = Curve((sin(t), cos(t)), (t, 0, 2)) + >>> C.functions + (sin(t), cos(t)) + >>> C.limits + (t, 0, 2) + >>> C.parameter + t + >>> C = Curve((t, interpolate([1, 4, 9, 16], t)), (t, 0, 1)); C + Curve((t, t**2), (t, 0, 1)) + >>> C.subs(t, 4) + Point2D(4, 16) + >>> C.arbitrary_point(a) + Point2D(a, a**2) + + See Also + ======== + + sympy.core.function.Function + sympy.polys.polyfuncs.interpolate + + """ + + def __new__(cls, function, limits): + if not is_sequence(function) or len(function) != 2: + raise ValueError("Function argument should be (x(t), y(t)) " + "but got %s" % str(function)) + if not is_sequence(limits) or len(limits) != 3: + raise ValueError("Limit argument should be (t, tmin, tmax) " + "but got %s" % str(limits)) + + return GeometryEntity.__new__(cls, Tuple(*function), Tuple(*limits)) + + def __call__(self, f): + return self.subs(self.parameter, f) + + def _eval_subs(self, old, new): + if old == self.parameter: + return Point(*[f.subs(old, new) for f in self.functions]) + + def _eval_evalf(self, prec=15, **options): + f, (t, a, b) = self.args + dps = prec_to_dps(prec) + f = tuple([i.evalf(n=dps, **options) for i in f]) + a, b = [i.evalf(n=dps, **options) for i in (a, b)] + return self.func(f, (t, a, b)) + + def arbitrary_point(self, parameter='t'): + """A parameterized point on the curve. + + Parameters + ========== + + parameter : str or Symbol, optional + Default value is 't'. + The Curve's parameter is selected with None or self.parameter + otherwise the provided symbol is used. + + Returns + ======= + + Point : + Returns a point in parametric form. + + Raises + ====== + + ValueError + When `parameter` already appears in the functions. + + Examples + ======== + + >>> from sympy import Curve, Symbol + >>> from sympy.abc import s + >>> C = Curve([2*s, s**2], (s, 0, 2)) + >>> C.arbitrary_point() + Point2D(2*t, t**2) + >>> C.arbitrary_point(C.parameter) + Point2D(2*s, s**2) + >>> C.arbitrary_point(None) + Point2D(2*s, s**2) + >>> C.arbitrary_point(Symbol('a')) + Point2D(2*a, a**2) + + See Also + ======== + + sympy.geometry.point.Point + + """ + if parameter is None: + return Point(*self.functions) + + tnew = _symbol(parameter, self.parameter, real=True) + t = self.parameter + if (tnew.name != t.name and + tnew.name in (f.name for f in self.free_symbols)): + raise ValueError('Symbol %s already appears in object ' + 'and cannot be used as a parameter.' % tnew.name) + return Point(*[w.subs(t, tnew) for w in self.functions]) + + @property + def free_symbols(self): + """Return a set of symbols other than the bound symbols used to + parametrically define the Curve. + + Returns + ======= + + set : + Set of all non-parameterized symbols. + + Examples + ======== + + >>> from sympy.abc import t, a + >>> from sympy import Curve + >>> Curve((t, t**2), (t, 0, 2)).free_symbols + set() + >>> Curve((t, t**2), (t, a, 2)).free_symbols + {a} + + """ + free = set() + for a in self.functions + self.limits[1:]: + free |= a.free_symbols + free = free.difference({self.parameter}) + return free + + @property + def ambient_dimension(self): + """The dimension of the curve. + + Returns + ======= + + int : + the dimension of curve. + + Examples + ======== + + >>> from sympy.abc import t + >>> from sympy import Curve + >>> C = Curve((t, t**2), (t, 0, 2)) + >>> C.ambient_dimension + 2 + + """ + + return len(self.args[0]) + + @property + def functions(self): + """The functions specifying the curve. + + Returns + ======= + + functions : + list of parameterized coordinate functions. + + Examples + ======== + + >>> from sympy.abc import t + >>> from sympy import Curve + >>> C = Curve((t, t**2), (t, 0, 2)) + >>> C.functions + (t, t**2) + + See Also + ======== + + parameter + + """ + return self.args[0] + + @property + def limits(self): + """The limits for the curve. + + Returns + ======= + + limits : tuple + Contains parameter and lower and upper limits. + + Examples + ======== + + >>> from sympy.abc import t + >>> from sympy import Curve + >>> C = Curve([t, t**3], (t, -2, 2)) + >>> C.limits + (t, -2, 2) + + See Also + ======== + + plot_interval + + """ + return self.args[1] + + @property + def parameter(self): + """The curve function variable. + + Returns + ======= + + Symbol : + returns a bound symbol. + + Examples + ======== + + >>> from sympy.abc import t + >>> from sympy import Curve + >>> C = Curve([t, t**2], (t, 0, 2)) + >>> C.parameter + t + + See Also + ======== + + functions + + """ + return self.args[1][0] + + @property + def length(self): + """The curve length. + + Examples + ======== + + >>> from sympy import Curve + >>> from sympy.abc import t + >>> Curve((t, t), (t, 0, 1)).length + sqrt(2) + + """ + integrand = sqrt(sum(diff(func, self.limits[0])**2 for func in self.functions)) + return integrate(integrand, self.limits) + + def plot_interval(self, parameter='t'): + """The plot interval for the default geometric plot of the curve. + + Parameters + ========== + + parameter : str or Symbol, optional + Default value is 't'; + otherwise the provided symbol is used. + + Returns + ======= + + List : + the plot interval as below: + [parameter, lower_bound, upper_bound] + + Examples + ======== + + >>> from sympy import Curve, sin + >>> from sympy.abc import x, s + >>> Curve((x, sin(x)), (x, 1, 2)).plot_interval() + [t, 1, 2] + >>> Curve((x, sin(x)), (x, 1, 2)).plot_interval(s) + [s, 1, 2] + + See Also + ======== + + limits : Returns limits of the parameter interval + + """ + t = _symbol(parameter, self.parameter, real=True) + return [t] + list(self.limits[1:]) + + def rotate(self, angle=0, pt=None): + """This function is used to rotate a curve along given point ``pt`` at given angle(in radian). + + Parameters + ========== + + angle : + the angle at which the curve will be rotated(in radian) in counterclockwise direction. + default value of angle is 0. + + pt : Point + the point along which the curve will be rotated. + If no point given, the curve will be rotated around origin. + + Returns + ======= + + Curve : + returns a curve rotated at given angle along given point. + + Examples + ======== + + >>> from sympy import Curve, pi + >>> from sympy.abc import x + >>> Curve((x, x), (x, 0, 1)).rotate(pi/2) + Curve((-x, x), (x, 0, 1)) + + """ + if pt: + pt = -Point(pt, dim=2) + else: + pt = Point(0,0) + rv = self.translate(*pt.args) + f = list(rv.functions) + f.append(0) + f = Matrix(1, 3, f) + f *= rot_axis3(angle) + rv = self.func(f[0, :2].tolist()[0], self.limits) + pt = -pt + return rv.translate(*pt.args) + + def scale(self, x=1, y=1, pt=None): + """Override GeometryEntity.scale since Curve is not made up of Points. + + Returns + ======= + + Curve : + returns scaled curve. + + Examples + ======== + + >>> from sympy import Curve + >>> from sympy.abc import x + >>> Curve((x, x), (x, 0, 1)).scale(2) + Curve((2*x, x), (x, 0, 1)) + + """ + if pt: + pt = Point(pt, dim=2) + return self.translate(*(-pt).args).scale(x, y).translate(*pt.args) + fx, fy = self.functions + return self.func((fx*x, fy*y), self.limits) + + def translate(self, x=0, y=0): + """Translate the Curve by (x, y). + + Returns + ======= + + Curve : + returns a translated curve. + + Examples + ======== + + >>> from sympy import Curve + >>> from sympy.abc import x + >>> Curve((x, x), (x, 0, 1)).translate(1, 2) + Curve((x + 1, x + 2), (x, 0, 1)) + + """ + fx, fy = self.functions + return self.func((fx + x, fy + y), self.limits) diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/ellipse.py b/venv/lib/python3.11/site-packages/sympy/geometry/ellipse.py new file mode 100644 index 0000000000000000000000000000000000000000..199db25fde9b019893a275d69959154990e8a4a7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/ellipse.py @@ -0,0 +1,1768 @@ +"""Elliptical geometrical entities. + +Contains +* Ellipse +* Circle + +""" + +from sympy.core.expr import Expr +from sympy.core.relational import Eq +from sympy.core import S, pi, sympify +from sympy.core.evalf import N +from sympy.core.parameters import global_parameters +from sympy.core.logic import fuzzy_bool +from sympy.core.numbers import Rational, oo +from sympy.core.sorting import ordered +from sympy.core.symbol import Dummy, uniquely_named_symbol, _symbol +from sympy.simplify.simplify import simplify +from sympy.simplify.trigsimp import trigsimp +from sympy.functions.elementary.miscellaneous import sqrt, Max +from sympy.functions.elementary.trigonometric import cos, sin +from sympy.functions.special.elliptic_integrals import elliptic_e +from .entity import GeometryEntity, GeometrySet +from .exceptions import GeometryError +from .line import Line, Segment, Ray2D, Segment2D, Line2D, LinearEntity3D +from .point import Point, Point2D, Point3D +from .util import idiff, find +from sympy.polys import DomainError, Poly, PolynomialError +from sympy.polys.polyutils import _not_a_coeff, _nsort +from sympy.solvers import solve +from sympy.solvers.solveset import linear_coeffs +from sympy.utilities.misc import filldedent, func_name + +from mpmath.libmp.libmpf import prec_to_dps + +import random + +x, y = [Dummy('ellipse_dummy', real=True) for i in range(2)] + + +class Ellipse(GeometrySet): + """An elliptical GeometryEntity. + + Parameters + ========== + + center : Point, optional + Default value is Point(0, 0) + hradius : number or SymPy expression, optional + vradius : number or SymPy expression, optional + eccentricity : number or SymPy expression, optional + Two of `hradius`, `vradius` and `eccentricity` must be supplied to + create an Ellipse. The third is derived from the two supplied. + + Attributes + ========== + + center + hradius + vradius + area + circumference + eccentricity + periapsis + apoapsis + focus_distance + foci + + Raises + ====== + + GeometryError + When `hradius`, `vradius` and `eccentricity` are incorrectly supplied + as parameters. + TypeError + When `center` is not a Point. + + See Also + ======== + + Circle + + Notes + ----- + Constructed from a center and two radii, the first being the horizontal + radius (along the x-axis) and the second being the vertical radius (along + the y-axis). + + When symbolic value for hradius and vradius are used, any calculation that + refers to the foci or the major or minor axis will assume that the ellipse + has its major radius on the x-axis. If this is not true then a manual + rotation is necessary. + + Examples + ======== + + >>> from sympy import Ellipse, Point, Rational + >>> e1 = Ellipse(Point(0, 0), 5, 1) + >>> e1.hradius, e1.vradius + (5, 1) + >>> e2 = Ellipse(Point(3, 1), hradius=3, eccentricity=Rational(4, 5)) + >>> e2 + Ellipse(Point2D(3, 1), 3, 9/5) + + """ + + def __contains__(self, o): + if isinstance(o, Point): + res = self.equation(x, y).subs({x: o.x, y: o.y}) + return trigsimp(simplify(res)) is S.Zero + elif isinstance(o, Ellipse): + return self == o + return False + + def __eq__(self, o): + """Is the other GeometryEntity the same as this ellipse?""" + return isinstance(o, Ellipse) and (self.center == o.center and + self.hradius == o.hradius and + self.vradius == o.vradius) + + def __hash__(self): + return super().__hash__() + + def __new__( + cls, center=None, hradius=None, vradius=None, eccentricity=None, **kwargs): + + hradius = sympify(hradius) + vradius = sympify(vradius) + + if center is None: + center = Point(0, 0) + else: + if len(center) != 2: + raise ValueError('The center of "{}" must be a two dimensional point'.format(cls)) + center = Point(center, dim=2) + + if len(list(filter(lambda x: x is not None, (hradius, vradius, eccentricity)))) != 2: + raise ValueError(filldedent(''' + Exactly two arguments of "hradius", "vradius", and + "eccentricity" must not be None.''')) + + if eccentricity is not None: + eccentricity = sympify(eccentricity) + if eccentricity.is_negative: + raise GeometryError("Eccentricity of ellipse/circle should lie between [0, 1)") + elif hradius is None: + hradius = vradius / sqrt(1 - eccentricity**2) + elif vradius is None: + vradius = hradius * sqrt(1 - eccentricity**2) + + if hradius == vradius: + return Circle(center, hradius, **kwargs) + + if S.Zero in (hradius, vradius): + return Segment(Point(center[0] - hradius, center[1] - vradius), Point(center[0] + hradius, center[1] + vradius)) + + if hradius.is_real is False or vradius.is_real is False: + raise GeometryError("Invalid value encountered when computing hradius / vradius.") + + return GeometryEntity.__new__(cls, center, hradius, vradius, **kwargs) + + def _svg(self, scale_factor=1., fill_color="#66cc99"): + """Returns SVG ellipse element for the Ellipse. + + Parameters + ========== + + scale_factor : float + Multiplication factor for the SVG stroke-width. Default is 1. + fill_color : str, optional + Hex string for fill color. Default is "#66cc99". + """ + + c = N(self.center) + h, v = N(self.hradius), N(self.vradius) + return ( + '' + ).format(2. * scale_factor, fill_color, c.x, c.y, h, v) + + @property + def ambient_dimension(self): + return 2 + + @property + def apoapsis(self): + """The apoapsis of the ellipse. + + The greatest distance between the focus and the contour. + + Returns + ======= + + apoapsis : number + + See Also + ======== + + periapsis : Returns shortest distance between foci and contour + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> p1 = Point(0, 0) + >>> e1 = Ellipse(p1, 3, 1) + >>> e1.apoapsis + 2*sqrt(2) + 3 + + """ + return self.major * (1 + self.eccentricity) + + def arbitrary_point(self, parameter='t'): + """A parameterized point on the ellipse. + + Parameters + ========== + + parameter : str, optional + Default value is 't'. + + Returns + ======= + + arbitrary_point : Point + + Raises + ====== + + ValueError + When `parameter` already appears in the functions. + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> e1 = Ellipse(Point(0, 0), 3, 2) + >>> e1.arbitrary_point() + Point2D(3*cos(t), 2*sin(t)) + + """ + t = _symbol(parameter, real=True) + if t.name in (f.name for f in self.free_symbols): + raise ValueError(filldedent('Symbol %s already appears in object ' + 'and cannot be used as a parameter.' % t.name)) + return Point(self.center.x + self.hradius*cos(t), + self.center.y + self.vradius*sin(t)) + + @property + def area(self): + """The area of the ellipse. + + Returns + ======= + + area : number + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> p1 = Point(0, 0) + >>> e1 = Ellipse(p1, 3, 1) + >>> e1.area + 3*pi + + """ + return simplify(S.Pi * self.hradius * self.vradius) + + @property + def bounds(self): + """Return a tuple (xmin, ymin, xmax, ymax) representing the bounding + rectangle for the geometric figure. + + """ + + h, v = self.hradius, self.vradius + return (self.center.x - h, self.center.y - v, self.center.x + h, self.center.y + v) + + @property + def center(self): + """The center of the ellipse. + + Returns + ======= + + center : number + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> p1 = Point(0, 0) + >>> e1 = Ellipse(p1, 3, 1) + >>> e1.center + Point2D(0, 0) + + """ + return self.args[0] + + @property + def circumference(self): + """The circumference of the ellipse. + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> p1 = Point(0, 0) + >>> e1 = Ellipse(p1, 3, 1) + >>> e1.circumference + 12*elliptic_e(8/9) + + """ + if self.eccentricity == 1: + # degenerate + return 4*self.major + elif self.eccentricity == 0: + # circle + return 2*pi*self.hradius + else: + return 4*self.major*elliptic_e(self.eccentricity**2) + + @property + def eccentricity(self): + """The eccentricity of the ellipse. + + Returns + ======= + + eccentricity : number + + Examples + ======== + + >>> from sympy import Point, Ellipse, sqrt + >>> p1 = Point(0, 0) + >>> e1 = Ellipse(p1, 3, sqrt(2)) + >>> e1.eccentricity + sqrt(7)/3 + + """ + return self.focus_distance / self.major + + def encloses_point(self, p): + """ + Return True if p is enclosed by (is inside of) self. + + Notes + ----- + Being on the border of self is considered False. + + Parameters + ========== + + p : Point + + Returns + ======= + + encloses_point : True, False or None + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Ellipse, S + >>> from sympy.abc import t + >>> e = Ellipse((0, 0), 3, 2) + >>> e.encloses_point((0, 0)) + True + >>> e.encloses_point(e.arbitrary_point(t).subs(t, S.Half)) + False + >>> e.encloses_point((4, 0)) + False + + """ + p = Point(p, dim=2) + if p in self: + return False + + if len(self.foci) == 2: + # if the combined distance from the foci to p (h1 + h2) is less + # than the combined distance from the foci to the minor axis + # (which is the same as the major axis length) then p is inside + # the ellipse + h1, h2 = [f.distance(p) for f in self.foci] + test = 2*self.major - (h1 + h2) + else: + test = self.radius - self.center.distance(p) + + return fuzzy_bool(test.is_positive) + + def equation(self, x='x', y='y', _slope=None): + """ + Returns the equation of an ellipse aligned with the x and y axes; + when slope is given, the equation returned corresponds to an ellipse + with a major axis having that slope. + + Parameters + ========== + + x : str, optional + Label for the x-axis. Default value is 'x'. + y : str, optional + Label for the y-axis. Default value is 'y'. + _slope : Expr, optional + The slope of the major axis. Ignored when 'None'. + + Returns + ======= + + equation : SymPy expression + + See Also + ======== + + arbitrary_point : Returns parameterized point on ellipse + + Examples + ======== + + >>> from sympy import Point, Ellipse, pi + >>> from sympy.abc import x, y + >>> e1 = Ellipse(Point(1, 0), 3, 2) + >>> eq1 = e1.equation(x, y); eq1 + y**2/4 + (x/3 - 1/3)**2 - 1 + >>> eq2 = e1.equation(x, y, _slope=1); eq2 + (-x + y + 1)**2/8 + (x + y - 1)**2/18 - 1 + + A point on e1 satisfies eq1. Let's use one on the x-axis: + + >>> p1 = e1.center + Point(e1.major, 0) + >>> assert eq1.subs(x, p1.x).subs(y, p1.y) == 0 + + When rotated the same as the rotated ellipse, about the center + point of the ellipse, it will satisfy the rotated ellipse's + equation, too: + + >>> r1 = p1.rotate(pi/4, e1.center) + >>> assert eq2.subs(x, r1.x).subs(y, r1.y) == 0 + + References + ========== + + .. [1] https://math.stackexchange.com/questions/108270/what-is-the-equation-of-an-ellipse-that-is-not-aligned-with-the-axis + .. [2] https://en.wikipedia.org/wiki/Ellipse#Shifted_ellipse + + """ + + x = _symbol(x, real=True) + y = _symbol(y, real=True) + + dx = x - self.center.x + dy = y - self.center.y + + if _slope is not None: + L = (dy - _slope*dx)**2 + l = (_slope*dy + dx)**2 + h = 1 + _slope**2 + b = h*self.major**2 + a = h*self.minor**2 + return l/b + L/a - 1 + + else: + t1 = (dx/self.hradius)**2 + t2 = (dy/self.vradius)**2 + return t1 + t2 - 1 + + def evolute(self, x='x', y='y'): + """The equation of evolute of the ellipse. + + Parameters + ========== + + x : str, optional + Label for the x-axis. Default value is 'x'. + y : str, optional + Label for the y-axis. Default value is 'y'. + + Returns + ======= + + equation : SymPy expression + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> e1 = Ellipse(Point(1, 0), 3, 2) + >>> e1.evolute() + 2**(2/3)*y**(2/3) + (3*x - 3)**(2/3) - 5**(2/3) + """ + if len(self.args) != 3: + raise NotImplementedError('Evolute of arbitrary Ellipse is not supported.') + x = _symbol(x, real=True) + y = _symbol(y, real=True) + t1 = (self.hradius*(x - self.center.x))**Rational(2, 3) + t2 = (self.vradius*(y - self.center.y))**Rational(2, 3) + return t1 + t2 - (self.hradius**2 - self.vradius**2)**Rational(2, 3) + + @property + def foci(self): + """The foci of the ellipse. + + Notes + ----- + The foci can only be calculated if the major/minor axes are known. + + Raises + ====== + + ValueError + When the major and minor axis cannot be determined. + + See Also + ======== + + sympy.geometry.point.Point + focus_distance : Returns the distance between focus and center + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> p1 = Point(0, 0) + >>> e1 = Ellipse(p1, 3, 1) + >>> e1.foci + (Point2D(-2*sqrt(2), 0), Point2D(2*sqrt(2), 0)) + + """ + c = self.center + hr, vr = self.hradius, self.vradius + if hr == vr: + return (c, c) + + # calculate focus distance manually, since focus_distance calls this + # routine + fd = sqrt(self.major**2 - self.minor**2) + if hr == self.minor: + # foci on the y-axis + return (c + Point(0, -fd), c + Point(0, fd)) + elif hr == self.major: + # foci on the x-axis + return (c + Point(-fd, 0), c + Point(fd, 0)) + + @property + def focus_distance(self): + """The focal distance of the ellipse. + + The distance between the center and one focus. + + Returns + ======= + + focus_distance : number + + See Also + ======== + + foci + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> p1 = Point(0, 0) + >>> e1 = Ellipse(p1, 3, 1) + >>> e1.focus_distance + 2*sqrt(2) + + """ + return Point.distance(self.center, self.foci[0]) + + @property + def hradius(self): + """The horizontal radius of the ellipse. + + Returns + ======= + + hradius : number + + See Also + ======== + + vradius, major, minor + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> p1 = Point(0, 0) + >>> e1 = Ellipse(p1, 3, 1) + >>> e1.hradius + 3 + + """ + return self.args[1] + + def intersection(self, o): + """The intersection of this ellipse and another geometrical entity + `o`. + + Parameters + ========== + + o : GeometryEntity + + Returns + ======= + + intersection : list of GeometryEntity objects + + Notes + ----- + Currently supports intersections with Point, Line, Segment, Ray, + Circle and Ellipse types. + + See Also + ======== + + sympy.geometry.entity.GeometryEntity + + Examples + ======== + + >>> from sympy import Ellipse, Point, Line + >>> e = Ellipse(Point(0, 0), 5, 7) + >>> e.intersection(Point(0, 0)) + [] + >>> e.intersection(Point(5, 0)) + [Point2D(5, 0)] + >>> e.intersection(Line(Point(0,0), Point(0, 1))) + [Point2D(0, -7), Point2D(0, 7)] + >>> e.intersection(Line(Point(5,0), Point(5, 1))) + [Point2D(5, 0)] + >>> e.intersection(Line(Point(6,0), Point(6, 1))) + [] + >>> e = Ellipse(Point(-1, 0), 4, 3) + >>> e.intersection(Ellipse(Point(1, 0), 4, 3)) + [Point2D(0, -3*sqrt(15)/4), Point2D(0, 3*sqrt(15)/4)] + >>> e.intersection(Ellipse(Point(5, 0), 4, 3)) + [Point2D(2, -3*sqrt(7)/4), Point2D(2, 3*sqrt(7)/4)] + >>> e.intersection(Ellipse(Point(100500, 0), 4, 3)) + [] + >>> e.intersection(Ellipse(Point(0, 0), 3, 4)) + [Point2D(3, 0), Point2D(-363/175, -48*sqrt(111)/175), Point2D(-363/175, 48*sqrt(111)/175)] + >>> e.intersection(Ellipse(Point(-1, 0), 3, 4)) + [Point2D(-17/5, -12/5), Point2D(-17/5, 12/5), Point2D(7/5, -12/5), Point2D(7/5, 12/5)] + """ + # TODO: Replace solve with nonlinsolve, when nonlinsolve will be able to solve in real domain + + if isinstance(o, Point): + if o in self: + return [o] + else: + return [] + + elif isinstance(o, (Segment2D, Ray2D)): + ellipse_equation = self.equation(x, y) + result = solve([ellipse_equation, Line( + o.points[0], o.points[1]).equation(x, y)], [x, y], + set=True)[1] + return list(ordered([Point(i) for i in result if i in o])) + + elif isinstance(o, Polygon): + return o.intersection(self) + + elif isinstance(o, (Ellipse, Line2D)): + if o == self: + return self + else: + ellipse_equation = self.equation(x, y) + return list(ordered([Point(i) for i in solve( + [ellipse_equation, o.equation(x, y)], [x, y], + set=True)[1]])) + elif isinstance(o, LinearEntity3D): + raise TypeError('Entity must be two dimensional, not three dimensional') + else: + raise TypeError('Intersection not handled for %s' % func_name(o)) + + def is_tangent(self, o): + """Is `o` tangent to the ellipse? + + Parameters + ========== + + o : GeometryEntity + An Ellipse, LinearEntity or Polygon + + Raises + ====== + + NotImplementedError + When the wrong type of argument is supplied. + + Returns + ======= + + is_tangent: boolean + True if o is tangent to the ellipse, False otherwise. + + See Also + ======== + + tangent_lines + + Examples + ======== + + >>> from sympy import Point, Ellipse, Line + >>> p0, p1, p2 = Point(0, 0), Point(3, 0), Point(3, 3) + >>> e1 = Ellipse(p0, 3, 2) + >>> l1 = Line(p1, p2) + >>> e1.is_tangent(l1) + True + + """ + if isinstance(o, Point2D): + return False + elif isinstance(o, Ellipse): + intersect = self.intersection(o) + if isinstance(intersect, Ellipse): + return True + elif intersect: + return all((self.tangent_lines(i)[0]).equals(o.tangent_lines(i)[0]) for i in intersect) + else: + return False + elif isinstance(o, Line2D): + hit = self.intersection(o) + if not hit: + return False + if len(hit) == 1: + return True + # might return None if it can't decide + return hit[0].equals(hit[1]) + elif isinstance(o, (Segment2D, Ray2D)): + intersect = self.intersection(o) + if len(intersect) == 1: + return o in self.tangent_lines(intersect[0])[0] + else: + return False + elif isinstance(o, Polygon): + return all(self.is_tangent(s) for s in o.sides) + elif isinstance(o, (LinearEntity3D, Point3D)): + raise TypeError('Entity must be two dimensional, not three dimensional') + else: + raise TypeError('Is_tangent not handled for %s' % func_name(o)) + + @property + def major(self): + """Longer axis of the ellipse (if it can be determined) else hradius. + + Returns + ======= + + major : number or expression + + See Also + ======== + + hradius, vradius, minor + + Examples + ======== + + >>> from sympy import Point, Ellipse, Symbol + >>> p1 = Point(0, 0) + >>> e1 = Ellipse(p1, 3, 1) + >>> e1.major + 3 + + >>> a = Symbol('a') + >>> b = Symbol('b') + >>> Ellipse(p1, a, b).major + a + >>> Ellipse(p1, b, a).major + b + + >>> m = Symbol('m') + >>> M = m + 1 + >>> Ellipse(p1, m, M).major + m + 1 + + """ + ab = self.args[1:3] + if len(ab) == 1: + return ab[0] + a, b = ab + o = b - a < 0 + if o == True: + return a + elif o == False: + return b + return self.hradius + + @property + def minor(self): + """Shorter axis of the ellipse (if it can be determined) else vradius. + + Returns + ======= + + minor : number or expression + + See Also + ======== + + hradius, vradius, major + + Examples + ======== + + >>> from sympy import Point, Ellipse, Symbol + >>> p1 = Point(0, 0) + >>> e1 = Ellipse(p1, 3, 1) + >>> e1.minor + 1 + + >>> a = Symbol('a') + >>> b = Symbol('b') + >>> Ellipse(p1, a, b).minor + b + >>> Ellipse(p1, b, a).minor + a + + >>> m = Symbol('m') + >>> M = m + 1 + >>> Ellipse(p1, m, M).minor + m + + """ + ab = self.args[1:3] + if len(ab) == 1: + return ab[0] + a, b = ab + o = a - b < 0 + if o == True: + return a + elif o == False: + return b + return self.vradius + + def normal_lines(self, p, prec=None): + """Normal lines between `p` and the ellipse. + + Parameters + ========== + + p : Point + + Returns + ======= + + normal_lines : list with 1, 2 or 4 Lines + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> e = Ellipse((0, 0), 2, 3) + >>> c = e.center + >>> e.normal_lines(c + Point(1, 0)) + [Line2D(Point2D(0, 0), Point2D(1, 0))] + >>> e.normal_lines(c) + [Line2D(Point2D(0, 0), Point2D(0, 1)), Line2D(Point2D(0, 0), Point2D(1, 0))] + + Off-axis points require the solution of a quartic equation. This + often leads to very large expressions that may be of little practical + use. An approximate solution of `prec` digits can be obtained by + passing in the desired value: + + >>> e.normal_lines((3, 3), prec=2) + [Line2D(Point2D(-0.81, -2.7), Point2D(0.19, -1.2)), + Line2D(Point2D(1.5, -2.0), Point2D(2.5, -2.7))] + + Whereas the above solution has an operation count of 12, the exact + solution has an operation count of 2020. + """ + p = Point(p, dim=2) + + # XXX change True to something like self.angle == 0 if the arbitrarily + # rotated ellipse is introduced. + # https://github.com/sympy/sympy/issues/2815) + if True: + rv = [] + if p.x == self.center.x: + rv.append(Line(self.center, slope=oo)) + if p.y == self.center.y: + rv.append(Line(self.center, slope=0)) + if rv: + # at these special orientations of p either 1 or 2 normals + # exist and we are done + return rv + + # find the 4 normal points and construct lines through them with + # the corresponding slope + eq = self.equation(x, y) + dydx = idiff(eq, y, x) + norm = -1/dydx + slope = Line(p, (x, y)).slope + seq = slope - norm + + # TODO: Replace solve with solveset, when this line is tested + yis = solve(seq, y)[0] + xeq = eq.subs(y, yis).as_numer_denom()[0].expand() + if len(xeq.free_symbols) == 1: + try: + # this is so much faster, it's worth a try + xsol = Poly(xeq, x).real_roots() + except (DomainError, PolynomialError, NotImplementedError): + # TODO: Replace solve with solveset, when these lines are tested + xsol = _nsort(solve(xeq, x), separated=True)[0] + points = [Point(i, solve(eq.subs(x, i), y)[0]) for i in xsol] + else: + raise NotImplementedError( + 'intersections for the general ellipse are not supported') + slopes = [norm.subs(zip((x, y), pt.args)) for pt in points] + if prec is not None: + points = [pt.n(prec) for pt in points] + slopes = [i if _not_a_coeff(i) else i.n(prec) for i in slopes] + return [Line(pt, slope=s) for pt, s in zip(points, slopes)] + + @property + def periapsis(self): + """The periapsis of the ellipse. + + The shortest distance between the focus and the contour. + + Returns + ======= + + periapsis : number + + See Also + ======== + + apoapsis : Returns greatest distance between focus and contour + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> p1 = Point(0, 0) + >>> e1 = Ellipse(p1, 3, 1) + >>> e1.periapsis + 3 - 2*sqrt(2) + + """ + return self.major * (1 - self.eccentricity) + + @property + def semilatus_rectum(self): + """ + Calculates the semi-latus rectum of the Ellipse. + + Semi-latus rectum is defined as one half of the chord through a + focus parallel to the conic section directrix of a conic section. + + Returns + ======= + + semilatus_rectum : number + + See Also + ======== + + apoapsis : Returns greatest distance between focus and contour + + periapsis : The shortest distance between the focus and the contour + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> p1 = Point(0, 0) + >>> e1 = Ellipse(p1, 3, 1) + >>> e1.semilatus_rectum + 1/3 + + References + ========== + + .. [1] https://mathworld.wolfram.com/SemilatusRectum.html + .. [2] https://en.wikipedia.org/wiki/Ellipse#Semi-latus_rectum + + """ + return self.major * (1 - self.eccentricity ** 2) + + def auxiliary_circle(self): + """Returns a Circle whose diameter is the major axis of the ellipse. + + Examples + ======== + + >>> from sympy import Ellipse, Point, symbols + >>> c = Point(1, 2) + >>> Ellipse(c, 8, 7).auxiliary_circle() + Circle(Point2D(1, 2), 8) + >>> a, b = symbols('a b') + >>> Ellipse(c, a, b).auxiliary_circle() + Circle(Point2D(1, 2), Max(a, b)) + """ + return Circle(self.center, Max(self.hradius, self.vradius)) + + def director_circle(self): + """ + Returns a Circle consisting of all points where two perpendicular + tangent lines to the ellipse cross each other. + + Returns + ======= + + Circle + A director circle returned as a geometric object. + + Examples + ======== + + >>> from sympy import Ellipse, Point, symbols + >>> c = Point(3,8) + >>> Ellipse(c, 7, 9).director_circle() + Circle(Point2D(3, 8), sqrt(130)) + >>> a, b = symbols('a b') + >>> Ellipse(c, a, b).director_circle() + Circle(Point2D(3, 8), sqrt(a**2 + b**2)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Director_circle + + """ + return Circle(self.center, sqrt(self.hradius**2 + self.vradius**2)) + + def plot_interval(self, parameter='t'): + """The plot interval for the default geometric plot of the Ellipse. + + Parameters + ========== + + parameter : str, optional + Default value is 't'. + + Returns + ======= + + plot_interval : list + [parameter, lower_bound, upper_bound] + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> e1 = Ellipse(Point(0, 0), 3, 2) + >>> e1.plot_interval() + [t, -pi, pi] + + """ + t = _symbol(parameter, real=True) + return [t, -S.Pi, S.Pi] + + def random_point(self, seed=None): + """A random point on the ellipse. + + Returns + ======= + + point : Point + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> e1 = Ellipse(Point(0, 0), 3, 2) + >>> e1.random_point() # gives some random point + Point2D(...) + >>> p1 = e1.random_point(seed=0); p1.n(2) + Point2D(2.1, 1.4) + + Notes + ===== + + When creating a random point, one may simply replace the + parameter with a random number. When doing so, however, the + random number should be made a Rational or else the point + may not test as being in the ellipse: + + >>> from sympy.abc import t + >>> from sympy import Rational + >>> arb = e1.arbitrary_point(t); arb + Point2D(3*cos(t), 2*sin(t)) + >>> arb.subs(t, .1) in e1 + False + >>> arb.subs(t, Rational(.1)) in e1 + True + >>> arb.subs(t, Rational('.1')) in e1 + True + + See Also + ======== + sympy.geometry.point.Point + arbitrary_point : Returns parameterized point on ellipse + """ + t = _symbol('t', real=True) + x, y = self.arbitrary_point(t).args + # get a random value in [-1, 1) corresponding to cos(t) + # and confirm that it will test as being in the ellipse + if seed is not None: + rng = random.Random(seed) + else: + rng = random + # simplify this now or else the Float will turn s into a Float + r = Rational(rng.random()) + c = 2*r - 1 + s = sqrt(1 - c**2) + return Point(x.subs(cos(t), c), y.subs(sin(t), s)) + + def reflect(self, line): + """Override GeometryEntity.reflect since the radius + is not a GeometryEntity. + + Examples + ======== + + >>> from sympy import Circle, Line + >>> Circle((0, 1), 1).reflect(Line((0, 0), (1, 1))) + Circle(Point2D(1, 0), -1) + >>> from sympy import Ellipse, Line, Point + >>> Ellipse(Point(3, 4), 1, 3).reflect(Line(Point(0, -4), Point(5, 0))) + Traceback (most recent call last): + ... + NotImplementedError: + General Ellipse is not supported but the equation of the reflected + Ellipse is given by the zeros of: f(x, y) = (9*x/41 + 40*y/41 + + 37/41)**2 + (40*x/123 - 3*y/41 - 364/123)**2 - 1 + + Notes + ===== + + Until the general ellipse (with no axis parallel to the x-axis) is + supported a NotImplemented error is raised and the equation whose + zeros define the rotated ellipse is given. + + """ + + if line.slope in (0, oo): + c = self.center + c = c.reflect(line) + return self.func(c, -self.hradius, self.vradius) + else: + x, y = [uniquely_named_symbol( + name, (self, line), modify=lambda s: '_' + s, real=True) + for name in 'xy'] + expr = self.equation(x, y) + p = Point(x, y).reflect(line) + result = expr.subs(zip((x, y), p.args + ), simultaneous=True) + raise NotImplementedError(filldedent( + 'General Ellipse is not supported but the equation ' + 'of the reflected Ellipse is given by the zeros of: ' + + "f(%s, %s) = %s" % (str(x), str(y), str(result)))) + + def rotate(self, angle=0, pt=None): + """Rotate ``angle`` radians counterclockwise about Point ``pt``. + + Note: since the general ellipse is not supported, only rotations that + are integer multiples of pi/2 are allowed. + + Examples + ======== + + >>> from sympy import Ellipse, pi + >>> Ellipse((1, 0), 2, 1).rotate(pi/2) + Ellipse(Point2D(0, 1), 1, 2) + >>> Ellipse((1, 0), 2, 1).rotate(pi) + Ellipse(Point2D(-1, 0), 2, 1) + """ + if self.hradius == self.vradius: + return self.func(self.center.rotate(angle, pt), self.hradius) + if (angle/S.Pi).is_integer: + return super().rotate(angle, pt) + if (2*angle/S.Pi).is_integer: + return self.func(self.center.rotate(angle, pt), self.vradius, self.hradius) + # XXX see https://github.com/sympy/sympy/issues/2815 for general ellipes + raise NotImplementedError('Only rotations of pi/2 are currently supported for Ellipse.') + + def scale(self, x=1, y=1, pt=None): + """Override GeometryEntity.scale since it is the major and minor + axes which must be scaled and they are not GeometryEntities. + + Examples + ======== + + >>> from sympy import Ellipse + >>> Ellipse((0, 0), 2, 1).scale(2, 4) + Circle(Point2D(0, 0), 4) + >>> Ellipse((0, 0), 2, 1).scale(2) + Ellipse(Point2D(0, 0), 4, 1) + """ + c = self.center + if pt: + pt = Point(pt, dim=2) + return self.translate(*(-pt).args).scale(x, y).translate(*pt.args) + h = self.hradius + v = self.vradius + return self.func(c.scale(x, y), hradius=h*x, vradius=v*y) + + def tangent_lines(self, p): + """Tangent lines between `p` and the ellipse. + + If `p` is on the ellipse, returns the tangent line through point `p`. + Otherwise, returns the tangent line(s) from `p` to the ellipse, or + None if no tangent line is possible (e.g., `p` inside ellipse). + + Parameters + ========== + + p : Point + + Returns + ======= + + tangent_lines : list with 1 or 2 Lines + + Raises + ====== + + NotImplementedError + Can only find tangent lines for a point, `p`, on the ellipse. + + See Also + ======== + + sympy.geometry.point.Point, sympy.geometry.line.Line + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> e1 = Ellipse(Point(0, 0), 3, 2) + >>> e1.tangent_lines(Point(3, 0)) + [Line2D(Point2D(3, 0), Point2D(3, -12))] + + """ + p = Point(p, dim=2) + if self.encloses_point(p): + return [] + + if p in self: + delta = self.center - p + rise = (self.vradius**2)*delta.x + run = -(self.hradius**2)*delta.y + p2 = Point(simplify(p.x + run), + simplify(p.y + rise)) + return [Line(p, p2)] + else: + if len(self.foci) == 2: + f1, f2 = self.foci + maj = self.hradius + test = (2*maj - + Point.distance(f1, p) - + Point.distance(f2, p)) + else: + test = self.radius - Point.distance(self.center, p) + if test.is_number and test.is_positive: + return [] + # else p is outside the ellipse or we can't tell. In case of the + # latter, the solutions returned will only be valid if + # the point is not inside the ellipse; if it is, nan will result. + eq = self.equation(x, y) + dydx = idiff(eq, y, x) + slope = Line(p, Point(x, y)).slope + + # TODO: Replace solve with solveset, when this line is tested + tangent_points = solve([slope - dydx, eq], [x, y]) + + # handle horizontal and vertical tangent lines + if len(tangent_points) == 1: + if tangent_points[0][ + 0] == p.x or tangent_points[0][1] == p.y: + return [Line(p, p + Point(1, 0)), Line(p, p + Point(0, 1))] + else: + return [Line(p, p + Point(0, 1)), Line(p, tangent_points[0])] + + # others + return [Line(p, tangent_points[0]), Line(p, tangent_points[1])] + + @property + def vradius(self): + """The vertical radius of the ellipse. + + Returns + ======= + + vradius : number + + See Also + ======== + + hradius, major, minor + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> p1 = Point(0, 0) + >>> e1 = Ellipse(p1, 3, 1) + >>> e1.vradius + 1 + + """ + return self.args[2] + + + def second_moment_of_area(self, point=None): + """Returns the second moment and product moment area of an ellipse. + + Parameters + ========== + + point : Point, two-tuple of sympifiable objects, or None(default=None) + point is the point about which second moment of area is to be found. + If "point=None" it will be calculated about the axis passing through the + centroid of the ellipse. + + Returns + ======= + + I_xx, I_yy, I_xy : number or SymPy expression + I_xx, I_yy are second moment of area of an ellise. + I_xy is product moment of area of an ellipse. + + Examples + ======== + + >>> from sympy import Point, Ellipse + >>> p1 = Point(0, 0) + >>> e1 = Ellipse(p1, 3, 1) + >>> e1.second_moment_of_area() + (3*pi/4, 27*pi/4, 0) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/List_of_second_moments_of_area + + """ + + I_xx = (S.Pi*(self.hradius)*(self.vradius**3))/4 + I_yy = (S.Pi*(self.hradius**3)*(self.vradius))/4 + I_xy = 0 + + if point is None: + return I_xx, I_yy, I_xy + + # parallel axis theorem + I_xx = I_xx + self.area*((point[1] - self.center.y)**2) + I_yy = I_yy + self.area*((point[0] - self.center.x)**2) + I_xy = I_xy + self.area*(point[0] - self.center.x)*(point[1] - self.center.y) + + return I_xx, I_yy, I_xy + + + def polar_second_moment_of_area(self): + """Returns the polar second moment of area of an Ellipse + + It is a constituent of the second moment of area, linked through + the perpendicular axis theorem. While the planar second moment of + area describes an object's resistance to deflection (bending) when + subjected to a force applied to a plane parallel to the central + axis, the polar second moment of area describes an object's + resistance to deflection when subjected to a moment applied in a + plane perpendicular to the object's central axis (i.e. parallel to + the cross-section) + + Examples + ======== + + >>> from sympy import symbols, Circle, Ellipse + >>> c = Circle((5, 5), 4) + >>> c.polar_second_moment_of_area() + 128*pi + >>> a, b = symbols('a, b') + >>> e = Ellipse((0, 0), a, b) + >>> e.polar_second_moment_of_area() + pi*a**3*b/4 + pi*a*b**3/4 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Polar_moment_of_inertia + + """ + second_moment = self.second_moment_of_area() + return second_moment[0] + second_moment[1] + + + def section_modulus(self, point=None): + """Returns a tuple with the section modulus of an ellipse + + Section modulus is a geometric property of an ellipse defined as the + ratio of second moment of area to the distance of the extreme end of + the ellipse from the centroidal axis. + + Parameters + ========== + + point : Point, two-tuple of sympifyable objects, or None(default=None) + point is the point at which section modulus is to be found. + If "point=None" section modulus will be calculated for the + point farthest from the centroidal axis of the ellipse. + + Returns + ======= + + S_x, S_y: numbers or SymPy expressions + S_x is the section modulus with respect to the x-axis + S_y is the section modulus with respect to the y-axis + A negative sign indicates that the section modulus is + determined for a point below the centroidal axis. + + Examples + ======== + + >>> from sympy import Symbol, Ellipse, Circle, Point2D + >>> d = Symbol('d', positive=True) + >>> c = Circle((0, 0), d/2) + >>> c.section_modulus() + (pi*d**3/32, pi*d**3/32) + >>> e = Ellipse(Point2D(0, 0), 2, 4) + >>> e.section_modulus() + (8*pi, 4*pi) + >>> e.section_modulus((2, 2)) + (16*pi, 4*pi) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Section_modulus + + """ + x_c, y_c = self.center + if point is None: + # taking x and y as maximum distances from centroid + x_min, y_min, x_max, y_max = self.bounds + y = max(y_c - y_min, y_max - y_c) + x = max(x_c - x_min, x_max - x_c) + else: + # taking x and y as distances of the given point from the center + point = Point2D(point) + y = point.y - y_c + x = point.x - x_c + + second_moment = self.second_moment_of_area() + S_x = second_moment[0]/y + S_y = second_moment[1]/x + + return S_x, S_y + + +class Circle(Ellipse): + r"""A circle in space. + + Constructed simply from a center and a radius, from three + non-collinear points, or the equation of a circle. + + Parameters + ========== + + center : Point + radius : number or SymPy expression + points : sequence of three Points + equation : equation of a circle + + Attributes + ========== + + radius (synonymous with hradius, vradius, major and minor) + circumference + equation + + Raises + ====== + + GeometryError + When the given equation is not that of a circle. + When trying to construct circle from incorrect parameters. + + See Also + ======== + + Ellipse, sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Point, Circle, Eq + >>> from sympy.abc import x, y, a, b + + A circle constructed from a center and radius: + + >>> c1 = Circle(Point(0, 0), 5) + >>> c1.hradius, c1.vradius, c1.radius + (5, 5, 5) + + A circle constructed from three points: + + >>> c2 = Circle(Point(0, 0), Point(1, 1), Point(1, 0)) + >>> c2.hradius, c2.vradius, c2.radius, c2.center + (sqrt(2)/2, sqrt(2)/2, sqrt(2)/2, Point2D(1/2, 1/2)) + + A circle can be constructed from an equation in the form + `ax^2 + by^2 + gx + hy + c = 0`, too: + + >>> Circle(x**2 + y**2 - 25) + Circle(Point2D(0, 0), 5) + + If the variables corresponding to x and y are named something + else, their name or symbol can be supplied: + + >>> Circle(Eq(a**2 + b**2, 25), x='a', y=b) + Circle(Point2D(0, 0), 5) + """ + + def __new__(cls, *args, **kwargs): + evaluate = kwargs.get('evaluate', global_parameters.evaluate) + if len(args) == 1 and isinstance(args[0], (Expr, Eq)): + x = kwargs.get('x', 'x') + y = kwargs.get('y', 'y') + equation = args[0].expand() + if isinstance(equation, Eq): + equation = equation.lhs - equation.rhs + x = find(x, equation) + y = find(y, equation) + + try: + a, b, c, d, e = linear_coeffs(equation, x**2, y**2, x, y) + except ValueError: + raise GeometryError("The given equation is not that of a circle.") + + if S.Zero in (a, b) or a != b: + raise GeometryError("The given equation is not that of a circle.") + + center_x = -c/a/2 + center_y = -d/b/2 + r2 = (center_x**2) + (center_y**2) - e/a + + return Circle((center_x, center_y), sqrt(r2), evaluate=evaluate) + + else: + c, r = None, None + if len(args) == 3: + args = [Point(a, dim=2, evaluate=evaluate) for a in args] + t = Triangle(*args) + if not isinstance(t, Triangle): + return t + c = t.circumcenter + r = t.circumradius + elif len(args) == 2: + # Assume (center, radius) pair + c = Point(args[0], dim=2, evaluate=evaluate) + r = args[1] + # this will prohibit imaginary radius + try: + r = Point(r, 0, evaluate=evaluate).x + except ValueError: + raise GeometryError("Circle with imaginary radius is not permitted") + + if not (c is None or r is None): + if r == 0: + return c + return GeometryEntity.__new__(cls, c, r, **kwargs) + + raise GeometryError("Circle.__new__ received unknown arguments") + + def _eval_evalf(self, prec=15, **options): + pt, r = self.args + dps = prec_to_dps(prec) + pt = pt.evalf(n=dps, **options) + r = r.evalf(n=dps, **options) + return self.func(pt, r, evaluate=False) + + @property + def circumference(self): + """The circumference of the circle. + + Returns + ======= + + circumference : number or SymPy expression + + Examples + ======== + + >>> from sympy import Point, Circle + >>> c1 = Circle(Point(3, 4), 6) + >>> c1.circumference + 12*pi + + """ + return 2 * S.Pi * self.radius + + def equation(self, x='x', y='y'): + """The equation of the circle. + + Parameters + ========== + + x : str or Symbol, optional + Default value is 'x'. + y : str or Symbol, optional + Default value is 'y'. + + Returns + ======= + + equation : SymPy expression + + Examples + ======== + + >>> from sympy import Point, Circle + >>> c1 = Circle(Point(0, 0), 5) + >>> c1.equation() + x**2 + y**2 - 25 + + """ + x = _symbol(x, real=True) + y = _symbol(y, real=True) + t1 = (x - self.center.x)**2 + t2 = (y - self.center.y)**2 + return t1 + t2 - self.major**2 + + def intersection(self, o): + """The intersection of this circle with another geometrical entity. + + Parameters + ========== + + o : GeometryEntity + + Returns + ======= + + intersection : list of GeometryEntities + + Examples + ======== + + >>> from sympy import Point, Circle, Line, Ray + >>> p1, p2, p3 = Point(0, 0), Point(5, 5), Point(6, 0) + >>> p4 = Point(5, 0) + >>> c1 = Circle(p1, 5) + >>> c1.intersection(p2) + [] + >>> c1.intersection(p4) + [Point2D(5, 0)] + >>> c1.intersection(Ray(p1, p2)) + [Point2D(5*sqrt(2)/2, 5*sqrt(2)/2)] + >>> c1.intersection(Line(p2, p3)) + [] + + """ + return Ellipse.intersection(self, o) + + @property + def radius(self): + """The radius of the circle. + + Returns + ======= + + radius : number or SymPy expression + + See Also + ======== + + Ellipse.major, Ellipse.minor, Ellipse.hradius, Ellipse.vradius + + Examples + ======== + + >>> from sympy import Point, Circle + >>> c1 = Circle(Point(3, 4), 6) + >>> c1.radius + 6 + + """ + return self.args[1] + + def reflect(self, line): + """Override GeometryEntity.reflect since the radius + is not a GeometryEntity. + + Examples + ======== + + >>> from sympy import Circle, Line + >>> Circle((0, 1), 1).reflect(Line((0, 0), (1, 1))) + Circle(Point2D(1, 0), -1) + """ + c = self.center + c = c.reflect(line) + return self.func(c, -self.radius) + + def scale(self, x=1, y=1, pt=None): + """Override GeometryEntity.scale since the radius + is not a GeometryEntity. + + Examples + ======== + + >>> from sympy import Circle + >>> Circle((0, 0), 1).scale(2, 2) + Circle(Point2D(0, 0), 2) + >>> Circle((0, 0), 1).scale(2, 4) + Ellipse(Point2D(0, 0), 2, 4) + """ + c = self.center + if pt: + pt = Point(pt, dim=2) + return self.translate(*(-pt).args).scale(x, y).translate(*pt.args) + c = c.scale(x, y) + x, y = [abs(i) for i in (x, y)] + if x == y: + return self.func(c, x*self.radius) + h = v = self.radius + return Ellipse(c, hradius=h*x, vradius=v*y) + + @property + def vradius(self): + """ + This Ellipse property is an alias for the Circle's radius. + + Whereas hradius, major and minor can use Ellipse's conventions, + the vradius does not exist for a circle. It is always a positive + value in order that the Circle, like Polygons, will have an + area that can be positive or negative as determined by the sign + of the hradius. + + Examples + ======== + + >>> from sympy import Point, Circle + >>> c1 = Circle(Point(3, 4), 6) + >>> c1.vradius + 6 + """ + return abs(self.radius) + + +from .polygon import Polygon, Triangle diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/entity.py b/venv/lib/python3.11/site-packages/sympy/geometry/entity.py new file mode 100644 index 0000000000000000000000000000000000000000..5ea1e807542c43eb955c2d778cec0f101d78bdce --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/entity.py @@ -0,0 +1,641 @@ +"""The definition of the base geometrical entity with attributes common to +all derived geometrical entities. + +Contains +======== + +GeometryEntity +GeometricSet + +Notes +===== + +A GeometryEntity is any object that has special geometric properties. +A GeometrySet is a superclass of any GeometryEntity that can also +be viewed as a sympy.sets.Set. In particular, points are the only +GeometryEntity not considered a Set. + +Rn is a GeometrySet representing n-dimensional Euclidean space. R2 and +R3 are currently the only ambient spaces implemented. + +""" +from __future__ import annotations + +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.evalf import EvalfMixin, N +from sympy.core.numbers import oo +from sympy.core.symbol import Dummy +from sympy.core.sympify import sympify +from sympy.functions.elementary.trigonometric import cos, sin, atan +from sympy.matrices import eye +from sympy.multipledispatch import dispatch +from sympy.printing import sstr +from sympy.sets import Set, Union, FiniteSet +from sympy.sets.handlers.intersection import intersection_sets +from sympy.sets.handlers.union import union_sets +from sympy.solvers.solvers import solve +from sympy.utilities.misc import func_name +from sympy.utilities.iterables import is_sequence + + +# How entities are ordered; used by __cmp__ in GeometryEntity +ordering_of_classes = [ + "Point2D", + "Point3D", + "Point", + "Segment2D", + "Ray2D", + "Line2D", + "Segment3D", + "Line3D", + "Ray3D", + "Segment", + "Ray", + "Line", + "Plane", + "Triangle", + "RegularPolygon", + "Polygon", + "Circle", + "Ellipse", + "Curve", + "Parabola" +] + + +x, y = [Dummy('entity_dummy') for i in range(2)] +T = Dummy('entity_dummy', real=True) + + +class GeometryEntity(Basic, EvalfMixin): + """The base class for all geometrical entities. + + This class does not represent any particular geometric entity, it only + provides the implementation of some methods common to all subclasses. + + """ + + __slots__: tuple[str, ...] = () + + def __cmp__(self, other): + """Comparison of two GeometryEntities.""" + n1 = self.__class__.__name__ + n2 = other.__class__.__name__ + c = (n1 > n2) - (n1 < n2) + if not c: + return 0 + + i1 = -1 + for cls in self.__class__.__mro__: + try: + i1 = ordering_of_classes.index(cls.__name__) + break + except ValueError: + i1 = -1 + if i1 == -1: + return c + + i2 = -1 + for cls in other.__class__.__mro__: + try: + i2 = ordering_of_classes.index(cls.__name__) + break + except ValueError: + i2 = -1 + if i2 == -1: + return c + + return (i1 > i2) - (i1 < i2) + + def __contains__(self, other): + """Subclasses should implement this method for anything more complex than equality.""" + if type(self) is type(other): + return self == other + raise NotImplementedError() + + def __getnewargs__(self): + """Returns a tuple that will be passed to __new__ on unpickling.""" + return tuple(self.args) + + def __ne__(self, o): + """Test inequality of two geometrical entities.""" + return not self == o + + def __new__(cls, *args, **kwargs): + # Points are sequences, but they should not + # be converted to Tuples, so use this detection function instead. + def is_seq_and_not_point(a): + # we cannot use isinstance(a, Point) since we cannot import Point + if hasattr(a, 'is_Point') and a.is_Point: + return False + return is_sequence(a) + + args = [Tuple(*a) if is_seq_and_not_point(a) else sympify(a) for a in args] + return Basic.__new__(cls, *args) + + def __radd__(self, a): + """Implementation of reverse add method.""" + return a.__add__(self) + + def __rtruediv__(self, a): + """Implementation of reverse division method.""" + return a.__truediv__(self) + + def __repr__(self): + """String representation of a GeometryEntity that can be evaluated + by sympy.""" + return type(self).__name__ + repr(self.args) + + def __rmul__(self, a): + """Implementation of reverse multiplication method.""" + return a.__mul__(self) + + def __rsub__(self, a): + """Implementation of reverse subtraction method.""" + return a.__sub__(self) + + def __str__(self): + """String representation of a GeometryEntity.""" + return type(self).__name__ + sstr(self.args) + + def _eval_subs(self, old, new): + from sympy.geometry.point import Point, Point3D + if is_sequence(old) or is_sequence(new): + if isinstance(self, Point3D): + old = Point3D(old) + new = Point3D(new) + else: + old = Point(old) + new = Point(new) + return self._subs(old, new) + + def _repr_svg_(self): + """SVG representation of a GeometryEntity suitable for IPython""" + + try: + bounds = self.bounds + except (NotImplementedError, TypeError): + # if we have no SVG representation, return None so IPython + # will fall back to the next representation + return None + + if not all(x.is_number and x.is_finite for x in bounds): + return None + + svg_top = ''' + + + + + + + + + + + ''' + + # Establish SVG canvas that will fit all the data + small space + xmin, ymin, xmax, ymax = map(N, bounds) + if xmin == xmax and ymin == ymax: + # This is a point; buffer using an arbitrary size + xmin, ymin, xmax, ymax = xmin - .5, ymin -.5, xmax + .5, ymax + .5 + else: + # Expand bounds by a fraction of the data ranges + expand = 0.1 # or 10%; this keeps arrowheads in view (R plots use 4%) + widest_part = max([xmax - xmin, ymax - ymin]) + expand_amount = widest_part * expand + xmin -= expand_amount + ymin -= expand_amount + xmax += expand_amount + ymax += expand_amount + dx = xmax - xmin + dy = ymax - ymin + width = min([max([100., dx]), 300]) + height = min([max([100., dy]), 300]) + + scale_factor = 1. if max(width, height) == 0 else max(dx, dy) / max(width, height) + try: + svg = self._svg(scale_factor) + except (NotImplementedError, TypeError): + # if we have no SVG representation, return None so IPython + # will fall back to the next representation + return None + + view_box = "{} {} {} {}".format(xmin, ymin, dx, dy) + transform = "matrix(1,0,0,-1,0,{})".format(ymax + ymin) + svg_top = svg_top.format(view_box, width, height) + + return svg_top + ( + '{}' + ).format(transform, svg) + + def _svg(self, scale_factor=1., fill_color="#66cc99"): + """Returns SVG path element for the GeometryEntity. + + Parameters + ========== + + scale_factor : float + Multiplication factor for the SVG stroke-width. Default is 1. + fill_color : str, optional + Hex string for fill color. Default is "#66cc99". + """ + raise NotImplementedError() + + def _sympy_(self): + return self + + @property + def ambient_dimension(self): + """What is the dimension of the space that the object is contained in?""" + raise NotImplementedError() + + @property + def bounds(self): + """Return a tuple (xmin, ymin, xmax, ymax) representing the bounding + rectangle for the geometric figure. + + """ + + raise NotImplementedError() + + def encloses(self, o): + """ + Return True if o is inside (not on or outside) the boundaries of self. + + The object will be decomposed into Points and individual Entities need + only define an encloses_point method for their class. + + See Also + ======== + + sympy.geometry.ellipse.Ellipse.encloses_point + sympy.geometry.polygon.Polygon.encloses_point + + Examples + ======== + + >>> from sympy import RegularPolygon, Point, Polygon + >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices) + >>> t2 = Polygon(*RegularPolygon(Point(0, 0), 2, 3).vertices) + >>> t2.encloses(t) + True + >>> t.encloses(t2) + False + + """ + + from sympy.geometry.point import Point + from sympy.geometry.line import Segment, Ray, Line + from sympy.geometry.ellipse import Ellipse + from sympy.geometry.polygon import Polygon, RegularPolygon + + if isinstance(o, Point): + return self.encloses_point(o) + elif isinstance(o, Segment): + return all(self.encloses_point(x) for x in o.points) + elif isinstance(o, (Ray, Line)): + return False + elif isinstance(o, Ellipse): + return self.encloses_point(o.center) and \ + self.encloses_point( + Point(o.center.x + o.hradius, o.center.y)) and \ + not self.intersection(o) + elif isinstance(o, Polygon): + if isinstance(o, RegularPolygon): + if not self.encloses_point(o.center): + return False + return all(self.encloses_point(v) for v in o.vertices) + raise NotImplementedError() + + def equals(self, o): + return self == o + + def intersection(self, o): + """ + Returns a list of all of the intersections of self with o. + + Notes + ===== + + An entity is not required to implement this method. + + If two different types of entities can intersect, the item with + higher index in ordering_of_classes should implement + intersections with anything having a lower index. + + See Also + ======== + + sympy.geometry.util.intersection + + """ + raise NotImplementedError() + + def is_similar(self, other): + """Is this geometrical entity similar to another geometrical entity? + + Two entities are similar if a uniform scaling (enlarging or + shrinking) of one of the entities will allow one to obtain the other. + + Notes + ===== + + This method is not intended to be used directly but rather + through the `are_similar` function found in util.py. + An entity is not required to implement this method. + If two different types of entities can be similar, it is only + required that one of them be able to determine this. + + See Also + ======== + + scale + + """ + raise NotImplementedError() + + def reflect(self, line): + """ + Reflects an object across a line. + + Parameters + ========== + + line: Line + + Examples + ======== + + >>> from sympy import pi, sqrt, Line, RegularPolygon + >>> l = Line((0, pi), slope=sqrt(2)) + >>> pent = RegularPolygon((1, 2), 1, 5) + >>> rpent = pent.reflect(l) + >>> rpent + RegularPolygon(Point2D(-2*sqrt(2)*pi/3 - 1/3 + 4*sqrt(2)/3, 2/3 + 2*sqrt(2)/3 + 2*pi/3), -1, 5, -atan(2*sqrt(2)) + 3*pi/5) + + >>> from sympy import pi, Line, Circle, Point + >>> l = Line((0, pi), slope=1) + >>> circ = Circle(Point(0, 0), 5) + >>> rcirc = circ.reflect(l) + >>> rcirc + Circle(Point2D(-pi, pi), -5) + + """ + from sympy.geometry.point import Point + + g = self + l = line + o = Point(0, 0) + if l.slope.is_zero: + v = l.args[0].y + if not v: # x-axis + return g.scale(y=-1) + reps = [(p, p.translate(y=2*(v - p.y))) for p in g.atoms(Point)] + elif l.slope is oo: + v = l.args[0].x + if not v: # y-axis + return g.scale(x=-1) + reps = [(p, p.translate(x=2*(v - p.x))) for p in g.atoms(Point)] + else: + if not hasattr(g, 'reflect') and not all( + isinstance(arg, Point) for arg in g.args): + raise NotImplementedError( + 'reflect undefined or non-Point args in %s' % g) + a = atan(l.slope) + c = l.coefficients + d = -c[-1]/c[1] # y-intercept + # apply the transform to a single point + xf = Point(x, y) + xf = xf.translate(y=-d).rotate(-a, o).scale(y=-1 + ).rotate(a, o).translate(y=d) + # replace every point using that transform + reps = [(p, xf.xreplace({x: p.x, y: p.y})) for p in g.atoms(Point)] + return g.xreplace(dict(reps)) + + def rotate(self, angle, pt=None): + """Rotate ``angle`` radians counterclockwise about Point ``pt``. + + The default pt is the origin, Point(0, 0) + + See Also + ======== + + scale, translate + + Examples + ======== + + >>> from sympy import Point, RegularPolygon, Polygon, pi + >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices) + >>> t # vertex on x axis + Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2)) + >>> t.rotate(pi/2) # vertex on y axis now + Triangle(Point2D(0, 1), Point2D(-sqrt(3)/2, -1/2), Point2D(sqrt(3)/2, -1/2)) + + """ + newargs = [] + for a in self.args: + if isinstance(a, GeometryEntity): + newargs.append(a.rotate(angle, pt)) + else: + newargs.append(a) + return type(self)(*newargs) + + def scale(self, x=1, y=1, pt=None): + """Scale the object by multiplying the x,y-coordinates by x and y. + + If pt is given, the scaling is done relative to that point; the + object is shifted by -pt, scaled, and shifted by pt. + + See Also + ======== + + rotate, translate + + Examples + ======== + + >>> from sympy import RegularPolygon, Point, Polygon + >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices) + >>> t + Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2)) + >>> t.scale(2) + Triangle(Point2D(2, 0), Point2D(-1, sqrt(3)/2), Point2D(-1, -sqrt(3)/2)) + >>> t.scale(2, 2) + Triangle(Point2D(2, 0), Point2D(-1, sqrt(3)), Point2D(-1, -sqrt(3))) + + """ + from sympy.geometry.point import Point + if pt: + pt = Point(pt, dim=2) + return self.translate(*(-pt).args).scale(x, y).translate(*pt.args) + return type(self)(*[a.scale(x, y) for a in self.args]) # if this fails, override this class + + def translate(self, x=0, y=0): + """Shift the object by adding to the x,y-coordinates the values x and y. + + See Also + ======== + + rotate, scale + + Examples + ======== + + >>> from sympy import RegularPolygon, Point, Polygon + >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices) + >>> t + Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2)) + >>> t.translate(2) + Triangle(Point2D(3, 0), Point2D(3/2, sqrt(3)/2), Point2D(3/2, -sqrt(3)/2)) + >>> t.translate(2, 2) + Triangle(Point2D(3, 2), Point2D(3/2, sqrt(3)/2 + 2), Point2D(3/2, 2 - sqrt(3)/2)) + + """ + newargs = [] + for a in self.args: + if isinstance(a, GeometryEntity): + newargs.append(a.translate(x, y)) + else: + newargs.append(a) + return self.func(*newargs) + + def parameter_value(self, other, t): + """Return the parameter corresponding to the given point. + Evaluating an arbitrary point of the entity at this parameter + value will return the given point. + + Examples + ======== + + >>> from sympy import Line, Point + >>> from sympy.abc import t + >>> a = Point(0, 0) + >>> b = Point(2, 2) + >>> Line(a, b).parameter_value((1, 1), t) + {t: 1/2} + >>> Line(a, b).arbitrary_point(t).subs(_) + Point2D(1, 1) + """ + from sympy.geometry.point import Point + if not isinstance(other, GeometryEntity): + other = Point(other, dim=self.ambient_dimension) + if not isinstance(other, Point): + raise ValueError("other must be a point") + sol = solve(self.arbitrary_point(T) - other, T, dict=True) + if not sol: + raise ValueError("Given point is not on %s" % func_name(self)) + return {t: sol[0][T]} + + +class GeometrySet(GeometryEntity, Set): + """Parent class of all GeometryEntity that are also Sets + (compatible with sympy.sets) + """ + __slots__ = () + + def _contains(self, other): + """sympy.sets uses the _contains method, so include it for compatibility.""" + + if isinstance(other, Set) and other.is_FiniteSet: + return all(self.__contains__(i) for i in other) + + return self.__contains__(other) + +@dispatch(GeometrySet, Set) # type:ignore # noqa:F811 +def union_sets(self, o): # noqa:F811 + """ Returns the union of self and o + for use with sympy.sets.Set, if possible. """ + + + # if its a FiniteSet, merge any points + # we contain and return a union with the rest + if o.is_FiniteSet: + other_points = [p for p in o if not self._contains(p)] + if len(other_points) == len(o): + return None + return Union(self, FiniteSet(*other_points)) + if self._contains(o): + return self + return None + + +@dispatch(GeometrySet, Set) # type: ignore # noqa:F811 +def intersection_sets(self, o): # noqa:F811 + """ Returns a sympy.sets.Set of intersection objects, + if possible. """ + + from sympy.geometry.point import Point + + try: + # if o is a FiniteSet, find the intersection directly + # to avoid infinite recursion + if o.is_FiniteSet: + inter = FiniteSet(*(p for p in o if self.contains(p))) + else: + inter = self.intersection(o) + except NotImplementedError: + # sympy.sets.Set.reduce expects None if an object + # doesn't know how to simplify + return None + + # put the points in a FiniteSet + points = FiniteSet(*[p for p in inter if isinstance(p, Point)]) + non_points = [p for p in inter if not isinstance(p, Point)] + + return Union(*(non_points + [points])) + +def translate(x, y): + """Return the matrix to translate a 2-D point by x and y.""" + rv = eye(3) + rv[2, 0] = x + rv[2, 1] = y + return rv + + +def scale(x, y, pt=None): + """Return the matrix to multiply a 2-D point's coordinates by x and y. + + If pt is given, the scaling is done relative to that point.""" + rv = eye(3) + rv[0, 0] = x + rv[1, 1] = y + if pt: + from sympy.geometry.point import Point + pt = Point(pt, dim=2) + tr1 = translate(*(-pt).args) + tr2 = translate(*pt.args) + return tr1*rv*tr2 + return rv + + +def rotate(th): + """Return the matrix to rotate a 2-D point about the origin by ``angle``. + + The angle is measured in radians. To Point a point about a point other + then the origin, translate the Point, do the rotation, and + translate it back: + + >>> from sympy.geometry.entity import rotate, translate + >>> from sympy import Point, pi + >>> rot_about_11 = translate(-1, -1)*rotate(pi/2)*translate(1, 1) + >>> Point(1, 1).transform(rot_about_11) + Point2D(1, 1) + >>> Point(0, 0).transform(rot_about_11) + Point2D(2, 0) + """ + s = sin(th) + rv = eye(3)*cos(th) + rv[0, 1] = s + rv[1, 0] = -s + rv[2, 2] = 1 + return rv diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/exceptions.py b/venv/lib/python3.11/site-packages/sympy/geometry/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..41d97af718de2cebad3accefcd60e43ccf74a3f6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/exceptions.py @@ -0,0 +1,5 @@ +"""Geometry Errors.""" + +class GeometryError(ValueError): + """An exception raised by classes in the geometry module.""" + pass diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/line.py b/venv/lib/python3.11/site-packages/sympy/geometry/line.py new file mode 100644 index 0000000000000000000000000000000000000000..ed73d43d0c9581f9d51f299cf4425acb11958e57 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/line.py @@ -0,0 +1,2877 @@ +"""Line-like geometrical entities. + +Contains +======== +LinearEntity +Line +Ray +Segment +LinearEntity2D +Line2D +Ray2D +Segment2D +LinearEntity3D +Line3D +Ray3D +Segment3D + +""" + +from sympy.core.containers import Tuple +from sympy.core.evalf import N +from sympy.core.expr import Expr +from sympy.core.numbers import Rational, oo, Float +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.sorting import ordered +from sympy.core.symbol import _symbol, Dummy, uniquely_named_symbol +from sympy.core.sympify import sympify +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (_pi_coeff, acos, tan, atan2) +from .entity import GeometryEntity, GeometrySet +from .exceptions import GeometryError +from .point import Point, Point3D +from .util import find, intersection +from sympy.logic.boolalg import And +from sympy.matrices import Matrix +from sympy.sets.sets import Intersection +from sympy.simplify.simplify import simplify +from sympy.solvers.solvers import solve +from sympy.solvers.solveset import linear_coeffs +from sympy.utilities.misc import Undecidable, filldedent + + +import random + + +t, u = [Dummy('line_dummy') for i in range(2)] + + +class LinearEntity(GeometrySet): + """A base class for all linear entities (Line, Ray and Segment) + in n-dimensional Euclidean space. + + Attributes + ========== + + ambient_dimension + direction + length + p1 + p2 + points + + Notes + ===== + + This is an abstract class and is not meant to be instantiated. + + See Also + ======== + + sympy.geometry.entity.GeometryEntity + + """ + def __new__(cls, p1, p2=None, **kwargs): + p1, p2 = Point._normalize_dimension(p1, p2) + if p1 == p2: + # sometimes we return a single point if we are not given two unique + # points. This is done in the specific subclass + raise ValueError( + "%s.__new__ requires two unique Points." % cls.__name__) + if len(p1) != len(p2): + raise ValueError( + "%s.__new__ requires two Points of equal dimension." % cls.__name__) + + return GeometryEntity.__new__(cls, p1, p2, **kwargs) + + def __contains__(self, other): + """Return a definitive answer or else raise an error if it cannot + be determined that other is on the boundaries of self.""" + result = self.contains(other) + + if result is not None: + return result + else: + raise Undecidable( + "Cannot decide whether '%s' contains '%s'" % (self, other)) + + def _span_test(self, other): + """Test whether the point `other` lies in the positive span of `self`. + A point x is 'in front' of a point y if x.dot(y) >= 0. Return + -1 if `other` is behind `self.p1`, 0 if `other` is `self.p1` and + and 1 if `other` is in front of `self.p1`.""" + if self.p1 == other: + return 0 + + rel_pos = other - self.p1 + d = self.direction + if d.dot(rel_pos) > 0: + return 1 + return -1 + + @property + def ambient_dimension(self): + """A property method that returns the dimension of LinearEntity + object. + + Parameters + ========== + + p1 : LinearEntity + + Returns + ======= + + dimension : integer + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2 = Point(0, 0), Point(1, 1) + >>> l1 = Line(p1, p2) + >>> l1.ambient_dimension + 2 + + >>> from sympy import Point, Line + >>> p1, p2 = Point(0, 0, 0), Point(1, 1, 1) + >>> l1 = Line(p1, p2) + >>> l1.ambient_dimension + 3 + + """ + return len(self.p1) + + def angle_between(l1, l2): + """Return the non-reflex angle formed by rays emanating from + the origin with directions the same as the direction vectors + of the linear entities. + + Parameters + ========== + + l1 : LinearEntity + l2 : LinearEntity + + Returns + ======= + + angle : angle in radians + + Notes + ===== + + From the dot product of vectors v1 and v2 it is known that: + + ``dot(v1, v2) = |v1|*|v2|*cos(A)`` + + where A is the angle formed between the two vectors. We can + get the directional vectors of the two lines and readily + find the angle between the two using the above formula. + + See Also + ======== + + is_perpendicular, Ray2D.closing_angle + + Examples + ======== + + >>> from sympy import Line + >>> e = Line((0, 0), (1, 0)) + >>> ne = Line((0, 0), (1, 1)) + >>> sw = Line((1, 1), (0, 0)) + >>> ne.angle_between(e) + pi/4 + >>> sw.angle_between(e) + 3*pi/4 + + To obtain the non-obtuse angle at the intersection of lines, use + the ``smallest_angle_between`` method: + + >>> sw.smallest_angle_between(e) + pi/4 + + >>> from sympy import Point3D, Line3D + >>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(1, 1, 1), Point3D(-1, 2, 0) + >>> l1, l2 = Line3D(p1, p2), Line3D(p2, p3) + >>> l1.angle_between(l2) + acos(-sqrt(2)/3) + >>> l1.smallest_angle_between(l2) + acos(sqrt(2)/3) + """ + if not isinstance(l1, LinearEntity) and not isinstance(l2, LinearEntity): + raise TypeError('Must pass only LinearEntity objects') + + v1, v2 = l1.direction, l2.direction + return acos(v1.dot(v2)/(abs(v1)*abs(v2))) + + def smallest_angle_between(l1, l2): + """Return the smallest angle formed at the intersection of the + lines containing the linear entities. + + Parameters + ========== + + l1 : LinearEntity + l2 : LinearEntity + + Returns + ======= + + angle : angle in radians + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2, p3 = Point(0, 0), Point(0, 4), Point(2, -2) + >>> l1, l2 = Line(p1, p2), Line(p1, p3) + >>> l1.smallest_angle_between(l2) + pi/4 + + See Also + ======== + + angle_between, is_perpendicular, Ray2D.closing_angle + """ + if not isinstance(l1, LinearEntity) and not isinstance(l2, LinearEntity): + raise TypeError('Must pass only LinearEntity objects') + + v1, v2 = l1.direction, l2.direction + return acos(abs(v1.dot(v2))/(abs(v1)*abs(v2))) + + def arbitrary_point(self, parameter='t'): + """A parameterized point on the Line. + + Parameters + ========== + + parameter : str, optional + The name of the parameter which will be used for the parametric + point. The default value is 't'. When this parameter is 0, the + first point used to define the line will be returned, and when + it is 1 the second point will be returned. + + Returns + ======= + + point : Point + + Raises + ====== + + ValueError + When ``parameter`` already appears in the Line's definition. + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2 = Point(1, 0), Point(5, 3) + >>> l1 = Line(p1, p2) + >>> l1.arbitrary_point() + Point2D(4*t + 1, 3*t) + >>> from sympy import Point3D, Line3D + >>> p1, p2 = Point3D(1, 0, 0), Point3D(5, 3, 1) + >>> l1 = Line3D(p1, p2) + >>> l1.arbitrary_point() + Point3D(4*t + 1, 3*t, t) + + """ + t = _symbol(parameter, real=True) + if t.name in (f.name for f in self.free_symbols): + raise ValueError(filldedent(''' + Symbol %s already appears in object + and cannot be used as a parameter. + ''' % t.name)) + # multiply on the right so the variable gets + # combined with the coordinates of the point + return self.p1 + (self.p2 - self.p1)*t + + @staticmethod + def are_concurrent(*lines): + """Is a sequence of linear entities concurrent? + + Two or more linear entities are concurrent if they all + intersect at a single point. + + Parameters + ========== + + lines + A sequence of linear entities. + + Returns + ======= + + True : if the set of linear entities intersect in one point + False : otherwise. + + See Also + ======== + + sympy.geometry.util.intersection + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2 = Point(0, 0), Point(3, 5) + >>> p3, p4 = Point(-2, -2), Point(0, 2) + >>> l1, l2, l3 = Line(p1, p2), Line(p1, p3), Line(p1, p4) + >>> Line.are_concurrent(l1, l2, l3) + True + >>> l4 = Line(p2, p3) + >>> Line.are_concurrent(l2, l3, l4) + False + >>> from sympy import Point3D, Line3D + >>> p1, p2 = Point3D(0, 0, 0), Point3D(3, 5, 2) + >>> p3, p4 = Point3D(-2, -2, -2), Point3D(0, 2, 1) + >>> l1, l2, l3 = Line3D(p1, p2), Line3D(p1, p3), Line3D(p1, p4) + >>> Line3D.are_concurrent(l1, l2, l3) + True + >>> l4 = Line3D(p2, p3) + >>> Line3D.are_concurrent(l2, l3, l4) + False + + """ + common_points = Intersection(*lines) + if common_points.is_FiniteSet and len(common_points) == 1: + return True + return False + + def contains(self, other): + """Subclasses should implement this method and should return + True if other is on the boundaries of self; + False if not on the boundaries of self; + None if a determination cannot be made.""" + raise NotImplementedError() + + @property + def direction(self): + """The direction vector of the LinearEntity. + + Returns + ======= + + p : a Point; the ray from the origin to this point is the + direction of `self` + + Examples + ======== + + >>> from sympy import Line + >>> a, b = (1, 1), (1, 3) + >>> Line(a, b).direction + Point2D(0, 2) + >>> Line(b, a).direction + Point2D(0, -2) + + This can be reported so the distance from the origin is 1: + + >>> Line(b, a).direction.unit + Point2D(0, -1) + + See Also + ======== + + sympy.geometry.point.Point.unit + + """ + return self.p2 - self.p1 + + def intersection(self, other): + """The intersection with another geometrical entity. + + Parameters + ========== + + o : Point or LinearEntity + + Returns + ======= + + intersection : list of geometrical entities + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Point, Line, Segment + >>> p1, p2, p3 = Point(0, 0), Point(1, 1), Point(7, 7) + >>> l1 = Line(p1, p2) + >>> l1.intersection(p3) + [Point2D(7, 7)] + >>> p4, p5 = Point(5, 0), Point(0, 3) + >>> l2 = Line(p4, p5) + >>> l1.intersection(l2) + [Point2D(15/8, 15/8)] + >>> p6, p7 = Point(0, 5), Point(2, 6) + >>> s1 = Segment(p6, p7) + >>> l1.intersection(s1) + [] + >>> from sympy import Point3D, Line3D, Segment3D + >>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(1, 1, 1), Point3D(7, 7, 7) + >>> l1 = Line3D(p1, p2) + >>> l1.intersection(p3) + [Point3D(7, 7, 7)] + >>> l1 = Line3D(Point3D(4,19,12), Point3D(5,25,17)) + >>> l2 = Line3D(Point3D(-3, -15, -19), direction_ratio=[2,8,8]) + >>> l1.intersection(l2) + [Point3D(1, 1, -3)] + >>> p6, p7 = Point3D(0, 5, 2), Point3D(2, 6, 3) + >>> s1 = Segment3D(p6, p7) + >>> l1.intersection(s1) + [] + + """ + def intersect_parallel_rays(ray1, ray2): + if ray1.direction.dot(ray2.direction) > 0: + # rays point in the same direction + # so return the one that is "in front" + return [ray2] if ray1._span_test(ray2.p1) >= 0 else [ray1] + else: + # rays point in opposite directions + st = ray1._span_test(ray2.p1) + if st < 0: + return [] + elif st == 0: + return [ray2.p1] + return [Segment(ray1.p1, ray2.p1)] + + def intersect_parallel_ray_and_segment(ray, seg): + st1, st2 = ray._span_test(seg.p1), ray._span_test(seg.p2) + if st1 < 0 and st2 < 0: + return [] + elif st1 >= 0 and st2 >= 0: + return [seg] + elif st1 >= 0: # st2 < 0: + return [Segment(ray.p1, seg.p1)] + else: # st1 < 0 and st2 >= 0: + return [Segment(ray.p1, seg.p2)] + + def intersect_parallel_segments(seg1, seg2): + if seg1.contains(seg2): + return [seg2] + if seg2.contains(seg1): + return [seg1] + + # direct the segments so they're oriented the same way + if seg1.direction.dot(seg2.direction) < 0: + seg2 = Segment(seg2.p2, seg2.p1) + # order the segments so seg1 is "behind" seg2 + if seg1._span_test(seg2.p1) < 0: + seg1, seg2 = seg2, seg1 + if seg2._span_test(seg1.p2) < 0: + return [] + return [Segment(seg2.p1, seg1.p2)] + + if not isinstance(other, GeometryEntity): + other = Point(other, dim=self.ambient_dimension) + if other.is_Point: + if self.contains(other): + return [other] + else: + return [] + elif isinstance(other, LinearEntity): + # break into cases based on whether + # the lines are parallel, non-parallel intersecting, or skew + pts = Point._normalize_dimension(self.p1, self.p2, other.p1, other.p2) + rank = Point.affine_rank(*pts) + + if rank == 1: + # we're collinear + if isinstance(self, Line): + return [other] + if isinstance(other, Line): + return [self] + + if isinstance(self, Ray) and isinstance(other, Ray): + return intersect_parallel_rays(self, other) + if isinstance(self, Ray) and isinstance(other, Segment): + return intersect_parallel_ray_and_segment(self, other) + if isinstance(self, Segment) and isinstance(other, Ray): + return intersect_parallel_ray_and_segment(other, self) + if isinstance(self, Segment) and isinstance(other, Segment): + return intersect_parallel_segments(self, other) + elif rank == 2: + # we're in the same plane + l1 = Line(*pts[:2]) + l2 = Line(*pts[2:]) + + # check to see if we're parallel. If we are, we can't + # be intersecting, since the collinear case was already + # handled + if l1.direction.is_scalar_multiple(l2.direction): + return [] + + # find the intersection as if everything were lines + # by solving the equation t*d + p1 == s*d' + p1' + m = Matrix([l1.direction, -l2.direction]).transpose() + v = Matrix([l2.p1 - l1.p1]).transpose() + + # we cannot use m.solve(v) because that only works for square matrices + m_rref, pivots = m.col_insert(2, v).rref(simplify=True) + # rank == 2 ensures we have 2 pivots, but let's check anyway + if len(pivots) != 2: + raise GeometryError("Failed when solving Mx=b when M={} and b={}".format(m, v)) + coeff = m_rref[0, 2] + line_intersection = l1.direction*coeff + self.p1 + + # if both are lines, skip a containment check + if isinstance(self, Line) and isinstance(other, Line): + return [line_intersection] + + if ((isinstance(self, Line) or + self.contains(line_intersection)) and + other.contains(line_intersection)): + return [line_intersection] + if not self.atoms(Float) and not other.atoms(Float): + # if it can fail when there are no Floats then + # maybe the following parametric check should be + # done + return [] + # floats may fail exact containment so check that the + # arbitrary points, when equal, both give a + # non-negative parameter when the arbitrary point + # coordinates are equated + tu = solve(self.arbitrary_point(t) - other.arbitrary_point(u), + t, u, dict=True)[0] + def ok(p, l): + if isinstance(l, Line): + # p > -oo + return True + if isinstance(l, Ray): + # p >= 0 + return p.is_nonnegative + if isinstance(l, Segment): + # 0 <= p <= 1 + return p.is_nonnegative and (1 - p).is_nonnegative + raise ValueError("unexpected line type") + if ok(tu[t], self) and ok(tu[u], other): + return [line_intersection] + return [] + else: + # we're skew + return [] + + return other.intersection(self) + + def is_parallel(l1, l2): + """Are two linear entities parallel? + + Parameters + ========== + + l1 : LinearEntity + l2 : LinearEntity + + Returns + ======= + + True : if l1 and l2 are parallel, + False : otherwise. + + See Also + ======== + + coefficients + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2 = Point(0, 0), Point(1, 1) + >>> p3, p4 = Point(3, 4), Point(6, 7) + >>> l1, l2 = Line(p1, p2), Line(p3, p4) + >>> Line.is_parallel(l1, l2) + True + >>> p5 = Point(6, 6) + >>> l3 = Line(p3, p5) + >>> Line.is_parallel(l1, l3) + False + >>> from sympy import Point3D, Line3D + >>> p1, p2 = Point3D(0, 0, 0), Point3D(3, 4, 5) + >>> p3, p4 = Point3D(2, 1, 1), Point3D(8, 9, 11) + >>> l1, l2 = Line3D(p1, p2), Line3D(p3, p4) + >>> Line3D.is_parallel(l1, l2) + True + >>> p5 = Point3D(6, 6, 6) + >>> l3 = Line3D(p3, p5) + >>> Line3D.is_parallel(l1, l3) + False + + """ + if not isinstance(l1, LinearEntity) and not isinstance(l2, LinearEntity): + raise TypeError('Must pass only LinearEntity objects') + + return l1.direction.is_scalar_multiple(l2.direction) + + def is_perpendicular(l1, l2): + """Are two linear entities perpendicular? + + Parameters + ========== + + l1 : LinearEntity + l2 : LinearEntity + + Returns + ======= + + True : if l1 and l2 are perpendicular, + False : otherwise. + + See Also + ======== + + coefficients + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2, p3 = Point(0, 0), Point(1, 1), Point(-1, 1) + >>> l1, l2 = Line(p1, p2), Line(p1, p3) + >>> l1.is_perpendicular(l2) + True + >>> p4 = Point(5, 3) + >>> l3 = Line(p1, p4) + >>> l1.is_perpendicular(l3) + False + >>> from sympy import Point3D, Line3D + >>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(1, 1, 1), Point3D(-1, 2, 0) + >>> l1, l2 = Line3D(p1, p2), Line3D(p2, p3) + >>> l1.is_perpendicular(l2) + False + >>> p4 = Point3D(5, 3, 7) + >>> l3 = Line3D(p1, p4) + >>> l1.is_perpendicular(l3) + False + + """ + if not isinstance(l1, LinearEntity) and not isinstance(l2, LinearEntity): + raise TypeError('Must pass only LinearEntity objects') + + return S.Zero.equals(l1.direction.dot(l2.direction)) + + def is_similar(self, other): + """ + Return True if self and other are contained in the same line. + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2, p3 = Point(0, 1), Point(3, 4), Point(2, 3) + >>> l1 = Line(p1, p2) + >>> l2 = Line(p1, p3) + >>> l1.is_similar(l2) + True + """ + l = Line(self.p1, self.p2) + return l.contains(other) + + @property + def length(self): + """ + The length of the line. + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2 = Point(0, 0), Point(3, 5) + >>> l1 = Line(p1, p2) + >>> l1.length + oo + """ + return S.Infinity + + @property + def p1(self): + """The first defining point of a linear entity. + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2 = Point(0, 0), Point(5, 3) + >>> l = Line(p1, p2) + >>> l.p1 + Point2D(0, 0) + + """ + return self.args[0] + + @property + def p2(self): + """The second defining point of a linear entity. + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2 = Point(0, 0), Point(5, 3) + >>> l = Line(p1, p2) + >>> l.p2 + Point2D(5, 3) + + """ + return self.args[1] + + def parallel_line(self, p): + """Create a new Line parallel to this linear entity which passes + through the point `p`. + + Parameters + ========== + + p : Point + + Returns + ======= + + line : Line + + See Also + ======== + + is_parallel + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2, p3 = Point(0, 0), Point(2, 3), Point(-2, 2) + >>> l1 = Line(p1, p2) + >>> l2 = l1.parallel_line(p3) + >>> p3 in l2 + True + >>> l1.is_parallel(l2) + True + >>> from sympy import Point3D, Line3D + >>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(2, 3, 4), Point3D(-2, 2, 0) + >>> l1 = Line3D(p1, p2) + >>> l2 = l1.parallel_line(p3) + >>> p3 in l2 + True + >>> l1.is_parallel(l2) + True + + """ + p = Point(p, dim=self.ambient_dimension) + return Line(p, p + self.direction) + + def perpendicular_line(self, p): + """Create a new Line perpendicular to this linear entity which passes + through the point `p`. + + Parameters + ========== + + p : Point + + Returns + ======= + + line : Line + + See Also + ======== + + sympy.geometry.line.LinearEntity.is_perpendicular, perpendicular_segment + + Examples + ======== + + >>> from sympy import Point3D, Line3D + >>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(2, 3, 4), Point3D(-2, 2, 0) + >>> L = Line3D(p1, p2) + >>> P = L.perpendicular_line(p3); P + Line3D(Point3D(-2, 2, 0), Point3D(4/29, 6/29, 8/29)) + >>> L.is_perpendicular(P) + True + + In 3D the, the first point used to define the line is the point + through which the perpendicular was required to pass; the + second point is (arbitrarily) contained in the given line: + + >>> P.p2 in L + True + """ + p = Point(p, dim=self.ambient_dimension) + if p in self: + p = p + self.direction.orthogonal_direction + return Line(p, self.projection(p)) + + def perpendicular_segment(self, p): + """Create a perpendicular line segment from `p` to this line. + + The endpoints of the segment are ``p`` and the closest point in + the line containing self. (If self is not a line, the point might + not be in self.) + + Parameters + ========== + + p : Point + + Returns + ======= + + segment : Segment + + Notes + ===== + + Returns `p` itself if `p` is on this linear entity. + + See Also + ======== + + perpendicular_line + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2, p3 = Point(0, 0), Point(1, 1), Point(0, 2) + >>> l1 = Line(p1, p2) + >>> s1 = l1.perpendicular_segment(p3) + >>> l1.is_perpendicular(s1) + True + >>> p3 in s1 + True + >>> l1.perpendicular_segment(Point(4, 0)) + Segment2D(Point2D(4, 0), Point2D(2, 2)) + >>> from sympy import Point3D, Line3D + >>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(1, 1, 1), Point3D(0, 2, 0) + >>> l1 = Line3D(p1, p2) + >>> s1 = l1.perpendicular_segment(p3) + >>> l1.is_perpendicular(s1) + True + >>> p3 in s1 + True + >>> l1.perpendicular_segment(Point3D(4, 0, 0)) + Segment3D(Point3D(4, 0, 0), Point3D(4/3, 4/3, 4/3)) + + """ + p = Point(p, dim=self.ambient_dimension) + if p in self: + return p + l = self.perpendicular_line(p) + # The intersection should be unique, so unpack the singleton + p2, = Intersection(Line(self.p1, self.p2), l) + + return Segment(p, p2) + + @property + def points(self): + """The two points used to define this linear entity. + + Returns + ======= + + points : tuple of Points + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2 = Point(0, 0), Point(5, 11) + >>> l1 = Line(p1, p2) + >>> l1.points + (Point2D(0, 0), Point2D(5, 11)) + + """ + return (self.p1, self.p2) + + def projection(self, other): + """Project a point, line, ray, or segment onto this linear entity. + + Parameters + ========== + + other : Point or LinearEntity (Line, Ray, Segment) + + Returns + ======= + + projection : Point or LinearEntity (Line, Ray, Segment) + The return type matches the type of the parameter ``other``. + + Raises + ====== + + GeometryError + When method is unable to perform projection. + + Notes + ===== + + A projection involves taking the two points that define + the linear entity and projecting those points onto a + Line and then reforming the linear entity using these + projections. + A point P is projected onto a line L by finding the point + on L that is closest to P. This point is the intersection + of L and the line perpendicular to L that passes through P. + + See Also + ======== + + sympy.geometry.point.Point, perpendicular_line + + Examples + ======== + + >>> from sympy import Point, Line, Segment, Rational + >>> p1, p2, p3 = Point(0, 0), Point(1, 1), Point(Rational(1, 2), 0) + >>> l1 = Line(p1, p2) + >>> l1.projection(p3) + Point2D(1/4, 1/4) + >>> p4, p5 = Point(10, 0), Point(12, 1) + >>> s1 = Segment(p4, p5) + >>> l1.projection(s1) + Segment2D(Point2D(5, 5), Point2D(13/2, 13/2)) + >>> p1, p2, p3 = Point(0, 0, 1), Point(1, 1, 2), Point(2, 0, 1) + >>> l1 = Line(p1, p2) + >>> l1.projection(p3) + Point3D(2/3, 2/3, 5/3) + >>> p4, p5 = Point(10, 0, 1), Point(12, 1, 3) + >>> s1 = Segment(p4, p5) + >>> l1.projection(s1) + Segment3D(Point3D(10/3, 10/3, 13/3), Point3D(5, 5, 6)) + + """ + if not isinstance(other, GeometryEntity): + other = Point(other, dim=self.ambient_dimension) + + def proj_point(p): + return Point.project(p - self.p1, self.direction) + self.p1 + + if isinstance(other, Point): + return proj_point(other) + elif isinstance(other, LinearEntity): + p1, p2 = proj_point(other.p1), proj_point(other.p2) + # test to see if we're degenerate + if p1 == p2: + return p1 + projected = other.__class__(p1, p2) + projected = Intersection(self, projected) + if projected.is_empty: + return projected + # if we happen to have intersected in only a point, return that + if projected.is_FiniteSet and len(projected) == 1: + # projected is a set of size 1, so unpack it in `a` + a, = projected + return a + # order args so projection is in the same direction as self + if self.direction.dot(projected.direction) < 0: + p1, p2 = projected.args + projected = projected.func(p2, p1) + return projected + + raise GeometryError( + "Do not know how to project %s onto %s" % (other, self)) + + def random_point(self, seed=None): + """A random point on a LinearEntity. + + Returns + ======= + + point : Point + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Point, Line, Ray, Segment + >>> p1, p2 = Point(0, 0), Point(5, 3) + >>> line = Line(p1, p2) + >>> r = line.random_point(seed=42) # seed value is optional + >>> r.n(3) + Point2D(-0.72, -0.432) + >>> r in line + True + >>> Ray(p1, p2).random_point(seed=42).n(3) + Point2D(0.72, 0.432) + >>> Segment(p1, p2).random_point(seed=42).n(3) + Point2D(3.2, 1.92) + + """ + if seed is not None: + rng = random.Random(seed) + else: + rng = random + pt = self.arbitrary_point(t) + if isinstance(self, Ray): + v = abs(rng.gauss(0, 1)) + elif isinstance(self, Segment): + v = rng.random() + elif isinstance(self, Line): + v = rng.gauss(0, 1) + else: + raise NotImplementedError('unhandled line type') + return pt.subs(t, Rational(v)) + + def bisectors(self, other): + """Returns the perpendicular lines which pass through the intersections + of self and other that are in the same plane. + + Parameters + ========== + + line : Line3D + + Returns + ======= + + list: two Line instances + + Examples + ======== + + >>> from sympy import Point3D, Line3D + >>> r1 = Line3D(Point3D(0, 0, 0), Point3D(1, 0, 0)) + >>> r2 = Line3D(Point3D(0, 0, 0), Point3D(0, 1, 0)) + >>> r1.bisectors(r2) + [Line3D(Point3D(0, 0, 0), Point3D(1, 1, 0)), Line3D(Point3D(0, 0, 0), Point3D(1, -1, 0))] + + """ + if not isinstance(other, LinearEntity): + raise GeometryError("Expecting LinearEntity, not %s" % other) + + l1, l2 = self, other + + # make sure dimensions match or else a warning will rise from + # intersection calculation + if l1.p1.ambient_dimension != l2.p1.ambient_dimension: + if isinstance(l1, Line2D): + l1, l2 = l2, l1 + _, p1 = Point._normalize_dimension(l1.p1, l2.p1, on_morph='ignore') + _, p2 = Point._normalize_dimension(l1.p2, l2.p2, on_morph='ignore') + l2 = Line(p1, p2) + + point = intersection(l1, l2) + + # Three cases: Lines may intersect in a point, may be equal or may not intersect. + if not point: + raise GeometryError("The lines do not intersect") + else: + pt = point[0] + if isinstance(pt, Line): + # Intersection is a line because both lines are coincident + return [self] + + + d1 = l1.direction.unit + d2 = l2.direction.unit + + bis1 = Line(pt, pt + d1 + d2) + bis2 = Line(pt, pt + d1 - d2) + + return [bis1, bis2] + + +class Line(LinearEntity): + """An infinite line in space. + + A 2D line is declared with two distinct points, point and slope, or + an equation. A 3D line may be defined with a point and a direction ratio. + + Parameters + ========== + + p1 : Point + p2 : Point + slope : SymPy expression + direction_ratio : list + equation : equation of a line + + Notes + ===== + + `Line` will automatically subclass to `Line2D` or `Line3D` based + on the dimension of `p1`. The `slope` argument is only relevant + for `Line2D` and the `direction_ratio` argument is only relevant + for `Line3D`. + + The order of the points will define the direction of the line + which is used when calculating the angle between lines. + + See Also + ======== + + sympy.geometry.point.Point + sympy.geometry.line.Line2D + sympy.geometry.line.Line3D + + Examples + ======== + + >>> from sympy import Line, Segment, Point, Eq + >>> from sympy.abc import x, y, a, b + + >>> L = Line(Point(2,3), Point(3,5)) + >>> L + Line2D(Point2D(2, 3), Point2D(3, 5)) + >>> L.points + (Point2D(2, 3), Point2D(3, 5)) + >>> L.equation() + -2*x + y + 1 + >>> L.coefficients + (-2, 1, 1) + + Instantiate with keyword ``slope``: + + >>> Line(Point(0, 0), slope=0) + Line2D(Point2D(0, 0), Point2D(1, 0)) + + Instantiate with another linear object + + >>> s = Segment((0, 0), (0, 1)) + >>> Line(s).equation() + x + + The line corresponding to an equation in the for `ax + by + c = 0`, + can be entered: + + >>> Line(3*x + y + 18) + Line2D(Point2D(0, -18), Point2D(1, -21)) + + If `x` or `y` has a different name, then they can be specified, too, + as a string (to match the name) or symbol: + + >>> Line(Eq(3*a + b, -18), x='a', y=b) + Line2D(Point2D(0, -18), Point2D(1, -21)) + """ + def __new__(cls, *args, **kwargs): + if len(args) == 1 and isinstance(args[0], (Expr, Eq)): + missing = uniquely_named_symbol('?', args) + if not kwargs: + x = 'x' + y = 'y' + else: + x = kwargs.pop('x', missing) + y = kwargs.pop('y', missing) + if kwargs: + raise ValueError('expecting only x and y as keywords') + + equation = args[0] + if isinstance(equation, Eq): + equation = equation.lhs - equation.rhs + + def find_or_missing(x): + try: + return find(x, equation) + except ValueError: + return missing + x = find_or_missing(x) + y = find_or_missing(y) + + a, b, c = linear_coeffs(equation, x, y) + + if b: + return Line((0, -c/b), slope=-a/b) + if a: + return Line((-c/a, 0), slope=oo) + + raise ValueError('not found in equation: %s' % (set('xy') - {x, y})) + + else: + if len(args) > 0: + p1 = args[0] + if len(args) > 1: + p2 = args[1] + else: + p2 = None + + if isinstance(p1, LinearEntity): + if p2: + raise ValueError('If p1 is a LinearEntity, p2 must be None.') + dim = len(p1.p1) + else: + p1 = Point(p1) + dim = len(p1) + if p2 is not None or isinstance(p2, Point) and p2.ambient_dimension != dim: + p2 = Point(p2) + + if dim == 2: + return Line2D(p1, p2, **kwargs) + elif dim == 3: + return Line3D(p1, p2, **kwargs) + return LinearEntity.__new__(cls, p1, p2, **kwargs) + + def contains(self, other): + """ + Return True if `other` is on this Line, or False otherwise. + + Examples + ======== + + >>> from sympy import Line,Point + >>> p1, p2 = Point(0, 1), Point(3, 4) + >>> l = Line(p1, p2) + >>> l.contains(p1) + True + >>> l.contains((0, 1)) + True + >>> l.contains((0, 0)) + False + >>> a = (0, 0, 0) + >>> b = (1, 1, 1) + >>> c = (2, 2, 2) + >>> l1 = Line(a, b) + >>> l2 = Line(b, a) + >>> l1 == l2 + False + >>> l1 in l2 + True + + """ + if not isinstance(other, GeometryEntity): + other = Point(other, dim=self.ambient_dimension) + if isinstance(other, Point): + return Point.is_collinear(other, self.p1, self.p2) + if isinstance(other, LinearEntity): + return Point.is_collinear(self.p1, self.p2, other.p1, other.p2) + return False + + def distance(self, other): + """ + Finds the shortest distance between a line and a point. + + Raises + ====== + + NotImplementedError is raised if `other` is not a Point + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2 = Point(0, 0), Point(1, 1) + >>> s = Line(p1, p2) + >>> s.distance(Point(-1, 1)) + sqrt(2) + >>> s.distance((-1, 2)) + 3*sqrt(2)/2 + >>> p1, p2 = Point(0, 0, 0), Point(1, 1, 1) + >>> s = Line(p1, p2) + >>> s.distance(Point(-1, 1, 1)) + 2*sqrt(6)/3 + >>> s.distance((-1, 1, 1)) + 2*sqrt(6)/3 + + """ + if not isinstance(other, GeometryEntity): + other = Point(other, dim=self.ambient_dimension) + if self.contains(other): + return S.Zero + return self.perpendicular_segment(other).length + + def equals(self, other): + """Returns True if self and other are the same mathematical entities""" + if not isinstance(other, Line): + return False + return Point.is_collinear(self.p1, other.p1, self.p2, other.p2) + + def plot_interval(self, parameter='t'): + """The plot interval for the default geometric plot of line. Gives + values that will produce a line that is +/- 5 units long (where a + unit is the distance between the two points that define the line). + + Parameters + ========== + + parameter : str, optional + Default value is 't'. + + Returns + ======= + + plot_interval : list (plot interval) + [parameter, lower_bound, upper_bound] + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2 = Point(0, 0), Point(5, 3) + >>> l1 = Line(p1, p2) + >>> l1.plot_interval() + [t, -5, 5] + + """ + t = _symbol(parameter, real=True) + return [t, -5, 5] + + +class Ray(LinearEntity): + """A Ray is a semi-line in the space with a source point and a direction. + + Parameters + ========== + + p1 : Point + The source of the Ray + p2 : Point or radian value + This point determines the direction in which the Ray propagates. + If given as an angle it is interpreted in radians with the positive + direction being ccw. + + Attributes + ========== + + source + + See Also + ======== + + sympy.geometry.line.Ray2D + sympy.geometry.line.Ray3D + sympy.geometry.point.Point + sympy.geometry.line.Line + + Notes + ===== + + `Ray` will automatically subclass to `Ray2D` or `Ray3D` based on the + dimension of `p1`. + + Examples + ======== + + >>> from sympy import Ray, Point, pi + >>> r = Ray(Point(2, 3), Point(3, 5)) + >>> r + Ray2D(Point2D(2, 3), Point2D(3, 5)) + >>> r.points + (Point2D(2, 3), Point2D(3, 5)) + >>> r.source + Point2D(2, 3) + >>> r.xdirection + oo + >>> r.ydirection + oo + >>> r.slope + 2 + >>> Ray(Point(0, 0), angle=pi/4).slope + 1 + + """ + def __new__(cls, p1, p2=None, **kwargs): + p1 = Point(p1) + if p2 is not None: + p1, p2 = Point._normalize_dimension(p1, Point(p2)) + dim = len(p1) + + if dim == 2: + return Ray2D(p1, p2, **kwargs) + elif dim == 3: + return Ray3D(p1, p2, **kwargs) + return LinearEntity.__new__(cls, p1, p2, **kwargs) + + def _svg(self, scale_factor=1., fill_color="#66cc99"): + """Returns SVG path element for the LinearEntity. + + Parameters + ========== + + scale_factor : float + Multiplication factor for the SVG stroke-width. Default is 1. + fill_color : str, optional + Hex string for fill color. Default is "#66cc99". + """ + verts = (N(self.p1), N(self.p2)) + coords = ["{},{}".format(p.x, p.y) for p in verts] + path = "M {} L {}".format(coords[0], " L ".join(coords[1:])) + + return ( + '' + ).format(2.*scale_factor, path, fill_color) + + def contains(self, other): + """ + Is other GeometryEntity contained in this Ray? + + Examples + ======== + + >>> from sympy import Ray,Point,Segment + >>> p1, p2 = Point(0, 0), Point(4, 4) + >>> r = Ray(p1, p2) + >>> r.contains(p1) + True + >>> r.contains((1, 1)) + True + >>> r.contains((1, 3)) + False + >>> s = Segment((1, 1), (2, 2)) + >>> r.contains(s) + True + >>> s = Segment((1, 2), (2, 5)) + >>> r.contains(s) + False + >>> r1 = Ray((2, 2), (3, 3)) + >>> r.contains(r1) + True + >>> r1 = Ray((2, 2), (3, 5)) + >>> r.contains(r1) + False + """ + if not isinstance(other, GeometryEntity): + other = Point(other, dim=self.ambient_dimension) + if isinstance(other, Point): + if Point.is_collinear(self.p1, self.p2, other): + # if we're in the direction of the ray, our + # direction vector dot the ray's direction vector + # should be non-negative + return bool((self.p2 - self.p1).dot(other - self.p1) >= S.Zero) + return False + elif isinstance(other, Ray): + if Point.is_collinear(self.p1, self.p2, other.p1, other.p2): + return bool((self.p2 - self.p1).dot(other.p2 - other.p1) > S.Zero) + return False + elif isinstance(other, Segment): + return other.p1 in self and other.p2 in self + + # No other known entity can be contained in a Ray + return False + + def distance(self, other): + """ + Finds the shortest distance between the ray and a point. + + Raises + ====== + + NotImplementedError is raised if `other` is not a Point + + Examples + ======== + + >>> from sympy import Point, Ray + >>> p1, p2 = Point(0, 0), Point(1, 1) + >>> s = Ray(p1, p2) + >>> s.distance(Point(-1, -1)) + sqrt(2) + >>> s.distance((-1, 2)) + 3*sqrt(2)/2 + >>> p1, p2 = Point(0, 0, 0), Point(1, 1, 2) + >>> s = Ray(p1, p2) + >>> s + Ray3D(Point3D(0, 0, 0), Point3D(1, 1, 2)) + >>> s.distance(Point(-1, -1, 2)) + 4*sqrt(3)/3 + >>> s.distance((-1, -1, 2)) + 4*sqrt(3)/3 + + """ + if not isinstance(other, GeometryEntity): + other = Point(other, dim=self.ambient_dimension) + if self.contains(other): + return S.Zero + + proj = Line(self.p1, self.p2).projection(other) + if self.contains(proj): + return abs(other - proj) + else: + return abs(other - self.source) + + def equals(self, other): + """Returns True if self and other are the same mathematical entities""" + if not isinstance(other, Ray): + return False + return self.source == other.source and other.p2 in self + + def plot_interval(self, parameter='t'): + """The plot interval for the default geometric plot of the Ray. Gives + values that will produce a ray that is 10 units long (where a unit is + the distance between the two points that define the ray). + + Parameters + ========== + + parameter : str, optional + Default value is 't'. + + Returns + ======= + + plot_interval : list + [parameter, lower_bound, upper_bound] + + Examples + ======== + + >>> from sympy import Ray, pi + >>> r = Ray((0, 0), angle=pi/4) + >>> r.plot_interval() + [t, 0, 10] + + """ + t = _symbol(parameter, real=True) + return [t, 0, 10] + + @property + def source(self): + """The point from which the ray emanates. + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Point, Ray + >>> p1, p2 = Point(0, 0), Point(4, 1) + >>> r1 = Ray(p1, p2) + >>> r1.source + Point2D(0, 0) + >>> p1, p2 = Point(0, 0, 0), Point(4, 1, 5) + >>> r1 = Ray(p2, p1) + >>> r1.source + Point3D(4, 1, 5) + + """ + return self.p1 + + +class Segment(LinearEntity): + """A line segment in space. + + Parameters + ========== + + p1 : Point + p2 : Point + + Attributes + ========== + + length : number or SymPy expression + midpoint : Point + + See Also + ======== + + sympy.geometry.line.Segment2D + sympy.geometry.line.Segment3D + sympy.geometry.point.Point + sympy.geometry.line.Line + + Notes + ===== + + If 2D or 3D points are used to define `Segment`, it will + be automatically subclassed to `Segment2D` or `Segment3D`. + + Examples + ======== + + >>> from sympy import Point, Segment + >>> Segment((1, 0), (1, 1)) # tuples are interpreted as pts + Segment2D(Point2D(1, 0), Point2D(1, 1)) + >>> s = Segment(Point(4, 3), Point(1, 1)) + >>> s.points + (Point2D(4, 3), Point2D(1, 1)) + >>> s.slope + 2/3 + >>> s.length + sqrt(13) + >>> s.midpoint + Point2D(5/2, 2) + >>> Segment((1, 0, 0), (1, 1, 1)) # tuples are interpreted as pts + Segment3D(Point3D(1, 0, 0), Point3D(1, 1, 1)) + >>> s = Segment(Point(4, 3, 9), Point(1, 1, 7)); s + Segment3D(Point3D(4, 3, 9), Point3D(1, 1, 7)) + >>> s.points + (Point3D(4, 3, 9), Point3D(1, 1, 7)) + >>> s.length + sqrt(17) + >>> s.midpoint + Point3D(5/2, 2, 8) + + """ + def __new__(cls, p1, p2, **kwargs): + p1, p2 = Point._normalize_dimension(Point(p1), Point(p2)) + dim = len(p1) + + if dim == 2: + return Segment2D(p1, p2, **kwargs) + elif dim == 3: + return Segment3D(p1, p2, **kwargs) + return LinearEntity.__new__(cls, p1, p2, **kwargs) + + def contains(self, other): + """ + Is the other GeometryEntity contained within this Segment? + + Examples + ======== + + >>> from sympy import Point, Segment + >>> p1, p2 = Point(0, 1), Point(3, 4) + >>> s = Segment(p1, p2) + >>> s2 = Segment(p2, p1) + >>> s.contains(s2) + True + >>> from sympy import Point3D, Segment3D + >>> p1, p2 = Point3D(0, 1, 1), Point3D(3, 4, 5) + >>> s = Segment3D(p1, p2) + >>> s2 = Segment3D(p2, p1) + >>> s.contains(s2) + True + >>> s.contains((p1 + p2)/2) + True + """ + if not isinstance(other, GeometryEntity): + other = Point(other, dim=self.ambient_dimension) + if isinstance(other, Point): + if Point.is_collinear(other, self.p1, self.p2): + if isinstance(self, Segment2D): + # if it is collinear and is in the bounding box of the + # segment then it must be on the segment + vert = (1/self.slope).equals(0) + if vert is False: + isin = (self.p1.x - other.x)*(self.p2.x - other.x) <= 0 + if isin in (True, False): + return isin + if vert is True: + isin = (self.p1.y - other.y)*(self.p2.y - other.y) <= 0 + if isin in (True, False): + return isin + # use the triangle inequality + d1, d2 = other - self.p1, other - self.p2 + d = self.p2 - self.p1 + # without the call to simplify, SymPy cannot tell that an expression + # like (a+b)*(a/2+b/2) is always non-negative. If it cannot be + # determined, raise an Undecidable error + try: + # the triangle inequality says that |d1|+|d2| >= |d| and is strict + # only if other lies in the line segment + return bool(simplify(Eq(abs(d1) + abs(d2) - abs(d), 0))) + except TypeError: + raise Undecidable("Cannot determine if {} is in {}".format(other, self)) + if isinstance(other, Segment): + return other.p1 in self and other.p2 in self + + return False + + def equals(self, other): + """Returns True if self and other are the same mathematical entities""" + return isinstance(other, self.func) and list( + ordered(self.args)) == list(ordered(other.args)) + + def distance(self, other): + """ + Finds the shortest distance between a line segment and a point. + + Raises + ====== + + NotImplementedError is raised if `other` is not a Point + + Examples + ======== + + >>> from sympy import Point, Segment + >>> p1, p2 = Point(0, 1), Point(3, 4) + >>> s = Segment(p1, p2) + >>> s.distance(Point(10, 15)) + sqrt(170) + >>> s.distance((0, 12)) + sqrt(73) + >>> from sympy import Point3D, Segment3D + >>> p1, p2 = Point3D(0, 0, 3), Point3D(1, 1, 4) + >>> s = Segment3D(p1, p2) + >>> s.distance(Point3D(10, 15, 12)) + sqrt(341) + >>> s.distance((10, 15, 12)) + sqrt(341) + """ + if not isinstance(other, GeometryEntity): + other = Point(other, dim=self.ambient_dimension) + if isinstance(other, Point): + vp1 = other - self.p1 + vp2 = other - self.p2 + + dot_prod_sign_1 = self.direction.dot(vp1) >= 0 + dot_prod_sign_2 = self.direction.dot(vp2) <= 0 + if dot_prod_sign_1 and dot_prod_sign_2: + return Line(self.p1, self.p2).distance(other) + if dot_prod_sign_1 and not dot_prod_sign_2: + return abs(vp2) + if not dot_prod_sign_1 and dot_prod_sign_2: + return abs(vp1) + raise NotImplementedError() + + @property + def length(self): + """The length of the line segment. + + See Also + ======== + + sympy.geometry.point.Point.distance + + Examples + ======== + + >>> from sympy import Point, Segment + >>> p1, p2 = Point(0, 0), Point(4, 3) + >>> s1 = Segment(p1, p2) + >>> s1.length + 5 + >>> from sympy import Point3D, Segment3D + >>> p1, p2 = Point3D(0, 0, 0), Point3D(4, 3, 3) + >>> s1 = Segment3D(p1, p2) + >>> s1.length + sqrt(34) + + """ + return Point.distance(self.p1, self.p2) + + @property + def midpoint(self): + """The midpoint of the line segment. + + See Also + ======== + + sympy.geometry.point.Point.midpoint + + Examples + ======== + + >>> from sympy import Point, Segment + >>> p1, p2 = Point(0, 0), Point(4, 3) + >>> s1 = Segment(p1, p2) + >>> s1.midpoint + Point2D(2, 3/2) + >>> from sympy import Point3D, Segment3D + >>> p1, p2 = Point3D(0, 0, 0), Point3D(4, 3, 3) + >>> s1 = Segment3D(p1, p2) + >>> s1.midpoint + Point3D(2, 3/2, 3/2) + + """ + return Point.midpoint(self.p1, self.p2) + + def perpendicular_bisector(self, p=None): + """The perpendicular bisector of this segment. + + If no point is specified or the point specified is not on the + bisector then the bisector is returned as a Line. Otherwise a + Segment is returned that joins the point specified and the + intersection of the bisector and the segment. + + Parameters + ========== + + p : Point + + Returns + ======= + + bisector : Line or Segment + + See Also + ======== + + LinearEntity.perpendicular_segment + + Examples + ======== + + >>> from sympy import Point, Segment + >>> p1, p2, p3 = Point(0, 0), Point(6, 6), Point(5, 1) + >>> s1 = Segment(p1, p2) + >>> s1.perpendicular_bisector() + Line2D(Point2D(3, 3), Point2D(-3, 9)) + + >>> s1.perpendicular_bisector(p3) + Segment2D(Point2D(5, 1), Point2D(3, 3)) + + """ + l = self.perpendicular_line(self.midpoint) + if p is not None: + p2 = Point(p, dim=self.ambient_dimension) + if p2 in l: + return Segment(p2, self.midpoint) + return l + + def plot_interval(self, parameter='t'): + """The plot interval for the default geometric plot of the Segment gives + values that will produce the full segment in a plot. + + Parameters + ========== + + parameter : str, optional + Default value is 't'. + + Returns + ======= + + plot_interval : list + [parameter, lower_bound, upper_bound] + + Examples + ======== + + >>> from sympy import Point, Segment + >>> p1, p2 = Point(0, 0), Point(5, 3) + >>> s1 = Segment(p1, p2) + >>> s1.plot_interval() + [t, 0, 1] + + """ + t = _symbol(parameter, real=True) + return [t, 0, 1] + + +class LinearEntity2D(LinearEntity): + """A base class for all linear entities (line, ray and segment) + in a 2-dimensional Euclidean space. + + Attributes + ========== + + p1 + p2 + coefficients + slope + points + + Notes + ===== + + This is an abstract class and is not meant to be instantiated. + + See Also + ======== + + sympy.geometry.entity.GeometryEntity + + """ + @property + def bounds(self): + """Return a tuple (xmin, ymin, xmax, ymax) representing the bounding + rectangle for the geometric figure. + + """ + verts = self.points + xs = [p.x for p in verts] + ys = [p.y for p in verts] + return (min(xs), min(ys), max(xs), max(ys)) + + def perpendicular_line(self, p): + """Create a new Line perpendicular to this linear entity which passes + through the point `p`. + + Parameters + ========== + + p : Point + + Returns + ======= + + line : Line + + See Also + ======== + + sympy.geometry.line.LinearEntity.is_perpendicular, perpendicular_segment + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2, p3 = Point(0, 0), Point(2, 3), Point(-2, 2) + >>> L = Line(p1, p2) + >>> P = L.perpendicular_line(p3); P + Line2D(Point2D(-2, 2), Point2D(-5, 4)) + >>> L.is_perpendicular(P) + True + + In 2D, the first point of the perpendicular line is the + point through which was required to pass; the second + point is arbitrarily chosen. To get a line that explicitly + uses a point in the line, create a line from the perpendicular + segment from the line to the point: + + >>> Line(L.perpendicular_segment(p3)) + Line2D(Point2D(-2, 2), Point2D(4/13, 6/13)) + """ + p = Point(p, dim=self.ambient_dimension) + # any two lines in R^2 intersect, so blindly making + # a line through p in an orthogonal direction will work + # and is faster than finding the projection point as in 3D + return Line(p, p + self.direction.orthogonal_direction) + + @property + def slope(self): + """The slope of this linear entity, or infinity if vertical. + + Returns + ======= + + slope : number or SymPy expression + + See Also + ======== + + coefficients + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2 = Point(0, 0), Point(3, 5) + >>> l1 = Line(p1, p2) + >>> l1.slope + 5/3 + + >>> p3 = Point(0, 4) + >>> l2 = Line(p1, p3) + >>> l2.slope + oo + + """ + d1, d2 = (self.p1 - self.p2).args + if d1 == 0: + return S.Infinity + return simplify(d2/d1) + + +class Line2D(LinearEntity2D, Line): + """An infinite line in space 2D. + + A line is declared with two distinct points or a point and slope + as defined using keyword `slope`. + + Parameters + ========== + + p1 : Point + pt : Point + slope : SymPy expression + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Line, Segment, Point + >>> L = Line(Point(2,3), Point(3,5)) + >>> L + Line2D(Point2D(2, 3), Point2D(3, 5)) + >>> L.points + (Point2D(2, 3), Point2D(3, 5)) + >>> L.equation() + -2*x + y + 1 + >>> L.coefficients + (-2, 1, 1) + + Instantiate with keyword ``slope``: + + >>> Line(Point(0, 0), slope=0) + Line2D(Point2D(0, 0), Point2D(1, 0)) + + Instantiate with another linear object + + >>> s = Segment((0, 0), (0, 1)) + >>> Line(s).equation() + x + """ + def __new__(cls, p1, pt=None, slope=None, **kwargs): + if isinstance(p1, LinearEntity): + if pt is not None: + raise ValueError('When p1 is a LinearEntity, pt should be None') + p1, pt = Point._normalize_dimension(*p1.args, dim=2) + else: + p1 = Point(p1, dim=2) + if pt is not None and slope is None: + try: + p2 = Point(pt, dim=2) + except (NotImplementedError, TypeError, ValueError): + raise ValueError(filldedent(''' + The 2nd argument was not a valid Point. + If it was a slope, enter it with keyword "slope". + ''')) + elif slope is not None and pt is None: + slope = sympify(slope) + if slope.is_finite is False: + # when infinite slope, don't change x + dx = 0 + dy = 1 + else: + # go over 1 up slope + dx = 1 + dy = slope + # XXX avoiding simplification by adding to coords directly + p2 = Point(p1.x + dx, p1.y + dy, evaluate=False) + else: + raise ValueError('A 2nd Point or keyword "slope" must be used.') + return LinearEntity2D.__new__(cls, p1, p2, **kwargs) + + def _svg(self, scale_factor=1., fill_color="#66cc99"): + """Returns SVG path element for the LinearEntity. + + Parameters + ========== + + scale_factor : float + Multiplication factor for the SVG stroke-width. Default is 1. + fill_color : str, optional + Hex string for fill color. Default is "#66cc99". + """ + verts = (N(self.p1), N(self.p2)) + coords = ["{},{}".format(p.x, p.y) for p in verts] + path = "M {} L {}".format(coords[0], " L ".join(coords[1:])) + + return ( + '' + ).format(2.*scale_factor, path, fill_color) + + @property + def coefficients(self): + """The coefficients (`a`, `b`, `c`) for `ax + by + c = 0`. + + See Also + ======== + + sympy.geometry.line.Line2D.equation + + Examples + ======== + + >>> from sympy import Point, Line + >>> from sympy.abc import x, y + >>> p1, p2 = Point(0, 0), Point(5, 3) + >>> l = Line(p1, p2) + >>> l.coefficients + (-3, 5, 0) + + >>> p3 = Point(x, y) + >>> l2 = Line(p1, p3) + >>> l2.coefficients + (-y, x, 0) + + """ + p1, p2 = self.points + if p1.x == p2.x: + return (S.One, S.Zero, -p1.x) + elif p1.y == p2.y: + return (S.Zero, S.One, -p1.y) + return tuple([simplify(i) for i in + (self.p1.y - self.p2.y, + self.p2.x - self.p1.x, + self.p1.x*self.p2.y - self.p1.y*self.p2.x)]) + + def equation(self, x='x', y='y'): + """The equation of the line: ax + by + c. + + Parameters + ========== + + x : str, optional + The name to use for the x-axis, default value is 'x'. + y : str, optional + The name to use for the y-axis, default value is 'y'. + + Returns + ======= + + equation : SymPy expression + + See Also + ======== + + sympy.geometry.line.Line2D.coefficients + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2 = Point(1, 0), Point(5, 3) + >>> l1 = Line(p1, p2) + >>> l1.equation() + -3*x + 4*y + 3 + + """ + x = _symbol(x, real=True) + y = _symbol(y, real=True) + p1, p2 = self.points + if p1.x == p2.x: + return x - p1.x + elif p1.y == p2.y: + return y - p1.y + + a, b, c = self.coefficients + return a*x + b*y + c + + +class Ray2D(LinearEntity2D, Ray): + """ + A Ray is a semi-line in the space with a source point and a direction. + + Parameters + ========== + + p1 : Point + The source of the Ray + p2 : Point or radian value + This point determines the direction in which the Ray propagates. + If given as an angle it is interpreted in radians with the positive + direction being ccw. + + Attributes + ========== + + source + xdirection + ydirection + + See Also + ======== + + sympy.geometry.point.Point, Line + + Examples + ======== + + >>> from sympy import Point, pi, Ray + >>> r = Ray(Point(2, 3), Point(3, 5)) + >>> r + Ray2D(Point2D(2, 3), Point2D(3, 5)) + >>> r.points + (Point2D(2, 3), Point2D(3, 5)) + >>> r.source + Point2D(2, 3) + >>> r.xdirection + oo + >>> r.ydirection + oo + >>> r.slope + 2 + >>> Ray(Point(0, 0), angle=pi/4).slope + 1 + + """ + def __new__(cls, p1, pt=None, angle=None, **kwargs): + p1 = Point(p1, dim=2) + if pt is not None and angle is None: + try: + p2 = Point(pt, dim=2) + except (NotImplementedError, TypeError, ValueError): + raise ValueError(filldedent(''' + The 2nd argument was not a valid Point; if + it was meant to be an angle it should be + given with keyword "angle".''')) + if p1 == p2: + raise ValueError('A Ray requires two distinct points.') + elif angle is not None and pt is None: + # we need to know if the angle is an odd multiple of pi/2 + angle = sympify(angle) + c = _pi_coeff(angle) + p2 = None + if c is not None: + if c.is_Rational: + if c.q == 2: + if c.p == 1: + p2 = p1 + Point(0, 1) + elif c.p == 3: + p2 = p1 + Point(0, -1) + elif c.q == 1: + if c.p == 0: + p2 = p1 + Point(1, 0) + elif c.p == 1: + p2 = p1 + Point(-1, 0) + if p2 is None: + c *= S.Pi + else: + c = angle % (2*S.Pi) + if not p2: + m = 2*c/S.Pi + left = And(1 < m, m < 3) # is it in quadrant 2 or 3? + x = Piecewise((-1, left), (Piecewise((0, Eq(m % 1, 0)), (1, True)), True)) + y = Piecewise((-tan(c), left), (Piecewise((1, Eq(m, 1)), (-1, Eq(m, 3)), (tan(c), True)), True)) + p2 = p1 + Point(x, y) + else: + raise ValueError('A 2nd point or keyword "angle" must be used.') + + return LinearEntity2D.__new__(cls, p1, p2, **kwargs) + + @property + def xdirection(self): + """The x direction of the ray. + + Positive infinity if the ray points in the positive x direction, + negative infinity if the ray points in the negative x direction, + or 0 if the ray is vertical. + + See Also + ======== + + ydirection + + Examples + ======== + + >>> from sympy import Point, Ray + >>> p1, p2, p3 = Point(0, 0), Point(1, 1), Point(0, -1) + >>> r1, r2 = Ray(p1, p2), Ray(p1, p3) + >>> r1.xdirection + oo + >>> r2.xdirection + 0 + + """ + if self.p1.x < self.p2.x: + return S.Infinity + elif self.p1.x == self.p2.x: + return S.Zero + else: + return S.NegativeInfinity + + @property + def ydirection(self): + """The y direction of the ray. + + Positive infinity if the ray points in the positive y direction, + negative infinity if the ray points in the negative y direction, + or 0 if the ray is horizontal. + + See Also + ======== + + xdirection + + Examples + ======== + + >>> from sympy import Point, Ray + >>> p1, p2, p3 = Point(0, 0), Point(-1, -1), Point(-1, 0) + >>> r1, r2 = Ray(p1, p2), Ray(p1, p3) + >>> r1.ydirection + -oo + >>> r2.ydirection + 0 + + """ + if self.p1.y < self.p2.y: + return S.Infinity + elif self.p1.y == self.p2.y: + return S.Zero + else: + return S.NegativeInfinity + + def closing_angle(r1, r2): + """Return the angle by which r2 must be rotated so it faces the same + direction as r1. + + Parameters + ========== + + r1 : Ray2D + r2 : Ray2D + + Returns + ======= + + angle : angle in radians (ccw angle is positive) + + See Also + ======== + + LinearEntity.angle_between + + Examples + ======== + + >>> from sympy import Ray, pi + >>> r1 = Ray((0, 0), (1, 0)) + >>> r2 = r1.rotate(-pi/2) + >>> angle = r1.closing_angle(r2); angle + pi/2 + >>> r2.rotate(angle).direction.unit == r1.direction.unit + True + >>> r2.closing_angle(r1) + -pi/2 + """ + if not all(isinstance(r, Ray2D) for r in (r1, r2)): + # although the direction property is defined for + # all linear entities, only the Ray is truly a + # directed object + raise TypeError('Both arguments must be Ray2D objects.') + + a1 = atan2(*list(reversed(r1.direction.args))) + a2 = atan2(*list(reversed(r2.direction.args))) + if a1*a2 < 0: + a1 = 2*S.Pi + a1 if a1 < 0 else a1 + a2 = 2*S.Pi + a2 if a2 < 0 else a2 + return a1 - a2 + + +class Segment2D(LinearEntity2D, Segment): + """A line segment in 2D space. + + Parameters + ========== + + p1 : Point + p2 : Point + + Attributes + ========== + + length : number or SymPy expression + midpoint : Point + + See Also + ======== + + sympy.geometry.point.Point, Line + + Examples + ======== + + >>> from sympy import Point, Segment + >>> Segment((1, 0), (1, 1)) # tuples are interpreted as pts + Segment2D(Point2D(1, 0), Point2D(1, 1)) + >>> s = Segment(Point(4, 3), Point(1, 1)); s + Segment2D(Point2D(4, 3), Point2D(1, 1)) + >>> s.points + (Point2D(4, 3), Point2D(1, 1)) + >>> s.slope + 2/3 + >>> s.length + sqrt(13) + >>> s.midpoint + Point2D(5/2, 2) + + """ + def __new__(cls, p1, p2, **kwargs): + p1 = Point(p1, dim=2) + p2 = Point(p2, dim=2) + + if p1 == p2: + return p1 + + return LinearEntity2D.__new__(cls, p1, p2, **kwargs) + + def _svg(self, scale_factor=1., fill_color="#66cc99"): + """Returns SVG path element for the LinearEntity. + + Parameters + ========== + + scale_factor : float + Multiplication factor for the SVG stroke-width. Default is 1. + fill_color : str, optional + Hex string for fill color. Default is "#66cc99". + """ + verts = (N(self.p1), N(self.p2)) + coords = ["{},{}".format(p.x, p.y) for p in verts] + path = "M {} L {}".format(coords[0], " L ".join(coords[1:])) + return ( + '' + ).format(2.*scale_factor, path, fill_color) + + +class LinearEntity3D(LinearEntity): + """An base class for all linear entities (line, ray and segment) + in a 3-dimensional Euclidean space. + + Attributes + ========== + + p1 + p2 + direction_ratio + direction_cosine + points + + Notes + ===== + + This is a base class and is not meant to be instantiated. + """ + def __new__(cls, p1, p2, **kwargs): + p1 = Point3D(p1, dim=3) + p2 = Point3D(p2, dim=3) + if p1 == p2: + # if it makes sense to return a Point, handle in subclass + raise ValueError( + "%s.__new__ requires two unique Points." % cls.__name__) + + return GeometryEntity.__new__(cls, p1, p2, **kwargs) + + ambient_dimension = 3 + + @property + def direction_ratio(self): + """The direction ratio of a given line in 3D. + + See Also + ======== + + sympy.geometry.line.Line3D.equation + + Examples + ======== + + >>> from sympy import Point3D, Line3D + >>> p1, p2 = Point3D(0, 0, 0), Point3D(5, 3, 1) + >>> l = Line3D(p1, p2) + >>> l.direction_ratio + [5, 3, 1] + """ + p1, p2 = self.points + return p1.direction_ratio(p2) + + @property + def direction_cosine(self): + """The normalized direction ratio of a given line in 3D. + + See Also + ======== + + sympy.geometry.line.Line3D.equation + + Examples + ======== + + >>> from sympy import Point3D, Line3D + >>> p1, p2 = Point3D(0, 0, 0), Point3D(5, 3, 1) + >>> l = Line3D(p1, p2) + >>> l.direction_cosine + [sqrt(35)/7, 3*sqrt(35)/35, sqrt(35)/35] + >>> sum(i**2 for i in _) + 1 + """ + p1, p2 = self.points + return p1.direction_cosine(p2) + + +class Line3D(LinearEntity3D, Line): + """An infinite 3D line in space. + + A line is declared with two distinct points or a point and direction_ratio + as defined using keyword `direction_ratio`. + + Parameters + ========== + + p1 : Point3D + pt : Point3D + direction_ratio : list + + See Also + ======== + + sympy.geometry.point.Point3D + sympy.geometry.line.Line + sympy.geometry.line.Line2D + + Examples + ======== + + >>> from sympy import Line3D, Point3D + >>> L = Line3D(Point3D(2, 3, 4), Point3D(3, 5, 1)) + >>> L + Line3D(Point3D(2, 3, 4), Point3D(3, 5, 1)) + >>> L.points + (Point3D(2, 3, 4), Point3D(3, 5, 1)) + """ + def __new__(cls, p1, pt=None, direction_ratio=(), **kwargs): + if isinstance(p1, LinearEntity3D): + if pt is not None: + raise ValueError('if p1 is a LinearEntity, pt must be None.') + p1, pt = p1.args + else: + p1 = Point(p1, dim=3) + if pt is not None and len(direction_ratio) == 0: + pt = Point(pt, dim=3) + elif len(direction_ratio) == 3 and pt is None: + pt = Point3D(p1.x + direction_ratio[0], p1.y + direction_ratio[1], + p1.z + direction_ratio[2]) + else: + raise ValueError('A 2nd Point or keyword "direction_ratio" must ' + 'be used.') + + return LinearEntity3D.__new__(cls, p1, pt, **kwargs) + + def equation(self, x='x', y='y', z='z'): + """Return the equations that define the line in 3D. + + Parameters + ========== + + x : str, optional + The name to use for the x-axis, default value is 'x'. + y : str, optional + The name to use for the y-axis, default value is 'y'. + z : str, optional + The name to use for the z-axis, default value is 'z'. + + Returns + ======= + + equation : Tuple of simultaneous equations + + Examples + ======== + + >>> from sympy import Point3D, Line3D, solve + >>> from sympy.abc import x, y, z + >>> p1, p2 = Point3D(1, 0, 0), Point3D(5, 3, 0) + >>> l1 = Line3D(p1, p2) + >>> eq = l1.equation(x, y, z); eq + (-3*x + 4*y + 3, z) + >>> solve(eq.subs(z, 0), (x, y, z)) + {x: 4*y/3 + 1} + """ + x, y, z, k = [_symbol(i, real=True) for i in (x, y, z, 'k')] + p1, p2 = self.points + d1, d2, d3 = p1.direction_ratio(p2) + x1, y1, z1 = p1 + eqs = [-d1*k + x - x1, -d2*k + y - y1, -d3*k + z - z1] + # eliminate k from equations by solving first eq with k for k + for i, e in enumerate(eqs): + if e.has(k): + kk = solve(e, k)[0] + eqs.pop(i) + break + return Tuple(*[i.subs(k, kk).as_numer_denom()[0] for i in eqs]) + + def distance(self, other): + """ + Finds the shortest distance between a line and another object. + + Parameters + ========== + + Point3D, Line3D, Plane, tuple, list + + Returns + ======= + + distance + + Notes + ===== + + This method accepts only 3D entities as it's parameter + + Tuples and lists are converted to Point3D and therefore must be of + length 3, 2 or 1. + + NotImplementedError is raised if `other` is not an instance of one + of the specified classes: Point3D, Line3D, or Plane. + + Examples + ======== + + >>> from sympy.geometry import Line3D + >>> l1 = Line3D((0, 0, 0), (0, 0, 1)) + >>> l2 = Line3D((0, 1, 0), (1, 1, 1)) + >>> l1.distance(l2) + 1 + + The computed distance may be symbolic, too: + + >>> from sympy.abc import x, y + >>> l1 = Line3D((0, 0, 0), (0, 0, 1)) + >>> l2 = Line3D((0, x, 0), (y, x, 1)) + >>> l1.distance(l2) + Abs(x*y)/Abs(sqrt(y**2)) + + """ + + from .plane import Plane # Avoid circular import + + if isinstance(other, (tuple, list)): + try: + other = Point3D(other) + except ValueError: + pass + + if isinstance(other, Point3D): + return super().distance(other) + + if isinstance(other, Line3D): + if self == other: + return S.Zero + if self.is_parallel(other): + return super().distance(other.p1) + + # Skew lines + self_direction = Matrix(self.direction_ratio) + other_direction = Matrix(other.direction_ratio) + normal = self_direction.cross(other_direction) + plane_through_self = Plane(p1=self.p1, normal_vector=normal) + return other.p1.distance(plane_through_self) + + if isinstance(other, Plane): + return other.distance(self) + + msg = f"{other} has type {type(other)}, which is unsupported" + raise NotImplementedError(msg) + + +class Ray3D(LinearEntity3D, Ray): + """ + A Ray is a semi-line in the space with a source point and a direction. + + Parameters + ========== + + p1 : Point3D + The source of the Ray + p2 : Point or a direction vector + direction_ratio: Determines the direction in which the Ray propagates. + + + Attributes + ========== + + source + xdirection + ydirection + zdirection + + See Also + ======== + + sympy.geometry.point.Point3D, Line3D + + + Examples + ======== + + >>> from sympy import Point3D, Ray3D + >>> r = Ray3D(Point3D(2, 3, 4), Point3D(3, 5, 0)) + >>> r + Ray3D(Point3D(2, 3, 4), Point3D(3, 5, 0)) + >>> r.points + (Point3D(2, 3, 4), Point3D(3, 5, 0)) + >>> r.source + Point3D(2, 3, 4) + >>> r.xdirection + oo + >>> r.ydirection + oo + >>> r.direction_ratio + [1, 2, -4] + + """ + def __new__(cls, p1, pt=None, direction_ratio=(), **kwargs): + if isinstance(p1, LinearEntity3D): + if pt is not None: + raise ValueError('If p1 is a LinearEntity, pt must be None') + p1, pt = p1.args + else: + p1 = Point(p1, dim=3) + if pt is not None and len(direction_ratio) == 0: + pt = Point(pt, dim=3) + elif len(direction_ratio) == 3 and pt is None: + pt = Point3D(p1.x + direction_ratio[0], p1.y + direction_ratio[1], + p1.z + direction_ratio[2]) + else: + raise ValueError(filldedent(''' + A 2nd Point or keyword "direction_ratio" must be used. + ''')) + + return LinearEntity3D.__new__(cls, p1, pt, **kwargs) + + @property + def xdirection(self): + """The x direction of the ray. + + Positive infinity if the ray points in the positive x direction, + negative infinity if the ray points in the negative x direction, + or 0 if the ray is vertical. + + See Also + ======== + + ydirection + + Examples + ======== + + >>> from sympy import Point3D, Ray3D + >>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(1, 1, 1), Point3D(0, -1, 0) + >>> r1, r2 = Ray3D(p1, p2), Ray3D(p1, p3) + >>> r1.xdirection + oo + >>> r2.xdirection + 0 + + """ + if self.p1.x < self.p2.x: + return S.Infinity + elif self.p1.x == self.p2.x: + return S.Zero + else: + return S.NegativeInfinity + + @property + def ydirection(self): + """The y direction of the ray. + + Positive infinity if the ray points in the positive y direction, + negative infinity if the ray points in the negative y direction, + or 0 if the ray is horizontal. + + See Also + ======== + + xdirection + + Examples + ======== + + >>> from sympy import Point3D, Ray3D + >>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(-1, -1, -1), Point3D(-1, 0, 0) + >>> r1, r2 = Ray3D(p1, p2), Ray3D(p1, p3) + >>> r1.ydirection + -oo + >>> r2.ydirection + 0 + + """ + if self.p1.y < self.p2.y: + return S.Infinity + elif self.p1.y == self.p2.y: + return S.Zero + else: + return S.NegativeInfinity + + @property + def zdirection(self): + """The z direction of the ray. + + Positive infinity if the ray points in the positive z direction, + negative infinity if the ray points in the negative z direction, + or 0 if the ray is horizontal. + + See Also + ======== + + xdirection + + Examples + ======== + + >>> from sympy import Point3D, Ray3D + >>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(-1, -1, -1), Point3D(-1, 0, 0) + >>> r1, r2 = Ray3D(p1, p2), Ray3D(p1, p3) + >>> r1.ydirection + -oo + >>> r2.ydirection + 0 + >>> r2.zdirection + 0 + + """ + if self.p1.z < self.p2.z: + return S.Infinity + elif self.p1.z == self.p2.z: + return S.Zero + else: + return S.NegativeInfinity + + +class Segment3D(LinearEntity3D, Segment): + """A line segment in a 3D space. + + Parameters + ========== + + p1 : Point3D + p2 : Point3D + + Attributes + ========== + + length : number or SymPy expression + midpoint : Point3D + + See Also + ======== + + sympy.geometry.point.Point3D, Line3D + + Examples + ======== + + >>> from sympy import Point3D, Segment3D + >>> Segment3D((1, 0, 0), (1, 1, 1)) # tuples are interpreted as pts + Segment3D(Point3D(1, 0, 0), Point3D(1, 1, 1)) + >>> s = Segment3D(Point3D(4, 3, 9), Point3D(1, 1, 7)); s + Segment3D(Point3D(4, 3, 9), Point3D(1, 1, 7)) + >>> s.points + (Point3D(4, 3, 9), Point3D(1, 1, 7)) + >>> s.length + sqrt(17) + >>> s.midpoint + Point3D(5/2, 2, 8) + + """ + def __new__(cls, p1, p2, **kwargs): + p1 = Point(p1, dim=3) + p2 = Point(p2, dim=3) + + if p1 == p2: + return p1 + + return LinearEntity3D.__new__(cls, p1, p2, **kwargs) diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/parabola.py b/venv/lib/python3.11/site-packages/sympy/geometry/parabola.py new file mode 100644 index 0000000000000000000000000000000000000000..183c593785bb610e6f451a0c87abb2aa34d22494 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/parabola.py @@ -0,0 +1,422 @@ +"""Parabolic geometrical entity. + +Contains +* Parabola + +""" + +from sympy.core import S +from sympy.core.sorting import ordered +from sympy.core.symbol import _symbol, symbols +from sympy.geometry.entity import GeometryEntity, GeometrySet +from sympy.geometry.point import Point, Point2D +from sympy.geometry.line import Line, Line2D, Ray2D, Segment2D, LinearEntity3D +from sympy.geometry.ellipse import Ellipse +from sympy.functions import sign +from sympy.simplify.simplify import simplify +from sympy.solvers.solvers import solve + + +class Parabola(GeometrySet): + """A parabolic GeometryEntity. + + A parabola is declared with a point, that is called 'focus', and + a line, that is called 'directrix'. + Only vertical or horizontal parabolas are currently supported. + + Parameters + ========== + + focus : Point + Default value is Point(0, 0) + directrix : Line + + Attributes + ========== + + focus + directrix + axis of symmetry + focal length + p parameter + vertex + eccentricity + + Raises + ====== + ValueError + When `focus` is not a two dimensional point. + When `focus` is a point of directrix. + NotImplementedError + When `directrix` is neither horizontal nor vertical. + + Examples + ======== + + >>> from sympy import Parabola, Point, Line + >>> p1 = Parabola(Point(0, 0), Line(Point(5, 8), Point(7,8))) + >>> p1.focus + Point2D(0, 0) + >>> p1.directrix + Line2D(Point2D(5, 8), Point2D(7, 8)) + + """ + + def __new__(cls, focus=None, directrix=None, **kwargs): + + if focus: + focus = Point(focus, dim=2) + else: + focus = Point(0, 0) + + directrix = Line(directrix) + + if directrix.contains(focus): + raise ValueError('The focus must not be a point of directrix') + + return GeometryEntity.__new__(cls, focus, directrix, **kwargs) + + @property + def ambient_dimension(self): + """Returns the ambient dimension of parabola. + + Returns + ======= + + ambient_dimension : integer + + Examples + ======== + + >>> from sympy import Parabola, Point, Line + >>> f1 = Point(0, 0) + >>> p1 = Parabola(f1, Line(Point(5, 8), Point(7, 8))) + >>> p1.ambient_dimension + 2 + + """ + return 2 + + @property + def axis_of_symmetry(self): + """Return the axis of symmetry of the parabola: a line + perpendicular to the directrix passing through the focus. + + Returns + ======= + + axis_of_symmetry : Line + + See Also + ======== + + sympy.geometry.line.Line + + Examples + ======== + + >>> from sympy import Parabola, Point, Line + >>> p1 = Parabola(Point(0, 0), Line(Point(5, 8), Point(7, 8))) + >>> p1.axis_of_symmetry + Line2D(Point2D(0, 0), Point2D(0, 1)) + + """ + return self.directrix.perpendicular_line(self.focus) + + @property + def directrix(self): + """The directrix of the parabola. + + Returns + ======= + + directrix : Line + + See Also + ======== + + sympy.geometry.line.Line + + Examples + ======== + + >>> from sympy import Parabola, Point, Line + >>> l1 = Line(Point(5, 8), Point(7, 8)) + >>> p1 = Parabola(Point(0, 0), l1) + >>> p1.directrix + Line2D(Point2D(5, 8), Point2D(7, 8)) + + """ + return self.args[1] + + @property + def eccentricity(self): + """The eccentricity of the parabola. + + Returns + ======= + + eccentricity : number + + A parabola may also be characterized as a conic section with an + eccentricity of 1. As a consequence of this, all parabolas are + similar, meaning that while they can be different sizes, + they are all the same shape. + + See Also + ======== + + https://en.wikipedia.org/wiki/Parabola + + + Examples + ======== + + >>> from sympy import Parabola, Point, Line + >>> p1 = Parabola(Point(0, 0), Line(Point(5, 8), Point(7, 8))) + >>> p1.eccentricity + 1 + + Notes + ----- + The eccentricity for every Parabola is 1 by definition. + + """ + return S.One + + def equation(self, x='x', y='y'): + """The equation of the parabola. + + Parameters + ========== + x : str, optional + Label for the x-axis. Default value is 'x'. + y : str, optional + Label for the y-axis. Default value is 'y'. + + Returns + ======= + equation : SymPy expression + + Examples + ======== + + >>> from sympy import Parabola, Point, Line + >>> p1 = Parabola(Point(0, 0), Line(Point(5, 8), Point(7, 8))) + >>> p1.equation() + -x**2 - 16*y + 64 + >>> p1.equation('f') + -f**2 - 16*y + 64 + >>> p1.equation(y='z') + -x**2 - 16*z + 64 + + """ + x = _symbol(x, real=True) + y = _symbol(y, real=True) + + m = self.directrix.slope + if m is S.Infinity: + t1 = 4 * (self.p_parameter) * (x - self.vertex.x) + t2 = (y - self.vertex.y)**2 + elif m == 0: + t1 = 4 * (self.p_parameter) * (y - self.vertex.y) + t2 = (x - self.vertex.x)**2 + else: + a, b = self.focus + c, d = self.directrix.coefficients[:2] + t1 = (x - a)**2 + (y - b)**2 + t2 = self.directrix.equation(x, y)**2/(c**2 + d**2) + return t1 - t2 + + @property + def focal_length(self): + """The focal length of the parabola. + + Returns + ======= + + focal_lenght : number or symbolic expression + + Notes + ===== + + The distance between the vertex and the focus + (or the vertex and directrix), measured along the axis + of symmetry, is the "focal length". + + See Also + ======== + + https://en.wikipedia.org/wiki/Parabola + + Examples + ======== + + >>> from sympy import Parabola, Point, Line + >>> p1 = Parabola(Point(0, 0), Line(Point(5, 8), Point(7, 8))) + >>> p1.focal_length + 4 + + """ + distance = self.directrix.distance(self.focus) + focal_length = distance/2 + + return focal_length + + @property + def focus(self): + """The focus of the parabola. + + Returns + ======= + + focus : Point + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Parabola, Point, Line + >>> f1 = Point(0, 0) + >>> p1 = Parabola(f1, Line(Point(5, 8), Point(7, 8))) + >>> p1.focus + Point2D(0, 0) + + """ + return self.args[0] + + def intersection(self, o): + """The intersection of the parabola and another geometrical entity `o`. + + Parameters + ========== + + o : GeometryEntity, LinearEntity + + Returns + ======= + + intersection : list of GeometryEntity objects + + Examples + ======== + + >>> from sympy import Parabola, Point, Ellipse, Line, Segment + >>> p1 = Point(0,0) + >>> l1 = Line(Point(1, -2), Point(-1,-2)) + >>> parabola1 = Parabola(p1, l1) + >>> parabola1.intersection(Ellipse(Point(0, 0), 2, 5)) + [Point2D(-2, 0), Point2D(2, 0)] + >>> parabola1.intersection(Line(Point(-7, 3), Point(12, 3))) + [Point2D(-4, 3), Point2D(4, 3)] + >>> parabola1.intersection(Segment((-12, -65), (14, -68))) + [] + + """ + x, y = symbols('x y', real=True) + parabola_eq = self.equation() + if isinstance(o, Parabola): + if o in self: + return [o] + else: + return list(ordered([Point(i) for i in solve( + [parabola_eq, o.equation()], [x, y], set=True)[1]])) + elif isinstance(o, Point2D): + if simplify(parabola_eq.subs([(x, o._args[0]), (y, o._args[1])])) == 0: + return [o] + else: + return [] + elif isinstance(o, (Segment2D, Ray2D)): + result = solve([parabola_eq, + Line2D(o.points[0], o.points[1]).equation()], + [x, y], set=True)[1] + return list(ordered([Point2D(i) for i in result if i in o])) + elif isinstance(o, (Line2D, Ellipse)): + return list(ordered([Point2D(i) for i in solve( + [parabola_eq, o.equation()], [x, y], set=True)[1]])) + elif isinstance(o, LinearEntity3D): + raise TypeError('Entity must be two dimensional, not three dimensional') + else: + raise TypeError('Wrong type of argument were put') + + @property + def p_parameter(self): + """P is a parameter of parabola. + + Returns + ======= + + p : number or symbolic expression + + Notes + ===== + + The absolute value of p is the focal length. The sign on p tells + which way the parabola faces. Vertical parabolas that open up + and horizontal that open right, give a positive value for p. + Vertical parabolas that open down and horizontal that open left, + give a negative value for p. + + + See Also + ======== + + https://www.sparknotes.com/math/precalc/conicsections/section2/ + + Examples + ======== + + >>> from sympy import Parabola, Point, Line + >>> p1 = Parabola(Point(0, 0), Line(Point(5, 8), Point(7, 8))) + >>> p1.p_parameter + -4 + + """ + m = self.directrix.slope + if m is S.Infinity: + x = self.directrix.coefficients[2] + p = sign(self.focus.args[0] + x) + elif m == 0: + y = self.directrix.coefficients[2] + p = sign(self.focus.args[1] + y) + else: + d = self.directrix.projection(self.focus) + p = sign(self.focus.x - d.x) + return p * self.focal_length + + @property + def vertex(self): + """The vertex of the parabola. + + Returns + ======= + + vertex : Point + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Parabola, Point, Line + >>> p1 = Parabola(Point(0, 0), Line(Point(5, 8), Point(7, 8))) + >>> p1.vertex + Point2D(0, 4) + + """ + focus = self.focus + m = self.directrix.slope + if m is S.Infinity: + vertex = Point(focus.args[0] - self.p_parameter, focus.args[1]) + elif m == 0: + vertex = Point(focus.args[0], focus.args[1] - self.p_parameter) + else: + vertex = self.axis_of_symmetry.intersection(self)[0] + return vertex diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/plane.py b/venv/lib/python3.11/site-packages/sympy/geometry/plane.py new file mode 100644 index 0000000000000000000000000000000000000000..509dc4be5dc41c5df7c33561fdbe5bb0b6620352 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/plane.py @@ -0,0 +1,878 @@ +"""Geometrical Planes. + +Contains +======== +Plane + +""" + +from sympy.core import Dummy, Rational, S, Symbol +from sympy.core.symbol import _symbol +from sympy.functions.elementary.trigonometric import cos, sin, acos, asin, sqrt +from .entity import GeometryEntity +from .line import (Line, Ray, Segment, Line3D, LinearEntity, LinearEntity3D, + Ray3D, Segment3D) +from .point import Point, Point3D +from sympy.matrices import Matrix +from sympy.polys.polytools import cancel +from sympy.solvers import solve, linsolve +from sympy.utilities.iterables import uniq, is_sequence +from sympy.utilities.misc import filldedent, func_name, Undecidable + +from mpmath.libmp.libmpf import prec_to_dps + +import random + + +x, y, z, t = [Dummy('plane_dummy') for i in range(4)] + + +class Plane(GeometryEntity): + """ + A plane is a flat, two-dimensional surface. A plane is the two-dimensional + analogue of a point (zero-dimensions), a line (one-dimension) and a solid + (three-dimensions). A plane can generally be constructed by two types of + inputs. They are: + - three non-collinear points + - a point and the plane's normal vector + + Attributes + ========== + + p1 + normal_vector + + Examples + ======== + + >>> from sympy import Plane, Point3D + >>> Plane(Point3D(1, 1, 1), Point3D(2, 3, 4), Point3D(2, 2, 2)) + Plane(Point3D(1, 1, 1), (-1, 2, -1)) + >>> Plane((1, 1, 1), (2, 3, 4), (2, 2, 2)) + Plane(Point3D(1, 1, 1), (-1, 2, -1)) + >>> Plane(Point3D(1, 1, 1), normal_vector=(1,4,7)) + Plane(Point3D(1, 1, 1), (1, 4, 7)) + + """ + def __new__(cls, p1, a=None, b=None, **kwargs): + p1 = Point3D(p1, dim=3) + if a and b: + p2 = Point(a, dim=3) + p3 = Point(b, dim=3) + if Point3D.are_collinear(p1, p2, p3): + raise ValueError('Enter three non-collinear points') + a = p1.direction_ratio(p2) + b = p1.direction_ratio(p3) + normal_vector = tuple(Matrix(a).cross(Matrix(b))) + else: + a = kwargs.pop('normal_vector', a) + evaluate = kwargs.get('evaluate', True) + if is_sequence(a) and len(a) == 3: + normal_vector = Point3D(a).args if evaluate else a + else: + raise ValueError(filldedent(''' + Either provide 3 3D points or a point with a + normal vector expressed as a sequence of length 3''')) + if all(coord.is_zero for coord in normal_vector): + raise ValueError('Normal vector cannot be zero vector') + return GeometryEntity.__new__(cls, p1, normal_vector, **kwargs) + + def __contains__(self, o): + k = self.equation(x, y, z) + if isinstance(o, (LinearEntity, LinearEntity3D)): + d = Point3D(o.arbitrary_point(t)) + e = k.subs([(x, d.x), (y, d.y), (z, d.z)]) + return e.equals(0) + try: + o = Point(o, dim=3, strict=True) + d = k.xreplace(dict(zip((x, y, z), o.args))) + return d.equals(0) + except TypeError: + return False + + def _eval_evalf(self, prec=15, **options): + pt, tup = self.args + dps = prec_to_dps(prec) + pt = pt.evalf(n=dps, **options) + tup = tuple([i.evalf(n=dps, **options) for i in tup]) + return self.func(pt, normal_vector=tup, evaluate=False) + + def angle_between(self, o): + """Angle between the plane and other geometric entity. + + Parameters + ========== + + LinearEntity3D, Plane. + + Returns + ======= + + angle : angle in radians + + Notes + ===== + + This method accepts only 3D entities as it's parameter, but if you want + to calculate the angle between a 2D entity and a plane you should + first convert to a 3D entity by projecting onto a desired plane and + then proceed to calculate the angle. + + Examples + ======== + + >>> from sympy import Point3D, Line3D, Plane + >>> a = Plane(Point3D(1, 2, 2), normal_vector=(1, 2, 3)) + >>> b = Line3D(Point3D(1, 3, 4), Point3D(2, 2, 2)) + >>> a.angle_between(b) + -asin(sqrt(21)/6) + + """ + if isinstance(o, LinearEntity3D): + a = Matrix(self.normal_vector) + b = Matrix(o.direction_ratio) + c = a.dot(b) + d = sqrt(sum(i**2 for i in self.normal_vector)) + e = sqrt(sum(i**2 for i in o.direction_ratio)) + return asin(c/(d*e)) + if isinstance(o, Plane): + a = Matrix(self.normal_vector) + b = Matrix(o.normal_vector) + c = a.dot(b) + d = sqrt(sum(i**2 for i in self.normal_vector)) + e = sqrt(sum(i**2 for i in o.normal_vector)) + return acos(c/(d*e)) + + + def arbitrary_point(self, u=None, v=None): + """ Returns an arbitrary point on the Plane. If given two + parameters, the point ranges over the entire plane. If given 1 + or no parameters, returns a point with one parameter which, + when varying from 0 to 2*pi, moves the point in a circle of + radius 1 about p1 of the Plane. + + Examples + ======== + + >>> from sympy import Plane, Ray + >>> from sympy.abc import u, v, t, r + >>> p = Plane((1, 1, 1), normal_vector=(1, 0, 0)) + >>> p.arbitrary_point(u, v) + Point3D(1, u + 1, v + 1) + >>> p.arbitrary_point(t) + Point3D(1, cos(t) + 1, sin(t) + 1) + + While arbitrary values of u and v can move the point anywhere in + the plane, the single-parameter point can be used to construct a + ray whose arbitrary point can be located at angle t and radius + r from p.p1: + + >>> Ray(p.p1, _).arbitrary_point(r) + Point3D(1, r*cos(t) + 1, r*sin(t) + 1) + + Returns + ======= + + Point3D + + """ + circle = v is None + if circle: + u = _symbol(u or 't', real=True) + else: + u = _symbol(u or 'u', real=True) + v = _symbol(v or 'v', real=True) + x, y, z = self.normal_vector + a, b, c = self.p1.args + # x1, y1, z1 is a nonzero vector parallel to the plane + if x.is_zero and y.is_zero: + x1, y1, z1 = S.One, S.Zero, S.Zero + else: + x1, y1, z1 = -y, x, S.Zero + # x2, y2, z2 is also parallel to the plane, and orthogonal to x1, y1, z1 + x2, y2, z2 = tuple(Matrix((x, y, z)).cross(Matrix((x1, y1, z1)))) + if circle: + x1, y1, z1 = (w/sqrt(x1**2 + y1**2 + z1**2) for w in (x1, y1, z1)) + x2, y2, z2 = (w/sqrt(x2**2 + y2**2 + z2**2) for w in (x2, y2, z2)) + p = Point3D(a + x1*cos(u) + x2*sin(u), \ + b + y1*cos(u) + y2*sin(u), \ + c + z1*cos(u) + z2*sin(u)) + else: + p = Point3D(a + x1*u + x2*v, b + y1*u + y2*v, c + z1*u + z2*v) + return p + + + @staticmethod + def are_concurrent(*planes): + """Is a sequence of Planes concurrent? + + Two or more Planes are concurrent if their intersections + are a common line. + + Parameters + ========== + + planes: list + + Returns + ======= + + Boolean + + Examples + ======== + + >>> from sympy import Plane, Point3D + >>> a = Plane(Point3D(5, 0, 0), normal_vector=(1, -1, 1)) + >>> b = Plane(Point3D(0, -2, 0), normal_vector=(3, 1, 1)) + >>> c = Plane(Point3D(0, -1, 0), normal_vector=(5, -1, 9)) + >>> Plane.are_concurrent(a, b) + True + >>> Plane.are_concurrent(a, b, c) + False + + """ + planes = list(uniq(planes)) + for i in planes: + if not isinstance(i, Plane): + raise ValueError('All objects should be Planes but got %s' % i.func) + if len(planes) < 2: + return False + planes = list(planes) + first = planes.pop(0) + sol = first.intersection(planes[0]) + if sol == []: + return False + else: + line = sol[0] + for i in planes[1:]: + l = first.intersection(i) + if not l or l[0] not in line: + return False + return True + + + def distance(self, o): + """Distance between the plane and another geometric entity. + + Parameters + ========== + + Point3D, LinearEntity3D, Plane. + + Returns + ======= + + distance + + Notes + ===== + + This method accepts only 3D entities as it's parameter, but if you want + to calculate the distance between a 2D entity and a plane you should + first convert to a 3D entity by projecting onto a desired plane and + then proceed to calculate the distance. + + Examples + ======== + + >>> from sympy import Point3D, Line3D, Plane + >>> a = Plane(Point3D(1, 1, 1), normal_vector=(1, 1, 1)) + >>> b = Point3D(1, 2, 3) + >>> a.distance(b) + sqrt(3) + >>> c = Line3D(Point3D(2, 3, 1), Point3D(1, 2, 2)) + >>> a.distance(c) + 0 + + """ + if self.intersection(o) != []: + return S.Zero + + if isinstance(o, (Segment3D, Ray3D)): + a, b = o.p1, o.p2 + pi, = self.intersection(Line3D(a, b)) + if pi in o: + return self.distance(pi) + elif a in Segment3D(pi, b): + return self.distance(a) + else: + assert isinstance(o, Segment3D) is True + return self.distance(b) + + # following code handles `Point3D`, `LinearEntity3D`, `Plane` + a = o if isinstance(o, Point3D) else o.p1 + n = Point3D(self.normal_vector).unit + d = (a - self.p1).dot(n) + return abs(d) + + + def equals(self, o): + """ + Returns True if self and o are the same mathematical entities. + + Examples + ======== + + >>> from sympy import Plane, Point3D + >>> a = Plane(Point3D(1, 2, 3), normal_vector=(1, 1, 1)) + >>> b = Plane(Point3D(1, 2, 3), normal_vector=(2, 2, 2)) + >>> c = Plane(Point3D(1, 2, 3), normal_vector=(-1, 4, 6)) + >>> a.equals(a) + True + >>> a.equals(b) + True + >>> a.equals(c) + False + """ + if isinstance(o, Plane): + a = self.equation() + b = o.equation() + return cancel(a/b).is_constant() + else: + return False + + + def equation(self, x=None, y=None, z=None): + """The equation of the Plane. + + Examples + ======== + + >>> from sympy import Point3D, Plane + >>> a = Plane(Point3D(1, 1, 2), Point3D(2, 4, 7), Point3D(3, 5, 1)) + >>> a.equation() + -23*x + 11*y - 2*z + 16 + >>> a = Plane(Point3D(1, 4, 2), normal_vector=(6, 6, 6)) + >>> a.equation() + 6*x + 6*y + 6*z - 42 + + """ + x, y, z = [i if i else Symbol(j, real=True) for i, j in zip((x, y, z), 'xyz')] + a = Point3D(x, y, z) + b = self.p1.direction_ratio(a) + c = self.normal_vector + return (sum(i*j for i, j in zip(b, c))) + + + def intersection(self, o): + """ The intersection with other geometrical entity. + + Parameters + ========== + + Point, Point3D, LinearEntity, LinearEntity3D, Plane + + Returns + ======= + + List + + Examples + ======== + + >>> from sympy import Point3D, Line3D, Plane + >>> a = Plane(Point3D(1, 2, 3), normal_vector=(1, 1, 1)) + >>> b = Point3D(1, 2, 3) + >>> a.intersection(b) + [Point3D(1, 2, 3)] + >>> c = Line3D(Point3D(1, 4, 7), Point3D(2, 2, 2)) + >>> a.intersection(c) + [Point3D(2, 2, 2)] + >>> d = Plane(Point3D(6, 0, 0), normal_vector=(2, -5, 3)) + >>> e = Plane(Point3D(2, 0, 0), normal_vector=(3, 4, -3)) + >>> d.intersection(e) + [Line3D(Point3D(78/23, -24/23, 0), Point3D(147/23, 321/23, 23))] + + """ + if not isinstance(o, GeometryEntity): + o = Point(o, dim=3) + if isinstance(o, Point): + if o in self: + return [o] + else: + return [] + if isinstance(o, (LinearEntity, LinearEntity3D)): + # recast to 3D + p1, p2 = o.p1, o.p2 + if isinstance(o, Segment): + o = Segment3D(p1, p2) + elif isinstance(o, Ray): + o = Ray3D(p1, p2) + elif isinstance(o, Line): + o = Line3D(p1, p2) + else: + raise ValueError('unhandled linear entity: %s' % o.func) + if o in self: + return [o] + else: + a = Point3D(o.arbitrary_point(t)) + p1, n = self.p1, Point3D(self.normal_vector) + + # TODO: Replace solve with solveset, when this line is tested + c = solve((a - p1).dot(n), t) + if not c: + return [] + else: + c = [i for i in c if i.is_real is not False] + if len(c) > 1: + c = [i for i in c if i.is_real] + if len(c) != 1: + raise Undecidable("not sure which point is real") + p = a.subs(t, c[0]) + if p not in o: + return [] # e.g. a segment might not intersect a plane + return [p] + if isinstance(o, Plane): + if self.equals(o): + return [self] + if self.is_parallel(o): + return [] + else: + x, y, z = map(Dummy, 'xyz') + a, b = Matrix([self.normal_vector]), Matrix([o.normal_vector]) + c = list(a.cross(b)) + d = self.equation(x, y, z) + e = o.equation(x, y, z) + result = list(linsolve([d, e], x, y, z))[0] + for i in (x, y, z): result = result.subs(i, 0) + return [Line3D(Point3D(result), direction_ratio=c)] + + + def is_coplanar(self, o): + """ Returns True if `o` is coplanar with self, else False. + + Examples + ======== + + >>> from sympy import Plane + >>> o = (0, 0, 0) + >>> p = Plane(o, (1, 1, 1)) + >>> p2 = Plane(o, (2, 2, 2)) + >>> p == p2 + False + >>> p.is_coplanar(p2) + True + """ + if isinstance(o, Plane): + return not cancel(self.equation(x, y, z)/o.equation(x, y, z)).has(x, y, z) + if isinstance(o, Point3D): + return o in self + elif isinstance(o, LinearEntity3D): + return all(i in self for i in self) + elif isinstance(o, GeometryEntity): # XXX should only be handling 2D objects now + return all(i == 0 for i in self.normal_vector[:2]) + + + def is_parallel(self, l): + """Is the given geometric entity parallel to the plane? + + Parameters + ========== + + LinearEntity3D or Plane + + Returns + ======= + + Boolean + + Examples + ======== + + >>> from sympy import Plane, Point3D + >>> a = Plane(Point3D(1,4,6), normal_vector=(2, 4, 6)) + >>> b = Plane(Point3D(3,1,3), normal_vector=(4, 8, 12)) + >>> a.is_parallel(b) + True + + """ + if isinstance(l, LinearEntity3D): + a = l.direction_ratio + b = self.normal_vector + return sum(i*j for i, j in zip(a, b)) == 0 + if isinstance(l, Plane): + a = Matrix(l.normal_vector) + b = Matrix(self.normal_vector) + return bool(a.cross(b).is_zero_matrix) + + + def is_perpendicular(self, l): + """Is the given geometric entity perpendicualar to the given plane? + + Parameters + ========== + + LinearEntity3D or Plane + + Returns + ======= + + Boolean + + Examples + ======== + + >>> from sympy import Plane, Point3D + >>> a = Plane(Point3D(1,4,6), normal_vector=(2, 4, 6)) + >>> b = Plane(Point3D(2, 2, 2), normal_vector=(-1, 2, -1)) + >>> a.is_perpendicular(b) + True + + """ + if isinstance(l, LinearEntity3D): + a = Matrix(l.direction_ratio) + b = Matrix(self.normal_vector) + if a.cross(b).is_zero_matrix: + return True + else: + return False + elif isinstance(l, Plane): + a = Matrix(l.normal_vector) + b = Matrix(self.normal_vector) + if a.dot(b) == 0: + return True + else: + return False + else: + return False + + @property + def normal_vector(self): + """Normal vector of the given plane. + + Examples + ======== + + >>> from sympy import Point3D, Plane + >>> a = Plane(Point3D(1, 1, 1), Point3D(2, 3, 4), Point3D(2, 2, 2)) + >>> a.normal_vector + (-1, 2, -1) + >>> a = Plane(Point3D(1, 1, 1), normal_vector=(1, 4, 7)) + >>> a.normal_vector + (1, 4, 7) + + """ + return self.args[1] + + @property + def p1(self): + """The only defining point of the plane. Others can be obtained from the + arbitrary_point method. + + See Also + ======== + + sympy.geometry.point.Point3D + + Examples + ======== + + >>> from sympy import Point3D, Plane + >>> a = Plane(Point3D(1, 1, 1), Point3D(2, 3, 4), Point3D(2, 2, 2)) + >>> a.p1 + Point3D(1, 1, 1) + + """ + return self.args[0] + + def parallel_plane(self, pt): + """ + Plane parallel to the given plane and passing through the point pt. + + Parameters + ========== + + pt: Point3D + + Returns + ======= + + Plane + + Examples + ======== + + >>> from sympy import Plane, Point3D + >>> a = Plane(Point3D(1, 4, 6), normal_vector=(2, 4, 6)) + >>> a.parallel_plane(Point3D(2, 3, 5)) + Plane(Point3D(2, 3, 5), (2, 4, 6)) + + """ + a = self.normal_vector + return Plane(pt, normal_vector=a) + + def perpendicular_line(self, pt): + """A line perpendicular to the given plane. + + Parameters + ========== + + pt: Point3D + + Returns + ======= + + Line3D + + Examples + ======== + + >>> from sympy import Plane, Point3D + >>> a = Plane(Point3D(1,4,6), normal_vector=(2, 4, 6)) + >>> a.perpendicular_line(Point3D(9, 8, 7)) + Line3D(Point3D(9, 8, 7), Point3D(11, 12, 13)) + + """ + a = self.normal_vector + return Line3D(pt, direction_ratio=a) + + def perpendicular_plane(self, *pts): + """ + Return a perpendicular passing through the given points. If the + direction ratio between the points is the same as the Plane's normal + vector then, to select from the infinite number of possible planes, + a third point will be chosen on the z-axis (or the y-axis + if the normal vector is already parallel to the z-axis). If less than + two points are given they will be supplied as follows: if no point is + given then pt1 will be self.p1; if a second point is not given it will + be a point through pt1 on a line parallel to the z-axis (if the normal + is not already the z-axis, otherwise on the line parallel to the + y-axis). + + Parameters + ========== + + pts: 0, 1 or 2 Point3D + + Returns + ======= + + Plane + + Examples + ======== + + >>> from sympy import Plane, Point3D + >>> a, b = Point3D(0, 0, 0), Point3D(0, 1, 0) + >>> Z = (0, 0, 1) + >>> p = Plane(a, normal_vector=Z) + >>> p.perpendicular_plane(a, b) + Plane(Point3D(0, 0, 0), (1, 0, 0)) + """ + if len(pts) > 2: + raise ValueError('No more than 2 pts should be provided.') + + pts = list(pts) + if len(pts) == 0: + pts.append(self.p1) + if len(pts) == 1: + x, y, z = self.normal_vector + if x == y == 0: + dir = (0, 1, 0) + else: + dir = (0, 0, 1) + pts.append(pts[0] + Point3D(*dir)) + + p1, p2 = [Point(i, dim=3) for i in pts] + l = Line3D(p1, p2) + n = Line3D(p1, direction_ratio=self.normal_vector) + if l in n: # XXX should an error be raised instead? + # there are infinitely many perpendicular planes; + x, y, z = self.normal_vector + if x == y == 0: + # the z axis is the normal so pick a pt on the y-axis + p3 = Point3D(0, 1, 0) # case 1 + else: + # else pick a pt on the z axis + p3 = Point3D(0, 0, 1) # case 2 + # in case that point is already given, move it a bit + if p3 in l: + p3 *= 2 # case 3 + else: + p3 = p1 + Point3D(*self.normal_vector) # case 4 + return Plane(p1, p2, p3) + + def projection_line(self, line): + """Project the given line onto the plane through the normal plane + containing the line. + + Parameters + ========== + + LinearEntity or LinearEntity3D + + Returns + ======= + + Point3D, Line3D, Ray3D or Segment3D + + Notes + ===== + + For the interaction between 2D and 3D lines(segments, rays), you should + convert the line to 3D by using this method. For example for finding the + intersection between a 2D and a 3D line, convert the 2D line to a 3D line + by projecting it on a required plane and then proceed to find the + intersection between those lines. + + Examples + ======== + + >>> from sympy import Plane, Line, Line3D, Point3D + >>> a = Plane(Point3D(1, 1, 1), normal_vector=(1, 1, 1)) + >>> b = Line(Point3D(1, 1), Point3D(2, 2)) + >>> a.projection_line(b) + Line3D(Point3D(4/3, 4/3, 1/3), Point3D(5/3, 5/3, -1/3)) + >>> c = Line3D(Point3D(1, 1, 1), Point3D(2, 2, 2)) + >>> a.projection_line(c) + Point3D(1, 1, 1) + + """ + if not isinstance(line, (LinearEntity, LinearEntity3D)): + raise NotImplementedError('Enter a linear entity only') + a, b = self.projection(line.p1), self.projection(line.p2) + if a == b: + # projection does not imply intersection so for + # this case (line parallel to plane's normal) we + # return the projection point + return a + if isinstance(line, (Line, Line3D)): + return Line3D(a, b) + if isinstance(line, (Ray, Ray3D)): + return Ray3D(a, b) + if isinstance(line, (Segment, Segment3D)): + return Segment3D(a, b) + + def projection(self, pt): + """Project the given point onto the plane along the plane normal. + + Parameters + ========== + + Point or Point3D + + Returns + ======= + + Point3D + + Examples + ======== + + >>> from sympy import Plane, Point3D + >>> A = Plane(Point3D(1, 1, 2), normal_vector=(1, 1, 1)) + + The projection is along the normal vector direction, not the z + axis, so (1, 1) does not project to (1, 1, 2) on the plane A: + + >>> b = Point3D(1, 1) + >>> A.projection(b) + Point3D(5/3, 5/3, 2/3) + >>> _ in A + True + + But the point (1, 1, 2) projects to (1, 1) on the XY-plane: + + >>> XY = Plane((0, 0, 0), (0, 0, 1)) + >>> XY.projection((1, 1, 2)) + Point3D(1, 1, 0) + """ + rv = Point(pt, dim=3) + if rv in self: + return rv + return self.intersection(Line3D(rv, rv + Point3D(self.normal_vector)))[0] + + def random_point(self, seed=None): + """ Returns a random point on the Plane. + + Returns + ======= + + Point3D + + Examples + ======== + + >>> from sympy import Plane + >>> p = Plane((1, 0, 0), normal_vector=(0, 1, 0)) + >>> r = p.random_point(seed=42) # seed value is optional + >>> r.n(3) + Point3D(2.29, 0, -1.35) + + The random point can be moved to lie on the circle of radius + 1 centered on p1: + + >>> c = p.p1 + (r - p.p1).unit + >>> c.distance(p.p1).equals(1) + True + """ + if seed is not None: + rng = random.Random(seed) + else: + rng = random + params = { + x: 2*Rational(rng.gauss(0, 1)) - 1, + y: 2*Rational(rng.gauss(0, 1)) - 1} + return self.arbitrary_point(x, y).subs(params) + + def parameter_value(self, other, u, v=None): + """Return the parameter(s) corresponding to the given point. + + Examples + ======== + + >>> from sympy import pi, Plane + >>> from sympy.abc import t, u, v + >>> p = Plane((2, 0, 0), (0, 0, 1), (0, 1, 0)) + + By default, the parameter value returned defines a point + that is a distance of 1 from the Plane's p1 value and + in line with the given point: + + >>> on_circle = p.arbitrary_point(t).subs(t, pi/4) + >>> on_circle.distance(p.p1) + 1 + >>> p.parameter_value(on_circle, t) + {t: pi/4} + + Moving the point twice as far from p1 does not change + the parameter value: + + >>> off_circle = p.p1 + (on_circle - p.p1)*2 + >>> off_circle.distance(p.p1) + 2 + >>> p.parameter_value(off_circle, t) + {t: pi/4} + + If the 2-value parameter is desired, supply the two + parameter symbols and a replacement dictionary will + be returned: + + >>> p.parameter_value(on_circle, u, v) + {u: sqrt(10)/10, v: sqrt(10)/30} + >>> p.parameter_value(off_circle, u, v) + {u: sqrt(10)/5, v: sqrt(10)/15} + """ + if not isinstance(other, GeometryEntity): + other = Point(other, dim=self.ambient_dimension) + if not isinstance(other, Point): + raise ValueError("other must be a point") + if other == self.p1: + return other + if isinstance(u, Symbol) and v is None: + delta = self.arbitrary_point(u) - self.p1 + eq = delta - (other - self.p1).unit + sol = solve(eq, u, dict=True) + elif isinstance(u, Symbol) and isinstance(v, Symbol): + pt = self.arbitrary_point(u, v) + sol = solve(pt - other, (u, v), dict=True) + else: + raise ValueError('expecting 1 or 2 symbols') + if not sol: + raise ValueError("Given point is not on %s" % func_name(self)) + return sol[0] # {t: tval} or {u: uval, v: vval} + + @property + def ambient_dimension(self): + return self.p1.ambient_dimension diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/point.py b/venv/lib/python3.11/site-packages/sympy/geometry/point.py new file mode 100644 index 0000000000000000000000000000000000000000..19e6c566f06de4df086912470dc35d0f4af3bd38 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/point.py @@ -0,0 +1,1378 @@ +"""Geometrical Points. + +Contains +======== +Point +Point2D +Point3D + +When methods of Point require 1 or more points as arguments, they +can be passed as a sequence of coordinates or Points: + +>>> from sympy import Point +>>> Point(1, 1).is_collinear((2, 2), (3, 4)) +False +>>> Point(1, 1).is_collinear(Point(2, 2), Point(3, 4)) +False + +""" + +import warnings + +from sympy.core import S, sympify, Expr +from sympy.core.add import Add +from sympy.core.containers import Tuple +from sympy.core.numbers import Float +from sympy.core.parameters import global_parameters +from sympy.simplify.simplify import nsimplify, simplify +from sympy.geometry.exceptions import GeometryError +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.complexes import im +from sympy.functions.elementary.trigonometric import cos, sin +from sympy.matrices import Matrix +from sympy.matrices.expressions import Transpose +from sympy.utilities.iterables import uniq, is_sequence +from sympy.utilities.misc import filldedent, func_name, Undecidable + +from .entity import GeometryEntity + +from mpmath.libmp.libmpf import prec_to_dps + + +class Point(GeometryEntity): + """A point in a n-dimensional Euclidean space. + + Parameters + ========== + + coords : sequence of n-coordinate values. In the special + case where n=2 or 3, a Point2D or Point3D will be created + as appropriate. + evaluate : if `True` (default), all floats are turn into + exact types. + dim : number of coordinates the point should have. If coordinates + are unspecified, they are padded with zeros. + on_morph : indicates what should happen when the number of + coordinates of a point need to be changed by adding or + removing zeros. Possible values are `'warn'`, `'error'`, or + `ignore` (default). No warning or error is given when `*args` + is empty and `dim` is given. An error is always raised when + trying to remove nonzero coordinates. + + + Attributes + ========== + + length + origin: A `Point` representing the origin of the + appropriately-dimensioned space. + + Raises + ====== + + TypeError : When instantiating with anything but a Point or sequence + ValueError : when instantiating with a sequence with length < 2 or + when trying to reduce dimensions if keyword `on_morph='error'` is + set. + + See Also + ======== + + sympy.geometry.line.Segment : Connects two Points + + Examples + ======== + + >>> from sympy import Point + >>> from sympy.abc import x + >>> Point(1, 2, 3) + Point3D(1, 2, 3) + >>> Point([1, 2]) + Point2D(1, 2) + >>> Point(0, x) + Point2D(0, x) + >>> Point(dim=4) + Point(0, 0, 0, 0) + + Floats are automatically converted to Rational unless the + evaluate flag is False: + + >>> Point(0.5, 0.25) + Point2D(1/2, 1/4) + >>> Point(0.5, 0.25, evaluate=False) + Point2D(0.5, 0.25) + + """ + + is_Point = True + + def __new__(cls, *args, **kwargs): + evaluate = kwargs.get('evaluate', global_parameters.evaluate) + on_morph = kwargs.get('on_morph', 'ignore') + + # unpack into coords + coords = args[0] if len(args) == 1 else args + + # check args and handle quickly handle Point instances + if isinstance(coords, Point): + # even if we're mutating the dimension of a point, we + # don't reevaluate its coordinates + evaluate = False + if len(coords) == kwargs.get('dim', len(coords)): + return coords + + if not is_sequence(coords): + raise TypeError(filldedent(''' + Expecting sequence of coordinates, not `{}`''' + .format(func_name(coords)))) + # A point where only `dim` is specified is initialized + # to zeros. + if len(coords) == 0 and kwargs.get('dim', None): + coords = (S.Zero,)*kwargs.get('dim') + + coords = Tuple(*coords) + dim = kwargs.get('dim', len(coords)) + + if len(coords) < 2: + raise ValueError(filldedent(''' + Point requires 2 or more coordinates or + keyword `dim` > 1.''')) + if len(coords) != dim: + message = ("Dimension of {} needs to be changed " + "from {} to {}.").format(coords, len(coords), dim) + if on_morph == 'ignore': + pass + elif on_morph == "error": + raise ValueError(message) + elif on_morph == 'warn': + warnings.warn(message, stacklevel=2) + else: + raise ValueError(filldedent(''' + on_morph value should be 'error', + 'warn' or 'ignore'.''')) + if any(coords[dim:]): + raise ValueError('Nonzero coordinates cannot be removed.') + if any(a.is_number and im(a).is_zero is False for a in coords): + raise ValueError('Imaginary coordinates are not permitted.') + if not all(isinstance(a, Expr) for a in coords): + raise TypeError('Coordinates must be valid SymPy expressions.') + + # pad with zeros appropriately + coords = coords[:dim] + (S.Zero,)*(dim - len(coords)) + + # Turn any Floats into rationals and simplify + # any expressions before we instantiate + if evaluate: + coords = coords.xreplace({ + f: simplify(nsimplify(f, rational=True)) + for f in coords.atoms(Float)}) + + # return 2D or 3D instances + if len(coords) == 2: + kwargs['_nocheck'] = True + return Point2D(*coords, **kwargs) + elif len(coords) == 3: + kwargs['_nocheck'] = True + return Point3D(*coords, **kwargs) + + # the general Point + return GeometryEntity.__new__(cls, *coords) + + def __abs__(self): + """Returns the distance between this point and the origin.""" + origin = Point([0]*len(self)) + return Point.distance(origin, self) + + def __add__(self, other): + """Add other to self by incrementing self's coordinates by + those of other. + + Notes + ===== + + >>> from sympy import Point + + When sequences of coordinates are passed to Point methods, they + are converted to a Point internally. This __add__ method does + not do that so if floating point values are used, a floating + point result (in terms of SymPy Floats) will be returned. + + >>> Point(1, 2) + (.1, .2) + Point2D(1.1, 2.2) + + If this is not desired, the `translate` method can be used or + another Point can be added: + + >>> Point(1, 2).translate(.1, .2) + Point2D(11/10, 11/5) + >>> Point(1, 2) + Point(.1, .2) + Point2D(11/10, 11/5) + + See Also + ======== + + sympy.geometry.point.Point.translate + + """ + try: + s, o = Point._normalize_dimension(self, Point(other, evaluate=False)) + except TypeError: + raise GeometryError("Don't know how to add {} and a Point object".format(other)) + + coords = [simplify(a + b) for a, b in zip(s, o)] + return Point(coords, evaluate=False) + + def __contains__(self, item): + return item in self.args + + def __truediv__(self, divisor): + """Divide point's coordinates by a factor.""" + divisor = sympify(divisor) + coords = [simplify(x/divisor) for x in self.args] + return Point(coords, evaluate=False) + + def __eq__(self, other): + if not isinstance(other, Point) or len(self.args) != len(other.args): + return False + return self.args == other.args + + def __getitem__(self, key): + return self.args[key] + + def __hash__(self): + return hash(self.args) + + def __iter__(self): + return self.args.__iter__() + + def __len__(self): + return len(self.args) + + def __mul__(self, factor): + """Multiply point's coordinates by a factor. + + Notes + ===== + + >>> from sympy import Point + + When multiplying a Point by a floating point number, + the coordinates of the Point will be changed to Floats: + + >>> Point(1, 2)*0.1 + Point2D(0.1, 0.2) + + If this is not desired, the `scale` method can be used or + else only multiply or divide by integers: + + >>> Point(1, 2).scale(1.1, 1.1) + Point2D(11/10, 11/5) + >>> Point(1, 2)*11/10 + Point2D(11/10, 11/5) + + See Also + ======== + + sympy.geometry.point.Point.scale + """ + factor = sympify(factor) + coords = [simplify(x*factor) for x in self.args] + return Point(coords, evaluate=False) + + def __rmul__(self, factor): + """Multiply a factor by point's coordinates.""" + return self.__mul__(factor) + + def __neg__(self): + """Negate the point.""" + coords = [-x for x in self.args] + return Point(coords, evaluate=False) + + def __sub__(self, other): + """Subtract two points, or subtract a factor from this point's + coordinates.""" + return self + [-x for x in other] + + @classmethod + def _normalize_dimension(cls, *points, **kwargs): + """Ensure that points have the same dimension. + By default `on_morph='warn'` is passed to the + `Point` constructor.""" + # if we have a built-in ambient dimension, use it + dim = getattr(cls, '_ambient_dimension', None) + # override if we specified it + dim = kwargs.get('dim', dim) + # if no dim was given, use the highest dimensional point + if dim is None: + dim = max(i.ambient_dimension for i in points) + if all(i.ambient_dimension == dim for i in points): + return list(points) + kwargs['dim'] = dim + kwargs['on_morph'] = kwargs.get('on_morph', 'warn') + return [Point(i, **kwargs) for i in points] + + @staticmethod + def affine_rank(*args): + """The affine rank of a set of points is the dimension + of the smallest affine space containing all the points. + For example, if the points lie on a line (and are not all + the same) their affine rank is 1. If the points lie on a plane + but not a line, their affine rank is 2. By convention, the empty + set has affine rank -1.""" + + if len(args) == 0: + return -1 + # make sure we're genuinely points + # and translate every point to the origin + points = Point._normalize_dimension(*[Point(i) for i in args]) + origin = points[0] + points = [i - origin for i in points[1:]] + + m = Matrix([i.args for i in points]) + # XXX fragile -- what is a better way? + return m.rank(iszerofunc = lambda x: + abs(x.n(2)) < 1e-12 if x.is_number else x.is_zero) + + @property + def ambient_dimension(self): + """Number of components this point has.""" + return getattr(self, '_ambient_dimension', len(self)) + + @classmethod + def are_coplanar(cls, *points): + """Return True if there exists a plane in which all the points + lie. A trivial True value is returned if `len(points) < 3` or + all Points are 2-dimensional. + + Parameters + ========== + + A set of points + + Raises + ====== + + ValueError : if less than 3 unique points are given + + Returns + ======= + + boolean + + Examples + ======== + + >>> from sympy import Point3D + >>> p1 = Point3D(1, 2, 2) + >>> p2 = Point3D(2, 7, 2) + >>> p3 = Point3D(0, 0, 2) + >>> p4 = Point3D(1, 1, 2) + >>> Point3D.are_coplanar(p1, p2, p3, p4) + True + >>> p5 = Point3D(0, 1, 3) + >>> Point3D.are_coplanar(p1, p2, p3, p5) + False + + """ + if len(points) <= 1: + return True + + points = cls._normalize_dimension(*[Point(i) for i in points]) + # quick exit if we are in 2D + if points[0].ambient_dimension == 2: + return True + points = list(uniq(points)) + return Point.affine_rank(*points) <= 2 + + def distance(self, other): + """The Euclidean distance between self and another GeometricEntity. + + Returns + ======= + + distance : number or symbolic expression. + + Raises + ====== + + TypeError : if other is not recognized as a GeometricEntity or is a + GeometricEntity for which distance is not defined. + + See Also + ======== + + sympy.geometry.line.Segment.length + sympy.geometry.point.Point.taxicab_distance + + Examples + ======== + + >>> from sympy import Point, Line + >>> p1, p2 = Point(1, 1), Point(4, 5) + >>> l = Line((3, 1), (2, 2)) + >>> p1.distance(p2) + 5 + >>> p1.distance(l) + sqrt(2) + + The computed distance may be symbolic, too: + + >>> from sympy.abc import x, y + >>> p3 = Point(x, y) + >>> p3.distance((0, 0)) + sqrt(x**2 + y**2) + + """ + if not isinstance(other, GeometryEntity): + try: + other = Point(other, dim=self.ambient_dimension) + except TypeError: + raise TypeError("not recognized as a GeometricEntity: %s" % type(other)) + if isinstance(other, Point): + s, p = Point._normalize_dimension(self, Point(other)) + return sqrt(Add(*((a - b)**2 for a, b in zip(s, p)))) + distance = getattr(other, 'distance', None) + if distance is None: + raise TypeError("distance between Point and %s is not defined" % type(other)) + return distance(self) + + def dot(self, p): + """Return dot product of self with another Point.""" + if not is_sequence(p): + p = Point(p) # raise the error via Point + return Add(*(a*b for a, b in zip(self, p))) + + def equals(self, other): + """Returns whether the coordinates of self and other agree.""" + # a point is equal to another point if all its components are equal + if not isinstance(other, Point) or len(self) != len(other): + return False + return all(a.equals(b) for a, b in zip(self, other)) + + def _eval_evalf(self, prec=15, **options): + """Evaluate the coordinates of the point. + + This method will, where possible, create and return a new Point + where the coordinates are evaluated as floating point numbers to + the precision indicated (default=15). + + Parameters + ========== + + prec : int + + Returns + ======= + + point : Point + + Examples + ======== + + >>> from sympy import Point, Rational + >>> p1 = Point(Rational(1, 2), Rational(3, 2)) + >>> p1 + Point2D(1/2, 3/2) + >>> p1.evalf() + Point2D(0.5, 1.5) + + """ + dps = prec_to_dps(prec) + coords = [x.evalf(n=dps, **options) for x in self.args] + return Point(*coords, evaluate=False) + + def intersection(self, other): + """The intersection between this point and another GeometryEntity. + + Parameters + ========== + + other : GeometryEntity or sequence of coordinates + + Returns + ======= + + intersection : list of Points + + Notes + ===== + + The return value will either be an empty list if there is no + intersection, otherwise it will contain this point. + + Examples + ======== + + >>> from sympy import Point + >>> p1, p2, p3 = Point(0, 0), Point(1, 1), Point(0, 0) + >>> p1.intersection(p2) + [] + >>> p1.intersection(p3) + [Point2D(0, 0)] + + """ + if not isinstance(other, GeometryEntity): + other = Point(other) + if isinstance(other, Point): + if self == other: + return [self] + p1, p2 = Point._normalize_dimension(self, other) + if p1 == self and p1 == p2: + return [self] + return [] + return other.intersection(self) + + def is_collinear(self, *args): + """Returns `True` if there exists a line + that contains `self` and `points`. Returns `False` otherwise. + A trivially True value is returned if no points are given. + + Parameters + ========== + + args : sequence of Points + + Returns + ======= + + is_collinear : boolean + + See Also + ======== + + sympy.geometry.line.Line + + Examples + ======== + + >>> from sympy import Point + >>> from sympy.abc import x + >>> p1, p2 = Point(0, 0), Point(1, 1) + >>> p3, p4, p5 = Point(2, 2), Point(x, x), Point(1, 2) + >>> Point.is_collinear(p1, p2, p3, p4) + True + >>> Point.is_collinear(p1, p2, p3, p5) + False + + """ + points = (self,) + args + points = Point._normalize_dimension(*[Point(i) for i in points]) + points = list(uniq(points)) + return Point.affine_rank(*points) <= 1 + + def is_concyclic(self, *args): + """Do `self` and the given sequence of points lie in a circle? + + Returns True if the set of points are concyclic and + False otherwise. A trivial value of True is returned + if there are fewer than 2 other points. + + Parameters + ========== + + args : sequence of Points + + Returns + ======= + + is_concyclic : boolean + + + Examples + ======== + + >>> from sympy import Point + + Define 4 points that are on the unit circle: + + >>> p1, p2, p3, p4 = Point(1, 0), (0, 1), (-1, 0), (0, -1) + + >>> p1.is_concyclic() == p1.is_concyclic(p2, p3, p4) == True + True + + Define a point not on that circle: + + >>> p = Point(1, 1) + + >>> p.is_concyclic(p1, p2, p3) + False + + """ + points = (self,) + args + points = Point._normalize_dimension(*[Point(i) for i in points]) + points = list(uniq(points)) + if not Point.affine_rank(*points) <= 2: + return False + origin = points[0] + points = [p - origin for p in points] + # points are concyclic if they are coplanar and + # there is a point c so that ||p_i-c|| == ||p_j-c|| for all + # i and j. Rearranging this equation gives us the following + # condition: the matrix `mat` must not a pivot in the last + # column. + mat = Matrix([list(i) + [i.dot(i)] for i in points]) + rref, pivots = mat.rref() + if len(origin) not in pivots: + return True + return False + + @property + def is_nonzero(self): + """True if any coordinate is nonzero, False if every coordinate is zero, + and None if it cannot be determined.""" + is_zero = self.is_zero + if is_zero is None: + return None + return not is_zero + + def is_scalar_multiple(self, p): + """Returns whether each coordinate of `self` is a scalar + multiple of the corresponding coordinate in point p. + """ + s, o = Point._normalize_dimension(self, Point(p)) + # 2d points happen a lot, so optimize this function call + if s.ambient_dimension == 2: + (x1, y1), (x2, y2) = s.args, o.args + rv = (x1*y2 - x2*y1).equals(0) + if rv is None: + raise Undecidable(filldedent( + '''Cannot determine if %s is a scalar multiple of + %s''' % (s, o))) + + # if the vectors p1 and p2 are linearly dependent, then they must + # be scalar multiples of each other + m = Matrix([s.args, o.args]) + return m.rank() < 2 + + @property + def is_zero(self): + """True if every coordinate is zero, False if any coordinate is not zero, + and None if it cannot be determined.""" + nonzero = [x.is_nonzero for x in self.args] + if any(nonzero): + return False + if any(x is None for x in nonzero): + return None + return True + + @property + def length(self): + """ + Treating a Point as a Line, this returns 0 for the length of a Point. + + Examples + ======== + + >>> from sympy import Point + >>> p = Point(0, 1) + >>> p.length + 0 + """ + return S.Zero + + def midpoint(self, p): + """The midpoint between self and point p. + + Parameters + ========== + + p : Point + + Returns + ======= + + midpoint : Point + + See Also + ======== + + sympy.geometry.line.Segment.midpoint + + Examples + ======== + + >>> from sympy import Point + >>> p1, p2 = Point(1, 1), Point(13, 5) + >>> p1.midpoint(p2) + Point2D(7, 3) + + """ + s, p = Point._normalize_dimension(self, Point(p)) + return Point([simplify((a + b)*S.Half) for a, b in zip(s, p)]) + + @property + def origin(self): + """A point of all zeros of the same ambient dimension + as the current point""" + return Point([0]*len(self), evaluate=False) + + @property + def orthogonal_direction(self): + """Returns a non-zero point that is orthogonal to the + line containing `self` and the origin. + + Examples + ======== + + >>> from sympy import Line, Point + >>> a = Point(1, 2, 3) + >>> a.orthogonal_direction + Point3D(-2, 1, 0) + >>> b = _ + >>> Line(b, b.origin).is_perpendicular(Line(a, a.origin)) + True + """ + dim = self.ambient_dimension + # if a coordinate is zero, we can put a 1 there and zeros elsewhere + if self[0].is_zero: + return Point([1] + (dim - 1)*[0]) + if self[1].is_zero: + return Point([0,1] + (dim - 2)*[0]) + # if the first two coordinates aren't zero, we can create a non-zero + # orthogonal vector by swapping them, negating one, and padding with zeros + return Point([-self[1], self[0]] + (dim - 2)*[0]) + + @staticmethod + def project(a, b): + """Project the point `a` onto the line between the origin + and point `b` along the normal direction. + + Parameters + ========== + + a : Point + b : Point + + Returns + ======= + + p : Point + + See Also + ======== + + sympy.geometry.line.LinearEntity.projection + + Examples + ======== + + >>> from sympy import Line, Point + >>> a = Point(1, 2) + >>> b = Point(2, 5) + >>> z = a.origin + >>> p = Point.project(a, b) + >>> Line(p, a).is_perpendicular(Line(p, b)) + True + >>> Point.is_collinear(z, p, b) + True + """ + a, b = Point._normalize_dimension(Point(a), Point(b)) + if b.is_zero: + raise ValueError("Cannot project to the zero vector.") + return b*(a.dot(b) / b.dot(b)) + + def taxicab_distance(self, p): + """The Taxicab Distance from self to point p. + + Returns the sum of the horizontal and vertical distances to point p. + + Parameters + ========== + + p : Point + + Returns + ======= + + taxicab_distance : The sum of the horizontal + and vertical distances to point p. + + See Also + ======== + + sympy.geometry.point.Point.distance + + Examples + ======== + + >>> from sympy import Point + >>> p1, p2 = Point(1, 1), Point(4, 5) + >>> p1.taxicab_distance(p2) + 7 + + """ + s, p = Point._normalize_dimension(self, Point(p)) + return Add(*(abs(a - b) for a, b in zip(s, p))) + + def canberra_distance(self, p): + """The Canberra Distance from self to point p. + + Returns the weighted sum of horizontal and vertical distances to + point p. + + Parameters + ========== + + p : Point + + Returns + ======= + + canberra_distance : The weighted sum of horizontal and vertical + distances to point p. The weight used is the sum of absolute values + of the coordinates. + + Examples + ======== + + >>> from sympy import Point + >>> p1, p2 = Point(1, 1), Point(3, 3) + >>> p1.canberra_distance(p2) + 1 + >>> p1, p2 = Point(0, 0), Point(3, 3) + >>> p1.canberra_distance(p2) + 2 + + Raises + ====== + + ValueError when both vectors are zero. + + See Also + ======== + + sympy.geometry.point.Point.distance + + """ + + s, p = Point._normalize_dimension(self, Point(p)) + if self.is_zero and p.is_zero: + raise ValueError("Cannot project to the zero vector.") + return Add(*((abs(a - b)/(abs(a) + abs(b))) for a, b in zip(s, p))) + + @property + def unit(self): + """Return the Point that is in the same direction as `self` + and a distance of 1 from the origin""" + return self / abs(self) + + +class Point2D(Point): + """A point in a 2-dimensional Euclidean space. + + Parameters + ========== + + coords + A sequence of 2 coordinate values. + + Attributes + ========== + + x + y + length + + Raises + ====== + + TypeError + When trying to add or subtract points with different dimensions. + When trying to create a point with more than two dimensions. + When `intersection` is called with object other than a Point. + + See Also + ======== + + sympy.geometry.line.Segment : Connects two Points + + Examples + ======== + + >>> from sympy import Point2D + >>> from sympy.abc import x + >>> Point2D(1, 2) + Point2D(1, 2) + >>> Point2D([1, 2]) + Point2D(1, 2) + >>> Point2D(0, x) + Point2D(0, x) + + Floats are automatically converted to Rational unless the + evaluate flag is False: + + >>> Point2D(0.5, 0.25) + Point2D(1/2, 1/4) + >>> Point2D(0.5, 0.25, evaluate=False) + Point2D(0.5, 0.25) + + """ + + _ambient_dimension = 2 + + def __new__(cls, *args, _nocheck=False, **kwargs): + if not _nocheck: + kwargs['dim'] = 2 + args = Point(*args, **kwargs) + return GeometryEntity.__new__(cls, *args) + + def __contains__(self, item): + return item == self + + @property + def bounds(self): + """Return a tuple (xmin, ymin, xmax, ymax) representing the bounding + rectangle for the geometric figure. + + """ + + return (self.x, self.y, self.x, self.y) + + def rotate(self, angle, pt=None): + """Rotate ``angle`` radians counterclockwise about Point ``pt``. + + See Also + ======== + + translate, scale + + Examples + ======== + + >>> from sympy import Point2D, pi + >>> t = Point2D(1, 0) + >>> t.rotate(pi/2) + Point2D(0, 1) + >>> t.rotate(pi/2, (2, 0)) + Point2D(2, -1) + + """ + c = cos(angle) + s = sin(angle) + + rv = self + if pt is not None: + pt = Point(pt, dim=2) + rv -= pt + x, y = rv.args + rv = Point(c*x - s*y, s*x + c*y) + if pt is not None: + rv += pt + return rv + + def scale(self, x=1, y=1, pt=None): + """Scale the coordinates of the Point by multiplying by + ``x`` and ``y`` after subtracting ``pt`` -- default is (0, 0) -- + and then adding ``pt`` back again (i.e. ``pt`` is the point of + reference for the scaling). + + See Also + ======== + + rotate, translate + + Examples + ======== + + >>> from sympy import Point2D + >>> t = Point2D(1, 1) + >>> t.scale(2) + Point2D(2, 1) + >>> t.scale(2, 2) + Point2D(2, 2) + + """ + if pt: + pt = Point(pt, dim=2) + return self.translate(*(-pt).args).scale(x, y).translate(*pt.args) + return Point(self.x*x, self.y*y) + + def transform(self, matrix): + """Return the point after applying the transformation described + by the 3x3 Matrix, ``matrix``. + + See Also + ======== + sympy.geometry.point.Point2D.rotate + sympy.geometry.point.Point2D.scale + sympy.geometry.point.Point2D.translate + """ + if not (matrix.is_Matrix and matrix.shape == (3, 3)): + raise ValueError("matrix must be a 3x3 matrix") + x, y = self.args + return Point(*(Matrix(1, 3, [x, y, 1])*matrix).tolist()[0][:2]) + + def translate(self, x=0, y=0): + """Shift the Point by adding x and y to the coordinates of the Point. + + See Also + ======== + + sympy.geometry.point.Point2D.rotate, scale + + Examples + ======== + + >>> from sympy import Point2D + >>> t = Point2D(0, 1) + >>> t.translate(2) + Point2D(2, 1) + >>> t.translate(2, 2) + Point2D(2, 3) + >>> t + Point2D(2, 2) + Point2D(2, 3) + + """ + return Point(self.x + x, self.y + y) + + @property + def coordinates(self): + """ + Returns the two coordinates of the Point. + + Examples + ======== + + >>> from sympy import Point2D + >>> p = Point2D(0, 1) + >>> p.coordinates + (0, 1) + """ + return self.args + + @property + def x(self): + """ + Returns the X coordinate of the Point. + + Examples + ======== + + >>> from sympy import Point2D + >>> p = Point2D(0, 1) + >>> p.x + 0 + """ + return self.args[0] + + @property + def y(self): + """ + Returns the Y coordinate of the Point. + + Examples + ======== + + >>> from sympy import Point2D + >>> p = Point2D(0, 1) + >>> p.y + 1 + """ + return self.args[1] + +class Point3D(Point): + """A point in a 3-dimensional Euclidean space. + + Parameters + ========== + + coords + A sequence of 3 coordinate values. + + Attributes + ========== + + x + y + z + length + + Raises + ====== + + TypeError + When trying to add or subtract points with different dimensions. + When `intersection` is called with object other than a Point. + + Examples + ======== + + >>> from sympy import Point3D + >>> from sympy.abc import x + >>> Point3D(1, 2, 3) + Point3D(1, 2, 3) + >>> Point3D([1, 2, 3]) + Point3D(1, 2, 3) + >>> Point3D(0, x, 3) + Point3D(0, x, 3) + + Floats are automatically converted to Rational unless the + evaluate flag is False: + + >>> Point3D(0.5, 0.25, 2) + Point3D(1/2, 1/4, 2) + >>> Point3D(0.5, 0.25, 3, evaluate=False) + Point3D(0.5, 0.25, 3) + + """ + + _ambient_dimension = 3 + + def __new__(cls, *args, _nocheck=False, **kwargs): + if not _nocheck: + kwargs['dim'] = 3 + args = Point(*args, **kwargs) + return GeometryEntity.__new__(cls, *args) + + def __contains__(self, item): + return item == self + + @staticmethod + def are_collinear(*points): + """Is a sequence of points collinear? + + Test whether or not a set of points are collinear. Returns True if + the set of points are collinear, or False otherwise. + + Parameters + ========== + + points : sequence of Point + + Returns + ======= + + are_collinear : boolean + + See Also + ======== + + sympy.geometry.line.Line3D + + Examples + ======== + + >>> from sympy import Point3D + >>> from sympy.abc import x + >>> p1, p2 = Point3D(0, 0, 0), Point3D(1, 1, 1) + >>> p3, p4, p5 = Point3D(2, 2, 2), Point3D(x, x, x), Point3D(1, 2, 6) + >>> Point3D.are_collinear(p1, p2, p3, p4) + True + >>> Point3D.are_collinear(p1, p2, p3, p5) + False + """ + return Point.is_collinear(*points) + + def direction_cosine(self, point): + """ + Gives the direction cosine between 2 points + + Parameters + ========== + + p : Point3D + + Returns + ======= + + list + + Examples + ======== + + >>> from sympy import Point3D + >>> p1 = Point3D(1, 2, 3) + >>> p1.direction_cosine(Point3D(2, 3, 5)) + [sqrt(6)/6, sqrt(6)/6, sqrt(6)/3] + """ + a = self.direction_ratio(point) + b = sqrt(Add(*(i**2 for i in a))) + return [(point.x - self.x) / b,(point.y - self.y) / b, + (point.z - self.z) / b] + + def direction_ratio(self, point): + """ + Gives the direction ratio between 2 points + + Parameters + ========== + + p : Point3D + + Returns + ======= + + list + + Examples + ======== + + >>> from sympy import Point3D + >>> p1 = Point3D(1, 2, 3) + >>> p1.direction_ratio(Point3D(2, 3, 5)) + [1, 1, 2] + """ + return [(point.x - self.x),(point.y - self.y),(point.z - self.z)] + + def intersection(self, other): + """The intersection between this point and another GeometryEntity. + + Parameters + ========== + + other : GeometryEntity or sequence of coordinates + + Returns + ======= + + intersection : list of Points + + Notes + ===== + + The return value will either be an empty list if there is no + intersection, otherwise it will contain this point. + + Examples + ======== + + >>> from sympy import Point3D + >>> p1, p2, p3 = Point3D(0, 0, 0), Point3D(1, 1, 1), Point3D(0, 0, 0) + >>> p1.intersection(p2) + [] + >>> p1.intersection(p3) + [Point3D(0, 0, 0)] + + """ + if not isinstance(other, GeometryEntity): + other = Point(other, dim=3) + if isinstance(other, Point3D): + if self == other: + return [self] + return [] + return other.intersection(self) + + def scale(self, x=1, y=1, z=1, pt=None): + """Scale the coordinates of the Point by multiplying by + ``x`` and ``y`` after subtracting ``pt`` -- default is (0, 0) -- + and then adding ``pt`` back again (i.e. ``pt`` is the point of + reference for the scaling). + + See Also + ======== + + translate + + Examples + ======== + + >>> from sympy import Point3D + >>> t = Point3D(1, 1, 1) + >>> t.scale(2) + Point3D(2, 1, 1) + >>> t.scale(2, 2) + Point3D(2, 2, 1) + + """ + if pt: + pt = Point3D(pt) + return self.translate(*(-pt).args).scale(x, y, z).translate(*pt.args) + return Point3D(self.x*x, self.y*y, self.z*z) + + def transform(self, matrix): + """Return the point after applying the transformation described + by the 4x4 Matrix, ``matrix``. + + See Also + ======== + sympy.geometry.point.Point3D.scale + sympy.geometry.point.Point3D.translate + """ + if not (matrix.is_Matrix and matrix.shape == (4, 4)): + raise ValueError("matrix must be a 4x4 matrix") + x, y, z = self.args + m = Transpose(matrix) + return Point3D(*(Matrix(1, 4, [x, y, z, 1])*m).tolist()[0][:3]) + + def translate(self, x=0, y=0, z=0): + """Shift the Point by adding x and y to the coordinates of the Point. + + See Also + ======== + + scale + + Examples + ======== + + >>> from sympy import Point3D + >>> t = Point3D(0, 1, 1) + >>> t.translate(2) + Point3D(2, 1, 1) + >>> t.translate(2, 2) + Point3D(2, 3, 1) + >>> t + Point3D(2, 2, 2) + Point3D(2, 3, 3) + + """ + return Point3D(self.x + x, self.y + y, self.z + z) + + @property + def coordinates(self): + """ + Returns the three coordinates of the Point. + + Examples + ======== + + >>> from sympy import Point3D + >>> p = Point3D(0, 1, 2) + >>> p.coordinates + (0, 1, 2) + """ + return self.args + + @property + def x(self): + """ + Returns the X coordinate of the Point. + + Examples + ======== + + >>> from sympy import Point3D + >>> p = Point3D(0, 1, 3) + >>> p.x + 0 + """ + return self.args[0] + + @property + def y(self): + """ + Returns the Y coordinate of the Point. + + Examples + ======== + + >>> from sympy import Point3D + >>> p = Point3D(0, 1, 2) + >>> p.y + 1 + """ + return self.args[1] + + @property + def z(self): + """ + Returns the Z coordinate of the Point. + + Examples + ======== + + >>> from sympy import Point3D + >>> p = Point3D(0, 1, 1) + >>> p.z + 1 + """ + return self.args[2] diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/polygon.py b/venv/lib/python3.11/site-packages/sympy/geometry/polygon.py new file mode 100644 index 0000000000000000000000000000000000000000..63031183438e2d228f881fd82e1b0ecca04ec534 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/polygon.py @@ -0,0 +1,2891 @@ +from sympy.core import Expr, S, oo, pi, sympify +from sympy.core.evalf import N +from sympy.core.sorting import default_sort_key, ordered +from sympy.core.symbol import _symbol, Dummy, Symbol +from sympy.functions.elementary.complexes import sign +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import cos, sin, tan +from .ellipse import Circle +from .entity import GeometryEntity, GeometrySet +from .exceptions import GeometryError +from .line import Line, Segment, Ray +from .point import Point +from sympy.logic import And +from sympy.matrices import Matrix +from sympy.simplify.simplify import simplify +from sympy.solvers.solvers import solve +from sympy.utilities.iterables import has_dups, has_variety, uniq, rotate_left, least_rotation +from sympy.utilities.misc import as_int, func_name + +from mpmath.libmp.libmpf import prec_to_dps + +import warnings + + +x, y, T = [Dummy('polygon_dummy', real=True) for i in range(3)] + + +class Polygon(GeometrySet): + """A two-dimensional polygon. + + A simple polygon in space. Can be constructed from a sequence of points + or from a center, radius, number of sides and rotation angle. + + Parameters + ========== + + vertices + A sequence of points. + + n : int, optional + If $> 0$, an n-sided RegularPolygon is created. + Default value is $0$. + + Attributes + ========== + + area + angles + perimeter + vertices + centroid + sides + + Raises + ====== + + GeometryError + If all parameters are not Points. + + See Also + ======== + + sympy.geometry.point.Point, sympy.geometry.line.Segment, Triangle + + Notes + ===== + + Polygons are treated as closed paths rather than 2D areas so + some calculations can be be negative or positive (e.g., area) + based on the orientation of the points. + + Any consecutive identical points are reduced to a single point + and any points collinear and between two points will be removed + unless they are needed to define an explicit intersection (see examples). + + A Triangle, Segment or Point will be returned when there are 3 or + fewer points provided. + + Examples + ======== + + >>> from sympy import Polygon, pi + >>> p1, p2, p3, p4, p5 = [(0, 0), (1, 0), (5, 1), (0, 1), (3, 0)] + >>> Polygon(p1, p2, p3, p4) + Polygon(Point2D(0, 0), Point2D(1, 0), Point2D(5, 1), Point2D(0, 1)) + >>> Polygon(p1, p2) + Segment2D(Point2D(0, 0), Point2D(1, 0)) + >>> Polygon(p1, p2, p5) + Segment2D(Point2D(0, 0), Point2D(3, 0)) + + The area of a polygon is calculated as positive when vertices are + traversed in a ccw direction. When the sides of a polygon cross the + area will have positive and negative contributions. The following + defines a Z shape where the bottom right connects back to the top + left. + + >>> Polygon((0, 2), (2, 2), (0, 0), (2, 0)).area + 0 + + When the keyword `n` is used to define the number of sides of the + Polygon then a RegularPolygon is created and the other arguments are + interpreted as center, radius and rotation. The unrotated RegularPolygon + will always have a vertex at Point(r, 0) where `r` is the radius of the + circle that circumscribes the RegularPolygon. Its method `spin` can be + used to increment that angle. + + >>> p = Polygon((0,0), 1, n=3) + >>> p + RegularPolygon(Point2D(0, 0), 1, 3, 0) + >>> p.vertices[0] + Point2D(1, 0) + >>> p.args[0] + Point2D(0, 0) + >>> p.spin(pi/2) + >>> p.vertices[0] + Point2D(0, 1) + + """ + + __slots__ = () + + def __new__(cls, *args, n = 0, **kwargs): + if n: + args = list(args) + # return a virtual polygon with n sides + if len(args) == 2: # center, radius + args.append(n) + elif len(args) == 3: # center, radius, rotation + args.insert(2, n) + return RegularPolygon(*args, **kwargs) + + vertices = [Point(a, dim=2, **kwargs) for a in args] + + # remove consecutive duplicates + nodup = [] + for p in vertices: + if nodup and p == nodup[-1]: + continue + nodup.append(p) + if len(nodup) > 1 and nodup[-1] == nodup[0]: + nodup.pop() # last point was same as first + + # remove collinear points + i = -3 + while i < len(nodup) - 3 and len(nodup) > 2: + a, b, c = nodup[i], nodup[i + 1], nodup[i + 2] + if Point.is_collinear(a, b, c): + nodup.pop(i + 1) + if a == c: + nodup.pop(i) + else: + i += 1 + + vertices = list(nodup) + + if len(vertices) > 3: + return GeometryEntity.__new__(cls, *vertices, **kwargs) + elif len(vertices) == 3: + return Triangle(*vertices, **kwargs) + elif len(vertices) == 2: + return Segment(*vertices, **kwargs) + else: + return Point(*vertices, **kwargs) + + @property + def area(self): + """ + The area of the polygon. + + Notes + ===== + + The area calculation can be positive or negative based on the + orientation of the points. If any side of the polygon crosses + any other side, there will be areas having opposite signs. + + See Also + ======== + + sympy.geometry.ellipse.Ellipse.area + + Examples + ======== + + >>> from sympy import Point, Polygon + >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)]) + >>> poly = Polygon(p1, p2, p3, p4) + >>> poly.area + 3 + + In the Z shaped polygon (with the lower right connecting back + to the upper left) the areas cancel out: + + >>> Z = Polygon((0, 1), (1, 1), (0, 0), (1, 0)) + >>> Z.area + 0 + + In the M shaped polygon, areas do not cancel because no side + crosses any other (though there is a point of contact). + + >>> M = Polygon((0, 0), (0, 1), (2, 0), (3, 1), (3, 0)) + >>> M.area + -3/2 + + """ + area = 0 + args = self.args + for i in range(len(args)): + x1, y1 = args[i - 1].args + x2, y2 = args[i].args + area += x1*y2 - x2*y1 + return simplify(area) / 2 + + @staticmethod + def _is_clockwise(a, b, c): + """Return True/False for cw/ccw orientation. + + Examples + ======== + + >>> from sympy import Point, Polygon + >>> a, b, c = [Point(i) for i in [(0, 0), (1, 1), (1, 0)]] + >>> Polygon._is_clockwise(a, b, c) + True + >>> Polygon._is_clockwise(a, c, b) + False + """ + ba = b - a + ca = c - a + t_area = simplify(ba.x*ca.y - ca.x*ba.y) + res = t_area.is_nonpositive + if res is None: + raise ValueError("Can't determine orientation") + return res + + @property + def angles(self): + """The internal angle at each vertex. + + Returns + ======= + + angles : dict + A dictionary where each key is a vertex and each value is the + internal angle at that vertex. The vertices are represented as + Points. + + See Also + ======== + + sympy.geometry.point.Point, sympy.geometry.line.LinearEntity.angle_between + + Examples + ======== + + >>> from sympy import Point, Polygon + >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)]) + >>> poly = Polygon(p1, p2, p3, p4) + >>> poly.angles[p1] + pi/2 + >>> poly.angles[p2] + acos(-4*sqrt(17)/17) + + """ + + args = self.vertices + n = len(args) + ret = {} + for i in range(n): + a, b, c = args[i - 2], args[i - 1], args[i] + reflex_ang = Ray(b, a).angle_between(Ray(b, c)) + if self._is_clockwise(a, b, c): + ret[b] = 2*S.Pi - reflex_ang + else: + ret[b] = reflex_ang + + # internal sum should be pi*(n - 2), not pi*(n+2) + # so if ratio is (n+2)/(n-2) > 1 it is wrong + wrong = ((sum(ret.values())/S.Pi-1)/(n - 2) - 1).is_positive + if wrong: + two_pi = 2*S.Pi + for b in ret: + ret[b] = two_pi - ret[b] + elif wrong is None: + raise ValueError("could not determine Polygon orientation.") + return ret + + @property + def ambient_dimension(self): + return self.vertices[0].ambient_dimension + + @property + def perimeter(self): + """The perimeter of the polygon. + + Returns + ======= + + perimeter : number or Basic instance + + See Also + ======== + + sympy.geometry.line.Segment.length + + Examples + ======== + + >>> from sympy import Point, Polygon + >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)]) + >>> poly = Polygon(p1, p2, p3, p4) + >>> poly.perimeter + sqrt(17) + 7 + """ + p = 0 + args = self.vertices + for i in range(len(args)): + p += args[i - 1].distance(args[i]) + return simplify(p) + + @property + def vertices(self): + """The vertices of the polygon. + + Returns + ======= + + vertices : list of Points + + Notes + ===== + + When iterating over the vertices, it is more efficient to index self + rather than to request the vertices and index them. Only use the + vertices when you want to process all of them at once. This is even + more important with RegularPolygons that calculate each vertex. + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Point, Polygon + >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)]) + >>> poly = Polygon(p1, p2, p3, p4) + >>> poly.vertices + [Point2D(0, 0), Point2D(1, 0), Point2D(5, 1), Point2D(0, 1)] + >>> poly.vertices[0] + Point2D(0, 0) + + """ + return list(self.args) + + @property + def centroid(self): + """The centroid of the polygon. + + Returns + ======= + + centroid : Point + + See Also + ======== + + sympy.geometry.point.Point, sympy.geometry.util.centroid + + Examples + ======== + + >>> from sympy import Point, Polygon + >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)]) + >>> poly = Polygon(p1, p2, p3, p4) + >>> poly.centroid + Point2D(31/18, 11/18) + + """ + A = 1/(6*self.area) + cx, cy = 0, 0 + args = self.args + for i in range(len(args)): + x1, y1 = args[i - 1].args + x2, y2 = args[i].args + v = x1*y2 - x2*y1 + cx += v*(x1 + x2) + cy += v*(y1 + y2) + return Point(simplify(A*cx), simplify(A*cy)) + + + def second_moment_of_area(self, point=None): + """Returns the second moment and product moment of area of a two dimensional polygon. + + Parameters + ========== + + point : Point, two-tuple of sympifyable objects, or None(default=None) + point is the point about which second moment of area is to be found. + If "point=None" it will be calculated about the axis passing through the + centroid of the polygon. + + Returns + ======= + + I_xx, I_yy, I_xy : number or SymPy expression + I_xx, I_yy are second moment of area of a two dimensional polygon. + I_xy is product moment of area of a two dimensional polygon. + + Examples + ======== + + >>> from sympy import Polygon, symbols + >>> a, b = symbols('a, b') + >>> p1, p2, p3, p4, p5 = [(0, 0), (a, 0), (a, b), (0, b), (a/3, b/3)] + >>> rectangle = Polygon(p1, p2, p3, p4) + >>> rectangle.second_moment_of_area() + (a*b**3/12, a**3*b/12, 0) + >>> rectangle.second_moment_of_area(p5) + (a*b**3/9, a**3*b/9, a**2*b**2/36) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Second_moment_of_area + + """ + + I_xx, I_yy, I_xy = 0, 0, 0 + args = self.vertices + for i in range(len(args)): + x1, y1 = args[i-1].args + x2, y2 = args[i].args + v = x1*y2 - x2*y1 + I_xx += (y1**2 + y1*y2 + y2**2)*v + I_yy += (x1**2 + x1*x2 + x2**2)*v + I_xy += (x1*y2 + 2*x1*y1 + 2*x2*y2 + x2*y1)*v + A = self.area + c_x = self.centroid[0] + c_y = self.centroid[1] + # parallel axis theorem + I_xx_c = (I_xx/12) - (A*(c_y**2)) + I_yy_c = (I_yy/12) - (A*(c_x**2)) + I_xy_c = (I_xy/24) - (A*(c_x*c_y)) + if point is None: + return I_xx_c, I_yy_c, I_xy_c + + I_xx = (I_xx_c + A*((point[1]-c_y)**2)) + I_yy = (I_yy_c + A*((point[0]-c_x)**2)) + I_xy = (I_xy_c + A*((point[0]-c_x)*(point[1]-c_y))) + + return I_xx, I_yy, I_xy + + + def first_moment_of_area(self, point=None): + """ + Returns the first moment of area of a two-dimensional polygon with + respect to a certain point of interest. + + First moment of area is a measure of the distribution of the area + of a polygon in relation to an axis. The first moment of area of + the entire polygon about its own centroid is always zero. Therefore, + here it is calculated for an area, above or below a certain point + of interest, that makes up a smaller portion of the polygon. This + area is bounded by the point of interest and the extreme end + (top or bottom) of the polygon. The first moment for this area is + is then determined about the centroidal axis of the initial polygon. + + References + ========== + + .. [1] https://skyciv.com/docs/tutorials/section-tutorials/calculating-the-statical-or-first-moment-of-area-of-beam-sections/?cc=BMD + .. [2] https://mechanicalc.com/reference/cross-sections + + Parameters + ========== + + point: Point, two-tuple of sympifyable objects, or None (default=None) + point is the point above or below which the area of interest lies + If ``point=None`` then the centroid acts as the point of interest. + + Returns + ======= + + Q_x, Q_y: number or SymPy expressions + Q_x is the first moment of area about the x-axis + Q_y is the first moment of area about the y-axis + A negative sign indicates that the section modulus is + determined for a section below (or left of) the centroidal axis + + Examples + ======== + + >>> from sympy import Point, Polygon + >>> a, b = 50, 10 + >>> p1, p2, p3, p4 = [(0, b), (0, 0), (a, 0), (a, b)] + >>> p = Polygon(p1, p2, p3, p4) + >>> p.first_moment_of_area() + (625, 3125) + >>> p.first_moment_of_area(point=Point(30, 7)) + (525, 3000) + """ + if point: + xc, yc = self.centroid + else: + point = self.centroid + xc, yc = point + + h_line = Line(point, slope=0) + v_line = Line(point, slope=S.Infinity) + + h_poly = self.cut_section(h_line) + v_poly = self.cut_section(v_line) + + poly_1 = h_poly[0] if h_poly[0].area <= h_poly[1].area else h_poly[1] + poly_2 = v_poly[0] if v_poly[0].area <= v_poly[1].area else v_poly[1] + + Q_x = (poly_1.centroid.y - yc)*poly_1.area + Q_y = (poly_2.centroid.x - xc)*poly_2.area + + return Q_x, Q_y + + + def polar_second_moment_of_area(self): + """Returns the polar modulus of a two-dimensional polygon + + It is a constituent of the second moment of area, linked through + the perpendicular axis theorem. While the planar second moment of + area describes an object's resistance to deflection (bending) when + subjected to a force applied to a plane parallel to the central + axis, the polar second moment of area describes an object's + resistance to deflection when subjected to a moment applied in a + plane perpendicular to the object's central axis (i.e. parallel to + the cross-section) + + Examples + ======== + + >>> from sympy import Polygon, symbols + >>> a, b = symbols('a, b') + >>> rectangle = Polygon((0, 0), (a, 0), (a, b), (0, b)) + >>> rectangle.polar_second_moment_of_area() + a**3*b/12 + a*b**3/12 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Polar_moment_of_inertia + + """ + second_moment = self.second_moment_of_area() + return second_moment[0] + second_moment[1] + + + def section_modulus(self, point=None): + """Returns a tuple with the section modulus of a two-dimensional + polygon. + + Section modulus is a geometric property of a polygon defined as the + ratio of second moment of area to the distance of the extreme end of + the polygon from the centroidal axis. + + Parameters + ========== + + point : Point, two-tuple of sympifyable objects, or None(default=None) + point is the point at which section modulus is to be found. + If "point=None" it will be calculated for the point farthest from the + centroidal axis of the polygon. + + Returns + ======= + + S_x, S_y: numbers or SymPy expressions + S_x is the section modulus with respect to the x-axis + S_y is the section modulus with respect to the y-axis + A negative sign indicates that the section modulus is + determined for a point below the centroidal axis + + Examples + ======== + + >>> from sympy import symbols, Polygon, Point + >>> a, b = symbols('a, b', positive=True) + >>> rectangle = Polygon((0, 0), (a, 0), (a, b), (0, b)) + >>> rectangle.section_modulus() + (a*b**2/6, a**2*b/6) + >>> rectangle.section_modulus(Point(a/4, b/4)) + (-a*b**2/3, -a**2*b/3) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Section_modulus + + """ + x_c, y_c = self.centroid + if point is None: + # taking x and y as maximum distances from centroid + x_min, y_min, x_max, y_max = self.bounds + y = max(y_c - y_min, y_max - y_c) + x = max(x_c - x_min, x_max - x_c) + else: + # taking x and y as distances of the given point from the centroid + y = point.y - y_c + x = point.x - x_c + + second_moment= self.second_moment_of_area() + S_x = second_moment[0]/y + S_y = second_moment[1]/x + + return S_x, S_y + + + @property + def sides(self): + """The directed line segments that form the sides of the polygon. + + Returns + ======= + + sides : list of sides + Each side is a directed Segment. + + See Also + ======== + + sympy.geometry.point.Point, sympy.geometry.line.Segment + + Examples + ======== + + >>> from sympy import Point, Polygon + >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)]) + >>> poly = Polygon(p1, p2, p3, p4) + >>> poly.sides + [Segment2D(Point2D(0, 0), Point2D(1, 0)), + Segment2D(Point2D(1, 0), Point2D(5, 1)), + Segment2D(Point2D(5, 1), Point2D(0, 1)), Segment2D(Point2D(0, 1), Point2D(0, 0))] + + """ + res = [] + args = self.vertices + for i in range(-len(args), 0): + res.append(Segment(args[i], args[i + 1])) + return res + + @property + def bounds(self): + """Return a tuple (xmin, ymin, xmax, ymax) representing the bounding + rectangle for the geometric figure. + + """ + + verts = self.vertices + xs = [p.x for p in verts] + ys = [p.y for p in verts] + return (min(xs), min(ys), max(xs), max(ys)) + + def is_convex(self): + """Is the polygon convex? + + A polygon is convex if all its interior angles are less than 180 + degrees and there are no intersections between sides. + + Returns + ======= + + is_convex : boolean + True if this polygon is convex, False otherwise. + + See Also + ======== + + sympy.geometry.util.convex_hull + + Examples + ======== + + >>> from sympy import Point, Polygon + >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)]) + >>> poly = Polygon(p1, p2, p3, p4) + >>> poly.is_convex() + True + + """ + # Determine orientation of points + args = self.vertices + cw = self._is_clockwise(args[-2], args[-1], args[0]) + for i in range(1, len(args)): + if cw ^ self._is_clockwise(args[i - 2], args[i - 1], args[i]): + return False + # check for intersecting sides + sides = self.sides + for i, si in enumerate(sides): + pts = si.args + # exclude the sides connected to si + for j in range(1 if i == len(sides) - 1 else 0, i - 1): + sj = sides[j] + if sj.p1 not in pts and sj.p2 not in pts: + hit = si.intersection(sj) + if hit: + return False + return True + + def encloses_point(self, p): + """ + Return True if p is enclosed by (is inside of) self. + + Notes + ===== + + Being on the border of self is considered False. + + Parameters + ========== + + p : Point + + Returns + ======= + + encloses_point : True, False or None + + See Also + ======== + + sympy.geometry.point.Point, sympy.geometry.ellipse.Ellipse.encloses_point + + Examples + ======== + + >>> from sympy import Polygon, Point + >>> p = Polygon((0, 0), (4, 0), (4, 4)) + >>> p.encloses_point(Point(2, 1)) + True + >>> p.encloses_point(Point(2, 2)) + False + >>> p.encloses_point(Point(5, 5)) + False + + References + ========== + + .. [1] https://paulbourke.net/geometry/polygonmesh/#insidepoly + + """ + p = Point(p, dim=2) + if p in self.vertices or any(p in s for s in self.sides): + return False + + # move to p, checking that the result is numeric + lit = [] + for v in self.vertices: + lit.append(v - p) # the difference is simplified + if lit[-1].free_symbols: + return None + + poly = Polygon(*lit) + + # polygon closure is assumed in the following test but Polygon removes duplicate pts so + # the last point has to be added so all sides are computed. Using Polygon.sides is + # not good since Segments are unordered. + args = poly.args + indices = list(range(-len(args), 1)) + + if poly.is_convex(): + orientation = None + for i in indices: + a = args[i] + b = args[i + 1] + test = ((-a.y)*(b.x - a.x) - (-a.x)*(b.y - a.y)).is_negative + if orientation is None: + orientation = test + elif test is not orientation: + return False + return True + + hit_odd = False + p1x, p1y = args[0].args + for i in indices[1:]: + p2x, p2y = args[i].args + if 0 > min(p1y, p2y): + if 0 <= max(p1y, p2y): + if 0 <= max(p1x, p2x): + if p1y != p2y: + xinters = (-p1y)*(p2x - p1x)/(p2y - p1y) + p1x + if p1x == p2x or 0 <= xinters: + hit_odd = not hit_odd + p1x, p1y = p2x, p2y + return hit_odd + + def arbitrary_point(self, parameter='t'): + """A parameterized point on the polygon. + + The parameter, varying from 0 to 1, assigns points to the position on + the perimeter that is that fraction of the total perimeter. So the + point evaluated at t=1/2 would return the point from the first vertex + that is 1/2 way around the polygon. + + Parameters + ========== + + parameter : str, optional + Default value is 't'. + + Returns + ======= + + arbitrary_point : Point + + Raises + ====== + + ValueError + When `parameter` already appears in the Polygon's definition. + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Polygon, Symbol + >>> t = Symbol('t', real=True) + >>> tri = Polygon((0, 0), (1, 0), (1, 1)) + >>> p = tri.arbitrary_point('t') + >>> perimeter = tri.perimeter + >>> s1, s2 = [s.length for s in tri.sides[:2]] + >>> p.subs(t, (s1 + s2/2)/perimeter) + Point2D(1, 1/2) + + """ + t = _symbol(parameter, real=True) + if t.name in (f.name for f in self.free_symbols): + raise ValueError('Symbol %s already appears in object and cannot be used as a parameter.' % t.name) + sides = [] + perimeter = self.perimeter + perim_fraction_start = 0 + for s in self.sides: + side_perim_fraction = s.length/perimeter + perim_fraction_end = perim_fraction_start + side_perim_fraction + pt = s.arbitrary_point(parameter).subs( + t, (t - perim_fraction_start)/side_perim_fraction) + sides.append( + (pt, (And(perim_fraction_start <= t, t < perim_fraction_end)))) + perim_fraction_start = perim_fraction_end + return Piecewise(*sides) + + def parameter_value(self, other, t): + if not isinstance(other,GeometryEntity): + other = Point(other, dim=self.ambient_dimension) + if not isinstance(other,Point): + raise ValueError("other must be a point") + if other.free_symbols: + raise NotImplementedError('non-numeric coordinates') + unknown = False + p = self.arbitrary_point(T) + for pt, cond in p.args: + sol = solve(pt - other, T, dict=True) + if not sol: + continue + value = sol[0][T] + if simplify(cond.subs(T, value)) == True: + return {t: value} + unknown = True + if unknown: + raise ValueError("Given point may not be on %s" % func_name(self)) + raise ValueError("Given point is not on %s" % func_name(self)) + + def plot_interval(self, parameter='t'): + """The plot interval for the default geometric plot of the polygon. + + Parameters + ========== + + parameter : str, optional + Default value is 't'. + + Returns + ======= + + plot_interval : list (plot interval) + [parameter, lower_bound, upper_bound] + + Examples + ======== + + >>> from sympy import Polygon + >>> p = Polygon((0, 0), (1, 0), (1, 1)) + >>> p.plot_interval() + [t, 0, 1] + + """ + t = Symbol(parameter, real=True) + return [t, 0, 1] + + def intersection(self, o): + """The intersection of polygon and geometry entity. + + The intersection may be empty and can contain individual Points and + complete Line Segments. + + Parameters + ========== + + other: GeometryEntity + + Returns + ======= + + intersection : list + The list of Segments and Points + + See Also + ======== + + sympy.geometry.point.Point, sympy.geometry.line.Segment + + Examples + ======== + + >>> from sympy import Point, Polygon, Line + >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)]) + >>> poly1 = Polygon(p1, p2, p3, p4) + >>> p5, p6, p7 = map(Point, [(3, 2), (1, -1), (0, 2)]) + >>> poly2 = Polygon(p5, p6, p7) + >>> poly1.intersection(poly2) + [Point2D(1/3, 1), Point2D(2/3, 0), Point2D(9/5, 1/5), Point2D(7/3, 1)] + >>> poly1.intersection(Line(p1, p2)) + [Segment2D(Point2D(0, 0), Point2D(1, 0))] + >>> poly1.intersection(p1) + [Point2D(0, 0)] + """ + intersection_result = [] + k = o.sides if isinstance(o, Polygon) else [o] + for side in self.sides: + for side1 in k: + intersection_result.extend(side.intersection(side1)) + + intersection_result = list(uniq(intersection_result)) + points = [entity for entity in intersection_result if isinstance(entity, Point)] + segments = [entity for entity in intersection_result if isinstance(entity, Segment)] + + if points and segments: + points_in_segments = list(uniq([point for point in points for segment in segments if point in segment])) + if points_in_segments: + for i in points_in_segments: + points.remove(i) + return list(ordered(segments + points)) + else: + return list(ordered(intersection_result)) + + + def cut_section(self, line): + """ + Returns a tuple of two polygon segments that lie above and below + the intersecting line respectively. + + Parameters + ========== + + line: Line object of geometry module + line which cuts the Polygon. The part of the Polygon that lies + above and below this line is returned. + + Returns + ======= + + upper_polygon, lower_polygon: Polygon objects or None + upper_polygon is the polygon that lies above the given line. + lower_polygon is the polygon that lies below the given line. + upper_polygon and lower polygon are ``None`` when no polygon + exists above the line or below the line. + + Raises + ====== + + ValueError: When the line does not intersect the polygon + + Examples + ======== + + >>> from sympy import Polygon, Line + >>> a, b = 20, 10 + >>> p1, p2, p3, p4 = [(0, b), (0, 0), (a, 0), (a, b)] + >>> rectangle = Polygon(p1, p2, p3, p4) + >>> t = rectangle.cut_section(Line((0, 5), slope=0)) + >>> t + (Polygon(Point2D(0, 10), Point2D(0, 5), Point2D(20, 5), Point2D(20, 10)), + Polygon(Point2D(0, 5), Point2D(0, 0), Point2D(20, 0), Point2D(20, 5))) + >>> upper_segment, lower_segment = t + >>> upper_segment.area + 100 + >>> upper_segment.centroid + Point2D(10, 15/2) + >>> lower_segment.centroid + Point2D(10, 5/2) + + References + ========== + + .. [1] https://github.com/sympy/sympy/wiki/A-method-to-return-a-cut-section-of-any-polygon-geometry + + """ + intersection_points = self.intersection(line) + if not intersection_points: + raise ValueError("This line does not intersect the polygon") + + points = list(self.vertices) + points.append(points[0]) + + eq = line.equation(x, y) + + # considering equation of line to be `ax +by + c` + a = eq.coeff(x) + b = eq.coeff(y) + + upper_vertices = [] + lower_vertices = [] + # prev is true when previous point is above the line + prev = True + prev_point = None + for point in points: + # when coefficient of y is 0, right side of the line is + # considered + compare = eq.subs({x: point.x, y: point.y})/b if b \ + else eq.subs(x, point.x)/a + + # if point lies above line + if compare > 0: + if not prev: + # if previous point lies below the line, the intersection + # point of the polygon edge and the line has to be included + edge = Line(point, prev_point) + new_point = edge.intersection(line) + upper_vertices.append(new_point[0]) + lower_vertices.append(new_point[0]) + + upper_vertices.append(point) + prev = True + else: + if prev and prev_point: + edge = Line(point, prev_point) + new_point = edge.intersection(line) + upper_vertices.append(new_point[0]) + lower_vertices.append(new_point[0]) + lower_vertices.append(point) + prev = False + prev_point = point + + upper_polygon, lower_polygon = None, None + if upper_vertices and isinstance(Polygon(*upper_vertices), Polygon): + upper_polygon = Polygon(*upper_vertices) + if lower_vertices and isinstance(Polygon(*lower_vertices), Polygon): + lower_polygon = Polygon(*lower_vertices) + + return upper_polygon, lower_polygon + + + def distance(self, o): + """ + Returns the shortest distance between self and o. + + If o is a point, then self does not need to be convex. + If o is another polygon self and o must be convex. + + Examples + ======== + + >>> from sympy import Point, Polygon, RegularPolygon + >>> p1, p2 = map(Point, [(0, 0), (7, 5)]) + >>> poly = Polygon(*RegularPolygon(p1, 1, 3).vertices) + >>> poly.distance(p2) + sqrt(61) + """ + if isinstance(o, Point): + dist = oo + for side in self.sides: + current = side.distance(o) + if current == 0: + return S.Zero + elif current < dist: + dist = current + return dist + elif isinstance(o, Polygon) and self.is_convex() and o.is_convex(): + return self._do_poly_distance(o) + raise NotImplementedError() + + def _do_poly_distance(self, e2): + """ + Calculates the least distance between the exteriors of two + convex polygons e1 and e2. Does not check for the convexity + of the polygons as this is checked by Polygon.distance. + + Notes + ===== + + - Prints a warning if the two polygons possibly intersect as the return + value will not be valid in such a case. For a more through test of + intersection use intersection(). + + See Also + ======== + + sympy.geometry.point.Point.distance + + Examples + ======== + + >>> from sympy import Point, Polygon + >>> square = Polygon(Point(0, 0), Point(0, 1), Point(1, 1), Point(1, 0)) + >>> triangle = Polygon(Point(1, 2), Point(2, 2), Point(2, 1)) + >>> square._do_poly_distance(triangle) + sqrt(2)/2 + + Description of method used + ========================== + + Method: + [1] https://web.archive.org/web/20150509035744/http://cgm.cs.mcgill.ca/~orm/mind2p.html + Uses rotating calipers: + [2] https://en.wikipedia.org/wiki/Rotating_calipers + and antipodal points: + [3] https://en.wikipedia.org/wiki/Antipodal_point + """ + e1 = self + + '''Tests for a possible intersection between the polygons and outputs a warning''' + e1_center = e1.centroid + e2_center = e2.centroid + e1_max_radius = S.Zero + e2_max_radius = S.Zero + for vertex in e1.vertices: + r = Point.distance(e1_center, vertex) + if e1_max_radius < r: + e1_max_radius = r + for vertex in e2.vertices: + r = Point.distance(e2_center, vertex) + if e2_max_radius < r: + e2_max_radius = r + center_dist = Point.distance(e1_center, e2_center) + if center_dist <= e1_max_radius + e2_max_radius: + warnings.warn("Polygons may intersect producing erroneous output", + stacklevel=3) + + ''' + Find the upper rightmost vertex of e1 and the lowest leftmost vertex of e2 + ''' + e1_ymax = Point(0, -oo) + e2_ymin = Point(0, oo) + + for vertex in e1.vertices: + if vertex.y > e1_ymax.y or (vertex.y == e1_ymax.y and vertex.x > e1_ymax.x): + e1_ymax = vertex + for vertex in e2.vertices: + if vertex.y < e2_ymin.y or (vertex.y == e2_ymin.y and vertex.x < e2_ymin.x): + e2_ymin = vertex + min_dist = Point.distance(e1_ymax, e2_ymin) + + ''' + Produce a dictionary with vertices of e1 as the keys and, for each vertex, the points + to which the vertex is connected as its value. The same is then done for e2. + ''' + e1_connections = {} + e2_connections = {} + + for side in e1.sides: + if side.p1 in e1_connections: + e1_connections[side.p1].append(side.p2) + else: + e1_connections[side.p1] = [side.p2] + + if side.p2 in e1_connections: + e1_connections[side.p2].append(side.p1) + else: + e1_connections[side.p2] = [side.p1] + + for side in e2.sides: + if side.p1 in e2_connections: + e2_connections[side.p1].append(side.p2) + else: + e2_connections[side.p1] = [side.p2] + + if side.p2 in e2_connections: + e2_connections[side.p2].append(side.p1) + else: + e2_connections[side.p2] = [side.p1] + + e1_current = e1_ymax + e2_current = e2_ymin + support_line = Line(Point(S.Zero, S.Zero), Point(S.One, S.Zero)) + + ''' + Determine which point in e1 and e2 will be selected after e2_ymin and e1_ymax, + this information combined with the above produced dictionaries determines the + path that will be taken around the polygons + ''' + point1 = e1_connections[e1_ymax][0] + point2 = e1_connections[e1_ymax][1] + angle1 = support_line.angle_between(Line(e1_ymax, point1)) + angle2 = support_line.angle_between(Line(e1_ymax, point2)) + if angle1 < angle2: + e1_next = point1 + elif angle2 < angle1: + e1_next = point2 + elif Point.distance(e1_ymax, point1) > Point.distance(e1_ymax, point2): + e1_next = point2 + else: + e1_next = point1 + + point1 = e2_connections[e2_ymin][0] + point2 = e2_connections[e2_ymin][1] + angle1 = support_line.angle_between(Line(e2_ymin, point1)) + angle2 = support_line.angle_between(Line(e2_ymin, point2)) + if angle1 > angle2: + e2_next = point1 + elif angle2 > angle1: + e2_next = point2 + elif Point.distance(e2_ymin, point1) > Point.distance(e2_ymin, point2): + e2_next = point2 + else: + e2_next = point1 + + ''' + Loop which determines the distance between anti-podal pairs and updates the + minimum distance accordingly. It repeats until it reaches the starting position. + ''' + while True: + e1_angle = support_line.angle_between(Line(e1_current, e1_next)) + e2_angle = pi - support_line.angle_between(Line( + e2_current, e2_next)) + + if (e1_angle < e2_angle) is True: + support_line = Line(e1_current, e1_next) + e1_segment = Segment(e1_current, e1_next) + min_dist_current = e1_segment.distance(e2_current) + + if min_dist_current.evalf() < min_dist.evalf(): + min_dist = min_dist_current + + if e1_connections[e1_next][0] != e1_current: + e1_current = e1_next + e1_next = e1_connections[e1_next][0] + else: + e1_current = e1_next + e1_next = e1_connections[e1_next][1] + elif (e1_angle > e2_angle) is True: + support_line = Line(e2_next, e2_current) + e2_segment = Segment(e2_current, e2_next) + min_dist_current = e2_segment.distance(e1_current) + + if min_dist_current.evalf() < min_dist.evalf(): + min_dist = min_dist_current + + if e2_connections[e2_next][0] != e2_current: + e2_current = e2_next + e2_next = e2_connections[e2_next][0] + else: + e2_current = e2_next + e2_next = e2_connections[e2_next][1] + else: + support_line = Line(e1_current, e1_next) + e1_segment = Segment(e1_current, e1_next) + e2_segment = Segment(e2_current, e2_next) + min1 = e1_segment.distance(e2_next) + min2 = e2_segment.distance(e1_next) + + min_dist_current = min(min1, min2) + if min_dist_current.evalf() < min_dist.evalf(): + min_dist = min_dist_current + + if e1_connections[e1_next][0] != e1_current: + e1_current = e1_next + e1_next = e1_connections[e1_next][0] + else: + e1_current = e1_next + e1_next = e1_connections[e1_next][1] + + if e2_connections[e2_next][0] != e2_current: + e2_current = e2_next + e2_next = e2_connections[e2_next][0] + else: + e2_current = e2_next + e2_next = e2_connections[e2_next][1] + if e1_current == e1_ymax and e2_current == e2_ymin: + break + return min_dist + + def _svg(self, scale_factor=1., fill_color="#66cc99"): + """Returns SVG path element for the Polygon. + + Parameters + ========== + + scale_factor : float + Multiplication factor for the SVG stroke-width. Default is 1. + fill_color : str, optional + Hex string for fill color. Default is "#66cc99". + """ + verts = map(N, self.vertices) + coords = ["{},{}".format(p.x, p.y) for p in verts] + path = "M {} L {} z".format(coords[0], " L ".join(coords[1:])) + return ( + '' + ).format(2. * scale_factor, path, fill_color) + + def _hashable_content(self): + + D = {} + def ref_list(point_list): + kee = {} + for i, p in enumerate(ordered(set(point_list))): + kee[p] = i + D[i] = p + return [kee[p] for p in point_list] + + S1 = ref_list(self.args) + r_nor = rotate_left(S1, least_rotation(S1)) + S2 = ref_list(list(reversed(self.args))) + r_rev = rotate_left(S2, least_rotation(S2)) + if r_nor < r_rev: + r = r_nor + else: + r = r_rev + canonical_args = [ D[order] for order in r ] + return tuple(canonical_args) + + def __contains__(self, o): + """ + Return True if o is contained within the boundary lines of self.altitudes + + Parameters + ========== + + other : GeometryEntity + + Returns + ======= + + contained in : bool + The points (and sides, if applicable) are contained in self. + + See Also + ======== + + sympy.geometry.entity.GeometryEntity.encloses + + Examples + ======== + + >>> from sympy import Line, Segment, Point + >>> p = Point(0, 0) + >>> q = Point(1, 1) + >>> s = Segment(p, q*2) + >>> l = Line(p, q) + >>> p in q + False + >>> p in s + True + >>> q*3 in s + False + >>> s in l + True + + """ + + if isinstance(o, Polygon): + return self == o + elif isinstance(o, Segment): + return any(o in s for s in self.sides) + elif isinstance(o, Point): + if o in self.vertices: + return True + for side in self.sides: + if o in side: + return True + + return False + + def bisectors(p, prec=None): + """Returns angle bisectors of a polygon. If prec is given + then approximate the point defining the ray to that precision. + + The distance between the points defining the bisector ray is 1. + + Examples + ======== + + >>> from sympy import Polygon, Point + >>> p = Polygon(Point(0, 0), Point(2, 0), Point(1, 1), Point(0, 3)) + >>> p.bisectors(2) + {Point2D(0, 0): Ray2D(Point2D(0, 0), Point2D(0.71, 0.71)), + Point2D(0, 3): Ray2D(Point2D(0, 3), Point2D(0.23, 2.0)), + Point2D(1, 1): Ray2D(Point2D(1, 1), Point2D(0.19, 0.42)), + Point2D(2, 0): Ray2D(Point2D(2, 0), Point2D(1.1, 0.38))} + """ + b = {} + pts = list(p.args) + pts.append(pts[0]) # close it + cw = Polygon._is_clockwise(*pts[:3]) + if cw: + pts = list(reversed(pts)) + for v, a in p.angles.items(): + i = pts.index(v) + p1, p2 = Point._normalize_dimension(pts[i], pts[i + 1]) + ray = Ray(p1, p2).rotate(a/2, v) + dir = ray.direction + ray = Ray(ray.p1, ray.p1 + dir/dir.distance((0, 0))) + if prec is not None: + ray = Ray(ray.p1, ray.p2.n(prec)) + b[v] = ray + return b + + +class RegularPolygon(Polygon): + """ + A regular polygon. + + Such a polygon has all internal angles equal and all sides the same length. + + Parameters + ========== + + center : Point + radius : number or Basic instance + The distance from the center to a vertex + n : int + The number of sides + + Attributes + ========== + + vertices + center + radius + rotation + apothem + interior_angle + exterior_angle + circumcircle + incircle + angles + + Raises + ====== + + GeometryError + If the `center` is not a Point, or the `radius` is not a number or Basic + instance, or the number of sides, `n`, is less than three. + + Notes + ===== + + A RegularPolygon can be instantiated with Polygon with the kwarg n. + + Regular polygons are instantiated with a center, radius, number of sides + and a rotation angle. Whereas the arguments of a Polygon are vertices, the + vertices of the RegularPolygon must be obtained with the vertices method. + + See Also + ======== + + sympy.geometry.point.Point, Polygon + + Examples + ======== + + >>> from sympy import RegularPolygon, Point + >>> r = RegularPolygon(Point(0, 0), 5, 3) + >>> r + RegularPolygon(Point2D(0, 0), 5, 3, 0) + >>> r.vertices[0] + Point2D(5, 0) + + """ + + __slots__ = ('_n', '_center', '_radius', '_rot') + + def __new__(self, c, r, n, rot=0, **kwargs): + r, n, rot = map(sympify, (r, n, rot)) + c = Point(c, dim=2, **kwargs) + if not isinstance(r, Expr): + raise GeometryError("r must be an Expr object, not %s" % r) + if n.is_Number: + as_int(n) # let an error raise if necessary + if n < 3: + raise GeometryError("n must be a >= 3, not %s" % n) + + obj = GeometryEntity.__new__(self, c, r, n, **kwargs) + obj._n = n + obj._center = c + obj._radius = r + obj._rot = rot % (2*S.Pi/n) if rot.is_number else rot + return obj + + def _eval_evalf(self, prec=15, **options): + c, r, n, a = self.args + dps = prec_to_dps(prec) + c, r, a = [i.evalf(n=dps, **options) for i in (c, r, a)] + return self.func(c, r, n, a) + + @property + def args(self): + """ + Returns the center point, the radius, + the number of sides, and the orientation angle. + + Examples + ======== + + >>> from sympy import RegularPolygon, Point + >>> r = RegularPolygon(Point(0, 0), 5, 3) + >>> r.args + (Point2D(0, 0), 5, 3, 0) + """ + return self._center, self._radius, self._n, self._rot + + def __str__(self): + return 'RegularPolygon(%s, %s, %s, %s)' % tuple(self.args) + + def __repr__(self): + return 'RegularPolygon(%s, %s, %s, %s)' % tuple(self.args) + + @property + def area(self): + """Returns the area. + + Examples + ======== + + >>> from sympy import RegularPolygon + >>> square = RegularPolygon((0, 0), 1, 4) + >>> square.area + 2 + >>> _ == square.length**2 + True + """ + c, r, n, rot = self.args + return sign(r)*n*self.length**2/(4*tan(pi/n)) + + @property + def length(self): + """Returns the length of the sides. + + The half-length of the side and the apothem form two legs + of a right triangle whose hypotenuse is the radius of the + regular polygon. + + Examples + ======== + + >>> from sympy import RegularPolygon + >>> from sympy import sqrt + >>> s = square_in_unit_circle = RegularPolygon((0, 0), 1, 4) + >>> s.length + sqrt(2) + >>> sqrt((_/2)**2 + s.apothem**2) == s.radius + True + + """ + return self.radius*2*sin(pi/self._n) + + @property + def center(self): + """The center of the RegularPolygon + + This is also the center of the circumscribing circle. + + Returns + ======= + + center : Point + + See Also + ======== + + sympy.geometry.point.Point, sympy.geometry.ellipse.Ellipse.center + + Examples + ======== + + >>> from sympy import RegularPolygon, Point + >>> rp = RegularPolygon(Point(0, 0), 5, 4) + >>> rp.center + Point2D(0, 0) + """ + return self._center + + centroid = center + + @property + def circumcenter(self): + """ + Alias for center. + + Examples + ======== + + >>> from sympy import RegularPolygon, Point + >>> rp = RegularPolygon(Point(0, 0), 5, 4) + >>> rp.circumcenter + Point2D(0, 0) + """ + return self.center + + @property + def radius(self): + """Radius of the RegularPolygon + + This is also the radius of the circumscribing circle. + + Returns + ======= + + radius : number or instance of Basic + + See Also + ======== + + sympy.geometry.line.Segment.length, sympy.geometry.ellipse.Circle.radius + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy import RegularPolygon, Point + >>> radius = Symbol('r') + >>> rp = RegularPolygon(Point(0, 0), radius, 4) + >>> rp.radius + r + + """ + return self._radius + + @property + def circumradius(self): + """ + Alias for radius. + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy import RegularPolygon, Point + >>> radius = Symbol('r') + >>> rp = RegularPolygon(Point(0, 0), radius, 4) + >>> rp.circumradius + r + """ + return self.radius + + @property + def rotation(self): + """CCW angle by which the RegularPolygon is rotated + + Returns + ======= + + rotation : number or instance of Basic + + Examples + ======== + + >>> from sympy import pi + >>> from sympy.abc import a + >>> from sympy import RegularPolygon, Point + >>> RegularPolygon(Point(0, 0), 3, 4, pi/4).rotation + pi/4 + + Numerical rotation angles are made canonical: + + >>> RegularPolygon(Point(0, 0), 3, 4, a).rotation + a + >>> RegularPolygon(Point(0, 0), 3, 4, pi).rotation + 0 + + """ + return self._rot + + @property + def apothem(self): + """The inradius of the RegularPolygon. + + The apothem/inradius is the radius of the inscribed circle. + + Returns + ======= + + apothem : number or instance of Basic + + See Also + ======== + + sympy.geometry.line.Segment.length, sympy.geometry.ellipse.Circle.radius + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy import RegularPolygon, Point + >>> radius = Symbol('r') + >>> rp = RegularPolygon(Point(0, 0), radius, 4) + >>> rp.apothem + sqrt(2)*r/2 + + """ + return self.radius * cos(S.Pi/self._n) + + @property + def inradius(self): + """ + Alias for apothem. + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy import RegularPolygon, Point + >>> radius = Symbol('r') + >>> rp = RegularPolygon(Point(0, 0), radius, 4) + >>> rp.inradius + sqrt(2)*r/2 + """ + return self.apothem + + @property + def interior_angle(self): + """Measure of the interior angles. + + Returns + ======= + + interior_angle : number + + See Also + ======== + + sympy.geometry.line.LinearEntity.angle_between + + Examples + ======== + + >>> from sympy import RegularPolygon, Point + >>> rp = RegularPolygon(Point(0, 0), 4, 8) + >>> rp.interior_angle + 3*pi/4 + + """ + return (self._n - 2)*S.Pi/self._n + + @property + def exterior_angle(self): + """Measure of the exterior angles. + + Returns + ======= + + exterior_angle : number + + See Also + ======== + + sympy.geometry.line.LinearEntity.angle_between + + Examples + ======== + + >>> from sympy import RegularPolygon, Point + >>> rp = RegularPolygon(Point(0, 0), 4, 8) + >>> rp.exterior_angle + pi/4 + + """ + return 2*S.Pi/self._n + + @property + def circumcircle(self): + """The circumcircle of the RegularPolygon. + + Returns + ======= + + circumcircle : Circle + + See Also + ======== + + circumcenter, sympy.geometry.ellipse.Circle + + Examples + ======== + + >>> from sympy import RegularPolygon, Point + >>> rp = RegularPolygon(Point(0, 0), 4, 8) + >>> rp.circumcircle + Circle(Point2D(0, 0), 4) + + """ + return Circle(self.center, self.radius) + + @property + def incircle(self): + """The incircle of the RegularPolygon. + + Returns + ======= + + incircle : Circle + + See Also + ======== + + inradius, sympy.geometry.ellipse.Circle + + Examples + ======== + + >>> from sympy import RegularPolygon, Point + >>> rp = RegularPolygon(Point(0, 0), 4, 7) + >>> rp.incircle + Circle(Point2D(0, 0), 4*cos(pi/7)) + + """ + return Circle(self.center, self.apothem) + + @property + def angles(self): + """ + Returns a dictionary with keys, the vertices of the Polygon, + and values, the interior angle at each vertex. + + Examples + ======== + + >>> from sympy import RegularPolygon, Point + >>> r = RegularPolygon(Point(0, 0), 5, 3) + >>> r.angles + {Point2D(-5/2, -5*sqrt(3)/2): pi/3, + Point2D(-5/2, 5*sqrt(3)/2): pi/3, + Point2D(5, 0): pi/3} + """ + ret = {} + ang = self.interior_angle + for v in self.vertices: + ret[v] = ang + return ret + + def encloses_point(self, p): + """ + Return True if p is enclosed by (is inside of) self. + + Notes + ===== + + Being on the border of self is considered False. + + The general Polygon.encloses_point method is called only if + a point is not within or beyond the incircle or circumcircle, + respectively. + + Parameters + ========== + + p : Point + + Returns + ======= + + encloses_point : True, False or None + + See Also + ======== + + sympy.geometry.ellipse.Ellipse.encloses_point + + Examples + ======== + + >>> from sympy import RegularPolygon, S, Point, Symbol + >>> p = RegularPolygon((0, 0), 3, 4) + >>> p.encloses_point(Point(0, 0)) + True + >>> r, R = p.inradius, p.circumradius + >>> p.encloses_point(Point((r + R)/2, 0)) + True + >>> p.encloses_point(Point(R/2, R/2 + (R - r)/10)) + False + >>> t = Symbol('t', real=True) + >>> p.encloses_point(p.arbitrary_point().subs(t, S.Half)) + False + >>> p.encloses_point(Point(5, 5)) + False + + """ + + c = self.center + d = Segment(c, p).length + if d >= self.radius: + return False + elif d < self.inradius: + return True + else: + # now enumerate the RegularPolygon like a general polygon. + return Polygon.encloses_point(self, p) + + def spin(self, angle): + """Increment *in place* the virtual Polygon's rotation by ccw angle. + + See also: rotate method which moves the center. + + >>> from sympy import Polygon, Point, pi + >>> r = Polygon(Point(0,0), 1, n=3) + >>> r.vertices[0] + Point2D(1, 0) + >>> r.spin(pi/6) + >>> r.vertices[0] + Point2D(sqrt(3)/2, 1/2) + + See Also + ======== + + rotation + rotate : Creates a copy of the RegularPolygon rotated about a Point + + """ + self._rot += angle + + def rotate(self, angle, pt=None): + """Override GeometryEntity.rotate to first rotate the RegularPolygon + about its center. + + >>> from sympy import Point, RegularPolygon, pi + >>> t = RegularPolygon(Point(1, 0), 1, 3) + >>> t.vertices[0] # vertex on x-axis + Point2D(2, 0) + >>> t.rotate(pi/2).vertices[0] # vertex on y axis now + Point2D(0, 2) + + See Also + ======== + + rotation + spin : Rotates a RegularPolygon in place + + """ + + r = type(self)(*self.args) # need a copy or else changes are in-place + r._rot += angle + return GeometryEntity.rotate(r, angle, pt) + + def scale(self, x=1, y=1, pt=None): + """Override GeometryEntity.scale since it is the radius that must be + scaled (if x == y) or else a new Polygon must be returned. + + >>> from sympy import RegularPolygon + + Symmetric scaling returns a RegularPolygon: + + >>> RegularPolygon((0, 0), 1, 4).scale(2, 2) + RegularPolygon(Point2D(0, 0), 2, 4, 0) + + Asymmetric scaling returns a kite as a Polygon: + + >>> RegularPolygon((0, 0), 1, 4).scale(2, 1) + Polygon(Point2D(2, 0), Point2D(0, 1), Point2D(-2, 0), Point2D(0, -1)) + + """ + if pt: + pt = Point(pt, dim=2) + return self.translate(*(-pt).args).scale(x, y).translate(*pt.args) + if x != y: + return Polygon(*self.vertices).scale(x, y) + c, r, n, rot = self.args + r *= x + return self.func(c, r, n, rot) + + def reflect(self, line): + """Override GeometryEntity.reflect since this is not made of only + points. + + Examples + ======== + + >>> from sympy import RegularPolygon, Line + + >>> RegularPolygon((0, 0), 1, 4).reflect(Line((0, 1), slope=-2)) + RegularPolygon(Point2D(4/5, 2/5), -1, 4, atan(4/3)) + + """ + c, r, n, rot = self.args + v = self.vertices[0] + d = v - c + cc = c.reflect(line) + vv = v.reflect(line) + dd = vv - cc + # calculate rotation about the new center + # which will align the vertices + l1 = Ray((0, 0), dd) + l2 = Ray((0, 0), d) + ang = l1.closing_angle(l2) + rot += ang + # change sign of radius as point traversal is reversed + return self.func(cc, -r, n, rot) + + @property + def vertices(self): + """The vertices of the RegularPolygon. + + Returns + ======= + + vertices : list + Each vertex is a Point. + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import RegularPolygon, Point + >>> rp = RegularPolygon(Point(0, 0), 5, 4) + >>> rp.vertices + [Point2D(5, 0), Point2D(0, 5), Point2D(-5, 0), Point2D(0, -5)] + + """ + c = self._center + r = abs(self._radius) + rot = self._rot + v = 2*S.Pi/self._n + + return [Point(c.x + r*cos(k*v + rot), c.y + r*sin(k*v + rot)) + for k in range(self._n)] + + def __eq__(self, o): + if not isinstance(o, Polygon): + return False + elif not isinstance(o, RegularPolygon): + return Polygon.__eq__(o, self) + return self.args == o.args + + def __hash__(self): + return super().__hash__() + + +class Triangle(Polygon): + """ + A polygon with three vertices and three sides. + + Parameters + ========== + + points : sequence of Points + keyword: asa, sas, or sss to specify sides/angles of the triangle + + Attributes + ========== + + vertices + altitudes + orthocenter + circumcenter + circumradius + circumcircle + inradius + incircle + exradii + medians + medial + nine_point_circle + + Raises + ====== + + GeometryError + If the number of vertices is not equal to three, or one of the vertices + is not a Point, or a valid keyword is not given. + + See Also + ======== + + sympy.geometry.point.Point, Polygon + + Examples + ======== + + >>> from sympy import Triangle, Point + >>> Triangle(Point(0, 0), Point(4, 0), Point(4, 3)) + Triangle(Point2D(0, 0), Point2D(4, 0), Point2D(4, 3)) + + Keywords sss, sas, or asa can be used to give the desired + side lengths (in order) and interior angles (in degrees) that + define the triangle: + + >>> Triangle(sss=(3, 4, 5)) + Triangle(Point2D(0, 0), Point2D(3, 0), Point2D(3, 4)) + >>> Triangle(asa=(30, 1, 30)) + Triangle(Point2D(0, 0), Point2D(1, 0), Point2D(1/2, sqrt(3)/6)) + >>> Triangle(sas=(1, 45, 2)) + Triangle(Point2D(0, 0), Point2D(2, 0), Point2D(sqrt(2)/2, sqrt(2)/2)) + + """ + + def __new__(cls, *args, **kwargs): + if len(args) != 3: + if 'sss' in kwargs: + return _sss(*[simplify(a) for a in kwargs['sss']]) + if 'asa' in kwargs: + return _asa(*[simplify(a) for a in kwargs['asa']]) + if 'sas' in kwargs: + return _sas(*[simplify(a) for a in kwargs['sas']]) + msg = "Triangle instantiates with three points or a valid keyword." + raise GeometryError(msg) + + vertices = [Point(a, dim=2, **kwargs) for a in args] + + # remove consecutive duplicates + nodup = [] + for p in vertices: + if nodup and p == nodup[-1]: + continue + nodup.append(p) + if len(nodup) > 1 and nodup[-1] == nodup[0]: + nodup.pop() # last point was same as first + + # remove collinear points + i = -3 + while i < len(nodup) - 3 and len(nodup) > 2: + a, b, c = sorted( + [nodup[i], nodup[i + 1], nodup[i + 2]], key=default_sort_key) + if Point.is_collinear(a, b, c): + nodup[i] = a + nodup[i + 1] = None + nodup.pop(i + 1) + i += 1 + + vertices = list(filter(lambda x: x is not None, nodup)) + + if len(vertices) == 3: + return GeometryEntity.__new__(cls, *vertices, **kwargs) + elif len(vertices) == 2: + return Segment(*vertices, **kwargs) + else: + return Point(*vertices, **kwargs) + + @property + def vertices(self): + """The triangle's vertices + + Returns + ======= + + vertices : tuple + Each element in the tuple is a Point + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Triangle, Point + >>> t = Triangle(Point(0, 0), Point(4, 0), Point(4, 3)) + >>> t.vertices + (Point2D(0, 0), Point2D(4, 0), Point2D(4, 3)) + + """ + return self.args + + def is_similar(t1, t2): + """Is another triangle similar to this one. + + Two triangles are similar if one can be uniformly scaled to the other. + + Parameters + ========== + + other: Triangle + + Returns + ======= + + is_similar : boolean + + See Also + ======== + + sympy.geometry.entity.GeometryEntity.is_similar + + Examples + ======== + + >>> from sympy import Triangle, Point + >>> t1 = Triangle(Point(0, 0), Point(4, 0), Point(4, 3)) + >>> t2 = Triangle(Point(0, 0), Point(-4, 0), Point(-4, -3)) + >>> t1.is_similar(t2) + True + + >>> t2 = Triangle(Point(0, 0), Point(-4, 0), Point(-4, -4)) + >>> t1.is_similar(t2) + False + + """ + if not isinstance(t2, Polygon): + return False + + s1_1, s1_2, s1_3 = [side.length for side in t1.sides] + s2 = [side.length for side in t2.sides] + + def _are_similar(u1, u2, u3, v1, v2, v3): + e1 = simplify(u1/v1) + e2 = simplify(u2/v2) + e3 = simplify(u3/v3) + return bool(e1 == e2) and bool(e2 == e3) + + # There's only 6 permutations, so write them out + return _are_similar(s1_1, s1_2, s1_3, *s2) or \ + _are_similar(s1_1, s1_3, s1_2, *s2) or \ + _are_similar(s1_2, s1_1, s1_3, *s2) or \ + _are_similar(s1_2, s1_3, s1_1, *s2) or \ + _are_similar(s1_3, s1_1, s1_2, *s2) or \ + _are_similar(s1_3, s1_2, s1_1, *s2) + + def is_equilateral(self): + """Are all the sides the same length? + + Returns + ======= + + is_equilateral : boolean + + See Also + ======== + + sympy.geometry.entity.GeometryEntity.is_similar, RegularPolygon + is_isosceles, is_right, is_scalene + + Examples + ======== + + >>> from sympy import Triangle, Point + >>> t1 = Triangle(Point(0, 0), Point(4, 0), Point(4, 3)) + >>> t1.is_equilateral() + False + + >>> from sympy import sqrt + >>> t2 = Triangle(Point(0, 0), Point(10, 0), Point(5, 5*sqrt(3))) + >>> t2.is_equilateral() + True + + """ + return not has_variety(s.length for s in self.sides) + + def is_isosceles(self): + """Are two or more of the sides the same length? + + Returns + ======= + + is_isosceles : boolean + + See Also + ======== + + is_equilateral, is_right, is_scalene + + Examples + ======== + + >>> from sympy import Triangle, Point + >>> t1 = Triangle(Point(0, 0), Point(4, 0), Point(2, 4)) + >>> t1.is_isosceles() + True + + """ + return has_dups(s.length for s in self.sides) + + def is_scalene(self): + """Are all the sides of the triangle of different lengths? + + Returns + ======= + + is_scalene : boolean + + See Also + ======== + + is_equilateral, is_isosceles, is_right + + Examples + ======== + + >>> from sympy import Triangle, Point + >>> t1 = Triangle(Point(0, 0), Point(4, 0), Point(1, 4)) + >>> t1.is_scalene() + True + + """ + return not has_dups(s.length for s in self.sides) + + def is_right(self): + """Is the triangle right-angled. + + Returns + ======= + + is_right : boolean + + See Also + ======== + + sympy.geometry.line.LinearEntity.is_perpendicular + is_equilateral, is_isosceles, is_scalene + + Examples + ======== + + >>> from sympy import Triangle, Point + >>> t1 = Triangle(Point(0, 0), Point(4, 0), Point(4, 3)) + >>> t1.is_right() + True + + """ + s = self.sides + return Segment.is_perpendicular(s[0], s[1]) or \ + Segment.is_perpendicular(s[1], s[2]) or \ + Segment.is_perpendicular(s[0], s[2]) + + @property + def altitudes(self): + """The altitudes of the triangle. + + An altitude of a triangle is a segment through a vertex, + perpendicular to the opposite side, with length being the + height of the vertex measured from the line containing the side. + + Returns + ======= + + altitudes : dict + The dictionary consists of keys which are vertices and values + which are Segments. + + See Also + ======== + + sympy.geometry.point.Point, sympy.geometry.line.Segment.length + + Examples + ======== + + >>> from sympy import Point, Triangle + >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1) + >>> t = Triangle(p1, p2, p3) + >>> t.altitudes[p1] + Segment2D(Point2D(0, 0), Point2D(1/2, 1/2)) + + """ + s = self.sides + v = self.vertices + return {v[0]: s[1].perpendicular_segment(v[0]), + v[1]: s[2].perpendicular_segment(v[1]), + v[2]: s[0].perpendicular_segment(v[2])} + + @property + def orthocenter(self): + """The orthocenter of the triangle. + + The orthocenter is the intersection of the altitudes of a triangle. + It may lie inside, outside or on the triangle. + + Returns + ======= + + orthocenter : Point + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Point, Triangle + >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1) + >>> t = Triangle(p1, p2, p3) + >>> t.orthocenter + Point2D(0, 0) + + """ + a = self.altitudes + v = self.vertices + return Line(a[v[0]]).intersection(Line(a[v[1]]))[0] + + @property + def circumcenter(self): + """The circumcenter of the triangle + + The circumcenter is the center of the circumcircle. + + Returns + ======= + + circumcenter : Point + + See Also + ======== + + sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Point, Triangle + >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1) + >>> t = Triangle(p1, p2, p3) + >>> t.circumcenter + Point2D(1/2, 1/2) + """ + a, b, c = [x.perpendicular_bisector() for x in self.sides] + return a.intersection(b)[0] + + @property + def circumradius(self): + """The radius of the circumcircle of the triangle. + + Returns + ======= + + circumradius : number of Basic instance + + See Also + ======== + + sympy.geometry.ellipse.Circle.radius + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy import Point, Triangle + >>> a = Symbol('a') + >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, a) + >>> t = Triangle(p1, p2, p3) + >>> t.circumradius + sqrt(a**2/4 + 1/4) + """ + return Point.distance(self.circumcenter, self.vertices[0]) + + @property + def circumcircle(self): + """The circle which passes through the three vertices of the triangle. + + Returns + ======= + + circumcircle : Circle + + See Also + ======== + + sympy.geometry.ellipse.Circle + + Examples + ======== + + >>> from sympy import Point, Triangle + >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1) + >>> t = Triangle(p1, p2, p3) + >>> t.circumcircle + Circle(Point2D(1/2, 1/2), sqrt(2)/2) + + """ + return Circle(self.circumcenter, self.circumradius) + + def bisectors(self): + """The angle bisectors of the triangle. + + An angle bisector of a triangle is a straight line through a vertex + which cuts the corresponding angle in half. + + Returns + ======= + + bisectors : dict + Each key is a vertex (Point) and each value is the corresponding + bisector (Segment). + + See Also + ======== + + sympy.geometry.point.Point, sympy.geometry.line.Segment + + Examples + ======== + + >>> from sympy import Point, Triangle, Segment + >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1) + >>> t = Triangle(p1, p2, p3) + >>> from sympy import sqrt + >>> t.bisectors()[p2] == Segment(Point(1, 0), Point(0, sqrt(2) - 1)) + True + + """ + # use lines containing sides so containment check during + # intersection calculation can be avoided, thus reducing + # the processing time for calculating the bisectors + s = [Line(l) for l in self.sides] + v = self.vertices + c = self.incenter + l1 = Segment(v[0], Line(v[0], c).intersection(s[1])[0]) + l2 = Segment(v[1], Line(v[1], c).intersection(s[2])[0]) + l3 = Segment(v[2], Line(v[2], c).intersection(s[0])[0]) + return {v[0]: l1, v[1]: l2, v[2]: l3} + + @property + def incenter(self): + """The center of the incircle. + + The incircle is the circle which lies inside the triangle and touches + all three sides. + + Returns + ======= + + incenter : Point + + See Also + ======== + + incircle, sympy.geometry.point.Point + + Examples + ======== + + >>> from sympy import Point, Triangle + >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1) + >>> t = Triangle(p1, p2, p3) + >>> t.incenter + Point2D(1 - sqrt(2)/2, 1 - sqrt(2)/2) + + """ + s = self.sides + l = Matrix([s[i].length for i in [1, 2, 0]]) + p = sum(l) + v = self.vertices + x = simplify(l.dot(Matrix([vi.x for vi in v]))/p) + y = simplify(l.dot(Matrix([vi.y for vi in v]))/p) + return Point(x, y) + + @property + def inradius(self): + """The radius of the incircle. + + Returns + ======= + + inradius : number of Basic instance + + See Also + ======== + + incircle, sympy.geometry.ellipse.Circle.radius + + Examples + ======== + + >>> from sympy import Point, Triangle + >>> p1, p2, p3 = Point(0, 0), Point(4, 0), Point(0, 3) + >>> t = Triangle(p1, p2, p3) + >>> t.inradius + 1 + + """ + return simplify(2 * self.area / self.perimeter) + + @property + def incircle(self): + """The incircle of the triangle. + + The incircle is the circle which lies inside the triangle and touches + all three sides. + + Returns + ======= + + incircle : Circle + + See Also + ======== + + sympy.geometry.ellipse.Circle + + Examples + ======== + + >>> from sympy import Point, Triangle + >>> p1, p2, p3 = Point(0, 0), Point(2, 0), Point(0, 2) + >>> t = Triangle(p1, p2, p3) + >>> t.incircle + Circle(Point2D(2 - sqrt(2), 2 - sqrt(2)), 2 - sqrt(2)) + + """ + return Circle(self.incenter, self.inradius) + + @property + def exradii(self): + """The radius of excircles of a triangle. + + An excircle of the triangle is a circle lying outside the triangle, + tangent to one of its sides and tangent to the extensions of the + other two. + + Returns + ======= + + exradii : dict + + See Also + ======== + + sympy.geometry.polygon.Triangle.inradius + + Examples + ======== + + The exradius touches the side of the triangle to which it is keyed, e.g. + the exradius touching side 2 is: + + >>> from sympy import Point, Triangle + >>> p1, p2, p3 = Point(0, 0), Point(6, 0), Point(0, 2) + >>> t = Triangle(p1, p2, p3) + >>> t.exradii[t.sides[2]] + -2 + sqrt(10) + + References + ========== + + .. [1] https://mathworld.wolfram.com/Exradius.html + .. [2] https://mathworld.wolfram.com/Excircles.html + + """ + + side = self.sides + a = side[0].length + b = side[1].length + c = side[2].length + s = (a+b+c)/2 + area = self.area + exradii = {self.sides[0]: simplify(area/(s-a)), + self.sides[1]: simplify(area/(s-b)), + self.sides[2]: simplify(area/(s-c))} + + return exradii + + @property + def excenters(self): + """Excenters of the triangle. + + An excenter is the center of a circle that is tangent to a side of the + triangle and the extensions of the other two sides. + + Returns + ======= + + excenters : dict + + + Examples + ======== + + The excenters are keyed to the side of the triangle to which their corresponding + excircle is tangent: The center is keyed, e.g. the excenter of a circle touching + side 0 is: + + >>> from sympy import Point, Triangle + >>> p1, p2, p3 = Point(0, 0), Point(6, 0), Point(0, 2) + >>> t = Triangle(p1, p2, p3) + >>> t.excenters[t.sides[0]] + Point2D(12*sqrt(10), 2/3 + sqrt(10)/3) + + See Also + ======== + + sympy.geometry.polygon.Triangle.exradii + + References + ========== + + .. [1] https://mathworld.wolfram.com/Excircles.html + + """ + + s = self.sides + v = self.vertices + a = s[0].length + b = s[1].length + c = s[2].length + x = [v[0].x, v[1].x, v[2].x] + y = [v[0].y, v[1].y, v[2].y] + + exc_coords = { + "x1": simplify(-a*x[0]+b*x[1]+c*x[2]/(-a+b+c)), + "x2": simplify(a*x[0]-b*x[1]+c*x[2]/(a-b+c)), + "x3": simplify(a*x[0]+b*x[1]-c*x[2]/(a+b-c)), + "y1": simplify(-a*y[0]+b*y[1]+c*y[2]/(-a+b+c)), + "y2": simplify(a*y[0]-b*y[1]+c*y[2]/(a-b+c)), + "y3": simplify(a*y[0]+b*y[1]-c*y[2]/(a+b-c)) + } + + excenters = { + s[0]: Point(exc_coords["x1"], exc_coords["y1"]), + s[1]: Point(exc_coords["x2"], exc_coords["y2"]), + s[2]: Point(exc_coords["x3"], exc_coords["y3"]) + } + + return excenters + + @property + def medians(self): + """The medians of the triangle. + + A median of a triangle is a straight line through a vertex and the + midpoint of the opposite side, and divides the triangle into two + equal areas. + + Returns + ======= + + medians : dict + Each key is a vertex (Point) and each value is the median (Segment) + at that point. + + See Also + ======== + + sympy.geometry.point.Point.midpoint, sympy.geometry.line.Segment.midpoint + + Examples + ======== + + >>> from sympy import Point, Triangle + >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1) + >>> t = Triangle(p1, p2, p3) + >>> t.medians[p1] + Segment2D(Point2D(0, 0), Point2D(1/2, 1/2)) + + """ + s = self.sides + v = self.vertices + return {v[0]: Segment(v[0], s[1].midpoint), + v[1]: Segment(v[1], s[2].midpoint), + v[2]: Segment(v[2], s[0].midpoint)} + + @property + def medial(self): + """The medial triangle of the triangle. + + The triangle which is formed from the midpoints of the three sides. + + Returns + ======= + + medial : Triangle + + See Also + ======== + + sympy.geometry.line.Segment.midpoint + + Examples + ======== + + >>> from sympy import Point, Triangle + >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1) + >>> t = Triangle(p1, p2, p3) + >>> t.medial + Triangle(Point2D(1/2, 0), Point2D(1/2, 1/2), Point2D(0, 1/2)) + + """ + s = self.sides + return Triangle(s[0].midpoint, s[1].midpoint, s[2].midpoint) + + @property + def nine_point_circle(self): + """The nine-point circle of the triangle. + + Nine-point circle is the circumcircle of the medial triangle, which + passes through the feet of altitudes and the middle points of segments + connecting the vertices and the orthocenter. + + Returns + ======= + + nine_point_circle : Circle + + See also + ======== + + sympy.geometry.line.Segment.midpoint + sympy.geometry.polygon.Triangle.medial + sympy.geometry.polygon.Triangle.orthocenter + + Examples + ======== + + >>> from sympy import Point, Triangle + >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1) + >>> t = Triangle(p1, p2, p3) + >>> t.nine_point_circle + Circle(Point2D(1/4, 1/4), sqrt(2)/4) + + """ + return Circle(*self.medial.vertices) + + @property + def eulerline(self): + """The Euler line of the triangle. + + The line which passes through circumcenter, centroid and orthocenter. + + Returns + ======= + + eulerline : Line (or Point for equilateral triangles in which case all + centers coincide) + + Examples + ======== + + >>> from sympy import Point, Triangle + >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1) + >>> t = Triangle(p1, p2, p3) + >>> t.eulerline + Line2D(Point2D(0, 0), Point2D(1/2, 1/2)) + + """ + if self.is_equilateral(): + return self.orthocenter + return Line(self.orthocenter, self.circumcenter) + +def rad(d): + """Return the radian value for the given degrees (pi = 180 degrees).""" + return d*pi/180 + + +def deg(r): + """Return the degree value for the given radians (pi = 180 degrees).""" + return r/pi*180 + + +def _slope(d): + rv = tan(rad(d)) + return rv + + +def _asa(d1, l, d2): + """Return triangle having side with length l on the x-axis.""" + xy = Line((0, 0), slope=_slope(d1)).intersection( + Line((l, 0), slope=_slope(180 - d2)))[0] + return Triangle((0, 0), (l, 0), xy) + + +def _sss(l1, l2, l3): + """Return triangle having side of length l1 on the x-axis.""" + c1 = Circle((0, 0), l3) + c2 = Circle((l1, 0), l2) + inter = [a for a in c1.intersection(c2) if a.y.is_nonnegative] + if not inter: + return None + pt = inter[0] + return Triangle((0, 0), (l1, 0), pt) + + +def _sas(l1, d, l2): + """Return triangle having side with length l2 on the x-axis.""" + p1 = Point(0, 0) + p2 = Point(l2, 0) + p3 = Point(cos(rad(d))*l1, sin(rad(d))*l1) + return Triangle(p1, p2, p3) diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/geometry/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_curve.py b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_curve.py new file mode 100644 index 0000000000000000000000000000000000000000..50aa80273a1d8eb9e414a8d591571f3127352dad --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_curve.py @@ -0,0 +1,120 @@ +from sympy.core.containers import Tuple +from sympy.core.numbers import (Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.hyperbolic import asinh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.geometry import Curve, Line, Point, Ellipse, Ray, Segment, Circle, Polygon, RegularPolygon +from sympy.testing.pytest import raises, slow + + +def test_curve(): + x = Symbol('x', real=True) + s = Symbol('s') + z = Symbol('z') + + # this curve is independent of the indicated parameter + c = Curve([2*s, s**2], (z, 0, 2)) + + assert c.parameter == z + assert c.functions == (2*s, s**2) + assert c.arbitrary_point() == Point(2*s, s**2) + assert c.arbitrary_point(z) == Point(2*s, s**2) + + # this is how it is normally used + c = Curve([2*s, s**2], (s, 0, 2)) + + assert c.parameter == s + assert c.functions == (2*s, s**2) + t = Symbol('t') + # the t returned as assumptions + assert c.arbitrary_point() != Point(2*t, t**2) + t = Symbol('t', real=True) + # now t has the same assumptions so the test passes + assert c.arbitrary_point() == Point(2*t, t**2) + assert c.arbitrary_point(z) == Point(2*z, z**2) + assert c.arbitrary_point(c.parameter) == Point(2*s, s**2) + assert c.arbitrary_point(None) == Point(2*s, s**2) + assert c.plot_interval() == [t, 0, 2] + assert c.plot_interval(z) == [z, 0, 2] + + assert Curve([x, x], (x, 0, 1)).rotate(pi/2) == Curve([-x, x], (x, 0, 1)) + assert Curve([x, x], (x, 0, 1)).rotate(pi/2, (1, 2)).scale(2, 3).translate( + 1, 3).arbitrary_point(s) == \ + Line((0, 0), (1, 1)).rotate(pi/2, (1, 2)).scale(2, 3).translate( + 1, 3).arbitrary_point(s) == \ + Point(-2*s + 7, 3*s + 6) + + raises(ValueError, lambda: Curve((s), (s, 1, 2))) + raises(ValueError, lambda: Curve((x, x * 2), (1, x))) + + raises(ValueError, lambda: Curve((s, s + t), (s, 1, 2)).arbitrary_point()) + raises(ValueError, lambda: Curve((s, s + t), (t, 1, 2)).arbitrary_point(s)) + + +@slow +def test_free_symbols(): + a, b, c, d, e, f, s = symbols('a:f,s') + assert Point(a, b).free_symbols == {a, b} + assert Line((a, b), (c, d)).free_symbols == {a, b, c, d} + assert Ray((a, b), (c, d)).free_symbols == {a, b, c, d} + assert Ray((a, b), angle=c).free_symbols == {a, b, c} + assert Segment((a, b), (c, d)).free_symbols == {a, b, c, d} + assert Line((a, b), slope=c).free_symbols == {a, b, c} + assert Curve((a*s, b*s), (s, c, d)).free_symbols == {a, b, c, d} + assert Ellipse((a, b), c, d).free_symbols == {a, b, c, d} + assert Ellipse((a, b), c, eccentricity=d).free_symbols == \ + {a, b, c, d} + assert Ellipse((a, b), vradius=c, eccentricity=d).free_symbols == \ + {a, b, c, d} + assert Circle((a, b), c).free_symbols == {a, b, c} + assert Circle((a, b), (c, d), (e, f)).free_symbols == \ + {e, d, c, b, f, a} + assert Polygon((a, b), (c, d), (e, f)).free_symbols == \ + {e, b, d, f, a, c} + assert RegularPolygon((a, b), c, d, e).free_symbols == {e, a, b, c, d} + + +def test_transform(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + c = Curve((x, x**2), (x, 0, 1)) + cout = Curve((2*x - 4, 3*x**2 - 10), (x, 0, 1)) + pts = [Point(0, 0), Point(S.Half, Rational(1, 4)), Point(1, 1)] + pts_out = [Point(-4, -10), Point(-3, Rational(-37, 4)), Point(-2, -7)] + + assert c.scale(2, 3, (4, 5)) == cout + assert [c.subs(x, xi/2) for xi in Tuple(0, 1, 2)] == pts + assert [cout.subs(x, xi/2) for xi in Tuple(0, 1, 2)] == pts_out + assert Curve((x + y, 3*x), (x, 0, 1)).subs(y, S.Half) == \ + Curve((x + S.Half, 3*x), (x, 0, 1)) + assert Curve((x, 3*x), (x, 0, 1)).translate(4, 5) == \ + Curve((x + 4, 3*x + 5), (x, 0, 1)) + + +def test_length(): + t = Symbol('t', real=True) + + c1 = Curve((t, 0), (t, 0, 1)) + assert c1.length == 1 + + c2 = Curve((t, t), (t, 0, 1)) + assert c2.length == sqrt(2) + + c3 = Curve((t ** 2, t), (t, 2, 5)) + assert c3.length == -sqrt(17) - asinh(4) / 4 + asinh(10) / 4 + 5 * sqrt(101) / 2 + + +def test_parameter_value(): + t = Symbol('t') + C = Curve([2*t, t**2], (t, 0, 2)) + assert C.parameter_value((2, 1), t) == {t: 1} + raises(ValueError, lambda: C.parameter_value((2, 0), t)) + + +def test_issue_17997(): + t, s = symbols('t s') + c = Curve((t, t**2), (t, 0, 10)) + p = Curve([2*s, s**2], (s, 0, 2)) + assert c(2) == Point(2, 4) + assert p(1) == Point(2, 1) diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_ellipse.py b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_ellipse.py new file mode 100644 index 0000000000000000000000000000000000000000..a79eba8c35771bda9f0980aca68d937f8e625c0a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_ellipse.py @@ -0,0 +1,613 @@ +from sympy.core import expand +from sympy.core.numbers import (Rational, oo, pi) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sec +from sympy.geometry.line import Segment2D +from sympy.geometry.point import Point2D +from sympy.geometry import (Circle, Ellipse, GeometryError, Line, Point, + Polygon, Ray, RegularPolygon, Segment, + Triangle, intersection) +from sympy.testing.pytest import raises, slow +from sympy.integrals.integrals import integrate +from sympy.functions.special.elliptic_integrals import elliptic_e +from sympy.functions.elementary.miscellaneous import Max + + +def test_ellipse_equation_using_slope(): + from sympy.abc import x, y + + e1 = Ellipse(Point(1, 0), 3, 2) + assert str(e1.equation(_slope=1)) == str((-x + y + 1)**2/8 + (x + y - 1)**2/18 - 1) + + e2 = Ellipse(Point(0, 0), 4, 1) + assert str(e2.equation(_slope=1)) == str((-x + y)**2/2 + (x + y)**2/32 - 1) + + e3 = Ellipse(Point(1, 5), 6, 2) + assert str(e3.equation(_slope=2)) == str((-2*x + y - 3)**2/20 + (x + 2*y - 11)**2/180 - 1) + + +def test_object_from_equation(): + from sympy.abc import x, y, a, b, c, d, e + assert Circle(x**2 + y**2 + 3*x + 4*y - 8) == Circle(Point2D(S(-3) / 2, -2), sqrt(57) / 2) + assert Circle(x**2 + y**2 + 6*x + 8*y + 25) == Circle(Point2D(-3, -4), 0) + assert Circle(a**2 + b**2 + 6*a + 8*b + 25, x='a', y='b') == Circle(Point2D(-3, -4), 0) + assert Circle(x**2 + y**2 - 25) == Circle(Point2D(0, 0), 5) + assert Circle(x**2 + y**2) == Circle(Point2D(0, 0), 0) + assert Circle(a**2 + b**2, x='a', y='b') == Circle(Point2D(0, 0), 0) + assert Circle(x**2 + y**2 + 6*x + 8) == Circle(Point2D(-3, 0), 1) + assert Circle(x**2 + y**2 + 6*y + 8) == Circle(Point2D(0, -3), 1) + assert Circle((x - 1)**2 + y**2 - 9) == Circle(Point2D(1, 0), 3) + assert Circle(6*(x**2) + 6*(y**2) + 6*x + 8*y - 25) == Circle(Point2D(Rational(-1, 2), Rational(-2, 3)), 5*sqrt(7)/6) + assert Circle(Eq(a**2 + b**2, 25), x='a', y=b) == Circle(Point2D(0, 0), 5) + raises(GeometryError, lambda: Circle(x**2 + y**2 + 3*x + 4*y + 26)) + raises(GeometryError, lambda: Circle(x**2 + y**2 + 25)) + raises(GeometryError, lambda: Circle(a**2 + b**2 + 25, x='a', y='b')) + raises(GeometryError, lambda: Circle(x**2 + 6*y + 8)) + raises(GeometryError, lambda: Circle(6*(x ** 2) + 4*(y**2) + 6*x + 8*y + 25)) + raises(ValueError, lambda: Circle(a**2 + b**2 + 3*a + 4*b - 8)) + # .equation() adds 'real=True' assumption; '==' would fail if assumptions differed + x, y = symbols('x y', real=True) + eq = a*x**2 + a*y**2 + c*x + d*y + e + assert expand(Circle(eq).equation()*a) == eq + + +@slow +def test_ellipse_geom(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + t = Symbol('t', real=True) + y1 = Symbol('y1', real=True) + half = S.Half + p1 = Point(0, 0) + p2 = Point(1, 1) + p4 = Point(0, 1) + + e1 = Ellipse(p1, 1, 1) + e2 = Ellipse(p2, half, 1) + e3 = Ellipse(p1, y1, y1) + c1 = Circle(p1, 1) + c2 = Circle(p2, 1) + c3 = Circle(Point(sqrt(2), sqrt(2)), 1) + l1 = Line(p1, p2) + + # Test creation with three points + cen, rad = Point(3*half, 2), 5*half + assert Circle(Point(0, 0), Point(3, 0), Point(0, 4)) == Circle(cen, rad) + assert Circle(Point(0, 0), Point(1, 1), Point(2, 2)) == Segment2D(Point2D(0, 0), Point2D(2, 2)) + + raises(ValueError, lambda: Ellipse(None, None, None, 1)) + raises(ValueError, lambda: Ellipse()) + raises(GeometryError, lambda: Circle(Point(0, 0))) + raises(GeometryError, lambda: Circle(Symbol('x')*Symbol('y'))) + + # Basic Stuff + assert Ellipse(None, 1, 1).center == Point(0, 0) + assert e1 == c1 + assert e1 != e2 + assert e1 != l1 + assert p4 in e1 + assert e1 in e1 + assert e2 in e2 + assert 1 not in e2 + assert p2 not in e2 + assert e1.area == pi + assert e2.area == pi/2 + assert e3.area == pi*y1*abs(y1) + assert c1.area == e1.area + assert c1.circumference == e1.circumference + assert e3.circumference == 2*pi*y1 + assert e1.plot_interval() == e2.plot_interval() == [t, -pi, pi] + assert e1.plot_interval(x) == e2.plot_interval(x) == [x, -pi, pi] + + assert c1.minor == 1 + assert c1.major == 1 + assert c1.hradius == 1 + assert c1.vradius == 1 + + assert Ellipse((1, 1), 0, 0) == Point(1, 1) + assert Ellipse((1, 1), 1, 0) == Segment(Point(0, 1), Point(2, 1)) + assert Ellipse((1, 1), 0, 1) == Segment(Point(1, 0), Point(1, 2)) + + # Private Functions + assert hash(c1) == hash(Circle(Point(1, 0), Point(0, 1), Point(0, -1))) + assert c1 in e1 + assert (Line(p1, p2) in e1) is False + assert e1.__cmp__(e1) == 0 + assert e1.__cmp__(Point(0, 0)) > 0 + + # Encloses + assert e1.encloses(Segment(Point(-0.5, -0.5), Point(0.5, 0.5))) is True + assert e1.encloses(Line(p1, p2)) is False + assert e1.encloses(Ray(p1, p2)) is False + assert e1.encloses(e1) is False + assert e1.encloses( + Polygon(Point(-0.5, -0.5), Point(-0.5, 0.5), Point(0.5, 0.5))) is True + assert e1.encloses(RegularPolygon(p1, 0.5, 3)) is True + assert e1.encloses(RegularPolygon(p1, 5, 3)) is False + assert e1.encloses(RegularPolygon(p2, 5, 3)) is False + + assert e2.arbitrary_point() in e2 + raises(ValueError, lambda: Ellipse(Point(x, y), 1, 1).arbitrary_point(parameter='x')) + + # Foci + f1, f2 = Point(sqrt(12), 0), Point(-sqrt(12), 0) + ef = Ellipse(Point(0, 0), 4, 2) + assert ef.foci in [(f1, f2), (f2, f1)] + + # Tangents + v = sqrt(2) / 2 + p1_1 = Point(v, v) + p1_2 = p2 + Point(half, 0) + p1_3 = p2 + Point(0, 1) + assert e1.tangent_lines(p4) == c1.tangent_lines(p4) + assert e2.tangent_lines(p1_2) == [Line(Point(Rational(3, 2), 1), Point(Rational(3, 2), S.Half))] + assert e2.tangent_lines(p1_3) == [Line(Point(1, 2), Point(Rational(5, 4), 2))] + assert c1.tangent_lines(p1_1) != [Line(p1_1, Point(0, sqrt(2)))] + assert c1.tangent_lines(p1) == [] + assert e2.is_tangent(Line(p1_2, p2 + Point(half, 1))) + assert e2.is_tangent(Line(p1_3, p2 + Point(half, 1))) + assert c1.is_tangent(Line(p1_1, Point(0, sqrt(2)))) + assert e1.is_tangent(Line(Point(0, 0), Point(1, 1))) is False + assert c1.is_tangent(e1) is True + assert c1.is_tangent(Ellipse(Point(2, 0), 1, 1)) is True + assert c1.is_tangent( + Polygon(Point(1, 1), Point(1, -1), Point(2, 0))) is False + assert c1.is_tangent( + Polygon(Point(1, 1), Point(1, 0), Point(2, 0))) is False + assert Circle(Point(5, 5), 3).is_tangent(Circle(Point(0, 5), 1)) is False + + assert Ellipse(Point(5, 5), 2, 1).tangent_lines(Point(0, 0)) == \ + [Line(Point(0, 0), Point(Rational(77, 25), Rational(132, 25))), + Line(Point(0, 0), Point(Rational(33, 5), Rational(22, 5)))] + assert Ellipse(Point(5, 5), 2, 1).tangent_lines(Point(3, 4)) == \ + [Line(Point(3, 4), Point(4, 4)), Line(Point(3, 4), Point(3, 5))] + assert Circle(Point(5, 5), 2).tangent_lines(Point(3, 3)) == \ + [Line(Point(3, 3), Point(4, 3)), Line(Point(3, 3), Point(3, 4))] + assert Circle(Point(5, 5), 2).tangent_lines(Point(5 - 2*sqrt(2), 5)) == \ + [Line(Point(5 - 2*sqrt(2), 5), Point(5 - sqrt(2), 5 - sqrt(2))), + Line(Point(5 - 2*sqrt(2), 5), Point(5 - sqrt(2), 5 + sqrt(2))), ] + assert Circle(Point(5, 5), 5).tangent_lines(Point(4, 0)) == \ + [Line(Point(4, 0), Point(Rational(40, 13), Rational(5, 13))), + Line(Point(4, 0), Point(5, 0))] + assert Circle(Point(5, 5), 5).tangent_lines(Point(0, 6)) == \ + [Line(Point(0, 6), Point(0, 7)), + Line(Point(0, 6), Point(Rational(5, 13), Rational(90, 13)))] + + # for numerical calculations, we shouldn't demand exact equality, + # so only test up to the desired precision + def lines_close(l1, l2, prec): + """ tests whether l1 and 12 are within 10**(-prec) + of each other """ + return abs(l1.p1 - l2.p1) < 10**(-prec) and abs(l1.p2 - l2.p2) < 10**(-prec) + def line_list_close(ll1, ll2, prec): + return all(lines_close(l1, l2, prec) for l1, l2 in zip(ll1, ll2)) + + e = Ellipse(Point(0, 0), 2, 1) + assert e.normal_lines(Point(0, 0)) == \ + [Line(Point(0, 0), Point(0, 1)), Line(Point(0, 0), Point(1, 0))] + assert e.normal_lines(Point(1, 0)) == \ + [Line(Point(0, 0), Point(1, 0))] + assert e.normal_lines((0, 1)) == \ + [Line(Point(0, 0), Point(0, 1))] + assert line_list_close(e.normal_lines(Point(1, 1), 2), [ + Line(Point(Rational(-51, 26), Rational(-1, 5)), Point(Rational(-25, 26), Rational(17, 83))), + Line(Point(Rational(28, 29), Rational(-7, 8)), Point(Rational(57, 29), Rational(-9, 2)))], 2) + # test the failure of Poly.intervals and checks a point on the boundary + p = Point(sqrt(3), S.Half) + assert p in e + assert line_list_close(e.normal_lines(p, 2), [ + Line(Point(Rational(-341, 171), Rational(-1, 13)), Point(Rational(-170, 171), Rational(5, 64))), + Line(Point(Rational(26, 15), Rational(-1, 2)), Point(Rational(41, 15), Rational(-43, 26)))], 2) + # be sure to use the slope that isn't undefined on boundary + e = Ellipse((0, 0), 2, 2*sqrt(3)/3) + assert line_list_close(e.normal_lines((1, 1), 2), [ + Line(Point(Rational(-64, 33), Rational(-20, 71)), Point(Rational(-31, 33), Rational(2, 13))), + Line(Point(1, -1), Point(2, -4))], 2) + # general ellipse fails except under certain conditions + e = Ellipse((0, 0), x, 1) + assert e.normal_lines((x + 1, 0)) == [Line(Point(0, 0), Point(1, 0))] + raises(NotImplementedError, lambda: e.normal_lines((x + 1, 1))) + # Properties + major = 3 + minor = 1 + e4 = Ellipse(p2, minor, major) + assert e4.focus_distance == sqrt(major**2 - minor**2) + ecc = e4.focus_distance / major + assert e4.eccentricity == ecc + assert e4.periapsis == major*(1 - ecc) + assert e4.apoapsis == major*(1 + ecc) + assert e4.semilatus_rectum == major*(1 - ecc ** 2) + # independent of orientation + e4 = Ellipse(p2, major, minor) + assert e4.focus_distance == sqrt(major**2 - minor**2) + ecc = e4.focus_distance / major + assert e4.eccentricity == ecc + assert e4.periapsis == major*(1 - ecc) + assert e4.apoapsis == major*(1 + ecc) + + # Intersection + l1 = Line(Point(1, -5), Point(1, 5)) + l2 = Line(Point(-5, -1), Point(5, -1)) + l3 = Line(Point(-1, -1), Point(1, 1)) + l4 = Line(Point(-10, 0), Point(0, 10)) + pts_c1_l3 = [Point(sqrt(2)/2, sqrt(2)/2), Point(-sqrt(2)/2, -sqrt(2)/2)] + + assert intersection(e2, l4) == [] + assert intersection(c1, Point(1, 0)) == [Point(1, 0)] + assert intersection(c1, l1) == [Point(1, 0)] + assert intersection(c1, l2) == [Point(0, -1)] + assert intersection(c1, l3) in [pts_c1_l3, [pts_c1_l3[1], pts_c1_l3[0]]] + assert intersection(c1, c2) == [Point(0, 1), Point(1, 0)] + assert intersection(c1, c3) == [Point(sqrt(2)/2, sqrt(2)/2)] + assert e1.intersection(l1) == [Point(1, 0)] + assert e2.intersection(l4) == [] + assert e1.intersection(Circle(Point(0, 2), 1)) == [Point(0, 1)] + assert e1.intersection(Circle(Point(5, 0), 1)) == [] + assert e1.intersection(Ellipse(Point(2, 0), 1, 1)) == [Point(1, 0)] + assert e1.intersection(Ellipse(Point(5, 0), 1, 1)) == [] + assert e1.intersection(Point(2, 0)) == [] + assert e1.intersection(e1) == e1 + assert intersection(Ellipse(Point(0, 0), 2, 1), Ellipse(Point(3, 0), 1, 2)) == [Point(2, 0)] + assert intersection(Circle(Point(0, 0), 2), Circle(Point(3, 0), 1)) == [Point(2, 0)] + assert intersection(Circle(Point(0, 0), 2), Circle(Point(7, 0), 1)) == [] + assert intersection(Ellipse(Point(0, 0), 5, 17), Ellipse(Point(4, 0), 1, 0.2) + ) == [Point(5.0, 0, evaluate=False)] + assert intersection(Ellipse(Point(0, 0), 5, 17), Ellipse(Point(4, 0), 0.999, 0.2)) == [] + assert Circle((0, 0), S.Half).intersection( + Triangle((-1, 0), (1, 0), (0, 1))) == [ + Point(Rational(-1, 2), 0), Point(S.Half, 0)] + raises(TypeError, lambda: intersection(e2, Line((0, 0, 0), (0, 0, 1)))) + raises(TypeError, lambda: intersection(e2, Rational(12))) + raises(TypeError, lambda: Ellipse.intersection(e2, 1)) + # some special case intersections + csmall = Circle(p1, 3) + cbig = Circle(p1, 5) + cout = Circle(Point(5, 5), 1) + # one circle inside of another + assert csmall.intersection(cbig) == [] + # separate circles + assert csmall.intersection(cout) == [] + # coincident circles + assert csmall.intersection(csmall) == csmall + + v = sqrt(2) + t1 = Triangle(Point(0, v), Point(0, -v), Point(v, 0)) + points = intersection(t1, c1) + assert len(points) == 4 + assert Point(0, 1) in points + assert Point(0, -1) in points + assert Point(v/2, v/2) in points + assert Point(v/2, -v/2) in points + + circ = Circle(Point(0, 0), 5) + elip = Ellipse(Point(0, 0), 5, 20) + assert intersection(circ, elip) in \ + [[Point(5, 0), Point(-5, 0)], [Point(-5, 0), Point(5, 0)]] + assert elip.tangent_lines(Point(0, 0)) == [] + elip = Ellipse(Point(0, 0), 3, 2) + assert elip.tangent_lines(Point(3, 0)) == \ + [Line(Point(3, 0), Point(3, -12))] + + e1 = Ellipse(Point(0, 0), 5, 10) + e2 = Ellipse(Point(2, 1), 4, 8) + a = Rational(53, 17) + c = 2*sqrt(3991)/17 + ans = [Point(a - c/8, a/2 + c), Point(a + c/8, a/2 - c)] + assert e1.intersection(e2) == ans + e2 = Ellipse(Point(x, y), 4, 8) + c = sqrt(3991) + ans = [Point(-c/68 + a, c*Rational(2, 17) + a/2), Point(c/68 + a, c*Rational(-2, 17) + a/2)] + assert [p.subs({x: 2, y:1}) for p in e1.intersection(e2)] == ans + + # Combinations of above + assert e3.is_tangent(e3.tangent_lines(p1 + Point(y1, 0))[0]) + + e = Ellipse((1, 2), 3, 2) + assert e.tangent_lines(Point(10, 0)) == \ + [Line(Point(10, 0), Point(1, 0)), + Line(Point(10, 0), Point(Rational(14, 5), Rational(18, 5)))] + + # encloses_point + e = Ellipse((0, 0), 1, 2) + assert e.encloses_point(e.center) + assert e.encloses_point(e.center + Point(0, e.vradius - Rational(1, 10))) + assert e.encloses_point(e.center + Point(e.hradius - Rational(1, 10), 0)) + assert e.encloses_point(e.center + Point(e.hradius, 0)) is False + assert e.encloses_point( + e.center + Point(e.hradius + Rational(1, 10), 0)) is False + e = Ellipse((0, 0), 2, 1) + assert e.encloses_point(e.center) + assert e.encloses_point(e.center + Point(0, e.vradius - Rational(1, 10))) + assert e.encloses_point(e.center + Point(e.hradius - Rational(1, 10), 0)) + assert e.encloses_point(e.center + Point(e.hradius, 0)) is False + assert e.encloses_point( + e.center + Point(e.hradius + Rational(1, 10), 0)) is False + assert c1.encloses_point(Point(1, 0)) is False + assert c1.encloses_point(Point(0.3, 0.4)) is True + + assert e.scale(2, 3) == Ellipse((0, 0), 4, 3) + assert e.scale(3, 6) == Ellipse((0, 0), 6, 6) + assert e.rotate(pi) == e + assert e.rotate(pi, (1, 2)) == Ellipse(Point(2, 4), 2, 1) + raises(NotImplementedError, lambda: e.rotate(pi/3)) + + # Circle rotation tests (Issue #11743) + # Link - https://github.com/sympy/sympy/issues/11743 + cir = Circle(Point(1, 0), 1) + assert cir.rotate(pi/2) == Circle(Point(0, 1), 1) + assert cir.rotate(pi/3) == Circle(Point(S.Half, sqrt(3)/2), 1) + assert cir.rotate(pi/3, Point(1, 0)) == Circle(Point(1, 0), 1) + assert cir.rotate(pi/3, Point(0, 1)) == Circle(Point(S.Half + sqrt(3)/2, S.Half + sqrt(3)/2), 1) + + +def test_construction(): + e1 = Ellipse(hradius=2, vradius=1, eccentricity=None) + assert e1.eccentricity == sqrt(3)/2 + + e2 = Ellipse(hradius=2, vradius=None, eccentricity=sqrt(3)/2) + assert e2.vradius == 1 + + e3 = Ellipse(hradius=None, vradius=1, eccentricity=sqrt(3)/2) + assert e3.hradius == 2 + + # filter(None, iterator) filters out anything falsey, including 0 + # eccentricity would be filtered out in this case and the constructor would throw an error + e4 = Ellipse(Point(0, 0), hradius=1, eccentricity=0) + assert e4.vradius == 1 + + #tests for eccentricity > 1 + raises(GeometryError, lambda: Ellipse(Point(3, 1), hradius=3, eccentricity = S(3)/2)) + raises(GeometryError, lambda: Ellipse(Point(3, 1), hradius=3, eccentricity=sec(5))) + raises(GeometryError, lambda: Ellipse(Point(3, 1), hradius=3, eccentricity=S.Pi-S(2))) + + #tests for eccentricity = 1 + #if vradius is not defined + assert Ellipse(None, 1, None, 1).length == 2 + #if hradius is not defined + raises(GeometryError, lambda: Ellipse(None, None, 1, eccentricity = 1)) + + #tests for eccentricity < 0 + raises(GeometryError, lambda: Ellipse(Point(3, 1), hradius=3, eccentricity = -3)) + raises(GeometryError, lambda: Ellipse(Point(3, 1), hradius=3, eccentricity = -0.5)) + +def test_ellipse_random_point(): + y1 = Symbol('y1', real=True) + e3 = Ellipse(Point(0, 0), y1, y1) + rx, ry = Symbol('rx'), Symbol('ry') + for ind in range(0, 5): + r = e3.random_point() + # substitution should give zero*y1**2 + assert e3.equation(rx, ry).subs(zip((rx, ry), r.args)).equals(0) + # test for the case with seed + r = e3.random_point(seed=1) + assert e3.equation(rx, ry).subs(zip((rx, ry), r.args)).equals(0) + + +def test_repr(): + assert repr(Circle((0, 1), 2)) == 'Circle(Point2D(0, 1), 2)' + + +def test_transform(): + c = Circle((1, 1), 2) + assert c.scale(-1) == Circle((-1, 1), 2) + assert c.scale(y=-1) == Circle((1, -1), 2) + assert c.scale(2) == Ellipse((2, 1), 4, 2) + + assert Ellipse((0, 0), 2, 3).scale(2, 3, (4, 5)) == \ + Ellipse(Point(-4, -10), 4, 9) + assert Circle((0, 0), 2).scale(2, 3, (4, 5)) == \ + Ellipse(Point(-4, -10), 4, 6) + assert Ellipse((0, 0), 2, 3).scale(3, 3, (4, 5)) == \ + Ellipse(Point(-8, -10), 6, 9) + assert Circle((0, 0), 2).scale(3, 3, (4, 5)) == \ + Circle(Point(-8, -10), 6) + assert Circle(Point(-8, -10), 6).scale(Rational(1, 3), Rational(1, 3), (4, 5)) == \ + Circle((0, 0), 2) + assert Circle((0, 0), 2).translate(4, 5) == \ + Circle((4, 5), 2) + assert Circle((0, 0), 2).scale(3, 3) == \ + Circle((0, 0), 6) + + +def test_bounds(): + e1 = Ellipse(Point(0, 0), 3, 5) + e2 = Ellipse(Point(2, -2), 7, 7) + c1 = Circle(Point(2, -2), 7) + c2 = Circle(Point(-2, 0), Point(0, 2), Point(2, 0)) + assert e1.bounds == (-3, -5, 3, 5) + assert e2.bounds == (-5, -9, 9, 5) + assert c1.bounds == (-5, -9, 9, 5) + assert c2.bounds == (-2, -2, 2, 2) + + +def test_reflect(): + b = Symbol('b') + m = Symbol('m') + l = Line((0, b), slope=m) + t1 = Triangle((0, 0), (1, 0), (2, 3)) + assert t1.area == -t1.reflect(l).area + e = Ellipse((1, 0), 1, 2) + assert e.area == -e.reflect(Line((1, 0), slope=0)).area + assert e.area == -e.reflect(Line((1, 0), slope=oo)).area + raises(NotImplementedError, lambda: e.reflect(Line((1, 0), slope=m))) + assert Circle((0, 1), 1).reflect(Line((0, 0), (1, 1))) == Circle(Point2D(1, 0), -1) + + +def test_is_tangent(): + e1 = Ellipse(Point(0, 0), 3, 5) + c1 = Circle(Point(2, -2), 7) + assert e1.is_tangent(Point(0, 0)) is False + assert e1.is_tangent(Point(3, 0)) is False + assert e1.is_tangent(e1) is True + assert e1.is_tangent(Ellipse((0, 0), 1, 2)) is False + assert e1.is_tangent(Ellipse((0, 0), 3, 2)) is True + assert c1.is_tangent(Ellipse((2, -2), 7, 1)) is True + assert c1.is_tangent(Circle((11, -2), 2)) is True + assert c1.is_tangent(Circle((7, -2), 2)) is True + assert c1.is_tangent(Ray((-5, -2), (-15, -20))) is False + assert c1.is_tangent(Ray((-3, -2), (-15, -20))) is False + assert c1.is_tangent(Ray((-3, -22), (15, 20))) is False + assert c1.is_tangent(Ray((9, 20), (9, -20))) is True + assert c1.is_tangent(Ray((2, 5), (9, 5))) is True + assert c1.is_tangent(Segment((2, 5), (9, 5))) is True + assert e1.is_tangent(Segment((2, 2), (-7, 7))) is False + assert e1.is_tangent(Segment((0, 0), (1, 2))) is False + assert c1.is_tangent(Segment((0, 0), (-5, -2))) is False + assert e1.is_tangent(Segment((3, 0), (12, 12))) is False + assert e1.is_tangent(Segment((12, 12), (3, 0))) is False + assert e1.is_tangent(Segment((-3, 0), (3, 0))) is False + assert e1.is_tangent(Segment((-3, 5), (3, 5))) is True + assert e1.is_tangent(Line((10, 0), (10, 10))) is False + assert e1.is_tangent(Line((0, 0), (1, 1))) is False + assert e1.is_tangent(Line((-3, 0), (-2.99, -0.001))) is False + assert e1.is_tangent(Line((-3, 0), (-3, 1))) is True + assert e1.is_tangent(Polygon((0, 0), (5, 5), (5, -5))) is False + assert e1.is_tangent(Polygon((-100, -50), (-40, -334), (-70, -52))) is False + assert e1.is_tangent(Polygon((-3, 0), (3, 0), (0, 1))) is False + assert e1.is_tangent(Polygon((-3, 0), (3, 0), (0, 5))) is False + assert e1.is_tangent(Polygon((-3, 0), (0, -5), (3, 0), (0, 5))) is False + assert e1.is_tangent(Polygon((-3, -5), (-3, 5), (3, 5), (3, -5))) is True + assert c1.is_tangent(Polygon((-3, -5), (-3, 5), (3, 5), (3, -5))) is False + assert e1.is_tangent(Polygon((0, 0), (3, 0), (7, 7), (0, 5))) is False + assert e1.is_tangent(Polygon((3, 12), (3, -12), (6, 5))) is False + assert e1.is_tangent(Polygon((3, 12), (3, -12), (0, -5), (0, 5))) is False + assert e1.is_tangent(Polygon((3, 0), (5, 7), (6, -5))) is False + assert c1.is_tangent(Segment((0, 0), (-5, -2))) is False + assert e1.is_tangent(Segment((-3, 0), (3, 0))) is False + assert e1.is_tangent(Segment((-3, 5), (3, 5))) is True + assert e1.is_tangent(Polygon((0, 0), (5, 5), (5, -5))) is False + assert e1.is_tangent(Polygon((-100, -50), (-40, -334), (-70, -52))) is False + assert e1.is_tangent(Polygon((-3, -5), (-3, 5), (3, 5), (3, -5))) is True + assert c1.is_tangent(Polygon((-3, -5), (-3, 5), (3, 5), (3, -5))) is False + assert e1.is_tangent(Polygon((3, 12), (3, -12), (0, -5), (0, 5))) is False + assert e1.is_tangent(Polygon((3, 0), (5, 7), (6, -5))) is False + raises(TypeError, lambda: e1.is_tangent(Point(0, 0, 0))) + raises(TypeError, lambda: e1.is_tangent(Rational(5))) + + +def test_parameter_value(): + t = Symbol('t') + e = Ellipse(Point(0, 0), 3, 5) + assert e.parameter_value((3, 0), t) == {t: 0} + raises(ValueError, lambda: e.parameter_value((4, 0), t)) + + +@slow +def test_second_moment_of_area(): + x, y = symbols('x, y') + e = Ellipse(Point(0, 0), 5, 4) + I_yy = 2*4*integrate(sqrt(25 - x**2)*x**2, (x, -5, 5))/5 + I_xx = 2*5*integrate(sqrt(16 - y**2)*y**2, (y, -4, 4))/4 + Y = 3*sqrt(1 - x**2/5**2) + I_xy = integrate(integrate(y, (y, -Y, Y))*x, (x, -5, 5)) + assert I_yy == e.second_moment_of_area()[1] + assert I_xx == e.second_moment_of_area()[0] + assert I_xy == e.second_moment_of_area()[2] + #checking for other point + t1 = e.second_moment_of_area(Point(6,5)) + t2 = (580*pi, 845*pi, 600*pi) + assert t1==t2 + + +def test_section_modulus_and_polar_second_moment_of_area(): + d = Symbol('d', positive=True) + c = Circle((3, 7), 8) + assert c.polar_second_moment_of_area() == 2048*pi + assert c.section_modulus() == (128*pi, 128*pi) + c = Circle((2, 9), d/2) + assert c.polar_second_moment_of_area() == pi*d**3*Abs(d)/64 + pi*d*Abs(d)**3/64 + assert c.section_modulus() == (pi*d**3/S(32), pi*d**3/S(32)) + + a, b = symbols('a, b', positive=True) + e = Ellipse((4, 6), a, b) + assert e.section_modulus() == (pi*a*b**2/S(4), pi*a**2*b/S(4)) + assert e.polar_second_moment_of_area() == pi*a**3*b/S(4) + pi*a*b**3/S(4) + e = e.rotate(pi/2) # no change in polar and section modulus + assert e.section_modulus() == (pi*a**2*b/S(4), pi*a*b**2/S(4)) + assert e.polar_second_moment_of_area() == pi*a**3*b/S(4) + pi*a*b**3/S(4) + + e = Ellipse((a, b), 2, 6) + assert e.section_modulus() == (18*pi, 6*pi) + assert e.polar_second_moment_of_area() == 120*pi + + e = Ellipse(Point(0, 0), 2, 2) + assert e.section_modulus() == (2*pi, 2*pi) + assert e.section_modulus(Point(2, 2)) == (2*pi, 2*pi) + assert e.section_modulus((2, 2)) == (2*pi, 2*pi) + + +def test_circumference(): + M = Symbol('M') + m = Symbol('m') + assert Ellipse(Point(0, 0), M, m).circumference == 4 * M * elliptic_e((M ** 2 - m ** 2) / M**2) + + assert Ellipse(Point(0, 0), 5, 4).circumference == 20 * elliptic_e(S(9) / 25) + + # circle + assert Ellipse(None, 1, None, 0).circumference == 2*pi + + # test numerically + assert abs(Ellipse(None, hradius=5, vradius=3).circumference.evalf(16) - 25.52699886339813) < 1e-10 + + +def test_issue_15259(): + assert Circle((1, 2), 0) == Point(1, 2) + + +def test_issue_15797_equals(): + Ri = 0.024127189424130748 + Ci = (0.0864931002830291, 0.0819863295239654) + A = Point(0, 0.0578591400998346) + c = Circle(Ci, Ri) # evaluated + assert c.is_tangent(c.tangent_lines(A)[0]) == True + assert c.center.x.is_Rational + assert c.center.y.is_Rational + assert c.radius.is_Rational + u = Circle(Ci, Ri, evaluate=False) # unevaluated + assert u.center.x.is_Float + assert u.center.y.is_Float + assert u.radius.is_Float + + +def test_auxiliary_circle(): + x, y, a, b = symbols('x y a b') + e = Ellipse((x, y), a, b) + # the general result + assert e.auxiliary_circle() == Circle((x, y), Max(a, b)) + # a special case where Ellipse is a Circle + assert Circle((3, 4), 8).auxiliary_circle() == Circle((3, 4), 8) + + +def test_director_circle(): + x, y, a, b = symbols('x y a b') + e = Ellipse((x, y), a, b) + # the general result + assert e.director_circle() == Circle((x, y), sqrt(a**2 + b**2)) + # a special case where Ellipse is a Circle + assert Circle((3, 4), 8).director_circle() == Circle((3, 4), 8*sqrt(2)) + + +def test_evolute(): + #ellipse centered at h,k + x, y, h, k = symbols('x y h k',real = True) + a, b = symbols('a b') + e = Ellipse(Point(h, k), a, b) + t1 = (e.hradius*(x - e.center.x))**Rational(2, 3) + t2 = (e.vradius*(y - e.center.y))**Rational(2, 3) + E = t1 + t2 - (e.hradius**2 - e.vradius**2)**Rational(2, 3) + assert e.evolute() == E + #Numerical Example + e = Ellipse(Point(1, 1), 6, 3) + t1 = (6*(x - 1))**Rational(2, 3) + t2 = (3*(y - 1))**Rational(2, 3) + E = t1 + t2 - (27)**Rational(2, 3) + assert e.evolute() == E + + +def test_svg(): + e1 = Ellipse(Point(1, 0), 3, 2) + assert e1._svg(2, "#FFAAFF") == '' diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_entity.py b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_entity.py new file mode 100644 index 0000000000000000000000000000000000000000..0d440fd5dbd193c7c490b45a706fab2703e247ec --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_entity.py @@ -0,0 +1,120 @@ +from sympy.core.numbers import (Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.geometry import (Circle, Ellipse, Point, Line, Parabola, + Polygon, Ray, RegularPolygon, Segment, Triangle, Plane, Curve) +from sympy.geometry.entity import scale, GeometryEntity +from sympy.testing.pytest import raises + + +def test_entity(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + + assert GeometryEntity(x, y) in GeometryEntity(x, y) + raises(NotImplementedError, lambda: Point(0, 0) in GeometryEntity(x, y)) + + assert GeometryEntity(x, y) == GeometryEntity(x, y) + assert GeometryEntity(x, y).equals(GeometryEntity(x, y)) + + c = Circle((0, 0), 5) + assert GeometryEntity.encloses(c, Point(0, 0)) + assert GeometryEntity.encloses(c, Segment((0, 0), (1, 1))) + assert GeometryEntity.encloses(c, Line((0, 0), (1, 1))) is False + assert GeometryEntity.encloses(c, Circle((0, 0), 4)) + assert GeometryEntity.encloses(c, Polygon(Point(0, 0), Point(1, 0), Point(0, 1))) + assert GeometryEntity.encloses(c, RegularPolygon(Point(8, 8), 1, 3)) is False + + +def test_svg(): + a = Symbol('a') + b = Symbol('b') + d = Symbol('d') + + entity = Circle(Point(a, b), d) + assert entity._repr_svg_() is None + + entity = Circle(Point(0, 0), S.Infinity) + assert entity._repr_svg_() is None + + +def test_subs(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + p = Point(x, 2) + q = Point(1, 1) + r = Point(3, 4) + for o in [p, + Segment(p, q), + Ray(p, q), + Line(p, q), + Triangle(p, q, r), + RegularPolygon(p, 3, 6), + Polygon(p, q, r, Point(5, 4)), + Circle(p, 3), + Ellipse(p, 3, 4)]: + assert 'y' in str(o.subs(x, y)) + assert p.subs({x: 1}) == Point(1, 2) + assert Point(1, 2).subs(Point(1, 2), Point(3, 4)) == Point(3, 4) + assert Point(1, 2).subs((1, 2), Point(3, 4)) == Point(3, 4) + assert Point(1, 2).subs(Point(1, 2), Point(3, 4)) == Point(3, 4) + assert Point(1, 2).subs({(1, 2)}) == Point(2, 2) + raises(ValueError, lambda: Point(1, 2).subs(1)) + raises(TypeError, lambda: Point(1, 1).subs((Point(1, 1), Point(1, + 2)), 1, 2)) + + +def test_transform(): + assert scale(1, 2, (3, 4)).tolist() == \ + [[1, 0, 0], [0, 2, 0], [0, -4, 1]] + + +def test_reflect_entity_overrides(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + b = Symbol('b') + m = Symbol('m') + l = Line((0, b), slope=m) + p = Point(x, y) + r = p.reflect(l) + c = Circle((x, y), 3) + cr = c.reflect(l) + assert cr == Circle(r, -3) + assert c.area == -cr.area + + pent = RegularPolygon((1, 2), 1, 5) + slope = S.ComplexInfinity + while slope is S.ComplexInfinity: + slope = Rational(*(x._random()/2).as_real_imag()) + l = Line(pent.vertices[1], slope=slope) + rpent = pent.reflect(l) + assert rpent.center == pent.center.reflect(l) + rvert = [i.reflect(l) for i in pent.vertices] + for v in rpent.vertices: + for i in range(len(rvert)): + ri = rvert[i] + if ri.equals(v): + rvert.remove(ri) + break + assert not rvert + assert pent.area.equals(-rpent.area) + + +def test_geometry_EvalfMixin(): + x = pi + t = Symbol('t') + for g in [ + Point(x, x), + Plane(Point(0, x, 0), (0, 0, x)), + Curve((x*t, x), (t, 0, x)), + Ellipse((x, x), x, -x), + Circle((x, x), x), + Line((0, x), (x, 0)), + Segment((0, x), (x, 0)), + Ray((0, x), (x, 0)), + Parabola((0, x), Line((-x, 0), (x, 0))), + Polygon((0, 0), (0, x), (x, 0), (x, x)), + RegularPolygon((0, x), x, 4, x), + Triangle((0, 0), (x, 0), (x, x)), + ]: + assert str(g).replace('pi', '3.1') == str(g.n(2)) diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_geometrysets.py b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_geometrysets.py new file mode 100644 index 0000000000000000000000000000000000000000..c52898b3c9ba4e9db80c244db3aebf88db2cc8b4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_geometrysets.py @@ -0,0 +1,38 @@ +from sympy.core.numbers import Rational +from sympy.core.singleton import S +from sympy.geometry import Circle, Line, Point, Polygon, Segment +from sympy.sets import FiniteSet, Union, Intersection, EmptySet + + +def test_booleans(): + """ test basic unions and intersections """ + half = S.Half + + p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)]) + p5, p6, p7 = map(Point, [(3, 2), (1, -1), (0, 2)]) + l1 = Line(Point(0,0), Point(1,1)) + l2 = Line(Point(half, half), Point(5,5)) + l3 = Line(p2, p3) + l4 = Line(p3, p4) + poly1 = Polygon(p1, p2, p3, p4) + poly2 = Polygon(p5, p6, p7) + poly3 = Polygon(p1, p2, p5) + assert Union(l1, l2).equals(l1) + assert Intersection(l1, l2).equals(l1) + assert Intersection(l1, l4) == FiniteSet(Point(1,1)) + assert Intersection(Union(l1, l4), l3) == FiniteSet(Point(Rational(-1, 3), Rational(-1, 3)), Point(5, 1)) + assert Intersection(l1, FiniteSet(Point(7,-7))) == EmptySet + assert Intersection(Circle(Point(0,0), 3), Line(p1,p2)) == FiniteSet(Point(-3,0), Point(3,0)) + assert Intersection(l1, FiniteSet(p1)) == FiniteSet(p1) + assert Union(l1, FiniteSet(p1)) == l1 + + fs = FiniteSet(Point(Rational(1, 3), 1), Point(Rational(2, 3), 0), Point(Rational(9, 5), Rational(1, 5)), Point(Rational(7, 3), 1)) + # test the intersection of polygons + assert Intersection(poly1, poly2) == fs + # make sure if we union polygons with subsets, the subsets go away + assert Union(poly1, poly2, fs) == Union(poly1, poly2) + # make sure that if we union with a FiniteSet that isn't a subset, + # that the points in the intersection stop being listed + assert Union(poly1, FiniteSet(Point(0,0), Point(3,5))) == Union(poly1, FiniteSet(Point(3,5))) + # intersect two polygons that share an edge + assert Intersection(poly1, poly3) == Union(FiniteSet(Point(Rational(3, 2), 1), Point(2, 1)), Segment(Point(0, 0), Point(1, 0))) diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_line.py b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_line.py new file mode 100644 index 0000000000000000000000000000000000000000..5158ec05ab414020fbbe2681a2658454dd15b6eb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_line.py @@ -0,0 +1,861 @@ +from sympy.core.numbers import (Float, Rational, oo, pi) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (acos, cos, sin) +from sympy.sets import EmptySet +from sympy.simplify.simplify import simplify +from sympy.functions.elementary.trigonometric import tan +from sympy.geometry import (Circle, GeometryError, Line, Point, Ray, + Segment, Triangle, intersection, Point3D, Line3D, Ray3D, Segment3D, + Point2D, Line2D, Plane) +from sympy.geometry.line import Undecidable +from sympy.geometry.polygon import _asa as asa +from sympy.utilities.iterables import cartes +from sympy.testing.pytest import raises, warns + + +x = Symbol('x', real=True) +y = Symbol('y', real=True) +z = Symbol('z', real=True) +k = Symbol('k', real=True) +x1 = Symbol('x1', real=True) +y1 = Symbol('y1', real=True) +t = Symbol('t', real=True) +a, b = symbols('a,b', real=True) +m = symbols('m', real=True) + + +def test_object_from_equation(): + from sympy.abc import x, y, a, b + assert Line(3*x + y + 18) == Line2D(Point2D(0, -18), Point2D(1, -21)) + assert Line(3*x + 5 * y + 1) == Line2D( + Point2D(0, Rational(-1, 5)), Point2D(1, Rational(-4, 5))) + assert Line(3*a + b + 18, x="a", y="b") == Line2D( + Point2D(0, -18), Point2D(1, -21)) + assert Line(3*x + y) == Line2D(Point2D(0, 0), Point2D(1, -3)) + assert Line(x + y) == Line2D(Point2D(0, 0), Point2D(1, -1)) + assert Line(Eq(3*a + b, -18), x="a", y=b) == Line2D( + Point2D(0, -18), Point2D(1, -21)) + # issue 22361 + assert Line(x - 1) == Line2D(Point2D(1, 0), Point2D(1, 1)) + assert Line(2*x - 2, y=x) == Line2D(Point2D(0, 1), Point2D(1, 1)) + assert Line(y) == Line2D(Point2D(0, 0), Point2D(1, 0)) + assert Line(2*y, x=y) == Line2D(Point2D(0, 0), Point2D(0, 1)) + assert Line(y, x=y) == Line2D(Point2D(0, 0), Point2D(0, 1)) + raises(ValueError, lambda: Line(x / y)) + raises(ValueError, lambda: Line(a / b, x='a', y='b')) + raises(ValueError, lambda: Line(y / x)) + raises(ValueError, lambda: Line(b / a, x='a', y='b')) + raises(ValueError, lambda: Line((x + 1)**2 + y)) + + +def feq(a, b): + """Test if two floating point values are 'equal'.""" + t_float = Float("1.0E-10") + return -t_float < a - b < t_float + + +def test_angle_between(): + a = Point(1, 2, 3, 4) + b = a.orthogonal_direction + o = a.origin + assert feq(Line.angle_between(Line(Point(0, 0), Point(1, 1)), + Line(Point(0, 0), Point(5, 0))).evalf(), pi.evalf() / 4) + assert Line(a, o).angle_between(Line(b, o)) == pi / 2 + z = Point3D(0, 0, 0) + assert Line3D.angle_between(Line3D(z, Point3D(1, 1, 1)), + Line3D(z, Point3D(5, 0, 0))) == acos(sqrt(3) / 3) + # direction of points is used to determine angle + assert Line3D.angle_between(Line3D(z, Point3D(1, 1, 1)), + Line3D(Point3D(5, 0, 0), z)) == acos(-sqrt(3) / 3) + + +def test_closing_angle(): + a = Ray((0, 0), angle=0) + b = Ray((1, 2), angle=pi/2) + assert a.closing_angle(b) == -pi/2 + assert b.closing_angle(a) == pi/2 + assert a.closing_angle(a) == 0 + + +def test_smallest_angle(): + a = Line(Point(1, 1), Point(1, 2)) + b = Line(Point(1, 1),Point(2, 3)) + assert a.smallest_angle_between(b) == acos(2*sqrt(5)/5) + + +def test_svg(): + a = Line(Point(1, 1),Point(1, 2)) + assert a._svg() == '' + a = Segment(Point(1, 0),Point(1, 1)) + assert a._svg() == '' + a = Ray(Point(2, 3), Point(3, 5)) + assert a._svg() == '' + + +def test_arbitrary_point(): + l1 = Line3D(Point3D(0, 0, 0), Point3D(1, 1, 1)) + l2 = Line(Point(x1, x1), Point(y1, y1)) + assert l2.arbitrary_point() in l2 + assert Ray((1, 1), angle=pi / 4).arbitrary_point() == \ + Point(t + 1, t + 1) + assert Segment((1, 1), (2, 3)).arbitrary_point() == Point(1 + t, 1 + 2 * t) + assert l1.perpendicular_segment(l1.arbitrary_point()) == l1.arbitrary_point() + assert Ray3D((1, 1, 1), direction_ratio=[1, 2, 3]).arbitrary_point() == \ + Point3D(t + 1, 2 * t + 1, 3 * t + 1) + assert Segment3D(Point3D(0, 0, 0), Point3D(1, 1, 1)).midpoint == \ + Point3D(S.Half, S.Half, S.Half) + assert Segment3D(Point3D(x1, x1, x1), Point3D(y1, y1, y1)).length == sqrt(3) * sqrt((x1 - y1) ** 2) + assert Segment3D((1, 1, 1), (2, 3, 4)).arbitrary_point() == \ + Point3D(t + 1, 2 * t + 1, 3 * t + 1) + raises(ValueError, (lambda: Line((x, 1), (2, 3)).arbitrary_point(x))) + + +def test_are_concurrent_2d(): + l1 = Line(Point(0, 0), Point(1, 1)) + l2 = Line(Point(x1, x1), Point(x1, 1 + x1)) + assert Line.are_concurrent(l1) is False + assert Line.are_concurrent(l1, l2) + assert Line.are_concurrent(l1, l1, l1, l2) + assert Line.are_concurrent(l1, l2, Line(Point(5, x1), Point(Rational(-3, 5), x1))) + assert Line.are_concurrent(l1, Line(Point(0, 0), Point(-x1, x1)), l2) is False + + +def test_are_concurrent_3d(): + p1 = Point3D(0, 0, 0) + l1 = Line(p1, Point3D(1, 1, 1)) + parallel_1 = Line3D(Point3D(0, 0, 0), Point3D(1, 0, 0)) + parallel_2 = Line3D(Point3D(0, 1, 0), Point3D(1, 1, 0)) + assert Line3D.are_concurrent(l1) is False + assert Line3D.are_concurrent(l1, Line(Point3D(x1, x1, x1), Point3D(y1, y1, y1))) is False + assert Line3D.are_concurrent(l1, Line3D(p1, Point3D(x1, x1, x1)), + Line(Point3D(x1, x1, x1), Point3D(x1, 1 + x1, 1))) is True + assert Line3D.are_concurrent(parallel_1, parallel_2) is False + + +def test_arguments(): + """Functions accepting `Point` objects in `geometry` + should also accept tuples, lists, and generators and + automatically convert them to points.""" + from sympy.utilities.iterables import subsets + + singles2d = ((1, 2), [1, 3], Point(1, 5)) + doubles2d = subsets(singles2d, 2) + l2d = Line(Point2D(1, 2), Point2D(2, 3)) + singles3d = ((1, 2, 3), [1, 2, 4], Point(1, 2, 6)) + doubles3d = subsets(singles3d, 2) + l3d = Line(Point3D(1, 2, 3), Point3D(1, 1, 2)) + singles4d = ((1, 2, 3, 4), [1, 2, 3, 5], Point(1, 2, 3, 7)) + doubles4d = subsets(singles4d, 2) + l4d = Line(Point(1, 2, 3, 4), Point(2, 2, 2, 2)) + # test 2D + test_single = ['contains', 'distance', 'equals', 'parallel_line', 'perpendicular_line', 'perpendicular_segment', + 'projection', 'intersection'] + for p in doubles2d: + Line2D(*p) + for func in test_single: + for p in singles2d: + getattr(l2d, func)(p) + # test 3D + for p in doubles3d: + Line3D(*p) + for func in test_single: + for p in singles3d: + getattr(l3d, func)(p) + # test 4D + for p in doubles4d: + Line(*p) + for func in test_single: + for p in singles4d: + getattr(l4d, func)(p) + + +def test_basic_properties_2d(): + p1 = Point(0, 0) + p2 = Point(1, 1) + p10 = Point(2000, 2000) + p_r3 = Ray(p1, p2).random_point() + p_r4 = Ray(p2, p1).random_point() + + l1 = Line(p1, p2) + l3 = Line(Point(x1, x1), Point(x1, 1 + x1)) + l4 = Line(p1, Point(1, 0)) + + r1 = Ray(p1, Point(0, 1)) + r2 = Ray(Point(0, 1), p1) + + s1 = Segment(p1, p10) + p_s1 = s1.random_point() + + assert Line((1, 1), slope=1) == Line((1, 1), (2, 2)) + assert Line((1, 1), slope=oo) == Line((1, 1), (1, 2)) + assert Line((1, 1), slope=oo).bounds == (1, 1, 1, 2) + assert Line((1, 1), slope=-oo) == Line((1, 1), (1, 2)) + assert Line(p1, p2).scale(2, 1) == Line(p1, Point(2, 1)) + assert Line(p1, p2) == Line(p1, p2) + assert Line(p1, p2) != Line(p2, p1) + assert l1 != Line(Point(x1, x1), Point(y1, y1)) + assert l1 != l3 + assert Line(p1, p10) != Line(p10, p1) + assert Line(p1, p10) != p1 + assert p1 in l1 # is p1 on the line l1? + assert p1 not in l3 + assert s1 in Line(p1, p10) + assert Ray(Point(0, 0), Point(0, 1)) in Ray(Point(0, 0), Point(0, 2)) + assert Ray(Point(0, 0), Point(0, 2)) in Ray(Point(0, 0), Point(0, 1)) + assert Ray(Point(0, 0), Point(0, 2)).xdirection == S.Zero + assert Ray(Point(0, 0), Point(1, 2)).xdirection == S.Infinity + assert Ray(Point(0, 0), Point(-1, 2)).xdirection == S.NegativeInfinity + assert Ray(Point(0, 0), Point(2, 0)).ydirection == S.Zero + assert Ray(Point(0, 0), Point(2, 2)).ydirection == S.Infinity + assert Ray(Point(0, 0), Point(2, -2)).ydirection == S.NegativeInfinity + assert (r1 in s1) is False + assert Segment(p1, p2) in s1 + assert Ray(Point(x1, x1), Point(x1, 1 + x1)) != Ray(p1, Point(-1, 5)) + assert Segment(p1, p2).midpoint == Point(S.Half, S.Half) + assert Segment(p1, Point(-x1, x1)).length == sqrt(2 * (x1 ** 2)) + + assert l1.slope == 1 + assert l3.slope is oo + assert l4.slope == 0 + assert Line(p1, Point(0, 1)).slope is oo + assert Line(r1.source, r1.random_point()).slope == r1.slope + assert Line(r2.source, r2.random_point()).slope == r2.slope + assert Segment(Point(0, -1), Segment(p1, Point(0, 1)).random_point()).slope == Segment(p1, Point(0, 1)).slope + + assert l4.coefficients == (0, 1, 0) + assert Line((-x, x), (-x + 1, x - 1)).coefficients == (1, 1, 0) + assert Line(p1, Point(0, 1)).coefficients == (1, 0, 0) + # issue 7963 + r = Ray((0, 0), angle=x) + assert r.subs(x, 3 * pi / 4) == Ray((0, 0), (-1, 1)) + assert r.subs(x, 5 * pi / 4) == Ray((0, 0), (-1, -1)) + assert r.subs(x, -pi / 4) == Ray((0, 0), (1, -1)) + assert r.subs(x, pi / 2) == Ray((0, 0), (0, 1)) + assert r.subs(x, -pi / 2) == Ray((0, 0), (0, -1)) + + for ind in range(0, 5): + assert l3.random_point() in l3 + + assert p_r3.x >= p1.x and p_r3.y >= p1.y + assert p_r4.x <= p2.x and p_r4.y <= p2.y + assert p1.x <= p_s1.x <= p10.x and p1.y <= p_s1.y <= p10.y + assert hash(s1) != hash(Segment(p10, p1)) + + assert s1.plot_interval() == [t, 0, 1] + assert Line(p1, p10).plot_interval() == [t, -5, 5] + assert Ray((0, 0), angle=pi / 4).plot_interval() == [t, 0, 10] + + +def test_basic_properties_3d(): + p1 = Point3D(0, 0, 0) + p2 = Point3D(1, 1, 1) + p3 = Point3D(x1, x1, x1) + p5 = Point3D(x1, 1 + x1, 1) + + l1 = Line3D(p1, p2) + l3 = Line3D(p3, p5) + + r1 = Ray3D(p1, Point3D(-1, 5, 0)) + r3 = Ray3D(p1, p2) + + s1 = Segment3D(p1, p2) + + assert Line3D((1, 1, 1), direction_ratio=[2, 3, 4]) == Line3D(Point3D(1, 1, 1), Point3D(3, 4, 5)) + assert Line3D((1, 1, 1), direction_ratio=[1, 5, 7]) == Line3D(Point3D(1, 1, 1), Point3D(2, 6, 8)) + assert Line3D((1, 1, 1), direction_ratio=[1, 2, 3]) == Line3D(Point3D(1, 1, 1), Point3D(2, 3, 4)) + assert Line3D(Point3D(0, 0, 0), Point3D(1, 0, 0)).direction_cosine == [1, 0, 0] + assert Line3D(Line3D(p1, Point3D(0, 1, 0))) == Line3D(p1, Point3D(0, 1, 0)) + assert Ray3D(Line3D(Point3D(0, 0, 0), Point3D(1, 0, 0))) == Ray3D(p1, Point3D(1, 0, 0)) + assert Line3D(p1, p2) != Line3D(p2, p1) + assert l1 != l3 + assert l1 != Line3D(p3, Point3D(y1, y1, y1)) + assert r3 != r1 + assert Ray3D(Point3D(0, 0, 0), Point3D(1, 1, 1)) in Ray3D(Point3D(0, 0, 0), Point3D(2, 2, 2)) + assert Ray3D(Point3D(0, 0, 0), Point3D(2, 2, 2)) in Ray3D(Point3D(0, 0, 0), Point3D(1, 1, 1)) + assert Ray3D(Point3D(0, 0, 0), Point3D(2, 2, 2)).xdirection == S.Infinity + assert Ray3D(Point3D(0, 0, 0), Point3D(2, 2, 2)).ydirection == S.Infinity + assert Ray3D(Point3D(0, 0, 0), Point3D(2, 2, 2)).zdirection == S.Infinity + assert Ray3D(Point3D(0, 0, 0), Point3D(-2, 2, 2)).xdirection == S.NegativeInfinity + assert Ray3D(Point3D(0, 0, 0), Point3D(2, -2, 2)).ydirection == S.NegativeInfinity + assert Ray3D(Point3D(0, 0, 0), Point3D(2, 2, -2)).zdirection == S.NegativeInfinity + assert Ray3D(Point3D(0, 0, 0), Point3D(0, 2, 2)).xdirection == S.Zero + assert Ray3D(Point3D(0, 0, 0), Point3D(2, 0, 2)).ydirection == S.Zero + assert Ray3D(Point3D(0, 0, 0), Point3D(2, 2, 0)).zdirection == S.Zero + assert p1 in l1 + assert p1 not in l3 + + assert l1.direction_ratio == [1, 1, 1] + + assert s1.midpoint == Point3D(S.Half, S.Half, S.Half) + # Test zdirection + assert Ray3D(p1, Point3D(0, 0, -1)).zdirection is S.NegativeInfinity + + +def test_contains(): + p1 = Point(0, 0) + + r = Ray(p1, Point(4, 4)) + r1 = Ray3D(p1, Point3D(0, 0, -1)) + r2 = Ray3D(p1, Point3D(0, 1, 0)) + r3 = Ray3D(p1, Point3D(0, 0, 1)) + + l = Line(Point(0, 1), Point(3, 4)) + # Segment contains + assert Point(0, (a + b) / 2) in Segment((0, a), (0, b)) + assert Point((a + b) / 2, 0) in Segment((a, 0), (b, 0)) + assert Point3D(0, 1, 0) in Segment3D((0, 1, 0), (0, 1, 0)) + assert Point3D(1, 0, 0) in Segment3D((1, 0, 0), (1, 0, 0)) + assert Segment3D(Point3D(0, 0, 0), Point3D(1, 0, 0)).contains([]) is True + assert Segment3D(Point3D(0, 0, 0), Point3D(1, 0, 0)).contains( + Segment3D(Point3D(2, 2, 2), Point3D(3, 2, 2))) is False + # Line contains + assert l.contains(Point(0, 1)) is True + assert l.contains((0, 1)) is True + assert l.contains((0, 0)) is False + # Ray contains + assert r.contains(p1) is True + assert r.contains((1, 1)) is True + assert r.contains((1, 3)) is False + assert r.contains(Segment((1, 1), (2, 2))) is True + assert r.contains(Segment((1, 2), (2, 5))) is False + assert r.contains(Ray((2, 2), (3, 3))) is True + assert r.contains(Ray((2, 2), (3, 5))) is False + assert r1.contains(Segment3D(p1, Point3D(0, 0, -10))) is True + assert r1.contains(Segment3D(Point3D(1, 1, 1), Point3D(2, 2, 2))) is False + assert r2.contains(Point3D(0, 0, 0)) is True + assert r3.contains(Point3D(0, 0, 0)) is True + assert Ray3D(Point3D(1, 1, 1), Point3D(1, 0, 0)).contains([]) is False + assert Line3D((0, 0, 0), (x, y, z)).contains((2 * x, 2 * y, 2 * z)) + with warns(UserWarning, test_stacklevel=False): + assert Line3D(p1, Point3D(0, 1, 0)).contains(Point(1.0, 1.0)) is False + + with warns(UserWarning, test_stacklevel=False): + assert r3.contains(Point(1.0, 1.0)) is False + + +def test_contains_nonreal_symbols(): + u, v, w, z = symbols('u, v, w, z') + l = Segment(Point(u, w), Point(v, z)) + p = Point(u*Rational(2, 3) + v/3, w*Rational(2, 3) + z/3) + assert l.contains(p) + + +def test_distance_2d(): + p1 = Point(0, 0) + p2 = Point(1, 1) + half = S.Half + + s1 = Segment(Point(0, 0), Point(1, 1)) + s2 = Segment(Point(half, half), Point(1, 0)) + + r = Ray(p1, p2) + + assert s1.distance(Point(0, 0)) == 0 + assert s1.distance((0, 0)) == 0 + assert s2.distance(Point(0, 0)) == 2 ** half / 2 + assert s2.distance(Point(Rational(3) / 2, Rational(3) / 2)) == 2 ** half + assert Line(p1, p2).distance(Point(-1, 1)) == sqrt(2) + assert Line(p1, p2).distance(Point(1, -1)) == sqrt(2) + assert Line(p1, p2).distance(Point(2, 2)) == 0 + assert Line(p1, p2).distance((-1, 1)) == sqrt(2) + assert Line((0, 0), (0, 1)).distance(p1) == 0 + assert Line((0, 0), (0, 1)).distance(p2) == 1 + assert Line((0, 0), (1, 0)).distance(p1) == 0 + assert Line((0, 0), (1, 0)).distance(p2) == 1 + assert r.distance(Point(-1, -1)) == sqrt(2) + assert r.distance(Point(1, 1)) == 0 + assert r.distance(Point(-1, 1)) == sqrt(2) + assert Ray((1, 1), (2, 2)).distance(Point(1.5, 3)) == 3 * sqrt(2) / 4 + assert r.distance((1, 1)) == 0 + + +def test_dimension_normalization(): + with warns(UserWarning, test_stacklevel=False): + assert Ray((1, 1), (2, 1, 2)) == Ray((1, 1, 0), (2, 1, 2)) + + +def test_distance_3d(): + p1, p2 = Point3D(0, 0, 0), Point3D(1, 1, 1) + p3 = Point3D(Rational(3) / 2, Rational(3) / 2, Rational(3) / 2) + + s1 = Segment3D(Point3D(0, 0, 0), Point3D(1, 1, 1)) + s2 = Segment3D(Point3D(S.Half, S.Half, S.Half), Point3D(1, 0, 1)) + + r = Ray3D(p1, p2) + + assert s1.distance(p1) == 0 + assert s2.distance(p1) == sqrt(3) / 2 + assert s2.distance(p3) == 2 * sqrt(6) / 3 + assert s1.distance((0, 0, 0)) == 0 + assert s2.distance((0, 0, 0)) == sqrt(3) / 2 + assert s1.distance(p1) == 0 + assert s2.distance(p1) == sqrt(3) / 2 + assert s2.distance(p3) == 2 * sqrt(6) / 3 + assert s1.distance((0, 0, 0)) == 0 + assert s2.distance((0, 0, 0)) == sqrt(3) / 2 + # Line to point + assert Line3D(p1, p2).distance(Point3D(-1, 1, 1)) == 2 * sqrt(6) / 3 + assert Line3D(p1, p2).distance(Point3D(1, -1, 1)) == 2 * sqrt(6) / 3 + assert Line3D(p1, p2).distance(Point3D(2, 2, 2)) == 0 + assert Line3D(p1, p2).distance((2, 2, 2)) == 0 + assert Line3D(p1, p2).distance((1, -1, 1)) == 2 * sqrt(6) / 3 + assert Line3D((0, 0, 0), (0, 1, 0)).distance(p1) == 0 + assert Line3D((0, 0, 0), (0, 1, 0)).distance(p2) == sqrt(2) + assert Line3D((0, 0, 0), (1, 0, 0)).distance(p1) == 0 + assert Line3D((0, 0, 0), (1, 0, 0)).distance(p2) == sqrt(2) + # Line to line + assert Line3D((0, 0, 0), (1, 0, 0)).distance(Line3D((0, 0, 0), (0, 1, 2))) == 0 + assert Line3D((0, 0, 0), (1, 0, 0)).distance(Line3D((0, 0, 0), (1, 0, 0))) == 0 + assert Line3D((0, 0, 0), (1, 0, 0)).distance(Line3D((10, 0, 0), (10, 1, 2))) == 0 + assert Line3D((0, 0, 0), (1, 0, 0)).distance(Line3D((0, 1, 0), (0, 1, 1))) == 1 + # Line to plane + assert Line3D((0, 0, 0), (1, 0, 0)).distance(Plane((2, 0, 0), (0, 0, 1))) == 0 + assert Line3D((0, 0, 0), (1, 0, 0)).distance(Plane((0, 1, 0), (0, 1, 0))) == 1 + assert Line3D((0, 0, 0), (1, 0, 0)).distance(Plane((1, 1, 3), (1, 0, 0))) == 0 + # Ray to point + assert r.distance(Point3D(-1, -1, -1)) == sqrt(3) + assert r.distance(Point3D(1, 1, 1)) == 0 + assert r.distance((-1, -1, -1)) == sqrt(3) + assert r.distance((1, 1, 1)) == 0 + assert Ray3D((0, 0, 0), (1, 1, 2)).distance((-1, -1, 2)) == 4 * sqrt(3) / 3 + assert Ray3D((1, 1, 1), (2, 2, 2)).distance(Point3D(1.5, -3, -1)) == Rational(9) / 2 + assert Ray3D((1, 1, 1), (2, 2, 2)).distance(Point3D(1.5, 3, 1)) == sqrt(78) / 6 + + +def test_equals(): + p1 = Point(0, 0) + p2 = Point(1, 1) + + l1 = Line(p1, p2) + l2 = Line((0, 5), slope=m) + l3 = Line(Point(x1, x1), Point(x1, 1 + x1)) + + assert l1.perpendicular_line(p1.args).equals(Line(Point(0, 0), Point(1, -1))) + assert l1.perpendicular_line(p1).equals(Line(Point(0, 0), Point(1, -1))) + assert Line(Point(x1, x1), Point(y1, y1)).parallel_line(Point(-x1, x1)). \ + equals(Line(Point(-x1, x1), Point(-y1, 2 * x1 - y1))) + assert l3.parallel_line(p1.args).equals(Line(Point(0, 0), Point(0, -1))) + assert l3.parallel_line(p1).equals(Line(Point(0, 0), Point(0, -1))) + assert (l2.distance(Point(2, 3)) - 2 * abs(m + 1) / sqrt(m ** 2 + 1)).equals(0) + assert Line3D(p1, Point3D(0, 1, 0)).equals(Point(1.0, 1.0)) is False + assert Line3D(Point3D(0, 0, 0), Point3D(1, 0, 0)).equals(Line3D(Point3D(-5, 0, 0), Point3D(-1, 0, 0))) is True + assert Line3D(Point3D(0, 0, 0), Point3D(1, 0, 0)).equals(Line3D(p1, Point3D(0, 1, 0))) is False + assert Ray3D(p1, Point3D(0, 0, -1)).equals(Point(1.0, 1.0)) is False + assert Ray3D(p1, Point3D(0, 0, -1)).equals(Ray3D(p1, Point3D(0, 0, -1))) is True + assert Line3D((0, 0), (t, t)).perpendicular_line(Point(0, 1, 0)).equals( + Line3D(Point3D(0, 1, 0), Point3D(S.Half, S.Half, 0))) + assert Line3D((0, 0), (t, t)).perpendicular_segment(Point(0, 1, 0)).equals(Segment3D((0, 1), (S.Half, S.Half))) + assert Line3D(p1, Point3D(0, 1, 0)).equals(Point(1.0, 1.0)) is False + + +def test_equation(): + p1 = Point(0, 0) + p2 = Point(1, 1) + l1 = Line(p1, p2) + l3 = Line(Point(x1, x1), Point(x1, 1 + x1)) + + assert simplify(l1.equation()) in (x - y, y - x) + assert simplify(l3.equation()) in (x - x1, x1 - x) + assert simplify(l1.equation()) in (x - y, y - x) + assert simplify(l3.equation()) in (x - x1, x1 - x) + + assert Line(p1, Point(1, 0)).equation(x=x, y=y) == y + assert Line(p1, Point(0, 1)).equation() == x + assert Line(Point(2, 0), Point(2, 1)).equation() == x - 2 + assert Line(p2, Point(2, 1)).equation() == y - 1 + + assert Line3D(Point(x1, x1, x1), Point(y1, y1, y1) + ).equation() == (-x + y, -x + z) + assert Line3D(Point(1, 2, 3), Point(2, 3, 4) + ).equation() == (-x + y - 1, -x + z - 2) + assert Line3D(Point(1, 2, 3), Point(1, 3, 4) + ).equation() == (x - 1, -y + z - 1) + assert Line3D(Point(1, 2, 3), Point(2, 2, 4) + ).equation() == (y - 2, -x + z - 2) + assert Line3D(Point(1, 2, 3), Point(2, 3, 3) + ).equation() == (-x + y - 1, z - 3) + assert Line3D(Point(1, 2, 3), Point(1, 2, 4) + ).equation() == (x - 1, y - 2) + assert Line3D(Point(1, 2, 3), Point(1, 3, 3) + ).equation() == (x - 1, z - 3) + assert Line3D(Point(1, 2, 3), Point(2, 2, 3) + ).equation() == (y - 2, z - 3) + + +def test_intersection_2d(): + p1 = Point(0, 0) + p2 = Point(1, 1) + p3 = Point(x1, x1) + p4 = Point(y1, y1) + + l1 = Line(p1, p2) + l3 = Line(Point(0, 0), Point(3, 4)) + + r1 = Ray(Point(1, 1), Point(2, 2)) + r2 = Ray(Point(0, 0), Point(3, 4)) + r4 = Ray(p1, p2) + r6 = Ray(Point(0, 1), Point(1, 2)) + r7 = Ray(Point(0.5, 0.5), Point(1, 1)) + + s1 = Segment(p1, p2) + s2 = Segment(Point(0.25, 0.25), Point(0.5, 0.5)) + s3 = Segment(Point(0, 0), Point(3, 4)) + + assert intersection(l1, p1) == [p1] + assert intersection(l1, Point(x1, 1 + x1)) == [] + assert intersection(l1, Line(p3, p4)) in [[l1], [Line(p3, p4)]] + assert intersection(l1, l1.parallel_line(Point(x1, 1 + x1))) == [] + assert intersection(l3, l3) == [l3] + assert intersection(l3, r2) == [r2] + assert intersection(l3, s3) == [s3] + assert intersection(s3, l3) == [s3] + assert intersection(Segment(Point(-10, 10), Point(10, 10)), Segment(Point(-5, -5), Point(-5, 5))) == [] + assert intersection(r2, l3) == [r2] + assert intersection(r1, Ray(Point(2, 2), Point(0, 0))) == [Segment(Point(1, 1), Point(2, 2))] + assert intersection(r1, Ray(Point(1, 1), Point(-1, -1))) == [Point(1, 1)] + assert intersection(r1, Segment(Point(0, 0), Point(2, 2))) == [Segment(Point(1, 1), Point(2, 2))] + + assert r4.intersection(s2) == [s2] + assert r4.intersection(Segment(Point(2, 3), Point(3, 4))) == [] + assert r4.intersection(Segment(Point(-1, -1), Point(0.5, 0.5))) == [Segment(p1, Point(0.5, 0.5))] + assert r4.intersection(Ray(p2, p1)) == [s1] + assert Ray(p2, p1).intersection(r6) == [] + assert r4.intersection(r7) == r7.intersection(r4) == [r7] + assert Ray3D((0, 0), (3, 0)).intersection(Ray3D((1, 0), (3, 0))) == [Ray3D((1, 0), (3, 0))] + assert Ray3D((1, 0), (3, 0)).intersection(Ray3D((0, 0), (3, 0))) == [Ray3D((1, 0), (3, 0))] + assert Ray(Point(0, 0), Point(0, 4)).intersection(Ray(Point(0, 1), Point(0, -1))) == \ + [Segment(Point(0, 0), Point(0, 1))] + + assert Segment3D((0, 0), (3, 0)).intersection( + Segment3D((1, 0), (2, 0))) == [Segment3D((1, 0), (2, 0))] + assert Segment3D((1, 0), (2, 0)).intersection( + Segment3D((0, 0), (3, 0))) == [Segment3D((1, 0), (2, 0))] + assert Segment3D((0, 0), (3, 0)).intersection( + Segment3D((3, 0), (4, 0))) == [Point3D((3, 0))] + assert Segment3D((0, 0), (3, 0)).intersection( + Segment3D((2, 0), (5, 0))) == [Segment3D((2, 0), (3, 0))] + assert Segment3D((0, 0), (3, 0)).intersection( + Segment3D((-2, 0), (1, 0))) == [Segment3D((0, 0), (1, 0))] + assert Segment3D((0, 0), (3, 0)).intersection( + Segment3D((-2, 0), (0, 0))) == [Point3D(0, 0)] + assert s1.intersection(Segment(Point(1, 1), Point(2, 2))) == [Point(1, 1)] + assert s1.intersection(Segment(Point(0.5, 0.5), Point(1.5, 1.5))) == [Segment(Point(0.5, 0.5), p2)] + assert s1.intersection(Segment(Point(4, 4), Point(5, 5))) == [] + assert s1.intersection(Segment(Point(-1, -1), p1)) == [p1] + assert s1.intersection(Segment(Point(-1, -1), Point(0.5, 0.5))) == [Segment(p1, Point(0.5, 0.5))] + assert s1.intersection(Line(Point(1, 0), Point(2, 1))) == [] + assert s1.intersection(s2) == [s2] + assert s2.intersection(s1) == [s2] + + assert asa(120, 8, 52) == \ + Triangle( + Point(0, 0), + Point(8, 0), + Point(-4 * cos(19 * pi / 90) / sin(2 * pi / 45), + 4 * sqrt(3) * cos(19 * pi / 90) / sin(2 * pi / 45))) + assert Line((0, 0), (1, 1)).intersection(Ray((1, 0), (1, 2))) == [Point(1, 1)] + assert Line((0, 0), (1, 1)).intersection(Segment((1, 0), (1, 2))) == [Point(1, 1)] + assert Ray((0, 0), (1, 1)).intersection(Ray((1, 0), (1, 2))) == [Point(1, 1)] + assert Ray((0, 0), (1, 1)).intersection(Segment((1, 0), (1, 2))) == [Point(1, 1)] + assert Ray((0, 0), (10, 10)).contains(Segment((1, 1), (2, 2))) is True + assert Segment((1, 1), (2, 2)) in Line((0, 0), (10, 10)) + assert s1.intersection(Ray((1, 1), (4, 4))) == [Point(1, 1)] + + # This test is disabled because it hangs after rref changes which simplify + # intermediate results and return a different representation from when the + # test was written. + # # 16628 - this should be fast + # p0 = Point2D(Rational(249, 5), Rational(497999, 10000)) + # p1 = Point2D((-58977084786*sqrt(405639795226) + 2030690077184193 + + # 20112207807*sqrt(630547164901) + 99600*sqrt(255775022850776494562626)) + # /(2000*sqrt(255775022850776494562626) + 1991998000*sqrt(405639795226) + # + 1991998000*sqrt(630547164901) + 1622561172902000), + # (-498000*sqrt(255775022850776494562626) - 995999*sqrt(630547164901) + + # 90004251917891999 + + # 496005510002*sqrt(405639795226))/(10000*sqrt(255775022850776494562626) + # + 9959990000*sqrt(405639795226) + 9959990000*sqrt(630547164901) + + # 8112805864510000)) + # p2 = Point2D(Rational(497, 10), Rational(-497, 10)) + # p3 = Point2D(Rational(-497, 10), Rational(-497, 10)) + # l = Line(p0, p1) + # s = Segment(p2, p3) + # n = (-52673223862*sqrt(405639795226) - 15764156209307469 - + # 9803028531*sqrt(630547164901) + + # 33200*sqrt(255775022850776494562626)) + # d = sqrt(405639795226) + 315274080450 + 498000*sqrt( + # 630547164901) + sqrt(255775022850776494562626) + # assert intersection(l, s) == [ + # Point2D(n/d*Rational(3, 2000), Rational(-497, 10))] + + +def test_line_intersection(): + # see also test_issue_11238 in test_matrices.py + x0 = tan(pi*Rational(13, 45)) + x1 = sqrt(3) + x2 = x0**2 + x, y = [8*x0/(x0 + x1), (24*x0 - 8*x1*x2)/(x2 - 3)] + assert Line(Point(0, 0), Point(1, -sqrt(3))).contains(Point(x, y)) is True + + +def test_intersection_3d(): + p1 = Point3D(0, 0, 0) + p2 = Point3D(1, 1, 1) + + l1 = Line3D(p1, p2) + l2 = Line3D(Point3D(0, 0, 0), Point3D(3, 4, 0)) + + r1 = Ray3D(Point3D(1, 1, 1), Point3D(2, 2, 2)) + r2 = Ray3D(Point3D(0, 0, 0), Point3D(3, 4, 0)) + + s1 = Segment3D(Point3D(0, 0, 0), Point3D(3, 4, 0)) + + assert intersection(l1, p1) == [p1] + assert intersection(l1, Point3D(x1, 1 + x1, 1)) == [] + assert intersection(l1, l1.parallel_line(p1)) == [Line3D(Point3D(0, 0, 0), Point3D(1, 1, 1))] + assert intersection(l2, r2) == [r2] + assert intersection(l2, s1) == [s1] + assert intersection(r2, l2) == [r2] + assert intersection(r1, Ray3D(Point3D(1, 1, 1), Point3D(-1, -1, -1))) == [Point3D(1, 1, 1)] + assert intersection(r1, Segment3D(Point3D(0, 0, 0), Point3D(2, 2, 2))) == [ + Segment3D(Point3D(1, 1, 1), Point3D(2, 2, 2))] + assert intersection(Ray3D(Point3D(1, 0, 0), Point3D(-1, 0, 0)), Ray3D(Point3D(0, 1, 0), Point3D(0, -1, 0))) \ + == [Point3D(0, 0, 0)] + assert intersection(r1, Ray3D(Point3D(2, 2, 2), Point3D(0, 0, 0))) == \ + [Segment3D(Point3D(1, 1, 1), Point3D(2, 2, 2))] + assert intersection(s1, r2) == [s1] + + assert Line3D(Point3D(4, 0, 1), Point3D(0, 4, 1)).intersection(Line3D(Point3D(0, 0, 1), Point3D(4, 4, 1))) == \ + [Point3D(2, 2, 1)] + assert Line3D((0, 1, 2), (0, 2, 3)).intersection(Line3D((0, 1, 2), (0, 1, 1))) == [Point3D(0, 1, 2)] + assert Line3D((0, 0), (t, t)).intersection(Line3D((0, 1), (t, t))) == \ + [Point3D(t, t)] + + assert Ray3D(Point3D(0, 0, 0), Point3D(0, 4, 0)).intersection(Ray3D(Point3D(0, 1, 1), Point3D(0, -1, 1))) == [] + + +def test_is_parallel(): + p1 = Point3D(0, 0, 0) + p2 = Point3D(1, 1, 1) + p3 = Point3D(x1, x1, x1) + + l2 = Line(Point(x1, x1), Point(y1, y1)) + l2_1 = Line(Point(x1, x1), Point(x1, 1 + x1)) + + assert Line.is_parallel(Line(Point(0, 0), Point(1, 1)), l2) + assert Line.is_parallel(l2, Line(Point(x1, x1), Point(x1, 1 + x1))) is False + assert Line.is_parallel(l2, l2.parallel_line(Point(-x1, x1))) + assert Line.is_parallel(l2_1, l2_1.parallel_line(Point(0, 0))) + assert Line3D(p1, p2).is_parallel(Line3D(p1, p2)) # same as in 2D + assert Line3D(Point3D(4, 0, 1), Point3D(0, 4, 1)).is_parallel(Line3D(Point3D(0, 0, 1), Point3D(4, 4, 1))) is False + assert Line3D(p1, p2).parallel_line(p3) == Line3D(Point3D(x1, x1, x1), + Point3D(x1 + 1, x1 + 1, x1 + 1)) + assert Line3D(p1, p2).parallel_line(p3.args) == \ + Line3D(Point3D(x1, x1, x1), Point3D(x1 + 1, x1 + 1, x1 + 1)) + assert Line3D(Point3D(4, 0, 1), Point3D(0, 4, 1)).is_parallel(Line3D(Point3D(0, 0, 1), Point3D(4, 4, 1))) is False + + +def test_is_perpendicular(): + p1 = Point(0, 0) + p2 = Point(1, 1) + + l1 = Line(p1, p2) + l2 = Line(Point(x1, x1), Point(y1, y1)) + l1_1 = Line(p1, Point(-x1, x1)) + # 2D + assert Line.is_perpendicular(l1, l1_1) + assert Line.is_perpendicular(l1, l2) is False + p = l1.random_point() + assert l1.perpendicular_segment(p) == p + # 3D + assert Line3D.is_perpendicular(Line3D(Point3D(0, 0, 0), Point3D(1, 0, 0)), + Line3D(Point3D(0, 0, 0), Point3D(0, 1, 0))) is True + assert Line3D.is_perpendicular(Line3D(Point3D(0, 0, 0), Point3D(1, 0, 0)), + Line3D(Point3D(0, 1, 0), Point3D(1, 1, 0))) is False + assert Line3D.is_perpendicular(Line3D(Point3D(0, 0, 0), Point3D(1, 1, 1)), + Line3D(Point3D(x1, x1, x1), Point3D(y1, y1, y1))) is False + + +def test_is_similar(): + p1 = Point(2000, 2000) + p2 = p1.scale(2, 2) + + r1 = Ray3D(Point3D(1, 1, 1), Point3D(1, 0, 0)) + r2 = Ray(Point(0, 0), Point(0, 1)) + + s1 = Segment(Point(0, 0), p1) + + assert s1.is_similar(Segment(p1, p2)) + assert s1.is_similar(r2) is False + assert r1.is_similar(Line3D(Point3D(1, 1, 1), Point3D(1, 0, 0))) is True + assert r1.is_similar(Line3D(Point3D(0, 0, 0), Point3D(0, 1, 0))) is False + + +def test_length(): + s2 = Segment3D(Point3D(x1, x1, x1), Point3D(y1, y1, y1)) + assert Line(Point(0, 0), Point(1, 1)).length is oo + assert s2.length == sqrt(3) * sqrt((x1 - y1) ** 2) + assert Line3D(Point3D(0, 0, 0), Point3D(1, 1, 1)).length is oo + + +def test_projection(): + p1 = Point(0, 0) + p2 = Point3D(0, 0, 0) + p3 = Point(-x1, x1) + + l1 = Line(p1, Point(1, 1)) + l2 = Line3D(Point3D(0, 0, 0), Point3D(1, 0, 0)) + l3 = Line3D(p2, Point3D(1, 1, 1)) + + r1 = Ray(Point(1, 1), Point(2, 2)) + + s1 = Segment(Point2D(0, 0), Point2D(0, 1)) + s2 = Segment(Point2D(1, 0), Point2D(2, 1/2)) + + assert Line(Point(x1, x1), Point(y1, y1)).projection(Point(y1, y1)) == Point(y1, y1) + assert Line(Point(x1, x1), Point(x1, 1 + x1)).projection(Point(1, 1)) == Point(x1, 1) + assert Segment(Point(-2, 2), Point(0, 4)).projection(r1) == Segment(Point(-1, 3), Point(0, 4)) + assert Segment(Point(0, 4), Point(-2, 2)).projection(r1) == Segment(Point(0, 4), Point(-1, 3)) + assert s2.projection(s1) == EmptySet + assert l1.projection(p3) == p1 + assert l1.projection(Ray(p1, Point(-1, 5))) == Ray(Point(0, 0), Point(2, 2)) + assert l1.projection(Ray(p1, Point(-1, 1))) == p1 + assert r1.projection(Ray(Point(1, 1), Point(-1, -1))) == Point(1, 1) + assert r1.projection(Ray(Point(0, 4), Point(-1, -5))) == Segment(Point(1, 1), Point(2, 2)) + assert r1.projection(Segment(Point(-1, 5), Point(-5, -10))) == Segment(Point(1, 1), Point(2, 2)) + assert r1.projection(Ray(Point(1, 1), Point(-1, -1))) == Point(1, 1) + assert r1.projection(Ray(Point(0, 4), Point(-1, -5))) == Segment(Point(1, 1), Point(2, 2)) + assert r1.projection(Segment(Point(-1, 5), Point(-5, -10))) == Segment(Point(1, 1), Point(2, 2)) + + assert l3.projection(Ray3D(p2, Point3D(-1, 5, 0))) == Ray3D(Point3D(0, 0, 0), Point3D(Rational(4, 3), Rational(4, 3), Rational(4, 3))) + assert l3.projection(Ray3D(p2, Point3D(-1, 1, 1))) == Ray3D(Point3D(0, 0, 0), Point3D(Rational(1, 3), Rational(1, 3), Rational(1, 3))) + assert l2.projection(Point3D(5, 5, 0)) == Point3D(5, 0) + assert l2.projection(Line3D(Point3D(0, 1, 0), Point3D(1, 1, 0))).equals(l2) + + +def test_perpendicular_line(): + # 3d - requires a particular orthogonal to be selected + p1, p2, p3 = Point(0, 0, 0), Point(2, 3, 4), Point(-2, 2, 0) + l = Line(p1, p2) + p = l.perpendicular_line(p3) + assert p.p1 == p3 + assert p.p2 in l + # 2d - does not require special selection + p1, p2, p3 = Point(0, 0), Point(2, 3), Point(-2, 2) + l = Line(p1, p2) + p = l.perpendicular_line(p3) + assert p.p1 == p3 + # p is directed from l to p3 + assert p.direction.unit == (p3 - l.projection(p3)).unit + + +def test_perpendicular_bisector(): + s1 = Segment(Point(0, 0), Point(1, 1)) + aline = Line(Point(S.Half, S.Half), Point(Rational(3, 2), Rational(-1, 2))) + on_line = Segment(Point(S.Half, S.Half), Point(Rational(3, 2), Rational(-1, 2))).midpoint + + assert s1.perpendicular_bisector().equals(aline) + assert s1.perpendicular_bisector(on_line).equals(Segment(s1.midpoint, on_line)) + assert s1.perpendicular_bisector(on_line + (1, 0)).equals(aline) + + +def test_raises(): + d, e = symbols('a,b', real=True) + s = Segment((d, 0), (e, 0)) + + raises(TypeError, lambda: Line((1, 1), 1)) + raises(ValueError, lambda: Line(Point(0, 0), Point(0, 0))) + raises(Undecidable, lambda: Point(2 * d, 0) in s) + raises(ValueError, lambda: Ray3D(Point(1.0, 1.0))) + raises(ValueError, lambda: Line3D(Point3D(0, 0, 0), Point3D(0, 0, 0))) + raises(TypeError, lambda: Line3D((1, 1), 1)) + raises(ValueError, lambda: Line3D(Point3D(0, 0, 0))) + raises(TypeError, lambda: Ray((1, 1), 1)) + raises(GeometryError, lambda: Line(Point(0, 0), Point(1, 0)) + .projection(Circle(Point(0, 0), 1))) + + +def test_ray_generation(): + assert Ray((1, 1), angle=pi / 4) == Ray((1, 1), (2, 2)) + assert Ray((1, 1), angle=pi / 2) == Ray((1, 1), (1, 2)) + assert Ray((1, 1), angle=-pi / 2) == Ray((1, 1), (1, 0)) + assert Ray((1, 1), angle=-3 * pi / 2) == Ray((1, 1), (1, 2)) + assert Ray((1, 1), angle=5 * pi / 2) == Ray((1, 1), (1, 2)) + assert Ray((1, 1), angle=5.0 * pi / 2) == Ray((1, 1), (1, 2)) + assert Ray((1, 1), angle=pi) == Ray((1, 1), (0, 1)) + assert Ray((1, 1), angle=3.0 * pi) == Ray((1, 1), (0, 1)) + assert Ray((1, 1), angle=4.0 * pi) == Ray((1, 1), (2, 1)) + assert Ray((1, 1), angle=0) == Ray((1, 1), (2, 1)) + assert Ray((1, 1), angle=4.05 * pi) == Ray(Point(1, 1), + Point(2, -sqrt(5) * sqrt(2 * sqrt(5) + 10) / 4 - sqrt( + 2 * sqrt(5) + 10) / 4 + 2 + sqrt(5))) + assert Ray((1, 1), angle=4.02 * pi) == Ray(Point(1, 1), + Point(2, 1 + tan(4.02 * pi))) + assert Ray((1, 1), angle=5) == Ray((1, 1), (2, 1 + tan(5))) + + assert Ray3D((1, 1, 1), direction_ratio=[4, 4, 4]) == Ray3D(Point3D(1, 1, 1), Point3D(5, 5, 5)) + assert Ray3D((1, 1, 1), direction_ratio=[1, 2, 3]) == Ray3D(Point3D(1, 1, 1), Point3D(2, 3, 4)) + assert Ray3D((1, 1, 1), direction_ratio=[1, 1, 1]) == Ray3D(Point3D(1, 1, 1), Point3D(2, 2, 2)) + + +def test_issue_7814(): + circle = Circle(Point(x, 0), y) + line = Line(Point(k, z), slope=0) + _s = sqrt((y - z)*(y + z)) + assert line.intersection(circle) == [Point2D(x + _s, z), Point2D(x - _s, z)] + + +def test_issue_2941(): + def _check(): + for f, g in cartes(*[(Line, Ray, Segment)] * 2): + l1 = f(a, b) + l2 = g(c, d) + assert l1.intersection(l2) == l2.intersection(l1) + # intersect at end point + c, d = (-2, -2), (-2, 0) + a, b = (0, 0), (1, 1) + _check() + # midline intersection + c, d = (-2, -3), (-2, 0) + _check() + + +def test_parameter_value(): + t = Symbol('t') + p1, p2 = Point(0, 1), Point(5, 6) + l = Line(p1, p2) + assert l.parameter_value((5, 6), t) == {t: 1} + raises(ValueError, lambda: l.parameter_value((0, 0), t)) + + +def test_bisectors(): + r1 = Line3D(Point3D(0, 0, 0), Point3D(1, 0, 0)) + r2 = Line3D(Point3D(0, 0, 0), Point3D(0, 1, 0)) + bisections = r1.bisectors(r2) + assert bisections == [Line3D(Point3D(0, 0, 0), Point3D(1, 1, 0)), + Line3D(Point3D(0, 0, 0), Point3D(1, -1, 0))] + ans = [Line3D(Point3D(0, 0, 0), Point3D(1, 0, 1)), + Line3D(Point3D(0, 0, 0), Point3D(-1, 0, 1))] + l1 = (0, 0, 0), (0, 0, 1) + l2 = (0, 0), (1, 0) + for a, b in cartes((Line, Segment, Ray), repeat=2): + assert a(*l1).bisectors(b(*l2)) == ans + + +def test_issue_8615(): + a = Line3D(Point3D(6, 5, 0), Point3D(6, -6, 0)) + b = Line3D(Point3D(6, -1, 19/10), Point3D(6, -1, 0)) + assert a.intersection(b) == [Point3D(6, -1, 0)] + + +def test_issue_12598(): + r1 = Ray(Point(0, 1), Point(0.98, 0.79).n(2)) + r2 = Ray(Point(0, 0), Point(0.71, 0.71).n(2)) + assert str(r1.intersection(r2)[0]) == 'Point2D(0.82, 0.82)' + l1 = Line((0, 0), (1, 1)) + l2 = Segment((-1, 1), (0, -1)).n(2) + assert str(l1.intersection(l2)[0]) == 'Point2D(-0.33, -0.33)' + l2 = Segment((-1, 1), (-1/2, 1/2)).n(2) + assert not l1.intersection(l2) diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_parabola.py b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_parabola.py new file mode 100644 index 0000000000000000000000000000000000000000..2a683f26619952d93475aca9ebd3d47cfb3657a6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_parabola.py @@ -0,0 +1,143 @@ +from sympy.core.numbers import (Rational, oo) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.complexes import sign +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.geometry.ellipse import (Circle, Ellipse) +from sympy.geometry.line import (Line, Ray2D, Segment2D) +from sympy.geometry.parabola import Parabola +from sympy.geometry.point import (Point, Point2D) +from sympy.testing.pytest import raises + +from sympy.abc import x, y + +def test_parabola_geom(): + a, b = symbols('a b') + p1 = Point(0, 0) + p2 = Point(3, 7) + p3 = Point(0, 4) + p4 = Point(6, 0) + p5 = Point(a, a) + d1 = Line(Point(4, 0), Point(4, 9)) + d2 = Line(Point(7, 6), Point(3, 6)) + d3 = Line(Point(4, 0), slope=oo) + d4 = Line(Point(7, 6), slope=0) + d5 = Line(Point(b, a), slope=oo) + d6 = Line(Point(a, b), slope=0) + + half = S.Half + + pa1 = Parabola(None, d2) + pa2 = Parabola(directrix=d1) + pa3 = Parabola(p1, d1) + pa4 = Parabola(p2, d2) + pa5 = Parabola(p2, d4) + pa6 = Parabola(p3, d2) + pa7 = Parabola(p2, d1) + pa8 = Parabola(p4, d1) + pa9 = Parabola(p4, d3) + pa10 = Parabola(p5, d5) + pa11 = Parabola(p5, d6) + d = Line(Point(3, 7), Point(2, 9)) + pa12 = Parabola(Point(7, 8), d) + pa12r = Parabola(Point(7, 8).reflect(d), d) + + raises(ValueError, lambda: + Parabola(Point(7, 8, 9), Line(Point(6, 7), Point(7, 7)))) + raises(ValueError, lambda: + Parabola(Point(0, 2), Line(Point(7, 2), Point(6, 2)))) + raises(ValueError, lambda: Parabola(Point(7, 8), Point(3, 8))) + + # Basic Stuff + assert pa1.focus == Point(0, 0) + assert pa1.ambient_dimension == S(2) + assert pa2 == pa3 + assert pa4 != pa7 + assert pa6 != pa7 + assert pa6.focus == Point2D(0, 4) + assert pa6.focal_length == 1 + assert pa6.p_parameter == -1 + assert pa6.vertex == Point2D(0, 5) + assert pa6.eccentricity == 1 + assert pa7.focus == Point2D(3, 7) + assert pa7.focal_length == half + assert pa7.p_parameter == -half + assert pa7.vertex == Point2D(7*half, 7) + assert pa4.focal_length == half + assert pa4.p_parameter == half + assert pa4.vertex == Point2D(3, 13*half) + assert pa8.focal_length == 1 + assert pa8.p_parameter == 1 + assert pa8.vertex == Point2D(5, 0) + assert pa4.focal_length == pa5.focal_length + assert pa4.p_parameter == pa5.p_parameter + assert pa4.vertex == pa5.vertex + assert pa4.equation() == pa5.equation() + assert pa8.focal_length == pa9.focal_length + assert pa8.p_parameter == pa9.p_parameter + assert pa8.vertex == pa9.vertex + assert pa8.equation() == pa9.equation() + assert pa10.focal_length == pa11.focal_length == sqrt((a - b) ** 2) / 2 # if a, b real == abs(a - b)/2 + assert pa11.vertex == Point(*pa10.vertex[::-1]) == Point(a, + a - sqrt((a - b)**2)*sign(a - b)/2) # change axis x->y, y->x on pa10 + aos = pa12.axis_of_symmetry + assert aos == Line(Point(7, 8), Point(5, 7)) + assert pa12.directrix == Line(Point(3, 7), Point(2, 9)) + assert pa12.directrix.angle_between(aos) == S.Pi/2 + assert pa12.eccentricity == 1 + assert pa12.equation(x, y) == (x - 7)**2 + (y - 8)**2 - (-2*x - y + 13)**2/5 + assert pa12.focal_length == 9*sqrt(5)/10 + assert pa12.focus == Point(7, 8) + assert pa12.p_parameter == 9*sqrt(5)/10 + assert pa12.vertex == Point2D(S(26)/5, S(71)/10) + assert pa12r.focal_length == 9*sqrt(5)/10 + assert pa12r.focus == Point(-S(1)/5, S(22)/5) + assert pa12r.p_parameter == -9*sqrt(5)/10 + assert pa12r.vertex == Point(S(8)/5, S(53)/10) + + +def test_parabola_intersection(): + l1 = Line(Point(1, -2), Point(-1,-2)) + l2 = Line(Point(1, 2), Point(-1,2)) + l3 = Line(Point(1, 0), Point(-1,0)) + + p1 = Point(0,0) + p2 = Point(0, -2) + p3 = Point(120, -12) + parabola1 = Parabola(p1, l1) + + # parabola with parabola + assert parabola1.intersection(parabola1) == [parabola1] + assert parabola1.intersection(Parabola(p1, l2)) == [Point2D(-2, 0), Point2D(2, 0)] + assert parabola1.intersection(Parabola(p2, l3)) == [Point2D(0, -1)] + assert parabola1.intersection(Parabola(Point(16, 0), l1)) == [Point2D(8, 15)] + assert parabola1.intersection(Parabola(Point(0, 16), l1)) == [Point2D(-6, 8), Point2D(6, 8)] + assert parabola1.intersection(Parabola(p3, l3)) == [] + # parabola with point + assert parabola1.intersection(p1) == [] + assert parabola1.intersection(Point2D(0, -1)) == [Point2D(0, -1)] + assert parabola1.intersection(Point2D(4, 3)) == [Point2D(4, 3)] + # parabola with line + assert parabola1.intersection(Line(Point2D(-7, 3), Point(12, 3))) == [Point2D(-4, 3), Point2D(4, 3)] + assert parabola1.intersection(Line(Point(-4, -1), Point(4, -1))) == [Point(0, -1)] + assert parabola1.intersection(Line(Point(2, 0), Point(0, -2))) == [Point2D(2, 0)] + raises(TypeError, lambda: parabola1.intersection(Line(Point(0, 0, 0), Point(1, 1, 1)))) + # parabola with segment + assert parabola1.intersection(Segment2D((-4, -5), (4, 3))) == [Point2D(0, -1), Point2D(4, 3)] + assert parabola1.intersection(Segment2D((0, -5), (0, 6))) == [Point2D(0, -1)] + assert parabola1.intersection(Segment2D((-12, -65), (14, -68))) == [] + # parabola with ray + assert parabola1.intersection(Ray2D((-4, -5), (4, 3))) == [Point2D(0, -1), Point2D(4, 3)] + assert parabola1.intersection(Ray2D((0, 7), (1, 14))) == [Point2D(14 + 2*sqrt(57), 105 + 14*sqrt(57))] + assert parabola1.intersection(Ray2D((0, 7), (0, 14))) == [] + # parabola with ellipse/circle + assert parabola1.intersection(Circle(p1, 2)) == [Point2D(-2, 0), Point2D(2, 0)] + assert parabola1.intersection(Circle(p2, 1)) == [Point2D(0, -1)] + assert parabola1.intersection(Ellipse(p2, 2, 1)) == [Point2D(0, -1)] + assert parabola1.intersection(Ellipse(Point(0, 19), 5, 7)) == [] + assert parabola1.intersection(Ellipse((0, 3), 12, 4)) == [ + Point2D(0, -1), + Point2D(-4*sqrt(17)/3, Rational(59, 9)), + Point2D(4*sqrt(17)/3, Rational(59, 9))] + # parabola with unsupported type + raises(TypeError, lambda: parabola1.intersection(2)) diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_plane.py b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_plane.py new file mode 100644 index 0000000000000000000000000000000000000000..1010fce5c3bc68348eacee13f29c1d7588f17e39 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_plane.py @@ -0,0 +1,268 @@ +from sympy.core.numbers import (Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, symbols) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (asin, cos, sin) +from sympy.geometry import Line, Point, Ray, Segment, Point3D, Line3D, Ray3D, Segment3D, Plane, Circle +from sympy.geometry.util import are_coplanar +from sympy.testing.pytest import raises + + +def test_plane(): + x, y, z, u, v = symbols('x y z u v', real=True) + p1 = Point3D(0, 0, 0) + p2 = Point3D(1, 1, 1) + p3 = Point3D(1, 2, 3) + pl3 = Plane(p1, p2, p3) + pl4 = Plane(p1, normal_vector=(1, 1, 1)) + pl4b = Plane(p1, p2) + pl5 = Plane(p3, normal_vector=(1, 2, 3)) + pl6 = Plane(Point3D(2, 3, 7), normal_vector=(2, 2, 2)) + pl7 = Plane(Point3D(1, -5, -6), normal_vector=(1, -2, 1)) + pl8 = Plane(p1, normal_vector=(0, 0, 1)) + pl9 = Plane(p1, normal_vector=(0, 12, 0)) + pl10 = Plane(p1, normal_vector=(-2, 0, 0)) + pl11 = Plane(p2, normal_vector=(0, 0, 1)) + l1 = Line3D(Point3D(5, 0, 0), Point3D(1, -1, 1)) + l2 = Line3D(Point3D(0, -2, 0), Point3D(3, 1, 1)) + l3 = Line3D(Point3D(0, -1, 0), Point3D(5, -1, 9)) + + raises(ValueError, lambda: Plane(p1, p1, p1)) + + assert Plane(p1, p2, p3) != Plane(p1, p3, p2) + assert Plane(p1, p2, p3).is_coplanar(Plane(p1, p3, p2)) + assert Plane(p1, p2, p3).is_coplanar(p1) + assert Plane(p1, p2, p3).is_coplanar(Circle(p1, 1)) is False + assert Plane(p1, normal_vector=(0, 0, 1)).is_coplanar(Circle(p1, 1)) + + assert pl3 == Plane(Point3D(0, 0, 0), normal_vector=(1, -2, 1)) + assert pl3 != pl4 + assert pl4 == pl4b + assert pl5 == Plane(Point3D(1, 2, 3), normal_vector=(1, 2, 3)) + + assert pl5.equation(x, y, z) == x + 2*y + 3*z - 14 + assert pl3.equation(x, y, z) == x - 2*y + z + + assert pl3.p1 == p1 + assert pl4.p1 == p1 + assert pl5.p1 == p3 + + assert pl4.normal_vector == (1, 1, 1) + assert pl5.normal_vector == (1, 2, 3) + + assert p1 in pl3 + assert p1 in pl4 + assert p3 in pl5 + + assert pl3.projection(Point(0, 0)) == p1 + p = pl3.projection(Point3D(1, 1, 0)) + assert p == Point3D(Rational(7, 6), Rational(2, 3), Rational(1, 6)) + assert p in pl3 + + l = pl3.projection_line(Line(Point(0, 0), Point(1, 1))) + assert l == Line3D(Point3D(0, 0, 0), Point3D(Rational(7, 6), Rational(2, 3), Rational(1, 6))) + assert l in pl3 + # get a segment that does not intersect the plane which is also + # parallel to pl3's normal veector + t = Dummy() + r = pl3.random_point() + a = pl3.perpendicular_line(r).arbitrary_point(t) + s = Segment3D(a.subs(t, 1), a.subs(t, 2)) + assert s.p1 not in pl3 and s.p2 not in pl3 + assert pl3.projection_line(s).equals(r) + assert pl3.projection_line(Segment(Point(1, 0), Point(1, 1))) == \ + Segment3D(Point3D(Rational(5, 6), Rational(1, 3), Rational(-1, 6)), Point3D(Rational(7, 6), Rational(2, 3), Rational(1, 6))) + assert pl6.projection_line(Ray(Point(1, 0), Point(1, 1))) == \ + Ray3D(Point3D(Rational(14, 3), Rational(11, 3), Rational(11, 3)), Point3D(Rational(13, 3), Rational(13, 3), Rational(10, 3))) + assert pl3.perpendicular_line(r.args) == pl3.perpendicular_line(r) + + assert pl3.is_parallel(pl6) is False + assert pl4.is_parallel(pl6) + assert pl3.is_parallel(Line(p1, p2)) + assert pl6.is_parallel(l1) is False + + assert pl3.is_perpendicular(pl6) + assert pl4.is_perpendicular(pl7) + assert pl6.is_perpendicular(pl7) + assert pl6.is_perpendicular(pl4) is False + assert pl6.is_perpendicular(l1) is False + assert pl6.is_perpendicular(Line((0, 0, 0), (1, 1, 1))) + assert pl6.is_perpendicular((1, 1)) is False + + assert pl6.distance(pl6.arbitrary_point(u, v)) == 0 + assert pl7.distance(pl7.arbitrary_point(u, v)) == 0 + assert pl6.distance(pl6.arbitrary_point(t)) == 0 + assert pl7.distance(pl7.arbitrary_point(t)) == 0 + assert pl6.p1.distance(pl6.arbitrary_point(t)).simplify() == 1 + assert pl7.p1.distance(pl7.arbitrary_point(t)).simplify() == 1 + assert pl3.arbitrary_point(t) == Point3D(-sqrt(30)*sin(t)/30 + \ + 2*sqrt(5)*cos(t)/5, sqrt(30)*sin(t)/15 + sqrt(5)*cos(t)/5, sqrt(30)*sin(t)/6) + assert pl3.arbitrary_point(u, v) == Point3D(2*u - v, u + 2*v, 5*v) + + assert pl7.distance(Point3D(1, 3, 5)) == 5*sqrt(6)/6 + assert pl6.distance(Point3D(0, 0, 0)) == 4*sqrt(3) + assert pl6.distance(pl6.p1) == 0 + assert pl7.distance(pl6) == 0 + assert pl7.distance(l1) == 0 + assert pl6.distance(Segment3D(Point3D(2, 3, 1), Point3D(1, 3, 4))) == \ + pl6.distance(Point3D(1, 3, 4)) == 4*sqrt(3)/3 + assert pl6.distance(Segment3D(Point3D(1, 3, 4), Point3D(0, 3, 7))) == \ + pl6.distance(Point3D(0, 3, 7)) == 2*sqrt(3)/3 + assert pl6.distance(Segment3D(Point3D(0, 3, 7), Point3D(-1, 3, 10))) == 0 + assert pl6.distance(Segment3D(Point3D(-1, 3, 10), Point3D(-2, 3, 13))) == 0 + assert pl6.distance(Segment3D(Point3D(-2, 3, 13), Point3D(-3, 3, 16))) == \ + pl6.distance(Point3D(-2, 3, 13)) == 2*sqrt(3)/3 + assert pl6.distance(Plane(Point3D(5, 5, 5), normal_vector=(8, 8, 8))) == sqrt(3) + assert pl6.distance(Ray3D(Point3D(1, 3, 4), direction_ratio=[1, 0, -3])) == 4*sqrt(3)/3 + assert pl6.distance(Ray3D(Point3D(2, 3, 1), direction_ratio=[-1, 0, 3])) == 0 + + + assert pl6.angle_between(pl3) == pi/2 + assert pl6.angle_between(pl6) == 0 + assert pl6.angle_between(pl4) == 0 + assert pl7.angle_between(Line3D(Point3D(2, 3, 5), Point3D(2, 4, 6))) == \ + -asin(sqrt(3)/6) + assert pl6.angle_between(Ray3D(Point3D(2, 4, 1), Point3D(6, 5, 3))) == \ + asin(sqrt(7)/3) + assert pl7.angle_between(Segment3D(Point3D(5, 6, 1), Point3D(1, 2, 4))) == \ + asin(7*sqrt(246)/246) + + assert are_coplanar(l1, l2, l3) is False + assert are_coplanar(l1) is False + assert are_coplanar(Point3D(2, 7, 2), Point3D(0, 0, 2), + Point3D(1, 1, 2), Point3D(1, 2, 2)) + assert are_coplanar(Plane(p1, p2, p3), Plane(p1, p3, p2)) + assert Plane.are_concurrent(pl3, pl4, pl5) is False + assert Plane.are_concurrent(pl6) is False + raises(ValueError, lambda: Plane.are_concurrent(Point3D(0, 0, 0))) + raises(ValueError, lambda: Plane((1, 2, 3), normal_vector=(0, 0, 0))) + + assert pl3.parallel_plane(Point3D(1, 2, 5)) == Plane(Point3D(1, 2, 5), \ + normal_vector=(1, -2, 1)) + + # perpendicular_plane + p = Plane((0, 0, 0), (1, 0, 0)) + # default + assert p.perpendicular_plane() == Plane(Point3D(0, 0, 0), (0, 1, 0)) + # 1 pt + assert p.perpendicular_plane(Point3D(1, 0, 1)) == \ + Plane(Point3D(1, 0, 1), (0, 1, 0)) + # pts as tuples + assert p.perpendicular_plane((1, 0, 1), (1, 1, 1)) == \ + Plane(Point3D(1, 0, 1), (0, 0, -1)) + # more than two planes + raises(ValueError, lambda: p.perpendicular_plane((1, 0, 1), (1, 1, 1), (1, 1, 0))) + + a, b = Point3D(0, 0, 0), Point3D(0, 1, 0) + Z = (0, 0, 1) + p = Plane(a, normal_vector=Z) + # case 4 + assert p.perpendicular_plane(a, b) == Plane(a, (1, 0, 0)) + n = Point3D(*Z) + # case 1 + assert p.perpendicular_plane(a, n) == Plane(a, (-1, 0, 0)) + # case 2 + assert Plane(a, normal_vector=b.args).perpendicular_plane(a, a + b) == \ + Plane(Point3D(0, 0, 0), (1, 0, 0)) + # case 1&3 + assert Plane(b, normal_vector=Z).perpendicular_plane(b, b + n) == \ + Plane(Point3D(0, 1, 0), (-1, 0, 0)) + # case 2&3 + assert Plane(b, normal_vector=b.args).perpendicular_plane(n, n + b) == \ + Plane(Point3D(0, 0, 1), (1, 0, 0)) + + p = Plane(a, normal_vector=(0, 0, 1)) + assert p.perpendicular_plane() == Plane(a, normal_vector=(1, 0, 0)) + + assert pl6.intersection(pl6) == [pl6] + assert pl4.intersection(pl4.p1) == [pl4.p1] + assert pl3.intersection(pl6) == [ + Line3D(Point3D(8, 4, 0), Point3D(2, 4, 6))] + assert pl3.intersection(Line3D(Point3D(1,2,4), Point3D(4,4,2))) == [ + Point3D(2, Rational(8, 3), Rational(10, 3))] + assert pl3.intersection(Plane(Point3D(6, 0, 0), normal_vector=(2, -5, 3)) + ) == [Line3D(Point3D(-24, -12, 0), Point3D(-25, -13, -1))] + assert pl6.intersection(Ray3D(Point3D(2, 3, 1), Point3D(1, 3, 4))) == [ + Point3D(-1, 3, 10)] + assert pl6.intersection(Segment3D(Point3D(2, 3, 1), Point3D(1, 3, 4))) == [] + assert pl7.intersection(Line(Point(2, 3), Point(4, 2))) == [ + Point3D(Rational(13, 2), Rational(3, 4), 0)] + r = Ray(Point(2, 3), Point(4, 2)) + assert Plane((1,2,0), normal_vector=(0,0,1)).intersection(r) == [ + Ray3D(Point(2, 3), Point(4, 2))] + assert pl9.intersection(pl8) == [Line3D(Point3D(0, 0, 0), Point3D(12, 0, 0))] + assert pl10.intersection(pl11) == [Line3D(Point3D(0, 0, 1), Point3D(0, 2, 1))] + assert pl4.intersection(pl8) == [Line3D(Point3D(0, 0, 0), Point3D(1, -1, 0))] + assert pl11.intersection(pl8) == [] + assert pl9.intersection(pl11) == [Line3D(Point3D(0, 0, 1), Point3D(12, 0, 1))] + assert pl9.intersection(pl4) == [Line3D(Point3D(0, 0, 0), Point3D(12, 0, -12))] + assert pl3.random_point() in pl3 + assert pl3.random_point(seed=1) in pl3 + + # test geometrical entity using equals + assert pl4.intersection(pl4.p1)[0].equals(pl4.p1) + assert pl3.intersection(pl6)[0].equals(Line3D(Point3D(8, 4, 0), Point3D(2, 4, 6))) + pl8 = Plane((1, 2, 0), normal_vector=(0, 0, 1)) + assert pl8.intersection(Line3D(p1, (1, 12, 0)))[0].equals(Line((0, 0, 0), (0.1, 1.2, 0))) + assert pl8.intersection(Ray3D(p1, (1, 12, 0)))[0].equals(Ray((0, 0, 0), (1, 12, 0))) + assert pl8.intersection(Segment3D(p1, (21, 1, 0)))[0].equals(Segment3D(p1, (21, 1, 0))) + assert pl8.intersection(Plane(p1, normal_vector=(0, 0, 112)))[0].equals(pl8) + assert pl8.intersection(Plane(p1, normal_vector=(0, 12, 0)))[0].equals( + Line3D(p1, direction_ratio=(112 * pi, 0, 0))) + assert pl8.intersection(Plane(p1, normal_vector=(11, 0, 1)))[0].equals( + Line3D(p1, direction_ratio=(0, -11, 0))) + assert pl8.intersection(Plane(p1, normal_vector=(1, 0, 11)))[0].equals( + Line3D(p1, direction_ratio=(0, 11, 0))) + assert pl8.intersection(Plane(p1, normal_vector=(-1, -1, -11)))[0].equals( + Line3D(p1, direction_ratio=(1, -1, 0))) + assert pl3.random_point() in pl3 + assert len(pl8.intersection(Ray3D(Point3D(0, 2, 3), Point3D(1, 0, 3)))) == 0 + # check if two plane are equals + assert pl6.intersection(pl6)[0].equals(pl6) + assert pl8.equals(Plane(p1, normal_vector=(0, 12, 0))) is False + assert pl8.equals(pl8) + assert pl8.equals(Plane(p1, normal_vector=(0, 0, -12))) + assert pl8.equals(Plane(p1, normal_vector=(0, 0, -12*sqrt(3)))) + assert pl8.equals(p1) is False + + # issue 8570 + l2 = Line3D(Point3D(Rational(50000004459633, 5000000000000), + Rational(-891926590718643, 1000000000000000), + Rational(231800966893633, 100000000000000)), + Point3D(Rational(50000004459633, 50000000000000), + Rational(-222981647679771, 250000000000000), + Rational(231800966893633, 100000000000000))) + + p2 = Plane(Point3D(Rational(402775636372767, 100000000000000), + Rational(-97224357654973, 100000000000000), + Rational(216793600814789, 100000000000000)), + (-S('9.00000087501922'), -S('4.81170658872543e-13'), + S('0.0'))) + + assert str([i.n(2) for i in p2.intersection(l2)]) == \ + '[Point3D(4.0, -0.89, 2.3)]' + + +def test_dimension_normalization(): + A = Plane(Point3D(1, 1, 2), normal_vector=(1, 1, 1)) + b = Point(1, 1) + assert A.projection(b) == Point(Rational(5, 3), Rational(5, 3), Rational(2, 3)) + + a, b = Point(0, 0), Point3D(0, 1) + Z = (0, 0, 1) + p = Plane(a, normal_vector=Z) + assert p.perpendicular_plane(a, b) == Plane(Point3D(0, 0, 0), (1, 0, 0)) + assert Plane((1, 2, 1), (2, 1, 0), (3, 1, 2) + ).intersection((2, 1)) == [Point(2, 1, 0)] + + +def test_parameter_value(): + t, u, v = symbols("t, u v") + p1, p2, p3 = Point(0, 0, 0), Point(0, 0, 1), Point(0, 1, 0) + p = Plane(p1, p2, p3) + assert p.parameter_value((0, -3, 2), t) == {t: asin(2*sqrt(13)/13)} + assert p.parameter_value((0, -3, 2), u, v) == {u: 3, v: 2} + assert p.parameter_value(p1, t) == p1 + raises(ValueError, lambda: p.parameter_value((1, 0, 0), t)) + raises(ValueError, lambda: p.parameter_value(Line(Point(0, 0), Point(1, 1)), t)) + raises(ValueError, lambda: p.parameter_value((0, -3, 2), t, 1)) diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_point.py b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_point.py new file mode 100644 index 0000000000000000000000000000000000000000..1f2b2768eb3fba2009f702351de1aac3ed6e71d4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_point.py @@ -0,0 +1,481 @@ +from sympy.core.basic import Basic +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.parameters import evaluate +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.core.sympify import sympify +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.geometry import Line, Point, Point2D, Point3D, Line3D, Plane +from sympy.geometry.entity import rotate, scale, translate, GeometryEntity +from sympy.matrices import Matrix +from sympy.utilities.iterables import subsets, permutations, cartes +from sympy.utilities.misc import Undecidable +from sympy.testing.pytest import raises, warns + + +def test_point(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + x1 = Symbol('x1', real=True) + x2 = Symbol('x2', real=True) + y1 = Symbol('y1', real=True) + y2 = Symbol('y2', real=True) + half = S.Half + p1 = Point(x1, x2) + p2 = Point(y1, y2) + p3 = Point(0, 0) + p4 = Point(1, 1) + p5 = Point(0, 1) + line = Line(Point(1, 0), slope=1) + + assert p1 in p1 + assert p1 not in p2 + assert p2.y == y2 + assert (p3 + p4) == p4 + assert (p2 - p1) == Point(y1 - x1, y2 - x2) + assert -p2 == Point(-y1, -y2) + raises(TypeError, lambda: Point(1)) + raises(ValueError, lambda: Point([1])) + raises(ValueError, lambda: Point(3, I)) + raises(ValueError, lambda: Point(2*I, I)) + raises(ValueError, lambda: Point(3 + I, I)) + + assert Point(34.05, sqrt(3)) == Point(Rational(681, 20), sqrt(3)) + assert Point.midpoint(p3, p4) == Point(half, half) + assert Point.midpoint(p1, p4) == Point(half + half*x1, half + half*x2) + assert Point.midpoint(p2, p2) == p2 + assert p2.midpoint(p2) == p2 + assert p1.origin == Point(0, 0) + + assert Point.distance(p3, p4) == sqrt(2) + assert Point.distance(p1, p1) == 0 + assert Point.distance(p3, p2) == sqrt(p2.x**2 + p2.y**2) + raises(TypeError, lambda: Point.distance(p1, 0)) + raises(TypeError, lambda: Point.distance(p1, GeometryEntity())) + + # distance should be symmetric + assert p1.distance(line) == line.distance(p1) + assert p4.distance(line) == line.distance(p4) + + assert Point.taxicab_distance(p4, p3) == 2 + + assert Point.canberra_distance(p4, p5) == 1 + raises(ValueError, lambda: Point.canberra_distance(p3, p3)) + + p1_1 = Point(x1, x1) + p1_2 = Point(y2, y2) + p1_3 = Point(x1 + 1, x1) + assert Point.is_collinear(p3) + + with warns(UserWarning, test_stacklevel=False): + assert Point.is_collinear(p3, Point(p3, dim=4)) + assert p3.is_collinear() + assert Point.is_collinear(p3, p4) + assert Point.is_collinear(p3, p4, p1_1, p1_2) + assert Point.is_collinear(p3, p4, p1_1, p1_3) is False + assert Point.is_collinear(p3, p3, p4, p5) is False + + raises(TypeError, lambda: Point.is_collinear(line)) + raises(TypeError, lambda: p1_1.is_collinear(line)) + + assert p3.intersection(Point(0, 0)) == [p3] + assert p3.intersection(p4) == [] + assert p3.intersection(line) == [] + with warns(UserWarning, test_stacklevel=False): + assert Point.intersection(Point(0, 0, 0), Point(0, 0)) == [Point(0, 0, 0)] + + x_pos = Symbol('x', positive=True) + p2_1 = Point(x_pos, 0) + p2_2 = Point(0, x_pos) + p2_3 = Point(-x_pos, 0) + p2_4 = Point(0, -x_pos) + p2_5 = Point(x_pos, 5) + assert Point.is_concyclic(p2_1) + assert Point.is_concyclic(p2_1, p2_2) + assert Point.is_concyclic(p2_1, p2_2, p2_3, p2_4) + for pts in permutations((p2_1, p2_2, p2_3, p2_5)): + assert Point.is_concyclic(*pts) is False + assert Point.is_concyclic(p4, p4 * 2, p4 * 3) is False + assert Point(0, 0).is_concyclic((1, 1), (2, 2), (2, 1)) is False + assert Point.is_concyclic(Point(0, 0, 0, 0), Point(1, 0, 0, 0), Point(1, 1, 0, 0), Point(1, 1, 1, 0)) is False + + assert p1.is_scalar_multiple(p1) + assert p1.is_scalar_multiple(2*p1) + assert not p1.is_scalar_multiple(p2) + assert Point.is_scalar_multiple(Point(1, 1), (-1, -1)) + assert Point.is_scalar_multiple(Point(0, 0), (0, -1)) + # test when is_scalar_multiple can't be determined + raises(Undecidable, lambda: Point.is_scalar_multiple(Point(sympify("x1%y1"), sympify("x2%y2")), Point(0, 1))) + + assert Point(0, 1).orthogonal_direction == Point(1, 0) + assert Point(1, 0).orthogonal_direction == Point(0, 1) + + assert p1.is_zero is None + assert p3.is_zero + assert p4.is_zero is False + assert p1.is_nonzero is None + assert p3.is_nonzero is False + assert p4.is_nonzero + + assert p4.scale(2, 3) == Point(2, 3) + assert p3.scale(2, 3) == p3 + + assert p4.rotate(pi, Point(0.5, 0.5)) == p3 + assert p1.__radd__(p2) == p1.midpoint(p2).scale(2, 2) + assert (-p3).__rsub__(p4) == p3.midpoint(p4).scale(2, 2) + + assert p4 * 5 == Point(5, 5) + assert p4 / 5 == Point(0.2, 0.2) + assert 5 * p4 == Point(5, 5) + + raises(ValueError, lambda: Point(0, 0) + 10) + + # Point differences should be simplified + assert Point(x*(x - 1), y) - Point(x**2 - x, y + 1) == Point(0, -1) + + a, b = S.Half, Rational(1, 3) + assert Point(a, b).evalf(2) == \ + Point(a.n(2), b.n(2), evaluate=False) + raises(ValueError, lambda: Point(1, 2) + 1) + + # test project + assert Point.project((0, 1), (1, 0)) == Point(0, 0) + assert Point.project((1, 1), (1, 0)) == Point(1, 0) + raises(ValueError, lambda: Point.project(p1, Point(0, 0))) + + # test transformations + p = Point(1, 0) + assert p.rotate(pi/2) == Point(0, 1) + assert p.rotate(pi/2, p) == p + p = Point(1, 1) + assert p.scale(2, 3) == Point(2, 3) + assert p.translate(1, 2) == Point(2, 3) + assert p.translate(1) == Point(2, 1) + assert p.translate(y=1) == Point(1, 2) + assert p.translate(*p.args) == Point(2, 2) + + # Check invalid input for transform + raises(ValueError, lambda: p3.transform(p3)) + raises(ValueError, lambda: p.transform(Matrix([[1, 0], [0, 1]]))) + + # test __contains__ + assert 0 in Point(0, 0, 0, 0) + assert 1 not in Point(0, 0, 0, 0) + + # test affine_rank + assert Point.affine_rank() == -1 + + +def test_point3D(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + x1 = Symbol('x1', real=True) + x2 = Symbol('x2', real=True) + x3 = Symbol('x3', real=True) + y1 = Symbol('y1', real=True) + y2 = Symbol('y2', real=True) + y3 = Symbol('y3', real=True) + half = S.Half + p1 = Point3D(x1, x2, x3) + p2 = Point3D(y1, y2, y3) + p3 = Point3D(0, 0, 0) + p4 = Point3D(1, 1, 1) + p5 = Point3D(0, 1, 2) + + assert p1 in p1 + assert p1 not in p2 + assert p2.y == y2 + assert (p3 + p4) == p4 + assert (p2 - p1) == Point3D(y1 - x1, y2 - x2, y3 - x3) + assert -p2 == Point3D(-y1, -y2, -y3) + + assert Point(34.05, sqrt(3)) == Point(Rational(681, 20), sqrt(3)) + assert Point3D.midpoint(p3, p4) == Point3D(half, half, half) + assert Point3D.midpoint(p1, p4) == Point3D(half + half*x1, half + half*x2, + half + half*x3) + assert Point3D.midpoint(p2, p2) == p2 + assert p2.midpoint(p2) == p2 + + assert Point3D.distance(p3, p4) == sqrt(3) + assert Point3D.distance(p1, p1) == 0 + assert Point3D.distance(p3, p2) == sqrt(p2.x**2 + p2.y**2 + p2.z**2) + + p1_1 = Point3D(x1, x1, x1) + p1_2 = Point3D(y2, y2, y2) + p1_3 = Point3D(x1 + 1, x1, x1) + Point3D.are_collinear(p3) + assert Point3D.are_collinear(p3, p4) + assert Point3D.are_collinear(p3, p4, p1_1, p1_2) + assert Point3D.are_collinear(p3, p4, p1_1, p1_3) is False + assert Point3D.are_collinear(p3, p3, p4, p5) is False + + assert p3.intersection(Point3D(0, 0, 0)) == [p3] + assert p3.intersection(p4) == [] + + + assert p4 * 5 == Point3D(5, 5, 5) + assert p4 / 5 == Point3D(0.2, 0.2, 0.2) + assert 5 * p4 == Point3D(5, 5, 5) + + raises(ValueError, lambda: Point3D(0, 0, 0) + 10) + + # Test coordinate properties + assert p1.coordinates == (x1, x2, x3) + assert p2.coordinates == (y1, y2, y3) + assert p3.coordinates == (0, 0, 0) + assert p4.coordinates == (1, 1, 1) + assert p5.coordinates == (0, 1, 2) + assert p5.x == 0 + assert p5.y == 1 + assert p5.z == 2 + + # Point differences should be simplified + assert Point3D(x*(x - 1), y, 2) - Point3D(x**2 - x, y + 1, 1) == \ + Point3D(0, -1, 1) + + a, b, c = S.Half, Rational(1, 3), Rational(1, 4) + assert Point3D(a, b, c).evalf(2) == \ + Point(a.n(2), b.n(2), c.n(2), evaluate=False) + raises(ValueError, lambda: Point3D(1, 2, 3) + 1) + + # test transformations + p = Point3D(1, 1, 1) + assert p.scale(2, 3) == Point3D(2, 3, 1) + assert p.translate(1, 2) == Point3D(2, 3, 1) + assert p.translate(1) == Point3D(2, 1, 1) + assert p.translate(z=1) == Point3D(1, 1, 2) + assert p.translate(*p.args) == Point3D(2, 2, 2) + + # Test __new__ + assert Point3D(0.1, 0.2, evaluate=False, on_morph='ignore').args[0].is_Float + + # Test length property returns correctly + assert p.length == 0 + assert p1_1.length == 0 + assert p1_2.length == 0 + + # Test are_colinear type error + raises(TypeError, lambda: Point3D.are_collinear(p, x)) + + # Test are_coplanar + assert Point.are_coplanar() + assert Point.are_coplanar((1, 2, 0), (1, 2, 0), (1, 3, 0)) + assert Point.are_coplanar((1, 2, 0), (1, 2, 3)) + with warns(UserWarning, test_stacklevel=False): + raises(ValueError, lambda: Point2D.are_coplanar((1, 2), (1, 2, 3))) + assert Point3D.are_coplanar((1, 2, 0), (1, 2, 3)) + assert Point.are_coplanar((0, 0, 0), (1, 1, 0), (1, 1, 1), (1, 2, 1)) is False + planar2 = Point3D(1, -1, 1) + planar3 = Point3D(-1, 1, 1) + assert Point3D.are_coplanar(p, planar2, planar3) == True + assert Point3D.are_coplanar(p, planar2, planar3, p3) == False + assert Point.are_coplanar(p, planar2) + planar2 = Point3D(1, 1, 2) + planar3 = Point3D(1, 1, 3) + assert Point3D.are_coplanar(p, planar2, planar3) # line, not plane + plane = Plane((1, 2, 1), (2, 1, 0), (3, 1, 2)) + assert Point.are_coplanar(*[plane.projection(((-1)**i, i)) for i in range(4)]) + + # all 2D points are coplanar + assert Point.are_coplanar(Point(x, y), Point(x, x + y), Point(y, x + 2)) is True + + # Test Intersection + assert planar2.intersection(Line3D(p, planar3)) == [Point3D(1, 1, 2)] + + # Test Scale + assert planar2.scale(1, 1, 1) == planar2 + assert planar2.scale(2, 2, 2, planar3) == Point3D(1, 1, 1) + assert planar2.scale(1, 1, 1, p3) == planar2 + + # Test Transform + identity = Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + assert p.transform(identity) == p + trans = Matrix([[1, 0, 0, 1], [0, 1, 0, 1], [0, 0, 1, 1], [0, 0, 0, 1]]) + assert p.transform(trans) == Point3D(2, 2, 2) + raises(ValueError, lambda: p.transform(p)) + raises(ValueError, lambda: p.transform(Matrix([[1, 0], [0, 1]]))) + + # Test Equals + assert p.equals(x1) == False + + # Test __sub__ + p_4d = Point(0, 0, 0, 1) + with warns(UserWarning, test_stacklevel=False): + assert p - p_4d == Point(1, 1, 1, -1) + p_4d3d = Point(0, 0, 1, 0) + with warns(UserWarning, test_stacklevel=False): + assert p - p_4d3d == Point(1, 1, 0, 0) + + +def test_Point2D(): + + # Test Distance + p1 = Point2D(1, 5) + p2 = Point2D(4, 2.5) + p3 = (6, 3) + assert p1.distance(p2) == sqrt(61)/2 + assert p2.distance(p3) == sqrt(17)/2 + + # Test coordinates + assert p1.x == 1 + assert p1.y == 5 + assert p2.x == 4 + assert p2.y == S(5)/2 + assert p1.coordinates == (1, 5) + assert p2.coordinates == (4, S(5)/2) + + # test bounds + assert p1.bounds == (1, 5, 1, 5) + +def test_issue_9214(): + p1 = Point3D(4, -2, 6) + p2 = Point3D(1, 2, 3) + p3 = Point3D(7, 2, 3) + + assert Point3D.are_collinear(p1, p2, p3) is False + + +def test_issue_11617(): + p1 = Point3D(1,0,2) + p2 = Point2D(2,0) + + with warns(UserWarning, test_stacklevel=False): + assert p1.distance(p2) == sqrt(5) + + +def test_transform(): + p = Point(1, 1) + assert p.transform(rotate(pi/2)) == Point(-1, 1) + assert p.transform(scale(3, 2)) == Point(3, 2) + assert p.transform(translate(1, 2)) == Point(2, 3) + assert Point(1, 1).scale(2, 3, (4, 5)) == \ + Point(-2, -7) + assert Point(1, 1).translate(4, 5) == \ + Point(5, 6) + + +def test_concyclic_doctest_bug(): + p1, p2 = Point(-1, 0), Point(1, 0) + p3, p4 = Point(0, 1), Point(-1, 2) + assert Point.is_concyclic(p1, p2, p3) + assert not Point.is_concyclic(p1, p2, p3, p4) + + +def test_arguments(): + """Functions accepting `Point` objects in `geometry` + should also accept tuples and lists and + automatically convert them to points.""" + + singles2d = ((1,2), [1,2], Point(1,2)) + singles2d2 = ((1,3), [1,3], Point(1,3)) + doubles2d = cartes(singles2d, singles2d2) + p2d = Point2D(1,2) + singles3d = ((1,2,3), [1,2,3], Point(1,2,3)) + doubles3d = subsets(singles3d, 2) + p3d = Point3D(1,2,3) + singles4d = ((1,2,3,4), [1,2,3,4], Point(1,2,3,4)) + doubles4d = subsets(singles4d, 2) + p4d = Point(1,2,3,4) + + # test 2D + test_single = ['distance', 'is_scalar_multiple', 'taxicab_distance', 'midpoint', 'intersection', 'dot', 'equals', '__add__', '__sub__'] + test_double = ['is_concyclic', 'is_collinear'] + for p in singles2d: + Point2D(p) + for func in test_single: + for p in singles2d: + getattr(p2d, func)(p) + for func in test_double: + for p in doubles2d: + getattr(p2d, func)(*p) + + # test 3D + test_double = ['is_collinear'] + for p in singles3d: + Point3D(p) + for func in test_single: + for p in singles3d: + getattr(p3d, func)(p) + for func in test_double: + for p in doubles3d: + getattr(p3d, func)(*p) + + # test 4D + test_double = ['is_collinear'] + for p in singles4d: + Point(p) + for func in test_single: + for p in singles4d: + getattr(p4d, func)(p) + for func in test_double: + for p in doubles4d: + getattr(p4d, func)(*p) + + # test evaluate=False for ops + x = Symbol('x') + a = Point(0, 1) + assert a + (0.1, x) == Point(0.1, 1 + x, evaluate=False) + a = Point(0, 1) + assert a/10.0 == Point(0, 0.1, evaluate=False) + a = Point(0, 1) + assert a*10.0 == Point(0, 10.0, evaluate=False) + + # test evaluate=False when changing dimensions + u = Point(.1, .2, evaluate=False) + u4 = Point(u, dim=4, on_morph='ignore') + assert u4.args == (.1, .2, 0, 0) + assert all(i.is_Float for i in u4.args[:2]) + # and even when *not* changing dimensions + assert all(i.is_Float for i in Point(u).args) + + # never raise error if creating an origin + assert Point(dim=3, on_morph='error') + + # raise error with unmatched dimension + raises(ValueError, lambda: Point(1, 1, dim=3, on_morph='error')) + # test unknown on_morph + raises(ValueError, lambda: Point(1, 1, dim=3, on_morph='unknown')) + # test invalid expressions + raises(TypeError, lambda: Point(Basic(), Basic())) + +def test_unit(): + assert Point(1, 1).unit == Point(sqrt(2)/2, sqrt(2)/2) + + +def test_dot(): + raises(TypeError, lambda: Point(1, 2).dot(Line((0, 0), (1, 1)))) + + +def test__normalize_dimension(): + assert Point._normalize_dimension(Point(1, 2), Point(3, 4)) == [ + Point(1, 2), Point(3, 4)] + assert Point._normalize_dimension( + Point(1, 2), Point(3, 4, 0), on_morph='ignore') == [ + Point(1, 2, 0), Point(3, 4, 0)] + + +def test_issue_22684(): + # Used to give an error + with evaluate(False): + Point(1, 2) + + +def test_direction_cosine(): + p1 = Point3D(0, 0, 0) + p2 = Point3D(1, 1, 1) + + assert p1.direction_cosine(Point3D(1, 0, 0)) == [1, 0, 0] + assert p1.direction_cosine(Point3D(0, 1, 0)) == [0, 1, 0] + assert p1.direction_cosine(Point3D(0, 0, pi)) == [0, 0, 1] + + assert p1.direction_cosine(Point3D(5, 0, 0)) == [1, 0, 0] + assert p1.direction_cosine(Point3D(0, sqrt(3), 0)) == [0, 1, 0] + assert p1.direction_cosine(Point3D(0, 0, 5)) == [0, 0, 1] + + assert p1.direction_cosine(Point3D(2.4, 2.4, 0)) == [sqrt(2)/2, sqrt(2)/2, 0] + assert p1.direction_cosine(Point3D(1, 1, 1)) == [sqrt(3) / 3, sqrt(3) / 3, sqrt(3) / 3] + assert p1.direction_cosine(Point3D(-12, 0 -15)) == [-4*sqrt(41)/41, -5*sqrt(41)/41, 0] + + assert p2.direction_cosine(Point3D(0, 0, 0)) == [-sqrt(3) / 3, -sqrt(3) / 3, -sqrt(3) / 3] + assert p2.direction_cosine(Point3D(1, 1, 12)) == [0, 0, 1] + assert p2.direction_cosine(Point3D(12, 1, 12)) == [sqrt(2) / 2, 0, sqrt(2) / 2] diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_polygon.py b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_polygon.py new file mode 100644 index 0000000000000000000000000000000000000000..520023349f363bdb12146465305c2a5650c80934 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_polygon.py @@ -0,0 +1,676 @@ +from sympy.core.numbers import (Float, Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (acos, cos, sin) +from sympy.functions.elementary.trigonometric import tan +from sympy.geometry import (Circle, Ellipse, GeometryError, Point, Point2D, + Polygon, Ray, RegularPolygon, Segment, Triangle, + are_similar, convex_hull, intersection, Line, Ray2D) +from sympy.testing.pytest import raises, slow, warns +from sympy.core.random import verify_numerically +from sympy.geometry.polygon import rad, deg +from sympy.integrals.integrals import integrate +from sympy.utilities.iterables import rotate_left + + +def feq(a, b): + """Test if two floating point values are 'equal'.""" + t_float = Float("1.0E-10") + return -t_float < a - b < t_float + +@slow +def test_polygon(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + q = Symbol('q', real=True) + u = Symbol('u', real=True) + v = Symbol('v', real=True) + w = Symbol('w', real=True) + x1 = Symbol('x1', real=True) + half = S.Half + a, b, c = Point(0, 0), Point(2, 0), Point(3, 3) + t = Triangle(a, b, c) + assert Polygon(Point(0, 0)) == Point(0, 0) + assert Polygon(a, Point(1, 0), b, c) == t + assert Polygon(Point(1, 0), b, c, a) == t + assert Polygon(b, c, a, Point(1, 0)) == t + # 2 "remove folded" tests + assert Polygon(a, Point(3, 0), b, c) == t + assert Polygon(a, b, Point(3, -1), b, c) == t + # remove multiple collinear points + assert Polygon(Point(-4, 15), Point(-11, 15), Point(-15, 15), + Point(-15, 33/5), Point(-15, -87/10), Point(-15, -15), + Point(-42/5, -15), Point(-2, -15), Point(7, -15), Point(15, -15), + Point(15, -3), Point(15, 10), Point(15, 15)) == \ + Polygon(Point(-15, -15), Point(15, -15), Point(15, 15), Point(-15, 15)) + + p1 = Polygon( + Point(0, 0), Point(3, -1), + Point(6, 0), Point(4, 5), + Point(2, 3), Point(0, 3)) + p2 = Polygon( + Point(6, 0), Point(3, -1), + Point(0, 0), Point(0, 3), + Point(2, 3), Point(4, 5)) + p3 = Polygon( + Point(0, 0), Point(3, 0), + Point(5, 2), Point(4, 4)) + p4 = Polygon( + Point(0, 0), Point(4, 4), + Point(5, 2), Point(3, 0)) + p5 = Polygon( + Point(0, 0), Point(4, 4), + Point(0, 4)) + p6 = Polygon( + Point(-11, 1), Point(-9, 6.6), + Point(-4, -3), Point(-8.4, -8.7)) + p7 = Polygon( + Point(x, y), Point(q, u), + Point(v, w)) + p8 = Polygon( + Point(x, y), Point(v, w), + Point(q, u)) + p9 = Polygon( + Point(0, 0), Point(4, 4), + Point(3, 0), Point(5, 2)) + p10 = Polygon( + Point(0, 2), Point(2, 2), + Point(0, 0), Point(2, 0)) + p11 = Polygon(Point(0, 0), 1, n=3) + p12 = Polygon(Point(0, 0), 1, 0, n=3) + p13 = Polygon( + Point(0, 0),Point(8, 8), + Point(23, 20),Point(0, 20)) + p14 = Polygon(*rotate_left(p13.args, 1)) + + + r = Ray(Point(-9, 6.6), Point(-9, 5.5)) + # + # General polygon + # + assert p1 == p2 + assert len(p1.args) == 6 + assert len(p1.sides) == 6 + assert p1.perimeter == 5 + 2*sqrt(10) + sqrt(29) + sqrt(8) + assert p1.area == 22 + assert not p1.is_convex() + assert Polygon((-1, 1), (2, -1), (2, 1), (-1, -1), (3, 0) + ).is_convex() is False + # ensure convex for both CW and CCW point specification + assert p3.is_convex() + assert p4.is_convex() + dict5 = p5.angles + assert dict5[Point(0, 0)] == pi / 4 + assert dict5[Point(0, 4)] == pi / 2 + assert p5.encloses_point(Point(x, y)) is None + assert p5.encloses_point(Point(1, 3)) + assert p5.encloses_point(Point(0, 0)) is False + assert p5.encloses_point(Point(4, 0)) is False + assert p1.encloses(Circle(Point(2.5, 2.5), 5)) is False + assert p1.encloses(Ellipse(Point(2.5, 2), 5, 6)) is False + assert p5.plot_interval('x') == [x, 0, 1] + assert p5.distance( + Polygon(Point(10, 10), Point(14, 14), Point(10, 14))) == 6 * sqrt(2) + assert p5.distance( + Polygon(Point(1, 8), Point(5, 8), Point(8, 12), Point(1, 12))) == 4 + with warns(UserWarning, \ + match="Polygons may intersect producing erroneous output"): + Polygon(Point(0, 0), Point(1, 0), Point(1, 1)).distance( + Polygon(Point(0, 0), Point(0, 1), Point(1, 1))) + assert hash(p5) == hash(Polygon(Point(0, 0), Point(4, 4), Point(0, 4))) + assert hash(p1) == hash(p2) + assert hash(p7) == hash(p8) + assert hash(p3) != hash(p9) + assert p5 == Polygon(Point(4, 4), Point(0, 4), Point(0, 0)) + assert Polygon(Point(4, 4), Point(0, 4), Point(0, 0)) in p5 + assert p5 != Point(0, 4) + assert Point(0, 1) in p5 + assert p5.arbitrary_point('t').subs(Symbol('t', real=True), 0) == \ + Point(0, 0) + raises(ValueError, lambda: Polygon( + Point(x, 0), Point(0, y), Point(x, y)).arbitrary_point('x')) + assert p6.intersection(r) == [Point(-9, Rational(-84, 13)), Point(-9, Rational(33, 5))] + assert p10.area == 0 + assert p11 == RegularPolygon(Point(0, 0), 1, 3, 0) + assert p11 == p12 + assert p11.vertices[0] == Point(1, 0) + assert p11.args[0] == Point(0, 0) + p11.spin(pi/2) + assert p11.vertices[0] == Point(0, 1) + # + # Regular polygon + # + p1 = RegularPolygon(Point(0, 0), 10, 5) + p2 = RegularPolygon(Point(0, 0), 5, 5) + raises(GeometryError, lambda: RegularPolygon(Point(0, 0), Point(0, + 1), Point(1, 1))) + raises(GeometryError, lambda: RegularPolygon(Point(0, 0), 1, 2)) + raises(ValueError, lambda: RegularPolygon(Point(0, 0), 1, 2.5)) + + assert p1 != p2 + assert p1.interior_angle == pi*Rational(3, 5) + assert p1.exterior_angle == pi*Rational(2, 5) + assert p2.apothem == 5*cos(pi/5) + assert p2.circumcenter == p1.circumcenter == Point(0, 0) + assert p1.circumradius == p1.radius == 10 + assert p2.circumcircle == Circle(Point(0, 0), 5) + assert p2.incircle == Circle(Point(0, 0), p2.apothem) + assert p2.inradius == p2.apothem == (5 * (1 + sqrt(5)) / 4) + p2.spin(pi / 10) + dict1 = p2.angles + assert dict1[Point(0, 5)] == 3 * pi / 5 + assert p1.is_convex() + assert p1.rotation == 0 + assert p1.encloses_point(Point(0, 0)) + assert p1.encloses_point(Point(11, 0)) is False + assert p2.encloses_point(Point(0, 4.9)) + p1.spin(pi/3) + assert p1.rotation == pi/3 + assert p1.vertices[0] == Point(5, 5*sqrt(3)) + for var in p1.args: + if isinstance(var, Point): + assert var == Point(0, 0) + else: + assert var in (5, 10, pi / 3) + assert p1 != Point(0, 0) + assert p1 != p5 + + # while spin works in place (notice that rotation is 2pi/3 below) + # rotate returns a new object + p1_old = p1 + assert p1.rotate(pi/3) == RegularPolygon(Point(0, 0), 10, 5, pi*Rational(2, 3)) + assert p1 == p1_old + + assert p1.area == (-250*sqrt(5) + 1250)/(4*tan(pi/5)) + assert p1.length == 20*sqrt(-sqrt(5)/8 + Rational(5, 8)) + assert p1.scale(2, 2) == \ + RegularPolygon(p1.center, p1.radius*2, p1._n, p1.rotation) + assert RegularPolygon((0, 0), 1, 4).scale(2, 3) == \ + Polygon(Point(2, 0), Point(0, 3), Point(-2, 0), Point(0, -3)) + + assert repr(p1) == str(p1) + + # + # Angles + # + angles = p4.angles + assert feq(angles[Point(0, 0)].evalf(), Float("0.7853981633974483")) + assert feq(angles[Point(4, 4)].evalf(), Float("1.2490457723982544")) + assert feq(angles[Point(5, 2)].evalf(), Float("1.8925468811915388")) + assert feq(angles[Point(3, 0)].evalf(), Float("2.3561944901923449")) + + angles = p3.angles + assert feq(angles[Point(0, 0)].evalf(), Float("0.7853981633974483")) + assert feq(angles[Point(4, 4)].evalf(), Float("1.2490457723982544")) + assert feq(angles[Point(5, 2)].evalf(), Float("1.8925468811915388")) + assert feq(angles[Point(3, 0)].evalf(), Float("2.3561944901923449")) + + # https://github.com/sympy/sympy/issues/24885 + interior_angles_sum = sum(p13.angles.values()) + assert feq(interior_angles_sum, (len(p13.angles) - 2)*pi ) + interior_angles_sum = sum(p14.angles.values()) + assert feq(interior_angles_sum, (len(p14.angles) - 2)*pi ) + + # + # Triangle + # + p1 = Point(0, 0) + p2 = Point(5, 0) + p3 = Point(0, 5) + t1 = Triangle(p1, p2, p3) + t2 = Triangle(p1, p2, Point(Rational(5, 2), sqrt(Rational(75, 4)))) + t3 = Triangle(p1, Point(x1, 0), Point(0, x1)) + s1 = t1.sides + assert Triangle(p1, p2, p1) == Polygon(p1, p2, p1) == Segment(p1, p2) + raises(GeometryError, lambda: Triangle(Point(0, 0))) + + # Basic stuff + assert Triangle(p1, p1, p1) == p1 + assert Triangle(p2, p2*2, p2*3) == Segment(p2, p2*3) + assert t1.area == Rational(25, 2) + assert t1.is_right() + assert t2.is_right() is False + assert t3.is_right() + assert p1 in t1 + assert t1.sides[0] in t1 + assert Segment((0, 0), (1, 0)) in t1 + assert Point(5, 5) not in t2 + assert t1.is_convex() + assert feq(t1.angles[p1].evalf(), pi.evalf()/2) + + assert t1.is_equilateral() is False + assert t2.is_equilateral() + assert t3.is_equilateral() is False + assert are_similar(t1, t2) is False + assert are_similar(t1, t3) + assert are_similar(t2, t3) is False + assert t1.is_similar(Point(0, 0)) is False + assert t1.is_similar(t2) is False + + # Bisectors + bisectors = t1.bisectors() + assert bisectors[p1] == Segment( + p1, Point(Rational(5, 2), Rational(5, 2))) + assert t2.bisectors()[p2] == Segment( + Point(5, 0), Point(Rational(5, 4), 5*sqrt(3)/4)) + p4 = Point(0, x1) + assert t3.bisectors()[p4] == Segment(p4, Point(x1*(sqrt(2) - 1), 0)) + ic = (250 - 125*sqrt(2))/50 + assert t1.incenter == Point(ic, ic) + + # Inradius + assert t1.inradius == t1.incircle.radius == 5 - 5*sqrt(2)/2 + assert t2.inradius == t2.incircle.radius == 5*sqrt(3)/6 + assert t3.inradius == t3.incircle.radius == x1**2/((2 + sqrt(2))*Abs(x1)) + + # Exradius + assert t1.exradii[t1.sides[2]] == 5*sqrt(2)/2 + + # Excenters + assert t1.excenters[t1.sides[2]] == Point2D(25*sqrt(2), -5*sqrt(2)/2) + + # Circumcircle + assert t1.circumcircle.center == Point(2.5, 2.5) + + # Medians + Centroid + m = t1.medians + assert t1.centroid == Point(Rational(5, 3), Rational(5, 3)) + assert m[p1] == Segment(p1, Point(Rational(5, 2), Rational(5, 2))) + assert t3.medians[p1] == Segment(p1, Point(x1/2, x1/2)) + assert intersection(m[p1], m[p2], m[p3]) == [t1.centroid] + assert t1.medial == Triangle(Point(2.5, 0), Point(0, 2.5), Point(2.5, 2.5)) + + # Nine-point circle + assert t1.nine_point_circle == Circle(Point(2.5, 0), + Point(0, 2.5), Point(2.5, 2.5)) + assert t1.nine_point_circle == Circle(Point(0, 0), + Point(0, 2.5), Point(2.5, 2.5)) + + # Perpendicular + altitudes = t1.altitudes + assert altitudes[p1] == Segment(p1, Point(Rational(5, 2), Rational(5, 2))) + assert altitudes[p2].equals(s1[0]) + assert altitudes[p3] == s1[2] + assert t1.orthocenter == p1 + t = S('''Triangle( + Point(100080156402737/5000000000000, 79782624633431/500000000000), + Point(39223884078253/2000000000000, 156345163124289/1000000000000), + Point(31241359188437/1250000000000, 338338270939941/1000000000000000))''') + assert t.orthocenter == S('''Point(-780660869050599840216997''' + '''79471538701955848721853/80368430960602242240789074233100000000000000,''' + '''20151573611150265741278060334545897615974257/16073686192120448448157''' + '''8148466200000000000)''') + + # Ensure + assert len(intersection(*bisectors.values())) == 1 + assert len(intersection(*altitudes.values())) == 1 + assert len(intersection(*m.values())) == 1 + + # Distance + p1 = Polygon( + Point(0, 0), Point(1, 0), + Point(1, 1), Point(0, 1)) + p2 = Polygon( + Point(0, Rational(5)/4), Point(1, Rational(5)/4), + Point(1, Rational(9)/4), Point(0, Rational(9)/4)) + p3 = Polygon( + Point(1, 2), Point(2, 2), + Point(2, 1)) + p4 = Polygon( + Point(1, 1), Point(Rational(6)/5, 1), + Point(1, Rational(6)/5)) + pt1 = Point(half, half) + pt2 = Point(1, 1) + + '''Polygon to Point''' + assert p1.distance(pt1) == half + assert p1.distance(pt2) == 0 + assert p2.distance(pt1) == Rational(3)/4 + assert p3.distance(pt2) == sqrt(2)/2 + + '''Polygon to Polygon''' + # p1.distance(p2) emits a warning + with warns(UserWarning, \ + match="Polygons may intersect producing erroneous output"): + assert p1.distance(p2) == half/2 + + assert p1.distance(p3) == sqrt(2)/2 + + # p3.distance(p4) emits a warning + with warns(UserWarning, \ + match="Polygons may intersect producing erroneous output"): + assert p3.distance(p4) == (sqrt(2)/2 - sqrt(Rational(2)/25)/2) + + +def test_convex_hull(): + p = [Point(-5, -1), Point(-2, 1), Point(-2, -1), Point(-1, -3), \ + Point(0, 0), Point(1, 1), Point(2, 2), Point(2, -1), Point(3, 1), \ + Point(4, -1), Point(6, 2)] + ch = Polygon(p[0], p[3], p[9], p[10], p[6], p[1]) + #test handling of duplicate points + p.append(p[3]) + + #more than 3 collinear points + another_p = [Point(-45, -85), Point(-45, 85), Point(-45, 26), \ + Point(-45, -24)] + ch2 = Segment(another_p[0], another_p[1]) + + assert convex_hull(*another_p) == ch2 + assert convex_hull(*p) == ch + assert convex_hull(p[0]) == p[0] + assert convex_hull(p[0], p[1]) == Segment(p[0], p[1]) + + # no unique points + assert convex_hull(*[p[-1]]*3) == p[-1] + + # collection of items + assert convex_hull(*[Point(0, 0), \ + Segment(Point(1, 0), Point(1, 1)), \ + RegularPolygon(Point(2, 0), 2, 4)]) == \ + Polygon(Point(0, 0), Point(2, -2), Point(4, 0), Point(2, 2)) + + +def test_encloses(): + # square with a dimpled left side + s = Polygon(Point(0, 0), Point(1, 0), Point(1, 1), Point(0, 1), \ + Point(S.Half, S.Half)) + # the following is True if the polygon isn't treated as closing on itself + assert s.encloses(Point(0, S.Half)) is False + assert s.encloses(Point(S.Half, S.Half)) is False # it's a vertex + assert s.encloses(Point(Rational(3, 4), S.Half)) is True + + +def test_triangle_kwargs(): + assert Triangle(sss=(3, 4, 5)) == \ + Triangle(Point(0, 0), Point(3, 0), Point(3, 4)) + assert Triangle(asa=(30, 2, 30)) == \ + Triangle(Point(0, 0), Point(2, 0), Point(1, sqrt(3)/3)) + assert Triangle(sas=(1, 45, 2)) == \ + Triangle(Point(0, 0), Point(2, 0), Point(sqrt(2)/2, sqrt(2)/2)) + assert Triangle(sss=(1, 2, 5)) is None + assert deg(rad(180)) == 180 + + +def test_transform(): + pts = [Point(0, 0), Point(S.Half, Rational(1, 4)), Point(1, 1)] + pts_out = [Point(-4, -10), Point(-3, Rational(-37, 4)), Point(-2, -7)] + assert Triangle(*pts).scale(2, 3, (4, 5)) == Triangle(*pts_out) + assert RegularPolygon((0, 0), 1, 4).scale(2, 3, (4, 5)) == \ + Polygon(Point(-2, -10), Point(-4, -7), Point(-6, -10), Point(-4, -13)) + # Checks for symmetric scaling + assert RegularPolygon((0, 0), 1, 4).scale(2, 2) == \ + RegularPolygon(Point2D(0, 0), 2, 4, 0) + +def test_reflect(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + b = Symbol('b') + m = Symbol('m') + l = Line((0, b), slope=m) + p = Point(x, y) + r = p.reflect(l) + dp = l.perpendicular_segment(p).length + dr = l.perpendicular_segment(r).length + + assert verify_numerically(dp, dr) + + assert Polygon((1, 0), (2, 0), (2, 2)).reflect(Line((3, 0), slope=oo)) \ + == Triangle(Point(5, 0), Point(4, 0), Point(4, 2)) + assert Polygon((1, 0), (2, 0), (2, 2)).reflect(Line((0, 3), slope=oo)) \ + == Triangle(Point(-1, 0), Point(-2, 0), Point(-2, 2)) + assert Polygon((1, 0), (2, 0), (2, 2)).reflect(Line((0, 3), slope=0)) \ + == Triangle(Point(1, 6), Point(2, 6), Point(2, 4)) + assert Polygon((1, 0), (2, 0), (2, 2)).reflect(Line((3, 0), slope=0)) \ + == Triangle(Point(1, 0), Point(2, 0), Point(2, -2)) + +def test_bisectors(): + p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1) + p = Polygon(Point(0, 0), Point(2, 0), Point(1, 1), Point(0, 3)) + q = Polygon(Point(1, 0), Point(2, 0), Point(3, 3), Point(-1, 5)) + poly = Polygon(Point(3, 4), Point(0, 0), Point(8, 7), Point(-1, 1), Point(19, -19)) + t = Triangle(p1, p2, p3) + assert t.bisectors()[p2] == Segment(Point(1, 0), Point(0, sqrt(2) - 1)) + assert p.bisectors()[Point2D(0, 3)] == Ray2D(Point2D(0, 3), \ + Point2D(sin(acos(2*sqrt(5)/5)/2), 3 - cos(acos(2*sqrt(5)/5)/2))) + assert q.bisectors()[Point2D(-1, 5)] == \ + Ray2D(Point2D(-1, 5), Point2D(-1 + sqrt(29)*(5*sin(acos(9*sqrt(145)/145)/2) + \ + 2*cos(acos(9*sqrt(145)/145)/2))/29, sqrt(29)*(-5*cos(acos(9*sqrt(145)/145)/2) + \ + 2*sin(acos(9*sqrt(145)/145)/2))/29 + 5)) + assert poly.bisectors()[Point2D(-1, 1)] == Ray2D(Point2D(-1, 1), \ + Point2D(-1 + sin(acos(sqrt(26)/26)/2 + pi/4), 1 - sin(-acos(sqrt(26)/26)/2 + pi/4))) + +def test_incenter(): + assert Triangle(Point(0, 0), Point(1, 0), Point(0, 1)).incenter \ + == Point(1 - sqrt(2)/2, 1 - sqrt(2)/2) + +def test_inradius(): + assert Triangle(Point(0, 0), Point(4, 0), Point(0, 3)).inradius == 1 + +def test_incircle(): + assert Triangle(Point(0, 0), Point(2, 0), Point(0, 2)).incircle \ + == Circle(Point(2 - sqrt(2), 2 - sqrt(2)), 2 - sqrt(2)) + +def test_exradii(): + t = Triangle(Point(0, 0), Point(6, 0), Point(0, 2)) + assert t.exradii[t.sides[2]] == (-2 + sqrt(10)) + +def test_medians(): + t = Triangle(Point(0, 0), Point(1, 0), Point(0, 1)) + assert t.medians[Point(0, 0)] == Segment(Point(0, 0), Point(S.Half, S.Half)) + +def test_medial(): + assert Triangle(Point(0, 0), Point(1, 0), Point(0, 1)).medial \ + == Triangle(Point(S.Half, 0), Point(S.Half, S.Half), Point(0, S.Half)) + +def test_nine_point_circle(): + assert Triangle(Point(0, 0), Point(1, 0), Point(0, 1)).nine_point_circle \ + == Circle(Point2D(Rational(1, 4), Rational(1, 4)), sqrt(2)/4) + +def test_eulerline(): + assert Triangle(Point(0, 0), Point(1, 0), Point(0, 1)).eulerline \ + == Line(Point2D(0, 0), Point2D(S.Half, S.Half)) + assert Triangle(Point(0, 0), Point(10, 0), Point(5, 5*sqrt(3))).eulerline \ + == Point2D(5, 5*sqrt(3)/3) + assert Triangle(Point(4, -6), Point(4, -1), Point(-3, 3)).eulerline \ + == Line(Point2D(Rational(64, 7), 3), Point2D(Rational(-29, 14), Rational(-7, 2))) + +def test_intersection(): + poly1 = Triangle(Point(0, 0), Point(1, 0), Point(0, 1)) + poly2 = Polygon(Point(0, 1), Point(-5, 0), + Point(0, -4), Point(0, Rational(1, 5)), + Point(S.Half, -0.1), Point(1, 0), Point(0, 1)) + + assert poly1.intersection(poly2) == [Point2D(Rational(1, 3), 0), + Segment(Point(0, Rational(1, 5)), Point(0, 0)), + Segment(Point(1, 0), Point(0, 1))] + assert poly2.intersection(poly1) == [Point(Rational(1, 3), 0), + Segment(Point(0, 0), Point(0, Rational(1, 5))), + Segment(Point(1, 0), Point(0, 1))] + assert poly1.intersection(Point(0, 0)) == [Point(0, 0)] + assert poly1.intersection(Point(-12, -43)) == [] + assert poly2.intersection(Line((-12, 0), (12, 0))) == [Point(-5, 0), + Point(0, 0), Point(Rational(1, 3), 0), Point(1, 0)] + assert poly2.intersection(Line((-12, 12), (12, 12))) == [] + assert poly2.intersection(Ray((-3, 4), (1, 0))) == [Segment(Point(1, 0), + Point(0, 1))] + assert poly2.intersection(Circle((0, -1), 1)) == [Point(0, -2), + Point(0, 0)] + assert poly1.intersection(poly1) == [Segment(Point(0, 0), Point(1, 0)), + Segment(Point(0, 1), Point(0, 0)), Segment(Point(1, 0), Point(0, 1))] + assert poly2.intersection(poly2) == [Segment(Point(-5, 0), Point(0, -4)), + Segment(Point(0, -4), Point(0, Rational(1, 5))), + Segment(Point(0, Rational(1, 5)), Point(S.Half, Rational(-1, 10))), + Segment(Point(0, 1), Point(-5, 0)), + Segment(Point(S.Half, Rational(-1, 10)), Point(1, 0)), + Segment(Point(1, 0), Point(0, 1))] + assert poly2.intersection(Triangle(Point(0, 1), Point(1, 0), Point(-1, 1))) \ + == [Point(Rational(-5, 7), Rational(6, 7)), Segment(Point2D(0, 1), Point(1, 0))] + assert poly1.intersection(RegularPolygon((-12, -15), 3, 3)) == [] + + +def test_parameter_value(): + t = Symbol('t') + sq = Polygon((0, 0), (0, 1), (1, 1), (1, 0)) + assert sq.parameter_value((0.5, 1), t) == {t: Rational(3, 8)} + q = Polygon((0, 0), (2, 1), (2, 4), (4, 0)) + assert q.parameter_value((4, 0), t) == {t: -6 + 3*sqrt(5)} # ~= 0.708 + + raises(ValueError, lambda: sq.parameter_value((5, 6), t)) + raises(ValueError, lambda: sq.parameter_value(Circle(Point(0, 0), 1), t)) + + +def test_issue_12966(): + poly = Polygon(Point(0, 0), Point(0, 10), Point(5, 10), Point(5, 5), + Point(10, 5), Point(10, 0)) + t = Symbol('t') + pt = poly.arbitrary_point(t) + DELTA = 5/poly.perimeter + assert [pt.subs(t, DELTA*i) for i in range(int(1/DELTA))] == [ + Point(0, 0), Point(0, 5), Point(0, 10), Point(5, 10), + Point(5, 5), Point(10, 5), Point(10, 0), Point(5, 0)] + + +def test_second_moment_of_area(): + x, y = symbols('x, y') + # triangle + p1, p2, p3 = [(0, 0), (4, 0), (0, 2)] + p = (0, 0) + # equation of hypotenuse + eq_y = (1-x/4)*2 + I_yy = integrate((x**2) * (integrate(1, (y, 0, eq_y))), (x, 0, 4)) + I_xx = integrate(1 * (integrate(y**2, (y, 0, eq_y))), (x, 0, 4)) + I_xy = integrate(x * (integrate(y, (y, 0, eq_y))), (x, 0, 4)) + + triangle = Polygon(p1, p2, p3) + + assert (I_xx - triangle.second_moment_of_area(p)[0]) == 0 + assert (I_yy - triangle.second_moment_of_area(p)[1]) == 0 + assert (I_xy - triangle.second_moment_of_area(p)[2]) == 0 + + # rectangle + p1, p2, p3, p4=[(0, 0), (4, 0), (4, 2), (0, 2)] + I_yy = integrate((x**2) * integrate(1, (y, 0, 2)), (x, 0, 4)) + I_xx = integrate(1 * integrate(y**2, (y, 0, 2)), (x, 0, 4)) + I_xy = integrate(x * integrate(y, (y, 0, 2)), (x, 0, 4)) + + rectangle = Polygon(p1, p2, p3, p4) + + assert (I_xx - rectangle.second_moment_of_area(p)[0]) == 0 + assert (I_yy - rectangle.second_moment_of_area(p)[1]) == 0 + assert (I_xy - rectangle.second_moment_of_area(p)[2]) == 0 + + + r = RegularPolygon(Point(0, 0), 5, 3) + assert r.second_moment_of_area() == (1875*sqrt(3)/S(32), 1875*sqrt(3)/S(32), 0) + + +def test_first_moment(): + a, b = symbols('a, b', positive=True) + # rectangle + p1 = Polygon((0, 0), (a, 0), (a, b), (0, b)) + assert p1.first_moment_of_area() == (a*b**2/8, a**2*b/8) + assert p1.first_moment_of_area((a/3, b/4)) == (-3*a*b**2/32, -a**2*b/9) + + p1 = Polygon((0, 0), (40, 0), (40, 30), (0, 30)) + assert p1.first_moment_of_area() == (4500, 6000) + + # triangle + p2 = Polygon((0, 0), (a, 0), (a/2, b)) + assert p2.first_moment_of_area() == (4*a*b**2/81, a**2*b/24) + assert p2.first_moment_of_area((a/8, b/6)) == (-25*a*b**2/648, -5*a**2*b/768) + + p2 = Polygon((0, 0), (12, 0), (12, 30)) + assert p2.first_moment_of_area() == (S(1600)/3, -S(640)/3) + + +def test_section_modulus_and_polar_second_moment_of_area(): + a, b = symbols('a, b', positive=True) + x, y = symbols('x, y') + rectangle = Polygon((0, b), (0, 0), (a, 0), (a, b)) + assert rectangle.section_modulus(Point(x, y)) == (a*b**3/12/(-b/2 + y), a**3*b/12/(-a/2 + x)) + assert rectangle.polar_second_moment_of_area() == a**3*b/12 + a*b**3/12 + + convex = RegularPolygon((0, 0), 1, 6) + assert convex.section_modulus() == (Rational(5, 8), sqrt(3)*Rational(5, 16)) + assert convex.polar_second_moment_of_area() == 5*sqrt(3)/S(8) + + concave = Polygon((0, 0), (1, 8), (3, 4), (4, 6), (7, 1)) + assert concave.section_modulus() == (Rational(-6371, 429), Rational(-9778, 519)) + assert concave.polar_second_moment_of_area() == Rational(-38669, 252) + + +def test_cut_section(): + # concave polygon + p = Polygon((-1, -1), (1, Rational(5, 2)), (2, 1), (3, Rational(5, 2)), (4, 2), (5, 3), (-1, 3)) + l = Line((0, 0), (Rational(9, 2), 3)) + p1 = p.cut_section(l)[0] + p2 = p.cut_section(l)[1] + assert p1 == Polygon( + Point2D(Rational(-9, 13), Rational(-6, 13)), Point2D(1, Rational(5, 2)), Point2D(Rational(24, 13), Rational(16, 13)), + Point2D(Rational(12, 5), Rational(8, 5)), Point2D(3, Rational(5, 2)), Point2D(Rational(24, 7), Rational(16, 7)), + Point2D(Rational(9, 2), 3), Point2D(-1, 3), Point2D(-1, Rational(-2, 3))) + assert p2 == Polygon(Point2D(-1, -1), Point2D(Rational(-9, 13), Rational(-6, 13)), Point2D(Rational(24, 13), Rational(16, 13)), + Point2D(2, 1), Point2D(Rational(12, 5), Rational(8, 5)), Point2D(Rational(24, 7), Rational(16, 7)), Point2D(4, 2), Point2D(5, 3), + Point2D(Rational(9, 2), 3), Point2D(-1, Rational(-2, 3))) + + # convex polygon + p = RegularPolygon(Point2D(0, 0), 6, 6) + s = p.cut_section(Line((0, 0), slope=1)) + assert s[0] == Polygon(Point2D(-3*sqrt(3) + 9, -3*sqrt(3) + 9), Point2D(3, 3*sqrt(3)), + Point2D(-3, 3*sqrt(3)), Point2D(-6, 0), Point2D(-9 + 3*sqrt(3), -9 + 3*sqrt(3))) + assert s[1] == Polygon(Point2D(6, 0), Point2D(-3*sqrt(3) + 9, -3*sqrt(3) + 9), + Point2D(-9 + 3*sqrt(3), -9 + 3*sqrt(3)), Point2D(-3, -3*sqrt(3)), Point2D(3, -3*sqrt(3))) + + # case where line does not intersects but coincides with the edge of polygon + a, b = 20, 10 + t1, t2, t3, t4 = [(0, b), (0, 0), (a, 0), (a, b)] + p = Polygon(t1, t2, t3, t4) + p1, p2 = p.cut_section(Line((0, b), slope=0)) + assert p1 == None + assert p2 == Polygon(Point2D(0, 10), Point2D(0, 0), Point2D(20, 0), Point2D(20, 10)) + + p3, p4 = p.cut_section(Line((0, 0), slope=0)) + assert p3 == Polygon(Point2D(0, 10), Point2D(0, 0), Point2D(20, 0), Point2D(20, 10)) + assert p4 == None + + # case where the line does not intersect with a polygon at all + raises(ValueError, lambda: p.cut_section(Line((0, a), slope=0))) + +def test_type_of_triangle(): + # Isoceles triangle + p1 = Polygon(Point(0, 0), Point(5, 0), Point(2, 4)) + assert p1.is_isosceles() == True + assert p1.is_scalene() == False + assert p1.is_equilateral() == False + + # Scalene triangle + p2 = Polygon (Point(0, 0), Point(0, 2), Point(4, 0)) + assert p2.is_isosceles() == False + assert p2.is_scalene() == True + assert p2.is_equilateral() == False + + # Equilateral triangle + p3 = Polygon(Point(0, 0), Point(6, 0), Point(3, sqrt(27))) + assert p3.is_isosceles() == True + assert p3.is_scalene() == False + assert p3.is_equilateral() == True + +def test_do_poly_distance(): + # Non-intersecting polygons + square1 = Polygon (Point(0, 0), Point(0, 1), Point(1, 1), Point(1, 0)) + triangle1 = Polygon(Point(1, 2), Point(2, 2), Point(2, 1)) + assert square1._do_poly_distance(triangle1) == sqrt(2)/2 + + # Polygons which sides intersect + square2 = Polygon(Point(1, 0), Point(2, 0), Point(2, 1), Point(1, 1)) + with warns(UserWarning, \ + match="Polygons may intersect producing erroneous output", test_stacklevel=False): + assert square1._do_poly_distance(square2) == 0 + + # Polygons which bodies intersect + triangle2 = Polygon(Point(0, -1), Point(2, -1), Point(S.Half, S.Half)) + with warns(UserWarning, \ + match="Polygons may intersect producing erroneous output", test_stacklevel=False): + assert triangle2._do_poly_distance(square1) == 0 diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_util.py b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_util.py new file mode 100644 index 0000000000000000000000000000000000000000..da52a795a9383c6438ca06303e8ae6506dccdc65 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/tests/test_util.py @@ -0,0 +1,170 @@ +import pytest +from sympy.core.numbers import Float +from sympy.core.function import (Derivative, Function) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions import exp, cos, sin, tan, cosh, sinh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.geometry import Point, Point2D, Line, Polygon, Segment, convex_hull,\ + intersection, centroid, Point3D, Line3D, Ray, Ellipse +from sympy.geometry.util import idiff, closest_points, farthest_points, _ordered_points, are_coplanar +from sympy.solvers.solvers import solve +from sympy.testing.pytest import raises + + +def test_idiff(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + t = Symbol('t', real=True) + f = Function('f') + g = Function('g') + # the use of idiff in ellipse also provides coverage + circ = x**2 + y**2 - 4 + ans = -3*x*(x**2/y**2 + 1)/y**3 + assert ans == idiff(circ, y, x, 3), idiff(circ, y, x, 3) + assert ans == idiff(circ, [y], x, 3) + assert idiff(circ, y, x, 3) == ans + explicit = 12*x/sqrt(-x**2 + 4)**5 + assert ans.subs(y, solve(circ, y)[0]).equals(explicit) + assert True in [sol.diff(x, 3).equals(explicit) for sol in solve(circ, y)] + assert idiff(x + t + y, [y, t], x) == -Derivative(t, x) - 1 + assert idiff(f(x) * exp(f(x)) - x * exp(x), f(x), x) == (x + 1)*exp(x)*exp(-f(x))/(f(x) + 1) + assert idiff(f(x) - y * exp(x), [f(x), y], x) == (y + Derivative(y, x))*exp(x) + assert idiff(f(x) - y * exp(x), [y, f(x)], x) == -y + Derivative(f(x), x)*exp(-x) + assert idiff(f(x) - g(x), [f(x), g(x)], x) == Derivative(g(x), x) + # this should be fast + fxy = y - (-10*(-sin(x) + 1/x)**2 + tan(x)**2 + 2*cosh(x/10)) + assert idiff(fxy, y, x) == -20*sin(x)*cos(x) + 2*tan(x)**3 + \ + 2*tan(x) + sinh(x/10)/5 + 20*cos(x)/x - 20*sin(x)/x**2 + 20/x**3 + + +def test_intersection(): + assert intersection(Point(0, 0)) == [] + raises(TypeError, lambda: intersection(Point(0, 0), 3)) + assert intersection( + Segment((0, 0), (2, 0)), + Segment((-1, 0), (1, 0)), + Line((0, 0), (0, 1)), pairwise=True) == [ + Point(0, 0), Segment((0, 0), (1, 0))] + assert intersection( + Line((0, 0), (0, 1)), + Segment((0, 0), (2, 0)), + Segment((-1, 0), (1, 0)), pairwise=True) == [ + Point(0, 0), Segment((0, 0), (1, 0))] + assert intersection( + Line((0, 0), (0, 1)), + Segment((0, 0), (2, 0)), + Segment((-1, 0), (1, 0)), + Line((0, 0), slope=1), pairwise=True) == [ + Point(0, 0), Segment((0, 0), (1, 0))] + R = 4.0 + c = intersection( + Ray(Point2D(0.001, -1), + Point2D(0.0008, -1.7)), + Ellipse(center=Point2D(0, 0), hradius=R, vradius=2.0), pairwise=True)[0].coordinates + assert c == pytest.approx( + Point2D(0.000714285723396502, -1.99999996811224, evaluate=False).coordinates) + # check this is responds to a lower precision parameter + R = Float(4, 5) + c2 = intersection( + Ray(Point2D(0.001, -1), + Point2D(0.0008, -1.7)), + Ellipse(center=Point2D(0, 0), hradius=R, vradius=2.0), pairwise=True)[0].coordinates + assert c2 == pytest.approx( + Point2D(0.000714285723396502, -1.99999996811224, evaluate=False).coordinates) + assert c[0]._prec == 53 + assert c2[0]._prec == 20 + + +def test_convex_hull(): + raises(TypeError, lambda: convex_hull(Point(0, 0), 3)) + points = [(1, -1), (1, -2), (3, -1), (-5, -2), (15, -4)] + assert convex_hull(*points, **{"polygon": False}) == ( + [Point2D(-5, -2), Point2D(1, -1), Point2D(3, -1), Point2D(15, -4)], + [Point2D(-5, -2), Point2D(15, -4)]) + + +def test_centroid(): + p = Polygon((0, 0), (10, 0), (10, 10)) + q = p.translate(0, 20) + assert centroid(p, q) == Point(20, 40)/3 + p = Segment((0, 0), (2, 0)) + q = Segment((0, 0), (2, 2)) + assert centroid(p, q) == Point(1, -sqrt(2) + 2) + assert centroid(Point(0, 0), Point(2, 0)) == Point(2, 0)/2 + assert centroid(Point(0, 0), Point(0, 0), Point(2, 0)) == Point(2, 0)/3 + + +def test_farthest_points_closest_points(): + from sympy.core.random import randint + from sympy.utilities.iterables import subsets + + for how in (min, max): + if how == min: + func = closest_points + else: + func = farthest_points + + raises(ValueError, lambda: func(Point2D(0, 0), Point2D(0, 0))) + + # 3rd pt dx is close and pt is closer to 1st pt + p1 = [Point2D(0, 0), Point2D(3, 0), Point2D(1, 1)] + # 3rd pt dx is close and pt is closer to 2nd pt + p2 = [Point2D(0, 0), Point2D(3, 0), Point2D(2, 1)] + # 3rd pt dx is close and but pt is not closer + p3 = [Point2D(0, 0), Point2D(3, 0), Point2D(1, 10)] + # 3rd pt dx is not closer and it's closer to 2nd pt + p4 = [Point2D(0, 0), Point2D(3, 0), Point2D(4, 0)] + # 3rd pt dx is not closer and it's closer to 1st pt + p5 = [Point2D(0, 0), Point2D(3, 0), Point2D(-1, 0)] + # duplicate point doesn't affect outcome + dup = [Point2D(0, 0), Point2D(3, 0), Point2D(3, 0), Point2D(-1, 0)] + # symbolic + x = Symbol('x', positive=True) + s = [Point2D(a) for a in ((x, 1), (x + 3, 2), (x + 2, 2))] + + for points in (p1, p2, p3, p4, p5, dup, s): + d = how(i.distance(j) for i, j in subsets(set(points), 2)) + ans = a, b = list(func(*points))[0] + assert a.distance(b) == d + assert ans == _ordered_points(ans) + + # if the following ever fails, the above tests were not sufficient + # and the logical error in the routine should be fixed + points = set() + while len(points) != 7: + points.add(Point2D(randint(1, 100), randint(1, 100))) + points = list(points) + d = how(i.distance(j) for i, j in subsets(points, 2)) + ans = a, b = list(func(*points))[0] + assert a.distance(b) == d + assert ans == _ordered_points(ans) + + # equidistant points + a, b, c = ( + Point2D(0, 0), Point2D(1, 0), Point2D(S.Half, sqrt(3)/2)) + ans = {_ordered_points((i, j)) + for i, j in subsets((a, b, c), 2)} + assert closest_points(b, c, a) == ans + assert farthest_points(b, c, a) == ans + + # unique to farthest + points = [(1, 1), (1, 2), (3, 1), (-5, 2), (15, 4)] + assert farthest_points(*points) == { + (Point2D(-5, 2), Point2D(15, 4))} + points = [(1, -1), (1, -2), (3, -1), (-5, -2), (15, -4)] + assert farthest_points(*points) == { + (Point2D(-5, -2), Point2D(15, -4))} + assert farthest_points((1, 1), (0, 0)) == { + (Point2D(0, 0), Point2D(1, 1))} + raises(ValueError, lambda: farthest_points((1, 1))) + + +def test_are_coplanar(): + a = Line3D(Point3D(5, 0, 0), Point3D(1, -1, 1)) + b = Line3D(Point3D(0, -2, 0), Point3D(3, 1, 1)) + c = Line3D(Point3D(0, -1, 0), Point3D(5, -1, 9)) + d = Line(Point2D(0, 3), Point2D(1, 5)) + + assert are_coplanar(a, b, c) == False + assert are_coplanar(a, d) == False diff --git a/venv/lib/python3.11/site-packages/sympy/geometry/util.py b/venv/lib/python3.11/site-packages/sympy/geometry/util.py new file mode 100644 index 0000000000000000000000000000000000000000..1d8fb77550f2faea8185ff0c373b5f1680e623ec --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/geometry/util.py @@ -0,0 +1,731 @@ +"""Utility functions for geometrical entities. + +Contains +======== +intersection +convex_hull +closest_points +farthest_points +are_coplanar +are_similar + +""" + +from collections import deque +from math import sqrt as _sqrt + +from sympy import nsimplify +from .entity import GeometryEntity +from .exceptions import GeometryError +from .point import Point, Point2D, Point3D +from sympy.core.containers import OrderedSet +from sympy.core.exprtools import factor_terms +from sympy.core.function import Function, expand_mul +from sympy.core.numbers import Float +from sympy.core.sorting import ordered +from sympy.core.symbol import Symbol +from sympy.core.singleton import S +from sympy.polys.polytools import cancel +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.utilities.iterables import is_sequence + +from mpmath.libmp.libmpf import prec_to_dps + + +def find(x, equation): + """ + Checks whether a Symbol matching ``x`` is present in ``equation`` + or not. If present, the matching symbol is returned, else a + ValueError is raised. If ``x`` is a string the matching symbol + will have the same name; if ``x`` is a Symbol then it will be + returned if found. + + Examples + ======== + + >>> from sympy.geometry.util import find + >>> from sympy import Dummy + >>> from sympy.abc import x + >>> find('x', x) + x + >>> find('x', Dummy('x')) + _x + + The dummy symbol is returned since it has a matching name: + + >>> _.name == 'x' + True + >>> find(x, Dummy('x')) + Traceback (most recent call last): + ... + ValueError: could not find x + """ + + free = equation.free_symbols + xs = [i for i in free if (i.name if isinstance(x, str) else i) == x] + if not xs: + raise ValueError('could not find %s' % x) + if len(xs) != 1: + raise ValueError('ambiguous %s' % x) + return xs[0] + + +def _ordered_points(p): + """Return the tuple of points sorted numerically according to args""" + return tuple(sorted(p, key=lambda x: x.args)) + + +def are_coplanar(*e): + """ Returns True if the given entities are coplanar otherwise False + + Parameters + ========== + + e: entities to be checked for being coplanar + + Returns + ======= + + Boolean + + Examples + ======== + + >>> from sympy import Point3D, Line3D + >>> from sympy.geometry.util import are_coplanar + >>> a = Line3D(Point3D(5, 0, 0), Point3D(1, -1, 1)) + >>> b = Line3D(Point3D(0, -2, 0), Point3D(3, 1, 1)) + >>> c = Line3D(Point3D(0, -1, 0), Point3D(5, -1, 9)) + >>> are_coplanar(a, b, c) + False + + """ + from .line import LinearEntity3D + from .plane import Plane + # XXX update tests for coverage + + e = set(e) + # first work with a Plane if present + for i in list(e): + if isinstance(i, Plane): + e.remove(i) + return all(p.is_coplanar(i) for p in e) + + if all(isinstance(i, Point3D) for i in e): + if len(e) < 3: + return False + + # remove pts that are collinear with 2 pts + a, b = e.pop(), e.pop() + for i in list(e): + if Point3D.are_collinear(a, b, i): + e.remove(i) + + if not e: + return False + else: + # define a plane + p = Plane(a, b, e.pop()) + for i in e: + if i not in p: + return False + return True + else: + pt3d = [] + for i in e: + if isinstance(i, Point3D): + pt3d.append(i) + elif isinstance(i, LinearEntity3D): + pt3d.extend(i.args) + elif isinstance(i, GeometryEntity): # XXX we should have a GeometryEntity3D class so we can tell the difference between 2D and 3D -- here we just want to deal with 2D objects; if new 3D objects are encountered that we didn't handle above, an error should be raised + # all 2D objects have some Point that defines them; so convert those points to 3D pts by making z=0 + for p in i.args: + if isinstance(p, Point): + pt3d.append(Point3D(*(p.args + (0,)))) + return are_coplanar(*pt3d) + + +def are_similar(e1, e2): + """Are two geometrical entities similar. + + Can one geometrical entity be uniformly scaled to the other? + + Parameters + ========== + + e1 : GeometryEntity + e2 : GeometryEntity + + Returns + ======= + + are_similar : boolean + + Raises + ====== + + GeometryError + When `e1` and `e2` cannot be compared. + + Notes + ===== + + If the two objects are equal then they are similar. + + See Also + ======== + + sympy.geometry.entity.GeometryEntity.is_similar + + Examples + ======== + + >>> from sympy import Point, Circle, Triangle, are_similar + >>> c1, c2 = Circle(Point(0, 0), 4), Circle(Point(1, 4), 3) + >>> t1 = Triangle(Point(0, 0), Point(1, 0), Point(0, 1)) + >>> t2 = Triangle(Point(0, 0), Point(2, 0), Point(0, 2)) + >>> t3 = Triangle(Point(0, 0), Point(3, 0), Point(0, 1)) + >>> are_similar(t1, t2) + True + >>> are_similar(t1, t3) + False + + """ + if e1 == e2: + return True + is_similar1 = getattr(e1, 'is_similar', None) + if is_similar1: + return is_similar1(e2) + is_similar2 = getattr(e2, 'is_similar', None) + if is_similar2: + return is_similar2(e1) + n1 = e1.__class__.__name__ + n2 = e2.__class__.__name__ + raise GeometryError( + "Cannot test similarity between %s and %s" % (n1, n2)) + + +def centroid(*args): + """Find the centroid (center of mass) of the collection containing only Points, + Segments or Polygons. The centroid is the weighted average of the individual centroid + where the weights are the lengths (of segments) or areas (of polygons). + Overlapping regions will add to the weight of that region. + + If there are no objects (or a mixture of objects) then None is returned. + + See Also + ======== + + sympy.geometry.point.Point, sympy.geometry.line.Segment, + sympy.geometry.polygon.Polygon + + Examples + ======== + + >>> from sympy import Point, Segment, Polygon + >>> from sympy.geometry.util import centroid + >>> p = Polygon((0, 0), (10, 0), (10, 10)) + >>> q = p.translate(0, 20) + >>> p.centroid, q.centroid + (Point2D(20/3, 10/3), Point2D(20/3, 70/3)) + >>> centroid(p, q) + Point2D(20/3, 40/3) + >>> p, q = Segment((0, 0), (2, 0)), Segment((0, 0), (2, 2)) + >>> centroid(p, q) + Point2D(1, 2 - sqrt(2)) + >>> centroid(Point(0, 0), Point(2, 0)) + Point2D(1, 0) + + Stacking 3 polygons on top of each other effectively triples the + weight of that polygon: + + >>> p = Polygon((0, 0), (1, 0), (1, 1), (0, 1)) + >>> q = Polygon((1, 0), (3, 0), (3, 1), (1, 1)) + >>> centroid(p, q) + Point2D(3/2, 1/2) + >>> centroid(p, p, p, q) # centroid x-coord shifts left + Point2D(11/10, 1/2) + + Stacking the squares vertically above and below p has the same + effect: + + >>> centroid(p, p.translate(0, 1), p.translate(0, -1), q) + Point2D(11/10, 1/2) + + """ + from .line import Segment + from .polygon import Polygon + if args: + if all(isinstance(g, Point) for g in args): + c = Point(0, 0) + for g in args: + c += g + den = len(args) + elif all(isinstance(g, Segment) for g in args): + c = Point(0, 0) + L = 0 + for g in args: + l = g.length + c += g.midpoint*l + L += l + den = L + elif all(isinstance(g, Polygon) for g in args): + c = Point(0, 0) + A = 0 + for g in args: + a = g.area + c += g.centroid*a + A += a + den = A + c /= den + return c.func(*[i.simplify() for i in c.args]) + + +def closest_points(*args): + """Return the subset of points from a set of points that were + the closest to each other in the 2D plane. + + Parameters + ========== + + args + A collection of Points on 2D plane. + + Notes + ===== + + This can only be performed on a set of points whose coordinates can + be ordered on the number line. If there are no ties then a single + pair of Points will be in the set. + + Examples + ======== + + >>> from sympy import closest_points, Triangle + >>> Triangle(sss=(3, 4, 5)).args + (Point2D(0, 0), Point2D(3, 0), Point2D(3, 4)) + >>> closest_points(*_) + {(Point2D(0, 0), Point2D(3, 0))} + + References + ========== + + .. [1] https://www.cs.mcgill.ca/~cs251/ClosestPair/ClosestPairPS.html + + .. [2] Sweep line algorithm + https://en.wikipedia.org/wiki/Sweep_line_algorithm + + """ + p = [Point2D(i) for i in set(args)] + if len(p) < 2: + raise ValueError('At least 2 distinct points must be given.') + + try: + p.sort(key=lambda x: x.args) + except TypeError: + raise ValueError("The points could not be sorted.") + + if not all(i.is_Rational for j in p for i in j.args): + def hypot(x, y): + arg = x*x + y*y + if arg.is_Rational: + return _sqrt(arg) + return sqrt(arg) + else: + from math import hypot + + rv = [(0, 1)] + best_dist = hypot(p[1].x - p[0].x, p[1].y - p[0].y) + left = 0 + box = deque([0, 1]) + for i in range(2, len(p)): + while left < i and p[i][0] - p[left][0] > best_dist: + box.popleft() + left += 1 + + for j in box: + d = hypot(p[i].x - p[j].x, p[i].y - p[j].y) + if d < best_dist: + rv = [(j, i)] + elif d == best_dist: + rv.append((j, i)) + else: + continue + best_dist = d + box.append(i) + + return {tuple([p[i] for i in pair]) for pair in rv} + + +def convex_hull(*args, polygon=True): + """The convex hull surrounding the Points contained in the list of entities. + + Parameters + ========== + + args : a collection of Points, Segments and/or Polygons + + Optional parameters + =================== + + polygon : Boolean. If True, returns a Polygon, if false a tuple, see below. + Default is True. + + Returns + ======= + + convex_hull : Polygon if ``polygon`` is True else as a tuple `(U, L)` where + ``L`` and ``U`` are the lower and upper hulls, respectively. + + Notes + ===== + + This can only be performed on a set of points whose coordinates can + be ordered on the number line. + + See Also + ======== + + sympy.geometry.point.Point, sympy.geometry.polygon.Polygon + + Examples + ======== + + >>> from sympy import convex_hull + >>> points = [(1, 1), (1, 2), (3, 1), (-5, 2), (15, 4)] + >>> convex_hull(*points) + Polygon(Point2D(-5, 2), Point2D(1, 1), Point2D(3, 1), Point2D(15, 4)) + >>> convex_hull(*points, **dict(polygon=False)) + ([Point2D(-5, 2), Point2D(15, 4)], + [Point2D(-5, 2), Point2D(1, 1), Point2D(3, 1), Point2D(15, 4)]) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Graham_scan + + .. [2] Andrew's Monotone Chain Algorithm + (A.M. Andrew, + "Another Efficient Algorithm for Convex Hulls in Two Dimensions", 1979) + https://web.archive.org/web/20210511015444/http://geomalgorithms.com/a10-_hull-1.html + + """ + from .line import Segment + from .polygon import Polygon + p = OrderedSet() + for e in args: + if not isinstance(e, GeometryEntity): + try: + e = Point(e) + except NotImplementedError: + raise ValueError('%s is not a GeometryEntity and cannot be made into Point' % str(e)) + if isinstance(e, Point): + p.add(e) + elif isinstance(e, Segment): + p.update(e.points) + elif isinstance(e, Polygon): + p.update(e.vertices) + else: + raise NotImplementedError( + 'Convex hull for %s not implemented.' % type(e)) + + # make sure all our points are of the same dimension + if any(len(x) != 2 for x in p): + raise ValueError('Can only compute the convex hull in two dimensions') + + p = list(p) + if len(p) == 1: + return p[0] if polygon else (p[0], None) + elif len(p) == 2: + s = Segment(p[0], p[1]) + return s if polygon else (s, None) + + def _orientation(p, q, r): + '''Return positive if p-q-r are clockwise, neg if ccw, zero if + collinear.''' + return (q.y - p.y)*(r.x - p.x) - (q.x - p.x)*(r.y - p.y) + + # scan to find upper and lower convex hulls of a set of 2d points. + U = [] + L = [] + try: + p.sort(key=lambda x: x.args) + except TypeError: + raise ValueError("The points could not be sorted.") + for p_i in p: + while len(U) > 1 and _orientation(U[-2], U[-1], p_i) <= 0: + U.pop() + while len(L) > 1 and _orientation(L[-2], L[-1], p_i) >= 0: + L.pop() + U.append(p_i) + L.append(p_i) + U.reverse() + convexHull = tuple(L + U[1:-1]) + + if len(convexHull) == 2: + s = Segment(convexHull[0], convexHull[1]) + return s if polygon else (s, None) + if polygon: + return Polygon(*convexHull) + else: + U.reverse() + return (U, L) + +def farthest_points(*args): + """Return the subset of points from a set of points that were + the furthest apart from each other in the 2D plane. + + Parameters + ========== + + args + A collection of Points on 2D plane. + + Notes + ===== + + This can only be performed on a set of points whose coordinates can + be ordered on the number line. If there are no ties then a single + pair of Points will be in the set. + + Examples + ======== + + >>> from sympy.geometry import farthest_points, Triangle + >>> Triangle(sss=(3, 4, 5)).args + (Point2D(0, 0), Point2D(3, 0), Point2D(3, 4)) + >>> farthest_points(*_) + {(Point2D(0, 0), Point2D(3, 4))} + + References + ========== + + .. [1] https://code.activestate.com/recipes/117225-convex-hull-and-diameter-of-2d-point-sets/ + + .. [2] Rotating Callipers Technique + https://en.wikipedia.org/wiki/Rotating_calipers + + """ + + def rotatingCalipers(Points): + U, L = convex_hull(*Points, **{"polygon": False}) + + if L is None: + if isinstance(U, Point): + raise ValueError('At least two distinct points must be given.') + yield U.args + else: + i = 0 + j = len(L) - 1 + while i < len(U) - 1 or j > 0: + yield U[i], L[j] + # if all the way through one side of hull, advance the other side + if i == len(U) - 1: + j -= 1 + elif j == 0: + i += 1 + # still points left on both lists, compare slopes of next hull edges + # being careful to avoid divide-by-zero in slope calculation + elif (U[i+1].y - U[i].y) * (L[j].x - L[j-1].x) > \ + (L[j].y - L[j-1].y) * (U[i+1].x - U[i].x): + i += 1 + else: + j -= 1 + + p = [Point2D(i) for i in set(args)] + + if not all(i.is_Rational for j in p for i in j.args): + def hypot(x, y): + arg = x*x + y*y + if arg.is_Rational: + return _sqrt(arg) + return sqrt(arg) + else: + from math import hypot + + rv = [] + diam = 0 + for pair in rotatingCalipers(args): + h, q = _ordered_points(pair) + d = hypot(h.x - q.x, h.y - q.y) + if d > diam: + rv = [(h, q)] + elif d == diam: + rv.append((h, q)) + else: + continue + diam = d + + return set(rv) + + +def idiff(eq, y, x, n=1): + """Return ``dy/dx`` assuming that ``eq == 0``. + + Parameters + ========== + + y : the dependent variable or a list of dependent variables (with y first) + x : the variable that the derivative is being taken with respect to + n : the order of the derivative (default is 1) + + Examples + ======== + + >>> from sympy.abc import x, y, a + >>> from sympy.geometry.util import idiff + + >>> circ = x**2 + y**2 - 4 + >>> idiff(circ, y, x) + -x/y + >>> idiff(circ, y, x, 2).simplify() + (-x**2 - y**2)/y**3 + + Here, ``a`` is assumed to be independent of ``x``: + + >>> idiff(x + a + y, y, x) + -1 + + Now the x-dependence of ``a`` is made explicit by listing ``a`` after + ``y`` in a list. + + >>> idiff(x + a + y, [y, a], x) + -Derivative(a, x) - 1 + + See Also + ======== + + sympy.core.function.Derivative: represents unevaluated derivatives + sympy.core.function.diff: explicitly differentiates wrt symbols + + """ + if is_sequence(y): + dep = set(y) + y = y[0] + elif isinstance(y, Symbol): + dep = {y} + elif isinstance(y, Function): + pass + else: + raise ValueError("expecting x-dependent symbol(s) or function(s) but got: %s" % y) + + f = {s: Function(s.name)(x) for s in eq.free_symbols + if s != x and s in dep} + + if isinstance(y, Symbol): + dydx = Function(y.name)(x).diff(x) + else: + dydx = y.diff(x) + + eq = eq.subs(f) + derivs = {} + for i in range(n): + # equation will be linear in dydx, a*dydx + b, so dydx = -b/a + deq = eq.diff(x) + b = deq.xreplace({dydx: S.Zero}) + a = (deq - b).xreplace({dydx: S.One}) + yp = factor_terms(expand_mul(cancel((-b/a).subs(derivs)), deep=False)) + if i == n - 1: + return yp.subs([(v, k) for k, v in f.items()]) + derivs[dydx] = yp + eq = dydx - yp + dydx = dydx.diff(x) + + +def intersection(*entities, pairwise=False, **kwargs): + """The intersection of a collection of GeometryEntity instances. + + Parameters + ========== + entities : sequence of GeometryEntity + pairwise (keyword argument) : Can be either True or False + + Returns + ======= + intersection : list of GeometryEntity + + Raises + ====== + NotImplementedError + When unable to calculate intersection. + + Notes + ===== + The intersection of any geometrical entity with itself should return + a list with one item: the entity in question. + An intersection requires two or more entities. If only a single + entity is given then the function will return an empty list. + It is possible for `intersection` to miss intersections that one + knows exists because the required quantities were not fully + simplified internally. + Reals should be converted to Rationals, e.g. Rational(str(real_num)) + or else failures due to floating point issues may result. + + Case 1: When the keyword argument 'pairwise' is False (default value): + In this case, the function returns a list of intersections common to + all entities. + + Case 2: When the keyword argument 'pairwise' is True: + In this case, the functions returns a list intersections that occur + between any pair of entities. + + See Also + ======== + + sympy.geometry.entity.GeometryEntity.intersection + + Examples + ======== + + >>> from sympy import Ray, Circle, intersection + >>> c = Circle((0, 1), 1) + >>> intersection(c, c.center) + [] + >>> right = Ray((0, 0), (1, 0)) + >>> up = Ray((0, 0), (0, 1)) + >>> intersection(c, right, up) + [Point2D(0, 0)] + >>> intersection(c, right, up, pairwise=True) + [Point2D(0, 0), Point2D(0, 2)] + >>> left = Ray((1, 0), (0, 0)) + >>> intersection(right, left) + [Segment2D(Point2D(0, 0), Point2D(1, 0))] + + """ + if len(entities) <= 1: + return [] + + entities = list(entities) + prec = None + for i, e in enumerate(entities): + if not isinstance(e, GeometryEntity): + # entities may be an immutable tuple + e = Point(e) + # convert to exact Rationals + d = {} + for f in e.atoms(Float): + prec = f._prec if prec is None else min(f._prec, prec) + d.setdefault(f, nsimplify(f, rational=True)) + entities[i] = e.xreplace(d) + + if not pairwise: + # find the intersection common to all objects + res = entities[0].intersection(entities[1]) + for entity in entities[2:]: + newres = [] + for x in res: + newres.extend(x.intersection(entity)) + res = newres + else: + # find all pairwise intersections + ans = [] + for j in range(len(entities)): + for k in range(j + 1, len(entities)): + ans.extend(intersection(entities[j], entities[k])) + res = list(ordered(set(ans))) + + # convert back to Floats + if prec is not None: + p = prec_to_dps(prec) + res = [i.n(p) for i in res] + return res diff --git a/venv/lib/python3.11/site-packages/sympy/holonomic/__init__.py b/venv/lib/python3.11/site-packages/sympy/holonomic/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..45412acad0ab9e5c7424b1888648a638ef208142 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/holonomic/__init__.py @@ -0,0 +1,18 @@ +r""" +The :py:mod:`~sympy.holonomic` module is intended to deal with holonomic functions along +with various operations on them like addition, multiplication, composition, +integration and differentiation. The module also implements various kinds of +conversions such as converting holonomic functions to a different form and the +other way around. +""" + +from .holonomic import (DifferentialOperator, HolonomicFunction, DifferentialOperators, + from_hyper, from_meijerg, expr_to_holonomic) +from .recurrence import RecurrenceOperators, RecurrenceOperator, HolonomicSequence + +__all__ = [ + 'DifferentialOperator', 'HolonomicFunction', 'DifferentialOperators', + 'from_hyper', 'from_meijerg', 'expr_to_holonomic', + + 'RecurrenceOperators', 'RecurrenceOperator', 'HolonomicSequence', +] diff --git a/venv/lib/python3.11/site-packages/sympy/holonomic/holonomic.py b/venv/lib/python3.11/site-packages/sympy/holonomic/holonomic.py new file mode 100644 index 0000000000000000000000000000000000000000..e31c4d4511d4c07aa4049a62253cdb060758cf3d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/holonomic/holonomic.py @@ -0,0 +1,2765 @@ +""" +This module implements Holonomic Functions and +various operations on them. +""" + +from sympy.core import Add, Mul, Pow +from sympy.core.numbers import (NaN, Infinity, NegativeInfinity, Float, I, pi, + equal_valued, int_valued) +from sympy.core.singleton import S +from sympy.core.sorting import ordered +from sympy.core.symbol import Dummy, Symbol +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import binomial, factorial, rf +from sympy.functions.elementary.exponential import exp_polar, exp, log +from sympy.functions.elementary.hyperbolic import (cosh, sinh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin, sinc) +from sympy.functions.special.error_functions import (Ci, Shi, Si, erf, erfc, erfi) +from sympy.functions.special.gamma_functions import gamma +from sympy.functions.special.hyper import hyper, meijerg +from sympy.integrals import meijerint +from sympy.matrices import Matrix +from sympy.polys.rings import PolyElement +from sympy.polys.fields import FracElement +from sympy.polys.domains import QQ, RR +from sympy.polys.polyclasses import DMF +from sympy.polys.polyroots import roots +from sympy.polys.polytools import Poly +from sympy.polys.matrices import DomainMatrix +from sympy.printing import sstr +from sympy.series.limits import limit +from sympy.series.order import Order +from sympy.simplify.hyperexpand import hyperexpand +from sympy.simplify.simplify import nsimplify +from sympy.solvers.solvers import solve + +from .recurrence import HolonomicSequence, RecurrenceOperator, RecurrenceOperators +from .holonomicerrors import (NotPowerSeriesError, NotHyperSeriesError, + SingularityError, NotHolonomicError) + + +def _find_nonzero_solution(r, homosys): + ones = lambda shape: DomainMatrix.ones(shape, r.domain) + particular, nullspace = r._solve(homosys) + nullity = nullspace.shape[0] + nullpart = ones((1, nullity)) * nullspace + sol = (particular + nullpart).transpose() + return sol + + + +def DifferentialOperators(base, generator): + r""" + This function is used to create annihilators using ``Dx``. + + Explanation + =========== + + Returns an Algebra of Differential Operators also called Weyl Algebra + and the operator for differentiation i.e. the ``Dx`` operator. + + Parameters + ========== + + base: + Base polynomial ring for the algebra. + The base polynomial ring is the ring of polynomials in :math:`x` that + will appear as coefficients in the operators. + generator: + Generator of the algebra which can + be either a noncommutative ``Symbol`` or a string. e.g. "Dx" or "D". + + Examples + ======== + + >>> from sympy import ZZ + >>> from sympy.abc import x + >>> from sympy.holonomic.holonomic import DifferentialOperators + >>> R, Dx = DifferentialOperators(ZZ.old_poly_ring(x), 'Dx') + >>> R + Univariate Differential Operator Algebra in intermediate Dx over the base ring ZZ[x] + >>> Dx*x + (1) + (x)*Dx + """ + + ring = DifferentialOperatorAlgebra(base, generator) + return (ring, ring.derivative_operator) + + +class DifferentialOperatorAlgebra: + r""" + An Ore Algebra is a set of noncommutative polynomials in the + intermediate ``Dx`` and coefficients in a base polynomial ring :math:`A`. + It follows the commutation rule: + + .. math :: + Dxa = \sigma(a)Dx + \delta(a) + + for :math:`a \subset A`. + + Where :math:`\sigma: A \Rightarrow A` is an endomorphism and :math:`\delta: A \rightarrow A` + is a skew-derivation i.e. :math:`\delta(ab) = \delta(a) b + \sigma(a) \delta(b)`. + + If one takes the sigma as identity map and delta as the standard derivation + then it becomes the algebra of Differential Operators also called + a Weyl Algebra i.e. an algebra whose elements are Differential Operators. + + This class represents a Weyl Algebra and serves as the parent ring for + Differential Operators. + + Examples + ======== + + >>> from sympy import ZZ + >>> from sympy import symbols + >>> from sympy.holonomic.holonomic import DifferentialOperators + >>> x = symbols('x') + >>> R, Dx = DifferentialOperators(ZZ.old_poly_ring(x), 'Dx') + >>> R + Univariate Differential Operator Algebra in intermediate Dx over the base ring + ZZ[x] + + See Also + ======== + + DifferentialOperator + """ + + def __init__(self, base, generator): + # the base polynomial ring for the algebra + self.base = base + # the operator representing differentiation i.e. `Dx` + self.derivative_operator = DifferentialOperator( + [base.zero, base.one], self) + + if generator is None: + self.gen_symbol = Symbol('Dx', commutative=False) + else: + if isinstance(generator, str): + self.gen_symbol = Symbol(generator, commutative=False) + elif isinstance(generator, Symbol): + self.gen_symbol = generator + + def __str__(self): + string = 'Univariate Differential Operator Algebra in intermediate '\ + + sstr(self.gen_symbol) + ' over the base ring ' + \ + (self.base).__str__() + + return string + + __repr__ = __str__ + + def __eq__(self, other): + return self.base == other.base and \ + self.gen_symbol == other.gen_symbol + + +class DifferentialOperator: + """ + Differential Operators are elements of Weyl Algebra. The Operators + are defined by a list of polynomials in the base ring and the + parent ring of the Operator i.e. the algebra it belongs to. + + Explanation + =========== + + Takes a list of polynomials for each power of ``Dx`` and the + parent ring which must be an instance of DifferentialOperatorAlgebra. + + A Differential Operator can be created easily using + the operator ``Dx``. See examples below. + + Examples + ======== + + >>> from sympy.holonomic.holonomic import DifferentialOperator, DifferentialOperators + >>> from sympy import ZZ + >>> from sympy import symbols + >>> x = symbols('x') + >>> R, Dx = DifferentialOperators(ZZ.old_poly_ring(x),'Dx') + + >>> DifferentialOperator([0, 1, x**2], R) + (1)*Dx + (x**2)*Dx**2 + + >>> (x*Dx*x + 1 - Dx**2)**2 + (2*x**2 + 2*x + 1) + (4*x**3 + 2*x**2 - 4)*Dx + (x**4 - 6*x - 2)*Dx**2 + (-2*x**2)*Dx**3 + (1)*Dx**4 + + See Also + ======== + + DifferentialOperatorAlgebra + """ + + _op_priority = 20 + + def __init__(self, list_of_poly, parent): + """ + Parameters + ========== + + list_of_poly: + List of polynomials belonging to the base ring of the algebra. + parent: + Parent algebra of the operator. + """ + + # the parent ring for this operator + # must be an DifferentialOperatorAlgebra object + self.parent = parent + base = self.parent.base + self.x = base.gens[0] if isinstance(base.gens[0], Symbol) else base.gens[0][0] + # sequence of polynomials in x for each power of Dx + # the list should not have trailing zeroes + # represents the operator + # convert the expressions into ring elements using from_sympy + for i, j in enumerate(list_of_poly): + if not isinstance(j, base.dtype): + list_of_poly[i] = base.from_sympy(sympify(j)) + else: + list_of_poly[i] = base.from_sympy(base.to_sympy(j)) + + self.listofpoly = list_of_poly + # highest power of `Dx` + self.order = len(self.listofpoly) - 1 + + def __mul__(self, other): + """ + Multiplies two DifferentialOperator and returns another + DifferentialOperator instance using the commutation rule + Dx*a = a*Dx + a' + """ + + listofself = self.listofpoly + if isinstance(other, DifferentialOperator): + listofother = other.listofpoly + elif isinstance(other, self.parent.base.dtype): + listofother = [other] + else: + listofother = [self.parent.base.from_sympy(sympify(other))] + + # multiplies a polynomial `b` with a list of polynomials + def _mul_dmp_diffop(b, listofother): + if isinstance(listofother, list): + return [i * b for i in listofother] + return [b * listofother] + + sol = _mul_dmp_diffop(listofself[0], listofother) + + # compute Dx^i * b + def _mul_Dxi_b(b): + sol1 = [self.parent.base.zero] + sol2 = [] + + if isinstance(b, list): + for i in b: + sol1.append(i) + sol2.append(i.diff()) + else: + sol1.append(self.parent.base.from_sympy(b)) + sol2.append(self.parent.base.from_sympy(b).diff()) + + return _add_lists(sol1, sol2) + + for i in range(1, len(listofself)): + # find Dx^i * b in ith iteration + listofother = _mul_Dxi_b(listofother) + # solution = solution + listofself[i] * (Dx^i * b) + sol = _add_lists(sol, _mul_dmp_diffop(listofself[i], listofother)) + + return DifferentialOperator(sol, self.parent) + + def __rmul__(self, other): + if not isinstance(other, DifferentialOperator): + + if not isinstance(other, self.parent.base.dtype): + other = (self.parent.base).from_sympy(sympify(other)) + + sol = [other * j for j in self.listofpoly] + return DifferentialOperator(sol, self.parent) + + def __add__(self, other): + if isinstance(other, DifferentialOperator): + + sol = _add_lists(self.listofpoly, other.listofpoly) + return DifferentialOperator(sol, self.parent) + + list_self = self.listofpoly + if not isinstance(other, self.parent.base.dtype): + list_other = [((self.parent).base).from_sympy(sympify(other))] + else: + list_other = [other] + sol = [list_self[0] + list_other[0]] + list_self[1:] + return DifferentialOperator(sol, self.parent) + + __radd__ = __add__ + + def __sub__(self, other): + return self + (-1) * other + + def __rsub__(self, other): + return (-1) * self + other + + def __neg__(self): + return -1 * self + + def __truediv__(self, other): + return self * (S.One / other) + + def __pow__(self, n): + if n == 1: + return self + result = DifferentialOperator([self.parent.base.one], self.parent) + if n == 0: + return result + # if self is `Dx` + if self.listofpoly == self.parent.derivative_operator.listofpoly: + sol = [self.parent.base.zero]*n + [self.parent.base.one] + return DifferentialOperator(sol, self.parent) + x = self + while True: + if n % 2: + result *= x + n >>= 1 + if not n: + break + x *= x + return result + + def __str__(self): + listofpoly = self.listofpoly + print_str = '' + + for i, j in enumerate(listofpoly): + if j == self.parent.base.zero: + continue + + j = self.parent.base.to_sympy(j) + + if i == 0: + print_str += '(' + sstr(j) + ')' + continue + + if print_str: + print_str += ' + ' + + if i == 1: + print_str += '(' + sstr(j) + ')*%s' %(self.parent.gen_symbol) + continue + + print_str += '(' + sstr(j) + ')' + '*%s**' %(self.parent.gen_symbol) + sstr(i) + + return print_str + + __repr__ = __str__ + + def __eq__(self, other): + if isinstance(other, DifferentialOperator): + return self.listofpoly == other.listofpoly and \ + self.parent == other.parent + return self.listofpoly[0] == other and \ + all(i is self.parent.base.zero for i in self.listofpoly[1:]) + + def is_singular(self, x0): + """ + Checks if the differential equation is singular at x0. + """ + + base = self.parent.base + return x0 in roots(base.to_sympy(self.listofpoly[-1]), self.x) + + +class HolonomicFunction: + r""" + A Holonomic Function is a solution to a linear homogeneous ordinary + differential equation with polynomial coefficients. This differential + equation can also be represented by an annihilator i.e. a Differential + Operator ``L`` such that :math:`L.f = 0`. For uniqueness of these functions, + initial conditions can also be provided along with the annihilator. + + Explanation + =========== + + Holonomic functions have closure properties and thus forms a ring. + Given two Holonomic Functions f and g, their sum, product, + integral and derivative is also a Holonomic Function. + + For ordinary points initial condition should be a vector of values of + the derivatives i.e. :math:`[y(x_0), y'(x_0), y''(x_0) ... ]`. + + For regular singular points initial conditions can also be provided in this + format: + :math:`{s0: [C_0, C_1, ...], s1: [C^1_0, C^1_1, ...], ...}` + where s0, s1, ... are the roots of indicial equation and vectors + :math:`[C_0, C_1, ...], [C^0_0, C^0_1, ...], ...` are the corresponding initial + terms of the associated power series. See Examples below. + + Examples + ======== + + >>> from sympy.holonomic.holonomic import HolonomicFunction, DifferentialOperators + >>> from sympy import QQ + >>> from sympy import symbols, S + >>> x = symbols('x') + >>> R, Dx = DifferentialOperators(QQ.old_poly_ring(x),'Dx') + + >>> p = HolonomicFunction(Dx - 1, x, 0, [1]) # e^x + >>> q = HolonomicFunction(Dx**2 + 1, x, 0, [0, 1]) # sin(x) + + >>> p + q # annihilator of e^x + sin(x) + HolonomicFunction((-1) + (1)*Dx + (-1)*Dx**2 + (1)*Dx**3, x, 0, [1, 2, 1]) + + >>> p * q # annihilator of e^x * sin(x) + HolonomicFunction((2) + (-2)*Dx + (1)*Dx**2, x, 0, [0, 1]) + + An example of initial conditions for regular singular points, + the indicial equation has only one root `1/2`. + + >>> HolonomicFunction(-S(1)/2 + x*Dx, x, 0, {S(1)/2: [1]}) + HolonomicFunction((-1/2) + (x)*Dx, x, 0, {1/2: [1]}) + + >>> HolonomicFunction(-S(1)/2 + x*Dx, x, 0, {S(1)/2: [1]}).to_expr() + sqrt(x) + + To plot a Holonomic Function, one can use `.evalf()` for numerical + computation. Here's an example on `sin(x)**2/x` using numpy and matplotlib. + + >>> import sympy.holonomic # doctest: +SKIP + >>> from sympy import var, sin # doctest: +SKIP + >>> import matplotlib.pyplot as plt # doctest: +SKIP + >>> import numpy as np # doctest: +SKIP + >>> var("x") # doctest: +SKIP + >>> r = np.linspace(1, 5, 100) # doctest: +SKIP + >>> y = sympy.holonomic.expr_to_holonomic(sin(x)**2/x, x0=1).evalf(r) # doctest: +SKIP + >>> plt.plot(r, y, label="holonomic function") # doctest: +SKIP + >>> plt.show() # doctest: +SKIP + + """ + + _op_priority = 20 + + def __init__(self, annihilator, x, x0=0, y0=None): + """ + + Parameters + ========== + + annihilator: + Annihilator of the Holonomic Function, represented by a + `DifferentialOperator` object. + x: + Variable of the function. + x0: + The point at which initial conditions are stored. + Generally an integer. + y0: + The initial condition. The proper format for the initial condition + is described in class docstring. To make the function unique, + length of the vector `y0` should be equal to or greater than the + order of differential equation. + """ + + # initial condition + self.y0 = y0 + # the point for initial conditions, default is zero. + self.x0 = x0 + # differential operator L such that L.f = 0 + self.annihilator = annihilator + self.x = x + + def __str__(self): + if self._have_init_cond(): + str_sol = 'HolonomicFunction(%s, %s, %s, %s)' % (str(self.annihilator),\ + sstr(self.x), sstr(self.x0), sstr(self.y0)) + else: + str_sol = 'HolonomicFunction(%s, %s)' % (str(self.annihilator),\ + sstr(self.x)) + + return str_sol + + __repr__ = __str__ + + def unify(self, other): + """ + Unifies the base polynomial ring of a given two Holonomic + Functions. + """ + + R1 = self.annihilator.parent.base + R2 = other.annihilator.parent.base + + dom1 = R1.dom + dom2 = R2.dom + + if R1 == R2: + return (self, other) + + R = (dom1.unify(dom2)).old_poly_ring(self.x) + + newparent, _ = DifferentialOperators(R, str(self.annihilator.parent.gen_symbol)) + + sol1 = [R1.to_sympy(i) for i in self.annihilator.listofpoly] + sol2 = [R2.to_sympy(i) for i in other.annihilator.listofpoly] + + sol1 = DifferentialOperator(sol1, newparent) + sol2 = DifferentialOperator(sol2, newparent) + + sol1 = HolonomicFunction(sol1, self.x, self.x0, self.y0) + sol2 = HolonomicFunction(sol2, other.x, other.x0, other.y0) + + return (sol1, sol2) + + def is_singularics(self): + """ + Returns True if the function have singular initial condition + in the dictionary format. + + Returns False if the function have ordinary initial condition + in the list format. + + Returns None for all other cases. + """ + + if isinstance(self.y0, dict): + return True + elif isinstance(self.y0, list): + return False + + def _have_init_cond(self): + """ + Checks if the function have initial condition. + """ + return bool(self.y0) + + def _singularics_to_ord(self): + """ + Converts a singular initial condition to ordinary if possible. + """ + a = list(self.y0)[0] + b = self.y0[a] + + if len(self.y0) == 1 and a == int(a) and a > 0: + a = int(a) + y0 = [S.Zero] * a + y0 += [j * factorial(a + i) for i, j in enumerate(b)] + + return HolonomicFunction(self.annihilator, self.x, self.x0, y0) + + def __add__(self, other): + # if the ground domains are different + if self.annihilator.parent.base != other.annihilator.parent.base: + a, b = self.unify(other) + return a + b + + deg1 = self.annihilator.order + deg2 = other.annihilator.order + dim = max(deg1, deg2) + R = self.annihilator.parent.base + K = R.get_field() + + rowsself = [self.annihilator] + rowsother = [other.annihilator] + gen = self.annihilator.parent.derivative_operator + + # constructing annihilators up to order dim + for i in range(dim - deg1): + diff1 = (gen * rowsself[-1]) + rowsself.append(diff1) + + for i in range(dim - deg2): + diff2 = (gen * rowsother[-1]) + rowsother.append(diff2) + + row = rowsself + rowsother + + # constructing the matrix of the ansatz + r = [] + + for expr in row: + p = [] + for i in range(dim + 1): + if i >= len(expr.listofpoly): + p.append(K.zero) + else: + p.append(K.new(expr.listofpoly[i].to_list())) + r.append(p) + + # solving the linear system using gauss jordan solver + r = DomainMatrix(r, (len(row), dim+1), K).transpose() + homosys = DomainMatrix.zeros((dim+1, 1), K) + sol = _find_nonzero_solution(r, homosys) + + # if a solution is not obtained then increasing the order by 1 in each + # iteration + while sol.is_zero_matrix: + dim += 1 + + diff1 = (gen * rowsself[-1]) + rowsself.append(diff1) + + diff2 = (gen * rowsother[-1]) + rowsother.append(diff2) + + row = rowsself + rowsother + r = [] + + for expr in row: + p = [] + for i in range(dim + 1): + if i >= len(expr.listofpoly): + p.append(K.zero) + else: + p.append(K.new(expr.listofpoly[i].to_list())) + r.append(p) + + # solving the linear system using gauss jordan solver + r = DomainMatrix(r, (len(row), dim+1), K).transpose() + homosys = DomainMatrix.zeros((dim+1, 1), K) + sol = _find_nonzero_solution(r, homosys) + + # taking only the coefficients needed to multiply with `self` + # can be also be done the other way by taking R.H.S and multiplying with + # `other` + sol = sol.flat()[:dim + 1 - deg1] + sol1 = _normalize(sol, self.annihilator.parent) + # annihilator of the solution + sol = sol1 * (self.annihilator) + sol = _normalize(sol.listofpoly, self.annihilator.parent, negative=False) + + if not (self._have_init_cond() and other._have_init_cond()): + return HolonomicFunction(sol, self.x) + + # both the functions have ordinary initial conditions + if self.is_singularics() == False and other.is_singularics() == False: + + # directly add the corresponding value + if self.x0 == other.x0: + # try to extended the initial conditions + # using the annihilator + y1 = _extend_y0(self, sol.order) + y2 = _extend_y0(other, sol.order) + y0 = [a + b for a, b in zip(y1, y2)] + return HolonomicFunction(sol, self.x, self.x0, y0) + + # change the initial conditions to a same point + selfat0 = self.annihilator.is_singular(0) + otherat0 = other.annihilator.is_singular(0) + if self.x0 == 0 and not selfat0 and not otherat0: + return self + other.change_ics(0) + if other.x0 == 0 and not selfat0 and not otherat0: + return self.change_ics(0) + other + + selfatx0 = self.annihilator.is_singular(self.x0) + otheratx0 = other.annihilator.is_singular(self.x0) + if not selfatx0 and not otheratx0: + return self + other.change_ics(self.x0) + return self.change_ics(other.x0) + other + + if self.x0 != other.x0: + return HolonomicFunction(sol, self.x) + + # if the functions have singular_ics + y1 = None + y2 = None + + if self.is_singularics() == False and other.is_singularics() == True: + # convert the ordinary initial condition to singular. + _y0 = [j / factorial(i) for i, j in enumerate(self.y0)] + y1 = {S.Zero: _y0} + y2 = other.y0 + elif self.is_singularics() == True and other.is_singularics() == False: + _y0 = [j / factorial(i) for i, j in enumerate(other.y0)] + y1 = self.y0 + y2 = {S.Zero: _y0} + elif self.is_singularics() == True and other.is_singularics() == True: + y1 = self.y0 + y2 = other.y0 + + # computing singular initial condition for the result + # taking union of the series terms of both functions + y0 = {} + for i in y1: + # add corresponding initial terms if the power + # on `x` is same + if i in y2: + y0[i] = [a + b for a, b in zip(y1[i], y2[i])] + else: + y0[i] = y1[i] + for i in y2: + if i not in y1: + y0[i] = y2[i] + return HolonomicFunction(sol, self.x, self.x0, y0) + + def integrate(self, limits, initcond=False): + """ + Integrates the given holonomic function. + + Examples + ======== + + >>> from sympy.holonomic.holonomic import HolonomicFunction, DifferentialOperators + >>> from sympy import QQ + >>> from sympy import symbols + >>> x = symbols('x') + >>> R, Dx = DifferentialOperators(QQ.old_poly_ring(x),'Dx') + >>> HolonomicFunction(Dx - 1, x, 0, [1]).integrate((x, 0, x)) # e^x - 1 + HolonomicFunction((-1)*Dx + (1)*Dx**2, x, 0, [0, 1]) + >>> HolonomicFunction(Dx**2 + 1, x, 0, [1, 0]).integrate((x, 0, x)) + HolonomicFunction((1)*Dx + (1)*Dx**3, x, 0, [0, 1, 0]) + """ + + # to get the annihilator, just multiply by Dx from right + D = self.annihilator.parent.derivative_operator + + # if the function have initial conditions of the series format + if self.is_singularics() == True: + + r = self._singularics_to_ord() + if r: + return r.integrate(limits, initcond=initcond) + + # computing singular initial condition for the function + # produced after integration. + y0 = {} + for i in self.y0: + c = self.y0[i] + c2 = [] + for j, cj in enumerate(c): + if cj == 0: + c2.append(S.Zero) + + # if power on `x` is -1, the integration becomes log(x) + # TODO: Implement this case + elif i + j + 1 == 0: + raise NotImplementedError("logarithmic terms in the series are not supported") + else: + c2.append(cj / S(i + j + 1)) + y0[i + 1] = c2 + + if hasattr(limits, "__iter__"): + raise NotImplementedError("Definite integration for singular initial conditions") + + return HolonomicFunction(self.annihilator * D, self.x, self.x0, y0) + + # if no initial conditions are available for the function + if not self._have_init_cond(): + if initcond: + return HolonomicFunction(self.annihilator * D, self.x, self.x0, [S.Zero]) + return HolonomicFunction(self.annihilator * D, self.x) + + # definite integral + # initial conditions for the answer will be stored at point `a`, + # where `a` is the lower limit of the integrand + if hasattr(limits, "__iter__"): + + if len(limits) == 3 and limits[0] == self.x: + x0 = self.x0 + a = limits[1] + b = limits[2] + definite = True + + else: + definite = False + + y0 = [S.Zero] + y0 += self.y0 + + indefinite_integral = HolonomicFunction(self.annihilator * D, self.x, self.x0, y0) + + if not definite: + return indefinite_integral + + # use evalf to get the values at `a` + if x0 != a: + try: + indefinite_expr = indefinite_integral.to_expr() + except (NotHyperSeriesError, NotPowerSeriesError): + indefinite_expr = None + + if indefinite_expr: + lower = indefinite_expr.subs(self.x, a) + if isinstance(lower, NaN): + lower = indefinite_expr.limit(self.x, a) + else: + lower = indefinite_integral.evalf(a) + + if b == self.x: + y0[0] = y0[0] - lower + return HolonomicFunction(self.annihilator * D, self.x, x0, y0) + + elif S(b).is_Number: + if indefinite_expr: + upper = indefinite_expr.subs(self.x, b) + if isinstance(upper, NaN): + upper = indefinite_expr.limit(self.x, b) + else: + upper = indefinite_integral.evalf(b) + + return upper - lower + + + # if the upper limit is `x`, the answer will be a function + if b == self.x: + return HolonomicFunction(self.annihilator * D, self.x, a, y0) + + # if the upper limits is a Number, a numerical value will be returned + elif S(b).is_Number: + try: + s = HolonomicFunction(self.annihilator * D, self.x, a,\ + y0).to_expr() + indefinite = s.subs(self.x, b) + if not isinstance(indefinite, NaN): + return indefinite + else: + return s.limit(self.x, b) + except (NotHyperSeriesError, NotPowerSeriesError): + return HolonomicFunction(self.annihilator * D, self.x, a, y0).evalf(b) + + return HolonomicFunction(self.annihilator * D, self.x) + + def diff(self, *args, **kwargs): + r""" + Differentiation of the given Holonomic function. + + Examples + ======== + + >>> from sympy.holonomic.holonomic import HolonomicFunction, DifferentialOperators + >>> from sympy import ZZ + >>> from sympy import symbols + >>> x = symbols('x') + >>> R, Dx = DifferentialOperators(ZZ.old_poly_ring(x),'Dx') + >>> HolonomicFunction(Dx**2 + 1, x, 0, [0, 1]).diff().to_expr() + cos(x) + >>> HolonomicFunction(Dx - 2, x, 0, [1]).diff().to_expr() + 2*exp(2*x) + + See Also + ======== + + integrate + """ + kwargs.setdefault('evaluate', True) + if args: + if args[0] != self.x: + return S.Zero + elif len(args) == 2: + sol = self + for i in range(args[1]): + sol = sol.diff(args[0]) + return sol + + ann = self.annihilator + + # if the function is constant. + if ann.listofpoly[0] == ann.parent.base.zero and ann.order == 1: + return S.Zero + + # if the coefficient of y in the differential equation is zero. + # a shifting is done to compute the answer in this case. + elif ann.listofpoly[0] == ann.parent.base.zero: + + sol = DifferentialOperator(ann.listofpoly[1:], ann.parent) + + if self._have_init_cond(): + # if ordinary initial condition + if self.is_singularics() == False: + return HolonomicFunction(sol, self.x, self.x0, self.y0[1:]) + # TODO: support for singular initial condition + return HolonomicFunction(sol, self.x) + else: + return HolonomicFunction(sol, self.x) + + # the general algorithm + R = ann.parent.base + K = R.get_field() + + seq_dmf = [K.new(i.to_list()) for i in ann.listofpoly] + + # -y = a1*y'/a0 + a2*y''/a0 ... + an*y^n/a0 + rhs = [i / seq_dmf[0] for i in seq_dmf[1:]] + rhs.insert(0, K.zero) + + # differentiate both lhs and rhs + sol = _derivate_diff_eq(rhs, K) + + # add the term y' in lhs to rhs + sol = _add_lists(sol, [K.zero, K.one]) + + sol = _normalize(sol[1:], self.annihilator.parent, negative=False) + + if not self._have_init_cond() or self.is_singularics() == True: + return HolonomicFunction(sol, self.x) + + y0 = _extend_y0(self, sol.order + 1)[1:] + return HolonomicFunction(sol, self.x, self.x0, y0) + + def __eq__(self, other): + if self.annihilator != other.annihilator or self.x != other.x: + return False + if self._have_init_cond() and other._have_init_cond(): + return self.x0 == other.x0 and self.y0 == other.y0 + return True + + def __mul__(self, other): + ann_self = self.annihilator + + if not isinstance(other, HolonomicFunction): + other = sympify(other) + + if other.has(self.x): + raise NotImplementedError(" Can't multiply a HolonomicFunction and expressions/functions.") + + if not self._have_init_cond(): + return self + y0 = _extend_y0(self, ann_self.order) + y1 = [(Poly.new(j, self.x) * other).rep for j in y0] + return HolonomicFunction(ann_self, self.x, self.x0, y1) + + if self.annihilator.parent.base != other.annihilator.parent.base: + a, b = self.unify(other) + return a * b + + ann_other = other.annihilator + + a = ann_self.order + b = ann_other.order + + R = ann_self.parent.base + K = R.get_field() + + list_self = [K.new(j.to_list()) for j in ann_self.listofpoly] + list_other = [K.new(j.to_list()) for j in ann_other.listofpoly] + + # will be used to reduce the degree + self_red = [-list_self[i] / list_self[a] for i in range(a)] + + other_red = [-list_other[i] / list_other[b] for i in range(b)] + + # coeff_mull[i][j] is the coefficient of Dx^i(f).Dx^j(g) + coeff_mul = [[K.zero for i in range(b + 1)] for j in range(a + 1)] + coeff_mul[0][0] = K.one + + # making the ansatz + lin_sys_elements = [[coeff_mul[i][j] for i in range(a) for j in range(b)]] + lin_sys = DomainMatrix(lin_sys_elements, (1, a*b), K).transpose() + + homo_sys = DomainMatrix.zeros((a*b, 1), K) + + sol = _find_nonzero_solution(lin_sys, homo_sys) + + # until a non trivial solution is found + while sol.is_zero_matrix: + + # updating the coefficients Dx^i(f).Dx^j(g) for next degree + for i in range(a - 1, -1, -1): + for j in range(b - 1, -1, -1): + coeff_mul[i][j + 1] += coeff_mul[i][j] + coeff_mul[i + 1][j] += coeff_mul[i][j] + if isinstance(coeff_mul[i][j], K.dtype): + coeff_mul[i][j] = DMFdiff(coeff_mul[i][j], K) + else: + coeff_mul[i][j] = coeff_mul[i][j].diff(self.x) + + # reduce the terms to lower power using annihilators of f, g + for i in range(a + 1): + if coeff_mul[i][b].is_zero: + continue + for j in range(b): + coeff_mul[i][j] += other_red[j] * coeff_mul[i][b] + coeff_mul[i][b] = K.zero + + # not d2 + 1, as that is already covered in previous loop + for j in range(b): + if coeff_mul[a][j] == 0: + continue + for i in range(a): + coeff_mul[i][j] += self_red[i] * coeff_mul[a][j] + coeff_mul[a][j] = K.zero + + lin_sys_elements.append([coeff_mul[i][j] for i in range(a) for j in range(b)]) + lin_sys = DomainMatrix(lin_sys_elements, (len(lin_sys_elements), a*b), K).transpose() + + sol = _find_nonzero_solution(lin_sys, homo_sys) + + sol_ann = _normalize(sol.flat(), self.annihilator.parent, negative=False) + + if not (self._have_init_cond() and other._have_init_cond()): + return HolonomicFunction(sol_ann, self.x) + + if self.is_singularics() == False and other.is_singularics() == False: + + # if both the conditions are at same point + if self.x0 == other.x0: + + # try to find more initial conditions + y0_self = _extend_y0(self, sol_ann.order) + y0_other = _extend_y0(other, sol_ann.order) + # h(x0) = f(x0) * g(x0) + y0 = [y0_self[0] * y0_other[0]] + + # coefficient of Dx^j(f)*Dx^i(g) in Dx^i(fg) + for i in range(1, min(len(y0_self), len(y0_other))): + coeff = [[0 for i in range(i + 1)] for j in range(i + 1)] + for j in range(i + 1): + for k in range(i + 1): + if j + k == i: + coeff[j][k] = binomial(i, j) + + sol = 0 + for j in range(i + 1): + for k in range(i + 1): + sol += coeff[j][k]* y0_self[j] * y0_other[k] + + y0.append(sol) + + return HolonomicFunction(sol_ann, self.x, self.x0, y0) + + # if the points are different, consider one + selfat0 = self.annihilator.is_singular(0) + otherat0 = other.annihilator.is_singular(0) + + if self.x0 == 0 and not selfat0 and not otherat0: + return self * other.change_ics(0) + if other.x0 == 0 and not selfat0 and not otherat0: + return self.change_ics(0) * other + + selfatx0 = self.annihilator.is_singular(self.x0) + otheratx0 = other.annihilator.is_singular(self.x0) + if not selfatx0 and not otheratx0: + return self * other.change_ics(self.x0) + return self.change_ics(other.x0) * other + + if self.x0 != other.x0: + return HolonomicFunction(sol_ann, self.x) + + # if the functions have singular_ics + y1 = None + y2 = None + + if self.is_singularics() == False and other.is_singularics() == True: + _y0 = [j / factorial(i) for i, j in enumerate(self.y0)] + y1 = {S.Zero: _y0} + y2 = other.y0 + elif self.is_singularics() == True and other.is_singularics() == False: + _y0 = [j / factorial(i) for i, j in enumerate(other.y0)] + y1 = self.y0 + y2 = {S.Zero: _y0} + elif self.is_singularics() == True and other.is_singularics() == True: + y1 = self.y0 + y2 = other.y0 + + y0 = {} + # multiply every possible pair of the series terms + for i in y1: + for j in y2: + k = min(len(y1[i]), len(y2[j])) + c = [sum((y1[i][b] * y2[j][a - b] for b in range(a + 1)), + start=S.Zero) for a in range(k)] + if not i + j in y0: + y0[i + j] = c + else: + y0[i + j] = [a + b for a, b in zip(c, y0[i + j])] + return HolonomicFunction(sol_ann, self.x, self.x0, y0) + + __rmul__ = __mul__ + + def __sub__(self, other): + return self + other * -1 + + def __rsub__(self, other): + return self * -1 + other + + def __neg__(self): + return -1 * self + + def __truediv__(self, other): + return self * (S.One / other) + + def __pow__(self, n): + if self.annihilator.order <= 1: + ann = self.annihilator + parent = ann.parent + + if self.y0 is None: + y0 = None + else: + y0 = [list(self.y0)[0] ** n] + + p0 = ann.listofpoly[0] + p1 = ann.listofpoly[1] + + p0 = (Poly.new(p0, self.x) * n).rep + + sol = [parent.base.to_sympy(i) for i in [p0, p1]] + dd = DifferentialOperator(sol, parent) + return HolonomicFunction(dd, self.x, self.x0, y0) + if n < 0: + raise NotHolonomicError("Negative Power on a Holonomic Function") + Dx = self.annihilator.parent.derivative_operator + result = HolonomicFunction(Dx, self.x, S.Zero, [S.One]) + if n == 0: + return result + x = self + while True: + if n % 2: + result *= x + n >>= 1 + if not n: + break + x *= x + return result + + def degree(self): + """ + Returns the highest power of `x` in the annihilator. + """ + return max(i.degree() for i in self.annihilator.listofpoly) + + def composition(self, expr, *args, **kwargs): + """ + Returns function after composition of a holonomic + function with an algebraic function. The method cannot compute + initial conditions for the result by itself, so they can be also be + provided. + + Examples + ======== + + >>> from sympy.holonomic.holonomic import HolonomicFunction, DifferentialOperators + >>> from sympy import QQ + >>> from sympy import symbols + >>> x = symbols('x') + >>> R, Dx = DifferentialOperators(QQ.old_poly_ring(x),'Dx') + >>> HolonomicFunction(Dx - 1, x).composition(x**2, 0, [1]) # e^(x**2) + HolonomicFunction((-2*x) + (1)*Dx, x, 0, [1]) + >>> HolonomicFunction(Dx**2 + 1, x).composition(x**2 - 1, 1, [1, 0]) + HolonomicFunction((4*x**3) + (-1)*Dx + (x)*Dx**2, x, 1, [1, 0]) + + See Also + ======== + + from_hyper + """ + + R = self.annihilator.parent + a = self.annihilator.order + diff = expr.diff(self.x) + listofpoly = self.annihilator.listofpoly + + for i, j in enumerate(listofpoly): + if isinstance(j, self.annihilator.parent.base.dtype): + listofpoly[i] = self.annihilator.parent.base.to_sympy(j) + + r = listofpoly[a].subs({self.x:expr}) + subs = [-listofpoly[i].subs({self.x:expr}) / r for i in range (a)] + coeffs = [S.Zero for i in range(a)] # coeffs[i] == coeff of (D^i f)(a) in D^k (f(a)) + coeffs[0] = S.One + system = [coeffs] + homogeneous = Matrix([[S.Zero for i in range(a)]]).transpose() + while True: + coeffs_next = [p.diff(self.x) for p in coeffs] + for i in range(a - 1): + coeffs_next[i + 1] += (coeffs[i] * diff) + for i in range(a): + coeffs_next[i] += (coeffs[-1] * subs[i] * diff) + coeffs = coeffs_next + # check for linear relations + system.append(coeffs) + sol, taus = (Matrix(system).transpose() + ).gauss_jordan_solve(homogeneous) + if sol.is_zero_matrix is not True: + break + + tau = list(taus)[0] + sol = sol.subs(tau, 1) + sol = _normalize(sol[0:], R, negative=False) + + # if initial conditions are given for the resulting function + if args: + return HolonomicFunction(sol, self.x, args[0], args[1]) + return HolonomicFunction(sol, self.x) + + def to_sequence(self, lb=True): + r""" + Finds recurrence relation for the coefficients in the series expansion + of the function about :math:`x_0`, where :math:`x_0` is the point at + which the initial condition is stored. + + Explanation + =========== + + If the point :math:`x_0` is ordinary, solution of the form :math:`[(R, n_0)]` + is returned. Where :math:`R` is the recurrence relation and :math:`n_0` is the + smallest ``n`` for which the recurrence holds true. + + If the point :math:`x_0` is regular singular, a list of solutions in + the format :math:`(R, p, n_0)` is returned, i.e. `[(R, p, n_0), ... ]`. + Each tuple in this vector represents a recurrence relation :math:`R` + associated with a root of the indicial equation ``p``. Conditions of + a different format can also be provided in this case, see the + docstring of HolonomicFunction class. + + If it's not possible to numerically compute a initial condition, + it is returned as a symbol :math:`C_j`, denoting the coefficient of + :math:`(x - x_0)^j` in the power series about :math:`x_0`. + + Examples + ======== + + >>> from sympy.holonomic.holonomic import HolonomicFunction, DifferentialOperators + >>> from sympy import QQ + >>> from sympy import symbols, S + >>> x = symbols('x') + >>> R, Dx = DifferentialOperators(QQ.old_poly_ring(x),'Dx') + >>> HolonomicFunction(Dx - 1, x, 0, [1]).to_sequence() + [(HolonomicSequence((-1) + (n + 1)Sn, n), u(0) = 1, 0)] + >>> HolonomicFunction((1 + x)*Dx**2 + Dx, x, 0, [0, 1]).to_sequence() + [(HolonomicSequence((n**2) + (n**2 + n)Sn, n), u(0) = 0, u(1) = 1, u(2) = -1/2, 2)] + >>> HolonomicFunction(-S(1)/2 + x*Dx, x, 0, {S(1)/2: [1]}).to_sequence() + [(HolonomicSequence((n), n), u(0) = 1, 1/2, 1)] + + See Also + ======== + + HolonomicFunction.series + + References + ========== + + .. [1] https://hal.inria.fr/inria-00070025/document + .. [2] https://www3.risc.jku.at/publications/download/risc_2244/DIPLFORM.pdf + + """ + + if self.x0 != 0: + return self.shift_x(self.x0).to_sequence() + + # check whether a power series exists if the point is singular + if self.annihilator.is_singular(self.x0): + return self._frobenius(lb=lb) + + dict1 = {} + n = Symbol('n', integer=True) + dom = self.annihilator.parent.base.dom + R, _ = RecurrenceOperators(dom.old_poly_ring(n), 'Sn') + + # substituting each term of the form `x^k Dx^j` in the + # annihilator, according to the formula below: + # x^k Dx^j = Sum(rf(n + 1 - k, j) * a(n + j - k) * x^n, (n, k, oo)) + # for explanation see [2]. + for i, j in enumerate(self.annihilator.listofpoly): + + listofdmp = j.all_coeffs() + degree = len(listofdmp) - 1 + + for k in range(degree + 1): + coeff = listofdmp[degree - k] + + if coeff == 0: + continue + + if (i - k, k) in dict1: + dict1[(i - k, k)] += (dom.to_sympy(coeff) * rf(n - k + 1, i)) + else: + dict1[(i - k, k)] = (dom.to_sympy(coeff) * rf(n - k + 1, i)) + + + sol = [] + keylist = [i[0] for i in dict1] + lower = min(keylist) + upper = max(keylist) + degree = self.degree() + + # the recurrence relation holds for all values of + # n greater than smallest_n, i.e. n >= smallest_n + smallest_n = lower + degree + dummys = {} + eqs = [] + unknowns = [] + + # an appropriate shift of the recurrence + for j in range(lower, upper + 1): + if j in keylist: + temp = sum((v.subs(n, n - lower) + for k, v in dict1.items() if k[0] == j), + start=S.Zero) + sol.append(temp) + else: + sol.append(S.Zero) + + # the recurrence relation + sol = RecurrenceOperator(sol, R) + + # computing the initial conditions for recurrence + order = sol.order + all_roots = roots(R.base.to_sympy(sol.listofpoly[-1]), n, filter='Z') + all_roots = all_roots.keys() + + if all_roots: + max_root = max(all_roots) + 1 + smallest_n = max(max_root, smallest_n) + order += smallest_n + + y0 = _extend_y0(self, order) + # u(n) = y^n(0)/factorial(n) + u0 = [j / factorial(i) for i, j in enumerate(y0)] + + # if sufficient conditions can't be computed then + # try to use the series method i.e. + # equate the coefficients of x^k in the equation formed by + # substituting the series in differential equation, to zero. + if len(u0) < order: + + for i in range(degree): + eq = S.Zero + + for j in dict1: + + if i + j[0] < 0: + dummys[i + j[0]] = S.Zero + + elif i + j[0] < len(u0): + dummys[i + j[0]] = u0[i + j[0]] + + elif not i + j[0] in dummys: + dummys[i + j[0]] = Symbol('C_%s' %(i + j[0])) + unknowns.append(dummys[i + j[0]]) + + if j[1] <= i: + eq += dict1[j].subs(n, i) * dummys[i + j[0]] + + eqs.append(eq) + + # solve the system of equations formed + soleqs = solve(eqs, *unknowns) + + if isinstance(soleqs, dict): + + for i in range(len(u0), order): + + if i not in dummys: + dummys[i] = Symbol('C_%s' %i) + + if dummys[i] in soleqs: + u0.append(soleqs[dummys[i]]) + + else: + u0.append(dummys[i]) + + if lb: + return [(HolonomicSequence(sol, u0), smallest_n)] + return [HolonomicSequence(sol, u0)] + + for i in range(len(u0), order): + + if i not in dummys: + dummys[i] = Symbol('C_%s' %i) + + s = False + for j in soleqs: + if dummys[i] in j: + u0.append(j[dummys[i]]) + s = True + if not s: + u0.append(dummys[i]) + + if lb: + return [(HolonomicSequence(sol, u0), smallest_n)] + + return [HolonomicSequence(sol, u0)] + + def _frobenius(self, lb=True): + # compute the roots of indicial equation + indicialroots = self._indicial() + + reals = [] + compl = [] + for i in ordered(indicialroots.keys()): + if i.is_real: + reals.extend([i] * indicialroots[i]) + else: + a, b = i.as_real_imag() + compl.extend([(i, a, b)] * indicialroots[i]) + + # sort the roots for a fixed ordering of solution + compl.sort(key=lambda x : x[1]) + compl.sort(key=lambda x : x[2]) + reals.sort() + + # grouping the roots, roots differ by an integer are put in the same group. + grp = [] + + for i in reals: + if len(grp) == 0: + grp.append([i]) + continue + for j in grp: + if int_valued(j[0] - i): + j.append(i) + break + else: + grp.append([i]) + + # True if none of the roots differ by an integer i.e. + # each element in group have only one member + independent = all(len(i) == 1 for i in grp) + + allpos = all(i >= 0 for i in reals) + allint = all(int_valued(i) for i in reals) + + # if initial conditions are provided + # then use them. + if self.is_singularics() == True: + rootstoconsider = [] + for i in ordered(self.y0.keys()): + for j in ordered(indicialroots.keys()): + if equal_valued(j, i): + rootstoconsider.append(i) + + elif allpos and allint: + rootstoconsider = [min(reals)] + + elif independent: + rootstoconsider = [i[0] for i in grp] + [j[0] for j in compl] + + elif not allint: + rootstoconsider = [i for i in reals if not int(i) == i] + + elif not allpos: + + if not self._have_init_cond() or S(self.y0[0]).is_finite == False: + rootstoconsider = [min(reals)] + + else: + posroots = [i for i in reals if i >= 0] + rootstoconsider = [min(posroots)] + + n = Symbol('n', integer=True) + dom = self.annihilator.parent.base.dom + R, _ = RecurrenceOperators(dom.old_poly_ring(n), 'Sn') + + finalsol = [] + char = ord('C') + + for p in rootstoconsider: + dict1 = {} + + for i, j in enumerate(self.annihilator.listofpoly): + + listofdmp = j.all_coeffs() + degree = len(listofdmp) - 1 + + for k in range(degree + 1): + coeff = listofdmp[degree - k] + + if coeff == 0: + continue + + if (i - k, k - i) in dict1: + dict1[(i - k, k - i)] += (dom.to_sympy(coeff) * rf(n - k + 1 + p, i)) + else: + dict1[(i - k, k - i)] = (dom.to_sympy(coeff) * rf(n - k + 1 + p, i)) + + sol = [] + keylist = [i[0] for i in dict1] + lower = min(keylist) + upper = max(keylist) + degree = max(i[1] for i in dict1) + degree2 = min(i[1] for i in dict1) + + smallest_n = lower + degree + dummys = {} + eqs = [] + unknowns = [] + + for j in range(lower, upper + 1): + if j in keylist: + temp = sum((v.subs(n, n - lower) + for k, v in dict1.items() if k[0] == j), + start=S.Zero) + sol.append(temp) + else: + sol.append(S.Zero) + + # the recurrence relation + sol = RecurrenceOperator(sol, R) + + # computing the initial conditions for recurrence + order = sol.order + all_roots = roots(R.base.to_sympy(sol.listofpoly[-1]), n, filter='Z') + all_roots = all_roots.keys() + + if all_roots: + max_root = max(all_roots) + 1 + smallest_n = max(max_root, smallest_n) + order += smallest_n + + u0 = [] + + if self.is_singularics() == True: + u0 = self.y0[p] + + elif self.is_singularics() == False and p >= 0 and int(p) == p and len(rootstoconsider) == 1: + y0 = _extend_y0(self, order + int(p)) + # u(n) = y^n(0)/factorial(n) + if len(y0) > int(p): + u0 = [y0[i] / factorial(i) for i in range(int(p), len(y0))] + + if len(u0) < order: + + for i in range(degree2, degree): + eq = S.Zero + + for j in dict1: + if i + j[0] < 0: + dummys[i + j[0]] = S.Zero + + elif i + j[0] < len(u0): + dummys[i + j[0]] = u0[i + j[0]] + + elif not i + j[0] in dummys: + letter = chr(char) + '_%s' %(i + j[0]) + dummys[i + j[0]] = Symbol(letter) + unknowns.append(dummys[i + j[0]]) + + if j[1] <= i: + eq += dict1[j].subs(n, i) * dummys[i + j[0]] + + eqs.append(eq) + + # solve the system of equations formed + soleqs = solve(eqs, *unknowns) + + if isinstance(soleqs, dict): + + for i in range(len(u0), order): + + if i not in dummys: + letter = chr(char) + '_%s' %i + dummys[i] = Symbol(letter) + + if dummys[i] in soleqs: + u0.append(soleqs[dummys[i]]) + + else: + u0.append(dummys[i]) + + if lb: + finalsol.append((HolonomicSequence(sol, u0), p, smallest_n)) + continue + else: + finalsol.append((HolonomicSequence(sol, u0), p)) + continue + + for i in range(len(u0), order): + + if i not in dummys: + letter = chr(char) + '_%s' %i + dummys[i] = Symbol(letter) + + s = False + for j in soleqs: + if dummys[i] in j: + u0.append(j[dummys[i]]) + s = True + if not s: + u0.append(dummys[i]) + if lb: + finalsol.append((HolonomicSequence(sol, u0), p, smallest_n)) + + else: + finalsol.append((HolonomicSequence(sol, u0), p)) + char += 1 + return finalsol + + def series(self, n=6, coefficient=False, order=True, _recur=None): + r""" + Finds the power series expansion of given holonomic function about :math:`x_0`. + + Explanation + =========== + + A list of series might be returned if :math:`x_0` is a regular point with + multiple roots of the indicial equation. + + Examples + ======== + + >>> from sympy.holonomic.holonomic import HolonomicFunction, DifferentialOperators + >>> from sympy import QQ + >>> from sympy import symbols + >>> x = symbols('x') + >>> R, Dx = DifferentialOperators(QQ.old_poly_ring(x),'Dx') + >>> HolonomicFunction(Dx - 1, x, 0, [1]).series() # e^x + 1 + x + x**2/2 + x**3/6 + x**4/24 + x**5/120 + O(x**6) + >>> HolonomicFunction(Dx**2 + 1, x, 0, [0, 1]).series(n=8) # sin(x) + x - x**3/6 + x**5/120 - x**7/5040 + O(x**8) + + See Also + ======== + + HolonomicFunction.to_sequence + """ + + if _recur is None: + recurrence = self.to_sequence() + else: + recurrence = _recur + + if isinstance(recurrence, tuple) and len(recurrence) == 2: + recurrence = recurrence[0] + constantpower = 0 + elif isinstance(recurrence, tuple) and len(recurrence) == 3: + constantpower = recurrence[1] + recurrence = recurrence[0] + + elif len(recurrence) == 1 and len(recurrence[0]) == 2: + recurrence = recurrence[0][0] + constantpower = 0 + elif len(recurrence) == 1 and len(recurrence[0]) == 3: + constantpower = recurrence[0][1] + recurrence = recurrence[0][0] + else: + return [self.series(_recur=i) for i in recurrence] + + n = n - int(constantpower) + l = len(recurrence.u0) - 1 + k = recurrence.recurrence.order + x = self.x + x0 = self.x0 + seq_dmp = recurrence.recurrence.listofpoly + R = recurrence.recurrence.parent.base + K = R.get_field() + seq = [K.new(j.to_list()) for j in seq_dmp] + sub = [-seq[i] / seq[k] for i in range(k)] + sol = list(recurrence.u0) + + if l + 1 < n: + # use the initial conditions to find the next term + for i in range(l + 1 - k, n - k): + coeff = sum((DMFsubs(sub[j], i) * sol[i + j] + for j in range(k) if i + j >= 0), start=S.Zero) + sol.append(coeff) + + if coefficient: + return sol + + ser = sum((x**(i + constantpower) * j for i, j in enumerate(sol)), + start=S.Zero) + if order: + ser += Order(x**(n + int(constantpower)), x) + if x0 != 0: + return ser.subs(x, x - x0) + return ser + + def _indicial(self): + """ + Computes roots of the Indicial equation. + """ + + if self.x0 != 0: + return self.shift_x(self.x0)._indicial() + + list_coeff = self.annihilator.listofpoly + R = self.annihilator.parent.base + x = self.x + s = R.zero + y = R.one + + def _pole_degree(poly): + root_all = roots(R.to_sympy(poly), x, filter='Z') + if 0 in root_all.keys(): + return root_all[0] + else: + return 0 + + degree = max(j.degree() for j in list_coeff) + inf = 10 * (max(1, degree) + max(1, self.annihilator.order)) + + deg = lambda q: inf if q.is_zero else _pole_degree(q) + b = min(deg(q) - j for j, q in enumerate(list_coeff)) + + for i, j in enumerate(list_coeff): + listofdmp = j.all_coeffs() + degree = len(listofdmp) - 1 + if 0 <= i + b <= degree: + s = s + listofdmp[degree - i - b] * y + y *= R.from_sympy(x - i) + + return roots(R.to_sympy(s), x) + + def evalf(self, points, method='RK4', h=0.05, derivatives=False): + r""" + Finds numerical value of a holonomic function using numerical methods. + (RK4 by default). A set of points (real or complex) must be provided + which will be the path for the numerical integration. + + Explanation + =========== + + The path should be given as a list :math:`[x_1, x_2, \dots x_n]`. The numerical + values will be computed at each point in this order + :math:`x_1 \rightarrow x_2 \rightarrow x_3 \dots \rightarrow x_n`. + + Returns values of the function at :math:`x_1, x_2, \dots x_n` in a list. + + Examples + ======== + + >>> from sympy.holonomic.holonomic import HolonomicFunction, DifferentialOperators + >>> from sympy import QQ + >>> from sympy import symbols + >>> x = symbols('x') + >>> R, Dx = DifferentialOperators(QQ.old_poly_ring(x),'Dx') + + A straight line on the real axis from (0 to 1) + + >>> r = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] + + Runge-Kutta 4th order on e^x from 0.1 to 1. + Exact solution at 1 is 2.71828182845905 + + >>> HolonomicFunction(Dx - 1, x, 0, [1]).evalf(r) + [1.10517083333333, 1.22140257085069, 1.34985849706254, 1.49182424008069, + 1.64872063859684, 1.82211796209193, 2.01375162659678, 2.22553956329232, + 2.45960141378007, 2.71827974413517] + + Euler's method for the same + + >>> HolonomicFunction(Dx - 1, x, 0, [1]).evalf(r, method='Euler') + [1.1, 1.21, 1.331, 1.4641, 1.61051, 1.771561, 1.9487171, 2.14358881, + 2.357947691, 2.5937424601] + + One can also observe that the value obtained using Runge-Kutta 4th order + is much more accurate than Euler's method. + """ + + from sympy.holonomic.numerical import _evalf + lp = False + + # if a point `b` is given instead of a mesh + if not hasattr(points, "__iter__"): + lp = True + b = S(points) + if self.x0 == b: + return _evalf(self, [b], method=method, derivatives=derivatives)[-1] + + if not b.is_Number: + raise NotImplementedError + + a = self.x0 + if a > b: + h = -h + n = int((b - a) / h) + points = [a + h] + for i in range(n - 1): + points.append(points[-1] + h) + + for i in roots(self.annihilator.parent.base.to_sympy(self.annihilator.listofpoly[-1]), self.x): + if i == self.x0 or i in points: + raise SingularityError(self, i) + + if lp: + return _evalf(self, points, method=method, derivatives=derivatives)[-1] + return _evalf(self, points, method=method, derivatives=derivatives) + + def change_x(self, z): + """ + Changes only the variable of Holonomic Function, for internal + purposes. For composition use HolonomicFunction.composition() + """ + + dom = self.annihilator.parent.base.dom + R = dom.old_poly_ring(z) + parent, _ = DifferentialOperators(R, 'Dx') + sol = [R(j.to_list()) for j in self.annihilator.listofpoly] + sol = DifferentialOperator(sol, parent) + return HolonomicFunction(sol, z, self.x0, self.y0) + + def shift_x(self, a): + """ + Substitute `x + a` for `x`. + """ + + x = self.x + listaftershift = self.annihilator.listofpoly + base = self.annihilator.parent.base + + sol = [base.from_sympy(base.to_sympy(i).subs(x, x + a)) for i in listaftershift] + sol = DifferentialOperator(sol, self.annihilator.parent) + x0 = self.x0 - a + if not self._have_init_cond(): + return HolonomicFunction(sol, x) + return HolonomicFunction(sol, x, x0, self.y0) + + def to_hyper(self, as_list=False, _recur=None): + r""" + Returns a hypergeometric function (or linear combination of them) + representing the given holonomic function. + + Explanation + =========== + + Returns an answer of the form: + `a_1 \cdot x^{b_1} \cdot{hyper()} + a_2 \cdot x^{b_2} \cdot{hyper()} \dots` + + This is very useful as one can now use ``hyperexpand`` to find the + symbolic expressions/functions. + + Examples + ======== + + >>> from sympy.holonomic.holonomic import HolonomicFunction, DifferentialOperators + >>> from sympy import ZZ + >>> from sympy import symbols + >>> x = symbols('x') + >>> R, Dx = DifferentialOperators(ZZ.old_poly_ring(x),'Dx') + >>> # sin(x) + >>> HolonomicFunction(Dx**2 + 1, x, 0, [0, 1]).to_hyper() + x*hyper((), (3/2,), -x**2/4) + >>> # exp(x) + >>> HolonomicFunction(Dx - 1, x, 0, [1]).to_hyper() + hyper((), (), x) + + See Also + ======== + + from_hyper, from_meijerg + """ + + if _recur is None: + recurrence = self.to_sequence() + else: + recurrence = _recur + + if isinstance(recurrence, tuple) and len(recurrence) == 2: + smallest_n = recurrence[1] + recurrence = recurrence[0] + constantpower = 0 + elif isinstance(recurrence, tuple) and len(recurrence) == 3: + smallest_n = recurrence[2] + constantpower = recurrence[1] + recurrence = recurrence[0] + elif len(recurrence) == 1 and len(recurrence[0]) == 2: + smallest_n = recurrence[0][1] + recurrence = recurrence[0][0] + constantpower = 0 + elif len(recurrence) == 1 and len(recurrence[0]) == 3: + smallest_n = recurrence[0][2] + constantpower = recurrence[0][1] + recurrence = recurrence[0][0] + else: + sol = self.to_hyper(as_list=as_list, _recur=recurrence[0]) + for i in recurrence[1:]: + sol += self.to_hyper(as_list=as_list, _recur=i) + return sol + + u0 = recurrence.u0 + r = recurrence.recurrence + x = self.x + x0 = self.x0 + + # order of the recurrence relation + m = r.order + + # when no recurrence exists, and the power series have finite terms + if m == 0: + nonzeroterms = roots(r.parent.base.to_sympy(r.listofpoly[0]), recurrence.n, filter='R') + + sol = S.Zero + for j, i in enumerate(nonzeroterms): + + if i < 0 or not int_valued(i): + continue + + i = int(i) + if i < len(u0): + if isinstance(u0[i], (PolyElement, FracElement)): + u0[i] = u0[i].as_expr() + sol += u0[i] * x**i + + else: + sol += Symbol('C_%s' %j) * x**i + + if isinstance(sol, (PolyElement, FracElement)): + sol = sol.as_expr() * x**constantpower + else: + sol = sol * x**constantpower + if as_list: + if x0 != 0: + return [(sol.subs(x, x - x0), )] + return [(sol, )] + if x0 != 0: + return sol.subs(x, x - x0) + return sol + + if smallest_n + m > len(u0): + raise NotImplementedError("Can't compute sufficient Initial Conditions") + + # check if the recurrence represents a hypergeometric series + if any(i != r.parent.base.zero for i in r.listofpoly[1:-1]): + raise NotHyperSeriesError(self, self.x0) + + a = r.listofpoly[0] + b = r.listofpoly[-1] + + # the constant multiple of argument of hypergeometric function + if isinstance(a.LC(), (PolyElement, FracElement)): + c = - (S(a.LC().as_expr()) * m**(a.degree())) / (S(b.LC().as_expr()) * m**(b.degree())) + else: + c = - (S(a.LC()) * m**(a.degree())) / (S(b.LC()) * m**(b.degree())) + + sol = 0 + + arg1 = roots(r.parent.base.to_sympy(a), recurrence.n) + arg2 = roots(r.parent.base.to_sympy(b), recurrence.n) + + # iterate through the initial conditions to find + # the hypergeometric representation of the given + # function. + # The answer will be a linear combination + # of different hypergeometric series which satisfies + # the recurrence. + if as_list: + listofsol = [] + for i in range(smallest_n + m): + + # if the recurrence relation doesn't hold for `n = i`, + # then a Hypergeometric representation doesn't exist. + # add the algebraic term a * x**i to the solution, + # where a is u0[i] + if i < smallest_n: + if as_list: + listofsol.append(((S(u0[i]) * x**(i+constantpower)).subs(x, x-x0), )) + else: + sol += S(u0[i]) * x**i + continue + + # if the coefficient u0[i] is zero, then the + # independent hypergeomtric series starting with + # x**i is not a part of the answer. + if S(u0[i]) == 0: + continue + + ap = [] + bq = [] + + # substitute m * n + i for n + for k in ordered(arg1.keys()): + ap.extend([nsimplify((i - k) / m)] * arg1[k]) + + for k in ordered(arg2.keys()): + bq.extend([nsimplify((i - k) / m)] * arg2[k]) + + # convention of (k + 1) in the denominator + if 1 in bq: + bq.remove(1) + else: + ap.append(1) + if as_list: + listofsol.append(((S(u0[i])*x**(i+constantpower)).subs(x, x-x0), (hyper(ap, bq, c*x**m)).subs(x, x-x0))) + else: + sol += S(u0[i]) * hyper(ap, bq, c * x**m) * x**i + if as_list: + return listofsol + sol = sol * x**constantpower + if x0 != 0: + return sol.subs(x, x - x0) + + return sol + + def to_expr(self): + """ + Converts a Holonomic Function back to elementary functions. + + Examples + ======== + + >>> from sympy.holonomic.holonomic import HolonomicFunction, DifferentialOperators + >>> from sympy import ZZ + >>> from sympy import symbols, S + >>> x = symbols('x') + >>> R, Dx = DifferentialOperators(ZZ.old_poly_ring(x),'Dx') + >>> HolonomicFunction(x**2*Dx**2 + x*Dx + (x**2 - 1), x, 0, [0, S(1)/2]).to_expr() + besselj(1, x) + >>> HolonomicFunction((1 + x)*Dx**3 + Dx**2, x, 0, [1, 1, 1]).to_expr() + x*log(x + 1) + log(x + 1) + 1 + + """ + + return hyperexpand(self.to_hyper()).simplify() + + def change_ics(self, b, lenics=None): + """ + Changes the point `x0` to ``b`` for initial conditions. + + Examples + ======== + + >>> from sympy.holonomic import expr_to_holonomic + >>> from sympy import symbols, sin, exp + >>> x = symbols('x') + + >>> expr_to_holonomic(sin(x)).change_ics(1) + HolonomicFunction((1) + (1)*Dx**2, x, 1, [sin(1), cos(1)]) + + >>> expr_to_holonomic(exp(x)).change_ics(2) + HolonomicFunction((-1) + (1)*Dx, x, 2, [exp(2)]) + """ + + symbolic = True + + if lenics is None and len(self.y0) > self.annihilator.order: + lenics = len(self.y0) + dom = self.annihilator.parent.base.domain + + try: + sol = expr_to_holonomic(self.to_expr(), x=self.x, x0=b, lenics=lenics, domain=dom) + except (NotPowerSeriesError, NotHyperSeriesError): + symbolic = False + + if symbolic and sol.x0 == b: + return sol + + y0 = self.evalf(b, derivatives=True) + return HolonomicFunction(self.annihilator, self.x, b, y0) + + def to_meijerg(self): + """ + Returns a linear combination of Meijer G-functions. + + Examples + ======== + + >>> from sympy.holonomic import expr_to_holonomic + >>> from sympy import sin, cos, hyperexpand, log, symbols + >>> x = symbols('x') + >>> hyperexpand(expr_to_holonomic(cos(x) + sin(x)).to_meijerg()) + sin(x) + cos(x) + >>> hyperexpand(expr_to_holonomic(log(x)).to_meijerg()).simplify() + log(x) + + See Also + ======== + + to_hyper + """ + + # convert to hypergeometric first + rep = self.to_hyper(as_list=True) + sol = S.Zero + + for i in rep: + if len(i) == 1: + sol += i[0] + + elif len(i) == 2: + sol += i[0] * _hyper_to_meijerg(i[1]) + + return sol + + +def from_hyper(func, x0=0, evalf=False): + r""" + Converts a hypergeometric function to holonomic. + ``func`` is the Hypergeometric Function and ``x0`` is the point at + which initial conditions are required. + + Examples + ======== + + >>> from sympy.holonomic.holonomic import from_hyper + >>> from sympy import symbols, hyper, S + >>> x = symbols('x') + >>> from_hyper(hyper([], [S(3)/2], x**2/4)) + HolonomicFunction((-x) + (2)*Dx + (x)*Dx**2, x, 1, [sinh(1), -sinh(1) + cosh(1)]) + """ + + a = func.ap + b = func.bq + z = func.args[2] + x = z.atoms(Symbol).pop() + R, Dx = DifferentialOperators(QQ.old_poly_ring(x), 'Dx') + + # generalized hypergeometric differential equation + xDx = x*Dx + r1 = 1 + for ai in a: # XXX gives sympify error if Mul is used with list of all factors + r1 *= xDx + ai + xDx_1 = xDx - 1 + # r2 = Mul(*([Dx] + [xDx_1 + bi for bi in b])) # XXX gives sympify error + r2 = Dx + for bi in b: + r2 *= xDx_1 + bi + sol = r1 - r2 + + simp = hyperexpand(func) + + if simp in (Infinity, NegativeInfinity): + return HolonomicFunction(sol, x).composition(z) + + # if the function is known symbolically + if not isinstance(simp, hyper): + y0 = _find_conditions(simp, x, x0, sol.order, use_limit=False) + while not y0: + # if values don't exist at 0, then try to find initial + # conditions at 1. If it doesn't exist at 1 too then + # try 2 and so on. + x0 += 1 + y0 = _find_conditions(simp, x, x0, sol.order, use_limit=False) + + return HolonomicFunction(sol, x).composition(z, x0, y0) + + if isinstance(simp, hyper): + x0 = 1 + # use evalf if the function can't be simplified + y0 = _find_conditions(simp, x, x0, sol.order, evalf, use_limit=False) + while not y0: + x0 += 1 + y0 = _find_conditions(simp, x, x0, sol.order, evalf, use_limit=False) + return HolonomicFunction(sol, x).composition(z, x0, y0) + + return HolonomicFunction(sol, x).composition(z) + + +def from_meijerg(func, x0=0, evalf=False, initcond=True, domain=QQ): + """ + Converts a Meijer G-function to Holonomic. + ``func`` is the G-Function and ``x0`` is the point at + which initial conditions are required. + + Examples + ======== + + >>> from sympy.holonomic.holonomic import from_meijerg + >>> from sympy import symbols, meijerg, S + >>> x = symbols('x') + >>> from_meijerg(meijerg(([], []), ([S(1)/2], [0]), x**2/4)) + HolonomicFunction((1) + (1)*Dx**2, x, 0, [0, 1/sqrt(pi)]) + """ + + a = func.ap + b = func.bq + n = len(func.an) + m = len(func.bm) + p = len(a) + z = func.args[2] + x = z.atoms(Symbol).pop() + R, Dx = DifferentialOperators(domain.old_poly_ring(x), 'Dx') + + # compute the differential equation satisfied by the + # Meijer G-function. + xDx = x*Dx + xDx1 = xDx + 1 + r1 = x*(-1)**(m + n - p) + for ai in a: # XXX gives sympify error if args given in list + r1 *= xDx1 - ai + # r2 = Mul(*[xDx - bi for bi in b]) # gives sympify error + r2 = 1 + for bi in b: + r2 *= xDx - bi + sol = r1 - r2 + + if not initcond: + return HolonomicFunction(sol, x).composition(z) + + simp = hyperexpand(func) + + if simp in (Infinity, NegativeInfinity): + return HolonomicFunction(sol, x).composition(z) + + # computing initial conditions + if not isinstance(simp, meijerg): + y0 = _find_conditions(simp, x, x0, sol.order, use_limit=False) + while not y0: + x0 += 1 + y0 = _find_conditions(simp, x, x0, sol.order, use_limit=False) + + return HolonomicFunction(sol, x).composition(z, x0, y0) + + if isinstance(simp, meijerg): + x0 = 1 + y0 = _find_conditions(simp, x, x0, sol.order, evalf, use_limit=False) + while not y0: + x0 += 1 + y0 = _find_conditions(simp, x, x0, sol.order, evalf, use_limit=False) + + return HolonomicFunction(sol, x).composition(z, x0, y0) + + return HolonomicFunction(sol, x).composition(z) + + +x_1 = Dummy('x_1') +_lookup_table = None +domain_for_table = None +from sympy.integrals.meijerint import _mytype + + +def expr_to_holonomic(func, x=None, x0=0, y0=None, lenics=None, domain=None, initcond=True): + """ + Converts a function or an expression to a holonomic function. + + Parameters + ========== + + func: + The expression to be converted. + x: + variable for the function. + x0: + point at which initial condition must be computed. + y0: + One can optionally provide initial condition if the method + is not able to do it automatically. + lenics: + Number of terms in the initial condition. By default it is + equal to the order of the annihilator. + domain: + Ground domain for the polynomials in ``x`` appearing as coefficients + in the annihilator. + initcond: + Set it false if you do not want the initial conditions to be computed. + + Examples + ======== + + >>> from sympy.holonomic.holonomic import expr_to_holonomic + >>> from sympy import sin, exp, symbols + >>> x = symbols('x') + >>> expr_to_holonomic(sin(x)) + HolonomicFunction((1) + (1)*Dx**2, x, 0, [0, 1]) + >>> expr_to_holonomic(exp(x)) + HolonomicFunction((-1) + (1)*Dx, x, 0, [1]) + + See Also + ======== + + sympy.integrals.meijerint._rewrite1, _convert_poly_rat_alg, _create_table + """ + func = sympify(func) + syms = func.free_symbols + + if not x: + if len(syms) == 1: + x= syms.pop() + else: + raise ValueError("Specify the variable for the function") + elif x in syms: + syms.remove(x) + + extra_syms = list(syms) + + if domain is None: + if func.has(Float): + domain = RR + else: + domain = QQ + if len(extra_syms) != 0: + domain = domain[extra_syms].get_field() + + # try to convert if the function is polynomial or rational + solpoly = _convert_poly_rat_alg(func, x, x0=x0, y0=y0, lenics=lenics, domain=domain, initcond=initcond) + if solpoly: + return solpoly + + # create the lookup table + global _lookup_table, domain_for_table + if not _lookup_table: + domain_for_table = domain + _lookup_table = {} + _create_table(_lookup_table, domain=domain) + elif domain != domain_for_table: + domain_for_table = domain + _lookup_table = {} + _create_table(_lookup_table, domain=domain) + + # use the table directly to convert to Holonomic + if func.is_Function: + f = func.subs(x, x_1) + t = _mytype(f, x_1) + if t in _lookup_table: + l = _lookup_table[t] + sol = l[0][1].change_x(x) + else: + sol = _convert_meijerint(func, x, initcond=False, domain=domain) + if not sol: + raise NotImplementedError + if y0: + sol.y0 = y0 + if y0 or not initcond: + sol.x0 = x0 + return sol + if not lenics: + lenics = sol.annihilator.order + _y0 = _find_conditions(func, x, x0, lenics) + while not _y0: + x0 += 1 + _y0 = _find_conditions(func, x, x0, lenics) + return HolonomicFunction(sol.annihilator, x, x0, _y0) + + if y0 or not initcond: + sol = sol.composition(func.args[0]) + if y0: + sol.y0 = y0 + sol.x0 = x0 + return sol + if not lenics: + lenics = sol.annihilator.order + + _y0 = _find_conditions(func, x, x0, lenics) + while not _y0: + x0 += 1 + _y0 = _find_conditions(func, x, x0, lenics) + return sol.composition(func.args[0], x0, _y0) + + # iterate through the expression recursively + args = func.args + f = func.func + sol = expr_to_holonomic(args[0], x=x, initcond=False, domain=domain) + + if f is Add: + for i in range(1, len(args)): + sol += expr_to_holonomic(args[i], x=x, initcond=False, domain=domain) + + elif f is Mul: + for i in range(1, len(args)): + sol *= expr_to_holonomic(args[i], x=x, initcond=False, domain=domain) + + elif f is Pow: + sol = sol**args[1] + sol.x0 = x0 + if not sol: + raise NotImplementedError + if y0: + sol.y0 = y0 + if y0 or not initcond: + return sol + if sol.y0: + return sol + if not lenics: + lenics = sol.annihilator.order + if sol.annihilator.is_singular(x0): + r = sol._indicial() + l = list(r) + if len(r) == 1 and r[l[0]] == S.One: + r = l[0] + g = func / (x - x0)**r + singular_ics = _find_conditions(g, x, x0, lenics) + singular_ics = [j / factorial(i) for i, j in enumerate(singular_ics)] + y0 = {r:singular_ics} + return HolonomicFunction(sol.annihilator, x, x0, y0) + + _y0 = _find_conditions(func, x, x0, lenics) + while not _y0: + x0 += 1 + _y0 = _find_conditions(func, x, x0, lenics) + + return HolonomicFunction(sol.annihilator, x, x0, _y0) + + +## Some helper functions ## + +def _normalize(list_of, parent, negative=True): + """ + Normalize a given annihilator + """ + + num = [] + denom = [] + base = parent.base + K = base.get_field() + lcm_denom = base.from_sympy(S.One) + list_of_coeff = [] + + # convert polynomials to the elements of associated + # fraction field + for i, j in enumerate(list_of): + if isinstance(j, base.dtype): + list_of_coeff.append(K.new(j.to_list())) + elif not isinstance(j, K.dtype): + list_of_coeff.append(K.from_sympy(sympify(j))) + else: + list_of_coeff.append(j) + + # corresponding numerators of the sequence of polynomials + num.append(list_of_coeff[i].numer()) + + # corresponding denominators + denom.append(list_of_coeff[i].denom()) + + # lcm of denominators in the coefficients + for i in denom: + lcm_denom = i.lcm(lcm_denom) + + if negative: + lcm_denom = -lcm_denom + + lcm_denom = K.new(lcm_denom.to_list()) + + # multiply the coefficients with lcm + for i, j in enumerate(list_of_coeff): + list_of_coeff[i] = j * lcm_denom + + gcd_numer = base((list_of_coeff[-1].numer() / list_of_coeff[-1].denom()).to_list()) + + # gcd of numerators in the coefficients + for i in num: + gcd_numer = i.gcd(gcd_numer) + + gcd_numer = K.new(gcd_numer.to_list()) + + # divide all the coefficients by the gcd + for i, j in enumerate(list_of_coeff): + frac_ans = j / gcd_numer + list_of_coeff[i] = base((frac_ans.numer() / frac_ans.denom()).to_list()) + + return DifferentialOperator(list_of_coeff, parent) + + +def _derivate_diff_eq(listofpoly, K): + """ + Let a differential equation a0(x)y(x) + a1(x)y'(x) + ... = 0 + where a0, a1,... are polynomials or rational functions. The function + returns b0, b1, b2... such that the differential equation + b0(x)y(x) + b1(x)y'(x) +... = 0 is formed after differentiating the + former equation. + """ + + sol = [] + a = len(listofpoly) - 1 + sol.append(DMFdiff(listofpoly[0], K)) + + for i, j in enumerate(listofpoly[1:]): + sol.append(DMFdiff(j, K) + listofpoly[i]) + + sol.append(listofpoly[a]) + return sol + + +def _hyper_to_meijerg(func): + """ + Converts a `hyper` to meijerg. + """ + ap = func.ap + bq = func.bq + + if any(i <= 0 and int(i) == i for i in ap): + return hyperexpand(func) + + z = func.args[2] + + # parameters of the `meijerg` function. + an = (1 - i for i in ap) + anp = () + bm = (S.Zero, ) + bmq = (1 - i for i in bq) + + k = S.One + + for i in bq: + k = k * gamma(i) + + for i in ap: + k = k / gamma(i) + + return k * meijerg(an, anp, bm, bmq, -z) + + +def _add_lists(list1, list2): + """Takes polynomial sequences of two annihilators a and b and returns + the list of polynomials of sum of a and b. + """ + if len(list1) <= len(list2): + sol = [a + b for a, b in zip(list1, list2)] + list2[len(list1):] + else: + sol = [a + b for a, b in zip(list1, list2)] + list1[len(list2):] + return sol + + +def _extend_y0(Holonomic, n): + """ + Tries to find more initial conditions by substituting the initial + value point in the differential equation. + """ + + if Holonomic.annihilator.is_singular(Holonomic.x0) or Holonomic.is_singularics() == True: + return Holonomic.y0 + + annihilator = Holonomic.annihilator + a = annihilator.order + + listofpoly = [] + + y0 = Holonomic.y0 + R = annihilator.parent.base + K = R.get_field() + + for j in annihilator.listofpoly: + if isinstance(j, annihilator.parent.base.dtype): + listofpoly.append(K.new(j.to_list())) + + if len(y0) < a or n <= len(y0): + return y0 + list_red = [-listofpoly[i] / listofpoly[a] + for i in range(a)] + y1 = y0[:min(len(y0), a)] + for _ in range(n - a): + sol = 0 + for a, b in zip(y1, list_red): + r = DMFsubs(b, Holonomic.x0) + if not getattr(r, 'is_finite', True): + return y0 + if isinstance(r, (PolyElement, FracElement)): + r = r.as_expr() + sol += a * r + y1.append(sol) + list_red = _derivate_diff_eq(list_red, K) + return y0 + y1[len(y0):] + + +def DMFdiff(frac, K): + # differentiate a DMF object represented as p/q + if not isinstance(frac, DMF): + return frac.diff() + + p = K.numer(frac) + q = K.denom(frac) + sol_num = - p * q.diff() + q * p.diff() + sol_denom = q**2 + return K((sol_num.to_list(), sol_denom.to_list())) + + +def DMFsubs(frac, x0, mpm=False): + # substitute the point x0 in DMF object of the form p/q + if not isinstance(frac, DMF): + return frac + + p = frac.num + q = frac.den + sol_p = S.Zero + sol_q = S.Zero + + if mpm: + from mpmath import mp + + for i, j in enumerate(reversed(p)): + if mpm: + j = sympify(j)._to_mpmath(mp.prec) + sol_p += j * x0**i + + for i, j in enumerate(reversed(q)): + if mpm: + j = sympify(j)._to_mpmath(mp.prec) + sol_q += j * x0**i + + if isinstance(sol_p, (PolyElement, FracElement)): + sol_p = sol_p.as_expr() + if isinstance(sol_q, (PolyElement, FracElement)): + sol_q = sol_q.as_expr() + + return sol_p / sol_q + + +def _convert_poly_rat_alg(func, x, x0=0, y0=None, lenics=None, domain=QQ, initcond=True): + """ + Converts polynomials, rationals and algebraic functions to holonomic. + """ + + ispoly = func.is_polynomial() + if not ispoly: + israt = func.is_rational_function() + else: + israt = True + + if not (ispoly or israt): + basepoly, ratexp = func.as_base_exp() + if basepoly.is_polynomial() and ratexp.is_Number: + if isinstance(ratexp, Float): + ratexp = nsimplify(ratexp) + m, n = ratexp.p, ratexp.q + is_alg = True + else: + is_alg = False + else: + is_alg = True + + if not (ispoly or israt or is_alg): + return None + + R = domain.old_poly_ring(x) + _, Dx = DifferentialOperators(R, 'Dx') + + # if the function is constant + if not func.has(x): + return HolonomicFunction(Dx, x, 0, [func]) + + if ispoly: + # differential equation satisfied by polynomial + sol = func * Dx - func.diff(x) + sol = _normalize(sol.listofpoly, sol.parent, negative=False) + is_singular = sol.is_singular(x0) + + # try to compute the conditions for singular points + if y0 is None and x0 == 0 and is_singular: + rep = R.from_sympy(func).to_list() + for i, j in enumerate(reversed(rep)): + if j == 0: + continue + coeff = list(reversed(rep))[i:] + indicial = i + break + for i, j in enumerate(coeff): + if isinstance(j, (PolyElement, FracElement)): + coeff[i] = j.as_expr() + y0 = {indicial: S(coeff)} + + elif israt: + p, q = func.as_numer_denom() + # differential equation satisfied by rational + sol = p * q * Dx + p * q.diff(x) - q * p.diff(x) + sol = _normalize(sol.listofpoly, sol.parent, negative=False) + + elif is_alg: + sol = n * (x / m) * Dx - 1 + sol = HolonomicFunction(sol, x).composition(basepoly).annihilator + is_singular = sol.is_singular(x0) + + # try to compute the conditions for singular points + if y0 is None and x0 == 0 and is_singular and \ + (lenics is None or lenics <= 1): + rep = R.from_sympy(basepoly).to_list() + for i, j in enumerate(reversed(rep)): + if j == 0: + continue + if isinstance(j, (PolyElement, FracElement)): + j = j.as_expr() + + coeff = S(j)**ratexp + indicial = S(i) * ratexp + break + if isinstance(coeff, (PolyElement, FracElement)): + coeff = coeff.as_expr() + y0 = {indicial: S([coeff])} + + if y0 or not initcond: + return HolonomicFunction(sol, x, x0, y0) + + if not lenics: + lenics = sol.order + + if sol.is_singular(x0): + r = HolonomicFunction(sol, x, x0)._indicial() + l = list(r) + if len(r) == 1 and r[l[0]] == S.One: + r = l[0] + g = func / (x - x0)**r + singular_ics = _find_conditions(g, x, x0, lenics) + singular_ics = [j / factorial(i) for i, j in enumerate(singular_ics)] + y0 = {r:singular_ics} + return HolonomicFunction(sol, x, x0, y0) + + y0 = _find_conditions(func, x, x0, lenics) + while not y0: + x0 += 1 + y0 = _find_conditions(func, x, x0, lenics) + + return HolonomicFunction(sol, x, x0, y0) + + +def _convert_meijerint(func, x, initcond=True, domain=QQ): + args = meijerint._rewrite1(func, x) + + if args: + fac, po, g, _ = args + else: + return None + + # lists for sum of meijerg functions + fac_list = [fac * i[0] for i in g] + t = po.as_base_exp() + s = t[1] if t[0] == x else S.Zero + po_list = [s + i[1] for i in g] + G_list = [i[2] for i in g] + + # finds meijerg representation of x**s * meijerg(a1 ... ap, b1 ... bq, z) + def _shift(func, s): + z = func.args[-1] + if z.has(I): + z = z.subs(exp_polar, exp) + + d = z.collect(x, evaluate=False) + b = list(d)[0] + a = d[b] + + t = b.as_base_exp() + b = t[1] if t[0] == x else S.Zero + r = s / b + an = (i + r for i in func.args[0][0]) + ap = (i + r for i in func.args[0][1]) + bm = (i + r for i in func.args[1][0]) + bq = (i + r for i in func.args[1][1]) + + return a**-r, meijerg((an, ap), (bm, bq), z) + + coeff, m = _shift(G_list[0], po_list[0]) + sol = fac_list[0] * coeff * from_meijerg(m, initcond=initcond, domain=domain) + + # add all the meijerg functions after converting to holonomic + for i in range(1, len(G_list)): + coeff, m = _shift(G_list[i], po_list[i]) + sol += fac_list[i] * coeff * from_meijerg(m, initcond=initcond, domain=domain) + + return sol + + +def _create_table(table, domain=QQ): + """ + Creates the look-up table. For a similar implementation + see meijerint._create_lookup_table. + """ + + def add(formula, annihilator, arg, x0=0, y0=()): + """ + Adds a formula in the dictionary + """ + table.setdefault(_mytype(formula, x_1), []).append((formula, + HolonomicFunction(annihilator, arg, x0, y0))) + + R = domain.old_poly_ring(x_1) + _, Dx = DifferentialOperators(R, 'Dx') + + # add some basic functions + add(sin(x_1), Dx**2 + 1, x_1, 0, [0, 1]) + add(cos(x_1), Dx**2 + 1, x_1, 0, [1, 0]) + add(exp(x_1), Dx - 1, x_1, 0, 1) + add(log(x_1), Dx + x_1*Dx**2, x_1, 1, [0, 1]) + + add(erf(x_1), 2*x_1*Dx + Dx**2, x_1, 0, [0, 2/sqrt(pi)]) + add(erfc(x_1), 2*x_1*Dx + Dx**2, x_1, 0, [1, -2/sqrt(pi)]) + add(erfi(x_1), -2*x_1*Dx + Dx**2, x_1, 0, [0, 2/sqrt(pi)]) + + add(sinh(x_1), Dx**2 - 1, x_1, 0, [0, 1]) + add(cosh(x_1), Dx**2 - 1, x_1, 0, [1, 0]) + + add(sinc(x_1), x_1 + 2*Dx + x_1*Dx**2, x_1) + + add(Si(x_1), x_1*Dx + 2*Dx**2 + x_1*Dx**3, x_1) + add(Ci(x_1), x_1*Dx + 2*Dx**2 + x_1*Dx**3, x_1) + + add(Shi(x_1), -x_1*Dx + 2*Dx**2 + x_1*Dx**3, x_1) + + +def _find_conditions(func, x, x0, order, evalf=False, use_limit=True): + y0 = [] + for _ in range(order): + val = func.subs(x, x0) + if evalf: + val = val.evalf() + if use_limit and isinstance(val, NaN): + val = limit(func, x, x0) + if val.is_finite is False or isinstance(val, NaN): + return None + y0.append(val) + func = func.diff(x) + return y0 diff --git a/venv/lib/python3.11/site-packages/sympy/holonomic/holonomicerrors.py b/venv/lib/python3.11/site-packages/sympy/holonomic/holonomicerrors.py new file mode 100644 index 0000000000000000000000000000000000000000..459a94eb25b186e30b9577d972ebc62a36801f6f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/holonomic/holonomicerrors.py @@ -0,0 +1,49 @@ +""" Common Exceptions for `holonomic` module. """ + +class BaseHolonomicError(Exception): + + def new(self, *args): + raise NotImplementedError("abstract base class") + +class NotPowerSeriesError(BaseHolonomicError): + + def __init__(self, holonomic, x0): + self.holonomic = holonomic + self.x0 = x0 + + def __str__(self): + s = 'A Power Series does not exists for ' + s += str(self.holonomic) + s += ' about %s.' %self.x0 + return s + +class NotHolonomicError(BaseHolonomicError): + + def __init__(self, m): + self.m = m + + def __str__(self): + return self.m + +class SingularityError(BaseHolonomicError): + + def __init__(self, holonomic, x0): + self.holonomic = holonomic + self.x0 = x0 + + def __str__(self): + s = str(self.holonomic) + s += ' has a singularity at %s.' %self.x0 + return s + +class NotHyperSeriesError(BaseHolonomicError): + + def __init__(self, holonomic, x0): + self.holonomic = holonomic + self.x0 = x0 + + def __str__(self): + s = 'Power series expansion of ' + s += str(self.holonomic) + s += ' about %s is not hypergeometric' %self.x0 + return s diff --git a/venv/lib/python3.11/site-packages/sympy/holonomic/numerical.py b/venv/lib/python3.11/site-packages/sympy/holonomic/numerical.py new file mode 100644 index 0000000000000000000000000000000000000000..418b7c627e2e3d74dcded080a9b6d351cdaa9f3f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/holonomic/numerical.py @@ -0,0 +1,98 @@ +"""Numerical Methods for Holonomic Functions""" + +from sympy.core.sympify import sympify +from sympy.holonomic.holonomic import DMFsubs + +from mpmath import mp + + +def _evalf(func, points, derivatives=False, method='RK4'): + """ + Numerical methods for numerical integration along a given set of + points in the complex plane. + """ + + ann = func.annihilator + a = ann.order + R = ann.parent.base + K = R.get_field() + + if method == 'Euler': + meth = _euler + else: + meth = _rk4 + + dmf = [K.new(j.to_list()) for j in ann.listofpoly] + red = [-dmf[i] / dmf[a] for i in range(a)] + + y0 = func.y0 + if len(y0) < a: + raise TypeError("Not Enough Initial Conditions") + x0 = func.x0 + sol = [meth(red, x0, points[0], y0, a)] + + for i, j in enumerate(points[1:]): + sol.append(meth(red, points[i], j, sol[-1], a)) + + if not derivatives: + return [sympify(i[0]) for i in sol] + else: + return sympify(sol) + + +def _euler(red, x0, x1, y0, a): + """ + Euler's method for numerical integration. + From x0 to x1 with initial values given at x0 as vector y0. + """ + + A = sympify(x0)._to_mpmath(mp.prec) + B = sympify(x1)._to_mpmath(mp.prec) + y_0 = [sympify(i)._to_mpmath(mp.prec) for i in y0] + h = B - A + f_0 = y_0[1:] + f_0_n = 0 + + for i in range(a): + f_0_n += sympify(DMFsubs(red[i], A, mpm=True))._to_mpmath(mp.prec) * y_0[i] + f_0.append(f_0_n) + + return [y_0[i] + h * f_0[i] for i in range(a)] + + +def _rk4(red, x0, x1, y0, a): + """ + Runge-Kutta 4th order numerical method. + """ + + A = sympify(x0)._to_mpmath(mp.prec) + B = sympify(x1)._to_mpmath(mp.prec) + y_0 = [sympify(i)._to_mpmath(mp.prec) for i in y0] + h = B - A + + f_0_n = 0 + f_1_n = 0 + f_2_n = 0 + f_3_n = 0 + + f_0 = y_0[1:] + for i in range(a): + f_0_n += sympify(DMFsubs(red[i], A, mpm=True))._to_mpmath(mp.prec) * y_0[i] + f_0.append(f_0_n) + + f_1 = [y_0[i] + f_0[i]*h/2 for i in range(1, a)] + for i in range(a): + f_1_n += sympify(DMFsubs(red[i], A + h/2, mpm=True))._to_mpmath(mp.prec) * (y_0[i] + f_0[i]*h/2) + f_1.append(f_1_n) + + f_2 = [y_0[i] + f_1[i]*h/2 for i in range(1, a)] + for i in range(a): + f_2_n += sympify(DMFsubs(red[i], A + h/2, mpm=True))._to_mpmath(mp.prec) * (y_0[i] + f_1[i]*h/2) + f_2.append(f_2_n) + + f_3 = [y_0[i] + f_2[i]*h for i in range(1, a)] + for i in range(a): + f_3_n += sympify(DMFsubs(red[i], A + h, mpm=True))._to_mpmath(mp.prec) * (y_0[i] + f_2[i]*h) + f_3.append(f_3_n) + + return [y_0[i] + h*(f_0[i]+2*f_1[i]+2*f_2[i]+f_3[i])/6 for i in range(a)] diff --git a/venv/lib/python3.11/site-packages/sympy/holonomic/recurrence.py b/venv/lib/python3.11/site-packages/sympy/holonomic/recurrence.py new file mode 100644 index 0000000000000000000000000000000000000000..8c2c17ceda2d042c12778977cadd5ce9ee3b7479 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/holonomic/recurrence.py @@ -0,0 +1,342 @@ +"""Recurrence Operators""" + +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.printing import sstr +from sympy.core.sympify import sympify + + +def RecurrenceOperators(base, generator): + """ + Returns an Algebra of Recurrence Operators and the operator for + shifting i.e. the `Sn` operator. + The first argument needs to be the base polynomial ring for the algebra + and the second argument must be a generator which can be either a + noncommutative Symbol or a string. + + Examples + ======== + + >>> from sympy import ZZ + >>> from sympy import symbols + >>> from sympy.holonomic.recurrence import RecurrenceOperators + >>> n = symbols('n', integer=True) + >>> R, Sn = RecurrenceOperators(ZZ.old_poly_ring(n), 'Sn') + """ + + ring = RecurrenceOperatorAlgebra(base, generator) + return (ring, ring.shift_operator) + + +class RecurrenceOperatorAlgebra: + """ + A Recurrence Operator Algebra is a set of noncommutative polynomials + in intermediate `Sn` and coefficients in a base ring A. It follows the + commutation rule: + Sn * a(n) = a(n + 1) * Sn + + This class represents a Recurrence Operator Algebra and serves as the parent ring + for Recurrence Operators. + + Examples + ======== + + >>> from sympy import ZZ + >>> from sympy import symbols + >>> from sympy.holonomic.recurrence import RecurrenceOperators + >>> n = symbols('n', integer=True) + >>> R, Sn = RecurrenceOperators(ZZ.old_poly_ring(n), 'Sn') + >>> R + Univariate Recurrence Operator Algebra in intermediate Sn over the base ring + ZZ[n] + + See Also + ======== + + RecurrenceOperator + """ + + def __init__(self, base, generator): + # the base ring for the algebra + self.base = base + # the operator representing shift i.e. `Sn` + self.shift_operator = RecurrenceOperator( + [base.zero, base.one], self) + + if generator is None: + self.gen_symbol = symbols('Sn', commutative=False) + else: + if isinstance(generator, str): + self.gen_symbol = symbols(generator, commutative=False) + elif isinstance(generator, Symbol): + self.gen_symbol = generator + + def __str__(self): + string = 'Univariate Recurrence Operator Algebra in intermediate '\ + + sstr(self.gen_symbol) + ' over the base ring ' + \ + (self.base).__str__() + + return string + + __repr__ = __str__ + + def __eq__(self, other): + if self.base == other.base and self.gen_symbol == other.gen_symbol: + return True + else: + return False + + +def _add_lists(list1, list2): + if len(list1) <= len(list2): + sol = [a + b for a, b in zip(list1, list2)] + list2[len(list1):] + else: + sol = [a + b for a, b in zip(list1, list2)] + list1[len(list2):] + return sol + + +class RecurrenceOperator: + """ + The Recurrence Operators are defined by a list of polynomials + in the base ring and the parent ring of the Operator. + + Explanation + =========== + + Takes a list of polynomials for each power of Sn and the + parent ring which must be an instance of RecurrenceOperatorAlgebra. + + A Recurrence Operator can be created easily using + the operator `Sn`. See examples below. + + Examples + ======== + + >>> from sympy.holonomic.recurrence import RecurrenceOperator, RecurrenceOperators + >>> from sympy import ZZ + >>> from sympy import symbols + >>> n = symbols('n', integer=True) + >>> R, Sn = RecurrenceOperators(ZZ.old_poly_ring(n),'Sn') + + >>> RecurrenceOperator([0, 1, n**2], R) + (1)Sn + (n**2)Sn**2 + + >>> Sn*n + (n + 1)Sn + + >>> n*Sn*n + 1 - Sn**2*n + (1) + (n**2 + n)Sn + (-n - 2)Sn**2 + + See Also + ======== + + DifferentialOperatorAlgebra + """ + + _op_priority = 20 + + def __init__(self, list_of_poly, parent): + # the parent ring for this operator + # must be an RecurrenceOperatorAlgebra object + self.parent = parent + # sequence of polynomials in n for each power of Sn + # represents the operator + # convert the expressions into ring elements using from_sympy + if isinstance(list_of_poly, list): + for i, j in enumerate(list_of_poly): + if isinstance(j, int): + list_of_poly[i] = self.parent.base.from_sympy(S(j)) + elif not isinstance(j, self.parent.base.dtype): + list_of_poly[i] = self.parent.base.from_sympy(j) + + self.listofpoly = list_of_poly + self.order = len(self.listofpoly) - 1 + + def __mul__(self, other): + """ + Multiplies two Operators and returns another + RecurrenceOperator instance using the commutation rule + Sn * a(n) = a(n + 1) * Sn + """ + + listofself = self.listofpoly + base = self.parent.base + + if not isinstance(other, RecurrenceOperator): + if not isinstance(other, self.parent.base.dtype): + listofother = [self.parent.base.from_sympy(sympify(other))] + + else: + listofother = [other] + else: + listofother = other.listofpoly + # multiply a polynomial `b` with a list of polynomials + + def _mul_dmp_diffop(b, listofother): + if isinstance(listofother, list): + return [i * b for i in listofother] + return [b * listofother] + + sol = _mul_dmp_diffop(listofself[0], listofother) + + # compute Sn^i * b + def _mul_Sni_b(b): + sol = [base.zero] + + if isinstance(b, list): + for i in b: + j = base.to_sympy(i).subs(base.gens[0], base.gens[0] + S.One) + sol.append(base.from_sympy(j)) + + else: + j = b.subs(base.gens[0], base.gens[0] + S.One) + sol.append(base.from_sympy(j)) + + return sol + + for i in range(1, len(listofself)): + # find Sn^i * b in ith iteration + listofother = _mul_Sni_b(listofother) + # solution = solution + listofself[i] * (Sn^i * b) + sol = _add_lists(sol, _mul_dmp_diffop(listofself[i], listofother)) + + return RecurrenceOperator(sol, self.parent) + + def __rmul__(self, other): + if not isinstance(other, RecurrenceOperator): + + if isinstance(other, int): + other = S(other) + + if not isinstance(other, self.parent.base.dtype): + other = (self.parent.base).from_sympy(other) + + sol = [other * j for j in self.listofpoly] + return RecurrenceOperator(sol, self.parent) + + def __add__(self, other): + if isinstance(other, RecurrenceOperator): + + sol = _add_lists(self.listofpoly, other.listofpoly) + return RecurrenceOperator(sol, self.parent) + + else: + + if isinstance(other, int): + other = S(other) + list_self = self.listofpoly + if not isinstance(other, self.parent.base.dtype): + list_other = [((self.parent).base).from_sympy(other)] + else: + list_other = [other] + sol = [list_self[0] + list_other[0]] + list_self[1:] + + return RecurrenceOperator(sol, self.parent) + + __radd__ = __add__ + + def __sub__(self, other): + return self + (-1) * other + + def __rsub__(self, other): + return (-1) * self + other + + def __pow__(self, n): + if n == 1: + return self + result = RecurrenceOperator([self.parent.base.one], self.parent) + if n == 0: + return result + # if self is `Sn` + if self.listofpoly == self.parent.shift_operator.listofpoly: + sol = [self.parent.base.zero] * n + [self.parent.base.one] + return RecurrenceOperator(sol, self.parent) + x = self + while True: + if n % 2: + result *= x + n >>= 1 + if not n: + break + x *= x + return result + + def __str__(self): + listofpoly = self.listofpoly + print_str = '' + + for i, j in enumerate(listofpoly): + if j == self.parent.base.zero: + continue + + j = self.parent.base.to_sympy(j) + + if i == 0: + print_str += '(' + sstr(j) + ')' + continue + + if print_str: + print_str += ' + ' + + if i == 1: + print_str += '(' + sstr(j) + ')Sn' + continue + + print_str += '(' + sstr(j) + ')' + 'Sn**' + sstr(i) + + return print_str + + __repr__ = __str__ + + def __eq__(self, other): + if isinstance(other, RecurrenceOperator): + if self.listofpoly == other.listofpoly and self.parent == other.parent: + return True + else: + return False + return self.listofpoly[0] == other and \ + all(i is self.parent.base.zero for i in self.listofpoly[1:]) + + +class HolonomicSequence: + """ + A Holonomic Sequence is a type of sequence satisfying a linear homogeneous + recurrence relation with Polynomial coefficients. Alternatively, A sequence + is Holonomic if and only if its generating function is a Holonomic Function. + """ + + def __init__(self, recurrence, u0=[]): + self.recurrence = recurrence + if not isinstance(u0, list): + self.u0 = [u0] + else: + self.u0 = u0 + + if len(self.u0) == 0: + self._have_init_cond = False + else: + self._have_init_cond = True + self.n = recurrence.parent.base.gens[0] + + def __repr__(self): + str_sol = 'HolonomicSequence(%s, %s)' % ((self.recurrence).__repr__(), sstr(self.n)) + if not self._have_init_cond: + return str_sol + else: + cond_str = '' + seq_str = 0 + for i in self.u0: + cond_str += ', u(%s) = %s' % (sstr(seq_str), sstr(i)) + seq_str += 1 + + sol = str_sol + cond_str + return sol + + __str__ = __repr__ + + def __eq__(self, other): + if self.recurrence != other.recurrence or self.n != other.n: + return False + if self._have_init_cond and other._have_init_cond: + return self.u0 == other.u0 + return True diff --git a/venv/lib/python3.11/site-packages/sympy/holonomic/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/holonomic/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/holonomic/tests/test_holonomic.py b/venv/lib/python3.11/site-packages/sympy/holonomic/tests/test_holonomic.py new file mode 100644 index 0000000000000000000000000000000000000000..49956419e917b3bc81a163d29862c539f33f6284 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/holonomic/tests/test_holonomic.py @@ -0,0 +1,851 @@ +from sympy.holonomic import (DifferentialOperator, HolonomicFunction, + DifferentialOperators, from_hyper, + from_meijerg, expr_to_holonomic) +from sympy.holonomic.recurrence import RecurrenceOperators, HolonomicSequence +from sympy.core import EulerGamma +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import (asinh, cosh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.functions.special.bessel import besselj +from sympy.functions.special.beta_functions import beta +from sympy.functions.special.error_functions import (Ci, Si, erf, erfc) +from sympy.functions.special.gamma_functions import gamma +from sympy.functions.special.hyper import (hyper, meijerg) +from sympy.printing.str import sstr +from sympy.series.order import O +from sympy.simplify.hyperexpand import hyperexpand +from sympy.polys.domains.integerring import ZZ +from sympy.polys.domains.rationalfield import QQ +from sympy.polys.domains.realfield import RR + + +def test_DifferentialOperator(): + x = symbols('x') + R, Dx = DifferentialOperators(QQ.old_poly_ring(x), 'Dx') + assert Dx == R.derivative_operator + assert Dx == DifferentialOperator([R.base.zero, R.base.one], R) + assert x * Dx + x**2 * Dx**2 == DifferentialOperator([0, x, x**2], R) + assert (x**2 + 1) + Dx + x * \ + Dx**5 == DifferentialOperator([x**2 + 1, 1, 0, 0, 0, x], R) + assert (x * Dx + x**2 + 1 - Dx * (x**3 + x))**3 == (-48 * x**6) + \ + (-57 * x**7) * Dx + (-15 * x**8) * Dx**2 + (-x**9) * Dx**3 + p = (x * Dx**2 + (x**2 + 3) * Dx**5) * (Dx + x**2) + q = (2 * x) + (4 * x**2) * Dx + (x**3) * Dx**2 + \ + (20 * x**2 + x + 60) * Dx**3 + (10 * x**3 + 30 * x) * Dx**4 + \ + (x**4 + 3 * x**2) * Dx**5 + (x**2 + 3) * Dx**6 + assert p == q + + +def test_HolonomicFunction_addition(): + x = symbols('x') + R, Dx = DifferentialOperators(ZZ.old_poly_ring(x), 'Dx') + p = HolonomicFunction(Dx**2 * x, x) + q = HolonomicFunction((2) * Dx + (x) * Dx**2, x) + assert p == q + p = HolonomicFunction(x * Dx + 1, x) + q = HolonomicFunction(Dx + 1, x) + r = HolonomicFunction((x - 2) + (x**2 - 2) * Dx + (x**2 - x) * Dx**2, x) + assert p + q == r + p = HolonomicFunction(x * Dx + Dx**2 * (x**2 + 2), x) + q = HolonomicFunction(Dx - 3, x) + r = HolonomicFunction((-54 * x**2 - 126 * x - 150) + (-135 * x**3 - 252 * x**2 - 270 * x + 140) * Dx +\ + (-27 * x**4 - 24 * x**2 + 14 * x - 150) * Dx**2 + \ + (9 * x**4 + 15 * x**3 + 38 * x**2 + 30 * x +40) * Dx**3, x) + assert p + q == r + p = HolonomicFunction(Dx**5 - 1, x) + q = HolonomicFunction(x**3 + Dx, x) + r = HolonomicFunction((-x**18 + 45*x**14 - 525*x**10 + 1575*x**6 - x**3 - 630*x**2) + \ + (-x**15 + 30*x**11 - 195*x**7 + 210*x**3 - 1)*Dx + (x**18 - 45*x**14 + 525*x**10 - \ + 1575*x**6 + x**3 + 630*x**2)*Dx**5 + (x**15 - 30*x**11 + 195*x**7 - 210*x**3 + \ + 1)*Dx**6, x) + assert p+q == r + + p = x**2 + 3*x + 8 + q = x**3 - 7*x + 5 + p = p*Dx - p.diff() + q = q*Dx - q.diff() + r = HolonomicFunction(p, x) + HolonomicFunction(q, x) + s = HolonomicFunction((6*x**2 + 18*x + 14) + (-4*x**3 - 18*x**2 - 62*x + 10)*Dx +\ + (x**4 + 6*x**3 + 31*x**2 - 10*x - 71)*Dx**2, x) + assert r == s + + +def test_HolonomicFunction_multiplication(): + x = symbols('x') + R, Dx = DifferentialOperators(ZZ.old_poly_ring(x), 'Dx') + p = HolonomicFunction(Dx+x+x*Dx**2, x) + q = HolonomicFunction(x*Dx+Dx*x+Dx**2, x) + r = HolonomicFunction((8*x**6 + 4*x**4 + 6*x**2 + 3) + (24*x**5 - 4*x**3 + 24*x)*Dx + \ + (8*x**6 + 20*x**4 + 12*x**2 + 2)*Dx**2 + (8*x**5 + 4*x**3 + 4*x)*Dx**3 + \ + (2*x**4 + x**2)*Dx**4, x) + assert p*q == r + p = HolonomicFunction(Dx**2+1, x) + q = HolonomicFunction(Dx-1, x) + r = HolonomicFunction((2) + (-2)*Dx + (1)*Dx**2, x) + assert p*q == r + p = HolonomicFunction(Dx**2+1+x+Dx, x) + q = HolonomicFunction((Dx*x-1)**2, x) + r = HolonomicFunction((4*x**7 + 11*x**6 + 16*x**5 + 4*x**4 - 6*x**3 - 7*x**2 - 8*x - 2) + \ + (8*x**6 + 26*x**5 + 24*x**4 - 3*x**3 - 11*x**2 - 6*x - 2)*Dx + \ + (8*x**6 + 18*x**5 + 15*x**4 - 3*x**3 - 6*x**2 - 6*x - 2)*Dx**2 + (8*x**5 + \ + 10*x**4 + 6*x**3 - 2*x**2 - 4*x)*Dx**3 + (4*x**5 + 3*x**4 - x**2)*Dx**4, x) + assert p*q == r + p = HolonomicFunction(x*Dx**2-1, x) + q = HolonomicFunction(Dx*x-x, x) + r = HolonomicFunction((x - 3) + (-2*x + 2)*Dx + (x)*Dx**2, x) + assert p*q == r + + +def test_HolonomicFunction_power(): + x = symbols('x') + R, Dx = DifferentialOperators(ZZ.old_poly_ring(x), 'Dx') + p = HolonomicFunction(Dx+x+x*Dx**2, x) + a = HolonomicFunction(Dx, x) + for n in range(10): + assert a == p**n + a *= p + + +def test_addition_initial_condition(): + x = symbols('x') + R, Dx = DifferentialOperators(QQ.old_poly_ring(x), 'Dx') + p = HolonomicFunction(Dx-1, x, 0, [3]) + q = HolonomicFunction(Dx**2+1, x, 0, [1, 0]) + r = HolonomicFunction(-1 + Dx - Dx**2 + Dx**3, x, 0, [4, 3, 2]) + assert p + q == r + p = HolonomicFunction(Dx - x + Dx**2, x, 0, [1, 2]) + q = HolonomicFunction(Dx**2 + x, x, 0, [1, 0]) + r = HolonomicFunction((-x**4 - x**3/4 - x**2 + Rational(1, 4)) + (x**3 + x**2/4 + x*Rational(3, 4) + 1)*Dx + \ + (x*Rational(-3, 2) + Rational(7, 4))*Dx**2 + (x**2 - x*Rational(7, 4) + Rational(1, 4))*Dx**3 + (x**2 + x/4 + S.Half)*Dx**4, x, 0, [2, 2, -2, 2]) + assert p + q == r + p = HolonomicFunction(Dx**2 + 4*x*Dx + x**2, x, 0, [3, 4]) + q = HolonomicFunction(Dx**2 + 1, x, 0, [1, 1]) + r = HolonomicFunction((x**6 + 2*x**4 - 5*x**2 - 6) + (4*x**5 + 36*x**3 - 32*x)*Dx + \ + (x**6 + 3*x**4 + 5*x**2 - 9)*Dx**2 + (4*x**5 + 36*x**3 - 32*x)*Dx**3 + (x**4 + \ + 10*x**2 - 3)*Dx**4, x, 0, [4, 5, -1, -17]) + assert p + q == r + q = HolonomicFunction(Dx**3 + x, x, 2, [3, 0, 1]) + p = HolonomicFunction(Dx - 1, x, 2, [1]) + r = HolonomicFunction((-x**2 - x + 1) + (x**2 + x)*Dx + (-x - 2)*Dx**3 + \ + (x + 1)*Dx**4, x, 2, [4, 1, 2, -5 ]) + assert p + q == r + p = expr_to_holonomic(sin(x)) + q = expr_to_holonomic(1/x, x0=1) + r = HolonomicFunction((x**2 + 6) + (x**3 + 2*x)*Dx + (x**2 + 6)*Dx**2 + (x**3 + 2*x)*Dx**3, \ + x, 1, [sin(1) + 1, -1 + cos(1), -sin(1) + 2]) + assert p + q == r + C_1 = symbols('C_1') + p = expr_to_holonomic(sqrt(x)) + q = expr_to_holonomic(sqrt(x**2-x)) + r = (p + q).to_expr().subs(C_1, -I/2).expand() + assert r == I*sqrt(x)*sqrt(-x + 1) + sqrt(x) + + +def test_multiplication_initial_condition(): + x = symbols('x') + R, Dx = DifferentialOperators(QQ.old_poly_ring(x), 'Dx') + p = HolonomicFunction(Dx**2 + x*Dx - 1, x, 0, [3, 1]) + q = HolonomicFunction(Dx**2 + 1, x, 0, [1, 1]) + r = HolonomicFunction((x**4 + 14*x**2 + 60) + 4*x*Dx + (x**4 + 9*x**2 + 20)*Dx**2 + \ + (2*x**3 + 18*x)*Dx**3 + (x**2 + 10)*Dx**4, x, 0, [3, 4, 2, 3]) + assert p * q == r + p = HolonomicFunction(Dx**2 + x, x, 0, [1, 0]) + q = HolonomicFunction(Dx**3 - x**2, x, 0, [3, 3, 3]) + r = HolonomicFunction((x**8 - 37*x**7/27 - 10*x**6/27 - 164*x**5/9 - 184*x**4/9 + \ + 160*x**3/27 + 404*x**2/9 + 8*x + Rational(40, 3)) + (6*x**7 - 128*x**6/9 - 98*x**5/9 - 28*x**4/9 + \ + 8*x**3/9 + 28*x**2 + x*Rational(40, 9) - 40)*Dx + (3*x**6 - 82*x**5/9 + 76*x**4/9 + 4*x**3/3 + \ + 220*x**2/9 - x*Rational(80, 3))*Dx**2 + (-2*x**6 + 128*x**5/27 - 2*x**4/3 -80*x**2/9 + Rational(200, 9))*Dx**3 + \ + (3*x**5 - 64*x**4/9 - 28*x**3/9 + 6*x**2 - x*Rational(20, 9) - Rational(20, 3))*Dx**4 + (-4*x**3 + 64*x**2/9 + \ + x*Rational(8, 3))*Dx**5 + (x**4 - 64*x**3/27 - 4*x**2/3 + Rational(20, 9))*Dx**6, x, 0, [3, 3, 3, -3, -12, -24]) + assert p * q == r + p = HolonomicFunction(Dx - 1, x, 0, [2]) + q = HolonomicFunction(Dx**2 + 1, x, 0, [0, 1]) + r = HolonomicFunction(2 -2*Dx + Dx**2, x, 0, [0, 2]) + assert p * q == r + q = HolonomicFunction(x*Dx**2 + 1 + 2*Dx, x, 0,[0, 1]) + r = HolonomicFunction((x - 1) + (-2*x + 2)*Dx + x*Dx**2, x, 0, [0, 2]) + assert p * q == r + p = HolonomicFunction(Dx**2 - 1, x, 0, [1, 3]) + q = HolonomicFunction(Dx**3 + 1, x, 0, [1, 2, 1]) + r = HolonomicFunction(6*Dx + 3*Dx**2 + 2*Dx**3 - 3*Dx**4 + Dx**6, x, 0, [1, 5, 14, 17, 17, 2]) + assert p * q == r + p = expr_to_holonomic(sin(x)) + q = expr_to_holonomic(1/x, x0=1) + r = HolonomicFunction(x + 2*Dx + x*Dx**2, x, 1, [sin(1), -sin(1) + cos(1)]) + assert p * q == r + p = expr_to_holonomic(sqrt(x)) + q = expr_to_holonomic(sqrt(x**2-x)) + r = (p * q).to_expr() + assert r == I*x*sqrt(-x + 1) + + +def test_HolonomicFunction_composition(): + x = symbols('x') + R, Dx = DifferentialOperators(ZZ.old_poly_ring(x), 'Dx') + p = HolonomicFunction(Dx-1, x).composition(x**2+x) + r = HolonomicFunction((-2*x - 1) + Dx, x) + assert p == r + p = HolonomicFunction(Dx**2+1, x).composition(x**5+x**2+1) + r = HolonomicFunction((125*x**12 + 150*x**9 + 60*x**6 + 8*x**3) + (-20*x**3 - 2)*Dx + \ + (5*x**4 + 2*x)*Dx**2, x) + assert p == r + p = HolonomicFunction(Dx**2*x+x, x).composition(2*x**3+x**2+1) + r = HolonomicFunction((216*x**9 + 324*x**8 + 180*x**7 + 152*x**6 + 112*x**5 + \ + 36*x**4 + 4*x**3) + (24*x**4 + 16*x**3 + 3*x**2 - 6*x - 1)*Dx + (6*x**5 + 5*x**4 + \ + x**3 + 3*x**2 + x)*Dx**2, x) + assert p == r + p = HolonomicFunction(Dx**2+1, x).composition(1-x**2) + r = HolonomicFunction((4*x**3) - Dx + x*Dx**2, x) + assert p == r + p = HolonomicFunction(Dx**2+1, x).composition(x - 2/(x**2 + 1)) + r = HolonomicFunction((x**12 + 6*x**10 + 12*x**9 + 15*x**8 + 48*x**7 + 68*x**6 + \ + 72*x**5 + 111*x**4 + 112*x**3 + 54*x**2 + 12*x + 1) + (12*x**8 + 32*x**6 + \ + 24*x**4 - 4)*Dx + (x**12 + 6*x**10 + 4*x**9 + 15*x**8 + 16*x**7 + 20*x**6 + 24*x**5+ \ + 15*x**4 + 16*x**3 + 6*x**2 + 4*x + 1)*Dx**2, x) + assert p == r + + +def test_from_hyper(): + x = symbols('x') + R, Dx = DifferentialOperators(QQ.old_poly_ring(x), 'Dx') + p = hyper([1, 1], [Rational(3, 2)], x**2/4) + q = HolonomicFunction((4*x) + (5*x**2 - 8)*Dx + (x**3 - 4*x)*Dx**2, x, 1, [2*sqrt(3)*pi/9, -4*sqrt(3)*pi/27 + Rational(4, 3)]) + r = from_hyper(p) + assert r == q + p = from_hyper(hyper([1], [Rational(3, 2)], x**2/4)) + q = HolonomicFunction(-x + (-x**2/2 + 2)*Dx + x*Dx**2, x) + # x0 = 1 + y0 = '[sqrt(pi)*exp(1/4)*erf(1/2), -sqrt(pi)*exp(1/4)*erf(1/2)/2 + 1]' + assert sstr(p.y0) == y0 + assert q.annihilator == p.annihilator + + +def test_from_meijerg(): + x = symbols('x') + R, Dx = DifferentialOperators(QQ.old_poly_ring(x), 'Dx') + p = from_meijerg(meijerg(([], [Rational(3, 2)]), ([S.Half], [S.Half, 1]), x)) + q = HolonomicFunction(x/2 - Rational(1, 4) + (-x**2 + x/4)*Dx + x**2*Dx**2 + x**3*Dx**3, x, 1, \ + [1/sqrt(pi), 1/(2*sqrt(pi)), -1/(4*sqrt(pi))]) + assert p == q + p = from_meijerg(meijerg(([], []), ([0], []), x)) + q = HolonomicFunction(1 + Dx, x, 0, [1]) + assert p == q + p = from_meijerg(meijerg(([1], []), ([S.Half], [0]), x)) + q = HolonomicFunction((x + S.Half)*Dx + x*Dx**2, x, 1, [sqrt(pi)*erf(1), exp(-1)]) + assert p == q + p = from_meijerg(meijerg(([0], [1]), ([0], []), 2*x**2)) + q = HolonomicFunction((3*x**2 - 1)*Dx + x**3*Dx**2, x, 1, [-exp(Rational(-1, 2)) + 1, -exp(Rational(-1, 2))]) + assert p == q + + +def test_to_Sequence(): + x = symbols('x') + R, Dx = DifferentialOperators(ZZ.old_poly_ring(x), 'Dx') + n = symbols('n', integer=True) + _, Sn = RecurrenceOperators(ZZ.old_poly_ring(n), 'Sn') + p = HolonomicFunction(x**2*Dx**4 + x + Dx, x).to_sequence() + q = [(HolonomicSequence(1 + (n + 2)*Sn**2 + (n**4 + 6*n**3 + 11*n**2 + 6*n)*Sn**3), 0, 1)] + assert p == q + p = HolonomicFunction(x**2*Dx**4 + x**3 + Dx**2, x).to_sequence() + q = [(HolonomicSequence(1 + (n**4 + 14*n**3 + 72*n**2 + 163*n + 140)*Sn**5), 0, 0)] + assert p == q + p = HolonomicFunction(x**3*Dx**4 + 1 + Dx**2, x).to_sequence() + q = [(HolonomicSequence(1 + (n**4 - 2*n**3 - n**2 + 2*n)*Sn + (n**2 + 3*n + 2)*Sn**2), 0, 0)] + assert p == q + p = HolonomicFunction(3*x**3*Dx**4 + 2*x*Dx + x*Dx**3, x).to_sequence() + q = [(HolonomicSequence(2*n + (3*n**4 - 6*n**3 - 3*n**2 + 6*n)*Sn + (n**3 + 3*n**2 + 2*n)*Sn**2), 0, 1)] + assert p == q + + +def test_to_Sequence_Initial_Coniditons(): + x = symbols('x') + R, Dx = DifferentialOperators(QQ.old_poly_ring(x), 'Dx') + n = symbols('n', integer=True) + _, Sn = RecurrenceOperators(QQ.old_poly_ring(n), 'Sn') + p = HolonomicFunction(Dx - 1, x, 0, [1]).to_sequence() + q = [(HolonomicSequence(-1 + (n + 1)*Sn, 1), 0)] + assert p == q + p = HolonomicFunction(Dx**2 + 1, x, 0, [0, 1]).to_sequence() + q = [(HolonomicSequence(1 + (n**2 + 3*n + 2)*Sn**2, [0, 1]), 0)] + assert p == q + p = HolonomicFunction(Dx**2 + 1 + x**3*Dx, x, 0, [2, 3]).to_sequence() + q = [(HolonomicSequence(n + Sn**2 + (n**2 + 7*n + 12)*Sn**4, [2, 3, -1, Rational(-1, 2), Rational(1, 12)]), 1)] + assert p == q + p = HolonomicFunction(x**3*Dx**5 + 1 + Dx, x).to_sequence() + q = [(HolonomicSequence(1 + (n + 1)*Sn + (n**5 - 5*n**3 + 4*n)*Sn**2), 0, 3)] + assert p == q + C_0, C_1, C_2, C_3 = symbols('C_0, C_1, C_2, C_3') + p = expr_to_holonomic(log(1+x**2)) + q = [(HolonomicSequence(n**2 + (n**2 + 2*n)*Sn**2, [0, 0, C_2]), 0, 1)] + assert p.to_sequence() == q + p = p.diff() + q = [(HolonomicSequence((n + 2) + (n + 2)*Sn**2, [C_0, 0]), 1, 0)] + assert p.to_sequence() == q + p = expr_to_holonomic(erf(x) + x).to_sequence() + q = [(HolonomicSequence((2*n**2 - 2*n) + (n**3 + 2*n**2 - n - 2)*Sn**2, [0, 1 + 2/sqrt(pi), 0, C_3]), 0, 2)] + assert p == q + +def test_series(): + x = symbols('x') + R, Dx = DifferentialOperators(ZZ.old_poly_ring(x), 'Dx') + p = HolonomicFunction(Dx**2 + 2*x*Dx, x, 0, [0, 1]).series(n=10) + q = x - x**3/3 + x**5/10 - x**7/42 + x**9/216 + O(x**10) + assert p == q + p = HolonomicFunction(Dx - 1, x).composition(x**2, 0, [1]) # e^(x**2) + q = HolonomicFunction(Dx**2 + 1, x, 0, [1, 0]) # cos(x) + r = (p * q).series(n=10) # expansion of cos(x) * exp(x**2) + s = 1 + x**2/2 + x**4/24 - 31*x**6/720 - 179*x**8/8064 + O(x**10) + assert r == s + t = HolonomicFunction((1 + x)*Dx**2 + Dx, x, 0, [0, 1]) # log(1 + x) + r = (p * t + q).series(n=10) + s = 1 + x - x**2 + 4*x**3/3 - 17*x**4/24 + 31*x**5/30 - 481*x**6/720 +\ + 71*x**7/105 - 20159*x**8/40320 + 379*x**9/840 + O(x**10) + assert r == s + p = HolonomicFunction((6+6*x-3*x**2) - (10*x-3*x**2-3*x**3)*Dx + \ + (4-6*x**3+2*x**4)*Dx**2, x, 0, [0, 1]).series(n=7) + q = x + x**3/6 - 3*x**4/16 + x**5/20 - 23*x**6/960 + O(x**7) + assert p == q + p = HolonomicFunction((6+6*x-3*x**2) - (10*x-3*x**2-3*x**3)*Dx + \ + (4-6*x**3+2*x**4)*Dx**2, x, 0, [1, 0]).series(n=7) + q = 1 - 3*x**2/4 - x**3/4 - 5*x**4/32 - 3*x**5/40 - 17*x**6/384 + O(x**7) + assert p == q + p = expr_to_holonomic(erf(x) + x).series(n=10) + C_3 = symbols('C_3') + q = (erf(x) + x).series(n=10) + assert p.subs(C_3, -2/(3*sqrt(pi))) == q + assert expr_to_holonomic(sqrt(x**3 + x)).series(n=10) == sqrt(x**3 + x).series(n=10) + assert expr_to_holonomic((2*x - 3*x**2)**Rational(1, 3)).series() == ((2*x - 3*x**2)**Rational(1, 3)).series() + assert expr_to_holonomic(sqrt(x**2-x)).series() == (sqrt(x**2-x)).series() + assert expr_to_holonomic(cos(x)**2/x**2, y0={-2: [1, 0, -1]}).series(n=10) == (cos(x)**2/x**2).series(n=10) + assert expr_to_holonomic(cos(x)**2/x**2, x0=1).series(n=10).together() == (cos(x)**2/x**2).series(n=10, x0=1).together() + assert expr_to_holonomic(cos(x-1)**2/(x-1)**2, x0=1, y0={-2: [1, 0, -1]}).series(n=10) \ + == (cos(x-1)**2/(x-1)**2).series(x0=1, n=10) + +def test_evalf_euler(): + x = symbols('x') + R, Dx = DifferentialOperators(QQ.old_poly_ring(x), 'Dx') + + # log(1+x) + p = HolonomicFunction((1 + x)*Dx**2 + Dx, x, 0, [0, 1]) + + # path taken is a straight line from 0 to 1, on the real axis + r = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] + s = '0.699525841805253' # approx. equal to log(2) i.e. 0.693147180559945 + assert sstr(p.evalf(r, method='Euler')[-1]) == s + + # path taken is a triangle 0-->1+i-->2 + r = [0.1 + 0.1*I] + for i in range(9): + r.append(r[-1]+0.1+0.1*I) + for i in range(10): + r.append(r[-1]+0.1-0.1*I) + + # close to the exact solution 1.09861228866811 + # imaginary part also close to zero + s = '1.07530466271334 - 0.0251200594793912*I' + assert sstr(p.evalf(r, method='Euler')[-1]) == s + + # sin(x) + p = HolonomicFunction(Dx**2 + 1, x, 0, [0, 1]) + s = '0.905546532085401 - 6.93889390390723e-18*I' + assert sstr(p.evalf(r, method='Euler')[-1]) == s + + # computing sin(pi/2) using this method + # using a linear path from 0 to pi/2 + r = [0.1] + for i in range(14): + r.append(r[-1] + 0.1) + r.append(pi/2) + s = '1.08016557252834' # close to 1.0 (exact solution) + assert sstr(p.evalf(r, method='Euler')[-1]) == s + + # trying different path, a rectangle (0-->i-->pi/2 + i-->pi/2) + # computing the same value sin(pi/2) using different path + r = [0.1*I] + for i in range(9): + r.append(r[-1]+0.1*I) + for i in range(15): + r.append(r[-1]+0.1) + r.append(pi/2+I) + for i in range(10): + r.append(r[-1]-0.1*I) + + # close to 1.0 + s = '0.976882381836257 - 1.65557671738537e-16*I' + assert sstr(p.evalf(r, method='Euler')[-1]) == s + + # cos(x) + p = HolonomicFunction(Dx**2 + 1, x, 0, [1, 0]) + # compute cos(pi) along 0-->pi + r = [0.05] + for i in range(61): + r.append(r[-1]+0.05) + r.append(pi) + # close to -1 (exact answer) + s = '-1.08140824719196' + assert sstr(p.evalf(r, method='Euler')[-1]) == s + + # a rectangular path (0 -> i -> 2+i -> 2) + r = [0.1*I] + for i in range(9): + r.append(r[-1]+0.1*I) + for i in range(20): + r.append(r[-1]+0.1) + for i in range(10): + r.append(r[-1]-0.1*I) + + p = HolonomicFunction(Dx**2 + 1, x, 0, [1,1]).evalf(r, method='Euler') + s = '0.501421652861245 - 3.88578058618805e-16*I' + assert sstr(p[-1]) == s + +def test_evalf_rk4(): + x = symbols('x') + R, Dx = DifferentialOperators(QQ.old_poly_ring(x), 'Dx') + + # log(1+x) + p = HolonomicFunction((1 + x)*Dx**2 + Dx, x, 0, [0, 1]) + + # path taken is a straight line from 0 to 1, on the real axis + r = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] + s = '0.693146363174626' # approx. equal to log(2) i.e. 0.693147180559945 + assert sstr(p.evalf(r)[-1]) == s + + # path taken is a triangle 0-->1+i-->2 + r = [0.1 + 0.1*I] + for i in range(9): + r.append(r[-1]+0.1+0.1*I) + for i in range(10): + r.append(r[-1]+0.1-0.1*I) + + # close to the exact solution 1.09861228866811 + # imaginary part also close to zero + s = '1.098616 + 1.36083e-7*I' + assert sstr(p.evalf(r)[-1].n(7)) == s + + # sin(x) + p = HolonomicFunction(Dx**2 + 1, x, 0, [0, 1]) + s = '0.90929463522785 + 1.52655665885959e-16*I' + assert sstr(p.evalf(r)[-1]) == s + + # computing sin(pi/2) using this method + # using a linear path from 0 to pi/2 + r = [0.1] + for i in range(14): + r.append(r[-1] + 0.1) + r.append(pi/2) + s = '0.999999895088917' # close to 1.0 (exact solution) + assert sstr(p.evalf(r)[-1]) == s + + # trying different path, a rectangle (0-->i-->pi/2 + i-->pi/2) + # computing the same value sin(pi/2) using different path + r = [0.1*I] + for i in range(9): + r.append(r[-1]+0.1*I) + for i in range(15): + r.append(r[-1]+0.1) + r.append(pi/2+I) + for i in range(10): + r.append(r[-1]-0.1*I) + + # close to 1.0 + s = '1.00000003415141 + 6.11940487991086e-16*I' + assert sstr(p.evalf(r)[-1]) == s + + # cos(x) + p = HolonomicFunction(Dx**2 + 1, x, 0, [1, 0]) + # compute cos(pi) along 0-->pi + r = [0.05] + for i in range(61): + r.append(r[-1]+0.05) + r.append(pi) + # close to -1 (exact answer) + s = '-0.999999993238714' + assert sstr(p.evalf(r)[-1]) == s + + # a rectangular path (0 -> i -> 2+i -> 2) + r = [0.1*I] + for i in range(9): + r.append(r[-1]+0.1*I) + for i in range(20): + r.append(r[-1]+0.1) + for i in range(10): + r.append(r[-1]-0.1*I) + + p = HolonomicFunction(Dx**2 + 1, x, 0, [1,1]).evalf(r) + s = '0.493152791638442 - 1.41553435639707e-15*I' + assert sstr(p[-1]) == s + + +def test_expr_to_holonomic(): + x = symbols('x') + R, Dx = DifferentialOperators(QQ.old_poly_ring(x), 'Dx') + p = expr_to_holonomic((sin(x)/x)**2) + q = HolonomicFunction(8*x + (4*x**2 + 6)*Dx + 6*x*Dx**2 + x**2*Dx**3, x, 0, \ + [1, 0, Rational(-2, 3)]) + assert p == q + p = expr_to_holonomic(1/(1+x**2)**2) + q = HolonomicFunction(4*x + (x**2 + 1)*Dx, x, 0, [1]) + assert p == q + p = expr_to_holonomic(exp(x)*sin(x)+x*log(1+x)) + q = HolonomicFunction((2*x**3 + 10*x**2 + 20*x + 18) + (-2*x**4 - 10*x**3 - 20*x**2 \ + - 18*x)*Dx + (2*x**5 + 6*x**4 + 7*x**3 + 8*x**2 + 10*x - 4)*Dx**2 + \ + (-2*x**5 - 5*x**4 - 2*x**3 + 2*x**2 - x + 4)*Dx**3 + (x**5 + 2*x**4 - x**3 - \ + 7*x**2/2 + x + Rational(5, 2))*Dx**4, x, 0, [0, 1, 4, -1]) + assert p == q + p = expr_to_holonomic(x*exp(x)+cos(x)+1) + q = HolonomicFunction((-x - 3)*Dx + (x + 2)*Dx**2 + (-x - 3)*Dx**3 + (x + 2)*Dx**4, x, \ + 0, [2, 1, 1, 3]) + assert p == q + assert (x*exp(x)+cos(x)+1).series(n=10) == p.series(n=10) + p = expr_to_holonomic(log(1 + x)**2 + 1) + q = HolonomicFunction(Dx + (3*x + 3)*Dx**2 + (x**2 + 2*x + 1)*Dx**3, x, 0, [1, 0, 2]) + assert p == q + p = expr_to_holonomic(erf(x)**2 + x) + q = HolonomicFunction((8*x**4 - 2*x**2 + 2)*Dx**2 + (6*x**3 - x/2)*Dx**3 + \ + (x**2+ Rational(1, 4))*Dx**4, x, 0, [0, 1, 8/pi, 0]) + assert p == q + p = expr_to_holonomic(cosh(x)*x) + q = HolonomicFunction((-x**2 + 2) -2*x*Dx + x**2*Dx**2, x, 0, [0, 1]) + assert p == q + p = expr_to_holonomic(besselj(2, x)) + q = HolonomicFunction((x**2 - 4) + x*Dx + x**2*Dx**2, x, 0, [0, 0]) + assert p == q + p = expr_to_holonomic(besselj(0, x) + exp(x)) + q = HolonomicFunction((-x**2 - x/2 + S.Half) + (x**2 - x/2 - Rational(3, 2))*Dx + (-x**2 + x/2 + 1)*Dx**2 +\ + (x**2 + x/2)*Dx**3, x, 0, [2, 1, S.Half]) + assert p == q + p = expr_to_holonomic(sin(x)**2/x) + q = HolonomicFunction(4 + 4*x*Dx + 3*Dx**2 + x*Dx**3, x, 0, [0, 1, 0]) + assert p == q + p = expr_to_holonomic(sin(x)**2/x, x0=2) + q = HolonomicFunction((4) + (4*x)*Dx + (3)*Dx**2 + (x)*Dx**3, x, 2, [sin(2)**2/2, + sin(2)*cos(2) - sin(2)**2/4, -3*sin(2)**2/4 + cos(2)**2 - sin(2)*cos(2)]) + assert p == q + p = expr_to_holonomic(log(x)/2 - Ci(2*x)/2 + Ci(2)/2) + q = HolonomicFunction(4*Dx + 4*x*Dx**2 + 3*Dx**3 + x*Dx**4, x, 0, \ + [-log(2)/2 - EulerGamma/2 + Ci(2)/2, 0, 1, 0]) + assert p == q + p = p.to_expr() + q = log(x)/2 - Ci(2*x)/2 + Ci(2)/2 + assert p == q + p = expr_to_holonomic(x**S.Half, x0=1) + q = HolonomicFunction(x*Dx - S.Half, x, 1, [1]) + assert p == q + p = expr_to_holonomic(sqrt(1 + x**2)) + q = HolonomicFunction((-x) + (x**2 + 1)*Dx, x, 0, [1]) + assert p == q + assert (expr_to_holonomic(sqrt(x) + sqrt(2*x)).to_expr()-\ + (sqrt(x) + sqrt(2*x))).simplify() == 0 + assert expr_to_holonomic(3*x+2*sqrt(x)).to_expr() == 3*x+2*sqrt(x) + p = expr_to_holonomic((x**4+x**3+5*x**2+3*x+2)/x**2, lenics=3) + q = HolonomicFunction((-2*x**4 - x**3 + 3*x + 4) + (x**5 + x**4 + 5*x**3 + 3*x**2 + \ + 2*x)*Dx, x, 0, {-2: [2, 3, 5]}) + assert p == q + p = expr_to_holonomic(1/(x-1)**2, lenics=3, x0=1) + q = HolonomicFunction((2) + (x - 1)*Dx, x, 1, {-2: [1, 0, 0]}) + assert p == q + a = symbols("a") + p = expr_to_holonomic(sqrt(a*x), x=x) + assert p.to_expr() == sqrt(a)*sqrt(x) + +def test_to_hyper(): + x = symbols('x') + R, Dx = DifferentialOperators(QQ.old_poly_ring(x), 'Dx') + p = HolonomicFunction(Dx - 2, x, 0, [3]).to_hyper() + q = 3 * hyper([], [], 2*x) + assert p == q + p = hyperexpand(HolonomicFunction((1 + x) * Dx - 3, x, 0, [2]).to_hyper()).expand() + q = 2*x**3 + 6*x**2 + 6*x + 2 + assert p == q + p = HolonomicFunction((1 + x)*Dx**2 + Dx, x, 0, [0, 1]).to_hyper() + q = -x**2*hyper((2, 2, 1), (3, 2), -x)/2 + x + assert p == q + p = HolonomicFunction(2*x*Dx + Dx**2, x, 0, [0, 2/sqrt(pi)]).to_hyper() + q = 2*x*hyper((S.Half,), (Rational(3, 2),), -x**2)/sqrt(pi) + assert p == q + p = hyperexpand(HolonomicFunction(2*x*Dx + Dx**2, x, 0, [1, -2/sqrt(pi)]).to_hyper()) + q = erfc(x) + assert p.rewrite(erfc) == q + p = hyperexpand(HolonomicFunction((x**2 - 1) + x*Dx + x**2*Dx**2, + x, 0, [0, S.Half]).to_hyper()) + q = besselj(1, x) + assert p == q + p = hyperexpand(HolonomicFunction(x*Dx**2 + Dx + x, x, 0, [1, 0]).to_hyper()) + q = besselj(0, x) + assert p == q + +def test_to_expr(): + x = symbols('x') + R, Dx = DifferentialOperators(ZZ.old_poly_ring(x), 'Dx') + p = HolonomicFunction(Dx - 1, x, 0, [1]).to_expr() + q = exp(x) + assert p == q + p = HolonomicFunction(Dx**2 + 1, x, 0, [1, 0]).to_expr() + q = cos(x) + assert p == q + p = HolonomicFunction(Dx**2 - 1, x, 0, [1, 0]).to_expr() + q = cosh(x) + assert p == q + p = HolonomicFunction(2 + (4*x - 1)*Dx + \ + (x**2 - x)*Dx**2, x, 0, [1, 2]).to_expr().expand() + q = 1/(x**2 - 2*x + 1) + assert p == q + p = expr_to_holonomic(sin(x)**2/x).integrate((x, 0, x)).to_expr() + q = (sin(x)**2/x).integrate((x, 0, x)) + assert p == q + C_0, C_1, C_2, C_3 = symbols('C_0, C_1, C_2, C_3') + p = expr_to_holonomic(log(1+x**2)).to_expr() + q = C_2*log(x**2 + 1) + assert p == q + p = expr_to_holonomic(log(1+x**2)).diff().to_expr() + q = C_0*x/(x**2 + 1) + assert p == q + p = expr_to_holonomic(erf(x) + x).to_expr() + q = 3*C_3*x - 3*sqrt(pi)*C_3*erf(x)/2 + x + 2*x/sqrt(pi) + assert p == q + p = expr_to_holonomic(sqrt(x), x0=1).to_expr() + assert p == sqrt(x) + assert expr_to_holonomic(sqrt(x)).to_expr() == sqrt(x) + p = expr_to_holonomic(sqrt(1 + x**2)).to_expr() + assert p == sqrt(1+x**2) + p = expr_to_holonomic((2*x**2 + 1)**Rational(2, 3)).to_expr() + assert p == (2*x**2 + 1)**Rational(2, 3) + p = expr_to_holonomic(sqrt(-x**2+2*x)).to_expr() + assert p == sqrt(x)*sqrt(-x + 2) + p = expr_to_holonomic((-2*x**3+7*x)**Rational(2, 3)).to_expr() + q = x**Rational(2, 3)*(-2*x**2 + 7)**Rational(2, 3) + assert p == q + p = from_hyper(hyper((-2, -3), (S.Half, ), x)) + s = hyperexpand(hyper((-2, -3), (S.Half, ), x)) + D_0 = Symbol('D_0') + C_0 = Symbol('C_0') + assert (p.to_expr().subs({C_0:1, D_0:0}) - s).simplify() == 0 + p.y0 = {0: [1], S.Half: [0]} + assert p.to_expr() == s + assert expr_to_holonomic(x**5).to_expr() == x**5 + assert expr_to_holonomic(2*x**3-3*x**2).to_expr().expand() == \ + 2*x**3-3*x**2 + a = symbols("a") + p = (expr_to_holonomic(1.4*x)*expr_to_holonomic(a*x, x)).to_expr() + q = 1.4*a*x**2 + assert p == q + p = (expr_to_holonomic(1.4*x)+expr_to_holonomic(a*x, x)).to_expr() + q = x*(a + 1.4) + assert p == q + p = (expr_to_holonomic(1.4*x)+expr_to_holonomic(x)).to_expr() + assert p == 2.4*x + + +def test_integrate(): + x = symbols('x') + R, Dx = DifferentialOperators(ZZ.old_poly_ring(x), 'Dx') + p = expr_to_holonomic(sin(x)**2/x, x0=1).integrate((x, 2, 3)) + q = '0.166270406994788' + assert sstr(p) == q + p = expr_to_holonomic(sin(x)).integrate((x, 0, x)).to_expr() + q = 1 - cos(x) + assert p == q + p = expr_to_holonomic(sin(x)).integrate((x, 0, 3)) + q = 1 - cos(3) + assert p == q + p = expr_to_holonomic(sin(x)/x, x0=1).integrate((x, 1, 2)) + q = '0.659329913368450' + assert sstr(p) == q + p = expr_to_holonomic(sin(x)**2/x, x0=1).integrate((x, 1, 0)) + q = '-0.423690480850035' + assert sstr(p) == q + p = expr_to_holonomic(sin(x)/x) + assert p.integrate(x).to_expr() == Si(x) + assert p.integrate((x, 0, 2)) == Si(2) + p = expr_to_holonomic(sin(x)**2/x) + q = p.to_expr() + assert p.integrate(x).to_expr() == q.integrate((x, 0, x)) + assert p.integrate((x, 0, 1)) == q.integrate((x, 0, 1)) + assert expr_to_holonomic(1/x, x0=1).integrate(x).to_expr() == log(x) + p = expr_to_holonomic((x + 1)**3*exp(-x), x0=-1).integrate(x).to_expr() + q = (-x**3 - 6*x**2 - 15*x + 6*exp(x + 1) - 16)*exp(-x) + assert p == q + p = expr_to_holonomic(cos(x)**2/x**2, y0={-2: [1, 0, -1]}).integrate(x).to_expr() + q = -Si(2*x) - cos(x)**2/x + assert p == q + p = expr_to_holonomic(sqrt(x**2+x)).integrate(x).to_expr() + q = (x**Rational(3, 2)*(2*x**2 + 3*x + 1) - x*sqrt(x + 1)*asinh(sqrt(x)))/(4*x*sqrt(x + 1)) + assert p == q + p = expr_to_holonomic(sqrt(x**2+1)).integrate(x).to_expr() + q = (sqrt(x**2+1)).integrate(x) + assert (p-q).simplify() == 0 + p = expr_to_holonomic(1/x**2, y0={-2:[1, 0, 0]}) + r = expr_to_holonomic(1/x**2, lenics=3) + assert p == r + q = expr_to_holonomic(cos(x)**2) + assert (r*q).integrate(x).to_expr() == -Si(2*x) - cos(x)**2/x + + +def test_diff(): + x, y = symbols('x, y') + R, Dx = DifferentialOperators(ZZ.old_poly_ring(x), 'Dx') + p = HolonomicFunction(x*Dx**2 + 1, x, 0, [0, 1]) + assert p.diff().to_expr() == p.to_expr().diff().simplify() + p = HolonomicFunction(Dx**2 - 1, x, 0, [1, 0]) + assert p.diff(x, 2).to_expr() == p.to_expr() + p = expr_to_holonomic(Si(x)) + assert p.diff().to_expr() == sin(x)/x + assert p.diff(y) == 0 + C_0, C_1, C_2, C_3 = symbols('C_0, C_1, C_2, C_3') + q = Si(x) + assert p.diff(x).to_expr() == q.diff() + assert p.diff(x, 2).to_expr().subs(C_0, Rational(-1, 3)).cancel() == q.diff(x, 2).cancel() + assert p.diff(x, 3).series().subs({C_3: Rational(-1, 3), C_0: 0}) == q.diff(x, 3).series() + + +def test_extended_domain_in_expr_to_holonomic(): + x = symbols('x') + p = expr_to_holonomic(1.2*cos(3.1*x)) + assert p.to_expr() == 1.2*cos(3.1*x) + assert sstr(p.integrate(x).to_expr()) == '0.387096774193548*sin(3.1*x)' + _, Dx = DifferentialOperators(RR.old_poly_ring(x), 'Dx') + p = expr_to_holonomic(1.1329138213*x) + q = HolonomicFunction((-1.1329138213) + (1.1329138213*x)*Dx, x, 0, {1: [1.1329138213]}) + assert p == q + assert p.to_expr() == 1.1329138213*x + assert sstr(p.integrate((x, 1, 2))) == sstr((1.1329138213*x).integrate((x, 1, 2))) + y, z = symbols('y, z') + p = expr_to_holonomic(sin(x*y*z), x=x) + assert p.to_expr() == sin(x*y*z) + assert p.integrate(x).to_expr() == (-cos(x*y*z) + 1)/(y*z) + p = expr_to_holonomic(sin(x*y + z), x=x).integrate(x).to_expr() + q = (cos(z) - cos(x*y + z))/y + assert p == q + a = symbols('a') + p = expr_to_holonomic(a*x, x) + assert p.to_expr() == a*x + assert p.integrate(x).to_expr() == a*x**2/2 + D_2, C_1 = symbols("D_2, C_1") + p = expr_to_holonomic(x) + expr_to_holonomic(1.2*cos(x)) + p = p.to_expr().subs(D_2, 0) + assert p - x - 1.2*cos(1.0*x) == 0 + p = expr_to_holonomic(x) * expr_to_holonomic(1.2*cos(x)) + p = p.to_expr().subs(C_1, 0) + assert p - 1.2*x*cos(1.0*x) == 0 + + +def test_to_meijerg(): + x = symbols('x') + assert hyperexpand(expr_to_holonomic(sin(x)).to_meijerg()) == sin(x) + assert hyperexpand(expr_to_holonomic(cos(x)).to_meijerg()) == cos(x) + assert hyperexpand(expr_to_holonomic(exp(x)).to_meijerg()) == exp(x) + assert hyperexpand(expr_to_holonomic(log(x)).to_meijerg()).simplify() == log(x) + assert expr_to_holonomic(4*x**2/3 + 7).to_meijerg() == 4*x**2/3 + 7 + assert hyperexpand(expr_to_holonomic(besselj(2, x), lenics=3).to_meijerg()) == besselj(2, x) + p = hyper((Rational(-1, 2), -3), (), x) + assert from_hyper(p).to_meijerg() == hyperexpand(p) + p = hyper((S.One, S(3)), (S(2), ), x) + assert (hyperexpand(from_hyper(p).to_meijerg()) - hyperexpand(p)).expand() == 0 + p = from_hyper(hyper((-2, -3), (S.Half, ), x)) + s = hyperexpand(hyper((-2, -3), (S.Half, ), x)) + C_0 = Symbol('C_0') + C_1 = Symbol('C_1') + D_0 = Symbol('D_0') + assert (hyperexpand(p.to_meijerg()).subs({C_0:1, D_0:0}) - s).simplify() == 0 + p.y0 = {0: [1], S.Half: [0]} + assert (hyperexpand(p.to_meijerg()) - s).simplify() == 0 + p = expr_to_holonomic(besselj(S.Half, x), initcond=False) + assert (p.to_expr() - (D_0*sin(x) + C_0*cos(x) + C_1*sin(x))/sqrt(x)).simplify() == 0 + p = expr_to_holonomic(besselj(S.Half, x), y0={Rational(-1, 2): [sqrt(2)/sqrt(pi), sqrt(2)/sqrt(pi)]}) + assert (p.to_expr() - besselj(S.Half, x) - besselj(Rational(-1, 2), x)).simplify() == 0 + + +def test_gaussian(): + mu, x = symbols("mu x") + sd = symbols("sd", positive=True) + Q = QQ[mu, sd].get_field() + e = sqrt(2)*exp(-(-mu + x)**2/(2*sd**2))/(2*sqrt(pi)*sd) + h1 = expr_to_holonomic(e, x, domain=Q) + + _, Dx = DifferentialOperators(Q.old_poly_ring(x), 'Dx') + h2 = HolonomicFunction((-mu/sd**2 + x/sd**2) + (1)*Dx, x) + + assert h1 == h2 + + +def test_beta(): + a, b, x = symbols("a b x", positive=True) + e = x**(a - 1)*(-x + 1)**(b - 1)/beta(a, b) + Q = QQ[a, b].get_field() + h1 = expr_to_holonomic(e, x, domain=Q) + + _, Dx = DifferentialOperators(Q.old_poly_ring(x), 'Dx') + h2 = HolonomicFunction((a + x*(-a - b + 2) - 1) + (x**2 - x)*Dx, x) + + assert h1 == h2 + + +def test_gamma(): + a, b, x = symbols("a b x", positive=True) + e = b**(-a)*x**(a - 1)*exp(-x/b)/gamma(a) + Q = QQ[a, b].get_field() + h1 = expr_to_holonomic(e, x, domain=Q) + + _, Dx = DifferentialOperators(Q.old_poly_ring(x), 'Dx') + h2 = HolonomicFunction((-a + 1 + x/b) + (x)*Dx, x) + + assert h1 == h2 + + +def test_symbolic_power(): + x, n = symbols("x n") + Q = QQ[n].get_field() + _, Dx = DifferentialOperators(Q.old_poly_ring(x), 'Dx') + h1 = HolonomicFunction((-1) + (x)*Dx, x) ** -n + h2 = HolonomicFunction((n) + (x)*Dx, x) + + assert h1 == h2 + + +def test_negative_power(): + x = symbols("x") + _, Dx = DifferentialOperators(QQ.old_poly_ring(x), 'Dx') + h1 = HolonomicFunction((-1) + (x)*Dx, x) ** -2 + h2 = HolonomicFunction((2) + (x)*Dx, x) + + assert h1 == h2 + + +def test_expr_in_power(): + x, n = symbols("x n") + Q = QQ[n].get_field() + _, Dx = DifferentialOperators(Q.old_poly_ring(x), 'Dx') + h1 = HolonomicFunction((-1) + (x)*Dx, x) ** (n - 3) + h2 = HolonomicFunction((-n + 3) + (x)*Dx, x) + + assert h1 == h2 + + +def test_DifferentialOperatorEqPoly(): + x = symbols('x', integer=True) + R, Dx = DifferentialOperators(QQ.old_poly_ring(x), 'Dx') + do = DifferentialOperator([x**2, R.base.zero, R.base.zero], R) + do2 = DifferentialOperator([x**2, 1, x], R) + assert not do == do2 + + # polynomial comparison issue, see https://github.com/sympy/sympy/pull/15799 + # should work once that is solved + # p = do.listofpoly[0] + # assert do == p + + p2 = do2.listofpoly[0] + assert not do2 == p2 + + +def test_DifferentialOperatorPow(): + x = symbols('x', integer=True) + R, _ = DifferentialOperators(QQ.old_poly_ring(x), 'Dx') + do = DifferentialOperator([x**2, R.base.zero, R.base.zero], R) + a = DifferentialOperator([R.base.one], R) + for n in range(10): + assert a == do**n + a *= do diff --git a/venv/lib/python3.11/site-packages/sympy/holonomic/tests/test_recurrence.py b/venv/lib/python3.11/site-packages/sympy/holonomic/tests/test_recurrence.py new file mode 100644 index 0000000000000000000000000000000000000000..526595e91c5fc507877275e3e53e78c6f3716095 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/holonomic/tests/test_recurrence.py @@ -0,0 +1,41 @@ +from sympy.holonomic.recurrence import RecurrenceOperators, RecurrenceOperator +from sympy.core.symbol import symbols +from sympy.polys.domains.rationalfield import QQ + + +def test_RecurrenceOperator(): + n = symbols('n', integer=True) + R, Sn = RecurrenceOperators(QQ.old_poly_ring(n), 'Sn') + assert Sn*n == (n + 1)*Sn + assert Sn*n**2 == (n**2+1+2*n)*Sn + assert Sn**2*n**2 == (n**2 + 4*n + 4)*Sn**2 + p = (Sn**3*n**2 + Sn*n)**2 + q = (n**2 + 3*n + 2)*Sn**2 + (2*n**3 + 19*n**2 + 57*n + 52)*Sn**4 + (n**4 + 18*n**3 + \ + 117*n**2 + 324*n + 324)*Sn**6 + assert p == q + + +def test_RecurrenceOperatorEqPoly(): + n = symbols('n', integer=True) + R, Sn = RecurrenceOperators(QQ.old_poly_ring(n), 'Sn') + rr = RecurrenceOperator([n**2, 0, 0], R) + rr2 = RecurrenceOperator([n**2, 1, n], R) + assert not rr == rr2 + + # polynomial comparison issue, see https://github.com/sympy/sympy/pull/15799 + # should work once that is solved + # d = rr.listofpoly[0] + # assert rr == d + + d2 = rr2.listofpoly[0] + assert not rr2 == d2 + + +def test_RecurrenceOperatorPow(): + n = symbols('n', integer=True) + R, _ = RecurrenceOperators(QQ.old_poly_ring(n), 'Sn') + rr = RecurrenceOperator([n**2, 0, 0], R) + a = RecurrenceOperator([R.base.one], R) + for m in range(10): + assert a == rr**m + a *= rr diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/__init__.py b/venv/lib/python3.11/site-packages/sympy/integrals/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e78fe96f84d68e9b119571eb22dedb7033811b23 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/__init__.py @@ -0,0 +1,45 @@ +"""Integration functions that integrate a SymPy expression. + + Examples + ======== + + >>> from sympy import integrate, sin + >>> from sympy.abc import x + >>> integrate(1/x,x) + log(x) + >>> integrate(sin(x),x) + -cos(x) +""" +from .integrals import integrate, Integral, line_integrate +from .transforms import (mellin_transform, inverse_mellin_transform, + MellinTransform, InverseMellinTransform, + laplace_transform, inverse_laplace_transform, + laplace_correspondence, laplace_initial_conds, + LaplaceTransform, InverseLaplaceTransform, + fourier_transform, inverse_fourier_transform, + FourierTransform, InverseFourierTransform, + sine_transform, inverse_sine_transform, + SineTransform, InverseSineTransform, + cosine_transform, inverse_cosine_transform, + CosineTransform, InverseCosineTransform, + hankel_transform, inverse_hankel_transform, + HankelTransform, InverseHankelTransform) +from .singularityfunctions import singularityintegrate + +__all__ = [ + 'integrate', 'Integral', 'line_integrate', + + 'mellin_transform', 'inverse_mellin_transform', 'MellinTransform', + 'InverseMellinTransform', 'laplace_transform', + 'inverse_laplace_transform', 'LaplaceTransform', + 'laplace_correspondence', 'laplace_initial_conds', + 'InverseLaplaceTransform', 'fourier_transform', + 'inverse_fourier_transform', 'FourierTransform', + 'InverseFourierTransform', 'sine_transform', 'inverse_sine_transform', + 'SineTransform', 'InverseSineTransform', 'cosine_transform', + 'inverse_cosine_transform', 'CosineTransform', 'InverseCosineTransform', + 'hankel_transform', 'inverse_hankel_transform', 'HankelTransform', + 'InverseHankelTransform', + + 'singularityintegrate', +] diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/benchmarks/__init__.py b/venv/lib/python3.11/site-packages/sympy/integrals/benchmarks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/benchmarks/bench_integrate.py b/venv/lib/python3.11/site-packages/sympy/integrals/benchmarks/bench_integrate.py new file mode 100644 index 0000000000000000000000000000000000000000..833bc57403b34df1e75c798084ffc4d8afe9eae6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/benchmarks/bench_integrate.py @@ -0,0 +1,21 @@ +from sympy.core.symbol import Symbol +from sympy.functions.elementary.trigonometric import sin +from sympy.integrals.integrals import integrate + +x = Symbol('x') + + +def bench_integrate_sin(): + integrate(sin(x), x) + + +def bench_integrate_x1sin(): + integrate(x**1*sin(x), x) + + +def bench_integrate_x2sin(): + integrate(x**2*sin(x), x) + + +def bench_integrate_x3sin(): + integrate(x**3*sin(x), x) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/benchmarks/bench_trigintegrate.py b/venv/lib/python3.11/site-packages/sympy/integrals/benchmarks/bench_trigintegrate.py new file mode 100644 index 0000000000000000000000000000000000000000..403c5471b8048ff2aa97bf2f837b9ea05f0fd904 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/benchmarks/bench_trigintegrate.py @@ -0,0 +1,13 @@ +from sympy.core.symbol import Symbol +from sympy.functions.elementary.trigonometric import sin +from sympy.integrals.trigonometry import trigintegrate + +x = Symbol('x') + + +def timeit_trigintegrate_sin3x(): + trigintegrate(sin(x)**3, x) + + +def timeit_trigintegrate_x2(): + trigintegrate(x**2, x) # -> None diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/deltafunctions.py b/venv/lib/python3.11/site-packages/sympy/integrals/deltafunctions.py new file mode 100644 index 0000000000000000000000000000000000000000..ae9fef0b0010a313e0866a54d978024dd475f882 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/deltafunctions.py @@ -0,0 +1,201 @@ +from sympy.core.mul import Mul +from sympy.core.singleton import S +from sympy.core.sorting import default_sort_key +from sympy.functions import DiracDelta, Heaviside +from .integrals import Integral, integrate + + +def change_mul(node, x): + """change_mul(node, x) + + Rearranges the operands of a product, bringing to front any simple + DiracDelta expression. + + Explanation + =========== + + If no simple DiracDelta expression was found, then all the DiracDelta + expressions are simplified (using DiracDelta.expand(diracdelta=True, wrt=x)). + + Return: (dirac, new node) + Where: + o dirac is either a simple DiracDelta expression or None (if no simple + expression was found); + o new node is either a simplified DiracDelta expressions or None (if it + could not be simplified). + + Examples + ======== + + >>> from sympy import DiracDelta, cos + >>> from sympy.integrals.deltafunctions import change_mul + >>> from sympy.abc import x, y + >>> change_mul(x*y*DiracDelta(x)*cos(x), x) + (DiracDelta(x), x*y*cos(x)) + >>> change_mul(x*y*DiracDelta(x**2 - 1)*cos(x), x) + (None, x*y*cos(x)*DiracDelta(x - 1)/2 + x*y*cos(x)*DiracDelta(x + 1)/2) + >>> change_mul(x*y*DiracDelta(cos(x))*cos(x), x) + (None, None) + + See Also + ======== + + sympy.functions.special.delta_functions.DiracDelta + deltaintegrate + """ + + new_args = [] + dirac = None + + #Sorting is needed so that we consistently collapse the same delta; + #However, we must preserve the ordering of non-commutative terms + c, nc = node.args_cnc() + sorted_args = sorted(c, key=default_sort_key) + sorted_args.extend(nc) + + for arg in sorted_args: + if arg.is_Pow and isinstance(arg.base, DiracDelta): + new_args.append(arg.func(arg.base, arg.exp - 1)) + arg = arg.base + if dirac is None and (isinstance(arg, DiracDelta) and arg.is_simple(x)): + dirac = arg + else: + new_args.append(arg) + if not dirac: # there was no simple dirac + new_args = [] + for arg in sorted_args: + if isinstance(arg, DiracDelta): + new_args.append(arg.expand(diracdelta=True, wrt=x)) + elif arg.is_Pow and isinstance(arg.base, DiracDelta): + new_args.append(arg.func(arg.base.expand(diracdelta=True, wrt=x), arg.exp)) + else: + new_args.append(arg) + if new_args != sorted_args: + nnode = Mul(*new_args).expand() + else: # if the node didn't change there is nothing to do + nnode = None + return (None, nnode) + return (dirac, Mul(*new_args)) + + +def deltaintegrate(f, x): + """ + deltaintegrate(f, x) + + Explanation + =========== + + The idea for integration is the following: + + - If we are dealing with a DiracDelta expression, i.e. DiracDelta(g(x)), + we try to simplify it. + + If we could simplify it, then we integrate the resulting expression. + We already know we can integrate a simplified expression, because only + simple DiracDelta expressions are involved. + + If we couldn't simplify it, there are two cases: + + 1) The expression is a simple expression: we return the integral, + taking care if we are dealing with a Derivative or with a proper + DiracDelta. + + 2) The expression is not simple (i.e. DiracDelta(cos(x))): we can do + nothing at all. + + - If the node is a multiplication node having a DiracDelta term: + + First we expand it. + + If the expansion did work, then we try to integrate the expansion. + + If not, we try to extract a simple DiracDelta term, then we have two + cases: + + 1) We have a simple DiracDelta term, so we return the integral. + + 2) We didn't have a simple term, but we do have an expression with + simplified DiracDelta terms, so we integrate this expression. + + Examples + ======== + + >>> from sympy.abc import x, y, z + >>> from sympy.integrals.deltafunctions import deltaintegrate + >>> from sympy import sin, cos, DiracDelta + >>> deltaintegrate(x*sin(x)*cos(x)*DiracDelta(x - 1), x) + sin(1)*cos(1)*Heaviside(x - 1) + >>> deltaintegrate(y**2*DiracDelta(x - z)*DiracDelta(y - z), y) + z**2*DiracDelta(x - z)*Heaviside(y - z) + + See Also + ======== + + sympy.functions.special.delta_functions.DiracDelta + sympy.integrals.integrals.Integral + """ + if not f.has(DiracDelta): + return None + + # g(x) = DiracDelta(h(x)) + if f.func == DiracDelta: + h = f.expand(diracdelta=True, wrt=x) + if h == f: # can't simplify the expression + #FIXME: the second term tells whether is DeltaDirac or Derivative + #For integrating derivatives of DiracDelta we need the chain rule + if f.is_simple(x): + if (len(f.args) <= 1 or f.args[1] == 0): + return Heaviside(f.args[0]) + else: + return (DiracDelta(f.args[0], f.args[1] - 1) / + f.args[0].as_poly().LC()) + else: # let's try to integrate the simplified expression + fh = integrate(h, x) + return fh + elif f.is_Mul or f.is_Pow: # g(x) = a*b*c*f(DiracDelta(h(x)))*d*e + g = f.expand() + if f != g: # the expansion worked + fh = integrate(g, x) + if fh is not None and not isinstance(fh, Integral): + return fh + else: + # no expansion performed, try to extract a simple DiracDelta term + deltaterm, rest_mult = change_mul(f, x) + + if not deltaterm: + if rest_mult: + fh = integrate(rest_mult, x) + return fh + else: + from sympy.solvers import solve + deltaterm = deltaterm.expand(diracdelta=True, wrt=x) + if deltaterm.is_Mul: # Take out any extracted factors + deltaterm, rest_mult_2 = change_mul(deltaterm, x) + rest_mult = rest_mult*rest_mult_2 + point = solve(deltaterm.args[0], x)[0] + + # Return the largest hyperreal term left after + # repeated integration by parts. For example, + # + # integrate(y*DiracDelta(x, 1),x) == y*DiracDelta(x,0), not 0 + # + # This is so Integral(y*DiracDelta(x).diff(x),x).doit() + # will return y*DiracDelta(x) instead of 0 or DiracDelta(x), + # both of which are correct everywhere the value is defined + # but give wrong answers for nested integration. + n = (0 if len(deltaterm.args)==1 else deltaterm.args[1]) + m = 0 + while n >= 0: + r = S.NegativeOne**n*rest_mult.diff(x, n).subs(x, point) + if r.is_zero: + n -= 1 + m += 1 + else: + if m == 0: + return r*Heaviside(x - point) + else: + return r*DiracDelta(x,m-1) + # In some very weak sense, x=0 is still a singularity, + # but we hope will not be of any practical consequence. + return S.Zero + return None diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/heurisch.py b/venv/lib/python3.11/site-packages/sympy/integrals/heurisch.py new file mode 100644 index 0000000000000000000000000000000000000000..a27e2700afd08db16c7a86020eabe5feeb6e1c85 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/heurisch.py @@ -0,0 +1,781 @@ +from __future__ import annotations + +from collections import defaultdict +from functools import reduce +from itertools import permutations + +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.mul import Mul +from sympy.core.symbol import Wild, Dummy, Symbol +from sympy.core.basic import sympify +from sympy.core.numbers import Rational, pi, I +from sympy.core.relational import Eq, Ne +from sympy.core.singleton import S +from sympy.core.sorting import ordered +from sympy.core.traversal import iterfreeargs + +from sympy.functions import exp, sin, cos, tan, cot, asin, atan +from sympy.functions import log, sinh, cosh, tanh, coth, asinh +from sympy.functions import sqrt, erf, erfi, li, Ei +from sympy.functions import besselj, bessely, besseli, besselk +from sympy.functions import hankel1, hankel2, jn, yn +from sympy.functions.elementary.complexes import Abs, re, im, sign, arg +from sympy.functions.elementary.exponential import LambertW +from sympy.functions.elementary.integers import floor, ceiling +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.delta_functions import Heaviside, DiracDelta + +from sympy.simplify.radsimp import collect + +from sympy.logic.boolalg import And, Or +from sympy.utilities.iterables import uniq + +from sympy.polys import quo, gcd, lcm, factor_list, cancel, PolynomialError +from sympy.polys.monomials import itermonomials +from sympy.polys.polyroots import root_factors + +from sympy.polys.rings import PolyRing +from sympy.polys.solvers import solve_lin_sys +from sympy.polys.constructor import construct_domain + +from sympy.integrals.integrals import integrate + + +def components(f, x): + """ + Returns a set of all functional components of the given expression + which includes symbols, function applications and compositions and + non-integer powers. Fractional powers are collected with + minimal, positive exponents. + + Examples + ======== + + >>> from sympy import cos, sin + >>> from sympy.abc import x + >>> from sympy.integrals.heurisch import components + + >>> components(sin(x)*cos(x)**2, x) + {x, sin(x), cos(x)} + + See Also + ======== + + heurisch + """ + result = set() + + if f.has_free(x): + if f.is_symbol and f.is_commutative: + result.add(f) + elif f.is_Function or f.is_Derivative: + for g in f.args: + result |= components(g, x) + + result.add(f) + elif f.is_Pow: + result |= components(f.base, x) + + if not f.exp.is_Integer: + if f.exp.is_Rational: + result.add(f.base**Rational(1, f.exp.q)) + else: + result |= components(f.exp, x) | {f} + else: + for g in f.args: + result |= components(g, x) + + return result + +# name -> [] of symbols +_symbols_cache: dict[str, list[Dummy]] = {} + + +# NB @cacheit is not convenient here +def _symbols(name, n): + """get vector of symbols local to this module""" + try: + lsyms = _symbols_cache[name] + except KeyError: + lsyms = [] + _symbols_cache[name] = lsyms + + while len(lsyms) < n: + lsyms.append( Dummy('%s%i' % (name, len(lsyms))) ) + + return lsyms[:n] + + +def heurisch_wrapper(f, x, rewrite=False, hints=None, mappings=None, retries=3, + degree_offset=0, unnecessary_permutations=None, + _try_heurisch=None): + """ + A wrapper around the heurisch integration algorithm. + + Explanation + =========== + + This method takes the result from heurisch and checks for poles in the + denominator. For each of these poles, the integral is reevaluated, and + the final integration result is given in terms of a Piecewise. + + Examples + ======== + + >>> from sympy import cos, symbols + >>> from sympy.integrals.heurisch import heurisch, heurisch_wrapper + >>> n, x = symbols('n x') + >>> heurisch(cos(n*x), x) + sin(n*x)/n + >>> heurisch_wrapper(cos(n*x), x) + Piecewise((sin(n*x)/n, Ne(n, 0)), (x, True)) + + See Also + ======== + + heurisch + """ + from sympy.solvers.solvers import solve, denoms + f = sympify(f) + if not f.has_free(x): + return f*x + + res = heurisch(f, x, rewrite, hints, mappings, retries, degree_offset, + unnecessary_permutations, _try_heurisch) + if not isinstance(res, Basic): + return res + + # We consider each denominator in the expression, and try to find + # cases where one or more symbolic denominator might be zero. The + # conditions for these cases are stored in the list slns. + # + # Since denoms returns a set we use ordered. This is important because the + # ordering of slns determines the order of the resulting Piecewise so we + # need a deterministic order here to make the output deterministic. + slns = [] + for d in ordered(denoms(res)): + try: + slns += solve([d], dict=True, exclude=(x,)) + except NotImplementedError: + pass + if not slns: + return res + slns = list(uniq(slns)) + # Remove the solutions corresponding to poles in the original expression. + slns0 = [] + for d in denoms(f): + try: + slns0 += solve([d], dict=True, exclude=(x,)) + except NotImplementedError: + pass + slns = [s for s in slns if s not in slns0] + if not slns: + return res + if len(slns) > 1: + eqs = [] + for sub_dict in slns: + eqs.extend([Eq(key, value) for key, value in sub_dict.items()]) + slns = solve(eqs, dict=True, exclude=(x,)) + slns + # For each case listed in the list slns, we reevaluate the integral. + pairs = [] + for sub_dict in slns: + expr = heurisch(f.subs(sub_dict), x, rewrite, hints, mappings, retries, + degree_offset, unnecessary_permutations, + _try_heurisch) + cond = And(*[Eq(key, value) for key, value in sub_dict.items()]) + generic = Or(*[Ne(key, value) for key, value in sub_dict.items()]) + if expr is None: + expr = integrate(f.subs(sub_dict),x) + pairs.append((expr, cond)) + # If there is one condition, put the generic case first. Otherwise, + # doing so may lead to longer Piecewise formulas + if len(pairs) == 1: + pairs = [(heurisch(f, x, rewrite, hints, mappings, retries, + degree_offset, unnecessary_permutations, + _try_heurisch), + generic), + (pairs[0][0], True)] + else: + pairs.append((heurisch(f, x, rewrite, hints, mappings, retries, + degree_offset, unnecessary_permutations, + _try_heurisch), + True)) + return Piecewise(*pairs) + +class BesselTable: + """ + Derivatives of Bessel functions of orders n and n-1 + in terms of each other. + + See the docstring of DiffCache. + """ + + def __init__(self): + self.table = {} + self.n = Dummy('n') + self.z = Dummy('z') + self._create_table() + + def _create_table(t): + table, n, z = t.table, t.n, t.z + for f in (besselj, bessely, hankel1, hankel2): + table[f] = (f(n-1, z) - n*f(n, z)/z, + (n-1)*f(n-1, z)/z - f(n, z)) + + f = besseli + table[f] = (f(n-1, z) - n*f(n, z)/z, + (n-1)*f(n-1, z)/z + f(n, z)) + f = besselk + table[f] = (-f(n-1, z) - n*f(n, z)/z, + (n-1)*f(n-1, z)/z - f(n, z)) + + for f in (jn, yn): + table[f] = (f(n-1, z) - (n+1)*f(n, z)/z, + (n-1)*f(n-1, z)/z - f(n, z)) + + def diffs(t, f, n, z): + if f in t.table: + diff0, diff1 = t.table[f] + repl = [(t.n, n), (t.z, z)] + return (diff0.subs(repl), diff1.subs(repl)) + + def has(t, f): + return f in t.table + +_bessel_table = None + +class DiffCache: + """ + Store for derivatives of expressions. + + Explanation + =========== + + The standard form of the derivative of a Bessel function of order n + contains two Bessel functions of orders n-1 and n+1, respectively. + Such forms cannot be used in parallel Risch algorithm, because + there is a linear recurrence relation between the three functions + while the algorithm expects that functions and derivatives are + represented in terms of algebraically independent transcendentals. + + The solution is to take two of the functions, e.g., those of orders + n and n-1, and to express the derivatives in terms of the pair. + To guarantee that the proper form is used the two derivatives are + cached as soon as one is encountered. + + Derivatives of other functions are also cached at no extra cost. + All derivatives are with respect to the same variable `x`. + """ + + def __init__(self, x): + self.cache = {} + self.x = x + + global _bessel_table + if not _bessel_table: + _bessel_table = BesselTable() + + def get_diff(self, f): + cache = self.cache + + if f in cache: + pass + elif (not hasattr(f, 'func') or + not _bessel_table.has(f.func)): + cache[f] = cancel(f.diff(self.x)) + else: + n, z = f.args + d0, d1 = _bessel_table.diffs(f.func, n, z) + dz = self.get_diff(z) + cache[f] = d0*dz + cache[f.func(n-1, z)] = d1*dz + + return cache[f] + +def heurisch(f, x, rewrite=False, hints=None, mappings=None, retries=3, + degree_offset=0, unnecessary_permutations=None, + _try_heurisch=None): + """ + Compute indefinite integral using heuristic Risch algorithm. + + Explanation + =========== + + This is a heuristic approach to indefinite integration in finite + terms using the extended heuristic (parallel) Risch algorithm, based + on Manuel Bronstein's "Poor Man's Integrator". + + The algorithm supports various classes of functions including + transcendental elementary or special functions like Airy, + Bessel, Whittaker and Lambert. + + Note that this algorithm is not a decision procedure. If it isn't + able to compute the antiderivative for a given function, then this is + not a proof that such a functions does not exist. One should use + recursive Risch algorithm in such case. It's an open question if + this algorithm can be made a full decision procedure. + + This is an internal integrator procedure. You should use top level + 'integrate' function in most cases, as this procedure needs some + preprocessing steps and otherwise may fail. + + Specification + ============= + + heurisch(f, x, rewrite=False, hints=None) + + where + f : expression + x : symbol + + rewrite -> force rewrite 'f' in terms of 'tan' and 'tanh' + hints -> a list of functions that may appear in anti-derivate + + - hints = None --> no suggestions at all + - hints = [ ] --> try to figure out + - hints = [f1, ..., fn] --> we know better + + Examples + ======== + + >>> from sympy import tan + >>> from sympy.integrals.heurisch import heurisch + >>> from sympy.abc import x, y + + >>> heurisch(y*tan(x), x) + y*log(tan(x)**2 + 1)/2 + + See Manuel Bronstein's "Poor Man's Integrator": + + References + ========== + + .. [1] https://www-sop.inria.fr/cafe/Manuel.Bronstein/pmint/index.html + + For more information on the implemented algorithm refer to: + + .. [2] K. Geddes, L. Stefanus, On the Risch-Norman Integration + Method and its Implementation in Maple, Proceedings of + ISSAC'89, ACM Press, 212-217. + + .. [3] J. H. Davenport, On the Parallel Risch Algorithm (I), + Proceedings of EUROCAM'82, LNCS 144, Springer, 144-157. + + .. [4] J. H. Davenport, On the Parallel Risch Algorithm (III): + Use of Tangents, SIGSAM Bulletin 16 (1982), 3-6. + + .. [5] J. H. Davenport, B. M. Trager, On the Parallel Risch + Algorithm (II), ACM Transactions on Mathematical + Software 11 (1985), 356-362. + + See Also + ======== + + sympy.integrals.integrals.Integral.doit + sympy.integrals.integrals.Integral + sympy.integrals.heurisch.components + """ + f = sympify(f) + + # There are some functions that Heurisch cannot currently handle, + # so do not even try. + # Set _try_heurisch=True to skip this check + if _try_heurisch is not True: + if f.has(Abs, re, im, sign, Heaviside, DiracDelta, floor, ceiling, arg): + return + + if not f.has_free(x): + return f*x + + if not f.is_Add: + indep, f = f.as_independent(x) + else: + indep = S.One + + rewritables = { + (sin, cos, cot): tan, + (sinh, cosh, coth): tanh, + } + + if rewrite: + for candidates, rule in rewritables.items(): + f = f.rewrite(candidates, rule) + else: + for candidates in rewritables.keys(): + if f.has(*candidates): + break + else: + rewrite = True + + terms = components(f, x) + dcache = DiffCache(x) + + if hints is not None: + if not hints: + a = Wild('a', exclude=[x]) + b = Wild('b', exclude=[x]) + c = Wild('c', exclude=[x]) + + for g in set(terms): # using copy of terms + if g.is_Function: + if isinstance(g, li): + M = g.args[0].match(a*x**b) + + if M is not None: + terms.add( x*(li(M[a]*x**M[b]) - (M[a]*x**M[b])**(-1/M[b])*Ei((M[b]+1)*log(M[a]*x**M[b])/M[b])) ) + #terms.add( x*(li(M[a]*x**M[b]) - (x**M[b])**(-1/M[b])*Ei((M[b]+1)*log(M[a]*x**M[b])/M[b])) ) + #terms.add( x*(li(M[a]*x**M[b]) - x*Ei((M[b]+1)*log(M[a]*x**M[b])/M[b])) ) + #terms.add( li(M[a]*x**M[b]) - Ei((M[b]+1)*log(M[a]*x**M[b])/M[b]) ) + + elif isinstance(g, exp): + M = g.args[0].match(a*x**2) + + if M is not None: + if M[a].is_positive: + terms.add(erfi(sqrt(M[a])*x)) + else: # M[a].is_negative or unknown + terms.add(erf(sqrt(-M[a])*x)) + + M = g.args[0].match(a*x**2 + b*x + c) + + if M is not None: + if M[a].is_positive: + terms.add(sqrt(pi/4*(-M[a]))*exp(M[c] - M[b]**2/(4*M[a]))* + erfi(sqrt(M[a])*x + M[b]/(2*sqrt(M[a])))) + elif M[a].is_negative: + terms.add(sqrt(pi/4*(-M[a]))*exp(M[c] - M[b]**2/(4*M[a]))* + erf(sqrt(-M[a])*x - M[b]/(2*sqrt(-M[a])))) + + M = g.args[0].match(a*log(x)**2) + + if M is not None: + if M[a].is_positive: + terms.add(erfi(sqrt(M[a])*log(x) + 1/(2*sqrt(M[a])))) + if M[a].is_negative: + terms.add(erf(sqrt(-M[a])*log(x) - 1/(2*sqrt(-M[a])))) + + elif g.is_Pow: + if g.exp.is_Rational and g.exp.q == 2: + M = g.base.match(a*x**2 + b) + + if M is not None and M[b].is_positive: + if M[a].is_positive: + terms.add(asinh(sqrt(M[a]/M[b])*x)) + elif M[a].is_negative: + terms.add(asin(sqrt(-M[a]/M[b])*x)) + + M = g.base.match(a*x**2 - b) + + if M is not None and M[b].is_positive: + if M[a].is_positive: + dF = 1/sqrt(M[a]*x**2 - M[b]) + F = log(2*sqrt(M[a])*sqrt(M[a]*x**2 - M[b]) + 2*M[a]*x)/sqrt(M[a]) + dcache.cache[F] = dF # hack: F.diff(x) doesn't automatically simplify to f + terms.add(F) + elif M[a].is_negative: + terms.add(-M[b]/2*sqrt(-M[a])* + atan(sqrt(-M[a])*x/sqrt(M[a]*x**2 - M[b]))) + + else: + terms |= set(hints) + + for g in set(terms): # using copy of terms + terms |= components(dcache.get_diff(g), x) + + # XXX: The commented line below makes heurisch more deterministic wrt + # PYTHONHASHSEED and the iteration order of sets. There are other places + # where sets are iterated over but this one is possibly the most important. + # Theoretically the order here should not matter but different orderings + # can expose potential bugs in the different code paths so potentially it + # is better to keep the non-determinism. + # + # terms = list(ordered(terms)) + + # TODO: caching is significant factor for why permutations work at all. Change this. + V = _symbols('x', len(terms)) + + + # sort mapping expressions from largest to smallest (last is always x). + mapping = list(reversed(list(zip(*ordered( # + [(a[0].as_independent(x)[1], a) for a in zip(terms, V)])))[1])) # + rev_mapping = {v: k for k, v in mapping} # + if mappings is None: # + # optimizing the number of permutations of mapping # + assert mapping[-1][0] == x # if not, find it and correct this comment + unnecessary_permutations = [mapping.pop(-1)] + # permute types of objects + types = defaultdict(list) + for i in mapping: + e, _ = i + types[type(e)].append(i) + mapping = [types[i] for i in types] + def _iter_mappings(): + for i in permutations(mapping): + # make the expression of a given type be ordered + yield [j for i in i for j in ordered(i)] + mappings = _iter_mappings() + else: + unnecessary_permutations = unnecessary_permutations or [] + + def _substitute(expr): + return expr.subs(mapping) + + for mapping in mappings: + mapping = list(mapping) + mapping = mapping + unnecessary_permutations + diffs = [ _substitute(dcache.get_diff(g)) for g in terms ] + denoms = [ g.as_numer_denom()[1] for g in diffs ] + if all(h.is_polynomial(*V) for h in denoms) and _substitute(f).is_rational_function(*V): + denom = reduce(lambda p, q: lcm(p, q, *V), denoms) + break + else: + if not rewrite: + result = heurisch(f, x, rewrite=True, hints=hints, + unnecessary_permutations=unnecessary_permutations) + + if result is not None: + return indep*result + return None + + numers = [ cancel(denom*g) for g in diffs ] + def _derivation(h): + return Add(*[ d * h.diff(v) for d, v in zip(numers, V) ]) + + def _deflation(p): + for y in V: + if not p.has(y): + continue + + if _derivation(p) is not S.Zero: + c, q = p.as_poly(y).primitive() + return _deflation(c)*gcd(q, q.diff(y)).as_expr() + + return p + + def _splitter(p): + for y in V: + if not p.has(y): + continue + + if _derivation(y) is not S.Zero: + c, q = p.as_poly(y).primitive() + + q = q.as_expr() + + h = gcd(q, _derivation(q), y) + s = quo(h, gcd(q, q.diff(y), y), y) + + c_split = _splitter(c) + + if s.as_poly(y).degree() == 0: + return (c_split[0], q * c_split[1]) + + q_split = _splitter(cancel(q / s)) + + return (c_split[0]*q_split[0]*s, c_split[1]*q_split[1]) + + return (S.One, p) + + special = {} + + for term in terms: + if term.is_Function: + if isinstance(term, tan): + special[1 + _substitute(term)**2] = False + elif isinstance(term, tanh): + special[1 + _substitute(term)] = False + special[1 - _substitute(term)] = False + elif isinstance(term, LambertW): + special[_substitute(term)] = True + + F = _substitute(f) + + P, Q = F.as_numer_denom() + + u_split = _splitter(denom) + v_split = _splitter(Q) + + polys = set(list(v_split) + [ u_split[0] ] + list(special.keys())) + + s = u_split[0] * Mul(*[ k for k, v in special.items() if v ]) + polified = [ p.as_poly(*V) for p in [s, P, Q] ] + + if None in polified: + return None + + #--- definitions for _integrate + a, b, c = [ p.total_degree() for p in polified ] + + poly_denom = (s * v_split[0] * _deflation(v_split[1])).as_expr() + + def _exponent(g): + if g.is_Pow: + if g.exp.is_Rational and g.exp.q != 1: + if g.exp.p > 0: + return g.exp.p + g.exp.q - 1 + else: + return abs(g.exp.p + g.exp.q) + else: + return 1 + elif not g.is_Atom and g.args: + return max(_exponent(h) for h in g.args) + else: + return 1 + + A, B = _exponent(f), a + max(b, c) + + if A > 1 and B > 1: + monoms = tuple(ordered(itermonomials(V, A + B - 1 + degree_offset))) + else: + monoms = tuple(ordered(itermonomials(V, A + B + degree_offset))) + + poly_coeffs = _symbols('A', len(monoms)) + + poly_part = Add(*[ poly_coeffs[i]*monomial + for i, monomial in enumerate(monoms) ]) + + reducibles = set() + + for poly in ordered(polys): + coeff, factors = factor_list(poly, *V) + reducibles.add(coeff) + reducibles.update(fact for fact, mul in factors) + + def _integrate(field=None): + atans = set() + pairs = set() + + if field == 'Q': + irreducibles = set(reducibles) + else: + setV = set(V) + irreducibles = set() + for poly in ordered(reducibles): + zV = setV & set(iterfreeargs(poly)) + for z in ordered(zV): + s = set(root_factors(poly, z, filter=field)) + irreducibles |= s + break + + log_part, atan_part = [], [] + + for poly in ordered(irreducibles): + m = collect(poly, I, evaluate=False) + y = m.get(I, S.Zero) + if y: + x = m.get(S.One, S.Zero) + if x.has(I) or y.has(I): + continue # nontrivial x + I*y + pairs.add((x, y)) + irreducibles.remove(poly) + + while pairs: + x, y = pairs.pop() + if (x, -y) in pairs: + pairs.remove((x, -y)) + # Choosing b with no minus sign + if y.could_extract_minus_sign(): + y = -y + irreducibles.add(x*x + y*y) + atans.add(atan(x/y)) + else: + irreducibles.add(x + I*y) + + + B = _symbols('B', len(irreducibles)) + C = _symbols('C', len(atans)) + + # Note: the ordering matters here + for poly, b in reversed(list(zip(ordered(irreducibles), B))): + if poly.has(*V): + poly_coeffs.append(b) + log_part.append(b * log(poly)) + + for poly, c in reversed(list(zip(ordered(atans), C))): + if poly.has(*V): + poly_coeffs.append(c) + atan_part.append(c * poly) + + # TODO: Currently it's better to use symbolic expressions here instead + # of rational functions, because it's simpler and FracElement doesn't + # give big speed improvement yet. This is because cancellation is slow + # due to slow polynomial GCD algorithms. If this gets improved then + # revise this code. + candidate = poly_part/poly_denom + Add(*log_part) + Add(*atan_part) + h = F - _derivation(candidate) / denom + raw_numer = h.as_numer_denom()[0] + + # Rewrite raw_numer as a polynomial in K[coeffs][V] where K is a field + # that we have to determine. We can't use simply atoms() because log(3), + # sqrt(y) and similar expressions can appear, leading to non-trivial + # domains. + syms = set(poly_coeffs) | set(V) + non_syms = set() + + def find_non_syms(expr): + if expr.is_Integer or expr.is_Rational: + pass # ignore trivial numbers + elif expr in syms: + pass # ignore variables + elif not expr.has_free(*syms): + non_syms.add(expr) + elif expr.is_Add or expr.is_Mul or expr.is_Pow: + list(map(find_non_syms, expr.args)) + else: + # TODO: Non-polynomial expression. This should have been + # filtered out at an earlier stage. + raise PolynomialError + + try: + find_non_syms(raw_numer) + except PolynomialError: + return None + else: + ground, _ = construct_domain(non_syms, field=True) + + coeff_ring = PolyRing(poly_coeffs, ground) + ring = PolyRing(V, coeff_ring) + try: + numer = ring.from_expr(raw_numer) + except ValueError: + raise PolynomialError + solution = solve_lin_sys(numer.coeffs(), coeff_ring, _raw=False) + + if solution is None: + return None + else: + return candidate.xreplace(solution).xreplace( + dict(zip(poly_coeffs, [S.Zero]*len(poly_coeffs)))) + + if all(isinstance(_, Symbol) for _ in V): + more_free = F.free_symbols - set(V) + else: + Fd = F.as_dummy() + more_free = Fd.xreplace(dict(zip(V, (Dummy() for _ in V))) + ).free_symbols & Fd.free_symbols + if not more_free: + # all free generators are identified in V + solution = _integrate('Q') + + if solution is None: + solution = _integrate() + else: + solution = _integrate() + + if solution is not None: + antideriv = solution.subs(rev_mapping) + antideriv = cancel(antideriv).expand() + + if antideriv.is_Add: + antideriv = antideriv.as_independent(x)[1] + + return indep*antideriv + else: + if retries >= 0: + result = heurisch(f, x, mappings=mappings, rewrite=rewrite, hints=hints, retries=retries - 1, unnecessary_permutations=unnecessary_permutations) + + if result is not None: + return indep*result + + return None diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/integrals.py b/venv/lib/python3.11/site-packages/sympy/integrals/integrals.py new file mode 100644 index 0000000000000000000000000000000000000000..b9ed4a22802acc455f5162e109fc575223c97338 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/integrals.py @@ -0,0 +1,1640 @@ +from __future__ import annotations + +from sympy.concrete.expr_with_limits import AddWithLimits +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.expr import Expr +from sympy.core.exprtools import factor_terms +from sympy.core.function import diff +from sympy.core.logic import fuzzy_bool +from sympy.core.mul import Mul +from sympy.core.numbers import oo, pi +from sympy.core.relational import Ne +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, Wild) +from sympy.core.sympify import sympify +from sympy.functions import Piecewise, sqrt, piecewise_fold, tan, cot, atan +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.complexes import Abs, sign +from sympy.functions.elementary.miscellaneous import Min, Max +from sympy.functions.special.singularity_functions import Heaviside +from .rationaltools import ratint +from sympy.matrices import MatrixBase +from sympy.polys import Poly, PolynomialError +from sympy.series.formal import FormalPowerSeries +from sympy.series.limits import limit +from sympy.series.order import Order +from sympy.tensor.functions import shape +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.utilities.iterables import is_sequence +from sympy.utilities.misc import filldedent + + +class Integral(AddWithLimits): + """Represents unevaluated integral.""" + + __slots__ = () + + args: tuple[Expr, Tuple] # type: ignore + + def __new__(cls, function, *symbols, **assumptions) -> Integral: + """Create an unevaluated integral. + + Explanation + =========== + + Arguments are an integrand followed by one or more limits. + + If no limits are given and there is only one free symbol in the + expression, that symbol will be used, otherwise an error will be + raised. + + >>> from sympy import Integral + >>> from sympy.abc import x, y + >>> Integral(x) + Integral(x, x) + >>> Integral(y) + Integral(y, y) + + When limits are provided, they are interpreted as follows (using + ``x`` as though it were the variable of integration): + + (x,) or x - indefinite integral + (x, a) - "evaluate at" integral is an abstract antiderivative + (x, a, b) - definite integral + + The ``as_dummy`` method can be used to see which symbols cannot be + targeted by subs: those with a prepended underscore cannot be + changed with ``subs``. (Also, the integration variables themselves -- + the first element of a limit -- can never be changed by subs.) + + >>> i = Integral(x, x) + >>> at = Integral(x, (x, x)) + >>> i.as_dummy() + Integral(x, x) + >>> at.as_dummy() + Integral(_0, (_0, x)) + + """ + + #This will help other classes define their own definitions + #of behaviour with Integral. + if hasattr(function, '_eval_Integral'): + return function._eval_Integral(*symbols, **assumptions) + + if isinstance(function, Poly): + sympy_deprecation_warning( + """ + integrate(Poly) and Integral(Poly) are deprecated. Instead, + use the Poly.integrate() method, or convert the Poly to an + Expr first with the Poly.as_expr() method. + """, + deprecated_since_version="1.6", + active_deprecations_target="deprecated-integrate-poly") + + obj = AddWithLimits.__new__(cls, function, *symbols, **assumptions) + return obj + + def __getnewargs__(self): + return (self.function,) + tuple([tuple(xab) for xab in self.limits]) + + @property + def free_symbols(self): + """ + This method returns the symbols that will exist when the + integral is evaluated. This is useful if one is trying to + determine whether an integral depends on a certain + symbol or not. + + Examples + ======== + + >>> from sympy import Integral + >>> from sympy.abc import x, y + >>> Integral(x, (x, y, 1)).free_symbols + {y} + + See Also + ======== + + sympy.concrete.expr_with_limits.ExprWithLimits.function + sympy.concrete.expr_with_limits.ExprWithLimits.limits + sympy.concrete.expr_with_limits.ExprWithLimits.variables + """ + return super().free_symbols + + def _eval_is_zero(self): + # This is a very naive and quick test, not intended to do the integral to + # answer whether it is zero or not, e.g. Integral(sin(x), (x, 0, 2*pi)) + # is zero but this routine should return None for that case. But, like + # Mul, there are trivial situations for which the integral will be + # zero so we check for those. + if self.function.is_zero: + return True + got_none = False + for l in self.limits: + if len(l) == 3: + z = (l[1] == l[2]) or (l[1] - l[2]).is_zero + if z: + return True + elif z is None: + got_none = True + free = self.function.free_symbols + for xab in self.limits: + if len(xab) == 1: + free.add(xab[0]) + continue + if len(xab) == 2 and xab[0] not in free: + if xab[1].is_zero: + return True + elif xab[1].is_zero is None: + got_none = True + # take integration symbol out of free since it will be replaced + # with the free symbols in the limits + free.discard(xab[0]) + # add in the new symbols + for i in xab[1:]: + free.update(i.free_symbols) + if self.function.is_zero is False and got_none is False: + return False + + def transform(self, x, u): + r""" + Performs a change of variables from `x` to `u` using the relationship + given by `x` and `u` which will define the transformations `f` and `F` + (which are inverses of each other) as follows: + + 1) If `x` is a Symbol (which is a variable of integration) then `u` + will be interpreted as some function, f(u), with inverse F(u). + This, in effect, just makes the substitution of x with f(x). + + 2) If `u` is a Symbol then `x` will be interpreted as some function, + F(x), with inverse f(u). This is commonly referred to as + u-substitution. + + Once f and F have been identified, the transformation is made as + follows: + + .. math:: \int_a^b x \mathrm{d}x \rightarrow \int_{F(a)}^{F(b)} f(x) + \frac{\mathrm{d}}{\mathrm{d}x} + + where `F(x)` is the inverse of `f(x)` and the limits and integrand have + been corrected so as to retain the same value after integration. + + Notes + ===== + + The mappings, F(x) or f(u), must lead to a unique integral. Linear + or rational linear expression, ``2*x``, ``1/x`` and ``sqrt(x)``, will + always work; quadratic expressions like ``x**2 - 1`` are acceptable + as long as the resulting integrand does not depend on the sign of + the solutions (see examples). + + The integral will be returned unchanged if ``x`` is not a variable of + integration. + + ``x`` must be (or contain) only one of of the integration variables. If + ``u`` has more than one free symbol then it should be sent as a tuple + (``u``, ``uvar``) where ``uvar`` identifies which variable is replacing + the integration variable. + XXX can it contain another integration variable? + + Examples + ======== + + >>> from sympy.abc import a, x, u + >>> from sympy import Integral, cos, sqrt + + >>> i = Integral(x*cos(x**2 - 1), (x, 0, 1)) + + transform can change the variable of integration + + >>> i.transform(x, u) + Integral(u*cos(u**2 - 1), (u, 0, 1)) + + transform can perform u-substitution as long as a unique + integrand is obtained: + + >>> ui = i.transform(x**2 - 1, u) + >>> ui + Integral(cos(u)/2, (u, -1, 0)) + + This attempt fails because x = +/-sqrt(u + 1) and the + sign does not cancel out of the integrand: + + >>> Integral(cos(x**2 - 1), (x, 0, 1)).transform(x**2 - 1, u) + Traceback (most recent call last): + ... + ValueError: + The mapping between F(x) and f(u) did not give a unique integrand. + + transform can do a substitution. Here, the previous + result is transformed back into the original expression + using "u-substitution": + + >>> ui.transform(sqrt(u + 1), x) == i + True + + We can accomplish the same with a regular substitution: + + >>> ui.transform(u, x**2 - 1) == i + True + + If the `x` does not contain a symbol of integration then + the integral will be returned unchanged. Integral `i` does + not have an integration variable `a` so no change is made: + + >>> i.transform(a, x) == i + True + + When `u` has more than one free symbol the symbol that is + replacing `x` must be identified by passing `u` as a tuple: + + >>> Integral(x, (x, 0, 1)).transform(x, (u + a, u)) + Integral(a + u, (u, -a, 1 - a)) + >>> Integral(x, (x, 0, 1)).transform(x, (u + a, a)) + Integral(a + u, (a, -u, 1 - u)) + + See Also + ======== + + sympy.concrete.expr_with_limits.ExprWithLimits.variables : Lists the integration variables + as_dummy : Replace integration variables with dummy ones + """ + d = Dummy('d') + + xfree = x.free_symbols.intersection(self.variables) + if len(xfree) > 1: + raise ValueError( + 'F(x) can only contain one of: %s' % self.variables) + xvar = xfree.pop() if xfree else d + + if xvar not in self.variables: + return self + + u = sympify(u) + if isinstance(u, Expr): + ufree = u.free_symbols + if len(ufree) == 0: + raise ValueError(filldedent(''' + f(u) cannot be a constant''')) + if len(ufree) > 1: + raise ValueError(filldedent(''' + When f(u) has more than one free symbol, the one replacing x + must be identified: pass f(u) as (f(u), u)''')) + uvar = ufree.pop() + else: + u, uvar = u + if uvar not in u.free_symbols: + raise ValueError(filldedent(''' + Expecting a tuple (expr, symbol) where symbol identified + a free symbol in expr, but symbol is not in expr's free + symbols.''')) + if not isinstance(uvar, Symbol): + # This probably never evaluates to True + raise ValueError(filldedent(''' + Expecting a tuple (expr, symbol) but didn't get + a symbol; got %s''' % uvar)) + + if x.is_Symbol and u.is_Symbol: + return self.xreplace({x: u}) + + if not x.is_Symbol and not u.is_Symbol: + raise ValueError('either x or u must be a symbol') + + if uvar == xvar: + return self.transform(x, (u.subs(uvar, d), d)).xreplace({d: uvar}) + + if uvar in self.limits: + raise ValueError(filldedent(''' + u must contain the same variable as in x + or a variable that is not already an integration variable''')) + + from sympy.solvers.solvers import solve + if not x.is_Symbol: + F = [x.subs(xvar, d)] + soln = solve(u - x, xvar, check=False) + if not soln: + raise ValueError('no solution for solve(F(x) - f(u), x)') + f = [fi.subs(uvar, d) for fi in soln] + else: + f = [u.subs(uvar, d)] + from sympy.simplify.simplify import posify + pdiff, reps = posify(u - x) + puvar = uvar.subs([(v, k) for k, v in reps.items()]) + soln = [s.subs(reps) for s in solve(pdiff, puvar)] + if not soln: + raise ValueError('no solution for solve(F(x) - f(u), u)') + F = [fi.subs(xvar, d) for fi in soln] + + newfuncs = {(self.function.subs(xvar, fi)*fi.diff(d) + ).subs(d, uvar) for fi in f} + if len(newfuncs) > 1: + raise ValueError(filldedent(''' + The mapping between F(x) and f(u) did not give + a unique integrand.''')) + newfunc = newfuncs.pop() + + def _calc_limit_1(F, a, b): + """ + replace d with a, using subs if possible, otherwise limit + where sign of b is considered + """ + wok = F.subs(d, a) + if wok is S.NaN or wok.is_finite is False and a.is_finite: + return limit(sign(b)*F, d, a) + return wok + + def _calc_limit(a, b): + """ + replace d with a, using subs if possible, otherwise limit + where sign of b is considered + """ + avals = list({_calc_limit_1(Fi, a, b) for Fi in F}) + if len(avals) > 1: + raise ValueError(filldedent(''' + The mapping between F(x) and f(u) did not + give a unique limit.''')) + return avals[0] + + newlimits = [] + for xab in self.limits: + sym = xab[0] + if sym == xvar: + if len(xab) == 3: + a, b = xab[1:] + a, b = _calc_limit(a, b), _calc_limit(b, a) + if fuzzy_bool(a - b > 0): + a, b = b, a + newfunc = -newfunc + newlimits.append((uvar, a, b)) + elif len(xab) == 2: + a = _calc_limit(xab[1], 1) + newlimits.append((uvar, a)) + else: + newlimits.append(uvar) + else: + newlimits.append(xab) + + return self.func(newfunc, *newlimits) + + def doit(self, **hints): + """ + Perform the integration using any hints given. + + Examples + ======== + + >>> from sympy import Piecewise, S + >>> from sympy.abc import x, t + >>> p = x**2 + Piecewise((0, x/t < 0), (1, True)) + >>> p.integrate((t, S(4)/5, 1), (x, -1, 1)) + 1/3 + + See Also + ======== + + sympy.integrals.trigonometry.trigintegrate + sympy.integrals.heurisch.heurisch + sympy.integrals.rationaltools.ratint + as_sum : Approximate the integral using a sum + """ + if not hints.get('integrals', True): + return self + + deep = hints.get('deep', True) + meijerg = hints.get('meijerg', None) + conds = hints.get('conds', 'piecewise') + risch = hints.get('risch', None) + heurisch = hints.get('heurisch', None) + manual = hints.get('manual', None) + if len(list(filter(None, (manual, meijerg, risch, heurisch)))) > 1: + raise ValueError("At most one of manual, meijerg, risch, heurisch can be True") + elif manual: + meijerg = risch = heurisch = False + elif meijerg: + manual = risch = heurisch = False + elif risch: + manual = meijerg = heurisch = False + elif heurisch: + manual = meijerg = risch = False + eval_kwargs = {"meijerg": meijerg, "risch": risch, "manual": manual, "heurisch": heurisch, + "conds": conds} + + if conds not in ('separate', 'piecewise', 'none'): + raise ValueError('conds must be one of "separate", "piecewise", ' + '"none", got: %s' % conds) + + if risch and any(len(xab) > 1 for xab in self.limits): + raise ValueError('risch=True is only allowed for indefinite integrals.') + + # check for the trivial zero + if self.is_zero: + return S.Zero + + # hacks to handle integrals of + # nested summations + from sympy.concrete.summations import Sum + if isinstance(self.function, Sum): + if any(v in self.function.limits[0] for v in self.variables): + raise ValueError('Limit of the sum cannot be an integration variable.') + if any(l.is_infinite for l in self.function.limits[0][1:]): + return self + _i = self + _sum = self.function + return _sum.func(_i.func(_sum.function, *_i.limits).doit(), *_sum.limits).doit() + + # now compute and check the function + function = self.function + + # hack to use a consistent Heaviside(x, 1/2) + function = function.replace( + lambda x: isinstance(x, Heaviside) and x.args[1]*2 != 1, + lambda x: Heaviside(x.args[0])) + + if deep: + function = function.doit(**hints) + if function.is_zero: + return S.Zero + + # hacks to handle special cases + if isinstance(function, MatrixBase): + return function.applyfunc( + lambda f: self.func(f, *self.limits).doit(**hints)) + + if isinstance(function, FormalPowerSeries): + if len(self.limits) > 1: + raise NotImplementedError + xab = self.limits[0] + if len(xab) > 1: + return function.integrate(xab, **eval_kwargs) + else: + return function.integrate(xab[0], **eval_kwargs) + + # There is no trivial answer and special handling + # is done so continue + + # first make sure any definite limits have integration + # variables with matching assumptions + reps = {} + for xab in self.limits: + if len(xab) != 3: + # it makes sense to just make + # all x real but in practice with the + # current state of integration...this + # doesn't work out well + # x = xab[0] + # if x not in reps and not x.is_real: + # reps[x] = Dummy(real=True) + continue + x, a, b = xab + l = (a, b) + if all(i.is_nonnegative for i in l) and not x.is_nonnegative: + d = Dummy(positive=True) + elif all(i.is_nonpositive for i in l) and not x.is_nonpositive: + d = Dummy(negative=True) + elif all(i.is_real for i in l) and not x.is_real: + d = Dummy(real=True) + else: + d = None + if d: + reps[x] = d + if reps: + undo = {v: k for k, v in reps.items()} + did = self.xreplace(reps).doit(**hints) + if isinstance(did, tuple): # when separate=True + did = tuple([i.xreplace(undo) for i in did]) + else: + did = did.xreplace(undo) + return did + + # continue with existing assumptions + undone_limits = [] + # ulj = free symbols of any undone limits' upper and lower limits + ulj = set() + for xab in self.limits: + # compute uli, the free symbols in the + # Upper and Lower limits of limit I + if len(xab) == 1: + uli = set(xab[:1]) + elif len(xab) == 2: + uli = xab[1].free_symbols + elif len(xab) == 3: + uli = xab[1].free_symbols.union(xab[2].free_symbols) + # this integral can be done as long as there is no blocking + # limit that has been undone. An undone limit is blocking if + # it contains an integration variable that is in this limit's + # upper or lower free symbols or vice versa + if xab[0] in ulj or any(v[0] in uli for v in undone_limits): + undone_limits.append(xab) + ulj.update(uli) + function = self.func(*([function] + [xab])) + factored_function = function.factor() + if not isinstance(factored_function, Integral): + function = factored_function + continue + + if function.has(Abs, sign) and ( + (len(xab) < 3 and all(x.is_extended_real for x in xab)) or + (len(xab) == 3 and all(x.is_extended_real and not x.is_infinite for + x in xab[1:]))): + # some improper integrals are better off with Abs + xr = Dummy("xr", real=True) + function = (function.xreplace({xab[0]: xr}) + .rewrite(Piecewise).xreplace({xr: xab[0]})) + elif function.has(Min, Max): + function = function.rewrite(Piecewise) + if (function.has(Piecewise) and + not isinstance(function, Piecewise)): + function = piecewise_fold(function) + if isinstance(function, Piecewise): + if len(xab) == 1: + antideriv = function._eval_integral(xab[0], + **eval_kwargs) + else: + antideriv = self._eval_integral( + function, xab[0], **eval_kwargs) + else: + # There are a number of tradeoffs in using the + # Meijer G method. It can sometimes be a lot faster + # than other methods, and sometimes slower. And + # there are certain types of integrals for which it + # is more likely to work than others. These + # heuristics are incorporated in deciding what + # integration methods to try, in what order. See the + # integrate() docstring for details. + def try_meijerg(function, xab): + ret = None + if len(xab) == 3 and meijerg is not False: + x, a, b = xab + try: + res = meijerint_definite(function, x, a, b) + except NotImplementedError: + _debug('NotImplementedError ' + 'from meijerint_definite') + res = None + if res is not None: + f, cond = res + if conds == 'piecewise': + u = self.func(function, (x, a, b)) + # if Piecewise modifies cond too + # much it may not be recognized by + # _condsimp pattern matching so just + # turn off all evaluation + return Piecewise((f, cond), (u, True), + evaluate=False) + elif conds == 'separate': + if len(self.limits) != 1: + raise ValueError(filldedent(''' + conds=separate not supported in + multiple integrals''')) + ret = f, cond + else: + ret = f + return ret + + meijerg1 = meijerg + if (meijerg is not False and + len(xab) == 3 and xab[1].is_extended_real and xab[2].is_extended_real + and not function.is_Poly and + (xab[1].has(oo, -oo) or xab[2].has(oo, -oo))): + ret = try_meijerg(function, xab) + if ret is not None: + function = ret + continue + meijerg1 = False + # If the special meijerg code did not succeed in + # finding a definite integral, then the code using + # meijerint_indefinite will not either (it might + # find an antiderivative, but the answer is likely + # to be nonsensical). Thus if we are requested to + # only use Meijer G-function methods, we give up at + # this stage. Otherwise we just disable G-function + # methods. + if meijerg1 is False and meijerg is True: + antideriv = None + else: + antideriv = self._eval_integral( + function, xab[0], **eval_kwargs) + if antideriv is None and meijerg is True: + ret = try_meijerg(function, xab) + if ret is not None: + function = ret + continue + + final = hints.get('final', True) + # dotit may be iterated but floor terms making atan and acot + # continuous should only be added in the final round + if (final and not isinstance(antideriv, Integral) and + antideriv is not None): + for atan_term in antideriv.atoms(atan): + atan_arg = atan_term.args[0] + # Checking `atan_arg` to be linear combination of `tan` or `cot` + for tan_part in atan_arg.atoms(tan): + x1 = Dummy('x1') + tan_exp1 = atan_arg.subs(tan_part, x1) + # The coefficient of `tan` should be constant + coeff = tan_exp1.diff(x1) + if x1 not in coeff.free_symbols: + a = tan_part.args[0] + antideriv = antideriv.subs(atan_term, Add(atan_term, + sign(coeff)*pi*floor((a-pi/2)/pi))) + for cot_part in atan_arg.atoms(cot): + x1 = Dummy('x1') + cot_exp1 = atan_arg.subs(cot_part, x1) + # The coefficient of `cot` should be constant + coeff = cot_exp1.diff(x1) + if x1 not in coeff.free_symbols: + a = cot_part.args[0] + antideriv = antideriv.subs(atan_term, Add(atan_term, + sign(coeff)*pi*floor((a)/pi))) + + if antideriv is None: + undone_limits.append(xab) + function = self.func(*([function] + [xab])).factor() + factored_function = function.factor() + if not isinstance(factored_function, Integral): + function = factored_function + continue + else: + if len(xab) == 1: + function = antideriv + else: + if len(xab) == 3: + x, a, b = xab + elif len(xab) == 2: + x, b = xab + a = None + else: + raise NotImplementedError + + if deep: + if isinstance(a, Basic): + a = a.doit(**hints) + if isinstance(b, Basic): + b = b.doit(**hints) + + if antideriv.is_Poly: + gens = list(antideriv.gens) + gens.remove(x) + + antideriv = antideriv.as_expr() + + function = antideriv._eval_interval(x, a, b) + function = Poly(function, *gens) + else: + def is_indef_int(g, x): + return (isinstance(g, Integral) and + any(i == (x,) for i in g.limits)) + + def eval_factored(f, x, a, b): + # _eval_interval for integrals with + # (constant) factors + # a single indefinite integral is assumed + args = [] + for g in Mul.make_args(f): + if is_indef_int(g, x): + args.append(g._eval_interval(x, a, b)) + else: + args.append(g) + return Mul(*args) + + integrals, others, piecewises = [], [], [] + for f in Add.make_args(antideriv): + if any(is_indef_int(g, x) + for g in Mul.make_args(f)): + integrals.append(f) + elif any(isinstance(g, Piecewise) + for g in Mul.make_args(f)): + piecewises.append(piecewise_fold(f)) + else: + others.append(f) + uneval = Add(*[eval_factored(f, x, a, b) + for f in integrals]) + try: + evalued = Add(*others)._eval_interval(x, a, b) + evalued_pw = piecewise_fold(Add(*piecewises))._eval_interval(x, a, b) + function = uneval + evalued + evalued_pw + except NotImplementedError: + # This can happen if _eval_interval depends in a + # complicated way on limits that cannot be computed + undone_limits.append(xab) + function = self.func(*([function] + [xab])) + factored_function = function.factor() + if not isinstance(factored_function, Integral): + function = factored_function + return function + + def _eval_derivative(self, sym): + """Evaluate the derivative of the current Integral object by + differentiating under the integral sign [1], using the Fundamental + Theorem of Calculus [2] when possible. + + Explanation + =========== + + Whenever an Integral is encountered that is equivalent to zero or + has an integrand that is independent of the variable of integration + those integrals are performed. All others are returned as Integral + instances which can be resolved with doit() (provided they are integrable). + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Differentiation_under_the_integral_sign + .. [2] https://en.wikipedia.org/wiki/Fundamental_theorem_of_calculus + + Examples + ======== + + >>> from sympy import Integral + >>> from sympy.abc import x, y + >>> i = Integral(x + y, y, (y, 1, x)) + >>> i.diff(x) + Integral(x + y, (y, x)) + Integral(1, y, (y, 1, x)) + >>> i.doit().diff(x) == i.diff(x).doit() + True + >>> i.diff(y) + 0 + + The previous must be true since there is no y in the evaluated integral: + + >>> i.free_symbols + {x} + >>> i.doit() + 2*x**3/3 - x/2 - 1/6 + + """ + + # differentiate under the integral sign; we do not + # check for regularity conditions (TODO), see issue 4215 + + # get limits and the function + f, limits = self.function, list(self.limits) + + # the order matters if variables of integration appear in the limits + # so work our way in from the outside to the inside. + limit = limits.pop(-1) + if len(limit) == 3: + x, a, b = limit + elif len(limit) == 2: + x, b = limit + a = None + else: + a = b = None + x = limit[0] + + if limits: # f is the argument to an integral + f = self.func(f, *tuple(limits)) + + # assemble the pieces + def _do(f, ab): + dab_dsym = diff(ab, sym) + if not dab_dsym: + return S.Zero + if isinstance(f, Integral): + limits = [(x, x) if (len(l) == 1 and l[0] == x) else l + for l in f.limits] + f = self.func(f.function, *limits) + return f.subs(x, ab)*dab_dsym + + rv = S.Zero + if b is not None: + rv += _do(f, b) + if a is not None: + rv -= _do(f, a) + if len(limit) == 1 and sym == x: + # the dummy variable *is* also the real-world variable + arg = f + rv += arg + else: + # the dummy variable might match sym but it's + # only a dummy and the actual variable is determined + # by the limits, so mask off the variable of integration + # while differentiating + u = Dummy('u') + arg = f.subs(x, u).diff(sym).subs(u, x) + if arg: + rv += self.func(arg, (x, a, b)) + return rv + + def _eval_integral(self, f, x, meijerg=None, risch=None, manual=None, + heurisch=None, conds='piecewise',final=None): + """ + Calculate the anti-derivative to the function f(x). + + Explanation + =========== + + The following algorithms are applied (roughly in this order): + + 1. Simple heuristics (based on pattern matching and integral table): + + - most frequently used functions (e.g. polynomials, products of + trig functions) + + 2. Integration of rational functions: + + - A complete algorithm for integrating rational functions is + implemented (the Lazard-Rioboo-Trager algorithm). The algorithm + also uses the partial fraction decomposition algorithm + implemented in apart() as a preprocessor to make this process + faster. Note that the integral of a rational function is always + elementary, but in general, it may include a RootSum. + + 3. Full Risch algorithm: + + - The Risch algorithm is a complete decision + procedure for integrating elementary functions, which means that + given any elementary function, it will either compute an + elementary antiderivative, or else prove that none exists. + Currently, part of transcendental case is implemented, meaning + elementary integrals containing exponentials, logarithms, and + (soon!) trigonometric functions can be computed. The algebraic + case, e.g., functions containing roots, is much more difficult + and is not implemented yet. + + - If the routine fails (because the integrand is not elementary, or + because a case is not implemented yet), it continues on to the + next algorithms below. If the routine proves that the integrals + is nonelementary, it still moves on to the algorithms below, + because we might be able to find a closed-form solution in terms + of special functions. If risch=True, however, it will stop here. + + 4. The Meijer G-Function algorithm: + + - This algorithm works by first rewriting the integrand in terms of + very general Meijer G-Function (meijerg in SymPy), integrating + it, and then rewriting the result back, if possible. This + algorithm is particularly powerful for definite integrals (which + is actually part of a different method of Integral), since it can + compute closed-form solutions of definite integrals even when no + closed-form indefinite integral exists. But it also is capable + of computing many indefinite integrals as well. + + - Another advantage of this method is that it can use some results + about the Meijer G-Function to give a result in terms of a + Piecewise expression, which allows to express conditionally + convergent integrals. + + - Setting meijerg=True will cause integrate() to use only this + method. + + 5. The "manual integration" algorithm: + + - This algorithm tries to mimic how a person would find an + antiderivative by hand, for example by looking for a + substitution or applying integration by parts. This algorithm + does not handle as many integrands but can return results in a + more familiar form. + + - Sometimes this algorithm can evaluate parts of an integral; in + this case integrate() will try to evaluate the rest of the + integrand using the other methods here. + + - Setting manual=True will cause integrate() to use only this + method. + + 6. The Heuristic Risch algorithm: + + - This is a heuristic version of the Risch algorithm, meaning that + it is not deterministic. This is tried as a last resort because + it can be very slow. It is still used because not enough of the + full Risch algorithm is implemented, so that there are still some + integrals that can only be computed using this method. The goal + is to implement enough of the Risch and Meijer G-function methods + so that this can be deleted. + + Setting heurisch=True will cause integrate() to use only this + method. Set heurisch=False to not use it. + + """ + + from sympy.integrals.risch import risch_integrate, NonElementaryIntegral + from sympy.integrals.manualintegrate import manualintegrate + + if risch: + try: + return risch_integrate(f, x, conds=conds) + except NotImplementedError: + return None + + if manual: + try: + result = manualintegrate(f, x) + if result is not None and result.func != Integral: + return result + except (ValueError, PolynomialError): + pass + + eval_kwargs = {"meijerg": meijerg, "risch": risch, "manual": manual, + "heurisch": heurisch, "conds": conds} + + # if it is a poly(x) then let the polynomial integrate itself (fast) + # + # It is important to make this check first, otherwise the other code + # will return a SymPy expression instead of a Polynomial. + # + # see Polynomial for details. + if isinstance(f, Poly) and not (manual or meijerg or risch): + # Note: this is deprecated, but the deprecation warning is already + # issued in the Integral constructor. + return f.integrate(x) + + # Piecewise antiderivatives need to call special integrate. + if isinstance(f, Piecewise): + return f.piecewise_integrate(x, **eval_kwargs) + + # let's cut it short if `f` does not depend on `x`; if + # x is only a dummy, that will be handled below + if not f.has(x): + return f*x + + # try to convert to poly(x) and then integrate if successful (fast) + poly = f.as_poly(x) + if poly is not None and not (manual or meijerg or risch): + return poly.integrate().as_expr() + + if risch is not False: + try: + result, i = risch_integrate(f, x, separate_integral=True, + conds=conds) + except NotImplementedError: + pass + else: + if i: + # There was a nonelementary integral. Try integrating it. + + # if no part of the NonElementaryIntegral is integrated by + # the Risch algorithm, then use the original function to + # integrate, instead of re-written one + if result == 0: + return NonElementaryIntegral(f, x).doit(risch=False) + else: + return result + i.doit(risch=False) + else: + return result + + # since Integral(f=g1+g2+...) == Integral(g1) + Integral(g2) + ... + # we are going to handle Add terms separately, + # if `f` is not Add -- we only have one term + + # Note that in general, this is a bad idea, because Integral(g1) + + # Integral(g2) might not be computable, even if Integral(g1 + g2) is. + # For example, Integral(x**x + x**x*log(x)). But many heuristics only + # work term-wise. So we compute this step last, after trying + # risch_integrate. We also try risch_integrate again in this loop, + # because maybe the integral is a sum of an elementary part and a + # nonelementary part (like erf(x) + exp(x)). risch_integrate() is + # quite fast, so this is acceptable. + from sympy.simplify.fu import sincos_to_sum + parts = [] + args = Add.make_args(f) + for g in args: + coeff, g = g.as_independent(x) + + # g(x) = const + if g is S.One and not meijerg: + parts.append(coeff*x) + continue + + # g(x) = expr + O(x**n) + order_term = g.getO() + + if order_term is not None: + h = self._eval_integral(g.removeO(), x, **eval_kwargs) + + if h is not None: + h_order_expr = self._eval_integral(order_term.expr, x, **eval_kwargs) + + if h_order_expr is not None: + h_order_term = order_term.func( + h_order_expr, *order_term.variables) + parts.append(coeff*(h + h_order_term)) + continue + + # NOTE: if there is O(x**n) and we fail to integrate then + # there is no point in trying other methods because they + # will fail, too. + return None + + # c + # g(x) = (a*x+b) + if g.is_Pow and not g.exp.has(x) and not meijerg: + a = Wild('a', exclude=[x]) + b = Wild('b', exclude=[x]) + + M = g.base.match(a*x + b) + + if M is not None: + if g.exp == -1: + h = log(g.base) + elif conds != 'piecewise': + h = g.base**(g.exp + 1) / (g.exp + 1) + else: + h1 = log(g.base) + h2 = g.base**(g.exp + 1) / (g.exp + 1) + h = Piecewise((h2, Ne(g.exp, -1)), (h1, True)) + + parts.append(coeff * h / M[a]) + continue + + # poly(x) + # g(x) = ------- + # poly(x) + if g.is_rational_function(x) and not (manual or meijerg or risch): + parts.append(coeff * ratint(g, x)) + continue + + if not (manual or meijerg or risch): + # g(x) = Mul(trig) + h = trigintegrate(g, x, conds=conds) + if h is not None: + parts.append(coeff * h) + continue + + # g(x) has at least a DiracDelta term + h = deltaintegrate(g, x) + if h is not None: + parts.append(coeff * h) + continue + + from .singularityfunctions import singularityintegrate + # g(x) has at least a Singularity Function term + h = singularityintegrate(g, x) + if h is not None: + parts.append(coeff * h) + continue + + # Try risch again. + if risch is not False: + try: + h, i = risch_integrate(g, x, + separate_integral=True, conds=conds) + except NotImplementedError: + h = None + else: + if i: + h = h + i.doit(risch=False) + + parts.append(coeff*h) + continue + + # fall back to heurisch + if heurisch is not False: + from sympy.integrals.heurisch import (heurisch as heurisch_, + heurisch_wrapper) + try: + if conds == 'piecewise': + h = heurisch_wrapper(g, x, hints=[]) + else: + h = heurisch_(g, x, hints=[]) + except PolynomialError: + # XXX: this exception means there is a bug in the + # implementation of heuristic Risch integration + # algorithm. + h = None + else: + h = None + + if meijerg is not False and h is None: + # rewrite using G functions + try: + h = meijerint_indefinite(g, x) + except NotImplementedError: + _debug('NotImplementedError from meijerint_definite') + if h is not None: + parts.append(coeff * h) + continue + + if h is None and manual is not False: + try: + result = manualintegrate(g, x) + if result is not None and not isinstance(result, Integral): + if result.has(Integral) and not manual: + # Try to have other algorithms do the integrals + # manualintegrate can't handle, + # unless we were asked to use manual only. + # Keep the rest of eval_kwargs in case another + # method was set to False already + new_eval_kwargs = eval_kwargs + new_eval_kwargs["manual"] = False + new_eval_kwargs["final"] = False + result = result.func(*[ + arg.doit(**new_eval_kwargs) if + arg.has(Integral) else arg + for arg in result.args + ]).expand(multinomial=False, + log=False, + power_exp=False, + power_base=False) + if not result.has(Integral): + parts.append(coeff * result) + continue + except (ValueError, PolynomialError): + # can't handle some SymPy expressions + pass + + # if we failed maybe it was because we had + # a product that could have been expanded, + # so let's try an expansion of the whole + # thing before giving up; we don't try this + # at the outset because there are things + # that cannot be solved unless they are + # NOT expanded e.g., x**x*(1+log(x)). There + # should probably be a checker somewhere in this + # routine to look for such cases and try to do + # collection on the expressions if they are already + # in an expanded form + if not h and len(args) == 1: + f = sincos_to_sum(f).expand(mul=True, deep=False) + if f.is_Add: + # Note: risch will be identical on the expanded + # expression, but maybe it will be able to pick out parts, + # like x*(exp(x) + erf(x)). + return self._eval_integral(f, x, **eval_kwargs) + + if h is not None: + parts.append(coeff * h) + else: + return None + + return Add(*parts) + + def _eval_lseries(self, x, logx=None, cdir=0): + expr = self.as_dummy() + symb = x + for l in expr.limits: + if x in l[1:]: + symb = l[0] + break + for term in expr.function.lseries(symb, logx): + yield integrate(term, *expr.limits) + + def _eval_nseries(self, x, n, logx=None, cdir=0): + symb = x + for l in self.limits: + if x in l[1:]: + symb = l[0] + break + terms, order = self.function.nseries( + x=symb, n=n, logx=logx).as_coeff_add(Order) + order = [o.subs(symb, x) for o in order] + return integrate(terms, *self.limits) + Add(*order)*x + + def _eval_as_leading_term(self, x, logx, cdir): + series_gen = self.args[0].lseries(x) + for leading_term in series_gen: + if leading_term != 0: + break + return integrate(leading_term, *self.args[1:]) + + def _eval_simplify(self, **kwargs): + expr = factor_terms(self) + if isinstance(expr, Integral): + from sympy.simplify.simplify import simplify + return expr.func(*[simplify(i, **kwargs) for i in expr.args]) + return expr.simplify(**kwargs) + + def as_sum(self, n=None, method="midpoint", evaluate=True): + """ + Approximates a definite integral by a sum. + + Parameters + ========== + + n : + The number of subintervals to use, optional. + method : + One of: 'left', 'right', 'midpoint', 'trapezoid'. + evaluate : bool + If False, returns an unevaluated Sum expression. The default + is True, evaluate the sum. + + Notes + ===== + + These methods of approximate integration are described in [1]. + + Examples + ======== + + >>> from sympy import Integral, sin, sqrt + >>> from sympy.abc import x, n + >>> e = Integral(sin(x), (x, 3, 7)) + >>> e + Integral(sin(x), (x, 3, 7)) + + For demonstration purposes, this interval will only be split into 2 + regions, bounded by [3, 5] and [5, 7]. + + The left-hand rule uses function evaluations at the left of each + interval: + + >>> e.as_sum(2, 'left') + 2*sin(5) + 2*sin(3) + + The midpoint rule uses evaluations at the center of each interval: + + >>> e.as_sum(2, 'midpoint') + 2*sin(4) + 2*sin(6) + + The right-hand rule uses function evaluations at the right of each + interval: + + >>> e.as_sum(2, 'right') + 2*sin(5) + 2*sin(7) + + The trapezoid rule uses function evaluations on both sides of the + intervals. This is equivalent to taking the average of the left and + right hand rule results: + + >>> s = e.as_sum(2, 'trapezoid') + >>> s + 2*sin(5) + sin(3) + sin(7) + >>> (e.as_sum(2, 'left') + e.as_sum(2, 'right'))/2 == s + True + + Here, the discontinuity at x = 0 can be avoided by using the + midpoint or right-hand method: + + >>> e = Integral(1/sqrt(x), (x, 0, 1)) + >>> e.as_sum(5).n(4) + 1.730 + >>> e.as_sum(10).n(4) + 1.809 + >>> e.doit().n(4) # the actual value is 2 + 2.000 + + The left- or trapezoid method will encounter the discontinuity and + return infinity: + + >>> e.as_sum(5, 'left') + zoo + + The number of intervals can be symbolic. If omitted, a dummy symbol + will be used for it. + + >>> e = Integral(x**2, (x, 0, 2)) + >>> e.as_sum(n, 'right').expand() + 8/3 + 4/n + 4/(3*n**2) + + This shows that the midpoint rule is more accurate, as its error + term decays as the square of n: + + >>> e.as_sum(method='midpoint').expand() + 8/3 - 2/(3*_n**2) + + A symbolic sum is returned with evaluate=False: + + >>> e.as_sum(n, 'midpoint', evaluate=False) + 2*Sum((2*_k/n - 1/n)**2, (_k, 1, n))/n + + See Also + ======== + + Integral.doit : Perform the integration using any hints + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Riemann_sum#Riemann_summation_methods + """ + + from sympy.concrete.summations import Sum + limits = self.limits + if len(limits) > 1: + raise NotImplementedError( + "Multidimensional midpoint rule not implemented yet") + else: + limit = limits[0] + if (len(limit) != 3 or limit[1].is_finite is False or + limit[2].is_finite is False): + raise ValueError("Expecting a definite integral over " + "a finite interval.") + if n is None: + n = Dummy('n', integer=True, positive=True) + else: + n = sympify(n) + if (n.is_positive is False or n.is_integer is False or + n.is_finite is False): + raise ValueError("n must be a positive integer, got %s" % n) + x, a, b = limit + dx = (b - a)/n + k = Dummy('k', integer=True, positive=True) + f = self.function + + if method == "left": + result = dx*Sum(f.subs(x, a + (k-1)*dx), (k, 1, n)) + elif method == "right": + result = dx*Sum(f.subs(x, a + k*dx), (k, 1, n)) + elif method == "midpoint": + result = dx*Sum(f.subs(x, a + k*dx - dx/2), (k, 1, n)) + elif method == "trapezoid": + result = dx*((f.subs(x, a) + f.subs(x, b))/2 + + Sum(f.subs(x, a + k*dx), (k, 1, n - 1))) + else: + raise ValueError("Unknown method %s" % method) + return result.doit() if evaluate else result + + def principal_value(self, **kwargs): + """ + Compute the Cauchy Principal Value of the definite integral of a real function in the given interval + on the real axis. + + Explanation + =========== + + In mathematics, the Cauchy principal value, is a method for assigning values to certain improper + integrals which would otherwise be undefined. + + Examples + ======== + + >>> from sympy import Integral, oo + >>> from sympy.abc import x + >>> Integral(x+1, (x, -oo, oo)).principal_value() + oo + >>> f = 1 / (x**3) + >>> Integral(f, (x, -oo, oo)).principal_value() + 0 + >>> Integral(f, (x, -10, 10)).principal_value() + 0 + >>> Integral(f, (x, -10, oo)).principal_value() + Integral(f, (x, -oo, 10)).principal_value() + 0 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Cauchy_principal_value + .. [2] https://mathworld.wolfram.com/CauchyPrincipalValue.html + """ + if len(self.limits) != 1 or len(list(self.limits[0])) != 3: + raise ValueError("You need to insert a variable, lower_limit, and upper_limit correctly to calculate " + "cauchy's principal value") + x, a, b = self.limits[0] + if not (a.is_comparable and b.is_comparable and a <= b): + raise ValueError("The lower_limit must be smaller than or equal to the upper_limit to calculate " + "cauchy's principal value. Also, a and b need to be comparable.") + if a == b: + return S.Zero + + from sympy.calculus.singularities import singularities + + r = Dummy('r') + f = self.function + singularities_list = [s for s in singularities(f, x) if s.is_comparable and a <= s <= b] + for i in singularities_list: + if i in (a, b): + raise ValueError( + 'The principal value is not defined in the given interval due to singularity at %d.' % (i)) + F = integrate(f, x, **kwargs) + if F.has(Integral): + return self + if a is -oo and b is oo: + I = limit(F - F.subs(x, -x), x, oo) + else: + I = limit(F, x, b, '-') - limit(F, x, a, '+') + for s in singularities_list: + I += limit(((F.subs(x, s - r)) - F.subs(x, s + r)), r, 0, '+') + return I + + + +def integrate(*args, meijerg=None, conds='piecewise', risch=None, heurisch=None, manual=None, **kwargs): + """integrate(f, var, ...) + + .. deprecated:: 1.6 + + Using ``integrate()`` with :class:`~.Poly` is deprecated. Use + :meth:`.Poly.integrate` instead. See :ref:`deprecated-integrate-poly`. + + Explanation + =========== + + Compute definite or indefinite integral of one or more variables + using Risch-Norman algorithm and table lookup. This procedure is + able to handle elementary algebraic and transcendental functions + and also a huge class of special functions, including Airy, + Bessel, Whittaker and Lambert. + + var can be: + + - a symbol -- indefinite integration + - a tuple (symbol, a) -- indefinite integration with result + given with ``a`` replacing ``symbol`` + - a tuple (symbol, a, b) -- definite integration + + Several variables can be specified, in which case the result is + multiple integration. (If var is omitted and the integrand is + univariate, the indefinite integral in that variable will be performed.) + + Indefinite integrals are returned without terms that are independent + of the integration variables. (see examples) + + Definite improper integrals often entail delicate convergence + conditions. Pass conds='piecewise', 'separate' or 'none' to have + these returned, respectively, as a Piecewise function, as a separate + result (i.e. result will be a tuple), or not at all (default is + 'piecewise'). + + **Strategy** + + SymPy uses various approaches to definite integration. One method is to + find an antiderivative for the integrand, and then use the fundamental + theorem of calculus. Various functions are implemented to integrate + polynomial, rational and trigonometric functions, and integrands + containing DiracDelta terms. + + SymPy also implements the part of the Risch algorithm, which is a decision + procedure for integrating elementary functions, i.e., the algorithm can + either find an elementary antiderivative, or prove that one does not + exist. There is also a (very successful, albeit somewhat slow) general + implementation of the heuristic Risch algorithm. This algorithm will + eventually be phased out as more of the full Risch algorithm is + implemented. See the docstring of Integral._eval_integral() for more + details on computing the antiderivative using algebraic methods. + + The option risch=True can be used to use only the (full) Risch algorithm. + This is useful if you want to know if an elementary function has an + elementary antiderivative. If the indefinite Integral returned by this + function is an instance of NonElementaryIntegral, that means that the + Risch algorithm has proven that integral to be non-elementary. Note that + by default, additional methods (such as the Meijer G method outlined + below) are tried on these integrals, as they may be expressible in terms + of special functions, so if you only care about elementary answers, use + risch=True. Also note that an unevaluated Integral returned by this + function is not necessarily a NonElementaryIntegral, even with risch=True, + as it may just be an indication that the particular part of the Risch + algorithm needed to integrate that function is not yet implemented. + + Another family of strategies comes from re-writing the integrand in + terms of so-called Meijer G-functions. Indefinite integrals of a + single G-function can always be computed, and the definite integral + of a product of two G-functions can be computed from zero to + infinity. Various strategies are implemented to rewrite integrands + as G-functions, and use this information to compute integrals (see + the ``meijerint`` module). + + The option manual=True can be used to use only an algorithm that tries + to mimic integration by hand. This algorithm does not handle as many + integrands as the other algorithms implemented but may return results in + a more familiar form. The ``manualintegrate`` module has functions that + return the steps used (see the module docstring for more information). + + In general, the algebraic methods work best for computing + antiderivatives of (possibly complicated) combinations of elementary + functions. The G-function methods work best for computing definite + integrals from zero to infinity of moderately complicated + combinations of special functions, or indefinite integrals of very + simple combinations of special functions. + + The strategy employed by the integration code is as follows: + + - If computing a definite integral, and both limits are real, + and at least one limit is +- oo, try the G-function method of + definite integration first. + + - Try to find an antiderivative, using all available methods, ordered + by performance (that is try fastest method first, slowest last; in + particular polynomial integration is tried first, Meijer + G-functions second to last, and heuristic Risch last). + + - If still not successful, try G-functions irrespective of the + limits. + + The option meijerg=True, False, None can be used to, respectively: + always use G-function methods and no others, never use G-function + methods, or use all available methods (in order as described above). + It defaults to None. + + Examples + ======== + + >>> from sympy import integrate, log, exp, oo + >>> from sympy.abc import a, x, y + + >>> integrate(x*y, x) + x**2*y/2 + + >>> integrate(log(x), x) + x*log(x) - x + + >>> integrate(log(x), (x, 1, a)) + a*log(a) - a + 1 + + >>> integrate(x) + x**2/2 + + Terms that are independent of x are dropped by indefinite integration: + + >>> from sympy import sqrt + >>> integrate(sqrt(1 + x), (x, 0, x)) + 2*(x + 1)**(3/2)/3 - 2/3 + >>> integrate(sqrt(1 + x), x) + 2*(x + 1)**(3/2)/3 + + >>> integrate(x*y) + Traceback (most recent call last): + ... + ValueError: specify integration variables to integrate x*y + + Note that ``integrate(x)`` syntax is meant only for convenience + in interactive sessions and should be avoided in library code. + + >>> integrate(x**a*exp(-x), (x, 0, oo)) # same as conds='piecewise' + Piecewise((gamma(a + 1), re(a) > -1), + (Integral(x**a*exp(-x), (x, 0, oo)), True)) + + >>> integrate(x**a*exp(-x), (x, 0, oo), conds='none') + gamma(a + 1) + + >>> integrate(x**a*exp(-x), (x, 0, oo), conds='separate') + (gamma(a + 1), re(a) > -1) + + See Also + ======== + + Integral, Integral.doit + + """ + doit_flags = { + 'deep': False, + 'meijerg': meijerg, + 'conds': conds, + 'risch': risch, + 'heurisch': heurisch, + 'manual': manual + } + + integral = Integral(*args, **kwargs) + + if isinstance(integral, Integral): + return integral.doit(**doit_flags) + else: + new_args = [a.doit(**doit_flags) if isinstance(a, Integral) else a + for a in integral.args] + return integral.func(*new_args) + +def line_integrate(field, curve, vars): + """line_integrate(field, Curve, variables) + + Compute the line integral. + + Examples + ======== + + >>> from sympy import Curve, line_integrate, E, ln + >>> from sympy.abc import x, y, t + >>> C = Curve([E**t + 1, E**t - 1], (t, 0, ln(2))) + >>> line_integrate(x + y, C, [x, y]) + 3*sqrt(2) + + See Also + ======== + + sympy.integrals.integrals.integrate, Integral + """ + from sympy.geometry import Curve + F = sympify(field) + if not F: + raise ValueError( + "Expecting function specifying field as first argument.") + if not isinstance(curve, Curve): + raise ValueError("Expecting Curve entity as second argument.") + if not is_sequence(vars): + raise ValueError("Expecting ordered iterable for variables.") + if len(curve.functions) != len(vars): + raise ValueError("Field variable size does not match curve dimension.") + + if curve.parameter in vars: + raise ValueError("Curve parameter clashes with field parameters.") + + # Calculate derivatives for line parameter functions + # F(r) -> F(r(t)) and finally F(r(t)*r'(t)) + Ft = F + dldt = 0 + for i, var in enumerate(vars): + _f = curve.functions[i] + _dn = diff(_f, curve.parameter) + # ...arc length + dldt = dldt + (_dn * _dn) + Ft = Ft.subs(var, _f) + Ft = Ft * sqrt(dldt) + + integral = Integral(Ft, curve.limits).doit(deep=False) + return integral + + +### Property function dispatching ### + +@shape.register(Integral) +def _(expr): + return shape(expr.function) + +# Delayed imports +from .deltafunctions import deltaintegrate +from .meijerint import meijerint_definite, meijerint_indefinite, _debug +from .trigonometry import trigintegrate diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/intpoly.py b/venv/lib/python3.11/site-packages/sympy/integrals/intpoly.py new file mode 100644 index 0000000000000000000000000000000000000000..38fd071183fb2192f4c1443d04c8f0ecfb6cc4ea --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/intpoly.py @@ -0,0 +1,1302 @@ +""" +Module to implement integration of uni/bivariate polynomials over +2D Polytopes and uni/bi/trivariate polynomials over 3D Polytopes. + +Uses evaluation techniques as described in Chin et al. (2015) [1]. + + +References +=========== + +.. [1] Chin, Eric B., Jean B. Lasserre, and N. Sukumar. "Numerical integration +of homogeneous functions on convex and nonconvex polygons and polyhedra." +Computational Mechanics 56.6 (2015): 967-981 + +PDF link : http://dilbert.engr.ucdavis.edu/~suku/quadrature/cls-integration.pdf +""" + +from functools import cmp_to_key + +from sympy.abc import x, y, z +from sympy.core import S, diff, Expr, Symbol +from sympy.core.sympify import _sympify +from sympy.geometry import Segment2D, Polygon, Point, Point2D +from sympy.polys.polytools import LC, gcd_list, degree_list, Poly +from sympy.simplify.simplify import nsimplify + + +def polytope_integrate(poly, expr=None, *, clockwise=False, max_degree=None): + """Integrates polynomials over 2/3-Polytopes. + + Explanation + =========== + + This function accepts the polytope in ``poly`` and the function in ``expr`` + (uni/bi/trivariate polynomials are implemented) and returns + the exact integral of ``expr`` over ``poly``. + + Parameters + ========== + + poly : The input Polygon. + + expr : The input polynomial. + + clockwise : Binary value to sort input points of 2-Polytope clockwise.(Optional) + + max_degree : The maximum degree of any monomial of the input polynomial.(Optional) + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import Point, Polygon + >>> from sympy.integrals.intpoly import polytope_integrate + >>> polygon = Polygon(Point(0, 0), Point(0, 1), Point(1, 1), Point(1, 0)) + >>> polys = [1, x, y, x*y, x**2*y, x*y**2] + >>> expr = x*y + >>> polytope_integrate(polygon, expr) + 1/4 + >>> polytope_integrate(polygon, polys, max_degree=3) + {1: 1, x: 1/2, y: 1/2, x*y: 1/4, x*y**2: 1/6, x**2*y: 1/6} + """ + if clockwise: + if isinstance(poly, Polygon): + poly = Polygon(*point_sort(poly.vertices), evaluate=False) + else: + raise TypeError("clockwise=True works for only 2-Polytope" + "V-representation input") + + if isinstance(poly, Polygon): + # For Vertex Representation(2D case) + hp_params = hyperplane_parameters(poly) + facets = poly.sides + elif len(poly[0]) == 2: + # For Hyperplane Representation(2D case) + plen = len(poly) + if len(poly[0][0]) == 2: + intersections = [intersection(poly[(i - 1) % plen], poly[i], + "plane2D") + for i in range(0, plen)] + hp_params = poly + lints = len(intersections) + facets = [Segment2D(intersections[i], + intersections[(i + 1) % lints]) + for i in range(lints)] + else: + raise NotImplementedError("Integration for H-representation 3D" + "case not implemented yet.") + else: + # For Vertex Representation(3D case) + vertices = poly[0] + facets = poly[1:] + hp_params = hyperplane_parameters(facets, vertices) + + if max_degree is None: + if expr is None: + raise TypeError('Input expression must be a valid SymPy expression') + return main_integrate3d(expr, facets, vertices, hp_params) + + if max_degree is not None: + result = {} + if expr is not None: + f_expr = [] + for e in expr: + _ = decompose(e) + if len(_) == 1 and not _.popitem()[0]: + f_expr.append(e) + elif Poly(e).total_degree() <= max_degree: + f_expr.append(e) + expr = f_expr + + if not isinstance(expr, list) and expr is not None: + raise TypeError('Input polynomials must be list of expressions') + + if len(hp_params[0][0]) == 3: + result_dict = main_integrate3d(0, facets, vertices, hp_params, + max_degree) + else: + result_dict = main_integrate(0, facets, hp_params, max_degree) + + if expr is None: + return result_dict + + for poly in expr: + poly = _sympify(poly) + if poly not in result: + if poly.is_zero: + result[S.Zero] = S.Zero + continue + integral_value = S.Zero + monoms = decompose(poly, separate=True) + for monom in monoms: + monom = nsimplify(monom) + coeff, m = strip(monom) + integral_value += result_dict[m] * coeff + result[poly] = integral_value + return result + + if expr is None: + raise TypeError('Input expression must be a valid SymPy expression') + + return main_integrate(expr, facets, hp_params) + + +def strip(monom): + if monom.is_zero: + return S.Zero, S.Zero + elif monom.is_number: + return monom, S.One + else: + coeff = LC(monom) + return coeff, monom / coeff + +def _polynomial_integrate(polynomials, facets, hp_params): + dims = (x, y) + dim_length = len(dims) + integral_value = S.Zero + for deg in polynomials: + poly_contribute = S.Zero + facet_count = 0 + for hp in hp_params: + value_over_boundary = integration_reduction(facets, + facet_count, + hp[0], hp[1], + polynomials[deg], + dims, deg) + poly_contribute += value_over_boundary * (hp[1] / norm(hp[0])) + facet_count += 1 + poly_contribute /= (dim_length + deg) + integral_value += poly_contribute + + return integral_value + + +def main_integrate3d(expr, facets, vertices, hp_params, max_degree=None): + """Function to translate the problem of integrating uni/bi/tri-variate + polynomials over a 3-Polytope to integrating over its faces. + This is done using Generalized Stokes' Theorem and Euler's Theorem. + + Parameters + ========== + + expr : + The input polynomial. + facets : + Faces of the 3-Polytope(expressed as indices of `vertices`). + vertices : + Vertices that constitute the Polytope. + hp_params : + Hyperplane Parameters of the facets. + max_degree : optional + Max degree of constituent monomial in given list of polynomial. + + Examples + ======== + + >>> from sympy.integrals.intpoly import main_integrate3d, \ + hyperplane_parameters + >>> cube = [[(0, 0, 0), (0, 0, 5), (0, 5, 0), (0, 5, 5), (5, 0, 0),\ + (5, 0, 5), (5, 5, 0), (5, 5, 5)],\ + [2, 6, 7, 3], [3, 7, 5, 1], [7, 6, 4, 5], [1, 5, 4, 0],\ + [3, 1, 0, 2], [0, 4, 6, 2]] + >>> vertices = cube[0] + >>> faces = cube[1:] + >>> hp_params = hyperplane_parameters(faces, vertices) + >>> main_integrate3d(1, faces, vertices, hp_params) + -125 + """ + result = {} + dims = (x, y, z) + dim_length = len(dims) + if max_degree: + grad_terms = gradient_terms(max_degree, 3) + flat_list = [term for z_terms in grad_terms + for x_term in z_terms + for term in x_term] + + for term in flat_list: + result[term[0]] = 0 + + for facet_count, hp in enumerate(hp_params): + a, b = hp[0], hp[1] + x0 = vertices[facets[facet_count][0]] + + for i, monom in enumerate(flat_list): + # Every monomial is a tuple : + # (term, x_degree, y_degree, z_degree, value over boundary) + expr, x_d, y_d, z_d, z_index, y_index, x_index, _ = monom + degree = x_d + y_d + z_d + if b.is_zero: + value_over_face = S.Zero + else: + value_over_face = \ + integration_reduction_dynamic(facets, facet_count, a, + b, expr, degree, dims, + x_index, y_index, + z_index, x0, grad_terms, + i, vertices, hp) + monom[7] = value_over_face + result[expr] += value_over_face * \ + (b / norm(a)) / (dim_length + x_d + y_d + z_d) + return result + else: + integral_value = S.Zero + polynomials = decompose(expr) + for deg in polynomials: + poly_contribute = S.Zero + facet_count = 0 + for i, facet in enumerate(facets): + hp = hp_params[i] + if hp[1].is_zero: + continue + pi = polygon_integrate(facet, hp, i, facets, vertices, expr, deg) + poly_contribute += pi *\ + (hp[1] / norm(tuple(hp[0]))) + facet_count += 1 + poly_contribute /= (dim_length + deg) + integral_value += poly_contribute + return integral_value + + +def main_integrate(expr, facets, hp_params, max_degree=None): + """Function to translate the problem of integrating univariate/bivariate + polynomials over a 2-Polytope to integrating over its boundary facets. + This is done using Generalized Stokes's Theorem and Euler's Theorem. + + Parameters + ========== + + expr : + The input polynomial. + facets : + Facets(Line Segments) of the 2-Polytope. + hp_params : + Hyperplane Parameters of the facets. + max_degree : optional + The maximum degree of any monomial of the input polynomial. + + >>> from sympy.abc import x, y + >>> from sympy.integrals.intpoly import main_integrate,\ + hyperplane_parameters + >>> from sympy import Point, Polygon + >>> triangle = Polygon(Point(0, 3), Point(5, 3), Point(1, 1)) + >>> facets = triangle.sides + >>> hp_params = hyperplane_parameters(triangle) + >>> main_integrate(x**2 + y**2, facets, hp_params) + 325/6 + """ + dims = (x, y) + dim_length = len(dims) + result = {} + + if max_degree: + grad_terms = [[0, 0, 0, 0]] + gradient_terms(max_degree) + + for facet_count, hp in enumerate(hp_params): + a, b = hp[0], hp[1] + x0 = facets[facet_count].points[0] + + for i, monom in enumerate(grad_terms): + # Every monomial is a tuple : + # (term, x_degree, y_degree, value over boundary) + m, x_d, y_d, _ = monom + value = result.get(m, None) + degree = S.Zero + if b.is_zero: + value_over_boundary = S.Zero + else: + degree = x_d + y_d + value_over_boundary = \ + integration_reduction_dynamic(facets, facet_count, a, + b, m, degree, dims, x_d, + y_d, max_degree, x0, + grad_terms, i) + monom[3] = value_over_boundary + if value is not None: + result[m] += value_over_boundary * \ + (b / norm(a)) / (dim_length + degree) + else: + result[m] = value_over_boundary * \ + (b / norm(a)) / (dim_length + degree) + return result + else: + if not isinstance(expr, list): + polynomials = decompose(expr) + return _polynomial_integrate(polynomials, facets, hp_params) + else: + return {e: _polynomial_integrate(decompose(e), facets, hp_params) for e in expr} + + +def polygon_integrate(facet, hp_param, index, facets, vertices, expr, degree): + """Helper function to integrate the input uni/bi/trivariate polynomial + over a certain face of the 3-Polytope. + + Parameters + ========== + + facet : + Particular face of the 3-Polytope over which ``expr`` is integrated. + index : + The index of ``facet`` in ``facets``. + facets : + Faces of the 3-Polytope(expressed as indices of `vertices`). + vertices : + Vertices that constitute the facet. + expr : + The input polynomial. + degree : + Degree of ``expr``. + + Examples + ======== + + >>> from sympy.integrals.intpoly import polygon_integrate + >>> cube = [[(0, 0, 0), (0, 0, 5), (0, 5, 0), (0, 5, 5), (5, 0, 0),\ + (5, 0, 5), (5, 5, 0), (5, 5, 5)],\ + [2, 6, 7, 3], [3, 7, 5, 1], [7, 6, 4, 5], [1, 5, 4, 0],\ + [3, 1, 0, 2], [0, 4, 6, 2]] + >>> facet = cube[1] + >>> facets = cube[1:] + >>> vertices = cube[0] + >>> polygon_integrate(facet, [(0, 1, 0), 5], 0, facets, vertices, 1, 0) + -25 + """ + expr = S(expr) + if expr.is_zero: + return S.Zero + result = S.Zero + x0 = vertices[facet[0]] + facet_len = len(facet) + for i, fac in enumerate(facet): + side = (vertices[fac], vertices[facet[(i + 1) % facet_len]]) + result += distance_to_side(x0, side, hp_param[0]) *\ + lineseg_integrate(facet, i, side, expr, degree) + if not expr.is_number: + expr = diff(expr, x) * x0[0] + diff(expr, y) * x0[1] +\ + diff(expr, z) * x0[2] + result += polygon_integrate(facet, hp_param, index, facets, vertices, + expr, degree - 1) + result /= (degree + 2) + return result + + +def distance_to_side(point, line_seg, A): + """Helper function to compute the signed distance between given 3D point + and a line segment. + + Parameters + ========== + + point : 3D Point + line_seg : Line Segment + + Examples + ======== + + >>> from sympy.integrals.intpoly import distance_to_side + >>> point = (0, 0, 0) + >>> distance_to_side(point, [(0, 0, 1), (0, 1, 0)], (1, 0, 0)) + -sqrt(2)/2 + """ + x1, x2 = line_seg + rev_normal = [-1 * S(i)/norm(A) for i in A] + vector = [x2[i] - x1[i] for i in range(0, 3)] + vector = [vector[i]/norm(vector) for i in range(0, 3)] + + n_side = cross_product((0, 0, 0), rev_normal, vector) + vectorx0 = [line_seg[0][i] - point[i] for i in range(0, 3)] + dot_product = sum(vectorx0[i] * n_side[i] for i in range(0, 3)) + + return dot_product + + +def lineseg_integrate(polygon, index, line_seg, expr, degree): + """Helper function to compute the line integral of ``expr`` over ``line_seg``. + + Parameters + =========== + + polygon : + Face of a 3-Polytope. + index : + Index of line_seg in polygon. + line_seg : + Line Segment. + + Examples + ======== + + >>> from sympy.integrals.intpoly import lineseg_integrate + >>> polygon = [(0, 5, 0), (5, 5, 0), (5, 5, 5), (0, 5, 5)] + >>> line_seg = [(0, 5, 0), (5, 5, 0)] + >>> lineseg_integrate(polygon, 0, line_seg, 1, 0) + 5 + """ + expr = _sympify(expr) + if expr.is_zero: + return S.Zero + result = S.Zero + x0 = line_seg[0] + distance = norm(tuple([line_seg[1][i] - line_seg[0][i] for i in + range(3)])) + if isinstance(expr, Expr): + expr_dict = {x: line_seg[1][0], + y: line_seg[1][1], + z: line_seg[1][2]} + result += distance * expr.subs(expr_dict) + else: + result += distance * expr + + expr = diff(expr, x) * x0[0] + diff(expr, y) * x0[1] +\ + diff(expr, z) * x0[2] + + result += lineseg_integrate(polygon, index, line_seg, expr, degree - 1) + result /= (degree + 1) + return result + + +def integration_reduction(facets, index, a, b, expr, dims, degree): + """Helper method for main_integrate. Returns the value of the input + expression evaluated over the polytope facet referenced by a given index. + + Parameters + =========== + + facets : + List of facets of the polytope. + index : + Index referencing the facet to integrate the expression over. + a : + Hyperplane parameter denoting direction. + b : + Hyperplane parameter denoting distance. + expr : + The expression to integrate over the facet. + dims : + List of symbols denoting axes. + degree : + Degree of the homogeneous polynomial. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy.integrals.intpoly import integration_reduction,\ + hyperplane_parameters + >>> from sympy import Point, Polygon + >>> triangle = Polygon(Point(0, 3), Point(5, 3), Point(1, 1)) + >>> facets = triangle.sides + >>> a, b = hyperplane_parameters(triangle)[0] + >>> integration_reduction(facets, 0, a, b, 1, (x, y), 0) + 5 + """ + expr = _sympify(expr) + if expr.is_zero: + return expr + + value = S.Zero + x0 = facets[index].points[0] + m = len(facets) + gens = (x, y) + + inner_product = diff(expr, gens[0]) * x0[0] + diff(expr, gens[1]) * x0[1] + + if inner_product != 0: + value += integration_reduction(facets, index, a, b, + inner_product, dims, degree - 1) + + value += left_integral2D(m, index, facets, x0, expr, gens) + + return value/(len(dims) + degree - 1) + + +def left_integral2D(m, index, facets, x0, expr, gens): + """Computes the left integral of Eq 10 in Chin et al. + For the 2D case, the integral is just an evaluation of the polynomial + at the intersection of two facets which is multiplied by the distance + between the first point of facet and that intersection. + + Parameters + ========== + + m : + No. of hyperplanes. + index : + Index of facet to find intersections with. + facets : + List of facets(Line Segments in 2D case). + x0 : + First point on facet referenced by index. + expr : + Input polynomial + gens : + Generators which generate the polynomial + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy.integrals.intpoly import left_integral2D + >>> from sympy import Point, Polygon + >>> triangle = Polygon(Point(0, 3), Point(5, 3), Point(1, 1)) + >>> facets = triangle.sides + >>> left_integral2D(3, 0, facets, facets[0].points[0], 1, (x, y)) + 5 + """ + value = S.Zero + for j in range(m): + intersect = () + if j in ((index - 1) % m, (index + 1) % m): + intersect = intersection(facets[index], facets[j], "segment2D") + if intersect: + distance_origin = norm(tuple(map(lambda x, y: x - y, + intersect, x0))) + if is_vertex(intersect): + if isinstance(expr, Expr): + if len(gens) == 3: + expr_dict = {gens[0]: intersect[0], + gens[1]: intersect[1], + gens[2]: intersect[2]} + else: + expr_dict = {gens[0]: intersect[0], + gens[1]: intersect[1]} + value += distance_origin * expr.subs(expr_dict) + else: + value += distance_origin * expr + return value + + +def integration_reduction_dynamic(facets, index, a, b, expr, degree, dims, + x_index, y_index, max_index, x0, + monomial_values, monom_index, vertices=None, + hp_param=None): + """The same integration_reduction function which uses a dynamic + programming approach to compute terms by using the values of the integral + of previously computed terms. + + Parameters + ========== + + facets : + Facets of the Polytope. + index : + Index of facet to find intersections with.(Used in left_integral()). + a, b : + Hyperplane parameters. + expr : + Input monomial. + degree : + Total degree of ``expr``. + dims : + Tuple denoting axes variables. + x_index : + Exponent of 'x' in ``expr``. + y_index : + Exponent of 'y' in ``expr``. + max_index : + Maximum exponent of any monomial in ``monomial_values``. + x0 : + First point on ``facets[index]``. + monomial_values : + List of monomial values constituting the polynomial. + monom_index : + Index of monomial whose integration is being found. + vertices : optional + Coordinates of vertices constituting the 3-Polytope. + hp_param : optional + Hyperplane Parameter of the face of the facets[index]. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy.integrals.intpoly import (integration_reduction_dynamic, \ + hyperplane_parameters) + >>> from sympy import Point, Polygon + >>> triangle = Polygon(Point(0, 3), Point(5, 3), Point(1, 1)) + >>> facets = triangle.sides + >>> a, b = hyperplane_parameters(triangle)[0] + >>> x0 = facets[0].points[0] + >>> monomial_values = [[0, 0, 0, 0], [1, 0, 0, 5],\ + [y, 0, 1, 15], [x, 1, 0, None]] + >>> integration_reduction_dynamic(facets, 0, a, b, x, 1, (x, y), 1, 0, 1,\ + x0, monomial_values, 3) + 25/2 + """ + value = S.Zero + m = len(facets) + + if expr == S.Zero: + return expr + + if len(dims) == 2: + if not expr.is_number: + _, x_degree, y_degree, _ = monomial_values[monom_index] + x_index = monom_index - max_index + \ + x_index - 2 if x_degree > 0 else 0 + y_index = monom_index - 1 if y_degree > 0 else 0 + x_value, y_value =\ + monomial_values[x_index][3], monomial_values[y_index][3] + + value += x_degree * x_value * x0[0] + y_degree * y_value * x0[1] + + value += left_integral2D(m, index, facets, x0, expr, dims) + else: + # For 3D use case the max_index contains the z_degree of the term + z_index = max_index + if not expr.is_number: + x_degree, y_degree, z_degree = y_index,\ + z_index - x_index - y_index, x_index + x_value = monomial_values[z_index - 1][y_index - 1][x_index][7]\ + if x_degree > 0 else 0 + y_value = monomial_values[z_index - 1][y_index][x_index][7]\ + if y_degree > 0 else 0 + z_value = monomial_values[z_index - 1][y_index][x_index - 1][7]\ + if z_degree > 0 else 0 + + value += x_degree * x_value * x0[0] + y_degree * y_value * x0[1] \ + + z_degree * z_value * x0[2] + + value += left_integral3D(facets, index, expr, + vertices, hp_param, degree) + return value / (len(dims) + degree - 1) + + +def left_integral3D(facets, index, expr, vertices, hp_param, degree): + """Computes the left integral of Eq 10 in Chin et al. + + Explanation + =========== + + For the 3D case, this is the sum of the integral values over constituting + line segments of the face (which is accessed by facets[index]) multiplied + by the distance between the first point of facet and that line segment. + + Parameters + ========== + + facets : + List of faces of the 3-Polytope. + index : + Index of face over which integral is to be calculated. + expr : + Input polynomial. + vertices : + List of vertices that constitute the 3-Polytope. + hp_param : + The hyperplane parameters of the face. + degree : + Degree of the ``expr``. + + Examples + ======== + + >>> from sympy.integrals.intpoly import left_integral3D + >>> cube = [[(0, 0, 0), (0, 0, 5), (0, 5, 0), (0, 5, 5), (5, 0, 0),\ + (5, 0, 5), (5, 5, 0), (5, 5, 5)],\ + [2, 6, 7, 3], [3, 7, 5, 1], [7, 6, 4, 5], [1, 5, 4, 0],\ + [3, 1, 0, 2], [0, 4, 6, 2]] + >>> facets = cube[1:] + >>> vertices = cube[0] + >>> left_integral3D(facets, 3, 1, vertices, ([0, -1, 0], -5), 0) + -50 + """ + value = S.Zero + facet = facets[index] + x0 = vertices[facet[0]] + facet_len = len(facet) + for i, fac in enumerate(facet): + side = (vertices[fac], vertices[facet[(i + 1) % facet_len]]) + value += distance_to_side(x0, side, hp_param[0]) * \ + lineseg_integrate(facet, i, side, expr, degree) + return value + + +def gradient_terms(binomial_power=0, no_of_gens=2): + """Returns a list of all the possible monomials between + 0 and y**binomial_power for 2D case and z**binomial_power + for 3D case. + + Parameters + ========== + + binomial_power : + Power upto which terms are generated. + no_of_gens : + Denotes whether terms are being generated for 2D or 3D case. + + Examples + ======== + + >>> from sympy.integrals.intpoly import gradient_terms + >>> gradient_terms(2) + [[1, 0, 0, 0], [y, 0, 1, 0], [y**2, 0, 2, 0], [x, 1, 0, 0], + [x*y, 1, 1, 0], [x**2, 2, 0, 0]] + >>> gradient_terms(2, 3) + [[[[1, 0, 0, 0, 0, 0, 0, 0]]], [[[y, 0, 1, 0, 1, 0, 0, 0], + [z, 0, 0, 1, 1, 0, 1, 0]], [[x, 1, 0, 0, 1, 1, 0, 0]]], + [[[y**2, 0, 2, 0, 2, 0, 0, 0], [y*z, 0, 1, 1, 2, 0, 1, 0], + [z**2, 0, 0, 2, 2, 0, 2, 0]], [[x*y, 1, 1, 0, 2, 1, 0, 0], + [x*z, 1, 0, 1, 2, 1, 1, 0]], [[x**2, 2, 0, 0, 2, 2, 0, 0]]]] + """ + if no_of_gens == 2: + count = 0 + terms = [None] * int((binomial_power ** 2 + 3 * binomial_power + 2) / 2) + for x_count in range(0, binomial_power + 1): + for y_count in range(0, binomial_power - x_count + 1): + terms[count] = [x**x_count*y**y_count, + x_count, y_count, 0] + count += 1 + else: + terms = [[[[x ** x_count * y ** y_count * + z ** (z_count - y_count - x_count), + x_count, y_count, z_count - y_count - x_count, + z_count, x_count, z_count - y_count - x_count, 0] + for y_count in range(z_count - x_count, -1, -1)] + for x_count in range(0, z_count + 1)] + for z_count in range(0, binomial_power + 1)] + return terms + + +def hyperplane_parameters(poly, vertices=None): + """A helper function to return the hyperplane parameters + of which the facets of the polytope are a part of. + + Parameters + ========== + + poly : + The input 2/3-Polytope. + vertices : + Vertex indices of 3-Polytope. + + Examples + ======== + + >>> from sympy import Point, Polygon + >>> from sympy.integrals.intpoly import hyperplane_parameters + >>> hyperplane_parameters(Polygon(Point(0, 3), Point(5, 3), Point(1, 1))) + [((0, 1), 3), ((1, -2), -1), ((-2, -1), -3)] + >>> cube = [[(0, 0, 0), (0, 0, 5), (0, 5, 0), (0, 5, 5), (5, 0, 0),\ + (5, 0, 5), (5, 5, 0), (5, 5, 5)],\ + [2, 6, 7, 3], [3, 7, 5, 1], [7, 6, 4, 5], [1, 5, 4, 0],\ + [3, 1, 0, 2], [0, 4, 6, 2]] + >>> hyperplane_parameters(cube[1:], cube[0]) + [([0, -1, 0], -5), ([0, 0, -1], -5), ([-1, 0, 0], -5), + ([0, 1, 0], 0), ([1, 0, 0], 0), ([0, 0, 1], 0)] + """ + if isinstance(poly, Polygon): + vertices = list(poly.vertices) + [poly.vertices[0]] # Close the polygon + params = [None] * (len(vertices) - 1) + + for i in range(len(vertices) - 1): + v1 = vertices[i] + v2 = vertices[i + 1] + + a1 = v1[1] - v2[1] + a2 = v2[0] - v1[0] + b = v2[0] * v1[1] - v2[1] * v1[0] + + factor = gcd_list([a1, a2, b]) + + b = S(b) / factor + a = (S(a1) / factor, S(a2) / factor) + params[i] = (a, b) + else: + params = [None] * len(poly) + for i, polygon in enumerate(poly): + v1, v2, v3 = [vertices[vertex] for vertex in polygon[:3]] + normal = cross_product(v1, v2, v3) + b = sum(normal[j] * v1[j] for j in range(0, 3)) + fac = gcd_list(normal) + if fac.is_zero: + fac = 1 + normal = [j / fac for j in normal] + b = b / fac + params[i] = (normal, b) + return params + + +def cross_product(v1, v2, v3): + """Returns the cross-product of vectors (v2 - v1) and (v3 - v1) + That is : (v2 - v1) X (v3 - v1) + """ + v2 = [v2[j] - v1[j] for j in range(0, 3)] + v3 = [v3[j] - v1[j] for j in range(0, 3)] + return [v3[2] * v2[1] - v3[1] * v2[2], + v3[0] * v2[2] - v3[2] * v2[0], + v3[1] * v2[0] - v3[0] * v2[1]] + + +def best_origin(a, b, lineseg, expr): + """Helper method for polytope_integrate. Currently not used in the main + algorithm. + + Explanation + =========== + + Returns a point on the lineseg whose vector inner product with the + divergence of `expr` yields an expression with the least maximum + total power. + + Parameters + ========== + + a : + Hyperplane parameter denoting direction. + b : + Hyperplane parameter denoting distance. + lineseg : + Line segment on which to find the origin. + expr : + The expression which determines the best point. + + Algorithm(currently works only for 2D use case) + =============================================== + + 1 > Firstly, check for edge cases. Here that would refer to vertical + or horizontal lines. + + 2 > If input expression is a polynomial containing more than one generator + then find out the total power of each of the generators. + + x**2 + 3 + x*y + x**4*y**5 ---> {x: 7, y: 6} + + If expression is a constant value then pick the first boundary point + of the line segment. + + 3 > First check if a point exists on the line segment where the value of + the highest power generator becomes 0. If not check if the value of + the next highest becomes 0. If none becomes 0 within line segment + constraints then pick the first boundary point of the line segment. + Actually, any point lying on the segment can be picked as best origin + in the last case. + + Examples + ======== + + >>> from sympy.integrals.intpoly import best_origin + >>> from sympy.abc import x, y + >>> from sympy import Point, Segment2D + >>> l = Segment2D(Point(0, 3), Point(1, 1)) + >>> expr = x**3*y**7 + >>> best_origin((2, 1), 3, l, expr) + (0, 3.0) + """ + a1, b1 = lineseg.points[0] + + def x_axis_cut(ls): + """Returns the point where the input line segment + intersects the x-axis. + + Parameters + ========== + + ls : + Line segment + """ + p, q = ls.points + if p.y.is_zero: + return tuple(p) + elif q.y.is_zero: + return tuple(q) + elif p.y/q.y < S.Zero: + return p.y * (p.x - q.x)/(q.y - p.y) + p.x, S.Zero + else: + return () + + def y_axis_cut(ls): + """Returns the point where the input line segment + intersects the y-axis. + + Parameters + ========== + + ls : + Line segment + """ + p, q = ls.points + if p.x.is_zero: + return tuple(p) + elif q.x.is_zero: + return tuple(q) + elif p.x/q.x < S.Zero: + return S.Zero, p.x * (p.y - q.y)/(q.x - p.x) + p.y + else: + return () + + gens = (x, y) + power_gens = {} + + for i in gens: + power_gens[i] = S.Zero + + if len(gens) > 1: + # Special case for vertical and horizontal lines + if len(gens) == 2: + if a[0] == 0: + if y_axis_cut(lineseg): + return S.Zero, b/a[1] + else: + return a1, b1 + elif a[1] == 0: + if x_axis_cut(lineseg): + return b/a[0], S.Zero + else: + return a1, b1 + + if isinstance(expr, Expr): # Find the sum total of power of each + if expr.is_Add: # generator and store in a dictionary. + for monomial in expr.args: + if monomial.is_Pow: + if monomial.args[0] in gens: + power_gens[monomial.args[0]] += monomial.args[1] + else: + for univariate in monomial.args: + term_type = len(univariate.args) + if term_type == 0 and univariate in gens: + power_gens[univariate] += 1 + elif term_type == 2 and univariate.args[0] in gens: + power_gens[univariate.args[0]] +=\ + univariate.args[1] + elif expr.is_Mul: + for term in expr.args: + term_type = len(term.args) + if term_type == 0 and term in gens: + power_gens[term] += 1 + elif term_type == 2 and term.args[0] in gens: + power_gens[term.args[0]] += term.args[1] + elif expr.is_Pow: + power_gens[expr.args[0]] = expr.args[1] + elif expr.is_Symbol: + power_gens[expr] += 1 + else: # If `expr` is a constant take first vertex of the line segment. + return a1, b1 + + # TODO : This part is quite hacky. Should be made more robust with + # TODO : respect to symbol names and scalable w.r.t higher dimensions. + power_gens = sorted(power_gens.items(), key=lambda k: str(k[0])) + if power_gens[0][1] >= power_gens[1][1]: + if y_axis_cut(lineseg): + x0 = (S.Zero, b / a[1]) + elif x_axis_cut(lineseg): + x0 = (b / a[0], S.Zero) + else: + x0 = (a1, b1) + else: + if x_axis_cut(lineseg): + x0 = (b/a[0], S.Zero) + elif y_axis_cut(lineseg): + x0 = (S.Zero, b/a[1]) + else: + x0 = (a1, b1) + else: + x0 = (b/a[0]) + return x0 + + +def decompose(expr, separate=False): + """Decomposes an input polynomial into homogeneous ones of + smaller or equal degree. + + Explanation + =========== + + Returns a dictionary with keys as the degree of the smaller + constituting polynomials. Values are the constituting polynomials. + + Parameters + ========== + + expr : Expr + Polynomial(SymPy expression). + separate : bool + If True then simply return a list of the constituent monomials + If not then break up the polynomial into constituent homogeneous + polynomials. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy.integrals.intpoly import decompose + >>> decompose(x**2 + x*y + x + y + x**3*y**2 + y**5) + {1: x + y, 2: x**2 + x*y, 5: x**3*y**2 + y**5} + >>> decompose(x**2 + x*y + x + y + x**3*y**2 + y**5, True) + {x, x**2, y, y**5, x*y, x**3*y**2} + """ + poly_dict = {} + + if isinstance(expr, Expr) and not expr.is_number: + if expr.is_Symbol: + poly_dict[1] = expr + elif expr.is_Add: + symbols = expr.atoms(Symbol) + degrees = [(sum(degree_list(monom, *symbols)), monom) + for monom in expr.args] + if separate: + return {monom[1] for monom in degrees} + else: + for monom in degrees: + degree, term = monom + if poly_dict.get(degree): + poly_dict[degree] += term + else: + poly_dict[degree] = term + elif expr.is_Pow: + _, degree = expr.args + poly_dict[degree] = expr + else: # Now expr can only be of `Mul` type + degree = 0 + for term in expr.args: + term_type = len(term.args) + if term_type == 0 and term.is_Symbol: + degree += 1 + elif term_type == 2: + degree += term.args[1] + poly_dict[degree] = expr + else: + poly_dict[0] = expr + + if separate: + return set(poly_dict.values()) + return poly_dict + + +def point_sort(poly, normal=None, clockwise=True): + """Returns the same polygon with points sorted in clockwise or + anti-clockwise order. + + Note that it's necessary for input points to be sorted in some order + (clockwise or anti-clockwise) for the integration algorithm to work. + As a convention algorithm has been implemented keeping clockwise + orientation in mind. + + Parameters + ========== + + poly: + 2D or 3D Polygon. + normal : optional + The normal of the plane which the 3-Polytope is a part of. + clockwise : bool, optional + Returns points sorted in clockwise order if True and + anti-clockwise if False. + + Examples + ======== + + >>> from sympy.integrals.intpoly import point_sort + >>> from sympy import Point + >>> point_sort([Point(0, 0), Point(1, 0), Point(1, 1)]) + [Point2D(1, 1), Point2D(1, 0), Point2D(0, 0)] + """ + pts = poly.vertices if isinstance(poly, Polygon) else poly + n = len(pts) + if n < 2: + return list(pts) + + order = S.One if clockwise else S.NegativeOne + dim = len(pts[0]) + if dim == 2: + center = Point(sum((vertex.x for vertex in pts)) / n, + sum((vertex.y for vertex in pts)) / n) + else: + center = Point(sum((vertex.x for vertex in pts)) / n, + sum((vertex.y for vertex in pts)) / n, + sum((vertex.z for vertex in pts)) / n) + + def compare(a, b): + if a.x - center.x >= S.Zero and b.x - center.x < S.Zero: + return -order + elif a.x - center.x < 0 and b.x - center.x >= 0: + return order + elif a.x - center.x == 0 and b.x - center.x == 0: + if a.y - center.y >= 0 or b.y - center.y >= 0: + return -order if a.y > b.y else order + return -order if b.y > a.y else order + + det = (a.x - center.x) * (b.y - center.y) -\ + (b.x - center.x) * (a.y - center.y) + if det < 0: + return -order + elif det > 0: + return order + + first = (a.x - center.x) * (a.x - center.x) +\ + (a.y - center.y) * (a.y - center.y) + second = (b.x - center.x) * (b.x - center.x) +\ + (b.y - center.y) * (b.y - center.y) + return -order if first > second else order + + def compare3d(a, b): + det = cross_product(center, a, b) + dot_product = sum(det[i] * normal[i] for i in range(0, 3)) + if dot_product < 0: + return -order + elif dot_product > 0: + return order + + return sorted(pts, key=cmp_to_key(compare if dim==2 else compare3d)) + + +def norm(point): + """Returns the Euclidean norm of a point from origin. + + Parameters + ========== + + point: + This denotes a point in the dimension_al spac_e. + + Examples + ======== + + >>> from sympy.integrals.intpoly import norm + >>> from sympy import Point + >>> norm(Point(2, 7)) + sqrt(53) + """ + half = S.Half + if isinstance(point, (list, tuple)): + return sum(coord ** 2 for coord in point) ** half + elif isinstance(point, Point): + if isinstance(point, Point2D): + return (point.x ** 2 + point.y ** 2) ** half + else: + return (point.x ** 2 + point.y ** 2 + point.z) ** half + elif isinstance(point, dict): + return sum(i**2 for i in point.values()) ** half + + +def intersection(geom_1, geom_2, intersection_type): + """Returns intersection between geometric objects. + + Explanation + =========== + + Note that this function is meant for use in integration_reduction and + at that point in the calling function the lines denoted by the segments + surely intersect within segment boundaries. Coincident lines are taken + to be non-intersecting. Also, the hyperplane intersection for 2D case is + also implemented. + + Parameters + ========== + + geom_1, geom_2: + The input line segments. + + Examples + ======== + + >>> from sympy.integrals.intpoly import intersection + >>> from sympy import Point, Segment2D + >>> l1 = Segment2D(Point(1, 1), Point(3, 5)) + >>> l2 = Segment2D(Point(2, 0), Point(2, 5)) + >>> intersection(l1, l2, "segment2D") + (2, 3) + >>> p1 = ((-1, 0), 0) + >>> p2 = ((0, 1), 1) + >>> intersection(p1, p2, "plane2D") + (0, 1) + """ + if intersection_type[:-2] == "segment": + if intersection_type == "segment2D": + x1, y1 = geom_1.points[0] + x2, y2 = geom_1.points[1] + x3, y3 = geom_2.points[0] + x4, y4 = geom_2.points[1] + elif intersection_type == "segment3D": + x1, y1, z1 = geom_1.points[0] + x2, y2, z2 = geom_1.points[1] + x3, y3, z3 = geom_2.points[0] + x4, y4, z4 = geom_2.points[1] + + denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) + if denom: + t1 = x1 * y2 - y1 * x2 + t2 = x3 * y4 - x4 * y3 + return (S(t1 * (x3 - x4) - t2 * (x1 - x2)) / denom, + S(t1 * (y3 - y4) - t2 * (y1 - y2)) / denom) + if intersection_type[:-2] == "plane": + if intersection_type == "plane2D": # Intersection of hyperplanes + a1x, a1y = geom_1[0] + a2x, a2y = geom_2[0] + b1, b2 = geom_1[1], geom_2[1] + + denom = a1x * a2y - a2x * a1y + if denom: + return (S(b1 * a2y - b2 * a1y) / denom, + S(b2 * a1x - b1 * a2x) / denom) + + +def is_vertex(ent): + """If the input entity is a vertex return True. + + Parameter + ========= + + ent : + Denotes a geometric entity representing a point. + + Examples + ======== + + >>> from sympy import Point + >>> from sympy.integrals.intpoly import is_vertex + >>> is_vertex((2, 3)) + True + >>> is_vertex((2, 3, 6)) + True + >>> is_vertex(Point(2, 3)) + True + """ + if isinstance(ent, tuple): + if len(ent) in [2, 3]: + return True + elif isinstance(ent, Point): + return True + return False + + +def plot_polytope(poly): + """Plots the 2D polytope using the functions written in plotting + module which in turn uses matplotlib backend. + + Parameter + ========= + + poly: + Denotes a 2-Polytope. + """ + from sympy.plotting.plot import Plot, List2DSeries + + xl = [vertex.x for vertex in poly.vertices] + yl = [vertex.y for vertex in poly.vertices] + + xl.append(poly.vertices[0].x) # Closing the polygon + yl.append(poly.vertices[0].y) + + l2ds = List2DSeries(xl, yl) + p = Plot(l2ds, axes='label_axes=True') + p.show() + + +def plot_polynomial(expr): + """Plots the polynomial using the functions written in + plotting module which in turn uses matplotlib backend. + + Parameter + ========= + + expr: + Denotes a polynomial(SymPy expression). + """ + from sympy.plotting.plot import plot3d, plot + gens = expr.free_symbols + if len(gens) == 2: + plot3d(expr) + else: + plot(expr) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/laplace.py b/venv/lib/python3.11/site-packages/sympy/integrals/laplace.py new file mode 100644 index 0000000000000000000000000000000000000000..604c4a2711440f13c62f0d778e382e4daaf33148 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/laplace.py @@ -0,0 +1,2377 @@ +"""Laplace Transforms""" +import sys +import sympy +from sympy.core import S, pi, I +from sympy.core.add import Add +from sympy.core.cache import cacheit +from sympy.core.expr import Expr +from sympy.core.function import ( + AppliedUndef, Derivative, expand, expand_complex, expand_mul, expand_trig, + Lambda, WildFunction, diff, Subs) +from sympy.core.mul import Mul, prod +from sympy.core.relational import ( + _canonical, Ge, Gt, Lt, Unequality, Eq, Ne, Relational) +from sympy.core.sorting import ordered +from sympy.core.symbol import Dummy, symbols, Wild +from sympy.functions.elementary.complexes import ( + re, im, arg, Abs, polar_lift, periodic_argument) +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.hyperbolic import cosh, coth, sinh, asinh +from sympy.functions.elementary.miscellaneous import Max, Min, sqrt +from sympy.functions.elementary.piecewise import ( + Piecewise, piecewise_exclusive) +from sympy.functions.elementary.trigonometric import cos, sin, atan, sinc +from sympy.functions.special.bessel import besseli, besselj, besselk, bessely +from sympy.functions.special.delta_functions import DiracDelta, Heaviside +from sympy.functions.special.error_functions import erf, erfc, Ei +from sympy.functions.special.gamma_functions import ( + digamma, gamma, lowergamma, uppergamma) +from sympy.functions.special.singularity_functions import SingularityFunction +from sympy.integrals import integrate, Integral +from sympy.integrals.transforms import ( + _simplify, IntegralTransform, IntegralTransformError) +from sympy.logic.boolalg import to_cnf, conjuncts, disjuncts, Or, And +from sympy.matrices.matrixbase import MatrixBase +from sympy.polys.matrices.linsolve import _lin_eq2dict +from sympy.polys.polyerrors import PolynomialError +from sympy.polys.polyroots import roots +from sympy.polys.polytools import Poly +from sympy.polys.rationaltools import together +from sympy.polys.rootoftools import RootSum +from sympy.utilities.exceptions import ( + sympy_deprecation_warning, SymPyDeprecationWarning, ignore_warnings) +from sympy.utilities.misc import debugf + +_LT_level = 0 + + +def DEBUG_WRAP(func): + def wrap(*args, **kwargs): + from sympy import SYMPY_DEBUG + global _LT_level + + if not SYMPY_DEBUG: + return func(*args, **kwargs) + + if _LT_level == 0: + print('\n' + '-'*78, file=sys.stderr) + print('-LT- %s%s%s' % (' '*_LT_level, func.__name__, args), + file=sys.stderr) + _LT_level += 1 + if ( + func.__name__ == '_laplace_transform_integration' or + func.__name__ == '_inverse_laplace_transform_integration'): + sympy.SYMPY_DEBUG = False + print('**** %sIntegrating ...' % (' '*_LT_level), file=sys.stderr) + result = func(*args, **kwargs) + sympy.SYMPY_DEBUG = True + else: + result = func(*args, **kwargs) + _LT_level -= 1 + print('-LT- %s---> %s' % (' '*_LT_level, result), file=sys.stderr) + if _LT_level == 0: + print('-'*78 + '\n', file=sys.stderr) + return result + return wrap + + +def _debug(text): + from sympy import SYMPY_DEBUG + + if SYMPY_DEBUG: + print('-LT- %s%s' % (' '*_LT_level, text), file=sys.stderr) + + +def _simplifyconds(expr, s, a): + r""" + Naively simplify some conditions occurring in ``expr``, + given that `\operatorname{Re}(s) > a`. + + Examples + ======== + + >>> from sympy.integrals.laplace import _simplifyconds + >>> from sympy.abc import x + >>> from sympy import sympify as S + >>> _simplifyconds(abs(x**2) < 1, x, 1) + False + >>> _simplifyconds(abs(x**2) < 1, x, 2) + False + >>> _simplifyconds(abs(x**2) < 1, x, 0) + Abs(x**2) < 1 + >>> _simplifyconds(abs(1/x**2) < 1, x, 1) + True + >>> _simplifyconds(S(1) < abs(x), x, 1) + True + >>> _simplifyconds(S(1) < abs(1/x), x, 1) + False + + >>> from sympy import Ne + >>> _simplifyconds(Ne(1, x**3), x, 1) + True + >>> _simplifyconds(Ne(1, x**3), x, 2) + True + >>> _simplifyconds(Ne(1, x**3), x, 0) + Ne(1, x**3) + """ + + def power(ex): + if ex == s: + return 1 + if ex.is_Pow and ex.base == s: + return ex.exp + return None + + def bigger(ex1, ex2): + """ Return True only if |ex1| > |ex2|, False only if |ex1| < |ex2|. + Else return None. """ + if ex1.has(s) and ex2.has(s): + return None + if isinstance(ex1, Abs): + ex1 = ex1.args[0] + if isinstance(ex2, Abs): + ex2 = ex2.args[0] + if ex1.has(s): + return bigger(1/ex2, 1/ex1) + n = power(ex2) + if n is None: + return None + try: + if n > 0 and (Abs(ex1) <= Abs(a)**n) == S.true: + return False + if n < 0 and (Abs(ex1) >= Abs(a)**n) == S.true: + return True + except TypeError: + return None + + def replie(x, y): + """ simplify x < y """ + if (not (x.is_positive or isinstance(x, Abs)) + or not (y.is_positive or isinstance(y, Abs))): + return (x < y) + r = bigger(x, y) + if r is not None: + return not r + return (x < y) + + def replue(x, y): + b = bigger(x, y) + if b in (True, False): + return True + return Unequality(x, y) + + def repl(ex, *args): + if ex in (True, False): + return bool(ex) + return ex.replace(*args) + + from sympy.simplify.radsimp import collect_abs + expr = collect_abs(expr) + expr = repl(expr, Lt, replie) + expr = repl(expr, Gt, lambda x, y: replie(y, x)) + expr = repl(expr, Unequality, replue) + return S(expr) + + +@DEBUG_WRAP +def expand_dirac_delta(expr): + """ + Expand an expression involving DiractDelta to get it as a linear + combination of DiracDelta functions. + """ + return _lin_eq2dict(expr, expr.atoms(DiracDelta)) + + +@DEBUG_WRAP +def _laplace_transform_integration(f, t, s_, *, simplify): + """ The backend function for doing Laplace transforms by integration. + + This backend assumes that the frontend has already split sums + such that `f` is to an addition anymore. + """ + s = Dummy('s') + + if f.has(DiracDelta): + return None + + F = integrate(f*exp(-s*t), (t, S.Zero, S.Infinity)) + + if not F.has(Integral): + return _simplify(F.subs(s, s_), simplify), S.NegativeInfinity, S.true + + if not F.is_Piecewise: + return None + + F, cond = F.args[0] + if F.has(Integral): + return None + + def process_conds(conds): + """ Turn ``conds`` into a strip and auxiliary conditions. """ + from sympy.solvers.inequalities import _solve_inequality + a = S.NegativeInfinity + aux = S.true + conds = conjuncts(to_cnf(conds)) + p, q, w1, w2, w3, w4, w5 = symbols( + 'p q w1 w2 w3 w4 w5', cls=Wild, exclude=[s]) + patterns = ( + p*Abs(arg((s + w3)*q)) < w2, + p*Abs(arg((s + w3)*q)) <= w2, + Abs(periodic_argument((s + w3)**p*q, w1)) < w2, + Abs(periodic_argument((s + w3)**p*q, w1)) <= w2, + Abs(periodic_argument((polar_lift(s + w3))**p*q, w1)) < w2, + Abs(periodic_argument((polar_lift(s + w3))**p*q, w1)) <= w2) + for c in conds: + a_ = S.Infinity + aux_ = [] + for d in disjuncts(c): + if d.is_Relational and s in d.rhs.free_symbols: + d = d.reversed + if d.is_Relational and isinstance(d, (Ge, Gt)): + d = d.reversedsign + for pat in patterns: + m = d.match(pat) + if m: + break + if m and m[q].is_positive and m[w2]/m[p] == pi/2: + d = -re(s + m[w3]) < 0 + m = d.match(p - cos(w1*Abs(arg(s*w5))*w2)*Abs(s**w3)**w4 < 0) + if not m: + m = d.match( + cos(p - Abs(periodic_argument(s**w1*w5, q))*w2) * + Abs(s**w3)**w4 < 0) + if not m: + m = d.match( + p - cos( + Abs(periodic_argument(polar_lift(s)**w1*w5, q))*w2 + )*Abs(s**w3)**w4 < 0) + if m and all(m[wild].is_positive for wild in [ + w1, w2, w3, w4, w5]): + d = re(s) > m[p] + d_ = d.replace( + re, lambda x: x.expand().as_real_imag()[0]).subs(re(s), t) + if ( + not d.is_Relational or d.rel_op in ('==', '!=') + or d_.has(s) or not d_.has(t)): + aux_ += [d] + continue + soln = _solve_inequality(d_, t) + if not soln.is_Relational or soln.rel_op in ('==', '!='): + aux_ += [d] + continue + if soln.lts == t: + return None + else: + a_ = Min(soln.lts, a_) + if a_ is not S.Infinity: + a = Max(a_, a) + else: + aux = And(aux, Or(*aux_)) + return a, aux.canonical if aux.is_Relational else aux + + conds = [process_conds(c) for c in disjuncts(cond)] + conds2 = [x for x in conds if x[1] != + S.false and x[0] is not S.NegativeInfinity] + if not conds2: + conds2 = [x for x in conds if x[1] != S.false] + conds = list(ordered(conds2)) + + def cnt(expr): + if expr in (True, False): + return 0 + return expr.count_ops() + conds.sort(key=lambda x: (-x[0], cnt(x[1]))) + + if not conds: + return None + a, aux = conds[0] # XXX is [0] always the right one? + + def sbs(expr): + return expr.subs(s, s_) + if simplify: + F = _simplifyconds(F, s, a) + aux = _simplifyconds(aux, s, a) + return _simplify(F.subs(s, s_), simplify), sbs(a), _canonical(sbs(aux)) + + +@DEBUG_WRAP +def _laplace_deep_collect(f, t): + """ + This is an internal helper function that traverses through the expression + tree of `f(t)` and collects arguments. The purpose of it is that + anything like `f(w*t-1*t-c)` will be written as `f((w-1)*t-c)` such that + it can match `f(a*t+b)`. + """ + if not isinstance(f, Expr): + return f + if (p := f.as_poly(t)) is not None: + return p.as_expr() + func = f.func + args = [_laplace_deep_collect(arg, t) for arg in f.args] + return func(*args) + + +@cacheit +def _laplace_build_rules(): + """ + This is an internal helper function that returns the table of Laplace + transform rules in terms of the time variable `t` and the frequency + variable `s`. It is used by ``_laplace_apply_rules``. Each entry is a + tuple containing: + + (time domain pattern, + frequency-domain replacement, + condition for the rule to be applied, + convergence plane, + preparation function) + + The preparation function is a function with one argument that is applied + to the expression before matching. For most rules it should be + ``_laplace_deep_collect``. + """ + t = Dummy('t') + s = Dummy('s') + a = Wild('a', exclude=[t]) + b = Wild('b', exclude=[t]) + n = Wild('n', exclude=[t]) + tau = Wild('tau', exclude=[t]) + omega = Wild('omega', exclude=[t]) + def dco(f): return _laplace_deep_collect(f, t) + _debug('_laplace_build_rules is building rules') + + laplace_transform_rules = [ + (a, a/s, + S.true, S.Zero, dco), # 4.2.1 + (DiracDelta(a*t-b), exp(-s*b/a)/Abs(a), + Or(And(a > 0, b >= 0), And(a < 0, b <= 0)), + S.NegativeInfinity, dco), # Not in Bateman54 + (DiracDelta(a*t-b), S(0), + Or(And(a < 0, b >= 0), And(a > 0, b <= 0)), + S.NegativeInfinity, dco), # Not in Bateman54 + (Heaviside(a*t-b), exp(-s*b/a)/s, + And(a > 0, b > 0), S.Zero, dco), # 4.4.1 + (Heaviside(a*t-b), (1-exp(-s*b/a))/s, + And(a < 0, b < 0), S.Zero, dco), # 4.4.1 + (Heaviside(a*t-b), 1/s, + And(a > 0, b <= 0), S.Zero, dco), # 4.4.1 + (Heaviside(a*t-b), 0, + And(a < 0, b > 0), S.Zero, dco), # 4.4.1 + (t, 1/s**2, + S.true, S.Zero, dco), # 4.2.3 + (1/(a*t+b), -exp(-b/a*s)*Ei(-b/a*s)/a, + Abs(arg(b/a)) < pi, S.Zero, dco), # 4.2.6 + (1/sqrt(a*t+b), sqrt(a*pi/s)*exp(b/a*s)*erfc(sqrt(b/a*s))/a, + Abs(arg(b/a)) < pi, S.Zero, dco), # 4.2.18 + ((a*t+b)**(-S(3)/2), + 2*b**(-S(1)/2)-2*(pi*s/a)**(S(1)/2)*exp(b/a*s) * erfc(sqrt(b/a*s))/a, + Abs(arg(b/a)) < pi, S.Zero, dco), # 4.2.20 + (sqrt(t)/(t+b), sqrt(pi/s)-pi*sqrt(b)*exp(b*s)*erfc(sqrt(b*s)), + Abs(arg(b)) < pi, S.Zero, dco), # 4.2.22 + (1/(a*sqrt(t) + t**(3/2)), pi*a**(S(1)/2)*exp(a*s)*erfc(sqrt(a*s)), + S.true, S.Zero, dco), # Not in Bateman54 + (t**n, gamma(n+1)/s**(n+1), + n > -1, S.Zero, dco), # 4.3.1 + ((a*t+b)**n, uppergamma(n+1, b/a*s)*exp(-b/a*s)/s**(n+1)/a, + And(n > -1, Abs(arg(b/a)) < pi), S.Zero, dco), # 4.3.4 + (t**n/(t+a), a**n*gamma(n+1)*uppergamma(-n, a*s), + And(n > -1, Abs(arg(a)) < pi), S.Zero, dco), # 4.3.7 + (exp(a*t-tau), exp(-tau)/(s-a), + S.true, re(a), dco), # 4.5.1 + (t*exp(a*t-tau), exp(-tau)/(s-a)**2, + S.true, re(a), dco), # 4.5.2 + (t**n*exp(a*t), gamma(n+1)/(s-a)**(n+1), + re(n) > -1, re(a), dco), # 4.5.3 + (exp(-a*t**2), sqrt(pi/4/a)*exp(s**2/4/a)*erfc(s/sqrt(4*a)), + re(a) > 0, S.Zero, dco), # 4.5.21 + (t*exp(-a*t**2), + 1/(2*a)-2/sqrt(pi)/(4*a)**(S(3)/2)*s*erfc(s/sqrt(4*a)), + re(a) > 0, S.Zero, dco), # 4.5.22 + (exp(-a/t), 2*sqrt(a/s)*besselk(1, 2*sqrt(a*s)), + re(a) >= 0, S.Zero, dco), # 4.5.25 + (sqrt(t)*exp(-a/t), + S(1)/2*sqrt(pi/s**3)*(1+2*sqrt(a*s))*exp(-2*sqrt(a*s)), + re(a) >= 0, S.Zero, dco), # 4.5.26 + (exp(-a/t)/sqrt(t), sqrt(pi/s)*exp(-2*sqrt(a*s)), + re(a) >= 0, S.Zero, dco), # 4.5.27 + (exp(-a/t)/(t*sqrt(t)), sqrt(pi/a)*exp(-2*sqrt(a*s)), + re(a) > 0, S.Zero, dco), # 4.5.28 + (t**n*exp(-a/t), 2*(a/s)**((n+1)/2)*besselk(n+1, 2*sqrt(a*s)), + re(a) > 0, S.Zero, dco), # 4.5.29 + # TODO: rules with sqrt(a*t) and sqrt(a/t) have stopped working after + # changes to as_base_exp + # (exp(-2*sqrt(a*t)), + # s**(-1)-sqrt(pi*a)*s**(-S(3)/2)*exp(a/s) * erfc(sqrt(a/s)), + # Abs(arg(a)) < pi, S.Zero, dco), # 4.5.31 + # (exp(-2*sqrt(a*t))/sqrt(t), (pi/s)**(S(1)/2)*exp(a/s)*erfc(sqrt(a/s)), + # Abs(arg(a)) < pi, S.Zero, dco), # 4.5.33 + (exp(-a*exp(-t)), a**(-s)*lowergamma(s, a), + S.true, S.Zero, dco), # 4.5.36 + (exp(-a*exp(t)), a**s*uppergamma(-s, a), + re(a) > 0, S.Zero, dco), # 4.5.37 + (log(a*t), -log(exp(S.EulerGamma)*s/a)/s, + a > 0, S.Zero, dco), # 4.6.1 + (log(1+a*t), -exp(s/a)/s*Ei(-s/a), + Abs(arg(a)) < pi, S.Zero, dco), # 4.6.4 + (log(a*t+b), (log(b)-exp(s/b/a)/s*a*Ei(-s/b))/s*a, + And(a > 0, Abs(arg(b)) < pi), S.Zero, dco), # 4.6.5 + (log(t)/sqrt(t), -sqrt(pi/s)*log(4*s*exp(S.EulerGamma)), + S.true, S.Zero, dco), # 4.6.9 + (t**n*log(t), gamma(n+1)*s**(-n-1)*(digamma(n+1)-log(s)), + re(n) > -1, S.Zero, dco), # 4.6.11 + (log(a*t)**2, (log(exp(S.EulerGamma)*s/a)**2+pi**2/6)/s, + a > 0, S.Zero, dco), # 4.6.13 + (sin(omega*t), omega/(s**2+omega**2), + S.true, Abs(im(omega)), dco), # 4,7,1 + (Abs(sin(omega*t)), omega/(s**2+omega**2)*coth(pi*s/2/omega), + omega > 0, S.Zero, dco), # 4.7.2 + (sin(omega*t)/t, atan(omega/s), + S.true, Abs(im(omega)), dco), # 4.7.16 + (sin(omega*t)**2/t, log(1+4*omega**2/s**2)/4, + S.true, 2*Abs(im(omega)), dco), # 4.7.17 + (sin(omega*t)**2/t**2, + omega*atan(2*omega/s)-s*log(1+4*omega**2/s**2)/4, + S.true, 2*Abs(im(omega)), dco), # 4.7.20 + # (sin(2*sqrt(a*t)), sqrt(pi*a)/s/sqrt(s)*exp(-a/s), + # S.true, S.Zero, dco), # 4.7.32 + # (sin(2*sqrt(a*t))/t, pi*erf(sqrt(a/s)), + # S.true, S.Zero, dco), # 4.7.34 + (cos(omega*t), s/(s**2+omega**2), + S.true, Abs(im(omega)), dco), # 4.7.43 + (cos(omega*t)**2, (s**2+2*omega**2)/(s**2+4*omega**2)/s, + S.true, 2*Abs(im(omega)), dco), # 4.7.45 + # (sqrt(t)*cos(2*sqrt(a*t)), sqrt(pi)/2*s**(-S(5)/2)*(s-2*a)*exp(-a/s), + # S.true, S.Zero, dco), # 4.7.66 + # (cos(2*sqrt(a*t))/sqrt(t), sqrt(pi/s)*exp(-a/s), + # S.true, S.Zero, dco), # 4.7.67 + (sin(a*t)*sin(b*t), 2*a*b*s/(s**2+(a+b)**2)/(s**2+(a-b)**2), + S.true, Abs(im(a))+Abs(im(b)), dco), # 4.7.78 + (cos(a*t)*sin(b*t), b*(s**2-a**2+b**2)/(s**2+(a+b)**2)/(s**2+(a-b)**2), + S.true, Abs(im(a))+Abs(im(b)), dco), # 4.7.79 + (cos(a*t)*cos(b*t), s*(s**2+a**2+b**2)/(s**2+(a+b)**2)/(s**2+(a-b)**2), + S.true, Abs(im(a))+Abs(im(b)), dco), # 4.7.80 + (sinh(a*t), a/(s**2-a**2), + S.true, Abs(re(a)), dco), # 4.9.1 + (cosh(a*t), s/(s**2-a**2), + S.true, Abs(re(a)), dco), # 4.9.2 + (sinh(a*t)**2, 2*a**2/(s**3-4*a**2*s), + S.true, 2*Abs(re(a)), dco), # 4.9.3 + (cosh(a*t)**2, (s**2-2*a**2)/(s**3-4*a**2*s), + S.true, 2*Abs(re(a)), dco), # 4.9.4 + (sinh(a*t)/t, log((s+a)/(s-a))/2, + S.true, Abs(re(a)), dco), # 4.9.12 + (t**n*sinh(a*t), gamma(n+1)/2*((s-a)**(-n-1)-(s+a)**(-n-1)), + n > -2, Abs(a), dco), # 4.9.18 + (t**n*cosh(a*t), gamma(n+1)/2*((s-a)**(-n-1)+(s+a)**(-n-1)), + n > -1, Abs(a), dco), # 4.9.19 + # TODO + # (sinh(2*sqrt(a*t)), sqrt(pi*a)/s/sqrt(s)*exp(a/s), + # S.true, S.Zero, dco), # 4.9.34 + # (cosh(2*sqrt(a*t)), 1/s+sqrt(pi*a)/s/sqrt(s)*exp(a/s)*erf(sqrt(a/s)), + # S.true, S.Zero, dco), # 4.9.35 + # ( + # sqrt(t)*sinh(2*sqrt(a*t)), + # pi**(S(1)/2)*s**(-S(5)/2)*(s/2+a) * + # exp(a/s)*erf(sqrt(a/s))-a**(S(1)/2)*s**(-2), + # S.true, S.Zero, dco), # 4.9.36 + # (sqrt(t)*cosh(2*sqrt(a*t)), pi**(S(1)/2)*s**(-S(5)/2)*(s/2+a)*exp(a/s), + # S.true, S.Zero, dco), # 4.9.37 + # (sinh(2*sqrt(a*t))/sqrt(t), + # pi**(S(1)/2)*s**(-S(1)/2)*exp(a/s) * erf(sqrt(a/s)), + # S.true, S.Zero, dco), # 4.9.38 + # (cosh(2*sqrt(a*t))/sqrt(t), pi**(S(1)/2)*s**(-S(1)/2)*exp(a/s), + # S.true, S.Zero, dco), # 4.9.39 + # (sinh(sqrt(a*t))**2/sqrt(t), pi**(S(1)/2)/2*s**(-S(1)/2)*(exp(a/s)-1), + # S.true, S.Zero, dco), # 4.9.40 + # (cosh(sqrt(a*t))**2/sqrt(t), pi**(S(1)/2)/2*s**(-S(1)/2)*(exp(a/s)+1), + # S.true, S.Zero, dco), # 4.9.41 + (erf(a*t), exp(s**2/(2*a)**2)*erfc(s/(2*a))/s, + 4*Abs(arg(a)) < pi, S.Zero, dco), # 4.12.2 + # (erf(sqrt(a*t)), sqrt(a)/sqrt(s+a)/s, + # S.true, Max(S.Zero, -re(a)), dco), # 4.12.4 + # (exp(a*t)*erf(sqrt(a*t)), sqrt(a)/sqrt(s)/(s-a), + # S.true, Max(S.Zero, re(a)), dco), # 4.12.5 + # (erf(sqrt(a/t)/2), (1-exp(-sqrt(a*s)))/s, + # re(a) > 0, S.Zero, dco), # 4.12.6 + # (erfc(sqrt(a*t)), (sqrt(s+a)-sqrt(a))/sqrt(s+a)/s, + # S.true, -re(a), dco), # 4.12.9 + # (exp(a*t)*erfc(sqrt(a*t)), 1/(s+sqrt(a*s)), + # S.true, S.Zero, dco), # 4.12.10 + # (erfc(sqrt(a/t)/2), exp(-sqrt(a*s))/s, + # re(a) > 0, S.Zero, dco), # 4.2.11 + (besselj(n, a*t), a**n/(sqrt(s**2+a**2)*(s+sqrt(s**2+a**2))**n), + re(n) > -1, Abs(im(a)), dco), # 4.14.1 + (t**b*besselj(n, a*t), + 2**n/sqrt(pi)*gamma(n+S.Half)*a**n*(s**2+a**2)**(-n-S.Half), + And(re(n) > -S.Half, Eq(b, n)), Abs(im(a)), dco), # 4.14.7 + (t**b*besselj(n, a*t), + 2**(n+1)/sqrt(pi)*gamma(n+S(3)/2)*a**n*s*(s**2+a**2)**(-n-S(3)/2), + And(re(n) > -1, Eq(b, n+1)), Abs(im(a)), dco), # 4.14.8 + # (besselj(0, 2*sqrt(a*t)), exp(-a/s)/s, + # S.true, S.Zero, dco), # 4.14.25 + # (t**(b)*besselj(n, 2*sqrt(a*t)), a**(n/2)*s**(-n-1)*exp(-a/s), + # And(re(n) > -1, Eq(b, n*S.Half)), S.Zero, dco), # 4.14.30 + (besselj(0, a*sqrt(t**2+b*t)), + exp(b*s-b*sqrt(s**2+a**2))/sqrt(s**2+a**2), + Abs(arg(b)) < pi, Abs(im(a)), dco), # 4.15.19 + (besseli(n, a*t), a**n/(sqrt(s**2-a**2)*(s+sqrt(s**2-a**2))**n), + re(n) > -1, Abs(re(a)), dco), # 4.16.1 + (t**b*besseli(n, a*t), + 2**n/sqrt(pi)*gamma(n+S.Half)*a**n*(s**2-a**2)**(-n-S.Half), + And(re(n) > -S.Half, Eq(b, n)), Abs(re(a)), dco), # 4.16.6 + (t**b*besseli(n, a*t), + 2**(n+1)/sqrt(pi)*gamma(n+S(3)/2)*a**n*s*(s**2-a**2)**(-n-S(3)/2), + And(re(n) > -1, Eq(b, n+1)), Abs(re(a)), dco), # 4.16.7 + # (t**(b)*besseli(n, 2*sqrt(a*t)), a**(n/2)*s**(-n-1)*exp(a/s), + # And(re(n) > -1, Eq(b, n*S.Half)), S.Zero, dco), # 4.16.18 + (bessely(0, a*t), -2/pi*asinh(s/a)/sqrt(s**2+a**2), + S.true, Abs(im(a)), dco), # 4.15.44 + (besselk(0, a*t), log((s + sqrt(s**2-a**2))/a)/(sqrt(s**2-a**2)), + S.true, -re(a), dco) # 4.16.23 + ] + return laplace_transform_rules, t, s + + +@DEBUG_WRAP +def _laplace_rule_timescale(f, t, s): + """ + This function applies the time-scaling rule of the Laplace transform in + a straight-forward way. For example, if it gets ``(f(a*t), t, s)``, it will + compute ``LaplaceTransform(f(t)/a, t, s/a)`` if ``a>0``. + """ + + a = Wild('a', exclude=[t]) + g = WildFunction('g', nargs=1) + ma1 = f.match(g) + if ma1: + arg = ma1[g].args[0].collect(t) + ma2 = arg.match(a*t) + if ma2 and ma2[a].is_positive and ma2[a] != 1: + _debug(' rule: time scaling (4.1.4)') + r, pr, cr = _laplace_transform( + 1/ma2[a]*ma1[g].func(t), t, s/ma2[a], simplify=False) + return (r, pr, cr) + return None + + +@DEBUG_WRAP +def _laplace_rule_heaviside(f, t, s): + """ + This function deals with time-shifted Heaviside step functions. If the time + shift is positive, it applies the time-shift rule of the Laplace transform. + For example, if it gets ``(Heaviside(t-a)*f(t), t, s)``, it will compute + ``exp(-a*s)*LaplaceTransform(f(t+a), t, s)``. + + If the time shift is negative, the Heaviside function is simply removed + as it means nothing to the Laplace transform. + + The function does not remove a factor ``Heaviside(t)``; this is done by + the simple rules. + """ + + a = Wild('a', exclude=[t]) + y = Wild('y') + g = Wild('g') + if ma1 := f.match(Heaviside(y) * g): + if ma2 := ma1[y].match(t - a): + if ma2[a].is_positive: + _debug(' rule: time shift (4.1.4)') + r, pr, cr = _laplace_transform( + ma1[g].subs(t, t + ma2[a]), t, s, simplify=False) + return (exp(-ma2[a] * s) * r, pr, cr) + if ma2[a].is_negative: + _debug( + ' rule: Heaviside factor; negative time shift (4.1.4)') + r, pr, cr = _laplace_transform(ma1[g], t, s, simplify=False) + return (r, pr, cr) + if ma2 := ma1[y].match(a - t): + if ma2[a].is_positive: + _debug(' rule: Heaviside window open') + r, pr, cr = _laplace_transform( + (1 - Heaviside(t - ma2[a])) * ma1[g], t, s, simplify=False) + return (r, pr, cr) + if ma2[a].is_negative: + _debug(' rule: Heaviside window closed') + return (0, 0, S.true) + return None + + +@DEBUG_WRAP +def _laplace_rule_exp(f, t, s): + """ + If this function finds a factor ``exp(a*t)``, it applies the + frequency-shift rule of the Laplace transform and adjusts the convergence + plane accordingly. For example, if it gets ``(exp(-a*t)*f(t), t, s)``, it + will compute ``LaplaceTransform(f(t), t, s+a)``. + """ + + a = Wild('a', exclude=[t]) + y = Wild('y') + z = Wild('z') + ma1 = f.match(exp(y)*z) + if ma1: + ma2 = ma1[y].collect(t).match(a*t) + if ma2: + _debug(' rule: multiply with exp (4.1.5)') + r, pr, cr = _laplace_transform(ma1[z], t, s-ma2[a], + simplify=False) + return (r, pr+re(ma2[a]), cr) + return None + + +@DEBUG_WRAP +def _laplace_rule_delta(f, t, s): + """ + If this function finds a factor ``DiracDelta(b*t-a)``, it applies the + masking property of the delta distribution. For example, if it gets + ``(DiracDelta(t-a)*f(t), t, s)``, it will return + ``(f(a)*exp(-a*s), -a, True)``. + """ + # This rule is not in Bateman54 + + a = Wild('a', exclude=[t]) + b = Wild('b', exclude=[t]) + + y = Wild('y') + z = Wild('z') + ma1 = f.match(DiracDelta(y)*z) + if ma1 and not ma1[z].has(DiracDelta): + ma2 = ma1[y].collect(t).match(b*t-a) + if ma2: + _debug(' rule: multiply with DiracDelta') + loc = ma2[a]/ma2[b] + if re(loc) >= 0 and im(loc) == 0: + fn = exp(-ma2[a]/ma2[b]*s)*ma1[z] + if fn.has(sin, cos): + # Then it may be possible that a sinc() is present in the + # term; let's try this: + fn = fn.rewrite(sinc).ratsimp() + n, d = [x.subs(t, ma2[a]/ma2[b]) for x in fn.as_numer_denom()] + if d != 0: + return (n/d/ma2[b], S.NegativeInfinity, S.true) + else: + return None + else: + return (0, S.NegativeInfinity, S.true) + if ma1[y].is_polynomial(t): + ro = roots(ma1[y], t) + if ro != {} and set(ro.values()) == {1}: + slope = diff(ma1[y], t) + r = Add( + *[exp(-x*s)*ma1[z].subs(t, s)/slope.subs(t, x) + for x in list(ro.keys()) if im(x) == 0 and re(x) >= 0]) + return (r, S.NegativeInfinity, S.true) + return None + + +@DEBUG_WRAP +def _laplace_trig_split(fn): + """ + Helper function for `_laplace_rule_trig`. This function returns two terms + `f` and `g`. `f` contains all product terms with sin, cos, sinh, cosh in + them; `g` contains everything else. + """ + trigs = [S.One] + other = [S.One] + for term in Mul.make_args(fn): + if term.has(sin, cos, sinh, cosh, exp): + trigs.append(term) + else: + other.append(term) + f = Mul(*trigs) + g = Mul(*other) + return f, g + + +@DEBUG_WRAP +def _laplace_trig_expsum(f, t): + """ + Helper function for `_laplace_rule_trig`. This function expects the `f` + from `_laplace_trig_split`. It returns two lists `xm` and `xn`. `xm` is + a list of dictionaries with keys `k` and `a` representing a function + `k*exp(a*t)`. `xn` is a list of all terms that cannot be brought into + that form, which may happen, e.g., when a trigonometric function has + another function in its argument. + """ + c1 = Wild('c1', exclude=[t]) + c0 = Wild('c0', exclude=[t]) + p = Wild('p', exclude=[t]) + xm = [] + xn = [] + + x1 = f.rewrite(exp).expand() + + for term in Add.make_args(x1): + if not term.has(t): + xm.append({'k': term, 'a': 0, re: 0, im: 0}) + continue + term = _laplace_deep_collect(term.powsimp(combine='exp'), t) + + if (r := term.match(p*exp(c1*t+c0))) is not None: + xm.append({ + 'k': r[p]*exp(r[c0]), 'a': r[c1], + re: re(r[c1]), im: im(r[c1])}) + else: + xn.append(term) + return xm, xn + + +@DEBUG_WRAP +def _laplace_trig_ltex(xm, t, s): + """ + Helper function for `_laplace_rule_trig`. This function takes the list of + exponentials `xm` from `_laplace_trig_expsum` and simplifies complex + conjugate and real symmetric poles. It returns the result as a sum and + the convergence plane. + """ + results = [] + planes = [] + + def _simpc(coeffs): + nc = coeffs.copy() + for k in range(len(nc)): + ri = nc[k].as_real_imag() + if ri[0].has(im): + nc[k] = nc[k].rewrite(cos) + else: + nc[k] = (ri[0] + I*ri[1]).rewrite(cos) + return nc + + def _quadpole(t1, k1, k2, k3, s): + a, k0, a_r, a_i = t1['a'], t1['k'], t1[re], t1[im] + nc = [ + k0 + k1 + k2 + k3, + a*(k0 + k1 - k2 - k3) - 2*I*a_i*k1 + 2*I*a_i*k2, + ( + a**2*(-k0 - k1 - k2 - k3) + + a*(4*I*a_i*k0 + 4*I*a_i*k3) + + 4*a_i**2*k0 + 4*a_i**2*k3), + ( + a**3*(-k0 - k1 + k2 + k3) + + a**2*(4*I*a_i*k0 + 2*I*a_i*k1 - 2*I*a_i*k2 - 4*I*a_i*k3) + + a*(4*a_i**2*k0 - 4*a_i**2*k3)) + ] + dc = [ + S.One, S.Zero, 2*a_i**2 - 2*a_r**2, + S.Zero, a_i**4 + 2*a_i**2*a_r**2 + a_r**4] + n = Add( + *[x*s**y for x, y in zip(_simpc(nc), range(len(nc))[::-1])]) + d = Add( + *[x*s**y for x, y in zip(dc, range(len(dc))[::-1])]) + return n/d + + def _ccpole(t1, k1, s): + a, k0, a_r, a_i = t1['a'], t1['k'], t1[re], t1[im] + nc = [k0 + k1, -a*k0 - a*k1 + 2*I*a_i*k0] + dc = [S.One, -2*a_r, a_i**2 + a_r**2] + n = Add( + *[x*s**y for x, y in zip(_simpc(nc), range(len(nc))[::-1])]) + d = Add( + *[x*s**y for x, y in zip(dc, range(len(dc))[::-1])]) + return n/d + + def _rspole(t1, k2, s): + a, k0, a_r, a_i = t1['a'], t1['k'], t1[re], t1[im] + nc = [k0 + k2, a*k0 - a*k2 - 2*I*a_i*k0] + dc = [S.One, -2*I*a_i, -a_i**2 - a_r**2] + n = Add( + *[x*s**y for x, y in zip(_simpc(nc), range(len(nc))[::-1])]) + d = Add( + *[x*s**y for x, y in zip(dc, range(len(dc))[::-1])]) + return n/d + + def _sypole(t1, k3, s): + a, k0 = t1['a'], t1['k'] + nc = [k0 + k3, a*(k0 - k3)] + dc = [S.One, S.Zero, -a**2] + n = Add( + *[x*s**y for x, y in zip(_simpc(nc), range(len(nc))[::-1])]) + d = Add( + *[x*s**y for x, y in zip(dc, range(len(dc))[::-1])]) + return n/d + + def _simplepole(t1, s): + a, k0 = t1['a'], t1['k'] + n = k0 + d = s - a + return n/d + + while len(xm) > 0: + t1 = xm.pop() + i_imagsym = None + i_realsym = None + i_pointsym = None + # The following code checks all remaining poles. If t1 is a pole at + # a+b*I, then we check for a-b*I, -a+b*I, and -a-b*I, and + # assign the respective indices to i_imagsym, i_realsym, i_pointsym. + # -a-b*I / i_pointsym only applies if both a and b are != 0. + for i in range(len(xm)): + real_eq = t1[re] == xm[i][re] + realsym = t1[re] == -xm[i][re] + imag_eq = t1[im] == xm[i][im] + imagsym = t1[im] == -xm[i][im] + if realsym and imagsym and t1[re] != 0 and t1[im] != 0: + i_pointsym = i + elif realsym and imag_eq and t1[re] != 0: + i_realsym = i + elif real_eq and imagsym and t1[im] != 0: + i_imagsym = i + + # The next part looks for four possible pole constellations: + # quad: a+b*I, a-b*I, -a+b*I, -a-b*I + # cc: a+b*I, a-b*I (a may be zero) + # quad: a+b*I, -a+b*I (b may be zero) + # point: a+b*I, -a-b*I (a!=0 and b!=0 is needed, but that has been + # asserted when finding i_pointsym above.) + # If none apply, then t1 is a simple pole. + if ( + i_imagsym is not None and i_realsym is not None + and i_pointsym is not None): + results.append( + _quadpole(t1, + xm[i_imagsym]['k'], xm[i_realsym]['k'], + xm[i_pointsym]['k'], s)) + planes.append(Abs(re(t1['a']))) + # The three additional poles have now been used; to pop them + # easily we have to do it from the back. + indices_to_pop = [i_imagsym, i_realsym, i_pointsym] + indices_to_pop.sort(reverse=True) + for i in indices_to_pop: + xm.pop(i) + elif i_imagsym is not None: + results.append(_ccpole(t1, xm[i_imagsym]['k'], s)) + planes.append(t1[re]) + xm.pop(i_imagsym) + elif i_realsym is not None: + results.append(_rspole(t1, xm[i_realsym]['k'], s)) + planes.append(Abs(t1[re])) + xm.pop(i_realsym) + elif i_pointsym is not None: + results.append(_sypole(t1, xm[i_pointsym]['k'], s)) + planes.append(Abs(t1[re])) + xm.pop(i_pointsym) + else: + results.append(_simplepole(t1, s)) + planes.append(t1[re]) + + return Add(*results), Max(*planes) + + +@DEBUG_WRAP +def _laplace_rule_trig(fn, t_, s): + """ + This rule covers trigonometric factors by splitting everything into a + sum of exponential functions and collecting complex conjugate poles and + real symmetric poles. + """ + t = Dummy('t', real=True) + + if not fn.has(sin, cos, sinh, cosh): + return None + + f, g = _laplace_trig_split(fn.subs(t_, t)) + xm, xn = _laplace_trig_expsum(f, t) + + if len(xn) > 0: + # TODO not implemented yet, but also not important + return None + + if not g.has(t): + r, p = _laplace_trig_ltex(xm, t, s) + return g*r, p, S.true + else: + # Just transform `g` and make frequency-shifted copies + planes = [] + results = [] + G, G_plane, G_cond = _laplace_transform(g, t, s, simplify=False) + for x1 in xm: + results.append(x1['k']*G.subs(s, s-x1['a'])) + planes.append(G_plane+re(x1['a'])) + return Add(*results).subs(t, t_), Max(*planes), G_cond + + +@DEBUG_WRAP +def _laplace_rule_diff(f, t, s): + """ + This function looks for derivatives in the time domain and replaces it + by factors of `s` and initial conditions in the frequency domain. For + example, if it gets ``(diff(f(t), t), t, s)``, it will compute + ``s*LaplaceTransform(f(t), t, s) - f(0)``. + """ + + a = Wild('a', exclude=[t]) + n = Wild('n', exclude=[t]) + g = WildFunction('g') + ma1 = f.match(a*Derivative(g, (t, n))) + if ma1 and ma1[n].is_integer: + m = [z.has(t) for z in ma1[g].args] + if sum(m) == 1: + _debug(' rule: time derivative (4.1.8)') + d = [] + for k in range(ma1[n]): + if k == 0: + y = ma1[g].subs(t, 0) + else: + y = Derivative(ma1[g], (t, k)).subs(t, 0) + d.append(s**(ma1[n]-k-1)*y) + r, pr, cr = _laplace_transform(ma1[g], t, s, simplify=False) + return (ma1[a]*(s**ma1[n]*r - Add(*d)), pr, cr) + return None + + +@DEBUG_WRAP +def _laplace_rule_sdiff(f, t, s): + """ + This function looks for multiplications with polynoimials in `t` as they + correspond to differentiation in the frequency domain. For example, if it + gets ``(t*f(t), t, s)``, it will compute + ``-Derivative(LaplaceTransform(f(t), t, s), s)``. + """ + + if f.is_Mul: + pfac = [1] + ofac = [1] + for fac in Mul.make_args(f): + if fac.is_polynomial(t): + pfac.append(fac) + else: + ofac.append(fac) + if len(pfac) > 1: + pex = prod(pfac) + pc = Poly(pex, t).all_coeffs() + N = len(pc) + if N > 1: + oex = prod(ofac) + r_, p_, c_ = _laplace_transform(oex, t, s, simplify=False) + deri = [r_] + d1 = False + try: + d1 = -diff(deri[-1], s) + except ValueError: + d1 = False + if r_.has(LaplaceTransform): + for k in range(N-1): + deri.append((-1)**(k+1)*Derivative(r_, s, k+1)) + elif d1: + deri.append(d1) + for k in range(N-2): + deri.append(-diff(deri[-1], s)) + if d1: + r = Add(*[pc[N-n-1]*deri[n] for n in range(N)]) + return (r, p_, c_) + # We still have to cover the possibility that there is a symbolic positive + # integer exponent. + n = Wild('n', exclude=[t]) + g = Wild('g') + if ma1 := f.match(t**n*g): + if ma1[n].is_integer and ma1[n].is_positive: + r_, p_, c_ = _laplace_transform(ma1[g], t, s, simplify=False) + return (-1)**ma1[n]*diff(r_, (s, ma1[n])), p_, c_ + return None + + +@DEBUG_WRAP +def _laplace_expand(f, t, s): + """ + This function tries to expand its argument with successively stronger + methods: first it will expand on the top level, then it will expand any + multiplications in depth, then it will try all available expansion methods, + and finally it will try to expand trigonometric functions. + + If it can expand, it will then compute the Laplace transform of the + expanded term. + """ + + r = expand(f, deep=False) + if r.is_Add: + return _laplace_transform(r, t, s, simplify=False) + r = expand_mul(f) + if r.is_Add: + return _laplace_transform(r, t, s, simplify=False) + r = expand(f) + if r.is_Add: + return _laplace_transform(r, t, s, simplify=False) + if r != f: + return _laplace_transform(r, t, s, simplify=False) + r = expand(expand_trig(f)) + if r.is_Add: + return _laplace_transform(r, t, s, simplify=False) + return None + + +@DEBUG_WRAP +def _laplace_apply_prog_rules(f, t, s): + """ + This function applies all program rules and returns the result if one + of them gives a result. + """ + + prog_rules = [_laplace_rule_heaviside, _laplace_rule_delta, + _laplace_rule_timescale, _laplace_rule_exp, + _laplace_rule_trig, + _laplace_rule_diff, _laplace_rule_sdiff] + + for p_rule in prog_rules: + if (L := p_rule(f, t, s)) is not None: + return L + return None + + +@DEBUG_WRAP +def _laplace_apply_simple_rules(f, t, s): + """ + This function applies all simple rules and returns the result if one + of them gives a result. + """ + simple_rules, t_, s_ = _laplace_build_rules() + prep_old = '' + prep_f = '' + for t_dom, s_dom, check, plane, prep in simple_rules: + if prep_old != prep: + prep_f = prep(f.subs({t: t_})) + prep_old = prep + ma = prep_f.match(t_dom) + if ma: + try: + c = check.xreplace(ma) + except TypeError: + # This may happen if the time function has imaginary + # numbers in it. Then we give up. + continue + if c == S.true: + return (s_dom.xreplace(ma).subs({s_: s}), + plane.xreplace(ma), S.true) + return None + + +@DEBUG_WRAP +def _piecewise_to_heaviside(f, t): + """ + This function converts a Piecewise expression to an expression written + with Heaviside. It is not exact, but valid in the context of the Laplace + transform. + """ + if not t.is_real: + r = Dummy('r', real=True) + return _piecewise_to_heaviside(f.xreplace({t: r}), r).xreplace({r: t}) + x = piecewise_exclusive(f) + r = [] + for fn, cond in x.args: + # Here we do not need to do many checks because piecewise_exclusive + # has a clearly predictable output. However, if any of the conditions + # is not relative to t, this function just returns the input argument. + if isinstance(cond, Relational) and t in cond.args: + if isinstance(cond, (Eq, Ne)): + # We do not cover this case; these would be single-point + # exceptions that do not play a role in Laplace practice, + # except if they contain Dirac impulses, and then we can + # expect users to not try to use Piecewise for writing it. + return f + else: + r.append(Heaviside(cond.gts - cond.lts)*fn) + elif isinstance(cond, Or) and len(cond.args) == 2: + # Or(t<2, t>4), Or(t>4, t<=2), ... in any order with any <= >= + for c2 in cond.args: + if c2.lhs == t: + r.append(Heaviside(c2.gts - c2.lts)*fn) + else: + return f + elif isinstance(cond, And) and len(cond.args) == 2: + # And(t>2, t<4), And(t>4, t<=2), ... in any order with any <= >= + c0, c1 = cond.args + if c0.lhs == t and c1.lhs == t: + if '>' in c0.rel_op: + c0, c1 = c1, c0 + r.append( + (Heaviside(c1.gts - c1.lts) - + Heaviside(c0.lts - c0.gts))*fn) + else: + return f + else: + return f + return Add(*r) + + +def laplace_correspondence(f, fdict, /): + """ + This helper function takes a function `f` that is the result of a + ``laplace_transform`` or an ``inverse_laplace_transform``. It replaces all + unevaluated ``LaplaceTransform(y(t), t, s)`` by `Y(s)` for any `s` and + all ``InverseLaplaceTransform(Y(s), s, t)`` by `y(t)` for any `t` if + ``fdict`` contains a correspondence ``{y: Y}``. + + Parameters + ========== + + f : sympy expression + Expression containing unevaluated ``LaplaceTransform`` or + ``LaplaceTransform`` objects. + fdict : dictionary + Dictionary containing one or more function correspondences, + e.g., ``{x: X, y: Y}`` meaning that ``X`` and ``Y`` are the + Laplace transforms of ``x`` and ``y``, respectively. + + Examples + ======== + + >>> from sympy import laplace_transform, diff, Function + >>> from sympy import laplace_correspondence, inverse_laplace_transform + >>> from sympy.abc import t, s + >>> y = Function("y") + >>> Y = Function("Y") + >>> z = Function("z") + >>> Z = Function("Z") + >>> f = laplace_transform(diff(y(t), t, 1) + z(t), t, s, noconds=True) + >>> laplace_correspondence(f, {y: Y, z: Z}) + s*Y(s) + Z(s) - y(0) + >>> f = inverse_laplace_transform(Y(s), s, t) + >>> laplace_correspondence(f, {y: Y}) + y(t) + """ + p = Wild('p') + s = Wild('s') + t = Wild('t') + a = Wild('a') + if ( + not isinstance(f, Expr) + or (not f.has(LaplaceTransform) + and not f.has(InverseLaplaceTransform))): + return f + for y, Y in fdict.items(): + if ( + (m := f.match(LaplaceTransform(y(a), t, s))) is not None + and m[a] == m[t]): + return Y(m[s]) + if ( + (m := f.match(InverseLaplaceTransform(Y(a), s, t, p))) + is not None + and m[a] == m[s]): + return y(m[t]) + func = f.func + args = [laplace_correspondence(arg, fdict) for arg in f.args] + return func(*args) + + +def laplace_initial_conds(f, t, fdict, /): + """ + This helper function takes a function `f` that is the result of a + ``laplace_transform``. It takes an fdict of the form ``{y: [1, 4, 2]}``, + where the values in the list are the initial value, the initial slope, the + initial second derivative, etc., of the function `y(t)`, and replaces all + unevaluated initial conditions. + + Parameters + ========== + + f : sympy expression + Expression containing initial conditions of unevaluated functions. + t : sympy expression + Variable for which the initial conditions are to be applied. + fdict : dictionary + Dictionary containing a list of initial conditions for every + function, e.g., ``{y: [0, 1, 2], x: [3, 4, 5]}``. The order + of derivatives is ascending, so `0`, `1`, `2` are `y(0)`, `y'(0)`, + and `y''(0)`, respectively. + + Examples + ======== + + >>> from sympy import laplace_transform, diff, Function + >>> from sympy import laplace_correspondence, laplace_initial_conds + >>> from sympy.abc import t, s + >>> y = Function("y") + >>> Y = Function("Y") + >>> f = laplace_transform(diff(y(t), t, 3), t, s, noconds=True) + >>> g = laplace_correspondence(f, {y: Y}) + >>> laplace_initial_conds(g, t, {y: [2, 4, 8, 16, 32]}) + s**3*Y(s) - 2*s**2 - 4*s - 8 + """ + for y, ic in fdict.items(): + for k in range(len(ic)): + if k == 0: + f = f.replace(y(0), ic[0]) + elif k == 1: + f = f.replace(Subs(Derivative(y(t), t), t, 0), ic[1]) + else: + f = f.replace(Subs(Derivative(y(t), (t, k)), t, 0), ic[k]) + return f + + +@DEBUG_WRAP +def _laplace_transform(fn, t_, s_, *, simplify): + """ + Front-end function of the Laplace transform. It tries to apply all known + rules recursively, and if everything else fails, it tries to integrate. + """ + + terms_t = Add.make_args(fn) + terms_s = [] + terms = [] + planes = [] + conditions = [] + + for ff in terms_t: + k, ft = ff.as_independent(t_, as_Add=False) + if ft.has(SingularityFunction): + _terms = Add.make_args(ft.rewrite(Heaviside)) + for _term in _terms: + k1, f1 = _term.as_independent(t_, as_Add=False) + terms.append((k*k1, f1)) + elif ft.func == Piecewise and not ft.has(DiracDelta(t_)): + _terms = Add.make_args(_piecewise_to_heaviside(ft, t_)) + for _term in _terms: + k1, f1 = _term.as_independent(t_, as_Add=False) + terms.append((k*k1, f1)) + else: + terms.append((k, ft)) + + for k, ft in terms: + if ft.has(SingularityFunction): + r = (LaplaceTransform(ft, t_, s_), S.NegativeInfinity, True) + else: + if ft.has(Heaviside(t_)) and not ft.has(DiracDelta(t_)): + # For t>=0, Heaviside(t)=1 can be used, except if there is also + # a DiracDelta(t) present, in which case removing Heaviside(t) + # is unnecessary because _laplace_rule_delta can deal with it. + ft = ft.subs(Heaviside(t_), 1) + if ( + (r := _laplace_apply_simple_rules(ft, t_, s_)) + is not None or + (r := _laplace_apply_prog_rules(ft, t_, s_)) + is not None or + (r := _laplace_expand(ft, t_, s_)) is not None): + pass + elif any(undef.has(t_) for undef in ft.atoms(AppliedUndef)): + # If there are undefined functions f(t) then integration is + # unlikely to do anything useful so we skip it and given an + # unevaluated LaplaceTransform. + r = (LaplaceTransform(ft, t_, s_), S.NegativeInfinity, True) + elif (r := _laplace_transform_integration( + ft, t_, s_, simplify=simplify)) is not None: + pass + else: + r = (LaplaceTransform(ft, t_, s_), S.NegativeInfinity, True) + (ri_, pi_, ci_) = r + terms_s.append(k*ri_) + planes.append(pi_) + conditions.append(ci_) + + result = Add(*terms_s) + if simplify: + result = result.simplify(doit=False) + plane = Max(*planes) + condition = And(*conditions) + + return result, plane, condition + + +class LaplaceTransform(IntegralTransform): + """ + Class representing unevaluated Laplace transforms. + + For usage of this class, see the :class:`IntegralTransform` docstring. + + For how to compute Laplace transforms, see the :func:`laplace_transform` + docstring. + + If this is called with ``.doit()``, it returns the Laplace transform as an + expression. If it is called with ``.doit(noconds=False)``, it returns a + tuple containing the same expression, a convergence plane, and conditions. + """ + + _name = 'Laplace' + + def _compute_transform(self, f, t, s, **hints): + _simplify = hints.get('simplify', False) + LT = _laplace_transform_integration(f, t, s, simplify=_simplify) + return LT + + def _as_integral(self, f, t, s): + return Integral(f*exp(-s*t), (t, S.Zero, S.Infinity)) + + def doit(self, **hints): + """ + Try to evaluate the transform in closed form. + + Explanation + =========== + + Standard hints are the following: + - ``noconds``: if True, do not return convergence conditions. The + default setting is `True`. + - ``simplify``: if True, it simplifies the final result. The + default setting is `False`. + """ + _noconds = hints.get('noconds', True) + _simplify = hints.get('simplify', False) + + debugf('[LT doit] (%s, %s, %s)', (self.function, + self.function_variable, + self.transform_variable)) + + t_ = self.function_variable + s_ = self.transform_variable + fn = self.function + + r = _laplace_transform(fn, t_, s_, simplify=_simplify) + + if _noconds: + return r[0] + else: + return r + + +def laplace_transform(f, t, s, legacy_matrix=True, **hints): + r""" + Compute the Laplace Transform `F(s)` of `f(t)`, + + .. math :: F(s) = \int_{0^{-}}^\infty e^{-st} f(t) \mathrm{d}t. + + Explanation + =========== + + For all sensible functions, this converges absolutely in a + half-plane + + .. math :: a < \operatorname{Re}(s) + + This function returns ``(F, a, cond)`` where ``F`` is the Laplace + transform of ``f``, `a` is the half-plane of convergence, and `cond` are + auxiliary convergence conditions. + + The implementation is rule-based, and if you are interested in which + rules are applied, and whether integration is attempted, you can switch + debug information on by setting ``sympy.SYMPY_DEBUG=True``. The numbers + of the rules in the debug information (and the code) refer to Bateman's + Tables of Integral Transforms [1]. + + The lower bound is `0-`, meaning that this bound should be approached + from the lower side. This is only necessary if distributions are involved. + At present, it is only done if `f(t)` contains ``DiracDelta``, in which + case the Laplace transform is computed implicitly as + + .. math :: + F(s) = \lim_{\tau\to 0^{-}} \int_{\tau}^\infty e^{-st} + f(t) \mathrm{d}t + + by applying rules. + + If the Laplace transform cannot be fully computed in closed form, this + function returns expressions containing unevaluated + :class:`LaplaceTransform` objects. + + For a description of possible hints, refer to the docstring of + :func:`sympy.integrals.transforms.IntegralTransform.doit`. If + ``noconds=True``, only `F` will be returned (i.e. not ``cond``, and also + not the plane ``a``). + + .. deprecated:: 1.9 + Legacy behavior for matrices where ``laplace_transform`` with + ``noconds=False`` (the default) returns a Matrix whose elements are + tuples. The behavior of ``laplace_transform`` for matrices will change + in a future release of SymPy to return a tuple of the transformed + Matrix and the convergence conditions for the matrix as a whole. Use + ``legacy_matrix=False`` to enable the new behavior. + + Examples + ======== + + >>> from sympy import DiracDelta, exp, laplace_transform + >>> from sympy.abc import t, s, a + >>> laplace_transform(t**4, t, s) + (24/s**5, 0, True) + >>> laplace_transform(t**a, t, s) + (gamma(a + 1)/(s*s**a), 0, re(a) > -1) + >>> laplace_transform(DiracDelta(t)-a*exp(-a*t), t, s, simplify=True) + (s/(a + s), -re(a), True) + + There are also helper functions that make it easy to solve differential + equations by Laplace transform. For example, to solve + + .. math :: m x''(t) + d x'(t) + k x(t) = 0 + + with initial value `0` and initial derivative `v`: + + >>> from sympy import Function, laplace_correspondence, diff, solve + >>> from sympy import laplace_initial_conds, inverse_laplace_transform + >>> from sympy.abc import d, k, m, v + >>> x = Function('x') + >>> X = Function('X') + >>> f = m*diff(x(t), t, 2) + d*diff(x(t), t) + k*x(t) + >>> F = laplace_transform(f, t, s, noconds=True) + >>> F = laplace_correspondence(F, {x: X}) + >>> F = laplace_initial_conds(F, t, {x: [0, v]}) + >>> F + d*s*X(s) + k*X(s) + m*(s**2*X(s) - v) + >>> Xs = solve(F, X(s))[0] + >>> Xs + m*v/(d*s + k + m*s**2) + >>> inverse_laplace_transform(Xs, s, t) + 2*v*exp(-d*t/(2*m))*sin(t*sqrt((-d**2 + 4*k*m)/m**2)/2)*Heaviside(t)/sqrt((-d**2 + 4*k*m)/m**2) + + References + ========== + + .. [1] Erdelyi, A. (ed.), Tables of Integral Transforms, Volume 1, + Bateman Manuscript Prooject, McGraw-Hill (1954), available: + https://resolver.caltech.edu/CaltechAUTHORS:20140123-101456353 + + See Also + ======== + + inverse_laplace_transform, mellin_transform, fourier_transform + hankel_transform, inverse_hankel_transform + + """ + + _noconds = hints.get('noconds', False) + _simplify = hints.get('simplify', False) + + if isinstance(f, MatrixBase) and hasattr(f, 'applyfunc'): + + conds = not hints.get('noconds', False) + + if conds and legacy_matrix: + adt = 'deprecated-laplace-transform-matrix' + sympy_deprecation_warning( + """ +Calling laplace_transform() on a Matrix with noconds=False (the default) is +deprecated. Either noconds=True or use legacy_matrix=False to get the new +behavior. + """, + deprecated_since_version='1.9', + active_deprecations_target=adt, + ) + # Temporarily disable the deprecation warning for non-Expr objects + # in Matrix + with ignore_warnings(SymPyDeprecationWarning): + return f.applyfunc( + lambda fij: laplace_transform(fij, t, s, **hints)) + else: + elements_trans = [laplace_transform( + fij, t, s, **hints) for fij in f] + if conds: + elements, avals, conditions = zip(*elements_trans) + f_laplace = type(f)(*f.shape, elements) + return f_laplace, Max(*avals), And(*conditions) + else: + return type(f)(*f.shape, elements_trans) + + LT, p, c = LaplaceTransform(f, t, s).doit(noconds=False, + simplify=_simplify) + + if not _noconds: + return LT, p, c + else: + return LT + + +@DEBUG_WRAP +def _inverse_laplace_transform_integration(F, s, t_, plane, *, simplify): + """ The backend function for inverse Laplace transforms. """ + from sympy.integrals.meijerint import meijerint_inversion, _get_coeff_exp + from sympy.integrals.transforms import inverse_mellin_transform + + # There are two strategies we can try: + # 1) Use inverse mellin transform, related by a simple change of variables. + # 2) Use the inversion integral. + + t = Dummy('t', real=True) + + def pw_simp(*args): + """ Simplify a piecewise expression from hyperexpand. """ + if len(args) != 3: + return Piecewise(*args) + arg = args[2].args[0].argument + coeff, exponent = _get_coeff_exp(arg, t) + e1 = args[0].args[0] + e2 = args[1].args[0] + return ( + Heaviside(1/Abs(coeff) - t**exponent)*e1 + + Heaviside(t**exponent - 1/Abs(coeff))*e2) + + if F.is_rational_function(s): + F = F.apart(s) + + if F.is_Add: + f = Add( + *[_inverse_laplace_transform_integration(X, s, t, plane, simplify) + for X in F.args]) + return _simplify(f.subs(t, t_), simplify), True + + try: + f, cond = inverse_mellin_transform(F, s, exp(-t), (None, S.Infinity), + needeval=True, noconds=False) + except IntegralTransformError: + f = None + + if f is None: + f = meijerint_inversion(F, s, t) + if f is None: + return None + if f.is_Piecewise: + f, cond = f.args[0] + if f.has(Integral): + return None + else: + cond = S.true + f = f.replace(Piecewise, pw_simp) + + if f.is_Piecewise: + # many of the functions called below can't work with piecewise + # (b/c it has a bool in args) + return f.subs(t, t_), cond + + u = Dummy('u') + + def simp_heaviside(arg, H0=S.Half): + a = arg.subs(exp(-t), u) + if a.has(t): + return Heaviside(arg, H0) + from sympy.solvers.inequalities import _solve_inequality + rel = _solve_inequality(a > 0, u) + if rel.lts == u: + k = log(rel.gts) + return Heaviside(t + k, H0) + else: + k = log(rel.lts) + return Heaviside(-(t + k), H0) + + f = f.replace(Heaviside, simp_heaviside) + + def simp_exp(arg): + return expand_complex(exp(arg)) + + f = f.replace(exp, simp_exp) + + return _simplify(f.subs(t, t_), simplify), cond + + +@DEBUG_WRAP +def _complete_the_square_in_denom(f, s): + from sympy.simplify.radsimp import fraction + [n, d] = fraction(f) + if d.is_polynomial(s): + cf = d.as_poly(s).all_coeffs() + if len(cf) == 3: + a, b, c = cf + d = a*((s+b/(2*a))**2+c/a-(b/(2*a))**2) + return n/d + + +@cacheit +def _inverse_laplace_build_rules(): + """ + This is an internal helper function that returns the table of inverse + Laplace transform rules in terms of the time variable `t` and the + frequency variable `s`. It is used by `_inverse_laplace_apply_rules`. + """ + s = Dummy('s') + t = Dummy('t') + a = Wild('a', exclude=[s]) + b = Wild('b', exclude=[s]) + c = Wild('c', exclude=[s]) + + _debug('_inverse_laplace_build_rules is building rules') + + def _frac(f, s): + try: + return f.factor(s) + except PolynomialError: + return f + + def same(f): return f + # This list is sorted according to the prep function needed. + _ILT_rules = [ + (a/s, a, S.true, same, 1), + ( + b*(s+a)**(-c), t**(c-1)*exp(-a*t)/gamma(c), + S.true, same, 1), + (1/(s**2+a**2)**2, (sin(a*t) - a*t*cos(a*t))/(2*a**3), + S.true, same, 1), + # The next two rules must be there in that order. For the second + # one, the condition would be a != 0 or, respectively, to take the + # limit a -> 0 after the transform if a == 0. It is much simpler if + # the case a == 0 has its own rule. + (1/(s**b), t**(b - 1)/gamma(b), S.true, same, 1), + (1/(s*(s+a)**b), lowergamma(b, a*t)/(a**b*gamma(b)), + S.true, same, 1) + ] + return _ILT_rules, s, t + + +@DEBUG_WRAP +def _inverse_laplace_apply_simple_rules(f, s, t): + """ + Helper function for the class InverseLaplaceTransform. + """ + if f == 1: + _debug(' rule: 1 o---o DiracDelta()') + return DiracDelta(t), S.true + + _ILT_rules, s_, t_ = _inverse_laplace_build_rules() + _prep = '' + fsubs = f.subs({s: s_}) + + for s_dom, t_dom, check, prep, fac in _ILT_rules: + if _prep != (prep, fac): + _F = prep(fsubs*fac) + _prep = (prep, fac) + ma = _F.match(s_dom) + if ma: + c = check + if c is not S.true: + args = [x.xreplace(ma) for x in c[0]] + c = c[1](*args) + if c == S.true: + return Heaviside(t)*t_dom.xreplace(ma).subs({t_: t}), S.true + + return None + + +@DEBUG_WRAP +def _inverse_laplace_diff(f, s, t, plane): + """ + Helper function for the class InverseLaplaceTransform. + """ + a = Wild('a', exclude=[s]) + n = Wild('n', exclude=[s]) + g = Wild('g') + ma = f.match(a*Derivative(g, (s, n))) + if ma and ma[n].is_integer: + _debug(' rule: t**n*f(t) o---o (-1)**n*diff(F(s), s, n)') + r, c = _inverse_laplace_transform( + ma[g], s, t, plane, simplify=False, dorational=False) + return (-t)**ma[n]*r, c + return None + + +@DEBUG_WRAP +def _inverse_laplace_time_shift(F, s, t, plane): + """ + Helper function for the class InverseLaplaceTransform. + """ + a = Wild('a', exclude=[s]) + g = Wild('g') + + if not F.has(s): + return F*DiracDelta(t), S.true + if not F.has(exp): + return None + + ma1 = F.match(exp(a*s)) + if ma1: + if ma1[a].is_negative: + _debug(' rule: exp(-a*s) o---o DiracDelta(t-a)') + return DiracDelta(t+ma1[a]), S.true + else: + return InverseLaplaceTransform(F, s, t, plane), S.true + + ma1 = F.match(exp(a*s)*g) + if ma1: + if ma1[a].is_negative: + _debug(' rule: exp(-a*s)*F(s) o---o Heaviside(t-a)*f(t-a)') + return _inverse_laplace_transform( + ma1[g], s, t+ma1[a], plane, simplify=False, dorational=True) + else: + return InverseLaplaceTransform(F, s, t, plane), S.true + return None + + +@DEBUG_WRAP +def _inverse_laplace_freq_shift(F, s, t, plane): + """ + Helper function for the class InverseLaplaceTransform. + """ + if not F.has(s): + return F*DiracDelta(t), S.true + if len(args := F.args) == 1: + a = Wild('a', exclude=[s]) + if (ma := args[0].match(s-a)) and re(ma[a]).is_positive: + _debug(' rule: F(s-a) o---o exp(-a*t)*f(t)') + return ( + exp(-ma[a]*t) * + InverseLaplaceTransform(F.func(s), s, t, plane), S.true) + return None + + +@DEBUG_WRAP +def _inverse_laplace_time_diff(F, s, t, plane): + """ + Helper function for the class InverseLaplaceTransform. + """ + n = Wild('n', exclude=[s]) + g = Wild('g') + + ma1 = F.match(s**n*g) + if ma1 and ma1[n].is_integer and ma1[n].is_positive: + _debug(' rule: s**n*F(s) o---o diff(f(t), t, n)') + r, c = _inverse_laplace_transform( + ma1[g], s, t, plane, simplify=False, dorational=True) + r = r.replace(Heaviside(t), 1) + if r.has(InverseLaplaceTransform): + return diff(r, t, ma1[n]), c + else: + return Heaviside(t)*diff(r, t, ma1[n]), c + return None + + +@DEBUG_WRAP +def _inverse_laplace_irrational(fn, s, t, plane): + """ + Helper function for the class InverseLaplaceTransform. + """ + + a = Wild('a', exclude=[s]) + b = Wild('b', exclude=[s]) + m = Wild('m', exclude=[s]) + n = Wild('n', exclude=[s]) + + result = None + condition = S.true + + fa = fn.as_ordered_factors() + + ma = [x.match((a*s**m+b)**n) for x in fa] + + if None in ma: + return None + + constants = S.One + zeros = [] + poles = [] + rest = [] + + for term in ma: + if term[a] == 0: + constants = constants*term + elif term[n].is_positive: + zeros.append(term) + elif term[n].is_negative: + poles.append(term) + else: + rest.append(term) + + # The code below assumes that the poles are sorted in a specific way: + poles = sorted(poles, key=lambda x: (x[n], x[b] != 0, x[b])) + zeros = sorted(zeros, key=lambda x: (x[n], x[b] != 0, x[b])) + + if len(rest) != 0: + return None + + if len(poles) == 1 and len(zeros) == 0: + if poles[0][n] == -1 and poles[0][m] == S.Half: + # 1/(a0*sqrt(s)+b0) == 1/a0 * 1/(sqrt(s)+b0/a0) + a_ = poles[0][b]/poles[0][a] + k_ = 1/poles[0][a]*constants + if a_.is_positive: + result = ( + k_/sqrt(pi)/sqrt(t) - + k_*a_*exp(a_**2*t)*erfc(a_*sqrt(t))) + _debug(' rule 5.3.4') + elif poles[0][n] == -2 and poles[0][m] == S.Half: + # 1/(a0*sqrt(s)+b0)**2 == 1/a0**2 * 1/(sqrt(s)+b0/a0)**2 + a_sq = poles[0][b]/poles[0][a] + a_ = a_sq**2 + k_ = 1/poles[0][a]**2*constants + if a_sq.is_positive: + result = ( + k_*(1 - 2/sqrt(pi)*sqrt(a_)*sqrt(t) + + (1-2*a_*t)*exp(a_*t)*(erf(sqrt(a_)*sqrt(t))-1))) + _debug(' rule 5.3.10') + elif poles[0][n] == -3 and poles[0][m] == S.Half: + # 1/(a0*sqrt(s)+b0)**3 == 1/a0**3 * 1/(sqrt(s)+b0/a0)**3 + a_ = poles[0][b]/poles[0][a] + k_ = 1/poles[0][a]**3*constants + if a_.is_positive: + result = ( + k_*(2/sqrt(pi)*(a_**2*t+1)*sqrt(t) - + a_*t*exp(a_**2*t)*(2*a_**2*t+3)*erfc(a_*sqrt(t)))) + _debug(' rule 5.3.13') + elif poles[0][n] == -4 and poles[0][m] == S.Half: + # 1/(a0*sqrt(s)+b0)**4 == 1/a0**4 * 1/(sqrt(s)+b0/a0)**4 + a_ = poles[0][b]/poles[0][a] + k_ = 1/poles[0][a]**4*constants/3 + if a_.is_positive: + result = ( + k_*(t*(4*a_**4*t**2+12*a_**2*t+3)*exp(a_**2*t) * + erfc(a_*sqrt(t)) - + 2/sqrt(pi)*a_**3*t**(S(5)/2)*(2*a_**2*t+5))) + _debug(' rule 5.3.16') + elif poles[0][n] == -S.Half and poles[0][m] == 2: + # 1/sqrt(a0*s**2+b0) == 1/sqrt(a0) * 1/sqrt(s**2+b0/a0) + a_ = sqrt(poles[0][b]/poles[0][a]) + k_ = 1/sqrt(poles[0][a])*constants + result = (k_*(besselj(0, a_*t))) + _debug(' rule 5.3.35/44') + + elif len(poles) == 1 and len(zeros) == 1: + if ( + poles[0][n] == -3 and poles[0][m] == S.Half and + zeros[0][n] == S.Half and zeros[0][b] == 0): + # sqrt(az*s)/(ap*sqrt(s+bp)**3) + # == sqrt(az)/ap * sqrt(s)/(sqrt(s+bp)**3) + a_ = poles[0][b] + k_ = sqrt(zeros[0][a])/poles[0][a]*constants + result = ( + k_*(2*a_**4*t**2+5*a_**2*t+1)*exp(a_**2*t) * + erfc(a_*sqrt(t)) - 2/sqrt(pi)*a_*(a_**2*t+2)*sqrt(t)) + _debug(' rule 5.3.14') + if ( + poles[0][n] == -1 and poles[0][m] == 1 and + zeros[0][n] == S.Half and zeros[0][m] == 1): + # sqrt(az*s+bz)/(ap*s+bp) + # == sqrt(az)/ap * (sqrt(s+bz/az)/(s+bp/ap)) + a_ = zeros[0][b]/zeros[0][a] + b_ = poles[0][b]/poles[0][a] + k_ = sqrt(zeros[0][a])/poles[0][a]*constants + result = ( + k_*(exp(-a_*t)/sqrt(t)/sqrt(pi)+sqrt(a_-b_) * + exp(-b_*t)*erf(sqrt(a_-b_)*sqrt(t)))) + _debug(' rule 5.3.22') + + elif len(poles) == 2 and len(zeros) == 0: + if ( + poles[0][n] == -1 and poles[0][m] == 1 and + poles[1][n] == -S.Half and poles[1][m] == 1 and + poles[1][b] == 0): + # 1/((a0*s+b0)*sqrt(a1*s)) + # == 1/(a0*sqrt(a1)) * 1/((s+b0/a0)*sqrt(s)) + a_ = -poles[0][b]/poles[0][a] + k_ = 1/sqrt(poles[1][a])/poles[0][a]*constants + if a_.is_positive: + result = (k_/sqrt(a_)*exp(a_*t)*erf(sqrt(a_)*sqrt(t))) + _debug(' rule 5.3.1') + elif ( + poles[0][n] == -1 and poles[0][m] == 1 and poles[0][b] == 0 and + poles[1][n] == -1 and poles[1][m] == S.Half): + # 1/(a0*s*(a1*sqrt(s)+b1)) + # == 1/(a0*a1) * 1/(s*(sqrt(s)+b1/a1)) + a_ = poles[1][b]/poles[1][a] + k_ = 1/poles[0][a]/poles[1][a]/a_*constants + if a_.is_positive: + result = k_*(1-exp(a_**2*t)*erfc(a_*sqrt(t))) + _debug(' rule 5.3.5') + elif ( + poles[0][n] == -1 and poles[0][m] == S.Half and + poles[1][n] == -S.Half and poles[1][m] == 1 and + poles[1][b] == 0): + # 1/((a0*sqrt(s)+b0)*(sqrt(a1*s)) + # == 1/(a0*sqrt(a1)) * 1/((sqrt(s)+b0/a0)"sqrt(s)) + a_ = poles[0][b]/poles[0][a] + k_ = 1/(poles[0][a]*sqrt(poles[1][a]))*constants + if a_.is_positive: + result = k_*exp(a_**2*t)*erfc(a_*sqrt(t)) + _debug(' rule 5.3.7') + elif ( + poles[0][n] == -S(3)/2 and poles[0][m] == 1 and + poles[0][b] == 0 and poles[1][n] == -1 and + poles[1][m] == S.Half): + # 1/((a0**(3/2)*s**(3/2))*(a1*sqrt(s)+b1)) + # == 1/(a0**(3/2)*a1) 1/((s**(3/2))*(sqrt(s)+b1/a1)) + # Note that Bateman54 5.3 (8) is incorrect; there (sqrt(p)+a) + # should be (sqrt(p)+a)**(-1). + a_ = poles[1][b]/poles[1][a] + k_ = 1/(poles[0][a]**(S(3)/2)*poles[1][a])/a_**2*constants + if a_.is_positive: + result = ( + k_*(2/sqrt(pi)*a_*sqrt(t)+exp(a_**2*t)*erfc(a_*sqrt(t))-1)) + _debug(' rule 5.3.8') + elif ( + poles[0][n] == -2 and poles[0][m] == S.Half and + poles[1][n] == -1 and poles[1][m] == 1 and + poles[1][b] == 0): + # 1/((a0*sqrt(s)+b0)**2*a1*s) + # == 1/a0**2/a1 * 1/(sqrt(s)+b0/a0)**2/s + a_sq = poles[0][b]/poles[0][a] + a_ = a_sq**2 + k_ = 1/poles[0][a]**2/poles[1][a]*constants + if a_sq.is_positive: + result = ( + k_*(1/a_ + (2*t-1/a_)*exp(a_*t)*erfc(sqrt(a_)*sqrt(t)) - + 2/sqrt(pi)/sqrt(a_)*sqrt(t))) + _debug(' rule 5.3.11') + elif ( + poles[0][n] == -2 and poles[0][m] == S.Half and + poles[1][n] == -S.Half and poles[1][m] == 1 and + poles[1][b] == 0): + # 1/((a0*sqrt(s)+b0)**2*sqrt(a1*s)) + # == 1/a0**2/sqrt(a1) * 1/(sqrt(s)+b0/a0)**2/sqrt(s) + a_ = poles[0][b]/poles[0][a] + k_ = 1/poles[0][a]**2/sqrt(poles[1][a])*constants + if a_.is_positive: + result = ( + k_*(2/sqrt(pi)*sqrt(t) - + 2*a_*t*exp(a_**2*t)*erfc(a_*sqrt(t)))) + _debug(' rule 5.3.12') + elif ( + poles[0][n] == -3 and poles[0][m] == S.Half and + poles[1][n] == -S.Half and poles[1][m] == 1 and + poles[1][b] == 0): + # 1 / (sqrt(a1*s)*(a0*sqrt(s+b0)**3)) + # == 1/(sqrt(a1)*a0) * 1/(sqrt(s)*(sqrt(s+b0)**3)) + a_ = poles[0][b] + k_ = constants/sqrt(poles[1][a])/poles[0][a] + result = k_*( + (2*a_**2*t+1)*t*exp(a_**2*t)*erfc(a_*sqrt(t)) - + 2/sqrt(pi)*a_*t**(S(3)/2)) + _debug(' rule 5.3.15') + elif ( + poles[0][n] == -1 and poles[0][m] == 1 and + poles[1][n] == -S.Half and poles[1][m] == 1): + # 1 / ( (a0*s+b0)* sqrt(a1*s+b1) ) + # == 1/(sqrt(a1)*a0) * 1 / ( (s+b0/a0)* sqrt(s+b1/a1) ) + a_ = poles[0][b]/poles[0][a] + b_ = poles[1][b]/poles[1][a] + k_ = constants/sqrt(poles[1][a])/poles[0][a] + result = k_*( + 1/sqrt(b_-a_)*exp(-a_*t)*erf(sqrt(b_-a_)*sqrt(t))) + _debug(' rule 5.3.23') + + elif len(poles) == 2 and len(zeros) == 1: + if ( + poles[0][n] == -1 and poles[0][m] == 1 and + poles[1][n] == -1 and poles[1][m] == S.Half and + zeros[0][n] == S.Half and zeros[0][m] == 1 and + zeros[0][b] == 0): + # sqrt(za0*s)/((a0*s+b0)*(a1*sqrt(s)+b1)) + # == sqrt(za0)/(a0*a1) * s/((s+b0/a0)*(sqrt(s)+b1/a1)) + a_sq = poles[1][b]/poles[1][a] + a_ = a_sq**2 + b_ = -poles[0][b]/poles[0][a] + k_ = sqrt(zeros[0][a])/poles[0][a]/poles[1][a]/(a_-b_)*constants + if a_sq.is_positive and b_.is_positive: + result = k_*( + a_*exp(a_*t)*erfc(sqrt(a_)*sqrt(t)) + + sqrt(a_)*sqrt(b_)*exp(b_*t)*erfc(sqrt(b_)*sqrt(t)) - + b_*exp(b_*t)) + _debug(' rule 5.3.6') + elif ( + poles[0][n] == -1 and poles[0][m] == 1 and + poles[0][b] == 0 and poles[1][n] == -1 and + poles[1][m] == S.Half and zeros[0][n] == 1 and + zeros[0][m] == S.Half): + # (az*sqrt(s)+bz)/(a0*s*(a1*sqrt(s)+b1)) + # == az/a0/a1 * (sqrt(z)+bz/az)/(s*(sqrt(s)+b1/a1)) + a_num = zeros[0][b]/zeros[0][a] + a_ = poles[1][b]/poles[1][a] + if a_+a_num == 0: + k_ = zeros[0][a]/poles[0][a]/poles[1][a]*constants + result = k_*( + 2*exp(a_**2*t)*erfc(a_*sqrt(t))-1) + _debug(' rule 5.3.17') + elif ( + poles[1][n] == -1 and poles[1][m] == 1 and + poles[1][b] == 0 and poles[0][n] == -2 and + poles[0][m] == S.Half and zeros[0][n] == 2 and + zeros[0][m] == S.Half): + # (az*sqrt(s)+bz)**2/(a1*s*(a0*sqrt(s)+b0)**2) + # == az**2/a1/a0**2 * (sqrt(z)+bz/az)**2/(s*(sqrt(s)+b0/a0)**2) + a_num = zeros[0][b]/zeros[0][a] + a_ = poles[0][b]/poles[0][a] + if a_+a_num == 0: + k_ = zeros[0][a]**2/poles[1][a]/poles[0][a]**2*constants + result = k_*( + 1 + 8*a_**2*t*exp(a_**2*t)*erfc(a_*sqrt(t)) - + 8/sqrt(pi)*a_*sqrt(t)) + _debug(' rule 5.3.18') + elif ( + poles[1][n] == -1 and poles[1][m] == 1 and + poles[1][b] == 0 and poles[0][n] == -3 and + poles[0][m] == S.Half and zeros[0][n] == 3 and + zeros[0][m] == S.Half): + # (az*sqrt(s)+bz)**3/(a1*s*(a0*sqrt(s)+b0)**3) + # == az**3/a1/a0**3 * (sqrt(z)+bz/az)**3/(s*(sqrt(s)+b0/a0)**3) + a_num = zeros[0][b]/zeros[0][a] + a_ = poles[0][b]/poles[0][a] + if a_+a_num == 0: + k_ = zeros[0][a]**3/poles[1][a]/poles[0][a]**3*constants + result = k_*( + 2*(8*a_**4*t**2+8*a_**2*t+1)*exp(a_**2*t) * + erfc(a_*sqrt(t))-8/sqrt(pi)*a_*sqrt(t)*(2*a_**2*t+1)-1) + _debug(' rule 5.3.19') + + elif len(poles) == 3 and len(zeros) == 0: + if ( + poles[0][n] == -1 and poles[0][b] == 0 and poles[0][m] == 1 and + poles[1][n] == -1 and poles[1][m] == 1 and + poles[2][n] == -S.Half and poles[2][m] == 1): + # 1/((a0*s)*(a1*s+b1)*sqrt(a2*s)) + # == 1/(a0*a1*sqrt(a2)) * 1/((s)*(s+b1/a1)*sqrt(s)) + a_ = -poles[1][b]/poles[1][a] + k_ = 1/poles[0][a]/poles[1][a]/sqrt(poles[2][a])*constants + if a_.is_positive: + result = k_ * ( + a_**(-S(3)/2) * exp(a_*t) * erf(sqrt(a_)*sqrt(t)) - + 2/a_/sqrt(pi)*sqrt(t)) + _debug(' rule 5.3.2') + elif ( + poles[0][n] == -1 and poles[0][m] == 1 and + poles[1][n] == -1 and poles[1][m] == S.Half and + poles[2][n] == -S.Half and poles[2][m] == 1 and + poles[2][b] == 0): + # 1/((a0*s+b0)*(a1*sqrt(s)+b1)*(sqrt(a2)*sqrt(s))) + # == 1/(a0*a1*sqrt(a2)) * 1/((s+b0/a0)*(sqrt(s)+b1/a1)*sqrt(s)) + a_sq = poles[1][b]/poles[1][a] + a_ = a_sq**2 + b_ = -poles[0][b]/poles[0][a] + k_ = ( + 1/poles[0][a]/poles[1][a]/sqrt(poles[2][a]) / + (sqrt(b_)*(a_-b_))) + if a_sq.is_positive and b_.is_positive: + result = k_ * ( + sqrt(b_)*exp(a_*t)*erfc(sqrt(a_)*sqrt(t)) + + sqrt(a_)*exp(b_*t)*erf(sqrt(b_)*sqrt(t)) - + sqrt(b_)*exp(b_*t)) + _debug(' rule 5.3.9') + + if result is None: + return None + else: + return Heaviside(t)*result, condition + + +@DEBUG_WRAP +def _inverse_laplace_early_prog_rules(F, s, t, plane): + """ + Helper function for the class InverseLaplaceTransform. + """ + prog_rules = [_inverse_laplace_irrational] + + for p_rule in prog_rules: + if (r := p_rule(F, s, t, plane)) is not None: + return r + return None + + +@DEBUG_WRAP +def _inverse_laplace_apply_prog_rules(F, s, t, plane): + """ + Helper function for the class InverseLaplaceTransform. + """ + prog_rules = [_inverse_laplace_time_shift, _inverse_laplace_freq_shift, + _inverse_laplace_time_diff, _inverse_laplace_diff, + _inverse_laplace_irrational] + + for p_rule in prog_rules: + if (r := p_rule(F, s, t, plane)) is not None: + return r + return None + + +@DEBUG_WRAP +def _inverse_laplace_expand(fn, s, t, plane): + """ + Helper function for the class InverseLaplaceTransform. + """ + if fn.is_Add: + return None + r = expand(fn, deep=False) + if r.is_Add: + return _inverse_laplace_transform( + r, s, t, plane, simplify=False, dorational=True) + r = expand_mul(fn) + if r.is_Add: + return _inverse_laplace_transform( + r, s, t, plane, simplify=False, dorational=True) + r = expand(fn) + if r.is_Add: + return _inverse_laplace_transform( + r, s, t, plane, simplify=False, dorational=True) + if fn.is_rational_function(s): + r = fn.apart(s).doit() + if r.is_Add: + return _inverse_laplace_transform( + r, s, t, plane, simplify=False, dorational=True) + return None + + +@DEBUG_WRAP +def _inverse_laplace_rational(fn, s, t, plane, *, simplify): + """ + Helper function for the class InverseLaplaceTransform. + """ + x_ = symbols('x_') + f = fn.apart(s) + terms = Add.make_args(f) + terms_t = [] + conditions = [S.true] + for term in terms: + [n, d] = term.as_numer_denom() + dc = d.as_poly(s).all_coeffs() + dc_lead = dc[0] + dc = [x/dc_lead for x in dc] + nc = [x/dc_lead for x in n.as_poly(s).all_coeffs()] + if len(dc) == 1: + N = len(nc)-1 + for c in enumerate(nc): + r = c[1]*DiracDelta(t, N-c[0]) + terms_t.append(r) + elif len(dc) == 2: + r = nc[0]*exp(-dc[1]*t) + terms_t.append(Heaviside(t)*r) + elif len(dc) == 3: + a = dc[1]/2 + b = (dc[2]-a**2).factor() + if len(nc) == 1: + nc = [S.Zero] + nc + l, m = tuple(nc) + if b == 0: + r = (m*t+l*(1-a*t))*exp(-a*t) + else: + hyp = False + if b.is_negative: + b = -b + hyp = True + b2 = list(roots(x_**2-b, x_).keys())[0] + bs = sqrt(b).simplify() + if hyp: + r = ( + l*exp(-a*t)*cosh(b2*t) + (m-a*l) / + bs*exp(-a*t)*sinh(bs*t)) + else: + r = l*exp(-a*t)*cos(b2*t) + (m-a*l)/bs*exp(-a*t)*sin(bs*t) + terms_t.append(Heaviside(t)*r) + else: + ft, cond = _inverse_laplace_transform( + term, s, t, plane, simplify=simplify, dorational=False) + terms_t.append(ft) + conditions.append(cond) + + result = Add(*terms_t) + if simplify: + result = result.simplify(doit=False) + return result, And(*conditions) + + +@DEBUG_WRAP +def _inverse_laplace_transform(fn, s_, t_, plane, *, simplify, dorational): + """ + Front-end function of the inverse Laplace transform. It tries to apply all + known rules recursively. If everything else fails, it tries to integrate. + """ + terms = Add.make_args(fn) + terms_t = [] + conditions = [] + + for term in terms: + if term.has(exp): + # Simplify expressions with exp() such that time-shifted + # expressions have negative exponents in the numerator instead of + # positive exponents in the numerator and denominator; this is a + # (necessary) trick. It will, for example, convert + # (s**2*exp(2*s) + 4*exp(s) - 4)*exp(-2*s)/(s*(s**2 + 1)) into + # (s**2 + 4*exp(-s) - 4*exp(-2*s))/(s*(s**2 + 1)) + term = term.subs(s_, -s_).together().subs(s_, -s_) + k, f = term.as_independent(s_, as_Add=False) + if ( + dorational and term.is_rational_function(s_) and + (r := _inverse_laplace_rational( + f, s_, t_, plane, simplify=simplify)) + is not None or + (r := _inverse_laplace_apply_simple_rules(f, s_, t_)) + is not None or + (r := _inverse_laplace_early_prog_rules(f, s_, t_, plane)) + is not None or + (r := _inverse_laplace_expand(f, s_, t_, plane)) + is not None or + (r := _inverse_laplace_apply_prog_rules(f, s_, t_, plane)) + is not None): + pass + elif any(undef.has(s_) for undef in f.atoms(AppliedUndef)): + # If there are undefined functions f(t) then integration is + # unlikely to do anything useful so we skip it and given an + # unevaluated LaplaceTransform. + r = (InverseLaplaceTransform(f, s_, t_, plane), S.true) + elif ( + r := _inverse_laplace_transform_integration( + f, s_, t_, plane, simplify=simplify)) is not None: + pass + else: + r = (InverseLaplaceTransform(f, s_, t_, plane), S.true) + (ri_, ci_) = r + terms_t.append(k*ri_) + conditions.append(ci_) + + result = Add(*terms_t) + if simplify: + result = result.simplify(doit=False) + condition = And(*conditions) + + return result, condition + + +class InverseLaplaceTransform(IntegralTransform): + """ + Class representing unevaluated inverse Laplace transforms. + + For usage of this class, see the :class:`IntegralTransform` docstring. + + For how to compute inverse Laplace transforms, see the + :func:`inverse_laplace_transform` docstring. + """ + + _name = 'Inverse Laplace' + _none_sentinel = Dummy('None') + _c = Dummy('c') + + def __new__(cls, F, s, x, plane, **opts): + if plane is None: + plane = InverseLaplaceTransform._none_sentinel + return IntegralTransform.__new__(cls, F, s, x, plane, **opts) + + @property + def fundamental_plane(self): + plane = self.args[3] + if plane is InverseLaplaceTransform._none_sentinel: + plane = None + return plane + + def _compute_transform(self, F, s, t, **hints): + return _inverse_laplace_transform_integration( + F, s, t, self.fundamental_plane, **hints) + + def _as_integral(self, F, s, t): + c = self.__class__._c + return ( + Integral(exp(s*t)*F, (s, c - S.ImaginaryUnit*S.Infinity, + c + S.ImaginaryUnit*S.Infinity)) / + (2*S.Pi*S.ImaginaryUnit)) + + def doit(self, **hints): + """ + Try to evaluate the transform in closed form. + + Explanation + =========== + + Standard hints are the following: + - ``noconds``: if True, do not return convergence conditions. The + default setting is `True`. + - ``simplify``: if True, it simplifies the final result. The + default setting is `False`. + """ + _noconds = hints.get('noconds', True) + _simplify = hints.get('simplify', False) + + debugf('[ILT doit] (%s, %s, %s)', (self.function, + self.function_variable, + self.transform_variable)) + + s_ = self.function_variable + t_ = self.transform_variable + fn = self.function + plane = self.fundamental_plane + + r = _inverse_laplace_transform( + fn, s_, t_, plane, simplify=_simplify, dorational=True) + + if _noconds: + return r[0] + else: + return r + + +def inverse_laplace_transform(F, s, t, plane=None, **hints): + r""" + Compute the inverse Laplace transform of `F(s)`, defined as + + .. math :: + f(t) = \frac{1}{2\pi i} \int_{c-i\infty}^{c+i\infty} e^{st} + F(s) \mathrm{d}s, + + for `c` so large that `F(s)` has no singularites in the + half-plane `\operatorname{Re}(s) > c-\epsilon`. + + Explanation + =========== + + The plane can be specified by + argument ``plane``, but will be inferred if passed as None. + + Under certain regularity conditions, this recovers `f(t)` from its + Laplace Transform `F(s)`, for non-negative `t`, and vice + versa. + + If the integral cannot be computed in closed form, this function returns + an unevaluated :class:`InverseLaplaceTransform` object. + + Note that this function will always assume `t` to be real, + regardless of the SymPy assumption on `t`. + + For a description of possible hints, refer to the docstring of + :func:`sympy.integrals.transforms.IntegralTransform.doit`. + + Examples + ======== + + >>> from sympy import inverse_laplace_transform, exp, Symbol + >>> from sympy.abc import s, t + >>> a = Symbol('a', positive=True) + >>> inverse_laplace_transform(exp(-a*s)/s, s, t) + Heaviside(-a + t) + + See Also + ======== + + laplace_transform + hankel_transform, inverse_hankel_transform + """ + _noconds = hints.get('noconds', True) + _simplify = hints.get('simplify', False) + + if isinstance(F, MatrixBase) and hasattr(F, 'applyfunc'): + return F.applyfunc( + lambda Fij: inverse_laplace_transform(Fij, s, t, plane, **hints)) + + r, c = InverseLaplaceTransform(F, s, t, plane).doit( + noconds=False, simplify=_simplify) + + if _noconds: + return r + else: + return r, c + + +def _fast_inverse_laplace(e, s, t): + """Fast inverse Laplace transform of rational function including RootSum""" + a, b, n = symbols('a, b, n', cls=Wild, exclude=[s]) + + def _ilt(e): + if not e.has(s): + return e + elif e.is_Add: + return _ilt_add(e) + elif e.is_Mul: + return _ilt_mul(e) + elif e.is_Pow: + return _ilt_pow(e) + elif isinstance(e, RootSum): + return _ilt_rootsum(e) + else: + raise NotImplementedError + + def _ilt_add(e): + return e.func(*map(_ilt, e.args)) + + def _ilt_mul(e): + coeff, expr = e.as_independent(s) + if expr.is_Mul: + raise NotImplementedError + return coeff * _ilt(expr) + + def _ilt_pow(e): + match = e.match((a*s + b)**n) + if match is not None: + nm, am, bm = match[n], match[a], match[b] + if nm.is_Integer and nm < 0: + return t**(-nm-1)*exp(-(bm/am)*t)/(am**-nm*gamma(-nm)) + if nm == 1: + return exp(-(bm/am)*t) / am + raise NotImplementedError + + def _ilt_rootsum(e): + expr = e.fun.expr + [variable] = e.fun.variables + return RootSum(e.poly, Lambda(variable, together(_ilt(expr)))) + + return _ilt(e) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/manualintegrate.py b/venv/lib/python3.11/site-packages/sympy/integrals/manualintegrate.py new file mode 100644 index 0000000000000000000000000000000000000000..2908fb33003ba9c22da47e550edcfea2b41a26a0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/manualintegrate.py @@ -0,0 +1,2174 @@ +"""Integration method that emulates by-hand techniques. + +This module also provides functionality to get the steps used to evaluate a +particular integral, in the ``integral_steps`` function. This will return +nested ``Rule`` s representing the integration rules used. + +Each ``Rule`` class represents a (maybe parametrized) integration rule, e.g. +``SinRule`` for integrating ``sin(x)`` and ``ReciprocalSqrtQuadraticRule`` +for integrating ``1/sqrt(a+b*x+c*x**2)``. The ``eval`` method returns the +integration result. + +The ``manualintegrate`` function computes the integral by calling ``eval`` +on the rule returned by ``integral_steps``. + +The integrator can be extended with new heuristics and evaluation +techniques. To do so, extend the ``Rule`` class, implement ``eval`` method, +then write a function that accepts an ``IntegralInfo`` object and returns +either a ``Rule`` instance or ``None``. If the new technique requires a new +match, add the key and call to the antiderivative function to integral_steps. +To enable simple substitutions, add the match to find_substitutions. + +""" + +from __future__ import annotations +from typing import NamedTuple, Type, Callable, Sequence +from abc import ABC, abstractmethod +from dataclasses import dataclass +from collections import defaultdict +from collections.abc import Mapping + +from sympy.core.add import Add +from sympy.core.cache import cacheit +from sympy.core.containers import Dict +from sympy.core.expr import Expr +from sympy.core.function import Derivative +from sympy.core.logic import fuzzy_not +from sympy.core.mul import Mul +from sympy.core.numbers import Integer, Number, E +from sympy.core.power import Pow +from sympy.core.relational import Eq, Ne, Boolean +from sympy.core.singleton import S +from sympy.core.symbol import Dummy, Symbol, Wild +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.hyperbolic import (HyperbolicFunction, csch, + cosh, coth, sech, sinh, tanh, asinh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (TrigonometricFunction, + cos, sin, tan, cot, csc, sec, acos, asin, atan, acot, acsc, asec) +from sympy.functions.special.delta_functions import Heaviside, DiracDelta +from sympy.functions.special.error_functions import (erf, erfi, fresnelc, + fresnels, Ci, Chi, Si, Shi, Ei, li) +from sympy.functions.special.gamma_functions import uppergamma +from sympy.functions.special.elliptic_integrals import elliptic_e, elliptic_f +from sympy.functions.special.polynomials import (chebyshevt, chebyshevu, + legendre, hermite, laguerre, assoc_laguerre, gegenbauer, jacobi, + OrthogonalPolynomial) +from sympy.functions.special.zeta_functions import polylog +from .integrals import Integral +from sympy.logic.boolalg import And +from sympy.ntheory.factor_ import primefactors +from sympy.polys.polytools import degree, lcm_list, gcd_list, Poly +from sympy.simplify.radsimp import fraction +from sympy.simplify.simplify import simplify +from sympy.solvers.solvers import solve +from sympy.strategies.core import switch, do_one, null_safe, condition +from sympy.utilities.iterables import iterable +from sympy.utilities.misc import debug + + +@dataclass +class Rule(ABC): + integrand: Expr + variable: Symbol + + @abstractmethod + def eval(self) -> Expr: + pass + + @abstractmethod + def contains_dont_know(self) -> bool: + pass + + +@dataclass +class AtomicRule(Rule, ABC): + """A simple rule that does not depend on other rules""" + def contains_dont_know(self) -> bool: + return False + + +@dataclass +class ConstantRule(AtomicRule): + """integrate(a, x) -> a*x""" + def eval(self) -> Expr: + return self.integrand * self.variable + + +@dataclass +class ConstantTimesRule(Rule): + """integrate(a*f(x), x) -> a*integrate(f(x), x)""" + constant: Expr + other: Expr + substep: Rule + + def eval(self) -> Expr: + return self.constant * self.substep.eval() + + def contains_dont_know(self) -> bool: + return self.substep.contains_dont_know() + + +@dataclass +class PowerRule(AtomicRule): + """integrate(x**a, x)""" + base: Expr + exp: Expr + + def eval(self) -> Expr: + return Piecewise( + ((self.base**(self.exp + 1))/(self.exp + 1), Ne(self.exp, -1)), + (log(self.base), True), + ) + + +@dataclass +class NestedPowRule(AtomicRule): + """integrate((x**a)**b, x)""" + base: Expr + exp: Expr + + def eval(self) -> Expr: + m = self.base * self.integrand + return Piecewise((m / (self.exp + 1), Ne(self.exp, -1)), + (m * log(self.base), True)) + + +@dataclass +class AddRule(Rule): + """integrate(f(x) + g(x), x) -> integrate(f(x), x) + integrate(g(x), x)""" + substeps: list[Rule] + + def eval(self) -> Expr: + return Add(*(substep.eval() for substep in self.substeps)) + + def contains_dont_know(self) -> bool: + return any(substep.contains_dont_know() for substep in self.substeps) + + +@dataclass +class URule(Rule): + """integrate(f(g(x))*g'(x), x) -> integrate(f(u), u), u = g(x)""" + u_var: Symbol + u_func: Expr + substep: Rule + + def eval(self) -> Expr: + result = self.substep.eval() + if self.u_func.is_Pow: + base, exp_ = self.u_func.as_base_exp() + if exp_ == -1: + # avoid needless -log(1/x) from substitution + result = result.subs(log(self.u_var), -log(base)) + return result.subs(self.u_var, self.u_func) + + def contains_dont_know(self) -> bool: + return self.substep.contains_dont_know() + + +@dataclass +class PartsRule(Rule): + """integrate(u(x)*v'(x), x) -> u(x)*v(x) - integrate(u'(x)*v(x), x)""" + u: Symbol + dv: Expr + v_step: Rule + second_step: Rule | None # None when is a substep of CyclicPartsRule + + def eval(self) -> Expr: + assert self.second_step is not None + v = self.v_step.eval() + return self.u * v - self.second_step.eval() + + def contains_dont_know(self) -> bool: + return self.v_step.contains_dont_know() or ( + self.second_step is not None and self.second_step.contains_dont_know()) + + +@dataclass +class CyclicPartsRule(Rule): + """Apply PartsRule multiple times to integrate exp(x)*sin(x)""" + parts_rules: list[PartsRule] + coefficient: Expr + + def eval(self) -> Expr: + result = [] + sign = 1 + for rule in self.parts_rules: + result.append(sign * rule.u * rule.v_step.eval()) + sign *= -1 + return Add(*result) / (1 - self.coefficient) + + def contains_dont_know(self) -> bool: + return any(substep.contains_dont_know() for substep in self.parts_rules) + + +@dataclass +class TrigRule(AtomicRule, ABC): + pass + + +@dataclass +class SinRule(TrigRule): + """integrate(sin(x), x) -> -cos(x)""" + def eval(self) -> Expr: + return -cos(self.variable) + + +@dataclass +class CosRule(TrigRule): + """integrate(cos(x), x) -> sin(x)""" + def eval(self) -> Expr: + return sin(self.variable) + + +@dataclass +class SecTanRule(TrigRule): + """integrate(sec(x)*tan(x), x) -> sec(x)""" + def eval(self) -> Expr: + return sec(self.variable) + + +@dataclass +class CscCotRule(TrigRule): + """integrate(csc(x)*cot(x), x) -> -csc(x)""" + def eval(self) -> Expr: + return -csc(self.variable) + + +@dataclass +class Sec2Rule(TrigRule): + """integrate(sec(x)**2, x) -> tan(x)""" + def eval(self) -> Expr: + return tan(self.variable) + + +@dataclass +class Csc2Rule(TrigRule): + """integrate(csc(x)**2, x) -> -cot(x)""" + def eval(self) -> Expr: + return -cot(self.variable) + + +@dataclass +class HyperbolicRule(AtomicRule, ABC): + pass + + +@dataclass +class SinhRule(HyperbolicRule): + """integrate(sinh(x), x) -> cosh(x)""" + def eval(self) -> Expr: + return cosh(self.variable) + + +@dataclass +class CoshRule(HyperbolicRule): + """integrate(cosh(x), x) -> sinh(x)""" + def eval(self): + return sinh(self.variable) + + +@dataclass +class ExpRule(AtomicRule): + """integrate(a**x, x) -> a**x/ln(a)""" + base: Expr + exp: Expr + + def eval(self) -> Expr: + return self.integrand / log(self.base) + + +@dataclass +class ReciprocalRule(AtomicRule): + """integrate(1/x, x) -> ln(x)""" + base: Expr + + def eval(self) -> Expr: + return log(self.base) + + +@dataclass +class ArcsinRule(AtomicRule): + """integrate(1/sqrt(1-x**2), x) -> asin(x)""" + def eval(self) -> Expr: + return asin(self.variable) + + +@dataclass +class ArcsinhRule(AtomicRule): + """integrate(1/sqrt(1+x**2), x) -> asin(x)""" + def eval(self) -> Expr: + return asinh(self.variable) + + +@dataclass +class ReciprocalSqrtQuadraticRule(AtomicRule): + """integrate(1/sqrt(a+b*x+c*x**2), x) -> log(2*sqrt(c)*sqrt(a+b*x+c*x**2)+b+2*c*x)/sqrt(c)""" + a: Expr + b: Expr + c: Expr + + def eval(self) -> Expr: + a, b, c, x = self.a, self.b, self.c, self.variable + return log(2*sqrt(c)*sqrt(a+b*x+c*x**2)+b+2*c*x)/sqrt(c) + + +@dataclass +class SqrtQuadraticDenomRule(AtomicRule): + """integrate(poly(x)/sqrt(a+b*x+c*x**2), x)""" + a: Expr + b: Expr + c: Expr + coeffs: list[Expr] + + def eval(self) -> Expr: + a, b, c, coeffs, x = self.a, self.b, self.c, self.coeffs.copy(), self.variable + # Integrate poly/sqrt(a+b*x+c*x**2) using recursion. + # coeffs are coefficients of the polynomial. + # Let I_n = x**n/sqrt(a+b*x+c*x**2), then + # I_n = A * x**(n-1)*sqrt(a+b*x+c*x**2) - B * I_{n-1} - C * I_{n-2} + # where A = 1/(n*c), B = (2*n-1)*b/(2*n*c), C = (n-1)*a/(n*c) + # See https://github.com/sympy/sympy/pull/23608 for proof. + result_coeffs = [] + coeffs = coeffs.copy() + for i in range(len(coeffs)-2): + n = len(coeffs)-1-i + coeff = coeffs[i]/(c*n) + result_coeffs.append(coeff) + coeffs[i+1] -= (2*n-1)*b/2*coeff + coeffs[i+2] -= (n-1)*a*coeff + d, e = coeffs[-1], coeffs[-2] + s = sqrt(a+b*x+c*x**2) + constant = d-b*e/(2*c) + if constant == 0: + I0 = 0 + else: + step = inverse_trig_rule(IntegralInfo(1/s, x), degenerate=False) + I0 = constant*step.eval() + return Add(*(result_coeffs[i]*x**(len(coeffs)-2-i) + for i in range(len(result_coeffs))), e/c)*s + I0 + + +@dataclass +class SqrtQuadraticRule(AtomicRule): + """integrate(sqrt(a+b*x+c*x**2), x)""" + a: Expr + b: Expr + c: Expr + + def eval(self) -> Expr: + step = sqrt_quadratic_rule(IntegralInfo(self.integrand, self.variable), degenerate=False) + return step.eval() + + +@dataclass +class AlternativeRule(Rule): + """Multiple ways to do integration.""" + alternatives: list[Rule] + + def eval(self) -> Expr: + return self.alternatives[0].eval() + + def contains_dont_know(self) -> bool: + return any(substep.contains_dont_know() for substep in self.alternatives) + + +@dataclass +class DontKnowRule(Rule): + """Leave the integral as is.""" + def eval(self) -> Expr: + return Integral(self.integrand, self.variable) + + def contains_dont_know(self) -> bool: + return True + + +@dataclass +class DerivativeRule(AtomicRule): + """integrate(f'(x), x) -> f(x)""" + def eval(self) -> Expr: + assert isinstance(self.integrand, Derivative) + variable_count = list(self.integrand.variable_count) + for i, (var, count) in enumerate(variable_count): + if var == self.variable: + variable_count[i] = (var, count - 1) + break + return Derivative(self.integrand.expr, *variable_count) + + +@dataclass +class RewriteRule(Rule): + """Rewrite integrand to another form that is easier to handle.""" + rewritten: Expr + substep: Rule + + def eval(self) -> Expr: + return self.substep.eval() + + def contains_dont_know(self) -> bool: + return self.substep.contains_dont_know() + + +@dataclass +class CompleteSquareRule(RewriteRule): + """Rewrite a+b*x+c*x**2 to a-b**2/(4*c) + c*(x+b/(2*c))**2""" + pass + + +@dataclass +class PiecewiseRule(Rule): + subfunctions: Sequence[tuple[Rule, bool | Boolean]] + + def eval(self) -> Expr: + return Piecewise(*[(substep.eval(), cond) + for substep, cond in self.subfunctions]) + + def contains_dont_know(self) -> bool: + return any(substep.contains_dont_know() for substep, _ in self.subfunctions) + + +@dataclass +class HeavisideRule(Rule): + harg: Expr + ibnd: Expr + substep: Rule + + def eval(self) -> Expr: + # If we are integrating over x and the integrand has the form + # Heaviside(m*x+b)*g(x) == Heaviside(harg)*g(symbol) + # then there needs to be continuity at -b/m == ibnd, + # so we subtract the appropriate term. + result = self.substep.eval() + return Heaviside(self.harg) * (result - result.subs(self.variable, self.ibnd)) + + def contains_dont_know(self) -> bool: + return self.substep.contains_dont_know() + + +@dataclass +class DiracDeltaRule(AtomicRule): + n: Expr + a: Expr + b: Expr + + def eval(self) -> Expr: + n, a, b, x = self.n, self.a, self.b, self.variable + if n == 0: + return Heaviside(a+b*x)/b + return DiracDelta(a+b*x, n-1)/b + + +@dataclass +class TrigSubstitutionRule(Rule): + theta: Expr + func: Expr + rewritten: Expr + substep: Rule + restriction: bool | Boolean + + def eval(self) -> Expr: + theta, func, x = self.theta, self.func, self.variable + func = func.subs(sec(theta), 1/cos(theta)) + func = func.subs(csc(theta), 1/sin(theta)) + func = func.subs(cot(theta), 1/tan(theta)) + + trig_function = list(func.find(TrigonometricFunction)) + assert len(trig_function) == 1 + trig_function = trig_function[0] + relation = solve(x - func, trig_function) + assert len(relation) == 1 + numer, denom = fraction(relation[0]) + + if isinstance(trig_function, sin): + opposite = numer + hypotenuse = denom + adjacent = sqrt(denom**2 - numer**2) + inverse = asin(relation[0]) + elif isinstance(trig_function, cos): + adjacent = numer + hypotenuse = denom + opposite = sqrt(denom**2 - numer**2) + inverse = acos(relation[0]) + else: # tan + opposite = numer + adjacent = denom + hypotenuse = sqrt(denom**2 + numer**2) + inverse = atan(relation[0]) + + substitution = [ + (sin(theta), opposite/hypotenuse), + (cos(theta), adjacent/hypotenuse), + (tan(theta), opposite/adjacent), + (theta, inverse) + ] + return Piecewise( + (self.substep.eval().subs(substitution).trigsimp(), self.restriction) # type: ignore + ) + + def contains_dont_know(self) -> bool: + return self.substep.contains_dont_know() + + +@dataclass +class ArctanRule(AtomicRule): + """integrate(a/(b*x**2+c), x) -> a/b / sqrt(c/b) * atan(x/sqrt(c/b))""" + a: Expr + b: Expr + c: Expr + + def eval(self) -> Expr: + a, b, c, x = self.a, self.b, self.c, self.variable + return a/b / sqrt(c/b) * atan(x/sqrt(c/b)) + + +@dataclass +class OrthogonalPolyRule(AtomicRule, ABC): + n: Expr + + +@dataclass +class JacobiRule(OrthogonalPolyRule): + a: Expr + b: Expr + + def eval(self) -> Expr: + n, a, b, x = self.n, self.a, self.b, self.variable + return Piecewise( + (2*jacobi(n + 1, a - 1, b - 1, x)/(n + a + b), Ne(n + a + b, 0)), + (x, Eq(n, 0)), + ((a + b + 2)*x**2/4 + (a - b)*x/2, Eq(n, 1))) + + +@dataclass +class GegenbauerRule(OrthogonalPolyRule): + a: Expr + + def eval(self) -> Expr: + n, a, x = self.n, self.a, self.variable + return Piecewise( + (gegenbauer(n + 1, a - 1, x)/(2*(a - 1)), Ne(a, 1)), + (chebyshevt(n + 1, x)/(n + 1), Ne(n, -1)), + (S.Zero, True)) + + +@dataclass +class ChebyshevTRule(OrthogonalPolyRule): + def eval(self) -> Expr: + n, x = self.n, self.variable + return Piecewise( + ((chebyshevt(n + 1, x)/(n + 1) - + chebyshevt(n - 1, x)/(n - 1))/2, Ne(Abs(n), 1)), + (x**2/2, True)) + + +@dataclass +class ChebyshevURule(OrthogonalPolyRule): + def eval(self) -> Expr: + n, x = self.n, self.variable + return Piecewise( + (chebyshevt(n + 1, x)/(n + 1), Ne(n, -1)), + (S.Zero, True)) + + +@dataclass +class LegendreRule(OrthogonalPolyRule): + def eval(self) -> Expr: + n, x = self.n, self.variable + return(legendre(n + 1, x) - legendre(n - 1, x))/(2*n + 1) + + +@dataclass +class HermiteRule(OrthogonalPolyRule): + def eval(self) -> Expr: + n, x = self.n, self.variable + return hermite(n + 1, x)/(2*(n + 1)) + + +@dataclass +class LaguerreRule(OrthogonalPolyRule): + def eval(self) -> Expr: + n, x = self.n, self.variable + return laguerre(n, x) - laguerre(n + 1, x) + + +@dataclass +class AssocLaguerreRule(OrthogonalPolyRule): + a: Expr + + def eval(self) -> Expr: + return -assoc_laguerre(self.n + 1, self.a - 1, self.variable) + + +@dataclass +class IRule(AtomicRule, ABC): + a: Expr + b: Expr + + +@dataclass +class CiRule(IRule): + def eval(self) -> Expr: + a, b, x = self.a, self.b, self.variable + return cos(b)*Ci(a*x) - sin(b)*Si(a*x) + + +@dataclass +class ChiRule(IRule): + def eval(self) -> Expr: + a, b, x = self.a, self.b, self.variable + return cosh(b)*Chi(a*x) + sinh(b)*Shi(a*x) + + +@dataclass +class EiRule(IRule): + def eval(self) -> Expr: + a, b, x = self.a, self.b, self.variable + return exp(b)*Ei(a*x) + + +@dataclass +class SiRule(IRule): + def eval(self) -> Expr: + a, b, x = self.a, self.b, self.variable + return sin(b)*Ci(a*x) + cos(b)*Si(a*x) + + +@dataclass +class ShiRule(IRule): + def eval(self) -> Expr: + a, b, x = self.a, self.b, self.variable + return sinh(b)*Chi(a*x) + cosh(b)*Shi(a*x) + + +@dataclass +class LiRule(IRule): + def eval(self) -> Expr: + a, b, x = self.a, self.b, self.variable + return li(a*x + b)/a + + +@dataclass +class ErfRule(AtomicRule): + a: Expr + b: Expr + c: Expr + + def eval(self) -> Expr: + a, b, c, x = self.a, self.b, self.c, self.variable + if a.is_extended_real: + return Piecewise( + (sqrt(S.Pi)/sqrt(-a)/2 * exp(c - b**2/(4*a)) * + erf((-2*a*x - b)/(2*sqrt(-a))), a < 0), + (sqrt(S.Pi)/sqrt(a)/2 * exp(c - b**2/(4*a)) * + erfi((2*a*x + b)/(2*sqrt(a))), True)) + return sqrt(S.Pi)/sqrt(a)/2 * exp(c - b**2/(4*a)) * \ + erfi((2*a*x + b)/(2*sqrt(a))) + + +@dataclass +class FresnelCRule(AtomicRule): + a: Expr + b: Expr + c: Expr + + def eval(self) -> Expr: + a, b, c, x = self.a, self.b, self.c, self.variable + return sqrt(S.Pi)/sqrt(2*a) * ( + cos(b**2/(4*a) - c)*fresnelc((2*a*x + b)/sqrt(2*a*S.Pi)) + + sin(b**2/(4*a) - c)*fresnels((2*a*x + b)/sqrt(2*a*S.Pi))) + + +@dataclass +class FresnelSRule(AtomicRule): + a: Expr + b: Expr + c: Expr + + def eval(self) -> Expr: + a, b, c, x = self.a, self.b, self.c, self.variable + return sqrt(S.Pi)/sqrt(2*a) * ( + cos(b**2/(4*a) - c)*fresnels((2*a*x + b)/sqrt(2*a*S.Pi)) - + sin(b**2/(4*a) - c)*fresnelc((2*a*x + b)/sqrt(2*a*S.Pi))) + + +@dataclass +class PolylogRule(AtomicRule): + a: Expr + b: Expr + + def eval(self) -> Expr: + return polylog(self.b + 1, self.a * self.variable) + + +@dataclass +class UpperGammaRule(AtomicRule): + a: Expr + e: Expr + + def eval(self) -> Expr: + a, e, x = self.a, self.e, self.variable + return x**e * (-a*x)**(-e) * uppergamma(e + 1, -a*x)/a + + +@dataclass +class EllipticFRule(AtomicRule): + a: Expr + d: Expr + + def eval(self) -> Expr: + return elliptic_f(self.variable, self.d/self.a)/sqrt(self.a) + + +@dataclass +class EllipticERule(AtomicRule): + a: Expr + d: Expr + + def eval(self) -> Expr: + return elliptic_e(self.variable, self.d/self.a)*sqrt(self.a) + + +class IntegralInfo(NamedTuple): + integrand: Expr + symbol: Symbol + + +def manual_diff(f, symbol): + """Derivative of f in form expected by find_substitutions + + SymPy's derivatives for some trig functions (like cot) are not in a form + that works well with finding substitutions; this replaces the + derivatives for those particular forms with something that works better. + + """ + if f.args: + arg = f.args[0] + if isinstance(f, tan): + return arg.diff(symbol) * sec(arg)**2 + elif isinstance(f, cot): + return -arg.diff(symbol) * csc(arg)**2 + elif isinstance(f, sec): + return arg.diff(symbol) * sec(arg) * tan(arg) + elif isinstance(f, csc): + return -arg.diff(symbol) * csc(arg) * cot(arg) + elif isinstance(f, Add): + return sum(manual_diff(arg, symbol) for arg in f.args) + elif isinstance(f, Mul): + if len(f.args) == 2 and isinstance(f.args[0], Number): + return f.args[0] * manual_diff(f.args[1], symbol) + return f.diff(symbol) + +def manual_subs(expr, *args): + """ + A wrapper for `expr.subs(*args)` with additional logic for substitution + of invertible functions. + """ + if len(args) == 1: + sequence = args[0] + if isinstance(sequence, (Dict, Mapping)): + sequence = sequence.items() + elif not iterable(sequence): + raise ValueError("Expected an iterable of (old, new) pairs") + elif len(args) == 2: + sequence = [args] + else: + raise ValueError("subs accepts either 1 or 2 arguments") + + new_subs = [] + for old, new in sequence: + if isinstance(old, log): + # If log(x) = y, then exp(a*log(x)) = exp(a*y) + # that is, x**a = exp(a*y). Replace nontrivial powers of x + # before subs turns them into `exp(y)**a`, but + # do not replace x itself yet, to avoid `log(exp(y))`. + x0 = old.args[0] + expr = expr.replace(lambda x: x.is_Pow and x.base == x0, + lambda x: exp(x.exp*new)) + new_subs.append((x0, exp(new))) + + return expr.subs(list(sequence) + new_subs) + +# Method based on that on SIN, described in "Symbolic Integration: The +# Stormy Decade" + +inverse_trig_functions = (atan, asin, acos, acot, acsc, asec) + + +def find_substitutions(integrand, symbol, u_var): + results = [] + + def test_subterm(u, u_diff): + if u_diff == 0: + return False + substituted = integrand / u_diff + debug("substituted: {}, u: {}, u_var: {}".format(substituted, u, u_var)) + substituted = manual_subs(substituted, u, u_var).cancel() + + if substituted.has_free(symbol): + return False + # avoid increasing the degree of a rational function + if integrand.is_rational_function(symbol) and substituted.is_rational_function(u_var): + deg_before = max(degree(t, symbol) for t in integrand.as_numer_denom()) + deg_after = max(degree(t, u_var) for t in substituted.as_numer_denom()) + if deg_after > deg_before: + return False + return substituted.as_independent(u_var, as_Add=False) + + def exp_subterms(term: Expr): + linear_coeffs = [] + terms = [] + n = Wild('n', properties=[lambda n: n.is_Integer]) + for exp_ in term.find(exp): + arg = exp_.args[0] + if symbol not in arg.free_symbols: + continue + match = arg.match(n*symbol) + if match: + linear_coeffs.append(match[n]) + else: + terms.append(exp_) + if linear_coeffs: + terms.append(exp(gcd_list(linear_coeffs)*symbol)) + return terms + + def possible_subterms(term): + if isinstance(term, (TrigonometricFunction, HyperbolicFunction, + *inverse_trig_functions, + exp, log, Heaviside)): + return [term.args[0]] + elif isinstance(term, (chebyshevt, chebyshevu, + legendre, hermite, laguerre)): + return [term.args[1]] + elif isinstance(term, (gegenbauer, assoc_laguerre)): + return [term.args[2]] + elif isinstance(term, jacobi): + return [term.args[3]] + elif isinstance(term, Mul): + r = [] + for u in term.args: + r.append(u) + r.extend(possible_subterms(u)) + return r + elif isinstance(term, Pow): + r = [arg for arg in term.args if arg.has(symbol)] + if term.exp.is_Integer: + r.extend([term.base**d for d in primefactors(term.exp) + if 1 < d < abs(term.args[1])]) + if term.base.is_Add: + r.extend([t for t in possible_subterms(term.base) + if t.is_Pow]) + return r + elif isinstance(term, Add): + r = [] + for arg in term.args: + r.append(arg) + r.extend(possible_subterms(arg)) + return r + return [] + + for u in list(dict.fromkeys(possible_subterms(integrand) + exp_subterms(integrand))): + if u == symbol: + continue + u_diff = manual_diff(u, symbol) + new_integrand = test_subterm(u, u_diff) + if new_integrand is not False: + constant, new_integrand = new_integrand + if new_integrand == integrand.subs(symbol, u_var): + continue + substitution = (u, constant, new_integrand) + if substitution not in results: + results.append(substitution) + + return results + +def rewriter(condition, rewrite): + """Strategy that rewrites an integrand.""" + def _rewriter(integral): + integrand, symbol = integral + debug("Integral: {} is rewritten with {} on symbol: {}".format(integrand, rewrite, symbol)) + if condition(*integral): + rewritten = rewrite(*integral) + if rewritten != integrand: + substep = integral_steps(rewritten, symbol) + if not isinstance(substep, DontKnowRule) and substep: + return RewriteRule(integrand, symbol, rewritten, substep) + return _rewriter + +def proxy_rewriter(condition, rewrite): + """Strategy that rewrites an integrand based on some other criteria.""" + def _proxy_rewriter(criteria): + criteria, integral = criteria + integrand, symbol = integral + debug("Integral: {} is rewritten with {} on symbol: {} and criteria: {}".format(integrand, rewrite, symbol, criteria)) + args = criteria + list(integral) + if condition(*args): + rewritten = rewrite(*args) + if rewritten != integrand: + return RewriteRule(integrand, symbol, rewritten, integral_steps(rewritten, symbol)) + return _proxy_rewriter + +def multiplexer(conditions): + """Apply the rule that matches the condition, else None""" + def multiplexer_rl(expr): + for key, rule in conditions.items(): + if key(expr): + return rule(expr) + return multiplexer_rl + +def alternatives(*rules): + """Strategy that makes an AlternativeRule out of multiple possible results.""" + def _alternatives(integral): + alts = [] + count = 0 + debug("List of Alternative Rules") + for rule in rules: + count = count + 1 + debug("Rule {}: {}".format(count, rule)) + + result = rule(integral) + if (result and not isinstance(result, DontKnowRule) and + result != integral and result not in alts): + alts.append(result) + if len(alts) == 1: + return alts[0] + elif alts: + doable = [rule for rule in alts if not rule.contains_dont_know()] + if doable: + return AlternativeRule(*integral, doable) + else: + return AlternativeRule(*integral, alts) + return _alternatives + +def constant_rule(integral): + return ConstantRule(*integral) + +def power_rule(integral): + integrand, symbol = integral + base, expt = integrand.as_base_exp() + + if symbol not in expt.free_symbols and isinstance(base, Symbol): + if simplify(expt + 1) == 0: + return ReciprocalRule(integrand, symbol, base) + return PowerRule(integrand, symbol, base, expt) + elif symbol not in base.free_symbols and isinstance(expt, Symbol): + rule = ExpRule(integrand, symbol, base, expt) + + if fuzzy_not(log(base).is_zero): + return rule + elif log(base).is_zero: + return ConstantRule(1, symbol) + + return PiecewiseRule(integrand, symbol, [ + (rule, Ne(log(base), 0)), + (ConstantRule(1, symbol), True) + ]) + +def exp_rule(integral): + integrand, symbol = integral + if isinstance(integrand.args[0], Symbol): + return ExpRule(integrand, symbol, E, integrand.args[0]) + + +def orthogonal_poly_rule(integral): + orthogonal_poly_classes = { + jacobi: JacobiRule, + gegenbauer: GegenbauerRule, + chebyshevt: ChebyshevTRule, + chebyshevu: ChebyshevURule, + legendre: LegendreRule, + hermite: HermiteRule, + laguerre: LaguerreRule, + assoc_laguerre: AssocLaguerreRule + } + orthogonal_poly_var_index = { + jacobi: 3, + gegenbauer: 2, + assoc_laguerre: 2 + } + integrand, symbol = integral + for klass in orthogonal_poly_classes: + if isinstance(integrand, klass): + var_index = orthogonal_poly_var_index.get(klass, 1) + if (integrand.args[var_index] is symbol and not + any(v.has(symbol) for v in integrand.args[:var_index])): + return orthogonal_poly_classes[klass](integrand, symbol, *integrand.args[:var_index]) + + +_special_function_patterns: list[tuple[Type, Expr, Callable | None, tuple]] = [] +_wilds = [] +_symbol = Dummy('x') + + +def special_function_rule(integral): + integrand, symbol = integral + if not _special_function_patterns: + a = Wild('a', exclude=[_symbol], properties=[lambda x: not x.is_zero]) + b = Wild('b', exclude=[_symbol]) + c = Wild('c', exclude=[_symbol]) + d = Wild('d', exclude=[_symbol], properties=[lambda x: not x.is_zero]) + e = Wild('e', exclude=[_symbol], properties=[ + lambda x: not (x.is_nonnegative and x.is_integer)]) + _wilds.extend((a, b, c, d, e)) + # patterns consist of a SymPy class, a wildcard expr, an optional + # condition coded as a lambda (when Wild properties are not enough), + # followed by an applicable rule + linear_pattern = a*_symbol + b + quadratic_pattern = a*_symbol**2 + b*_symbol + c + _special_function_patterns.extend(( + (Mul, exp(linear_pattern, evaluate=False)/_symbol, None, EiRule), + (Mul, cos(linear_pattern, evaluate=False)/_symbol, None, CiRule), + (Mul, cosh(linear_pattern, evaluate=False)/_symbol, None, ChiRule), + (Mul, sin(linear_pattern, evaluate=False)/_symbol, None, SiRule), + (Mul, sinh(linear_pattern, evaluate=False)/_symbol, None, ShiRule), + (Pow, 1/log(linear_pattern, evaluate=False), None, LiRule), + (exp, exp(quadratic_pattern, evaluate=False), None, ErfRule), + (sin, sin(quadratic_pattern, evaluate=False), None, FresnelSRule), + (cos, cos(quadratic_pattern, evaluate=False), None, FresnelCRule), + (Mul, _symbol**e*exp(a*_symbol, evaluate=False), None, UpperGammaRule), + (Mul, polylog(b, a*_symbol, evaluate=False)/_symbol, None, PolylogRule), + (Pow, 1/sqrt(a - d*sin(_symbol, evaluate=False)**2), + lambda a, d: a != d, EllipticFRule), + (Pow, sqrt(a - d*sin(_symbol, evaluate=False)**2), + lambda a, d: a != d, EllipticERule), + )) + _integrand = integrand.subs(symbol, _symbol) + for type_, pattern, constraint, rule in _special_function_patterns: + if isinstance(_integrand, type_): + match = _integrand.match(pattern) + if match: + wild_vals = tuple(match.get(w) for w in _wilds + if match.get(w) is not None) + if constraint is None or constraint(*wild_vals): + return rule(integrand, symbol, *wild_vals) + + +def _add_degenerate_step(generic_cond, generic_step: Rule, degenerate_step: Rule | None) -> Rule: + if degenerate_step is None: + return generic_step + if isinstance(generic_step, PiecewiseRule): + subfunctions = [(substep, (cond & generic_cond).simplify()) + for substep, cond in generic_step.subfunctions] + else: + subfunctions = [(generic_step, generic_cond)] + if isinstance(degenerate_step, PiecewiseRule): + subfunctions += degenerate_step.subfunctions + else: + subfunctions.append((degenerate_step, S.true)) + return PiecewiseRule(generic_step.integrand, generic_step.variable, subfunctions) + + +def nested_pow_rule(integral: IntegralInfo): + # nested (c*(a+b*x)**d)**e + integrand, x = integral + + a_ = Wild('a', exclude=[x]) + b_ = Wild('b', exclude=[x, 0]) + pattern = a_+b_*x + generic_cond = S.true + + class NoMatch(Exception): + pass + + def _get_base_exp(expr: Expr) -> tuple[Expr, Expr]: + if not expr.has_free(x): + return S.One, S.Zero + if expr.is_Mul: + _, terms = expr.as_coeff_mul() + if not terms: + return S.One, S.Zero + results = [_get_base_exp(term) for term in terms] + bases = {b for b, _ in results} + bases.discard(S.One) + if len(bases) == 1: + return bases.pop(), Add(*(e for _, e in results)) + raise NoMatch + if expr.is_Pow: + b, e = expr.base, expr.exp # type: ignore + if e.has_free(x): + raise NoMatch + base_, sub_exp = _get_base_exp(b) + return base_, sub_exp * e + match = expr.match(pattern) + if match: + a, b = match[a_], match[b_] + base_ = x + a/b + nonlocal generic_cond + generic_cond = Ne(b, 0) + return base_, S.One + raise NoMatch + + try: + base, exp_ = _get_base_exp(integrand) + except NoMatch: + return + if generic_cond is S.true: + degenerate_step = None + else: + # equivalent with subs(b, 0) but no need to find b + degenerate_step = ConstantRule(integrand.subs(x, 0), x) + generic_step = NestedPowRule(integrand, x, base, exp_) + return _add_degenerate_step(generic_cond, generic_step, degenerate_step) + + +def inverse_trig_rule(integral: IntegralInfo, degenerate=True): + """ + Set degenerate=False on recursive call where coefficient of quadratic term + is assumed non-zero. + """ + integrand, symbol = integral + base, exp = integrand.as_base_exp() + a = Wild('a', exclude=[symbol]) + b = Wild('b', exclude=[symbol]) + c = Wild('c', exclude=[symbol, 0]) + match = base.match(a + b*symbol + c*symbol**2) + + if not match: + return + + def make_inverse_trig(RuleClass, a, sign_a, c, sign_c, h) -> Rule: + u_var = Dummy("u") + rewritten = 1/sqrt(sign_a*a + sign_c*c*(symbol-h)**2) # a>0, c>0 + quadratic_base = sqrt(c/a)*(symbol-h) + constant = 1/sqrt(c) + u_func = None + if quadratic_base is not symbol: + u_func = quadratic_base + quadratic_base = u_var + standard_form = 1/sqrt(sign_a + sign_c*quadratic_base**2) + substep = RuleClass(standard_form, quadratic_base) + if constant != 1: + substep = ConstantTimesRule(constant*standard_form, symbol, constant, standard_form, substep) + if u_func is not None: + substep = URule(rewritten, symbol, u_var, u_func, substep) + if h != 0: + substep = CompleteSquareRule(integrand, symbol, rewritten, substep) + return substep + + a, b, c = [match.get(i, S.Zero) for i in (a, b, c)] + generic_cond = Ne(c, 0) + if not degenerate or generic_cond is S.true: + degenerate_step = None + elif b.is_zero: + degenerate_step = ConstantRule(a ** exp, symbol) + else: + degenerate_step = sqrt_linear_rule(IntegralInfo((a + b * symbol) ** exp, symbol)) + + if simplify(2*exp + 1) == 0: + h, k = -b/(2*c), a - b**2/(4*c) # rewrite base to k + c*(symbol-h)**2 + non_square_cond = Ne(k, 0) + square_step = None + if non_square_cond is not S.true: + square_step = NestedPowRule(1/sqrt(c*(symbol-h)**2), symbol, symbol-h, S.NegativeOne) + if non_square_cond is S.false: + return square_step + generic_step = ReciprocalSqrtQuadraticRule(integrand, symbol, a, b, c) + step = _add_degenerate_step(non_square_cond, generic_step, square_step) + if k.is_real and c.is_real: + # list of ((rule, base_exp, a, sign_a, b, sign_b), condition) + rules = [] + for args, cond in ( # don't apply ArccoshRule to x**2-1 + ((ArcsinRule, k, 1, -c, -1, h), And(k > 0, c < 0)), # 1-x**2 + ((ArcsinhRule, k, 1, c, 1, h), And(k > 0, c > 0)), # 1+x**2 + ): + if cond is S.true: + return make_inverse_trig(*args) + if cond is not S.false: + rules.append((make_inverse_trig(*args), cond)) + if rules: + if not k.is_positive: # conditions are not thorough, need fall back rule + rules.append((generic_step, S.true)) + step = PiecewiseRule(integrand, symbol, rules) + else: + step = generic_step + return _add_degenerate_step(generic_cond, step, degenerate_step) + if exp == S.Half: + step = SqrtQuadraticRule(integrand, symbol, a, b, c) + return _add_degenerate_step(generic_cond, step, degenerate_step) + + +def add_rule(integral): + integrand, symbol = integral + results = [integral_steps(g, symbol) + for g in integrand.as_ordered_terms()] + return None if None in results else AddRule(integrand, symbol, results) + + +def mul_rule(integral: IntegralInfo): + integrand, symbol = integral + + # Constant times function case + coeff, f = integrand.as_independent(symbol) + if coeff != 1: + next_step = integral_steps(f, symbol) + if next_step is not None: + return ConstantTimesRule(integrand, symbol, coeff, f, next_step) + + +def _parts_rule(integrand, symbol) -> tuple[Expr, Expr, Expr, Expr, Rule] | None: + # LIATE rule: + # log, inverse trig, algebraic, trigonometric, exponential + def pull_out_algebraic(integrand): + integrand = integrand.cancel().together() + # iterating over Piecewise args would not work here + algebraic = ([] if isinstance(integrand, Piecewise) or not integrand.is_Mul + else [arg for arg in integrand.args if arg.is_algebraic_expr(symbol)]) + if algebraic: + u = Mul(*algebraic) + dv = (integrand / u).cancel() + return u, dv + + def pull_out_u(*functions) -> Callable[[Expr], tuple[Expr, Expr] | None]: + def pull_out_u_rl(integrand: Expr) -> tuple[Expr, Expr] | None: + if any(integrand.has(f) for f in functions): + args = [arg for arg in integrand.args + if any(isinstance(arg, cls) for cls in functions)] + if args: + u = Mul(*args) # type: ignore + dv = integrand / u + return u, dv + return None + + return pull_out_u_rl + + liate_rules = [pull_out_u(log), pull_out_u(*inverse_trig_functions), + pull_out_algebraic, pull_out_u(sin, cos), + pull_out_u(exp)] + + + dummy = Dummy("temporary") + # we can integrate log(x) and atan(x) by setting dv = 1 + if isinstance(integrand, (log, *inverse_trig_functions)): + integrand = dummy * integrand + + for index, rule in enumerate(liate_rules): + result = rule(integrand) + + if result: + u, dv = result + + # Don't pick u to be a constant if possible + if symbol not in u.free_symbols and not u.has(dummy): + return None + + u = u.subs(dummy, 1) + dv = dv.subs(dummy, 1) + + # Don't pick a non-polynomial algebraic to be differentiated + if rule == pull_out_algebraic and not u.is_polynomial(symbol): + return None + # Don't trade one logarithm for another + if isinstance(u, log): + rec_dv = 1/dv + if (rec_dv.is_polynomial(symbol) and + degree(rec_dv, symbol) == 1): + return None + + # Can integrate a polynomial times OrthogonalPolynomial + if rule == pull_out_algebraic: + if dv.is_Derivative or dv.has(TrigonometricFunction) or \ + isinstance(dv, OrthogonalPolynomial): + v_step = integral_steps(dv, symbol) + if v_step.contains_dont_know(): + return None + else: + du = u.diff(symbol) + v = v_step.eval() + return u, dv, v, du, v_step + + # make sure dv is amenable to integration + accept = False + if index < 2: # log and inverse trig are usually worth trying + accept = True + elif (rule == pull_out_algebraic and dv.args and + all(isinstance(a, (sin, cos, exp)) + for a in dv.args)): + accept = True + else: + for lrule in liate_rules[index + 1:]: + r = lrule(integrand) + if r and r[0].subs(dummy, 1).equals(dv): + accept = True + break + + if accept: + du = u.diff(symbol) + v_step = integral_steps(simplify(dv), symbol) + if not v_step.contains_dont_know(): + v = v_step.eval() + return u, dv, v, du, v_step + return None + + +def parts_rule(integral): + integrand, symbol = integral + constant, integrand = integrand.as_coeff_Mul() + + result = _parts_rule(integrand, symbol) + + steps = [] + if result: + u, dv, v, du, v_step = result + debug("u : {}, dv : {}, v : {}, du : {}, v_step: {}".format(u, dv, v, du, v_step)) + steps.append(result) + + if isinstance(v, Integral): + return + + # Set a limit on the number of times u can be used + if isinstance(u, (sin, cos, exp, sinh, cosh)): + cachekey = u.xreplace({symbol: _cache_dummy}) + if _parts_u_cache[cachekey] > 2: + return + _parts_u_cache[cachekey] += 1 + + # Try cyclic integration by parts a few times + for _ in range(4): + debug("Cyclic integration {} with v: {}, du: {}, integrand: {}".format(_, v, du, integrand)) + coefficient = ((v * du) / integrand).cancel() + if coefficient == 1: + break + if symbol not in coefficient.free_symbols: + rule = CyclicPartsRule(integrand, symbol, + [PartsRule(None, None, u, dv, v_step, None) + for (u, dv, v, du, v_step) in steps], + (-1) ** len(steps) * coefficient) + if (constant != 1) and rule: + rule = ConstantTimesRule(constant * integrand, symbol, constant, integrand, rule) + return rule + + # _parts_rule is sensitive to constants, factor it out + next_constant, next_integrand = (v * du).as_coeff_Mul() + result = _parts_rule(next_integrand, symbol) + + if result: + u, dv, v, du, v_step = result + u *= next_constant + du *= next_constant + steps.append((u, dv, v, du, v_step)) + else: + break + + def make_second_step(steps, integrand): + if steps: + u, dv, v, du, v_step = steps[0] + return PartsRule(integrand, symbol, u, dv, v_step, make_second_step(steps[1:], v * du)) + return integral_steps(integrand, symbol) + + if steps: + u, dv, v, du, v_step = steps[0] + rule = PartsRule(integrand, symbol, u, dv, v_step, make_second_step(steps[1:], v * du)) + if (constant != 1) and rule: + rule = ConstantTimesRule(constant * integrand, symbol, constant, integrand, rule) + return rule + + +def trig_rule(integral): + integrand, symbol = integral + if integrand == sin(symbol): + return SinRule(integrand, symbol) + if integrand == cos(symbol): + return CosRule(integrand, symbol) + if integrand == sec(symbol)**2: + return Sec2Rule(integrand, symbol) + if integrand == csc(symbol)**2: + return Csc2Rule(integrand, symbol) + + if isinstance(integrand, tan): + rewritten = sin(*integrand.args) / cos(*integrand.args) + elif isinstance(integrand, cot): + rewritten = cos(*integrand.args) / sin(*integrand.args) + elif isinstance(integrand, sec): + arg = integrand.args[0] + rewritten = ((sec(arg)**2 + tan(arg) * sec(arg)) / + (sec(arg) + tan(arg))) + elif isinstance(integrand, csc): + arg = integrand.args[0] + rewritten = ((csc(arg)**2 + cot(arg) * csc(arg)) / + (csc(arg) + cot(arg))) + else: + return + + return RewriteRule(integrand, symbol, rewritten, integral_steps(rewritten, symbol)) + +def trig_product_rule(integral: IntegralInfo): + integrand, symbol = integral + if integrand == sec(symbol) * tan(symbol): + return SecTanRule(integrand, symbol) + if integrand == csc(symbol) * cot(symbol): + return CscCotRule(integrand, symbol) + + +def quadratic_denom_rule(integral): + integrand, symbol = integral + a = Wild('a', exclude=[symbol]) + b = Wild('b', exclude=[symbol]) + c = Wild('c', exclude=[symbol]) + + match = integrand.match(a / (b * symbol ** 2 + c)) + + if match: + a, b, c = match[a], match[b], match[c] + general_rule = ArctanRule(integrand, symbol, a, b, c) + if b.is_extended_real and c.is_extended_real: + positive_cond = c/b > 0 + if positive_cond is S.true: + return general_rule + coeff = a/(2*sqrt(-c)*sqrt(b)) + constant = sqrt(-c/b) + r1 = 1/(symbol-constant) + r2 = 1/(symbol+constant) + log_steps = [ReciprocalRule(r1, symbol, symbol-constant), + ConstantTimesRule(-r2, symbol, -1, r2, ReciprocalRule(r2, symbol, symbol+constant))] + rewritten = sub = r1 - r2 + negative_step = AddRule(sub, symbol, log_steps) + if coeff != 1: + rewritten = Mul(coeff, sub, evaluate=False) + negative_step = ConstantTimesRule(rewritten, symbol, coeff, sub, negative_step) + negative_step = RewriteRule(integrand, symbol, rewritten, negative_step) + if positive_cond is S.false: + return negative_step + return PiecewiseRule(integrand, symbol, [(general_rule, positive_cond), (negative_step, S.true)]) + + power = PowerRule(integrand, symbol, symbol, -2) + if b != 1: + power = ConstantTimesRule(integrand, symbol, 1/b, symbol**-2, power) + + return PiecewiseRule(integrand, symbol, [(general_rule, Ne(c, 0)), (power, True)]) + + d = Wild('d', exclude=[symbol]) + match2 = integrand.match(a / (b * symbol ** 2 + c * symbol + d)) + if match2: + b, c = match2[b], match2[c] + if b.is_zero: + return + u = Dummy('u') + u_func = symbol + c/(2*b) + integrand2 = integrand.subs(symbol, u - c / (2*b)) + next_step = integral_steps(integrand2, u) + if next_step: + return URule(integrand2, symbol, u, u_func, next_step) + else: + return + e = Wild('e', exclude=[symbol]) + match3 = integrand.match((a* symbol + b) / (c * symbol ** 2 + d * symbol + e)) + if match3: + a, b, c, d, e = match3[a], match3[b], match3[c], match3[d], match3[e] + if c.is_zero: + return + denominator = c * symbol**2 + d * symbol + e + const = a/(2*c) + numer1 = (2*c*symbol+d) + numer2 = - const*d + b + u = Dummy('u') + step1 = URule(integrand, symbol, + u, denominator, integral_steps(u**(-1), u)) + if const != 1: + step1 = ConstantTimesRule(const*numer1/denominator, symbol, + const, numer1/denominator, step1) + if numer2.is_zero: + return step1 + step2 = integral_steps(numer2/denominator, symbol) + substeps = AddRule(integrand, symbol, [step1, step2]) + rewriten = const*numer1/denominator+numer2/denominator + return RewriteRule(integrand, symbol, rewriten, substeps) + + return + + +def sqrt_linear_rule(integral: IntegralInfo): + """ + Substitute common (a+b*x)**(1/n) + """ + integrand, x = integral + a = Wild('a', exclude=[x]) + b = Wild('b', exclude=[x, 0]) + a0 = b0 = 0 + bases, qs, bs = [], [], [] + for pow_ in integrand.find(Pow): # collect all (a+b*x)**(p/q) + base, exp_ = pow_.base, pow_.exp + if exp_.is_Integer or x not in base.free_symbols: # skip 1/x and sqrt(2) + continue + if not exp_.is_Rational: # exclude x**pi + return + match = base.match(a+b*x) + if not match: # skip non-linear + continue # for sqrt(x+sqrt(x)), although base is non-linear, we can still substitute sqrt(x) + a1, b1 = match[a], match[b] + if a0*b1 != a1*b0 or not (b0/b1).is_nonnegative: # cannot transform sqrt(x) to sqrt(x+1) or sqrt(-x) + return + if b0 == 0 or (b0/b1 > 1) is S.true: # choose the latter of sqrt(2*x) and sqrt(x) as representative + a0, b0 = a1, b1 + bases.append(base) + bs.append(b1) + qs.append(exp_.q) + if b0 == 0: # no such pattern found + return + q0: Integer = lcm_list(qs) + u_x = (a0 + b0*x)**(1/q0) + u = Dummy("u") + substituted = integrand.subs({base**(S.One/q): (b/b0)**(S.One/q)*u**(q0/q) + for base, b, q in zip(bases, bs, qs)}).subs(x, (u**q0-a0)/b0) + substep = integral_steps(substituted*u**(q0-1)*q0/b0, u) + if not substep.contains_dont_know(): + step: Rule = URule(integrand, x, u, u_x, substep) + generic_cond = Ne(b0, 0) + if generic_cond is not S.true: # possible degenerate case + simplified = integrand.subs(dict.fromkeys(bs, 0)) + degenerate_step = integral_steps(simplified, x) + step = PiecewiseRule(integrand, x, [(step, generic_cond), (degenerate_step, S.true)]) + return step + + +def sqrt_quadratic_rule(integral: IntegralInfo, degenerate=True): + integrand, x = integral + a = Wild('a', exclude=[x]) + b = Wild('b', exclude=[x]) + c = Wild('c', exclude=[x, 0]) + f = Wild('f') + n = Wild('n', properties=[lambda n: n.is_Integer and n.is_odd]) + match = integrand.match(f*sqrt(a+b*x+c*x**2)**n) + if not match: + return + a, b, c, f, n = match[a], match[b], match[c], match[f], match[n] + f_poly = f.as_poly(x) + if f_poly is None: + return + + generic_cond = Ne(c, 0) + if not degenerate or generic_cond is S.true: + degenerate_step = None + elif b.is_zero: + degenerate_step = integral_steps(f*sqrt(a)**n, x) + else: + degenerate_step = sqrt_linear_rule(IntegralInfo(f*sqrt(a+b*x)**n, x)) + + def sqrt_quadratic_denom_rule(numer_poly: Poly, integrand: Expr): + denom = sqrt(a+b*x+c*x**2) + deg = numer_poly.degree() + if deg <= 1: + # integrand == (d+e*x)/sqrt(a+b*x+c*x**2) + e, d = numer_poly.all_coeffs() if deg == 1 else (S.Zero, numer_poly.as_expr()) + # rewrite numerator to A*(2*c*x+b) + B + A = e/(2*c) + B = d-A*b + pre_substitute = (2*c*x+b)/denom + constant_step: Rule | None = None + linear_step: Rule | None = None + if A != 0: + u = Dummy("u") + pow_rule = PowerRule(1/sqrt(u), u, u, -S.Half) + linear_step = URule(pre_substitute, x, u, a+b*x+c*x**2, pow_rule) + if A != 1: + linear_step = ConstantTimesRule(A*pre_substitute, x, A, pre_substitute, linear_step) + if B != 0: + constant_step = inverse_trig_rule(IntegralInfo(1/denom, x), degenerate=False) + if B != 1: + constant_step = ConstantTimesRule(B/denom, x, B, 1/denom, constant_step) # type: ignore + if linear_step and constant_step: + add = Add(A*pre_substitute, B/denom, evaluate=False) + step: Rule | None = RewriteRule(integrand, x, add, AddRule(add, x, [linear_step, constant_step])) + else: + step = linear_step or constant_step + else: + coeffs = numer_poly.all_coeffs() + step = SqrtQuadraticDenomRule(integrand, x, a, b, c, coeffs) + return step + + if n > 0: # rewrite poly * sqrt(s)**(2*k-1) to poly*s**k / sqrt(s) + numer_poly = f_poly * (a+b*x+c*x**2)**((n+1)/2) + rewritten = numer_poly.as_expr()/sqrt(a+b*x+c*x**2) + substep = sqrt_quadratic_denom_rule(numer_poly, rewritten) + generic_step = RewriteRule(integrand, x, rewritten, substep) + elif n == -1: + generic_step = sqrt_quadratic_denom_rule(f_poly, integrand) + else: + return # todo: handle n < -1 case + return _add_degenerate_step(generic_cond, generic_step, degenerate_step) + + +def hyperbolic_rule(integral: tuple[Expr, Symbol]): + integrand, symbol = integral + if isinstance(integrand, HyperbolicFunction) and integrand.args[0] == symbol: + if integrand.func == sinh: + return SinhRule(integrand, symbol) + if integrand.func == cosh: + return CoshRule(integrand, symbol) + u = Dummy('u') + if integrand.func == tanh: + rewritten = sinh(symbol)/cosh(symbol) + return RewriteRule(integrand, symbol, rewritten, + URule(rewritten, symbol, u, cosh(symbol), ReciprocalRule(1/u, u, u))) + if integrand.func == coth: + rewritten = cosh(symbol)/sinh(symbol) + return RewriteRule(integrand, symbol, rewritten, + URule(rewritten, symbol, u, sinh(symbol), ReciprocalRule(1/u, u, u))) + else: + rewritten = integrand.rewrite(tanh) + if integrand.func == sech: + return RewriteRule(integrand, symbol, rewritten, + URule(rewritten, symbol, u, tanh(symbol/2), + ArctanRule(2/(u**2 + 1), u, S(2), S.One, S.One))) + if integrand.func == csch: + return RewriteRule(integrand, symbol, rewritten, + URule(rewritten, symbol, u, tanh(symbol/2), + ReciprocalRule(1/u, u, u))) + +@cacheit +def make_wilds(symbol): + a = Wild('a', exclude=[symbol]) + b = Wild('b', exclude=[symbol]) + m = Wild('m', exclude=[symbol], properties=[lambda n: isinstance(n, Integer)]) + n = Wild('n', exclude=[symbol], properties=[lambda n: isinstance(n, Integer)]) + + return a, b, m, n + +@cacheit +def sincos_pattern(symbol): + a, b, m, n = make_wilds(symbol) + pattern = sin(a*symbol)**m * cos(b*symbol)**n + + return pattern, a, b, m, n + +@cacheit +def tansec_pattern(symbol): + a, b, m, n = make_wilds(symbol) + pattern = tan(a*symbol)**m * sec(b*symbol)**n + + return pattern, a, b, m, n + +@cacheit +def cotcsc_pattern(symbol): + a, b, m, n = make_wilds(symbol) + pattern = cot(a*symbol)**m * csc(b*symbol)**n + + return pattern, a, b, m, n + +@cacheit +def heaviside_pattern(symbol): + m = Wild('m', exclude=[symbol]) + b = Wild('b', exclude=[symbol]) + g = Wild('g') + pattern = Heaviside(m*symbol + b) * g + + return pattern, m, b, g + +def uncurry(func): + def uncurry_rl(args): + return func(*args) + return uncurry_rl + +def trig_rewriter(rewrite): + def trig_rewriter_rl(args): + a, b, m, n, integrand, symbol = args + rewritten = rewrite(a, b, m, n, integrand, symbol) + if rewritten != integrand: + return RewriteRule(integrand, symbol, rewritten, integral_steps(rewritten, symbol)) + return trig_rewriter_rl + +sincos_botheven_condition = uncurry( + lambda a, b, m, n, i, s: m.is_even and n.is_even and + m.is_nonnegative and n.is_nonnegative) + +sincos_botheven = trig_rewriter( + lambda a, b, m, n, i, symbol: ( (((1 - cos(2*a*symbol)) / 2) ** (m / 2)) * + (((1 + cos(2*b*symbol)) / 2) ** (n / 2)) )) + +sincos_sinodd_condition = uncurry(lambda a, b, m, n, i, s: m.is_odd and m >= 3) + +sincos_sinodd = trig_rewriter( + lambda a, b, m, n, i, symbol: ( (1 - cos(a*symbol)**2)**((m - 1) / 2) * + sin(a*symbol) * + cos(b*symbol) ** n)) + +sincos_cosodd_condition = uncurry(lambda a, b, m, n, i, s: n.is_odd and n >= 3) + +sincos_cosodd = trig_rewriter( + lambda a, b, m, n, i, symbol: ( (1 - sin(b*symbol)**2)**((n - 1) / 2) * + cos(b*symbol) * + sin(a*symbol) ** m)) + +tansec_seceven_condition = uncurry(lambda a, b, m, n, i, s: n.is_even and n >= 4) +tansec_seceven = trig_rewriter( + lambda a, b, m, n, i, symbol: ( (1 + tan(b*symbol)**2) ** (n/2 - 1) * + sec(b*symbol)**2 * + tan(a*symbol) ** m )) + +tansec_tanodd_condition = uncurry(lambda a, b, m, n, i, s: m.is_odd) +tansec_tanodd = trig_rewriter( + lambda a, b, m, n, i, symbol: ( (sec(a*symbol)**2 - 1) ** ((m - 1) / 2) * + tan(a*symbol) * + sec(b*symbol) ** n )) + +tan_tansquared_condition = uncurry(lambda a, b, m, n, i, s: m == 2 and n == 0) +tan_tansquared = trig_rewriter( + lambda a, b, m, n, i, symbol: ( sec(a*symbol)**2 - 1)) + +cotcsc_csceven_condition = uncurry(lambda a, b, m, n, i, s: n.is_even and n >= 4) +cotcsc_csceven = trig_rewriter( + lambda a, b, m, n, i, symbol: ( (1 + cot(b*symbol)**2) ** (n/2 - 1) * + csc(b*symbol)**2 * + cot(a*symbol) ** m )) + +cotcsc_cotodd_condition = uncurry(lambda a, b, m, n, i, s: m.is_odd) +cotcsc_cotodd = trig_rewriter( + lambda a, b, m, n, i, symbol: ( (csc(a*symbol)**2 - 1) ** ((m - 1) / 2) * + cot(a*symbol) * + csc(b*symbol) ** n )) + +def trig_sincos_rule(integral): + integrand, symbol = integral + + if any(integrand.has(f) for f in (sin, cos)): + pattern, a, b, m, n = sincos_pattern(symbol) + match = integrand.match(pattern) + if not match: + return + + return multiplexer({ + sincos_botheven_condition: sincos_botheven, + sincos_sinodd_condition: sincos_sinodd, + sincos_cosodd_condition: sincos_cosodd + })(tuple( + [match.get(i, S.Zero) for i in (a, b, m, n)] + + [integrand, symbol])) + +def trig_tansec_rule(integral): + integrand, symbol = integral + + integrand = integrand.subs({ + 1 / cos(symbol): sec(symbol) + }) + + if any(integrand.has(f) for f in (tan, sec)): + pattern, a, b, m, n = tansec_pattern(symbol) + match = integrand.match(pattern) + if not match: + return + + return multiplexer({ + tansec_tanodd_condition: tansec_tanodd, + tansec_seceven_condition: tansec_seceven, + tan_tansquared_condition: tan_tansquared + })(tuple( + [match.get(i, S.Zero) for i in (a, b, m, n)] + + [integrand, symbol])) + +def trig_cotcsc_rule(integral): + integrand, symbol = integral + integrand = integrand.subs({ + 1 / sin(symbol): csc(symbol), + 1 / tan(symbol): cot(symbol), + cos(symbol) / tan(symbol): cot(symbol) + }) + + if any(integrand.has(f) for f in (cot, csc)): + pattern, a, b, m, n = cotcsc_pattern(symbol) + match = integrand.match(pattern) + if not match: + return + + return multiplexer({ + cotcsc_cotodd_condition: cotcsc_cotodd, + cotcsc_csceven_condition: cotcsc_csceven + })(tuple( + [match.get(i, S.Zero) for i in (a, b, m, n)] + + [integrand, symbol])) + +def trig_sindouble_rule(integral): + integrand, symbol = integral + a = Wild('a', exclude=[sin(2*symbol)]) + match = integrand.match(sin(2*symbol)*a) + if match: + sin_double = 2*sin(symbol)*cos(symbol)/sin(2*symbol) + return integral_steps(integrand * sin_double, symbol) + +def trig_powers_products_rule(integral): + return do_one(null_safe(trig_sincos_rule), + null_safe(trig_tansec_rule), + null_safe(trig_cotcsc_rule), + null_safe(trig_sindouble_rule))(integral) + +def trig_substitution_rule(integral): + integrand, symbol = integral + A = Wild('a', exclude=[0, symbol]) + B = Wild('b', exclude=[0, symbol]) + theta = Dummy("theta") + target_pattern = A + B*symbol**2 + + matches = integrand.find(target_pattern) + for expr in matches: + match = expr.match(target_pattern) + a = match.get(A, S.Zero) + b = match.get(B, S.Zero) + + a_positive = ((a.is_number and a > 0) or a.is_positive) + b_positive = ((b.is_number and b > 0) or b.is_positive) + a_negative = ((a.is_number and a < 0) or a.is_negative) + b_negative = ((b.is_number and b < 0) or b.is_negative) + x_func = None + if a_positive and b_positive: + # a**2 + b*x**2. Assume sec(theta) > 0, -pi/2 < theta < pi/2 + x_func = (sqrt(a)/sqrt(b)) * tan(theta) + # Do not restrict the domain: tan(theta) takes on any real + # value on the interval -pi/2 < theta < pi/2 so x takes on + # any value + restriction = True + elif a_positive and b_negative: + # a**2 - b*x**2. Assume cos(theta) > 0, -pi/2 < theta < pi/2 + constant = sqrt(a)/sqrt(-b) + x_func = constant * sin(theta) + restriction = And(symbol > -constant, symbol < constant) + elif a_negative and b_positive: + # b*x**2 - a**2. Assume sin(theta) > 0, 0 < theta < pi + constant = sqrt(-a)/sqrt(b) + x_func = constant * sec(theta) + restriction = And(symbol > -constant, symbol < constant) + if x_func: + # Manually simplify sqrt(trig(theta)**2) to trig(theta) + # Valid due to assumed domain restriction + substitutions = {} + for f in [sin, cos, tan, + sec, csc, cot]: + substitutions[sqrt(f(theta)**2)] = f(theta) + substitutions[sqrt(f(theta)**(-2))] = 1/f(theta) + + replaced = integrand.subs(symbol, x_func).trigsimp() + replaced = manual_subs(replaced, substitutions) + if not replaced.has(symbol): + replaced *= manual_diff(x_func, theta) + replaced = replaced.trigsimp() + secants = replaced.find(1/cos(theta)) + if secants: + replaced = replaced.xreplace({ + 1/cos(theta): sec(theta) + }) + + substep = integral_steps(replaced, theta) + if not substep.contains_dont_know(): + return TrigSubstitutionRule(integrand, symbol, + theta, x_func, replaced, substep, restriction) + +def heaviside_rule(integral): + integrand, symbol = integral + pattern, m, b, g = heaviside_pattern(symbol) + match = integrand.match(pattern) + if match and 0 != match[g]: + # f = Heaviside(m*x + b)*g + substep = integral_steps(match[g], symbol) + m, b = match[m], match[b] + return HeavisideRule(integrand, symbol, m*symbol + b, -b/m, substep) + + +def dirac_delta_rule(integral: IntegralInfo): + integrand, x = integral + if len(integrand.args) == 1: + n = S.Zero + else: + n = integrand.args[1] # type: ignore + if not n.is_Integer or n < 0: + return + a, b = Wild('a', exclude=[x]), Wild('b', exclude=[x, 0]) + match = integrand.args[0].match(a+b*x) + if not match: + return + a, b = match[a], match[b] + generic_cond = Ne(b, 0) + if generic_cond is S.true: + degenerate_step = None + else: + degenerate_step = ConstantRule(DiracDelta(a, n), x) + generic_step = DiracDeltaRule(integrand, x, n, a, b) + return _add_degenerate_step(generic_cond, generic_step, degenerate_step) + + +def substitution_rule(integral): + integrand, symbol = integral + + u_var = Dummy("u") + substitutions = find_substitutions(integrand, symbol, u_var) + count = 0 + if substitutions: + debug("List of Substitution Rules") + ways = [] + for u_func, c, substituted in substitutions: + subrule = integral_steps(substituted, u_var) + count = count + 1 + debug("Rule {}: {}".format(count, subrule)) + + if subrule.contains_dont_know(): + continue + + if simplify(c - 1) != 0: + _, denom = c.as_numer_denom() + if subrule: + subrule = ConstantTimesRule(c * substituted, u_var, c, substituted, subrule) + + if denom.free_symbols: + piecewise = [] + could_be_zero = [] + + if isinstance(denom, Mul): + could_be_zero = denom.args + else: + could_be_zero.append(denom) + + for expr in could_be_zero: + if not fuzzy_not(expr.is_zero): + substep = integral_steps(manual_subs(integrand, expr, 0), symbol) + + if substep: + piecewise.append(( + substep, + Eq(expr, 0) + )) + piecewise.append((subrule, True)) + subrule = PiecewiseRule(substituted, symbol, piecewise) + + ways.append(URule(integrand, symbol, u_var, u_func, subrule)) + + if len(ways) > 1: + return AlternativeRule(integrand, symbol, ways) + elif ways: + return ways[0] + + +partial_fractions_rule = rewriter( + lambda integrand, symbol: integrand.is_rational_function(), + lambda integrand, symbol: integrand.apart(symbol)) + +cancel_rule = rewriter( + # lambda integrand, symbol: integrand.is_algebraic_expr(), + # lambda integrand, symbol: isinstance(integrand, Mul), + lambda integrand, symbol: True, + lambda integrand, symbol: integrand.cancel()) + +distribute_expand_rule = rewriter( + lambda integrand, symbol: ( + isinstance(integrand, (Pow, Mul)) or all(arg.is_Pow or arg.is_polynomial(symbol) for arg in integrand.args)), + lambda integrand, symbol: integrand.expand()) + +trig_expand_rule = rewriter( + # If there are trig functions with different arguments, expand them + lambda integrand, symbol: ( + len({a.args[0] for a in integrand.atoms(TrigonometricFunction)}) > 1), + lambda integrand, symbol: integrand.expand(trig=True)) + +def derivative_rule(integral): + integrand = integral[0] + diff_variables = integrand.variables + undifferentiated_function = integrand.expr + integrand_variables = undifferentiated_function.free_symbols + + if integral.symbol in integrand_variables: + if integral.symbol in diff_variables: + return DerivativeRule(*integral) + else: + return DontKnowRule(integrand, integral.symbol) + else: + return ConstantRule(*integral) + +def rewrites_rule(integral): + integrand, symbol = integral + + if integrand.match(1/cos(symbol)): + rewritten = integrand.subs(1/cos(symbol), sec(symbol)) + return RewriteRule(integrand, symbol, rewritten, integral_steps(rewritten, symbol)) + +def fallback_rule(integral): + return DontKnowRule(*integral) + +# Cache is used to break cyclic integrals. +# Need to use the same dummy variable in cached expressions for them to match. +# Also record "u" of integration by parts, to avoid infinite repetition. +_integral_cache: dict[Expr, Expr | None] = {} +_parts_u_cache: dict[Expr, int] = defaultdict(int) +_cache_dummy = Dummy("z") + +def integral_steps(integrand, symbol, **options): + """Returns the steps needed to compute an integral. + + Explanation + =========== + + This function attempts to mirror what a student would do by hand as + closely as possible. + + SymPy Gamma uses this to provide a step-by-step explanation of an + integral. The code it uses to format the results of this function can be + found at + https://github.com/sympy/sympy_gamma/blob/master/app/logic/intsteps.py. + + Examples + ======== + + >>> from sympy import exp, sin + >>> from sympy.integrals.manualintegrate import integral_steps + >>> from sympy.abc import x + >>> print(repr(integral_steps(exp(x) / (1 + exp(2 * x)), x))) \ + # doctest: +NORMALIZE_WHITESPACE + URule(integrand=exp(x)/(exp(2*x) + 1), variable=x, u_var=_u, u_func=exp(x), + substep=ArctanRule(integrand=1/(_u**2 + 1), variable=_u, a=1, b=1, c=1)) + >>> print(repr(integral_steps(sin(x), x))) \ + # doctest: +NORMALIZE_WHITESPACE + SinRule(integrand=sin(x), variable=x) + >>> print(repr(integral_steps((x**2 + 3)**2, x))) \ + # doctest: +NORMALIZE_WHITESPACE + RewriteRule(integrand=(x**2 + 3)**2, variable=x, rewritten=x**4 + 6*x**2 + 9, + substep=AddRule(integrand=x**4 + 6*x**2 + 9, variable=x, + substeps=[PowerRule(integrand=x**4, variable=x, base=x, exp=4), + ConstantTimesRule(integrand=6*x**2, variable=x, constant=6, other=x**2, + substep=PowerRule(integrand=x**2, variable=x, base=x, exp=2)), + ConstantRule(integrand=9, variable=x)])) + + Returns + ======= + + rule : Rule + The first step; most rules have substeps that must also be + considered. These substeps can be evaluated using ``manualintegrate`` + to obtain a result. + + """ + cachekey = integrand.xreplace({symbol: _cache_dummy}) + if cachekey in _integral_cache: + if _integral_cache[cachekey] is None: + # Stop this attempt, because it leads around in a loop + return DontKnowRule(integrand, symbol) + else: + # TODO: This is for future development, as currently + # _integral_cache gets no values other than None + return (_integral_cache[cachekey].xreplace(_cache_dummy, symbol), + symbol) + else: + _integral_cache[cachekey] = None + + integral = IntegralInfo(integrand, symbol) + + def key(integral): + integrand = integral.integrand + + if symbol not in integrand.free_symbols: + return Number + for cls in (Symbol, TrigonometricFunction, OrthogonalPolynomial): + if isinstance(integrand, cls): + return cls + return type(integrand) + + def integral_is_subclass(*klasses): + def _integral_is_subclass(integral): + k = key(integral) + return k and issubclass(k, klasses) + return _integral_is_subclass + + result = do_one( + null_safe(special_function_rule), + null_safe(switch(key, { + Pow: do_one(null_safe(power_rule), null_safe(inverse_trig_rule), + null_safe(sqrt_linear_rule), + null_safe(quadratic_denom_rule)), + Symbol: power_rule, + exp: exp_rule, + Add: add_rule, + Mul: do_one(null_safe(mul_rule), null_safe(trig_product_rule), + null_safe(heaviside_rule), null_safe(quadratic_denom_rule), + null_safe(sqrt_linear_rule), + null_safe(sqrt_quadratic_rule)), + Derivative: derivative_rule, + TrigonometricFunction: trig_rule, + Heaviside: heaviside_rule, + DiracDelta: dirac_delta_rule, + OrthogonalPolynomial: orthogonal_poly_rule, + Number: constant_rule + })), + do_one( + null_safe(trig_rule), + null_safe(hyperbolic_rule), + null_safe(alternatives( + rewrites_rule, + substitution_rule, + condition( + integral_is_subclass(Mul, Pow), + partial_fractions_rule), + condition( + integral_is_subclass(Mul, Pow), + cancel_rule), + condition( + integral_is_subclass(Mul, log, + *inverse_trig_functions), + parts_rule), + condition( + integral_is_subclass(Mul, Pow), + distribute_expand_rule), + trig_powers_products_rule, + trig_expand_rule + )), + null_safe(condition(integral_is_subclass(Mul, Pow), nested_pow_rule)), + null_safe(trig_substitution_rule) + ), + fallback_rule)(integral) + del _integral_cache[cachekey] + return result + + +def manualintegrate(f, var): + """manualintegrate(f, var) + + Explanation + =========== + + Compute indefinite integral of a single variable using an algorithm that + resembles what a student would do by hand. + + Unlike :func:`~.integrate`, var can only be a single symbol. + + Examples + ======== + + >>> from sympy import sin, cos, tan, exp, log, integrate + >>> from sympy.integrals.manualintegrate import manualintegrate + >>> from sympy.abc import x + >>> manualintegrate(1 / x, x) + log(x) + >>> integrate(1/x) + log(x) + >>> manualintegrate(log(x), x) + x*log(x) - x + >>> integrate(log(x)) + x*log(x) - x + >>> manualintegrate(exp(x) / (1 + exp(2 * x)), x) + atan(exp(x)) + >>> integrate(exp(x) / (1 + exp(2 * x))) + RootSum(4*_z**2 + 1, Lambda(_i, _i*log(2*_i + exp(x)))) + >>> manualintegrate(cos(x)**4 * sin(x), x) + -cos(x)**5/5 + >>> integrate(cos(x)**4 * sin(x), x) + -cos(x)**5/5 + >>> manualintegrate(cos(x)**4 * sin(x)**3, x) + cos(x)**7/7 - cos(x)**5/5 + >>> integrate(cos(x)**4 * sin(x)**3, x) + cos(x)**7/7 - cos(x)**5/5 + >>> manualintegrate(tan(x), x) + -log(cos(x)) + >>> integrate(tan(x), x) + -log(cos(x)) + + See Also + ======== + + sympy.integrals.integrals.integrate + sympy.integrals.integrals.Integral.doit + sympy.integrals.integrals.Integral + """ + result = integral_steps(f, var).eval() + # Clear the cache of u-parts + _parts_u_cache.clear() + # If we got Piecewise with two parts, put generic first + if isinstance(result, Piecewise) and len(result.args) == 2: + cond = result.args[0][1] + if isinstance(cond, Eq) and result.args[1][1] == True: + result = result.func( + (result.args[1][0], Ne(*cond.args)), + (result.args[0][0], True)) + return result diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/meijerint.py b/venv/lib/python3.11/site-packages/sympy/integrals/meijerint.py new file mode 100644 index 0000000000000000000000000000000000000000..89d8401c7eee0147df2e824dc1dc50b59ca7f0be --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/meijerint.py @@ -0,0 +1,2191 @@ +""" +Integrate functions by rewriting them as Meijer G-functions. + +There are three user-visible functions that can be used by other parts of the +sympy library to solve various integration problems: + +- meijerint_indefinite +- meijerint_definite +- meijerint_inversion + +They can be used to compute, respectively, indefinite integrals, definite +integrals over intervals of the real line, and inverse laplace-type integrals +(from c-I*oo to c+I*oo). See the respective docstrings for details. + +The main references for this are: + +[L] Luke, Y. L. (1969), The Special Functions and Their Approximations, + Volume 1 + +[R] Kelly B. Roach. Meijer G Function Representations. + In: Proceedings of the 1997 International Symposium on Symbolic and + Algebraic Computation, pages 205-211, New York, 1997. ACM. + +[P] A. P. Prudnikov, Yu. A. Brychkov and O. I. Marichev (1990). + Integrals and Series: More Special Functions, Vol. 3,. + Gordon and Breach Science Publisher +""" + +from __future__ import annotations +import itertools + +from sympy import SYMPY_DEBUG +from sympy.core import S, Expr +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.cache import cacheit +from sympy.core.containers import Tuple +from sympy.core.exprtools import factor_terms +from sympy.core.function import (expand, expand_mul, expand_power_base, + expand_trig, Function) +from sympy.core.mul import Mul +from sympy.core.intfunc import ilcm +from sympy.core.numbers import Rational, pi +from sympy.core.relational import Eq, Ne, _canonical_coeff +from sympy.core.sorting import default_sort_key, ordered +from sympy.core.symbol import Dummy, symbols, Wild, Symbol +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.complexes import (re, im, arg, Abs, sign, + unpolarify, polarify, polar_lift, principal_branch, unbranched_argument, + periodic_argument) +from sympy.functions.elementary.exponential import exp, exp_polar, log +from sympy.functions.elementary.integers import ceiling +from sympy.functions.elementary.hyperbolic import (cosh, sinh, + _rewrite_hyperbolics_as_exp, HyperbolicFunction) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise, piecewise_fold +from sympy.functions.elementary.trigonometric import (cos, sin, sinc, + TrigonometricFunction) +from sympy.functions.special.bessel import besselj, bessely, besseli, besselk +from sympy.functions.special.delta_functions import DiracDelta, Heaviside +from sympy.functions.special.elliptic_integrals import elliptic_k, elliptic_e +from sympy.functions.special.error_functions import (erf, erfc, erfi, Ei, + expint, Si, Ci, Shi, Chi, fresnels, fresnelc) +from sympy.functions.special.gamma_functions import gamma +from sympy.functions.special.hyper import hyper, meijerg +from sympy.functions.special.singularity_functions import SingularityFunction +from .integrals import Integral +from sympy.logic.boolalg import And, Or, BooleanAtom, Not, BooleanFunction +from sympy.polys import cancel, factor +from sympy.utilities.iterables import multiset_partitions +from sympy.utilities.misc import debug as _debug +from sympy.utilities.misc import debugf as _debugf + +# keep this at top for easy reference +z = Dummy('z') + + +def _has(res, *f): + # return True if res has f; in the case of Piecewise + # only return True if *all* pieces have f + res = piecewise_fold(res) + if getattr(res, 'is_Piecewise', False): + return all(_has(i, *f) for i in res.args) + return res.has(*f) + + +def _create_lookup_table(table): + """ Add formulae for the function -> meijerg lookup table. """ + def wild(n): + return Wild(n, exclude=[z]) + p, q, a, b, c = list(map(wild, 'pqabc')) + n = Wild('n', properties=[lambda x: x.is_Integer and x > 0]) + t = p*z**q + + def add(formula, an, ap, bm, bq, arg=t, fac=S.One, cond=True, hint=True): + table.setdefault(_mytype(formula, z), []).append((formula, + [(fac, meijerg(an, ap, bm, bq, arg))], cond, hint)) + + def addi(formula, inst, cond, hint=True): + table.setdefault( + _mytype(formula, z), []).append((formula, inst, cond, hint)) + + def constant(a): + return [(a, meijerg([1], [], [], [0], z)), + (a, meijerg([], [1], [0], [], z))] + table[()] = [(a, constant(a), True, True)] + + # [P], Section 8. + class IsNonPositiveInteger(Function): + + @classmethod + def eval(cls, arg): + arg = unpolarify(arg) + if arg.is_Integer is True: + return arg <= 0 + + # Section 8.4.2 + # TODO this needs more polar_lift (c/f entry for exp) + add(Heaviside(t - b)*(t - b)**(a - 1), [a], [], [], [0], t/b, + gamma(a)*b**(a - 1), And(b > 0)) + add(Heaviside(b - t)*(b - t)**(a - 1), [], [a], [0], [], t/b, + gamma(a)*b**(a - 1), And(b > 0)) + add(Heaviside(z - (b/p)**(1/q))*(t - b)**(a - 1), [a], [], [], [0], t/b, + gamma(a)*b**(a - 1), And(b > 0)) + add(Heaviside((b/p)**(1/q) - z)*(b - t)**(a - 1), [], [a], [0], [], t/b, + gamma(a)*b**(a - 1), And(b > 0)) + add((b + t)**(-a), [1 - a], [], [0], [], t/b, b**(-a)/gamma(a), + hint=Not(IsNonPositiveInteger(a))) + add(Abs(b - t)**(-a), [1 - a], [(1 - a)/2], [0], [(1 - a)/2], t/b, + 2*sin(pi*a/2)*gamma(1 - a)*Abs(b)**(-a), re(a) < 1) + add((t**a - b**a)/(t - b), [0, a], [], [0, a], [], t/b, + b**(a - 1)*sin(a*pi)/pi) + + # 12 + def A1(r, sign, nu): + return pi**Rational(-1, 2)*(-sign*nu/2)**(1 - 2*r) + + def tmpadd(r, sgn): + # XXX the a**2 is bad for matching + add((sqrt(a**2 + t) + sgn*a)**b/(a**2 + t)**r, + [(1 + b)/2, 1 - 2*r + b/2], [], + [(b - sgn*b)/2], [(b + sgn*b)/2], t/a**2, + a**(b - 2*r)*A1(r, sgn, b)) + tmpadd(0, 1) + tmpadd(0, -1) + tmpadd(S.Half, 1) + tmpadd(S.Half, -1) + + # 13 + def tmpadd(r, sgn): + add((sqrt(a + p*z**q) + sgn*sqrt(p)*z**(q/2))**b/(a + p*z**q)**r, + [1 - r + sgn*b/2], [1 - r - sgn*b/2], [0, S.Half], [], + p*z**q/a, a**(b/2 - r)*A1(r, sgn, b)) + tmpadd(0, 1) + tmpadd(0, -1) + tmpadd(S.Half, 1) + tmpadd(S.Half, -1) + # (those after look obscure) + + # Section 8.4.3 + add(exp(polar_lift(-1)*t), [], [], [0], []) + + # TODO can do sin^n, sinh^n by expansion ... where? + # 8.4.4 (hyperbolic functions) + add(sinh(t), [], [1], [S.Half], [1, 0], t**2/4, pi**Rational(3, 2)) + add(cosh(t), [], [S.Half], [0], [S.Half, S.Half], t**2/4, pi**Rational(3, 2)) + + # Section 8.4.5 + # TODO can do t + a. but can also do by expansion... (XXX not really) + add(sin(t), [], [], [S.Half], [0], t**2/4, sqrt(pi)) + add(cos(t), [], [], [0], [S.Half], t**2/4, sqrt(pi)) + + # Section 8.4.6 (sinc function) + add(sinc(t), [], [], [0], [Rational(-1, 2)], t**2/4, sqrt(pi)/2) + + # Section 8.5.5 + def make_log1(subs): + N = subs[n] + return [(S.NegativeOne**N*factorial(N), + meijerg([], [1]*(N + 1), [0]*(N + 1), [], t))] + + def make_log2(subs): + N = subs[n] + return [(factorial(N), + meijerg([1]*(N + 1), [], [], [0]*(N + 1), t))] + # TODO these only hold for positive p, and can be made more general + # but who uses log(x)*Heaviside(a-x) anyway ... + # TODO also it would be nice to derive them recursively ... + addi(log(t)**n*Heaviside(1 - t), make_log1, True) + addi(log(t)**n*Heaviside(t - 1), make_log2, True) + + def make_log3(subs): + return make_log1(subs) + make_log2(subs) + addi(log(t)**n, make_log3, True) + addi(log(t + a), + constant(log(a)) + [(S.One, meijerg([1, 1], [], [1], [0], t/a))], + True) + addi(log(Abs(t - a)), constant(log(Abs(a))) + + [(pi, meijerg([1, 1], [S.Half], [1], [0, S.Half], t/a))], + True) + # TODO log(x)/(x+a) and log(x)/(x-1) can also be done. should they + # be derivable? + # TODO further formulae in this section seem obscure + + # Sections 8.4.9-10 + # TODO + + # Section 8.4.11 + addi(Ei(t), + constant(-S.ImaginaryUnit*pi) + [(S.NegativeOne, meijerg([], [1], [0, 0], [], + t*polar_lift(-1)))], + True) + + # Section 8.4.12 + add(Si(t), [1], [], [S.Half], [0, 0], t**2/4, sqrt(pi)/2) + add(Ci(t), [], [1], [0, 0], [S.Half], t**2/4, -sqrt(pi)/2) + + # Section 8.4.13 + add(Shi(t), [S.Half], [], [0], [Rational(-1, 2), Rational(-1, 2)], polar_lift(-1)*t**2/4, + t*sqrt(pi)/4) + add(Chi(t), [], [S.Half, 1], [0, 0], [S.Half, S.Half], t**2/4, - + pi**S('3/2')/2) + + # generalized exponential integral + add(expint(a, t), [], [a], [a - 1, 0], [], t) + + # Section 8.4.14 + add(erf(t), [1], [], [S.Half], [0], t**2, 1/sqrt(pi)) + # TODO exp(-x)*erf(I*x) does not work + add(erfc(t), [], [1], [0, S.Half], [], t**2, 1/sqrt(pi)) + # This formula for erfi(z) yields a wrong(?) minus sign + #add(erfi(t), [1], [], [S.Half], [0], -t**2, I/sqrt(pi)) + add(erfi(t), [S.Half], [], [0], [Rational(-1, 2)], -t**2, t/sqrt(pi)) + + # Fresnel Integrals + add(fresnels(t), [1], [], [Rational(3, 4)], [0, Rational(1, 4)], pi**2*t**4/16, S.Half) + add(fresnelc(t), [1], [], [Rational(1, 4)], [0, Rational(3, 4)], pi**2*t**4/16, S.Half) + + ##### bessel-type functions ##### + # Section 8.4.19 + add(besselj(a, t), [], [], [a/2], [-a/2], t**2/4) + + # all of the following are derivable + #add(sin(t)*besselj(a, t), [Rational(1, 4), Rational(3, 4)], [], [(1+a)/2], + # [-a/2, a/2, (1-a)/2], t**2, 1/sqrt(2)) + #add(cos(t)*besselj(a, t), [Rational(1, 4), Rational(3, 4)], [], [a/2], + # [-a/2, (1+a)/2, (1-a)/2], t**2, 1/sqrt(2)) + #add(besselj(a, t)**2, [S.Half], [], [a], [-a, 0], t**2, 1/sqrt(pi)) + #add(besselj(a, t)*besselj(b, t), [0, S.Half], [], [(a + b)/2], + # [-(a+b)/2, (a - b)/2, (b - a)/2], t**2, 1/sqrt(pi)) + + # Section 8.4.20 + add(bessely(a, t), [], [-(a + 1)/2], [a/2, -a/2], [-(a + 1)/2], t**2/4) + + # TODO all of the following should be derivable + #add(sin(t)*bessely(a, t), [Rational(1, 4), Rational(3, 4)], [(1 - a - 1)/2], + # [(1 + a)/2, (1 - a)/2], [(1 - a - 1)/2, (1 - 1 - a)/2, (1 - 1 + a)/2], + # t**2, 1/sqrt(2)) + #add(cos(t)*bessely(a, t), [Rational(1, 4), Rational(3, 4)], [(0 - a - 1)/2], + # [(0 + a)/2, (0 - a)/2], [(0 - a - 1)/2, (1 - 0 - a)/2, (1 - 0 + a)/2], + # t**2, 1/sqrt(2)) + #add(besselj(a, t)*bessely(b, t), [0, S.Half], [(a - b - 1)/2], + # [(a + b)/2, (a - b)/2], [(a - b - 1)/2, -(a + b)/2, (b - a)/2], + # t**2, 1/sqrt(pi)) + #addi(bessely(a, t)**2, + # [(2/sqrt(pi), meijerg([], [S.Half, S.Half - a], [0, a, -a], + # [S.Half - a], t**2)), + # (1/sqrt(pi), meijerg([S.Half], [], [a], [-a, 0], t**2))], + # True) + #addi(bessely(a, t)*bessely(b, t), + # [(2/sqrt(pi), meijerg([], [0, S.Half, (1 - a - b)/2], + # [(a + b)/2, (a - b)/2, (b - a)/2, -(a + b)/2], + # [(1 - a - b)/2], t**2)), + # (1/sqrt(pi), meijerg([0, S.Half], [], [(a + b)/2], + # [-(a + b)/2, (a - b)/2, (b - a)/2], t**2))], + # True) + + # Section 8.4.21 ? + # Section 8.4.22 + add(besseli(a, t), [], [(1 + a)/2], [a/2], [-a/2, (1 + a)/2], t**2/4, pi) + # TODO many more formulas. should all be derivable + + # Section 8.4.23 + add(besselk(a, t), [], [], [a/2, -a/2], [], t**2/4, S.Half) + # TODO many more formulas. should all be derivable + + # Complete elliptic integrals K(z) and E(z) + add(elliptic_k(t), [S.Half, S.Half], [], [0], [0], -t, S.Half) + add(elliptic_e(t), [S.Half, 3*S.Half], [], [0], [0], -t, Rational(-1, 2)/2) + + +#################################################################### +# First some helper functions. +#################################################################### + +from sympy.utilities.timeutils import timethis +timeit = timethis('meijerg') + + +def _mytype(f: Basic, x: Symbol) -> tuple[type[Basic], ...]: + """ Create a hashable entity describing the type of f. """ + def key(x: type[Basic]) -> tuple[int, int, str]: + return x.class_key() + + if x not in f.free_symbols: + return () + elif f.is_Function: + return type(f), + return tuple(sorted((t for a in f.args for t in _mytype(a, x)), key=key)) + + +class _CoeffExpValueError(ValueError): + """ + Exception raised by _get_coeff_exp, for internal use only. + """ + pass + + +def _get_coeff_exp(expr, x): + """ + When expr is known to be of the form c*x**b, with c and/or b possibly 1, + return c, b. + + Examples + ======== + + >>> from sympy.abc import x, a, b + >>> from sympy.integrals.meijerint import _get_coeff_exp + >>> _get_coeff_exp(a*x**b, x) + (a, b) + >>> _get_coeff_exp(x, x) + (1, 1) + >>> _get_coeff_exp(2*x, x) + (2, 1) + >>> _get_coeff_exp(x**3, x) + (1, 3) + """ + from sympy.simplify import powsimp + (c, m) = expand_power_base(powsimp(expr)).as_coeff_mul(x) + if not m: + return c, S.Zero + [m] = m + if m.is_Pow: + if m.base != x: + raise _CoeffExpValueError('expr not of form a*x**b') + return c, m.exp + elif m == x: + return c, S.One + else: + raise _CoeffExpValueError('expr not of form a*x**b: %s' % expr) + + +def _exponents(expr, x): + """ + Find the exponents of ``x`` (not including zero) in ``expr``. + + Examples + ======== + + >>> from sympy.integrals.meijerint import _exponents + >>> from sympy.abc import x, y + >>> from sympy import sin + >>> _exponents(x, x) + {1} + >>> _exponents(x**2, x) + {2} + >>> _exponents(x**2 + x, x) + {1, 2} + >>> _exponents(x**3*sin(x + x**y) + 1/x, x) + {-1, 1, 3, y} + """ + def _exponents_(expr, x, res): + if expr == x: + res.update([1]) + return + if expr.is_Pow and expr.base == x: + res.update([expr.exp]) + return + for argument in expr.args: + _exponents_(argument, x, res) + res = set() + _exponents_(expr, x, res) + return res + + +def _functions(expr, x): + """ Find the types of functions in expr, to estimate the complexity. """ + return {e.func for e in expr.atoms(Function) if x in e.free_symbols} + + +def _find_splitting_points(expr, x): + """ + Find numbers a such that a linear substitution x -> x + a would + (hopefully) simplify expr. + + Examples + ======== + + >>> from sympy.integrals.meijerint import _find_splitting_points as fsp + >>> from sympy import sin + >>> from sympy.abc import x + >>> fsp(x, x) + {0} + >>> fsp((x-1)**3, x) + {1} + >>> fsp(sin(x+3)*x, x) + {-3, 0} + """ + p, q = [Wild(n, exclude=[x]) for n in 'pq'] + + def compute_innermost(expr, res): + if not isinstance(expr, Expr): + return + m = expr.match(p*x + q) + if m and m[p] != 0: + res.add(-m[q]/m[p]) + return + if expr.is_Atom: + return + for argument in expr.args: + compute_innermost(argument, res) + innermost = set() + compute_innermost(expr, innermost) + return innermost + + +def _split_mul(f, x): + """ + Split expression ``f`` into fac, po, g, where fac is a constant factor, + po = x**s for some s independent of s, and g is "the rest". + + Examples + ======== + + >>> from sympy.integrals.meijerint import _split_mul + >>> from sympy import sin + >>> from sympy.abc import s, x + >>> _split_mul((3*x)**s*sin(x**2)*x, x) + (3**s, x*x**s, sin(x**2)) + """ + fac = S.One + po = S.One + g = S.One + f = expand_power_base(f) + + args = Mul.make_args(f) + for a in args: + if a == x: + po *= x + elif x not in a.free_symbols: + fac *= a + else: + if a.is_Pow and x not in a.exp.free_symbols: + c, t = a.base.as_coeff_mul(x) + if t != (x,): + c, t = expand_mul(a.base).as_coeff_mul(x) + if t == (x,): + po *= x**a.exp + fac *= unpolarify(polarify(c**a.exp, subs=False)) + continue + g *= a + + return fac, po, g + + +def _mul_args(f): + """ + Return a list ``L`` such that ``Mul(*L) == f``. + + If ``f`` is not a ``Mul`` or ``Pow``, ``L=[f]``. + If ``f=g**n`` for an integer ``n``, ``L=[g]*n``. + If ``f`` is a ``Mul``, ``L`` comes from applying ``_mul_args`` to all factors of ``f``. + """ + args = Mul.make_args(f) + gs = [] + for g in args: + if g.is_Pow and g.exp.is_Integer: + n = g.exp + base = g.base + if n < 0: + n = -n + base = 1/base + gs += [base]*n + else: + gs.append(g) + return gs + + +def _mul_as_two_parts(f): + """ + Find all the ways to split ``f`` into a product of two terms. + Return None on failure. + + Explanation + =========== + + Although the order is canonical from multiset_partitions, this is + not necessarily the best order to process the terms. For example, + if the case of len(gs) == 2 is removed and multiset is allowed to + sort the terms, some tests fail. + + Examples + ======== + + >>> from sympy.integrals.meijerint import _mul_as_two_parts + >>> from sympy import sin, exp, ordered + >>> from sympy.abc import x + >>> list(ordered(_mul_as_two_parts(x*sin(x)*exp(x)))) + [(x, exp(x)*sin(x)), (x*exp(x), sin(x)), (x*sin(x), exp(x))] + """ + + gs = _mul_args(f) + if len(gs) < 2: + return None + if len(gs) == 2: + return [tuple(gs)] + return [(Mul(*x), Mul(*y)) for (x, y) in multiset_partitions(gs, 2)] + + +def _inflate_g(g, n): + """ Return C, h such that h is a G function of argument z**n and + g = C*h. """ + # TODO should this be a method of meijerg? + # See: [L, page 150, equation (5)] + def inflate(params, n): + """ (a1, .., ak) -> (a1/n, (a1+1)/n, ..., (ak + n-1)/n) """ + return [(a + i)/n for a, i in itertools.product(params, range(n))] + v = S(len(g.ap) - len(g.bq)) + C = n**(1 + g.nu + v/2) + C /= (2*pi)**((n - 1)*g.delta) + return C, meijerg(inflate(g.an, n), inflate(g.aother, n), + inflate(g.bm, n), inflate(g.bother, n), + g.argument**n * n**(n*v)) + + +def _flip_g(g): + """ Turn the G function into one of inverse argument + (i.e. G(1/x) -> G'(x)) """ + # See [L], section 5.2 + def tr(l): + return [1 - a for a in l] + return meijerg(tr(g.bm), tr(g.bother), tr(g.an), tr(g.aother), 1/g.argument) + + +def _inflate_fox_h(g, a): + r""" + Let d denote the integrand in the definition of the G function ``g``. + Consider the function H which is defined in the same way, but with + integrand d/Gamma(a*s) (contour conventions as usual). + + If ``a`` is rational, the function H can be written as C*G, for a constant C + and a G-function G. + + This function returns C, G. + """ + if a < 0: + return _inflate_fox_h(_flip_g(g), -a) + p = S(a.p) + q = S(a.q) + # We use the substitution s->qs, i.e. inflate g by q. We are left with an + # extra factor of Gamma(p*s), for which we use Gauss' multiplication + # theorem. + D, g = _inflate_g(g, q) + z = g.argument + D /= (2*pi)**((1 - p)/2)*p**Rational(-1, 2) + z /= p**p + bs = [(n + 1)/p for n in range(p)] + return D, meijerg(g.an, g.aother, g.bm, list(g.bother) + bs, z) + + +_dummies: dict[tuple[str, str], Dummy] = {} + + +def _dummy(name, token, expr, **kwargs): + """ + Return a dummy. This will return the same dummy if the same token+name is + requested more than once, and it is not already in expr. + This is for being cache-friendly. + """ + d = _dummy_(name, token, **kwargs) + if d in expr.free_symbols: + return Dummy(name, **kwargs) + return d + + +def _dummy_(name, token, **kwargs): + """ + Return a dummy associated to name and token. Same effect as declaring + it globally. + """ + if not (name, token) in _dummies: + _dummies[(name, token)] = Dummy(name, **kwargs) + return _dummies[(name, token)] + + +def _is_analytic(f, x): + """ Check if f(x), when expressed using G functions on the positive reals, + will in fact agree with the G functions almost everywhere """ + return not any(x in expr.free_symbols for expr in f.atoms(Heaviside, Abs)) + + +def _condsimp(cond, first=True): + """ + Do naive simplifications on ``cond``. + + Explanation + =========== + + Note that this routine is completely ad-hoc, simplification rules being + added as need arises rather than following any logical pattern. + + Examples + ======== + + >>> from sympy.integrals.meijerint import _condsimp as simp + >>> from sympy import Or, Eq + >>> from sympy.abc import x, y + >>> simp(Or(x < y, Eq(x, y))) + x <= y + """ + if first: + cond = cond.replace(lambda _: _.is_Relational, _canonical_coeff) + first = False + if not isinstance(cond, BooleanFunction): + return cond + p, q, r = symbols('p q r', cls=Wild) + # transforms tests use 0, 4, 5 and 11-14 + # meijer tests use 0, 2, 11, 14 + # joint_rv uses 6, 7 + rules = [ + (Or(p < q, Eq(p, q)), p <= q), # 0 + # The next two obviously are instances of a general pattern, but it is + # easier to spell out the few cases we care about. + (And(Abs(arg(p)) <= pi, Abs(arg(p) - 2*pi) <= pi), + Eq(arg(p) - pi, 0)), # 1 + (And(Abs(2*arg(p) + pi) <= pi, Abs(2*arg(p) - pi) <= pi), + Eq(arg(p), 0)), # 2 + (And(Abs(2*arg(p) + pi) < pi, Abs(2*arg(p) - pi) <= pi), + S.false), # 3 + (And(Abs(arg(p) - pi/2) <= pi/2, Abs(arg(p) + pi/2) <= pi/2), + Eq(arg(p), 0)), # 4 + (And(Abs(arg(p) - pi/2) <= pi/2, Abs(arg(p) + pi/2) < pi/2), + S.false), # 5 + (And(Abs(arg(p**2/2 + 1)) < pi, Ne(Abs(arg(p**2/2 + 1)), pi)), + S.true), # 6 + (Or(Abs(arg(p**2/2 + 1)) < pi, Ne(1/(p**2/2 + 1), 0)), + S.true), # 7 + (And(Abs(unbranched_argument(p)) <= pi, + Abs(unbranched_argument(exp_polar(-2*pi*S.ImaginaryUnit)*p)) <= pi), + Eq(unbranched_argument(exp_polar(-S.ImaginaryUnit*pi)*p), 0)), # 8 + (And(Abs(unbranched_argument(p)) <= pi/2, + Abs(unbranched_argument(exp_polar(-pi*S.ImaginaryUnit)*p)) <= pi/2), + Eq(unbranched_argument(exp_polar(-S.ImaginaryUnit*pi/2)*p), 0)), # 9 + (Or(p <= q, And(p < q, r)), p <= q), # 10 + (Ne(p**2, 1) & (p**2 > 1), p**2 > 1), # 11 + (Ne(1/p, 1) & (cos(Abs(arg(p)))*Abs(p) > 1), Abs(p) > 1), # 12 + (Ne(p, 2) & (cos(Abs(arg(p)))*Abs(p) > 2), Abs(p) > 2), # 13 + ((Abs(arg(p)) < pi/2) & (cos(Abs(arg(p)))*sqrt(Abs(p**2)) > 1), p**2 > 1), # 14 + ] + cond = cond.func(*[_condsimp(_, first) for _ in cond.args]) + change = True + while change: + change = False + for irule, (fro, to) in enumerate(rules): + if fro.func != cond.func: + continue + for n, arg1 in enumerate(cond.args): + if r in fro.args[0].free_symbols: + m = arg1.match(fro.args[1]) + num = 1 + else: + num = 0 + m = arg1.match(fro.args[0]) + if not m: + continue + otherargs = [x.subs(m) for x in fro.args[:num] + fro.args[num + 1:]] + otherlist = [n] + for arg2 in otherargs: + for k, arg3 in enumerate(cond.args): + if k in otherlist: + continue + if arg2 == arg3: + otherlist += [k] + break + if isinstance(arg3, And) and arg2.args[1] == r and \ + isinstance(arg2, And) and arg2.args[0] in arg3.args: + otherlist += [k] + break + if isinstance(arg3, And) and arg2.args[0] == r and \ + isinstance(arg2, And) and arg2.args[1] in arg3.args: + otherlist += [k] + break + if len(otherlist) != len(otherargs) + 1: + continue + newargs = [arg_ for (k, arg_) in enumerate(cond.args) + if k not in otherlist] + [to.subs(m)] + if SYMPY_DEBUG: + if irule not in (0, 2, 4, 5, 6, 7, 11, 12, 13, 14): + print('used new rule:', irule) + cond = cond.func(*newargs) + change = True + break + + # final tweak + def rel_touchup(rel): + if rel.rel_op != '==' or rel.rhs != 0: + return rel + + # handle Eq(*, 0) + LHS = rel.lhs + m = LHS.match(arg(p)**q) + if not m: + m = LHS.match(unbranched_argument(polar_lift(p)**q)) + if not m: + if isinstance(LHS, periodic_argument) and not LHS.args[0].is_polar \ + and LHS.args[1] is S.Infinity: + return (LHS.args[0] > 0) + return rel + return (m[p] > 0) + cond = cond.replace(lambda _: _.is_Relational, rel_touchup) + if SYMPY_DEBUG: + print('_condsimp: ', cond) + return cond + +def _eval_cond(cond): + """ Re-evaluate the conditions. """ + if isinstance(cond, bool): + return cond + return _condsimp(cond.doit()) + +#################################################################### +# Now the "backbone" functions to do actual integration. +#################################################################### + + +def _my_principal_branch(expr, period, full_pb=False): + """ Bring expr nearer to its principal branch by removing superfluous + factors. + This function does *not* guarantee to yield the principal branch, + to avoid introducing opaque principal_branch() objects, + unless full_pb=True. """ + res = principal_branch(expr, period) + if not full_pb: + res = res.replace(principal_branch, lambda x, y: x) + return res + + +def _rewrite_saxena_1(fac, po, g, x): + """ + Rewrite the integral fac*po*g dx, from zero to infinity, as + integral fac*G, where G has argument a*x. Note po=x**s. + Return fac, G. + """ + _, s = _get_coeff_exp(po, x) + a, b = _get_coeff_exp(g.argument, x) + period = g.get_period() + a = _my_principal_branch(a, period) + + # We substitute t = x**b. + C = fac/(Abs(b)*a**((s + 1)/b - 1)) + # Absorb a factor of (at)**((1 + s)/b - 1). + + def tr(l): + return [a + (1 + s)/b - 1 for a in l] + return C, meijerg(tr(g.an), tr(g.aother), tr(g.bm), tr(g.bother), + a*x) + + +def _check_antecedents_1(g, x, helper=False): + r""" + Return a condition under which the mellin transform of g exists. + Any power of x has already been absorbed into the G function, + so this is just $\int_0^\infty g\, dx$. + + See [L, section 5.6.1]. (Note that s=1.) + + If ``helper`` is True, only check if the MT exists at infinity, i.e. if + $\int_1^\infty g\, dx$ exists. + """ + # NOTE if you update these conditions, please update the documentation as well + delta = g.delta + eta, _ = _get_coeff_exp(g.argument, x) + m, n, p, q = S([len(g.bm), len(g.an), len(g.ap), len(g.bq)]) + + if p > q: + def tr(l): + return [1 - x for x in l] + return _check_antecedents_1(meijerg(tr(g.bm), tr(g.bother), + tr(g.an), tr(g.aother), x/eta), + x) + + tmp = [-re(b) < 1 for b in g.bm] + [1 < 1 - re(a) for a in g.an] + cond_3 = And(*tmp) + + tmp += [-re(b) < 1 for b in g.bother] + tmp += [1 < 1 - re(a) for a in g.aother] + cond_3_star = And(*tmp) + + cond_4 = (-re(g.nu) + (q + 1 - p)/2 > q - p) + + def debug(*msg): + _debug(*msg) + + def debugf(string, arg): + _debugf(string, arg) + + debug('Checking antecedents for 1 function:') + debugf(' delta=%s, eta=%s, m=%s, n=%s, p=%s, q=%s', + (delta, eta, m, n, p, q)) + debugf(' ap = %s, %s', (list(g.an), list(g.aother))) + debugf(' bq = %s, %s', (list(g.bm), list(g.bother))) + debugf(' cond_3=%s, cond_3*=%s, cond_4=%s', (cond_3, cond_3_star, cond_4)) + + conds = [] + + # case 1 + case1 = [] + tmp1 = [1 <= n, p < q, 1 <= m] + tmp2 = [1 <= p, 1 <= m, Eq(q, p + 1), Not(And(Eq(n, 0), Eq(m, p + 1)))] + tmp3 = [1 <= p, Eq(q, p)] + for k in range(ceiling(delta/2) + 1): + tmp3 += [Ne(Abs(unbranched_argument(eta)), (delta - 2*k)*pi)] + tmp = [delta > 0, Abs(unbranched_argument(eta)) < delta*pi] + extra = [Ne(eta, 0), cond_3] + if helper: + extra = [] + for t in [tmp1, tmp2, tmp3]: + case1 += [And(*(t + tmp + extra))] + conds += case1 + debug(' case 1:', case1) + + # case 2 + extra = [cond_3] + if helper: + extra = [] + case2 = [And(Eq(n, 0), p + 1 <= m, m <= q, + Abs(unbranched_argument(eta)) < delta*pi, *extra)] + conds += case2 + debug(' case 2:', case2) + + # case 3 + extra = [cond_3, cond_4] + if helper: + extra = [] + case3 = [And(p < q, 1 <= m, delta > 0, Eq(Abs(unbranched_argument(eta)), delta*pi), + *extra)] + case3 += [And(p <= q - 2, Eq(delta, 0), Eq(Abs(unbranched_argument(eta)), 0), *extra)] + conds += case3 + debug(' case 3:', case3) + + # TODO altered cases 4-7 + + # extra case from wofram functions site: + # (reproduced verbatim from Prudnikov, section 2.24.2) + # https://functions.wolfram.com/HypergeometricFunctions/MeijerG/21/02/01/ + case_extra = [] + case_extra += [Eq(p, q), Eq(delta, 0), Eq(unbranched_argument(eta), 0), Ne(eta, 0)] + if not helper: + case_extra += [cond_3] + s = [] + for a, b in zip(g.ap, g.bq): + s += [b - a] + case_extra += [re(Add(*s)) < 0] + case_extra = And(*case_extra) + conds += [case_extra] + debug(' extra case:', [case_extra]) + + case_extra_2 = [And(delta > 0, Abs(unbranched_argument(eta)) < delta*pi)] + if not helper: + case_extra_2 += [cond_3] + case_extra_2 = And(*case_extra_2) + conds += [case_extra_2] + debug(' second extra case:', [case_extra_2]) + + # TODO This leaves only one case from the three listed by Prudnikov. + # Investigate if these indeed cover everything; if so, remove the rest. + + return Or(*conds) + + +def _int0oo_1(g, x): + r""" + Evaluate $\int_0^\infty g\, dx$ using G functions, + assuming the necessary conditions are fulfilled. + + Examples + ======== + + >>> from sympy.abc import a, b, c, d, x, y + >>> from sympy import meijerg + >>> from sympy.integrals.meijerint import _int0oo_1 + >>> _int0oo_1(meijerg([a], [b], [c], [d], x*y), x) + gamma(-a)*gamma(c + 1)/(y*gamma(-d)*gamma(b + 1)) + """ + from sympy.simplify import gammasimp + # See [L, section 5.6.1]. Note that s=1. + eta, _ = _get_coeff_exp(g.argument, x) + res = 1/eta + # XXX TODO we should reduce order first + for b in g.bm: + res *= gamma(b + 1) + for a in g.an: + res *= gamma(1 - a - 1) + for b in g.bother: + res /= gamma(1 - b - 1) + for a in g.aother: + res /= gamma(a + 1) + return gammasimp(unpolarify(res)) + + +def _rewrite_saxena(fac, po, g1, g2, x, full_pb=False): + """ + Rewrite the integral ``fac*po*g1*g2`` from 0 to oo in terms of G + functions with argument ``c*x``. + + Explanation + =========== + + Return C, f1, f2 such that integral C f1 f2 from 0 to infinity equals + integral fac ``po``, ``g1``, ``g2`` from 0 to infinity. + + Examples + ======== + + >>> from sympy.integrals.meijerint import _rewrite_saxena + >>> from sympy.abc import s, t, m + >>> from sympy import meijerg + >>> g1 = meijerg([], [], [0], [], s*t) + >>> g2 = meijerg([], [], [m/2], [-m/2], t**2/4) + >>> r = _rewrite_saxena(1, t**0, g1, g2, t) + >>> r[0] + s/(4*sqrt(pi)) + >>> r[1] + meijerg(((), ()), ((-1/2, 0), ()), s**2*t/4) + >>> r[2] + meijerg(((), ()), ((m/2,), (-m/2,)), t/4) + """ + def pb(g): + a, b = _get_coeff_exp(g.argument, x) + per = g.get_period() + return meijerg(g.an, g.aother, g.bm, g.bother, + _my_principal_branch(a, per, full_pb)*x**b) + + _, s = _get_coeff_exp(po, x) + _, b1 = _get_coeff_exp(g1.argument, x) + _, b2 = _get_coeff_exp(g2.argument, x) + if (b1 < 0) == True: + b1 = -b1 + g1 = _flip_g(g1) + if (b2 < 0) == True: + b2 = -b2 + g2 = _flip_g(g2) + if not b1.is_Rational or not b2.is_Rational: + return + m1, n1 = b1.p, b1.q + m2, n2 = b2.p, b2.q + tau = ilcm(m1*n2, m2*n1) + r1 = tau//(m1*n2) + r2 = tau//(m2*n1) + + C1, g1 = _inflate_g(g1, r1) + C2, g2 = _inflate_g(g2, r2) + g1 = pb(g1) + g2 = pb(g2) + + fac *= C1*C2 + a1, b = _get_coeff_exp(g1.argument, x) + a2, _ = _get_coeff_exp(g2.argument, x) + + # arbitrarily tack on the x**s part to g1 + # TODO should we try both? + exp = (s + 1)/b - 1 + fac = fac/(Abs(b) * a1**exp) + + def tr(l): + return [a + exp for a in l] + g1 = meijerg(tr(g1.an), tr(g1.aother), tr(g1.bm), tr(g1.bother), a1*x) + g2 = meijerg(g2.an, g2.aother, g2.bm, g2.bother, a2*x) + + from sympy.simplify import powdenest + return powdenest(fac, polar=True), g1, g2 + + +def _check_antecedents(g1, g2, x): + """ Return a condition under which the integral theorem applies. """ + # Yes, this is madness. + # XXX TODO this is a testing *nightmare* + # NOTE if you update these conditions, please update the documentation as well + + # The following conditions are found in + # [P], Section 2.24.1 + # + # They are also reproduced (verbatim!) at + # https://functions.wolfram.com/HypergeometricFunctions/MeijerG/21/02/03/ + # + # Note: k=l=r=alpha=1 + sigma, _ = _get_coeff_exp(g1.argument, x) + omega, _ = _get_coeff_exp(g2.argument, x) + s, t, u, v = S([len(g1.bm), len(g1.an), len(g1.ap), len(g1.bq)]) + m, n, p, q = S([len(g2.bm), len(g2.an), len(g2.ap), len(g2.bq)]) + bstar = s + t - (u + v)/2 + cstar = m + n - (p + q)/2 + rho = g1.nu + (u - v)/2 + 1 + mu = g2.nu + (p - q)/2 + 1 + phi = q - p - (v - u) + eta = 1 - (v - u) - mu - rho + psi = (pi*(q - m - n) + Abs(unbranched_argument(omega)))/(q - p) + theta = (pi*(v - s - t) + Abs(unbranched_argument(sigma)))/(v - u) + + _debug('Checking antecedents:') + _debugf(' sigma=%s, s=%s, t=%s, u=%s, v=%s, b*=%s, rho=%s', + (sigma, s, t, u, v, bstar, rho)) + _debugf(' omega=%s, m=%s, n=%s, p=%s, q=%s, c*=%s, mu=%s,', + (omega, m, n, p, q, cstar, mu)) + _debugf(' phi=%s, eta=%s, psi=%s, theta=%s', (phi, eta, psi, theta)) + + def _c1(): + for g in [g1, g2]: + for i, j in itertools.product(g.an, g.bm): + diff = i - j + if diff.is_integer and diff.is_positive: + return False + return True + c1 = _c1() + c2 = And(*[re(1 + i + j) > 0 for i in g1.bm for j in g2.bm]) + c3 = And(*[re(1 + i + j) < 1 + 1 for i in g1.an for j in g2.an]) + c4 = And(*[(p - q)*re(1 + i - 1) - re(mu) > Rational(-3, 2) for i in g1.an]) + c5 = And(*[(p - q)*re(1 + i) - re(mu) > Rational(-3, 2) for i in g1.bm]) + c6 = And(*[(u - v)*re(1 + i - 1) - re(rho) > Rational(-3, 2) for i in g2.an]) + c7 = And(*[(u - v)*re(1 + i) - re(rho) > Rational(-3, 2) for i in g2.bm]) + c8 = (Abs(phi) + 2*re((rho - 1)*(q - p) + (v - u)*(q - p) + (mu - + 1)*(v - u)) > 0) + c9 = (Abs(phi) - 2*re((rho - 1)*(q - p) + (v - u)*(q - p) + (mu - + 1)*(v - u)) > 0) + c10 = (Abs(unbranched_argument(sigma)) < bstar*pi) + c11 = Eq(Abs(unbranched_argument(sigma)), bstar*pi) + c12 = (Abs(unbranched_argument(omega)) < cstar*pi) + c13 = Eq(Abs(unbranched_argument(omega)), cstar*pi) + + # The following condition is *not* implemented as stated on the wolfram + # function site. In the book of Prudnikov there is an additional part + # (the And involving re()). However, I only have this book in russian, and + # I don't read any russian. The following condition is what other people + # have told me it means. + # Worryingly, it is different from the condition implemented in REDUCE. + # The REDUCE implementation: + # https://reduce-algebra.svn.sourceforge.net/svnroot/reduce-algebra/trunk/packages/defint/definta.red + # (search for tst14) + # The Wolfram alpha version: + # https://functions.wolfram.com/HypergeometricFunctions/MeijerG/21/02/03/03/0014/ + z0 = exp(-(bstar + cstar)*pi*S.ImaginaryUnit) + zos = unpolarify(z0*omega/sigma) + zso = unpolarify(z0*sigma/omega) + if zos == 1/zso: + c14 = And(Eq(phi, 0), bstar + cstar <= 1, + Or(Ne(zos, 1), re(mu + rho + v - u) < 1, + re(mu + rho + q - p) < 1)) + else: + def _cond(z): + '''Returns True if abs(arg(1-z)) < pi, avoiding arg(0). + + Explanation + =========== + + If ``z`` is 1 then arg is NaN. This raises a + TypeError on `NaN < pi`. Previously this gave `False` so + this behavior has been hardcoded here but someone should + check if this NaN is more serious! This NaN is triggered by + test_meijerint() in test_meijerint.py: + `meijerint_definite(exp(x), x, 0, I)` + ''' + return z != 1 and Abs(arg(1 - z)) < pi + + c14 = And(Eq(phi, 0), bstar - 1 + cstar <= 0, + Or(And(Ne(zos, 1), _cond(zos)), + And(re(mu + rho + v - u) < 1, Eq(zos, 1)))) + + c14_alt = And(Eq(phi, 0), cstar - 1 + bstar <= 0, + Or(And(Ne(zso, 1), _cond(zso)), + And(re(mu + rho + q - p) < 1, Eq(zso, 1)))) + + # Since r=k=l=1, in our case there is c14_alt which is the same as calling + # us with (g1, g2) = (g2, g1). The conditions below enumerate all cases + # (i.e. we don't have to try arguments reversed by hand), and indeed try + # all symmetric cases. (i.e. whenever there is a condition involving c14, + # there is also a dual condition which is exactly what we would get when g1, + # g2 were interchanged, *but c14 was unaltered*). + # Hence the following seems correct: + c14 = Or(c14, c14_alt) + + ''' + When `c15` is NaN (e.g. from `psi` being NaN as happens during + 'test_issue_4992' and/or `theta` is NaN as in 'test_issue_6253', + both in `test_integrals.py`) the comparison to 0 formerly gave False + whereas now an error is raised. To keep the old behavior, the value + of NaN is replaced with False but perhaps a closer look at this condition + should be made: XXX how should conditions leading to c15=NaN be handled? + ''' + try: + lambda_c = (q - p)*Abs(omega)**(1/(q - p))*cos(psi) \ + + (v - u)*Abs(sigma)**(1/(v - u))*cos(theta) + # the TypeError might be raised here, e.g. if lambda_c is NaN + if _eval_cond(lambda_c > 0) != False: + c15 = (lambda_c > 0) + else: + def lambda_s0(c1, c2): + return c1*(q - p)*Abs(omega)**(1/(q - p))*sin(psi) \ + + c2*(v - u)*Abs(sigma)**(1/(v - u))*sin(theta) + lambda_s = Piecewise( + ((lambda_s0(+1, +1)*lambda_s0(-1, -1)), + And(Eq(unbranched_argument(sigma), 0), Eq(unbranched_argument(omega), 0))), + (lambda_s0(sign(unbranched_argument(omega)), +1)*lambda_s0(sign(unbranched_argument(omega)), -1), + And(Eq(unbranched_argument(sigma), 0), Ne(unbranched_argument(omega), 0))), + (lambda_s0(+1, sign(unbranched_argument(sigma)))*lambda_s0(-1, sign(unbranched_argument(sigma))), + And(Ne(unbranched_argument(sigma), 0), Eq(unbranched_argument(omega), 0))), + (lambda_s0(sign(unbranched_argument(omega)), sign(unbranched_argument(sigma))), True)) + tmp = [lambda_c > 0, + And(Eq(lambda_c, 0), Ne(lambda_s, 0), re(eta) > -1), + And(Eq(lambda_c, 0), Eq(lambda_s, 0), re(eta) > 0)] + c15 = Or(*tmp) + except TypeError: + c15 = False + for cond, i in [(c1, 1), (c2, 2), (c3, 3), (c4, 4), (c5, 5), (c6, 6), + (c7, 7), (c8, 8), (c9, 9), (c10, 10), (c11, 11), + (c12, 12), (c13, 13), (c14, 14), (c15, 15)]: + _debugf(' c%s: %s', (i, cond)) + + # We will return Or(*conds) + conds = [] + + def pr(count): + _debugf(' case %s: %s', (count, conds[-1])) + conds += [And(m*n*s*t != 0, bstar.is_positive is True, cstar.is_positive is True, c1, c2, c3, c10, + c12)] # 1 + pr(1) + conds += [And(Eq(u, v), Eq(bstar, 0), cstar.is_positive is True, sigma.is_positive is True, re(rho) < 1, + c1, c2, c3, c12)] # 2 + pr(2) + conds += [And(Eq(p, q), Eq(cstar, 0), bstar.is_positive is True, omega.is_positive is True, re(mu) < 1, + c1, c2, c3, c10)] # 3 + pr(3) + conds += [And(Eq(p, q), Eq(u, v), Eq(bstar, 0), Eq(cstar, 0), + sigma.is_positive is True, omega.is_positive is True, re(mu) < 1, re(rho) < 1, + Ne(sigma, omega), c1, c2, c3)] # 4 + pr(4) + conds += [And(Eq(p, q), Eq(u, v), Eq(bstar, 0), Eq(cstar, 0), + sigma.is_positive is True, omega.is_positive is True, re(mu + rho) < 1, + Ne(omega, sigma), c1, c2, c3)] # 5 + pr(5) + conds += [And(p > q, s.is_positive is True, bstar.is_positive is True, cstar >= 0, + c1, c2, c3, c5, c10, c13)] # 6 + pr(6) + conds += [And(p < q, t.is_positive is True, bstar.is_positive is True, cstar >= 0, + c1, c2, c3, c4, c10, c13)] # 7 + pr(7) + conds += [And(u > v, m.is_positive is True, cstar.is_positive is True, bstar >= 0, + c1, c2, c3, c7, c11, c12)] # 8 + pr(8) + conds += [And(u < v, n.is_positive is True, cstar.is_positive is True, bstar >= 0, + c1, c2, c3, c6, c11, c12)] # 9 + pr(9) + conds += [And(p > q, Eq(u, v), Eq(bstar, 0), cstar >= 0, sigma.is_positive is True, + re(rho) < 1, c1, c2, c3, c5, c13)] # 10 + pr(10) + conds += [And(p < q, Eq(u, v), Eq(bstar, 0), cstar >= 0, sigma.is_positive is True, + re(rho) < 1, c1, c2, c3, c4, c13)] # 11 + pr(11) + conds += [And(Eq(p, q), u > v, bstar >= 0, Eq(cstar, 0), omega.is_positive is True, + re(mu) < 1, c1, c2, c3, c7, c11)] # 12 + pr(12) + conds += [And(Eq(p, q), u < v, bstar >= 0, Eq(cstar, 0), omega.is_positive is True, + re(mu) < 1, c1, c2, c3, c6, c11)] # 13 + pr(13) + conds += [And(p < q, u > v, bstar >= 0, cstar >= 0, + c1, c2, c3, c4, c7, c11, c13)] # 14 + pr(14) + conds += [And(p > q, u < v, bstar >= 0, cstar >= 0, + c1, c2, c3, c5, c6, c11, c13)] # 15 + pr(15) + conds += [And(p > q, u > v, bstar >= 0, cstar >= 0, + c1, c2, c3, c5, c7, c8, c11, c13, c14)] # 16 + pr(16) + conds += [And(p < q, u < v, bstar >= 0, cstar >= 0, + c1, c2, c3, c4, c6, c9, c11, c13, c14)] # 17 + pr(17) + conds += [And(Eq(t, 0), s.is_positive is True, bstar.is_positive is True, phi.is_positive is True, c1, c2, c10)] # 18 + pr(18) + conds += [And(Eq(s, 0), t.is_positive is True, bstar.is_positive is True, phi.is_negative is True, c1, c3, c10)] # 19 + pr(19) + conds += [And(Eq(n, 0), m.is_positive is True, cstar.is_positive is True, phi.is_negative is True, c1, c2, c12)] # 20 + pr(20) + conds += [And(Eq(m, 0), n.is_positive is True, cstar.is_positive is True, phi.is_positive is True, c1, c3, c12)] # 21 + pr(21) + conds += [And(Eq(s*t, 0), bstar.is_positive is True, cstar.is_positive is True, + c1, c2, c3, c10, c12)] # 22 + pr(22) + conds += [And(Eq(m*n, 0), bstar.is_positive is True, cstar.is_positive is True, + c1, c2, c3, c10, c12)] # 23 + pr(23) + + # The following case is from [Luke1969]. As far as I can tell, it is *not* + # covered by Prudnikov's. + # Let G1 and G2 be the two G-functions. Suppose the integral exists from + # 0 to a > 0 (this is easy the easy part), that G1 is exponential decay at + # infinity, and that the mellin transform of G2 exists. + # Then the integral exists. + mt1_exists = _check_antecedents_1(g1, x, helper=True) + mt2_exists = _check_antecedents_1(g2, x, helper=True) + conds += [And(mt2_exists, Eq(t, 0), u < s, bstar.is_positive is True, c10, c1, c2, c3)] + pr('E1') + conds += [And(mt2_exists, Eq(s, 0), v < t, bstar.is_positive is True, c10, c1, c2, c3)] + pr('E2') + conds += [And(mt1_exists, Eq(n, 0), p < m, cstar.is_positive is True, c12, c1, c2, c3)] + pr('E3') + conds += [And(mt1_exists, Eq(m, 0), q < n, cstar.is_positive is True, c12, c1, c2, c3)] + pr('E4') + + # Let's short-circuit if this worked ... + # the rest is corner-cases and terrible to read. + r = Or(*conds) + if _eval_cond(r) != False: + return r + + conds += [And(m + n > p, Eq(t, 0), Eq(phi, 0), s.is_positive is True, bstar.is_positive is True, cstar.is_negative is True, + Abs(unbranched_argument(omega)) < (m + n - p + 1)*pi, + c1, c2, c10, c14, c15)] # 24 + pr(24) + conds += [And(m + n > q, Eq(s, 0), Eq(phi, 0), t.is_positive is True, bstar.is_positive is True, cstar.is_negative is True, + Abs(unbranched_argument(omega)) < (m + n - q + 1)*pi, + c1, c3, c10, c14, c15)] # 25 + pr(25) + conds += [And(Eq(p, q - 1), Eq(t, 0), Eq(phi, 0), s.is_positive is True, bstar.is_positive is True, + cstar >= 0, cstar*pi < Abs(unbranched_argument(omega)), + c1, c2, c10, c14, c15)] # 26 + pr(26) + conds += [And(Eq(p, q + 1), Eq(s, 0), Eq(phi, 0), t.is_positive is True, bstar.is_positive is True, + cstar >= 0, cstar*pi < Abs(unbranched_argument(omega)), + c1, c3, c10, c14, c15)] # 27 + pr(27) + conds += [And(p < q - 1, Eq(t, 0), Eq(phi, 0), s.is_positive is True, bstar.is_positive is True, + cstar >= 0, cstar*pi < Abs(unbranched_argument(omega)), + Abs(unbranched_argument(omega)) < (m + n - p + 1)*pi, + c1, c2, c10, c14, c15)] # 28 + pr(28) + conds += [And( + p > q + 1, Eq(s, 0), Eq(phi, 0), t.is_positive is True, bstar.is_positive is True, cstar >= 0, + cstar*pi < Abs(unbranched_argument(omega)), + Abs(unbranched_argument(omega)) < (m + n - q + 1)*pi, + c1, c3, c10, c14, c15)] # 29 + pr(29) + conds += [And(Eq(n, 0), Eq(phi, 0), s + t > 0, m.is_positive is True, cstar.is_positive is True, bstar.is_negative is True, + Abs(unbranched_argument(sigma)) < (s + t - u + 1)*pi, + c1, c2, c12, c14, c15)] # 30 + pr(30) + conds += [And(Eq(m, 0), Eq(phi, 0), s + t > v, n.is_positive is True, cstar.is_positive is True, bstar.is_negative is True, + Abs(unbranched_argument(sigma)) < (s + t - v + 1)*pi, + c1, c3, c12, c14, c15)] # 31 + pr(31) + conds += [And(Eq(n, 0), Eq(phi, 0), Eq(u, v - 1), m.is_positive is True, cstar.is_positive is True, + bstar >= 0, bstar*pi < Abs(unbranched_argument(sigma)), + Abs(unbranched_argument(sigma)) < (bstar + 1)*pi, + c1, c2, c12, c14, c15)] # 32 + pr(32) + conds += [And(Eq(m, 0), Eq(phi, 0), Eq(u, v + 1), n.is_positive is True, cstar.is_positive is True, + bstar >= 0, bstar*pi < Abs(unbranched_argument(sigma)), + Abs(unbranched_argument(sigma)) < (bstar + 1)*pi, + c1, c3, c12, c14, c15)] # 33 + pr(33) + conds += [And( + Eq(n, 0), Eq(phi, 0), u < v - 1, m.is_positive is True, cstar.is_positive is True, bstar >= 0, + bstar*pi < Abs(unbranched_argument(sigma)), + Abs(unbranched_argument(sigma)) < (s + t - u + 1)*pi, + c1, c2, c12, c14, c15)] # 34 + pr(34) + conds += [And( + Eq(m, 0), Eq(phi, 0), u > v + 1, n.is_positive is True, cstar.is_positive is True, bstar >= 0, + bstar*pi < Abs(unbranched_argument(sigma)), + Abs(unbranched_argument(sigma)) < (s + t - v + 1)*pi, + c1, c3, c12, c14, c15)] # 35 + pr(35) + + return Or(*conds) + + # NOTE An alternative, but as far as I can tell weaker, set of conditions + # can be found in [L, section 5.6.2]. + + +def _int0oo(g1, g2, x): + """ + Express integral from zero to infinity g1*g2 using a G function, + assuming the necessary conditions are fulfilled. + + Examples + ======== + + >>> from sympy.integrals.meijerint import _int0oo + >>> from sympy.abc import s, t, m + >>> from sympy import meijerg, S + >>> g1 = meijerg([], [], [-S(1)/2, 0], [], s**2*t/4) + >>> g2 = meijerg([], [], [m/2], [-m/2], t/4) + >>> _int0oo(g1, g2, t) + 4*meijerg(((0, 1/2), ()), ((m/2,), (-m/2,)), s**(-2))/s**2 + """ + # See: [L, section 5.6.2, equation (1)] + eta, _ = _get_coeff_exp(g1.argument, x) + omega, _ = _get_coeff_exp(g2.argument, x) + + def neg(l): + return [-x for x in l] + a1 = neg(g1.bm) + list(g2.an) + a2 = list(g2.aother) + neg(g1.bother) + b1 = neg(g1.an) + list(g2.bm) + b2 = list(g2.bother) + neg(g1.aother) + return meijerg(a1, a2, b1, b2, omega/eta)/eta + + +def _rewrite_inversion(fac, po, g, x): + """ Absorb ``po`` == x**s into g. """ + _, s = _get_coeff_exp(po, x) + a, b = _get_coeff_exp(g.argument, x) + + def tr(l): + return [t + s/b for t in l] + from sympy.simplify import powdenest + return (powdenest(fac/a**(s/b), polar=True), + meijerg(tr(g.an), tr(g.aother), tr(g.bm), tr(g.bother), g.argument)) + + +def _check_antecedents_inversion(g, x): + """ Check antecedents for the laplace inversion integral. """ + _debug('Checking antecedents for inversion:') + z = g.argument + _, e = _get_coeff_exp(z, x) + if e < 0: + _debug(' Flipping G.') + # We want to assume that argument gets large as |x| -> oo + return _check_antecedents_inversion(_flip_g(g), x) + + def statement_half(a, b, c, z, plus): + coeff, exponent = _get_coeff_exp(z, x) + a *= exponent + b *= coeff**c + c *= exponent + conds = [] + wp = b*exp(S.ImaginaryUnit*re(c)*pi/2) + wm = b*exp(-S.ImaginaryUnit*re(c)*pi/2) + if plus: + w = wp + else: + w = wm + conds += [And(Or(Eq(b, 0), re(c) <= 0), re(a) <= -1)] + conds += [And(Ne(b, 0), Eq(im(c), 0), re(c) > 0, re(w) < 0)] + conds += [And(Ne(b, 0), Eq(im(c), 0), re(c) > 0, re(w) <= 0, + re(a) <= -1)] + return Or(*conds) + + def statement(a, b, c, z): + """ Provide a convergence statement for z**a * exp(b*z**c), + c/f sphinx docs. """ + return And(statement_half(a, b, c, z, True), + statement_half(a, b, c, z, False)) + + # Notations from [L], section 5.7-10 + m, n, p, q = S([len(g.bm), len(g.an), len(g.ap), len(g.bq)]) + tau = m + n - p + nu = q - m - n + rho = (tau - nu)/2 + sigma = q - p + if sigma == 1: + epsilon = S.Half + elif sigma > 1: + epsilon = 1 + else: + epsilon = S.NaN + theta = ((1 - sigma)/2 + Add(*g.bq) - Add(*g.ap))/sigma + delta = g.delta + _debugf(' m=%s, n=%s, p=%s, q=%s, tau=%s, nu=%s, rho=%s, sigma=%s', + (m, n, p, q, tau, nu, rho, sigma)) + _debugf(' epsilon=%s, theta=%s, delta=%s', (epsilon, theta, delta)) + + # First check if the computation is valid. + if not (g.delta >= e/2 or (p >= 1 and p >= q)): + _debug(' Computation not valid for these parameters.') + return False + + # Now check if the inversion integral exists. + + # Test "condition A" + for a, b in itertools.product(g.an, g.bm): + if (a - b).is_integer and a > b: + _debug(' Not a valid G function.') + return False + + # There are two cases. If p >= q, we can directly use a slater expansion + # like [L], 5.2 (11). Note in particular that the asymptotics of such an + # expansion even hold when some of the parameters differ by integers, i.e. + # the formula itself would not be valid! (b/c G functions are cts. in their + # parameters) + # When p < q, we need to use the theorems of [L], 5.10. + + if p >= q: + _debug(' Using asymptotic Slater expansion.') + return And(*[statement(a - 1, 0, 0, z) for a in g.an]) + + def E(z): + return And(*[statement(a - 1, 0, 0, z) for a in g.an]) + + def H(z): + return statement(theta, -sigma, 1/sigma, z) + + def Hp(z): + return statement_half(theta, -sigma, 1/sigma, z, True) + + def Hm(z): + return statement_half(theta, -sigma, 1/sigma, z, False) + + # [L], section 5.10 + conds = [] + # Theorem 1 -- p < q from test above + conds += [And(1 <= n, 1 <= m, rho*pi - delta >= pi/2, delta > 0, + E(z*exp(S.ImaginaryUnit*pi*(nu + 1))))] + # Theorem 2, statements (2) and (3) + conds += [And(p + 1 <= m, m + 1 <= q, delta > 0, delta < pi/2, n == 0, + (m - p + 1)*pi - delta >= pi/2, + Hp(z*exp(S.ImaginaryUnit*pi*(q - m))), + Hm(z*exp(-S.ImaginaryUnit*pi*(q - m))))] + # Theorem 2, statement (5) -- p < q from test above + conds += [And(m == q, n == 0, delta > 0, + (sigma + epsilon)*pi - delta >= pi/2, H(z))] + # Theorem 3, statements (6) and (7) + conds += [And(Or(And(p <= q - 2, 1 <= tau, tau <= sigma/2), + And(p + 1 <= m + n, m + n <= (p + q)/2)), + delta > 0, delta < pi/2, (tau + 1)*pi - delta >= pi/2, + Hp(z*exp(S.ImaginaryUnit*pi*nu)), + Hm(z*exp(-S.ImaginaryUnit*pi*nu)))] + # Theorem 4, statements (10) and (11) -- p < q from test above + conds += [And(1 <= m, rho > 0, delta > 0, delta + rho*pi < pi/2, + (tau + epsilon)*pi - delta >= pi/2, + Hp(z*exp(S.ImaginaryUnit*pi*nu)), + Hm(z*exp(-S.ImaginaryUnit*pi*nu)))] + # Trivial case + conds += [m == 0] + + # TODO + # Theorem 5 is quite general + # Theorem 6 contains special cases for q=p+1 + + return Or(*conds) + + +def _int_inversion(g, x, t): + """ + Compute the laplace inversion integral, assuming the formula applies. + """ + b, a = _get_coeff_exp(g.argument, x) + C, g = _inflate_fox_h(meijerg(g.an, g.aother, g.bm, g.bother, b/t**a), -a) + return C/t*g + + +#################################################################### +# Finally, the real meat. +#################################################################### + +_lookup_table = None + + +@cacheit +@timeit +def _rewrite_single(f, x, recursive=True): + """ + Try to rewrite f as a sum of single G functions of the form + C*x**s*G(a*x**b), where b is a rational number and C is independent of x. + We guarantee that result.argument.as_coeff_mul(x) returns (a, (x**b,)) + or (a, ()). + Returns a list of tuples (C, s, G) and a condition cond. + Returns None on failure. + """ + from .transforms import (mellin_transform, inverse_mellin_transform, + IntegralTransformError, MellinTransformStripError) + + global _lookup_table + if not _lookup_table: + _lookup_table = {} + _create_lookup_table(_lookup_table) + + if isinstance(f, meijerg): + coeff, m = factor(f.argument, x).as_coeff_mul(x) + if len(m) > 1: + return None + m = m[0] + if m.is_Pow: + if m.base != x or not m.exp.is_Rational: + return None + elif m != x: + return None + return [(1, 0, meijerg(f.an, f.aother, f.bm, f.bother, coeff*m))], True + + f_ = f + f = f.subs(x, z) + t = _mytype(f, z) + if t in _lookup_table: + l = _lookup_table[t] + for formula, terms, cond, hint in l: + subs = f.match(formula, old=True) + if subs: + subs_ = {} + for fro, to in subs.items(): + subs_[fro] = unpolarify(polarify(to, lift=True), + exponents_only=True) + subs = subs_ + if not isinstance(hint, bool): + hint = hint.subs(subs) + if hint == False: + continue + if not isinstance(cond, (bool, BooleanAtom)): + cond = unpolarify(cond.subs(subs)) + if _eval_cond(cond) == False: + continue + if not isinstance(terms, list): + terms = terms(subs) + res = [] + for fac, g in terms: + r1 = _get_coeff_exp(unpolarify(fac.subs(subs).subs(z, x), + exponents_only=True), x) + try: + g = g.subs(subs).subs(z, x) + except ValueError: + continue + # NOTE these substitutions can in principle introduce oo, + # zoo and other absurdities. It shouldn't matter, + # but better be safe. + if Tuple(*(r1 + (g,))).has(S.Infinity, S.ComplexInfinity, S.NegativeInfinity): + continue + g = meijerg(g.an, g.aother, g.bm, g.bother, + unpolarify(g.argument, exponents_only=True)) + res.append(r1 + (g,)) + if res: + return res, cond + + # try recursive mellin transform + if not recursive: + return None + _debug('Trying recursive Mellin transform method.') + + def my_imt(F, s, x, strip): + """ Calling simplify() all the time is slow and not helpful, since + most of the time it only factors things in a way that has to be + un-done anyway. But sometimes it can remove apparent poles. """ + # XXX should this be in inverse_mellin_transform? + try: + return inverse_mellin_transform(F, s, x, strip, + as_meijerg=True, needeval=True) + except MellinTransformStripError: + from sympy.simplify import simplify + return inverse_mellin_transform( + simplify(cancel(expand(F))), s, x, strip, + as_meijerg=True, needeval=True) + f = f_ + s = _dummy('s', 'rewrite-single', f) + # to avoid infinite recursion, we have to force the two g functions case + + def my_integrator(f, x): + r = _meijerint_definite_4(f, x, only_double=True) + if r is not None: + from sympy.simplify import hyperexpand + res, cond = r + res = _my_unpolarify(hyperexpand(res, rewrite='nonrepsmall')) + return Piecewise((res, cond), + (Integral(f, (x, S.Zero, S.Infinity)), True)) + return Integral(f, (x, S.Zero, S.Infinity)) + try: + F, strip, _ = mellin_transform(f, x, s, integrator=my_integrator, + simplify=False, needeval=True) + g = my_imt(F, s, x, strip) + except IntegralTransformError: + g = None + if g is None: + # We try to find an expression by analytic continuation. + # (also if the dummy is already in the expression, there is no point in + # putting in another one) + a = _dummy_('a', 'rewrite-single') + if a not in f.free_symbols and _is_analytic(f, x): + try: + F, strip, _ = mellin_transform(f.subs(x, a*x), x, s, + integrator=my_integrator, + needeval=True, simplify=False) + g = my_imt(F, s, x, strip).subs(a, 1) + except IntegralTransformError: + g = None + if g is None or g.has(S.Infinity, S.NaN, S.ComplexInfinity): + _debug('Recursive Mellin transform failed.') + return None + args = Add.make_args(g) + res = [] + for f in args: + c, m = f.as_coeff_mul(x) + if len(m) > 1: + raise NotImplementedError('Unexpected form...') + g = m[0] + a, b = _get_coeff_exp(g.argument, x) + res += [(c, 0, meijerg(g.an, g.aother, g.bm, g.bother, + unpolarify(polarify( + a, lift=True), exponents_only=True) + *x**b))] + _debug('Recursive Mellin transform worked:', g) + return res, True + + +def _rewrite1(f, x, recursive=True): + """ + Try to rewrite ``f`` using a (sum of) single G functions with argument a*x**b. + Return fac, po, g such that f = fac*po*g, fac is independent of ``x``. + and po = x**s. + Here g is a result from _rewrite_single. + Return None on failure. + """ + fac, po, g = _split_mul(f, x) + g = _rewrite_single(g, x, recursive) + if g: + return fac, po, g[0], g[1] + + +def _rewrite2(f, x): + """ + Try to rewrite ``f`` as a product of two G functions of arguments a*x**b. + Return fac, po, g1, g2 such that f = fac*po*g1*g2, where fac is + independent of x and po is x**s. + Here g1 and g2 are results of _rewrite_single. + Returns None on failure. + """ + fac, po, g = _split_mul(f, x) + if any(_rewrite_single(expr, x, False) is None for expr in _mul_args(g)): + return None + l = _mul_as_two_parts(g) + if not l: + return None + l = list(ordered(l, [ + lambda p: max(len(_exponents(p[0], x)), len(_exponents(p[1], x))), + lambda p: max(len(_functions(p[0], x)), len(_functions(p[1], x))), + lambda p: max(len(_find_splitting_points(p[0], x)), + len(_find_splitting_points(p[1], x)))])) + + for recursive, (fac1, fac2) in itertools.product((False, True), l): + g1 = _rewrite_single(fac1, x, recursive) + g2 = _rewrite_single(fac2, x, recursive) + if g1 and g2: + cond = And(g1[1], g2[1]) + if cond != False: + return fac, po, g1[0], g2[0], cond + + +def meijerint_indefinite(f, x): + """ + Compute an indefinite integral of ``f`` by rewriting it as a G function. + + Examples + ======== + + >>> from sympy.integrals.meijerint import meijerint_indefinite + >>> from sympy import sin + >>> from sympy.abc import x + >>> meijerint_indefinite(sin(x), x) + -cos(x) + """ + f = sympify(f) + results = [] + for a in sorted(_find_splitting_points(f, x) | {S.Zero}, key=default_sort_key): + res = _meijerint_indefinite_1(f.subs(x, x + a), x) + if not res: + continue + res = res.subs(x, x - a) + if _has(res, hyper, meijerg): + results.append(res) + else: + return res + if f.has(HyperbolicFunction): + _debug('Try rewriting hyperbolics in terms of exp.') + rv = meijerint_indefinite( + _rewrite_hyperbolics_as_exp(f), x) + if rv: + if not isinstance(rv, list): + from sympy.simplify.radsimp import collect + return collect(factor_terms(rv), rv.atoms(exp)) + results.extend(rv) + if results: + return next(ordered(results)) + + +def _meijerint_indefinite_1(f, x): + """ Helper that does not attempt any substitution. """ + _debug('Trying to compute the indefinite integral of', f, 'wrt', x) + from sympy.simplify import hyperexpand, powdenest + + gs = _rewrite1(f, x) + if gs is None: + # Note: the code that calls us will do expand() and try again + return None + + fac, po, gl, cond = gs + _debug(' could rewrite:', gs) + res = S.Zero + for C, s, g in gl: + a, b = _get_coeff_exp(g.argument, x) + _, c = _get_coeff_exp(po, x) + c += s + + # we do a substitution t=a*x**b, get integrand fac*t**rho*g + fac_ = fac * C * x**(1 + c) / b + rho = (c + 1)/b + + # we now use t**rho*G(params, t) = G(params + rho, t) + # [L, page 150, equation (4)] + # and integral G(params, t) dt = G(1, params+1, 0, t) + # (or a similar expression with 1 and 0 exchanged ... pick the one + # which yields a well-defined function) + # [R, section 5] + # (Note that this dummy will immediately go away again, so we + # can safely pass S.One for ``expr``.) + t = _dummy('t', 'meijerint-indefinite', S.One) + + def tr(p): + return [a + rho for a in p] + if any(b.is_integer and (b <= 0) == True for b in tr(g.bm)): + r = -meijerg( + list(g.an), list(g.aother) + [1-rho], list(g.bm) + [-rho], list(g.bother), t) + else: + r = meijerg( + list(g.an) + [1-rho], list(g.aother), list(g.bm), list(g.bother) + [-rho], t) + # The antiderivative is most often expected to be defined + # in the neighborhood of x = 0. + if b.is_extended_nonnegative and not f.subs(x, 0).has(S.NaN, S.ComplexInfinity): + place = 0 # Assume we can expand at zero + else: + place = None + r = hyperexpand(r.subs(t, a*x**b), place=place) + + # now substitute back + # Note: we really do want the powers of x to combine. + res += powdenest(fac_*r, polar=True) + + def _clean(res): + """This multiplies out superfluous powers of x we created, and chops off + constants: + + >> _clean(x*(exp(x)/x - 1/x) + 3) + exp(x) + + cancel is used before mul_expand since it is possible for an + expression to have an additive constant that does not become isolated + with simple expansion. Such a situation was identified in issue 6369: + + Examples + ======== + + >>> from sympy import sqrt, cancel + >>> from sympy.abc import x + >>> a = sqrt(2*x + 1) + >>> bad = (3*x*a**5 + 2*x - a**5 + 1)/a**2 + >>> bad.expand().as_independent(x)[0] + 0 + >>> cancel(bad).expand().as_independent(x)[0] + 1 + """ + res = expand_mul(cancel(res), deep=False) + return Add._from_args(res.as_coeff_add(x)[1]) + + res = piecewise_fold(res, evaluate=None) + if res.is_Piecewise: + newargs = [] + for e, c in res.args: + e = _my_unpolarify(_clean(e)) + newargs += [(e, c)] + res = Piecewise(*newargs, evaluate=False) + else: + res = _my_unpolarify(_clean(res)) + return Piecewise((res, _my_unpolarify(cond)), (Integral(f, x), True)) + + +@timeit +def meijerint_definite(f, x, a, b): + """ + Integrate ``f`` over the interval [``a``, ``b``], by rewriting it as a product + of two G functions, or as a single G function. + + Return res, cond, where cond are convergence conditions. + + Examples + ======== + + >>> from sympy.integrals.meijerint import meijerint_definite + >>> from sympy import exp, oo + >>> from sympy.abc import x + >>> meijerint_definite(exp(-x**2), x, -oo, oo) + (sqrt(pi), True) + + This function is implemented as a succession of functions + meijerint_definite, _meijerint_definite_2, _meijerint_definite_3, + _meijerint_definite_4. Each function in the list calls the next one + (presumably) several times. This means that calling meijerint_definite + can be very costly. + """ + # This consists of three steps: + # 1) Change the integration limits to 0, oo + # 2) Rewrite in terms of G functions + # 3) Evaluate the integral + # + # There are usually several ways of doing this, and we want to try all. + # This function does (1), calls _meijerint_definite_2 for step (2). + _debugf('Integrating %s wrt %s from %s to %s.', (f, x, a, b)) + f = sympify(f) + if f.has(DiracDelta): + _debug('Integrand has DiracDelta terms - giving up.') + return None + + if f.has(SingularityFunction): + _debug('Integrand has Singularity Function terms - giving up.') + return None + + f_, x_, a_, b_ = f, x, a, b + + # Let's use a dummy in case any of the boundaries has x. + d = Dummy('x') + f = f.subs(x, d) + x = d + + if a == b: + return (S.Zero, True) + + results = [] + if a is S.NegativeInfinity and b is not S.Infinity: + return meijerint_definite(f.subs(x, -x), x, -b, -a) + + elif a is S.NegativeInfinity: + # Integrating -oo to oo. We need to find a place to split the integral. + _debug(' Integrating -oo to +oo.') + innermost = _find_splitting_points(f, x) + _debug(' Sensible splitting points:', innermost) + for c in sorted(innermost, key=default_sort_key, reverse=True) + [S.Zero]: + _debug(' Trying to split at', c) + if not c.is_extended_real: + _debug(' Non-real splitting point.') + continue + res1 = _meijerint_definite_2(f.subs(x, x + c), x) + if res1 is None: + _debug(' But could not compute first integral.') + continue + res2 = _meijerint_definite_2(f.subs(x, c - x), x) + if res2 is None: + _debug(' But could not compute second integral.') + continue + res1, cond1 = res1 + res2, cond2 = res2 + cond = _condsimp(And(cond1, cond2)) + if cond == False: + _debug(' But combined condition is always false.') + continue + res = res1 + res2 + return res, cond + + elif a is S.Infinity: + res = meijerint_definite(f, x, b, S.Infinity) + return -res[0], res[1] + + elif (a, b) == (S.Zero, S.Infinity): + # This is a common case - try it directly first. + res = _meijerint_definite_2(f, x) + if res: + if _has(res[0], meijerg): + results.append(res) + else: + return res + + else: + if b is S.Infinity: + for split in _find_splitting_points(f, x): + if (a - split >= 0) == True: + _debugf('Trying x -> x + %s', split) + res = _meijerint_definite_2(f.subs(x, x + split) + *Heaviside(x + split - a), x) + if res: + if _has(res[0], meijerg): + results.append(res) + else: + return res + + f = f.subs(x, x + a) + b = b - a + a = 0 + if b is not S.Infinity: + phi = exp(S.ImaginaryUnit*arg(b)) + b = Abs(b) + f = f.subs(x, phi*x) + f *= Heaviside(b - x)*phi + b = S.Infinity + + _debug('Changed limits to', a, b) + _debug('Changed function to', f) + res = _meijerint_definite_2(f, x) + if res: + if _has(res[0], meijerg): + results.append(res) + else: + return res + if f_.has(HyperbolicFunction): + _debug('Try rewriting hyperbolics in terms of exp.') + rv = meijerint_definite( + _rewrite_hyperbolics_as_exp(f_), x_, a_, b_) + if rv: + if not isinstance(rv, list): + from sympy.simplify.radsimp import collect + rv = (collect(factor_terms(rv[0]), rv[0].atoms(exp)),) + rv[1:] + return rv + results.extend(rv) + if results: + return next(ordered(results)) + + +def _guess_expansion(f, x): + """ Try to guess sensible rewritings for integrand f(x). """ + res = [(f, 'original integrand')] + + orig = res[-1][0] + saw = {orig} + expanded = expand_mul(orig) + if expanded not in saw: + res += [(expanded, 'expand_mul')] + saw.add(expanded) + + expanded = expand(orig) + if expanded not in saw: + res += [(expanded, 'expand')] + saw.add(expanded) + + if orig.has(TrigonometricFunction, HyperbolicFunction): + expanded = expand_mul(expand_trig(orig)) + if expanded not in saw: + res += [(expanded, 'expand_trig, expand_mul')] + saw.add(expanded) + + if orig.has(cos, sin): + from sympy.simplify.fu import sincos_to_sum + reduced = sincos_to_sum(orig) + if reduced not in saw: + res += [(reduced, 'trig power reduction')] + saw.add(reduced) + + return res + + +def _meijerint_definite_2(f, x): + """ + Try to integrate f dx from zero to infinity. + + The body of this function computes various 'simplifications' + f1, f2, ... of f (e.g. by calling expand_mul(), trigexpand() + - see _guess_expansion) and calls _meijerint_definite_3 with each of + these in succession. + If _meijerint_definite_3 succeeds with any of the simplified functions, + returns this result. + """ + # This function does preparation for (2), calls + # _meijerint_definite_3 for (2) and (3) combined. + + # use a positive dummy - we integrate from 0 to oo + # XXX if a nonnegative symbol is used there will be test failures + dummy = _dummy('x', 'meijerint-definite2', f, positive=True) + f = f.subs(x, dummy) + x = dummy + + if f == 0: + return S.Zero, True + + for g, explanation in _guess_expansion(f, x): + _debug('Trying', explanation) + res = _meijerint_definite_3(g, x) + if res: + return res + + +def _meijerint_definite_3(f, x): + """ + Try to integrate f dx from zero to infinity. + + This function calls _meijerint_definite_4 to try to compute the + integral. If this fails, it tries using linearity. + """ + res = _meijerint_definite_4(f, x) + if res and res[1] != False: + return res + if f.is_Add: + _debug('Expanding and evaluating all terms.') + ress = [_meijerint_definite_4(g, x) for g in f.args] + if all(r is not None for r in ress): + conds = [] + res = S.Zero + for r, c in ress: + res += r + conds += [c] + c = And(*conds) + if c != False: + return res, c + + +def _my_unpolarify(f): + return _eval_cond(unpolarify(f)) + + +@timeit +def _meijerint_definite_4(f, x, only_double=False): + """ + Try to integrate f dx from zero to infinity. + + Explanation + =========== + + This function tries to apply the integration theorems found in literature, + i.e. it tries to rewrite f as either one or a product of two G-functions. + + The parameter ``only_double`` is used internally in the recursive algorithm + to disable trying to rewrite f as a single G-function. + """ + from sympy.simplify import hyperexpand + # This function does (2) and (3) + _debug('Integrating', f) + # Try single G function. + if not only_double: + gs = _rewrite1(f, x, recursive=False) + if gs is not None: + fac, po, g, cond = gs + _debug('Could rewrite as single G function:', fac, po, g) + res = S.Zero + for C, s, f in g: + if C == 0: + continue + C, f = _rewrite_saxena_1(fac*C, po*x**s, f, x) + res += C*_int0oo_1(f, x) + cond = And(cond, _check_antecedents_1(f, x)) + if cond == False: + break + cond = _my_unpolarify(cond) + if cond == False: + _debug('But cond is always False.') + else: + _debug('Result before branch substitutions is:', res) + return _my_unpolarify(hyperexpand(res)), cond + + # Try two G functions. + gs = _rewrite2(f, x) + if gs is not None: + for full_pb in [False, True]: + fac, po, g1, g2, cond = gs + _debug('Could rewrite as two G functions:', fac, po, g1, g2) + res = S.Zero + for C1, s1, f1 in g1: + for C2, s2, f2 in g2: + r = _rewrite_saxena(fac*C1*C2, po*x**(s1 + s2), + f1, f2, x, full_pb) + if r is None: + _debug('Non-rational exponents.') + return + C, f1_, f2_ = r + _debug('Saxena subst for yielded:', C, f1_, f2_) + cond = And(cond, _check_antecedents(f1_, f2_, x)) + if cond == False: + break + res += C*_int0oo(f1_, f2_, x) + else: + continue + break + cond = _my_unpolarify(cond) + if cond == False: + _debugf('But cond is always False (full_pb=%s).', full_pb) + else: + _debugf('Result before branch substitutions is: %s', (res, )) + if only_double: + return res, cond + return _my_unpolarify(hyperexpand(res)), cond + + +def meijerint_inversion(f, x, t): + r""" + Compute the inverse laplace transform + $\int_{c+i\infty}^{c-i\infty} f(x) e^{tx}\, dx$, + for real c larger than the real part of all singularities of ``f``. + + Note that ``t`` is always assumed real and positive. + + Return None if the integral does not exist or could not be evaluated. + + Examples + ======== + + >>> from sympy.abc import x, t + >>> from sympy.integrals.meijerint import meijerint_inversion + >>> meijerint_inversion(1/x, x, t) + Heaviside(t) + """ + f_ = f + t_ = t + t = Dummy('t', polar=True) # We don't want sqrt(t**2) = abs(t) etc + f = f.subs(t_, t) + _debug('Laplace-inverting', f) + if not _is_analytic(f, x): + _debug('But expression is not analytic.') + return None + # Exponentials correspond to shifts; we filter them out and then + # shift the result later. If we are given an Add this will not + # work, but the calling code will take care of that. + shift = S.Zero + + if f.is_Mul: + args = list(f.args) + elif isinstance(f, exp): + args = [f] + else: + args = None + + if args: + newargs = [] + exponentials = [] + while args: + arg = args.pop() + if isinstance(arg, exp): + arg2 = expand(arg) + if arg2.is_Mul: + args += arg2.args + continue + try: + a, b = _get_coeff_exp(arg.args[0], x) + except _CoeffExpValueError: + b = 0 + if b == 1: + exponentials.append(a) + else: + newargs.append(arg) + elif arg.is_Pow: + arg2 = expand(arg) + if arg2.is_Mul: + args += arg2.args + continue + if x not in arg.base.free_symbols: + try: + a, b = _get_coeff_exp(arg.exp, x) + except _CoeffExpValueError: + b = 0 + if b == 1: + exponentials.append(a*log(arg.base)) + newargs.append(arg) + else: + newargs.append(arg) + shift = Add(*exponentials) + f = Mul(*newargs) + + if x not in f.free_symbols: + _debug('Expression consists of constant and exp shift:', f, shift) + cond = Eq(im(shift), 0) + if cond == False: + _debug('but shift is nonreal, cannot be a Laplace transform') + return None + res = f*DiracDelta(t + shift) + _debug('Result is a delta function, possibly conditional:', res, cond) + # cond is True or Eq + return Piecewise((res.subs(t, t_), cond)) + + gs = _rewrite1(f, x) + if gs is not None: + fac, po, g, cond = gs + _debug('Could rewrite as single G function:', fac, po, g) + res = S.Zero + for C, s, f in g: + C, f = _rewrite_inversion(fac*C, po*x**s, f, x) + res += C*_int_inversion(f, x, t) + cond = And(cond, _check_antecedents_inversion(f, x)) + if cond == False: + break + cond = _my_unpolarify(cond) + if cond == False: + _debug('But cond is always False.') + else: + _debug('Result before branch substitution:', res) + from sympy.simplify import hyperexpand + res = _my_unpolarify(hyperexpand(res)) + if not res.has(Heaviside): + res *= Heaviside(t) + res = res.subs(t, t + shift) + if not isinstance(cond, bool): + cond = cond.subs(t, t + shift) + from .transforms import InverseLaplaceTransform + return Piecewise((res.subs(t, t_), cond), + (InverseLaplaceTransform(f_.subs(t, t_), x, t_, None), True)) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/meijerint_doc.py b/venv/lib/python3.11/site-packages/sympy/integrals/meijerint_doc.py new file mode 100644 index 0000000000000000000000000000000000000000..1df385db6b6fe6d46d4a14217aa53d1fdd9670b5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/meijerint_doc.py @@ -0,0 +1,38 @@ +""" This module cooks up a docstring when imported. Its only purpose is to + be displayed in the sphinx documentation. """ + +from __future__ import annotations +from typing import Any + +from sympy.integrals.meijerint import _create_lookup_table +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.expr import Expr +from sympy.core.relational import Eq +from sympy.core.symbol import Symbol +from sympy.printing.latex import latex + +t: dict[tuple[type[Basic], ...], list[Any]] = {} +_create_lookup_table(t) + + +doc = "" +for about, category in t.items(): + if about == (): + doc += 'Elementary functions:\n\n' + else: + doc += 'Functions involving ' + ', '.join('`%s`' % latex( + list(category[0][0].atoms(func))[0]) for func in about) + ':\n\n' + for formula, gs, cond, hint in category: + if not isinstance(gs, list): + g: Expr = Symbol('\\text{generated}') + else: + g = Add(*[fac*f for (fac, f) in gs]) + obj = Eq(formula, g) + if cond is True: + cond = "" + else: + cond = ',\\text{ if } %s' % latex(cond) + doc += ".. math::\n %s%s\n\n" % (latex(obj), cond) + +__doc__ = doc diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/prde.py b/venv/lib/python3.11/site-packages/sympy/integrals/prde.py new file mode 100644 index 0000000000000000000000000000000000000000..28e91ea0ff3a82cbeca24c6ed4267503ceb758b5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/prde.py @@ -0,0 +1,1333 @@ +""" +Algorithms for solving Parametric Risch Differential Equations. + +The methods used for solving Parametric Risch Differential Equations parallel +those for solving Risch Differential Equations. See the outline in the +docstring of rde.py for more information. + +The Parametric Risch Differential Equation problem is, given f, g1, ..., gm in +K(t), to determine if there exist y in K(t) and c1, ..., cm in Const(K) such +that Dy + f*y == Sum(ci*gi, (i, 1, m)), and to find such y and ci if they exist. + +For the algorithms here G is a list of tuples of factions of the terms on the +right hand side of the equation (i.e., gi in k(t)), and Q is a list of terms on +the right hand side of the equation (i.e., qi in k[t]). See the docstring of +each function for more information. +""" +import itertools +from functools import reduce + +from sympy.core.intfunc import ilcm +from sympy.core import Dummy, Add, Mul, Pow, S +from sympy.integrals.rde import (order_at, order_at_oo, weak_normalizer, + bound_degree) +from sympy.integrals.risch import (gcdex_diophantine, frac_in, derivation, + residue_reduce, splitfactor, residue_reduce_derivation, DecrementLevel, + recognize_log_derivative) +from sympy.polys import Poly, lcm, cancel, sqf_list +from sympy.polys.polymatrix import PolyMatrix as Matrix +from sympy.solvers import solve + +zeros = Matrix.zeros +eye = Matrix.eye + + +def prde_normal_denom(fa, fd, G, DE): + """ + Parametric Risch Differential Equation - Normal part of the denominator. + + Explanation + =========== + + Given a derivation D on k[t] and f, g1, ..., gm in k(t) with f weakly + normalized with respect to t, return the tuple (a, b, G, h) such that + a, h in k[t], b in k, G = [g1, ..., gm] in k(t)^m, and for any solution + c1, ..., cm in Const(k) and y in k(t) of Dy + f*y == Sum(ci*gi, (i, 1, m)), + q == y*h in k satisfies a*Dq + b*q == Sum(ci*Gi, (i, 1, m)). + """ + dn, ds = splitfactor(fd, DE) + Gas, Gds = list(zip(*G)) + gd = reduce(lambda i, j: i.lcm(j), Gds, Poly(1, DE.t)) + en, es = splitfactor(gd, DE) + + p = dn.gcd(en) + h = en.gcd(en.diff(DE.t)).quo(p.gcd(p.diff(DE.t))) + + a = dn*h + c = a*h + + ba = a*fa - dn*derivation(h, DE)*fd + ba, bd = ba.cancel(fd, include=True) + + G = [(c*A).cancel(D, include=True) for A, D in G] + + return (a, (ba, bd), G, h) + +def real_imag(ba, bd, gen): + """ + Helper function, to get the real and imaginary part of a rational function + evaluated at sqrt(-1) without actually evaluating it at sqrt(-1). + + Explanation + =========== + + Separates the even and odd power terms by checking the degree of terms wrt + mod 4. Returns a tuple (ba[0], ba[1], bd) where ba[0] is real part + of the numerator ba[1] is the imaginary part and bd is the denominator + of the rational function. + """ + bd = bd.as_poly(gen).as_dict() + ba = ba.as_poly(gen).as_dict() + denom_real = [value if key[0] % 4 == 0 else -value if key[0] % 4 == 2 else 0 for key, value in bd.items()] + denom_imag = [value if key[0] % 4 == 1 else -value if key[0] % 4 == 3 else 0 for key, value in bd.items()] + bd_real = sum(r for r in denom_real) + bd_imag = sum(r for r in denom_imag) + num_real = [value if key[0] % 4 == 0 else -value if key[0] % 4 == 2 else 0 for key, value in ba.items()] + num_imag = [value if key[0] % 4 == 1 else -value if key[0] % 4 == 3 else 0 for key, value in ba.items()] + ba_real = sum(r for r in num_real) + ba_imag = sum(r for r in num_imag) + ba = ((ba_real*bd_real + ba_imag*bd_imag).as_poly(gen), (ba_imag*bd_real - ba_real*bd_imag).as_poly(gen)) + bd = (bd_real*bd_real + bd_imag*bd_imag).as_poly(gen) + return (ba[0], ba[1], bd) + + +def prde_special_denom(a, ba, bd, G, DE, case='auto'): + """ + Parametric Risch Differential Equation - Special part of the denominator. + + Explanation + =========== + + Case is one of {'exp', 'tan', 'primitive'} for the hyperexponential, + hypertangent, and primitive cases, respectively. For the hyperexponential + (resp. hypertangent) case, given a derivation D on k[t] and a in k[t], + b in k, and g1, ..., gm in k(t) with Dt/t in k (resp. Dt/(t**2 + 1) in + k, sqrt(-1) not in k), a != 0, and gcd(a, t) == 1 (resp. + gcd(a, t**2 + 1) == 1), return the tuple (A, B, GG, h) such that A, B, h in + k[t], GG = [gg1, ..., ggm] in k(t)^m, and for any solution c1, ..., cm in + Const(k) and q in k of a*Dq + b*q == Sum(ci*gi, (i, 1, m)), r == q*h in + k[t] satisfies A*Dr + B*r == Sum(ci*ggi, (i, 1, m)). + + For case == 'primitive', k == k[t], so it returns (a, b, G, 1) in this + case. + """ + # TODO: Merge this with the very similar special_denom() in rde.py + if case == 'auto': + case = DE.case + + if case == 'exp': + p = Poly(DE.t, DE.t) + elif case == 'tan': + p = Poly(DE.t**2 + 1, DE.t) + elif case in ('primitive', 'base'): + B = ba.quo(bd) + return (a, B, G, Poly(1, DE.t)) + else: + raise ValueError("case must be one of {'exp', 'tan', 'primitive', " + "'base'}, not %s." % case) + + nb = order_at(ba, p, DE.t) - order_at(bd, p, DE.t) + nc = min(order_at(Ga, p, DE.t) - order_at(Gd, p, DE.t) for Ga, Gd in G) + n = min(0, nc - min(0, nb)) + if not nb: + # Possible cancellation. + if case == 'exp': + dcoeff = DE.d.quo(Poly(DE.t, DE.t)) + with DecrementLevel(DE): # We are guaranteed to not have problems, + # because case != 'base'. + alphaa, alphad = frac_in(-ba.eval(0)/bd.eval(0)/a.eval(0), DE.t) + etaa, etad = frac_in(dcoeff, DE.t) + A = parametric_log_deriv(alphaa, alphad, etaa, etad, DE) + if A is not None: + Q, m, z = A + if Q == 1: + n = min(n, m) + + elif case == 'tan': + dcoeff = DE.d.quo(Poly(DE.t**2 + 1, DE.t)) + with DecrementLevel(DE): # We are guaranteed to not have problems, + # because case != 'base'. + betaa, alphaa, alphad = real_imag(ba, bd*a, DE.t) + betad = alphad + etaa, etad = frac_in(dcoeff, DE.t) + if recognize_log_derivative(Poly(2, DE.t)*betaa, betad, DE): + A = parametric_log_deriv(alphaa, alphad, etaa, etad, DE) + B = parametric_log_deriv(betaa, betad, etaa, etad, DE) + if A is not None and B is not None: + Q, s, z = A + # TODO: Add test + if Q == 1: + n = min(n, s/2) + + N = max(0, -nb) + pN = p**N + pn = p**-n # This is 1/h + + A = a*pN + B = ba*pN.quo(bd) + Poly(n, DE.t)*a*derivation(p, DE).quo(p)*pN + G = [(Ga*pN*pn).cancel(Gd, include=True) for Ga, Gd in G] + h = pn + + # (a*p**N, (b + n*a*Dp/p)*p**N, g1*p**(N - n), ..., gm*p**(N - n), p**-n) + return (A, B, G, h) + + +def prde_linear_constraints(a, b, G, DE): + """ + Parametric Risch Differential Equation - Generate linear constraints on the constants. + + Explanation + =========== + + Given a derivation D on k[t], a, b, in k[t] with gcd(a, b) == 1, and + G = [g1, ..., gm] in k(t)^m, return Q = [q1, ..., qm] in k[t]^m and a + matrix M with entries in k(t) such that for any solution c1, ..., cm in + Const(k) and p in k[t] of a*Dp + b*p == Sum(ci*gi, (i, 1, m)), + (c1, ..., cm) is a solution of Mx == 0, and p and the ci satisfy + a*Dp + b*p == Sum(ci*qi, (i, 1, m)). + + Because M has entries in k(t), and because Matrix does not play well with + Poly, M will be a Matrix of Basic expressions. + """ + m = len(G) + + Gns, Gds = list(zip(*G)) + d = reduce(lambda i, j: i.lcm(j), Gds) + d = Poly(d, field=True) + Q = [(ga*(d).quo(gd)).div(d) for ga, gd in G] + + if not all(ri.is_zero for _, ri in Q): + N = max(ri.degree(DE.t) for _, ri in Q) + M = Matrix(N + 1, m, lambda i, j: Q[j][1].nth(i), DE.t) + else: + M = Matrix(0, m, [], DE.t) # No constraints, return the empty matrix. + + qs, _ = list(zip(*Q)) + return (qs, M) + +def poly_linear_constraints(p, d): + """ + Given p = [p1, ..., pm] in k[t]^m and d in k[t], return + q = [q1, ..., qm] in k[t]^m and a matrix M with entries in k such + that Sum(ci*pi, (i, 1, m)), for c1, ..., cm in k, is divisible + by d if and only if (c1, ..., cm) is a solution of Mx = 0, in + which case the quotient is Sum(ci*qi, (i, 1, m)). + """ + m = len(p) + q, r = zip(*[pi.div(d) for pi in p]) + + if not all(ri.is_zero for ri in r): + n = max(ri.degree() for ri in r) + M = Matrix(n + 1, m, lambda i, j: r[j].nth(i), d.gens) + else: + M = Matrix(0, m, [], d.gens) # No constraints. + + return q, M + +def constant_system(A, u, DE): + """ + Generate a system for the constant solutions. + + Explanation + =========== + + Given a differential field (K, D) with constant field C = Const(K), a Matrix + A, and a vector (Matrix) u with coefficients in K, returns the tuple + (B, v, s), where B is a Matrix with coefficients in C and v is a vector + (Matrix) such that either v has coefficients in C, in which case s is True + and the solutions in C of Ax == u are exactly all the solutions of Bx == v, + or v has a non-constant coefficient, in which case s is False Ax == u has no + constant solution. + + This algorithm is used both in solving parametric problems and in + determining if an element a of K is a derivative of an element of K or the + logarithmic derivative of a K-radical using the structure theorem approach. + + Because Poly does not play well with Matrix yet, this algorithm assumes that + all matrix entries are Basic expressions. + """ + if not A: + return A, u + Au = A.row_join(u) + Au, _ = Au.rref() + # Warning: This will NOT return correct results if cancel() cannot reduce + # an identically zero expression to 0. The danger is that we might + # incorrectly prove that an integral is nonelementary (such as + # risch_integrate(exp((sin(x)**2 + cos(x)**2 - 1)*x**2), x). + # But this is a limitation in computer algebra in general, and implicit + # in the correctness of the Risch Algorithm is the computability of the + # constant field (actually, this same correctness problem exists in any + # algorithm that uses rref()). + # + # We therefore limit ourselves to constant fields that are computable + # via the cancel() function, in order to prevent a speed bottleneck from + # calling some more complex simplification function (rational function + # coefficients will fall into this class). Furthermore, (I believe) this + # problem will only crop up if the integral explicitly contains an + # expression in the constant field that is identically zero, but cannot + # be reduced to such by cancel(). Therefore, a careful user can avoid this + # problem entirely by being careful with the sorts of expressions that + # appear in his integrand in the variables other than the integration + # variable (the structure theorems should be able to completely decide these + # problems in the integration variable). + + A, u = Au[:, :-1], Au[:, -1] + + D = lambda x: derivation(x, DE, basic=True) + + for j, i in itertools.product(range(A.cols), range(A.rows)): + if A[i, j].expr.has(*DE.T): + # This assumes that const(F(t0, ..., tn) == const(K) == F + Ri = A[i, :] + # Rm+1; m = A.rows + DAij = D(A[i, j]) + Rm1 = Ri.applyfunc(lambda x: D(x) / DAij) + um1 = D(u[i]) / DAij + + Aj = A[:, j] + A = A - Aj * Rm1 + u = u - Aj * um1 + + A = A.col_join(Rm1) + u = u.col_join(Matrix([um1], u.gens)) + + return (A, u) + + +def prde_spde(a, b, Q, n, DE): + """ + Special Polynomial Differential Equation algorithm: Parametric Version. + + Explanation + =========== + + Given a derivation D on k[t], an integer n, and a, b, q1, ..., qm in k[t] + with deg(a) > 0 and gcd(a, b) == 1, return (A, B, Q, R, n1), with + Qq = [q1, ..., qm] and R = [r1, ..., rm], such that for any solution + c1, ..., cm in Const(k) and q in k[t] of degree at most n of + a*Dq + b*q == Sum(ci*gi, (i, 1, m)), p = (q - Sum(ci*ri, (i, 1, m)))/a has + degree at most n1 and satisfies A*Dp + B*p == Sum(ci*qi, (i, 1, m)) + """ + R, Z = list(zip(*[gcdex_diophantine(b, a, qi) for qi in Q])) + + A = a + B = b + derivation(a, DE) + Qq = [zi - derivation(ri, DE) for ri, zi in zip(R, Z)] + R = list(R) + n1 = n - a.degree(DE.t) + + return (A, B, Qq, R, n1) + + +def prde_no_cancel_b_large(b, Q, n, DE): + """ + Parametric Poly Risch Differential Equation - No cancellation: deg(b) large enough. + + Explanation + =========== + + Given a derivation D on k[t], n in ZZ, and b, q1, ..., qm in k[t] with + b != 0 and either D == d/dt or deg(b) > max(0, deg(D) - 1), returns + h1, ..., hr in k[t] and a matrix A with coefficients in Const(k) such that + if c1, ..., cm in Const(k) and q in k[t] satisfy deg(q) <= n and + Dq + b*q == Sum(ci*qi, (i, 1, m)), then q = Sum(dj*hj, (j, 1, r)), where + d1, ..., dr in Const(k) and A*Matrix([[c1, ..., cm, d1, ..., dr]]).T == 0. + """ + db = b.degree(DE.t) + m = len(Q) + H = [Poly(0, DE.t)]*m + + for N, i in itertools.product(range(n, -1, -1), range(m)): # [n, ..., 0] + si = Q[i].nth(N + db)/b.LC() + sitn = Poly(si*DE.t**N, DE.t) + H[i] = H[i] + sitn + Q[i] = Q[i] - derivation(sitn, DE) - b*sitn + + if all(qi.is_zero for qi in Q): + dc = -1 + else: + dc = max(qi.degree(DE.t) for qi in Q) + M = Matrix(dc + 1, m, lambda i, j: Q[j].nth(i), DE.t) + A, u = constant_system(M, zeros(dc + 1, 1, DE.t), DE) + c = eye(m, DE.t) + A = A.row_join(zeros(A.rows, m, DE.t)).col_join(c.row_join(-c)) + + return (H, A) + + +def prde_no_cancel_b_small(b, Q, n, DE): + """ + Parametric Poly Risch Differential Equation - No cancellation: deg(b) small enough. + + Explanation + =========== + + Given a derivation D on k[t], n in ZZ, and b, q1, ..., qm in k[t] with + deg(b) < deg(D) - 1 and either D == d/dt or deg(D) >= 2, returns + h1, ..., hr in k[t] and a matrix A with coefficients in Const(k) such that + if c1, ..., cm in Const(k) and q in k[t] satisfy deg(q) <= n and + Dq + b*q == Sum(ci*qi, (i, 1, m)) then q = Sum(dj*hj, (j, 1, r)) where + d1, ..., dr in Const(k) and A*Matrix([[c1, ..., cm, d1, ..., dr]]).T == 0. + """ + m = len(Q) + H = [Poly(0, DE.t)]*m + + for N, i in itertools.product(range(n, 0, -1), range(m)): # [n, ..., 1] + si = Q[i].nth(N + DE.d.degree(DE.t) - 1)/(N*DE.d.LC()) + sitn = Poly(si*DE.t**N, DE.t) + H[i] = H[i] + sitn + Q[i] = Q[i] - derivation(sitn, DE) - b*sitn + + if b.degree(DE.t) > 0: + for i in range(m): + si = Poly(Q[i].nth(b.degree(DE.t))/b.LC(), DE.t) + H[i] = H[i] + si + Q[i] = Q[i] - derivation(si, DE) - b*si + if all(qi.is_zero for qi in Q): + dc = -1 + else: + dc = max(qi.degree(DE.t) for qi in Q) + M = Matrix(dc + 1, m, lambda i, j: Q[j].nth(i), DE.t) + A, u = constant_system(M, zeros(dc + 1, 1, DE.t), DE) + c = eye(m, DE.t) + A = A.row_join(zeros(A.rows, m, DE.t)).col_join(c.row_join(-c)) + return (H, A) + + # else: b is in k, deg(qi) < deg(Dt) + + t = DE.t + if DE.case != 'base': + with DecrementLevel(DE): + t0 = DE.t # k = k0(t0) + ba, bd = frac_in(b, t0, field=True) + Q0 = [frac_in(qi.TC(), t0, field=True) for qi in Q] + f, B = param_rischDE(ba, bd, Q0, DE) + + # f = [f1, ..., fr] in k^r and B is a matrix with + # m + r columns and entries in Const(k) = Const(k0) + # such that Dy0 + b*y0 = Sum(ci*qi, (i, 1, m)) has + # a solution y0 in k with c1, ..., cm in Const(k) + # if and only y0 = Sum(dj*fj, (j, 1, r)) where + # d1, ..., dr ar in Const(k) and + # B*Matrix([c1, ..., cm, d1, ..., dr]) == 0. + + # Transform fractions (fa, fd) in f into constant + # polynomials fa/fd in k[t]. + # (Is there a better way?) + f = [Poly(fa.as_expr()/fd.as_expr(), t, field=True) + for fa, fd in f] + B = Matrix.from_Matrix(B.to_Matrix(), t) + else: + # Base case. Dy == 0 for all y in k and b == 0. + # Dy + b*y = Sum(ci*qi) is solvable if and only if + # Sum(ci*qi) == 0 in which case the solutions are + # y = d1*f1 for f1 = 1 and any d1 in Const(k) = k. + + f = [Poly(1, t, field=True)] # r = 1 + B = Matrix([[qi.TC() for qi in Q] + [S.Zero]], DE.t) + # The condition for solvability is + # B*Matrix([c1, ..., cm, d1]) == 0 + # There are no constraints on d1. + + # Coefficients of t^j (j > 0) in Sum(ci*qi) must be zero. + d = max(qi.degree(DE.t) for qi in Q) + if d > 0: + M = Matrix(d, m, lambda i, j: Q[j].nth(i + 1), DE.t) + A, _ = constant_system(M, zeros(d, 1, DE.t), DE) + else: + # No constraints on the hj. + A = Matrix(0, m, [], DE.t) + + # Solutions of the original equation are + # y = Sum(dj*fj, (j, 1, r) + Sum(ei*hi, (i, 1, m)), + # where ei == ci (i = 1, ..., m), when + # A*Matrix([c1, ..., cm]) == 0 and + # B*Matrix([c1, ..., cm, d1, ..., dr]) == 0 + + # Build combined constraint matrix with m + r + m columns. + + r = len(f) + I = eye(m, DE.t) + A = A.row_join(zeros(A.rows, r + m, DE.t)) + B = B.row_join(zeros(B.rows, m, DE.t)) + C = I.row_join(zeros(m, r, DE.t)).row_join(-I) + + return f + H, A.col_join(B).col_join(C) + + +def prde_cancel_liouvillian(b, Q, n, DE): + """ + Pg, 237. + """ + H = [] + + # Why use DecrementLevel? Below line answers that: + # Assuming that we can solve such problems over 'k' (not k[t]) + if DE.case == 'primitive': + with DecrementLevel(DE): + ba, bd = frac_in(b, DE.t, field=True) + + for i in range(n, -1, -1): + if DE.case == 'exp': # this re-checking can be avoided + with DecrementLevel(DE): + ba, bd = frac_in(b + (i*(derivation(DE.t, DE)/DE.t)).as_poly(b.gens), + DE.t, field=True) + with DecrementLevel(DE): + Qy = [frac_in(q.nth(i), DE.t, field=True) for q in Q] + fi, Ai = param_rischDE(ba, bd, Qy, DE) + fi = [Poly(fa.as_expr()/fd.as_expr(), DE.t, field=True) + for fa, fd in fi] + Ai = Ai.set_gens(DE.t) + + ri = len(fi) + + if i == n: + M = Ai + else: + M = Ai.col_join(M.row_join(zeros(M.rows, ri, DE.t))) + + Fi, hi = [None]*ri, [None]*ri + + # from eq. on top of p.238 (unnumbered) + for j in range(ri): + hji = fi[j] * (DE.t**i).as_poly(fi[j].gens) + hi[j] = hji + # building up Sum(djn*(D(fjn*t^n) - b*fjnt^n)) + Fi[j] = -(derivation(hji, DE) - b*hji) + + H += hi + # in the next loop instead of Q it has + # to be Q + Fi taking its place + Q = Q + Fi + + return (H, M) + + +def param_poly_rischDE(a, b, q, n, DE): + """Polynomial solutions of a parametric Risch differential equation. + + Explanation + =========== + + Given a derivation D in k[t], a, b in k[t] relatively prime, and q + = [q1, ..., qm] in k[t]^m, return h = [h1, ..., hr] in k[t]^r and + a matrix A with m + r columns and entries in Const(k) such that + a*Dp + b*p = Sum(ci*qi, (i, 1, m)) has a solution p of degree <= n + in k[t] with c1, ..., cm in Const(k) if and only if p = Sum(dj*hj, + (j, 1, r)) where d1, ..., dr are in Const(k) and (c1, ..., cm, + d1, ..., dr) is a solution of Ax == 0. + """ + m = len(q) + if n < 0: + # Only the trivial zero solution is possible. + # Find relations between the qi. + if all(qi.is_zero for qi in q): + return [], zeros(1, m, DE.t) # No constraints. + + N = max(qi.degree(DE.t) for qi in q) + M = Matrix(N + 1, m, lambda i, j: q[j].nth(i), DE.t) + A, _ = constant_system(M, zeros(M.rows, 1, DE.t), DE) + + return [], A + + if a.is_ground: + # Normalization: a = 1. + a = a.LC() + b, q = b.to_field().exquo_ground(a), [qi.to_field().exquo_ground(a) for qi in q] + + if not b.is_zero and (DE.case == 'base' or + b.degree() > max(0, DE.d.degree() - 1)): + return prde_no_cancel_b_large(b, q, n, DE) + + elif ((b.is_zero or b.degree() < DE.d.degree() - 1) + and (DE.case == 'base' or DE.d.degree() >= 2)): + return prde_no_cancel_b_small(b, q, n, DE) + + elif (DE.d.degree() >= 2 and + b.degree() == DE.d.degree() - 1 and + n > -b.as_poly().LC()/DE.d.as_poly().LC()): + raise NotImplementedError("prde_no_cancel_b_equal() is " + "not yet implemented.") + + else: + # Liouvillian cases + if DE.case in ('primitive', 'exp'): + return prde_cancel_liouvillian(b, q, n, DE) + else: + raise NotImplementedError("non-linear and hypertangent " + "cases have not yet been implemented") + + # else: deg(a) > 0 + + # Iterate SPDE as long as possible cumulating coefficient + # and terms for the recovery of original solutions. + alpha, beta = a.one, [a.zero]*m + while n >= 0: # and a, b relatively prime + a, b, q, r, n = prde_spde(a, b, q, n, DE) + beta = [betai + alpha*ri for betai, ri in zip(beta, r)] + alpha *= a + # Solutions p of a*Dp + b*p = Sum(ci*qi) correspond to + # solutions alpha*p + Sum(ci*betai) of the initial equation. + d = a.gcd(b) + if not d.is_ground: + break + + # a*Dp + b*p = Sum(ci*qi) may have a polynomial solution + # only if the sum is divisible by d. + + qq, M = poly_linear_constraints(q, d) + # qq = [qq1, ..., qqm] where qqi = qi.quo(d). + # M is a matrix with m columns an entries in k. + # Sum(fi*qi, (i, 1, m)), where f1, ..., fm are elements of k, is + # divisible by d if and only if M*Matrix([f1, ..., fm]) == 0, + # in which case the quotient is Sum(fi*qqi). + + A, _ = constant_system(M, zeros(M.rows, 1, DE.t), DE) + # A is a matrix with m columns and entries in Const(k). + # Sum(ci*qqi) is Sum(ci*qi).quo(d), and the remainder is zero + # for c1, ..., cm in Const(k) if and only if + # A*Matrix([c1, ...,cm]) == 0. + + V = A.nullspace() + # V = [v1, ..., vu] where each vj is a column matrix with + # entries aj1, ..., ajm in Const(k). + # Sum(aji*qi) is divisible by d with exact quotient Sum(aji*qqi). + # Sum(ci*qi) is divisible by d if and only if ci = Sum(dj*aji) + # (i = 1, ..., m) for some d1, ..., du in Const(k). + # In that case, solutions of + # a*Dp + b*p = Sum(ci*qi) = Sum(dj*Sum(aji*qi)) + # are the same as those of + # (a/d)*Dp + (b/d)*p = Sum(dj*rj) + # where rj = Sum(aji*qqi). + + if not V: # No non-trivial solution. + return [], eye(m, DE.t) # Could return A, but this has + # the minimum number of rows. + + Mqq = Matrix([qq]) # A single row. + r = [(Mqq*vj)[0] for vj in V] # [r1, ..., ru] + + # Solutions of (a/d)*Dp + (b/d)*p = Sum(dj*rj) correspond to + # solutions alpha*p + Sum(Sum(dj*aji)*betai) of the initial + # equation. These are equal to alpha*p + Sum(dj*fj) where + # fj = Sum(aji*betai). + Mbeta = Matrix([beta]) + f = [(Mbeta*vj)[0] for vj in V] # [f1, ..., fu] + + # + # Solve the reduced equation recursively. + # + g, B = param_poly_rischDE(a.quo(d), b.quo(d), r, n, DE) + + # g = [g1, ..., gv] in k[t]^v and and B is a matrix with u + v + # columns and entries in Const(k) such that + # (a/d)*Dp + (b/d)*p = Sum(dj*rj) has a solution p of degree <= n + # in k[t] if and only if p = Sum(ek*gk) where e1, ..., ev are in + # Const(k) and B*Matrix([d1, ..., du, e1, ..., ev]) == 0. + # The solutions of the original equation are then + # Sum(dj*fj, (j, 1, u)) + alpha*Sum(ek*gk, (k, 1, v)). + + # Collect solution components. + h = f + [alpha*gk for gk in g] + + # Build combined relation matrix. + A = -eye(m, DE.t) + for vj in V: + A = A.row_join(vj) + A = A.row_join(zeros(m, len(g), DE.t)) + A = A.col_join(zeros(B.rows, m, DE.t).row_join(B)) + + return h, A + + +def param_rischDE(fa, fd, G, DE): + """ + Solve a Parametric Risch Differential Equation: Dy + f*y == Sum(ci*Gi, (i, 1, m)). + + Explanation + =========== + + Given a derivation D in k(t), f in k(t), and G + = [G1, ..., Gm] in k(t)^m, return h = [h1, ..., hr] in k(t)^r and + a matrix A with m + r columns and entries in Const(k) such that + Dy + f*y = Sum(ci*Gi, (i, 1, m)) has a solution y + in k(t) with c1, ..., cm in Const(k) if and only if y = Sum(dj*hj, + (j, 1, r)) where d1, ..., dr are in Const(k) and (c1, ..., cm, + d1, ..., dr) is a solution of Ax == 0. + + Elements of k(t) are tuples (a, d) with a and d in k[t]. + """ + m = len(G) + q, (fa, fd) = weak_normalizer(fa, fd, DE) + # Solutions of the weakly normalized equation Dz + f*z = q*Sum(ci*Gi) + # correspond to solutions y = z/q of the original equation. + gamma = q + G = [(q*ga).cancel(gd, include=True) for ga, gd in G] + + a, (ba, bd), G, hn = prde_normal_denom(fa, fd, G, DE) + # Solutions q in k of a*Dq + b*q = Sum(ci*Gi) correspond + # to solutions z = q/hn of the weakly normalized equation. + gamma *= hn + + A, B, G, hs = prde_special_denom(a, ba, bd, G, DE) + # Solutions p in k[t] of A*Dp + B*p = Sum(ci*Gi) correspond + # to solutions q = p/hs of the previous equation. + gamma *= hs + + g = A.gcd(B) + a, b, g = A.quo(g), B.quo(g), [gia.cancel(gid*g, include=True) for + gia, gid in G] + + # a*Dp + b*p = Sum(ci*gi) may have a polynomial solution + # only if the sum is in k[t]. + + q, M = prde_linear_constraints(a, b, g, DE) + + # q = [q1, ..., qm] where qi in k[t] is the polynomial component + # of the partial fraction expansion of gi. + # M is a matrix with m columns and entries in k. + # Sum(fi*gi, (i, 1, m)), where f1, ..., fm are elements of k, + # is a polynomial if and only if M*Matrix([f1, ..., fm]) == 0, + # in which case the sum is equal to Sum(fi*qi). + + M, _ = constant_system(M, zeros(M.rows, 1, DE.t), DE) + # M is a matrix with m columns and entries in Const(k). + # Sum(ci*gi) is in k[t] for c1, ..., cm in Const(k) + # if and only if M*Matrix([c1, ..., cm]) == 0, + # in which case the sum is Sum(ci*qi). + + ## Reduce number of constants at this point + + V = M.nullspace() + # V = [v1, ..., vu] where each vj is a column matrix with + # entries aj1, ..., ajm in Const(k). + # Sum(aji*gi) is in k[t] and equal to Sum(aji*qi) (j = 1, ..., u). + # Sum(ci*gi) is in k[t] if and only is ci = Sum(dj*aji) + # (i = 1, ..., m) for some d1, ..., du in Const(k). + # In that case, + # Sum(ci*gi) = Sum(ci*qi) = Sum(dj*Sum(aji*qi)) = Sum(dj*rj) + # where rj = Sum(aji*qi) (j = 1, ..., u) in k[t]. + + if not V: # No non-trivial solution + return [], eye(m, DE.t) + + Mq = Matrix([q]) # A single row. + r = [(Mq*vj)[0] for vj in V] # [r1, ..., ru] + + # Solutions of a*Dp + b*p = Sum(dj*rj) correspond to solutions + # y = p/gamma of the initial equation with ci = Sum(dj*aji). + + try: + # We try n=5. At least for prde_spde, it will always + # terminate no matter what n is. + n = bound_degree(a, b, r, DE, parametric=True) + except NotImplementedError: + # A temporary bound is set. Eventually, it will be removed. + # the currently added test case takes large time + # even with n=5, and much longer with large n's. + n = 5 + + h, B = param_poly_rischDE(a, b, r, n, DE) + + # h = [h1, ..., hv] in k[t]^v and and B is a matrix with u + v + # columns and entries in Const(k) such that + # a*Dp + b*p = Sum(dj*rj) has a solution p of degree <= n + # in k[t] if and only if p = Sum(ek*hk) where e1, ..., ev are in + # Const(k) and B*Matrix([d1, ..., du, e1, ..., ev]) == 0. + # The solutions of the original equation for ci = Sum(dj*aji) + # (i = 1, ..., m) are then y = Sum(ek*hk, (k, 1, v))/gamma. + + ## Build combined relation matrix with m + u + v columns. + + A = -eye(m, DE.t) + for vj in V: + A = A.row_join(vj) + A = A.row_join(zeros(m, len(h), DE.t)) + A = A.col_join(zeros(B.rows, m, DE.t).row_join(B)) + + ## Eliminate d1, ..., du. + + W = A.nullspace() + + # W = [w1, ..., wt] where each wl is a column matrix with + # entries blk (k = 1, ..., m + u + v) in Const(k). + # The vectors (bl1, ..., blm) generate the space of those + # constant families (c1, ..., cm) for which a solution of + # the equation Dy + f*y == Sum(ci*Gi) exists. They generate + # the space and form a basis except possibly when Dy + f*y == 0 + # is solvable in k(t}. The corresponding solutions are + # y = Sum(blk'*hk, (k, 1, v))/gamma, where k' = k + m + u. + + v = len(h) + shape = (len(W), m+v) + elements = [wl[:m] + wl[-v:] for wl in W] # excise dj's. + items = [e for row in elements for e in row] + + # Need to set the shape in case W is empty + M = Matrix(*shape, items, DE.t) + N = M.nullspace() + + # N = [n1, ..., ns] where the ni in Const(k)^(m + v) are column + # vectors generating the space of linear relations between + # c1, ..., cm, e1, ..., ev. + + C = Matrix([ni[:] for ni in N], DE.t) # rows n1, ..., ns. + + return [hk.cancel(gamma, include=True) for hk in h], C + + +def limited_integrate_reduce(fa, fd, G, DE): + """ + Simpler version of step 1 & 2 for the limited integration problem. + + Explanation + =========== + + Given a derivation D on k(t) and f, g1, ..., gn in k(t), return + (a, b, h, N, g, V) such that a, b, h in k[t], N is a non-negative integer, + g in k(t), V == [v1, ..., vm] in k(t)^m, and for any solution v in k(t), + c1, ..., cm in C of f == Dv + Sum(ci*wi, (i, 1, m)), p = v*h is in k, and + p and the ci satisfy a*Dp + b*p == g + Sum(ci*vi, (i, 1, m)). Furthermore, + if S1irr == Sirr, then p is in k[t], and if t is nonlinear or Liouvillian + over k, then deg(p) <= N. + + So that the special part is always computed, this function calls the more + general prde_special_denom() automatically if it cannot determine that + S1irr == Sirr. Furthermore, it will automatically call bound_degree() when + t is linear and non-Liouvillian, which for the transcendental case, implies + that Dt == a*t + b with for some a, b in k*. + """ + dn, ds = splitfactor(fd, DE) + E = [splitfactor(gd, DE) for _, gd in G] + En, Es = list(zip(*E)) + c = reduce(lambda i, j: i.lcm(j), (dn,) + En) # lcm(dn, en1, ..., enm) + hn = c.gcd(c.diff(DE.t)) + a = hn + b = -derivation(hn, DE) + N = 0 + + # These are the cases where we know that S1irr = Sirr, but there could be + # others, and this algorithm will need to be extended to handle them. + if DE.case in ('base', 'primitive', 'exp', 'tan'): + hs = reduce(lambda i, j: i.lcm(j), (ds,) + Es) # lcm(ds, es1, ..., esm) + a = hn*hs + b -= (hn*derivation(hs, DE)).quo(hs) + mu = min(order_at_oo(fa, fd, DE.t), min(order_at_oo(ga, gd, DE.t) for + ga, gd in G)) + # So far, all the above are also nonlinear or Liouvillian, but if this + # changes, then this will need to be updated to call bound_degree() + # as per the docstring of this function (DE.case == 'other_linear'). + N = hn.degree(DE.t) + hs.degree(DE.t) + max(0, 1 - DE.d.degree(DE.t) - mu) + else: + # TODO: implement this + raise NotImplementedError + + V = [(-a*hn*ga).cancel(gd, include=True) for ga, gd in G] + return (a, b, a, N, (a*hn*fa).cancel(fd, include=True), V) + + +def limited_integrate(fa, fd, G, DE): + """ + Solves the limited integration problem: f = Dv + Sum(ci*wi, (i, 1, n)) + """ + fa, fd = fa*Poly(1/fd.LC(), DE.t), fd.monic() + # interpreting limited integration problem as a + # parametric Risch DE problem + Fa = Poly(0, DE.t) + Fd = Poly(1, DE.t) + G = [(fa, fd)] + G + h, A = param_rischDE(Fa, Fd, G, DE) + V = A.nullspace() + V = [v for v in V if v[0] != 0] + if not V: + return None + else: + # we can take any vector from V, we take V[0] + c0 = V[0][0] + # v = [-1, c1, ..., cm, d1, ..., dr] + v = V[0]/(-c0) + r = len(h) + m = len(v) - r - 1 + C = list(v[1: m + 1]) + y = -sum(v[m + 1 + i]*h[i][0].as_expr()/h[i][1].as_expr() \ + for i in range(r)) + y_num, y_den = y.as_numer_denom() + Ya, Yd = Poly(y_num, DE.t), Poly(y_den, DE.t) + Y = Ya*Poly(1/Yd.LC(), DE.t), Yd.monic() + return Y, C + + +def parametric_log_deriv_heu(fa, fd, wa, wd, DE, c1=None): + """ + Parametric logarithmic derivative heuristic. + + Explanation + =========== + + Given a derivation D on k[t], f in k(t), and a hyperexponential monomial + theta over k(t), raises either NotImplementedError, in which case the + heuristic failed, or returns None, in which case it has proven that no + solution exists, or returns a solution (n, m, v) of the equation + n*f == Dv/v + m*Dtheta/theta, with v in k(t)* and n, m in ZZ with n != 0. + + If this heuristic fails, the structure theorem approach will need to be + used. + + The argument w == Dtheta/theta + """ + # TODO: finish writing this and write tests + c1 = c1 or Dummy('c1') + + p, a = fa.div(fd) + q, b = wa.div(wd) + + B = max(0, derivation(DE.t, DE).degree(DE.t) - 1) + C = max(p.degree(DE.t), q.degree(DE.t)) + + if q.degree(DE.t) > B: + eqs = [p.nth(i) - c1*q.nth(i) for i in range(B + 1, C + 1)] + s = solve(eqs, c1) + if not s or not s[c1].is_Rational: + # deg(q) > B, no solution for c. + return None + + M, N = s[c1].as_numer_denom() + M_poly = M.as_poly(q.gens) + N_poly = N.as_poly(q.gens) + + nfmwa = N_poly*fa*wd - M_poly*wa*fd + nfmwd = fd*wd + Qv = is_log_deriv_k_t_radical_in_field(nfmwa, nfmwd, DE, 'auto') + if Qv is None: + # (N*f - M*w) is not the logarithmic derivative of a k(t)-radical. + return None + + Q, v = Qv + + if Q.is_zero or v.is_zero: + return None + + return (Q*N, Q*M, v) + + if p.degree(DE.t) > B: + return None + + c = lcm(fd.as_poly(DE.t).LC(), wd.as_poly(DE.t).LC()) + l = fd.monic().lcm(wd.monic())*Poly(c, DE.t) + ln, ls = splitfactor(l, DE) + z = ls*ln.gcd(ln.diff(DE.t)) + + if not z.has(DE.t): + # TODO: We treat this as 'no solution', until the structure + # theorem version of parametric_log_deriv is implemented. + return None + + u1, r1 = (fa*l.quo(fd)).div(z) # (l*f).div(z) + u2, r2 = (wa*l.quo(wd)).div(z) # (l*w).div(z) + + eqs = [r1.nth(i) - c1*r2.nth(i) for i in range(z.degree(DE.t))] + s = solve(eqs, c1) + if not s or not s[c1].is_Rational: + # deg(q) <= B, no solution for c. + return None + + M, N = s[c1].as_numer_denom() + + nfmwa = N.as_poly(DE.t)*fa*wd - M.as_poly(DE.t)*wa*fd + nfmwd = fd*wd + Qv = is_log_deriv_k_t_radical_in_field(nfmwa, nfmwd, DE) + if Qv is None: + # (N*f - M*w) is not the logarithmic derivative of a k(t)-radical. + return None + + Q, v = Qv + + if Q.is_zero or v.is_zero: + return None + + return (Q*N, Q*M, v) + + +def parametric_log_deriv(fa, fd, wa, wd, DE): + # TODO: Write the full algorithm using the structure theorems. +# try: + A = parametric_log_deriv_heu(fa, fd, wa, wd, DE) +# except NotImplementedError: + # Heuristic failed, we have to use the full method. + # TODO: This could be implemented more efficiently. + # It isn't too worrisome, because the heuristic handles most difficult + # cases. + return A + + +def is_deriv_k(fa, fd, DE): + r""" + Checks if Df/f is the derivative of an element of k(t). + + Explanation + =========== + + a in k(t) is the derivative of an element of k(t) if there exists b in k(t) + such that a = Db. Either returns (ans, u), such that Df/f == Du, or None, + which means that Df/f is not the derivative of an element of k(t). ans is + a list of tuples such that Add(*[i*j for i, j in ans]) == u. This is useful + for seeing exactly which elements of k(t) produce u. + + This function uses the structure theorem approach, which says that for any + f in K, Df/f is the derivative of a element of K if and only if there are ri + in QQ such that:: + + --- --- Dt + \ r * Dt + \ r * i Df + / i i / i --- = --. + --- --- t f + i in L i in E i + K/C(x) K/C(x) + + + Where C = Const(K), L_K/C(x) = { i in {1, ..., n} such that t_i is + transcendental over C(x)(t_1, ..., t_i-1) and Dt_i = Da_i/a_i, for some a_i + in C(x)(t_1, ..., t_i-1)* } (i.e., the set of all indices of logarithmic + monomials of K over C(x)), and E_K/C(x) = { i in {1, ..., n} such that t_i + is transcendental over C(x)(t_1, ..., t_i-1) and Dt_i/t_i = Da_i, for some + a_i in C(x)(t_1, ..., t_i-1) } (i.e., the set of all indices of + hyperexponential monomials of K over C(x)). If K is an elementary extension + over C(x), then the cardinality of L_K/C(x) U E_K/C(x) is exactly the + transcendence degree of K over C(x). Furthermore, because Const_D(K) == + Const_D(C(x)) == C, deg(Dt_i) == 1 when t_i is in E_K/C(x) and + deg(Dt_i) == 0 when t_i is in L_K/C(x), implying in particular that E_K/C(x) + and L_K/C(x) are disjoint. + + The sets L_K/C(x) and E_K/C(x) must, by their nature, be computed + recursively using this same function. Therefore, it is required to pass + them as indices to D (or T). E_args are the arguments of the + hyperexponentials indexed by E_K (i.e., if i is in E_K, then T[i] == + exp(E_args[i])). This is needed to compute the final answer u such that + Df/f == Du. + + log(f) will be the same as u up to a additive constant. This is because + they will both behave the same as monomials. For example, both log(x) and + log(2*x) == log(x) + log(2) satisfy Dt == 1/x, because log(2) is constant. + Therefore, the term const is returned. const is such that + log(const) + f == u. This is calculated by dividing the arguments of one + logarithm from the other. Therefore, it is necessary to pass the arguments + of the logarithmic terms in L_args. + + To handle the case where we are given Df/f, not f, use is_deriv_k_in_field(). + + See also + ======== + is_log_deriv_k_t_radical_in_field, is_log_deriv_k_t_radical + + """ + # Compute Df/f + dfa, dfd = (fd*derivation(fa, DE) - fa*derivation(fd, DE)), fd*fa + dfa, dfd = dfa.cancel(dfd, include=True) + + # Our assumption here is that each monomial is recursively transcendental + if len(DE.exts) != len(DE.D): + if [i for i in DE.cases if i == 'tan'] or \ + ({i for i in DE.cases if i == 'primitive'} - + set(DE.indices('log'))): + raise NotImplementedError("Real version of the structure " + "theorems with hypertangent support is not yet implemented.") + + # TODO: What should really be done in this case? + raise NotImplementedError("Nonelementary extensions not supported " + "in the structure theorems.") + + E_part = [DE.D[i].quo(Poly(DE.T[i], DE.T[i])).as_expr() for i in DE.indices('exp')] + L_part = [DE.D[i].as_expr() for i in DE.indices('log')] + + # The expression dfa/dfd might not be polynomial in any of its symbols so we + # use a Dummy as the generator for PolyMatrix. + dum = Dummy() + lhs = Matrix([E_part + L_part], dum) + rhs = Matrix([dfa.as_expr()/dfd.as_expr()], dum) + + A, u = constant_system(lhs, rhs, DE) + + u = u.to_Matrix() # Poly to Expr + + if not A or not all(derivation(i, DE, basic=True).is_zero for i in u): + # If the elements of u are not all constant + # Note: See comment in constant_system + + # Also note: derivation(basic=True) calls cancel() + return None + else: + if not all(i.is_Rational for i in u): + raise NotImplementedError("Cannot work with non-rational " + "coefficients in this case.") + else: + terms = ([DE.extargs[i] for i in DE.indices('exp')] + + [DE.T[i] for i in DE.indices('log')]) + ans = list(zip(terms, u)) + result = Add(*[Mul(i, j) for i, j in ans]) + argterms = ([DE.T[i] for i in DE.indices('exp')] + + [DE.extargs[i] for i in DE.indices('log')]) + l = [] + ld = [] + for i, j in zip(argterms, u): + # We need to get around things like sqrt(x**2) != x + # and also sqrt(x**2 + 2*x + 1) != x + 1 + # Issue 10798: i need not be a polynomial + i, d = i.as_numer_denom() + icoeff, iterms = sqf_list(i) + l.append(Mul(*([Pow(icoeff, j)] + [Pow(b, e*j) for b, e in iterms]))) + dcoeff, dterms = sqf_list(d) + ld.append(Mul(*([Pow(dcoeff, j)] + [Pow(b, e*j) for b, e in dterms]))) + const = cancel(fa.as_expr()/fd.as_expr()/Mul(*l)*Mul(*ld)) + + return (ans, result, const) + + +def is_log_deriv_k_t_radical(fa, fd, DE, Df=True): + r""" + Checks if Df is the logarithmic derivative of a k(t)-radical. + + Explanation + =========== + + b in k(t) can be written as the logarithmic derivative of a k(t) radical if + there exist n in ZZ and u in k(t) with n, u != 0 such that n*b == Du/u. + Either returns (ans, u, n, const) or None, which means that Df cannot be + written as the logarithmic derivative of a k(t)-radical. ans is a list of + tuples such that Mul(*[i**j for i, j in ans]) == u. This is useful for + seeing exactly what elements of k(t) produce u. + + This function uses the structure theorem approach, which says that for any + f in K, Df is the logarithmic derivative of a K-radical if and only if there + are ri in QQ such that:: + + --- --- Dt + \ r * Dt + \ r * i + / i i / i --- = Df. + --- --- t + i in L i in E i + K/C(x) K/C(x) + + + Where C = Const(K), L_K/C(x) = { i in {1, ..., n} such that t_i is + transcendental over C(x)(t_1, ..., t_i-1) and Dt_i = Da_i/a_i, for some a_i + in C(x)(t_1, ..., t_i-1)* } (i.e., the set of all indices of logarithmic + monomials of K over C(x)), and E_K/C(x) = { i in {1, ..., n} such that t_i + is transcendental over C(x)(t_1, ..., t_i-1) and Dt_i/t_i = Da_i, for some + a_i in C(x)(t_1, ..., t_i-1) } (i.e., the set of all indices of + hyperexponential monomials of K over C(x)). If K is an elementary extension + over C(x), then the cardinality of L_K/C(x) U E_K/C(x) is exactly the + transcendence degree of K over C(x). Furthermore, because Const_D(K) == + Const_D(C(x)) == C, deg(Dt_i) == 1 when t_i is in E_K/C(x) and + deg(Dt_i) == 0 when t_i is in L_K/C(x), implying in particular that E_K/C(x) + and L_K/C(x) are disjoint. + + The sets L_K/C(x) and E_K/C(x) must, by their nature, be computed + recursively using this same function. Therefore, it is required to pass + them as indices to D (or T). L_args are the arguments of the logarithms + indexed by L_K (i.e., if i is in L_K, then T[i] == log(L_args[i])). This is + needed to compute the final answer u such that n*f == Du/u. + + exp(f) will be the same as u up to a multiplicative constant. This is + because they will both behave the same as monomials. For example, both + exp(x) and exp(x + 1) == E*exp(x) satisfy Dt == t. Therefore, the term const + is returned. const is such that exp(const)*f == u. This is calculated by + subtracting the arguments of one exponential from the other. Therefore, it + is necessary to pass the arguments of the exponential terms in E_args. + + To handle the case where we are given Df, not f, use + is_log_deriv_k_t_radical_in_field(). + + See also + ======== + + is_log_deriv_k_t_radical_in_field, is_deriv_k + + """ + if Df: + dfa, dfd = (fd*derivation(fa, DE) - fa*derivation(fd, DE)).cancel(fd**2, + include=True) + else: + dfa, dfd = fa, fd + + # Our assumption here is that each monomial is recursively transcendental + if len(DE.exts) != len(DE.D): + if [i for i in DE.cases if i == 'tan'] or \ + ({i for i in DE.cases if i == 'primitive'} - + set(DE.indices('log'))): + raise NotImplementedError("Real version of the structure " + "theorems with hypertangent support is not yet implemented.") + + # TODO: What should really be done in this case? + raise NotImplementedError("Nonelementary extensions not supported " + "in the structure theorems.") + + E_part = [DE.D[i].quo(Poly(DE.T[i], DE.T[i])).as_expr() for i in DE.indices('exp')] + L_part = [DE.D[i].as_expr() for i in DE.indices('log')] + + # The expression dfa/dfd might not be polynomial in any of its symbols so we + # use a Dummy as the generator for PolyMatrix. + dum = Dummy() + lhs = Matrix([E_part + L_part], dum) + rhs = Matrix([dfa.as_expr()/dfd.as_expr()], dum) + + A, u = constant_system(lhs, rhs, DE) + + u = u.to_Matrix() # Poly to Expr + + if not A or not all(derivation(i, DE, basic=True).is_zero for i in u): + # If the elements of u are not all constant + # Note: See comment in constant_system + + # Also note: derivation(basic=True) calls cancel() + return None + else: + if not all(i.is_Rational for i in u): + # TODO: But maybe we can tell if they're not rational, like + # log(2)/log(3). Also, there should be an option to continue + # anyway, even if the result might potentially be wrong. + raise NotImplementedError("Cannot work with non-rational " + "coefficients in this case.") + else: + n = S.One*reduce(ilcm, [i.as_numer_denom()[1] for i in u]) + u *= n + terms = ([DE.T[i] for i in DE.indices('exp')] + + [DE.extargs[i] for i in DE.indices('log')]) + ans = list(zip(terms, u)) + result = Mul(*[Pow(i, j) for i, j in ans]) + + # exp(f) will be the same as result up to a multiplicative + # constant. We now find the log of that constant. + argterms = ([DE.extargs[i] for i in DE.indices('exp')] + + [DE.T[i] for i in DE.indices('log')]) + const = cancel(fa.as_expr()/fd.as_expr() - + Add(*[Mul(i, j/n) for i, j in zip(argterms, u)])) + + return (ans, result, n, const) + + +def is_log_deriv_k_t_radical_in_field(fa, fd, DE, case='auto', z=None): + """ + Checks if f can be written as the logarithmic derivative of a k(t)-radical. + + Explanation + =========== + + It differs from is_log_deriv_k_t_radical(fa, fd, DE, Df=False) + for any given fa, fd, DE in that it finds the solution in the + given field not in some (possibly unspecified extension) and + "in_field" with the function name is used to indicate that. + + f in k(t) can be written as the logarithmic derivative of a k(t) radical if + there exist n in ZZ and u in k(t) with n, u != 0 such that n*f == Du/u. + Either returns (n, u) or None, which means that f cannot be written as the + logarithmic derivative of a k(t)-radical. + + case is one of {'primitive', 'exp', 'tan', 'auto'} for the primitive, + hyperexponential, and hypertangent cases, respectively. If case is 'auto', + it will attempt to determine the type of the derivation automatically. + + See also + ======== + is_log_deriv_k_t_radical, is_deriv_k + + """ + fa, fd = fa.cancel(fd, include=True) + + # f must be simple + n, s = splitfactor(fd, DE) + if not s.is_one: + pass + + z = z or Dummy('z') + H, b = residue_reduce(fa, fd, DE, z=z) + if not b: + # I will have to verify, but I believe that the answer should be + # None in this case. This should never happen for the + # functions given when solving the parametric logarithmic + # derivative problem when integration elementary functions (see + # Bronstein's book, page 255), so most likely this indicates a bug. + return None + + roots = [(i, i.real_roots()) for i, _ in H] + if not all(len(j) == i.degree() and all(k.is_Rational for k in j) for + i, j in roots): + # If f is the logarithmic derivative of a k(t)-radical, then all the + # roots of the resultant must be rational numbers. + return None + + # [(a, i), ...], where i*log(a) is a term in the log-part of the integral + # of f + respolys, residues = list(zip(*roots)) or [[], []] + # Note: this might be empty, but everything below should work find in that + # case (it should be the same as if it were [[1, 1]]) + residueterms = [(H[j][1].subs(z, i), i) for j in range(len(H)) for + i in residues[j]] + + # TODO: finish writing this and write tests + + p = cancel(fa.as_expr()/fd.as_expr() - residue_reduce_derivation(H, DE, z)) + + p = p.as_poly(DE.t) + if p is None: + # f - Dg will be in k[t] if f is the logarithmic derivative of a k(t)-radical + return None + + if p.degree(DE.t) >= max(1, DE.d.degree(DE.t)): + return None + + if case == 'auto': + case = DE.case + + if case == 'exp': + wa, wd = derivation(DE.t, DE).cancel(Poly(DE.t, DE.t), include=True) + with DecrementLevel(DE): + pa, pd = frac_in(p, DE.t, cancel=True) + wa, wd = frac_in((wa, wd), DE.t) + A = parametric_log_deriv(pa, pd, wa, wd, DE) + if A is None: + return None + n, e, u = A + u *= DE.t**e + + elif case == 'primitive': + with DecrementLevel(DE): + pa, pd = frac_in(p, DE.t) + A = is_log_deriv_k_t_radical_in_field(pa, pd, DE, case='auto') + if A is None: + return None + n, u = A + + elif case == 'base': + # TODO: we can use more efficient residue reduction from ratint() + if not fd.is_sqf or fa.degree() >= fd.degree(): + # f is the logarithmic derivative in the base case if and only if + # f = fa/fd, fd is square-free, deg(fa) < deg(fd), and + # gcd(fa, fd) == 1. The last condition is handled by cancel() above. + return None + # Note: if residueterms = [], returns (1, 1) + # f had better be 0 in that case. + n = S.One*reduce(ilcm, [i.as_numer_denom()[1] for _, i in residueterms], 1) + u = Mul(*[Pow(i, j*n) for i, j in residueterms]) + return (n, u) + + elif case == 'tan': + raise NotImplementedError("The hypertangent case is " + "not yet implemented for is_log_deriv_k_t_radical_in_field()") + + elif case in ('other_linear', 'other_nonlinear'): + # XXX: If these are supported by the structure theorems, change to NotImplementedError. + raise ValueError("The %s case is not supported in this function." % case) + + else: + raise ValueError("case must be one of {'primitive', 'exp', 'tan', " + "'base', 'auto'}, not %s" % case) + + common_denom = S.One*reduce(ilcm, [i.as_numer_denom()[1] for i in [j for _, j in + residueterms]] + [n], 1) + residueterms = [(i, j*common_denom) for i, j in residueterms] + m = common_denom//n + if common_denom != n*m: # Verify exact division + raise ValueError("Inexact division") + u = cancel(u**m*Mul(*[Pow(i, j) for i, j in residueterms])) + + return (common_denom, u) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/quadrature.py b/venv/lib/python3.11/site-packages/sympy/integrals/quadrature.py new file mode 100644 index 0000000000000000000000000000000000000000..b518bd427dc9980d6a941d2e1ef4d139c5f0f5f9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/quadrature.py @@ -0,0 +1,617 @@ +from sympy.core import S, Dummy, pi +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.trigonometric import sin, cos +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.special.gamma_functions import gamma +from sympy.polys.orthopolys import (legendre_poly, laguerre_poly, + hermite_poly, jacobi_poly) +from sympy.polys.rootoftools import RootOf + + +def gauss_legendre(n, n_digits): + r""" + Computes the Gauss-Legendre quadrature [1]_ points and weights. + + Explanation + =========== + + The Gauss-Legendre quadrature approximates the integral: + + .. math:: + \int_{-1}^1 f(x)\,dx \approx \sum_{i=1}^n w_i f(x_i) + + The nodes `x_i` of an order `n` quadrature rule are the roots of `P_n` + and the weights `w_i` are given by: + + .. math:: + w_i = \frac{2}{\left(1-x_i^2\right) \left(P'_n(x_i)\right)^2} + + Parameters + ========== + + n : + The order of quadrature. + n_digits : + Number of significant digits of the points and weights to return. + + Returns + ======= + + (x, w) : the ``x`` and ``w`` are lists of points and weights as Floats. + The points `x_i` and weights `w_i` are returned as ``(x, w)`` + tuple of lists. + + Examples + ======== + + >>> from sympy.integrals.quadrature import gauss_legendre + >>> x, w = gauss_legendre(3, 5) + >>> x + [-0.7746, 0, 0.7746] + >>> w + [0.55556, 0.88889, 0.55556] + >>> x, w = gauss_legendre(4, 5) + >>> x + [-0.86114, -0.33998, 0.33998, 0.86114] + >>> w + [0.34785, 0.65215, 0.65215, 0.34785] + + See Also + ======== + + gauss_laguerre, gauss_gen_laguerre, gauss_hermite, gauss_chebyshev_t, gauss_chebyshev_u, gauss_jacobi, gauss_lobatto + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gaussian_quadrature + .. [2] https://people.sc.fsu.edu/~jburkardt/cpp_src/legendre_rule/legendre_rule.html + """ + x = Dummy("x") + p = legendre_poly(n, x, polys=True) + pd = p.diff(x) + xi = [] + w = [] + for r in p.real_roots(): + if isinstance(r, RootOf): + r = r.eval_rational(S.One/10**(n_digits+2)) + xi.append(r.n(n_digits)) + w.append((2/((1-r**2) * pd.subs(x, r)**2)).n(n_digits)) + return xi, w + + +def gauss_laguerre(n, n_digits): + r""" + Computes the Gauss-Laguerre quadrature [1]_ points and weights. + + Explanation + =========== + + The Gauss-Laguerre quadrature approximates the integral: + + .. math:: + \int_0^{\infty} e^{-x} f(x)\,dx \approx \sum_{i=1}^n w_i f(x_i) + + + The nodes `x_i` of an order `n` quadrature rule are the roots of `L_n` + and the weights `w_i` are given by: + + .. math:: + w_i = \frac{x_i}{(n+1)^2 \left(L_{n+1}(x_i)\right)^2} + + Parameters + ========== + + n : + The order of quadrature. + n_digits : + Number of significant digits of the points and weights to return. + + Returns + ======= + + (x, w) : The ``x`` and ``w`` are lists of points and weights as Floats. + The points `x_i` and weights `w_i` are returned as ``(x, w)`` + tuple of lists. + + Examples + ======== + + >>> from sympy.integrals.quadrature import gauss_laguerre + >>> x, w = gauss_laguerre(3, 5) + >>> x + [0.41577, 2.2943, 6.2899] + >>> w + [0.71109, 0.27852, 0.010389] + >>> x, w = gauss_laguerre(6, 5) + >>> x + [0.22285, 1.1889, 2.9927, 5.7751, 9.8375, 15.983] + >>> w + [0.45896, 0.417, 0.11337, 0.010399, 0.00026102, 8.9855e-7] + + See Also + ======== + + gauss_legendre, gauss_gen_laguerre, gauss_hermite, gauss_chebyshev_t, gauss_chebyshev_u, gauss_jacobi, gauss_lobatto + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gauss%E2%80%93Laguerre_quadrature + .. [2] https://people.sc.fsu.edu/~jburkardt/cpp_src/laguerre_rule/laguerre_rule.html + """ + x = Dummy("x") + p = laguerre_poly(n, x, polys=True) + p1 = laguerre_poly(n+1, x, polys=True) + xi = [] + w = [] + for r in p.real_roots(): + if isinstance(r, RootOf): + r = r.eval_rational(S.One/10**(n_digits+2)) + xi.append(r.n(n_digits)) + w.append((r/((n+1)**2 * p1.subs(x, r)**2)).n(n_digits)) + return xi, w + + +def gauss_hermite(n, n_digits): + r""" + Computes the Gauss-Hermite quadrature [1]_ points and weights. + + Explanation + =========== + + The Gauss-Hermite quadrature approximates the integral: + + .. math:: + \int_{-\infty}^{\infty} e^{-x^2} f(x)\,dx \approx + \sum_{i=1}^n w_i f(x_i) + + The nodes `x_i` of an order `n` quadrature rule are the roots of `H_n` + and the weights `w_i` are given by: + + .. math:: + w_i = \frac{2^{n-1} n! \sqrt{\pi}}{n^2 \left(H_{n-1}(x_i)\right)^2} + + Parameters + ========== + + n : + The order of quadrature. + n_digits : + Number of significant digits of the points and weights to return. + + Returns + ======= + + (x, w) : The ``x`` and ``w`` are lists of points and weights as Floats. + The points `x_i` and weights `w_i` are returned as ``(x, w)`` + tuple of lists. + + Examples + ======== + + >>> from sympy.integrals.quadrature import gauss_hermite + >>> x, w = gauss_hermite(3, 5) + >>> x + [-1.2247, 0, 1.2247] + >>> w + [0.29541, 1.1816, 0.29541] + + >>> x, w = gauss_hermite(6, 5) + >>> x + [-2.3506, -1.3358, -0.43608, 0.43608, 1.3358, 2.3506] + >>> w + [0.00453, 0.15707, 0.72463, 0.72463, 0.15707, 0.00453] + + See Also + ======== + + gauss_legendre, gauss_laguerre, gauss_gen_laguerre, gauss_chebyshev_t, gauss_chebyshev_u, gauss_jacobi, gauss_lobatto + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gauss-Hermite_Quadrature + .. [2] https://people.sc.fsu.edu/~jburkardt/cpp_src/hermite_rule/hermite_rule.html + .. [3] https://people.sc.fsu.edu/~jburkardt/cpp_src/gen_hermite_rule/gen_hermite_rule.html + """ + x = Dummy("x") + p = hermite_poly(n, x, polys=True) + p1 = hermite_poly(n-1, x, polys=True) + xi = [] + w = [] + for r in p.real_roots(): + if isinstance(r, RootOf): + r = r.eval_rational(S.One/10**(n_digits+2)) + xi.append(r.n(n_digits)) + w.append(((2**(n-1) * factorial(n) * sqrt(pi)) / + (n**2 * p1.subs(x, r)**2)).n(n_digits)) + return xi, w + + +def gauss_gen_laguerre(n, alpha, n_digits): + r""" + Computes the generalized Gauss-Laguerre quadrature [1]_ points and weights. + + Explanation + =========== + + The generalized Gauss-Laguerre quadrature approximates the integral: + + .. math:: + \int_{0}^\infty x^{\alpha} e^{-x} f(x)\,dx \approx + \sum_{i=1}^n w_i f(x_i) + + The nodes `x_i` of an order `n` quadrature rule are the roots of + `L^{\alpha}_n` and the weights `w_i` are given by: + + .. math:: + w_i = \frac{\Gamma(\alpha+n)} + {n \Gamma(n) L^{\alpha}_{n-1}(x_i) L^{\alpha+1}_{n-1}(x_i)} + + Parameters + ========== + + n : + The order of quadrature. + + alpha : + The exponent of the singularity, `\alpha > -1`. + + n_digits : + Number of significant digits of the points and weights to return. + + Returns + ======= + + (x, w) : the ``x`` and ``w`` are lists of points and weights as Floats. + The points `x_i` and weights `w_i` are returned as ``(x, w)`` + tuple of lists. + + Examples + ======== + + >>> from sympy import S + >>> from sympy.integrals.quadrature import gauss_gen_laguerre + >>> x, w = gauss_gen_laguerre(3, -S.Half, 5) + >>> x + [0.19016, 1.7845, 5.5253] + >>> w + [1.4493, 0.31413, 0.00906] + + >>> x, w = gauss_gen_laguerre(4, 3*S.Half, 5) + >>> x + [0.97851, 2.9904, 6.3193, 11.712] + >>> w + [0.53087, 0.67721, 0.11895, 0.0023152] + + See Also + ======== + + gauss_legendre, gauss_laguerre, gauss_hermite, gauss_chebyshev_t, gauss_chebyshev_u, gauss_jacobi, gauss_lobatto + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gauss%E2%80%93Laguerre_quadrature + .. [2] https://people.sc.fsu.edu/~jburkardt/cpp_src/gen_laguerre_rule/gen_laguerre_rule.html + """ + x = Dummy("x") + p = laguerre_poly(n, x, alpha=alpha, polys=True) + p1 = laguerre_poly(n-1, x, alpha=alpha, polys=True) + p2 = laguerre_poly(n-1, x, alpha=alpha+1, polys=True) + xi = [] + w = [] + for r in p.real_roots(): + if isinstance(r, RootOf): + r = r.eval_rational(S.One/10**(n_digits+2)) + xi.append(r.n(n_digits)) + w.append((gamma(alpha+n) / + (n*gamma(n)*p1.subs(x, r)*p2.subs(x, r))).n(n_digits)) + return xi, w + + +def gauss_chebyshev_t(n, n_digits): + r""" + Computes the Gauss-Chebyshev quadrature [1]_ points and weights of + the first kind. + + Explanation + =========== + + The Gauss-Chebyshev quadrature of the first kind approximates the integral: + + .. math:: + \int_{-1}^{1} \frac{1}{\sqrt{1-x^2}} f(x)\,dx \approx + \sum_{i=1}^n w_i f(x_i) + + The nodes `x_i` of an order `n` quadrature rule are the roots of `T_n` + and the weights `w_i` are given by: + + .. math:: + w_i = \frac{\pi}{n} + + Parameters + ========== + + n : + The order of quadrature. + + n_digits : + Number of significant digits of the points and weights to return. + + Returns + ======= + + (x, w) : the ``x`` and ``w`` are lists of points and weights as Floats. + The points `x_i` and weights `w_i` are returned as ``(x, w)`` + tuple of lists. + + Examples + ======== + + >>> from sympy.integrals.quadrature import gauss_chebyshev_t + >>> x, w = gauss_chebyshev_t(3, 5) + >>> x + [0.86602, 0, -0.86602] + >>> w + [1.0472, 1.0472, 1.0472] + + >>> x, w = gauss_chebyshev_t(6, 5) + >>> x + [0.96593, 0.70711, 0.25882, -0.25882, -0.70711, -0.96593] + >>> w + [0.5236, 0.5236, 0.5236, 0.5236, 0.5236, 0.5236] + + See Also + ======== + + gauss_legendre, gauss_laguerre, gauss_hermite, gauss_gen_laguerre, gauss_chebyshev_u, gauss_jacobi, gauss_lobatto + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Chebyshev%E2%80%93Gauss_quadrature + .. [2] https://people.sc.fsu.edu/~jburkardt/cpp_src/chebyshev1_rule/chebyshev1_rule.html + """ + xi = [] + w = [] + for i in range(1, n+1): + xi.append((cos((2*i-S.One)/(2*n)*S.Pi)).n(n_digits)) + w.append((S.Pi/n).n(n_digits)) + return xi, w + + +def gauss_chebyshev_u(n, n_digits): + r""" + Computes the Gauss-Chebyshev quadrature [1]_ points and weights of + the second kind. + + Explanation + =========== + + The Gauss-Chebyshev quadrature of the second kind approximates the + integral: + + .. math:: + \int_{-1}^{1} \sqrt{1-x^2} f(x)\,dx \approx \sum_{i=1}^n w_i f(x_i) + + The nodes `x_i` of an order `n` quadrature rule are the roots of `U_n` + and the weights `w_i` are given by: + + .. math:: + w_i = \frac{\pi}{n+1} \sin^2 \left(\frac{i}{n+1}\pi\right) + + Parameters + ========== + + n : the order of quadrature + + n_digits : number of significant digits of the points and weights to return + + Returns + ======= + + (x, w) : the ``x`` and ``w`` are lists of points and weights as Floats. + The points `x_i` and weights `w_i` are returned as ``(x, w)`` + tuple of lists. + + Examples + ======== + + >>> from sympy.integrals.quadrature import gauss_chebyshev_u + >>> x, w = gauss_chebyshev_u(3, 5) + >>> x + [0.70711, 0, -0.70711] + >>> w + [0.3927, 0.7854, 0.3927] + + >>> x, w = gauss_chebyshev_u(6, 5) + >>> x + [0.90097, 0.62349, 0.22252, -0.22252, -0.62349, -0.90097] + >>> w + [0.084489, 0.27433, 0.42658, 0.42658, 0.27433, 0.084489] + + See Also + ======== + + gauss_legendre, gauss_laguerre, gauss_hermite, gauss_gen_laguerre, gauss_chebyshev_t, gauss_jacobi, gauss_lobatto + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Chebyshev%E2%80%93Gauss_quadrature + .. [2] https://people.sc.fsu.edu/~jburkardt/cpp_src/chebyshev2_rule/chebyshev2_rule.html + """ + xi = [] + w = [] + for i in range(1, n+1): + xi.append((cos(i/(n+S.One)*S.Pi)).n(n_digits)) + w.append((S.Pi/(n+S.One)*sin(i*S.Pi/(n+S.One))**2).n(n_digits)) + return xi, w + + +def gauss_jacobi(n, alpha, beta, n_digits): + r""" + Computes the Gauss-Jacobi quadrature [1]_ points and weights. + + Explanation + =========== + + The Gauss-Jacobi quadrature of the first kind approximates the integral: + + .. math:: + \int_{-1}^1 (1-x)^\alpha (1+x)^\beta f(x)\,dx \approx + \sum_{i=1}^n w_i f(x_i) + + The nodes `x_i` of an order `n` quadrature rule are the roots of + `P^{(\alpha,\beta)}_n` and the weights `w_i` are given by: + + .. math:: + w_i = -\frac{2n+\alpha+\beta+2}{n+\alpha+\beta+1} + \frac{\Gamma(n+\alpha+1)\Gamma(n+\beta+1)} + {\Gamma(n+\alpha+\beta+1)(n+1)!} + \frac{2^{\alpha+\beta}}{P'_n(x_i) + P^{(\alpha,\beta)}_{n+1}(x_i)} + + Parameters + ========== + + n : the order of quadrature + + alpha : the first parameter of the Jacobi Polynomial, `\alpha > -1` + + beta : the second parameter of the Jacobi Polynomial, `\beta > -1` + + n_digits : number of significant digits of the points and weights to return + + Returns + ======= + + (x, w) : the ``x`` and ``w`` are lists of points and weights as Floats. + The points `x_i` and weights `w_i` are returned as ``(x, w)`` + tuple of lists. + + Examples + ======== + + >>> from sympy import S + >>> from sympy.integrals.quadrature import gauss_jacobi + >>> x, w = gauss_jacobi(3, S.Half, -S.Half, 5) + >>> x + [-0.90097, -0.22252, 0.62349] + >>> w + [1.7063, 1.0973, 0.33795] + + >>> x, w = gauss_jacobi(6, 1, 1, 5) + >>> x + [-0.87174, -0.5917, -0.2093, 0.2093, 0.5917, 0.87174] + >>> w + [0.050584, 0.22169, 0.39439, 0.39439, 0.22169, 0.050584] + + See Also + ======== + + gauss_legendre, gauss_laguerre, gauss_hermite, gauss_gen_laguerre, + gauss_chebyshev_t, gauss_chebyshev_u, gauss_lobatto + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gauss%E2%80%93Jacobi_quadrature + .. [2] https://people.sc.fsu.edu/~jburkardt/cpp_src/jacobi_rule/jacobi_rule.html + .. [3] https://people.sc.fsu.edu/~jburkardt/cpp_src/gegenbauer_rule/gegenbauer_rule.html + """ + x = Dummy("x") + p = jacobi_poly(n, alpha, beta, x, polys=True) + pd = p.diff(x) + pn = jacobi_poly(n+1, alpha, beta, x, polys=True) + xi = [] + w = [] + for r in p.real_roots(): + if isinstance(r, RootOf): + r = r.eval_rational(S.One/10**(n_digits+2)) + xi.append(r.n(n_digits)) + w.append(( + - (2*n+alpha+beta+2) / (n+alpha+beta+S.One) * + (gamma(n+alpha+1)*gamma(n+beta+1)) / + (gamma(n+alpha+beta+S.One)*gamma(n+2)) * + 2**(alpha+beta) / (pd.subs(x, r) * pn.subs(x, r))).n(n_digits)) + return xi, w + + +def gauss_lobatto(n, n_digits): + r""" + Computes the Gauss-Lobatto quadrature [1]_ points and weights. + + Explanation + =========== + + The Gauss-Lobatto quadrature approximates the integral: + + .. math:: + \int_{-1}^1 f(x)\,dx \approx \sum_{i=1}^n w_i f(x_i) + + The nodes `x_i` of an order `n` quadrature rule are the roots of `P'_(n-1)` + and the weights `w_i` are given by: + + .. math:: + &w_i = \frac{2}{n(n-1) \left[P_{n-1}(x_i)\right]^2},\quad x\neq\pm 1\\ + &w_i = \frac{2}{n(n-1)},\quad x=\pm 1 + + Parameters + ========== + + n : the order of quadrature + + n_digits : number of significant digits of the points and weights to return + + Returns + ======= + + (x, w) : the ``x`` and ``w`` are lists of points and weights as Floats. + The points `x_i` and weights `w_i` are returned as ``(x, w)`` + tuple of lists. + + Examples + ======== + + >>> from sympy.integrals.quadrature import gauss_lobatto + >>> x, w = gauss_lobatto(3, 5) + >>> x + [-1, 0, 1] + >>> w + [0.33333, 1.3333, 0.33333] + >>> x, w = gauss_lobatto(4, 5) + >>> x + [-1, -0.44721, 0.44721, 1] + >>> w + [0.16667, 0.83333, 0.83333, 0.16667] + + See Also + ======== + + gauss_legendre,gauss_laguerre, gauss_gen_laguerre, gauss_hermite, gauss_chebyshev_t, gauss_chebyshev_u, gauss_jacobi + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Lobatto_rules + .. [2] https://web.archive.org/web/20200118141346/http://people.math.sfu.ca/~cbm/aands/page_888.htm + """ + x = Dummy("x") + p = legendre_poly(n-1, x, polys=True) + pd = p.diff(x) + xi = [] + w = [] + for r in pd.real_roots(): + if isinstance(r, RootOf): + r = r.eval_rational(S.One/10**(n_digits+2)) + xi.append(r.n(n_digits)) + w.append((2/(n*(n-1) * p.subs(x, r)**2)).n(n_digits)) + + xi.insert(0, -1) + xi.append(1) + w.insert(0, (S(2)/(n*(n-1))).n(n_digits)) + w.append((S(2)/(n*(n-1))).n(n_digits)) + return xi, w diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/rationaltools.py b/venv/lib/python3.11/site-packages/sympy/integrals/rationaltools.py new file mode 100644 index 0000000000000000000000000000000000000000..e95ff5da2e9d1be6f07d8fe6e9c572f692e92efb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/rationaltools.py @@ -0,0 +1,445 @@ +"""This module implements tools for integrating rational functions. """ + +from sympy.core.function import Lambda +from sympy.core.numbers import I +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.trigonometric import atan +from sympy.polys.polyerrors import DomainError +from sympy.polys.polyroots import roots +from sympy.polys.polytools import cancel +from sympy.polys.rootoftools import RootSum +from sympy.polys import Poly, resultant, ZZ + + +def ratint(f, x, **flags): + """ + Performs indefinite integration of rational functions. + + Explanation + =========== + + Given a field :math:`K` and a rational function :math:`f = p/q`, + where :math:`p` and :math:`q` are polynomials in :math:`K[x]`, + returns a function :math:`g` such that :math:`f = g'`. + + Examples + ======== + + >>> from sympy.integrals.rationaltools import ratint + >>> from sympy.abc import x + + >>> ratint(36/(x**5 - 2*x**4 - 2*x**3 + 4*x**2 + x - 2), x) + (12*x + 6)/(x**2 - 1) + 4*log(x - 2) - 4*log(x + 1) + + References + ========== + + .. [1] M. Bronstein, Symbolic Integration I: Transcendental + Functions, Second Edition, Springer-Verlag, 2005, pp. 35-70 + + See Also + ======== + + sympy.integrals.integrals.Integral.doit + sympy.integrals.rationaltools.ratint_logpart + sympy.integrals.rationaltools.ratint_ratpart + + """ + if isinstance(f, tuple): + p, q = f + else: + p, q = f.as_numer_denom() + + p, q = Poly(p, x, composite=False, field=True), Poly(q, x, composite=False, field=True) + + coeff, p, q = p.cancel(q) + poly, p = p.div(q) + + result = poly.integrate(x).as_expr() + + if p.is_zero: + return coeff*result + + g, h = ratint_ratpart(p, q, x) + + P, Q = h.as_numer_denom() + + P = Poly(P, x) + Q = Poly(Q, x) + + q, r = P.div(Q) + + result += g + q.integrate(x).as_expr() + + if not r.is_zero: + symbol = flags.get('symbol', 't') + + if not isinstance(symbol, Symbol): + t = Dummy(symbol) + else: + t = symbol.as_dummy() + + L = ratint_logpart(r, Q, x, t) + + real = flags.get('real') + + if real is None: + if isinstance(f, tuple): + p, q = f + atoms = p.atoms() | q.atoms() + else: + atoms = f.atoms() + + for elt in atoms - {x}: + if not elt.is_extended_real: + real = False + break + else: + real = True + + eps = S.Zero + + if not real: + for h, q in L: + _, h = h.primitive() + eps += RootSum( + q, Lambda(t, t*log(h.as_expr())), quadratic=True) + else: + for h, q in L: + _, h = h.primitive() + R = log_to_real(h, q, x, t) + + if R is not None: + eps += R + else: + eps += RootSum( + q, Lambda(t, t*log(h.as_expr())), quadratic=True) + + result += eps + + return coeff*result + + +def ratint_ratpart(f, g, x): + """ + Horowitz-Ostrogradsky algorithm. + + Explanation + =========== + + Given a field K and polynomials f and g in K[x], such that f and g + are coprime and deg(f) < deg(g), returns fractions A and B in K(x), + such that f/g = A' + B and B has square-free denominator. + + Examples + ======== + + >>> from sympy.integrals.rationaltools import ratint_ratpart + >>> from sympy.abc import x, y + >>> from sympy import Poly + >>> ratint_ratpart(Poly(1, x, domain='ZZ'), + ... Poly(x + 1, x, domain='ZZ'), x) + (0, 1/(x + 1)) + >>> ratint_ratpart(Poly(1, x, domain='EX'), + ... Poly(x**2 + y**2, x, domain='EX'), x) + (0, 1/(x**2 + y**2)) + >>> ratint_ratpart(Poly(36, x, domain='ZZ'), + ... Poly(x**5 - 2*x**4 - 2*x**3 + 4*x**2 + x - 2, x, domain='ZZ'), x) + ((12*x + 6)/(x**2 - 1), 12/(x**2 - x - 2)) + + See Also + ======== + + ratint, ratint_logpart + """ + from sympy.solvers.solvers import solve + + f = Poly(f, x) + g = Poly(g, x) + + u, v, _ = g.cofactors(g.diff()) + + n = u.degree() + m = v.degree() + + A_coeffs = [ Dummy('a' + str(n - i)) for i in range(0, n) ] + B_coeffs = [ Dummy('b' + str(m - i)) for i in range(0, m) ] + + C_coeffs = A_coeffs + B_coeffs + + A = Poly(A_coeffs, x, domain=ZZ[C_coeffs]) + B = Poly(B_coeffs, x, domain=ZZ[C_coeffs]) + + H = f - A.diff()*v + A*(u.diff()*v).quo(u) - B*u + + result = solve(H.coeffs(), C_coeffs) + + A = A.as_expr().subs(result) + B = B.as_expr().subs(result) + + rat_part = cancel(A/u.as_expr(), x) + log_part = cancel(B/v.as_expr(), x) + + return rat_part, log_part + + +def ratint_logpart(f, g, x, t=None): + r""" + Lazard-Rioboo-Trager algorithm. + + Explanation + =========== + + Given a field K and polynomials f and g in K[x], such that f and g + are coprime, deg(f) < deg(g) and g is square-free, returns a list + of tuples (s_i, q_i) of polynomials, for i = 1..n, such that s_i + in K[t, x] and q_i in K[t], and:: + + ___ ___ + d f d \ ` \ ` + -- - = -- ) ) a log(s_i(a, x)) + dx g dx /__, /__, + i=1..n a | q_i(a) = 0 + + Examples + ======== + + >>> from sympy.integrals.rationaltools import ratint_logpart + >>> from sympy.abc import x + >>> from sympy import Poly + >>> ratint_logpart(Poly(1, x, domain='ZZ'), + ... Poly(x**2 + x + 1, x, domain='ZZ'), x) + [(Poly(x + 3*_t/2 + 1/2, x, domain='QQ[_t]'), + ...Poly(3*_t**2 + 1, _t, domain='ZZ'))] + >>> ratint_logpart(Poly(12, x, domain='ZZ'), + ... Poly(x**2 - x - 2, x, domain='ZZ'), x) + [(Poly(x - 3*_t/8 - 1/2, x, domain='QQ[_t]'), + ...Poly(-_t**2 + 16, _t, domain='ZZ'))] + + See Also + ======== + + ratint, ratint_ratpart + """ + f, g = Poly(f, x), Poly(g, x) + + t = t or Dummy('t') + a, b = g, f - g.diff()*Poly(t, x) + + res, R = resultant(a, b, includePRS=True) + res = Poly(res, t, composite=False) + + assert res, "BUG: resultant(%s, %s) cannot be zero" % (a, b) + + R_map, H = {}, [] + + for r in R: + R_map[r.degree()] = r + + def _include_sign(c, sqf): + if c.is_extended_real and (c < 0) == True: + h, k = sqf[0] + c_poly = c.as_poly(h.gens) + sqf[0] = h*c_poly, k + + C, res_sqf = res.sqf_list() + _include_sign(C, res_sqf) + + for q, i in res_sqf: + _, q = q.primitive() + + if g.degree() == i: + H.append((g, q)) + else: + h = R_map[i] + h_lc = Poly(h.LC(), t, field=True) + + c, h_lc_sqf = h_lc.sqf_list(all=True) + _include_sign(c, h_lc_sqf) + + for a, j in h_lc_sqf: + h = h.quo(Poly(a.gcd(q)**j, x)) + + inv, coeffs = h_lc.invert(q), [S.One] + + for coeff in h.coeffs()[1:]: + coeff = coeff.as_poly(inv.gens) + T = (inv*coeff).rem(q) + coeffs.append(T.as_expr()) + + h = Poly(dict(list(zip(h.monoms(), coeffs))), x) + + H.append((h, q)) + + return H + + +def log_to_atan(f, g): + """ + Convert complex logarithms to real arctangents. + + Explanation + =========== + + Given a real field K and polynomials f and g in K[x], with g != 0, + returns a sum h of arctangents of polynomials in K[x], such that: + + dh d f + I g + -- = -- I log( ------- ) + dx dx f - I g + + Examples + ======== + + >>> from sympy.integrals.rationaltools import log_to_atan + >>> from sympy.abc import x + >>> from sympy import Poly, sqrt, S + >>> log_to_atan(Poly(x, x, domain='ZZ'), Poly(1, x, domain='ZZ')) + 2*atan(x) + >>> log_to_atan(Poly(x + S(1)/2, x, domain='QQ'), + ... Poly(sqrt(3)/2, x, domain='EX')) + 2*atan(2*sqrt(3)*x/3 + sqrt(3)/3) + + See Also + ======== + + log_to_real + """ + if f.degree() < g.degree(): + f, g = -g, f + + f = f.to_field() + g = g.to_field() + + p, q = f.div(g) + + if q.is_zero: + return 2*atan(p.as_expr()) + else: + s, t, h = g.gcdex(-f) + u = (f*s + g*t).quo(h) + A = 2*atan(u.as_expr()) + + return A + log_to_atan(s, t) + + +def _get_real_roots(f, x): + """get real roots of f if possible""" + rs = roots(f, filter='R') + + try: + num_roots = f.count_roots() + except DomainError: + return rs + else: + if len(rs) == num_roots: + return rs + else: + return None + + +def log_to_real(h, q, x, t): + r""" + Convert complex logarithms to real functions. + + Explanation + =========== + + Given real field K and polynomials h in K[t,x] and q in K[t], + returns real function f such that: + ___ + df d \ ` + -- = -- ) a log(h(a, x)) + dx dx /__, + a | q(a) = 0 + + Examples + ======== + + >>> from sympy.integrals.rationaltools import log_to_real + >>> from sympy.abc import x, y + >>> from sympy import Poly, S + >>> log_to_real(Poly(x + 3*y/2 + S(1)/2, x, domain='QQ[y]'), + ... Poly(3*y**2 + 1, y, domain='ZZ'), x, y) + 2*sqrt(3)*atan(2*sqrt(3)*x/3 + sqrt(3)/3)/3 + >>> log_to_real(Poly(x**2 - 1, x, domain='ZZ'), + ... Poly(-2*y + 1, y, domain='ZZ'), x, y) + log(x**2 - 1)/2 + + See Also + ======== + + log_to_atan + """ + from sympy.simplify.radsimp import collect + u, v = symbols('u,v', cls=Dummy) + + H = h.as_expr().xreplace({t: u + I*v}).expand() + Q = q.as_expr().xreplace({t: u + I*v}).expand() + + H_map = collect(H, I, evaluate=False) + Q_map = collect(Q, I, evaluate=False) + + a, b = H_map.get(S.One, S.Zero), H_map.get(I, S.Zero) + c, d = Q_map.get(S.One, S.Zero), Q_map.get(I, S.Zero) + + R = Poly(resultant(c, d, v), u) + + R_u = _get_real_roots(R, u) + + if R_u is None: + return None + + result = S.Zero + + for r_u in R_u.keys(): + C = Poly(c.xreplace({u: r_u}), v) + if not C: + # t was split into real and imaginary parts + # and denom Q(u, v) = c + I*d. We just found + # that c(r_u) is 0 so the roots are in d + C = Poly(d.xreplace({u: r_u}), v) + # we were going to reject roots from C that + # did not set d to zero, but since we are now + # using C = d and c is already 0, there is + # nothing to check + d = S.Zero + + R_v = _get_real_roots(C, v) + + if R_v is None: + return None + + R_v_paired = [] # take one from each pair of conjugate roots + for r_v in R_v: + if r_v not in R_v_paired and -r_v not in R_v_paired: + if r_v.is_negative or r_v.could_extract_minus_sign(): + R_v_paired.append(-r_v) + elif not r_v.is_zero: + R_v_paired.append(r_v) + + for r_v in R_v_paired: + + D = d.xreplace({u: r_u, v: r_v}) + + if D.evalf(chop=True) != 0: + continue + + A = Poly(a.xreplace({u: r_u, v: r_v}), x) + B = Poly(b.xreplace({u: r_u, v: r_v}), x) + + AB = (A**2 + B**2).as_expr() + + result += r_u*log(AB) + r_v*log_to_atan(A, B) + + R_q = _get_real_roots(q, t) + + if R_q is None: + return None + + for r in R_q.keys(): + result += r*log(h.as_expr().subs(t, r)) + + return result diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/rde.py b/venv/lib/python3.11/site-packages/sympy/integrals/rde.py new file mode 100644 index 0000000000000000000000000000000000000000..9fb14a1d14b743b1e0885bf25a0d4b409e8a610d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/rde.py @@ -0,0 +1,800 @@ +""" +Algorithms for solving the Risch differential equation. + +Given a differential field K of characteristic 0 that is a simple +monomial extension of a base field k and f, g in K, the Risch +Differential Equation problem is to decide if there exist y in K such +that Dy + f*y == g and to find one if there are some. If t is a +monomial over k and the coefficients of f and g are in k(t), then y is +in k(t), and the outline of the algorithm here is given as: + +1. Compute the normal part n of the denominator of y. The problem is +then reduced to finding y' in k, where y == y'/n. +2. Compute the special part s of the denominator of y. The problem is +then reduced to finding y'' in k[t], where y == y''/(n*s) +3. Bound the degree of y''. +4. Reduce the equation Dy + f*y == g to a similar equation with f, g in +k[t]. +5. Find the solutions in k[t] of bounded degree of the reduced equation. + +See Chapter 6 of "Symbolic Integration I: Transcendental Functions" by +Manuel Bronstein. See also the docstring of risch.py. +""" + +from operator import mul +from functools import reduce + +from sympy.core import oo +from sympy.core.symbol import Dummy + +from sympy.polys import Poly, gcd, ZZ, cancel + +from sympy.functions.elementary.complexes import (im, re) +from sympy.functions.elementary.miscellaneous import sqrt + +from sympy.integrals.risch import (gcdex_diophantine, frac_in, derivation, + splitfactor, NonElementaryIntegralException, DecrementLevel, recognize_log_derivative) + +# TODO: Add messages to NonElementaryIntegralException errors + + +def order_at(a, p, t): + """ + Computes the order of a at p, with respect to t. + + Explanation + =========== + + For a, p in k[t], the order of a at p is defined as nu_p(a) = max({n + in Z+ such that p**n|a}), where a != 0. If a == 0, nu_p(a) = +oo. + + To compute the order at a rational function, a/b, use the fact that + nu_p(a/b) == nu_p(a) - nu_p(b). + """ + if a.is_zero: + return oo + if p == Poly(t, t): + return a.as_poly(t).ET()[0][0] + + # Uses binary search for calculating the power. power_list collects the tuples + # (p^k,k) where each k is some power of 2. After deciding the largest k + # such that k is power of 2 and p^k|a the loop iteratively calculates + # the actual power. + power_list = [] + p1 = p + r = a.rem(p1) + tracks_power = 1 + while r.is_zero: + power_list.append((p1,tracks_power)) + p1 = p1*p1 + tracks_power *= 2 + r = a.rem(p1) + n = 0 + product = Poly(1, t) + while len(power_list) != 0: + final = power_list.pop() + productf = product*final[0] + r = a.rem(productf) + if r.is_zero: + n += final[1] + product = productf + return n + + +def order_at_oo(a, d, t): + """ + Computes the order of a/d at oo (infinity), with respect to t. + + For f in k(t), the order or f at oo is defined as deg(d) - deg(a), where + f == a/d. + """ + if a.is_zero: + return oo + return d.degree(t) - a.degree(t) + + +def weak_normalizer(a, d, DE, z=None): + """ + Weak normalization. + + Explanation + =========== + + Given a derivation D on k[t] and f == a/d in k(t), return q in k[t] + such that f - Dq/q is weakly normalized with respect to t. + + f in k(t) is said to be "weakly normalized" with respect to t if + residue_p(f) is not a positive integer for any normal irreducible p + in k[t] such that f is in R_p (Definition 6.1.1). If f has an + elementary integral, this is equivalent to no logarithm of + integral(f) whose argument depends on t has a positive integer + coefficient, where the arguments of the logarithms not in k(t) are + in k[t]. + + Returns (q, f - Dq/q) + """ + z = z or Dummy('z') + dn, ds = splitfactor(d, DE) + + # Compute d1, where dn == d1*d2**2*...*dn**n is a square-free + # factorization of d. + g = gcd(dn, dn.diff(DE.t)) + d_sqf_part = dn.quo(g) + d1 = d_sqf_part.quo(gcd(d_sqf_part, g)) + + a1, b = gcdex_diophantine(d.quo(d1).as_poly(DE.t), d1.as_poly(DE.t), + a.as_poly(DE.t)) + r = (a - Poly(z, DE.t)*derivation(d1, DE)).as_poly(DE.t).resultant( + d1.as_poly(DE.t)) + r = Poly(r, z) + + if not r.expr.has(z): + return (Poly(1, DE.t), (a, d)) + + N = [i for i in r.real_roots() if i in ZZ and i > 0] + + q = reduce(mul, [gcd(a - Poly(n, DE.t)*derivation(d1, DE), d1) for n in N], + Poly(1, DE.t)) + + dq = derivation(q, DE) + sn = q*a - d*dq + sd = q*d + sn, sd = sn.cancel(sd, include=True) + + return (q, (sn, sd)) + + +def normal_denom(fa, fd, ga, gd, DE): + """ + Normal part of the denominator. + + Explanation + =========== + + Given a derivation D on k[t] and f, g in k(t) with f weakly + normalized with respect to t, either raise NonElementaryIntegralException, + in which case the equation Dy + f*y == g has no solution in k(t), or the + quadruplet (a, b, c, h) such that a, h in k[t], b, c in k, and for any + solution y in k(t) of Dy + f*y == g, q = y*h in k satisfies + a*Dq + b*q == c. + + This constitutes step 1 in the outline given in the rde.py docstring. + """ + dn, ds = splitfactor(fd, DE) + en, es = splitfactor(gd, DE) + + p = dn.gcd(en) + h = en.gcd(en.diff(DE.t)).quo(p.gcd(p.diff(DE.t))) + + a = dn*h + c = a*h + if c.div(en)[1]: + # en does not divide dn*h**2 + raise NonElementaryIntegralException + ca = c*ga + ca, cd = ca.cancel(gd, include=True) + + ba = a*fa - dn*derivation(h, DE)*fd + ba, bd = ba.cancel(fd, include=True) + + # (dn*h, dn*h*f - dn*Dh, dn*h**2*g, h) + return (a, (ba, bd), (ca, cd), h) + + +def special_denom(a, ba, bd, ca, cd, DE, case='auto'): + """ + Special part of the denominator. + + Explanation + =========== + + case is one of {'exp', 'tan', 'primitive'} for the hyperexponential, + hypertangent, and primitive cases, respectively. For the + hyperexponential (resp. hypertangent) case, given a derivation D on + k[t] and a in k[t], b, c, in k with Dt/t in k (resp. Dt/(t**2 + 1) in + k, sqrt(-1) not in k), a != 0, and gcd(a, t) == 1 (resp. + gcd(a, t**2 + 1) == 1), return the quadruplet (A, B, C, 1/h) such that + A, B, C, h in k[t] and for any solution q in k of a*Dq + b*q == c, + r = qh in k[t] satisfies A*Dr + B*r == C. + + For ``case == 'primitive'``, k == k[t], so it returns (a, b, c, 1) in + this case. + + This constitutes step 2 of the outline given in the rde.py docstring. + """ + # TODO: finish writing this and write tests + + if case == 'auto': + case = DE.case + + if case == 'exp': + p = Poly(DE.t, DE.t) + elif case == 'tan': + p = Poly(DE.t**2 + 1, DE.t) + elif case in ('primitive', 'base'): + B = ba.to_field().quo(bd) + C = ca.to_field().quo(cd) + return (a, B, C, Poly(1, DE.t)) + else: + raise ValueError("case must be one of {'exp', 'tan', 'primitive', " + "'base'}, not %s." % case) + + nb = order_at(ba, p, DE.t) - order_at(bd, p, DE.t) + nc = order_at(ca, p, DE.t) - order_at(cd, p, DE.t) + + n = min(0, nc - min(0, nb)) + if not nb: + # Possible cancellation. + from .prde import parametric_log_deriv + if case == 'exp': + dcoeff = DE.d.quo(Poly(DE.t, DE.t)) + with DecrementLevel(DE): # We are guaranteed to not have problems, + # because case != 'base'. + alphaa, alphad = frac_in(-ba.eval(0)/bd.eval(0)/a.eval(0), DE.t) + etaa, etad = frac_in(dcoeff, DE.t) + A = parametric_log_deriv(alphaa, alphad, etaa, etad, DE) + if A is not None: + Q, m, z = A + if Q == 1: + n = min(n, m) + + elif case == 'tan': + dcoeff = DE.d.quo(Poly(DE.t**2+1, DE.t)) + with DecrementLevel(DE): # We are guaranteed to not have problems, + # because case != 'base'. + alphaa, alphad = frac_in(im(-ba.eval(sqrt(-1))/bd.eval(sqrt(-1))/a.eval(sqrt(-1))), DE.t) + betaa, betad = frac_in(re(-ba.eval(sqrt(-1))/bd.eval(sqrt(-1))/a.eval(sqrt(-1))), DE.t) + etaa, etad = frac_in(dcoeff, DE.t) + + if recognize_log_derivative(Poly(2, DE.t)*betaa, betad, DE): + A = parametric_log_deriv(alphaa*Poly(sqrt(-1), DE.t)*betad+alphad*betaa, alphad*betad, etaa, etad, DE) + if A is not None: + Q, m, z = A + if Q == 1: + n = min(n, m) + N = max(0, -nb, n - nc) + pN = p**N + pn = p**-n + + A = a*pN + B = ba*pN.quo(bd) + Poly(n, DE.t)*a*derivation(p, DE).quo(p)*pN + C = (ca*pN*pn).quo(cd) + h = pn + + # (a*p**N, (b + n*a*Dp/p)*p**N, c*p**(N - n), p**-n) + return (A, B, C, h) + + +def bound_degree(a, b, cQ, DE, case='auto', parametric=False): + """ + Bound on polynomial solutions. + + Explanation + =========== + + Given a derivation D on k[t] and ``a``, ``b``, ``c`` in k[t] with ``a != 0``, return + n in ZZ such that deg(q) <= n for any solution q in k[t] of + a*Dq + b*q == c, when parametric=False, or deg(q) <= n for any solution + c1, ..., cm in Const(k) and q in k[t] of a*Dq + b*q == Sum(ci*gi, (i, 1, m)) + when parametric=True. + + For ``parametric=False``, ``cQ`` is ``c``, a ``Poly``; for ``parametric=True``, ``cQ`` is Q == + [q1, ..., qm], a list of Polys. + + This constitutes step 3 of the outline given in the rde.py docstring. + """ + # TODO: finish writing this and write tests + + if case == 'auto': + case = DE.case + + da = a.degree(DE.t) + db = b.degree(DE.t) + + # The parametric and regular cases are identical, except for this part + if parametric: + dc = max(i.degree(DE.t) for i in cQ) + else: + dc = cQ.degree(DE.t) + + alpha = cancel(-b.as_poly(DE.t).LC().as_expr()/ + a.as_poly(DE.t).LC().as_expr()) + + if case == 'base': + n = max(0, dc - max(db, da - 1)) + if db == da - 1 and alpha.is_Integer: + n = max(0, alpha, dc - db) + + elif case == 'primitive': + if db > da: + n = max(0, dc - db) + else: + n = max(0, dc - da + 1) + + etaa, etad = frac_in(DE.d, DE.T[DE.level - 1]) + + t1 = DE.t + with DecrementLevel(DE): + alphaa, alphad = frac_in(alpha, DE.t) + if db == da - 1: + from .prde import limited_integrate + # if alpha == m*Dt + Dz for z in k and m in ZZ: + try: + (za, zd), m = limited_integrate(alphaa, alphad, [(etaa, etad)], + DE) + except NonElementaryIntegralException: + pass + else: + if len(m) != 1: + raise ValueError("Length of m should be 1") + n = max(n, m[0]) + + elif db == da: + # if alpha == Dz/z for z in k*: + # beta = -lc(a*Dz + b*z)/(z*lc(a)) + # if beta == m*Dt + Dw for w in k and m in ZZ: + # n = max(n, m) + from .prde import is_log_deriv_k_t_radical_in_field + A = is_log_deriv_k_t_radical_in_field(alphaa, alphad, DE) + if A is not None: + aa, z = A + if aa == 1: + beta = -(a*derivation(z, DE).as_poly(t1) + + b*z.as_poly(t1)).LC()/(z.as_expr()*a.LC()) + betaa, betad = frac_in(beta, DE.t) + from .prde import limited_integrate + try: + (za, zd), m = limited_integrate(betaa, betad, + [(etaa, etad)], DE) + except NonElementaryIntegralException: + pass + else: + if len(m) != 1: + raise ValueError("Length of m should be 1") + n = max(n, m[0].as_expr()) + + elif case == 'exp': + from .prde import parametric_log_deriv + + n = max(0, dc - max(db, da)) + if da == db: + etaa, etad = frac_in(DE.d.quo(Poly(DE.t, DE.t)), DE.T[DE.level - 1]) + with DecrementLevel(DE): + alphaa, alphad = frac_in(alpha, DE.t) + A = parametric_log_deriv(alphaa, alphad, etaa, etad, DE) + if A is not None: + # if alpha == m*Dt/t + Dz/z for z in k* and m in ZZ: + # n = max(n, m) + a, m, z = A + if a == 1: + n = max(n, m) + + elif case in ('tan', 'other_nonlinear'): + delta = DE.d.degree(DE.t) + lam = DE.d.LC() + alpha = cancel(alpha/lam) + n = max(0, dc - max(da + delta - 1, db)) + if db == da + delta - 1 and alpha.is_Integer: + n = max(0, alpha, dc - db) + + else: + raise ValueError("case must be one of {'exp', 'tan', 'primitive', " + "'other_nonlinear', 'base'}, not %s." % case) + + return n + + +def spde(a, b, c, n, DE): + """ + Rothstein's Special Polynomial Differential Equation algorithm. + + Explanation + =========== + + Given a derivation D on k[t], an integer n and ``a``,``b``,``c`` in k[t] with + ``a != 0``, either raise NonElementaryIntegralException, in which case the + equation a*Dq + b*q == c has no solution of degree at most ``n`` in + k[t], or return the tuple (B, C, m, alpha, beta) such that B, C, + alpha, beta in k[t], m in ZZ, and any solution q in k[t] of degree + at most n of a*Dq + b*q == c must be of the form + q == alpha*h + beta, where h in k[t], deg(h) <= m, and Dh + B*h == C. + + This constitutes step 4 of the outline given in the rde.py docstring. + """ + zero = Poly(0, DE.t) + + alpha = Poly(1, DE.t) + beta = Poly(0, DE.t) + + while True: + if c.is_zero: + return (zero, zero, 0, zero, beta) # -1 is more to the point + if (n < 0) is True: + raise NonElementaryIntegralException + + g = a.gcd(b) + if not c.rem(g).is_zero: # g does not divide c + raise NonElementaryIntegralException + + a, b, c = a.quo(g), b.quo(g), c.quo(g) + + if a.degree(DE.t) == 0: + b = b.to_field().quo(a) + c = c.to_field().quo(a) + return (b, c, n, alpha, beta) + + r, z = gcdex_diophantine(b, a, c) + b += derivation(a, DE) + c = z - derivation(r, DE) + n -= a.degree(DE.t) + + beta += alpha * r + alpha *= a + +def no_cancel_b_large(b, c, n, DE): + """ + Poly Risch Differential Equation - No cancellation: deg(b) large enough. + + Explanation + =========== + + Given a derivation D on k[t], ``n`` either an integer or +oo, and ``b``,``c`` + in k[t] with ``b != 0`` and either D == d/dt or + deg(b) > max(0, deg(D) - 1), either raise NonElementaryIntegralException, in + which case the equation ``Dq + b*q == c`` has no solution of degree at + most n in k[t], or a solution q in k[t] of this equation with + ``deg(q) < n``. + """ + q = Poly(0, DE.t) + + while not c.is_zero: + m = c.degree(DE.t) - b.degree(DE.t) + if not 0 <= m <= n: # n < 0 or m < 0 or m > n + raise NonElementaryIntegralException + + p = Poly(c.as_poly(DE.t).LC()/b.as_poly(DE.t).LC()*DE.t**m, DE.t, + expand=False) + q = q + p + n = m - 1 + c = c - derivation(p, DE) - b*p + + return q + + +def no_cancel_b_small(b, c, n, DE): + """ + Poly Risch Differential Equation - No cancellation: deg(b) small enough. + + Explanation + =========== + + Given a derivation D on k[t], ``n`` either an integer or +oo, and ``b``,``c`` + in k[t] with deg(b) < deg(D) - 1 and either D == d/dt or + deg(D) >= 2, either raise NonElementaryIntegralException, in which case the + equation Dq + b*q == c has no solution of degree at most n in k[t], + or a solution q in k[t] of this equation with deg(q) <= n, or the + tuple (h, b0, c0) such that h in k[t], b0, c0, in k, and for any + solution q in k[t] of degree at most n of Dq + bq == c, y == q - h + is a solution in k of Dy + b0*y == c0. + """ + q = Poly(0, DE.t) + + while not c.is_zero: + if n == 0: + m = 0 + else: + m = c.degree(DE.t) - DE.d.degree(DE.t) + 1 + + if not 0 <= m <= n: # n < 0 or m < 0 or m > n + raise NonElementaryIntegralException + + if m > 0: + p = Poly(c.as_poly(DE.t).LC()/(m*DE.d.as_poly(DE.t).LC())*DE.t**m, + DE.t, expand=False) + else: + if b.degree(DE.t) != c.degree(DE.t): + raise NonElementaryIntegralException + if b.degree(DE.t) == 0: + return (q, b.as_poly(DE.T[DE.level - 1]), + c.as_poly(DE.T[DE.level - 1])) + p = Poly(c.as_poly(DE.t).LC()/b.as_poly(DE.t).LC(), DE.t, + expand=False) + + q = q + p + n = m - 1 + c = c - derivation(p, DE) - b*p + + return q + + +# TODO: better name for this function +def no_cancel_equal(b, c, n, DE): + """ + Poly Risch Differential Equation - No cancellation: deg(b) == deg(D) - 1 + + Explanation + =========== + + Given a derivation D on k[t] with deg(D) >= 2, n either an integer + or +oo, and b, c in k[t] with deg(b) == deg(D) - 1, either raise + NonElementaryIntegralException, in which case the equation Dq + b*q == c has + no solution of degree at most n in k[t], or a solution q in k[t] of + this equation with deg(q) <= n, or the tuple (h, m, C) such that h + in k[t], m in ZZ, and C in k[t], and for any solution q in k[t] of + degree at most n of Dq + b*q == c, y == q - h is a solution in k[t] + of degree at most m of Dy + b*y == C. + """ + q = Poly(0, DE.t) + lc = cancel(-b.as_poly(DE.t).LC()/DE.d.as_poly(DE.t).LC()) + if lc.is_Integer and lc.is_positive: + M = lc + else: + M = -1 + + while not c.is_zero: + m = max(M, c.degree(DE.t) - DE.d.degree(DE.t) + 1) + + if not 0 <= m <= n: # n < 0 or m < 0 or m > n + raise NonElementaryIntegralException + + u = cancel(m*DE.d.as_poly(DE.t).LC() + b.as_poly(DE.t).LC()) + if u.is_zero: + return (q, m, c) + if m > 0: + p = Poly(c.as_poly(DE.t).LC()/u*DE.t**m, DE.t, expand=False) + else: + if c.degree(DE.t) != DE.d.degree(DE.t) - 1: + raise NonElementaryIntegralException + else: + p = c.as_poly(DE.t).LC()/b.as_poly(DE.t).LC() + + q = q + p + n = m - 1 + c = c - derivation(p, DE) - b*p + + return q + + +def cancel_primitive(b, c, n, DE): + """ + Poly Risch Differential Equation - Cancellation: Primitive case. + + Explanation + =========== + + Given a derivation D on k[t], n either an integer or +oo, ``b`` in k, and + ``c`` in k[t] with Dt in k and ``b != 0``, either raise + NonElementaryIntegralException, in which case the equation Dq + b*q == c + has no solution of degree at most n in k[t], or a solution q in k[t] of + this equation with deg(q) <= n. + """ + # Delayed imports + from .prde import is_log_deriv_k_t_radical_in_field + with DecrementLevel(DE): + ba, bd = frac_in(b, DE.t) + A = is_log_deriv_k_t_radical_in_field(ba, bd, DE) + if A is not None: + n, z = A + if n == 1: # b == Dz/z + raise NotImplementedError("is_deriv_in_field() is required to " + " solve this problem.") + # if z*c == Dp for p in k[t] and deg(p) <= n: + # return p/z + # else: + # raise NonElementaryIntegralException + + if c.is_zero: + return c # return 0 + + if n < c.degree(DE.t): + raise NonElementaryIntegralException + + q = Poly(0, DE.t) + while not c.is_zero: + m = c.degree(DE.t) + if n < m: + raise NonElementaryIntegralException + with DecrementLevel(DE): + a2a, a2d = frac_in(c.LC(), DE.t) + sa, sd = rischDE(ba, bd, a2a, a2d, DE) + stm = Poly(sa.as_expr()/sd.as_expr()*DE.t**m, DE.t, expand=False) + q += stm + n = m - 1 + c -= b*stm + derivation(stm, DE) + + return q + + +def cancel_exp(b, c, n, DE): + """ + Poly Risch Differential Equation - Cancellation: Hyperexponential case. + + Explanation + =========== + + Given a derivation D on k[t], n either an integer or +oo, ``b`` in k, and + ``c`` in k[t] with Dt/t in k and ``b != 0``, either raise + NonElementaryIntegralException, in which case the equation Dq + b*q == c + has no solution of degree at most n in k[t], or a solution q in k[t] of + this equation with deg(q) <= n. + """ + from .prde import parametric_log_deriv + eta = DE.d.quo(Poly(DE.t, DE.t)).as_expr() + + with DecrementLevel(DE): + etaa, etad = frac_in(eta, DE.t) + ba, bd = frac_in(b, DE.t) + A = parametric_log_deriv(ba, bd, etaa, etad, DE) + if A is not None: + a, m, z = A + if a == 1: + raise NotImplementedError("is_deriv_in_field() is required to " + "solve this problem.") + # if c*z*t**m == Dp for p in k and q = p/(z*t**m) in k[t] and + # deg(q) <= n: + # return q + # else: + # raise NonElementaryIntegralException + + if c.is_zero: + return c # return 0 + + if n < c.degree(DE.t): + raise NonElementaryIntegralException + + q = Poly(0, DE.t) + while not c.is_zero: + m = c.degree(DE.t) + if n < m: + raise NonElementaryIntegralException + # a1 = b + m*Dt/t + a1 = b.as_expr() + with DecrementLevel(DE): + # TODO: Write a dummy function that does this idiom + a1a, a1d = frac_in(a1, DE.t) + a1a = a1a*etad + etaa*a1d*Poly(m, DE.t) + a1d = a1d*etad + + a2a, a2d = frac_in(c.LC(), DE.t) + + sa, sd = rischDE(a1a, a1d, a2a, a2d, DE) + stm = Poly(sa.as_expr()/sd.as_expr()*DE.t**m, DE.t, expand=False) + q += stm + n = m - 1 + c -= b*stm + derivation(stm, DE) # deg(c) becomes smaller + return q + + +def solve_poly_rde(b, cQ, n, DE, parametric=False): + """ + Solve a Polynomial Risch Differential Equation with degree bound ``n``. + + This constitutes step 4 of the outline given in the rde.py docstring. + + For parametric=False, cQ is c, a Poly; for parametric=True, cQ is Q == + [q1, ..., qm], a list of Polys. + """ + # No cancellation + if not b.is_zero and (DE.case == 'base' or + b.degree(DE.t) > max(0, DE.d.degree(DE.t) - 1)): + + if parametric: + # Delayed imports + from .prde import prde_no_cancel_b_large + return prde_no_cancel_b_large(b, cQ, n, DE) + return no_cancel_b_large(b, cQ, n, DE) + + elif (b.is_zero or b.degree(DE.t) < DE.d.degree(DE.t) - 1) and \ + (DE.case == 'base' or DE.d.degree(DE.t) >= 2): + + if parametric: + from .prde import prde_no_cancel_b_small + return prde_no_cancel_b_small(b, cQ, n, DE) + + R = no_cancel_b_small(b, cQ, n, DE) + + if isinstance(R, Poly): + return R + else: + # XXX: Might k be a field? (pg. 209) + h, b0, c0 = R + with DecrementLevel(DE): + b0, c0 = b0.as_poly(DE.t), c0.as_poly(DE.t) + if b0 is None: # See above comment + raise ValueError("b0 should be a non-Null value") + if c0 is None: + raise ValueError("c0 should be a non-Null value") + y = solve_poly_rde(b0, c0, n, DE).as_poly(DE.t) + return h + y + + elif DE.d.degree(DE.t) >= 2 and b.degree(DE.t) == DE.d.degree(DE.t) - 1 and \ + n > -b.as_poly(DE.t).LC()/DE.d.as_poly(DE.t).LC(): + + # TODO: Is this check necessary, and if so, what should it do if it fails? + # b comes from the first element returned from spde() + if not b.as_poly(DE.t).LC().is_number: + raise TypeError("Result should be a number") + + if parametric: + raise NotImplementedError("prde_no_cancel_b_equal() is not yet " + "implemented.") + + R = no_cancel_equal(b, cQ, n, DE) + + if isinstance(R, Poly): + return R + else: + h, m, C = R + # XXX: Or should it be rischDE()? + y = solve_poly_rde(b, C, m, DE) + return h + y + + else: + # Cancellation + if b.is_zero: + raise NotImplementedError("Remaining cases for Poly (P)RDE are " + "not yet implemented (is_deriv_in_field() required).") + else: + if DE.case == 'exp': + if parametric: + raise NotImplementedError("Parametric RDE cancellation " + "hyperexponential case is not yet implemented.") + return cancel_exp(b, cQ, n, DE) + + elif DE.case == 'primitive': + if parametric: + raise NotImplementedError("Parametric RDE cancellation " + "primitive case is not yet implemented.") + return cancel_primitive(b, cQ, n, DE) + + else: + raise NotImplementedError("Other Poly (P)RDE cancellation " + "cases are not yet implemented (%s)." % DE.case) + + if parametric: + raise NotImplementedError("Remaining cases for Poly PRDE not yet " + "implemented.") + raise NotImplementedError("Remaining cases for Poly RDE not yet " + "implemented.") + + +def rischDE(fa, fd, ga, gd, DE): + """ + Solve a Risch Differential Equation: Dy + f*y == g. + + Explanation + =========== + + See the outline in the docstring of rde.py for more information + about the procedure used. Either raise NonElementaryIntegralException, in + which case there is no solution y in the given differential field, + or return y in k(t) satisfying Dy + f*y == g, or raise + NotImplementedError, in which case, the algorithms necessary to + solve the given Risch Differential Equation have not yet been + implemented. + """ + _, (fa, fd) = weak_normalizer(fa, fd, DE) + a, (ba, bd), (ca, cd), hn = normal_denom(fa, fd, ga, gd, DE) + A, B, C, hs = special_denom(a, ba, bd, ca, cd, DE) + try: + # Until this is fully implemented, use oo. Note that this will almost + # certainly cause non-termination in spde() (unless A == 1), and + # *might* lead to non-termination in the next step for a nonelementary + # integral (I don't know for certain yet). Fortunately, spde() is + # currently written recursively, so this will just give + # RuntimeError: maximum recursion depth exceeded. + n = bound_degree(A, B, C, DE) + except NotImplementedError: + # Useful for debugging: + # import warnings + # warnings.warn("rischDE: Proceeding with n = oo; may cause " + # "non-termination.") + n = oo + + B, C, m, alpha, beta = spde(A, B, C, n, DE) + if C.is_zero: + y = C + else: + y = solve_poly_rde(B, C, m, DE) + + return (alpha*y + beta, hn*hs) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/risch.py b/venv/lib/python3.11/site-packages/sympy/integrals/risch.py new file mode 100644 index 0000000000000000000000000000000000000000..89e5f10bbb1d011d98a5884ce74ab25b615e1c51 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/risch.py @@ -0,0 +1,1851 @@ +""" +The Risch Algorithm for transcendental function integration. + +The core algorithms for the Risch algorithm are here. The subproblem +algorithms are in the rde.py and prde.py files for the Risch +Differential Equation solver and the parametric problems solvers, +respectively. All important information concerning the differential extension +for an integrand is stored in a DifferentialExtension object, which in the code +is usually called DE. Throughout the code and Inside the DifferentialExtension +object, the conventions/attribute names are that the base domain is QQ and each +differential extension is x, t0, t1, ..., tn-1 = DE.t. DE.x is the variable of +integration (Dx == 1), DE.D is a list of the derivatives of +x, t1, t2, ..., tn-1 = t, DE.T is the list [x, t1, t2, ..., tn-1], DE.t is the +outer-most variable of the differential extension at the given level (the level +can be adjusted using DE.increment_level() and DE.decrement_level()), +k is the field C(x, t0, ..., tn-2), where C is the constant field. The +numerator of a fraction is denoted by a and the denominator by +d. If the fraction is named f, fa == numer(f) and fd == denom(f). +Fractions are returned as tuples (fa, fd). DE.d and DE.t are used to +represent the topmost derivation and extension variable, respectively. +The docstring of a function signifies whether an argument is in k[t], in +which case it will just return a Poly in t, or in k(t), in which case it +will return the fraction (fa, fd). Other variable names probably come +from the names used in Bronstein's book. +""" +from types import GeneratorType +from functools import reduce + +from sympy.core.function import Lambda +from sympy.core.mul import Mul +from sympy.core.intfunc import ilcm +from sympy.core.numbers import I +from sympy.core.power import Pow +from sympy.core.relational import Ne +from sympy.core.singleton import S +from sympy.core.sorting import ordered, default_sort_key +from sympy.core.symbol import Dummy, Symbol +from sympy.functions.elementary.exponential import log, exp +from sympy.functions.elementary.hyperbolic import (cosh, coth, sinh, + tanh) +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (atan, sin, cos, + tan, acot, cot, asin, acos) +from .integrals import integrate, Integral +from .heurisch import _symbols +from sympy.polys.polyerrors import PolynomialError +from sympy.polys.polytools import (real_roots, cancel, Poly, gcd, + reduced) +from sympy.polys.rootoftools import RootSum +from sympy.utilities.iterables import numbered_symbols + + +def integer_powers(exprs): + """ + Rewrites a list of expressions as integer multiples of each other. + + Explanation + =========== + + For example, if you have [x, x/2, x**2 + 1, 2*x/3], then you can rewrite + this as [(x/6) * 6, (x/6) * 3, (x**2 + 1) * 1, (x/6) * 4]. This is useful + in the Risch integration algorithm, where we must write exp(x) + exp(x/2) + as (exp(x/2))**2 + exp(x/2), but not as exp(x) + sqrt(exp(x)) (this is + because only the transcendental case is implemented and we therefore cannot + integrate algebraic extensions). The integer multiples returned by this + function for each term are the smallest possible (their content equals 1). + + Returns a list of tuples where the first element is the base term and the + second element is a list of `(item, factor)` terms, where `factor` is the + integer multiplicative factor that must multiply the base term to obtain + the original item. + + The easiest way to understand this is to look at an example: + + >>> from sympy.abc import x + >>> from sympy.integrals.risch import integer_powers + >>> integer_powers([x, x/2, x**2 + 1, 2*x/3]) + [(x/6, [(x, 6), (x/2, 3), (2*x/3, 4)]), (x**2 + 1, [(x**2 + 1, 1)])] + + We can see how this relates to the example at the beginning of the + docstring. It chose x/6 as the first base term. Then, x can be written as + (x/2) * 2, so we get (0, 2), and so on. Now only element (x**2 + 1) + remains, and there are no other terms that can be written as a rational + multiple of that, so we get that it can be written as (x**2 + 1) * 1. + + """ + # Here is the strategy: + + # First, go through each term and determine if it can be rewritten as a + # rational multiple of any of the terms gathered so far. + # cancel(a/b).is_Rational is sufficient for this. If it is a multiple, we + # add its multiple to the dictionary. + + terms = {} + for term in exprs: + for trm, trm_list in terms.items(): + a = cancel(term/trm) + if a.is_Rational: + trm_list.append((term, a)) + break + else: + terms[term] = [(term, S.One)] + + # After we have done this, we have all the like terms together, so we just + # need to find a common denominator so that we can get the base term and + # integer multiples such that each term can be written as an integer + # multiple of the base term, and the content of the integers is 1. + + newterms = {} + for term, term_list in terms.items(): + common_denom = reduce(ilcm, [i.as_numer_denom()[1] for _, i in + term_list]) + newterm = term/common_denom + newmults = [(i, j*common_denom) for i, j in term_list] + newterms[newterm] = newmults + + return sorted(iter(newterms.items()), key=lambda item: item[0].sort_key()) + + +class DifferentialExtension: + """ + A container for all the information relating to a differential extension. + + Explanation + =========== + + The attributes of this object are (see also the docstring of __init__): + + - f: The original (Expr) integrand. + - x: The variable of integration. + - T: List of variables in the extension. + - D: List of derivations in the extension; corresponds to the elements of T. + - fa: Poly of the numerator of the integrand. + - fd: Poly of the denominator of the integrand. + - Tfuncs: Lambda() representations of each element of T (except for x). + For back-substitution after integration. + - backsubs: A (possibly empty) list of further substitutions to be made on + the final integral to make it look more like the integrand. + - exts: + - extargs: + - cases: List of string representations of the cases of T. + - t: The top level extension variable, as defined by the current level + (see level below). + - d: The top level extension derivation, as defined by the current + derivation (see level below). + - case: The string representation of the case of self.d. + (Note that self.T and self.D will always contain the complete extension, + regardless of the level. Therefore, you should ALWAYS use DE.t and DE.d + instead of DE.T[-1] and DE.D[-1]. If you want to have a list of the + derivations or variables only up to the current level, use + DE.D[:len(DE.D) + DE.level + 1] and DE.T[:len(DE.T) + DE.level + 1]. Note + that, in particular, the derivation() function does this.) + + The following are also attributes, but will probably not be useful other + than in internal use: + - newf: Expr form of fa/fd. + - level: The number (between -1 and -len(self.T)) such that + self.T[self.level] == self.t and self.D[self.level] == self.d. + Use the methods self.increment_level() and self.decrement_level() to change + the current level. + """ + # __slots__ is defined mainly so we can iterate over all the attributes + # of the class easily (the memory use doesn't matter too much, since we + # only create one DifferentialExtension per integration). Also, it's nice + # to have a safeguard when debugging. + __slots__ = ('f', 'x', 'T', 'D', 'fa', 'fd', 'Tfuncs', 'backsubs', + 'exts', 'extargs', 'cases', 'case', 't', 'd', 'newf', 'level', + 'ts', 'dummy') + + def __init__(self, f=None, x=None, handle_first='log', dummy=False, extension=None, rewrite_complex=None): + """ + Tries to build a transcendental extension tower from ``f`` with respect to ``x``. + + Explanation + =========== + + If it is successful, creates a DifferentialExtension object with, among + others, the attributes fa, fd, D, T, Tfuncs, and backsubs such that + fa and fd are Polys in T[-1] with rational coefficients in T[:-1], + fa/fd == f, and D[i] is a Poly in T[i] with rational coefficients in + T[:i] representing the derivative of T[i] for each i from 1 to len(T). + Tfuncs is a list of Lambda objects for back replacing the functions + after integrating. Lambda() is only used (instead of lambda) to make + them easier to test and debug. Note that Tfuncs corresponds to the + elements of T, except for T[0] == x, but they should be back-substituted + in reverse order. backsubs is a (possibly empty) back-substitution list + that should be applied on the completed integral to make it look more + like the original integrand. + + If it is unsuccessful, it raises NotImplementedError. + + You can also create an object by manually setting the attributes as a + dictionary to the extension keyword argument. You must include at least + D. Warning, any attribute that is not given will be set to None. The + attributes T, t, d, cases, case, x, and level are set automatically and + do not need to be given. The functions in the Risch Algorithm will NOT + check to see if an attribute is None before using it. This also does not + check to see if the extension is valid (non-algebraic) or even if it is + self-consistent. Therefore, this should only be used for + testing/debugging purposes. + """ + # XXX: If you need to debug this function, set the break point here + + if extension: + if 'D' not in extension: + raise ValueError("At least the key D must be included with " + "the extension flag to DifferentialExtension.") + for attr in extension: + setattr(self, attr, extension[attr]) + + self._auto_attrs() + + return + elif f is None or x is None: + raise ValueError("Either both f and x or a manual extension must " + "be given.") + + if handle_first not in ('log', 'exp'): + raise ValueError("handle_first must be 'log' or 'exp', not %s." % + str(handle_first)) + + # f will be the original function, self.f might change if we reset + # (e.g., we pull out a constant from an exponential) + self.f = f + self.x = x + # setting the default value 'dummy' + self.dummy = dummy + self.reset() + exp_new_extension, log_new_extension = True, True + + # case of 'automatic' choosing + if rewrite_complex is None: + rewrite_complex = I in self.f.atoms() + + if rewrite_complex: + rewritables = { + (sin, cos, cot, tan, sinh, cosh, coth, tanh): exp, + (asin, acos, acot, atan): log, + } + # rewrite the trigonometric components + for candidates, rule in rewritables.items(): + self.newf = self.newf.rewrite(candidates, rule) + self.newf = cancel(self.newf) + else: + if any(i.has(x) for i in self.f.atoms(sin, cos, tan, atan, asin, acos)): + raise NotImplementedError("Trigonometric extensions are not " + "supported (yet!)") + + exps = set() + pows = set() + numpows = set() + sympows = set() + logs = set() + symlogs = set() + + while True: + if self.newf.is_rational_function(*self.T): + break + + if not exp_new_extension and not log_new_extension: + # We couldn't find a new extension on the last pass, so I guess + # we can't do it. + raise NotImplementedError("Couldn't find an elementary " + "transcendental extension for %s. Try using a " % str(f) + + "manual extension with the extension flag.") + + exps, pows, numpows, sympows, log_new_extension = \ + self._rewrite_exps_pows(exps, pows, numpows, sympows, log_new_extension) + + logs, symlogs = self._rewrite_logs(logs, symlogs) + + if handle_first == 'exp' or not log_new_extension: + exp_new_extension = self._exp_part(exps) + if exp_new_extension is None: + # reset and restart + self.f = self.newf + self.reset() + exp_new_extension = True + continue + + if handle_first == 'log' or not exp_new_extension: + log_new_extension = self._log_part(logs) + + self.fa, self.fd = frac_in(self.newf, self.t) + self._auto_attrs() + + return + + def __getattr__(self, attr): + # Avoid AttributeErrors when debugging + if attr not in self.__slots__: + raise AttributeError("%s has no attribute %s" % (repr(self), repr(attr))) + return None + + def _rewrite_exps_pows(self, exps, pows, numpows, + sympows, log_new_extension): + """ + Rewrite exps/pows for better processing. + """ + from .prde import is_deriv_k + + # Pre-preparsing. + ################# + # Get all exp arguments, so we can avoid ahead of time doing + # something like t1 = exp(x), t2 = exp(x/2) == sqrt(t1). + + # Things like sqrt(exp(x)) do not automatically simplify to + # exp(x/2), so they will be viewed as algebraic. The easiest way + # to handle this is to convert all instances of exp(a)**Rational + # to exp(Rational*a) before doing anything else. Note that the + # _exp_part code can generate terms of this form, so we do need to + # do this at each pass (or else modify it to not do that). + + ratpows = [i for i in self.newf.atoms(Pow) + if (isinstance(i.base, exp) and i.exp.is_Rational)] + + ratpows_repl = [ + (i, i.base.base**(i.exp*i.base.exp)) for i in ratpows] + self.backsubs += [(j, i) for i, j in ratpows_repl] + self.newf = self.newf.xreplace(dict(ratpows_repl)) + + # To make the process deterministic, the args are sorted + # so that functions with smaller op-counts are processed first. + # Ties are broken with the default_sort_key. + + # XXX Although the method is deterministic no additional work + # has been done to guarantee that the simplest solution is + # returned and that it would be affected be using different + # variables. Though it is possible that this is the case + # one should know that it has not been done intentionally, so + # further improvements may be possible. + + # TODO: This probably doesn't need to be completely recomputed at + # each pass. + exps = update_sets(exps, self.newf.atoms(exp), + lambda i: i.exp.is_rational_function(*self.T) and + i.exp.has(*self.T)) + pows = update_sets(pows, self.newf.atoms(Pow), + lambda i: i.exp.is_rational_function(*self.T) and + i.exp.has(*self.T)) + numpows = update_sets(numpows, set(pows), + lambda i: not i.base.has(*self.T)) + sympows = update_sets(sympows, set(pows) - set(numpows), + lambda i: i.base.is_rational_function(*self.T) and + not i.exp.is_Integer) + + # The easiest way to deal with non-base E powers is to convert them + # into base E, integrate, and then convert back. + for i in ordered(pows): + old = i + new = exp(i.exp*log(i.base)) + # If exp is ever changed to automatically reduce exp(x*log(2)) + # to 2**x, then this will break. The solution is to not change + # exp to do that :) + if i in sympows: + if i.exp.is_Rational: + raise NotImplementedError("Algebraic extensions are " + "not supported (%s)." % str(i)) + # We can add a**b only if log(a) in the extension, because + # a**b == exp(b*log(a)). + basea, based = frac_in(i.base, self.t) + A = is_deriv_k(basea, based, self) + if A is None: + # Nonelementary monomial (so far) + + # TODO: Would there ever be any benefit from just + # adding log(base) as a new monomial? + # ANSWER: Yes, otherwise we can't integrate x**x (or + # rather prove that it has no elementary integral) + # without first manually rewriting it as exp(x*log(x)) + self.newf = self.newf.xreplace({old: new}) + self.backsubs += [(new, old)] + log_new_extension = self._log_part([log(i.base)]) + exps = update_sets(exps, self.newf.atoms(exp), lambda i: + i.exp.is_rational_function(*self.T) and i.exp.has(*self.T)) + continue + ans, u, const = A + newterm = exp(i.exp*(log(const) + u)) + # Under the current implementation, exp kills terms + # only if they are of the form a*log(x), where a is a + # Number. This case should have already been killed by the + # above tests. Again, if this changes to kill more than + # that, this will break, which maybe is a sign that you + # shouldn't be changing that. Actually, if anything, this + # auto-simplification should be removed. See + # https://groups.google.com/group/sympy/browse_thread/thread/a61d48235f16867f + + self.newf = self.newf.xreplace({i: newterm}) + + elif i not in numpows: + continue + else: + # i in numpows + newterm = new + # TODO: Just put it in self.Tfuncs + self.backsubs.append((new, old)) + self.newf = self.newf.xreplace({old: newterm}) + exps.append(newterm) + + return exps, pows, numpows, sympows, log_new_extension + + def _rewrite_logs(self, logs, symlogs): + """ + Rewrite logs for better processing. + """ + atoms = self.newf.atoms(log) + logs = update_sets(logs, atoms, + lambda i: i.args[0].is_rational_function(*self.T) and + i.args[0].has(*self.T)) + symlogs = update_sets(symlogs, atoms, + lambda i: i.has(*self.T) and i.args[0].is_Pow and + i.args[0].base.is_rational_function(*self.T) and + not i.args[0].exp.is_Integer) + + # We can handle things like log(x**y) by converting it to y*log(x) + # This will fix not only symbolic exponents of the argument, but any + # non-Integer exponent, like log(sqrt(x)). The exponent can also + # depend on x, like log(x**x). + for i in ordered(symlogs): + # Unlike in the exponential case above, we do not ever + # potentially add new monomials (above we had to add log(a)). + # Therefore, there is no need to run any is_deriv functions + # here. Just convert log(a**b) to b*log(a) and let + # log_new_extension() handle it from there. + lbase = log(i.args[0].base) + logs.append(lbase) + new = i.args[0].exp*lbase + self.newf = self.newf.xreplace({i: new}) + self.backsubs.append((new, i)) + + # remove any duplicates + logs = sorted(set(logs), key=default_sort_key) + + return logs, symlogs + + def _auto_attrs(self): + """ + Set attributes that are generated automatically. + """ + if not self.T: + # i.e., when using the extension flag and T isn't given + self.T = [i.gen for i in self.D] + if not self.x: + self.x = self.T[0] + self.cases = [get_case(d, t) for d, t in zip(self.D, self.T)] + self.level = -1 + self.t = self.T[self.level] + self.d = self.D[self.level] + self.case = self.cases[self.level] + + def _exp_part(self, exps): + """ + Try to build an exponential extension. + + Returns + ======= + + Returns True if there was a new extension, False if there was no new + extension but it was able to rewrite the given exponentials in terms + of the existing extension, and None if the entire extension building + process should be restarted. If the process fails because there is no + way around an algebraic extension (e.g., exp(log(x)/2)), it will raise + NotImplementedError. + """ + from .prde import is_log_deriv_k_t_radical + new_extension = False + restart = False + expargs = [i.exp for i in exps] + ip = integer_powers(expargs) + for arg, others in ip: + # Minimize potential problems with algebraic substitution + others.sort(key=lambda i: i[1]) + + arga, argd = frac_in(arg, self.t) + A = is_log_deriv_k_t_radical(arga, argd, self) + + if A is not None: + ans, u, n, const = A + # if n is 1 or -1, it's algebraic, but we can handle it + if n == -1: + # This probably will never happen, because + # Rational.as_numer_denom() returns the negative term in + # the numerator. But in case that changes, reduce it to + # n == 1. + n = 1 + u **= -1 + const *= -1 + ans = [(i, -j) for i, j in ans] + + if n == 1: + # Example: exp(x + x**2) over QQ(x, exp(x), exp(x**2)) + self.newf = self.newf.xreplace({exp(arg): exp(const)*Mul(*[ + u**power for u, power in ans])}) + self.newf = self.newf.xreplace({exp(p*exparg): + exp(const*p) * Mul(*[u**power for u, power in ans]) + for exparg, p in others}) + # TODO: Add something to backsubs to put exp(const*p) + # back together. + + continue + + else: + # Bad news: we have an algebraic radical. But maybe we + # could still avoid it by choosing a different extension. + # For example, integer_powers() won't handle exp(x/2 + 1) + # over QQ(x, exp(x)), but if we pull out the exp(1), it + # will. Or maybe we have exp(x + x**2/2), over + # QQ(x, exp(x), exp(x**2)), which is exp(x)*sqrt(exp(x**2)), + # but if we use QQ(x, exp(x), exp(x**2/2)), then they will + # all work. + # + # So here is what we do: If there is a non-zero const, pull + # it out and retry. Also, if len(ans) > 1, then rewrite + # exp(arg) as the product of exponentials from ans, and + # retry that. If const == 0 and len(ans) == 1, then we + # assume that it would have been handled by either + # integer_powers() or n == 1 above if it could be handled, + # so we give up at that point. For example, you can never + # handle exp(log(x)/2) because it equals sqrt(x). + + if const or len(ans) > 1: + rad = Mul(*[term**(power/n) for term, power in ans]) + self.newf = self.newf.xreplace({exp(p*exparg): + exp(const*p)*rad for exparg, p in others}) + self.newf = self.newf.xreplace(dict(list(zip(reversed(self.T), + reversed([f(self.x) for f in self.Tfuncs]))))) + restart = True + break + else: + # TODO: give algebraic dependence in error string + raise NotImplementedError("Cannot integrate over " + "algebraic extensions.") + + else: + arga, argd = frac_in(arg, self.t) + darga = (argd*derivation(Poly(arga, self.t), self) - + arga*derivation(Poly(argd, self.t), self)) + dargd = argd**2 + darga, dargd = darga.cancel(dargd, include=True) + darg = darga.as_expr()/dargd.as_expr() + self.t = next(self.ts) + self.T.append(self.t) + self.extargs.append(arg) + self.exts.append('exp') + self.D.append(darg.as_poly(self.t, expand=False)*Poly(self.t, + self.t, expand=False)) + if self.dummy: + i = Dummy("i") + else: + i = Symbol('i') + self.Tfuncs += [Lambda(i, exp(arg.subs(self.x, i)))] + self.newf = self.newf.xreplace( + {exp(exparg): self.t**p for exparg, p in others}) + new_extension = True + + if restart: + return None + return new_extension + + def _log_part(self, logs): + """ + Try to build a logarithmic extension. + + Returns + ======= + + Returns True if there was a new extension and False if there was no new + extension but it was able to rewrite the given logarithms in terms + of the existing extension. Unlike with exponential extensions, there + is no way that a logarithm is not transcendental over and cannot be + rewritten in terms of an already existing extension in a non-algebraic + way, so this function does not ever return None or raise + NotImplementedError. + """ + from .prde import is_deriv_k + new_extension = False + logargs = [i.args[0] for i in logs] + for arg in ordered(logargs): + # The log case is easier, because whenever a logarithm is algebraic + # over the base field, it is of the form a1*t1 + ... an*tn + c, + # which is a polynomial, so we can just replace it with that. + # In other words, we don't have to worry about radicals. + arga, argd = frac_in(arg, self.t) + A = is_deriv_k(arga, argd, self) + if A is not None: + ans, u, const = A + newterm = log(const) + u + self.newf = self.newf.xreplace({log(arg): newterm}) + continue + + else: + arga, argd = frac_in(arg, self.t) + darga = (argd*derivation(Poly(arga, self.t), self) - + arga*derivation(Poly(argd, self.t), self)) + dargd = argd**2 + darg = darga.as_expr()/dargd.as_expr() + self.t = next(self.ts) + self.T.append(self.t) + self.extargs.append(arg) + self.exts.append('log') + self.D.append(cancel(darg.as_expr()/arg).as_poly(self.t, + expand=False)) + if self.dummy: + i = Dummy("i") + else: + i = Symbol('i') + self.Tfuncs += [Lambda(i, log(arg.subs(self.x, i)))] + self.newf = self.newf.xreplace({log(arg): self.t}) + new_extension = True + + return new_extension + + @property + def _important_attrs(self): + """ + Returns some of the more important attributes of self. + + Explanation + =========== + + Used for testing and debugging purposes. + + The attributes are (fa, fd, D, T, Tfuncs, backsubs, + exts, extargs). + """ + return (self.fa, self.fd, self.D, self.T, self.Tfuncs, + self.backsubs, self.exts, self.extargs) + + # NOTE: this printing doesn't follow the Python's standard + # eval(repr(DE)) == DE, where DE is the DifferentialExtension object, + # also this printing is supposed to contain all the important + # attributes of a DifferentialExtension object + def __repr__(self): + # no need to have GeneratorType object printed in it + r = [(attr, getattr(self, attr)) for attr in self.__slots__ + if not isinstance(getattr(self, attr), GeneratorType)] + return self.__class__.__name__ + '(dict(%r))' % (r) + + # fancy printing of DifferentialExtension object + def __str__(self): + return (self.__class__.__name__ + '({fa=%s, fd=%s, D=%s})' % + (self.fa, self.fd, self.D)) + + # should only be used for debugging purposes, internally + # f1 = f2 = log(x) at different places in code execution + # may return D1 != D2 as True, since 'level' or other attribute + # may differ + def __eq__(self, other): + for attr in self.__class__.__slots__: + d1, d2 = getattr(self, attr), getattr(other, attr) + if not (isinstance(d1, GeneratorType) or d1 == d2): + return False + return True + + def reset(self): + """ + Reset self to an initial state. Used by __init__. + """ + self.t = self.x + self.T = [self.x] + self.D = [Poly(1, self.x)] + self.level = -1 + self.exts = [None] + self.extargs = [None] + if self.dummy: + self.ts = numbered_symbols('t', cls=Dummy) + else: + # For testing + self.ts = numbered_symbols('t') + # For various things that we change to make things work that we need to + # change back when we are done. + self.backsubs = [] + self.Tfuncs = [] + self.newf = self.f + + def indices(self, extension): + """ + Parameters + ========== + + extension : str + Represents a valid extension type. + + Returns + ======= + + list: A list of indices of 'exts' where extension of + type 'extension' is present. + + Examples + ======== + + >>> from sympy.integrals.risch import DifferentialExtension + >>> from sympy import log, exp + >>> from sympy.abc import x + >>> DE = DifferentialExtension(log(x) + exp(x), x, handle_first='exp') + >>> DE.indices('log') + [2] + >>> DE.indices('exp') + [1] + + """ + return [i for i, ext in enumerate(self.exts) if ext == extension] + + def increment_level(self): + """ + Increment the level of self. + + Explanation + =========== + + This makes the working differential extension larger. self.level is + given relative to the end of the list (-1, -2, etc.), so we do not need + do worry about it when building the extension. + """ + if self.level >= -1: + raise ValueError("The level of the differential extension cannot " + "be incremented any further.") + + self.level += 1 + self.t = self.T[self.level] + self.d = self.D[self.level] + self.case = self.cases[self.level] + return None + + def decrement_level(self): + """ + Decrease the level of self. + + Explanation + =========== + + This makes the working differential extension smaller. self.level is + given relative to the end of the list (-1, -2, etc.), so we do not need + do worry about it when building the extension. + """ + if self.level <= -len(self.T): + raise ValueError("The level of the differential extension cannot " + "be decremented any further.") + + self.level -= 1 + self.t = self.T[self.level] + self.d = self.D[self.level] + self.case = self.cases[self.level] + return None + + +def update_sets(seq, atoms, func): + s = set(seq) + s = atoms.intersection(s) + new = atoms - s + s.update(list(filter(func, new))) + return list(s) + + +class DecrementLevel: + """ + A context manager for decrementing the level of a DifferentialExtension. + """ + __slots__ = ('DE',) + + def __init__(self, DE): + self.DE = DE + return + + def __enter__(self): + self.DE.decrement_level() + + def __exit__(self, exc_type, exc_value, traceback): + self.DE.increment_level() + + +class NonElementaryIntegralException(Exception): + """ + Exception used by subroutines within the Risch algorithm to indicate to one + another that the function being integrated does not have an elementary + integral in the given differential field. + """ + # TODO: Rewrite algorithms below to use this (?) + + # TODO: Pass through information about why the integral was nonelementary, + # and store that in the resulting NonElementaryIntegral somehow. + pass + + +def gcdex_diophantine(a, b, c): + """ + Extended Euclidean Algorithm, Diophantine version. + + Explanation + =========== + + Given ``a``, ``b`` in K[x] and ``c`` in (a, b), the ideal generated by ``a`` and + ``b``, return (s, t) such that s*a + t*b == c and either s == 0 or s.degree() + < b.degree(). + """ + # Extended Euclidean Algorithm (Diophantine Version) pg. 13 + # TODO: This should go in densetools.py. + # XXX: Better name? + + s, g = a.half_gcdex(b) + s *= c.exquo(g) # Inexact division means c is not in (a, b) + if s and s.degree() >= b.degree(): + _, s = s.div(b) + t = (c - s*a).exquo(b) + return (s, t) + + +def frac_in(f, t, *, cancel=False, **kwargs): + """ + Returns the tuple (fa, fd), where fa and fd are Polys in t. + + Explanation + =========== + + This is a common idiom in the Risch Algorithm functions, so we abstract + it out here. ``f`` should be a basic expression, a Poly, or a tuple (fa, fd), + where fa and fd are either basic expressions or Polys, and f == fa/fd. + **kwargs are applied to Poly. + """ + if isinstance(f, tuple): + fa, fd = f + f = fa.as_expr()/fd.as_expr() + fa, fd = f.as_expr().as_numer_denom() + fa, fd = fa.as_poly(t, **kwargs), fd.as_poly(t, **kwargs) + if cancel: + fa, fd = fa.cancel(fd, include=True) + if fa is None or fd is None: + raise ValueError("Could not turn %s into a fraction in %s." % (f, t)) + return (fa, fd) + + +def as_poly_1t(p, t, z): + """ + (Hackish) way to convert an element ``p`` of K[t, 1/t] to K[t, z]. + + In other words, ``z == 1/t`` will be a dummy variable that Poly can handle + better. + + See issue 5131. + + Examples + ======== + + >>> from sympy import random_poly + >>> from sympy.integrals.risch import as_poly_1t + >>> from sympy.abc import x, z + + >>> p1 = random_poly(x, 10, -10, 10) + >>> p2 = random_poly(x, 10, -10, 10) + >>> p = p1 + p2.subs(x, 1/x) + >>> as_poly_1t(p, x, z).as_expr().subs(z, 1/x) == p + True + """ + # TODO: Use this on the final result. That way, we can avoid answers like + # (...)*exp(-x). + pa, pd = frac_in(p, t, cancel=True) + if not pd.is_monomial: + # XXX: Is there a better Poly exception that we could raise here? + # Either way, if you see this (from the Risch Algorithm) it indicates + # a bug. + raise PolynomialError("%s is not an element of K[%s, 1/%s]." % (p, t, t)) + + t_part, remainder = pa.div(pd) + + ans = t_part.as_poly(t, z, expand=False) + + if remainder: + one = remainder.one + tp = t*one + r = pd.degree() - remainder.degree() + z_part = remainder.transform(one, tp) * tp**r + z_part = z_part.replace(t, z).to_field().quo_ground(pd.LC()) + ans += z_part.as_poly(t, z, expand=False) + + return ans + + +def derivation(p, DE, coefficientD=False, basic=False): + """ + Computes Dp. + + Explanation + =========== + + Given the derivation D with D = d/dx and p is a polynomial in t over + K(x), return Dp. + + If coefficientD is True, it computes the derivation kD + (kappaD), which is defined as kD(sum(ai*Xi**i, (i, 0, n))) == + sum(Dai*Xi**i, (i, 1, n)) (Definition 3.2.2, page 80). X in this case is + T[-1], so coefficientD computes the derivative just with respect to T[:-1], + with T[-1] treated as a constant. + + If ``basic=True``, the returns a Basic expression. Elements of D can still be + instances of Poly. + """ + if basic: + r = 0 + else: + r = Poly(0, DE.t) + + t = DE.t + if coefficientD: + if DE.level <= -len(DE.T): + # 'base' case, the answer is 0. + return r + DE.decrement_level() + + D = DE.D[:len(DE.D) + DE.level + 1] + T = DE.T[:len(DE.T) + DE.level + 1] + + for d, v in zip(D, T): + pv = p.as_poly(v) + if pv is None or basic: + pv = p.as_expr() + + if basic: + r += d.as_expr()*pv.diff(v) + else: + r += (d.as_expr()*pv.diff(v).as_expr()).as_poly(t) + + if basic: + r = cancel(r) + if coefficientD: + DE.increment_level() + + return r + + +def get_case(d, t): + """ + Returns the type of the derivation d. + + Returns one of {'exp', 'tan', 'base', 'primitive', 'other_linear', + 'other_nonlinear'}. + """ + if not d.expr.has(t): + if d.is_one: + return 'base' + return 'primitive' + if d.rem(Poly(t, t)).is_zero: + return 'exp' + if d.rem(Poly(1 + t**2, t)).is_zero: + return 'tan' + if d.degree(t) > 1: + return 'other_nonlinear' + return 'other_linear' + + +def splitfactor(p, DE, coefficientD=False, z=None): + """ + Splitting factorization. + + Explanation + =========== + + Given a derivation D on k[t] and ``p`` in k[t], return (p_n, p_s) in + k[t] x k[t] such that p = p_n*p_s, p_s is special, and each square + factor of p_n is normal. + + Page. 100 + """ + kinv = [1/x for x in DE.T[:DE.level]] + if z: + kinv.append(z) + + One = Poly(1, DE.t, domain=p.get_domain()) + Dp = derivation(p, DE, coefficientD=coefficientD) + # XXX: Is this right? + if p.is_zero: + return (p, One) + + if not p.expr.has(DE.t): + s = p.as_poly(*kinv).gcd(Dp.as_poly(*kinv)).as_poly(DE.t) + n = p.exquo(s) + return (n, s) + + if not Dp.is_zero: + h = p.gcd(Dp).to_field() + g = p.gcd(p.diff(DE.t)).to_field() + s = h.exquo(g) + + if s.degree(DE.t) == 0: + return (p, One) + + q_split = splitfactor(p.exquo(s), DE, coefficientD=coefficientD) + + return (q_split[0], q_split[1]*s) + else: + return (p, One) + + +def splitfactor_sqf(p, DE, coefficientD=False, z=None, basic=False): + """ + Splitting Square-free Factorization. + + Explanation + =========== + + Given a derivation D on k[t] and ``p`` in k[t], returns (N1, ..., Nm) + and (S1, ..., Sm) in k[t]^m such that p = + (N1*N2**2*...*Nm**m)*(S1*S2**2*...*Sm**m) is a splitting + factorization of ``p`` and the Ni and Si are square-free and coprime. + """ + # TODO: This algorithm appears to be faster in every case + # TODO: Verify this and splitfactor() for multiple extensions + kkinv = [1/x for x in DE.T[:DE.level]] + DE.T[:DE.level] + if z: + kkinv = [z] + + S = [] + N = [] + p_sqf = p.sqf_list_include() + if p.is_zero: + return (((p, 1),), ()) + + for pi, i in p_sqf: + Si = pi.as_poly(*kkinv).gcd(derivation(pi, DE, + coefficientD=coefficientD,basic=basic).as_poly(*kkinv)).as_poly(DE.t) + pi = Poly(pi, DE.t) + Si = Poly(Si, DE.t) + Ni = pi.exquo(Si) + if not Si.is_one: + S.append((Si, i)) + if not Ni.is_one: + N.append((Ni, i)) + + return (tuple(N), tuple(S)) + + +def canonical_representation(a, d, DE): + """ + Canonical Representation. + + Explanation + =========== + + Given a derivation D on k[t] and f = a/d in k(t), return (f_p, f_s, + f_n) in k[t] x k(t) x k(t) such that f = f_p + f_s + f_n is the + canonical representation of f (f_p is a polynomial, f_s is reduced + (has a special denominator), and f_n is simple (has a normal + denominator). + """ + # Make d monic + l = Poly(1/d.LC(), DE.t) + a, d = a.mul(l), d.mul(l) + + q, r = a.div(d) + dn, ds = splitfactor(d, DE) + + b, c = gcdex_diophantine(dn.as_poly(DE.t), ds.as_poly(DE.t), r.as_poly(DE.t)) + b, c = b.as_poly(DE.t), c.as_poly(DE.t) + + return (q, (b, ds), (c, dn)) + + +def hermite_reduce(a, d, DE): + """ + Hermite Reduction - Mack's Linear Version. + + Given a derivation D on k(t) and f = a/d in k(t), returns g, h, r in + k(t) such that f = Dg + h + r, h is simple, and r is reduced. + + """ + # Make d monic + l = Poly(1/d.LC(), DE.t) + a, d = a.mul(l), d.mul(l) + + fp, fs, fn = canonical_representation(a, d, DE) + a, d = fn + l = Poly(1/d.LC(), DE.t) + a, d = a.mul(l), d.mul(l) + + ga = Poly(0, DE.t) + gd = Poly(1, DE.t) + + dd = derivation(d, DE) + dm = gcd(d.to_field(), dd.to_field()).as_poly(DE.t) + ds, _ = d.div(dm) + + while dm.degree(DE.t) > 0: + + ddm = derivation(dm, DE) + dm2 = gcd(dm.to_field(), ddm.to_field()) + dms, _ = dm.div(dm2) + ds_ddm = ds.mul(ddm) + ds_ddm_dm, _ = ds_ddm.div(dm) + + b, c = gcdex_diophantine(-ds_ddm_dm.as_poly(DE.t), + dms.as_poly(DE.t), a.as_poly(DE.t)) + b, c = b.as_poly(DE.t), c.as_poly(DE.t) + + db = derivation(b, DE).as_poly(DE.t) + ds_dms, _ = ds.div(dms) + a = c.as_poly(DE.t) - db.mul(ds_dms).as_poly(DE.t) + + ga = ga*dm + b*gd + gd = gd*dm + ga, gd = ga.cancel(gd, include=True) + dm = dm2 + + q, r = a.div(ds) + ga, gd = ga.cancel(gd, include=True) + + r, d = r.cancel(ds, include=True) + rra = q*fs[1] + fp*fs[1] + fs[0] + rrd = fs[1] + rra, rrd = rra.cancel(rrd, include=True) + + return ((ga, gd), (r, d), (rra, rrd)) + + +def polynomial_reduce(p, DE): + """ + Polynomial Reduction. + + Explanation + =========== + + Given a derivation D on k(t) and p in k[t] where t is a nonlinear + monomial over k, return q, r in k[t] such that p = Dq + r, and + deg(r) < deg_t(Dt). + """ + q = Poly(0, DE.t) + while p.degree(DE.t) >= DE.d.degree(DE.t): + m = p.degree(DE.t) - DE.d.degree(DE.t) + 1 + q0 = Poly(DE.t**m, DE.t).mul(Poly(p.as_poly(DE.t).LC()/ + (m*DE.d.LC()), DE.t)) + q += q0 + p = p - derivation(q0, DE) + + return (q, p) + + +def laurent_series(a, d, F, n, DE): + """ + Contribution of ``F`` to the full partial fraction decomposition of A/D. + + Explanation + =========== + + Given a field K of characteristic 0 and ``A``,``D``,``F`` in K[x] with D monic, + nonzero, coprime with A, and ``F`` the factor of multiplicity n in the square- + free factorization of D, return the principal parts of the Laurent series of + A/D at all the zeros of ``F``. + """ + if F.degree()==0: + return 0 + Z = _symbols('z', n) + z = Symbol('z') + Z.insert(0, z) + delta_a = Poly(0, DE.t) + delta_d = Poly(1, DE.t) + + E = d.quo(F**n) + ha, hd = (a, E*Poly(z**n, DE.t)) + dF = derivation(F,DE) + B, _ = gcdex_diophantine(E, F, Poly(1,DE.t)) + C, _ = gcdex_diophantine(dF, F, Poly(1,DE.t)) + + # initialization + F_store = F + V, DE_D_list, H_list= [], [], [] + + for j in range(0, n): + # jth derivative of z would be substituted with dfnth/(j+1) where dfnth =(d^n)f/(dx)^n + F_store = derivation(F_store, DE) + v = (F_store.as_expr())/(j + 1) + V.append(v) + DE_D_list.append(Poly(Z[j + 1],Z[j])) + + DE_new = DifferentialExtension(extension = {'D': DE_D_list}) #a differential indeterminate + for j in range(0, n): + zEha = Poly(z**(n + j), DE.t)*E**(j + 1)*ha + zEhd = hd + Pa, Pd = cancel((zEha, zEhd))[1], cancel((zEha, zEhd))[2] + Q = Pa.quo(Pd) + for i in range(0, j + 1): + Q = Q.subs(Z[i], V[i]) + Dha = (hd*derivation(ha, DE, basic=True).as_poly(DE.t) + + ha*derivation(hd, DE, basic=True).as_poly(DE.t) + + hd*derivation(ha, DE_new, basic=True).as_poly(DE.t) + + ha*derivation(hd, DE_new, basic=True).as_poly(DE.t)) + Dhd = Poly(j + 1, DE.t)*hd**2 + ha, hd = Dha, Dhd + + Ff, _ = F.div(gcd(F, Q)) + F_stara, F_stard = frac_in(Ff, DE.t) + if F_stara.degree(DE.t) - F_stard.degree(DE.t) > 0: + QBC = Poly(Q, DE.t)*B**(1 + j)*C**(n + j) + H = QBC + H_list.append(H) + H = (QBC*F_stard).rem(F_stara) + alphas = real_roots(F_stara) + for alpha in list(alphas): + delta_a = delta_a*Poly((DE.t - alpha)**(n - j), DE.t) + Poly(H.eval(alpha), DE.t) + delta_d = delta_d*Poly((DE.t - alpha)**(n - j), DE.t) + return (delta_a, delta_d, H_list) + + +def recognize_derivative(a, d, DE, z=None): + """ + Compute the squarefree factorization of the denominator of f + and for each Di the polynomial H in K[x] (see Theorem 2.7.1), using the + LaurentSeries algorithm. Write Di = GiEi where Gj = gcd(Hn, Di) and + gcd(Ei,Hn) = 1. Since the residues of f at the roots of Gj are all 0, and + the residue of f at a root alpha of Ei is Hi(a) != 0, f is the derivative of a + rational function if and only if Ei = 1 for each i, which is equivalent to + Di | H[-1] for each i. + """ + flag =True + a, d = a.cancel(d, include=True) + _, r = a.div(d) + Np, Sp = splitfactor_sqf(d, DE, coefficientD=True, z=z) + + j = 1 + for s, _ in Sp: + delta_a, delta_d, H = laurent_series(r, d, s, j, DE) + g = gcd(d, H[-1]).as_poly() + if g is not d: + flag = False + break + j = j + 1 + return flag + + +def recognize_log_derivative(a, d, DE, z=None): + """ + There exists a v in K(x)* such that f = dv/v + where f a rational function if and only if f can be written as f = A/D + where D is squarefree,deg(A) < deg(D), gcd(A, D) = 1, + and all the roots of the Rothstein-Trager resultant are integers. In that case, + any of the Rothstein-Trager, Lazard-Rioboo-Trager or Czichowski algorithm + produces u in K(x) such that du/dx = uf. + """ + + z = z or Dummy('z') + a, d = a.cancel(d, include=True) + _, a = a.div(d) + + pz = Poly(z, DE.t) + Dd = derivation(d, DE) + q = a - pz*Dd + r, _ = d.resultant(q, includePRS=True) + r = Poly(r, z) + Np, Sp = splitfactor_sqf(r, DE, coefficientD=True, z=z) + + for s, _ in Sp: + # TODO also consider the complex roots which should + # turn the flag false + a = real_roots(s.as_poly(z)) + + if not all(j.is_Integer for j in a): + return False + return True + +def residue_reduce(a, d, DE, z=None, invert=True): + """ + Lazard-Rioboo-Rothstein-Trager resultant reduction. + + Explanation + =========== + + Given a derivation ``D`` on k(t) and f in k(t) simple, return g + elementary over k(t) and a Boolean b in {True, False} such that f - + Dg in k[t] if b == True or f + h and f + h - Dg do not have an + elementary integral over k(t) for any h in k (reduced) if b == + False. + + Returns (G, b), where G is a tuple of tuples of the form (s_i, S_i), + such that g = Add(*[RootSum(s_i, lambda z: z*log(S_i(z, t))) for + S_i, s_i in G]). f - Dg is the remaining integral, which is elementary + only if b == True, and hence the integral of f is elementary only if + b == True. + + f - Dg is not calculated in this function because that would require + explicitly calculating the RootSum. Use residue_reduce_derivation(). + """ + # TODO: Use log_to_atan() from rationaltools.py + # If r = residue_reduce(...), then the logarithmic part is given by: + # sum([RootSum(a[0].as_poly(z), lambda i: i*log(a[1].as_expr()).subs(z, + # i)).subs(t, log(x)) for a in r[0]]) + + z = z or Dummy('z') + a, d = a.cancel(d, include=True) + a, d = a.to_field().mul_ground(1/d.LC()), d.to_field().mul_ground(1/d.LC()) + kkinv = [1/x for x in DE.T[:DE.level]] + DE.T[:DE.level] + + if a.is_zero: + return ([], True) + _, a = a.div(d) + + pz = Poly(z, DE.t) + + Dd = derivation(d, DE) + q = a - pz*Dd + + if Dd.degree(DE.t) <= d.degree(DE.t): + r, R = d.resultant(q, includePRS=True) + else: + r, R = q.resultant(d, includePRS=True) + + R_map, H = {}, [] + for i in R: + R_map[i.degree()] = i + + r = Poly(r, z) + Np, Sp = splitfactor_sqf(r, DE, coefficientD=True, z=z) + + for s, i in Sp: + if i == d.degree(DE.t): + s = Poly(s, z).monic() + H.append((s, d)) + else: + h = R_map.get(i) + if h is None: + continue + h_lc = Poly(h.as_poly(DE.t).LC(), DE.t, field=True) + + h_lc_sqf = h_lc.sqf_list_include(all=True) + + for a, j in h_lc_sqf: + h = Poly(h, DE.t, field=True).exquo(Poly(gcd(a, s**j, *kkinv), + DE.t)) + + s = Poly(s, z).monic() + + if invert: + h_lc = Poly(h.as_poly(DE.t).LC(), DE.t, field=True, expand=False) + inv, coeffs = h_lc.as_poly(z, field=True).invert(s), [S.One] + + for coeff in h.coeffs()[1:]: + L = reduced(inv*coeff.as_poly(inv.gens), [s])[1] + coeffs.append(L.as_expr()) + + h = Poly(dict(list(zip(h.monoms(), coeffs))), DE.t) + + H.append((s, h)) + + b = not any(cancel(i.as_expr()).has(DE.t, z) for i, _ in Np) + + return (H, b) + + +def residue_reduce_to_basic(H, DE, z): + """ + Converts the tuple returned by residue_reduce() into a Basic expression. + """ + # TODO: check what Lambda does with RootOf + i = Dummy('i') + s = list(zip(reversed(DE.T), reversed([f(DE.x) for f in DE.Tfuncs]))) + + return sum(RootSum(a[0].as_poly(z), Lambda(i, i*log(a[1].as_expr()).subs( + {z: i}).subs(s))) for a in H) + + +def residue_reduce_derivation(H, DE, z): + """ + Computes the derivation of an expression returned by residue_reduce(). + + In general, this is a rational function in t, so this returns an + as_expr() result. + """ + # TODO: verify that this is correct for multiple extensions + i = Dummy('i') + return S(sum(RootSum(a[0].as_poly(z), Lambda(i, i*derivation(a[1], + DE).as_expr().subs(z, i)/a[1].as_expr().subs(z, i))) for a in H)) + + +def integrate_primitive_polynomial(p, DE): + """ + Integration of primitive polynomials. + + Explanation + =========== + + Given a primitive monomial t over k, and ``p`` in k[t], return q in k[t], + r in k, and a bool b in {True, False} such that r = p - Dq is in k if b is + True, or r = p - Dq does not have an elementary integral over k(t) if b is + False. + """ + Zero = Poly(0, DE.t) + q = Poly(0, DE.t) + + if not p.expr.has(DE.t): + return (Zero, p, True) + + from .prde import limited_integrate + while True: + if not p.expr.has(DE.t): + return (q, p, True) + + Dta, Dtb = frac_in(DE.d, DE.T[DE.level - 1]) + + with DecrementLevel(DE): # We had better be integrating the lowest extension (x) + # with ratint(). + a = p.LC() + aa, ad = frac_in(a, DE.t) + + try: + rv = limited_integrate(aa, ad, [(Dta, Dtb)], DE) + if rv is None: + raise NonElementaryIntegralException + (ba, bd), c = rv + except NonElementaryIntegralException: + return (q, p, False) + + m = p.degree(DE.t) + q0 = c[0].as_poly(DE.t)*Poly(DE.t**(m + 1)/(m + 1), DE.t) + \ + (ba.as_expr()/bd.as_expr()).as_poly(DE.t)*Poly(DE.t**m, DE.t) + + p = p - derivation(q0, DE) + q = q + q0 + + +def integrate_primitive(a, d, DE, z=None): + """ + Integration of primitive functions. + + Explanation + =========== + + Given a primitive monomial t over k and f in k(t), return g elementary over + k(t), i in k(t), and b in {True, False} such that i = f - Dg is in k if b + is True or i = f - Dg does not have an elementary integral over k(t) if b + is False. + + This function returns a Basic expression for the first argument. If b is + True, the second argument is Basic expression in k to recursively integrate. + If b is False, the second argument is an unevaluated Integral, which has + been proven to be nonelementary. + """ + # XXX: a and d must be canceled, or this might return incorrect results + z = z or Dummy("z") + s = list(zip(reversed(DE.T), reversed([f(DE.x) for f in DE.Tfuncs]))) + + g1, h, r = hermite_reduce(a, d, DE) + g2, b = residue_reduce(h[0], h[1], DE, z=z) + if not b: + i = cancel(a.as_expr()/d.as_expr() - (g1[1]*derivation(g1[0], DE) - + g1[0]*derivation(g1[1], DE)).as_expr()/(g1[1]**2).as_expr() - + residue_reduce_derivation(g2, DE, z)) + i = NonElementaryIntegral(cancel(i).subs(s), DE.x) + return ((g1[0].as_expr()/g1[1].as_expr()).subs(s) + + residue_reduce_to_basic(g2, DE, z), i, b) + + # h - Dg2 + r + p = cancel(h[0].as_expr()/h[1].as_expr() - residue_reduce_derivation(g2, + DE, z) + r[0].as_expr()/r[1].as_expr()) + p = p.as_poly(DE.t) + + q, i, b = integrate_primitive_polynomial(p, DE) + + ret = ((g1[0].as_expr()/g1[1].as_expr() + q.as_expr()).subs(s) + + residue_reduce_to_basic(g2, DE, z)) + if not b: + # TODO: This does not do the right thing when b is False + i = NonElementaryIntegral(cancel(i.as_expr()).subs(s), DE.x) + else: + i = cancel(i.as_expr()) + + return (ret, i, b) + + +def integrate_hyperexponential_polynomial(p, DE, z): + """ + Integration of hyperexponential polynomials. + + Explanation + =========== + + Given a hyperexponential monomial t over k and ``p`` in k[t, 1/t], return q in + k[t, 1/t] and a bool b in {True, False} such that p - Dq in k if b is True, + or p - Dq does not have an elementary integral over k(t) if b is False. + """ + t1 = DE.t + dtt = DE.d.exquo(Poly(DE.t, DE.t)) + qa = Poly(0, DE.t) + qd = Poly(1, DE.t) + b = True + + if p.is_zero: + return(qa, qd, b) + + from sympy.integrals.rde import rischDE + + with DecrementLevel(DE): + for i in range(-p.degree(z), p.degree(t1) + 1): + if not i: + continue + elif i < 0: + # If you get AttributeError: 'NoneType' object has no attribute 'nth' + # then this should really not have expand=False + # But it shouldn't happen because p is already a Poly in t and z + a = p.as_poly(z, expand=False).nth(-i) + else: + # If you get AttributeError: 'NoneType' object has no attribute 'nth' + # then this should really not have expand=False + a = p.as_poly(t1, expand=False).nth(i) + + aa, ad = frac_in(a, DE.t, field=True) + aa, ad = aa.cancel(ad, include=True) + iDt = Poly(i, t1)*dtt + iDta, iDtd = frac_in(iDt, DE.t, field=True) + try: + va, vd = rischDE(iDta, iDtd, Poly(aa, DE.t), Poly(ad, DE.t), DE) + va, vd = frac_in((va, vd), t1, cancel=True) + except NonElementaryIntegralException: + b = False + else: + qa = qa*vd + va*Poly(t1**i)*qd + qd *= vd + + return (qa, qd, b) + + +def integrate_hyperexponential(a, d, DE, z=None, conds='piecewise'): + """ + Integration of hyperexponential functions. + + Explanation + =========== + + Given a hyperexponential monomial t over k and f in k(t), return g + elementary over k(t), i in k(t), and a bool b in {True, False} such that + i = f - Dg is in k if b is True or i = f - Dg does not have an elementary + integral over k(t) if b is False. + + This function returns a Basic expression for the first argument. If b is + True, the second argument is Basic expression in k to recursively integrate. + If b is False, the second argument is an unevaluated Integral, which has + been proven to be nonelementary. + """ + # XXX: a and d must be canceled, or this might return incorrect results + z = z or Dummy("z") + s = list(zip(reversed(DE.T), reversed([f(DE.x) for f in DE.Tfuncs]))) + + g1, h, r = hermite_reduce(a, d, DE) + g2, b = residue_reduce(h[0], h[1], DE, z=z) + if not b: + i = cancel(a.as_expr()/d.as_expr() - (g1[1]*derivation(g1[0], DE) - + g1[0]*derivation(g1[1], DE)).as_expr()/(g1[1]**2).as_expr() - + residue_reduce_derivation(g2, DE, z)) + i = NonElementaryIntegral(cancel(i.subs(s)), DE.x) + return ((g1[0].as_expr()/g1[1].as_expr()).subs(s) + + residue_reduce_to_basic(g2, DE, z), i, b) + + # p should be a polynomial in t and 1/t, because Sirr == k[t, 1/t] + # h - Dg2 + r + p = cancel(h[0].as_expr()/h[1].as_expr() - residue_reduce_derivation(g2, + DE, z) + r[0].as_expr()/r[1].as_expr()) + pp = as_poly_1t(p, DE.t, z) + + qa, qd, b = integrate_hyperexponential_polynomial(pp, DE, z) + + i = pp.nth(0, 0) + + ret = ((g1[0].as_expr()/g1[1].as_expr()).subs(s) \ + + residue_reduce_to_basic(g2, DE, z)) + + qas = qa.as_expr().subs(s) + qds = qd.as_expr().subs(s) + if conds == 'piecewise' and DE.x not in qds.free_symbols: + # We have to be careful if the exponent is S.Zero! + + # XXX: Does qd = 0 always necessarily correspond to the exponential + # equaling 1? + ret += Piecewise( + (qas/qds, Ne(qds, 0)), + (integrate((p - i).subs(DE.t, 1).subs(s), DE.x), True) + ) + else: + ret += qas/qds + + if not b: + i = p - (qd*derivation(qa, DE) - qa*derivation(qd, DE)).as_expr()/\ + (qd**2).as_expr() + i = NonElementaryIntegral(cancel(i).subs(s), DE.x) + return (ret, i, b) + + +def integrate_hypertangent_polynomial(p, DE): + """ + Integration of hypertangent polynomials. + + Explanation + =========== + + Given a differential field k such that sqrt(-1) is not in k, a + hypertangent monomial t over k, and p in k[t], return q in k[t] and + c in k such that p - Dq - c*D(t**2 + 1)/(t**1 + 1) is in k and p - + Dq does not have an elementary integral over k(t) if Dc != 0. + """ + # XXX: Make sure that sqrt(-1) is not in k. + q, r = polynomial_reduce(p, DE) + a = DE.d.exquo(Poly(DE.t**2 + 1, DE.t)) + c = Poly(r.nth(1)/(2*a.as_expr()), DE.t) + return (q, c) + + +def integrate_nonlinear_no_specials(a, d, DE, z=None): + """ + Integration of nonlinear monomials with no specials. + + Explanation + =========== + + Given a nonlinear monomial t over k such that Sirr ({p in k[t] | p is + special, monic, and irreducible}) is empty, and f in k(t), returns g + elementary over k(t) and a Boolean b in {True, False} such that f - Dg is + in k if b == True, or f - Dg does not have an elementary integral over k(t) + if b == False. + + This function is applicable to all nonlinear extensions, but in the case + where it returns b == False, it will only have proven that the integral of + f - Dg is nonelementary if Sirr is empty. + + This function returns a Basic expression. + """ + # TODO: Integral from k? + # TODO: split out nonelementary integral + # XXX: a and d must be canceled, or this might not return correct results + z = z or Dummy("z") + s = list(zip(reversed(DE.T), reversed([f(DE.x) for f in DE.Tfuncs]))) + + g1, h, r = hermite_reduce(a, d, DE) + g2, b = residue_reduce(h[0], h[1], DE, z=z) + if not b: + return ((g1[0].as_expr()/g1[1].as_expr()).subs(s) + + residue_reduce_to_basic(g2, DE, z), b) + + # Because f has no specials, this should be a polynomial in t, or else + # there is a bug. + p = cancel(h[0].as_expr()/h[1].as_expr() - residue_reduce_derivation(g2, + DE, z).as_expr() + r[0].as_expr()/r[1].as_expr()).as_poly(DE.t) + q1, q2 = polynomial_reduce(p, DE) + + if q2.expr.has(DE.t): + b = False + else: + b = True + + ret = (cancel(g1[0].as_expr()/g1[1].as_expr() + q1.as_expr()).subs(s) + + residue_reduce_to_basic(g2, DE, z)) + return (ret, b) + + +class NonElementaryIntegral(Integral): + """ + Represents a nonelementary Integral. + + Explanation + =========== + + If the result of integrate() is an instance of this class, it is + guaranteed to be nonelementary. Note that integrate() by default will try + to find any closed-form solution, even in terms of special functions which + may themselves not be elementary. To make integrate() only give + elementary solutions, or, in the cases where it can prove the integral to + be nonelementary, instances of this class, use integrate(risch=True). + In this case, integrate() may raise NotImplementedError if it cannot make + such a determination. + + integrate() uses the deterministic Risch algorithm to integrate elementary + functions or prove that they have no elementary integral. In some cases, + this algorithm can split an integral into an elementary and nonelementary + part, so that the result of integrate will be the sum of an elementary + expression and a NonElementaryIntegral. + + Examples + ======== + + >>> from sympy import integrate, exp, log, Integral + >>> from sympy.abc import x + + >>> a = integrate(exp(-x**2), x, risch=True) + >>> print(a) + Integral(exp(-x**2), x) + >>> type(a) + + + >>> expr = (2*log(x)**2 - log(x) - x**2)/(log(x)**3 - x**2*log(x)) + >>> b = integrate(expr, x, risch=True) + >>> print(b) + -log(-x + log(x))/2 + log(x + log(x))/2 + Integral(1/log(x), x) + >>> type(b.atoms(Integral).pop()) + + + """ + # TODO: This is useful in and of itself, because isinstance(result, + # NonElementaryIntegral) will tell if the integral has been proven to be + # elementary. But should we do more? Perhaps a no-op .doit() if + # elementary=True? Or maybe some information on why the integral is + # nonelementary. + pass + + +def risch_integrate(f, x, extension=None, handle_first='log', + separate_integral=False, rewrite_complex=None, + conds='piecewise'): + r""" + The Risch Integration Algorithm. + + Explanation + =========== + + Only transcendental functions are supported. Currently, only exponentials + and logarithms are supported, but support for trigonometric functions is + forthcoming. + + If this function returns an unevaluated Integral in the result, it means + that it has proven that integral to be nonelementary. Any errors will + result in raising NotImplementedError. The unevaluated Integral will be + an instance of NonElementaryIntegral, a subclass of Integral. + + handle_first may be either 'exp' or 'log'. This changes the order in + which the extension is built, and may result in a different (but + equivalent) solution (for an example of this, see issue 5109). It is also + possible that the integral may be computed with one but not the other, + because not all cases have been implemented yet. It defaults to 'log' so + that the outer extension is exponential when possible, because more of the + exponential case has been implemented. + + If ``separate_integral`` is ``True``, the result is returned as a tuple (ans, i), + where the integral is ans + i, ans is elementary, and i is either a + NonElementaryIntegral or 0. This useful if you want to try further + integrating the NonElementaryIntegral part using other algorithms to + possibly get a solution in terms of special functions. It is False by + default. + + Examples + ======== + + >>> from sympy.integrals.risch import risch_integrate + >>> from sympy import exp, log, pprint + >>> from sympy.abc import x + + First, we try integrating exp(-x**2). Except for a constant factor of + 2/sqrt(pi), this is the famous error function. + + >>> pprint(risch_integrate(exp(-x**2), x)) + / + | + | 2 + | -x + | e dx + | + / + + The unevaluated Integral in the result means that risch_integrate() has + proven that exp(-x**2) does not have an elementary anti-derivative. + + In many cases, risch_integrate() can split out the elementary + anti-derivative part from the nonelementary anti-derivative part. + For example, + + >>> pprint(risch_integrate((2*log(x)**2 - log(x) - x**2)/(log(x)**3 - + ... x**2*log(x)), x)) + / + | + log(-x + log(x)) log(x + log(x)) | 1 + - ---------------- + --------------- + | ------ dx + 2 2 | log(x) + | + / + + This means that it has proven that the integral of 1/log(x) is + nonelementary. This function is also known as the logarithmic integral, + and is often denoted as Li(x). + + risch_integrate() currently only accepts purely transcendental functions + with exponentials and logarithms, though note that this can include + nested exponentials and logarithms, as well as exponentials with bases + other than E. + + >>> pprint(risch_integrate(exp(x)*exp(exp(x)), x)) + / x\ + \e / + e + >>> pprint(risch_integrate(exp(exp(x)), x)) + / + | + | / x\ + | \e / + | e dx + | + / + + >>> pprint(risch_integrate(x*x**x*log(x) + x**x + x*x**x, x)) + x + x*x + >>> pprint(risch_integrate(x**x, x)) + / + | + | x + | x dx + | + / + + >>> pprint(risch_integrate(-1/(x*log(x)*log(log(x))**2), x)) + 1 + ----------- + log(log(x)) + + """ + f = S(f) + + DE = extension or DifferentialExtension(f, x, handle_first=handle_first, + dummy=True, rewrite_complex=rewrite_complex) + fa, fd = DE.fa, DE.fd + + result = S.Zero + for case in reversed(DE.cases): + if not fa.expr.has(DE.t) and not fd.expr.has(DE.t) and not case == 'base': + DE.decrement_level() + fa, fd = frac_in((fa, fd), DE.t) + continue + + fa, fd = fa.cancel(fd, include=True) + if case == 'exp': + ans, i, b = integrate_hyperexponential(fa, fd, DE, conds=conds) + elif case == 'primitive': + ans, i, b = integrate_primitive(fa, fd, DE) + elif case == 'base': + # XXX: We can't call ratint() directly here because it doesn't + # handle polynomials correctly. + ans = integrate(fa.as_expr()/fd.as_expr(), DE.x, risch=False) + b = False + i = S.Zero + else: + raise NotImplementedError("Only exponential and logarithmic " + "extensions are currently supported.") + + result += ans + if b: + DE.decrement_level() + fa, fd = frac_in(i, DE.t) + else: + result = result.subs(DE.backsubs) + if not i.is_zero: + i = NonElementaryIntegral(i.function.subs(DE.backsubs),i.limits) + if not separate_integral: + result += i + return result + else: + + if isinstance(i, NonElementaryIntegral): + return (result, i) + else: + return (result, 0) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/singularityfunctions.py b/venv/lib/python3.11/site-packages/sympy/integrals/singularityfunctions.py new file mode 100644 index 0000000000000000000000000000000000000000..3e33d0542c45b67b193f17e00c25837f3a82109a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/singularityfunctions.py @@ -0,0 +1,63 @@ +from sympy.functions import SingularityFunction, DiracDelta +from sympy.integrals import integrate + + +def singularityintegrate(f, x): + """ + This function handles the indefinite integrations of Singularity functions. + The ``integrate`` function calls this function internally whenever an + instance of SingularityFunction is passed as argument. + + Explanation + =========== + + The idea for integration is the following: + + - If we are dealing with a SingularityFunction expression, + i.e. ``SingularityFunction(x, a, n)``, we just return + ``SingularityFunction(x, a, n + 1)/(n + 1)`` if ``n >= 0`` and + ``SingularityFunction(x, a, n + 1)`` if ``n < 0``. + + - If the node is a multiplication or power node having a + SingularityFunction term we rewrite the whole expression in terms of + Heaviside and DiracDelta and then integrate the output. Lastly, we + rewrite the output of integration back in terms of SingularityFunction. + + - If none of the above case arises, we return None. + + Examples + ======== + + >>> from sympy.integrals.singularityfunctions import singularityintegrate + >>> from sympy import SingularityFunction, symbols, Function + >>> x, a, n, y = symbols('x a n y') + >>> f = Function('f') + >>> singularityintegrate(SingularityFunction(x, a, 3), x) + SingularityFunction(x, a, 4)/4 + >>> singularityintegrate(5*SingularityFunction(x, 5, -2), x) + 5*SingularityFunction(x, 5, -1) + >>> singularityintegrate(6*SingularityFunction(x, 5, -1), x) + 6*SingularityFunction(x, 5, 0) + >>> singularityintegrate(x*SingularityFunction(x, 0, -1), x) + 0 + >>> singularityintegrate(SingularityFunction(x, 1, -1) * f(x), x) + f(1)*SingularityFunction(x, 1, 0) + + """ + + if not f.has(SingularityFunction): + return None + + if isinstance(f, SingularityFunction): + x, a, n = f.args + if n.is_positive or n.is_zero: + return SingularityFunction(x, a, n + 1)/(n + 1) + elif n in (-1, -2, -3, -4): + return SingularityFunction(x, a, n + 1) + + if f.is_Mul or f.is_Pow: + + expr = f.rewrite(DiracDelta) + expr = integrate(expr, x) + return expr.rewrite(SingularityFunction) + return None diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_deltafunctions.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_deltafunctions.py new file mode 100644 index 0000000000000000000000000000000000000000..d4fd567349b50f795e08d583fd08db67b1596577 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_deltafunctions.py @@ -0,0 +1,79 @@ +from sympy.core.function import Function +from sympy.core.numbers import (Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.functions.special.delta_functions import (DiracDelta, Heaviside) +from sympy.integrals.deltafunctions import change_mul, deltaintegrate + +f = Function("f") +x_1, x_2, x, y, z = symbols("x_1 x_2 x y z") + + +def test_change_mul(): + assert change_mul(x, x) == (None, None) + assert change_mul(x*y, x) == (None, None) + assert change_mul(x*y*DiracDelta(x), x) == (DiracDelta(x), x*y) + assert change_mul(x*y*DiracDelta(x)*DiracDelta(y), x) == \ + (DiracDelta(x), x*y*DiracDelta(y)) + assert change_mul(DiracDelta(x)**2, x) == \ + (DiracDelta(x), DiracDelta(x)) + assert change_mul(y*DiracDelta(x)**2, x) == \ + (DiracDelta(x), y*DiracDelta(x)) + + +def test_deltaintegrate(): + assert deltaintegrate(x, x) is None + assert deltaintegrate(x + DiracDelta(x), x) is None + assert deltaintegrate(DiracDelta(x, 0), x) == Heaviside(x) + for n in range(10): + assert deltaintegrate(DiracDelta(x, n + 1), x) == DiracDelta(x, n) + assert deltaintegrate(DiracDelta(x), x) == Heaviside(x) + assert deltaintegrate(DiracDelta(-x), x) == Heaviside(x) + assert deltaintegrate(DiracDelta(x - y), x) == Heaviside(x - y) + assert deltaintegrate(DiracDelta(y - x), x) == Heaviside(x - y) + + assert deltaintegrate(x*DiracDelta(x), x) == 0 + assert deltaintegrate((x - y)*DiracDelta(x - y), x) == 0 + + assert deltaintegrate(DiracDelta(x)**2, x) == DiracDelta(0)*Heaviside(x) + assert deltaintegrate(y*DiracDelta(x)**2, x) == \ + y*DiracDelta(0)*Heaviside(x) + assert deltaintegrate(DiracDelta(x, 1), x) == DiracDelta(x, 0) + assert deltaintegrate(y*DiracDelta(x, 1), x) == y*DiracDelta(x, 0) + assert deltaintegrate(DiracDelta(x, 1)**2, x) == -DiracDelta(0, 2)*Heaviside(x) + assert deltaintegrate(y*DiracDelta(x, 1)**2, x) == -y*DiracDelta(0, 2)*Heaviside(x) + + + assert deltaintegrate(DiracDelta(x) * f(x), x) == f(0) * Heaviside(x) + assert deltaintegrate(DiracDelta(-x) * f(x), x) == f(0) * Heaviside(x) + assert deltaintegrate(DiracDelta(x - 1) * f(x), x) == f(1) * Heaviside(x - 1) + assert deltaintegrate(DiracDelta(1 - x) * f(x), x) == f(1) * Heaviside(x - 1) + assert deltaintegrate(DiracDelta(x**2 + x - 2), x) == \ + Heaviside(x - 1)/3 + Heaviside(x + 2)/3 + + p = cos(x)*(DiracDelta(x) + DiracDelta(x**2 - 1))*sin(x)*(x - pi) + assert deltaintegrate(p, x) - (-pi*(cos(1)*Heaviside(-1 + x)*sin(1)/2 - \ + cos(1)*Heaviside(1 + x)*sin(1)/2) + \ + cos(1)*Heaviside(1 + x)*sin(1)/2 + \ + cos(1)*Heaviside(-1 + x)*sin(1)/2) == 0 + + p = x_2*DiracDelta(x - x_2)*DiracDelta(x_2 - x_1) + assert deltaintegrate(p, x_2) == x*DiracDelta(x - x_1)*Heaviside(x_2 - x) + + p = x*y**2*z*DiracDelta(y - x)*DiracDelta(y - z)*DiracDelta(x - z) + assert deltaintegrate(p, y) == x**3*z*DiracDelta(x - z)**2*Heaviside(y - x) + assert deltaintegrate((x + 1)*DiracDelta(2*x), x) == S.Half * Heaviside(x) + assert deltaintegrate((x + 1)*DiracDelta(x*Rational(2, 3) + Rational(4, 9)), x) == \ + S.Half * Heaviside(x + Rational(2, 3)) + + a, b, c = symbols('a b c', commutative=False) + assert deltaintegrate(DiracDelta(x - y)*f(x - b)*f(x - a), x) == \ + f(y - b)*f(y - a)*Heaviside(x - y) + + p = f(x - a)*DiracDelta(x - y)*f(x - c)*f(x - b) + assert deltaintegrate(p, x) == f(y - a)*f(y - c)*f(y - b)*Heaviside(x - y) + + p = DiracDelta(x - z)*f(x - b)*f(x - a)*DiracDelta(x - y) + assert deltaintegrate(p, x) == DiracDelta(y - z)*f(y - b)*f(y - a) * \ + Heaviside(x - y) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_failing_integrals.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_failing_integrals.py new file mode 100644 index 0000000000000000000000000000000000000000..9bb434cf0009cd96ad2b7882d109b7fbe23193c2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_failing_integrals.py @@ -0,0 +1,277 @@ +# A collection of failing integrals from the issues. + +from sympy.core.numbers import (I, Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.complexes import sign +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import (sech, sinh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (acos, atan, cos, sin, tan) +from sympy.functions.special.delta_functions import DiracDelta +from sympy.functions.special.gamma_functions import gamma +from sympy.integrals.integrals import (Integral, integrate) +from sympy.simplify.fu import fu + + +from sympy.testing.pytest import XFAIL, slow, tooslow + +from sympy.abc import x, k, c, y, b, h, a, m, z, n, t + + +@tooslow +@XFAIL +def test_issue_3880(): + # integrate_hyperexponential(Poly(t*2*(1 - t0**2)*t0*(x**3 + x**2), t), Poly((1 + t0**2)**2*2*(x**2 + x + 1), t), [Poly(1, x), Poly(1 + t0**2, t0), Poly(t, t)], [x, t0, t], [exp, tan]) + assert not integrate(exp(x)*cos(2*x)*sin(2*x) * (x**3 + x**2)/(2*(x**2 + x + 1)), x).has(Integral) + + +def test_issue_4212_real(): + xr = symbols('xr', real=True) + negabsx = Piecewise((-xr, xr < 0), (xr, True)) + assert integrate(sign(xr), xr) == negabsx + + +@XFAIL +def test_issue_4212(): + # XXX: Maybe this should be expected to fail without real assumptions on x. + # As a complex function sign(x) is not analytic and so there is no complex + # function whose complex derivative is sign(x). With real assumptions this + # works (see test_issue_4212_real above). + assert not integrate(sign(x), x).has(Integral) + + +def test_issue_4511(): + # This works, but gives a slightly over-complicated answer. + f = integrate(cos(x)**2 / (1 - sin(x)), x) + assert fu(f) == x - cos(x) - 1 + assert f == ((x*tan(x/2)**2 + x - 2)/(tan(x/2)**2 + 1)).expand() + + +def test_integrate_DiracDelta_no_meijerg(): + assert integrate(integrate(integrate( + DiracDelta(x - y - z), (z, 0, oo)), (y, 0, 1), meijerg=False), (x, 0, 1)) == S.Half + + +@XFAIL +def test_integrate_DiracDelta_fails(): + # issue 6427 + # works without meijerg. See test_integrate_DiracDelta_no_meijerg above. + assert integrate(integrate(integrate( + DiracDelta(x - y - z), (z, 0, oo)), (y, 0, 1)), (x, 0, 1)) == S.Half + + +@XFAIL +@slow +def test_issue_4525(): + # Warning: takes a long time + assert not integrate((x**m * (1 - x)**n * (a + b*x + c*x**2))/(1 + x**2), (x, 0, 1)).has(Integral) + + +@XFAIL +@tooslow +def test_issue_4540(): + # Note, this integral is probably nonelementary + assert not integrate( + (sin(1/x) - x*exp(x)) / + ((-sin(1/x) + x*exp(x))*x + x*sin(1/x)), x).has(Integral) + + +@XFAIL +@slow +def test_issue_4891(): + # Requires the hypergeometric function. + assert not integrate(cos(x)**y, x).has(Integral) + + +@XFAIL +@slow +def test_issue_1796a(): + assert not integrate(exp(2*b*x)*exp(-a*x**2), x).has(Integral) + + +@XFAIL +def test_issue_4895b(): + assert not integrate(exp(2*b*x)*exp(-a*x**2), (x, -oo, 0)).has(Integral) + + +@XFAIL +def test_issue_4895c(): + assert not integrate(exp(2*b*x)*exp(-a*x**2), (x, -oo, oo)).has(Integral) + + +@XFAIL +def test_issue_4895d(): + assert not integrate(exp(2*b*x)*exp(-a*x**2), (x, 0, oo)).has(Integral) + + +@XFAIL +@slow +def test_issue_4941(): + assert not integrate(sqrt(1 + sinh(x/20)**2), (x, -25, 25)).has(Integral) + + +@XFAIL +def test_issue_4992(): + # Nonelementary integral. Requires hypergeometric/Meijer-G handling. + assert not integrate(log(x) * x**(k - 1) * exp(-x) / gamma(k), (x, 0, oo)).has(Integral) + + +@XFAIL +def test_issue_16396a(): + i = integrate(1/(1+sqrt(tan(x))), (x, pi/3, pi/6)) + assert not i.has(Integral) + + +@XFAIL +def test_issue_16396b(): + i = integrate(x*sin(x)/(1+cos(x)**2), (x, 0, pi)) + assert not i.has(Integral) + + +@XFAIL +def test_issue_16046(): + assert integrate(exp(exp(I*x)), [x, 0, 2*pi]) == 2*pi + + +@XFAIL +def test_issue_15925a(): + assert not integrate(sqrt((1+sin(x))**2+(cos(x))**2), (x, -pi/2, pi/2)).has(Integral) + + +def test_issue_15925b(): + f = sqrt((-12*cos(x)**2*sin(x))**2+(12*cos(x)*sin(x)**2)**2) + assert integrate(f, (x, 0, pi/6)) == Rational(3, 2) + + +@XFAIL +def test_issue_15925b_manual(): + assert not integrate(sqrt((-12*cos(x)**2*sin(x))**2+(12*cos(x)*sin(x)**2)**2), + (x, 0, pi/6), manual=True).has(Integral) + + +@XFAIL +@tooslow +def test_issue_15227(): + i = integrate(log(1-x)*log((1+x)**2)/x, (x, 0, 1)) + assert not i.has(Integral) + # assert i == -5*zeta(3)/4 + + +@XFAIL +@slow +def test_issue_14716(): + i = integrate(log(x + 5)*cos(pi*x),(x, S.Half, 1)) + assert not i.has(Integral) + # Mathematica can not solve it either, but + # integrate(log(x + 5)*cos(pi*x),(x, S.Half, 1)).transform(x, y - 5).doit() + # works + # assert i == -log(Rational(11, 2))/pi - Si(pi*Rational(11, 2))/pi + Si(6*pi)/pi + + +@XFAIL +def test_issue_14709a(): + i = integrate(x*acos(1 - 2*x/h), (x, 0, h)) + assert not i.has(Integral) + # assert i == 5*h**2*pi/16 + + +@slow +@XFAIL +def test_issue_14398(): + assert not integrate(exp(x**2)*cos(x), x).has(Integral) + + +@XFAIL +def test_issue_14074(): + i = integrate(log(sin(x)), (x, 0, pi/2)) + assert not i.has(Integral) + # assert i == -pi*log(2)/2 + + +@XFAIL +@slow +def test_issue_14078b(): + i = integrate((atan(4*x)-atan(2*x))/x, (x, 0, oo)) + assert not i.has(Integral) + # assert i == pi*log(2)/2 + + +@XFAIL +def test_issue_13792(): + i = integrate(log(1/x) / (1 - x), (x, 0, 1)) + assert not i.has(Integral) + # assert i in [polylog(2, -exp_polar(I*pi)), pi**2/6] + + +@XFAIL +def test_issue_11845a(): + assert not integrate(exp(y - x**3), (x, 0, 1)).has(Integral) + + +@XFAIL +def test_issue_11845b(): + assert not integrate(exp(-y - x**3), (x, 0, 1)).has(Integral) + + +@XFAIL +def test_issue_11813(): + assert not integrate((a - x)**Rational(-1, 2)*x, (x, 0, a)).has(Integral) + + +@XFAIL +def test_issue_11254c(): + assert not integrate(sech(x)**2, (x, 0, 1)).has(Integral) + + +@XFAIL +def test_issue_10584(): + assert not integrate(sqrt(x**2 + 1/x**2), x).has(Integral) + + +@XFAIL +def test_issue_9101(): + assert not integrate(log(x + sqrt(x**2 + y**2 + z**2)), z).has(Integral) + + +@XFAIL +def test_issue_7147(): + assert not integrate(x/sqrt(a*x**2 + b*x + c)**3, x).has(Integral) + + +@XFAIL +def test_issue_7109(): + assert not integrate(sqrt(a**2/(a**2 - x**2)), x).has(Integral) + + +@XFAIL +def test_integrate_Piecewise_rational_over_reals(): + f = Piecewise( + (0, t - 478.515625*pi < 0), + (13.2075145209219*pi/(0.000871222*t + 0.995)**2, t - 478.515625*pi >= 0)) + + assert abs((integrate(f, (t, 0, oo)) - 15235.9375*pi).evalf()) <= 1e-7 + + +@XFAIL +def test_issue_4311_slow(): + # Not slow when bypassing heurish + assert not integrate(x*abs(9-x**2), x).has(Integral) + +@XFAIL +def test_issue_20370(): + a = symbols('a', positive=True) + assert integrate((1 + a * cos(x))**-1, (x, 0, 2 * pi)) == (2 * pi / sqrt(1 - a**2)) + + +@XFAIL +def test_polylog(): + # log(1/x)*log(x+1)-polylog(2, -x) + assert not integrate(log(1/x)/(x + 1), x).has(Integral) + + +@XFAIL +def test_polylog_manual(): + # Make sure _parts_rule does not go into an infinite loop here + assert not integrate(log(1/x)/(x + 1), x, manual=True).has(Integral) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_heurisch.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_heurisch.py new file mode 100644 index 0000000000000000000000000000000000000000..f02556dedd597721529cab47bf53609110e0ce2a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_heurisch.py @@ -0,0 +1,417 @@ +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.function import (Derivative, Function, diff) +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.relational import Eq, Ne +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.exponential import (LambertW, exp, log) +from sympy.functions.elementary.hyperbolic import (asinh, cosh, sinh, tanh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (acos, asin, atan, cos, sin, tan) +from sympy.functions.special.bessel import (besselj, besselk, bessely, jn) +from sympy.functions.special.error_functions import erf +from sympy.integrals.integrals import Integral +from sympy.logic.boolalg import And +from sympy.matrices import Matrix +from sympy.simplify.ratsimp import ratsimp +from sympy.simplify.simplify import simplify +from sympy.integrals.heurisch import components, heurisch, heurisch_wrapper +from sympy.testing.pytest import XFAIL, slow +from sympy.integrals.integrals import integrate +from sympy import S + +x, y, z, nu = symbols('x,y,z,nu') +f = Function('f') + + +def test_components(): + assert components(x*y, x) == {x} + assert components(1/(x + y), x) == {x} + assert components(sin(x), x) == {sin(x), x} + assert components(sin(x)*sqrt(log(x)), x) == \ + {log(x), sin(x), sqrt(log(x)), x} + assert components(x*sin(exp(x)*y), x) == \ + {sin(y*exp(x)), x, exp(x)} + assert components(x**Rational(17, 54)/sqrt(sin(x)), x) == \ + {sin(x), x**Rational(1, 54), sqrt(sin(x)), x} + + assert components(f(x), x) == \ + {x, f(x)} + assert components(Derivative(f(x), x), x) == \ + {x, f(x), Derivative(f(x), x)} + assert components(f(x)*diff(f(x), x), x) == \ + {x, f(x), Derivative(f(x), x), Derivative(f(x), x)} + + +def test_issue_10680(): + assert isinstance(integrate(x**log(x**log(x**log(x))),x), Integral) + + +def test_issue_21166(): + assert integrate(sin(x/sqrt(abs(x))), (x, -1, 1)) == 0 + + +def test_heurisch_polynomials(): + assert heurisch(1, x) == x + assert heurisch(x, x) == x**2/2 + assert heurisch(x**17, x) == x**18/18 + # For coverage + assert heurisch_wrapper(y, x) == y*x + + +def test_heurisch_fractions(): + assert heurisch(1/x, x) == log(x) + assert heurisch(1/(2 + x), x) == log(x + 2) + assert heurisch(1/(x + sin(y)), x) == log(x + sin(y)) + + # Up to a constant, where C = pi*I*Rational(5, 12), Mathematica gives identical + # result in the first case. The difference is because SymPy changes + # signs of expressions without any care. + # XXX ^ ^ ^ is this still correct? + assert heurisch(5*x**5/( + 2*x**6 - 5), x) in [5*log(2*x**6 - 5) / 12, 5*log(-2*x**6 + 5) / 12] + assert heurisch(5*x**5/(2*x**6 + 5), x) == 5*log(2*x**6 + 5) / 12 + + assert heurisch(1/x**2, x) == -1/x + assert heurisch(-1/x**5, x) == 1/(4*x**4) + + +def test_heurisch_log(): + assert heurisch(log(x), x) == x*log(x) - x + assert heurisch(log(3*x), x) == -x + x*log(3) + x*log(x) + assert heurisch(log(x**2), x) in [x*log(x**2) - 2*x, 2*x*log(x) - 2*x] + + +def test_heurisch_exp(): + assert heurisch(exp(x), x) == exp(x) + assert heurisch(exp(-x), x) == -exp(-x) + assert heurisch(exp(17*x), x) == exp(17*x) / 17 + assert heurisch(x*exp(x), x) == x*exp(x) - exp(x) + assert heurisch(x*exp(x**2), x) == exp(x**2) / 2 + + assert heurisch(exp(-x**2), x) is None + + assert heurisch(2**x, x) == 2**x/log(2) + assert heurisch(x*2**x, x) == x*2**x/log(2) - 2**x*log(2)**(-2) + + assert heurisch(Integral(x**z*y, (y, 1, 2), (z, 2, 3)).function, x) == (x*x**z*y)/(z+1) + assert heurisch(Sum(x**z, (z, 1, 2)).function, z) == x**z/log(x) + + # https://github.com/sympy/sympy/issues/23707 + anti = -exp(z)/(sqrt(x - y)*exp(z*sqrt(x - y)) - exp(z*sqrt(x - y))) + assert heurisch(exp(z)*exp(-z*sqrt(x - y)), z) == anti + + +def test_heurisch_trigonometric(): + assert heurisch(sin(x), x) == -cos(x) + assert heurisch(pi*sin(x) + 1, x) == x - pi*cos(x) + + assert heurisch(cos(x), x) == sin(x) + assert heurisch(tan(x), x) in [ + log(1 + tan(x)**2)/2, + log(tan(x) + I) + I*x, + log(tan(x) - I) - I*x, + ] + + assert heurisch(sin(x)*sin(y), x) == -cos(x)*sin(y) + assert heurisch(sin(x)*sin(y), y) == -cos(y)*sin(x) + + # gives sin(x) in answer when run via setup.py and cos(x) when run via py.test + assert heurisch(sin(x)*cos(x), x) in [sin(x)**2 / 2, -cos(x)**2 / 2] + assert heurisch(cos(x)/sin(x), x) == log(sin(x)) + + assert heurisch(x*sin(7*x), x) == sin(7*x) / 49 - x*cos(7*x) / 7 + assert heurisch(1/pi/4 * x**2*cos(x), x) == 1/pi/4*(x**2*sin(x) - + 2*sin(x) + 2*x*cos(x)) + + assert heurisch(acos(x/4) * asin(x/4), x) == 2*x - (sqrt(16 - x**2))*asin(x/4) \ + + (sqrt(16 - x**2))*acos(x/4) + x*asin(x/4)*acos(x/4) + + assert heurisch(sin(x)/(cos(x)**2+1), x) == -atan(cos(x)) #fixes issue 13723 + assert heurisch(1/(cos(x)+2), x) == 2*sqrt(3)*atan(sqrt(3)*tan(x/2)/3)/3 + assert heurisch(2*sin(x)*cos(x)/(sin(x)**4 + 1), x) == atan(sqrt(2)*sin(x) + - 1) - atan(sqrt(2)*sin(x) + 1) + + assert heurisch(1/cosh(x), x) == 2*atan(tanh(x/2)) + + +def test_heurisch_hyperbolic(): + assert heurisch(sinh(x), x) == cosh(x) + assert heurisch(cosh(x), x) == sinh(x) + + assert heurisch(x*sinh(x), x) == x*cosh(x) - sinh(x) + assert heurisch(x*cosh(x), x) == x*sinh(x) - cosh(x) + + assert heurisch( + x*asinh(x/2), x) == x**2*asinh(x/2)/2 + asinh(x/2) - x*sqrt(4 + x**2)/4 + + +def test_heurisch_mixed(): + assert heurisch(sin(x)*exp(x), x) == exp(x)*sin(x)/2 - exp(x)*cos(x)/2 + assert heurisch(sin(x/sqrt(-x)), x) == 2*x*cos(x/sqrt(-x))/sqrt(-x) - 2*sin(x/sqrt(-x)) + + +def test_heurisch_radicals(): + assert heurisch(1/sqrt(x), x) == 2*sqrt(x) + assert heurisch(1/sqrt(x)**3, x) == -2/sqrt(x) + assert heurisch(sqrt(x)**3, x) == 2*sqrt(x)**5/5 + + assert heurisch(sin(x)*sqrt(cos(x)), x) == -2*sqrt(cos(x))**3/3 + y = Symbol('y') + assert heurisch(sin(y*sqrt(x)), x) == 2/y**2*sin(y*sqrt(x)) - \ + 2*sqrt(x)*cos(y*sqrt(x))/y + assert heurisch_wrapper(sin(y*sqrt(x)), x) == Piecewise( + (-2*sqrt(x)*cos(sqrt(x)*y)/y + 2*sin(sqrt(x)*y)/y**2, Ne(y, 0)), + (0, True)) + y = Symbol('y', positive=True) + assert heurisch_wrapper(sin(y*sqrt(x)), x) == 2/y**2*sin(y*sqrt(x)) - \ + 2*sqrt(x)*cos(y*sqrt(x))/y + + +def test_heurisch_special(): + assert heurisch(erf(x), x) == x*erf(x) + exp(-x**2)/sqrt(pi) + assert heurisch(exp(-x**2)*erf(x), x) == sqrt(pi)*erf(x)**2 / 4 + + +def test_heurisch_symbolic_coeffs(): + assert heurisch(1/(x + y), x) == log(x + y) + assert heurisch(1/(x + sqrt(2)), x) == log(x + sqrt(2)) + assert simplify(diff(heurisch(log(x + y + z), y), y)) == log(x + y + z) + + +def test_heurisch_symbolic_coeffs_1130(): + y = Symbol('y') + assert heurisch_wrapper(1/(x**2 + y), x) == Piecewise( + (log(x - sqrt(-y))/(2*sqrt(-y)) - log(x + sqrt(-y))/(2*sqrt(-y)), + Ne(y, 0)), (-1/x, True)) + y = Symbol('y', positive=True) + assert heurisch_wrapper(1/(x**2 + y), x) == (atan(x/sqrt(y))/sqrt(y)) + + +def test_heurisch_hacking(): + assert heurisch(sqrt(1 + 7*x**2), x, hints=[]) == \ + x*sqrt(1 + 7*x**2)/2 + sqrt(7)*asinh(sqrt(7)*x)/14 + assert heurisch(sqrt(1 - 7*x**2), x, hints=[]) == \ + x*sqrt(1 - 7*x**2)/2 + sqrt(7)*asin(sqrt(7)*x)/14 + + assert heurisch(1/sqrt(1 + 7*x**2), x, hints=[]) == \ + sqrt(7)*asinh(sqrt(7)*x)/7 + assert heurisch(1/sqrt(1 - 7*x**2), x, hints=[]) == \ + sqrt(7)*asin(sqrt(7)*x)/7 + + assert heurisch(exp(-7*x**2), x, hints=[]) == \ + sqrt(7*pi)*erf(sqrt(7)*x)/14 + + assert heurisch(1/sqrt(9 - 4*x**2), x, hints=[]) == \ + asin(x*Rational(2, 3))/2 + + assert heurisch(1/sqrt(9 + 4*x**2), x, hints=[]) == \ + asinh(x*Rational(2, 3))/2 + + assert heurisch(1/sqrt(3*x**2-4), x, hints=[]) == \ + sqrt(3)*log(3*x + sqrt(3)*sqrt(3*x**2 - 4))/3 + + +def test_heurisch_function(): + assert heurisch(f(x), x) is None + +@XFAIL +def test_heurisch_function_derivative(): + # TODO: it looks like this used to work just by coincindence and + # thanks to sloppy implementation. Investigate why this used to + # work at all and if support for this can be restored. + + df = diff(f(x), x) + + assert heurisch(f(x)*df, x) == f(x)**2/2 + assert heurisch(f(x)**2*df, x) == f(x)**3/3 + assert heurisch(df/f(x), x) == log(f(x)) + + +def test_heurisch_wrapper(): + f = 1/(y + x) + assert heurisch_wrapper(f, x) == log(x + y) + f = 1/(y - x) + assert heurisch_wrapper(f, x) == -log(x - y) + f = 1/((y - x)*(y + x)) + assert heurisch_wrapper(f, x) == Piecewise( + (-log(x - y)/(2*y) + log(x + y)/(2*y), Ne(y, 0)), (1/x, True)) + # issue 6926 + f = sqrt(x**2/((y - x)*(y + x))) + assert heurisch_wrapper(f, x) == x*sqrt(-x**2/(x**2 - y**2)) \ + - y**2*sqrt(-x**2/(x**2 - y**2))/x + + +def test_issue_3609(): + assert heurisch(1/(x * (1 + log(x)**2)), x) == atan(log(x)) + +### These are examples from the Poor Man's Integrator +### http://www-sop.inria.fr/cafe/Manuel.Bronstein/pmint/examples/ + + +def test_pmint_rat(): + # TODO: heurisch() is off by a constant: -3/4. Possibly different permutation + # would give the optimal result? + + def drop_const(expr, x): + if expr.is_Add: + return Add(*[ arg for arg in expr.args if arg.has(x) ]) + else: + return expr + + f = (x**7 - 24*x**4 - 4*x**2 + 8*x - 8)/(x**8 + 6*x**6 + 12*x**4 + 8*x**2) + g = (4 + 8*x**2 + 6*x + 3*x**3)/(x**5 + 4*x**3 + 4*x) + log(x) + + assert drop_const(ratsimp(heurisch(f, x)), x) == g + + +def test_pmint_trig(): + f = (x - tan(x)) / tan(x)**2 + tan(x) + g = -x**2/2 - x/tan(x) + log(tan(x)**2 + 1)/2 + + assert heurisch(f, x) == g + + +def test_pmint_logexp(): + f = (1 + x + x*exp(x))*(x + log(x) + exp(x) - 1)/(x + log(x) + exp(x))**2/x + g = log(x + exp(x) + log(x)) + 1/(x + exp(x) + log(x)) + + assert ratsimp(heurisch(f, x)) == g + + +def test_pmint_erf(): + f = exp(-x**2)*erf(x)/(erf(x)**3 - erf(x)**2 - erf(x) + 1) + g = sqrt(pi)*log(erf(x) - 1)/8 - sqrt(pi)*log(erf(x) + 1)/8 - sqrt(pi)/(4*erf(x) - 4) + + assert ratsimp(heurisch(f, x)) == g + + +def test_pmint_LambertW(): + f = LambertW(x) + g = x*LambertW(x) - x + x/LambertW(x) + + assert heurisch(f, x) == g + + +def test_pmint_besselj(): + f = besselj(nu + 1, x)/besselj(nu, x) + g = nu*log(x) - log(besselj(nu, x)) + + assert heurisch(f, x) == g + + f = (nu*besselj(nu, x) - x*besselj(nu + 1, x))/x + g = besselj(nu, x) + + assert heurisch(f, x) == g + + f = jn(nu + 1, x)/jn(nu, x) + g = nu*log(x) - log(jn(nu, x)) + + assert heurisch(f, x) == g + + +@slow +def test_pmint_bessel_products(): + f = x*besselj(nu, x)*bessely(nu, 2*x) + g = -2*x*besselj(nu, x)*bessely(nu - 1, 2*x)/3 + x*besselj(nu - 1, x)*bessely(nu, 2*x)/3 + + assert heurisch(f, x) == g + + f = x*besselj(nu, x)*besselk(nu, 2*x) + g = -2*x*besselj(nu, x)*besselk(nu - 1, 2*x)/5 - x*besselj(nu - 1, x)*besselk(nu, 2*x)/5 + + assert heurisch(f, x) == g + + +def test_pmint_WrightOmega(): + def omega(x): + return LambertW(exp(x)) + + f = (1 + omega(x) * (2 + cos(omega(x)) * (x + omega(x))))/(1 + omega(x))/(x + omega(x)) + g = log(x + LambertW(exp(x))) + sin(LambertW(exp(x))) + + assert heurisch(f, x) == g + + +def test_RR(): + # Make sure the algorithm does the right thing if the ring is RR. See + # issue 8685. + assert heurisch(sqrt(1 + 0.25*x**2), x, hints=[]) == \ + 0.5*x*sqrt(0.25*x**2 + 1) + 1.0*asinh(0.5*x) + +# TODO: convert the rest of PMINT tests: +# Airy functions +# f = (x - AiryAi(x)*AiryAi(1, x)) / (x**2 - AiryAi(x)**2) +# g = Rational(1,2)*ln(x + AiryAi(x)) + Rational(1,2)*ln(x - AiryAi(x)) +# f = x**2 * AiryAi(x) +# g = -AiryAi(x) + AiryAi(1, x)*x +# Whittaker functions +# f = WhittakerW(mu + 1, nu, x) / (WhittakerW(mu, nu, x) * x) +# g = x/2 - mu*ln(x) - ln(WhittakerW(mu, nu, x)) + + +def test_issue_22527(): + t, R = symbols(r't R') + z = Function('z')(t) + def f(x): + return x/sqrt(R**2 - x**2) + Uz = integrate(f(z), z) + Ut = integrate(f(t), t) + assert Ut == Uz.subs(z, t) + + +def test_heurisch_complex_erf_issue_26338(): + r = symbols('r', real=True) + a = sqrt(pi)*erf((1 + I)/2)/2 + assert integrate(exp(-I*r**2/2), (r, 0, 1)) == a - I*a + + a = exp(-x**2/(2*(2 - I)**2)) + assert heurisch(a, x, hints=[]) is None # None, not a wrong soln + a = exp(-r**2/(2*(2 - I)**2)) + assert heurisch(a, r, hints=[]) is None + a = sqrt(pi)*erf((1 + I)/2)/2 + assert integrate(exp(-I*x**2/2), (x, 0, 1)) == a - I*a + + +def test_issue_15498(): + Z0 = Function('Z0') + k01, k10, t, s= symbols('k01 k10 t s', real=True, positive=True) + m = Matrix([[exp(-k10*t)]]) + _83 = Rational(83, 100) # 0.83 works, too + [a, b, c, d, e, f, g] = [100, 0.5, _83, 50, 0.6, 2, 120] + AIF_btf = a*(d*e*(1 - exp(-(t - b)/e)) + f*g*(1 - exp(-(t - b)/g))) + AIF_atf = a*(d*e*exp(-(t - b)/e)*(exp((c - b)/e) - 1 + ) + f*g*exp(-(t - b)/g)*(exp((c - b)/g) - 1)) + AIF_sym = Piecewise((0, t < b), (AIF_btf, And(b <= t, t < c)), (AIF_atf, c <= t)) + aif_eq = Eq(Z0(t), AIF_sym) + f_vec = Matrix([[k01*Z0(t)]]) + integrand = m*m.subs(t, s)**-1*f_vec.subs(aif_eq.lhs, aif_eq.rhs).subs(t, s) + solution = integrate(integrand[0], (s, 0, t)) + assert solution is not None # does not hang and takes less than 10 s + + +@slow +def test_heurisch_issue_26930(): + integrand = x**Rational(4, 3)*log(x) + anti = 3*x**(S(7)/3)*log(x)/7 - 9*x**(S(7)/3)/49 + assert heurisch(integrand, x) == anti + assert integrate(integrand, x) == anti + assert integrate(integrand, (x, 0, 1)) == -S(9)/49 + + +def test_heurisch_issue_26922(): + + a, b, x = symbols("a, b, x", real=True, positive=True) + C = symbols("C", real=True) + i1 = -C*x*exp(-a*x**2 - sqrt(b)*x) + i2 = C*x*exp(-a*x**2 + sqrt(b)*x) + i = Integral(i1, x) + Integral(i2, x) + res = ( + -C*exp(-a*x**2)*exp(sqrt(b)*x)/(2*a) + + C*exp(-a*x**2)*exp(-sqrt(b)*x)/(2*a) + + sqrt(pi)*C*sqrt(b)*exp(b/(4*a))*erf(sqrt(a)*x - sqrt(b)/(2*sqrt(a)))/(4*a**(S(3)/2)) + + sqrt(pi)*C*sqrt(b)*exp(b/(4*a))*erf(sqrt(a)*x + sqrt(b)/(2*sqrt(a)))/(4*a**(S(3)/2)) + ) + + assert i.doit(heurisch=False).expand() == res diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_integrals.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_integrals.py new file mode 100644 index 0000000000000000000000000000000000000000..41e1ef3aa36334189f14cf734ac2ad26d001b506 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_integrals.py @@ -0,0 +1,2187 @@ +import math +from sympy.concrete.summations import (Sum, summation) +from sympy.core.add import Add +from sympy.core.containers import Tuple +from sympy.core.expr import Expr +from sympy.core.function import (Derivative, Function, Lambda, diff) +from sympy.core import EulerGamma +from sympy.core.numbers import (E, I, Rational, nan, oo, pi, zoo, all_close) +from sympy.core.relational import (Eq, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.core.sympify import sympify +from sympy.functions.elementary.complexes import (Abs, im, polar_lift, re, sign) +from sympy.functions.elementary.exponential import (LambertW, exp, exp_polar, log) +from sympy.functions.elementary.hyperbolic import (acosh, asinh, cosh, coth, csch, sinh, tanh, sech) +from sympy.functions.elementary.miscellaneous import (Max, Min, sqrt) +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (acos, asin, atan, cos, sin, sinc, tan, sec) +from sympy.functions.special.delta_functions import DiracDelta, Heaviside +from sympy.functions.special.error_functions import (Ci, Ei, Si, erf, erfc, erfi, fresnelc, li) +from sympy.functions.special.gamma_functions import (gamma, polygamma) +from sympy.functions.special.hyper import (hyper, meijerg) +from sympy.functions.special.singularity_functions import SingularityFunction +from sympy.functions.special.zeta_functions import lerchphi +from sympy.integrals.integrals import integrate +from sympy.logic.boolalg import And +from sympy.matrices.dense import Matrix +from sympy.polys.polytools import (Poly, factor) +from sympy.printing.str import sstr +from sympy.series.order import O +from sympy.sets.sets import Interval +from sympy.simplify.gammasimp import gammasimp +from sympy.simplify.simplify import simplify +from sympy.simplify.trigsimp import trigsimp +from sympy.tensor.indexed import (Idx, IndexedBase) +from sympy.core.expr import unchanged +from sympy.functions.elementary.integers import floor +from sympy.integrals.integrals import Integral +from sympy.integrals.risch import NonElementaryIntegral +from sympy.physics import units +from sympy.testing.pytest import raises, slow, warns_deprecated_sympy, warns +from sympy.utilities.exceptions import SymPyDeprecationWarning +from sympy.core.random import verify_numerically + + +x, y, z, a, b, c, d, e, s, t, x_1, x_2 = symbols('x y z a b c d e s t x_1 x_2') +n = Symbol('n', integer=True) +f = Function('f') + + +def NS(e, n=15, **options): + return sstr(sympify(e).evalf(n, **options), full_prec=True) + + +def test_poly_deprecated(): + p = Poly(2*x, x) + assert p.integrate(x) == Poly(x**2, x, domain='QQ') + # The stacklevel is based on Integral(Poly) + with warns(SymPyDeprecationWarning, test_stacklevel=False): + integrate(p, x) + with warns(SymPyDeprecationWarning, test_stacklevel=False): + Integral(p, (x,)) + + +@slow +def test_principal_value(): + g = 1 / x + assert Integral(g, (x, -oo, oo)).principal_value() == 0 + assert Integral(g, (y, -oo, oo)).principal_value() == oo * sign(1 / x) + raises(ValueError, lambda: Integral(g, (x)).principal_value()) + raises(ValueError, lambda: Integral(g).principal_value()) + + l = 1 / ((x ** 3) - 1) + assert Integral(l, (x, -oo, oo)).principal_value().together() == -sqrt(3)*pi/3 + raises(ValueError, lambda: Integral(l, (x, -oo, 1)).principal_value()) + + d = 1 / (x ** 2 - 1) + assert Integral(d, (x, -oo, oo)).principal_value() == 0 + assert Integral(d, (x, -2, 2)).principal_value() == -log(3) + + v = x / (x ** 2 - 1) + assert Integral(v, (x, -oo, oo)).principal_value() == 0 + assert Integral(v, (x, -2, 2)).principal_value() == 0 + + s = x ** 2 / (x ** 2 - 1) + assert Integral(s, (x, -oo, oo)).principal_value() is oo + assert Integral(s, (x, -2, 2)).principal_value() == -log(3) + 4 + + f = 1 / ((x ** 2 - 1) * (1 + x ** 2)) + assert Integral(f, (x, -oo, oo)).principal_value() == -pi / 2 + assert Integral(f, (x, -2, 2)).principal_value() == -atan(2) - log(3) / 2 + + +def diff_test(i): + """Return the set of symbols, s, which were used in testing that + i.diff(s) agrees with i.doit().diff(s). If there is an error then + the assertion will fail, causing the test to fail.""" + syms = i.free_symbols + for s in syms: + assert (i.diff(s).doit() - i.doit().diff(s)).expand() == 0 + return syms + + +def test_improper_integral(): + assert integrate(log(x), (x, 0, 1)) == -1 + assert integrate(x**(-2), (x, 1, oo)) == 1 + assert integrate(1/(1 + exp(x)), (x, 0, oo)) == log(2) + + +def test_constructor(): + # this is shared by Sum, so testing Integral's constructor + # is equivalent to testing Sum's + s1 = Integral(n, n) + assert s1.limits == (Tuple(n),) + s2 = Integral(n, (n,)) + assert s2.limits == (Tuple(n),) + s3 = Integral(Sum(x, (x, 1, y))) + assert s3.limits == (Tuple(y),) + s4 = Integral(n, Tuple(n,)) + assert s4.limits == (Tuple(n),) + + s5 = Integral(n, (n, Interval(1, 2))) + assert s5.limits == (Tuple(n, 1, 2),) + + # Testing constructor with inequalities: + s6 = Integral(n, n > 10) + assert s6.limits == (Tuple(n, 10, oo),) + s7 = Integral(n, (n > 2) & (n < 5)) + assert s7.limits == (Tuple(n, 2, 5),) + + +def test_basics(): + + assert Integral(0, x) != 0 + assert Integral(x, (x, 1, 1)) != 0 + assert Integral(oo, x) != oo + assert Integral(S.NaN, x) is S.NaN + + assert diff(Integral(y, y), x) == 0 + assert diff(Integral(x, (x, 0, 1)), x) == 0 + assert diff(Integral(x, x), x) == x + assert diff(Integral(t, (t, 0, x)), x) == x + + e = (t + 1)**2 + assert diff(integrate(e, (t, 0, x)), x) == \ + diff(Integral(e, (t, 0, x)), x).doit().expand() == \ + ((1 + x)**2).expand() + assert diff(integrate(e, (t, 0, x)), t) == \ + diff(Integral(e, (t, 0, x)), t) == 0 + assert diff(integrate(e, (t, 0, x)), a) == \ + diff(Integral(e, (t, 0, x)), a) == 0 + assert diff(integrate(e, t), a) == diff(Integral(e, t), a) == 0 + + assert integrate(e, (t, a, x)).diff(x) == \ + Integral(e, (t, a, x)).diff(x).doit().expand() + assert Integral(e, (t, a, x)).diff(x).doit() == ((1 + x)**2) + assert integrate(e, (t, x, a)).diff(x).doit() == (-(1 + x)**2).expand() + + assert integrate(t**2, (t, x, 2*x)).diff(x) == 7*x**2 + + assert Integral(x, x).atoms() == {x} + assert Integral(f(x), (x, 0, 1)).atoms() == {S.Zero, S.One, x} + + assert diff_test(Integral(x, (x, 3*y))) == {y} + assert diff_test(Integral(x, (a, 3*y))) == {x, y} + + assert integrate(x, (x, oo, oo)) == 0 #issue 8171 + assert integrate(x, (x, -oo, -oo)) == 0 + + # sum integral of terms + assert integrate(y + x + exp(x), x) == x*y + x**2/2 + exp(x) + + assert Integral(x).is_commutative + n = Symbol('n', commutative=False) + assert Integral(n + x, x).is_commutative is False + + +def test_diff_wrt(): + class Test(Expr): + _diff_wrt = True + is_commutative = True + + t = Test() + assert integrate(t + 1, t) == t**2/2 + t + assert integrate(t + 1, (t, 0, 1)) == Rational(3, 2) + + raises(ValueError, lambda: integrate(x + 1, x + 1)) + raises(ValueError, lambda: integrate(x + 1, (x + 1, 0, 1))) + + +def test_basics_multiple(): + assert diff_test(Integral(x, (x, 3*x, 5*y), (y, x, 2*x))) == {x} + assert diff_test(Integral(x, (x, 5*y), (y, x, 2*x))) == {x} + assert diff_test(Integral(x, (x, 5*y), (y, y, 2*x))) == {x, y} + assert diff_test(Integral(y, y, x)) == {x, y} + assert diff_test(Integral(y*x, x, y)) == {x, y} + assert diff_test(Integral(x + y, y, (y, 1, x))) == {x} + assert diff_test(Integral(x + y, (x, x, y), (y, y, x))) == {x, y} + + +def test_conjugate_transpose(): + A, B = symbols("A B", commutative=False) + + x = Symbol("x", complex=True) + p = Integral(A*B, (x,)) + assert p.adjoint().doit() == p.doit().adjoint() + assert p.conjugate().doit() == p.doit().conjugate() + assert p.transpose().doit() == p.doit().transpose() + + x = Symbol("x", real=True) + p = Integral(A*B, (x,)) + assert p.adjoint().doit() == p.doit().adjoint() + assert p.conjugate().doit() == p.doit().conjugate() + assert p.transpose().doit() == p.doit().transpose() + + +def test_integration(): + assert integrate(0, (t, 0, x)) == 0 + assert integrate(3, (t, 0, x)) == 3*x + assert integrate(t, (t, 0, x)) == x**2/2 + assert integrate(3*t, (t, 0, x)) == 3*x**2/2 + assert integrate(3*t**2, (t, 0, x)) == x**3 + assert integrate(1/t, (t, 1, x)) == log(x) + assert integrate(-1/t**2, (t, 1, x)) == 1/x - 1 + assert integrate(t**2 + 5*t - 8, (t, 0, x)) == x**3/3 + 5*x**2/2 - 8*x + assert integrate(x**2, x) == x**3/3 + assert integrate((3*t*x)**5, x) == (3*t)**5 * x**6 / 6 + + b = Symbol("b") + c = Symbol("c") + assert integrate(a*t, (t, 0, x)) == a*x**2/2 + assert integrate(a*t**4, (t, 0, x)) == a*x**5/5 + assert integrate(a*t**2 + b*t + c, (t, 0, x)) == a*x**3/3 + b*x**2/2 + c*x + + +def test_multiple_integration(): + assert integrate((x**2)*(y**2), (x, 0, 1), (y, -1, 2)) == Rational(1) + assert integrate((y**2)*(x**2), x, y) == Rational(1, 9)*(x**3)*(y**3) + assert integrate(1/(x + 3)/(1 + x)**3, x) == \ + log(3 + x)*Rational(-1, 8) + log(1 + x)*Rational(1, 8) + x/(4 + 8*x + 4*x**2) + assert integrate(sin(x*y)*y, (x, 0, 1), (y, 0, 1)) == -sin(1) + 1 + + +def test_issue_3532(): + assert integrate(exp(-x), (x, 0, oo)) == 1 + + +def test_issue_3560(): + assert integrate(sqrt(x)**3, x) == 2*sqrt(x)**5/5 + assert integrate(sqrt(x), x) == 2*sqrt(x)**3/3 + assert integrate(1/sqrt(x)**3, x) == -2/sqrt(x) + + +def test_issue_18038(): + raises(AttributeError, lambda: integrate((x, x))) + + +def test_integrate_poly(): + p = Poly(x + x**2*y + y**3, x, y) + + # The stacklevel is based on Integral(Poly) + with warns_deprecated_sympy(): + qx = Integral(p, x) + with warns(SymPyDeprecationWarning, test_stacklevel=False): + qx = integrate(p, x) + with warns(SymPyDeprecationWarning, test_stacklevel=False): + qy = integrate(p, y) + + assert isinstance(qx, Poly) is True + assert isinstance(qy, Poly) is True + + assert qx.gens == (x, y) + assert qy.gens == (x, y) + + assert qx.as_expr() == x**2/2 + x**3*y/3 + x*y**3 + assert qy.as_expr() == x*y + x**2*y**2/2 + y**4/4 + + +def test_integrate_poly_definite(): + p = Poly(x + x**2*y + y**3, x, y) + + with warns_deprecated_sympy(): + Qx = Integral(p, (x, 0, 1)) + with warns(SymPyDeprecationWarning, test_stacklevel=False): + Qx = integrate(p, (x, 0, 1)) + with warns(SymPyDeprecationWarning, test_stacklevel=False): + Qy = integrate(p, (y, 0, pi)) + + assert isinstance(Qx, Poly) is True + assert isinstance(Qy, Poly) is True + + assert Qx.gens == (y,) + assert Qy.gens == (x,) + + assert Qx.as_expr() == S.Half + y/3 + y**3 + assert Qy.as_expr() == pi**4/4 + pi*x + pi**2*x**2/2 + + +def test_integrate_omit_var(): + y = Symbol('y') + + assert integrate(x) == x**2/2 + + raises(ValueError, lambda: integrate(2)) + raises(ValueError, lambda: integrate(x*y)) + + +def test_integrate_poly_accurately(): + y = Symbol('y') + assert integrate(x*sin(y), x) == x**2*sin(y)/2 + + # when passed to risch_norman, this will be a CPU hog, so this really + # checks, that integrated function is recognized as polynomial + assert integrate(x**1000*sin(y), x) == x**1001*sin(y)/1001 + + +def test_issue_3635(): + y = Symbol('y') + assert integrate(x**2, y) == x**2*y + assert integrate(x**2, (y, -1, 1)) == 2*x**2 + +# works in SymPy and py.test but hangs in `setup.py test` + + +def test_integrate_linearterm_pow(): + # check integrate((a*x+b)^c, x) -- issue 3499 + y = Symbol('y', positive=True) + # TODO: Remove conds='none' below, let the assumption take care of it. + assert integrate(x**y, x, conds='none') == x**(y + 1)/(y + 1) + assert integrate((exp(y)*x + 1/y)**(1 + sin(y)), x, conds='none') == \ + exp(-y)*(exp(y)*x + 1/y)**(2 + sin(y)) / (2 + sin(y)) + + +def test_issue_3618(): + assert integrate(pi*sqrt(x), x) == 2*pi*sqrt(x)**3/3 + assert integrate(pi*sqrt(x) + E*sqrt(x)**3, x) == \ + 2*pi*sqrt(x)**3/3 + 2*E *sqrt(x)**5/5 + + +def test_issue_3623(): + assert integrate(cos((n + 1)*x), x) == Piecewise( + (sin(x*(n + 1))/(n + 1), Ne(n + 1, 0)), (x, True)) + assert integrate(cos((n - 1)*x), x) == Piecewise( + (sin(x*(n - 1))/(n - 1), Ne(n - 1, 0)), (x, True)) + assert integrate(cos((n + 1)*x) + cos((n - 1)*x), x) == \ + Piecewise((sin(x*(n - 1))/(n - 1), Ne(n - 1, 0)), (x, True)) + \ + Piecewise((sin(x*(n + 1))/(n + 1), Ne(n + 1, 0)), (x, True)) + + +def test_issue_3664(): + n = Symbol('n', integer=True, nonzero=True) + assert integrate(-1./2 * x * sin(n * pi * x/2), [x, -2, 0]) == \ + 2.0*cos(pi*n)/(pi*n) + assert integrate(x * sin(n * pi * x/2) * Rational(-1, 2), [x, -2, 0]) == \ + 2*cos(pi*n)/(pi*n) + + +def test_issue_3679(): + # definite integration of rational functions gives wrong answers + assert NS(Integral(1/(x**2 - 8*x + 17), (x, 2, 4))) == '1.10714871779409' + + +def test_issue_3686(): # remove this when fresnel integrals are implemented + from sympy.core.function import expand_func + from sympy.functions.special.error_functions import fresnels + assert expand_func(integrate(sin(x**2), x)) == \ + sqrt(2)*sqrt(pi)*fresnels(sqrt(2)*x/sqrt(pi))/2 + + +def test_integrate_units(): + m = units.m + s = units.s + assert integrate(x * m/s, (x, 1*s, 5*s)) == 12*m*s + + +def test_transcendental_functions(): + assert integrate(LambertW(2*x), x) == \ + -x + x*LambertW(2*x) + x/LambertW(2*x) + + +def test_log_polylog(): + assert integrate(log(1 - x)/x, (x, 0, 1)) == -pi**2/6 + assert integrate(log(x)*(1 - x)**(-1), (x, 0, 1)) == -pi**2/6 + + +def test_issue_3740(): + f = 4*log(x) - 2*log(x)**2 + fid = diff(integrate(f, x), x) + assert abs(f.subs(x, 42).evalf() - fid.subs(x, 42).evalf()) < 1e-10 + + +def test_issue_3788(): + assert integrate(1/(1 + x**2), x) == atan(x) + + +def test_issue_3952(): + f = sin(x) + assert integrate(f, x) == -cos(x) + raises(ValueError, lambda: integrate(f, 2*x)) + + +def test_issue_4516(): + assert integrate(2**x - 2*x, x) == 2**x/log(2) - x**2 + + +def test_issue_7450(): + ans = integrate(exp(-(1 + I)*x), (x, 0, oo)) + assert re(ans) == S.Half and im(ans) == Rational(-1, 2) + + +def test_issue_8623(): + assert integrate((1 + cos(2*x)) / (3 - 2*cos(2*x)), (x, 0, pi)) == -pi/2 + sqrt(5)*pi/2 + assert integrate((1 + cos(2*x))/(3 - 2*cos(2*x))) == -x/2 + sqrt(5)*(atan(sqrt(5)*tan(x)) + \ + pi*floor((x - pi/2)/pi))/2 + + +def test_issue_9569(): + assert integrate(1 / (2 - cos(x)), (x, 0, pi)) == pi/sqrt(3) + assert integrate(1/(2 - cos(x))) == 2*sqrt(3)*(atan(sqrt(3)*tan(x/2)) + pi*floor((x/2 - pi/2)/pi))/3 + + +def test_issue_13733(): + s = Symbol('s', positive=True) + pz = exp(-(z - y)**2/(2*s*s))/sqrt(2*pi*s*s) + pzgx = integrate(pz, (z, x, oo)) + assert integrate(pzgx, (x, 0, oo)) == sqrt(2)*s*exp(-y**2/(2*s**2))/(2*sqrt(pi)) + \ + y*erf(sqrt(2)*y/(2*s))/2 + y/2 + + +def test_issue_13749(): + assert integrate(1 / (2 + cos(x)), (x, 0, pi)) == pi/sqrt(3) + assert integrate(1/(2 + cos(x))) == 2*sqrt(3)*(atan(sqrt(3)*tan(x/2)/3) + pi*floor((x/2 - pi/2)/pi))/3 + + +def test_issue_18133(): + assert integrate(exp(x)/(1 + x)**2, x) == NonElementaryIntegral(exp(x)/(x + 1)**2, x) + + +def test_issue_21741(): + a = 4e6 + b = 2.5e-7 + r = Piecewise((b*I*exp(-a*I*pi*t*y)*exp(-a*I*pi*x*z)/(pi*x), Ne(x, 0)), + (z*exp(-a*I*pi*t*y), True)) + fun = E**((-2*I*pi*(z*x+t*y))/(500*10**(-9))) + assert all_close(integrate(fun, z), r) + + +def test_matrices(): + M = Matrix(2, 2, lambda i, j: (i + j + 1)*sin((i + j + 1)*x)) + + assert integrate(M, x) == Matrix([ + [-cos(x), -cos(2*x)], + [-cos(2*x), -cos(3*x)], + ]) + + +def test_integrate_functions(): + # issue 4111 + assert integrate(f(x), x) == Integral(f(x), x) + assert integrate(f(x), (x, 0, 1)) == Integral(f(x), (x, 0, 1)) + assert integrate(f(x)*diff(f(x), x), x) == f(x)**2/2 + assert integrate(diff(f(x), x) / f(x), x) == log(f(x)) + + +def test_integrate_derivatives(): + assert integrate(Derivative(f(x), x), x) == f(x) + assert integrate(Derivative(f(y), y), x) == x*Derivative(f(y), y) + assert integrate(Derivative(f(x), x)**2, x) == \ + Integral(Derivative(f(x), x)**2, x) + + +def test_transform(): + a = Integral(x**2 + 1, (x, -1, 2)) + fx = x + fy = 3*y + 1 + assert a.doit() == a.transform(fx, fy).doit() + assert a.transform(fx, fy).transform(fy, fx) == a + fx = 3*x + 1 + fy = y + assert a.transform(fx, fy).transform(fy, fx) == a + a = Integral(sin(1/x), (x, 0, 1)) + assert a.transform(x, 1/y) == Integral(sin(y)/y**2, (y, 1, oo)) + assert a.transform(x, 1/y).transform(y, 1/x) == a + a = Integral(exp(-x**2), (x, -oo, oo)) + assert a.transform(x, 2*y) == Integral(2*exp(-4*y**2), (y, -oo, oo)) + # < 3 arg limit handled properly + assert Integral(x, x).transform(x, a*y).doit() == \ + Integral(y*a**2, y).doit() + _3 = S(3) + assert Integral(x, (x, 0, -_3)).transform(x, 1/y).doit() == \ + Integral(-1/x**3, (x, -oo, -1/_3)).doit() + assert Integral(x, (x, 0, _3)).transform(x, 1/y) == \ + Integral(y**(-3), (y, 1/_3, oo)) + # issue 8400 + i = Integral(x + y, (x, 1, 2), (y, 1, 2)) + assert i.transform(x, (x + 2*y, x)).doit() == \ + i.transform(x, (x + 2*z, x)).doit() == 3 + + i = Integral(x, (x, a, b)) + assert i.transform(x, 2*s) == Integral(4*s, (s, a/2, b/2)) + raises(ValueError, lambda: i.transform(x, 1)) + raises(ValueError, lambda: i.transform(x, s*t)) + raises(ValueError, lambda: i.transform(x, -s)) + raises(ValueError, lambda: i.transform(x, (s, t))) + raises(ValueError, lambda: i.transform(2*x, 2*s)) + + i = Integral(x**2, (x, 1, 2)) + raises(ValueError, lambda: i.transform(x**2, s)) + + am = Symbol('a', negative=True) + bp = Symbol('b', positive=True) + i = Integral(x, (x, bp, am)) + i.transform(x, 2*s) + assert i.transform(x, 2*s) == Integral(-4*s, (s, am/2, bp/2)) + + i = Integral(x, (x, a)) + assert i.transform(x, 2*s) == Integral(4*s, (s, a/2)) + + +def test_issue_4052(): + f = S.Half*asin(x) + x*sqrt(1 - x**2)/2 + + assert integrate(cos(asin(x)), x) == f + assert integrate(sin(acos(x)), x) == f + + +@slow +def test_evalf_integrals(): + assert NS(Integral(x, (x, 2, 5)), 15) == '10.5000000000000' + gauss = Integral(exp(-x**2), (x, -oo, oo)) + assert NS(gauss, 15) == '1.77245385090552' + assert NS(gauss**2 - pi + E*Rational( + 1, 10**20), 15) in ('2.71828182845904e-20', '2.71828182845905e-20') + # A monster of an integral from http://mathworld.wolfram.com/DefiniteIntegral.html + t = Symbol('t') + a = 8*sqrt(3)/(1 + 3*t**2) + b = 16*sqrt(2)*(3*t + 1)*sqrt(4*t**2 + t + 1)**3 + c = (3*t**2 + 1)*(11*t**2 + 2*t + 3)**2 + d = sqrt(2)*(249*t**2 + 54*t + 65)/(11*t**2 + 2*t + 3)**2 + f = a - b/c - d + assert NS(Integral(f, (t, 0, 1)), 50) == \ + NS((3*sqrt(2) - 49*pi + 162*atan(sqrt(2)))/12, 50) + # http://mathworld.wolfram.com/VardisIntegral.html + assert NS(Integral(log(log(1/x))/(1 + x + x**2), (x, 0, 1)), 15) == \ + NS('pi/sqrt(3) * log(2*pi**(5/6) / gamma(1/6))', 15) + # http://mathworld.wolfram.com/AhmedsIntegral.html + assert NS(Integral(atan(sqrt(x**2 + 2))/(sqrt(x**2 + 2)*(x**2 + 1)), (x, + 0, 1)), 15) == NS(5*pi**2/96, 15) + # http://mathworld.wolfram.com/AbelsIntegral.html + assert NS(Integral(x/((exp(pi*x) - exp( + -pi*x))*(x**2 + 1)), (x, 0, oo)), 15) == NS('log(2)/2-1/4', 15) + # Complex part trimming + # http://mathworld.wolfram.com/VardisIntegral.html + assert NS(Integral(log(log(sin(x)/cos(x))), (x, pi/4, pi/2)), 15, chop=True) == \ + NS('pi/4*log(4*pi**3/gamma(1/4)**4)', 15) + # + # Endpoints causing trouble (rounding error in integration points -> complex log) + assert NS( + 2 + Integral(log(2*cos(x/2)), (x, -pi, pi)), 17, chop=True) == NS(2, 17) + assert NS( + 2 + Integral(log(2*cos(x/2)), (x, -pi, pi)), 20, chop=True) == NS(2, 20) + assert NS( + 2 + Integral(log(2*cos(x/2)), (x, -pi, pi)), 22, chop=True) == NS(2, 22) + # Needs zero handling + assert NS(pi - 4*Integral( + 'sqrt(1-x**2)', (x, 0, 1)), 15, maxn=30, chop=True) in ('0.0', '0') + # Oscillatory quadrature + a = Integral(sin(x)/x**2, (x, 1, oo)).evalf(maxn=15) + assert 0.49 < a < 0.51 + assert NS( + Integral(sin(x)/x**2, (x, 1, oo)), quad='osc') == '0.504067061906928' + assert NS(Integral( + cos(pi*x + 1)/x, (x, -oo, -1)), quad='osc') == '0.276374705640365' + # indefinite integrals aren't evaluated + assert NS(Integral(x, x)) == 'Integral(x, x)' + assert NS(Integral(x, (x, y))) == 'Integral(x, (x, y))' + + +def test_evalf_issue_939(): + # https://github.com/sympy/sympy/issues/4038 + + # The output form of an integral may differ by a step function between + # revisions, making this test a bit useless. This can't be said about + # other two tests. For now, all values of this evaluation are used here, + # but in future this should be reconsidered. + assert NS(integrate(1/(x**5 + 1), x).subs(x, 4), chop=True) in \ + ['-0.000976138910649103', '0.965906660135753', '1.93278945918216'] + + assert NS(Integral(1/(x**5 + 1), (x, 2, 4))) == '0.0144361088886740' + assert NS( + integrate(1/(x**5 + 1), (x, 2, 4)), chop=True) == '0.0144361088886740' + + +def test_double_previously_failing_integrals(): + # Double integrals not implemented <- Sure it is! + res = integrate(sqrt(x) + x*y, (x, 1, 2), (y, -1, 1)) + # Old numerical test + assert NS(res, 15) == '2.43790283299492' + # Symbolic test + assert res == Rational(-4, 3) + 8*sqrt(2)/3 + # double integral + zero detection + assert integrate(sin(x + x*y), (x, -1, 1), (y, -1, 1)) is S.Zero + + +def test_integrate_SingularityFunction(): + in_1 = SingularityFunction(x, a, 3) + SingularityFunction(x, 5, -1) + out_1 = SingularityFunction(x, a, 4)/4 + SingularityFunction(x, 5, 0) + assert integrate(in_1, x) == out_1 + + in_2 = 10*SingularityFunction(x, 4, 0) - 5*SingularityFunction(x, -6, -2) + out_2 = 10*SingularityFunction(x, 4, 1) - 5*SingularityFunction(x, -6, -1) + assert integrate(in_2, x) == out_2 + + in_3 = 2*x**2*y -10*SingularityFunction(x, -4, 7) - 2*SingularityFunction(y, 10, -2) + out_3_1 = 2*x**3*y/3 - 2*x*SingularityFunction(y, 10, -2) - 5*SingularityFunction(x, -4, 8)/4 + out_3_2 = x**2*y**2 - 10*y*SingularityFunction(x, -4, 7) - 2*SingularityFunction(y, 10, -1) + assert integrate(in_3, x) == out_3_1 + assert integrate(in_3, y) == out_3_2 + + assert unchanged(Integral, in_3, (x,)) + assert Integral(in_3, x) == Integral(in_3, (x,)) + assert Integral(in_3, x).doit() == out_3_1 + + in_4 = 10*SingularityFunction(x, -4, 7) - 2*SingularityFunction(x, 10, -2) + out_4 = 5*SingularityFunction(x, -4, 8)/4 - 2*SingularityFunction(x, 10, -1) + assert integrate(in_4, (x, -oo, x)) == out_4 + + assert integrate(SingularityFunction(x, 5, -1), x) == SingularityFunction(x, 5, 0) + assert integrate(SingularityFunction(x, 0, -1), (x, -oo, oo)) == 1 + assert integrate(5*SingularityFunction(x, 5, -1), (x, -oo, oo)) == 5 + assert integrate(SingularityFunction(x, 5, -1) * f(x), (x, -oo, oo)) == f(5) + + +def test_integrate_DiracDelta(): + # This is here to check that deltaintegrate is being called, but also + # to test definite integrals. More tests are in test_deltafunctions.py + assert integrate(DiracDelta(x) * f(x), (x, -oo, oo)) == f(0) + assert integrate(DiracDelta(x)**2, (x, -oo, oo)) == DiracDelta(0) + # issue 4522 + assert integrate(integrate((4 - 4*x + x*y - 4*y) * \ + DiracDelta(x)*DiracDelta(y - 1), (x, 0, 1)), (y, 0, 1)) == 0 + # issue 5729 + p = exp(-(x**2 + y**2))/pi + assert integrate(p*DiracDelta(x - 10*y), (x, -oo, oo), (y, -oo, oo)) == \ + integrate(p*DiracDelta(x - 10*y), (y, -oo, oo), (x, -oo, oo)) == \ + integrate(p*DiracDelta(10*x - y), (x, -oo, oo), (y, -oo, oo)) == \ + integrate(p*DiracDelta(10*x - y), (y, -oo, oo), (x, -oo, oo)) == \ + 1/sqrt(101*pi) + + +def test_integrate_returns_piecewise(): + assert integrate(x**y, x) == Piecewise( + (x**(y + 1)/(y + 1), Ne(y, -1)), (log(x), True)) + assert integrate(x**y, y) == Piecewise( + (x**y/log(x), Ne(log(x), 0)), (y, True)) + assert integrate(exp(n*x), x) == Piecewise( + (exp(n*x)/n, Ne(n, 0)), (x, True)) + assert integrate(x*exp(n*x), x) == Piecewise( + ((n*x - 1)*exp(n*x)/n**2, Ne(n**2, 0)), (x**2/2, True)) + assert integrate(x**(n*y), x) == Piecewise( + (x**(n*y + 1)/(n*y + 1), Ne(n*y, -1)), (log(x), True)) + assert integrate(x**(n*y), y) == Piecewise( + (x**(n*y)/(n*log(x)), Ne(n*log(x), 0)), (y, True)) + assert integrate(cos(n*x), x) == Piecewise( + (sin(n*x)/n, Ne(n, 0)), (x, True)) + assert integrate(cos(n*x)**2, x) == Piecewise( + ((n*x/2 + sin(n*x)*cos(n*x)/2)/n, Ne(n, 0)), (x, True)) + assert integrate(x*cos(n*x), x) == Piecewise( + (x*sin(n*x)/n + cos(n*x)/n**2, Ne(n, 0)), (x**2/2, True)) + assert integrate(sin(n*x), x) == Piecewise( + (-cos(n*x)/n, Ne(n, 0)), (0, True)) + assert integrate(sin(n*x)**2, x) == Piecewise( + ((n*x/2 - sin(n*x)*cos(n*x)/2)/n, Ne(n, 0)), (0, True)) + assert integrate(x*sin(n*x), x) == Piecewise( + (-x*cos(n*x)/n + sin(n*x)/n**2, Ne(n, 0)), (0, True)) + assert integrate(exp(x*y), (x, 0, z)) == Piecewise( + (exp(y*z)/y - 1/y, (y > -oo) & (y < oo) & Ne(y, 0)), (z, True)) + # https://github.com/sympy/sympy/issues/23707 + assert integrate(exp(t)*exp(-t*sqrt(x - y)), t) == Piecewise( + (-exp(t)/(sqrt(x - y)*exp(t*sqrt(x - y)) - exp(t*sqrt(x - y))), + Ne(x, y + 1)), (t, True)) + + +def test_integrate_max_min(): + x = symbols('x', real=True) + assert integrate(Min(x, 2), (x, 0, 3)) == 4 + assert integrate(Max(x**2, x**3), (x, 0, 2)) == Rational(49, 12) + assert integrate(Min(exp(x), exp(-x))**2, x) == Piecewise( \ + (exp(2*x)/2, x <= 0), (1 - exp(-2*x)/2, True)) + # issue 7907 + c = symbols('c', extended_real=True) + int1 = integrate(Max(c, x)*exp(-x**2), (x, -oo, oo)) + int2 = integrate(c*exp(-x**2), (x, -oo, c)) + int3 = integrate(x*exp(-x**2), (x, c, oo)) + assert int1 == int2 + int3 == sqrt(pi)*c*erf(c)/2 + \ + sqrt(pi)*c/2 + exp(-c**2)/2 + + +def test_integrate_Abs_sign(): + assert integrate(Abs(x), (x, -2, 1)) == Rational(5, 2) + assert integrate(Abs(x), (x, 0, 1)) == S.Half + assert integrate(Abs(x + 1), (x, 0, 1)) == Rational(3, 2) + assert integrate(Abs(x**2 - 1), (x, -2, 2)) == 4 + assert integrate(Abs(x**2 - 3*x), (x, -15, 15)) == 2259 + assert integrate(sign(x), (x, -1, 2)) == 1 + assert integrate(sign(x)*sin(x), (x, -pi, pi)) == 4 + assert integrate(sign(x - 2) * x**2, (x, 0, 3)) == Rational(11, 3) + + t, s = symbols('t s', real=True) + assert integrate(Abs(t), t) == Piecewise( + (-t**2/2, t <= 0), (t**2/2, True)) + assert integrate(Abs(2*t - 6), t) == Piecewise( + (-t**2 + 6*t, t <= 3), (t**2 - 6*t + 18, True)) + assert (integrate(abs(t - s**2), (t, 0, 2)) == + 2*s**2*Min(2, s**2) - 2*s**2 - Min(2, s**2)**2 + 2) + assert integrate(exp(-Abs(t)), t) == Piecewise( + (exp(t), t <= 0), (2 - exp(-t), True)) + assert integrate(sign(2*t - 6), t) == Piecewise( + (-t, t < 3), (t - 6, True)) + assert integrate(2*t*sign(t**2 - 1), t) == Piecewise( + (t**2, t < -1), (-t**2 + 2, t < 1), (t**2, True)) + assert integrate(sign(t), (t, s + 1)) == Piecewise( + (s + 1, s + 1 > 0), (-s - 1, s + 1 < 0), (0, True)) + + +def test_subs1(): + e = Integral(exp(x - y), x) + assert e.subs(y, 3) == Integral(exp(x - 3), x) + e = Integral(exp(x - y), (x, 0, 1)) + assert e.subs(y, 3) == Integral(exp(x - 3), (x, 0, 1)) + f = Lambda(x, exp(-x**2)) + conv = Integral(f(x - y)*f(y), (y, -oo, oo)) + assert conv.subs({x: 0}) == Integral(exp(-2*y**2), (y, -oo, oo)) + + +def test_subs2(): + e = Integral(exp(x - y), x, t) + assert e.subs(y, 3) == Integral(exp(x - 3), x, t) + e = Integral(exp(x - y), (x, 0, 1), (t, 0, 1)) + assert e.subs(y, 3) == Integral(exp(x - 3), (x, 0, 1), (t, 0, 1)) + f = Lambda(x, exp(-x**2)) + conv = Integral(f(x - y)*f(y), (y, -oo, oo), (t, 0, 1)) + assert conv.subs({x: 0}) == Integral(exp(-2*y**2), (y, -oo, oo), (t, 0, 1)) + + +def test_subs3(): + e = Integral(exp(x - y), (x, 0, y), (t, y, 1)) + assert e.subs(y, 3) == Integral(exp(x - 3), (x, 0, 3), (t, 3, 1)) + f = Lambda(x, exp(-x**2)) + conv = Integral(f(x - y)*f(y), (y, -oo, oo), (t, x, 1)) + assert conv.subs({x: 0}) == Integral(exp(-2*y**2), (y, -oo, oo), (t, 0, 1)) + + +def test_subs4(): + e = Integral(exp(x), (x, 0, y), (t, y, 1)) + assert e.subs(y, 3) == Integral(exp(x), (x, 0, 3), (t, 3, 1)) + f = Lambda(x, exp(-x**2)) + conv = Integral(f(y)*f(y), (y, -oo, oo), (t, x, 1)) + assert conv.subs({x: 0}) == Integral(exp(-2*y**2), (y, -oo, oo), (t, 0, 1)) + + +def test_subs5(): + e = Integral(exp(-x**2), (x, -oo, oo)) + assert e.subs(x, 5) == e + e = Integral(exp(-x**2 + y), x) + assert e.subs(y, 5) == Integral(exp(-x**2 + 5), x) + e = Integral(exp(-x**2 + y), (x, x)) + assert e.subs(x, 5) == Integral(exp(y - x**2), (x, 5)) + assert e.subs(y, 5) == Integral(exp(-x**2 + 5), x) + e = Integral(exp(-x**2 + y), (y, -oo, oo), (x, -oo, oo)) + assert e.subs(x, 5) == e + assert e.subs(y, 5) == e + # Test evaluation of antiderivatives + e = Integral(exp(-x**2), (x, x)) + assert e.subs(x, 5) == Integral(exp(-x**2), (x, 5)) + e = Integral(exp(x), x) + assert (e.subs(x,1) - e.subs(x,0) - Integral(exp(x), (x, 0, 1)) + ).doit().is_zero + + +def test_subs6(): + a, b = symbols('a b') + e = Integral(x*y, (x, f(x), f(y))) + assert e.subs(x, 1) == Integral(x*y, (x, f(1), f(y))) + assert e.subs(y, 1) == Integral(x, (x, f(x), f(1))) + e = Integral(x*y, (x, f(x), f(y)), (y, f(x), f(y))) + assert e.subs(x, 1) == Integral(x*y, (x, f(1), f(y)), (y, f(1), f(y))) + assert e.subs(y, 1) == Integral(x*y, (x, f(x), f(y)), (y, f(x), f(1))) + e = Integral(x*y, (x, f(x), f(a)), (y, f(x), f(a))) + assert e.subs(a, 1) == Integral(x*y, (x, f(x), f(1)), (y, f(x), f(1))) + + +def test_subs7(): + e = Integral(x, (x, 1, y), (y, 1, 2)) + assert e.subs({x: 1, y: 2}) == e + e = Integral(sin(x) + sin(y), (x, sin(x), sin(y)), + (y, 1, 2)) + assert e.subs(sin(y), 1) == e + assert e.subs(sin(x), 1) == Integral(sin(x) + sin(y), (x, 1, sin(y)), + (y, 1, 2)) + +def test_expand(): + e = Integral(f(x)+f(x**2), (x, 1, y)) + assert e.expand() == Integral(f(x), (x, 1, y)) + Integral(f(x**2), (x, 1, y)) + e = Integral(f(x)+f(x**2), (x, 1, oo)) + assert e.expand() == e + assert e.expand(force=True) == Integral(f(x), (x, 1, oo)) + \ + Integral(f(x**2), (x, 1, oo)) + + +def test_integration_variable(): + raises(ValueError, lambda: Integral(exp(-x**2), 3)) + raises(ValueError, lambda: Integral(exp(-x**2), (3, -oo, oo))) + + +def test_expand_integral(): + assert Integral(cos(x**2)*(sin(x**2) + 1), (x, 0, 1)).expand() == \ + Integral(cos(x**2)*sin(x**2), (x, 0, 1)) + \ + Integral(cos(x**2), (x, 0, 1)) + assert Integral(cos(x**2)*(sin(x**2) + 1), x).expand() == \ + Integral(cos(x**2)*sin(x**2), x) + \ + Integral(cos(x**2), x) + + +def test_as_sum_midpoint1(): + e = Integral(sqrt(x**3 + 1), (x, 2, 10)) + assert e.as_sum(1, method="midpoint") == 8*sqrt(217) + assert e.as_sum(2, method="midpoint") == 4*sqrt(65) + 12*sqrt(57) + assert e.as_sum(3, method="midpoint") == 8*sqrt(217)/3 + \ + 8*sqrt(3081)/27 + 8*sqrt(52809)/27 + assert e.as_sum(4, method="midpoint") == 2*sqrt(730) + \ + 4*sqrt(7) + 4*sqrt(86) + 6*sqrt(14) + assert abs(e.as_sum(4, method="midpoint").n() - e.n()) < 0.5 + + e = Integral(sqrt(x**3 + y**3), (x, 2, 10), (y, 0, 10)) + raises(NotImplementedError, lambda: e.as_sum(4)) + + +def test_as_sum_midpoint2(): + e = Integral((x + y)**2, (x, 0, 1)) + n = Symbol('n', positive=True, integer=True) + assert e.as_sum(1, method="midpoint").expand() == Rational(1, 4) + y + y**2 + assert e.as_sum(2, method="midpoint").expand() == Rational(5, 16) + y + y**2 + assert e.as_sum(3, method="midpoint").expand() == Rational(35, 108) + y + y**2 + assert e.as_sum(4, method="midpoint").expand() == Rational(21, 64) + y + y**2 + assert e.as_sum(n, method="midpoint").expand() == \ + y**2 + y + Rational(1, 3) - 1/(12*n**2) + + +def test_as_sum_left(): + e = Integral((x + y)**2, (x, 0, 1)) + assert e.as_sum(1, method="left").expand() == y**2 + assert e.as_sum(2, method="left").expand() == Rational(1, 8) + y/2 + y**2 + assert e.as_sum(3, method="left").expand() == Rational(5, 27) + y*Rational(2, 3) + y**2 + assert e.as_sum(4, method="left").expand() == Rational(7, 32) + y*Rational(3, 4) + y**2 + assert e.as_sum(n, method="left").expand() == \ + y**2 + y + Rational(1, 3) - y/n - 1/(2*n) + 1/(6*n**2) + assert e.as_sum(10, method="left", evaluate=False).has(Sum) + + +def test_as_sum_right(): + e = Integral((x + y)**2, (x, 0, 1)) + assert e.as_sum(1, method="right").expand() == 1 + 2*y + y**2 + assert e.as_sum(2, method="right").expand() == Rational(5, 8) + y*Rational(3, 2) + y**2 + assert e.as_sum(3, method="right").expand() == Rational(14, 27) + y*Rational(4, 3) + y**2 + assert e.as_sum(4, method="right").expand() == Rational(15, 32) + y*Rational(5, 4) + y**2 + assert e.as_sum(n, method="right").expand() == \ + y**2 + y + Rational(1, 3) + y/n + 1/(2*n) + 1/(6*n**2) + + +def test_as_sum_trapezoid(): + e = Integral((x + y)**2, (x, 0, 1)) + assert e.as_sum(1, method="trapezoid").expand() == y**2 + y + S.Half + assert e.as_sum(2, method="trapezoid").expand() == y**2 + y + Rational(3, 8) + assert e.as_sum(3, method="trapezoid").expand() == y**2 + y + Rational(19, 54) + assert e.as_sum(4, method="trapezoid").expand() == y**2 + y + Rational(11, 32) + assert e.as_sum(n, method="trapezoid").expand() == \ + y**2 + y + Rational(1, 3) + 1/(6*n**2) + assert Integral(sign(x), (x, 0, 1)).as_sum(1, 'trapezoid') == S.Half + + +def test_as_sum_raises(): + e = Integral((x + y)**2, (x, 0, 1)) + raises(ValueError, lambda: e.as_sum(-1)) + raises(ValueError, lambda: e.as_sum(0)) + raises(ValueError, lambda: Integral(x).as_sum(3)) + raises(ValueError, lambda: e.as_sum(oo)) + raises(ValueError, lambda: e.as_sum(3, method='xxxx2')) + + +def test_nested_doit(): + e = Integral(Integral(x, x), x) + f = Integral(x, x, x) + assert e.doit() == f.doit() + + +def test_issue_4665(): + # Allow only upper or lower limit evaluation + e = Integral(x**2, (x, None, 1)) + f = Integral(x**2, (x, 1, None)) + assert e.doit() == Rational(1, 3) + assert f.doit() == Rational(-1, 3) + assert Integral(x*y, (x, None, y)).subs(y, t) == Integral(x*t, (x, None, t)) + assert Integral(x*y, (x, y, None)).subs(y, t) == Integral(x*t, (x, t, None)) + assert integrate(x**2, (x, None, 1)) == Rational(1, 3) + assert integrate(x**2, (x, 1, None)) == Rational(-1, 3) + assert integrate("x**2", ("x", "1", None)) == Rational(-1, 3) + + +def test_integral_reconstruct(): + e = Integral(x**2, (x, -1, 1)) + assert e == Integral(*e.args) + + +def test_doit_integrals(): + e = Integral(Integral(2*x), (x, 0, 1)) + assert e.doit() == Rational(1, 3) + assert e.doit(deep=False) == Rational(1, 3) + f = Function('f') + # doesn't matter if the integral can't be performed + assert Integral(f(x), (x, 1, 1)).doit() == 0 + # doesn't matter if the limits can't be evaluated + assert Integral(0, (x, 1, Integral(f(x), x))).doit() == 0 + assert Integral(x, (a, 0)).doit() == 0 + limits = ((a, 1, exp(x)), (x, 0)) + assert Integral(a, *limits).doit() == Rational(1, 4) + assert Integral(a, *list(reversed(limits))).doit() == 0 + + +def test_issue_4884(): + assert integrate(sqrt(x)*(1 + x)) == \ + Piecewise( + (2*sqrt(x)*(x + 1)**2/5 - 2*sqrt(x)*(x + 1)/15 - 4*sqrt(x)/15, + Abs(x + 1) > 1), + (2*I*sqrt(-x)*(x + 1)**2/5 - 2*I*sqrt(-x)*(x + 1)/15 - + 4*I*sqrt(-x)/15, True)) + assert integrate(x**x*(1 + log(x))) == x**x + +def test_issue_18153(): + assert integrate(x**n*log(x),x) == \ + Piecewise( + (n*x*x**n*log(x)/(n**2 + 2*n + 1) + + x*x**n*log(x)/(n**2 + 2*n + 1) - x*x**n/(n**2 + 2*n + 1) + , Ne(n, -1)), (log(x)**2/2, True) + ) + + +def test_is_number(): + from sympy.abc import x, y, z + assert Integral(x).is_number is False + assert Integral(1, x).is_number is False + assert Integral(1, (x, 1)).is_number is True + assert Integral(1, (x, 1, 2)).is_number is True + assert Integral(1, (x, 1, y)).is_number is False + assert Integral(1, (x, y)).is_number is False + assert Integral(x, y).is_number is False + assert Integral(x, (y, 1, x)).is_number is False + assert Integral(x, (y, 1, 2)).is_number is False + assert Integral(x, (x, 1, 2)).is_number is True + # `foo.is_number` should always be equivalent to `not foo.free_symbols` + # in each of these cases, there are pseudo-free symbols + i = Integral(x, (y, 1, 1)) + assert i.is_number is False and i.n() == 0 + i = Integral(x, (y, z, z)) + assert i.is_number is False and i.n() == 0 + i = Integral(1, (y, z, z + 2)) + assert i.is_number is False and i.n() == 2.0 + + assert Integral(x*y, (x, 1, 2), (y, 1, 3)).is_number is True + assert Integral(x*y, (x, 1, 2), (y, 1, z)).is_number is False + assert Integral(x, (x, 1)).is_number is True + assert Integral(x, (x, 1, Integral(y, (y, 1, 2)))).is_number is True + assert Integral(Sum(z, (z, 1, 2)), (x, 1, 2)).is_number is True + # it is possible to get a false negative if the integrand is + # actually an unsimplified zero, but this is true of is_number in general. + assert Integral(sin(x)**2 + cos(x)**2 - 1, x).is_number is False + assert Integral(f(x), (x, 0, 1)).is_number is True + + +def test_free_symbols(): + from sympy.abc import x, y, z + assert Integral(0, x).free_symbols == {x} + assert Integral(x).free_symbols == {x} + assert Integral(x, (x, None, y)).free_symbols == {y} + assert Integral(x, (x, y, None)).free_symbols == {y} + assert Integral(x, (x, 1, y)).free_symbols == {y} + assert Integral(x, (x, y, 1)).free_symbols == {y} + assert Integral(x, (x, x, y)).free_symbols == {x, y} + assert Integral(x, x, y).free_symbols == {x, y} + assert Integral(x, (x, 1, 2)).free_symbols == set() + assert Integral(x, (y, 1, 2)).free_symbols == {x} + # pseudo-free in this case + assert Integral(x, (y, z, z)).free_symbols == {x, z} + assert Integral(x, (y, 1, 2), (y, None, None) + ).free_symbols == {x, y} + assert Integral(x, (y, 1, 2), (x, 1, y) + ).free_symbols == {y} + assert Integral(2, (y, 1, 2), (y, 1, x), (x, 1, 2) + ).free_symbols == set() + assert Integral(2, (y, x, 2), (y, 1, x), (x, 1, 2) + ).free_symbols == set() + assert Integral(2, (x, 1, 2), (y, x, 2), (y, 1, 2) + ).free_symbols == {x} + assert Integral(f(x), (f(x), 1, y)).free_symbols == {y} + assert Integral(f(x), (f(x), 1, x)).free_symbols == {x} + + +def test_is_zero(): + from sympy.abc import x, m + assert Integral(0, (x, 1, x)).is_zero + assert Integral(1, (x, 1, 1)).is_zero + assert Integral(1, (x, 1, 2), (y, 2)).is_zero is False + assert Integral(x, (m, 0)).is_zero + assert Integral(x + m, (m, 0)).is_zero is None + i = Integral(m, (m, 1, exp(x)), (x, 0)) + assert i.is_zero is None + assert Integral(m, (x, 0), (m, 1, exp(x))).is_zero is True + + assert Integral(x, (x, oo, oo)).is_zero # issue 8171 + assert Integral(x, (x, -oo, -oo)).is_zero + + # this is zero but is beyond the scope of what is_zero + # should be doing + assert Integral(sin(x), (x, 0, 2*pi)).is_zero is None + + +def test_series(): + from sympy.abc import x + i = Integral(cos(x), (x, x)) + e = i.lseries(x) + assert i.nseries(x, n=8).removeO() == Add(*[next(e) for j in range(4)]) + + +def test_trig_nonelementary_integrals(): + x = Symbol('x') + assert integrate((1 + sin(x))/x, x) == log(x) + Si(x) + # next one comes out as log(x) + log(x**2)/2 + Ci(x) + # so not hardcoding this log ugliness + assert integrate((cos(x) + 2)/x, x).has(Ci) + + +def test_issue_4403(): + x = Symbol('x') + y = Symbol('y') + z = Symbol('z', positive=True) + assert integrate(sqrt(x**2 + z**2), x) == \ + z**2*asinh(x/z)/2 + x*sqrt(x**2 + z**2)/2 + assert integrate(sqrt(x**2 - z**2), x) == \ + x*sqrt(x**2 - z**2)/2 - z**2*log(x + sqrt(x**2 - z**2))/2 + + x = Symbol('x', real=True) + y = Symbol('y', positive=True) + assert integrate(1/(x**2 + y**2)**S('3/2'), x) == \ + x/(y**2*sqrt(x**2 + y**2)) + # If y is real and nonzero, we get x*Abs(y)/(y**3*sqrt(x**2 + y**2)), + # which results from sqrt(1 + x**2/y**2) = sqrt(x**2 + y**2)/|y|. + + +def test_issue_4403_2(): + assert integrate(sqrt(-x**2 - 4), x) == \ + -2*atan(x/sqrt(-4 - x**2)) + x*sqrt(-4 - x**2)/2 + + +def test_issue_4100(): + R = Symbol('R', positive=True) + assert integrate(sqrt(R**2 - x**2), (x, 0, R)) == pi*R**2/4 + + +def test_issue_5167(): + from sympy.abc import w, x, y, z + f = Function('f') + assert Integral(Integral(f(x), x), x) == Integral(f(x), x, x) + assert Integral(f(x)).args == (f(x), Tuple(x)) + assert Integral(Integral(f(x))).args == (f(x), Tuple(x), Tuple(x)) + assert Integral(Integral(f(x)), y).args == (f(x), Tuple(x), Tuple(y)) + assert Integral(Integral(f(x), z), y).args == (f(x), Tuple(z), Tuple(y)) + assert Integral(Integral(Integral(f(x), x), y), z).args == \ + (f(x), Tuple(x), Tuple(y), Tuple(z)) + assert integrate(Integral(f(x), x), x) == Integral(f(x), x, x) + assert integrate(Integral(f(x), y), x) == y*Integral(f(x), x) + assert integrate(Integral(f(x), x), y) in [Integral(y*f(x), x), y*Integral(f(x), x)] + assert integrate(Integral(2, x), x) == x**2 + assert integrate(Integral(2, x), y) == 2*x*y + # don't re-order given limits + assert Integral(1, x, y).args != Integral(1, y, x).args + # do as many as possible + assert Integral(f(x), y, x, y, x).doit() == y**2*Integral(f(x), x, x)/2 + assert Integral(f(x), (x, 1, 2), (w, 1, x), (z, 1, y)).doit() == \ + y*(x - 1)*Integral(f(x), (x, 1, 2)) - (x - 1)*Integral(f(x), (x, 1, 2)) + + +def test_issue_4890(): + z = Symbol('z', positive=True) + assert integrate(exp(-log(x)**2), x) == \ + sqrt(pi)*exp(Rational(1, 4))*erf(log(x) - S.Half)/2 + assert integrate(exp(log(x)**2), x) == \ + sqrt(pi)*exp(Rational(-1, 4))*erfi(log(x)+S.Half)/2 + assert integrate(exp(-z*log(x)**2), x) == \ + sqrt(pi)*exp(1/(4*z))*erf(sqrt(z)*log(x) - 1/(2*sqrt(z)))/(2*sqrt(z)) + + +def test_issue_4551(): + assert not integrate(1/(x*sqrt(1 - x**2)), x).has(Integral) + + +def test_issue_4376(): + n = Symbol('n', integer=True, positive=True) + assert simplify(integrate(n*(x**(1/n) - 1), (x, 0, S.Half)) - + (n**2 - 2**(1/n)*n**2 - n*2**(1/n))/(2**(1 + 1/n) + n*2**(1 + 1/n))) == 0 + + +def test_issue_4517(): + assert integrate((sqrt(x) - x**3)/x**Rational(1, 3), x) == \ + 6*x**Rational(7, 6)/7 - 3*x**Rational(11, 3)/11 + + +def test_issue_4527(): + k, m = symbols('k m', integer=True) + assert integrate(sin(k*x)*sin(m*x), (x, 0, pi)).simplify() == \ + Piecewise((0, Eq(k, 0) | Eq(m, 0)), + (-pi/2, Eq(k, -m) | (Eq(k, 0) & Eq(m, 0))), + (pi/2, Eq(k, m) | (Eq(k, 0) & Eq(m, 0))), + (0, True)) + # Should be possible to further simplify to: + # Piecewise( + # (0, Eq(k, 0) | Eq(m, 0)), + # (-pi/2, Eq(k, -m)), + # (pi/2, Eq(k, m)), + # (0, True)) + assert integrate(sin(k*x)*sin(m*x), (x,)) == Piecewise( + (0, And(Eq(k, 0), Eq(m, 0))), + (-x*sin(m*x)**2/2 - x*cos(m*x)**2/2 + sin(m*x)*cos(m*x)/(2*m), Eq(k, -m)), + (x*sin(m*x)**2/2 + x*cos(m*x)**2/2 - sin(m*x)*cos(m*x)/(2*m), Eq(k, m)), + (m*sin(k*x)*cos(m*x)/(k**2 - m**2) - + k*sin(m*x)*cos(k*x)/(k**2 - m**2), True)) + + +def test_issue_4199(): + ypos = Symbol('y', positive=True) + # TODO: Remove conds='none' below, let the assumption take care of it. + assert integrate(exp(-I*2*pi*ypos*x)*x, (x, -oo, oo), conds='none') == \ + Integral(exp(-I*2*pi*ypos*x)*x, (x, -oo, oo)) + + +def test_issue_3940(): + a, b, c, d = symbols('a:d', positive=True) + assert integrate(exp(-x**2 + I*c*x), x) == \ + -sqrt(pi)*exp(-c**2/4)*erf(I*c/2 - x)/2 + assert integrate(exp(a*x**2 + b*x + c), x).equals( + sqrt(pi)*exp(c - b**2/(4*a))*erfi((2*a*x + b)/(2*sqrt(a)))/(2*sqrt(a))) + + from sympy.core.function import expand_mul + from sympy.abc import k + assert expand_mul(integrate(exp(-x**2)*exp(I*k*x), (x, -oo, oo))) == \ + sqrt(pi)*exp(-k**2/4) + a, d = symbols('a d', positive=True) + assert expand_mul(integrate(exp(-a*x**2 + 2*d*x), (x, -oo, oo))) == \ + sqrt(pi)*exp(d**2/a)/sqrt(a) + + +def test_issue_5413(): + # Note that this is not the same as testing ratint() because integrate() + # pulls out the coefficient. + assert integrate(-a/(a**2 + x**2), x) == I*log(-I*a + x)/2 - I*log(I*a + x)/2 + + +def test_issue_4892a(): + A, z = symbols('A z') + c = Symbol('c', nonzero=True) + P1 = -A*exp(-z) + P2 = -A/(c*t)*(sin(x)**2 + cos(y)**2) + + h1 = -sin(x)**2 - cos(y)**2 + h2 = -sin(x)**2 + sin(y)**2 - 1 + + # there is still some non-deterministic behavior in integrate + # or trigsimp which permits one of the following + assert integrate(c*(P2 - P1), t) in [ + c*(-A*(-h1)*log(c*t)/c + A*t*exp(-z)), + c*(-A*(-h2)*log(c*t)/c + A*t*exp(-z)), + c*( A* h1 *log(c*t)/c + A*t*exp(-z)), + c*( A* h2 *log(c*t)/c + A*t*exp(-z)), + (A*c*t - A*(-h1)*log(t)*exp(z))*exp(-z), + (A*c*t - A*(-h2)*log(t)*exp(z))*exp(-z), + ] + + +def test_issue_4892b(): + # Issues relating to issue 4596 are making the actual result of this hard + # to test. The answer should be something like + # + # (-sin(y) + sqrt(-72 + 48*cos(y) - 8*cos(y)**2)/2)*log(x + sqrt(-72 + + # 48*cos(y) - 8*cos(y)**2)/(2*(3 - cos(y)))) + (-sin(y) - sqrt(-72 + + # 48*cos(y) - 8*cos(y)**2)/2)*log(x - sqrt(-72 + 48*cos(y) - + # 8*cos(y)**2)/(2*(3 - cos(y)))) + x**2*sin(y)/2 + 2*x*cos(y) + + expr = (sin(y)*x**3 + 2*cos(y)*x**2 + 12)/(x**2 + 2) + assert trigsimp(factor(integrate(expr, x).diff(x) - expr)) == 0 + + +def test_issue_5178(): + assert integrate(sin(x)*f(y, z), (x, 0, pi), (y, 0, pi), (z, 0, pi)) == \ + 2*Integral(f(y, z), (y, 0, pi), (z, 0, pi)) + + +def test_integrate_series(): + f = sin(x).series(x, 0, 10) + g = x**2/2 - x**4/24 + x**6/720 - x**8/40320 + x**10/3628800 + O(x**11) + + assert integrate(f, x) == g + assert diff(integrate(f, x), x) == f + + assert integrate(O(x**5), x) == O(x**6) + + +def test_atom_bug(): + from sympy.integrals.heurisch import heurisch + assert heurisch(meijerg([], [], [1], [], x), x) is None + + +def test_limit_bug(): + z = Symbol('z', zero=False) + assert integrate(sin(x*y*z), (x, 0, pi), (y, 0, pi)).together() == \ + (log(z) - Ci(pi**2*z) + EulerGamma + 2*log(pi))/z + + +def test_issue_4703(): + g = Function('g') + assert integrate(exp(x)*g(x), x).has(Integral) + + +def test_issue_1888(): + f = Function('f') + assert integrate(f(x).diff(x)**2, x).has(Integral) + +# The following tests work using meijerint. + + +def test_issue_3558(): + assert integrate(cos(x*y), (x, -pi/2, pi/2), (y, 0, pi)) == 2*Si(pi**2/2) + + +def test_issue_4422(): + assert integrate(1/sqrt(16 + 4*x**2), x) == asinh(x/2) / 2 + + +def test_issue_4493(): + assert simplify(integrate(x*sqrt(1 + 2*x), x)) == \ + sqrt(2*x + 1)*(6*x**2 + x - 1)/15 + + +def test_issue_4737(): + assert integrate(sin(x)/x, (x, -oo, oo)) == pi + assert integrate(sin(x)/x, (x, 0, oo)) == pi/2 + assert integrate(sin(x)/x, x) == Si(x) + + +def test_issue_4992(): + # Note: psi in _check_antecedents becomes NaN. + from sympy.core.function import expand_func + a = Symbol('a', positive=True) + assert simplify(expand_func(integrate(exp(-x)*log(x)*x**a, (x, 0, oo)))) == \ + (a*polygamma(0, a) + 1)*gamma(a) + + +def test_issue_4487(): + from sympy.functions.special.gamma_functions import lowergamma + assert simplify(integrate(exp(-x)*x**y, x)) == lowergamma(y + 1, x) + + +def test_issue_4215(): + x = Symbol("x") + assert integrate(1/(x**2), (x, -1, 1)) is oo + + +def test_issue_4400(): + n = Symbol('n', integer=True, positive=True) + assert integrate((x**n)*log(x), x) == \ + n*x*x**n*log(x)/(n**2 + 2*n + 1) + x*x**n*log(x)/(n**2 + 2*n + 1) - \ + x*x**n/(n**2 + 2*n + 1) + + +def test_issue_6253(): + # Note: this used to raise NotImplementedError + # Note: psi in _check_antecedents becomes NaN. + assert integrate((sqrt(1 - x) + sqrt(1 + x))**2/x, x, meijerg=True) == \ + Integral((sqrt(-x + 1) + sqrt(x + 1))**2/x, x) + + +def test_issue_4153(): + assert integrate(1/(1 + x + y + z), (x, 0, 1), (y, 0, 1), (z, 0, 1)) in [ + -12*log(3) - 3*log(6)/2 + 3*log(8)/2 + 5*log(2) + 7*log(4), + 6*log(2) + 8*log(4) - 27*log(3)/2, 22*log(2) - 27*log(3)/2, + -12*log(3) - 3*log(6)/2 + 47*log(2)/2] + + +def test_issue_4326(): + R, b, h = symbols('R b h') + # It doesn't matter if we can do the integral. Just make sure the result + # doesn't contain nan. This is really a test against _eval_interval. + e = integrate(((h*(x - R + b))/b)*sqrt(R**2 - x**2), (x, R - b, R)) + assert not e.has(nan) + # See that it evaluates + assert not e.has(Integral) + + +def test_powers(): + assert integrate(2**x + 3**x, x) == 2**x/log(2) + 3**x/log(3) + + +def test_manual_option(): + raises(ValueError, lambda: integrate(1/x, x, manual=True, meijerg=True)) + # an example of a function that manual integration cannot handle + assert integrate(log(1+x)/x, (x, 0, 1), manual=True).has(Integral) + + +def test_meijerg_option(): + raises(ValueError, lambda: integrate(1/x, x, meijerg=True, risch=True)) + # an example of a function that meijerg integration cannot handle + assert integrate(tan(x), x, meijerg=True) == Integral(tan(x), x) + + +def test_risch_option(): + # risch=True only allowed on indefinite integrals + raises(ValueError, lambda: integrate(1/log(x), (x, 0, oo), risch=True)) + assert integrate(exp(-x**2), x, risch=True) == NonElementaryIntegral(exp(-x**2), x) + assert integrate(log(1/x)*y, x, y, risch=True) == y**2*(x*log(1/x)/2 + x/2) + assert integrate(erf(x), x, risch=True) == Integral(erf(x), x) + # TODO: How to test risch=False? + + +@slow +def test_heurisch_option(): + raises(ValueError, lambda: integrate(1/x, x, risch=True, heurisch=True)) + # an integral that heurisch can handle + assert integrate(exp(x**2), x, heurisch=True) == sqrt(pi)*erfi(x)/2 + # an integral that heurisch currently cannot handle + assert integrate(exp(x)/x, x, heurisch=True) == Integral(exp(x)/x, x) + # an integral where heurisch currently hangs, issue 15471 + assert integrate(log(x)*cos(log(x))/x**Rational(3, 4), x, heurisch=False) == ( + -128*x**Rational(1, 4)*sin(log(x))/289 + 240*x**Rational(1, 4)*cos(log(x))/289 + + (16*x**Rational(1, 4)*sin(log(x))/17 + 4*x**Rational(1, 4)*cos(log(x))/17)*log(x)) + + +def test_issue_6828(): + f = 1/(1.08*x**2 - 4.3) + g = integrate(f, x).diff(x) + assert verify_numerically(f, g, tol=1e-12) + + +def test_issue_4803(): + x_max = Symbol("x_max") + assert integrate(y/pi*exp(-(x_max - x)/cos(a)), x) == \ + y*exp((x - x_max)/cos(a))*cos(a)/pi + + +def test_issue_4234(): + assert integrate(1/sqrt(1 + tan(x)**2)) == tan(x)/sqrt(1 + tan(x)**2) + + +def test_issue_4492(): + assert simplify(integrate(x**2 * sqrt(5 - x**2), x)).factor( + deep=True) == Piecewise( + (I*(2*x**5 - 15*x**3 + 25*x - 25*sqrt(x**2 - 5)*acosh(sqrt(5)*x/5)) / + (8*sqrt(x**2 - 5)), (x > sqrt(5)) | (x < -sqrt(5))), + ((2*x**5 - 15*x**3 + 25*x - 25*sqrt(5 - x**2)*asin(sqrt(5)*x/5)) / + (-8*sqrt(-x**2 + 5)), True)) + + +def test_issue_2708(): + # This test needs to use an integration function that can + # not be evaluated in closed form. Update as needed. + f = 1/(a + z + log(z)) + integral_f = NonElementaryIntegral(f, (z, 2, 3)) + assert Integral(f, (z, 2, 3)).doit() == integral_f + assert integrate(f + exp(z), (z, 2, 3)) == integral_f - exp(2) + exp(3) + assert integrate(2*f + exp(z), (z, 2, 3)) == \ + 2*integral_f - exp(2) + exp(3) + assert integrate(exp(1.2*n*s*z*(-t + z)/t), (z, 0, x)) == \ + NonElementaryIntegral(exp(-1.2*n*s*z)*exp(1.2*n*s*z**2/t), + (z, 0, x)) + + +def test_issue_2884(): + f = (4.000002016020*x + 4.000002016020*y + 4.000006024032)*exp(10.0*x) + e = integrate(f, (x, 0.1, 0.2)) + assert str(e) == '1.86831064982608*y + 2.16387491480008' + + +def test_issue_8368i(): + from sympy.functions.elementary.complexes import arg, Abs + assert integrate(exp(-s*x)*cosh(x), (x, 0, oo)) == \ + Piecewise( + ( pi*Piecewise( + ( -s/(pi*(-s**2 + 1)), + Abs(s**2) < 1), + ( 1/(pi*s*(1 - 1/s**2)), + Abs(s**(-2)) < 1), + ( meijerg( + ((S.Half,), (0, 0)), + ((0, S.Half), (0,)), + polar_lift(s)**2), + True) + ), + s**2 > 1 + ), + ( + Integral(exp(-s*x)*cosh(x), (x, 0, oo)), + True)) + assert integrate(exp(-s*x)*sinh(x), (x, 0, oo)) == \ + Piecewise( + ( -1/(s + 1)/2 - 1/(-s + 1)/2, + And( + Abs(s) > 1, + Abs(arg(s)) < pi/2, + Abs(arg(s)) <= pi/2 + )), + ( Integral(exp(-s*x)*sinh(x), (x, 0, oo)), + True)) + + +def test_issue_8901(): + assert integrate(sinh(1.0*x)) == 1.0*cosh(1.0*x) + assert integrate(tanh(1.0*x)) == 1.0*x - 1.0*log(tanh(1.0*x) + 1) + assert integrate(tanh(x)) == x - log(tanh(x) + 1) + + +@slow +def test_issue_8945(): + assert integrate(sin(x)**3/x, (x, 0, 1)) == -Si(3)/4 + 3*Si(1)/4 + assert integrate(sin(x)**3/x, (x, 0, oo)) == pi/4 + assert integrate(cos(x)**2/x**2, x) == -Si(2*x) - cos(2*x)/(2*x) - 1/(2*x) + + +@slow +def test_issue_7130(): + i, L, a, b = symbols('i L a b') + integrand = (cos(pi*i*x/L)**2 / (a + b*x)).rewrite(exp) + assert x not in integrate(integrand, (x, 0, L)).free_symbols + + +def test_issue_10567(): + a, b, c, t = symbols('a b c t') + vt = Matrix([a*t, b, c]) + assert integrate(vt, t) == Integral(vt, t).doit() + assert integrate(vt, t) == Matrix([[a*t**2/2], [b*t], [c*t]]) + + +def test_issue_11742(): + assert integrate(sqrt(-x**2 + 8*x + 48), (x, 4, 12)) == 16*pi + + +def test_issue_11856(): + t = symbols('t') + assert integrate(sinc(pi*t), t) == Si(pi*t)/pi + + +@slow +def test_issue_11876(): + assert integrate(sqrt(log(1/x)), (x, 0, 1)) == sqrt(pi)/2 + + +def test_issue_4950(): + assert integrate((-60*exp(x) - 19.2*exp(4*x))*exp(4*x), x) ==\ + -2.4*exp(8*x) - 12.0*exp(5*x) + + +def test_issue_4968(): + assert integrate(sin(log(x**2))) == x*sin(log(x**2))/5 - 2*x*cos(log(x**2))/5 + + +def test_singularities(): + assert integrate(1/x**2, (x, -oo, oo)) is oo + assert integrate(1/x**2, (x, -1, 1)) is oo + assert integrate(1/(x - 1)**2, (x, -2, 2)) is oo + + assert integrate(1/x**2, (x, 1, -1)) is -oo + assert integrate(1/(x - 1)**2, (x, 2, -2)) is -oo + + +def test_issue_12645(): + x, y = symbols('x y', real=True) + assert (integrate(sin(x*x*x + y*y), + (x, -sqrt(pi - y*y), sqrt(pi - y*y)), + (y, -sqrt(pi), sqrt(pi))) + == Integral(sin(x**3 + y**2), + (x, -sqrt(-y**2 + pi), sqrt(-y**2 + pi)), + (y, -sqrt(pi), sqrt(pi)))) + + +def test_issue_12677(): + assert integrate(sin(x) / (cos(x)**3), (x, 0, pi/6)) == Rational(1, 6) + + +def test_issue_14078(): + assert integrate((cos(3*x)-cos(x))/x, (x, 0, oo)) == -log(3) + + +def test_issue_14064(): + assert integrate(1/cosh(x), (x, 0, oo)) == pi/2 + + +def test_issue_14027(): + assert integrate(1/(1 + exp(x - S.Half)/(1 + exp(x))), x) == \ + x - exp(S.Half)*log(exp(x) + exp(S.Half)/(1 + exp(S.Half)))/(exp(S.Half) + E) + + +def test_issue_8170(): + assert integrate(tan(x), (x, 0, pi/2)) is S.Infinity + + +def test_issue_8440_14040(): + assert integrate(1/x, (x, -1, 1)) is S.NaN + assert integrate(1/(x + 1), (x, -2, 3)) is S.NaN + + +def test_issue_14096(): + assert integrate(1/(x + y)**2, (x, 0, 1)) == -1/(y + 1) + 1/y + assert integrate(1/(1 + x + y + z)**2, (x, 0, 1), (y, 0, 1), (z, 0, 1)) == \ + -4*log(4) - 6*log(2) + 9*log(3) + + +def test_issue_14144(): + assert Abs(integrate(1/sqrt(1 - x**3), (x, 0, 1)).n() - 1.402182) < 1e-6 + assert Abs(integrate(sqrt(1 - x**3), (x, 0, 1)).n() - 0.841309) < 1e-6 + + +def test_issue_14375(): + # This raised a TypeError. The antiderivative has exp_polar, which + # may be possible to unpolarify, so the exact output is not asserted here. + assert integrate(exp(I*x)*log(x), x).has(Ei) + + +def test_issue_14437(): + f = Function('f')(x, y, z) + assert integrate(f, (x, 0, 1), (y, 0, 2), (z, 0, 3)) == \ + Integral(f, (x, 0, 1), (y, 0, 2), (z, 0, 3)) + + +def test_issue_14470(): + assert integrate(1/sqrt(exp(x) + 1), x) == log(sqrt(exp(x) + 1) - 1) - log(sqrt(exp(x) + 1) + 1) + + +def test_issue_14877(): + f = exp(1 - exp(x**2)*x + 2*x**2)*(2*x**3 + x)/(1 - exp(x**2)*x)**2 + assert integrate(f, x) == \ + -exp(2*x**2 - x*exp(x**2) + 1)/(x*exp(3*x**2) - exp(2*x**2)) + + +def test_issue_14782(): + f = sqrt(-x**2 + 1)*(-x**2 + x) + assert integrate(f, [x, -1, 1]) == - pi / 8 + + +@slow +def test_issue_14782_slow(): + f = sqrt(-x**2 + 1)*(-x**2 + x) + assert integrate(f, [x, 0, 1]) == S.One / 3 - pi / 16 + + +def test_issue_12081(): + f = x**(Rational(-3, 2))*exp(-x) + assert integrate(f, [x, 0, oo]) is oo + + +def test_issue_15285(): + y = 1/x - 1 + f = 4*y*exp(-2*y)/x**2 + assert integrate(f, [x, 0, 1]) == 1 + + +def test_issue_15432(): + assert integrate(x**n * exp(-x) * log(x), (x, 0, oo)).gammasimp() == Piecewise( + (gamma(n + 1)*polygamma(0, n) + gamma(n + 1)/n, re(n) + 1 > 0), + (Integral(x**n*exp(-x)*log(x), (x, 0, oo)), True)) + + +def test_issue_15124(): + omega = IndexedBase('omega') + m, p = symbols('m p', cls=Idx) + assert integrate(exp(x*I*(omega[m] + omega[p])), x, conds='none') == \ + -I*exp(I*x*omega[m])*exp(I*x*omega[p])/(omega[m] + omega[p]) + + +def test_issue_15218(): + with warns_deprecated_sympy(): + Integral(Eq(x, y)) + with warns_deprecated_sympy(): + assert Integral(Eq(x, y), x) == Eq(Integral(x, x), Integral(y, x)) + with warns_deprecated_sympy(): + assert Integral(Eq(x, y), x).doit() == Eq(x**2/2, x*y) + with warns(SymPyDeprecationWarning, test_stacklevel=False): + # The warning is made in the ExprWithLimits superclass. The stacklevel + # is correct for integrate(Eq) but not Eq.integrate + assert Eq(x, y).integrate(x) == Eq(x**2/2, x*y) + + # These are not deprecated because they are definite integrals + assert integrate(Eq(x, y), (x, 0, 1)) == Eq(S.Half, y) + assert Eq(x, y).integrate((x, 0, 1)) == Eq(S.Half, y) + + +def test_issue_15292(): + res = integrate(exp(-x**2*cos(2*t)) * cos(x**2*sin(2*t)), (x, 0, oo)) + assert isinstance(res, Piecewise) + assert gammasimp((res - sqrt(pi)/2 * cos(t)).subs(t, pi/6)) == 0 + + +def test_issue_4514(): + assert integrate(sin(2*x)/sin(x), x) == 2*sin(x) + + +def test_issue_15457(): + x, a, b = symbols('x a b', real=True) + definite = integrate(exp(Abs(x-2)), (x, a, b)) + indefinite = integrate(exp(Abs(x-2)), x) + assert definite.subs({a: 1, b: 3}) == -2 + 2*E + assert indefinite.subs(x, 3) - indefinite.subs(x, 1) == -2 + 2*E + assert definite.subs({a: -3, b: -1}) == -exp(3) + exp(5) + assert indefinite.subs(x, -1) - indefinite.subs(x, -3) == -exp(3) + exp(5) + + +def test_issue_15431(): + assert integrate(x*exp(x)*log(x), x) == \ + (x*exp(x) - exp(x))*log(x) - exp(x) + Ei(x) + + +def test_issue_15640_log_substitutions(): + f = x/log(x) + F = Ei(2*log(x)) + assert integrate(f, x) == F and F.diff(x) == f + f = x**3/log(x)**2 + F = -x**4/log(x) + 4*Ei(4*log(x)) + assert integrate(f, x) == F and F.diff(x) == f + f = sqrt(log(x))/x**2 + F = -sqrt(pi)*erfc(sqrt(log(x)))/2 - sqrt(log(x))/x + assert integrate(f, x) == F and F.diff(x) == f + + +def test_issue_15509(): + from sympy.vector import CoordSys3D + N = CoordSys3D('N') + x = N.x + assert integrate(cos(a*x + b), (x, x_1, x_2), heurisch=True) == Piecewise( + (-sin(a*x_1 + b)/a + sin(a*x_2 + b)/a, (a > -oo) & (a < oo) & Ne(a, 0)), \ + (-x_1*cos(b) + x_2*cos(b), True)) + + +def test_issue_4311_fast(): + x = symbols('x', real=True) + assert integrate(x*abs(9-x**2), x) == Piecewise( + (x**4/4 - 9*x**2/2, x <= -3), + (-x**4/4 + 9*x**2/2 - Rational(81, 2), x <= 3), + (x**4/4 - 9*x**2/2, True)) + + +def test_integrate_with_complex_constants(): + K = Symbol('K', positive=True) + x = Symbol('x', real=True) + m = Symbol('m', real=True) + t = Symbol('t', real=True) + assert integrate(exp(-I*K*x**2+m*x), x) == sqrt(pi)*exp(-I*m**2 + /(4*K))*erfi((-2*I*K*x + m)/(2*sqrt(K)*sqrt(-I)))/(2*sqrt(K)*sqrt(-I)) + assert integrate(1/(1 + I*x**2), x) == (-I*(sqrt(-I)*log(x - I*sqrt(-I))/2 + - sqrt(-I)*log(x + I*sqrt(-I))/2)) + assert integrate(exp(-I*x**2), x) == sqrt(pi)*erf(sqrt(I)*x)/(2*sqrt(I)) + + assert integrate((1/(exp(I*t)-2)), t) == -t/2 - I*log(exp(I*t) - 2)/2 + assert integrate((1/(exp(I*t)-2)), (t, 0, 2*pi)) == -pi + + +def test_issue_14241(): + x = Symbol('x') + n = Symbol('n', positive=True, integer=True) + assert integrate(n * x ** (n - 1) / (x + 1), x) == \ + n**2*x**n*lerchphi(x*exp_polar(I*pi), 1, n)*gamma(n)/gamma(n + 1) + + +def test_issue_13112(): + assert integrate(sin(t)**2 / (5 - 4*cos(t)), [t, 0, 2*pi]) == pi / 4 + + +def test_issue_14709b(): + h = Symbol('h', positive=True) + i = integrate(x*acos(1 - 2*x/h), (x, 0, h)) + assert i == 5*h**2*pi/16 + + +def test_issue_8614(): + x = Symbol('x') + t = Symbol('t') + assert integrate(exp(t)/t, (t, -oo, x)) == Ei(x) + assert integrate((exp(-x) - exp(-2*x))/x, (x, 0, oo)) == log(2) + + +@slow +def test_issue_15494(): + s = symbols('s', positive=True) + + integrand = (exp(s/2) - 2*exp(1.6*s) + exp(s))*exp(s) + solution = integrate(integrand, s) + assert solution != S.NaN + # Not sure how to test this properly as it is a symbolic expression with floats + # assert str(solution) == '0.666666666666667*exp(1.5*s) + 0.5*exp(2.0*s) - 0.769230769230769*exp(2.6*s)' + # Maybe + assert abs(solution.subs(s, 1) - (-3.67440080236188)) <= 1e-8 + + integrand = (exp(s/2) - 2*exp(S(8)/5*s) + exp(s))*exp(s) + assert integrate(integrand, s) == -10*exp(13*s/5)/13 + 2*exp(3*s/2)/3 + exp(2*s)/2 + + +def test_li_integral(): + y = Symbol('y') + assert Integral(li(y*x**2), x).doit() == Piecewise((x*li(x**2*y) - \ + x*Ei(3*log(x**2*y)/2)/sqrt(x**2*y), + Ne(y, 0)), (0, True)) + + +def test_issue_17473(): + x = Symbol('x') + n = Symbol('n') + h = S.Half + ans = x**(n + 1)*gamma(h + h/n)*hyper((h + h/n,), + (3*h, 3*h + h/n), -x**(2*n)/4)/(2*n*gamma(3*h + h/n)) + got = integrate(sin(x**n), x) + assert got == ans + _x = Symbol('x', zero=False) + reps = {x: _x} + assert integrate(sin(_x**n), _x) == ans.xreplace(reps).expand() + + +def test_issue_17671(): + assert integrate(log(log(x)) / x**2, [x, 1, oo]) == -EulerGamma + assert integrate(log(log(x)) / x**3, [x, 1, oo]) == -log(2)/2 - EulerGamma/2 + assert integrate(log(log(x)) / x**10, [x, 1, oo]) == -log(9)/9 - EulerGamma/9 + + +def test_issue_2975(): + w = Symbol('w') + C = Symbol('C') + y = Symbol('y') + assert integrate(1/(y**2+C)**(S(3)/2), (y, -w/2, w/2)) == w/(C**(S(3)/2)*sqrt(1 + w**2/(4*C))) + + +def test_issue_7827(): + x, n, M = symbols('x n M') + N = Symbol('N', integer=True) + assert integrate(summation(x*n, (n, 1, N)), x) == x**2*(N**2/4 + N/4) + assert integrate(summation(x*sin(n), (n,1,N)), x) == \ + Sum(x**2*sin(n)/2, (n, 1, N)) + assert integrate(summation(sin(n*x), (n,1,N)), x) == \ + Sum(Piecewise((-cos(n*x)/n, Ne(n, 0)), (0, True)), (n, 1, N)) + assert integrate(integrate(summation(sin(n*x), (n,1,N)), x), x) == \ + Piecewise((Sum(Piecewise((-sin(n*x)/n**2, Ne(n, 0)), (-x/n, True)), + (n, 1, N)), (n > -oo) & (n < oo) & Ne(n, 0)), (0, True)) + assert integrate(Sum(x, (n, 1, M)), x) == M*x**2/2 + raises(ValueError, lambda: integrate(Sum(x, (x, y, n)), y)) + raises(ValueError, lambda: integrate(Sum(x, (x, 1, n)), n)) + raises(ValueError, lambda: integrate(Sum(x, (x, 1, y)), x)) + + +def test_issue_4231(): + f = (1 + 2*x + sqrt(x + log(x))*(1 + 3*x) + x**2)/(x*(x + sqrt(x + log(x)))*sqrt(x + log(x))) + assert integrate(f, x) == 2*sqrt(x + log(x)) + 2*log(x + sqrt(x + log(x))) + + +def test_issue_17841(): + f = diff(1/(x**2+x+I), x) + assert integrate(f, x) == 1/(x**2 + x + I) + + +def test_issue_21034(): + x = Symbol('x', real=True, nonzero=True) + f1 = x*(-x**4/asin(5)**4 - x*sinh(x + log(asin(5))) + 5) + f2 = (x + cosh(cos(4)))/(x*(x + 1/(12*x))) + + assert integrate(f1, x) == \ + -x**6/(6*asin(5)**4) - x**2*cosh(x + log(asin(5))) + 5*x**2/2 + 2*x*sinh(x + log(asin(5))) - 2*cosh(x + log(asin(5))) + + assert integrate(f2, x) == \ + log(x**2 + S(1)/12)/2 + 2*sqrt(3)*cosh(cos(4))*atan(2*sqrt(3)*x) + + +def test_issue_4187(): + assert integrate(log(x)*exp(-x), x) == Ei(-x) - exp(-x)*log(x) + assert integrate(log(x)*exp(-x), (x, 0, oo)) == -EulerGamma + + +def test_issue_5547(): + L = Symbol('L') + z = Symbol('z') + r0 = Symbol('r0') + R0 = Symbol('R0') + + assert integrate(r0**2*cos(z)**2, (z, -L/2, L/2)) == -r0**2*(-L/4 - + sin(L/2)*cos(L/2)/2) + r0**2*(L/4 + sin(L/2)*cos(L/2)/2) + + assert integrate(r0**2*cos(R0*z)**2, (z, -L/2, L/2)) == Piecewise( + (-r0**2*(-L*R0/4 - sin(L*R0/2)*cos(L*R0/2)/2)/R0 + + r0**2*(L*R0/4 + sin(L*R0/2)*cos(L*R0/2)/2)/R0, (R0 > -oo) & (R0 < oo) & Ne(R0, 0)), + (L*r0**2, True)) + + w = 2*pi*z/L + + sol = sqrt(2)*sqrt(L)*r0**2*fresnelc(sqrt(2)*sqrt(L))*gamma(S.One/4)/(16*gamma(S(5)/4)) + L*r0**2/2 + + assert integrate(r0**2*cos(w*z)**2, (z, -L/2, L/2)) == sol + + +def test_issue_15810(): + assert integrate(1/(2**(2*x/3) + 1), (x, 0, oo)) == Rational(3, 2) + + +def test_issue_21024(): + x = Symbol('x', real=True, nonzero=True) + f = log(x)*log(4*x) + log(3*x + exp(2)) + F = x*log(x)**2 + x*log(3*x + exp(2)) + x*(1 - 2*log(2)) + \ + (-2*x + 2*x*log(2))*log(x) + exp(2)*log(3*x + exp(2))/3 + assert F == integrate(f, x) + + f = (x + exp(3))/x**2 + F = log(x) - exp(3)/x + assert F == integrate(f, x) + + f = (x**2 + exp(5))/x + F = x**2/2 + exp(5)*log(x) + assert F == integrate(f, x) + + f = x/(2*x + tanh(1)) + F = x/2 - log(2*x + tanh(1))*tanh(1)/4 + assert F == integrate(f, x) + + f = x - sinh(4)/x + F = x**2/2 - log(x)*sinh(4) + assert F == integrate(f, x) + + f = log(x + exp(5)/x) + F = x*log(x + exp(5)/x) - x + 2*exp(Rational(5, 2))*atan(x*exp(Rational(-5, 2))) + assert F == integrate(f, x) + + f = x**5/(x + E) + F = x**5/5 - E*x**4/4 + x**3*exp(2)/3 - x**2*exp(3)/2 + x*exp(4) - exp(5)*log(x + E) + assert F == integrate(f, x) + + f = 4*x/(x + sinh(5)) + F = 4*x - 4*log(x + sinh(5))*sinh(5) + assert F == integrate(f, x) + + f = x**2/(2*x + sinh(2)) + F = x**2/4 - x*sinh(2)/4 + log(2*x + sinh(2))*sinh(2)**2/8 + assert F == integrate(f, x) + + f = -x**2/(x + E) + F = -x**2/2 + E*x - exp(2)*log(x + E) + assert F == integrate(f, x) + + f = (2*x + 3)*exp(5)/x + F = 2*x*exp(5) + 3*exp(5)*log(x) + assert F == integrate(f, x) + + f = x + 2 + cosh(3)/x + F = x**2/2 + 2*x + log(x)*cosh(3) + assert F == integrate(f, x) + + f = x - tanh(1)/x**3 + F = x**2/2 + tanh(1)/(2*x**2) + assert F == integrate(f, x) + + f = (3*x - exp(6))/x + F = 3*x - exp(6)*log(x) + assert F == integrate(f, x) + + f = x**4/(x + exp(5))**2 + x + F = x**3/3 + x**2*(Rational(1, 2) - exp(5)) + 3*x*exp(10) - 4*exp(15)*log(x + exp(5)) - exp(20)/(x + exp(5)) + assert F == integrate(f, x) + + f = x*(x + exp(10)/x**2) + x + F = x**3/3 + x**2/2 + exp(10)*log(x) + assert F == integrate(f, x) + + f = x + x/(5*x + sinh(3)) + F = x**2/2 + x/5 - log(5*x + sinh(3))*sinh(3)/25 + assert F == integrate(f, x) + + f = (x + exp(3))/(2*x**2 + 2*x) + F = exp(3)*log(x)/2 - exp(3)*log(x + 1)/2 + log(x + 1)/2 + assert F == integrate(f, x).expand() + + f = log(x + 4*sinh(4)) + F = x*log(x + 4*sinh(4)) - x + 4*log(x + 4*sinh(4))*sinh(4) + assert F == integrate(f, x) + + f = -x + 20*(exp(-5) - atan(4)/x)**3*sin(4)/x + F = (-x**2*exp(15)/2 + 20*log(x)*sin(4) - (-180*x**2*exp(5)*sin(4)*atan(4) + 90*x*exp(10)*sin(4)*atan(4)**2 - \ + 20*exp(15)*sin(4)*atan(4)**3)/(3*x**3))*exp(-15) + assert F == integrate(f, x) + + f = 2*x**2*exp(-4) + 6/x + F_true = (2*x**3/3 + 6*exp(4)*log(x))*exp(-4) + assert F_true == integrate(f, x) + + +def test_issue_21721(): + a = Symbol('a') + assert integrate(1/(pi*(1+(x-a)**2)),(x,-oo,oo)).expand() == \ + -Heaviside(im(a) - 1, 0) + Heaviside(im(a) + 1, 0) + + +def test_issue_21831(): + theta = symbols('theta') + assert integrate(cos(3*theta)/(5-4*cos(theta)), (theta, 0, 2*pi)) == pi/12 + integrand = cos(2*theta)/(5 - 4*cos(theta)) + assert integrate(integrand, (theta, 0, 2*pi)) == pi/6 + + +@slow +def test_issue_22033_integral(): + assert integrate((x**2 - Rational(1, 4))**2 * sqrt(1 - x**2), (x, -1, 1)) == pi/32 + + +@slow +def test_issue_21671(): + assert integrate(1,(z,x**2+y**2,2-x**2-y**2),(y,-sqrt(1-x**2),sqrt(1-x**2)),(x,-1,1)) == pi + assert integrate(-4*(1 - x**2)**(S(3)/2)/3 + 2*sqrt(1 - x**2)*(2 - 2*x**2), (x, -1, 1)) == pi + + +def test_issue_18527(): + # The manual integrator can not currently solve this. Assert that it does + # not give an incorrect result involving Abs when x has real assumptions. + xr = symbols('xr', real=True) + expr = (cos(x)/(4+(sin(x))**2)) + res_real = integrate(expr.subs(x, xr), xr, manual=True).subs(xr, x) + assert integrate(expr, x, manual=True) == res_real == Integral(expr, x) + + +def test_issue_23718(): + f = 1/(b*cos(x) + a*sin(x)) + Fpos = (-log(-a/b + tan(x/2) - sqrt(a**2 + b**2)/b)/sqrt(a**2 + b**2) + +log(-a/b + tan(x/2) + sqrt(a**2 + b**2)/b)/sqrt(a**2 + b**2)) + F = Piecewise( + # XXX: The zoo case here is for a=b=0 so it should just be zoo or maybe + # it doesn't really need to be included at all given that the original + # integrand is really undefined in that case anyway. + (zoo*(-log(tan(x/2) - 1) + log(tan(x/2) + 1)), Eq(a, 0) & Eq(b, 0)), + (log(tan(x/2))/a, Eq(b, 0)), + (-I/(-I*b*sin(x) + b*cos(x)), Eq(a, -I*b)), + (I/(I*b*sin(x) + b*cos(x)), Eq(a, I*b)), + (Fpos, True), + ) + assert integrate(f, x) == F + + ap, bp = symbols('a, b', positive=True) + rep = {a: ap, b: bp} + assert integrate(f.subs(rep), x) == Fpos.subs(rep) + + +def test_issue_23566(): + i = integrate(1/sqrt(x**2-1), (x, -2, -1)) + assert i == -log(2 - sqrt(3)) + assert math.isclose(i.n(), 1.31695789692482) + + +def test_pr_23583(): + # This result from meijerg is wrong. Check whether new result is correct when this test fail. + assert integrate(1/sqrt((x - I)**2-1)) == Piecewise((acosh(x - I), Abs((x - I)**2) > 1), (-I*asin(x - I), True)) + + +def test_issue_7264(): + assert integrate(exp(x)*sqrt(1 + exp(2*x))) == sqrt(exp(2*x) + 1)*exp(x)/2 + asinh(exp(x))/2 + + +def test_issue_11254a(): + assert integrate(sech(x), (x, 0, 1)) == 2*atan(tanh(S.Half)) + + +def test_issue_11254b(): + assert integrate(csch(x), x) == log(tanh(x/2)) + assert integrate(csch(x), (x, 0, 1)) == oo + + +def test_issue_11254d(): + # (sech(x)**2).rewrite(sinh) + assert integrate(-1/sinh(x + I*pi/2, evaluate=False)**2, x) == -2/(exp(2*x) + 1) + assert integrate(cosh(x)**(-2), x) == 2*tanh(x/2)/(tanh(x/2)**2 + 1) + + +def test_issue_22863(): + i = integrate((3*x**3-x**2+2*x-4)/sqrt(x**2-3*x+2), (x, 0, 1)) + assert i == -101*sqrt(2)/8 - 135*log(3 - 2*sqrt(2))/16 + assert math.isclose(i.n(), -2.98126694400554) + + +def test_issue_9723(): + assert integrate(sqrt(x + sqrt(x))) == \ + 2*sqrt(sqrt(x) + x)*(sqrt(x)/12 + x/3 - S(1)/8) + log(2*sqrt(x) + 2*sqrt(sqrt(x) + x) + 1)/8 + assert integrate(sqrt(2*x+3+sqrt(4*x+5))**3) == \ + sqrt(2*x + sqrt(4*x + 5) + 3) * \ + (9*x/10 + 11*(4*x + 5)**(S(3)/2)/40 + sqrt(4*x + 5)/40 + (4*x + 5)**2/10 + S(11)/10)/2 + + +def test_issue_23704(): + # XXX: This is testing that an exception is not raised in risch Ideally + # manualintegrate (manual=True) would be able to compute this but + # manualintegrate is very slow for this example so we don't test that here. + assert (integrate(log(x)/x**2/(c*x**2+b*x+a),x, risch=True) + == NonElementaryIntegral(log(x)/(a*x**2 + b*x**3 + c*x**4), x)) + + +def test_exp_substitution(): + assert integrate(1/sqrt(1-exp(2*x))) == log(sqrt(1 - exp(2*x)) - 1)/2 - log(sqrt(1 - exp(2*x)) + 1)/2 + + +def test_hyperbolic(): + assert integrate(coth(x)) == x - log(tanh(x) + 1) + log(tanh(x)) + assert integrate(sech(x)) == 2*atan(tanh(x/2)) + assert integrate(csch(x)) == log(tanh(x/2)) + + +def test_nested_pow(): + assert integrate(sqrt(x**2)) == x*sqrt(x**2)/2 + assert integrate(sqrt(x**(S(5)/3))) == 6*x*sqrt(x**(S(5)/3))/11 + assert integrate(1/sqrt(x**2)) == x*log(x)/sqrt(x**2) + assert integrate(x*sqrt(x**(-4))) == x**2*sqrt(x**-4)*log(x) + + +def test_sqrt_quadratic(): + assert integrate(1/sqrt(3*x**2+4*x+5)) == sqrt(3)*asinh(3*sqrt(11)*(x + S(2)/3)/11)/3 + assert integrate(1/sqrt(-3*x**2+4*x+5)) == sqrt(3)*asin(3*sqrt(19)*(x - S(2)/3)/19)/3 + assert integrate(1/sqrt(3*x**2+4*x-5)) == sqrt(3)*log(6*x + 2*sqrt(3)*sqrt(3*x**2 + 4*x - 5) + 4)/3 + assert integrate(1/sqrt(4*x**2-4*x+1)) == (x - S.Half)*log(x - S.Half)/(2*sqrt((x - S.Half)**2)) + assert integrate(1/sqrt(a+b*x+c*x**2), x) == \ + Piecewise((log(b + 2*sqrt(c)*sqrt(a + b*x + c*x**2) + 2*c*x)/sqrt(c), Ne(c, 0) & Ne(a - b**2/(4*c), 0)), + ((b/(2*c) + x)*log(b/(2*c) + x)/sqrt(c*(b/(2*c) + x)**2), Ne(c, 0)), + (2*sqrt(a + b*x)/b, Ne(b, 0)), (x/sqrt(a), True)) + + assert integrate((7*x+6)/sqrt(3*x**2+4*x+5)) == \ + 7*sqrt(3*x**2 + 4*x + 5)/3 + 4*sqrt(3)*asinh(3*sqrt(11)*(x + S(2)/3)/11)/9 + assert integrate((7*x+6)/sqrt(-3*x**2+4*x+5)) == \ + -7*sqrt(-3*x**2 + 4*x + 5)/3 + 32*sqrt(3)*asin(3*sqrt(19)*(x - S(2)/3)/19)/9 + assert integrate((7*x+6)/sqrt(3*x**2+4*x-5)) == \ + 7*sqrt(3*x**2 + 4*x - 5)/3 + 4*sqrt(3)*log(6*x + 2*sqrt(3)*sqrt(3*x**2 + 4*x - 5) + 4)/9 + assert integrate((d+e*x)/sqrt(a+b*x+c*x**2), x) == \ + Piecewise(((-b*e/(2*c) + d) * + Piecewise((log(b + 2*sqrt(c)*sqrt(a + b*x + c*x**2) + 2*c*x)/sqrt(c), Ne(a - b**2/(4*c), 0)), + ((b/(2*c) + x)*log(b/(2*c) + x)/sqrt(c*(b/(2*c) + x)**2), True)) + + e*sqrt(a + b*x + c*x**2)/c, Ne(c, 0)), + ((2*d*sqrt(a + b*x) + 2*e*(-a*sqrt(a + b*x) + (a + b*x)**(S(3)/2)/3)/b)/b, Ne(b, 0)), + ((d*x + e*x**2/2)/sqrt(a), True)) + + assert integrate((3*x**3-x**2+2*x-4)/sqrt(x**2-3*x+2)) == \ + sqrt(x**2 - 3*x + 2)*(x**2 + 13*x/4 + S(101)/8) + 135*log(2*x + 2*sqrt(x**2 - 3*x + 2) - 3)/16 + + assert integrate(sqrt(53225*x**2-66732*x+23013)) == \ + (x/2 - S(16683)/53225)*sqrt(53225*x**2 - 66732*x + 23013) + \ + 111576969*sqrt(2129)*asinh(53225*x/10563 - S(11122)/3521)/1133160250 + assert integrate(sqrt(a+b*x+c*x**2), x) == \ + Piecewise(((a/2 - b**2/(8*c)) * + Piecewise((log(b + 2*sqrt(c)*sqrt(a + b*x + c*x**2) + 2*c*x)/sqrt(c), Ne(a - b**2/(4*c), 0)), + ((b/(2*c) + x)*log(b/(2*c) + x)/sqrt(c*(b/(2*c) + x)**2), True)) + + (b/(4*c) + x/2)*sqrt(a + b*x + c*x**2), Ne(c, 0)), + (2*(a + b*x)**(S(3)/2)/(3*b), Ne(b, 0)), + (sqrt(a)*x, True)) + + assert integrate(x*sqrt(x**2+2*x+4)) == \ + (x**2/3 + x/6 + S(5)/6)*sqrt(x**2 + 2*x + 4) - 3*asinh(sqrt(3)*(x + 1)/3)/2 + + +def test_mul_pow_derivative(): + assert integrate(x*sec(x)*tan(x)) == x*sec(x) - log(tan(x) + sec(x)) + assert integrate(x*sec(x)**2, x) == x*tan(x) + log(cos(x)) + assert integrate(x**3*Derivative(f(x), (x, 4))) == \ + x**3*Derivative(f(x), (x, 3)) - 3*x**2*Derivative(f(x), (x, 2)) + 6*x*Derivative(f(x), x) - 6*f(x) + + +def test_issue_20782(): + fun1 = Piecewise((0, x < 0.0), (1, True)) + fun2 = -Piecewise((0, x < 1.0), (1, True)) + fun_sum = fun1 + fun2 + L = (x, -float('Inf'), 1) + + assert integrate(fun1, L) == 1 + assert integrate(fun2, L) == 0 + assert integrate(-fun1, L) == -1 + assert integrate(-fun2, L) == 0 + assert integrate(fun_sum, L) == 1. + assert integrate(-fun_sum, L) == -1. + + +def test_issue_20781(): + P = lambda a: Piecewise((0, x < a), (1, x >= a)) + f = lambda a: P(int(a)) + P(float(a)) + L = (x, -float('Inf'), x) + f1 = integrate(f(1), L) + assert f1 == 2*x - Min(1.0, x) - Min(x, Max(1.0, 1, evaluate=False)) + # XXX is_zero is True for S(0) and Float(0) and this is baked into + # the code more deeply than the issue of Float(0) != S(0) + assert integrate(f(0), (x, -float('Inf'), x) + ) == 2*x - 2*Min(0, x) + + +@slow +def test_issue_19427(): + # + x = Symbol("x") + + # Have always been okay: + assert integrate((x ** 4) * sqrt(1 - x ** 2), (x, -1, 1)) == pi / 16 + assert integrate((-2 * x ** 2) * sqrt(1 - x ** 2), (x, -1, 1)) == -pi / 4 + assert integrate((1) * sqrt(1 - x ** 2), (x, -1, 1)) == pi / 2 + + # Sum of the above, used to incorrectly return 0 for a while: + assert integrate((x ** 4 - 2 * x ** 2 + 1) * sqrt(1 - x ** 2), (x, -1, 1)) == 5 * pi / 16 + + +def test_issue_23942(): + I1 = Integral(1/sqrt(a*(1 + x)**3 + (1 + x)**2), (x, 0, z)) + assert I1.series(a, 1, n=1) == Integral(1/sqrt(x**3 + 4*x**2 + 5*x + 2), (x, 0, z)) + O(a - 1, (a, 1)) + I2 = Integral(1/sqrt(a*(4 - x)**4 + (5 + x)**2), (x, 0, z)) + assert I2.series(a, 2, n=1) == Integral(1/sqrt(2*x**4 - 32*x**3 + 193*x**2 - 502*x + 537), (x, 0, z)) + O(a - 2, (a, 2)) + + +def test_issue_25886(): + # https://github.com/sympy/sympy/issues/25886 + f = (1-x)*exp(0.937098661j*x) + F_exp = (1.0*(-1.0671234968289*I*y + + 1.13875255748434 + + 1.0671234968289*I)*exp(0.937098661*I*y) + - 1.13875255748434*exp(0.937098661*I)) + F = integrate(f, (x, y, 1.0)) + assert F.is_same(F_exp, math.isclose) + + +def test_old_issues(): + # https://github.com/sympy/sympy/issues/5212 + I1 = integrate(cos(log(x**2))/x) + assert I1 == sin(log(x**2))/2 + # https://github.com/sympy/sympy/issues/5462 + I2 = integrate(1/(x**2+y**2)**(Rational(3,2)),x) + assert I2 == x/(y**3*sqrt(x**2/y**2 + 1)) + # https://github.com/sympy/sympy/issues/6278 + I3 = integrate(1/(cos(x)+2),(x,0,2*pi)) + assert I3 == 2*sqrt(3)*pi/3 + + +def test_integral_issue_26566(): + # Define the symbols + x = symbols('x', real=True) + a = symbols('a', real=True, positive=True) + + # Define the integral expression + integral_expr = sin(a * (x + pi))**2 + symbolic_result = integrate(integral_expr, (x, -pi, -pi/2)) + + # Known correct result + correct_result = pi / 4 + + # Substitute a specific value for 'a' to evaluate both results + a_value = 1 + numeric_symbolic_result = symbolic_result.subs(a, a_value).evalf() + numeric_correct_result = correct_result.evalf() + + # Assert that the symbolic result matches the correct value + assert simplify(numeric_symbolic_result - numeric_correct_result) == 0 + + +def test_definite_integral_with_floats_issue_27231(): + # Define the symbol and the integral expression + x = symbols('x', real=True) + integral_expr = sqrt(1 - 0.5625 * (x + 0.333333333333333) ** 2) + + # Perform the definite integral with the known limits + result_symbolic = integrate(integral_expr, (x, -1, 1)) + result_numeric = result_symbolic.evalf() + + # Expected result with higher precision + expected_result = sqrt(3) / 6 + 4 * pi / 9 + + # Verify that the result is approximately equal within a larger tolerance + assert abs(result_numeric - expected_result.evalf()) < 1e-8 + + +def test_issue_27374(): + #https://github.com/sympy/sympy/issues/27374 + r = sqrt(x**2 + z**2) + u = erf(a*r/sqrt(2))/r + Ec = diff(u, z, z).subs([(x, sqrt(b*b-z*z))]) + expected_result = -2*sqrt(2)*b*a**3*exp(-b**2*a**2/2)/(3*sqrt(pi)) + assert simplify(integrate(Ec, (z, -b, b))) == expected_result diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_intpoly.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_intpoly.py new file mode 100644 index 0000000000000000000000000000000000000000..ddbaad1fbdeca53ccab8e8b22758a6ad2d89836e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_intpoly.py @@ -0,0 +1,627 @@ +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.miscellaneous import sqrt + +from sympy.core import S, Rational + +from sympy.integrals.intpoly import (decompose, best_origin, distance_to_side, + polytope_integrate, point_sort, + hyperplane_parameters, main_integrate3d, + main_integrate, polygon_integrate, + lineseg_integrate, integration_reduction, + integration_reduction_dynamic, is_vertex) + +from sympy.geometry.line import Segment2D +from sympy.geometry.polygon import Polygon +from sympy.geometry.point import Point, Point2D +from sympy.abc import x, y, z + +from sympy.testing.pytest import slow + + +def test_decompose(): + assert decompose(x) == {1: x} + assert decompose(x**2) == {2: x**2} + assert decompose(x*y) == {2: x*y} + assert decompose(x + y) == {1: x + y} + assert decompose(x**2 + y) == {1: y, 2: x**2} + assert decompose(8*x**2 + 4*y + 7) == {0: 7, 1: 4*y, 2: 8*x**2} + assert decompose(x**2 + 3*y*x) == {2: x**2 + 3*x*y} + assert decompose(9*x**2 + y + 4*x + x**3 + y**2*x + 3) ==\ + {0: 3, 1: 4*x + y, 2: 9*x**2, 3: x**3 + x*y**2} + + assert decompose(x, True) == {x} + assert decompose(x ** 2, True) == {x**2} + assert decompose(x * y, True) == {x * y} + assert decompose(x + y, True) == {x, y} + assert decompose(x ** 2 + y, True) == {y, x ** 2} + assert decompose(8 * x ** 2 + 4 * y + 7, True) == {7, 4*y, 8*x**2} + assert decompose(x ** 2 + 3 * y * x, True) == {x ** 2, 3 * x * y} + assert decompose(9 * x ** 2 + y + 4 * x + x ** 3 + y ** 2 * x + 3, True) == \ + {3, y, 4*x, 9*x**2, x*y**2, x**3} + + +def test_best_origin(): + expr1 = y ** 2 * x ** 5 + y ** 5 * x ** 7 + 7 * x + x ** 12 + y ** 7 * x + + l1 = Segment2D(Point(0, 3), Point(1, 1)) + l2 = Segment2D(Point(S(3) / 2, 0), Point(S(3) / 2, 3)) + l3 = Segment2D(Point(0, S(3) / 2), Point(3, S(3) / 2)) + l4 = Segment2D(Point(0, 2), Point(2, 0)) + l5 = Segment2D(Point(0, 2), Point(1, 1)) + l6 = Segment2D(Point(2, 0), Point(1, 1)) + + assert best_origin((2, 1), 3, l1, expr1) == (0, 3) + # XXX: Should these return exact Rational output? Maybe best_origin should + # sympify its arguments... + assert best_origin((2, 0), 3, l2, x ** 7) == (1.5, 0) + assert best_origin((0, 2), 3, l3, x ** 7) == (0, 1.5) + assert best_origin((1, 1), 2, l4, x ** 7 * y ** 3) == (0, 2) + assert best_origin((1, 1), 2, l4, x ** 3 * y ** 7) == (2, 0) + assert best_origin((1, 1), 2, l5, x ** 2 * y ** 9) == (0, 2) + assert best_origin((1, 1), 2, l6, x ** 9 * y ** 2) == (2, 0) + + +@slow +def test_polytope_integrate(): + # Convex 2-Polytopes + # Vertex representation + assert polytope_integrate(Polygon(Point(0, 0), Point(0, 2), + Point(4, 0)), 1) == 4 + assert polytope_integrate(Polygon(Point(0, 0), Point(0, 1), + Point(1, 1), Point(1, 0)), x * y) ==\ + Rational(1, 4) + assert polytope_integrate(Polygon(Point(0, 3), Point(5, 3), Point(1, 1)), + 6*x**2 - 40*y) == Rational(-935, 3) + + assert polytope_integrate(Polygon(Point(0, 0), Point(0, sqrt(3)), + Point(sqrt(3), sqrt(3)), + Point(sqrt(3), 0)), 1) == 3 + + hexagon = Polygon(Point(0, 0), Point(-sqrt(3) / 2, S.Half), + Point(-sqrt(3) / 2, S(3) / 2), Point(0, 2), + Point(sqrt(3) / 2, S(3) / 2), Point(sqrt(3) / 2, S.Half)) + + assert polytope_integrate(hexagon, 1) == S(3*sqrt(3)) / 2 + + # Hyperplane representation + assert polytope_integrate([((-1, 0), 0), ((1, 2), 4), + ((0, -1), 0)], 1) == 4 + assert polytope_integrate([((-1, 0), 0), ((0, 1), 1), + ((1, 0), 1), ((0, -1), 0)], x * y) == Rational(1, 4) + assert polytope_integrate([((0, 1), 3), ((1, -2), -1), + ((-2, -1), -3)], 6*x**2 - 40*y) == Rational(-935, 3) + assert polytope_integrate([((-1, 0), 0), ((0, sqrt(3)), 3), + ((sqrt(3), 0), 3), ((0, -1), 0)], 1) == 3 + + hexagon = [((Rational(-1, 2), -sqrt(3) / 2), 0), + ((-1, 0), sqrt(3) / 2), + ((Rational(-1, 2), sqrt(3) / 2), sqrt(3)), + ((S.Half, sqrt(3) / 2), sqrt(3)), + ((1, 0), sqrt(3) / 2), + ((S.Half, -sqrt(3) / 2), 0)] + assert polytope_integrate(hexagon, 1) == S(3*sqrt(3)) / 2 + + # Non-convex polytopes + # Vertex representation + assert polytope_integrate(Polygon(Point(-1, -1), Point(-1, 1), + Point(1, 1), Point(0, 0), + Point(1, -1)), 1) == 3 + assert polytope_integrate(Polygon(Point(-1, -1), Point(-1, 1), + Point(0, 0), Point(1, 1), + Point(1, -1), Point(0, 0)), 1) == 2 + # Hyperplane representation + assert polytope_integrate([((-1, 0), 1), ((0, 1), 1), ((1, -1), 0), + ((1, 1), 0), ((0, -1), 1)], 1) == 3 + assert polytope_integrate([((-1, 0), 1), ((1, 1), 0), ((-1, 1), 0), + ((1, 0), 1), ((-1, -1), 0), + ((1, -1), 0)], 1) == 2 + + # Tests for 2D polytopes mentioned in Chin et al(Page 10): + # http://dilbert.engr.ucdavis.edu/~suku/quadrature/cls-integration.pdf + fig1 = Polygon(Point(1.220, -0.827), Point(-1.490, -4.503), + Point(-3.766, -1.622), Point(-4.240, -0.091), + Point(-3.160, 4), Point(-0.981, 4.447), + Point(0.132, 4.027)) + assert polytope_integrate(fig1, x**2 + x*y + y**2) ==\ + S(2031627344735367)/(8*10**12) + + fig2 = Polygon(Point(4.561, 2.317), Point(1.491, -1.315), + Point(-3.310, -3.164), Point(-4.845, -3.110), + Point(-4.569, 1.867)) + assert polytope_integrate(fig2, x**2 + x*y + y**2) ==\ + S(517091313866043)/(16*10**11) + + fig3 = Polygon(Point(-2.740, -1.888), Point(-3.292, 4.233), + Point(-2.723, -0.697), Point(-0.643, -3.151)) + assert polytope_integrate(fig3, x**2 + x*y + y**2) ==\ + S(147449361647041)/(8*10**12) + + fig4 = Polygon(Point(0.211, -4.622), Point(-2.684, 3.851), + Point(0.468, 4.879), Point(4.630, -1.325), + Point(-0.411, -1.044)) + assert polytope_integrate(fig4, x**2 + x*y + y**2) ==\ + S(180742845225803)/(10**12) + + # Tests for many polynomials with maximum degree given(2D case). + tri = Polygon(Point(0, 3), Point(5, 3), Point(1, 1)) + polys = [] + expr1 = x**9*y + x**7*y**3 + 2*x**2*y**8 + expr2 = x**6*y**4 + x**5*y**5 + 2*y**10 + expr3 = x**10 + x**9*y + x**8*y**2 + x**5*y**5 + polys.extend((expr1, expr2, expr3)) + result_dict = polytope_integrate(tri, polys, max_degree=10) + assert result_dict[expr1] == Rational(615780107, 594) + assert result_dict[expr2] == Rational(13062161, 27) + assert result_dict[expr3] == Rational(1946257153, 924) + + tri = Polygon(Point(0, 3), Point(5, 3), Point(1, 1)) + expr1 = x**7*y**1 + 2*x**2*y**6 + expr2 = x**6*y**4 + x**5*y**5 + 2*y**10 + expr3 = x**10 + x**9*y + x**8*y**2 + x**5*y**5 + polys.extend((expr1, expr2, expr3)) + assert polytope_integrate(tri, polys, max_degree=9) == \ + {x**7*y + 2*x**2*y**6: Rational(489262, 9)} + + # Tests when all integral of all monomials up to a max_degree is to be + # calculated. + assert polytope_integrate(Polygon(Point(0, 0), Point(0, 1), + Point(1, 1), Point(1, 0)), + max_degree=4) == {0: 0, 1: 1, x: S.Half, + x ** 2 * y ** 2: S.One / 9, + x ** 4: S.One / 5, + y ** 4: S.One / 5, + y: S.Half, + x * y ** 2: S.One / 6, + y ** 2: S.One / 3, + x ** 3: S.One / 4, + x ** 2 * y: S.One / 6, + x ** 3 * y: S.One / 8, + x * y: S.One / 4, + y ** 3: S.One / 4, + x ** 2: S.One / 3, + x * y ** 3: S.One / 8} + + # Tests for 3D polytopes + cube1 = [[(0, 0, 0), (0, 6, 6), (6, 6, 6), (3, 6, 0), + (0, 6, 0), (6, 0, 6), (3, 0, 0), (0, 0, 6)], + [1, 2, 3, 4], [3, 2, 5, 6], [1, 7, 5, 2], [0, 6, 5, 7], + [1, 4, 0, 7], [0, 4, 3, 6]] + assert polytope_integrate(cube1, 1) == S(162) + + # 3D Test cases in Chin et al(2015) + cube2 = [[(0, 0, 0), (0, 0, 5), (0, 5, 0), (0, 5, 5), (5, 0, 0), + (5, 0, 5), (5, 5, 0), (5, 5, 5)], + [3, 7, 6, 2], [1, 5, 7, 3], [5, 4, 6, 7], [0, 4, 5, 1], + [2, 0, 1, 3], [2, 6, 4, 0]] + + cube3 = [[(0, 0, 0), (5, 0, 0), (5, 4, 0), (3, 2, 0), (3, 5, 0), + (0, 5, 0), (0, 0, 5), (5, 0, 5), (5, 4, 5), (3, 2, 5), + (3, 5, 5), (0, 5, 5)], + [6, 11, 5, 0], [1, 7, 6, 0], [5, 4, 3, 2, 1, 0], [11, 10, 4, 5], + [10, 9, 3, 4], [9, 8, 2, 3], [8, 7, 1, 2], [7, 8, 9, 10, 11, 6]] + + cube4 = [[(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), + (S.One / 4, S.One / 4, S.One / 4)], + [0, 2, 1], [1, 3, 0], [4, 2, 3], [4, 3, 1], + [0, 1, 2], [2, 4, 1], [0, 3, 2]] + + assert polytope_integrate(cube2, x ** 2 + y ** 2 + x * y + z ** 2) ==\ + Rational(15625, 4) + assert polytope_integrate(cube3, x ** 2 + y ** 2 + x * y + z ** 2) ==\ + S(33835) / 12 + assert polytope_integrate(cube4, x ** 2 + y ** 2 + x * y + z ** 2) ==\ + S(37) / 960 + + # Test cases from Mathematica's PolyhedronData library + octahedron = [[(S.NegativeOne / sqrt(2), 0, 0), (0, S.One / sqrt(2), 0), + (0, 0, S.NegativeOne / sqrt(2)), (0, 0, S.One / sqrt(2)), + (0, S.NegativeOne / sqrt(2), 0), (S.One / sqrt(2), 0, 0)], + [3, 4, 5], [3, 5, 1], [3, 1, 0], [3, 0, 4], [4, 0, 2], + [4, 2, 5], [2, 0, 1], [5, 2, 1]] + + assert polytope_integrate(octahedron, 1) == sqrt(2) / 3 + + great_stellated_dodecahedron =\ + [[(-0.32491969623290634095, 0, 0.42532540417601993887), + (0.32491969623290634095, 0, -0.42532540417601993887), + (-0.52573111211913359231, 0, 0.10040570794311363956), + (0.52573111211913359231, 0, -0.10040570794311363956), + (-0.10040570794311363956, -0.3090169943749474241, 0.42532540417601993887), + (-0.10040570794311363956, 0.30901699437494742410, 0.42532540417601993887), + (0.10040570794311363956, -0.3090169943749474241, -0.42532540417601993887), + (0.10040570794311363956, 0.30901699437494742410, -0.42532540417601993887), + (-0.16245984811645317047, -0.5, 0.10040570794311363956), + (-0.16245984811645317047, 0.5, 0.10040570794311363956), + (0.16245984811645317047, -0.5, -0.10040570794311363956), + (0.16245984811645317047, 0.5, -0.10040570794311363956), + (-0.42532540417601993887, -0.3090169943749474241, -0.10040570794311363956), + (-0.42532540417601993887, 0.30901699437494742410, -0.10040570794311363956), + (-0.26286555605956679615, 0.1909830056250525759, -0.42532540417601993887), + (-0.26286555605956679615, -0.1909830056250525759, -0.42532540417601993887), + (0.26286555605956679615, 0.1909830056250525759, 0.42532540417601993887), + (0.26286555605956679615, -0.1909830056250525759, 0.42532540417601993887), + (0.42532540417601993887, -0.3090169943749474241, 0.10040570794311363956), + (0.42532540417601993887, 0.30901699437494742410, 0.10040570794311363956)], + [12, 3, 0, 6, 16], [17, 7, 0, 3, 13], + [9, 6, 0, 7, 8], [18, 2, 1, 4, 14], + [15, 5, 1, 2, 19], [11, 4, 1, 5, 10], + [8, 19, 2, 18, 9], [10, 13, 3, 12, 11], + [16, 14, 4, 11, 12], [13, 10, 5, 15, 17], + [14, 16, 6, 9, 18], [19, 8, 7, 17, 15]] + # Actual volume is : 0.163118960624632 + assert Abs(polytope_integrate(great_stellated_dodecahedron, 1) -\ + 0.163118960624632) < 1e-12 + + expr = x **2 + y ** 2 + z ** 2 + octahedron_five_compound = [[(0, -0.7071067811865475244, 0), + (0, 0.70710678118654752440, 0), + (0.1148764602736805918, + -0.35355339059327376220, -0.60150095500754567366), + (0.1148764602736805918, 0.35355339059327376220, + -0.60150095500754567366), + (0.18587401723009224507, + -0.57206140281768429760, 0.37174803446018449013), + (0.18587401723009224507, 0.57206140281768429760, + 0.37174803446018449013), + (0.30075047750377283683, -0.21850801222441053540, + 0.60150095500754567366), + (0.30075047750377283683, 0.21850801222441053540, + 0.60150095500754567366), + (0.48662449473386508189, -0.35355339059327376220, + -0.37174803446018449013), + (0.48662449473386508189, 0.35355339059327376220, + -0.37174803446018449013), + (-0.60150095500754567366, 0, -0.37174803446018449013), + (-0.30075047750377283683, -0.21850801222441053540, + -0.60150095500754567366), + (-0.30075047750377283683, 0.21850801222441053540, + -0.60150095500754567366), + (0.60150095500754567366, 0, 0.37174803446018449013), + (0.4156269377774534286, -0.57206140281768429760, 0), + (0.4156269377774534286, 0.57206140281768429760, 0), + (0.37174803446018449013, 0, -0.60150095500754567366), + (-0.4156269377774534286, -0.57206140281768429760, 0), + (-0.4156269377774534286, 0.57206140281768429760, 0), + (-0.67249851196395732696, -0.21850801222441053540, 0), + (-0.67249851196395732696, 0.21850801222441053540, 0), + (0.67249851196395732696, -0.21850801222441053540, 0), + (0.67249851196395732696, 0.21850801222441053540, 0), + (-0.37174803446018449013, 0, 0.60150095500754567366), + (-0.48662449473386508189, -0.35355339059327376220, + 0.37174803446018449013), + (-0.48662449473386508189, 0.35355339059327376220, + 0.37174803446018449013), + (-0.18587401723009224507, -0.57206140281768429760, + -0.37174803446018449013), + (-0.18587401723009224507, 0.57206140281768429760, + -0.37174803446018449013), + (-0.11487646027368059176, -0.35355339059327376220, + 0.60150095500754567366), + (-0.11487646027368059176, 0.35355339059327376220, + 0.60150095500754567366)], + [0, 10, 16], [23, 10, 0], [16, 13, 0], + [0, 13, 23], [16, 10, 1], [1, 10, 23], + [1, 13, 16], [23, 13, 1], [2, 4, 19], + [22, 4, 2], [2, 19, 27], [27, 22, 2], + [20, 5, 3], [3, 5, 21], [26, 20, 3], + [3, 21, 26], [29, 19, 4], [4, 22, 29], + [5, 20, 28], [28, 21, 5], [6, 8, 15], + [17, 8, 6], [6, 15, 25], [25, 17, 6], + [14, 9, 7], [7, 9, 18], [24, 14, 7], + [7, 18, 24], [8, 12, 15], [17, 12, 8], + [14, 11, 9], [9, 11, 18], [11, 14, 24], + [24, 18, 11], [25, 15, 12], [12, 17, 25], + [29, 27, 19], [20, 26, 28], [28, 26, 21], + [22, 27, 29]] + assert Abs(polytope_integrate(octahedron_five_compound, expr)) - 0.353553\ + < 1e-6 + + cube_five_compound = [[(-0.1624598481164531631, -0.5, -0.6881909602355867691), + (-0.1624598481164531631, 0.5, -0.6881909602355867691), + (0.1624598481164531631, -0.5, 0.68819096023558676910), + (0.1624598481164531631, 0.5, 0.68819096023558676910), + (-0.52573111211913359231, 0, -0.6881909602355867691), + (0.52573111211913359231, 0, 0.68819096023558676910), + (-0.26286555605956679615, -0.8090169943749474241, + -0.1624598481164531631), + (-0.26286555605956679615, 0.8090169943749474241, + -0.1624598481164531631), + (0.26286555605956680301, -0.8090169943749474241, + 0.1624598481164531631), + (0.26286555605956680301, 0.8090169943749474241, + 0.1624598481164531631), + (-0.42532540417601993887, -0.3090169943749474241, + 0.68819096023558676910), + (-0.42532540417601993887, 0.30901699437494742410, + 0.68819096023558676910), + (0.42532540417601996609, -0.3090169943749474241, + -0.6881909602355867691), + (0.42532540417601996609, 0.30901699437494742410, + -0.6881909602355867691), + (-0.6881909602355867691, -0.5, 0.1624598481164531631), + (-0.6881909602355867691, 0.5, 0.1624598481164531631), + (0.68819096023558676910, -0.5, -0.1624598481164531631), + (0.68819096023558676910, 0.5, -0.1624598481164531631), + (-0.85065080835203998877, 0, -0.1624598481164531631), + (0.85065080835203993218, 0, 0.1624598481164531631)], + [18, 10, 3, 7], [13, 19, 8, 0], [18, 0, 8, 10], + [3, 19, 13, 7], [18, 7, 13, 0], [8, 19, 3, 10], + [6, 2, 11, 18], [1, 9, 19, 12], [11, 9, 1, 18], + [6, 12, 19, 2], [1, 12, 6, 18], [11, 2, 19, 9], + [4, 14, 11, 7], [17, 5, 8, 12], [4, 12, 8, 14], + [11, 5, 17, 7], [4, 7, 17, 12], [8, 5, 11, 14], + [6, 10, 15, 4], [13, 9, 5, 16], [15, 9, 13, 4], + [6, 16, 5, 10], [13, 16, 6, 4], [15, 10, 5, 9], + [14, 15, 1, 0], [16, 17, 3, 2], [14, 2, 3, 15], + [1, 17, 16, 0], [14, 0, 16, 2], [3, 17, 1, 15]] + assert Abs(polytope_integrate(cube_five_compound, expr) - 1.25) < 1e-12 + + echidnahedron = [[(0, 0, -2.4898982848827801995), + (0, 0, 2.4898982848827802734), + (0, -4.2360679774997896964, -2.4898982848827801995), + (0, -4.2360679774997896964, 2.4898982848827802734), + (0, 4.2360679774997896964, -2.4898982848827801995), + (0, 4.2360679774997896964, 2.4898982848827802734), + (-4.0287400534704067567, -1.3090169943749474241, -2.4898982848827801995), + (-4.0287400534704067567, -1.3090169943749474241, 2.4898982848827802734), + (-4.0287400534704067567, 1.3090169943749474241, -2.4898982848827801995), + (-4.0287400534704067567, 1.3090169943749474241, 2.4898982848827802734), + (4.0287400534704069747, -1.3090169943749474241, -2.4898982848827801995), + (4.0287400534704069747, -1.3090169943749474241, 2.4898982848827802734), + (4.0287400534704069747, 1.3090169943749474241, -2.4898982848827801995), + (4.0287400534704069747, 1.3090169943749474241, 2.4898982848827802734), + (-2.4898982848827801995, -3.4270509831248422723, -2.4898982848827801995), + (-2.4898982848827801995, -3.4270509831248422723, 2.4898982848827802734), + (-2.4898982848827801995, 3.4270509831248422723, -2.4898982848827801995), + (-2.4898982848827801995, 3.4270509831248422723, 2.4898982848827802734), + (2.4898982848827802734, -3.4270509831248422723, -2.4898982848827801995), + (2.4898982848827802734, -3.4270509831248422723, 2.4898982848827802734), + (2.4898982848827802734, 3.4270509831248422723, -2.4898982848827801995), + (2.4898982848827802734, 3.4270509831248422723, 2.4898982848827802734), + (-4.7169310137059934362, -0.8090169943749474241, -1.1135163644116066184), + (-4.7169310137059934362, 0.8090169943749474241, -1.1135163644116066184), + (4.7169310137059937438, -0.8090169943749474241, 1.11351636441160673519), + (4.7169310137059937438, 0.8090169943749474241, 1.11351636441160673519), + (-4.2916056095299737777, -2.1180339887498948482, 1.11351636441160673519), + (-4.2916056095299737777, 2.1180339887498948482, 1.11351636441160673519), + (4.2916056095299737777, -2.1180339887498948482, -1.1135163644116066184), + (4.2916056095299737777, 2.1180339887498948482, -1.1135163644116066184), + (-3.6034146492943870399, 0, -3.3405490932348205213), + (3.6034146492943870399, 0, 3.3405490932348202056), + (-3.3405490932348205213, -3.4270509831248422723, 1.11351636441160673519), + (-3.3405490932348205213, 3.4270509831248422723, 1.11351636441160673519), + (3.3405490932348202056, -3.4270509831248422723, -1.1135163644116066184), + (3.3405490932348202056, 3.4270509831248422723, -1.1135163644116066184), + (-2.9152236890588002395, -2.1180339887498948482, 3.3405490932348202056), + (-2.9152236890588002395, 2.1180339887498948482, 3.3405490932348202056), + (2.9152236890588002395, -2.1180339887498948482, -3.3405490932348205213), + (2.9152236890588002395, 2.1180339887498948482, -3.3405490932348205213), + (-2.2270327288232132368, 0, -1.1135163644116066184), + (-2.2270327288232132368, -4.2360679774997896964, -1.1135163644116066184), + (-2.2270327288232132368, 4.2360679774997896964, -1.1135163644116066184), + (2.2270327288232134704, 0, 1.11351636441160673519), + (2.2270327288232134704, -4.2360679774997896964, 1.11351636441160673519), + (2.2270327288232134704, 4.2360679774997896964, 1.11351636441160673519), + (-1.8017073246471935200, -1.3090169943749474241, 1.11351636441160673519), + (-1.8017073246471935200, 1.3090169943749474241, 1.11351636441160673519), + (1.8017073246471935043, -1.3090169943749474241, -1.1135163644116066184), + (1.8017073246471935043, 1.3090169943749474241, -1.1135163644116066184), + (-1.3763819204711735382, 0, -4.7169310137059934362), + (-1.3763819204711735382, 0, 0.26286555605956679615), + (1.37638192047117353821, 0, 4.7169310137059937438), + (1.37638192047117353821, 0, -0.26286555605956679615), + (-1.1135163644116066184, -3.4270509831248422723, -3.3405490932348205213), + (-1.1135163644116066184, -0.8090169943749474241, 4.7169310137059937438), + (-1.1135163644116066184, -0.8090169943749474241, -0.26286555605956679615), + (-1.1135163644116066184, 0.8090169943749474241, 4.7169310137059937438), + (-1.1135163644116066184, 0.8090169943749474241, -0.26286555605956679615), + (-1.1135163644116066184, 3.4270509831248422723, -3.3405490932348205213), + (1.11351636441160673519, -3.4270509831248422723, 3.3405490932348202056), + (1.11351636441160673519, -0.8090169943749474241, -4.7169310137059934362), + (1.11351636441160673519, -0.8090169943749474241, 0.26286555605956679615), + (1.11351636441160673519, 0.8090169943749474241, -4.7169310137059934362), + (1.11351636441160673519, 0.8090169943749474241, 0.26286555605956679615), + (1.11351636441160673519, 3.4270509831248422723, 3.3405490932348202056), + (-0.85065080835203998877, 0, 1.11351636441160673519), + (0.85065080835203993218, 0, -1.1135163644116066184), + (-0.6881909602355867691, -0.5, -1.1135163644116066184), + (-0.6881909602355867691, 0.5, -1.1135163644116066184), + (-0.6881909602355867691, -4.7360679774997896964, -1.1135163644116066184), + (-0.6881909602355867691, -2.1180339887498948482, -1.1135163644116066184), + (-0.6881909602355867691, 2.1180339887498948482, -1.1135163644116066184), + (-0.6881909602355867691, 4.7360679774997896964, -1.1135163644116066184), + (0.68819096023558676910, -0.5, 1.11351636441160673519), + (0.68819096023558676910, 0.5, 1.11351636441160673519), + (0.68819096023558676910, -4.7360679774997896964, 1.11351636441160673519), + (0.68819096023558676910, -2.1180339887498948482, 1.11351636441160673519), + (0.68819096023558676910, 2.1180339887498948482, 1.11351636441160673519), + (0.68819096023558676910, 4.7360679774997896964, 1.11351636441160673519), + (-0.42532540417601993887, -1.3090169943749474241, -4.7169310137059934362), + (-0.42532540417601993887, -1.3090169943749474241, 0.26286555605956679615), + (-0.42532540417601993887, 1.3090169943749474241, -4.7169310137059934362), + (-0.42532540417601993887, 1.3090169943749474241, 0.26286555605956679615), + (-0.26286555605956679615, -0.8090169943749474241, 1.11351636441160673519), + (-0.26286555605956679615, 0.8090169943749474241, 1.11351636441160673519), + (0.26286555605956679615, -0.8090169943749474241, -1.1135163644116066184), + (0.26286555605956679615, 0.8090169943749474241, -1.1135163644116066184), + (0.42532540417601996609, -1.3090169943749474241, 4.7169310137059937438), + (0.42532540417601996609, -1.3090169943749474241, -0.26286555605956679615), + (0.42532540417601996609, 1.3090169943749474241, 4.7169310137059937438), + (0.42532540417601996609, 1.3090169943749474241, -0.26286555605956679615)], + [9, 66, 47], [44, 62, 77], [20, 91, 49], [33, 47, 83], + [3, 77, 84], [12, 49, 53], [36, 84, 66], [28, 53, 62], + [73, 83, 91], [15, 84, 46], [25, 64, 43], [16, 58, 72], + [26, 46, 51], [11, 43, 74], [4, 72, 91], [60, 74, 84], + [35, 91, 64], [23, 51, 58], [19, 74, 77], [79, 83, 78], + [6, 56, 40], [76, 77, 81], [21, 78, 75], [8, 40, 58], + [31, 75, 74], [42, 58, 83], [41, 81, 56], [13, 75, 43], + [27, 51, 47], [2, 89, 71], [24, 43, 62], [17, 47, 85], + [14, 71, 56], [65, 85, 75], [22, 56, 51], [34, 62, 89], + [5, 85, 78], [32, 81, 46], [10, 53, 48], [45, 78, 64], + [7, 46, 66], [18, 48, 89], [37, 66, 85], [70, 89, 81], + [29, 64, 53], [88, 74, 1], [38, 67, 48], [42, 83, 72], + [57, 1, 85], [34, 48, 62], [59, 72, 87], [19, 62, 74], + [63, 87, 67], [17, 85, 83], [52, 75, 1], [39, 87, 49], + [22, 51, 40], [55, 1, 66], [29, 49, 64], [30, 40, 69], + [13, 64, 75], [82, 69, 87], [7, 66, 51], [90, 85, 1], + [59, 69, 72], [70, 81, 71], [88, 1, 84], [73, 72, 83], + [54, 71, 68], [5, 83, 85], [50, 68, 69], [3, 84, 81], + [57, 66, 1], [30, 68, 40], [28, 62, 48], [52, 1, 74], + [23, 40, 51], [38, 48, 86], [9, 51, 66], [80, 86, 68], + [11, 74, 62], [55, 84, 1], [54, 86, 71], [35, 64, 49], + [90, 1, 75], [41, 71, 81], [39, 49, 67], [15, 81, 84], + [61, 67, 86], [21, 75, 64], [24, 53, 43], [50, 69, 0], + [37, 85, 47], [31, 43, 75], [61, 0, 67], [27, 47, 58], + [10, 67, 53], [8, 58, 69], [90, 75, 85], [45, 91, 78], + [80, 68, 0], [36, 66, 46], [65, 78, 85], [63, 0, 87], + [32, 46, 56], [20, 87, 91], [14, 56, 68], [57, 85, 66], + [33, 58, 47], [61, 86, 0], [60, 84, 77], [37, 47, 66], + [82, 0, 69], [44, 77, 89], [16, 69, 58], [18, 89, 86], + [55, 66, 84], [26, 56, 46], [63, 67, 0], [31, 74, 43], + [36, 46, 84], [50, 0, 68], [25, 43, 53], [6, 68, 56], + [12, 53, 67], [88, 84, 74], [76, 89, 77], [82, 87, 0], + [65, 75, 78], [60, 77, 74], [80, 0, 86], [79, 78, 91], + [2, 86, 89], [4, 91, 87], [52, 74, 75], [21, 64, 78], + [18, 86, 48], [23, 58, 40], [5, 78, 83], [28, 48, 53], + [6, 40, 68], [25, 53, 64], [54, 68, 86], [33, 83, 58], + [17, 83, 47], [12, 67, 49], [41, 56, 71], [9, 47, 51], + [35, 49, 91], [2, 71, 86], [79, 91, 83], [38, 86, 67], + [26, 51, 56], [7, 51, 46], [4, 87, 72], [34, 89, 48], + [15, 46, 81], [42, 72, 58], [10, 48, 67], [27, 58, 51], + [39, 67, 87], [76, 81, 89], [3, 81, 77], [8, 69, 40], + [29, 53, 49], [19, 77, 62], [22, 40, 56], [20, 49, 87], + [32, 56, 81], [59, 87, 69], [24, 62, 53], [11, 62, 43], + [14, 68, 71], [73, 91, 72], [13, 43, 64], [70, 71, 89], + [16, 72, 69], [44, 89, 62], [30, 69, 68], [45, 64, 91]] + # Actual volume is : 51.405764746872634 + assert Abs(polytope_integrate(echidnahedron, 1) - 51.4057647468726) < 1e-12 + assert Abs(polytope_integrate(echidnahedron, expr) - 253.569603474519) <\ + 1e-12 + + # Tests for many polynomials with maximum degree given(2D case). + assert polytope_integrate(cube2, [x**2, y*z], max_degree=2) == \ + {y * z: 3125 / S(4), x ** 2: 3125 / S(3)} + + assert polytope_integrate(cube2, max_degree=2) == \ + {1: 125, x: 625 / S(2), x * z: 3125 / S(4), y: 625 / S(2), + y * z: 3125 / S(4), z ** 2: 3125 / S(3), y ** 2: 3125 / S(3), + z: 625 / S(2), x * y: 3125 / S(4), x ** 2: 3125 / S(3)} + +def test_point_sort(): + assert point_sort([Point(0, 0), Point(1, 0), Point(1, 1)]) == \ + [Point2D(1, 1), Point2D(1, 0), Point2D(0, 0)] + + fig6 = Polygon((0, 0), (1, 0), (1, 1)) + assert polytope_integrate(fig6, x*y) == Rational(-1, 8) + assert polytope_integrate(fig6, x*y, clockwise = True) == Rational(1, 8) + + +def test_polytopes_intersecting_sides(): + fig5 = Polygon(Point(-4.165, -0.832), Point(-3.668, 1.568), + Point(-3.266, 1.279), Point(-1.090, -2.080), + Point(3.313, -0.683), Point(3.033, -4.845), + Point(-4.395, 4.840), Point(-1.007, -3.328)) + assert polytope_integrate(fig5, x**2 + x*y + y**2) ==\ + S(1633405224899363)/(24*10**12) + + fig6 = Polygon(Point(-3.018, -4.473), Point(-0.103, 2.378), + Point(-1.605, -2.308), Point(4.516, -0.771), + Point(4.203, 0.478)) + assert polytope_integrate(fig6, x**2 + x*y + y**2) ==\ + S(88161333955921)/(3*10**12) + + +def test_max_degree(): + polygon = Polygon((0, 0), (0, 1), (1, 1), (1, 0)) + polys = [1, x, y, x*y, x**2*y, x*y**2] + assert polytope_integrate(polygon, polys, max_degree=3) == \ + {1: 1, x: S.Half, y: S.Half, x*y: Rational(1, 4), x**2*y: Rational(1, 6), x*y**2: Rational(1, 6)} + assert polytope_integrate(polygon, polys, max_degree=2) == \ + {1: 1, x: S.Half, y: S.Half, x*y: Rational(1, 4)} + assert polytope_integrate(polygon, polys, max_degree=1) == \ + {1: 1, x: S.Half, y: S.Half} + + +def test_main_integrate3d(): + cube = [[(0, 0, 0), (0, 0, 5), (0, 5, 0), (0, 5, 5), (5, 0, 0),\ + (5, 0, 5), (5, 5, 0), (5, 5, 5)],\ + [2, 6, 7, 3], [3, 7, 5, 1], [7, 6, 4, 5], [1, 5, 4, 0],\ + [3, 1, 0, 2], [0, 4, 6, 2]] + vertices = cube[0] + faces = cube[1:] + hp_params = hyperplane_parameters(faces, vertices) + assert main_integrate3d(1, faces, vertices, hp_params) == -125 + assert main_integrate3d(1, faces, vertices, hp_params, max_degree=1) == \ + {1: -125, y: Rational(-625, 2), z: Rational(-625, 2), x: Rational(-625, 2)} + + +def test_main_integrate(): + triangle = Polygon((0, 3), (5, 3), (1, 1)) + facets = triangle.sides + hp_params = hyperplane_parameters(triangle) + assert main_integrate(x**2 + y**2, facets, hp_params) == Rational(325, 6) + assert main_integrate(x**2 + y**2, facets, hp_params, max_degree=1) == \ + {0: 0, 1: 5, y: Rational(35, 3), x: 10} + + +def test_polygon_integrate(): + cube = [[(0, 0, 0), (0, 0, 5), (0, 5, 0), (0, 5, 5), (5, 0, 0),\ + (5, 0, 5), (5, 5, 0), (5, 5, 5)],\ + [2, 6, 7, 3], [3, 7, 5, 1], [7, 6, 4, 5], [1, 5, 4, 0],\ + [3, 1, 0, 2], [0, 4, 6, 2]] + facet = cube[1] + facets = cube[1:] + vertices = cube[0] + assert polygon_integrate(facet, [(0, 1, 0), 5], 0, facets, vertices, 1, 0) == -25 + + +def test_distance_to_side(): + point = (0, 0, 0) + assert distance_to_side(point, [(0, 0, 1), (0, 1, 0)], (1, 0, 0)) == -sqrt(2)/2 + + +def test_lineseg_integrate(): + polygon = [(0, 5, 0), (5, 5, 0), (5, 5, 5), (0, 5, 5)] + line_seg = [(0, 5, 0), (5, 5, 0)] + assert lineseg_integrate(polygon, 0, line_seg, 1, 0) == 5 + assert lineseg_integrate(polygon, 0, line_seg, 0, 0) == 0 + + +def test_integration_reduction(): + triangle = Polygon(Point(0, 3), Point(5, 3), Point(1, 1)) + facets = triangle.sides + a, b = hyperplane_parameters(triangle)[0] + assert integration_reduction(facets, 0, a, b, 1, (x, y), 0) == 5 + assert integration_reduction(facets, 0, a, b, 0, (x, y), 0) == 0 + + +def test_integration_reduction_dynamic(): + triangle = Polygon(Point(0, 3), Point(5, 3), Point(1, 1)) + facets = triangle.sides + a, b = hyperplane_parameters(triangle)[0] + x0 = facets[0].points[0] + monomial_values = [[0, 0, 0, 0], [1, 0, 0, 5],\ + [y, 0, 1, 15], [x, 1, 0, None]] + + assert integration_reduction_dynamic(facets, 0, a, b, x, 1, (x, y), 1,\ + 0, 1, x0, monomial_values, 3) == Rational(25, 2) + assert integration_reduction_dynamic(facets, 0, a, b, 0, 1, (x, y), 1,\ + 0, 1, x0, monomial_values, 3) == 0 + + +def test_is_vertex(): + assert is_vertex(2) is False + assert is_vertex((2, 3)) is True + assert is_vertex(Point(2, 3)) is True + assert is_vertex((2, 3, 4)) is True + assert is_vertex((2, 3, 4, 5)) is False + + +def test_issue_19234(): + polygon = Polygon(Point(0, 0), Point(0, 1), Point(1, 1), Point(1, 0)) + polys = [ 1, x, y, x*y, x**2*y, x*y**2] + assert polytope_integrate(polygon, polys) == \ + {1: 1, x: S.Half, y: S.Half, x*y: Rational(1, 4), x**2*y: Rational(1, 6), x*y**2: Rational(1, 6)} + polys = [ 1, x, y, x*y, 3 + x**2*y, x + x*y**2] + assert polytope_integrate(polygon, polys) == \ + {1: 1, x: S.Half, y: S.Half, x*y: Rational(1, 4), x**2*y + 3: Rational(19, 6), x*y**2 + x: Rational(2, 3)} diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_laplace.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_laplace.py new file mode 100644 index 0000000000000000000000000000000000000000..cb7222d01e3dcb3e14e8d0564610ab553e637155 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_laplace.py @@ -0,0 +1,774 @@ +from sympy.integrals.laplace import ( + laplace_transform, inverse_laplace_transform, + LaplaceTransform, InverseLaplaceTransform, + _laplace_deep_collect, laplace_correspondence, + laplace_initial_conds) +from sympy.core.function import Function, expand_mul +from sympy.core import EulerGamma, Subs, Derivative, diff +from sympy.core.exprtools import factor_terms +from sympy.core.numbers import I, oo, pi +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import Symbol, symbols +from sympy.simplify.simplify import simplify +from sympy.functions.elementary.complexes import Abs, re +from sympy.functions.elementary.exponential import exp, log, exp_polar +from sympy.functions.elementary.hyperbolic import cosh, sinh, coth, asinh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import atan, cos, sin +from sympy.logic.boolalg import And +from sympy.functions.special.gamma_functions import ( + lowergamma, gamma, uppergamma) +from sympy.functions.special.delta_functions import DiracDelta, Heaviside +from sympy.functions.special.singularity_functions import SingularityFunction +from sympy.functions.special.zeta_functions import lerchphi +from sympy.functions.special.error_functions import ( + fresnelc, fresnels, erf, erfc, Ei, Ci, expint, E1) +from sympy.functions.special.bessel import besseli, besselj, besselk, bessely +from sympy.testing.pytest import slow, warns_deprecated_sympy +from sympy.matrices import Matrix, eye +from sympy.abc import s + + +@slow +def test_laplace_transform(): + LT = laplace_transform + ILT = inverse_laplace_transform + a, b, c = symbols('a, b, c', positive=True) + np = symbols('np', integer=True, positive=True) + t, w, x = symbols('t, w, x') + f = Function('f') + F = Function('F') + g = Function('g') + y = Function('y') + Y = Function('Y') + + # Test helper functions + assert ( + _laplace_deep_collect(exp((t+a)*(t+b)) + + besselj(2, exp((t+a)*(t+b)-t**2)), t) == + exp(a*b + t**2 + t*(a + b)) + besselj(2, exp(a*b + t*(a + b)))) + L = laplace_transform(diff(y(t), t, 3), t, s, noconds=True) + L = laplace_correspondence(L, {y: Y}) + L = laplace_initial_conds(L, t, {y: [2, 4, 8, 16, 32]}) + assert L == s**3*Y(s) - 2*s**2 - 4*s - 8 + # Test whether `noconds=True` in `doit`: + assert (2*LaplaceTransform(exp(t), t, s) - 1).doit() == -1 + 2/(s - 1) + assert (LT(a*t+t**2+t**(S(5)/2), t, s) == + (a/s**2 + 2/s**3 + 15*sqrt(pi)/(8*s**(S(7)/2)), 0, True)) + assert LT(b/(t+a), t, s) == (-b*exp(-a*s)*Ei(-a*s), 0, True) + assert (LT(1/sqrt(t+a), t, s) == + (sqrt(pi)*sqrt(1/s)*exp(a*s)*erfc(sqrt(a)*sqrt(s)), 0, True)) + assert (LT(sqrt(t)/(t+a), t, s) == + (-pi*sqrt(a)*exp(a*s)*erfc(sqrt(a)*sqrt(s)) + sqrt(pi)*sqrt(1/s), + 0, True)) + assert (LT((t+a)**(-S(3)/2), t, s) == + (-2*sqrt(pi)*sqrt(s)*exp(a*s)*erfc(sqrt(a)*sqrt(s)) + 2/sqrt(a), + 0, True)) + assert (LT(t**(S(1)/2)*(t+a)**(-1), t, s) == + (-pi*sqrt(a)*exp(a*s)*erfc(sqrt(a)*sqrt(s)) + sqrt(pi)*sqrt(1/s), + 0, True)) + assert (LT(1/(a*sqrt(t) + t**(3/2)), t, s) == + (pi*sqrt(a)*exp(a*s)*erfc(sqrt(a)*sqrt(s)), 0, True)) + assert (LT((t+a)**b, t, s) == + (s**(-b - 1)*exp(-a*s)*uppergamma(b + 1, a*s), 0, True)) + assert LT(t**5/(t+a), t, s) == (120*a**5*uppergamma(-5, a*s), 0, True) + assert LT(exp(t), t, s) == (1/(s - 1), 1, True) + assert LT(exp(2*t), t, s) == (1/(s - 2), 2, True) + assert LT(exp(a*t), t, s) == (1/(s - a), a, True) + assert LT(exp(a*(t-b)), t, s) == (exp(-a*b)/(-a + s), a, True) + assert LT(t*exp(-a*(t)), t, s) == ((a + s)**(-2), -a, True) + assert LT(t*exp(-a*(t-b)), t, s) == (exp(a*b)/(a + s)**2, -a, True) + assert LT(b*t*exp(-a*t), t, s) == (b/(a + s)**2, -a, True) + assert LT(exp(-a*exp(-t)), t, s) == (lowergamma(s, a)/a**s, 0, True) + assert LT(exp(-a*exp(t)), t, s) == (a**s*uppergamma(-s, a), 0, True) + assert (LT(t**(S(7)/4)*exp(-8*t)/gamma(S(11)/4), t, s) == + ((s + 8)**(-S(11)/4), -8, True)) + assert (LT(t**(S(3)/2)*exp(-8*t), t, s) == + (3*sqrt(pi)/(4*(s + 8)**(S(5)/2)), -8, True)) + assert LT(t**a*exp(-a*t), t, s) == ((a+s)**(-a-1)*gamma(a+1), -a, True) + assert (LT(b*exp(-a*t**2), t, s) == + (sqrt(pi)*b*exp(s**2/(4*a))*erfc(s/(2*sqrt(a)))/(2*sqrt(a)), + 0, True)) + assert (LT(exp(-2*t**2), t, s) == + (sqrt(2)*sqrt(pi)*exp(s**2/8)*erfc(sqrt(2)*s/4)/4, 0, True)) + assert (LT(b*exp(2*t**2), t, s) == + (b*LaplaceTransform(exp(2*t**2), t, s), -oo, True)) + assert (LT(t*exp(-a*t**2), t, s) == + (1/(2*a) - s*erfc(s/(2*sqrt(a)))/(4*sqrt(pi)*a**(S(3)/2)), + 0, True)) + assert (LT(exp(-a/t), t, s) == + (2*sqrt(a)*sqrt(1/s)*besselk(1, 2*sqrt(a)*sqrt(s)), 0, True)) + assert LT(sqrt(t)*exp(-a/t), t, s, simplify=True) == ( + sqrt(pi)*(sqrt(a)*sqrt(s) + 1/S(2))*sqrt(s**(-3)) * + exp(-2*sqrt(a)*sqrt(s)), 0, True) + assert (LT(exp(-a/t)/sqrt(t), t, s) == + (sqrt(pi)*sqrt(1/s)*exp(-2*sqrt(a)*sqrt(s)), 0, True)) + assert (LT(exp(-a/t)/(t*sqrt(t)), t, s) == + (sqrt(pi)*sqrt(1/a)*exp(-2*sqrt(a)*sqrt(s)), 0, True)) + # TODO: rules with sqrt(a*t) and sqrt(a/t) have stopped working after + # changes to as_base_exp + # assert ( + # LT(exp(-2*sqrt(a*t)), t, s) == + # (1/s - sqrt(pi)*sqrt(a) * exp(a/s)*erfc(sqrt(a)*sqrt(1/s)) / + # s**(S(3)/2), 0, True)) + # assert LT(exp(-2*sqrt(a*t))/sqrt(t), t, s) == ( + # exp(a/s)*erfc(sqrt(a) * sqrt(1/s))*(sqrt(pi)*sqrt(1/s)), 0, True) + assert (LT(t**4*exp(-2/t), t, s) == + (8*sqrt(2)*(1/s)**(S(5)/2)*besselk(5, 2*sqrt(2)*sqrt(s)), + 0, True)) + assert LT(sinh(a*t), t, s) == (a/(-a**2 + s**2), a, True) + assert (LT(b*sinh(a*t)**2, t, s) == + (2*a**2*b/(-4*a**2*s + s**3), 2*a, True)) + assert (LT(b*sinh(a*t)**2, t, s, simplify=True) == + (2*a**2*b/(s*(-4*a**2 + s**2)), 2*a, True)) + # The following line confirms that issue #21202 is solved + assert LT(cosh(2*t), t, s) == (s/(-4 + s**2), 2, True) + assert LT(cosh(a*t), t, s) == (s/(-a**2 + s**2), a, True) + assert (LT(cosh(a*t)**2, t, s, simplify=True) == + ((2*a**2 - s**2)/(s*(4*a**2 - s**2)), 2*a, True)) + assert (LT(sinh(x+3), x, s, simplify=True) == + ((s*sinh(3) + cosh(3))/(s**2 - 1), 1, True)) + L, _, _ = LT(42*sin(w*t+x)**2, t, s) + assert ( + L - + 21*(s**2 + s*(-s*cos(2*x) + 2*w*sin(2*x)) + + 4*w**2)/(s*(s**2 + 4*w**2))).simplify() == 0 + # The following line replaces the old test test_issue_7173() + assert LT(sinh(a*t)*cosh(a*t), t, s, simplify=True) == (a/(-4*a**2 + s**2), + 2*a, True) + assert LT(sinh(a*t)/t, t, s) == (log((a + s)/(-a + s))/2, a, True) + assert (LT(t**(-S(3)/2)*sinh(a*t), t, s) == + (-sqrt(pi)*(sqrt(-a + s) - sqrt(a + s)), a, True)) + # assert (LT(sinh(2*sqrt(a*t)), t, s) == + # (sqrt(pi)*sqrt(a)*exp(a/s)/s**(S(3)/2), 0, True)) + # assert (LT(sqrt(t)*sinh(2*sqrt(a*t)), t, s, simplify=True) == + # ((-sqrt(a)*s**(S(5)/2) + sqrt(pi)*s**2*(2*a + s)*exp(a/s) * + # erf(sqrt(a)*sqrt(1/s))/2)/s**(S(9)/2), 0, True)) + # assert (LT(sinh(2*sqrt(a*t))/sqrt(t), t, s) == + # (sqrt(pi)*exp(a/s)*erf(sqrt(a)*sqrt(1/s))/sqrt(s), 0, True)) + # assert (LT(sinh(sqrt(a*t))**2/sqrt(t), t, s) == + # (sqrt(pi)*(exp(a/s) - 1)/(2*sqrt(s)), 0, True)) + assert (LT(t**(S(3)/7)*cosh(a*t), t, s) == + (((a + s)**(-S(10)/7) + (-a+s)**(-S(10)/7))*gamma(S(10)/7)/2, + a, True)) + # assert (LT(cosh(2*sqrt(a*t)), t, s) == + # (sqrt(pi)*sqrt(a)*exp(a/s)*erf(sqrt(a)*sqrt(1/s))/s**(S(3)/2) + + # 1/s, 0, True)) + # assert (LT(sqrt(t)*cosh(2*sqrt(a*t)), t, s) == + # (sqrt(pi)*(a + s/2)*exp(a/s)/s**(S(5)/2), 0, True)) + # assert (LT(cosh(2*sqrt(a*t))/sqrt(t), t, s) == + # (sqrt(pi)*exp(a/s)/sqrt(s), 0, True)) + # assert (LT(cosh(sqrt(a*t))**2/sqrt(t), t, s) == + # (sqrt(pi)*(exp(a/s) + 1)/(2*sqrt(s)), 0, True)) + assert LT(log(t), t, s, simplify=True) == ( + (-log(s) - EulerGamma)/s, 0, True) + assert (LT(-log(t/a), t, s, simplify=True) == + ((log(a) + log(s) + EulerGamma)/s, 0, True)) + assert LT(log(1+a*t), t, s) == (-exp(s/a)*Ei(-s/a)/s, 0, True) + assert (LT(log(t+a), t, s, simplify=True) == + ((s*log(a) - exp(s/a)*Ei(-s/a))/s**2, 0, True)) + assert (LT(log(t)/sqrt(t), t, s, simplify=True) == + (sqrt(pi)*(-log(s) - log(4) - EulerGamma)/sqrt(s), 0, True)) + assert (LT(t**(S(5)/2)*log(t), t, s, simplify=True) == + (sqrt(pi)*(-15*log(s) - log(1073741824) - 15*EulerGamma + 46) / + (8*s**(S(7)/2)), 0, True)) + assert (LT(t**3*log(t), t, s, noconds=True, simplify=True) - + 6*(-log(s) - S.EulerGamma + S(11)/6)/s**4).simplify() == S.Zero + assert (LT(log(t)**2, t, s, simplify=True) == + (((log(s) + EulerGamma)**2 + pi**2/6)/s, 0, True)) + assert (LT(exp(-a*t)*log(t), t, s, simplify=True) == + ((-log(a + s) - EulerGamma)/(a + s), -a, True)) + assert LT(sin(a*t), t, s) == (a/(a**2 + s**2), 0, True) + assert (LT(Abs(sin(a*t)), t, s) == + (a*coth(pi*s/(2*a))/(a**2 + s**2), 0, True)) + assert LT(sin(a*t)/t, t, s) == (atan(a/s), 0, True) + assert LT(sin(a*t)**2/t, t, s) == (log(4*a**2/s**2 + 1)/4, 0, True) + assert (LT(sin(a*t)**2/t**2, t, s) == + (a*atan(2*a/s) - s*log(4*a**2/s**2 + 1)/4, 0, True)) + # assert (LT(sin(2*sqrt(a*t)), t, s) == + # (sqrt(pi)*sqrt(a)*exp(-a/s)/s**(S(3)/2), 0, True)) + # assert LT(sin(2*sqrt(a*t))/t, t, s) == (pi*erf(sqrt(a)*sqrt(1/s)), 0, True) + assert LT(cos(a*t), t, s) == (s/(a**2 + s**2), 0, True) + assert (LT(cos(a*t)**2, t, s) == + ((2*a**2 + s**2)/(s*(4*a**2 + s**2)), 0, True)) + # assert (LT(sqrt(t)*cos(2*sqrt(a*t)), t, s, simplify=True) == + # (sqrt(pi)*(-a + s/2)*exp(-a/s)/s**(S(5)/2), 0, True)) + # assert (LT(cos(2*sqrt(a*t))/sqrt(t), t, s) == + # (sqrt(pi)*sqrt(1/s)*exp(-a/s), 0, True)) + assert (LT(sin(a*t)*sin(b*t), t, s) == + (2*a*b*s/((s**2 + (a - b)**2)*(s**2 + (a + b)**2)), 0, True)) + assert (LT(cos(a*t)*sin(b*t), t, s) == + (b*(-a**2 + b**2 + s**2)/((s**2 + (a - b)**2)*(s**2 + (a + b)**2)), + 0, True)) + assert (LT(cos(a*t)*cos(b*t), t, s) == + (s*(a**2 + b**2 + s**2)/((s**2 + (a - b)**2)*(s**2 + (a + b)**2)), + 0, True)) + assert (LT(-a*t*cos(a*t) + sin(a*t), t, s, simplify=True) == + (2*a**3/(a**4 + 2*a**2*s**2 + s**4), 0, True)) + assert LT(c*exp(-b*t)*sin(a*t), t, s) == (a * + c/(a**2 + (b + s)**2), -b, True) + assert LT(c*exp(-b*t)*cos(a*t), t, s) == (c*(b + s)/(a**2 + (b + s)**2), + -b, True) + L, plane, cond = LT(cos(x + 3), x, s, simplify=True) + assert plane == 0 + assert L - (s*cos(3) - sin(3))/(s**2 + 1) == 0 + # Error functions (laplace7.pdf) + assert LT(erf(a*t), t, s) == (exp(s**2/(4*a**2))*erfc(s/(2*a))/s, 0, True) + # assert LT(erf(sqrt(a*t)), t, s) == (sqrt(a)/(s*sqrt(a + s)), 0, True) + # assert (LT(exp(a*t)*erf(sqrt(a*t)), t, s, simplify=True) == + # (-sqrt(a)/(sqrt(s)*(a - s)), a, True)) + # assert (LT(erf(sqrt(a/t)/2), t, s, simplify=True) == + # (1/s - exp(-sqrt(a)*sqrt(s))/s, 0, True)) + # assert (LT(erfc(sqrt(a*t)), t, s, simplify=True) == + # (-sqrt(a)/(s*sqrt(a + s)) + 1/s, -a, True)) + # assert (LT(exp(a*t)*erfc(sqrt(a*t)), t, s) == + # (1/(sqrt(a)*sqrt(s) + s), 0, True)) + # assert LT(erfc(sqrt(a/t)/2), t, s) == (exp(-sqrt(a)*sqrt(s))/s, 0, True) + # Bessel functions (laplace8.pdf) + assert LT(besselj(0, a*t), t, s) == (1/sqrt(a**2 + s**2), 0, True) + assert (LT(besselj(1, a*t), t, s, simplify=True) == + (a/(a**2 + s**2 + s*sqrt(a**2 + s**2)), 0, True)) + assert (LT(besselj(2, a*t), t, s, simplify=True) == + (a**2/(sqrt(a**2 + s**2)*(s + sqrt(a**2 + s**2))**2), 0, True)) + assert (LT(t*besselj(0, a*t), t, s) == + (s/(a**2 + s**2)**(S(3)/2), 0, True)) + assert (LT(t*besselj(1, a*t), t, s) == + (a/(a**2 + s**2)**(S(3)/2), 0, True)) + assert (LT(t**2*besselj(2, a*t), t, s) == + (3*a**2/(a**2 + s**2)**(S(5)/2), 0, True)) + # assert LT(besselj(0, 2*sqrt(a*t)), t, s) == (exp(-a/s)/s, 0, True) + # assert (LT(t**(S(3)/2)*besselj(3, 2*sqrt(a*t)), t, s) == + # (a**(S(3)/2)*exp(-a/s)/s**4, 0, True)) + assert (LT(besselj(0, a*sqrt(t**2+b*t)), t, s, simplify=True) == + (exp(b*(s - sqrt(a**2 + s**2)))/sqrt(a**2 + s**2), 0, True)) + assert LT(besseli(0, a*t), t, s) == (1/sqrt(-a**2 + s**2), a, True) + assert (LT(besseli(1, a*t), t, s, simplify=True) == + (a/(-a**2 + s**2 + s*sqrt(-a**2 + s**2)), a, True)) + assert (LT(besseli(2, a*t), t, s, simplify=True) == + (a**2/(sqrt(-a**2 + s**2)*(s + sqrt(-a**2 + s**2))**2), a, True)) + assert LT(t*besseli(0, a*t), t, s) == (s/(-a**2 + s**2)**(S(3)/2), a, True) + assert LT(t*besseli(1, a*t), t, s) == (a/(-a**2 + s**2)**(S(3)/2), a, True) + assert (LT(t**2*besseli(2, a*t), t, s) == + (3*a**2/(-a**2 + s**2)**(S(5)/2), a, True)) + # assert (LT(t**(S(3)/2)*besseli(3, 2*sqrt(a*t)), t, s) == + # (a**(S(3)/2)*exp(a/s)/s**4, 0, True)) + assert (LT(bessely(0, a*t), t, s) == + (-2*asinh(s/a)/(pi*sqrt(a**2 + s**2)), 0, True)) + assert (LT(besselk(0, a*t), t, s) == + (log((s + sqrt(-a**2 + s**2))/a)/sqrt(-a**2 + s**2), -a, True)) + assert (LT(sin(a*t)**4, t, s, simplify=True) == + (24*a**4/(s*(64*a**4 + 20*a**2*s**2 + s**4)), 0, True)) + # Test general rules and unevaluated forms + # These all also test whether issue #7219 is solved. + assert LT(Heaviside(t-1)*cos(t-1), t, s) == (s*exp(-s)/(s**2 + 1), 0, True) + assert LT(a*f(t), t, w) == (a*LaplaceTransform(f(t), t, w), -oo, True) + assert (LT(a*Heaviside(t+1)*f(t+1), t, s) == + (a*LaplaceTransform(f(t + 1), t, s), -oo, True)) + assert (LT(a*Heaviside(t-1)*f(t-1), t, s) == + (a*LaplaceTransform(f(t), t, s)*exp(-s), -oo, True)) + assert (LT(b*f(t/a), t, s) == + (a*b*LaplaceTransform(f(t), t, a*s), -oo, True)) + assert LT(exp(-f(x)*t), t, s) == (1/(s + f(x)), -re(f(x)), True) + assert (LT(exp(-a*t)*f(t), t, s) == + (LaplaceTransform(f(t), t, a + s), -oo, True)) + # assert (LT(exp(-a*t)*erfc(sqrt(b/t)/2), t, s) == + # (exp(-sqrt(b)*sqrt(a + s))/(a + s), -a, True)) + assert (LT(sinh(a*t)*f(t), t, s) == + (LaplaceTransform(f(t), t, -a + s)/2 - + LaplaceTransform(f(t), t, a + s)/2, -oo, True)) + assert (LT(sinh(a*t)*t, t, s, simplify=True) == + (2*a*s/(a**4 - 2*a**2*s**2 + s**4), a, True)) + assert (LT(cosh(a*t)*f(t), t, s) == + (LaplaceTransform(f(t), t, -a + s)/2 + + LaplaceTransform(f(t), t, a + s)/2, -oo, True)) + assert (LT(cosh(a*t)*t, t, s, simplify=True) == + (1/(2*(a + s)**2) + 1/(2*(a - s)**2), a, True)) + assert (LT(sin(a*t)*f(t), t, s, simplify=True) == + (I*(-LaplaceTransform(f(t), t, -I*a + s) + + LaplaceTransform(f(t), t, I*a + s))/2, -oo, True)) + assert (LT(sin(f(t)), t, s) == + (LaplaceTransform(sin(f(t)), t, s), -oo, True)) + assert (LT(sin(a*t)*t, t, s, simplify=True) == + (2*a*s/(a**4 + 2*a**2*s**2 + s**4), 0, True)) + assert (LT(cos(a*t)*f(t), t, s) == + (LaplaceTransform(f(t), t, -I*a + s)/2 + + LaplaceTransform(f(t), t, I*a + s)/2, -oo, True)) + assert (LT(cos(a*t)*t, t, s, simplify=True) == + ((-a**2 + s**2)/(a**4 + 2*a**2*s**2 + s**4), 0, True)) + L, plane, _ = LT(sin(a*t+b)**2*f(t), t, s) + assert plane == -oo + assert ( + -L + ( + LaplaceTransform(f(t), t, s)/2 - + LaplaceTransform(f(t), t, -2*I*a + s)*exp(2*I*b)/4 - + LaplaceTransform(f(t), t, 2*I*a + s)*exp(-2*I*b)/4)) == 0 + L = LT(sin(a*t+b)**2*f(t), t, s, noconds=True) + assert ( + laplace_correspondence(L, {f: F}) == + F(s)/2 - F(-2*I*a + s)*exp(2*I*b)/4 - + F(2*I*a + s)*exp(-2*I*b)/4) + L, plane, _ = LT(sin(a*t)**3*cosh(b*t), t, s) + assert plane == b + assert ( + -L - 3*a/(8*(9*a**2 + b**2 + 2*b*s + s**2)) - + 3*a/(8*(9*a**2 + b**2 - 2*b*s + s**2)) + + 3*a/(8*(a**2 + b**2 + 2*b*s + s**2)) + + 3*a/(8*(a**2 + b**2 - 2*b*s + s**2))).simplify() == 0 + assert (LT(t**2*exp(-t**2), t, s) == + (sqrt(pi)*s**2*exp(s**2/4)*erfc(s/2)/8 - s/4 + + sqrt(pi)*exp(s**2/4)*erfc(s/2)/4, 0, True)) + assert (LT((a*t**2 + b*t + c)*f(t), t, s) == + (a*Derivative(LaplaceTransform(f(t), t, s), (s, 2)) - + b*Derivative(LaplaceTransform(f(t), t, s), s) + + c*LaplaceTransform(f(t), t, s), -oo, True)) + assert (LT(t**np*g(t), t, s) == + ((-1)**np*Derivative(LaplaceTransform(g(t), t, s), (s, np)), + -oo, True)) + # The following tests check whether _piecewise_to_heaviside works: + x1 = Piecewise((0, t <= 0), (1, t <= 1), (0, True)) + X1 = LT(x1, t, s)[0] + assert X1 == 1/s - exp(-s)/s + y1 = ILT(X1, s, t) + assert y1 == Heaviside(t) - Heaviside(t - 1) + x1 = Piecewise((0, t <= 0), (t, t <= 1), (2-t, t <= 2), (0, True)) + X1 = LT(x1, t, s)[0].simplify() + assert X1 == (exp(2*s) - 2*exp(s) + 1)*exp(-2*s)/s**2 + y1 = ILT(X1, s, t) + assert ( + -y1 + t*Heaviside(t) + (t - 2)*Heaviside(t - 2) - + 2*(t - 1)*Heaviside(t - 1)).simplify() == 0 + x1 = Piecewise((exp(t), t <= 0), (1, t <= 1), (exp(-(t)), True)) + X1 = LT(x1, t, s)[0] + assert X1 == exp(-1)*exp(-s)/(s + 1) + 1/s - exp(-s)/s + y1 = ILT(X1, s, t) + assert y1 == ( + exp(-1)*exp(1 - t)*Heaviside(t - 1) + Heaviside(t) - Heaviside(t - 1)) + x1 = Piecewise((0, x <= 0), (1, x <= 1), (0, True)) + X1 = LT(x1, t, s)[0] + assert X1 == Piecewise((0, x <= 0), (1, x <= 1), (0, True))/s + x1 = [ + a*Piecewise((1, And(t > 1, t <= 3)), (2, True)), + a*Piecewise((1, And(t >= 1, t <= 3)), (2, True)), + a*Piecewise((1, And(t >= 1, t < 3)), (2, True)), + a*Piecewise((1, And(t > 1, t < 3)), (2, True))] + for x2 in x1: + assert LT(x2, t, s)[0].expand() == 2*a/s - a*exp(-s)/s + a*exp(-3*s)/s + assert ( + LT(Piecewise((1, Eq(t, 1)), (2, True)), t, s)[0] == + LaplaceTransform(Piecewise((1, Eq(t, 1)), (2, True)), t, s)) + # The following lines test whether _laplace_transform successfully + # removes Heaviside(1) before processing espressions. It fails if + # Heaviside(t) remains because then meijerg functions will appear. + X1 = 1/sqrt(a*s**2-b) + x1 = ILT(X1, s, t) + Y1 = LT(x1, t, s)[0] + Z1 = (Y1**2/X1**2).simplify() + assert Z1 == 1 + # The following two lines test whether issues #5813 and #7176 are solved. + assert (LT(diff(f(t), (t, 1)), t, s, noconds=True) == + s*LaplaceTransform(f(t), t, s) - f(0)) + assert (LT(diff(f(t), (t, 3)), t, s, noconds=True) == + s**3*LaplaceTransform(f(t), t, s) - s**2*f(0) - + s*Subs(Derivative(f(t), t), t, 0) - + Subs(Derivative(f(t), (t, 2)), t, 0)) + # Issue #7219 + assert (LT(diff(f(x, t, w), t, 2), t, s) == + (s**2*LaplaceTransform(f(x, t, w), t, s) - s*f(x, 0, w) - + Subs(Derivative(f(x, t, w), t), t, 0), -oo, True)) + # Issue #23307 + assert (LT(10*diff(f(t), (t, 1)), t, s, noconds=True) == + 10*s*LaplaceTransform(f(t), t, s) - 10*f(0)) + assert (LT(a*f(b*t)+g(c*t), t, s, noconds=True) == + a*LaplaceTransform(f(t), t, s/b)/b + + LaplaceTransform(g(t), t, s/c)/c) + assert inverse_laplace_transform( + f(w), w, t, plane=0) == InverseLaplaceTransform(f(w), w, t, 0) + assert (LT(f(t)*g(t), t, s, noconds=True) == + LaplaceTransform(f(t)*g(t), t, s)) + # Issue #24294 + assert (LT(b*f(a*t), t, s, noconds=True) == + b*LaplaceTransform(f(t), t, s/a)/a) + assert LT(3*exp(t)*Heaviside(t), t, s) == (3/(s - 1), 1, True) + assert (LT(2*sin(t)*Heaviside(t), t, s, simplify=True) == + (2/(s**2 + 1), 0, True)) + # Issue #25293 + assert ( + LT((1/(t-1))*sin(4*pi*(t-1))*DiracDelta(t-1) * + (Heaviside(t-1/4) - Heaviside(t-2)), t, s)[0] == 4*pi*exp(-s)) + # additional basic tests from wikipedia + assert (LT((t - a)**b*exp(-c*(t - a))*Heaviside(t - a), t, s) == + ((c + s)**(-b - 1)*exp(-a*s)*gamma(b + 1), -c, True)) + assert ( + LT((exp(2*t)-1)*exp(-b-t)*Heaviside(t)/2, t, s, noconds=True, + simplify=True) == + exp(-b)/(s**2 - 1)) + # DiracDelta function: standard cases + assert LT(DiracDelta(t), t, s) == (1, -oo, True) + assert LT(DiracDelta(a*t), t, s) == (1/a, -oo, True) + assert LT(DiracDelta(t/42), t, s) == (42, -oo, True) + assert LT(DiracDelta(t+42), t, s) == (0, -oo, True) + assert (LT(DiracDelta(t)+DiracDelta(t-42), t, s) == + (1 + exp(-42*s), -oo, True)) + assert (LT(DiracDelta(t)-a*exp(-a*t), t, s, simplify=True) == + (s/(a + s), -a, True)) + assert ( + LT(exp(-t)*(DiracDelta(t)+DiracDelta(t-42)), t, s, simplify=True) == + (exp(-42*s - 42) + 1, -oo, True)) + assert LT(f(t)*DiracDelta(t-42), t, s) == (f(42)*exp(-42*s), -oo, True) + assert LT(f(t)*DiracDelta(b*t-a), t, s) == (f(a/b)*exp(-a*s/b)/b, + -oo, True) + assert LT(f(t)*DiracDelta(b*t+a), t, s) == (0, -oo, True) + # SingularityFunction + assert LT(SingularityFunction(t, a, -1), t, s)[0] == exp(-a*s) + assert LT(SingularityFunction(t, a, 1), t, s)[0] == exp(-a*s)/s**2 + assert LT(SingularityFunction(t, a, x), t, s)[0] == ( + LaplaceTransform(SingularityFunction(t, a, x), t, s)) + # Collection of cases that cannot be fully evaluated and/or would catch + # some common implementation errors + assert (LT(DiracDelta(t**2), t, s, noconds=True) == + LaplaceTransform(DiracDelta(t**2), t, s)) + assert LT(DiracDelta(t**2 - 1), t, s) == (exp(-s)/2, -oo, True) + assert LT(DiracDelta(t*(1 - t)), t, s) == (1 - exp(-s), -oo, True) + assert (LT((DiracDelta(t) + 1)*(DiracDelta(t - 1) + 1), t, s) == + (LaplaceTransform(DiracDelta(t)*DiracDelta(t - 1), t, s) + + 1 + exp(-s) + 1/s, 0, True)) + assert LT(DiracDelta(2*t-2*exp(a)), t, s) == (exp(-s*exp(a))/2, -oo, True) + assert LT(DiracDelta(-2*t+2*exp(a)), t, s) == (exp(-s*exp(a))/2, -oo, True) + # Heaviside tests + assert LT(Heaviside(t), t, s) == (1/s, 0, True) + assert LT(Heaviside(t - a), t, s) == (exp(-a*s)/s, 0, True) + assert LT(Heaviside(t-1), t, s) == (exp(-s)/s, 0, True) + assert LT(Heaviside(2*t-4), t, s) == (exp(-2*s)/s, 0, True) + assert LT(Heaviside(2*t+4), t, s) == (1/s, 0, True) + assert (LT(Heaviside(-2*t+4), t, s, simplify=True) == + (1/s - exp(-2*s)/s, 0, True)) + assert (LT(g(t)*Heaviside(t - w), t, s) == + (LaplaceTransform(g(t)*Heaviside(t - w), t, s), -oo, True)) + assert ( + LT(Heaviside(t-a)*g(t), t, s) == + (LaplaceTransform(g(a + t), t, s)*exp(-a*s), -oo, True)) + assert ( + LT(Heaviside(t+a)*g(t), t, s) == + (LaplaceTransform(g(t), t, s), -oo, True)) + assert ( + LT(Heaviside(-t+a)*g(t), t, s) == + (LaplaceTransform(g(t), t, s) - + LaplaceTransform(g(a + t), t, s)*exp(-a*s), -oo, True)) + assert ( + LT(Heaviside(-t-a)*g(t), t, s) == (0, 0, True)) + # Fresnel functions + assert (laplace_transform(fresnels(t), t, s, simplify=True) == + ((-sin(s**2/(2*pi))*fresnels(s/pi) + + sqrt(2)*sin(s**2/(2*pi) + pi/4)/2 - + cos(s**2/(2*pi))*fresnelc(s/pi))/s, 0, True)) + assert (laplace_transform(fresnelc(t), t, s, simplify=True) == + ((sin(s**2/(2*pi))*fresnelc(s/pi) - + cos(s**2/(2*pi))*fresnels(s/pi) + + sqrt(2)*cos(s**2/(2*pi) + pi/4)/2)/s, 0, True)) + # Matrix tests + Mt = Matrix([[exp(t), t*exp(-t)], [t*exp(-t), exp(t)]]) + Ms = Matrix([[1/(s - 1), (s + 1)**(-2)], + [(s + 1)**(-2), 1/(s - 1)]]) + # The default behaviour for Laplace transform of a Matrix returns a Matrix + # of Tuples and is deprecated: + with warns_deprecated_sympy(): + Ms_conds = Matrix( + [[(1/(s - 1), 1, True), ((s + 1)**(-2), -1, True)], + [((s + 1)**(-2), -1, True), (1/(s - 1), 1, True)]]) + with warns_deprecated_sympy(): + assert LT(Mt, t, s) == Ms_conds + # The new behavior is to return a tuple of a Matrix and the convergence + # conditions for the matrix as a whole: + assert LT(Mt, t, s, legacy_matrix=False) == (Ms, 1, True) + # With noconds=True the transformed matrix is returned without conditions + # either way: + assert LT(Mt, t, s, noconds=True) == Ms + assert LT(Mt, t, s, legacy_matrix=False, noconds=True) == Ms + + +@slow +def test_inverse_laplace_transform(): + s = symbols('s') + k, n, t = symbols('k, n, t', real=True) + a, b, c, d = symbols('a, b, c, d', positive=True) + f = Function('f') + F = Function('F') + + def ILT(g): + return inverse_laplace_transform(g, s, t) + + def ILTS(g): + return inverse_laplace_transform(g, s, t, simplify=True) + + def ILTF(g): + return laplace_correspondence( + inverse_laplace_transform(g, s, t), {f: F}) + + # Tests for the rules in Bateman54. + + # Section 4.1: Some of the Laplace transform rules can also be used well + # in the inverse transform. + assert ILTF(exp(-a*s)*F(s)) == f(-a + t) + assert ILTF(k*F(s-a)) == k*f(t)*exp(-a*t) + assert ILTF(diff(F(s), s, 3)) == -t**3*f(t) + assert ILTF(diff(F(s), s, 4)) == t**4*f(t) + + # Section 5.1: Most rules are impractical for a computer algebra system. + + # Section 5.2: Rational functions + assert ILT(2) == 2*DiracDelta(t) + assert ILT(1/s) == Heaviside(t) + assert ILT(1/s**2) == t*Heaviside(t) + assert ILT(1/s**5) == t**4*Heaviside(t)/24 + assert ILT(1/s**n) == t**(n - 1)*Heaviside(t)/gamma(n) + assert ILT(a/(a + s)) == a*exp(-a*t)*Heaviside(t) + assert ILT(s/(a + s)) == -a*exp(-a*t)*Heaviside(t) + DiracDelta(t) + assert (ILT(b*s/(s+a)**2) == + b*(-a*t*exp(-a*t)*Heaviside(t) + exp(-a*t)*Heaviside(t))) + assert (ILTS(c/((s+a)*(s+b))) == + c*(exp(a*t) - exp(b*t))*exp(-t*(a + b))*Heaviside(t)/(a - b)) + assert (ILTS(c*s/((s+a)*(s+b))) == + c*(a*exp(b*t) - b*exp(a*t))*exp(-t*(a + b))*Heaviside(t)/(a - b)) + assert ILTS(s/(a + s)**3) == t*(-a*t + 2)*exp(-a*t)*Heaviside(t)/2 + assert ILTS(1/(s*(a + s)**3)) == ( + -a**2*t**2 - 2*a*t + 2*exp(a*t) - 2)*exp(-a*t)*Heaviside(t)/(2*a**3) + assert ILT(1/(s*(a + s)**n)) == ( + Heaviside(t)*lowergamma(n, a*t)/(a**n*gamma(n))) + assert ILT((s-a)**(-b)) == t**(b - 1)*exp(a*t)*Heaviside(t)/gamma(b) + assert ILT((a + s)**(-2)) == t*exp(-a*t)*Heaviside(t) + assert ILT((a + s)**(-5)) == t**4*exp(-a*t)*Heaviside(t)/24 + assert ILT(s**2/(s**2 + 1)) == -sin(t)*Heaviside(t) + DiracDelta(t) + assert ILT(1 - 1/(s**2 + 1)) == -sin(t)*Heaviside(t) + DiracDelta(t) + assert ILT(a/(a**2 + s**2)) == sin(a*t)*Heaviside(t) + assert ILT(s/(s**2 + a**2)) == cos(a*t)*Heaviside(t) + assert ILT(b/(b**2 + (a + s)**2)) == exp(-a*t)*sin(b*t)*Heaviside(t) + assert (ILT(b*s/(b**2 + (a + s)**2)) == + b*(-a*exp(-a*t)*sin(b*t)/b + exp(-a*t)*cos(b*t))*Heaviside(t)) + assert ILT(1/(s**2*(s**2 + 1))) == t*Heaviside(t) - sin(t)*Heaviside(t) + assert (ILTS(c*s/(d**2*(s+a)**2+b**2)) == + c*(-a*d*sin(b*t/d) + b*cos(b*t/d))*exp(-a*t)*Heaviside(t)/(b*d**2)) + assert ILTS((b*s**2 + d)/(a**2 + s**2)**2) == ( + 2*a**2*b*sin(a*t) + (a**2*b - d)*(a*t*cos(a*t) - + sin(a*t)))*Heaviside(t)/(2*a**3) + assert ILTS(b/(s**2-a**2)) == b*sinh(a*t)*Heaviside(t)/a + assert (ILT(b/(s**2-a**2)) == + b*(exp(a*t)*Heaviside(t)/(2*a) - exp(-a*t)*Heaviside(t)/(2*a))) + assert ILTS(b*s/(s**2-a**2)) == b*cosh(a*t)*Heaviside(t) + assert (ILT(b/(s*(s+a))) == + b*(Heaviside(t)/a - exp(-a*t)*Heaviside(t)/a)) + # Issue #24424 + assert (ILTS((s + 8)/((s + 2)*(s**2 + 2*s + 10))) == + ((8*sin(3*t) - 9*cos(3*t))*exp(t) + 9)*exp(-2*t)*Heaviside(t)/15) + # Issue #8514; this is not important anymore, since this function + # is not solved by integration anymore + assert (ILT(1/(a*s**2+b*s+c)) == + 2*exp(-b*t/(2*a))*sin(t*sqrt(4*a*c - b**2)/(2*a)) * + Heaviside(t)/sqrt(4*a*c - b**2)) + + # Section 5.3: Irrational algebraic functions + assert ( # (1) + ILT(1/sqrt(s)/(b*s-a)) == + exp(a*t/b)*Heaviside(t)*erf(sqrt(a)*sqrt(t)/sqrt(b))/(sqrt(a)*sqrt(b))) + assert ( # (2) + ILT(1/sqrt(k*s)/(c*s-a)/s) == + (-2*c*sqrt(t)/(sqrt(pi)*a) + + c**(S(3)/2)*exp(a*t/c)*erf(sqrt(a)*sqrt(t)/sqrt(c))/a**(S(3)/2)) * + Heaviside(t)/(c*sqrt(k))) + assert ( # (4) + ILT(1/(sqrt(c*s)+a)) == (-a*exp(a**2*t/c)*erfc(a*sqrt(t)/sqrt(c))/c + + 1/(sqrt(pi)*sqrt(c)*sqrt(t)))*Heaviside(t)) + assert ( # (5) + ILT(a/s/(b*sqrt(s)+a)) == + (-exp(a**2*t/b**2)*erfc(a*sqrt(t)/b) + 1)*Heaviside(t)) + assert ( # (6) + ILT((a-b)*sqrt(s)/(sqrt(s)+sqrt(a))/(s-b)) == + (sqrt(a)*sqrt(b)*exp(b*t)*erfc(sqrt(b)*sqrt(t)) + + a*exp(a*t)*erfc(sqrt(a)*sqrt(t)) - b*exp(b*t))*Heaviside(t)) + assert ( # (7) + ILT(1/sqrt(s)/(sqrt(b*s)+a)) == + exp(a**2*t/b)*Heaviside(t)*erfc(a*sqrt(t)/sqrt(b))/sqrt(b)) + assert ( # (8) + ILT(a**2/(sqrt(s)+a)/s**(S(3)/2)) == + (2*a*sqrt(t)/sqrt(pi) + exp(a**2*t)*erfc(a*sqrt(t)) - 1) * + Heaviside(t)) + assert ( # (9) + ILT((a-b)*sqrt(b)/(s-b)/sqrt(s)/(sqrt(s)+sqrt(a))) == + (sqrt(a)*exp(b*t)*erf(sqrt(b)*sqrt(t)) + + sqrt(b)*exp(a*t)*erfc(sqrt(a)*sqrt(t)) - + sqrt(b)*exp(b*t))*Heaviside(t)) + assert ( # (10) + ILT(1/(sqrt(s)+sqrt(a))**2) == + (-2*sqrt(a)*sqrt(t)/sqrt(pi) + + (-2*a*t + 1)*(erf(sqrt(a)*sqrt(t)) - + 1)*exp(a*t) + 1)*Heaviside(t)) + assert ( # (11) + ILT(1/(sqrt(s)+sqrt(a))**2/s) == + ((2*t - 1/a)*exp(a*t)*erfc(sqrt(a)*sqrt(t)) + 1/a - + 2*sqrt(t)/(sqrt(pi)*sqrt(a)))*Heaviside(t)) + assert ( # (12) + ILT(1/(sqrt(s)+a)**2/sqrt(s)) == + (-2*a*t*exp(a**2*t)*erfc(a*sqrt(t)) + + 2*sqrt(t)/sqrt(pi))*Heaviside(t)) + assert ( # (13) + ILT(1/(sqrt(s)+a)**3) == + (-a*t*(2*a**2*t + 3)*exp(a**2*t)*erfc(a*sqrt(t)) + + 2*sqrt(t)*(a**2*t + 1)/sqrt(pi))*Heaviside(t)) + x = ( + - ILT(sqrt(s)/(sqrt(s)+a)**3) + + 2*(sqrt(pi)*a**2*t*(-2*sqrt(pi)*erfc(a*sqrt(t)) + + 2*exp(-a**2*t)/(a*sqrt(t))) * + (-a**4*t**2 - 5*a**2*t/2 - S.Half) * exp(a**2*t)/2 + + sqrt(pi)*a*sqrt(t)*(a**2*t + 1)/2) * + Heaviside(t)/(pi*a**2*t)).simplify() + assert ( # (14) + x == 0) + x = ( + - ILT(1/sqrt(s)/(sqrt(s)+a)**3) + + Heaviside(t)*(sqrt(t)*((2*a**2*t + 1) * + (sqrt(pi)*a*sqrt(t)*exp(a**2*t) * + erfc(a*sqrt(t)) - 1) + 1) / + (sqrt(pi)*a))).simplify() + assert ( # (15) + x == 0) + assert ( # (16) + factor_terms(ILT(3/(sqrt(s)+a)**4)) == + 3*(-2*a**3*t**(S(5)/2)*(2*a**2*t + 5)/(3*sqrt(pi)) + + t*(4*a**4*t**2 + 12*a**2*t + 3)*exp(a**2*t) * + erfc(a*sqrt(t))/3)*Heaviside(t)) + assert ( # (17) + ILT((sqrt(s)-a)/(s*(sqrt(s)+a))) == + (2*exp(a**2*t)*erfc(a*sqrt(t))-1)*Heaviside(t)) + assert ( # (18) + ILT((sqrt(s)-a)**2/(s*(sqrt(s)+a)**2)) == ( + 1 + 8*a**2*t*exp(a**2*t)*erfc(a*sqrt(t)) - + 8/sqrt(pi)*a*sqrt(t))*Heaviside(t)) + assert ( # (19) + ILT((sqrt(s)-a)**3/(s*(sqrt(s)+a)**3)) == Heaviside(t)*( + 2*(8*a**4*t**2+8*a**2*t+1)*exp(a**2*t) * + erfc(a*sqrt(t))-8/sqrt(pi)*a*sqrt(t)*(2*a**2*t+1)-1)) + assert ( # (22) + ILT(sqrt(s+a)/(s+b)) == Heaviside(t)*( + exp(-a*t)/sqrt(t)/sqrt(pi) + + sqrt(a-b)*exp(-b*t)*erf(sqrt(a-b)*sqrt(t)))) + assert ( # (23) + ILT(1/sqrt(s+b)/(s+a)) == Heaviside(t)*( + 1/sqrt(b-a)*exp(-a*t)*erf(sqrt(b-a)*sqrt(t)))) + assert ( # (35) + ILT(1/sqrt(s**2+a**2)) == Heaviside(t)*( + besselj(0, a*t))) + assert ( # (44) + ILT(1/sqrt(s**2-a**2)) == Heaviside(t)*( + besseli(0, a*t))) + + # Miscellaneous tests + # Can _inverse_laplace_time_shift deal with positive exponents? + assert ( + - ILT((s**2*exp(2*s) + 4*exp(s) - 4)*exp(-2*s)/(s*(s**2 + 1))) + + cos(t)*Heaviside(t) + 4*cos(t - 2)*Heaviside(t - 2) - + 4*cos(t - 1)*Heaviside(t - 1) - 4*Heaviside(t - 2) + + 4*Heaviside(t - 1)).simplify() == 0 + + +@slow +def test_inverse_laplace_transform_old(): + from sympy.functions.special.delta_functions import DiracDelta + ILT = inverse_laplace_transform + a, b, c, d = symbols('a b c d', positive=True) + n, r = symbols('n, r', real=True) + t, z = symbols('t z') + f = Function('f') + F = Function('F') + + def simp_hyp(expr): + return factor_terms(expand_mul(expr)).rewrite(sin) + + L = ILT(F(s), s, t) + assert laplace_correspondence(L, {f: F}) == f(t) + assert ILT(exp(-a*s)/s, s, t) == Heaviside(-a + t) + assert ILT(exp(-a*s)/(b + s), s, t) == exp(-b*(-a + t))*Heaviside(-a + t) + assert (ILT((b + s)/(a**2 + (b + s)**2), s, t) == + exp(-b*t)*cos(a*t)*Heaviside(t)) + assert (ILT(exp(-a*s)/s**b, s, t) == + (-a + t)**(b - 1)*Heaviside(-a + t)/gamma(b)) + assert (ILT(exp(-a*s)/sqrt(s**2 + 1), s, t) == + Heaviside(-a + t)*besselj(0, a - t)) + assert ILT(1/(s*sqrt(s + 1)), s, t) == Heaviside(t)*erf(sqrt(t)) + # TODO sinh/cosh shifted come out a mess. also delayed trig is a mess + # TODO should this simplify further? + assert (ILT(exp(-a*s)/s**b, s, t) == + (t - a)**(b - 1)*Heaviside(t - a)/gamma(b)) + assert (ILT(exp(-a*s)/sqrt(1 + s**2), s, t) == + Heaviside(t - a)*besselj(0, a - t)) # note: besselj(0, x) is even + # XXX ILT turns these branch factor into trig functions ... + assert ( + simplify(ILT(a**b*(s + sqrt(s**2 - a**2))**(-b)/sqrt(s**2 - a**2), + s, t).rewrite(exp)) == + Heaviside(t)*besseli(b, a*t)) + assert ( + ILT(a**b*(s + sqrt(s**2 + a**2))**(-b)/sqrt(s**2 + a**2), + s, t, simplify=True).rewrite(exp) == + Heaviside(t)*besselj(b, a*t)) + assert ILT(1/(s*sqrt(s + 1)), s, t) == Heaviside(t)*erf(sqrt(t)) + # TODO can we make erf(t) work? + assert (ILT((s * eye(2) - Matrix([[1, 0], [0, 2]])).inv(), s, t) == + Matrix([[exp(t)*Heaviside(t), 0], [0, exp(2*t)*Heaviside(t)]])) + # Test time_diff rule + assert (ILT(s**42*f(s), s, t) == + Derivative(InverseLaplaceTransform(f(s), s, t, None), (t, 42))) + assert ILT(cos(s), s, t) == InverseLaplaceTransform(cos(s), s, t, None) + # Rules for testing different DiracDelta cases + assert ( + ILT(1 + 2*s + 3*s**2 + 5*s**3, s, t) == DiracDelta(t) + + 2*DiracDelta(t, 1) + 3*DiracDelta(t, 2) + 5*DiracDelta(t, 3)) + assert (ILT(2*exp(3*s) - 5*exp(-7*s), s, t) == + 2*InverseLaplaceTransform(exp(3*s), s, t, None) - + 5*DiracDelta(t - 7)) + a = cos(sin(7)/2) + assert ILT(a*exp(-3*s), s, t) == a*DiracDelta(t - 3) + assert ILT(exp(2*s), s, t) == InverseLaplaceTransform(exp(2*s), s, t, None) + r = Symbol('r', real=True) + assert ILT(exp(r*s), s, t) == InverseLaplaceTransform(exp(r*s), s, t, None) + # Rules for testing whether Heaviside(t) is treated properly in diff rule + assert ILT(s**2/(a**2 + s**2), s, t) == ( + -a*sin(a*t)*Heaviside(t) + DiracDelta(t)) + assert ILT(s**2*(f(s) + 1/(a**2 + s**2)), s, t) == ( + -a*sin(a*t)*Heaviside(t) + DiracDelta(t) + + Derivative(InverseLaplaceTransform(f(s), s, t, None), (t, 2))) + # Rules from the previous test_inverse_laplace_transform_delta_cond(): + assert (ILT(exp(r*s), s, t, noconds=False) == + (InverseLaplaceTransform(exp(r*s), s, t, None), True)) + # inversion does not exist: verify it doesn't evaluate to DiracDelta + for z in (Symbol('z', extended_real=False), + Symbol('z', imaginary=True, zero=False)): + f = ILT(exp(z*s), s, t, noconds=False) + f = f[0] if isinstance(f, tuple) else f + assert f.func != DiracDelta + + +@slow +def test_expint(): + x = Symbol('x') + a = Symbol('a') + u = Symbol('u', polar=True) + + # TODO LT of Si, Shi, Chi is a mess ... + assert laplace_transform(Ci(x), x, s) == (-log(1 + s**2)/2/s, 0, True) + assert (laplace_transform(expint(a, x), x, s, simplify=True) == + (lerchphi(s*exp_polar(I*pi), 1, a), 0, re(a) > S.Zero)) + assert (laplace_transform(expint(1, x), x, s, simplify=True) == + (log(s + 1)/s, 0, True)) + assert (laplace_transform(expint(2, x), x, s, simplify=True) == + ((s - log(s + 1))/s**2, 0, True)) + assert (inverse_laplace_transform(-log(1 + s**2)/2/s, s, u).expand() == + Heaviside(u)*Ci(u)) + assert ( + inverse_laplace_transform(log(s + 1)/s, s, x, + simplify=True).rewrite(expint) == + Heaviside(x)*E1(x)) + assert ( + inverse_laplace_transform( + (s - log(s + 1))/s**2, s, x, + simplify=True).rewrite(expint).expand() == + (expint(2, x)*Heaviside(x)).rewrite(Ei).rewrite(expint).expand()) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_lineintegrals.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_lineintegrals.py new file mode 100644 index 0000000000000000000000000000000000000000..d0af146b52406a153d033286f3fcfa79334d2a73 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_lineintegrals.py @@ -0,0 +1,13 @@ +from sympy.core.numbers import E +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.geometry.curve import Curve +from sympy.integrals.integrals import line_integrate + +s, t, x, y, z = symbols('s,t,x,y,z') + + +def test_lineintegral(): + c = Curve([E**t + 1, E**t - 1], (t, 0, log(2))) + assert line_integrate(x + y, c, [x, y]) == 3*sqrt(2) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_manual.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_manual.py new file mode 100644 index 0000000000000000000000000000000000000000..74cae4521ec97608a21553e0203be60c210387b3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_manual.py @@ -0,0 +1,714 @@ +from sympy.core.expr import Expr +from sympy.core.mul import Mul +from sympy.core.function import (Derivative, Function, diff, expand) +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.relational import Ne +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import (asinh, csch, cosh, coth, sech, sinh, tanh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise, piecewise_fold +from sympy.functions.elementary.trigonometric import (acos, acot, acsc, asec, asin, atan, cos, cot, csc, sec, sin, tan) +from sympy.functions.special.delta_functions import Heaviside, DiracDelta +from sympy.functions.special.elliptic_integrals import (elliptic_e, elliptic_f) +from sympy.functions.special.error_functions import (Chi, Ci, Ei, Shi, Si, erf, erfi, fresnelc, fresnels, li) +from sympy.functions.special.gamma_functions import uppergamma +from sympy.functions.special.polynomials import (assoc_laguerre, chebyshevt, chebyshevu, gegenbauer, hermite, jacobi, laguerre, legendre) +from sympy.functions.special.zeta_functions import polylog +from sympy.integrals.integrals import (Integral, integrate) +from sympy.logic.boolalg import And +from sympy.integrals.manualintegrate import (manualintegrate, find_substitutions, + _parts_rule, integral_steps, manual_subs) +from sympy.testing.pytest import raises, slow + +x, y, z, u, n, a, b, c, d, e = symbols('x y z u n a b c d e') +f = Function('f') + + +def assert_is_integral_of(f: Expr, F: Expr): + assert manualintegrate(f, x) == F + assert F.diff(x).equals(f) + + +def test_find_substitutions(): + assert find_substitutions((cot(x)**2 + 1)**2*csc(x)**2*cot(x)**2, x, u) == \ + [(cot(x), 1, -u**6 - 2*u**4 - u**2)] + assert find_substitutions((sec(x)**2 + tan(x) * sec(x)) / (sec(x) + tan(x)), + x, u) == [(sec(x) + tan(x), 1, 1/u)] + assert (-x**2, Rational(-1, 2), exp(u)) in find_substitutions(x * exp(-x**2), x, u) + assert not find_substitutions(Derivative(f(x), x)**2, x, u) + + +def test_manualintegrate_polynomials(): + assert manualintegrate(y, x) == x*y + assert manualintegrate(exp(2), x) == x * exp(2) + assert manualintegrate(x**2, x) == x**3 / 3 + assert manualintegrate(3 * x**2 + 4 * x**3, x) == x**3 + x**4 + + assert manualintegrate((x + 2)**3, x) == (x + 2)**4 / 4 + assert manualintegrate((3*x + 4)**2, x) == (3*x + 4)**3 / 9 + + assert manualintegrate((u + 2)**3, u) == (u + 2)**4 / 4 + assert manualintegrate((3*u + 4)**2, u) == (3*u + 4)**3 / 9 + + +def test_manualintegrate_exponentials(): + assert manualintegrate(exp(2*x), x) == exp(2*x) / 2 + assert manualintegrate(2**x, x) == (2 ** x) / log(2) + assert_is_integral_of(1/sqrt(1-exp(2*x)), + log(sqrt(1 - exp(2*x)) - 1)/2 - log(sqrt(1 - exp(2*x)) + 1)/2) + + assert manualintegrate(1 / x, x) == log(x) + assert manualintegrate(1 / (2*x + 3), x) == log(2*x + 3) / 2 + assert manualintegrate(log(x)**2 / x, x) == log(x)**3 / 3 + + assert_is_integral_of(x**x*(log(x)+1), x**x) + + +def test_manualintegrate_parts(): + assert manualintegrate(exp(x) * sin(x), x) == \ + (exp(x) * sin(x)) / 2 - (exp(x) * cos(x)) / 2 + assert manualintegrate(2*x*cos(x), x) == 2*x*sin(x) + 2*cos(x) + assert manualintegrate(x * log(x), x) == x**2*log(x)/2 - x**2/4 + assert manualintegrate(log(x), x) == x * log(x) - x + assert manualintegrate((3*x**2 + 5) * exp(x), x) == \ + 3*x**2*exp(x) - 6*x*exp(x) + 11*exp(x) + assert manualintegrate(atan(x), x) == x*atan(x) - log(x**2 + 1)/2 + + # Make sure _parts_rule doesn't pick u = constant but can pick dv = + # constant if necessary, e.g. for integrate(atan(x)) + assert _parts_rule(cos(x), x) == None + assert _parts_rule(exp(x), x) == None + assert _parts_rule(x**2, x) == None + result = _parts_rule(atan(x), x) + assert result[0] == atan(x) and result[1] == 1 + + +def test_manualintegrate_trigonometry(): + assert manualintegrate(sin(x), x) == -cos(x) + assert manualintegrate(tan(x), x) == -log(cos(x)) + + assert manualintegrate(sec(x), x) == log(sec(x) + tan(x)) + assert manualintegrate(csc(x), x) == -log(csc(x) + cot(x)) + + assert manualintegrate(sin(x) * cos(x), x) in [sin(x) ** 2 / 2, -cos(x)**2 / 2] + assert manualintegrate(-sec(x) * tan(x), x) == -sec(x) + assert manualintegrate(csc(x) * cot(x), x) == -csc(x) + assert manualintegrate(sec(x)**2, x) == tan(x) + assert manualintegrate(csc(x)**2, x) == -cot(x) + + assert manualintegrate(x * sec(x**2), x) == log(tan(x**2) + sec(x**2))/2 + assert manualintegrate(cos(x)*csc(sin(x)), x) == -log(cot(sin(x)) + csc(sin(x))) + assert manualintegrate(cos(3*x)*sec(x), x) == -x + sin(2*x) + assert manualintegrate(sin(3*x)*sec(x), x) == \ + -3*log(cos(x)) + 2*log(cos(x)**2) - 2*cos(x)**2 + + assert_is_integral_of(sinh(2*x), cosh(2*x)/2) + assert_is_integral_of(x*cosh(x**2), sinh(x**2)/2) + assert_is_integral_of(tanh(x), log(cosh(x))) + assert_is_integral_of(coth(x), log(sinh(x))) + f, F = sech(x), 2*atan(tanh(x/2)) + assert manualintegrate(f, x) == F + assert (F.diff(x) - f).rewrite(exp).simplify() == 0 # todo: equals returns None + f, F = csch(x), log(tanh(x/2)) + assert manualintegrate(f, x) == F + assert (F.diff(x) - f).rewrite(exp).simplify() == 0 + + +@slow +def test_manualintegrate_trigpowers(): + assert manualintegrate(sin(x)**2 * cos(x), x) == sin(x)**3 / 3 + assert manualintegrate(sin(x)**2 * cos(x) **2, x) == \ + x / 8 - sin(4*x) / 32 + assert manualintegrate(sin(x) * cos(x)**3, x) == -cos(x)**4 / 4 + assert manualintegrate(sin(x)**3 * cos(x)**2, x) == \ + cos(x)**5 / 5 - cos(x)**3 / 3 + + assert manualintegrate(tan(x)**3 * sec(x), x) == sec(x)**3/3 - sec(x) + assert manualintegrate(tan(x) * sec(x) **2, x) == sec(x)**2/2 + + assert manualintegrate(cot(x)**5 * csc(x), x) == \ + -csc(x)**5/5 + 2*csc(x)**3/3 - csc(x) + assert manualintegrate(cot(x)**2 * csc(x)**6, x) == \ + -cot(x)**7/7 - 2*cot(x)**5/5 - cot(x)**3/3 + + +@slow +def test_manualintegrate_inversetrig(): + # atan + assert manualintegrate(exp(x) / (1 + exp(2*x)), x) == atan(exp(x)) + assert manualintegrate(1 / (4 + 9 * x**2), x) == atan(3 * x/2) / 6 + assert manualintegrate(1 / (16 + 16 * x**2), x) == atan(x) / 16 + assert manualintegrate(1 / (4 + x**2), x) == atan(x / 2) / 2 + assert manualintegrate(1 / (1 + 4 * x**2), x) == atan(2*x) / 2 + ra = Symbol('a', real=True) + rb = Symbol('b', real=True) + assert manualintegrate(1/(ra + rb*x**2), x) == \ + Piecewise((atan(x/sqrt(ra/rb))/(rb*sqrt(ra/rb)), ra/rb > 0), + ((log(x - sqrt(-ra/rb)) - log(x + sqrt(-ra/rb)))/(2*sqrt(rb)*sqrt(-ra)), True)) + assert manualintegrate(1/(4 + rb*x**2), x) == \ + Piecewise((atan(x/(2*sqrt(1/rb)))/(2*rb*sqrt(1/rb)), 1/rb > 0), + (-I*(log(x - 2*sqrt(-1/rb)) - log(x + 2*sqrt(-1/rb)))/(4*sqrt(rb)), True)) + assert manualintegrate(1/(ra + 4*x**2), x) == \ + Piecewise((atan(2*x/sqrt(ra))/(2*sqrt(ra)), ra > 0), + ((log(x - sqrt(-ra)/2) - log(x + sqrt(-ra)/2))/(4*sqrt(-ra)), True)) + assert manualintegrate(1/(4 + 4*x**2), x) == atan(x) / 4 + + assert manualintegrate(1/(a + b*x**2), x) == Piecewise((atan(x/sqrt(a/b))/(b*sqrt(a/b)), Ne(a, 0)), + (-1/(b*x), True)) + + # asin + assert manualintegrate(1/sqrt(1-x**2), x) == asin(x) + assert manualintegrate(1/sqrt(4-4*x**2), x) == asin(x)/2 + assert manualintegrate(3/sqrt(1-9*x**2), x) == asin(3*x) + assert manualintegrate(1/sqrt(4-9*x**2), x) == asin(x*Rational(3, 2))/3 + + # asinh + assert manualintegrate(1/sqrt(x**2 + 1), x) == \ + asinh(x) + assert manualintegrate(1/sqrt(x**2 + 4), x) == \ + asinh(x/2) + assert manualintegrate(1/sqrt(4*x**2 + 4), x) == \ + asinh(x)/2 + assert manualintegrate(1/sqrt(4*x**2 + 1), x) == \ + asinh(2*x)/2 + assert manualintegrate(1/sqrt(ra*x**2 + 1), x) == \ + Piecewise((asin(x*sqrt(-ra))/sqrt(-ra), ra < 0), (asinh(sqrt(ra)*x)/sqrt(ra), ra > 0), (x, True)) + assert manualintegrate(1/sqrt(ra + x**2), x) == \ + Piecewise((asinh(x*sqrt(1/ra)), ra > 0), (log(2*x + 2*sqrt(ra + x**2)), True)) + + # log + assert manualintegrate(1/sqrt(x**2 - 1), x) == log(2*x + 2*sqrt(x**2 - 1)) + assert manualintegrate(1/sqrt(x**2 - 4), x) == log(2*x + 2*sqrt(x**2 - 4)) + assert manualintegrate(1/sqrt(4*x**2 - 4), x) == log(8*x + 4*sqrt(4*x**2 - 4))/2 + assert manualintegrate(1/sqrt(9*x**2 - 1), x) == log(18*x + 6*sqrt(9*x**2 - 1))/3 + assert manualintegrate(1/sqrt(ra*x**2 - 4), x) == \ + Piecewise((log(2*sqrt(ra)*sqrt(ra*x**2 - 4) + 2*ra*x)/sqrt(ra), Ne(ra, 0)), (-I*x/2, True)) + assert manualintegrate(1/sqrt(-ra + 4*x**2), x) == \ + Piecewise((asinh(2*x*sqrt(-1/ra))/2, ra < 0), (log(8*x + 4*sqrt(-ra + 4*x**2))/2, True)) + + # From https://www.wikiwand.com/en/List_of_integrals_of_inverse_trigonometric_functions + # asin + assert manualintegrate(asin(x), x) == x*asin(x) + sqrt(1 - x**2) + assert manualintegrate(asin(a*x), x) == Piecewise(((a*x*asin(a*x) + sqrt(-a**2*x**2 + 1))/a, Ne(a, 0)), (0, True)) + assert manualintegrate(x*asin(a*x), x) == \ + -a*Piecewise((-x*sqrt(-a**2*x**2 + 1)/(2*a**2) + + log(-2*a**2*x + 2*sqrt(-a**2)*sqrt(-a**2*x**2 + 1))/(2*a**2*sqrt(-a**2)), Ne(a**2, 0)), + (x**3/3, True))/2 + x**2*asin(a*x)/2 + # acos + assert manualintegrate(acos(x), x) == x*acos(x) - sqrt(1 - x**2) + assert manualintegrate(acos(a*x), x) == Piecewise(((a*x*acos(a*x) - sqrt(-a**2*x**2 + 1))/a, Ne(a, 0)), (pi*x/2, True)) + assert manualintegrate(x*acos(a*x), x) == \ + a*Piecewise((-x*sqrt(-a**2*x**2 + 1)/(2*a**2) + + log(-2*a**2*x + 2*sqrt(-a**2)*sqrt(-a**2*x**2 + 1))/(2*a**2*sqrt(-a**2)), Ne(a**2, 0)), + (x**3/3, True))/2 + x**2*acos(a*x)/2 + # atan + assert manualintegrate(atan(x), x) == x*atan(x) - log(x**2 + 1)/2 + assert manualintegrate(atan(a*x), x) == Piecewise(((a*x*atan(a*x) - log(a**2*x**2 + 1)/2)/a, Ne(a, 0)), (0, True)) + assert manualintegrate(x*atan(a*x), x) == -a*(x/a**2 - atan(x/sqrt(a**(-2)))/(a**4*sqrt(a**(-2))))/2 + x**2*atan(a*x)/2 + # acsc + assert manualintegrate(acsc(x), x) == x*acsc(x) + Integral(1/(x*sqrt(1 - 1/x**2)), x) + assert manualintegrate(acsc(a*x), x) == x*acsc(a*x) + Integral(1/(x*sqrt(1 - 1/(a**2*x**2))), x)/a + assert manualintegrate(x*acsc(a*x), x) == x**2*acsc(a*x)/2 + Integral(1/sqrt(1 - 1/(a**2*x**2)), x)/(2*a) + # asec + assert manualintegrate(asec(x), x) == x*asec(x) - Integral(1/(x*sqrt(1 - 1/x**2)), x) + assert manualintegrate(asec(a*x), x) == x*asec(a*x) - Integral(1/(x*sqrt(1 - 1/(a**2*x**2))), x)/a + assert manualintegrate(x*asec(a*x), x) == x**2*asec(a*x)/2 - Integral(1/sqrt(1 - 1/(a**2*x**2)), x)/(2*a) + # acot + assert manualintegrate(acot(x), x) == x*acot(x) + log(x**2 + 1)/2 + assert manualintegrate(acot(a*x), x) == Piecewise(((a*x*acot(a*x) + log(a**2*x**2 + 1)/2)/a, Ne(a, 0)), (pi*x/2, True)) + assert manualintegrate(x*acot(a*x), x) == a*(x/a**2 - atan(x/sqrt(a**(-2)))/(a**4*sqrt(a**(-2))))/2 + x**2*acot(a*x)/2 + + # piecewise + assert manualintegrate(1/sqrt(ra-rb*x**2), x) == \ + Piecewise((asin(x*sqrt(rb/ra))/sqrt(rb), And(-rb < 0, ra > 0)), + (asinh(x*sqrt(-rb/ra))/sqrt(-rb), And(-rb > 0, ra > 0)), + (log(-2*rb*x + 2*sqrt(-rb)*sqrt(ra - rb*x**2))/sqrt(-rb), Ne(rb, 0)), + (x/sqrt(ra), True)) + assert manualintegrate(1/sqrt(ra + rb*x**2), x) == \ + Piecewise((asin(x*sqrt(-rb/ra))/sqrt(-rb), And(ra > 0, rb < 0)), + (asinh(x*sqrt(rb/ra))/sqrt(rb), And(ra > 0, rb > 0)), + (log(2*sqrt(rb)*sqrt(ra + rb*x**2) + 2*rb*x)/sqrt(rb), Ne(rb, 0)), + (x/sqrt(ra), True)) + + +def test_manualintegrate_trig_substitution(): + assert manualintegrate(sqrt(16*x**2 - 9)/x, x) == \ + Piecewise((sqrt(16*x**2 - 9) - 3*acos(3/(4*x)), + And(x < Rational(3, 4), x > Rational(-3, 4)))) + assert manualintegrate(1/(x**4 * sqrt(25-x**2)), x) == \ + Piecewise((-sqrt(-x**2/25 + 1)/(125*x) - + (-x**2/25 + 1)**(3*S.Half)/(15*x**3), And(x < 5, x > -5))) + assert manualintegrate(x**7/(49*x**2 + 1)**(3 * S.Half), x) == \ + ((49*x**2 + 1)**(5*S.Half)/28824005 - + (49*x**2 + 1)**(3*S.Half)/5764801 + + 3*sqrt(49*x**2 + 1)/5764801 + 1/(5764801*sqrt(49*x**2 + 1))) + +def test_manualintegrate_trivial_substitution(): + assert manualintegrate((exp(x) - exp(-x))/x, x) == -Ei(-x) + Ei(x) + f = Function('f') + assert manualintegrate((f(x) - f(-x))/x, x) == \ + -Integral(f(-x)/x, x) + Integral(f(x)/x, x) + + +def test_manualintegrate_rational(): + assert manualintegrate(1/(4 - x**2), x) == -log(x - 2)/4 + log(x + 2)/4 + assert manualintegrate(1/(-1 + x**2), x) == log(x - 1)/2 - log(x + 1)/2 + + +def test_manualintegrate_special(): + f, F = 4*exp(-x**2/3), 2*sqrt(3)*sqrt(pi)*erf(sqrt(3)*x/3) + assert_is_integral_of(f, F) + f, F = 3*exp(4*x**2), 3*sqrt(pi)*erfi(2*x)/4 + assert_is_integral_of(f, F) + f, F = x**Rational(1, 3)*exp(-x/8), -16*uppergamma(Rational(4, 3), x/8) + assert_is_integral_of(f, F) + f, F = exp(2*x)/x, Ei(2*x) + assert_is_integral_of(f, F) + f, F = exp(1 + 2*x - x**2), sqrt(pi)*exp(2)*erf(x - 1)/2 + assert_is_integral_of(f, F) + f = sin(x**2 + 4*x + 1) + F = (sqrt(2)*sqrt(pi)*(-sin(3)*fresnelc(sqrt(2)*(2*x + 4)/(2*sqrt(pi))) + + cos(3)*fresnels(sqrt(2)*(2*x + 4)/(2*sqrt(pi))))/2) + assert_is_integral_of(f, F) + f, F = cos(4*x**2), sqrt(2)*sqrt(pi)*fresnelc(2*sqrt(2)*x/sqrt(pi))/4 + assert_is_integral_of(f, F) + f, F = sin(3*x + 2)/x, sin(2)*Ci(3*x) + cos(2)*Si(3*x) + assert_is_integral_of(f, F) + f, F = sinh(3*x - 2)/x, -sinh(2)*Chi(3*x) + cosh(2)*Shi(3*x) + assert_is_integral_of(f, F) + f, F = 5*cos(2*x - 3)/x, 5*cos(3)*Ci(2*x) + 5*sin(3)*Si(2*x) + assert_is_integral_of(f, F) + f, F = cosh(x/2)/x, Chi(x/2) + assert_is_integral_of(f, F) + f, F = cos(x**2)/x, Ci(x**2)/2 + assert_is_integral_of(f, F) + f, F = 1/log(2*x + 1), li(2*x + 1)/2 + assert_is_integral_of(f, F) + f, F = polylog(2, 5*x)/x, polylog(3, 5*x) + assert_is_integral_of(f, F) + f, F = 5/sqrt(3 - 2*sin(x)**2), 5*sqrt(3)*elliptic_f(x, Rational(2, 3))/3 + assert_is_integral_of(f, F) + f, F = sqrt(4 + 9*sin(x)**2), 2*elliptic_e(x, Rational(-9, 4)) + assert_is_integral_of(f, F) + + +def test_manualintegrate_derivative(): + assert manualintegrate(pi * Derivative(x**2 + 2*x + 3), x) == \ + pi * (x**2 + 2*x + 3) + assert manualintegrate(Derivative(x**2 + 2*x + 3, y), x) == \ + Integral(Derivative(x**2 + 2*x + 3, y)) + assert manualintegrate(Derivative(sin(x), x, x, x, y), x) == \ + Derivative(sin(x), x, x, y) + + +def test_manualintegrate_Heaviside(): + assert_is_integral_of(DiracDelta(3*x+2), Heaviside(3*x+2)/3) + assert_is_integral_of(DiracDelta(3*x, 0), Heaviside(3*x)/3) + assert manualintegrate(DiracDelta(a+b*x, 1), x) == \ + Piecewise((DiracDelta(a + b*x)/b, Ne(b, 0)), (x*DiracDelta(a, 1), True)) + assert_is_integral_of(DiracDelta(x/3-1, 2), 3*DiracDelta(x/3-1, 1)) + assert manualintegrate(Heaviside(x), x) == x*Heaviside(x) + assert manualintegrate(x*Heaviside(2), x) == x**2/2 + assert manualintegrate(x*Heaviside(-2), x) == 0 + assert manualintegrate(x*Heaviside( x), x) == x**2*Heaviside( x)/2 + assert manualintegrate(x*Heaviside(-x), x) == x**2*Heaviside(-x)/2 + assert manualintegrate(Heaviside(2*x + 4), x) == (x+2)*Heaviside(2*x + 4) + assert manualintegrate(x*Heaviside(x), x) == x**2*Heaviside(x)/2 + assert manualintegrate(Heaviside(x + 1)*Heaviside(1 - x)*x**2, x) == \ + ((x**3/3 + Rational(1, 3))*Heaviside(x + 1) - Rational(2, 3))*Heaviside(-x + 1) + + y = Symbol('y') + assert manualintegrate(sin(7 + x)*Heaviside(3*x - 7), x) == \ + (- cos(x + 7) + cos(Rational(28, 3)))*Heaviside(3*x - S(7)) + + assert manualintegrate(sin(y + x)*Heaviside(3*x - y), x) == \ + (cos(y*Rational(4, 3)) - cos(x + y))*Heaviside(3*x - y) + + +def test_manualintegrate_orthogonal_poly(): + n = symbols('n') + a, b = 7, Rational(5, 3) + polys = [jacobi(n, a, b, x), gegenbauer(n, a, x), chebyshevt(n, x), + chebyshevu(n, x), legendre(n, x), hermite(n, x), laguerre(n, x), + assoc_laguerre(n, a, x)] + for p in polys: + integral = manualintegrate(p, x) + for deg in [-2, -1, 0, 1, 3, 5, 8]: + # some accept negative "degree", some do not + try: + p_subbed = p.subs(n, deg) + except ValueError: + continue + assert (integral.subs(n, deg).diff(x) - p_subbed).expand() == 0 + + # can also integrate simple expressions with these polynomials + q = x*p.subs(x, 2*x + 1) + integral = manualintegrate(q, x) + for deg in [2, 4, 7]: + assert (integral.subs(n, deg).diff(x) - q.subs(n, deg)).expand() == 0 + + # cannot integrate with respect to any other parameter + t = symbols('t') + for i in range(len(p.args) - 1): + new_args = list(p.args) + new_args[i] = t + assert isinstance(manualintegrate(p.func(*new_args), t), Integral) + + +@slow +def test_issue_6799(): + r, x, phi = map(Symbol, 'r x phi'.split()) + n = Symbol('n', integer=True, positive=True) + + integrand = (cos(n*(x-phi))*cos(n*x)) + limits = (x, -pi, pi) + assert manualintegrate(integrand, x) == \ + ((n*x/2 + sin(2*n*x)/4)*cos(n*phi) - sin(n*phi)*cos(n*x)**2/2)/n + assert r * integrate(integrand, limits).trigsimp() / pi == r * cos(n * phi) + assert not integrate(integrand, limits).has(Dummy) + + +def test_issue_12251(): + assert manualintegrate(x**y, x) == Piecewise( + (x**(y + 1)/(y + 1), Ne(y, -1)), (log(x), True)) + + +def test_issue_3796(): + assert manualintegrate(diff(exp(x + x**2)), x) == exp(x + x**2) + assert integrate(x * exp(x**4), x, risch=False) == -I*sqrt(pi)*erf(I*x**2)/4 + + +def test_manual_true(): + assert integrate(exp(x) * sin(x), x, manual=True) == \ + (exp(x) * sin(x)) / 2 - (exp(x) * cos(x)) / 2 + assert integrate(sin(x) * cos(x), x, manual=True) in \ + [sin(x) ** 2 / 2, -cos(x)**2 / 2] + + +def test_issue_6746(): + y = Symbol('y') + n = Symbol('n') + assert manualintegrate(y**x, x) == Piecewise( + (y**x/log(y), Ne(log(y), 0)), (x, True)) + assert manualintegrate(y**(n*x), x) == Piecewise( + (Piecewise( + (y**(n*x)/log(y), Ne(log(y), 0)), + (n*x, True) + )/n, Ne(n, 0)), + (x, True)) + assert manualintegrate(exp(n*x), x) == Piecewise( + (exp(n*x)/n, Ne(n, 0)), (x, True)) + + y = Symbol('y', positive=True) + assert manualintegrate((y + 1)**x, x) == (y + 1)**x/log(y + 1) + y = Symbol('y', zero=True) + assert manualintegrate((y + 1)**x, x) == x + y = Symbol('y') + n = Symbol('n', nonzero=True) + assert manualintegrate(y**(n*x), x) == Piecewise( + (y**(n*x)/log(y), Ne(log(y), 0)), (n*x, True))/n + y = Symbol('y', positive=True) + assert manualintegrate((y + 1)**(n*x), x) == \ + (y + 1)**(n*x)/(n*log(y + 1)) + a = Symbol('a', negative=True) + b = Symbol('b') + assert manualintegrate(1/(a + b*x**2), x) == atan(x/sqrt(a/b))/(b*sqrt(a/b)) + b = Symbol('b', negative=True) + assert manualintegrate(1/(a + b*x**2), x) == \ + atan(x/(sqrt(-a)*sqrt(-1/b)))/(b*sqrt(-a)*sqrt(-1/b)) + assert manualintegrate(1/((x**a + y**b + 4)*sqrt(a*x**2 + 1)), x) == \ + y**(-b)*Integral(x**(-a)/(y**(-b)*sqrt(a*x**2 + 1) + + x**(-a)*sqrt(a*x**2 + 1) + 4*x**(-a)*y**(-b)*sqrt(a*x**2 + 1)), x) + assert manualintegrate(1/((x**2 + 4)*sqrt(4*x**2 + 1)), x) == \ + Integral(1/((x**2 + 4)*sqrt(4*x**2 + 1)), x) + assert manualintegrate(1/(x - a**x + x*b**2), x) == \ + Integral(1/(-a**x + b**2*x + x), x) + + +@slow +def test_issue_2850(): + assert manualintegrate(asin(x)*log(x), x) == -x*asin(x) - sqrt(-x**2 + 1) \ + + (x*asin(x) + sqrt(-x**2 + 1))*log(x) - Integral(sqrt(-x**2 + 1)/x, x) + assert manualintegrate(acos(x)*log(x), x) == -x*acos(x) + sqrt(-x**2 + 1) + \ + (x*acos(x) - sqrt(-x**2 + 1))*log(x) + Integral(sqrt(-x**2 + 1)/x, x) + assert manualintegrate(atan(x)*log(x), x) == -x*atan(x) + (x*atan(x) - \ + log(x**2 + 1)/2)*log(x) + log(x**2 + 1)/2 + Integral(log(x**2 + 1)/x, x)/2 + + +def test_issue_9462(): + assert manualintegrate(sin(2*x)*exp(x), x) == exp(x)*sin(2*x)/5 - 2*exp(x)*cos(2*x)/5 + assert not integral_steps(sin(2*x)*exp(x), x).contains_dont_know() + assert manualintegrate((x - 3) / (x**2 - 2*x + 2)**2, x) == \ + Integral(x/(x**4 - 4*x**3 + 8*x**2 - 8*x + 4), x) \ + - 3*Integral(1/(x**4 - 4*x**3 + 8*x**2 - 8*x + 4), x) + + +def test_cyclic_parts(): + f = cos(x)*exp(x/4) + F = 16*exp(x/4)*sin(x)/17 + 4*exp(x/4)*cos(x)/17 + assert manualintegrate(f, x) == F and F.diff(x) == f + f = x*cos(x)*exp(x/4) + F = (x*(16*exp(x/4)*sin(x)/17 + 4*exp(x/4)*cos(x)/17) - + 128*exp(x/4)*sin(x)/289 + 240*exp(x/4)*cos(x)/289) + assert manualintegrate(f, x) == F and F.diff(x) == f + + +@slow +def test_issue_10847_slow(): + assert manualintegrate((4*x**4 + 4*x**3 + 16*x**2 + 12*x + 8) + / (x**6 + 2*x**5 + 3*x**4 + 4*x**3 + 3*x**2 + 2*x + 1), x) == \ + 2*x/(x**2 + 1) + 3*atan(x) - 1/(x**2 + 1) - 3/(x + 1) + + +@slow +def test_issue_10847(): + + assert manualintegrate(x**2 / (x**2 - c), x) == \ + c*Piecewise((atan(x/sqrt(-c))/sqrt(-c), Ne(c, 0)), (-1/x, True)) + x + + rc = Symbol('c', real=True) + assert manualintegrate(x**2 / (x**2 - rc), x) == \ + rc*Piecewise((atan(x/sqrt(-rc))/sqrt(-rc), rc < 0), + ((log(-sqrt(rc) + x) - log(sqrt(rc) + x))/(2*sqrt(rc)), True)) + x + + assert manualintegrate(sqrt(x - y) * log(z / x), x) == \ + 4*y**2*Piecewise((atan(sqrt(x - y)/sqrt(y))/sqrt(y), Ne(y, 0)), + (-1/sqrt(x - y), True))/3 - 4*y*sqrt(x - y)/3 + \ + 2*(x - y)**Rational(3, 2)*log(z/x)/3 + 4*(x - y)**Rational(3, 2)/9 + ry = Symbol('y', real=True) + rz = Symbol('z', real=True) + assert manualintegrate(sqrt(x - ry) * log(rz / x), x) == \ + 4*ry**2*Piecewise((atan(sqrt(x - ry)/sqrt(ry))/sqrt(ry), ry > 0), + ((log(-sqrt(-ry) + sqrt(x - ry)) - log(sqrt(-ry) + sqrt(x - ry)))/(2*sqrt(-ry)), True))/3 \ + - 4*ry*sqrt(x - ry)/3 + 2*(x - ry)**Rational(3, 2)*log(rz/x)/3 \ + + 4*(x - ry)**Rational(3, 2)/9 + + assert manualintegrate(sqrt(x) * log(x), x) == 2*x**Rational(3, 2)*log(x)/3 - 4*x**Rational(3, 2)/9 + + result = manualintegrate(sqrt(a*x + b) / x, x) + assert result == Piecewise((-2*b*Piecewise( + (-atan(sqrt(a*x + b)/sqrt(-b))/sqrt(-b), Ne(b, 0)), + (1/sqrt(a*x + b), True)) + 2*sqrt(a*x + b), Ne(a, 0)), + (sqrt(b)*log(x), True)) + assert piecewise_fold(result) == Piecewise( + (2*b*atan(sqrt(a*x + b)/sqrt(-b))/sqrt(-b) + 2*sqrt(a*x + b), Ne(a, 0) & Ne(b, 0)), + (-2*b/sqrt(a*x + b) + 2*sqrt(a*x + b), Ne(a, 0)), + (sqrt(b)*log(x), True)) + + ra = Symbol('a', real=True) + rb = Symbol('b', real=True) + assert manualintegrate(sqrt(ra*x + rb) / x, x) == \ + Piecewise( + (-2*rb*Piecewise( + (-atan(sqrt(ra*x + rb)/sqrt(-rb))/sqrt(-rb), rb < 0), + (-I*(log(-sqrt(rb) + sqrt(ra*x + rb)) - log(sqrt(rb) + sqrt(ra*x + rb)))/(2*sqrt(-rb)), True)) + + 2*sqrt(ra*x + rb), Ne(ra, 0)), + (sqrt(rb)*log(x), True)) + + assert expand(manualintegrate(sqrt(ra*x + rb) / (x + rc), x)) == \ + Piecewise((-2*ra*rc*Piecewise((atan(sqrt(ra*x + rb)/sqrt(ra*rc - rb))/sqrt(ra*rc - rb), ra*rc - rb > 0), + (log(-sqrt(-ra*rc + rb) + sqrt(ra*x + rb))/(2*sqrt(-ra*rc + rb)) - + log(sqrt(-ra*rc + rb) + sqrt(ra*x + rb))/(2*sqrt(-ra*rc + rb)), True)) + + 2*rb*Piecewise((atan(sqrt(ra*x + rb)/sqrt(ra*rc - rb))/sqrt(ra*rc - rb), ra*rc - rb > 0), + (log(-sqrt(-ra*rc + rb) + sqrt(ra*x + rb))/(2*sqrt(-ra*rc + rb)) - + log(sqrt(-ra*rc + rb) + sqrt(ra*x + rb))/(2*sqrt(-ra*rc + rb)), True)) + + 2*sqrt(ra*x + rb), Ne(ra, 0)), (sqrt(rb)*log(rc + x), True)) + + assert manualintegrate(sqrt(2*x + 3) / (x + 1), x) == 2*sqrt(2*x + 3) - log(sqrt(2*x + 3) + 1) + log(sqrt(2*x + 3) - 1) + assert manualintegrate(sqrt(2*x + 3) / 2 * x, x) == (2*x + 3)**Rational(5, 2)/20 - (2*x + 3)**Rational(3, 2)/4 + assert manualintegrate(x**Rational(3,2) * log(x), x) == 2*x**Rational(5,2)*log(x)/5 - 4*x**Rational(5,2)/25 + assert manualintegrate(x**(-3) * log(x), x) == -log(x)/(2*x**2) - 1/(4*x**2) + assert manualintegrate(log(y)/(y**2*(1 - 1/y)), y) == \ + log(y)*log(-1 + 1/y) - Integral(log(-1 + 1/y)/y, y) + + +def test_issue_12899(): + assert manualintegrate(f(x,y).diff(x),y) == Integral(Derivative(f(x,y),x),y) + assert manualintegrate(f(x,y).diff(y).diff(x),y) == Derivative(f(x,y),x) + + +def test_constant_independent_of_symbol(): + assert manualintegrate(Integral(y, (x, 1, 2)), x) == \ + x*Integral(y, (x, 1, 2)) + + +def test_issue_12641(): + assert manualintegrate(sin(2*x), x) == -cos(2*x)/2 + assert manualintegrate(cos(x)*sin(2*x), x) == -2*cos(x)**3/3 + assert manualintegrate((sin(2*x)*cos(x))/(1 + cos(x)), x) == \ + -2*log(cos(x) + 1) - cos(x)**2 + 2*cos(x) + + +@slow +def test_issue_13297(): + assert manualintegrate(sin(x) * cos(x)**5, x) == -cos(x)**6 / 6 + + +def test_issue_14470(): + assert_is_integral_of(1/(x*sqrt(x + 1)), log(sqrt(x + 1) - 1) - log(sqrt(x + 1) + 1)) + + +@slow +def test_issue_9858(): + assert manualintegrate(exp(x)*cos(exp(x)), x) == sin(exp(x)) + assert manualintegrate(exp(2*x)*cos(exp(x)), x) == \ + exp(x)*sin(exp(x)) + cos(exp(x)) + res = manualintegrate(exp(10*x)*sin(exp(x)), x) + assert not res.has(Integral) + assert res.diff(x) == exp(10*x)*sin(exp(x)) + # an example with many similar integrations by parts + assert manualintegrate(sum(x*exp(k*x) for k in range(1, 8)), x) == ( + x*exp(7*x)/7 + x*exp(6*x)/6 + x*exp(5*x)/5 + x*exp(4*x)/4 + + x*exp(3*x)/3 + x*exp(2*x)/2 + x*exp(x) - exp(7*x)/49 -exp(6*x)/36 - + exp(5*x)/25 - exp(4*x)/16 - exp(3*x)/9 - exp(2*x)/4 - exp(x)) + + +def test_issue_8520(): + assert manualintegrate(x/(x**4 + 1), x) == atan(x**2)/2 + assert manualintegrate(x**2/(x**6 + 25), x) == atan(x**3/5)/15 + f = x/(9*x**4 + 4)**2 + assert manualintegrate(f, x).diff(x).factor() == f + + +def test_manual_subs(): + x, y = symbols('x y') + expr = log(x) + exp(x) + # if log(x) is y, then exp(y) is x + assert manual_subs(expr, log(x), y) == y + exp(exp(y)) + # if exp(x) is y, then log(y) need not be x + assert manual_subs(expr, exp(x), y) == log(x) + y + + raises(ValueError, lambda: manual_subs(expr, x)) + raises(ValueError, lambda: manual_subs(expr, exp(x), x, y)) + + +@slow +def test_issue_15471(): + f = log(x)*cos(log(x))/x**Rational(3, 4) + F = -128*x**Rational(1, 4)*sin(log(x))/289 + 240*x**Rational(1, 4)*cos(log(x))/289 + (16*x**Rational(1, 4)*sin(log(x))/17 + 4*x**Rational(1, 4)*cos(log(x))/17)*log(x) + assert_is_integral_of(f, F) + + +def test_quadratic_denom(): + f = (5*x + 2)/(3*x**2 - 2*x + 8) + assert manualintegrate(f, x) == 5*log(3*x**2 - 2*x + 8)/6 + 11*sqrt(23)*atan(3*sqrt(23)*(x - Rational(1, 3))/23)/69 + g = 3/(2*x**2 + 3*x + 1) + assert manualintegrate(g, x) == 3*log(4*x + 2) - 3*log(4*x + 4) + +def test_issue_22757(): + assert manualintegrate(sin(x), y) == y * sin(x) + + +def test_issue_23348(): + steps = integral_steps(tan(x), x) + constant_times_step = steps.substep.substep + assert constant_times_step.integrand == constant_times_step.constant * constant_times_step.other + + +def test_issue_23566(): + i = Integral(1/sqrt(x**2 - 1), (x, -2, -1)).doit(manual=True) + assert i == -log(4 - 2*sqrt(3)) + log(2) + assert str(i.n()) == '1.31695789692482' + + +def test_issue_25093(): + ap = Symbol('ap', positive=True) + an = Symbol('an', negative=True) + assert manualintegrate(exp(a*x**2 + b), x) == sqrt(pi)*exp(b)*erfi(sqrt(a)*x)/(2*sqrt(a)) + assert manualintegrate(exp(ap*x**2 + b), x) == sqrt(pi)*exp(b)*erfi(sqrt(ap)*x)/(2*sqrt(ap)) + assert manualintegrate(exp(an*x**2 + b), x) == -sqrt(pi)*exp(b)*erf(an*x/sqrt(-an))/(2*sqrt(-an)) + assert manualintegrate(sin(a*x**2 + b), x) == ( + sqrt(2)*sqrt(pi)*(sin(b)*fresnelc(sqrt(2)*sqrt(a)*x/sqrt(pi)) + + cos(b)*fresnels(sqrt(2)*sqrt(a)*x/sqrt(pi)))/(2*sqrt(a))) + assert manualintegrate(cos(a*x**2 + b), x) == ( + sqrt(2)*sqrt(pi)*(-sin(b)*fresnels(sqrt(2)*sqrt(a)*x/sqrt(pi)) + + cos(b)*fresnelc(sqrt(2)*sqrt(a)*x/sqrt(pi)))/(2*sqrt(a))) + + +def test_nested_pow(): + assert_is_integral_of(sqrt(x**2), x*sqrt(x**2)/2) + assert_is_integral_of(sqrt(x**(S(5)/3)), 6*x*sqrt(x**(S(5)/3))/11) + assert_is_integral_of(1/sqrt(x**2), x*log(x)/sqrt(x**2)) + assert_is_integral_of(x*sqrt(x**(-4)), x**2*sqrt(x**-4)*log(x)) + f = (c*(a+b*x)**d)**e + F1 = (c*(a + b*x)**d)**e*(a/b + x)/(d*e + 1) + F2 = (c*(a + b*x)**d)**e*(a/b + x)*log(a/b + x) + assert manualintegrate(f, x) == \ + Piecewise((Piecewise((F1, Ne(d*e, -1)), (F2, True)), Ne(b, 0)), (x*(a**d*c)**e, True)) + assert F1.diff(x).equals(f) + assert F2.diff(x).subs(d*e, -1).equals(f) + + +def test_manualintegrate_sqrt_linear(): + assert_is_integral_of((5*x**3+4)/sqrt(2+3*x), + 10*(3*x + 2)**(S(7)/2)/567 - 4*(3*x + 2)**(S(5)/2)/27 + + 40*(3*x + 2)**(S(3)/2)/81 + 136*sqrt(3*x + 2)/81) + assert manualintegrate(x/sqrt(a+b*x)**3, x) == \ + Piecewise((Mul(2, b**-2, a/sqrt(a + b*x) + sqrt(a + b*x)), Ne(b, 0)), (x**2/(2*a**(S(3)/2)), True)) + assert_is_integral_of((sqrt(3*x+3)+1)/((2*x+2)**(1/S(3))+1), + 3*sqrt(6)*(2*x + 2)**(S(7)/6)/14 - 3*sqrt(6)*(2*x + 2)**(S(5)/6)/10 - + 3*sqrt(6)*(2*x + 2)**(S.One/6)/2 + 3*(2*x + 2)**(S(2)/3)/4 - 3*(2*x + 2)**(S.One/3)/2 + + sqrt(6)*sqrt(2*x + 2)/2 + 3*log((2*x + 2)**(S.One/3) + 1)/2 + + 3*sqrt(6)*atan((2*x + 2)**(S.One/6))/2) + assert_is_integral_of(sqrt(x+sqrt(x)), + 2*sqrt(sqrt(x) + x)*(sqrt(x)/12 + x/3 - S(1)/8) + log(2*sqrt(x) + 2*sqrt(sqrt(x) + x) + 1)/8) + assert_is_integral_of(sqrt(2*x+3+sqrt(4*x+5))**3, + sqrt(2*x + sqrt(4*x + 5) + 3) * + (9*x/10 + 11*(4*x + 5)**(S(3)/2)/40 + sqrt(4*x + 5)/40 + (4*x + 5)**2/10 + S(11)/10)/2) + + +def test_manualintegrate_sqrt_quadratic(): + assert_is_integral_of(1/sqrt((x - I)**2-1), log(2*x + 2*sqrt(x**2 - 2*I*x - 2) - 2*I)) + assert_is_integral_of(1/sqrt(3*x**2+4*x+5), sqrt(3)*asinh(3*sqrt(11)*(x + S(2)/3)/11)/3) + assert_is_integral_of(1/sqrt(-3*x**2+4*x+5), sqrt(3)*asin(3*sqrt(19)*(x - S(2)/3)/19)/3) + assert_is_integral_of(1/sqrt(3*x**2+4*x-5), sqrt(3)*log(6*x + 2*sqrt(3)*sqrt(3*x**2 + 4*x - 5) + 4)/3) + assert_is_integral_of(1/sqrt(4*x**2-4*x+1), (x - S.Half)*log(x - S.Half)/(2*sqrt((x - S.Half)**2))) + assert manualintegrate(1/sqrt(a+b*x+c*x**2), x) == \ + Piecewise((log(b + 2*sqrt(c)*sqrt(a + b*x + c*x**2) + 2*c*x)/sqrt(c), Ne(c, 0) & Ne(a - b**2/(4*c), 0)), + ((b/(2*c) + x)*log(b/(2*c) + x)/sqrt(c*(b/(2*c) + x)**2), Ne(c, 0)), + (2*sqrt(a + b*x)/b, Ne(b, 0)), (x/sqrt(a), True)) + + assert_is_integral_of((7*x+6)/sqrt(3*x**2+4*x+5), + 7*sqrt(3*x**2 + 4*x + 5)/3 + 4*sqrt(3)*asinh(3*sqrt(11)*(x + S(2)/3)/11)/9) + assert_is_integral_of((7*x+6)/sqrt(-3*x**2+4*x+5), + -7*sqrt(-3*x**2 + 4*x + 5)/3 + 32*sqrt(3)*asin(3*sqrt(19)*(x - S(2)/3)/19)/9) + assert_is_integral_of((7*x+6)/sqrt(3*x**2+4*x-5), + 7*sqrt(3*x**2 + 4*x - 5)/3 + 4*sqrt(3)*log(6*x + 2*sqrt(3)*sqrt(3*x**2 + 4*x - 5) + 4)/9) + assert manualintegrate((d+e*x)/sqrt(a+b*x+c*x**2), x) == \ + Piecewise(((-b*e/(2*c) + d) * + Piecewise((log(b + 2*sqrt(c)*sqrt(a + b*x + c*x**2) + 2*c*x)/sqrt(c), Ne(a - b**2/(4*c), 0)), + ((b/(2*c) + x)*log(b/(2*c) + x)/sqrt(c*(b/(2*c) + x)**2), True)) + + e*sqrt(a + b*x + c*x**2)/c, Ne(c, 0)), + ((2*d*sqrt(a + b*x) + 2*e*(-a*sqrt(a + b*x) + (a + b*x)**(S(3)/2)/3)/b)/b, Ne(b, 0)), + ((d*x + e*x**2/2)/sqrt(a), True)) + + assert manualintegrate((3*x**3-x**2+2*x-4)/sqrt(x**2-3*x+2), x) == \ + sqrt(x**2 - 3*x + 2)*(x**2 + 13*x/4 + S(101)/8) + 135*log(2*x + 2*sqrt(x**2 - 3*x + 2) - 3)/16 + + assert_is_integral_of(sqrt(53225*x**2-66732*x+23013), + (x/2 - S(16683)/53225)*sqrt(53225*x**2 - 66732*x + 23013) + + 111576969*sqrt(2129)*asinh(53225*x/10563 - S(11122)/3521)/1133160250) + assert manualintegrate(sqrt(a+c*x**2), x) == \ + Piecewise((a*Piecewise((log(2*sqrt(c)*sqrt(a + c*x**2) + 2*c*x)/sqrt(c), Ne(a, 0)), + (x*log(x)/sqrt(c*x**2), True))/2 + x*sqrt(a + c*x**2)/2, Ne(c, 0)), + (sqrt(a)*x, True)) + assert manualintegrate(sqrt(a+b*x+c*x**2), x) == \ + Piecewise(((a/2 - b**2/(8*c)) * + Piecewise((log(b + 2*sqrt(c)*sqrt(a + b*x + c*x**2) + 2*c*x)/sqrt(c), Ne(a - b**2/(4*c), 0)), + ((b/(2*c) + x)*log(b/(2*c) + x)/sqrt(c*(b/(2*c) + x)**2), True)) + + (b/(4*c) + x/2)*sqrt(a + b*x + c*x**2), Ne(c, 0)), + (2*(a + b*x)**(S(3)/2)/(3*b), Ne(b, 0)), + (sqrt(a)*x, True)) + + assert_is_integral_of(x*sqrt(x**2+2*x+4), + (x**2/3 + x/6 + S(5)/6)*sqrt(x**2 + 2*x + 4) - 3*asinh(sqrt(3)*(x + 1)/3)/2) + + +def test_mul_pow_derivative(): + assert_is_integral_of(x*sec(x)*tan(x), x*sec(x) - log(tan(x) + sec(x))) + assert_is_integral_of(x*sec(x)**2, x*tan(x) + log(cos(x))) + assert_is_integral_of(x**3*Derivative(f(x), (x, 4)), + x**3*Derivative(f(x), (x, 3)) - 3*x**2*Derivative(f(x), (x, 2)) + + 6*x*Derivative(f(x), x) - 6*f(x)) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_meijerint.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_meijerint.py new file mode 100644 index 0000000000000000000000000000000000000000..899bc96e63d6dd5bccd52856f15d3085e87d5807 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_meijerint.py @@ -0,0 +1,774 @@ +from sympy.core.function import expand_func +from sympy.core.numbers import (I, Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.sorting import default_sort_key +from sympy.functions.elementary.complexes import Abs, arg, re, unpolarify +from sympy.functions.elementary.exponential import (exp, exp_polar, log) +from sympy.functions.elementary.hyperbolic import cosh, acosh, sinh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise, piecewise_fold +from sympy.functions.elementary.trigonometric import (cos, sin, sinc, asin) +from sympy.functions.special.error_functions import (erf, erfc) +from sympy.functions.special.gamma_functions import (gamma, polygamma) +from sympy.functions.special.hyper import (hyper, meijerg) +from sympy.integrals.integrals import (Integral, integrate) +from sympy.simplify.hyperexpand import hyperexpand +from sympy.simplify.simplify import simplify +from sympy.integrals.meijerint import (_rewrite_single, _rewrite1, + meijerint_indefinite, _inflate_g, _create_lookup_table, + meijerint_definite, meijerint_inversion) +from sympy.testing.pytest import slow +from sympy.core.random import (verify_numerically, + random_complex_number as randcplx) +from sympy.abc import x, y, a, b, c, d, s, t, z + + +def test_rewrite_single(): + def t(expr, c, m): + e = _rewrite_single(meijerg([a], [b], [c], [d], expr), x) + assert e is not None + assert isinstance(e[0][0][2], meijerg) + assert e[0][0][2].argument.as_coeff_mul(x) == (c, (m,)) + + def tn(expr): + assert _rewrite_single(meijerg([a], [b], [c], [d], expr), x) is None + + t(x, 1, x) + t(x**2, 1, x**2) + t(x**2 + y*x**2, y + 1, x**2) + tn(x**2 + x) + tn(x**y) + + def u(expr, x): + from sympy.core.add import Add + r = _rewrite_single(expr, x) + e = Add(*[res[0]*res[2] for res in r[0]]).replace( + exp_polar, exp) # XXX Hack? + assert verify_numerically(e, expr, x) + + u(exp(-x)*sin(x), x) + + # The following has stopped working because hyperexpand changed slightly. + # It is probably not worth fixing + #u(exp(-x)*sin(x)*cos(x), x) + + # This one cannot be done numerically, since it comes out as a g-function + # of argument 4*pi + # NOTE This also tests a bug in inverse mellin transform (which used to + # turn exp(4*pi*I*t) into a factor of exp(4*pi*I)**t instead of + # exp_polar). + #u(exp(x)*sin(x), x) + assert _rewrite_single(exp(x)*sin(x), x) == \ + ([(-sqrt(2)/(2*sqrt(pi)), 0, + meijerg(((Rational(-1, 2), 0, Rational(1, 4), S.Half, Rational(3, 4)), (1,)), + ((), (Rational(-1, 2), 0)), 64*exp_polar(-4*I*pi)/x**4))], True) + + +def test_rewrite1(): + assert _rewrite1(x**3*meijerg([a], [b], [c], [d], x**2 + y*x**2)*5, x) == \ + (5, x**3, [(1, 0, meijerg([a], [b], [c], [d], x**2*(y + 1)))], True) + + +def test_meijerint_indefinite_numerically(): + def t(fac, arg): + g = meijerg([a], [b], [c], [d], arg)*fac + subs = {a: randcplx()/10, b: randcplx()/10 + I, + c: randcplx(), d: randcplx()} + integral = meijerint_indefinite(g, x) + assert integral is not None + assert verify_numerically(g.subs(subs), integral.diff(x).subs(subs), x) + t(1, x) + t(2, x) + t(1, 2*x) + t(1, x**2) + t(5, x**S('3/2')) + t(x**3, x) + t(3*x**S('3/2'), 4*x**S('7/3')) + + +def test_meijerint_definite(): + v, b = meijerint_definite(x, x, 0, 0) + assert v.is_zero and b is True + v, b = meijerint_definite(x, x, oo, oo) + assert v.is_zero and b is True + + +def test_inflate(): + subs = {a: randcplx()/10, b: randcplx()/10 + I, c: randcplx(), + d: randcplx(), y: randcplx()/10} + + def t(a, b, arg, n): + from sympy.core.mul import Mul + m1 = meijerg(a, b, arg) + m2 = Mul(*_inflate_g(m1, n)) + # NOTE: (the random number)**9 must still be on the principal sheet. + # Thus make b&d small to create random numbers of small imaginary part. + return verify_numerically(m1.subs(subs), m2.subs(subs), x, b=0.1, d=-0.1) + assert t([[a], [b]], [[c], [d]], x, 3) + assert t([[a, y], [b]], [[c], [d]], x, 3) + assert t([[a], [b]], [[c, y], [d]], 2*x**3, 3) + + +def test_recursive(): + from sympy.core.symbol import symbols + a, b, c = symbols('a b c', positive=True) + r = exp(-(x - a)**2)*exp(-(x - b)**2) + e = integrate(r, (x, 0, oo), meijerg=True) + assert simplify(e.expand()) == ( + sqrt(2)*sqrt(pi)*( + (erf(sqrt(2)*(a + b)/2) + 1)*exp(-a**2/2 + a*b - b**2/2))/4) + e = integrate(exp(-(x - a)**2)*exp(-(x - b)**2)*exp(c*x), (x, 0, oo), meijerg=True) + assert simplify(e) == ( + sqrt(2)*sqrt(pi)*(erf(sqrt(2)*(2*a + 2*b + c)/4) + 1)*exp(-a**2 - b**2 + + (2*a + 2*b + c)**2/8)/4) + assert simplify(integrate(exp(-(x - a - b - c)**2), (x, 0, oo), meijerg=True)) == \ + sqrt(pi)/2*(1 + erf(a + b + c)) + assert simplify(integrate(exp(-(x + a + b + c)**2), (x, 0, oo), meijerg=True)) == \ + sqrt(pi)/2*(1 - erf(a + b + c)) + + +@slow +def test_meijerint(): + from sympy.core.function import expand + from sympy.core.symbol import symbols + s, t, mu = symbols('s t mu', real=True) + assert integrate(meijerg([], [], [0], [], s*t) + *meijerg([], [], [mu/2], [-mu/2], t**2/4), + (t, 0, oo)).is_Piecewise + s = symbols('s', positive=True) + assert integrate(x**s*meijerg([[], []], [[0], []], x), (x, 0, oo)) == \ + gamma(s + 1) + assert integrate(x**s*meijerg([[], []], [[0], []], x), (x, 0, oo), + meijerg=True) == gamma(s + 1) + assert isinstance(integrate(x**s*meijerg([[], []], [[0], []], x), + (x, 0, oo), meijerg=False), + Integral) + + assert meijerint_indefinite(exp(x), x) == exp(x) + + # TODO what simplifications should be done automatically? + # This tests "extra case" for antecedents_1. + a, b = symbols('a b', positive=True) + assert simplify(meijerint_definite(x**a, x, 0, b)[0]) == \ + b**(a + 1)/(a + 1) + + # This tests various conditions and expansions: + assert meijerint_definite((x + 1)**3*exp(-x), x, 0, oo) == (16, True) + + # Again, how about simplifications? + sigma, mu = symbols('sigma mu', positive=True) + i, c = meijerint_definite(exp(-((x - mu)/(2*sigma))**2), x, 0, oo) + assert simplify(i) == sqrt(pi)*sigma*(2 - erfc(mu/(2*sigma))) + assert c == True + + i, _ = meijerint_definite(exp(-mu*x)*exp(sigma*x), x, 0, oo) + # TODO it would be nice to test the condition + assert simplify(i) == 1/(mu - sigma) + + # Test substitutions to change limits + assert meijerint_definite(exp(x), x, -oo, 2) == (exp(2), True) + # Note: causes a NaN in _check_antecedents + assert expand(meijerint_definite(exp(x), x, 0, I)[0]) == exp(I) - 1 + assert expand(meijerint_definite(exp(-x), x, 0, x)[0]) == \ + 1 - exp(-exp(I*arg(x))*abs(x)) + + # Test -oo to oo + assert meijerint_definite(exp(-x**2), x, -oo, oo) == (sqrt(pi), True) + assert meijerint_definite(exp(-abs(x)), x, -oo, oo) == (2, True) + assert meijerint_definite(exp(-(2*x - 3)**2), x, -oo, oo) == \ + (sqrt(pi)/2, True) + assert meijerint_definite(exp(-abs(2*x - 3)), x, -oo, oo) == (1, True) + assert meijerint_definite(exp(-((x - mu)/sigma)**2/2)/sqrt(2*pi*sigma**2), + x, -oo, oo) == (1, True) + assert meijerint_definite(sinc(x)**2, x, -oo, oo) == (pi, True) + + # Test one of the extra conditions for 2 g-functinos + assert meijerint_definite(exp(-x)*sin(x), x, 0, oo) == (S.Half, True) + + # Test a bug + def res(n): + return (1/(1 + x**2)).diff(x, n).subs(x, 1)*(-1)**n + for n in range(6): + assert integrate(exp(-x)*sin(x)*x**n, (x, 0, oo), meijerg=True) == \ + res(n) + + # This used to test trigexpand... now it is done by linear substitution + assert simplify(integrate(exp(-x)*sin(x + a), (x, 0, oo), meijerg=True) + ) == sqrt(2)*sin(a + pi/4)/2 + + # Test the condition 14 from prudnikov. + # (This is besselj*besselj in disguise, to stop the product from being + # recognised in the tables.) + a, b, s = symbols('a b s') + assert meijerint_definite(meijerg([], [], [a/2], [-a/2], x/4) + *meijerg([], [], [b/2], [-b/2], x/4)*x**(s - 1), x, 0, oo + ) == ( + (4*2**(2*s - 2)*gamma(-2*s + 1)*gamma(a/2 + b/2 + s) + /(gamma(-a/2 + b/2 - s + 1)*gamma(a/2 - b/2 - s + 1) + *gamma(a/2 + b/2 - s + 1)), + (re(s) < 1) & (re(s) < S(1)/2) & (re(a)/2 + re(b)/2 + re(s) > 0))) + + # test a bug + assert integrate(sin(x**a)*sin(x**b), (x, 0, oo), meijerg=True) == \ + Integral(sin(x**a)*sin(x**b), (x, 0, oo)) + + # test better hyperexpand + assert integrate(exp(-x**2)*log(x), (x, 0, oo), meijerg=True) == \ + (sqrt(pi)*polygamma(0, S.Half)/4).expand() + + # Test hyperexpand bug. + from sympy.functions.special.gamma_functions import lowergamma + n = symbols('n', integer=True) + assert simplify(integrate(exp(-x)*x**n, x, meijerg=True)) == \ + lowergamma(n + 1, x) + + # Test a bug with argument 1/x + alpha = symbols('alpha', positive=True) + assert meijerint_definite((2 - x)**alpha*sin(alpha/x), x, 0, 2) == \ + (sqrt(pi)*alpha*gamma(alpha + 1)*meijerg(((), (alpha/2 + S.Half, + alpha/2 + 1)), ((0, 0, S.Half), (Rational(-1, 2),)), alpha**2/16)/4, True) + + # test a bug related to 3016 + a, s = symbols('a s', positive=True) + assert simplify(integrate(x**s*exp(-a*x**2), (x, -oo, oo))) == \ + a**(-s/2 - S.Half)*((-1)**s + 1)*gamma(s/2 + S.Half)/2 + + +def test_bessel(): + from sympy.functions.special.bessel import (besseli, besselj) + assert simplify(integrate(besselj(a, z)*besselj(b, z)/z, (z, 0, oo), + meijerg=True, conds='none')) == \ + 2*sin(pi*(a/2 - b/2))/(pi*(a - b)*(a + b)) + assert simplify(integrate(besselj(a, z)*besselj(a, z)/z, (z, 0, oo), + meijerg=True, conds='none')) == 1/(2*a) + + # TODO more orthogonality integrals + + assert simplify(integrate(sin(z*x)*(x**2 - 1)**(-(y + S.Half)), + (x, 1, oo), meijerg=True, conds='none') + *2/((z/2)**y*sqrt(pi)*gamma(S.Half - y))) == \ + besselj(y, z) + + # Werner Rosenheinrich + # SOME INDEFINITE INTEGRALS OF BESSEL FUNCTIONS + + assert integrate(x*besselj(0, x), x, meijerg=True) == x*besselj(1, x) + assert integrate(x*besseli(0, x), x, meijerg=True) == x*besseli(1, x) + # TODO can do higher powers, but come out as high order ... should they be + # reduced to order 0, 1? + assert integrate(besselj(1, x), x, meijerg=True) == -besselj(0, x) + assert integrate(besselj(1, x)**2/x, x, meijerg=True) == \ + -(besselj(0, x)**2 + besselj(1, x)**2)/2 + # TODO more besseli when tables are extended or recursive mellin works + assert integrate(besselj(0, x)**2/x**2, x, meijerg=True) == \ + -2*x*besselj(0, x)**2 - 2*x*besselj(1, x)**2 \ + + 2*besselj(0, x)*besselj(1, x) - besselj(0, x)**2/x + assert integrate(besselj(0, x)*besselj(1, x), x, meijerg=True) == \ + -besselj(0, x)**2/2 + assert integrate(x**2*besselj(0, x)*besselj(1, x), x, meijerg=True) == \ + x**2*besselj(1, x)**2/2 + assert integrate(besselj(0, x)*besselj(1, x)/x, x, meijerg=True) == \ + (x*besselj(0, x)**2 + x*besselj(1, x)**2 - + besselj(0, x)*besselj(1, x)) + # TODO how does besselj(0, a*x)*besselj(0, b*x) work? + # TODO how does besselj(0, x)**2*besselj(1, x)**2 work? + # TODO sin(x)*besselj(0, x) etc come out a mess + # TODO can x*log(x)*besselj(0, x) be done? + # TODO how does besselj(1, x)*besselj(0, x+a) work? + # TODO more indefinite integrals when struve functions etc are implemented + + # test a substitution + assert integrate(besselj(1, x**2)*x, x, meijerg=True) == \ + -besselj(0, x**2)/2 + + +def test_inversion(): + from sympy.functions.special.bessel import besselj + from sympy.functions.special.delta_functions import Heaviside + + def inv(f): + return piecewise_fold(meijerint_inversion(f, s, t)) + assert inv(1/(s**2 + 1)) == sin(t)*Heaviside(t) + assert inv(s/(s**2 + 1)) == cos(t)*Heaviside(t) + assert inv(exp(-s)/s) == Heaviside(t - 1) + assert inv(1/sqrt(1 + s**2)) == besselj(0, t)*Heaviside(t) + + # Test some antcedents checking. + assert meijerint_inversion(sqrt(s)/sqrt(1 + s**2), s, t) is None + assert inv(exp(s**2)) is None + assert meijerint_inversion(exp(-s**2), s, t) is None + + +def test_inversion_conditional_output(): + from sympy.core.symbol import Symbol + from sympy.integrals.transforms import InverseLaplaceTransform + + a = Symbol('a', positive=True) + F = sqrt(pi/a)*exp(-2*sqrt(a)*sqrt(s)) + f = meijerint_inversion(F, s, t) + assert not f.is_Piecewise + + b = Symbol('b', real=True) + F = F.subs(a, b) + f2 = meijerint_inversion(F, s, t) + assert f2.is_Piecewise + # first piece is same as f + assert f2.args[0][0] == f.subs(a, b) + # last piece is an unevaluated transform + assert f2.args[-1][1] + ILT = InverseLaplaceTransform(F, s, t, None) + assert f2.args[-1][0] == ILT or f2.args[-1][0] == ILT.as_integral + + +def test_inversion_exp_real_nonreal_shift(): + from sympy.core.symbol import Symbol + from sympy.functions.special.delta_functions import DiracDelta + r = Symbol('r', real=True) + c = Symbol('c', extended_real=False) + a = 1 + 2*I + z = Symbol('z') + assert not meijerint_inversion(exp(r*s), s, t).is_Piecewise + assert meijerint_inversion(exp(a*s), s, t) is None + assert meijerint_inversion(exp(c*s), s, t) is None + f = meijerint_inversion(exp(z*s), s, t) + assert f.is_Piecewise + assert isinstance(f.args[0][0], DiracDelta) + + +@slow +def test_lookup_table(): + from sympy.core.random import uniform, randrange + from sympy.core.add import Add + from sympy.integrals.meijerint import z as z_dummy + table = {} + _create_lookup_table(table) + for l in table.values(): + for formula, terms, cond, hint in sorted(l, key=default_sort_key): + subs = {} + for ai in list(formula.free_symbols) + [z_dummy]: + if hasattr(ai, 'properties') and ai.properties: + # these Wilds match positive integers + subs[ai] = randrange(1, 10) + else: + subs[ai] = uniform(1.5, 2.0) + if not isinstance(terms, list): + terms = terms(subs) + + # First test that hyperexpand can do this. + expanded = [hyperexpand(g) for (_, g) in terms] + assert all(x.is_Piecewise or not x.has(meijerg) for x in expanded) + + # Now test that the meijer g-function is indeed as advertised. + expanded = Add(*[f*x for (f, x) in terms]) + a, b = formula.n(subs=subs), expanded.n(subs=subs) + r = min(abs(a), abs(b)) + if r < 1: + assert abs(a - b).n() <= 1e-10 + else: + assert (abs(a - b)/r).n() <= 1e-10 + + +def test_branch_bug(): + from sympy.functions.special.gamma_functions import lowergamma + from sympy.simplify.powsimp import powdenest + # TODO gammasimp cannot prove that the factor is unity + assert powdenest(integrate(erf(x**3), x, meijerg=True).diff(x), + polar=True) == 2*erf(x**3)*gamma(Rational(2, 3))/3/gamma(Rational(5, 3)) + assert integrate(erf(x**3), x, meijerg=True) == \ + 2*x*erf(x**3)*gamma(Rational(2, 3))/(3*gamma(Rational(5, 3))) \ + - 2*gamma(Rational(2, 3))*lowergamma(Rational(2, 3), x**6)/(3*sqrt(pi)*gamma(Rational(5, 3))) + + +def test_linear_subs(): + from sympy.functions.special.bessel import besselj + assert integrate(sin(x - 1), x, meijerg=True) == -cos(1 - x) + assert integrate(besselj(1, x - 1), x, meijerg=True) == -besselj(0, 1 - x) + + +@slow +def test_probability(): + # various integrals from probability theory + from sympy.core.function import expand_mul + from sympy.core.symbol import (Symbol, symbols) + from sympy.simplify.gammasimp import gammasimp + from sympy.simplify.powsimp import powsimp + mu1, mu2 = symbols('mu1 mu2', nonzero=True) + sigma1, sigma2 = symbols('sigma1 sigma2', positive=True) + rate = Symbol('lambda', positive=True) + + def normal(x, mu, sigma): + return 1/sqrt(2*pi*sigma**2)*exp(-(x - mu)**2/2/sigma**2) + + def exponential(x, rate): + return rate*exp(-rate*x) + + assert integrate(normal(x, mu1, sigma1), (x, -oo, oo), meijerg=True) == 1 + assert integrate(x*normal(x, mu1, sigma1), (x, -oo, oo), meijerg=True) == \ + mu1 + assert integrate(x**2*normal(x, mu1, sigma1), (x, -oo, oo), meijerg=True) \ + == mu1**2 + sigma1**2 + assert integrate(x**3*normal(x, mu1, sigma1), (x, -oo, oo), meijerg=True) \ + == mu1**3 + 3*mu1*sigma1**2 + assert integrate(normal(x, mu1, sigma1)*normal(y, mu2, sigma2), + (x, -oo, oo), (y, -oo, oo), meijerg=True) == 1 + assert integrate(x*normal(x, mu1, sigma1)*normal(y, mu2, sigma2), + (x, -oo, oo), (y, -oo, oo), meijerg=True) == mu1 + assert integrate(y*normal(x, mu1, sigma1)*normal(y, mu2, sigma2), + (x, -oo, oo), (y, -oo, oo), meijerg=True) == mu2 + assert integrate(x*y*normal(x, mu1, sigma1)*normal(y, mu2, sigma2), + (x, -oo, oo), (y, -oo, oo), meijerg=True) == mu1*mu2 + assert integrate((x + y + 1)*normal(x, mu1, sigma1)*normal(y, mu2, sigma2), + (x, -oo, oo), (y, -oo, oo), meijerg=True) == 1 + mu1 + mu2 + assert integrate((x + y - 1)*normal(x, mu1, sigma1)*normal(y, mu2, sigma2), + (x, -oo, oo), (y, -oo, oo), meijerg=True) == \ + -1 + mu1 + mu2 + + i = integrate(x**2*normal(x, mu1, sigma1)*normal(y, mu2, sigma2), + (x, -oo, oo), (y, -oo, oo), meijerg=True) + assert not i.has(Abs) + assert simplify(i) == mu1**2 + sigma1**2 + assert integrate(y**2*normal(x, mu1, sigma1)*normal(y, mu2, sigma2), + (x, -oo, oo), (y, -oo, oo), meijerg=True) == \ + sigma2**2 + mu2**2 + + assert integrate(exponential(x, rate), (x, 0, oo), meijerg=True) == 1 + assert integrate(x*exponential(x, rate), (x, 0, oo), meijerg=True) == \ + 1/rate + assert integrate(x**2*exponential(x, rate), (x, 0, oo), meijerg=True) == \ + 2/rate**2 + + def E(expr): + res1 = integrate(expr*exponential(x, rate)*normal(y, mu1, sigma1), + (x, 0, oo), (y, -oo, oo), meijerg=True) + res2 = integrate(expr*exponential(x, rate)*normal(y, mu1, sigma1), + (y, -oo, oo), (x, 0, oo), meijerg=True) + assert expand_mul(res1) == expand_mul(res2) + return res1 + + assert E(1) == 1 + assert E(x*y) == mu1/rate + assert E(x*y**2) == mu1**2/rate + sigma1**2/rate + ans = sigma1**2 + 1/rate**2 + assert simplify(E((x + y + 1)**2) - E(x + y + 1)**2) == ans + assert simplify(E((x + y - 1)**2) - E(x + y - 1)**2) == ans + assert simplify(E((x + y)**2) - E(x + y)**2) == ans + + # Beta' distribution + alpha, beta = symbols('alpha beta', positive=True) + betadist = x**(alpha - 1)*(1 + x)**(-alpha - beta)*gamma(alpha + beta) \ + /gamma(alpha)/gamma(beta) + assert integrate(betadist, (x, 0, oo), meijerg=True) == 1 + i = integrate(x*betadist, (x, 0, oo), meijerg=True, conds='separate') + assert (gammasimp(i[0]), i[1]) == (alpha/(beta - 1), 1 < beta) + j = integrate(x**2*betadist, (x, 0, oo), meijerg=True, conds='separate') + assert j[1] == (beta > 2) + assert gammasimp(j[0] - i[0]**2) == (alpha + beta - 1)*alpha \ + /(beta - 2)/(beta - 1)**2 + + # Beta distribution + # NOTE: this is evaluated using antiderivatives. It also tests that + # meijerint_indefinite returns the simplest possible answer. + a, b = symbols('a b', positive=True) + betadist = x**(a - 1)*(-x + 1)**(b - 1)*gamma(a + b)/(gamma(a)*gamma(b)) + assert simplify(integrate(betadist, (x, 0, 1), meijerg=True)) == 1 + assert simplify(integrate(x*betadist, (x, 0, 1), meijerg=True)) == \ + a/(a + b) + assert simplify(integrate(x**2*betadist, (x, 0, 1), meijerg=True)) == \ + a*(a + 1)/(a + b)/(a + b + 1) + assert simplify(integrate(x**y*betadist, (x, 0, 1), meijerg=True)) == \ + gamma(a + b)*gamma(a + y)/gamma(a)/gamma(a + b + y) + + # Chi distribution + k = Symbol('k', integer=True, positive=True) + chi = 2**(1 - k/2)*x**(k - 1)*exp(-x**2/2)/gamma(k/2) + assert powsimp(integrate(chi, (x, 0, oo), meijerg=True)) == 1 + assert simplify(integrate(x*chi, (x, 0, oo), meijerg=True)) == \ + sqrt(2)*gamma((k + 1)/2)/gamma(k/2) + assert simplify(integrate(x**2*chi, (x, 0, oo), meijerg=True)) == k + + # Chi^2 distribution + chisquared = 2**(-k/2)/gamma(k/2)*x**(k/2 - 1)*exp(-x/2) + assert powsimp(integrate(chisquared, (x, 0, oo), meijerg=True)) == 1 + assert simplify(integrate(x*chisquared, (x, 0, oo), meijerg=True)) == k + assert simplify(integrate(x**2*chisquared, (x, 0, oo), meijerg=True)) == \ + k*(k + 2) + assert gammasimp(integrate(((x - k)/sqrt(2*k))**3*chisquared, (x, 0, oo), + meijerg=True)) == 2*sqrt(2)/sqrt(k) + + # Dagum distribution + a, b, p = symbols('a b p', positive=True) + # XXX (x/b)**a does not work + dagum = a*p/x*(x/b)**(a*p)/(1 + x**a/b**a)**(p + 1) + assert simplify(integrate(dagum, (x, 0, oo), meijerg=True)) == 1 + # XXX conditions are a mess + arg = x*dagum + assert simplify(integrate(arg, (x, 0, oo), meijerg=True, conds='none') + ) == a*b*gamma(1 - 1/a)*gamma(p + 1 + 1/a)/( + (a*p + 1)*gamma(p)) + assert simplify(integrate(x*arg, (x, 0, oo), meijerg=True, conds='none') + ) == a*b**2*gamma(1 - 2/a)*gamma(p + 1 + 2/a)/( + (a*p + 2)*gamma(p)) + + # F-distribution + d1, d2 = symbols('d1 d2', positive=True) + f = sqrt(((d1*x)**d1 * d2**d2)/(d1*x + d2)**(d1 + d2))/x \ + /gamma(d1/2)/gamma(d2/2)*gamma((d1 + d2)/2) + assert simplify(integrate(f, (x, 0, oo), meijerg=True)) == 1 + # TODO conditions are a mess + assert simplify(integrate(x*f, (x, 0, oo), meijerg=True, conds='none') + ) == d2/(d2 - 2) + assert simplify(integrate(x**2*f, (x, 0, oo), meijerg=True, conds='none') + ) == d2**2*(d1 + 2)/d1/(d2 - 4)/(d2 - 2) + + # TODO gamma, rayleigh + + # inverse gaussian + lamda, mu = symbols('lamda mu', positive=True) + dist = sqrt(lamda/2/pi)*x**(Rational(-3, 2))*exp(-lamda*(x - mu)**2/x/2/mu**2) + mysimp = lambda expr: simplify(expr.rewrite(exp)) + assert mysimp(integrate(dist, (x, 0, oo))) == 1 + assert mysimp(integrate(x*dist, (x, 0, oo))) == mu + assert mysimp(integrate((x - mu)**2*dist, (x, 0, oo))) == mu**3/lamda + assert mysimp(integrate((x - mu)**3*dist, (x, 0, oo))) == 3*mu**5/lamda**2 + + # Levi + c = Symbol('c', positive=True) + assert integrate(sqrt(c/2/pi)*exp(-c/2/(x - mu))/(x - mu)**S('3/2'), + (x, mu, oo)) == 1 + # higher moments oo + + # log-logistic + alpha, beta = symbols('alpha beta', positive=True) + distn = (beta/alpha)*x**(beta - 1)/alpha**(beta - 1)/ \ + (1 + x**beta/alpha**beta)**2 + # FIXME: If alpha, beta are not declared as finite the line below hangs + # after the changes in: + # https://github.com/sympy/sympy/pull/16603 + assert simplify(integrate(distn, (x, 0, oo))) == 1 + # NOTE the conditions are a mess, but correctly state beta > 1 + assert simplify(integrate(x*distn, (x, 0, oo), conds='none')) == \ + pi*alpha/beta/sin(pi/beta) + # (similar comment for conditions applies) + assert simplify(integrate(x**y*distn, (x, 0, oo), conds='none')) == \ + pi*alpha**y*y/beta/sin(pi*y/beta) + + # weibull + k = Symbol('k', positive=True) + n = Symbol('n', positive=True) + distn = k/lamda*(x/lamda)**(k - 1)*exp(-(x/lamda)**k) + assert simplify(integrate(distn, (x, 0, oo))) == 1 + assert simplify(integrate(x**n*distn, (x, 0, oo))) == \ + lamda**n*gamma(1 + n/k) + + # rice distribution + from sympy.functions.special.bessel import besseli + nu, sigma = symbols('nu sigma', positive=True) + rice = x/sigma**2*exp(-(x**2 + nu**2)/2/sigma**2)*besseli(0, x*nu/sigma**2) + assert integrate(rice, (x, 0, oo), meijerg=True) == 1 + # can someone verify higher moments? + + # Laplace distribution + mu = Symbol('mu', real=True) + b = Symbol('b', positive=True) + laplace = exp(-abs(x - mu)/b)/2/b + assert integrate(laplace, (x, -oo, oo), meijerg=True) == 1 + assert integrate(x*laplace, (x, -oo, oo), meijerg=True) == mu + assert integrate(x**2*laplace, (x, -oo, oo), meijerg=True) == \ + 2*b**2 + mu**2 + + # TODO are there other distributions supported on (-oo, oo) that we can do? + + # misc tests + k = Symbol('k', positive=True) + assert gammasimp(expand_mul(integrate(log(x)*x**(k - 1)*exp(-x)/gamma(k), + (x, 0, oo)))) == polygamma(0, k) + + +@slow +def test_expint(): + """ Test various exponential integrals. """ + from sympy.core.symbol import Symbol + from sympy.functions.elementary.hyperbolic import sinh + from sympy.functions.special.error_functions import (Chi, Ci, Ei, Shi, Si, expint) + assert simplify(unpolarify(integrate(exp(-z*x)/x**y, (x, 1, oo), + meijerg=True, conds='none' + ).rewrite(expint).expand(func=True))) == expint(y, z) + + assert integrate(exp(-z*x)/x, (x, 1, oo), meijerg=True, + conds='none').rewrite(expint).expand() == \ + expint(1, z) + assert integrate(exp(-z*x)/x**2, (x, 1, oo), meijerg=True, + conds='none').rewrite(expint).expand() == \ + expint(2, z).rewrite(Ei).rewrite(expint) + assert integrate(exp(-z*x)/x**3, (x, 1, oo), meijerg=True, + conds='none').rewrite(expint).expand() == \ + expint(3, z).rewrite(Ei).rewrite(expint).expand() + + t = Symbol('t', positive=True) + assert integrate(-cos(x)/x, (x, t, oo), meijerg=True).expand() == Ci(t) + assert integrate(-sin(x)/x, (x, t, oo), meijerg=True).expand() == \ + Si(t) - pi/2 + assert integrate(sin(x)/x, (x, 0, z), meijerg=True) == Si(z) + assert integrate(sinh(x)/x, (x, 0, z), meijerg=True) == Shi(z) + assert integrate(exp(-x)/x, x, meijerg=True).expand().rewrite(expint) == \ + I*pi - expint(1, x) + assert integrate(exp(-x)/x**2, x, meijerg=True).rewrite(expint).expand() \ + == expint(1, x) - exp(-x)/x - I*pi + + u = Symbol('u', polar=True) + assert integrate(cos(u)/u, u, meijerg=True).expand().as_independent(u)[1] \ + == Ci(u) + assert integrate(cosh(u)/u, u, meijerg=True).expand().as_independent(u)[1] \ + == Chi(u) + + assert integrate(expint(1, x), x, meijerg=True + ).rewrite(expint).expand() == x*expint(1, x) - exp(-x) + assert integrate(expint(2, x), x, meijerg=True + ).rewrite(expint).expand() == \ + -x**2*expint(1, x)/2 + x*exp(-x)/2 - exp(-x)/2 + assert simplify(unpolarify(integrate(expint(y, x), x, + meijerg=True).rewrite(expint).expand(func=True))) == \ + -expint(y + 1, x) + + assert integrate(Si(x), x, meijerg=True) == x*Si(x) + cos(x) + assert integrate(Ci(u), u, meijerg=True).expand() == u*Ci(u) - sin(u) + assert integrate(Shi(x), x, meijerg=True) == x*Shi(x) - cosh(x) + assert integrate(Chi(u), u, meijerg=True).expand() == u*Chi(u) - sinh(u) + + assert integrate(Si(x)*exp(-x), (x, 0, oo), meijerg=True) == pi/4 + assert integrate(expint(1, x)*sin(x), (x, 0, oo), meijerg=True) == log(2)/2 + + +def test_messy(): + from sympy.functions.elementary.hyperbolic import (acosh, acoth) + from sympy.functions.elementary.trigonometric import (asin, atan) + from sympy.functions.special.bessel import besselj + from sympy.functions.special.error_functions import (Chi, E1, Shi, Si) + from sympy.integrals.transforms import (fourier_transform, laplace_transform) + assert (laplace_transform(Si(x), x, s, simplify=True) == + ((-atan(s) + pi/2)/s, 0, True)) + + assert laplace_transform(Shi(x), x, s, simplify=True) == ( + acoth(s)/s, -oo, s**2 > 1) + + # where should the logs be simplified? + assert laplace_transform(Chi(x), x, s, simplify=True) == ( + (log(s**(-2)) - log(1 - 1/s**2))/(2*s), -oo, s**2 > 1) + + # TODO maybe simplify the inequalities? when the simplification + # allows for generators instead of symbols this will work + assert laplace_transform(besselj(a, x), x, s)[1:] == \ + (0, (re(a) > -2) & (re(a) > -1)) + + # NOTE s < 0 can be done, but argument reduction is not good enough yet + ans = fourier_transform(besselj(1, x)/x, x, s, noconds=False) + assert (ans[0].factor(deep=True).expand(), ans[1]) == \ + (Piecewise((0, (s > 1/(2*pi)) | (s < -1/(2*pi))), + (2*sqrt(-4*pi**2*s**2 + 1), True)), s > 0) + # TODO FT(besselj(0,x)) - conditions are messy (but for acceptable reasons) + # - folding could be better + + assert integrate(E1(x)*besselj(0, x), (x, 0, oo), meijerg=True) == \ + log(1 + sqrt(2)) + assert integrate(E1(x)*besselj(1, x), (x, 0, oo), meijerg=True) == \ + log(S.Half + sqrt(2)/2) + + assert integrate(1/x/sqrt(1 - x**2), x, meijerg=True) == \ + Piecewise((-acosh(1/x), abs(x**(-2)) > 1), (I*asin(1/x), True)) + + +def test_issue_6122(): + assert integrate(exp(-I*x**2), (x, -oo, oo), meijerg=True) == \ + -I*sqrt(pi)*exp(I*pi/4) + + +def test_issue_6252(): + expr = 1/x/(a + b*x)**Rational(1, 3) + anti = integrate(expr, x, meijerg=True) + assert not anti.has(hyper) + # XXX the expression is a mess, but actually upon differentiation and + # putting in numerical values seems to work... + + +def test_issue_6348(): + assert integrate(exp(I*x)/(1 + x**2), (x, -oo, oo)).simplify().rewrite(exp) \ + == pi*exp(-1) + + +def test_fresnel(): + from sympy.functions.special.error_functions import (fresnelc, fresnels) + + assert expand_func(integrate(sin(pi*x**2/2), x)) == fresnels(x) + assert expand_func(integrate(cos(pi*x**2/2), x)) == fresnelc(x) + + +def test_issue_6860(): + assert meijerint_indefinite(x**x**x, x) is None + + +def test_issue_7337(): + f = meijerint_indefinite(x*sqrt(2*x + 3), x).together() + assert f == sqrt(2*x + 3)*(2*x**2 + x - 3)/5 + assert f._eval_interval(x, S.NegativeOne, S.One) == Rational(2, 5) + + +def test_issue_8368(): + assert meijerint_indefinite(cosh(x)*exp(-x*t), x) == ( + (-t - 1)*exp(x) + (-t + 1)*exp(-x))*exp(-t*x)/2/(t**2 - 1) + + +def test_issue_10211(): + from sympy.abc import h, w + assert integrate((1/sqrt((y-x)**2 + h**2)**3), (x,0,w), (y,0,w)) == \ + 2*sqrt(1 + w**2/h**2)/h - 2/h + + +def test_issue_11806(): + from sympy.core.symbol import symbols + y, L = symbols('y L', positive=True) + assert integrate(1/sqrt(x**2 + y**2)**3, (x, -L, L)) == \ + 2*L/(y**2*sqrt(L**2 + y**2)) + +def test_issue_10681(): + from sympy.polys.domains.realfield import RR + from sympy.abc import R, r + f = integrate(r**2*(R**2-r**2)**0.5, r, meijerg=True) + g = (1.0/3)*R**1.0*r**3*hyper((-0.5, Rational(3, 2)), (Rational(5, 2),), + r**2*exp_polar(2*I*pi)/R**2) + assert RR.almosteq((f/g).n(), 1.0, 1e-12) + +def test_issue_13536(): + from sympy.core.symbol import Symbol + a = Symbol('a', positive=True) + assert integrate(1/x**2, (x, oo, a)) == -1/a + + +def test_issue_6462(): + from sympy.core.symbol import Symbol + x = Symbol('x') + n = Symbol('n') + # Not the actual issue, still wrong answer for n = 1, but that there is no + # exception + assert integrate(cos(x**n)/x**n, x, meijerg=True).subs(n, 2).equals( + integrate(cos(x**2)/x**2, x, meijerg=True)) + + +def test_indefinite_1_bug(): + assert integrate((b + t)**(-a), t, meijerg=True) == -b*(1 + t/b)**(1 - a)/(a*b**a - b**a) + + +def test_pr_23583(): + # This result is wrong. Check whether new result is correct when this test fail. + assert integrate(1/sqrt((x - I)**2-1), meijerg=True) == \ + Piecewise((acosh(x - I), Abs((x - I)**2) > 1), (-I*asin(x - I), True)) + + +# 25786 +def test_integrate_function_of_square_over_negatives(): + assert integrate(exp(-x**2), (x,-5,0), meijerg=True) == sqrt(pi)/2 * erf(5) + + +def test_issue_25949(): + from sympy.core.symbol import symbols + y = symbols("y", nonzero=True) + assert integrate(cosh(y*(x + 1)), (x, -1, -0.25), meijerg=True) == sinh(0.75*y)/y diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_prde.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_prde.py new file mode 100644 index 0000000000000000000000000000000000000000..a7429ea8634c742eb77cdb26f99b2cb15853cd42 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_prde.py @@ -0,0 +1,322 @@ +"""Most of these tests come from the examples in Bronstein's book.""" +from sympy.integrals.risch import DifferentialExtension, derivation +from sympy.integrals.prde import (prde_normal_denom, prde_special_denom, + prde_linear_constraints, constant_system, prde_spde, prde_no_cancel_b_large, + prde_no_cancel_b_small, limited_integrate_reduce, limited_integrate, + is_deriv_k, is_log_deriv_k_t_radical, parametric_log_deriv_heu, + is_log_deriv_k_t_radical_in_field, param_poly_rischDE, param_rischDE, + prde_cancel_liouvillian) + +from sympy.polys.polymatrix import PolyMatrix as Matrix + +from sympy.core.numbers import Rational +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.polys.domains.rationalfield import QQ +from sympy.polys.polytools import Poly +from sympy.abc import x, t, n + +t0, t1, t2, t3, k = symbols('t:4 k') + + +def test_prde_normal_denom(): + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1 + t**2, t)]}) + fa = Poly(1, t) + fd = Poly(x, t) + G = [(Poly(t, t), Poly(1 + t**2, t)), (Poly(1, t), Poly(x + x*t**2, t))] + assert prde_normal_denom(fa, fd, G, DE) == \ + (Poly(x, t, domain='ZZ(x)'), (Poly(1, t, domain='ZZ(x)'), Poly(1, t, + domain='ZZ(x)')), [(Poly(x*t, t, domain='ZZ(x)'), + Poly(t**2 + 1, t, domain='ZZ(x)')), (Poly(1, t, domain='ZZ(x)'), + Poly(t**2 + 1, t, domain='ZZ(x)'))], Poly(1, t, domain='ZZ(x)')) + G = [(Poly(t, t), Poly(t**2 + 2*t + 1, t)), (Poly(x*t, t), + Poly(t**2 + 2*t + 1, t)), (Poly(x*t**2, t), Poly(t**2 + 2*t + 1, t))] + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)]}) + assert prde_normal_denom(Poly(x, t), Poly(1, t), G, DE) == \ + (Poly(t + 1, t), (Poly((-1 + x)*t + x, t), Poly(1, t, domain='ZZ[x]')), [(Poly(t, t), + Poly(1, t)), (Poly(x*t, t), Poly(1, t, domain='ZZ[x]')), (Poly(x*t**2, t), + Poly(1, t, domain='ZZ[x]'))], Poly(t + 1, t)) + + +def test_prde_special_denom(): + a = Poly(t + 1, t) + ba = Poly(t**2, t) + bd = Poly(1, t) + G = [(Poly(t, t), Poly(1, t)), (Poly(t**2, t), Poly(1, t)), (Poly(t**3, t), Poly(1, t))] + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)]}) + assert prde_special_denom(a, ba, bd, G, DE) == \ + (Poly(t + 1, t), Poly(t**2, t), [(Poly(t, t), Poly(1, t)), + (Poly(t**2, t), Poly(1, t)), (Poly(t**3, t), Poly(1, t))], Poly(1, t)) + G = [(Poly(t, t), Poly(1, t)), (Poly(1, t), Poly(t, t))] + assert prde_special_denom(Poly(1, t), Poly(t**2, t), Poly(1, t), G, DE) == \ + (Poly(1, t), Poly(t**2 - 1, t), [(Poly(t**2, t), Poly(1, t)), + (Poly(1, t), Poly(1, t))], Poly(t, t)) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(-2*x*t0, t0)]}) + DE.decrement_level() + G = [(Poly(t, t), Poly(t**2, t)), (Poly(2*t, t), Poly(t, t))] + assert prde_special_denom(Poly(5*x*t + 1, t), Poly(t**2 + 2*x**3*t, t), Poly(t**3 + 2, t), G, DE) == \ + (Poly(5*x*t + 1, t), Poly(0, t, domain='ZZ[x]'), [(Poly(t, t), Poly(t**2, t)), + (Poly(2*t, t), Poly(t, t))], Poly(1, x)) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly((t**2 + 1)*2*x, t)]}) + G = [(Poly(t + x, t), Poly(t*x, t)), (Poly(2*t, t), Poly(x**2, x))] + assert prde_special_denom(Poly(5*x*t + 1, t), Poly(t**2 + 2*x**3*t, t), Poly(t**3, t), G, DE) == \ + (Poly(5*x*t + 1, t), Poly(0, t, domain='ZZ[x]'), [(Poly(t + x, t), Poly(x*t, t)), + (Poly(2*t, t, x), Poly(x**2, t, x))], Poly(1, t)) + assert prde_special_denom(Poly(t + 1, t), Poly(t**2, t), Poly(t**3, t), G, DE) == \ + (Poly(t + 1, t), Poly(0, t, domain='ZZ[x]'), [(Poly(t + x, t), Poly(x*t, t)), (Poly(2*t, t, x), + Poly(x**2, t, x))], Poly(1, t)) + + +def test_prde_linear_constraints(): + DE = DifferentialExtension(extension={'D': [Poly(1, x)]}) + G = [(Poly(2*x**3 + 3*x + 1, x), Poly(x**2 - 1, x)), (Poly(1, x), Poly(x - 1, x)), + (Poly(1, x), Poly(x + 1, x))] + assert prde_linear_constraints(Poly(1, x), Poly(0, x), G, DE) == \ + ((Poly(2*x, x, domain='QQ'), Poly(0, x, domain='QQ'), Poly(0, x, domain='QQ')), + Matrix([[1, 1, -1], [5, 1, 1]], x)) + G = [(Poly(t, t), Poly(1, t)), (Poly(t**2, t), Poly(1, t)), (Poly(t**3, t), Poly(1, t))] + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)]}) + assert prde_linear_constraints(Poly(t + 1, t), Poly(t**2, t), G, DE) == \ + ((Poly(t, t, domain='QQ'), Poly(t**2, t, domain='QQ'), Poly(t**3, t, domain='QQ')), + Matrix(0, 3, [], t)) + G = [(Poly(2*x, t), Poly(t, t)), (Poly(-x, t), Poly(t, t))] + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t)]}) + assert prde_linear_constraints(Poly(1, t), Poly(0, t), G, DE) == \ + ((Poly(0, t, domain='QQ[x]'), Poly(0, t, domain='QQ[x]')), Matrix([[2*x, -x]], t)) + + +def test_constant_system(): + A = Matrix([[-(x + 3)/(x - 1), (x + 1)/(x - 1), 1], + [-x - 3, x + 1, x - 1], + [2*(x + 3)/(x - 1), 0, 0]], t) + u = Matrix([[(x + 1)/(x - 1)], [x + 1], [0]], t) + DE = DifferentialExtension(extension={'D': [Poly(1, x)]}) + R = QQ.frac_field(x)[t] + assert constant_system(A, u, DE) == \ + (Matrix([[1, 0, 0], + [0, 1, 0], + [0, 0, 0], + [0, 0, 1]], ring=R), Matrix([0, 1, 0, 0], ring=R)) + + +def test_prde_spde(): + D = [Poly(x, t), Poly(-x*t, t)] + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t)]}) + # TODO: when bound_degree() can handle this, test degree bound from that too + assert prde_spde(Poly(t, t), Poly(-1/x, t), D, n, DE) == \ + (Poly(t, t), Poly(0, t, domain='ZZ(x)'), + [Poly(2*x, t, domain='ZZ(x)'), Poly(-x, t, domain='ZZ(x)')], + [Poly(-x**2, t, domain='ZZ(x)'), Poly(0, t, domain='ZZ(x)')], n - 1) + + +def test_prde_no_cancel(): + # b large + DE = DifferentialExtension(extension={'D': [Poly(1, x)]}) + assert prde_no_cancel_b_large(Poly(1, x), [Poly(x**2, x), Poly(1, x)], 2, DE) == \ + ([Poly(x**2 - 2*x + 2, x), Poly(1, x)], Matrix([[1, 0, -1, 0], + [0, 1, 0, -1]], x)) + assert prde_no_cancel_b_large(Poly(1, x), [Poly(x**3, x), Poly(1, x)], 3, DE) == \ + ([Poly(x**3 - 3*x**2 + 6*x - 6, x), Poly(1, x)], Matrix([[1, 0, -1, 0], + [0, 1, 0, -1]], x)) + assert prde_no_cancel_b_large(Poly(x, x), [Poly(x**2, x), Poly(1, x)], 1, DE) == \ + ([Poly(x, x, domain='ZZ'), Poly(0, x, domain='ZZ')], Matrix([[1, -1, 0, 0], + [1, 0, -1, 0], + [0, 1, 0, -1]], x)) + # b small + # XXX: Is there a better example of a monomial with D.degree() > 2? + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t**3 + 1, t)]}) + + # My original q was t**4 + t + 1, but this solution implies q == t**4 + # (c1 = 4), with some of the ci for the original q equal to 0. + G = [Poly(t**6, t), Poly(x*t**5, t), Poly(t**3, t), Poly(x*t**2, t), Poly(1 + x, t)] + R = QQ.frac_field(x)[t] + assert prde_no_cancel_b_small(Poly(x*t, t), G, 4, DE) == \ + ([Poly(t**4/4 - x/12*t**3 + x**2/24*t**2 + (Rational(-11, 12) - x**3/24)*t + x/24, t), + Poly(x/3*t**3 - x**2/6*t**2 + (Rational(-1, 3) + x**3/6)*t - x/6, t), Poly(t, t), + Poly(0, t), Poly(0, t)], Matrix([[1, 0, -1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, Rational(-1, 4), 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, -1, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, -1, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, -1, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, -1, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, -1]], ring=R)) + + # TODO: Add test for deg(b) <= 0 with b small + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1 + t**2, t)]}) + b = Poly(-1/x**2, t, field=True) # deg(b) == 0 + q = [Poly(x**i*t**j, t, field=True) for i in range(2) for j in range(3)] + h, A = prde_no_cancel_b_small(b, q, 3, DE) + V = A.nullspace() + R = QQ.frac_field(x)[t] + assert len(V) == 1 + assert V[0] == Matrix([Rational(-1, 2), 0, 0, 1, 0, 0]*3, ring=R) + assert (Matrix([h])*V[0][6:, :])[0] == Poly(x**2/2, t, domain='QQ(x)') + assert (Matrix([q])*V[0][:6, :])[0] == Poly(x - S.Half, t, domain='QQ(x)') + + +def test_prde_cancel_liouvillian(): + ### 1. case == 'primitive' + # used when integrating f = log(x) - log(x - 1) + # Not taken from 'the' book + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t)]}) + p0 = Poly(0, t, field=True) + p1 = Poly((x - 1)*t, t, domain='ZZ(x)') + p2 = Poly(x - 1, t, domain='ZZ(x)') + p3 = Poly(-x**2 + x, t, domain='ZZ(x)') + h, A = prde_cancel_liouvillian(Poly(-1/(x - 1), t), [Poly(-x + 1, t), Poly(1, t)], 1, DE) + V = A.nullspace() + assert h == [p0, p0, p1, p0, p0, p0, p0, p0, p0, p0, p2, p3, p0, p0, p0, p0] + assert A.rank() == 16 + assert (Matrix([h])*V[0][:16, :]) == Matrix([[Poly(0, t, domain='QQ(x)')]]) + + ### 2. case == 'exp' + # used when integrating log(x/exp(x) + 1) + # Not taken from book + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(-t, t)]}) + assert prde_cancel_liouvillian(Poly(0, t, domain='QQ[x]'), [Poly(1, t, domain='QQ(x)')], 0, DE) == \ + ([Poly(1, t, domain='QQ'), Poly(x, t, domain='ZZ(x)')], Matrix([[-1, 0, 1]], DE.t)) + + +def test_param_poly_rischDE(): + DE = DifferentialExtension(extension={'D': [Poly(1, x)]}) + a = Poly(x**2 - x, x, field=True) + b = Poly(1, x, field=True) + q = [Poly(x, x, field=True), Poly(x**2, x, field=True)] + h, A = param_poly_rischDE(a, b, q, 3, DE) + + assert A.nullspace() == [Matrix([0, 1, 1, 1], DE.t)] # c1, c2, d1, d2 + # Solution of a*Dp + b*p = c1*q1 + c2*q2 = q2 = x**2 + # is d1*h1 + d2*h2 = h1 + h2 = x. + assert h[0] + h[1] == Poly(x, x, domain='QQ') + # a*Dp + b*p = q1 = x has no solution. + + a = Poly(x**2 - x, x, field=True) + b = Poly(x**2 - 5*x + 3, x, field=True) + q = [Poly(1, x, field=True), Poly(x, x, field=True), + Poly(x**2, x, field=True)] + h, A = param_poly_rischDE(a, b, q, 3, DE) + + assert A.nullspace() == [Matrix([3, -5, 1, -5, 1, 1], DE.t)] + p = -Poly(5, DE.t)*h[0] + h[1] + h[2] # Poly(1, x) + assert a*derivation(p, DE) + b*p == Poly(x**2 - 5*x + 3, x, domain='QQ') + + +def test_param_rischDE(): + DE = DifferentialExtension(extension={'D': [Poly(1, x)]}) + p1, px = Poly(1, x, field=True), Poly(x, x, field=True) + G = [(p1, px), (p1, p1), (px, p1)] # [1/x, 1, x] + h, A = param_rischDE(-p1, Poly(x**2, x, field=True), G, DE) + assert len(h) == 3 + p = [hi[0].as_expr()/hi[1].as_expr() for hi in h] + V = A.nullspace() + assert len(V) == 2 + assert V[0] == Matrix([-1, 1, 0, -1, 1, 0], DE.t) + y = -p[0] + p[1] + 0*p[2] # x + assert y.diff(x) - y/x**2 == 1 - 1/x # Dy + f*y == -G0 + G1 + 0*G2 + + # the below test computation takes place while computing the integral + # of 'f = log(log(x + exp(x)))' + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)]}) + G = [(Poly(t + x, t, domain='ZZ(x)'), Poly(1, t, domain='QQ')), (Poly(0, t, domain='QQ'), Poly(1, t, domain='QQ'))] + h, A = param_rischDE(Poly(-t - 1, t, field=True), Poly(t + x, t, field=True), G, DE) + assert len(h) == 5 + p = [hi[0].as_expr()/hi[1].as_expr() for hi in h] + V = A.nullspace() + assert len(V) == 3 + assert V[0] == Matrix([0, 0, 0, 0, 1, 0, 0], DE.t) + y = 0*p[0] + 0*p[1] + 1*p[2] + 0*p[3] + 0*p[4] + assert y.diff(t) - y/(t + x) == 0 # Dy + f*y = 0*G0 + 0*G1 + + +def test_limited_integrate_reduce(): + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t)]}) + assert limited_integrate_reduce(Poly(x, t), Poly(t**2, t), [(Poly(x, t), + Poly(t, t))], DE) == \ + (Poly(t, t), Poly(-1/x, t), Poly(t, t), 1, (Poly(x, t), Poly(1, t, domain='ZZ[x]')), + [(Poly(-x*t, t), Poly(1, t, domain='ZZ[x]'))]) + + +def test_limited_integrate(): + DE = DifferentialExtension(extension={'D': [Poly(1, x)]}) + G = [(Poly(x, x), Poly(x + 1, x))] + assert limited_integrate(Poly(-(1 + x + 5*x**2 - 3*x**3), x), + Poly(1 - x - x**2 + x**3, x), G, DE) == \ + ((Poly(x**2 - x + 2, x), Poly(x - 1, x, domain='QQ')), [2]) + G = [(Poly(1, x), Poly(x, x))] + assert limited_integrate(Poly(5*x**2, x), Poly(3, x), G, DE) == \ + ((Poly(5*x**3/9, x), Poly(1, x, domain='QQ')), [0]) + + +def test_is_log_deriv_k_t_radical(): + DE = DifferentialExtension(extension={'D': [Poly(1, x)], 'exts': [None], + 'extargs': [None]}) + assert is_log_deriv_k_t_radical(Poly(2*x, x), Poly(1, x), DE) is None + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(2*t1, t1), Poly(1/x, t2)], + 'exts': [None, 'exp', 'log'], 'extargs': [None, 2*x, x]}) + assert is_log_deriv_k_t_radical(Poly(x + t2/2, t2), Poly(1, t2), DE) == \ + ([(t1, 1), (x, 1)], t1*x, 2, 0) + # TODO: Add more tests + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t0, t0), Poly(1/x, t)], + 'exts': [None, 'exp', 'log'], 'extargs': [None, x, x]}) + assert is_log_deriv_k_t_radical(Poly(x + t/2 + 3, t), Poly(1, t), DE) == \ + ([(t0, 2), (x, 1)], x*t0**2, 2, 3) + + +def test_is_deriv_k(): + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t1), Poly(1/(x + 1), t2)], + 'exts': [None, 'log', 'log'], 'extargs': [None, x, x + 1]}) + assert is_deriv_k(Poly(2*x**2 + 2*x, t2), Poly(1, t2), DE) == \ + ([(t1, 1), (t2, 1)], t1 + t2, 2) + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t1), Poly(t2, t2)], + 'exts': [None, 'log', 'exp'], 'extargs': [None, x, x]}) + assert is_deriv_k(Poly(x**2*t2**3, t2), Poly(1, t2), DE) == \ + ([(x, 3), (t1, 2)], 2*t1 + 3*x, 1) + # TODO: Add more tests, including ones with exponentials + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(2/x, t1)], + 'exts': [None, 'log'], 'extargs': [None, x**2]}) + assert is_deriv_k(Poly(x, t1), Poly(1, t1), DE) == \ + ([(t1, S.Half)], t1/2, 1) + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(2/(1 + x), t0)], + 'exts': [None, 'log'], 'extargs': [None, x**2 + 2*x + 1]}) + assert is_deriv_k(Poly(1 + x, t0), Poly(1, t0), DE) == \ + ([(t0, S.Half)], t0/2, 1) + + # Issue 10798 + # DE = DifferentialExtension(log(1/x), x) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(-1/x, t)], + 'exts': [None, 'log'], 'extargs': [None, 1/x]}) + assert is_deriv_k(Poly(1, t), Poly(x, t), DE) == ([(t, 1)], t, 1) + + +def test_is_log_deriv_k_t_radical_in_field(): + # NOTE: any potential constant factor in the second element of the result + # doesn't matter, because it cancels in Da/a. + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t)]}) + assert is_log_deriv_k_t_radical_in_field(Poly(5*t + 1, t), Poly(2*t*x, t), DE) == \ + (2, t*x**5) + assert is_log_deriv_k_t_radical_in_field(Poly(2 + 3*t, t), Poly(5*x*t, t), DE) == \ + (5, x**3*t**2) + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(-t/x**2, t)]}) + assert is_log_deriv_k_t_radical_in_field(Poly(-(1 + 2*t), t), + Poly(2*x**2 + 2*x**2*t, t), DE) == \ + (2, t + t**2) + assert is_log_deriv_k_t_radical_in_field(Poly(-1, t), Poly(x**2, t), DE) == \ + (1, t) + assert is_log_deriv_k_t_radical_in_field(Poly(1, t), Poly(2*x**2, t), DE) == \ + (2, 1/t) + + +def test_parametric_log_deriv(): + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t)]}) + assert parametric_log_deriv_heu(Poly(5*t**2 + t - 6, t), Poly(2*x*t**2, t), + Poly(-1, t), Poly(x*t**2, t), DE) == \ + (2, 6, t*x**5) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_quadrature.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_quadrature.py new file mode 100644 index 0000000000000000000000000000000000000000..97471dbdbc13fda0bce7a8823ff2cefac4ab8802 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_quadrature.py @@ -0,0 +1,601 @@ +from sympy.core import S, Rational +from sympy.integrals.quadrature import (gauss_legendre, gauss_laguerre, + gauss_hermite, gauss_gen_laguerre, + gauss_chebyshev_t, gauss_chebyshev_u, + gauss_jacobi, gauss_lobatto) + + +def test_legendre(): + x, w = gauss_legendre(1, 17) + assert [str(r) for r in x] == ['0'] + assert [str(r) for r in w] == ['2.0000000000000000'] + + x, w = gauss_legendre(2, 17) + assert [str(r) for r in x] == [ + '-0.57735026918962576', + '0.57735026918962576'] + assert [str(r) for r in w] == [ + '1.0000000000000000', + '1.0000000000000000'] + + x, w = gauss_legendre(3, 17) + assert [str(r) for r in x] == [ + '-0.77459666924148338', + '0', + '0.77459666924148338'] + assert [str(r) for r in w] == [ + '0.55555555555555556', + '0.88888888888888889', + '0.55555555555555556'] + + x, w = gauss_legendre(4, 17) + assert [str(r) for r in x] == [ + '-0.86113631159405258', + '-0.33998104358485626', + '0.33998104358485626', + '0.86113631159405258'] + assert [str(r) for r in w] == [ + '0.34785484513745386', + '0.65214515486254614', + '0.65214515486254614', + '0.34785484513745386'] + + +def test_legendre_precise(): + x, w = gauss_legendre(3, 40) + assert [str(r) for r in x] == [ + '-0.7745966692414833770358530799564799221666', + '0', + '0.7745966692414833770358530799564799221666'] + assert [str(r) for r in w] == [ + '0.5555555555555555555555555555555555555556', + '0.8888888888888888888888888888888888888889', + '0.5555555555555555555555555555555555555556'] + + +def test_laguerre(): + x, w = gauss_laguerre(1, 17) + assert [str(r) for r in x] == ['1.0000000000000000'] + assert [str(r) for r in w] == ['1.0000000000000000'] + + x, w = gauss_laguerre(2, 17) + assert [str(r) for r in x] == [ + '0.58578643762690495', + '3.4142135623730950'] + assert [str(r) for r in w] == [ + '0.85355339059327376', + '0.14644660940672624'] + + x, w = gauss_laguerre(3, 17) + assert [str(r) for r in x] == [ + '0.41577455678347908', + '2.2942803602790417', + '6.2899450829374792', + ] + assert [str(r) for r in w] == [ + '0.71109300992917302', + '0.27851773356924085', + '0.010389256501586136', + ] + + x, w = gauss_laguerre(4, 17) + assert [str(r) for r in x] == [ + '0.32254768961939231', + '1.7457611011583466', + '4.5366202969211280', + '9.3950709123011331'] + assert [str(r) for r in w] == [ + '0.60315410434163360', + '0.35741869243779969', + '0.038887908515005384', + '0.00053929470556132745'] + + x, w = gauss_laguerre(5, 17) + assert [str(r) for r in x] == [ + '0.26356031971814091', + '1.4134030591065168', + '3.5964257710407221', + '7.0858100058588376', + '12.640800844275783'] + assert [str(r) for r in w] == [ + '0.52175561058280865', + '0.39866681108317593', + '0.075942449681707595', + '0.0036117586799220485', + '2.3369972385776228e-5'] + + +def test_laguerre_precise(): + x, w = gauss_laguerre(3, 40) + assert [str(r) for r in x] == [ + '0.4157745567834790833115338731282744735466', + '2.294280360279041719822050361359593868960', + '6.289945082937479196866415765512131657493'] + assert [str(r) for r in w] == [ + '0.7110930099291730154495901911425944313094', + '0.2785177335692408488014448884567264810349', + '0.01038925650158613574896492040067908765572'] + + +def test_hermite(): + x, w = gauss_hermite(1, 17) + assert [str(r) for r in x] == ['0'] + assert [str(r) for r in w] == ['1.7724538509055160'] + + x, w = gauss_hermite(2, 17) + assert [str(r) for r in x] == [ + '-0.70710678118654752', + '0.70710678118654752'] + assert [str(r) for r in w] == [ + '0.88622692545275801', + '0.88622692545275801'] + + x, w = gauss_hermite(3, 17) + assert [str(r) for r in x] == [ + '-1.2247448713915890', + '0', + '1.2247448713915890'] + assert [str(r) for r in w] == [ + '0.29540897515091934', + '1.1816359006036774', + '0.29540897515091934'] + + x, w = gauss_hermite(4, 17) + assert [str(r) for r in x] == [ + '-1.6506801238857846', + '-0.52464762327529032', + '0.52464762327529032', + '1.6506801238857846'] + assert [str(r) for r in w] == [ + '0.081312835447245177', + '0.80491409000551284', + '0.80491409000551284', + '0.081312835447245177'] + + x, w = gauss_hermite(5, 17) + assert [str(r) for r in x] == [ + '-2.0201828704560856', + '-0.95857246461381851', + '0', + '0.95857246461381851', + '2.0201828704560856'] + assert [str(r) for r in w] == [ + '0.019953242059045913', + '0.39361932315224116', + '0.94530872048294188', + '0.39361932315224116', + '0.019953242059045913'] + + +def test_hermite_precise(): + x, w = gauss_hermite(3, 40) + assert [str(r) for r in x] == [ + '-1.224744871391589049098642037352945695983', + '0', + '1.224744871391589049098642037352945695983'] + assert [str(r) for r in w] == [ + '0.2954089751509193378830279138901908637996', + '1.181635900603677351532111655560763455198', + '0.2954089751509193378830279138901908637996'] + + +def test_gen_laguerre(): + x, w = gauss_gen_laguerre(1, Rational(-1, 2), 17) + assert [str(r) for r in x] == ['0.50000000000000000'] + assert [str(r) for r in w] == ['1.7724538509055160'] + + x, w = gauss_gen_laguerre(2, Rational(-1, 2), 17) + assert [str(r) for r in x] == [ + '0.27525512860841095', + '2.7247448713915890'] + assert [str(r) for r in w] == [ + '1.6098281800110257', + '0.16262567089449035'] + + x, w = gauss_gen_laguerre(3, Rational(-1, 2), 17) + assert [str(r) for r in x] == [ + '0.19016350919348813', + '1.7844927485432516', + '5.5253437422632603'] + assert [str(r) for r in w] == [ + '1.4492591904487850', + '0.31413464064571329', + '0.0090600198110176913'] + + x, w = gauss_gen_laguerre(4, Rational(-1, 2), 17) + assert [str(r) for r in x] == [ + '0.14530352150331709', + '1.3390972881263614', + '3.9269635013582872', + '8.5886356890120343'] + assert [str(r) for r in w] == [ + '1.3222940251164826', + '0.41560465162978376', + '0.034155966014826951', + '0.00039920814442273524'] + + x, w = gauss_gen_laguerre(5, Rational(-1, 2), 17) + assert [str(r) for r in x] == [ + '0.11758132021177814', + '1.0745620124369040', + '3.0859374437175500', + '6.4147297336620305', + '11.807189489971737'] + assert [str(r) for r in w] == [ + '1.2217252674706516', + '0.48027722216462937', + '0.067748788910962126', + '0.0026872914935624654', + '1.5280865710465241e-5'] + + x, w = gauss_gen_laguerre(1, 2, 17) + assert [str(r) for r in x] == ['3.0000000000000000'] + assert [str(r) for r in w] == ['2.0000000000000000'] + + x, w = gauss_gen_laguerre(2, 2, 17) + assert [str(r) for r in x] == [ + '2.0000000000000000', + '6.0000000000000000'] + assert [str(r) for r in w] == [ + '1.5000000000000000', + '0.50000000000000000'] + + x, w = gauss_gen_laguerre(3, 2, 17) + assert [str(r) for r in x] == [ + '1.5173870806774125', + '4.3115831337195203', + '9.1710297856030672'] + assert [str(r) for r in w] == [ + '1.0374949614904253', + '0.90575000470306537', + '0.056755033806509347'] + + x, w = gauss_gen_laguerre(4, 2, 17) + assert [str(r) for r in x] == [ + '1.2267632635003021', + '3.4125073586969460', + '6.9026926058516134', + '12.458036771951139'] + assert [str(r) for r in w] == [ + '0.72552499769865438', + '1.0634242919791946', + '0.20669613102835355', + '0.0043545792937974889'] + + x, w = gauss_gen_laguerre(5, 2, 17) + assert [str(r) for r in x] == [ + '1.0311091440933816', + '2.8372128239538217', + '5.6202942725987079', + '9.6829098376640271', + '15.828473921690062'] + assert [str(r) for r in w] == [ + '0.52091739683509184', + '1.0667059331592211', + '0.38354972366693113', + '0.028564233532974658', + '0.00026271280578124935'] + + +def test_gen_laguerre_precise(): + x, w = gauss_gen_laguerre(3, Rational(-1, 2), 40) + assert [str(r) for r in x] == [ + '0.1901635091934881328718554276203028970878', + '1.784492748543251591186722461957367638500', + '5.525343742263260275941422110422329464413'] + assert [str(r) for r in w] == [ + '1.449259190448785048183829411195134343108', + '0.3141346406457132878326231270167565378246', + '0.009060019811017691281714945129254301865020'] + + x, w = gauss_gen_laguerre(3, 2, 40) + assert [str(r) for r in x] == [ + '1.517387080677412495020323111016672547482', + '4.311583133719520302881184669723530562299', + '9.171029785603067202098492219259796890218'] + assert [str(r) for r in w] == [ + '1.037494961490425285817554606541269153041', + '0.9057500047030653669269785048806009945254', + '0.05675503380650934725546688857812985243312'] + + +def test_chebyshev_t(): + x, w = gauss_chebyshev_t(1, 17) + assert [str(r) for r in x] == ['0'] + assert [str(r) for r in w] == ['3.1415926535897932'] + + x, w = gauss_chebyshev_t(2, 17) + assert [str(r) for r in x] == [ + '0.70710678118654752', + '-0.70710678118654752'] + assert [str(r) for r in w] == [ + '1.5707963267948966', + '1.5707963267948966'] + + x, w = gauss_chebyshev_t(3, 17) + assert [str(r) for r in x] == [ + '0.86602540378443865', + '0', + '-0.86602540378443865'] + assert [str(r) for r in w] == [ + '1.0471975511965977', + '1.0471975511965977', + '1.0471975511965977'] + + x, w = gauss_chebyshev_t(4, 17) + assert [str(r) for r in x] == [ + '0.92387953251128676', + '0.38268343236508977', + '-0.38268343236508977', + '-0.92387953251128676'] + assert [str(r) for r in w] == [ + '0.78539816339744831', + '0.78539816339744831', + '0.78539816339744831', + '0.78539816339744831'] + + x, w = gauss_chebyshev_t(5, 17) + assert [str(r) for r in x] == [ + '0.95105651629515357', + '0.58778525229247313', + '0', + '-0.58778525229247313', + '-0.95105651629515357'] + assert [str(r) for r in w] == [ + '0.62831853071795865', + '0.62831853071795865', + '0.62831853071795865', + '0.62831853071795865', + '0.62831853071795865'] + + +def test_chebyshev_t_precise(): + x, w = gauss_chebyshev_t(3, 40) + assert [str(r) for r in x] == [ + '0.8660254037844386467637231707529361834714', + '0', + '-0.8660254037844386467637231707529361834714'] + assert [str(r) for r in w] == [ + '1.047197551196597746154214461093167628066', + '1.047197551196597746154214461093167628066', + '1.047197551196597746154214461093167628066'] + + +def test_chebyshev_u(): + x, w = gauss_chebyshev_u(1, 17) + assert [str(r) for r in x] == ['0'] + assert [str(r) for r in w] == ['1.5707963267948966'] + + x, w = gauss_chebyshev_u(2, 17) + assert [str(r) for r in x] == [ + '0.50000000000000000', + '-0.50000000000000000'] + assert [str(r) for r in w] == [ + '0.78539816339744831', + '0.78539816339744831'] + + x, w = gauss_chebyshev_u(3, 17) + assert [str(r) for r in x] == [ + '0.70710678118654752', + '0', + '-0.70710678118654752'] + assert [str(r) for r in w] == [ + '0.39269908169872415', + '0.78539816339744831', + '0.39269908169872415'] + + x, w = gauss_chebyshev_u(4, 17) + assert [str(r) for r in x] == [ + '0.80901699437494742', + '0.30901699437494742', + '-0.30901699437494742', + '-0.80901699437494742'] + assert [str(r) for r in w] == [ + '0.21707871342270599', + '0.56831944997474231', + '0.56831944997474231', + '0.21707871342270599'] + + x, w = gauss_chebyshev_u(5, 17) + assert [str(r) for r in x] == [ + '0.86602540378443865', + '0.50000000000000000', + '0', + '-0.50000000000000000', + '-0.86602540378443865'] + assert [str(r) for r in w] == [ + '0.13089969389957472', + '0.39269908169872415', + '0.52359877559829887', + '0.39269908169872415', + '0.13089969389957472'] + + +def test_chebyshev_u_precise(): + x, w = gauss_chebyshev_u(3, 40) + assert [str(r) for r in x] == [ + '0.7071067811865475244008443621048490392848', + '0', + '-0.7071067811865475244008443621048490392848'] + assert [str(r) for r in w] == [ + '0.3926990816987241548078304229099378605246', + '0.7853981633974483096156608458198757210493', + '0.3926990816987241548078304229099378605246'] + + +def test_jacobi(): + x, w = gauss_jacobi(1, Rational(-1, 2), S.Half, 17) + assert [str(r) for r in x] == ['0.50000000000000000'] + assert [str(r) for r in w] == ['3.1415926535897932'] + + x, w = gauss_jacobi(2, Rational(-1, 2), S.Half, 17) + assert [str(r) for r in x] == [ + '-0.30901699437494742', + '0.80901699437494742'] + assert [str(r) for r in w] == [ + '0.86831485369082398', + '2.2732777998989693'] + + x, w = gauss_jacobi(3, Rational(-1, 2), S.Half, 17) + assert [str(r) for r in x] == [ + '-0.62348980185873353', + '0.22252093395631440', + '0.90096886790241913'] + assert [str(r) for r in w] == [ + '0.33795476356635433', + '1.0973322242791115', + '1.7063056657443274'] + + x, w = gauss_jacobi(4, Rational(-1, 2), S.Half, 17) + assert [str(r) for r in x] == [ + '-0.76604444311897804', + '-0.17364817766693035', + '0.50000000000000000', + '0.93969262078590838'] + assert [str(r) for r in w] == [ + '0.16333179083642836', + '0.57690240318269103', + '1.0471975511965977', + '1.3541609083740761'] + + x, w = gauss_jacobi(5, Rational(-1, 2), S.Half, 17) + assert [str(r) for r in x] == [ + '-0.84125353283118117', + '-0.41541501300188643', + '0.14231483827328514', + '0.65486073394528506', + '0.95949297361449739'] + assert [str(r) for r in w] == [ + '0.090675770007435372', + '0.33391416373675607', + '0.65248870981926643', + '0.94525424081394926', + '1.1192597692123861'] + + x, w = gauss_jacobi(1, 2, 3, 17) + assert [str(r) for r in x] == ['0.14285714285714286'] + assert [str(r) for r in w] == ['1.0666666666666667'] + + x, w = gauss_jacobi(2, 2, 3, 17) + assert [str(r) for r in x] == [ + '-0.24025307335204215', + '0.46247529557426437'] + assert [str(r) for r in w] == [ + '0.48514624517838660', + '0.58152042148828007'] + + x, w = gauss_jacobi(3, 2, 3, 17) + assert [str(r) for r in x] == [ + '-0.46115870378089762', + '0.10438533038323902', + '0.62950064612493132'] + assert [str(r) for r in w] == [ + '0.17937613502213266', + '0.61595640991147154', + '0.27133412173306246'] + + x, w = gauss_jacobi(4, 2, 3, 17) + assert [str(r) for r in x] == [ + '-0.59903470850824782', + '-0.14761105199952565', + '0.32554377081188859', + '0.72879429738819258'] + assert [str(r) for r in w] == [ + '0.067809641836772187', + '0.38956404952032481', + '0.47995970868024150', + '0.12933326662932816'] + + x, w = gauss_jacobi(5, 2, 3, 17) + assert [str(r) for r in x] == [ + '-0.69045775012676106', + '-0.32651993134900065', + '0.082337849552034905', + '0.47517887061283164', + '0.79279429464422850'] + assert [str(r) for r in w] == [ + '0.027410178066337099', + '0.21291786060364828', + '0.43908437944395081', + '0.32220656547221822', + '0.065047683080512268'] + + +def test_jacobi_precise(): + x, w = gauss_jacobi(3, Rational(-1, 2), S.Half, 40) + assert [str(r) for r in x] == [ + '-0.6234898018587335305250048840042398106323', + '0.2225209339563144042889025644967947594664', + '0.9009688679024191262361023195074450511659'] + assert [str(r) for r in w] == [ + '0.3379547635663543330553835737094171534907', + '1.097332224279111467485302294320899710461', + '1.706305665744327437921957515249186020246'] + + x, w = gauss_jacobi(3, 2, 3, 40) + assert [str(r) for r in x] == [ + '-0.4611587037808976179121958105554375981274', + '0.1043853303832390210914918407615869143233', + '0.6295006461249313240934312425211234110769'] + assert [str(r) for r in w] == [ + '0.1793761350221326596137764371503859752628', + '0.6159564099114715430909548532229749439714', + '0.2713341217330624639619353762933057474325'] + + +def test_lobatto(): + x, w = gauss_lobatto(2, 17) + assert [str(r) for r in x] == [ + '-1', + '1'] + assert [str(r) for r in w] == [ + '1.0000000000000000', + '1.0000000000000000'] + + x, w = gauss_lobatto(3, 17) + assert [str(r) for r in x] == [ + '-1', + '0', + '1'] + assert [str(r) for r in w] == [ + '0.33333333333333333', + '1.3333333333333333', + '0.33333333333333333'] + + x, w = gauss_lobatto(4, 17) + assert [str(r) for r in x] == [ + '-1', + '-0.44721359549995794', + '0.44721359549995794', + '1'] + assert [str(r) for r in w] == [ + '0.16666666666666667', + '0.83333333333333333', + '0.83333333333333333', + '0.16666666666666667'] + + x, w = gauss_lobatto(5, 17) + assert [str(r) for r in x] == [ + '-1', + '-0.65465367070797714', + '0', + '0.65465367070797714', + '1'] + assert [str(r) for r in w] == [ + '0.10000000000000000', + '0.54444444444444444', + '0.71111111111111111', + '0.54444444444444444', + '0.10000000000000000'] + + +def test_lobatto_precise(): + x, w = gauss_lobatto(3, 40) + assert [str(r) for r in x] == [ + '-1', + '0', + '1'] + assert [str(r) for r in w] == [ + '0.3333333333333333333333333333333333333333', + '1.333333333333333333333333333333333333333', + '0.3333333333333333333333333333333333333333'] diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_rationaltools.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_rationaltools.py new file mode 100644 index 0000000000000000000000000000000000000000..809bf30c1c35f80e2a0b15c1e639603eab28a250 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_rationaltools.py @@ -0,0 +1,183 @@ +from sympy.core.numbers import (I, Rational) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, symbols) +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import atan +from sympy.integrals.integrals import integrate +from sympy.polys.polytools import Poly +from sympy.simplify.simplify import simplify + +from sympy.integrals.rationaltools import ratint, ratint_logpart, log_to_atan + +from sympy.abc import a, b, x, t + +half = S.Half + + +def test_ratint(): + assert ratint(S.Zero, x) == 0 + assert ratint(S(7), x) == 7*x + + assert ratint(x, x) == x**2/2 + assert ratint(2*x, x) == x**2 + assert ratint(-2*x, x) == -x**2 + + assert ratint(8*x**7 + 2*x + 1, x) == x**8 + x**2 + x + + f = S.One + g = x + 1 + + assert ratint(f / g, x) == log(x + 1) + assert ratint((f, g), x) == log(x + 1) + + f = x**3 - x + g = x - 1 + + assert ratint(f/g, x) == x**3/3 + x**2/2 + + f = x + g = (x - a)*(x + a) + + assert ratint(f/g, x) == log(x**2 - a**2)/2 + + f = S.One + g = x**2 + 1 + + assert ratint(f/g, x, real=None) == atan(x) + assert ratint(f/g, x, real=True) == atan(x) + + assert ratint(f/g, x, real=False) == I*log(x + I)/2 - I*log(x - I)/2 + + f = S(36) + g = x**5 - 2*x**4 - 2*x**3 + 4*x**2 + x - 2 + + assert ratint(f/g, x) == \ + -4*log(x + 1) + 4*log(x - 2) + (12*x + 6)/(x**2 - 1) + + f = x**4 - 3*x**2 + 6 + g = x**6 - 5*x**4 + 5*x**2 + 4 + + assert ratint(f/g, x) == \ + atan(x) + atan(x**3) + atan(x/2 - Rational(3, 2)*x**3 + S.Half*x**5) + + f = x**7 - 24*x**4 - 4*x**2 + 8*x - 8 + g = x**8 + 6*x**6 + 12*x**4 + 8*x**2 + + assert ratint(f/g, x) == \ + (4 + 6*x + 8*x**2 + 3*x**3)/(4*x + 4*x**3 + x**5) + log(x) + + assert ratint((x**3*f)/(x*g), x) == \ + -(12 - 16*x + 6*x**2 - 14*x**3)/(4 + 4*x**2 + x**4) - \ + 5*sqrt(2)*atan(x*sqrt(2)/2) + S.Half*x**2 - 3*log(2 + x**2) + + f = x**5 - x**4 + 4*x**3 + x**2 - x + 5 + g = x**4 - 2*x**3 + 5*x**2 - 4*x + 4 + + assert ratint(f/g, x) == \ + x + S.Half*x**2 + S.Half*log(2 - x + x**2) + (9 - 4*x)/(7*x**2 - 7*x + 14) + \ + 13*sqrt(7)*atan(Rational(-1, 7)*sqrt(7) + 2*x*sqrt(7)/7)/49 + + assert ratint(1/(x**2 + x + 1), x) == \ + 2*sqrt(3)*atan(sqrt(3)/3 + 2*x*sqrt(3)/3)/3 + + assert ratint(1/(x**3 + 1), x) == \ + -log(1 - x + x**2)/6 + log(1 + x)/3 + sqrt(3)*atan(-sqrt(3) + /3 + 2*x*sqrt(3)/3)/3 + + assert ratint(1/(x**2 + x + 1), x, real=False) == \ + -I*3**half*log(half + x - half*I*3**half)/3 + \ + I*3**half*log(half + x + half*I*3**half)/3 + + assert ratint(1/(x**3 + 1), x, real=False) == log(1 + x)/3 + \ + (Rational(-1, 6) + I*3**half/6)*log(-half + x + I*3**half/2) + \ + (Rational(-1, 6) - I*3**half/6)*log(-half + x - I*3**half/2) + + # issue 4991 + assert ratint(1/(x*(a + b*x)**3), x) == \ + (3*a + 2*b*x)/(2*a**4 + 4*a**3*b*x + 2*a**2*b**2*x**2) + ( + log(x) - log(a/b + x))/a**3 + + assert ratint(x/(1 - x**2), x) == -log(x**2 - 1)/2 + assert ratint(-x/(1 - x**2), x) == log(x**2 - 1)/2 + + assert ratint((x/4 - 4/(1 - x)).diff(x), x) == x/4 + 4/(x - 1) + + ans = atan(x) + assert ratint(1/(x**2 + 1), x, symbol=x) == ans + assert ratint(1/(x**2 + 1), x, symbol='x') == ans + assert ratint(1/(x**2 + 1), x, symbol=a) == ans + # this asserts that as_dummy must return a unique symbol + # even if the symbol is already a Dummy + d = Dummy() + assert ratint(1/(d**2 + 1), d, symbol=d) == atan(d) + + +def test_ratint_logpart(): + assert ratint_logpart(x, x**2 - 9, x, t) == \ + [(Poly(x**2 - 9, x), Poly(-2*t + 1, t))] + assert ratint_logpart(x**2, x**3 - 5, x, t) == \ + [(Poly(x**3 - 5, x), Poly(-3*t + 1, t))] + + +def test_issue_5414(): + assert ratint(1/(x**2 + 16), x) == atan(x/4)/4 + + +def test_issue_5249(): + assert ratint( + 1/(x**2 + a**2), x) == (-I*log(-I*a + x)/2 + I*log(I*a + x)/2)/a + + +def test_issue_5817(): + a, b, c = symbols('a,b,c', positive=True) + + assert simplify(ratint(a/(b*c*x**2 + a**2 + b*a), x)) == \ + sqrt(a)*atan(sqrt( + b)*sqrt(c)*x/(sqrt(a)*sqrt(a + b)))/(sqrt(b)*sqrt(c)*sqrt(a + b)) + + +def test_issue_5981(): + u = symbols('u') + assert integrate(1/(u**2 + 1)) == atan(u) + +def test_issue_10488(): + a,b,c,x = symbols('a b c x', positive=True) + assert integrate(x/(a*x+b),x) == x/a - b*log(a*x + b)/a**2 + + +def test_issues_8246_12050_13501_14080(): + a = symbols('a', nonzero=True) + assert integrate(a/(x**2 + a**2), x) == atan(x/a) + assert integrate(1/(x**2 + a**2), x) == atan(x/a)/a + assert integrate(1/(1 + a**2*x**2), x) == atan(a*x)/a + + +def test_issue_6308(): + k, a0 = symbols('k a0', real=True) + assert integrate((x**2 + 1 - k**2)/(x**2 + 1 + a0**2), x) == \ + x - (a0**2 + k**2)*atan(x/sqrt(a0**2 + 1))/sqrt(a0**2 + 1) + + +def test_issue_5907(): + a = symbols('a', nonzero=True) + assert integrate(1/(x**2 + a**2)**2, x) == \ + x/(2*a**4 + 2*a**2*x**2) + atan(x/a)/(2*a**3) + + +def test_log_to_atan(): + f, g = (Poly(x + S.Half, x, domain='QQ'), Poly(sqrt(3)/2, x, domain='EX')) + fg_ans = 2*atan(2*sqrt(3)*x/3 + sqrt(3)/3) + assert log_to_atan(f, g) == fg_ans + assert log_to_atan(g, f) == -fg_ans + + +def test_issue_25896(): + # for both tests, C = 0 in log_to_real + # but this only has a log result + e = (2*x + 1)/(x**2 + x + 1) + 1/x + assert ratint(e, x) == log(x**3 + x**2 + x) + # while this has more + assert ratint((4*x + 7)/(x**2 + 4*x + 6) + 2/x, x) == ( + 2*log(x) + 2*log(x**2 + 4*x + 6) - sqrt(2)*atan( + sqrt(2)*x/2 + sqrt(2))/2) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_rde.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_rde.py new file mode 100644 index 0000000000000000000000000000000000000000..3c7df5ce05846dc270756cd878870bbff78ff976 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_rde.py @@ -0,0 +1,202 @@ +"""Most of these tests come from the examples in Bronstein's book.""" +from sympy.core.numbers import (I, Rational, oo) +from sympy.core.symbol import symbols +from sympy.polys.polytools import Poly +from sympy.integrals.risch import (DifferentialExtension, + NonElementaryIntegralException) +from sympy.integrals.rde import (order_at, order_at_oo, weak_normalizer, + normal_denom, special_denom, bound_degree, spde, solve_poly_rde, + no_cancel_equal, cancel_primitive, cancel_exp, rischDE) + +from sympy.testing.pytest import raises +from sympy.abc import x, t, z, n + +t0, t1, t2, k = symbols('t:3 k') + + +def test_order_at(): + a = Poly(t**4, t) + b = Poly((t**2 + 1)**3*t, t) + c = Poly((t**2 + 1)**6*t, t) + d = Poly((t**2 + 1)**10*t**10, t) + e = Poly((t**2 + 1)**100*t**37, t) + p1 = Poly(t, t) + p2 = Poly(1 + t**2, t) + assert order_at(a, p1, t) == 4 + assert order_at(b, p1, t) == 1 + assert order_at(c, p1, t) == 1 + assert order_at(d, p1, t) == 10 + assert order_at(e, p1, t) == 37 + assert order_at(a, p2, t) == 0 + assert order_at(b, p2, t) == 3 + assert order_at(c, p2, t) == 6 + assert order_at(d, p1, t) == 10 + assert order_at(e, p2, t) == 100 + assert order_at(Poly(0, t), Poly(t, t), t) is oo + assert order_at_oo(Poly(t**2 - 1, t), Poly(t + 1), t) == \ + order_at_oo(Poly(t - 1, t), Poly(1, t), t) == -1 + assert order_at_oo(Poly(0, t), Poly(1, t), t) is oo + +def test_weak_normalizer(): + a = Poly((1 + x)*t**5 + 4*t**4 + (-1 - 3*x)*t**3 - 4*t**2 + (-2 + 2*x)*t, t) + d = Poly(t**4 - 3*t**2 + 2, t) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)]}) + r = weak_normalizer(a, d, DE, z) + assert r == (Poly(t**5 - t**4 - 4*t**3 + 4*t**2 + 4*t - 4, t, domain='ZZ[x]'), + (Poly((1 + x)*t**2 + x*t, t, domain='ZZ[x]'), + Poly(t + 1, t, domain='ZZ[x]'))) + assert weak_normalizer(r[1][0], r[1][1], DE) == (Poly(1, t), r[1]) + r = weak_normalizer(Poly(1 + t**2), Poly(t**2 - 1, t), DE, z) + assert r == (Poly(t**4 - 2*t**2 + 1, t), (Poly(-3*t**2 + 1, t), Poly(t**2 - 1, t))) + assert weak_normalizer(r[1][0], r[1][1], DE, z) == (Poly(1, t), r[1]) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1 + t**2)]}) + r = weak_normalizer(Poly(1 + t**2), Poly(t, t), DE, z) + assert r == (Poly(t, t), (Poly(0, t), Poly(1, t))) + assert weak_normalizer(r[1][0], r[1][1], DE, z) == (Poly(1, t), r[1]) + + +def test_normal_denom(): + DE = DifferentialExtension(extension={'D': [Poly(1, x)]}) + raises(NonElementaryIntegralException, lambda: normal_denom(Poly(1, x), Poly(1, x), + Poly(1, x), Poly(x, x), DE)) + fa, fd = Poly(t**2 + 1, t), Poly(1, t) + ga, gd = Poly(1, t), Poly(t**2, t) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t**2 + 1, t)]}) + assert normal_denom(fa, fd, ga, gd, DE) == \ + (Poly(t, t), (Poly(t**3 - t**2 + t - 1, t), Poly(1, t)), (Poly(1, t), + Poly(1, t)), Poly(t, t)) + + +def test_special_denom(): + # TODO: add more tests here + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)]}) + assert special_denom(Poly(1, t), Poly(t**2, t), Poly(1, t), Poly(t**2 - 1, t), + Poly(t, t), DE) == \ + (Poly(1, t), Poly(t**2 - 1, t), Poly(t**2 - 1, t), Poly(t, t)) +# assert special_denom(Poly(1, t), Poly(2*x, t), Poly((1 + 2*x)*t, t), DE) == 1 + + # issue 3940 + # Note, this isn't a very good test, because the denominator is just 1, + # but at least it tests the exp cancellation case + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(-2*x*t0, t0), + Poly(I*k*t1, t1)]}) + DE.decrement_level() + assert special_denom(Poly(1, t0), Poly(I*k, t0), Poly(1, t0), Poly(t0, t0), + Poly(1, t0), DE) == \ + (Poly(1, t0, domain='ZZ'), Poly(I*k, t0, domain='ZZ_I[k,x]'), + Poly(t0, t0, domain='ZZ'), Poly(1, t0, domain='ZZ')) + + + assert special_denom(Poly(1, t), Poly(t**2, t), Poly(1, t), Poly(t**2 - 1, t), + Poly(t, t), DE, case='tan') == \ + (Poly(1, t, t0, domain='ZZ'), Poly(t**2, t0, t, domain='ZZ[x]'), + Poly(t, t, t0, domain='ZZ'), Poly(1, t0, domain='ZZ')) + + raises(ValueError, lambda: special_denom(Poly(1, t), Poly(t**2, t), Poly(1, t), Poly(t**2 - 1, t), + Poly(t, t), DE, case='unrecognized_case')) + + +def test_bound_degree_fail(): + # Primitive + DE = DifferentialExtension(extension={'D': [Poly(1, x), + Poly(t0/x**2, t0), Poly(1/x, t)]}) + assert bound_degree(Poly(t**2, t), Poly(-(1/x**2*t**2 + 1/x), t), + Poly((2*x - 1)*t**4 + (t0 + x)/x*t**3 - (t0 + 4*x**2)/2*x*t**2 + x*t, + t), DE) == 3 + + +def test_bound_degree(): + # Base + DE = DifferentialExtension(extension={'D': [Poly(1, x)]}) + assert bound_degree(Poly(1, x), Poly(-2*x, x), Poly(1, x), DE) == 0 + + # Primitive (see above test_bound_degree_fail) + # TODO: Add test for when the degree bound becomes larger after limited_integrate + # TODO: Add test for db == da - 1 case + + # Exp + # TODO: Add tests + # TODO: Add test for when the degree becomes larger after parametric_log_deriv() + + # Nonlinear + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t**2 + 1, t)]}) + assert bound_degree(Poly(t, t), Poly((t - 1)*(t**2 + 1), t), Poly(1, t), DE) == 0 + + +def test_spde(): + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t**2 + 1, t)]}) + raises(NonElementaryIntegralException, lambda: spde(Poly(t, t), Poly((t - 1)*(t**2 + 1), t), Poly(1, t), 0, DE)) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)]}) + assert spde(Poly(t**2 + x*t*2 + x**2, t), Poly(t**2/x**2 + (2/x - 1)*t, t), + Poly(t**2/x**2 + (2/x - 1)*t, t), 0, DE) == \ + (Poly(0, t), Poly(0, t), 0, Poly(0, t), Poly(1, t, domain='ZZ(x)')) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t0/x**2, t0), Poly(1/x, t)]}) + assert spde(Poly(t**2, t), Poly(-t**2/x**2 - 1/x, t), + Poly((2*x - 1)*t**4 + (t0 + x)/x*t**3 - (t0 + 4*x**2)/(2*x)*t**2 + x*t, t), 3, DE) == \ + (Poly(0, t), Poly(0, t), 0, Poly(0, t), + Poly(t0*t**2/2 + x**2*t**2 - x**2*t, t, domain='ZZ(x,t0)')) + DE = DifferentialExtension(extension={'D': [Poly(1, x)]}) + assert spde(Poly(x**2 + x + 1, x), Poly(-2*x - 1, x), Poly(x**5/2 + + 3*x**4/4 + x**3 - x**2 + 1, x), 4, DE) == \ + (Poly(0, x, domain='QQ'), Poly(x/2 - Rational(1, 4), x), 2, Poly(x**2 + x + 1, x), Poly(x*Rational(5, 4), x)) + assert spde(Poly(x**2 + x + 1, x), Poly(-2*x - 1, x), Poly(x**5/2 + + 3*x**4/4 + x**3 - x**2 + 1, x), n, DE) == \ + (Poly(0, x, domain='QQ'), Poly(x/2 - Rational(1, 4), x), -2 + n, Poly(x**2 + x + 1, x), Poly(x*Rational(5, 4), x)) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1, t)]}) + raises(NonElementaryIntegralException, lambda: spde(Poly((t - 1)*(t**2 + 1)**2, t), Poly((t - 1)*(t**2 + 1), t), Poly(1, t), 0, DE)) + DE = DifferentialExtension(extension={'D': [Poly(1, x)]}) + assert spde(Poly(x**2 - x, x), Poly(1, x), Poly(9*x**4 - 10*x**3 + 2*x**2, x), 4, DE) == \ + (Poly(0, x, domain='ZZ'), Poly(0, x), 0, Poly(0, x), Poly(3*x**3 - 2*x**2, x, domain='QQ')) + assert spde(Poly(x**2 - x, x), Poly(x**2 - 5*x + 3, x), Poly(x**7 - x**6 - 2*x**4 + 3*x**3 - x**2, x), 5, DE) == \ + (Poly(1, x, domain='QQ'), Poly(x + 1, x, domain='QQ'), 1, Poly(x**4 - x**3, x), Poly(x**3 - x**2, x, domain='QQ')) + +def test_solve_poly_rde_no_cancel(): + # deg(b) large + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1 + t**2, t)]}) + assert solve_poly_rde(Poly(t**2 + 1, t), Poly(t**3 + (x + 1)*t**2 + t + x + 2, t), + oo, DE) == Poly(t + x, t) + # deg(b) small + DE = DifferentialExtension(extension={'D': [Poly(1, x)]}) + assert solve_poly_rde(Poly(0, x), Poly(x/2 - Rational(1, 4), x), oo, DE) == \ + Poly(x**2/4 - x/4, x) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t**2 + 1, t)]}) + assert solve_poly_rde(Poly(2, t), Poly(t**2 + 2*t + 3, t), 1, DE) == \ + Poly(t + 1, t, x) + # deg(b) == deg(D) - 1 + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t**2 + 1, t)]}) + assert no_cancel_equal(Poly(1 - t, t), + Poly(t**3 + t**2 - 2*x*t - 2*x, t), oo, DE) == \ + (Poly(t**2, t), 1, Poly((-2 - 2*x)*t - 2*x, t)) + + +def test_solve_poly_rde_cancel(): + # exp + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)]}) + assert cancel_exp(Poly(2*x, t), Poly(2*x, t), 0, DE) == \ + Poly(1, t) + assert cancel_exp(Poly(2*x, t), Poly((1 + 2*x)*t, t), 1, DE) == \ + Poly(t, t) + # TODO: Add more exp tests, including tests that require is_deriv_in_field() + + # primitive + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t)]}) + + # If the DecrementLevel context manager is working correctly, this shouldn't + # cause any problems with the further tests. + raises(NonElementaryIntegralException, lambda: cancel_primitive(Poly(1, t), Poly(t, t), oo, DE)) + + assert cancel_primitive(Poly(1, t), Poly(t + 1/x, t), 2, DE) == \ + Poly(t, t) + assert cancel_primitive(Poly(4*x, t), Poly(4*x*t**2 + 2*t/x, t), 3, DE) == \ + Poly(t**2, t) + + # TODO: Add more primitive tests, including tests that require is_deriv_in_field() + + +def test_rischDE(): + # TODO: Add more tests for rischDE, including ones from the text + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)]}) + DE.decrement_level() + assert rischDE(Poly(-2*x, x), Poly(1, x), Poly(1 - 2*x - 2*x**2, x), + Poly(1, x), DE) == \ + (Poly(x + 1, x), Poly(1, x)) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_risch.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_risch.py new file mode 100644 index 0000000000000000000000000000000000000000..68be260e1f3d42fb14c790afe6bda1768e777666 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_risch.py @@ -0,0 +1,763 @@ +"""Most of these tests come from the examples in Bronstein's book.""" +from sympy.core.function import (Function, Lambda, diff, expand_log) +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.relational import Ne +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (atan, sin, tan) +from sympy.polys.polytools import (Poly, cancel, factor) +from sympy.integrals.risch import (gcdex_diophantine, frac_in, as_poly_1t, + derivation, splitfactor, splitfactor_sqf, canonical_representation, + hermite_reduce, polynomial_reduce, residue_reduce, residue_reduce_to_basic, + integrate_primitive, integrate_hyperexponential_polynomial, + integrate_hyperexponential, integrate_hypertangent_polynomial, + integrate_nonlinear_no_specials, integer_powers, DifferentialExtension, + risch_integrate, DecrementLevel, NonElementaryIntegral, recognize_log_derivative, + recognize_derivative, laurent_series) +from sympy.testing.pytest import raises + +from sympy.abc import x, t, nu, z, a, y +t0, t1, t2 = symbols('t:3') +i = Symbol('i') + +def test_gcdex_diophantine(): + assert gcdex_diophantine(Poly(x**4 - 2*x**3 - 6*x**2 + 12*x + 15), + Poly(x**3 + x**2 - 4*x - 4), Poly(x**2 - 1)) == \ + (Poly((-x**2 + 4*x - 3)/5), Poly((x**3 - 7*x**2 + 16*x - 10)/5)) + assert gcdex_diophantine(Poly(x**3 + 6*x + 7), Poly(x**2 + 3*x + 2), Poly(x + 1)) == \ + (Poly(1/13, x, domain='QQ'), Poly(-1/13*x + 3/13, x, domain='QQ')) + + +def test_frac_in(): + assert frac_in(Poly((x + 1)/x*t, t), x) == \ + (Poly(t*x + t, x), Poly(x, x)) + assert frac_in((x + 1)/x*t, x) == \ + (Poly(t*x + t, x), Poly(x, x)) + assert frac_in((Poly((x + 1)/x*t, t), Poly(t + 1, t)), x) == \ + (Poly(t*x + t, x), Poly((1 + t)*x, x)) + raises(ValueError, lambda: frac_in((x + 1)/log(x)*t, x)) + assert frac_in(Poly((2 + 2*x + x*(1 + x))/(1 + x)**2, t), x, cancel=True) == \ + (Poly(x + 2, x), Poly(x + 1, x)) + + +def test_as_poly_1t(): + assert as_poly_1t(2/t + t, t, z) in [ + Poly(t + 2*z, t, z), Poly(t + 2*z, z, t)] + assert as_poly_1t(2/t + 3/t**2, t, z) in [ + Poly(2*z + 3*z**2, t, z), Poly(2*z + 3*z**2, z, t)] + assert as_poly_1t(2/((exp(2) + 1)*t), t, z) in [ + Poly(2/(exp(2) + 1)*z, t, z), Poly(2/(exp(2) + 1)*z, z, t)] + assert as_poly_1t(2/((exp(2) + 1)*t) + t, t, z) in [ + Poly(t + 2/(exp(2) + 1)*z, t, z), Poly(t + 2/(exp(2) + 1)*z, z, t)] + assert as_poly_1t(S.Zero, t, z) == Poly(0, t, z) + + +def test_derivation(): + p = Poly(4*x**4*t**5 + (-4*x**3 - 4*x**4)*t**4 + (-3*x**2 + 2*x**3)*t**3 + + (2*x + 7*x**2 + 2*x**3)*t**2 + (1 - 4*x - 4*x**2)*t - 1 + 2*x, t) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(-t**2 - 3/(2*x)*t + 1/(2*x), t)]}) + assert derivation(p, DE) == Poly(-20*x**4*t**6 + (2*x**3 + 16*x**4)*t**5 + + (21*x**2 + 12*x**3)*t**4 + (x*Rational(7, 2) - 25*x**2 - 12*x**3)*t**3 + + (-5 - x*Rational(15, 2) + 7*x**2)*t**2 - (3 - 8*x - 10*x**2 - 4*x**3)/(2*x)*t + + (1 - 4*x**2)/(2*x), t) + assert derivation(Poly(1, t), DE) == Poly(0, t) + assert derivation(Poly(t, t), DE) == DE.d + assert derivation(Poly(t**2 + 1/x*t + (1 - 2*x)/(4*x**2), t), DE) == \ + Poly(-2*t**3 - 4/x*t**2 - (5 - 2*x)/(2*x**2)*t - (1 - 2*x)/(2*x**3), t, domain='ZZ(x)') + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t1), Poly(t, t)]}) + assert derivation(Poly(x*t*t1, t), DE) == Poly(t*t1 + x*t*t1 + t, t) + assert derivation(Poly(x*t*t1, t), DE, coefficientD=True) == \ + Poly((1 + t1)*t, t) + DE = DifferentialExtension(extension={'D': [Poly(1, x)]}) + assert derivation(Poly(x, x), DE) == Poly(1, x) + # Test basic option + assert derivation((x + 1)/(x - 1), DE, basic=True) == -2/(1 - 2*x + x**2) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)]}) + assert derivation((t + 1)/(t - 1), DE, basic=True) == -2*t/(1 - 2*t + t**2) + assert derivation(t + 1, DE, basic=True) == t + + +def test_splitfactor(): + p = Poly(4*x**4*t**5 + (-4*x**3 - 4*x**4)*t**4 + (-3*x**2 + 2*x**3)*t**3 + + (2*x + 7*x**2 + 2*x**3)*t**2 + (1 - 4*x - 4*x**2)*t - 1 + 2*x, t, field=True) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(-t**2 - 3/(2*x)*t + 1/(2*x), t)]}) + assert splitfactor(p, DE) == (Poly(4*x**4*t**3 + (-8*x**3 - 4*x**4)*t**2 + + (4*x**2 + 8*x**3)*t - 4*x**2, t, domain='ZZ(x)'), + Poly(t**2 + 1/x*t + (1 - 2*x)/(4*x**2), t, domain='ZZ(x)')) + assert splitfactor(Poly(x, t), DE) == (Poly(x, t), Poly(1, t)) + r = Poly(-4*x**4*z**2 + 4*x**6*z**2 - z*x**3 - 4*x**5*z**3 + 4*x**3*z**3 + x**4 + z*x**5 - x**6, t) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t)]}) + assert splitfactor(r, DE, coefficientD=True) == \ + (Poly(x*z - x**2 - z*x**3 + x**4, t), Poly(-x**2 + 4*x**2*z**2, t)) + assert splitfactor_sqf(r, DE, coefficientD=True) == \ + (((Poly(x*z - x**2 - z*x**3 + x**4, t), 1),), ((Poly(-x**2 + 4*x**2*z**2, t), 1),)) + assert splitfactor(Poly(0, t), DE) == (Poly(0, t), Poly(1, t)) + assert splitfactor_sqf(Poly(0, t), DE) == (((Poly(0, t), 1),), ()) + + +def test_canonical_representation(): + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1 + t**2, t)]}) + assert canonical_representation(Poly(x - t, t), Poly(t**2, t), DE) == \ + (Poly(0, t, domain='ZZ[x]'), (Poly(0, t, domain='QQ[x]'), + Poly(1, t, domain='ZZ')), (Poly(-t + x, t, domain='QQ[x]'), + Poly(t**2, t))) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t**2 + 1, t)]}) + assert canonical_representation(Poly(t**5 + t**3 + x**2*t + 1, t), + Poly((t**2 + 1)**3, t), DE) == \ + (Poly(0, t, domain='ZZ[x]'), (Poly(t**5 + t**3 + x**2*t + 1, t, domain='QQ[x]'), + Poly(t**6 + 3*t**4 + 3*t**2 + 1, t, domain='QQ')), + (Poly(0, t, domain='QQ[x]'), Poly(1, t, domain='QQ'))) + + +def test_hermite_reduce(): + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t**2 + 1, t)]}) + + assert hermite_reduce(Poly(x - t, t), Poly(t**2, t), DE) == \ + ((Poly(-x, t, domain='QQ[x]'), Poly(t, t, domain='QQ[x]')), + (Poly(0, t, domain='QQ[x]'), Poly(1, t, domain='QQ[x]')), + (Poly(-x, t, domain='QQ[x]'), Poly(1, t, domain='QQ[x]'))) + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(-t**2 - t/x - (1 - nu**2/x**2), t)]}) + + assert hermite_reduce( + Poly(x**2*t**5 + x*t**4 - nu**2*t**3 - x*(x**2 + 1)*t**2 - (x**2 - nu**2)*t - x**5/4, t), + Poly(x**2*t**4 + x**2*(x**2 + 2)*t**2 + x**2 + x**4 + x**6/4, t), DE) == \ + ((Poly(-x**2 - 4, t, domain='ZZ(x,nu)'), Poly(4*t**2 + 2*x**2 + 4, t, domain='ZZ(x,nu)')), + (Poly((-2*nu**2 - x**4)*t - (2*x**3 + 2*x), t, domain='ZZ(x,nu)'), + Poly(2*x**2*t**2 + x**4 + 2*x**2, t, domain='ZZ(x,nu)')), + (Poly(x*t + 1, t, domain='ZZ(x,nu)'), Poly(x, t, domain='ZZ(x,nu)'))) + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t)]}) + + a = Poly((-2 + 3*x)*t**3 + (-1 + x)*t**2 + (-4*x + 2*x**2)*t + x**2, t) + d = Poly(x*t**6 - 4*x**2*t**5 + 6*x**3*t**4 - 4*x**4*t**3 + x**5*t**2, t) + + assert hermite_reduce(a, d, DE) == \ + ((Poly(3*t**2 + t + 3*x, t, domain='ZZ(x)'), + Poly(3*t**4 - 9*x*t**3 + 9*x**2*t**2 - 3*x**3*t, t, domain='ZZ(x)')), + (Poly(0, t, domain='ZZ(x)'), Poly(1, t, domain='ZZ(x)')), + (Poly(0, t, domain='ZZ(x)'), Poly(1, t, domain='ZZ(x)'))) + + assert hermite_reduce( + Poly(-t**2 + 2*t + 2, t, domain='ZZ(x)'), + Poly(-x*t**2 + 2*x*t - x, t, domain='ZZ(x)'), DE) == \ + ((Poly(3, t, domain='ZZ(x)'), Poly(t - 1, t, domain='ZZ(x)')), + (Poly(0, t, domain='ZZ(x)'), Poly(1, t, domain='ZZ(x)')), + (Poly(1, t, domain='ZZ(x)'), Poly(x, t, domain='ZZ(x)'))) + + assert hermite_reduce( + Poly(-x**2*t**6 + (-1 - 2*x**3 + x**4)*t**3 + (-3 - 3*x**4)*t**2 - + 2*x*t - x - 3*x**2, t, domain='ZZ(x)'), + Poly(x**4*t**6 - 2*x**2*t**3 + 1, t, domain='ZZ(x)'), DE) == \ + ((Poly(x**3*t + x**4 + 1, t, domain='ZZ(x)'), Poly(x**3*t**3 - x, t, domain='ZZ(x)')), + (Poly(0, t, domain='ZZ(x)'), Poly(1, t, domain='ZZ(x)')), + (Poly(-1, t, domain='ZZ(x)'), Poly(x**2, t, domain='ZZ(x)'))) + + assert hermite_reduce( + Poly((-2 + 3*x)*t**3 + (-1 + x)*t**2 + (-4*x + 2*x**2)*t + x**2, t), + Poly(x*t**6 - 4*x**2*t**5 + 6*x**3*t**4 - 4*x**4*t**3 + x**5*t**2, t), DE) == \ + ((Poly(3*t**2 + t + 3*x, t, domain='ZZ(x)'), + Poly(3*t**4 - 9*x*t**3 + 9*x**2*t**2 - 3*x**3*t, t, domain='ZZ(x)')), + (Poly(0, t, domain='ZZ(x)'), Poly(1, t, domain='ZZ(x)')), + (Poly(0, t, domain='ZZ(x)'), Poly(1, t, domain='ZZ(x)'))) + + +def test_polynomial_reduce(): + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1 + t**2, t)]}) + assert polynomial_reduce(Poly(1 + x*t + t**2, t), DE) == \ + (Poly(t, t), Poly(x*t, t)) + assert polynomial_reduce(Poly(0, t), DE) == \ + (Poly(0, t), Poly(0, t)) + + +def test_laurent_series(): + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1, t)]}) + a = Poly(36, t) + d = Poly((t - 2)*(t**2 - 1)**2, t) + F = Poly(t**2 - 1, t) + n = 2 + assert laurent_series(a, d, F, n, DE) == \ + (Poly(-3*t**3 + 3*t**2 - 6*t - 8, t), Poly(t**5 + t**4 - 2*t**3 - 2*t**2 + t + 1, t), + [Poly(-3*t**3 - 6*t**2, t, domain='QQ'), Poly(2*t**6 + 6*t**5 - 8*t**3, t, domain='QQ')]) + + +def test_recognize_derivative(): + DE = DifferentialExtension(extension={'D': [Poly(1, t)]}) + a = Poly(36, t) + d = Poly((t - 2)*(t**2 - 1)**2, t) + assert recognize_derivative(a, d, DE) == False + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t)]}) + a = Poly(2, t) + d = Poly(t**2 - 1, t) + assert recognize_derivative(a, d, DE) == False + assert recognize_derivative(Poly(x*t, t), Poly(1, t), DE) == True + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t**2 + 1, t)]}) + assert recognize_derivative(Poly(t, t), Poly(1, t), DE) == True + + +def test_recognize_log_derivative(): + + a = Poly(2*x**2 + 4*x*t - 2*t - x**2*t, t) + d = Poly((2*x + t)*(t + x**2), t) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)]}) + assert recognize_log_derivative(a, d, DE, z) == True + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t)]}) + assert recognize_log_derivative(Poly(t + 1, t), Poly(t + x, t), DE) == True + assert recognize_log_derivative(Poly(2, t), Poly(t**2 - 1, t), DE) == True + DE = DifferentialExtension(extension={'D': [Poly(1, x)]}) + assert recognize_log_derivative(Poly(1, x), Poly(x**2 - 2, x), DE) == False + assert recognize_log_derivative(Poly(1, x), Poly(x**2 + x, x), DE) == True + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t**2 + 1, t)]}) + assert recognize_log_derivative(Poly(1, t), Poly(t**2 - 2, t), DE) == False + assert recognize_log_derivative(Poly(1, t), Poly(t**2 + t, t), DE) == False + + +def test_residue_reduce(): + a = Poly(2*t**2 - t - x**2, t) + d = Poly(t**3 - x**2*t, t) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t)], 'Tfuncs': [log]}) + assert residue_reduce(a, d, DE, z, invert=False) == \ + ([(Poly(z**2 - Rational(1, 4), z, domain='ZZ(x)'), + Poly((1 + 3*x*z - 6*z**2 - 2*x**2 + 4*x**2*z**2)*t - x*z + x**2 + + 2*x**2*z**2 - 2*z*x**3, t, domain='ZZ(z, x)'))], False) + assert residue_reduce(a, d, DE, z, invert=True) == \ + ([(Poly(z**2 - Rational(1, 4), z, domain='ZZ(x)'), Poly(t + 2*x*z, t))], False) + assert residue_reduce(Poly(-2/x, t), Poly(t**2 - 1, t,), DE, z, invert=False) == \ + ([(Poly(z**2 - 1, z, domain='QQ'), Poly(-2*z*t/x - 2/x, t, domain='ZZ(z,x)'))], True) + ans = residue_reduce(Poly(-2/x, t), Poly(t**2 - 1, t), DE, z, invert=True) + assert ans == ([(Poly(z**2 - 1, z, domain='QQ'), Poly(t + z, t))], True) + assert residue_reduce_to_basic(ans[0], DE, z) == -log(-1 + log(x)) + log(1 + log(x)) + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(-t**2 - t/x - (1 - nu**2/x**2), t)]}) + # TODO: Skip or make faster + assert residue_reduce(Poly((-2*nu**2 - x**4)/(2*x**2)*t - (1 + x**2)/x, t), + Poly(t**2 + 1 + x**2/2, t), DE, z) == \ + ([(Poly(z + S.Half, z, domain='QQ'), Poly(t**2 + 1 + x**2/2, t, + domain='ZZ(x,nu)'))], True) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1 + t**2, t)]}) + assert residue_reduce(Poly(-2*x*t + 1 - x**2, t), + Poly(t**2 + 2*x*t + 1 + x**2, t), DE, z) == \ + ([(Poly(z**2 + Rational(1, 4), z), Poly(t + x + 2*z, t))], True) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)]}) + assert residue_reduce(Poly(t, t), Poly(t + sqrt(2), t), DE, z) == \ + ([(Poly(z - 1, z, domain='QQ'), Poly(t + sqrt(2), t))], True) + + +def test_integrate_hyperexponential(): + # TODO: Add tests for integrate_hyperexponential() from the book + a = Poly((1 + 2*t1 + t1**2 + 2*t1**3)*t**2 + (1 + t1**2)*t + 1 + t1**2, t) + d = Poly(1, t) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1 + t1**2, t1), + Poly(t*(1 + t1**2), t)], 'Tfuncs': [tan, Lambda(i, exp(tan(i)))]}) + assert integrate_hyperexponential(a, d, DE) == \ + (exp(2*tan(x))*tan(x) + exp(tan(x)), 1 + t1**2, True) + a = Poly((t1**3 + (x + 1)*t1**2 + t1 + x + 2)*t, t) + assert integrate_hyperexponential(a, d, DE) == \ + ((x + tan(x))*exp(tan(x)), 0, True) + + a = Poly(t, t) + d = Poly(1, t) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(2*x*t, t)], + 'Tfuncs': [Lambda(i, exp(x**2))]}) + + assert integrate_hyperexponential(a, d, DE) == \ + (0, NonElementaryIntegral(exp(x**2), x), False) + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)], 'Tfuncs': [exp]}) + assert integrate_hyperexponential(a, d, DE) == (exp(x), 0, True) + + a = Poly(25*t**6 - 10*t**5 + 7*t**4 - 8*t**3 + 13*t**2 + 2*t - 1, t) + d = Poly(25*t**6 + 35*t**4 + 11*t**2 + 1, t) + assert integrate_hyperexponential(a, d, DE) == \ + (-(11 - 10*exp(x))/(5 + 25*exp(2*x)) + log(1 + exp(2*x)), -1, True) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t0, t0), Poly(t0*t, t)], + 'Tfuncs': [exp, Lambda(i, exp(exp(i)))]}) + assert integrate_hyperexponential(Poly(2*t0*t**2, t), Poly(1, t), DE) == (exp(2*exp(x)), 0, True) + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t0, t0), Poly(-t0*t, t)], + 'Tfuncs': [exp, Lambda(i, exp(-exp(i)))]}) + assert integrate_hyperexponential(Poly(-27*exp(9) - 162*t0*exp(9) + + 27*x*t0*exp(9), t), Poly((36*exp(18) + x**2*exp(18) - 12*x*exp(18))*t, t), DE) == \ + (27*exp(exp(x))/(-6*exp(9) + x*exp(9)), 0, True) + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)], 'Tfuncs': [exp]}) + assert integrate_hyperexponential(Poly(x**2/2*t, t), Poly(1, t), DE) == \ + ((2 - 2*x + x**2)*exp(x)/2, 0, True) + assert integrate_hyperexponential(Poly(1 + t, t), Poly(t, t), DE) == \ + (-exp(-x), 1, True) # x - exp(-x) + assert integrate_hyperexponential(Poly(x, t), Poly(t + 1, t), DE) == \ + (0, NonElementaryIntegral(x/(1 + exp(x)), x), False) + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t0), Poly(2*x*t1, t1)], + 'Tfuncs': [log, Lambda(i, exp(i**2))]}) + + elem, nonelem, b = integrate_hyperexponential(Poly((8*x**7 - 12*x**5 + 6*x**3 - x)*t1**4 + + (8*t0*x**7 - 8*t0*x**6 - 4*t0*x**5 + 2*t0*x**3 + 2*t0*x**2 - t0*x + + 24*x**8 - 36*x**6 - 4*x**5 + 22*x**4 + 4*x**3 - 7*x**2 - x + 1)*t1**3 + + (8*t0*x**8 - 4*t0*x**6 - 16*t0*x**5 - 2*t0*x**4 + 12*t0*x**3 + + t0*x**2 - 2*t0*x + 24*x**9 - 36*x**7 - 8*x**6 + 22*x**5 + 12*x**4 - + 7*x**3 - 6*x**2 + x + 1)*t1**2 + (8*t0*x**8 - 8*t0*x**6 - 16*t0*x**5 + + 6*t0*x**4 + 10*t0*x**3 - 2*t0*x**2 - t0*x + 8*x**10 - 12*x**8 - 4*x**7 + + 2*x**6 + 12*x**5 + 3*x**4 - 9*x**3 - x**2 + 2*x)*t1 + 8*t0*x**7 - + 12*t0*x**6 - 4*t0*x**5 + 8*t0*x**4 - t0*x**2 - 4*x**7 + 4*x**6 + + 4*x**5 - 4*x**4 - x**3 + x**2, t1), Poly((8*x**7 - 12*x**5 + 6*x**3 - + x)*t1**4 + (24*x**8 + 8*x**7 - 36*x**6 - 12*x**5 + 18*x**4 + 6*x**3 - + 3*x**2 - x)*t1**3 + (24*x**9 + 24*x**8 - 36*x**7 - 36*x**6 + 18*x**5 + + 18*x**4 - 3*x**3 - 3*x**2)*t1**2 + (8*x**10 + 24*x**9 - 12*x**8 - + 36*x**7 + 6*x**6 + 18*x**5 - x**4 - 3*x**3)*t1 + 8*x**10 - 12*x**8 + + 6*x**6 - x**4, t1), DE) + + assert factor(elem) == -((x - 1)*log(x)/((x + exp(x**2))*(2*x**2 - 1))) + assert (nonelem, b) == (NonElementaryIntegral(exp(x**2)/(exp(x**2) + 1), x), False) + +def test_integrate_hyperexponential_polynomial(): + # Without proper cancellation within integrate_hyperexponential_polynomial(), + # this will take a long time to complete, and will return a complicated + # expression + p = Poly((-28*x**11*t0 - 6*x**8*t0 + 6*x**9*t0 - 15*x**8*t0**2 + + 15*x**7*t0**2 + 84*x**10*t0**2 - 140*x**9*t0**3 - 20*x**6*t0**3 + + 20*x**7*t0**3 - 15*x**6*t0**4 + 15*x**5*t0**4 + 140*x**8*t0**4 - + 84*x**7*t0**5 - 6*x**4*t0**5 + 6*x**5*t0**5 + x**3*t0**6 - x**4*t0**6 + + 28*x**6*t0**6 - 4*x**5*t0**7 + x**9 - x**10 + 4*x**12)/(-8*x**11*t0 + + 28*x**10*t0**2 - 56*x**9*t0**3 + 70*x**8*t0**4 - 56*x**7*t0**5 + + 28*x**6*t0**6 - 8*x**5*t0**7 + x**4*t0**8 + x**12)*t1**2 + + (-28*x**11*t0 - 12*x**8*t0 + 12*x**9*t0 - 30*x**8*t0**2 + + 30*x**7*t0**2 + 84*x**10*t0**2 - 140*x**9*t0**3 - 40*x**6*t0**3 + + 40*x**7*t0**3 - 30*x**6*t0**4 + 30*x**5*t0**4 + 140*x**8*t0**4 - + 84*x**7*t0**5 - 12*x**4*t0**5 + 12*x**5*t0**5 - 2*x**4*t0**6 + + 2*x**3*t0**6 + 28*x**6*t0**6 - 4*x**5*t0**7 + 2*x**9 - 2*x**10 + + 4*x**12)/(-8*x**11*t0 + 28*x**10*t0**2 - 56*x**9*t0**3 + + 70*x**8*t0**4 - 56*x**7*t0**5 + 28*x**6*t0**6 - 8*x**5*t0**7 + + x**4*t0**8 + x**12)*t1 + (-2*x**2*t0 + 2*x**3*t0 + x*t0**2 - + x**2*t0**2 + x**3 - x**4)/(-4*x**5*t0 + 6*x**4*t0**2 - 4*x**3*t0**3 + + x**2*t0**4 + x**6), t1, z, expand=False) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t0), Poly(2*x*t1, t1)]}) + assert integrate_hyperexponential_polynomial(p, DE, z) == ( + Poly((x - t0)*t1**2 + (-2*t0 + 2*x)*t1, t1), Poly(-2*x*t0 + x**2 + + t0**2, t1), True) + + DE = DifferentialExtension(extension={'D':[Poly(1, x), Poly(t0, t0)]}) + assert integrate_hyperexponential_polynomial(Poly(0, t0), DE, z) == ( + Poly(0, t0), Poly(1, t0), True) + + +def test_integrate_hyperexponential_returns_piecewise(): + a, b = symbols('a b') + DE = DifferentialExtension(a**x, x) + assert integrate_hyperexponential(DE.fa, DE.fd, DE) == (Piecewise( + (exp(x*log(a))/log(a), Ne(log(a), 0)), (x, True)), 0, True) + DE = DifferentialExtension(a**(b*x), x) + assert integrate_hyperexponential(DE.fa, DE.fd, DE) == (Piecewise( + (exp(b*x*log(a))/(b*log(a)), Ne(b*log(a), 0)), (x, True)), 0, True) + DE = DifferentialExtension(exp(a*x), x) + assert integrate_hyperexponential(DE.fa, DE.fd, DE) == (Piecewise( + (exp(a*x)/a, Ne(a, 0)), (x, True)), 0, True) + DE = DifferentialExtension(x*exp(a*x), x) + assert integrate_hyperexponential(DE.fa, DE.fd, DE) == (Piecewise( + ((a*x - 1)*exp(a*x)/a**2, Ne(a**2, 0)), (x**2/2, True)), 0, True) + DE = DifferentialExtension(x**2*exp(a*x), x) + assert integrate_hyperexponential(DE.fa, DE.fd, DE) == (Piecewise( + ((x**2*a**2 - 2*a*x + 2)*exp(a*x)/a**3, Ne(a**3, 0)), + (x**3/3, True)), 0, True) + DE = DifferentialExtension(x**y + z, y) + assert integrate_hyperexponential(DE.fa, DE.fd, DE) == (Piecewise( + (exp(log(x)*y)/log(x), Ne(log(x), 0)), (y, True)), z, True) + DE = DifferentialExtension(x**y + z + x**(2*y), y) + assert integrate_hyperexponential(DE.fa, DE.fd, DE) == (Piecewise( + ((exp(2*log(x)*y)*log(x) + + 2*exp(log(x)*y)*log(x))/(2*log(x)**2), Ne(2*log(x)**2, 0)), + (2*y, True), + ), z, True) + # TODO: Add a test where two different parts of the extension use a + # Piecewise, like y**x + z**x. + + +def test_issue_13947(): + a, t, s = symbols('a t s') + assert risch_integrate(2**(-pi)/(2**t + 1), t) == \ + 2**(-pi)*t - 2**(-pi)*log(2**t + 1)/log(2) + assert risch_integrate(a**(t - s)/(a**t + 1), t) == \ + exp(-s*log(a))*log(a**t + 1)/log(a) + + +def test_integrate_primitive(): + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t)], + 'Tfuncs': [log]}) + assert integrate_primitive(Poly(t, t), Poly(1, t), DE) == (x*log(x), -1, True) + assert integrate_primitive(Poly(x, t), Poly(t, t), DE) == (0, NonElementaryIntegral(x/log(x), x), False) + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t1), Poly(1/(x + 1), t2)], + 'Tfuncs': [log, Lambda(i, log(i + 1))]}) + assert integrate_primitive(Poly(t1, t2), Poly(t2, t2), DE) == \ + (0, NonElementaryIntegral(log(x)/log(1 + x), x), False) + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t1), Poly(1/(x*t1), t2)], + 'Tfuncs': [log, Lambda(i, log(log(i)))]}) + assert integrate_primitive(Poly(t2, t2), Poly(t1, t2), DE) == \ + (0, NonElementaryIntegral(log(log(x))/log(x), x), False) + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(1/x, t0)], + 'Tfuncs': [log]}) + assert integrate_primitive(Poly(x**2*t0**3 + (3*x**2 + x)*t0**2 + (3*x**2 + + 2*x)*t0 + x**2 + x, t0), Poly(x**2*t0**4 + 4*x**2*t0**3 + 6*x**2*t0**2 + + 4*x**2*t0 + x**2, t0), DE) == \ + (-1/(log(x) + 1), NonElementaryIntegral(1/(log(x) + 1), x), False) + +def test_integrate_hypertangent_polynomial(): + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t**2 + 1, t)]}) + assert integrate_hypertangent_polynomial(Poly(t**2 + x*t + 1, t), DE) == \ + (Poly(t, t), Poly(x/2, t)) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(a*(t**2 + 1), t)]}) + assert integrate_hypertangent_polynomial(Poly(t**5, t), DE) == \ + (Poly(1/(4*a)*t**4 - 1/(2*a)*t**2, t), Poly(1/(2*a), t)) + + +def test_integrate_nonlinear_no_specials(): + a, d, = Poly(x**2*t**5 + x*t**4 - nu**2*t**3 - x*(x**2 + 1)*t**2 - (x**2 - + nu**2)*t - x**5/4, t), Poly(x**2*t**4 + x**2*(x**2 + 2)*t**2 + x**2 + x**4 + x**6/4, t) + # f(x) == phi_nu(x), the logarithmic derivative of J_v, the Bessel function, + # which has no specials (see Chapter 5, note 4 of Bronstein's book). + f = Function('phi_nu') + DE = DifferentialExtension(extension={'D': [Poly(1, x), + Poly(-t**2 - t/x - (1 - nu**2/x**2), t)], 'Tfuncs': [f]}) + assert integrate_nonlinear_no_specials(a, d, DE) == \ + (-log(1 + f(x)**2 + x**2/2)/2 + (- 4 - x**2)/(4 + 2*x**2 + 4*f(x)**2), True) + assert integrate_nonlinear_no_specials(Poly(t, t), Poly(1, t), DE) == \ + (0, False) + + +def test_integer_powers(): + assert integer_powers([x, x/2, x**2 + 1, x*Rational(2, 3)]) == [ + (x/6, [(x, 6), (x/2, 3), (x*Rational(2, 3), 4)]), + (1 + x**2, [(1 + x**2, 1)])] + + +def test_DifferentialExtension_exp(): + assert DifferentialExtension(exp(x) + exp(x**2), x)._important_attrs == \ + (Poly(t1 + t0, t1), Poly(1, t1), [Poly(1, x,), Poly(t0, t0), + Poly(2*x*t1, t1)], [x, t0, t1], [Lambda(i, exp(i)), + Lambda(i, exp(i**2))], [], [None, 'exp', 'exp'], [None, x, x**2]) + assert DifferentialExtension(exp(x) + exp(2*x), x)._important_attrs == \ + (Poly(t0**2 + t0, t0), Poly(1, t0), [Poly(1, x), Poly(t0, t0)], [x, t0], + [Lambda(i, exp(i))], [], [None, 'exp'], [None, x]) + assert DifferentialExtension(exp(x) + exp(x/2), x)._important_attrs == \ + (Poly(t0**2 + t0, t0), Poly(1, t0), [Poly(1, x), Poly(t0/2, t0)], + [x, t0], [Lambda(i, exp(i/2))], [], [None, 'exp'], [None, x/2]) + assert DifferentialExtension(exp(x) + exp(x**2) + exp(x + x**2), x)._important_attrs == \ + (Poly((1 + t0)*t1 + t0, t1), Poly(1, t1), [Poly(1, x), Poly(t0, t0), + Poly(2*x*t1, t1)], [x, t0, t1], [Lambda(i, exp(i)), + Lambda(i, exp(i**2))], [], [None, 'exp', 'exp'], [None, x, x**2]) + assert DifferentialExtension(exp(x) + exp(x**2) + exp(x + x**2 + 1), x)._important_attrs == \ + (Poly((1 + S.Exp1*t0)*t1 + t0, t1), Poly(1, t1), [Poly(1, x), + Poly(t0, t0), Poly(2*x*t1, t1)], [x, t0, t1], [Lambda(i, exp(i)), + Lambda(i, exp(i**2))], [], [None, 'exp', 'exp'], [None, x, x**2]) + assert DifferentialExtension(exp(x) + exp(x**2) + exp(x/2 + x**2), x)._important_attrs == \ + (Poly((t0 + 1)*t1 + t0**2, t1), Poly(1, t1), [Poly(1, x), + Poly(t0/2, t0), Poly(2*x*t1, t1)], [x, t0, t1], + [Lambda(i, exp(i/2)), Lambda(i, exp(i**2))], + [(exp(x/2), sqrt(exp(x)))], [None, 'exp', 'exp'], [None, x/2, x**2]) + assert DifferentialExtension(exp(x) + exp(x**2) + exp(x/2 + x**2 + 3), x)._important_attrs == \ + (Poly((t0*exp(3) + 1)*t1 + t0**2, t1), Poly(1, t1), [Poly(1, x), + Poly(t0/2, t0), Poly(2*x*t1, t1)], [x, t0, t1], [Lambda(i, exp(i/2)), + Lambda(i, exp(i**2))], [(exp(x/2), sqrt(exp(x)))], [None, 'exp', 'exp'], + [None, x/2, x**2]) + assert DifferentialExtension(sqrt(exp(x)), x)._important_attrs == \ + (Poly(t0, t0), Poly(1, t0), [Poly(1, x), Poly(t0/2, t0)], [x, t0], + [Lambda(i, exp(i/2))], [(exp(x/2), sqrt(exp(x)))], [None, 'exp'], [None, x/2]) + + assert DifferentialExtension(exp(x/2), x)._important_attrs == \ + (Poly(t0, t0), Poly(1, t0), [Poly(1, x), Poly(t0/2, t0)], [x, t0], + [Lambda(i, exp(i/2))], [], [None, 'exp'], [None, x/2]) + + +def test_DifferentialExtension_log(): + assert DifferentialExtension(log(x)*log(x + 1)*log(2*x**2 + 2*x), x)._important_attrs == \ + (Poly(t0*t1**2 + (t0*log(2) + t0**2)*t1, t1), Poly(1, t1), + [Poly(1, x), Poly(1/x, t0), + Poly(1/(x + 1), t1, expand=False)], [x, t0, t1], + [Lambda(i, log(i)), Lambda(i, log(i + 1))], [], [None, 'log', 'log'], + [None, x, x + 1]) + assert DifferentialExtension(x**x*log(x), x)._important_attrs == \ + (Poly(t0*t1, t1), Poly(1, t1), [Poly(1, x), Poly(1/x, t0), + Poly((1 + t0)*t1, t1)], [x, t0, t1], [Lambda(i, log(i)), + Lambda(i, exp(t0*i))], [(exp(x*log(x)), x**x)], [None, 'log', 'exp'], + [None, x, t0*x]) + + +def test_DifferentialExtension_symlog(): + # See comment on test_risch_integrate below + assert DifferentialExtension(log(x**x), x)._important_attrs == \ + (Poly(t0*x, t1), Poly(1, t1), [Poly(1, x), Poly(1/x, t0), Poly((t0 + + 1)*t1, t1)], [x, t0, t1], [Lambda(i, log(i)), Lambda(i, exp(i*t0))], + [(exp(x*log(x)), x**x)], [None, 'log', 'exp'], [None, x, t0*x]) + assert DifferentialExtension(log(x**y), x)._important_attrs == \ + (Poly(y*t0, t0), Poly(1, t0), [Poly(1, x), Poly(1/x, t0)], [x, t0], + [Lambda(i, log(i))], [(y*log(x), log(x**y))], [None, 'log'], + [None, x]) + assert DifferentialExtension(log(sqrt(x)), x)._important_attrs == \ + (Poly(t0, t0), Poly(2, t0), [Poly(1, x), Poly(1/x, t0)], [x, t0], + [Lambda(i, log(i))], [(log(x)/2, log(sqrt(x)))], [None, 'log'], + [None, x]) + + +def test_DifferentialExtension_handle_first(): + assert DifferentialExtension(exp(x)*log(x), x, handle_first='log')._important_attrs == \ + (Poly(t0*t1, t1), Poly(1, t1), [Poly(1, x), Poly(1/x, t0), + Poly(t1, t1)], [x, t0, t1], [Lambda(i, log(i)), Lambda(i, exp(i))], + [], [None, 'log', 'exp'], [None, x, x]) + assert DifferentialExtension(exp(x)*log(x), x, handle_first='exp')._important_attrs == \ + (Poly(t0*t1, t1), Poly(1, t1), [Poly(1, x), Poly(t0, t0), + Poly(1/x, t1)], [x, t0, t1], [Lambda(i, exp(i)), Lambda(i, log(i))], + [], [None, 'exp', 'log'], [None, x, x]) + + # This one must have the log first, regardless of what we set it to + # (because the log is inside of the exponential: x**x == exp(x*log(x))) + assert DifferentialExtension(-x**x*log(x)**2 + x**x - x**x/x, x, + handle_first='exp')._important_attrs == \ + DifferentialExtension(-x**x*log(x)**2 + x**x - x**x/x, x, + handle_first='log')._important_attrs == \ + (Poly((-1 + x - x*t0**2)*t1, t1), Poly(x, t1), + [Poly(1, x), Poly(1/x, t0), Poly((1 + t0)*t1, t1)], [x, t0, t1], + [Lambda(i, log(i)), Lambda(i, exp(t0*i))], [(exp(x*log(x)), x**x)], + [None, 'log', 'exp'], [None, x, t0*x]) + + +def test_DifferentialExtension_all_attrs(): + # Test 'unimportant' attributes + DE = DifferentialExtension(exp(x)*log(x), x, handle_first='exp') + assert DE.f == exp(x)*log(x) + assert DE.newf == t0*t1 + assert DE.x == x + assert DE.cases == ['base', 'exp', 'primitive'] + assert DE.case == 'primitive' + + assert DE.level == -1 + assert DE.t == t1 == DE.T[DE.level] + assert DE.d == Poly(1/x, t1) == DE.D[DE.level] + raises(ValueError, lambda: DE.increment_level()) + DE.decrement_level() + assert DE.level == -2 + assert DE.t == t0 == DE.T[DE.level] + assert DE.d == Poly(t0, t0) == DE.D[DE.level] + assert DE.case == 'exp' + DE.decrement_level() + assert DE.level == -3 + assert DE.t == x == DE.T[DE.level] == DE.x + assert DE.d == Poly(1, x) == DE.D[DE.level] + assert DE.case == 'base' + raises(ValueError, lambda: DE.decrement_level()) + DE.increment_level() + DE.increment_level() + assert DE.level == -1 + assert DE.t == t1 == DE.T[DE.level] + assert DE.d == Poly(1/x, t1) == DE.D[DE.level] + assert DE.case == 'primitive' + + # Test methods + assert DE.indices('log') == [2] + assert DE.indices('exp') == [1] + + +def test_DifferentialExtension_extension_flag(): + raises(ValueError, lambda: DifferentialExtension(extension={'T': [x, t]})) + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)]}) + assert DE._important_attrs == (None, None, [Poly(1, x), Poly(t, t)], [x, t], + None, None, None, None) + assert DE.d == Poly(t, t) + assert DE.t == t + assert DE.level == -1 + assert DE.cases == ['base', 'exp'] + assert DE.x == x + assert DE.case == 'exp' + + DE = DifferentialExtension(extension={'D': [Poly(1, x), Poly(t, t)], + 'exts': [None, 'exp'], 'extargs': [None, x]}) + assert DE._important_attrs == (None, None, [Poly(1, x), Poly(t, t)], [x, t], + None, None, [None, 'exp'], [None, x]) + raises(ValueError, lambda: DifferentialExtension()) + + +def test_DifferentialExtension_misc(): + # Odd ends + assert DifferentialExtension(sin(y)*exp(x), x)._important_attrs == \ + (Poly(sin(y)*t0, t0, domain='ZZ[sin(y)]'), Poly(1, t0, domain='ZZ'), + [Poly(1, x, domain='ZZ'), Poly(t0, t0, domain='ZZ')], [x, t0], + [Lambda(i, exp(i))], [], [None, 'exp'], [None, x]) + raises(NotImplementedError, lambda: DifferentialExtension(sin(x), x)) + assert DifferentialExtension(10**x, x)._important_attrs == \ + (Poly(t0, t0), Poly(1, t0), [Poly(1, x), Poly(log(10)*t0, t0)], [x, t0], + [Lambda(i, exp(i*log(10)))], [(exp(x*log(10)), 10**x)], [None, 'exp'], + [None, x*log(10)]) + assert DifferentialExtension(log(x) + log(x**2), x)._important_attrs in [ + (Poly(3*t0, t0), Poly(2, t0), [Poly(1, x), Poly(2/x, t0)], [x, t0], + [Lambda(i, log(i**2))], [], [None, ], [], [1], [x**2]), + (Poly(3*t0, t0), Poly(1, t0), [Poly(1, x), Poly(1/x, t0)], [x, t0], + [Lambda(i, log(i))], [], [None, 'log'], [None, x])] + assert DifferentialExtension(S.Zero, x)._important_attrs == \ + (Poly(0, x), Poly(1, x), [Poly(1, x)], [x], [], [], [None], [None]) + assert DifferentialExtension(tan(atan(x).rewrite(log)), x)._important_attrs == \ + (Poly(x, x), Poly(1, x), [Poly(1, x)], [x], [], [], [None], [None]) + + +def test_DifferentialExtension_Rothstein(): + # Rothstein's integral + f = (2581284541*exp(x) + 1757211400)/(39916800*exp(3*x) + + 119750400*exp(x)**2 + 119750400*exp(x) + 39916800)*exp(1/(exp(x) + 1) - 10*x) + assert DifferentialExtension(f, x)._important_attrs == \ + (Poly((1757211400 + 2581284541*t0)*t1, t1), Poly(39916800 + + 119750400*t0 + 119750400*t0**2 + 39916800*t0**3, t1), + [Poly(1, x), Poly(t0, t0), Poly(-(10 + 21*t0 + 10*t0**2)/(1 + 2*t0 + + t0**2)*t1, t1, domain='ZZ(t0)')], [x, t0, t1], + [Lambda(i, exp(i)), Lambda(i, exp(1/(t0 + 1) - 10*i))], [], + [None, 'exp', 'exp'], [None, x, 1/(t0 + 1) - 10*x]) + + +class _TestingException(Exception): + """Dummy Exception class for testing.""" + pass + + +def test_DecrementLevel(): + DE = DifferentialExtension(x*log(exp(x) + 1), x) + assert DE.level == -1 + assert DE.t == t1 + assert DE.d == Poly(t0/(t0 + 1), t1) + assert DE.case == 'primitive' + + with DecrementLevel(DE): + assert DE.level == -2 + assert DE.t == t0 + assert DE.d == Poly(t0, t0) + assert DE.case == 'exp' + + with DecrementLevel(DE): + assert DE.level == -3 + assert DE.t == x + assert DE.d == Poly(1, x) + assert DE.case == 'base' + + assert DE.level == -2 + assert DE.t == t0 + assert DE.d == Poly(t0, t0) + assert DE.case == 'exp' + + assert DE.level == -1 + assert DE.t == t1 + assert DE.d == Poly(t0/(t0 + 1), t1) + assert DE.case == 'primitive' + + # Test that __exit__ is called after an exception correctly + try: + with DecrementLevel(DE): + raise _TestingException + except _TestingException: + pass + else: + raise AssertionError("Did not raise.") + + assert DE.level == -1 + assert DE.t == t1 + assert DE.d == Poly(t0/(t0 + 1), t1) + assert DE.case == 'primitive' + + +def test_risch_integrate(): + assert risch_integrate(t0*exp(x), x) == t0*exp(x) + assert risch_integrate(sin(x), x, rewrite_complex=True) == -exp(I*x)/2 - exp(-I*x)/2 + + # From my GSoC writeup + assert risch_integrate((1 + 2*x**2 + x**4 + 2*x**3*exp(2*x**2))/ + (x**4*exp(x**2) + 2*x**2*exp(x**2) + exp(x**2)), x) == \ + NonElementaryIntegral(exp(-x**2), x) + exp(x**2)/(1 + x**2) + + + assert risch_integrate(0, x) == 0 + + # also tests prde_cancel() + e1 = log(x/exp(x) + 1) + ans1 = risch_integrate(e1, x) + assert ans1 == (x*log(x*exp(-x) + 1) + NonElementaryIntegral((x**2 - x)/(x + exp(x)), x)) + assert cancel(diff(ans1, x) - e1) == 0 + + # also tests issue #10798 + e2 = (log(-1/y)/2 - log(1/y)/2)/y - (log(1 - 1/y)/2 - log(1 + 1/y)/2)/y + ans2 = risch_integrate(e2, y) + assert ans2 == log(1/y)*log(1 - 1/y)/2 - log(1/y)*log(1 + 1/y)/2 + \ + NonElementaryIntegral((I*pi*y**2 - 2*y*log(1/y) - I*pi)/(2*y**3 - 2*y), y) + assert expand_log(cancel(diff(ans2, y) - e2), force=True) == 0 + + # These are tested here in addition to in test_DifferentialExtension above + # (symlogs) to test that backsubs works correctly. The integrals should be + # written in terms of the original logarithms in the integrands. + + # XXX: Unfortunately, making backsubs work on this one is a little + # trickier, because x**x is converted to exp(x*log(x)), and so log(x**x) + # is converted to x*log(x). (x**2*log(x)).subs(x*log(x), log(x**x)) is + # smart enough, the issue is that these splits happen at different places + # in the algorithm. Maybe a heuristic is in order + assert risch_integrate(log(x**x), x) == x**2*log(x)/2 - x**2/4 + + assert risch_integrate(log(x**y), x) == x*log(x**y) - x*y + assert risch_integrate(log(sqrt(x)), x) == x*log(sqrt(x)) - x/2 + + +def test_risch_integrate_float(): + assert risch_integrate((-60*exp(x) - 19.2*exp(4*x))*exp(4*x), x) == -2.4*exp(8*x) - 12.0*exp(5*x) + + +def test_NonElementaryIntegral(): + assert isinstance(risch_integrate(exp(x**2), x), NonElementaryIntegral) + assert isinstance(risch_integrate(x**x*log(x), x), NonElementaryIntegral) + # Make sure methods of Integral still give back a NonElementaryIntegral + assert isinstance(NonElementaryIntegral(x**x*t0, x).subs(t0, log(x)), NonElementaryIntegral) + + +def test_xtothex(): + a = risch_integrate(x**x, x) + assert a == NonElementaryIntegral(x**x, x) + assert isinstance(a, NonElementaryIntegral) + + +def test_DifferentialExtension_equality(): + DE1 = DE2 = DifferentialExtension(log(x), x) + assert DE1 == DE2 + + +def test_DifferentialExtension_printing(): + DE = DifferentialExtension(exp(2*x**2) + log(exp(x**2) + 1), x) + assert repr(DE) == ("DifferentialExtension(dict([('f', exp(2*x**2) + log(exp(x**2) + 1)), " + "('x', x), ('T', [x, t0, t1]), ('D', [Poly(1, x, domain='ZZ'), Poly(2*x*t0, t0, domain='ZZ[x]'), " + "Poly(2*t0*x/(t0 + 1), t1, domain='ZZ(x,t0)')]), ('fa', Poly(t1 + t0**2, t1, domain='ZZ[t0]')), " + "('fd', Poly(1, t1, domain='ZZ')), ('Tfuncs', [Lambda(i, exp(i**2)), Lambda(i, log(t0 + 1))]), " + "('backsubs', []), ('exts', [None, 'exp', 'log']), ('extargs', [None, x**2, t0 + 1]), " + "('cases', ['base', 'exp', 'primitive']), ('case', 'primitive'), ('t', t1), " + "('d', Poly(2*t0*x/(t0 + 1), t1, domain='ZZ(x,t0)')), ('newf', t0**2 + t1), ('level', -1), " + "('dummy', False)]))") + + assert str(DE) == ("DifferentialExtension({fa=Poly(t1 + t0**2, t1, domain='ZZ[t0]'), " + "fd=Poly(1, t1, domain='ZZ'), D=[Poly(1, x, domain='ZZ'), Poly(2*x*t0, t0, domain='ZZ[x]'), " + "Poly(2*t0*x/(t0 + 1), t1, domain='ZZ(x,t0)')]})") + + +def test_issue_23948(): + f = ( + ( (-2*x**5 + 28*x**4 - 144*x**3 + 324*x**2 - 270*x)*log(x)**2 + +(-4*x**6 + 56*x**5 - 288*x**4 + 648*x**3 - 540*x**2)*log(x) + +(2*x**5 - 28*x**4 + 144*x**3 - 324*x**2 + 270*x)*exp(x) + +(2*x**5 - 28*x**4 + 144*x**3 - 324*x**2 + 270*x)*log(5) + -2*x**7 + 26*x**6 - 116*x**5 + 180*x**4 + 54*x**3 - 270*x**2 + )*log(-log(x)**2 - 2*x*log(x) + exp(x) + log(5) - x**2 - x)**2 + +( (4*x**5 - 44*x**4 + 168*x**3 - 216*x**2 - 108*x + 324)*log(x) + +(-2*x**5 + 24*x**4 - 108*x**3 + 216*x**2 - 162*x)*exp(x) + +4*x**6 - 42*x**5 + 144*x**4 - 108*x**3 - 324*x**2 + 486*x + )*log(-log(x)**2 - 2*x*log(x) + exp(x) + log(5) - x**2 - x) + )/(x*exp(x)**2*log(x)**2 + 2*x**2*exp(x)**2*log(x) - x*exp(x)**3 + +(-x*log(5) + x**3 + x**2)*exp(x)**2) + + F = ((x**4 - 12*x**3 + 54*x**2 - 108*x + 81)*exp(-2*x) + *log(-x**2 - 2*x*log(x) - x + exp(x) - log(x)**2 + log(5))**2) + + assert risch_integrate(f, x) == F diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_singularityfunctions.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_singularityfunctions.py new file mode 100644 index 0000000000000000000000000000000000000000..587e5f104cbf095f851ec538601ca146377b51ae --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_singularityfunctions.py @@ -0,0 +1,22 @@ +from sympy.integrals.singularityfunctions import singularityintegrate +from sympy.core.function import Function +from sympy.core.symbol import symbols +from sympy.functions.special.singularity_functions import SingularityFunction + +x, a, n, y = symbols('x a n y') +f = Function('f') + + +def test_singularityintegrate(): + assert singularityintegrate(x, x) is None + assert singularityintegrate(x + SingularityFunction(x, 9, 1), x) is None + + assert 4*singularityintegrate(SingularityFunction(x, a, 3), x) == 4*SingularityFunction(x, a, 4)/4 + assert singularityintegrate(5*SingularityFunction(x, 5, -2), x) == 5*SingularityFunction(x, 5, -1) + assert singularityintegrate(6*SingularityFunction(x, 5, -1), x) == 6*SingularityFunction(x, 5, 0) + assert singularityintegrate(x*SingularityFunction(x, 0, -1), x) == 0 + assert singularityintegrate((x - 5)*SingularityFunction(x, 5, -1), x) == 0 + assert singularityintegrate(SingularityFunction(x, 0, -1) * f(x), x) == f(0) * SingularityFunction(x, 0, 0) + assert singularityintegrate(SingularityFunction(x, 1, -1) * f(x), x) == f(1) * SingularityFunction(x, 1, 0) + assert singularityintegrate(y*SingularityFunction(x, 0, -1)**2, x) == \ + y*SingularityFunction(0, 0, -1)*SingularityFunction(x, 0, 0) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_transforms.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_transforms.py new file mode 100644 index 0000000000000000000000000000000000000000..fdf7192594bad532c93ffb31e5b2f05cfd8970f1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_transforms.py @@ -0,0 +1,637 @@ +from sympy.integrals.transforms import ( + mellin_transform, inverse_mellin_transform, + fourier_transform, inverse_fourier_transform, + sine_transform, inverse_sine_transform, + cosine_transform, inverse_cosine_transform, + hankel_transform, inverse_hankel_transform, + FourierTransform, SineTransform, CosineTransform, InverseFourierTransform, + InverseSineTransform, InverseCosineTransform, IntegralTransformError) +from sympy.integrals.laplace import ( + laplace_transform, inverse_laplace_transform) +from sympy.core.function import Function, expand_mul +from sympy.core import EulerGamma +from sympy.core.numbers import I, Rational, oo, pi +from sympy.core.singleton import S +from sympy.core.symbol import Symbol, symbols +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.complexes import re, unpolarify +from sympy.functions.elementary.exponential import exp, exp_polar, log +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import atan, cos, sin, tan +from sympy.functions.special.bessel import besseli, besselj, besselk, bessely +from sympy.functions.special.delta_functions import Heaviside +from sympy.functions.special.error_functions import erf, expint +from sympy.functions.special.gamma_functions import gamma +from sympy.functions.special.hyper import meijerg +from sympy.simplify.gammasimp import gammasimp +from sympy.simplify.hyperexpand import hyperexpand +from sympy.simplify.trigsimp import trigsimp +from sympy.testing.pytest import XFAIL, slow, skip, raises +from sympy.abc import x, s, a, b, c, d + + +nu, beta, rho = symbols('nu beta rho') + + +def test_undefined_function(): + from sympy.integrals.transforms import MellinTransform + f = Function('f') + assert mellin_transform(f(x), x, s) == MellinTransform(f(x), x, s) + assert mellin_transform(f(x) + exp(-x), x, s) == \ + (MellinTransform(f(x), x, s) + gamma(s + 1)/s, (0, oo), True) + + +def test_free_symbols(): + f = Function('f') + assert mellin_transform(f(x), x, s).free_symbols == {s} + assert mellin_transform(f(x)*a, x, s).free_symbols == {s, a} + + +def test_as_integral(): + from sympy.integrals.integrals import Integral + f = Function('f') + assert mellin_transform(f(x), x, s).rewrite('Integral') == \ + Integral(x**(s - 1)*f(x), (x, 0, oo)) + assert fourier_transform(f(x), x, s).rewrite('Integral') == \ + Integral(f(x)*exp(-2*I*pi*s*x), (x, -oo, oo)) + assert laplace_transform(f(x), x, s, noconds=True).rewrite('Integral') == \ + Integral(f(x)*exp(-s*x), (x, 0, oo)) + assert str(2*pi*I*inverse_mellin_transform(f(s), s, x, (a, b)).rewrite('Integral')) \ + == "Integral(f(s)/x**s, (s, _c - oo*I, _c + oo*I))" + assert str(2*pi*I*inverse_laplace_transform(f(s), s, x).rewrite('Integral')) == \ + "Integral(f(s)*exp(s*x), (s, _c - oo*I, _c + oo*I))" + assert inverse_fourier_transform(f(s), s, x).rewrite('Integral') == \ + Integral(f(s)*exp(2*I*pi*s*x), (s, -oo, oo)) + +# NOTE this is stuck in risch because meijerint cannot handle it + + +@slow +@XFAIL +def test_mellin_transform_fail(): + skip("Risch takes forever.") + + MT = mellin_transform + + bpos = symbols('b', positive=True) + # bneg = symbols('b', negative=True) + + expr = (sqrt(x + b**2) + b)**a/sqrt(x + b**2) + # TODO does not work with bneg, argument wrong. Needs changes to matching. + assert MT(expr.subs(b, -bpos), x, s) == \ + ((-1)**(a + 1)*2**(a + 2*s)*bpos**(a + 2*s - 1)*gamma(a + s) + *gamma(1 - a - 2*s)/gamma(1 - s), + (-re(a), -re(a)/2 + S.Half), True) + + expr = (sqrt(x + b**2) + b)**a + assert MT(expr.subs(b, -bpos), x, s) == \ + ( + 2**(a + 2*s)*a*bpos**(a + 2*s)*gamma(-a - 2* + s)*gamma(a + s)/gamma(-s + 1), + (-re(a), -re(a)/2), True) + + # Test exponent 1: + assert MT(expr.subs({b: -bpos, a: 1}), x, s) == \ + (-bpos**(2*s + 1)*gamma(s)*gamma(-s - S.Half)/(2*sqrt(pi)), + (-1, Rational(-1, 2)), True) + + +def test_mellin_transform(): + from sympy.functions.elementary.miscellaneous import (Max, Min) + MT = mellin_transform + + bpos = symbols('b', positive=True) + + # 8.4.2 + assert MT(x**nu*Heaviside(x - 1), x, s) == \ + (-1/(nu + s), (-oo, -re(nu)), True) + assert MT(x**nu*Heaviside(1 - x), x, s) == \ + (1/(nu + s), (-re(nu), oo), True) + + assert MT((1 - x)**(beta - 1)*Heaviside(1 - x), x, s) == \ + (gamma(beta)*gamma(s)/gamma(beta + s), (0, oo), re(beta) > 0) + assert MT((x - 1)**(beta - 1)*Heaviside(x - 1), x, s) == \ + (gamma(beta)*gamma(1 - beta - s)/gamma(1 - s), + (-oo, 1 - re(beta)), re(beta) > 0) + + assert MT((1 + x)**(-rho), x, s) == \ + (gamma(s)*gamma(rho - s)/gamma(rho), (0, re(rho)), True) + + assert MT(abs(1 - x)**(-rho), x, s) == ( + 2*sin(pi*rho/2)*gamma(1 - rho)* + cos(pi*(s - rho/2))*gamma(s)*gamma(rho-s)/pi, + (0, re(rho)), re(rho) < 1) + mt = MT((1 - x)**(beta - 1)*Heaviside(1 - x) + + a*(x - 1)**(beta - 1)*Heaviside(x - 1), x, s) + assert mt[1], mt[2] == ((0, -re(beta) + 1), re(beta) > 0) + + assert MT((x**a - b**a)/(x - b), x, s)[0] == \ + pi*b**(a + s - 1)*sin(pi*a)/(sin(pi*s)*sin(pi*(a + s))) + assert MT((x**a - bpos**a)/(x - bpos), x, s) == \ + (pi*bpos**(a + s - 1)*sin(pi*a)/(sin(pi*s)*sin(pi*(a + s))), + (Max(0, -re(a)), Min(1, 1 - re(a))), True) + + expr = (sqrt(x + b**2) + b)**a + assert MT(expr.subs(b, bpos), x, s) == \ + (-a*(2*bpos)**(a + 2*s)*gamma(s)*gamma(-a - 2*s)/gamma(-a - s + 1), + (0, -re(a)/2), True) + + expr = (sqrt(x + b**2) + b)**a/sqrt(x + b**2) + assert MT(expr.subs(b, bpos), x, s) == \ + (2**(a + 2*s)*bpos**(a + 2*s - 1)*gamma(s) + *gamma(1 - a - 2*s)/gamma(1 - a - s), + (0, -re(a)/2 + S.Half), True) + + # 8.4.2 + assert MT(exp(-x), x, s) == (gamma(s), (0, oo), True) + assert MT(exp(-1/x), x, s) == (gamma(-s), (-oo, 0), True) + + # 8.4.5 + assert MT(log(x)**4*Heaviside(1 - x), x, s) == (24/s**5, (0, oo), True) + assert MT(log(x)**3*Heaviside(x - 1), x, s) == (6/s**4, (-oo, 0), True) + assert MT(log(x + 1), x, s) == (pi/(s*sin(pi*s)), (-1, 0), True) + assert MT(log(1/x + 1), x, s) == (pi/(s*sin(pi*s)), (0, 1), True) + assert MT(log(abs(1 - x)), x, s) == (pi/(s*tan(pi*s)), (-1, 0), True) + assert MT(log(abs(1 - 1/x)), x, s) == (pi/(s*tan(pi*s)), (0, 1), True) + + # 8.4.14 + assert MT(erf(sqrt(x)), x, s) == \ + (-gamma(s + S.Half)/(sqrt(pi)*s), (Rational(-1, 2), 0), True) + + +def test_mellin_transform2(): + MT = mellin_transform + # TODO we cannot currently do these (needs summation of 3F2(-1)) + # this also implies that they cannot be written as a single g-function + # (although this is possible) + mt = MT(log(x)/(x + 1), x, s) + assert mt[1:] == ((0, 1), True) + assert not hyperexpand(mt[0], allow_hyper=True).has(meijerg) + mt = MT(log(x)**2/(x + 1), x, s) + assert mt[1:] == ((0, 1), True) + assert not hyperexpand(mt[0], allow_hyper=True).has(meijerg) + mt = MT(log(x)/(x + 1)**2, x, s) + assert mt[1:] == ((0, 2), True) + assert not hyperexpand(mt[0], allow_hyper=True).has(meijerg) + + +@slow +def test_mellin_transform_bessel(): + from sympy.functions.elementary.miscellaneous import Max + MT = mellin_transform + + # 8.4.19 + assert MT(besselj(a, 2*sqrt(x)), x, s) == \ + (gamma(a/2 + s)/gamma(a/2 - s + 1), (-re(a)/2, Rational(3, 4)), True) + assert MT(sin(sqrt(x))*besselj(a, sqrt(x)), x, s) == \ + (2**a*gamma(-2*s + S.Half)*gamma(a/2 + s + S.Half)/( + gamma(-a/2 - s + 1)*gamma(a - 2*s + 1)), ( + -re(a)/2 - S.Half, Rational(1, 4)), True) + assert MT(cos(sqrt(x))*besselj(a, sqrt(x)), x, s) == \ + (2**a*gamma(a/2 + s)*gamma(-2*s + S.Half)/( + gamma(-a/2 - s + S.Half)*gamma(a - 2*s + 1)), ( + -re(a)/2, Rational(1, 4)), True) + assert MT(besselj(a, sqrt(x))**2, x, s) == \ + (gamma(a + s)*gamma(S.Half - s) + / (sqrt(pi)*gamma(1 - s)*gamma(1 + a - s)), + (-re(a), S.Half), True) + assert MT(besselj(a, sqrt(x))*besselj(-a, sqrt(x)), x, s) == \ + (gamma(s)*gamma(S.Half - s) + / (sqrt(pi)*gamma(1 - a - s)*gamma(1 + a - s)), + (0, S.Half), True) + # NOTE: prudnikov gives the strip below as (1/2 - re(a), 1). As far as + # I can see this is wrong (since besselj(z) ~ 1/sqrt(z) for z large) + assert MT(besselj(a - 1, sqrt(x))*besselj(a, sqrt(x)), x, s) == \ + (gamma(1 - s)*gamma(a + s - S.Half) + / (sqrt(pi)*gamma(Rational(3, 2) - s)*gamma(a - s + S.Half)), + (S.Half - re(a), S.Half), True) + assert MT(besselj(a, sqrt(x))*besselj(b, sqrt(x)), x, s) == \ + (4**s*gamma(1 - 2*s)*gamma((a + b)/2 + s) + / (gamma(1 - s + (b - a)/2)*gamma(1 - s + (a - b)/2) + *gamma( 1 - s + (a + b)/2)), + (-(re(a) + re(b))/2, S.Half), True) + assert MT(besselj(a, sqrt(x))**2 + besselj(-a, sqrt(x))**2, x, s)[1:] == \ + ((Max(re(a), -re(a)), S.Half), True) + + # Section 8.4.20 + assert MT(bessely(a, 2*sqrt(x)), x, s) == \ + (-cos(pi*(a/2 - s))*gamma(s - a/2)*gamma(s + a/2)/pi, + (Max(-re(a)/2, re(a)/2), Rational(3, 4)), True) + assert MT(sin(sqrt(x))*bessely(a, sqrt(x)), x, s) == \ + (-4**s*sin(pi*(a/2 - s))*gamma(S.Half - 2*s) + * gamma((1 - a)/2 + s)*gamma((1 + a)/2 + s) + / (sqrt(pi)*gamma(1 - s - a/2)*gamma(1 - s + a/2)), + (Max(-(re(a) + 1)/2, (re(a) - 1)/2), Rational(1, 4)), True) + assert MT(cos(sqrt(x))*bessely(a, sqrt(x)), x, s) == \ + (-4**s*cos(pi*(a/2 - s))*gamma(s - a/2)*gamma(s + a/2)*gamma(S.Half - 2*s) + / (sqrt(pi)*gamma(S.Half - s - a/2)*gamma(S.Half - s + a/2)), + (Max(-re(a)/2, re(a)/2), Rational(1, 4)), True) + assert MT(besselj(a, sqrt(x))*bessely(a, sqrt(x)), x, s) == \ + (-cos(pi*s)*gamma(s)*gamma(a + s)*gamma(S.Half - s) + / (pi**S('3/2')*gamma(1 + a - s)), + (Max(-re(a), 0), S.Half), True) + assert MT(besselj(a, sqrt(x))*bessely(b, sqrt(x)), x, s) == \ + (-4**s*cos(pi*(a/2 - b/2 + s))*gamma(1 - 2*s) + * gamma(a/2 - b/2 + s)*gamma(a/2 + b/2 + s) + / (pi*gamma(a/2 - b/2 - s + 1)*gamma(a/2 + b/2 - s + 1)), + (Max((-re(a) + re(b))/2, (-re(a) - re(b))/2), S.Half), True) + # NOTE bessely(a, sqrt(x))**2 and bessely(a, sqrt(x))*bessely(b, sqrt(x)) + # are a mess (no matter what way you look at it ...) + assert MT(bessely(a, sqrt(x))**2, x, s)[1:] == \ + ((Max(-re(a), 0, re(a)), S.Half), True) + + # Section 8.4.22 + # TODO we can't do any of these (delicate cancellation) + + # Section 8.4.23 + assert MT(besselk(a, 2*sqrt(x)), x, s) == \ + (gamma( + s - a/2)*gamma(s + a/2)/2, (Max(-re(a)/2, re(a)/2), oo), True) + assert MT(besselj(a, 2*sqrt(2*sqrt(x)))*besselk( + a, 2*sqrt(2*sqrt(x))), x, s) == (4**(-s)*gamma(2*s)* + gamma(a/2 + s)/(2*gamma(a/2 - s + 1)), (Max(0, -re(a)/2), oo), True) + # TODO bessely(a, x)*besselk(a, x) is a mess + assert MT(besseli(a, sqrt(x))*besselk(a, sqrt(x)), x, s) == \ + (gamma(s)*gamma( + a + s)*gamma(-s + S.Half)/(2*sqrt(pi)*gamma(a - s + 1)), + (Max(-re(a), 0), S.Half), True) + assert MT(besseli(b, sqrt(x))*besselk(a, sqrt(x)), x, s) == \ + (2**(2*s - 1)*gamma(-2*s + 1)*gamma(-a/2 + b/2 + s)* \ + gamma(a/2 + b/2 + s)/(gamma(-a/2 + b/2 - s + 1)* \ + gamma(a/2 + b/2 - s + 1)), (Max(-re(a)/2 - re(b)/2, \ + re(a)/2 - re(b)/2), S.Half), True) + + # TODO products of besselk are a mess + + mt = MT(exp(-x/2)*besselk(a, x/2), x, s) + mt0 = gammasimp(trigsimp(gammasimp(mt[0].expand(func=True)))) + assert mt0 == 2*pi**Rational(3, 2)*cos(pi*s)*gamma(S.Half - s)/( + (cos(2*pi*a) - cos(2*pi*s))*gamma(-a - s + 1)*gamma(a - s + 1)) + assert mt[1:] == ((Max(-re(a), re(a)), oo), True) + # TODO exp(x/2)*besselk(a, x/2) [etc] cannot currently be done + # TODO various strange products of special orders + + +@slow +def test_expint(): + from sympy.functions.elementary.miscellaneous import Max + from sympy.functions.special.error_functions import Ci, E1, Si + from sympy.simplify.simplify import simplify + + aneg = Symbol('a', negative=True) + u = Symbol('u', polar=True) + + assert mellin_transform(E1(x), x, s) == (gamma(s)/s, (0, oo), True) + assert inverse_mellin_transform(gamma(s)/s, s, x, + (0, oo)).rewrite(expint).expand() == E1(x) + assert mellin_transform(expint(a, x), x, s) == \ + (gamma(s)/(a + s - 1), (Max(1 - re(a), 0), oo), True) + # XXX IMT has hickups with complicated strips ... + assert simplify(unpolarify( + inverse_mellin_transform(gamma(s)/(aneg + s - 1), s, x, + (1 - aneg, oo)).rewrite(expint).expand(func=True))) == \ + expint(aneg, x) + + assert mellin_transform(Si(x), x, s) == \ + (-2**s*sqrt(pi)*gamma(s/2 + S.Half)/( + 2*s*gamma(-s/2 + 1)), (-1, 0), True) + assert inverse_mellin_transform(-2**s*sqrt(pi)*gamma((s + 1)/2) + /(2*s*gamma(-s/2 + 1)), s, x, (-1, 0)) \ + == Si(x) + + assert mellin_transform(Ci(sqrt(x)), x, s) == \ + (-2**(2*s - 1)*sqrt(pi)*gamma(s)/(s*gamma(-s + S.Half)), (0, 1), True) + assert inverse_mellin_transform( + -4**s*sqrt(pi)*gamma(s)/(2*s*gamma(-s + S.Half)), + s, u, (0, 1)).expand() == Ci(sqrt(u)) + + +@slow +def test_inverse_mellin_transform(): + from sympy.core.function import expand + from sympy.functions.elementary.miscellaneous import (Max, Min) + from sympy.functions.elementary.trigonometric import cot + from sympy.simplify.powsimp import powsimp + from sympy.simplify.simplify import simplify + IMT = inverse_mellin_transform + + assert IMT(gamma(s), s, x, (0, oo)) == exp(-x) + assert IMT(gamma(-s), s, x, (-oo, 0)) == exp(-1/x) + assert simplify(IMT(s/(2*s**2 - 2), s, x, (2, oo))) == \ + (x**2 + 1)*Heaviside(1 - x)/(4*x) + + # test passing "None" + assert IMT(1/(s**2 - 1), s, x, (-1, None)) == \ + -x*Heaviside(-x + 1)/2 - Heaviside(x - 1)/(2*x) + assert IMT(1/(s**2 - 1), s, x, (None, 1)) == \ + -x*Heaviside(-x + 1)/2 - Heaviside(x - 1)/(2*x) + + # test expansion of sums + assert IMT(gamma(s) + gamma(s - 1), s, x, (1, oo)) == (x + 1)*exp(-x)/x + + # test factorisation of polys + r = symbols('r', real=True) + assert IMT(1/(s**2 + 1), s, exp(-x), (None, oo) + ).subs(x, r).rewrite(sin).simplify() \ + == sin(r)*Heaviside(1 - exp(-r)) + + # test multiplicative substitution + _a, _b = symbols('a b', positive=True) + assert IMT(_b**(-s/_a)*factorial(s/_a)/s, s, x, (0, oo)) == exp(-_b*x**_a) + assert IMT(factorial(_a/_b + s/_b)/(_a + s), s, x, (-_a, oo)) == x**_a*exp(-x**_b) + + def simp_pows(expr): + return simplify(powsimp(expand_mul(expr, deep=False), force=True)).replace(exp_polar, exp) + + # Now test the inverses of all direct transforms tested above + + # Section 8.4.2 + nu = symbols('nu', real=True) + assert IMT(-1/(nu + s), s, x, (-oo, None)) == x**nu*Heaviside(x - 1) + assert IMT(1/(nu + s), s, x, (None, oo)) == x**nu*Heaviside(1 - x) + assert simp_pows(IMT(gamma(beta)*gamma(s)/gamma(s + beta), s, x, (0, oo))) \ + == (1 - x)**(beta - 1)*Heaviside(1 - x) + assert simp_pows(IMT(gamma(beta)*gamma(1 - beta - s)/gamma(1 - s), + s, x, (-oo, None))) \ + == (x - 1)**(beta - 1)*Heaviside(x - 1) + assert simp_pows(IMT(gamma(s)*gamma(rho - s)/gamma(rho), s, x, (0, None))) \ + == (1/(x + 1))**rho + expr = IMT(d**c*d**(s - 1)*sin(pi*c) + *gamma(s)*gamma(s + c)*gamma(1 - s)*gamma(1 - s - c)/pi, + s, x, (Max(-re(c), 0), Min(1 - re(c), 1))) + assert powsimp(expand_mul(expr, deep=False)).replace(exp_polar, exp).simplify() \ + == (-d**c + x**c)/(-d + x) + + assert simplify(IMT(1/sqrt(pi)*(-c/2)*gamma(s)*gamma((1 - c)/2 - s) + *gamma(-c/2 - s)/gamma(1 - c - s), + s, x, (0, -re(c)/2))) == \ + (1 + sqrt(x + 1))**c + assert simplify(IMT(2**(a + 2*s)*b**(a + 2*s - 1)*gamma(s)*gamma(1 - a - 2*s) + /gamma(1 - a - s), s, x, (0, (-re(a) + 1)/2))) == \ + b**(a - 1)*(b**2*(sqrt(1 + x/b**2) + 1)**a + x*(sqrt(1 + x/b**2) + 1 + )**(a - 1))/(b**2 + x) + assert simplify(IMT(-2**(c + 2*s)*c*b**(c + 2*s)*gamma(s)*gamma(-c - 2*s) + / gamma(-c - s + 1), s, x, (0, -re(c)/2))) == \ + b**c*(sqrt(1 + x/b**2) + 1)**c + + # Section 8.4.5 + assert IMT(24/s**5, s, x, (0, oo)) == log(x)**4*Heaviside(1 - x) + assert expand(IMT(6/s**4, s, x, (-oo, 0)), force=True) == \ + log(x)**3*Heaviside(x - 1) + assert IMT(pi/(s*sin(pi*s)), s, x, (-1, 0)) == log(x + 1) + assert IMT(pi/(s*sin(pi*s/2)), s, x, (-2, 0)) == log(x**2 + 1) + assert IMT(pi/(s*sin(2*pi*s)), s, x, (Rational(-1, 2), 0)) == log(sqrt(x) + 1) + assert IMT(pi/(s*sin(pi*s)), s, x, (0, 1)) == log(1 + 1/x) + + # TODO + def mysimp(expr): + from sympy.core.function import expand + from sympy.simplify.powsimp import powsimp + from sympy.simplify.simplify import logcombine + return expand( + powsimp(logcombine(expr, force=True), force=True, deep=True), + force=True).replace(exp_polar, exp) + + assert mysimp(mysimp(IMT(pi/(s*tan(pi*s)), s, x, (-1, 0)))) in [ + log(1 - x)*Heaviside(1 - x) + log(x - 1)*Heaviside(x - 1), + log(x)*Heaviside(x - 1) + log(1 - 1/x)*Heaviside(x - 1) + log(-x + + 1)*Heaviside(-x + 1)] + # test passing cot + assert mysimp(IMT(pi*cot(pi*s)/s, s, x, (0, 1))) in [ + log(1/x - 1)*Heaviside(1 - x) + log(1 - 1/x)*Heaviside(x - 1), + -log(x)*Heaviside(-x + 1) + log(1 - 1/x)*Heaviside(x - 1) + log(-x + + 1)*Heaviside(-x + 1), ] + + # 8.4.14 + assert IMT(-gamma(s + S.Half)/(sqrt(pi)*s), s, x, (Rational(-1, 2), 0)) == \ + erf(sqrt(x)) + + # 8.4.19 + assert simplify(IMT(gamma(a/2 + s)/gamma(a/2 - s + 1), s, x, (-re(a)/2, Rational(3, 4)))) \ + == besselj(a, 2*sqrt(x)) + assert simplify(IMT(2**a*gamma(S.Half - 2*s)*gamma(s + (a + 1)/2) + / (gamma(1 - s - a/2)*gamma(1 - 2*s + a)), + s, x, (-(re(a) + 1)/2, Rational(1, 4)))) == \ + sin(sqrt(x))*besselj(a, sqrt(x)) + assert simplify(IMT(2**a*gamma(a/2 + s)*gamma(S.Half - 2*s) + / (gamma(S.Half - s - a/2)*gamma(1 - 2*s + a)), + s, x, (-re(a)/2, Rational(1, 4)))) == \ + cos(sqrt(x))*besselj(a, sqrt(x)) + # TODO this comes out as an amazing mess, but simplifies nicely + assert simplify(IMT(gamma(a + s)*gamma(S.Half - s) + / (sqrt(pi)*gamma(1 - s)*gamma(1 + a - s)), + s, x, (-re(a), S.Half))) == \ + besselj(a, sqrt(x))**2 + assert simplify(IMT(gamma(s)*gamma(S.Half - s) + / (sqrt(pi)*gamma(1 - s - a)*gamma(1 + a - s)), + s, x, (0, S.Half))) == \ + besselj(-a, sqrt(x))*besselj(a, sqrt(x)) + assert simplify(IMT(4**s*gamma(-2*s + 1)*gamma(a/2 + b/2 + s) + / (gamma(-a/2 + b/2 - s + 1)*gamma(a/2 - b/2 - s + 1) + *gamma(a/2 + b/2 - s + 1)), + s, x, (-(re(a) + re(b))/2, S.Half))) == \ + besselj(a, sqrt(x))*besselj(b, sqrt(x)) + + # Section 8.4.20 + # TODO this can be further simplified! + assert simplify(IMT(-2**(2*s)*cos(pi*a/2 - pi*b/2 + pi*s)*gamma(-2*s + 1) * + gamma(a/2 - b/2 + s)*gamma(a/2 + b/2 + s) / + (pi*gamma(a/2 - b/2 - s + 1)*gamma(a/2 + b/2 - s + 1)), + s, x, + (Max(-re(a)/2 - re(b)/2, -re(a)/2 + re(b)/2), S.Half))) == \ + besselj(a, sqrt(x))*-(besselj(-b, sqrt(x)) - + besselj(b, sqrt(x))*cos(pi*b))/sin(pi*b) + # TODO more + + # for coverage + + assert IMT(pi/cos(pi*s), s, x, (0, S.Half)) == sqrt(x)/(x + 1) + + +def test_fourier_transform(): + from sympy.core.function import (expand, expand_complex, expand_trig) + from sympy.polys.polytools import factor + from sympy.simplify.simplify import simplify + FT = fourier_transform + IFT = inverse_fourier_transform + + def simp(x): + return simplify(expand_trig(expand_complex(expand(x)))) + + def sinc(x): + return sin(pi*x)/(pi*x) + k = symbols('k', real=True) + f = Function("f") + + # TODO for this to work with real a, need to expand abs(a*x) to abs(a)*abs(x) + a = symbols('a', positive=True) + b = symbols('b', positive=True) + + posk = symbols('posk', positive=True) + + # Test unevaluated form + assert fourier_transform(f(x), x, k) == FourierTransform(f(x), x, k) + assert inverse_fourier_transform( + f(k), k, x) == InverseFourierTransform(f(k), k, x) + + # basic examples from wikipedia + assert simp(FT(Heaviside(1 - abs(2*a*x)), x, k)) == sinc(k/a)/a + # TODO IFT is a *mess* + assert simp(FT(Heaviside(1 - abs(a*x))*(1 - abs(a*x)), x, k)) == sinc(k/a)**2/a + # TODO IFT + + assert factor(FT(exp(-a*x)*Heaviside(x), x, k), extension=I) == \ + 1/(a + 2*pi*I*k) + # NOTE: the ift comes out in pieces + assert IFT(1/(a + 2*pi*I*x), x, posk, + noconds=False) == (exp(-a*posk), True) + assert IFT(1/(a + 2*pi*I*x), x, -posk, + noconds=False) == (0, True) + assert IFT(1/(a + 2*pi*I*x), x, symbols('k', negative=True), + noconds=False) == (0, True) + # TODO IFT without factoring comes out as meijer g + + assert factor(FT(x*exp(-a*x)*Heaviside(x), x, k), extension=I) == \ + 1/(a + 2*pi*I*k)**2 + assert FT(exp(-a*x)*sin(b*x)*Heaviside(x), x, k) == \ + b/(b**2 + (a + 2*I*pi*k)**2) + + assert FT(exp(-a*x**2), x, k) == sqrt(pi)*exp(-pi**2*k**2/a)/sqrt(a) + assert IFT(sqrt(pi/a)*exp(-(pi*k)**2/a), k, x) == exp(-a*x**2) + assert FT(exp(-a*abs(x)), x, k) == 2*a/(a**2 + 4*pi**2*k**2) + # TODO IFT (comes out as meijer G) + + # TODO besselj(n, x), n an integer > 0 actually can be done... + + # TODO are there other common transforms (no distributions!)? + + +def test_sine_transform(): + t = symbols("t") + w = symbols("w") + a = symbols("a") + f = Function("f") + + # Test unevaluated form + assert sine_transform(f(t), t, w) == SineTransform(f(t), t, w) + assert inverse_sine_transform( + f(w), w, t) == InverseSineTransform(f(w), w, t) + + assert sine_transform(1/sqrt(t), t, w) == 1/sqrt(w) + assert inverse_sine_transform(1/sqrt(w), w, t) == 1/sqrt(t) + + assert sine_transform((1/sqrt(t))**3, t, w) == 2*sqrt(w) + + assert sine_transform(t**(-a), t, w) == 2**( + -a + S.Half)*w**(a - 1)*gamma(-a/2 + 1)/gamma((a + 1)/2) + assert inverse_sine_transform(2**(-a + S( + 1)/2)*w**(a - 1)*gamma(-a/2 + 1)/gamma(a/2 + S.Half), w, t) == t**(-a) + + assert sine_transform( + exp(-a*t), t, w) == sqrt(2)*w/(sqrt(pi)*(a**2 + w**2)) + assert inverse_sine_transform( + sqrt(2)*w/(sqrt(pi)*(a**2 + w**2)), w, t) == exp(-a*t) + + assert sine_transform( + log(t)/t, t, w) == sqrt(2)*sqrt(pi)*-(log(w**2) + 2*EulerGamma)/4 + + assert sine_transform( + t*exp(-a*t**2), t, w) == sqrt(2)*w*exp(-w**2/(4*a))/(4*a**Rational(3, 2)) + assert inverse_sine_transform( + sqrt(2)*w*exp(-w**2/(4*a))/(4*a**Rational(3, 2)), w, t) == t*exp(-a*t**2) + + +def test_cosine_transform(): + from sympy.functions.special.error_functions import (Ci, Si) + + t = symbols("t") + w = symbols("w") + a = symbols("a") + f = Function("f") + + # Test unevaluated form + assert cosine_transform(f(t), t, w) == CosineTransform(f(t), t, w) + assert inverse_cosine_transform( + f(w), w, t) == InverseCosineTransform(f(w), w, t) + + assert cosine_transform(1/sqrt(t), t, w) == 1/sqrt(w) + assert inverse_cosine_transform(1/sqrt(w), w, t) == 1/sqrt(t) + + assert cosine_transform(1/( + a**2 + t**2), t, w) == sqrt(2)*sqrt(pi)*exp(-a*w)/(2*a) + + assert cosine_transform(t**( + -a), t, w) == 2**(-a + S.Half)*w**(a - 1)*gamma((-a + 1)/2)/gamma(a/2) + assert inverse_cosine_transform(2**(-a + S( + 1)/2)*w**(a - 1)*gamma(-a/2 + S.Half)/gamma(a/2), w, t) == t**(-a) + + assert cosine_transform( + exp(-a*t), t, w) == sqrt(2)*a/(sqrt(pi)*(a**2 + w**2)) + assert inverse_cosine_transform( + sqrt(2)*a/(sqrt(pi)*(a**2 + w**2)), w, t) == exp(-a*t) + + assert cosine_transform(exp(-a*sqrt(t))*cos(a*sqrt( + t)), t, w) == a*exp(-a**2/(2*w))/(2*w**Rational(3, 2)) + + assert cosine_transform(1/(a + t), t, w) == sqrt(2)*( + (-2*Si(a*w) + pi)*sin(a*w)/2 - cos(a*w)*Ci(a*w))/sqrt(pi) + assert inverse_cosine_transform(sqrt(2)*meijerg(((S.Half, 0), ()), ( + (S.Half, 0, 0), (S.Half,)), a**2*w**2/4)/(2*pi), w, t) == 1/(a + t) + + assert cosine_transform(1/sqrt(a**2 + t**2), t, w) == sqrt(2)*meijerg( + ((S.Half,), ()), ((0, 0), (S.Half,)), a**2*w**2/4)/(2*sqrt(pi)) + assert inverse_cosine_transform(sqrt(2)*meijerg(((S.Half,), ()), ((0, 0), (S.Half,)), a**2*w**2/4)/(2*sqrt(pi)), w, t) == 1/(t*sqrt(a**2/t**2 + 1)) + + +def test_hankel_transform(): + r = Symbol("r") + k = Symbol("k") + nu = Symbol("nu") + m = Symbol("m") + a = symbols("a") + + assert hankel_transform(1/r, r, k, 0) == 1/k + assert inverse_hankel_transform(1/k, k, r, 0) == 1/r + + assert hankel_transform( + 1/r**m, r, k, 0) == 2**(-m + 1)*k**(m - 2)*gamma(-m/2 + 1)/gamma(m/2) + assert inverse_hankel_transform( + 2**(-m + 1)*k**(m - 2)*gamma(-m/2 + 1)/gamma(m/2), k, r, 0) == r**(-m) + + assert hankel_transform(1/r**m, r, k, nu) == ( + 2*2**(-m)*k**(m - 2)*gamma(-m/2 + nu/2 + 1)/gamma(m/2 + nu/2)) + assert inverse_hankel_transform(2**(-m + 1)*k**( + m - 2)*gamma(-m/2 + nu/2 + 1)/gamma(m/2 + nu/2), k, r, nu) == r**(-m) + + assert hankel_transform(r**nu*exp(-a*r), r, k, nu) == \ + 2**(nu + 1)*a*k**(-nu - 3)*(a**2/k**2 + 1)**(-nu - S( + 3)/2)*gamma(nu + Rational(3, 2))/sqrt(pi) + assert inverse_hankel_transform( + 2**(nu + 1)*a*k**(-nu - 3)*(a**2/k**2 + 1)**(-nu - Rational(3, 2))*gamma( + nu + Rational(3, 2))/sqrt(pi), k, r, nu) == r**nu*exp(-a*r) + + +def test_issue_7181(): + assert mellin_transform(1/(1 - x), x, s) != None + + +def test_issue_8882(): + # This is the original test. + # from sympy import diff, Integral, integrate + # r = Symbol('r') + # psi = 1/r*sin(r)*exp(-(a0*r)) + # h = -1/2*diff(psi, r, r) - 1/r*psi + # f = 4*pi*psi*h*r**2 + # assert integrate(f, (r, -oo, 3), meijerg=True).has(Integral) == True + + # To save time, only the critical part is included. + F = -a**(-s + 1)*(4 + 1/a**2)**(-s/2)*sqrt(1/a**2)*exp(-s*I*pi)* \ + sin(s*atan(sqrt(1/a**2)/2))*gamma(s) + raises(IntegralTransformError, lambda: + inverse_mellin_transform(F, s, x, (-1, oo), + **{'as_meijerg': True, 'needeval': True})) + + +def test_issue_12591(): + x, y = symbols("x y", real=True) + assert fourier_transform(exp(x), x, y) == FourierTransform(exp(x), x, y) diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_trigonometry.py b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_trigonometry.py new file mode 100644 index 0000000000000000000000000000000000000000..857c8503c5aa690d66e9cdab49730b4ea655a52c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/tests/test_trigonometry.py @@ -0,0 +1,98 @@ +from sympy.core import Ne, Rational, Symbol +from sympy.functions import sin, cos, tan, csc, sec, cot, log, Piecewise +from sympy.integrals.trigonometry import trigintegrate + +x = Symbol('x') + + +def test_trigintegrate_odd(): + assert trigintegrate(Rational(1), x) == x + assert trigintegrate(x, x) is None + assert trigintegrate(x**2, x) is None + + assert trigintegrate(sin(x), x) == -cos(x) + assert trigintegrate(cos(x), x) == sin(x) + + assert trigintegrate(sin(3*x), x) == -cos(3*x)/3 + assert trigintegrate(cos(3*x), x) == sin(3*x)/3 + + y = Symbol('y') + assert trigintegrate(sin(y*x), x) == Piecewise( + (-cos(y*x)/y, Ne(y, 0)), (0, True)) + assert trigintegrate(cos(y*x), x) == Piecewise( + (sin(y*x)/y, Ne(y, 0)), (x, True)) + assert trigintegrate(sin(y*x)**2, x) == Piecewise( + ((x*y/2 - sin(x*y)*cos(x*y)/2)/y, Ne(y, 0)), (0, True)) + assert trigintegrate(sin(y*x)*cos(y*x), x) == Piecewise( + (sin(x*y)**2/(2*y), Ne(y, 0)), (0, True)) + assert trigintegrate(cos(y*x)**2, x) == Piecewise( + ((x*y/2 + sin(x*y)*cos(x*y)/2)/y, Ne(y, 0)), (x, True)) + + y = Symbol('y', positive=True) + # TODO: remove conds='none' below. For this to work we would have to rule + # out (e.g. by trying solve) the condition y = 0, incompatible with + # y.is_positive being True. + assert trigintegrate(sin(y*x), x, conds='none') == -cos(y*x)/y + assert trigintegrate(cos(y*x), x, conds='none') == sin(y*x)/y + + assert trigintegrate(sin(x)*cos(x), x) == sin(x)**2/2 + assert trigintegrate(sin(x)*cos(x)**2, x) == -cos(x)**3/3 + assert trigintegrate(sin(x)**2*cos(x), x) == sin(x)**3/3 + + # check if it selects right function to substitute, + # so the result is kept simple + assert trigintegrate(sin(x)**7 * cos(x), x) == sin(x)**8/8 + assert trigintegrate(sin(x) * cos(x)**7, x) == -cos(x)**8/8 + + assert trigintegrate(sin(x)**7 * cos(x)**3, x) == \ + -sin(x)**10/10 + sin(x)**8/8 + assert trigintegrate(sin(x)**3 * cos(x)**7, x) == \ + cos(x)**10/10 - cos(x)**8/8 + + # both n, m are odd and -ve, and not necessarily equal + assert trigintegrate(sin(x)**-1*cos(x)**-1, x) == \ + -log(sin(x)**2 - 1)/2 + log(sin(x)) + + +def test_trigintegrate_even(): + assert trigintegrate(sin(x)**2, x) == x/2 - cos(x)*sin(x)/2 + assert trigintegrate(cos(x)**2, x) == x/2 + cos(x)*sin(x)/2 + + assert trigintegrate(sin(3*x)**2, x) == x/2 - cos(3*x)*sin(3*x)/6 + assert trigintegrate(cos(3*x)**2, x) == x/2 + cos(3*x)*sin(3*x)/6 + assert trigintegrate(sin(x)**2 * cos(x)**2, x) == \ + x/8 - sin(2*x)*cos(2*x)/16 + + assert trigintegrate(sin(x)**4 * cos(x)**2, x) == \ + x/16 - sin(x) *cos(x)/16 - sin(x)**3*cos(x)/24 + \ + sin(x)**5*cos(x)/6 + + assert trigintegrate(sin(x)**2 * cos(x)**4, x) == \ + x/16 + cos(x) *sin(x)/16 + cos(x)**3*sin(x)/24 - \ + cos(x)**5*sin(x)/6 + + assert trigintegrate(sin(x)**(-4), x) == -2*cos(x)/(3*sin(x)) \ + - cos(x)/(3*sin(x)**3) + + assert trigintegrate(cos(x)**(-6), x) == sin(x)/(5*cos(x)**5) \ + + 4*sin(x)/(15*cos(x)**3) + 8*sin(x)/(15*cos(x)) + + +def test_trigintegrate_mixed(): + assert trigintegrate(sin(x)*sec(x), x) == -log(cos(x)) + assert trigintegrate(sin(x)*csc(x), x) == x + assert trigintegrate(sin(x)*cot(x), x) == sin(x) + + assert trigintegrate(cos(x)*sec(x), x) == x + assert trigintegrate(cos(x)*csc(x), x) == log(sin(x)) + assert trigintegrate(cos(x)*tan(x), x) == -cos(x) + assert trigintegrate(cos(x)*cot(x), x) == log(cos(x) - 1)/2 \ + - log(cos(x) + 1)/2 + cos(x) + assert trigintegrate(cot(x)*cos(x)**2, x) == log(sin(x)) - sin(x)**2/2 + + +def test_trigintegrate_symbolic(): + n = Symbol('n', integer=True) + assert trigintegrate(cos(x)**n, x) is None + assert trigintegrate(sin(x)**n, x) is None + assert trigintegrate(cot(x)**n, x) is None diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/transforms.py b/venv/lib/python3.11/site-packages/sympy/integrals/transforms.py new file mode 100644 index 0000000000000000000000000000000000000000..19dbd990e4f0320e76707a1a2a8b1601fcd8a3df --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/transforms.py @@ -0,0 +1,1590 @@ +""" Integral Transforms """ +from functools import reduce, wraps +from itertools import repeat +from sympy.core import S, pi +from sympy.core.add import Add +from sympy.core.function import ( + AppliedUndef, count_ops, expand, expand_mul, Function) +from sympy.core.mul import Mul +from sympy.core.intfunc import igcd, ilcm +from sympy.core.sorting import default_sort_key +from sympy.core.symbol import Dummy +from sympy.core.traversal import postorder_traversal +from sympy.functions.combinatorial.factorials import factorial, rf +from sympy.functions.elementary.complexes import re, arg, Abs +from sympy.functions.elementary.exponential import exp, exp_polar +from sympy.functions.elementary.hyperbolic import cosh, coth, sinh, tanh +from sympy.functions.elementary.integers import ceiling +from sympy.functions.elementary.miscellaneous import Max, Min, sqrt +from sympy.functions.elementary.piecewise import piecewise_fold +from sympy.functions.elementary.trigonometric import cos, cot, sin, tan +from sympy.functions.special.bessel import besselj +from sympy.functions.special.delta_functions import Heaviside +from sympy.functions.special.gamma_functions import gamma +from sympy.functions.special.hyper import meijerg +from sympy.integrals import integrate, Integral +from sympy.integrals.meijerint import _dummy +from sympy.logic.boolalg import to_cnf, conjuncts, disjuncts, Or, And +from sympy.polys.polyroots import roots +from sympy.polys.polytools import factor, Poly +from sympy.polys.rootoftools import CRootOf +from sympy.utilities.iterables import iterable +from sympy.utilities.misc import debug + + +########################################################################## +# Helpers / Utilities +########################################################################## + + +class IntegralTransformError(NotImplementedError): + """ + Exception raised in relation to problems computing transforms. + + Explanation + =========== + + This class is mostly used internally; if integrals cannot be computed + objects representing unevaluated transforms are usually returned. + + The hint ``needeval=True`` can be used to disable returning transform + objects, and instead raise this exception if an integral cannot be + computed. + """ + def __init__(self, transform, function, msg): + super().__init__( + "%s Transform could not be computed: %s." % (transform, msg)) + self.function = function + + +class IntegralTransform(Function): + """ + Base class for integral transforms. + + Explanation + =========== + + This class represents unevaluated transforms. + + To implement a concrete transform, derive from this class and implement + the ``_compute_transform(f, x, s, **hints)`` and ``_as_integral(f, x, s)`` + functions. If the transform cannot be computed, raise :obj:`IntegralTransformError`. + + Also set ``cls._name``. For instance, + + >>> from sympy import LaplaceTransform + >>> LaplaceTransform._name + 'Laplace' + + Implement ``self._collapse_extra`` if your function returns more than just a + number and possibly a convergence condition. + """ + + @property + def function(self): + """ The function to be transformed. """ + return self.args[0] + + @property + def function_variable(self): + """ The dependent variable of the function to be transformed. """ + return self.args[1] + + @property + def transform_variable(self): + """ The independent transform variable. """ + return self.args[2] + + @property + def free_symbols(self): + """ + This method returns the symbols that will exist when the transform + is evaluated. + """ + return self.function.free_symbols.union({self.transform_variable}) \ + - {self.function_variable} + + def _compute_transform(self, f, x, s, **hints): + raise NotImplementedError + + def _as_integral(self, f, x, s): + raise NotImplementedError + + def _collapse_extra(self, extra): + cond = And(*extra) + if cond == False: + raise IntegralTransformError(self.__class__.name, None, '') + return cond + + def _try_directly(self, **hints): + T = None + try_directly = not any(func.has(self.function_variable) + for func in self.function.atoms(AppliedUndef)) + if try_directly: + try: + T = self._compute_transform(self.function, + self.function_variable, self.transform_variable, **hints) + except IntegralTransformError: + debug('[IT _try ] Caught IntegralTransformError, returns None') + T = None + + fn = self.function + if not fn.is_Add: + fn = expand_mul(fn) + return fn, T + + def doit(self, **hints): + """ + Try to evaluate the transform in closed form. + + Explanation + =========== + + This general function handles linearity, but apart from that leaves + pretty much everything to _compute_transform. + + Standard hints are the following: + + - ``simplify``: whether or not to simplify the result + - ``noconds``: if True, do not return convergence conditions + - ``needeval``: if True, raise IntegralTransformError instead of + returning IntegralTransform objects + + The default values of these hints depend on the concrete transform, + usually the default is + ``(simplify, noconds, needeval) = (True, False, False)``. + """ + needeval = hints.pop('needeval', False) + simplify = hints.pop('simplify', True) + hints['simplify'] = simplify + + fn, T = self._try_directly(**hints) + + if T is not None: + return T + + if fn.is_Add: + hints['needeval'] = needeval + res = [self.__class__(*([x] + list(self.args[1:]))).doit(**hints) + for x in fn.args] + extra = [] + ress = [] + for x in res: + if not isinstance(x, tuple): + x = [x] + ress.append(x[0]) + if len(x) == 2: + # only a condition + extra.append(x[1]) + elif len(x) > 2: + # some region parameters and a condition (Mellin, Laplace) + extra += [x[1:]] + if simplify==True: + res = Add(*ress).simplify() + else: + res = Add(*ress) + if not extra: + return res + try: + extra = self._collapse_extra(extra) + if iterable(extra): + return (res,) + tuple(extra) + else: + return (res, extra) + except IntegralTransformError: + pass + + if needeval: + raise IntegralTransformError( + self.__class__._name, self.function, 'needeval') + + # TODO handle derivatives etc + + # pull out constant coefficients + coeff, rest = fn.as_coeff_mul(self.function_variable) + return coeff*self.__class__(*([Mul(*rest)] + list(self.args[1:]))) + + @property + def as_integral(self): + return self._as_integral(self.function, self.function_variable, + self.transform_variable) + + def _eval_rewrite_as_Integral(self, *args, **kwargs): + return self.as_integral + + +def _simplify(expr, doit): + if doit: + from sympy.simplify import simplify + from sympy.simplify.powsimp import powdenest + return simplify(powdenest(piecewise_fold(expr), polar=True)) + return expr + + +def _noconds_(default): + """ + This is a decorator generator for dropping convergence conditions. + + Explanation + =========== + + Suppose you define a function ``transform(*args)`` which returns a tuple of + the form ``(result, cond1, cond2, ...)``. + + Decorating it ``@_noconds_(default)`` will add a new keyword argument + ``noconds`` to it. If ``noconds=True``, the return value will be altered to + be only ``result``, whereas if ``noconds=False`` the return value will not + be altered. + + The default value of the ``noconds`` keyword will be ``default`` (i.e. the + argument of this function). + """ + def make_wrapper(func): + @wraps(func) + def wrapper(*args, noconds=default, **kwargs): + res = func(*args, **kwargs) + if noconds: + return res[0] + return res + return wrapper + return make_wrapper +_noconds = _noconds_(False) + + +########################################################################## +# Mellin Transform +########################################################################## + +def _default_integrator(f, x): + return integrate(f, (x, S.Zero, S.Infinity)) + + +@_noconds +def _mellin_transform(f, x, s_, integrator=_default_integrator, simplify=True): + """ Backend function to compute Mellin transforms. """ + # We use a fresh dummy, because assumptions on s might drop conditions on + # convergence of the integral. + s = _dummy('s', 'mellin-transform', f) + F = integrator(x**(s - 1) * f, x) + + if not F.has(Integral): + return _simplify(F.subs(s, s_), simplify), (S.NegativeInfinity, S.Infinity), S.true + + if not F.is_Piecewise: # XXX can this work if integration gives continuous result now? + raise IntegralTransformError('Mellin', f, 'could not compute integral') + + F, cond = F.args[0] + if F.has(Integral): + raise IntegralTransformError( + 'Mellin', f, 'integral in unexpected form') + + def process_conds(cond): + """ + Turn ``cond`` into a strip (a, b), and auxiliary conditions. + """ + from sympy.solvers.inequalities import _solve_inequality + a = S.NegativeInfinity + b = S.Infinity + aux = S.true + conds = conjuncts(to_cnf(cond)) + t = Dummy('t', real=True) + for c in conds: + a_ = S.Infinity + b_ = S.NegativeInfinity + aux_ = [] + for d in disjuncts(c): + d_ = d.replace( + re, lambda x: x.as_real_imag()[0]).subs(re(s), t) + if not d.is_Relational or \ + d.rel_op in ('==', '!=') \ + or d_.has(s) or not d_.has(t): + aux_ += [d] + continue + soln = _solve_inequality(d_, t) + if not soln.is_Relational or \ + soln.rel_op in ('==', '!='): + aux_ += [d] + continue + if soln.lts == t: + b_ = Max(soln.gts, b_) + else: + a_ = Min(soln.lts, a_) + if a_ is not S.Infinity and a_ != b: + a = Max(a_, a) + elif b_ is not S.NegativeInfinity and b_ != a: + b = Min(b_, b) + else: + aux = And(aux, Or(*aux_)) + return a, b, aux + + conds = [process_conds(c) for c in disjuncts(cond)] + conds = [x for x in conds if x[2] != False] + conds.sort(key=lambda x: (x[0] - x[1], count_ops(x[2]))) + + if not conds: + raise IntegralTransformError('Mellin', f, 'no convergence found') + + a, b, aux = conds[0] + return _simplify(F.subs(s, s_), simplify), (a, b), aux + + +class MellinTransform(IntegralTransform): + """ + Class representing unevaluated Mellin transforms. + + For usage of this class, see the :class:`IntegralTransform` docstring. + + For how to compute Mellin transforms, see the :func:`mellin_transform` + docstring. + """ + + _name = 'Mellin' + + def _compute_transform(self, f, x, s, **hints): + return _mellin_transform(f, x, s, **hints) + + def _as_integral(self, f, x, s): + return Integral(f*x**(s - 1), (x, S.Zero, S.Infinity)) + + def _collapse_extra(self, extra): + a = [] + b = [] + cond = [] + for (sa, sb), c in extra: + a += [sa] + b += [sb] + cond += [c] + res = (Max(*a), Min(*b)), And(*cond) + if (res[0][0] >= res[0][1]) == True or res[1] == False: + raise IntegralTransformError( + 'Mellin', None, 'no combined convergence.') + return res + + +def mellin_transform(f, x, s, **hints): + r""" + Compute the Mellin transform `F(s)` of `f(x)`, + + .. math :: F(s) = \int_0^\infty x^{s-1} f(x) \mathrm{d}x. + + For all "sensible" functions, this converges absolutely in a strip + `a < \operatorname{Re}(s) < b`. + + Explanation + =========== + + The Mellin transform is related via change of variables to the Fourier + transform, and also to the (bilateral) Laplace transform. + + This function returns ``(F, (a, b), cond)`` + where ``F`` is the Mellin transform of ``f``, ``(a, b)`` is the fundamental strip + (as above), and ``cond`` are auxiliary convergence conditions. + + If the integral cannot be computed in closed form, this function returns + an unevaluated :class:`MellinTransform` object. + + For a description of possible hints, refer to the docstring of + :func:`sympy.integrals.transforms.IntegralTransform.doit`. If ``noconds=False``, + then only `F` will be returned (i.e. not ``cond``, and also not the strip + ``(a, b)``). + + Examples + ======== + + >>> from sympy import mellin_transform, exp + >>> from sympy.abc import x, s + >>> mellin_transform(exp(-x), x, s) + (gamma(s), (0, oo), True) + + See Also + ======== + + inverse_mellin_transform, laplace_transform, fourier_transform + hankel_transform, inverse_hankel_transform + """ + return MellinTransform(f, x, s).doit(**hints) + + +def _rewrite_sin(m_n, s, a, b): + """ + Re-write the sine function ``sin(m*s + n)`` as gamma functions, compatible + with the strip (a, b). + + Return ``(gamma1, gamma2, fac)`` so that ``f == fac/(gamma1 * gamma2)``. + + Examples + ======== + + >>> from sympy.integrals.transforms import _rewrite_sin + >>> from sympy import pi, S + >>> from sympy.abc import s + >>> _rewrite_sin((pi, 0), s, 0, 1) + (gamma(s), gamma(1 - s), pi) + >>> _rewrite_sin((pi, 0), s, 1, 0) + (gamma(s - 1), gamma(2 - s), -pi) + >>> _rewrite_sin((pi, 0), s, -1, 0) + (gamma(s + 1), gamma(-s), -pi) + >>> _rewrite_sin((pi, pi/2), s, S(1)/2, S(3)/2) + (gamma(s - 1/2), gamma(3/2 - s), -pi) + >>> _rewrite_sin((pi, pi), s, 0, 1) + (gamma(s), gamma(1 - s), -pi) + >>> _rewrite_sin((2*pi, 0), s, 0, S(1)/2) + (gamma(2*s), gamma(1 - 2*s), pi) + >>> _rewrite_sin((2*pi, 0), s, S(1)/2, 1) + (gamma(2*s - 1), gamma(2 - 2*s), -pi) + """ + # (This is a separate function because it is moderately complicated, + # and I want to doctest it.) + # We want to use pi/sin(pi*x) = gamma(x)*gamma(1-x). + # But there is one complication: the gamma functions determine the + # integration contour in the definition of the G-function. Usually + # it would not matter if this is slightly shifted, unless this way + # we create an undefined function! + # So we try to write this in such a way that the gammas are + # eminently on the right side of the strip. + m, n = m_n + + m = expand_mul(m/pi) + n = expand_mul(n/pi) + r = ceiling(-m*a - n.as_real_imag()[0]) # Don't use re(n), does not expand + return gamma(m*s + n + r), gamma(1 - n - r - m*s), (-1)**r*pi + + +class MellinTransformStripError(ValueError): + """ + Exception raised by _rewrite_gamma. Mainly for internal use. + """ + pass + + +def _rewrite_gamma(f, s, a, b): + """ + Try to rewrite the product f(s) as a product of gamma functions, + so that the inverse Mellin transform of f can be expressed as a meijer + G function. + + Explanation + =========== + + Return (an, ap), (bm, bq), arg, exp, fac such that + G((an, ap), (bm, bq), arg/z**exp)*fac is the inverse Mellin transform of f(s). + + Raises IntegralTransformError or MellinTransformStripError on failure. + + It is asserted that f has no poles in the fundamental strip designated by + (a, b). One of a and b is allowed to be None. The fundamental strip is + important, because it determines the inversion contour. + + This function can handle exponentials, linear factors, trigonometric + functions. + + This is a helper function for inverse_mellin_transform that will not + attempt any transformations on f. + + Examples + ======== + + >>> from sympy.integrals.transforms import _rewrite_gamma + >>> from sympy.abc import s + >>> from sympy import oo + >>> _rewrite_gamma(s*(s+3)*(s-1), s, -oo, oo) + (([], [-3, 0, 1]), ([-2, 1, 2], []), 1, 1, -1) + >>> _rewrite_gamma((s-1)**2, s, -oo, oo) + (([], [1, 1]), ([2, 2], []), 1, 1, 1) + + Importance of the fundamental strip: + + >>> _rewrite_gamma(1/s, s, 0, oo) + (([1], []), ([], [0]), 1, 1, 1) + >>> _rewrite_gamma(1/s, s, None, oo) + (([1], []), ([], [0]), 1, 1, 1) + >>> _rewrite_gamma(1/s, s, 0, None) + (([1], []), ([], [0]), 1, 1, 1) + >>> _rewrite_gamma(1/s, s, -oo, 0) + (([], [1]), ([0], []), 1, 1, -1) + >>> _rewrite_gamma(1/s, s, None, 0) + (([], [1]), ([0], []), 1, 1, -1) + >>> _rewrite_gamma(1/s, s, -oo, None) + (([], [1]), ([0], []), 1, 1, -1) + + >>> _rewrite_gamma(2**(-s+3), s, -oo, oo) + (([], []), ([], []), 1/2, 1, 8) + """ + # Our strategy will be as follows: + # 1) Guess a constant c such that the inversion integral should be + # performed wrt s'=c*s (instead of plain s). Write s for s'. + # 2) Process all factors, rewrite them independently as gamma functions in + # argument s, or exponentials of s. + # 3) Try to transform all gamma functions s.t. they have argument + # a+s or a-s. + # 4) Check that the resulting G function parameters are valid. + # 5) Combine all the exponentials. + + a_, b_ = S([a, b]) + + def left(c, is_numer): + """ + Decide whether pole at c lies to the left of the fundamental strip. + """ + # heuristically, this is the best chance for us to solve the inequalities + c = expand(re(c)) + if a_ is None and b_ is S.Infinity: + return True + if a_ is None: + return c < b_ + if b_ is None: + return c <= a_ + if (c >= b_) == True: + return False + if (c <= a_) == True: + return True + if is_numer: + return None + if a_.free_symbols or b_.free_symbols or c.free_symbols: + return None # XXX + #raise IntegralTransformError('Inverse Mellin', f, + # 'Could not determine position of singularity %s' + # ' relative to fundamental strip' % c) + raise MellinTransformStripError('Pole inside critical strip?') + + # 1) + s_multipliers = [] + for g in f.atoms(gamma): + if not g.has(s): + continue + arg = g.args[0] + if arg.is_Add: + arg = arg.as_independent(s)[1] + coeff, _ = arg.as_coeff_mul(s) + s_multipliers += [coeff] + for g in f.atoms(sin, cos, tan, cot): + if not g.has(s): + continue + arg = g.args[0] + if arg.is_Add: + arg = arg.as_independent(s)[1] + coeff, _ = arg.as_coeff_mul(s) + s_multipliers += [coeff/pi] + s_multipliers = [Abs(x) if x.is_extended_real else x for x in s_multipliers] + common_coefficient = S.One + for x in s_multipliers: + if not x.is_Rational: + common_coefficient = x + break + s_multipliers = [x/common_coefficient for x in s_multipliers] + if not (all(x.is_Rational for x in s_multipliers) and + common_coefficient.is_extended_real): + raise IntegralTransformError("Gamma", None, "Nonrational multiplier") + s_multiplier = common_coefficient/reduce(ilcm, [S(x.q) + for x in s_multipliers], S.One) + if s_multiplier == common_coefficient: + if len(s_multipliers) == 0: + s_multiplier = common_coefficient + else: + s_multiplier = common_coefficient \ + *reduce(igcd, [S(x.p) for x in s_multipliers]) + + f = f.subs(s, s/s_multiplier) + fac = S.One/s_multiplier + exponent = S.One/s_multiplier + if a_ is not None: + a_ *= s_multiplier + if b_ is not None: + b_ *= s_multiplier + + # 2) + numer, denom = f.as_numer_denom() + numer = Mul.make_args(numer) + denom = Mul.make_args(denom) + args = list(zip(numer, repeat(True))) + list(zip(denom, repeat(False))) + + facs = [] + dfacs = [] + # *_gammas will contain pairs (a, c) representing Gamma(a*s + c) + numer_gammas = [] + denom_gammas = [] + # exponentials will contain bases for exponentials of s + exponentials = [] + + def exception(fact): + return IntegralTransformError("Inverse Mellin", f, "Unrecognised form '%s'." % fact) + while args: + fact, is_numer = args.pop() + if is_numer: + ugammas, lgammas = numer_gammas, denom_gammas + ufacs = facs + else: + ugammas, lgammas = denom_gammas, numer_gammas + ufacs = dfacs + + def linear_arg(arg): + """ Test if arg is of form a*s+b, raise exception if not. """ + if not arg.is_polynomial(s): + raise exception(fact) + p = Poly(arg, s) + if p.degree() != 1: + raise exception(fact) + return p.all_coeffs() + + # constants + if not fact.has(s): + ufacs += [fact] + # exponentials + elif fact.is_Pow or isinstance(fact, exp): + if fact.is_Pow: + base = fact.base + exp_ = fact.exp + else: + base = exp_polar(1) + exp_ = fact.exp + if exp_.is_Integer: + cond = is_numer + if exp_ < 0: + cond = not cond + args += [(base, cond)]*Abs(exp_) + continue + elif not base.has(s): + a, b = linear_arg(exp_) + if not is_numer: + base = 1/base + exponentials += [base**a] + facs += [base**b] + else: + raise exception(fact) + # linear factors + elif fact.is_polynomial(s): + p = Poly(fact, s) + if p.degree() != 1: + # We completely factor the poly. For this we need the roots. + # Now roots() only works in some cases (low degree), and CRootOf + # only works without parameters. So try both... + coeff = p.LT()[1] + rs = roots(p, s) + if len(rs) != p.degree(): + rs = CRootOf.all_roots(p) + ufacs += [coeff] + args += [(s - c, is_numer) for c in rs] + continue + a, c = p.all_coeffs() + ufacs += [a] + c /= -a + # Now need to convert s - c + if left(c, is_numer): + ugammas += [(S.One, -c + 1)] + lgammas += [(S.One, -c)] + else: + ufacs += [-1] + ugammas += [(S.NegativeOne, c + 1)] + lgammas += [(S.NegativeOne, c)] + elif isinstance(fact, gamma): + a, b = linear_arg(fact.args[0]) + if is_numer: + if (a > 0 and (left(-b/a, is_numer) == False)) or \ + (a < 0 and (left(-b/a, is_numer) == True)): + raise NotImplementedError( + 'Gammas partially over the strip.') + ugammas += [(a, b)] + elif isinstance(fact, sin): + # We try to re-write all trigs as gammas. This is not in + # general the best strategy, since sometimes this is impossible, + # but rewriting as exponentials would work. However trig functions + # in inverse mellin transforms usually all come from simplifying + # gamma terms, so this should work. + a = fact.args[0] + if is_numer: + # No problem with the poles. + gamma1, gamma2, fac_ = gamma(a/pi), gamma(1 - a/pi), pi + else: + gamma1, gamma2, fac_ = _rewrite_sin(linear_arg(a), s, a_, b_) + args += [(gamma1, not is_numer), (gamma2, not is_numer)] + ufacs += [fac_] + elif isinstance(fact, tan): + a = fact.args[0] + args += [(sin(a, evaluate=False), is_numer), + (sin(pi/2 - a, evaluate=False), not is_numer)] + elif isinstance(fact, cos): + a = fact.args[0] + args += [(sin(pi/2 - a, evaluate=False), is_numer)] + elif isinstance(fact, cot): + a = fact.args[0] + args += [(sin(pi/2 - a, evaluate=False), is_numer), + (sin(a, evaluate=False), not is_numer)] + else: + raise exception(fact) + + fac *= Mul(*facs)/Mul(*dfacs) + + # 3) + an, ap, bm, bq = [], [], [], [] + for gammas, plus, minus, is_numer in [(numer_gammas, an, bm, True), + (denom_gammas, bq, ap, False)]: + while gammas: + a, c = gammas.pop() + if a != -1 and a != +1: + # We use the gamma function multiplication theorem. + p = Abs(S(a)) + newa = a/p + newc = c/p + if not a.is_Integer: + raise TypeError("a is not an integer") + for k in range(p): + gammas += [(newa, newc + k/p)] + if is_numer: + fac *= (2*pi)**((1 - p)/2) * p**(c - S.Half) + exponentials += [p**a] + else: + fac /= (2*pi)**((1 - p)/2) * p**(c - S.Half) + exponentials += [p**(-a)] + continue + if a == +1: + plus.append(1 - c) + else: + minus.append(c) + + # 4) + # TODO + + # 5) + arg = Mul(*exponentials) + + # for testability, sort the arguments + an.sort(key=default_sort_key) + ap.sort(key=default_sort_key) + bm.sort(key=default_sort_key) + bq.sort(key=default_sort_key) + + return (an, ap), (bm, bq), arg, exponent, fac + + +@_noconds_(True) +def _inverse_mellin_transform(F, s, x_, strip, as_meijerg=False): + """ A helper for the real inverse_mellin_transform function, this one here + assumes x to be real and positive. """ + x = _dummy('t', 'inverse-mellin-transform', F, positive=True) + # Actually, we won't try integration at all. Instead we use the definition + # of the Meijer G function as a fairly general inverse mellin transform. + F = F.rewrite(gamma) + for g in [factor(F), expand_mul(F), expand(F)]: + if g.is_Add: + # do all terms separately + ress = [_inverse_mellin_transform(G, s, x, strip, as_meijerg, + noconds=False) + for G in g.args] + conds = [p[1] for p in ress] + ress = [p[0] for p in ress] + res = Add(*ress) + if not as_meijerg: + res = factor(res, gens=res.atoms(Heaviside)) + return res.subs(x, x_), And(*conds) + + try: + a, b, C, e, fac = _rewrite_gamma(g, s, strip[0], strip[1]) + except IntegralTransformError: + continue + try: + G = meijerg(a, b, C/x**e) + except ValueError: + continue + if as_meijerg: + h = G + else: + try: + from sympy.simplify import hyperexpand + h = hyperexpand(G) + except NotImplementedError: + raise IntegralTransformError( + 'Inverse Mellin', F, 'Could not calculate integral') + + if h.is_Piecewise and len(h.args) == 3: + # XXX we break modularity here! + h = Heaviside(x - Abs(C))*h.args[0].args[0] \ + + Heaviside(Abs(C) - x)*h.args[1].args[0] + # We must ensure that the integral along the line we want converges, + # and return that value. + # See [L], 5.2 + cond = [Abs(arg(G.argument)) < G.delta*pi] + # Note: we allow ">=" here, this corresponds to convergence if we let + # limits go to oo symmetrically. ">" corresponds to absolute convergence. + cond += [And(Or(len(G.ap) != len(G.bq), 0 >= re(G.nu) + 1), + Abs(arg(G.argument)) == G.delta*pi)] + cond = Or(*cond) + if cond == False: + raise IntegralTransformError( + 'Inverse Mellin', F, 'does not converge') + return (h*fac).subs(x, x_), cond + + raise IntegralTransformError('Inverse Mellin', F, '') + +_allowed = None + + +class InverseMellinTransform(IntegralTransform): + """ + Class representing unevaluated inverse Mellin transforms. + + For usage of this class, see the :class:`IntegralTransform` docstring. + + For how to compute inverse Mellin transforms, see the + :func:`inverse_mellin_transform` docstring. + """ + + _name = 'Inverse Mellin' + _none_sentinel = Dummy('None') + _c = Dummy('c') + + def __new__(cls, F, s, x, a, b, **opts): + if a is None: + a = InverseMellinTransform._none_sentinel + if b is None: + b = InverseMellinTransform._none_sentinel + return IntegralTransform.__new__(cls, F, s, x, a, b, **opts) + + @property + def fundamental_strip(self): + a, b = self.args[3], self.args[4] + if a is InverseMellinTransform._none_sentinel: + a = None + if b is InverseMellinTransform._none_sentinel: + b = None + return a, b + + def _compute_transform(self, F, s, x, **hints): + # IntegralTransform's doit will cause this hint to exist, but + # InverseMellinTransform should ignore it + hints.pop('simplify', True) + global _allowed + if _allowed is None: + _allowed = { + exp, gamma, sin, cos, tan, cot, cosh, sinh, tanh, coth, + factorial, rf} + for f in postorder_traversal(F): + if f.is_Function and f.has(s) and f.func not in _allowed: + raise IntegralTransformError('Inverse Mellin', F, + 'Component %s not recognised.' % f) + strip = self.fundamental_strip + return _inverse_mellin_transform(F, s, x, strip, **hints) + + def _as_integral(self, F, s, x): + c = self.__class__._c + return Integral(F*x**(-s), (s, c - S.ImaginaryUnit*S.Infinity, c + + S.ImaginaryUnit*S.Infinity))/(2*S.Pi*S.ImaginaryUnit) + + +def inverse_mellin_transform(F, s, x, strip, **hints): + r""" + Compute the inverse Mellin transform of `F(s)` over the fundamental + strip given by ``strip=(a, b)``. + + Explanation + =========== + + This can be defined as + + .. math:: f(x) = \frac{1}{2\pi i} \int_{c - i\infty}^{c + i\infty} x^{-s} F(s) \mathrm{d}s, + + for any `c` in the fundamental strip. Under certain regularity + conditions on `F` and/or `f`, + this recovers `f` from its Mellin transform `F` + (and vice versa), for positive real `x`. + + One of `a` or `b` may be passed as ``None``; a suitable `c` will be + inferred. + + If the integral cannot be computed in closed form, this function returns + an unevaluated :class:`InverseMellinTransform` object. + + Note that this function will assume x to be positive and real, regardless + of the SymPy assumptions! + + For a description of possible hints, refer to the docstring of + :func:`sympy.integrals.transforms.IntegralTransform.doit`. + + Examples + ======== + + >>> from sympy import inverse_mellin_transform, oo, gamma + >>> from sympy.abc import x, s + >>> inverse_mellin_transform(gamma(s), s, x, (0, oo)) + exp(-x) + + The fundamental strip matters: + + >>> f = 1/(s**2 - 1) + >>> inverse_mellin_transform(f, s, x, (-oo, -1)) + x*(1 - 1/x**2)*Heaviside(x - 1)/2 + >>> inverse_mellin_transform(f, s, x, (-1, 1)) + -x*Heaviside(1 - x)/2 - Heaviside(x - 1)/(2*x) + >>> inverse_mellin_transform(f, s, x, (1, oo)) + (1/2 - x**2/2)*Heaviside(1 - x)/x + + See Also + ======== + + mellin_transform + hankel_transform, inverse_hankel_transform + """ + return InverseMellinTransform(F, s, x, strip[0], strip[1]).doit(**hints) + + +########################################################################## +# Fourier Transform +########################################################################## + +@_noconds_(True) +def _fourier_transform(f, x, k, a, b, name, simplify=True): + r""" + Compute a general Fourier-type transform + + .. math:: + + F(k) = a \int_{-\infty}^{\infty} e^{bixk} f(x)\, dx. + + For suitable choice of *a* and *b*, this reduces to the standard Fourier + and inverse Fourier transforms. + """ + F = integrate(a*f*exp(b*S.ImaginaryUnit*x*k), (x, S.NegativeInfinity, S.Infinity)) + + if not F.has(Integral): + return _simplify(F, simplify), S.true + + integral_f = integrate(f, (x, S.NegativeInfinity, S.Infinity)) + if integral_f in (S.NegativeInfinity, S.Infinity, S.NaN) or integral_f.has(Integral): + raise IntegralTransformError(name, f, 'function not integrable on real axis') + + if not F.is_Piecewise: + raise IntegralTransformError(name, f, 'could not compute integral') + + F, cond = F.args[0] + if F.has(Integral): + raise IntegralTransformError(name, f, 'integral in unexpected form') + + return _simplify(F, simplify), cond + + +class FourierTypeTransform(IntegralTransform): + """ Base class for Fourier transforms.""" + + def a(self): + raise NotImplementedError( + "Class %s must implement a(self) but does not" % self.__class__) + + def b(self): + raise NotImplementedError( + "Class %s must implement b(self) but does not" % self.__class__) + + def _compute_transform(self, f, x, k, **hints): + return _fourier_transform(f, x, k, + self.a(), self.b(), + self.__class__._name, **hints) + + def _as_integral(self, f, x, k): + a = self.a() + b = self.b() + return Integral(a*f*exp(b*S.ImaginaryUnit*x*k), (x, S.NegativeInfinity, S.Infinity)) + + +class FourierTransform(FourierTypeTransform): + """ + Class representing unevaluated Fourier transforms. + + For usage of this class, see the :class:`IntegralTransform` docstring. + + For how to compute Fourier transforms, see the :func:`fourier_transform` + docstring. + """ + + _name = 'Fourier' + + def a(self): + return 1 + + def b(self): + return -2*S.Pi + + +def fourier_transform(f, x, k, **hints): + r""" + Compute the unitary, ordinary-frequency Fourier transform of ``f``, defined + as + + .. math:: F(k) = \int_{-\infty}^\infty f(x) e^{-2\pi i x k} \mathrm{d} x. + + Explanation + =========== + + If the transform cannot be computed in closed form, this + function returns an unevaluated :class:`FourierTransform` object. + + For other Fourier transform conventions, see the function + :func:`sympy.integrals.transforms._fourier_transform`. + + For a description of possible hints, refer to the docstring of + :func:`sympy.integrals.transforms.IntegralTransform.doit`. + Note that for this transform, by default ``noconds=True``. + + Examples + ======== + + >>> from sympy import fourier_transform, exp + >>> from sympy.abc import x, k + >>> fourier_transform(exp(-x**2), x, k) + sqrt(pi)*exp(-pi**2*k**2) + >>> fourier_transform(exp(-x**2), x, k, noconds=False) + (sqrt(pi)*exp(-pi**2*k**2), True) + + See Also + ======== + + inverse_fourier_transform + sine_transform, inverse_sine_transform + cosine_transform, inverse_cosine_transform + hankel_transform, inverse_hankel_transform + mellin_transform, laplace_transform + """ + return FourierTransform(f, x, k).doit(**hints) + + +class InverseFourierTransform(FourierTypeTransform): + """ + Class representing unevaluated inverse Fourier transforms. + + For usage of this class, see the :class:`IntegralTransform` docstring. + + For how to compute inverse Fourier transforms, see the + :func:`inverse_fourier_transform` docstring. + """ + + _name = 'Inverse Fourier' + + def a(self): + return 1 + + def b(self): + return 2*S.Pi + + +def inverse_fourier_transform(F, k, x, **hints): + r""" + Compute the unitary, ordinary-frequency inverse Fourier transform of `F`, + defined as + + .. math:: f(x) = \int_{-\infty}^\infty F(k) e^{2\pi i x k} \mathrm{d} k. + + Explanation + =========== + + If the transform cannot be computed in closed form, this + function returns an unevaluated :class:`InverseFourierTransform` object. + + For other Fourier transform conventions, see the function + :func:`sympy.integrals.transforms._fourier_transform`. + + For a description of possible hints, refer to the docstring of + :func:`sympy.integrals.transforms.IntegralTransform.doit`. + Note that for this transform, by default ``noconds=True``. + + Examples + ======== + + >>> from sympy import inverse_fourier_transform, exp, sqrt, pi + >>> from sympy.abc import x, k + >>> inverse_fourier_transform(sqrt(pi)*exp(-(pi*k)**2), k, x) + exp(-x**2) + >>> inverse_fourier_transform(sqrt(pi)*exp(-(pi*k)**2), k, x, noconds=False) + (exp(-x**2), True) + + See Also + ======== + + fourier_transform + sine_transform, inverse_sine_transform + cosine_transform, inverse_cosine_transform + hankel_transform, inverse_hankel_transform + mellin_transform, laplace_transform + """ + return InverseFourierTransform(F, k, x).doit(**hints) + + +########################################################################## +# Fourier Sine and Cosine Transform +########################################################################## + +@_noconds_(True) +def _sine_cosine_transform(f, x, k, a, b, K, name, simplify=True): + """ + Compute a general sine or cosine-type transform + F(k) = a int_0^oo b*sin(x*k) f(x) dx. + F(k) = a int_0^oo b*cos(x*k) f(x) dx. + + For suitable choice of a and b, this reduces to the standard sine/cosine + and inverse sine/cosine transforms. + """ + F = integrate(a*f*K(b*x*k), (x, S.Zero, S.Infinity)) + + if not F.has(Integral): + return _simplify(F, simplify), S.true + + if not F.is_Piecewise: + raise IntegralTransformError(name, f, 'could not compute integral') + + F, cond = F.args[0] + if F.has(Integral): + raise IntegralTransformError(name, f, 'integral in unexpected form') + + return _simplify(F, simplify), cond + + +class SineCosineTypeTransform(IntegralTransform): + """ + Base class for sine and cosine transforms. + Specify cls._kern. + """ + + def a(self): + raise NotImplementedError( + "Class %s must implement a(self) but does not" % self.__class__) + + def b(self): + raise NotImplementedError( + "Class %s must implement b(self) but does not" % self.__class__) + + + def _compute_transform(self, f, x, k, **hints): + return _sine_cosine_transform(f, x, k, + self.a(), self.b(), + self.__class__._kern, + self.__class__._name, **hints) + + def _as_integral(self, f, x, k): + a = self.a() + b = self.b() + K = self.__class__._kern + return Integral(a*f*K(b*x*k), (x, S.Zero, S.Infinity)) + + +class SineTransform(SineCosineTypeTransform): + """ + Class representing unevaluated sine transforms. + + For usage of this class, see the :class:`IntegralTransform` docstring. + + For how to compute sine transforms, see the :func:`sine_transform` + docstring. + """ + + _name = 'Sine' + _kern = sin + + def a(self): + return sqrt(2)/sqrt(pi) + + def b(self): + return S.One + + +def sine_transform(f, x, k, **hints): + r""" + Compute the unitary, ordinary-frequency sine transform of `f`, defined + as + + .. math:: F(k) = \sqrt{\frac{2}{\pi}} \int_{0}^\infty f(x) \sin(2\pi x k) \mathrm{d} x. + + Explanation + =========== + + If the transform cannot be computed in closed form, this + function returns an unevaluated :class:`SineTransform` object. + + For a description of possible hints, refer to the docstring of + :func:`sympy.integrals.transforms.IntegralTransform.doit`. + Note that for this transform, by default ``noconds=True``. + + Examples + ======== + + >>> from sympy import sine_transform, exp + >>> from sympy.abc import x, k, a + >>> sine_transform(x*exp(-a*x**2), x, k) + sqrt(2)*k*exp(-k**2/(4*a))/(4*a**(3/2)) + >>> sine_transform(x**(-a), x, k) + 2**(1/2 - a)*k**(a - 1)*gamma(1 - a/2)/gamma(a/2 + 1/2) + + See Also + ======== + + fourier_transform, inverse_fourier_transform + inverse_sine_transform + cosine_transform, inverse_cosine_transform + hankel_transform, inverse_hankel_transform + mellin_transform, laplace_transform + """ + return SineTransform(f, x, k).doit(**hints) + + +class InverseSineTransform(SineCosineTypeTransform): + """ + Class representing unevaluated inverse sine transforms. + + For usage of this class, see the :class:`IntegralTransform` docstring. + + For how to compute inverse sine transforms, see the + :func:`inverse_sine_transform` docstring. + """ + + _name = 'Inverse Sine' + _kern = sin + + def a(self): + return sqrt(2)/sqrt(pi) + + def b(self): + return S.One + + +def inverse_sine_transform(F, k, x, **hints): + r""" + Compute the unitary, ordinary-frequency inverse sine transform of `F`, + defined as + + .. math:: f(x) = \sqrt{\frac{2}{\pi}} \int_{0}^\infty F(k) \sin(2\pi x k) \mathrm{d} k. + + Explanation + =========== + + If the transform cannot be computed in closed form, this + function returns an unevaluated :class:`InverseSineTransform` object. + + For a description of possible hints, refer to the docstring of + :func:`sympy.integrals.transforms.IntegralTransform.doit`. + Note that for this transform, by default ``noconds=True``. + + Examples + ======== + + >>> from sympy import inverse_sine_transform, exp, sqrt, gamma + >>> from sympy.abc import x, k, a + >>> inverse_sine_transform(2**((1-2*a)/2)*k**(a - 1)* + ... gamma(-a/2 + 1)/gamma((a+1)/2), k, x) + x**(-a) + >>> inverse_sine_transform(sqrt(2)*k*exp(-k**2/(4*a))/(4*sqrt(a)**3), k, x) + x*exp(-a*x**2) + + See Also + ======== + + fourier_transform, inverse_fourier_transform + sine_transform + cosine_transform, inverse_cosine_transform + hankel_transform, inverse_hankel_transform + mellin_transform, laplace_transform + """ + return InverseSineTransform(F, k, x).doit(**hints) + + +class CosineTransform(SineCosineTypeTransform): + """ + Class representing unevaluated cosine transforms. + + For usage of this class, see the :class:`IntegralTransform` docstring. + + For how to compute cosine transforms, see the :func:`cosine_transform` + docstring. + """ + + _name = 'Cosine' + _kern = cos + + def a(self): + return sqrt(2)/sqrt(pi) + + def b(self): + return S.One + + +def cosine_transform(f, x, k, **hints): + r""" + Compute the unitary, ordinary-frequency cosine transform of `f`, defined + as + + .. math:: F(k) = \sqrt{\frac{2}{\pi}} \int_{0}^\infty f(x) \cos(2\pi x k) \mathrm{d} x. + + Explanation + =========== + + If the transform cannot be computed in closed form, this + function returns an unevaluated :class:`CosineTransform` object. + + For a description of possible hints, refer to the docstring of + :func:`sympy.integrals.transforms.IntegralTransform.doit`. + Note that for this transform, by default ``noconds=True``. + + Examples + ======== + + >>> from sympy import cosine_transform, exp, sqrt, cos + >>> from sympy.abc import x, k, a + >>> cosine_transform(exp(-a*x), x, k) + sqrt(2)*a/(sqrt(pi)*(a**2 + k**2)) + >>> cosine_transform(exp(-a*sqrt(x))*cos(a*sqrt(x)), x, k) + a*exp(-a**2/(2*k))/(2*k**(3/2)) + + See Also + ======== + + fourier_transform, inverse_fourier_transform, + sine_transform, inverse_sine_transform + inverse_cosine_transform + hankel_transform, inverse_hankel_transform + mellin_transform, laplace_transform + """ + return CosineTransform(f, x, k).doit(**hints) + + +class InverseCosineTransform(SineCosineTypeTransform): + """ + Class representing unevaluated inverse cosine transforms. + + For usage of this class, see the :class:`IntegralTransform` docstring. + + For how to compute inverse cosine transforms, see the + :func:`inverse_cosine_transform` docstring. + """ + + _name = 'Inverse Cosine' + _kern = cos + + def a(self): + return sqrt(2)/sqrt(pi) + + def b(self): + return S.One + + +def inverse_cosine_transform(F, k, x, **hints): + r""" + Compute the unitary, ordinary-frequency inverse cosine transform of `F`, + defined as + + .. math:: f(x) = \sqrt{\frac{2}{\pi}} \int_{0}^\infty F(k) \cos(2\pi x k) \mathrm{d} k. + + Explanation + =========== + + If the transform cannot be computed in closed form, this + function returns an unevaluated :class:`InverseCosineTransform` object. + + For a description of possible hints, refer to the docstring of + :func:`sympy.integrals.transforms.IntegralTransform.doit`. + Note that for this transform, by default ``noconds=True``. + + Examples + ======== + + >>> from sympy import inverse_cosine_transform, sqrt, pi + >>> from sympy.abc import x, k, a + >>> inverse_cosine_transform(sqrt(2)*a/(sqrt(pi)*(a**2 + k**2)), k, x) + exp(-a*x) + >>> inverse_cosine_transform(1/sqrt(k), k, x) + 1/sqrt(x) + + See Also + ======== + + fourier_transform, inverse_fourier_transform, + sine_transform, inverse_sine_transform + cosine_transform + hankel_transform, inverse_hankel_transform + mellin_transform, laplace_transform + """ + return InverseCosineTransform(F, k, x).doit(**hints) + + +########################################################################## +# Hankel Transform +########################################################################## + +@_noconds_(True) +def _hankel_transform(f, r, k, nu, name, simplify=True): + r""" + Compute a general Hankel transform + + .. math:: F_\nu(k) = \int_{0}^\infty f(r) J_\nu(k r) r \mathrm{d} r. + """ + F = integrate(f*besselj(nu, k*r)*r, (r, S.Zero, S.Infinity)) + + if not F.has(Integral): + return _simplify(F, simplify), S.true + + if not F.is_Piecewise: + raise IntegralTransformError(name, f, 'could not compute integral') + + F, cond = F.args[0] + if F.has(Integral): + raise IntegralTransformError(name, f, 'integral in unexpected form') + + return _simplify(F, simplify), cond + + +class HankelTypeTransform(IntegralTransform): + """ + Base class for Hankel transforms. + """ + + def doit(self, **hints): + return self._compute_transform(self.function, + self.function_variable, + self.transform_variable, + self.args[3], + **hints) + + def _compute_transform(self, f, r, k, nu, **hints): + return _hankel_transform(f, r, k, nu, self._name, **hints) + + def _as_integral(self, f, r, k, nu): + return Integral(f*besselj(nu, k*r)*r, (r, S.Zero, S.Infinity)) + + @property + def as_integral(self): + return self._as_integral(self.function, + self.function_variable, + self.transform_variable, + self.args[3]) + + +class HankelTransform(HankelTypeTransform): + """ + Class representing unevaluated Hankel transforms. + + For usage of this class, see the :class:`IntegralTransform` docstring. + + For how to compute Hankel transforms, see the :func:`hankel_transform` + docstring. + """ + + _name = 'Hankel' + + +def hankel_transform(f, r, k, nu, **hints): + r""" + Compute the Hankel transform of `f`, defined as + + .. math:: F_\nu(k) = \int_{0}^\infty f(r) J_\nu(k r) r \mathrm{d} r. + + Explanation + =========== + + If the transform cannot be computed in closed form, this + function returns an unevaluated :class:`HankelTransform` object. + + For a description of possible hints, refer to the docstring of + :func:`sympy.integrals.transforms.IntegralTransform.doit`. + Note that for this transform, by default ``noconds=True``. + + Examples + ======== + + >>> from sympy import hankel_transform, inverse_hankel_transform + >>> from sympy import exp + >>> from sympy.abc import r, k, m, nu, a + + >>> ht = hankel_transform(1/r**m, r, k, nu) + >>> ht + 2*k**(m - 2)*gamma(-m/2 + nu/2 + 1)/(2**m*gamma(m/2 + nu/2)) + + >>> inverse_hankel_transform(ht, k, r, nu) + r**(-m) + + >>> ht = hankel_transform(exp(-a*r), r, k, 0) + >>> ht + a/(k**3*(a**2/k**2 + 1)**(3/2)) + + >>> inverse_hankel_transform(ht, k, r, 0) + exp(-a*r) + + See Also + ======== + + fourier_transform, inverse_fourier_transform + sine_transform, inverse_sine_transform + cosine_transform, inverse_cosine_transform + inverse_hankel_transform + mellin_transform, laplace_transform + """ + return HankelTransform(f, r, k, nu).doit(**hints) + + +class InverseHankelTransform(HankelTypeTransform): + """ + Class representing unevaluated inverse Hankel transforms. + + For usage of this class, see the :class:`IntegralTransform` docstring. + + For how to compute inverse Hankel transforms, see the + :func:`inverse_hankel_transform` docstring. + """ + + _name = 'Inverse Hankel' + + +def inverse_hankel_transform(F, k, r, nu, **hints): + r""" + Compute the inverse Hankel transform of `F` defined as + + .. math:: f(r) = \int_{0}^\infty F_\nu(k) J_\nu(k r) k \mathrm{d} k. + + Explanation + =========== + + If the transform cannot be computed in closed form, this + function returns an unevaluated :class:`InverseHankelTransform` object. + + For a description of possible hints, refer to the docstring of + :func:`sympy.integrals.transforms.IntegralTransform.doit`. + Note that for this transform, by default ``noconds=True``. + + Examples + ======== + + >>> from sympy import hankel_transform, inverse_hankel_transform + >>> from sympy import exp + >>> from sympy.abc import r, k, m, nu, a + + >>> ht = hankel_transform(1/r**m, r, k, nu) + >>> ht + 2*k**(m - 2)*gamma(-m/2 + nu/2 + 1)/(2**m*gamma(m/2 + nu/2)) + + >>> inverse_hankel_transform(ht, k, r, nu) + r**(-m) + + >>> ht = hankel_transform(exp(-a*r), r, k, 0) + >>> ht + a/(k**3*(a**2/k**2 + 1)**(3/2)) + + >>> inverse_hankel_transform(ht, k, r, 0) + exp(-a*r) + + See Also + ======== + + fourier_transform, inverse_fourier_transform + sine_transform, inverse_sine_transform + cosine_transform, inverse_cosine_transform + hankel_transform + mellin_transform, laplace_transform + """ + return InverseHankelTransform(F, k, r, nu).doit(**hints) + + +########################################################################## +# Laplace Transform +########################################################################## + +# Stub classes and functions that used to be here +import sympy.integrals.laplace as _laplace + +LaplaceTransform = _laplace.LaplaceTransform +laplace_transform = _laplace.laplace_transform +laplace_correspondence = _laplace.laplace_correspondence +laplace_initial_conds = _laplace.laplace_initial_conds +InverseLaplaceTransform = _laplace.InverseLaplaceTransform +inverse_laplace_transform = _laplace.inverse_laplace_transform diff --git a/venv/lib/python3.11/site-packages/sympy/integrals/trigonometry.py b/venv/lib/python3.11/site-packages/sympy/integrals/trigonometry.py new file mode 100644 index 0000000000000000000000000000000000000000..dd6389bcc79f28ed6c255546685da1a0e061c327 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/integrals/trigonometry.py @@ -0,0 +1,335 @@ +from sympy.core import cacheit, Dummy, Ne, Integer, Rational, S, Wild +from sympy.functions import binomial, sin, cos, Piecewise, Abs +from .integrals import integrate + +# TODO sin(a*x)*cos(b*x) -> sin((a+b)x) + sin((a-b)x) ? + +# creating, each time, Wild's and sin/cos/Mul is expensive. Also, our match & +# subs are very slow when not cached, and if we create Wild each time, we +# effectively block caching. +# +# so we cache the pattern + +# need to use a function instead of lamda since hash of lambda changes on +# each call to _pat_sincos +def _integer_instance(n): + return isinstance(n, Integer) + +@cacheit +def _pat_sincos(x): + a = Wild('a', exclude=[x]) + n, m = [Wild(s, exclude=[x], properties=[_integer_instance]) + for s in 'nm'] + pat = sin(a*x)**n * cos(a*x)**m + return pat, a, n, m + +_u = Dummy('u') + + +def trigintegrate(f, x, conds='piecewise'): + """ + Integrate f = Mul(trig) over x. + + Examples + ======== + + >>> from sympy import sin, cos, tan, sec + >>> from sympy.integrals.trigonometry import trigintegrate + >>> from sympy.abc import x + + >>> trigintegrate(sin(x)*cos(x), x) + sin(x)**2/2 + + >>> trigintegrate(sin(x)**2, x) + x/2 - sin(x)*cos(x)/2 + + >>> trigintegrate(tan(x)*sec(x), x) + 1/cos(x) + + >>> trigintegrate(sin(x)*tan(x), x) + -log(sin(x) - 1)/2 + log(sin(x) + 1)/2 - sin(x) + + References + ========== + + .. [1] https://en.wikibooks.org/wiki/Calculus/Integration_techniques + + See Also + ======== + + sympy.integrals.integrals.Integral.doit + sympy.integrals.integrals.Integral + """ + pat, a, n, m = _pat_sincos(x) + + f = f.rewrite('sincos') + M = f.match(pat) + + if M is None: + return + + n, m = M[n], M[m] + if n.is_zero and m.is_zero: + return x + zz = x if n.is_zero else S.Zero + + a = M[a] + + if n.is_odd or m.is_odd: + u = _u + n_, m_ = n.is_odd, m.is_odd + + # take smallest n or m -- to choose simplest substitution + if n_ and m_: + + # Make sure to choose the positive one + # otherwise an incorrect integral can occur. + if n < 0 and m > 0: + m_ = True + n_ = False + elif m < 0 and n > 0: + n_ = True + m_ = False + # Both are negative so choose the smallest n or m + # in absolute value for simplest substitution. + elif (n < 0 and m < 0): + n_ = n > m + m_ = not (n > m) + + # Both n and m are odd and positive + else: + n_ = (n < m) # NB: careful here, one of the + m_ = not (n < m) # conditions *must* be true + + # n m u=C (n-1)/2 m + # S(x) * C(x) dx --> -(1-u^2) * u du + if n_: + ff = -(1 - u**2)**((n - 1)/2) * u**m + uu = cos(a*x) + + # n m u=S n (m-1)/2 + # S(x) * C(x) dx --> u * (1-u^2) du + elif m_: + ff = u**n * (1 - u**2)**((m - 1)/2) + uu = sin(a*x) + + fi = integrate(ff, u) # XXX cyclic deps + fx = fi.subs(u, uu) + if conds == 'piecewise': + return Piecewise((fx / a, Ne(a, 0)), (zz, True)) + return fx / a + + # n & m are both even + # + # 2k 2m 2l 2l + # we transform S (x) * C (x) into terms with only S (x) or C (x) + # + # example: + # 100 4 100 2 2 100 4 2 + # S (x) * C (x) = S (x) * (1-S (x)) = S (x) * (1 + S (x) - 2*S (x)) + # + # 104 102 100 + # = S (x) - 2*S (x) + S (x) + # 2k + # then S is integrated with recursive formula + + # take largest n or m -- to choose simplest substitution + n_ = (Abs(n) > Abs(m)) + m_ = (Abs(m) > Abs(n)) + res = S.Zero + + if n_: + # 2k 2 k i 2i + # C = (1 - S ) = sum(i, (-) * B(k, i) * S ) + if m > 0: + for i in range(0, m//2 + 1): + res += (S.NegativeOne**i * binomial(m//2, i) * + _sin_pow_integrate(n + 2*i, x)) + + elif m == 0: + res = _sin_pow_integrate(n, x) + else: + + # m < 0 , |n| > |m| + # / + # | + # | m n + # | cos (x) sin (x) dx = + # | + # | + #/ + # / + # | + # -1 m+1 n-1 n - 1 | m+2 n-2 + # ________ cos (x) sin (x) + _______ | cos (x) sin (x) dx + # | + # m + 1 m + 1 | + # / + + res = (Rational(-1, m + 1) * cos(x)**(m + 1) * sin(x)**(n - 1) + + Rational(n - 1, m + 1) * + trigintegrate(cos(x)**(m + 2)*sin(x)**(n - 2), x)) + + elif m_: + # 2k 2 k i 2i + # S = (1 - C ) = sum(i, (-) * B(k, i) * C ) + if n > 0: + + # / / + # | | + # | m n | -m n + # | cos (x)*sin (x) dx or | cos (x) * sin (x) dx + # | | + # / / + # + # |m| > |n| ; m, n >0 ; m, n belong to Z - {0} + # n 2 + # sin (x) term is expanded here in terms of cos (x), + # and then integrated. + # + + for i in range(0, n//2 + 1): + res += (S.NegativeOne**i * binomial(n//2, i) * + _cos_pow_integrate(m + 2*i, x)) + + elif n == 0: + + # / + # | + # | 1 + # | _ _ _ + # | m + # | cos (x) + # / + # + + res = _cos_pow_integrate(m, x) + else: + + # n < 0 , |m| > |n| + # / + # | + # | m n + # | cos (x) sin (x) dx = + # | + # | + #/ + # / + # | + # 1 m-1 n+1 m - 1 | m-2 n+2 + # _______ cos (x) sin (x) + _______ | cos (x) sin (x) dx + # | + # n + 1 n + 1 | + # / + + res = (Rational(1, n + 1) * cos(x)**(m - 1)*sin(x)**(n + 1) + + Rational(m - 1, n + 1) * + trigintegrate(cos(x)**(m - 2)*sin(x)**(n + 2), x)) + + else: + if m == n: + ##Substitute sin(2x)/2 for sin(x)cos(x) and then Integrate. + res = integrate((sin(2*x)*S.Half)**m, x) + elif (m == -n): + if n < 0: + # Same as the scheme described above. + # the function argument to integrate in the end will + # be 1, this cannot be integrated by trigintegrate. + # Hence use sympy.integrals.integrate. + res = (Rational(1, n + 1) * cos(x)**(m - 1) * sin(x)**(n + 1) + + Rational(m - 1, n + 1) * + integrate(cos(x)**(m - 2) * sin(x)**(n + 2), x)) + else: + res = (Rational(-1, m + 1) * cos(x)**(m + 1) * sin(x)**(n - 1) + + Rational(n - 1, m + 1) * + integrate(cos(x)**(m + 2)*sin(x)**(n - 2), x)) + if conds == 'piecewise': + return Piecewise((res.subs(x, a*x) / a, Ne(a, 0)), (zz, True)) + return res.subs(x, a*x) / a + + +def _sin_pow_integrate(n, x): + if n > 0: + if n == 1: + #Recursion break + return -cos(x) + + # n > 0 + # / / + # | | + # | n -1 n-1 n - 1 | n-2 + # | sin (x) dx = ______ cos (x) sin (x) + _______ | sin (x) dx + # | | + # | n n | + #/ / + # + # + + return (Rational(-1, n) * cos(x) * sin(x)**(n - 1) + + Rational(n - 1, n) * _sin_pow_integrate(n - 2, x)) + + if n < 0: + if n == -1: + ##Make sure this does not come back here again. + ##Recursion breaks here or at n==0. + return trigintegrate(1/sin(x), x) + + # n < 0 + # / / + # | | + # | n 1 n+1 n + 2 | n+2 + # | sin (x) dx = _______ cos (x) sin (x) + _______ | sin (x) dx + # | | + # | n + 1 n + 1 | + #/ / + # + + return (Rational(1, n + 1) * cos(x) * sin(x)**(n + 1) + + Rational(n + 2, n + 1) * _sin_pow_integrate(n + 2, x)) + + else: + #n == 0 + #Recursion break. + return x + + +def _cos_pow_integrate(n, x): + if n > 0: + if n == 1: + #Recursion break. + return sin(x) + + # n > 0 + # / / + # | | + # | n 1 n-1 n - 1 | n-2 + # | sin (x) dx = ______ sin (x) cos (x) + _______ | cos (x) dx + # | | + # | n n | + #/ / + # + + return (Rational(1, n) * sin(x) * cos(x)**(n - 1) + + Rational(n - 1, n) * _cos_pow_integrate(n - 2, x)) + + if n < 0: + if n == -1: + ##Recursion break + return trigintegrate(1/cos(x), x) + + # n < 0 + # / / + # | | + # | n -1 n+1 n + 2 | n+2 + # | cos (x) dx = _______ sin (x) cos (x) + _______ | cos (x) dx + # | | + # | n + 1 n + 1 | + #/ / + # + + return (Rational(-1, n + 1) * sin(x) * cos(x)**(n + 1) + + Rational(n + 2, n + 1) * _cos_pow_integrate(n + 2, x)) + else: + # n == 0 + #Recursion Break. + return x diff --git a/venv/lib/python3.11/site-packages/sympy/interactive/__init__.py b/venv/lib/python3.11/site-packages/sympy/interactive/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1b3f043ada6222d79dd52fd28b035e2ea45c5683 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/interactive/__init__.py @@ -0,0 +1,8 @@ +"""Helper module for setting up interactive SymPy sessions. """ + +from .printing import init_printing +from .session import init_session +from .traversal import interactive_traversal + + +__all__ = ['init_printing', 'init_session', 'interactive_traversal'] diff --git a/venv/lib/python3.11/site-packages/sympy/interactive/printing.py b/venv/lib/python3.11/site-packages/sympy/interactive/printing.py new file mode 100644 index 0000000000000000000000000000000000000000..2fcc73e3e96a5b7e25f7fc7ebf54a5781c3b15b9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/interactive/printing.py @@ -0,0 +1,532 @@ +"""Tools for setting up printing in interactive sessions. """ + +from io import BytesIO + +from sympy.printing.latex import latex as default_latex +from sympy.printing.preview import preview +from sympy.utilities.misc import debug +from sympy.printing.defaults import Printable +from sympy.external import import_module + + +def _init_python_printing(stringify_func, **settings): + """Setup printing in Python interactive session. """ + import sys + import builtins + + def _displayhook(arg): + """Python's pretty-printer display hook. + + This function was adapted from: + + https://www.python.org/dev/peps/pep-0217/ + + """ + if arg is not None: + builtins._ = None + print(stringify_func(arg, **settings)) + builtins._ = arg + + sys.displayhook = _displayhook + + +def _init_ipython_printing(ip, stringify_func, use_latex, euler, forecolor, + backcolor, fontsize, latex_mode, print_builtin, + latex_printer, scale, **settings): + """Setup printing in IPython interactive session. """ + IPython = import_module("IPython", min_module_version="1.0") + try: + from IPython.lib.latextools import latex_to_png + except ImportError: + pass + + # Guess best font color if none was given based on the ip.colors string. + # From the IPython documentation: + # It has four case-insensitive values: 'nocolor', 'neutral', 'linux', + # 'lightbg'. The default is neutral, which should be legible on either + # dark or light terminal backgrounds. linux is optimised for dark + # backgrounds and lightbg for light ones. + if forecolor is None: + color = ip.colors.lower() + if color == 'lightbg': + forecolor = 'Black' + elif color == 'linux': + forecolor = 'White' + else: + # No idea, go with gray. + forecolor = 'Gray' + debug("init_printing: Automatic foreground color:", forecolor) + + if use_latex == "svg": + extra_preamble = "\n\\special{color %s}" % forecolor + else: + extra_preamble = "" + + imagesize = 'tight' + offset = "0cm,0cm" + resolution = round(150*scale) + dvi = r"-T %s -D %d -bg %s -fg %s -O %s" % ( + imagesize, resolution, backcolor, forecolor, offset) + dvioptions = dvi.split() + + svg_scale = 150/72*scale + dvioptions_svg = ["--no-fonts", "--scale={}".format(svg_scale)] + + debug("init_printing: DVIOPTIONS:", dvioptions) + debug("init_printing: DVIOPTIONS_SVG:", dvioptions_svg) + + latex = latex_printer or default_latex + + def _print_plain(arg, p, cycle): + """caller for pretty, for use in IPython 0.11""" + if _can_print(arg): + p.text(stringify_func(arg)) + else: + p.text(IPython.lib.pretty.pretty(arg)) + + def _preview_wrapper(o): + exprbuffer = BytesIO() + try: + preview(o, output='png', viewer='BytesIO', euler=euler, + outputbuffer=exprbuffer, extra_preamble=extra_preamble, + dvioptions=dvioptions, fontsize=fontsize) + except Exception as e: + # IPython swallows exceptions + debug("png printing:", "_preview_wrapper exception raised:", + repr(e)) + raise + return exprbuffer.getvalue() + + def _svg_wrapper(o): + exprbuffer = BytesIO() + try: + preview(o, output='svg', viewer='BytesIO', euler=euler, + outputbuffer=exprbuffer, extra_preamble=extra_preamble, + dvioptions=dvioptions_svg, fontsize=fontsize) + except Exception as e: + # IPython swallows exceptions + debug("svg printing:", "_preview_wrapper exception raised:", + repr(e)) + raise + return exprbuffer.getvalue().decode('utf-8') + + def _matplotlib_wrapper(o): + # mathtext can't render some LaTeX commands. For example, it can't + # render any LaTeX environments such as array or matrix. So here we + # ensure that if mathtext fails to render, we return None. + try: + try: + return latex_to_png(o, color=forecolor, scale=scale) + except TypeError: # Old IPython version without color and scale + return latex_to_png(o) + except ValueError as e: + debug('matplotlib exception caught:', repr(e)) + return None + + + # Hook methods for builtin SymPy printers + printing_hooks = ('_latex', '_sympystr', '_pretty', '_sympyrepr') + + + def _can_print(o): + """Return True if type o can be printed with one of the SymPy printers. + + If o is a container type, this is True if and only if every element of + o can be printed in this way. + """ + + try: + # If you're adding another type, make sure you add it to printable_types + # later in this file as well + + builtin_types = (list, tuple, set, frozenset) + if isinstance(o, builtin_types): + # If the object is a custom subclass with a custom str or + # repr, use that instead. + if (type(o).__str__ not in (i.__str__ for i in builtin_types) or + type(o).__repr__ not in (i.__repr__ for i in builtin_types)): + return False + return all(_can_print(i) for i in o) + elif isinstance(o, dict): + return all(_can_print(i) and _can_print(o[i]) for i in o) + elif isinstance(o, bool): + return False + elif isinstance(o, Printable): + # types known to SymPy + return True + elif any(hasattr(o, hook) for hook in printing_hooks): + # types which add support themselves + return True + elif isinstance(o, (float, int)) and print_builtin: + return True + return False + except RuntimeError: + return False + # This is in case maximum recursion depth is reached. + # Since RecursionError is for versions of Python 3.5+ + # so this is to guard against RecursionError for older versions. + + def _print_latex_png(o): + """ + A function that returns a png rendered by an external latex + distribution, falling back to matplotlib rendering + """ + if _can_print(o): + s = latex(o, mode=latex_mode, **settings) + if latex_mode == 'plain': + s = '$\\displaystyle %s$' % s + try: + return _preview_wrapper(s) + except RuntimeError as e: + debug('preview failed with:', repr(e), + ' Falling back to matplotlib backend') + if latex_mode != 'inline': + s = latex(o, mode='inline', **settings) + return _matplotlib_wrapper(s) + + def _print_latex_svg(o): + """ + A function that returns a svg rendered by an external latex + distribution, no fallback available. + """ + if _can_print(o): + s = latex(o, mode=latex_mode, **settings) + if latex_mode == 'plain': + s = '$\\displaystyle %s$' % s + try: + return _svg_wrapper(s) + except RuntimeError as e: + debug('preview failed with:', repr(e), + ' No fallback available.') + + def _print_latex_matplotlib(o): + """ + A function that returns a png rendered by mathtext + """ + if _can_print(o): + s = latex(o, mode='inline', **settings) + return _matplotlib_wrapper(s) + + def _print_latex_text(o): + """ + A function to generate the latex representation of SymPy expressions. + """ + if _can_print(o): + s = latex(o, mode=latex_mode, **settings) + if latex_mode == 'plain': + return '$\\displaystyle %s$' % s + return s + + # Printable is our own type, so we handle it with methods instead of + # the approach required by builtin types. This allows downstream + # packages to override the methods in their own subclasses of Printable, + # which avoids the effects of gh-16002. + printable_types = [float, tuple, list, set, frozenset, dict, int] + + plaintext_formatter = ip.display_formatter.formatters['text/plain'] + + # Exception to the rule above: IPython has better dispatching rules + # for plaintext printing (xref ipython/ipython#8938), and we can't + # use `_repr_pretty_` without hitting a recursion error in _print_plain. + for cls in printable_types + [Printable]: + plaintext_formatter.for_type(cls, _print_plain) + + svg_formatter = ip.display_formatter.formatters['image/svg+xml'] + if use_latex in ('svg', ): + debug("init_printing: using svg formatter") + for cls in printable_types: + svg_formatter.for_type(cls, _print_latex_svg) + Printable._repr_svg_ = _print_latex_svg + else: + debug("init_printing: not using any svg formatter") + for cls in printable_types: + # Better way to set this, but currently does not work in IPython + #png_formatter.for_type(cls, None) + if cls in svg_formatter.type_printers: + svg_formatter.type_printers.pop(cls) + Printable._repr_svg_ = Printable._repr_disabled + + png_formatter = ip.display_formatter.formatters['image/png'] + if use_latex in (True, 'png'): + debug("init_printing: using png formatter") + for cls in printable_types: + png_formatter.for_type(cls, _print_latex_png) + Printable._repr_png_ = _print_latex_png + elif use_latex == 'matplotlib': + debug("init_printing: using matplotlib formatter") + for cls in printable_types: + png_formatter.for_type(cls, _print_latex_matplotlib) + Printable._repr_png_ = _print_latex_matplotlib + else: + debug("init_printing: not using any png formatter") + for cls in printable_types: + # Better way to set this, but currently does not work in IPython + #png_formatter.for_type(cls, None) + if cls in png_formatter.type_printers: + png_formatter.type_printers.pop(cls) + Printable._repr_png_ = Printable._repr_disabled + + latex_formatter = ip.display_formatter.formatters['text/latex'] + if use_latex in (True, 'mathjax'): + debug("init_printing: using mathjax formatter") + for cls in printable_types: + latex_formatter.for_type(cls, _print_latex_text) + Printable._repr_latex_ = _print_latex_text + else: + debug("init_printing: not using text/latex formatter") + for cls in printable_types: + # Better way to set this, but currently does not work in IPython + #latex_formatter.for_type(cls, None) + if cls in latex_formatter.type_printers: + latex_formatter.type_printers.pop(cls) + Printable._repr_latex_ = Printable._repr_disabled + +def _is_ipython(shell): + """Is a shell instance an IPython shell?""" + # shortcut, so we don't import IPython if we don't have to + from sys import modules + if 'IPython' not in modules: + return False + try: + from IPython.core.interactiveshell import InteractiveShell + except ImportError: + # IPython < 0.11 + try: + from IPython.iplib import InteractiveShell + except ImportError: + # Reaching this points means IPython has changed in a backward-incompatible way + # that we don't know about. Warn? + return False + return isinstance(shell, InteractiveShell) + +# Used by the doctester to override the default for no_global +NO_GLOBAL = False + +def init_printing(pretty_print=True, order=None, use_unicode=None, + use_latex=None, wrap_line=None, num_columns=None, + no_global=False, ip=None, euler=False, forecolor=None, + backcolor='Transparent', fontsize='10pt', + latex_mode='plain', print_builtin=True, + str_printer=None, pretty_printer=None, + latex_printer=None, scale=1.0, **settings): + r""" + Initializes pretty-printer depending on the environment. + + Parameters + ========== + + pretty_print : bool, default=True + If ``True``, use :func:`~.pretty_print` to stringify or the provided pretty + printer; if ``False``, use :func:`~.sstrrepr` to stringify or the provided string + printer. + order : string or None, default='lex' + There are a few different settings for this parameter: + ``'lex'`` (default), which is lexographic order; + ``'grlex'``, which is graded lexographic order; + ``'grevlex'``, which is reversed graded lexographic order; + ``'old'``, which is used for compatibility reasons and for long expressions; + ``None``, which sets it to lex. + use_unicode : bool or None, default=None + If ``True``, use unicode characters; + if ``False``, do not use unicode characters; + if ``None``, make a guess based on the environment. + use_latex : string, bool, or None, default=None + If ``True``, use default LaTeX rendering in GUI interfaces (png and + mathjax); + if ``False``, do not use LaTeX rendering; + if ``None``, make a guess based on the environment; + if ``'png'``, enable LaTeX rendering with an external LaTeX compiler, + falling back to matplotlib if external compilation fails; + if ``'matplotlib'``, enable LaTeX rendering with matplotlib; + if ``'mathjax'``, enable LaTeX text generation, for example MathJax + rendering in IPython notebook or text rendering in LaTeX documents; + if ``'svg'``, enable LaTeX rendering with an external latex compiler, + no fallback + wrap_line : bool + If True, lines will wrap at the end; if False, they will not wrap + but continue as one line. This is only relevant if ``pretty_print`` is + True. + num_columns : int or None, default=None + If ``int``, number of columns before wrapping is set to num_columns; if + ``None``, number of columns before wrapping is set to terminal width. + This is only relevant if ``pretty_print`` is ``True``. + no_global : bool, default=False + If ``True``, the settings become system wide; + if ``False``, use just for this console/session. + ip : An interactive console + This can either be an instance of IPython, + or a class that derives from code.InteractiveConsole. + euler : bool, optional, default=False + Loads the euler package in the LaTeX preamble for handwritten style + fonts (https://www.ctan.org/pkg/euler). + forecolor : string or None, optional, default=None + DVI setting for foreground color. ``None`` means that either ``'Black'``, + ``'White'``, or ``'Gray'`` will be selected based on a guess of the IPython + terminal color setting. See notes. + backcolor : string, optional, default='Transparent' + DVI setting for background color. See notes. + fontsize : string or int, optional, default='10pt' + A font size to pass to the LaTeX documentclass function in the + preamble. Note that the options are limited by the documentclass. + Consider using scale instead. + latex_mode : string, optional, default='plain' + The mode used in the LaTeX printer. Can be one of: + ``{'inline'|'plain'|'equation'|'equation*'}``. + print_builtin : boolean, optional, default=True + If ``True`` then floats and integers will be printed. If ``False`` the + printer will only print SymPy types. + str_printer : function, optional, default=None + A custom string printer function. This should mimic + :func:`~.sstrrepr`. + pretty_printer : function, optional, default=None + A custom pretty printer. This should mimic :func:`~.pretty`. + latex_printer : function, optional, default=None + A custom LaTeX printer. This should mimic :func:`~.latex`. + scale : float, optional, default=1.0 + Scale the LaTeX output when using the ``'png'`` or ``'svg'`` backends. + Useful for high dpi screens. + settings : + Any additional settings for the ``latex`` and ``pretty`` commands can + be used to fine-tune the output. + + Examples + ======== + + >>> from sympy.interactive import init_printing + >>> from sympy import Symbol, sqrt + >>> from sympy.abc import x, y + >>> sqrt(5) + sqrt(5) + >>> init_printing(pretty_print=True) # doctest: +SKIP + >>> sqrt(5) # doctest: +SKIP + ___ + \/ 5 + >>> theta = Symbol('theta') # doctest: +SKIP + >>> init_printing(use_unicode=True) # doctest: +SKIP + >>> theta # doctest: +SKIP + \u03b8 + >>> init_printing(use_unicode=False) # doctest: +SKIP + >>> theta # doctest: +SKIP + theta + >>> init_printing(order='lex') # doctest: +SKIP + >>> str(y + x + y**2 + x**2) # doctest: +SKIP + x**2 + x + y**2 + y + >>> init_printing(order='grlex') # doctest: +SKIP + >>> str(y + x + y**2 + x**2) # doctest: +SKIP + x**2 + x + y**2 + y + >>> init_printing(order='grevlex') # doctest: +SKIP + >>> str(y * x**2 + x * y**2) # doctest: +SKIP + x**2*y + x*y**2 + >>> init_printing(order='old') # doctest: +SKIP + >>> str(x**2 + y**2 + x + y) # doctest: +SKIP + x**2 + x + y**2 + y + >>> init_printing(num_columns=10) # doctest: +SKIP + >>> x**2 + x + y**2 + y # doctest: +SKIP + x + y + + x**2 + y**2 + + Notes + ===== + + The foreground and background colors can be selected when using ``'png'`` or + ``'svg'`` LaTeX rendering. Note that before the ``init_printing`` command is + executed, the LaTeX rendering is handled by the IPython console and not SymPy. + + The colors can be selected among the 68 standard colors known to ``dvips``, + for a list see [1]_. In addition, the background color can be + set to ``'Transparent'`` (which is the default value). + + When using the ``'Auto'`` foreground color, the guess is based on the + ``colors`` variable in the IPython console, see [2]_. Hence, if + that variable is set correctly in your IPython console, there is a high + chance that the output will be readable, although manual settings may be + needed. + + + References + ========== + + .. [1] https://en.wikibooks.org/wiki/LaTeX/Colors#The_68_standard_colors_known_to_dvips + + .. [2] https://ipython.readthedocs.io/en/stable/config/details.html#terminal-colors + + See Also + ======== + + sympy.printing.latex + sympy.printing.pretty + + """ + import sys + from sympy.printing.printer import Printer + + if pretty_print: + if pretty_printer is not None: + stringify_func = pretty_printer + else: + from sympy.printing import pretty as stringify_func + else: + if str_printer is not None: + stringify_func = str_printer + else: + from sympy.printing import sstrrepr as stringify_func + + # Even if ip is not passed, double check that not in IPython shell + in_ipython = False + if ip is None: + try: + ip = get_ipython() + except NameError: + pass + else: + in_ipython = (ip is not None) + + if ip and not in_ipython: + in_ipython = _is_ipython(ip) + + if in_ipython and pretty_print: + try: + from IPython.terminal.interactiveshell import TerminalInteractiveShell + from code import InteractiveConsole + except ImportError: + pass + else: + # This will be True if we are in the qtconsole or notebook + if not isinstance(ip, (InteractiveConsole, TerminalInteractiveShell)) \ + and 'ipython-console' not in ''.join(sys.argv): + if use_unicode is None: + debug("init_printing: Setting use_unicode to True") + use_unicode = True + if use_latex is None: + debug("init_printing: Setting use_latex to True") + use_latex = True + + if not NO_GLOBAL and not no_global: + Printer.set_global_settings(order=order, use_unicode=use_unicode, + wrap_line=wrap_line, num_columns=num_columns) + else: + _stringify_func = stringify_func + + if pretty_print: + stringify_func = lambda expr, **settings: \ + _stringify_func(expr, order=order, + use_unicode=use_unicode, + wrap_line=wrap_line, + num_columns=num_columns, + **settings) + else: + stringify_func = \ + lambda expr, **settings: _stringify_func( + expr, order=order, **settings) + + if in_ipython: + mode_in_settings = settings.pop("mode", None) + if mode_in_settings: + debug("init_printing: Mode is not able to be set due to internals" + "of IPython printing") + _init_ipython_printing(ip, stringify_func, use_latex, euler, + forecolor, backcolor, fontsize, latex_mode, + print_builtin, latex_printer, scale, + **settings) + else: + _init_python_printing(stringify_func, **settings) diff --git a/venv/lib/python3.11/site-packages/sympy/interactive/session.py b/venv/lib/python3.11/site-packages/sympy/interactive/session.py new file mode 100644 index 0000000000000000000000000000000000000000..348b0938d69e5e7ffa9510f7d9ac759eb6683b8f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/interactive/session.py @@ -0,0 +1,463 @@ +"""Tools for setting up interactive sessions. """ + +from sympy.external.gmpy import GROUND_TYPES +from sympy.external.importtools import version_tuple + +from sympy.interactive.printing import init_printing + +from sympy.utilities.misc import ARCH + +preexec_source = """\ +from sympy import * +x, y, z, t = symbols('x y z t') +k, m, n = symbols('k m n', integer=True) +f, g, h = symbols('f g h', cls=Function) +init_printing() +""" + +verbose_message = """\ +These commands were executed: +%(source)s +Documentation can be found at https://docs.sympy.org/%(version)s +""" + +no_ipython = """\ +Could not locate IPython. Having IPython installed is greatly recommended. +See http://ipython.scipy.org for more details. If you use Debian/Ubuntu, +just install the 'ipython' package and start isympy again. +""" + + +def _make_message(ipython=True, quiet=False, source=None): + """Create a banner for an interactive session. """ + from sympy import __version__ as sympy_version + from sympy import SYMPY_DEBUG + + import sys + import os + + if quiet: + return "" + + python_version = "%d.%d.%d" % sys.version_info[:3] + + if ipython: + shell_name = "IPython" + else: + shell_name = "Python" + + info = ['ground types: %s' % GROUND_TYPES] + + cache = os.getenv('SYMPY_USE_CACHE') + + if cache is not None and cache.lower() == 'no': + info.append('cache: off') + + if SYMPY_DEBUG: + info.append('debugging: on') + + args = shell_name, sympy_version, python_version, ARCH, ', '.join(info) + message = "%s console for SymPy %s (Python %s-%s) (%s)\n" % args + + if source is None: + source = preexec_source + + _source = "" + + for line in source.split('\n')[:-1]: + if not line: + _source += '\n' + else: + _source += '>>> ' + line + '\n' + + doc_version = sympy_version + if 'dev' in doc_version: + doc_version = "dev" + else: + doc_version = "%s/" % doc_version + + message += '\n' + verbose_message % {'source': _source, + 'version': doc_version} + + return message + + +def int_to_Integer(s): + """ + Wrap integer literals with Integer. + + This is based on the decistmt example from + https://docs.python.org/3/library/tokenize.html. + + Only integer literals are converted. Float literals are left alone. + + Examples + ======== + + >>> from sympy import Integer # noqa: F401 + >>> from sympy.interactive.session import int_to_Integer + >>> s = '1.2 + 1/2 - 0x12 + a1' + >>> int_to_Integer(s) + '1.2 +Integer (1 )/Integer (2 )-Integer (0x12 )+a1 ' + >>> s = 'print (1/2)' + >>> int_to_Integer(s) + 'print (Integer (1 )/Integer (2 ))' + >>> exec(s) + 0.5 + >>> exec(int_to_Integer(s)) + 1/2 + """ + from tokenize import generate_tokens, untokenize, NUMBER, NAME, OP + from io import StringIO + + def _is_int(num): + """ + Returns true if string value num (with token NUMBER) represents an integer. + """ + # XXX: Is there something in the standard library that will do this? + if '.' in num or 'j' in num.lower() or 'e' in num.lower(): + return False + return True + + result = [] + g = generate_tokens(StringIO(s).readline) # tokenize the string + for toknum, tokval, _, _, _ in g: + if toknum == NUMBER and _is_int(tokval): # replace NUMBER tokens + result.extend([ + (NAME, 'Integer'), + (OP, '('), + (NUMBER, tokval), + (OP, ')') + ]) + else: + result.append((toknum, tokval)) + return untokenize(result) + + +def enable_automatic_int_sympification(shell): + """ + Allow IPython to automatically convert integer literals to Integer. + """ + import ast + old_run_cell = shell.run_cell + + def my_run_cell(cell, *args, **kwargs): + try: + # Check the cell for syntax errors. This way, the syntax error + # will show the original input, not the transformed input. The + # downside here is that IPython magic like %timeit will not work + # with transformed input (but on the other hand, IPython magic + # that doesn't expect transformed input will continue to work). + ast.parse(cell) + except SyntaxError: + pass + else: + cell = int_to_Integer(cell) + return old_run_cell(cell, *args, **kwargs) + + shell.run_cell = my_run_cell + + +def enable_automatic_symbols(shell): + """Allow IPython to automatically create symbols (``isympy -a``). """ + # XXX: This should perhaps use tokenize, like int_to_Integer() above. + # This would avoid re-executing the code, which can lead to subtle + # issues. For example: + # + # In [1]: a = 1 + # + # In [2]: for i in range(10): + # ...: a += 1 + # ...: + # + # In [3]: a + # Out[3]: 11 + # + # In [4]: a = 1 + # + # In [5]: for i in range(10): + # ...: a += 1 + # ...: print b + # ...: + # b + # b + # b + # b + # b + # b + # b + # b + # b + # b + # + # In [6]: a + # Out[6]: 12 + # + # Note how the for loop is executed again because `b` was not defined, but `a` + # was already incremented once, so the result is that it is incremented + # multiple times. + + import re + re_nameerror = re.compile( + "name '(?P[A-Za-z_][A-Za-z0-9_]*)' is not defined") + + def _handler(self, etype, value, tb, tb_offset=None): + """Handle :exc:`NameError` exception and allow injection of missing symbols. """ + if etype is NameError and tb.tb_next and not tb.tb_next.tb_next: + match = re_nameerror.match(str(value)) + + if match is not None: + # XXX: Make sure Symbol is in scope. Otherwise you'll get infinite recursion. + self.run_cell("%(symbol)s = Symbol('%(symbol)s')" % + {'symbol': match.group("symbol")}, store_history=False) + + try: + code = self.user_ns['In'][-1] + except (KeyError, IndexError): + pass + else: + self.run_cell(code, store_history=False) + return None + finally: + self.run_cell("del %s" % match.group("symbol"), + store_history=False) + + stb = self.InteractiveTB.structured_traceback( + etype, value, tb, tb_offset=tb_offset) + self._showtraceback(etype, value, stb) + + shell.set_custom_exc((NameError,), _handler) + + +def init_ipython_session(shell=None, argv=[], auto_symbols=False, auto_int_to_Integer=False): + """Construct new IPython session. """ + import IPython + + if version_tuple(IPython.__version__) >= version_tuple('0.11'): + if not shell: + # use an app to parse the command line, and init config + # IPython 1.0 deprecates the frontend module, so we import directly + # from the terminal module to prevent a deprecation message from being + # shown. + if version_tuple(IPython.__version__) >= version_tuple('1.0'): + from IPython.terminal import ipapp + else: + from IPython.frontend.terminal import ipapp + app = ipapp.TerminalIPythonApp() + + # don't draw IPython banner during initialization: + app.display_banner = False + app.initialize(argv) + + shell = app.shell + + if auto_symbols: + enable_automatic_symbols(shell) + if auto_int_to_Integer: + enable_automatic_int_sympification(shell) + + return shell + else: + from IPython.Shell import make_IPython + return make_IPython(argv) + + +def init_python_session(): + """Construct new Python session. """ + from code import InteractiveConsole + + class SymPyConsole(InteractiveConsole): + """An interactive console with readline support. """ + + def __init__(self): + ns_locals = {} + InteractiveConsole.__init__(self, locals=ns_locals) + try: + import rlcompleter + import readline + except ImportError: + pass + else: + import os + import atexit + + readline.set_completer(rlcompleter.Completer(ns_locals).complete) + readline.parse_and_bind('tab: complete') + + if hasattr(readline, 'read_history_file'): + history = os.path.expanduser('~/.sympy-history') + + try: + readline.read_history_file(history) + except OSError: + pass + + atexit.register(readline.write_history_file, history) + + return SymPyConsole() + + +def init_session(ipython=None, pretty_print=True, order=None, + use_unicode=None, use_latex=None, quiet=False, auto_symbols=False, + auto_int_to_Integer=False, str_printer=None, pretty_printer=None, + latex_printer=None, argv=[]): + """ + Initialize an embedded IPython or Python session. The IPython session is + initiated with the --pylab option, without the numpy imports, so that + matplotlib plotting can be interactive. + + Parameters + ========== + + pretty_print: boolean + If True, use pretty_print to stringify; + if False, use sstrrepr to stringify. + order: string or None + There are a few different settings for this parameter: + lex (default), which is lexographic order; + grlex, which is graded lexographic order; + grevlex, which is reversed graded lexographic order; + old, which is used for compatibility reasons and for long expressions; + None, which sets it to lex. + use_unicode: boolean or None + If True, use unicode characters; + if False, do not use unicode characters. + use_latex: boolean or None + If True, use latex rendering if IPython GUI's; + if False, do not use latex rendering. + quiet: boolean + If True, init_session will not print messages regarding its status; + if False, init_session will print messages regarding its status. + auto_symbols: boolean + If True, IPython will automatically create symbols for you. + If False, it will not. + The default is False. + auto_int_to_Integer: boolean + If True, IPython will automatically wrap int literals with Integer, so + that things like 1/2 give Rational(1, 2). + If False, it will not. + The default is False. + ipython: boolean or None + If True, printing will initialize for an IPython console; + if False, printing will initialize for a normal console; + The default is None, which automatically determines whether we are in + an ipython instance or not. + str_printer: function, optional, default=None + A custom string printer function. This should mimic + sympy.printing.sstrrepr(). + pretty_printer: function, optional, default=None + A custom pretty printer. This should mimic sympy.printing.pretty(). + latex_printer: function, optional, default=None + A custom LaTeX printer. This should mimic sympy.printing.latex() + This should mimic sympy.printing.latex(). + argv: list of arguments for IPython + See sympy.bin.isympy for options that can be used to initialize IPython. + + See Also + ======== + + sympy.interactive.printing.init_printing: for examples and the rest of the parameters. + + + Examples + ======== + + >>> from sympy import init_session, Symbol, sin, sqrt + >>> sin(x) #doctest: +SKIP + NameError: name 'x' is not defined + >>> init_session() #doctest: +SKIP + >>> sin(x) #doctest: +SKIP + sin(x) + >>> sqrt(5) #doctest: +SKIP + ___ + \\/ 5 + >>> init_session(pretty_print=False) #doctest: +SKIP + >>> sqrt(5) #doctest: +SKIP + sqrt(5) + >>> y + x + y**2 + x**2 #doctest: +SKIP + x**2 + x + y**2 + y + >>> init_session(order='grlex') #doctest: +SKIP + >>> y + x + y**2 + x**2 #doctest: +SKIP + x**2 + y**2 + x + y + >>> init_session(order='grevlex') #doctest: +SKIP + >>> y * x**2 + x * y**2 #doctest: +SKIP + x**2*y + x*y**2 + >>> init_session(order='old') #doctest: +SKIP + >>> x**2 + y**2 + x + y #doctest: +SKIP + x + y + x**2 + y**2 + >>> theta = Symbol('theta') #doctest: +SKIP + >>> theta #doctest: +SKIP + theta + >>> init_session(use_unicode=True) #doctest: +SKIP + >>> theta # doctest: +SKIP + \u03b8 + """ + import sys + + in_ipython = False + + if ipython is not False: + try: + import IPython + except ImportError: + if ipython is True: + raise RuntimeError("IPython is not available on this system") + ip = None + else: + try: + from IPython import get_ipython + ip = get_ipython() + except ImportError: + ip = None + in_ipython = bool(ip) + if ipython is None: + ipython = in_ipython + + if ipython is False: + ip = init_python_session() + mainloop = ip.interact + else: + ip = init_ipython_session(ip, argv=argv, auto_symbols=auto_symbols, + auto_int_to_Integer=auto_int_to_Integer) + + if version_tuple(IPython.__version__) >= version_tuple('0.11'): + # runsource is gone, use run_cell instead, which doesn't + # take a symbol arg. The second arg is `store_history`, + # and False means don't add the line to IPython's history. + ip.runsource = lambda src, symbol='exec': ip.run_cell(src, False) + + # Enable interactive plotting using pylab. + try: + ip.enable_pylab(import_all=False) + except Exception: + # Causes an import error if matplotlib is not installed. + # Causes other errors (depending on the backend) if there + # is no display, or if there is some problem in the + # backend, so we have a bare "except Exception" here + pass + if not in_ipython: + mainloop = ip.mainloop + + if auto_symbols and (not ipython or version_tuple(IPython.__version__) < version_tuple('0.11')): + raise RuntimeError("automatic construction of symbols is possible only in IPython 0.11 or above") + if auto_int_to_Integer and (not ipython or version_tuple(IPython.__version__) < version_tuple('0.11')): + raise RuntimeError("automatic int to Integer transformation is possible only in IPython 0.11 or above") + + _preexec_source = preexec_source + + ip.runsource(_preexec_source, symbol='exec') + init_printing(pretty_print=pretty_print, order=order, + use_unicode=use_unicode, use_latex=use_latex, ip=ip, + str_printer=str_printer, pretty_printer=pretty_printer, + latex_printer=latex_printer) + + message = _make_message(ipython, quiet, _preexec_source) + + if not in_ipython: + print(message) + mainloop() + sys.exit('Exiting ...') + else: + print(message) + import atexit + atexit.register(lambda: print("Exiting ...\n")) diff --git a/venv/lib/python3.11/site-packages/sympy/interactive/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/interactive/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/interactive/tests/test_interactive.py b/venv/lib/python3.11/site-packages/sympy/interactive/tests/test_interactive.py new file mode 100644 index 0000000000000000000000000000000000000000..3e088c42fd872c13849e593b04734158f5d1e5bc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/interactive/tests/test_interactive.py @@ -0,0 +1,10 @@ +from sympy.interactive.session import int_to_Integer + + +def test_int_to_Integer(): + assert int_to_Integer("1 + 2.2 + 0x3 + 40") == \ + 'Integer (1 )+2.2 +Integer (0x3 )+Integer (40 )' + assert int_to_Integer("0b101") == 'Integer (0b101 )' + assert int_to_Integer("ab1 + 1 + '1 + 2'") == "ab1 +Integer (1 )+'1 + 2'" + assert int_to_Integer("(2 + \n3)") == '(Integer (2 )+\nInteger (3 ))' + assert int_to_Integer("2 + 2.0 + 2j + 2e-10") == 'Integer (2 )+2.0 +2j +2e-10 ' diff --git a/venv/lib/python3.11/site-packages/sympy/interactive/tests/test_ipython.py b/venv/lib/python3.11/site-packages/sympy/interactive/tests/test_ipython.py new file mode 100644 index 0000000000000000000000000000000000000000..ac4734406d2f1197732a9dcbdd94b2b34e9fe170 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/interactive/tests/test_ipython.py @@ -0,0 +1,278 @@ +"""Tests of tools for setting up interactive IPython sessions. """ + +from sympy.interactive.session import (init_ipython_session, + enable_automatic_symbols, enable_automatic_int_sympification) + +from sympy.core import Symbol, Rational, Integer +from sympy.external import import_module +from sympy.testing.pytest import raises + +# TODO: The code below could be made more granular with something like: +# +# @requires('IPython', version=">=1.0") +# def test_automatic_symbols(ipython): + +ipython = import_module("IPython", min_module_version="1.0") + +if not ipython: + #bin/test will not execute any tests now + disabled = True + +# WARNING: These tests will modify the existing IPython environment. IPython +# uses a single instance for its interpreter, so there is no way to isolate +# the test from another IPython session. It also means that if this test is +# run twice in the same Python session it will fail. This isn't usually a +# problem because the test suite is run in a subprocess by default, but if the +# tests are run with subprocess=False it can pollute the current IPython +# session. See the discussion in issue #15149. + +def test_automatic_symbols(): + # NOTE: Because of the way the hook works, you have to use run_cell(code, + # True). This means that the code must have no Out, or it will be printed + # during the tests. + app = init_ipython_session() + app.run_cell("from sympy import *") + + enable_automatic_symbols(app) + + symbol = "verylongsymbolname" + assert symbol not in app.user_ns + app.run_cell("a = %s" % symbol, True) + assert symbol not in app.user_ns + app.run_cell("a = type(%s)" % symbol, True) + assert app.user_ns['a'] == Symbol + app.run_cell("%s = Symbol('%s')" % (symbol, symbol), True) + assert symbol in app.user_ns + + # Check that built-in names aren't overridden + app.run_cell("a = all == __builtin__.all", True) + assert "all" not in app.user_ns + assert app.user_ns['a'] is True + + # Check that SymPy names aren't overridden + app.run_cell("import sympy") + app.run_cell("a = factorial == sympy.factorial", True) + assert app.user_ns['a'] is True + + +def test_int_to_Integer(): + # XXX: Warning, don't test with == here. 0.5 == Rational(1, 2) is True! + app = init_ipython_session() + app.run_cell("from sympy import Integer") + app.run_cell("a = 1") + assert isinstance(app.user_ns['a'], int) + + enable_automatic_int_sympification(app) + app.run_cell("a = 1/2") + assert isinstance(app.user_ns['a'], Rational) + app.run_cell("a = 1") + assert isinstance(app.user_ns['a'], Integer) + app.run_cell("a = int(1)") + assert isinstance(app.user_ns['a'], int) + app.run_cell("a = (1/\n2)") + assert app.user_ns['a'] == Rational(1, 2) + # TODO: How can we test that the output of a SyntaxError is the original + # input, not the transformed input? + + +def test_ipythonprinting(): + # Initialize and setup IPython session + app = init_ipython_session() + app.run_cell("ip = get_ipython()") + app.run_cell("inst = ip.instance()") + app.run_cell("format = inst.display_formatter.format") + app.run_cell("from sympy import Symbol") + + # Printing without printing extension + app.run_cell("a = format(Symbol('pi'))") + app.run_cell("a2 = format(Symbol('pi')**2)") + # Deal with API change starting at IPython 1.0 + if int(ipython.__version__.split(".")[0]) < 1: + assert app.user_ns['a']['text/plain'] == "pi" + assert app.user_ns['a2']['text/plain'] == "pi**2" + else: + assert app.user_ns['a'][0]['text/plain'] == "pi" + assert app.user_ns['a2'][0]['text/plain'] == "pi**2" + + # Load printing extension + app.run_cell("from sympy import init_printing") + app.run_cell("init_printing()") + # Printing with printing extension + app.run_cell("a = format(Symbol('pi'))") + app.run_cell("a2 = format(Symbol('pi')**2)") + # Deal with API change starting at IPython 1.0 + if int(ipython.__version__.split(".")[0]) < 1: + assert app.user_ns['a']['text/plain'] in ('\N{GREEK SMALL LETTER PI}', 'pi') + assert app.user_ns['a2']['text/plain'] in (' 2\n\N{GREEK SMALL LETTER PI} ', ' 2\npi ') + else: + assert app.user_ns['a'][0]['text/plain'] in ('\N{GREEK SMALL LETTER PI}', 'pi') + assert app.user_ns['a2'][0]['text/plain'] in (' 2\n\N{GREEK SMALL LETTER PI} ', ' 2\npi ') + + +def test_print_builtin_option(): + # Initialize and setup IPython session + app = init_ipython_session() + app.run_cell("ip = get_ipython()") + app.run_cell("inst = ip.instance()") + app.run_cell("format = inst.display_formatter.format") + app.run_cell("from sympy import Symbol") + app.run_cell("from sympy import init_printing") + + app.run_cell("a = format({Symbol('pi'): 3.14, Symbol('n_i'): 3})") + # Deal with API change starting at IPython 1.0 + if int(ipython.__version__.split(".")[0]) < 1: + text = app.user_ns['a']['text/plain'] + raises(KeyError, lambda: app.user_ns['a']['text/latex']) + else: + text = app.user_ns['a'][0]['text/plain'] + raises(KeyError, lambda: app.user_ns['a'][0]['text/latex']) + # XXX: How can we make this ignore the terminal width? This test fails if + # the terminal is too narrow. + assert text in ("{pi: 3.14, n_i: 3}", + '{n\N{LATIN SUBSCRIPT SMALL LETTER I}: 3, \N{GREEK SMALL LETTER PI}: 3.14}', + "{n_i: 3, pi: 3.14}", + '{\N{GREEK SMALL LETTER PI}: 3.14, n\N{LATIN SUBSCRIPT SMALL LETTER I}: 3}') + + # If we enable the default printing, then the dictionary's should render + # as a LaTeX version of the whole dict: ${\pi: 3.14, n_i: 3}$ + app.run_cell("inst.display_formatter.formatters['text/latex'].enabled = True") + app.run_cell("init_printing(use_latex=True)") + app.run_cell("a = format({Symbol('pi'): 3.14, Symbol('n_i'): 3})") + # Deal with API change starting at IPython 1.0 + if int(ipython.__version__.split(".")[0]) < 1: + text = app.user_ns['a']['text/plain'] + latex = app.user_ns['a']['text/latex'] + else: + text = app.user_ns['a'][0]['text/plain'] + latex = app.user_ns['a'][0]['text/latex'] + assert text in ("{pi: 3.14, n_i: 3}", + '{n\N{LATIN SUBSCRIPT SMALL LETTER I}: 3, \N{GREEK SMALL LETTER PI}: 3.14}', + "{n_i: 3, pi: 3.14}", + '{\N{GREEK SMALL LETTER PI}: 3.14, n\N{LATIN SUBSCRIPT SMALL LETTER I}: 3}') + assert latex == r'$\displaystyle \left\{ n_{i} : 3, \ \pi : 3.14\right\}$' + + # Objects with an _latex overload should also be handled by our tuple + # printer. + app.run_cell("""\ + class WithOverload: + def _latex(self, printer): + return r"\\LaTeX" + """) + app.run_cell("a = format((WithOverload(),))") + # Deal with API change starting at IPython 1.0 + if int(ipython.__version__.split(".")[0]) < 1: + latex = app.user_ns['a']['text/latex'] + else: + latex = app.user_ns['a'][0]['text/latex'] + assert latex == r'$\displaystyle \left( \LaTeX,\right)$' + + app.run_cell("inst.display_formatter.formatters['text/latex'].enabled = True") + app.run_cell("init_printing(use_latex=True, print_builtin=False)") + app.run_cell("a = format({Symbol('pi'): 3.14, Symbol('n_i'): 3})") + # Deal with API change starting at IPython 1.0 + if int(ipython.__version__.split(".")[0]) < 1: + text = app.user_ns['a']['text/plain'] + raises(KeyError, lambda: app.user_ns['a']['text/latex']) + else: + text = app.user_ns['a'][0]['text/plain'] + raises(KeyError, lambda: app.user_ns['a'][0]['text/latex']) + # Note : In Python 3 we have one text type: str which holds Unicode data + # and two byte types bytes and bytearray. + # Python 3.3.3 + IPython 0.13.2 gives: '{n_i: 3, pi: 3.14}' + # Python 3.3.3 + IPython 1.1.0 gives: '{n_i: 3, pi: 3.14}' + assert text in ("{pi: 3.14, n_i: 3}", "{n_i: 3, pi: 3.14}") + + +def test_builtin_containers(): + # Initialize and setup IPython session + app = init_ipython_session() + app.run_cell("ip = get_ipython()") + app.run_cell("inst = ip.instance()") + app.run_cell("format = inst.display_formatter.format") + app.run_cell("inst.display_formatter.formatters['text/latex'].enabled = True") + app.run_cell("from sympy import init_printing, Matrix") + app.run_cell('init_printing(use_latex=True, use_unicode=False)') + + # Make sure containers that shouldn't pretty print don't. + app.run_cell('a = format((True, False))') + app.run_cell('import sys') + app.run_cell('b = format(sys.flags)') + app.run_cell('c = format((Matrix([1, 2]),))') + # Deal with API change starting at IPython 1.0 + if int(ipython.__version__.split(".")[0]) < 1: + assert app.user_ns['a']['text/plain'] == '(True, False)' + assert 'text/latex' not in app.user_ns['a'] + assert app.user_ns['b']['text/plain'][:10] == 'sys.flags(' + assert 'text/latex' not in app.user_ns['b'] + assert app.user_ns['c']['text/plain'] == \ +"""\ + [1] \n\ +([ ],) + [2] \ +""" + assert app.user_ns['c']['text/latex'] == '$\\displaystyle \\left( \\left[\\begin{matrix}1\\\\2\\end{matrix}\\right],\\right)$' + else: + assert app.user_ns['a'][0]['text/plain'] == '(True, False)' + assert 'text/latex' not in app.user_ns['a'][0] + assert app.user_ns['b'][0]['text/plain'][:10] == 'sys.flags(' + assert 'text/latex' not in app.user_ns['b'][0] + assert app.user_ns['c'][0]['text/plain'] == \ +"""\ + [1] \n\ +([ ],) + [2] \ +""" + assert app.user_ns['c'][0]['text/latex'] == '$\\displaystyle \\left( \\left[\\begin{matrix}1\\\\2\\end{matrix}\\right],\\right)$' + +def test_matplotlib_bad_latex(): + # Initialize and setup IPython session + app = init_ipython_session() + app.run_cell("import IPython") + app.run_cell("ip = get_ipython()") + app.run_cell("inst = ip.instance()") + app.run_cell("format = inst.display_formatter.format") + app.run_cell("from sympy import init_printing, Matrix") + app.run_cell("init_printing(use_latex='matplotlib')") + + # The png formatter is not enabled by default in this context + app.run_cell("inst.display_formatter.formatters['image/png'].enabled = True") + + # Make sure no warnings are raised by IPython + app.run_cell("import warnings") + # IPython.core.formatters.FormatterWarning was introduced in IPython 2.0 + if int(ipython.__version__.split(".")[0]) < 2: + app.run_cell("warnings.simplefilter('error')") + else: + app.run_cell("warnings.simplefilter('error', IPython.core.formatters.FormatterWarning)") + + # This should not raise an exception + app.run_cell("a = format(Matrix([1, 2, 3]))") + + # issue 9799 + app.run_cell("from sympy import Piecewise, Symbol, Eq") + app.run_cell("x = Symbol('x'); pw = format(Piecewise((1, Eq(x, 0)), (0, True)))") + + +def test_override_repr_latex(): + # Initialize and setup IPython session + app = init_ipython_session() + app.run_cell("import IPython") + app.run_cell("ip = get_ipython()") + app.run_cell("inst = ip.instance()") + app.run_cell("format = inst.display_formatter.format") + app.run_cell("inst.display_formatter.formatters['text/latex'].enabled = True") + app.run_cell("from sympy import init_printing") + app.run_cell("from sympy import Symbol") + app.run_cell("init_printing(use_latex=True)") + app.run_cell("""\ + class SymbolWithOverload(Symbol): + def _repr_latex_(self): + return r"Hello " + super()._repr_latex_() + " world" + """) + app.run_cell("a = format(SymbolWithOverload('s'))") + + if int(ipython.__version__.split(".")[0]) < 1: + latex = app.user_ns['a']['text/latex'] + else: + latex = app.user_ns['a'][0]['text/latex'] + assert latex == r'Hello $\displaystyle s$ world' diff --git a/venv/lib/python3.11/site-packages/sympy/interactive/traversal.py b/venv/lib/python3.11/site-packages/sympy/interactive/traversal.py new file mode 100644 index 0000000000000000000000000000000000000000..1315ec4ef7868b666bb6b978b3d8b20442d100b0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/interactive/traversal.py @@ -0,0 +1,95 @@ +from sympy.core.basic import Basic +from sympy.printing import pprint + +import random + +def interactive_traversal(expr): + """Traverse a tree asking a user which branch to choose. """ + + RED, BRED = '\033[0;31m', '\033[1;31m' + GREEN, BGREEN = '\033[0;32m', '\033[1;32m' + YELLOW, BYELLOW = '\033[0;33m', '\033[1;33m' # noqa + BLUE, BBLUE = '\033[0;34m', '\033[1;34m' # noqa + MAGENTA, BMAGENTA = '\033[0;35m', '\033[1;35m'# noqa + CYAN, BCYAN = '\033[0;36m', '\033[1;36m' # noqa + END = '\033[0m' + + def cprint(*args): + print("".join(map(str, args)) + END) + + def _interactive_traversal(expr, stage): + if stage > 0: + print() + + cprint("Current expression (stage ", BYELLOW, stage, END, "):") + print(BCYAN) + pprint(expr) + print(END) + + if isinstance(expr, Basic): + if expr.is_Add: + args = expr.as_ordered_terms() + elif expr.is_Mul: + args = expr.as_ordered_factors() + else: + args = expr.args + elif hasattr(expr, "__iter__"): + args = list(expr) + else: + return expr + + n_args = len(args) + + if not n_args: + return expr + + for i, arg in enumerate(args): + cprint(GREEN, "[", BGREEN, i, GREEN, "] ", BLUE, type(arg), END) + pprint(arg) + print() + + if n_args == 1: + choices = '0' + else: + choices = '0-%d' % (n_args - 1) + + try: + choice = input("Your choice [%s,f,l,r,d,?]: " % choices) + except EOFError: + result = expr + print() + else: + if choice == '?': + cprint(RED, "%s - select subexpression with the given index" % + choices) + cprint(RED, "f - select the first subexpression") + cprint(RED, "l - select the last subexpression") + cprint(RED, "r - select a random subexpression") + cprint(RED, "d - done\n") + + result = _interactive_traversal(expr, stage) + elif choice in ('d', ''): + result = expr + elif choice == 'f': + result = _interactive_traversal(args[0], stage + 1) + elif choice == 'l': + result = _interactive_traversal(args[-1], stage + 1) + elif choice == 'r': + result = _interactive_traversal(random.choice(args), stage + 1) + else: + try: + choice = int(choice) + except ValueError: + cprint(BRED, + "Choice must be a number in %s range\n" % choices) + result = _interactive_traversal(expr, stage) + else: + if choice < 0 or choice >= n_args: + cprint(BRED, "Choice must be in %s range\n" % choices) + result = _interactive_traversal(expr, stage) + else: + result = _interactive_traversal(args[choice], stage + 1) + + return result + + return _interactive_traversal(expr, 0) diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/__init__.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d023d86f2c6f0c64d7ac460c50eedc355e78b21f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/__init__.py @@ -0,0 +1,3 @@ +from sympy.liealgebras.cartan_type import CartanType + +__all__ = ['CartanType'] diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/cartan_matrix.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/cartan_matrix.py new file mode 100644 index 0000000000000000000000000000000000000000..2d29b37bc9a1a26790ee88b5902951afe4fc4560 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/cartan_matrix.py @@ -0,0 +1,25 @@ +from .cartan_type import CartanType + +def CartanMatrix(ct): + """Access the Cartan matrix of a specific Lie algebra + + Examples + ======== + + >>> from sympy.liealgebras.cartan_matrix import CartanMatrix + >>> CartanMatrix("A2") + Matrix([ + [ 2, -1], + [-1, 2]]) + + >>> CartanMatrix(['C', 3]) + Matrix([ + [ 2, -1, 0], + [-1, 2, -1], + [ 0, -2, 2]]) + + This method works by returning the Cartan matrix + which corresponds to Cartan type t. + """ + + return CartanType(ct).cartan_matrix() diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/cartan_type.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/cartan_type.py new file mode 100644 index 0000000000000000000000000000000000000000..16bb152469238ea912a30c2d0f8210d6f729bdb1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/cartan_type.py @@ -0,0 +1,73 @@ +from sympy.core import Atom, Basic + + +class CartanType_generator(): + """ + Constructor for actually creating things + """ + def __call__(self, *args): + c = args[0] + if isinstance(c, list): + letter, n = c[0], int(c[1]) + elif isinstance(c, str): + letter, n = c[0], int(c[1:]) + else: + raise TypeError("Argument must be a string (e.g. 'A3') or a list (e.g. ['A', 3])") + + if n < 0: + raise ValueError("Lie algebra rank cannot be negative") + if letter == "A": + from . import type_a + return type_a.TypeA(n) + if letter == "B": + from . import type_b + return type_b.TypeB(n) + + if letter == "C": + from . import type_c + return type_c.TypeC(n) + + if letter == "D": + from . import type_d + return type_d.TypeD(n) + + if letter == "E": + if n >= 6 and n <= 8: + from . import type_e + return type_e.TypeE(n) + + if letter == "F": + if n == 4: + from . import type_f + return type_f.TypeF(n) + + if letter == "G": + if n == 2: + from . import type_g + return type_g.TypeG(n) + +CartanType = CartanType_generator() + + +class Standard_Cartan(Atom): + """ + Concrete base class for Cartan types such as A4, etc + """ + + def __new__(cls, series, n): + obj = Basic.__new__(cls) + obj.n = n + obj.series = series + return obj + + def rank(self): + """ + Returns the rank of the Lie algebra + """ + return self.n + + def series(self): + """ + Returns the type of the Lie algebra + """ + return self.series diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/dynkin_diagram.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/dynkin_diagram.py new file mode 100644 index 0000000000000000000000000000000000000000..cc9e2dac4d54490b803eeaf9637cb9b66b01f058 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/dynkin_diagram.py @@ -0,0 +1,24 @@ +from .cartan_type import CartanType + + +def DynkinDiagram(t): + """Display the Dynkin diagram of a given Lie algebra + + Works by generating the CartanType for the input, t, and then returning the + Dynkin diagram method from the individual classes. + + Examples + ======== + + >>> from sympy.liealgebras.dynkin_diagram import DynkinDiagram + >>> print(DynkinDiagram("A3")) + 0---0---0 + 1 2 3 + + >>> print(DynkinDiagram("B4")) + 0---0---0=>=0 + 1 2 3 4 + + """ + + return CartanType(t).dynkin_diagram() diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/root_system.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/root_system.py new file mode 100644 index 0000000000000000000000000000000000000000..36eb24605e78bbdc669736910d89be5606df1389 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/root_system.py @@ -0,0 +1,196 @@ +from .cartan_type import CartanType +from sympy.core.basic import Atom + +class RootSystem(Atom): + """Represent the root system of a simple Lie algebra + + Every simple Lie algebra has a unique root system. To find the root + system, we first consider the Cartan subalgebra of g, which is the maximal + abelian subalgebra, and consider the adjoint action of g on this + subalgebra. There is a root system associated with this action. Now, a + root system over a vector space V is a set of finite vectors Phi (called + roots), which satisfy: + + 1. The roots span V + 2. The only scalar multiples of x in Phi are x and -x + 3. For every x in Phi, the set Phi is closed under reflection + through the hyperplane perpendicular to x. + 4. If x and y are roots in Phi, then the projection of y onto + the line through x is a half-integral multiple of x. + + Now, there is a subset of Phi, which we will call Delta, such that: + 1. Delta is a basis of V + 2. Each root x in Phi can be written x = sum k_y y for y in Delta + + The elements of Delta are called the simple roots. + Therefore, we see that the simple roots span the root space of a given + simple Lie algebra. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Root_system + .. [2] Lie Algebras and Representation Theory - Humphreys + + """ + + def __new__(cls, cartantype): + """Create a new RootSystem object + + This method assigns an attribute called cartan_type to each instance of + a RootSystem object. When an instance of RootSystem is called, it + needs an argument, which should be an instance of a simple Lie algebra. + We then take the CartanType of this argument and set it as the + cartan_type attribute of the RootSystem instance. + + """ + obj = Atom.__new__(cls) + obj.cartan_type = CartanType(cartantype) + return obj + + def simple_roots(self): + """Generate the simple roots of the Lie algebra + + The rank of the Lie algebra determines the number of simple roots that + it has. This method obtains the rank of the Lie algebra, and then uses + the simple_root method from the Lie algebra classes to generate all the + simple roots. + + Examples + ======== + + >>> from sympy.liealgebras.root_system import RootSystem + >>> c = RootSystem("A3") + >>> roots = c.simple_roots() + >>> roots + {1: [1, -1, 0, 0], 2: [0, 1, -1, 0], 3: [0, 0, 1, -1]} + + """ + n = self.cartan_type.rank() + roots = {i: self.cartan_type.simple_root(i) for i in range(1, n+1)} + return roots + + + def all_roots(self): + """Generate all the roots of a given root system + + The result is a dictionary where the keys are integer numbers. It + generates the roots by getting the dictionary of all positive roots + from the bases classes, and then taking each root, and multiplying it + by -1 and adding it to the dictionary. In this way all the negative + roots are generated. + + """ + alpha = self.cartan_type.positive_roots() + keys = list(alpha.keys()) + k = max(keys) + for val in keys: + k += 1 + root = alpha[val] + newroot = [-x for x in root] + alpha[k] = newroot + return alpha + + def root_space(self): + """Return the span of the simple roots + + The root space is the vector space spanned by the simple roots, i.e. it + is a vector space with a distinguished basis, the simple roots. This + method returns a string that represents the root space as the span of + the simple roots, alpha[1],...., alpha[n]. + + Examples + ======== + + >>> from sympy.liealgebras.root_system import RootSystem + >>> c = RootSystem("A3") + >>> c.root_space() + 'alpha[1] + alpha[2] + alpha[3]' + + """ + n = self.cartan_type.rank() + rs = " + ".join("alpha["+str(i) +"]" for i in range(1, n+1)) + return rs + + def add_simple_roots(self, root1, root2): + """Add two simple roots together + + The function takes as input two integers, root1 and root2. It then + uses these integers as keys in the dictionary of simple roots, and gets + the corresponding simple roots, and then adds them together. + + Examples + ======== + + >>> from sympy.liealgebras.root_system import RootSystem + >>> c = RootSystem("A3") + >>> newroot = c.add_simple_roots(1, 2) + >>> newroot + [1, 0, -1, 0] + + """ + + alpha = self.simple_roots() + if root1 > len(alpha) or root2 > len(alpha): + raise ValueError("You've used a root that doesn't exist!") + a1 = alpha[root1] + a2 = alpha[root2] + newroot = [_a1 + _a2 for _a1, _a2 in zip(a1, a2)] + return newroot + + def add_as_roots(self, root1, root2): + """Add two roots together if and only if their sum is also a root + + It takes as input two vectors which should be roots. It then computes + their sum and checks if it is in the list of all possible roots. If it + is, it returns the sum. Otherwise it returns a string saying that the + sum is not a root. + + Examples + ======== + + >>> from sympy.liealgebras.root_system import RootSystem + >>> c = RootSystem("A3") + >>> c.add_as_roots([1, 0, -1, 0], [0, 0, 1, -1]) + [1, 0, 0, -1] + >>> c.add_as_roots([1, -1, 0, 0], [0, 0, -1, 1]) + 'The sum of these two roots is not a root' + + """ + alpha = self.all_roots() + newroot = [r1 + r2 for r1, r2 in zip(root1, root2)] + if newroot in alpha.values(): + return newroot + else: + return "The sum of these two roots is not a root" + + + def cartan_matrix(self): + """Cartan matrix of Lie algebra associated with this root system + + Examples + ======== + + >>> from sympy.liealgebras.root_system import RootSystem + >>> c = RootSystem("A3") + >>> c.cartan_matrix() + Matrix([ + [ 2, -1, 0], + [-1, 2, -1], + [ 0, -1, 2]]) + """ + return self.cartan_type.cartan_matrix() + + def dynkin_diagram(self): + """Dynkin diagram of the Lie algebra associated with this root system + + Examples + ======== + + >>> from sympy.liealgebras.root_system import RootSystem + >>> c = RootSystem("A3") + >>> print(c.dynkin_diagram()) + 0---0---0 + 1 2 3 + """ + return self.cartan_type.dynkin_diagram() diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_cartan_matrix.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_cartan_matrix.py new file mode 100644 index 0000000000000000000000000000000000000000..98b1793dee63e0e87c610768554a8388dfd641a1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_cartan_matrix.py @@ -0,0 +1,10 @@ +from sympy.liealgebras.cartan_matrix import CartanMatrix +from sympy.matrices import Matrix + +def test_CartanMatrix(): + c = CartanMatrix("A3") + m = Matrix(3, 3, [2, -1, 0, -1, 2, -1, 0, -1, 2]) + assert c == m + a = CartanMatrix(["G",2]) + mt = Matrix(2, 2, [2, -1, -3, 2]) + assert a == mt diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_cartan_type.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_cartan_type.py new file mode 100644 index 0000000000000000000000000000000000000000..257eeca41d0f5f2eb240cc270f76d452848ed405 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_cartan_type.py @@ -0,0 +1,12 @@ +from sympy.liealgebras.cartan_type import CartanType, Standard_Cartan + +def test_Standard_Cartan(): + c = CartanType("A4") + assert c.rank() == 4 + assert c.series == "A" + m = Standard_Cartan("A", 2) + assert m.rank() == 2 + assert m.series == "A" + b = CartanType("B12") + assert b.rank() == 12 + assert b.series == "B" diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_dynkin_diagram.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_dynkin_diagram.py new file mode 100644 index 0000000000000000000000000000000000000000..ad2ee4c162945c437ecf83d75c7fef9455c9464a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_dynkin_diagram.py @@ -0,0 +1,9 @@ +from sympy.liealgebras.dynkin_diagram import DynkinDiagram + +def test_DynkinDiagram(): + c = DynkinDiagram("A3") + diag = "0---0---0\n1 2 3" + assert c == diag + ct = DynkinDiagram(["B", 3]) + diag2 = "0---0=>=0\n1 2 3" + assert ct == diag2 diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_root_system.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_root_system.py new file mode 100644 index 0000000000000000000000000000000000000000..42110da5a1c59a7e6b2e537ee13746bfce361579 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_root_system.py @@ -0,0 +1,18 @@ +from sympy.liealgebras.root_system import RootSystem +from sympy.liealgebras.type_a import TypeA +from sympy.matrices import Matrix + +def test_root_system(): + c = RootSystem("A3") + assert c.cartan_type == TypeA(3) + assert c.simple_roots() == {1: [1, -1, 0, 0], 2: [0, 1, -1, 0], 3: [0, 0, 1, -1]} + assert c.root_space() == "alpha[1] + alpha[2] + alpha[3]" + assert c.cartan_matrix() == Matrix([[ 2, -1, 0], [-1, 2, -1], [ 0, -1, 2]]) + assert c.dynkin_diagram() == "0---0---0\n1 2 3" + assert c.add_simple_roots(1, 2) == [1, 0, -1, 0] + assert c.all_roots() == {1: [1, -1, 0, 0], 2: [1, 0, -1, 0], + 3: [1, 0, 0, -1], 4: [0, 1, -1, 0], 5: [0, 1, 0, -1], + 6: [0, 0, 1, -1], 7: [-1, 1, 0, 0], 8: [-1, 0, 1, 0], + 9: [-1, 0, 0, 1], 10: [0, -1, 1, 0], + 11: [0, -1, 0, 1], 12: [0, 0, -1, 1]} + assert c.add_as_roots([1, 0, -1, 0], [0, 0, 1, -1]) == [1, 0, 0, -1] diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_A.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_A.py new file mode 100644 index 0000000000000000000000000000000000000000..85d6f451ee167cf6db17ab20e59efab86ac0b691 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_A.py @@ -0,0 +1,17 @@ +from sympy.liealgebras.cartan_type import CartanType +from sympy.matrices import Matrix + +def test_type_A(): + c = CartanType("A3") + m = Matrix(3, 3, [2, -1, 0, -1, 2, -1, 0, -1, 2]) + assert m == c.cartan_matrix() + assert c.basis() == 8 + assert c.roots() == 12 + assert c.dimension() == 4 + assert c.simple_root(1) == [1, -1, 0, 0] + assert c.highest_root() == [1, 0, 0, -1] + assert c.lie_algebra() == "su(4)" + diag = "0---0---0\n1 2 3" + assert c.dynkin_diagram() == diag + assert c.positive_roots() == {1: [1, -1, 0, 0], 2: [1, 0, -1, 0], + 3: [1, 0, 0, -1], 4: [0, 1, -1, 0], 5: [0, 1, 0, -1], 6: [0, 0, 1, -1]} diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_B.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_B.py new file mode 100644 index 0000000000000000000000000000000000000000..8f2a9011f96bc647e48d39e16cf10703a99d86b3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_B.py @@ -0,0 +1,17 @@ +from sympy.liealgebras.cartan_type import CartanType +from sympy.matrices import Matrix + +def test_type_B(): + c = CartanType("B3") + m = Matrix(3, 3, [2, -1, 0, -1, 2, -2, 0, -1, 2]) + assert m == c.cartan_matrix() + assert c.dimension() == 3 + assert c.roots() == 18 + assert c.simple_root(3) == [0, 0, 1] + assert c.basis() == 3 + assert c.lie_algebra() == "so(6)" + diag = "0---0=>=0\n1 2 3" + assert c.dynkin_diagram() == diag + assert c.positive_roots() == {1: [1, -1, 0], 2: [1, 1, 0], 3: [1, 0, -1], + 4: [1, 0, 1], 5: [0, 1, -1], 6: [0, 1, 1], 7: [1, 0, 0], + 8: [0, 1, 0], 9: [0, 0, 1]} diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_C.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_C.py new file mode 100644 index 0000000000000000000000000000000000000000..8154c201e6c50adb7c74458b240ed98b9a0dd123 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_C.py @@ -0,0 +1,22 @@ +from sympy.liealgebras.cartan_type import CartanType +from sympy.matrices import Matrix + +def test_type_C(): + c = CartanType("C4") + m = Matrix(4, 4, [2, -1, 0, 0, -1, 2, -1, 0, 0, -1, 2, -1, 0, 0, -2, 2]) + assert c.cartan_matrix() == m + assert c.dimension() == 4 + assert c.simple_root(4) == [0, 0, 0, 2] + assert c.roots() == 32 + assert c.basis() == 36 + assert c.lie_algebra() == "sp(8)" + t = CartanType(['C', 3]) + assert t.dimension() == 3 + diag = "0---0---0=<=0\n1 2 3 4" + assert c.dynkin_diagram() == diag + assert c.positive_roots() == {1: [1, -1, 0, 0], 2: [1, 1, 0, 0], + 3: [1, 0, -1, 0], 4: [1, 0, 1, 0], 5: [1, 0, 0, -1], + 6: [1, 0, 0, 1], 7: [0, 1, -1, 0], 8: [0, 1, 1, 0], + 9: [0, 1, 0, -1], 10: [0, 1, 0, 1], 11: [0, 0, 1, -1], + 12: [0, 0, 1, 1], 13: [2, 0, 0, 0], 14: [0, 2, 0, 0], 15: [0, 0, 2, 0], + 16: [0, 0, 0, 2]} diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_D.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_D.py new file mode 100644 index 0000000000000000000000000000000000000000..ddf6a34cb5be475cc30042e95bf8eae2376a2223 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_D.py @@ -0,0 +1,19 @@ +from sympy.liealgebras.cartan_type import CartanType +from sympy.matrices import Matrix + + + +def test_type_D(): + c = CartanType("D4") + m = Matrix(4, 4, [2, -1, 0, 0, -1, 2, -1, -1, 0, -1, 2, 0, 0, -1, 0, 2]) + assert c.cartan_matrix() == m + assert c.basis() == 6 + assert c.lie_algebra() == "so(8)" + assert c.roots() == 24 + assert c.simple_root(3) == [0, 0, 1, -1] + diag = " 3\n 0\n |\n |\n0---0---0\n1 2 4" + assert diag == c.dynkin_diagram() + assert c.positive_roots() == {1: [1, -1, 0, 0], 2: [1, 1, 0, 0], + 3: [1, 0, -1, 0], 4: [1, 0, 1, 0], 5: [1, 0, 0, -1], 6: [1, 0, 0, 1], + 7: [0, 1, -1, 0], 8: [0, 1, 1, 0], 9: [0, 1, 0, -1], 10: [0, 1, 0, 1], + 11: [0, 0, 1, -1], 12: [0, 0, 1, 1]} diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_E.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_E.py new file mode 100644 index 0000000000000000000000000000000000000000..bdb08342f41ede3390f34e9b297864eda16bedc7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_E.py @@ -0,0 +1,22 @@ +from sympy.liealgebras.cartan_type import CartanType +from sympy.matrices import Matrix +from sympy.core.backend import Rational + +def test_type_E(): + c = CartanType("E6") + m = Matrix(6, 6, [2, 0, -1, 0, 0, 0, 0, 2, 0, -1, 0, 0, + -1, 0, 2, -1, 0, 0, 0, -1, -1, 2, -1, 0, 0, 0, 0, + -1, 2, -1, 0, 0, 0, 0, -1, 2]) + assert c.cartan_matrix() == m + assert c.dimension() == 8 + assert c.simple_root(6) == [0, 0, 0, -1, 1, 0, 0, 0] + assert c.roots() == 72 + assert c.basis() == 78 + diag = " "*8 + "2\n" + " "*8 + "0\n" + " "*8 + "|\n" + " "*8 + "|\n" + diag += "---".join("0" for i in range(1, 6))+"\n" + diag += "1 " + " ".join(str(i) for i in range(3, 7)) + assert c.dynkin_diagram() == diag + posroots = c.positive_roots() + assert posroots[8] == [1, 0, 0, 0, 1, 0, 0, 0] + assert posroots[21] == [Rational(1,2),Rational(1,2),Rational(1,2),Rational(1,2), + Rational(1,2),Rational(-1,2),Rational(-1,2),Rational(1,2)] diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_F.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_F.py new file mode 100644 index 0000000000000000000000000000000000000000..fbb58223d0b5886e6044108c9c5cc3bbf371dd14 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_F.py @@ -0,0 +1,24 @@ +from sympy.liealgebras.cartan_type import CartanType +from sympy.matrices import Matrix +from sympy.core.backend import S + +def test_type_F(): + c = CartanType("F4") + m = Matrix(4, 4, [2, -1, 0, 0, -1, 2, -2, 0, 0, -1, 2, -1, 0, 0, -1, 2]) + assert c.cartan_matrix() == m + assert c.dimension() == 4 + assert c.simple_root(1) == [1, -1, 0, 0] + assert c.simple_root(2) == [0, 1, -1, 0] + assert c.simple_root(3) == [0, 0, 0, 1] + assert c.simple_root(4) == [-S.Half, -S.Half, -S.Half, -S.Half] + assert c.roots() == 48 + assert c.basis() == 52 + diag = "0---0=>=0---0\n" + " ".join(str(i) for i in range(1, 5)) + assert c.dynkin_diagram() == diag + assert c.positive_roots() == {1: [1, -1, 0, 0], 2: [1, 1, 0, 0], 3: [1, 0, -1, 0], + 4: [1, 0, 1, 0], 5: [1, 0, 0, -1], 6: [1, 0, 0, 1], 7: [0, 1, -1, 0], + 8: [0, 1, 1, 0], 9: [0, 1, 0, -1], 10: [0, 1, 0, 1], 11: [0, 0, 1, -1], + 12: [0, 0, 1, 1], 13: [1, 0, 0, 0], 14: [0, 1, 0, 0], 15: [0, 0, 1, 0], + 16: [0, 0, 0, 1], 17: [S.Half, S.Half, S.Half, S.Half], 18: [S.Half, -S.Half, S.Half, S.Half], + 19: [S.Half, S.Half, -S.Half, S.Half], 20: [S.Half, S.Half, S.Half, -S.Half], 21: [S.Half, S.Half, -S.Half, -S.Half], + 22: [S.Half, -S.Half, S.Half, -S.Half], 23: [S.Half, -S.Half, -S.Half, S.Half], 24: [S.Half, -S.Half, -S.Half, -S.Half]} diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_G.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_G.py new file mode 100644 index 0000000000000000000000000000000000000000..c427eeb85bad8fc77d17a1563a7b796d4e0f217f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_type_G.py @@ -0,0 +1,16 @@ +# coding=utf-8 +from sympy.liealgebras.cartan_type import CartanType +from sympy.matrices import Matrix + +def test_type_G(): + c = CartanType("G2") + m = Matrix(2, 2, [2, -1, -3, 2]) + assert c.cartan_matrix() == m + assert c.simple_root(2) == [1, -2, 1] + assert c.basis() == 14 + assert c.roots() == 12 + assert c.dimension() == 3 + diag = "0≡<≡0\n1 2" + assert diag == c.dynkin_diagram() + assert c.positive_roots() == {1: [0, 1, -1], 2: [1, -2, 1], 3: [1, -1, 0], + 4: [1, 0, 1], 5: [1, 1, -2], 6: [2, -1, -1]} diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_weyl_group.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_weyl_group.py new file mode 100644 index 0000000000000000000000000000000000000000..e4e57246fdcb5a431d8bbd65f1f60e0254a9cdf0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/tests/test_weyl_group.py @@ -0,0 +1,35 @@ +from sympy.liealgebras.weyl_group import WeylGroup +from sympy.matrices import Matrix + +def test_weyl_group(): + c = WeylGroup("A3") + assert c.matrix_form('r1*r2') == Matrix([[0, 0, 1, 0], [1, 0, 0, 0], + [0, 1, 0, 0], [0, 0, 0, 1]]) + assert c.generators() == ['r1', 'r2', 'r3'] + assert c.group_order() == 24.0 + assert c.group_name() == "S4: the symmetric group acting on 4 elements." + assert c.coxeter_diagram() == "0---0---0\n1 2 3" + assert c.element_order('r1*r2*r3') == 4 + assert c.element_order('r1*r3*r2*r3') == 3 + d = WeylGroup("B5") + assert d.group_order() == 3840 + assert d.element_order('r1*r2*r4*r5') == 12 + assert d.matrix_form('r2*r3') == Matrix([[0, 0, 1, 0, 0], [1, 0, 0, 0, 0], + [0, 1, 0, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 1]]) + assert d.element_order('r1*r2*r1*r3*r5') == 6 + e = WeylGroup("D5") + assert e.element_order('r2*r3*r5') == 4 + assert e.matrix_form('r2*r3*r5') == Matrix([[1, 0, 0, 0, 0], [0, 0, 0, 0, -1], + [0, 1, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, -1, 0]]) + f = WeylGroup("G2") + assert f.element_order('r1*r2*r1*r2') == 3 + assert f.element_order('r2*r1*r1*r2') == 1 + + assert f.matrix_form('r1*r2*r1*r2') == Matrix([[0, 1, 0], [0, 0, 1], [1, 0, 0]]) + g = WeylGroup("F4") + assert g.matrix_form('r2*r3') == Matrix([[1, 0, 0, 0], [0, 1, 0, 0], + [0, 0, 0, -1], [0, 0, 1, 0]]) + + assert g.element_order('r2*r3') == 4 + h = WeylGroup("E6") + assert h.group_order() == 51840 diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/type_a.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/type_a.py new file mode 100644 index 0000000000000000000000000000000000000000..96dc615366ae20d668d651620ac088f15751c50e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/type_a.py @@ -0,0 +1,164 @@ +from sympy.liealgebras.cartan_type import Standard_Cartan +from sympy.core.backend import eye + + +class TypeA(Standard_Cartan): + """ + This class contains the information about + the A series of simple Lie algebras. + ==== + """ + + def __new__(cls, n): + if n < 1: + raise ValueError("n cannot be less than 1") + return Standard_Cartan.__new__(cls, "A", n) + + + def dimension(self): + """Dimension of the vector space V underlying the Lie algebra + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("A4") + >>> c.dimension() + 5 + """ + return self.n+1 + + + def basic_root(self, i, j): + """ + This is a method just to generate roots + with a 1 iin the ith position and a -1 + in the jth position. + + """ + + n = self.n + root = [0]*(n+1) + root[i] = 1 + root[j] = -1 + return root + + def simple_root(self, i): + """ + Every lie algebra has a unique root system. + Given a root system Q, there is a subset of the + roots such that an element of Q is called a + simple root if it cannot be written as the sum + of two elements in Q. If we let D denote the + set of simple roots, then it is clear that every + element of Q can be written as a linear combination + of elements of D with all coefficients non-negative. + + In A_n the ith simple root is the root which has a 1 + in the ith position, a -1 in the (i+1)th position, + and zeroes elsewhere. + + This method returns the ith simple root for the A series. + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("A4") + >>> c.simple_root(1) + [1, -1, 0, 0, 0] + + """ + + return self.basic_root(i-1, i) + + def positive_roots(self): + """ + This method generates all the positive roots of + A_n. This is half of all of the roots of A_n; + by multiplying all the positive roots by -1 we + get the negative roots. + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("A3") + >>> c.positive_roots() + {1: [1, -1, 0, 0], 2: [1, 0, -1, 0], 3: [1, 0, 0, -1], 4: [0, 1, -1, 0], + 5: [0, 1, 0, -1], 6: [0, 0, 1, -1]} + """ + + n = self.n + posroots = {} + k = 0 + for i in range(0, n): + for j in range(i+1, n+1): + k += 1 + posroots[k] = self.basic_root(i, j) + return posroots + + def highest_root(self): + """ + Returns the highest weight root for A_n + """ + + return self.basic_root(0, self.n) + + def roots(self): + """ + Returns the total number of roots for A_n + """ + n = self.n + return n*(n+1) + + def cartan_matrix(self): + """ + Returns the Cartan matrix for A_n. + The Cartan matrix matrix for a Lie algebra is + generated by assigning an ordering to the simple + roots, (alpha[1], ...., alpha[l]). Then the ijth + entry of the Cartan matrix is (). + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType('A4') + >>> c.cartan_matrix() + Matrix([ + [ 2, -1, 0, 0], + [-1, 2, -1, 0], + [ 0, -1, 2, -1], + [ 0, 0, -1, 2]]) + + """ + + n = self.n + m = 2 * eye(n) + for i in range(1, n - 1): + m[i, i+1] = -1 + m[i, i-1] = -1 + m[0,1] = -1 + m[n-1, n-2] = -1 + return m + + def basis(self): + """ + Returns the number of independent generators of A_n + """ + n = self.n + return n**2 - 1 + + def lie_algebra(self): + """ + Returns the Lie algebra associated with A_n + """ + n = self.n + return "su(" + str(n + 1) + ")" + + def dynkin_diagram(self): + n = self.n + diag = "---".join("0" for i in range(1, n+1)) + "\n" + diag += " ".join(str(i) for i in range(1, n+1)) + return diag diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/type_b.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/type_b.py new file mode 100644 index 0000000000000000000000000000000000000000..c6ee85502261f4702769067c64021521a2bc1725 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/type_b.py @@ -0,0 +1,170 @@ +from .cartan_type import Standard_Cartan +from sympy.core.backend import eye + +class TypeB(Standard_Cartan): + + def __new__(cls, n): + if n < 2: + raise ValueError("n cannot be less than 2") + return Standard_Cartan.__new__(cls, "B", n) + + def dimension(self): + """Dimension of the vector space V underlying the Lie algebra + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("B3") + >>> c.dimension() + 3 + """ + + return self.n + + def basic_root(self, i, j): + """ + This is a method just to generate roots + with a 1 iin the ith position and a -1 + in the jth position. + + """ + root = [0]*self.n + root[i] = 1 + root[j] = -1 + return root + + def simple_root(self, i): + """ + Every lie algebra has a unique root system. + Given a root system Q, there is a subset of the + roots such that an element of Q is called a + simple root if it cannot be written as the sum + of two elements in Q. If we let D denote the + set of simple roots, then it is clear that every + element of Q can be written as a linear combination + of elements of D with all coefficients non-negative. + + In B_n the first n-1 simple roots are the same as the + roots in A_(n-1) (a 1 in the ith position, a -1 in + the (i+1)th position, and zeroes elsewhere). The n-th + simple root is the root with a 1 in the nth position + and zeroes elsewhere. + + This method returns the ith simple root for the B series. + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("B3") + >>> c.simple_root(2) + [0, 1, -1] + + """ + n = self.n + if i < n: + return self.basic_root(i-1, i) + else: + root = [0]*self.n + root[n-1] = 1 + return root + + def positive_roots(self): + """ + This method generates all the positive roots of + A_n. This is half of all of the roots of B_n; + by multiplying all the positive roots by -1 we + get the negative roots. + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("A3") + >>> c.positive_roots() + {1: [1, -1, 0, 0], 2: [1, 0, -1, 0], 3: [1, 0, 0, -1], 4: [0, 1, -1, 0], + 5: [0, 1, 0, -1], 6: [0, 0, 1, -1]} + """ + + n = self.n + posroots = {} + k = 0 + for i in range(0, n-1): + for j in range(i+1, n): + k += 1 + posroots[k] = self.basic_root(i, j) + k += 1 + root = self.basic_root(i, j) + root[j] = 1 + posroots[k] = root + + for i in range(0, n): + k += 1 + root = [0]*n + root[i] = 1 + posroots[k] = root + + return posroots + + def roots(self): + """ + Returns the total number of roots for B_n" + """ + + n = self.n + return 2*(n**2) + + def cartan_matrix(self): + """ + Returns the Cartan matrix for B_n. + The Cartan matrix matrix for a Lie algebra is + generated by assigning an ordering to the simple + roots, (alpha[1], ...., alpha[l]). Then the ijth + entry of the Cartan matrix is (). + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType('B4') + >>> c.cartan_matrix() + Matrix([ + [ 2, -1, 0, 0], + [-1, 2, -1, 0], + [ 0, -1, 2, -2], + [ 0, 0, -1, 2]]) + + """ + + n = self.n + m = 2* eye(n) + for i in range(1, n - 1): + m[i, i+1] = -1 + m[i, i-1] = -1 + m[0, 1] = -1 + m[n-2, n-1] = -2 + m[n-1, n-2] = -1 + return m + + def basis(self): + """ + Returns the number of independent generators of B_n + """ + + n = self.n + return (n**2 - n)/2 + + def lie_algebra(self): + """ + Returns the Lie algebra associated with B_n + """ + + n = self.n + return "so(" + str(2*n) + ")" + + def dynkin_diagram(self): + n = self.n + diag = "---".join("0" for i in range(1, n)) + "=>=0\n" + diag += " ".join(str(i) for i in range(1, n+1)) + return diag diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/type_c.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/type_c.py new file mode 100644 index 0000000000000000000000000000000000000000..615bb900b5ba9613fd02e43f476d34eef0d5d35c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/type_c.py @@ -0,0 +1,169 @@ +from .cartan_type import Standard_Cartan +from sympy.core.backend import eye + +class TypeC(Standard_Cartan): + + def __new__(cls, n): + if n < 3: + raise ValueError("n cannot be less than 3") + return Standard_Cartan.__new__(cls, "C", n) + + + def dimension(self): + """Dimension of the vector space V underlying the Lie algebra + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("C3") + >>> c.dimension() + 3 + """ + n = self.n + return n + + def basic_root(self, i, j): + """Generate roots with 1 in ith position and a -1 in jth position + """ + n = self.n + root = [0]*n + root[i] = 1 + root[j] = -1 + return root + + def simple_root(self, i): + """The ith simple root for the C series + + Every lie algebra has a unique root system. + Given a root system Q, there is a subset of the + roots such that an element of Q is called a + simple root if it cannot be written as the sum + of two elements in Q. If we let D denote the + set of simple roots, then it is clear that every + element of Q can be written as a linear combination + of elements of D with all coefficients non-negative. + + In C_n, the first n-1 simple roots are the same as + the roots in A_(n-1) (a 1 in the ith position, a -1 + in the (i+1)th position, and zeroes elsewhere). The + nth simple root is the root in which there is a 2 in + the nth position and zeroes elsewhere. + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("C3") + >>> c.simple_root(2) + [0, 1, -1] + + """ + + n = self.n + if i < n: + return self.basic_root(i-1,i) + else: + root = [0]*self.n + root[n-1] = 2 + return root + + + def positive_roots(self): + """Generates all the positive roots of A_n + + This is half of all of the roots of C_n; by multiplying all the + positive roots by -1 we get the negative roots. + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("A3") + >>> c.positive_roots() + {1: [1, -1, 0, 0], 2: [1, 0, -1, 0], 3: [1, 0, 0, -1], 4: [0, 1, -1, 0], + 5: [0, 1, 0, -1], 6: [0, 0, 1, -1]} + + """ + + n = self.n + posroots = {} + k = 0 + for i in range(0, n-1): + for j in range(i+1, n): + k += 1 + posroots[k] = self.basic_root(i, j) + k += 1 + root = self.basic_root(i, j) + root[j] = 1 + posroots[k] = root + + for i in range(0, n): + k += 1 + root = [0]*n + root[i] = 2 + posroots[k] = root + + return posroots + + def roots(self): + """ + Returns the total number of roots for C_n" + """ + + n = self.n + return 2*(n**2) + + def cartan_matrix(self): + """The Cartan matrix for C_n + + The Cartan matrix matrix for a Lie algebra is + generated by assigning an ordering to the simple + roots, (alpha[1], ...., alpha[l]). Then the ijth + entry of the Cartan matrix is (). + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType('C4') + >>> c.cartan_matrix() + Matrix([ + [ 2, -1, 0, 0], + [-1, 2, -1, 0], + [ 0, -1, 2, -1], + [ 0, 0, -2, 2]]) + + """ + + n = self.n + m = 2 * eye(n) + for i in range(1, n - 1): + m[i, i+1] = -1 + m[i, i-1] = -1 + m[0,1] = -1 + m[n-1, n-2] = -2 + return m + + + def basis(self): + """ + Returns the number of independent generators of C_n + """ + + n = self.n + return n*(2*n + 1) + + def lie_algebra(self): + """ + Returns the Lie algebra associated with C_n" + """ + + n = self.n + return "sp(" + str(2*n) + ")" + + def dynkin_diagram(self): + n = self.n + diag = "---".join("0" for i in range(1, n)) + "=<=0\n" + diag += " ".join(str(i) for i in range(1, n+1)) + return diag diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/type_d.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/type_d.py new file mode 100644 index 0000000000000000000000000000000000000000..9450d76e906c79e23db0ce223ed0de03d71c1199 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/type_d.py @@ -0,0 +1,173 @@ +from .cartan_type import Standard_Cartan +from sympy.core.backend import eye + +class TypeD(Standard_Cartan): + + def __new__(cls, n): + if n < 3: + raise ValueError("n cannot be less than 3") + return Standard_Cartan.__new__(cls, "D", n) + + + def dimension(self): + """Dmension of the vector space V underlying the Lie algebra + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("D4") + >>> c.dimension() + 4 + """ + + return self.n + + def basic_root(self, i, j): + """ + This is a method just to generate roots + with a 1 iin the ith position and a -1 + in the jth position. + + """ + + n = self.n + root = [0]*n + root[i] = 1 + root[j] = -1 + return root + + def simple_root(self, i): + """ + Every lie algebra has a unique root system. + Given a root system Q, there is a subset of the + roots such that an element of Q is called a + simple root if it cannot be written as the sum + of two elements in Q. If we let D denote the + set of simple roots, then it is clear that every + element of Q can be written as a linear combination + of elements of D with all coefficients non-negative. + + In D_n, the first n-1 simple roots are the same as + the roots in A_(n-1) (a 1 in the ith position, a -1 + in the (i+1)th position, and zeroes elsewhere). + The nth simple root is the root in which there 1s in + the nth and (n-1)th positions, and zeroes elsewhere. + + This method returns the ith simple root for the D series. + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("D4") + >>> c.simple_root(2) + [0, 1, -1, 0] + + """ + + n = self.n + if i < n: + return self.basic_root(i-1, i) + else: + root = [0]*n + root[n-2] = 1 + root[n-1] = 1 + return root + + + def positive_roots(self): + """ + This method generates all the positive roots of + A_n. This is half of all of the roots of D_n + by multiplying all the positive roots by -1 we + get the negative roots. + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("A3") + >>> c.positive_roots() + {1: [1, -1, 0, 0], 2: [1, 0, -1, 0], 3: [1, 0, 0, -1], 4: [0, 1, -1, 0], + 5: [0, 1, 0, -1], 6: [0, 0, 1, -1]} + """ + + n = self.n + posroots = {} + k = 0 + for i in range(0, n-1): + for j in range(i+1, n): + k += 1 + posroots[k] = self.basic_root(i, j) + k += 1 + root = self.basic_root(i, j) + root[j] = 1 + posroots[k] = root + return posroots + + def roots(self): + """ + Returns the total number of roots for D_n" + """ + + n = self.n + return 2*n*(n-1) + + def cartan_matrix(self): + """ + Returns the Cartan matrix for D_n. + The Cartan matrix matrix for a Lie algebra is + generated by assigning an ordering to the simple + roots, (alpha[1], ...., alpha[l]). Then the ijth + entry of the Cartan matrix is (). + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType('D4') + >>> c.cartan_matrix() + Matrix([ + [ 2, -1, 0, 0], + [-1, 2, -1, -1], + [ 0, -1, 2, 0], + [ 0, -1, 0, 2]]) + + """ + + n = self.n + m = 2*eye(n) + for i in range(1, n - 2): + m[i,i+1] = -1 + m[i,i-1] = -1 + m[n-2, n-3] = -1 + m[n-3, n-1] = -1 + m[n-1, n-3] = -1 + m[0, 1] = -1 + return m + + def basis(self): + """ + Returns the number of independent generators of D_n + """ + n = self.n + return n*(n-1)/2 + + def lie_algebra(self): + """ + Returns the Lie algebra associated with D_n" + """ + + n = self.n + return "so(" + str(2*n) + ")" + + def dynkin_diagram(self): + n = self.n + diag = " "*4*(n-3) + str(n-1) + "\n" + diag += " "*4*(n-3) + "0\n" + diag += " "*4*(n-3) +"|\n" + diag += " "*4*(n-3) + "|\n" + diag += "---".join("0" for i in range(1,n)) + "\n" + diag += " ".join(str(i) for i in range(1, n-1)) + " "+str(n) + return diag diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/type_e.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/type_e.py new file mode 100644 index 0000000000000000000000000000000000000000..3db9a820d31bff31acc58ba1592a1b10f8be53db --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/type_e.py @@ -0,0 +1,275 @@ +import itertools + +from .cartan_type import Standard_Cartan +from sympy.core.backend import eye, Rational +from sympy.core.singleton import S + +class TypeE(Standard_Cartan): + + def __new__(cls, n): + if n < 6 or n > 8: + raise ValueError("Invalid value of n") + return Standard_Cartan.__new__(cls, "E", n) + + def dimension(self): + """Dimension of the vector space V underlying the Lie algebra + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("E6") + >>> c.dimension() + 8 + """ + + return 8 + + def basic_root(self, i, j): + """ + This is a method just to generate roots + with a -1 in the ith position and a 1 + in the jth position. + + """ + + root = [0]*8 + root[i] = -1 + root[j] = 1 + return root + + def simple_root(self, i): + """ + Every Lie algebra has a unique root system. + Given a root system Q, there is a subset of the + roots such that an element of Q is called a + simple root if it cannot be written as the sum + of two elements in Q. If we let D denote the + set of simple roots, then it is clear that every + element of Q can be written as a linear combination + of elements of D with all coefficients non-negative. + + This method returns the ith simple root for E_n. + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("E6") + >>> c.simple_root(2) + [1, 1, 0, 0, 0, 0, 0, 0] + """ + n = self.n + if i == 1: + root = [-0.5]*8 + root[0] = 0.5 + root[7] = 0.5 + return root + elif i == 2: + root = [0]*8 + root[1] = 1 + root[0] = 1 + return root + else: + if i in (7, 8) and n == 6: + raise ValueError("E6 only has six simple roots!") + if i == 8 and n == 7: + raise ValueError("E7 only has seven simple roots!") + + return self.basic_root(i - 3, i - 2) + + def positive_roots(self): + """ + This method generates all the positive roots of + A_n. This is half of all of the roots of E_n; + by multiplying all the positive roots by -1 we + get the negative roots. + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("A3") + >>> c.positive_roots() + {1: [1, -1, 0, 0], 2: [1, 0, -1, 0], 3: [1, 0, 0, -1], 4: [0, 1, -1, 0], + 5: [0, 1, 0, -1], 6: [0, 0, 1, -1]} + """ + n = self.n + neghalf = Rational(-1, 2) + poshalf = S.Half + if n == 6: + posroots = {} + k = 0 + for i in range(n-1): + for j in range(i+1, n-1): + k += 1 + root = self.basic_root(i, j) + posroots[k] = root + k += 1 + root = self.basic_root(i, j) + root[i] = 1 + posroots[k] = root + + root = [poshalf, poshalf, poshalf, poshalf, poshalf, + neghalf, neghalf, poshalf] + for a, b, c, d, e in itertools.product( + range(2), range(2), range(2), range(2), range(2)): + if (a + b + c + d + e)%2 == 0: + k += 1 + if a == 1: + root[0] = neghalf + if b == 1: + root[1] = neghalf + if c == 1: + root[2] = neghalf + if d == 1: + root[3] = neghalf + if e == 1: + root[4] = neghalf + posroots[k] = root[:] + return posroots + if n == 7: + posroots = {} + k = 0 + for i in range(n-1): + for j in range(i+1, n-1): + k += 1 + root = self.basic_root(i, j) + posroots[k] = root + k += 1 + root = self.basic_root(i, j) + root[i] = 1 + posroots[k] = root + + k += 1 + posroots[k] = [0, 0, 0, 0, 0, 1, 1, 0] + root = [poshalf, poshalf, poshalf, poshalf, poshalf, + neghalf, neghalf, poshalf] + for a, b, c, d, e, f in itertools.product( + range(2), range(2), range(2), range(2), range(2), range(2)): + if (a + b + c + d + e + f)%2 == 0: + k += 1 + if a == 1: + root[0] = neghalf + if b == 1: + root[1] = neghalf + if c == 1: + root[2] = neghalf + if d == 1: + root[3] = neghalf + if e == 1: + root[4] = neghalf + if f == 1: + root[5] = poshalf + posroots[k] = root[:] + return posroots + if n == 8: + posroots = {} + k = 0 + for i in range(n): + for j in range(i+1, n): + k += 1 + root = self.basic_root(i, j) + posroots[k] = root + k += 1 + root = self.basic_root(i, j) + root[i] = 1 + posroots[k] = root + + root = [poshalf, poshalf, poshalf, poshalf, poshalf, + neghalf, neghalf, poshalf] + for a, b, c, d, e, f, g in itertools.product( + range(2), range(2), range(2), range(2), range(2), + range(2), range(2)): + if (a + b + c + d + e + f + g)%2 == 0: + k += 1 + if a == 1: + root[0] = neghalf + if b == 1: + root[1] = neghalf + if c == 1: + root[2] = neghalf + if d == 1: + root[3] = neghalf + if e == 1: + root[4] = neghalf + if f == 1: + root[5] = poshalf + if g == 1: + root[6] = poshalf + posroots[k] = root[:] + return posroots + + + + def roots(self): + """ + Returns the total number of roots of E_n + """ + + n = self.n + if n == 6: + return 72 + if n == 7: + return 126 + if n == 8: + return 240 + + + def cartan_matrix(self): + """ + Returns the Cartan matrix for G_2 + The Cartan matrix matrix for a Lie algebra is + generated by assigning an ordering to the simple + roots, (alpha[1], ...., alpha[l]). Then the ijth + entry of the Cartan matrix is (). + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType('A4') + >>> c.cartan_matrix() + Matrix([ + [ 2, -1, 0, 0], + [-1, 2, -1, 0], + [ 0, -1, 2, -1], + [ 0, 0, -1, 2]]) + + + """ + + n = self.n + m = 2*eye(n) + for i in range(3, n - 1): + m[i, i+1] = -1 + m[i, i-1] = -1 + m[0, 2] = m[2, 0] = -1 + m[1, 3] = m[3, 1] = -1 + m[2, 3] = -1 + m[n-1, n-2] = -1 + return m + + + def basis(self): + """ + Returns the number of independent generators of E_n + """ + + n = self.n + if n == 6: + return 78 + if n == 7: + return 133 + if n == 8: + return 248 + + def dynkin_diagram(self): + n = self.n + diag = " "*8 + str(2) + "\n" + diag += " "*8 + "0\n" + diag += " "*8 + "|\n" + diag += " "*8 + "|\n" + diag += "---".join("0" for i in range(1, n)) + "\n" + diag += "1 " + " ".join(str(i) for i in range(3, n+1)) + return diag diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/type_f.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/type_f.py new file mode 100644 index 0000000000000000000000000000000000000000..f04da557870f2cd21818cf69c454ef598e2ab65a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/type_f.py @@ -0,0 +1,162 @@ +from .cartan_type import Standard_Cartan +from sympy.core.backend import Matrix, Rational + + +class TypeF(Standard_Cartan): + + def __new__(cls, n): + if n != 4: + raise ValueError("n should be 4") + return Standard_Cartan.__new__(cls, "F", 4) + + def dimension(self): + """Dimension of the vector space V underlying the Lie algebra + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("F4") + >>> c.dimension() + 4 + """ + + return 4 + + + def basic_root(self, i, j): + """Generate roots with 1 in ith position and -1 in jth position + + """ + + n = self.n + root = [0]*n + root[i] = 1 + root[j] = -1 + return root + + def simple_root(self, i): + """The ith simple root of F_4 + + Every lie algebra has a unique root system. + Given a root system Q, there is a subset of the + roots such that an element of Q is called a + simple root if it cannot be written as the sum + of two elements in Q. If we let D denote the + set of simple roots, then it is clear that every + element of Q can be written as a linear combination + of elements of D with all coefficients non-negative. + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("F4") + >>> c.simple_root(3) + [0, 0, 0, 1] + + """ + + if i < 3: + return self.basic_root(i-1, i) + if i == 3: + root = [0]*4 + root[3] = 1 + return root + if i == 4: + root = [Rational(-1, 2)]*4 + return root + + def positive_roots(self): + """Generate all the positive roots of A_n + + This is half of all of the roots of F_4; by multiplying all the + positive roots by -1 we get the negative roots. + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("A3") + >>> c.positive_roots() + {1: [1, -1, 0, 0], 2: [1, 0, -1, 0], 3: [1, 0, 0, -1], 4: [0, 1, -1, 0], + 5: [0, 1, 0, -1], 6: [0, 0, 1, -1]} + + """ + + n = self.n + posroots = {} + k = 0 + for i in range(0, n-1): + for j in range(i+1, n): + k += 1 + posroots[k] = self.basic_root(i, j) + k += 1 + root = self.basic_root(i, j) + root[j] = 1 + posroots[k] = root + + for i in range(0, n): + k += 1 + root = [0]*n + root[i] = 1 + posroots[k] = root + + k += 1 + root = [Rational(1, 2)]*n + posroots[k] = root + for i in range(1, 4): + k += 1 + root = [Rational(1, 2)]*n + root[i] = Rational(-1, 2) + posroots[k] = root + + posroots[k+1] = [Rational(1, 2), Rational(1, 2), Rational(-1, 2), Rational(-1, 2)] + posroots[k+2] = [Rational(1, 2), Rational(-1, 2), Rational(1, 2), Rational(-1, 2)] + posroots[k+3] = [Rational(1, 2), Rational(-1, 2), Rational(-1, 2), Rational(1, 2)] + posroots[k+4] = [Rational(1, 2), Rational(-1, 2), Rational(-1, 2), Rational(-1, 2)] + + return posroots + + + def roots(self): + """ + Returns the total number of roots for F_4 + """ + return 48 + + def cartan_matrix(self): + """The Cartan matrix for F_4 + + The Cartan matrix matrix for a Lie algebra is + generated by assigning an ordering to the simple + roots, (alpha[1], ...., alpha[l]). Then the ijth + entry of the Cartan matrix is (). + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType('A4') + >>> c.cartan_matrix() + Matrix([ + [ 2, -1, 0, 0], + [-1, 2, -1, 0], + [ 0, -1, 2, -1], + [ 0, 0, -1, 2]]) + """ + + m = Matrix( 4, 4, [2, -1, 0, 0, -1, 2, -2, 0, 0, + -1, 2, -1, 0, 0, -1, 2]) + return m + + def basis(self): + """ + Returns the number of independent generators of F_4 + """ + return 52 + + def dynkin_diagram(self): + diag = "0---0=>=0---0\n" + diag += " ".join(str(i) for i in range(1, 5)) + return diag diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/type_g.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/type_g.py new file mode 100644 index 0000000000000000000000000000000000000000..014409cf5ed966b53c596b14e0073e89ceee05b6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/type_g.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +from .cartan_type import Standard_Cartan +from sympy.core.backend import Matrix + +class TypeG(Standard_Cartan): + + def __new__(cls, n): + if n != 2: + raise ValueError("n should be 2") + return Standard_Cartan.__new__(cls, "G", 2) + + + def dimension(self): + """Dimension of the vector space V underlying the Lie algebra + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("G2") + >>> c.dimension() + 3 + """ + return 3 + + def simple_root(self, i): + """The ith simple root of G_2 + + Every lie algebra has a unique root system. + Given a root system Q, there is a subset of the + roots such that an element of Q is called a + simple root if it cannot be written as the sum + of two elements in Q. If we let D denote the + set of simple roots, then it is clear that every + element of Q can be written as a linear combination + of elements of D with all coefficients non-negative. + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("G2") + >>> c.simple_root(1) + [0, 1, -1] + + """ + if i == 1: + return [0, 1, -1] + else: + return [1, -2, 1] + + def positive_roots(self): + """Generate all the positive roots of A_n + + This is half of all of the roots of A_n; by multiplying all the + positive roots by -1 we get the negative roots. + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("A3") + >>> c.positive_roots() + {1: [1, -1, 0, 0], 2: [1, 0, -1, 0], 3: [1, 0, 0, -1], 4: [0, 1, -1, 0], + 5: [0, 1, 0, -1], 6: [0, 0, 1, -1]} + + """ + + roots = {1: [0, 1, -1], 2: [1, -2, 1], 3: [1, -1, 0], 4: [1, 0, 1], + 5: [1, 1, -2], 6: [2, -1, -1]} + return roots + + def roots(self): + """ + Returns the total number of roots of G_2" + """ + return 12 + + def cartan_matrix(self): + """The Cartan matrix for G_2 + + The Cartan matrix matrix for a Lie algebra is + generated by assigning an ordering to the simple + roots, (alpha[1], ...., alpha[l]). Then the ijth + entry of the Cartan matrix is (). + + Examples + ======== + + >>> from sympy.liealgebras.cartan_type import CartanType + >>> c = CartanType("G2") + >>> c.cartan_matrix() + Matrix([ + [ 2, -1], + [-3, 2]]) + + """ + + m = Matrix( 2, 2, [2, -1, -3, 2]) + return m + + def basis(self): + """ + Returns the number of independent generators of G_2 + """ + return 14 + + def dynkin_diagram(self): + diag = "0≡<≡0\n1 2" + return diag diff --git a/venv/lib/python3.11/site-packages/sympy/liealgebras/weyl_group.py b/venv/lib/python3.11/site-packages/sympy/liealgebras/weyl_group.py new file mode 100644 index 0000000000000000000000000000000000000000..15ff70b6f1fc4649268a38ee13e1f717a1c9f5fa --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/liealgebras/weyl_group.py @@ -0,0 +1,403 @@ +# -*- coding: utf-8 -*- + +from .cartan_type import CartanType +from mpmath import fac +from sympy.core.backend import Matrix, eye, Rational, igcd +from sympy.core.basic import Atom + +class WeylGroup(Atom): + + """ + For each semisimple Lie group, we have a Weyl group. It is a subgroup of + the isometry group of the root system. Specifically, it's the subgroup + that is generated by reflections through the hyperplanes orthogonal to + the roots. Therefore, Weyl groups are reflection groups, and so a Weyl + group is a finite Coxeter group. + + """ + + def __new__(cls, cartantype): + obj = Atom.__new__(cls) + obj.cartan_type = CartanType(cartantype) + return obj + + def generators(self): + """ + This method creates the generating reflections of the Weyl group for + a given Lie algebra. For a Lie algebra of rank n, there are n + different generating reflections. This function returns them as + a list. + + Examples + ======== + + >>> from sympy.liealgebras.weyl_group import WeylGroup + >>> c = WeylGroup("F4") + >>> c.generators() + ['r1', 'r2', 'r3', 'r4'] + """ + n = self.cartan_type.rank() + generators = [] + for i in range(1, n+1): + reflection = "r"+str(i) + generators.append(reflection) + return generators + + def group_order(self): + """ + This method returns the order of the Weyl group. + For types A, B, C, D, and E the order depends on + the rank of the Lie algebra. For types F and G, + the order is fixed. + + Examples + ======== + + >>> from sympy.liealgebras.weyl_group import WeylGroup + >>> c = WeylGroup("D4") + >>> c.group_order() + 192.0 + """ + n = self.cartan_type.rank() + if self.cartan_type.series == "A": + return fac(n+1) + + if self.cartan_type.series in ("B", "C"): + return fac(n)*(2**n) + + if self.cartan_type.series == "D": + return fac(n)*(2**(n-1)) + + if self.cartan_type.series == "E": + if n == 6: + return 51840 + if n == 7: + return 2903040 + if n == 8: + return 696729600 + if self.cartan_type.series == "F": + return 1152 + + if self.cartan_type.series == "G": + return 12 + + def group_name(self): + """ + This method returns some general information about the Weyl group for + a given Lie algebra. It returns the name of the group and the elements + it acts on, if relevant. + """ + n = self.cartan_type.rank() + if self.cartan_type.series == "A": + return "S"+str(n+1) + ": the symmetric group acting on " + str(n+1) + " elements." + + if self.cartan_type.series in ("B", "C"): + return "The hyperoctahedral group acting on " + str(2*n) + " elements." + + if self.cartan_type.series == "D": + return "The symmetry group of the " + str(n) + "-dimensional demihypercube." + + if self.cartan_type.series == "E": + if n == 6: + return "The symmetry group of the 6-polytope." + + if n == 7: + return "The symmetry group of the 7-polytope." + + if n == 8: + return "The symmetry group of the 8-polytope." + + if self.cartan_type.series == "F": + return "The symmetry group of the 24-cell, or icositetrachoron." + + if self.cartan_type.series == "G": + return "D6, the dihedral group of order 12, and symmetry group of the hexagon." + + def element_order(self, weylelt): + """ + This method returns the order of a given Weyl group element, which should + be specified by the user in the form of products of the generating + reflections, i.e. of the form r1*r2 etc. + + For types A-F, this method current works by taking the matrix form of + the specified element, and then finding what power of the matrix is the + identity. It then returns this power. + + Examples + ======== + + >>> from sympy.liealgebras.weyl_group import WeylGroup + >>> b = WeylGroup("B4") + >>> b.element_order('r1*r4*r2') + 4 + """ + n = self.cartan_type.rank() + if self.cartan_type.series == "A": + a = self.matrix_form(weylelt) + order = 1 + while a != eye(n+1): + a *= self.matrix_form(weylelt) + order += 1 + return order + + if self.cartan_type.series == "D": + a = self.matrix_form(weylelt) + order = 1 + while a != eye(n): + a *= self.matrix_form(weylelt) + order += 1 + return order + + if self.cartan_type.series == "E": + a = self.matrix_form(weylelt) + order = 1 + while a != eye(8): + a *= self.matrix_form(weylelt) + order += 1 + return order + + if self.cartan_type.series == "G": + elts = list(weylelt) + reflections = elts[1::3] + m = self.delete_doubles(reflections) + while self.delete_doubles(m) != m: + m = self.delete_doubles(m) + reflections = m + if len(reflections) % 2 == 1: + return 2 + + elif len(reflections) == 0: + return 1 + + else: + if len(reflections) == 1: + return 2 + else: + m = len(reflections) // 2 + lcm = (6 * m)/ igcd(m, 6) + order = lcm / m + return order + + + if self.cartan_type.series == 'F': + a = self.matrix_form(weylelt) + order = 1 + while a != eye(4): + a *= self.matrix_form(weylelt) + order += 1 + return order + + + if self.cartan_type.series in ("B", "C"): + a = self.matrix_form(weylelt) + order = 1 + while a != eye(n): + a *= self.matrix_form(weylelt) + order += 1 + return order + + def delete_doubles(self, reflections): + """ + This is a helper method for determining the order of an element in the + Weyl group of G2. It takes a Weyl element and if repeated simple reflections + in it, it deletes them. + """ + counter = 0 + copy = list(reflections) + for elt in copy: + if counter < len(copy)-1: + if copy[counter + 1] == elt: + del copy[counter] + del copy[counter] + counter += 1 + + + return copy + + + def matrix_form(self, weylelt): + """ + This method takes input from the user in the form of products of the + generating reflections, and returns the matrix corresponding to the + element of the Weyl group. Since each element of the Weyl group is + a reflection of some type, there is a corresponding matrix representation. + This method uses the standard representation for all the generating + reflections. + + Examples + ======== + + >>> from sympy.liealgebras.weyl_group import WeylGroup + >>> f = WeylGroup("F4") + >>> f.matrix_form('r2*r3') + Matrix([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, -1], + [0, 0, 1, 0]]) + + """ + elts = list(weylelt) + reflections = elts[1::3] + n = self.cartan_type.rank() + if self.cartan_type.series == 'A': + matrixform = eye(n+1) + for elt in reflections: + a = int(elt) + mat = eye(n+1) + mat[a-1, a-1] = 0 + mat[a-1, a] = 1 + mat[a, a-1] = 1 + mat[a, a] = 0 + matrixform *= mat + return matrixform + + if self.cartan_type.series == 'D': + matrixform = eye(n) + for elt in reflections: + a = int(elt) + mat = eye(n) + if a < n: + mat[a-1, a-1] = 0 + mat[a-1, a] = 1 + mat[a, a-1] = 1 + mat[a, a] = 0 + matrixform *= mat + else: + mat[n-2, n-1] = -1 + mat[n-2, n-2] = 0 + mat[n-1, n-2] = -1 + mat[n-1, n-1] = 0 + matrixform *= mat + return matrixform + + if self.cartan_type.series == 'G': + matrixform = eye(3) + for elt in reflections: + a = int(elt) + if a == 1: + gen1 = Matrix([[1, 0, 0], [0, 0, 1], [0, 1, 0]]) + matrixform *= gen1 + else: + gen2 = Matrix([[Rational(2, 3), Rational(2, 3), Rational(-1, 3)], + [Rational(2, 3), Rational(-1, 3), Rational(2, 3)], + [Rational(-1, 3), Rational(2, 3), Rational(2, 3)]]) + matrixform *= gen2 + return matrixform + + if self.cartan_type.series == 'F': + matrixform = eye(4) + for elt in reflections: + a = int(elt) + if a == 1: + mat = Matrix([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) + matrixform *= mat + elif a == 2: + mat = Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) + matrixform *= mat + elif a == 3: + mat = Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]]) + matrixform *= mat + else: + + mat = Matrix([[Rational(1, 2), Rational(1, 2), Rational(1, 2), Rational(1, 2)], + [Rational(1, 2), Rational(1, 2), Rational(-1, 2), Rational(-1, 2)], + [Rational(1, 2), Rational(-1, 2), Rational(1, 2), Rational(-1, 2)], + [Rational(1, 2), Rational(-1, 2), Rational(-1, 2), Rational(1, 2)]]) + matrixform *= mat + return matrixform + + if self.cartan_type.series == 'E': + matrixform = eye(8) + for elt in reflections: + a = int(elt) + if a == 1: + mat = Matrix([[Rational(3, 4), Rational(1, 4), Rational(1, 4), Rational(1, 4), + Rational(1, 4), Rational(1, 4), Rational(1, 4), Rational(-1, 4)], + [Rational(1, 4), Rational(3, 4), Rational(-1, 4), Rational(-1, 4), + Rational(-1, 4), Rational(-1, 4), Rational(1, 4), Rational(-1, 4)], + [Rational(1, 4), Rational(-1, 4), Rational(3, 4), Rational(-1, 4), + Rational(-1, 4), Rational(-1, 4), Rational(-1, 4), Rational(1, 4)], + [Rational(1, 4), Rational(-1, 4), Rational(-1, 4), Rational(3, 4), + Rational(-1, 4), Rational(-1, 4), Rational(-1, 4), Rational(1, 4)], + [Rational(1, 4), Rational(-1, 4), Rational(-1, 4), Rational(-1, 4), + Rational(3, 4), Rational(-1, 4), Rational(-1, 4), Rational(1, 4)], + [Rational(1, 4), Rational(-1, 4), Rational(-1, 4), Rational(-1, 4), + Rational(-1, 4), Rational(3, 4), Rational(-1, 4), Rational(1, 4)], + [Rational(1, 4), Rational(-1, 4), Rational(-1, 4), Rational(-1, 4), + Rational(-1, 4), Rational(-1, 4), Rational(-3, 4), Rational(1, 4)], + [Rational(1, 4), Rational(-1, 4), Rational(-1, 4), Rational(-1, 4), + Rational(-1, 4), Rational(-1, 4), Rational(-1, 4), Rational(3, 4)]]) + matrixform *= mat + elif a == 2: + mat = eye(8) + mat[0, 0] = 0 + mat[0, 1] = -1 + mat[1, 0] = -1 + mat[1, 1] = 0 + matrixform *= mat + else: + mat = eye(8) + mat[a-3, a-3] = 0 + mat[a-3, a-2] = 1 + mat[a-2, a-3] = 1 + mat[a-2, a-2] = 0 + matrixform *= mat + return matrixform + + + if self.cartan_type.series in ("B", "C"): + matrixform = eye(n) + for elt in reflections: + a = int(elt) + mat = eye(n) + if a == 1: + mat[0, 0] = -1 + matrixform *= mat + else: + mat[a - 2, a - 2] = 0 + mat[a-2, a-1] = 1 + mat[a - 1, a - 2] = 1 + mat[a -1, a - 1] = 0 + matrixform *= mat + return matrixform + + + + def coxeter_diagram(self): + """ + This method returns the Coxeter diagram corresponding to a Weyl group. + The Coxeter diagram can be obtained from a Lie algebra's Dynkin diagram + by deleting all arrows; the Coxeter diagram is the undirected graph. + The vertices of the Coxeter diagram represent the generating reflections + of the Weyl group, $s_i$. An edge is drawn between $s_i$ and $s_j$ if the order + $m(i, j)$ of $s_is_j$ is greater than two. If there is one edge, the order + $m(i, j)$ is 3. If there are two edges, the order $m(i, j)$ is 4, and if there + are three edges, the order $m(i, j)$ is 6. + + Examples + ======== + + >>> from sympy.liealgebras.weyl_group import WeylGroup + >>> c = WeylGroup("B3") + >>> print(c.coxeter_diagram()) + 0---0===0 + 1 2 3 + """ + n = self.cartan_type.rank() + if self.cartan_type.series in ("A", "D", "E"): + return self.cartan_type.dynkin_diagram() + + if self.cartan_type.series in ("B", "C"): + diag = "---".join("0" for i in range(1, n)) + "===0\n" + diag += " ".join(str(i) for i in range(1, n+1)) + return diag + + if self.cartan_type.series == "F": + diag = "0---0===0---0\n" + diag += " ".join(str(i) for i in range(1, 5)) + return diag + + if self.cartan_type.series == "G": + diag = "0≡≡≡0\n1 2" + return diag diff --git a/venv/lib/python3.11/site-packages/sympy/logic/__init__.py b/venv/lib/python3.11/site-packages/sympy/logic/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cb26903a384e9df3a0f02a92c488c5442cee1486 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/__init__.py @@ -0,0 +1,12 @@ +from .boolalg import (to_cnf, to_dnf, to_nnf, And, Or, Not, Xor, Nand, Nor, Implies, + Equivalent, ITE, POSform, SOPform, simplify_logic, bool_map, true, false, + gateinputcount) +from .inference import satisfiable + +__all__ = [ + 'to_cnf', 'to_dnf', 'to_nnf', 'And', 'Or', 'Not', 'Xor', 'Nand', 'Nor', + 'Implies', 'Equivalent', 'ITE', 'POSform', 'SOPform', 'simplify_logic', + 'bool_map', 'true', 'false', 'gateinputcount', + + 'satisfiable', +] diff --git a/venv/lib/python3.11/site-packages/sympy/logic/algorithms/__init__.py b/venv/lib/python3.11/site-packages/sympy/logic/algorithms/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/logic/algorithms/dpll.py b/venv/lib/python3.11/site-packages/sympy/logic/algorithms/dpll.py new file mode 100644 index 0000000000000000000000000000000000000000..40e6802f7626c982a9a6cd7146baea3ac6b8b6e0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/algorithms/dpll.py @@ -0,0 +1,308 @@ +"""Implementation of DPLL algorithm + +Further improvements: eliminate calls to pl_true, implement branching rules, +efficient unit propagation. + +References: + - https://en.wikipedia.org/wiki/DPLL_algorithm + - https://www.researchgate.net/publication/242384772_Implementations_of_the_DPLL_Algorithm +""" + +from sympy.core.sorting import default_sort_key +from sympy.logic.boolalg import Or, Not, conjuncts, disjuncts, to_cnf, \ + to_int_repr, _find_predicates +from sympy.assumptions.cnf import CNF +from sympy.logic.inference import pl_true, literal_symbol + + +def dpll_satisfiable(expr): + """ + Check satisfiability of a propositional sentence. + It returns a model rather than True when it succeeds + + >>> from sympy.abc import A, B + >>> from sympy.logic.algorithms.dpll import dpll_satisfiable + >>> dpll_satisfiable(A & ~B) + {A: True, B: False} + >>> dpll_satisfiable(A & ~A) + False + + """ + if not isinstance(expr, CNF): + clauses = conjuncts(to_cnf(expr)) + else: + clauses = expr.clauses + if False in clauses: + return False + symbols = sorted(_find_predicates(expr), key=default_sort_key) + symbols_int_repr = set(range(1, len(symbols) + 1)) + clauses_int_repr = to_int_repr(clauses, symbols) + result = dpll_int_repr(clauses_int_repr, symbols_int_repr, {}) + if not result: + return result + output = {} + for key in result: + output.update({symbols[key - 1]: result[key]}) + return output + + +def dpll(clauses, symbols, model): + """ + Compute satisfiability in a partial model. + Clauses is an array of conjuncts. + + >>> from sympy.abc import A, B, D + >>> from sympy.logic.algorithms.dpll import dpll + >>> dpll([A, B, D], [A, B], {D: False}) + False + + """ + # compute DP kernel + P, value = find_unit_clause(clauses, model) + while P: + model.update({P: value}) + symbols.remove(P) + if not value: + P = ~P + clauses = unit_propagate(clauses, P) + P, value = find_unit_clause(clauses, model) + P, value = find_pure_symbol(symbols, clauses) + while P: + model.update({P: value}) + symbols.remove(P) + if not value: + P = ~P + clauses = unit_propagate(clauses, P) + P, value = find_pure_symbol(symbols, clauses) + # end DP kernel + unknown_clauses = [] + for c in clauses: + val = pl_true(c, model) + if val is False: + return False + if val is not True: + unknown_clauses.append(c) + if not unknown_clauses: + return model + if not clauses: + return model + P = symbols.pop() + model_copy = model.copy() + model.update({P: True}) + model_copy.update({P: False}) + symbols_copy = symbols[:] + return (dpll(unit_propagate(unknown_clauses, P), symbols, model) or + dpll(unit_propagate(unknown_clauses, Not(P)), symbols_copy, model_copy)) + + +def dpll_int_repr(clauses, symbols, model): + """ + Compute satisfiability in a partial model. + Arguments are expected to be in integer representation + + >>> from sympy.logic.algorithms.dpll import dpll_int_repr + >>> dpll_int_repr([{1}, {2}, {3}], {1, 2}, {3: False}) + False + + """ + # compute DP kernel + P, value = find_unit_clause_int_repr(clauses, model) + while P: + model.update({P: value}) + symbols.remove(P) + if not value: + P = -P + clauses = unit_propagate_int_repr(clauses, P) + P, value = find_unit_clause_int_repr(clauses, model) + P, value = find_pure_symbol_int_repr(symbols, clauses) + while P: + model.update({P: value}) + symbols.remove(P) + if not value: + P = -P + clauses = unit_propagate_int_repr(clauses, P) + P, value = find_pure_symbol_int_repr(symbols, clauses) + # end DP kernel + unknown_clauses = [] + for c in clauses: + val = pl_true_int_repr(c, model) + if val is False: + return False + if val is not True: + unknown_clauses.append(c) + if not unknown_clauses: + return model + P = symbols.pop() + model_copy = model.copy() + model.update({P: True}) + model_copy.update({P: False}) + symbols_copy = symbols.copy() + return (dpll_int_repr(unit_propagate_int_repr(unknown_clauses, P), symbols, model) or + dpll_int_repr(unit_propagate_int_repr(unknown_clauses, -P), symbols_copy, model_copy)) + +### helper methods for DPLL + + +def pl_true_int_repr(clause, model={}): + """ + Lightweight version of pl_true. + Argument clause represents the set of args of an Or clause. This is used + inside dpll_int_repr, it is not meant to be used directly. + + >>> from sympy.logic.algorithms.dpll import pl_true_int_repr + >>> pl_true_int_repr({1, 2}, {1: False}) + >>> pl_true_int_repr({1, 2}, {1: False, 2: False}) + False + + """ + result = False + for lit in clause: + if lit < 0: + p = model.get(-lit) + if p is not None: + p = not p + else: + p = model.get(lit) + if p is True: + return True + elif p is None: + result = None + return result + + +def unit_propagate(clauses, symbol): + """ + Returns an equivalent set of clauses + If a set of clauses contains the unit clause l, the other clauses are + simplified by the application of the two following rules: + + 1. every clause containing l is removed + 2. in every clause that contains ~l this literal is deleted + + Arguments are expected to be in CNF. + + >>> from sympy.abc import A, B, D + >>> from sympy.logic.algorithms.dpll import unit_propagate + >>> unit_propagate([A | B, D | ~B, B], B) + [D, B] + + """ + output = [] + for c in clauses: + if c.func != Or: + output.append(c) + continue + for arg in c.args: + if arg == ~symbol: + output.append(Or(*[x for x in c.args if x != ~symbol])) + break + if arg == symbol: + break + else: + output.append(c) + return output + + +def unit_propagate_int_repr(clauses, s): + """ + Same as unit_propagate, but arguments are expected to be in integer + representation + + >>> from sympy.logic.algorithms.dpll import unit_propagate_int_repr + >>> unit_propagate_int_repr([{1, 2}, {3, -2}, {2}], 2) + [{3}] + + """ + negated = {-s} + return [clause - negated for clause in clauses if s not in clause] + + +def find_pure_symbol(symbols, unknown_clauses): + """ + Find a symbol and its value if it appears only as a positive literal + (or only as a negative) in clauses. + + >>> from sympy.abc import A, B, D + >>> from sympy.logic.algorithms.dpll import find_pure_symbol + >>> find_pure_symbol([A, B, D], [A|~B,~B|~D,D|A]) + (A, True) + + """ + for sym in symbols: + found_pos, found_neg = False, False + for c in unknown_clauses: + if not found_pos and sym in disjuncts(c): + found_pos = True + if not found_neg and Not(sym) in disjuncts(c): + found_neg = True + if found_pos != found_neg: + return sym, found_pos + return None, None + + +def find_pure_symbol_int_repr(symbols, unknown_clauses): + """ + Same as find_pure_symbol, but arguments are expected + to be in integer representation + + >>> from sympy.logic.algorithms.dpll import find_pure_symbol_int_repr + >>> find_pure_symbol_int_repr({1,2,3}, + ... [{1, -2}, {-2, -3}, {3, 1}]) + (1, True) + + """ + all_symbols = set().union(*unknown_clauses) + found_pos = all_symbols.intersection(symbols) + found_neg = all_symbols.intersection([-s for s in symbols]) + for p in found_pos: + if -p not in found_neg: + return p, True + for p in found_neg: + if -p not in found_pos: + return -p, False + return None, None + + +def find_unit_clause(clauses, model): + """ + A unit clause has only 1 variable that is not bound in the model. + + >>> from sympy.abc import A, B, D + >>> from sympy.logic.algorithms.dpll import find_unit_clause + >>> find_unit_clause([A | B | D, B | ~D, A | ~B], {A:True}) + (B, False) + + """ + for clause in clauses: + num_not_in_model = 0 + for literal in disjuncts(clause): + sym = literal_symbol(literal) + if sym not in model: + num_not_in_model += 1 + P, value = sym, not isinstance(literal, Not) + if num_not_in_model == 1: + return P, value + return None, None + + +def find_unit_clause_int_repr(clauses, model): + """ + Same as find_unit_clause, but arguments are expected to be in + integer representation. + + >>> from sympy.logic.algorithms.dpll import find_unit_clause_int_repr + >>> find_unit_clause_int_repr([{1, 2, 3}, + ... {2, -3}, {1, -2}], {1: True}) + (2, False) + + """ + bound = set(model) | {-sym for sym in model} + for clause in clauses: + unbound = clause - bound + if len(unbound) == 1: + p = unbound.pop() + if p < 0: + return -p, False + else: + return p, True + return None, None diff --git a/venv/lib/python3.11/site-packages/sympy/logic/algorithms/dpll2.py b/venv/lib/python3.11/site-packages/sympy/logic/algorithms/dpll2.py new file mode 100644 index 0000000000000000000000000000000000000000..4f18c81189d6be565dc9b7caa3f0bf48e978bb56 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/algorithms/dpll2.py @@ -0,0 +1,688 @@ +"""Implementation of DPLL algorithm + +Features: + - Clause learning + - Watch literal scheme + - VSIDS heuristic + +References: + - https://en.wikipedia.org/wiki/DPLL_algorithm +""" + +from collections import defaultdict +from heapq import heappush, heappop + +from sympy.core.sorting import ordered +from sympy.assumptions.cnf import EncodedCNF + +from sympy.logic.algorithms.lra_theory import LRASolver + + +def dpll_satisfiable(expr, all_models=False, use_lra_theory=False): + """ + Check satisfiability of a propositional sentence. + It returns a model rather than True when it succeeds. + Returns a generator of all models if all_models is True. + + Examples + ======== + + >>> from sympy.abc import A, B + >>> from sympy.logic.algorithms.dpll2 import dpll_satisfiable + >>> dpll_satisfiable(A & ~B) + {A: True, B: False} + >>> dpll_satisfiable(A & ~A) + False + + """ + if not isinstance(expr, EncodedCNF): + exprs = EncodedCNF() + exprs.add_prop(expr) + expr = exprs + + # Return UNSAT when False (encoded as 0) is present in the CNF + if {0} in expr.data: + if all_models: + return (f for f in [False]) + return False + + if use_lra_theory: + lra, immediate_conflicts = LRASolver.from_encoded_cnf(expr) + else: + lra = None + immediate_conflicts = [] + solver = SATSolver(expr.data + immediate_conflicts, expr.variables, set(), expr.symbols, lra_theory=lra) + models = solver._find_model() + + if all_models: + return _all_models(models) + + try: + return next(models) + except StopIteration: + return False + + # Uncomment to confirm the solution is valid (hitting set for the clauses) + #else: + #for cls in clauses_int_repr: + #assert solver.var_settings.intersection(cls) + + +def _all_models(models): + satisfiable = False + try: + while True: + yield next(models) + satisfiable = True + except StopIteration: + if not satisfiable: + yield False + + +class SATSolver: + """ + Class for representing a SAT solver capable of + finding a model to a boolean theory in conjunctive + normal form. + """ + + def __init__(self, clauses, variables, var_settings, symbols=None, + heuristic='vsids', clause_learning='none', INTERVAL=500, + lra_theory = None): + + self.var_settings = var_settings + self.heuristic = heuristic + self.is_unsatisfied = False + self._unit_prop_queue = [] + self.update_functions = [] + self.INTERVAL = INTERVAL + + if symbols is None: + self.symbols = list(ordered(variables)) + else: + self.symbols = symbols + + self._initialize_variables(variables) + self._initialize_clauses(clauses) + + if 'vsids' == heuristic: + self._vsids_init() + self.heur_calculate = self._vsids_calculate + self.heur_lit_assigned = self._vsids_lit_assigned + self.heur_lit_unset = self._vsids_lit_unset + self.heur_clause_added = self._vsids_clause_added + + # Note: Uncomment this if/when clause learning is enabled + #self.update_functions.append(self._vsids_decay) + + else: + raise NotImplementedError + + if 'simple' == clause_learning: + self.add_learned_clause = self._simple_add_learned_clause + self.compute_conflict = self._simple_compute_conflict + self.update_functions.append(self._simple_clean_clauses) + elif 'none' == clause_learning: + self.add_learned_clause = lambda x: None + self.compute_conflict = lambda: None + else: + raise NotImplementedError + + # Create the base level + self.levels = [Level(0)] + self._current_level.varsettings = var_settings + + # Keep stats + self.num_decisions = 0 + self.num_learned_clauses = 0 + self.original_num_clauses = len(self.clauses) + + self.lra = lra_theory + + def _initialize_variables(self, variables): + """Set up the variable data structures needed.""" + self.sentinels = defaultdict(set) + self.occurrence_count = defaultdict(int) + self.variable_set = [False] * (len(variables) + 1) + + def _initialize_clauses(self, clauses): + """Set up the clause data structures needed. + + For each clause, the following changes are made: + - Unit clauses are queued for propagation right away. + - Non-unit clauses have their first and last literals set as sentinels. + - The number of clauses a literal appears in is computed. + """ + self.clauses = [list(clause) for clause in clauses] + + for i, clause in enumerate(self.clauses): + + # Handle the unit clauses + if 1 == len(clause): + self._unit_prop_queue.append(clause[0]) + continue + + self.sentinels[clause[0]].add(i) + self.sentinels[clause[-1]].add(i) + + for lit in clause: + self.occurrence_count[lit] += 1 + + def _find_model(self): + """ + Main DPLL loop. Returns a generator of models. + + Variables are chosen successively, and assigned to be either + True or False. If a solution is not found with this setting, + the opposite is chosen and the search continues. The solver + halts when every variable has a setting. + + Examples + ======== + + >>> from sympy.logic.algorithms.dpll2 import SATSolver + >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2}, + ... {3, -2}], {1, 2, 3}, set()) + >>> list(l._find_model()) + [{1: True, 2: False, 3: False}, {1: True, 2: True, 3: True}] + + >>> from sympy.abc import A, B, C + >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2}, + ... {3, -2}], {1, 2, 3}, set(), [A, B, C]) + >>> list(l._find_model()) + [{A: True, B: False, C: False}, {A: True, B: True, C: True}] + + """ + + # We use this variable to keep track of if we should flip a + # variable setting in successive rounds + flip_var = False + + # Check if unit prop says the theory is unsat right off the bat + self._simplify() + if self.is_unsatisfied: + return + + # While the theory still has clauses remaining + while True: + # Perform cleanup / fixup at regular intervals + if self.num_decisions % self.INTERVAL == 0: + for func in self.update_functions: + func() + + if flip_var: + # We have just backtracked and we are trying to opposite literal + flip_var = False + lit = self._current_level.decision + + else: + # Pick a literal to set + lit = self.heur_calculate() + self.num_decisions += 1 + + # Stopping condition for a satisfying theory + if 0 == lit: + + # check if assignment satisfies lra theory + if self.lra: + for enc_var in self.var_settings: + res = self.lra.assert_lit(enc_var) + if res is not None: + break + res = self.lra.check() + self.lra.reset_bounds() + else: + res = None + if res is None or res[0]: + yield {self.symbols[abs(lit) - 1]: + lit > 0 for lit in self.var_settings} + else: + self._simple_add_learned_clause(res[1]) + + # backtrack until we unassign one of the literals causing the conflict + while not any(-lit in res[1] for lit in self._current_level.var_settings): + self._undo() + + while self._current_level.flipped: + self._undo() + if len(self.levels) == 1: + return + flip_lit = -self._current_level.decision + self._undo() + self.levels.append(Level(flip_lit, flipped=True)) + flip_var = True + continue + + # Start the new decision level + self.levels.append(Level(lit)) + + # Assign the literal, updating the clauses it satisfies + self._assign_literal(lit) + + # _simplify the theory + self._simplify() + + # Check if we've made the theory unsat + if self.is_unsatisfied: + + self.is_unsatisfied = False + + # We unroll all of the decisions until we can flip a literal + while self._current_level.flipped: + self._undo() + + # If we've unrolled all the way, the theory is unsat + if 1 == len(self.levels): + return + + # Detect and add a learned clause + self.add_learned_clause(self.compute_conflict()) + + # Try the opposite setting of the most recent decision + flip_lit = -self._current_level.decision + self._undo() + self.levels.append(Level(flip_lit, flipped=True)) + flip_var = True + + ######################## + # Helper Methods # + ######################## + @property + def _current_level(self): + """The current decision level data structure + + Examples + ======== + + >>> from sympy.logic.algorithms.dpll2 import SATSolver + >>> l = SATSolver([{1}, {2}], {1, 2}, set()) + >>> next(l._find_model()) + {1: True, 2: True} + >>> l._current_level.decision + 0 + >>> l._current_level.flipped + False + >>> l._current_level.var_settings + {1, 2} + + """ + return self.levels[-1] + + def _clause_sat(self, cls): + """Check if a clause is satisfied by the current variable setting. + + Examples + ======== + + >>> from sympy.logic.algorithms.dpll2 import SATSolver + >>> l = SATSolver([{1}, {-1}], {1}, set()) + >>> try: + ... next(l._find_model()) + ... except StopIteration: + ... pass + >>> l._clause_sat(0) + False + >>> l._clause_sat(1) + True + + """ + for lit in self.clauses[cls]: + if lit in self.var_settings: + return True + return False + + def _is_sentinel(self, lit, cls): + """Check if a literal is a sentinel of a given clause. + + Examples + ======== + + >>> from sympy.logic.algorithms.dpll2 import SATSolver + >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2}, + ... {3, -2}], {1, 2, 3}, set()) + >>> next(l._find_model()) + {1: True, 2: False, 3: False} + >>> l._is_sentinel(2, 3) + True + >>> l._is_sentinel(-3, 1) + False + + """ + return cls in self.sentinels[lit] + + def _assign_literal(self, lit): + """Make a literal assignment. + + The literal assignment must be recorded as part of the current + decision level. Additionally, if the literal is marked as a + sentinel of any clause, then a new sentinel must be chosen. If + this is not possible, then unit propagation is triggered and + another literal is added to the queue to be set in the future. + + Examples + ======== + + >>> from sympy.logic.algorithms.dpll2 import SATSolver + >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2}, + ... {3, -2}], {1, 2, 3}, set()) + >>> next(l._find_model()) + {1: True, 2: False, 3: False} + >>> l.var_settings + {-3, -2, 1} + + >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2}, + ... {3, -2}], {1, 2, 3}, set()) + >>> l._assign_literal(-1) + >>> try: + ... next(l._find_model()) + ... except StopIteration: + ... pass + >>> l.var_settings + {-1} + + """ + self.var_settings.add(lit) + self._current_level.var_settings.add(lit) + self.variable_set[abs(lit)] = True + self.heur_lit_assigned(lit) + + sentinel_list = list(self.sentinels[-lit]) + + for cls in sentinel_list: + if not self._clause_sat(cls): + other_sentinel = None + for newlit in self.clauses[cls]: + if newlit != -lit: + if self._is_sentinel(newlit, cls): + other_sentinel = newlit + elif not self.variable_set[abs(newlit)]: + self.sentinels[-lit].remove(cls) + self.sentinels[newlit].add(cls) + other_sentinel = None + break + + # Check if no sentinel update exists + if other_sentinel: + self._unit_prop_queue.append(other_sentinel) + + def _undo(self): + """ + _undo the changes of the most recent decision level. + + Examples + ======== + + >>> from sympy.logic.algorithms.dpll2 import SATSolver + >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2}, + ... {3, -2}], {1, 2, 3}, set()) + >>> next(l._find_model()) + {1: True, 2: False, 3: False} + >>> level = l._current_level + >>> level.decision, level.var_settings, level.flipped + (-3, {-3, -2}, False) + >>> l._undo() + >>> level = l._current_level + >>> level.decision, level.var_settings, level.flipped + (0, {1}, False) + + """ + # Undo the variable settings + for lit in self._current_level.var_settings: + self.var_settings.remove(lit) + self.heur_lit_unset(lit) + self.variable_set[abs(lit)] = False + + # Pop the level off the stack + self.levels.pop() + + ######################### + # Propagation # + ######################### + """ + Propagation methods should attempt to soundly simplify the boolean + theory, and return True if any simplification occurred and False + otherwise. + """ + def _simplify(self): + """Iterate over the various forms of propagation to simplify the theory. + + Examples + ======== + + >>> from sympy.logic.algorithms.dpll2 import SATSolver + >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2}, + ... {3, -2}], {1, 2, 3}, set()) + >>> l.variable_set + [False, False, False, False] + >>> l.sentinels + {-3: {0, 2}, -2: {3, 4}, 2: {0, 3}, 3: {2, 4}} + + >>> l._simplify() + + >>> l.variable_set + [False, True, False, False] + >>> l.sentinels + {-3: {0, 2}, -2: {3, 4}, -1: set(), 2: {0, 3}, + ...3: {2, 4}} + + """ + changed = True + while changed: + changed = False + changed |= self._unit_prop() + changed |= self._pure_literal() + + def _unit_prop(self): + """Perform unit propagation on the current theory.""" + result = len(self._unit_prop_queue) > 0 + while self._unit_prop_queue: + next_lit = self._unit_prop_queue.pop() + if -next_lit in self.var_settings: + self.is_unsatisfied = True + self._unit_prop_queue = [] + return False + else: + self._assign_literal(next_lit) + + return result + + def _pure_literal(self): + """Look for pure literals and assign them when found.""" + return False + + ######################### + # Heuristics # + ######################### + def _vsids_init(self): + """Initialize the data structures needed for the VSIDS heuristic.""" + self.lit_heap = [] + self.lit_scores = {} + + for var in range(1, len(self.variable_set)): + self.lit_scores[var] = float(-self.occurrence_count[var]) + self.lit_scores[-var] = float(-self.occurrence_count[-var]) + heappush(self.lit_heap, (self.lit_scores[var], var)) + heappush(self.lit_heap, (self.lit_scores[-var], -var)) + + def _vsids_decay(self): + """Decay the VSIDS scores for every literal. + + Examples + ======== + + >>> from sympy.logic.algorithms.dpll2 import SATSolver + >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2}, + ... {3, -2}], {1, 2, 3}, set()) + + >>> l.lit_scores + {-3: -2.0, -2: -2.0, -1: 0.0, 1: 0.0, 2: -2.0, 3: -2.0} + + >>> l._vsids_decay() + + >>> l.lit_scores + {-3: -1.0, -2: -1.0, -1: 0.0, 1: 0.0, 2: -1.0, 3: -1.0} + + """ + # We divide every literal score by 2 for a decay factor + # Note: This doesn't change the heap property + for lit in self.lit_scores.keys(): + self.lit_scores[lit] /= 2.0 + + def _vsids_calculate(self): + """ + VSIDS Heuristic Calculation + + Examples + ======== + + >>> from sympy.logic.algorithms.dpll2 import SATSolver + >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2}, + ... {3, -2}], {1, 2, 3}, set()) + + >>> l.lit_heap + [(-2.0, -3), (-2.0, 2), (-2.0, -2), (0.0, 1), (-2.0, 3), (0.0, -1)] + + >>> l._vsids_calculate() + -3 + + >>> l.lit_heap + [(-2.0, -2), (-2.0, 2), (0.0, -1), (0.0, 1), (-2.0, 3)] + + """ + if len(self.lit_heap) == 0: + return 0 + + # Clean out the front of the heap as long the variables are set + while self.variable_set[abs(self.lit_heap[0][1])]: + heappop(self.lit_heap) + if len(self.lit_heap) == 0: + return 0 + + return heappop(self.lit_heap)[1] + + def _vsids_lit_assigned(self, lit): + """Handle the assignment of a literal for the VSIDS heuristic.""" + pass + + def _vsids_lit_unset(self, lit): + """Handle the unsetting of a literal for the VSIDS heuristic. + + Examples + ======== + + >>> from sympy.logic.algorithms.dpll2 import SATSolver + >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2}, + ... {3, -2}], {1, 2, 3}, set()) + >>> l.lit_heap + [(-2.0, -3), (-2.0, 2), (-2.0, -2), (0.0, 1), (-2.0, 3), (0.0, -1)] + + >>> l._vsids_lit_unset(2) + + >>> l.lit_heap + [(-2.0, -3), (-2.0, -2), (-2.0, -2), (-2.0, 2), (-2.0, 3), (0.0, -1), + ...(-2.0, 2), (0.0, 1)] + + """ + var = abs(lit) + heappush(self.lit_heap, (self.lit_scores[var], var)) + heappush(self.lit_heap, (self.lit_scores[-var], -var)) + + def _vsids_clause_added(self, cls): + """Handle the addition of a new clause for the VSIDS heuristic. + + Examples + ======== + + >>> from sympy.logic.algorithms.dpll2 import SATSolver + >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2}, + ... {3, -2}], {1, 2, 3}, set()) + + >>> l.num_learned_clauses + 0 + >>> l.lit_scores + {-3: -2.0, -2: -2.0, -1: 0.0, 1: 0.0, 2: -2.0, 3: -2.0} + + >>> l._vsids_clause_added({2, -3}) + + >>> l.num_learned_clauses + 1 + >>> l.lit_scores + {-3: -1.0, -2: -2.0, -1: 0.0, 1: 0.0, 2: -1.0, 3: -2.0} + + """ + self.num_learned_clauses += 1 + for lit in cls: + self.lit_scores[lit] += 1 + + ######################## + # Clause Learning # + ######################## + def _simple_add_learned_clause(self, cls): + """Add a new clause to the theory. + + Examples + ======== + + >>> from sympy.logic.algorithms.dpll2 import SATSolver + >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2}, + ... {3, -2}], {1, 2, 3}, set()) + + >>> l.num_learned_clauses + 0 + >>> l.clauses + [[2, -3], [1], [3, -3], [2, -2], [3, -2]] + >>> l.sentinels + {-3: {0, 2}, -2: {3, 4}, 2: {0, 3}, 3: {2, 4}} + + >>> l._simple_add_learned_clause([3]) + + >>> l.clauses + [[2, -3], [1], [3, -3], [2, -2], [3, -2], [3]] + >>> l.sentinels + {-3: {0, 2}, -2: {3, 4}, 2: {0, 3}, 3: {2, 4, 5}} + + """ + cls_num = len(self.clauses) + self.clauses.append(cls) + + for lit in cls: + self.occurrence_count[lit] += 1 + + self.sentinels[cls[0]].add(cls_num) + self.sentinels[cls[-1]].add(cls_num) + + self.heur_clause_added(cls) + + def _simple_compute_conflict(self): + """ Build a clause representing the fact that at least one decision made + so far is wrong. + + Examples + ======== + + >>> from sympy.logic.algorithms.dpll2 import SATSolver + >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2}, + ... {3, -2}], {1, 2, 3}, set()) + >>> next(l._find_model()) + {1: True, 2: False, 3: False} + >>> l._simple_compute_conflict() + [3] + + """ + return [-(level.decision) for level in self.levels[1:]] + + def _simple_clean_clauses(self): + """Clean up learned clauses.""" + pass + + +class Level: + """ + Represents a single level in the DPLL algorithm, and contains + enough information for a sound backtracking procedure. + """ + + def __init__(self, decision, flipped=False): + self.decision = decision + self.var_settings = set() + self.flipped = flipped diff --git a/venv/lib/python3.11/site-packages/sympy/logic/algorithms/lra_theory.py b/venv/lib/python3.11/site-packages/sympy/logic/algorithms/lra_theory.py new file mode 100644 index 0000000000000000000000000000000000000000..1690760d36003aed6866f593120c05a5b8f92c83 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/algorithms/lra_theory.py @@ -0,0 +1,912 @@ +"""Implements "A Fast Linear-Arithmetic Solver for DPLL(T)" + +The LRASolver class defined in this file can be used +in conjunction with a SAT solver to check the +satisfiability of formulas involving inequalities. + +Here's an example of how that would work: + + Suppose you want to check the satisfiability of + the following formula: + + >>> from sympy.core.relational import Eq + >>> from sympy.abc import x, y + >>> f = ((x > 0) | (x < 0)) & (Eq(x, 0) | Eq(y, 1)) & (~Eq(y, 1) | Eq(1, 2)) + + First a preprocessing step should be done on f. During preprocessing, + f should be checked for any predicates such as `Q.prime` that can't be + handled. Also unequality like `~Eq(y, 1)` should be split. + + I should mention that the paper says to split both equalities and + unequality, but this implementation only requires that unequality + be split. + + >>> f = ((x > 0) | (x < 0)) & (Eq(x, 0) | Eq(y, 1)) & ((y < 1) | (y > 1) | Eq(1, 2)) + + Then an LRASolver instance needs to be initialized with this formula. + + >>> from sympy.assumptions.cnf import CNF, EncodedCNF + >>> from sympy.assumptions.ask import Q + >>> from sympy.logic.algorithms.lra_theory import LRASolver + >>> cnf = CNF.from_prop(f) + >>> enc = EncodedCNF() + >>> enc.add_from_cnf(cnf) + >>> lra, conflicts = LRASolver.from_encoded_cnf(enc) + + Any immediate one-lital conflicts clauses will be detected here. + In this example, `~Eq(1, 2)` is one such conflict clause. We'll + want to add it to `f` so that the SAT solver is forced to + assign Eq(1, 2) to False. + + >>> f = f & ~Eq(1, 2) + + Now that the one-literal conflict clauses have been added + and an lra object has been initialized, we can pass `f` + to a SAT solver. The SAT solver will give us a satisfying + assignment such as: + + (1 = 2): False + (y = 1): True + (y < 1): True + (y > 1): True + (x = 0): True + (x < 0): True + (x > 0): True + + Next you would pass this assignment to the LRASolver + which will be able to determine that this particular + assignment is satisfiable or not. + + Note that since EncodedCNF is inherently non-deterministic, + the int each predicate is encoded as is not consistent. As a + result, the code below likely does not reflect the assignment + given above. + + >>> lra.assert_lit(-1) #doctest: +SKIP + >>> lra.assert_lit(2) #doctest: +SKIP + >>> lra.assert_lit(3) #doctest: +SKIP + >>> lra.assert_lit(4) #doctest: +SKIP + >>> lra.assert_lit(5) #doctest: +SKIP + >>> lra.assert_lit(6) #doctest: +SKIP + >>> lra.assert_lit(7) #doctest: +SKIP + >>> is_sat, conflict_or_assignment = lra.check() + + As the particular assignment suggested is not satisfiable, + the LRASolver will return unsat and a conflict clause when + given that assignment. The conflict clause will always be + minimal, but there can be multiple minimal conflict clauses. + One possible conflict clause could be `~(x < 0) | ~(x > 0)`. + + We would then add whatever conflict clause is given to + `f` to prevent the SAT solver from coming up with an + assignment with the same conflicting literals. In this case, + the conflict clause `~(x < 0) | ~(x > 0)` would prevent + any assignment where both (x < 0) and (x > 0) were both + true. + + The SAT solver would then find another assignment + and we would check that assignment with the LRASolver + and so on. Eventually either a satisfying assignment + that the SAT solver and LRASolver agreed on would be found + or enough conflict clauses would be added so that the + boolean formula was unsatisfiable. + + +This implementation is based on [1]_, which includes a +detailed explanation of the algorithm and pseudocode +for the most important functions. + +[1]_ also explains how backtracking and theory propagation +could be implemented to speed up the current implementation, +but these are not currently implemented. + +TODO: + - Handle non-rational real numbers + - Handle positive and negative infinity + - Implement backtracking and theory proposition + - Simplify matrix by removing unused variables using Gaussian elimination + +References +========== + +.. [1] Dutertre, B., de Moura, L.: + A Fast Linear-Arithmetic Solver for DPLL(T) + https://link.springer.com/chapter/10.1007/11817963_11 +""" +from sympy.solvers.solveset import linear_eq_to_matrix +from sympy.matrices.dense import eye +from sympy.assumptions import Predicate +from sympy.assumptions.assume import AppliedPredicate +from sympy.assumptions.ask import Q +from sympy.core import Dummy +from sympy.core.mul import Mul +from sympy.core.add import Add +from sympy.core.relational import Eq, Ne +from sympy.core.sympify import sympify +from sympy.core.singleton import S +from sympy.core.numbers import Rational, oo +from sympy.matrices.dense import Matrix + +class UnhandledInput(Exception): + """ + Raised while creating an LRASolver if non-linearity + or non-rational numbers are present. + """ + +# predicates that LRASolver understands and makes use of +ALLOWED_PRED = {Q.eq, Q.gt, Q.lt, Q.le, Q.ge} + +# if true ~Q.gt(x, y) implies Q.le(x, y) +HANDLE_NEGATION = True + +class LRASolver(): + """ + Linear Arithmetic Solver for DPLL(T) implemented with an algorithm based on + the Dual Simplex method. Uses Bland's pivoting rule to avoid cycling. + + References + ========== + + .. [1] Dutertre, B., de Moura, L.: + A Fast Linear-Arithmetic Solver for DPLL(T) + https://link.springer.com/chapter/10.1007/11817963_11 + """ + + def __init__(self, A, slack_variables, nonslack_variables, enc_to_boundary, s_subs, testing_mode): + """ + Use the "from_encoded_cnf" method to create a new LRASolver. + """ + self.run_checks = testing_mode + self.s_subs = s_subs # used only for test_lra_theory.test_random_problems + + if any(not isinstance(a, Rational) for a in A): + raise UnhandledInput("Non-rational numbers are not handled") + if any(not isinstance(b.bound, Rational) for b in enc_to_boundary.values()): + raise UnhandledInput("Non-rational numbers are not handled") + m, n = len(slack_variables), len(slack_variables)+len(nonslack_variables) + if m != 0: + assert A.shape == (m, n) + if self.run_checks: + assert A[:, n-m:] == -eye(m) + + self.enc_to_boundary = enc_to_boundary # mapping of int to Boundary objects + self.boundary_to_enc = {value: key for key, value in enc_to_boundary.items()} + self.A = A + self.slack = slack_variables + self.nonslack = nonslack_variables + self.all_var = nonslack_variables + slack_variables + + self.slack_set = set(slack_variables) + + self.is_sat = True # While True, all constraints asserted so far are satisfiable + self.result = None # always one of: (True, assignment), (False, conflict clause), None + + @staticmethod + def from_encoded_cnf(encoded_cnf, testing_mode=False): + """ + Creates an LRASolver from an EncodedCNF object + and a list of conflict clauses for propositions + that can be simplified to True or False. + + Parameters + ========== + + encoded_cnf : EncodedCNF + + testing_mode : bool + Setting testing_mode to True enables some slow assert statements + and sorting to reduce nonterministic behavior. + + Returns + ======= + + (lra, conflicts) + + lra : LRASolver + + conflicts : list + Contains a one-literal conflict clause for each proposition + that can be simplified to True or False. + + Example + ======= + + >>> from sympy.core.relational import Eq + >>> from sympy.assumptions.cnf import CNF, EncodedCNF + >>> from sympy.assumptions.ask import Q + >>> from sympy.logic.algorithms.lra_theory import LRASolver + >>> from sympy.abc import x, y, z + >>> phi = (x >= 0) & ((x + y <= 2) | (x + 2 * y - z >= 6)) + >>> phi = phi & (Eq(x + y, 2) | (x + 2 * y - z > 4)) + >>> phi = phi & Q.gt(2, 1) + >>> cnf = CNF.from_prop(phi) + >>> enc = EncodedCNF() + >>> enc.from_cnf(cnf) + >>> lra, conflicts = LRASolver.from_encoded_cnf(enc, testing_mode=True) + >>> lra #doctest: +SKIP + + >>> conflicts #doctest: +SKIP + [[4]] + """ + # This function has three main jobs: + # - raise errors if the input formula is not handled + # - preprocesses the formula into a matrix and single variable constraints + # - create one-literal conflict clauses from predicates that are always True + # or always False such as Q.gt(3, 2) + # + # See the preprocessing section of "A Fast Linear-Arithmetic Solver for DPLL(T)" + # for an explanation of how the formula is converted into a matrix + # and a set of single variable constraints. + + encoding = {} # maps int to boundary + A = [] + + basic = [] + s_count = 0 + s_subs = {} + nonbasic = [] + + if testing_mode: + # sort to reduce nondeterminism + encoded_cnf_items = sorted(encoded_cnf.encoding.items(), key=lambda x: str(x)) + else: + encoded_cnf_items = encoded_cnf.encoding.items() + + empty_var = Dummy() + var_to_lra_var = {} + conflicts = [] + + for prop, enc in encoded_cnf_items: + if isinstance(prop, Predicate): + prop = prop(empty_var) + if not isinstance(prop, AppliedPredicate): + if prop == True: + conflicts.append([enc]) + continue + if prop == False: + conflicts.append([-enc]) + continue + + raise ValueError(f"Unhandled Predicate: {prop}") + + assert prop.function in ALLOWED_PRED + if prop.lhs == S.NaN or prop.rhs == S.NaN: + raise ValueError(f"{prop} contains nan") + if prop.lhs.is_imaginary or prop.rhs.is_imaginary: + raise UnhandledInput(f"{prop} contains an imaginary component") + if prop.lhs == oo or prop.rhs == oo: + raise UnhandledInput(f"{prop} contains infinity") + + prop = _eval_binrel(prop) # simplify variable-less quantities to True / False if possible + if prop == True: + conflicts.append([enc]) + continue + elif prop == False: + conflicts.append([-enc]) + continue + elif prop is None: + raise UnhandledInput(f"{prop} could not be simplified") + + expr = prop.lhs - prop.rhs + if prop.function in [Q.ge, Q.gt]: + expr = -expr + + # expr should be less than (or equal to) 0 + # otherwise prop is False + if prop.function in [Q.le, Q.ge]: + bool = (expr <= 0) + elif prop.function in [Q.lt, Q.gt]: + bool = (expr < 0) + else: + assert prop.function == Q.eq + bool = Eq(expr, 0) + + if bool == True: + conflicts.append([enc]) + continue + elif bool == False: + conflicts.append([-enc]) + continue + + + vars, const = _sep_const_terms(expr) # example: (2x + 3y + 2) --> (2x + 3y), (2) + vars, var_coeff = _sep_const_coeff(vars) # examples: (2x) --> (x, 2); (2x + 3y) --> (2x + 3y), (1) + const = const / var_coeff + + terms = _list_terms(vars) # example: (2x + 3y) --> [2x, 3y] + for term in terms: + term, _ = _sep_const_coeff(term) + assert len(term.free_symbols) > 0 + if term not in var_to_lra_var: + var_to_lra_var[term] = LRAVariable(term) + nonbasic.append(term) + + if len(terms) > 1: + if vars not in s_subs: + s_count += 1 + d = Dummy(f"s{s_count}") + var_to_lra_var[d] = LRAVariable(d) + basic.append(d) + s_subs[vars] = d + A.append(vars - d) + var = s_subs[vars] + else: + var = terms[0] + + assert var_coeff != 0 + + equality = prop.function == Q.eq + upper = var_coeff > 0 if not equality else None + strict = prop.function in [Q.gt, Q.lt] + b = Boundary(var_to_lra_var[var], -const, upper, equality, strict) + encoding[enc] = b + + fs = [v.free_symbols for v in nonbasic + basic] + assert all(len(syms) > 0 for syms in fs) + fs_count = sum(len(syms) for syms in fs) + if len(fs) > 0 and len(set.union(*fs)) < fs_count: + raise UnhandledInput("Nonlinearity is not handled") + + A, _ = linear_eq_to_matrix(A, nonbasic + basic) + nonbasic = [var_to_lra_var[nb] for nb in nonbasic] + basic = [var_to_lra_var[b] for b in basic] + for idx, var in enumerate(nonbasic + basic): + var.col_idx = idx + + return LRASolver(A, basic, nonbasic, encoding, s_subs, testing_mode), conflicts + + def reset_bounds(self): + """ + Resets the state of the LRASolver to before + anything was asserted. + """ + self.result = None + for var in self.all_var: + var.lower = LRARational(-float("inf"), 0) + var.lower_from_eq = False + var.lower_from_neg = False + var.upper = LRARational(float("inf"), 0) + var.upper_from_eq= False + var.lower_from_neg = False + var.assign = LRARational(0, 0) + + def assert_lit(self, enc_constraint): + """ + Assert a literal representing a constraint + and update the internal state accordingly. + + Note that due to peculiarities of this implementation + asserting ~(x > 0) will assert (x <= 0) but asserting + ~Eq(x, 0) will not do anything. + + Parameters + ========== + + enc_constraint : int + A mapping of encodings to constraints + can be found in `self.enc_to_boundary`. + + Returns + ======= + + None or (False, explanation) + + explanation : set of ints + A conflict clause that "explains" why + the literals asserted so far are unsatisfiable. + """ + if abs(enc_constraint) not in self.enc_to_boundary: + return None + + if not HANDLE_NEGATION and enc_constraint < 0: + return None + + boundary = self.enc_to_boundary[abs(enc_constraint)] + sym, c, negated = boundary.var, boundary.bound, enc_constraint < 0 + + if boundary.equality and negated: + return None # negated equality is not handled and should only appear in conflict clauses + + upper = boundary.upper != negated + if boundary.strict != negated: + delta = -1 if upper else 1 + c = LRARational(c, delta) + else: + c = LRARational(c, 0) + + if boundary.equality: + res1 = self._assert_lower(sym, c, from_equality=True, from_neg=negated) + if res1 and res1[0] == False: + res = res1 + else: + res2 = self._assert_upper(sym, c, from_equality=True, from_neg=negated) + res = res2 + elif upper: + res = self._assert_upper(sym, c, from_neg=negated) + else: + res = self._assert_lower(sym, c, from_neg=negated) + + if self.is_sat and sym not in self.slack_set: + self.is_sat = res is None + else: + self.is_sat = False + + return res + + def _assert_upper(self, xi, ci, from_equality=False, from_neg=False): + """ + Adjusts the upper bound on variable xi if the new upper bound is + more limiting. The assignment of variable xi is adjusted to be + within the new bound if needed. + + Also calls `self._update` to update the assignment for slack variables + to keep all equalities satisfied. + """ + if self.result: + assert self.result[0] != False + self.result = None + if ci >= xi.upper: + return None + if ci < xi.lower: + assert (xi.lower[1] >= 0) is True + assert (ci[1] <= 0) is True + + lit1, neg1 = Boundary.from_lower(xi) + + lit2 = Boundary(var=xi, const=ci[0], strict=ci[1] != 0, upper=True, equality=from_equality) + if from_neg: + lit2 = lit2.get_negated() + neg2 = -1 if from_neg else 1 + + conflict = [-neg1*self.boundary_to_enc[lit1], -neg2*self.boundary_to_enc[lit2]] + self.result = False, conflict + return self.result + xi.upper = ci + xi.upper_from_eq = from_equality + xi.upper_from_neg = from_neg + if xi in self.nonslack and xi.assign > ci: + self._update(xi, ci) + + if self.run_checks and all(v.assign[0] != float("inf") and v.assign[0] != -float("inf") + for v in self.all_var): + M = self.A + X = Matrix([v.assign[0] for v in self.all_var]) + assert all(abs(val) < 10 ** (-10) for val in M * X) + + return None + + def _assert_lower(self, xi, ci, from_equality=False, from_neg=False): + """ + Adjusts the lower bound on variable xi if the new lower bound is + more limiting. The assignment of variable xi is adjusted to be + within the new bound if needed. + + Also calls `self._update` to update the assignment for slack variables + to keep all equalities satisfied. + """ + if self.result: + assert self.result[0] != False + self.result = None + if ci <= xi.lower: + return None + if ci > xi.upper: + assert (xi.upper[1] <= 0) is True + assert (ci[1] >= 0) is True + + lit1, neg1 = Boundary.from_upper(xi) + + lit2 = Boundary(var=xi, const=ci[0], strict=ci[1] != 0, upper=False, equality=from_equality) + if from_neg: + lit2 = lit2.get_negated() + neg2 = -1 if from_neg else 1 + + conflict = [-neg1*self.boundary_to_enc[lit1],-neg2*self.boundary_to_enc[lit2]] + self.result = False, conflict + return self.result + xi.lower = ci + xi.lower_from_eq = from_equality + xi.lower_from_neg = from_neg + if xi in self.nonslack and xi.assign < ci: + self._update(xi, ci) + + if self.run_checks and all(v.assign[0] != float("inf") and v.assign[0] != -float("inf") + for v in self.all_var): + M = self.A + X = Matrix([v.assign[0] for v in self.all_var]) + assert all(abs(val) < 10 ** (-10) for val in M * X) + + return None + + def _update(self, xi, v): + """ + Updates all slack variables that have equations that contain + variable xi so that they stay satisfied given xi is equal to v. + """ + i = xi.col_idx + for j, b in enumerate(self.slack): + aji = self.A[j, i] + b.assign = b.assign + (v - xi.assign)*aji + xi.assign = v + + def check(self): + """ + Searches for an assignment that satisfies all constraints + or determines that no such assignment exists and gives + a minimal conflict clause that "explains" why the + constraints are unsatisfiable. + + Returns + ======= + + (True, assignment) or (False, explanation) + + assignment : dict of LRAVariables to values + Assigned values are tuples that represent a rational number + plus some infinatesimal delta. + + explanation : set of ints + """ + if self.is_sat: + return True, {var: var.assign for var in self.all_var} + if self.result: + return self.result + + from sympy.matrices.dense import Matrix + M = self.A.copy() + basic = {s: i for i, s in enumerate(self.slack)} # contains the row index associated with each basic variable + nonbasic = set(self.nonslack) + while True: + if self.run_checks: + # nonbasic variables must always be within bounds + assert all(((nb.assign >= nb.lower) == True) and ((nb.assign <= nb.upper) == True) for nb in nonbasic) + + # assignments for x must always satisfy Ax = 0 + # probably have to turn this off when dealing with strict ineq + if all(v.assign[0] != float("inf") and v.assign[0] != -float("inf") + for v in self.all_var): + X = Matrix([v.assign[0] for v in self.all_var]) + assert all(abs(val) < 10**(-10) for val in M*X) + + # check upper and lower match this format: + # x <= rat + delta iff x < rat + # x >= rat - delta iff x > rat + # this wouldn't make sense: + # x <= rat - delta + # x >= rat + delta + assert all(x.upper[1] <= 0 for x in self.all_var) + assert all(x.lower[1] >= 0 for x in self.all_var) + + cand = [b for b in basic if b.assign < b.lower or b.assign > b.upper] + + if len(cand) == 0: + return True, {var: var.assign for var in self.all_var} + + xi = min(cand, key=lambda v: v.col_idx) # Bland's rule + i = basic[xi] + + if xi.assign < xi.lower: + cand = [nb for nb in nonbasic + if (M[i, nb.col_idx] > 0 and nb.assign < nb.upper) + or (M[i, nb.col_idx] < 0 and nb.assign > nb.lower)] + if len(cand) == 0: + N_plus = [nb for nb in nonbasic if M[i, nb.col_idx] > 0] + N_minus = [nb for nb in nonbasic if M[i, nb.col_idx] < 0] + + conflict = [] + conflict += [Boundary.from_upper(nb) for nb in N_plus] + conflict += [Boundary.from_lower(nb) for nb in N_minus] + conflict.append(Boundary.from_lower(xi)) + conflict = [-neg*self.boundary_to_enc[c] for c, neg in conflict] + return False, conflict + xj = min(cand, key=str) + M = self._pivot_and_update(M, basic, nonbasic, xi, xj, xi.lower) + + if xi.assign > xi.upper: + cand = [nb for nb in nonbasic + if (M[i, nb.col_idx] < 0 and nb.assign < nb.upper) + or (M[i, nb.col_idx] > 0 and nb.assign > nb.lower)] + + if len(cand) == 0: + N_plus = [nb for nb in nonbasic if M[i, nb.col_idx] > 0] + N_minus = [nb for nb in nonbasic if M[i, nb.col_idx] < 0] + + conflict = [] + conflict += [Boundary.from_upper(nb) for nb in N_minus] + conflict += [Boundary.from_lower(nb) for nb in N_plus] + conflict.append(Boundary.from_upper(xi)) + + conflict = [-neg*self.boundary_to_enc[c] for c, neg in conflict] + return False, conflict + xj = min(cand, key=lambda v: v.col_idx) + M = self._pivot_and_update(M, basic, nonbasic, xi, xj, xi.upper) + + def _pivot_and_update(self, M, basic, nonbasic, xi, xj, v): + """ + Pivots basic variable xi with nonbasic variable xj, + and sets value of xi to v and adjusts the values of all basic variables + to keep equations satisfied. + """ + i, j = basic[xi], xj.col_idx + assert M[i, j] != 0 + theta = (v - xi.assign)*(1/M[i, j]) + xi.assign = v + xj.assign = xj.assign + theta + for xk in basic: + if xk != xi: + k = basic[xk] + akj = M[k, j] + xk.assign = xk.assign + theta*akj + # pivot + basic[xj] = basic[xi] + del basic[xi] + nonbasic.add(xi) + nonbasic.remove(xj) + return self._pivot(M, i, j) + + @staticmethod + def _pivot(M, i, j): + """ + Performs a pivot operation about entry i, j of M by performing + a series of row operations on a copy of M and returning the result. + The original M is left unmodified. + + Conceptually, M represents a system of equations and pivoting + can be thought of as rearranging equation i to be in terms of + variable j and then substituting in the rest of the equations + to get rid of other occurances of variable j. + + Example + ======= + + >>> from sympy.matrices.dense import Matrix + >>> from sympy.logic.algorithms.lra_theory import LRASolver + >>> from sympy import var + >>> Matrix(3, 3, var('a:i')) + Matrix([ + [a, b, c], + [d, e, f], + [g, h, i]]) + + This matrix is equivalent to: + 0 = a*x + b*y + c*z + 0 = d*x + e*y + f*z + 0 = g*x + h*y + i*z + + >>> LRASolver._pivot(_, 1, 0) + Matrix([ + [ 0, -a*e/d + b, -a*f/d + c], + [-1, -e/d, -f/d], + [ 0, h - e*g/d, i - f*g/d]]) + + We rearrange equation 1 in terms of variable 0 (x) + and substitute to remove x from the other equations. + + 0 = 0 + (-a*e/d + b)*y + (-a*f/d + c)*z + 0 = -x + (-e/d)*y + (-f/d)*z + 0 = 0 + (h - e*g/d)*y + (i - f*g/d)*z + """ + _, _, Mij = M[i, :], M[:, j], M[i, j] + if Mij == 0: + raise ZeroDivisionError("Tried to pivot about zero-valued entry.") + A = M.copy() + A[i, :] = -A[i, :]/Mij + for row in range(M.shape[0]): + if row != i: + A[row, :] = A[row, :] + A[row, j] * A[i, :] + + return A + + +def _sep_const_coeff(expr): + """ + Example + ======= + + >>> from sympy.logic.algorithms.lra_theory import _sep_const_coeff + >>> from sympy.abc import x, y + >>> _sep_const_coeff(2*x) + (x, 2) + >>> _sep_const_coeff(2*x + 3*y) + (2*x + 3*y, 1) + """ + if isinstance(expr, Add): + return expr, sympify(1) + + if isinstance(expr, Mul): + coeffs = expr.args + else: + coeffs = [expr] + + var, const = [], [] + for c in coeffs: + c = sympify(c) + if len(c.free_symbols)==0: + const.append(c) + else: + var.append(c) + return Mul(*var), Mul(*const) + + +def _list_terms(expr): + if not isinstance(expr, Add): + return [expr] + + return expr.args + + +def _sep_const_terms(expr): + """ + Example + ======= + + >>> from sympy.logic.algorithms.lra_theory import _sep_const_terms + >>> from sympy.abc import x, y + >>> _sep_const_terms(2*x + 3*y + 2) + (2*x + 3*y, 2) + """ + if isinstance(expr, Add): + terms = expr.args + else: + terms = [expr] + + var, const = [], [] + for t in terms: + if len(t.free_symbols) == 0: + const.append(t) + else: + var.append(t) + return sum(var), sum(const) + + +def _eval_binrel(binrel): + """ + Simplify binary relation to True / False if possible. + """ + if not (len(binrel.lhs.free_symbols) == 0 and len(binrel.rhs.free_symbols) == 0): + return binrel + if binrel.function == Q.lt: + res = binrel.lhs < binrel.rhs + elif binrel.function == Q.gt: + res = binrel.lhs > binrel.rhs + elif binrel.function == Q.le: + res = binrel.lhs <= binrel.rhs + elif binrel.function == Q.ge: + res = binrel.lhs >= binrel.rhs + elif binrel.function == Q.eq: + res = Eq(binrel.lhs, binrel.rhs) + elif binrel.function == Q.ne: + res = Ne(binrel.lhs, binrel.rhs) + + if res == True or res == False: + return res + else: + return None + + +class Boundary: + """ + Represents an upper or lower bound or an equality between a symbol + and some constant. + """ + def __init__(self, var, const, upper, equality, strict=None): + if not equality in [True, False]: + assert equality in [True, False] + + + self.var = var + if isinstance(const, tuple): + s = const[1] != 0 + if strict: + assert s == strict + self.bound = const[0] + self.strict = s + else: + self.bound = const + self.strict = strict + self.upper = upper if not equality else None + self.equality = equality + self.strict = strict + assert self.strict is not None + + @staticmethod + def from_upper(var): + neg = -1 if var.upper_from_neg else 1 + b = Boundary(var, var.upper[0], True, var.upper_from_eq, var.upper[1] != 0) + if neg < 0: + b = b.get_negated() + return b, neg + + @staticmethod + def from_lower(var): + neg = -1 if var.lower_from_neg else 1 + b = Boundary(var, var.lower[0], False, var.lower_from_eq, var.lower[1] != 0) + if neg < 0: + b = b.get_negated() + return b, neg + + def get_negated(self): + return Boundary(self.var, self.bound, not self.upper, self.equality, not self.strict) + + def get_inequality(self): + if self.equality: + return Eq(self.var.var, self.bound) + elif self.upper and self.strict: + return self.var.var < self.bound + elif not self.upper and self.strict: + return self.var.var > self.bound + elif self.upper: + return self.var.var <= self.bound + else: + return self.var.var >= self.bound + + def __repr__(self): + return repr("Boundary(" + repr(self.get_inequality()) + ")") + + def __eq__(self, other): + other = (other.var, other.bound, other.strict, other.upper, other.equality) + return (self.var, self.bound, self.strict, self.upper, self.equality) == other + + def __hash__(self): + return hash((self.var, self.bound, self.strict, self.upper, self.equality)) + + +class LRARational(): + """ + Represents a rational plus or minus some amount + of arbitrary small deltas. + """ + def __init__(self, rational, delta): + self.value = (rational, delta) + + def __lt__(self, other): + return self.value < other.value + + def __le__(self, other): + return self.value <= other.value + + def __eq__(self, other): + return self.value == other.value + + def __add__(self, other): + return LRARational(self.value[0] + other.value[0], self.value[1] + other.value[1]) + + def __sub__(self, other): + return LRARational(self.value[0] - other.value[0], self.value[1] - other.value[1]) + + def __mul__(self, other): + assert not isinstance(other, LRARational) + return LRARational(self.value[0] * other, self.value[1] * other) + + def __getitem__(self, index): + return self.value[index] + + def __repr__(self): + return repr(self.value) + + +class LRAVariable(): + """ + Object to keep track of upper and lower bounds + on `self.var`. + """ + def __init__(self, var): + self.upper = LRARational(float("inf"), 0) + self.upper_from_eq = False + self.upper_from_neg = False + self.lower = LRARational(-float("inf"), 0) + self.lower_from_eq = False + self.lower_from_neg = False + self.assign = LRARational(0,0) + self.var = var + self.col_idx = None + + def __repr__(self): + return repr(self.var) + + def __eq__(self, other): + if not isinstance(other, LRAVariable): + return False + return other.var == self.var + + def __hash__(self): + return hash(self.var) diff --git a/venv/lib/python3.11/site-packages/sympy/logic/algorithms/minisat22_wrapper.py b/venv/lib/python3.11/site-packages/sympy/logic/algorithms/minisat22_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..1d5c1f8f14f04309f7cb8197cc05d01a3c108545 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/algorithms/minisat22_wrapper.py @@ -0,0 +1,46 @@ +from sympy.assumptions.cnf import EncodedCNF + +def minisat22_satisfiable(expr, all_models=False, minimal=False): + + if not isinstance(expr, EncodedCNF): + exprs = EncodedCNF() + exprs.add_prop(expr) + expr = exprs + + from pysat.solvers import Minisat22 + + # Return UNSAT when False (encoded as 0) is present in the CNF + if {0} in expr.data: + if all_models: + return (f for f in [False]) + return False + + r = Minisat22(expr.data) + + if minimal: + r.set_phases([-(i+1) for i in range(r.nof_vars())]) + + if not r.solve(): + return False + + if not all_models: + return {expr.symbols[abs(lit) - 1]: lit > 0 for lit in r.get_model()} + + else: + # Make solutions SymPy compatible by creating a generator + def _gen(results): + satisfiable = False + while results.solve(): + sol = results.get_model() + yield {expr.symbols[abs(lit) - 1]: lit > 0 for lit in sol} + if minimal: + results.add_clause([-i for i in sol if i>0]) + else: + results.add_clause([-i for i in sol]) + satisfiable = True + if not satisfiable: + yield False + raise StopIteration + + + return _gen(r) diff --git a/venv/lib/python3.11/site-packages/sympy/logic/algorithms/pycosat_wrapper.py b/venv/lib/python3.11/site-packages/sympy/logic/algorithms/pycosat_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..5ff498b7e3f6b73d95e9b949598ef32df4ecf226 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/algorithms/pycosat_wrapper.py @@ -0,0 +1,41 @@ +from sympy.assumptions.cnf import EncodedCNF + + +def pycosat_satisfiable(expr, all_models=False): + import pycosat + if not isinstance(expr, EncodedCNF): + exprs = EncodedCNF() + exprs.add_prop(expr) + expr = exprs + + # Return UNSAT when False (encoded as 0) is present in the CNF + if {0} in expr.data: + if all_models: + return (f for f in [False]) + return False + + if not all_models: + r = pycosat.solve(expr.data) + result = (r != "UNSAT") + if not result: + return result + return {expr.symbols[abs(lit) - 1]: lit > 0 for lit in r} + else: + r = pycosat.itersolve(expr.data) + result = (r != "UNSAT") + if not result: + return result + + # Make solutions SymPy compatible by creating a generator + def _gen(results): + satisfiable = False + try: + while True: + sol = next(results) + yield {expr.symbols[abs(lit) - 1]: lit > 0 for lit in sol} + satisfiable = True + except StopIteration: + if not satisfiable: + yield False + + return _gen(r) diff --git a/venv/lib/python3.11/site-packages/sympy/logic/algorithms/z3_wrapper.py b/venv/lib/python3.11/site-packages/sympy/logic/algorithms/z3_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..fe44f713a2edfd5286c0f81b737212146766b11b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/algorithms/z3_wrapper.py @@ -0,0 +1,115 @@ +from sympy.printing.smtlib import smtlib_code +from sympy.assumptions.assume import AppliedPredicate +from sympy.assumptions.cnf import EncodedCNF +from sympy.assumptions.ask import Q + +from sympy.core import Add, Mul +from sympy.core.relational import Equality, LessThan, GreaterThan, StrictLessThan, StrictGreaterThan +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import Pow +from sympy.functions.elementary.miscellaneous import Min, Max +from sympy.logic.boolalg import And, Or, Xor, Implies +from sympy.logic.boolalg import Not, ITE +from sympy.assumptions.relation.equality import StrictGreaterThanPredicate, StrictLessThanPredicate, GreaterThanPredicate, LessThanPredicate, EqualityPredicate +from sympy.external import import_module + +def z3_satisfiable(expr, all_models=False): + if not isinstance(expr, EncodedCNF): + exprs = EncodedCNF() + exprs.add_prop(expr) + expr = exprs + + z3 = import_module("z3") + if z3 is None: + raise ImportError("z3 is not installed") + + s = encoded_cnf_to_z3_solver(expr, z3) + + res = str(s.check()) + if res == "unsat": + return False + elif res == "sat": + return z3_model_to_sympy_model(s.model(), expr) + else: + return None + + +def z3_model_to_sympy_model(z3_model, enc_cnf): + rev_enc = {value : key for key, value in enc_cnf.encoding.items()} + return {rev_enc[int(var.name()[1:])] : bool(z3_model[var]) for var in z3_model} + + +def clause_to_assertion(clause): + clause_strings = [f"d{abs(lit)}" if lit > 0 else f"(not d{abs(lit)})" for lit in clause] + return "(assert (or " + " ".join(clause_strings) + "))" + + +def encoded_cnf_to_z3_solver(enc_cnf, z3): + def dummify_bool(pred): + return False + assert isinstance(pred, AppliedPredicate) + + if pred.function in [Q.positive, Q.negative, Q.zero]: + return pred + else: + return False + + s = z3.Solver() + + declarations = [f"(declare-const d{var} Bool)" for var in enc_cnf.variables] + assertions = [clause_to_assertion(clause) for clause in enc_cnf.data] + + symbols = set() + for pred, enc in enc_cnf.encoding.items(): + if not isinstance(pred, AppliedPredicate): + continue + if pred.function not in (Q.gt, Q.lt, Q.ge, Q.le, Q.ne, Q.eq, Q.positive, Q.negative, Q.extended_negative, Q.extended_positive, Q.zero, Q.nonzero, Q.nonnegative, Q.nonpositive, Q.extended_nonzero, Q.extended_nonnegative, Q.extended_nonpositive): + continue + + pred_str = smtlib_code(pred, auto_declare=False, auto_assert=False, known_functions=known_functions) + + symbols |= pred.free_symbols + pred = pred_str + clause = f"(implies d{enc} {pred})" + assertion = "(assert " + clause + ")" + assertions.append(assertion) + + for sym in symbols: + declarations.append(f"(declare-const {sym} Real)") + + declarations = "\n".join(declarations) + assertions = "\n".join(assertions) + s.from_string(declarations) + s.from_string(assertions) + + return s + + +known_functions = { + Add: '+', + Mul: '*', + + Equality: '=', + LessThan: '<=', + GreaterThan: '>=', + StrictLessThan: '<', + StrictGreaterThan: '>', + + EqualityPredicate(): '=', + LessThanPredicate(): '<=', + GreaterThanPredicate(): '>=', + StrictLessThanPredicate(): '<', + StrictGreaterThanPredicate(): '>', + + Abs: 'abs', + Min: 'min', + Max: 'max', + Pow: '^', + + And: 'and', + Or: 'or', + Xor: 'xor', + Not: 'not', + ITE: 'ite', + Implies: '=>', + } diff --git a/venv/lib/python3.11/site-packages/sympy/logic/boolalg.py b/venv/lib/python3.11/site-packages/sympy/logic/boolalg.py new file mode 100644 index 0000000000000000000000000000000000000000..8e11a9b6361ac5d7e355d5d4fb176d8df443e07e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/boolalg.py @@ -0,0 +1,3587 @@ +""" +Boolean algebra module for SymPy +""" + +from __future__ import annotations +from typing import TYPE_CHECKING, overload, Any +from collections.abc import Iterable, Mapping + +from collections import defaultdict +from itertools import chain, combinations, product, permutations +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.cache import cacheit +from sympy.core.containers import Tuple +from sympy.core.decorators import sympify_method_args, sympify_return +from sympy.core.function import Application, Derivative +from sympy.core.kind import BooleanKind, NumberKind +from sympy.core.numbers import Number +from sympy.core.operations import LatticeOp +from sympy.core.singleton import Singleton, S +from sympy.core.sorting import ordered +from sympy.core.sympify import _sympy_converter, _sympify, sympify +from sympy.utilities.iterables import sift, ibin +from sympy.utilities.misc import filldedent + + +def as_Boolean(e): + """Like ``bool``, return the Boolean value of an expression, e, + which can be any instance of :py:class:`~.Boolean` or ``bool``. + + Examples + ======== + + >>> from sympy import true, false, nan + >>> from sympy.logic.boolalg import as_Boolean + >>> from sympy.abc import x + >>> as_Boolean(0) is false + True + >>> as_Boolean(1) is true + True + >>> as_Boolean(x) + x + >>> as_Boolean(2) + Traceback (most recent call last): + ... + TypeError: expecting bool or Boolean, not `2`. + >>> as_Boolean(nan) + Traceback (most recent call last): + ... + TypeError: expecting bool or Boolean, not `nan`. + + """ + from sympy.core.symbol import Symbol + if e == True: + return true + if e == False: + return false + if isinstance(e, Symbol): + z = e.is_zero + if z is None: + return e + return false if z else true + if isinstance(e, Boolean): + return e + raise TypeError('expecting bool or Boolean, not `%s`.' % e) + + +@sympify_method_args +class Boolean(Basic): + """A Boolean object is an object for which logic operations make sense.""" + + __slots__ = () + + kind = BooleanKind + + if TYPE_CHECKING: + + def __new__(cls, *args: Basic | complex) -> Boolean: + ... + + @overload # type: ignore + def subs(self, arg1: Mapping[Basic | complex, Boolean | complex], arg2: None=None) -> Boolean: ... + @overload + def subs(self, arg1: Iterable[tuple[Basic | complex, Boolean | complex]], arg2: None=None, **kwargs: Any) -> Boolean: ... + @overload + def subs(self, arg1: Boolean | complex, arg2: Boolean | complex) -> Boolean: ... + @overload + def subs(self, arg1: Mapping[Basic | complex, Basic | complex], arg2: None=None, **kwargs: Any) -> Basic: ... + @overload + def subs(self, arg1: Iterable[tuple[Basic | complex, Basic | complex]], arg2: None=None, **kwargs: Any) -> Basic: ... + @overload + def subs(self, arg1: Basic | complex, arg2: Basic | complex, **kwargs: Any) -> Basic: ... + + def subs(self, arg1: Mapping[Basic | complex, Basic | complex] | Basic | complex, # type: ignore + arg2: Basic | complex | None = None, **kwargs: Any) -> Basic: + ... + + def simplify(self, **kwargs) -> Boolean: + ... + + @sympify_return([('other', 'Boolean')], NotImplemented) + def __and__(self, other): + return And(self, other) + + __rand__ = __and__ + + @sympify_return([('other', 'Boolean')], NotImplemented) + def __or__(self, other): + return Or(self, other) + + __ror__ = __or__ + + def __invert__(self): + """Overloading for ~""" + return Not(self) + + @sympify_return([('other', 'Boolean')], NotImplemented) + def __rshift__(self, other): + return Implies(self, other) + + @sympify_return([('other', 'Boolean')], NotImplemented) + def __lshift__(self, other): + return Implies(other, self) + + __rrshift__ = __lshift__ + __rlshift__ = __rshift__ + + @sympify_return([('other', 'Boolean')], NotImplemented) + def __xor__(self, other): + return Xor(self, other) + + __rxor__ = __xor__ + + def equals(self, other): + """ + Returns ``True`` if the given formulas have the same truth table. + For two formulas to be equal they must have the same literals. + + Examples + ======== + + >>> from sympy.abc import A, B, C + >>> from sympy import And, Or, Not + >>> (A >> B).equals(~B >> ~A) + True + >>> Not(And(A, B, C)).equals(And(Not(A), Not(B), Not(C))) + False + >>> Not(And(A, Not(A))).equals(Or(B, Not(B))) + False + + """ + from sympy.logic.inference import satisfiable + from sympy.core.relational import Relational + + if self.has(Relational) or other.has(Relational): + raise NotImplementedError('handling of relationals') + return self.atoms() == other.atoms() and \ + not satisfiable(Not(Equivalent(self, other))) + + def to_nnf(self, simplify=True): + # override where necessary + return self + + def as_set(self): + """ + Rewrites Boolean expression in terms of real sets. + + Examples + ======== + + >>> from sympy import Symbol, Eq, Or, And + >>> x = Symbol('x', real=True) + >>> Eq(x, 0).as_set() + {0} + >>> (x > 0).as_set() + Interval.open(0, oo) + >>> And(-2 < x, x < 2).as_set() + Interval.open(-2, 2) + >>> Or(x < -2, 2 < x).as_set() + Union(Interval.open(-oo, -2), Interval.open(2, oo)) + + """ + from sympy.calculus.util import periodicity + from sympy.core.relational import Relational + + free = self.free_symbols + if len(free) == 1: + x = free.pop() + if x.kind is NumberKind: + reps = {} + for r in self.atoms(Relational): + if periodicity(r, x) not in (0, None): + s = r._eval_as_set() + if s in (S.EmptySet, S.UniversalSet, S.Reals): + reps[r] = s.as_relational(x) + continue + raise NotImplementedError(filldedent(''' + as_set is not implemented for relationals + with periodic solutions + ''')) + new = self.subs(reps) + if new.func != self.func: + return new.as_set() # restart with new obj + else: + return new._eval_as_set() + + return self._eval_as_set() + else: + raise NotImplementedError("Sorry, as_set has not yet been" + " implemented for multivariate" + " expressions") + + @property + def binary_symbols(self): + from sympy.core.relational import Eq, Ne + return set().union(*[i.binary_symbols for i in self.args + if i.is_Boolean or i.is_Symbol + or isinstance(i, (Eq, Ne))]) + + def _eval_refine(self, assumptions): + from sympy.assumptions import ask + ret = ask(self, assumptions) + if ret is True: + return true + elif ret is False: + return false + return None + + +class BooleanAtom(Boolean): + """ + Base class of :py:class:`~.BooleanTrue` and :py:class:`~.BooleanFalse`. + """ + is_Boolean = True + is_Atom = True + _op_priority = 11 # higher than Expr + + def simplify(self, *a, **kw): + return self + + def expand(self, *a, **kw): + return self + + @property + def canonical(self): + return self + + def _noop(self, other=None): + raise TypeError('BooleanAtom not allowed in this context.') + + __add__ = _noop + __radd__ = _noop + __sub__ = _noop + __rsub__ = _noop + __mul__ = _noop + __rmul__ = _noop + __pow__ = _noop + __rpow__ = _noop + __truediv__ = _noop + __rtruediv__ = _noop + __mod__ = _noop + __rmod__ = _noop + _eval_power = _noop + + def __lt__(self, other): + raise TypeError(filldedent(''' + A Boolean argument can only be used in + Eq and Ne; all other relationals expect + real expressions. + ''')) + + __le__ = __lt__ + __gt__ = __lt__ + __ge__ = __lt__ + # \\\ + + def _eval_simplify(self, **kwargs): + return self + + +class BooleanTrue(BooleanAtom, metaclass=Singleton): + """ + SymPy version of ``True``, a singleton that can be accessed via ``S.true``. + + This is the SymPy version of ``True``, for use in the logic module. The + primary advantage of using ``true`` instead of ``True`` is that shorthand Boolean + operations like ``~`` and ``>>`` will work as expected on this class, whereas with + True they act bitwise on 1. Functions in the logic module will return this + class when they evaluate to true. + + Notes + ===== + + There is liable to be some confusion as to when ``True`` should + be used and when ``S.true`` should be used in various contexts + throughout SymPy. An important thing to remember is that + ``sympify(True)`` returns ``S.true``. This means that for the most + part, you can just use ``True`` and it will automatically be converted + to ``S.true`` when necessary, similar to how you can generally use 1 + instead of ``S.One``. + + The rule of thumb is: + + "If the boolean in question can be replaced by an arbitrary symbolic + ``Boolean``, like ``Or(x, y)`` or ``x > 1``, use ``S.true``. + Otherwise, use ``True``" + + In other words, use ``S.true`` only on those contexts where the + boolean is being used as a symbolic representation of truth. + For example, if the object ends up in the ``.args`` of any expression, + then it must necessarily be ``S.true`` instead of ``True``, as + elements of ``.args`` must be ``Basic``. On the other hand, + ``==`` is not a symbolic operation in SymPy, since it always returns + ``True`` or ``False``, and does so in terms of structural equality + rather than mathematical, so it should return ``True``. The assumptions + system should use ``True`` and ``False``. Aside from not satisfying + the above rule of thumb, the assumptions system uses a three-valued logic + (``True``, ``False``, ``None``), whereas ``S.true`` and ``S.false`` + represent a two-valued logic. When in doubt, use ``True``. + + "``S.true == True is True``." + + While "``S.true is True``" is ``False``, "``S.true == True``" + is ``True``, so if there is any doubt over whether a function or + expression will return ``S.true`` or ``True``, just use ``==`` + instead of ``is`` to do the comparison, and it will work in either + case. Finally, for boolean flags, it's better to just use ``if x`` + instead of ``if x is True``. To quote PEP 8: + + Do not compare boolean values to ``True`` or ``False`` + using ``==``. + + * Yes: ``if greeting:`` + * No: ``if greeting == True:`` + * Worse: ``if greeting is True:`` + + Examples + ======== + + >>> from sympy import sympify, true, false, Or + >>> sympify(True) + True + >>> _ is True, _ is true + (False, True) + + >>> Or(true, false) + True + >>> _ is true + True + + Python operators give a boolean result for true but a + bitwise result for True + + >>> ~true, ~True # doctest: +SKIP + (False, -2) + >>> true >> true, True >> True + (True, 0) + + See Also + ======== + + sympy.logic.boolalg.BooleanFalse + + """ + def __bool__(self): + return True + + def __hash__(self): + return hash(True) + + def __eq__(self, other): + if other is True: + return True + if other is False: + return False + return super().__eq__(other) + + @property + def negated(self): + return false + + def as_set(self): + """ + Rewrite logic operators and relationals in terms of real sets. + + Examples + ======== + + >>> from sympy import true + >>> true.as_set() + UniversalSet + + """ + return S.UniversalSet + + +class BooleanFalse(BooleanAtom, metaclass=Singleton): + """ + SymPy version of ``False``, a singleton that can be accessed via ``S.false``. + + This is the SymPy version of ``False``, for use in the logic module. The + primary advantage of using ``false`` instead of ``False`` is that shorthand + Boolean operations like ``~`` and ``>>`` will work as expected on this class, + whereas with ``False`` they act bitwise on 0. Functions in the logic module + will return this class when they evaluate to false. + + Notes + ====== + + See the notes section in :py:class:`sympy.logic.boolalg.BooleanTrue` + + Examples + ======== + + >>> from sympy import sympify, true, false, Or + >>> sympify(False) + False + >>> _ is False, _ is false + (False, True) + + >>> Or(true, false) + True + >>> _ is true + True + + Python operators give a boolean result for false but a + bitwise result for False + + >>> ~false, ~False # doctest: +SKIP + (True, -1) + >>> false >> false, False >> False + (True, 0) + + See Also + ======== + + sympy.logic.boolalg.BooleanTrue + + """ + def __bool__(self): + return False + + def __hash__(self): + return hash(False) + + def __eq__(self, other): + if other is True: + return False + if other is False: + return True + return super().__eq__(other) + + @property + def negated(self): + return true + + def as_set(self): + """ + Rewrite logic operators and relationals in terms of real sets. + + Examples + ======== + + >>> from sympy import false + >>> false.as_set() + EmptySet + """ + return S.EmptySet + + +true = BooleanTrue() +false = BooleanFalse() +# We want S.true and S.false to work, rather than S.BooleanTrue and +# S.BooleanFalse, but making the class and instance names the same causes some +# major issues (like the inability to import the class directly from this +# file). +S.true = true +S.false = false + +_sympy_converter[bool] = lambda x: true if x else false + + +class BooleanFunction(Application, Boolean): + """Boolean function is a function that lives in a boolean space + It is used as base class for :py:class:`~.And`, :py:class:`~.Or`, + :py:class:`~.Not`, etc. + """ + is_Boolean = True + + def _eval_simplify(self, **kwargs): + rv = simplify_univariate(self) + if not isinstance(rv, BooleanFunction): + return rv.simplify(**kwargs) + rv = rv.func(*[a.simplify(**kwargs) for a in rv.args]) + return simplify_logic(rv) + + def simplify(self, **kwargs): + from sympy.simplify.simplify import simplify + return simplify(self, **kwargs) + + def __lt__(self, other): + raise TypeError(filldedent(''' + A Boolean argument can only be used in + Eq and Ne; all other relationals expect + real expressions. + ''')) + __le__ = __lt__ + __ge__ = __lt__ + __gt__ = __lt__ + + @classmethod + def binary_check_and_simplify(self, *args): + return [as_Boolean(i) for i in args] + + def to_nnf(self, simplify=True): + return self._to_nnf(*self.args, simplify=simplify) + + def to_anf(self, deep=True): + return self._to_anf(*self.args, deep=deep) + + @classmethod + def _to_nnf(cls, *args, **kwargs): + simplify = kwargs.get('simplify', True) + argset = set() + for arg in args: + if not is_literal(arg): + arg = arg.to_nnf(simplify) + if simplify: + if isinstance(arg, cls): + arg = arg.args + else: + arg = (arg,) + for a in arg: + if Not(a) in argset: + return cls.zero + argset.add(a) + else: + argset.add(arg) + return cls(*argset) + + @classmethod + def _to_anf(cls, *args, **kwargs): + deep = kwargs.get('deep', True) + new_args = [] + for arg in args: + if deep: + if not is_literal(arg) or isinstance(arg, Not): + arg = arg.to_anf(deep=deep) + new_args.append(arg) + return cls(*new_args, remove_true=False) + + # the diff method below is copied from Expr class + def diff(self, *symbols, **assumptions): + assumptions.setdefault("evaluate", True) + return Derivative(self, *symbols, **assumptions) + + def _eval_derivative(self, x): + if x in self.binary_symbols: + from sympy.core.relational import Eq + from sympy.functions.elementary.piecewise import Piecewise + return Piecewise( + (0, Eq(self.subs(x, 0), self.subs(x, 1))), + (1, True)) + elif x in self.free_symbols: + # not implemented, see https://www.encyclopediaofmath.org/ + # index.php/Boolean_differential_calculus + pass + else: + return S.Zero + + +class And(LatticeOp, BooleanFunction): + """ + Logical AND function. + + It evaluates its arguments in order, returning false immediately + when an argument is false and true if they are all true. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import And + >>> x & y + x & y + + Notes + ===== + + The ``&`` operator is provided as a convenience, but note that its use + here is different from its normal use in Python, which is bitwise + and. Hence, ``And(a, b)`` and ``a & b`` will produce different results if + ``a`` and ``b`` are integers. + + >>> And(x, y).subs(x, 1) + y + + """ + zero = false + identity = true + + nargs = None + + if TYPE_CHECKING: + + def __new__(cls, *args: Boolean | bool) -> Boolean: # type: ignore + ... + + @property + def args(self) -> tuple[Boolean, ...]: + ... + + @classmethod + def _new_args_filter(cls, args): + args = BooleanFunction.binary_check_and_simplify(*args) + args = LatticeOp._new_args_filter(args, And) + newargs = [] + rel = set() + for x in ordered(args): + if x.is_Relational: + c = x.canonical + if c in rel: + continue + elif c.negated.canonical in rel: + return [false] + else: + rel.add(c) + newargs.append(x) + return newargs + + def _eval_subs(self, old, new): + args = [] + bad = None + for i in self.args: + try: + i = i.subs(old, new) + except TypeError: + # store TypeError + if bad is None: + bad = i + continue + if i == False: + return false + elif i != True: + args.append(i) + if bad is not None: + # let it raise + bad.subs(old, new) + # If old is And, replace the parts of the arguments with new if all + # are there + if isinstance(old, And): + old_set = set(old.args) + if old_set.issubset(args): + args = set(args) - old_set + args.add(new) + + return self.func(*args) + + def _eval_simplify(self, **kwargs): + from sympy.core.relational import Equality, Relational + from sympy.solvers.solveset import linear_coeffs + # standard simplify + rv = super()._eval_simplify(**kwargs) + if not isinstance(rv, And): + return rv + + # simplify args that are equalities involving + # symbols so x == 0 & x == y -> x==0 & y == 0 + Rel, nonRel = sift(rv.args, lambda i: isinstance(i, Relational), + binary=True) + if not Rel: + return rv + eqs, other = sift(Rel, lambda i: isinstance(i, Equality), binary=True) + + measure = kwargs['measure'] + if eqs: + ratio = kwargs['ratio'] + reps = {} + sifted = {} + # group by length of free symbols + sifted = sift(ordered([ + (i.free_symbols, i) for i in eqs]), + lambda x: len(x[0])) + eqs = [] + nonlineqs = [] + while 1 in sifted: + for free, e in sifted.pop(1): + x = free.pop() + if (e.lhs != x or x in e.rhs.free_symbols) and x not in reps: + try: + m, b = linear_coeffs( + Add(e.lhs, -e.rhs, evaluate=False), x) + enew = e.func(x, -b/m) + if measure(enew) <= ratio*measure(e): + e = enew + else: + eqs.append(e) + continue + except ValueError: + pass + if x in reps: + eqs.append(e.subs(x, reps[x])) + elif e.lhs == x and x not in e.rhs.free_symbols: + reps[x] = e.rhs + eqs.append(e) + else: + # x is not yet identified, but may be later + nonlineqs.append(e) + resifted = defaultdict(list) + for k in sifted: + for f, e in sifted[k]: + e = e.xreplace(reps) + f = e.free_symbols + resifted[len(f)].append((f, e)) + sifted = resifted + for k in sifted: + eqs.extend([e for f, e in sifted[k]]) + nonlineqs = [ei.subs(reps) for ei in nonlineqs] + other = [ei.subs(reps) for ei in other] + rv = rv.func(*([i.canonical for i in (eqs + nonlineqs + other)] + nonRel)) + patterns = _simplify_patterns_and() + threeterm_patterns = _simplify_patterns_and3() + return _apply_patternbased_simplification(rv, patterns, + measure, false, + threeterm_patterns=threeterm_patterns) + + def _eval_as_set(self): + from sympy.sets.sets import Intersection + return Intersection(*[arg.as_set() for arg in self.args]) + + def _eval_rewrite_as_Nor(self, *args, **kwargs): + return Nor(*[Not(arg) for arg in self.args]) + + def to_anf(self, deep=True): + if deep: + result = And._to_anf(*self.args, deep=deep) + return distribute_xor_over_and(result) + return self + + +class Or(LatticeOp, BooleanFunction): + """ + Logical OR function + + It evaluates its arguments in order, returning true immediately + when an argument is true, and false if they are all false. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import Or + >>> x | y + x | y + + Notes + ===== + + The ``|`` operator is provided as a convenience, but note that its use + here is different from its normal use in Python, which is bitwise + or. Hence, ``Or(a, b)`` and ``a | b`` will return different things if + ``a`` and ``b`` are integers. + + >>> Or(x, y).subs(x, 0) + y + + """ + zero = true + identity = false + + if TYPE_CHECKING: + + def __new__(cls, *args: Boolean | bool) -> Boolean: # type: ignore + ... + + @property + def args(self) -> tuple[Boolean, ...]: + ... + + @classmethod + def _new_args_filter(cls, args): + newargs = [] + rel = [] + args = BooleanFunction.binary_check_and_simplify(*args) + for x in args: + if x.is_Relational: + c = x.canonical + if c in rel: + continue + nc = c.negated.canonical + if any(r == nc for r in rel): + return [true] + rel.append(c) + newargs.append(x) + return LatticeOp._new_args_filter(newargs, Or) + + def _eval_subs(self, old, new): + args = [] + bad = None + for i in self.args: + try: + i = i.subs(old, new) + except TypeError: + # store TypeError + if bad is None: + bad = i + continue + if i == True: + return true + elif i != False: + args.append(i) + if bad is not None: + # let it raise + bad.subs(old, new) + # If old is Or, replace the parts of the arguments with new if all + # are there + if isinstance(old, Or): + old_set = set(old.args) + if old_set.issubset(args): + args = set(args) - old_set + args.add(new) + + return self.func(*args) + + def _eval_as_set(self): + from sympy.sets.sets import Union + return Union(*[arg.as_set() for arg in self.args]) + + def _eval_rewrite_as_Nand(self, *args, **kwargs): + return Nand(*[Not(arg) for arg in self.args]) + + def _eval_simplify(self, **kwargs): + from sympy.core.relational import Le, Ge, Eq + lege = self.atoms(Le, Ge) + if lege: + reps = {i: self.func( + Eq(i.lhs, i.rhs), i.strict) for i in lege} + return self.xreplace(reps)._eval_simplify(**kwargs) + # standard simplify + rv = super()._eval_simplify(**kwargs) + if not isinstance(rv, Or): + return rv + patterns = _simplify_patterns_or() + return _apply_patternbased_simplification(rv, patterns, + kwargs['measure'], true) + + def to_anf(self, deep=True): + args = range(1, len(self.args) + 1) + args = (combinations(self.args, j) for j in args) + args = chain.from_iterable(args) # powerset + args = (And(*arg) for arg in args) + args = (to_anf(x, deep=deep) if deep else x for x in args) + return Xor(*list(args), remove_true=False) + + +class Not(BooleanFunction): + """ + Logical Not function (negation) + + + Returns ``true`` if the statement is ``false`` or ``False``. + Returns ``false`` if the statement is ``true`` or ``True``. + + Examples + ======== + + >>> from sympy import Not, And, Or + >>> from sympy.abc import x, A, B + >>> Not(True) + False + >>> Not(False) + True + >>> Not(And(True, False)) + True + >>> Not(Or(True, False)) + False + >>> Not(And(And(True, x), Or(x, False))) + ~x + >>> ~x + ~x + >>> Not(And(Or(A, B), Or(~A, ~B))) + ~((A | B) & (~A | ~B)) + + Notes + ===== + + - The ``~`` operator is provided as a convenience, but note that its use + here is different from its normal use in Python, which is bitwise + not. In particular, ``~a`` and ``Not(a)`` will be different if ``a`` is + an integer. Furthermore, since bools in Python subclass from ``int``, + ``~True`` is the same as ``~1`` which is ``-2``, which has a boolean + value of True. To avoid this issue, use the SymPy boolean types + ``true`` and ``false``. + + - As of Python 3.12, the bitwise not operator ``~`` used on a + Python ``bool`` is deprecated and will emit a warning. + + >>> from sympy import true + >>> ~True # doctest: +SKIP + -2 + >>> ~true + False + + """ + + is_Not = True + + @classmethod + def eval(cls, arg): + if isinstance(arg, Number) or arg in (True, False): + return false if arg else true + if arg.is_Not: + return arg.args[0] + # Simplify Relational objects. + if arg.is_Relational: + return arg.negated + + def _eval_as_set(self): + """ + Rewrite logic operators and relationals in terms of real sets. + + Examples + ======== + + >>> from sympy import Not, Symbol + >>> x = Symbol('x') + >>> Not(x > 0).as_set() + Interval(-oo, 0) + """ + return self.args[0].as_set().complement(S.Reals) + + def to_nnf(self, simplify=True): + if is_literal(self): + return self + + expr = self.args[0] + + func, args = expr.func, expr.args + + if func == And: + return Or._to_nnf(*[Not(arg) for arg in args], simplify=simplify) + + if func == Or: + return And._to_nnf(*[Not(arg) for arg in args], simplify=simplify) + + if func == Implies: + a, b = args + return And._to_nnf(a, Not(b), simplify=simplify) + + if func == Equivalent: + return And._to_nnf(Or(*args), Or(*[Not(arg) for arg in args]), + simplify=simplify) + + if func == Xor: + result = [] + for i in range(1, len(args)+1, 2): + for neg in combinations(args, i): + clause = [Not(s) if s in neg else s for s in args] + result.append(Or(*clause)) + return And._to_nnf(*result, simplify=simplify) + + if func == ITE: + a, b, c = args + return And._to_nnf(Or(a, Not(c)), Or(Not(a), Not(b)), simplify=simplify) + + raise ValueError("Illegal operator %s in expression" % func) + + def to_anf(self, deep=True): + return Xor._to_anf(true, self.args[0], deep=deep) + + +class Xor(BooleanFunction): + """ + Logical XOR (exclusive OR) function. + + + Returns True if an odd number of the arguments are True and the rest are + False. + + Returns False if an even number of the arguments are True and the rest are + False. + + Examples + ======== + + >>> from sympy.logic.boolalg import Xor + >>> from sympy import symbols + >>> x, y = symbols('x y') + >>> Xor(True, False) + True + >>> Xor(True, True) + False + >>> Xor(True, False, True, True, False) + True + >>> Xor(True, False, True, False) + False + >>> x ^ y + x ^ y + + Notes + ===== + + The ``^`` operator is provided as a convenience, but note that its use + here is different from its normal use in Python, which is bitwise xor. In + particular, ``a ^ b`` and ``Xor(a, b)`` will be different if ``a`` and + ``b`` are integers. + + >>> Xor(x, y).subs(y, 0) + x + + """ + def __new__(cls, *args, remove_true=True, **kwargs): + argset = set() + obj = super().__new__(cls, *args, **kwargs) + for arg in obj._args: + if isinstance(arg, Number) or arg in (True, False): + if arg: + arg = true + else: + continue + if isinstance(arg, Xor): + for a in arg.args: + argset.remove(a) if a in argset else argset.add(a) + elif arg in argset: + argset.remove(arg) + else: + argset.add(arg) + rel = [(r, r.canonical, r.negated.canonical) + for r in argset if r.is_Relational] + odd = False # is number of complimentary pairs odd? start 0 -> False + remove = [] + for i, (r, c, nc) in enumerate(rel): + for j in range(i + 1, len(rel)): + rj, cj = rel[j][:2] + if cj == nc: + odd = not odd + break + elif cj == c: + break + else: + continue + remove.append((r, rj)) + if odd: + argset.remove(true) if true in argset else argset.add(true) + for a, b in remove: + argset.remove(a) + argset.remove(b) + if len(argset) == 0: + return false + elif len(argset) == 1: + return argset.pop() + elif True in argset and remove_true: + argset.remove(True) + return Not(Xor(*argset)) + else: + obj._args = tuple(ordered(argset)) + obj._argset = frozenset(argset) + return obj + + # XXX: This should be cached on the object rather than using cacheit + # Maybe it can be computed in __new__? + @property # type: ignore + @cacheit + def args(self): + return tuple(ordered(self._argset)) + + def to_nnf(self, simplify=True): + args = [] + for i in range(0, len(self.args)+1, 2): + for neg in combinations(self.args, i): + clause = [Not(s) if s in neg else s for s in self.args] + args.append(Or(*clause)) + return And._to_nnf(*args, simplify=simplify) + + def _eval_rewrite_as_Or(self, *args, **kwargs): + a = self.args + return Or(*[_convert_to_varsSOP(x, self.args) + for x in _get_odd_parity_terms(len(a))]) + + def _eval_rewrite_as_And(self, *args, **kwargs): + a = self.args + return And(*[_convert_to_varsPOS(x, self.args) + for x in _get_even_parity_terms(len(a))]) + + def _eval_simplify(self, **kwargs): + # as standard simplify uses simplify_logic which writes things as + # And and Or, we only simplify the partial expressions before using + # patterns + rv = self.func(*[a.simplify(**kwargs) for a in self.args]) + rv = rv.to_anf() + if not isinstance(rv, Xor): # This shouldn't really happen here + return rv + patterns = _simplify_patterns_xor() + return _apply_patternbased_simplification(rv, patterns, + kwargs['measure'], None) + + def _eval_subs(self, old, new): + # If old is Xor, replace the parts of the arguments with new if all + # are there + if isinstance(old, Xor): + old_set = set(old.args) + if old_set.issubset(self.args): + args = set(self.args) - old_set + args.add(new) + return self.func(*args) + + +class Nand(BooleanFunction): + """ + Logical NAND function. + + It evaluates its arguments in order, giving True immediately if any + of them are False, and False if they are all True. + + Returns True if any of the arguments are False + Returns False if all arguments are True + + Examples + ======== + + >>> from sympy.logic.boolalg import Nand + >>> from sympy import symbols + >>> x, y = symbols('x y') + >>> Nand(False, True) + True + >>> Nand(True, True) + False + >>> Nand(x, y) + ~(x & y) + + """ + @classmethod + def eval(cls, *args): + return Not(And(*args)) + + +class Nor(BooleanFunction): + """ + Logical NOR function. + + It evaluates its arguments in order, giving False immediately if any + of them are True, and True if they are all False. + + Returns False if any argument is True + Returns True if all arguments are False + + Examples + ======== + + >>> from sympy.logic.boolalg import Nor + >>> from sympy import symbols + >>> x, y = symbols('x y') + + >>> Nor(True, False) + False + >>> Nor(True, True) + False + >>> Nor(False, True) + False + >>> Nor(False, False) + True + >>> Nor(x, y) + ~(x | y) + + """ + @classmethod + def eval(cls, *args): + return Not(Or(*args)) + + +class Xnor(BooleanFunction): + """ + Logical XNOR function. + + Returns False if an odd number of the arguments are True and the rest are + False. + + Returns True if an even number of the arguments are True and the rest are + False. + + Examples + ======== + + >>> from sympy.logic.boolalg import Xnor + >>> from sympy import symbols + >>> x, y = symbols('x y') + >>> Xnor(True, False) + False + >>> Xnor(True, True) + True + >>> Xnor(True, False, True, True, False) + False + >>> Xnor(True, False, True, False) + True + + """ + @classmethod + def eval(cls, *args): + return Not(Xor(*args)) + + +class Implies(BooleanFunction): + r""" + Logical implication. + + A implies B is equivalent to if A then B. Mathematically, it is written + as `A \Rightarrow B` and is equivalent to `\neg A \vee B` or ``~A | B``. + + Accepts two Boolean arguments; A and B. + Returns False if A is True and B is False + Returns True otherwise. + + Examples + ======== + + >>> from sympy.logic.boolalg import Implies + >>> from sympy import symbols + >>> x, y = symbols('x y') + + >>> Implies(True, False) + False + >>> Implies(False, False) + True + >>> Implies(True, True) + True + >>> Implies(False, True) + True + >>> x >> y + Implies(x, y) + >>> y << x + Implies(x, y) + + Notes + ===== + + The ``>>`` and ``<<`` operators are provided as a convenience, but note + that their use here is different from their normal use in Python, which is + bit shifts. Hence, ``Implies(a, b)`` and ``a >> b`` will return different + things if ``a`` and ``b`` are integers. In particular, since Python + considers ``True`` and ``False`` to be integers, ``True >> True`` will be + the same as ``1 >> 1``, i.e., 0, which has a truth value of False. To + avoid this issue, use the SymPy objects ``true`` and ``false``. + + >>> from sympy import true, false + >>> True >> False + 1 + >>> true >> false + False + + """ + @classmethod + def eval(cls, *args): + try: + newargs = [] + for x in args: + if isinstance(x, Number) or x in (0, 1): + newargs.append(bool(x)) + else: + newargs.append(x) + A, B = newargs + except ValueError: + raise ValueError( + "%d operand(s) used for an Implies " + "(pairs are required): %s" % (len(args), str(args))) + if A in (True, False) or B in (True, False): + return Or(Not(A), B) + elif A == B: + return true + elif A.is_Relational and B.is_Relational: + if A.canonical == B.canonical: + return true + if A.negated.canonical == B.canonical: + return B + else: + return Basic.__new__(cls, *args) + + def to_nnf(self, simplify=True): + a, b = self.args + return Or._to_nnf(Not(a), b, simplify=simplify) + + def to_anf(self, deep=True): + a, b = self.args + return Xor._to_anf(true, a, And(a, b), deep=deep) + + +class Equivalent(BooleanFunction): + """ + Equivalence relation. + + ``Equivalent(A, B)`` is True iff A and B are both True or both False. + + Returns True if all of the arguments are logically equivalent. + Returns False otherwise. + + For two arguments, this is equivalent to :py:class:`~.Xnor`. + + Examples + ======== + + >>> from sympy.logic.boolalg import Equivalent, And + >>> from sympy.abc import x + >>> Equivalent(False, False, False) + True + >>> Equivalent(True, False, False) + False + >>> Equivalent(x, And(x, True)) + True + + """ + def __new__(cls, *args, **options): + from sympy.core.relational import Relational + args = [_sympify(arg) for arg in args] + + argset = set(args) + for x in args: + if isinstance(x, Number) or x in [True, False]: # Includes 0, 1 + argset.discard(x) + argset.add(bool(x)) + rel = [] + for r in argset: + if isinstance(r, Relational): + rel.append((r, r.canonical, r.negated.canonical)) + remove = [] + for i, (r, c, nc) in enumerate(rel): + for j in range(i + 1, len(rel)): + rj, cj = rel[j][:2] + if cj == nc: + return false + elif cj == c: + remove.append((r, rj)) + break + for a, b in remove: + argset.remove(a) + argset.remove(b) + argset.add(True) + if len(argset) <= 1: + return true + if True in argset: + argset.discard(True) + return And(*argset) + if False in argset: + argset.discard(False) + return And(*[Not(arg) for arg in argset]) + _args = frozenset(argset) + obj = super().__new__(cls, _args) + obj._argset = _args + return obj + + # XXX: This should be cached on the object rather than using cacheit + # Maybe it can be computed in __new__? + @property # type: ignore + @cacheit + def args(self): + return tuple(ordered(self._argset)) + + def to_nnf(self, simplify=True): + args = [] + for a, b in zip(self.args, self.args[1:]): + args.append(Or(Not(a), b)) + args.append(Or(Not(self.args[-1]), self.args[0])) + return And._to_nnf(*args, simplify=simplify) + + def to_anf(self, deep=True): + a = And(*self.args) + b = And(*[to_anf(Not(arg), deep=False) for arg in self.args]) + b = distribute_xor_over_and(b) + return Xor._to_anf(a, b, deep=deep) + + +class ITE(BooleanFunction): + """ + If-then-else clause. + + ``ITE(A, B, C)`` evaluates and returns the result of B if A is true + else it returns the result of C. All args must be Booleans. + + From a logic gate perspective, ITE corresponds to a 2-to-1 multiplexer, + where A is the select signal. + + Examples + ======== + + >>> from sympy.logic.boolalg import ITE, And, Xor, Or + >>> from sympy.abc import x, y, z + >>> ITE(True, False, True) + False + >>> ITE(Or(True, False), And(True, True), Xor(True, True)) + True + >>> ITE(x, y, z) + ITE(x, y, z) + >>> ITE(True, x, y) + x + >>> ITE(False, x, y) + y + >>> ITE(x, y, y) + y + + Trying to use non-Boolean args will generate a TypeError: + + >>> ITE(True, [], ()) + Traceback (most recent call last): + ... + TypeError: expecting bool, Boolean or ITE, not `[]` + + """ + def __new__(cls, *args, **kwargs): + from sympy.core.relational import Eq, Ne + if len(args) != 3: + raise ValueError('expecting exactly 3 args') + a, b, c = args + # check use of binary symbols + if isinstance(a, (Eq, Ne)): + # in this context, we can evaluate the Eq/Ne + # if one arg is a binary symbol and the other + # is true/false + b, c = map(as_Boolean, (b, c)) + bin_syms = set().union(*[i.binary_symbols for i in (b, c)]) + if len(set(a.args) - bin_syms) == 1: + # one arg is a binary_symbols + _a = a + if a.lhs is true: + a = a.rhs + elif a.rhs is true: + a = a.lhs + elif a.lhs is false: + a = Not(a.rhs) + elif a.rhs is false: + a = Not(a.lhs) + else: + # binary can only equal True or False + a = false + if isinstance(_a, Ne): + a = Not(a) + else: + a, b, c = BooleanFunction.binary_check_and_simplify( + a, b, c) + rv = None + if kwargs.get('evaluate', True): + rv = cls.eval(a, b, c) + if rv is None: + rv = BooleanFunction.__new__(cls, a, b, c, evaluate=False) + return rv + + @classmethod + def eval(cls, *args): + from sympy.core.relational import Eq, Ne + # do the args give a singular result? + a, b, c = args + if isinstance(a, (Ne, Eq)): + _a = a + if true in a.args: + a = a.lhs if a.rhs is true else a.rhs + elif false in a.args: + a = Not(a.lhs) if a.rhs is false else Not(a.rhs) + else: + _a = None + if _a is not None and isinstance(_a, Ne): + a = Not(a) + if a is true: + return b + if a is false: + return c + if b == c: + return b + else: + # or maybe the results allow the answer to be expressed + # in terms of the condition + if b is true and c is false: + return a + if b is false and c is true: + return Not(a) + if [a, b, c] != args: + return cls(a, b, c, evaluate=False) + + def to_nnf(self, simplify=True): + a, b, c = self.args + return And._to_nnf(Or(Not(a), b), Or(a, c), simplify=simplify) + + def _eval_as_set(self): + return self.to_nnf().as_set() + + def _eval_rewrite_as_Piecewise(self, *args, **kwargs): + from sympy.functions.elementary.piecewise import Piecewise + return Piecewise((args[1], args[0]), (args[2], True)) + + +class Exclusive(BooleanFunction): + """ + True if only one or no argument is true. + + ``Exclusive(A, B, C)`` is equivalent to ``~(A & B) & ~(A & C) & ~(B & C)``. + + For two arguments, this is equivalent to :py:class:`~.Xor`. + + Examples + ======== + + >>> from sympy.logic.boolalg import Exclusive + >>> Exclusive(False, False, False) + True + >>> Exclusive(False, True, False) + True + >>> Exclusive(False, True, True) + False + + """ + @classmethod + def eval(cls, *args): + and_args = [] + for a, b in combinations(args, 2): + and_args.append(Not(And(a, b))) + return And(*and_args) + + +# end class definitions. Some useful methods + + +def conjuncts(expr): + """Return a list of the conjuncts in ``expr``. + + Examples + ======== + + >>> from sympy.logic.boolalg import conjuncts + >>> from sympy.abc import A, B + >>> conjuncts(A & B) + frozenset({A, B}) + >>> conjuncts(A | B) + frozenset({A | B}) + + """ + return And.make_args(expr) + + +def disjuncts(expr): + """Return a list of the disjuncts in ``expr``. + + Examples + ======== + + >>> from sympy.logic.boolalg import disjuncts + >>> from sympy.abc import A, B + >>> disjuncts(A | B) + frozenset({A, B}) + >>> disjuncts(A & B) + frozenset({A & B}) + + """ + return Or.make_args(expr) + + +def distribute_and_over_or(expr): + """ + Given a sentence ``expr`` consisting of conjunctions and disjunctions + of literals, return an equivalent sentence in CNF. + + Examples + ======== + + >>> from sympy.logic.boolalg import distribute_and_over_or, And, Or, Not + >>> from sympy.abc import A, B, C + >>> distribute_and_over_or(Or(A, And(Not(B), Not(C)))) + (A | ~B) & (A | ~C) + + """ + return _distribute((expr, And, Or)) + + +def distribute_or_over_and(expr): + """ + Given a sentence ``expr`` consisting of conjunctions and disjunctions + of literals, return an equivalent sentence in DNF. + + Note that the output is NOT simplified. + + Examples + ======== + + >>> from sympy.logic.boolalg import distribute_or_over_and, And, Or, Not + >>> from sympy.abc import A, B, C + >>> distribute_or_over_and(And(Or(Not(A), B), C)) + (B & C) | (C & ~A) + + """ + return _distribute((expr, Or, And)) + + +def distribute_xor_over_and(expr): + """ + Given a sentence ``expr`` consisting of conjunction and + exclusive disjunctions of literals, return an + equivalent exclusive disjunction. + + Note that the output is NOT simplified. + + Examples + ======== + + >>> from sympy.logic.boolalg import distribute_xor_over_and, And, Xor, Not + >>> from sympy.abc import A, B, C + >>> distribute_xor_over_and(And(Xor(Not(A), B), C)) + (B & C) ^ (C & ~A) + """ + return _distribute((expr, Xor, And)) + + +def _distribute(info): + """ + Distributes ``info[1]`` over ``info[2]`` with respect to ``info[0]``. + """ + if isinstance(info[0], info[2]): + for arg in info[0].args: + if isinstance(arg, info[1]): + conj = arg + break + else: + return info[0] + rest = info[2](*[a for a in info[0].args if a is not conj]) + return info[1](*list(map(_distribute, + [(info[2](c, rest), info[1], info[2]) + for c in conj.args])), remove_true=False) + elif isinstance(info[0], info[1]): + return info[1](*list(map(_distribute, + [(x, info[1], info[2]) + for x in info[0].args])), + remove_true=False) + else: + return info[0] + + +def to_anf(expr, deep=True): + r""" + Converts expr to Algebraic Normal Form (ANF). + + ANF is a canonical normal form, which means that two + equivalent formulas will convert to the same ANF. + + A logical expression is in ANF if it has the form + + .. math:: 1 \oplus a \oplus b \oplus ab \oplus abc + + i.e. it can be: + - purely true, + - purely false, + - conjunction of variables, + - exclusive disjunction. + + The exclusive disjunction can only contain true, variables + or conjunction of variables. No negations are permitted. + + If ``deep`` is ``False``, arguments of the boolean + expression are considered variables, i.e. only the + top-level expression is converted to ANF. + + Examples + ======== + >>> from sympy.logic.boolalg import And, Or, Not, Implies, Equivalent + >>> from sympy.logic.boolalg import to_anf + >>> from sympy.abc import A, B, C + >>> to_anf(Not(A)) + A ^ True + >>> to_anf(And(Or(A, B), Not(C))) + A ^ B ^ (A & B) ^ (A & C) ^ (B & C) ^ (A & B & C) + >>> to_anf(Implies(Not(A), Equivalent(B, C)), deep=False) + True ^ ~A ^ (~A & (Equivalent(B, C))) + + """ + expr = sympify(expr) + + if is_anf(expr): + return expr + return expr.to_anf(deep=deep) + + +def to_nnf(expr, simplify=True): + """ + Converts ``expr`` to Negation Normal Form (NNF). + + A logical expression is in NNF if it + contains only :py:class:`~.And`, :py:class:`~.Or` and :py:class:`~.Not`, + and :py:class:`~.Not` is applied only to literals. + If ``simplify`` is ``True``, the result contains no redundant clauses. + + Examples + ======== + + >>> from sympy.abc import A, B, C, D + >>> from sympy.logic.boolalg import Not, Equivalent, to_nnf + >>> to_nnf(Not((~A & ~B) | (C & D))) + (A | B) & (~C | ~D) + >>> to_nnf(Equivalent(A >> B, B >> A)) + (A | ~B | (A & ~B)) & (B | ~A | (B & ~A)) + + """ + if is_nnf(expr, simplify): + return expr + return expr.to_nnf(simplify) + + +def to_cnf(expr, simplify=False, force=False): + """ + Convert a propositional logical sentence ``expr`` to conjunctive normal + form: ``((A | ~B | ...) & (B | C | ...) & ...)``. + If ``simplify`` is ``True``, ``expr`` is evaluated to its simplest CNF + form using the Quine-McCluskey algorithm; this may take a long + time. If there are more than 8 variables the ``force`` flag must be set + to ``True`` to simplify (default is ``False``). + + Examples + ======== + + >>> from sympy.logic.boolalg import to_cnf + >>> from sympy.abc import A, B, D + >>> to_cnf(~(A | B) | D) + (D | ~A) & (D | ~B) + >>> to_cnf((A | B) & (A | ~A), True) + A | B + + """ + expr = sympify(expr) + if not isinstance(expr, BooleanFunction): + return expr + + if simplify: + if not force and len(_find_predicates(expr)) > 8: + raise ValueError(filldedent(''' + To simplify a logical expression with more + than 8 variables may take a long time and requires + the use of `force=True`.''')) + return simplify_logic(expr, 'cnf', True, force=force) + + # Don't convert unless we have to + if is_cnf(expr): + return expr + + expr = eliminate_implications(expr) + res = distribute_and_over_or(expr) + + return res + + +def to_dnf(expr, simplify=False, force=False): + """ + Convert a propositional logical sentence ``expr`` to disjunctive normal + form: ``((A & ~B & ...) | (B & C & ...) | ...)``. + If ``simplify`` is ``True``, ``expr`` is evaluated to its simplest DNF form using + the Quine-McCluskey algorithm; this may take a long + time. If there are more than 8 variables, the ``force`` flag must be set to + ``True`` to simplify (default is ``False``). + + Examples + ======== + + >>> from sympy.logic.boolalg import to_dnf + >>> from sympy.abc import A, B, C + >>> to_dnf(B & (A | C)) + (A & B) | (B & C) + >>> to_dnf((A & B) | (A & ~B) | (B & C) | (~B & C), True) + A | C + + """ + expr = sympify(expr) + if not isinstance(expr, BooleanFunction): + return expr + + if simplify: + if not force and len(_find_predicates(expr)) > 8: + raise ValueError(filldedent(''' + To simplify a logical expression with more + than 8 variables may take a long time and requires + the use of `force=True`.''')) + return simplify_logic(expr, 'dnf', True, force=force) + + # Don't convert unless we have to + if is_dnf(expr): + return expr + + expr = eliminate_implications(expr) + return distribute_or_over_and(expr) + + +def is_anf(expr): + r""" + Checks if ``expr`` is in Algebraic Normal Form (ANF). + + A logical expression is in ANF if it has the form + + .. math:: 1 \oplus a \oplus b \oplus ab \oplus abc + + i.e. it is purely true, purely false, conjunction of + variables or exclusive disjunction. The exclusive + disjunction can only contain true, variables or + conjunction of variables. No negations are permitted. + + Examples + ======== + + >>> from sympy.logic.boolalg import And, Not, Xor, true, is_anf + >>> from sympy.abc import A, B, C + >>> is_anf(true) + True + >>> is_anf(A) + True + >>> is_anf(And(A, B, C)) + True + >>> is_anf(Xor(A, Not(B))) + False + + """ + expr = sympify(expr) + + if is_literal(expr) and not isinstance(expr, Not): + return True + + if isinstance(expr, And): + for arg in expr.args: + if not arg.is_Symbol: + return False + return True + + elif isinstance(expr, Xor): + for arg in expr.args: + if isinstance(arg, And): + for a in arg.args: + if not a.is_Symbol: + return False + elif is_literal(arg): + if isinstance(arg, Not): + return False + else: + return False + return True + + else: + return False + + +def is_nnf(expr, simplified=True): + """ + Checks if ``expr`` is in Negation Normal Form (NNF). + + A logical expression is in NNF if it + contains only :py:class:`~.And`, :py:class:`~.Or` and :py:class:`~.Not`, + and :py:class:`~.Not` is applied only to literals. + If ``simplified`` is ``True``, checks if result contains no redundant clauses. + + Examples + ======== + + >>> from sympy.abc import A, B, C + >>> from sympy.logic.boolalg import Not, is_nnf + >>> is_nnf(A & B | ~C) + True + >>> is_nnf((A | ~A) & (B | C)) + False + >>> is_nnf((A | ~A) & (B | C), False) + True + >>> is_nnf(Not(A & B) | C) + False + >>> is_nnf((A >> B) & (B >> A)) + False + + """ + + expr = sympify(expr) + if is_literal(expr): + return True + + stack = [expr] + + while stack: + expr = stack.pop() + if expr.func in (And, Or): + if simplified: + args = expr.args + for arg in args: + if Not(arg) in args: + return False + stack.extend(expr.args) + + elif not is_literal(expr): + return False + + return True + + +def is_cnf(expr): + """ + Test whether or not an expression is in conjunctive normal form. + + Examples + ======== + + >>> from sympy.logic.boolalg import is_cnf + >>> from sympy.abc import A, B, C + >>> is_cnf(A | B | C) + True + >>> is_cnf(A & B & C) + True + >>> is_cnf((A & B) | C) + False + + """ + return _is_form(expr, And, Or) + + +def is_dnf(expr): + """ + Test whether or not an expression is in disjunctive normal form. + + Examples + ======== + + >>> from sympy.logic.boolalg import is_dnf + >>> from sympy.abc import A, B, C + >>> is_dnf(A | B | C) + True + >>> is_dnf(A & B & C) + True + >>> is_dnf((A & B) | C) + True + >>> is_dnf(A & (B | C)) + False + + """ + return _is_form(expr, Or, And) + + +def _is_form(expr, function1, function2): + """ + Test whether or not an expression is of the required form. + + """ + expr = sympify(expr) + + vals = function1.make_args(expr) if isinstance(expr, function1) else [expr] + for lit in vals: + if isinstance(lit, function2): + vals2 = function2.make_args(lit) if isinstance(lit, function2) else [lit] + for l in vals2: + if is_literal(l) is False: + return False + elif is_literal(lit) is False: + return False + + return True + + +def eliminate_implications(expr): + """ + Change :py:class:`~.Implies` and :py:class:`~.Equivalent` into + :py:class:`~.And`, :py:class:`~.Or`, and :py:class:`~.Not`. + That is, return an expression that is equivalent to ``expr``, but has only + ``&``, ``|``, and ``~`` as logical + operators. + + Examples + ======== + + >>> from sympy.logic.boolalg import Implies, Equivalent, \ + eliminate_implications + >>> from sympy.abc import A, B, C + >>> eliminate_implications(Implies(A, B)) + B | ~A + >>> eliminate_implications(Equivalent(A, B)) + (A | ~B) & (B | ~A) + >>> eliminate_implications(Equivalent(A, B, C)) + (A | ~C) & (B | ~A) & (C | ~B) + + """ + return to_nnf(expr, simplify=False) + + +def is_literal(expr): + """ + Returns True if expr is a literal, else False. + + Examples + ======== + + >>> from sympy import Or, Q + >>> from sympy.abc import A, B + >>> from sympy.logic.boolalg import is_literal + >>> is_literal(A) + True + >>> is_literal(~A) + True + >>> is_literal(Q.zero(A)) + True + >>> is_literal(A + B) + True + >>> is_literal(Or(A, B)) + False + + """ + from sympy.assumptions import AppliedPredicate + + if isinstance(expr, Not): + return is_literal(expr.args[0]) + elif expr in (True, False) or isinstance(expr, AppliedPredicate) or expr.is_Atom: + return True + elif not isinstance(expr, BooleanFunction) and all( + (isinstance(expr, AppliedPredicate) or a.is_Atom) for a in expr.args): + return True + return False + + +def to_int_repr(clauses, symbols): + """ + Takes clauses in CNF format and puts them into an integer representation. + + Examples + ======== + + >>> from sympy.logic.boolalg import to_int_repr + >>> from sympy.abc import x, y + >>> to_int_repr([x | y, y], [x, y]) == [{1, 2}, {2}] + True + + """ + + # Convert the symbol list into a dict + symbols = dict(zip(symbols, range(1, len(symbols) + 1))) + + def append_symbol(arg, symbols): + if isinstance(arg, Not): + return -symbols[arg.args[0]] + else: + return symbols[arg] + + return [{append_symbol(arg, symbols) for arg in Or.make_args(c)} + for c in clauses] + + +def term_to_integer(term): + """ + Return an integer corresponding to the base-2 digits given by *term*. + + Parameters + ========== + + term : a string or list of ones and zeros + + Examples + ======== + + >>> from sympy.logic.boolalg import term_to_integer + >>> term_to_integer([1, 0, 0]) + 4 + >>> term_to_integer('100') + 4 + + """ + + return int(''.join(list(map(str, list(term)))), 2) + + +integer_to_term = ibin # XXX could delete? + + +def truth_table(expr, variables, input=True): + """ + Return a generator of all possible configurations of the input variables, + and the result of the boolean expression for those values. + + Parameters + ========== + + expr : Boolean expression + + variables : list of variables + + input : bool (default ``True``) + Indicates whether to return the input combinations. + + Examples + ======== + + >>> from sympy.logic.boolalg import truth_table + >>> from sympy.abc import x,y + >>> table = truth_table(x >> y, [x, y]) + >>> for t in table: + ... print('{0} -> {1}'.format(*t)) + [0, 0] -> True + [0, 1] -> True + [1, 0] -> False + [1, 1] -> True + + >>> table = truth_table(x | y, [x, y]) + >>> list(table) + [([0, 0], False), ([0, 1], True), ([1, 0], True), ([1, 1], True)] + + If ``input`` is ``False``, ``truth_table`` returns only a list of truth values. + In this case, the corresponding input values of variables can be + deduced from the index of a given output. + + >>> from sympy.utilities.iterables import ibin + >>> vars = [y, x] + >>> values = truth_table(x >> y, vars, input=False) + >>> values = list(values) + >>> values + [True, False, True, True] + + >>> for i, value in enumerate(values): + ... print('{0} -> {1}'.format(list(zip( + ... vars, ibin(i, len(vars)))), value)) + [(y, 0), (x, 0)] -> True + [(y, 0), (x, 1)] -> False + [(y, 1), (x, 0)] -> True + [(y, 1), (x, 1)] -> True + + """ + variables = [sympify(v) for v in variables] + + expr = sympify(expr) + if not isinstance(expr, BooleanFunction) and not is_literal(expr): + return + + table = product((0, 1), repeat=len(variables)) + for term in table: + value = expr.xreplace(dict(zip(variables, term))) + + if input: + yield list(term), value + else: + yield value + + +def _check_pair(minterm1, minterm2): + """ + Checks if a pair of minterms differs by only one bit. If yes, returns + index, else returns `-1`. + """ + # Early termination seems to be faster than list comprehension, + # at least for large examples. + index = -1 + for x, i in enumerate(minterm1): # zip(minterm1, minterm2) is slower + if i != minterm2[x]: + if index == -1: + index = x + else: + return -1 + return index + + +def _convert_to_varsSOP(minterm, variables): + """ + Converts a term in the expansion of a function from binary to its + variable form (for SOP). + """ + temp = [variables[n] if val == 1 else Not(variables[n]) + for n, val in enumerate(minterm) if val != 3] + return And(*temp) + + +def _convert_to_varsPOS(maxterm, variables): + """ + Converts a term in the expansion of a function from binary to its + variable form (for POS). + """ + temp = [variables[n] if val == 0 else Not(variables[n]) + for n, val in enumerate(maxterm) if val != 3] + return Or(*temp) + + +def _convert_to_varsANF(term, variables): + """ + Converts a term in the expansion of a function from binary to its + variable form (for ANF). + + Parameters + ========== + + term : list of 1's and 0's (complementation pattern) + variables : list of variables + + """ + temp = [variables[n] for n, t in enumerate(term) if t == 1] + + if not temp: + return true + + return And(*temp) + + +def _get_odd_parity_terms(n): + """ + Returns a list of lists, with all possible combinations of n zeros and ones + with an odd number of ones. + """ + return [e for e in [ibin(i, n) for i in range(2**n)] if sum(e) % 2 == 1] + + +def _get_even_parity_terms(n): + """ + Returns a list of lists, with all possible combinations of n zeros and ones + with an even number of ones. + """ + return [e for e in [ibin(i, n) for i in range(2**n)] if sum(e) % 2 == 0] + + +def _simplified_pairs(terms): + """ + Reduces a set of minterms, if possible, to a simplified set of minterms + with one less variable in the terms using QM method. + """ + if not terms: + return [] + + simplified_terms = [] + todo = list(range(len(terms))) + + # Count number of ones as _check_pair can only potentially match if there + # is at most a difference of a single one + termdict = defaultdict(list) + for n, term in enumerate(terms): + ones = sum(1 for t in term if t == 1) + termdict[ones].append(n) + + variables = len(terms[0]) + for k in range(variables): + for i in termdict[k]: + for j in termdict[k+1]: + index = _check_pair(terms[i], terms[j]) + if index != -1: + # Mark terms handled + todo[i] = todo[j] = None + # Copy old term + newterm = terms[i][:] + # Set differing position to don't care + newterm[index] = 3 + # Add if not already there + if newterm not in simplified_terms: + simplified_terms.append(newterm) + + if simplified_terms: + # Further simplifications only among the new terms + simplified_terms = _simplified_pairs(simplified_terms) + + # Add remaining, non-simplified, terms + simplified_terms.extend([terms[i] for i in todo if i is not None]) + return simplified_terms + + +def _rem_redundancy(l1, terms): + """ + After the truth table has been sufficiently simplified, use the prime + implicant table method to recognize and eliminate redundant pairs, + and return the essential arguments. + """ + + if not terms: + return [] + + nterms = len(terms) + nl1 = len(l1) + + # Create dominating matrix + dommatrix = [[0]*nl1 for n in range(nterms)] + colcount = [0]*nl1 + rowcount = [0]*nterms + for primei, prime in enumerate(l1): + for termi, term in enumerate(terms): + # Check prime implicant covering term + if all(t == 3 or t == mt for t, mt in zip(prime, term)): + dommatrix[termi][primei] = 1 + colcount[primei] += 1 + rowcount[termi] += 1 + + # Keep track if anything changed + anythingchanged = True + # Then, go again + while anythingchanged: + anythingchanged = False + + for rowi in range(nterms): + # Still non-dominated? + if rowcount[rowi]: + row = dommatrix[rowi] + for row2i in range(nterms): + # Still non-dominated? + if rowi != row2i and rowcount[rowi] and (rowcount[rowi] <= rowcount[row2i]): + row2 = dommatrix[row2i] + if all(row2[n] >= row[n] for n in range(nl1)): + # row2 dominating row, remove row2 + rowcount[row2i] = 0 + anythingchanged = True + for primei, prime in enumerate(row2): + if prime: + # Make corresponding entry 0 + dommatrix[row2i][primei] = 0 + colcount[primei] -= 1 + + colcache = {} + + for coli in range(nl1): + # Still non-dominated? + if colcount[coli]: + if coli in colcache: + col = colcache[coli] + else: + col = [dommatrix[i][coli] for i in range(nterms)] + colcache[coli] = col + for col2i in range(nl1): + # Still non-dominated? + if coli != col2i and colcount[col2i] and (colcount[coli] >= colcount[col2i]): + if col2i in colcache: + col2 = colcache[col2i] + else: + col2 = [dommatrix[i][col2i] for i in range(nterms)] + colcache[col2i] = col2 + if all(col[n] >= col2[n] for n in range(nterms)): + # col dominating col2, remove col2 + colcount[col2i] = 0 + anythingchanged = True + for termi, term in enumerate(col2): + if term and dommatrix[termi][col2i]: + # Make corresponding entry 0 + dommatrix[termi][col2i] = 0 + rowcount[termi] -= 1 + + if not anythingchanged: + # Heuristically select the prime implicant covering most terms + maxterms = 0 + bestcolidx = -1 + for coli in range(nl1): + s = colcount[coli] + if s > maxterms: + bestcolidx = coli + maxterms = s + + # In case we found a prime implicant covering at least two terms + if bestcolidx != -1 and maxterms > 1: + for primei, prime in enumerate(l1): + if primei != bestcolidx: + for termi, term in enumerate(colcache[bestcolidx]): + if term and dommatrix[termi][primei]: + # Make corresponding entry 0 + dommatrix[termi][primei] = 0 + anythingchanged = True + rowcount[termi] -= 1 + colcount[primei] -= 1 + + return [l1[i] for i in range(nl1) if colcount[i]] + + +def _input_to_binlist(inputlist, variables): + binlist = [] + bits = len(variables) + for val in inputlist: + if isinstance(val, int): + binlist.append(ibin(val, bits)) + elif isinstance(val, dict): + nonspecvars = list(variables) + for key in val.keys(): + nonspecvars.remove(key) + for t in product((0, 1), repeat=len(nonspecvars)): + d = dict(zip(nonspecvars, t)) + d.update(val) + binlist.append([d[v] for v in variables]) + elif isinstance(val, (list, tuple)): + if len(val) != bits: + raise ValueError("Each term must contain {bits} bits as there are" + "\n{bits} variables (or be an integer)." + "".format(bits=bits)) + binlist.append(list(val)) + else: + raise TypeError("A term list can only contain lists," + " ints or dicts.") + return binlist + + +def SOPform(variables, minterms, dontcares=None): + """ + The SOPform function uses simplified_pairs and a redundant group- + eliminating algorithm to convert the list of all input combos that + generate '1' (the minterms) into the smallest sum-of-products form. + + The variables must be given as the first argument. + + Return a logical :py:class:`~.Or` function (i.e., the "sum of products" or + "SOP" form) that gives the desired outcome. If there are inputs that can + be ignored, pass them as a list, too. + + The result will be one of the (perhaps many) functions that satisfy + the conditions. + + Examples + ======== + + >>> from sympy.logic import SOPform + >>> from sympy import symbols + >>> w, x, y, z = symbols('w x y z') + >>> minterms = [[0, 0, 0, 1], [0, 0, 1, 1], + ... [0, 1, 1, 1], [1, 0, 1, 1], [1, 1, 1, 1]] + >>> dontcares = [[0, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 1]] + >>> SOPform([w, x, y, z], minterms, dontcares) + (y & z) | (~w & ~x) + + The terms can also be represented as integers: + + >>> minterms = [1, 3, 7, 11, 15] + >>> dontcares = [0, 2, 5] + >>> SOPform([w, x, y, z], minterms, dontcares) + (y & z) | (~w & ~x) + + They can also be specified using dicts, which does not have to be fully + specified: + + >>> minterms = [{w: 0, x: 1}, {y: 1, z: 1, x: 0}] + >>> SOPform([w, x, y, z], minterms) + (x & ~w) | (y & z & ~x) + + Or a combination: + + >>> minterms = [4, 7, 11, [1, 1, 1, 1]] + >>> dontcares = [{w : 0, x : 0, y: 0}, 5] + >>> SOPform([w, x, y, z], minterms, dontcares) + (w & y & z) | (~w & ~y) | (x & z & ~w) + + See also + ======== + + POSform + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Quine-McCluskey_algorithm + .. [2] https://en.wikipedia.org/wiki/Don%27t-care_term + + """ + if not minterms: + return false + + variables = tuple(map(sympify, variables)) + + + minterms = _input_to_binlist(minterms, variables) + dontcares = _input_to_binlist((dontcares or []), variables) + for d in dontcares: + if d in minterms: + raise ValueError('%s in minterms is also in dontcares' % d) + + return _sop_form(variables, minterms, dontcares) + + +def _sop_form(variables, minterms, dontcares): + new = _simplified_pairs(minterms + dontcares) + essential = _rem_redundancy(new, minterms) + return Or(*[_convert_to_varsSOP(x, variables) for x in essential]) + + +def POSform(variables, minterms, dontcares=None): + """ + The POSform function uses simplified_pairs and a redundant-group + eliminating algorithm to convert the list of all input combinations + that generate '1' (the minterms) into the smallest product-of-sums form. + + The variables must be given as the first argument. + + Return a logical :py:class:`~.And` function (i.e., the "product of sums" + or "POS" form) that gives the desired outcome. If there are inputs that can + be ignored, pass them as a list, too. + + The result will be one of the (perhaps many) functions that satisfy + the conditions. + + Examples + ======== + + >>> from sympy.logic import POSform + >>> from sympy import symbols + >>> w, x, y, z = symbols('w x y z') + >>> minterms = [[0, 0, 0, 1], [0, 0, 1, 1], [0, 1, 1, 1], + ... [1, 0, 1, 1], [1, 1, 1, 1]] + >>> dontcares = [[0, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 1]] + >>> POSform([w, x, y, z], minterms, dontcares) + z & (y | ~w) + + The terms can also be represented as integers: + + >>> minterms = [1, 3, 7, 11, 15] + >>> dontcares = [0, 2, 5] + >>> POSform([w, x, y, z], minterms, dontcares) + z & (y | ~w) + + They can also be specified using dicts, which does not have to be fully + specified: + + >>> minterms = [{w: 0, x: 1}, {y: 1, z: 1, x: 0}] + >>> POSform([w, x, y, z], minterms) + (x | y) & (x | z) & (~w | ~x) + + Or a combination: + + >>> minterms = [4, 7, 11, [1, 1, 1, 1]] + >>> dontcares = [{w : 0, x : 0, y: 0}, 5] + >>> POSform([w, x, y, z], minterms, dontcares) + (w | x) & (y | ~w) & (z | ~y) + + See also + ======== + + SOPform + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Quine-McCluskey_algorithm + .. [2] https://en.wikipedia.org/wiki/Don%27t-care_term + + """ + if not minterms: + return false + + variables = tuple(map(sympify, variables)) + minterms = _input_to_binlist(minterms, variables) + dontcares = _input_to_binlist((dontcares or []), variables) + for d in dontcares: + if d in minterms: + raise ValueError('%s in minterms is also in dontcares' % d) + + maxterms = [] + for t in product((0, 1), repeat=len(variables)): + t = list(t) + if (t not in minterms) and (t not in dontcares): + maxterms.append(t) + + new = _simplified_pairs(maxterms + dontcares) + essential = _rem_redundancy(new, maxterms) + return And(*[_convert_to_varsPOS(x, variables) for x in essential]) + + +def ANFform(variables, truthvalues): + """ + The ANFform function converts the list of truth values to + Algebraic Normal Form (ANF). + + The variables must be given as the first argument. + + Return True, False, logical :py:class:`~.And` function (i.e., the + "Zhegalkin monomial") or logical :py:class:`~.Xor` function (i.e., + the "Zhegalkin polynomial"). When True and False + are represented by 1 and 0, respectively, then + :py:class:`~.And` is multiplication and :py:class:`~.Xor` is addition. + + Formally a "Zhegalkin monomial" is the product (logical + And) of a finite set of distinct variables, including + the empty set whose product is denoted 1 (True). + A "Zhegalkin polynomial" is the sum (logical Xor) of a + set of Zhegalkin monomials, with the empty set denoted + by 0 (False). + + Parameters + ========== + + variables : list of variables + truthvalues : list of 1's and 0's (result column of truth table) + + Examples + ======== + >>> from sympy.logic.boolalg import ANFform + >>> from sympy.abc import x, y + >>> ANFform([x], [1, 0]) + x ^ True + >>> ANFform([x, y], [0, 1, 1, 1]) + x ^ y ^ (x & y) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Zhegalkin_polynomial + + """ + + n_vars = len(variables) + n_values = len(truthvalues) + + if n_values != 2 ** n_vars: + raise ValueError("The number of truth values must be equal to 2^%d, " + "got %d" % (n_vars, n_values)) + + variables = tuple(map(sympify, variables)) + + coeffs = anf_coeffs(truthvalues) + terms = [] + + for i, t in enumerate(product((0, 1), repeat=n_vars)): + if coeffs[i] == 1: + terms.append(t) + + return Xor(*[_convert_to_varsANF(x, variables) for x in terms], + remove_true=False) + + +def anf_coeffs(truthvalues): + """ + Convert a list of truth values of some boolean expression + to the list of coefficients of the polynomial mod 2 (exclusive + disjunction) representing the boolean expression in ANF + (i.e., the "Zhegalkin polynomial"). + + There are `2^n` possible Zhegalkin monomials in `n` variables, since + each monomial is fully specified by the presence or absence of + each variable. + + We can enumerate all the monomials. For example, boolean + function with four variables ``(a, b, c, d)`` can contain + up to `2^4 = 16` monomials. The 13-th monomial is the + product ``a & b & d``, because 13 in binary is 1, 1, 0, 1. + + A given monomial's presence or absence in a polynomial corresponds + to that monomial's coefficient being 1 or 0 respectively. + + Examples + ======== + >>> from sympy.logic.boolalg import anf_coeffs, bool_monomial, Xor + >>> from sympy.abc import a, b, c + >>> truthvalues = [0, 1, 1, 0, 0, 1, 0, 1] + >>> coeffs = anf_coeffs(truthvalues) + >>> coeffs + [0, 1, 1, 0, 0, 0, 1, 0] + >>> polynomial = Xor(*[ + ... bool_monomial(k, [a, b, c]) + ... for k, coeff in enumerate(coeffs) if coeff == 1 + ... ]) + >>> polynomial + b ^ c ^ (a & b) + + """ + + s = '{:b}'.format(len(truthvalues)) + n = len(s) - 1 + + if len(truthvalues) != 2**n: + raise ValueError("The number of truth values must be a power of two, " + "got %d" % len(truthvalues)) + + coeffs = [[v] for v in truthvalues] + + for i in range(n): + tmp = [] + for j in range(2 ** (n-i-1)): + tmp.append(coeffs[2*j] + + list(map(lambda x, y: x^y, coeffs[2*j], coeffs[2*j+1]))) + coeffs = tmp + + return coeffs[0] + + +def bool_minterm(k, variables): + """ + Return the k-th minterm. + + Minterms are numbered by a binary encoding of the complementation + pattern of the variables. This convention assigns the value 1 to + the direct form and 0 to the complemented form. + + Parameters + ========== + + k : int or list of 1's and 0's (complementation pattern) + variables : list of variables + + Examples + ======== + + >>> from sympy.logic.boolalg import bool_minterm + >>> from sympy.abc import x, y, z + >>> bool_minterm([1, 0, 1], [x, y, z]) + x & z & ~y + >>> bool_minterm(6, [x, y, z]) + x & y & ~z + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Canonical_normal_form#Indexing_minterms + + """ + if isinstance(k, int): + k = ibin(k, len(variables)) + variables = tuple(map(sympify, variables)) + return _convert_to_varsSOP(k, variables) + + +def bool_maxterm(k, variables): + """ + Return the k-th maxterm. + + Each maxterm is assigned an index based on the opposite + conventional binary encoding used for minterms. The maxterm + convention assigns the value 0 to the direct form and 1 to + the complemented form. + + Parameters + ========== + + k : int or list of 1's and 0's (complementation pattern) + variables : list of variables + + Examples + ======== + >>> from sympy.logic.boolalg import bool_maxterm + >>> from sympy.abc import x, y, z + >>> bool_maxterm([1, 0, 1], [x, y, z]) + y | ~x | ~z + >>> bool_maxterm(6, [x, y, z]) + z | ~x | ~y + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Canonical_normal_form#Indexing_maxterms + + """ + if isinstance(k, int): + k = ibin(k, len(variables)) + variables = tuple(map(sympify, variables)) + return _convert_to_varsPOS(k, variables) + + +def bool_monomial(k, variables): + """ + Return the k-th monomial. + + Monomials are numbered by a binary encoding of the presence and + absences of the variables. This convention assigns the value + 1 to the presence of variable and 0 to the absence of variable. + + Each boolean function can be uniquely represented by a + Zhegalkin Polynomial (Algebraic Normal Form). The Zhegalkin + Polynomial of the boolean function with `n` variables can contain + up to `2^n` monomials. We can enumerate all the monomials. + Each monomial is fully specified by the presence or absence + of each variable. + + For example, boolean function with four variables ``(a, b, c, d)`` + can contain up to `2^4 = 16` monomials. The 13-th monomial is the + product ``a & b & d``, because 13 in binary is 1, 1, 0, 1. + + Parameters + ========== + + k : int or list of 1's and 0's + variables : list of variables + + Examples + ======== + >>> from sympy.logic.boolalg import bool_monomial + >>> from sympy.abc import x, y, z + >>> bool_monomial([1, 0, 1], [x, y, z]) + x & z + >>> bool_monomial(6, [x, y, z]) + x & y + + """ + if isinstance(k, int): + k = ibin(k, len(variables)) + variables = tuple(map(sympify, variables)) + return _convert_to_varsANF(k, variables) + + +def _find_predicates(expr): + """Helper to find logical predicates in BooleanFunctions. + + A logical predicate is defined here as anything within a BooleanFunction + that is not a BooleanFunction itself. + + """ + if not isinstance(expr, BooleanFunction): + return {expr} + return set().union(*(map(_find_predicates, expr.args))) + + +def simplify_logic(expr, form=None, deep=True, force=False, dontcare=None): + """ + This function simplifies a boolean function to its simplified version + in SOP or POS form. The return type is an :py:class:`~.Or` or + :py:class:`~.And` object in SymPy. + + Parameters + ========== + + expr : Boolean + + form : string (``'cnf'`` or ``'dnf'``) or ``None`` (default). + If ``'cnf'`` or ``'dnf'``, the simplest expression in the corresponding + normal form is returned; if ``None``, the answer is returned + according to the form with fewest args (in CNF by default). + + deep : bool (default ``True``) + Indicates whether to recursively simplify any + non-boolean functions contained within the input. + + force : bool (default ``False``) + As the simplifications require exponential time in the number + of variables, there is by default a limit on expressions with + 8 variables. When the expression has more than 8 variables + only symbolical simplification (controlled by ``deep``) is + made. By setting ``force`` to ``True``, this limit is removed. Be + aware that this can lead to very long simplification times. + + dontcare : Boolean + Optimize expression under the assumption that inputs where this + expression is true are don't care. This is useful in e.g. Piecewise + conditions, where later conditions do not need to consider inputs that + are converted by previous conditions. For example, if a previous + condition is ``And(A, B)``, the simplification of expr can be made + with don't cares for ``And(A, B)``. + + Examples + ======== + + >>> from sympy.logic import simplify_logic + >>> from sympy.abc import x, y, z + >>> b = (~x & ~y & ~z) | ( ~x & ~y & z) + >>> simplify_logic(b) + ~x & ~y + >>> simplify_logic(x | y, dontcare=y) + x + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Don%27t-care_term + + """ + + if form not in (None, 'cnf', 'dnf'): + raise ValueError("form can be cnf or dnf only") + expr = sympify(expr) + # check for quick exit if form is given: right form and all args are + # literal and do not involve Not + if form: + form_ok = False + if form == 'cnf': + form_ok = is_cnf(expr) + elif form == 'dnf': + form_ok = is_dnf(expr) + + if form_ok and all(is_literal(a) + for a in expr.args): + return expr + from sympy.core.relational import Relational + if deep: + variables = expr.atoms(Relational) + from sympy.simplify.simplify import simplify + s = tuple(map(simplify, variables)) + expr = expr.xreplace(dict(zip(variables, s))) + if not isinstance(expr, BooleanFunction): + return expr + # Replace Relationals with Dummys to possibly + # reduce the number of variables + repl = {} + undo = {} + from sympy.core.symbol import Dummy + variables = expr.atoms(Relational) + if dontcare is not None: + dontcare = sympify(dontcare) + variables.update(dontcare.atoms(Relational)) + while variables: + var = variables.pop() + if var.is_Relational: + d = Dummy() + undo[d] = var + repl[var] = d + nvar = var.negated + if nvar in variables: + repl[nvar] = Not(d) + variables.remove(nvar) + + expr = expr.xreplace(repl) + + if dontcare is not None: + dontcare = dontcare.xreplace(repl) + + # Get new variables after replacing + variables = _find_predicates(expr) + if not force and len(variables) > 8: + return expr.xreplace(undo) + if dontcare is not None: + # Add variables from dontcare + dcvariables = _find_predicates(dontcare) + variables.update(dcvariables) + # if too many restore to variables only + if not force and len(variables) > 8: + variables = _find_predicates(expr) + dontcare = None + # group into constants and variable values + c, v = sift(ordered(variables), lambda x: x in (True, False), binary=True) + variables = c + v + # standardize constants to be 1 or 0 in keeping with truthtable + c = [1 if i == True else 0 for i in c] + truthtable = _get_truthtable(v, expr, c) + if dontcare is not None: + dctruthtable = _get_truthtable(v, dontcare, c) + truthtable = [t for t in truthtable if t not in dctruthtable] + else: + dctruthtable = [] + big = len(truthtable) >= (2 ** (len(variables) - 1)) + if form == 'dnf' or form is None and big: + return _sop_form(variables, truthtable, dctruthtable).xreplace(undo) + return POSform(variables, truthtable, dctruthtable).xreplace(undo) + + +def _get_truthtable(variables, expr, const): + """ Return a list of all combinations leading to a True result for ``expr``. + """ + _variables = variables.copy() + def _get_tt(inputs): + if _variables: + v = _variables.pop() + tab = [[i[0].xreplace({v: false}), [0] + i[1]] for i in inputs if i[0] is not false] + tab.extend([[i[0].xreplace({v: true}), [1] + i[1]] for i in inputs if i[0] is not false]) + return _get_tt(tab) + return inputs + res = [const + k[1] for k in _get_tt([[expr, []]]) if k[0]] + if res == [[]]: + return [] + else: + return res + + +def _finger(eq): + """ + Assign a 5-item fingerprint to each symbol in the equation: + [ + # of times it appeared as a Symbol; + # of times it appeared as a Not(symbol); + # of times it appeared as a Symbol in an And or Or; + # of times it appeared as a Not(Symbol) in an And or Or; + a sorted tuple of tuples, (i, j, k), where i is the number of arguments + in an And or Or with which it appeared as a Symbol, and j is + the number of arguments that were Not(Symbol); k is the number + of times that (i, j) was seen. + ] + + Examples + ======== + + >>> from sympy.logic.boolalg import _finger as finger + >>> from sympy import And, Or, Not, Xor, to_cnf, symbols + >>> from sympy.abc import a, b, x, y + >>> eq = Or(And(Not(y), a), And(Not(y), b), And(x, y)) + >>> dict(finger(eq)) + {(0, 0, 1, 0, ((2, 0, 1),)): [x], + (0, 0, 1, 0, ((2, 1, 1),)): [a, b], + (0, 0, 1, 2, ((2, 0, 1),)): [y]} + >>> dict(finger(x & ~y)) + {(0, 1, 0, 0, ()): [y], (1, 0, 0, 0, ()): [x]} + + In the following, the (5, 2, 6) means that there were 6 Or + functions in which a symbol appeared as itself amongst 5 arguments in + which there were also 2 negated symbols, e.g. ``(a0 | a1 | a2 | ~a3 | ~a4)`` + is counted once for a0, a1 and a2. + + >>> dict(finger(to_cnf(Xor(*symbols('a:5'))))) + {(0, 0, 8, 8, ((5, 0, 1), (5, 2, 6), (5, 4, 1))): [a0, a1, a2, a3, a4]} + + The equation must not have more than one level of nesting: + + >>> dict(finger(And(Or(x, y), y))) + {(0, 0, 1, 0, ((2, 0, 1),)): [x], (1, 0, 1, 0, ((2, 0, 1),)): [y]} + >>> dict(finger(And(Or(x, And(a, x)), y))) + Traceback (most recent call last): + ... + NotImplementedError: unexpected level of nesting + + So y and x have unique fingerprints, but a and b do not. + """ + f = eq.free_symbols + d = dict(list(zip(f, [[0]*4 + [defaultdict(int)] for fi in f]))) + for a in eq.args: + if a.is_Symbol: + d[a][0] += 1 + elif a.is_Not: + d[a.args[0]][1] += 1 + else: + o = len(a.args), sum(isinstance(ai, Not) for ai in a.args) + for ai in a.args: + if ai.is_Symbol: + d[ai][2] += 1 + d[ai][-1][o] += 1 + elif ai.is_Not: + d[ai.args[0]][3] += 1 + else: + raise NotImplementedError('unexpected level of nesting') + inv = defaultdict(list) + for k, v in ordered(iter(d.items())): + v[-1] = tuple(sorted([i + (j,) for i, j in v[-1].items()])) + inv[tuple(v)].append(k) + return inv + + +def bool_map(bool1, bool2): + """ + Return the simplified version of *bool1*, and the mapping of variables + that makes the two expressions *bool1* and *bool2* represent the same + logical behaviour for some correspondence between the variables + of each. + If more than one mappings of this sort exist, one of them + is returned. + + For example, ``And(x, y)`` is logically equivalent to ``And(a, b)`` for + the mapping ``{x: a, y: b}`` or ``{x: b, y: a}``. + If no such mapping exists, return ``False``. + + Examples + ======== + + >>> from sympy import SOPform, bool_map, Or, And, Not, Xor + >>> from sympy.abc import w, x, y, z, a, b, c, d + >>> function1 = SOPform([x, z, y],[[1, 0, 1], [0, 0, 1]]) + >>> function2 = SOPform([a, b, c],[[1, 0, 1], [1, 0, 0]]) + >>> bool_map(function1, function2) + (y & ~z, {y: a, z: b}) + + The results are not necessarily unique, but they are canonical. Here, + ``(w, z)`` could be ``(a, d)`` or ``(d, a)``: + + >>> eq = Or(And(Not(y), w), And(Not(y), z), And(x, y)) + >>> eq2 = Or(And(Not(c), a), And(Not(c), d), And(b, c)) + >>> bool_map(eq, eq2) + ((x & y) | (w & ~y) | (z & ~y), {w: a, x: b, y: c, z: d}) + >>> eq = And(Xor(a, b), c, And(c,d)) + >>> bool_map(eq, eq.subs(c, x)) + (c & d & (a | b) & (~a | ~b), {a: a, b: b, c: d, d: x}) + + """ + + def match(function1, function2): + """Return the mapping that equates variables between two + simplified boolean expressions if possible. + + By "simplified" we mean that a function has been denested + and is either an And (or an Or) whose arguments are either + symbols (x), negated symbols (Not(x)), or Or (or an And) whose + arguments are only symbols or negated symbols. For example, + ``And(x, Not(y), Or(w, Not(z)))``. + + Basic.match is not robust enough (see issue 4835) so this is + a workaround that is valid for simplified boolean expressions + """ + + # do some quick checks + if function1.__class__ != function2.__class__: + return None # maybe simplification makes them the same? + if len(function1.args) != len(function2.args): + return None # maybe simplification makes them the same? + if function1.is_Symbol: + return {function1: function2} + + # get the fingerprint dictionaries + f1 = _finger(function1) + f2 = _finger(function2) + + # more quick checks + if len(f1) != len(f2): + return False + + # assemble the match dictionary if possible + matchdict = {} + for k in f1.keys(): + if k not in f2: + return False + if len(f1[k]) != len(f2[k]): + return False + for i, x in enumerate(f1[k]): + matchdict[x] = f2[k][i] + return matchdict + + a = simplify_logic(bool1) + b = simplify_logic(bool2) + m = match(a, b) + if m: + return a, m + return m + + +def _apply_patternbased_simplification(rv, patterns, measure, + dominatingvalue, + replacementvalue=None, + threeterm_patterns=None): + """ + Replace patterns of Relational + + Parameters + ========== + + rv : Expr + Boolean expression + + patterns : tuple + Tuple of tuples, with (pattern to simplify, simplified pattern) with + two terms. + + measure : function + Simplification measure. + + dominatingvalue : Boolean or ``None`` + The dominating value for the function of consideration. + For example, for :py:class:`~.And` ``S.false`` is dominating. + As soon as one expression is ``S.false`` in :py:class:`~.And`, + the whole expression is ``S.false``. + + replacementvalue : Boolean or ``None``, optional + The resulting value for the whole expression if one argument + evaluates to ``dominatingvalue``. + For example, for :py:class:`~.Nand` ``S.false`` is dominating, but + in this case the resulting value is ``S.true``. Default is ``None``. + If ``replacementvalue`` is ``None`` and ``dominatingvalue`` is not + ``None``, ``replacementvalue = dominatingvalue``. + + threeterm_patterns : tuple, optional + Tuple of tuples, with (pattern to simplify, simplified pattern) with + three terms. + + """ + from sympy.core.relational import Relational, _canonical + + if replacementvalue is None and dominatingvalue is not None: + replacementvalue = dominatingvalue + # Use replacement patterns for Relationals + Rel, nonRel = sift(rv.args, lambda i: isinstance(i, Relational), + binary=True) + if len(Rel) <= 1: + return rv + Rel, nonRealRel = sift(Rel, lambda i: not any(s.is_real is False + for s in i.free_symbols), + binary=True) + Rel = [i.canonical for i in Rel] + + if threeterm_patterns and len(Rel) >= 3: + Rel = _apply_patternbased_threeterm_simplification(Rel, + threeterm_patterns, rv.func, dominatingvalue, + replacementvalue, measure) + + Rel = _apply_patternbased_twoterm_simplification(Rel, patterns, + rv.func, dominatingvalue, replacementvalue, measure) + + rv = rv.func(*([_canonical(i) for i in ordered(Rel)] + + nonRel + nonRealRel)) + return rv + + +def _apply_patternbased_twoterm_simplification(Rel, patterns, func, + dominatingvalue, + replacementvalue, + measure): + """ Apply pattern-based two-term simplification.""" + from sympy.functions.elementary.miscellaneous import Min, Max + from sympy.core.relational import Ge, Gt, _Inequality + changed = True + while changed and len(Rel) >= 2: + changed = False + # Use only < or <= + Rel = [r.reversed if isinstance(r, (Ge, Gt)) else r for r in Rel] + # Sort based on ordered + Rel = list(ordered(Rel)) + # Eq and Ne must be tested reversed as well + rtmp = [(r, ) if isinstance(r, _Inequality) else (r, r.reversed) for r in Rel] + # Create a list of possible replacements + results = [] + # Try all combinations of possibly reversed relational + for ((i, pi), (j, pj)) in combinations(enumerate(rtmp), 2): + for pattern, simp in patterns: + res = [] + for p1, p2 in product(pi, pj): + # use SymPy matching + oldexpr = Tuple(p1, p2) + tmpres = oldexpr.match(pattern) + if tmpres: + res.append((tmpres, oldexpr)) + + if res: + for tmpres, oldexpr in res: + # we have a matching, compute replacement + np = simp.xreplace(tmpres) + if np == dominatingvalue: + # if dominatingvalue, the whole expression + # will be replacementvalue + return [replacementvalue] + # add replacement + if not isinstance(np, ITE) and not np.has(Min, Max): + # We only want to use ITE and Min/Max replacements if + # they simplify to a relational + costsaving = measure(func(*oldexpr.args)) - measure(np) + if costsaving > 0: + results.append((costsaving, ([i, j], np))) + if results: + # Sort results based on complexity + results = sorted(results, + key=lambda pair: pair[0], reverse=True) + # Replace the one providing most simplification + replacement = results[0][1] + idx, newrel = replacement + idx.sort() + # Remove the old relationals + for index in reversed(idx): + del Rel[index] + if dominatingvalue is None or newrel != Not(dominatingvalue): + # Insert the new one (no need to insert a value that will + # not affect the result) + if newrel.func == func: + for a in newrel.args: + Rel.append(a) + else: + Rel.append(newrel) + # We did change something so try again + changed = True + return Rel + + +def _apply_patternbased_threeterm_simplification(Rel, patterns, func, + dominatingvalue, + replacementvalue, + measure): + """ Apply pattern-based three-term simplification.""" + from sympy.functions.elementary.miscellaneous import Min, Max + from sympy.core.relational import Le, Lt, _Inequality + changed = True + while changed and len(Rel) >= 3: + changed = False + # Use only > or >= + Rel = [r.reversed if isinstance(r, (Le, Lt)) else r for r in Rel] + # Sort based on ordered + Rel = list(ordered(Rel)) + # Create a list of possible replacements + results = [] + # Eq and Ne must be tested reversed as well + rtmp = [(r, ) if isinstance(r, _Inequality) else (r, r.reversed) for r in Rel] + # Try all combinations of possibly reversed relational + for ((i, pi), (j, pj), (k, pk)) in permutations(enumerate(rtmp), 3): + for pattern, simp in patterns: + res = [] + for p1, p2, p3 in product(pi, pj, pk): + # use SymPy matching + oldexpr = Tuple(p1, p2, p3) + tmpres = oldexpr.match(pattern) + if tmpres: + res.append((tmpres, oldexpr)) + + if res: + for tmpres, oldexpr in res: + # we have a matching, compute replacement + np = simp.xreplace(tmpres) + if np == dominatingvalue: + # if dominatingvalue, the whole expression + # will be replacementvalue + return [replacementvalue] + # add replacement + if not isinstance(np, ITE) and not np.has(Min, Max): + # We only want to use ITE and Min/Max replacements if + # they simplify to a relational + costsaving = measure(func(*oldexpr.args)) - measure(np) + if costsaving > 0: + results.append((costsaving, ([i, j, k], np))) + if results: + # Sort results based on complexity + results = sorted(results, + key=lambda pair: pair[0], reverse=True) + # Replace the one providing most simplification + replacement = results[0][1] + idx, newrel = replacement + idx.sort() + # Remove the old relationals + for index in reversed(idx): + del Rel[index] + if dominatingvalue is None or newrel != Not(dominatingvalue): + # Insert the new one (no need to insert a value that will + # not affect the result) + if newrel.func == func: + for a in newrel.args: + Rel.append(a) + else: + Rel.append(newrel) + # We did change something so try again + changed = True + return Rel + + +@cacheit +def _simplify_patterns_and(): + """ Two-term patterns for And.""" + + from sympy.core import Wild + from sympy.core.relational import Eq, Ne, Ge, Gt, Le, Lt + from sympy.functions.elementary.complexes import Abs + from sympy.functions.elementary.miscellaneous import Min, Max + a = Wild('a') + b = Wild('b') + c = Wild('c') + # Relationals patterns should be in alphabetical order + # (pattern1, pattern2, simplified) + # Do not use Ge, Gt + _matchers_and = ((Tuple(Eq(a, b), Lt(a, b)), false), + #(Tuple(Eq(a, b), Lt(b, a)), S.false), + #(Tuple(Le(b, a), Lt(a, b)), S.false), + #(Tuple(Lt(b, a), Le(a, b)), S.false), + (Tuple(Lt(b, a), Lt(a, b)), false), + (Tuple(Eq(a, b), Le(b, a)), Eq(a, b)), + #(Tuple(Eq(a, b), Le(a, b)), Eq(a, b)), + #(Tuple(Le(b, a), Lt(b, a)), Gt(a, b)), + (Tuple(Le(b, a), Le(a, b)), Eq(a, b)), + #(Tuple(Le(b, a), Ne(a, b)), Gt(a, b)), + #(Tuple(Lt(b, a), Ne(a, b)), Gt(a, b)), + (Tuple(Le(a, b), Lt(a, b)), Lt(a, b)), + (Tuple(Le(a, b), Ne(a, b)), Lt(a, b)), + (Tuple(Lt(a, b), Ne(a, b)), Lt(a, b)), + # Sign + (Tuple(Eq(a, b), Eq(a, -b)), And(Eq(a, S.Zero), Eq(b, S.Zero))), + # Min/Max/ITE + (Tuple(Le(b, a), Le(c, a)), Ge(a, Max(b, c))), + (Tuple(Le(b, a), Lt(c, a)), ITE(b > c, Ge(a, b), Gt(a, c))), + (Tuple(Lt(b, a), Lt(c, a)), Gt(a, Max(b, c))), + (Tuple(Le(a, b), Le(a, c)), Le(a, Min(b, c))), + (Tuple(Le(a, b), Lt(a, c)), ITE(b < c, Le(a, b), Lt(a, c))), + (Tuple(Lt(a, b), Lt(a, c)), Lt(a, Min(b, c))), + (Tuple(Le(a, b), Le(c, a)), ITE(Eq(b, c), Eq(a, b), ITE(b < c, false, And(Le(a, b), Ge(a, c))))), + (Tuple(Le(c, a), Le(a, b)), ITE(Eq(b, c), Eq(a, b), ITE(b < c, false, And(Le(a, b), Ge(a, c))))), + (Tuple(Lt(a, b), Lt(c, a)), ITE(b < c, false, And(Lt(a, b), Gt(a, c)))), + (Tuple(Lt(c, a), Lt(a, b)), ITE(b < c, false, And(Lt(a, b), Gt(a, c)))), + (Tuple(Le(a, b), Lt(c, a)), ITE(b <= c, false, And(Le(a, b), Gt(a, c)))), + (Tuple(Le(c, a), Lt(a, b)), ITE(b <= c, false, And(Lt(a, b), Ge(a, c)))), + (Tuple(Eq(a, b), Eq(a, c)), ITE(Eq(b, c), Eq(a, b), false)), + (Tuple(Lt(a, b), Lt(-b, a)), ITE(b > 0, Lt(Abs(a), b), false)), + (Tuple(Le(a, b), Le(-b, a)), ITE(b >= 0, Le(Abs(a), b), false)), + ) + return _matchers_and + + +@cacheit +def _simplify_patterns_and3(): + """ Three-term patterns for And.""" + + from sympy.core import Wild + from sympy.core.relational import Eq, Ge, Gt + + a = Wild('a') + b = Wild('b') + c = Wild('c') + # Relationals patterns should be in alphabetical order + # (pattern1, pattern2, pattern3, simplified) + # Do not use Le, Lt + _matchers_and = ((Tuple(Ge(a, b), Ge(b, c), Gt(c, a)), false), + (Tuple(Ge(a, b), Gt(b, c), Gt(c, a)), false), + (Tuple(Gt(a, b), Gt(b, c), Gt(c, a)), false), + # (Tuple(Ge(c, a), Gt(a, b), Gt(b, c)), S.false), + # Lower bound relations + # Commented out combinations that does not simplify + (Tuple(Ge(a, b), Ge(a, c), Ge(b, c)), And(Ge(a, b), Ge(b, c))), + (Tuple(Ge(a, b), Ge(a, c), Gt(b, c)), And(Ge(a, b), Gt(b, c))), + # (Tuple(Ge(a, b), Gt(a, c), Ge(b, c)), And(Ge(a, b), Ge(b, c))), + (Tuple(Ge(a, b), Gt(a, c), Gt(b, c)), And(Ge(a, b), Gt(b, c))), + # (Tuple(Gt(a, b), Ge(a, c), Ge(b, c)), And(Gt(a, b), Ge(b, c))), + (Tuple(Ge(a, c), Gt(a, b), Gt(b, c)), And(Gt(a, b), Gt(b, c))), + (Tuple(Ge(b, c), Gt(a, b), Gt(a, c)), And(Gt(a, b), Ge(b, c))), + (Tuple(Gt(a, b), Gt(a, c), Gt(b, c)), And(Gt(a, b), Gt(b, c))), + # Upper bound relations + # Commented out combinations that does not simplify + (Tuple(Ge(b, a), Ge(c, a), Ge(b, c)), And(Ge(c, a), Ge(b, c))), + (Tuple(Ge(b, a), Ge(c, a), Gt(b, c)), And(Ge(c, a), Gt(b, c))), + # (Tuple(Ge(b, a), Gt(c, a), Ge(b, c)), And(Gt(c, a), Ge(b, c))), + (Tuple(Ge(b, a), Gt(c, a), Gt(b, c)), And(Gt(c, a), Gt(b, c))), + # (Tuple(Gt(b, a), Ge(c, a), Ge(b, c)), And(Ge(c, a), Ge(b, c))), + (Tuple(Ge(c, a), Gt(b, a), Gt(b, c)), And(Ge(c, a), Gt(b, c))), + (Tuple(Ge(b, c), Gt(b, a), Gt(c, a)), And(Gt(c, a), Ge(b, c))), + (Tuple(Gt(b, a), Gt(c, a), Gt(b, c)), And(Gt(c, a), Gt(b, c))), + # Circular relation + (Tuple(Ge(a, b), Ge(b, c), Ge(c, a)), And(Eq(a, b), Eq(b, c))), + ) + return _matchers_and + + +@cacheit +def _simplify_patterns_or(): + """ Two-term patterns for Or.""" + + from sympy.core import Wild + from sympy.core.relational import Eq, Ne, Ge, Gt, Le, Lt + from sympy.functions.elementary.complexes import Abs + from sympy.functions.elementary.miscellaneous import Min, Max + a = Wild('a') + b = Wild('b') + c = Wild('c') + # Relationals patterns should be in alphabetical order + # (pattern1, pattern2, simplified) + # Do not use Ge, Gt + _matchers_or = ((Tuple(Le(b, a), Le(a, b)), true), + #(Tuple(Le(b, a), Lt(a, b)), true), + (Tuple(Le(b, a), Ne(a, b)), true), + #(Tuple(Le(a, b), Lt(b, a)), true), + #(Tuple(Le(a, b), Ne(a, b)), true), + #(Tuple(Eq(a, b), Le(b, a)), Ge(a, b)), + #(Tuple(Eq(a, b), Lt(b, a)), Ge(a, b)), + (Tuple(Eq(a, b), Le(a, b)), Le(a, b)), + (Tuple(Eq(a, b), Lt(a, b)), Le(a, b)), + #(Tuple(Le(b, a), Lt(b, a)), Ge(a, b)), + (Tuple(Lt(b, a), Lt(a, b)), Ne(a, b)), + (Tuple(Lt(b, a), Ne(a, b)), Ne(a, b)), + (Tuple(Le(a, b), Lt(a, b)), Le(a, b)), + #(Tuple(Lt(a, b), Ne(a, b)), Ne(a, b)), + (Tuple(Eq(a, b), Ne(a, c)), ITE(Eq(b, c), true, Ne(a, c))), + (Tuple(Ne(a, b), Ne(a, c)), ITE(Eq(b, c), Ne(a, b), true)), + # Min/Max/ITE + (Tuple(Le(b, a), Le(c, a)), Ge(a, Min(b, c))), + #(Tuple(Ge(b, a), Ge(c, a)), Ge(Min(b, c), a)), + (Tuple(Le(b, a), Lt(c, a)), ITE(b > c, Lt(c, a), Le(b, a))), + (Tuple(Lt(b, a), Lt(c, a)), Gt(a, Min(b, c))), + #(Tuple(Gt(b, a), Gt(c, a)), Gt(Min(b, c), a)), + (Tuple(Le(a, b), Le(a, c)), Le(a, Max(b, c))), + #(Tuple(Le(b, a), Le(c, a)), Le(Max(b, c), a)), + (Tuple(Le(a, b), Lt(a, c)), ITE(b >= c, Le(a, b), Lt(a, c))), + (Tuple(Lt(a, b), Lt(a, c)), Lt(a, Max(b, c))), + #(Tuple(Lt(b, a), Lt(c, a)), Lt(Max(b, c), a)), + (Tuple(Le(a, b), Le(c, a)), ITE(b >= c, true, Or(Le(a, b), Ge(a, c)))), + (Tuple(Le(c, a), Le(a, b)), ITE(b >= c, true, Or(Le(a, b), Ge(a, c)))), + (Tuple(Lt(a, b), Lt(c, a)), ITE(b > c, true, Or(Lt(a, b), Gt(a, c)))), + (Tuple(Lt(c, a), Lt(a, b)), ITE(b > c, true, Or(Lt(a, b), Gt(a, c)))), + (Tuple(Le(a, b), Lt(c, a)), ITE(b >= c, true, Or(Le(a, b), Gt(a, c)))), + (Tuple(Le(c, a), Lt(a, b)), ITE(b >= c, true, Or(Lt(a, b), Ge(a, c)))), + (Tuple(Lt(b, a), Lt(a, -b)), ITE(b >= 0, Gt(Abs(a), b), true)), + (Tuple(Le(b, a), Le(a, -b)), ITE(b > 0, Ge(Abs(a), b), true)), + ) + return _matchers_or + + +@cacheit +def _simplify_patterns_xor(): + """ Two-term patterns for Xor.""" + + from sympy.functions.elementary.miscellaneous import Min, Max + from sympy.core import Wild + from sympy.core.relational import Eq, Ne, Ge, Gt, Le, Lt + a = Wild('a') + b = Wild('b') + c = Wild('c') + # Relationals patterns should be in alphabetical order + # (pattern1, pattern2, simplified) + # Do not use Ge, Gt + _matchers_xor = (#(Tuple(Le(b, a), Lt(a, b)), true), + #(Tuple(Lt(b, a), Le(a, b)), true), + #(Tuple(Eq(a, b), Le(b, a)), Gt(a, b)), + #(Tuple(Eq(a, b), Lt(b, a)), Ge(a, b)), + (Tuple(Eq(a, b), Le(a, b)), Lt(a, b)), + (Tuple(Eq(a, b), Lt(a, b)), Le(a, b)), + (Tuple(Le(a, b), Lt(a, b)), Eq(a, b)), + (Tuple(Le(a, b), Le(b, a)), Ne(a, b)), + (Tuple(Le(b, a), Ne(a, b)), Le(a, b)), + # (Tuple(Lt(b, a), Lt(a, b)), Ne(a, b)), + (Tuple(Lt(b, a), Ne(a, b)), Lt(a, b)), + # (Tuple(Le(a, b), Lt(a, b)), Eq(a, b)), + # (Tuple(Le(a, b), Ne(a, b)), Ge(a, b)), + # (Tuple(Lt(a, b), Ne(a, b)), Gt(a, b)), + # Min/Max/ITE + (Tuple(Le(b, a), Le(c, a)), + And(Ge(a, Min(b, c)), Lt(a, Max(b, c)))), + (Tuple(Le(b, a), Lt(c, a)), + ITE(b > c, And(Gt(a, c), Lt(a, b)), + And(Ge(a, b), Le(a, c)))), + (Tuple(Lt(b, a), Lt(c, a)), + And(Gt(a, Min(b, c)), Le(a, Max(b, c)))), + (Tuple(Le(a, b), Le(a, c)), + And(Le(a, Max(b, c)), Gt(a, Min(b, c)))), + (Tuple(Le(a, b), Lt(a, c)), + ITE(b < c, And(Lt(a, c), Gt(a, b)), + And(Le(a, b), Ge(a, c)))), + (Tuple(Lt(a, b), Lt(a, c)), + And(Lt(a, Max(b, c)), Ge(a, Min(b, c)))), + ) + return _matchers_xor + + +def simplify_univariate(expr): + """return a simplified version of univariate boolean expression, else ``expr``""" + from sympy.functions.elementary.piecewise import Piecewise + from sympy.core.relational import Eq, Ne + if not isinstance(expr, BooleanFunction): + return expr + if expr.atoms(Eq, Ne): + return expr + c = expr + free = c.free_symbols + if len(free) != 1: + return c + x = free.pop() + ok, i = Piecewise((0, c), evaluate=False + )._intervals(x, err_on_Eq=True) + if not ok: + return c + if not i: + return false + args = [] + for a, b, _, _ in i: + if a is S.NegativeInfinity: + if b is S.Infinity: + c = true + else: + if c.subs(x, b) == True: + c = (x <= b) + else: + c = (x < b) + else: + incl_a = (c.subs(x, a) == True) + incl_b = (c.subs(x, b) == True) + if incl_a and incl_b: + if b.is_infinite: + c = (x >= a) + else: + c = And(a <= x, x <= b) + elif incl_a: + c = And(a <= x, x < b) + elif incl_b: + if b.is_infinite: + c = (x > a) + else: + c = And(a < x, x <= b) + else: + c = And(a < x, x < b) + args.append(c) + return Or(*args) + + +# Classes corresponding to logic gates +# Used in gateinputcount method +BooleanGates = (And, Or, Xor, Nand, Nor, Not, Xnor, ITE) + +def gateinputcount(expr): + """ + Return the total number of inputs for the logic gates realizing the + Boolean expression. + + Returns + ======= + + int + Number of gate inputs + + Note + ==== + + Not all Boolean functions count as gate here, only those that are + considered to be standard gates. These are: :py:class:`~.And`, + :py:class:`~.Or`, :py:class:`~.Xor`, :py:class:`~.Not`, and + :py:class:`~.ITE` (multiplexer). :py:class:`~.Nand`, :py:class:`~.Nor`, + and :py:class:`~.Xnor` will be evaluated to ``Not(And())`` etc. + + Examples + ======== + + >>> from sympy.logic import And, Or, Nand, Not, gateinputcount + >>> from sympy.abc import x, y, z + >>> expr = And(x, y) + >>> gateinputcount(expr) + 2 + >>> gateinputcount(Or(expr, z)) + 4 + + Note that ``Nand`` is automatically evaluated to ``Not(And())`` so + + >>> gateinputcount(Nand(x, y, z)) + 4 + >>> gateinputcount(Not(And(x, y, z))) + 4 + + Although this can be avoided by using ``evaluate=False`` + + >>> gateinputcount(Nand(x, y, z, evaluate=False)) + 3 + + Also note that a comparison will count as a Boolean variable: + + >>> gateinputcount(And(x > z, y >= 2)) + 2 + + As will a symbol: + >>> gateinputcount(x) + 0 + + """ + if not isinstance(expr, Boolean): + raise TypeError("Expression must be Boolean") + if isinstance(expr, BooleanGates): + return len(expr.args) + sum(gateinputcount(x) for x in expr.args) + return 0 diff --git a/venv/lib/python3.11/site-packages/sympy/logic/inference.py b/venv/lib/python3.11/site-packages/sympy/logic/inference.py new file mode 100644 index 0000000000000000000000000000000000000000..c3798231c09ae351ea7e7026d622b834fea3e3fa --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/inference.py @@ -0,0 +1,340 @@ +"""Inference in propositional logic""" + +from sympy.logic.boolalg import And, Not, conjuncts, to_cnf, BooleanFunction +from sympy.core.sorting import ordered +from sympy.core.sympify import sympify +from sympy.external.importtools import import_module + + +def literal_symbol(literal): + """ + The symbol in this literal (without the negation). + + Examples + ======== + + >>> from sympy.abc import A + >>> from sympy.logic.inference import literal_symbol + >>> literal_symbol(A) + A + >>> literal_symbol(~A) + A + + """ + + if literal is True or literal is False: + return literal + elif literal.is_Symbol: + return literal + elif literal.is_Not: + return literal_symbol(literal.args[0]) + else: + raise ValueError("Argument must be a boolean literal.") + + +def satisfiable(expr, algorithm=None, all_models=False, minimal=False, use_lra_theory=False): + """ + Check satisfiability of a propositional sentence. + Returns a model when it succeeds. + Returns {true: true} for trivially true expressions. + + On setting all_models to True, if given expr is satisfiable then + returns a generator of models. However, if expr is unsatisfiable + then returns a generator containing the single element False. + + Examples + ======== + + >>> from sympy.abc import A, B + >>> from sympy.logic.inference import satisfiable + >>> satisfiable(A & ~B) + {A: True, B: False} + >>> satisfiable(A & ~A) + False + >>> satisfiable(True) + {True: True} + >>> next(satisfiable(A & ~A, all_models=True)) + False + >>> models = satisfiable((A >> B) & B, all_models=True) + >>> next(models) + {A: False, B: True} + >>> next(models) + {A: True, B: True} + >>> def use_models(models): + ... for model in models: + ... if model: + ... # Do something with the model. + ... print(model) + ... else: + ... # Given expr is unsatisfiable. + ... print("UNSAT") + >>> use_models(satisfiable(A >> ~A, all_models=True)) + {A: False} + >>> use_models(satisfiable(A ^ A, all_models=True)) + UNSAT + + """ + if use_lra_theory: + if algorithm is not None and algorithm != "dpll2": + raise ValueError(f"Currently only dpll2 can handle using lra theory. {algorithm} is not handled.") + algorithm = "dpll2" + + if algorithm is None or algorithm == "pycosat": + pycosat = import_module('pycosat') + if pycosat is not None: + algorithm = "pycosat" + else: + if algorithm == "pycosat": + raise ImportError("pycosat module is not present") + # Silently fall back to dpll2 if pycosat + # is not installed + algorithm = "dpll2" + + if algorithm=="minisat22": + pysat = import_module('pysat') + if pysat is None: + algorithm = "dpll2" + + if algorithm=="z3": + z3 = import_module('z3') + if z3 is None: + algorithm = "dpll2" + + if algorithm == "dpll": + from sympy.logic.algorithms.dpll import dpll_satisfiable + return dpll_satisfiable(expr) + elif algorithm == "dpll2": + from sympy.logic.algorithms.dpll2 import dpll_satisfiable + return dpll_satisfiable(expr, all_models, use_lra_theory=use_lra_theory) + elif algorithm == "pycosat": + from sympy.logic.algorithms.pycosat_wrapper import pycosat_satisfiable + return pycosat_satisfiable(expr, all_models) + elif algorithm == "minisat22": + from sympy.logic.algorithms.minisat22_wrapper import minisat22_satisfiable + return minisat22_satisfiable(expr, all_models, minimal) + elif algorithm == "z3": + from sympy.logic.algorithms.z3_wrapper import z3_satisfiable + return z3_satisfiable(expr, all_models) + + raise NotImplementedError + + +def valid(expr): + """ + Check validity of a propositional sentence. + A valid propositional sentence is True under every assignment. + + Examples + ======== + + >>> from sympy.abc import A, B + >>> from sympy.logic.inference import valid + >>> valid(A | ~A) + True + >>> valid(A | B) + False + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Validity + + """ + return not satisfiable(Not(expr)) + + +def pl_true(expr, model=None, deep=False): + """ + Returns whether the given assignment is a model or not. + + If the assignment does not specify the value for every proposition, + this may return None to indicate 'not obvious'. + + Parameters + ========== + + model : dict, optional, default: {} + Mapping of symbols to boolean values to indicate assignment. + deep: boolean, optional, default: False + Gives the value of the expression under partial assignments + correctly. May still return None to indicate 'not obvious'. + + + Examples + ======== + + >>> from sympy.abc import A, B + >>> from sympy.logic.inference import pl_true + >>> pl_true( A & B, {A: True, B: True}) + True + >>> pl_true(A & B, {A: False}) + False + >>> pl_true(A & B, {A: True}) + >>> pl_true(A & B, {A: True}, deep=True) + >>> pl_true(A >> (B >> A)) + >>> pl_true(A >> (B >> A), deep=True) + True + >>> pl_true(A & ~A) + >>> pl_true(A & ~A, deep=True) + False + >>> pl_true(A & B & (~A | ~B), {A: True}) + >>> pl_true(A & B & (~A | ~B), {A: True}, deep=True) + False + + """ + + from sympy.core.symbol import Symbol + + boolean = (True, False) + + def _validate(expr): + if isinstance(expr, Symbol) or expr in boolean: + return True + if not isinstance(expr, BooleanFunction): + return False + return all(_validate(arg) for arg in expr.args) + + if expr in boolean: + return expr + expr = sympify(expr) + if not _validate(expr): + raise ValueError("%s is not a valid boolean expression" % expr) + if not model: + model = {} + model = {k: v for k, v in model.items() if v in boolean} + result = expr.subs(model) + if result in boolean: + return bool(result) + if deep: + model = dict.fromkeys(result.atoms(), True) + if pl_true(result, model): + if valid(result): + return True + else: + if not satisfiable(result): + return False + return None + + +def entails(expr, formula_set=None): + """ + Check whether the given expr_set entail an expr. + If formula_set is empty then it returns the validity of expr. + + Examples + ======== + + >>> from sympy.abc import A, B, C + >>> from sympy.logic.inference import entails + >>> entails(A, [A >> B, B >> C]) + False + >>> entails(C, [A >> B, B >> C, A]) + True + >>> entails(A >> B) + False + >>> entails(A >> (B >> A)) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Logical_consequence + + """ + if formula_set: + formula_set = list(formula_set) + else: + formula_set = [] + formula_set.append(Not(expr)) + return not satisfiable(And(*formula_set)) + + +class KB: + """Base class for all knowledge bases""" + def __init__(self, sentence=None): + self.clauses_ = set() + if sentence: + self.tell(sentence) + + def tell(self, sentence): + raise NotImplementedError + + def ask(self, query): + raise NotImplementedError + + def retract(self, sentence): + raise NotImplementedError + + @property + def clauses(self): + return list(ordered(self.clauses_)) + + +class PropKB(KB): + """A KB for Propositional Logic. Inefficient, with no indexing.""" + + def tell(self, sentence): + """Add the sentence's clauses to the KB + + Examples + ======== + + >>> from sympy.logic.inference import PropKB + >>> from sympy.abc import x, y + >>> l = PropKB() + >>> l.clauses + [] + + >>> l.tell(x | y) + >>> l.clauses + [x | y] + + >>> l.tell(y) + >>> l.clauses + [y, x | y] + + """ + for c in conjuncts(to_cnf(sentence)): + self.clauses_.add(c) + + def ask(self, query): + """Checks if the query is true given the set of clauses. + + Examples + ======== + + >>> from sympy.logic.inference import PropKB + >>> from sympy.abc import x, y + >>> l = PropKB() + >>> l.tell(x & ~y) + >>> l.ask(x) + True + >>> l.ask(y) + False + + """ + return entails(query, self.clauses_) + + def retract(self, sentence): + """Remove the sentence's clauses from the KB + + Examples + ======== + + >>> from sympy.logic.inference import PropKB + >>> from sympy.abc import x, y + >>> l = PropKB() + >>> l.clauses + [] + + >>> l.tell(x | y) + >>> l.clauses + [x | y] + + >>> l.retract(x | y) + >>> l.clauses + [] + + """ + for c in conjuncts(to_cnf(sentence)): + self.clauses_.discard(c) diff --git a/venv/lib/python3.11/site-packages/sympy/logic/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/logic/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/logic/tests/test_boolalg.py b/venv/lib/python3.11/site-packages/sympy/logic/tests/test_boolalg.py new file mode 100644 index 0000000000000000000000000000000000000000..88cdd647fdcc723faee328f71df96030841a3edb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/tests/test_boolalg.py @@ -0,0 +1,1367 @@ +from sympy.assumptions.ask import Q +from sympy.assumptions.refine import refine +from sympy.core.numbers import oo +from sympy.core.relational import Equality, Eq, Ne +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, symbols) +from sympy.functions import Piecewise +from sympy.functions.elementary.trigonometric import cos, sin +from sympy.sets.sets import Interval, Union +from sympy.sets.contains import Contains +from sympy.simplify.simplify import simplify +from sympy.logic.boolalg import ( + And, Boolean, Equivalent, ITE, Implies, Nand, Nor, Not, Or, + POSform, SOPform, Xor, Xnor, conjuncts, disjuncts, + distribute_or_over_and, distribute_and_over_or, + eliminate_implications, is_nnf, is_cnf, is_dnf, simplify_logic, + to_nnf, to_cnf, to_dnf, to_int_repr, bool_map, true, false, + BooleanAtom, is_literal, term_to_integer, + truth_table, as_Boolean, to_anf, is_anf, distribute_xor_over_and, + anf_coeffs, ANFform, bool_minterm, bool_maxterm, bool_monomial, + _check_pair, _convert_to_varsSOP, _convert_to_varsPOS, Exclusive, + gateinputcount) +from sympy.assumptions.cnf import CNF + +from sympy.testing.pytest import raises, XFAIL, slow + +from itertools import combinations, permutations, product + +A, B, C, D = symbols('A:D') +a, b, c, d, e, w, x, y, z = symbols('a:e w:z') + + +def test_overloading(): + """Test that |, & are overloaded as expected""" + + assert A & B == And(A, B) + assert A | B == Or(A, B) + assert (A & B) | C == Or(And(A, B), C) + assert A >> B == Implies(A, B) + assert A << B == Implies(B, A) + assert ~A == Not(A) + assert A ^ B == Xor(A, B) + + +def test_And(): + assert And() is true + assert And(A) == A + assert And(True) is true + assert And(False) is false + assert And(True, True) is true + assert And(True, False) is false + assert And(False, False) is false + assert And(True, A) == A + assert And(False, A) is false + assert And(True, True, True) is true + assert And(True, True, A) == A + assert And(True, False, A) is false + assert And(1, A) == A + raises(TypeError, lambda: And(2, A)) + assert And(A < 1, A >= 1) is false + e = A > 1 + assert And(e, e.canonical) == e.canonical + g, l, ge, le = A > B, B < A, A >= B, B <= A + assert And(g, l, ge, le) == And(ge, g) + assert {And(*i) for i in permutations((l, g, le, ge))} == {And(ge, g)} + assert And(And(Eq(a, 0), Eq(b, 0)), And(Ne(a, 0), Eq(c, 0))) is false + + +def test_Or(): + assert Or() is false + assert Or(A) == A + assert Or(True) is true + assert Or(False) is false + assert Or(True, True) is true + assert Or(True, False) is true + assert Or(False, False) is false + assert Or(True, A) is true + assert Or(False, A) == A + assert Or(True, False, False) is true + assert Or(True, False, A) is true + assert Or(False, False, A) == A + assert Or(1, A) is true + raises(TypeError, lambda: Or(2, A)) + assert Or(A < 1, A >= 1) is true + e = A > 1 + assert Or(e, e.canonical) == e + g, l, ge, le = A > B, B < A, A >= B, B <= A + assert Or(g, l, ge, le) == Or(g, ge) + + +def test_Xor(): + assert Xor() is false + assert Xor(A) == A + assert Xor(A, A) is false + assert Xor(True, A, A) is true + assert Xor(A, A, A, A, A) == A + assert Xor(True, False, False, A, B) == ~Xor(A, B) + assert Xor(True) is true + assert Xor(False) is false + assert Xor(True, True) is false + assert Xor(True, False) is true + assert Xor(False, False) is false + assert Xor(True, A) == ~A + assert Xor(False, A) == A + assert Xor(True, False, False) is true + assert Xor(True, False, A) == ~A + assert Xor(False, False, A) == A + assert isinstance(Xor(A, B), Xor) + assert Xor(A, B, Xor(C, D)) == Xor(A, B, C, D) + assert Xor(A, B, Xor(B, C)) == Xor(A, C) + assert Xor(A < 1, A >= 1, B) == Xor(0, 1, B) == Xor(1, 0, B) + e = A > 1 + assert Xor(e, e.canonical) == Xor(0, 0) == Xor(1, 1) + + +def test_rewrite_as_And(): + expr = x ^ y + assert expr.rewrite(And) == (x | y) & (~x | ~y) + + +def test_rewrite_as_Or(): + expr = x ^ y + assert expr.rewrite(Or) == (x & ~y) | (y & ~x) + + +def test_rewrite_as_Nand(): + expr = (y & z) | (z & ~w) + assert expr.rewrite(Nand) == ~(~(y & z) & ~(z & ~w)) + + +def test_rewrite_as_Nor(): + expr = z & (y | ~w) + assert expr.rewrite(Nor) == ~(~z | ~(y | ~w)) + + +def test_Not(): + raises(TypeError, lambda: Not(True, False)) + assert Not(True) is false + assert Not(False) is true + assert Not(0) is true + assert Not(1) is false + assert Not(2) is false + + +def test_Nand(): + assert Nand() is false + assert Nand(A) == ~A + assert Nand(True) is false + assert Nand(False) is true + assert Nand(True, True) is false + assert Nand(True, False) is true + assert Nand(False, False) is true + assert Nand(True, A) == ~A + assert Nand(False, A) is true + assert Nand(True, True, True) is false + assert Nand(True, True, A) == ~A + assert Nand(True, False, A) is true + + +def test_Nor(): + assert Nor() is true + assert Nor(A) == ~A + assert Nor(True) is false + assert Nor(False) is true + assert Nor(True, True) is false + assert Nor(True, False) is false + assert Nor(False, False) is true + assert Nor(True, A) is false + assert Nor(False, A) == ~A + assert Nor(True, True, True) is false + assert Nor(True, True, A) is false + assert Nor(True, False, A) is false + + +def test_Xnor(): + assert Xnor() is true + assert Xnor(A) == ~A + assert Xnor(A, A) is true + assert Xnor(True, A, A) is false + assert Xnor(A, A, A, A, A) == ~A + assert Xnor(True) is false + assert Xnor(False) is true + assert Xnor(True, True) is true + assert Xnor(True, False) is false + assert Xnor(False, False) is true + assert Xnor(True, A) == A + assert Xnor(False, A) == ~A + assert Xnor(True, False, False) is false + assert Xnor(True, False, A) == A + assert Xnor(False, False, A) == ~A + + +def test_Implies(): + raises(ValueError, lambda: Implies(A, B, C)) + assert Implies(True, True) is true + assert Implies(True, False) is false + assert Implies(False, True) is true + assert Implies(False, False) is true + assert Implies(0, A) is true + assert Implies(1, 1) is true + assert Implies(1, 0) is false + assert A >> B == B << A + assert (A < 1) >> (A >= 1) == (A >= 1) + assert (A < 1) >> (S.One > A) is true + assert A >> A is true + + +def test_Equivalent(): + assert Equivalent(A, B) == Equivalent(B, A) == Equivalent(A, B, A) + assert Equivalent() is true + assert Equivalent(A, A) == Equivalent(A) is true + assert Equivalent(True, True) == Equivalent(False, False) is true + assert Equivalent(True, False) == Equivalent(False, True) is false + assert Equivalent(A, True) == A + assert Equivalent(A, False) == Not(A) + assert Equivalent(A, B, True) == A & B + assert Equivalent(A, B, False) == ~A & ~B + assert Equivalent(1, A) == A + assert Equivalent(0, A) == Not(A) + assert Equivalent(A, Equivalent(B, C)) != Equivalent(Equivalent(A, B), C) + assert Equivalent(A < 1, A >= 1) is false + assert Equivalent(A < 1, A >= 1, 0) is false + assert Equivalent(A < 1, A >= 1, 1) is false + assert Equivalent(A < 1, S.One > A) == Equivalent(1, 1) == Equivalent(0, 0) + assert Equivalent(Equality(A, B), Equality(B, A)) is true + + +def test_Exclusive(): + assert Exclusive(False, False, False) is true + assert Exclusive(True, False, False) is true + assert Exclusive(True, True, False) is false + assert Exclusive(True, True, True) is false + + +def test_equals(): + assert Not(Or(A, B)).equals(And(Not(A), Not(B))) is True + assert Equivalent(A, B).equals((A >> B) & (B >> A)) is True + assert ((A | ~B) & (~A | B)).equals((~A & ~B) | (A & B)) is True + assert (A >> B).equals(~A >> ~B) is False + assert (A >> (B >> A)).equals(A >> (C >> A)) is False + raises(NotImplementedError, lambda: (A & B).equals(A > B)) + + +def test_simplification_boolalg(): + """ + Test working of simplification methods. + """ + set1 = [[0, 0, 1], [0, 1, 1], [1, 0, 0], [1, 1, 0]] + set2 = [[0, 0, 0], [0, 1, 0], [1, 0, 1], [1, 1, 1]] + assert SOPform([x, y, z], set1) == Or(And(Not(x), z), And(Not(z), x)) + assert Not(SOPform([x, y, z], set2)) == \ + Not(Or(And(Not(x), Not(z)), And(x, z))) + assert POSform([x, y, z], set1 + set2) is true + assert SOPform([x, y, z], set1 + set2) is true + assert SOPform([Dummy(), Dummy(), Dummy()], set1 + set2) is true + + minterms = [[0, 0, 0, 1], [0, 0, 1, 1], [0, 1, 1, 1], [1, 0, 1, 1], + [1, 1, 1, 1]] + dontcares = [[0, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 1]] + assert ( + SOPform([w, x, y, z], minterms, dontcares) == + Or(And(y, z), And(Not(w), Not(x)))) + assert POSform([w, x, y, z], minterms, dontcares) == And(Or(Not(w), y), z) + + minterms = [1, 3, 7, 11, 15] + dontcares = [0, 2, 5] + assert ( + SOPform([w, x, y, z], minterms, dontcares) == + Or(And(y, z), And(Not(w), Not(x)))) + assert POSform([w, x, y, z], minterms, dontcares) == And(Or(Not(w), y), z) + + minterms = [1, [0, 0, 1, 1], 7, [1, 0, 1, 1], + [1, 1, 1, 1]] + dontcares = [0, [0, 0, 1, 0], 5] + assert ( + SOPform([w, x, y, z], minterms, dontcares) == + Or(And(y, z), And(Not(w), Not(x)))) + assert POSform([w, x, y, z], minterms, dontcares) == And(Or(Not(w), y), z) + + minterms = [1, {y: 1, z: 1}] + dontcares = [0, [0, 0, 1, 0], 5] + assert ( + SOPform([w, x, y, z], minterms, dontcares) == + Or(And(y, z), And(Not(w), Not(x)))) + assert POSform([w, x, y, z], minterms, dontcares) == And(Or(Not(w), y), z) + + minterms = [{y: 1, z: 1}, 1] + dontcares = [[0, 0, 0, 0]] + + minterms = [[0, 0, 0]] + raises(ValueError, lambda: SOPform([w, x, y, z], minterms)) + raises(ValueError, lambda: POSform([w, x, y, z], minterms)) + + raises(TypeError, lambda: POSform([w, x, y, z], ["abcdefg"])) + + # test simplification + ans = And(A, Or(B, C)) + assert simplify_logic(A & (B | C)) == ans + assert simplify_logic((A & B) | (A & C)) == ans + assert simplify_logic(Implies(A, B)) == Or(Not(A), B) + assert simplify_logic(Equivalent(A, B)) == \ + Or(And(A, B), And(Not(A), Not(B))) + assert simplify_logic(And(Equality(A, 2), C)) == And(Equality(A, 2), C) + assert simplify_logic(And(Equality(A, 2), A)) == And(Equality(A, 2), A) + assert simplify_logic(And(Equality(A, B), C)) == And(Equality(A, B), C) + assert simplify_logic(Or(And(Equality(A, 3), B), And(Equality(A, 3), C))) \ + == And(Equality(A, 3), Or(B, C)) + b = (~x & ~y & ~z) | (~x & ~y & z) + e = And(A, b) + assert simplify_logic(e) == A & ~x & ~y + raises(ValueError, lambda: simplify_logic(A & (B | C), form='blabla')) + assert simplify(Or(x <= y, And(x < y, z))) == (x <= y) + assert simplify(Or(x <= y, And(y > x, z))) == (x <= y) + assert simplify(Or(x >= y, And(y < x, z))) == (x >= y) + + # Check that expressions with nine variables or more are not simplified + # (without the force-flag) + a, b, c, d, e, f, g, h, j = symbols('a b c d e f g h j') + expr = a & b & c & d & e & f & g & h & j | \ + a & b & c & d & e & f & g & h & ~j + # This expression can be simplified to get rid of the j variables + assert simplify_logic(expr) == expr + + # Test dontcare + assert simplify_logic((a & b) | c | d, dontcare=(a & b)) == c | d + + # check input + ans = SOPform([x, y], [[1, 0]]) + assert SOPform([x, y], [[1, 0]]) == ans + assert POSform([x, y], [[1, 0]]) == ans + + raises(ValueError, lambda: SOPform([x], [[1]], [[1]])) + assert SOPform([x], [[1]], [[0]]) is true + assert SOPform([x], [[0]], [[1]]) is true + assert SOPform([x], [], []) is false + + raises(ValueError, lambda: POSform([x], [[1]], [[1]])) + assert POSform([x], [[1]], [[0]]) is true + assert POSform([x], [[0]], [[1]]) is true + assert POSform([x], [], []) is false + + # check working of simplify + assert simplify((A & B) | (A & C)) == And(A, Or(B, C)) + assert simplify(And(x, Not(x))) == False + assert simplify(Or(x, Not(x))) == True + assert simplify(And(Eq(x, 0), Eq(x, y))) == And(Eq(x, 0), Eq(y, 0)) + assert And(Eq(x - 1, 0), Eq(x, y)).simplify() == And(Eq(x, 1), Eq(y, 1)) + assert And(Ne(x - 1, 0), Ne(x, y)).simplify() == And(Ne(x, 1), Ne(x, y)) + assert And(Eq(x - 1, 0), Ne(x, y)).simplify() == And(Eq(x, 1), Ne(y, 1)) + assert And(Eq(x - 1, 0), Eq(x, z + y), Eq(y + x, 0)).simplify( + ) == And(Eq(x, 1), Eq(y, -1), Eq(z, 2)) + assert And(Eq(x - 1, 0), Eq(x + 2, 3)).simplify() == Eq(x, 1) + assert And(Ne(x - 1, 0), Ne(x + 2, 3)).simplify() == Ne(x, 1) + assert And(Eq(x - 1, 0), Eq(x + 2, 2)).simplify() == False + assert And(Ne(x - 1, 0), Ne(x + 2, 2)).simplify( + ) == And(Ne(x, 1), Ne(x, 0)) + assert simplify(Xor(x, ~x)) == True + + +def test_bool_map(): + """ + Test working of bool_map function. + """ + + minterms = [[0, 0, 0, 1], [0, 0, 1, 1], [0, 1, 1, 1], [1, 0, 1, 1], + [1, 1, 1, 1]] + assert bool_map(Not(Not(a)), a) == (a, {a: a}) + assert bool_map(SOPform([w, x, y, z], minterms), + POSform([w, x, y, z], minterms)) == \ + (And(Or(Not(w), y), Or(Not(x), y), z), {x: x, w: w, z: z, y: y}) + assert bool_map(SOPform([x, z, y], [[1, 0, 1]]), + SOPform([a, b, c], [[1, 0, 1]])) != False + function1 = SOPform([x, z, y], [[1, 0, 1], [0, 0, 1]]) + function2 = SOPform([a, b, c], [[1, 0, 1], [1, 0, 0]]) + assert bool_map(function1, function2) == \ + (function1, {y: a, z: b}) + assert bool_map(Xor(x, y), ~Xor(x, y)) == False + assert bool_map(And(x, y), Or(x, y)) is None + assert bool_map(And(x, y), And(x, y, z)) is None + # issue 16179 + assert bool_map(Xor(x, y, z), ~Xor(x, y, z)) == False + assert bool_map(Xor(a, x, y, z), ~Xor(a, x, y, z)) == False + + +def test_bool_symbol(): + """Test that mixing symbols with boolean values + works as expected""" + + assert And(A, True) == A + assert And(A, True, True) == A + assert And(A, False) is false + assert And(A, True, False) is false + assert Or(A, True) is true + assert Or(A, False) == A + + +def test_is_boolean(): + assert isinstance(True, Boolean) is False + assert isinstance(true, Boolean) is True + assert 1 == True + assert 1 != true + assert (1 == true) is False + assert 0 == False + assert 0 != false + assert (0 == false) is False + assert true.is_Boolean is True + assert (A & B).is_Boolean + assert (A | B).is_Boolean + assert (~A).is_Boolean + assert (A ^ B).is_Boolean + assert A.is_Boolean != isinstance(A, Boolean) + assert isinstance(A, Boolean) + + +def test_subs(): + assert (A & B).subs(A, True) == B + assert (A & B).subs(A, False) is false + assert (A & B).subs(B, True) == A + assert (A & B).subs(B, False) is false + assert (A & B).subs({A: True, B: True}) is true + assert (A | B).subs(A, True) is true + assert (A | B).subs(A, False) == B + assert (A | B).subs(B, True) is true + assert (A | B).subs(B, False) == A + assert (A | B).subs({A: True, B: True}) is true + + +""" +we test for axioms of boolean algebra +see https://en.wikipedia.org/wiki/Boolean_algebra_(structure) +""" + + +def test_commutative(): + """Test for commutativity of And and Or""" + A, B = map(Boolean, symbols('A,B')) + + assert A & B == B & A + assert A | B == B | A + + +def test_and_associativity(): + """Test for associativity of And""" + + assert (A & B) & C == A & (B & C) + + +def test_or_assicativity(): + assert ((A | B) | C) == (A | (B | C)) + + +def test_double_negation(): + a = Boolean() + assert ~(~a) == a + + +# test methods + +def test_eliminate_implications(): + assert eliminate_implications(Implies(A, B, evaluate=False)) == (~A) | B + assert eliminate_implications( + A >> (C >> Not(B))) == Or(Or(Not(B), Not(C)), Not(A)) + assert eliminate_implications(Equivalent(A, B, C, D)) == \ + (~A | B) & (~B | C) & (~C | D) & (~D | A) + + +def test_conjuncts(): + assert conjuncts(A & B & C) == {A, B, C} + assert conjuncts((A | B) & C) == {A | B, C} + assert conjuncts(A) == {A} + assert conjuncts(True) == {True} + assert conjuncts(False) == {False} + + +def test_disjuncts(): + assert disjuncts(A | B | C) == {A, B, C} + assert disjuncts((A | B) & C) == {(A | B) & C} + assert disjuncts(A) == {A} + assert disjuncts(True) == {True} + assert disjuncts(False) == {False} + + +def test_distribute(): + assert distribute_and_over_or(Or(And(A, B), C)) == And(Or(A, C), Or(B, C)) + assert distribute_or_over_and(And(A, Or(B, C))) == Or(And(A, B), And(A, C)) + assert distribute_xor_over_and(And(A, Xor(B, C))) == Xor(And(A, B), And(A, C)) + + +def test_to_anf(): + x, y, z = symbols('x,y,z') + assert to_anf(And(x, y)) == And(x, y) + assert to_anf(Or(x, y)) == Xor(x, y, And(x, y)) + assert to_anf(Or(Implies(x, y), And(x, y), y)) == \ + Xor(x, True, x & y, remove_true=False) + assert to_anf(Or(Nand(x, y), Nor(x, y), Xnor(x, y), Implies(x, y))) == True + assert to_anf(Or(x, Not(y), Nor(x, z), And(x, y), Nand(y, z))) == \ + Xor(True, And(y, z), And(x, y, z), remove_true=False) + assert to_anf(Xor(x, y)) == Xor(x, y) + assert to_anf(Not(x)) == Xor(x, True, remove_true=False) + assert to_anf(Nand(x, y)) == Xor(True, And(x, y), remove_true=False) + assert to_anf(Nor(x, y)) == Xor(x, y, True, And(x, y), remove_true=False) + assert to_anf(Implies(x, y)) == Xor(x, True, And(x, y), remove_true=False) + assert to_anf(Equivalent(x, y)) == Xor(x, y, True, remove_true=False) + assert to_anf(Nand(x | y, x >> y), deep=False) == \ + Xor(True, And(Or(x, y), Implies(x, y)), remove_true=False) + assert to_anf(Nor(x ^ y, x & y), deep=False) == \ + Xor(True, Or(Xor(x, y), And(x, y)), remove_true=False) + # issue 25218 + assert to_anf(x ^ ~(x ^ y ^ ~y)) == False + + +def test_to_nnf(): + assert to_nnf(true) is true + assert to_nnf(false) is false + assert to_nnf(A) == A + assert to_nnf(A | ~A | B) is true + assert to_nnf(A & ~A & B) is false + assert to_nnf(A >> B) == ~A | B + assert to_nnf(Equivalent(A, B, C)) == (~A | B) & (~B | C) & (~C | A) + assert to_nnf(A ^ B ^ C) == \ + (A | B | C) & (~A | ~B | C) & (A | ~B | ~C) & (~A | B | ~C) + assert to_nnf(ITE(A, B, C)) == (~A | B) & (A | C) + assert to_nnf(Not(A | B | C)) == ~A & ~B & ~C + assert to_nnf(Not(A & B & C)) == ~A | ~B | ~C + assert to_nnf(Not(A >> B)) == A & ~B + assert to_nnf(Not(Equivalent(A, B, C))) == And(Or(A, B, C), Or(~A, ~B, ~C)) + assert to_nnf(Not(A ^ B ^ C)) == \ + (~A | B | C) & (A | ~B | C) & (A | B | ~C) & (~A | ~B | ~C) + assert to_nnf(Not(ITE(A, B, C))) == (~A | ~B) & (A | ~C) + assert to_nnf((A >> B) ^ (B >> A)) == (A & ~B) | (~A & B) + assert to_nnf((A >> B) ^ (B >> A), False) == \ + (~A | ~B | A | B) & ((A & ~B) | (~A & B)) + assert ITE(A, 1, 0).to_nnf() == A + assert ITE(A, 0, 1).to_nnf() == ~A + # although ITE can hold non-Boolean, it will complain if + # an attempt is made to convert the ITE to Boolean nnf + raises(TypeError, lambda: ITE(A < 1, [1], B).to_nnf()) + + +def test_to_cnf(): + assert to_cnf(~(B | C)) == And(Not(B), Not(C)) + assert to_cnf((A & B) | C) == And(Or(A, C), Or(B, C)) + assert to_cnf(A >> B) == (~A) | B + assert to_cnf(A >> (B & C)) == (~A | B) & (~A | C) + assert to_cnf(A & (B | C) | ~A & (B | C), True) == B | C + assert to_cnf(A & B) == And(A, B) + + assert to_cnf(Equivalent(A, B)) == And(Or(A, Not(B)), Or(B, Not(A))) + assert to_cnf(Equivalent(A, B & C)) == \ + (~A | B) & (~A | C) & (~B | ~C | A) + assert to_cnf(Equivalent(A, B | C), True) == \ + And(Or(Not(B), A), Or(Not(C), A), Or(B, C, Not(A))) + assert to_cnf(A + 1) == A + 1 + + +def test_issue_18904(): + x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15 = symbols('x1:16') + eq = ((x1 & x2 & x3 & x4 & x5 & x6 & x7 & x8 & x9) | + (x1 & x2 & x3 & x4 & x5 & x6 & x7 & x10 & x9) | + (x1 & x11 & x3 & x12 & x5 & x13 & x14 & x15 & x9)) + assert is_cnf(to_cnf(eq)) + raises(ValueError, lambda: to_cnf(eq, simplify=True)) + for f, t in zip((And, Or), (to_cnf, to_dnf)): + eq = f(x1, x2, x3, x4, x5, x6, x7, x8, x9) + raises(ValueError, lambda: to_cnf(eq, simplify=True)) + assert t(eq, simplify=True, force=True) == eq + + +def test_issue_9949(): + assert is_cnf(to_cnf((b > -5) | (a > 2) & (a < 4))) + + +def test_to_CNF(): + assert CNF.CNF_to_cnf(CNF.to_CNF(~(B | C))) == to_cnf(~(B | C)) + assert CNF.CNF_to_cnf(CNF.to_CNF((A & B) | C)) == to_cnf((A & B) | C) + assert CNF.CNF_to_cnf(CNF.to_CNF(A >> B)) == to_cnf(A >> B) + assert CNF.CNF_to_cnf(CNF.to_CNF(A >> (B & C))) == to_cnf(A >> (B & C)) + assert CNF.CNF_to_cnf(CNF.to_CNF(A & (B | C) | ~A & (B | C))) == to_cnf(A & (B | C) | ~A & (B | C)) + assert CNF.CNF_to_cnf(CNF.to_CNF(A & B)) == to_cnf(A & B) + + +def test_to_dnf(): + assert to_dnf(~(B | C)) == And(Not(B), Not(C)) + assert to_dnf(A & (B | C)) == Or(And(A, B), And(A, C)) + assert to_dnf(A >> B) == (~A) | B + assert to_dnf(A >> (B & C)) == (~A) | (B & C) + assert to_dnf(A | B) == A | B + + assert to_dnf(Equivalent(A, B), True) == \ + Or(And(A, B), And(Not(A), Not(B))) + assert to_dnf(Equivalent(A, B & C), True) == \ + Or(And(A, B, C), And(Not(A), Not(B)), And(Not(A), Not(C))) + assert to_dnf(A + 1) == A + 1 + + +def test_to_int_repr(): + x, y, z = map(Boolean, symbols('x,y,z')) + + def sorted_recursive(arg): + try: + return sorted(sorted_recursive(x) for x in arg) + except TypeError: # arg is not a sequence + return arg + + assert sorted_recursive(to_int_repr([x | y, z | x], [x, y, z])) == \ + sorted_recursive([[1, 2], [1, 3]]) + assert sorted_recursive(to_int_repr([x | y, z | ~x], [x, y, z])) == \ + sorted_recursive([[1, 2], [3, -1]]) + + +def test_is_anf(): + x, y = symbols('x,y') + assert is_anf(true) is True + assert is_anf(false) is True + assert is_anf(x) is True + assert is_anf(And(x, y)) is True + assert is_anf(Xor(x, y, And(x, y))) is True + assert is_anf(Xor(x, y, Or(x, y))) is False + assert is_anf(Xor(Not(x), y)) is False + + +def test_is_nnf(): + assert is_nnf(true) is True + assert is_nnf(A) is True + assert is_nnf(~A) is True + assert is_nnf(A & B) is True + assert is_nnf((A & B) | (~A & A) | (~B & B) | (~A & ~B), False) is True + assert is_nnf((A | B) & (~A | ~B)) is True + assert is_nnf(Not(Or(A, B))) is False + assert is_nnf(A ^ B) is False + assert is_nnf((A & B) | (~A & A) | (~B & B) | (~A & ~B), True) is False + + +def test_is_cnf(): + assert is_cnf(x) is True + assert is_cnf(x | y | z) is True + assert is_cnf(x & y & z) is True + assert is_cnf((x | y) & z) is True + assert is_cnf((x & y) | z) is False + assert is_cnf(~(x & y) | z) is False + + +def test_is_dnf(): + assert is_dnf(x) is True + assert is_dnf(x | y | z) is True + assert is_dnf(x & y & z) is True + assert is_dnf((x & y) | z) is True + assert is_dnf((x | y) & z) is False + assert is_dnf(~(x | y) & z) is False + + +def test_ITE(): + A, B, C = symbols('A:C') + assert ITE(True, False, True) is false + assert ITE(True, True, False) is true + assert ITE(False, True, False) is false + assert ITE(False, False, True) is true + assert isinstance(ITE(A, B, C), ITE) + + A = True + assert ITE(A, B, C) == B + A = False + assert ITE(A, B, C) == C + B = True + assert ITE(And(A, B), B, C) == C + assert ITE(Or(A, False), And(B, True), False) is false + assert ITE(x, A, B) == Not(x) + assert ITE(x, B, A) == x + assert ITE(1, x, y) == x + assert ITE(0, x, y) == y + raises(TypeError, lambda: ITE(2, x, y)) + raises(TypeError, lambda: ITE(1, [], y)) + raises(TypeError, lambda: ITE(1, (), y)) + raises(TypeError, lambda: ITE(1, y, [])) + assert ITE(1, 1, 1) is S.true + assert isinstance(ITE(1, 1, 1, evaluate=False), ITE) + + assert ITE(Eq(x, True), y, x) == ITE(x, y, x) + assert ITE(Eq(x, False), y, x) == ITE(~x, y, x) + assert ITE(Ne(x, True), y, x) == ITE(~x, y, x) + assert ITE(Ne(x, False), y, x) == ITE(x, y, x) + assert ITE(Eq(S.true, x), y, x) == ITE(x, y, x) + assert ITE(Eq(S.false, x), y, x) == ITE(~x, y, x) + assert ITE(Ne(S.true, x), y, x) == ITE(~x, y, x) + assert ITE(Ne(S.false, x), y, x) == ITE(x, y, x) + # 0 and 1 in the context are not treated as True/False + # so the equality must always be False since dissimilar + # objects cannot be equal + assert ITE(Eq(x, 0), y, x) == x + assert ITE(Eq(x, 1), y, x) == x + assert ITE(Ne(x, 0), y, x) == y + assert ITE(Ne(x, 1), y, x) == y + assert ITE(Eq(x, 0), y, z).subs(x, 0) == y + assert ITE(Eq(x, 0), y, z).subs(x, 1) == z + raises(ValueError, lambda: ITE(x > 1, y, x, z)) + + +def test_is_literal(): + assert is_literal(True) is True + assert is_literal(False) is True + assert is_literal(A) is True + assert is_literal(~A) is True + assert is_literal(Or(A, B)) is False + assert is_literal(Q.zero(A)) is True + assert is_literal(Not(Q.zero(A))) is True + assert is_literal(Or(A, B)) is False + assert is_literal(And(Q.zero(A), Q.zero(B))) is False + assert is_literal(x < 3) + assert not is_literal(x + y < 3) + + +def test_operators(): + # Mostly test __and__, __rand__, and so on + assert True & A == A & True == A + assert False & A == A & False == False + assert A & B == And(A, B) + assert True | A == A | True == True + assert False | A == A | False == A + assert A | B == Or(A, B) + assert ~A == Not(A) + assert True >> A == A << True == A + assert False >> A == A << False == True + assert A >> True == True << A == True + assert A >> False == False << A == ~A + assert A >> B == B << A == Implies(A, B) + assert True ^ A == A ^ True == ~A + assert False ^ A == A ^ False == A + assert A ^ B == Xor(A, B) + + +def test_true_false(): + assert true is S.true + assert false is S.false + assert true is not True + assert false is not False + assert true + assert not false + assert true == True + assert false == False + assert not (true == False) + assert not (false == True) + assert not (true == false) + + assert hash(true) == hash(True) + assert hash(false) == hash(False) + assert len({true, True}) == len({false, False}) == 1 + + assert isinstance(true, BooleanAtom) + assert isinstance(false, BooleanAtom) + # We don't want to subclass from bool, because bool subclasses from + # int. But operators like &, |, ^, <<, >>, and ~ act differently on 0 and + # 1 then we want them to on true and false. See the docstrings of the + # various And, Or, etc. functions for examples. + assert not isinstance(true, bool) + assert not isinstance(false, bool) + + # Note: using 'is' comparison is important here. We want these to return + # true and false, not True and False + + assert Not(true) is false + assert Not(True) is false + assert Not(false) is true + assert Not(False) is true + assert ~true is false + assert ~false is true + + for T, F in product((True, true), (False, false)): + assert And(T, F) is false + assert And(F, T) is false + assert And(F, F) is false + assert And(T, T) is true + assert And(T, x) == x + assert And(F, x) is false + if not (T is True and F is False): + assert T & F is false + assert F & T is false + if F is not False: + assert F & F is false + if T is not True: + assert T & T is true + + assert Or(T, F) is true + assert Or(F, T) is true + assert Or(F, F) is false + assert Or(T, T) is true + assert Or(T, x) is true + assert Or(F, x) == x + if not (T is True and F is False): + assert T | F is true + assert F | T is true + if F is not False: + assert F | F is false + if T is not True: + assert T | T is true + + assert Xor(T, F) is true + assert Xor(F, T) is true + assert Xor(F, F) is false + assert Xor(T, T) is false + assert Xor(T, x) == ~x + assert Xor(F, x) == x + if not (T is True and F is False): + assert T ^ F is true + assert F ^ T is true + if F is not False: + assert F ^ F is false + if T is not True: + assert T ^ T is false + + assert Nand(T, F) is true + assert Nand(F, T) is true + assert Nand(F, F) is true + assert Nand(T, T) is false + assert Nand(T, x) == ~x + assert Nand(F, x) is true + + assert Nor(T, F) is false + assert Nor(F, T) is false + assert Nor(F, F) is true + assert Nor(T, T) is false + assert Nor(T, x) is false + assert Nor(F, x) == ~x + + assert Implies(T, F) is false + assert Implies(F, T) is true + assert Implies(F, F) is true + assert Implies(T, T) is true + assert Implies(T, x) == x + assert Implies(F, x) is true + assert Implies(x, T) is true + assert Implies(x, F) == ~x + if not (T is True and F is False): + assert T >> F is false + assert F << T is false + assert F >> T is true + assert T << F is true + if F is not False: + assert F >> F is true + assert F << F is true + if T is not True: + assert T >> T is true + assert T << T is true + + assert Equivalent(T, F) is false + assert Equivalent(F, T) is false + assert Equivalent(F, F) is true + assert Equivalent(T, T) is true + assert Equivalent(T, x) == x + assert Equivalent(F, x) == ~x + assert Equivalent(x, T) == x + assert Equivalent(x, F) == ~x + + assert ITE(T, T, T) is true + assert ITE(T, T, F) is true + assert ITE(T, F, T) is false + assert ITE(T, F, F) is false + assert ITE(F, T, T) is true + assert ITE(F, T, F) is false + assert ITE(F, F, T) is true + assert ITE(F, F, F) is false + + assert all(i.simplify(1, 2) is i for i in (S.true, S.false)) + + +def test_bool_as_set(): + assert ITE(y <= 0, False, y >= 1).as_set() == Interval(1, oo) + assert And(x <= 2, x >= -2).as_set() == Interval(-2, 2) + assert Or(x >= 2, x <= -2).as_set() == Interval(-oo, -2) + Interval(2, oo) + assert Not(x > 2).as_set() == Interval(-oo, 2) + # issue 10240 + assert Not(And(x > 2, x < 3)).as_set() == \ + Union(Interval(-oo, 2), Interval(3, oo)) + assert true.as_set() == S.UniversalSet + assert false.as_set() is S.EmptySet + assert x.as_set() == S.UniversalSet + assert And(Or(x < 1, x > 3), x < 2).as_set() == Interval.open(-oo, 1) + assert And(x < 1, sin(x) < 3).as_set() == (x < 1).as_set() + raises(NotImplementedError, lambda: (sin(x) < 1).as_set()) + # watch for object morph in as_set + assert Eq(-1, cos(2 * x) ** 2 / sin(2 * x) ** 2).as_set() is S.EmptySet + + +@XFAIL +def test_multivariate_bool_as_set(): + x, y = symbols('x,y') + + assert And(x >= 0, y >= 0).as_set() == Interval(0, oo) * Interval(0, oo) + assert Or(x >= 0, y >= 0).as_set() == S.Reals * S.Reals - \ + Interval(-oo, 0, True, True) * Interval(-oo, 0, True, True) + + +def test_all_or_nothing(): + x = symbols('x', extended_real=True) + args = x >= -oo, x <= oo + v = And(*args) + if v.func is And: + assert len(v.args) == len(args) - args.count(S.true) + else: + assert v == True + v = Or(*args) + if v.func is Or: + assert len(v.args) == 2 + else: + assert v == True + + +def test_canonical_atoms(): + assert true.canonical == true + assert false.canonical == false + + +def test_negated_atoms(): + assert true.negated == false + assert false.negated == true + + +def test_issue_8777(): + assert And(x > 2, x < oo).as_set() == Interval(2, oo, left_open=True) + assert And(x >= 1, x < oo).as_set() == Interval(1, oo) + assert (x < oo).as_set() == Interval(-oo, oo) + assert (x > -oo).as_set() == Interval(-oo, oo) + + +def test_issue_8975(): + assert Or(And(-oo < x, x <= -2), And(2 <= x, x < oo)).as_set() == \ + Interval(-oo, -2) + Interval(2, oo) + + +def test_term_to_integer(): + assert term_to_integer([1, 0, 1, 0, 0, 1, 0]) == 82 + assert term_to_integer('0010101000111001') == 10809 + + +def test_issue_21971(): + a, b, c, d = symbols('a b c d') + f = a & b & c | a & c + assert f.subs(a & c, d) == b & d | d + assert f.subs(a & b & c, d) == a & c | d + + f = (a | b | c) & (a | c) + assert f.subs(a | c, d) == (b | d) & d + assert f.subs(a | b | c, d) == (a | c) & d + + f = (a ^ b ^ c) & (a ^ c) + assert f.subs(a ^ c, d) == (b ^ d) & d + assert f.subs(a ^ b ^ c, d) == (a ^ c) & d + + +def test_truth_table(): + assert list(truth_table(And(x, y), [x, y], input=False)) == \ + [False, False, False, True] + assert list(truth_table(x | y, [x, y], input=False)) == \ + [False, True, True, True] + assert list(truth_table(x >> y, [x, y], input=False)) == \ + [True, True, False, True] + assert list(truth_table(And(x, y), [x, y])) == \ + [([0, 0], False), ([0, 1], False), ([1, 0], False), ([1, 1], True)] + + +def test_issue_8571(): + for t in (S.true, S.false): + raises(TypeError, lambda: +t) + raises(TypeError, lambda: -t) + raises(TypeError, lambda: abs(t)) + # use int(bool(t)) to get 0 or 1 + raises(TypeError, lambda: int(t)) + + for o in [S.Zero, S.One, x]: + for _ in range(2): + raises(TypeError, lambda: o + t) + raises(TypeError, lambda: o - t) + raises(TypeError, lambda: o % t) + raises(TypeError, lambda: o * t) + raises(TypeError, lambda: o / t) + raises(TypeError, lambda: o ** t) + o, t = t, o # do again in reversed order + + +def test_expand_relational(): + n = symbols('n', negative=True) + p, q = symbols('p q', positive=True) + r = ((n + q * (-n / q + 1)) / (q * (-n / q + 1)) < 0) + assert r is not S.false + assert r.expand() is S.false + assert (q > 0).expand() is S.true + + +def test_issue_12717(): + assert S.true.is_Atom == True + assert S.false.is_Atom == True + + +def test_as_Boolean(): + nz = symbols('nz', nonzero=True) + assert all(as_Boolean(i) is S.true for i in (True, S.true, 1, nz)) + z = symbols('z', zero=True) + assert all(as_Boolean(i) is S.false for i in (False, S.false, 0, z)) + assert all(as_Boolean(i) == i for i in (x, x < 0)) + for i in (2, S(2), x + 1, []): + raises(TypeError, lambda: as_Boolean(i)) + + +def test_binary_symbols(): + assert ITE(x < 1, y, z).binary_symbols == {y, z} + for f in (Eq, Ne): + assert f(x, 1).binary_symbols == set() + assert f(x, True).binary_symbols == {x} + assert f(x, False).binary_symbols == {x} + assert S.true.binary_symbols == set() + assert S.false.binary_symbols == set() + assert x.binary_symbols == {x} + assert And(x, Eq(y, False), Eq(z, 1)).binary_symbols == {x, y} + assert Q.prime(x).binary_symbols == set() + assert Q.lt(x, 1).binary_symbols == set() + assert Q.is_true(x).binary_symbols == {x} + assert Q.eq(x, True).binary_symbols == {x} + assert Q.prime(x).binary_symbols == set() + + +def test_BooleanFunction_diff(): + assert And(x, y).diff(x) == Piecewise((0, Eq(y, False)), (1, True)) + + +def test_issue_14700(): + A, B, C, D, E, F, G, H = symbols('A B C D E F G H') + q = ((B & D & H & ~F) | (B & H & ~C & ~D) | (B & H & ~C & ~F) | + (B & H & ~D & ~G) | (B & H & ~F & ~G) | (C & G & ~B & ~D) | + (C & G & ~D & ~H) | (C & G & ~F & ~H) | (D & F & H & ~B) | + (D & F & ~G & ~H) | (B & D & F & ~C & ~H) | (D & E & F & ~B & ~C) | + (D & F & ~A & ~B & ~C) | (D & F & ~A & ~C & ~H) | + (A & B & D & F & ~E & ~H)) + soldnf = ((B & D & H & ~F) | (D & F & H & ~B) | (B & H & ~C & ~D) | + (B & H & ~D & ~G) | (C & G & ~B & ~D) | (C & G & ~D & ~H) | + (C & G & ~F & ~H) | (D & F & ~G & ~H) | (D & E & F & ~C & ~H) | + (D & F & ~A & ~C & ~H) | (A & B & D & F & ~E & ~H)) + solcnf = ((B | C | D) & (B | D | G) & (C | D | H) & (C | F | H) & + (D | G | H) & (F | G | H) & (B | F | ~D | ~H) & + (~B | ~D | ~F | ~H) & (D | ~B | ~C | ~G | ~H) & + (A | H | ~C | ~D | ~F | ~G) & (H | ~C | ~D | ~E | ~F | ~G) & + (B | E | H | ~A | ~D | ~F | ~G)) + assert simplify_logic(q, "dnf") == soldnf + assert simplify_logic(q, "cnf") == solcnf + + minterms = [[0, 1, 0, 0], [0, 1, 0, 1], [0, 1, 1, 0], [0, 1, 1, 1], + [0, 0, 1, 1], [1, 0, 1, 1]] + dontcares = [[1, 0, 0, 0], [1, 0, 0, 1], [1, 1, 0, 0], [1, 1, 0, 1]] + assert SOPform([w, x, y, z], minterms) == (x & ~w) | (y & z & ~x) + # Should not be more complicated with don't cares + assert SOPform([w, x, y, z], minterms, dontcares) == \ + (x & ~w) | (y & z & ~x) + + +def test_issue_25115(): + cond = Contains(x, S.Integers) + # Previously this raised an exception: + assert simplify_logic(cond) == cond + + +def test_relational_simplification(): + w, x, y, z = symbols('w x y z', real=True) + d, e = symbols('d e', real=False) + # Test all combinations or sign and order + assert Or(x >= y, x < y).simplify() == S.true + assert Or(x >= y, y > x).simplify() == S.true + assert Or(x >= y, -x > -y).simplify() == S.true + assert Or(x >= y, -y < -x).simplify() == S.true + assert Or(-x <= -y, x < y).simplify() == S.true + assert Or(-x <= -y, -x > -y).simplify() == S.true + assert Or(-x <= -y, y > x).simplify() == S.true + assert Or(-x <= -y, -y < -x).simplify() == S.true + assert Or(y <= x, x < y).simplify() == S.true + assert Or(y <= x, y > x).simplify() == S.true + assert Or(y <= x, -x > -y).simplify() == S.true + assert Or(y <= x, -y < -x).simplify() == S.true + assert Or(-y >= -x, x < y).simplify() == S.true + assert Or(-y >= -x, y > x).simplify() == S.true + assert Or(-y >= -x, -x > -y).simplify() == S.true + assert Or(-y >= -x, -y < -x).simplify() == S.true + + assert Or(x < y, x >= y).simplify() == S.true + assert Or(y > x, x >= y).simplify() == S.true + assert Or(-x > -y, x >= y).simplify() == S.true + assert Or(-y < -x, x >= y).simplify() == S.true + assert Or(x < y, -x <= -y).simplify() == S.true + assert Or(-x > -y, -x <= -y).simplify() == S.true + assert Or(y > x, -x <= -y).simplify() == S.true + assert Or(-y < -x, -x <= -y).simplify() == S.true + assert Or(x < y, y <= x).simplify() == S.true + assert Or(y > x, y <= x).simplify() == S.true + assert Or(-x > -y, y <= x).simplify() == S.true + assert Or(-y < -x, y <= x).simplify() == S.true + assert Or(x < y, -y >= -x).simplify() == S.true + assert Or(y > x, -y >= -x).simplify() == S.true + assert Or(-x > -y, -y >= -x).simplify() == S.true + assert Or(-y < -x, -y >= -x).simplify() == S.true + + # Some other tests + assert Or(x >= y, w < z, x <= y).simplify() == S.true + assert And(x >= y, x < y).simplify() == S.false + assert Or(x >= y, Eq(y, x)).simplify() == (x >= y) + assert And(x >= y, Eq(y, x)).simplify() == Eq(x, y) + assert And(Eq(x, y), x >= 1, 2 < y, y >= 5, z < y).simplify() == \ + (Eq(x, y) & (x >= 1) & (y >= 5) & (y > z)) + assert Or(Eq(x, y), x >= y, w < y, z < y).simplify() == \ + (x >= y) | (y > z) | (w < y) + assert And(Eq(x, y), x >= y, w < y, y >= z, z < y).simplify() == \ + Eq(x, y) & (y > z) & (w < y) + # assert And(Eq(x, y), x >= y, w < y, y >= z, z < y).simplify(relational_minmax=True) == \ + # And(Eq(x, y), y > Max(w, z)) + # assert Or(Eq(x, y), x >= 1, 2 < y, y >= 5, z < y).simplify(relational_minmax=True) == \ + # (Eq(x, y) | (x >= 1) | (y > Min(2, z))) + assert And(Eq(x, y), x >= 1, 2 < y, y >= 5, z < y).simplify() == \ + (Eq(x, y) & (x >= 1) & (y >= 5) & (y > z)) + assert (Eq(x, y) & Eq(d, e) & (x >= y) & (d >= e)).simplify() == \ + (Eq(x, y) & Eq(d, e) & (d >= e)) + assert And(Eq(x, y), Eq(x, -y)).simplify() == And(Eq(x, 0), Eq(y, 0)) + assert Xor(x >= y, x <= y).simplify() == Ne(x, y) + assert And(x > 1, x < -1, Eq(x, y)).simplify() == S.false + # From #16690 + assert And(x >= y, Eq(y, 0)).simplify() == And(x >= 0, Eq(y, 0)) + assert Or(Ne(x, 1), Ne(x, 2)).simplify() == S.true + assert And(Eq(x, 1), Ne(2, x)).simplify() == Eq(x, 1) + assert Or(Eq(x, 1), Ne(2, x)).simplify() == Ne(x, 2) + + +def test_issue_8373(): + x = symbols('x', real=True) + assert Or(x < 1, x > -1).simplify() == S.true + assert Or(x < 1, x >= 1).simplify() == S.true + assert And(x < 1, x >= 1).simplify() == S.false + assert Or(x <= 1, x >= 1).simplify() == S.true + + +def test_issue_7950(): + x = symbols('x', real=True) + assert And(Eq(x, 1), Eq(x, 2)).simplify() == S.false + + +@slow +def test_relational_simplification_numerically(): + def test_simplification_numerically_function(original, simplified): + symb = original.free_symbols + n = len(symb) + valuelist = list(set(combinations(list(range(-(n - 1), n)) * n, n))) + for values in valuelist: + sublist = dict(zip(symb, values)) + originalvalue = original.subs(sublist) + simplifiedvalue = simplified.subs(sublist) + assert originalvalue == simplifiedvalue, "Original: {}\nand" \ + " simplified: {}\ndo not evaluate to the same value for {}" \ + "".format(original, simplified, sublist) + + w, x, y, z = symbols('w x y z', real=True) + d, e = symbols('d e', real=False) + + expressions = (And(Eq(x, y), x >= y, w < y, y >= z, z < y), + And(Eq(x, y), x >= 1, 2 < y, y >= 5, z < y), + Or(Eq(x, y), x >= 1, 2 < y, y >= 5, z < y), + And(x >= y, Eq(y, x)), + Or(And(Eq(x, y), x >= y, w < y, Or(y >= z, z < y)), + And(Eq(x, y), x >= 1, 2 < y, y >= -1, z < y)), + (Eq(x, y) & Eq(d, e) & (x >= y) & (d >= e)), + ) + + for expression in expressions: + test_simplification_numerically_function(expression, + expression.simplify()) + + +def test_relational_simplification_patterns_numerically(): + from sympy.core import Wild + from sympy.logic.boolalg import _simplify_patterns_and, \ + _simplify_patterns_or, _simplify_patterns_xor + a = Wild('a') + b = Wild('b') + c = Wild('c') + symb = [a, b, c] + patternlists = [[And, _simplify_patterns_and()], + [Or, _simplify_patterns_or()], + [Xor, _simplify_patterns_xor()]] + valuelist = list(set(combinations(list(range(-2, 3)) * 3, 3))) + # Skip combinations of +/-2 and 0, except for all 0 + valuelist = [v for v in valuelist if any(w % 2 for w in v) or not any(v)] + for func, patternlist in patternlists: + for pattern in patternlist: + original = func(*pattern[0].args) + simplified = pattern[1] + for values in valuelist: + sublist = dict(zip(symb, values)) + originalvalue = original.xreplace(sublist) + simplifiedvalue = simplified.xreplace(sublist) + assert originalvalue == simplifiedvalue, "Original: {}\nand" \ + " simplified: {}\ndo not evaluate to the same value for" \ + "{}".format(pattern[0], simplified, sublist) + + +def test_issue_16803(): + n = symbols('n') + # No simplification done, but should not raise an exception + assert ((n > 3) | (n < 0) | ((n > 0) & (n < 3))).simplify() == \ + (n > 3) | (n < 0) | ((n > 0) & (n < 3)) + + +def test_issue_17530(): + r = {x: oo, y: oo} + assert Or(x + y > 0, x - y < 0).subs(r) + assert not And(x + y < 0, x - y < 0).subs(r) + raises(TypeError, lambda: Or(x + y < 0, x - y < 0).subs(r)) + raises(TypeError, lambda: And(x + y > 0, x - y < 0).subs(r)) + raises(TypeError, lambda: And(x + y > 0, x - y < 0).subs(r)) + + +def test_anf_coeffs(): + assert anf_coeffs([1, 0]) == [1, 1] + assert anf_coeffs([0, 0, 0, 1]) == [0, 0, 0, 1] + assert anf_coeffs([0, 1, 1, 1]) == [0, 1, 1, 1] + assert anf_coeffs([1, 1, 1, 0]) == [1, 0, 0, 1] + assert anf_coeffs([1, 0, 0, 0]) == [1, 1, 1, 1] + assert anf_coeffs([1, 0, 0, 1]) == [1, 1, 1, 0] + assert anf_coeffs([1, 1, 0, 1]) == [1, 0, 1, 1] + + +def test_ANFform(): + x, y = symbols('x,y') + assert ANFform([x], [1, 1]) == True + assert ANFform([x], [0, 0]) == False + assert ANFform([x], [1, 0]) == Xor(x, True, remove_true=False) + assert ANFform([x, y], [1, 1, 1, 0]) == \ + Xor(True, And(x, y), remove_true=False) + + +def test_bool_minterm(): + x, y = symbols('x,y') + assert bool_minterm(3, [x, y]) == And(x, y) + assert bool_minterm([1, 0], [x, y]) == And(Not(y), x) + + +def test_bool_maxterm(): + x, y = symbols('x,y') + assert bool_maxterm(2, [x, y]) == Or(Not(x), y) + assert bool_maxterm([0, 1], [x, y]) == Or(Not(y), x) + + +def test_bool_monomial(): + x, y = symbols('x,y') + assert bool_monomial(1, [x, y]) == y + assert bool_monomial([1, 1], [x, y]) == And(x, y) + + +def test_check_pair(): + assert _check_pair([0, 1, 0], [0, 1, 1]) == 2 + assert _check_pair([0, 1, 0], [1, 1, 1]) == -1 + + +def test_issue_19114(): + expr = (B & C) | (A & ~C) | (~A & ~B) + # Expression is minimal, but there are multiple minimal forms possible + res1 = (A & B) | (C & ~A) | (~B & ~C) + result = to_dnf(expr, simplify=True) + assert result in (expr, res1) + + +def test_issue_20870(): + result = SOPform([a, b, c, d], [1, 2, 3, 4, 5, 6, 8, 9, 11, 12, 14, 15]) + expected = ((d & ~b) | (a & b & c) | (a & ~c & ~d) | + (b & ~a & ~c) | (c & ~a & ~d)) + assert result == expected + + +def test_convert_to_varsSOP(): + assert _convert_to_varsSOP([0, 1, 0], [x, y, z]) == And(Not(x), y, Not(z)) + assert _convert_to_varsSOP([3, 1, 0], [x, y, z]) == And(y, Not(z)) + + +def test_convert_to_varsPOS(): + assert _convert_to_varsPOS([0, 1, 0], [x, y, z]) == Or(x, Not(y), z) + assert _convert_to_varsPOS([3, 1, 0], [x, y, z]) == Or(Not(y), z) + + +def test_gateinputcount(): + a, b, c, d, e = symbols('a:e') + assert gateinputcount(And(a, b)) == 2 + assert gateinputcount(a | b & c & d ^ (e | a)) == 9 + assert gateinputcount(And(a, True)) == 0 + raises(TypeError, lambda: gateinputcount(a * b)) + + +def test_refine(): + # relational + assert not refine(x < 0, ~(x < 0)) + assert refine(x < 0, (x < 0)) + assert refine(x < 0, (0 > x)) is S.true + assert refine(x < 0, (y < 0)) == (x < 0) + assert not refine(x <= 0, ~(x <= 0)) + assert refine(x <= 0, (x <= 0)) + assert refine(x <= 0, (0 >= x)) is S.true + assert refine(x <= 0, (y <= 0)) == (x <= 0) + assert not refine(x > 0, ~(x > 0)) + assert refine(x > 0, (x > 0)) + assert refine(x > 0, (0 < x)) is S.true + assert refine(x > 0, (y > 0)) == (x > 0) + assert not refine(x >= 0, ~(x >= 0)) + assert refine(x >= 0, (x >= 0)) + assert refine(x >= 0, (0 <= x)) is S.true + assert refine(x >= 0, (y >= 0)) == (x >= 0) + assert not refine(Eq(x, 0), ~(Eq(x, 0))) + assert refine(Eq(x, 0), (Eq(x, 0))) + assert refine(Eq(x, 0), (Eq(0, x))) is S.true + assert refine(Eq(x, 0), (Eq(y, 0))) == Eq(x, 0) + assert not refine(Ne(x, 0), ~(Ne(x, 0))) + assert refine(Ne(x, 0), (Ne(0, x))) is S.true + assert refine(Ne(x, 0), (Ne(x, 0))) + assert refine(Ne(x, 0), (Ne(y, 0))) == (Ne(x, 0)) + + # boolean functions + assert refine(And(x > 0, y > 0), (x > 0)) == (y > 0) + assert refine(And(x > 0, y > 0), (x > 0) & (y > 0)) is S.true + + # predicates + assert refine(Q.positive(x), Q.positive(x)) is S.true + assert refine(Q.positive(x), Q.negative(x)) is S.false + assert refine(Q.positive(x), Q.real(x)) == Q.positive(x) + + +def test_relational_threeterm_simplification_patterns_numerically(): + from sympy.core import Wild + from sympy.logic.boolalg import _simplify_patterns_and3 + a = Wild('a') + b = Wild('b') + c = Wild('c') + symb = [a, b, c] + patternlists = [[And, _simplify_patterns_and3()]] + valuelist = list(set(combinations(list(range(-2, 3)) * 3, 3))) + # Skip combinations of +/-2 and 0, except for all 0 + valuelist = [v for v in valuelist if any(w % 2 for w in v) or not any(v)] + for func, patternlist in patternlists: + for pattern in patternlist: + original = func(*pattern[0].args) + simplified = pattern[1] + for values in valuelist: + sublist = dict(zip(symb, values)) + originalvalue = original.xreplace(sublist) + simplifiedvalue = simplified.xreplace(sublist) + assert originalvalue == simplifiedvalue, "Original: {}\nand" \ + " simplified: {}\ndo not evaluate to the same value for" \ + "{}".format(pattern[0], simplified, sublist) + + +def test_issue_25451(): + x = Or(And(a, c), Eq(a, b)) + assert isinstance(x, Or) + assert set(x.args) == {And(a, c), Eq(a, b)} + + +def test_issue_26985(): + a, b, c, d = symbols('a b c d') + + # Expression before applying to_anf + x = Xor(c, And(a, b), And(a, c)) + y = Xor(a, b, And(a, c)) + + # Applying to_anf + result = Xor(Xor(d, And(x, y)), And(x, y)) + result_anf = to_anf(Xor(to_anf(Xor(d, And(x, y))), And(x, y))) + + assert result_anf == d + assert result == d diff --git a/venv/lib/python3.11/site-packages/sympy/logic/tests/test_dimacs.py b/venv/lib/python3.11/site-packages/sympy/logic/tests/test_dimacs.py new file mode 100644 index 0000000000000000000000000000000000000000..3a9a51a39d33fb807688614cb5809b621ce21a2c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/tests/test_dimacs.py @@ -0,0 +1,234 @@ +"""Various tests on satisfiability using dimacs cnf file syntax +You can find lots of cnf files in +ftp://dimacs.rutgers.edu/pub/challenge/satisfiability/benchmarks/cnf/ +""" + +from sympy.logic.utilities.dimacs import load +from sympy.logic.algorithms.dpll import dpll_satisfiable + + +def test_f1(): + assert bool(dpll_satisfiable(load(f1))) + + +def test_f2(): + assert bool(dpll_satisfiable(load(f2))) + + +def test_f3(): + assert bool(dpll_satisfiable(load(f3))) + + +def test_f4(): + assert not bool(dpll_satisfiable(load(f4))) + + +def test_f5(): + assert bool(dpll_satisfiable(load(f5))) + +f1 = """c simple example +c Resolution: SATISFIABLE +c +p cnf 3 2 +1 -3 0 +2 3 -1 0 +""" + + +f2 = """c an example from Quinn's text, 16 variables and 18 clauses. +c Resolution: SATISFIABLE +c +p cnf 16 18 + 1 2 0 + -2 -4 0 + 3 4 0 + -4 -5 0 + 5 -6 0 + 6 -7 0 + 6 7 0 + 7 -16 0 + 8 -9 0 + -8 -14 0 + 9 10 0 + 9 -10 0 +-10 -11 0 + 10 12 0 + 11 12 0 + 13 14 0 + 14 -15 0 + 15 16 0 +""" + +f3 = """c +p cnf 6 9 +-1 0 +-3 0 +2 -1 0 +2 -4 0 +5 -4 0 +-1 -3 0 +-4 -6 0 +1 3 -2 0 +4 6 -2 -5 0 +""" + +f4 = """c +c file: hole6.cnf [http://people.sc.fsu.edu/~jburkardt/data/cnf/hole6.cnf] +c +c SOURCE: John Hooker (jh38+@andrew.cmu.edu) +c +c DESCRIPTION: Pigeon hole problem of placing n (for file 'holen.cnf') pigeons +c in n+1 holes without placing 2 pigeons in the same hole +c +c NOTE: Part of the collection at the Forschungsinstitut fuer +c anwendungsorientierte Wissensverarbeitung in Ulm Germany. +c +c NOTE: Not satisfiable +c +p cnf 42 133 +-1 -7 0 +-1 -13 0 +-1 -19 0 +-1 -25 0 +-1 -31 0 +-1 -37 0 +-7 -13 0 +-7 -19 0 +-7 -25 0 +-7 -31 0 +-7 -37 0 +-13 -19 0 +-13 -25 0 +-13 -31 0 +-13 -37 0 +-19 -25 0 +-19 -31 0 +-19 -37 0 +-25 -31 0 +-25 -37 0 +-31 -37 0 +-2 -8 0 +-2 -14 0 +-2 -20 0 +-2 -26 0 +-2 -32 0 +-2 -38 0 +-8 -14 0 +-8 -20 0 +-8 -26 0 +-8 -32 0 +-8 -38 0 +-14 -20 0 +-14 -26 0 +-14 -32 0 +-14 -38 0 +-20 -26 0 +-20 -32 0 +-20 -38 0 +-26 -32 0 +-26 -38 0 +-32 -38 0 +-3 -9 0 +-3 -15 0 +-3 -21 0 +-3 -27 0 +-3 -33 0 +-3 -39 0 +-9 -15 0 +-9 -21 0 +-9 -27 0 +-9 -33 0 +-9 -39 0 +-15 -21 0 +-15 -27 0 +-15 -33 0 +-15 -39 0 +-21 -27 0 +-21 -33 0 +-21 -39 0 +-27 -33 0 +-27 -39 0 +-33 -39 0 +-4 -10 0 +-4 -16 0 +-4 -22 0 +-4 -28 0 +-4 -34 0 +-4 -40 0 +-10 -16 0 +-10 -22 0 +-10 -28 0 +-10 -34 0 +-10 -40 0 +-16 -22 0 +-16 -28 0 +-16 -34 0 +-16 -40 0 +-22 -28 0 +-22 -34 0 +-22 -40 0 +-28 -34 0 +-28 -40 0 +-34 -40 0 +-5 -11 0 +-5 -17 0 +-5 -23 0 +-5 -29 0 +-5 -35 0 +-5 -41 0 +-11 -17 0 +-11 -23 0 +-11 -29 0 +-11 -35 0 +-11 -41 0 +-17 -23 0 +-17 -29 0 +-17 -35 0 +-17 -41 0 +-23 -29 0 +-23 -35 0 +-23 -41 0 +-29 -35 0 +-29 -41 0 +-35 -41 0 +-6 -12 0 +-6 -18 0 +-6 -24 0 +-6 -30 0 +-6 -36 0 +-6 -42 0 +-12 -18 0 +-12 -24 0 +-12 -30 0 +-12 -36 0 +-12 -42 0 +-18 -24 0 +-18 -30 0 +-18 -36 0 +-18 -42 0 +-24 -30 0 +-24 -36 0 +-24 -42 0 +-30 -36 0 +-30 -42 0 +-36 -42 0 + 6 5 4 3 2 1 0 + 12 11 10 9 8 7 0 + 18 17 16 15 14 13 0 + 24 23 22 21 20 19 0 + 30 29 28 27 26 25 0 + 36 35 34 33 32 31 0 + 42 41 40 39 38 37 0 +""" + +f5 = """c simple example requiring variable selection +c +c NOTE: Satisfiable +c +p cnf 5 5 +1 2 3 0 +1 -2 3 0 +4 5 -3 0 +1 -4 -3 0 +-1 -5 0 +""" diff --git a/venv/lib/python3.11/site-packages/sympy/logic/tests/test_inference.py b/venv/lib/python3.11/site-packages/sympy/logic/tests/test_inference.py new file mode 100644 index 0000000000000000000000000000000000000000..ff37b1b104f6f106ec5df7809fd34959bce35917 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/tests/test_inference.py @@ -0,0 +1,396 @@ +"""For more tests on satisfiability, see test_dimacs""" + +from sympy.assumptions.ask import Q +from sympy.core.symbol import symbols +from sympy.core.relational import Unequality +from sympy.logic.boolalg import And, Or, Implies, Equivalent, true, false +from sympy.logic.inference import literal_symbol, \ + pl_true, satisfiable, valid, entails, PropKB +from sympy.logic.algorithms.dpll import dpll, dpll_satisfiable, \ + find_pure_symbol, find_unit_clause, unit_propagate, \ + find_pure_symbol_int_repr, find_unit_clause_int_repr, \ + unit_propagate_int_repr +from sympy.logic.algorithms.dpll2 import dpll_satisfiable as dpll2_satisfiable + +from sympy.logic.algorithms.z3_wrapper import z3_satisfiable +from sympy.assumptions.cnf import CNF, EncodedCNF +from sympy.logic.tests.test_lra_theory import make_random_problem +from sympy.core.random import randint + +from sympy.testing.pytest import raises, skip +from sympy.external import import_module + + +def test_literal(): + A, B = symbols('A,B') + assert literal_symbol(True) is True + assert literal_symbol(False) is False + assert literal_symbol(A) is A + assert literal_symbol(~A) is A + + +def test_find_pure_symbol(): + A, B, C = symbols('A,B,C') + assert find_pure_symbol([A], [A]) == (A, True) + assert find_pure_symbol([A, B], [~A | B, ~B | A]) == (None, None) + assert find_pure_symbol([A, B, C], [ A | ~B, ~B | ~C, C | A]) == (A, True) + assert find_pure_symbol([A, B, C], [~A | B, B | ~C, C | A]) == (B, True) + assert find_pure_symbol([A, B, C], [~A | ~B, ~B | ~C, C | A]) == (B, False) + assert find_pure_symbol( + [A, B, C], [~A | B, ~B | ~C, C | A]) == (None, None) + + +def test_find_pure_symbol_int_repr(): + assert find_pure_symbol_int_repr([1], [{1}]) == (1, True) + assert find_pure_symbol_int_repr([1, 2], + [{-1, 2}, {-2, 1}]) == (None, None) + assert find_pure_symbol_int_repr([1, 2, 3], + [{1, -2}, {-2, -3}, {3, 1}]) == (1, True) + assert find_pure_symbol_int_repr([1, 2, 3], + [{-1, 2}, {2, -3}, {3, 1}]) == (2, True) + assert find_pure_symbol_int_repr([1, 2, 3], + [{-1, -2}, {-2, -3}, {3, 1}]) == (2, False) + assert find_pure_symbol_int_repr([1, 2, 3], + [{-1, 2}, {-2, -3}, {3, 1}]) == (None, None) + + +def test_unit_clause(): + A, B, C = symbols('A,B,C') + assert find_unit_clause([A], {}) == (A, True) + assert find_unit_clause([A, ~A], {}) == (A, True) # Wrong ?? + assert find_unit_clause([A | B], {A: True}) == (B, True) + assert find_unit_clause([A | B], {B: True}) == (A, True) + assert find_unit_clause( + [A | B | C, B | ~C, A | ~B], {A: True}) == (B, False) + assert find_unit_clause([A | B | C, B | ~C, A | B], {A: True}) == (B, True) + assert find_unit_clause([A | B | C, B | ~C, A ], {}) == (A, True) + + +def test_unit_clause_int_repr(): + assert find_unit_clause_int_repr(map(set, [[1]]), {}) == (1, True) + assert find_unit_clause_int_repr(map(set, [[1], [-1]]), {}) == (1, True) + assert find_unit_clause_int_repr([{1, 2}], {1: True}) == (2, True) + assert find_unit_clause_int_repr([{1, 2}], {2: True}) == (1, True) + assert find_unit_clause_int_repr(map(set, + [[1, 2, 3], [2, -3], [1, -2]]), {1: True}) == (2, False) + assert find_unit_clause_int_repr(map(set, + [[1, 2, 3], [3, -3], [1, 2]]), {1: True}) == (2, True) + + A, B, C = symbols('A,B,C') + assert find_unit_clause([A | B | C, B | ~C, A ], {}) == (A, True) + + +def test_unit_propagate(): + A, B, C = symbols('A,B,C') + assert unit_propagate([A | B], A) == [] + assert unit_propagate([A | B, ~A | C, ~C | B, A], A) == [C, ~C | B, A] + + +def test_unit_propagate_int_repr(): + assert unit_propagate_int_repr([{1, 2}], 1) == [] + assert unit_propagate_int_repr(map(set, + [[1, 2], [-1, 3], [-3, 2], [1]]), 1) == [{3}, {-3, 2}] + + +def test_dpll(): + """This is also tested in test_dimacs""" + A, B, C = symbols('A,B,C') + assert dpll([A | B], [A, B], {A: True, B: True}) == {A: True, B: True} + + +def test_dpll_satisfiable(): + A, B, C = symbols('A,B,C') + assert dpll_satisfiable( A & ~A ) is False + assert dpll_satisfiable( A & ~B ) == {A: True, B: False} + assert dpll_satisfiable( + A | B ) in ({A: True}, {B: True}, {A: True, B: True}) + assert dpll_satisfiable( + (~A | B) & (~B | A) ) in ({A: True, B: True}, {A: False, B: False}) + assert dpll_satisfiable( (A | B) & (~B | C) ) in ({A: True, B: False}, + {A: True, C: True}, {B: True, C: True}) + assert dpll_satisfiable( A & B & C ) == {A: True, B: True, C: True} + assert dpll_satisfiable( (A | B) & (A >> B) ) == {B: True} + assert dpll_satisfiable( Equivalent(A, B) & A ) == {A: True, B: True} + assert dpll_satisfiable( Equivalent(A, B) & ~A ) == {A: False, B: False} + + +def test_dpll2_satisfiable(): + A, B, C = symbols('A,B,C') + assert dpll2_satisfiable( A & ~A ) is False + assert dpll2_satisfiable( A & ~B ) == {A: True, B: False} + assert dpll2_satisfiable( + A | B ) in ({A: True}, {B: True}, {A: True, B: True}) + assert dpll2_satisfiable( + (~A | B) & (~B | A) ) in ({A: True, B: True}, {A: False, B: False}) + assert dpll2_satisfiable( (A | B) & (~B | C) ) in ({A: True, B: False, C: True}, + {A: True, B: True, C: True}) + assert dpll2_satisfiable( A & B & C ) == {A: True, B: True, C: True} + assert dpll2_satisfiable( (A | B) & (A >> B) ) in ({B: True, A: False}, + {B: True, A: True}) + assert dpll2_satisfiable( Equivalent(A, B) & A ) == {A: True, B: True} + assert dpll2_satisfiable( Equivalent(A, B) & ~A ) == {A: False, B: False} + + +def test_minisat22_satisfiable(): + A, B, C = symbols('A,B,C') + minisat22_satisfiable = lambda expr: satisfiable(expr, algorithm="minisat22") + assert minisat22_satisfiable( A & ~A ) is False + assert minisat22_satisfiable( A & ~B ) == {A: True, B: False} + assert minisat22_satisfiable( + A | B ) in ({A: True}, {B: False}, {A: False, B: True}, {A: True, B: True}, {A: True, B: False}) + assert minisat22_satisfiable( + (~A | B) & (~B | A) ) in ({A: True, B: True}, {A: False, B: False}) + assert minisat22_satisfiable( (A | B) & (~B | C) ) in ({A: True, B: False, C: True}, + {A: True, B: True, C: True}, {A: False, B: True, C: True}, {A: True, B: False, C: False}) + assert minisat22_satisfiable( A & B & C ) == {A: True, B: True, C: True} + assert minisat22_satisfiable( (A | B) & (A >> B) ) in ({B: True, A: False}, + {B: True, A: True}) + assert minisat22_satisfiable( Equivalent(A, B) & A ) == {A: True, B: True} + assert minisat22_satisfiable( Equivalent(A, B) & ~A ) == {A: False, B: False} + +def test_minisat22_minimal_satisfiable(): + A, B, C = symbols('A,B,C') + minisat22_satisfiable = lambda expr, minimal=True: satisfiable(expr, algorithm="minisat22", minimal=True) + assert minisat22_satisfiable( A & ~A ) is False + assert minisat22_satisfiable( A & ~B ) == {A: True, B: False} + assert minisat22_satisfiable( + A | B ) in ({A: True}, {B: False}, {A: False, B: True}, {A: True, B: True}, {A: True, B: False}) + assert minisat22_satisfiable( + (~A | B) & (~B | A) ) in ({A: True, B: True}, {A: False, B: False}) + assert minisat22_satisfiable( (A | B) & (~B | C) ) in ({A: True, B: False, C: True}, + {A: True, B: True, C: True}, {A: False, B: True, C: True}, {A: True, B: False, C: False}) + assert minisat22_satisfiable( A & B & C ) == {A: True, B: True, C: True} + assert minisat22_satisfiable( (A | B) & (A >> B) ) in ({B: True, A: False}, + {B: True, A: True}) + assert minisat22_satisfiable( Equivalent(A, B) & A ) == {A: True, B: True} + assert minisat22_satisfiable( Equivalent(A, B) & ~A ) == {A: False, B: False} + g = satisfiable((A | B | C),algorithm="minisat22",minimal=True,all_models=True) + sol = next(g) + first_solution = {key for key, value in sol.items() if value} + sol=next(g) + second_solution = {key for key, value in sol.items() if value} + sol=next(g) + third_solution = {key for key, value in sol.items() if value} + assert not first_solution <= second_solution + assert not second_solution <= third_solution + assert not first_solution <= third_solution + +def test_satisfiable(): + A, B, C = symbols('A,B,C') + assert satisfiable(A & (A >> B) & ~B) is False + + +def test_valid(): + A, B, C = symbols('A,B,C') + assert valid(A >> (B >> A)) is True + assert valid((A >> (B >> C)) >> ((A >> B) >> (A >> C))) is True + assert valid((~B >> ~A) >> (A >> B)) is True + assert valid(A | B | C) is False + assert valid(A >> B) is False + + +def test_pl_true(): + A, B, C = symbols('A,B,C') + assert pl_true(True) is True + assert pl_true( A & B, {A: True, B: True}) is True + assert pl_true( A | B, {A: True}) is True + assert pl_true( A | B, {B: True}) is True + assert pl_true( A | B, {A: None, B: True}) is True + assert pl_true( A >> B, {A: False}) is True + assert pl_true( A | B | ~C, {A: False, B: True, C: True}) is True + assert pl_true(Equivalent(A, B), {A: False, B: False}) is True + + # test for false + assert pl_true(False) is False + assert pl_true( A & B, {A: False, B: False}) is False + assert pl_true( A & B, {A: False}) is False + assert pl_true( A & B, {B: False}) is False + assert pl_true( A | B, {A: False, B: False}) is False + + #test for None + assert pl_true(B, {B: None}) is None + assert pl_true( A & B, {A: True, B: None}) is None + assert pl_true( A >> B, {A: True, B: None}) is None + assert pl_true(Equivalent(A, B), {A: None}) is None + assert pl_true(Equivalent(A, B), {A: True, B: None}) is None + + # Test for deep + assert pl_true(A | B, {A: False}, deep=True) is None + assert pl_true(~A & ~B, {A: False}, deep=True) is None + assert pl_true(A | B, {A: False, B: False}, deep=True) is False + assert pl_true(A & B & (~A | ~B), {A: True}, deep=True) is False + assert pl_true((C >> A) >> (B >> A), {C: True}, deep=True) is True + + +def test_pl_true_wrong_input(): + from sympy.core.numbers import pi + raises(ValueError, lambda: pl_true('John Cleese')) + raises(ValueError, lambda: pl_true(42 + pi + pi ** 2)) + raises(ValueError, lambda: pl_true(42)) + + +def test_entails(): + A, B, C = symbols('A, B, C') + assert entails(A, [A >> B, ~B]) is False + assert entails(B, [Equivalent(A, B), A]) is True + assert entails((A >> B) >> (~A >> ~B)) is False + assert entails((A >> B) >> (~B >> ~A)) is True + + +def test_PropKB(): + A, B, C = symbols('A,B,C') + kb = PropKB() + assert kb.ask(A >> B) is False + assert kb.ask(A >> (B >> A)) is True + kb.tell(A >> B) + kb.tell(B >> C) + assert kb.ask(A) is False + assert kb.ask(B) is False + assert kb.ask(C) is False + assert kb.ask(~A) is False + assert kb.ask(~B) is False + assert kb.ask(~C) is False + assert kb.ask(A >> C) is True + kb.tell(A) + assert kb.ask(A) is True + assert kb.ask(B) is True + assert kb.ask(C) is True + assert kb.ask(~C) is False + kb.retract(A) + assert kb.ask(C) is False + + +def test_propKB_tolerant(): + """"tolerant to bad input""" + kb = PropKB() + A, B, C = symbols('A,B,C') + assert kb.ask(B) is False + +def test_satisfiable_non_symbols(): + x, y = symbols('x y') + assumptions = Q.zero(x*y) + facts = Implies(Q.zero(x*y), Q.zero(x) | Q.zero(y)) + query = ~Q.zero(x) & ~Q.zero(y) + refutations = [ + {Q.zero(x): True, Q.zero(x*y): True}, + {Q.zero(y): True, Q.zero(x*y): True}, + {Q.zero(x): True, Q.zero(y): True, Q.zero(x*y): True}, + {Q.zero(x): True, Q.zero(y): False, Q.zero(x*y): True}, + {Q.zero(x): False, Q.zero(y): True, Q.zero(x*y): True}] + assert not satisfiable(And(assumptions, facts, query), algorithm='dpll') + assert satisfiable(And(assumptions, facts, ~query), algorithm='dpll') in refutations + assert not satisfiable(And(assumptions, facts, query), algorithm='dpll2') + assert satisfiable(And(assumptions, facts, ~query), algorithm='dpll2') in refutations + +def test_satisfiable_bool(): + from sympy.core.singleton import S + assert satisfiable(true) == {true: true} + assert satisfiable(S.true) == {true: true} + assert satisfiable(false) is False + assert satisfiable(S.false) is False + + +def test_satisfiable_all_models(): + from sympy.abc import A, B + assert next(satisfiable(False, all_models=True)) is False + assert list(satisfiable((A >> ~A) & A, all_models=True)) == [False] + assert list(satisfiable(True, all_models=True)) == [{true: true}] + + models = [{A: True, B: False}, {A: False, B: True}] + result = satisfiable(A ^ B, all_models=True) + models.remove(next(result)) + models.remove(next(result)) + raises(StopIteration, lambda: next(result)) + assert not models + + assert list(satisfiable(Equivalent(A, B), all_models=True)) == \ + [{A: False, B: False}, {A: True, B: True}] + + models = [{A: False, B: False}, {A: False, B: True}, {A: True, B: True}] + for model in satisfiable(A >> B, all_models=True): + models.remove(model) + assert not models + + # This is a santiy test to check that only the required number + # of solutions are generated. The expr below has 2**100 - 1 models + # which would time out the test if all are generated at once. + from sympy.utilities.iterables import numbered_symbols + from sympy.logic.boolalg import Or + sym = numbered_symbols() + X = [next(sym) for i in range(100)] + result = satisfiable(Or(*X), all_models=True) + for i in range(10): + assert next(result) + + +def test_z3(): + z3 = import_module("z3") + + if not z3: + skip("z3 not installed.") + A, B, C = symbols('A,B,C') + x, y, z = symbols('x,y,z') + assert z3_satisfiable((x >= 2) & (x < 1)) is False + assert z3_satisfiable( A & ~A ) is False + + model = z3_satisfiable(A & (~A | B | C)) + assert bool(model) is True + assert model[A] is True + + # test nonlinear function + assert z3_satisfiable((x ** 2 >= 2) & (x < 1) & (x > -1)) is False + + +def test_z3_vs_lra_dpll2(): + z3 = import_module("z3") + if z3 is None: + skip("z3 not installed.") + + def boolean_formula_to_encoded_cnf(bf): + cnf = CNF.from_prop(bf) + enc = EncodedCNF() + enc.from_cnf(cnf) + return enc + + def make_random_cnf(num_clauses=5, num_constraints=10, num_var=2): + assert num_clauses <= num_constraints + constraints = make_random_problem(num_variables=num_var, num_constraints=num_constraints, rational=False) + clauses = [[cons] for cons in constraints[:num_clauses]] + for cons in constraints[num_clauses:]: + if isinstance(cons, Unequality): + cons = ~cons + i = randint(0, num_clauses-1) + clauses[i].append(cons) + + clauses = [Or(*clause) for clause in clauses] + cnf = And(*clauses) + return boolean_formula_to_encoded_cnf(cnf) + + lra_dpll2_satisfiable = lambda x: dpll2_satisfiable(x, use_lra_theory=True) + + for _ in range(50): + cnf = make_random_cnf(num_clauses=10, num_constraints=15, num_var=2) + + try: + z3_sat = z3_satisfiable(cnf) + except z3.z3types.Z3Exception: + continue + + lra_dpll2_sat = lra_dpll2_satisfiable(cnf) is not False + + assert z3_sat == lra_dpll2_sat + +def test_issue_27733(): + x, y = symbols('x,y') + clauses = [[1, -3, -2], [5, 7, -8, -6, -4], [-10, -9, 10, 11, -4], [-12, 13, 14], [-10, 9, -6, 11, -4], + [16, -15, 18, -19, -17], [11, -6, 10, -9], [9, 11, -10, -9], [2, -3, -1], [-13, 12], [-15, 3, -17], + [-16, -15, 19, -17], [-6, -9, 10, 11, -4], [20, -1, -2], [-23, -22, -21], [10, 11, -10, -9], + [9, 11, -4, -10], [24, -6, -4], [-14, 12], [-10, -9, 9, -6, 11], [25, -27, -26], [-15, 19, -18, -17], + [5, 8, -7, -6, -4], [-30, -29, 28], [12], [14]] + + encoding = {Q.gt(y, i): i for i in range(1, 31) if i != 11 and i != 12} + encoding[Q.gt(x, 0)] = 11 + encoding[Q.lt(x, 0)] = 12 + + cnf = EncodedCNF(clauses, encoding) + assert satisfiable(cnf, use_lra_theory=True) is False diff --git a/venv/lib/python3.11/site-packages/sympy/logic/tests/test_lra_theory.py b/venv/lib/python3.11/site-packages/sympy/logic/tests/test_lra_theory.py new file mode 100644 index 0000000000000000000000000000000000000000..207a3c5ba2c1b16ee5323382deee0863a5dfb595 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/tests/test_lra_theory.py @@ -0,0 +1,440 @@ +from sympy.core.numbers import Rational, I, oo +from sympy.core.relational import Eq +from sympy.core.symbol import symbols +from sympy.core.singleton import S +from sympy.matrices.dense import Matrix +from sympy.matrices.dense import randMatrix +from sympy.assumptions.ask import Q +from sympy.logic.boolalg import And +from sympy.abc import x, y, z +from sympy.assumptions.cnf import CNF, EncodedCNF +from sympy.functions.elementary.trigonometric import cos +from sympy.external import import_module + +from sympy.logic.algorithms.lra_theory import LRASolver, UnhandledInput, LRARational, HANDLE_NEGATION +from sympy.core.random import random, choice, randint +from sympy.core.sympify import sympify +from sympy.ntheory.generate import randprime +from sympy.core.relational import StrictLessThan, StrictGreaterThan +import itertools + +from sympy.testing.pytest import raises, XFAIL, skip + +def make_random_problem(num_variables=2, num_constraints=2, sparsity=.1, rational=True, + disable_strict = False, disable_nonstrict=False, disable_equality=False): + def rand(sparsity=sparsity): + if random() < sparsity: + return sympify(0) + if rational: + int1, int2 = [randprime(0, 50) for _ in range(2)] + return Rational(int1, int2) * choice([-1, 1]) + else: + return randint(1, 10) * choice([-1, 1]) + + variables = symbols('x1:%s' % (num_variables + 1)) + constraints = [] + for _ in range(num_constraints): + lhs, rhs = sum(rand() * x for x in variables), rand(sparsity=0) # sparsity=0 bc of bug with smtlib_code + options = [] + if not disable_equality: + options += [Eq(lhs, rhs)] + if not disable_nonstrict: + options += [lhs <= rhs, lhs >= rhs] + if not disable_strict: + options += [lhs < rhs, lhs > rhs] + + constraints.append(choice(options)) + + return constraints + +def check_if_satisfiable_with_z3(constraints): + from sympy.external.importtools import import_module + from sympy.printing.smtlib import smtlib_code + from sympy.logic.boolalg import And + boolean_formula = And(*constraints) + z3 = import_module("z3") + if z3: + smtlib_string = smtlib_code(boolean_formula) + s = z3.Solver() + s.from_string(smtlib_string) + res = str(s.check()) + if res == 'sat': + return True + elif res == 'unsat': + return False + else: + raise ValueError(f"z3 was not able to check the satisfiability of {boolean_formula}") + +def find_rational_assignment(constr, assignment, iter=20): + eps = sympify(1) + + for _ in range(iter): + assign = {key: val[0] + val[1]*eps for key, val in assignment.items()} + try: + for cons in constr: + assert cons.subs(assign) == True + return assign + except AssertionError: + eps = eps/2 + + return None + +def boolean_formula_to_encoded_cnf(bf): + cnf = CNF.from_prop(bf) + enc = EncodedCNF() + enc.from_cnf(cnf) + return enc + + +def test_from_encoded_cnf(): + s1, s2 = symbols("s1 s2") + + # Test preprocessing + # Example is from section 3 of paper. + phi = (x >= 0) & ((x + y <= 2) | (x + 2 * y - z >= 6)) & (Eq(x + y, 2) | (x + 2 * y - z > 4)) + enc = boolean_formula_to_encoded_cnf(phi) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + assert lra.A.shape == (2, 5) + assert str(lra.slack) == '[_s1, _s2]' + assert str(lra.nonslack) == '[x, y, z]' + assert lra.A == Matrix([[ 1, 1, 0, -1, 0], + [-1, -2, 1, 0, -1]]) + assert {(str(b.var), b.bound, b.upper, b.equality, b.strict) for b in lra.enc_to_boundary.values()} == {('_s1', 2, None, True, False), + ('_s1', 2, True, False, False), + ('_s2', -4, True, False, True), + ('_s2', -6, True, False, False), + ('x', 0, False, False, False)} + + +def test_problem(): + from sympy.logic.algorithms.lra_theory import LRASolver + from sympy.assumptions.cnf import CNF, EncodedCNF + cons = [-2 * x - 2 * y >= 7, -9 * y >= 7, -6 * y >= 5] + cnf = CNF().from_prop(And(*cons)) + enc = EncodedCNF() + enc.from_cnf(cnf) + lra, _ = LRASolver.from_encoded_cnf(enc) + lra.assert_lit(1) + lra.assert_lit(2) + lra.assert_lit(3) + is_sat, assignment = lra.check() + assert is_sat is True + + +def test_random_problems(): + z3 = import_module("z3") + if z3 is None: + skip("z3 is not installed") + + special_cases = []; x1, x2, x3 = symbols("x1 x2 x3") + special_cases.append([x1 - 3 * x2 <= -5, 6 * x1 + 4 * x2 <= 0, -7 * x1 + 3 * x2 <= 3]) + special_cases.append([-3 * x1 >= 3, Eq(4 * x1, -1)]) + special_cases.append([-4 * x1 < 4, 6 * x1 <= -6]) + special_cases.append([-3 * x2 >= 7, 6 * x1 <= -5, -3 * x2 <= -4]) + special_cases.append([x + y >= 2, x + y <= 1]) + special_cases.append([x >= 0, x + y <= 2, x + 2 * y - z >= 6]) # from paper example + special_cases.append([-2 * x1 - 2 * x2 >= 7, -9 * x1 >= 7, -6 * x1 >= 5]) + special_cases.append([2 * x1 > -3, -9 * x1 < -6, 9 * x1 <= 6]) + special_cases.append([-2*x1 < -4, 9*x1 > -9]) + special_cases.append([-6*x1 >= -1, -8*x1 + x2 >= 5, -8*x1 + 7*x2 < 4, x1 > 7]) + special_cases.append([Eq(x1, 2), Eq(5*x1, -2), Eq(-7*x2, -6), Eq(9*x1 + 10*x2, 9)]) + special_cases.append([Eq(3*x1, 6), Eq(x1 - 8*x2, -9), Eq(-7*x1 + 5*x2, 3), Eq(3*x2, 7)]) + special_cases.append([-4*x1 < 4, 6*x1 <= -6]) + special_cases.append([-3*x1 + 8*x2 >= -8, -10*x2 > 9, 8*x1 - 4*x2 < 8, 10*x1 - 9*x2 >= -9]) + special_cases.append([x1 + 5*x2 >= -6, 9*x1 - 3*x2 >= -9, 6*x1 + 6*x2 < -10, -3*x1 + 3*x2 < -7]) + special_cases.append([-9*x1 < 7, -5*x1 - 7*x2 < -1, 3*x1 + 7*x2 > 1, -6*x1 - 6*x2 > 9]) + special_cases.append([9*x1 - 6*x2 >= -7, 9*x1 + 4*x2 < -8, -7*x2 <= 1, 10*x2 <= -7]) + + feasible_count = 0 + for i in range(50): + if i % 8 == 0: + constraints = make_random_problem(num_variables=1, num_constraints=2, rational=False) + elif i % 8 == 1: + constraints = make_random_problem(num_variables=2, num_constraints=4, rational=False, disable_equality=True, + disable_nonstrict=True) + elif i % 8 == 2: + constraints = make_random_problem(num_variables=2, num_constraints=4, rational=False, disable_strict=True) + elif i % 8 == 3: + constraints = make_random_problem(num_variables=3, num_constraints=12, rational=False) + else: + constraints = make_random_problem(num_variables=3, num_constraints=6, rational=False) + + if i < len(special_cases): + constraints = special_cases[i] + + if False in constraints or True in constraints: + continue + + phi = And(*constraints) + if phi == False: + continue + cnf = CNF.from_prop(phi); enc = EncodedCNF() + enc.from_cnf(cnf) + assert all(0 not in clause for clause in enc.data) + + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + s_subs = lra.s_subs + + lra.run_checks = True + s_subs_rev = {value: key for key, value in s_subs.items()} + lits = {lit for clause in enc.data for lit in clause} + + bounds = [(lra.enc_to_boundary[l], l) for l in lits if l in lra.enc_to_boundary] + bounds = sorted(bounds, key=lambda x: (str(x[0].var), x[0].bound, str(x[0].upper))) # to remove nondeterminism + + for b, l in bounds: + if lra.result and lra.result[0] == False: + break + lra.assert_lit(l) + + feasible = lra.check() + + if feasible[0] == True: + feasible_count += 1 + assert check_if_satisfiable_with_z3(constraints) is True + cons_funcs = [cons.func for cons in constraints] + assignment = feasible[1] + assignment = {key.var : value for key, value in assignment.items()} + if not (StrictLessThan in cons_funcs or StrictGreaterThan in cons_funcs): + assignment = {key: value[0] for key, value in assignment.items()} + for cons in constraints: + assert cons.subs(assignment) == True + + else: + rat_assignment = find_rational_assignment(constraints, assignment) + assert rat_assignment is not None + else: + assert check_if_satisfiable_with_z3(constraints) is False + + conflict = feasible[1] + assert len(conflict) >= 2 + conflict = {lra.enc_to_boundary[-l].get_inequality() for l in conflict} + conflict = {clause.subs(s_subs_rev) for clause in conflict} + assert check_if_satisfiable_with_z3(conflict) is False + + # check that conflict clause is probably minimal + for subset in itertools.combinations(conflict, len(conflict)-1): + assert check_if_satisfiable_with_z3(subset) is True + + +@XFAIL +def test_pos_neg_zero(): + bf = Q.positive(x) & Q.negative(x) & Q.zero(y) + enc = boolean_formula_to_encoded_cnf(bf) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + for lit in enc.encoding.values(): + if lra.assert_lit(lit) is not None: + break + assert len(lra.enc_to_boundary) == 3 + assert lra.check()[0] == False + + bf = Q.positive(x) & Q.lt(x, -1) + enc = boolean_formula_to_encoded_cnf(bf) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + for lit in enc.encoding.values(): + if lra.assert_lit(lit) is not None: + break + assert len(lra.enc_to_boundary) == 2 + assert lra.check()[0] == False + + bf = Q.positive(x) & Q.zero(x) + enc = boolean_formula_to_encoded_cnf(bf) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + for lit in enc.encoding.values(): + if lra.assert_lit(lit) is not None: + break + assert len(lra.enc_to_boundary) == 2 + assert lra.check()[0] == False + + bf = Q.positive(x) & Q.zero(y) + enc = boolean_formula_to_encoded_cnf(bf) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + for lit in enc.encoding.values(): + if lra.assert_lit(lit) is not None: + break + assert len(lra.enc_to_boundary) == 2 + assert lra.check()[0] == True + + +@XFAIL +def test_pos_neg_infinite(): + bf = Q.positive_infinite(x) & Q.lt(x, 10000000) & Q.positive_infinite(y) + enc = boolean_formula_to_encoded_cnf(bf) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + for lit in enc.encoding.values(): + if lra.assert_lit(lit) is not None: + break + assert len(lra.enc_to_boundary) == 3 + assert lra.check()[0] == False + + bf = Q.positive_infinite(x) & Q.gt(x, 10000000) & Q.positive_infinite(y) + enc = boolean_formula_to_encoded_cnf(bf) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + for lit in enc.encoding.values(): + if lra.assert_lit(lit) is not None: + break + assert len(lra.enc_to_boundary) == 3 + assert lra.check()[0] == True + + bf = Q.positive_infinite(x) & Q.negative_infinite(x) + enc = boolean_formula_to_encoded_cnf(bf) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + for lit in enc.encoding.values(): + if lra.assert_lit(lit) is not None: + break + assert len(lra.enc_to_boundary) == 2 + assert lra.check()[0] == False + + +def test_binrel_evaluation(): + bf = Q.gt(3, 2) + enc = boolean_formula_to_encoded_cnf(bf) + lra, conflicts = LRASolver.from_encoded_cnf(enc, testing_mode=True) + assert len(lra.enc_to_boundary) == 0 + assert conflicts == [[1]] + + bf = Q.lt(3, 2) + enc = boolean_formula_to_encoded_cnf(bf) + lra, conflicts = LRASolver.from_encoded_cnf(enc, testing_mode=True) + assert len(lra.enc_to_boundary) == 0 + assert conflicts == [[-1]] + + +def test_negation(): + assert HANDLE_NEGATION is True + bf = Q.gt(x, 1) & ~Q.gt(x, 0) + enc = boolean_formula_to_encoded_cnf(bf) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + for clause in enc.data: + for lit in clause: + lra.assert_lit(lit) + assert len(lra.enc_to_boundary) == 2 + assert lra.check()[0] == False + assert sorted(lra.check()[1]) in [[-1, 2], [-2, 1]] + + bf = ~Q.gt(x, 1) & ~Q.lt(x, 0) + enc = boolean_formula_to_encoded_cnf(bf) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + for clause in enc.data: + for lit in clause: + lra.assert_lit(lit) + assert len(lra.enc_to_boundary) == 2 + assert lra.check()[0] == True + + bf = ~Q.gt(x, 0) & ~Q.lt(x, 1) + enc = boolean_formula_to_encoded_cnf(bf) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + for clause in enc.data: + for lit in clause: + lra.assert_lit(lit) + assert len(lra.enc_to_boundary) == 2 + assert lra.check()[0] == False + + bf = ~Q.gt(x, 0) & ~Q.le(x, 0) + enc = boolean_formula_to_encoded_cnf(bf) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + for clause in enc.data: + for lit in clause: + lra.assert_lit(lit) + assert len(lra.enc_to_boundary) == 2 + assert lra.check()[0] == False + + bf = ~Q.le(x+y, 2) & ~Q.ge(x-y, 2) & ~Q.ge(y, 0) + enc = boolean_formula_to_encoded_cnf(bf) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + for clause in enc.data: + for lit in clause: + lra.assert_lit(lit) + assert len(lra.enc_to_boundary) == 3 + assert lra.check()[0] == False + assert len(lra.check()[1]) == 3 + assert all(i > 0 for i in lra.check()[1]) + + +def test_unhandled_input(): + nan = S.NaN + bf = Q.gt(3, nan) & Q.gt(x, nan) + enc = boolean_formula_to_encoded_cnf(bf) + raises(ValueError, lambda: LRASolver.from_encoded_cnf(enc, testing_mode=True)) + + bf = Q.gt(3, I) & Q.gt(x, I) + enc = boolean_formula_to_encoded_cnf(bf) + raises(UnhandledInput, lambda: LRASolver.from_encoded_cnf(enc, testing_mode=True)) + + bf = Q.gt(3, float("inf")) & Q.gt(x, float("inf")) + enc = boolean_formula_to_encoded_cnf(bf) + raises(UnhandledInput, lambda: LRASolver.from_encoded_cnf(enc, testing_mode=True)) + + bf = Q.gt(3, oo) & Q.gt(x, oo) + enc = boolean_formula_to_encoded_cnf(bf) + raises(UnhandledInput, lambda: LRASolver.from_encoded_cnf(enc, testing_mode=True)) + + # test non-linearity + bf = Q.gt(x**2 + x, 2) + enc = boolean_formula_to_encoded_cnf(bf) + raises(UnhandledInput, lambda: LRASolver.from_encoded_cnf(enc, testing_mode=True)) + + bf = Q.gt(cos(x) + x, 2) + enc = boolean_formula_to_encoded_cnf(bf) + raises(UnhandledInput, lambda: LRASolver.from_encoded_cnf(enc, testing_mode=True)) + +@XFAIL +def test_infinite_strict_inequalities(): + # Extensive testing of the interaction between strict inequalities + # and constraints containing infinity is needed because + # the paper's rule for strict inequalities don't work when + # infinite numbers are allowed. Using the paper's rules you + # can end up with situations where oo + delta > oo is considered + # True when oo + delta should be equal to oo. + # See https://math.stackexchange.com/questions/4757069/can-this-method-of-converting-strict-inequalities-to-equisatisfiable-nonstrict-i + bf = (-x - y >= -float("inf")) & (x > 0) & (y >= float("inf")) + enc = boolean_formula_to_encoded_cnf(bf) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + for lit in sorted(enc.encoding.values()): + if lra.assert_lit(lit) is not None: + break + assert len(lra.enc_to_boundary) == 3 + assert lra.check()[0] == True + + +def test_pivot(): + for _ in range(10): + m = randMatrix(5) + rref = m.rref() + for _ in range(5): + i, j = randint(0, 4), randint(0, 4) + if m[i, j] != 0: + assert LRASolver._pivot(m, i, j).rref() == rref + + +def test_reset_bounds(): + bf = Q.ge(x, 1) & Q.lt(x, 1) + enc = boolean_formula_to_encoded_cnf(bf) + lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True) + for clause in enc.data: + for lit in clause: + lra.assert_lit(lit) + assert len(lra.enc_to_boundary) == 2 + assert lra.check()[0] == False + + lra.reset_bounds() + assert lra.check()[0] == True + for var in lra.all_var: + assert var.upper == LRARational(float("inf"), 0) + assert var.upper_from_eq == False + assert var.upper_from_neg == False + assert var.lower == LRARational(-float("inf"), 0) + assert var.lower_from_eq == False + assert var.lower_from_neg == False + assert var.assign == LRARational(0, 0) + assert var.var is not None + assert var.col_idx is not None + + +def test_empty_cnf(): + cnf = CNF() + enc = EncodedCNF() + enc.from_cnf(cnf) + lra, conflict = LRASolver.from_encoded_cnf(enc) + assert len(conflict) == 0 + assert lra.check() == (True, {}) diff --git a/venv/lib/python3.11/site-packages/sympy/logic/utilities/__init__.py b/venv/lib/python3.11/site-packages/sympy/logic/utilities/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3526c3e53d624bc70afe2df05f123c835781364c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/utilities/__init__.py @@ -0,0 +1,3 @@ +from .dimacs import load_file + +__all__ = ['load_file'] diff --git a/venv/lib/python3.11/site-packages/sympy/logic/utilities/dimacs.py b/venv/lib/python3.11/site-packages/sympy/logic/utilities/dimacs.py new file mode 100644 index 0000000000000000000000000000000000000000..51302d8052c8ed8443239c1e21a2f063cf34e4ab --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/logic/utilities/dimacs.py @@ -0,0 +1,69 @@ +"""For reading in DIMACS file format + +www.cs.ubc.ca/~hoos/SATLIB/Benchmarks/SAT/satformat.ps + +""" + +from sympy.core import Symbol +from sympy.logic.boolalg import And, Or +import re +from pathlib import Path + + +def load(s): + """Loads a boolean expression from a string. + + Examples + ======== + + >>> from sympy.logic.utilities.dimacs import load + >>> load('1') + cnf_1 + >>> load('1 2') + cnf_1 | cnf_2 + >>> load('1 \\n 2') + cnf_1 & cnf_2 + >>> load('1 2 \\n 3') + cnf_3 & (cnf_1 | cnf_2) + """ + clauses = [] + + lines = s.split('\n') + + pComment = re.compile(r'c.*') + pStats = re.compile(r'p\s*cnf\s*(\d*)\s*(\d*)') + + while len(lines) > 0: + line = lines.pop(0) + + # Only deal with lines that aren't comments + if not pComment.match(line): + m = pStats.match(line) + + if not m: + nums = line.rstrip('\n').split(' ') + list = [] + for lit in nums: + if lit != '': + if int(lit) == 0: + continue + num = abs(int(lit)) + sign = True + if int(lit) < 0: + sign = False + + if sign: + list.append(Symbol("cnf_%s" % num)) + else: + list.append(~Symbol("cnf_%s" % num)) + + if len(list) > 0: + clauses.append(Or(*list)) + + return And(*clauses) + + +def load_file(location): + """Loads a boolean expression from a file.""" + s = Path(location).read_text() + return load(s) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/__init__.py b/venv/lib/python3.11/site-packages/sympy/matrices/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..37b558f3f03f149dae6af20254e9b88192f7f1ed --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/__init__.py @@ -0,0 +1,72 @@ +"""A module that handles matrices. + +Includes functions for fast creating matrices like zero, one/eye, random +matrix, etc. +""" +from .exceptions import ShapeError, NonSquareMatrixError +from .kind import MatrixKind +from .dense import ( + GramSchmidt, casoratian, diag, eye, hessian, jordan_cell, + list2numpy, matrix2numpy, matrix_multiply_elementwise, ones, + randMatrix, rot_axis1, rot_axis2, rot_axis3, rot_ccw_axis1, + rot_ccw_axis2, rot_ccw_axis3, rot_givens, + symarray, wronskian, zeros) +from .dense import MutableDenseMatrix +from .matrixbase import DeferredVector, MatrixBase + +MutableMatrix = MutableDenseMatrix +Matrix = MutableMatrix + +from .sparse import MutableSparseMatrix +from .sparsetools import banded +from .immutable import ImmutableDenseMatrix, ImmutableSparseMatrix + +ImmutableMatrix = ImmutableDenseMatrix +SparseMatrix = MutableSparseMatrix + +from .expressions import ( + MatrixSlice, BlockDiagMatrix, BlockMatrix, FunctionMatrix, Identity, + Inverse, MatAdd, MatMul, MatPow, MatrixExpr, MatrixSymbol, Trace, + Transpose, ZeroMatrix, OneMatrix, blockcut, block_collapse, matrix_symbols, Adjoint, + hadamard_product, HadamardProduct, HadamardPower, Determinant, det, + diagonalize_vector, DiagMatrix, DiagonalMatrix, DiagonalOf, trace, + DotProduct, kronecker_product, KroneckerProduct, + PermutationMatrix, MatrixPermute, MatrixSet, Permanent, per) + +from .utilities import dotprodsimp + +__all__ = [ + 'ShapeError', 'NonSquareMatrixError', 'MatrixKind', + + 'GramSchmidt', 'casoratian', 'diag', 'eye', 'hessian', 'jordan_cell', + 'list2numpy', 'matrix2numpy', 'matrix_multiply_elementwise', 'ones', + 'randMatrix', 'rot_axis1', 'rot_axis2', 'rot_axis3', 'symarray', + 'wronskian', 'zeros', 'rot_ccw_axis1', 'rot_ccw_axis2', 'rot_ccw_axis3', + 'rot_givens', + + 'MutableDenseMatrix', + + 'DeferredVector', 'MatrixBase', + + 'Matrix', 'MutableMatrix', + + 'MutableSparseMatrix', + + 'banded', + + 'ImmutableDenseMatrix', 'ImmutableSparseMatrix', + + 'ImmutableMatrix', 'SparseMatrix', + + 'MatrixSlice', 'BlockDiagMatrix', 'BlockMatrix', 'FunctionMatrix', + 'Identity', 'Inverse', 'MatAdd', 'MatMul', 'MatPow', 'MatrixExpr', + 'MatrixSymbol', 'Trace', 'Transpose', 'ZeroMatrix', 'OneMatrix', + 'blockcut', 'block_collapse', 'matrix_symbols', 'Adjoint', + 'hadamard_product', 'HadamardProduct', 'HadamardPower', 'Determinant', + 'det', 'diagonalize_vector', 'DiagMatrix', 'DiagonalMatrix', + 'DiagonalOf', 'trace', 'DotProduct', 'kronecker_product', + 'KroneckerProduct', 'PermutationMatrix', 'MatrixPermute', 'MatrixSet', + 'Permanent', 'per', + + 'dotprodsimp', +] diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/benchmarks/__init__.py b/venv/lib/python3.11/site-packages/sympy/matrices/benchmarks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/benchmarks/bench_matrix.py b/venv/lib/python3.11/site-packages/sympy/matrices/benchmarks/bench_matrix.py new file mode 100644 index 0000000000000000000000000000000000000000..4fb845600533c4c6fef196fe5a45b98890f4ad78 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/benchmarks/bench_matrix.py @@ -0,0 +1,21 @@ +from sympy.core.numbers import Integer +from sympy.matrices.dense import (eye, zeros) + +i3 = Integer(3) +M = eye(100) + + +def timeit_Matrix__getitem_ii(): + M[3, 3] + + +def timeit_Matrix__getitem_II(): + M[i3, i3] + + +def timeit_Matrix__getslice(): + M[:, :] + + +def timeit_Matrix_zeronm(): + zeros(100, 100) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/common.py b/venv/lib/python3.11/site-packages/sympy/matrices/common.py new file mode 100644 index 0000000000000000000000000000000000000000..bcb54726fe1a0c36658d8bf63b974db5a3ce8bad --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/common.py @@ -0,0 +1,3258 @@ +""" +A module containing deprecated matrix mixin classes. + +The classes in this module are deprecated and will be removed in a future +release. They are kept here for backwards compatibility in case downstream +code was subclassing them. + +Importing anything else from this module is deprecated so anything here +should either not be used or should be imported from somewhere else. +""" +from __future__ import annotations +from collections import defaultdict +from collections.abc import Iterable +from inspect import isfunction +from functools import reduce + +from sympy.assumptions.refine import refine +from sympy.core import SympifyError, Add +from sympy.core.basic import Atom +from sympy.core.decorators import call_highest_priority +from sympy.core.logic import fuzzy_and, FuzzyBool +from sympy.core.numbers import Integer +from sympy.core.mod import Mod +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.core.sympify import sympify +from sympy.functions.elementary.complexes import Abs, re, im +from sympy.utilities.exceptions import sympy_deprecation_warning +from .utilities import _dotprodsimp, _simplify +from sympy.polys.polytools import Poly +from sympy.utilities.iterables import flatten, is_sequence +from sympy.utilities.misc import as_int, filldedent +from sympy.tensor.array import NDimArray + +from .utilities import _get_intermediate_simp_bool + + +# These exception types were previously defined in this module but were moved +# to exceptions.py. We reimport them here for backwards compatibility in case +# downstream code was importing them from here. +from .exceptions import ( # noqa: F401 + MatrixError, ShapeError, NonSquareMatrixError, NonInvertibleMatrixError, + NonPositiveDefiniteMatrixError +) + + +_DEPRECATED_MIXINS = ( + 'MatrixShaping', + 'MatrixSpecial', + 'MatrixProperties', + 'MatrixOperations', + 'MatrixArithmetic', + 'MatrixCommon', + 'MatrixDeterminant', + 'MatrixReductions', + 'MatrixSubspaces', + 'MatrixEigen', + 'MatrixCalculus', + 'MatrixDeprecated', +) + + +class _MatrixDeprecatedMeta(type): + + # + # Override the default __instancecheck__ implementation to ensure that + # e.g. isinstance(M, MatrixCommon) still works when M is one of the + # matrix classes. Matrix no longer inherits from MatrixCommon so + # isinstance(M, MatrixCommon) would now return False by default. + # + # There were lots of places in the codebase where this was being done + # so it seems likely that downstream code may be doing it too. All use + # of these mixins is deprecated though so we give a deprecation warning + # unconditionally if they are being used with isinstance. + # + # Any code seeing this deprecation warning should be changed to use + # isinstance(M, MatrixBase) instead which also works in previous versions + # of SymPy. + # + + def __instancecheck__(cls, instance): + + sympy_deprecation_warning( + f""" + Checking whether an object is an instance of {cls.__name__} is + deprecated. + + Use `isinstance(obj, Matrix)` instead of `isinstance(obj, {cls.__name__})`. + """, + deprecated_since_version="1.13", + active_deprecations_target="deprecated-matrix-mixins", + stacklevel=3, + ) + + from sympy.matrices.matrixbase import MatrixBase + from sympy.matrices.matrices import ( + MatrixDeterminant, + MatrixReductions, + MatrixSubspaces, + MatrixEigen, + MatrixCalculus, + MatrixDeprecated + ) + + all_mixins = ( + MatrixRequired, + MatrixShaping, + MatrixSpecial, + MatrixProperties, + MatrixOperations, + MatrixArithmetic, + MatrixCommon, + MatrixDeterminant, + MatrixReductions, + MatrixSubspaces, + MatrixEigen, + MatrixCalculus, + MatrixDeprecated + ) + + if cls in all_mixins and isinstance(instance, MatrixBase): + return True + else: + return super().__instancecheck__(instance) + + +class MatrixRequired(metaclass=_MatrixDeprecatedMeta): + """Deprecated mixin class for making matrix classes.""" + + rows: int + cols: int + _simplify = None + + def __init_subclass__(cls, **kwargs): + + # Warn if any downstream code is subclassing this class or any of the + # deprecated mixin classes that are all ultimately subclasses of this + # class. + # + # We don't want to warn about the deprecated mixins themselves being + # created, but only about them being used as mixins by downstream code. + # Otherwise just importing this module would trigger a warning. + # Ultimately the whole module should be deprecated and removed but for + # SymPy 1.13 it is premature to do that given that this module was the + # main way to import matrix exception types in all previous versions. + + if cls.__name__ not in _DEPRECATED_MIXINS: + sympy_deprecation_warning( + f""" + Inheriting from the Matrix mixin classes is deprecated. + + The class {cls.__name__} is subclassing a deprecated mixin. + """, + deprecated_since_version="1.13", + active_deprecations_target="deprecated-matrix-mixins", + stacklevel=3, + ) + + super().__init_subclass__(**kwargs) + + @classmethod + def _new(cls, *args, **kwargs): + """`_new` must, at minimum, be callable as + `_new(rows, cols, mat) where mat is a flat list of the + elements of the matrix.""" + raise NotImplementedError("Subclasses must implement this.") + + def __eq__(self, other): + raise NotImplementedError("Subclasses must implement this.") + + def __getitem__(self, key): + """Implementations of __getitem__ should accept ints, in which + case the matrix is indexed as a flat list, tuples (i,j) in which + case the (i,j) entry is returned, slices, or mixed tuples (a,b) + where a and b are any combination of slices and integers.""" + raise NotImplementedError("Subclasses must implement this.") + + def __len__(self): + """The total number of entries in the matrix.""" + raise NotImplementedError("Subclasses must implement this.") + + @property + def shape(self): + raise NotImplementedError("Subclasses must implement this.") + + +class MatrixShaping(MatrixRequired): + """Provides basic matrix shaping and extracting of submatrices""" + + def _eval_col_del(self, col): + def entry(i, j): + return self[i, j] if j < col else self[i, j + 1] + return self._new(self.rows, self.cols - 1, entry) + + def _eval_col_insert(self, pos, other): + + def entry(i, j): + if j < pos: + return self[i, j] + elif pos <= j < pos + other.cols: + return other[i, j - pos] + return self[i, j - other.cols] + + return self._new(self.rows, self.cols + other.cols, entry) + + def _eval_col_join(self, other): + rows = self.rows + + def entry(i, j): + if i < rows: + return self[i, j] + return other[i - rows, j] + + return classof(self, other)._new(self.rows + other.rows, self.cols, + entry) + + def _eval_extract(self, rowsList, colsList): + mat = list(self) + cols = self.cols + indices = (i * cols + j for i in rowsList for j in colsList) + return self._new(len(rowsList), len(colsList), + [mat[i] for i in indices]) + + def _eval_get_diag_blocks(self): + sub_blocks = [] + + def recurse_sub_blocks(M): + for i in range(1, M.shape[0] + 1): + if i == 1: + to_the_right = M[0, i:] + to_the_bottom = M[i:, 0] + else: + to_the_right = M[:i, i:] + to_the_bottom = M[i:, :i] + if any(to_the_right) or any(to_the_bottom): + continue + sub_blocks.append(M[:i, :i]) + if M.shape != M[:i, :i].shape: + recurse_sub_blocks(M[i:, i:]) + return + + recurse_sub_blocks(self) + return sub_blocks + + def _eval_row_del(self, row): + def entry(i, j): + return self[i, j] if i < row else self[i + 1, j] + return self._new(self.rows - 1, self.cols, entry) + + def _eval_row_insert(self, pos, other): + entries = list(self) + insert_pos = pos * self.cols + entries[insert_pos:insert_pos] = list(other) + return self._new(self.rows + other.rows, self.cols, entries) + + def _eval_row_join(self, other): + cols = self.cols + + def entry(i, j): + if j < cols: + return self[i, j] + return other[i, j - cols] + + return classof(self, other)._new(self.rows, self.cols + other.cols, + entry) + + def _eval_tolist(self): + return [list(self[i,:]) for i in range(self.rows)] + + def _eval_todok(self): + dok = {} + rows, cols = self.shape + for i in range(rows): + for j in range(cols): + val = self[i, j] + if val != self.zero: + dok[i, j] = val + return dok + + def _eval_vec(self): + rows = self.rows + + def entry(n, _): + # we want to read off the columns first + j = n // rows + i = n - j * rows + return self[i, j] + + return self._new(len(self), 1, entry) + + def _eval_vech(self, diagonal): + c = self.cols + v = [] + if diagonal: + for j in range(c): + for i in range(j, c): + v.append(self[i, j]) + else: + for j in range(c): + for i in range(j + 1, c): + v.append(self[i, j]) + return self._new(len(v), 1, v) + + def col_del(self, col): + """Delete the specified column.""" + if col < 0: + col += self.cols + if not 0 <= col < self.cols: + raise IndexError("Column {} is out of range.".format(col)) + return self._eval_col_del(col) + + def col_insert(self, pos, other): + """Insert one or more columns at the given column position. + + Examples + ======== + + >>> from sympy import zeros, ones + >>> M = zeros(3) + >>> V = ones(3, 1) + >>> M.col_insert(1, V) + Matrix([ + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0]]) + + See Also + ======== + + col + row_insert + """ + # Allows you to build a matrix even if it is null matrix + if not self: + return type(self)(other) + + pos = as_int(pos) + + if pos < 0: + pos = self.cols + pos + if pos < 0: + pos = 0 + elif pos > self.cols: + pos = self.cols + + if self.rows != other.rows: + raise ShapeError( + "The matrices have incompatible number of rows ({} and {})" + .format(self.rows, other.rows)) + + return self._eval_col_insert(pos, other) + + def col_join(self, other): + """Concatenates two matrices along self's last and other's first row. + + Examples + ======== + + >>> from sympy import zeros, ones + >>> M = zeros(3) + >>> V = ones(1, 3) + >>> M.col_join(V) + Matrix([ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [1, 1, 1]]) + + See Also + ======== + + col + row_join + """ + # A null matrix can always be stacked (see #10770) + if self.rows == 0 and self.cols != other.cols: + return self._new(0, other.cols, []).col_join(other) + + if self.cols != other.cols: + raise ShapeError( + "The matrices have incompatible number of columns ({} and {})" + .format(self.cols, other.cols)) + return self._eval_col_join(other) + + def col(self, j): + """Elementary column selector. + + Examples + ======== + + >>> from sympy import eye + >>> eye(2).col(0) + Matrix([ + [1], + [0]]) + + See Also + ======== + + row + col_del + col_join + col_insert + """ + return self[:, j] + + def extract(self, rowsList, colsList): + r"""Return a submatrix by specifying a list of rows and columns. + Negative indices can be given. All indices must be in the range + $-n \le i < n$ where $n$ is the number of rows or columns. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix(4, 3, range(12)) + >>> m + Matrix([ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [9, 10, 11]]) + >>> m.extract([0, 1, 3], [0, 1]) + Matrix([ + [0, 1], + [3, 4], + [9, 10]]) + + Rows or columns can be repeated: + + >>> m.extract([0, 0, 1], [-1]) + Matrix([ + [2], + [2], + [5]]) + + Every other row can be taken by using range to provide the indices: + + >>> m.extract(range(0, m.rows, 2), [-1]) + Matrix([ + [2], + [8]]) + + RowsList or colsList can also be a list of booleans, in which case + the rows or columns corresponding to the True values will be selected: + + >>> m.extract([0, 1, 2, 3], [True, False, True]) + Matrix([ + [0, 2], + [3, 5], + [6, 8], + [9, 11]]) + """ + + if not is_sequence(rowsList) or not is_sequence(colsList): + raise TypeError("rowsList and colsList must be iterable") + # ensure rowsList and colsList are lists of integers + if rowsList and all(isinstance(i, bool) for i in rowsList): + rowsList = [index for index, item in enumerate(rowsList) if item] + if colsList and all(isinstance(i, bool) for i in colsList): + colsList = [index for index, item in enumerate(colsList) if item] + + # ensure everything is in range + rowsList = [a2idx(k, self.rows) for k in rowsList] + colsList = [a2idx(k, self.cols) for k in colsList] + + return self._eval_extract(rowsList, colsList) + + def get_diag_blocks(self): + """Obtains the square sub-matrices on the main diagonal of a square matrix. + + Useful for inverting symbolic matrices or solving systems of + linear equations which may be decoupled by having a block diagonal + structure. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import x, y, z + >>> A = Matrix([[1, 3, 0, 0], [y, z*z, 0, 0], [0, 0, x, 0], [0, 0, 0, 0]]) + >>> a1, a2, a3 = A.get_diag_blocks() + >>> a1 + Matrix([ + [1, 3], + [y, z**2]]) + >>> a2 + Matrix([[x]]) + >>> a3 + Matrix([[0]]) + + """ + return self._eval_get_diag_blocks() + + @classmethod + def hstack(cls, *args): + """Return a matrix formed by joining args horizontally (i.e. + by repeated application of row_join). + + Examples + ======== + + >>> from sympy import Matrix, eye + >>> Matrix.hstack(eye(2), 2*eye(2)) + Matrix([ + [1, 0, 2, 0], + [0, 1, 0, 2]]) + """ + if len(args) == 0: + return cls._new() + + kls = type(args[0]) + return reduce(kls.row_join, args) + + def reshape(self, rows, cols): + """Reshape the matrix. Total number of elements must remain the same. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix(2, 3, lambda i, j: 1) + >>> m + Matrix([ + [1, 1, 1], + [1, 1, 1]]) + >>> m.reshape(1, 6) + Matrix([[1, 1, 1, 1, 1, 1]]) + >>> m.reshape(3, 2) + Matrix([ + [1, 1], + [1, 1], + [1, 1]]) + + """ + if self.rows * self.cols != rows * cols: + raise ValueError("Invalid reshape parameters %d %d" % (rows, cols)) + return self._new(rows, cols, lambda i, j: self[i * cols + j]) + + def row_del(self, row): + """Delete the specified row.""" + if row < 0: + row += self.rows + if not 0 <= row < self.rows: + raise IndexError("Row {} is out of range.".format(row)) + + return self._eval_row_del(row) + + def row_insert(self, pos, other): + """Insert one or more rows at the given row position. + + Examples + ======== + + >>> from sympy import zeros, ones + >>> M = zeros(3) + >>> V = ones(1, 3) + >>> M.row_insert(1, V) + Matrix([ + [0, 0, 0], + [1, 1, 1], + [0, 0, 0], + [0, 0, 0]]) + + See Also + ======== + + row + col_insert + """ + # Allows you to build a matrix even if it is null matrix + if not self: + return self._new(other) + + pos = as_int(pos) + + if pos < 0: + pos = self.rows + pos + if pos < 0: + pos = 0 + elif pos > self.rows: + pos = self.rows + + if self.cols != other.cols: + raise ShapeError( + "The matrices have incompatible number of columns ({} and {})" + .format(self.cols, other.cols)) + + return self._eval_row_insert(pos, other) + + def row_join(self, other): + """Concatenates two matrices along self's last and rhs's first column + + Examples + ======== + + >>> from sympy import zeros, ones + >>> M = zeros(3) + >>> V = ones(3, 1) + >>> M.row_join(V) + Matrix([ + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1]]) + + See Also + ======== + + row + col_join + """ + # A null matrix can always be stacked (see #10770) + if self.cols == 0 and self.rows != other.rows: + return self._new(other.rows, 0, []).row_join(other) + + if self.rows != other.rows: + raise ShapeError( + "The matrices have incompatible number of rows ({} and {})" + .format(self.rows, other.rows)) + return self._eval_row_join(other) + + def diagonal(self, k=0): + """Returns the kth diagonal of self. The main diagonal + corresponds to `k=0`; diagonals above and below correspond to + `k > 0` and `k < 0`, respectively. The values of `self[i, j]` + for which `j - i = k`, are returned in order of increasing + `i + j`, starting with `i + j = |k|`. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix(3, 3, lambda i, j: j - i); m + Matrix([ + [ 0, 1, 2], + [-1, 0, 1], + [-2, -1, 0]]) + >>> _.diagonal() + Matrix([[0, 0, 0]]) + >>> m.diagonal(1) + Matrix([[1, 1]]) + >>> m.diagonal(-2) + Matrix([[-2]]) + + Even though the diagonal is returned as a Matrix, the element + retrieval can be done with a single index: + + >>> Matrix.diag(1, 2, 3).diagonal()[1] # instead of [0, 1] + 2 + + See Also + ======== + + diag + """ + rv = [] + k = as_int(k) + r = 0 if k > 0 else -k + c = 0 if r else k + while True: + if r == self.rows or c == self.cols: + break + rv.append(self[r, c]) + r += 1 + c += 1 + if not rv: + raise ValueError(filldedent(''' + The %s diagonal is out of range [%s, %s]''' % ( + k, 1 - self.rows, self.cols - 1))) + return self._new(1, len(rv), rv) + + def row(self, i): + """Elementary row selector. + + Examples + ======== + + >>> from sympy import eye + >>> eye(2).row(0) + Matrix([[1, 0]]) + + See Also + ======== + + col + row_del + row_join + row_insert + """ + return self[i, :] + + @property + def shape(self): + """The shape (dimensions) of the matrix as the 2-tuple (rows, cols). + + Examples + ======== + + >>> from sympy import zeros + >>> M = zeros(2, 3) + >>> M.shape + (2, 3) + >>> M.rows + 2 + >>> M.cols + 3 + """ + return (self.rows, self.cols) + + def todok(self): + """Return the matrix as dictionary of keys. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix.eye(3) + >>> M.todok() + {(0, 0): 1, (1, 1): 1, (2, 2): 1} + """ + return self._eval_todok() + + def tolist(self): + """Return the Matrix as a nested Python list. + + Examples + ======== + + >>> from sympy import Matrix, ones + >>> m = Matrix(3, 3, range(9)) + >>> m + Matrix([ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8]]) + >>> m.tolist() + [[0, 1, 2], [3, 4, 5], [6, 7, 8]] + >>> ones(3, 0).tolist() + [[], [], []] + + When there are no rows then it will not be possible to tell how + many columns were in the original matrix: + + >>> ones(0, 3).tolist() + [] + + """ + if not self.rows: + return [] + if not self.cols: + return [[] for i in range(self.rows)] + return self._eval_tolist() + + def todod(M): + """Returns matrix as dict of dicts containing non-zero elements of the Matrix + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([[0, 1],[0, 3]]) + >>> A + Matrix([ + [0, 1], + [0, 3]]) + >>> A.todod() + {0: {1: 1}, 1: {1: 3}} + + + """ + rowsdict = {} + Mlol = M.tolist() + for i, Mi in enumerate(Mlol): + row = {j: Mij for j, Mij in enumerate(Mi) if Mij} + if row: + rowsdict[i] = row + return rowsdict + + def vec(self): + """Return the Matrix converted into a one column matrix by stacking columns + + Examples + ======== + + >>> from sympy import Matrix + >>> m=Matrix([[1, 3], [2, 4]]) + >>> m + Matrix([ + [1, 3], + [2, 4]]) + >>> m.vec() + Matrix([ + [1], + [2], + [3], + [4]]) + + See Also + ======== + + vech + """ + return self._eval_vec() + + def vech(self, diagonal=True, check_symmetry=True): + """Reshapes the matrix into a column vector by stacking the + elements in the lower triangle. + + Parameters + ========== + + diagonal : bool, optional + If ``True``, it includes the diagonal elements. + + check_symmetry : bool, optional + If ``True``, it checks whether the matrix is symmetric. + + Examples + ======== + + >>> from sympy import Matrix + >>> m=Matrix([[1, 2], [2, 3]]) + >>> m + Matrix([ + [1, 2], + [2, 3]]) + >>> m.vech() + Matrix([ + [1], + [2], + [3]]) + >>> m.vech(diagonal=False) + Matrix([[2]]) + + Notes + ===== + + This should work for symmetric matrices and ``vech`` can + represent symmetric matrices in vector form with less size than + ``vec``. + + See Also + ======== + + vec + """ + if not self.is_square: + raise NonSquareMatrixError + + if check_symmetry and not self.is_symmetric(): + raise ValueError("The matrix is not symmetric.") + + return self._eval_vech(diagonal) + + @classmethod + def vstack(cls, *args): + """Return a matrix formed by joining args vertically (i.e. + by repeated application of col_join). + + Examples + ======== + + >>> from sympy import Matrix, eye + >>> Matrix.vstack(eye(2), 2*eye(2)) + Matrix([ + [1, 0], + [0, 1], + [2, 0], + [0, 2]]) + """ + if len(args) == 0: + return cls._new() + + kls = type(args[0]) + return reduce(kls.col_join, args) + + +class MatrixSpecial(MatrixRequired): + """Construction of special matrices""" + + @classmethod + def _eval_diag(cls, rows, cols, diag_dict): + """diag_dict is a defaultdict containing + all the entries of the diagonal matrix.""" + def entry(i, j): + return diag_dict[(i, j)] + return cls._new(rows, cols, entry) + + @classmethod + def _eval_eye(cls, rows, cols): + vals = [cls.zero]*(rows*cols) + vals[::cols+1] = [cls.one]*min(rows, cols) + return cls._new(rows, cols, vals, copy=False) + + @classmethod + def _eval_jordan_block(cls, size: int, eigenvalue, band='upper'): + if band == 'lower': + def entry(i, j): + if i == j: + return eigenvalue + elif j + 1 == i: + return cls.one + return cls.zero + else: + def entry(i, j): + if i == j: + return eigenvalue + elif i + 1 == j: + return cls.one + return cls.zero + return cls._new(size, size, entry) + + @classmethod + def _eval_ones(cls, rows, cols): + def entry(i, j): + return cls.one + return cls._new(rows, cols, entry) + + @classmethod + def _eval_zeros(cls, rows, cols): + return cls._new(rows, cols, [cls.zero]*(rows*cols), copy=False) + + @classmethod + def _eval_wilkinson(cls, n): + def entry(i, j): + return cls.one if i + 1 == j else cls.zero + + D = cls._new(2*n + 1, 2*n + 1, entry) + + wminus = cls.diag(list(range(-n, n + 1)), unpack=True) + D + D.T + wplus = abs(cls.diag(list(range(-n, n + 1)), unpack=True)) + D + D.T + + return wminus, wplus + + @classmethod + def diag(kls, *args, strict=False, unpack=True, rows=None, cols=None, **kwargs): + """Returns a matrix with the specified diagonal. + If matrices are passed, a block-diagonal matrix + is created (i.e. the "direct sum" of the matrices). + + kwargs + ====== + + rows : rows of the resulting matrix; computed if + not given. + + cols : columns of the resulting matrix; computed if + not given. + + cls : class for the resulting matrix + + unpack : bool which, when True (default), unpacks a single + sequence rather than interpreting it as a Matrix. + + strict : bool which, when False (default), allows Matrices to + have variable-length rows. + + Examples + ======== + + >>> from sympy import Matrix + >>> Matrix.diag(1, 2, 3) + Matrix([ + [1, 0, 0], + [0, 2, 0], + [0, 0, 3]]) + + The current default is to unpack a single sequence. If this is + not desired, set `unpack=False` and it will be interpreted as + a matrix. + + >>> Matrix.diag([1, 2, 3]) == Matrix.diag(1, 2, 3) + True + + When more than one element is passed, each is interpreted as + something to put on the diagonal. Lists are converted to + matrices. Filling of the diagonal always continues from + the bottom right hand corner of the previous item: this + will create a block-diagonal matrix whether the matrices + are square or not. + + >>> col = [1, 2, 3] + >>> row = [[4, 5]] + >>> Matrix.diag(col, row) + Matrix([ + [1, 0, 0], + [2, 0, 0], + [3, 0, 0], + [0, 4, 5]]) + + When `unpack` is False, elements within a list need not all be + of the same length. Setting `strict` to True would raise a + ValueError for the following: + + >>> Matrix.diag([[1, 2, 3], [4, 5], [6]], unpack=False) + Matrix([ + [1, 2, 3], + [4, 5, 0], + [6, 0, 0]]) + + The type of the returned matrix can be set with the ``cls`` + keyword. + + >>> from sympy import ImmutableMatrix + >>> from sympy.utilities.misc import func_name + >>> func_name(Matrix.diag(1, cls=ImmutableMatrix)) + 'ImmutableDenseMatrix' + + A zero dimension matrix can be used to position the start of + the filling at the start of an arbitrary row or column: + + >>> from sympy import ones + >>> r2 = ones(0, 2) + >>> Matrix.diag(r2, 1, 2) + Matrix([ + [0, 0, 1, 0], + [0, 0, 0, 2]]) + + See Also + ======== + eye + diagonal + .dense.diag + .expressions.blockmatrix.BlockMatrix + .sparsetools.banded + """ + from sympy.matrices.matrixbase import MatrixBase + from sympy.matrices.dense import Matrix + from sympy.matrices import SparseMatrix + klass = kwargs.get('cls', kls) + if unpack and len(args) == 1 and is_sequence(args[0]) and \ + not isinstance(args[0], MatrixBase): + args = args[0] + + # fill a default dict with the diagonal entries + diag_entries = defaultdict(int) + rmax = cmax = 0 # keep track of the biggest index seen + for m in args: + if isinstance(m, list): + if strict: + # if malformed, Matrix will raise an error + _ = Matrix(m) + r, c = _.shape + m = _.tolist() + else: + r, c, smat = SparseMatrix._handle_creation_inputs(m) + for (i, j), _ in smat.items(): + diag_entries[(i + rmax, j + cmax)] = _ + m = [] # to skip process below + elif hasattr(m, 'shape'): # a Matrix + # convert to list of lists + r, c = m.shape + m = m.tolist() + else: # in this case, we're a single value + diag_entries[(rmax, cmax)] = m + rmax += 1 + cmax += 1 + continue + # process list of lists + for i, mi in enumerate(m): + for j, _ in enumerate(mi): + diag_entries[(i + rmax, j + cmax)] = _ + rmax += r + cmax += c + if rows is None: + rows, cols = cols, rows + if rows is None: + rows, cols = rmax, cmax + else: + cols = rows if cols is None else cols + if rows < rmax or cols < cmax: + raise ValueError(filldedent(''' + The constructed matrix is {} x {} but a size of {} x {} + was specified.'''.format(rmax, cmax, rows, cols))) + return klass._eval_diag(rows, cols, diag_entries) + + @classmethod + def eye(kls, rows, cols=None, **kwargs): + """Returns an identity matrix. + + Parameters + ========== + + rows : rows of the matrix + cols : cols of the matrix (if None, cols=rows) + + kwargs + ====== + cls : class of the returned matrix + """ + if cols is None: + cols = rows + if rows < 0 or cols < 0: + raise ValueError("Cannot create a {} x {} matrix. " + "Both dimensions must be positive".format(rows, cols)) + klass = kwargs.get('cls', kls) + rows, cols = as_int(rows), as_int(cols) + + return klass._eval_eye(rows, cols) + + @classmethod + def jordan_block(kls, size=None, eigenvalue=None, *, band='upper', **kwargs): + """Returns a Jordan block + + Parameters + ========== + + size : Integer, optional + Specifies the shape of the Jordan block matrix. + + eigenvalue : Number or Symbol + Specifies the value for the main diagonal of the matrix. + + .. note:: + The keyword ``eigenval`` is also specified as an alias + of this keyword, but it is not recommended to use. + + We may deprecate the alias in later release. + + band : 'upper' or 'lower', optional + Specifies the position of the off-diagonal to put `1` s on. + + cls : Matrix, optional + Specifies the matrix class of the output form. + + If it is not specified, the class type where the method is + being executed on will be returned. + + Returns + ======= + + Matrix + A Jordan block matrix. + + Raises + ====== + + ValueError + If insufficient arguments are given for matrix size + specification, or no eigenvalue is given. + + Examples + ======== + + Creating a default Jordan block: + + >>> from sympy import Matrix + >>> from sympy.abc import x + >>> Matrix.jordan_block(4, x) + Matrix([ + [x, 1, 0, 0], + [0, x, 1, 0], + [0, 0, x, 1], + [0, 0, 0, x]]) + + Creating an alternative Jordan block matrix where `1` is on + lower off-diagonal: + + >>> Matrix.jordan_block(4, x, band='lower') + Matrix([ + [x, 0, 0, 0], + [1, x, 0, 0], + [0, 1, x, 0], + [0, 0, 1, x]]) + + Creating a Jordan block with keyword arguments + + >>> Matrix.jordan_block(size=4, eigenvalue=x) + Matrix([ + [x, 1, 0, 0], + [0, x, 1, 0], + [0, 0, x, 1], + [0, 0, 0, x]]) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Jordan_matrix + """ + klass = kwargs.pop('cls', kls) + + eigenval = kwargs.get('eigenval', None) + if eigenvalue is None and eigenval is None: + raise ValueError("Must supply an eigenvalue") + elif eigenvalue != eigenval and None not in (eigenval, eigenvalue): + raise ValueError( + "Inconsistent values are given: 'eigenval'={}, " + "'eigenvalue'={}".format(eigenval, eigenvalue)) + else: + if eigenval is not None: + eigenvalue = eigenval + + if size is None: + raise ValueError("Must supply a matrix size") + + size = as_int(size) + return klass._eval_jordan_block(size, eigenvalue, band) + + @classmethod + def ones(kls, rows, cols=None, **kwargs): + """Returns a matrix of ones. + + Parameters + ========== + + rows : rows of the matrix + cols : cols of the matrix (if None, cols=rows) + + kwargs + ====== + cls : class of the returned matrix + """ + if cols is None: + cols = rows + klass = kwargs.get('cls', kls) + rows, cols = as_int(rows), as_int(cols) + + return klass._eval_ones(rows, cols) + + @classmethod + def zeros(kls, rows, cols=None, **kwargs): + """Returns a matrix of zeros. + + Parameters + ========== + + rows : rows of the matrix + cols : cols of the matrix (if None, cols=rows) + + kwargs + ====== + cls : class of the returned matrix + """ + if cols is None: + cols = rows + if rows < 0 or cols < 0: + raise ValueError("Cannot create a {} x {} matrix. " + "Both dimensions must be positive".format(rows, cols)) + klass = kwargs.get('cls', kls) + rows, cols = as_int(rows), as_int(cols) + + return klass._eval_zeros(rows, cols) + + @classmethod + def companion(kls, poly): + """Returns a companion matrix of a polynomial. + + Examples + ======== + + >>> from sympy import Matrix, Poly, Symbol, symbols + >>> x = Symbol('x') + >>> c0, c1, c2, c3, c4 = symbols('c0:5') + >>> p = Poly(c0 + c1*x + c2*x**2 + c3*x**3 + c4*x**4 + x**5, x) + >>> Matrix.companion(p) + Matrix([ + [0, 0, 0, 0, -c0], + [1, 0, 0, 0, -c1], + [0, 1, 0, 0, -c2], + [0, 0, 1, 0, -c3], + [0, 0, 0, 1, -c4]]) + """ + poly = kls._sympify(poly) + if not isinstance(poly, Poly): + raise ValueError("{} must be a Poly instance.".format(poly)) + if not poly.is_monic: + raise ValueError("{} must be a monic polynomial.".format(poly)) + if not poly.is_univariate: + raise ValueError( + "{} must be a univariate polynomial.".format(poly)) + + size = poly.degree() + if not size >= 1: + raise ValueError( + "{} must have degree not less than 1.".format(poly)) + + coeffs = poly.all_coeffs() + def entry(i, j): + if j == size - 1: + return -coeffs[-1 - i] + elif i == j + 1: + return kls.one + return kls.zero + return kls._new(size, size, entry) + + + @classmethod + def wilkinson(kls, n, **kwargs): + """Returns two square Wilkinson Matrix of size 2*n + 1 + $W_{2n + 1}^-, W_{2n + 1}^+ =$ Wilkinson(n) + + Examples + ======== + + >>> from sympy import Matrix + >>> wminus, wplus = Matrix.wilkinson(3) + >>> wminus + Matrix([ + [-3, 1, 0, 0, 0, 0, 0], + [ 1, -2, 1, 0, 0, 0, 0], + [ 0, 1, -1, 1, 0, 0, 0], + [ 0, 0, 1, 0, 1, 0, 0], + [ 0, 0, 0, 1, 1, 1, 0], + [ 0, 0, 0, 0, 1, 2, 1], + [ 0, 0, 0, 0, 0, 1, 3]]) + >>> wplus + Matrix([ + [3, 1, 0, 0, 0, 0, 0], + [1, 2, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 0, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 0, 1, 2, 1], + [0, 0, 0, 0, 0, 1, 3]]) + + References + ========== + + .. [1] https://blogs.mathworks.com/cleve/2013/04/15/wilkinsons-matrices-2/ + .. [2] J. H. Wilkinson, The Algebraic Eigenvalue Problem, Claredon Press, Oxford, 1965, 662 pp. + + """ + klass = kwargs.get('cls', kls) + n = as_int(n) + return klass._eval_wilkinson(n) + +class MatrixProperties(MatrixRequired): + """Provides basic properties of a matrix.""" + + def _eval_atoms(self, *types): + result = set() + for i in self: + result.update(i.atoms(*types)) + return result + + def _eval_free_symbols(self): + return set().union(*(i.free_symbols for i in self if i)) + + def _eval_has(self, *patterns): + return any(a.has(*patterns) for a in self) + + def _eval_is_anti_symmetric(self, simpfunc): + if not all(simpfunc(self[i, j] + self[j, i]).is_zero for i in range(self.rows) for j in range(self.cols)): + return False + return True + + def _eval_is_diagonal(self): + for i in range(self.rows): + for j in range(self.cols): + if i != j and self[i, j]: + return False + return True + + # _eval_is_hermitian is called by some general SymPy + # routines and has a different *args signature. Make + # sure the names don't clash by adding `_matrix_` in name. + def _eval_is_matrix_hermitian(self, simpfunc): + mat = self._new(self.rows, self.cols, lambda i, j: simpfunc(self[i, j] - self[j, i].conjugate())) + return mat.is_zero_matrix + + def _eval_is_Identity(self) -> FuzzyBool: + def dirac(i, j): + if i == j: + return 1 + return 0 + + return all(self[i, j] == dirac(i, j) + for i in range(self.rows) + for j in range(self.cols)) + + def _eval_is_lower_hessenberg(self): + return all(self[i, j].is_zero + for i in range(self.rows) + for j in range(i + 2, self.cols)) + + def _eval_is_lower(self): + return all(self[i, j].is_zero + for i in range(self.rows) + for j in range(i + 1, self.cols)) + + def _eval_is_symbolic(self): + return self.has(Symbol) + + def _eval_is_symmetric(self, simpfunc): + mat = self._new(self.rows, self.cols, lambda i, j: simpfunc(self[i, j] - self[j, i])) + return mat.is_zero_matrix + + def _eval_is_zero_matrix(self): + if any(i.is_zero == False for i in self): + return False + if any(i.is_zero is None for i in self): + return None + return True + + def _eval_is_upper_hessenberg(self): + return all(self[i, j].is_zero + for i in range(2, self.rows) + for j in range(min(self.cols, (i - 1)))) + + def _eval_values(self): + return [i for i in self if not i.is_zero] + + def _has_positive_diagonals(self): + diagonal_entries = (self[i, i] for i in range(self.rows)) + return fuzzy_and(x.is_positive for x in diagonal_entries) + + def _has_nonnegative_diagonals(self): + diagonal_entries = (self[i, i] for i in range(self.rows)) + return fuzzy_and(x.is_nonnegative for x in diagonal_entries) + + def atoms(self, *types): + """Returns the atoms that form the current object. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import Matrix + >>> Matrix([[x]]) + Matrix([[x]]) + >>> _.atoms() + {x} + >>> Matrix([[x, y], [y, x]]) + Matrix([ + [x, y], + [y, x]]) + >>> _.atoms() + {x, y} + """ + + types = tuple(t if isinstance(t, type) else type(t) for t in types) + if not types: + types = (Atom,) + return self._eval_atoms(*types) + + @property + def free_symbols(self): + """Returns the free symbols within the matrix. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy import Matrix + >>> Matrix([[x], [1]]).free_symbols + {x} + """ + return self._eval_free_symbols() + + def has(self, *patterns): + """Test whether any subexpression matches any of the patterns. + + Examples + ======== + + >>> from sympy import Matrix, SparseMatrix, Float + >>> from sympy.abc import x, y + >>> A = Matrix(((1, x), (0.2, 3))) + >>> B = SparseMatrix(((1, x), (0.2, 3))) + >>> A.has(x) + True + >>> A.has(y) + False + >>> A.has(Float) + True + >>> B.has(x) + True + >>> B.has(y) + False + >>> B.has(Float) + True + """ + return self._eval_has(*patterns) + + def is_anti_symmetric(self, simplify=True): + """Check if matrix M is an antisymmetric matrix, + that is, M is a square matrix with all M[i, j] == -M[j, i]. + + When ``simplify=True`` (default), the sum M[i, j] + M[j, i] is + simplified before testing to see if it is zero. By default, + the SymPy simplify function is used. To use a custom function + set simplify to a function that accepts a single argument which + returns a simplified expression. To skip simplification, set + simplify to False but note that although this will be faster, + it may induce false negatives. + + Examples + ======== + + >>> from sympy import Matrix, symbols + >>> m = Matrix(2, 2, [0, 1, -1, 0]) + >>> m + Matrix([ + [ 0, 1], + [-1, 0]]) + >>> m.is_anti_symmetric() + True + >>> x, y = symbols('x y') + >>> m = Matrix(2, 3, [0, 0, x, -y, 0, 0]) + >>> m + Matrix([ + [ 0, 0, x], + [-y, 0, 0]]) + >>> m.is_anti_symmetric() + False + + >>> from sympy.abc import x, y + >>> m = Matrix(3, 3, [0, x**2 + 2*x + 1, y, + ... -(x + 1)**2, 0, x*y, + ... -y, -x*y, 0]) + + Simplification of matrix elements is done by default so even + though two elements which should be equal and opposite would not + pass an equality test, the matrix is still reported as + anti-symmetric: + + >>> m[0, 1] == -m[1, 0] + False + >>> m.is_anti_symmetric() + True + + If ``simplify=False`` is used for the case when a Matrix is already + simplified, this will speed things up. Here, we see that without + simplification the matrix does not appear anti-symmetric: + + >>> print(m.is_anti_symmetric(simplify=False)) + None + + But if the matrix were already expanded, then it would appear + anti-symmetric and simplification in the is_anti_symmetric routine + is not needed: + + >>> m = m.expand() + >>> m.is_anti_symmetric(simplify=False) + True + """ + # accept custom simplification + simpfunc = simplify + if not isfunction(simplify): + simpfunc = _simplify if simplify else lambda x: x + + if not self.is_square: + return False + return self._eval_is_anti_symmetric(simpfunc) + + def is_diagonal(self): + """Check if matrix is diagonal, + that is matrix in which the entries outside the main diagonal are all zero. + + Examples + ======== + + >>> from sympy import Matrix, diag + >>> m = Matrix(2, 2, [1, 0, 0, 2]) + >>> m + Matrix([ + [1, 0], + [0, 2]]) + >>> m.is_diagonal() + True + + >>> m = Matrix(2, 2, [1, 1, 0, 2]) + >>> m + Matrix([ + [1, 1], + [0, 2]]) + >>> m.is_diagonal() + False + + >>> m = diag(1, 2, 3) + >>> m + Matrix([ + [1, 0, 0], + [0, 2, 0], + [0, 0, 3]]) + >>> m.is_diagonal() + True + + See Also + ======== + + is_lower + is_upper + sympy.matrices.matrixbase.MatrixCommon.is_diagonalizable + diagonalize + """ + return self._eval_is_diagonal() + + @property + def is_weakly_diagonally_dominant(self): + r"""Tests if the matrix is row weakly diagonally dominant. + + Explanation + =========== + + A $n, n$ matrix $A$ is row weakly diagonally dominant if + + .. math:: + \left|A_{i, i}\right| \ge \sum_{j = 0, j \neq i}^{n-1} + \left|A_{i, j}\right| \quad {\text{for all }} + i \in \{ 0, ..., n-1 \} + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([[3, -2, 1], [1, -3, 2], [-1, 2, 4]]) + >>> A.is_weakly_diagonally_dominant + True + + >>> A = Matrix([[-2, 2, 1], [1, 3, 2], [1, -2, 0]]) + >>> A.is_weakly_diagonally_dominant + False + + >>> A = Matrix([[-4, 2, 1], [1, 6, 2], [1, -2, 5]]) + >>> A.is_weakly_diagonally_dominant + True + + Notes + ===== + + If you want to test whether a matrix is column diagonally + dominant, you can apply the test after transposing the matrix. + """ + if not self.is_square: + return False + + rows, cols = self.shape + + def test_row(i): + summation = self.zero + for j in range(cols): + if i != j: + summation += Abs(self[i, j]) + return (Abs(self[i, i]) - summation).is_nonnegative + + return fuzzy_and(test_row(i) for i in range(rows)) + + @property + def is_strongly_diagonally_dominant(self): + r"""Tests if the matrix is row strongly diagonally dominant. + + Explanation + =========== + + A $n, n$ matrix $A$ is row strongly diagonally dominant if + + .. math:: + \left|A_{i, i}\right| > \sum_{j = 0, j \neq i}^{n-1} + \left|A_{i, j}\right| \quad {\text{for all }} + i \in \{ 0, ..., n-1 \} + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([[3, -2, 1], [1, -3, 2], [-1, 2, 4]]) + >>> A.is_strongly_diagonally_dominant + False + + >>> A = Matrix([[-2, 2, 1], [1, 3, 2], [1, -2, 0]]) + >>> A.is_strongly_diagonally_dominant + False + + >>> A = Matrix([[-4, 2, 1], [1, 6, 2], [1, -2, 5]]) + >>> A.is_strongly_diagonally_dominant + True + + Notes + ===== + + If you want to test whether a matrix is column diagonally + dominant, you can apply the test after transposing the matrix. + """ + if not self.is_square: + return False + + rows, cols = self.shape + + def test_row(i): + summation = self.zero + for j in range(cols): + if i != j: + summation += Abs(self[i, j]) + return (Abs(self[i, i]) - summation).is_positive + + return fuzzy_and(test_row(i) for i in range(rows)) + + @property + def is_hermitian(self): + """Checks if the matrix is Hermitian. + + In a Hermitian matrix element i,j is the complex conjugate of + element j,i. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy import I + >>> from sympy.abc import x + >>> a = Matrix([[1, I], [-I, 1]]) + >>> a + Matrix([ + [ 1, I], + [-I, 1]]) + >>> a.is_hermitian + True + >>> a[0, 0] = 2*I + >>> a.is_hermitian + False + >>> a[0, 0] = x + >>> a.is_hermitian + >>> a[0, 1] = a[1, 0]*I + >>> a.is_hermitian + False + """ + if not self.is_square: + return False + + return self._eval_is_matrix_hermitian(_simplify) + + @property + def is_Identity(self) -> FuzzyBool: + if not self.is_square: + return False + return self._eval_is_Identity() + + @property + def is_lower_hessenberg(self): + r"""Checks if the matrix is in the lower-Hessenberg form. + + The lower hessenberg matrix has zero entries + above the first superdiagonal. + + Examples + ======== + + >>> from sympy import Matrix + >>> a = Matrix([[1, 2, 0, 0], [5, 2, 3, 0], [3, 4, 3, 7], [5, 6, 1, 1]]) + >>> a + Matrix([ + [1, 2, 0, 0], + [5, 2, 3, 0], + [3, 4, 3, 7], + [5, 6, 1, 1]]) + >>> a.is_lower_hessenberg + True + + See Also + ======== + + is_upper_hessenberg + is_lower + """ + return self._eval_is_lower_hessenberg() + + @property + def is_lower(self): + """Check if matrix is a lower triangular matrix. True can be returned + even if the matrix is not square. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix(2, 2, [1, 0, 0, 1]) + >>> m + Matrix([ + [1, 0], + [0, 1]]) + >>> m.is_lower + True + + >>> m = Matrix(4, 3, [0, 0, 0, 2, 0, 0, 1, 4, 0, 6, 6, 5]) + >>> m + Matrix([ + [0, 0, 0], + [2, 0, 0], + [1, 4, 0], + [6, 6, 5]]) + >>> m.is_lower + True + + >>> from sympy.abc import x, y + >>> m = Matrix(2, 2, [x**2 + y, y**2 + x, 0, x + y]) + >>> m + Matrix([ + [x**2 + y, x + y**2], + [ 0, x + y]]) + >>> m.is_lower + False + + See Also + ======== + + is_upper + is_diagonal + is_lower_hessenberg + """ + return self._eval_is_lower() + + @property + def is_square(self): + """Checks if a matrix is square. + + A matrix is square if the number of rows equals the number of columns. + The empty matrix is square by definition, since the number of rows and + the number of columns are both zero. + + Examples + ======== + + >>> from sympy import Matrix + >>> a = Matrix([[1, 2, 3], [4, 5, 6]]) + >>> b = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> c = Matrix([]) + >>> a.is_square + False + >>> b.is_square + True + >>> c.is_square + True + """ + return self.rows == self.cols + + def is_symbolic(self): + """Checks if any elements contain Symbols. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import x, y + >>> M = Matrix([[x, y], [1, 0]]) + >>> M.is_symbolic() + True + + """ + return self._eval_is_symbolic() + + def is_symmetric(self, simplify=True): + """Check if matrix is symmetric matrix, + that is square matrix and is equal to its transpose. + + By default, simplifications occur before testing symmetry. + They can be skipped using 'simplify=False'; while speeding things a bit, + this may however induce false negatives. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix(2, 2, [0, 1, 1, 2]) + >>> m + Matrix([ + [0, 1], + [1, 2]]) + >>> m.is_symmetric() + True + + >>> m = Matrix(2, 2, [0, 1, 2, 0]) + >>> m + Matrix([ + [0, 1], + [2, 0]]) + >>> m.is_symmetric() + False + + >>> m = Matrix(2, 3, [0, 0, 0, 0, 0, 0]) + >>> m + Matrix([ + [0, 0, 0], + [0, 0, 0]]) + >>> m.is_symmetric() + False + + >>> from sympy.abc import x, y + >>> m = Matrix(3, 3, [1, x**2 + 2*x + 1, y, (x + 1)**2, 2, 0, y, 0, 3]) + >>> m + Matrix([ + [ 1, x**2 + 2*x + 1, y], + [(x + 1)**2, 2, 0], + [ y, 0, 3]]) + >>> m.is_symmetric() + True + + If the matrix is already simplified, you may speed-up is_symmetric() + test by using 'simplify=False'. + + >>> bool(m.is_symmetric(simplify=False)) + False + >>> m1 = m.expand() + >>> m1.is_symmetric(simplify=False) + True + """ + simpfunc = simplify + if not isfunction(simplify): + simpfunc = _simplify if simplify else lambda x: x + + if not self.is_square: + return False + + return self._eval_is_symmetric(simpfunc) + + @property + def is_upper_hessenberg(self): + """Checks if the matrix is the upper-Hessenberg form. + + The upper hessenberg matrix has zero entries + below the first subdiagonal. + + Examples + ======== + + >>> from sympy import Matrix + >>> a = Matrix([[1, 4, 2, 3], [3, 4, 1, 7], [0, 2, 3, 4], [0, 0, 1, 3]]) + >>> a + Matrix([ + [1, 4, 2, 3], + [3, 4, 1, 7], + [0, 2, 3, 4], + [0, 0, 1, 3]]) + >>> a.is_upper_hessenberg + True + + See Also + ======== + + is_lower_hessenberg + is_upper + """ + return self._eval_is_upper_hessenberg() + + @property + def is_upper(self): + """Check if matrix is an upper triangular matrix. True can be returned + even if the matrix is not square. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix(2, 2, [1, 0, 0, 1]) + >>> m + Matrix([ + [1, 0], + [0, 1]]) + >>> m.is_upper + True + + >>> m = Matrix(4, 3, [5, 1, 9, 0, 4, 6, 0, 0, 5, 0, 0, 0]) + >>> m + Matrix([ + [5, 1, 9], + [0, 4, 6], + [0, 0, 5], + [0, 0, 0]]) + >>> m.is_upper + True + + >>> m = Matrix(2, 3, [4, 2, 5, 6, 1, 1]) + >>> m + Matrix([ + [4, 2, 5], + [6, 1, 1]]) + >>> m.is_upper + False + + See Also + ======== + + is_lower + is_diagonal + is_upper_hessenberg + """ + return all(self[i, j].is_zero + for i in range(1, self.rows) + for j in range(min(i, self.cols))) + + @property + def is_zero_matrix(self): + """Checks if a matrix is a zero matrix. + + A matrix is zero if every element is zero. A matrix need not be square + to be considered zero. The empty matrix is zero by the principle of + vacuous truth. For a matrix that may or may not be zero (e.g. + contains a symbol), this will be None + + Examples + ======== + + >>> from sympy import Matrix, zeros + >>> from sympy.abc import x + >>> a = Matrix([[0, 0], [0, 0]]) + >>> b = zeros(3, 4) + >>> c = Matrix([[0, 1], [0, 0]]) + >>> d = Matrix([]) + >>> e = Matrix([[x, 0], [0, 0]]) + >>> a.is_zero_matrix + True + >>> b.is_zero_matrix + True + >>> c.is_zero_matrix + False + >>> d.is_zero_matrix + True + >>> e.is_zero_matrix + """ + return self._eval_is_zero_matrix() + + def values(self): + """Return non-zero values of self.""" + return self._eval_values() + + +class MatrixOperations(MatrixRequired): + """Provides basic matrix shape and elementwise + operations. Should not be instantiated directly.""" + + def _eval_adjoint(self): + return self.transpose().conjugate() + + def _eval_applyfunc(self, f): + out = self._new(self.rows, self.cols, [f(x) for x in self]) + return out + + def _eval_as_real_imag(self): # type: ignore + return (self.applyfunc(re), self.applyfunc(im)) + + def _eval_conjugate(self): + return self.applyfunc(lambda x: x.conjugate()) + + def _eval_permute_cols(self, perm): + # apply the permutation to a list + mapping = list(perm) + + def entry(i, j): + return self[i, mapping[j]] + + return self._new(self.rows, self.cols, entry) + + def _eval_permute_rows(self, perm): + # apply the permutation to a list + mapping = list(perm) + + def entry(i, j): + return self[mapping[i], j] + + return self._new(self.rows, self.cols, entry) + + def _eval_trace(self): + return sum(self[i, i] for i in range(self.rows)) + + def _eval_transpose(self): + return self._new(self.cols, self.rows, lambda i, j: self[j, i]) + + def adjoint(self): + """Conjugate transpose or Hermitian conjugation.""" + return self._eval_adjoint() + + def applyfunc(self, f): + """Apply a function to each element of the matrix. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix(2, 2, lambda i, j: i*2+j) + >>> m + Matrix([ + [0, 1], + [2, 3]]) + >>> m.applyfunc(lambda i: 2*i) + Matrix([ + [0, 2], + [4, 6]]) + + """ + if not callable(f): + raise TypeError("`f` must be callable.") + + return self._eval_applyfunc(f) + + def as_real_imag(self, deep=True, **hints): + """Returns a tuple containing the (real, imaginary) part of matrix.""" + # XXX: Ignoring deep and hints... + return self._eval_as_real_imag() + + def conjugate(self): + """Return the by-element conjugation. + + Examples + ======== + + >>> from sympy import SparseMatrix, I + >>> a = SparseMatrix(((1, 2 + I), (3, 4), (I, -I))) + >>> a + Matrix([ + [1, 2 + I], + [3, 4], + [I, -I]]) + >>> a.C + Matrix([ + [ 1, 2 - I], + [ 3, 4], + [-I, I]]) + + See Also + ======== + + transpose: Matrix transposition + H: Hermite conjugation + sympy.matrices.matrixbase.MatrixBase.D: Dirac conjugation + """ + return self._eval_conjugate() + + def doit(self, **hints): + return self.applyfunc(lambda x: x.doit(**hints)) + + def evalf(self, n=15, subs=None, maxn=100, chop=False, strict=False, quad=None, verbose=False): + """Apply evalf() to each element of self.""" + options = {'subs':subs, 'maxn':maxn, 'chop':chop, 'strict':strict, + 'quad':quad, 'verbose':verbose} + return self.applyfunc(lambda i: i.evalf(n, **options)) + + def expand(self, deep=True, modulus=None, power_base=True, power_exp=True, + mul=True, log=True, multinomial=True, basic=True, **hints): + """Apply core.function.expand to each entry of the matrix. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy import Matrix + >>> Matrix(1, 1, [x*(x+1)]) + Matrix([[x*(x + 1)]]) + >>> _.expand() + Matrix([[x**2 + x]]) + + """ + return self.applyfunc(lambda x: x.expand( + deep, modulus, power_base, power_exp, mul, log, multinomial, basic, + **hints)) + + @property + def H(self): + """Return Hermite conjugate. + + Examples + ======== + + >>> from sympy import Matrix, I + >>> m = Matrix((0, 1 + I, 2, 3)) + >>> m + Matrix([ + [ 0], + [1 + I], + [ 2], + [ 3]]) + >>> m.H + Matrix([[0, 1 - I, 2, 3]]) + + See Also + ======== + + conjugate: By-element conjugation + sympy.matrices.matrixbase.MatrixBase.D: Dirac conjugation + """ + return self.T.C + + def permute(self, perm, orientation='rows', direction='forward'): + r"""Permute the rows or columns of a matrix by the given list of + swaps. + + Parameters + ========== + + perm : Permutation, list, or list of lists + A representation for the permutation. + + If it is ``Permutation``, it is used directly with some + resizing with respect to the matrix size. + + If it is specified as list of lists, + (e.g., ``[[0, 1], [0, 2]]``), then the permutation is formed + from applying the product of cycles. The direction how the + cyclic product is applied is described in below. + + If it is specified as a list, the list should represent + an array form of a permutation. (e.g., ``[1, 2, 0]``) which + would would form the swapping function + `0 \mapsto 1, 1 \mapsto 2, 2\mapsto 0`. + + orientation : 'rows', 'cols' + A flag to control whether to permute the rows or the columns + + direction : 'forward', 'backward' + A flag to control whether to apply the permutations from + the start of the list first, or from the back of the list + first. + + For example, if the permutation specification is + ``[[0, 1], [0, 2]]``, + + If the flag is set to ``'forward'``, the cycle would be + formed as `0 \mapsto 2, 2 \mapsto 1, 1 \mapsto 0`. + + If the flag is set to ``'backward'``, the cycle would be + formed as `0 \mapsto 1, 1 \mapsto 2, 2 \mapsto 0`. + + If the argument ``perm`` is not in a form of list of lists, + this flag takes no effect. + + Examples + ======== + + >>> from sympy import eye + >>> M = eye(3) + >>> M.permute([[0, 1], [0, 2]], orientation='rows', direction='forward') + Matrix([ + [0, 0, 1], + [1, 0, 0], + [0, 1, 0]]) + + >>> from sympy import eye + >>> M = eye(3) + >>> M.permute([[0, 1], [0, 2]], orientation='rows', direction='backward') + Matrix([ + [0, 1, 0], + [0, 0, 1], + [1, 0, 0]]) + + Notes + ===== + + If a bijective function + `\sigma : \mathbb{N}_0 \rightarrow \mathbb{N}_0` denotes the + permutation. + + If the matrix `A` is the matrix to permute, represented as + a horizontal or a vertical stack of vectors: + + .. math:: + A = + \begin{bmatrix} + a_0 \\ a_1 \\ \vdots \\ a_{n-1} + \end{bmatrix} = + \begin{bmatrix} + \alpha_0 & \alpha_1 & \cdots & \alpha_{n-1} + \end{bmatrix} + + If the matrix `B` is the result, the permutation of matrix rows + is defined as: + + .. math:: + B := \begin{bmatrix} + a_{\sigma(0)} \\ a_{\sigma(1)} \\ \vdots \\ a_{\sigma(n-1)} + \end{bmatrix} + + And the permutation of matrix columns is defined as: + + .. math:: + B := \begin{bmatrix} + \alpha_{\sigma(0)} & \alpha_{\sigma(1)} & + \cdots & \alpha_{\sigma(n-1)} + \end{bmatrix} + """ + from sympy.combinatorics import Permutation + + # allow british variants and `columns` + if direction == 'forwards': + direction = 'forward' + if direction == 'backwards': + direction = 'backward' + if orientation == 'columns': + orientation = 'cols' + + if direction not in ('forward', 'backward'): + raise TypeError("direction='{}' is an invalid kwarg. " + "Try 'forward' or 'backward'".format(direction)) + if orientation not in ('rows', 'cols'): + raise TypeError("orientation='{}' is an invalid kwarg. " + "Try 'rows' or 'cols'".format(orientation)) + + if not isinstance(perm, (Permutation, Iterable)): + raise ValueError( + "{} must be a list, a list of lists, " + "or a SymPy permutation object.".format(perm)) + + # ensure all swaps are in range + max_index = self.rows if orientation == 'rows' else self.cols + if not all(0 <= t <= max_index for t in flatten(list(perm))): + raise IndexError("`swap` indices out of range.") + + if perm and not isinstance(perm, Permutation) and \ + isinstance(perm[0], Iterable): + if direction == 'forward': + perm = list(reversed(perm)) + perm = Permutation(perm, size=max_index+1) + else: + perm = Permutation(perm, size=max_index+1) + + if orientation == 'rows': + return self._eval_permute_rows(perm) + if orientation == 'cols': + return self._eval_permute_cols(perm) + + def permute_cols(self, swaps, direction='forward'): + """Alias for + ``self.permute(swaps, orientation='cols', direction=direction)`` + + See Also + ======== + + permute + """ + return self.permute(swaps, orientation='cols', direction=direction) + + def permute_rows(self, swaps, direction='forward'): + """Alias for + ``self.permute(swaps, orientation='rows', direction=direction)`` + + See Also + ======== + + permute + """ + return self.permute(swaps, orientation='rows', direction=direction) + + def refine(self, assumptions=True): + """Apply refine to each element of the matrix. + + Examples + ======== + + >>> from sympy import Symbol, Matrix, Abs, sqrt, Q + >>> x = Symbol('x') + >>> Matrix([[Abs(x)**2, sqrt(x**2)],[sqrt(x**2), Abs(x)**2]]) + Matrix([ + [ Abs(x)**2, sqrt(x**2)], + [sqrt(x**2), Abs(x)**2]]) + >>> _.refine(Q.real(x)) + Matrix([ + [ x**2, Abs(x)], + [Abs(x), x**2]]) + + """ + return self.applyfunc(lambda x: refine(x, assumptions)) + + def replace(self, F, G, map=False, simultaneous=True, exact=None): + """Replaces Function F in Matrix entries with Function G. + + Examples + ======== + + >>> from sympy import symbols, Function, Matrix + >>> F, G = symbols('F, G', cls=Function) + >>> M = Matrix(2, 2, lambda i, j: F(i+j)) ; M + Matrix([ + [F(0), F(1)], + [F(1), F(2)]]) + >>> N = M.replace(F,G) + >>> N + Matrix([ + [G(0), G(1)], + [G(1), G(2)]]) + """ + return self.applyfunc( + lambda x: x.replace(F, G, map=map, simultaneous=simultaneous, exact=exact)) + + def rot90(self, k=1): + """Rotates Matrix by 90 degrees + + Parameters + ========== + + k : int + Specifies how many times the matrix is rotated by 90 degrees + (clockwise when positive, counter-clockwise when negative). + + Examples + ======== + + >>> from sympy import Matrix, symbols + >>> A = Matrix(2, 2, symbols('a:d')) + >>> A + Matrix([ + [a, b], + [c, d]]) + + Rotating the matrix clockwise one time: + + >>> A.rot90(1) + Matrix([ + [c, a], + [d, b]]) + + Rotating the matrix anticlockwise two times: + + >>> A.rot90(-2) + Matrix([ + [d, c], + [b, a]]) + """ + + mod = k%4 + if mod == 0: + return self + if mod == 1: + return self[::-1, ::].T + if mod == 2: + return self[::-1, ::-1] + if mod == 3: + return self[::, ::-1].T + + def simplify(self, **kwargs): + """Apply simplify to each element of the matrix. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import SparseMatrix, sin, cos + >>> SparseMatrix(1, 1, [x*sin(y)**2 + x*cos(y)**2]) + Matrix([[x*sin(y)**2 + x*cos(y)**2]]) + >>> _.simplify() + Matrix([[x]]) + """ + return self.applyfunc(lambda x: x.simplify(**kwargs)) + + def subs(self, *args, **kwargs): # should mirror core.basic.subs + """Return a new matrix with subs applied to each entry. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import SparseMatrix, Matrix + >>> SparseMatrix(1, 1, [x]) + Matrix([[x]]) + >>> _.subs(x, y) + Matrix([[y]]) + >>> Matrix(_).subs(y, x) + Matrix([[x]]) + """ + + if len(args) == 1 and not isinstance(args[0], (dict, set)) and iter(args[0]) and not is_sequence(args[0]): + args = (list(args[0]),) + + return self.applyfunc(lambda x: x.subs(*args, **kwargs)) + + def trace(self): + """ + Returns the trace of a square matrix i.e. the sum of the + diagonal elements. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix(2, 2, [1, 2, 3, 4]) + >>> A.trace() + 5 + + """ + if self.rows != self.cols: + raise NonSquareMatrixError() + return self._eval_trace() + + def transpose(self): + """ + Returns the transpose of the matrix. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix(2, 2, [1, 2, 3, 4]) + >>> A.transpose() + Matrix([ + [1, 3], + [2, 4]]) + + >>> from sympy import Matrix, I + >>> m=Matrix(((1, 2+I), (3, 4))) + >>> m + Matrix([ + [1, 2 + I], + [3, 4]]) + >>> m.transpose() + Matrix([ + [ 1, 3], + [2 + I, 4]]) + >>> m.T == m.transpose() + True + + See Also + ======== + + conjugate: By-element conjugation + + """ + return self._eval_transpose() + + @property + def T(self): + '''Matrix transposition''' + return self.transpose() + + @property + def C(self): + '''By-element conjugation''' + return self.conjugate() + + def n(self, *args, **kwargs): + """Apply evalf() to each element of self.""" + return self.evalf(*args, **kwargs) + + def xreplace(self, rule): # should mirror core.basic.xreplace + """Return a new matrix with xreplace applied to each entry. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import SparseMatrix, Matrix + >>> SparseMatrix(1, 1, [x]) + Matrix([[x]]) + >>> _.xreplace({x: y}) + Matrix([[y]]) + >>> Matrix(_).xreplace({y: x}) + Matrix([[x]]) + """ + return self.applyfunc(lambda x: x.xreplace(rule)) + + def _eval_simplify(self, **kwargs): + # XXX: We can't use self.simplify here as mutable subclasses will + # override simplify and have it return None + return MatrixOperations.simplify(self, **kwargs) + + def _eval_trigsimp(self, **opts): + from sympy.simplify.trigsimp import trigsimp + return self.applyfunc(lambda x: trigsimp(x, **opts)) + + def upper_triangular(self, k=0): + """Return the elements on and above the kth diagonal of a matrix. + If k is not specified then simply returns upper-triangular portion + of a matrix + + Examples + ======== + + >>> from sympy import ones + >>> A = ones(4) + >>> A.upper_triangular() + Matrix([ + [1, 1, 1, 1], + [0, 1, 1, 1], + [0, 0, 1, 1], + [0, 0, 0, 1]]) + + >>> A.upper_triangular(2) + Matrix([ + [0, 0, 1, 1], + [0, 0, 0, 1], + [0, 0, 0, 0], + [0, 0, 0, 0]]) + + >>> A.upper_triangular(-1) + Matrix([ + [1, 1, 1, 1], + [1, 1, 1, 1], + [0, 1, 1, 1], + [0, 0, 1, 1]]) + + """ + + def entry(i, j): + return self[i, j] if i + k <= j else self.zero + + return self._new(self.rows, self.cols, entry) + + + def lower_triangular(self, k=0): + """Return the elements on and below the kth diagonal of a matrix. + If k is not specified then simply returns lower-triangular portion + of a matrix + + Examples + ======== + + >>> from sympy import ones + >>> A = ones(4) + >>> A.lower_triangular() + Matrix([ + [1, 0, 0, 0], + [1, 1, 0, 0], + [1, 1, 1, 0], + [1, 1, 1, 1]]) + + >>> A.lower_triangular(-2) + Matrix([ + [0, 0, 0, 0], + [0, 0, 0, 0], + [1, 0, 0, 0], + [1, 1, 0, 0]]) + + >>> A.lower_triangular(1) + Matrix([ + [1, 1, 0, 0], + [1, 1, 1, 0], + [1, 1, 1, 1], + [1, 1, 1, 1]]) + + """ + + def entry(i, j): + return self[i, j] if i + k >= j else self.zero + + return self._new(self.rows, self.cols, entry) + + + +class MatrixArithmetic(MatrixRequired): + """Provides basic matrix arithmetic operations. + Should not be instantiated directly.""" + + _op_priority = 10.01 + + def _eval_Abs(self): + return self._new(self.rows, self.cols, lambda i, j: Abs(self[i, j])) + + def _eval_add(self, other): + return self._new(self.rows, self.cols, + lambda i, j: self[i, j] + other[i, j]) + + def _eval_matrix_mul(self, other): + def entry(i, j): + vec = [self[i,k]*other[k,j] for k in range(self.cols)] + try: + return Add(*vec) + except (TypeError, SympifyError): + # Some matrices don't work with `sum` or `Add` + # They don't work with `sum` because `sum` tries to add `0` + # Fall back to a safe way to multiply if the `Add` fails. + return reduce(lambda a, b: a + b, vec) + + return self._new(self.rows, other.cols, entry) + + def _eval_matrix_mul_elementwise(self, other): + return self._new(self.rows, self.cols, lambda i, j: self[i,j]*other[i,j]) + + def _eval_matrix_rmul(self, other): + def entry(i, j): + return sum(other[i,k]*self[k,j] for k in range(other.cols)) + return self._new(other.rows, self.cols, entry) + + def _eval_pow_by_recursion(self, num): + if num == 1: + return self + + if num % 2 == 1: + a, b = self, self._eval_pow_by_recursion(num - 1) + else: + a = b = self._eval_pow_by_recursion(num // 2) + + return a.multiply(b) + + def _eval_pow_by_cayley(self, exp): + from sympy.discrete.recurrences import linrec_coeffs + row = self.shape[0] + p = self.charpoly() + + coeffs = (-p).all_coeffs()[1:] + coeffs = linrec_coeffs(coeffs, exp) + new_mat = self.eye(row) + ans = self.zeros(row) + + for i in range(row): + ans += coeffs[i]*new_mat + new_mat *= self + + return ans + + def _eval_pow_by_recursion_dotprodsimp(self, num, prevsimp=None): + if prevsimp is None: + prevsimp = [True]*len(self) + + if num == 1: + return self + + if num % 2 == 1: + a, b = self, self._eval_pow_by_recursion_dotprodsimp(num - 1, + prevsimp=prevsimp) + else: + a = b = self._eval_pow_by_recursion_dotprodsimp(num // 2, + prevsimp=prevsimp) + + m = a.multiply(b, dotprodsimp=False) + lenm = len(m) + elems = [None]*lenm + + for i in range(lenm): + if prevsimp[i]: + elems[i], prevsimp[i] = _dotprodsimp(m[i], withsimp=True) + else: + elems[i] = m[i] + + return m._new(m.rows, m.cols, elems) + + def _eval_scalar_mul(self, other): + return self._new(self.rows, self.cols, lambda i, j: self[i,j]*other) + + def _eval_scalar_rmul(self, other): + return self._new(self.rows, self.cols, lambda i, j: other*self[i,j]) + + def _eval_Mod(self, other): + return self._new(self.rows, self.cols, lambda i, j: Mod(self[i, j], other)) + + # Python arithmetic functions + def __abs__(self): + """Returns a new matrix with entry-wise absolute values.""" + return self._eval_Abs() + + @call_highest_priority('__radd__') + def __add__(self, other): + """Return self + other, raising ShapeError if shapes do not match.""" + if isinstance(other, NDimArray): # Matrix and array addition is currently not implemented + return NotImplemented + other = _matrixify(other) + # matrix-like objects can have shapes. This is + # our first sanity check. + if hasattr(other, 'shape'): + if self.shape != other.shape: + raise ShapeError("Matrix size mismatch: %s + %s" % ( + self.shape, other.shape)) + + # honest SymPy matrices defer to their class's routine + if getattr(other, 'is_Matrix', False): + # call the highest-priority class's _eval_add + a, b = self, other + if a.__class__ != classof(a, b): + b, a = a, b + return a._eval_add(b) + # Matrix-like objects can be passed to CommonMatrix routines directly. + if getattr(other, 'is_MatrixLike', False): + return MatrixArithmetic._eval_add(self, other) + + raise TypeError('cannot add %s and %s' % (type(self), type(other))) + + @call_highest_priority('__rtruediv__') + def __truediv__(self, other): + return self * (self.one / other) + + @call_highest_priority('__rmatmul__') + def __matmul__(self, other): + other = _matrixify(other) + if not getattr(other, 'is_Matrix', False) and not getattr(other, 'is_MatrixLike', False): + return NotImplemented + + return self.__mul__(other) + + def __mod__(self, other): + return self.applyfunc(lambda x: x % other) + + @call_highest_priority('__rmul__') + def __mul__(self, other): + """Return self*other where other is either a scalar or a matrix + of compatible dimensions. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([[1, 2, 3], [4, 5, 6]]) + >>> 2*A == A*2 == Matrix([[2, 4, 6], [8, 10, 12]]) + True + >>> B = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> A*B + Matrix([ + [30, 36, 42], + [66, 81, 96]]) + >>> B*A + Traceback (most recent call last): + ... + ShapeError: Matrices size mismatch. + >>> + + See Also + ======== + + matrix_multiply_elementwise + """ + + return self.multiply(other) + + def multiply(self, other, dotprodsimp=None): + """Same as __mul__() but with optional simplification. + + Parameters + ========== + + dotprodsimp : bool, optional + Specifies whether intermediate term algebraic simplification is used + during matrix multiplications to control expression blowup and thus + speed up calculation. Default is off. + """ + + isimpbool = _get_intermediate_simp_bool(False, dotprodsimp) + other = _matrixify(other) + # matrix-like objects can have shapes. This is + # our first sanity check. Double check other is not explicitly not a Matrix. + if (hasattr(other, 'shape') and len(other.shape) == 2 and + (getattr(other, 'is_Matrix', True) or + getattr(other, 'is_MatrixLike', True))): + if self.shape[1] != other.shape[0]: + raise ShapeError("Matrix size mismatch: %s * %s." % ( + self.shape, other.shape)) + + # honest SymPy matrices defer to their class's routine + if getattr(other, 'is_Matrix', False): + m = self._eval_matrix_mul(other) + if isimpbool: + return m._new(m.rows, m.cols, [_dotprodsimp(e) for e in m]) + return m + + # Matrix-like objects can be passed to CommonMatrix routines directly. + if getattr(other, 'is_MatrixLike', False): + return MatrixArithmetic._eval_matrix_mul(self, other) + + # if 'other' is not iterable then scalar multiplication. + if not isinstance(other, Iterable): + try: + return self._eval_scalar_mul(other) + except TypeError: + pass + + return NotImplemented + + def multiply_elementwise(self, other): + """Return the Hadamard product (elementwise product) of A and B + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([[0, 1, 2], [3, 4, 5]]) + >>> B = Matrix([[1, 10, 100], [100, 10, 1]]) + >>> A.multiply_elementwise(B) + Matrix([ + [ 0, 10, 200], + [300, 40, 5]]) + + See Also + ======== + + sympy.matrices.matrixbase.MatrixBase.cross + sympy.matrices.matrixbase.MatrixBase.dot + multiply + """ + if self.shape != other.shape: + raise ShapeError("Matrix shapes must agree {} != {}".format(self.shape, other.shape)) + + return self._eval_matrix_mul_elementwise(other) + + def __neg__(self): + return self._eval_scalar_mul(-1) + + @call_highest_priority('__rpow__') + def __pow__(self, exp): + """Return self**exp a scalar or symbol.""" + + return self.pow(exp) + + + def pow(self, exp, method=None): + r"""Return self**exp a scalar or symbol. + + Parameters + ========== + + method : multiply, mulsimp, jordan, cayley + If multiply then it returns exponentiation using recursion. + If jordan then Jordan form exponentiation will be used. + If cayley then the exponentiation is done using Cayley-Hamilton + theorem. + If mulsimp then the exponentiation is done using recursion + with dotprodsimp. This specifies whether intermediate term + algebraic simplification is used during naive matrix power to + control expression blowup and thus speed up calculation. + If None, then it heuristically decides which method to use. + + """ + + if method is not None and method not in ['multiply', 'mulsimp', 'jordan', 'cayley']: + raise TypeError('No such method') + if self.rows != self.cols: + raise NonSquareMatrixError() + a = self + jordan_pow = getattr(a, '_matrix_pow_by_jordan_blocks', None) + exp = sympify(exp) + + if exp.is_zero: + return a._new(a.rows, a.cols, lambda i, j: int(i == j)) + if exp == 1: + return a + + diagonal = getattr(a, 'is_diagonal', None) + if diagonal is not None and diagonal(): + return a._new(a.rows, a.cols, lambda i, j: a[i,j]**exp if i == j else 0) + + if exp.is_Number and exp % 1 == 0: + if a.rows == 1: + return a._new([[a[0]**exp]]) + if exp < 0: + exp = -exp + a = a.inv() + # When certain conditions are met, + # Jordan block algorithm is faster than + # computation by recursion. + if method == 'jordan': + try: + return jordan_pow(exp) + except MatrixError: + if method == 'jordan': + raise + + elif method == 'cayley': + if not exp.is_Number or exp % 1 != 0: + raise ValueError("cayley method is only valid for integer powers") + return a._eval_pow_by_cayley(exp) + + elif method == "mulsimp": + if not exp.is_Number or exp % 1 != 0: + raise ValueError("mulsimp method is only valid for integer powers") + return a._eval_pow_by_recursion_dotprodsimp(exp) + + elif method == "multiply": + if not exp.is_Number or exp % 1 != 0: + raise ValueError("multiply method is only valid for integer powers") + return a._eval_pow_by_recursion(exp) + + elif method is None and exp.is_Number and exp % 1 == 0: + if exp.is_Float: + exp = Integer(exp) + # Decide heuristically which method to apply + if a.rows == 2 and exp > 100000: + return jordan_pow(exp) + elif _get_intermediate_simp_bool(True, None): + return a._eval_pow_by_recursion_dotprodsimp(exp) + elif exp > 10000: + return a._eval_pow_by_cayley(exp) + else: + return a._eval_pow_by_recursion(exp) + + if jordan_pow: + try: + return jordan_pow(exp) + except NonInvertibleMatrixError: + # Raised by jordan_pow on zero determinant matrix unless exp is + # definitely known to be a non-negative integer. + # Here we raise if n is definitely not a non-negative integer + # but otherwise we can leave this as an unevaluated MatPow. + if exp.is_integer is False or exp.is_nonnegative is False: + raise + + from sympy.matrices.expressions import MatPow + return MatPow(a, exp) + + @call_highest_priority('__add__') + def __radd__(self, other): + return self + other + + @call_highest_priority('__matmul__') + def __rmatmul__(self, other): + other = _matrixify(other) + if not getattr(other, 'is_Matrix', False) and not getattr(other, 'is_MatrixLike', False): + return NotImplemented + + return self.__rmul__(other) + + @call_highest_priority('__mul__') + def __rmul__(self, other): + return self.rmultiply(other) + + def rmultiply(self, other, dotprodsimp=None): + """Same as __rmul__() but with optional simplification. + + Parameters + ========== + + dotprodsimp : bool, optional + Specifies whether intermediate term algebraic simplification is used + during matrix multiplications to control expression blowup and thus + speed up calculation. Default is off. + """ + isimpbool = _get_intermediate_simp_bool(False, dotprodsimp) + other = _matrixify(other) + # matrix-like objects can have shapes. This is + # our first sanity check. Double check other is not explicitly not a Matrix. + if (hasattr(other, 'shape') and len(other.shape) == 2 and + (getattr(other, 'is_Matrix', True) or + getattr(other, 'is_MatrixLike', True))): + if self.shape[0] != other.shape[1]: + raise ShapeError("Matrix size mismatch.") + + # honest SymPy matrices defer to their class's routine + if getattr(other, 'is_Matrix', False): + m = self._eval_matrix_rmul(other) + if isimpbool: + return m._new(m.rows, m.cols, [_dotprodsimp(e) for e in m]) + return m + # Matrix-like objects can be passed to CommonMatrix routines directly. + if getattr(other, 'is_MatrixLike', False): + return MatrixArithmetic._eval_matrix_rmul(self, other) + + # if 'other' is not iterable then scalar multiplication. + if not isinstance(other, Iterable): + try: + return self._eval_scalar_rmul(other) + except TypeError: + pass + + return NotImplemented + + @call_highest_priority('__sub__') + def __rsub__(self, a): + return (-self) + a + + @call_highest_priority('__rsub__') + def __sub__(self, a): + return self + (-a) + + +class MatrixCommon(MatrixArithmetic, MatrixOperations, MatrixProperties, + MatrixSpecial, MatrixShaping): + """All common matrix operations including basic arithmetic, shaping, + and special matrices like `zeros`, and `eye`.""" + _diff_wrt: bool = True + + +class _MinimalMatrix: + """Class providing the minimum functionality + for a matrix-like object and implementing every method + required for a `MatrixRequired`. This class does not have everything + needed to become a full-fledged SymPy object, but it will satisfy the + requirements of anything inheriting from `MatrixRequired`. If you wish + to make a specialized matrix type, make sure to implement these + methods and properties with the exception of `__init__` and `__repr__` + which are included for convenience.""" + + is_MatrixLike = True + _sympify = staticmethod(sympify) + _class_priority = 3 + zero = S.Zero + one = S.One + + is_Matrix = True + is_MatrixExpr = False + + @classmethod + def _new(cls, *args, **kwargs): + return cls(*args, **kwargs) + + def __init__(self, rows, cols=None, mat=None, copy=False): + if isfunction(mat): + # if we passed in a function, use that to populate the indices + mat = [mat(i, j) for i in range(rows) for j in range(cols)] + if cols is None and mat is None: + mat = rows + rows, cols = getattr(mat, 'shape', (rows, cols)) + try: + # if we passed in a list of lists, flatten it and set the size + if cols is None and mat is None: + mat = rows + cols = len(mat[0]) + rows = len(mat) + mat = [x for l in mat for x in l] + except (IndexError, TypeError): + pass + self.mat = tuple(self._sympify(x) for x in mat) + self.rows, self.cols = rows, cols + if self.rows is None or self.cols is None: + raise NotImplementedError("Cannot initialize matrix with given parameters") + + def __getitem__(self, key): + def _normalize_slices(row_slice, col_slice): + """Ensure that row_slice and col_slice do not have + `None` in their arguments. Any integers are converted + to slices of length 1""" + if not isinstance(row_slice, slice): + row_slice = slice(row_slice, row_slice + 1, None) + row_slice = slice(*row_slice.indices(self.rows)) + + if not isinstance(col_slice, slice): + col_slice = slice(col_slice, col_slice + 1, None) + col_slice = slice(*col_slice.indices(self.cols)) + + return (row_slice, col_slice) + + def _coord_to_index(i, j): + """Return the index in _mat corresponding + to the (i,j) position in the matrix. """ + return i * self.cols + j + + if isinstance(key, tuple): + i, j = key + if isinstance(i, slice) or isinstance(j, slice): + # if the coordinates are not slices, make them so + # and expand the slices so they don't contain `None` + i, j = _normalize_slices(i, j) + + rowsList, colsList = list(range(self.rows))[i], \ + list(range(self.cols))[j] + indices = (i * self.cols + j for i in rowsList for j in + colsList) + return self._new(len(rowsList), len(colsList), + [self.mat[i] for i in indices]) + + # if the key is a tuple of ints, change + # it to an array index + key = _coord_to_index(i, j) + return self.mat[key] + + def __eq__(self, other): + try: + classof(self, other) + except TypeError: + return False + return ( + self.shape == other.shape and list(self) == list(other)) + + def __len__(self): + return self.rows*self.cols + + def __repr__(self): + return "_MinimalMatrix({}, {}, {})".format(self.rows, self.cols, + self.mat) + + @property + def shape(self): + return (self.rows, self.cols) + + +class _CastableMatrix: # this is needed here ONLY FOR TESTS. + def as_mutable(self): + return self + + def as_immutable(self): + return self + + +class _MatrixWrapper: + """Wrapper class providing the minimum functionality for a matrix-like + object: .rows, .cols, .shape, indexability, and iterability. CommonMatrix + math operations should work on matrix-like objects. This one is intended for + matrix-like objects which use the same indexing format as SymPy with respect + to returning matrix elements instead of rows for non-tuple indexes. + """ + + is_Matrix = False # needs to be here because of __getattr__ + is_MatrixLike = True + + def __init__(self, mat, shape): + self.mat = mat + self.shape = shape + self.rows, self.cols = shape + + def __getitem__(self, key): + if isinstance(key, tuple): + return sympify(self.mat.__getitem__(key)) + + return sympify(self.mat.__getitem__((key // self.rows, key % self.cols))) + + def __iter__(self): # supports numpy.matrix and numpy.array + mat = self.mat + cols = self.cols + + return iter(sympify(mat[r, c]) for r in range(self.rows) for c in range(cols)) + + +def _matrixify(mat): + """If `mat` is a Matrix or is matrix-like, + return a Matrix or MatrixWrapper object. Otherwise + `mat` is passed through without modification.""" + + if getattr(mat, 'is_Matrix', False) or getattr(mat, 'is_MatrixLike', False): + return mat + + if not(getattr(mat, 'is_Matrix', True) or getattr(mat, 'is_MatrixLike', True)): + return mat + + shape = None + + if hasattr(mat, 'shape'): # numpy, scipy.sparse + if len(mat.shape) == 2: + shape = mat.shape + elif hasattr(mat, 'rows') and hasattr(mat, 'cols'): # mpmath + shape = (mat.rows, mat.cols) + + if shape: + return _MatrixWrapper(mat, shape) + + return mat + + +def a2idx(j, n=None): + """Return integer after making positive and validating against n.""" + if not isinstance(j, int): + jindex = getattr(j, '__index__', None) + if jindex is not None: + j = jindex() + else: + raise IndexError("Invalid index a[%r]" % (j,)) + if n is not None: + if j < 0: + j += n + if not (j >= 0 and j < n): + raise IndexError("Index out of range: a[%s]" % (j,)) + return int(j) + + +def classof(A, B): + """ + Get the type of the result when combining matrices of different types. + + Currently the strategy is that immutability is contagious. + + Examples + ======== + + >>> from sympy import Matrix, ImmutableMatrix + >>> from sympy.matrices.matrixbase import classof + >>> M = Matrix([[1, 2], [3, 4]]) # a Mutable Matrix + >>> IM = ImmutableMatrix([[1, 2], [3, 4]]) + >>> classof(M, IM) + + """ + priority_A = getattr(A, '_class_priority', None) + priority_B = getattr(B, '_class_priority', None) + if None not in (priority_A, priority_B): + if A._class_priority > B._class_priority: + return A.__class__ + else: + return B.__class__ + + try: + import numpy + except ImportError: + pass + else: + if isinstance(A, numpy.ndarray): + return B.__class__ + if isinstance(B, numpy.ndarray): + return A.__class__ + + raise TypeError("Incompatible classes %s, %s" % (A.__class__, B.__class__)) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/decompositions.py b/venv/lib/python3.11/site-packages/sympy/matrices/decompositions.py new file mode 100644 index 0000000000000000000000000000000000000000..a8dd466d84c957b870396a050fd25ec21e7113a3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/decompositions.py @@ -0,0 +1,1621 @@ +import copy + +from sympy.core import S +from sympy.core.function import expand_mul +from sympy.functions.elementary.miscellaneous import Min, sqrt +from sympy.functions.elementary.complexes import sign + +from .exceptions import NonSquareMatrixError, NonPositiveDefiniteMatrixError +from .utilities import _get_intermediate_simp, _iszero +from .determinant import _find_reasonable_pivot_naive + + +def _rank_decomposition(M, iszerofunc=_iszero, simplify=False): + r"""Returns a pair of matrices (`C`, `F`) with matching rank + such that `A = C F`. + + Parameters + ========== + + iszerofunc : Function, optional + A function used for detecting whether an element can + act as a pivot. ``lambda x: x.is_zero`` is used by default. + + simplify : Bool or Function, optional + A function used to simplify elements when looking for a + pivot. By default SymPy's ``simplify`` is used. + + Returns + ======= + + (C, F) : Matrices + `C` and `F` are full-rank matrices with rank as same as `A`, + whose product gives `A`. + + See Notes for additional mathematical details. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([ + ... [1, 3, 1, 4], + ... [2, 7, 3, 9], + ... [1, 5, 3, 1], + ... [1, 2, 0, 8] + ... ]) + >>> C, F = A.rank_decomposition() + >>> C + Matrix([ + [1, 3, 4], + [2, 7, 9], + [1, 5, 1], + [1, 2, 8]]) + >>> F + Matrix([ + [1, 0, -2, 0], + [0, 1, 1, 0], + [0, 0, 0, 1]]) + >>> C * F == A + True + + Notes + ===== + + Obtaining `F`, an RREF of `A`, is equivalent to creating a + product + + .. math:: + E_n E_{n-1} ... E_1 A = F + + where `E_n, E_{n-1}, \dots, E_1` are the elimination matrices or + permutation matrices equivalent to each row-reduction step. + + The inverse of the same product of elimination matrices gives + `C`: + + .. math:: + C = \left(E_n E_{n-1} \dots E_1\right)^{-1} + + It is not necessary, however, to actually compute the inverse: + the columns of `C` are those from the original matrix with the + same column indices as the indices of the pivot columns of `F`. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Rank_factorization + + .. [2] Piziak, R.; Odell, P. L. (1 June 1999). + "Full Rank Factorization of Matrices". + Mathematics Magazine. 72 (3): 193. doi:10.2307/2690882 + + See Also + ======== + + sympy.matrices.matrixbase.MatrixBase.rref + """ + + F, pivot_cols = M.rref(simplify=simplify, iszerofunc=iszerofunc, + pivots=True) + rank = len(pivot_cols) + + C = M.extract(range(M.rows), pivot_cols) + F = F[:rank, :] + + return C, F + + +def _liupc(M): + """Liu's algorithm, for pre-determination of the Elimination Tree of + the given matrix, used in row-based symbolic Cholesky factorization. + + Examples + ======== + + >>> from sympy import SparseMatrix + >>> S = SparseMatrix([ + ... [1, 0, 3, 2], + ... [0, 0, 1, 0], + ... [4, 0, 0, 5], + ... [0, 6, 7, 0]]) + >>> S.liupc() + ([[0], [], [0], [1, 2]], [4, 3, 4, 4]) + + References + ========== + + .. [1] Symbolic Sparse Cholesky Factorization using Elimination Trees, + Jeroen Van Grondelle (1999) + https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.39.7582 + """ + # Algorithm 2.4, p 17 of reference + + # get the indices of the elements that are non-zero on or below diag + R = [[] for r in range(M.rows)] + + for r, c, _ in M.row_list(): + if c <= r: + R[r].append(c) + + inf = len(R) # nothing will be this large + parent = [inf]*M.rows + virtual = [inf]*M.rows + + for r in range(M.rows): + for c in R[r][:-1]: + while virtual[c] < r: + t = virtual[c] + virtual[c] = r + c = t + + if virtual[c] == inf: + parent[c] = virtual[c] = r + + return R, parent + +def _row_structure_symbolic_cholesky(M): + """Symbolic cholesky factorization, for pre-determination of the + non-zero structure of the Cholesky factororization. + + Examples + ======== + + >>> from sympy import SparseMatrix + >>> S = SparseMatrix([ + ... [1, 0, 3, 2], + ... [0, 0, 1, 0], + ... [4, 0, 0, 5], + ... [0, 6, 7, 0]]) + >>> S.row_structure_symbolic_cholesky() + [[0], [], [0], [1, 2]] + + References + ========== + + .. [1] Symbolic Sparse Cholesky Factorization using Elimination Trees, + Jeroen Van Grondelle (1999) + https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.39.7582 + """ + + R, parent = M.liupc() + inf = len(R) # this acts as infinity + Lrow = copy.deepcopy(R) + + for k in range(M.rows): + for j in R[k]: + while j != inf and j != k: + Lrow[k].append(j) + j = parent[j] + + Lrow[k] = sorted(set(Lrow[k])) + + return Lrow + + +def _cholesky(M, hermitian=True): + """Returns the Cholesky-type decomposition L of a matrix A + such that L * L.H == A if hermitian flag is True, + or L * L.T == A if hermitian is False. + + A must be a Hermitian positive-definite matrix if hermitian is True, + or a symmetric matrix if it is False. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix(((25, 15, -5), (15, 18, 0), (-5, 0, 11))) + >>> A.cholesky() + Matrix([ + [ 5, 0, 0], + [ 3, 3, 0], + [-1, 1, 3]]) + >>> A.cholesky() * A.cholesky().T + Matrix([ + [25, 15, -5], + [15, 18, 0], + [-5, 0, 11]]) + + The matrix can have complex entries: + + >>> from sympy import I + >>> A = Matrix(((9, 3*I), (-3*I, 5))) + >>> A.cholesky() + Matrix([ + [ 3, 0], + [-I, 2]]) + >>> A.cholesky() * A.cholesky().H + Matrix([ + [ 9, 3*I], + [-3*I, 5]]) + + Non-hermitian Cholesky-type decomposition may be useful when the + matrix is not positive-definite. + + >>> A = Matrix([[1, 2], [2, 1]]) + >>> L = A.cholesky(hermitian=False) + >>> L + Matrix([ + [1, 0], + [2, sqrt(3)*I]]) + >>> L*L.T == A + True + + See Also + ======== + + sympy.matrices.dense.DenseMatrix.LDLdecomposition + sympy.matrices.matrixbase.MatrixBase.LUdecomposition + QRdecomposition + """ + + from .dense import MutableDenseMatrix + + if not M.is_square: + raise NonSquareMatrixError("Matrix must be square.") + if hermitian and not M.is_hermitian: + raise ValueError("Matrix must be Hermitian.") + if not hermitian and not M.is_symmetric(): + raise ValueError("Matrix must be symmetric.") + + L = MutableDenseMatrix.zeros(M.rows, M.rows) + + if hermitian: + for i in range(M.rows): + for j in range(i): + L[i, j] = ((1 / L[j, j])*(M[i, j] - + sum(L[i, k]*L[j, k].conjugate() for k in range(j)))) + + Lii2 = (M[i, i] - + sum(L[i, k]*L[i, k].conjugate() for k in range(i))) + + if Lii2.is_positive is False: + raise NonPositiveDefiniteMatrixError( + "Matrix must be positive-definite") + + L[i, i] = sqrt(Lii2) + + else: + for i in range(M.rows): + for j in range(i): + L[i, j] = ((1 / L[j, j])*(M[i, j] - + sum(L[i, k]*L[j, k] for k in range(j)))) + + L[i, i] = sqrt(M[i, i] - + sum(L[i, k]**2 for k in range(i))) + + return M._new(L) + +def _cholesky_sparse(M, hermitian=True): + """ + Returns the Cholesky decomposition L of a matrix A + such that L * L.T = A + + A must be a square, symmetric, positive-definite + and non-singular matrix + + Examples + ======== + + >>> from sympy import SparseMatrix + >>> A = SparseMatrix(((25,15,-5),(15,18,0),(-5,0,11))) + >>> A.cholesky() + Matrix([ + [ 5, 0, 0], + [ 3, 3, 0], + [-1, 1, 3]]) + >>> A.cholesky() * A.cholesky().T == A + True + + The matrix can have complex entries: + + >>> from sympy import I + >>> A = SparseMatrix(((9, 3*I), (-3*I, 5))) + >>> A.cholesky() + Matrix([ + [ 3, 0], + [-I, 2]]) + >>> A.cholesky() * A.cholesky().H + Matrix([ + [ 9, 3*I], + [-3*I, 5]]) + + Non-hermitian Cholesky-type decomposition may be useful when the + matrix is not positive-definite. + + >>> A = SparseMatrix([[1, 2], [2, 1]]) + >>> L = A.cholesky(hermitian=False) + >>> L + Matrix([ + [1, 0], + [2, sqrt(3)*I]]) + >>> L*L.T == A + True + + See Also + ======== + + sympy.matrices.sparse.SparseMatrix.LDLdecomposition + sympy.matrices.matrixbase.MatrixBase.LUdecomposition + QRdecomposition + """ + + from .dense import MutableDenseMatrix + + if not M.is_square: + raise NonSquareMatrixError("Matrix must be square.") + if hermitian and not M.is_hermitian: + raise ValueError("Matrix must be Hermitian.") + if not hermitian and not M.is_symmetric(): + raise ValueError("Matrix must be symmetric.") + + dps = _get_intermediate_simp(expand_mul, expand_mul) + Crowstruc = M.row_structure_symbolic_cholesky() + C = MutableDenseMatrix.zeros(M.rows) + + for i in range(len(Crowstruc)): + for j in Crowstruc[i]: + if i != j: + C[i, j] = M[i, j] + summ = 0 + + for p1 in Crowstruc[i]: + if p1 < j: + for p2 in Crowstruc[j]: + if p2 < j: + if p1 == p2: + if hermitian: + summ += C[i, p1]*C[j, p1].conjugate() + else: + summ += C[i, p1]*C[j, p1] + else: + break + else: + break + + C[i, j] = dps((C[i, j] - summ) / C[j, j]) + + else: # i == j + C[j, j] = M[j, j] + summ = 0 + + for k in Crowstruc[j]: + if k < j: + if hermitian: + summ += C[j, k]*C[j, k].conjugate() + else: + summ += C[j, k]**2 + else: + break + + Cjj2 = dps(C[j, j] - summ) + + if hermitian and Cjj2.is_positive is False: + raise NonPositiveDefiniteMatrixError( + "Matrix must be positive-definite") + + C[j, j] = sqrt(Cjj2) + + return M._new(C) + + +def _LDLdecomposition(M, hermitian=True): + """Returns the LDL Decomposition (L, D) of matrix A, + such that L * D * L.H == A if hermitian flag is True, or + L * D * L.T == A if hermitian is False. + This method eliminates the use of square root. + Further this ensures that all the diagonal entries of L are 1. + A must be a Hermitian positive-definite matrix if hermitian is True, + or a symmetric matrix otherwise. + + Examples + ======== + + >>> from sympy import Matrix, eye + >>> A = Matrix(((25, 15, -5), (15, 18, 0), (-5, 0, 11))) + >>> L, D = A.LDLdecomposition() + >>> L + Matrix([ + [ 1, 0, 0], + [ 3/5, 1, 0], + [-1/5, 1/3, 1]]) + >>> D + Matrix([ + [25, 0, 0], + [ 0, 9, 0], + [ 0, 0, 9]]) + >>> L * D * L.T * A.inv() == eye(A.rows) + True + + The matrix can have complex entries: + + >>> from sympy import I + >>> A = Matrix(((9, 3*I), (-3*I, 5))) + >>> L, D = A.LDLdecomposition() + >>> L + Matrix([ + [ 1, 0], + [-I/3, 1]]) + >>> D + Matrix([ + [9, 0], + [0, 4]]) + >>> L*D*L.H == A + True + + See Also + ======== + + sympy.matrices.dense.DenseMatrix.cholesky + sympy.matrices.matrixbase.MatrixBase.LUdecomposition + QRdecomposition + """ + + from .dense import MutableDenseMatrix + + if not M.is_square: + raise NonSquareMatrixError("Matrix must be square.") + if hermitian and not M.is_hermitian: + raise ValueError("Matrix must be Hermitian.") + if not hermitian and not M.is_symmetric(): + raise ValueError("Matrix must be symmetric.") + + D = MutableDenseMatrix.zeros(M.rows, M.rows) + L = MutableDenseMatrix.eye(M.rows) + + if hermitian: + for i in range(M.rows): + for j in range(i): + L[i, j] = (1 / D[j, j])*(M[i, j] - sum( + L[i, k]*L[j, k].conjugate()*D[k, k] for k in range(j))) + + D[i, i] = (M[i, i] - + sum(L[i, k]*L[i, k].conjugate()*D[k, k] for k in range(i))) + + if D[i, i].is_positive is False: + raise NonPositiveDefiniteMatrixError( + "Matrix must be positive-definite") + + else: + for i in range(M.rows): + for j in range(i): + L[i, j] = (1 / D[j, j])*(M[i, j] - sum( + L[i, k]*L[j, k]*D[k, k] for k in range(j))) + + D[i, i] = M[i, i] - sum(L[i, k]**2*D[k, k] for k in range(i)) + + return M._new(L), M._new(D) + +def _LDLdecomposition_sparse(M, hermitian=True): + """ + Returns the LDL Decomposition (matrices ``L`` and ``D``) of matrix + ``A``, such that ``L * D * L.T == A``. ``A`` must be a square, + symmetric, positive-definite and non-singular. + + This method eliminates the use of square root and ensures that all + the diagonal entries of L are 1. + + Examples + ======== + + >>> from sympy import SparseMatrix + >>> A = SparseMatrix(((25, 15, -5), (15, 18, 0), (-5, 0, 11))) + >>> L, D = A.LDLdecomposition() + >>> L + Matrix([ + [ 1, 0, 0], + [ 3/5, 1, 0], + [-1/5, 1/3, 1]]) + >>> D + Matrix([ + [25, 0, 0], + [ 0, 9, 0], + [ 0, 0, 9]]) + >>> L * D * L.T == A + True + + """ + + from .dense import MutableDenseMatrix + + if not M.is_square: + raise NonSquareMatrixError("Matrix must be square.") + if hermitian and not M.is_hermitian: + raise ValueError("Matrix must be Hermitian.") + if not hermitian and not M.is_symmetric(): + raise ValueError("Matrix must be symmetric.") + + dps = _get_intermediate_simp(expand_mul, expand_mul) + Lrowstruc = M.row_structure_symbolic_cholesky() + L = MutableDenseMatrix.eye(M.rows) + D = MutableDenseMatrix.zeros(M.rows, M.cols) + + for i in range(len(Lrowstruc)): + for j in Lrowstruc[i]: + if i != j: + L[i, j] = M[i, j] + summ = 0 + + for p1 in Lrowstruc[i]: + if p1 < j: + for p2 in Lrowstruc[j]: + if p2 < j: + if p1 == p2: + if hermitian: + summ += L[i, p1]*L[j, p1].conjugate()*D[p1, p1] + else: + summ += L[i, p1]*L[j, p1]*D[p1, p1] + else: + break + else: + break + + L[i, j] = dps((L[i, j] - summ) / D[j, j]) + + else: # i == j + D[i, i] = M[i, i] + summ = 0 + + for k in Lrowstruc[i]: + if k < i: + if hermitian: + summ += L[i, k]*L[i, k].conjugate()*D[k, k] + else: + summ += L[i, k]**2*D[k, k] + else: + break + + D[i, i] = dps(D[i, i] - summ) + + if hermitian and D[i, i].is_positive is False: + raise NonPositiveDefiniteMatrixError( + "Matrix must be positive-definite") + + return M._new(L), M._new(D) + + +def _LUdecomposition(M, iszerofunc=_iszero, simpfunc=None, rankcheck=False): + """Returns (L, U, perm) where L is a lower triangular matrix with unit + diagonal, U is an upper triangular matrix, and perm is a list of row + swap index pairs. If A is the original matrix, then + ``A = (L*U).permuteBkwd(perm)``, and the row permutation matrix P such + that $P A = L U$ can be computed by ``P = eye(A.rows).permuteFwd(perm)``. + + See documentation for LUCombined for details about the keyword argument + rankcheck, iszerofunc, and simpfunc. + + Parameters + ========== + + rankcheck : bool, optional + Determines if this function should detect the rank + deficiency of the matrixis and should raise a + ``ValueError``. + + iszerofunc : function, optional + A function which determines if a given expression is zero. + + The function should be a callable that takes a single + SymPy expression and returns a 3-valued boolean value + ``True``, ``False``, or ``None``. + + It is internally used by the pivot searching algorithm. + See the notes section for a more information about the + pivot searching algorithm. + + simpfunc : function or None, optional + A function that simplifies the input. + + If this is specified as a function, this function should be + a callable that takes a single SymPy expression and returns + an another SymPy expression that is algebraically + equivalent. + + If ``None``, it indicates that the pivot search algorithm + should not attempt to simplify any candidate pivots. + + It is internally used by the pivot searching algorithm. + See the notes section for a more information about the + pivot searching algorithm. + + Examples + ======== + + >>> from sympy import Matrix + >>> a = Matrix([[4, 3], [6, 3]]) + >>> L, U, _ = a.LUdecomposition() + >>> L + Matrix([ + [ 1, 0], + [3/2, 1]]) + >>> U + Matrix([ + [4, 3], + [0, -3/2]]) + + See Also + ======== + + sympy.matrices.dense.DenseMatrix.cholesky + sympy.matrices.dense.DenseMatrix.LDLdecomposition + QRdecomposition + LUdecomposition_Simple + LUdecompositionFF + LUsolve + """ + + combined, p = M.LUdecomposition_Simple(iszerofunc=iszerofunc, + simpfunc=simpfunc, rankcheck=rankcheck) + + # L is lower triangular ``M.rows x M.rows`` + # U is upper triangular ``M.rows x M.cols`` + # L has unit diagonal. For each column in combined, the subcolumn + # below the diagonal of combined is shared by L. + # If L has more columns than combined, then the remaining subcolumns + # below the diagonal of L are zero. + # The upper triangular portion of L and combined are equal. + def entry_L(i, j): + if i < j: + # Super diagonal entry + return M.zero + elif i == j: + return M.one + elif j < combined.cols: + return combined[i, j] + + # Subdiagonal entry of L with no corresponding + # entry in combined + return M.zero + + def entry_U(i, j): + return M.zero if i > j else combined[i, j] + + L = M._new(combined.rows, combined.rows, entry_L) + U = M._new(combined.rows, combined.cols, entry_U) + + return L, U, p + +def _LUdecomposition_Simple(M, iszerofunc=_iszero, simpfunc=None, + rankcheck=False): + r"""Compute the PLU decomposition of the matrix. + + Parameters + ========== + + rankcheck : bool, optional + Determines if this function should detect the rank + deficiency of the matrixis and should raise a + ``ValueError``. + + iszerofunc : function, optional + A function which determines if a given expression is zero. + + The function should be a callable that takes a single + SymPy expression and returns a 3-valued boolean value + ``True``, ``False``, or ``None``. + + It is internally used by the pivot searching algorithm. + See the notes section for a more information about the + pivot searching algorithm. + + simpfunc : function or None, optional + A function that simplifies the input. + + If this is specified as a function, this function should be + a callable that takes a single SymPy expression and returns + an another SymPy expression that is algebraically + equivalent. + + If ``None``, it indicates that the pivot search algorithm + should not attempt to simplify any candidate pivots. + + It is internally used by the pivot searching algorithm. + See the notes section for a more information about the + pivot searching algorithm. + + Returns + ======= + + (lu, row_swaps) : (Matrix, list) + If the original matrix is a $m, n$ matrix: + + *lu* is a $m, n$ matrix, which contains result of the + decomposition in a compressed form. See the notes section + to see how the matrix is compressed. + + *row_swaps* is a $m$-element list where each element is a + pair of row exchange indices. + + ``A = (L*U).permute_backward(perm)``, and the row + permutation matrix $P$ from the formula $P A = L U$ can be + computed by ``P=eye(A.row).permute_forward(perm)``. + + Raises + ====== + + ValueError + Raised if ``rankcheck=True`` and the matrix is found to + be rank deficient during the computation. + + Notes + ===== + + About the PLU decomposition: + + PLU decomposition is a generalization of a LU decomposition + which can be extended for rank-deficient matrices. + + It can further be generalized for non-square matrices, and this + is the notation that SymPy is using. + + PLU decomposition is a decomposition of a $m, n$ matrix $A$ in + the form of $P A = L U$ where + + * $L$ is a $m, m$ lower triangular matrix with unit diagonal + entries. + * $U$ is a $m, n$ upper triangular matrix. + * $P$ is a $m, m$ permutation matrix. + + So, for a square matrix, the decomposition would look like: + + .. math:: + L = \begin{bmatrix} + 1 & 0 & 0 & \cdots & 0 \\ + L_{1, 0} & 1 & 0 & \cdots & 0 \\ + L_{2, 0} & L_{2, 1} & 1 & \cdots & 0 \\ + \vdots & \vdots & \vdots & \ddots & \vdots \\ + L_{n-1, 0} & L_{n-1, 1} & L_{n-1, 2} & \cdots & 1 + \end{bmatrix} + + .. math:: + U = \begin{bmatrix} + U_{0, 0} & U_{0, 1} & U_{0, 2} & \cdots & U_{0, n-1} \\ + 0 & U_{1, 1} & U_{1, 2} & \cdots & U_{1, n-1} \\ + 0 & 0 & U_{2, 2} & \cdots & U_{2, n-1} \\ + \vdots & \vdots & \vdots & \ddots & \vdots \\ + 0 & 0 & 0 & \cdots & U_{n-1, n-1} + \end{bmatrix} + + And for a matrix with more rows than the columns, + the decomposition would look like: + + .. math:: + L = \begin{bmatrix} + 1 & 0 & 0 & \cdots & 0 & 0 & \cdots & 0 \\ + L_{1, 0} & 1 & 0 & \cdots & 0 & 0 & \cdots & 0 \\ + L_{2, 0} & L_{2, 1} & 1 & \cdots & 0 & 0 & \cdots & 0 \\ + \vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \ddots + & \vdots \\ + L_{n-1, 0} & L_{n-1, 1} & L_{n-1, 2} & \cdots & 1 & 0 + & \cdots & 0 \\ + L_{n, 0} & L_{n, 1} & L_{n, 2} & \cdots & L_{n, n-1} & 1 + & \cdots & 0 \\ + \vdots & \vdots & \vdots & \ddots & \vdots & \vdots + & \ddots & \vdots \\ + L_{m-1, 0} & L_{m-1, 1} & L_{m-1, 2} & \cdots & L_{m-1, n-1} + & 0 & \cdots & 1 \\ + \end{bmatrix} + + .. math:: + U = \begin{bmatrix} + U_{0, 0} & U_{0, 1} & U_{0, 2} & \cdots & U_{0, n-1} \\ + 0 & U_{1, 1} & U_{1, 2} & \cdots & U_{1, n-1} \\ + 0 & 0 & U_{2, 2} & \cdots & U_{2, n-1} \\ + \vdots & \vdots & \vdots & \ddots & \vdots \\ + 0 & 0 & 0 & \cdots & U_{n-1, n-1} \\ + 0 & 0 & 0 & \cdots & 0 \\ + \vdots & \vdots & \vdots & \ddots & \vdots \\ + 0 & 0 & 0 & \cdots & 0 + \end{bmatrix} + + Finally, for a matrix with more columns than the rows, the + decomposition would look like: + + .. math:: + L = \begin{bmatrix} + 1 & 0 & 0 & \cdots & 0 \\ + L_{1, 0} & 1 & 0 & \cdots & 0 \\ + L_{2, 0} & L_{2, 1} & 1 & \cdots & 0 \\ + \vdots & \vdots & \vdots & \ddots & \vdots \\ + L_{m-1, 0} & L_{m-1, 1} & L_{m-1, 2} & \cdots & 1 + \end{bmatrix} + + .. math:: + U = \begin{bmatrix} + U_{0, 0} & U_{0, 1} & U_{0, 2} & \cdots & U_{0, m-1} + & \cdots & U_{0, n-1} \\ + 0 & U_{1, 1} & U_{1, 2} & \cdots & U_{1, m-1} + & \cdots & U_{1, n-1} \\ + 0 & 0 & U_{2, 2} & \cdots & U_{2, m-1} + & \cdots & U_{2, n-1} \\ + \vdots & \vdots & \vdots & \ddots & \vdots + & \cdots & \vdots \\ + 0 & 0 & 0 & \cdots & U_{m-1, m-1} + & \cdots & U_{m-1, n-1} \\ + \end{bmatrix} + + About the compressed LU storage: + + The results of the decomposition are often stored in compressed + forms rather than returning $L$ and $U$ matrices individually. + + It may be less intiuitive, but it is commonly used for a lot of + numeric libraries because of the efficiency. + + The storage matrix is defined as following for this specific + method: + + * The subdiagonal elements of $L$ are stored in the subdiagonal + portion of $LU$, that is $LU_{i, j} = L_{i, j}$ whenever + $i > j$. + * The elements on the diagonal of $L$ are all 1, and are not + explicitly stored. + * $U$ is stored in the upper triangular portion of $LU$, that is + $LU_{i, j} = U_{i, j}$ whenever $i <= j$. + * For a case of $m > n$, the right side of the $L$ matrix is + trivial to store. + * For a case of $m < n$, the below side of the $U$ matrix is + trivial to store. + + So, for a square matrix, the compressed output matrix would be: + + .. math:: + LU = \begin{bmatrix} + U_{0, 0} & U_{0, 1} & U_{0, 2} & \cdots & U_{0, n-1} \\ + L_{1, 0} & U_{1, 1} & U_{1, 2} & \cdots & U_{1, n-1} \\ + L_{2, 0} & L_{2, 1} & U_{2, 2} & \cdots & U_{2, n-1} \\ + \vdots & \vdots & \vdots & \ddots & \vdots \\ + L_{n-1, 0} & L_{n-1, 1} & L_{n-1, 2} & \cdots & U_{n-1, n-1} + \end{bmatrix} + + For a matrix with more rows than the columns, the compressed + output matrix would be: + + .. math:: + LU = \begin{bmatrix} + U_{0, 0} & U_{0, 1} & U_{0, 2} & \cdots & U_{0, n-1} \\ + L_{1, 0} & U_{1, 1} & U_{1, 2} & \cdots & U_{1, n-1} \\ + L_{2, 0} & L_{2, 1} & U_{2, 2} & \cdots & U_{2, n-1} \\ + \vdots & \vdots & \vdots & \ddots & \vdots \\ + L_{n-1, 0} & L_{n-1, 1} & L_{n-1, 2} & \cdots + & U_{n-1, n-1} \\ + \vdots & \vdots & \vdots & \ddots & \vdots \\ + L_{m-1, 0} & L_{m-1, 1} & L_{m-1, 2} & \cdots + & L_{m-1, n-1} \\ + \end{bmatrix} + + For a matrix with more columns than the rows, the compressed + output matrix would be: + + .. math:: + LU = \begin{bmatrix} + U_{0, 0} & U_{0, 1} & U_{0, 2} & \cdots & U_{0, m-1} + & \cdots & U_{0, n-1} \\ + L_{1, 0} & U_{1, 1} & U_{1, 2} & \cdots & U_{1, m-1} + & \cdots & U_{1, n-1} \\ + L_{2, 0} & L_{2, 1} & U_{2, 2} & \cdots & U_{2, m-1} + & \cdots & U_{2, n-1} \\ + \vdots & \vdots & \vdots & \ddots & \vdots + & \cdots & \vdots \\ + L_{m-1, 0} & L_{m-1, 1} & L_{m-1, 2} & \cdots & U_{m-1, m-1} + & \cdots & U_{m-1, n-1} \\ + \end{bmatrix} + + About the pivot searching algorithm: + + When a matrix contains symbolic entries, the pivot search algorithm + differs from the case where every entry can be categorized as zero or + nonzero. + The algorithm searches column by column through the submatrix whose + top left entry coincides with the pivot position. + If it exists, the pivot is the first entry in the current search + column that iszerofunc guarantees is nonzero. + If no such candidate exists, then each candidate pivot is simplified + if simpfunc is not None. + The search is repeated, with the difference that a candidate may be + the pivot if ``iszerofunc()`` cannot guarantee that it is nonzero. + In the second search the pivot is the first candidate that + iszerofunc can guarantee is nonzero. + If no such candidate exists, then the pivot is the first candidate + for which iszerofunc returns None. + If no such candidate exists, then the search is repeated in the next + column to the right. + The pivot search algorithm differs from the one in ``rref()``, which + relies on ``_find_reasonable_pivot()``. + Future versions of ``LUdecomposition_simple()`` may use + ``_find_reasonable_pivot()``. + + See Also + ======== + + sympy.matrices.matrixbase.MatrixBase.LUdecomposition + LUdecompositionFF + LUsolve + """ + + if rankcheck: + # https://github.com/sympy/sympy/issues/9796 + pass + + if S.Zero in M.shape: + # Define LU decomposition of a matrix with no entries as a matrix + # of the same dimensions with all zero entries. + return M.zeros(M.rows, M.cols), [] + + dps = _get_intermediate_simp() + lu = M.as_mutable() + row_swaps = [] + + pivot_col = 0 + + for pivot_row in range(0, lu.rows - 1): + # Search for pivot. Prefer entry that iszeropivot determines + # is nonzero, over entry that iszeropivot cannot guarantee + # is zero. + # XXX ``_find_reasonable_pivot`` uses slow zero testing. Blocked by bug #10279 + # Future versions of LUdecomposition_simple can pass iszerofunc and simpfunc + # to _find_reasonable_pivot(). + # In pass 3 of _find_reasonable_pivot(), the predicate in ``if x.equals(S.Zero):`` + # calls sympy.simplify(), and not the simplification function passed in via + # the keyword argument simpfunc. + iszeropivot = True + + while pivot_col != M.cols and iszeropivot: + sub_col = (lu[r, pivot_col] for r in range(pivot_row, M.rows)) + + pivot_row_offset, pivot_value, is_assumed_non_zero, ind_simplified_pairs =\ + _find_reasonable_pivot_naive(sub_col, iszerofunc, simpfunc) + + iszeropivot = pivot_value is None + + if iszeropivot: + # All candidate pivots in this column are zero. + # Proceed to next column. + pivot_col += 1 + + if rankcheck and pivot_col != pivot_row: + # All entries including and below the pivot position are + # zero, which indicates that the rank of the matrix is + # strictly less than min(num rows, num cols) + # Mimic behavior of previous implementation, by throwing a + # ValueError. + raise ValueError("Rank of matrix is strictly less than" + " number of rows or columns." + " Pass keyword argument" + " rankcheck=False to compute" + " the LU decomposition of this matrix.") + + candidate_pivot_row = None if pivot_row_offset is None else pivot_row + pivot_row_offset + + if candidate_pivot_row is None and iszeropivot: + # If candidate_pivot_row is None and iszeropivot is True + # after pivot search has completed, then the submatrix + # below and to the right of (pivot_row, pivot_col) is + # all zeros, indicating that Gaussian elimination is + # complete. + return lu, row_swaps + + # Update entries simplified during pivot search. + for offset, val in ind_simplified_pairs: + lu[pivot_row + offset, pivot_col] = val + + if pivot_row != candidate_pivot_row: + # Row swap book keeping: + # Record which rows were swapped. + # Update stored portion of L factor by multiplying L on the + # left and right with the current permutation. + # Swap rows of U. + row_swaps.append([pivot_row, candidate_pivot_row]) + + # Update L. + lu[pivot_row, 0:pivot_row], lu[candidate_pivot_row, 0:pivot_row] = \ + lu[candidate_pivot_row, 0:pivot_row], lu[pivot_row, 0:pivot_row] + + # Swap pivot row of U with candidate pivot row. + lu[pivot_row, pivot_col:lu.cols], lu[candidate_pivot_row, pivot_col:lu.cols] = \ + lu[candidate_pivot_row, pivot_col:lu.cols], lu[pivot_row, pivot_col:lu.cols] + + # Introduce zeros below the pivot by adding a multiple of the + # pivot row to a row under it, and store the result in the + # row under it. + # Only entries in the target row whose index is greater than + # start_col may be nonzero. + start_col = pivot_col + 1 + + for row in range(pivot_row + 1, lu.rows): + # Store factors of L in the subcolumn below + # (pivot_row, pivot_row). + lu[row, pivot_row] = \ + dps(lu[row, pivot_col]/lu[pivot_row, pivot_col]) + + # Form the linear combination of the pivot row and the current + # row below the pivot row that zeros the entries below the pivot. + # Employing slicing instead of a loop here raises + # NotImplementedError: Cannot add Zero to MutableSparseMatrix + # in sympy/matrices/tests/test_sparse.py. + # c = pivot_row + 1 if pivot_row == pivot_col else pivot_col + for c in range(start_col, lu.cols): + lu[row, c] = dps(lu[row, c] - lu[row, pivot_row]*lu[pivot_row, c]) + + if pivot_row != pivot_col: + # matrix rank < min(num rows, num cols), + # so factors of L are not stored directly below the pivot. + # These entries are zero by construction, so don't bother + # computing them. + for row in range(pivot_row + 1, lu.rows): + lu[row, pivot_col] = M.zero + + pivot_col += 1 + + if pivot_col == lu.cols: + # All candidate pivots are zero implies that Gaussian + # elimination is complete. + return lu, row_swaps + + if rankcheck: + if iszerofunc( + lu[Min(lu.rows, lu.cols) - 1, Min(lu.rows, lu.cols) - 1]): + raise ValueError("Rank of matrix is strictly less than" + " number of rows or columns." + " Pass keyword argument" + " rankcheck=False to compute" + " the LU decomposition of this matrix.") + + return lu, row_swaps + +def _LUdecompositionFF(M): + """Compute a fraction-free LU decomposition. + + Returns 4 matrices P, L, D, U such that PA = L D**-1 U. + If the elements of the matrix belong to some integral domain I, then all + elements of L, D and U are guaranteed to belong to I. + + See Also + ======== + + sympy.matrices.matrixbase.MatrixBase.LUdecomposition + LUdecomposition_Simple + LUsolve + + References + ========== + + .. [1] W. Zhou & D.J. Jeffrey, "Fraction-free matrix factors: new forms + for LU and QR factors". Frontiers in Computer Science in China, + Vol 2, no. 1, pp. 67-80, 2008. + """ + + from sympy.matrices import SparseMatrix + + zeros = SparseMatrix.zeros + eye = SparseMatrix.eye + n, m = M.rows, M.cols + U, L, P = M.as_mutable(), eye(n), eye(n) + DD = zeros(n, n) + oldpivot = 1 + + for k in range(n - 1): + if U[k, k] == 0: + for kpivot in range(k + 1, n): + if U[kpivot, k]: + break + else: + raise ValueError("Matrix is not full rank") + + U[k, k:], U[kpivot, k:] = U[kpivot, k:], U[k, k:] + L[k, :k], L[kpivot, :k] = L[kpivot, :k], L[k, :k] + P[k, :], P[kpivot, :] = P[kpivot, :], P[k, :] + + L [k, k] = Ukk = U[k, k] + DD[k, k] = oldpivot * Ukk + + for i in range(k + 1, n): + L[i, k] = Uik = U[i, k] + + for j in range(k + 1, m): + U[i, j] = (Ukk * U[i, j] - U[k, j] * Uik) / oldpivot + + U[i, k] = 0 + + oldpivot = Ukk + + DD[n - 1, n - 1] = oldpivot + + return P, L, DD, U + +def _singular_value_decomposition(A): + r"""Returns a Condensed Singular Value decomposition. + + Explanation + =========== + + A Singular Value decomposition is a decomposition in the form $A = U \Sigma V^H$ + where + + - $U, V$ are column orthogonal matrix. + - $\Sigma$ is a diagonal matrix, where the main diagonal contains singular + values of matrix A. + + A column orthogonal matrix satisfies + $\mathbb{I} = U^H U$ while a full orthogonal matrix satisfies + relation $\mathbb{I} = U U^H = U^H U$ where $\mathbb{I}$ is an identity + matrix with matching dimensions. + + For matrices which are not square or are rank-deficient, it is + sufficient to return a column orthogonal matrix because augmenting + them may introduce redundant computations. + In condensed Singular Value Decomposition we only return column orthogonal + matrices because of this reason + + If you want to augment the results to return a full orthogonal + decomposition, you should use the following procedures. + + - Augment the $U, V$ matrices with columns that are orthogonal to every + other columns and make it square. + - Augment the $\Sigma$ matrix with zero rows to make it have the same + shape as the original matrix. + + The procedure will be illustrated in the examples section. + + Examples + ======== + + we take a full rank matrix first: + + >>> from sympy import Matrix + >>> A = Matrix([[1, 2],[2,1]]) + >>> U, S, V = A.singular_value_decomposition() + >>> U + Matrix([ + [ sqrt(2)/2, sqrt(2)/2], + [-sqrt(2)/2, sqrt(2)/2]]) + >>> S + Matrix([ + [1, 0], + [0, 3]]) + >>> V + Matrix([ + [-sqrt(2)/2, sqrt(2)/2], + [ sqrt(2)/2, sqrt(2)/2]]) + + If a matrix if square and full rank both U, V + are orthogonal in both directions + + >>> U * U.H + Matrix([ + [1, 0], + [0, 1]]) + >>> U.H * U + Matrix([ + [1, 0], + [0, 1]]) + + >>> V * V.H + Matrix([ + [1, 0], + [0, 1]]) + >>> V.H * V + Matrix([ + [1, 0], + [0, 1]]) + >>> A == U * S * V.H + True + + >>> C = Matrix([ + ... [1, 0, 0, 0, 2], + ... [0, 0, 3, 0, 0], + ... [0, 0, 0, 0, 0], + ... [0, 2, 0, 0, 0], + ... ]) + >>> U, S, V = C.singular_value_decomposition() + + >>> V.H * V + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + >>> V * V.H + Matrix([ + [1/5, 0, 0, 0, 2/5], + [ 0, 1, 0, 0, 0], + [ 0, 0, 1, 0, 0], + [ 0, 0, 0, 0, 0], + [2/5, 0, 0, 0, 4/5]]) + + If you want to augment the results to be a full orthogonal + decomposition, you should augment $V$ with an another orthogonal + column. + + You are able to append an arbitrary standard basis that are linearly + independent to every other columns and you can run the Gram-Schmidt + process to make them augmented as orthogonal basis. + + >>> V_aug = V.row_join(Matrix([[0,0,0,0,1], + ... [0,0,0,1,0]]).H) + >>> V_aug = V_aug.QRdecomposition()[0] + >>> V_aug + Matrix([ + [0, sqrt(5)/5, 0, -2*sqrt(5)/5, 0], + [1, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 1], + [0, 2*sqrt(5)/5, 0, sqrt(5)/5, 0]]) + >>> V_aug.H * V_aug + Matrix([ + [1, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1]]) + >>> V_aug * V_aug.H + Matrix([ + [1, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1]]) + + Similarly we augment U + + >>> U_aug = U.row_join(Matrix([0,0,1,0])) + >>> U_aug = U_aug.QRdecomposition()[0] + >>> U_aug + Matrix([ + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + [1, 0, 0, 0]]) + + >>> U_aug.H * U_aug + Matrix([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]]) + >>> U_aug * U_aug.H + Matrix([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]]) + + We add 2 zero columns and one row to S + + >>> S_aug = S.col_join(Matrix([[0,0,0]])) + >>> S_aug = S_aug.row_join(Matrix([[0,0,0,0], + ... [0,0,0,0]]).H) + >>> S_aug + Matrix([ + [2, 0, 0, 0, 0], + [0, sqrt(5), 0, 0, 0], + [0, 0, 3, 0, 0], + [0, 0, 0, 0, 0]]) + + + + >>> U_aug * S_aug * V_aug.H == C + True + + """ + + AH = A.H + m, n = A.shape + if m >= n: + V, S = (AH * A).diagonalize() + + ranked = [] + for i, x in enumerate(S.diagonal()): + if not x.is_zero: + ranked.append(i) + + V = V[:, ranked] + + Singular_vals = [sqrt(S[i, i]) for i in range(S.rows) if i in ranked] + + S = S.diag(*Singular_vals) + V, _ = V.QRdecomposition() + U = A * V * S.inv() + else: + U, S = (A * AH).diagonalize() + + ranked = [] + for i, x in enumerate(S.diagonal()): + if not x.is_zero: + ranked.append(i) + + U = U[:, ranked] + Singular_vals = [sqrt(S[i, i]) for i in range(S.rows) if i in ranked] + + S = S.diag(*Singular_vals) + U, _ = U.QRdecomposition() + V = AH * U * S.inv() + + return U, S, V + +def _QRdecomposition_optional(M, normalize=True): + def dot(u, v): + return u.dot(v, hermitian=True) + + dps = _get_intermediate_simp(expand_mul, expand_mul) + + A = M.as_mutable() + ranked = [] + + Q = A + R = A.zeros(A.cols) + + for j in range(A.cols): + for i in range(j): + if Q[:, i].is_zero_matrix: + continue + + R[i, j] = dot(Q[:, i], Q[:, j]) / dot(Q[:, i], Q[:, i]) + R[i, j] = dps(R[i, j]) + Q[:, j] -= Q[:, i] * R[i, j] + + Q[:, j] = dps(Q[:, j]) + if Q[:, j].is_zero_matrix is not True: + ranked.append(j) + R[j, j] = M.one + + Q = Q.extract(range(Q.rows), ranked) + R = R.extract(ranked, range(R.cols)) + + if normalize: + # Normalization + for i in range(Q.cols): + norm = Q[:, i].norm() + Q[:, i] /= norm + R[i, :] *= norm + + return M.__class__(Q), M.__class__(R) + + +def _QRdecomposition(M): + r"""Returns a QR decomposition. + + Explanation + =========== + + A QR decomposition is a decomposition in the form $A = Q R$ + where + + - $Q$ is a column orthogonal matrix. + - $R$ is a upper triangular (trapezoidal) matrix. + + A column orthogonal matrix satisfies + $\mathbb{I} = Q^H Q$ while a full orthogonal matrix satisfies + relation $\mathbb{I} = Q Q^H = Q^H Q$ where $I$ is an identity + matrix with matching dimensions. + + For matrices which are not square or are rank-deficient, it is + sufficient to return a column orthogonal matrix because augmenting + them may introduce redundant computations. + And an another advantage of this is that you can easily inspect the + matrix rank by counting the number of columns of $Q$. + + If you want to augment the results to return a full orthogonal + decomposition, you should use the following procedures. + + - Augment the $Q$ matrix with columns that are orthogonal to every + other columns and make it square. + - Augment the $R$ matrix with zero rows to make it have the same + shape as the original matrix. + + The procedure will be illustrated in the examples section. + + Examples + ======== + + A full rank matrix example: + + >>> from sympy import Matrix + >>> A = Matrix([[12, -51, 4], [6, 167, -68], [-4, 24, -41]]) + >>> Q, R = A.QRdecomposition() + >>> Q + Matrix([ + [ 6/7, -69/175, -58/175], + [ 3/7, 158/175, 6/175], + [-2/7, 6/35, -33/35]]) + >>> R + Matrix([ + [14, 21, -14], + [ 0, 175, -70], + [ 0, 0, 35]]) + + If the matrix is square and full rank, the $Q$ matrix becomes + orthogonal in both directions, and needs no augmentation. + + >>> Q * Q.H + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + >>> Q.H * Q + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + + >>> A == Q*R + True + + A rank deficient matrix example: + + >>> A = Matrix([[12, -51, 0], [6, 167, 0], [-4, 24, 0]]) + >>> Q, R = A.QRdecomposition() + >>> Q + Matrix([ + [ 6/7, -69/175], + [ 3/7, 158/175], + [-2/7, 6/35]]) + >>> R + Matrix([ + [14, 21, 0], + [ 0, 175, 0]]) + + QRdecomposition might return a matrix Q that is rectangular. + In this case the orthogonality condition might be satisfied as + $\mathbb{I} = Q.H*Q$ but not in the reversed product + $\mathbb{I} = Q * Q.H$. + + >>> Q.H * Q + Matrix([ + [1, 0], + [0, 1]]) + >>> Q * Q.H + Matrix([ + [27261/30625, 348/30625, -1914/6125], + [ 348/30625, 30589/30625, 198/6125], + [ -1914/6125, 198/6125, 136/1225]]) + + If you want to augment the results to be a full orthogonal + decomposition, you should augment $Q$ with an another orthogonal + column. + + You are able to append an identity matrix, + and you can run the Gram-Schmidt + process to make them augmented as orthogonal basis. + + >>> Q_aug = Q.row_join(Matrix.eye(3)) + >>> Q_aug = Q_aug.QRdecomposition()[0] + >>> Q_aug + Matrix([ + [ 6/7, -69/175, 58/175], + [ 3/7, 158/175, -6/175], + [-2/7, 6/35, 33/35]]) + >>> Q_aug.H * Q_aug + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + >>> Q_aug * Q_aug.H + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + + Augmenting the $R$ matrix with zero row is straightforward. + + >>> R_aug = R.col_join(Matrix([[0, 0, 0]])) + >>> R_aug + Matrix([ + [14, 21, 0], + [ 0, 175, 0], + [ 0, 0, 0]]) + >>> Q_aug * R_aug == A + True + + A zero matrix example: + + >>> from sympy import Matrix + >>> A = Matrix.zeros(3, 4) + >>> Q, R = A.QRdecomposition() + + They may return matrices with zero rows and columns. + + >>> Q + Matrix(3, 0, []) + >>> R + Matrix(0, 4, []) + >>> Q*R + Matrix([ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]]) + + As the same augmentation rule described above, $Q$ can be augmented + with columns of an identity matrix and $R$ can be augmented with + rows of a zero matrix. + + >>> Q_aug = Q.row_join(Matrix.eye(3)) + >>> R_aug = R.col_join(Matrix.zeros(3, 4)) + >>> Q_aug * Q_aug.T + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + >>> R_aug + Matrix([ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]]) + >>> Q_aug * R_aug == A + True + + See Also + ======== + + sympy.matrices.dense.DenseMatrix.cholesky + sympy.matrices.dense.DenseMatrix.LDLdecomposition + sympy.matrices.matrixbase.MatrixBase.LUdecomposition + QRsolve + """ + return _QRdecomposition_optional(M, normalize=True) + +def _upper_hessenberg_decomposition(A): + """Converts a matrix into Hessenberg matrix H. + + Returns 2 matrices H, P s.t. + $P H P^{T} = A$, where H is an upper hessenberg matrix + and P is an orthogonal matrix + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([ + ... [1,2,3], + ... [-3,5,6], + ... [4,-8,9], + ... ]) + >>> H, P = A.upper_hessenberg_decomposition() + >>> H + Matrix([ + [1, 6/5, 17/5], + [5, 213/25, -134/25], + [0, 216/25, 137/25]]) + >>> P + Matrix([ + [1, 0, 0], + [0, -3/5, 4/5], + [0, 4/5, 3/5]]) + >>> P * H * P.H == A + True + + + References + ========== + + .. [#] https://mathworld.wolfram.com/HessenbergDecomposition.html + """ + + M = A.as_mutable() + + if not M.is_square: + raise NonSquareMatrixError("Matrix must be square.") + + n = M.cols + P = M.eye(n) + H = M + + for j in range(n - 2): + + u = H[j + 1:, j] + + if u[1:, :].is_zero_matrix: + continue + + if sign(u[0]) != 0: + u[0] = u[0] + sign(u[0]) * u.norm() + else: + u[0] = u[0] + u.norm() + + v = u / u.norm() + + H[j + 1:, :] = H[j + 1:, :] - 2 * v * (v.H * H[j + 1:, :]) + H[:, j + 1:] = H[:, j + 1:] - (H[:, j + 1:] * (2 * v)) * v.H + P[:, j + 1:] = P[:, j + 1:] - (P[:, j + 1:] * (2 * v)) * v.H + + return H, P diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/dense.py b/venv/lib/python3.11/site-packages/sympy/matrices/dense.py new file mode 100644 index 0000000000000000000000000000000000000000..98bf9931df54f67abfd9c4dc810b46fdcf70288f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/dense.py @@ -0,0 +1,1094 @@ +from __future__ import annotations +import random + +from sympy.core.basic import Basic +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.core.sympify import sympify +from sympy.functions.elementary.trigonometric import cos, sin +from sympy.utilities.decorator import doctest_depends_on +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.utilities.iterables import is_sequence + +from .exceptions import ShapeError +from .decompositions import _cholesky, _LDLdecomposition +from .matrixbase import MatrixBase +from .repmatrix import MutableRepMatrix, RepMatrix +from .solvers import _lower_triangular_solve, _upper_triangular_solve + + +__doctest_requires__ = {('symarray',): ['numpy']} + + +def _iszero(x): + """Returns True if x is zero.""" + return x.is_zero + + +class DenseMatrix(RepMatrix): + """Matrix implementation based on DomainMatrix as the internal representation""" + + # + # DenseMatrix is a superclass for both MutableDenseMatrix and + # ImmutableDenseMatrix. Methods shared by both classes but not for the + # Sparse classes should be implemented here. + # + + is_MatrixExpr: bool = False + + _op_priority = 10.01 + _class_priority = 4 + + @property + def _mat(self): + sympy_deprecation_warning( + """ + The private _mat attribute of Matrix is deprecated. Use the + .flat() method instead. + """, + deprecated_since_version="1.9", + active_deprecations_target="deprecated-private-matrix-attributes" + ) + + return self.flat() + + def _eval_inverse(self, **kwargs): + return self.inv(method=kwargs.get('method', 'GE'), + iszerofunc=kwargs.get('iszerofunc', _iszero), + try_block_diag=kwargs.get('try_block_diag', False)) + + def as_immutable(self): + """Returns an Immutable version of this Matrix + """ + from .immutable import ImmutableDenseMatrix as cls + return cls._fromrep(self._rep.copy()) + + def as_mutable(self): + """Returns a mutable version of this matrix + + Examples + ======== + + >>> from sympy import ImmutableMatrix + >>> X = ImmutableMatrix([[1, 2], [3, 4]]) + >>> Y = X.as_mutable() + >>> Y[1, 1] = 5 # Can set values in Y + >>> Y + Matrix([ + [1, 2], + [3, 5]]) + """ + return Matrix(self) + + def cholesky(self, hermitian=True): + return _cholesky(self, hermitian=hermitian) + + def LDLdecomposition(self, hermitian=True): + return _LDLdecomposition(self, hermitian=hermitian) + + def lower_triangular_solve(self, rhs): + return _lower_triangular_solve(self, rhs) + + def upper_triangular_solve(self, rhs): + return _upper_triangular_solve(self, rhs) + + cholesky.__doc__ = _cholesky.__doc__ + LDLdecomposition.__doc__ = _LDLdecomposition.__doc__ + lower_triangular_solve.__doc__ = _lower_triangular_solve.__doc__ + upper_triangular_solve.__doc__ = _upper_triangular_solve.__doc__ + + +def _force_mutable(x): + """Return a matrix as a Matrix, otherwise return x.""" + if getattr(x, 'is_Matrix', False): + return x.as_mutable() + elif isinstance(x, Basic): + return x + elif hasattr(x, '__array__'): + a = x.__array__() + if len(a.shape) == 0: + return sympify(a) + return Matrix(x) + return x + + +class MutableDenseMatrix(DenseMatrix, MutableRepMatrix): + + def simplify(self, **kwargs): + """Applies simplify to the elements of a matrix in place. + + This is a shortcut for M.applyfunc(lambda x: simplify(x, ratio, measure)) + + See Also + ======== + + sympy.simplify.simplify.simplify + """ + from sympy.simplify.simplify import simplify as _simplify + for (i, j), element in self.todok().items(): + self[i, j] = _simplify(element, **kwargs) + + +MutableMatrix = Matrix = MutableDenseMatrix + +########### +# Numpy Utility Functions: +# list2numpy, matrix2numpy, symmarray +########### + + +def list2numpy(l, dtype=object): # pragma: no cover + """Converts Python list of SymPy expressions to a NumPy array. + + See Also + ======== + + matrix2numpy + """ + from numpy import empty + a = empty(len(l), dtype) + for i, s in enumerate(l): + a[i] = s + return a + + +def matrix2numpy(m, dtype=object): # pragma: no cover + """Converts SymPy's matrix to a NumPy array. + + See Also + ======== + + list2numpy + """ + from numpy import empty + a = empty(m.shape, dtype) + for i in range(m.rows): + for j in range(m.cols): + a[i, j] = m[i, j] + return a + + +########### +# Rotation matrices: +# rot_givens, rot_axis[123], rot_ccw_axis[123] +########### + + +def rot_givens(i, j, theta, dim=3): + r"""Returns a a Givens rotation matrix, a a rotation in the + plane spanned by two coordinates axes. + + Explanation + =========== + + The Givens rotation corresponds to a generalization of rotation + matrices to any number of dimensions, given by: + + .. math:: + G(i, j, \theta) = + \begin{bmatrix} + 1 & \cdots & 0 & \cdots & 0 & \cdots & 0 \\ + \vdots & \ddots & \vdots & & \vdots & & \vdots \\ + 0 & \cdots & c & \cdots & -s & \cdots & 0 \\ + \vdots & & \vdots & \ddots & \vdots & & \vdots \\ + 0 & \cdots & s & \cdots & c & \cdots & 0 \\ + \vdots & & \vdots & & \vdots & \ddots & \vdots \\ + 0 & \cdots & 0 & \cdots & 0 & \cdots & 1 + \end{bmatrix} + + Where $c = \cos(\theta)$ and $s = \sin(\theta)$ appear at the intersections + ``i``\th and ``j``\th rows and columns. + + For fixed ``i > j``\, the non-zero elements of a Givens matrix are + given by: + + - $g_{kk} = 1$ for $k \ne i,\,j$ + - $g_{kk} = c$ for $k = i,\,j$ + - $g_{ji} = -g_{ij} = -s$ + + Parameters + ========== + + i : int between ``0`` and ``dim - 1`` + Represents first axis + j : int between ``0`` and ``dim - 1`` + Represents second axis + dim : int bigger than 1 + Number of dimensions. Defaults to 3. + + Examples + ======== + + >>> from sympy import pi, rot_givens + + A counterclockwise rotation of pi/3 (60 degrees) around + the third axis (z-axis): + + >>> rot_givens(1, 0, pi/3) + Matrix([ + [ 1/2, -sqrt(3)/2, 0], + [sqrt(3)/2, 1/2, 0], + [ 0, 0, 1]]) + + If we rotate by pi/2 (90 degrees): + + >>> rot_givens(1, 0, pi/2) + Matrix([ + [0, -1, 0], + [1, 0, 0], + [0, 0, 1]]) + + This can be generalized to any number + of dimensions: + + >>> rot_givens(1, 0, pi/2, dim=4) + Matrix([ + [0, -1, 0, 0], + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]]) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Givens_rotation + + See Also + ======== + + rot_axis1: Returns a rotation matrix for a rotation of theta (in radians) + about the 1-axis (clockwise around the x axis) + rot_axis2: Returns a rotation matrix for a rotation of theta (in radians) + about the 2-axis (clockwise around the y axis) + rot_axis3: Returns a rotation matrix for a rotation of theta (in radians) + about the 3-axis (clockwise around the z axis) + rot_ccw_axis1: Returns a rotation matrix for a rotation of theta (in radians) + about the 1-axis (counterclockwise around the x axis) + rot_ccw_axis2: Returns a rotation matrix for a rotation of theta (in radians) + about the 2-axis (counterclockwise around the y axis) + rot_ccw_axis3: Returns a rotation matrix for a rotation of theta (in radians) + about the 3-axis (counterclockwise around the z axis) + """ + if not isinstance(dim, int) or dim < 2: + raise ValueError('dim must be an integer biggen than one, ' + 'got {}.'.format(dim)) + + if i == j: + raise ValueError('i and j must be different, ' + 'got ({}, {})'.format(i, j)) + + for ij in [i, j]: + if not isinstance(ij, int) or ij < 0 or ij > dim - 1: + raise ValueError('i and j must be integers between 0 and ' + '{}, got i={} and j={}.'.format(dim-1, i, j)) + + theta = sympify(theta) + c = cos(theta) + s = sin(theta) + M = eye(dim) + M[i, i] = c + M[j, j] = c + M[i, j] = s + M[j, i] = -s + return M + + +def rot_axis3(theta): + r"""Returns a rotation matrix for a rotation of theta (in radians) + about the 3-axis. + + Explanation + =========== + + For a right-handed coordinate system, this corresponds to a + clockwise rotation around the `z`-axis, given by: + + .. math:: + + R = \begin{bmatrix} + \cos(\theta) & \sin(\theta) & 0 \\ + -\sin(\theta) & \cos(\theta) & 0 \\ + 0 & 0 & 1 + \end{bmatrix} + + Examples + ======== + + >>> from sympy import pi, rot_axis3 + + A rotation of pi/3 (60 degrees): + + >>> theta = pi/3 + >>> rot_axis3(theta) + Matrix([ + [ 1/2, sqrt(3)/2, 0], + [-sqrt(3)/2, 1/2, 0], + [ 0, 0, 1]]) + + If we rotate by pi/2 (90 degrees): + + >>> rot_axis3(pi/2) + Matrix([ + [ 0, 1, 0], + [-1, 0, 0], + [ 0, 0, 1]]) + + See Also + ======== + + rot_givens: Returns a Givens rotation matrix (generalized rotation for + any number of dimensions) + rot_ccw_axis3: Returns a rotation matrix for a rotation of theta (in radians) + about the 3-axis (counterclockwise around the z axis) + rot_axis1: Returns a rotation matrix for a rotation of theta (in radians) + about the 1-axis (clockwise around the x axis) + rot_axis2: Returns a rotation matrix for a rotation of theta (in radians) + about the 2-axis (clockwise around the y axis) + """ + return rot_givens(0, 1, theta, dim=3) + + +def rot_axis2(theta): + r"""Returns a rotation matrix for a rotation of theta (in radians) + about the 2-axis. + + Explanation + =========== + + For a right-handed coordinate system, this corresponds to a + clockwise rotation around the `y`-axis, given by: + + .. math:: + + R = \begin{bmatrix} + \cos(\theta) & 0 & -\sin(\theta) \\ + 0 & 1 & 0 \\ + \sin(\theta) & 0 & \cos(\theta) + \end{bmatrix} + + Examples + ======== + + >>> from sympy import pi, rot_axis2 + + A rotation of pi/3 (60 degrees): + + >>> theta = pi/3 + >>> rot_axis2(theta) + Matrix([ + [ 1/2, 0, -sqrt(3)/2], + [ 0, 1, 0], + [sqrt(3)/2, 0, 1/2]]) + + If we rotate by pi/2 (90 degrees): + + >>> rot_axis2(pi/2) + Matrix([ + [0, 0, -1], + [0, 1, 0], + [1, 0, 0]]) + + See Also + ======== + + rot_givens: Returns a Givens rotation matrix (generalized rotation for + any number of dimensions) + rot_ccw_axis2: Returns a rotation matrix for a rotation of theta (in radians) + about the 2-axis (clockwise around the y axis) + rot_axis1: Returns a rotation matrix for a rotation of theta (in radians) + about the 1-axis (counterclockwise around the x axis) + rot_axis3: Returns a rotation matrix for a rotation of theta (in radians) + about the 3-axis (counterclockwise around the z axis) + """ + return rot_givens(2, 0, theta, dim=3) + + +def rot_axis1(theta): + r"""Returns a rotation matrix for a rotation of theta (in radians) + about the 1-axis. + + Explanation + =========== + + For a right-handed coordinate system, this corresponds to a + clockwise rotation around the `x`-axis, given by: + + .. math:: + + R = \begin{bmatrix} + 1 & 0 & 0 \\ + 0 & \cos(\theta) & \sin(\theta) \\ + 0 & -\sin(\theta) & \cos(\theta) + \end{bmatrix} + + Examples + ======== + + >>> from sympy import pi, rot_axis1 + + A rotation of pi/3 (60 degrees): + + >>> theta = pi/3 + >>> rot_axis1(theta) + Matrix([ + [1, 0, 0], + [0, 1/2, sqrt(3)/2], + [0, -sqrt(3)/2, 1/2]]) + + If we rotate by pi/2 (90 degrees): + + >>> rot_axis1(pi/2) + Matrix([ + [1, 0, 0], + [0, 0, 1], + [0, -1, 0]]) + + See Also + ======== + + rot_givens: Returns a Givens rotation matrix (generalized rotation for + any number of dimensions) + rot_ccw_axis1: Returns a rotation matrix for a rotation of theta (in radians) + about the 1-axis (counterclockwise around the x axis) + rot_axis2: Returns a rotation matrix for a rotation of theta (in radians) + about the 2-axis (clockwise around the y axis) + rot_axis3: Returns a rotation matrix for a rotation of theta (in radians) + about the 3-axis (clockwise around the z axis) + """ + return rot_givens(1, 2, theta, dim=3) + + +def rot_ccw_axis3(theta): + r"""Returns a rotation matrix for a rotation of theta (in radians) + about the 3-axis. + + Explanation + =========== + + For a right-handed coordinate system, this corresponds to a + counterclockwise rotation around the `z`-axis, given by: + + .. math:: + + R = \begin{bmatrix} + \cos(\theta) & -\sin(\theta) & 0 \\ + \sin(\theta) & \cos(\theta) & 0 \\ + 0 & 0 & 1 + \end{bmatrix} + + Examples + ======== + + >>> from sympy import pi, rot_ccw_axis3 + + A rotation of pi/3 (60 degrees): + + >>> theta = pi/3 + >>> rot_ccw_axis3(theta) + Matrix([ + [ 1/2, -sqrt(3)/2, 0], + [sqrt(3)/2, 1/2, 0], + [ 0, 0, 1]]) + + If we rotate by pi/2 (90 degrees): + + >>> rot_ccw_axis3(pi/2) + Matrix([ + [0, -1, 0], + [1, 0, 0], + [0, 0, 1]]) + + See Also + ======== + + rot_givens: Returns a Givens rotation matrix (generalized rotation for + any number of dimensions) + rot_axis3: Returns a rotation matrix for a rotation of theta (in radians) + about the 3-axis (clockwise around the z axis) + rot_ccw_axis1: Returns a rotation matrix for a rotation of theta (in radians) + about the 1-axis (counterclockwise around the x axis) + rot_ccw_axis2: Returns a rotation matrix for a rotation of theta (in radians) + about the 2-axis (counterclockwise around the y axis) + """ + return rot_givens(1, 0, theta, dim=3) + + +def rot_ccw_axis2(theta): + r"""Returns a rotation matrix for a rotation of theta (in radians) + about the 2-axis. + + Explanation + =========== + + For a right-handed coordinate system, this corresponds to a + counterclockwise rotation around the `y`-axis, given by: + + .. math:: + + R = \begin{bmatrix} + \cos(\theta) & 0 & \sin(\theta) \\ + 0 & 1 & 0 \\ + -\sin(\theta) & 0 & \cos(\theta) + \end{bmatrix} + + Examples + ======== + + >>> from sympy import pi, rot_ccw_axis2 + + A rotation of pi/3 (60 degrees): + + >>> theta = pi/3 + >>> rot_ccw_axis2(theta) + Matrix([ + [ 1/2, 0, sqrt(3)/2], + [ 0, 1, 0], + [-sqrt(3)/2, 0, 1/2]]) + + If we rotate by pi/2 (90 degrees): + + >>> rot_ccw_axis2(pi/2) + Matrix([ + [ 0, 0, 1], + [ 0, 1, 0], + [-1, 0, 0]]) + + See Also + ======== + + rot_givens: Returns a Givens rotation matrix (generalized rotation for + any number of dimensions) + rot_axis2: Returns a rotation matrix for a rotation of theta (in radians) + about the 2-axis (clockwise around the y axis) + rot_ccw_axis1: Returns a rotation matrix for a rotation of theta (in radians) + about the 1-axis (counterclockwise around the x axis) + rot_ccw_axis3: Returns a rotation matrix for a rotation of theta (in radians) + about the 3-axis (counterclockwise around the z axis) + """ + return rot_givens(0, 2, theta, dim=3) + + +def rot_ccw_axis1(theta): + r"""Returns a rotation matrix for a rotation of theta (in radians) + about the 1-axis. + + Explanation + =========== + + For a right-handed coordinate system, this corresponds to a + counterclockwise rotation around the `x`-axis, given by: + + .. math:: + + R = \begin{bmatrix} + 1 & 0 & 0 \\ + 0 & \cos(\theta) & -\sin(\theta) \\ + 0 & \sin(\theta) & \cos(\theta) + \end{bmatrix} + + Examples + ======== + + >>> from sympy import pi, rot_ccw_axis1 + + A rotation of pi/3 (60 degrees): + + >>> theta = pi/3 + >>> rot_ccw_axis1(theta) + Matrix([ + [1, 0, 0], + [0, 1/2, -sqrt(3)/2], + [0, sqrt(3)/2, 1/2]]) + + If we rotate by pi/2 (90 degrees): + + >>> rot_ccw_axis1(pi/2) + Matrix([ + [1, 0, 0], + [0, 0, -1], + [0, 1, 0]]) + + See Also + ======== + + rot_givens: Returns a Givens rotation matrix (generalized rotation for + any number of dimensions) + rot_axis1: Returns a rotation matrix for a rotation of theta (in radians) + about the 1-axis (clockwise around the x axis) + rot_ccw_axis2: Returns a rotation matrix for a rotation of theta (in radians) + about the 2-axis (counterclockwise around the y axis) + rot_ccw_axis3: Returns a rotation matrix for a rotation of theta (in radians) + about the 3-axis (counterclockwise around the z axis) + """ + return rot_givens(2, 1, theta, dim=3) + + +@doctest_depends_on(modules=('numpy',)) +def symarray(prefix, shape, **kwargs): # pragma: no cover + r"""Create a numpy ndarray of symbols (as an object array). + + The created symbols are named ``prefix_i1_i2_``... You should thus provide a + non-empty prefix if you want your symbols to be unique for different output + arrays, as SymPy symbols with identical names are the same object. + + Parameters + ---------- + + prefix : string + A prefix prepended to the name of every symbol. + + shape : int or tuple + Shape of the created array. If an int, the array is one-dimensional; for + more than one dimension the shape must be a tuple. + + \*\*kwargs : dict + keyword arguments passed on to Symbol + + Examples + ======== + These doctests require numpy. + + >>> from sympy import symarray + >>> symarray('', 3) + [_0 _1 _2] + + If you want multiple symarrays to contain distinct symbols, you *must* + provide unique prefixes: + + >>> a = symarray('', 3) + >>> b = symarray('', 3) + >>> a[0] == b[0] + True + >>> a = symarray('a', 3) + >>> b = symarray('b', 3) + >>> a[0] == b[0] + False + + Creating symarrays with a prefix: + + >>> symarray('a', 3) + [a_0 a_1 a_2] + + For more than one dimension, the shape must be given as a tuple: + + >>> symarray('a', (2, 3)) + [[a_0_0 a_0_1 a_0_2] + [a_1_0 a_1_1 a_1_2]] + >>> symarray('a', (2, 3, 2)) + [[[a_0_0_0 a_0_0_1] + [a_0_1_0 a_0_1_1] + [a_0_2_0 a_0_2_1]] + + [[a_1_0_0 a_1_0_1] + [a_1_1_0 a_1_1_1] + [a_1_2_0 a_1_2_1]]] + + For setting assumptions of the underlying Symbols: + + >>> [s.is_real for s in symarray('a', 2, real=True)] + [True, True] + """ + from numpy import empty, ndindex + arr = empty(shape, dtype=object) + for index in ndindex(shape): + arr[index] = Symbol('%s_%s' % (prefix, '_'.join(map(str, index))), + **kwargs) + return arr + + +############### +# Functions +############### + +def casoratian(seqs, n, zero=True): + """Given linear difference operator L of order 'k' and homogeneous + equation Ly = 0 we want to compute kernel of L, which is a set + of 'k' sequences: a(n), b(n), ... z(n). + + Solutions of L are linearly independent iff their Casoratian, + denoted as C(a, b, ..., z), do not vanish for n = 0. + + Casoratian is defined by k x k determinant:: + + + a(n) b(n) . . . z(n) + + | a(n+1) b(n+1) . . . z(n+1) | + | . . . . | + | . . . . | + | . . . . | + + a(n+k-1) b(n+k-1) . . . z(n+k-1) + + + It proves very useful in rsolve_hyper() where it is applied + to a generating set of a recurrence to factor out linearly + dependent solutions and return a basis: + + >>> from sympy import Symbol, casoratian, factorial + >>> n = Symbol('n', integer=True) + + Exponential and factorial are linearly independent: + + >>> casoratian([2**n, factorial(n)], n) != 0 + True + + """ + + seqs = list(map(sympify, seqs)) + + if not zero: + f = lambda i, j: seqs[j].subs(n, n + i) + else: + f = lambda i, j: seqs[j].subs(n, i) + + k = len(seqs) + + return Matrix(k, k, f).det() + + +def eye(*args, **kwargs): + """Create square identity matrix n x n + + See Also + ======== + + diag + zeros + ones + """ + + return Matrix.eye(*args, **kwargs) + + +def diag(*values, strict=True, unpack=False, **kwargs): + """Returns a matrix with the provided values placed on the + diagonal. If non-square matrices are included, they will + produce a block-diagonal matrix. + + Examples + ======== + + This version of diag is a thin wrapper to Matrix.diag that differs + in that it treats all lists like matrices -- even when a single list + is given. If this is not desired, either put a `*` before the list or + set `unpack=True`. + + >>> from sympy import diag + + >>> diag([1, 2, 3], unpack=True) # = diag(1,2,3) or diag(*[1,2,3]) + Matrix([ + [1, 0, 0], + [0, 2, 0], + [0, 0, 3]]) + + >>> diag([1, 2, 3]) # a column vector + Matrix([ + [1], + [2], + [3]]) + + See Also + ======== + .matrixbase.MatrixBase.eye + .matrixbase.MatrixBase.diagonal + .matrixbase.MatrixBase.diag + .expressions.blockmatrix.BlockMatrix + """ + return Matrix.diag(*values, strict=strict, unpack=unpack, **kwargs) + + +def GramSchmidt(vlist, orthonormal=False): + """Apply the Gram-Schmidt process to a set of vectors. + + Parameters + ========== + + vlist : List of Matrix + Vectors to be orthogonalized for. + + orthonormal : Bool, optional + If true, return an orthonormal basis. + + Returns + ======= + + vlist : List of Matrix + Orthogonalized vectors + + Notes + ===== + + This routine is mostly duplicate from ``Matrix.orthogonalize``, + except for some difference that this always raises error when + linearly dependent vectors are found, and the keyword ``normalize`` + has been named as ``orthonormal`` in this function. + + See Also + ======== + + .matrixbase.MatrixBase.orthogonalize + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process + """ + return MutableDenseMatrix.orthogonalize( + *vlist, normalize=orthonormal, rankcheck=True + ) + + +def hessian(f, varlist, constraints=()): + """Compute Hessian matrix for a function f wrt parameters in varlist + which may be given as a sequence or a row/column vector. A list of + constraints may optionally be given. + + Examples + ======== + + >>> from sympy import Function, hessian, pprint + >>> from sympy.abc import x, y + >>> f = Function('f')(x, y) + >>> g1 = Function('g')(x, y) + >>> g2 = x**2 + 3*y + >>> pprint(hessian(f, (x, y), [g1, g2])) + [ d d ] + [ 0 0 --(g(x, y)) --(g(x, y)) ] + [ dx dy ] + [ ] + [ 0 0 2*x 3 ] + [ ] + [ 2 2 ] + [d d d ] + [--(g(x, y)) 2*x ---(f(x, y)) -----(f(x, y))] + [dx 2 dy dx ] + [ dx ] + [ ] + [ 2 2 ] + [d d d ] + [--(g(x, y)) 3 -----(f(x, y)) ---(f(x, y)) ] + [dy dy dx 2 ] + [ dy ] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hessian_matrix + + See Also + ======== + + sympy.matrices.matrixbase.MatrixBase.jacobian + wronskian + """ + # f is the expression representing a function f, return regular matrix + if isinstance(varlist, MatrixBase): + if 1 not in varlist.shape: + raise ShapeError("`varlist` must be a column or row vector.") + if varlist.cols == 1: + varlist = varlist.T + varlist = varlist.tolist()[0] + if is_sequence(varlist): + n = len(varlist) + if not n: + raise ShapeError("`len(varlist)` must not be zero.") + else: + raise ValueError("Improper variable list in hessian function") + if not getattr(f, 'diff'): + # check differentiability + raise ValueError("Function `f` (%s) is not differentiable" % f) + m = len(constraints) + N = m + n + out = zeros(N) + for k, g in enumerate(constraints): + if not getattr(g, 'diff'): + # check differentiability + raise ValueError("Function `f` (%s) is not differentiable" % f) + for i in range(n): + out[k, i + m] = g.diff(varlist[i]) + for i in range(n): + for j in range(i, n): + out[i + m, j + m] = f.diff(varlist[i]).diff(varlist[j]) + for i in range(N): + for j in range(i + 1, N): + out[j, i] = out[i, j] + return out + + +def jordan_cell(eigenval, n): + """ + Create a Jordan block: + + Examples + ======== + + >>> from sympy import jordan_cell + >>> from sympy.abc import x + >>> jordan_cell(x, 4) + Matrix([ + [x, 1, 0, 0], + [0, x, 1, 0], + [0, 0, x, 1], + [0, 0, 0, x]]) + """ + + return Matrix.jordan_block(size=n, eigenvalue=eigenval) + + +def matrix_multiply_elementwise(A, B): + """Return the Hadamard product (elementwise product) of A and B + + >>> from sympy import Matrix, matrix_multiply_elementwise + >>> A = Matrix([[0, 1, 2], [3, 4, 5]]) + >>> B = Matrix([[1, 10, 100], [100, 10, 1]]) + >>> matrix_multiply_elementwise(A, B) + Matrix([ + [ 0, 10, 200], + [300, 40, 5]]) + + See Also + ======== + + sympy.matrices.matrixbase.MatrixBase.__mul__ + """ + return A.multiply_elementwise(B) + + +def ones(*args, **kwargs): + """Returns a matrix of ones with ``rows`` rows and ``cols`` columns; + if ``cols`` is omitted a square matrix will be returned. + + See Also + ======== + + zeros + eye + diag + """ + + if 'c' in kwargs: + kwargs['cols'] = kwargs.pop('c') + + return Matrix.ones(*args, **kwargs) + + +def randMatrix(r, c=None, min=0, max=99, seed=None, symmetric=False, + percent=100, prng=None): + """Create random matrix with dimensions ``r`` x ``c``. If ``c`` is omitted + the matrix will be square. If ``symmetric`` is True the matrix must be + square. If ``percent`` is less than 100 then only approximately the given + percentage of elements will be non-zero. + + The pseudo-random number generator used to generate matrix is chosen in the + following way. + + * If ``prng`` is supplied, it will be used as random number generator. + It should be an instance of ``random.Random``, or at least have + ``randint`` and ``shuffle`` methods with same signatures. + * if ``prng`` is not supplied but ``seed`` is supplied, then new + ``random.Random`` with given ``seed`` will be created; + * otherwise, a new ``random.Random`` with default seed will be used. + + Examples + ======== + + >>> from sympy import randMatrix + >>> randMatrix(3) # doctest:+SKIP + [25, 45, 27] + [44, 54, 9] + [23, 96, 46] + >>> randMatrix(3, 2) # doctest:+SKIP + [87, 29] + [23, 37] + [90, 26] + >>> randMatrix(3, 3, 0, 2) # doctest:+SKIP + [0, 2, 0] + [2, 0, 1] + [0, 0, 1] + >>> randMatrix(3, symmetric=True) # doctest:+SKIP + [85, 26, 29] + [26, 71, 43] + [29, 43, 57] + >>> A = randMatrix(3, seed=1) + >>> B = randMatrix(3, seed=2) + >>> A == B + False + >>> A == randMatrix(3, seed=1) + True + >>> randMatrix(3, symmetric=True, percent=50) # doctest:+SKIP + [77, 70, 0], + [70, 0, 0], + [ 0, 0, 88] + """ + # Note that ``Random()`` is equivalent to ``Random(None)`` + prng = prng or random.Random(seed) + + if c is None: + c = r + + if symmetric and r != c: + raise ValueError('For symmetric matrices, r must equal c, but %i != %i' % (r, c)) + + ij = range(r * c) + if percent != 100: + ij = prng.sample(ij, int(len(ij)*percent // 100)) + + m = zeros(r, c) + + if not symmetric: + for ijk in ij: + i, j = divmod(ijk, c) + m[i, j] = prng.randint(min, max) + else: + for ijk in ij: + i, j = divmod(ijk, c) + if i <= j: + m[i, j] = m[j, i] = prng.randint(min, max) + + return m + + +def wronskian(functions, var, method='bareiss'): + """ + Compute Wronskian for [] of functions + + :: + + | f1 f2 ... fn | + | f1' f2' ... fn' | + | . . . . | + W(f1, ..., fn) = | . . . . | + | . . . . | + | (n) (n) (n) | + | D (f1) D (f2) ... D (fn) | + + see: https://en.wikipedia.org/wiki/Wronskian + + See Also + ======== + + sympy.matrices.matrixbase.MatrixBase.jacobian + hessian + """ + + functions = [sympify(f) for f in functions] + n = len(functions) + if n == 0: + return S.One + W = Matrix(n, n, lambda i, j: functions[i].diff(var, j)) + return W.det(method) + + +def zeros(*args, **kwargs): + """Returns a matrix of zeros with ``rows`` rows and ``cols`` columns; + if ``cols`` is omitted a square matrix will be returned. + + See Also + ======== + + ones + eye + diag + """ + + if 'c' in kwargs: + kwargs['cols'] = kwargs.pop('c') + + return Matrix.zeros(*args, **kwargs) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/determinant.py b/venv/lib/python3.11/site-packages/sympy/matrices/determinant.py new file mode 100644 index 0000000000000000000000000000000000000000..9206c0714999ebe0cde5c4300d9b3293939177df --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/determinant.py @@ -0,0 +1,1021 @@ +from types import FunctionType + +from sympy.core.cache import cacheit +from sympy.core.numbers import Float, Integer +from sympy.core.singleton import S +from sympy.core.symbol import uniquely_named_symbol +from sympy.core.mul import Mul +from sympy.polys import PurePoly, cancel +from sympy.functions.combinatorial.numbers import nC +from sympy.polys.matrices.domainmatrix import DomainMatrix +from sympy.polys.matrices.ddm import DDM + +from .exceptions import NonSquareMatrixError +from .utilities import ( + _get_intermediate_simp, _get_intermediate_simp_bool, + _iszero, _is_zero_after_expand_mul, _dotprodsimp, _simplify) + + +def _find_reasonable_pivot(col, iszerofunc=_iszero, simpfunc=_simplify): + """ Find the lowest index of an item in ``col`` that is + suitable for a pivot. If ``col`` consists only of + Floats, the pivot with the largest norm is returned. + Otherwise, the first element where ``iszerofunc`` returns + False is used. If ``iszerofunc`` does not return false, + items are simplified and retested until a suitable + pivot is found. + + Returns a 4-tuple + (pivot_offset, pivot_val, assumed_nonzero, newly_determined) + where pivot_offset is the index of the pivot, pivot_val is + the (possibly simplified) value of the pivot, assumed_nonzero + is True if an assumption that the pivot was non-zero + was made without being proved, and newly_determined are + elements that were simplified during the process of pivot + finding.""" + + newly_determined = [] + col = list(col) + # a column that contains a mix of floats and integers + # but at least one float is considered a numerical + # column, and so we do partial pivoting + if all(isinstance(x, (Float, Integer)) for x in col) and any( + isinstance(x, Float) for x in col): + col_abs = [abs(x) for x in col] + max_value = max(col_abs) + if iszerofunc(max_value): + # just because iszerofunc returned True, doesn't + # mean the value is numerically zero. Make sure + # to replace all entries with numerical zeros + if max_value != 0: + newly_determined = [(i, 0) for i, x in enumerate(col) if x != 0] + return (None, None, False, newly_determined) + index = col_abs.index(max_value) + return (index, col[index], False, newly_determined) + + # PASS 1 (iszerofunc directly) + possible_zeros = [] + for i, x in enumerate(col): + is_zero = iszerofunc(x) + # is someone wrote a custom iszerofunc, it may return + # BooleanFalse or BooleanTrue instead of True or False, + # so use == for comparison instead of `is` + if is_zero == False: + # we found something that is definitely not zero + return (i, x, False, newly_determined) + possible_zeros.append(is_zero) + + # by this point, we've found no certain non-zeros + if all(possible_zeros): + # if everything is definitely zero, we have + # no pivot + return (None, None, False, newly_determined) + + # PASS 2 (iszerofunc after simplify) + # we haven't found any for-sure non-zeros, so + # go through the elements iszerofunc couldn't + # make a determination about and opportunistically + # simplify to see if we find something + for i, x in enumerate(col): + if possible_zeros[i] is not None: + continue + simped = simpfunc(x) + is_zero = iszerofunc(simped) + if is_zero in (True, False): + newly_determined.append((i, simped)) + if is_zero == False: + return (i, simped, False, newly_determined) + possible_zeros[i] = is_zero + + # after simplifying, some things that were recognized + # as zeros might be zeros + if all(possible_zeros): + # if everything is definitely zero, we have + # no pivot + return (None, None, False, newly_determined) + + # PASS 3 (.equals(0)) + # some expressions fail to simplify to zero, but + # ``.equals(0)`` evaluates to True. As a last-ditch + # attempt, apply ``.equals`` to these expressions + for i, x in enumerate(col): + if possible_zeros[i] is not None: + continue + if x.equals(S.Zero): + # ``.iszero`` may return False with + # an implicit assumption (e.g., ``x.equals(0)`` + # when ``x`` is a symbol), so only treat it + # as proved when ``.equals(0)`` returns True + possible_zeros[i] = True + newly_determined.append((i, S.Zero)) + + if all(possible_zeros): + return (None, None, False, newly_determined) + + # at this point there is nothing that could definitely + # be a pivot. To maintain compatibility with existing + # behavior, we'll assume that an illdetermined thing is + # non-zero. We should probably raise a warning in this case + i = possible_zeros.index(None) + return (i, col[i], True, newly_determined) + + +def _find_reasonable_pivot_naive(col, iszerofunc=_iszero, simpfunc=None): + """ + Helper that computes the pivot value and location from a + sequence of contiguous matrix column elements. As a side effect + of the pivot search, this function may simplify some of the elements + of the input column. A list of these simplified entries and their + indices are also returned. + This function mimics the behavior of _find_reasonable_pivot(), + but does less work trying to determine if an indeterminate candidate + pivot simplifies to zero. This more naive approach can be much faster, + with the trade-off that it may erroneously return a pivot that is zero. + + ``col`` is a sequence of contiguous column entries to be searched for + a suitable pivot. + ``iszerofunc`` is a callable that returns a Boolean that indicates + if its input is zero, or None if no such determination can be made. + ``simpfunc`` is a callable that simplifies its input. It must return + its input if it does not simplify its input. Passing in + ``simpfunc=None`` indicates that the pivot search should not attempt + to simplify any candidate pivots. + + Returns a 4-tuple: + (pivot_offset, pivot_val, assumed_nonzero, newly_determined) + ``pivot_offset`` is the sequence index of the pivot. + ``pivot_val`` is the value of the pivot. + pivot_val and col[pivot_index] are equivalent, but will be different + when col[pivot_index] was simplified during the pivot search. + ``assumed_nonzero`` is a boolean indicating if the pivot cannot be + guaranteed to be zero. If assumed_nonzero is true, then the pivot + may or may not be non-zero. If assumed_nonzero is false, then + the pivot is non-zero. + ``newly_determined`` is a list of index-value pairs of pivot candidates + that were simplified during the pivot search. + """ + + # indeterminates holds the index-value pairs of each pivot candidate + # that is neither zero or non-zero, as determined by iszerofunc(). + # If iszerofunc() indicates that a candidate pivot is guaranteed + # non-zero, or that every candidate pivot is zero then the contents + # of indeterminates are unused. + # Otherwise, the only viable candidate pivots are symbolic. + # In this case, indeterminates will have at least one entry, + # and all but the first entry are ignored when simpfunc is None. + indeterminates = [] + for i, col_val in enumerate(col): + col_val_is_zero = iszerofunc(col_val) + if col_val_is_zero == False: + # This pivot candidate is non-zero. + return i, col_val, False, [] + elif col_val_is_zero is None: + # The candidate pivot's comparison with zero + # is indeterminate. + indeterminates.append((i, col_val)) + + if len(indeterminates) == 0: + # All candidate pivots are guaranteed to be zero, i.e. there is + # no pivot. + return None, None, False, [] + + if simpfunc is None: + # Caller did not pass in a simplification function that might + # determine if an indeterminate pivot candidate is guaranteed + # to be nonzero, so assume the first indeterminate candidate + # is non-zero. + return indeterminates[0][0], indeterminates[0][1], True, [] + + # newly_determined holds index-value pairs of candidate pivots + # that were simplified during the search for a non-zero pivot. + newly_determined = [] + for i, col_val in indeterminates: + tmp_col_val = simpfunc(col_val) + if id(col_val) != id(tmp_col_val): + # simpfunc() simplified this candidate pivot. + newly_determined.append((i, tmp_col_val)) + if iszerofunc(tmp_col_val) == False: + # Candidate pivot simplified to a guaranteed non-zero value. + return i, tmp_col_val, False, newly_determined + + return indeterminates[0][0], indeterminates[0][1], True, newly_determined + + +# This functions is a candidate for caching if it gets implemented for matrices. +def _berkowitz_toeplitz_matrix(M): + """Return (A,T) where T the Toeplitz matrix used in the Berkowitz algorithm + corresponding to ``M`` and A is the first principal submatrix. + """ + + # the 0 x 0 case is trivial + if M.rows == 0 and M.cols == 0: + return M._new(1,1, [M.one]) + + # + # Partition M = [ a_11 R ] + # [ C A ] + # + + a, R = M[0,0], M[0, 1:] + C, A = M[1:, 0], M[1:,1:] + + # + # The Toeplitz matrix looks like + # + # [ 1 ] + # [ -a 1 ] + # [ -RC -a 1 ] + # [ -RAC -RC -a 1 ] + # [ -RA**2C -RAC -RC -a 1 ] + # etc. + + # Compute the diagonal entries. + # Because multiplying matrix times vector is so much + # more efficient than matrix times matrix, recursively + # compute -R * A**n * C. + diags = [C] + for i in range(M.rows - 2): + diags.append(A.multiply(diags[i], dotprodsimp=None)) + diags = [(-R).multiply(d, dotprodsimp=None)[0, 0] for d in diags] + diags = [M.one, -a] + diags + + def entry(i,j): + if j > i: + return M.zero + return diags[i - j] + + toeplitz = M._new(M.cols + 1, M.rows, entry) + return (A, toeplitz) + + +# This functions is a candidate for caching if it gets implemented for matrices. +def _berkowitz_vector(M): + """ Run the Berkowitz algorithm and return a vector whose entries + are the coefficients of the characteristic polynomial of ``M``. + + Given N x N matrix, efficiently compute + coefficients of characteristic polynomials of ``M`` + without division in the ground domain. + + This method is particularly useful for computing determinant, + principal minors and characteristic polynomial when ``M`` + has complicated coefficients e.g. polynomials. Semi-direct + usage of this algorithm is also important in computing + efficiently sub-resultant PRS. + + Assuming that M is a square matrix of dimension N x N and + I is N x N identity matrix, then the Berkowitz vector is + an N x 1 vector whose entries are coefficients of the + polynomial + + charpoly(M) = det(t*I - M) + + As a consequence, all polynomials generated by Berkowitz + algorithm are monic. + + For more information on the implemented algorithm refer to: + + [1] S.J. Berkowitz, On computing the determinant in small + parallel time using a small number of processors, ACM, + Information Processing Letters 18, 1984, pp. 147-150 + + [2] M. Keber, Division-Free computation of sub-resultants + using Bezout matrices, Tech. Report MPI-I-2006-1-006, + Saarbrucken, 2006 + """ + + # handle the trivial cases + if M.rows == 0 and M.cols == 0: + return M._new(1, 1, [M.one]) + elif M.rows == 1 and M.cols == 1: + return M._new(2, 1, [M.one, -M[0,0]]) + + submat, toeplitz = _berkowitz_toeplitz_matrix(M) + + return toeplitz.multiply(_berkowitz_vector(submat), dotprodsimp=None) + + +def _adjugate(M, method="berkowitz"): + """Returns the adjugate, or classical adjoint, of + a matrix. That is, the transpose of the matrix of cofactors. + + https://en.wikipedia.org/wiki/Adjugate + + Parameters + ========== + + method : string, optional + Method to use to find the cofactors, can be "bareiss", "berkowitz", + "bird", "laplace" or "lu". + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[1, 2], [3, 4]]) + >>> M.adjugate() + Matrix([ + [ 4, -2], + [-3, 1]]) + + See Also + ======== + + cofactor_matrix + sympy.matrices.matrixbase.MatrixBase.transpose + """ + + return M.cofactor_matrix(method=method).transpose() + + +# This functions is a candidate for caching if it gets implemented for matrices. +def _charpoly(M, x='lambda', simplify=_simplify): + """Computes characteristic polynomial det(x*I - M) where I is + the identity matrix. + + A PurePoly is returned, so using different variables for ``x`` does + not affect the comparison or the polynomials: + + Parameters + ========== + + x : string, optional + Name for the "lambda" variable, defaults to "lambda". + + simplify : function, optional + Simplification function to use on the characteristic polynomial + calculated. Defaults to ``simplify``. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import x, y + >>> M = Matrix([[1, 3], [2, 0]]) + >>> M.charpoly() + PurePoly(lambda**2 - lambda - 6, lambda, domain='ZZ') + >>> M.charpoly(x) == M.charpoly(y) + True + >>> M.charpoly(x) == M.charpoly(y) + True + + Specifying ``x`` is optional; a symbol named ``lambda`` is used by + default (which looks good when pretty-printed in unicode): + + >>> M.charpoly().as_expr() + lambda**2 - lambda - 6 + + And if ``x`` clashes with an existing symbol, underscores will + be prepended to the name to make it unique: + + >>> M = Matrix([[1, 2], [x, 0]]) + >>> M.charpoly(x).as_expr() + _x**2 - _x - 2*x + + Whether you pass a symbol or not, the generator can be obtained + with the gen attribute since it may not be the same as the symbol + that was passed: + + >>> M.charpoly(x).gen + _x + >>> M.charpoly(x).gen == x + False + + Notes + ===== + + The Samuelson-Berkowitz algorithm is used to compute + the characteristic polynomial efficiently and without any + division operations. Thus the characteristic polynomial over any + commutative ring without zero divisors can be computed. + + If the determinant det(x*I - M) can be found out easily as + in the case of an upper or a lower triangular matrix, then + instead of Samuelson-Berkowitz algorithm, eigenvalues are computed + and the characteristic polynomial with their help. + + See Also + ======== + + det + """ + + if not M.is_square: + raise NonSquareMatrixError() + + # Use DomainMatrix. We are already going to convert this to a Poly so there + # is no need to worry about expanding powers etc. Also since this algorithm + # does not require division or zero detection it is fine to use EX. + # + # M.to_DM() will fall back on EXRAW rather than EX. EXRAW is a lot faster + # for elementary arithmetic because it does not call cancel for each + # operation but it generates large unsimplified results that are slow in + # the subsequent call to simplify. Using EX instead is faster overall + # but at least in some cases EXRAW+simplify gives a simpler result so we + # preserve that existing behaviour of charpoly for now... + dM = M.to_DM() + + K = dM.domain + + cp = dM.charpoly() + + x = uniquely_named_symbol(x, [M], modify=lambda s: '_' + s) + + if K.is_EXRAW or simplify is not _simplify: + # XXX: Converting back to Expr is expensive. We only do it if the + # caller supplied a custom simplify function for backwards + # compatibility or otherwise if the domain was EX. For any other domain + # there should be no benefit in simplifying at this stage because Poly + # will put everything into canonical form anyway. + berk_vector = [K.to_sympy(c) for c in cp] + berk_vector = [simplify(a) for a in berk_vector] + p = PurePoly(berk_vector, x) + + else: + # Convert from the list of domain elements directly to Poly. + p = PurePoly(cp, x, domain=K) + + return p + + +def _cofactor(M, i, j, method="berkowitz"): + """Calculate the cofactor of an element. + + Parameters + ========== + + method : string, optional + Method to use to find the cofactors, can be "bareiss", "berkowitz", + "bird", "laplace" or "lu". + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[1, 2], [3, 4]]) + >>> M.cofactor(0, 1) + -3 + + See Also + ======== + + cofactor_matrix + minor + minor_submatrix + """ + + if not M.is_square or M.rows < 1: + raise NonSquareMatrixError() + + return S.NegativeOne**((i + j) % 2) * M.minor(i, j, method) + + +def _cofactor_matrix(M, method="berkowitz"): + """Return a matrix containing the cofactor of each element. + + Parameters + ========== + + method : string, optional + Method to use to find the cofactors, can be "bareiss", "berkowitz", + "bird", "laplace" or "lu". + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[1, 2], [3, 4]]) + >>> M.cofactor_matrix() + Matrix([ + [ 4, -3], + [-2, 1]]) + + See Also + ======== + + cofactor + minor + minor_submatrix + """ + + if not M.is_square: + raise NonSquareMatrixError() + + return M._new(M.rows, M.cols, + lambda i, j: M.cofactor(i, j, method)) + +def _per(M): + """Returns the permanent of a matrix. Unlike determinant, + permanent is defined for both square and non-square matrices. + + For an m x n matrix, with m less than or equal to n, + it is given as the sum over the permutations s of size + less than or equal to m on [1, 2, . . . n] of the product + from i = 1 to m of M[i, s[i]]. Taking the transpose will + not affect the value of the permanent. + + In the case of a square matrix, this is the same as the permutation + definition of the determinant, but it does not take the sign of the + permutation into account. Computing the permanent with this definition + is quite inefficient, so here the Ryser formula is used. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> M.per() + 450 + >>> M = Matrix([1, 5, 7]) + >>> M.per() + 13 + + References + ========== + + .. [1] Prof. Frank Ben's notes: https://math.berkeley.edu/~bernd/ban275.pdf + .. [2] Wikipedia article on Permanent: https://en.wikipedia.org/wiki/Permanent_%28mathematics%29 + .. [3] https://reference.wolfram.com/language/ref/Permanent.html + .. [4] Permanent of a rectangular matrix : https://arxiv.org/pdf/0904.3251.pdf + """ + import itertools + + m, n = M.shape + if m > n: + M = M.T + m, n = n, m + s = list(range(n)) + + subsets = [] + for i in range(1, m + 1): + subsets += list(map(list, itertools.combinations(s, i))) + + perm = 0 + for subset in subsets: + prod = 1 + sub_len = len(subset) + for i in range(m): + prod *= sum(M[i, j] for j in subset) + perm += prod * S.NegativeOne**sub_len * nC(n - sub_len, m - sub_len) + perm *= S.NegativeOne**m + return perm.simplify() + +def _det_DOM(M): + DOM = DomainMatrix.from_Matrix(M, field=True, extension=True) + K = DOM.domain + return K.to_sympy(DOM.det()) + +# This functions is a candidate for caching if it gets implemented for matrices. +def _det(M, method="bareiss", iszerofunc=None): + """Computes the determinant of a matrix if ``M`` is a concrete matrix object + otherwise return an expressions ``Determinant(M)`` if ``M`` is a + ``MatrixSymbol`` or other expression. + + Parameters + ========== + + method : string, optional + Specifies the algorithm used for computing the matrix determinant. + + If the matrix is at most 3x3, a hard-coded formula is used and the + specified method is ignored. Otherwise, it defaults to + ``'bareiss'``. + + Also, if the matrix is an upper or a lower triangular matrix, determinant + is computed by simple multiplication of diagonal elements, and the + specified method is ignored. + + If it is set to ``'domain-ge'``, then Gaussian elimination method will + be used via using DomainMatrix. + + If it is set to ``'bareiss'``, Bareiss' fraction-free algorithm will + be used. + + If it is set to ``'berkowitz'``, Berkowitz' algorithm will be used. + + If it is set to ``'bird'``, Bird's algorithm will be used [1]_. + + If it is set to ``'laplace'``, Laplace's algorithm will be used. + + Otherwise, if it is set to ``'lu'``, LU decomposition will be used. + + .. note:: + For backward compatibility, legacy keys like "bareis" and + "det_lu" can still be used to indicate the corresponding + methods. + And the keys are also case-insensitive for now. However, it is + suggested to use the precise keys for specifying the method. + + iszerofunc : FunctionType or None, optional + If it is set to ``None``, it will be defaulted to ``_iszero`` if the + method is set to ``'bareiss'``, and ``_is_zero_after_expand_mul`` if + the method is set to ``'lu'``. + + It can also accept any user-specified zero testing function, if it + is formatted as a function which accepts a single symbolic argument + and returns ``True`` if it is tested as zero and ``False`` if it + tested as non-zero, and also ``None`` if it is undecidable. + + Returns + ======= + + det : Basic + Result of determinant. + + Raises + ====== + + ValueError + If unrecognized keys are given for ``method`` or ``iszerofunc``. + + NonSquareMatrixError + If attempted to calculate determinant from a non-square matrix. + + Examples + ======== + + >>> from sympy import Matrix, eye, det + >>> I3 = eye(3) + >>> det(I3) + 1 + >>> M = Matrix([[1, 2], [3, 4]]) + >>> det(M) + -2 + >>> det(M) == M.det() + True + >>> M.det(method="domain-ge") + -2 + + References + ========== + + .. [1] Bird, R. S. (2011). A simple division-free algorithm for computing + determinants. Inf. Process. Lett., 111(21), 1072-1074. doi: + 10.1016/j.ipl.2011.08.006 + """ + + # sanitize `method` + method = method.lower() + + if method == "bareis": + method = "bareiss" + elif method == "det_lu": + method = "lu" + + if method not in ("bareiss", "berkowitz", "lu", "domain-ge", "bird", + "laplace"): + raise ValueError("Determinant method '%s' unrecognized" % method) + + if iszerofunc is None: + if method == "bareiss": + iszerofunc = _is_zero_after_expand_mul + elif method == "lu": + iszerofunc = _iszero + + elif not isinstance(iszerofunc, FunctionType): + raise ValueError("Zero testing method '%s' unrecognized" % iszerofunc) + + n = M.rows + + if n == M.cols: # square check is done in individual method functions + if n == 0: + return M.one + elif n == 1: + return M[0, 0] + elif n == 2: + m = M[0, 0] * M[1, 1] - M[0, 1] * M[1, 0] + return _get_intermediate_simp(_dotprodsimp)(m) + elif n == 3: + m = (M[0, 0] * M[1, 1] * M[2, 2] + + M[0, 1] * M[1, 2] * M[2, 0] + + M[0, 2] * M[1, 0] * M[2, 1] + - M[0, 2] * M[1, 1] * M[2, 0] + - M[0, 0] * M[1, 2] * M[2, 1] + - M[0, 1] * M[1, 0] * M[2, 2]) + return _get_intermediate_simp(_dotprodsimp)(m) + + dets = [] + for b in M.strongly_connected_components(): + if method == "domain-ge": # uses DomainMatrix to evaluate determinant + det = _det_DOM(M[b, b]) + elif method == "bareiss": + det = M[b, b]._eval_det_bareiss(iszerofunc=iszerofunc) + elif method == "berkowitz": + det = M[b, b]._eval_det_berkowitz() + elif method == "lu": + det = M[b, b]._eval_det_lu(iszerofunc=iszerofunc) + elif method == "bird": + det = M[b, b]._eval_det_bird() + elif method == "laplace": + det = M[b, b]._eval_det_laplace() + dets.append(det) + return Mul(*dets) + + +# This functions is a candidate for caching if it gets implemented for matrices. +def _det_bareiss(M, iszerofunc=_is_zero_after_expand_mul): + """Compute matrix determinant using Bareiss' fraction-free + algorithm which is an extension of the well known Gaussian + elimination method. This approach is best suited for dense + symbolic matrices and will result in a determinant with + minimal number of fractions. It means that less term + rewriting is needed on resulting formulae. + + Parameters + ========== + + iszerofunc : function, optional + The function to use to determine zeros when doing an LU decomposition. + Defaults to ``lambda x: x.is_zero``. + + TODO: Implement algorithm for sparse matrices (SFF), + http://www.eecis.udel.edu/~saunders/papers/sffge/it5.ps. + """ + + # Recursively implemented Bareiss' algorithm as per Deanna Richelle Leggett's + # thesis http://www.math.usm.edu/perry/Research/Thesis_DRL.pdf + def bareiss(mat, cumm=1): + if mat.rows == 0: + return mat.one + elif mat.rows == 1: + return mat[0, 0] + + # find a pivot and extract the remaining matrix + # With the default iszerofunc, _find_reasonable_pivot slows down + # the computation by the factor of 2.5 in one test. + # Relevant issues: #10279 and #13877. + pivot_pos, pivot_val, _, _ = _find_reasonable_pivot(mat[:, 0], iszerofunc=iszerofunc) + if pivot_pos is None: + return mat.zero + + # if we have a valid pivot, we'll do a "row swap", so keep the + # sign of the det + sign = (-1) ** (pivot_pos % 2) + + # we want every row but the pivot row and every column + rows = [i for i in range(mat.rows) if i != pivot_pos] + cols = list(range(mat.cols)) + tmp_mat = mat.extract(rows, cols) + + def entry(i, j): + ret = (pivot_val*tmp_mat[i, j + 1] - mat[pivot_pos, j + 1]*tmp_mat[i, 0]) / cumm + if _get_intermediate_simp_bool(True): + return _dotprodsimp(ret) + elif not ret.is_Atom: + return cancel(ret) + return ret + + return sign*bareiss(M._new(mat.rows - 1, mat.cols - 1, entry), pivot_val) + + if not M.is_square: + raise NonSquareMatrixError() + + if M.rows == 0: + return M.one + # sympy/matrices/tests/test_matrices.py contains a test that + # suggests that the determinant of a 0 x 0 matrix is one, by + # convention. + + return bareiss(M) + + +def _det_berkowitz(M): + """ Use the Berkowitz algorithm to compute the determinant.""" + + if not M.is_square: + raise NonSquareMatrixError() + + if M.rows == 0: + return M.one + # sympy/matrices/tests/test_matrices.py contains a test that + # suggests that the determinant of a 0 x 0 matrix is one, by + # convention. + + berk_vector = _berkowitz_vector(M) + return (-1)**(len(berk_vector) - 1) * berk_vector[-1] + + +# This functions is a candidate for caching if it gets implemented for matrices. +def _det_LU(M, iszerofunc=_iszero, simpfunc=None): + """ Computes the determinant of a matrix from its LU decomposition. + This function uses the LU decomposition computed by + LUDecomposition_Simple(). + + The keyword arguments iszerofunc and simpfunc are passed to + LUDecomposition_Simple(). + iszerofunc is a callable that returns a boolean indicating if its + input is zero, or None if it cannot make the determination. + simpfunc is a callable that simplifies its input. + The default is simpfunc=None, which indicate that the pivot search + algorithm should not attempt to simplify any candidate pivots. + If simpfunc fails to simplify its input, then it must return its input + instead of a copy. + + Parameters + ========== + + iszerofunc : function, optional + The function to use to determine zeros when doing an LU decomposition. + Defaults to ``lambda x: x.is_zero``. + + simpfunc : function, optional + The simplification function to use when looking for zeros for pivots. + """ + + if not M.is_square: + raise NonSquareMatrixError() + + if M.rows == 0: + return M.one + # sympy/matrices/tests/test_matrices.py contains a test that + # suggests that the determinant of a 0 x 0 matrix is one, by + # convention. + + lu, row_swaps = M.LUdecomposition_Simple(iszerofunc=iszerofunc, + simpfunc=simpfunc) + # P*A = L*U => det(A) = det(L)*det(U)/det(P) = det(P)*det(U). + # Lower triangular factor L encoded in lu has unit diagonal => det(L) = 1. + # P is a permutation matrix => det(P) in {-1, 1} => 1/det(P) = det(P). + # LUdecomposition_Simple() returns a list of row exchange index pairs, rather + # than a permutation matrix, but det(P) = (-1)**len(row_swaps). + + # Avoid forming the potentially time consuming product of U's diagonal entries + # if the product is zero. + # Bottom right entry of U is 0 => det(A) = 0. + # It may be impossible to determine if this entry of U is zero when it is symbolic. + if iszerofunc(lu[lu.rows-1, lu.rows-1]): + return M.zero + + # Compute det(P) + det = -M.one if len(row_swaps)%2 else M.one + + # Compute det(U) by calculating the product of U's diagonal entries. + # The upper triangular portion of lu is the upper triangular portion of the + # U factor in the LU decomposition. + for k in range(lu.rows): + det *= lu[k, k] + + # return det(P)*det(U) + return det + + +@cacheit +def __det_laplace(M): + """Compute the determinant of a matrix using Laplace expansion. + + This is a recursive function, and it should not be called directly. + Use _det_laplace() instead. The reason for splitting this function + into two is to allow caching of determinants of submatrices. While + one could also define this function inside _det_laplace(), that + would remove the advantage of using caching in Cramer Solve. + """ + n = M.shape[0] + if n == 1: + return M[0] + elif n == 2: + return M[0, 0] * M[1, 1] - M[0, 1] * M[1, 0] + else: + return sum((-1) ** i * M[0, i] * + __det_laplace(M.minor_submatrix(0, i)) for i in range(n)) + + +def _det_laplace(M): + """Compute the determinant of a matrix using Laplace expansion. + + While Laplace expansion is not the most efficient method of computing + a determinant, it is a simple one, and it has the advantage of + being division free. To improve efficiency, this function uses + caching to avoid recomputing determinants of submatrices. + """ + if not M.is_square: + raise NonSquareMatrixError() + if M.shape[0] == 0: + return M.one + # sympy/matrices/tests/test_matrices.py contains a test that + # suggests that the determinant of a 0 x 0 matrix is one, by + # convention. + return __det_laplace(M.as_immutable()) + + +def _det_bird(M): + r"""Compute the determinant of a matrix using Bird's algorithm. + + Bird's algorithm is a simple division-free algorithm for computing, which + is of lower order than the Laplace's algorithm. It is described in [1]_. + + References + ========== + + .. [1] Bird, R. S. (2011). A simple division-free algorithm for computing + determinants. Inf. Process. Lett., 111(21), 1072-1074. doi: + 10.1016/j.ipl.2011.08.006 + """ + def mu(X): + n = X.shape[0] + zero = X.domain.zero + + total = zero + diag_sums = [zero] + for i in reversed(range(1, n)): + total -= X[i][i] + diag_sums.append(total) + diag_sums = diag_sums[::-1] + + elems = [[zero] * i + [diag_sums[i]] + X_i[i + 1:] for i, X_i in + enumerate(X)] + return DDM(elems, X.shape, X.domain) + + Mddm = M._rep.to_ddm() + n = M.shape[0] + if n == 0: + return M.one + # sympy/matrices/tests/test_matrices.py contains a test that + # suggests that the determinant of a 0 x 0 matrix is one, by + # convention. + Fn1 = Mddm + for _ in range(n - 1): + Fn1 = mu(Fn1).matmul(Mddm) + detA = Fn1[0][0] + if n % 2 == 0: + detA = -detA + + return Mddm.domain.to_sympy(detA) + + +def _minor(M, i, j, method="berkowitz"): + """Return the (i,j) minor of ``M``. That is, + return the determinant of the matrix obtained by deleting + the `i`th row and `j`th column from ``M``. + + Parameters + ========== + + i, j : int + The row and column to exclude to obtain the submatrix. + + method : string, optional + Method to use to find the determinant of the submatrix, can be + "bareiss", "berkowitz", "bird", "laplace" or "lu". + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> M.minor(1, 1) + -12 + + See Also + ======== + + minor_submatrix + cofactor + det + """ + + if not M.is_square: + raise NonSquareMatrixError() + + return M.minor_submatrix(i, j).det(method=method) + + +def _minor_submatrix(M, i, j): + """Return the submatrix obtained by removing the `i`th row + and `j`th column from ``M`` (works with Pythonic negative indices). + + Parameters + ========== + + i, j : int + The row and column to exclude to obtain the submatrix. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> M.minor_submatrix(1, 1) + Matrix([ + [1, 3], + [7, 9]]) + + See Also + ======== + + minor + cofactor + """ + + if i < 0: + i += M.rows + if j < 0: + j += M.cols + + if not 0 <= i < M.rows or not 0 <= j < M.cols: + raise ValueError("`i` and `j` must satisfy 0 <= i < ``M.rows`` " + "(%d)" % M.rows + "and 0 <= j < ``M.cols`` (%d)." % M.cols) + + rows = [a for a in range(M.rows) if a != i] + cols = [a for a in range(M.cols) if a != j] + + return M.extract(rows, cols) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/eigen.py b/venv/lib/python3.11/site-packages/sympy/matrices/eigen.py new file mode 100644 index 0000000000000000000000000000000000000000..87b2418efcece1c0b158ec56995bb011286feb3c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/eigen.py @@ -0,0 +1,1346 @@ +from types import FunctionType +from collections import Counter + +from mpmath import mp, workprec +from mpmath.libmp.libmpf import prec_to_dps + +from sympy.core.sorting import default_sort_key +from sympy.core.evalf import DEFAULT_MAXPREC, PrecisionExhausted +from sympy.core.logic import fuzzy_and, fuzzy_or +from sympy.core.numbers import Float +from sympy.core.sympify import _sympify +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.polys import roots, CRootOf, ZZ, QQ, EX +from sympy.polys.matrices import DomainMatrix +from sympy.polys.matrices.eigen import dom_eigenvects, dom_eigenvects_to_sympy +from sympy.polys.polytools import gcd + +from .exceptions import MatrixError, NonSquareMatrixError +from .determinant import _find_reasonable_pivot + +from .utilities import _iszero, _simplify + + +__doctest_requires__ = { + ('_is_indefinite', + '_is_negative_definite', + '_is_negative_semidefinite', + '_is_positive_definite', + '_is_positive_semidefinite'): ['matplotlib'], +} + + +def _eigenvals_eigenvects_mpmath(M): + norm2 = lambda v: mp.sqrt(sum(i**2 for i in v)) + + v1 = None + prec = max(x._prec for x in M.atoms(Float)) + eps = 2**-prec + + while prec < DEFAULT_MAXPREC: + with workprec(prec): + A = mp.matrix(M.evalf(n=prec_to_dps(prec))) + E, ER = mp.eig(A) + v2 = norm2([i for e in E for i in (mp.re(e), mp.im(e))]) + if v1 is not None and mp.fabs(v1 - v2) < eps: + return E, ER + v1 = v2 + prec *= 2 + + # we get here because the next step would have taken us + # past MAXPREC or because we never took a step; in case + # of the latter, we refuse to send back a solution since + # it would not have been verified; we also resist taking + # a small step to arrive exactly at MAXPREC since then + # the two calculations might be artificially close. + raise PrecisionExhausted + + +def _eigenvals_mpmath(M, multiple=False): + """Compute eigenvalues using mpmath""" + E, _ = _eigenvals_eigenvects_mpmath(M) + result = [_sympify(x) for x in E] + if multiple: + return result + return dict(Counter(result)) + + +def _eigenvects_mpmath(M): + E, ER = _eigenvals_eigenvects_mpmath(M) + result = [] + for i in range(M.rows): + eigenval = _sympify(E[i]) + eigenvect = _sympify(ER[:, i]) + result.append((eigenval, 1, [eigenvect])) + + return result + + +# This function is a candidate for caching if it gets implemented for matrices. +def _eigenvals( + M, error_when_incomplete=True, *, simplify=False, multiple=False, + rational=False, **flags): + r"""Compute eigenvalues of the matrix. + + Parameters + ========== + + error_when_incomplete : bool, optional + If it is set to ``True``, it will raise an error if not all + eigenvalues are computed. This is caused by ``roots`` not returning + a full list of eigenvalues. + + simplify : bool or function, optional + If it is set to ``True``, it attempts to return the most + simplified form of expressions returned by applying default + simplification method in every routine. + + If it is set to ``False``, it will skip simplification in this + particular routine to save computation resources. + + If a function is passed to, it will attempt to apply + the particular function as simplification method. + + rational : bool, optional + If it is set to ``True``, every floating point numbers would be + replaced with rationals before computation. It can solve some + issues of ``roots`` routine not working well with floats. + + multiple : bool, optional + If it is set to ``True``, the result will be in the form of a + list. + + If it is set to ``False``, the result will be in the form of a + dictionary. + + Returns + ======= + + eigs : list or dict + Eigenvalues of a matrix. The return format would be specified by + the key ``multiple``. + + Raises + ====== + + MatrixError + If not enough roots had got computed. + + NonSquareMatrixError + If attempted to compute eigenvalues from a non-square matrix. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix(3, 3, [0, 1, 1, 1, 0, 0, 1, 1, 1]) + >>> M.eigenvals() + {-1: 1, 0: 1, 2: 1} + + See Also + ======== + + MatrixBase.charpoly + eigenvects + + Notes + ===== + + Eigenvalues of a matrix $A$ can be computed by solving a matrix + equation $\det(A - \lambda I) = 0$ + + It's not always possible to return radical solutions for + eigenvalues for matrices larger than $4, 4$ shape due to + Abel-Ruffini theorem. + + If there is no radical solution is found for the eigenvalue, + it may return eigenvalues in the form of + :class:`sympy.polys.rootoftools.ComplexRootOf`. + """ + if not M: + if multiple: + return [] + return {} + + if not M.is_square: + raise NonSquareMatrixError("{} must be a square matrix.".format(M)) + + if M._rep.domain not in (ZZ, QQ): + # Skip this check for ZZ/QQ because it can be slow + if all(x.is_number for x in M) and M.has(Float): + return _eigenvals_mpmath(M, multiple=multiple) + + if rational: + from sympy.simplify import nsimplify + M = M.applyfunc( + lambda x: nsimplify(x, rational=True) if x.has(Float) else x) + + if multiple: + return _eigenvals_list( + M, error_when_incomplete=error_when_incomplete, simplify=simplify, + **flags) + return _eigenvals_dict( + M, error_when_incomplete=error_when_incomplete, simplify=simplify, + **flags) + + +eigenvals_error_message = \ +"It is not always possible to express the eigenvalues of a matrix " + \ +"of size 5x5 or higher in radicals. " + \ +"We have CRootOf, but domains other than the rationals are not " + \ +"currently supported. " + \ +"If there are no symbols in the matrix, " + \ +"it should still be possible to compute numeric approximations " + \ +"of the eigenvalues using " + \ +"M.evalf().eigenvals() or M.charpoly().nroots()." + + +def _eigenvals_list( + M, error_when_incomplete=True, simplify=False, **flags): + iblocks = M.strongly_connected_components() + all_eigs = [] + is_dom = M._rep.domain in (ZZ, QQ) + for b in iblocks: + + # Fast path for a 1x1 block: + if is_dom and len(b) == 1: + index = b[0] + val = M[index, index] + all_eigs.append(val) + continue + + block = M[b, b] + + if isinstance(simplify, FunctionType): + charpoly = block.charpoly(simplify=simplify) + else: + charpoly = block.charpoly() + + eigs = roots(charpoly, multiple=True, **flags) + + if len(eigs) != block.rows: + try: + eigs = charpoly.all_roots(multiple=True) + except NotImplementedError: + if error_when_incomplete: + raise MatrixError(eigenvals_error_message) + else: + eigs = [] + + all_eigs += eigs + + if not simplify: + return all_eigs + if not isinstance(simplify, FunctionType): + simplify = _simplify + return [simplify(value) for value in all_eigs] + + +def _eigenvals_dict( + M, error_when_incomplete=True, simplify=False, **flags): + iblocks = M.strongly_connected_components() + all_eigs = {} + is_dom = M._rep.domain in (ZZ, QQ) + for b in iblocks: + + # Fast path for a 1x1 block: + if is_dom and len(b) == 1: + index = b[0] + val = M[index, index] + all_eigs[val] = all_eigs.get(val, 0) + 1 + continue + + block = M[b, b] + + if isinstance(simplify, FunctionType): + charpoly = block.charpoly(simplify=simplify) + else: + charpoly = block.charpoly() + + eigs = roots(charpoly, multiple=False, **flags) + + if sum(eigs.values()) != block.rows: + try: + eigs = dict(charpoly.all_roots(multiple=False)) + except NotImplementedError: + if error_when_incomplete: + raise MatrixError(eigenvals_error_message) + else: + eigs = {} + + for k, v in eigs.items(): + if k in all_eigs: + all_eigs[k] += v + else: + all_eigs[k] = v + + if not simplify: + return all_eigs + if not isinstance(simplify, FunctionType): + simplify = _simplify + return {simplify(key): value for key, value in all_eigs.items()} + + +def _eigenspace(M, eigenval, iszerofunc=_iszero, simplify=False): + """Get a basis for the eigenspace for a particular eigenvalue""" + m = M - M.eye(M.rows) * eigenval + ret = m.nullspace(iszerofunc=iszerofunc) + + # The nullspace for a real eigenvalue should be non-trivial. + # If we didn't find an eigenvector, try once more a little harder + if len(ret) == 0 and simplify: + ret = m.nullspace(iszerofunc=iszerofunc, simplify=True) + if len(ret) == 0: + raise NotImplementedError( + "Can't evaluate eigenvector for eigenvalue {}".format(eigenval)) + return ret + + +def _eigenvects_DOM(M, **kwargs): + DOM = DomainMatrix.from_Matrix(M, field=True, extension=True) + DOM = DOM.to_dense() + + if DOM.domain != EX: + rational, algebraic = dom_eigenvects(DOM) + eigenvects = dom_eigenvects_to_sympy( + rational, algebraic, M.__class__, **kwargs) + eigenvects = sorted(eigenvects, key=lambda x: default_sort_key(x[0])) + + return eigenvects + return None + + +def _eigenvects_sympy(M, iszerofunc, simplify=True, **flags): + eigenvals = M.eigenvals(rational=False, **flags) + + # Make sure that we have all roots in radical form + for x in eigenvals: + if x.has(CRootOf): + raise MatrixError( + "Eigenvector computation is not implemented if the matrix have " + "eigenvalues in CRootOf form") + + eigenvals = sorted(eigenvals.items(), key=default_sort_key) + ret = [] + for val, mult in eigenvals: + vects = _eigenspace(M, val, iszerofunc=iszerofunc, simplify=simplify) + ret.append((val, mult, vects)) + return ret + + +# This functions is a candidate for caching if it gets implemented for matrices. +def _eigenvects(M, error_when_incomplete=True, iszerofunc=_iszero, *, chop=False, **flags): + """Compute eigenvectors of the matrix. + + Parameters + ========== + + error_when_incomplete : bool, optional + Raise an error when not all eigenvalues are computed. This is + caused by ``roots`` not returning a full list of eigenvalues. + + iszerofunc : function, optional + Specifies a zero testing function to be used in ``rref``. + + Default value is ``_iszero``, which uses SymPy's naive and fast + default assumption handler. + + It can also accept any user-specified zero testing function, if it + is formatted as a function which accepts a single symbolic argument + and returns ``True`` if it is tested as zero and ``False`` if it + is tested as non-zero, and ``None`` if it is undecidable. + + simplify : bool or function, optional + If ``True``, ``as_content_primitive()`` will be used to tidy up + normalization artifacts. + + It will also be used by the ``nullspace`` routine. + + chop : bool or positive number, optional + If the matrix contains any Floats, they will be changed to Rationals + for computation purposes, but the answers will be returned after + being evaluated with evalf. The ``chop`` flag is passed to ``evalf``. + When ``chop=True`` a default precision will be used; a number will + be interpreted as the desired level of precision. + + Returns + ======= + + ret : [(eigenval, multiplicity, eigenspace), ...] + A ragged list containing tuples of data obtained by ``eigenvals`` + and ``nullspace``. + + ``eigenspace`` is a list containing the ``eigenvector`` for each + eigenvalue. + + ``eigenvector`` is a vector in the form of a ``Matrix``. e.g. + a vector of length 3 is returned as ``Matrix([a_1, a_2, a_3])``. + + Raises + ====== + + NotImplementedError + If failed to compute nullspace. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix(3, 3, [0, 1, 1, 1, 0, 0, 1, 1, 1]) + >>> M.eigenvects() + [(-1, 1, [Matrix([ + [-1], + [ 1], + [ 0]])]), (0, 1, [Matrix([ + [ 0], + [-1], + [ 1]])]), (2, 1, [Matrix([ + [2/3], + [1/3], + [ 1]])])] + + See Also + ======== + + eigenvals + MatrixBase.nullspace + """ + simplify = flags.get('simplify', True) + primitive = flags.get('simplify', False) + flags.pop('simplify', None) # remove this if it's there + flags.pop('multiple', None) # remove this if it's there + + if not isinstance(simplify, FunctionType): + simpfunc = _simplify if simplify else lambda x: x + + has_floats = M.has(Float) + if has_floats: + if all(x.is_number for x in M): + return _eigenvects_mpmath(M) + from sympy.simplify import nsimplify + M = M.applyfunc(lambda x: nsimplify(x, rational=True)) + + ret = _eigenvects_DOM(M) + if ret is None: + ret = _eigenvects_sympy(M, iszerofunc, simplify=simplify, **flags) + + if primitive: + # if the primitive flag is set, get rid of any common + # integer denominators + def denom_clean(l): + return [(v / gcd(list(v))).applyfunc(simpfunc) for v in l] + + ret = [(val, mult, denom_clean(es)) for val, mult, es in ret] + + if has_floats: + # if we had floats to start with, turn the eigenvectors to floats + ret = [(val.evalf(chop=chop), mult, [v.evalf(chop=chop) for v in es]) + for val, mult, es in ret] + + return ret + + +def _is_diagonalizable_with_eigen(M, reals_only=False): + """See _is_diagonalizable. This function returns the bool along with the + eigenvectors to avoid calculating them again in functions like + ``diagonalize``.""" + + if not M.is_square: + return False, [] + + eigenvecs = M.eigenvects(simplify=True) + + for val, mult, basis in eigenvecs: + if reals_only and not val.is_real: # if we have a complex eigenvalue + return False, eigenvecs + + if mult != len(basis): # if the geometric multiplicity doesn't equal the algebraic + return False, eigenvecs + + return True, eigenvecs + +def _is_diagonalizable(M, reals_only=False, **kwargs): + """Returns ``True`` if a matrix is diagonalizable. + + Parameters + ========== + + reals_only : bool, optional + If ``True``, it tests whether the matrix can be diagonalized + to contain only real numbers on the diagonal. + + + If ``False``, it tests whether the matrix can be diagonalized + at all, even with numbers that may not be real. + + Examples + ======== + + Example of a diagonalizable matrix: + + >>> from sympy import Matrix + >>> M = Matrix([[1, 2, 0], [0, 3, 0], [2, -4, 2]]) + >>> M.is_diagonalizable() + True + + Example of a non-diagonalizable matrix: + + >>> M = Matrix([[0, 1], [0, 0]]) + >>> M.is_diagonalizable() + False + + Example of a matrix that is diagonalized in terms of non-real entries: + + >>> M = Matrix([[0, 1], [-1, 0]]) + >>> M.is_diagonalizable(reals_only=False) + True + >>> M.is_diagonalizable(reals_only=True) + False + + See Also + ======== + + sympy.matrices.matrixbase.MatrixBase.is_diagonal + diagonalize + """ + if not M.is_square: + return False + + if all(e.is_real for e in M) and M.is_symmetric(): + return True + + if all(e.is_complex for e in M) and M.is_hermitian: + return True + + return _is_diagonalizable_with_eigen(M, reals_only=reals_only)[0] + + +#G&VL, Matrix Computations, Algo 5.4.2 +def _householder_vector(x): + if not x.cols == 1: + raise ValueError("Input must be a column matrix") + v = x.copy() + v_plus = x.copy() + v_minus = x.copy() + q = x[0, 0] / abs(x[0, 0]) + norm_x = x.norm() + v_plus[0, 0] = x[0, 0] + q * norm_x + v_minus[0, 0] = x[0, 0] - q * norm_x + if x[1:, 0].norm() == 0: + bet = 0 + v[0, 0] = 1 + else: + if v_plus.norm() <= v_minus.norm(): + v = v_plus + else: + v = v_minus + v = v / v[0] + bet = 2 / (v.norm() ** 2) + return v, bet + + +def _bidiagonal_decmp_hholder(M): + m = M.rows + n = M.cols + A = M.as_mutable() + U, V = A.eye(m), A.eye(n) + for i in range(min(m, n)): + v, bet = _householder_vector(A[i:, i]) + hh_mat = A.eye(m - i) - bet * v * v.H + A[i:, i:] = hh_mat * A[i:, i:] + temp = A.eye(m) + temp[i:, i:] = hh_mat + U = U * temp + if i + 1 <= n - 2: + v, bet = _householder_vector(A[i, i+1:].T) + hh_mat = A.eye(n - i - 1) - bet * v * v.H + A[i:, i+1:] = A[i:, i+1:] * hh_mat + temp = A.eye(n) + temp[i+1:, i+1:] = hh_mat + V = temp * V + return U, A, V + + +def _eval_bidiag_hholder(M): + m = M.rows + n = M.cols + A = M.as_mutable() + for i in range(min(m, n)): + v, bet = _householder_vector(A[i:, i]) + hh_mat = A.eye(m-i) - bet * v * v.H + A[i:, i:] = hh_mat * A[i:, i:] + if i + 1 <= n - 2: + v, bet = _householder_vector(A[i, i+1:].T) + hh_mat = A.eye(n - i - 1) - bet * v * v.H + A[i:, i+1:] = A[i:, i+1:] * hh_mat + return A + + +def _bidiagonal_decomposition(M, upper=True): + """ + Returns $(U,B,V.H)$ for + + $$A = UBV^{H}$$ + + where $A$ is the input matrix, and $B$ is its Bidiagonalized form + + Note: Bidiagonal Computation can hang for symbolic matrices. + + Parameters + ========== + + upper : bool. Whether to do upper bidiagnalization or lower. + True for upper and False for lower. + + References + ========== + + .. [1] Algorithm 5.4.2, Matrix computations by Golub and Van Loan, 4th edition + .. [2] Complex Matrix Bidiagonalization, https://github.com/vslobody/Householder-Bidiagonalization + + """ + + if not isinstance(upper, bool): + raise ValueError("upper must be a boolean") + + if upper: + return _bidiagonal_decmp_hholder(M) + + X = _bidiagonal_decmp_hholder(M.H) + return X[2].H, X[1].H, X[0].H + + +def _bidiagonalize(M, upper=True): + """ + Returns $B$, the Bidiagonalized form of the input matrix. + + Note: Bidiagonal Computation can hang for symbolic matrices. + + Parameters + ========== + + upper : bool. Whether to do upper bidiagnalization or lower. + True for upper and False for lower. + + References + ========== + + .. [1] Algorithm 5.4.2, Matrix computations by Golub and Van Loan, 4th edition + .. [2] Complex Matrix Bidiagonalization : https://github.com/vslobody/Householder-Bidiagonalization + + """ + + if not isinstance(upper, bool): + raise ValueError("upper must be a boolean") + + if upper: + return _eval_bidiag_hholder(M) + return _eval_bidiag_hholder(M.H).H + + +def _diagonalize(M, reals_only=False, sort=False, normalize=False): + """ + Return (P, D), where D is diagonal and + + D = P^-1 * M * P + + where M is current matrix. + + Parameters + ========== + + reals_only : bool. Whether to throw an error if complex numbers are need + to diagonalize. (Default: False) + + sort : bool. Sort the eigenvalues along the diagonal. (Default: False) + + normalize : bool. If True, normalize the columns of P. (Default: False) + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix(3, 3, [1, 2, 0, 0, 3, 0, 2, -4, 2]) + >>> M + Matrix([ + [1, 2, 0], + [0, 3, 0], + [2, -4, 2]]) + >>> (P, D) = M.diagonalize() + >>> D + Matrix([ + [1, 0, 0], + [0, 2, 0], + [0, 0, 3]]) + >>> P + Matrix([ + [-1, 0, -1], + [ 0, 0, -1], + [ 2, 1, 2]]) + >>> P.inv() * M * P + Matrix([ + [1, 0, 0], + [0, 2, 0], + [0, 0, 3]]) + + See Also + ======== + + sympy.matrices.matrixbase.MatrixBase.is_diagonal + is_diagonalizable + """ + + if not M.is_square: + raise NonSquareMatrixError() + + is_diagonalizable, eigenvecs = _is_diagonalizable_with_eigen(M, + reals_only=reals_only) + + if not is_diagonalizable: + raise MatrixError("Matrix is not diagonalizable") + + if sort: + eigenvecs = sorted(eigenvecs, key=default_sort_key) + + p_cols, diag = [], [] + + for val, mult, basis in eigenvecs: + diag += [val] * mult + p_cols += basis + + if normalize: + p_cols = [v / v.norm() for v in p_cols] + + return M.hstack(*p_cols), M.diag(*diag) + + +def _fuzzy_positive_definite(M): + positive_diagonals = M._has_positive_diagonals() + if positive_diagonals is False: + return False + + if positive_diagonals and M.is_strongly_diagonally_dominant: + return True + + return None + + +def _fuzzy_positive_semidefinite(M): + nonnegative_diagonals = M._has_nonnegative_diagonals() + if nonnegative_diagonals is False: + return False + + if nonnegative_diagonals and M.is_weakly_diagonally_dominant: + return True + + return None + + +def _is_positive_definite(M): + if not M.is_hermitian: + if not M.is_square: + return False + M = M + M.H + + fuzzy = _fuzzy_positive_definite(M) + if fuzzy is not None: + return fuzzy + + return _is_positive_definite_GE(M) + + +def _is_positive_semidefinite(M): + if not M.is_hermitian: + if not M.is_square: + return False + M = M + M.H + + fuzzy = _fuzzy_positive_semidefinite(M) + if fuzzy is not None: + return fuzzy + + return _is_positive_semidefinite_cholesky(M) + + +def _is_negative_definite(M): + return _is_positive_definite(-M) + + +def _is_negative_semidefinite(M): + return _is_positive_semidefinite(-M) + + +def _is_indefinite(M): + if M.is_hermitian: + eigen = M.eigenvals() + args1 = [x.is_positive for x in eigen.keys()] + any_positive = fuzzy_or(args1) + args2 = [x.is_negative for x in eigen.keys()] + any_negative = fuzzy_or(args2) + + return fuzzy_and([any_positive, any_negative]) + + elif M.is_square: + return (M + M.H).is_indefinite + + return False + + +def _is_positive_definite_GE(M): + """A division-free gaussian elimination method for testing + positive-definiteness.""" + M = M.as_mutable() + size = M.rows + + for i in range(size): + is_positive = M[i, i].is_positive + if is_positive is not True: + return is_positive + for j in range(i+1, size): + M[j, i+1:] = M[i, i] * M[j, i+1:] - M[j, i] * M[i, i+1:] + return True + + +def _is_positive_semidefinite_cholesky(M): + """Uses Cholesky factorization with complete pivoting + + References + ========== + + .. [1] http://eprints.ma.man.ac.uk/1199/1/covered/MIMS_ep2008_116.pdf + + .. [2] https://www.value-at-risk.net/cholesky-factorization/ + """ + M = M.as_mutable() + for k in range(M.rows): + diags = [M[i, i] for i in range(k, M.rows)] + pivot, pivot_val, nonzero, _ = _find_reasonable_pivot(diags) + + if nonzero: + return None + + if pivot is None: + for i in range(k+1, M.rows): + for j in range(k, M.cols): + iszero = M[i, j].is_zero + if iszero is None: + return None + elif iszero is False: + return False + return True + + if M[k, k].is_negative or pivot_val.is_negative: + return False + elif not (M[k, k].is_nonnegative and pivot_val.is_nonnegative): + return None + + if pivot > 0: + M.col_swap(k, k+pivot) + M.row_swap(k, k+pivot) + + M[k, k] = sqrt(M[k, k]) + M[k, k+1:] /= M[k, k] + M[k+1:, k+1:] -= M[k, k+1:].H * M[k, k+1:] + + return M[-1, -1].is_nonnegative + + +_doc_positive_definite = \ + r"""Finds out the definiteness of a matrix. + + Explanation + =========== + + A square real matrix $A$ is: + + - A positive definite matrix if $x^T A x > 0$ + for all non-zero real vectors $x$. + - A positive semidefinite matrix if $x^T A x \geq 0$ + for all non-zero real vectors $x$. + - A negative definite matrix if $x^T A x < 0$ + for all non-zero real vectors $x$. + - A negative semidefinite matrix if $x^T A x \leq 0$ + for all non-zero real vectors $x$. + - An indefinite matrix if there exists non-zero real vectors + $x, y$ with $x^T A x > 0 > y^T A y$. + + A square complex matrix $A$ is: + + - A positive definite matrix if $\text{re}(x^H A x) > 0$ + for all non-zero complex vectors $x$. + - A positive semidefinite matrix if $\text{re}(x^H A x) \geq 0$ + for all non-zero complex vectors $x$. + - A negative definite matrix if $\text{re}(x^H A x) < 0$ + for all non-zero complex vectors $x$. + - A negative semidefinite matrix if $\text{re}(x^H A x) \leq 0$ + for all non-zero complex vectors $x$. + - An indefinite matrix if there exists non-zero complex vectors + $x, y$ with $\text{re}(x^H A x) > 0 > \text{re}(y^H A y)$. + + A matrix need not be symmetric or hermitian to be positive definite. + + - A real non-symmetric matrix is positive definite if and only if + $\frac{A + A^T}{2}$ is positive definite. + - A complex non-hermitian matrix is positive definite if and only if + $\frac{A + A^H}{2}$ is positive definite. + + And this extension can apply for all the definitions above. + + However, for complex cases, you can restrict the definition of + $\text{re}(x^H A x) > 0$ to $x^H A x > 0$ and require the matrix + to be hermitian. + But we do not present this restriction for computation because you + can check ``M.is_hermitian`` independently with this and use + the same procedure. + + Examples + ======== + + An example of symmetric positive definite matrix: + + .. plot:: + :context: reset + :format: doctest + :include-source: True + + >>> from sympy import Matrix, symbols + >>> from sympy.plotting import plot3d + >>> a, b = symbols('a b') + >>> x = Matrix([a, b]) + + >>> A = Matrix([[1, 0], [0, 1]]) + >>> A.is_positive_definite + True + >>> A.is_positive_semidefinite + True + + >>> p = plot3d((x.T*A*x)[0, 0], (a, -1, 1), (b, -1, 1)) + + An example of symmetric positive semidefinite matrix: + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> A = Matrix([[1, -1], [-1, 1]]) + >>> A.is_positive_definite + False + >>> A.is_positive_semidefinite + True + + >>> p = plot3d((x.T*A*x)[0, 0], (a, -1, 1), (b, -1, 1)) + + An example of symmetric negative definite matrix: + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> A = Matrix([[-1, 0], [0, -1]]) + >>> A.is_negative_definite + True + >>> A.is_negative_semidefinite + True + >>> A.is_indefinite + False + + >>> p = plot3d((x.T*A*x)[0, 0], (a, -1, 1), (b, -1, 1)) + + An example of symmetric indefinite matrix: + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> A = Matrix([[1, 2], [2, -1]]) + >>> A.is_indefinite + True + + >>> p = plot3d((x.T*A*x)[0, 0], (a, -1, 1), (b, -1, 1)) + + An example of non-symmetric positive definite matrix. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> A = Matrix([[1, 2], [-2, 1]]) + >>> A.is_positive_definite + True + >>> A.is_positive_semidefinite + True + + >>> p = plot3d((x.T*A*x)[0, 0], (a, -1, 1), (b, -1, 1)) + + Notes + ===== + + Although some people trivialize the definition of positive definite + matrices only for symmetric or hermitian matrices, this restriction + is not correct because it does not classify all instances of + positive definite matrices from the definition $x^T A x > 0$ or + $\text{re}(x^H A x) > 0$. + + For instance, ``Matrix([[1, 2], [-2, 1]])`` presented in + the example above is an example of real positive definite matrix + that is not symmetric. + + However, since the following formula holds true; + + .. math:: + \text{re}(x^H A x) > 0 \iff + \text{re}(x^H \frac{A + A^H}{2} x) > 0 + + We can classify all positive definite matrices that may or may not + be symmetric or hermitian by transforming the matrix to + $\frac{A + A^T}{2}$ or $\frac{A + A^H}{2}$ + (which is guaranteed to be always real symmetric or complex + hermitian) and we can defer most of the studies to symmetric or + hermitian positive definite matrices. + + But it is a different problem for the existence of Cholesky + decomposition. Because even though a non symmetric or a non + hermitian matrix can be positive definite, Cholesky or LDL + decomposition does not exist because the decompositions require the + matrix to be symmetric or hermitian. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Definiteness_of_a_matrix#Eigenvalues + + .. [2] https://mathworld.wolfram.com/PositiveDefiniteMatrix.html + + .. [3] Johnson, C. R. "Positive Definite Matrices." Amer. + Math. Monthly 77, 259-264 1970. + """ + +_is_positive_definite.__doc__ = _doc_positive_definite +_is_positive_semidefinite.__doc__ = _doc_positive_definite +_is_negative_definite.__doc__ = _doc_positive_definite +_is_negative_semidefinite.__doc__ = _doc_positive_definite +_is_indefinite.__doc__ = _doc_positive_definite + + +def _jordan_form(M, calc_transform=True, *, chop=False): + """Return $(P, J)$ where $J$ is a Jordan block + matrix and $P$ is a matrix such that $M = P J P^{-1}$ + + Parameters + ========== + + calc_transform : bool + If ``False``, then only $J$ is returned. + + chop : bool + All matrices are converted to exact types when computing + eigenvalues and eigenvectors. As a result, there may be + approximation errors. If ``chop==True``, these errors + will be truncated. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[ 6, 5, -2, -3], [-3, -1, 3, 3], [ 2, 1, -2, -3], [-1, 1, 5, 5]]) + >>> P, J = M.jordan_form() + >>> J + Matrix([ + [2, 1, 0, 0], + [0, 2, 0, 0], + [0, 0, 2, 1], + [0, 0, 0, 2]]) + + See Also + ======== + + jordan_block + """ + + if not M.is_square: + raise NonSquareMatrixError("Only square matrices have Jordan forms") + + mat = M + has_floats = M.has(Float) + + if has_floats: + try: + max_prec = max(term._prec for term in M.values() if isinstance(term, Float)) + except ValueError: + # if no term in the matrix is explicitly a Float calling max() + # will throw a error so setting max_prec to default value of 53 + max_prec = 53 + + # setting minimum max_dps to 15 to prevent loss of precision in + # matrix containing non evaluated expressions + max_dps = max(prec_to_dps(max_prec), 15) + + def restore_floats(*args): + """If ``has_floats`` is `True`, cast all ``args`` as + matrices of floats.""" + + if has_floats: + args = [m.evalf(n=max_dps, chop=chop) for m in args] + if len(args) == 1: + return args[0] + + return args + + # cache calculations for some speedup + mat_cache = {} + + def eig_mat(val, pow): + """Cache computations of ``(M - val*I)**pow`` for quick + retrieval""" + + if (val, pow) in mat_cache: + return mat_cache[(val, pow)] + + if (val, pow - 1) in mat_cache: + mat_cache[(val, pow)] = mat_cache[(val, pow - 1)].multiply( + mat_cache[(val, 1)], dotprodsimp=None) + else: + mat_cache[(val, pow)] = (mat - val*M.eye(M.rows)).pow(pow) + + return mat_cache[(val, pow)] + + # helper functions + def nullity_chain(val, algebraic_multiplicity): + """Calculate the sequence [0, nullity(E), nullity(E**2), ...] + until it is constant where ``E = M - val*I``""" + + # mat.rank() is faster than computing the null space, + # so use the rank-nullity theorem + cols = M.cols + ret = [0] + nullity = cols - eig_mat(val, 1).rank() + i = 2 + + while nullity != ret[-1]: + ret.append(nullity) + + if nullity == algebraic_multiplicity: + break + + nullity = cols - eig_mat(val, i).rank() + i += 1 + + # Due to issues like #7146 and #15872, SymPy sometimes + # gives the wrong rank. In this case, raise an error + # instead of returning an incorrect matrix + if nullity < ret[-1] or nullity > algebraic_multiplicity: + raise MatrixError( + "SymPy had encountered an inconsistent " + "result while computing Jordan block: " + "{}".format(M)) + + return ret + + def blocks_from_nullity_chain(d): + """Return a list of the size of each Jordan block. + If d_n is the nullity of E**n, then the number + of Jordan blocks of size n is + + 2*d_n - d_(n-1) - d_(n+1)""" + + # d[0] is always the number of columns, so skip past it + mid = [2*d[n] - d[n - 1] - d[n + 1] for n in range(1, len(d) - 1)] + # d is assumed to plateau with "d[ len(d) ] == d[-1]", so + # 2*d_n - d_(n-1) - d_(n+1) == d_n - d_(n-1) + end = [d[-1] - d[-2]] if len(d) > 1 else [d[0]] + + return mid + end + + def pick_vec(small_basis, big_basis): + """Picks a vector from big_basis that isn't in + the subspace spanned by small_basis""" + + if len(small_basis) == 0: + return big_basis[0] + + for v in big_basis: + _, pivots = M.hstack(*(small_basis + [v])).echelon_form( + with_pivots=True) + + if pivots[-1] == len(small_basis): + return v + + # roots doesn't like Floats, so replace them with Rationals + if has_floats: + from sympy.simplify import nsimplify + mat = mat.applyfunc(lambda x: nsimplify(x, rational=True)) + + # first calculate the jordan block structure + eigs = mat.eigenvals() + + # Make sure that we have all roots in radical form + for x in eigs: + if x.has(CRootOf): + raise MatrixError( + "Jordan normal form is not implemented if the matrix have " + "eigenvalues in CRootOf form") + + # most matrices have distinct eigenvalues + # and so are diagonalizable. In this case, don't + # do extra work! + if len(eigs.keys()) == mat.cols: + blocks = sorted(eigs.keys(), key=default_sort_key) + jordan_mat = mat.diag(*blocks) + + if not calc_transform: + return restore_floats(jordan_mat) + + jordan_basis = [eig_mat(eig, 1).nullspace()[0] + for eig in blocks] + basis_mat = mat.hstack(*jordan_basis) + + return restore_floats(basis_mat, jordan_mat) + + block_structure = [] + + for eig in sorted(eigs.keys(), key=default_sort_key): + algebraic_multiplicity = eigs[eig] + chain = nullity_chain(eig, algebraic_multiplicity) + block_sizes = blocks_from_nullity_chain(chain) + + # if block_sizes = = [a, b, c, ...], then the number of + # Jordan blocks of size 1 is a, of size 2 is b, etc. + # create an array that has (eig, block_size) with one + # entry for each block + size_nums = [(i+1, num) for i, num in enumerate(block_sizes)] + + # we expect larger Jordan blocks to come earlier + size_nums.reverse() + + block_structure.extend( + [(eig, size) for size, num in size_nums for _ in range(num)]) + + jordan_form_size = sum(size for eig, size in block_structure) + + if jordan_form_size != M.rows: + raise MatrixError( + "SymPy had encountered an inconsistent result while " + "computing Jordan block. : {}".format(M)) + + blocks = (mat.jordan_block(size=size, eigenvalue=eig) for eig, size in block_structure) + jordan_mat = mat.diag(*blocks) + + if not calc_transform: + return restore_floats(jordan_mat) + + # For each generalized eigenspace, calculate a basis. + # We start by looking for a vector in null( (A - eig*I)**n ) + # which isn't in null( (A - eig*I)**(n-1) ) where n is + # the size of the Jordan block + # + # Ideally we'd just loop through block_structure and + # compute each generalized eigenspace. However, this + # causes a lot of unneeded computation. Instead, we + # go through the eigenvalues separately, since we know + # their generalized eigenspaces must have bases that + # are linearly independent. + jordan_basis = [] + + for eig in sorted(eigs.keys(), key=default_sort_key): + eig_basis = [] + + for block_eig, size in block_structure: + if block_eig != eig: + continue + + null_big = (eig_mat(eig, size)).nullspace() + null_small = (eig_mat(eig, size - 1)).nullspace() + + # we want to pick something that is in the big basis + # and not the small, but also something that is independent + # of any other generalized eigenvectors from a different + # generalized eigenspace sharing the same eigenvalue. + vec = pick_vec(null_small + eig_basis, null_big) + new_vecs = [eig_mat(eig, i).multiply(vec, dotprodsimp=None) + for i in range(size)] + + eig_basis.extend(new_vecs) + jordan_basis.extend(reversed(new_vecs)) + + basis_mat = mat.hstack(*jordan_basis) + + return restore_floats(basis_mat, jordan_mat) + + +def _left_eigenvects(M, **flags): + """Returns left eigenvectors and eigenvalues. + + This function returns the list of triples (eigenval, multiplicity, + basis) for the left eigenvectors. Options are the same as for + eigenvects(), i.e. the ``**flags`` arguments gets passed directly to + eigenvects(). + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[0, 1, 1], [1, 0, 0], [1, 1, 1]]) + >>> M.eigenvects() + [(-1, 1, [Matrix([ + [-1], + [ 1], + [ 0]])]), (0, 1, [Matrix([ + [ 0], + [-1], + [ 1]])]), (2, 1, [Matrix([ + [2/3], + [1/3], + [ 1]])])] + >>> M.left_eigenvects() + [(-1, 1, [Matrix([[-2, 1, 1]])]), (0, 1, [Matrix([[-1, -1, 1]])]), (2, + 1, [Matrix([[1, 1, 1]])])] + + """ + + eigs = M.transpose().eigenvects(**flags) + + return [(val, mult, [l.transpose() for l in basis]) for val, mult, basis in eigs] + + +def _singular_values(M): + """Compute the singular values of a Matrix + + Examples + ======== + + >>> from sympy import Matrix, Symbol + >>> x = Symbol('x', real=True) + >>> M = Matrix([[0, 1, 0], [0, x, 0], [-1, 0, 0]]) + >>> M.singular_values() + [sqrt(x**2 + 1), 1, 0] + + See Also + ======== + + condition_number + """ + + if M.rows >= M.cols: + valmultpairs = M.H.multiply(M).eigenvals() + else: + valmultpairs = M.multiply(M.H).eigenvals() + + # Expands result from eigenvals into a simple list + vals = [] + + for k, v in valmultpairs.items(): + vals += [sqrt(k)] * v # dangerous! same k in several spots! + + # Pad with zeros if singular values are computed in reverse way, + # to give consistent format. + if len(vals) < M.cols: + vals += [M.zero] * (M.cols - len(vals)) + + # sort them in descending order + vals.sort(reverse=True, key=default_sort_key) + + return vals diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/exceptions.py b/venv/lib/python3.11/site-packages/sympy/matrices/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..bfc7cfa0bdffd59ff2bc5a9cd85cf9b04ed1a63d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/exceptions.py @@ -0,0 +1,26 @@ +""" +Exceptions raised by the matrix module. +""" + + +class MatrixError(Exception): + pass + + +class ShapeError(ValueError, MatrixError): + """Wrong matrix shape""" + pass + + +class NonSquareMatrixError(ShapeError): + pass + + +class NonInvertibleMatrixError(ValueError, MatrixError): + """The matrix in not invertible (division by multidimensional zero error).""" + pass + + +class NonPositiveDefiniteMatrixError(ValueError, MatrixError): + """The matrix is not a positive-definite matrix.""" + pass diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/__init__.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5f4ab203ab74165d1003cdedd83945ea3fcf8f47 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/__init__.py @@ -0,0 +1,62 @@ +""" A module which handles Matrix Expressions """ + +from .slice import MatrixSlice +from .blockmatrix import BlockMatrix, BlockDiagMatrix, block_collapse, blockcut +from .companion import CompanionMatrix +from .funcmatrix import FunctionMatrix +from .inverse import Inverse +from .matadd import MatAdd +from .matexpr import MatrixExpr, MatrixSymbol, matrix_symbols +from .matmul import MatMul +from .matpow import MatPow +from .trace import Trace, trace +from .determinant import Determinant, det, Permanent, per +from .transpose import Transpose +from .adjoint import Adjoint +from .hadamard import hadamard_product, HadamardProduct, hadamard_power, HadamardPower +from .diagonal import DiagonalMatrix, DiagonalOf, DiagMatrix, diagonalize_vector +from .dotproduct import DotProduct +from .kronecker import kronecker_product, KroneckerProduct, combine_kronecker +from .permutation import PermutationMatrix, MatrixPermute +from .sets import MatrixSet +from .special import ZeroMatrix, Identity, OneMatrix + +__all__ = [ + 'MatrixSlice', + + 'BlockMatrix', 'BlockDiagMatrix', 'block_collapse', 'blockcut', + 'FunctionMatrix', + + 'CompanionMatrix', + + 'Inverse', + + 'MatAdd', + + 'Identity', 'MatrixExpr', 'MatrixSymbol', 'ZeroMatrix', 'OneMatrix', + 'matrix_symbols', 'MatrixSet', + + 'MatMul', + + 'MatPow', + + 'Trace', 'trace', + + 'Determinant', 'det', + + 'Transpose', + + 'Adjoint', + + 'hadamard_product', 'HadamardProduct', 'hadamard_power', 'HadamardPower', + + 'DiagonalMatrix', 'DiagonalOf', 'DiagMatrix', 'diagonalize_vector', + + 'DotProduct', + + 'kronecker_product', 'KroneckerProduct', 'combine_kronecker', + + 'PermutationMatrix', 'MatrixPermute', + + 'Permanent', 'per' +] diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/_shape.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/_shape.py new file mode 100644 index 0000000000000000000000000000000000000000..a95d481bf8e1edf4c62992044cd50563b335caac --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/_shape.py @@ -0,0 +1,102 @@ +from sympy.core.relational import Eq +from sympy.core.expr import Expr +from sympy.core.numbers import Integer +from sympy.logic.boolalg import Boolean, And +from sympy.matrices.expressions.matexpr import MatrixExpr +from sympy.matrices.exceptions import ShapeError +from typing import Union + + +def is_matadd_valid(*args: MatrixExpr) -> Boolean: + """Return the symbolic condition how ``MatAdd``, ``HadamardProduct`` + makes sense. + + Parameters + ========== + + args + The list of arguments of matrices to be tested for. + + Examples + ======== + + >>> from sympy import MatrixSymbol, symbols + >>> from sympy.matrices.expressions._shape import is_matadd_valid + + >>> m, n, p, q = symbols('m n p q') + >>> A = MatrixSymbol('A', m, n) + >>> B = MatrixSymbol('B', p, q) + >>> is_matadd_valid(A, B) + Eq(m, p) & Eq(n, q) + """ + rows, cols = zip(*(arg.shape for arg in args)) + return And( + *(Eq(i, j) for i, j in zip(rows[:-1], rows[1:])), + *(Eq(i, j) for i, j in zip(cols[:-1], cols[1:])), + ) + + +def is_matmul_valid(*args: Union[MatrixExpr, Expr]) -> Boolean: + """Return the symbolic condition how ``MatMul`` makes sense + + Parameters + ========== + + args + The list of arguments of matrices and scalar expressions to be tested + for. + + Examples + ======== + + >>> from sympy import MatrixSymbol, symbols + >>> from sympy.matrices.expressions._shape import is_matmul_valid + + >>> m, n, p, q = symbols('m n p q') + >>> A = MatrixSymbol('A', m, n) + >>> B = MatrixSymbol('B', p, q) + >>> is_matmul_valid(A, B) + Eq(n, p) + """ + rows, cols = zip(*(arg.shape for arg in args if isinstance(arg, MatrixExpr))) + return And(*(Eq(i, j) for i, j in zip(cols[:-1], rows[1:]))) + + +def is_square(arg: MatrixExpr, /) -> Boolean: + """Return the symbolic condition how the matrix is assumed to be square + + Parameters + ========== + + arg + The matrix to be tested for. + + Examples + ======== + + >>> from sympy import MatrixSymbol, symbols + >>> from sympy.matrices.expressions._shape import is_square + + >>> m, n = symbols('m n') + >>> A = MatrixSymbol('A', m, n) + >>> is_square(A) + Eq(m, n) + """ + return Eq(arg.rows, arg.cols) + + +def validate_matadd_integer(*args: MatrixExpr) -> None: + """Validate matrix shape for addition only for integer values""" + rows, cols = zip(*(x.shape for x in args)) + if len(set(filter(lambda x: isinstance(x, (int, Integer)), rows))) > 1: + raise ShapeError(f"Matrices have mismatching shape: {rows}") + if len(set(filter(lambda x: isinstance(x, (int, Integer)), cols))) > 1: + raise ShapeError(f"Matrices have mismatching shape: {cols}") + + +def validate_matmul_integer(*args: MatrixExpr) -> None: + """Validate matrix shape for multiplication only for integer values""" + for A, B in zip(args[:-1], args[1:]): + i, j = A.cols, B.rows + if isinstance(i, (int, Integer)) and isinstance(j, (int, Integer)) and i != j: + raise ShapeError("Matrices are not aligned", i, j) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/adjoint.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/adjoint.py new file mode 100644 index 0000000000000000000000000000000000000000..2039a7b2eb8eeacb02435979121c4133a11d8e02 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/adjoint.py @@ -0,0 +1,60 @@ +from sympy.core import Basic +from sympy.functions import adjoint, conjugate +from sympy.matrices.expressions.matexpr import MatrixExpr + + +class Adjoint(MatrixExpr): + """ + The Hermitian adjoint of a matrix expression. + + This is a symbolic object that simply stores its argument without + evaluating it. To actually compute the adjoint, use the ``adjoint()`` + function. + + Examples + ======== + + >>> from sympy import MatrixSymbol, Adjoint, adjoint + >>> A = MatrixSymbol('A', 3, 5) + >>> B = MatrixSymbol('B', 5, 3) + >>> Adjoint(A*B) + Adjoint(A*B) + >>> adjoint(A*B) + Adjoint(B)*Adjoint(A) + >>> adjoint(A*B) == Adjoint(A*B) + False + >>> adjoint(A*B) == Adjoint(A*B).doit() + True + """ + is_Adjoint = True + + def doit(self, **hints): + arg = self.arg + if hints.get('deep', True) and isinstance(arg, Basic): + return adjoint(arg.doit(**hints)) + else: + return adjoint(self.arg) + + @property + def arg(self): + return self.args[0] + + @property + def shape(self): + return self.arg.shape[::-1] + + def _entry(self, i, j, **kwargs): + return conjugate(self.arg._entry(j, i, **kwargs)) + + def _eval_adjoint(self): + return self.arg + + def _eval_transpose(self): + return self.arg.conjugate() + + def _eval_conjugate(self): + return self.arg.transpose() + + def _eval_trace(self): + from sympy.matrices.expressions.trace import Trace + return conjugate(Trace(self.arg)) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/applyfunc.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/applyfunc.py new file mode 100644 index 0000000000000000000000000000000000000000..c0363658447a8dc37a152b30e45533bac582b10c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/applyfunc.py @@ -0,0 +1,204 @@ +from sympy.core.expr import ExprBuilder +from sympy.core.function import (Function, FunctionClass, Lambda) +from sympy.core.symbol import Dummy +from sympy.core.sympify import sympify, _sympify +from sympy.matrices.expressions import MatrixExpr +from sympy.matrices.matrixbase import MatrixBase + + +class ElementwiseApplyFunction(MatrixExpr): + r""" + Apply function to a matrix elementwise without evaluating. + + Examples + ======== + + It can be created by calling ``.applyfunc()`` on a matrix + expression: + + >>> from sympy import MatrixSymbol + >>> from sympy.matrices.expressions.applyfunc import ElementwiseApplyFunction + >>> from sympy import exp + >>> X = MatrixSymbol("X", 3, 3) + >>> X.applyfunc(exp) + Lambda(_d, exp(_d)).(X) + + Otherwise using the class constructor: + + >>> from sympy import eye + >>> expr = ElementwiseApplyFunction(exp, eye(3)) + >>> expr + Lambda(_d, exp(_d)).(Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]])) + >>> expr.doit() + Matrix([ + [E, 1, 1], + [1, E, 1], + [1, 1, E]]) + + Notice the difference with the real mathematical functions: + + >>> exp(eye(3)) + Matrix([ + [E, 0, 0], + [0, E, 0], + [0, 0, E]]) + """ + + def __new__(cls, function, expr): + expr = _sympify(expr) + if not expr.is_Matrix: + raise ValueError("{} must be a matrix instance.".format(expr)) + + if expr.shape == (1, 1): + # Check if the function returns a matrix, in that case, just apply + # the function instead of creating an ElementwiseApplyFunc object: + ret = function(expr) + if isinstance(ret, MatrixExpr): + return ret + + if not isinstance(function, (FunctionClass, Lambda)): + d = Dummy('d') + function = Lambda(d, function(d)) + + function = sympify(function) + if not isinstance(function, (FunctionClass, Lambda)): + raise ValueError( + "{} should be compatible with SymPy function classes." + .format(function)) + + if 1 not in function.nargs: + raise ValueError( + '{} should be able to accept 1 arguments.'.format(function)) + + if not isinstance(function, Lambda): + d = Dummy('d') + function = Lambda(d, function(d)) + + obj = MatrixExpr.__new__(cls, function, expr) + return obj + + @property + def function(self): + return self.args[0] + + @property + def expr(self): + return self.args[1] + + @property + def shape(self): + return self.expr.shape + + def doit(self, **hints): + deep = hints.get("deep", True) + expr = self.expr + if deep: + expr = expr.doit(**hints) + function = self.function + if isinstance(function, Lambda) and function.is_identity: + # This is a Lambda containing the identity function. + return expr + if isinstance(expr, MatrixBase): + return expr.applyfunc(self.function) + elif isinstance(expr, ElementwiseApplyFunction): + return ElementwiseApplyFunction( + lambda x: self.function(expr.function(x)), + expr.expr + ).doit(**hints) + else: + return self + + def _entry(self, i, j, **kwargs): + return self.function(self.expr._entry(i, j, **kwargs)) + + def _get_function_fdiff(self): + d = Dummy("d") + function = self.function(d) + fdiff = function.diff(d) + if isinstance(fdiff, Function): + fdiff = type(fdiff) + else: + fdiff = Lambda(d, fdiff) + return fdiff + + def _eval_derivative(self, x): + from sympy.matrices.expressions.hadamard import hadamard_product + dexpr = self.expr.diff(x) + fdiff = self._get_function_fdiff() + return hadamard_product( + dexpr, + ElementwiseApplyFunction(fdiff, self.expr) + ) + + def _eval_derivative_matrix_lines(self, x): + from sympy.matrices.expressions.special import Identity + from sympy.tensor.array.expressions.array_expressions import ArrayContraction + from sympy.tensor.array.expressions.array_expressions import ArrayDiagonal + from sympy.tensor.array.expressions.array_expressions import ArrayTensorProduct + + fdiff = self._get_function_fdiff() + lr = self.expr._eval_derivative_matrix_lines(x) + ewdiff = ElementwiseApplyFunction(fdiff, self.expr) + if 1 in x.shape: + # Vector: + iscolumn = self.shape[1] == 1 + for i in lr: + if iscolumn: + ptr1 = i.first_pointer + ptr2 = Identity(self.shape[1]) + else: + ptr1 = Identity(self.shape[0]) + ptr2 = i.second_pointer + + subexpr = ExprBuilder( + ArrayDiagonal, + [ + ExprBuilder( + ArrayTensorProduct, + [ + ewdiff, + ptr1, + ptr2, + ] + ), + (0, 2) if iscolumn else (1, 4) + ], + validator=ArrayDiagonal._validate + ) + i._lines = [subexpr] + i._first_pointer_parent = subexpr.args[0].args + i._first_pointer_index = 1 + i._second_pointer_parent = subexpr.args[0].args + i._second_pointer_index = 2 + else: + # Matrix case: + for i in lr: + ptr1 = i.first_pointer + ptr2 = i.second_pointer + newptr1 = Identity(ptr1.shape[1]) + newptr2 = Identity(ptr2.shape[1]) + subexpr = ExprBuilder( + ArrayContraction, + [ + ExprBuilder( + ArrayTensorProduct, + [ptr1, newptr1, ewdiff, ptr2, newptr2] + ), + (1, 2, 4), + (5, 7, 8), + ], + validator=ArrayContraction._validate + ) + i._first_pointer_parent = subexpr.args[0].args + i._first_pointer_index = 1 + i._second_pointer_parent = subexpr.args[0].args + i._second_pointer_index = 4 + i._lines = [subexpr] + return lr + + def _eval_transpose(self): + from sympy.matrices.expressions.transpose import Transpose + return self.func(self.function, Transpose(self.expr).doit()) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/blockmatrix.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/blockmatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..0125d6233ba7cf8c0b590fbb655d9c7c447e0bd4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/blockmatrix.py @@ -0,0 +1,975 @@ +from sympy.assumptions.ask import (Q, ask) +from sympy.core import Basic, Add, Mul, S +from sympy.core.sympify import _sympify +from sympy.functions.elementary.complexes import re, im +from sympy.strategies import typed, exhaust, condition, do_one, unpack +from sympy.strategies.traverse import bottom_up +from sympy.utilities.iterables import is_sequence, sift +from sympy.utilities.misc import filldedent + +from sympy.matrices import Matrix, ShapeError +from sympy.matrices.exceptions import NonInvertibleMatrixError +from sympy.matrices.expressions.determinant import det, Determinant +from sympy.matrices.expressions.inverse import Inverse +from sympy.matrices.expressions.matadd import MatAdd +from sympy.matrices.expressions.matexpr import MatrixExpr, MatrixElement +from sympy.matrices.expressions.matmul import MatMul +from sympy.matrices.expressions.matpow import MatPow +from sympy.matrices.expressions.slice import MatrixSlice +from sympy.matrices.expressions.special import ZeroMatrix, Identity +from sympy.matrices.expressions.trace import trace +from sympy.matrices.expressions.transpose import Transpose, transpose + + +class BlockMatrix(MatrixExpr): + """A BlockMatrix is a Matrix comprised of other matrices. + + The submatrices are stored in a SymPy Matrix object but accessed as part of + a Matrix Expression + + >>> from sympy import (MatrixSymbol, BlockMatrix, symbols, + ... Identity, ZeroMatrix, block_collapse) + >>> n,m,l = symbols('n m l') + >>> X = MatrixSymbol('X', n, n) + >>> Y = MatrixSymbol('Y', m, m) + >>> Z = MatrixSymbol('Z', n, m) + >>> B = BlockMatrix([[X, Z], [ZeroMatrix(m,n), Y]]) + >>> print(B) + Matrix([ + [X, Z], + [0, Y]]) + + >>> C = BlockMatrix([[Identity(n), Z]]) + >>> print(C) + Matrix([[I, Z]]) + + >>> print(block_collapse(C*B)) + Matrix([[X, Z + Z*Y]]) + + Some matrices might be comprised of rows of blocks with + the matrices in each row having the same height and the + rows all having the same total number of columns but + not having the same number of columns for each matrix + in each row. In this case, the matrix is not a block + matrix and should be instantiated by Matrix. + + >>> from sympy import ones, Matrix + >>> dat = [ + ... [ones(3,2), ones(3,3)*2], + ... [ones(2,3)*3, ones(2,2)*4]] + ... + >>> BlockMatrix(dat) + Traceback (most recent call last): + ... + ValueError: + Although this matrix is comprised of blocks, the blocks do not fill + the matrix in a size-symmetric fashion. To create a full matrix from + these arguments, pass them directly to Matrix. + >>> Matrix(dat) + Matrix([ + [1, 1, 2, 2, 2], + [1, 1, 2, 2, 2], + [1, 1, 2, 2, 2], + [3, 3, 3, 4, 4], + [3, 3, 3, 4, 4]]) + + See Also + ======== + sympy.matrices.matrixbase.MatrixBase.irregular + """ + def __new__(cls, *args, **kwargs): + from sympy.matrices.immutable import ImmutableDenseMatrix + isMat = lambda i: getattr(i, 'is_Matrix', False) + if len(args) != 1 or \ + not is_sequence(args[0]) or \ + len({isMat(r) for r in args[0]}) != 1: + raise ValueError(filldedent(''' + expecting a sequence of 1 or more rows + containing Matrices.''')) + rows = args[0] if args else [] + if not isMat(rows): + if rows and isMat(rows[0]): + rows = [rows] # rows is not list of lists or [] + # regularity check + # same number of matrices in each row + blocky = ok = len({len(r) for r in rows}) == 1 + if ok: + # same number of rows for each matrix in a row + for r in rows: + ok = len({i.rows for i in r}) == 1 + if not ok: + break + blocky = ok + if ok: + # same number of cols for each matrix in each col + for c in range(len(rows[0])): + ok = len({rows[i][c].cols + for i in range(len(rows))}) == 1 + if not ok: + break + if not ok: + # same total cols in each row + ok = len({ + sum(i.cols for i in r) for r in rows}) == 1 + if blocky and ok: + raise ValueError(filldedent(''' + Although this matrix is comprised of blocks, + the blocks do not fill the matrix in a + size-symmetric fashion. To create a full matrix + from these arguments, pass them directly to + Matrix.''')) + raise ValueError(filldedent(''' + When there are not the same number of rows in each + row's matrices or there are not the same number of + total columns in each row, the matrix is not a + block matrix. If this matrix is known to consist of + blocks fully filling a 2-D space then see + Matrix.irregular.''')) + mat = ImmutableDenseMatrix(rows, evaluate=False) + obj = Basic.__new__(cls, mat) + return obj + + @property + def shape(self): + numrows = numcols = 0 + M = self.blocks + for i in range(M.shape[0]): + numrows += M[i, 0].shape[0] + for i in range(M.shape[1]): + numcols += M[0, i].shape[1] + return (numrows, numcols) + + @property + def blockshape(self): + return self.blocks.shape + + @property + def blocks(self): + return self.args[0] + + @property + def rowblocksizes(self): + return [self.blocks[i, 0].rows for i in range(self.blockshape[0])] + + @property + def colblocksizes(self): + return [self.blocks[0, i].cols for i in range(self.blockshape[1])] + + def structurally_equal(self, other): + return (isinstance(other, BlockMatrix) + and self.shape == other.shape + and self.blockshape == other.blockshape + and self.rowblocksizes == other.rowblocksizes + and self.colblocksizes == other.colblocksizes) + + def _blockmul(self, other): + if (isinstance(other, BlockMatrix) and + self.colblocksizes == other.rowblocksizes): + return BlockMatrix(self.blocks*other.blocks) + + return self * other + + def _blockadd(self, other): + if (isinstance(other, BlockMatrix) + and self.structurally_equal(other)): + return BlockMatrix(self.blocks + other.blocks) + + return self + other + + def _eval_transpose(self): + # Flip all the individual matrices + matrices = [transpose(matrix) for matrix in self.blocks] + # Make a copy + M = Matrix(self.blockshape[0], self.blockshape[1], matrices) + # Transpose the block structure + M = M.transpose() + return BlockMatrix(M) + + def _eval_adjoint(self): + return BlockMatrix( + Matrix(self.blockshape[0], self.blockshape[1], self.blocks).adjoint() + ) + + def _eval_trace(self): + if self.rowblocksizes == self.colblocksizes: + blocks = [self.blocks[i, i] for i in range(self.blockshape[0])] + return Add(*[trace(block) for block in blocks]) + + def _eval_determinant(self): + if self.blockshape == (1, 1): + return det(self.blocks[0, 0]) + if self.blockshape == (2, 2): + [[A, B], + [C, D]] = self.blocks.tolist() + if ask(Q.invertible(A)): + return det(A)*det(D - C*A.I*B) + elif ask(Q.invertible(D)): + return det(D)*det(A - B*D.I*C) + return Determinant(self) + + def _eval_as_real_imag(self): + real_matrices = [re(matrix) for matrix in self.blocks] + real_matrices = Matrix(self.blockshape[0], self.blockshape[1], real_matrices) + + im_matrices = [im(matrix) for matrix in self.blocks] + im_matrices = Matrix(self.blockshape[0], self.blockshape[1], im_matrices) + + return (BlockMatrix(real_matrices), BlockMatrix(im_matrices)) + + def _eval_derivative(self, x): + return BlockMatrix(self.blocks.diff(x)) + + def transpose(self): + """Return transpose of matrix. + + Examples + ======== + + >>> from sympy import MatrixSymbol, BlockMatrix, ZeroMatrix + >>> from sympy.abc import m, n + >>> X = MatrixSymbol('X', n, n) + >>> Y = MatrixSymbol('Y', m, m) + >>> Z = MatrixSymbol('Z', n, m) + >>> B = BlockMatrix([[X, Z], [ZeroMatrix(m,n), Y]]) + >>> B.transpose() + Matrix([ + [X.T, 0], + [Z.T, Y.T]]) + >>> _.transpose() + Matrix([ + [X, Z], + [0, Y]]) + """ + return self._eval_transpose() + + def schur(self, mat = 'A', generalized = False): + """Return the Schur Complement of the 2x2 BlockMatrix + + Parameters + ========== + + mat : String, optional + The matrix with respect to which the + Schur Complement is calculated. 'A' is + used by default + + generalized : bool, optional + If True, returns the generalized Schur + Component which uses Moore-Penrose Inverse + + Examples + ======== + + >>> from sympy import symbols, MatrixSymbol, BlockMatrix + >>> m, n = symbols('m n') + >>> A = MatrixSymbol('A', n, n) + >>> B = MatrixSymbol('B', n, m) + >>> C = MatrixSymbol('C', m, n) + >>> D = MatrixSymbol('D', m, m) + >>> X = BlockMatrix([[A, B], [C, D]]) + + The default Schur Complement is evaluated with "A" + + >>> X.schur() + -C*A**(-1)*B + D + >>> X.schur('D') + A - B*D**(-1)*C + + Schur complement with non-invertible matrices is not + defined. Instead, the generalized Schur complement can + be calculated which uses the Moore-Penrose Inverse. To + achieve this, `generalized` must be set to `True` + + >>> X.schur('B', generalized=True) + C - D*(B.T*B)**(-1)*B.T*A + >>> X.schur('C', generalized=True) + -A*(C.T*C)**(-1)*C.T*D + B + + Returns + ======= + + M : Matrix + The Schur Complement Matrix + + Raises + ====== + + ShapeError + If the block matrix is not a 2x2 matrix + + NonInvertibleMatrixError + If given matrix is non-invertible + + References + ========== + + .. [1] Wikipedia Article on Schur Component : https://en.wikipedia.org/wiki/Schur_complement + + See Also + ======== + + sympy.matrices.matrixbase.MatrixBase.pinv + """ + + if self.blockshape == (2, 2): + [[A, B], + [C, D]] = self.blocks.tolist() + d={'A' : A, 'B' : B, 'C' : C, 'D' : D} + try: + inv = (d[mat].T*d[mat]).inv()*d[mat].T if generalized else d[mat].inv() + if mat == 'A': + return D - C * inv * B + elif mat == 'B': + return C - D * inv * A + elif mat == 'C': + return B - A * inv * D + elif mat == 'D': + return A - B * inv * C + #For matrices where no sub-matrix is square + return self + except NonInvertibleMatrixError: + raise NonInvertibleMatrixError('The given matrix is not invertible. Please set generalized=True \ + to compute the generalized Schur Complement which uses Moore-Penrose Inverse') + else: + raise ShapeError('Schur Complement can only be calculated for 2x2 block matrices') + + def LDUdecomposition(self): + """Returns the Block LDU decomposition of + a 2x2 Block Matrix + + Returns + ======= + + (L, D, U) : Matrices + L : Lower Diagonal Matrix + D : Diagonal Matrix + U : Upper Diagonal Matrix + + Examples + ======== + + >>> from sympy import symbols, MatrixSymbol, BlockMatrix, block_collapse + >>> m, n = symbols('m n') + >>> A = MatrixSymbol('A', n, n) + >>> B = MatrixSymbol('B', n, m) + >>> C = MatrixSymbol('C', m, n) + >>> D = MatrixSymbol('D', m, m) + >>> X = BlockMatrix([[A, B], [C, D]]) + >>> L, D, U = X.LDUdecomposition() + >>> block_collapse(L*D*U) + Matrix([ + [A, B], + [C, D]]) + + Raises + ====== + + ShapeError + If the block matrix is not a 2x2 matrix + + NonInvertibleMatrixError + If the matrix "A" is non-invertible + + See Also + ======== + sympy.matrices.expressions.blockmatrix.BlockMatrix.UDLdecomposition + sympy.matrices.expressions.blockmatrix.BlockMatrix.LUdecomposition + """ + if self.blockshape == (2,2): + [[A, B], + [C, D]] = self.blocks.tolist() + try: + AI = A.I + except NonInvertibleMatrixError: + raise NonInvertibleMatrixError('Block LDU decomposition cannot be calculated when\ + "A" is singular') + Ip = Identity(B.shape[0]) + Iq = Identity(B.shape[1]) + Z = ZeroMatrix(*B.shape) + L = BlockMatrix([[Ip, Z], [C*AI, Iq]]) + D = BlockDiagMatrix(A, self.schur()) + U = BlockMatrix([[Ip, AI*B],[Z.T, Iq]]) + return L, D, U + else: + raise ShapeError("Block LDU decomposition is supported only for 2x2 block matrices") + + def UDLdecomposition(self): + """Returns the Block UDL decomposition of + a 2x2 Block Matrix + + Returns + ======= + + (U, D, L) : Matrices + U : Upper Diagonal Matrix + D : Diagonal Matrix + L : Lower Diagonal Matrix + + Examples + ======== + + >>> from sympy import symbols, MatrixSymbol, BlockMatrix, block_collapse + >>> m, n = symbols('m n') + >>> A = MatrixSymbol('A', n, n) + >>> B = MatrixSymbol('B', n, m) + >>> C = MatrixSymbol('C', m, n) + >>> D = MatrixSymbol('D', m, m) + >>> X = BlockMatrix([[A, B], [C, D]]) + >>> U, D, L = X.UDLdecomposition() + >>> block_collapse(U*D*L) + Matrix([ + [A, B], + [C, D]]) + + Raises + ====== + + ShapeError + If the block matrix is not a 2x2 matrix + + NonInvertibleMatrixError + If the matrix "D" is non-invertible + + See Also + ======== + sympy.matrices.expressions.blockmatrix.BlockMatrix.LDUdecomposition + sympy.matrices.expressions.blockmatrix.BlockMatrix.LUdecomposition + """ + if self.blockshape == (2,2): + [[A, B], + [C, D]] = self.blocks.tolist() + try: + DI = D.I + except NonInvertibleMatrixError: + raise NonInvertibleMatrixError('Block UDL decomposition cannot be calculated when\ + "D" is singular') + Ip = Identity(A.shape[0]) + Iq = Identity(B.shape[1]) + Z = ZeroMatrix(*B.shape) + U = BlockMatrix([[Ip, B*DI], [Z.T, Iq]]) + D = BlockDiagMatrix(self.schur('D'), D) + L = BlockMatrix([[Ip, Z],[DI*C, Iq]]) + return U, D, L + else: + raise ShapeError("Block UDL decomposition is supported only for 2x2 block matrices") + + def LUdecomposition(self): + """Returns the Block LU decomposition of + a 2x2 Block Matrix + + Returns + ======= + + (L, U) : Matrices + L : Lower Diagonal Matrix + U : Upper Diagonal Matrix + + Examples + ======== + + >>> from sympy import symbols, MatrixSymbol, BlockMatrix, block_collapse + >>> m, n = symbols('m n') + >>> A = MatrixSymbol('A', n, n) + >>> B = MatrixSymbol('B', n, m) + >>> C = MatrixSymbol('C', m, n) + >>> D = MatrixSymbol('D', m, m) + >>> X = BlockMatrix([[A, B], [C, D]]) + >>> L, U = X.LUdecomposition() + >>> block_collapse(L*U) + Matrix([ + [A, B], + [C, D]]) + + Raises + ====== + + ShapeError + If the block matrix is not a 2x2 matrix + + NonInvertibleMatrixError + If the matrix "A" is non-invertible + + See Also + ======== + sympy.matrices.expressions.blockmatrix.BlockMatrix.UDLdecomposition + sympy.matrices.expressions.blockmatrix.BlockMatrix.LDUdecomposition + """ + if self.blockshape == (2,2): + [[A, B], + [C, D]] = self.blocks.tolist() + try: + A = A**S.Half + AI = A.I + except NonInvertibleMatrixError: + raise NonInvertibleMatrixError('Block LU decomposition cannot be calculated when\ + "A" is singular') + Z = ZeroMatrix(*B.shape) + Q = self.schur()**S.Half + L = BlockMatrix([[A, Z], [C*AI, Q]]) + U = BlockMatrix([[A, AI*B],[Z.T, Q]]) + return L, U + else: + raise ShapeError("Block LU decomposition is supported only for 2x2 block matrices") + + def _entry(self, i, j, **kwargs): + # Find row entry + orig_i, orig_j = i, j + for row_block, numrows in enumerate(self.rowblocksizes): + cmp = i < numrows + if cmp == True: + break + elif cmp == False: + i -= numrows + elif row_block < self.blockshape[0] - 1: + # Can't tell which block and it's not the last one, return unevaluated + return MatrixElement(self, orig_i, orig_j) + for col_block, numcols in enumerate(self.colblocksizes): + cmp = j < numcols + if cmp == True: + break + elif cmp == False: + j -= numcols + elif col_block < self.blockshape[1] - 1: + return MatrixElement(self, orig_i, orig_j) + return self.blocks[row_block, col_block][i, j] + + @property + def is_Identity(self): + if self.blockshape[0] != self.blockshape[1]: + return False + for i in range(self.blockshape[0]): + for j in range(self.blockshape[1]): + if i==j and not self.blocks[i, j].is_Identity: + return False + if i!=j and not self.blocks[i, j].is_ZeroMatrix: + return False + return True + + @property + def is_structurally_symmetric(self): + return self.rowblocksizes == self.colblocksizes + + def equals(self, other): + if self == other: + return True + if (isinstance(other, BlockMatrix) and self.blocks == other.blocks): + return True + return super().equals(other) + + +class BlockDiagMatrix(BlockMatrix): + """A sparse matrix with block matrices along its diagonals + + Examples + ======== + + >>> from sympy import MatrixSymbol, BlockDiagMatrix, symbols + >>> n, m, l = symbols('n m l') + >>> X = MatrixSymbol('X', n, n) + >>> Y = MatrixSymbol('Y', m, m) + >>> BlockDiagMatrix(X, Y) + Matrix([ + [X, 0], + [0, Y]]) + + Notes + ===== + + If you want to get the individual diagonal blocks, use + :meth:`get_diag_blocks`. + + See Also + ======== + + sympy.matrices.dense.diag + """ + def __new__(cls, *mats): + return Basic.__new__(BlockDiagMatrix, *[_sympify(m) for m in mats]) + + @property + def diag(self): + return self.args + + @property + def blocks(self): + from sympy.matrices.immutable import ImmutableDenseMatrix + mats = self.args + data = [[mats[i] if i == j else ZeroMatrix(mats[i].rows, mats[j].cols) + for j in range(len(mats))] + for i in range(len(mats))] + return ImmutableDenseMatrix(data, evaluate=False) + + @property + def shape(self): + return (sum(block.rows for block in self.args), + sum(block.cols for block in self.args)) + + @property + def blockshape(self): + n = len(self.args) + return (n, n) + + @property + def rowblocksizes(self): + return [block.rows for block in self.args] + + @property + def colblocksizes(self): + return [block.cols for block in self.args] + + def _all_square_blocks(self): + """Returns true if all blocks are square""" + return all(mat.is_square for mat in self.args) + + def _eval_determinant(self): + if self._all_square_blocks(): + return Mul(*[det(mat) for mat in self.args]) + # At least one block is non-square. Since the entire matrix must be square we know there must + # be at least two blocks in this matrix, in which case the entire matrix is necessarily rank-deficient + return S.Zero + + def _eval_inverse(self, expand='ignored'): + if self._all_square_blocks(): + return BlockDiagMatrix(*[mat.inverse() for mat in self.args]) + # See comment in _eval_determinant() + raise NonInvertibleMatrixError('Matrix det == 0; not invertible.') + + def _eval_transpose(self): + return BlockDiagMatrix(*[mat.transpose() for mat in self.args]) + + def _blockmul(self, other): + if (isinstance(other, BlockDiagMatrix) and + self.colblocksizes == other.rowblocksizes): + return BlockDiagMatrix(*[a*b for a, b in zip(self.args, other.args)]) + else: + return BlockMatrix._blockmul(self, other) + + def _blockadd(self, other): + if (isinstance(other, BlockDiagMatrix) and + self.blockshape == other.blockshape and + self.rowblocksizes == other.rowblocksizes and + self.colblocksizes == other.colblocksizes): + return BlockDiagMatrix(*[a + b for a, b in zip(self.args, other.args)]) + else: + return BlockMatrix._blockadd(self, other) + + def get_diag_blocks(self): + """Return the list of diagonal blocks of the matrix. + + Examples + ======== + + >>> from sympy import BlockDiagMatrix, Matrix + + >>> A = Matrix([[1, 2], [3, 4]]) + >>> B = Matrix([[5, 6], [7, 8]]) + >>> M = BlockDiagMatrix(A, B) + + How to get diagonal blocks from the block diagonal matrix: + + >>> diag_blocks = M.get_diag_blocks() + >>> diag_blocks[0] + Matrix([ + [1, 2], + [3, 4]]) + >>> diag_blocks[1] + Matrix([ + [5, 6], + [7, 8]]) + """ + return self.args + + +def block_collapse(expr): + """Evaluates a block matrix expression + + >>> from sympy import MatrixSymbol, BlockMatrix, symbols, Identity, ZeroMatrix, block_collapse + >>> n,m,l = symbols('n m l') + >>> X = MatrixSymbol('X', n, n) + >>> Y = MatrixSymbol('Y', m, m) + >>> Z = MatrixSymbol('Z', n, m) + >>> B = BlockMatrix([[X, Z], [ZeroMatrix(m, n), Y]]) + >>> print(B) + Matrix([ + [X, Z], + [0, Y]]) + + >>> C = BlockMatrix([[Identity(n), Z]]) + >>> print(C) + Matrix([[I, Z]]) + + >>> print(block_collapse(C*B)) + Matrix([[X, Z + Z*Y]]) + """ + from sympy.strategies.util import expr_fns + + hasbm = lambda expr: isinstance(expr, MatrixExpr) and expr.has(BlockMatrix) + + conditioned_rl = condition( + hasbm, + typed( + {MatAdd: do_one(bc_matadd, bc_block_plus_ident), + MatMul: do_one(bc_matmul, bc_dist), + MatPow: bc_matmul, + Transpose: bc_transpose, + Inverse: bc_inverse, + BlockMatrix: do_one(bc_unpack, deblock)} + ) + ) + + rule = exhaust( + bottom_up( + exhaust(conditioned_rl), + fns=expr_fns + ) + ) + + result = rule(expr) + doit = getattr(result, 'doit', None) + if doit is not None: + return doit() + else: + return result + +def bc_unpack(expr): + if expr.blockshape == (1, 1): + return expr.blocks[0, 0] + return expr + +def bc_matadd(expr): + args = sift(expr.args, lambda M: isinstance(M, BlockMatrix)) + blocks = args[True] + if not blocks: + return expr + + nonblocks = args[False] + block = blocks[0] + for b in blocks[1:]: + block = block._blockadd(b) + if nonblocks: + return MatAdd(*nonblocks) + block + else: + return block + +def bc_block_plus_ident(expr): + idents = [arg for arg in expr.args if arg.is_Identity] + if not idents: + return expr + + blocks = [arg for arg in expr.args if isinstance(arg, BlockMatrix)] + if (blocks and all(b.structurally_equal(blocks[0]) for b in blocks) + and blocks[0].is_structurally_symmetric): + block_id = BlockDiagMatrix(*[Identity(k) + for k in blocks[0].rowblocksizes]) + rest = [arg for arg in expr.args if not arg.is_Identity and not isinstance(arg, BlockMatrix)] + return MatAdd(block_id * len(idents), *blocks, *rest).doit() + + return expr + +def bc_dist(expr): + """ Turn a*[X, Y] into [a*X, a*Y] """ + factor, mat = expr.as_coeff_mmul() + if factor == 1: + return expr + + unpacked = unpack(mat) + + if isinstance(unpacked, BlockDiagMatrix): + B = unpacked.diag + new_B = [factor * mat for mat in B] + return BlockDiagMatrix(*new_B) + elif isinstance(unpacked, BlockMatrix): + B = unpacked.blocks + new_B = [ + [factor * B[i, j] for j in range(B.cols)] for i in range(B.rows)] + return BlockMatrix(new_B) + return expr + + +def bc_matmul(expr): + if isinstance(expr, MatPow): + if expr.args[1].is_Integer and expr.args[1] > 0: + factor, matrices = 1, [expr.args[0]]*expr.args[1] + else: + return expr + else: + factor, matrices = expr.as_coeff_matrices() + + i = 0 + while (i+1 < len(matrices)): + A, B = matrices[i:i+2] + if isinstance(A, BlockMatrix) and isinstance(B, BlockMatrix): + matrices[i] = A._blockmul(B) + matrices.pop(i+1) + elif isinstance(A, BlockMatrix): + matrices[i] = A._blockmul(BlockMatrix([[B]])) + matrices.pop(i+1) + elif isinstance(B, BlockMatrix): + matrices[i] = BlockMatrix([[A]])._blockmul(B) + matrices.pop(i+1) + else: + i+=1 + return MatMul(factor, *matrices).doit() + +def bc_transpose(expr): + collapse = block_collapse(expr.arg) + return collapse._eval_transpose() + + +def bc_inverse(expr): + if isinstance(expr.arg, BlockDiagMatrix): + return expr.inverse() + + expr2 = blockinverse_1x1(expr) + if expr != expr2: + return expr2 + return blockinverse_2x2(Inverse(reblock_2x2(expr.arg))) + +def blockinverse_1x1(expr): + if isinstance(expr.arg, BlockMatrix) and expr.arg.blockshape == (1, 1): + mat = Matrix([[expr.arg.blocks[0].inverse()]]) + return BlockMatrix(mat) + return expr + + +def blockinverse_2x2(expr): + if isinstance(expr.arg, BlockMatrix) and expr.arg.blockshape == (2, 2): + # See: Inverses of 2x2 Block Matrices, Tzon-Tzer Lu and Sheng-Hua Shiou + [[A, B], + [C, D]] = expr.arg.blocks.tolist() + + formula = _choose_2x2_inversion_formula(A, B, C, D) + if formula != None: + MI = expr.arg.schur(formula).I + if formula == 'A': + AI = A.I + return BlockMatrix([[AI + AI * B * MI * C * AI, -AI * B * MI], [-MI * C * AI, MI]]) + if formula == 'B': + BI = B.I + return BlockMatrix([[-MI * D * BI, MI], [BI + BI * A * MI * D * BI, -BI * A * MI]]) + if formula == 'C': + CI = C.I + return BlockMatrix([[-CI * D * MI, CI + CI * D * MI * A * CI], [MI, -MI * A * CI]]) + if formula == 'D': + DI = D.I + return BlockMatrix([[MI, -MI * B * DI], [-DI * C * MI, DI + DI * C * MI * B * DI]]) + + return expr + + +def _choose_2x2_inversion_formula(A, B, C, D): + """ + Assuming [[A, B], [C, D]] would form a valid square block matrix, find + which of the classical 2x2 block matrix inversion formulas would be + best suited. + + Returns 'A', 'B', 'C', 'D' to represent the algorithm involving inversion + of the given argument or None if the matrix cannot be inverted using + any of those formulas. + """ + # Try to find a known invertible matrix. Note that the Schur complement + # is currently not being considered for this + A_inv = ask(Q.invertible(A)) + if A_inv == True: + return 'A' + B_inv = ask(Q.invertible(B)) + if B_inv == True: + return 'B' + C_inv = ask(Q.invertible(C)) + if C_inv == True: + return 'C' + D_inv = ask(Q.invertible(D)) + if D_inv == True: + return 'D' + # Otherwise try to find a matrix that isn't known to be non-invertible + if A_inv != False: + return 'A' + if B_inv != False: + return 'B' + if C_inv != False: + return 'C' + if D_inv != False: + return 'D' + return None + + +def deblock(B): + """ Flatten a BlockMatrix of BlockMatrices """ + if not isinstance(B, BlockMatrix) or not B.blocks.has(BlockMatrix): + return B + wrap = lambda x: x if isinstance(x, BlockMatrix) else BlockMatrix([[x]]) + bb = B.blocks.applyfunc(wrap) # everything is a block + + try: + MM = Matrix(0, sum(bb[0, i].blocks.shape[1] for i in range(bb.shape[1])), []) + for row in range(0, bb.shape[0]): + M = Matrix(bb[row, 0].blocks) + for col in range(1, bb.shape[1]): + M = M.row_join(bb[row, col].blocks) + MM = MM.col_join(M) + + return BlockMatrix(MM) + except ShapeError: + return B + + +def reblock_2x2(expr): + """ + Reblock a BlockMatrix so that it has 2x2 blocks of block matrices. If + possible in such a way that the matrix continues to be invertible using the + classical 2x2 block inversion formulas. + """ + if not isinstance(expr, BlockMatrix) or not all(d > 2 for d in expr.blockshape): + return expr + + BM = BlockMatrix # for brevity's sake + rowblocks, colblocks = expr.blockshape + blocks = expr.blocks + for i in range(1, rowblocks): + for j in range(1, colblocks): + # try to split rows at i and cols at j + A = bc_unpack(BM(blocks[:i, :j])) + B = bc_unpack(BM(blocks[:i, j:])) + C = bc_unpack(BM(blocks[i:, :j])) + D = bc_unpack(BM(blocks[i:, j:])) + + formula = _choose_2x2_inversion_formula(A, B, C, D) + if formula is not None: + return BlockMatrix([[A, B], [C, D]]) + + # else: nothing worked, just split upper left corner + return BM([[blocks[0, 0], BM(blocks[0, 1:])], + [BM(blocks[1:, 0]), BM(blocks[1:, 1:])]]) + + +def bounds(sizes): + """ Convert sequence of numbers into pairs of low-high pairs + + >>> from sympy.matrices.expressions.blockmatrix import bounds + >>> bounds((1, 10, 50)) + [(0, 1), (1, 11), (11, 61)] + """ + low = 0 + rv = [] + for size in sizes: + rv.append((low, low + size)) + low += size + return rv + +def blockcut(expr, rowsizes, colsizes): + """ Cut a matrix expression into Blocks + + >>> from sympy import ImmutableMatrix, blockcut + >>> M = ImmutableMatrix(4, 4, range(16)) + >>> B = blockcut(M, (1, 3), (1, 3)) + >>> type(B).__name__ + 'BlockMatrix' + >>> ImmutableMatrix(B.blocks[0, 1]) + Matrix([[1, 2, 3]]) + """ + + rowbounds = bounds(rowsizes) + colbounds = bounds(colsizes) + return BlockMatrix([[MatrixSlice(expr, rowbound, colbound) + for colbound in colbounds] + for rowbound in rowbounds]) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/companion.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/companion.py new file mode 100644 index 0000000000000000000000000000000000000000..6969c917f63806cb1f5417804e01ecc1350d1406 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/companion.py @@ -0,0 +1,56 @@ +from sympy.core.singleton import S +from sympy.core.sympify import _sympify +from sympy.polys.polytools import Poly + +from .matexpr import MatrixExpr + + +class CompanionMatrix(MatrixExpr): + """A symbolic companion matrix of a polynomial. + + Examples + ======== + + >>> from sympy import Poly, Symbol, symbols + >>> from sympy.matrices.expressions import CompanionMatrix + >>> x = Symbol('x') + >>> c0, c1, c2, c3, c4 = symbols('c0:5') + >>> p = Poly(c0 + c1*x + c2*x**2 + c3*x**3 + c4*x**4 + x**5, x) + >>> CompanionMatrix(p) + CompanionMatrix(Poly(x**5 + c4*x**4 + c3*x**3 + c2*x**2 + c1*x + c0, + x, domain='ZZ[c0,c1,c2,c3,c4]')) + """ + def __new__(cls, poly): + poly = _sympify(poly) + if not isinstance(poly, Poly): + raise ValueError("{} must be a Poly instance.".format(poly)) + if not poly.is_monic: + raise ValueError("{} must be a monic polynomial.".format(poly)) + if not poly.is_univariate: + raise ValueError( + "{} must be a univariate polynomial.".format(poly)) + if not poly.degree() >= 1: + raise ValueError( + "{} must have degree not less than 1.".format(poly)) + + return super().__new__(cls, poly) + + + @property + def shape(self): + poly = self.args[0] + size = poly.degree() + return size, size + + + def _entry(self, i, j): + if j == self.cols - 1: + return -self.args[0].all_coeffs()[-1 - i] + elif i == j + 1: + return S.One + return S.Zero + + + def as_explicit(self): + from sympy.matrices.immutable import ImmutableDenseMatrix + return ImmutableDenseMatrix.companion(self.args[0]) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/determinant.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/determinant.py new file mode 100644 index 0000000000000000000000000000000000000000..b323b3f93a5a0404bf2205f39d25b931d173b6d9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/determinant.py @@ -0,0 +1,148 @@ +from sympy.core.basic import Basic +from sympy.core.expr import Expr +from sympy.core.singleton import S +from sympy.core.sympify import sympify +from sympy.matrices.exceptions import NonSquareMatrixError +from sympy.matrices.matrixbase import MatrixBase + + +class Determinant(Expr): + """Matrix Determinant + + Represents the determinant of a matrix expression. + + Examples + ======== + + >>> from sympy import MatrixSymbol, Determinant, eye + >>> A = MatrixSymbol('A', 3, 3) + >>> Determinant(A) + Determinant(A) + >>> Determinant(eye(3)).doit() + 1 + """ + is_commutative = True + + def __new__(cls, mat): + mat = sympify(mat) + if not mat.is_Matrix: + raise TypeError("Input to Determinant, %s, not a matrix" % str(mat)) + + if mat.is_square is False: + raise NonSquareMatrixError("Det of a non-square matrix") + + return Basic.__new__(cls, mat) + + @property + def arg(self): + return self.args[0] + + @property + def kind(self): + return self.arg.kind.element_kind + + def doit(self, **hints): + arg = self.arg + if hints.get('deep', True): + arg = arg.doit(**hints) + + result = arg._eval_determinant() + if result is not None: + return result + + return self + + +def det(matexpr): + """ Matrix Determinant + + Examples + ======== + + >>> from sympy import MatrixSymbol, det, eye + >>> A = MatrixSymbol('A', 3, 3) + >>> det(A) + Determinant(A) + >>> det(eye(3)) + 1 + """ + + return Determinant(matexpr).doit() + +class Permanent(Expr): + """Matrix Permanent + + Represents the permanent of a matrix expression. + + Examples + ======== + + >>> from sympy import MatrixSymbol, Permanent, ones + >>> A = MatrixSymbol('A', 3, 3) + >>> Permanent(A) + Permanent(A) + >>> Permanent(ones(3, 3)).doit() + 6 + """ + + def __new__(cls, mat): + mat = sympify(mat) + if not mat.is_Matrix: + raise TypeError("Input to Permanent, %s, not a matrix" % str(mat)) + + return Basic.__new__(cls, mat) + + @property + def arg(self): + return self.args[0] + + def doit(self, expand=False, **hints): + if isinstance(self.arg, MatrixBase): + return self.arg.per() + else: + return self + +def per(matexpr): + """ Matrix Permanent + + Examples + ======== + + >>> from sympy import MatrixSymbol, Matrix, per, ones + >>> A = MatrixSymbol('A', 3, 3) + >>> per(A) + Permanent(A) + >>> per(ones(5, 5)) + 120 + >>> M = Matrix([1, 2, 5]) + >>> per(M) + 8 + """ + + return Permanent(matexpr).doit() + +from sympy.assumptions.ask import ask, Q +from sympy.assumptions.refine import handlers_dict + + +def refine_Determinant(expr, assumptions): + """ + >>> from sympy import MatrixSymbol, Q, assuming, refine, det + >>> X = MatrixSymbol('X', 2, 2) + >>> det(X) + Determinant(X) + >>> with assuming(Q.orthogonal(X)): + ... print(refine(det(X))) + 1 + """ + if ask(Q.orthogonal(expr.arg), assumptions): + return S.One + elif ask(Q.singular(expr.arg), assumptions): + return S.Zero + elif ask(Q.unit_triangular(expr.arg), assumptions): + return S.One + + return expr + + +handlers_dict['Determinant'] = refine_Determinant diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/diagonal.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/diagonal.py new file mode 100644 index 0000000000000000000000000000000000000000..ba8a0216588143e3e251dab84c25f038fad550a4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/diagonal.py @@ -0,0 +1,220 @@ +from sympy.core.sympify import _sympify + +from sympy.matrices.expressions import MatrixExpr +from sympy.core import S, Eq, Ge +from sympy.core.mul import Mul +from sympy.functions.special.tensor_functions import KroneckerDelta + + +class DiagonalMatrix(MatrixExpr): + """DiagonalMatrix(M) will create a matrix expression that + behaves as though all off-diagonal elements, + `M[i, j]` where `i != j`, are zero. + + Examples + ======== + + >>> from sympy import MatrixSymbol, DiagonalMatrix, Symbol + >>> n = Symbol('n', integer=True) + >>> m = Symbol('m', integer=True) + >>> D = DiagonalMatrix(MatrixSymbol('x', 2, 3)) + >>> D[1, 2] + 0 + >>> D[1, 1] + x[1, 1] + + The length of the diagonal -- the lesser of the two dimensions of `M` -- + is accessed through the `diagonal_length` property: + + >>> D.diagonal_length + 2 + >>> DiagonalMatrix(MatrixSymbol('x', n + 1, n)).diagonal_length + n + + When one of the dimensions is symbolic the other will be treated as + though it is smaller: + + >>> tall = DiagonalMatrix(MatrixSymbol('x', n, 3)) + >>> tall.diagonal_length + 3 + >>> tall[10, 1] + 0 + + When the size of the diagonal is not known, a value of None will + be returned: + + >>> DiagonalMatrix(MatrixSymbol('x', n, m)).diagonal_length is None + True + + """ + arg = property(lambda self: self.args[0]) + + shape = property(lambda self: self.arg.shape) # type:ignore + + @property + def diagonal_length(self): + r, c = self.shape + if r.is_Integer and c.is_Integer: + m = min(r, c) + elif r.is_Integer and not c.is_Integer: + m = r + elif c.is_Integer and not r.is_Integer: + m = c + elif r == c: + m = r + else: + try: + m = min(r, c) + except TypeError: + m = None + return m + + def _entry(self, i, j, **kwargs): + if self.diagonal_length is not None: + if Ge(i, self.diagonal_length) is S.true: + return S.Zero + elif Ge(j, self.diagonal_length) is S.true: + return S.Zero + eq = Eq(i, j) + if eq is S.true: + return self.arg[i, i] + elif eq is S.false: + return S.Zero + return self.arg[i, j]*KroneckerDelta(i, j) + + +class DiagonalOf(MatrixExpr): + """DiagonalOf(M) will create a matrix expression that + is equivalent to the diagonal of `M`, represented as + a single column matrix. + + Examples + ======== + + >>> from sympy import MatrixSymbol, DiagonalOf, Symbol + >>> n = Symbol('n', integer=True) + >>> m = Symbol('m', integer=True) + >>> x = MatrixSymbol('x', 2, 3) + >>> diag = DiagonalOf(x) + >>> diag.shape + (2, 1) + + The diagonal can be addressed like a matrix or vector and will + return the corresponding element of the original matrix: + + >>> diag[1, 0] == diag[1] == x[1, 1] + True + + The length of the diagonal -- the lesser of the two dimensions of `M` -- + is accessed through the `diagonal_length` property: + + >>> diag.diagonal_length + 2 + >>> DiagonalOf(MatrixSymbol('x', n + 1, n)).diagonal_length + n + + When only one of the dimensions is symbolic the other will be + treated as though it is smaller: + + >>> dtall = DiagonalOf(MatrixSymbol('x', n, 3)) + >>> dtall.diagonal_length + 3 + + When the size of the diagonal is not known, a value of None will + be returned: + + >>> DiagonalOf(MatrixSymbol('x', n, m)).diagonal_length is None + True + + """ + arg = property(lambda self: self.args[0]) + @property + def shape(self): + r, c = self.arg.shape + if r.is_Integer and c.is_Integer: + m = min(r, c) + elif r.is_Integer and not c.is_Integer: + m = r + elif c.is_Integer and not r.is_Integer: + m = c + elif r == c: + m = r + else: + try: + m = min(r, c) + except TypeError: + m = None + return m, S.One + + @property + def diagonal_length(self): + return self.shape[0] + + def _entry(self, i, j, **kwargs): + return self.arg._entry(i, i, **kwargs) + + +class DiagMatrix(MatrixExpr): + """ + Turn a vector into a diagonal matrix. + """ + def __new__(cls, vector): + vector = _sympify(vector) + obj = MatrixExpr.__new__(cls, vector) + shape = vector.shape + dim = shape[1] if shape[0] == 1 else shape[0] + if vector.shape[0] != 1: + obj._iscolumn = True + else: + obj._iscolumn = False + obj._shape = (dim, dim) + obj._vector = vector + return obj + + @property + def shape(self): + return self._shape + + def _entry(self, i, j, **kwargs): + if self._iscolumn: + result = self._vector._entry(i, 0, **kwargs) + else: + result = self._vector._entry(0, j, **kwargs) + if i != j: + result *= KroneckerDelta(i, j) + return result + + def _eval_transpose(self): + return self + + def as_explicit(self): + from sympy.matrices.dense import diag + return diag(*list(self._vector.as_explicit())) + + def doit(self, **hints): + from sympy.assumptions import ask, Q + from sympy.matrices.expressions.matmul import MatMul + from sympy.matrices.expressions.transpose import Transpose + from sympy.matrices.dense import eye + from sympy.matrices.matrixbase import MatrixBase + vector = self._vector + # This accounts for shape (1, 1) and identity matrices, among others: + if ask(Q.diagonal(vector)): + return vector + if isinstance(vector, MatrixBase): + ret = eye(max(vector.shape)) + for i in range(ret.shape[0]): + ret[i, i] = vector[i] + return type(vector)(ret) + if vector.is_MatMul: + matrices = [arg for arg in vector.args if arg.is_Matrix] + scalars = [arg for arg in vector.args if arg not in matrices] + if scalars: + return Mul.fromiter(scalars)*DiagMatrix(MatMul.fromiter(matrices).doit()).doit() + if isinstance(vector, Transpose): + vector = vector.arg + return DiagMatrix(vector) + + +def diagonalize_vector(vector): + return DiagMatrix(vector).doit() diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/dotproduct.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/dotproduct.py new file mode 100644 index 0000000000000000000000000000000000000000..3a413f8c79a221505f0c082d7f19f78597a2befc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/dotproduct.py @@ -0,0 +1,55 @@ +from sympy.core import Basic, Expr +from sympy.core.sympify import _sympify +from sympy.matrices.expressions.transpose import transpose + + +class DotProduct(Expr): + """ + Dot product of vector matrices + + The input should be two 1 x n or n x 1 matrices. The output represents the + scalar dotproduct. + + This is similar to using MatrixElement and MatMul, except DotProduct does + not require that one vector to be a row vector and the other vector to be + a column vector. + + >>> from sympy import MatrixSymbol, DotProduct + >>> A = MatrixSymbol('A', 1, 3) + >>> B = MatrixSymbol('B', 1, 3) + >>> DotProduct(A, B) + DotProduct(A, B) + >>> DotProduct(A, B).doit() + A[0, 0]*B[0, 0] + A[0, 1]*B[0, 1] + A[0, 2]*B[0, 2] + """ + + def __new__(cls, arg1, arg2): + arg1, arg2 = _sympify((arg1, arg2)) + + if not arg1.is_Matrix: + raise TypeError("Argument 1 of DotProduct is not a matrix") + if not arg2.is_Matrix: + raise TypeError("Argument 2 of DotProduct is not a matrix") + if not (1 in arg1.shape): + raise TypeError("Argument 1 of DotProduct is not a vector") + if not (1 in arg2.shape): + raise TypeError("Argument 2 of DotProduct is not a vector") + + if set(arg1.shape) != set(arg2.shape): + raise TypeError("DotProduct arguments are not the same length") + + return Basic.__new__(cls, arg1, arg2) + + def doit(self, expand=False, **hints): + if self.args[0].shape == self.args[1].shape: + if self.args[0].shape[0] == 1: + mul = self.args[0]*transpose(self.args[1]) + else: + mul = transpose(self.args[0])*self.args[1] + else: + if self.args[0].shape[0] == 1: + mul = self.args[0]*self.args[1] + else: + mul = transpose(self.args[0])*transpose(self.args[1]) + + return mul[0] diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/factorizations.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/factorizations.py new file mode 100644 index 0000000000000000000000000000000000000000..aff2bb81ecff99d8e733f282ac2dd187d76ce895 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/factorizations.py @@ -0,0 +1,62 @@ +from sympy.matrices.expressions import MatrixExpr +from sympy.assumptions.ask import Q + +class Factorization(MatrixExpr): + arg = property(lambda self: self.args[0]) + shape = property(lambda self: self.arg.shape) # type: ignore + +class LofLU(Factorization): + @property + def predicates(self): + return (Q.lower_triangular,) +class UofLU(Factorization): + @property + def predicates(self): + return (Q.upper_triangular,) + +class LofCholesky(LofLU): pass +class UofCholesky(UofLU): pass + +class QofQR(Factorization): + @property + def predicates(self): + return (Q.orthogonal,) +class RofQR(Factorization): + @property + def predicates(self): + return (Q.upper_triangular,) + +class EigenVectors(Factorization): + @property + def predicates(self): + return (Q.orthogonal,) +class EigenValues(Factorization): + @property + def predicates(self): + return (Q.diagonal,) + +class UofSVD(Factorization): + @property + def predicates(self): + return (Q.orthogonal,) +class SofSVD(Factorization): + @property + def predicates(self): + return (Q.diagonal,) +class VofSVD(Factorization): + @property + def predicates(self): + return (Q.orthogonal,) + + +def lu(expr): + return LofLU(expr), UofLU(expr) + +def qr(expr): + return QofQR(expr), RofQR(expr) + +def eig(expr): + return EigenValues(expr), EigenVectors(expr) + +def svd(expr): + return UofSVD(expr), SofSVD(expr), VofSVD(expr) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/fourier.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/fourier.py new file mode 100644 index 0000000000000000000000000000000000000000..5fa9222c2a9b218f42636267235d5dd44c25f8bb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/fourier.py @@ -0,0 +1,91 @@ +from sympy.core.sympify import _sympify +from sympy.matrices.expressions import MatrixExpr +from sympy.core.numbers import I +from sympy.core.singleton import S +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt + + +class DFT(MatrixExpr): + r""" + Returns a discrete Fourier transform matrix. The matrix is scaled + with :math:`\frac{1}{\sqrt{n}}` so that it is unitary. + + Parameters + ========== + + n : integer or Symbol + Size of the transform. + + Examples + ======== + + >>> from sympy.abc import n + >>> from sympy.matrices.expressions.fourier import DFT + >>> DFT(3) + DFT(3) + >>> DFT(3).as_explicit() + Matrix([ + [sqrt(3)/3, sqrt(3)/3, sqrt(3)/3], + [sqrt(3)/3, sqrt(3)*exp(-2*I*pi/3)/3, sqrt(3)*exp(2*I*pi/3)/3], + [sqrt(3)/3, sqrt(3)*exp(2*I*pi/3)/3, sqrt(3)*exp(-2*I*pi/3)/3]]) + >>> DFT(n).shape + (n, n) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/DFT_matrix + + """ + + def __new__(cls, n): + n = _sympify(n) + cls._check_dim(n) + + obj = super().__new__(cls, n) + return obj + + n = property(lambda self: self.args[0]) # type: ignore + shape = property(lambda self: (self.n, self.n)) # type: ignore + + def _entry(self, i, j, **kwargs): + w = exp(-2*S.Pi*I/self.n) + return w**(i*j) / sqrt(self.n) + + def _eval_inverse(self): + return IDFT(self.n) + + +class IDFT(DFT): + r""" + Returns an inverse discrete Fourier transform matrix. The matrix is scaled + with :math:`\frac{1}{\sqrt{n}}` so that it is unitary. + + Parameters + ========== + + n : integer or Symbol + Size of the transform + + Examples + ======== + + >>> from sympy.matrices.expressions.fourier import DFT, IDFT + >>> IDFT(3) + IDFT(3) + >>> IDFT(4)*DFT(4) + I + + See Also + ======== + + DFT + + """ + def _entry(self, i, j, **kwargs): + w = exp(-2*S.Pi*I/self.n) + return w**(-i*j) / sqrt(self.n) + + def _eval_inverse(self): + return DFT(self.n) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/funcmatrix.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/funcmatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..91106edb489b73ac9dd6cb94adc508c0db75d3a5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/funcmatrix.py @@ -0,0 +1,118 @@ +from .matexpr import MatrixExpr +from sympy.core.function import FunctionClass, Lambda +from sympy.core.symbol import Dummy +from sympy.core.sympify import _sympify, sympify +from sympy.matrices import Matrix +from sympy.functions.elementary.complexes import re, im + + +class FunctionMatrix(MatrixExpr): + """Represents a matrix using a function (``Lambda``) which gives + outputs according to the coordinates of each matrix entries. + + Parameters + ========== + + rows : nonnegative integer. Can be symbolic. + + cols : nonnegative integer. Can be symbolic. + + lamda : Function, Lambda or str + If it is a SymPy ``Function`` or ``Lambda`` instance, + it should be able to accept two arguments which represents the + matrix coordinates. + + If it is a pure string containing Python ``lambda`` semantics, + it is interpreted by the SymPy parser and casted into a SymPy + ``Lambda`` instance. + + Examples + ======== + + Creating a ``FunctionMatrix`` from ``Lambda``: + + >>> from sympy import FunctionMatrix, symbols, Lambda, MatPow + >>> i, j, n, m = symbols('i,j,n,m') + >>> FunctionMatrix(n, m, Lambda((i, j), i + j)) + FunctionMatrix(n, m, Lambda((i, j), i + j)) + + Creating a ``FunctionMatrix`` from a SymPy function: + + >>> from sympy import KroneckerDelta + >>> X = FunctionMatrix(3, 3, KroneckerDelta) + >>> X.as_explicit() + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + + Creating a ``FunctionMatrix`` from a SymPy undefined function: + + >>> from sympy import Function + >>> f = Function('f') + >>> X = FunctionMatrix(3, 3, f) + >>> X.as_explicit() + Matrix([ + [f(0, 0), f(0, 1), f(0, 2)], + [f(1, 0), f(1, 1), f(1, 2)], + [f(2, 0), f(2, 1), f(2, 2)]]) + + Creating a ``FunctionMatrix`` from Python ``lambda``: + + >>> FunctionMatrix(n, m, 'lambda i, j: i + j') + FunctionMatrix(n, m, Lambda((i, j), i + j)) + + Example of lazy evaluation of matrix product: + + >>> Y = FunctionMatrix(1000, 1000, Lambda((i, j), i + j)) + >>> isinstance(Y*Y, MatPow) # this is an expression object + True + >>> (Y**2)[10,10] # So this is evaluated lazily + 342923500 + + Notes + ===== + + This class provides an alternative way to represent an extremely + dense matrix with entries in some form of a sequence, in a most + sparse way. + """ + def __new__(cls, rows, cols, lamda): + rows, cols = _sympify(rows), _sympify(cols) + cls._check_dim(rows) + cls._check_dim(cols) + + lamda = sympify(lamda) + if not isinstance(lamda, (FunctionClass, Lambda)): + raise ValueError( + "{} should be compatible with SymPy function classes." + .format(lamda)) + + if 2 not in lamda.nargs: + raise ValueError( + '{} should be able to accept 2 arguments.'.format(lamda)) + + if not isinstance(lamda, Lambda): + i, j = Dummy('i'), Dummy('j') + lamda = Lambda((i, j), lamda(i, j)) + + return super().__new__(cls, rows, cols, lamda) + + @property + def shape(self): + return self.args[0:2] + + @property + def lamda(self): + return self.args[2] + + def _entry(self, i, j, **kwargs): + return self.lamda(i, j) + + def _eval_trace(self): + from sympy.matrices.expressions.trace import Trace + from sympy.concrete.summations import Sum + return Trace(self).rewrite(Sum).doit() + + def _eval_as_real_imag(self): + return (re(Matrix(self)), im(Matrix(self))) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/hadamard.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/hadamard.py new file mode 100644 index 0000000000000000000000000000000000000000..38c9033ebea3a7bfc569223978dc6ef3890206cf --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/hadamard.py @@ -0,0 +1,464 @@ +from collections import Counter + +from sympy.core import Mul, sympify +from sympy.core.add import Add +from sympy.core.expr import ExprBuilder +from sympy.core.sorting import default_sort_key +from sympy.functions.elementary.exponential import log +from sympy.matrices.expressions.matexpr import MatrixExpr +from sympy.matrices.expressions._shape import validate_matadd_integer as validate +from sympy.matrices.expressions.special import ZeroMatrix, OneMatrix +from sympy.strategies import ( + unpack, flatten, condition, exhaust, rm_id, sort +) +from sympy.utilities.exceptions import sympy_deprecation_warning + + +def hadamard_product(*matrices): + """ + Return the elementwise (aka Hadamard) product of matrices. + + Examples + ======== + + >>> from sympy import hadamard_product, MatrixSymbol + >>> A = MatrixSymbol('A', 2, 3) + >>> B = MatrixSymbol('B', 2, 3) + >>> hadamard_product(A) + A + >>> hadamard_product(A, B) + HadamardProduct(A, B) + >>> hadamard_product(A, B)[0, 1] + A[0, 1]*B[0, 1] + """ + if not matrices: + raise TypeError("Empty Hadamard product is undefined") + if len(matrices) == 1: + return matrices[0] + return HadamardProduct(*matrices).doit() + + +class HadamardProduct(MatrixExpr): + """ + Elementwise product of matrix expressions + + Examples + ======== + + Hadamard product for matrix symbols: + + >>> from sympy import hadamard_product, HadamardProduct, MatrixSymbol + >>> A = MatrixSymbol('A', 5, 5) + >>> B = MatrixSymbol('B', 5, 5) + >>> isinstance(hadamard_product(A, B), HadamardProduct) + True + + Notes + ===== + + This is a symbolic object that simply stores its argument without + evaluating it. To actually compute the product, use the function + ``hadamard_product()`` or ``HadamardProduct.doit`` + """ + is_HadamardProduct = True + + def __new__(cls, *args, evaluate=False, check=None): + args = list(map(sympify, args)) + if len(args) == 0: + # We currently don't have a way to support one-matrices of generic dimensions: + raise ValueError("HadamardProduct needs at least one argument") + + if not all(isinstance(arg, MatrixExpr) for arg in args): + raise TypeError("Mix of Matrix and Scalar symbols") + + if check is not None: + sympy_deprecation_warning( + "Passing check to HadamardProduct is deprecated and the check argument will be removed in a future version.", + deprecated_since_version="1.11", + active_deprecations_target='remove-check-argument-from-matrix-operations') + + if check is not False: + validate(*args) + + obj = super().__new__(cls, *args) + if evaluate: + obj = obj.doit(deep=False) + return obj + + @property + def shape(self): + return self.args[0].shape + + def _entry(self, i, j, **kwargs): + return Mul(*[arg._entry(i, j, **kwargs) for arg in self.args]) + + def _eval_transpose(self): + from sympy.matrices.expressions.transpose import transpose + return HadamardProduct(*list(map(transpose, self.args))) + + def doit(self, **hints): + expr = self.func(*(i.doit(**hints) for i in self.args)) + # Check for explicit matrices: + from sympy.matrices.matrixbase import MatrixBase + from sympy.matrices.immutable import ImmutableMatrix + + explicit = [i for i in expr.args if isinstance(i, MatrixBase)] + if explicit: + remainder = [i for i in expr.args if i not in explicit] + expl_mat = ImmutableMatrix([ + Mul.fromiter(i) for i in zip(*explicit) + ]).reshape(*self.shape) + expr = HadamardProduct(*([expl_mat] + remainder)) + + return canonicalize(expr) + + def _eval_derivative(self, x): + terms = [] + args = list(self.args) + for i in range(len(args)): + factors = args[:i] + [args[i].diff(x)] + args[i+1:] + terms.append(hadamard_product(*factors)) + return Add.fromiter(terms) + + def _eval_derivative_matrix_lines(self, x): + from sympy.tensor.array.expressions.array_expressions import ArrayDiagonal + from sympy.tensor.array.expressions.array_expressions import ArrayTensorProduct + from sympy.matrices.expressions.matexpr import _make_matrix + + with_x_ind = [i for i, arg in enumerate(self.args) if arg.has(x)] + lines = [] + for ind in with_x_ind: + left_args = self.args[:ind] + right_args = self.args[ind+1:] + + d = self.args[ind]._eval_derivative_matrix_lines(x) + hadam = hadamard_product(*(right_args + left_args)) + diagonal = [(0, 2), (3, 4)] + diagonal = [e for j, e in enumerate(diagonal) if self.shape[j] != 1] + for i in d: + l1 = i._lines[i._first_line_index] + l2 = i._lines[i._second_line_index] + subexpr = ExprBuilder( + ArrayDiagonal, + [ + ExprBuilder( + ArrayTensorProduct, + [ + ExprBuilder(_make_matrix, [l1]), + hadam, + ExprBuilder(_make_matrix, [l2]), + ] + ), + *diagonal], + + ) + i._first_pointer_parent = subexpr.args[0].args[0].args + i._first_pointer_index = 0 + i._second_pointer_parent = subexpr.args[0].args[2].args + i._second_pointer_index = 0 + i._lines = [subexpr] + lines.append(i) + + return lines + + +# TODO Implement algorithm for rewriting Hadamard product as diagonal matrix +# if matmul identy matrix is multiplied. +def canonicalize(x): + """Canonicalize the Hadamard product ``x`` with mathematical properties. + + Examples + ======== + + >>> from sympy import MatrixSymbol, HadamardProduct + >>> from sympy import OneMatrix, ZeroMatrix + >>> from sympy.matrices.expressions.hadamard import canonicalize + >>> from sympy import init_printing + >>> init_printing(use_unicode=False) + + >>> A = MatrixSymbol('A', 2, 2) + >>> B = MatrixSymbol('B', 2, 2) + >>> C = MatrixSymbol('C', 2, 2) + + Hadamard product associativity: + + >>> X = HadamardProduct(A, HadamardProduct(B, C)) + >>> X + A.*(B.*C) + >>> canonicalize(X) + A.*B.*C + + Hadamard product commutativity: + + >>> X = HadamardProduct(A, B) + >>> Y = HadamardProduct(B, A) + >>> X + A.*B + >>> Y + B.*A + >>> canonicalize(X) + A.*B + >>> canonicalize(Y) + A.*B + + Hadamard product identity: + + >>> X = HadamardProduct(A, OneMatrix(2, 2)) + >>> X + A.*1 + >>> canonicalize(X) + A + + Absorbing element of Hadamard product: + + >>> X = HadamardProduct(A, ZeroMatrix(2, 2)) + >>> X + A.*0 + >>> canonicalize(X) + 0 + + Rewriting to Hadamard Power + + >>> X = HadamardProduct(A, A, A) + >>> X + A.*A.*A + >>> canonicalize(X) + .3 + A + + Notes + ===== + + As the Hadamard product is associative, nested products can be flattened. + + The Hadamard product is commutative so that factors can be sorted for + canonical form. + + A matrix of only ones is an identity for Hadamard product, + so every matrices of only ones can be removed. + + Any zero matrix will make the whole product a zero matrix. + + Duplicate elements can be collected and rewritten as HadamardPower + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hadamard_product_(matrices) + """ + # Associativity + rule = condition( + lambda x: isinstance(x, HadamardProduct), + flatten + ) + fun = exhaust(rule) + x = fun(x) + + # Identity + fun = condition( + lambda x: isinstance(x, HadamardProduct), + rm_id(lambda x: isinstance(x, OneMatrix)) + ) + x = fun(x) + + # Absorbing by Zero Matrix + def absorb(x): + if any(isinstance(c, ZeroMatrix) for c in x.args): + return ZeroMatrix(*x.shape) + else: + return x + fun = condition( + lambda x: isinstance(x, HadamardProduct), + absorb + ) + x = fun(x) + + # Rewriting with HadamardPower + if isinstance(x, HadamardProduct): + tally = Counter(x.args) + + new_arg = [] + for base, exp in tally.items(): + if exp == 1: + new_arg.append(base) + else: + new_arg.append(HadamardPower(base, exp)) + + x = HadamardProduct(*new_arg) + + # Commutativity + fun = condition( + lambda x: isinstance(x, HadamardProduct), + sort(default_sort_key) + ) + x = fun(x) + + # Unpacking + x = unpack(x) + return x + + +def hadamard_power(base, exp): + base = sympify(base) + exp = sympify(exp) + if exp == 1: + return base + if not base.is_Matrix: + return base**exp + if exp.is_Matrix: + raise ValueError("cannot raise expression to a matrix") + return HadamardPower(base, exp) + + +class HadamardPower(MatrixExpr): + r""" + Elementwise power of matrix expressions + + Parameters + ========== + + base : scalar or matrix + + exp : scalar or matrix + + Notes + ===== + + There are four definitions for the hadamard power which can be used. + Let's consider `A, B` as `(m, n)` matrices, and `a, b` as scalars. + + Matrix raised to a scalar exponent: + + .. math:: + A^{\circ b} = \begin{bmatrix} + A_{0, 0}^b & A_{0, 1}^b & \cdots & A_{0, n-1}^b \\ + A_{1, 0}^b & A_{1, 1}^b & \cdots & A_{1, n-1}^b \\ + \vdots & \vdots & \ddots & \vdots \\ + A_{m-1, 0}^b & A_{m-1, 1}^b & \cdots & A_{m-1, n-1}^b + \end{bmatrix} + + Scalar raised to a matrix exponent: + + .. math:: + a^{\circ B} = \begin{bmatrix} + a^{B_{0, 0}} & a^{B_{0, 1}} & \cdots & a^{B_{0, n-1}} \\ + a^{B_{1, 0}} & a^{B_{1, 1}} & \cdots & a^{B_{1, n-1}} \\ + \vdots & \vdots & \ddots & \vdots \\ + a^{B_{m-1, 0}} & a^{B_{m-1, 1}} & \cdots & a^{B_{m-1, n-1}} + \end{bmatrix} + + Matrix raised to a matrix exponent: + + .. math:: + A^{\circ B} = \begin{bmatrix} + A_{0, 0}^{B_{0, 0}} & A_{0, 1}^{B_{0, 1}} & + \cdots & A_{0, n-1}^{B_{0, n-1}} \\ + A_{1, 0}^{B_{1, 0}} & A_{1, 1}^{B_{1, 1}} & + \cdots & A_{1, n-1}^{B_{1, n-1}} \\ + \vdots & \vdots & + \ddots & \vdots \\ + A_{m-1, 0}^{B_{m-1, 0}} & A_{m-1, 1}^{B_{m-1, 1}} & + \cdots & A_{m-1, n-1}^{B_{m-1, n-1}} + \end{bmatrix} + + Scalar raised to a scalar exponent: + + .. math:: + a^{\circ b} = a^b + """ + + def __new__(cls, base, exp): + base = sympify(base) + exp = sympify(exp) + + if base.is_scalar and exp.is_scalar: + return base ** exp + + if isinstance(base, MatrixExpr) and isinstance(exp, MatrixExpr): + validate(base, exp) + + obj = super().__new__(cls, base, exp) + return obj + + @property + def base(self): + return self._args[0] + + @property + def exp(self): + return self._args[1] + + @property + def shape(self): + if self.base.is_Matrix: + return self.base.shape + return self.exp.shape + + def _entry(self, i, j, **kwargs): + base = self.base + exp = self.exp + + if base.is_Matrix: + a = base._entry(i, j, **kwargs) + elif base.is_scalar: + a = base + else: + raise ValueError( + 'The base {} must be a scalar or a matrix.'.format(base)) + + if exp.is_Matrix: + b = exp._entry(i, j, **kwargs) + elif exp.is_scalar: + b = exp + else: + raise ValueError( + 'The exponent {} must be a scalar or a matrix.'.format(exp)) + + return a ** b + + def _eval_transpose(self): + from sympy.matrices.expressions.transpose import transpose + return HadamardPower(transpose(self.base), self.exp) + + def _eval_derivative(self, x): + dexp = self.exp.diff(x) + logbase = self.base.applyfunc(log) + dlbase = logbase.diff(x) + return hadamard_product( + dexp*logbase + self.exp*dlbase, + self + ) + + def _eval_derivative_matrix_lines(self, x): + from sympy.tensor.array.expressions.array_expressions import ArrayTensorProduct + from sympy.tensor.array.expressions.array_expressions import ArrayDiagonal + from sympy.matrices.expressions.matexpr import _make_matrix + + lr = self.base._eval_derivative_matrix_lines(x) + for i in lr: + diagonal = [(1, 2), (3, 4)] + diagonal = [e for j, e in enumerate(diagonal) if self.base.shape[j] != 1] + l1 = i._lines[i._first_line_index] + l2 = i._lines[i._second_line_index] + subexpr = ExprBuilder( + ArrayDiagonal, + [ + ExprBuilder( + ArrayTensorProduct, + [ + ExprBuilder(_make_matrix, [l1]), + self.exp*hadamard_power(self.base, self.exp-1), + ExprBuilder(_make_matrix, [l2]), + ] + ), + *diagonal], + validator=ArrayDiagonal._validate + ) + i._first_pointer_parent = subexpr.args[0].args[0].args + i._first_pointer_index = 0 + i._first_line_index = 0 + i._second_pointer_parent = subexpr.args[0].args[2].args + i._second_pointer_index = 0 + i._second_line_index = 0 + i._lines = [subexpr] + return lr diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/inverse.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/inverse.py new file mode 100644 index 0000000000000000000000000000000000000000..cfc3feccd7126a761f18f23599eed9413c86a9e5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/inverse.py @@ -0,0 +1,112 @@ +from sympy.core.sympify import _sympify +from sympy.core import S, Basic + +from sympy.matrices.exceptions import NonSquareMatrixError +from sympy.matrices.expressions.matpow import MatPow + + +class Inverse(MatPow): + """ + The multiplicative inverse of a matrix expression + + This is a symbolic object that simply stores its argument without + evaluating it. To actually compute the inverse, use the ``.inverse()`` + method of matrices. + + Examples + ======== + + >>> from sympy import MatrixSymbol, Inverse + >>> A = MatrixSymbol('A', 3, 3) + >>> B = MatrixSymbol('B', 3, 3) + >>> Inverse(A) + A**(-1) + >>> A.inverse() == Inverse(A) + True + >>> (A*B).inverse() + B**(-1)*A**(-1) + >>> Inverse(A*B) + (A*B)**(-1) + + """ + is_Inverse = True + exp = S.NegativeOne + + def __new__(cls, mat, exp=S.NegativeOne): + # exp is there to make it consistent with + # inverse.func(*inverse.args) == inverse + mat = _sympify(mat) + exp = _sympify(exp) + if not mat.is_Matrix: + raise TypeError("mat should be a matrix") + if mat.is_square is False: + raise NonSquareMatrixError("Inverse of non-square matrix %s" % mat) + return Basic.__new__(cls, mat, exp) + + @property + def arg(self): + return self.args[0] + + @property + def shape(self): + return self.arg.shape + + def _eval_inverse(self): + return self.arg + + def _eval_transpose(self): + return Inverse(self.arg.transpose()) + + def _eval_adjoint(self): + return Inverse(self.arg.adjoint()) + + def _eval_conjugate(self): + return Inverse(self.arg.conjugate()) + + def _eval_determinant(self): + from sympy.matrices.expressions.determinant import det + return 1/det(self.arg) + + def doit(self, **hints): + if 'inv_expand' in hints and hints['inv_expand'] == False: + return self + + arg = self.arg + if hints.get('deep', True): + arg = arg.doit(**hints) + + return arg.inverse() + + def _eval_derivative_matrix_lines(self, x): + arg = self.args[0] + lines = arg._eval_derivative_matrix_lines(x) + for line in lines: + line.first_pointer *= -self.T + line.second_pointer *= self + return lines + + +from sympy.assumptions.ask import ask, Q +from sympy.assumptions.refine import handlers_dict + + +def refine_Inverse(expr, assumptions): + """ + >>> from sympy import MatrixSymbol, Q, assuming, refine + >>> X = MatrixSymbol('X', 2, 2) + >>> X.I + X**(-1) + >>> with assuming(Q.orthogonal(X)): + ... print(refine(X.I)) + X.T + """ + if ask(Q.orthogonal(expr), assumptions): + return expr.arg.T + elif ask(Q.unitary(expr), assumptions): + return expr.arg.conjugate() + elif ask(Q.singular(expr), assumptions): + raise ValueError("Inverse of singular matrix %s" % expr.arg) + + return expr + +handlers_dict['Inverse'] = refine_Inverse diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/kronecker.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/kronecker.py new file mode 100644 index 0000000000000000000000000000000000000000..1dd175cb0d500af3e786e2d0dbf6b010947840b4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/kronecker.py @@ -0,0 +1,434 @@ +"""Implementation of the Kronecker product""" +from functools import reduce +from math import prod + +from sympy.core import Mul, sympify +from sympy.functions import adjoint +from sympy.matrices.exceptions import ShapeError +from sympy.matrices.expressions.matexpr import MatrixExpr +from sympy.matrices.expressions.transpose import transpose +from sympy.matrices.expressions.special import Identity +from sympy.matrices.matrixbase import MatrixBase +from sympy.strategies import ( + canon, condition, distribute, do_one, exhaust, flatten, typed, unpack) +from sympy.strategies.traverse import bottom_up +from sympy.utilities import sift + +from .matadd import MatAdd +from .matmul import MatMul +from .matpow import MatPow + + +def kronecker_product(*matrices): + """ + The Kronecker product of two or more arguments. + + This computes the explicit Kronecker product for subclasses of + ``MatrixBase`` i.e. explicit matrices. Otherwise, a symbolic + ``KroneckerProduct`` object is returned. + + + Examples + ======== + + For ``MatrixSymbol`` arguments a ``KroneckerProduct`` object is returned. + Elements of this matrix can be obtained by indexing, or for MatrixSymbols + with known dimension the explicit matrix can be obtained with + ``.as_explicit()`` + + >>> from sympy import kronecker_product, MatrixSymbol + >>> A = MatrixSymbol('A', 2, 2) + >>> B = MatrixSymbol('B', 2, 2) + >>> kronecker_product(A) + A + >>> kronecker_product(A, B) + KroneckerProduct(A, B) + >>> kronecker_product(A, B)[0, 1] + A[0, 0]*B[0, 1] + >>> kronecker_product(A, B).as_explicit() + Matrix([ + [A[0, 0]*B[0, 0], A[0, 0]*B[0, 1], A[0, 1]*B[0, 0], A[0, 1]*B[0, 1]], + [A[0, 0]*B[1, 0], A[0, 0]*B[1, 1], A[0, 1]*B[1, 0], A[0, 1]*B[1, 1]], + [A[1, 0]*B[0, 0], A[1, 0]*B[0, 1], A[1, 1]*B[0, 0], A[1, 1]*B[0, 1]], + [A[1, 0]*B[1, 0], A[1, 0]*B[1, 1], A[1, 1]*B[1, 0], A[1, 1]*B[1, 1]]]) + + For explicit matrices the Kronecker product is returned as a Matrix + + >>> from sympy import Matrix, kronecker_product + >>> sigma_x = Matrix([ + ... [0, 1], + ... [1, 0]]) + ... + >>> Isigma_y = Matrix([ + ... [0, 1], + ... [-1, 0]]) + ... + >>> kronecker_product(sigma_x, Isigma_y) + Matrix([ + [ 0, 0, 0, 1], + [ 0, 0, -1, 0], + [ 0, 1, 0, 0], + [-1, 0, 0, 0]]) + + See Also + ======== + KroneckerProduct + + """ + if not matrices: + raise TypeError("Empty Kronecker product is undefined") + if len(matrices) == 1: + return matrices[0] + else: + return KroneckerProduct(*matrices).doit() + + +class KroneckerProduct(MatrixExpr): + """ + The Kronecker product of two or more arguments. + + The Kronecker product is a non-commutative product of matrices. + Given two matrices of dimension (m, n) and (s, t) it produces a matrix + of dimension (m s, n t). + + This is a symbolic object that simply stores its argument without + evaluating it. To actually compute the product, use the function + ``kronecker_product()`` or call the ``.doit()`` or ``.as_explicit()`` + methods. + + >>> from sympy import KroneckerProduct, MatrixSymbol + >>> A = MatrixSymbol('A', 5, 5) + >>> B = MatrixSymbol('B', 5, 5) + >>> isinstance(KroneckerProduct(A, B), KroneckerProduct) + True + """ + is_KroneckerProduct = True + + def __new__(cls, *args, check=True): + args = list(map(sympify, args)) + if all(a.is_Identity for a in args): + ret = Identity(prod(a.rows for a in args)) + if all(isinstance(a, MatrixBase) for a in args): + return ret.as_explicit() + else: + return ret + + if check: + validate(*args) + return super().__new__(cls, *args) + + @property + def shape(self): + rows, cols = self.args[0].shape + for mat in self.args[1:]: + rows *= mat.rows + cols *= mat.cols + return (rows, cols) + + def _entry(self, i, j, **kwargs): + result = 1 + for mat in reversed(self.args): + i, m = divmod(i, mat.rows) + j, n = divmod(j, mat.cols) + result *= mat[m, n] + return result + + def _eval_adjoint(self): + return KroneckerProduct(*list(map(adjoint, self.args))).doit() + + def _eval_conjugate(self): + return KroneckerProduct(*[a.conjugate() for a in self.args]).doit() + + def _eval_transpose(self): + return KroneckerProduct(*list(map(transpose, self.args))).doit() + + def _eval_trace(self): + from .trace import trace + return Mul(*[trace(a) for a in self.args]) + + def _eval_determinant(self): + from .determinant import det, Determinant + if not all(a.is_square for a in self.args): + return Determinant(self) + + m = self.rows + return Mul(*[det(a)**(m/a.rows) for a in self.args]) + + def _eval_inverse(self): + try: + return KroneckerProduct(*[a.inverse() for a in self.args]) + except ShapeError: + from sympy.matrices.expressions.inverse import Inverse + return Inverse(self) + + def structurally_equal(self, other): + '''Determine whether two matrices have the same Kronecker product structure + + Examples + ======== + + >>> from sympy import KroneckerProduct, MatrixSymbol, symbols + >>> m, n = symbols(r'm, n', integer=True) + >>> A = MatrixSymbol('A', m, m) + >>> B = MatrixSymbol('B', n, n) + >>> C = MatrixSymbol('C', m, m) + >>> D = MatrixSymbol('D', n, n) + >>> KroneckerProduct(A, B).structurally_equal(KroneckerProduct(C, D)) + True + >>> KroneckerProduct(A, B).structurally_equal(KroneckerProduct(D, C)) + False + >>> KroneckerProduct(A, B).structurally_equal(C) + False + ''' + # Inspired by BlockMatrix + return (isinstance(other, KroneckerProduct) + and self.shape == other.shape + and len(self.args) == len(other.args) + and all(a.shape == b.shape for (a, b) in zip(self.args, other.args))) + + def has_matching_shape(self, other): + '''Determine whether two matrices have the appropriate structure to bring matrix + multiplication inside the KroneckerProdut + + Examples + ======== + >>> from sympy import KroneckerProduct, MatrixSymbol, symbols + >>> m, n = symbols(r'm, n', integer=True) + >>> A = MatrixSymbol('A', m, n) + >>> B = MatrixSymbol('B', n, m) + >>> KroneckerProduct(A, B).has_matching_shape(KroneckerProduct(B, A)) + True + >>> KroneckerProduct(A, B).has_matching_shape(KroneckerProduct(A, B)) + False + >>> KroneckerProduct(A, B).has_matching_shape(A) + False + ''' + return (isinstance(other, KroneckerProduct) + and self.cols == other.rows + and len(self.args) == len(other.args) + and all(a.cols == b.rows for (a, b) in zip(self.args, other.args))) + + def _eval_expand_kroneckerproduct(self, **hints): + return flatten(canon(typed({KroneckerProduct: distribute(KroneckerProduct, MatAdd)}))(self)) + + def _kronecker_add(self, other): + if self.structurally_equal(other): + return self.__class__(*[a + b for (a, b) in zip(self.args, other.args)]) + else: + return self + other + + def _kronecker_mul(self, other): + if self.has_matching_shape(other): + return self.__class__(*[a*b for (a, b) in zip(self.args, other.args)]) + else: + return self * other + + def doit(self, **hints): + deep = hints.get('deep', True) + if deep: + args = [arg.doit(**hints) for arg in self.args] + else: + args = self.args + return canonicalize(KroneckerProduct(*args)) + + +def validate(*args): + if not all(arg.is_Matrix for arg in args): + raise TypeError("Mix of Matrix and Scalar symbols") + + +# rules + +def extract_commutative(kron): + c_part = [] + nc_part = [] + for arg in kron.args: + c, nc = arg.args_cnc() + c_part.extend(c) + nc_part.append(Mul._from_args(nc)) + + c_part = Mul(*c_part) + if c_part != 1: + return c_part*KroneckerProduct(*nc_part) + return kron + + +def matrix_kronecker_product(*matrices): + """Compute the Kronecker product of a sequence of SymPy Matrices. + + This is the standard Kronecker product of matrices [1]. + + Parameters + ========== + + matrices : tuple of MatrixBase instances + The matrices to take the Kronecker product of. + + Returns + ======= + + matrix : MatrixBase + The Kronecker product matrix. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.matrices.expressions.kronecker import ( + ... matrix_kronecker_product) + + >>> m1 = Matrix([[1,2],[3,4]]) + >>> m2 = Matrix([[1,0],[0,1]]) + >>> matrix_kronecker_product(m1, m2) + Matrix([ + [1, 0, 2, 0], + [0, 1, 0, 2], + [3, 0, 4, 0], + [0, 3, 0, 4]]) + >>> matrix_kronecker_product(m2, m1) + Matrix([ + [1, 2, 0, 0], + [3, 4, 0, 0], + [0, 0, 1, 2], + [0, 0, 3, 4]]) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Kronecker_product + """ + # Make sure we have a sequence of Matrices + if not all(isinstance(m, MatrixBase) for m in matrices): + raise TypeError( + 'Sequence of Matrices expected, got: %s' % repr(matrices) + ) + + # Pull out the first element in the product. + matrix_expansion = matrices[-1] + # Do the kronecker product working from right to left. + for mat in reversed(matrices[:-1]): + rows = mat.rows + cols = mat.cols + # Go through each row appending kronecker product to. + # running matrix_expansion. + for i in range(rows): + start = matrix_expansion*mat[i*cols] + # Go through each column joining each item + for j in range(cols - 1): + start = start.row_join( + matrix_expansion*mat[i*cols + j + 1] + ) + # If this is the first element, make it the start of the + # new row. + if i == 0: + next = start + else: + next = next.col_join(start) + matrix_expansion = next + + MatrixClass = max(matrices, key=lambda M: M._class_priority).__class__ + if isinstance(matrix_expansion, MatrixClass): + return matrix_expansion + else: + return MatrixClass(matrix_expansion) + + +def explicit_kronecker_product(kron): + # Make sure we have a sequence of Matrices + if not all(isinstance(m, MatrixBase) for m in kron.args): + return kron + + return matrix_kronecker_product(*kron.args) + + +rules = (unpack, + explicit_kronecker_product, + flatten, + extract_commutative) + +canonicalize = exhaust(condition(lambda x: isinstance(x, KroneckerProduct), + do_one(*rules))) + + +def _kronecker_dims_key(expr): + if isinstance(expr, KroneckerProduct): + return tuple(a.shape for a in expr.args) + else: + return (0,) + + +def kronecker_mat_add(expr): + args = sift(expr.args, _kronecker_dims_key) + nonkrons = args.pop((0,), None) + if not args: + return expr + + krons = [reduce(lambda x, y: x._kronecker_add(y), group) + for group in args.values()] + + if not nonkrons: + return MatAdd(*krons) + else: + return MatAdd(*krons) + nonkrons + + +def kronecker_mat_mul(expr): + # modified from block matrix code + factor, matrices = expr.as_coeff_matrices() + + i = 0 + while i < len(matrices) - 1: + A, B = matrices[i:i+2] + if isinstance(A, KroneckerProduct) and isinstance(B, KroneckerProduct): + matrices[i] = A._kronecker_mul(B) + matrices.pop(i+1) + else: + i += 1 + + return factor*MatMul(*matrices) + + +def kronecker_mat_pow(expr): + if isinstance(expr.base, KroneckerProduct) and all(a.is_square for a in expr.base.args): + return KroneckerProduct(*[MatPow(a, expr.exp) for a in expr.base.args]) + else: + return expr + + +def combine_kronecker(expr): + """Combine KronekeckerProduct with expression. + + If possible write operations on KroneckerProducts of compatible shapes + as a single KroneckerProduct. + + Examples + ======== + + >>> from sympy.matrices.expressions import combine_kronecker + >>> from sympy import MatrixSymbol, KroneckerProduct, symbols + >>> m, n = symbols(r'm, n', integer=True) + >>> A = MatrixSymbol('A', m, n) + >>> B = MatrixSymbol('B', n, m) + >>> combine_kronecker(KroneckerProduct(A, B)*KroneckerProduct(B, A)) + KroneckerProduct(A*B, B*A) + >>> combine_kronecker(KroneckerProduct(A, B)+KroneckerProduct(B.T, A.T)) + KroneckerProduct(A + B.T, B + A.T) + >>> C = MatrixSymbol('C', n, n) + >>> D = MatrixSymbol('D', m, m) + >>> combine_kronecker(KroneckerProduct(C, D)**m) + KroneckerProduct(C**m, D**m) + """ + def haskron(expr): + return isinstance(expr, MatrixExpr) and expr.has(KroneckerProduct) + + rule = exhaust( + bottom_up(exhaust(condition(haskron, typed( + {MatAdd: kronecker_mat_add, + MatMul: kronecker_mat_mul, + MatPow: kronecker_mat_pow}))))) + result = rule(expr) + doit = getattr(result, 'doit', None) + if doit is not None: + return doit() + else: + return result diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/matadd.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/matadd.py new file mode 100644 index 0000000000000000000000000000000000000000..cfae1e5010e4077c7210c85c4315ed2404f245d7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/matadd.py @@ -0,0 +1,155 @@ +from functools import reduce +import operator + +from sympy.core import Basic, sympify +from sympy.core.add import add, Add, _could_extract_minus_sign +from sympy.core.sorting import default_sort_key +from sympy.functions import adjoint +from sympy.matrices.matrixbase import MatrixBase +from sympy.matrices.expressions.transpose import transpose +from sympy.strategies import (rm_id, unpack, flatten, sort, condition, + exhaust, do_one, glom) +from sympy.matrices.expressions.matexpr import MatrixExpr +from sympy.matrices.expressions.special import ZeroMatrix, GenericZeroMatrix +from sympy.matrices.expressions._shape import validate_matadd_integer as validate +from sympy.utilities.iterables import sift +from sympy.utilities.exceptions import sympy_deprecation_warning + +# XXX: MatAdd should perhaps not subclass directly from Add +class MatAdd(MatrixExpr, Add): + """A Sum of Matrix Expressions + + MatAdd inherits from and operates like SymPy Add + + Examples + ======== + + >>> from sympy import MatAdd, MatrixSymbol + >>> A = MatrixSymbol('A', 5, 5) + >>> B = MatrixSymbol('B', 5, 5) + >>> C = MatrixSymbol('C', 5, 5) + >>> MatAdd(A, B, C) + A + B + C + """ + is_MatAdd = True + + identity = GenericZeroMatrix() + + def __new__(cls, *args, evaluate=False, check=None, _sympify=True): + if not args: + return cls.identity + + # This must be removed aggressively in the constructor to avoid + # TypeErrors from GenericZeroMatrix().shape + args = list(filter(lambda i: cls.identity != i, args)) + if _sympify: + args = list(map(sympify, args)) + + if not all(isinstance(arg, MatrixExpr) for arg in args): + raise TypeError("Mix of Matrix and Scalar symbols") + + obj = Basic.__new__(cls, *args) + + if check is not None: + sympy_deprecation_warning( + "Passing check to MatAdd is deprecated and the check argument will be removed in a future version.", + deprecated_since_version="1.11", + active_deprecations_target='remove-check-argument-from-matrix-operations') + + if check is not False: + validate(*args) + + if evaluate: + obj = cls._evaluate(obj) + + return obj + + @classmethod + def _evaluate(cls, expr): + return canonicalize(expr) + + @property + def shape(self): + return self.args[0].shape + + def could_extract_minus_sign(self): + return _could_extract_minus_sign(self) + + def expand(self, **kwargs): + expanded = super(MatAdd, self).expand(**kwargs) + return self._evaluate(expanded) + + def _entry(self, i, j, **kwargs): + return Add(*[arg._entry(i, j, **kwargs) for arg in self.args]) + + def _eval_transpose(self): + return MatAdd(*[transpose(arg) for arg in self.args]).doit() + + def _eval_adjoint(self): + return MatAdd(*[adjoint(arg) for arg in self.args]).doit() + + def _eval_trace(self): + from .trace import trace + return Add(*[trace(arg) for arg in self.args]).doit() + + def doit(self, **hints): + deep = hints.get('deep', True) + if deep: + args = [arg.doit(**hints) for arg in self.args] + else: + args = self.args + return canonicalize(MatAdd(*args)) + + def _eval_derivative_matrix_lines(self, x): + add_lines = [arg._eval_derivative_matrix_lines(x) for arg in self.args] + return [j for i in add_lines for j in i] + +add.register_handlerclass((Add, MatAdd), MatAdd) + + +factor_of = lambda arg: arg.as_coeff_mmul()[0] +matrix_of = lambda arg: unpack(arg.as_coeff_mmul()[1]) +def combine(cnt, mat): + if cnt == 1: + return mat + else: + return cnt * mat + + +def merge_explicit(matadd): + """ Merge explicit MatrixBase arguments + + Examples + ======== + + >>> from sympy import MatrixSymbol, eye, Matrix, MatAdd, pprint + >>> from sympy.matrices.expressions.matadd import merge_explicit + >>> A = MatrixSymbol('A', 2, 2) + >>> B = eye(2) + >>> C = Matrix([[1, 2], [3, 4]]) + >>> X = MatAdd(A, B, C) + >>> pprint(X) + [1 0] [1 2] + A + [ ] + [ ] + [0 1] [3 4] + >>> pprint(merge_explicit(X)) + [2 2] + A + [ ] + [3 5] + """ + groups = sift(matadd.args, lambda arg: isinstance(arg, MatrixBase)) + if len(groups[True]) > 1: + return MatAdd(*(groups[False] + [reduce(operator.add, groups[True])])) + else: + return matadd + + +rules = (rm_id(lambda x: x == 0 or isinstance(x, ZeroMatrix)), + unpack, + flatten, + glom(matrix_of, factor_of, combine), + merge_explicit, + sort(default_sort_key)) + +canonicalize = exhaust(condition(lambda x: isinstance(x, MatAdd), + do_one(*rules))) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/matexpr.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/matexpr.py new file mode 100644 index 0000000000000000000000000000000000000000..a4e99296ccfcbdac5e09a86ecee020adf9831c73 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/matexpr.py @@ -0,0 +1,888 @@ +from __future__ import annotations +from functools import wraps + +from sympy.core import S, Integer, Basic, Mul, Add +from sympy.core.assumptions import check_assumptions +from sympy.core.decorators import call_highest_priority +from sympy.core.expr import Expr, ExprBuilder +from sympy.core.logic import FuzzyBool +from sympy.core.symbol import Str, Dummy, symbols, Symbol +from sympy.core.sympify import SympifyError, _sympify +from sympy.external.gmpy import SYMPY_INTS +from sympy.functions import conjugate, adjoint +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.matrices.exceptions import NonSquareMatrixError +from sympy.matrices.kind import MatrixKind +from sympy.matrices.matrixbase import MatrixBase +from sympy.multipledispatch import dispatch +from sympy.utilities.misc import filldedent + + +def _sympifyit(arg, retval=None): + # This version of _sympifyit sympifies MutableMatrix objects + def deco(func): + @wraps(func) + def __sympifyit_wrapper(a, b): + try: + b = _sympify(b) + return func(a, b) + except SympifyError: + return retval + + return __sympifyit_wrapper + + return deco + + +class MatrixExpr(Expr): + """Superclass for Matrix Expressions + + MatrixExprs represent abstract matrices, linear transformations represented + within a particular basis. + + Examples + ======== + + >>> from sympy import MatrixSymbol + >>> A = MatrixSymbol('A', 3, 3) + >>> y = MatrixSymbol('y', 3, 1) + >>> x = (A.T*A).I * A * y + + See Also + ======== + + MatrixSymbol, MatAdd, MatMul, Transpose, Inverse + """ + __slots__: tuple[str, ...] = () + + # Should not be considered iterable by the + # sympy.utilities.iterables.iterable function. Subclass that actually are + # iterable (i.e., explicit matrices) should set this to True. + _iterable = False + + _op_priority = 11.0 + + is_Matrix: bool = True + is_MatrixExpr: bool = True + is_Identity: FuzzyBool = None + is_Inverse = False + is_Transpose = False + is_ZeroMatrix = False + is_MatAdd = False + is_MatMul = False + + is_commutative = False + is_number = False + is_symbol = False + is_scalar = False + + kind: MatrixKind = MatrixKind() + + def __new__(cls, *args, **kwargs): + args = map(_sympify, args) + return Basic.__new__(cls, *args, **kwargs) + + # The following is adapted from the core Expr object + + @property + def shape(self) -> tuple[Expr, Expr]: + raise NotImplementedError + + @property + def _add_handler(self): + return MatAdd + + @property + def _mul_handler(self): + return MatMul + + def __neg__(self): + return MatMul(S.NegativeOne, self).doit() + + def __abs__(self): + raise NotImplementedError + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__radd__') + def __add__(self, other): + return MatAdd(self, other).doit() + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__add__') + def __radd__(self, other): + return MatAdd(other, self).doit() + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__rsub__') + def __sub__(self, other): + return MatAdd(self, -other).doit() + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__sub__') + def __rsub__(self, other): + return MatAdd(other, -self).doit() + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__rmul__') + def __mul__(self, other): + return MatMul(self, other).doit() + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__rmul__') + def __matmul__(self, other): + return MatMul(self, other).doit() + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__mul__') + def __rmul__(self, other): + return MatMul(other, self).doit() + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__mul__') + def __rmatmul__(self, other): + return MatMul(other, self).doit() + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__rpow__') + def __pow__(self, other): + return MatPow(self, other).doit() + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__pow__') + def __rpow__(self, other): + raise NotImplementedError("Matrix Power not defined") + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__rtruediv__') + def __truediv__(self, other): + return self * other**S.NegativeOne + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__truediv__') + def __rtruediv__(self, other): + raise NotImplementedError() + #return MatMul(other, Pow(self, S.NegativeOne)) + + @property + def rows(self): + return self.shape[0] + + @property + def cols(self): + return self.shape[1] + + @property + def is_square(self) -> bool | None: + rows, cols = self.shape + if isinstance(rows, Integer) and isinstance(cols, Integer): + return rows == cols + if rows == cols: + return True + return None + + def _eval_conjugate(self): + from sympy.matrices.expressions.adjoint import Adjoint + return Adjoint(Transpose(self)) + + def as_real_imag(self, deep=True, **hints): + return self._eval_as_real_imag() + + def _eval_as_real_imag(self): + real = S.Half * (self + self._eval_conjugate()) + im = (self - self._eval_conjugate())/(2*S.ImaginaryUnit) + return (real, im) + + def _eval_inverse(self): + return Inverse(self) + + def _eval_determinant(self): + return Determinant(self) + + def _eval_transpose(self): + return Transpose(self) + + def _eval_trace(self): + return None + + def _eval_power(self, exp): + """ + Override this in sub-classes to implement simplification of powers. The cases where the exponent + is -1, 0, 1 are already covered in MatPow.doit(), so implementations can exclude these cases. + """ + return MatPow(self, exp) + + def _eval_simplify(self, **kwargs): + if self.is_Atom: + return self + else: + from sympy.simplify import simplify + return self.func(*[simplify(x, **kwargs) for x in self.args]) + + def _eval_adjoint(self): + from sympy.matrices.expressions.adjoint import Adjoint + return Adjoint(self) + + def _eval_derivative_n_times(self, x, n): + return Basic._eval_derivative_n_times(self, x, n) + + def _eval_derivative(self, x): + # `x` is a scalar: + if self.has(x): + # See if there are other methods using it: + return super()._eval_derivative(x) + else: + return ZeroMatrix(*self.shape) + + @classmethod + def _check_dim(cls, dim): + """Helper function to check invalid matrix dimensions""" + ok = not dim.is_Float and check_assumptions( + dim, integer=True, nonnegative=True) + if ok is False: + raise ValueError( + "The dimension specification {} should be " + "a nonnegative integer.".format(dim)) + + + def _entry(self, i, j, **kwargs): + raise NotImplementedError( + "Indexing not implemented for %s" % self.__class__.__name__) + + def adjoint(self): + return adjoint(self) + + def as_coeff_Mul(self, rational=False): + """Efficiently extract the coefficient of a product.""" + return S.One, self + + def conjugate(self): + return conjugate(self) + + def transpose(self): + from sympy.matrices.expressions.transpose import transpose + return transpose(self) + + @property + def T(self): + '''Matrix transposition''' + return self.transpose() + + def inverse(self): + if self.is_square is False: + raise NonSquareMatrixError('Inverse of non-square matrix') + return self._eval_inverse() + + def inv(self): + return self.inverse() + + def det(self): + from sympy.matrices.expressions.determinant import det + return det(self) + + @property + def I(self): + return self.inverse() + + def valid_index(self, i, j): + def is_valid(idx): + return isinstance(idx, (int, Integer, Symbol, Expr)) + return (is_valid(i) and is_valid(j) and + (self.rows is None or + (i >= -self.rows) != False and (i < self.rows) != False) and + (j >= -self.cols) != False and (j < self.cols) != False) + + def __getitem__(self, key): + if not isinstance(key, tuple) and isinstance(key, slice): + from sympy.matrices.expressions.slice import MatrixSlice + return MatrixSlice(self, key, (0, None, 1)) + if isinstance(key, tuple) and len(key) == 2: + i, j = key + if isinstance(i, slice) or isinstance(j, slice): + from sympy.matrices.expressions.slice import MatrixSlice + return MatrixSlice(self, i, j) + i, j = _sympify(i), _sympify(j) + if self.valid_index(i, j) != False: + return self._entry(i, j) + else: + raise IndexError("Invalid indices (%s, %s)" % (i, j)) + elif isinstance(key, (SYMPY_INTS, Integer)): + # row-wise decomposition of matrix + rows, cols = self.shape + # allow single indexing if number of columns is known + if not isinstance(cols, Integer): + raise IndexError(filldedent(''' + Single indexing is only supported when the number + of columns is known.''')) + key = _sympify(key) + i = key // cols + j = key % cols + if self.valid_index(i, j) != False: + return self._entry(i, j) + else: + raise IndexError("Invalid index %s" % key) + elif isinstance(key, (Symbol, Expr)): + raise IndexError(filldedent(''' + Only integers may be used when addressing the matrix + with a single index.''')) + raise IndexError("Invalid index, wanted %s[i,j]" % self) + + def _is_shape_symbolic(self) -> bool: + return (not isinstance(self.rows, (SYMPY_INTS, Integer)) + or not isinstance(self.cols, (SYMPY_INTS, Integer))) + + def as_explicit(self): + """ + Returns a dense Matrix with elements represented explicitly + + Returns an object of type ImmutableDenseMatrix. + + Examples + ======== + + >>> from sympy import Identity + >>> I = Identity(3) + >>> I + I + >>> I.as_explicit() + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + + See Also + ======== + as_mutable: returns mutable Matrix type + + """ + if self._is_shape_symbolic(): + raise ValueError( + 'Matrix with symbolic shape ' + 'cannot be represented explicitly.') + from sympy.matrices.immutable import ImmutableDenseMatrix + return ImmutableDenseMatrix([[self[i, j] + for j in range(self.cols)] + for i in range(self.rows)]) + + def as_mutable(self): + """ + Returns a dense, mutable matrix with elements represented explicitly + + Examples + ======== + + >>> from sympy import Identity + >>> I = Identity(3) + >>> I + I + >>> I.shape + (3, 3) + >>> I.as_mutable() + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + + See Also + ======== + as_explicit: returns ImmutableDenseMatrix + """ + return self.as_explicit().as_mutable() + + def __array__(self, dtype=object, copy=None): + if copy is not None and not copy: + raise TypeError("Cannot implement copy=False when converting Matrix to ndarray") + from numpy import empty + a = empty(self.shape, dtype=object) + for i in range(self.rows): + for j in range(self.cols): + a[i, j] = self[i, j] + return a + + def equals(self, other): + """ + Test elementwise equality between matrices, potentially of different + types + + >>> from sympy import Identity, eye + >>> Identity(3).equals(eye(3)) + True + """ + return self.as_explicit().equals(other) + + def canonicalize(self): + return self + + def as_coeff_mmul(self): + return S.One, MatMul(self) + + @staticmethod + def from_index_summation(expr, first_index=None, last_index=None, dimensions=None): + r""" + Parse expression of matrices with explicitly summed indices into a + matrix expression without indices, if possible. + + This transformation expressed in mathematical notation: + + `\sum_{j=0}^{N-1} A_{i,j} B_{j,k} \Longrightarrow \mathbf{A}\cdot \mathbf{B}` + + Optional parameter ``first_index``: specify which free index to use as + the index starting the expression. + + Examples + ======== + + >>> from sympy import MatrixSymbol, MatrixExpr, Sum + >>> from sympy.abc import i, j, k, l, N + >>> A = MatrixSymbol("A", N, N) + >>> B = MatrixSymbol("B", N, N) + >>> expr = Sum(A[i, j]*B[j, k], (j, 0, N-1)) + >>> MatrixExpr.from_index_summation(expr) + A*B + + Transposition is detected: + + >>> expr = Sum(A[j, i]*B[j, k], (j, 0, N-1)) + >>> MatrixExpr.from_index_summation(expr) + A.T*B + + Detect the trace: + + >>> expr = Sum(A[i, i], (i, 0, N-1)) + >>> MatrixExpr.from_index_summation(expr) + Trace(A) + + More complicated expressions: + + >>> expr = Sum(A[i, j]*B[k, j]*A[l, k], (j, 0, N-1), (k, 0, N-1)) + >>> MatrixExpr.from_index_summation(expr) + A*B.T*A.T + """ + from sympy.tensor.array.expressions.from_indexed_to_array import convert_indexed_to_array + from sympy.tensor.array.expressions.from_array_to_matrix import convert_array_to_matrix + first_indices = [] + if first_index is not None: + first_indices.append(first_index) + if last_index is not None: + first_indices.append(last_index) + arr = convert_indexed_to_array(expr, first_indices=first_indices) + return convert_array_to_matrix(arr) + + def applyfunc(self, func): + from .applyfunc import ElementwiseApplyFunction + return ElementwiseApplyFunction(func, self) + + +@dispatch(MatrixExpr, Expr) +def _eval_is_eq(lhs, rhs): # noqa:F811 + return False + +@dispatch(MatrixExpr, MatrixExpr) # type: ignore +def _eval_is_eq(lhs, rhs): # noqa:F811 + if lhs.shape != rhs.shape: + return False + if (lhs - rhs).is_ZeroMatrix: + return True + +def get_postprocessor(cls): + def _postprocessor(expr): + # To avoid circular imports, we can't have MatMul/MatAdd on the top level + mat_class = {Mul: MatMul, Add: MatAdd}[cls] + nonmatrices = [] + matrices = [] + for term in expr.args: + if isinstance(term, MatrixExpr): + matrices.append(term) + else: + nonmatrices.append(term) + + if not matrices: + return cls._from_args(nonmatrices) + + if nonmatrices: + if cls == Mul: + for i in range(len(matrices)): + if not matrices[i].is_MatrixExpr: + # If one of the matrices explicit, absorb the scalar into it + # (doit will combine all explicit matrices into one, so it + # doesn't matter which) + matrices[i] = matrices[i].__mul__(cls._from_args(nonmatrices)) + nonmatrices = [] + break + + else: + # Maintain the ability to create Add(scalar, matrix) without + # raising an exception. That way different algorithms can + # replace matrix expressions with non-commutative symbols to + # manipulate them like non-commutative scalars. + return cls._from_args(nonmatrices + [mat_class(*matrices).doit(deep=False)]) + + if mat_class == MatAdd: + return mat_class(*matrices).doit(deep=False) + return mat_class(cls._from_args(nonmatrices), *matrices).doit(deep=False) + return _postprocessor + + +Basic._constructor_postprocessor_mapping[MatrixExpr] = { + "Mul": [get_postprocessor(Mul)], + "Add": [get_postprocessor(Add)], +} + + +def _matrix_derivative(expr, x, old_algorithm=False): + + if isinstance(expr, MatrixBase) or isinstance(x, MatrixBase): + # Do not use array expressions for explicit matrices: + old_algorithm = True + + if old_algorithm: + return _matrix_derivative_old_algorithm(expr, x) + + from sympy.tensor.array.expressions.from_matrix_to_array import convert_matrix_to_array + from sympy.tensor.array.expressions.arrayexpr_derivatives import array_derive + from sympy.tensor.array.expressions.from_array_to_matrix import convert_array_to_matrix + + array_expr = convert_matrix_to_array(expr) + diff_array_expr = array_derive(array_expr, x) + diff_matrix_expr = convert_array_to_matrix(diff_array_expr) + return diff_matrix_expr + + +def _matrix_derivative_old_algorithm(expr, x): + from sympy.tensor.array.array_derivatives import ArrayDerivative + lines = expr._eval_derivative_matrix_lines(x) + + parts = [i.build() for i in lines] + + from sympy.tensor.array.expressions.from_array_to_matrix import convert_array_to_matrix + + parts = [[convert_array_to_matrix(j) for j in i] for i in parts] + + def _get_shape(elem): + if isinstance(elem, MatrixExpr): + return elem.shape + return 1, 1 + + def get_rank(parts): + return sum(j not in (1, None) for i in parts for j in _get_shape(i)) + + ranks = [get_rank(i) for i in parts] + rank = ranks[0] + + def contract_one_dims(parts): + if len(parts) == 1: + return parts[0] + else: + p1, p2 = parts[:2] + if p2.is_Matrix: + p2 = p2.T + if p1 == Identity(1): + pbase = p2 + elif p2 == Identity(1): + pbase = p1 + else: + pbase = p1*p2 + if len(parts) == 2: + return pbase + else: # len(parts) > 2 + if pbase.is_Matrix: + raise ValueError("") + return pbase*Mul.fromiter(parts[2:]) + + if rank <= 2: + return Add.fromiter([contract_one_dims(i) for i in parts]) + + return ArrayDerivative(expr, x) + + +class MatrixElement(Expr): + parent = property(lambda self: self.args[0]) + i = property(lambda self: self.args[1]) + j = property(lambda self: self.args[2]) + _diff_wrt = True + is_symbol = True + is_commutative = True + + def __new__(cls, name, n, m): + n, m = map(_sympify, (n, m)) + if isinstance(name, str): + name = Symbol(name) + else: + if isinstance(name, MatrixBase): + if n.is_Integer and m.is_Integer: + return name[n, m] + name = _sympify(name) # change mutable into immutable + else: + name = _sympify(name) + if not isinstance(name.kind, MatrixKind): + raise TypeError("First argument of MatrixElement should be a matrix") + if not getattr(name, 'valid_index', lambda n, m: True)(n, m): + raise IndexError('indices out of range') + obj = Expr.__new__(cls, name, n, m) + return obj + + @property + def symbol(self): + return self.args[0] + + def doit(self, **hints): + deep = hints.get('deep', True) + if deep: + args = [arg.doit(**hints) for arg in self.args] + else: + args = self.args + return args[0][args[1], args[2]] + + @property + def indices(self): + return self.args[1:] + + def _eval_derivative(self, v): + + if not isinstance(v, MatrixElement): + return self.parent.diff(v)[self.i, self.j] + + M = self.args[0] + + m, n = self.parent.shape + + if M == v.args[0]: + return KroneckerDelta(self.args[1], v.args[1], (0, m-1)) * \ + KroneckerDelta(self.args[2], v.args[2], (0, n-1)) + + if isinstance(M, Inverse): + from sympy.concrete.summations import Sum + i, j = self.args[1:] + i1, i2 = symbols("z1, z2", cls=Dummy) + Y = M.args[0] + r1, r2 = Y.shape + return -Sum(M[i, i1]*Y[i1, i2].diff(v)*M[i2, j], (i1, 0, r1-1), (i2, 0, r2-1)) + + if self.has(v.args[0]): + return None + + return S.Zero + + +class MatrixSymbol(MatrixExpr): + """Symbolic representation of a Matrix object + + Creates a SymPy Symbol to represent a Matrix. This matrix has a shape and + can be included in Matrix Expressions + + Examples + ======== + + >>> from sympy import MatrixSymbol, Identity + >>> A = MatrixSymbol('A', 3, 4) # A 3 by 4 Matrix + >>> B = MatrixSymbol('B', 4, 3) # A 4 by 3 Matrix + >>> A.shape + (3, 4) + >>> 2*A*B + Identity(3) + I + 2*A*B + """ + is_commutative = False + is_symbol = True + _diff_wrt = True + + def __new__(cls, name, n, m): + n, m = _sympify(n), _sympify(m) + + cls._check_dim(m) + cls._check_dim(n) + + if isinstance(name, str): + name = Str(name) + obj = Basic.__new__(cls, name, n, m) + return obj + + @property + def shape(self): + return self.args[1], self.args[2] + + @property + def name(self): + return self.args[0].name + + def _entry(self, i, j, **kwargs): + return MatrixElement(self, i, j) + + @property + def free_symbols(self): + return {self} + + def _eval_simplify(self, **kwargs): + return self + + def _eval_derivative(self, x): + # x is a scalar: + return ZeroMatrix(self.shape[0], self.shape[1]) + + def _eval_derivative_matrix_lines(self, x): + if self != x: + first = ZeroMatrix(x.shape[0], self.shape[0]) if self.shape[0] != 1 else S.Zero + second = ZeroMatrix(x.shape[1], self.shape[1]) if self.shape[1] != 1 else S.Zero + return [_LeftRightArgs( + [first, second], + )] + else: + first = Identity(self.shape[0]) if self.shape[0] != 1 else S.One + second = Identity(self.shape[1]) if self.shape[1] != 1 else S.One + return [_LeftRightArgs( + [first, second], + )] + + +def matrix_symbols(expr): + return [sym for sym in expr.free_symbols if sym.is_Matrix] + + +class _LeftRightArgs: + r""" + Helper class to compute matrix derivatives. + + The logic: when an expression is derived by a matrix `X_{mn}`, two lines of + matrix multiplications are created: the one contracted to `m` (first line), + and the one contracted to `n` (second line). + + Transposition flips the side by which new matrices are connected to the + lines. + + The trace connects the end of the two lines. + """ + + def __init__(self, lines, higher=S.One): + self._lines = list(lines) + self._first_pointer_parent = self._lines + self._first_pointer_index = 0 + self._first_line_index = 0 + self._second_pointer_parent = self._lines + self._second_pointer_index = 1 + self._second_line_index = 1 + self.higher = higher + + @property + def first_pointer(self): + return self._first_pointer_parent[self._first_pointer_index] + + @first_pointer.setter + def first_pointer(self, value): + self._first_pointer_parent[self._first_pointer_index] = value + + @property + def second_pointer(self): + return self._second_pointer_parent[self._second_pointer_index] + + @second_pointer.setter + def second_pointer(self, value): + self._second_pointer_parent[self._second_pointer_index] = value + + def __repr__(self): + built = [self._build(i) for i in self._lines] + return "_LeftRightArgs(lines=%s, higher=%s)" % ( + built, + self.higher, + ) + + def transpose(self): + self._first_pointer_parent, self._second_pointer_parent = self._second_pointer_parent, self._first_pointer_parent + self._first_pointer_index, self._second_pointer_index = self._second_pointer_index, self._first_pointer_index + self._first_line_index, self._second_line_index = self._second_line_index, self._first_line_index + return self + + @staticmethod + def _build(expr): + if isinstance(expr, ExprBuilder): + return expr.build() + if isinstance(expr, list): + if len(expr) == 1: + return expr[0] + else: + return expr[0](*[_LeftRightArgs._build(i) for i in expr[1]]) + else: + return expr + + def build(self): + data = [self._build(i) for i in self._lines] + if self.higher != 1: + data += [self._build(self.higher)] + data = list(data) + return data + + def matrix_form(self): + if self.first != 1 and self.higher != 1: + raise ValueError("higher dimensional array cannot be represented") + + def _get_shape(elem): + if isinstance(elem, MatrixExpr): + return elem.shape + return (None, None) + + if _get_shape(self.first)[1] != _get_shape(self.second)[1]: + # Remove one-dimensional identity matrices: + # (this is needed by `a.diff(a)` where `a` is a vector) + if _get_shape(self.second) == (1, 1): + return self.first*self.second[0, 0] + if _get_shape(self.first) == (1, 1): + return self.first[1, 1]*self.second.T + raise ValueError("incompatible shapes") + if self.first != 1: + return self.first*self.second.T + else: + return self.higher + + def rank(self): + """ + Number of dimensions different from trivial (warning: not related to + matrix rank). + """ + rank = 0 + if self.first != 1: + rank += sum(i != 1 for i in self.first.shape) + if self.second != 1: + rank += sum(i != 1 for i in self.second.shape) + if self.higher != 1: + rank += 2 + return rank + + def _multiply_pointer(self, pointer, other): + from ...tensor.array.expressions.array_expressions import ArrayTensorProduct + from ...tensor.array.expressions.array_expressions import ArrayContraction + + subexpr = ExprBuilder( + ArrayContraction, + [ + ExprBuilder( + ArrayTensorProduct, + [ + pointer, + other + ] + ), + (1, 2) + ], + validator=ArrayContraction._validate + ) + + return subexpr + + def append_first(self, other): + self.first_pointer *= other + + def append_second(self, other): + self.second_pointer *= other + + +def _make_matrix(x): + from sympy.matrices.immutable import ImmutableDenseMatrix + if isinstance(x, MatrixExpr): + return x + return ImmutableDenseMatrix([[x]]) + + +from .matmul import MatMul +from .matadd import MatAdd +from .matpow import MatPow +from .transpose import Transpose +from .inverse import Inverse +from .special import ZeroMatrix, Identity +from .determinant import Determinant diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/matmul.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/matmul.py new file mode 100644 index 0000000000000000000000000000000000000000..1c46f7ff5251d89793423f92ea02d7243601de3f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/matmul.py @@ -0,0 +1,496 @@ +from sympy.assumptions.ask import ask, Q +from sympy.assumptions.refine import handlers_dict +from sympy.core import Basic, sympify, S +from sympy.core.mul import mul, Mul +from sympy.core.numbers import Number, Integer +from sympy.core.symbol import Dummy +from sympy.functions import adjoint +from sympy.strategies import (rm_id, unpack, typed, flatten, exhaust, + do_one, new) +from sympy.matrices.exceptions import NonInvertibleMatrixError +from sympy.matrices.matrixbase import MatrixBase +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.matrices.expressions._shape import validate_matmul_integer as validate + +from .inverse import Inverse +from .matexpr import MatrixExpr +from .matpow import MatPow +from .transpose import transpose +from .permutation import PermutationMatrix +from .special import ZeroMatrix, Identity, GenericIdentity, OneMatrix + + +# XXX: MatMul should perhaps not subclass directly from Mul +class MatMul(MatrixExpr, Mul): + """ + A product of matrix expressions + + Examples + ======== + + >>> from sympy import MatMul, MatrixSymbol + >>> A = MatrixSymbol('A', 5, 4) + >>> B = MatrixSymbol('B', 4, 3) + >>> C = MatrixSymbol('C', 3, 6) + >>> MatMul(A, B, C) + A*B*C + """ + is_MatMul = True + + identity = GenericIdentity() + + def __new__(cls, *args, evaluate=False, check=None, _sympify=True): + if not args: + return cls.identity + + # This must be removed aggressively in the constructor to avoid + # TypeErrors from GenericIdentity().shape + args = list(filter(lambda i: cls.identity != i, args)) + if _sympify: + args = list(map(sympify, args)) + obj = Basic.__new__(cls, *args) + factor, matrices = obj.as_coeff_matrices() + + if check is not None: + sympy_deprecation_warning( + "Passing check to MatMul is deprecated and the check argument will be removed in a future version.", + deprecated_since_version="1.11", + active_deprecations_target='remove-check-argument-from-matrix-operations') + + if check is not False: + validate(*matrices) + + if not matrices: + # Should it be + # + # return Basic.__neq__(cls, factor, GenericIdentity()) ? + return factor + + if evaluate: + return cls._evaluate(obj) + + return obj + + @classmethod + def _evaluate(cls, expr): + return canonicalize(expr) + + @property + def shape(self): + matrices = [arg for arg in self.args if arg.is_Matrix] + return (matrices[0].rows, matrices[-1].cols) + + def _entry(self, i, j, expand=True, **kwargs): + # Avoid cyclic imports + from sympy.concrete.summations import Sum + from sympy.matrices.immutable import ImmutableMatrix + + coeff, matrices = self.as_coeff_matrices() + + if len(matrices) == 1: # situation like 2*X, matmul is just X + return coeff * matrices[0][i, j] + + indices = [None]*(len(matrices) + 1) + ind_ranges = [None]*(len(matrices) - 1) + indices[0] = i + indices[-1] = j + + def f(): + counter = 1 + while True: + yield Dummy("i_%i" % counter) + counter += 1 + + dummy_generator = kwargs.get("dummy_generator", f()) + + for i in range(1, len(matrices)): + indices[i] = next(dummy_generator) + + for i, arg in enumerate(matrices[:-1]): + ind_ranges[i] = arg.shape[1] - 1 + matrices = [arg._entry(indices[i], indices[i+1], dummy_generator=dummy_generator) for i, arg in enumerate(matrices)] + expr_in_sum = Mul.fromiter(matrices) + if any(v.has(ImmutableMatrix) for v in matrices): + expand = True + result = coeff*Sum( + expr_in_sum, + *zip(indices[1:-1], [0]*len(ind_ranges), ind_ranges) + ) + + # Don't waste time in result.doit() if the sum bounds are symbolic + if not any(isinstance(v, (Integer, int)) for v in ind_ranges): + expand = False + return result.doit() if expand else result + + def as_coeff_matrices(self): + scalars = [x for x in self.args if not x.is_Matrix] + matrices = [x for x in self.args if x.is_Matrix] + coeff = Mul(*scalars) + if coeff.is_commutative is False: + raise NotImplementedError("noncommutative scalars in MatMul are not supported.") + + return coeff, matrices + + def as_coeff_mmul(self): + coeff, matrices = self.as_coeff_matrices() + return coeff, MatMul(*matrices) + + def expand(self, **kwargs): + expanded = super(MatMul, self).expand(**kwargs) + return self._evaluate(expanded) + + def _eval_transpose(self): + """Transposition of matrix multiplication. + + Notes + ===== + + The following rules are applied. + + Transposition for matrix multiplied with another matrix: + `\\left(A B\\right)^{T} = B^{T} A^{T}` + + Transposition for matrix multiplied with scalar: + `\\left(c A\\right)^{T} = c A^{T}` + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Transpose + """ + coeff, matrices = self.as_coeff_matrices() + return MatMul( + coeff, *[transpose(arg) for arg in matrices[::-1]]).doit() + + def _eval_adjoint(self): + return MatMul(*[adjoint(arg) for arg in self.args[::-1]]).doit() + + def _eval_trace(self): + factor, mmul = self.as_coeff_mmul() + if factor != 1: + from .trace import trace + return factor * trace(mmul.doit()) + + def _eval_determinant(self): + from sympy.matrices.expressions.determinant import Determinant + factor, matrices = self.as_coeff_matrices() + square_matrices = only_squares(*matrices) + return factor**self.rows * Mul(*list(map(Determinant, square_matrices))) + + def _eval_inverse(self): + if all(arg.is_square for arg in self.args if isinstance(arg, MatrixExpr)): + return MatMul(*( + arg.inverse() if isinstance(arg, MatrixExpr) else arg**-1 + for arg in self.args[::-1] + ) + ).doit() + return Inverse(self) + + def doit(self, **hints): + deep = hints.get('deep', True) + if deep: + args = tuple(arg.doit(**hints) for arg in self.args) + else: + args = self.args + + # treat scalar*MatrixSymbol or scalar*MatPow separately + expr = canonicalize(MatMul(*args)) + return expr + + # Needed for partial compatibility with Mul + def args_cnc(self, cset=False, warn=True, **kwargs): + coeff_c = [x for x in self.args if x.is_commutative] + coeff_nc = [x for x in self.args if not x.is_commutative] + if cset: + clen = len(coeff_c) + coeff_c = set(coeff_c) + if clen and warn and len(coeff_c) != clen: + raise ValueError('repeated commutative arguments: %s' % + [ci for ci in coeff_c if list(self.args).count(ci) > 1]) + return [coeff_c, coeff_nc] + + def _eval_derivative_matrix_lines(self, x): + from .transpose import Transpose + with_x_ind = [i for i, arg in enumerate(self.args) if arg.has(x)] + lines = [] + for ind in with_x_ind: + left_args = self.args[:ind] + right_args = self.args[ind+1:] + + if right_args: + right_mat = MatMul.fromiter(right_args) + else: + right_mat = Identity(self.shape[1]) + if left_args: + left_rev = MatMul.fromiter([Transpose(i).doit() if i.is_Matrix else i for i in reversed(left_args)]) + else: + left_rev = Identity(self.shape[0]) + + d = self.args[ind]._eval_derivative_matrix_lines(x) + for i in d: + i.append_first(left_rev) + i.append_second(right_mat) + lines.append(i) + + return lines + +mul.register_handlerclass((Mul, MatMul), MatMul) + + +# Rules +def newmul(*args): + if args[0] == 1: + args = args[1:] + return new(MatMul, *args) + +def any_zeros(mul): + if any(arg.is_zero or (arg.is_Matrix and arg.is_ZeroMatrix) + for arg in mul.args): + matrices = [arg for arg in mul.args if arg.is_Matrix] + return ZeroMatrix(matrices[0].rows, matrices[-1].cols) + return mul + +def merge_explicit(matmul): + """ Merge explicit MatrixBase arguments + + >>> from sympy import MatrixSymbol, Matrix, MatMul, pprint + >>> from sympy.matrices.expressions.matmul import merge_explicit + >>> A = MatrixSymbol('A', 2, 2) + >>> B = Matrix([[1, 1], [1, 1]]) + >>> C = Matrix([[1, 2], [3, 4]]) + >>> X = MatMul(A, B, C) + >>> pprint(X) + [1 1] [1 2] + A*[ ]*[ ] + [1 1] [3 4] + >>> pprint(merge_explicit(X)) + [4 6] + A*[ ] + [4 6] + + >>> X = MatMul(B, A, C) + >>> pprint(X) + [1 1] [1 2] + [ ]*A*[ ] + [1 1] [3 4] + >>> pprint(merge_explicit(X)) + [1 1] [1 2] + [ ]*A*[ ] + [1 1] [3 4] + """ + if not any(isinstance(arg, MatrixBase) for arg in matmul.args): + return matmul + newargs = [] + last = matmul.args[0] + for arg in matmul.args[1:]: + if isinstance(arg, (MatrixBase, Number)) and isinstance(last, (MatrixBase, Number)): + last = last * arg + else: + newargs.append(last) + last = arg + newargs.append(last) + + return MatMul(*newargs) + +def remove_ids(mul): + """ Remove Identities from a MatMul + + This is a modified version of sympy.strategies.rm_id. + This is necessary because MatMul may contain both MatrixExprs and Exprs + as args. + + See Also + ======== + + sympy.strategies.rm_id + """ + # Separate Exprs from MatrixExprs in args + factor, mmul = mul.as_coeff_mmul() + # Apply standard rm_id for MatMuls + result = rm_id(lambda x: x.is_Identity is True)(mmul) + if result != mmul: + return newmul(factor, *result.args) # Recombine and return + else: + return mul + +def factor_in_front(mul): + factor, matrices = mul.as_coeff_matrices() + if factor != 1: + return newmul(factor, *matrices) + return mul + +def combine_powers(mul): + r"""Combine consecutive powers with the same base into one, e.g. + $$A \times A^2 \Rightarrow A^3$$ + + This also cancels out the possible matrix inverses using the + knowledgebase of :class:`~.Inverse`, e.g., + $$ Y \times X \times X^{-1} \Rightarrow Y $$ + """ + factor, args = mul.as_coeff_matrices() + new_args = [args[0]] + + for i in range(1, len(args)): + A = new_args[-1] + B = args[i] + + if isinstance(B, Inverse) and isinstance(B.arg, MatMul): + Bargs = B.arg.args + l = len(Bargs) + if list(Bargs) == new_args[-l:]: + new_args = new_args[:-l] + [Identity(B.shape[0])] + continue + + if isinstance(A, Inverse) and isinstance(A.arg, MatMul): + Aargs = A.arg.args + l = len(Aargs) + if list(Aargs) == args[i:i+l]: + identity = Identity(A.shape[0]) + new_args[-1] = identity + for j in range(i, i+l): + args[j] = identity + continue + + if A.is_square == False or B.is_square == False: + new_args.append(B) + continue + + if isinstance(A, MatPow): + A_base, A_exp = A.args + else: + A_base, A_exp = A, S.One + + if isinstance(B, MatPow): + B_base, B_exp = B.args + else: + B_base, B_exp = B, S.One + + if A_base == B_base: + new_exp = A_exp + B_exp + new_args[-1] = MatPow(A_base, new_exp).doit(deep=False) + continue + elif not isinstance(B_base, MatrixBase): + try: + B_base_inv = B_base.inverse() + except NonInvertibleMatrixError: + B_base_inv = None + if B_base_inv is not None and A_base == B_base_inv: + new_exp = A_exp - B_exp + new_args[-1] = MatPow(A_base, new_exp).doit(deep=False) + continue + new_args.append(B) + + return newmul(factor, *new_args) + +def combine_permutations(mul): + """Refine products of permutation matrices as the products of cycles. + """ + args = mul.args + l = len(args) + if l < 2: + return mul + + result = [args[0]] + for i in range(1, l): + A = result[-1] + B = args[i] + if isinstance(A, PermutationMatrix) and \ + isinstance(B, PermutationMatrix): + cycle_1 = A.args[0] + cycle_2 = B.args[0] + result[-1] = PermutationMatrix(cycle_1 * cycle_2) + else: + result.append(B) + + return MatMul(*result) + +def combine_one_matrices(mul): + """ + Combine products of OneMatrix + + e.g. OneMatrix(2, 3) * OneMatrix(3, 4) -> 3 * OneMatrix(2, 4) + """ + factor, args = mul.as_coeff_matrices() + new_args = [args[0]] + + for B in args[1:]: + A = new_args[-1] + if not isinstance(A, OneMatrix) or not isinstance(B, OneMatrix): + new_args.append(B) + continue + new_args.pop() + new_args.append(OneMatrix(A.shape[0], B.shape[1])) + factor *= A.shape[1] + + return newmul(factor, *new_args) + +def distribute_monom(mul): + """ + Simplify MatMul expressions but distributing + rational term to MatMul. + + e.g. 2*(A+B) -> 2*A + 2*B + """ + args = mul.args + if len(args) == 2: + from .matadd import MatAdd + if args[0].is_MatAdd and args[1].is_Rational: + return MatAdd(*[MatMul(mat, args[1]).doit() for mat in args[0].args]) + if args[1].is_MatAdd and args[0].is_Rational: + return MatAdd(*[MatMul(args[0], mat).doit() for mat in args[1].args]) + return mul + +rules = ( + distribute_monom, any_zeros, remove_ids, combine_one_matrices, combine_powers, unpack, rm_id(lambda x: x == 1), + merge_explicit, factor_in_front, flatten, combine_permutations) + +canonicalize = exhaust(typed({MatMul: do_one(*rules)})) + +def only_squares(*matrices): + """factor matrices only if they are square""" + if matrices[0].rows != matrices[-1].cols: + raise RuntimeError("Invalid matrices being multiplied") + out = [] + start = 0 + for i, M in enumerate(matrices): + if M.cols == matrices[start].rows: + out.append(MatMul(*matrices[start:i+1]).doit()) + start = i+1 + return out + + +def refine_MatMul(expr, assumptions): + """ + >>> from sympy import MatrixSymbol, Q, assuming, refine + >>> X = MatrixSymbol('X', 2, 2) + >>> expr = X * X.T + >>> print(expr) + X*X.T + >>> with assuming(Q.orthogonal(X)): + ... print(refine(expr)) + I + """ + newargs = [] + exprargs = [] + + for args in expr.args: + if args.is_Matrix: + exprargs.append(args) + else: + newargs.append(args) + + last = exprargs[0] + for arg in exprargs[1:]: + if arg == last.T and ask(Q.orthogonal(arg), assumptions): + last = Identity(arg.shape[0]) + elif arg == last.conjugate() and ask(Q.unitary(arg), assumptions): + last = Identity(arg.shape[0]) + else: + newargs.append(last) + last = arg + newargs.append(last) + + return MatMul(*newargs) + + +handlers_dict['MatMul'] = refine_MatMul diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/matpow.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/matpow.py new file mode 100644 index 0000000000000000000000000000000000000000..b6472995e134e9e5ebfd28a901480665d1531275 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/matpow.py @@ -0,0 +1,150 @@ +from .matexpr import MatrixExpr +from .special import Identity +from sympy.core import S +from sympy.core.expr import ExprBuilder +from sympy.core.cache import cacheit +from sympy.core.power import Pow +from sympy.core.sympify import _sympify +from sympy.matrices import MatrixBase +from sympy.matrices.exceptions import NonSquareMatrixError + + +class MatPow(MatrixExpr): + def __new__(cls, base, exp, evaluate=False, **options): + base = _sympify(base) + if not base.is_Matrix: + raise TypeError("MatPow base should be a matrix") + + if base.is_square is False: + raise NonSquareMatrixError("Power of non-square matrix %s" % base) + + exp = _sympify(exp) + obj = super().__new__(cls, base, exp) + + if evaluate: + obj = obj.doit(deep=False) + + return obj + + @property + def base(self): + return self.args[0] + + @property + def exp(self): + return self.args[1] + + @property + def shape(self): + return self.base.shape + + @cacheit + def _get_explicit_matrix(self): + return self.base.as_explicit()**self.exp + + def _entry(self, i, j, **kwargs): + from sympy.matrices.expressions import MatMul + A = self.doit() + if isinstance(A, MatPow): + # We still have a MatPow, make an explicit MatMul out of it. + if A.exp.is_Integer and A.exp.is_positive: + A = MatMul(*[A.base for k in range(A.exp)]) + elif not self._is_shape_symbolic(): + return A._get_explicit_matrix()[i, j] + else: + # Leave the expression unevaluated: + from sympy.matrices.expressions.matexpr import MatrixElement + return MatrixElement(self, i, j) + return A[i, j] + + def doit(self, **hints): + if hints.get('deep', True): + base, exp = (arg.doit(**hints) for arg in self.args) + else: + base, exp = self.args + + # combine all powers, e.g. (A ** 2) ** 3 -> A ** 6 + while isinstance(base, MatPow): + exp *= base.args[1] + base = base.args[0] + + if isinstance(base, MatrixBase): + # Delegate + return base ** exp + + # Handle simple cases so that _eval_power() in MatrixExpr sub-classes can ignore them + if exp == S.One: + return base + if exp == S.Zero: + return Identity(base.rows) + if exp == S.NegativeOne: + from sympy.matrices.expressions import Inverse + return Inverse(base).doit(**hints) + + eval_power = getattr(base, '_eval_power', None) + if eval_power is not None: + return eval_power(exp) + + return MatPow(base, exp) + + def _eval_transpose(self): + base, exp = self.args + return MatPow(base.transpose(), exp) + + def _eval_adjoint(self): + base, exp = self.args + return MatPow(base.adjoint(), exp) + + def _eval_conjugate(self): + base, exp = self.args + return MatPow(base.conjugate(), exp) + + def _eval_derivative(self, x): + return Pow._eval_derivative(self, x) + + def _eval_derivative_matrix_lines(self, x): + from sympy.tensor.array.expressions.array_expressions import ArrayContraction + from ...tensor.array.expressions.array_expressions import ArrayTensorProduct + from .matmul import MatMul + from .inverse import Inverse + exp = self.exp + if self.base.shape == (1, 1) and not exp.has(x): + lr = self.base._eval_derivative_matrix_lines(x) + for i in lr: + subexpr = ExprBuilder( + ArrayContraction, + [ + ExprBuilder( + ArrayTensorProduct, + [ + Identity(1), + i._lines[0], + exp*self.base**(exp-1), + i._lines[1], + Identity(1), + ] + ), + (0, 3, 4), (5, 7, 8) + ], + validator=ArrayContraction._validate + ) + i._first_pointer_parent = subexpr.args[0].args + i._first_pointer_index = 0 + i._second_pointer_parent = subexpr.args[0].args + i._second_pointer_index = 4 + i._lines = [subexpr] + return lr + if (exp > 0) == True: + newexpr = MatMul.fromiter([self.base for i in range(exp)]) + elif (exp == -1) == True: + return Inverse(self.base)._eval_derivative_matrix_lines(x) + elif (exp < 0) == True: + newexpr = MatMul.fromiter([Inverse(self.base) for i in range(-exp)]) + elif (exp == 0) == True: + return self.doit()._eval_derivative_matrix_lines(x) + else: + raise NotImplementedError("cannot evaluate %s derived by %s" % (self, x)) + return newexpr._eval_derivative_matrix_lines(x) + + def _eval_inverse(self): + return MatPow(self.base, -self.exp) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/permutation.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/permutation.py new file mode 100644 index 0000000000000000000000000000000000000000..5634fa941a53d8890583fe61bb29bc34f4e6000d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/permutation.py @@ -0,0 +1,303 @@ +from sympy.core import S +from sympy.core.sympify import _sympify +from sympy.functions import KroneckerDelta + +from .matexpr import MatrixExpr +from .special import ZeroMatrix, Identity, OneMatrix + + +class PermutationMatrix(MatrixExpr): + """A Permutation Matrix + + Parameters + ========== + + perm : Permutation + The permutation the matrix uses. + + The size of the permutation determines the matrix size. + + See the documentation of + :class:`sympy.combinatorics.permutations.Permutation` for + the further information of how to create a permutation object. + + Examples + ======== + + >>> from sympy import Matrix, PermutationMatrix + >>> from sympy.combinatorics import Permutation + + Creating a permutation matrix: + + >>> p = Permutation(1, 2, 0) + >>> P = PermutationMatrix(p) + >>> P = P.as_explicit() + >>> P + Matrix([ + [0, 1, 0], + [0, 0, 1], + [1, 0, 0]]) + + Permuting a matrix row and column: + + >>> M = Matrix([0, 1, 2]) + >>> Matrix(P*M) + Matrix([ + [1], + [2], + [0]]) + + >>> Matrix(M.T*P) + Matrix([[2, 0, 1]]) + + See Also + ======== + + sympy.combinatorics.permutations.Permutation + """ + + def __new__(cls, perm): + from sympy.combinatorics.permutations import Permutation + + perm = _sympify(perm) + if not isinstance(perm, Permutation): + raise ValueError( + "{} must be a SymPy Permutation instance.".format(perm)) + + return super().__new__(cls, perm) + + @property + def shape(self): + size = self.args[0].size + return (size, size) + + @property + def is_Identity(self): + return self.args[0].is_Identity + + def doit(self, **hints): + if self.is_Identity: + return Identity(self.rows) + return self + + def _entry(self, i, j, **kwargs): + perm = self.args[0] + return KroneckerDelta(perm.apply(i), j) + + def _eval_power(self, exp): + return PermutationMatrix(self.args[0] ** exp).doit() + + def _eval_inverse(self): + return PermutationMatrix(self.args[0] ** -1) + + _eval_transpose = _eval_adjoint = _eval_inverse + + def _eval_determinant(self): + sign = self.args[0].signature() + if sign == 1: + return S.One + elif sign == -1: + return S.NegativeOne + raise NotImplementedError + + def _eval_rewrite_as_BlockDiagMatrix(self, *args, **kwargs): + from sympy.combinatorics.permutations import Permutation + from .blockmatrix import BlockDiagMatrix + + perm = self.args[0] + full_cyclic_form = perm.full_cyclic_form + + cycles_picks = [] + + # Stage 1. Decompose the cycles into the blockable form. + a, b, c = 0, 0, 0 + flag = False + for cycle in full_cyclic_form: + l = len(cycle) + m = max(cycle) + + if not flag: + if m + 1 > a + l: + flag = True + temp = [cycle] + b = m + c = l + else: + cycles_picks.append([cycle]) + a += l + + else: + if m > b: + if m + 1 == a + c + l: + temp.append(cycle) + cycles_picks.append(temp) + flag = False + a = m+1 + else: + b = m + temp.append(cycle) + c += l + else: + if b + 1 == a + c + l: + temp.append(cycle) + cycles_picks.append(temp) + flag = False + a = b+1 + else: + temp.append(cycle) + c += l + + # Stage 2. Normalize each decomposed cycles and build matrix. + p = 0 + args = [] + for pick in cycles_picks: + new_cycles = [] + l = 0 + for cycle in pick: + new_cycle = [i - p for i in cycle] + new_cycles.append(new_cycle) + l += len(cycle) + p += l + perm = Permutation(new_cycles) + mat = PermutationMatrix(perm) + args.append(mat) + + return BlockDiagMatrix(*args) + + +class MatrixPermute(MatrixExpr): + r"""Symbolic representation for permuting matrix rows or columns. + + Parameters + ========== + + perm : Permutation, PermutationMatrix + The permutation to use for permuting the matrix. + The permutation can be resized to the suitable one, + + axis : 0 or 1 + The axis to permute alongside. + If `0`, it will permute the matrix rows. + If `1`, it will permute the matrix columns. + + Notes + ===== + + This follows the same notation used in + :meth:`sympy.matrices.matrixbase.MatrixBase.permute`. + + Examples + ======== + + >>> from sympy import Matrix, MatrixPermute + >>> from sympy.combinatorics import Permutation + + Permuting the matrix rows: + + >>> p = Permutation(1, 2, 0) + >>> A = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> B = MatrixPermute(A, p, axis=0) + >>> B.as_explicit() + Matrix([ + [4, 5, 6], + [7, 8, 9], + [1, 2, 3]]) + + Permuting the matrix columns: + + >>> B = MatrixPermute(A, p, axis=1) + >>> B.as_explicit() + Matrix([ + [2, 3, 1], + [5, 6, 4], + [8, 9, 7]]) + + See Also + ======== + + sympy.matrices.matrixbase.MatrixBase.permute + """ + def __new__(cls, mat, perm, axis=S.Zero): + from sympy.combinatorics.permutations import Permutation + + mat = _sympify(mat) + if not mat.is_Matrix: + raise ValueError( + "{} must be a SymPy matrix instance.".format(perm)) + + perm = _sympify(perm) + if isinstance(perm, PermutationMatrix): + perm = perm.args[0] + + if not isinstance(perm, Permutation): + raise ValueError( + "{} must be a SymPy Permutation or a PermutationMatrix " \ + "instance".format(perm)) + + axis = _sympify(axis) + if axis not in (0, 1): + raise ValueError("The axis must be 0 or 1.") + + mat_size = mat.shape[axis] + if mat_size != perm.size: + try: + perm = perm.resize(mat_size) + except ValueError: + raise ValueError( + "Size does not match between the permutation {} " + "and the matrix {} threaded over the axis {} " + "and cannot be converted." + .format(perm, mat, axis)) + + return super().__new__(cls, mat, perm, axis) + + def doit(self, deep=True, **hints): + mat, perm, axis = self.args + + if deep: + mat = mat.doit(deep=deep, **hints) + perm = perm.doit(deep=deep, **hints) + + if perm.is_Identity: + return mat + + if mat.is_Identity: + if axis is S.Zero: + return PermutationMatrix(perm) + elif axis is S.One: + return PermutationMatrix(perm**-1) + + if isinstance(mat, (ZeroMatrix, OneMatrix)): + return mat + + if isinstance(mat, MatrixPermute) and mat.args[2] == axis: + return MatrixPermute(mat.args[0], perm * mat.args[1], axis) + + return self + + @property + def shape(self): + return self.args[0].shape + + def _entry(self, i, j, **kwargs): + mat, perm, axis = self.args + + if axis == 0: + return mat[perm.apply(i), j] + elif axis == 1: + return mat[i, perm.apply(j)] + + def _eval_rewrite_as_MatMul(self, *args, **kwargs): + from .matmul import MatMul + + mat, perm, axis = self.args + + deep = kwargs.get("deep", True) + + if deep: + mat = mat.rewrite(MatMul) + + if axis == 0: + return MatMul(PermutationMatrix(perm), mat) + elif axis == 1: + return MatMul(mat, PermutationMatrix(perm**-1)) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/sets.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/sets.py new file mode 100644 index 0000000000000000000000000000000000000000..ab4930ea8f1b058977a8dd1abdc62f1f5e2195c1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/sets.py @@ -0,0 +1,68 @@ +from sympy.core.assumptions import check_assumptions +from sympy.core.logic import fuzzy_and +from sympy.core.sympify import _sympify +from sympy.matrices.kind import MatrixKind +from sympy.sets.sets import Set, SetKind +from sympy.core.kind import NumberKind +from .matexpr import MatrixExpr + + +class MatrixSet(Set): + """ + MatrixSet represents the set of matrices with ``shape = (n, m)`` over the + given set. + + Examples + ======== + + >>> from sympy.matrices import MatrixSet + >>> from sympy import S, I, Matrix + >>> M = MatrixSet(2, 2, set=S.Reals) + >>> X = Matrix([[1, 2], [3, 4]]) + >>> X in M + True + >>> X = Matrix([[1, 2], [I, 4]]) + >>> X in M + False + + """ + is_empty = False + + def __new__(cls, n, m, set): + n, m, set = _sympify(n), _sympify(m), _sympify(set) + cls._check_dim(n) + cls._check_dim(m) + if not isinstance(set, Set): + raise TypeError("{} should be an instance of Set.".format(set)) + return Set.__new__(cls, n, m, set) + + @property + def shape(self): + return self.args[:2] + + @property + def set(self): + return self.args[2] + + def _contains(self, other): + if not isinstance(other, MatrixExpr): + raise TypeError("{} should be an instance of MatrixExpr.".format(other)) + if other.shape != self.shape: + are_symbolic = any(_sympify(x).is_Symbol for x in other.shape + self.shape) + if are_symbolic: + return None + return False + return fuzzy_and(self.set.contains(x) for x in other) + + @classmethod + def _check_dim(cls, dim): + """Helper function to check invalid matrix dimensions""" + ok = not dim.is_Float and check_assumptions( + dim, integer=True, nonnegative=True) + if ok is False: + raise ValueError( + "The dimension specification {} should be " + "a nonnegative integer.".format(dim)) + + def _kind(self): + return SetKind(MatrixKind(NumberKind)) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/slice.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/slice.py new file mode 100644 index 0000000000000000000000000000000000000000..1904b49f29c503fb4c0c909532f8342fb0f4b135 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/slice.py @@ -0,0 +1,114 @@ +from sympy.matrices.expressions.matexpr import MatrixExpr +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.functions.elementary.integers import floor + +def normalize(i, parentsize): + if isinstance(i, slice): + i = (i.start, i.stop, i.step) + if not isinstance(i, (tuple, list, Tuple)): + if (i < 0) == True: + i += parentsize + i = (i, i+1, 1) + i = list(i) + if len(i) == 2: + i.append(1) + start, stop, step = i + start = start or 0 + if stop is None: + stop = parentsize + if (start < 0) == True: + start += parentsize + if (stop < 0) == True: + stop += parentsize + step = step or 1 + + if ((stop - start) * step < 1) == True: + raise IndexError() + + return (start, stop, step) + +class MatrixSlice(MatrixExpr): + """ A MatrixSlice of a Matrix Expression + + Examples + ======== + + >>> from sympy import MatrixSlice, ImmutableMatrix + >>> M = ImmutableMatrix(4, 4, range(16)) + >>> M + Matrix([ + [ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11], + [12, 13, 14, 15]]) + + >>> B = MatrixSlice(M, (0, 2), (2, 4)) + >>> ImmutableMatrix(B) + Matrix([ + [2, 3], + [6, 7]]) + """ + parent = property(lambda self: self.args[0]) + rowslice = property(lambda self: self.args[1]) + colslice = property(lambda self: self.args[2]) + + def __new__(cls, parent, rowslice, colslice): + rowslice = normalize(rowslice, parent.shape[0]) + colslice = normalize(colslice, parent.shape[1]) + if not (len(rowslice) == len(colslice) == 3): + raise IndexError() + if ((0 > rowslice[0]) == True or + (parent.shape[0] < rowslice[1]) == True or + (0 > colslice[0]) == True or + (parent.shape[1] < colslice[1]) == True): + raise IndexError() + if isinstance(parent, MatrixSlice): + return mat_slice_of_slice(parent, rowslice, colslice) + return Basic.__new__(cls, parent, Tuple(*rowslice), Tuple(*colslice)) + + @property + def shape(self): + rows = self.rowslice[1] - self.rowslice[0] + rows = rows if self.rowslice[2] == 1 else floor(rows/self.rowslice[2]) + cols = self.colslice[1] - self.colslice[0] + cols = cols if self.colslice[2] == 1 else floor(cols/self.colslice[2]) + return rows, cols + + def _entry(self, i, j, **kwargs): + return self.parent._entry(i*self.rowslice[2] + self.rowslice[0], + j*self.colslice[2] + self.colslice[0], + **kwargs) + + @property + def on_diag(self): + return self.rowslice == self.colslice + + +def slice_of_slice(s, t): + start1, stop1, step1 = s + start2, stop2, step2 = t + + start = start1 + start2*step1 + step = step1 * step2 + stop = start1 + step1*stop2 + + if stop > stop1: + raise IndexError() + + return start, stop, step + + +def mat_slice_of_slice(parent, rowslice, colslice): + """ Collapse nested matrix slices + + >>> from sympy import MatrixSymbol + >>> X = MatrixSymbol('X', 10, 10) + >>> X[:, 1:5][5:8, :] + X[5:8, 1:5] + >>> X[1:9:2, 2:6][1:3, 2] + X[3:7:2, 4:5] + """ + row = slice_of_slice(parent.rowslice, rowslice) + col = slice_of_slice(parent.colslice, colslice) + return MatrixSlice(parent.parent, row, col) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/special.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/special.py new file mode 100644 index 0000000000000000000000000000000000000000..d1e426f16ada0e4245b644867974b41b6f86b5cc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/special.py @@ -0,0 +1,299 @@ +from sympy.assumptions.ask import ask, Q +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.sympify import _sympify +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.matrices.exceptions import NonInvertibleMatrixError +from .matexpr import MatrixExpr + + +class ZeroMatrix(MatrixExpr): + """The Matrix Zero 0 - additive identity + + Examples + ======== + + >>> from sympy import MatrixSymbol, ZeroMatrix + >>> A = MatrixSymbol('A', 3, 5) + >>> Z = ZeroMatrix(3, 5) + >>> A + Z + A + >>> Z*A.T + 0 + """ + is_ZeroMatrix = True + + def __new__(cls, m, n): + m, n = _sympify(m), _sympify(n) + cls._check_dim(m) + cls._check_dim(n) + + return super().__new__(cls, m, n) + + @property + def shape(self): + return (self.args[0], self.args[1]) + + def _eval_power(self, exp): + # exp = -1, 0, 1 are already handled at this stage + if (exp < 0) == True: + raise NonInvertibleMatrixError("Matrix det == 0; not invertible") + return self + + def _eval_transpose(self): + return ZeroMatrix(self.cols, self.rows) + + def _eval_adjoint(self): + return ZeroMatrix(self.cols, self.rows) + + def _eval_trace(self): + return S.Zero + + def _eval_determinant(self): + return S.Zero + + def _eval_inverse(self): + raise NonInvertibleMatrixError("Matrix det == 0; not invertible.") + + def _eval_as_real_imag(self): + return (self, self) + + def _eval_conjugate(self): + return self + + def _entry(self, i, j, **kwargs): + return S.Zero + + +class GenericZeroMatrix(ZeroMatrix): + """ + A zero matrix without a specified shape + + This exists primarily so MatAdd() with no arguments can return something + meaningful. + """ + def __new__(cls): + # super(ZeroMatrix, cls) instead of super(GenericZeroMatrix, cls) + # because ZeroMatrix.__new__ doesn't have the same signature + return super(ZeroMatrix, cls).__new__(cls) + + @property + def rows(self): + raise TypeError("GenericZeroMatrix does not have a specified shape") + + @property + def cols(self): + raise TypeError("GenericZeroMatrix does not have a specified shape") + + @property + def shape(self): + raise TypeError("GenericZeroMatrix does not have a specified shape") + + # Avoid Matrix.__eq__ which might call .shape + def __eq__(self, other): + return isinstance(other, GenericZeroMatrix) + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + return super().__hash__() + + + +class Identity(MatrixExpr): + """The Matrix Identity I - multiplicative identity + + Examples + ======== + + >>> from sympy import Identity, MatrixSymbol + >>> A = MatrixSymbol('A', 3, 5) + >>> I = Identity(3) + >>> I*A + A + """ + + is_Identity = True + + def __new__(cls, n): + n = _sympify(n) + cls._check_dim(n) + + return super().__new__(cls, n) + + @property + def rows(self): + return self.args[0] + + @property + def cols(self): + return self.args[0] + + @property + def shape(self): + return (self.args[0], self.args[0]) + + @property + def is_square(self): + return True + + def _eval_transpose(self): + return self + + def _eval_trace(self): + return self.rows + + def _eval_inverse(self): + return self + + def _eval_as_real_imag(self): + return (self, ZeroMatrix(*self.shape)) + + def _eval_conjugate(self): + return self + + def _eval_adjoint(self): + return self + + def _entry(self, i, j, **kwargs): + eq = Eq(i, j) + if eq is S.true: + return S.One + elif eq is S.false: + return S.Zero + return KroneckerDelta(i, j, (0, self.cols-1)) + + def _eval_determinant(self): + return S.One + + def _eval_power(self, exp): + return self + + +class GenericIdentity(Identity): + """ + An identity matrix without a specified shape + + This exists primarily so MatMul() with no arguments can return something + meaningful. + """ + def __new__(cls): + # super(Identity, cls) instead of super(GenericIdentity, cls) because + # Identity.__new__ doesn't have the same signature + return super(Identity, cls).__new__(cls) + + @property + def rows(self): + raise TypeError("GenericIdentity does not have a specified shape") + + @property + def cols(self): + raise TypeError("GenericIdentity does not have a specified shape") + + @property + def shape(self): + raise TypeError("GenericIdentity does not have a specified shape") + + @property + def is_square(self): + return True + + # Avoid Matrix.__eq__ which might call .shape + def __eq__(self, other): + return isinstance(other, GenericIdentity) + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + return super().__hash__() + + +class OneMatrix(MatrixExpr): + """ + Matrix whose all entries are ones. + """ + def __new__(cls, m, n, evaluate=False): + m, n = _sympify(m), _sympify(n) + cls._check_dim(m) + cls._check_dim(n) + + if evaluate: + condition = Eq(m, 1) & Eq(n, 1) + if condition == True: + return Identity(1) + + obj = super().__new__(cls, m, n) + return obj + + @property + def shape(self): + return self._args + + @property + def is_Identity(self): + return self._is_1x1() == True + + def as_explicit(self): + from sympy.matrices.immutable import ImmutableDenseMatrix + return ImmutableDenseMatrix.ones(*self.shape) + + def doit(self, **hints): + args = self.args + if hints.get('deep', True): + args = [a.doit(**hints) for a in args] + return self.func(*args, evaluate=True) + + def _eval_power(self, exp): + # exp = -1, 0, 1 are already handled at this stage + if self._is_1x1() == True: + return Identity(1) + if (exp < 0) == True: + raise NonInvertibleMatrixError("Matrix det == 0; not invertible") + if ask(Q.integer(exp)): + return self.shape[0] ** (exp - 1) * OneMatrix(*self.shape) + return super()._eval_power(exp) + + def _eval_transpose(self): + return OneMatrix(self.cols, self.rows) + + def _eval_adjoint(self): + return OneMatrix(self.cols, self.rows) + + def _eval_trace(self): + return S.One*self.rows + + def _is_1x1(self): + """Returns true if the matrix is known to be 1x1""" + shape = self.shape + return Eq(shape[0], 1) & Eq(shape[1], 1) + + def _eval_determinant(self): + condition = self._is_1x1() + if condition == True: + return S.One + elif condition == False: + return S.Zero + else: + from sympy.matrices.expressions.determinant import Determinant + return Determinant(self) + + def _eval_inverse(self): + condition = self._is_1x1() + if condition == True: + return Identity(1) + elif condition == False: + raise NonInvertibleMatrixError("Matrix det == 0; not invertible.") + else: + from .inverse import Inverse + return Inverse(self) + + def _eval_as_real_imag(self): + return (self, ZeroMatrix(*self.shape)) + + def _eval_conjugate(self): + return self + + def _entry(self, i, j, **kwargs): + return S.One diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_adjoint.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_adjoint.py new file mode 100644 index 0000000000000000000000000000000000000000..7106b5740b1dc7c32f2c6f5ecb9d286b5e1dd222 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_adjoint.py @@ -0,0 +1,34 @@ +from sympy.core import symbols, S +from sympy.functions import adjoint, conjugate, transpose +from sympy.matrices.expressions import MatrixSymbol, Adjoint, trace, Transpose +from sympy.matrices import eye, Matrix + +n, m, l, k, p = symbols('n m l k p', integer=True) +A = MatrixSymbol('A', n, m) +B = MatrixSymbol('B', m, l) +C = MatrixSymbol('C', n, n) + + +def test_adjoint(): + Sq = MatrixSymbol('Sq', n, n) + + assert Adjoint(A).shape == (m, n) + assert Adjoint(A*B).shape == (l, n) + assert adjoint(Adjoint(A)) == A + assert isinstance(Adjoint(Adjoint(A)), Adjoint) + + assert conjugate(Adjoint(A)) == Transpose(A) + assert transpose(Adjoint(A)) == Adjoint(Transpose(A)) + + assert Adjoint(eye(3)).doit() == eye(3) + + assert Adjoint(S(5)).doit() == S(5) + + assert Adjoint(Matrix([[1, 2], [3, 4]])).doit() == Matrix([[1, 3], [2, 4]]) + + assert adjoint(trace(Sq)) == conjugate(trace(Sq)) + assert trace(adjoint(Sq)) == conjugate(trace(Sq)) + + assert Adjoint(Sq)[0, 1] == conjugate(Sq[1, 0]) + + assert Adjoint(A*B).doit() == Adjoint(B) * Adjoint(A) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_applyfunc.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_applyfunc.py new file mode 100644 index 0000000000000000000000000000000000000000..d98732e2751e53938d96d7ea56c916e6fee4578e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_applyfunc.py @@ -0,0 +1,118 @@ +from sympy.core.symbol import symbols, Dummy +from sympy.matrices.expressions.applyfunc import ElementwiseApplyFunction +from sympy.core.function import Lambda +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.trigonometric import sin +from sympy.matrices.dense import Matrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.matmul import MatMul +from sympy.simplify.simplify import simplify + + +X = MatrixSymbol("X", 3, 3) +Y = MatrixSymbol("Y", 3, 3) + +k = symbols("k") +Xk = MatrixSymbol("X", k, k) + +Xd = X.as_explicit() + +x, y, z, t = symbols("x y z t") + + +def test_applyfunc_matrix(): + x = Dummy('x') + double = Lambda(x, x**2) + + expr = ElementwiseApplyFunction(double, Xd) + assert isinstance(expr, ElementwiseApplyFunction) + assert expr.doit() == Xd.applyfunc(lambda x: x**2) + assert expr.shape == (3, 3) + assert expr.func(*expr.args) == expr + assert simplify(expr) == expr + assert expr[0, 0] == double(Xd[0, 0]) + + expr = ElementwiseApplyFunction(double, X) + assert isinstance(expr, ElementwiseApplyFunction) + assert isinstance(expr.doit(), ElementwiseApplyFunction) + assert expr == X.applyfunc(double) + assert expr.func(*expr.args) == expr + + expr = ElementwiseApplyFunction(exp, X*Y) + assert expr.expr == X*Y + assert expr.function.dummy_eq(Lambda(x, exp(x))) + assert expr.dummy_eq((X*Y).applyfunc(exp)) + assert expr.func(*expr.args) == expr + + assert isinstance(X*expr, MatMul) + assert (X*expr).shape == (3, 3) + Z = MatrixSymbol("Z", 2, 3) + assert (Z*expr).shape == (2, 3) + + expr = ElementwiseApplyFunction(exp, Z.T)*ElementwiseApplyFunction(exp, Z) + assert expr.shape == (3, 3) + expr = ElementwiseApplyFunction(exp, Z)*ElementwiseApplyFunction(exp, Z.T) + assert expr.shape == (2, 2) + + M = Matrix([[x, y], [z, t]]) + expr = ElementwiseApplyFunction(sin, M) + assert isinstance(expr, ElementwiseApplyFunction) + assert expr.function.dummy_eq(Lambda(x, sin(x))) + assert expr.expr == M + assert expr.doit() == M.applyfunc(sin) + assert expr.doit() == Matrix([[sin(x), sin(y)], [sin(z), sin(t)]]) + assert expr.func(*expr.args) == expr + + expr = ElementwiseApplyFunction(double, Xk) + assert expr.doit() == expr + assert expr.subs(k, 2).shape == (2, 2) + assert (expr*expr).shape == (k, k) + M = MatrixSymbol("M", k, t) + expr2 = M.T*expr*M + assert isinstance(expr2, MatMul) + assert expr2.args[1] == expr + assert expr2.shape == (t, t) + expr3 = expr*M + assert expr3.shape == (k, t) + + expr1 = ElementwiseApplyFunction(lambda x: x+1, Xk) + expr2 = ElementwiseApplyFunction(lambda x: x, Xk) + assert expr1 != expr2 + + +def test_applyfunc_entry(): + + af = X.applyfunc(sin) + assert af[0, 0] == sin(X[0, 0]) + + af = Xd.applyfunc(sin) + assert af[0, 0] == sin(X[0, 0]) + + +def test_applyfunc_as_explicit(): + + af = X.applyfunc(sin) + assert af.as_explicit() == Matrix([ + [sin(X[0, 0]), sin(X[0, 1]), sin(X[0, 2])], + [sin(X[1, 0]), sin(X[1, 1]), sin(X[1, 2])], + [sin(X[2, 0]), sin(X[2, 1]), sin(X[2, 2])], + ]) + + +def test_applyfunc_transpose(): + + af = Xk.applyfunc(sin) + assert af.T.dummy_eq(Xk.T.applyfunc(sin)) + + +def test_applyfunc_shape_11_matrices(): + M = MatrixSymbol("M", 1, 1) + + double = Lambda(x, x*2) + + expr = M.applyfunc(sin) + assert isinstance(expr, ElementwiseApplyFunction) + + expr = M.applyfunc(double) + assert isinstance(expr, MatMul) + assert expr == 2*M diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_blockmatrix.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_blockmatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..1d4893cd9a4b3e47dd8e84db33031f7f6f3201fd --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_blockmatrix.py @@ -0,0 +1,469 @@ +from sympy.matrices.expressions.trace import Trace +from sympy.testing.pytest import raises, slow +from sympy.matrices.expressions.blockmatrix import ( + block_collapse, bc_matmul, bc_block_plus_ident, BlockDiagMatrix, + BlockMatrix, bc_dist, bc_matadd, bc_transpose, bc_inverse, + blockcut, reblock_2x2, deblock) +from sympy.matrices.expressions import ( + MatrixSymbol, Identity, trace, det, ZeroMatrix, OneMatrix) +from sympy.matrices.expressions.inverse import Inverse +from sympy.matrices.expressions.matpow import MatPow +from sympy.matrices.expressions.transpose import Transpose +from sympy.matrices.exceptions import NonInvertibleMatrixError +from sympy.matrices import ( + Matrix, ImmutableMatrix, ImmutableSparseMatrix, zeros) +from sympy.core import Tuple, Expr, S, Function +from sympy.core.symbol import Symbol, symbols +from sympy.functions import transpose, im, re + +i, j, k, l, m, n, p = symbols('i:n, p', integer=True) +A = MatrixSymbol('A', n, n) +B = MatrixSymbol('B', n, n) +C = MatrixSymbol('C', n, n) +D = MatrixSymbol('D', n, n) +G = MatrixSymbol('G', n, n) +H = MatrixSymbol('H', n, n) +b1 = BlockMatrix([[G, H]]) +b2 = BlockMatrix([[G], [H]]) + +def test_bc_matmul(): + assert bc_matmul(H*b1*b2*G) == BlockMatrix([[(H*G*G + H*H*H)*G]]) + +def test_bc_matadd(): + assert bc_matadd(BlockMatrix([[G, H]]) + BlockMatrix([[H, H]])) == \ + BlockMatrix([[G+H, H+H]]) + +def test_bc_transpose(): + assert bc_transpose(Transpose(BlockMatrix([[A, B], [C, D]]))) == \ + BlockMatrix([[A.T, C.T], [B.T, D.T]]) + +def test_bc_dist_diag(): + A = MatrixSymbol('A', n, n) + B = MatrixSymbol('B', m, m) + C = MatrixSymbol('C', l, l) + X = BlockDiagMatrix(A, B, C) + + assert bc_dist(X+X).equals(BlockDiagMatrix(2*A, 2*B, 2*C)) + +def test_block_plus_ident(): + A = MatrixSymbol('A', n, n) + B = MatrixSymbol('B', n, m) + C = MatrixSymbol('C', m, n) + D = MatrixSymbol('D', m, m) + X = BlockMatrix([[A, B], [C, D]]) + Z = MatrixSymbol('Z', n + m, n + m) + assert bc_block_plus_ident(X + Identity(m + n) + Z) == \ + BlockDiagMatrix(Identity(n), Identity(m)) + X + Z + +def test_BlockMatrix(): + A = MatrixSymbol('A', n, m) + B = MatrixSymbol('B', n, k) + C = MatrixSymbol('C', l, m) + D = MatrixSymbol('D', l, k) + M = MatrixSymbol('M', m + k, p) + N = MatrixSymbol('N', l + n, k + m) + X = BlockMatrix(Matrix([[A, B], [C, D]])) + + assert X.__class__(*X.args) == X + + # block_collapse does nothing on normal inputs + E = MatrixSymbol('E', n, m) + assert block_collapse(A + 2*E) == A + 2*E + F = MatrixSymbol('F', m, m) + assert block_collapse(E.T*A*F) == E.T*A*F + + assert X.shape == (l + n, k + m) + assert X.blockshape == (2, 2) + assert transpose(X) == BlockMatrix(Matrix([[A.T, C.T], [B.T, D.T]])) + assert transpose(X).shape == X.shape[::-1] + + # Test that BlockMatrices and MatrixSymbols can still mix + assert (X*M).is_MatMul + assert X._blockmul(M).is_MatMul + assert (X*M).shape == (n + l, p) + assert (X + N).is_MatAdd + assert X._blockadd(N).is_MatAdd + assert (X + N).shape == X.shape + + E = MatrixSymbol('E', m, 1) + F = MatrixSymbol('F', k, 1) + + Y = BlockMatrix(Matrix([[E], [F]])) + + assert (X*Y).shape == (l + n, 1) + assert block_collapse(X*Y).blocks[0, 0] == A*E + B*F + assert block_collapse(X*Y).blocks[1, 0] == C*E + D*F + + # block_collapse passes down into container objects, transposes, and inverse + assert block_collapse(transpose(X*Y)) == transpose(block_collapse(X*Y)) + assert block_collapse(Tuple(X*Y, 2*X)) == ( + block_collapse(X*Y), block_collapse(2*X)) + + # Make sure that MatrixSymbols will enter 1x1 BlockMatrix if it simplifies + Ab = BlockMatrix([[A]]) + Z = MatrixSymbol('Z', *A.shape) + assert block_collapse(Ab + Z) == A + Z + +def test_block_collapse_explicit_matrices(): + A = Matrix([[1, 2], [3, 4]]) + assert block_collapse(BlockMatrix([[A]])) == A + + A = ImmutableSparseMatrix([[1, 2], [3, 4]]) + assert block_collapse(BlockMatrix([[A]])) == A + +def test_issue_17624(): + a = MatrixSymbol("a", 2, 2) + z = ZeroMatrix(2, 2) + b = BlockMatrix([[a, z], [z, z]]) + assert block_collapse(b * b) == BlockMatrix([[a**2, z], [z, z]]) + assert block_collapse(b * b * b) == BlockMatrix([[a**3, z], [z, z]]) + +def test_issue_18618(): + A = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + assert A == Matrix(BlockDiagMatrix(A)) + +def test_BlockMatrix_trace(): + A, B, C, D = [MatrixSymbol(s, 3, 3) for s in 'ABCD'] + X = BlockMatrix([[A, B], [C, D]]) + assert trace(X) == trace(A) + trace(D) + assert trace(BlockMatrix([ZeroMatrix(n, n)])) == 0 + +def test_BlockMatrix_Determinant(): + A, B, C, D = [MatrixSymbol(s, 3, 3) for s in 'ABCD'] + X = BlockMatrix([[A, B], [C, D]]) + from sympy.assumptions.ask import Q + from sympy.assumptions.assume import assuming + with assuming(Q.invertible(A)): + assert det(X) == det(A) * det(X.schur('A')) + + assert isinstance(det(X), Expr) + assert det(BlockMatrix([A])) == det(A) + assert det(BlockMatrix([ZeroMatrix(n, n)])) == 0 + +def test_squareBlockMatrix(): + A = MatrixSymbol('A', n, n) + B = MatrixSymbol('B', n, m) + C = MatrixSymbol('C', m, n) + D = MatrixSymbol('D', m, m) + X = BlockMatrix([[A, B], [C, D]]) + Y = BlockMatrix([[A]]) + + assert X.is_square + + Q = X + Identity(m + n) + assert (block_collapse(Q) == + BlockMatrix([[A + Identity(n), B], [C, D + Identity(m)]])) + + assert (X + MatrixSymbol('Q', n + m, n + m)).is_MatAdd + assert (X * MatrixSymbol('Q', n + m, n + m)).is_MatMul + + assert block_collapse(Y.I) == A.I + + assert isinstance(X.inverse(), Inverse) + + assert not X.is_Identity + + Z = BlockMatrix([[Identity(n), B], [C, D]]) + assert not Z.is_Identity + + +def test_BlockMatrix_2x2_inverse_symbolic(): + A = MatrixSymbol('A', n, m) + B = MatrixSymbol('B', n, k - m) + C = MatrixSymbol('C', k - n, m) + D = MatrixSymbol('D', k - n, k - m) + X = BlockMatrix([[A, B], [C, D]]) + assert X.is_square and X.shape == (k, k) + assert isinstance(block_collapse(X.I), Inverse) # Can't invert when none of the blocks is square + + # test code path where only A is invertible + A = MatrixSymbol('A', n, n) + B = MatrixSymbol('B', n, m) + C = MatrixSymbol('C', m, n) + D = ZeroMatrix(m, m) + X = BlockMatrix([[A, B], [C, D]]) + assert block_collapse(X.inverse()) == BlockMatrix([ + [A.I + A.I * B * X.schur('A').I * C * A.I, -A.I * B * X.schur('A').I], + [-X.schur('A').I * C * A.I, X.schur('A').I], + ]) + + # test code path where only B is invertible + A = MatrixSymbol('A', n, m) + B = MatrixSymbol('B', n, n) + C = ZeroMatrix(m, m) + D = MatrixSymbol('D', m, n) + X = BlockMatrix([[A, B], [C, D]]) + assert block_collapse(X.inverse()) == BlockMatrix([ + [-X.schur('B').I * D * B.I, X.schur('B').I], + [B.I + B.I * A * X.schur('B').I * D * B.I, -B.I * A * X.schur('B').I], + ]) + + # test code path where only C is invertible + A = MatrixSymbol('A', n, m) + B = ZeroMatrix(n, n) + C = MatrixSymbol('C', m, m) + D = MatrixSymbol('D', m, n) + X = BlockMatrix([[A, B], [C, D]]) + assert block_collapse(X.inverse()) == BlockMatrix([ + [-C.I * D * X.schur('C').I, C.I + C.I * D * X.schur('C').I * A * C.I], + [X.schur('C').I, -X.schur('C').I * A * C.I], + ]) + + # test code path where only D is invertible + A = ZeroMatrix(n, n) + B = MatrixSymbol('B', n, m) + C = MatrixSymbol('C', m, n) + D = MatrixSymbol('D', m, m) + X = BlockMatrix([[A, B], [C, D]]) + assert block_collapse(X.inverse()) == BlockMatrix([ + [X.schur('D').I, -X.schur('D').I * B * D.I], + [-D.I * C * X.schur('D').I, D.I + D.I * C * X.schur('D').I * B * D.I], + ]) + + +def test_BlockMatrix_2x2_inverse_numeric(): + """Test 2x2 block matrix inversion numerically for all 4 formulas""" + M = Matrix([[1, 2], [3, 4]]) + # rank deficient matrices that have full rank when two of them combined + D1 = Matrix([[1, 2], [2, 4]]) + D2 = Matrix([[1, 3], [3, 9]]) + D3 = Matrix([[1, 4], [4, 16]]) + assert D1.rank() == D2.rank() == D3.rank() == 1 + assert (D1 + D2).rank() == (D2 + D3).rank() == (D3 + D1).rank() == 2 + + # Only A is invertible + K = BlockMatrix([[M, D1], [D2, D3]]) + assert block_collapse(K.inv()).as_explicit() == K.as_explicit().inv() + # Only B is invertible + K = BlockMatrix([[D1, M], [D2, D3]]) + assert block_collapse(K.inv()).as_explicit() == K.as_explicit().inv() + # Only C is invertible + K = BlockMatrix([[D1, D2], [M, D3]]) + assert block_collapse(K.inv()).as_explicit() == K.as_explicit().inv() + # Only D is invertible + K = BlockMatrix([[D1, D2], [D3, M]]) + assert block_collapse(K.inv()).as_explicit() == K.as_explicit().inv() + + +@slow +def test_BlockMatrix_3x3_symbolic(): + # Only test one of these, instead of all permutations, because it's slow + rowblocksizes = (n, m, k) + colblocksizes = (m, k, n) + K = BlockMatrix([ + [MatrixSymbol('M%s%s' % (rows, cols), rows, cols) for cols in colblocksizes] + for rows in rowblocksizes + ]) + collapse = block_collapse(K.I) + assert isinstance(collapse, BlockMatrix) + + +def test_BlockDiagMatrix(): + A = MatrixSymbol('A', n, n) + B = MatrixSymbol('B', m, m) + C = MatrixSymbol('C', l, l) + M = MatrixSymbol('M', n + m + l, n + m + l) + + X = BlockDiagMatrix(A, B, C) + Y = BlockDiagMatrix(A, 2*B, 3*C) + + assert X.blocks[1, 1] == B + assert X.shape == (n + m + l, n + m + l) + assert all(X.blocks[i, j].is_ZeroMatrix if i != j else X.blocks[i, j] in [A, B, C] + for i in range(3) for j in range(3)) + assert X.__class__(*X.args) == X + assert X.get_diag_blocks() == (A, B, C) + + assert isinstance(block_collapse(X.I * X), Identity) + + assert bc_matmul(X*X) == BlockDiagMatrix(A*A, B*B, C*C) + assert block_collapse(X*X) == BlockDiagMatrix(A*A, B*B, C*C) + #XXX: should be == ?? + assert block_collapse(X + X).equals(BlockDiagMatrix(2*A, 2*B, 2*C)) + assert block_collapse(X*Y) == BlockDiagMatrix(A*A, 2*B*B, 3*C*C) + assert block_collapse(X + Y) == BlockDiagMatrix(2*A, 3*B, 4*C) + + # Ensure that BlockDiagMatrices can still interact with normal MatrixExprs + assert (X*(2*M)).is_MatMul + assert (X + (2*M)).is_MatAdd + + assert (X._blockmul(M)).is_MatMul + assert (X._blockadd(M)).is_MatAdd + +def test_BlockDiagMatrix_nonsquare(): + A = MatrixSymbol('A', n, m) + B = MatrixSymbol('B', k, l) + X = BlockDiagMatrix(A, B) + assert X.shape == (n + k, m + l) + assert X.shape == (n + k, m + l) + assert X.rowblocksizes == [n, k] + assert X.colblocksizes == [m, l] + C = MatrixSymbol('C', n, m) + D = MatrixSymbol('D', k, l) + Y = BlockDiagMatrix(C, D) + assert block_collapse(X + Y) == BlockDiagMatrix(A + C, B + D) + assert block_collapse(X * Y.T) == BlockDiagMatrix(A * C.T, B * D.T) + raises(NonInvertibleMatrixError, lambda: BlockDiagMatrix(A, C.T).inverse()) + +def test_BlockDiagMatrix_determinant(): + A = MatrixSymbol('A', n, n) + B = MatrixSymbol('B', m, m) + assert det(BlockDiagMatrix()) == 1 + assert det(BlockDiagMatrix(A)) == det(A) + assert det(BlockDiagMatrix(A, B)) == det(A) * det(B) + + # non-square blocks + C = MatrixSymbol('C', m, n) + D = MatrixSymbol('D', n, m) + assert det(BlockDiagMatrix(C, D)) == 0 + +def test_BlockDiagMatrix_trace(): + assert trace(BlockDiagMatrix()) == 0 + assert trace(BlockDiagMatrix(ZeroMatrix(n, n))) == 0 + A = MatrixSymbol('A', n, n) + assert trace(BlockDiagMatrix(A)) == trace(A) + B = MatrixSymbol('B', m, m) + assert trace(BlockDiagMatrix(A, B)) == trace(A) + trace(B) + + # non-square blocks + C = MatrixSymbol('C', m, n) + D = MatrixSymbol('D', n, m) + assert isinstance(trace(BlockDiagMatrix(C, D)), Trace) + +def test_BlockDiagMatrix_transpose(): + A = MatrixSymbol('A', n, m) + B = MatrixSymbol('B', k, l) + assert transpose(BlockDiagMatrix()) == BlockDiagMatrix() + assert transpose(BlockDiagMatrix(A)) == BlockDiagMatrix(A.T) + assert transpose(BlockDiagMatrix(A, B)) == BlockDiagMatrix(A.T, B.T) + +def test_issue_2460(): + bdm1 = BlockDiagMatrix(Matrix([i]), Matrix([j])) + bdm2 = BlockDiagMatrix(Matrix([k]), Matrix([l])) + assert block_collapse(bdm1 + bdm2) == BlockDiagMatrix(Matrix([i + k]), Matrix([j + l])) + +def test_blockcut(): + A = MatrixSymbol('A', n, m) + B = blockcut(A, (n/2, n/2), (m/2, m/2)) + assert B == BlockMatrix([[A[:n/2, :m/2], A[:n/2, m/2:]], + [A[n/2:, :m/2], A[n/2:, m/2:]]]) + + M = ImmutableMatrix(4, 4, range(16)) + B = blockcut(M, (2, 2), (2, 2)) + assert M == ImmutableMatrix(B) + + B = blockcut(M, (1, 3), (2, 2)) + assert ImmutableMatrix(B.blocks[0, 1]) == ImmutableMatrix([[2, 3]]) + +def test_reblock_2x2(): + B = BlockMatrix([[MatrixSymbol('A_%d%d'%(i,j), 2, 2) + for j in range(3)] + for i in range(3)]) + assert B.blocks.shape == (3, 3) + + BB = reblock_2x2(B) + assert BB.blocks.shape == (2, 2) + + assert B.shape == BB.shape + assert B.as_explicit() == BB.as_explicit() + +def test_deblock(): + B = BlockMatrix([[MatrixSymbol('A_%d%d'%(i,j), n, n) + for j in range(4)] + for i in range(4)]) + + assert deblock(reblock_2x2(B)) == B + +def test_block_collapse_type(): + bm1 = BlockDiagMatrix(ImmutableMatrix([1]), ImmutableMatrix([2])) + bm2 = BlockDiagMatrix(ImmutableMatrix([3]), ImmutableMatrix([4])) + + assert bm1.T.__class__ == BlockDiagMatrix + assert block_collapse(bm1 - bm2).__class__ == BlockDiagMatrix + assert block_collapse(Inverse(bm1)).__class__ == BlockDiagMatrix + assert block_collapse(Transpose(bm1)).__class__ == BlockDiagMatrix + assert bc_transpose(Transpose(bm1)).__class__ == BlockDiagMatrix + assert bc_inverse(Inverse(bm1)).__class__ == BlockDiagMatrix + +def test_invalid_block_matrix(): + raises(ValueError, lambda: BlockMatrix([ + [Identity(2), Identity(5)], + ])) + raises(ValueError, lambda: BlockMatrix([ + [Identity(n), Identity(m)], + ])) + raises(ValueError, lambda: BlockMatrix([ + [ZeroMatrix(n, n), ZeroMatrix(n, n)], + [ZeroMatrix(n, n - 1), ZeroMatrix(n, n + 1)], + ])) + raises(ValueError, lambda: BlockMatrix([ + [ZeroMatrix(n - 1, n), ZeroMatrix(n, n)], + [ZeroMatrix(n + 1, n), ZeroMatrix(n, n)], + ])) + +def test_block_lu_decomposition(): + A = MatrixSymbol('A', n, n) + B = MatrixSymbol('B', n, m) + C = MatrixSymbol('C', m, n) + D = MatrixSymbol('D', m, m) + X = BlockMatrix([[A, B], [C, D]]) + + #LDU decomposition + L, D, U = X.LDUdecomposition() + assert block_collapse(L*D*U) == X + + #UDL decomposition + U, D, L = X.UDLdecomposition() + assert block_collapse(U*D*L) == X + + #LU decomposition + L, U = X.LUdecomposition() + assert block_collapse(L*U) == X + +def test_issue_21866(): + n = 10 + I = Identity(n) + O = ZeroMatrix(n, n) + A = BlockMatrix([[ I, O, O, O ], + [ O, I, O, O ], + [ O, O, I, O ], + [ I, O, O, I ]]) + Ainv = block_collapse(A.inv()) + AinvT = BlockMatrix([[ I, O, O, O ], + [ O, I, O, O ], + [ O, O, I, O ], + [ -I, O, O, I ]]) + assert Ainv == AinvT + + +def test_adjoint_and_special_matrices(): + A = Identity(3) + B = OneMatrix(3, 2) + C = ZeroMatrix(2, 3) + D = Identity(2) + X = BlockMatrix([[A, B], [C, D]]) + X2 = BlockMatrix([[A, S.ImaginaryUnit*B], [C, D]]) + assert X.adjoint() == BlockMatrix([[A, ZeroMatrix(3, 2)], [OneMatrix(2, 3), D]]) + assert re(X) == X + assert X2.adjoint() == BlockMatrix([[A, ZeroMatrix(3, 2)], [-S.ImaginaryUnit*OneMatrix(2, 3), D]]) + assert im(X2) == BlockMatrix([[ZeroMatrix(3, 3), OneMatrix(3, 2)], [ZeroMatrix(2, 3), ZeroMatrix(2, 2)]]) + + +def test_block_matrix_derivative(): + x = symbols('x') + A = Matrix(3, 3, [Function(f'a{i}')(x) for i in range(9)]) + bc = BlockMatrix([[A[:2, :2], A[:2, 2]], [A[2, :2], A[2:, 2]]]) + assert Matrix(bc.diff(x)) - A.diff(x) == zeros(3, 3) + + +def test_transpose_inverse_commute(): + n = Symbol('n') + I = Identity(n) + Z = ZeroMatrix(n, n) + A = BlockMatrix([[I, Z], [Z, I]]) + + assert block_collapse(A.transpose().inverse()) == A + assert block_collapse(A.inverse().transpose()) == A + + assert block_collapse(MatPow(A.transpose(), -2)) == MatPow(A, -2) + assert block_collapse(MatPow(A, -2).transpose()) == MatPow(A, -2) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_companion.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_companion.py new file mode 100644 index 0000000000000000000000000000000000000000..edc592c29098eddce0c6352806aa73d5d889e999 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_companion.py @@ -0,0 +1,48 @@ +from sympy.core.expr import unchanged +from sympy.core.symbol import Symbol, symbols +from sympy.matrices.immutable import ImmutableDenseMatrix +from sympy.matrices.expressions.companion import CompanionMatrix +from sympy.polys.polytools import Poly +from sympy.testing.pytest import raises + + +def test_creation(): + x = Symbol('x') + y = Symbol('y') + raises(ValueError, lambda: CompanionMatrix(1)) + raises(ValueError, lambda: CompanionMatrix(Poly([1], x))) + raises(ValueError, lambda: CompanionMatrix(Poly([2, 1], x))) + raises(ValueError, lambda: CompanionMatrix(Poly(x*y, [x, y]))) + assert unchanged(CompanionMatrix, Poly([1, 2, 3], x)) + + +def test_shape(): + c0, c1, c2 = symbols('c0:3') + x = Symbol('x') + assert CompanionMatrix(Poly([1, c0], x)).shape == (1, 1) + assert CompanionMatrix(Poly([1, c1, c0], x)).shape == (2, 2) + assert CompanionMatrix(Poly([1, c2, c1, c0], x)).shape == (3, 3) + + +def test_entry(): + c0, c1, c2 = symbols('c0:3') + x = Symbol('x') + A = CompanionMatrix(Poly([1, c2, c1, c0], x)) + assert A[0, 0] == 0 + assert A[1, 0] == 1 + assert A[1, 1] == 0 + assert A[2, 1] == 1 + assert A[0, 2] == -c0 + assert A[1, 2] == -c1 + assert A[2, 2] == -c2 + + +def test_as_explicit(): + c0, c1, c2 = symbols('c0:3') + x = Symbol('x') + assert CompanionMatrix(Poly([1, c0], x)).as_explicit() == \ + ImmutableDenseMatrix([-c0]) + assert CompanionMatrix(Poly([1, c1, c0], x)).as_explicit() == \ + ImmutableDenseMatrix([[0, -c0], [1, -c1]]) + assert CompanionMatrix(Poly([1, c2, c1, c0], x)).as_explicit() == \ + ImmutableDenseMatrix([[0, 0, -c0], [1, 0, -c1], [0, 1, -c2]]) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_derivatives.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_derivatives.py new file mode 100644 index 0000000000000000000000000000000000000000..77484c994dda62eea9771a76afd8b3caeadacb93 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_derivatives.py @@ -0,0 +1,477 @@ +""" +Some examples have been taken from: + +http://www.math.uwaterloo.ca/~hwolkowi//matrixcookbook.pdf +""" +from sympy import KroneckerProduct +from sympy.combinatorics import Permutation +from sympy.concrete.summations import Sum +from sympy.core.numbers import Rational +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin, tan) +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.matrices.expressions.determinant import Determinant +from sympy.matrices.expressions.diagonal import DiagMatrix +from sympy.matrices.expressions.hadamard import (HadamardPower, HadamardProduct, hadamard_product) +from sympy.matrices.expressions.inverse import Inverse +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.special import OneMatrix +from sympy.matrices.expressions.trace import Trace +from sympy.matrices.expressions.matadd import MatAdd +from sympy.matrices.expressions.matmul import MatMul +from sympy.matrices.expressions.special import (Identity, ZeroMatrix) +from sympy.tensor.array.array_derivatives import ArrayDerivative +from sympy.matrices.expressions import hadamard_power +from sympy.tensor.array.expressions.array_expressions import ArrayAdd, ArrayTensorProduct, PermuteDims + +i, j, k = symbols("i j k") +m, n = symbols("m n") + +X = MatrixSymbol("X", k, k) +x = MatrixSymbol("x", k, 1) +y = MatrixSymbol("y", k, 1) + +A = MatrixSymbol("A", k, k) +B = MatrixSymbol("B", k, k) +C = MatrixSymbol("C", k, k) +D = MatrixSymbol("D", k, k) + +a = MatrixSymbol("a", k, 1) +b = MatrixSymbol("b", k, 1) +c = MatrixSymbol("c", k, 1) +d = MatrixSymbol("d", k, 1) + + +KDelta = lambda i, j: KroneckerDelta(i, j, (0, k-1)) + + +def _check_derivative_with_explicit_matrix(expr, x, diffexpr, dim=2): + # TODO: this is commented because it slows down the tests. + return + + expr = expr.xreplace({k: dim}) + x = x.xreplace({k: dim}) + diffexpr = diffexpr.xreplace({k: dim}) + + expr = expr.as_explicit() + x = x.as_explicit() + diffexpr = diffexpr.as_explicit() + + assert expr.diff(x).reshape(*diffexpr.shape).tomatrix() == diffexpr + + +def test_matrix_derivative_by_scalar(): + assert A.diff(i) == ZeroMatrix(k, k) + assert (A*(X + B)*c).diff(i) == ZeroMatrix(k, 1) + assert x.diff(i) == ZeroMatrix(k, 1) + assert (x.T*y).diff(i) == ZeroMatrix(1, 1) + assert (x*x.T).diff(i) == ZeroMatrix(k, k) + assert (x + y).diff(i) == ZeroMatrix(k, 1) + assert hadamard_power(x, 2).diff(i) == ZeroMatrix(k, 1) + assert hadamard_power(x, i).diff(i).dummy_eq( + HadamardProduct(x.applyfunc(log), HadamardPower(x, i))) + assert hadamard_product(x, y).diff(i) == ZeroMatrix(k, 1) + assert hadamard_product(i*OneMatrix(k, 1), x, y).diff(i) == hadamard_product(x, y) + assert (i*x).diff(i) == x + assert (sin(i)*A*B*x).diff(i) == cos(i)*A*B*x + assert x.applyfunc(sin).diff(i) == ZeroMatrix(k, 1) + assert Trace(i**2*X).diff(i) == 2*i*Trace(X) + + mu = symbols("mu") + expr = (2*mu*x) + assert expr.diff(x) == 2*mu*Identity(k) + + +def test_one_matrix(): + assert MatMul(x.T, OneMatrix(k, 1)).diff(x) == OneMatrix(k, 1) + + +def test_matrix_derivative_non_matrix_result(): + # This is a 4-dimensional array: + I = Identity(k) + AdA = PermuteDims(ArrayTensorProduct(I, I), Permutation(3)(1, 2)) + assert A.diff(A) == AdA + assert A.T.diff(A) == PermuteDims(ArrayTensorProduct(I, I), Permutation(3)(1, 2, 3)) + assert (2*A).diff(A) == PermuteDims(ArrayTensorProduct(2*I, I), Permutation(3)(1, 2)) + assert MatAdd(A, A).diff(A) == ArrayAdd(AdA, AdA) + assert (A + B).diff(A) == AdA + + +def test_matrix_derivative_trivial_cases(): + # Cookbook example 33: + # TODO: find a way to represent a four-dimensional zero-array: + assert X.diff(A) == ArrayDerivative(X, A) + + +def test_matrix_derivative_with_inverse(): + + # Cookbook example 61: + expr = a.T*Inverse(X)*b + assert expr.diff(X) == -Inverse(X).T*a*b.T*Inverse(X).T + + # Cookbook example 62: + expr = Determinant(Inverse(X)) + # Not implemented yet: + # assert expr.diff(X) == -Determinant(X.inv())*(X.inv()).T + + # Cookbook example 63: + expr = Trace(A*Inverse(X)*B) + assert expr.diff(X) == -(X**(-1)*B*A*X**(-1)).T + + # Cookbook example 64: + expr = Trace(Inverse(X + A)) + assert expr.diff(X) == -(Inverse(X + A)).T**2 + + +def test_matrix_derivative_vectors_and_scalars(): + + assert x.diff(x) == Identity(k) + assert x[i, 0].diff(x[m, 0]).doit() == KDelta(m, i) + + assert x.T.diff(x) == Identity(k) + + # Cookbook example 69: + expr = x.T*a + assert expr.diff(x) == a + assert expr[0, 0].diff(x[m, 0]).doit() == a[m, 0] + expr = a.T*x + assert expr.diff(x) == a + + # Cookbook example 70: + expr = a.T*X*b + assert expr.diff(X) == a*b.T + + # Cookbook example 71: + expr = a.T*X.T*b + assert expr.diff(X) == b*a.T + + # Cookbook example 72: + expr = a.T*X*a + assert expr.diff(X) == a*a.T + expr = a.T*X.T*a + assert expr.diff(X) == a*a.T + + # Cookbook example 77: + expr = b.T*X.T*X*c + assert expr.diff(X) == X*b*c.T + X*c*b.T + + # Cookbook example 78: + expr = (B*x + b).T*C*(D*x + d) + assert expr.diff(x) == B.T*C*(D*x + d) + D.T*C.T*(B*x + b) + + # Cookbook example 81: + expr = x.T*B*x + assert expr.diff(x) == B*x + B.T*x + + # Cookbook example 82: + expr = b.T*X.T*D*X*c + assert expr.diff(X) == D.T*X*b*c.T + D*X*c*b.T + + # Cookbook example 83: + expr = (X*b + c).T*D*(X*b + c) + assert expr.diff(X) == D*(X*b + c)*b.T + D.T*(X*b + c)*b.T + assert str(expr[0, 0].diff(X[m, n]).doit()) == \ + 'b[n, 0]*Sum((c[_i_1, 0] + Sum(X[_i_1, _i_3]*b[_i_3, 0], (_i_3, 0, k - 1)))*D[_i_1, m], (_i_1, 0, k - 1)) + Sum((c[_i_2, 0] + Sum(X[_i_2, _i_4]*b[_i_4, 0], (_i_4, 0, k - 1)))*D[m, _i_2]*b[n, 0], (_i_2, 0, k - 1))' + + # See https://github.com/sympy/sympy/issues/16504#issuecomment-1018339957 + expr = x*x.T*x + I = Identity(k) + assert expr.diff(x) == KroneckerProduct(I, x.T*x) + 2*x*x.T + + +def test_matrix_derivatives_of_traces(): + + expr = Trace(A)*A + I = Identity(k) + assert expr.diff(A) == ArrayAdd(ArrayTensorProduct(I, A), PermuteDims(ArrayTensorProduct(Trace(A)*I, I), Permutation(3)(1, 2))) + assert expr[i, j].diff(A[m, n]).doit() == ( + KDelta(i, m)*KDelta(j, n)*Trace(A) + + KDelta(m, n)*A[i, j] + ) + + ## First order: + + # Cookbook example 99: + expr = Trace(X) + assert expr.diff(X) == Identity(k) + assert expr.rewrite(Sum).diff(X[m, n]).doit() == KDelta(m, n) + + # Cookbook example 100: + expr = Trace(X*A) + assert expr.diff(X) == A.T + assert expr.rewrite(Sum).diff(X[m, n]).doit() == A[n, m] + + # Cookbook example 101: + expr = Trace(A*X*B) + assert expr.diff(X) == A.T*B.T + assert expr.rewrite(Sum).diff(X[m, n]).doit().dummy_eq((A.T*B.T)[m, n]) + + # Cookbook example 102: + expr = Trace(A*X.T*B) + assert expr.diff(X) == B*A + + # Cookbook example 103: + expr = Trace(X.T*A) + assert expr.diff(X) == A + + # Cookbook example 104: + expr = Trace(A*X.T) + assert expr.diff(X) == A + + # Cookbook example 105: + # TODO: TensorProduct is not supported + #expr = Trace(TensorProduct(A, X)) + #assert expr.diff(X) == Trace(A)*Identity(k) + + ## Second order: + + # Cookbook example 106: + expr = Trace(X**2) + assert expr.diff(X) == 2*X.T + + # Cookbook example 107: + expr = Trace(X**2*B) + assert expr.diff(X) == (X*B + B*X).T + expr = Trace(MatMul(X, X, B)) + assert expr.diff(X) == (X*B + B*X).T + + # Cookbook example 108: + expr = Trace(X.T*B*X) + assert expr.diff(X) == B*X + B.T*X + + # Cookbook example 109: + expr = Trace(B*X*X.T) + assert expr.diff(X) == B*X + B.T*X + + # Cookbook example 110: + expr = Trace(X*X.T*B) + assert expr.diff(X) == B*X + B.T*X + + # Cookbook example 111: + expr = Trace(X*B*X.T) + assert expr.diff(X) == X*B.T + X*B + + # Cookbook example 112: + expr = Trace(B*X.T*X) + assert expr.diff(X) == X*B.T + X*B + + # Cookbook example 113: + expr = Trace(X.T*X*B) + assert expr.diff(X) == X*B.T + X*B + + # Cookbook example 114: + expr = Trace(A*X*B*X) + assert expr.diff(X) == A.T*X.T*B.T + B.T*X.T*A.T + + # Cookbook example 115: + expr = Trace(X.T*X) + assert expr.diff(X) == 2*X + expr = Trace(X*X.T) + assert expr.diff(X) == 2*X + + # Cookbook example 116: + expr = Trace(B.T*X.T*C*X*B) + assert expr.diff(X) == C.T*X*B*B.T + C*X*B*B.T + + # Cookbook example 117: + expr = Trace(X.T*B*X*C) + assert expr.diff(X) == B*X*C + B.T*X*C.T + + # Cookbook example 118: + expr = Trace(A*X*B*X.T*C) + assert expr.diff(X) == A.T*C.T*X*B.T + C*A*X*B + + # Cookbook example 119: + expr = Trace((A*X*B + C)*(A*X*B + C).T) + assert expr.diff(X) == 2*A.T*(A*X*B + C)*B.T + + # Cookbook example 120: + # TODO: no support for TensorProduct. + # expr = Trace(TensorProduct(X, X)) + # expr = Trace(X)*Trace(X) + # expr.diff(X) == 2*Trace(X)*Identity(k) + + # Higher Order + + # Cookbook example 121: + expr = Trace(X**k) + #assert expr.diff(X) == k*(X**(k-1)).T + + # Cookbook example 122: + expr = Trace(A*X**k) + #assert expr.diff(X) == # Needs indices + + # Cookbook example 123: + expr = Trace(B.T*X.T*C*X*X.T*C*X*B) + assert expr.diff(X) == C*X*X.T*C*X*B*B.T + C.T*X*B*B.T*X.T*C.T*X + C*X*B*B.T*X.T*C*X + C.T*X*X.T*C.T*X*B*B.T + + # Other + + # Cookbook example 124: + expr = Trace(A*X**(-1)*B) + assert expr.diff(X) == -Inverse(X).T*A.T*B.T*Inverse(X).T + + # Cookbook example 125: + expr = Trace(Inverse(X.T*C*X)*A) + # Warning: result in the cookbook is equivalent if B and C are symmetric: + assert expr.diff(X) == - X.inv().T*A.T*X.inv()*C.inv().T*X.inv().T - X.inv().T*A*X.inv()*C.inv()*X.inv().T + + # Cookbook example 126: + expr = Trace((X.T*C*X).inv()*(X.T*B*X)) + assert expr.diff(X) == -2*C*X*(X.T*C*X).inv()*X.T*B*X*(X.T*C*X).inv() + 2*B*X*(X.T*C*X).inv() + + # Cookbook example 127: + expr = Trace((A + X.T*C*X).inv()*(X.T*B*X)) + # Warning: result in the cookbook is equivalent if B and C are symmetric: + assert expr.diff(X) == B*X*Inverse(A + X.T*C*X) - C*X*Inverse(A + X.T*C*X)*X.T*B*X*Inverse(A + X.T*C*X) - C.T*X*Inverse(A.T + (C*X).T*X)*X.T*B.T*X*Inverse(A.T + (C*X).T*X) + B.T*X*Inverse(A.T + (C*X).T*X) + + +def test_derivatives_of_complicated_matrix_expr(): + expr = a.T*(A*X*(X.T*B + X*A) + B.T*X.T*(a*b.T*(X*D*X.T + X*(X.T*B + A*X)*D*B - X.T*C.T*A)*B + B*(X*D.T + B*A*X*A.T - 3*X*D))*B + 42*X*B*X.T*A.T*(X + X.T))*b + result = (B*(B*A*X*A.T - 3*X*D + X*D.T) + a*b.T*(X*(A*X + X.T*B)*D*B + X*D*X.T - X.T*C.T*A)*B)*B*b*a.T*B.T + B**2*b*a.T*B.T*X.T*a*b.T*X*D + 42*A*X*B.T*X.T*a*b.T + B*D*B**3*b*a.T*B.T*X.T*a*b.T*X + B*b*a.T*A*X + a*b.T*(42*X + 42*X.T)*A*X*B.T + b*a.T*X*B*a*b.T*B.T**2*X*D.T + b*a.T*X*B*a*b.T*B.T**3*D.T*(B.T*X + X.T*A.T) + 42*b*a.T*X*B*X.T*A.T + A.T*(42*X + 42*X.T)*b*a.T*X*B + A.T*B.T**2*X*B*a*b.T*B.T*A + A.T*a*b.T*(A.T*X.T + B.T*X) + A.T*X.T*b*a.T*X*B*a*b.T*B.T**3*D.T + B.T*X*B*a*b.T*B.T*D - 3*B.T*X*B*a*b.T*B.T*D.T - C.T*A*B**2*b*a.T*B.T*X.T*a*b.T + X.T*A.T*a*b.T*A.T + assert expr.diff(X) == result + + +def test_mixed_deriv_mixed_expressions(): + + expr = 3*Trace(A) + assert expr.diff(A) == 3*Identity(k) + + expr = k + deriv = expr.diff(A) + assert isinstance(deriv, ZeroMatrix) + assert deriv == ZeroMatrix(k, k) + + expr = Trace(A)**2 + assert expr.diff(A) == (2*Trace(A))*Identity(k) + + expr = Trace(A)*A + I = Identity(k) + assert expr.diff(A) == ArrayAdd(ArrayTensorProduct(I, A), PermuteDims(ArrayTensorProduct(Trace(A)*I, I), Permutation(3)(1, 2))) + + expr = Trace(Trace(A)*A) + assert expr.diff(A) == (2*Trace(A))*Identity(k) + + expr = Trace(Trace(Trace(A)*A)*A) + assert expr.diff(A) == (3*Trace(A)**2)*Identity(k) + + +def test_derivatives_matrix_norms(): + + expr = x.T*y + assert expr.diff(x) == y + assert expr[0, 0].diff(x[m, 0]).doit() == y[m, 0] + + expr = (x.T*y)**S.Half + assert expr.diff(x) == y/(2*sqrt(x.T*y)) + + expr = (x.T*x)**S.Half + assert expr.diff(x) == x*(x.T*x)**Rational(-1, 2) + + expr = (c.T*a*x.T*b)**S.Half + assert expr.diff(x) == b*a.T*c/sqrt(c.T*a*x.T*b)/2 + + expr = (c.T*a*x.T*b)**Rational(1, 3) + assert expr.diff(x) == b*a.T*c*(c.T*a*x.T*b)**Rational(-2, 3)/3 + + expr = (a.T*X*b)**S.Half + assert expr.diff(X) == a/(2*sqrt(a.T*X*b))*b.T + + expr = d.T*x*(a.T*X*b)**S.Half*y.T*c + assert expr.diff(X) == a/(2*sqrt(a.T*X*b))*x.T*d*y.T*c*b.T + + +def test_derivatives_elementwise_applyfunc(): + + expr = x.applyfunc(tan) + assert expr.diff(x).dummy_eq( + DiagMatrix(x.applyfunc(lambda x: tan(x)**2 + 1))) + assert expr[i, 0].diff(x[m, 0]).doit() == (tan(x[i, 0])**2 + 1)*KDelta(i, m) + _check_derivative_with_explicit_matrix(expr, x, expr.diff(x)) + + expr = (i**2*x).applyfunc(sin) + assert expr.diff(i).dummy_eq( + HadamardProduct((2*i)*x, (i**2*x).applyfunc(cos))) + assert expr[i, 0].diff(i).doit() == 2*i*x[i, 0]*cos(i**2*x[i, 0]) + _check_derivative_with_explicit_matrix(expr, i, expr.diff(i)) + + expr = (log(i)*A*B).applyfunc(sin) + assert expr.diff(i).dummy_eq( + HadamardProduct(A*B/i, (log(i)*A*B).applyfunc(cos))) + _check_derivative_with_explicit_matrix(expr, i, expr.diff(i)) + + expr = A*x.applyfunc(exp) + # TODO: restore this result (currently returning the transpose): + # assert expr.diff(x).dummy_eq(DiagMatrix(x.applyfunc(exp))*A.T) + _check_derivative_with_explicit_matrix(expr, x, expr.diff(x)) + + expr = x.T*A*x + k*y.applyfunc(sin).T*x + assert expr.diff(x).dummy_eq(A.T*x + A*x + k*y.applyfunc(sin)) + _check_derivative_with_explicit_matrix(expr, x, expr.diff(x)) + + expr = x.applyfunc(sin).T*y + # TODO: restore (currently returning the transpose): + # assert expr.diff(x).dummy_eq(DiagMatrix(x.applyfunc(cos))*y) + _check_derivative_with_explicit_matrix(expr, x, expr.diff(x)) + + expr = (a.T * X * b).applyfunc(sin) + assert expr.diff(X).dummy_eq(a*(a.T*X*b).applyfunc(cos)*b.T) + _check_derivative_with_explicit_matrix(expr, X, expr.diff(X)) + + expr = a.T * X.applyfunc(sin) * b + assert expr.diff(X).dummy_eq( + DiagMatrix(a)*X.applyfunc(cos)*DiagMatrix(b)) + _check_derivative_with_explicit_matrix(expr, X, expr.diff(X)) + + expr = a.T * (A*X*B).applyfunc(sin) * b + assert expr.diff(X).dummy_eq( + A.T*DiagMatrix(a)*(A*X*B).applyfunc(cos)*DiagMatrix(b)*B.T) + _check_derivative_with_explicit_matrix(expr, X, expr.diff(X)) + + expr = a.T * (A*X*b).applyfunc(sin) * b.T + # TODO: not implemented + #assert expr.diff(X) == ... + #_check_derivative_with_explicit_matrix(expr, X, expr.diff(X)) + + expr = a.T*A*X.applyfunc(sin)*B*b + assert expr.diff(X).dummy_eq( + HadamardProduct(A.T * a * b.T * B.T, X.applyfunc(cos))) + + expr = a.T * (A*X.applyfunc(sin)*B).applyfunc(log) * b + # TODO: wrong + # assert expr.diff(X) == A.T*DiagMatrix(a)*(A*X.applyfunc(sin)*B).applyfunc(Lambda(k, 1/k))*DiagMatrix(b)*B.T + + expr = a.T * (X.applyfunc(sin)).applyfunc(log) * b + # TODO: wrong + # assert expr.diff(X) == DiagMatrix(a)*X.applyfunc(sin).applyfunc(Lambda(k, 1/k))*DiagMatrix(b) + + +def test_derivatives_of_hadamard_expressions(): + + # Hadamard Product + + expr = hadamard_product(a, x, b) + assert expr.diff(x) == DiagMatrix(hadamard_product(b, a)) + + expr = a.T*hadamard_product(A, X, B)*b + assert expr.diff(X) == HadamardProduct(a*b.T, A, B) + + # Hadamard Power + + expr = hadamard_power(x, 2) + assert expr.diff(x).doit() == 2*DiagMatrix(x) + + expr = hadamard_power(x.T, 2) + assert expr.diff(x).doit() == 2*DiagMatrix(x) + + expr = hadamard_power(x, S.Half) + assert expr.diff(x) == S.Half*DiagMatrix(hadamard_power(x, Rational(-1, 2))) + + expr = hadamard_power(a.T*X*b, 2) + assert expr.diff(X) == 2*a*a.T*X*b*b.T + + expr = hadamard_power(a.T*X*b, S.Half) + assert expr.diff(X) == a/(2*sqrt(a.T*X*b))*b.T diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_determinant.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_determinant.py new file mode 100644 index 0000000000000000000000000000000000000000..d1a66c728f076f8c769d2519ee47c8a9cc90a90e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_determinant.py @@ -0,0 +1,65 @@ +from sympy.core import S, symbols +from sympy.matrices import eye, ones, Matrix, ShapeError +from sympy.matrices.expressions import ( + Identity, MatrixExpr, MatrixSymbol, Determinant, + det, per, ZeroMatrix, Transpose, + Permanent, MatMul +) +from sympy.matrices.expressions.special import OneMatrix +from sympy.testing.pytest import raises +from sympy.assumptions.ask import Q +from sympy.assumptions.refine import refine + +n = symbols('n', integer=True) +A = MatrixSymbol('A', n, n) +B = MatrixSymbol('B', n, n) +C = MatrixSymbol('C', 3, 4) + + +def test_det(): + assert isinstance(Determinant(A), Determinant) + assert not isinstance(Determinant(A), MatrixExpr) + raises(ShapeError, lambda: Determinant(C)) + assert det(eye(3)) == 1 + assert det(Matrix(3, 3, [1, 3, 2, 4, 1, 3, 2, 5, 2])) == 17 + _ = A / det(A) # Make sure this is possible + + raises(TypeError, lambda: Determinant(S.One)) + + assert Determinant(A).arg is A + + +def test_eval_determinant(): + assert det(Identity(n)) == 1 + assert det(ZeroMatrix(n, n)) == 0 + assert det(OneMatrix(n, n)) == Determinant(OneMatrix(n, n)) + assert det(OneMatrix(1, 1)) == 1 + assert det(OneMatrix(2, 2)) == 0 + assert det(Transpose(A)) == det(A) + assert Determinant(MatMul(eye(2), eye(2))).doit(deep=True) == 1 + + +def test_refine(): + assert refine(det(A), Q.orthogonal(A)) == 1 + assert refine(det(A), Q.singular(A)) == 0 + assert refine(det(A), Q.unit_triangular(A)) == 1 + assert refine(det(A), Q.normal(A)) == det(A) + + +def test_commutative(): + det_a = Determinant(A) + det_b = Determinant(B) + assert det_a.is_commutative + assert det_b.is_commutative + assert det_a * det_b == det_b * det_a + + +def test_permanent(): + assert isinstance(Permanent(A), Permanent) + assert not isinstance(Permanent(A), MatrixExpr) + assert isinstance(Permanent(C), Permanent) + assert Permanent(ones(3, 3)).doit() == 6 + _ = C / per(C) + assert per(Matrix(3, 3, [1, 3, 2, 4, 1, 3, 2, 5, 2])) == 103 + raises(TypeError, lambda: Permanent(S.One)) + assert Permanent(A).arg is A diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_diagonal.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_diagonal.py new file mode 100644 index 0000000000000000000000000000000000000000..3e4f7ea4c178121c33eeb26c09675403d274c1e8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_diagonal.py @@ -0,0 +1,156 @@ +from sympy.matrices.expressions import MatrixSymbol +from sympy.matrices.expressions.diagonal import DiagonalMatrix, DiagonalOf, DiagMatrix, diagonalize_vector +from sympy.assumptions.ask import (Q, ask) +from sympy.core.symbol import Symbol +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.matrices.dense import Matrix +from sympy.matrices.expressions.matmul import MatMul +from sympy.matrices.expressions.special import Identity +from sympy.testing.pytest import raises + + +n = Symbol('n') +m = Symbol('m') + + +def test_DiagonalMatrix(): + x = MatrixSymbol('x', n, m) + D = DiagonalMatrix(x) + assert D.diagonal_length is None + assert D.shape == (n, m) + + x = MatrixSymbol('x', n, n) + D = DiagonalMatrix(x) + assert D.diagonal_length == n + assert D.shape == (n, n) + assert D[1, 2] == 0 + assert D[1, 1] == x[1, 1] + i = Symbol('i') + j = Symbol('j') + x = MatrixSymbol('x', 3, 3) + ij = DiagonalMatrix(x)[i, j] + assert ij != 0 + assert ij.subs({i:0, j:0}) == x[0, 0] + assert ij.subs({i:0, j:1}) == 0 + assert ij.subs({i:1, j:1}) == x[1, 1] + assert ask(Q.diagonal(D)) # affirm that D is diagonal + + x = MatrixSymbol('x', n, 3) + D = DiagonalMatrix(x) + assert D.diagonal_length == 3 + assert D.shape == (n, 3) + assert D[2, m] == KroneckerDelta(2, m)*x[2, m] + assert D[3, m] == 0 + raises(IndexError, lambda: D[m, 3]) + + x = MatrixSymbol('x', 3, n) + D = DiagonalMatrix(x) + assert D.diagonal_length == 3 + assert D.shape == (3, n) + assert D[m, 2] == KroneckerDelta(m, 2)*x[m, 2] + assert D[m, 3] == 0 + raises(IndexError, lambda: D[3, m]) + + x = MatrixSymbol('x', n, m) + D = DiagonalMatrix(x) + assert D.diagonal_length is None + assert D.shape == (n, m) + assert D[m, 4] != 0 + + x = MatrixSymbol('x', 3, 4) + assert [DiagonalMatrix(x)[i] for i in range(12)] == [ + x[0, 0], 0, 0, 0, 0, x[1, 1], 0, 0, 0, 0, x[2, 2], 0] + + # shape is retained, issue 12427 + assert ( + DiagonalMatrix(MatrixSymbol('x', 3, 4))* + DiagonalMatrix(MatrixSymbol('x', 4, 2))).shape == (3, 2) + + +def test_DiagonalOf(): + x = MatrixSymbol('x', n, n) + d = DiagonalOf(x) + assert d.shape == (n, 1) + assert d.diagonal_length == n + assert d[2, 0] == d[2] == x[2, 2] + + x = MatrixSymbol('x', n, m) + d = DiagonalOf(x) + assert d.shape == (None, 1) + assert d.diagonal_length is None + assert d[2, 0] == d[2] == x[2, 2] + + d = DiagonalOf(MatrixSymbol('x', 4, 3)) + assert d.shape == (3, 1) + d = DiagonalOf(MatrixSymbol('x', n, 3)) + assert d.shape == (3, 1) + d = DiagonalOf(MatrixSymbol('x', 3, n)) + assert d.shape == (3, 1) + x = MatrixSymbol('x', n, m) + assert [DiagonalOf(x)[i] for i in range(4)] ==[ + x[0, 0], x[1, 1], x[2, 2], x[3, 3]] + + +def test_DiagMatrix(): + x = MatrixSymbol('x', n, 1) + d = DiagMatrix(x) + assert d.shape == (n, n) + assert d[0, 1] == 0 + assert d[0, 0] == x[0, 0] + + a = MatrixSymbol('a', 1, 1) + d = diagonalize_vector(a) + assert isinstance(d, MatrixSymbol) + assert a == d + assert diagonalize_vector(Identity(3)) == Identity(3) + assert DiagMatrix(Identity(3)).doit() == Identity(3) + assert isinstance(DiagMatrix(Identity(3)), DiagMatrix) + + # A diagonal matrix is equal to its transpose: + assert DiagMatrix(x).T == DiagMatrix(x) + assert diagonalize_vector(x.T) == DiagMatrix(x) + + dx = DiagMatrix(x) + assert dx[0, 0] == x[0, 0] + assert dx[1, 1] == x[1, 0] + assert dx[0, 1] == 0 + assert dx[0, m] == x[0, 0]*KroneckerDelta(0, m) + + z = MatrixSymbol('z', 1, n) + dz = DiagMatrix(z) + assert dz[0, 0] == z[0, 0] + assert dz[1, 1] == z[0, 1] + assert dz[0, 1] == 0 + assert dz[0, m] == z[0, m]*KroneckerDelta(0, m) + + v = MatrixSymbol('v', 3, 1) + dv = DiagMatrix(v) + assert dv.as_explicit() == Matrix([ + [v[0, 0], 0, 0], + [0, v[1, 0], 0], + [0, 0, v[2, 0]], + ]) + + v = MatrixSymbol('v', 1, 3) + dv = DiagMatrix(v) + assert dv.as_explicit() == Matrix([ + [v[0, 0], 0, 0], + [0, v[0, 1], 0], + [0, 0, v[0, 2]], + ]) + + dv = DiagMatrix(3*v) + assert dv.args == (3*v,) + assert dv.doit() == 3*DiagMatrix(v) + assert isinstance(dv.doit(), MatMul) + + a = MatrixSymbol("a", 3, 1).as_explicit() + expr = DiagMatrix(a) + result = Matrix([ + [a[0, 0], 0, 0], + [0, a[1, 0], 0], + [0, 0, a[2, 0]], + ]) + assert expr.doit() == result + expr = DiagMatrix(a.T) + assert expr.doit() == result diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_dotproduct.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_dotproduct.py new file mode 100644 index 0000000000000000000000000000000000000000..abf8ab8e935cbd3039f25f83d3603ac444e5a7bb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_dotproduct.py @@ -0,0 +1,35 @@ +from sympy.core.expr import unchanged +from sympy.core.mul import Mul +from sympy.matrices import Matrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.dotproduct import DotProduct +from sympy.testing.pytest import raises + + +A = Matrix(3, 1, [1, 2, 3]) +B = Matrix(3, 1, [1, 3, 5]) +C = Matrix(4, 1, [1, 2, 4, 5]) +D = Matrix(2, 2, [1, 2, 3, 4]) + +def test_docproduct(): + assert DotProduct(A, B).doit() == 22 + assert DotProduct(A.T, B).doit() == 22 + assert DotProduct(A, B.T).doit() == 22 + assert DotProduct(A.T, B.T).doit() == 22 + + raises(TypeError, lambda: DotProduct(1, A)) + raises(TypeError, lambda: DotProduct(A, 1)) + raises(TypeError, lambda: DotProduct(A, D)) + raises(TypeError, lambda: DotProduct(D, A)) + + raises(TypeError, lambda: DotProduct(B, C).doit()) + +def test_dotproduct_symbolic(): + A = MatrixSymbol('A', 3, 1) + B = MatrixSymbol('B', 3, 1) + + dot = DotProduct(A, B) + assert dot.is_scalar == True + assert unchanged(Mul, 2, dot) + # XXX Fix forced evaluation for arithmetics with matrix expressions + assert dot * A == (A[0, 0]*B[0, 0] + A[1, 0]*B[1, 0] + A[2, 0]*B[2, 0])*A diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_factorizations.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_factorizations.py new file mode 100644 index 0000000000000000000000000000000000000000..a0319acabbb7409dfa5c24ceca39e25ff0240618 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_factorizations.py @@ -0,0 +1,29 @@ +from sympy.matrices.expressions.factorizations import lu, LofCholesky, qr, svd +from sympy.assumptions.ask import (Q, ask) +from sympy.core.symbol import Symbol +from sympy.matrices.expressions.matexpr import MatrixSymbol + +n = Symbol('n') +X = MatrixSymbol('X', n, n) + +def test_LU(): + L, U = lu(X) + assert L.shape == U.shape == X.shape + assert ask(Q.lower_triangular(L)) + assert ask(Q.upper_triangular(U)) + +def test_Cholesky(): + LofCholesky(X) + +def test_QR(): + Q_, R = qr(X) + assert Q_.shape == R.shape == X.shape + assert ask(Q.orthogonal(Q_)) + assert ask(Q.upper_triangular(R)) + +def test_svd(): + U, S, V = svd(X) + assert U.shape == S.shape == V.shape == X.shape + assert ask(Q.orthogonal(U)) + assert ask(Q.orthogonal(V)) + assert ask(Q.diagonal(S)) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_fourier.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_fourier.py new file mode 100644 index 0000000000000000000000000000000000000000..0230c8a0957ed28fb0a5cc1e9ee77ecae797265b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_fourier.py @@ -0,0 +1,44 @@ +from sympy.assumptions.ask import (Q, ask) +from sympy.core.numbers import (I, Rational) +from sympy.core.singleton import S +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.simplify.simplify import simplify +from sympy.core.symbol import symbols +from sympy.matrices.expressions.fourier import DFT, IDFT +from sympy.matrices import det, Matrix, Identity +from sympy.testing.pytest import raises + + +def test_dft_creation(): + assert DFT(2) + assert DFT(0) + raises(ValueError, lambda: DFT(-1)) + raises(ValueError, lambda: DFT(2.0)) + raises(ValueError, lambda: DFT(2 + 1j)) + + n = symbols('n') + assert DFT(n) + n = symbols('n', integer=False) + raises(ValueError, lambda: DFT(n)) + n = symbols('n', negative=True) + raises(ValueError, lambda: DFT(n)) + + +def test_dft(): + n, i, j = symbols('n i j') + assert DFT(4).shape == (4, 4) + assert ask(Q.unitary(DFT(4))) + assert Abs(simplify(det(Matrix(DFT(4))))) == 1 + assert DFT(n)*IDFT(n) == Identity(n) + assert DFT(n)[i, j] == exp(-2*S.Pi*I/n)**(i*j) / sqrt(n) + + +def test_dft2(): + assert DFT(1).as_explicit() == Matrix([[1]]) + assert DFT(2).as_explicit() == 1/sqrt(2)*Matrix([[1,1],[1,-1]]) + assert DFT(4).as_explicit() == Matrix([[S.Half, S.Half, S.Half, S.Half], + [S.Half, -I/2, Rational(-1,2), I/2], + [S.Half, Rational(-1,2), S.Half, Rational(-1,2)], + [S.Half, I/2, Rational(-1,2), -I/2]]) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_funcmatrix.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_funcmatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..e4850fe5c739b9390fac6afa10757b5babf821c6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_funcmatrix.py @@ -0,0 +1,54 @@ +from sympy.core import symbols, Lambda +from sympy.core.sympify import SympifyError +from sympy.functions import KroneckerDelta +from sympy.matrices import Matrix +from sympy.matrices.expressions import FunctionMatrix, MatrixExpr, Identity +from sympy.testing.pytest import raises + + +def test_funcmatrix_creation(): + i, j, k = symbols('i j k') + assert FunctionMatrix(2, 2, Lambda((i, j), 0)) + assert FunctionMatrix(0, 0, Lambda((i, j), 0)) + + raises(ValueError, lambda: FunctionMatrix(-1, 0, Lambda((i, j), 0))) + raises(ValueError, lambda: FunctionMatrix(2.0, 0, Lambda((i, j), 0))) + raises(ValueError, lambda: FunctionMatrix(2j, 0, Lambda((i, j), 0))) + raises(ValueError, lambda: FunctionMatrix(0, -1, Lambda((i, j), 0))) + raises(ValueError, lambda: FunctionMatrix(0, 2.0, Lambda((i, j), 0))) + raises(ValueError, lambda: FunctionMatrix(0, 2j, Lambda((i, j), 0))) + + raises(ValueError, lambda: FunctionMatrix(2, 2, Lambda(i, 0))) + raises(SympifyError, lambda: FunctionMatrix(2, 2, lambda i, j: 0)) + raises(ValueError, lambda: FunctionMatrix(2, 2, Lambda((i,), 0))) + raises(ValueError, lambda: FunctionMatrix(2, 2, Lambda((i, j, k), 0))) + raises(ValueError, lambda: FunctionMatrix(2, 2, i+j)) + assert FunctionMatrix(2, 2, "lambda i, j: 0") == \ + FunctionMatrix(2, 2, Lambda((i, j), 0)) + + m = FunctionMatrix(2, 2, KroneckerDelta) + assert m.as_explicit() == Identity(2).as_explicit() + assert m.args[2].dummy_eq(Lambda((i, j), KroneckerDelta(i, j))) + + n = symbols('n') + assert FunctionMatrix(n, n, Lambda((i, j), 0)) + n = symbols('n', integer=False) + raises(ValueError, lambda: FunctionMatrix(n, n, Lambda((i, j), 0))) + n = symbols('n', negative=True) + raises(ValueError, lambda: FunctionMatrix(n, n, Lambda((i, j), 0))) + + +def test_funcmatrix(): + i, j = symbols('i,j') + X = FunctionMatrix(3, 3, Lambda((i, j), i - j)) + assert X[1, 1] == 0 + assert X[1, 2] == -1 + assert X.shape == (3, 3) + assert X.rows == X.cols == 3 + assert Matrix(X) == Matrix(3, 3, lambda i, j: i - j) + assert isinstance(X*X + X, MatrixExpr) + + +def test_replace_issue(): + X = FunctionMatrix(3, 3, KroneckerDelta) + assert X.replace(lambda x: True, lambda x: x) == X diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_hadamard.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_hadamard.py new file mode 100644 index 0000000000000000000000000000000000000000..800fa830a9b089103d69b372db93ebcea541d02b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_hadamard.py @@ -0,0 +1,141 @@ +from sympy.matrices.dense import Matrix, eye +from sympy.matrices.exceptions import ShapeError +from sympy.matrices.expressions.matadd import MatAdd +from sympy.matrices.expressions.special import Identity, OneMatrix, ZeroMatrix +from sympy.core import symbols +from sympy.testing.pytest import raises, warns_deprecated_sympy + +from sympy.matrices import MatrixSymbol +from sympy.matrices.expressions import (HadamardProduct, hadamard_product, HadamardPower, hadamard_power) + +n, m, k = symbols('n,m,k') +Z = MatrixSymbol('Z', n, n) +A = MatrixSymbol('A', n, m) +B = MatrixSymbol('B', n, m) +C = MatrixSymbol('C', m, k) + + +def test_HadamardProduct(): + assert HadamardProduct(A, B, A).shape == A.shape + + raises(TypeError, lambda: HadamardProduct(A, n)) + raises(TypeError, lambda: HadamardProduct(A, 1)) + + assert HadamardProduct(A, 2*B, -A)[1, 1] == \ + -2 * A[1, 1] * B[1, 1] * A[1, 1] + + mix = HadamardProduct(Z*A, B)*C + assert mix.shape == (n, k) + + assert set(HadamardProduct(A, B, A).T.args) == {A.T, A.T, B.T} + + +def test_HadamardProduct_isnt_commutative(): + assert HadamardProduct(A, B) != HadamardProduct(B, A) + + +def test_mixed_indexing(): + X = MatrixSymbol('X', 2, 2) + Y = MatrixSymbol('Y', 2, 2) + Z = MatrixSymbol('Z', 2, 2) + + assert (X*HadamardProduct(Y, Z))[0, 0] == \ + X[0, 0]*Y[0, 0]*Z[0, 0] + X[0, 1]*Y[1, 0]*Z[1, 0] + + +def test_canonicalize(): + X = MatrixSymbol('X', 2, 2) + Y = MatrixSymbol('Y', 2, 2) + with warns_deprecated_sympy(): + expr = HadamardProduct(X, check=False) + assert isinstance(expr, HadamardProduct) + expr2 = expr.doit() # unpack is called + assert isinstance(expr2, MatrixSymbol) + Z = ZeroMatrix(2, 2) + U = OneMatrix(2, 2) + assert HadamardProduct(Z, X).doit() == Z + assert HadamardProduct(U, X, X, U).doit() == HadamardPower(X, 2) + assert HadamardProduct(X, U, Y).doit() == HadamardProduct(X, Y) + assert HadamardProduct(X, Z, U, Y).doit() == Z + + +def test_hadamard(): + m, n, p = symbols('m, n, p', integer=True) + A = MatrixSymbol('A', m, n) + B = MatrixSymbol('B', m, n) + X = MatrixSymbol('X', m, m) + I = Identity(m) + + raises(TypeError, lambda: hadamard_product()) + assert hadamard_product(A) == A + assert isinstance(hadamard_product(A, B), HadamardProduct) + assert hadamard_product(A, B).doit() == hadamard_product(A, B) + assert hadamard_product(X, I) == HadamardProduct(I, X) + assert isinstance(hadamard_product(X, I), HadamardProduct) + + a = MatrixSymbol("a", k, 1) + expr = MatAdd(ZeroMatrix(k, 1), OneMatrix(k, 1)) + expr = HadamardProduct(expr, a) + assert expr.doit() == a + + raises(ValueError, lambda: HadamardProduct()) + + +def test_hadamard_product_with_explicit_mat(): + A = MatrixSymbol("A", 3, 3).as_explicit() + B = MatrixSymbol("B", 3, 3).as_explicit() + X = MatrixSymbol("X", 3, 3) + expr = hadamard_product(A, B) + ret = Matrix([i*j for i, j in zip(A, B)]).reshape(3, 3) + assert expr == ret + expr = hadamard_product(A, X, B) + assert expr == HadamardProduct(ret, X) + expr = hadamard_product(eye(3), A) + assert expr == Matrix([[A[0, 0], 0, 0], [0, A[1, 1], 0], [0, 0, A[2, 2]]]) + expr = hadamard_product(eye(3), eye(3)) + assert expr == eye(3) + + +def test_hadamard_power(): + m, n, p = symbols('m, n, p', integer=True) + A = MatrixSymbol('A', m, n) + + assert hadamard_power(A, 1) == A + assert isinstance(hadamard_power(A, 2), HadamardPower) + assert hadamard_power(A, n).T == hadamard_power(A.T, n) + assert hadamard_power(A, n)[0, 0] == A[0, 0]**n + assert hadamard_power(m, n) == m**n + raises(ValueError, lambda: hadamard_power(A, A)) + + +def test_hadamard_power_explicit(): + A = MatrixSymbol('A', 2, 2) + B = MatrixSymbol('B', 2, 2) + a, b = symbols('a b') + + assert HadamardPower(a, b) == a**b + + assert HadamardPower(a, B).as_explicit() == \ + Matrix([ + [a**B[0, 0], a**B[0, 1]], + [a**B[1, 0], a**B[1, 1]]]) + + assert HadamardPower(A, b).as_explicit() == \ + Matrix([ + [A[0, 0]**b, A[0, 1]**b], + [A[1, 0]**b, A[1, 1]**b]]) + + assert HadamardPower(A, B).as_explicit() == \ + Matrix([ + [A[0, 0]**B[0, 0], A[0, 1]**B[0, 1]], + [A[1, 0]**B[1, 0], A[1, 1]**B[1, 1]]]) + + +def test_shape_error(): + A = MatrixSymbol('A', 2, 3) + B = MatrixSymbol('B', 3, 3) + raises(ShapeError, lambda: HadamardProduct(A, B)) + raises(ShapeError, lambda: HadamardPower(A, B)) + A = MatrixSymbol('A', 3, 2) + raises(ShapeError, lambda: HadamardProduct(A, B)) + raises(ShapeError, lambda: HadamardPower(A, B)) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_indexing.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_indexing.py new file mode 100644 index 0000000000000000000000000000000000000000..500761f248eef5f627c2a7344a6817aca0b8a802 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_indexing.py @@ -0,0 +1,299 @@ +from sympy.concrete.summations import Sum +from sympy.core.symbol import symbols, Symbol, Dummy +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.matrices.dense import eye +from sympy.matrices.expressions.blockmatrix import BlockMatrix +from sympy.matrices.expressions.hadamard import HadamardPower +from sympy.matrices.expressions.matexpr import (MatrixSymbol, + MatrixExpr, MatrixElement) +from sympy.matrices.expressions.matpow import MatPow +from sympy.matrices.expressions.special import (ZeroMatrix, Identity, + OneMatrix) +from sympy.matrices.expressions.trace import Trace, trace +from sympy.matrices.immutable import ImmutableMatrix +from sympy.tensor.array.expressions.array_expressions import ArrayTensorProduct +from sympy.testing.pytest import XFAIL, raises + +k, l, m, n = symbols('k l m n', integer=True) +i, j = symbols('i j', integer=True) + +W = MatrixSymbol('W', k, l) +X = MatrixSymbol('X', l, m) +Y = MatrixSymbol('Y', l, m) +Z = MatrixSymbol('Z', m, n) + +X1 = MatrixSymbol('X1', m, m) +X2 = MatrixSymbol('X2', m, m) +X3 = MatrixSymbol('X3', m, m) +X4 = MatrixSymbol('X4', m, m) + +A = MatrixSymbol('A', 2, 2) +B = MatrixSymbol('B', 2, 2) +x = MatrixSymbol('x', 1, 2) +y = MatrixSymbol('x', 2, 1) + + +def test_symbolic_indexing(): + x12 = X[1, 2] + assert all(s in str(x12) for s in ['1', '2', X.name]) + # We don't care about the exact form of this. We do want to make sure + # that all of these features are present + + +def test_add_index(): + assert (X + Y)[i, j] == X[i, j] + Y[i, j] + + +def test_mul_index(): + assert (A*y)[0, 0] == A[0, 0]*y[0, 0] + A[0, 1]*y[1, 0] + assert (A*B).as_mutable() == (A.as_mutable() * B.as_mutable()) + X = MatrixSymbol('X', n, m) + Y = MatrixSymbol('Y', m, k) + + result = (X*Y)[4,2] + expected = Sum(X[4, i]*Y[i, 2], (i, 0, m - 1)) + assert result.args[0].dummy_eq(expected.args[0], i) + assert result.args[1][1:] == expected.args[1][1:] + + +def test_pow_index(): + Q = MatPow(A, 2) + assert Q[0, 0] == A[0, 0]**2 + A[0, 1]*A[1, 0] + n = symbols("n") + Q2 = A**n + assert Q2[0, 0] == 2*( + -sqrt((A[0, 0] + A[1, 1])**2 - 4*A[0, 0]*A[1, 1] + + 4*A[0, 1]*A[1, 0])/2 + A[0, 0]/2 + A[1, 1]/2 + )**n * \ + A[0, 1]*A[1, 0]/( + (sqrt(A[0, 0]**2 - 2*A[0, 0]*A[1, 1] + 4*A[0, 1]*A[1, 0] + + A[1, 1]**2) + A[0, 0] - A[1, 1])* + sqrt(A[0, 0]**2 - 2*A[0, 0]*A[1, 1] + 4*A[0, 1]*A[1, 0] + A[1, 1]**2) + ) - 2*( + sqrt((A[0, 0] + A[1, 1])**2 - 4*A[0, 0]*A[1, 1] + + 4*A[0, 1]*A[1, 0])/2 + A[0, 0]/2 + A[1, 1]/2 + )**n * A[0, 1]*A[1, 0]/( + (-sqrt(A[0, 0]**2 - 2*A[0, 0]*A[1, 1] + 4*A[0, 1]*A[1, 0] + + A[1, 1]**2) + A[0, 0] - A[1, 1])* + sqrt(A[0, 0]**2 - 2*A[0, 0]*A[1, 1] + 4*A[0, 1]*A[1, 0] + A[1, 1]**2) + ) + + +def test_transpose_index(): + assert X.T[i, j] == X[j, i] + + +def test_Identity_index(): + I = Identity(3) + assert I[0, 0] == I[1, 1] == I[2, 2] == 1 + assert I[1, 0] == I[0, 1] == I[2, 1] == 0 + assert I[i, 0].delta_range == (0, 2) + raises(IndexError, lambda: I[3, 3]) + + +def test_block_index(): + I = Identity(3) + Z = ZeroMatrix(3, 3) + B = BlockMatrix([[I, I], [I, I]]) + e3 = ImmutableMatrix(eye(3)) + BB = BlockMatrix([[e3, e3], [e3, e3]]) + assert B[0, 0] == B[3, 0] == B[0, 3] == B[3, 3] == 1 + assert B[4, 3] == B[5, 1] == 0 + + BB = BlockMatrix([[e3, e3], [e3, e3]]) + assert B.as_explicit() == BB.as_explicit() + + BI = BlockMatrix([[I, Z], [Z, I]]) + + assert BI.as_explicit().equals(eye(6)) + + +def test_block_index_symbolic(): + # Note that these matrices may be zero-sized and indices may be negative, which causes + # all naive simplifications given in the comments to be invalid + A1 = MatrixSymbol('A1', n, k) + A2 = MatrixSymbol('A2', n, l) + A3 = MatrixSymbol('A3', m, k) + A4 = MatrixSymbol('A4', m, l) + A = BlockMatrix([[A1, A2], [A3, A4]]) + assert A[0, 0] == MatrixElement(A, 0, 0) # Cannot be A1[0, 0] + assert A[n - 1, k - 1] == A1[n - 1, k - 1] + assert A[n, k] == A4[0, 0] + assert A[n + m - 1, 0] == MatrixElement(A, n + m - 1, 0) # Cannot be A3[m - 1, 0] + assert A[0, k + l - 1] == MatrixElement(A, 0, k + l - 1) # Cannot be A2[0, l - 1] + assert A[n + m - 1, k + l - 1] == MatrixElement(A, n + m - 1, k + l - 1) # Cannot be A4[m - 1, l - 1] + assert A[i, j] == MatrixElement(A, i, j) + assert A[n + i, k + j] == MatrixElement(A, n + i, k + j) # Cannot be A4[i, j] + assert A[n - i - 1, k - j - 1] == MatrixElement(A, n - i - 1, k - j - 1) # Cannot be A1[n - i - 1, k - j - 1] + + +def test_block_index_symbolic_nonzero(): + # All invalid simplifications from test_block_index_symbolic() that become valid if all + # matrices have nonzero size and all indices are nonnegative + k, l, m, n = symbols('k l m n', integer=True, positive=True) + i, j = symbols('i j', integer=True, nonnegative=True) + A1 = MatrixSymbol('A1', n, k) + A2 = MatrixSymbol('A2', n, l) + A3 = MatrixSymbol('A3', m, k) + A4 = MatrixSymbol('A4', m, l) + A = BlockMatrix([[A1, A2], [A3, A4]]) + assert A[0, 0] == A1[0, 0] + assert A[n + m - 1, 0] == A3[m - 1, 0] + assert A[0, k + l - 1] == A2[0, l - 1] + assert A[n + m - 1, k + l - 1] == A4[m - 1, l - 1] + assert A[i, j] == MatrixElement(A, i, j) + assert A[n + i, k + j] == A4[i, j] + assert A[n - i - 1, k - j - 1] == A1[n - i - 1, k - j - 1] + assert A[2 * n, 2 * k] == A4[n, k] + + +def test_block_index_large(): + n, m, k = symbols('n m k', integer=True, positive=True) + i = symbols('i', integer=True, nonnegative=True) + A1 = MatrixSymbol('A1', n, n) + A2 = MatrixSymbol('A2', n, m) + A3 = MatrixSymbol('A3', n, k) + A4 = MatrixSymbol('A4', m, n) + A5 = MatrixSymbol('A5', m, m) + A6 = MatrixSymbol('A6', m, k) + A7 = MatrixSymbol('A7', k, n) + A8 = MatrixSymbol('A8', k, m) + A9 = MatrixSymbol('A9', k, k) + A = BlockMatrix([[A1, A2, A3], [A4, A5, A6], [A7, A8, A9]]) + assert A[n + i, n + i] == MatrixElement(A, n + i, n + i) + + +@XFAIL +def test_block_index_symbolic_fail(): + # To make this work, symbolic matrix dimensions would need to be somehow assumed nonnegative + # even if the symbols aren't specified as such. Then 2 * n < n would correctly evaluate to + # False in BlockMatrix._entry() + A1 = MatrixSymbol('A1', n, 1) + A2 = MatrixSymbol('A2', m, 1) + A = BlockMatrix([[A1], [A2]]) + assert A[2 * n, 0] == A2[n, 0] + + +def test_slicing(): + A.as_explicit()[0, :] # does not raise an error + + +def test_errors(): + raises(IndexError, lambda: Identity(2)[1, 2, 3, 4, 5]) + raises(IndexError, lambda: Identity(2)[[1, 2, 3, 4, 5]]) + + +def test_matrix_expression_to_indices(): + i, j = symbols("i, j") + i1, i2, i3 = symbols("i_1:4") + + def replace_dummies(expr): + repl = {i: Symbol(i.name) for i in expr.atoms(Dummy)} + return expr.xreplace(repl) + + expr = W*X*Z + assert replace_dummies(expr._entry(i, j)) == \ + Sum(W[i, i1]*X[i1, i2]*Z[i2, j], (i1, 0, l-1), (i2, 0, m-1)) + assert MatrixExpr.from_index_summation(expr._entry(i, j)) == expr + + expr = Z.T*X.T*W.T + assert replace_dummies(expr._entry(i, j)) == \ + Sum(W[j, i2]*X[i2, i1]*Z[i1, i], (i1, 0, m-1), (i2, 0, l-1)) + assert MatrixExpr.from_index_summation(expr._entry(i, j), i) == expr + + expr = W*X*Z + W*Y*Z + assert replace_dummies(expr._entry(i, j)) == \ + Sum(W[i, i1]*X[i1, i2]*Z[i2, j], (i1, 0, l-1), (i2, 0, m-1)) +\ + Sum(W[i, i1]*Y[i1, i2]*Z[i2, j], (i1, 0, l-1), (i2, 0, m-1)) + assert MatrixExpr.from_index_summation(expr._entry(i, j)) == expr + + expr = 2*W*X*Z + 3*W*Y*Z + assert replace_dummies(expr._entry(i, j)) == \ + 2*Sum(W[i, i1]*X[i1, i2]*Z[i2, j], (i1, 0, l-1), (i2, 0, m-1)) +\ + 3*Sum(W[i, i1]*Y[i1, i2]*Z[i2, j], (i1, 0, l-1), (i2, 0, m-1)) + assert MatrixExpr.from_index_summation(expr._entry(i, j)) == expr + + expr = W*(X + Y)*Z + assert replace_dummies(expr._entry(i, j)) == \ + Sum(W[i, i1]*(X[i1, i2] + Y[i1, i2])*Z[i2, j], (i1, 0, l-1), (i2, 0, m-1)) + assert MatrixExpr.from_index_summation(expr._entry(i, j)) == expr + + expr = A*B**2*A + #assert replace_dummies(expr._entry(i, j)) == \ + # Sum(A[i, i1]*B[i1, i2]*B[i2, i3]*A[i3, j], (i1, 0, 1), (i2, 0, 1), (i3, 0, 1)) + + # Check that different dummies are used in sub-multiplications: + expr = (X1*X2 + X2*X1)*X3 + assert replace_dummies(expr._entry(i, j)) == \ + Sum((Sum(X1[i, i2] * X2[i2, i1], (i2, 0, m - 1)) + Sum(X1[i3, i1] * X2[i, i3], (i3, 0, m - 1))) * X3[ + i1, j], (i1, 0, m - 1)) + + +def test_matrix_expression_from_index_summation(): + from sympy.abc import a,b,c,d + A = MatrixSymbol("A", k, k) + B = MatrixSymbol("B", k, k) + C = MatrixSymbol("C", k, k) + w1 = MatrixSymbol("w1", k, 1) + + i0, i1, i2, i3, i4 = symbols("i0:5", cls=Dummy) + + expr = Sum(W[a,b]*X[b,c]*Z[c,d], (b, 0, l-1), (c, 0, m-1)) + assert MatrixExpr.from_index_summation(expr, a) == W*X*Z + expr = Sum(W.T[b,a]*X[b,c]*Z[c,d], (b, 0, l-1), (c, 0, m-1)) + assert MatrixExpr.from_index_summation(expr, a) == W*X*Z + expr = Sum(A[b, a]*B[b, c]*C[c, d], (b, 0, k-1), (c, 0, k-1)) + assert MatrixSymbol.from_index_summation(expr, a) == A.T*B*C + expr = Sum(A[b, a]*B[c, b]*C[c, d], (b, 0, k-1), (c, 0, k-1)) + assert MatrixSymbol.from_index_summation(expr, a) == A.T*B.T*C + expr = Sum(C[c, d]*A[b, a]*B[c, b], (b, 0, k-1), (c, 0, k-1)) + assert MatrixSymbol.from_index_summation(expr, a) == A.T*B.T*C + expr = Sum(A[a, b] + B[a, b], (a, 0, k-1), (b, 0, k-1)) + assert MatrixExpr.from_index_summation(expr, a) == OneMatrix(1, k)*A*OneMatrix(k, 1) + OneMatrix(1, k)*B*OneMatrix(k, 1) + expr = Sum(A[a, b]**2, (a, 0, k - 1), (b, 0, k - 1)) + assert MatrixExpr.from_index_summation(expr, a) == Trace(A * A.T) + expr = Sum(A[a, b]**3, (a, 0, k - 1), (b, 0, k - 1)) + assert MatrixExpr.from_index_summation(expr, a) == Trace(HadamardPower(A.T, 2) * A) + expr = Sum((A[a, b] + B[a, b])*C[b, c], (b, 0, k-1)) + assert MatrixExpr.from_index_summation(expr, a) == (A+B)*C + expr = Sum((A[a, b] + B[b, a])*C[b, c], (b, 0, k-1)) + assert MatrixExpr.from_index_summation(expr, a) == (A+B.T)*C + expr = Sum(A[a, b]*A[b, c]*A[c, d], (b, 0, k-1), (c, 0, k-1)) + assert MatrixExpr.from_index_summation(expr, a) == A**3 + expr = Sum(A[a, b]*A[b, c]*B[c, d], (b, 0, k-1), (c, 0, k-1)) + assert MatrixExpr.from_index_summation(expr, a) == A**2*B + + # Parse the trace of a matrix: + + expr = Sum(A[a, a], (a, 0, k-1)) + assert MatrixExpr.from_index_summation(expr, None) == trace(A) + expr = Sum(A[a, a]*B[b, c]*C[c, d], (a, 0, k-1), (c, 0, k-1)) + assert MatrixExpr.from_index_summation(expr, b) == trace(A)*B*C + + # Check wrong sum ranges (should raise an exception): + + ## Case 1: 0 to m instead of 0 to m-1 + expr = Sum(W[a,b]*X[b,c]*Z[c,d], (b, 0, l-1), (c, 0, m)) + raises(ValueError, lambda: MatrixExpr.from_index_summation(expr, a)) + ## Case 2: 1 to m-1 instead of 0 to m-1 + expr = Sum(W[a,b]*X[b,c]*Z[c,d], (b, 0, l-1), (c, 1, m-1)) + raises(ValueError, lambda: MatrixExpr.from_index_summation(expr, a)) + + # Parse nested sums: + expr = Sum(A[a, b]*Sum(B[b, c]*C[c, d], (c, 0, k-1)), (b, 0, k-1)) + assert MatrixExpr.from_index_summation(expr, a) == A*B*C + + # Test Kronecker delta: + expr = Sum(A[a, b]*KroneckerDelta(b, c)*B[c, d], (b, 0, k-1), (c, 0, k-1)) + assert MatrixExpr.from_index_summation(expr, a) == A*B + + expr = Sum(KroneckerDelta(i1, m)*KroneckerDelta(i2, n)*A[i, i1]*A[j, i2], (i1, 0, k-1), (i2, 0, k-1)) + assert MatrixExpr.from_index_summation(expr, m) == ArrayTensorProduct(A.T, A) + + # Test numbered indices: + expr = Sum(A[i1, i2]*w1[i2, 0], (i2, 0, k-1)) + assert MatrixExpr.from_index_summation(expr, i1) == MatrixElement(A*w1, i1, 0) + + expr = Sum(A[i1, i2]*B[i2, 0], (i2, 0, k-1)) + assert MatrixExpr.from_index_summation(expr, i1) == MatrixElement(A*B, i1, 0) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_inverse.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_inverse.py new file mode 100644 index 0000000000000000000000000000000000000000..4bcc7d4de2b2bee4c4922bda8bc48a52aa205961 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_inverse.py @@ -0,0 +1,69 @@ +from sympy.core import symbols, S +from sympy.matrices.expressions import MatrixSymbol, Inverse, MatPow, ZeroMatrix, OneMatrix +from sympy.matrices.exceptions import NonInvertibleMatrixError, NonSquareMatrixError +from sympy.matrices import eye, Identity +from sympy.testing.pytest import raises +from sympy.assumptions.ask import Q +from sympy.assumptions.refine import refine + +n, m, l = symbols('n m l', integer=True) +A = MatrixSymbol('A', n, m) +B = MatrixSymbol('B', m, l) +C = MatrixSymbol('C', n, n) +D = MatrixSymbol('D', n, n) +E = MatrixSymbol('E', m, n) + + +def test_inverse(): + assert Inverse(C).args == (C, S.NegativeOne) + assert Inverse(C).shape == (n, n) + assert Inverse(A*E).shape == (n, n) + assert Inverse(E*A).shape == (m, m) + assert Inverse(C).inverse() == C + assert Inverse(Inverse(C)).doit() == C + assert isinstance(Inverse(Inverse(C)), Inverse) + + assert Inverse(*Inverse(E*A).args) == Inverse(E*A) + + assert C.inverse().inverse() == C + + assert C.inverse()*C == Identity(C.rows) + + assert Identity(n).inverse() == Identity(n) + assert (3*Identity(n)).inverse() == Identity(n)/3 + + # Simplifies Muls if possible (i.e. submatrices are square) + assert (C*D).inverse() == D.I*C.I + # But still works when not possible + assert isinstance((A*E).inverse(), Inverse) + assert Inverse(C*D).doit(inv_expand=False) == Inverse(C*D) + + assert Inverse(eye(3)).doit() == eye(3) + assert Inverse(eye(3)).doit(deep=False) == eye(3) + + assert OneMatrix(1, 1).I == Identity(1) + assert isinstance(OneMatrix(n, n).I, Inverse) + +def test_inverse_non_invertible(): + raises(NonInvertibleMatrixError, lambda: ZeroMatrix(n, n).I) + raises(NonInvertibleMatrixError, lambda: OneMatrix(2, 2).I) + +def test_refine(): + assert refine(C.I, Q.orthogonal(C)) == C.T + + +def test_inverse_matpow_canonicalization(): + A = MatrixSymbol('A', 3, 3) + assert Inverse(MatPow(A, 3)).doit() == MatPow(Inverse(A), 3).doit() + + +def test_nonsquare_error(): + A = MatrixSymbol('A', 3, 4) + raises(NonSquareMatrixError, lambda: Inverse(A)) + + +def test_adjoint_trnaspose_conjugate(): + A = MatrixSymbol('A', n, n) + assert A.transpose().inverse() == A.inverse().transpose() + assert A.conjugate().inverse() == A.inverse().conjugate() + assert A.adjoint().inverse() == A.inverse().adjoint() diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_kronecker.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_kronecker.py new file mode 100644 index 0000000000000000000000000000000000000000..b4444716a76a52e3638dd7a36238a9f459179083 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_kronecker.py @@ -0,0 +1,150 @@ +from sympy.core.mod import Mod +from sympy.core.numbers import I +from sympy.core.symbol import symbols +from sympy.functions.elementary.integers import floor +from sympy.matrices.dense import (Matrix, eye) +from sympy.matrices import MatrixSymbol, Identity +from sympy.matrices.expressions import det, trace + +from sympy.matrices.expressions.kronecker import (KroneckerProduct, + kronecker_product, + combine_kronecker) + + +mat1 = Matrix([[1, 2 * I], [1 + I, 3]]) +mat2 = Matrix([[2 * I, 3], [4 * I, 2]]) + +i, j, k, n, m, o, p, x = symbols('i,j,k,n,m,o,p,x') +Z = MatrixSymbol('Z', n, n) +W = MatrixSymbol('W', m, m) +A = MatrixSymbol('A', n, m) +B = MatrixSymbol('B', n, m) +C = MatrixSymbol('C', m, k) + + +def test_KroneckerProduct(): + assert isinstance(KroneckerProduct(A, B), KroneckerProduct) + assert KroneckerProduct(A, B).subs(A, C) == KroneckerProduct(C, B) + assert KroneckerProduct(A, C).shape == (n*m, m*k) + assert (KroneckerProduct(A, C) + KroneckerProduct(-A, C)).is_ZeroMatrix + assert (KroneckerProduct(W, Z) * KroneckerProduct(W.I, Z.I)).is_Identity + + +def test_KroneckerProduct_identity(): + assert KroneckerProduct(Identity(m), Identity(n)) == Identity(m*n) + assert KroneckerProduct(eye(2), eye(3)) == eye(6) + + +def test_KroneckerProduct_explicit(): + X = MatrixSymbol('X', 2, 2) + Y = MatrixSymbol('Y', 2, 2) + kp = KroneckerProduct(X, Y) + assert kp.shape == (4, 4) + assert kp.as_explicit() == Matrix( + [ + [X[0, 0]*Y[0, 0], X[0, 0]*Y[0, 1], X[0, 1]*Y[0, 0], X[0, 1]*Y[0, 1]], + [X[0, 0]*Y[1, 0], X[0, 0]*Y[1, 1], X[0, 1]*Y[1, 0], X[0, 1]*Y[1, 1]], + [X[1, 0]*Y[0, 0], X[1, 0]*Y[0, 1], X[1, 1]*Y[0, 0], X[1, 1]*Y[0, 1]], + [X[1, 0]*Y[1, 0], X[1, 0]*Y[1, 1], X[1, 1]*Y[1, 0], X[1, 1]*Y[1, 1]] + ] + ) + + +def test_tensor_product_adjoint(): + assert KroneckerProduct(I*A, B).adjoint() == \ + -I*KroneckerProduct(A.adjoint(), B.adjoint()) + assert KroneckerProduct(mat1, mat2).adjoint() == \ + kronecker_product(mat1.adjoint(), mat2.adjoint()) + + +def test_tensor_product_conjugate(): + assert KroneckerProduct(I*A, B).conjugate() == \ + -I*KroneckerProduct(A.conjugate(), B.conjugate()) + assert KroneckerProduct(mat1, mat2).conjugate() == \ + kronecker_product(mat1.conjugate(), mat2.conjugate()) + + +def test_tensor_product_transpose(): + assert KroneckerProduct(I*A, B).transpose() == \ + I*KroneckerProduct(A.transpose(), B.transpose()) + assert KroneckerProduct(mat1, mat2).transpose() == \ + kronecker_product(mat1.transpose(), mat2.transpose()) + + +def test_KroneckerProduct_is_associative(): + assert kronecker_product(A, kronecker_product( + B, C)) == kronecker_product(kronecker_product(A, B), C) + assert kronecker_product(A, kronecker_product( + B, C)) == KroneckerProduct(A, B, C) + + +def test_KroneckerProduct_is_bilinear(): + assert kronecker_product(x*A, B) == x*kronecker_product(A, B) + assert kronecker_product(A, x*B) == x*kronecker_product(A, B) + + +def test_KroneckerProduct_determinant(): + kp = kronecker_product(W, Z) + assert det(kp) == det(W)**n * det(Z)**m + + +def test_KroneckerProduct_trace(): + kp = kronecker_product(W, Z) + assert trace(kp) == trace(W)*trace(Z) + + +def test_KroneckerProduct_isnt_commutative(): + assert KroneckerProduct(A, B) != KroneckerProduct(B, A) + assert KroneckerProduct(A, B).is_commutative is False + + +def test_KroneckerProduct_extracts_commutative_part(): + assert kronecker_product(x * A, 2 * B) == x * \ + 2 * KroneckerProduct(A, B) + + +def test_KroneckerProduct_inverse(): + kp = kronecker_product(W, Z) + assert kp.inverse() == kronecker_product(W.inverse(), Z.inverse()) + + +def test_KroneckerProduct_combine_add(): + kp1 = kronecker_product(A, B) + kp2 = kronecker_product(C, W) + assert combine_kronecker(kp1*kp2) == kronecker_product(A*C, B*W) + + +def test_KroneckerProduct_combine_mul(): + X = MatrixSymbol('X', m, n) + Y = MatrixSymbol('Y', m, n) + kp1 = kronecker_product(A, X) + kp2 = kronecker_product(B, Y) + assert combine_kronecker(kp1+kp2) == kronecker_product(A+B, X+Y) + + +def test_KroneckerProduct_combine_pow(): + X = MatrixSymbol('X', n, n) + Y = MatrixSymbol('Y', n, n) + assert combine_kronecker(KroneckerProduct( + X, Y)**x) == KroneckerProduct(X**x, Y**x) + assert combine_kronecker(x * KroneckerProduct(X, Y) + ** 2) == x * KroneckerProduct(X**2, Y**2) + assert combine_kronecker( + x * (KroneckerProduct(X, Y)**2) * KroneckerProduct(A, B)) == x * KroneckerProduct(X**2 * A, Y**2 * B) + # cannot simplify because of non-square arguments to kronecker product: + assert combine_kronecker(KroneckerProduct(A, B.T) ** m) == KroneckerProduct(A, B.T) ** m + + +def test_KroneckerProduct_expand(): + X = MatrixSymbol('X', n, n) + Y = MatrixSymbol('Y', n, n) + + assert KroneckerProduct(X + Y, Y + Z).expand(kroneckerproduct=True) == \ + KroneckerProduct(X, Y) + KroneckerProduct(X, Z) + \ + KroneckerProduct(Y, Y) + KroneckerProduct(Y, Z) + +def test_KroneckerProduct_entry(): + A = MatrixSymbol('A', n, m) + B = MatrixSymbol('B', o, p) + + assert KroneckerProduct(A, B)._entry(i, j) == A[Mod(floor(i/o), n), Mod(floor(j/p), m)]*B[Mod(i, o), Mod(j, p)] diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_matadd.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_matadd.py new file mode 100644 index 0000000000000000000000000000000000000000..43229ae8c2e42f0253a5f3eceefa5fffe7a99f29 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_matadd.py @@ -0,0 +1,58 @@ +from sympy.matrices.expressions import MatrixSymbol, MatAdd, MatPow, MatMul +from sympy.matrices.expressions.special import GenericZeroMatrix, ZeroMatrix +from sympy.matrices.exceptions import ShapeError +from sympy.matrices import eye, ImmutableMatrix +from sympy.core import Add, Basic, S +from sympy.core.add import add +from sympy.testing.pytest import XFAIL, raises + +X = MatrixSymbol('X', 2, 2) +Y = MatrixSymbol('Y', 2, 2) + +def test_evaluate(): + assert MatAdd(X, X, evaluate=True) == add(X, X, evaluate=True) == MatAdd(X, X).doit() + +def test_sort_key(): + assert MatAdd(Y, X).doit().args == add(Y, X).doit().args == (X, Y) + + +def test_matadd_sympify(): + assert isinstance(MatAdd(eye(1), eye(1)).args[0], Basic) + assert isinstance(add(eye(1), eye(1)).args[0], Basic) + + +def test_matadd_of_matrices(): + assert MatAdd(eye(2), 4*eye(2), eye(2)).doit() == ImmutableMatrix(6*eye(2)) + assert add(eye(2), 4*eye(2), eye(2)).doit() == ImmutableMatrix(6*eye(2)) + + +def test_doit_args(): + A = ImmutableMatrix([[1, 2], [3, 4]]) + B = ImmutableMatrix([[2, 3], [4, 5]]) + assert MatAdd(A, MatPow(B, 2)).doit() == A + B**2 + assert MatAdd(A, MatMul(A, B)).doit() == A + A*B + assert (MatAdd(A, X, MatMul(A, B), Y, MatAdd(2*A, B)).doit() == + add(A, X, MatMul(A, B), Y, add(2*A, B)).doit() == + MatAdd(3*A + A*B + B, X, Y)) + + +def test_generic_identity(): + assert MatAdd.identity == GenericZeroMatrix() + assert MatAdd.identity != S.Zero + + +def test_zero_matrix_add(): + assert Add(ZeroMatrix(2, 2), ZeroMatrix(2, 2)) == ZeroMatrix(2, 2) + +@XFAIL +def test_matrix_Add_with_scalar(): + raises(TypeError, lambda: Add(0, ZeroMatrix(2, 2))) + + +def test_shape_error(): + A = MatrixSymbol('A', 2, 3) + B = MatrixSymbol('B', 3, 3) + raises(ShapeError, lambda: MatAdd(A, B)) + + A = MatrixSymbol('A', 3, 2) + raises(ShapeError, lambda: MatAdd(A, B)) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_matexpr.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_matexpr.py new file mode 100644 index 0000000000000000000000000000000000000000..f2319e8d8097c2ad3519eab783c4665623c55b80 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_matexpr.py @@ -0,0 +1,592 @@ +from sympy.concrete.summations import Sum +from sympy.core.exprtools import gcd_terms +from sympy.core.function import (diff, expand) +from sympy.core.relational import Eq +from sympy.core.symbol import (Dummy, Symbol, Str) +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.matrices.dense import zeros +from sympy.polys.polytools import factor + +from sympy.core import (S, symbols, Add, Mul, SympifyError, Rational, + Function) +from sympy.functions import sin, cos, tan, sqrt, cbrt, exp +from sympy.simplify import simplify +from sympy.matrices import (ImmutableMatrix, Inverse, MatAdd, MatMul, + MatPow, Matrix, MatrixExpr, MatrixSymbol, + SparseMatrix, Transpose, Adjoint, MatrixSet) +from sympy.matrices.exceptions import NonSquareMatrixError +from sympy.matrices.expressions.determinant import Determinant, det +from sympy.matrices.expressions.matexpr import MatrixElement +from sympy.matrices.expressions.special import ZeroMatrix, Identity +from sympy.testing.pytest import raises, XFAIL, skip +from importlib.metadata import version + +n, m, l, k, p = symbols('n m l k p', integer=True) +x = symbols('x') +A = MatrixSymbol('A', n, m) +B = MatrixSymbol('B', m, l) +C = MatrixSymbol('C', n, n) +D = MatrixSymbol('D', n, n) +E = MatrixSymbol('E', m, n) +w = MatrixSymbol('w', n, 1) + + +def test_matrix_symbol_creation(): + assert MatrixSymbol('A', 2, 2) + assert MatrixSymbol('A', 0, 0) + raises(ValueError, lambda: MatrixSymbol('A', -1, 2)) + raises(ValueError, lambda: MatrixSymbol('A', 2.0, 2)) + raises(ValueError, lambda: MatrixSymbol('A', 2j, 2)) + raises(ValueError, lambda: MatrixSymbol('A', 2, -1)) + raises(ValueError, lambda: MatrixSymbol('A', 2, 2.0)) + raises(ValueError, lambda: MatrixSymbol('A', 2, 2j)) + + n = symbols('n') + assert MatrixSymbol('A', n, n) + n = symbols('n', integer=False) + raises(ValueError, lambda: MatrixSymbol('A', n, n)) + n = symbols('n', negative=True) + raises(ValueError, lambda: MatrixSymbol('A', n, n)) + + +def test_matexpr_properties(): + assert A.shape == (n, m) + assert (A * B).shape == (n, l) + assert A[0, 1].indices == (0, 1) + assert A[0, 0].symbol == A + assert A[0, 0].symbol.name == 'A' + + +def test_matexpr(): + assert (x*A).shape == A.shape + assert (x*A).__class__ == MatMul + assert 2*A - A - A == ZeroMatrix(*A.shape) + assert (A*B).shape == (n, l) + + +def test_matexpr_subs(): + A = MatrixSymbol('A', n, m) + B = MatrixSymbol('B', m, l) + C = MatrixSymbol('C', m, l) + + assert A.subs(n, m).shape == (m, m) + assert (A*B).subs(B, C) == A*C + assert (A*B).subs(l, n).is_square + + W = MatrixSymbol("W", 3, 3) + X = MatrixSymbol("X", 2, 2) + Y = MatrixSymbol("Y", 1, 2) + Z = MatrixSymbol("Z", n, 2) + # no restrictions on Symbol replacement + assert X.subs(X, Y) == Y + # it might be better to just change the name + y = Str('y') + assert X.subs(Str("X"), y).args == (y, 2, 2) + # it's ok to introduce a wider matrix + assert X[1, 1].subs(X, W) == W[1, 1] + # but for a given MatrixExpression, only change + # name if indexing on the new shape is valid. + # Here, X is 2,2; Y is 1,2 and Y[1, 1] is out + # of range so an error is raised + raises(IndexError, lambda: X[1, 1].subs(X, Y)) + # here, [0, 1] is in range so the subs succeeds + assert X[0, 1].subs(X, Y) == Y[0, 1] + # and here the size of n will accept any index + # in the first position + assert W[2, 1].subs(W, Z) == Z[2, 1] + # but not in the second position + raises(IndexError, lambda: W[2, 2].subs(W, Z)) + # any matrix should raise if invalid + raises(IndexError, lambda: W[2, 2].subs(W, zeros(2))) + + A = SparseMatrix([[1, 2], [3, 4]]) + B = Matrix([[1, 2], [3, 4]]) + C, D = MatrixSymbol('C', 2, 2), MatrixSymbol('D', 2, 2) + + assert (C*D).subs({C: A, D: B}) == MatMul(A, B) + + +def test_addition(): + A = MatrixSymbol('A', n, m) + B = MatrixSymbol('B', n, m) + + assert isinstance(A + B, MatAdd) + assert (A + B).shape == A.shape + assert isinstance(A - A + 2*B, MatMul) + + raises(TypeError, lambda: A + 1) + raises(TypeError, lambda: 5 + A) + raises(TypeError, lambda: 5 - A) + + assert A + ZeroMatrix(n, m) - A == ZeroMatrix(n, m) + raises(TypeError, lambda: ZeroMatrix(n, m) + S.Zero) + + +def test_multiplication(): + A = MatrixSymbol('A', n, m) + B = MatrixSymbol('B', m, l) + C = MatrixSymbol('C', n, n) + + assert (2*A*B).shape == (n, l) + assert (A*0*B) == ZeroMatrix(n, l) + assert (2*A).shape == A.shape + + assert A * ZeroMatrix(m, m) * B == ZeroMatrix(n, l) + + assert C * Identity(n) * C.I == Identity(n) + + assert B/2 == S.Half*B + raises(NotImplementedError, lambda: 2/B) + + A = MatrixSymbol('A', n, n) + B = MatrixSymbol('B', n, n) + assert Identity(n) * (A + B) == A + B + + assert A**2*A == A**3 + assert A**2*(A.I)**3 == A.I + assert A**3*(A.I)**2 == A + + +def test_MatPow(): + A = MatrixSymbol('A', n, n) + + AA = MatPow(A, 2) + assert AA.exp == 2 + assert AA.base == A + assert (A**n).exp == n + + assert A**0 == Identity(n) + assert A**1 == A + assert A**2 == AA + assert A**-1 == Inverse(A) + assert (A**-1)**-1 == A + assert (A**2)**3 == A**6 + assert A**S.Half == sqrt(A) + assert A**Rational(1, 3) == cbrt(A) + raises(NonSquareMatrixError, lambda: MatrixSymbol('B', 3, 2)**2) + + +def test_MatrixSymbol(): + n, m, t = symbols('n,m,t') + X = MatrixSymbol('X', n, m) + assert X.shape == (n, m) + raises(TypeError, lambda: MatrixSymbol('X', n, m)(t)) # issue 5855 + assert X.doit() == X + + +def test_dense_conversion(): + X = MatrixSymbol('X', 2, 2) + assert ImmutableMatrix(X) == ImmutableMatrix(2, 2, lambda i, j: X[i, j]) + assert Matrix(X) == Matrix(2, 2, lambda i, j: X[i, j]) + + +def test_free_symbols(): + assert (C*D).free_symbols == {C, D} + + +def test_zero_matmul(): + assert isinstance(S.Zero * MatrixSymbol('X', 2, 2), MatrixExpr) + + +def test_matadd_simplify(): + A = MatrixSymbol('A', 1, 1) + assert simplify(MatAdd(A, ImmutableMatrix([[sin(x)**2 + cos(x)**2]]))) == \ + MatAdd(A, Matrix([[1]])) + + +def test_matmul_simplify(): + A = MatrixSymbol('A', 1, 1) + assert simplify(MatMul(A, ImmutableMatrix([[sin(x)**2 + cos(x)**2]]))) == \ + MatMul(A, Matrix([[1]])) + + +def test_invariants(): + A = MatrixSymbol('A', n, m) + B = MatrixSymbol('B', m, l) + X = MatrixSymbol('X', n, n) + objs = [Identity(n), ZeroMatrix(m, n), A, MatMul(A, B), MatAdd(A, A), + Transpose(A), Adjoint(A), Inverse(X), MatPow(X, 2), MatPow(X, -1), + MatPow(X, 0)] + for obj in objs: + assert obj == obj.__class__(*obj.args) + + +def test_matexpr_indexing(): + A = MatrixSymbol('A', n, m) + A[1, 2] + A[l, k] + A[l + 1, k + 1] + A = MatrixSymbol('A', 2, 1) + for i in range(-2, 2): + for j in range(-1, 1): + A[i, j] + + +def test_single_indexing(): + A = MatrixSymbol('A', 2, 3) + assert A[1] == A[0, 1] + assert A[int(1)] == A[0, 1] + assert A[3] == A[1, 0] + assert list(A[:2, :2]) == [A[0, 0], A[0, 1], A[1, 0], A[1, 1]] + raises(IndexError, lambda: A[6]) + raises(IndexError, lambda: A[n]) + B = MatrixSymbol('B', n, m) + raises(IndexError, lambda: B[1]) + B = MatrixSymbol('B', n, 3) + assert B[3] == B[1, 0] + + +def test_MatrixElement_commutative(): + assert A[0, 1]*A[1, 0] == A[1, 0]*A[0, 1] + + +def test_MatrixSymbol_determinant(): + A = MatrixSymbol('A', 4, 4) + assert A.as_explicit().det() == A[0, 0]*A[1, 1]*A[2, 2]*A[3, 3] - \ + A[0, 0]*A[1, 1]*A[2, 3]*A[3, 2] - A[0, 0]*A[1, 2]*A[2, 1]*A[3, 3] + \ + A[0, 0]*A[1, 2]*A[2, 3]*A[3, 1] + A[0, 0]*A[1, 3]*A[2, 1]*A[3, 2] - \ + A[0, 0]*A[1, 3]*A[2, 2]*A[3, 1] - A[0, 1]*A[1, 0]*A[2, 2]*A[3, 3] + \ + A[0, 1]*A[1, 0]*A[2, 3]*A[3, 2] + A[0, 1]*A[1, 2]*A[2, 0]*A[3, 3] - \ + A[0, 1]*A[1, 2]*A[2, 3]*A[3, 0] - A[0, 1]*A[1, 3]*A[2, 0]*A[3, 2] + \ + A[0, 1]*A[1, 3]*A[2, 2]*A[3, 0] + A[0, 2]*A[1, 0]*A[2, 1]*A[3, 3] - \ + A[0, 2]*A[1, 0]*A[2, 3]*A[3, 1] - A[0, 2]*A[1, 1]*A[2, 0]*A[3, 3] + \ + A[0, 2]*A[1, 1]*A[2, 3]*A[3, 0] + A[0, 2]*A[1, 3]*A[2, 0]*A[3, 1] - \ + A[0, 2]*A[1, 3]*A[2, 1]*A[3, 0] - A[0, 3]*A[1, 0]*A[2, 1]*A[3, 2] + \ + A[0, 3]*A[1, 0]*A[2, 2]*A[3, 1] + A[0, 3]*A[1, 1]*A[2, 0]*A[3, 2] - \ + A[0, 3]*A[1, 1]*A[2, 2]*A[3, 0] - A[0, 3]*A[1, 2]*A[2, 0]*A[3, 1] + \ + A[0, 3]*A[1, 2]*A[2, 1]*A[3, 0] + + B = MatrixSymbol('B', 4, 4) + assert Determinant(A + B).doit() == det(A + B) == (A + B).det() + + +def test_MatrixElement_diff(): + assert (A[3, 0]*A[0, 0]).diff(A[0, 0]) == A[3, 0] + + +def test_MatrixElement_doit(): + u = MatrixSymbol('u', 2, 1) + v = ImmutableMatrix([3, 5]) + assert u[0, 0].subs(u, v).doit() == v[0, 0] + + +def test_identity_powers(): + M = Identity(n) + assert MatPow(M, 3).doit() == M**3 + assert M**n == M + assert MatPow(M, 0).doit() == M**2 + assert M**-2 == M + assert MatPow(M, -2).doit() == M**0 + N = Identity(3) + assert MatPow(N, 2).doit() == N**n + assert MatPow(N, 3).doit() == N + assert MatPow(N, -2).doit() == N**4 + assert MatPow(N, 2).doit() == N**0 + + +def test_Zero_power(): + z1 = ZeroMatrix(n, n) + assert z1**4 == z1 + raises(ValueError, lambda:z1**-2) + assert z1**0 == Identity(n) + assert MatPow(z1, 2).doit() == z1**2 + raises(ValueError, lambda:MatPow(z1, -2).doit()) + z2 = ZeroMatrix(3, 3) + assert MatPow(z2, 4).doit() == z2**4 + raises(ValueError, lambda:z2**-3) + assert z2**3 == MatPow(z2, 3).doit() + assert z2**0 == Identity(3) + raises(ValueError, lambda:MatPow(z2, -1).doit()) + + +def test_matrixelement_diff(): + dexpr = diff((D*w)[k,0], w[p,0]) + + assert w[k, p].diff(w[k, p]) == 1 + assert w[k, p].diff(w[0, 0]) == KroneckerDelta(0, k, (0, n-1))*KroneckerDelta(0, p, (0, 0)) + _i_1 = Dummy("_i_1") + assert dexpr.dummy_eq(Sum(KroneckerDelta(_i_1, p, (0, n-1))*D[k, _i_1], (_i_1, 0, n - 1))) + assert dexpr.doit() == D[k, p] + + +def test_MatrixElement_with_values(): + x, y, z, w = symbols("x y z w") + M = Matrix([[x, y], [z, w]]) + i, j = symbols("i, j") + Mij = M[i, j] + assert isinstance(Mij, MatrixElement) + Ms = SparseMatrix([[2, 3], [4, 5]]) + msij = Ms[i, j] + assert isinstance(msij, MatrixElement) + for oi, oj in [(0, 0), (0, 1), (1, 0), (1, 1)]: + assert Mij.subs({i: oi, j: oj}) == M[oi, oj] + assert msij.subs({i: oi, j: oj}) == Ms[oi, oj] + A = MatrixSymbol("A", 2, 2) + assert A[0, 0].subs(A, M) == x + assert A[i, j].subs(A, M) == M[i, j] + assert M[i, j].subs(M, A) == A[i, j] + + assert isinstance(M[3*i - 2, j], MatrixElement) + assert M[3*i - 2, j].subs({i: 1, j: 0}) == M[1, 0] + assert isinstance(M[i, 0], MatrixElement) + assert M[i, 0].subs(i, 0) == M[0, 0] + assert M[0, i].subs(i, 1) == M[0, 1] + + assert M[i, j].diff(x) == Matrix([[1, 0], [0, 0]])[i, j] + + raises(ValueError, lambda: M[i, 2]) + raises(ValueError, lambda: M[i, -1]) + raises(ValueError, lambda: M[2, i]) + raises(ValueError, lambda: M[-1, i]) + + +def test_inv(): + B = MatrixSymbol('B', 3, 3) + assert B.inv() == B**-1 + + # https://github.com/sympy/sympy/issues/19162 + X = MatrixSymbol('X', 1, 1).as_explicit() + assert X.inv() == Matrix([[1/X[0, 0]]]) + + X = MatrixSymbol('X', 2, 2).as_explicit() + detX = X[0, 0]*X[1, 1] - X[0, 1]*X[1, 0] + invX = Matrix([[ X[1, 1], -X[0, 1]], + [-X[1, 0], X[0, 0]]]) / detX + assert X.inv() == invX + + +@XFAIL +def test_factor_expand(): + A = MatrixSymbol("A", n, n) + B = MatrixSymbol("B", n, n) + expr1 = (A + B)*(C + D) + expr2 = A*C + B*C + A*D + B*D + assert expr1 != expr2 + assert expand(expr1) == expr2 + assert factor(expr2) == expr1 + + expr = B**(-1)*(A**(-1)*B**(-1) - A**(-1)*C*B**(-1))**(-1)*A**(-1) + I = Identity(n) + # Ideally we get the first, but we at least don't want a wrong answer + assert factor(expr) in [I - C, B**-1*(A**-1*(I - C)*B**-1)**-1*A**-1] + +def test_numpy_conversion(): + try: + from numpy import array, array_equal + except ImportError: + skip('NumPy must be available to test creating matrices from ndarrays') + A = MatrixSymbol('A', 2, 2) + np_array = array([[MatrixElement(A, 0, 0), MatrixElement(A, 0, 1)], + [MatrixElement(A, 1, 0), MatrixElement(A, 1, 1)]]) + assert array_equal(array(A), np_array) + assert array_equal(array(A, copy=True), np_array) + if(int(version('numpy').split('.')[0]) >= 2): #run this test only if numpy is new enough that copy variable is passed properly. + raises(TypeError, lambda: array(A, copy=False)) + +def test_issue_2749(): + A = MatrixSymbol("A", 5, 2) + assert (A.T * A).I.as_explicit() == Matrix([[(A.T * A).I[0, 0], (A.T * A).I[0, 1]], \ + [(A.T * A).I[1, 0], (A.T * A).I[1, 1]]]) + + +def test_issue_2750(): + x = MatrixSymbol('x', 1, 1) + assert (x.T*x).as_explicit()**-1 == Matrix([[x[0, 0]**(-2)]]) + + +def test_issue_7842(): + A = MatrixSymbol('A', 3, 1) + B = MatrixSymbol('B', 2, 1) + assert Eq(A, B) == False + assert Eq(A[1,0], B[1, 0]).func is Eq + A = ZeroMatrix(2, 3) + B = ZeroMatrix(2, 3) + assert Eq(A, B) == True + + +def test_issue_21195(): + t = symbols('t') + x = Function('x')(t) + dx = x.diff(t) + exp1 = cos(x) + cos(x)*dx + exp2 = sin(x) + tan(x)*(dx.diff(t)) + exp3 = sin(x)*sin(t)*(dx.diff(t)).diff(t) + A = Matrix([[exp1], [exp2], [exp3]]) + B = Matrix([[exp1.diff(x)], [exp2.diff(x)], [exp3.diff(x)]]) + assert A.diff(x) == B + + +def test_issue_24859(): + A = MatrixSymbol('A', 2, 3) + B = MatrixSymbol('B', 3, 2) + J = A*B + Jinv = Matrix(J).adjugate() + u = MatrixSymbol('u', 2, 3) + Jk = Jinv.subs(A, A + x*u) + + expected = B[0, 1]*u[1, 0] + B[1, 1]*u[1, 1] + B[2, 1]*u[1, 2] + assert Jk[0, 0].diff(x) == expected + assert diff(Jk[0, 0], x).doit() == expected + + +def test_MatMul_postprocessor(): + z = zeros(2) + z1 = ZeroMatrix(2, 2) + assert Mul(0, z) == Mul(z, 0) in [z, z1] + + M = Matrix([[1, 2], [3, 4]]) + Mx = Matrix([[x, 2*x], [3*x, 4*x]]) + assert Mul(x, M) == Mul(M, x) == Mx + + A = MatrixSymbol("A", 2, 2) + assert Mul(A, M) == MatMul(A, M) + assert Mul(M, A) == MatMul(M, A) + # Scalars should be absorbed into constant matrices + a = Mul(x, M, A) + b = Mul(M, x, A) + c = Mul(M, A, x) + assert a == b == c == MatMul(Mx, A) + a = Mul(x, A, M) + b = Mul(A, x, M) + c = Mul(A, M, x) + assert a == b == c == MatMul(A, Mx) + assert Mul(M, M) == M**2 + assert Mul(A, M, M) == MatMul(A, M**2) + assert Mul(M, M, A) == MatMul(M**2, A) + assert Mul(M, A, M) == MatMul(M, A, M) + + assert Mul(A, x, M, M, x) == MatMul(A, Mx**2) + + +@XFAIL +def test_MatAdd_postprocessor_xfail(): + # This is difficult to get working because of the way that Add processes + # its args. + z = zeros(2) + assert Add(z, S.NaN) == Add(S.NaN, z) + + +def test_MatAdd_postprocessor(): + # Some of these are nonsensical, but we do not raise errors for Add + # because that breaks algorithms that want to replace matrices with dummy + # symbols. + + z = zeros(2) + + assert Add(0, z) == Add(z, 0) == z + + a = Add(S.Infinity, z) + assert a == Add(z, S.Infinity) + assert isinstance(a, Add) + assert a.args == (S.Infinity, z) + + a = Add(S.ComplexInfinity, z) + assert a == Add(z, S.ComplexInfinity) + assert isinstance(a, Add) + assert a.args == (S.ComplexInfinity, z) + + a = Add(z, S.NaN) + # assert a == Add(S.NaN, z) # See the XFAIL above + assert isinstance(a, Add) + assert a.args == (S.NaN, z) + + M = Matrix([[1, 2], [3, 4]]) + a = Add(x, M) + assert a == Add(M, x) + assert isinstance(a, Add) + assert a.args == (x, M) + + A = MatrixSymbol("A", 2, 2) + assert Add(A, M) == Add(M, A) == A + M + + # Scalars should be absorbed into constant matrices (producing an error) + a = Add(x, M, A) + assert a == Add(M, x, A) == Add(M, A, x) == Add(x, A, M) == Add(A, x, M) == Add(A, M, x) + assert isinstance(a, Add) + assert a.args == (x, A + M) + + assert Add(M, M) == 2*M + assert Add(M, A, M) == Add(M, M, A) == Add(A, M, M) == A + 2*M + + a = Add(A, x, M, M, x) + assert isinstance(a, Add) + assert a.args == (2*x, A + 2*M) + + +def test_simplify_matrix_expressions(): + # Various simplification functions + assert type(gcd_terms(C*D + D*C)) == MatAdd + a = gcd_terms(2*C*D + 4*D*C) + assert type(a) == MatAdd + assert a.args == (2*C*D, 4*D*C) + + +def test_exp(): + A = MatrixSymbol('A', 2, 2) + B = MatrixSymbol('B', 2, 2) + expr1 = exp(A)*exp(B) + expr2 = exp(B)*exp(A) + assert expr1 != expr2 + assert expr1 - expr2 != 0 + assert not isinstance(expr1, exp) + assert not isinstance(expr2, exp) + + +def test_invalid_args(): + raises(SympifyError, lambda: MatrixSymbol(1, 2, 'A')) + + +def test_matrixsymbol_from_symbol(): + # The label should be preserved during doit and subs + A_label = Symbol('A', complex=True) + A = MatrixSymbol(A_label, 2, 2) + + A_1 = A.doit() + A_2 = A.subs(2, 3) + assert A_1.args == A.args + assert A_2.args[0] == A.args[0] + + +def test_as_explicit(): + Z = MatrixSymbol('Z', 2, 3) + assert Z.as_explicit() == ImmutableMatrix([ + [Z[0, 0], Z[0, 1], Z[0, 2]], + [Z[1, 0], Z[1, 1], Z[1, 2]], + ]) + raises(ValueError, lambda: A.as_explicit()) + + +def test_MatrixSet(): + M = MatrixSet(2, 2, set=S.Reals) + assert M.shape == (2, 2) + assert M.set == S.Reals + X = Matrix([[1, 2], [3, 4]]) + assert X in M + X = ZeroMatrix(2, 2) + assert X in M + raises(TypeError, lambda: A in M) + raises(TypeError, lambda: 1 in M) + M = MatrixSet(n, m, set=S.Reals) + assert A in M + raises(TypeError, lambda: C in M) + raises(TypeError, lambda: X in M) + M = MatrixSet(2, 2, set={1, 2, 3}) + X = Matrix([[1, 2], [3, 4]]) + Y = Matrix([[1, 2]]) + assert (X in M) == S.false + assert (Y in M) == S.false + raises(ValueError, lambda: MatrixSet(2, -2, S.Reals)) + raises(ValueError, lambda: MatrixSet(2.4, -1, S.Reals)) + raises(TypeError, lambda: MatrixSet(2, 2, (1, 2, 3))) + + +def test_matrixsymbol_solving(): + A = MatrixSymbol('A', 2, 2) + B = MatrixSymbol('B', 2, 2) + Z = ZeroMatrix(2, 2) + assert -(-A + B) - A + B == Z + assert (-(-A + B) - A + B).simplify() == Z + assert (-(-A + B) - A + B).expand() == Z + assert (-(-A + B) - A + B - Z).simplify() == Z + assert (-(-A + B) - A + B - Z).expand() == Z + assert (A*(A + B) + B*(A.T + B.T)).expand() == A**2 + A*B + B*A.T + B*B.T diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_matmul.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_matmul.py new file mode 100644 index 0000000000000000000000000000000000000000..813926e2c83e27716f4f894ebebd09b2a576f046 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_matmul.py @@ -0,0 +1,193 @@ +from sympy.core import I, symbols, Basic, Mul, S +from sympy.core.mul import mul +from sympy.functions import adjoint, transpose +from sympy.matrices.exceptions import ShapeError +from sympy.matrices import (Identity, Inverse, Matrix, MatrixSymbol, ZeroMatrix, + eye, ImmutableMatrix) +from sympy.matrices.expressions import Adjoint, Transpose, det, MatPow +from sympy.matrices.expressions.special import GenericIdentity +from sympy.matrices.expressions.matmul import (factor_in_front, remove_ids, + MatMul, combine_powers, any_zeros, unpack, only_squares) +from sympy.strategies import null_safe +from sympy.assumptions.ask import Q +from sympy.assumptions.refine import refine +from sympy.core.symbol import Symbol + +from sympy.testing.pytest import XFAIL, raises + +n, m, l, k = symbols('n m l k', integer=True) +x = symbols('x') +A = MatrixSymbol('A', n, m) +B = MatrixSymbol('B', m, l) +C = MatrixSymbol('C', n, n) +D = MatrixSymbol('D', n, n) +E = MatrixSymbol('E', m, n) + +def test_evaluate(): + assert MatMul(C, C, evaluate=True) == MatMul(C, C).doit() + +def test_adjoint(): + assert adjoint(A*B) == Adjoint(B)*Adjoint(A) + assert adjoint(2*A*B) == 2*Adjoint(B)*Adjoint(A) + assert adjoint(2*I*C) == -2*I*Adjoint(C) + + M = Matrix(2, 2, [1, 2 + I, 3, 4]) + MA = Matrix(2, 2, [1, 3, 2 - I, 4]) + assert adjoint(M) == MA + assert adjoint(2*M) == 2*MA + assert adjoint(MatMul(2, M)) == MatMul(2, MA).doit() + + +def test_transpose(): + assert transpose(A*B) == Transpose(B)*Transpose(A) + assert transpose(2*A*B) == 2*Transpose(B)*Transpose(A) + assert transpose(2*I*C) == 2*I*Transpose(C) + + M = Matrix(2, 2, [1, 2 + I, 3, 4]) + MT = Matrix(2, 2, [1, 3, 2 + I, 4]) + assert transpose(M) == MT + assert transpose(2*M) == 2*MT + assert transpose(x*M) == x*MT + assert transpose(MatMul(2, M)) == MatMul(2, MT).doit() + + +def test_factor_in_front(): + assert factor_in_front(MatMul(A, 2, B, evaluate=False)) ==\ + MatMul(2, A, B, evaluate=False) + + +def test_remove_ids(): + assert remove_ids(MatMul(A, Identity(m), B, evaluate=False)) == \ + MatMul(A, B, evaluate=False) + assert null_safe(remove_ids)(MatMul(Identity(n), evaluate=False)) == \ + MatMul(Identity(n), evaluate=False) + + +def test_combine_powers(): + assert combine_powers(MatMul(D, Inverse(D), D, evaluate=False)) == \ + MatMul(Identity(n), D, evaluate=False) + assert combine_powers(MatMul(B.T, Inverse(E*A), E, A, B, evaluate=False)) == \ + MatMul(B.T, Identity(m), B, evaluate=False) + assert combine_powers(MatMul(A, E, Inverse(A*E), D, evaluate=False)) == \ + MatMul(Identity(n), D, evaluate=False) + + +def test_any_zeros(): + assert any_zeros(MatMul(A, ZeroMatrix(m, k), evaluate=False)) == \ + ZeroMatrix(n, k) + + +def test_unpack(): + assert unpack(MatMul(A, evaluate=False)) == A + x = MatMul(A, B) + assert unpack(x) == x + + +def test_only_squares(): + assert only_squares(C) == [C] + assert only_squares(C, D) == [C, D] + assert only_squares(C, A, A.T, D) == [C, A*A.T, D] + + +def test_determinant(): + assert det(2*C) == 2**n*det(C) + assert det(2*C*D) == 2**n*det(C)*det(D) + assert det(3*C*A*A.T*D) == 3**n*det(C)*det(A*A.T)*det(D) + + +def test_doit(): + assert MatMul(C, 2, D).args == (C, 2, D) + assert MatMul(C, 2, D).doit().args == (2, C, D) + assert MatMul(C, Transpose(D*C)).args == (C, Transpose(D*C)) + assert MatMul(C, Transpose(D*C)).doit(deep=True).args == (C, C.T, D.T) + + +def test_doit_drills_down(): + X = ImmutableMatrix([[1, 2], [3, 4]]) + Y = ImmutableMatrix([[2, 3], [4, 5]]) + assert MatMul(X, MatPow(Y, 2)).doit() == X*Y**2 + assert MatMul(C, Transpose(D*C)).doit().args == (C, C.T, D.T) + + +def test_doit_deep_false_still_canonical(): + assert (MatMul(C, Transpose(D*C), 2).doit(deep=False).args == + (2, C, Transpose(D*C))) + + +def test_matmul_scalar_Matrix_doit(): + # Issue 9053 + X = Matrix([[1, 2], [3, 4]]) + assert MatMul(2, X).doit() == 2*X + + +def test_matmul_sympify(): + assert isinstance(MatMul(eye(1), eye(1)).args[0], Basic) + + +def test_collapse_MatrixBase(): + A = Matrix([[1, 1], [1, 1]]) + B = Matrix([[1, 2], [3, 4]]) + assert MatMul(A, B).doit() == ImmutableMatrix([[4, 6], [4, 6]]) + + +def test_refine(): + assert refine(C*C.T*D, Q.orthogonal(C)).doit() == D + + kC = k*C + assert refine(kC*C.T, Q.orthogonal(C)).doit() == k*Identity(n) + assert refine(kC* kC.T, Q.orthogonal(C)).doit() == (k**2)*Identity(n) + +def test_matmul_no_matrices(): + assert MatMul(1) == 1 + assert MatMul(n, m) == n*m + assert not isinstance(MatMul(n, m), MatMul) + +def test_matmul_args_cnc(): + assert MatMul(n, A, A.T).args_cnc() == [[n], [A, A.T]] + assert MatMul(A, A.T).args_cnc() == [[], [A, A.T]] + +@XFAIL +def test_matmul_args_cnc_symbols(): + # Not currently supported + a, b = symbols('a b', commutative=False) + assert MatMul(n, a, b, A, A.T).args_cnc() == [[n], [a, b, A, A.T]] + assert MatMul(n, a, A, b, A.T).args_cnc() == [[n], [a, A, b, A.T]] + +def test_issue_12950(): + M = Matrix([[Symbol("x")]]) * MatrixSymbol("A", 1, 1) + assert MatrixSymbol("A", 1, 1).as_explicit()[0]*Symbol('x') == M.as_explicit()[0] + +def test_construction_with_Mul(): + assert Mul(C, D) == MatMul(C, D) + assert Mul(D, C) == MatMul(D, C) + +def test_construction_with_mul(): + assert mul(C, D) == MatMul(C, D) + assert mul(D, C) == MatMul(D, C) + assert mul(C, D) != MatMul(D, C) + +def test_generic_identity(): + assert MatMul.identity == GenericIdentity() + assert MatMul.identity != S.One + + +def test_issue_23519(): + N = Symbol("N", integer=True) + M1 = MatrixSymbol("M1", N, N) + M2 = MatrixSymbol("M2", N, N) + I = Identity(N) + z = (M2 + 2 * (M2 + I) * M1 + I) + assert z.coeff(M1) == 2*I + 2*M2 + + +def test_shape_error(): + A = MatrixSymbol('A', 2, 2) + B = MatrixSymbol('B', 3, 3) + raises(ShapeError, lambda: MatMul(A, B)) + + +def test_matmul_transpose(): + # https://github.com/sympy/sympy/issues/9503 + M = Matrix(2, 2, [1, 2 + I, 3, 4]) + a = Symbol('a') + assert (MatMul(a, M).T).expand() == (a*Matrix([[1, 3],[2 + I, 4]])).expand() diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_matpow.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_matpow.py new file mode 100644 index 0000000000000000000000000000000000000000..2afb5fdc2aa652c321de52aba43db63da60941fd --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_matpow.py @@ -0,0 +1,217 @@ +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.simplify.powsimp import powsimp +from sympy.testing.pytest import raises +from sympy.core.expr import unchanged +from sympy.core import symbols, S +from sympy.matrices import Identity, MatrixSymbol, ImmutableMatrix, ZeroMatrix, OneMatrix, Matrix +from sympy.matrices.exceptions import NonSquareMatrixError +from sympy.matrices.expressions import MatPow, MatAdd, MatMul +from sympy.matrices.expressions.inverse import Inverse +from sympy.matrices.expressions.matexpr import MatrixElement + +n, m, l, k = symbols('n m l k', integer=True) +A = MatrixSymbol('A', n, m) +B = MatrixSymbol('B', m, l) +C = MatrixSymbol('C', n, n) +D = MatrixSymbol('D', n, n) +E = MatrixSymbol('E', m, n) + + +def test_entry_matrix(): + X = ImmutableMatrix([[1, 2], [3, 4]]) + assert MatPow(X, 0)[0, 0] == 1 + assert MatPow(X, 0)[0, 1] == 0 + assert MatPow(X, 1)[0, 0] == 1 + assert MatPow(X, 1)[0, 1] == 2 + assert MatPow(X, 2)[0, 0] == 7 + + +def test_entry_symbol(): + from sympy.concrete import Sum + assert MatPow(C, 0)[0, 0] == 1 + assert MatPow(C, 0)[0, 1] == 0 + assert MatPow(C, 1)[0, 0] == C[0, 0] + assert isinstance(MatPow(C, 2)[0, 0], Sum) + assert isinstance(MatPow(C, n)[0, 0], MatrixElement) + + +def test_as_explicit_symbol(): + X = MatrixSymbol('X', 2, 2) + assert MatPow(X, 0).as_explicit() == ImmutableMatrix(Identity(2)) + assert MatPow(X, 1).as_explicit() == X.as_explicit() + assert MatPow(X, 2).as_explicit() == (X.as_explicit())**2 + assert MatPow(X, n).as_explicit() == ImmutableMatrix([ + [(X ** n)[0, 0], (X ** n)[0, 1]], + [(X ** n)[1, 0], (X ** n)[1, 1]], + ]) + + a = MatrixSymbol("a", 3, 1) + b = MatrixSymbol("b", 3, 1) + c = MatrixSymbol("c", 3, 1) + + expr = (a.T*b)**S.Half + assert expr.as_explicit() == Matrix([[sqrt(a[0, 0]*b[0, 0] + a[1, 0]*b[1, 0] + a[2, 0]*b[2, 0])]]) + + expr = c*(a.T*b)**S.Half + m = sqrt(a[0, 0]*b[0, 0] + a[1, 0]*b[1, 0] + a[2, 0]*b[2, 0]) + assert expr.as_explicit() == Matrix([[c[0, 0]*m], [c[1, 0]*m], [c[2, 0]*m]]) + + expr = (a*b.T)**S.Half + denom = sqrt(a[0, 0]*b[0, 0] + a[1, 0]*b[1, 0] + a[2, 0]*b[2, 0]) + expected = (a*b.T).as_explicit()/denom + assert expr.as_explicit() == expected + + expr = X**-1 + det = X[0, 0]*X[1, 1] - X[1, 0]*X[0, 1] + expected = Matrix([[X[1, 1], -X[0, 1]], [-X[1, 0], X[0, 0]]])/det + assert expr.as_explicit() == expected + + expr = X**m + assert expr.as_explicit() == X.as_explicit()**m + + +def test_as_explicit_matrix(): + A = ImmutableMatrix([[1, 2], [3, 4]]) + assert MatPow(A, 0).as_explicit() == ImmutableMatrix(Identity(2)) + assert MatPow(A, 1).as_explicit() == A + assert MatPow(A, 2).as_explicit() == A**2 + assert MatPow(A, -1).as_explicit() == A.inv() + assert MatPow(A, -2).as_explicit() == (A.inv())**2 + # less expensive than testing on a 2x2 + A = ImmutableMatrix([4]) + assert MatPow(A, S.Half).as_explicit() == A**S.Half + + +def test_doit_symbol(): + assert MatPow(C, 0).doit() == Identity(n) + assert MatPow(C, 1).doit() == C + assert MatPow(C, -1).doit() == C.I + for r in [2, S.Half, S.Pi, n]: + assert MatPow(C, r).doit() == MatPow(C, r) + + +def test_doit_matrix(): + X = ImmutableMatrix([[1, 2], [3, 4]]) + assert MatPow(X, 0).doit() == ImmutableMatrix(Identity(2)) + assert MatPow(X, 1).doit() == X + assert MatPow(X, 2).doit() == X**2 + assert MatPow(X, -1).doit() == X.inv() + assert MatPow(X, -2).doit() == (X.inv())**2 + # less expensive than testing on a 2x2 + assert MatPow(ImmutableMatrix([4]), S.Half).doit() == ImmutableMatrix([2]) + X = ImmutableMatrix([[0, 2], [0, 4]]) # det() == 0 + raises(ValueError, lambda: MatPow(X,-1).doit()) + raises(ValueError, lambda: MatPow(X,-2).doit()) + + +def test_nonsquare(): + A = MatrixSymbol('A', 2, 3) + B = ImmutableMatrix([[1, 2, 3], [4, 5, 6]]) + for r in [-1, 0, 1, 2, S.Half, S.Pi, n]: + raises(NonSquareMatrixError, lambda: MatPow(A, r)) + raises(NonSquareMatrixError, lambda: MatPow(B, r)) + + +def test_doit_equals_pow(): #17179 + X = ImmutableMatrix ([[1,0],[0,1]]) + assert MatPow(X, n).doit() == X**n == X + + +def test_doit_nested_MatrixExpr(): + X = ImmutableMatrix([[1, 2], [3, 4]]) + Y = ImmutableMatrix([[2, 3], [4, 5]]) + assert MatPow(MatMul(X, Y), 2).doit() == (X*Y)**2 + assert MatPow(MatAdd(X, Y), 2).doit() == (X + Y)**2 + + +def test_identity_power(): + k = Identity(n) + assert MatPow(k, 4).doit() == k + assert MatPow(k, n).doit() == k + assert MatPow(k, -3).doit() == k + assert MatPow(k, 0).doit() == k + l = Identity(3) + assert MatPow(l, n).doit() == l + assert MatPow(l, -1).doit() == l + assert MatPow(l, 0).doit() == l + + +def test_zero_power(): + z1 = ZeroMatrix(n, n) + assert MatPow(z1, 3).doit() == z1 + raises(ValueError, lambda:MatPow(z1, -1).doit()) + assert MatPow(z1, 0).doit() == Identity(n) + assert MatPow(z1, n).doit() == z1 + raises(ValueError, lambda:MatPow(z1, -2).doit()) + z2 = ZeroMatrix(4, 4) + assert MatPow(z2, n).doit() == z2 + raises(ValueError, lambda:MatPow(z2, -3).doit()) + assert MatPow(z2, 2).doit() == z2 + assert MatPow(z2, 0).doit() == Identity(4) + raises(ValueError, lambda:MatPow(z2, -1).doit()) + + +def test_OneMatrix_power(): + o = OneMatrix(3, 3) + assert o ** 0 == Identity(3) + assert o ** 1 == o + assert o * o == o ** 2 == 3 * o + assert o * o * o == o ** 3 == 9 * o + + o = OneMatrix(n, n) + assert o * o == o ** 2 == n * o + # powsimp necessary as n ** (n - 2) * n does not produce n ** (n - 1) + assert powsimp(o ** (n - 1) * o) == o ** n == n ** (n - 1) * o + + +def test_transpose_power(): + from sympy.matrices.expressions.transpose import Transpose as TP + + assert (C*D).T**5 == ((C*D)**5).T == (D.T * C.T)**5 + assert ((C*D).T**5).T == (C*D)**5 + + assert (C.T.I.T)**7 == C**-7 + assert (C.T**l).T**k == C**(l*k) + + assert ((E.T * A.T)**5).T == (A*E)**5 + assert ((A*E).T**5).T**7 == (A*E)**35 + assert TP(TP(C**2 * D**3)**5).doit() == (C**2 * D**3)**5 + + assert ((D*C)**-5).T**-5 == ((D*C)**25).T + assert (((D*C)**l).T**k).T == (D*C)**(l*k) + + +def test_Inverse(): + assert Inverse(MatPow(C, 0)).doit() == Identity(n) + assert Inverse(MatPow(C, 1)).doit() == Inverse(C) + assert Inverse(MatPow(C, 2)).doit() == MatPow(C, -2) + assert Inverse(MatPow(C, -1)).doit() == C + + assert MatPow(Inverse(C), 0).doit() == Identity(n) + assert MatPow(Inverse(C), 1).doit() == Inverse(C) + assert MatPow(Inverse(C), 2).doit() == MatPow(C, -2) + assert MatPow(Inverse(C), -1).doit() == C + + +def test_combine_powers(): + assert (C ** 1) ** 1 == C + assert (C ** 2) ** 3 == MatPow(C, 6) + assert (C ** -2) ** -3 == MatPow(C, 6) + assert (C ** -1) ** -1 == C + assert (((C ** 2) ** 3) ** 4) ** 5 == MatPow(C, 120) + assert (C ** n) ** n == C ** (n ** 2) + + +def test_unchanged(): + assert unchanged(MatPow, C, 0) + assert unchanged(MatPow, C, 1) + assert unchanged(MatPow, Inverse(C), -1) + assert unchanged(Inverse, MatPow(C, -1), -1) + assert unchanged(MatPow, MatPow(C, -1), -1) + assert unchanged(MatPow, MatPow(C, 1), 1) + + +def test_no_exponentiation(): + # if this passes, Pow.as_numer_denom should recognize + # MatAdd as exponent + raises(NotImplementedError, lambda: 3**(-2*C)) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_permutation.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_permutation.py new file mode 100644 index 0000000000000000000000000000000000000000..41a924f6636afb2e5b6560987e38a0fa0c861f1e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_permutation.py @@ -0,0 +1,166 @@ +from sympy.combinatorics import Permutation +from sympy.core.expr import unchanged +from sympy.matrices import Matrix +from sympy.matrices.expressions import \ + MatMul, BlockDiagMatrix, Determinant, Inverse +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.special import ZeroMatrix, OneMatrix, Identity +from sympy.matrices.expressions.permutation import \ + MatrixPermute, PermutationMatrix +from sympy.testing.pytest import raises +from sympy.core.symbol import Symbol + + +def test_PermutationMatrix_basic(): + p = Permutation([1, 0]) + assert unchanged(PermutationMatrix, p) + raises(ValueError, lambda: PermutationMatrix((0, 1, 2))) + assert PermutationMatrix(p).as_explicit() == Matrix([[0, 1], [1, 0]]) + assert isinstance(PermutationMatrix(p)*MatrixSymbol('A', 2, 2), MatMul) + + +def test_PermutationMatrix_matmul(): + p = Permutation([1, 2, 0]) + P = PermutationMatrix(p) + M = Matrix([[0, 1, 2], [3, 4, 5], [6, 7, 8]]) + assert (P*M).as_explicit() == P.as_explicit()*M + assert (M*P).as_explicit() == M*P.as_explicit() + + P1 = PermutationMatrix(Permutation([1, 2, 0])) + P2 = PermutationMatrix(Permutation([2, 1, 0])) + P3 = PermutationMatrix(Permutation([1, 0, 2])) + assert P1*P2 == P3 + + +def test_PermutationMatrix_matpow(): + p1 = Permutation([1, 2, 0]) + P1 = PermutationMatrix(p1) + p2 = Permutation([2, 0, 1]) + P2 = PermutationMatrix(p2) + assert P1**2 == P2 + assert P1**3 == Identity(3) + + +def test_PermutationMatrix_identity(): + p = Permutation([0, 1]) + assert PermutationMatrix(p).is_Identity + + p = Permutation([1, 0]) + assert not PermutationMatrix(p).is_Identity + + +def test_PermutationMatrix_determinant(): + P = PermutationMatrix(Permutation([0, 1, 2])) + assert Determinant(P).doit() == 1 + P = PermutationMatrix(Permutation([0, 2, 1])) + assert Determinant(P).doit() == -1 + P = PermutationMatrix(Permutation([2, 0, 1])) + assert Determinant(P).doit() == 1 + + +def test_PermutationMatrix_inverse(): + P = PermutationMatrix(Permutation(0, 1, 2)) + assert Inverse(P).doit() == PermutationMatrix(Permutation(0, 2, 1)) + + +def test_PermutationMatrix_rewrite_BlockDiagMatrix(): + P = PermutationMatrix(Permutation([0, 1, 2, 3, 4, 5])) + P0 = PermutationMatrix(Permutation([0])) + assert P.rewrite(BlockDiagMatrix) == \ + BlockDiagMatrix(P0, P0, P0, P0, P0, P0) + + P = PermutationMatrix(Permutation([0, 1, 3, 2, 4, 5])) + P10 = PermutationMatrix(Permutation(0, 1)) + assert P.rewrite(BlockDiagMatrix) == \ + BlockDiagMatrix(P0, P0, P10, P0, P0) + + P = PermutationMatrix(Permutation([1, 0, 3, 2, 5, 4])) + assert P.rewrite(BlockDiagMatrix) == \ + BlockDiagMatrix(P10, P10, P10) + + P = PermutationMatrix(Permutation([0, 4, 3, 2, 1, 5])) + P3210 = PermutationMatrix(Permutation([3, 2, 1, 0])) + assert P.rewrite(BlockDiagMatrix) == \ + BlockDiagMatrix(P0, P3210, P0) + + P = PermutationMatrix(Permutation([0, 4, 2, 3, 1, 5])) + P3120 = PermutationMatrix(Permutation([3, 1, 2, 0])) + assert P.rewrite(BlockDiagMatrix) == \ + BlockDiagMatrix(P0, P3120, P0) + + P = PermutationMatrix(Permutation(0, 3)(1, 4)(2, 5)) + assert P.rewrite(BlockDiagMatrix) == BlockDiagMatrix(P) + + +def test_MartrixPermute_basic(): + p = Permutation(0, 1) + P = PermutationMatrix(p) + A = MatrixSymbol('A', 2, 2) + + raises(ValueError, lambda: MatrixPermute(Symbol('x'), p)) + raises(ValueError, lambda: MatrixPermute(A, Symbol('x'))) + + assert MatrixPermute(A, P) == MatrixPermute(A, p) + raises(ValueError, lambda: MatrixPermute(A, p, 2)) + + pp = Permutation(0, 1, size=3) + assert MatrixPermute(A, pp) == MatrixPermute(A, p) + pp = Permutation(0, 1, 2) + raises(ValueError, lambda: MatrixPermute(A, pp)) + + +def test_MatrixPermute_shape(): + p = Permutation(0, 1) + A = MatrixSymbol('A', 2, 3) + assert MatrixPermute(A, p).shape == (2, 3) + + +def test_MatrixPermute_explicit(): + p = Permutation(0, 1, 2) + A = MatrixSymbol('A', 3, 3) + AA = A.as_explicit() + assert MatrixPermute(A, p, 0).as_explicit() == \ + AA.permute(p, orientation='rows') + assert MatrixPermute(A, p, 1).as_explicit() == \ + AA.permute(p, orientation='cols') + + +def test_MatrixPermute_rewrite_MatMul(): + p = Permutation(0, 1, 2) + A = MatrixSymbol('A', 3, 3) + + assert MatrixPermute(A, p, 0).rewrite(MatMul).as_explicit() == \ + MatrixPermute(A, p, 0).as_explicit() + assert MatrixPermute(A, p, 1).rewrite(MatMul).as_explicit() == \ + MatrixPermute(A, p, 1).as_explicit() + + +def test_MatrixPermute_doit(): + p = Permutation(0, 1, 2) + A = MatrixSymbol('A', 3, 3) + assert MatrixPermute(A, p).doit() == MatrixPermute(A, p) + + p = Permutation(0, size=3) + A = MatrixSymbol('A', 3, 3) + assert MatrixPermute(A, p).doit().as_explicit() == \ + MatrixPermute(A, p).as_explicit() + + p = Permutation(0, 1, 2) + A = Identity(3) + assert MatrixPermute(A, p, 0).doit().as_explicit() == \ + MatrixPermute(A, p, 0).as_explicit() + assert MatrixPermute(A, p, 1).doit().as_explicit() == \ + MatrixPermute(A, p, 1).as_explicit() + + A = ZeroMatrix(3, 3) + assert MatrixPermute(A, p).doit() == A + A = OneMatrix(3, 3) + assert MatrixPermute(A, p).doit() == A + + A = MatrixSymbol('A', 4, 4) + p1 = Permutation(0, 1, 2, 3) + p2 = Permutation(0, 2, 3, 1) + expr = MatrixPermute(MatrixPermute(A, p1, 0), p2, 0) + assert expr.as_explicit() == expr.doit().as_explicit() + expr = MatrixPermute(MatrixPermute(A, p1, 1), p2, 1) + assert expr.as_explicit() == expr.doit().as_explicit() diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_sets.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_sets.py new file mode 100644 index 0000000000000000000000000000000000000000..e811c7968c5a22d65f1c99e995aaa7e5e59d15c4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_sets.py @@ -0,0 +1,42 @@ +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.matrices import Matrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.sets import MatrixSet +from sympy.matrices.expressions.special import ZeroMatrix +from sympy.testing.pytest import raises +from sympy.sets.sets import SetKind +from sympy.matrices.kind import MatrixKind +from sympy.core.kind import NumberKind + + +def test_MatrixSet(): + n, m = symbols('n m', integer=True) + A = MatrixSymbol('A', n, m) + C = MatrixSymbol('C', n, n) + + M = MatrixSet(2, 2, set=S.Reals) + assert M.shape == (2, 2) + assert M.set == S.Reals + X = Matrix([[1, 2], [3, 4]]) + assert X in M + X = ZeroMatrix(2, 2) + assert X in M + raises(TypeError, lambda: A in M) + raises(TypeError, lambda: 1 in M) + M = MatrixSet(n, m, set=S.Reals) + assert A in M + raises(TypeError, lambda: C in M) + raises(TypeError, lambda: X in M) + M = MatrixSet(2, 2, set={1, 2, 3}) + X = Matrix([[1, 2], [3, 4]]) + Y = Matrix([[1, 2]]) + assert (X in M) == S.false + assert (Y in M) == S.false + raises(ValueError, lambda: MatrixSet(2, -2, S.Reals)) + raises(ValueError, lambda: MatrixSet(2.4, -1, S.Reals)) + raises(TypeError, lambda: MatrixSet(2, 2, (1, 2, 3))) + + +def test_SetKind_MatrixSet(): + assert MatrixSet(2, 2, set=S.Reals).kind is SetKind(MatrixKind(NumberKind)) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_slice.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_slice.py new file mode 100644 index 0000000000000000000000000000000000000000..36490719e26908b9e913ed99b7673d602647c492 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_slice.py @@ -0,0 +1,65 @@ +from sympy.matrices.expressions.slice import MatrixSlice +from sympy.matrices.expressions import MatrixSymbol +from sympy.abc import a, b, c, d, k, l, m, n +from sympy.testing.pytest import raises, XFAIL +from sympy.functions.elementary.integers import floor +from sympy.assumptions import assuming, Q + + +X = MatrixSymbol('X', n, m) +Y = MatrixSymbol('Y', m, k) + +def test_shape(): + B = MatrixSlice(X, (a, b), (c, d)) + assert B.shape == (b - a, d - c) + +def test_entry(): + B = MatrixSlice(X, (a, b), (c, d)) + assert B[0,0] == X[a, c] + assert B[k,l] == X[a+k, c+l] + raises(IndexError, lambda : MatrixSlice(X, 1, (2, 5))[1, 0]) + + assert X[1::2, :][1, 3] == X[1+2, 3] + assert X[:, 1::2][3, 1] == X[3, 1+2] + +def test_on_diag(): + assert not MatrixSlice(X, (a, b), (c, d)).on_diag + assert MatrixSlice(X, (a, b), (a, b)).on_diag + +def test_inputs(): + assert MatrixSlice(X, 1, (2, 5)) == MatrixSlice(X, (1, 2), (2, 5)) + assert MatrixSlice(X, 1, (2, 5)).shape == (1, 3) + +def test_slicing(): + assert X[1:5, 2:4] == MatrixSlice(X, (1, 5), (2, 4)) + assert X[1, 2:4] == MatrixSlice(X, 1, (2, 4)) + assert X[1:5, :].shape == (4, X.shape[1]) + assert X[:, 1:5].shape == (X.shape[0], 4) + + assert X[::2, ::2].shape == (floor(n/2), floor(m/2)) + assert X[2, :] == MatrixSlice(X, 2, (0, m)) + assert X[k, :] == MatrixSlice(X, k, (0, m)) + +def test_exceptions(): + X = MatrixSymbol('x', 10, 20) + raises(IndexError, lambda: X[0:12, 2]) + raises(IndexError, lambda: X[0:9, 22]) + raises(IndexError, lambda: X[-1:5, 2]) + +@XFAIL +def test_symmetry(): + X = MatrixSymbol('x', 10, 10) + Y = X[:5, 5:] + with assuming(Q.symmetric(X)): + assert Y.T == X[5:, :5] + +def test_slice_of_slice(): + X = MatrixSymbol('x', 10, 10) + assert X[2, :][:, 3][0, 0] == X[2, 3] + assert X[:5, :5][:4, :4] == X[:4, :4] + assert X[1:5, 2:6][1:3, 2] == X[2:4, 4] + assert X[1:9:2, 2:6][1:3, 2] == X[3:7:2, 4] + +def test_negative_index(): + X = MatrixSymbol('x', 10, 10) + assert X[-1, :] == X[9, :] diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_special.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_special.py new file mode 100644 index 0000000000000000000000000000000000000000..beeaf1d76a63673b6622709cda598dfcb295bba4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_special.py @@ -0,0 +1,228 @@ +from sympy.core.add import Add +from sympy.core.expr import unchanged +from sympy.core.mul import Mul +from sympy.core.symbol import symbols +from sympy.core.relational import Eq +from sympy.concrete.summations import Sum +from sympy.functions.elementary.complexes import im, re +from sympy.functions.elementary.piecewise import Piecewise +from sympy.matrices.immutable import ImmutableDenseMatrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.matadd import MatAdd +from sympy.matrices.expressions.special import ( + ZeroMatrix, GenericZeroMatrix, Identity, GenericIdentity, OneMatrix) +from sympy.matrices.expressions.matmul import MatMul +from sympy.testing.pytest import raises + + +def test_zero_matrix_creation(): + assert unchanged(ZeroMatrix, 2, 2) + assert unchanged(ZeroMatrix, 0, 0) + raises(ValueError, lambda: ZeroMatrix(-1, 2)) + raises(ValueError, lambda: ZeroMatrix(2.0, 2)) + raises(ValueError, lambda: ZeroMatrix(2j, 2)) + raises(ValueError, lambda: ZeroMatrix(2, -1)) + raises(ValueError, lambda: ZeroMatrix(2, 2.0)) + raises(ValueError, lambda: ZeroMatrix(2, 2j)) + + n = symbols('n') + assert unchanged(ZeroMatrix, n, n) + n = symbols('n', integer=False) + raises(ValueError, lambda: ZeroMatrix(n, n)) + n = symbols('n', negative=True) + raises(ValueError, lambda: ZeroMatrix(n, n)) + + +def test_generic_zero_matrix(): + z = GenericZeroMatrix() + n = symbols('n', integer=True) + A = MatrixSymbol("A", n, n) + + assert z == z + assert z != A + assert A != z + + assert z.is_ZeroMatrix + + raises(TypeError, lambda: z.shape) + raises(TypeError, lambda: z.rows) + raises(TypeError, lambda: z.cols) + + assert MatAdd() == z + assert MatAdd(z, A) == MatAdd(A) + # Make sure it is hashable + hash(z) + + +def test_identity_matrix_creation(): + assert Identity(2) + assert Identity(0) + raises(ValueError, lambda: Identity(-1)) + raises(ValueError, lambda: Identity(2.0)) + raises(ValueError, lambda: Identity(2j)) + + n = symbols('n') + assert Identity(n) + n = symbols('n', integer=False) + raises(ValueError, lambda: Identity(n)) + n = symbols('n', negative=True) + raises(ValueError, lambda: Identity(n)) + + +def test_generic_identity(): + I = GenericIdentity() + n = symbols('n', integer=True) + A = MatrixSymbol("A", n, n) + + assert I == I + assert I != A + assert A != I + + assert I.is_Identity + assert I**-1 == I + + raises(TypeError, lambda: I.shape) + raises(TypeError, lambda: I.rows) + raises(TypeError, lambda: I.cols) + + assert MatMul() == I + assert MatMul(I, A) == MatMul(A) + # Make sure it is hashable + hash(I) + + +def test_one_matrix_creation(): + assert OneMatrix(2, 2) + assert OneMatrix(0, 0) + assert Eq(OneMatrix(1, 1), Identity(1)) + raises(ValueError, lambda: OneMatrix(-1, 2)) + raises(ValueError, lambda: OneMatrix(2.0, 2)) + raises(ValueError, lambda: OneMatrix(2j, 2)) + raises(ValueError, lambda: OneMatrix(2, -1)) + raises(ValueError, lambda: OneMatrix(2, 2.0)) + raises(ValueError, lambda: OneMatrix(2, 2j)) + + n = symbols('n') + assert OneMatrix(n, n) + n = symbols('n', integer=False) + raises(ValueError, lambda: OneMatrix(n, n)) + n = symbols('n', negative=True) + raises(ValueError, lambda: OneMatrix(n, n)) + + +def test_ZeroMatrix(): + n, m = symbols('n m', integer=True) + A = MatrixSymbol('A', n, m) + Z = ZeroMatrix(n, m) + + assert A + Z == A + assert A*Z.T == ZeroMatrix(n, n) + assert Z*A.T == ZeroMatrix(n, n) + assert A - A == ZeroMatrix(*A.shape) + + assert Z + + assert Z.transpose() == ZeroMatrix(m, n) + assert Z.conjugate() == Z + assert Z.adjoint() == ZeroMatrix(m, n) + assert re(Z) == Z + assert im(Z) == Z + + assert ZeroMatrix(n, n)**0 == Identity(n) + assert ZeroMatrix(3, 3).as_explicit() == ImmutableDenseMatrix.zeros(3, 3) + + +def test_ZeroMatrix_doit(): + n = symbols('n', integer=True) + Znn = ZeroMatrix(Add(n, n, evaluate=False), n) + assert isinstance(Znn.rows, Add) + assert Znn.doit() == ZeroMatrix(2*n, n) + assert isinstance(Znn.doit().rows, Mul) + + +def test_OneMatrix(): + n, m = symbols('n m', integer=True) + A = MatrixSymbol('A', n, m) + U = OneMatrix(n, m) + + assert U.shape == (n, m) + assert isinstance(A + U, Add) + assert U.transpose() == OneMatrix(m, n) + assert U.conjugate() == U + assert U.adjoint() == OneMatrix(m, n) + assert re(U) == U + assert im(U) == ZeroMatrix(n, m) + + assert OneMatrix(n, n) ** 0 == Identity(n) + + U = OneMatrix(n, n) + assert U[1, 2] == 1 + + U = OneMatrix(2, 3) + assert U.as_explicit() == ImmutableDenseMatrix.ones(2, 3) + + +def test_OneMatrix_doit(): + n = symbols('n', integer=True) + Unn = OneMatrix(Add(n, n, evaluate=False), n) + assert isinstance(Unn.rows, Add) + assert Unn.doit() == OneMatrix(2 * n, n) + assert isinstance(Unn.doit().rows, Mul) + + +def test_OneMatrix_mul(): + n, m, k = symbols('n m k', integer=True) + w = MatrixSymbol('w', n, 1) + assert OneMatrix(n, m) * OneMatrix(m, k) == OneMatrix(n, k) * m + assert w * OneMatrix(1, 1) == w + assert OneMatrix(1, 1) * w.T == w.T + + +def test_Identity(): + n, m = symbols('n m', integer=True) + A = MatrixSymbol('A', n, m) + i, j = symbols('i j') + + In = Identity(n) + Im = Identity(m) + + assert A*Im == A + assert In*A == A + + assert In.transpose() == In + assert In.inverse() == In + assert In.conjugate() == In + assert In.adjoint() == In + assert re(In) == In + assert im(In) == ZeroMatrix(n, n) + + assert In[i, j] != 0 + assert Sum(In[i, j], (i, 0, n-1), (j, 0, n-1)).subs(n,3).doit() == 3 + assert Sum(Sum(In[i, j], (i, 0, n-1)), (j, 0, n-1)).subs(n,3).doit() == 3 + + # If range exceeds the limit `(0, n-1)`, do not remove `Piecewise`: + expr = Sum(In[i, j], (i, 0, n-1)) + assert expr.doit() == 1 + expr = Sum(In[i, j], (i, 0, n-2)) + assert expr.doit().dummy_eq( + Piecewise( + (1, (j >= 0) & (j <= n-2)), + (0, True) + ) + ) + expr = Sum(In[i, j], (i, 1, n-1)) + assert expr.doit().dummy_eq( + Piecewise( + (1, (j >= 1) & (j <= n-1)), + (0, True) + ) + ) + assert Identity(3).as_explicit() == ImmutableDenseMatrix.eye(3) + + +def test_Identity_doit(): + n = symbols('n', integer=True) + Inn = Identity(Add(n, n, evaluate=False)) + assert isinstance(Inn.rows, Add) + assert Inn.doit() == Identity(2*n) + assert isinstance(Inn.doit().rows, Mul) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_trace.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_trace.py new file mode 100644 index 0000000000000000000000000000000000000000..3bd66bec2377dae634ff486f42cc474eda7b23b1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_trace.py @@ -0,0 +1,116 @@ +from sympy.core import Lambda, S, symbols +from sympy.concrete import Sum +from sympy.functions import adjoint, conjugate, transpose +from sympy.matrices import eye, Matrix, ShapeError, ImmutableMatrix +from sympy.matrices.expressions import ( + Adjoint, Identity, FunctionMatrix, MatrixExpr, MatrixSymbol, Trace, + ZeroMatrix, trace, MatPow, MatAdd, MatMul +) +from sympy.matrices.expressions.special import OneMatrix +from sympy.testing.pytest import raises +from sympy.abc import i + + +n = symbols('n', integer=True) +A = MatrixSymbol('A', n, n) +B = MatrixSymbol('B', n, n) +C = MatrixSymbol('C', 3, 4) + + +def test_Trace(): + assert isinstance(Trace(A), Trace) + assert not isinstance(Trace(A), MatrixExpr) + raises(ShapeError, lambda: Trace(C)) + assert trace(eye(3)) == 3 + assert trace(Matrix(3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 9])) == 15 + + assert adjoint(Trace(A)) == trace(Adjoint(A)) + assert conjugate(Trace(A)) == trace(Adjoint(A)) + assert transpose(Trace(A)) == Trace(A) + + _ = A / Trace(A) # Make sure this is possible + + # Some easy simplifications + assert trace(Identity(5)) == 5 + assert trace(ZeroMatrix(5, 5)) == 0 + assert trace(OneMatrix(1, 1)) == 1 + assert trace(OneMatrix(2, 2)) == 2 + assert trace(OneMatrix(n, n)) == n + assert trace(2*A*B) == 2*Trace(A*B) + assert trace(A.T) == trace(A) + + i, j = symbols('i j') + F = FunctionMatrix(3, 3, Lambda((i, j), i + j)) + assert trace(F) == (0 + 0) + (1 + 1) + (2 + 2) + + raises(TypeError, lambda: Trace(S.One)) + + assert Trace(A).arg is A + + assert str(trace(A)) == str(Trace(A).doit()) + + assert Trace(A).is_commutative is True + +def test_Trace_A_plus_B(): + assert trace(A + B) == Trace(A) + Trace(B) + assert Trace(A + B).arg == MatAdd(A, B) + assert Trace(A + B).doit() == Trace(A) + Trace(B) + + +def test_Trace_MatAdd_doit(): + # See issue #9028 + X = ImmutableMatrix([[1, 2, 3]]*3) + Y = MatrixSymbol('Y', 3, 3) + q = MatAdd(X, 2*X, Y, -3*Y) + assert Trace(q).arg == q + assert Trace(q).doit() == 18 - 2*Trace(Y) + + +def test_Trace_MatPow_doit(): + X = Matrix([[1, 2], [3, 4]]) + assert Trace(X).doit() == 5 + q = MatPow(X, 2) + assert Trace(q).arg == q + assert Trace(q).doit() == 29 + + +def test_Trace_MutableMatrix_plus(): + # See issue #9043 + X = Matrix([[1, 2], [3, 4]]) + assert Trace(X) + Trace(X) == 2*Trace(X) + + +def test_Trace_doit_deep_False(): + X = Matrix([[1, 2], [3, 4]]) + q = MatPow(X, 2) + assert Trace(q).doit(deep=False).arg == q + q = MatAdd(X, 2*X) + assert Trace(q).doit(deep=False).arg == q + q = MatMul(X, 2*X) + assert Trace(q).doit(deep=False).arg == q + + +def test_trace_constant_factor(): + # Issue 9052: gave 2*Trace(MatMul(A)) instead of 2*Trace(A) + assert trace(2*A) == 2*Trace(A) + X = ImmutableMatrix([[1, 2], [3, 4]]) + assert trace(MatMul(2, X)) == 10 + + +def test_trace_rewrite(): + assert trace(A).rewrite(Sum) == Sum(A[i, i], (i, 0, n - 1)) + assert trace(eye(3)).rewrite(Sum) == 3 + + +def test_trace_normalize(): + assert Trace(B*A) != Trace(A*B) + assert Trace(B*A)._normalize() == Trace(A*B) + assert Trace(B*A.T)._normalize() == Trace(A*B.T) + + +def test_trace_as_explicit(): + raises(ValueError, lambda: Trace(A).as_explicit()) + + X = MatrixSymbol("X", 3, 3) + assert Trace(X).as_explicit() == X[0, 0] + X[1, 1] + X[2, 2] + assert Trace(eye(3)).as_explicit() == 3 diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_transpose.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_transpose.py new file mode 100644 index 0000000000000000000000000000000000000000..a1a6113873426d99bacf85484d3b66781f300af7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/tests/test_transpose.py @@ -0,0 +1,69 @@ +from sympy.functions import adjoint, conjugate, transpose +from sympy.matrices.expressions import MatrixSymbol, Adjoint, trace, Transpose +from sympy.matrices import eye, Matrix +from sympy.assumptions.ask import Q +from sympy.assumptions.refine import refine +from sympy.core.singleton import S +from sympy.core.symbol import symbols + +n, m, l, k, p = symbols('n m l k p', integer=True) +A = MatrixSymbol('A', n, m) +B = MatrixSymbol('B', m, l) +C = MatrixSymbol('C', n, n) + + +def test_transpose(): + Sq = MatrixSymbol('Sq', n, n) + + assert transpose(A) == Transpose(A) + assert Transpose(A).shape == (m, n) + assert Transpose(A*B).shape == (l, n) + assert transpose(Transpose(A)) == A + assert isinstance(Transpose(Transpose(A)), Transpose) + + assert adjoint(Transpose(A)) == Adjoint(Transpose(A)) + assert conjugate(Transpose(A)) == Adjoint(A) + + assert Transpose(eye(3)).doit() == eye(3) + + assert Transpose(S(5)).doit() == S(5) + + assert Transpose(Matrix([[1, 2], [3, 4]])).doit() == Matrix([[1, 3], [2, 4]]) + + assert transpose(trace(Sq)) == trace(Sq) + assert trace(Transpose(Sq)) == trace(Sq) + + assert Transpose(Sq)[0, 1] == Sq[1, 0] + + assert Transpose(A*B).doit() == Transpose(B) * Transpose(A) + + +def test_transpose_MatAdd_MatMul(): + # Issue 16807 + from sympy.functions.elementary.trigonometric import cos + + x = symbols('x') + M = MatrixSymbol('M', 3, 3) + N = MatrixSymbol('N', 3, 3) + + assert (N + (cos(x) * M)).T == cos(x)*M.T + N.T + + +def test_refine(): + assert refine(C.T, Q.symmetric(C)) == C + + +def test_transpose1x1(): + m = MatrixSymbol('m', 1, 1) + assert m == refine(m.T) + assert m == refine(m.T.T) + +def test_issue_9817(): + from sympy.matrices.expressions import Identity + v = MatrixSymbol('v', 3, 1) + A = MatrixSymbol('A', 3, 3) + x = Matrix([i + 1 for i in range(3)]) + X = Identity(3) + quadratic = v.T * A * v + subbed = quadratic.xreplace({v:x, A:X}) + assert subbed.as_explicit() == Matrix([[14]]) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/trace.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/trace.py new file mode 100644 index 0000000000000000000000000000000000000000..b5f9f94ea7486dc21b47c2e2e783a93280b180e0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/trace.py @@ -0,0 +1,167 @@ +from sympy.core.basic import Basic +from sympy.core.expr import Expr, ExprBuilder +from sympy.core.singleton import S +from sympy.core.sorting import default_sort_key +from sympy.core.symbol import uniquely_named_symbol +from sympy.core.sympify import sympify +from sympy.matrices.matrixbase import MatrixBase +from sympy.matrices.exceptions import NonSquareMatrixError + + +class Trace(Expr): + """Matrix Trace + + Represents the trace of a matrix expression. + + Examples + ======== + + >>> from sympy import MatrixSymbol, Trace, eye + >>> A = MatrixSymbol('A', 3, 3) + >>> Trace(A) + Trace(A) + >>> Trace(eye(3)) + Trace(Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]])) + >>> Trace(eye(3)).simplify() + 3 + """ + is_Trace = True + is_commutative = True + + def __new__(cls, mat): + mat = sympify(mat) + + if not mat.is_Matrix: + raise TypeError("input to Trace, %s, is not a matrix" % str(mat)) + + if mat.is_square is False: + raise NonSquareMatrixError("Trace of a non-square matrix") + + return Basic.__new__(cls, mat) + + def _eval_transpose(self): + return self + + def _eval_derivative(self, v): + from sympy.concrete.summations import Sum + from .matexpr import MatrixElement + if isinstance(v, MatrixElement): + return self.rewrite(Sum).diff(v) + expr = self.doit() + if isinstance(expr, Trace): + # Avoid looping infinitely: + raise NotImplementedError + return expr._eval_derivative(v) + + def _eval_derivative_matrix_lines(self, x): + from sympy.tensor.array.expressions.array_expressions import ArrayTensorProduct, ArrayContraction + r = self.args[0]._eval_derivative_matrix_lines(x) + for lr in r: + if lr.higher == 1: + lr.higher = ExprBuilder( + ArrayContraction, + [ + ExprBuilder( + ArrayTensorProduct, + [ + lr._lines[0], + lr._lines[1], + ] + ), + (1, 3), + ], + validator=ArrayContraction._validate + ) + else: + # This is not a matrix line: + lr.higher = ExprBuilder( + ArrayContraction, + [ + ExprBuilder( + ArrayTensorProduct, + [ + lr._lines[0], + lr._lines[1], + lr.higher, + ] + ), + (1, 3), (0, 2) + ] + ) + lr._lines = [S.One, S.One] + lr._first_pointer_parent = lr._lines + lr._second_pointer_parent = lr._lines + lr._first_pointer_index = 0 + lr._second_pointer_index = 1 + return r + + @property + def arg(self): + return self.args[0] + + def doit(self, **hints): + if hints.get('deep', True): + arg = self.arg.doit(**hints) + result = arg._eval_trace() + if result is not None: + return result + else: + return Trace(arg) + else: + # _eval_trace would go too deep here + if isinstance(self.arg, MatrixBase): + return trace(self.arg) + else: + return Trace(self.arg) + + def as_explicit(self): + return Trace(self.arg.as_explicit()).doit() + + def _normalize(self): + # Normalization of trace of matrix products. Use transposition and + # cyclic properties of traces to make sure the arguments of the matrix + # product are sorted and the first argument is not a transposition. + from sympy.matrices.expressions.matmul import MatMul + from sympy.matrices.expressions.transpose import Transpose + trace_arg = self.arg + if isinstance(trace_arg, MatMul): + + def get_arg_key(x): + a = trace_arg.args[x] + if isinstance(a, Transpose): + a = a.arg + return default_sort_key(a) + + indmin = min(range(len(trace_arg.args)), key=get_arg_key) + if isinstance(trace_arg.args[indmin], Transpose): + trace_arg = Transpose(trace_arg).doit() + indmin = min(range(len(trace_arg.args)), key=lambda x: default_sort_key(trace_arg.args[x])) + trace_arg = MatMul.fromiter(trace_arg.args[indmin:] + trace_arg.args[:indmin]) + return Trace(trace_arg) + return self + + def _eval_rewrite_as_Sum(self, expr, **kwargs): + from sympy.concrete.summations import Sum + i = uniquely_named_symbol('i', [expr]) + s = Sum(self.arg[i, i], (i, 0, self.arg.rows - 1)) + return s.doit() + + +def trace(expr): + """Trace of a Matrix. Sum of the diagonal elements. + + Examples + ======== + + >>> from sympy import trace, Symbol, MatrixSymbol, eye + >>> n = Symbol('n') + >>> X = MatrixSymbol('X', n, n) # A square matrix + >>> trace(2*X) + 2*Trace(X) + >>> trace(eye(3)) + 3 + """ + return Trace(expr).doit() diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/expressions/transpose.py b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/transpose.py new file mode 100644 index 0000000000000000000000000000000000000000..b11f7fc21490aab219420610ca529d81d6995d40 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/expressions/transpose.py @@ -0,0 +1,103 @@ +from sympy.core.basic import Basic +from sympy.matrices.expressions.matexpr import MatrixExpr + + +class Transpose(MatrixExpr): + """ + The transpose of a matrix expression. + + This is a symbolic object that simply stores its argument without + evaluating it. To actually compute the transpose, use the ``transpose()`` + function, or the ``.T`` attribute of matrices. + + Examples + ======== + + >>> from sympy import MatrixSymbol, Transpose, transpose + >>> A = MatrixSymbol('A', 3, 5) + >>> B = MatrixSymbol('B', 5, 3) + >>> Transpose(A) + A.T + >>> A.T == transpose(A) == Transpose(A) + True + >>> Transpose(A*B) + (A*B).T + >>> transpose(A*B) + B.T*A.T + + """ + is_Transpose = True + + def doit(self, **hints): + arg = self.arg + if hints.get('deep', True) and isinstance(arg, Basic): + arg = arg.doit(**hints) + _eval_transpose = getattr(arg, '_eval_transpose', None) + if _eval_transpose is not None: + result = _eval_transpose() + return result if result is not None else Transpose(arg) + else: + return Transpose(arg) + + @property + def arg(self): + return self.args[0] + + @property + def shape(self): + return self.arg.shape[::-1] + + def _entry(self, i, j, expand=False, **kwargs): + return self.arg._entry(j, i, expand=expand, **kwargs) + + def _eval_adjoint(self): + return self.arg.conjugate() + + def _eval_conjugate(self): + return self.arg.adjoint() + + def _eval_transpose(self): + return self.arg + + def _eval_trace(self): + from .trace import Trace + return Trace(self.arg) # Trace(X.T) => Trace(X) + + def _eval_determinant(self): + from sympy.matrices.expressions.determinant import det + return det(self.arg) + + def _eval_derivative(self, x): + # x is a scalar: + return self.arg._eval_derivative(x) + + def _eval_derivative_matrix_lines(self, x): + lines = self.args[0]._eval_derivative_matrix_lines(x) + return [i.transpose() for i in lines] + + +def transpose(expr): + """Matrix transpose""" + return Transpose(expr).doit(deep=False) + + +from sympy.assumptions.ask import ask, Q +from sympy.assumptions.refine import handlers_dict + + +def refine_Transpose(expr, assumptions): + """ + >>> from sympy import MatrixSymbol, Q, assuming, refine + >>> X = MatrixSymbol('X', 2, 2) + >>> X.T + X.T + >>> with assuming(Q.symmetric(X)): + ... print(refine(X.T)) + X + """ + if ask(Q.symmetric(expr), assumptions): + return expr.arg + + return expr + +handlers_dict['Transpose'] = refine_Transpose diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/graph.py b/venv/lib/python3.11/site-packages/sympy/matrices/graph.py new file mode 100644 index 0000000000000000000000000000000000000000..4c6356db884cfcd3c759ada07ac559f43dbcbbcb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/graph.py @@ -0,0 +1,279 @@ +from sympy.utilities.iterables import \ + flatten, connected_components, strongly_connected_components +from .exceptions import NonSquareMatrixError + + +def _connected_components(M): + """Returns the list of connected vertices of the graph when + a square matrix is viewed as a weighted graph. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([ + ... [66, 0, 0, 68, 0, 0, 0, 0, 67], + ... [0, 55, 0, 0, 0, 0, 54, 53, 0], + ... [0, 0, 0, 0, 1, 2, 0, 0, 0], + ... [86, 0, 0, 88, 0, 0, 0, 0, 87], + ... [0, 0, 10, 0, 11, 12, 0, 0, 0], + ... [0, 0, 20, 0, 21, 22, 0, 0, 0], + ... [0, 45, 0, 0, 0, 0, 44, 43, 0], + ... [0, 35, 0, 0, 0, 0, 34, 33, 0], + ... [76, 0, 0, 78, 0, 0, 0, 0, 77]]) + >>> A.connected_components() + [[0, 3, 8], [1, 6, 7], [2, 4, 5]] + + Notes + ===== + + Even if any symbolic elements of the matrix can be indeterminate + to be zero mathematically, this only takes the account of the + structural aspect of the matrix, so they will considered to be + nonzero. + """ + if not M.is_square: + raise NonSquareMatrixError + + V = range(M.rows) + E = sorted(M.todok().keys()) + return connected_components((V, E)) + + +def _strongly_connected_components(M): + """Returns the list of strongly connected vertices of the graph when + a square matrix is viewed as a weighted graph. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([ + ... [44, 0, 0, 0, 43, 0, 45, 0, 0], + ... [0, 66, 62, 61, 0, 68, 0, 60, 67], + ... [0, 0, 22, 21, 0, 0, 0, 20, 0], + ... [0, 0, 12, 11, 0, 0, 0, 10, 0], + ... [34, 0, 0, 0, 33, 0, 35, 0, 0], + ... [0, 86, 82, 81, 0, 88, 0, 80, 87], + ... [54, 0, 0, 0, 53, 0, 55, 0, 0], + ... [0, 0, 2, 1, 0, 0, 0, 0, 0], + ... [0, 76, 72, 71, 0, 78, 0, 70, 77]]) + >>> A.strongly_connected_components() + [[0, 4, 6], [2, 3, 7], [1, 5, 8]] + """ + if not M.is_square: + raise NonSquareMatrixError + + # RepMatrix uses the more efficient DomainMatrix.scc() method + rep = getattr(M, '_rep', None) + if rep is not None: + return rep.scc() + + V = range(M.rows) + E = sorted(M.todok().keys()) + return strongly_connected_components((V, E)) + + +def _connected_components_decomposition(M): + """Decomposes a square matrix into block diagonal form only + using the permutations. + + Explanation + =========== + + The decomposition is in a form of $A = P^{-1} B P$ where $P$ is a + permutation matrix and $B$ is a block diagonal matrix. + + Returns + ======= + + P, B : PermutationMatrix, BlockDiagMatrix + *P* is a permutation matrix for the similarity transform + as in the explanation. And *B* is the block diagonal matrix of + the result of the permutation. + + If you would like to get the diagonal blocks from the + BlockDiagMatrix, see + :meth:`~sympy.matrices.expressions.blockmatrix.BlockDiagMatrix.get_diag_blocks`. + + Examples + ======== + + >>> from sympy import Matrix, pprint + >>> A = Matrix([ + ... [66, 0, 0, 68, 0, 0, 0, 0, 67], + ... [0, 55, 0, 0, 0, 0, 54, 53, 0], + ... [0, 0, 0, 0, 1, 2, 0, 0, 0], + ... [86, 0, 0, 88, 0, 0, 0, 0, 87], + ... [0, 0, 10, 0, 11, 12, 0, 0, 0], + ... [0, 0, 20, 0, 21, 22, 0, 0, 0], + ... [0, 45, 0, 0, 0, 0, 44, 43, 0], + ... [0, 35, 0, 0, 0, 0, 34, 33, 0], + ... [76, 0, 0, 78, 0, 0, 0, 0, 77]]) + + >>> P, B = A.connected_components_decomposition() + >>> pprint(P) + PermutationMatrix((1 3)(2 8 5 7 4 6)) + >>> pprint(B) + [[66 68 67] ] + [[ ] ] + [[86 88 87] 0 0 ] + [[ ] ] + [[76 78 77] ] + [ ] + [ [55 54 53] ] + [ [ ] ] + [ 0 [45 44 43] 0 ] + [ [ ] ] + [ [35 34 33] ] + [ ] + [ [0 1 2 ]] + [ [ ]] + [ 0 0 [10 11 12]] + [ [ ]] + [ [20 21 22]] + + >>> P = P.as_explicit() + >>> B = B.as_explicit() + >>> P.T*B*P == A + True + + Notes + ===== + + This problem corresponds to the finding of the connected components + of a graph, when a matrix is viewed as a weighted graph. + """ + from sympy.combinatorics.permutations import Permutation + from sympy.matrices.expressions.blockmatrix import BlockDiagMatrix + from sympy.matrices.expressions.permutation import PermutationMatrix + + iblocks = M.connected_components() + + p = Permutation(flatten(iblocks)) + P = PermutationMatrix(p) + + blocks = [] + for b in iblocks: + blocks.append(M[b, b]) + B = BlockDiagMatrix(*blocks) + return P, B + + +def _strongly_connected_components_decomposition(M, lower=True): + """Decomposes a square matrix into block triangular form only + using the permutations. + + Explanation + =========== + + The decomposition is in a form of $A = P^{-1} B P$ where $P$ is a + permutation matrix and $B$ is a block diagonal matrix. + + Parameters + ========== + + lower : bool + Makes $B$ lower block triangular when ``True``. + Otherwise, makes $B$ upper block triangular. + + Returns + ======= + + P, B : PermutationMatrix, BlockMatrix + *P* is a permutation matrix for the similarity transform + as in the explanation. And *B* is the block triangular matrix of + the result of the permutation. + + Examples + ======== + + >>> from sympy import Matrix, pprint + >>> A = Matrix([ + ... [44, 0, 0, 0, 43, 0, 45, 0, 0], + ... [0, 66, 62, 61, 0, 68, 0, 60, 67], + ... [0, 0, 22, 21, 0, 0, 0, 20, 0], + ... [0, 0, 12, 11, 0, 0, 0, 10, 0], + ... [34, 0, 0, 0, 33, 0, 35, 0, 0], + ... [0, 86, 82, 81, 0, 88, 0, 80, 87], + ... [54, 0, 0, 0, 53, 0, 55, 0, 0], + ... [0, 0, 2, 1, 0, 0, 0, 0, 0], + ... [0, 76, 72, 71, 0, 78, 0, 70, 77]]) + + A lower block triangular decomposition: + + >>> P, B = A.strongly_connected_components_decomposition() + >>> pprint(P) + PermutationMatrix((8)(1 4 3 2 6)(5 7)) + >>> pprint(B) + [[44 43 45] [0 0 0] [0 0 0] ] + [[ ] [ ] [ ] ] + [[34 33 35] [0 0 0] [0 0 0] ] + [[ ] [ ] [ ] ] + [[54 53 55] [0 0 0] [0 0 0] ] + [ ] + [ [0 0 0] [22 21 20] [0 0 0] ] + [ [ ] [ ] [ ] ] + [ [0 0 0] [12 11 10] [0 0 0] ] + [ [ ] [ ] [ ] ] + [ [0 0 0] [2 1 0 ] [0 0 0] ] + [ ] + [ [0 0 0] [62 61 60] [66 68 67]] + [ [ ] [ ] [ ]] + [ [0 0 0] [82 81 80] [86 88 87]] + [ [ ] [ ] [ ]] + [ [0 0 0] [72 71 70] [76 78 77]] + + >>> P = P.as_explicit() + >>> B = B.as_explicit() + >>> P.T * B * P == A + True + + An upper block triangular decomposition: + + >>> P, B = A.strongly_connected_components_decomposition(lower=False) + >>> pprint(P) + PermutationMatrix((0 1 5 7 4 3 2 8 6)) + >>> pprint(B) + [[66 68 67] [62 61 60] [0 0 0] ] + [[ ] [ ] [ ] ] + [[86 88 87] [82 81 80] [0 0 0] ] + [[ ] [ ] [ ] ] + [[76 78 77] [72 71 70] [0 0 0] ] + [ ] + [ [0 0 0] [22 21 20] [0 0 0] ] + [ [ ] [ ] [ ] ] + [ [0 0 0] [12 11 10] [0 0 0] ] + [ [ ] [ ] [ ] ] + [ [0 0 0] [2 1 0 ] [0 0 0] ] + [ ] + [ [0 0 0] [0 0 0] [44 43 45]] + [ [ ] [ ] [ ]] + [ [0 0 0] [0 0 0] [34 33 35]] + [ [ ] [ ] [ ]] + [ [0 0 0] [0 0 0] [54 53 55]] + + >>> P = P.as_explicit() + >>> B = B.as_explicit() + >>> P.T * B * P == A + True + """ + from sympy.combinatorics.permutations import Permutation + from sympy.matrices.expressions.blockmatrix import BlockMatrix + from sympy.matrices.expressions.permutation import PermutationMatrix + + iblocks = M.strongly_connected_components() + if not lower: + iblocks = list(reversed(iblocks)) + + p = Permutation(flatten(iblocks)) + P = PermutationMatrix(p) + + rows = [] + for a in iblocks: + cols = [] + for b in iblocks: + cols.append(M[a, b]) + rows.append(cols) + B = BlockMatrix(rows) + return P, B diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/immutable.py b/venv/lib/python3.11/site-packages/sympy/matrices/immutable.py new file mode 100644 index 0000000000000000000000000000000000000000..7ec2174bf1c785e1a4698e1b55078d300e62dafe --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/immutable.py @@ -0,0 +1,196 @@ +from mpmath.matrices.matrices import _matrix + +from sympy.core import Basic, Dict, Tuple +from sympy.core.numbers import Integer +from sympy.core.cache import cacheit +from sympy.core.sympify import _sympy_converter as sympify_converter, _sympify +from sympy.matrices.dense import DenseMatrix +from sympy.matrices.expressions import MatrixExpr +from sympy.matrices.matrixbase import MatrixBase +from sympy.matrices.repmatrix import RepMatrix +from sympy.matrices.sparse import SparseRepMatrix +from sympy.multipledispatch import dispatch + + +def sympify_matrix(arg): + return arg.as_immutable() + + +sympify_converter[MatrixBase] = sympify_matrix + + +def sympify_mpmath_matrix(arg): + mat = [_sympify(x) for x in arg] + return ImmutableDenseMatrix(arg.rows, arg.cols, mat) + + +sympify_converter[_matrix] = sympify_mpmath_matrix + + +class ImmutableRepMatrix(RepMatrix, MatrixExpr): # type: ignore + """Immutable matrix based on RepMatrix + + Uses DomainMAtrix as the internal representation. + """ + + # + # This is a subclass of RepMatrix that adds/overrides some methods to make + # the instances Basic and immutable. ImmutableRepMatrix is a superclass for + # both ImmutableDenseMatrix and ImmutableSparseMatrix. + # + + def __new__(cls, *args, **kwargs): + return cls._new(*args, **kwargs) + + __hash__ = MatrixExpr.__hash__ + + def copy(self): + return self + + @property + def cols(self): + return self._cols + + @property + def rows(self): + return self._rows + + @property + def shape(self): + return self._rows, self._cols + + def as_immutable(self): + return self + + def _entry(self, i, j, **kwargs): + return self[i, j] + + def __setitem__(self, *args): + raise TypeError("Cannot set values of {}".format(self.__class__)) + + def is_diagonalizable(self, reals_only=False, **kwargs): + return super().is_diagonalizable( + reals_only=reals_only, **kwargs) + + is_diagonalizable.__doc__ = SparseRepMatrix.is_diagonalizable.__doc__ + is_diagonalizable = cacheit(is_diagonalizable) + + def analytic_func(self, f, x): + return self.as_mutable().analytic_func(f, x).as_immutable() + + +class ImmutableDenseMatrix(DenseMatrix, ImmutableRepMatrix): # type: ignore + """Create an immutable version of a matrix. + + Examples + ======== + + >>> from sympy import eye, ImmutableMatrix + >>> ImmutableMatrix(eye(3)) + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + >>> _[0, 0] = 42 + Traceback (most recent call last): + ... + TypeError: Cannot set values of ImmutableDenseMatrix + """ + + # MatrixExpr is set as NotIterable, but we want explicit matrices to be + # iterable + _iterable = True + _class_priority = 8 + _op_priority = 10.001 + + @classmethod + def _new(cls, *args, **kwargs): + if len(args) == 1 and isinstance(args[0], ImmutableDenseMatrix): + return args[0] + if kwargs.get('copy', True) is False: + if len(args) != 3: + raise TypeError("'copy=False' requires a matrix be initialized as rows,cols,[list]") + rows, cols, flat_list = args + else: + rows, cols, flat_list = cls._handle_creation_inputs(*args, **kwargs) + flat_list = list(flat_list) # create a shallow copy + + rep = cls._flat_list_to_DomainMatrix(rows, cols, flat_list) + + return cls._fromrep(rep) + + @classmethod + def _fromrep(cls, rep): + rows, cols = rep.shape + flat_list = rep.to_sympy().to_list_flat() + obj = Basic.__new__(cls, + Integer(rows), + Integer(cols), + Tuple(*flat_list, sympify=False)) + obj._rows = rows + obj._cols = cols + obj._rep = rep + return obj + + +# make sure ImmutableDenseMatrix is aliased as ImmutableMatrix +ImmutableMatrix = ImmutableDenseMatrix + + +class ImmutableSparseMatrix(SparseRepMatrix, ImmutableRepMatrix): # type:ignore + """Create an immutable version of a sparse matrix. + + Examples + ======== + + >>> from sympy import eye, ImmutableSparseMatrix + >>> ImmutableSparseMatrix(1, 1, {}) + Matrix([[0]]) + >>> ImmutableSparseMatrix(eye(3)) + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + >>> _[0, 0] = 42 + Traceback (most recent call last): + ... + TypeError: Cannot set values of ImmutableSparseMatrix + >>> _.shape + (3, 3) + """ + is_Matrix = True + _class_priority = 9 + + @classmethod + def _new(cls, *args, **kwargs): + rows, cols, smat = cls._handle_creation_inputs(*args, **kwargs) + + rep = cls._smat_to_DomainMatrix(rows, cols, smat) + + return cls._fromrep(rep) + + @classmethod + def _fromrep(cls, rep): + rows, cols = rep.shape + smat = rep.to_sympy().to_dok() + obj = Basic.__new__(cls, Integer(rows), Integer(cols), Dict(smat)) + obj._rows = rows + obj._cols = cols + obj._rep = rep + return obj + + +@dispatch(ImmutableDenseMatrix, ImmutableDenseMatrix) +def _eval_is_eq(lhs, rhs): # noqa:F811 + """Helper method for Equality with matrices.sympy. + + Relational automatically converts matrices to ImmutableDenseMatrix + instances, so this method only applies here. Returns True if the + matrices are definitively the same, False if they are definitively + different, and None if undetermined (e.g. if they contain Symbols). + Returning None triggers default handling of Equalities. + + """ + if lhs.shape != rhs.shape: + return False + return (lhs - rhs).is_zero_matrix diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/inverse.py b/venv/lib/python3.11/site-packages/sympy/matrices/inverse.py new file mode 100644 index 0000000000000000000000000000000000000000..61d9e12edf013d2f5555d61786343aa3840edfd3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/inverse.py @@ -0,0 +1,524 @@ +from sympy.polys.matrices.exceptions import DMNonInvertibleMatrixError +from sympy.polys.domains import EX + +from .exceptions import MatrixError, NonSquareMatrixError, NonInvertibleMatrixError +from .utilities import _iszero + + +def _pinv_full_rank(M): + """Subroutine for full row or column rank matrices. + + For full row rank matrices, inverse of ``A * A.H`` Exists. + For full column rank matrices, inverse of ``A.H * A`` Exists. + + This routine can apply for both cases by checking the shape + and have small decision. + """ + + if M.is_zero_matrix: + return M.H + + if M.rows >= M.cols: + return M.H.multiply(M).inv().multiply(M.H) + else: + return M.H.multiply(M.multiply(M.H).inv()) + +def _pinv_rank_decomposition(M): + """Subroutine for rank decomposition + + With rank decompositions, `A` can be decomposed into two full- + rank matrices, and each matrix can take pseudoinverse + individually. + """ + + if M.is_zero_matrix: + return M.H + + B, C = M.rank_decomposition() + + Bp = _pinv_full_rank(B) + Cp = _pinv_full_rank(C) + + return Cp.multiply(Bp) + +def _pinv_diagonalization(M): + """Subroutine using diagonalization + + This routine can sometimes fail if SymPy's eigenvalue + computation is not reliable. + """ + + if M.is_zero_matrix: + return M.H + + A = M + AH = M.H + + try: + if M.rows >= M.cols: + P, D = AH.multiply(A).diagonalize(normalize=True) + D_pinv = D.applyfunc(lambda x: 0 if _iszero(x) else 1 / x) + + return P.multiply(D_pinv).multiply(P.H).multiply(AH) + + else: + P, D = A.multiply(AH).diagonalize( + normalize=True) + D_pinv = D.applyfunc(lambda x: 0 if _iszero(x) else 1 / x) + + return AH.multiply(P).multiply(D_pinv).multiply(P.H) + + except MatrixError: + raise NotImplementedError( + 'pinv for rank-deficient matrices where ' + 'diagonalization of A.H*A fails is not supported yet.') + +def _pinv(M, method='RD'): + """Calculate the Moore-Penrose pseudoinverse of the matrix. + + The Moore-Penrose pseudoinverse exists and is unique for any matrix. + If the matrix is invertible, the pseudoinverse is the same as the + inverse. + + Parameters + ========== + + method : String, optional + Specifies the method for computing the pseudoinverse. + + If ``'RD'``, Rank-Decomposition will be used. + + If ``'ED'``, Diagonalization will be used. + + Examples + ======== + + Computing pseudoinverse by rank decomposition : + + >>> from sympy import Matrix + >>> A = Matrix([[1, 2, 3], [4, 5, 6]]) + >>> A.pinv() + Matrix([ + [-17/18, 4/9], + [ -1/9, 1/9], + [ 13/18, -2/9]]) + + Computing pseudoinverse by diagonalization : + + >>> B = A.pinv(method='ED') + >>> B.simplify() + >>> B + Matrix([ + [-17/18, 4/9], + [ -1/9, 1/9], + [ 13/18, -2/9]]) + + See Also + ======== + + inv + pinv_solve + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Moore-Penrose_pseudoinverse + + """ + + # Trivial case: pseudoinverse of all-zero matrix is its transpose. + if M.is_zero_matrix: + return M.H + + if method == 'RD': + return _pinv_rank_decomposition(M) + elif method == 'ED': + return _pinv_diagonalization(M) + else: + raise ValueError('invalid pinv method %s' % repr(method)) + + +def _verify_invertible(M, iszerofunc=_iszero): + """Initial check to see if a matrix is invertible. Raises or returns + determinant for use in _inv_ADJ.""" + + if not M.is_square: + raise NonSquareMatrixError("A Matrix must be square to invert.") + + d = M.det(method='berkowitz') + zero = d.equals(0) + + if zero is None: # if equals() can't decide, will rref be able to? + ok = M.rref(simplify=True)[0] + zero = any(iszerofunc(ok[j, j]) for j in range(ok.rows)) + + if zero: + raise NonInvertibleMatrixError("Matrix det == 0; not invertible.") + + return d + +def _inv_ADJ(M, iszerofunc=_iszero): + """Calculates the inverse using the adjugate matrix and a determinant. + + See Also + ======== + + inv + inverse_GE + inverse_LU + inverse_CH + inverse_LDL + """ + + d = _verify_invertible(M, iszerofunc=iszerofunc) + + return M.adjugate() / d + +def _inv_GE(M, iszerofunc=_iszero): + """Calculates the inverse using Gaussian elimination. + + See Also + ======== + + inv + inverse_ADJ + inverse_LU + inverse_CH + inverse_LDL + """ + + from .dense import Matrix + + if not M.is_square: + raise NonSquareMatrixError("A Matrix must be square to invert.") + + big = Matrix.hstack(M.as_mutable(), Matrix.eye(M.rows)) + red = big.rref(iszerofunc=iszerofunc, simplify=True)[0] + + if any(iszerofunc(red[j, j]) for j in range(red.rows)): + raise NonInvertibleMatrixError("Matrix det == 0; not invertible.") + + return M._new(red[:, big.rows:]) + +def _inv_LU(M, iszerofunc=_iszero): + """Calculates the inverse using LU decomposition. + + See Also + ======== + + inv + inverse_ADJ + inverse_GE + inverse_CH + inverse_LDL + """ + + if not M.is_square: + raise NonSquareMatrixError("A Matrix must be square to invert.") + if M.free_symbols: + _verify_invertible(M, iszerofunc=iszerofunc) + + return M.LUsolve(M.eye(M.rows), iszerofunc=_iszero) + +def _inv_CH(M, iszerofunc=_iszero): + """Calculates the inverse using cholesky decomposition. + + See Also + ======== + + inv + inverse_ADJ + inverse_GE + inverse_LU + inverse_LDL + """ + + _verify_invertible(M, iszerofunc=iszerofunc) + + return M.cholesky_solve(M.eye(M.rows)) + +def _inv_LDL(M, iszerofunc=_iszero): + """Calculates the inverse using LDL decomposition. + + See Also + ======== + + inv + inverse_ADJ + inverse_GE + inverse_LU + inverse_CH + """ + + _verify_invertible(M, iszerofunc=iszerofunc) + + return M.LDLsolve(M.eye(M.rows)) + +def _inv_QR(M, iszerofunc=_iszero): + """Calculates the inverse using QR decomposition. + + See Also + ======== + + inv + inverse_ADJ + inverse_GE + inverse_CH + inverse_LDL + """ + + _verify_invertible(M, iszerofunc=iszerofunc) + + return M.QRsolve(M.eye(M.rows)) + +def _try_DM(M, use_EX=False): + """Try to convert a matrix to a ``DomainMatrix``.""" + dM = M.to_DM() + K = dM.domain + + # Return DomainMatrix if a domain is found. Only use EX if use_EX=True. + if not use_EX and K.is_EXRAW: + return None + elif K.is_EXRAW: + return dM.convert_to(EX) + else: + return dM + + +def _use_exact_domain(dom): + """Check whether to convert to an exact domain.""" + # DomainMatrix can handle RR and CC with partial pivoting. Other inexact + # domains like RR[a,b,...] can only be handled by converting to an exact + # domain like QQ[a,b,...] + if dom.is_RR or dom.is_CC: + return False + else: + return not dom.is_Exact + + +def _inv_DM(dM, cancel=True): + """Calculates the inverse using ``DomainMatrix``. + + See Also + ======== + + inv + inverse_ADJ + inverse_GE + inverse_CH + inverse_LDL + sympy.polys.matrices.domainmatrix.DomainMatrix.inv + """ + m, n = dM.shape + dom = dM.domain + + if m != n: + raise NonSquareMatrixError("A Matrix must be square to invert.") + + # Convert RR[a,b,...] to QQ[a,b,...] + use_exact = _use_exact_domain(dom) + + if use_exact: + dom_exact = dom.get_exact() + dM = dM.convert_to(dom_exact) + + try: + dMi, den = dM.inv_den() + except DMNonInvertibleMatrixError: + raise NonInvertibleMatrixError("Matrix det == 0; not invertible.") + + if use_exact: + dMi = dMi.convert_to(dom) + den = dom.convert_from(den, dom_exact) + + if cancel: + # Convert to field and cancel with the denominator. + if not dMi.domain.is_Field: + dMi = dMi.to_field() + Mi = (dMi / den).to_Matrix() + else: + # Convert to Matrix and divide without cancelling + Mi = dMi.to_Matrix() / dMi.domain.to_sympy(den) + + return Mi + +def _inv_block(M, iszerofunc=_iszero): + """Calculates the inverse using BLOCKWISE inversion. + + See Also + ======== + + inv + inverse_ADJ + inverse_GE + inverse_CH + inverse_LDL + """ + from sympy.matrices.expressions.blockmatrix import BlockMatrix + i = M.shape[0] + if i <= 20 : + return M.inv(method="LU", iszerofunc=_iszero) + A = M[:i // 2, :i //2] + B = M[:i // 2, i // 2:] + C = M[i // 2:, :i // 2] + D = M[i // 2:, i // 2:] + try: + D_inv = _inv_block(D) + except NonInvertibleMatrixError: + return M.inv(method="LU", iszerofunc=_iszero) + B_D_i = B*D_inv + BDC = B_D_i*C + A_n = A - BDC + try: + A_n = _inv_block(A_n) + except NonInvertibleMatrixError: + return M.inv(method="LU", iszerofunc=_iszero) + B_n = -A_n*B_D_i + dc = D_inv*C + C_n = -dc*A_n + D_n = D_inv + dc*-B_n + nn = BlockMatrix([[A_n, B_n], [C_n, D_n]]).as_explicit() + return nn + +def _inv(M, method=None, iszerofunc=_iszero, try_block_diag=False): + """ + Return the inverse of a matrix using the method indicated. The default + is DM if a suitable domain is found or otherwise GE for dense matrices + LDL for sparse matrices. + + Parameters + ========== + + method : ('DM', 'DMNC', 'GE', 'LU', 'ADJ', 'CH', 'LDL', 'QR') + + iszerofunc : function, optional + Zero-testing function to use. + + try_block_diag : bool, optional + If True then will try to form block diagonal matrices using the + method get_diag_blocks(), invert these individually, and then + reconstruct the full inverse matrix. + + Examples + ======== + + >>> from sympy import SparseMatrix, Matrix + >>> A = SparseMatrix([ + ... [ 2, -1, 0], + ... [-1, 2, -1], + ... [ 0, 0, 2]]) + >>> A.inv('CH') + Matrix([ + [2/3, 1/3, 1/6], + [1/3, 2/3, 1/3], + [ 0, 0, 1/2]]) + >>> A.inv(method='LDL') # use of 'method=' is optional + Matrix([ + [2/3, 1/3, 1/6], + [1/3, 2/3, 1/3], + [ 0, 0, 1/2]]) + >>> A * _ + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + >>> A = Matrix(A) + >>> A.inv('CH') + Matrix([ + [2/3, 1/3, 1/6], + [1/3, 2/3, 1/3], + [ 0, 0, 1/2]]) + >>> A.inv('ADJ') == A.inv('GE') == A.inv('LU') == A.inv('CH') == A.inv('LDL') == A.inv('QR') + True + + Notes + ===== + + According to the ``method`` keyword, it calls the appropriate method: + + DM .... Use DomainMatrix ``inv_den`` method + DMNC .... Use DomainMatrix ``inv_den`` method without cancellation + GE .... inverse_GE(); default for dense matrices + LU .... inverse_LU() + ADJ ... inverse_ADJ() + CH ... inverse_CH() + LDL ... inverse_LDL(); default for sparse matrices + QR ... inverse_QR() + + Note, the GE and LU methods may require the matrix to be simplified + before it is inverted in order to properly detect zeros during + pivoting. In difficult cases a custom zero detection function can + be provided by setting the ``iszerofunc`` argument to a function that + should return True if its argument is zero. The ADJ routine computes + the determinant and uses that to detect singular matrices in addition + to testing for zeros on the diagonal. + + See Also + ======== + + inverse_ADJ + inverse_GE + inverse_LU + inverse_CH + inverse_LDL + + Raises + ====== + + ValueError + If the determinant of the matrix is zero. + """ + + from sympy.matrices import diag, SparseMatrix + + if not M.is_square: + raise NonSquareMatrixError("A Matrix must be square to invert.") + + if try_block_diag: + blocks = M.get_diag_blocks() + r = [] + + for block in blocks: + r.append(block.inv(method=method, iszerofunc=iszerofunc)) + + return diag(*r) + + # Default: Use DomainMatrix if the domain is not EX. + # If DM is requested explicitly then use it even if the domain is EX. + if method is None and iszerofunc is _iszero: + dM = _try_DM(M, use_EX=False) + if dM is not None: + method = 'DM' + elif method in ("DM", "DMNC"): + dM = _try_DM(M, use_EX=True) + + # A suitable domain was not found, fall back to GE for dense matrices + # and LDL for sparse matrices. + if method is None: + if isinstance(M, SparseMatrix): + method = 'LDL' + else: + method = 'GE' + + if method == "DM": + rv = _inv_DM(dM) + elif method == "DMNC": + rv = _inv_DM(dM, cancel=False) + elif method == "GE": + rv = M.inverse_GE(iszerofunc=iszerofunc) + elif method == "LU": + rv = M.inverse_LU(iszerofunc=iszerofunc) + elif method == "ADJ": + rv = M.inverse_ADJ(iszerofunc=iszerofunc) + elif method == "CH": + rv = M.inverse_CH(iszerofunc=iszerofunc) + elif method == "LDL": + rv = M.inverse_LDL(iszerofunc=iszerofunc) + elif method == "QR": + rv = M.inverse_QR(iszerofunc=iszerofunc) + elif method == "BLOCK": + rv = M.inverse_BLOCK(iszerofunc=iszerofunc) + else: + raise ValueError("Inversion method unrecognized") + + return M._new(rv) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/kind.py b/venv/lib/python3.11/site-packages/sympy/matrices/kind.py new file mode 100644 index 0000000000000000000000000000000000000000..f9f53ffe16f7cbde60213e49071a2a74e80e5c6c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/kind.py @@ -0,0 +1,97 @@ +# sympy.matrices.kind + +from sympy.core.kind import Kind, _NumberKind, NumberKind +from sympy.core.mul import Mul + + +class MatrixKind(Kind): + """ + Kind for all matrices in SymPy. + + Basic class for this kind is ``MatrixBase`` and ``MatrixExpr``, + but any expression representing the matrix can have this. + + Parameters + ========== + + element_kind : Kind + Kind of the element. Default is + :class:`sympy.core.kind.NumberKind`, + which means that the matrix contains only numbers. + + Examples + ======== + + Any instance of matrix class has kind ``MatrixKind``: + + >>> from sympy import MatrixSymbol + >>> A = MatrixSymbol('A', 2, 2) + >>> A.kind + MatrixKind(NumberKind) + + An expression representing a matrix may not be an instance of + the Matrix class, but it will have kind ``MatrixKind``: + + >>> from sympy import MatrixExpr, Integral + >>> from sympy.abc import x + >>> intM = Integral(A, x) + >>> isinstance(intM, MatrixExpr) + False + >>> intM.kind + MatrixKind(NumberKind) + + Use ``isinstance()`` to check for ``MatrixKind`` without specifying the + element kind. Use ``is`` to check the kind including the element kind: + + >>> from sympy import Matrix + >>> from sympy.core import NumberKind + >>> from sympy.matrices import MatrixKind + >>> M = Matrix([1, 2]) + >>> isinstance(M.kind, MatrixKind) + True + >>> M.kind is MatrixKind(NumberKind) + True + + See Also + ======== + + sympy.core.kind.NumberKind + sympy.core.kind.UndefinedKind + sympy.core.containers.TupleKind + sympy.sets.sets.SetKind + + """ + def __new__(cls, element_kind=NumberKind): + obj = super().__new__(cls, element_kind) + obj.element_kind = element_kind + return obj + + def __repr__(self): + return "MatrixKind(%s)" % self.element_kind + + +@Mul._kind_dispatcher.register(_NumberKind, MatrixKind) +def num_mat_mul(k1, k2): + """ + Return MatrixKind. The element kind is selected by recursive dispatching. + Do not need to dispatch in reversed order because KindDispatcher + searches for this automatically. + """ + # Deal with Mul._kind_dispatcher's commutativity + # XXX: this function is called with either k1 or k2 as MatrixKind because + # the Mul kind dispatcher is commutative. Maybe it shouldn't be. Need to + # swap the args here because NumberKind does not have an element_kind + # attribute. + if not isinstance(k2, MatrixKind): + k1, k2 = k2, k1 + elemk = Mul._kind_dispatcher(k1, k2.element_kind) + return MatrixKind(elemk) + + +@Mul._kind_dispatcher.register(MatrixKind, MatrixKind) +def mat_mat_mul(k1, k2): + """ + Return MatrixKind. The element kind is selected by recursive dispatching. + """ + elemk = Mul._kind_dispatcher(k1.element_kind, k2.element_kind) + return MatrixKind(elemk) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/matrices.py b/venv/lib/python3.11/site-packages/sympy/matrices/matrices.py new file mode 100644 index 0000000000000000000000000000000000000000..fed41a626cb395ac0529071317630d853e0d3a96 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/matrices.py @@ -0,0 +1,687 @@ +# +# A module consisting of deprecated matrix classes. New code should not be +# added here. +# +from sympy.core.basic import Basic +from sympy.core.symbol import Dummy + +from .common import MatrixCommon + +from .exceptions import NonSquareMatrixError + +from .utilities import _iszero, _is_zero_after_expand_mul, _simplify + +from .determinant import ( + _find_reasonable_pivot, _find_reasonable_pivot_naive, + _adjugate, _charpoly, _cofactor, _cofactor_matrix, _per, + _det, _det_bareiss, _det_berkowitz, _det_bird, _det_laplace, _det_LU, + _minor, _minor_submatrix) + +from .reductions import _is_echelon, _echelon_form, _rank, _rref +from .subspaces import _columnspace, _nullspace, _rowspace, _orthogonalize + +from .eigen import ( + _eigenvals, _eigenvects, + _bidiagonalize, _bidiagonal_decomposition, + _is_diagonalizable, _diagonalize, + _is_positive_definite, _is_positive_semidefinite, + _is_negative_definite, _is_negative_semidefinite, _is_indefinite, + _jordan_form, _left_eigenvects, _singular_values) + + +# This class was previously defined in this module, but was moved to +# sympy.matrices.matrixbase. We import it here for backwards compatibility in +# case someone was importing it from here. +from .matrixbase import MatrixBase + + +__doctest_requires__ = { + ('MatrixEigen.is_indefinite', + 'MatrixEigen.is_negative_definite', + 'MatrixEigen.is_negative_semidefinite', + 'MatrixEigen.is_positive_definite', + 'MatrixEigen.is_positive_semidefinite'): ['matplotlib'], +} + + +class MatrixDeterminant(MatrixCommon): + """Provides basic matrix determinant operations. Should not be instantiated + directly. See ``determinant.py`` for their implementations.""" + + def _eval_det_bareiss(self, iszerofunc=_is_zero_after_expand_mul): + return _det_bareiss(self, iszerofunc=iszerofunc) + + def _eval_det_berkowitz(self): + return _det_berkowitz(self) + + def _eval_det_lu(self, iszerofunc=_iszero, simpfunc=None): + return _det_LU(self, iszerofunc=iszerofunc, simpfunc=simpfunc) + + def _eval_det_bird(self): + return _det_bird(self) + + def _eval_det_laplace(self): + return _det_laplace(self) + + def _eval_determinant(self): # for expressions.determinant.Determinant + return _det(self) + + def adjugate(self, method="berkowitz"): + return _adjugate(self, method=method) + + def charpoly(self, x='lambda', simplify=_simplify): + return _charpoly(self, x=x, simplify=simplify) + + def cofactor(self, i, j, method="berkowitz"): + return _cofactor(self, i, j, method=method) + + def cofactor_matrix(self, method="berkowitz"): + return _cofactor_matrix(self, method=method) + + def det(self, method="bareiss", iszerofunc=None): + return _det(self, method=method, iszerofunc=iszerofunc) + + def per(self): + return _per(self) + + def minor(self, i, j, method="berkowitz"): + return _minor(self, i, j, method=method) + + def minor_submatrix(self, i, j): + return _minor_submatrix(self, i, j) + + _find_reasonable_pivot.__doc__ = _find_reasonable_pivot.__doc__ + _find_reasonable_pivot_naive.__doc__ = _find_reasonable_pivot_naive.__doc__ + _eval_det_bareiss.__doc__ = _det_bareiss.__doc__ + _eval_det_berkowitz.__doc__ = _det_berkowitz.__doc__ + _eval_det_bird.__doc__ = _det_bird.__doc__ + _eval_det_laplace.__doc__ = _det_laplace.__doc__ + _eval_det_lu.__doc__ = _det_LU.__doc__ + _eval_determinant.__doc__ = _det.__doc__ + adjugate.__doc__ = _adjugate.__doc__ + charpoly.__doc__ = _charpoly.__doc__ + cofactor.__doc__ = _cofactor.__doc__ + cofactor_matrix.__doc__ = _cofactor_matrix.__doc__ + det.__doc__ = _det.__doc__ + per.__doc__ = _per.__doc__ + minor.__doc__ = _minor.__doc__ + minor_submatrix.__doc__ = _minor_submatrix.__doc__ + + +class MatrixReductions(MatrixDeterminant): + """Provides basic matrix row/column operations. Should not be instantiated + directly. See ``reductions.py`` for some of their implementations.""" + + def echelon_form(self, iszerofunc=_iszero, simplify=False, with_pivots=False): + return _echelon_form(self, iszerofunc=iszerofunc, simplify=simplify, + with_pivots=with_pivots) + + @property + def is_echelon(self): + return _is_echelon(self) + + def rank(self, iszerofunc=_iszero, simplify=False): + return _rank(self, iszerofunc=iszerofunc, simplify=simplify) + + def rref_rhs(self, rhs): + """Return reduced row-echelon form of matrix, matrix showing + rhs after reduction steps. ``rhs`` must have the same number + of rows as ``self``. + + Examples + ======== + + >>> from sympy import Matrix, symbols + >>> r1, r2 = symbols('r1 r2') + >>> Matrix([[1, 1], [2, 1]]).rref_rhs(Matrix([r1, r2])) + (Matrix([ + [1, 0], + [0, 1]]), Matrix([ + [ -r1 + r2], + [2*r1 - r2]])) + """ + r, _ = _rref(self.hstack(self, self.eye(self.rows), rhs)) + return r[:, :self.cols], r[:, -rhs.cols:] + + def rref(self, iszerofunc=_iszero, simplify=False, pivots=True, + normalize_last=True): + return _rref(self, iszerofunc=iszerofunc, simplify=simplify, + pivots=pivots, normalize_last=normalize_last) + + echelon_form.__doc__ = _echelon_form.__doc__ + is_echelon.__doc__ = _is_echelon.__doc__ + rank.__doc__ = _rank.__doc__ + rref.__doc__ = _rref.__doc__ + + def _normalize_op_args(self, op, col, k, col1, col2, error_str="col"): + """Validate the arguments for a row/column operation. ``error_str`` + can be one of "row" or "col" depending on the arguments being parsed.""" + if op not in ["n->kn", "n<->m", "n->n+km"]: + raise ValueError("Unknown {} operation '{}'. Valid col operations " + "are 'n->kn', 'n<->m', 'n->n+km'".format(error_str, op)) + + # define self_col according to error_str + self_cols = self.cols if error_str == 'col' else self.rows + + # normalize and validate the arguments + if op == "n->kn": + col = col if col is not None else col1 + if col is None or k is None: + raise ValueError("For a {0} operation 'n->kn' you must provide the " + "kwargs `{0}` and `k`".format(error_str)) + if not 0 <= col < self_cols: + raise ValueError("This matrix does not have a {} '{}'".format(error_str, col)) + + elif op == "n<->m": + # we need two cols to swap. It does not matter + # how they were specified, so gather them together and + # remove `None` + cols = {col, k, col1, col2}.difference([None]) + if len(cols) > 2: + # maybe the user left `k` by mistake? + cols = {col, col1, col2}.difference([None]) + if len(cols) != 2: + raise ValueError("For a {0} operation 'n<->m' you must provide the " + "kwargs `{0}1` and `{0}2`".format(error_str)) + col1, col2 = cols + if not 0 <= col1 < self_cols: + raise ValueError("This matrix does not have a {} '{}'".format(error_str, col1)) + if not 0 <= col2 < self_cols: + raise ValueError("This matrix does not have a {} '{}'".format(error_str, col2)) + + elif op == "n->n+km": + col = col1 if col is None else col + col2 = col1 if col2 is None else col2 + if col is None or col2 is None or k is None: + raise ValueError("For a {0} operation 'n->n+km' you must provide the " + "kwargs `{0}`, `k`, and `{0}2`".format(error_str)) + if col == col2: + raise ValueError("For a {0} operation 'n->n+km' `{0}` and `{0}2` must " + "be different.".format(error_str)) + if not 0 <= col < self_cols: + raise ValueError("This matrix does not have a {} '{}'".format(error_str, col)) + if not 0 <= col2 < self_cols: + raise ValueError("This matrix does not have a {} '{}'".format(error_str, col2)) + + else: + raise ValueError('invalid operation %s' % repr(op)) + + return op, col, k, col1, col2 + + def _eval_col_op_multiply_col_by_const(self, col, k): + def entry(i, j): + if j == col: + return k * self[i, j] + return self[i, j] + return self._new(self.rows, self.cols, entry) + + def _eval_col_op_swap(self, col1, col2): + def entry(i, j): + if j == col1: + return self[i, col2] + elif j == col2: + return self[i, col1] + return self[i, j] + return self._new(self.rows, self.cols, entry) + + def _eval_col_op_add_multiple_to_other_col(self, col, k, col2): + def entry(i, j): + if j == col: + return self[i, j] + k * self[i, col2] + return self[i, j] + return self._new(self.rows, self.cols, entry) + + def _eval_row_op_swap(self, row1, row2): + def entry(i, j): + if i == row1: + return self[row2, j] + elif i == row2: + return self[row1, j] + return self[i, j] + return self._new(self.rows, self.cols, entry) + + def _eval_row_op_multiply_row_by_const(self, row, k): + def entry(i, j): + if i == row: + return k * self[i, j] + return self[i, j] + return self._new(self.rows, self.cols, entry) + + def _eval_row_op_add_multiple_to_other_row(self, row, k, row2): + def entry(i, j): + if i == row: + return self[i, j] + k * self[row2, j] + return self[i, j] + return self._new(self.rows, self.cols, entry) + + def elementary_col_op(self, op="n->kn", col=None, k=None, col1=None, col2=None): + """Performs the elementary column operation `op`. + + `op` may be one of + + * ``"n->kn"`` (column n goes to k*n) + * ``"n<->m"`` (swap column n and column m) + * ``"n->n+km"`` (column n goes to column n + k*column m) + + Parameters + ========== + + op : string; the elementary row operation + col : the column to apply the column operation + k : the multiple to apply in the column operation + col1 : one column of a column swap + col2 : second column of a column swap or column "m" in the column operation + "n->n+km" + """ + + op, col, k, col1, col2 = self._normalize_op_args(op, col, k, col1, col2, "col") + + # now that we've validated, we're all good to dispatch + if op == "n->kn": + return self._eval_col_op_multiply_col_by_const(col, k) + if op == "n<->m": + return self._eval_col_op_swap(col1, col2) + if op == "n->n+km": + return self._eval_col_op_add_multiple_to_other_col(col, k, col2) + + def elementary_row_op(self, op="n->kn", row=None, k=None, row1=None, row2=None): + """Performs the elementary row operation `op`. + + `op` may be one of + + * ``"n->kn"`` (row n goes to k*n) + * ``"n<->m"`` (swap row n and row m) + * ``"n->n+km"`` (row n goes to row n + k*row m) + + Parameters + ========== + + op : string; the elementary row operation + row : the row to apply the row operation + k : the multiple to apply in the row operation + row1 : one row of a row swap + row2 : second row of a row swap or row "m" in the row operation + "n->n+km" + """ + + op, row, k, row1, row2 = self._normalize_op_args(op, row, k, row1, row2, "row") + + # now that we've validated, we're all good to dispatch + if op == "n->kn": + return self._eval_row_op_multiply_row_by_const(row, k) + if op == "n<->m": + return self._eval_row_op_swap(row1, row2) + if op == "n->n+km": + return self._eval_row_op_add_multiple_to_other_row(row, k, row2) + + +class MatrixSubspaces(MatrixReductions): + """Provides methods relating to the fundamental subspaces of a matrix. + Should not be instantiated directly. See ``subspaces.py`` for their + implementations.""" + + def columnspace(self, simplify=False): + return _columnspace(self, simplify=simplify) + + def nullspace(self, simplify=False, iszerofunc=_iszero): + return _nullspace(self, simplify=simplify, iszerofunc=iszerofunc) + + def rowspace(self, simplify=False): + return _rowspace(self, simplify=simplify) + + # This is a classmethod but is converted to such later in order to allow + # assignment of __doc__ since that does not work for already wrapped + # classmethods in Python 3.6. + def orthogonalize(cls, *vecs, **kwargs): + return _orthogonalize(cls, *vecs, **kwargs) + + columnspace.__doc__ = _columnspace.__doc__ + nullspace.__doc__ = _nullspace.__doc__ + rowspace.__doc__ = _rowspace.__doc__ + orthogonalize.__doc__ = _orthogonalize.__doc__ + + orthogonalize = classmethod(orthogonalize) # type:ignore + + +class MatrixEigen(MatrixSubspaces): + """Provides basic matrix eigenvalue/vector operations. + Should not be instantiated directly. See ``eigen.py`` for their + implementations.""" + + def eigenvals(self, error_when_incomplete=True, **flags): + return _eigenvals(self, error_when_incomplete=error_when_incomplete, **flags) + + def eigenvects(self, error_when_incomplete=True, iszerofunc=_iszero, **flags): + return _eigenvects(self, error_when_incomplete=error_when_incomplete, + iszerofunc=iszerofunc, **flags) + + def is_diagonalizable(self, reals_only=False, **kwargs): + return _is_diagonalizable(self, reals_only=reals_only, **kwargs) + + def diagonalize(self, reals_only=False, sort=False, normalize=False): + return _diagonalize(self, reals_only=reals_only, sort=sort, + normalize=normalize) + + def bidiagonalize(self, upper=True): + return _bidiagonalize(self, upper=upper) + + def bidiagonal_decomposition(self, upper=True): + return _bidiagonal_decomposition(self, upper=upper) + + @property + def is_positive_definite(self): + return _is_positive_definite(self) + + @property + def is_positive_semidefinite(self): + return _is_positive_semidefinite(self) + + @property + def is_negative_definite(self): + return _is_negative_definite(self) + + @property + def is_negative_semidefinite(self): + return _is_negative_semidefinite(self) + + @property + def is_indefinite(self): + return _is_indefinite(self) + + def jordan_form(self, calc_transform=True, **kwargs): + return _jordan_form(self, calc_transform=calc_transform, **kwargs) + + def left_eigenvects(self, **flags): + return _left_eigenvects(self, **flags) + + def singular_values(self): + return _singular_values(self) + + eigenvals.__doc__ = _eigenvals.__doc__ + eigenvects.__doc__ = _eigenvects.__doc__ + is_diagonalizable.__doc__ = _is_diagonalizable.__doc__ + diagonalize.__doc__ = _diagonalize.__doc__ + is_positive_definite.__doc__ = _is_positive_definite.__doc__ + is_positive_semidefinite.__doc__ = _is_positive_semidefinite.__doc__ + is_negative_definite.__doc__ = _is_negative_definite.__doc__ + is_negative_semidefinite.__doc__ = _is_negative_semidefinite.__doc__ + is_indefinite.__doc__ = _is_indefinite.__doc__ + jordan_form.__doc__ = _jordan_form.__doc__ + left_eigenvects.__doc__ = _left_eigenvects.__doc__ + singular_values.__doc__ = _singular_values.__doc__ + bidiagonalize.__doc__ = _bidiagonalize.__doc__ + bidiagonal_decomposition.__doc__ = _bidiagonal_decomposition.__doc__ + + +class MatrixCalculus(MatrixCommon): + """Provides calculus-related matrix operations.""" + + def diff(self, *args, evaluate=True, **kwargs): + """Calculate the derivative of each element in the matrix. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import x, y + >>> M = Matrix([[x, y], [1, 0]]) + >>> M.diff(x) + Matrix([ + [1, 0], + [0, 0]]) + + See Also + ======== + + integrate + limit + """ + # XXX this should be handled here rather than in Derivative + from sympy.tensor.array.array_derivatives import ArrayDerivative + deriv = ArrayDerivative(self, *args, evaluate=evaluate) + # XXX This can rather changed to always return immutable matrix + if not isinstance(self, Basic) and evaluate: + return deriv.as_mutable() + return deriv + + def _eval_derivative(self, arg): + return self.applyfunc(lambda x: x.diff(arg)) + + def integrate(self, *args, **kwargs): + """Integrate each element of the matrix. ``args`` will + be passed to the ``integrate`` function. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import x, y + >>> M = Matrix([[x, y], [1, 0]]) + >>> M.integrate((x, )) + Matrix([ + [x**2/2, x*y], + [ x, 0]]) + >>> M.integrate((x, 0, 2)) + Matrix([ + [2, 2*y], + [2, 0]]) + + See Also + ======== + + limit + diff + """ + return self.applyfunc(lambda x: x.integrate(*args, **kwargs)) + + def jacobian(self, X): + """Calculates the Jacobian matrix (derivative of a vector-valued function). + + Parameters + ========== + + ``self`` : vector of expressions representing functions f_i(x_1, ..., x_n). + X : set of x_i's in order, it can be a list or a Matrix + + Both ``self`` and X can be a row or a column matrix in any order + (i.e., jacobian() should always work). + + Examples + ======== + + >>> from sympy import sin, cos, Matrix + >>> from sympy.abc import rho, phi + >>> X = Matrix([rho*cos(phi), rho*sin(phi), rho**2]) + >>> Y = Matrix([rho, phi]) + >>> X.jacobian(Y) + Matrix([ + [cos(phi), -rho*sin(phi)], + [sin(phi), rho*cos(phi)], + [ 2*rho, 0]]) + >>> X = Matrix([rho*cos(phi), rho*sin(phi)]) + >>> X.jacobian(Y) + Matrix([ + [cos(phi), -rho*sin(phi)], + [sin(phi), rho*cos(phi)]]) + + See Also + ======== + + hessian + wronskian + """ + if not isinstance(X, MatrixBase): + X = self._new(X) + # Both X and ``self`` can be a row or a column matrix, so we need to make + # sure all valid combinations work, but everything else fails: + if self.shape[0] == 1: + m = self.shape[1] + elif self.shape[1] == 1: + m = self.shape[0] + else: + raise TypeError("``self`` must be a row or a column matrix") + if X.shape[0] == 1: + n = X.shape[1] + elif X.shape[1] == 1: + n = X.shape[0] + else: + raise TypeError("X must be a row or a column matrix") + + # m is the number of functions and n is the number of variables + # computing the Jacobian is now easy: + return self._new(m, n, lambda j, i: self[j].diff(X[i])) + + def limit(self, *args): + """Calculate the limit of each element in the matrix. + ``args`` will be passed to the ``limit`` function. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import x, y + >>> M = Matrix([[x, y], [1, 0]]) + >>> M.limit(x, 2) + Matrix([ + [2, y], + [1, 0]]) + + See Also + ======== + + integrate + diff + """ + return self.applyfunc(lambda x: x.limit(*args)) + + +# https://github.com/sympy/sympy/pull/12854 +class MatrixDeprecated(MatrixCommon): + """A class to house deprecated matrix methods.""" + def berkowitz_charpoly(self, x=Dummy('lambda'), simplify=_simplify): + return self.charpoly(x=x) + + def berkowitz_det(self): + """Computes determinant using Berkowitz method. + + See Also + ======== + + det + berkowitz + """ + return self.det(method='berkowitz') + + def berkowitz_eigenvals(self, **flags): + """Computes eigenvalues of a Matrix using Berkowitz method. + + See Also + ======== + + berkowitz + """ + return self.eigenvals(**flags) + + def berkowitz_minors(self): + """Computes principal minors using Berkowitz method. + + See Also + ======== + + berkowitz + """ + sign, minors = self.one, [] + + for poly in self.berkowitz(): + minors.append(sign * poly[-1]) + sign = -sign + + return tuple(minors) + + def berkowitz(self): + from sympy.matrices import zeros + berk = ((1,),) + if not self: + return berk + + if not self.is_square: + raise NonSquareMatrixError() + + A, N = self, self.rows + transforms = [0] * (N - 1) + + for n in range(N, 1, -1): + T, k = zeros(n + 1, n), n - 1 + + R, C = -A[k, :k], A[:k, k] + A, a = A[:k, :k], -A[k, k] + + items = [C] + + for i in range(0, n - 2): + items.append(A * items[i]) + + for i, B in enumerate(items): + items[i] = (R * B)[0, 0] + + items = [self.one, a] + items + + for i in range(n): + T[i:, i] = items[:n - i + 1] + + transforms[k - 1] = T + + polys = [self._new([self.one, -A[0, 0]])] + + for i, T in enumerate(transforms): + polys.append(T * polys[i]) + + return berk + tuple(map(tuple, polys)) + + def cofactorMatrix(self, method="berkowitz"): + return self.cofactor_matrix(method=method) + + def det_bareis(self): + return _det_bareiss(self) + + def det_LU_decomposition(self): + """Compute matrix determinant using LU decomposition. + + + Note that this method fails if the LU decomposition itself + fails. In particular, if the matrix has no inverse this method + will fail. + + TODO: Implement algorithm for sparse matrices (SFF), + https://www.eecis.udel.edu/~saunders/papers/sffge/it5.ps + + See Also + ======== + + + det + det_bareiss + berkowitz_det + """ + return self.det(method='lu') + + def jordan_cell(self, eigenval, n): + return self.jordan_block(size=n, eigenvalue=eigenval) + + def jordan_cells(self, calc_transformation=True): + P, J = self.jordan_form() + return P, J.get_diag_blocks() + + def minorEntry(self, i, j, method="berkowitz"): + return self.minor(i, j, method=method) + + def minorMatrix(self, i, j): + return self.minor_submatrix(i, j) + + def permuteBkwd(self, perm): + """Permute the rows of the matrix with the given permutation in reverse.""" + return self.permute_rows(perm, direction='backward') + + def permuteFwd(self, perm): + """Permute the rows of the matrix with the given permutation.""" + return self.permute_rows(perm, direction='forward') diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/matrixbase.py b/venv/lib/python3.11/site-packages/sympy/matrices/matrixbase.py new file mode 100644 index 0000000000000000000000000000000000000000..49acc04043b30e003f7eed256f2e06e6a6556401 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/matrixbase.py @@ -0,0 +1,5428 @@ +from __future__ import annotations +from collections import defaultdict +from collections.abc import Iterable +from inspect import isfunction +from functools import reduce + +from sympy.assumptions.refine import refine +from sympy.core import SympifyError, Add +from sympy.core.basic import Atom, Basic +from sympy.core.kind import UndefinedKind +from sympy.core.numbers import Integer +from sympy.core.mod import Mod +from sympy.core.symbol import Symbol, Dummy +from sympy.core.sympify import sympify, _sympify +from sympy.core.function import diff +from sympy.polys import cancel +from sympy.functions.elementary.complexes import Abs, re, im +from sympy.printing import sstr +from sympy.functions.elementary.miscellaneous import Max, Min, sqrt +from sympy.functions.special.tensor_functions import KroneckerDelta, LeviCivita +from sympy.core.singleton import S +from sympy.printing.defaults import Printable +from sympy.printing.str import StrPrinter +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.combinatorial.factorials import binomial, factorial + +import mpmath as mp +from collections.abc import Callable +from sympy.utilities.iterables import reshape +from sympy.core.expr import Expr +from sympy.core.power import Pow +from sympy.core.symbol import uniquely_named_symbol + +from .utilities import _dotprodsimp, _simplify as _utilities_simplify +from sympy.polys.polytools import Poly +from sympy.utilities.iterables import flatten, is_sequence +from sympy.utilities.misc import as_int, filldedent +from sympy.core.decorators import call_highest_priority +from sympy.core.logic import fuzzy_and, FuzzyBool +from sympy.tensor.array import NDimArray +from sympy.utilities.iterables import NotIterable + +from .utilities import _get_intermediate_simp_bool + +from .kind import MatrixKind + +from .exceptions import ( + MatrixError, ShapeError, NonSquareMatrixError, NonInvertibleMatrixError, +) + +from .utilities import _iszero, _is_zero_after_expand_mul + +from .determinant import ( + _find_reasonable_pivot, _find_reasonable_pivot_naive, + _adjugate, _charpoly, _cofactor, _cofactor_matrix, _per, + _det, _det_bareiss, _det_berkowitz, _det_bird, _det_laplace, _det_LU, + _minor, _minor_submatrix) + +from .reductions import _is_echelon, _echelon_form, _rank, _rref + +from .solvers import ( + _diagonal_solve, _lower_triangular_solve, _upper_triangular_solve, + _cholesky_solve, _LDLsolve, _LUsolve, _QRsolve, _gauss_jordan_solve, + _pinv_solve, _cramer_solve, _solve, _solve_least_squares) + +from .inverse import ( + _pinv, _inv_ADJ, _inv_GE, _inv_LU, _inv_CH, _inv_LDL, _inv_QR, + _inv, _inv_block) + +from .subspaces import _columnspace, _nullspace, _rowspace, _orthogonalize + +from .eigen import ( + _eigenvals, _eigenvects, + _bidiagonalize, _bidiagonal_decomposition, + _is_diagonalizable, _diagonalize, + _is_positive_definite, _is_positive_semidefinite, + _is_negative_definite, _is_negative_semidefinite, _is_indefinite, + _jordan_form, _left_eigenvects, _singular_values) + +from .decompositions import ( + _rank_decomposition, _cholesky, _LDLdecomposition, + _LUdecomposition, _LUdecomposition_Simple, _LUdecompositionFF, + _singular_value_decomposition, _QRdecomposition, _upper_hessenberg_decomposition) + +from .graph import ( + _connected_components, _connected_components_decomposition, + _strongly_connected_components, _strongly_connected_components_decomposition) + + +__doctest_requires__ = { + ('MatrixBase.is_indefinite', + 'MatrixBase.is_positive_definite', + 'MatrixBase.is_positive_semidefinite', + 'MatrixBase.is_negative_definite', + 'MatrixBase.is_negative_semidefinite'): ['matplotlib'], +} + + +class MatrixBase(Printable): + """All common matrix operations including basic arithmetic, shaping, + and special matrices like `zeros`, and `eye`.""" + + _op_priority = 10.01 + + # Added just for numpy compatibility + __array_priority__ = 11 + + is_Matrix = True + _class_priority = 3 + _sympify = staticmethod(sympify) + zero = S.Zero + one = S.One + + _diff_wrt: bool = True + rows: int + cols: int + _simplify = None + + @classmethod + def _new(cls, *args, **kwargs): + """`_new` must, at minimum, be callable as + `_new(rows, cols, mat) where mat is a flat list of the + elements of the matrix.""" + raise NotImplementedError("Subclasses must implement this.") + + def __eq__(self, other): + raise NotImplementedError("Subclasses must implement this.") + + def __getitem__(self, key): + """Implementations of __getitem__ should accept ints, in which + case the matrix is indexed as a flat list, tuples (i,j) in which + case the (i,j) entry is returned, slices, or mixed tuples (a,b) + where a and b are any combination of slices and integers.""" + raise NotImplementedError("Subclasses must implement this.") + + @property + def shape(self): + """The shape (dimensions) of the matrix as the 2-tuple (rows, cols). + + Examples + ======== + + >>> from sympy import zeros + >>> M = zeros(2, 3) + >>> M.shape + (2, 3) + >>> M.rows + 2 + >>> M.cols + 3 + """ + return (self.rows, self.cols) + + def _eval_col_del(self, col): + def entry(i, j): + return self[i, j] if j < col else self[i, j + 1] + return self._new(self.rows, self.cols - 1, entry) + + def _eval_col_insert(self, pos, other): + + def entry(i, j): + if j < pos: + return self[i, j] + elif pos <= j < pos + other.cols: + return other[i, j - pos] + return self[i, j - other.cols] + + return self._new(self.rows, self.cols + other.cols, entry) + + def _eval_col_join(self, other): + rows = self.rows + + def entry(i, j): + if i < rows: + return self[i, j] + return other[i - rows, j] + + return classof(self, other)._new(self.rows + other.rows, self.cols, + entry) + + def _eval_extract(self, rowsList, colsList): + mat = list(self) + cols = self.cols + indices = (i * cols + j for i in rowsList for j in colsList) + return self._new(len(rowsList), len(colsList), + [mat[i] for i in indices]) + + def _eval_get_diag_blocks(self): + sub_blocks = [] + + def recurse_sub_blocks(M): + for i in range(1, M.shape[0] + 1): + if i == 1: + to_the_right = M[0, i:] + to_the_bottom = M[i:, 0] + else: + to_the_right = M[:i, i:] + to_the_bottom = M[i:, :i] + if any(to_the_right) or any(to_the_bottom): + continue + sub_blocks.append(M[:i, :i]) + if M.shape != M[:i, :i].shape: + recurse_sub_blocks(M[i:, i:]) + return + + recurse_sub_blocks(self) + return sub_blocks + + def _eval_row_del(self, row): + def entry(i, j): + return self[i, j] if i < row else self[i + 1, j] + return self._new(self.rows - 1, self.cols, entry) + + def _eval_row_insert(self, pos, other): + entries = list(self) + insert_pos = pos * self.cols + entries[insert_pos:insert_pos] = list(other) + return self._new(self.rows + other.rows, self.cols, entries) + + def _eval_row_join(self, other): + cols = self.cols + + def entry(i, j): + if j < cols: + return self[i, j] + return other[i, j - cols] + + return classof(self, other)._new(self.rows, self.cols + other.cols, + entry) + + def _eval_tolist(self): + return [list(self[i,:]) for i in range(self.rows)] + + def _eval_todok(self): + dok = {} + rows, cols = self.shape + for i in range(rows): + for j in range(cols): + val = self[i, j] + if val != self.zero: + dok[i, j] = val + return dok + + @classmethod + def _eval_from_dok(cls, rows, cols, dok): + out_flat = [cls.zero] * (rows * cols) + for (i, j), val in dok.items(): + out_flat[i * cols + j] = val + return cls._new(rows, cols, out_flat) + + def _eval_vec(self): + rows = self.rows + + def entry(n, _): + # we want to read off the columns first + j = n // rows + i = n - j * rows + return self[i, j] + + return self._new(len(self), 1, entry) + + def _eval_vech(self, diagonal): + c = self.cols + v = [] + if diagonal: + for j in range(c): + for i in range(j, c): + v.append(self[i, j]) + else: + for j in range(c): + for i in range(j + 1, c): + v.append(self[i, j]) + return self._new(len(v), 1, v) + + def col_del(self, col): + """Delete the specified column.""" + if col < 0: + col += self.cols + if not 0 <= col < self.cols: + raise IndexError("Column {} is out of range.".format(col)) + return self._eval_col_del(col) + + def col_insert(self, pos, other): + """Insert one or more columns at the given column position. + + Examples + ======== + + >>> from sympy import zeros, ones + >>> M = zeros(3) + >>> V = ones(3, 1) + >>> M.col_insert(1, V) + Matrix([ + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0]]) + + See Also + ======== + + col + row_insert + """ + # Allows you to build a matrix even if it is null matrix + if not self: + return type(self)(other) + + pos = as_int(pos) + + if pos < 0: + pos = self.cols + pos + if pos < 0: + pos = 0 + elif pos > self.cols: + pos = self.cols + + if self.rows != other.rows: + raise ShapeError( + "The matrices have incompatible number of rows ({} and {})" + .format(self.rows, other.rows)) + + return self._eval_col_insert(pos, other) + + def col_join(self, other): + """Concatenates two matrices along self's last and other's first row. + + Examples + ======== + + >>> from sympy import zeros, ones + >>> M = zeros(3) + >>> V = ones(1, 3) + >>> M.col_join(V) + Matrix([ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [1, 1, 1]]) + + See Also + ======== + + col + row_join + """ + # A null matrix can always be stacked (see #10770) + if self.rows == 0 and self.cols != other.cols: + return self._new(0, other.cols, []).col_join(other) + + if self.cols != other.cols: + raise ShapeError( + "The matrices have incompatible number of columns ({} and {})" + .format(self.cols, other.cols)) + return self._eval_col_join(other) + + def col(self, j): + """Elementary column selector. + + Examples + ======== + + >>> from sympy import eye + >>> eye(2).col(0) + Matrix([ + [1], + [0]]) + + See Also + ======== + + row + col_del + col_join + col_insert + """ + return self[:, j] + + def extract(self, rowsList, colsList): + r"""Return a submatrix by specifying a list of rows and columns. + Negative indices can be given. All indices must be in the range + $-n \le i < n$ where $n$ is the number of rows or columns. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix(4, 3, range(12)) + >>> m + Matrix([ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [9, 10, 11]]) + >>> m.extract([0, 1, 3], [0, 1]) + Matrix([ + [0, 1], + [3, 4], + [9, 10]]) + + Rows or columns can be repeated: + + >>> m.extract([0, 0, 1], [-1]) + Matrix([ + [2], + [2], + [5]]) + + Every other row can be taken by using range to provide the indices: + + >>> m.extract(range(0, m.rows, 2), [-1]) + Matrix([ + [2], + [8]]) + + RowsList or colsList can also be a list of booleans, in which case + the rows or columns corresponding to the True values will be selected: + + >>> m.extract([0, 1, 2, 3], [True, False, True]) + Matrix([ + [0, 2], + [3, 5], + [6, 8], + [9, 11]]) + """ + + if not is_sequence(rowsList) or not is_sequence(colsList): + raise TypeError("rowsList and colsList must be iterable") + # ensure rowsList and colsList are lists of integers + if rowsList and all(isinstance(i, bool) for i in rowsList): + rowsList = [index for index, item in enumerate(rowsList) if item] + if colsList and all(isinstance(i, bool) for i in colsList): + colsList = [index for index, item in enumerate(colsList) if item] + + # ensure everything is in range + rowsList = [a2idx(k, self.rows) for k in rowsList] + colsList = [a2idx(k, self.cols) for k in colsList] + + return self._eval_extract(rowsList, colsList) + + def get_diag_blocks(self): + """Obtains the square sub-matrices on the main diagonal of a square matrix. + + Useful for inverting symbolic matrices or solving systems of + linear equations which may be decoupled by having a block diagonal + structure. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import x, y, z + >>> A = Matrix([[1, 3, 0, 0], [y, z*z, 0, 0], [0, 0, x, 0], [0, 0, 0, 0]]) + >>> a1, a2, a3 = A.get_diag_blocks() + >>> a1 + Matrix([ + [1, 3], + [y, z**2]]) + >>> a2 + Matrix([[x]]) + >>> a3 + Matrix([[0]]) + + """ + return self._eval_get_diag_blocks() + + @classmethod + def hstack(cls, *args): + """Return a matrix formed by joining args horizontally (i.e. + by repeated application of row_join). + + Examples + ======== + + >>> from sympy import Matrix, eye + >>> Matrix.hstack(eye(2), 2*eye(2)) + Matrix([ + [1, 0, 2, 0], + [0, 1, 0, 2]]) + """ + if len(args) == 0: + return cls._new() + + kls = type(args[0]) + return reduce(kls.row_join, args) + + def reshape(self, rows, cols): + """Reshape the matrix. Total number of elements must remain the same. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix(2, 3, lambda i, j: 1) + >>> m + Matrix([ + [1, 1, 1], + [1, 1, 1]]) + >>> m.reshape(1, 6) + Matrix([[1, 1, 1, 1, 1, 1]]) + >>> m.reshape(3, 2) + Matrix([ + [1, 1], + [1, 1], + [1, 1]]) + + """ + if self.rows * self.cols != rows * cols: + raise ValueError("Invalid reshape parameters %d %d" % (rows, cols)) + dok = {divmod(i*self.cols + j, cols): + v for (i, j), v in self.todok().items()} + return self._eval_from_dok(rows, cols, dok) + + def row_del(self, row): + """Delete the specified row.""" + if row < 0: + row += self.rows + if not 0 <= row < self.rows: + raise IndexError("Row {} is out of range.".format(row)) + + return self._eval_row_del(row) + + def row_insert(self, pos, other): + """Insert one or more rows at the given row position. + + Examples + ======== + + >>> from sympy import zeros, ones + >>> M = zeros(3) + >>> V = ones(1, 3) + >>> M.row_insert(1, V) + Matrix([ + [0, 0, 0], + [1, 1, 1], + [0, 0, 0], + [0, 0, 0]]) + + See Also + ======== + + row + col_insert + """ + # Allows you to build a matrix even if it is null matrix + if not self: + return self._new(other) + + pos = as_int(pos) + + if pos < 0: + pos = self.rows + pos + if pos < 0: + pos = 0 + elif pos > self.rows: + pos = self.rows + + if self.cols != other.cols: + raise ShapeError( + "The matrices have incompatible number of columns ({} and {})" + .format(self.cols, other.cols)) + + return self._eval_row_insert(pos, other) + + def row_join(self, other): + """Concatenates two matrices along self's last and rhs's first column + + Examples + ======== + + >>> from sympy import zeros, ones + >>> M = zeros(3) + >>> V = ones(3, 1) + >>> M.row_join(V) + Matrix([ + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1]]) + + See Also + ======== + + row + col_join + """ + # A null matrix can always be stacked (see #10770) + if self.cols == 0 and self.rows != other.rows: + return self._new(other.rows, 0, []).row_join(other) + + if self.rows != other.rows: + raise ShapeError( + "The matrices have incompatible number of rows ({} and {})" + .format(self.rows, other.rows)) + return self._eval_row_join(other) + + def diagonal(self, k=0): + """Returns the kth diagonal of self. The main diagonal + corresponds to `k=0`; diagonals above and below correspond to + `k > 0` and `k < 0`, respectively. The values of `self[i, j]` + for which `j - i = k`, are returned in order of increasing + `i + j`, starting with `i + j = |k|`. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix(3, 3, lambda i, j: j - i); m + Matrix([ + [ 0, 1, 2], + [-1, 0, 1], + [-2, -1, 0]]) + >>> _.diagonal() + Matrix([[0, 0, 0]]) + >>> m.diagonal(1) + Matrix([[1, 1]]) + >>> m.diagonal(-2) + Matrix([[-2]]) + + Even though the diagonal is returned as a Matrix, the element + retrieval can be done with a single index: + + >>> Matrix.diag(1, 2, 3).diagonal()[1] # instead of [0, 1] + 2 + + See Also + ======== + + diag + """ + rv = [] + k = as_int(k) + r = 0 if k > 0 else -k + c = 0 if r else k + while True: + if r == self.rows or c == self.cols: + break + rv.append(self[r, c]) + r += 1 + c += 1 + if not rv: + raise ValueError(filldedent(''' + The %s diagonal is out of range [%s, %s]''' % ( + k, 1 - self.rows, self.cols - 1))) + return self._new(1, len(rv), rv) + + def row(self, i): + """Elementary row selector. + + Examples + ======== + + >>> from sympy import eye + >>> eye(2).row(0) + Matrix([[1, 0]]) + + See Also + ======== + + col + row_del + row_join + row_insert + """ + return self[i, :] + + def todok(self): + """Return the matrix as dictionary of keys. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix.eye(3) + >>> M.todok() + {(0, 0): 1, (1, 1): 1, (2, 2): 1} + """ + return self._eval_todok() + + @classmethod + def from_dok(cls, rows, cols, dok): + """Create a matrix from a dictionary of keys. + + Examples + ======== + + >>> from sympy import Matrix + >>> d = {(0, 0): 1, (1, 2): 3, (2, 1): 4} + >>> Matrix.from_dok(3, 3, d) + Matrix([ + [1, 0, 0], + [0, 0, 3], + [0, 4, 0]]) + """ + dok = {ij: cls._sympify(val) for ij, val in dok.items()} + return cls._eval_from_dok(rows, cols, dok) + + def tolist(self): + """Return the Matrix as a nested Python list. + + Examples + ======== + + >>> from sympy import Matrix, ones + >>> m = Matrix(3, 3, range(9)) + >>> m + Matrix([ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8]]) + >>> m.tolist() + [[0, 1, 2], [3, 4, 5], [6, 7, 8]] + >>> ones(3, 0).tolist() + [[], [], []] + + When there are no rows then it will not be possible to tell how + many columns were in the original matrix: + + >>> ones(0, 3).tolist() + [] + + """ + if not self.rows: + return [] + if not self.cols: + return [[] for i in range(self.rows)] + return self._eval_tolist() + + def todod(M): + """Returns matrix as dict of dicts containing non-zero elements of the Matrix + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([[0, 1],[0, 3]]) + >>> A + Matrix([ + [0, 1], + [0, 3]]) + >>> A.todod() + {0: {1: 1}, 1: {1: 3}} + + + """ + rowsdict = {} + Mlol = M.tolist() + for i, Mi in enumerate(Mlol): + row = {j: Mij for j, Mij in enumerate(Mi) if Mij} + if row: + rowsdict[i] = row + return rowsdict + + def vec(self): + """Return the Matrix converted into a one column matrix by stacking columns + + Examples + ======== + + >>> from sympy import Matrix + >>> m=Matrix([[1, 3], [2, 4]]) + >>> m + Matrix([ + [1, 3], + [2, 4]]) + >>> m.vec() + Matrix([ + [1], + [2], + [3], + [4]]) + + See Also + ======== + + vech + """ + return self._eval_vec() + + def vech(self, diagonal=True, check_symmetry=True): + """Reshapes the matrix into a column vector by stacking the + elements in the lower triangle. + + Parameters + ========== + + diagonal : bool, optional + If ``True``, it includes the diagonal elements. + + check_symmetry : bool, optional + If ``True``, it checks whether the matrix is symmetric. + + Examples + ======== + + >>> from sympy import Matrix + >>> m=Matrix([[1, 2], [2, 3]]) + >>> m + Matrix([ + [1, 2], + [2, 3]]) + >>> m.vech() + Matrix([ + [1], + [2], + [3]]) + >>> m.vech(diagonal=False) + Matrix([[2]]) + + Notes + ===== + + This should work for symmetric matrices and ``vech`` can + represent symmetric matrices in vector form with less size than + ``vec``. + + See Also + ======== + + vec + """ + if not self.is_square: + raise NonSquareMatrixError + + if check_symmetry and not self.is_symmetric(): + raise ValueError("The matrix is not symmetric.") + + return self._eval_vech(diagonal) + + @classmethod + def vstack(cls, *args): + """Return a matrix formed by joining args vertically (i.e. + by repeated application of col_join). + + Examples + ======== + + >>> from sympy import Matrix, eye + >>> Matrix.vstack(eye(2), 2*eye(2)) + Matrix([ + [1, 0], + [0, 1], + [2, 0], + [0, 2]]) + """ + if len(args) == 0: + return cls._new() + + kls = type(args[0]) + return reduce(kls.col_join, args) + + @classmethod + def _eval_diag(cls, rows, cols, diag_dict): + """diag_dict is a defaultdict containing + all the entries of the diagonal matrix.""" + def entry(i, j): + return diag_dict[(i, j)] + return cls._new(rows, cols, entry) + + @classmethod + def _eval_eye(cls, rows, cols): + vals = [cls.zero]*(rows*cols) + vals[::cols+1] = [cls.one]*min(rows, cols) + return cls._new(rows, cols, vals, copy=False) + + @classmethod + def _eval_jordan_block(cls, size: int, eigenvalue, band='upper'): + if band == 'lower': + def entry(i, j): + if i == j: + return eigenvalue + elif j + 1 == i: + return cls.one + return cls.zero + else: + def entry(i, j): + if i == j: + return eigenvalue + elif i + 1 == j: + return cls.one + return cls.zero + return cls._new(size, size, entry) + + @classmethod + def _eval_ones(cls, rows, cols): + def entry(i, j): + return cls.one + return cls._new(rows, cols, entry) + + @classmethod + def _eval_zeros(cls, rows, cols): + return cls._new(rows, cols, [cls.zero]*(rows*cols), copy=False) + + @classmethod + def _eval_wilkinson(cls, n): + def entry(i, j): + return cls.one if i + 1 == j else cls.zero + + D = cls._new(2*n + 1, 2*n + 1, entry) + + wminus = cls.diag(list(range(-n, n + 1)), unpack=True) + D + D.T + wplus = abs(cls.diag(list(range(-n, n + 1)), unpack=True)) + D + D.T + + return wminus, wplus + + @classmethod + def diag(kls, *args, strict=False, unpack=True, rows=None, cols=None, **kwargs): + """Returns a matrix with the specified diagonal. + If matrices are passed, a block-diagonal matrix + is created (i.e. the "direct sum" of the matrices). + + kwargs + ====== + + rows : rows of the resulting matrix; computed if + not given. + + cols : columns of the resulting matrix; computed if + not given. + + cls : class for the resulting matrix + + unpack : bool which, when True (default), unpacks a single + sequence rather than interpreting it as a Matrix. + + strict : bool which, when False (default), allows Matrices to + have variable-length rows. + + Examples + ======== + + >>> from sympy import Matrix + >>> Matrix.diag(1, 2, 3) + Matrix([ + [1, 0, 0], + [0, 2, 0], + [0, 0, 3]]) + + The current default is to unpack a single sequence. If this is + not desired, set `unpack=False` and it will be interpreted as + a matrix. + + >>> Matrix.diag([1, 2, 3]) == Matrix.diag(1, 2, 3) + True + + When more than one element is passed, each is interpreted as + something to put on the diagonal. Lists are converted to + matrices. Filling of the diagonal always continues from + the bottom right hand corner of the previous item: this + will create a block-diagonal matrix whether the matrices + are square or not. + + >>> col = [1, 2, 3] + >>> row = [[4, 5]] + >>> Matrix.diag(col, row) + Matrix([ + [1, 0, 0], + [2, 0, 0], + [3, 0, 0], + [0, 4, 5]]) + + When `unpack` is False, elements within a list need not all be + of the same length. Setting `strict` to True would raise a + ValueError for the following: + + >>> Matrix.diag([[1, 2, 3], [4, 5], [6]], unpack=False) + Matrix([ + [1, 2, 3], + [4, 5, 0], + [6, 0, 0]]) + + The type of the returned matrix can be set with the ``cls`` + keyword. + + >>> from sympy import ImmutableMatrix + >>> from sympy.utilities.misc import func_name + >>> func_name(Matrix.diag(1, cls=ImmutableMatrix)) + 'ImmutableDenseMatrix' + + A zero dimension matrix can be used to position the start of + the filling at the start of an arbitrary row or column: + + >>> from sympy import ones + >>> r2 = ones(0, 2) + >>> Matrix.diag(r2, 1, 2) + Matrix([ + [0, 0, 1, 0], + [0, 0, 0, 2]]) + + See Also + ======== + eye + diagonal + .dense.diag + .expressions.blockmatrix.BlockMatrix + .sparsetools.banded + """ + from sympy.matrices.matrixbase import MatrixBase + from sympy.matrices.dense import Matrix + from sympy.matrices import SparseMatrix + klass = kwargs.get('cls', kls) + if unpack and len(args) == 1 and is_sequence(args[0]) and \ + not isinstance(args[0], MatrixBase): + args = args[0] + + # fill a default dict with the diagonal entries + diag_entries = defaultdict(int) + rmax = cmax = 0 # keep track of the biggest index seen + for m in args: + if isinstance(m, list): + if strict: + # if malformed, Matrix will raise an error + _ = Matrix(m) + r, c = _.shape + m = _.tolist() + else: + r, c, smat = SparseMatrix._handle_creation_inputs(m) + for (i, j), _ in smat.items(): + diag_entries[(i + rmax, j + cmax)] = _ + m = [] # to skip process below + elif hasattr(m, 'shape'): # a Matrix + # convert to list of lists + r, c = m.shape + m = m.tolist() + else: # in this case, we're a single value + diag_entries[(rmax, cmax)] = m + rmax += 1 + cmax += 1 + continue + # process list of lists + for i, mi in enumerate(m): + for j, _ in enumerate(mi): + diag_entries[(i + rmax, j + cmax)] = _ + rmax += r + cmax += c + if rows is None: + rows, cols = cols, rows + if rows is None: + rows, cols = rmax, cmax + else: + cols = rows if cols is None else cols + if rows < rmax or cols < cmax: + raise ValueError(filldedent(''' + The constructed matrix is {} x {} but a size of {} x {} + was specified.'''.format(rmax, cmax, rows, cols))) + return klass._eval_diag(rows, cols, diag_entries) + + @classmethod + def eye(kls, rows, cols=None, **kwargs): + """Returns an identity matrix. + + Parameters + ========== + + rows : rows of the matrix + cols : cols of the matrix (if None, cols=rows) + + kwargs + ====== + cls : class of the returned matrix + """ + if cols is None: + cols = rows + if rows < 0 or cols < 0: + raise ValueError("Cannot create a {} x {} matrix. " + "Both dimensions must be positive".format(rows, cols)) + klass = kwargs.get('cls', kls) + rows, cols = as_int(rows), as_int(cols) + + return klass._eval_eye(rows, cols) + + @classmethod + def jordan_block(kls, size=None, eigenvalue=None, *, band='upper', **kwargs): + """Returns a Jordan block + + Parameters + ========== + + size : Integer, optional + Specifies the shape of the Jordan block matrix. + + eigenvalue : Number or Symbol + Specifies the value for the main diagonal of the matrix. + + .. note:: + The keyword ``eigenval`` is also specified as an alias + of this keyword, but it is not recommended to use. + + We may deprecate the alias in later release. + + band : 'upper' or 'lower', optional + Specifies the position of the off-diagonal to put `1` s on. + + cls : Matrix, optional + Specifies the matrix class of the output form. + + If it is not specified, the class type where the method is + being executed on will be returned. + + Returns + ======= + + Matrix + A Jordan block matrix. + + Raises + ====== + + ValueError + If insufficient arguments are given for matrix size + specification, or no eigenvalue is given. + + Examples + ======== + + Creating a default Jordan block: + + >>> from sympy import Matrix + >>> from sympy.abc import x + >>> Matrix.jordan_block(4, x) + Matrix([ + [x, 1, 0, 0], + [0, x, 1, 0], + [0, 0, x, 1], + [0, 0, 0, x]]) + + Creating an alternative Jordan block matrix where `1` is on + lower off-diagonal: + + >>> Matrix.jordan_block(4, x, band='lower') + Matrix([ + [x, 0, 0, 0], + [1, x, 0, 0], + [0, 1, x, 0], + [0, 0, 1, x]]) + + Creating a Jordan block with keyword arguments + + >>> Matrix.jordan_block(size=4, eigenvalue=x) + Matrix([ + [x, 1, 0, 0], + [0, x, 1, 0], + [0, 0, x, 1], + [0, 0, 0, x]]) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Jordan_matrix + """ + klass = kwargs.pop('cls', kls) + + eigenval = kwargs.get('eigenval', None) + if eigenvalue is None and eigenval is None: + raise ValueError("Must supply an eigenvalue") + elif eigenvalue != eigenval and None not in (eigenval, eigenvalue): + raise ValueError( + "Inconsistent values are given: 'eigenval'={}, " + "'eigenvalue'={}".format(eigenval, eigenvalue)) + else: + if eigenval is not None: + eigenvalue = eigenval + + if size is None: + raise ValueError("Must supply a matrix size") + + size = as_int(size) + return klass._eval_jordan_block(size, eigenvalue, band) + + @classmethod + def ones(kls, rows, cols=None, **kwargs): + """Returns a matrix of ones. + + Parameters + ========== + + rows : rows of the matrix + cols : cols of the matrix (if None, cols=rows) + + kwargs + ====== + cls : class of the returned matrix + """ + if cols is None: + cols = rows + klass = kwargs.get('cls', kls) + rows, cols = as_int(rows), as_int(cols) + + return klass._eval_ones(rows, cols) + + @classmethod + def zeros(kls, rows, cols=None, **kwargs): + """Returns a matrix of zeros. + + Parameters + ========== + + rows : rows of the matrix + cols : cols of the matrix (if None, cols=rows) + + kwargs + ====== + cls : class of the returned matrix + """ + if cols is None: + cols = rows + if rows < 0 or cols < 0: + raise ValueError("Cannot create a {} x {} matrix. " + "Both dimensions must be positive".format(rows, cols)) + klass = kwargs.get('cls', kls) + rows, cols = as_int(rows), as_int(cols) + + return klass._eval_zeros(rows, cols) + + @classmethod + def companion(kls, poly): + """Returns a companion matrix of a polynomial. + + Examples + ======== + + >>> from sympy import Matrix, Poly, Symbol, symbols + >>> x = Symbol('x') + >>> c0, c1, c2, c3, c4 = symbols('c0:5') + >>> p = Poly(c0 + c1*x + c2*x**2 + c3*x**3 + c4*x**4 + x**5, x) + >>> Matrix.companion(p) + Matrix([ + [0, 0, 0, 0, -c0], + [1, 0, 0, 0, -c1], + [0, 1, 0, 0, -c2], + [0, 0, 1, 0, -c3], + [0, 0, 0, 1, -c4]]) + """ + poly = kls._sympify(poly) + if not isinstance(poly, Poly): + raise ValueError("{} must be a Poly instance.".format(poly)) + if not poly.is_monic: + raise ValueError("{} must be a monic polynomial.".format(poly)) + if not poly.is_univariate: + raise ValueError( + "{} must be a univariate polynomial.".format(poly)) + + size = poly.degree() + if not size >= 1: + raise ValueError( + "{} must have degree not less than 1.".format(poly)) + + coeffs = poly.all_coeffs() + def entry(i, j): + if j == size - 1: + return -coeffs[-1 - i] + elif i == j + 1: + return kls.one + return kls.zero + return kls._new(size, size, entry) + + + @classmethod + def wilkinson(kls, n, **kwargs): + """Returns two square Wilkinson Matrix of size 2*n + 1 + $W_{2n + 1}^-, W_{2n + 1}^+ =$ Wilkinson(n) + + Examples + ======== + + >>> from sympy import Matrix + >>> wminus, wplus = Matrix.wilkinson(3) + >>> wminus + Matrix([ + [-3, 1, 0, 0, 0, 0, 0], + [ 1, -2, 1, 0, 0, 0, 0], + [ 0, 1, -1, 1, 0, 0, 0], + [ 0, 0, 1, 0, 1, 0, 0], + [ 0, 0, 0, 1, 1, 1, 0], + [ 0, 0, 0, 0, 1, 2, 1], + [ 0, 0, 0, 0, 0, 1, 3]]) + >>> wplus + Matrix([ + [3, 1, 0, 0, 0, 0, 0], + [1, 2, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 0, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 0, 1, 2, 1], + [0, 0, 0, 0, 0, 1, 3]]) + + References + ========== + + .. [1] https://blogs.mathworks.com/cleve/2013/04/15/wilkinsons-matrices-2/ + .. [2] J. H. Wilkinson, The Algebraic Eigenvalue Problem, Claredon Press, Oxford, 1965, 662 pp. + + """ + klass = kwargs.get('cls', kls) + n = as_int(n) + return klass._eval_wilkinson(n) + + # The RepMatrix subclass uses more efficient sparse implementations of + # _eval_iter_values and other things. + + def _eval_iter_values(self): + return (i for i in self if i is not S.Zero) + + def _eval_values(self): + return list(self.iter_values()) + + def _eval_iter_items(self): + for i in range(self.rows): + for j in range(self.cols): + if self[i, j]: + yield (i, j), self[i, j] + + def _eval_atoms(self, *types): + values = self.values() + if len(values) < self.rows * self.cols and isinstance(S.Zero, types): + s = {S.Zero} + else: + s = set() + return s.union(*[v.atoms(*types) for v in values]) + + def _eval_free_symbols(self): + return set().union(*(i.free_symbols for i in set(self.values()))) + + def _eval_has(self, *patterns): + return any(a.has(*patterns) for a in self.iter_values()) + + def _eval_is_symbolic(self): + return self.has(Symbol) + + # _eval_is_hermitian is called by some general SymPy + # routines and has a different *args signature. Make + # sure the names don't clash by adding `_matrix_` in name. + def _eval_is_matrix_hermitian(self, simpfunc): + herm = lambda i, j: simpfunc(self[i, j] - self[j, i].adjoint()).is_zero + return fuzzy_and(herm(i, j) for (i, j), v in self.iter_items()) + + def _eval_is_zero_matrix(self): + return fuzzy_and(v.is_zero for v in self.iter_values()) + + def _eval_is_Identity(self) -> FuzzyBool: + one = self.one + zero = self.zero + ident = lambda i, j, v: v is one if i == j else v is zero + return all(ident(i, j, v) for (i, j), v in self.iter_items()) + + def _eval_is_diagonal(self): + return fuzzy_and(v.is_zero for (i, j), v in self.iter_items() if i != j) + + def _eval_is_lower(self): + return all(v.is_zero for (i, j), v in self.iter_items() if i < j) + + def _eval_is_upper(self): + return all(v.is_zero for (i, j), v in self.iter_items() if i > j) + + def _eval_is_lower_hessenberg(self): + return all(v.is_zero for (i, j), v in self.iter_items() if i + 1 < j) + + def _eval_is_upper_hessenberg(self): + return all(v.is_zero for (i, j), v in self.iter_items() if i > j + 1) + + def _eval_is_symmetric(self, simpfunc): + sym = lambda i, j: simpfunc(self[i, j] - self[j, i]).is_zero + return fuzzy_and(sym(i, j) for (i, j), v in self.iter_items()) + + def _eval_is_anti_symmetric(self, simpfunc): + anti = lambda i, j: simpfunc(self[i, j] + self[j, i]).is_zero + return fuzzy_and(anti(i, j) for (i, j), v in self.iter_items()) + + def _has_positive_diagonals(self): + diagonal_entries = (self[i, i] for i in range(self.rows)) + return fuzzy_and(x.is_positive for x in diagonal_entries) + + def _has_nonnegative_diagonals(self): + diagonal_entries = (self[i, i] for i in range(self.rows)) + return fuzzy_and(x.is_nonnegative for x in diagonal_entries) + + def atoms(self, *types): + """Returns the atoms that form the current object. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import Matrix + >>> Matrix([[x]]) + Matrix([[x]]) + >>> _.atoms() + {x} + >>> Matrix([[x, y], [y, x]]) + Matrix([ + [x, y], + [y, x]]) + >>> _.atoms() + {x, y} + """ + + types = tuple(t if isinstance(t, type) else type(t) for t in types) + if not types: + types = (Atom,) + return self._eval_atoms(*types) + + @property + def free_symbols(self): + """Returns the free symbols within the matrix. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy import Matrix + >>> Matrix([[x], [1]]).free_symbols + {x} + """ + return self._eval_free_symbols() + + def has(self, *patterns): + """Test whether any subexpression matches any of the patterns. + + Examples + ======== + + >>> from sympy import Matrix, SparseMatrix, Float + >>> from sympy.abc import x, y + >>> A = Matrix(((1, x), (0.2, 3))) + >>> B = SparseMatrix(((1, x), (0.2, 3))) + >>> A.has(x) + True + >>> A.has(y) + False + >>> A.has(Float) + True + >>> B.has(x) + True + >>> B.has(y) + False + >>> B.has(Float) + True + """ + return self._eval_has(*patterns) + + def is_anti_symmetric(self, simplify=True): + """Check if matrix M is an antisymmetric matrix, + that is, M is a square matrix with all M[i, j] == -M[j, i]. + + When ``simplify=True`` (default), the sum M[i, j] + M[j, i] is + simplified before testing to see if it is zero. By default, + the SymPy simplify function is used. To use a custom function + set simplify to a function that accepts a single argument which + returns a simplified expression. To skip simplification, set + simplify to False but note that although this will be faster, + it may induce false negatives. + + Examples + ======== + + >>> from sympy import Matrix, symbols + >>> m = Matrix(2, 2, [0, 1, -1, 0]) + >>> m + Matrix([ + [ 0, 1], + [-1, 0]]) + >>> m.is_anti_symmetric() + True + >>> x, y = symbols('x y') + >>> m = Matrix(2, 3, [0, 0, x, -y, 0, 0]) + >>> m + Matrix([ + [ 0, 0, x], + [-y, 0, 0]]) + >>> m.is_anti_symmetric() + False + + >>> from sympy.abc import x, y + >>> m = Matrix(3, 3, [0, x**2 + 2*x + 1, y, + ... -(x + 1)**2, 0, x*y, + ... -y, -x*y, 0]) + + Simplification of matrix elements is done by default so even + though two elements which should be equal and opposite would not + pass an equality test, the matrix is still reported as + anti-symmetric: + + >>> m[0, 1] == -m[1, 0] + False + >>> m.is_anti_symmetric() + True + + If ``simplify=False`` is used for the case when a Matrix is already + simplified, this will speed things up. Here, we see that without + simplification the matrix does not appear anti-symmetric: + + >>> print(m.is_anti_symmetric(simplify=False)) + None + + But if the matrix were already expanded, then it would appear + anti-symmetric and simplification in the is_anti_symmetric routine + is not needed: + + >>> m = m.expand() + >>> m.is_anti_symmetric(simplify=False) + True + """ + # accept custom simplification + simpfunc = simplify + if not isfunction(simplify): + simpfunc = _utilities_simplify if simplify else lambda x: x + + if not self.is_square: + return False + return self._eval_is_anti_symmetric(simpfunc) + + def is_diagonal(self): + """Check if matrix is diagonal, + that is matrix in which the entries outside the main diagonal are all zero. + + Examples + ======== + + >>> from sympy import Matrix, diag + >>> m = Matrix(2, 2, [1, 0, 0, 2]) + >>> m + Matrix([ + [1, 0], + [0, 2]]) + >>> m.is_diagonal() + True + + >>> m = Matrix(2, 2, [1, 1, 0, 2]) + >>> m + Matrix([ + [1, 1], + [0, 2]]) + >>> m.is_diagonal() + False + + >>> m = diag(1, 2, 3) + >>> m + Matrix([ + [1, 0, 0], + [0, 2, 0], + [0, 0, 3]]) + >>> m.is_diagonal() + True + + See Also + ======== + + is_lower + is_upper + sympy.matrices.matrixbase.MatrixBase.is_diagonalizable + diagonalize + """ + return self._eval_is_diagonal() + + @property + def is_weakly_diagonally_dominant(self): + r"""Tests if the matrix is row weakly diagonally dominant. + + Explanation + =========== + + A $n, n$ matrix $A$ is row weakly diagonally dominant if + + .. math:: + \left|A_{i, i}\right| \ge \sum_{j = 0, j \neq i}^{n-1} + \left|A_{i, j}\right| \quad {\text{for all }} + i \in \{ 0, ..., n-1 \} + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([[3, -2, 1], [1, -3, 2], [-1, 2, 4]]) + >>> A.is_weakly_diagonally_dominant + True + + >>> A = Matrix([[-2, 2, 1], [1, 3, 2], [1, -2, 0]]) + >>> A.is_weakly_diagonally_dominant + False + + >>> A = Matrix([[-4, 2, 1], [1, 6, 2], [1, -2, 5]]) + >>> A.is_weakly_diagonally_dominant + True + + Notes + ===== + + If you want to test whether a matrix is column diagonally + dominant, you can apply the test after transposing the matrix. + """ + if not self.is_square: + return False + + rows, cols = self.shape + + def test_row(i): + summation = self.zero + for j in range(cols): + if i != j: + summation += Abs(self[i, j]) + return (Abs(self[i, i]) - summation).is_nonnegative + + return fuzzy_and(test_row(i) for i in range(rows)) + + @property + def is_strongly_diagonally_dominant(self): + r"""Tests if the matrix is row strongly diagonally dominant. + + Explanation + =========== + + A $n, n$ matrix $A$ is row strongly diagonally dominant if + + .. math:: + \left|A_{i, i}\right| > \sum_{j = 0, j \neq i}^{n-1} + \left|A_{i, j}\right| \quad {\text{for all }} + i \in \{ 0, ..., n-1 \} + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([[3, -2, 1], [1, -3, 2], [-1, 2, 4]]) + >>> A.is_strongly_diagonally_dominant + False + + >>> A = Matrix([[-2, 2, 1], [1, 3, 2], [1, -2, 0]]) + >>> A.is_strongly_diagonally_dominant + False + + >>> A = Matrix([[-4, 2, 1], [1, 6, 2], [1, -2, 5]]) + >>> A.is_strongly_diagonally_dominant + True + + Notes + ===== + + If you want to test whether a matrix is column diagonally + dominant, you can apply the test after transposing the matrix. + """ + if not self.is_square: + return False + + rows, cols = self.shape + + def test_row(i): + summation = self.zero + for j in range(cols): + if i != j: + summation += Abs(self[i, j]) + return (Abs(self[i, i]) - summation).is_positive + + return fuzzy_and(test_row(i) for i in range(rows)) + + @property + def is_hermitian(self): + """Checks if the matrix is Hermitian. + + In a Hermitian matrix element i,j is the complex conjugate of + element j,i. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy import I + >>> from sympy.abc import x + >>> a = Matrix([[1, I], [-I, 1]]) + >>> a + Matrix([ + [ 1, I], + [-I, 1]]) + >>> a.is_hermitian + True + >>> a[0, 0] = 2*I + >>> a.is_hermitian + False + >>> a[0, 0] = x + >>> a.is_hermitian + >>> a[0, 1] = a[1, 0]*I + >>> a.is_hermitian + False + """ + if not self.is_square: + return False + + return self._eval_is_matrix_hermitian(_utilities_simplify) + + @property + def is_Identity(self) -> FuzzyBool: + if not self.is_square: + return False + return self._eval_is_Identity() + + @property + def is_lower_hessenberg(self): + r"""Checks if the matrix is in the lower-Hessenberg form. + + The lower hessenberg matrix has zero entries + above the first superdiagonal. + + Examples + ======== + + >>> from sympy import Matrix + >>> a = Matrix([[1, 2, 0, 0], [5, 2, 3, 0], [3, 4, 3, 7], [5, 6, 1, 1]]) + >>> a + Matrix([ + [1, 2, 0, 0], + [5, 2, 3, 0], + [3, 4, 3, 7], + [5, 6, 1, 1]]) + >>> a.is_lower_hessenberg + True + + See Also + ======== + + is_upper_hessenberg + is_lower + """ + return self._eval_is_lower_hessenberg() + + @property + def is_lower(self): + """Check if matrix is a lower triangular matrix. True can be returned + even if the matrix is not square. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix(2, 2, [1, 0, 0, 1]) + >>> m + Matrix([ + [1, 0], + [0, 1]]) + >>> m.is_lower + True + + >>> m = Matrix(4, 3, [0, 0, 0, 2, 0, 0, 1, 4, 0, 6, 6, 5]) + >>> m + Matrix([ + [0, 0, 0], + [2, 0, 0], + [1, 4, 0], + [6, 6, 5]]) + >>> m.is_lower + True + + >>> from sympy.abc import x, y + >>> m = Matrix(2, 2, [x**2 + y, y**2 + x, 0, x + y]) + >>> m + Matrix([ + [x**2 + y, x + y**2], + [ 0, x + y]]) + >>> m.is_lower + False + + See Also + ======== + + is_upper + is_diagonal + is_lower_hessenberg + """ + return self._eval_is_lower() + + @property + def is_square(self): + """Checks if a matrix is square. + + A matrix is square if the number of rows equals the number of columns. + The empty matrix is square by definition, since the number of rows and + the number of columns are both zero. + + Examples + ======== + + >>> from sympy import Matrix + >>> a = Matrix([[1, 2, 3], [4, 5, 6]]) + >>> b = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> c = Matrix([]) + >>> a.is_square + False + >>> b.is_square + True + >>> c.is_square + True + """ + return self.rows == self.cols + + def is_symbolic(self): + """Checks if any elements contain Symbols. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import x, y + >>> M = Matrix([[x, y], [1, 0]]) + >>> M.is_symbolic() + True + + """ + return self._eval_is_symbolic() + + def is_symmetric(self, simplify=True): + """Check if matrix is symmetric matrix, + that is square matrix and is equal to its transpose. + + By default, simplifications occur before testing symmetry. + They can be skipped using 'simplify=False'; while speeding things a bit, + this may however induce false negatives. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix(2, 2, [0, 1, 1, 2]) + >>> m + Matrix([ + [0, 1], + [1, 2]]) + >>> m.is_symmetric() + True + + >>> m = Matrix(2, 2, [0, 1, 2, 0]) + >>> m + Matrix([ + [0, 1], + [2, 0]]) + >>> m.is_symmetric() + False + + >>> m = Matrix(2, 3, [0, 0, 0, 0, 0, 0]) + >>> m + Matrix([ + [0, 0, 0], + [0, 0, 0]]) + >>> m.is_symmetric() + False + + >>> from sympy.abc import x, y + >>> m = Matrix(3, 3, [1, x**2 + 2*x + 1, y, (x + 1)**2, 2, 0, y, 0, 3]) + >>> m + Matrix([ + [ 1, x**2 + 2*x + 1, y], + [(x + 1)**2, 2, 0], + [ y, 0, 3]]) + >>> m.is_symmetric() + True + + If the matrix is already simplified, you may speed-up is_symmetric() + test by using 'simplify=False'. + + >>> bool(m.is_symmetric(simplify=False)) + False + >>> m1 = m.expand() + >>> m1.is_symmetric(simplify=False) + True + """ + simpfunc = simplify + if not isfunction(simplify): + simpfunc = _utilities_simplify if simplify else lambda x: x + + if not self.is_square: + return False + + return self._eval_is_symmetric(simpfunc) + + @property + def is_upper_hessenberg(self): + """Checks if the matrix is the upper-Hessenberg form. + + The upper hessenberg matrix has zero entries + below the first subdiagonal. + + Examples + ======== + + >>> from sympy import Matrix + >>> a = Matrix([[1, 4, 2, 3], [3, 4, 1, 7], [0, 2, 3, 4], [0, 0, 1, 3]]) + >>> a + Matrix([ + [1, 4, 2, 3], + [3, 4, 1, 7], + [0, 2, 3, 4], + [0, 0, 1, 3]]) + >>> a.is_upper_hessenberg + True + + See Also + ======== + + is_lower_hessenberg + is_upper + """ + return self._eval_is_upper_hessenberg() + + @property + def is_upper(self): + """Check if matrix is an upper triangular matrix. True can be returned + even if the matrix is not square. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix(2, 2, [1, 0, 0, 1]) + >>> m + Matrix([ + [1, 0], + [0, 1]]) + >>> m.is_upper + True + + >>> m = Matrix(4, 3, [5, 1, 9, 0, 4, 6, 0, 0, 5, 0, 0, 0]) + >>> m + Matrix([ + [5, 1, 9], + [0, 4, 6], + [0, 0, 5], + [0, 0, 0]]) + >>> m.is_upper + True + + >>> m = Matrix(2, 3, [4, 2, 5, 6, 1, 1]) + >>> m + Matrix([ + [4, 2, 5], + [6, 1, 1]]) + >>> m.is_upper + False + + See Also + ======== + + is_lower + is_diagonal + is_upper_hessenberg + """ + return self._eval_is_upper() + + @property + def is_zero_matrix(self): + """Checks if a matrix is a zero matrix. + + A matrix is zero if every element is zero. A matrix need not be square + to be considered zero. The empty matrix is zero by the principle of + vacuous truth. For a matrix that may or may not be zero (e.g. + contains a symbol), this will be None + + Examples + ======== + + >>> from sympy import Matrix, zeros + >>> from sympy.abc import x + >>> a = Matrix([[0, 0], [0, 0]]) + >>> b = zeros(3, 4) + >>> c = Matrix([[0, 1], [0, 0]]) + >>> d = Matrix([]) + >>> e = Matrix([[x, 0], [0, 0]]) + >>> a.is_zero_matrix + True + >>> b.is_zero_matrix + True + >>> c.is_zero_matrix + False + >>> d.is_zero_matrix + True + >>> e.is_zero_matrix + """ + return self._eval_is_zero_matrix() + + def values(self): + """Return non-zero values of self. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix([[0, 1], [2, 3]]) + >>> m.values() + [1, 2, 3] + + See Also + ======== + + iter_values + tolist + flat + """ + return self._eval_values() + + def iter_values(self): + """ + Iterate over non-zero values of self. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix([[0, 1], [2, 3]]) + >>> list(m.iter_values()) + [1, 2, 3] + + See Also + ======== + + values + """ + return self._eval_iter_values() + + def iter_items(self): + """Iterate over indices and values of nonzero items. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix([[0, 1], [2, 3]]) + >>> list(m.iter_items()) + [((0, 1), 1), ((1, 0), 2), ((1, 1), 3)] + + See Also + ======== + + iter_values + todok + """ + return self._eval_iter_items() + + def _eval_adjoint(self): + return self.transpose().applyfunc(lambda x: x.adjoint()) + + def _eval_applyfunc(self, f): + cols = self.cols + size = self.rows*self.cols + + dok = self.todok() + valmap = {v: f(v) for v in dok.values()} + + if len(dok) < size and ((fzero := f(S.Zero)) is not S.Zero): + out_flat = [fzero]*size + for (i, j), v in dok.items(): + out_flat[i*cols + j] = valmap[v] + out = self._new(self.rows, self.cols, out_flat) + else: + fdok = {ij: valmap[v] for ij, v in dok.items()} + out = self.from_dok(self.rows, self.cols, fdok) + + return out + + def _eval_as_real_imag(self): # type: ignore + return (self.applyfunc(re), self.applyfunc(im)) + + def _eval_conjugate(self): + return self.applyfunc(lambda x: x.conjugate()) + + def _eval_permute_cols(self, perm): + # apply the permutation to a list + mapping = list(perm) + + def entry(i, j): + return self[i, mapping[j]] + + return self._new(self.rows, self.cols, entry) + + def _eval_permute_rows(self, perm): + # apply the permutation to a list + mapping = list(perm) + + def entry(i, j): + return self[mapping[i], j] + + return self._new(self.rows, self.cols, entry) + + def _eval_trace(self): + return sum(self[i, i] for i in range(self.rows)) + + def _eval_transpose(self): + return self._new(self.cols, self.rows, lambda i, j: self[j, i]) + + def adjoint(self): + """Conjugate transpose or Hermitian conjugation.""" + return self._eval_adjoint() + + def applyfunc(self, f): + """Apply a function to each element of the matrix. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix(2, 2, lambda i, j: i*2+j) + >>> m + Matrix([ + [0, 1], + [2, 3]]) + >>> m.applyfunc(lambda i: 2*i) + Matrix([ + [0, 2], + [4, 6]]) + + """ + if not callable(f): + raise TypeError("`f` must be callable.") + + return self._eval_applyfunc(f) + + def as_real_imag(self, deep=True, **hints): + """Returns a tuple containing the (real, imaginary) part of matrix.""" + # XXX: Ignoring deep and hints... + return self._eval_as_real_imag() + + def conjugate(self): + """Return the by-element conjugation. + + Examples + ======== + + >>> from sympy import SparseMatrix, I + >>> a = SparseMatrix(((1, 2 + I), (3, 4), (I, -I))) + >>> a + Matrix([ + [1, 2 + I], + [3, 4], + [I, -I]]) + >>> a.C + Matrix([ + [ 1, 2 - I], + [ 3, 4], + [-I, I]]) + + See Also + ======== + + transpose: Matrix transposition + H: Hermite conjugation + sympy.matrices.matrixbase.MatrixBase.D: Dirac conjugation + """ + return self._eval_conjugate() + + def doit(self, **hints): + return self.applyfunc(lambda x: x.doit(**hints)) + + def evalf(self, n=15, subs=None, maxn=100, chop=False, strict=False, quad=None, verbose=False): + """Apply evalf() to each element of self.""" + options = {'subs':subs, 'maxn':maxn, 'chop':chop, 'strict':strict, + 'quad':quad, 'verbose':verbose} + return self.applyfunc(lambda i: i.evalf(n, **options)) + + def expand(self, deep=True, modulus=None, power_base=True, power_exp=True, + mul=True, log=True, multinomial=True, basic=True, **hints): + """Apply core.function.expand to each entry of the matrix. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy import Matrix + >>> Matrix(1, 1, [x*(x+1)]) + Matrix([[x*(x + 1)]]) + >>> _.expand() + Matrix([[x**2 + x]]) + + """ + return self.applyfunc(lambda x: x.expand( + deep, modulus, power_base, power_exp, mul, log, multinomial, basic, + **hints)) + + @property + def H(self): + """Return Hermite conjugate. + + Examples + ======== + + >>> from sympy import Matrix, I + >>> m = Matrix((0, 1 + I, 2, 3)) + >>> m + Matrix([ + [ 0], + [1 + I], + [ 2], + [ 3]]) + >>> m.H + Matrix([[0, 1 - I, 2, 3]]) + + See Also + ======== + + conjugate: By-element conjugation + sympy.matrices.matrixbase.MatrixBase.D: Dirac conjugation + """ + return self.adjoint() + + def permute(self, perm, orientation='rows', direction='forward'): + r"""Permute the rows or columns of a matrix by the given list of + swaps. + + Parameters + ========== + + perm : Permutation, list, or list of lists + A representation for the permutation. + + If it is ``Permutation``, it is used directly with some + resizing with respect to the matrix size. + + If it is specified as list of lists, + (e.g., ``[[0, 1], [0, 2]]``), then the permutation is formed + from applying the product of cycles. The direction how the + cyclic product is applied is described in below. + + If it is specified as a list, the list should represent + an array form of a permutation. (e.g., ``[1, 2, 0]``) which + would would form the swapping function + `0 \mapsto 1, 1 \mapsto 2, 2\mapsto 0`. + + orientation : 'rows', 'cols' + A flag to control whether to permute the rows or the columns + + direction : 'forward', 'backward' + A flag to control whether to apply the permutations from + the start of the list first, or from the back of the list + first. + + For example, if the permutation specification is + ``[[0, 1], [0, 2]]``, + + If the flag is set to ``'forward'``, the cycle would be + formed as `0 \mapsto 2, 2 \mapsto 1, 1 \mapsto 0`. + + If the flag is set to ``'backward'``, the cycle would be + formed as `0 \mapsto 1, 1 \mapsto 2, 2 \mapsto 0`. + + If the argument ``perm`` is not in a form of list of lists, + this flag takes no effect. + + Examples + ======== + + >>> from sympy import eye + >>> M = eye(3) + >>> M.permute([[0, 1], [0, 2]], orientation='rows', direction='forward') + Matrix([ + [0, 0, 1], + [1, 0, 0], + [0, 1, 0]]) + + >>> from sympy import eye + >>> M = eye(3) + >>> M.permute([[0, 1], [0, 2]], orientation='rows', direction='backward') + Matrix([ + [0, 1, 0], + [0, 0, 1], + [1, 0, 0]]) + + Notes + ===== + + If a bijective function + `\sigma : \mathbb{N}_0 \rightarrow \mathbb{N}_0` denotes the + permutation. + + If the matrix `A` is the matrix to permute, represented as + a horizontal or a vertical stack of vectors: + + .. math:: + A = + \begin{bmatrix} + a_0 \\ a_1 \\ \vdots \\ a_{n-1} + \end{bmatrix} = + \begin{bmatrix} + \alpha_0 & \alpha_1 & \cdots & \alpha_{n-1} + \end{bmatrix} + + If the matrix `B` is the result, the permutation of matrix rows + is defined as: + + .. math:: + B := \begin{bmatrix} + a_{\sigma(0)} \\ a_{\sigma(1)} \\ \vdots \\ a_{\sigma(n-1)} + \end{bmatrix} + + And the permutation of matrix columns is defined as: + + .. math:: + B := \begin{bmatrix} + \alpha_{\sigma(0)} & \alpha_{\sigma(1)} & + \cdots & \alpha_{\sigma(n-1)} + \end{bmatrix} + """ + from sympy.combinatorics import Permutation + + # allow british variants and `columns` + if direction == 'forwards': + direction = 'forward' + if direction == 'backwards': + direction = 'backward' + if orientation == 'columns': + orientation = 'cols' + + if direction not in ('forward', 'backward'): + raise TypeError("direction='{}' is an invalid kwarg. " + "Try 'forward' or 'backward'".format(direction)) + if orientation not in ('rows', 'cols'): + raise TypeError("orientation='{}' is an invalid kwarg. " + "Try 'rows' or 'cols'".format(orientation)) + + if not isinstance(perm, (Permutation, Iterable)): + raise ValueError( + "{} must be a list, a list of lists, " + "or a SymPy permutation object.".format(perm)) + + # ensure all swaps are in range + max_index = self.rows if orientation == 'rows' else self.cols + if not all(0 <= t <= max_index for t in flatten(list(perm))): + raise IndexError("`swap` indices out of range.") + + if perm and not isinstance(perm, Permutation) and \ + isinstance(perm[0], Iterable): + if direction == 'forward': + perm = list(reversed(perm)) + perm = Permutation(perm, size=max_index+1) + else: + perm = Permutation(perm, size=max_index+1) + + if orientation == 'rows': + return self._eval_permute_rows(perm) + if orientation == 'cols': + return self._eval_permute_cols(perm) + + def permute_cols(self, swaps, direction='forward'): + """Alias for + ``self.permute(swaps, orientation='cols', direction=direction)`` + + See Also + ======== + + permute + """ + return self.permute(swaps, orientation='cols', direction=direction) + + def permute_rows(self, swaps, direction='forward'): + """Alias for + ``self.permute(swaps, orientation='rows', direction=direction)`` + + See Also + ======== + + permute + """ + return self.permute(swaps, orientation='rows', direction=direction) + + def refine(self, assumptions=True): + """Apply refine to each element of the matrix. + + Examples + ======== + + >>> from sympy import Symbol, Matrix, Abs, sqrt, Q + >>> x = Symbol('x') + >>> Matrix([[Abs(x)**2, sqrt(x**2)],[sqrt(x**2), Abs(x)**2]]) + Matrix([ + [ Abs(x)**2, sqrt(x**2)], + [sqrt(x**2), Abs(x)**2]]) + >>> _.refine(Q.real(x)) + Matrix([ + [ x**2, Abs(x)], + [Abs(x), x**2]]) + + """ + return self.applyfunc(lambda x: refine(x, assumptions)) + + def replace(self, F, G, map=False, simultaneous=True, exact=None): + """Replaces Function F in Matrix entries with Function G. + + Examples + ======== + + >>> from sympy import symbols, Function, Matrix + >>> F, G = symbols('F, G', cls=Function) + >>> M = Matrix(2, 2, lambda i, j: F(i+j)) ; M + Matrix([ + [F(0), F(1)], + [F(1), F(2)]]) + >>> N = M.replace(F,G) + >>> N + Matrix([ + [G(0), G(1)], + [G(1), G(2)]]) + """ + kwargs = {'map': map, 'simultaneous': simultaneous, 'exact': exact} + + if map: + + d = {} + def func(eij): + eij, dij = eij.replace(F, G, **kwargs) + d.update(dij) + return eij + + M = self.applyfunc(func) + return M, d + + else: + return self.applyfunc(lambda i: i.replace(F, G, **kwargs)) + + def rot90(self, k=1): + """Rotates Matrix by 90 degrees + + Parameters + ========== + + k : int + Specifies how many times the matrix is rotated by 90 degrees + (clockwise when positive, counter-clockwise when negative). + + Examples + ======== + + >>> from sympy import Matrix, symbols + >>> A = Matrix(2, 2, symbols('a:d')) + >>> A + Matrix([ + [a, b], + [c, d]]) + + Rotating the matrix clockwise one time: + + >>> A.rot90(1) + Matrix([ + [c, a], + [d, b]]) + + Rotating the matrix anticlockwise two times: + + >>> A.rot90(-2) + Matrix([ + [d, c], + [b, a]]) + """ + + mod = k%4 + if mod == 0: + return self + if mod == 1: + return self[::-1, ::].T + if mod == 2: + return self[::-1, ::-1] + if mod == 3: + return self[::, ::-1].T + + def simplify(self, **kwargs): + """Apply simplify to each element of the matrix. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import SparseMatrix, sin, cos + >>> SparseMatrix(1, 1, [x*sin(y)**2 + x*cos(y)**2]) + Matrix([[x*sin(y)**2 + x*cos(y)**2]]) + >>> _.simplify() + Matrix([[x]]) + """ + return self.applyfunc(lambda x: x.simplify(**kwargs)) + + def subs(self, *args, **kwargs): # should mirror core.basic.subs + """Return a new matrix with subs applied to each entry. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import SparseMatrix, Matrix + >>> SparseMatrix(1, 1, [x]) + Matrix([[x]]) + >>> _.subs(x, y) + Matrix([[y]]) + >>> Matrix(_).subs(y, x) + Matrix([[x]]) + """ + + if len(args) == 1 and not isinstance(args[0], (dict, set)) and iter(args[0]) and not is_sequence(args[0]): + args = (list(args[0]),) + + return self.applyfunc(lambda x: x.subs(*args, **kwargs)) + + def trace(self): + """ + Returns the trace of a square matrix i.e. the sum of the + diagonal elements. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix(2, 2, [1, 2, 3, 4]) + >>> A.trace() + 5 + + """ + if self.rows != self.cols: + raise NonSquareMatrixError() + return self._eval_trace() + + def transpose(self): + """ + Returns the transpose of the matrix. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix(2, 2, [1, 2, 3, 4]) + >>> A.transpose() + Matrix([ + [1, 3], + [2, 4]]) + + >>> from sympy import Matrix, I + >>> m=Matrix(((1, 2+I), (3, 4))) + >>> m + Matrix([ + [1, 2 + I], + [3, 4]]) + >>> m.transpose() + Matrix([ + [ 1, 3], + [2 + I, 4]]) + >>> m.T == m.transpose() + True + + See Also + ======== + + conjugate: By-element conjugation + + """ + return self._eval_transpose() + + @property + def T(self): + '''Matrix transposition''' + return self.transpose() + + @property + def C(self): + '''By-element conjugation''' + return self.conjugate() + + def n(self, *args, **kwargs): + """Apply evalf() to each element of self.""" + return self.evalf(*args, **kwargs) + + def xreplace(self, rule): # should mirror core.basic.xreplace + """Return a new matrix with xreplace applied to each entry. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import SparseMatrix, Matrix + >>> SparseMatrix(1, 1, [x]) + Matrix([[x]]) + >>> _.xreplace({x: y}) + Matrix([[y]]) + >>> Matrix(_).xreplace({y: x}) + Matrix([[x]]) + """ + return self.applyfunc(lambda x: x.xreplace(rule)) + + def _eval_simplify(self, **kwargs): + # XXX: We can't use self.simplify here as mutable subclasses will + # override simplify and have it return None + return self.applyfunc(lambda x: x.simplify(**kwargs)) + + def _eval_trigsimp(self, **opts): + from sympy.simplify.trigsimp import trigsimp + return self.applyfunc(lambda x: trigsimp(x, **opts)) + + def upper_triangular(self, k=0): + """Return the elements on and above the kth diagonal of a matrix. + If k is not specified then simply returns upper-triangular portion + of a matrix + + Examples + ======== + + >>> from sympy import ones + >>> A = ones(4) + >>> A.upper_triangular() + Matrix([ + [1, 1, 1, 1], + [0, 1, 1, 1], + [0, 0, 1, 1], + [0, 0, 0, 1]]) + + >>> A.upper_triangular(2) + Matrix([ + [0, 0, 1, 1], + [0, 0, 0, 1], + [0, 0, 0, 0], + [0, 0, 0, 0]]) + + >>> A.upper_triangular(-1) + Matrix([ + [1, 1, 1, 1], + [1, 1, 1, 1], + [0, 1, 1, 1], + [0, 0, 1, 1]]) + + """ + + def entry(i, j): + return self[i, j] if i + k <= j else self.zero + + return self._new(self.rows, self.cols, entry) + + def lower_triangular(self, k=0): + """Return the elements on and below the kth diagonal of a matrix. + If k is not specified then simply returns lower-triangular portion + of a matrix + + Examples + ======== + + >>> from sympy import ones + >>> A = ones(4) + >>> A.lower_triangular() + Matrix([ + [1, 0, 0, 0], + [1, 1, 0, 0], + [1, 1, 1, 0], + [1, 1, 1, 1]]) + + >>> A.lower_triangular(-2) + Matrix([ + [0, 0, 0, 0], + [0, 0, 0, 0], + [1, 0, 0, 0], + [1, 1, 0, 0]]) + + >>> A.lower_triangular(1) + Matrix([ + [1, 1, 0, 0], + [1, 1, 1, 0], + [1, 1, 1, 1], + [1, 1, 1, 1]]) + + """ + + def entry(i, j): + return self[i, j] if i + k >= j else self.zero + + return self._new(self.rows, self.cols, entry) + + def _eval_Abs(self): + return self._new(self.rows, self.cols, lambda i, j: Abs(self[i, j])) + + def _eval_add(self, other): + return self._new(self.rows, self.cols, + lambda i, j: self[i, j] + other[i, j]) + + def _eval_matrix_mul(self, other): + def entry(i, j): + vec = [self[i,k]*other[k,j] for k in range(self.cols)] + try: + return Add(*vec) + except (TypeError, SympifyError): + # Some matrices don't work with `sum` or `Add` + # They don't work with `sum` because `sum` tries to add `0` + # Fall back to a safe way to multiply if the `Add` fails. + return reduce(lambda a, b: a + b, vec) + + return self._new(self.rows, other.cols, entry) + + def _eval_matrix_mul_elementwise(self, other): + return self._new(self.rows, self.cols, lambda i, j: self[i,j]*other[i,j]) + + def _eval_matrix_rmul(self, other): + def entry(i, j): + return sum(other[i,k]*self[k,j] for k in range(other.cols)) + return self._new(other.rows, self.cols, entry) + + def _eval_pow_by_recursion(self, num): + if num == 1: + return self + + if num % 2 == 1: + a, b = self, self._eval_pow_by_recursion(num - 1) + else: + a = b = self._eval_pow_by_recursion(num // 2) + + return a.multiply(b) + + def _eval_pow_by_cayley(self, exp): + from sympy.discrete.recurrences import linrec_coeffs + row = self.shape[0] + p = self.charpoly() + + coeffs = (-p).all_coeffs()[1:] + coeffs = linrec_coeffs(coeffs, exp) + new_mat = self.eye(row) + ans = self.zeros(row) + + for i in range(row): + ans += coeffs[i]*new_mat + new_mat *= self + + return ans + + def _eval_pow_by_recursion_dotprodsimp(self, num, prevsimp=None): + if prevsimp is None: + prevsimp = [True]*len(self) + + if num == 1: + return self + + if num % 2 == 1: + a, b = self, self._eval_pow_by_recursion_dotprodsimp(num - 1, + prevsimp=prevsimp) + else: + a = b = self._eval_pow_by_recursion_dotprodsimp(num // 2, + prevsimp=prevsimp) + + m = a.multiply(b, dotprodsimp=False) + lenm = len(m) + elems = [None]*lenm + + for i in range(lenm): + if prevsimp[i]: + elems[i], prevsimp[i] = _dotprodsimp(m[i], withsimp=True) + else: + elems[i] = m[i] + + return m._new(m.rows, m.cols, elems) + + def _eval_scalar_mul(self, other): + return self._new(self.rows, self.cols, lambda i, j: self[i,j]*other) + + def _eval_scalar_rmul(self, other): + return self._new(self.rows, self.cols, lambda i, j: other*self[i,j]) + + def _eval_Mod(self, other): + return self._new(self.rows, self.cols, lambda i, j: Mod(self[i, j], other)) + + # Python arithmetic functions + def __abs__(self): + """Returns a new matrix with entry-wise absolute values.""" + return self._eval_Abs() + + @call_highest_priority('__radd__') + def __add__(self, other): + """Return self + other, raising ShapeError if shapes do not match.""" + + other, T = _coerce_operand(self, other) + + if T != "is_matrix": + return NotImplemented + + if self.shape != other.shape: + raise ShapeError(f"Matrix size mismatch: {self.shape} + {other.shape}.") + + # Unify matrix types + a, b = self, other + if a.__class__ != classof(a, b): + b, a = a, b + + return a._eval_add(b) + + @call_highest_priority('__rtruediv__') + def __truediv__(self, other): + return self * (self.one / other) + + @call_highest_priority('__rmatmul__') + def __matmul__(self, other): + self, other, T = _unify_with_other(self, other) + + if T != "is_matrix": + return NotImplemented + + return self.__mul__(other) + + def __mod__(self, other): + return self.applyfunc(lambda x: x % other) + + @call_highest_priority('__rmul__') + def __mul__(self, other): + """Return self*other where other is either a scalar or a matrix + of compatible dimensions. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([[1, 2, 3], [4, 5, 6]]) + >>> 2*A == A*2 == Matrix([[2, 4, 6], [8, 10, 12]]) + True + >>> B = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> A*B + Matrix([ + [30, 36, 42], + [66, 81, 96]]) + >>> B*A + Traceback (most recent call last): + ... + ShapeError: Matrices size mismatch. + >>> + + See Also + ======== + + matrix_multiply_elementwise + """ + + return self.multiply(other) + + def multiply(self, other, dotprodsimp=None): + """Same as __mul__() but with optional simplification. + + Parameters + ========== + + dotprodsimp : bool, optional + Specifies whether intermediate term algebraic simplification is used + during matrix multiplications to control expression blowup and thus + speed up calculation. Default is off. + """ + + isimpbool = _get_intermediate_simp_bool(False, dotprodsimp) + + self, other, T = _unify_with_other(self, other) + + if T == "possible_scalar": + try: + return self._eval_scalar_mul(other) + except TypeError: + return NotImplemented + + elif T == "is_matrix": + + if self.shape[1] != other.shape[0]: + raise ShapeError(f"Matrix size mismatch: {self.shape} * {other.shape}.") + + m = self._eval_matrix_mul(other) + + if isimpbool: + m = m._new(m.rows, m.cols, [_dotprodsimp(e) for e in m]) + + return m + + else: + return NotImplemented + + def multiply_elementwise(self, other): + """Return the Hadamard product (elementwise product) of A and B + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([[0, 1, 2], [3, 4, 5]]) + >>> B = Matrix([[1, 10, 100], [100, 10, 1]]) + >>> A.multiply_elementwise(B) + Matrix([ + [ 0, 10, 200], + [300, 40, 5]]) + + See Also + ======== + + sympy.matrices.matrixbase.MatrixBase.cross + sympy.matrices.matrixbase.MatrixBase.dot + multiply + """ + if self.shape != other.shape: + raise ShapeError("Matrix shapes must agree {} != {}".format(self.shape, other.shape)) + + return self._eval_matrix_mul_elementwise(other) + + def __neg__(self): + return self._eval_scalar_mul(-1) + + @call_highest_priority('__rpow__') + def __pow__(self, exp): + """Return self**exp a scalar or symbol.""" + + return self.pow(exp) + + + def pow(self, exp, method=None): + r"""Return self**exp a scalar or symbol. + + Parameters + ========== + + method : multiply, mulsimp, jordan, cayley + If multiply then it returns exponentiation using recursion. + If jordan then Jordan form exponentiation will be used. + If cayley then the exponentiation is done using Cayley-Hamilton + theorem. + If mulsimp then the exponentiation is done using recursion + with dotprodsimp. This specifies whether intermediate term + algebraic simplification is used during naive matrix power to + control expression blowup and thus speed up calculation. + If None, then it heuristically decides which method to use. + + """ + + if method is not None and method not in ['multiply', 'mulsimp', 'jordan', 'cayley']: + raise TypeError('No such method') + if self.rows != self.cols: + raise NonSquareMatrixError() + a = self + jordan_pow = getattr(a, '_matrix_pow_by_jordan_blocks', None) + exp = sympify(exp) + + if exp.is_zero: + return a._new(a.rows, a.cols, lambda i, j: int(i == j)) + if exp == 1: + return a + + diagonal = getattr(a, 'is_diagonal', None) + if diagonal is not None and diagonal(): + return a._new(a.rows, a.cols, lambda i, j: a[i,j]**exp if i == j else 0) + + if exp.is_Number and exp % 1 == 0: + if a.rows == 1: + return a._new([[a[0]**exp]]) + if exp < 0: + exp = -exp + a = a.inv() + # When certain conditions are met, + # Jordan block algorithm is faster than + # computation by recursion. + if method == 'jordan': + try: + return jordan_pow(exp) + except MatrixError: + if method == 'jordan': + raise + + elif method == 'cayley': + if not exp.is_Number or exp % 1 != 0: + raise ValueError("cayley method is only valid for integer powers") + return a._eval_pow_by_cayley(exp) + + elif method == "mulsimp": + if not exp.is_Number or exp % 1 != 0: + raise ValueError("mulsimp method is only valid for integer powers") + return a._eval_pow_by_recursion_dotprodsimp(exp) + + elif method == "multiply": + if not exp.is_Number or exp % 1 != 0: + raise ValueError("multiply method is only valid for integer powers") + return a._eval_pow_by_recursion(exp) + + elif method is None and exp.is_Number and exp % 1 == 0: + if exp.is_Float: + exp = Integer(exp) + # Decide heuristically which method to apply + if a.rows == 2 and exp > 100000: + return jordan_pow(exp) + elif _get_intermediate_simp_bool(True, None): + return a._eval_pow_by_recursion_dotprodsimp(exp) + elif exp > 10000: + return a._eval_pow_by_cayley(exp) + else: + return a._eval_pow_by_recursion(exp) + + if jordan_pow: + try: + return jordan_pow(exp) + except NonInvertibleMatrixError: + # Raised by jordan_pow on zero determinant matrix unless exp is + # definitely known to be a non-negative integer. + # Here we raise if n is definitely not a non-negative integer + # but otherwise we can leave this as an unevaluated MatPow. + if exp.is_integer is False or exp.is_nonnegative is False: + raise + + from sympy.matrices.expressions import MatPow + return MatPow(a, exp) + + @call_highest_priority('__add__') + def __radd__(self, other): + return self.__add__(other) + + @call_highest_priority('__matmul__') + def __rmatmul__(self, other): + self, other, T = _unify_with_other(self, other) + + if T != "is_matrix": + return NotImplemented + + return self.__rmul__(other) + + @call_highest_priority('__mul__') + def __rmul__(self, other): + return self.rmultiply(other) + + def rmultiply(self, other, dotprodsimp=None): + """Same as __rmul__() but with optional simplification. + + Parameters + ========== + + dotprodsimp : bool, optional + Specifies whether intermediate term algebraic simplification is used + during matrix multiplications to control expression blowup and thus + speed up calculation. Default is off. + """ + isimpbool = _get_intermediate_simp_bool(False, dotprodsimp) + self, other, T = _unify_with_other(self, other) + + if T == "possible_scalar": + try: + return self._eval_scalar_rmul(other) + except TypeError: + return NotImplemented + + elif T == "is_matrix": + if self.shape[0] != other.shape[1]: + raise ShapeError("Matrix size mismatch.") + + m = self._eval_matrix_rmul(other) + + if isimpbool: + return m._new(m.rows, m.cols, [_dotprodsimp(e) for e in m]) + + return m + + else: + return NotImplemented + + @call_highest_priority('__sub__') + def __rsub__(self, a): + return (-self) + a + + @call_highest_priority('__rsub__') + def __sub__(self, a): + return self + (-a) + + def _eval_det_bareiss(self, iszerofunc=_is_zero_after_expand_mul): + return _det_bareiss(self, iszerofunc=iszerofunc) + + def _eval_det_berkowitz(self): + return _det_berkowitz(self) + + def _eval_det_lu(self, iszerofunc=_iszero, simpfunc=None): + return _det_LU(self, iszerofunc=iszerofunc, simpfunc=simpfunc) + + def _eval_det_bird(self): + return _det_bird(self) + + def _eval_det_laplace(self): + return _det_laplace(self) + + def _eval_determinant(self): # for expressions.determinant.Determinant + return _det(self) + + def adjugate(self, method="berkowitz"): + return _adjugate(self, method=method) + + def charpoly(self, x='lambda', simplify=_utilities_simplify): + return _charpoly(self, x=x, simplify=simplify) + + def cofactor(self, i, j, method="berkowitz"): + return _cofactor(self, i, j, method=method) + + def cofactor_matrix(self, method="berkowitz"): + return _cofactor_matrix(self, method=method) + + def det(self, method="bareiss", iszerofunc=None): + return _det(self, method=method, iszerofunc=iszerofunc) + + def per(self): + return _per(self) + + def minor(self, i, j, method="berkowitz"): + return _minor(self, i, j, method=method) + + def minor_submatrix(self, i, j): + return _minor_submatrix(self, i, j) + + _find_reasonable_pivot.__doc__ = _find_reasonable_pivot.__doc__ + _find_reasonable_pivot_naive.__doc__ = _find_reasonable_pivot_naive.__doc__ + _eval_det_bareiss.__doc__ = _det_bareiss.__doc__ + _eval_det_berkowitz.__doc__ = _det_berkowitz.__doc__ + _eval_det_bird.__doc__ = _det_bird.__doc__ + _eval_det_laplace.__doc__ = _det_laplace.__doc__ + _eval_det_lu.__doc__ = _det_LU.__doc__ + _eval_determinant.__doc__ = _det.__doc__ + adjugate.__doc__ = _adjugate.__doc__ + charpoly.__doc__ = _charpoly.__doc__ + cofactor.__doc__ = _cofactor.__doc__ + cofactor_matrix.__doc__ = _cofactor_matrix.__doc__ + det.__doc__ = _det.__doc__ + per.__doc__ = _per.__doc__ + minor.__doc__ = _minor.__doc__ + minor_submatrix.__doc__ = _minor_submatrix.__doc__ + + def echelon_form(self, iszerofunc=_iszero, simplify=False, with_pivots=False): + return _echelon_form(self, iszerofunc=iszerofunc, simplify=simplify, + with_pivots=with_pivots) + + @property + def is_echelon(self): + return _is_echelon(self) + + def rank(self, iszerofunc=_iszero, simplify=False): + return _rank(self, iszerofunc=iszerofunc, simplify=simplify) + + def rref_rhs(self, rhs): + """Return reduced row-echelon form of matrix, matrix showing + rhs after reduction steps. ``rhs`` must have the same number + of rows as ``self``. + + Examples + ======== + + >>> from sympy import Matrix, symbols + >>> r1, r2 = symbols('r1 r2') + >>> Matrix([[1, 1], [2, 1]]).rref_rhs(Matrix([r1, r2])) + (Matrix([ + [1, 0], + [0, 1]]), Matrix([ + [ -r1 + r2], + [2*r1 - r2]])) + """ + r, _ = _rref(self.hstack(self, self.eye(self.rows), rhs)) + return r[:, :self.cols], r[:, -rhs.cols:] + + def rref(self, iszerofunc=_iszero, simplify=False, pivots=True, + normalize_last=True): + return _rref(self, iszerofunc=iszerofunc, simplify=simplify, + pivots=pivots, normalize_last=normalize_last) + + echelon_form.__doc__ = _echelon_form.__doc__ + is_echelon.__doc__ = _is_echelon.__doc__ + rank.__doc__ = _rank.__doc__ + rref.__doc__ = _rref.__doc__ + + def _normalize_op_args(self, op, col, k, col1, col2, error_str="col"): + """Validate the arguments for a row/column operation. ``error_str`` + can be one of "row" or "col" depending on the arguments being parsed.""" + if op not in ["n->kn", "n<->m", "n->n+km"]: + raise ValueError("Unknown {} operation '{}'. Valid col operations " + "are 'n->kn', 'n<->m', 'n->n+km'".format(error_str, op)) + + # define self_col according to error_str + self_cols = self.cols if error_str == 'col' else self.rows + + # normalize and validate the arguments + if op == "n->kn": + col = col if col is not None else col1 + if col is None or k is None: + raise ValueError("For a {0} operation 'n->kn' you must provide the " + "kwargs `{0}` and `k`".format(error_str)) + if not 0 <= col < self_cols: + raise ValueError("This matrix does not have a {} '{}'".format(error_str, col)) + + elif op == "n<->m": + # we need two cols to swap. It does not matter + # how they were specified, so gather them together and + # remove `None` + cols = {col, k, col1, col2}.difference([None]) + if len(cols) > 2: + # maybe the user left `k` by mistake? + cols = {col, col1, col2}.difference([None]) + if len(cols) != 2: + raise ValueError("For a {0} operation 'n<->m' you must provide the " + "kwargs `{0}1` and `{0}2`".format(error_str)) + col1, col2 = cols + if not 0 <= col1 < self_cols: + raise ValueError("This matrix does not have a {} '{}'".format(error_str, col1)) + if not 0 <= col2 < self_cols: + raise ValueError("This matrix does not have a {} '{}'".format(error_str, col2)) + + elif op == "n->n+km": + col = col1 if col is None else col + col2 = col1 if col2 is None else col2 + if col is None or col2 is None or k is None: + raise ValueError("For a {0} operation 'n->n+km' you must provide the " + "kwargs `{0}`, `k`, and `{0}2`".format(error_str)) + if col == col2: + raise ValueError("For a {0} operation 'n->n+km' `{0}` and `{0}2` must " + "be different.".format(error_str)) + if not 0 <= col < self_cols: + raise ValueError("This matrix does not have a {} '{}'".format(error_str, col)) + if not 0 <= col2 < self_cols: + raise ValueError("This matrix does not have a {} '{}'".format(error_str, col2)) + + else: + raise ValueError('invalid operation %s' % repr(op)) + + return op, col, k, col1, col2 + + def _eval_col_op_multiply_col_by_const(self, col, k): + def entry(i, j): + if j == col: + return k * self[i, j] + return self[i, j] + return self._new(self.rows, self.cols, entry) + + def _eval_col_op_swap(self, col1, col2): + def entry(i, j): + if j == col1: + return self[i, col2] + elif j == col2: + return self[i, col1] + return self[i, j] + return self._new(self.rows, self.cols, entry) + + def _eval_col_op_add_multiple_to_other_col(self, col, k, col2): + def entry(i, j): + if j == col: + return self[i, j] + k * self[i, col2] + return self[i, j] + return self._new(self.rows, self.cols, entry) + + def _eval_row_op_swap(self, row1, row2): + def entry(i, j): + if i == row1: + return self[row2, j] + elif i == row2: + return self[row1, j] + return self[i, j] + return self._new(self.rows, self.cols, entry) + + def _eval_row_op_multiply_row_by_const(self, row, k): + def entry(i, j): + if i == row: + return k * self[i, j] + return self[i, j] + return self._new(self.rows, self.cols, entry) + + def _eval_row_op_add_multiple_to_other_row(self, row, k, row2): + def entry(i, j): + if i == row: + return self[i, j] + k * self[row2, j] + return self[i, j] + return self._new(self.rows, self.cols, entry) + + def elementary_col_op(self, op="n->kn", col=None, k=None, col1=None, col2=None): + """Performs the elementary column operation `op`. + + `op` may be one of + + * ``"n->kn"`` (column n goes to k*n) + * ``"n<->m"`` (swap column n and column m) + * ``"n->n+km"`` (column n goes to column n + k*column m) + + Parameters + ========== + + op : string; the elementary row operation + col : the column to apply the column operation + k : the multiple to apply in the column operation + col1 : one column of a column swap + col2 : second column of a column swap or column "m" in the column operation + "n->n+km" + """ + + op, col, k, col1, col2 = self._normalize_op_args(op, col, k, col1, col2, "col") + + # now that we've validated, we're all good to dispatch + if op == "n->kn": + return self._eval_col_op_multiply_col_by_const(col, k) + if op == "n<->m": + return self._eval_col_op_swap(col1, col2) + if op == "n->n+km": + return self._eval_col_op_add_multiple_to_other_col(col, k, col2) + + def elementary_row_op(self, op="n->kn", row=None, k=None, row1=None, row2=None): + """Performs the elementary row operation `op`. + + `op` may be one of + + * ``"n->kn"`` (row n goes to k*n) + * ``"n<->m"`` (swap row n and row m) + * ``"n->n+km"`` (row n goes to row n + k*row m) + + Parameters + ========== + + op : string; the elementary row operation + row : the row to apply the row operation + k : the multiple to apply in the row operation + row1 : one row of a row swap + row2 : second row of a row swap or row "m" in the row operation + "n->n+km" + """ + + op, row, k, row1, row2 = self._normalize_op_args(op, row, k, row1, row2, "row") + + # now that we've validated, we're all good to dispatch + if op == "n->kn": + return self._eval_row_op_multiply_row_by_const(row, k) + if op == "n<->m": + return self._eval_row_op_swap(row1, row2) + if op == "n->n+km": + return self._eval_row_op_add_multiple_to_other_row(row, k, row2) + + def columnspace(self, simplify=False): + return _columnspace(self, simplify=simplify) + + def nullspace(self, simplify=False, iszerofunc=_iszero): + return _nullspace(self, simplify=simplify, iszerofunc=iszerofunc) + + def rowspace(self, simplify=False): + return _rowspace(self, simplify=simplify) + + # This is a classmethod but is converted to such later in order to allow + # assignment of __doc__ since that does not work for already wrapped + # classmethods in Python 3.6. + def orthogonalize(cls, *vecs, **kwargs): + return _orthogonalize(cls, *vecs, **kwargs) + + columnspace.__doc__ = _columnspace.__doc__ + nullspace.__doc__ = _nullspace.__doc__ + rowspace.__doc__ = _rowspace.__doc__ + orthogonalize.__doc__ = _orthogonalize.__doc__ + + orthogonalize = classmethod(orthogonalize) # type:ignore + + def eigenvals(self, error_when_incomplete=True, **flags): + return _eigenvals(self, error_when_incomplete=error_when_incomplete, **flags) + + def eigenvects(self, error_when_incomplete=True, iszerofunc=_iszero, **flags): + return _eigenvects(self, error_when_incomplete=error_when_incomplete, + iszerofunc=iszerofunc, **flags) + + def is_diagonalizable(self, reals_only=False, **kwargs): + return _is_diagonalizable(self, reals_only=reals_only, **kwargs) + + def diagonalize(self, reals_only=False, sort=False, normalize=False): + return _diagonalize(self, reals_only=reals_only, sort=sort, + normalize=normalize) + + def bidiagonalize(self, upper=True): + return _bidiagonalize(self, upper=upper) + + def bidiagonal_decomposition(self, upper=True): + return _bidiagonal_decomposition(self, upper=upper) + + @property + def is_positive_definite(self): + return _is_positive_definite(self) + + @property + def is_positive_semidefinite(self): + return _is_positive_semidefinite(self) + + @property + def is_negative_definite(self): + return _is_negative_definite(self) + + @property + def is_negative_semidefinite(self): + return _is_negative_semidefinite(self) + + @property + def is_indefinite(self): + return _is_indefinite(self) + + def jordan_form(self, calc_transform=True, **kwargs): + return _jordan_form(self, calc_transform=calc_transform, **kwargs) + + def left_eigenvects(self, **flags): + return _left_eigenvects(self, **flags) + + def singular_values(self): + return _singular_values(self) + + eigenvals.__doc__ = _eigenvals.__doc__ + eigenvects.__doc__ = _eigenvects.__doc__ + is_diagonalizable.__doc__ = _is_diagonalizable.__doc__ + diagonalize.__doc__ = _diagonalize.__doc__ + is_positive_definite.__doc__ = _is_positive_definite.__doc__ + is_positive_semidefinite.__doc__ = _is_positive_semidefinite.__doc__ + is_negative_definite.__doc__ = _is_negative_definite.__doc__ + is_negative_semidefinite.__doc__ = _is_negative_semidefinite.__doc__ + is_indefinite.__doc__ = _is_indefinite.__doc__ + jordan_form.__doc__ = _jordan_form.__doc__ + left_eigenvects.__doc__ = _left_eigenvects.__doc__ + singular_values.__doc__ = _singular_values.__doc__ + bidiagonalize.__doc__ = _bidiagonalize.__doc__ + bidiagonal_decomposition.__doc__ = _bidiagonal_decomposition.__doc__ + + def diff(self, *args, evaluate=True, **kwargs): + """Calculate the derivative of each element in the matrix. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import x, y + >>> M = Matrix([[x, y], [1, 0]]) + >>> M.diff(x) + Matrix([ + [1, 0], + [0, 0]]) + + See Also + ======== + + integrate + limit + """ + # XXX this should be handled here rather than in Derivative + from sympy.tensor.array.array_derivatives import ArrayDerivative + deriv = ArrayDerivative(self, *args, evaluate=evaluate) + # XXX This can rather changed to always return immutable matrix + if not isinstance(self, Basic) and evaluate: + return deriv.as_mutable() + return deriv + + def _eval_derivative(self, arg): + return self.applyfunc(lambda x: x.diff(arg)) + + def integrate(self, *args, **kwargs): + """Integrate each element of the matrix. ``args`` will + be passed to the ``integrate`` function. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import x, y + >>> M = Matrix([[x, y], [1, 0]]) + >>> M.integrate((x, )) + Matrix([ + [x**2/2, x*y], + [ x, 0]]) + >>> M.integrate((x, 0, 2)) + Matrix([ + [2, 2*y], + [2, 0]]) + + See Also + ======== + + limit + diff + """ + return self.applyfunc(lambda x: x.integrate(*args, **kwargs)) + + def jacobian(self, X): + """Calculates the Jacobian matrix (derivative of a vector-valued function). + + Parameters + ========== + + ``self`` : vector of expressions representing functions f_i(x_1, ..., x_n). + X : set of x_i's in order, it can be a list or a Matrix + + Both ``self`` and X can be a row or a column matrix in any order + (i.e., jacobian() should always work). + + Examples + ======== + + >>> from sympy import sin, cos, Matrix + >>> from sympy.abc import rho, phi + >>> X = Matrix([rho*cos(phi), rho*sin(phi), rho**2]) + >>> Y = Matrix([rho, phi]) + >>> X.jacobian(Y) + Matrix([ + [cos(phi), -rho*sin(phi)], + [sin(phi), rho*cos(phi)], + [ 2*rho, 0]]) + >>> X = Matrix([rho*cos(phi), rho*sin(phi)]) + >>> X.jacobian(Y) + Matrix([ + [cos(phi), -rho*sin(phi)], + [sin(phi), rho*cos(phi)]]) + + See Also + ======== + + hessian + wronskian + """ + from sympy.matrices.matrixbase import MatrixBase + if not isinstance(X, MatrixBase): + X = self._new(X) + # Both X and ``self`` can be a row or a column matrix, so we need to make + # sure all valid combinations work, but everything else fails: + if self.shape[0] == 1: + m = self.shape[1] + elif self.shape[1] == 1: + m = self.shape[0] + else: + raise TypeError("``self`` must be a row or a column matrix") + if X.shape[0] == 1: + n = X.shape[1] + elif X.shape[1] == 1: + n = X.shape[0] + else: + raise TypeError("X must be a row or a column matrix") + + # m is the number of functions and n is the number of variables + # computing the Jacobian is now easy: + return self._new(m, n, lambda j, i: self[j].diff(X[i])) + + def limit(self, *args): + """Calculate the limit of each element in the matrix. + ``args`` will be passed to the ``limit`` function. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import x, y + >>> M = Matrix([[x, y], [1, 0]]) + >>> M.limit(x, 2) + Matrix([ + [2, y], + [1, 0]]) + + See Also + ======== + + integrate + diff + """ + return self.applyfunc(lambda x: x.limit(*args)) + + def berkowitz_charpoly(self, x=Dummy('lambda'), simplify=_utilities_simplify): + return self.charpoly(x=x) + + def berkowitz_det(self): + """Computes determinant using Berkowitz method. + + See Also + ======== + + det + """ + return self.det(method='berkowitz') + + def berkowitz_eigenvals(self, **flags): + """Computes eigenvalues of a Matrix using Berkowitz method.""" + return self.eigenvals(**flags) + + def berkowitz_minors(self): + """Computes principal minors using Berkowitz method.""" + sign, minors = self.one, [] + + for poly in self.berkowitz(): + minors.append(sign * poly[-1]) + sign = -sign + + return tuple(minors) + + def berkowitz(self): + from sympy.matrices import zeros + berk = ((1,),) + if not self: + return berk + + if not self.is_square: + raise NonSquareMatrixError() + + A, N = self, self.rows + transforms = [0] * (N - 1) + + for n in range(N, 1, -1): + T, k = zeros(n + 1, n), n - 1 + + R, C = -A[k, :k], A[:k, k] + A, a = A[:k, :k], -A[k, k] + + items = [C] + + for i in range(0, n - 2): + items.append(A * items[i]) + + for i, B in enumerate(items): + items[i] = (R * B)[0, 0] + + items = [self.one, a] + items + + for i in range(n): + T[i:, i] = items[:n - i + 1] + + transforms[k - 1] = T + + polys = [self._new([self.one, -A[0, 0]])] + + for i, T in enumerate(transforms): + polys.append(T * polys[i]) + + return berk + tuple(map(tuple, polys)) + + def cofactorMatrix(self, method="berkowitz"): + return self.cofactor_matrix(method=method) + + def det_bareis(self): + return _det_bareiss(self) + + def det_LU_decomposition(self): + """Compute matrix determinant using LU decomposition. + + + Note that this method fails if the LU decomposition itself + fails. In particular, if the matrix has no inverse this method + will fail. + + TODO: Implement algorithm for sparse matrices (SFF), + http://www.eecis.udel.edu/~saunders/papers/sffge/it5.ps. + + See Also + ======== + + + det + berkowitz_det + """ + return self.det(method='lu') + + def jordan_cell(self, eigenval, n): + return self.jordan_block(size=n, eigenvalue=eigenval) + + def jordan_cells(self, calc_transformation=True): + P, J = self.jordan_form() + return P, J.get_diag_blocks() + + def minorEntry(self, i, j, method="berkowitz"): + return self.minor(i, j, method=method) + + def minorMatrix(self, i, j): + return self.minor_submatrix(i, j) + + def permuteBkwd(self, perm): + """Permute the rows of the matrix with the given permutation in reverse.""" + return self.permute_rows(perm, direction='backward') + + def permuteFwd(self, perm): + """Permute the rows of the matrix with the given permutation.""" + return self.permute_rows(perm, direction='forward') + + @property + def kind(self) -> MatrixKind: + elem_kinds = {e.kind for e in self.flat()} + if len(elem_kinds) == 1: + elemkind, = elem_kinds + else: + elemkind = UndefinedKind + return MatrixKind(elemkind) + + def flat(self): + """ + Returns a flat list of all elements in the matrix. + + Examples + ======== + + >>> from sympy import Matrix + >>> m = Matrix([[0, 2], [3, 4]]) + >>> m.flat() + [0, 2, 3, 4] + + See Also + ======== + + tolist + values + """ + return [self[i, j] for i in range(self.rows) for j in range(self.cols)] + + def __array__(self, dtype=object, copy=None): + if copy is not None and not copy: + raise TypeError("Cannot implement copy=False when converting Matrix to ndarray") + from .dense import matrix2numpy + return matrix2numpy(self, dtype=dtype) + + def __len__(self): + """Return the number of elements of ``self``. + + Implemented mainly so bool(Matrix()) == False. + """ + return self.rows * self.cols + + def _matrix_pow_by_jordan_blocks(self, num): + from sympy.matrices import diag, MutableMatrix + + def jordan_cell_power(jc, n): + N = jc.shape[0] + l = jc[0,0] + if l.is_zero: + if N == 1 and n.is_nonnegative: + jc[0,0] = l**n + elif not (n.is_integer and n.is_nonnegative): + raise NonInvertibleMatrixError("Non-invertible matrix can only be raised to a nonnegative integer") + else: + for i in range(N): + jc[0,i] = KroneckerDelta(i, n) + else: + for i in range(N): + bn = binomial(n, i) + if isinstance(bn, binomial): + bn = bn._eval_expand_func() + jc[0,i] = l**(n-i)*bn + for i in range(N): + for j in range(1, N-i): + jc[j,i+j] = jc [j-1,i+j-1] + + P, J = self.jordan_form() + jordan_cells = J.get_diag_blocks() + # Make sure jordan_cells matrices are mutable: + jordan_cells = [MutableMatrix(j) for j in jordan_cells] + for j in jordan_cells: + jordan_cell_power(j, num) + return self._new(P.multiply(diag(*jordan_cells)) + .multiply(P.inv())) + + def __str__(self): + if S.Zero in self.shape: + return 'Matrix(%s, %s, [])' % (self.rows, self.cols) + return "Matrix(%s)" % str(self.tolist()) + + def _format_str(self, printer=None): + if not printer: + printer = StrPrinter() + # Handle zero dimensions: + if S.Zero in self.shape: + return 'Matrix(%s, %s, [])' % (self.rows, self.cols) + if self.rows == 1: + return "Matrix([%s])" % self.table(printer, rowsep=',\n') + return "Matrix([\n%s])" % self.table(printer, rowsep=',\n') + + @classmethod + def irregular(cls, ntop, *matrices, **kwargs): + """Return a matrix filled by the given matrices which + are listed in order of appearance from left to right, top to + bottom as they first appear in the matrix. They must fill the + matrix completely. + + Examples + ======== + + >>> from sympy import ones, Matrix + >>> Matrix.irregular(3, ones(2,1), ones(3,3)*2, ones(2,2)*3, + ... ones(1,1)*4, ones(2,2)*5, ones(1,2)*6, ones(1,2)*7) + Matrix([ + [1, 2, 2, 2, 3, 3], + [1, 2, 2, 2, 3, 3], + [4, 2, 2, 2, 5, 5], + [6, 6, 7, 7, 5, 5]]) + """ + ntop = as_int(ntop) + # make sure we are working with explicit matrices + b = [i.as_explicit() if hasattr(i, 'as_explicit') else i + for i in matrices] + q = list(range(len(b))) + dat = [i.rows for i in b] + active = [q.pop(0) for _ in range(ntop)] + cols = sum(b[i].cols for i in active) + rows = [] + while any(dat): + r = [] + for a, j in enumerate(active): + r.extend(b[j][-dat[j], :]) + dat[j] -= 1 + if dat[j] == 0 and q: + active[a] = q.pop(0) + if len(r) != cols: + raise ValueError(filldedent(''' + Matrices provided do not appear to fill + the space completely.''')) + rows.append(r) + return cls._new(rows) + + @classmethod + def _handle_ndarray(cls, arg): + # NumPy array or matrix or some other object that implements + # __array__. So let's first use this method to get a + # numpy.array() and then make a Python list out of it. + arr = arg.__array__() + if len(arr.shape) == 2: + rows, cols = arr.shape[0], arr.shape[1] + flat_list = [cls._sympify(i) for i in arr.ravel()] + return rows, cols, flat_list + elif len(arr.shape) == 1: + flat_list = [cls._sympify(i) for i in arr] + return arr.shape[0], 1, flat_list + else: + raise NotImplementedError( + "SymPy supports just 1D and 2D matrices") + + @classmethod + def _handle_creation_inputs(cls, *args, **kwargs): + """Return the number of rows, cols and flat matrix elements. + + Examples + ======== + + >>> from sympy import Matrix, I + + Matrix can be constructed as follows: + + * from a nested list of iterables + + >>> Matrix( ((1, 2+I), (3, 4)) ) + Matrix([ + [1, 2 + I], + [3, 4]]) + + * from un-nested iterable (interpreted as a column) + + >>> Matrix( [1, 2] ) + Matrix([ + [1], + [2]]) + + * from un-nested iterable with dimensions + + >>> Matrix(1, 2, [1, 2] ) + Matrix([[1, 2]]) + + * from no arguments (a 0 x 0 matrix) + + >>> Matrix() + Matrix(0, 0, []) + + * from a rule + + >>> Matrix(2, 2, lambda i, j: i/(j + 1) ) + Matrix([ + [0, 0], + [1, 1/2]]) + + See Also + ======== + irregular - filling a matrix with irregular blocks + """ + from sympy.matrices import SparseMatrix + from sympy.matrices.expressions.matexpr import MatrixSymbol + from sympy.matrices.expressions.blockmatrix import BlockMatrix + + flat_list = None + + if len(args) == 1: + # Matrix(SparseMatrix(...)) + if isinstance(args[0], SparseMatrix): + return args[0].rows, args[0].cols, flatten(args[0].tolist()) + + # Matrix(Matrix(...)) + elif isinstance(args[0], MatrixBase): + return args[0].rows, args[0].cols, args[0].flat() + + # Matrix(MatrixSymbol('X', 2, 2)) + elif isinstance(args[0], Basic) and args[0].is_Matrix: + return args[0].rows, args[0].cols, args[0].as_explicit().flat() + + elif isinstance(args[0], mp.matrix): + M = args[0] + flat_list = [cls._sympify(x) for x in M] + return M.rows, M.cols, flat_list + + # Matrix(numpy.ones((2, 2))) + elif hasattr(args[0], "__array__"): + return cls._handle_ndarray(args[0]) + + # Matrix([1, 2, 3]) or Matrix([[1, 2], [3, 4]]) + elif is_sequence(args[0]) \ + and not isinstance(args[0], DeferredVector): + dat = list(args[0]) + ismat = lambda i: isinstance(i, MatrixBase) and ( + evaluate or isinstance(i, (BlockMatrix, MatrixSymbol))) + raw = lambda i: is_sequence(i) and not ismat(i) + evaluate = kwargs.get('evaluate', True) + + + if evaluate: + + def make_explicit(x): + """make Block and Symbol explicit""" + if isinstance(x, BlockMatrix): + return x.as_explicit() + elif isinstance(x, MatrixSymbol) and all(_.is_Integer for _ in x.shape): + return x.as_explicit() + else: + return x + + def make_explicit_row(row): + # Could be list or could be list of lists + if isinstance(row, (list, tuple)): + return [make_explicit(x) for x in row] + else: + return make_explicit(row) + + if isinstance(dat, (list, tuple)): + dat = [make_explicit_row(row) for row in dat] + + if len(dat) == 0: + rows = cols = 0 + flat_list = [] + elif all(raw(i) for i in dat) and len(dat[0]) == 0: + if not all(len(i) == 0 for i in dat): + raise ValueError('mismatched dimensions') + rows = len(dat) + cols = 0 + flat_list = [] + elif not any(raw(i) or ismat(i) for i in dat): + # a column as a list of values + flat_list = [cls._sympify(i) for i in dat] + rows = len(flat_list) + cols = 1 if rows else 0 + elif evaluate and all(ismat(i) for i in dat): + # a column as a list of matrices + ncol = {i.cols for i in dat if any(i.shape)} + if ncol: + if len(ncol) != 1: + raise ValueError('mismatched dimensions') + flat_list = [_ for i in dat for r in i.tolist() for _ in r] + cols = ncol.pop() + rows = len(flat_list)//cols + else: + rows = cols = 0 + flat_list = [] + elif evaluate and any(ismat(i) for i in dat): + ncol = set() + flat_list = [] + for i in dat: + if ismat(i): + flat_list.extend( + [k for j in i.tolist() for k in j]) + if any(i.shape): + ncol.add(i.cols) + elif raw(i): + if i: + ncol.add(len(i)) + flat_list.extend([cls._sympify(ij) for ij in i]) + else: + ncol.add(1) + flat_list.append(i) + if len(ncol) > 1: + raise ValueError('mismatched dimensions') + cols = ncol.pop() + rows = len(flat_list)//cols + else: + # list of lists; each sublist is a logical row + # which might consist of many rows if the values in + # the row are matrices + flat_list = [] + ncol = set() + rows = cols = 0 + for row in dat: + if not is_sequence(row) and \ + not getattr(row, 'is_Matrix', False): + raise ValueError('expecting list of lists') + + if hasattr(row, '__array__'): + if 0 in row.shape: + continue + + if evaluate and all(ismat(i) for i in row): + r, c, flatT = cls._handle_creation_inputs( + [i.T for i in row]) + T = reshape(flatT, [c]) + flat = \ + [T[i][j] for j in range(c) for i in range(r)] + r, c = c, r + else: + r = 1 + if getattr(row, 'is_Matrix', False): + c = 1 + flat = [row] + else: + c = len(row) + flat = [cls._sympify(i) for i in row] + ncol.add(c) + if len(ncol) > 1: + raise ValueError('mismatched dimensions') + flat_list.extend(flat) + rows += r + cols = ncol.pop() if ncol else 0 + + elif len(args) == 3: + rows = as_int(args[0]) + cols = as_int(args[1]) + + if rows < 0 or cols < 0: + raise ValueError("Cannot create a {} x {} matrix. " + "Both dimensions must be positive".format(rows, cols)) + + # Matrix(2, 2, lambda i, j: i+j) + if len(args) == 3 and isinstance(args[2], Callable): + op = args[2] + flat_list = [] + for i in range(rows): + flat_list.extend( + [cls._sympify(op(cls._sympify(i), cls._sympify(j))) + for j in range(cols)]) + + # Matrix(2, 2, [1, 2, 3, 4]) + elif len(args) == 3 and is_sequence(args[2]): + flat_list = args[2] + if len(flat_list) != rows * cols: + raise ValueError( + 'List length should be equal to rows*columns') + flat_list = [cls._sympify(i) for i in flat_list] + + + # Matrix() + elif len(args) == 0: + # Empty Matrix + rows = cols = 0 + flat_list = [] + + if flat_list is None: + raise TypeError(filldedent(''' + Data type not understood; expecting list of lists + or lists of values.''')) + + return rows, cols, flat_list + + def _setitem(self, key, value): + """Helper to set value at location given by key. + + Examples + ======== + + >>> from sympy import Matrix, I, zeros, ones + >>> m = Matrix(((1, 2+I), (3, 4))) + >>> m + Matrix([ + [1, 2 + I], + [3, 4]]) + >>> m[1, 0] = 9 + >>> m + Matrix([ + [1, 2 + I], + [9, 4]]) + >>> m[1, 0] = [[0, 1]] + + To replace row r you assign to position r*m where m + is the number of columns: + + >>> M = zeros(4) + >>> m = M.cols + >>> M[3*m] = ones(1, m)*2; M + Matrix([ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [2, 2, 2, 2]]) + + And to replace column c you can assign to position c: + + >>> M[2] = ones(m, 1)*4; M + Matrix([ + [0, 0, 4, 0], + [0, 0, 4, 0], + [0, 0, 4, 0], + [2, 2, 4, 2]]) + """ + from .dense import Matrix + + is_slice = isinstance(key, slice) + i, j = key = self.key2ij(key) + is_mat = isinstance(value, MatrixBase) + if isinstance(i, slice) or isinstance(j, slice): + if is_mat: + self.copyin_matrix(key, value) + return + if not isinstance(value, Expr) and is_sequence(value): + self.copyin_list(key, value) + return + raise ValueError('unexpected value: %s' % value) + else: + if (not is_mat and + not isinstance(value, Basic) and is_sequence(value)): + value = Matrix(value) + is_mat = True + if is_mat: + if is_slice: + key = (slice(*divmod(i, self.cols)), + slice(*divmod(j, self.cols))) + else: + key = (slice(i, i + value.rows), + slice(j, j + value.cols)) + self.copyin_matrix(key, value) + else: + return i, j, self._sympify(value) + return + + def add(self, b): + """Return self + b.""" + return self + b + + def condition_number(self): + """Returns the condition number of a matrix. + + This is the maximum singular value divided by the minimum singular value + + Examples + ======== + + >>> from sympy import Matrix, S + >>> A = Matrix([[1, 0, 0], [0, 10, 0], [0, 0, S.One/10]]) + >>> A.condition_number() + 100 + + See Also + ======== + + singular_values + """ + + if not self: + return self.zero + singularvalues = self.singular_values() + return Max(*singularvalues) / Min(*singularvalues) + + def copy(self): + """ + Returns the copy of a matrix. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix(2, 2, [1, 2, 3, 4]) + >>> A.copy() + Matrix([ + [1, 2], + [3, 4]]) + + """ + return self._new(self.rows, self.cols, self.flat()) + + def cross(self, b): + r""" + Return the cross product of ``self`` and ``b`` relaxing the condition + of compatible dimensions: if each has 3 elements, a matrix of the + same type and shape as ``self`` will be returned. If ``b`` has the same + shape as ``self`` then common identities for the cross product (like + `a \times b = - b \times a`) will hold. + + Parameters + ========== + b : 3x1 or 1x3 Matrix + + See Also + ======== + + dot + hat + vee + multiply + multiply_elementwise + """ + from sympy.matrices.expressions.matexpr import MatrixExpr + + if not isinstance(b, (MatrixBase, MatrixExpr)): + raise TypeError( + "{} must be a Matrix, not {}.".format(b, type(b))) + + if not (self.rows * self.cols == b.rows * b.cols == 3): + raise ShapeError("Dimensions incorrect for cross product: %s x %s" % + ((self.rows, self.cols), (b.rows, b.cols))) + else: + return self._new(self.rows, self.cols, ( + (self[1] * b[2] - self[2] * b[1]), + (self[2] * b[0] - self[0] * b[2]), + (self[0] * b[1] - self[1] * b[0]))) + + def hat(self): + r""" + Return the skew-symmetric matrix representing the cross product, + so that ``self.hat() * b`` is equivalent to ``self.cross(b)``. + + Examples + ======== + + Calling ``hat`` creates a skew-symmetric 3x3 Matrix from a 3x1 Matrix: + + >>> from sympy import Matrix + >>> a = Matrix([1, 2, 3]) + >>> a.hat() + Matrix([ + [ 0, -3, 2], + [ 3, 0, -1], + [-2, 1, 0]]) + + Multiplying it with another 3x1 Matrix calculates the cross product: + + >>> b = Matrix([3, 2, 1]) + >>> a.hat() * b + Matrix([ + [-4], + [ 8], + [-4]]) + + Which is equivalent to calling the ``cross`` method: + + >>> a.cross(b) + Matrix([ + [-4], + [ 8], + [-4]]) + + See Also + ======== + + dot + cross + vee + multiply + multiply_elementwise + """ + + if self.shape != (3, 1): + raise ShapeError("Dimensions incorrect, expected (3, 1), got " + + str(self.shape)) + else: + x, y, z = self + return self._new(3, 3, ( + 0, -z, y, + z, 0, -x, + -y, x, 0)) + + def vee(self): + r""" + Return a 3x1 vector from a skew-symmetric matrix representing the cross product, + so that ``self * b`` is equivalent to ``self.vee().cross(b)``. + + Examples + ======== + + Calling ``vee`` creates a vector from a skew-symmetric Matrix: + + >>> from sympy import Matrix + >>> A = Matrix([[0, -3, 2], [3, 0, -1], [-2, 1, 0]]) + >>> a = A.vee() + >>> a + Matrix([ + [1], + [2], + [3]]) + + Calculating the matrix product of the original matrix with a vector + is equivalent to a cross product: + + >>> b = Matrix([3, 2, 1]) + >>> A * b + Matrix([ + [-4], + [ 8], + [-4]]) + + >>> a.cross(b) + Matrix([ + [-4], + [ 8], + [-4]]) + + ``vee`` can also be used to retrieve angular velocity expressions. + Defining a rotation matrix: + + >>> from sympy import rot_ccw_axis3, trigsimp + >>> from sympy.physics.mechanics import dynamicsymbols + >>> theta = dynamicsymbols('theta') + >>> R = rot_ccw_axis3(theta) + >>> R + Matrix([ + [cos(theta(t)), -sin(theta(t)), 0], + [sin(theta(t)), cos(theta(t)), 0], + [ 0, 0, 1]]) + + We can retrieve the angular velocity: + + >>> Omega = R.T * R.diff() + >>> Omega = trigsimp(Omega) + >>> Omega.vee() + Matrix([ + [ 0], + [ 0], + [Derivative(theta(t), t)]]) + + See Also + ======== + + dot + cross + hat + multiply + multiply_elementwise + """ + + if self.shape != (3, 3): + raise ShapeError("Dimensions incorrect, expected (3, 3), got " + + str(self.shape)) + elif not self.is_anti_symmetric(): + raise ValueError("Matrix is not skew-symmetric") + else: + return self._new(3, 1, ( + self[2, 1], + self[0, 2], + self[1, 0])) + + @property + def D(self): + """Return Dirac conjugate (if ``self.rows == 4``). + + Examples + ======== + + >>> from sympy import Matrix, I, eye + >>> m = Matrix((0, 1 + I, 2, 3)) + >>> m.D + Matrix([[0, 1 - I, -2, -3]]) + >>> m = (eye(4) + I*eye(4)) + >>> m[0, 3] = 2 + >>> m.D + Matrix([ + [1 - I, 0, 0, 0], + [ 0, 1 - I, 0, 0], + [ 0, 0, -1 + I, 0], + [ 2, 0, 0, -1 + I]]) + + If the matrix does not have 4 rows an AttributeError will be raised + because this property is only defined for matrices with 4 rows. + + >>> Matrix(eye(2)).D + Traceback (most recent call last): + ... + AttributeError: Matrix has no attribute D. + + See Also + ======== + + sympy.matrices.matrixbase.MatrixBase.conjugate: By-element conjugation + sympy.matrices.matrixbase.MatrixBase.H: Hermite conjugation + """ + from sympy.physics.matrices import mgamma + if self.rows != 4: + # In Python 3.2, properties can only return an AttributeError + # so we can't raise a ShapeError -- see commit which added the + # first line of this inline comment. Also, there is no need + # for a message since MatrixBase will raise the AttributeError + raise AttributeError + return self.H * mgamma(0) + + def dot(self, b, hermitian=None, conjugate_convention=None): + """Return the dot or inner product of two vectors of equal length. + Here ``self`` must be a ``Matrix`` of size 1 x n or n x 1, and ``b`` + must be either a matrix of size 1 x n, n x 1, or a list/tuple of length n. + A scalar is returned. + + By default, ``dot`` does not conjugate ``self`` or ``b``, even if there are + complex entries. Set ``hermitian=True`` (and optionally a ``conjugate_convention``) + to compute the hermitian inner product. + + Possible kwargs are ``hermitian`` and ``conjugate_convention``. + + If ``conjugate_convention`` is ``"left"``, ``"math"`` or ``"maths"``, + the conjugate of the first vector (``self``) is used. If ``"right"`` + or ``"physics"`` is specified, the conjugate of the second vector ``b`` is used. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> v = Matrix([1, 1, 1]) + >>> M.row(0).dot(v) + 6 + >>> M.col(0).dot(v) + 12 + >>> v = [3, 2, 1] + >>> M.row(0).dot(v) + 10 + + >>> from sympy import I + >>> q = Matrix([1*I, 1*I, 1*I]) + >>> q.dot(q, hermitian=False) + -3 + + >>> q.dot(q, hermitian=True) + 3 + + >>> q1 = Matrix([1, 1, 1*I]) + >>> q.dot(q1, hermitian=True, conjugate_convention="maths") + 1 - 2*I + >>> q.dot(q1, hermitian=True, conjugate_convention="physics") + 1 + 2*I + + + See Also + ======== + + cross + multiply + multiply_elementwise + """ + from .dense import Matrix + + if not isinstance(b, MatrixBase): + if is_sequence(b): + if len(b) != self.cols and len(b) != self.rows: + raise ShapeError( + "Dimensions incorrect for dot product: %s, %s" % ( + self.shape, len(b))) + return self.dot(Matrix(b)) + else: + raise TypeError( + "`b` must be an ordered iterable or Matrix, not %s." % + type(b)) + + if (1 not in self.shape) or (1 not in b.shape): + raise ShapeError + if len(self) != len(b): + raise ShapeError( + "Dimensions incorrect for dot product: %s, %s" % (self.shape, b.shape)) + + mat = self + n = len(mat) + if mat.shape != (1, n): + mat = mat.reshape(1, n) + if b.shape != (n, 1): + b = b.reshape(n, 1) + + # Now ``mat`` is a row vector and ``b`` is a column vector. + + # If it so happens that only conjugate_convention is passed + # then automatically set hermitian to True. If only hermitian + # is true but no conjugate_convention is not passed then + # automatically set it to ``"maths"`` + + if conjugate_convention is not None and hermitian is None: + hermitian = True + if hermitian and conjugate_convention is None: + conjugate_convention = "maths" + + if hermitian == True: + if conjugate_convention in ("maths", "left", "math"): + mat = mat.conjugate() + elif conjugate_convention in ("physics", "right"): + b = b.conjugate() + else: + raise ValueError("Unknown conjugate_convention was entered." + " conjugate_convention must be one of the" + " following: math, maths, left, physics or right.") + return (mat * b)[0] + + def dual(self): + """Returns the dual of a matrix. + + A dual of a matrix is: + + ``(1/2)*levicivita(i, j, k, l)*M(k, l)`` summed over indices `k` and `l` + + Since the levicivita method is anti_symmetric for any pairwise + exchange of indices, the dual of a symmetric matrix is the zero + matrix. Strictly speaking the dual defined here assumes that the + 'matrix' `M` is a contravariant anti_symmetric second rank tensor, + so that the dual is a covariant second rank tensor. + + """ + from sympy.matrices import zeros + + M, n = self[:, :], self.rows + work = zeros(n) + if self.is_symmetric(): + return work + + for i in range(1, n): + for j in range(1, n): + acum = 0 + for k in range(1, n): + acum += LeviCivita(i, j, 0, k) * M[0, k] + work[i, j] = acum + work[j, i] = -acum + + for l in range(1, n): + acum = 0 + for a in range(1, n): + for b in range(1, n): + acum += LeviCivita(0, l, a, b) * M[a, b] + acum /= 2 + work[0, l] = -acum + work[l, 0] = acum + + return work + + def _eval_matrix_exp_jblock(self): + """A helper function to compute an exponential of a Jordan block + matrix + + Examples + ======== + + >>> from sympy import Symbol, Matrix + >>> l = Symbol('lamda') + + A trivial example of 1*1 Jordan block: + + >>> m = Matrix.jordan_block(1, l) + >>> m._eval_matrix_exp_jblock() + Matrix([[exp(lamda)]]) + + An example of 3*3 Jordan block: + + >>> m = Matrix.jordan_block(3, l) + >>> m._eval_matrix_exp_jblock() + Matrix([ + [exp(lamda), exp(lamda), exp(lamda)/2], + [ 0, exp(lamda), exp(lamda)], + [ 0, 0, exp(lamda)]]) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Matrix_function#Jordan_decomposition + """ + size = self.rows + l = self[0, 0] + exp_l = exp(l) + + bands = {i: exp_l / factorial(i) for i in range(size)} + + from .sparsetools import banded + return self.__class__(banded(size, bands)) + + + def analytic_func(self, f, x): + """ + Computes f(A) where A is a Square Matrix + and f is an analytic function. + + Examples + ======== + + >>> from sympy import Symbol, Matrix, S, log + + >>> x = Symbol('x') + >>> m = Matrix([[S(5)/4, S(3)/4], [S(3)/4, S(5)/4]]) + >>> f = log(x) + >>> m.analytic_func(f, x) + Matrix([ + [ 0, log(2)], + [log(2), 0]]) + + Parameters + ========== + + f : Expr + Analytic Function + x : Symbol + parameter of f + + """ + + f, x = _sympify(f), _sympify(x) + if not self.is_square: + raise NonSquareMatrixError + if not x.is_symbol: + raise ValueError("{} must be a symbol.".format(x)) + if x not in f.free_symbols: + raise ValueError( + "{} must be a parameter of {}.".format(x, f)) + if x in self.free_symbols: + raise ValueError( + "{} must not be a parameter of {}.".format(x, self)) + + eigen = self.eigenvals() + max_mul = max(eigen.values()) + derivative = {} + dd = f + for i in range(max_mul - 1): + dd = diff(dd, x) + derivative[i + 1] = dd + n = self.shape[0] + r = self.zeros(n) + f_val = self.zeros(n, 1) + row = 0 + + for i in eigen: + mul = eigen[i] + f_val[row] = f.subs(x, i) + if f_val[row].is_number and not f_val[row].is_complex: + raise ValueError( + "Cannot evaluate the function because the " + "function {} is not analytic at the given " + "eigenvalue {}".format(f, f_val[row])) + val = 1 + for a in range(n): + r[row, a] = val + val *= i + if mul > 1: + coe = [1 for ii in range(n)] + deri = 1 + while mul > 1: + row = row + 1 + mul -= 1 + d_i = derivative[deri].subs(x, i) + if d_i.is_number and not d_i.is_complex: + raise ValueError( + "Cannot evaluate the function because the " + "derivative {} is not analytic at the given " + "eigenvalue {}".format(derivative[deri], d_i)) + f_val[row] = d_i + for a in range(n): + if a - deri + 1 <= 0: + r[row, a] = 0 + coe[a] = 0 + continue + coe[a] = coe[a]*(a - deri + 1) + r[row, a] = coe[a]*pow(i, a - deri) + deri += 1 + row += 1 + c = r.solve(f_val) + ans = self.zeros(n) + pre = self.eye(n) + for i in range(n): + ans = ans + c[i]*pre + pre *= self + return ans + + + def exp(self): + """Return the exponential of a square matrix. + + Examples + ======== + + >>> from sympy import Symbol, Matrix + + >>> t = Symbol('t') + >>> m = Matrix([[0, 1], [-1, 0]]) * t + >>> m.exp() + Matrix([ + [ exp(I*t)/2 + exp(-I*t)/2, -I*exp(I*t)/2 + I*exp(-I*t)/2], + [I*exp(I*t)/2 - I*exp(-I*t)/2, exp(I*t)/2 + exp(-I*t)/2]]) + """ + if not self.is_square: + raise NonSquareMatrixError( + "Exponentiation is valid only for square matrices") + try: + P, J = self.jordan_form() + cells = J.get_diag_blocks() + except MatrixError: + raise NotImplementedError( + "Exponentiation is implemented only for matrices for which the Jordan normal form can be computed") + + blocks = [cell._eval_matrix_exp_jblock() for cell in cells] + from sympy.matrices import diag + eJ = diag(*blocks) + # n = self.rows + ret = P.multiply(eJ, dotprodsimp=None).multiply(P.inv(), dotprodsimp=None) + if all(value.is_real for value in self.values()): + return type(self)(re(ret)) + else: + return type(self)(ret) + + def _eval_matrix_log_jblock(self): + """Helper function to compute logarithm of a jordan block. + + Examples + ======== + + >>> from sympy import Symbol, Matrix + >>> l = Symbol('lamda') + + A trivial example of 1*1 Jordan block: + + >>> m = Matrix.jordan_block(1, l) + >>> m._eval_matrix_log_jblock() + Matrix([[log(lamda)]]) + + An example of 3*3 Jordan block: + + >>> m = Matrix.jordan_block(3, l) + >>> m._eval_matrix_log_jblock() + Matrix([ + [log(lamda), 1/lamda, -1/(2*lamda**2)], + [ 0, log(lamda), 1/lamda], + [ 0, 0, log(lamda)]]) + """ + size = self.rows + l = self[0, 0] + + if l.is_zero: + raise MatrixError( + 'Could not take logarithm or reciprocal for the given ' + 'eigenvalue {}'.format(l)) + + bands = {0: log(l)} + for i in range(1, size): + bands[i] = -((-l) ** -i) / i + + from .sparsetools import banded + return self.__class__(banded(size, bands)) + + def log(self, simplify=cancel): + """Return the logarithm of a square matrix. + + Parameters + ========== + + simplify : function, bool + The function to simplify the result with. + + Default is ``cancel``, which is effective to reduce the + expression growing for taking reciprocals and inverses for + symbolic matrices. + + Examples + ======== + + >>> from sympy import S, Matrix + + Examples for positive-definite matrices: + + >>> m = Matrix([[1, 1], [0, 1]]) + >>> m.log() + Matrix([ + [0, 1], + [0, 0]]) + + >>> m = Matrix([[S(5)/4, S(3)/4], [S(3)/4, S(5)/4]]) + >>> m.log() + Matrix([ + [ 0, log(2)], + [log(2), 0]]) + + Examples for non positive-definite matrices: + + >>> m = Matrix([[S(3)/4, S(5)/4], [S(5)/4, S(3)/4]]) + >>> m.log() + Matrix([ + [ I*pi/2, log(2) - I*pi/2], + [log(2) - I*pi/2, I*pi/2]]) + + >>> m = Matrix( + ... [[0, 0, 0, 1], + ... [0, 0, 1, 0], + ... [0, 1, 0, 0], + ... [1, 0, 0, 0]]) + >>> m.log() + Matrix([ + [ I*pi/2, 0, 0, -I*pi/2], + [ 0, I*pi/2, -I*pi/2, 0], + [ 0, -I*pi/2, I*pi/2, 0], + [-I*pi/2, 0, 0, I*pi/2]]) + """ + if not self.is_square: + raise NonSquareMatrixError( + "Logarithm is valid only for square matrices") + + try: + if simplify: + P, J = simplify(self).jordan_form() + else: + P, J = self.jordan_form() + + cells = J.get_diag_blocks() + except MatrixError: + raise NotImplementedError( + "Logarithm is implemented only for matrices for which " + "the Jordan normal form can be computed") + + blocks = [ + cell._eval_matrix_log_jblock() + for cell in cells] + from sympy.matrices import diag + eJ = diag(*blocks) + + if simplify: + ret = simplify(P * eJ * simplify(P.inv())) + ret = self.__class__(ret) + else: + ret = P * eJ * P.inv() + + return ret + + def is_nilpotent(self): + """Checks if a matrix is nilpotent. + + A matrix B is nilpotent if for some integer k, B**k is + a zero matrix. + + Examples + ======== + + >>> from sympy import Matrix + >>> a = Matrix([[0, 0, 0], [1, 0, 0], [1, 1, 0]]) + >>> a.is_nilpotent() + True + + >>> a = Matrix([[1, 0, 1], [1, 0, 0], [1, 1, 0]]) + >>> a.is_nilpotent() + False + """ + if not self: + return True + if not self.is_square: + raise NonSquareMatrixError( + "Nilpotency is valid only for square matrices") + x = uniquely_named_symbol('x', self, modify=lambda s: '_' + s) + p = self.charpoly(x) + if p.args[0] == x ** self.rows: + return True + return False + + def key2bounds(self, keys): + """Converts a key with potentially mixed types of keys (integer and slice) + into a tuple of ranges and raises an error if any index is out of ``self``'s + range. + + See Also + ======== + + key2ij + """ + islice, jslice = [isinstance(k, slice) for k in keys] + if islice: + if not self.rows: + rlo = rhi = 0 + else: + rlo, rhi = keys[0].indices(self.rows)[:2] + else: + rlo = a2idx(keys[0], self.rows) + rhi = rlo + 1 + if jslice: + if not self.cols: + clo = chi = 0 + else: + clo, chi = keys[1].indices(self.cols)[:2] + else: + clo = a2idx(keys[1], self.cols) + chi = clo + 1 + return rlo, rhi, clo, chi + + def key2ij(self, key): + """Converts key into canonical form, converting integers or indexable + items into valid integers for ``self``'s range or returning slices + unchanged. + + See Also + ======== + + key2bounds + """ + if is_sequence(key): + if not len(key) == 2: + raise TypeError('key must be a sequence of length 2') + return [a2idx(i, n) if not isinstance(i, slice) else i + for i, n in zip(key, self.shape)] + elif isinstance(key, slice): + return key.indices(len(self))[:2] + else: + return divmod(a2idx(key, len(self)), self.cols) + + def normalized(self, iszerofunc=_iszero): + """Return the normalized version of ``self``. + + Parameters + ========== + + iszerofunc : Function, optional + A function to determine whether ``self`` is a zero vector. + The default ``_iszero`` tests to see if each element is + exactly zero. + + Returns + ======= + + Matrix + Normalized vector form of ``self``. + It has the same length as a unit vector. However, a zero vector + will be returned for a vector with norm 0. + + Raises + ====== + + ShapeError + If the matrix is not in a vector form. + + See Also + ======== + + norm + """ + if self.rows != 1 and self.cols != 1: + raise ShapeError("A Matrix must be a vector to normalize.") + norm = self.norm() + if iszerofunc(norm): + out = self.zeros(self.rows, self.cols) + else: + out = self.applyfunc(lambda i: i / norm) + return out + + def norm(self, ord=None): + """Return the Norm of a Matrix or Vector. + + In the simplest case this is the geometric size of the vector + Other norms can be specified by the ord parameter + + + ===== ============================ ========================== + ord norm for matrices norm for vectors + ===== ============================ ========================== + None Frobenius norm 2-norm + 'fro' Frobenius norm - does not exist + inf maximum row sum max(abs(x)) + -inf -- min(abs(x)) + 1 maximum column sum as below + -1 -- as below + 2 2-norm (largest sing. value) as below + -2 smallest singular value as below + other - does not exist sum(abs(x)**ord)**(1./ord) + ===== ============================ ========================== + + Examples + ======== + + >>> from sympy import Matrix, Symbol, trigsimp, cos, sin, oo + >>> x = Symbol('x', real=True) + >>> v = Matrix([cos(x), sin(x)]) + >>> trigsimp( v.norm() ) + 1 + >>> v.norm(10) + (sin(x)**10 + cos(x)**10)**(1/10) + >>> A = Matrix([[1, 1], [1, 1]]) + >>> A.norm(1) # maximum sum of absolute values of A is 2 + 2 + >>> A.norm(2) # Spectral norm (max of |Ax|/|x| under 2-vector-norm) + 2 + >>> A.norm(-2) # Inverse spectral norm (smallest singular value) + 0 + >>> A.norm() # Frobenius Norm + 2 + >>> A.norm(oo) # Infinity Norm + 2 + >>> Matrix([1, -2]).norm(oo) + 2 + >>> Matrix([-1, 2]).norm(-oo) + 1 + + See Also + ======== + + normalized + """ + # Row or Column Vector Norms + vals = list(self.values()) or [0] + if S.One in self.shape: + if ord in (2, None): # Common case sqrt() + return sqrt(Add(*(abs(i) ** 2 for i in vals))) + + elif ord == 1: # sum(abs(x)) + return Add(*(abs(i) for i in vals)) + + elif ord is S.Infinity: # max(abs(x)) + return Max(*[abs(i) for i in vals]) + + elif ord is S.NegativeInfinity: # min(abs(x)) + return Min(*[abs(i) for i in vals]) + + # Otherwise generalize the 2-norm, Sum(x_i**ord)**(1/ord) + # Note that while useful this is not mathematically a norm + try: + return Pow(Add(*(abs(i) ** ord for i in vals)), S.One / ord) + except (NotImplementedError, TypeError): + raise ValueError("Expected order to be Number, Symbol, oo") + + # Matrix Norms + else: + if ord == 1: # Maximum column sum + m = self.applyfunc(abs) + return Max(*[sum(m.col(i)) for i in range(m.cols)]) + + elif ord == 2: # Spectral Norm + # Maximum singular value + return Max(*self.singular_values()) + + elif ord == -2: + # Minimum singular value + return Min(*self.singular_values()) + + elif ord is S.Infinity: # Infinity Norm - Maximum row sum + m = self.applyfunc(abs) + return Max(*[sum(m.row(i)) for i in range(m.rows)]) + + elif (ord is None or isinstance(ord, + str) and ord.lower() in + ['f', 'fro', 'frobenius', 'vector']): + # Reshape as vector and send back to norm function + return self.vec().norm(ord=2) + + else: + raise NotImplementedError("Matrix Norms under development") + + def print_nonzero(self, symb="X"): + """Shows location of non-zero entries for fast shape lookup. + + Examples + ======== + + >>> from sympy import Matrix, eye + >>> m = Matrix(2, 3, lambda i, j: i*3+j) + >>> m + Matrix([ + [0, 1, 2], + [3, 4, 5]]) + >>> m.print_nonzero() + [ XX] + [XXX] + >>> m = eye(4) + >>> m.print_nonzero("x") + [x ] + [ x ] + [ x ] + [ x] + + """ + s = [] + for i in range(self.rows): + line = [] + for j in range(self.cols): + if self[i, j] == 0: + line.append(" ") + else: + line.append(str(symb)) + s.append("[%s]" % ''.join(line)) + print('\n'.join(s)) + + def project(self, v): + """Return the projection of ``self`` onto the line containing ``v``. + + Examples + ======== + + >>> from sympy import Matrix, S, sqrt + >>> V = Matrix([sqrt(3)/2, S.Half]) + >>> x = Matrix([[1, 0]]) + >>> V.project(x) + Matrix([[sqrt(3)/2, 0]]) + >>> V.project(-x) + Matrix([[sqrt(3)/2, 0]]) + """ + return v * (self.dot(v) / v.dot(v)) + + def table(self, printer, rowstart='[', rowend=']', rowsep='\n', + colsep=', ', align='right'): + r""" + String form of Matrix as a table. + + ``printer`` is the printer to use for on the elements (generally + something like StrPrinter()) + + ``rowstart`` is the string used to start each row (by default '['). + + ``rowend`` is the string used to end each row (by default ']'). + + ``rowsep`` is the string used to separate rows (by default a newline). + + ``colsep`` is the string used to separate columns (by default ', '). + + ``align`` defines how the elements are aligned. Must be one of 'left', + 'right', or 'center'. You can also use '<', '>', and '^' to mean the + same thing, respectively. + + This is used by the string printer for Matrix. + + Examples + ======== + + >>> from sympy import Matrix, StrPrinter + >>> M = Matrix([[1, 2], [-33, 4]]) + >>> printer = StrPrinter() + >>> M.table(printer) + '[ 1, 2]\n[-33, 4]' + >>> print(M.table(printer)) + [ 1, 2] + [-33, 4] + >>> print(M.table(printer, rowsep=',\n')) + [ 1, 2], + [-33, 4] + >>> print('[%s]' % M.table(printer, rowsep=',\n')) + [[ 1, 2], + [-33, 4]] + >>> print(M.table(printer, colsep=' ')) + [ 1 2] + [-33 4] + >>> print(M.table(printer, align='center')) + [ 1 , 2] + [-33, 4] + >>> print(M.table(printer, rowstart='{', rowend='}')) + { 1, 2} + {-33, 4} + """ + # Handle zero dimensions: + if S.Zero in self.shape: + return '[]' + # Build table of string representations of the elements + res = [] + # Track per-column max lengths for pretty alignment + maxlen = [0] * self.cols + for i in range(self.rows): + res.append([]) + for j in range(self.cols): + s = printer._print(self[i, j]) + res[-1].append(s) + maxlen[j] = max(len(s), maxlen[j]) + # Patch strings together + align = { + 'left': 'ljust', + 'right': 'rjust', + 'center': 'center', + '<': 'ljust', + '>': 'rjust', + '^': 'center', + }[align] + for i, row in enumerate(res): + for j, elem in enumerate(row): + row[j] = getattr(elem, align)(maxlen[j]) + res[i] = rowstart + colsep.join(row) + rowend + return rowsep.join(res) + + def rank_decomposition(self, iszerofunc=_iszero, simplify=False): + return _rank_decomposition(self, iszerofunc=iszerofunc, + simplify=simplify) + + def cholesky(self, hermitian=True): + raise NotImplementedError('This function is implemented in DenseMatrix or SparseMatrix') + + def LDLdecomposition(self, hermitian=True): + raise NotImplementedError('This function is implemented in DenseMatrix or SparseMatrix') + + def LUdecomposition(self, iszerofunc=_iszero, simpfunc=None, + rankcheck=False): + return _LUdecomposition(self, iszerofunc=iszerofunc, simpfunc=simpfunc, + rankcheck=rankcheck) + + def LUdecomposition_Simple(self, iszerofunc=_iszero, simpfunc=None, + rankcheck=False): + return _LUdecomposition_Simple(self, iszerofunc=iszerofunc, + simpfunc=simpfunc, rankcheck=rankcheck) + + def LUdecompositionFF(self): + return _LUdecompositionFF(self) + + def singular_value_decomposition(self): + return _singular_value_decomposition(self) + + def QRdecomposition(self): + return _QRdecomposition(self) + + def upper_hessenberg_decomposition(self): + return _upper_hessenberg_decomposition(self) + + def diagonal_solve(self, rhs): + return _diagonal_solve(self, rhs) + + def lower_triangular_solve(self, rhs): + raise NotImplementedError('This function is implemented in DenseMatrix or SparseMatrix') + + def upper_triangular_solve(self, rhs): + raise NotImplementedError('This function is implemented in DenseMatrix or SparseMatrix') + + def cholesky_solve(self, rhs): + return _cholesky_solve(self, rhs) + + def LDLsolve(self, rhs): + return _LDLsolve(self, rhs) + + def LUsolve(self, rhs, iszerofunc=_iszero): + return _LUsolve(self, rhs, iszerofunc=iszerofunc) + + def QRsolve(self, b): + return _QRsolve(self, b) + + def gauss_jordan_solve(self, B, freevar=False): + return _gauss_jordan_solve(self, B, freevar=freevar) + + def pinv_solve(self, B, arbitrary_matrix=None): + return _pinv_solve(self, B, arbitrary_matrix=arbitrary_matrix) + + def cramer_solve(self, rhs, det_method="laplace"): + return _cramer_solve(self, rhs, det_method=det_method) + + def solve(self, rhs, method='GJ'): + return _solve(self, rhs, method=method) + + def solve_least_squares(self, rhs, method='CH'): + return _solve_least_squares(self, rhs, method=method) + + def pinv(self, method='RD'): + return _pinv(self, method=method) + + def inverse_ADJ(self, iszerofunc=_iszero): + return _inv_ADJ(self, iszerofunc=iszerofunc) + + def inverse_BLOCK(self, iszerofunc=_iszero): + return _inv_block(self, iszerofunc=iszerofunc) + + def inverse_GE(self, iszerofunc=_iszero): + return _inv_GE(self, iszerofunc=iszerofunc) + + def inverse_LU(self, iszerofunc=_iszero): + return _inv_LU(self, iszerofunc=iszerofunc) + + def inverse_CH(self, iszerofunc=_iszero): + return _inv_CH(self, iszerofunc=iszerofunc) + + def inverse_LDL(self, iszerofunc=_iszero): + return _inv_LDL(self, iszerofunc=iszerofunc) + + def inverse_QR(self, iszerofunc=_iszero): + return _inv_QR(self, iszerofunc=iszerofunc) + + def inv(self, method=None, iszerofunc=_iszero, try_block_diag=False): + return _inv(self, method=method, iszerofunc=iszerofunc, + try_block_diag=try_block_diag) + + def connected_components(self): + return _connected_components(self) + + def connected_components_decomposition(self): + return _connected_components_decomposition(self) + + def strongly_connected_components(self): + return _strongly_connected_components(self) + + def strongly_connected_components_decomposition(self, lower=True): + return _strongly_connected_components_decomposition(self, lower=lower) + + _sage_ = Basic._sage_ + + rank_decomposition.__doc__ = _rank_decomposition.__doc__ + cholesky.__doc__ = _cholesky.__doc__ + LDLdecomposition.__doc__ = _LDLdecomposition.__doc__ + LUdecomposition.__doc__ = _LUdecomposition.__doc__ + LUdecomposition_Simple.__doc__ = _LUdecomposition_Simple.__doc__ + LUdecompositionFF.__doc__ = _LUdecompositionFF.__doc__ + singular_value_decomposition.__doc__ = _singular_value_decomposition.__doc__ + QRdecomposition.__doc__ = _QRdecomposition.__doc__ + upper_hessenberg_decomposition.__doc__ = _upper_hessenberg_decomposition.__doc__ + + diagonal_solve.__doc__ = _diagonal_solve.__doc__ + lower_triangular_solve.__doc__ = _lower_triangular_solve.__doc__ + upper_triangular_solve.__doc__ = _upper_triangular_solve.__doc__ + cholesky_solve.__doc__ = _cholesky_solve.__doc__ + LDLsolve.__doc__ = _LDLsolve.__doc__ + LUsolve.__doc__ = _LUsolve.__doc__ + QRsolve.__doc__ = _QRsolve.__doc__ + gauss_jordan_solve.__doc__ = _gauss_jordan_solve.__doc__ + pinv_solve.__doc__ = _pinv_solve.__doc__ + cramer_solve.__doc__ = _cramer_solve.__doc__ + solve.__doc__ = _solve.__doc__ + solve_least_squares.__doc__ = _solve_least_squares.__doc__ + + pinv.__doc__ = _pinv.__doc__ + inverse_ADJ.__doc__ = _inv_ADJ.__doc__ + inverse_GE.__doc__ = _inv_GE.__doc__ + inverse_LU.__doc__ = _inv_LU.__doc__ + inverse_CH.__doc__ = _inv_CH.__doc__ + inverse_LDL.__doc__ = _inv_LDL.__doc__ + inverse_QR.__doc__ = _inv_QR.__doc__ + inverse_BLOCK.__doc__ = _inv_block.__doc__ + inv.__doc__ = _inv.__doc__ + + connected_components.__doc__ = _connected_components.__doc__ + connected_components_decomposition.__doc__ = \ + _connected_components_decomposition.__doc__ + strongly_connected_components.__doc__ = \ + _strongly_connected_components.__doc__ + strongly_connected_components_decomposition.__doc__ = \ + _strongly_connected_components_decomposition.__doc__ + + +def _convert_matrix(typ, mat): + """Convert mat to a Matrix of type typ.""" + from sympy.matrices.matrixbase import MatrixBase + if getattr(mat, "is_Matrix", False) and not isinstance(mat, MatrixBase): + # This is needed for interop between Matrix and the redundant matrix + # mixin types like _MinimalMatrix etc. If anyone should happen to be + # using those then this keeps them working. Really _MinimalMatrix etc + # should be deprecated and removed though. + return typ(*mat.shape, list(mat)) + else: + return typ(mat) + + +def _has_matrix_shape(other): + shape = getattr(other, 'shape', None) + if shape is None: + return False + return isinstance(shape, tuple) and len(shape) == 2 + + +def _has_rows_cols(other): + return hasattr(other, 'rows') and hasattr(other, 'cols') + + +def _coerce_operand(self, other): + """Convert other to a Matrix, or check for possible scalar.""" + + INVALID = None, 'invalid_type' + + # Disallow mixing Matrix and Array + if isinstance(other, NDimArray): + return INVALID + + is_Matrix = getattr(other, 'is_Matrix', None) + + # Return a Matrix as-is + if is_Matrix: + return other, 'is_matrix' + + # Try to convert numpy array, mpmath matrix etc. + if is_Matrix is None: + if _has_matrix_shape(other) or _has_rows_cols(other): + return _convert_matrix(type(self), other), 'is_matrix' + + # Could be a scalar but only if not iterable... + if not isinstance(other, Iterable): + return other, 'possible_scalar' + + return INVALID + + +def classof(A, B): + """ + Get the type of the result when combining matrices of different types. + + Currently the strategy is that immutability is contagious. + + Examples + ======== + + >>> from sympy import Matrix, ImmutableMatrix + >>> from sympy.matrices.matrixbase import classof + >>> M = Matrix([[1, 2], [3, 4]]) # a Mutable Matrix + >>> IM = ImmutableMatrix([[1, 2], [3, 4]]) + >>> classof(M, IM) + + """ + priority_A = getattr(A, '_class_priority', None) + priority_B = getattr(B, '_class_priority', None) + if None not in (priority_A, priority_B): + if A._class_priority > B._class_priority: + return A.__class__ + else: + return B.__class__ + + try: + import numpy + except ImportError: + pass + else: + if isinstance(A, numpy.ndarray): + return B.__class__ + if isinstance(B, numpy.ndarray): + return A.__class__ + + raise TypeError("Incompatible classes %s, %s" % (A.__class__, B.__class__)) + + +def _unify_with_other(self, other): + """Unify self and other into a single matrix type, or check for scalar.""" + other, T = _coerce_operand(self, other) + + if T == "is_matrix": + typ = classof(self, other) + if typ != self.__class__: + self = _convert_matrix(typ, self) + if typ != other.__class__: + other = _convert_matrix(typ, other) + + return self, other, T + + +def a2idx(j, n=None): + """Return integer after making positive and validating against n.""" + if not isinstance(j, int): + jindex = getattr(j, '__index__', None) + if jindex is not None: + j = jindex() + else: + raise IndexError("Invalid index a[%r]" % (j,)) + if n is not None: + if j < 0: + j += n + if not (j >= 0 and j < n): + raise IndexError("Index out of range: a[%s]" % (j,)) + return int(j) + + +class DeferredVector(Symbol, NotIterable): # type: ignore + """A vector whose components are deferred (e.g. for use with lambdify). + + Examples + ======== + + >>> from sympy import DeferredVector, lambdify + >>> X = DeferredVector( 'X' ) + >>> X + X + >>> expr = (X[0] + 2, X[2] + 3) + >>> func = lambdify( X, expr) + >>> func( [1, 2, 3] ) + (3, 6) + """ + + def __getitem__(self, i): + if i == -0: + i = 0 + if i < 0: + raise IndexError('DeferredVector index out of range') + component_name = '%s[%d]' % (self.name, i) + return Symbol(component_name) + + def __str__(self): + return sstr(self) + + def __repr__(self): + return "DeferredVector('%s')" % self.name diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/normalforms.py b/venv/lib/python3.11/site-packages/sympy/matrices/normalforms.py new file mode 100644 index 0000000000000000000000000000000000000000..61a7d26bbdb8c8a3e8e3044d39b2403b2e14b7d5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/normalforms.py @@ -0,0 +1,156 @@ +'''Functions returning normal forms of matrices''' + +from sympy.polys.domains.integerring import ZZ +from sympy.polys.polytools import Poly +from sympy.polys.matrices import DomainMatrix +from sympy.polys.matrices.normalforms import ( + smith_normal_form as _snf, + is_smith_normal_form as _is_snf, + smith_normal_decomp as _snd, + invariant_factors as _invf, + hermite_normal_form as _hnf, + ) + + +def _to_domain(m, domain=None): + """Convert Matrix to DomainMatrix""" + # XXX: deprecated support for RawMatrix: + ring = getattr(m, "ring", None) + m = m.applyfunc(lambda e: e.as_expr() if isinstance(e, Poly) else e) + + dM = DomainMatrix.from_Matrix(m) + + domain = domain or ring + if domain is not None: + dM = dM.convert_to(domain) + return dM + + +def smith_normal_form(m, domain=None): + ''' + Return the Smith Normal Form of a matrix `m` over the ring `domain`. + This will only work if the ring is a principal ideal domain. + + Examples + ======== + + >>> from sympy import Matrix, ZZ + >>> from sympy.matrices.normalforms import smith_normal_form + >>> m = Matrix([[12, 6, 4], [3, 9, 6], [2, 16, 14]]) + >>> print(smith_normal_form(m, domain=ZZ)) + Matrix([[1, 0, 0], [0, 10, 0], [0, 0, 30]]) + + ''' + dM = _to_domain(m, domain) + return _snf(dM).to_Matrix() + + +def is_smith_normal_form(m, domain=None): + ''' + Checks that the matrix is in Smith Normal Form + ''' + dM = _to_domain(m, domain) + return _is_snf(dM) + + +def smith_normal_decomp(m, domain=None): + ''' + Return the Smith Normal Decomposition of a matrix `m` over the ring + `domain`. This will only work if the ring is a principal ideal domain. + + Examples + ======== + + >>> from sympy import Matrix, ZZ + >>> from sympy.matrices.normalforms import smith_normal_decomp + >>> m = Matrix([[12, 6, 4], [3, 9, 6], [2, 16, 14]]) + >>> a, s, t = smith_normal_decomp(m, domain=ZZ) + >>> assert a == s * m * t + ''' + dM = _to_domain(m, domain) + a, s, t = _snd(dM) + return a.to_Matrix(), s.to_Matrix(), t.to_Matrix() + + +def invariant_factors(m, domain=None): + ''' + Return the tuple of abelian invariants for a matrix `m` + (as in the Smith-Normal form) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Smith_normal_form#Algorithm + .. [2] https://web.archive.org/web/20200331143852/https://sierra.nmsu.edu/morandi/notes/SmithNormalForm.pdf + + ''' + dM = _to_domain(m, domain) + factors = _invf(dM) + factors = tuple(dM.domain.to_sympy(f) for f in factors) + # XXX: deprecated. + if hasattr(m, "ring"): + if m.ring.is_PolynomialRing: + K = m.ring + to_poly = lambda f: Poly(f, K.symbols, domain=K.domain) + factors = tuple(to_poly(f) for f in factors) + return factors + + +def hermite_normal_form(A, *, D=None, check_rank=False): + r""" + Compute the Hermite Normal Form of a Matrix *A* of integers. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.matrices.normalforms import hermite_normal_form + >>> m = Matrix([[12, 6, 4], [3, 9, 6], [2, 16, 14]]) + >>> print(hermite_normal_form(m)) + Matrix([[10, 0, 2], [0, 15, 3], [0, 0, 2]]) + + Parameters + ========== + + A : $m \times n$ ``Matrix`` of integers. + + D : int, optional + Let $W$ be the HNF of *A*. If known in advance, a positive integer *D* + being any multiple of $\det(W)$ may be provided. In this case, if *A* + also has rank $m$, then we may use an alternative algorithm that works + mod *D* in order to prevent coefficient explosion. + + check_rank : boolean, optional (default=False) + The basic assumption is that, if you pass a value for *D*, then + you already believe that *A* has rank $m$, so we do not waste time + checking it for you. If you do want this to be checked (and the + ordinary, non-modulo *D* algorithm to be used if the check fails), then + set *check_rank* to ``True``. + + Returns + ======= + + ``Matrix`` + The HNF of matrix *A*. + + Raises + ====== + + DMDomainError + If the domain of the matrix is not :ref:`ZZ`. + + DMShapeError + If the mod *D* algorithm is used but the matrix has more rows than + columns. + + References + ========== + + .. [1] Cohen, H. *A Course in Computational Algebraic Number Theory.* + (See Algorithms 2.4.5 and 2.4.8.) + + """ + # Accept any of Python int, SymPy Integer, and ZZ itself: + if D is not None and not ZZ.of_type(D): + D = ZZ(int(D)) + return _hnf(A._rep, D=D, check_rank=check_rank).to_Matrix() diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/reductions.py b/venv/lib/python3.11/site-packages/sympy/matrices/reductions.py new file mode 100644 index 0000000000000000000000000000000000000000..aace8c0336358e1869a34d99f79390cc0c0163fe --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/reductions.py @@ -0,0 +1,387 @@ +from types import FunctionType + +from sympy.polys.polyerrors import CoercionFailed +from sympy.polys.domains import ZZ, QQ + +from .utilities import _get_intermediate_simp, _iszero, _dotprodsimp, _simplify +from .determinant import _find_reasonable_pivot + + +def _row_reduce_list(mat, rows, cols, one, iszerofunc, simpfunc, + normalize_last=True, normalize=True, zero_above=True): + """Row reduce a flat list representation of a matrix and return a tuple + (rref_matrix, pivot_cols, swaps) where ``rref_matrix`` is a flat list, + ``pivot_cols`` are the pivot columns and ``swaps`` are any row swaps that + were used in the process of row reduction. + + Parameters + ========== + + mat : list + list of matrix elements, must be ``rows`` * ``cols`` in length + + rows, cols : integer + number of rows and columns in flat list representation + + one : SymPy object + represents the value one, from ``Matrix.one`` + + iszerofunc : determines if an entry can be used as a pivot + + simpfunc : used to simplify elements and test if they are + zero if ``iszerofunc`` returns `None` + + normalize_last : indicates where all row reduction should + happen in a fraction-free manner and then the rows are + normalized (so that the pivots are 1), or whether + rows should be normalized along the way (like the naive + row reduction algorithm) + + normalize : whether pivot rows should be normalized so that + the pivot value is 1 + + zero_above : whether entries above the pivot should be zeroed. + If ``zero_above=False``, an echelon matrix will be returned. + """ + + def get_col(i): + return mat[i::cols] + + def row_swap(i, j): + mat[i*cols:(i + 1)*cols], mat[j*cols:(j + 1)*cols] = \ + mat[j*cols:(j + 1)*cols], mat[i*cols:(i + 1)*cols] + + def cross_cancel(a, i, b, j): + """Does the row op row[i] = a*row[i] - b*row[j]""" + q = (j - i)*cols + for p in range(i*cols, (i + 1)*cols): + mat[p] = isimp(a*mat[p] - b*mat[p + q]) + + isimp = _get_intermediate_simp(_dotprodsimp) + piv_row, piv_col = 0, 0 + pivot_cols = [] + swaps = [] + + # use a fraction free method to zero above and below each pivot + while piv_col < cols and piv_row < rows: + pivot_offset, pivot_val, \ + assumed_nonzero, newly_determined = _find_reasonable_pivot( + get_col(piv_col)[piv_row:], iszerofunc, simpfunc) + + # _find_reasonable_pivot may have simplified some things + # in the process. Let's not let them go to waste + for (offset, val) in newly_determined: + offset += piv_row + mat[offset*cols + piv_col] = val + + if pivot_offset is None: + piv_col += 1 + continue + + pivot_cols.append(piv_col) + if pivot_offset != 0: + row_swap(piv_row, pivot_offset + piv_row) + swaps.append((piv_row, pivot_offset + piv_row)) + + # if we aren't normalizing last, we normalize + # before we zero the other rows + if normalize_last is False: + i, j = piv_row, piv_col + mat[i*cols + j] = one + for p in range(i*cols + j + 1, (i + 1)*cols): + mat[p] = isimp(mat[p] / pivot_val) + # after normalizing, the pivot value is 1 + pivot_val = one + + # zero above and below the pivot + for row in range(rows): + # don't zero our current row + if row == piv_row: + continue + # don't zero above the pivot unless we're told. + if zero_above is False and row < piv_row: + continue + # if we're already a zero, don't do anything + val = mat[row*cols + piv_col] + if iszerofunc(val): + continue + + cross_cancel(pivot_val, row, val, piv_row) + piv_row += 1 + + # normalize each row + if normalize_last is True and normalize is True: + for piv_i, piv_j in enumerate(pivot_cols): + pivot_val = mat[piv_i*cols + piv_j] + mat[piv_i*cols + piv_j] = one + for p in range(piv_i*cols + piv_j + 1, (piv_i + 1)*cols): + mat[p] = isimp(mat[p] / pivot_val) + + return mat, tuple(pivot_cols), tuple(swaps) + + +# This functions is a candidate for caching if it gets implemented for matrices. +def _row_reduce(M, iszerofunc, simpfunc, normalize_last=True, + normalize=True, zero_above=True): + + mat, pivot_cols, swaps = _row_reduce_list(list(M), M.rows, M.cols, M.one, + iszerofunc, simpfunc, normalize_last=normalize_last, + normalize=normalize, zero_above=zero_above) + + return M._new(M.rows, M.cols, mat), pivot_cols, swaps + + +def _is_echelon(M, iszerofunc=_iszero): + """Returns `True` if the matrix is in echelon form. That is, all rows of + zeros are at the bottom, and below each leading non-zero in a row are + exclusively zeros.""" + + if M.rows <= 0 or M.cols <= 0: + return True + + zeros_below = all(iszerofunc(t) for t in M[1:, 0]) + + if iszerofunc(M[0, 0]): + return zeros_below and _is_echelon(M[:, 1:], iszerofunc) + + return zeros_below and _is_echelon(M[1:, 1:], iszerofunc) + + +def _echelon_form(M, iszerofunc=_iszero, simplify=False, with_pivots=False): + """Returns a matrix row-equivalent to ``M`` that is in echelon form. Note + that echelon form of a matrix is *not* unique, however, properties like the + row space and the null space are preserved. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[1, 2], [3, 4]]) + >>> M.echelon_form() + Matrix([ + [1, 2], + [0, -2]]) + """ + + simpfunc = simplify if isinstance(simplify, FunctionType) else _simplify + + mat, pivots, _ = _row_reduce(M, iszerofunc, simpfunc, + normalize_last=True, normalize=False, zero_above=False) + + if with_pivots: + return mat, pivots + + return mat + + +# This functions is a candidate for caching if it gets implemented for matrices. +def _rank(M, iszerofunc=_iszero, simplify=False): + """Returns the rank of a matrix. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import x + >>> m = Matrix([[1, 2], [x, 1 - 1/x]]) + >>> m.rank() + 2 + >>> n = Matrix(3, 3, range(1, 10)) + >>> n.rank() + 2 + """ + + def _permute_complexity_right(M, iszerofunc): + """Permute columns with complicated elements as + far right as they can go. Since the ``sympy`` row reduction + algorithms start on the left, having complexity right-shifted + speeds things up. + + Returns a tuple (mat, perm) where perm is a permutation + of the columns to perform to shift the complex columns right, and mat + is the permuted matrix.""" + + def complexity(i): + # the complexity of a column will be judged by how many + # element's zero-ness cannot be determined + return sum(1 if iszerofunc(e) is None else 0 for e in M[:, i]) + + complex = [(complexity(i), i) for i in range(M.cols)] + perm = [j for (i, j) in sorted(complex)] + + return (M.permute(perm, orientation='cols'), perm) + + simpfunc = simplify if isinstance(simplify, FunctionType) else _simplify + + # for small matrices, we compute the rank explicitly + # if is_zero on elements doesn't answer the question + # for small matrices, we fall back to the full routine. + if M.rows <= 0 or M.cols <= 0: + return 0 + + if M.rows <= 1 or M.cols <= 1: + zeros = [iszerofunc(x) for x in M] + + if False in zeros: + return 1 + + if M.rows == 2 and M.cols == 2: + zeros = [iszerofunc(x) for x in M] + + if False not in zeros and None not in zeros: + return 0 + + d = M.det() + + if iszerofunc(d) and False in zeros: + return 1 + if iszerofunc(d) is False: + return 2 + + mat, _ = _permute_complexity_right(M, iszerofunc=iszerofunc) + _, pivots, _ = _row_reduce(mat, iszerofunc, simpfunc, normalize_last=True, + normalize=False, zero_above=False) + + return len(pivots) + + +def _to_DM_ZZ_QQ(M): + # We have to test for _rep here because there are tests that otherwise fail + # with e.g. "AttributeError: 'SubspaceOnlyMatrix' object has no attribute + # '_rep'." There is almost certainly no value in such tests. The + # presumption seems to be that someone could create a new class by + # inheriting some of the Matrix classes and not the full set that is used + # by the standard Matrix class but if anyone tried that it would fail in + # many ways. + if not hasattr(M, '_rep'): + return None + + rep = M._rep + K = rep.domain + + if K.is_ZZ: + return rep + elif K.is_QQ: + try: + return rep.convert_to(ZZ) + except CoercionFailed: + return rep + else: + if not all(e.is_Rational for e in M): + return None + try: + return rep.convert_to(ZZ) + except CoercionFailed: + return rep.convert_to(QQ) + + +def _rref_dm(dM): + """Compute the reduced row echelon form of a DomainMatrix.""" + K = dM.domain + + if K.is_ZZ: + dM_rref, den, pivots = dM.rref_den(keep_domain=False) + dM_rref = dM_rref.to_field() / den + elif K.is_QQ: + dM_rref, pivots = dM.rref() + else: + assert False # pragma: no cover + + M_rref = dM_rref.to_Matrix() + + return M_rref, pivots + + +def _rref(M, iszerofunc=_iszero, simplify=False, pivots=True, + normalize_last=True): + """Return reduced row-echelon form of matrix and indices + of pivot vars. + + Parameters + ========== + + iszerofunc : Function + A function used for detecting whether an element can + act as a pivot. ``lambda x: x.is_zero`` is used by default. + + simplify : Function + A function used to simplify elements when looking for a pivot. + By default SymPy's ``simplify`` is used. + + pivots : True or False + If ``True``, a tuple containing the row-reduced matrix and a tuple + of pivot columns is returned. If ``False`` just the row-reduced + matrix is returned. + + normalize_last : True or False + If ``True``, no pivots are normalized to `1` until after all + entries above and below each pivot are zeroed. This means the row + reduction algorithm is fraction free until the very last step. + If ``False``, the naive row reduction procedure is used where + each pivot is normalized to be `1` before row operations are + used to zero above and below the pivot. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import x + >>> m = Matrix([[1, 2], [x, 1 - 1/x]]) + >>> m.rref() + (Matrix([ + [1, 0], + [0, 1]]), (0, 1)) + >>> rref_matrix, rref_pivots = m.rref() + >>> rref_matrix + Matrix([ + [1, 0], + [0, 1]]) + >>> rref_pivots + (0, 1) + + ``iszerofunc`` can correct rounding errors in matrices with float + values. In the following example, calling ``rref()`` leads to + floating point errors, incorrectly row reducing the matrix. + ``iszerofunc= lambda x: abs(x) < 1e-9`` sets sufficiently small numbers + to zero, avoiding this error. + + >>> m = Matrix([[0.9, -0.1, -0.2, 0], [-0.8, 0.9, -0.4, 0], [-0.1, -0.8, 0.6, 0]]) + >>> m.rref() + (Matrix([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0]]), (0, 1, 2)) + >>> m.rref(iszerofunc=lambda x:abs(x)<1e-9) + (Matrix([ + [1, 0, -0.301369863013699, 0], + [0, 1, -0.712328767123288, 0], + [0, 0, 0, 0]]), (0, 1)) + + Notes + ===== + + The default value of ``normalize_last=True`` can provide significant + speedup to row reduction, especially on matrices with symbols. However, + if you depend on the form row reduction algorithm leaves entries + of the matrix, set ``normalize_last=False`` + """ + # Try to use DomainMatrix for ZZ or QQ + dM = _to_DM_ZZ_QQ(M) + + if dM is not None: + # Use DomainMatrix for ZZ or QQ + mat, pivot_cols = _rref_dm(dM) + else: + # Use the generic Matrix routine. + if isinstance(simplify, FunctionType): + simpfunc = simplify + else: + simpfunc = _simplify + + mat, pivot_cols, _ = _row_reduce(M, iszerofunc, simpfunc, + normalize_last, normalize=True, zero_above=True) + + if pivots: + return mat, pivot_cols + else: + return mat diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/repmatrix.py b/venv/lib/python3.11/site-packages/sympy/matrices/repmatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..57f32fae34786f68f579fad7de38c9e3cf43e131 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/repmatrix.py @@ -0,0 +1,1034 @@ +from collections import defaultdict + +from operator import index as index_ + +from sympy.core.expr import Expr +from sympy.core.kind import Kind, NumberKind, UndefinedKind +from sympy.core.numbers import Integer, Rational +from sympy.core.sympify import _sympify, SympifyError +from sympy.core.singleton import S +from sympy.polys.domains import ZZ, QQ, GF, EXRAW +from sympy.polys.matrices import DomainMatrix +from sympy.polys.matrices.exceptions import DMNonInvertibleMatrixError +from sympy.polys.polyerrors import CoercionFailed, NotInvertible +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.utilities.iterables import is_sequence +from sympy.utilities.misc import filldedent, as_int + +from .exceptions import ShapeError, NonSquareMatrixError, NonInvertibleMatrixError +from .matrixbase import classof, MatrixBase +from .kind import MatrixKind + + +class RepMatrix(MatrixBase): + """Matrix implementation based on DomainMatrix as an internal representation. + + The RepMatrix class is a superclass for Matrix, ImmutableMatrix, + SparseMatrix and ImmutableSparseMatrix which are the main usable matrix + classes in SymPy. Most methods on this class are simply forwarded to + DomainMatrix. + """ + + # + # MatrixBase is the common superclass for all of the usable explicit matrix + # classes in SymPy. The idea is that MatrixBase is an abstract class though + # and that subclasses will implement the lower-level methods. + # + # RepMatrix is a subclass of MatrixBase that uses DomainMatrix as an + # internal representation and delegates lower-level methods to + # DomainMatrix. All of SymPy's standard explicit matrix classes subclass + # RepMatrix and so use DomainMatrix internally. + # + # A RepMatrix uses an internal DomainMatrix with the domain set to ZZ, QQ + # or EXRAW. The EXRAW domain is equivalent to the previous implementation + # of Matrix that used Expr for the elements. The ZZ and QQ domains are used + # when applicable just because they are compatible with the previous + # implementation but are much more efficient. Other domains such as QQ[x] + # are not used because they differ from Expr in some way (e.g. automatic + # expansion of powers and products). + # + + _rep: DomainMatrix + + def __eq__(self, other): + # Skip sympify for mutable matrices... + if not isinstance(other, RepMatrix): + try: + other = _sympify(other) + except SympifyError: + return NotImplemented + if not isinstance(other, RepMatrix): + return NotImplemented + + return self._rep.unify_eq(other._rep) + + def to_DM(self, domain=None, **kwargs): + """Convert to a :class:`~.DomainMatrix`. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[1, 2], [3, 4]]) + >>> M.to_DM() + DomainMatrix({0: {0: 1, 1: 2}, 1: {0: 3, 1: 4}}, (2, 2), ZZ) + + The :meth:`DomainMatrix.to_Matrix` method can be used to convert back: + + >>> M.to_DM().to_Matrix() == M + True + + The domain can be given explicitly or otherwise it will be chosen by + :func:`construct_domain`. Any keyword arguments (besides ``domain``) + are passed to :func:`construct_domain`: + + >>> from sympy import QQ, symbols + >>> x = symbols('x') + >>> M = Matrix([[x, 1], [1, x]]) + >>> M + Matrix([ + [x, 1], + [1, x]]) + >>> M.to_DM().domain + ZZ[x] + >>> M.to_DM(field=True).domain + ZZ(x) + >>> M.to_DM(domain=QQ[x]).domain + QQ[x] + + See Also + ======== + + DomainMatrix + DomainMatrix.to_Matrix + DomainMatrix.convert_to + DomainMatrix.choose_domain + construct_domain + """ + if domain is not None: + if kwargs: + raise TypeError("Options cannot be used with domain parameter") + return self._rep.convert_to(domain) + + rep = self._rep + dom = rep.domain + + # If the internal DomainMatrix is already ZZ or QQ then we can maybe + # bypass calling construct_domain or performing any conversions. Some + # kwargs might affect this though e.g. field=True (not sure if there + # are others). + if not kwargs: + if dom.is_ZZ: + return rep.copy() + elif dom.is_QQ: + # All elements might be integers + try: + return rep.convert_to(ZZ) + except CoercionFailed: + pass + return rep.copy() + + # Let construct_domain choose a domain + rep_dom = rep.choose_domain(**kwargs) + + # XXX: There should be an option to construct_domain to choose EXRAW + # instead of EX. At least converting to EX does not initially trigger + # EX.simplify which is what we want here but should probably be + # considered a bug in EX. Perhaps also this could be handled in + # DomainMatrix.choose_domain rather than here... + if rep_dom.domain.is_EX: + rep_dom = rep_dom.convert_to(EXRAW) + + return rep_dom + + @classmethod + def _unify_element_sympy(cls, rep, element): + domain = rep.domain + element = _sympify(element) + + if domain != EXRAW: + # The domain can only be ZZ, QQ or EXRAW + if element.is_Integer: + new_domain = domain + elif element.is_Rational: + new_domain = QQ + else: + new_domain = EXRAW + + # XXX: This converts the domain for all elements in the matrix + # which can be slow. This happens e.g. if __setitem__ changes one + # element to something that does not fit in the domain + if new_domain != domain: + rep = rep.convert_to(new_domain) + domain = new_domain + + if domain != EXRAW: + element = new_domain.from_sympy(element) + + if domain == EXRAW and not isinstance(element, Expr): + sympy_deprecation_warning( + """ + non-Expr objects in a Matrix is deprecated. Matrix represents + a mathematical matrix. To represent a container of non-numeric + entities, Use a list of lists, TableForm, NumPy array, or some + other data structure instead. + """, + deprecated_since_version="1.9", + active_deprecations_target="deprecated-non-expr-in-matrix", + stacklevel=4, + ) + + return rep, element + + @classmethod + def _dod_to_DomainMatrix(cls, rows, cols, dod, types): + + if not all(issubclass(typ, Expr) for typ in types): + sympy_deprecation_warning( + """ + non-Expr objects in a Matrix is deprecated. Matrix represents + a mathematical matrix. To represent a container of non-numeric + entities, Use a list of lists, TableForm, NumPy array, or some + other data structure instead. + """, + deprecated_since_version="1.9", + active_deprecations_target="deprecated-non-expr-in-matrix", + stacklevel=6, + ) + + rep = DomainMatrix(dod, (rows, cols), EXRAW) + + if all(issubclass(typ, Rational) for typ in types): + if all(issubclass(typ, Integer) for typ in types): + rep = rep.convert_to(ZZ) + else: + rep = rep.convert_to(QQ) + + return rep + + @classmethod + def _flat_list_to_DomainMatrix(cls, rows, cols, flat_list): + + elements_dod = defaultdict(dict) + for n, element in enumerate(flat_list): + if element != 0: + i, j = divmod(n, cols) + elements_dod[i][j] = element + + types = set(map(type, flat_list)) + + rep = cls._dod_to_DomainMatrix(rows, cols, elements_dod, types) + return rep + + @classmethod + def _smat_to_DomainMatrix(cls, rows, cols, smat): + + elements_dod = defaultdict(dict) + for (i, j), element in smat.items(): + if element != 0: + elements_dod[i][j] = element + + types = set(map(type, smat.values())) + + rep = cls._dod_to_DomainMatrix(rows, cols, elements_dod, types) + return rep + + def flat(self): + return self._rep.to_sympy().to_list_flat() + + def _eval_tolist(self): + return self._rep.to_sympy().to_list() + + def _eval_todok(self): + return self._rep.to_sympy().to_dok() + + @classmethod + def _eval_from_dok(cls, rows, cols, dok): + return cls._fromrep(cls._smat_to_DomainMatrix(rows, cols, dok)) + + def _eval_values(self): + return list(self._eval_iter_values()) + + def _eval_iter_values(self): + rep = self._rep + K = rep.domain + values = rep.iter_values() + if not K.is_EXRAW: + values = map(K.to_sympy, values) + return values + + def _eval_iter_items(self): + rep = self._rep + K = rep.domain + to_sympy = K.to_sympy + items = rep.iter_items() + if not K.is_EXRAW: + items = ((i, to_sympy(v)) for i, v in items) + return items + + def copy(self): + return self._fromrep(self._rep.copy()) + + @property + def kind(self) -> MatrixKind: + domain = self._rep.domain + element_kind: Kind + if domain in (ZZ, QQ): + element_kind = NumberKind + elif domain == EXRAW: + kinds = {e.kind for e in self.values()} + if len(kinds) == 1: + [element_kind] = kinds + else: + element_kind = UndefinedKind + else: # pragma: no cover + raise RuntimeError("Domain should only be ZZ, QQ or EXRAW") + return MatrixKind(element_kind) + + def _eval_has(self, *patterns): + # if the matrix has any zeros, see if S.Zero + # has the pattern. If _smat is full length, + # the matrix has no zeros. + zhas = False + dok = self.todok() + if len(dok) != self.rows*self.cols: + zhas = S.Zero.has(*patterns) + return zhas or any(value.has(*patterns) for value in dok.values()) + + def _eval_is_Identity(self): + if not all(self[i, i] == 1 for i in range(self.rows)): + return False + return len(self.todok()) == self.rows + + def _eval_is_symmetric(self, simpfunc): + diff = (self - self.T).applyfunc(simpfunc) + return len(diff.values()) == 0 + + def _eval_transpose(self): + """Returns the transposed SparseMatrix of this SparseMatrix. + + Examples + ======== + + >>> from sympy import SparseMatrix + >>> a = SparseMatrix(((1, 2), (3, 4))) + >>> a + Matrix([ + [1, 2], + [3, 4]]) + >>> a.T + Matrix([ + [1, 3], + [2, 4]]) + """ + return self._fromrep(self._rep.transpose()) + + def _eval_col_join(self, other): + return self._fromrep(self._rep.vstack(other._rep)) + + def _eval_row_join(self, other): + return self._fromrep(self._rep.hstack(other._rep)) + + def _eval_extract(self, rowsList, colsList): + return self._fromrep(self._rep.extract(rowsList, colsList)) + + def __getitem__(self, key): + return _getitem_RepMatrix(self, key) + + @classmethod + def _eval_zeros(cls, rows, cols): + rep = DomainMatrix.zeros((rows, cols), ZZ) + return cls._fromrep(rep) + + @classmethod + def _eval_eye(cls, rows, cols): + rep = DomainMatrix.eye((rows, cols), ZZ) + return cls._fromrep(rep) + + def _eval_add(self, other): + return classof(self, other)._fromrep(self._rep + other._rep) + + def _eval_matrix_mul(self, other): + return classof(self, other)._fromrep(self._rep * other._rep) + + def _eval_matrix_mul_elementwise(self, other): + selfrep, otherrep = self._rep.unify(other._rep) + newrep = selfrep.mul_elementwise(otherrep) + return classof(self, other)._fromrep(newrep) + + def _eval_scalar_mul(self, other): + rep, other = self._unify_element_sympy(self._rep, other) + return self._fromrep(rep.scalarmul(other)) + + def _eval_scalar_rmul(self, other): + rep, other = self._unify_element_sympy(self._rep, other) + return self._fromrep(rep.rscalarmul(other)) + + def _eval_Abs(self): + return self._fromrep(self._rep.applyfunc(abs)) + + def _eval_conjugate(self): + rep = self._rep + domain = rep.domain + if domain in (ZZ, QQ): + return self.copy() + else: + return self._fromrep(rep.applyfunc(lambda e: e.conjugate())) + + def equals(self, other, failing_expression=False): + """Applies ``equals`` to corresponding elements of the matrices, + trying to prove that the elements are equivalent, returning True + if they are, False if any pair is not, and None (or the first + failing expression if failing_expression is True) if it cannot + be decided if the expressions are equivalent or not. This is, in + general, an expensive operation. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import x + >>> A = Matrix([x*(x - 1), 0]) + >>> B = Matrix([x**2 - x, 0]) + >>> A == B + False + >>> A.simplify() == B.simplify() + True + >>> A.equals(B) + True + >>> A.equals(2) + False + + See Also + ======== + sympy.core.expr.Expr.equals + """ + if self.shape != getattr(other, 'shape', None): + return False + + rv = True + for i in range(self.rows): + for j in range(self.cols): + ans = self[i, j].equals(other[i, j], failing_expression) + if ans is False: + return False + elif ans is not True and rv is True: + rv = ans + return rv + + def inv_mod(M, m): + r""" + Returns the inverse of the integer matrix ``M`` modulo ``m``. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix(2, 2, [1, 2, 3, 4]) + >>> A.inv_mod(5) + Matrix([ + [3, 1], + [4, 2]]) + >>> A.inv_mod(3) + Matrix([ + [1, 1], + [0, 1]]) + + """ + + if not M.is_square: + raise NonSquareMatrixError() + + try: + m = as_int(m) + except ValueError: + raise TypeError("inv_mod: modulus m must be an integer") + + K = GF(m, symmetric=False) + + try: + dM = M.to_DM(K) + except CoercionFailed: + raise ValueError("inv_mod: matrix entries must be integers") + + if K.is_Field: + try: + dMi = dM.inv() + except DMNonInvertibleMatrixError as exc: + msg = f'Matrix is not invertible (mod {m})' + raise NonInvertibleMatrixError(msg) from exc + else: + dMadj, det = dM.adj_det() + try: + detinv = 1 / det + except NotInvertible: + msg = f'Matrix is not invertible (mod {m})' + raise NonInvertibleMatrixError(msg) + dMi = dMadj * detinv + + return dMi.to_Matrix() + + def lll(self, delta=0.75): + """LLL-reduced basis for the rowspace of a matrix of integers. + + Performs the Lenstra–Lenstra–Lovász (LLL) basis reduction algorithm. + + The implementation is provided by :class:`~DomainMatrix`. See + :meth:`~DomainMatrix.lll` for more details. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[1, 0, 0, 0, -20160], + ... [0, 1, 0, 0, 33768], + ... [0, 0, 1, 0, 39578], + ... [0, 0, 0, 1, 47757]]) + >>> M.lll() + Matrix([ + [ 10, -3, -2, 8, -4], + [ 3, -9, 8, 1, -11], + [ -3, 13, -9, -3, -9], + [-12, -7, -11, 9, -1]]) + + See Also + ======== + + lll_transform + sympy.polys.matrices.domainmatrix.DomainMatrix.lll + """ + delta = QQ.from_sympy(_sympify(delta)) + dM = self._rep.convert_to(ZZ) + basis = dM.lll(delta=delta) + return self._fromrep(basis) + + def lll_transform(self, delta=0.75): + """LLL-reduced basis and transformation matrix. + + Performs the Lenstra–Lenstra–Lovász (LLL) basis reduction algorithm. + + The implementation is provided by :class:`~DomainMatrix`. See + :meth:`~DomainMatrix.lll_transform` for more details. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[1, 0, 0, 0, -20160], + ... [0, 1, 0, 0, 33768], + ... [0, 0, 1, 0, 39578], + ... [0, 0, 0, 1, 47757]]) + >>> B, T = M.lll_transform() + >>> B + Matrix([ + [ 10, -3, -2, 8, -4], + [ 3, -9, 8, 1, -11], + [ -3, 13, -9, -3, -9], + [-12, -7, -11, 9, -1]]) + >>> T + Matrix([ + [ 10, -3, -2, 8], + [ 3, -9, 8, 1], + [ -3, 13, -9, -3], + [-12, -7, -11, 9]]) + + The transformation matrix maps the original basis to the LLL-reduced + basis: + + >>> T * M == B + True + + See Also + ======== + + lll + sympy.polys.matrices.domainmatrix.DomainMatrix.lll_transform + """ + delta = QQ.from_sympy(_sympify(delta)) + dM = self._rep.convert_to(ZZ) + basis, transform = dM.lll_transform(delta=delta) + B = self._fromrep(basis) + T = self._fromrep(transform) + return B, T + + +class MutableRepMatrix(RepMatrix): + """Mutable matrix based on DomainMatrix as the internal representation""" + + # + # MutableRepMatrix is a subclass of RepMatrix that adds/overrides methods + # to make the instances mutable. MutableRepMatrix is a superclass for both + # MutableDenseMatrix and MutableSparseMatrix. + # + + is_zero = False + + def __new__(cls, *args, **kwargs): + return cls._new(*args, **kwargs) + + @classmethod + def _new(cls, *args, copy=True, **kwargs): + if copy is False: + # The input was rows, cols, [list]. + # It should be used directly without creating a copy. + if len(args) != 3: + raise TypeError("'copy=False' requires a matrix be initialized as rows,cols,[list]") + rows, cols, flat_list = args + else: + rows, cols, flat_list = cls._handle_creation_inputs(*args, **kwargs) + flat_list = list(flat_list) # create a shallow copy + + rep = cls._flat_list_to_DomainMatrix(rows, cols, flat_list) + + return cls._fromrep(rep) + + @classmethod + def _fromrep(cls, rep): + obj = super().__new__(cls) + obj.rows, obj.cols = rep.shape + obj._rep = rep + return obj + + def copy(self): + return self._fromrep(self._rep.copy()) + + def as_mutable(self): + return self.copy() + + def __setitem__(self, key, value): + """ + + Examples + ======== + + >>> from sympy import Matrix, I, zeros, ones + >>> m = Matrix(((1, 2+I), (3, 4))) + >>> m + Matrix([ + [1, 2 + I], + [3, 4]]) + >>> m[1, 0] = 9 + >>> m + Matrix([ + [1, 2 + I], + [9, 4]]) + >>> m[1, 0] = [[0, 1]] + + To replace row r you assign to position r*m where m + is the number of columns: + + >>> M = zeros(4) + >>> m = M.cols + >>> M[3*m] = ones(1, m)*2; M + Matrix([ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [2, 2, 2, 2]]) + + And to replace column c you can assign to position c: + + >>> M[2] = ones(m, 1)*4; M + Matrix([ + [0, 0, 4, 0], + [0, 0, 4, 0], + [0, 0, 4, 0], + [2, 2, 4, 2]]) + """ + rv = self._setitem(key, value) + if rv is not None: + i, j, value = rv + self._rep, value = self._unify_element_sympy(self._rep, value) + self._rep.rep.setitem(i, j, value) + + def _eval_col_del(self, col): + self._rep = DomainMatrix.hstack(self._rep[:,:col], self._rep[:,col+1:]) + self.cols -= 1 + + def _eval_row_del(self, row): + self._rep = DomainMatrix.vstack(self._rep[:row,:], self._rep[row+1:, :]) + self.rows -= 1 + + def _eval_col_insert(self, col, other): + other = self._new(other) + return self.hstack(self[:,:col], other, self[:,col:]) + + def _eval_row_insert(self, row, other): + other = self._new(other) + return self.vstack(self[:row,:], other, self[row:,:]) + + def col_op(self, j, f): + """In-place operation on col j using two-arg functor whose args are + interpreted as (self[i, j], i). + + Examples + ======== + + >>> from sympy import eye + >>> M = eye(3) + >>> M.col_op(1, lambda v, i: v + 2*M[i, 0]); M + Matrix([ + [1, 2, 0], + [0, 1, 0], + [0, 0, 1]]) + + See Also + ======== + col + row_op + """ + for i in range(self.rows): + self[i, j] = f(self[i, j], i) + + def col_swap(self, i, j): + """Swap the two given columns of the matrix in-place. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[1, 0], [1, 0]]) + >>> M + Matrix([ + [1, 0], + [1, 0]]) + >>> M.col_swap(0, 1) + >>> M + Matrix([ + [0, 1], + [0, 1]]) + + See Also + ======== + + col + row_swap + """ + for k in range(0, self.rows): + self[k, i], self[k, j] = self[k, j], self[k, i] + + def row_op(self, i, f): + """In-place operation on row ``i`` using two-arg functor whose args are + interpreted as ``(self[i, j], j)``. + + Examples + ======== + + >>> from sympy import eye + >>> M = eye(3) + >>> M.row_op(1, lambda v, j: v + 2*M[0, j]); M + Matrix([ + [1, 0, 0], + [2, 1, 0], + [0, 0, 1]]) + + See Also + ======== + row + zip_row_op + col_op + + """ + for j in range(self.cols): + self[i, j] = f(self[i, j], j) + + #The next three methods give direct support for the most common row operations inplace. + def row_mult(self,i,factor): + """Multiply the given row by the given factor in-place. + + Examples + ======== + + >>> from sympy import eye + >>> M = eye(3) + >>> M.row_mult(1,7); M + Matrix([ + [1, 0, 0], + [0, 7, 0], + [0, 0, 1]]) + + """ + for j in range(self.cols): + self[i,j] *= factor + + def row_add(self,s,t,k): + """Add k times row s (source) to row t (target) in place. + + Examples + ======== + + >>> from sympy import eye + >>> M = eye(3) + >>> M.row_add(0, 2,3); M + Matrix([ + [1, 0, 0], + [0, 1, 0], + [3, 0, 1]]) + """ + + for j in range(self.cols): + self[t,j] += k*self[s,j] + + def row_swap(self, i, j): + """Swap the two given rows of the matrix in-place. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix([[0, 1], [1, 0]]) + >>> M + Matrix([ + [0, 1], + [1, 0]]) + >>> M.row_swap(0, 1) + >>> M + Matrix([ + [1, 0], + [0, 1]]) + + See Also + ======== + + row + col_swap + """ + for k in range(0, self.cols): + self[i, k], self[j, k] = self[j, k], self[i, k] + + def zip_row_op(self, i, k, f): + """In-place operation on row ``i`` using two-arg functor whose args are + interpreted as ``(self[i, j], self[k, j])``. + + Examples + ======== + + >>> from sympy import eye + >>> M = eye(3) + >>> M.zip_row_op(1, 0, lambda v, u: v + 2*u); M + Matrix([ + [1, 0, 0], + [2, 1, 0], + [0, 0, 1]]) + + See Also + ======== + row + row_op + col_op + + """ + for j in range(self.cols): + self[i, j] = f(self[i, j], self[k, j]) + + def copyin_list(self, key, value): + """Copy in elements from a list. + + Parameters + ========== + + key : slice + The section of this matrix to replace. + value : iterable + The iterable to copy values from. + + Examples + ======== + + >>> from sympy import eye + >>> I = eye(3) + >>> I[:2, 0] = [1, 2] # col + >>> I + Matrix([ + [1, 0, 0], + [2, 1, 0], + [0, 0, 1]]) + >>> I[1, :2] = [[3, 4]] + >>> I + Matrix([ + [1, 0, 0], + [3, 4, 0], + [0, 0, 1]]) + + See Also + ======== + + copyin_matrix + """ + if not is_sequence(value): + raise TypeError("`value` must be an ordered iterable, not %s." % type(value)) + return self.copyin_matrix(key, type(self)(value)) + + def copyin_matrix(self, key, value): + """Copy in values from a matrix into the given bounds. + + Parameters + ========== + + key : slice + The section of this matrix to replace. + value : Matrix + The matrix to copy values from. + + Examples + ======== + + >>> from sympy import Matrix, eye + >>> M = Matrix([[0, 1], [2, 3], [4, 5]]) + >>> I = eye(3) + >>> I[:3, :2] = M + >>> I + Matrix([ + [0, 1, 0], + [2, 3, 0], + [4, 5, 1]]) + >>> I[0, 1] = M + >>> I + Matrix([ + [0, 0, 1], + [2, 2, 3], + [4, 4, 5]]) + + See Also + ======== + + copyin_list + """ + rlo, rhi, clo, chi = self.key2bounds(key) + shape = value.shape + dr, dc = rhi - rlo, chi - clo + if shape != (dr, dc): + raise ShapeError(filldedent("The Matrix `value` doesn't have the " + "same dimensions " + "as the in sub-Matrix given by `key`.")) + + for i in range(value.rows): + for j in range(value.cols): + self[i + rlo, j + clo] = value[i, j] + + def fill(self, value): + """Fill self with the given value. + + Notes + ===== + + Unless many values are going to be deleted (i.e. set to zero) + this will create a matrix that is slower than a dense matrix in + operations. + + Examples + ======== + + >>> from sympy import SparseMatrix + >>> M = SparseMatrix.zeros(3); M + Matrix([ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0]]) + >>> M.fill(1); M + Matrix([ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1]]) + + See Also + ======== + + zeros + ones + """ + value = _sympify(value) + if not value: + self._rep = DomainMatrix.zeros(self.shape, EXRAW) + else: + elements_dod = {i: dict.fromkeys(range(self.cols), value) for i in range(self.rows)} + self._rep = DomainMatrix(elements_dod, self.shape, EXRAW) + + +def _getitem_RepMatrix(self, key): + """Return portion of self defined by key. If the key involves a slice + then a list will be returned (if key is a single slice) or a matrix + (if key was a tuple involving a slice). + + Examples + ======== + + >>> from sympy import Matrix, I + >>> m = Matrix([ + ... [1, 2 + I], + ... [3, 4 ]]) + + If the key is a tuple that does not involve a slice then that element + is returned: + + >>> m[1, 0] + 3 + + When a tuple key involves a slice, a matrix is returned. Here, the + first column is selected (all rows, column 0): + + >>> m[:, 0] + Matrix([ + [1], + [3]]) + + If the slice is not a tuple then it selects from the underlying + list of elements that are arranged in row order and a list is + returned if a slice is involved: + + >>> m[0] + 1 + >>> m[::2] + [1, 3] + """ + if isinstance(key, tuple): + i, j = key + try: + return self._rep.getitem_sympy(index_(i), index_(j)) + except (TypeError, IndexError): + if (isinstance(i, Expr) and not i.is_number) or (isinstance(j, Expr) and not j.is_number): + if ((j < 0) is True) or ((j >= self.shape[1]) is True) or\ + ((i < 0) is True) or ((i >= self.shape[0]) is True): + raise ValueError("index out of boundary") + from sympy.matrices.expressions.matexpr import MatrixElement + return MatrixElement(self, i, j) + + if isinstance(i, slice): + i = range(self.rows)[i] + elif is_sequence(i): + pass + else: + i = [i] + if isinstance(j, slice): + j = range(self.cols)[j] + elif is_sequence(j): + pass + else: + j = [j] + return self.extract(i, j) + + else: + # Index/slice like a flattened list + rows, cols = self.shape + + # Raise the appropriate exception: + if not rows * cols: + return [][key] + + rep = self._rep.rep + domain = rep.domain + is_slice = isinstance(key, slice) + + if is_slice: + values = [rep.getitem(*divmod(n, cols)) for n in range(rows * cols)[key]] + else: + values = [rep.getitem(*divmod(index_(key), cols))] + + if domain != EXRAW: + to_sympy = domain.to_sympy + values = [to_sympy(val) for val in values] + + if is_slice: + return values + else: + return values[0] diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/solvers.py b/venv/lib/python3.11/site-packages/sympy/matrices/solvers.py new file mode 100644 index 0000000000000000000000000000000000000000..1fba990df80dcf46304ecb1412f5382f60948c51 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/solvers.py @@ -0,0 +1,942 @@ +from sympy.core.function import expand_mul +from sympy.core.symbol import Dummy, uniquely_named_symbol, symbols +from sympy.utilities.iterables import numbered_symbols + +from .exceptions import ShapeError, NonSquareMatrixError, NonInvertibleMatrixError +from .eigen import _fuzzy_positive_definite +from .utilities import _get_intermediate_simp, _iszero + + +def _diagonal_solve(M, rhs): + """Solves ``Ax = B`` efficiently, where A is a diagonal Matrix, + with non-zero diagonal entries. + + Examples + ======== + + >>> from sympy import Matrix, eye + >>> A = eye(2)*2 + >>> B = Matrix([[1, 2], [3, 4]]) + >>> A.diagonal_solve(B) == B/2 + True + + See Also + ======== + + sympy.matrices.dense.DenseMatrix.lower_triangular_solve + sympy.matrices.dense.DenseMatrix.upper_triangular_solve + gauss_jordan_solve + cholesky_solve + LDLsolve + LUsolve + QRsolve + pinv_solve + cramer_solve + """ + + if not M.is_diagonal(): + raise TypeError("Matrix should be diagonal") + if rhs.rows != M.rows: + raise TypeError("Size mismatch") + + return M._new( + rhs.rows, rhs.cols, lambda i, j: rhs[i, j] / M[i, i]) + + +def _lower_triangular_solve(M, rhs): + """Solves ``Ax = B``, where A is a lower triangular matrix. + + See Also + ======== + + upper_triangular_solve + gauss_jordan_solve + cholesky_solve + diagonal_solve + LDLsolve + LUsolve + QRsolve + pinv_solve + cramer_solve + """ + + from .dense import MutableDenseMatrix + + if not M.is_square: + raise NonSquareMatrixError("Matrix must be square.") + if rhs.rows != M.rows: + raise ShapeError("Matrices size mismatch.") + if not M.is_lower: + raise ValueError("Matrix must be lower triangular.") + + dps = _get_intermediate_simp() + X = MutableDenseMatrix.zeros(M.rows, rhs.cols) + + for j in range(rhs.cols): + for i in range(M.rows): + if M[i, i] == 0: + raise TypeError("Matrix must be non-singular.") + + X[i, j] = dps((rhs[i, j] - sum(M[i, k]*X[k, j] + for k in range(i))) / M[i, i]) + + return M._new(X) + +def _lower_triangular_solve_sparse(M, rhs): + """Solves ``Ax = B``, where A is a lower triangular matrix. + + See Also + ======== + + upper_triangular_solve + gauss_jordan_solve + cholesky_solve + diagonal_solve + LDLsolve + LUsolve + QRsolve + pinv_solve + cramer_solve + """ + + if not M.is_square: + raise NonSquareMatrixError("Matrix must be square.") + if rhs.rows != M.rows: + raise ShapeError("Matrices size mismatch.") + if not M.is_lower: + raise ValueError("Matrix must be lower triangular.") + + dps = _get_intermediate_simp() + rows = [[] for i in range(M.rows)] + + for i, j, v in M.row_list(): + if i > j: + rows[i].append((j, v)) + + X = rhs.as_mutable() + + for j in range(rhs.cols): + for i in range(rhs.rows): + for u, v in rows[i]: + X[i, j] -= v*X[u, j] + + X[i, j] = dps(X[i, j] / M[i, i]) + + return M._new(X) + + +def _upper_triangular_solve(M, rhs): + """Solves ``Ax = B``, where A is an upper triangular matrix. + + See Also + ======== + + lower_triangular_solve + gauss_jordan_solve + cholesky_solve + diagonal_solve + LDLsolve + LUsolve + QRsolve + pinv_solve + cramer_solve + """ + + from .dense import MutableDenseMatrix + + if not M.is_square: + raise NonSquareMatrixError("Matrix must be square.") + if rhs.rows != M.rows: + raise ShapeError("Matrix size mismatch.") + if not M.is_upper: + raise TypeError("Matrix is not upper triangular.") + + dps = _get_intermediate_simp() + X = MutableDenseMatrix.zeros(M.rows, rhs.cols) + + for j in range(rhs.cols): + for i in reversed(range(M.rows)): + if M[i, i] == 0: + raise ValueError("Matrix must be non-singular.") + + X[i, j] = dps((rhs[i, j] - sum(M[i, k]*X[k, j] + for k in range(i + 1, M.rows))) / M[i, i]) + + return M._new(X) + +def _upper_triangular_solve_sparse(M, rhs): + """Solves ``Ax = B``, where A is an upper triangular matrix. + + See Also + ======== + + lower_triangular_solve + gauss_jordan_solve + cholesky_solve + diagonal_solve + LDLsolve + LUsolve + QRsolve + pinv_solve + cramer_solve + """ + + if not M.is_square: + raise NonSquareMatrixError("Matrix must be square.") + if rhs.rows != M.rows: + raise ShapeError("Matrix size mismatch.") + if not M.is_upper: + raise TypeError("Matrix is not upper triangular.") + + dps = _get_intermediate_simp() + rows = [[] for i in range(M.rows)] + + for i, j, v in M.row_list(): + if i < j: + rows[i].append((j, v)) + + X = rhs.as_mutable() + + for j in range(rhs.cols): + for i in reversed(range(rhs.rows)): + for u, v in reversed(rows[i]): + X[i, j] -= v*X[u, j] + + X[i, j] = dps(X[i, j] / M[i, i]) + + return M._new(X) + + +def _cholesky_solve(M, rhs): + """Solves ``Ax = B`` using Cholesky decomposition, + for a general square non-singular matrix. + For a non-square matrix with rows > cols, + the least squares solution is returned. + + See Also + ======== + + sympy.matrices.dense.DenseMatrix.lower_triangular_solve + sympy.matrices.dense.DenseMatrix.upper_triangular_solve + gauss_jordan_solve + diagonal_solve + LDLsolve + LUsolve + QRsolve + pinv_solve + cramer_solve + """ + + if M.rows < M.cols: + raise NotImplementedError( + 'Under-determined System. Try M.gauss_jordan_solve(rhs)') + + hermitian = True + reform = False + + if M.is_symmetric(): + hermitian = False + elif not M.is_hermitian: + reform = True + + if reform or _fuzzy_positive_definite(M) is False: + H = M.H + M = H.multiply(M) + rhs = H.multiply(rhs) + hermitian = not M.is_symmetric() + + L = M.cholesky(hermitian=hermitian) + Y = L.lower_triangular_solve(rhs) + + if hermitian: + return (L.H).upper_triangular_solve(Y) + else: + return (L.T).upper_triangular_solve(Y) + + +def _LDLsolve(M, rhs): + """Solves ``Ax = B`` using LDL decomposition, + for a general square and non-singular matrix. + + For a non-square matrix with rows > cols, + the least squares solution is returned. + + Examples + ======== + + >>> from sympy import Matrix, eye + >>> A = eye(2)*2 + >>> B = Matrix([[1, 2], [3, 4]]) + >>> A.LDLsolve(B) == B/2 + True + + See Also + ======== + + sympy.matrices.dense.DenseMatrix.LDLdecomposition + sympy.matrices.dense.DenseMatrix.lower_triangular_solve + sympy.matrices.dense.DenseMatrix.upper_triangular_solve + gauss_jordan_solve + cholesky_solve + diagonal_solve + LUsolve + QRsolve + pinv_solve + cramer_solve + """ + + if M.rows < M.cols: + raise NotImplementedError( + 'Under-determined System. Try M.gauss_jordan_solve(rhs)') + + hermitian = True + reform = False + + if M.is_symmetric(): + hermitian = False + elif not M.is_hermitian: + reform = True + + if reform or _fuzzy_positive_definite(M) is False: + H = M.H + M = H.multiply(M) + rhs = H.multiply(rhs) + hermitian = not M.is_symmetric() + + L, D = M.LDLdecomposition(hermitian=hermitian) + Y = L.lower_triangular_solve(rhs) + Z = D.diagonal_solve(Y) + + if hermitian: + return (L.H).upper_triangular_solve(Z) + else: + return (L.T).upper_triangular_solve(Z) + + +def _LUsolve(M, rhs, iszerofunc=_iszero): + """Solve the linear system ``Ax = rhs`` for ``x`` where ``A = M``. + + This is for symbolic matrices, for real or complex ones use + mpmath.lu_solve or mpmath.qr_solve. + + See Also + ======== + + sympy.matrices.dense.DenseMatrix.lower_triangular_solve + sympy.matrices.dense.DenseMatrix.upper_triangular_solve + gauss_jordan_solve + cholesky_solve + diagonal_solve + LDLsolve + QRsolve + pinv_solve + LUdecomposition + cramer_solve + """ + + if rhs.rows != M.rows: + raise ShapeError( + "``M`` and ``rhs`` must have the same number of rows.") + + m = M.rows + n = M.cols + + if m < n: + raise NotImplementedError("Underdetermined systems not supported.") + + try: + A, perm = M.LUdecomposition_Simple( + iszerofunc=iszerofunc, rankcheck=True) + except ValueError: + raise NonInvertibleMatrixError("Matrix det == 0; not invertible.") + + dps = _get_intermediate_simp() + b = rhs.permute_rows(perm).as_mutable() + + # forward substitution, all diag entries are scaled to 1 + for i in range(m): + for j in range(min(i, n)): + scale = A[i, j] + b.zip_row_op(i, j, lambda x, y: dps(x - scale * y)) + + # consistency check for overdetermined systems + if m > n: + for i in range(n, m): + for j in range(b.cols): + if not iszerofunc(b[i, j]): + raise ValueError("The system is inconsistent.") + + b = b[0:n, :] # truncate zero rows if consistent + + # backward substitution + for i in range(n - 1, -1, -1): + for j in range(i + 1, n): + scale = A[i, j] + b.zip_row_op(i, j, lambda x, y: dps(x - scale * y)) + + scale = A[i, i] + b.row_op(i, lambda x, _: dps(scale**-1 * x)) + + return rhs.__class__(b) + + +def _QRsolve(M, b): + """Solve the linear system ``Ax = b``. + + ``M`` is the matrix ``A``, the method argument is the vector + ``b``. The method returns the solution vector ``x``. If ``b`` is a + matrix, the system is solved for each column of ``b`` and the + return value is a matrix of the same shape as ``b``. + + This method is slower (approximately by a factor of 2) but + more stable for floating-point arithmetic than the LUsolve method. + However, LUsolve usually uses an exact arithmetic, so you do not need + to use QRsolve. + + This is mainly for educational purposes and symbolic matrices, for real + (or complex) matrices use mpmath.qr_solve. + + See Also + ======== + + sympy.matrices.dense.DenseMatrix.lower_triangular_solve + sympy.matrices.dense.DenseMatrix.upper_triangular_solve + gauss_jordan_solve + cholesky_solve + diagonal_solve + LDLsolve + LUsolve + pinv_solve + QRdecomposition + cramer_solve + """ + + dps = _get_intermediate_simp(expand_mul, expand_mul) + Q, R = M.QRdecomposition() + y = Q.T * b + + # back substitution to solve R*x = y: + # We build up the result "backwards" in the vector 'x' and reverse it + # only in the end. + x = [] + n = R.rows + + for j in range(n - 1, -1, -1): + tmp = y[j, :] + + for k in range(j + 1, n): + tmp -= R[j, k] * x[n - 1 - k] + + tmp = dps(tmp) + + x.append(tmp / R[j, j]) + + return M.vstack(*x[::-1]) + + +def _gauss_jordan_solve(M, B, freevar=False): + """ + Solves ``Ax = B`` using Gauss Jordan elimination. + + There may be zero, one, or infinite solutions. If one solution + exists, it will be returned. If infinite solutions exist, it will + be returned parametrically. If no solutions exist, It will throw + ValueError. + + Parameters + ========== + + B : Matrix + The right hand side of the equation to be solved for. Must have + the same number of rows as matrix A. + + freevar : boolean, optional + Flag, when set to `True` will return the indices of the free + variables in the solutions (column Matrix), for a system that is + undetermined (e.g. A has more columns than rows), for which + infinite solutions are possible, in terms of arbitrary + values of free variables. Default `False`. + + Returns + ======= + + x : Matrix + The matrix that will satisfy ``Ax = B``. Will have as many rows as + matrix A has columns, and as many columns as matrix B. + + params : Matrix + If the system is underdetermined (e.g. A has more columns than + rows), infinite solutions are possible, in terms of arbitrary + parameters. These arbitrary parameters are returned as params + Matrix. + + free_var_index : List, optional + If the system is underdetermined (e.g. A has more columns than + rows), infinite solutions are possible, in terms of arbitrary + values of free variables. Then the indices of the free variables + in the solutions (column Matrix) are returned by free_var_index, + if the flag `freevar` is set to `True`. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([[1, 2, 1, 1], [1, 2, 2, -1], [2, 4, 0, 6]]) + >>> B = Matrix([7, 12, 4]) + >>> sol, params = A.gauss_jordan_solve(B) + >>> sol + Matrix([ + [-2*tau0 - 3*tau1 + 2], + [ tau0], + [ 2*tau1 + 5], + [ tau1]]) + >>> params + Matrix([ + [tau0], + [tau1]]) + >>> taus_zeroes = { tau:0 for tau in params } + >>> sol_unique = sol.xreplace(taus_zeroes) + >>> sol_unique + Matrix([ + [2], + [0], + [5], + [0]]) + + + >>> A = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 10]]) + >>> B = Matrix([3, 6, 9]) + >>> sol, params = A.gauss_jordan_solve(B) + >>> sol + Matrix([ + [-1], + [ 2], + [ 0]]) + >>> params + Matrix(0, 1, []) + + >>> A = Matrix([[2, -7], [-1, 4]]) + >>> B = Matrix([[-21, 3], [12, -2]]) + >>> sol, params = A.gauss_jordan_solve(B) + >>> sol + Matrix([ + [0, -2], + [3, -1]]) + >>> params + Matrix(0, 2, []) + + + >>> from sympy import Matrix + >>> A = Matrix([[1, 2, 1, 1], [1, 2, 2, -1], [2, 4, 0, 6]]) + >>> B = Matrix([7, 12, 4]) + >>> sol, params, freevars = A.gauss_jordan_solve(B, freevar=True) + >>> sol + Matrix([ + [-2*tau0 - 3*tau1 + 2], + [ tau0], + [ 2*tau1 + 5], + [ tau1]]) + >>> params + Matrix([ + [tau0], + [tau1]]) + >>> freevars + [1, 3] + + + See Also + ======== + + sympy.matrices.dense.DenseMatrix.lower_triangular_solve + sympy.matrices.dense.DenseMatrix.upper_triangular_solve + cholesky_solve + diagonal_solve + LDLsolve + LUsolve + QRsolve + pinv + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gaussian_elimination + + """ + + from sympy.matrices import Matrix, zeros + + cls = M.__class__ + aug = M.hstack(M.copy(), B.copy()) + B_cols = B.cols + row, col = aug[:, :-B_cols].shape + + # solve by reduced row echelon form + A, pivots = aug.rref(simplify=True) + A, v = A[:, :-B_cols], A[:, -B_cols:] + pivots = list(filter(lambda p: p < col, pivots)) + rank = len(pivots) + + # Get index of free symbols (free parameters) + # non-pivots columns are free variables + free_var_index = [c for c in range(A.cols) if c not in pivots] + + # Bring to block form + permutation = Matrix(pivots + free_var_index).T + + # check for existence of solutions + # rank of aug Matrix should be equal to rank of coefficient matrix + if not v[rank:, :].is_zero_matrix: + raise ValueError("Linear system has no solution") + + # Free parameters + # what are current unnumbered free symbol names? + name = uniquely_named_symbol('tau', [aug], + compare=lambda i: str(i).rstrip('1234567890'), + modify=lambda s: '_' + s).name + gen = numbered_symbols(name) + tau = Matrix([next(gen) for k in range((col - rank)*B_cols)]).reshape( + col - rank, B_cols) + + # Full parametric solution + V = A[:rank, free_var_index] + vt = v[:rank, :] + free_sol = tau.vstack(vt - V * tau, tau) + + # Undo permutation + sol = zeros(col, B_cols) + + for k in range(col): + sol[permutation[k], :] = free_sol[k,:] + + sol, tau = cls(sol), cls(tau) + + if freevar: + return sol, tau, free_var_index + else: + return sol, tau + + +def _pinv_solve(M, B, arbitrary_matrix=None): + """Solve ``Ax = B`` using the Moore-Penrose pseudoinverse. + + There may be zero, one, or infinite solutions. If one solution + exists, it will be returned. If infinite solutions exist, one will + be returned based on the value of arbitrary_matrix. If no solutions + exist, the least-squares solution is returned. + + Parameters + ========== + + B : Matrix + The right hand side of the equation to be solved for. Must have + the same number of rows as matrix A. + arbitrary_matrix : Matrix + If the system is underdetermined (e.g. A has more columns than + rows), infinite solutions are possible, in terms of an arbitrary + matrix. This parameter may be set to a specific matrix to use + for that purpose; if so, it must be the same shape as x, with as + many rows as matrix A has columns, and as many columns as matrix + B. If left as None, an appropriate matrix containing dummy + symbols in the form of ``wn_m`` will be used, with n and m being + row and column position of each symbol. + + Returns + ======= + + x : Matrix + The matrix that will satisfy ``Ax = B``. Will have as many rows as + matrix A has columns, and as many columns as matrix B. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([[1, 2, 3], [4, 5, 6]]) + >>> B = Matrix([7, 8]) + >>> A.pinv_solve(B) + Matrix([ + [ _w0_0/6 - _w1_0/3 + _w2_0/6 - 55/18], + [-_w0_0/3 + 2*_w1_0/3 - _w2_0/3 + 1/9], + [ _w0_0/6 - _w1_0/3 + _w2_0/6 + 59/18]]) + >>> A.pinv_solve(B, arbitrary_matrix=Matrix([0, 0, 0])) + Matrix([ + [-55/18], + [ 1/9], + [ 59/18]]) + + See Also + ======== + + sympy.matrices.dense.DenseMatrix.lower_triangular_solve + sympy.matrices.dense.DenseMatrix.upper_triangular_solve + gauss_jordan_solve + cholesky_solve + diagonal_solve + LDLsolve + LUsolve + QRsolve + pinv + + Notes + ===== + + This may return either exact solutions or least squares solutions. + To determine which, check ``A * A.pinv() * B == B``. It will be + True if exact solutions exist, and False if only a least-squares + solution exists. Be aware that the left hand side of that equation + may need to be simplified to correctly compare to the right hand + side. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Moore-Penrose_pseudoinverse#Obtaining_all_solutions_of_a_linear_system + + """ + + from sympy.matrices import eye + + A = M + A_pinv = M.pinv() + + if arbitrary_matrix is None: + rows, cols = A.cols, B.cols + w = symbols('w:{}_:{}'.format(rows, cols), cls=Dummy) + arbitrary_matrix = M.__class__(cols, rows, w).T + + return A_pinv.multiply(B) + (eye(A.cols) - + A_pinv.multiply(A)).multiply(arbitrary_matrix) + + +def _cramer_solve(M, rhs, det_method="laplace"): + """Solves system of linear equations using Cramer's rule. + + This method is relatively inefficient compared to other methods. + However it only uses a single division, assuming a division-free determinant + method is provided. This is helpful to minimize the chance of divide-by-zero + cases in symbolic solutions to linear systems. + + Parameters + ========== + M : Matrix + The matrix representing the left hand side of the equation. + rhs : Matrix + The matrix representing the right hand side of the equation. + det_method : str or callable + The method to use to calculate the determinant of the matrix. + The default is ``'laplace'``. If a callable is passed, it should take a + single argument, the matrix, and return the determinant of the matrix. + + Returns + ======= + x : Matrix + The matrix that will satisfy ``Ax = B``. Will have as many rows as + matrix A has columns, and as many columns as matrix B. + + Examples + ======== + + >>> from sympy import Matrix + >>> A = Matrix([[0, -6, 1], [0, -6, -1], [-5, -2, 3]]) + >>> B = Matrix([[-30, -9], [-18, -27], [-26, 46]]) + >>> x = A.cramer_solve(B) + >>> x + Matrix([ + [ 0, -5], + [ 4, 3], + [-6, 9]]) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Cramer%27s_rule#Explicit_formulas_for_small_systems + + """ + from .dense import zeros + + def entry(i, j): + return rhs[i, sol] if j == col else M[i, j] + + if det_method == "bird": + from .determinant import _det_bird + det = _det_bird + elif det_method == "laplace": + from .determinant import _det_laplace + det = _det_laplace + elif isinstance(det_method, str): + det = lambda matrix: matrix.det(method=det_method) + else: + det = det_method + det_M = det(M) + x = zeros(*rhs.shape) + for sol in range(rhs.shape[1]): + for col in range(rhs.shape[0]): + x[col, sol] = det(M.__class__(*M.shape, entry)) / det_M + return M.__class__(x) + + +def _solve(M, rhs, method='GJ'): + """Solves linear equation where the unique solution exists. + + Parameters + ========== + + rhs : Matrix + Vector representing the right hand side of the linear equation. + + method : string, optional + If set to ``'GJ'`` or ``'GE'``, the Gauss-Jordan elimination will be + used, which is implemented in the routine ``gauss_jordan_solve``. + + If set to ``'LU'``, ``LUsolve`` routine will be used. + + If set to ``'QR'``, ``QRsolve`` routine will be used. + + If set to ``'PINV'``, ``pinv_solve`` routine will be used. + + If set to ``'CRAMER'``, ``cramer_solve`` routine will be used. + + It also supports the methods available for special linear systems + + For positive definite systems: + + If set to ``'CH'``, ``cholesky_solve`` routine will be used. + + If set to ``'LDL'``, ``LDLsolve`` routine will be used. + + To use a different method and to compute the solution via the + inverse, use a method defined in the .inv() docstring. + + Returns + ======= + + solutions : Matrix + Vector representing the solution. + + Raises + ====== + + ValueError + If there is not a unique solution then a ``ValueError`` will be + raised. + + If ``M`` is not square, a ``ValueError`` and a different routine + for solving the system will be suggested. + """ + + if method in ('GJ', 'GE'): + try: + soln, param = M.gauss_jordan_solve(rhs) + + if param: + raise NonInvertibleMatrixError("Matrix det == 0; not invertible. " + "Try ``M.gauss_jordan_solve(rhs)`` to obtain a parametric solution.") + + except ValueError: + raise NonInvertibleMatrixError("Matrix det == 0; not invertible.") + + return soln + + elif method == 'LU': + return M.LUsolve(rhs) + elif method == 'CH': + return M.cholesky_solve(rhs) + elif method == 'QR': + return M.QRsolve(rhs) + elif method == 'LDL': + return M.LDLsolve(rhs) + elif method == 'PINV': + return M.pinv_solve(rhs) + elif method == 'CRAMER': + return M.cramer_solve(rhs) + else: + return M.inv(method=method).multiply(rhs) + + +def _solve_least_squares(M, rhs, method='CH'): + """Return the least-square fit to the data. + + Parameters + ========== + + rhs : Matrix + Vector representing the right hand side of the linear equation. + + method : string or boolean, optional + If set to ``'CH'``, ``cholesky_solve`` routine will be used. + + If set to ``'LDL'``, ``LDLsolve`` routine will be used. + + If set to ``'QR'``, ``QRsolve`` routine will be used. + + If set to ``'PINV'``, ``pinv_solve`` routine will be used. + + Otherwise, the conjugate of ``M`` will be used to create a system + of equations that is passed to ``solve`` along with the hint + defined by ``method``. + + Returns + ======= + + solutions : Matrix + Vector representing the solution. + + Examples + ======== + + >>> from sympy import Matrix, ones + >>> A = Matrix([1, 2, 3]) + >>> B = Matrix([2, 3, 4]) + >>> S = Matrix(A.row_join(B)) + >>> S + Matrix([ + [1, 2], + [2, 3], + [3, 4]]) + + If each line of S represent coefficients of Ax + By + and x and y are [2, 3] then S*xy is: + + >>> r = S*Matrix([2, 3]); r + Matrix([ + [ 8], + [13], + [18]]) + + But let's add 1 to the middle value and then solve for the + least-squares value of xy: + + >>> xy = S.solve_least_squares(Matrix([8, 14, 18])); xy + Matrix([ + [ 5/3], + [10/3]]) + + The error is given by S*xy - r: + + >>> S*xy - r + Matrix([ + [1/3], + [1/3], + [1/3]]) + >>> _.norm().n(2) + 0.58 + + If a different xy is used, the norm will be higher: + + >>> xy += ones(2, 1)/10 + >>> (S*xy - r).norm().n(2) + 1.5 + + """ + + if method == 'CH': + return M.cholesky_solve(rhs) + elif method == 'QR': + return M.QRsolve(rhs) + elif method == 'LDL': + return M.LDLsolve(rhs) + elif method == 'PINV': + return M.pinv_solve(rhs) + else: + t = M.H + return (t * M).solve(t * rhs, method=method) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/sparse.py b/venv/lib/python3.11/site-packages/sympy/matrices/sparse.py new file mode 100644 index 0000000000000000000000000000000000000000..95a7b3ca0ac29cf4409ec1eeecd059f9643e9bbc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/sparse.py @@ -0,0 +1,473 @@ +from collections.abc import Callable + +from sympy.core.containers import Dict +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.utilities.iterables import is_sequence +from sympy.utilities.misc import as_int + +from .matrixbase import MatrixBase +from .repmatrix import MutableRepMatrix, RepMatrix + +from .utilities import _iszero + +from .decompositions import ( + _liupc, _row_structure_symbolic_cholesky, _cholesky_sparse, + _LDLdecomposition_sparse) + +from .solvers import ( + _lower_triangular_solve_sparse, _upper_triangular_solve_sparse) + + +class SparseRepMatrix(RepMatrix): + """ + A sparse matrix (a matrix with a large number of zero elements). + + Examples + ======== + + >>> from sympy import SparseMatrix, ones + >>> SparseMatrix(2, 2, range(4)) + Matrix([ + [0, 1], + [2, 3]]) + >>> SparseMatrix(2, 2, {(1, 1): 2}) + Matrix([ + [0, 0], + [0, 2]]) + + A SparseMatrix can be instantiated from a ragged list of lists: + + >>> SparseMatrix([[1, 2, 3], [1, 2], [1]]) + Matrix([ + [1, 2, 3], + [1, 2, 0], + [1, 0, 0]]) + + For safety, one may include the expected size and then an error + will be raised if the indices of any element are out of range or + (for a flat list) if the total number of elements does not match + the expected shape: + + >>> SparseMatrix(2, 2, [1, 2]) + Traceback (most recent call last): + ... + ValueError: List length (2) != rows*columns (4) + + Here, an error is not raised because the list is not flat and no + element is out of range: + + >>> SparseMatrix(2, 2, [[1, 2]]) + Matrix([ + [1, 2], + [0, 0]]) + + But adding another element to the first (and only) row will cause + an error to be raised: + + >>> SparseMatrix(2, 2, [[1, 2, 3]]) + Traceback (most recent call last): + ... + ValueError: The location (0, 2) is out of designated range: (1, 1) + + To autosize the matrix, pass None for rows: + + >>> SparseMatrix(None, [[1, 2, 3]]) + Matrix([[1, 2, 3]]) + >>> SparseMatrix(None, {(1, 1): 1, (3, 3): 3}) + Matrix([ + [0, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 3]]) + + Values that are themselves a Matrix are automatically expanded: + + >>> SparseMatrix(4, 4, {(1, 1): ones(2)}) + Matrix([ + [0, 0, 0, 0], + [0, 1, 1, 0], + [0, 1, 1, 0], + [0, 0, 0, 0]]) + + A ValueError is raised if the expanding matrix tries to overwrite + a different element already present: + + >>> SparseMatrix(3, 3, {(0, 0): ones(2), (1, 1): 2}) + Traceback (most recent call last): + ... + ValueError: collision at (1, 1) + + See Also + ======== + DenseMatrix + MutableSparseMatrix + ImmutableSparseMatrix + """ + + @classmethod + def _handle_creation_inputs(cls, *args, **kwargs): + if len(args) == 1 and isinstance(args[0], MatrixBase): + rows = args[0].rows + cols = args[0].cols + smat = args[0].todok() + return rows, cols, smat + + smat = {} + # autosizing + if len(args) == 2 and args[0] is None: + args = [None, None, args[1]] + + if len(args) == 3: + r, c = args[:2] + if r is c is None: + rows = cols = None + elif None in (r, c): + raise ValueError( + 'Pass rows=None and no cols for autosizing.') + else: + rows, cols = as_int(args[0]), as_int(args[1]) + + if isinstance(args[2], Callable): + op = args[2] + + if None in (rows, cols): + raise ValueError( + "{} and {} must be integers for this " + "specification.".format(rows, cols)) + + row_indices = [cls._sympify(i) for i in range(rows)] + col_indices = [cls._sympify(j) for j in range(cols)] + + for i in row_indices: + for j in col_indices: + value = cls._sympify(op(i, j)) + if value != cls.zero: + smat[i, j] = value + + return rows, cols, smat + + elif isinstance(args[2], (dict, Dict)): + def update(i, j, v): + # update smat and make sure there are no collisions + if v: + if (i, j) in smat and v != smat[i, j]: + raise ValueError( + "There is a collision at {} for {} and {}." + .format((i, j), v, smat[i, j]) + ) + smat[i, j] = v + + # manual copy, copy.deepcopy() doesn't work + for (r, c), v in args[2].items(): + if isinstance(v, MatrixBase): + for (i, j), vv in v.todok().items(): + update(r + i, c + j, vv) + elif isinstance(v, (list, tuple)): + _, _, smat = cls._handle_creation_inputs(v, **kwargs) + for i, j in smat: + update(r + i, c + j, smat[i, j]) + else: + v = cls._sympify(v) + update(r, c, cls._sympify(v)) + + elif is_sequence(args[2]): + flat = not any(is_sequence(i) for i in args[2]) + if not flat: + _, _, smat = \ + cls._handle_creation_inputs(args[2], **kwargs) + else: + flat_list = args[2] + if len(flat_list) != rows * cols: + raise ValueError( + "The length of the flat list ({}) does not " + "match the specified size ({} * {})." + .format(len(flat_list), rows, cols) + ) + + for i in range(rows): + for j in range(cols): + value = flat_list[i*cols + j] + value = cls._sympify(value) + if value != cls.zero: + smat[i, j] = value + + if rows is None: # autosizing + keys = smat.keys() + rows = max(r for r, _ in keys) + 1 if keys else 0 + cols = max(c for _, c in keys) + 1 if keys else 0 + + else: + for i, j in smat.keys(): + if i and i >= rows or j and j >= cols: + raise ValueError( + "The location {} is out of the designated range" + "[{}, {}]x[{}, {}]" + .format((i, j), 0, rows - 1, 0, cols - 1) + ) + + return rows, cols, smat + + elif len(args) == 1 and isinstance(args[0], (list, tuple)): + # list of values or lists + v = args[0] + c = 0 + for i, row in enumerate(v): + if not isinstance(row, (list, tuple)): + row = [row] + for j, vv in enumerate(row): + if vv != cls.zero: + smat[i, j] = cls._sympify(vv) + c = max(c, len(row)) + rows = len(v) if c else 0 + cols = c + return rows, cols, smat + + else: + # handle full matrix forms with _handle_creation_inputs + rows, cols, mat = super()._handle_creation_inputs(*args) + for i in range(rows): + for j in range(cols): + value = mat[cols*i + j] + if value != cls.zero: + smat[i, j] = value + + return rows, cols, smat + + @property + def _smat(self): + + sympy_deprecation_warning( + """ + The private _smat attribute of SparseMatrix is deprecated. Use the + .todok() method instead. + """, + deprecated_since_version="1.9", + active_deprecations_target="deprecated-private-matrix-attributes" + ) + + return self.todok() + + def _eval_inverse(self, **kwargs): + return self.inv(method=kwargs.get('method', 'LDL'), + iszerofunc=kwargs.get('iszerofunc', _iszero), + try_block_diag=kwargs.get('try_block_diag', False)) + + def applyfunc(self, f): + """Apply a function to each element of the matrix. + + Examples + ======== + + >>> from sympy import SparseMatrix + >>> m = SparseMatrix(2, 2, lambda i, j: i*2+j) + >>> m + Matrix([ + [0, 1], + [2, 3]]) + >>> m.applyfunc(lambda i: 2*i) + Matrix([ + [0, 2], + [4, 6]]) + + """ + if not callable(f): + raise TypeError("`f` must be callable.") + + # XXX: This only applies the function to the nonzero elements of the + # matrix so is inconsistent with DenseMatrix.applyfunc e.g. + # zeros(2, 2).applyfunc(lambda x: x + 1) + dok = {} + for k, v in self.todok().items(): + fv = f(v) + if fv != 0: + dok[k] = fv + + return self._new(self.rows, self.cols, dok) + + def as_immutable(self): + """Returns an Immutable version of this Matrix.""" + from .immutable import ImmutableSparseMatrix + return ImmutableSparseMatrix(self) + + def as_mutable(self): + """Returns a mutable version of this matrix. + + Examples + ======== + + >>> from sympy import ImmutableMatrix + >>> X = ImmutableMatrix([[1, 2], [3, 4]]) + >>> Y = X.as_mutable() + >>> Y[1, 1] = 5 # Can set values in Y + >>> Y + Matrix([ + [1, 2], + [3, 5]]) + """ + return MutableSparseMatrix(self) + + def col_list(self): + """Returns a column-sorted list of non-zero elements of the matrix. + + Examples + ======== + + >>> from sympy import SparseMatrix + >>> a=SparseMatrix(((1, 2), (3, 4))) + >>> a + Matrix([ + [1, 2], + [3, 4]]) + >>> a.CL + [(0, 0, 1), (1, 0, 3), (0, 1, 2), (1, 1, 4)] + + See Also + ======== + + sympy.matrices.sparse.SparseMatrix.row_list + """ + return [tuple(k + (self[k],)) for k in sorted(self.todok().keys(), key=lambda k: list(reversed(k)))] + + def nnz(self): + """Returns the number of non-zero elements in Matrix.""" + return len(self.todok()) + + def row_list(self): + """Returns a row-sorted list of non-zero elements of the matrix. + + Examples + ======== + + >>> from sympy import SparseMatrix + >>> a = SparseMatrix(((1, 2), (3, 4))) + >>> a + Matrix([ + [1, 2], + [3, 4]]) + >>> a.RL + [(0, 0, 1), (0, 1, 2), (1, 0, 3), (1, 1, 4)] + + See Also + ======== + + sympy.matrices.sparse.SparseMatrix.col_list + """ + return [tuple(k + (self[k],)) for k in + sorted(self.todok().keys(), key=list)] + + def scalar_multiply(self, scalar): + "Scalar element-wise multiplication" + return scalar * self + + def solve_least_squares(self, rhs, method='LDL'): + """Return the least-square fit to the data. + + By default the cholesky_solve routine is used (method='CH'); other + methods of matrix inversion can be used. To find out which are + available, see the docstring of the .inv() method. + + Examples + ======== + + >>> from sympy import SparseMatrix, Matrix, ones + >>> A = Matrix([1, 2, 3]) + >>> B = Matrix([2, 3, 4]) + >>> S = SparseMatrix(A.row_join(B)) + >>> S + Matrix([ + [1, 2], + [2, 3], + [3, 4]]) + + If each line of S represent coefficients of Ax + By + and x and y are [2, 3] then S*xy is: + + >>> r = S*Matrix([2, 3]); r + Matrix([ + [ 8], + [13], + [18]]) + + But let's add 1 to the middle value and then solve for the + least-squares value of xy: + + >>> xy = S.solve_least_squares(Matrix([8, 14, 18])); xy + Matrix([ + [ 5/3], + [10/3]]) + + The error is given by S*xy - r: + + >>> S*xy - r + Matrix([ + [1/3], + [1/3], + [1/3]]) + >>> _.norm().n(2) + 0.58 + + If a different xy is used, the norm will be higher: + + >>> xy += ones(2, 1)/10 + >>> (S*xy - r).norm().n(2) + 1.5 + + """ + t = self.T + return (t*self).inv(method=method)*t*rhs + + def solve(self, rhs, method='LDL'): + """Return solution to self*soln = rhs using given inversion method. + + For a list of possible inversion methods, see the .inv() docstring. + """ + if not self.is_square: + if self.rows < self.cols: + raise ValueError('Under-determined system.') + elif self.rows > self.cols: + raise ValueError('For over-determined system, M, having ' + 'more rows than columns, try M.solve_least_squares(rhs).') + else: + return self.inv(method=method).multiply(rhs) + + RL = property(row_list, None, None, "Alternate faster representation") + CL = property(col_list, None, None, "Alternate faster representation") + + def liupc(self): + return _liupc(self) + + def row_structure_symbolic_cholesky(self): + return _row_structure_symbolic_cholesky(self) + + def cholesky(self, hermitian=True): + return _cholesky_sparse(self, hermitian=hermitian) + + def LDLdecomposition(self, hermitian=True): + return _LDLdecomposition_sparse(self, hermitian=hermitian) + + def lower_triangular_solve(self, rhs): + return _lower_triangular_solve_sparse(self, rhs) + + def upper_triangular_solve(self, rhs): + return _upper_triangular_solve_sparse(self, rhs) + + liupc.__doc__ = _liupc.__doc__ + row_structure_symbolic_cholesky.__doc__ = _row_structure_symbolic_cholesky.__doc__ + cholesky.__doc__ = _cholesky_sparse.__doc__ + LDLdecomposition.__doc__ = _LDLdecomposition_sparse.__doc__ + lower_triangular_solve.__doc__ = lower_triangular_solve.__doc__ + upper_triangular_solve.__doc__ = upper_triangular_solve.__doc__ + + +class MutableSparseMatrix(SparseRepMatrix, MutableRepMatrix): + + @classmethod + def _new(cls, *args, **kwargs): + rows, cols, smat = cls._handle_creation_inputs(*args, **kwargs) + + rep = cls._smat_to_DomainMatrix(rows, cols, smat) + + return cls._fromrep(rep) + + +SparseMatrix = MutableSparseMatrix diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/sparsetools.py b/venv/lib/python3.11/site-packages/sympy/matrices/sparsetools.py new file mode 100644 index 0000000000000000000000000000000000000000..50048f6dc7e5cf160366963d16427987616ddce7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/sparsetools.py @@ -0,0 +1,300 @@ +from sympy.core.containers import Dict +from sympy.core.symbol import Dummy +from sympy.utilities.iterables import is_sequence +from sympy.utilities.misc import as_int, filldedent + +from .sparse import MutableSparseMatrix as SparseMatrix + + +def _doktocsr(dok): + """Converts a sparse matrix to Compressed Sparse Row (CSR) format. + + Parameters + ========== + + A : contains non-zero elements sorted by key (row, column) + JA : JA[i] is the column corresponding to A[i] + IA : IA[i] contains the index in A for the first non-zero element + of row[i]. Thus IA[i+1] - IA[i] gives number of non-zero + elements row[i]. The length of IA is always 1 more than the + number of rows in the matrix. + + Examples + ======== + + >>> from sympy.matrices.sparsetools import _doktocsr + >>> from sympy import SparseMatrix, diag + >>> m = SparseMatrix(diag(1, 2, 3)) + >>> m[2, 0] = -1 + >>> _doktocsr(m) + [[1, 2, -1, 3], [0, 1, 0, 2], [0, 1, 2, 4], [3, 3]] + + """ + row, JA, A = [list(i) for i in zip(*dok.row_list())] + IA = [0]*((row[0] if row else 0) + 1) + for i, r in enumerate(row): + IA.extend([i]*(r - row[i - 1])) # if i = 0 nothing is extended + IA.extend([len(A)]*(dok.rows - len(IA) + 1)) + shape = [dok.rows, dok.cols] + return [A, JA, IA, shape] + + +def _csrtodok(csr): + """Converts a CSR representation to DOK representation. + + Examples + ======== + + >>> from sympy.matrices.sparsetools import _csrtodok + >>> _csrtodok([[5, 8, 3, 6], [0, 1, 2, 1], [0, 0, 2, 3, 4], [4, 3]]) + Matrix([ + [0, 0, 0], + [5, 8, 0], + [0, 0, 3], + [0, 6, 0]]) + + """ + smat = {} + A, JA, IA, shape = csr + for i in range(len(IA) - 1): + indices = slice(IA[i], IA[i + 1]) + for l, m in zip(A[indices], JA[indices]): + smat[i, m] = l + return SparseMatrix(*shape, smat) + + +def banded(*args, **kwargs): + """Returns a SparseMatrix from the given dictionary describing + the diagonals of the matrix. The keys are positive for upper + diagonals and negative for those below the main diagonal. The + values may be: + + * expressions or single-argument functions, + + * lists or tuples of values, + + * matrices + + Unless dimensions are given, the size of the returned matrix will + be large enough to contain the largest non-zero value provided. + + kwargs + ====== + + rows : rows of the resulting matrix; computed if + not given. + + cols : columns of the resulting matrix; computed if + not given. + + Examples + ======== + + >>> from sympy import banded, ones, Matrix + >>> from sympy.abc import x + + If explicit values are given in tuples, + the matrix will autosize to contain all values, otherwise + a single value is filled onto the entire diagonal: + + >>> banded({1: (1, 2, 3), -1: (4, 5, 6), 0: x}) + Matrix([ + [x, 1, 0, 0], + [4, x, 2, 0], + [0, 5, x, 3], + [0, 0, 6, x]]) + + A function accepting a single argument can be used to fill the + diagonal as a function of diagonal index (which starts at 0). + The size (or shape) of the matrix must be given to obtain more + than a 1x1 matrix: + + >>> s = lambda d: (1 + d)**2 + >>> banded(5, {0: s, 2: s, -2: 2}) + Matrix([ + [1, 0, 1, 0, 0], + [0, 4, 0, 4, 0], + [2, 0, 9, 0, 9], + [0, 2, 0, 16, 0], + [0, 0, 2, 0, 25]]) + + The diagonal of matrices placed on a diagonal will coincide + with the indicated diagonal: + + >>> vert = Matrix([1, 2, 3]) + >>> banded({0: vert}, cols=3) + Matrix([ + [1, 0, 0], + [2, 1, 0], + [3, 2, 1], + [0, 3, 2], + [0, 0, 3]]) + + >>> banded(4, {0: ones(2)}) + Matrix([ + [1, 1, 0, 0], + [1, 1, 0, 0], + [0, 0, 1, 1], + [0, 0, 1, 1]]) + + Errors are raised if the designated size will not hold + all values an integral number of times. Here, the rows + are designated as odd (but an even number is required to + hold the off-diagonal 2x2 ones): + + >>> banded({0: 2, 1: ones(2)}, rows=5) + Traceback (most recent call last): + ... + ValueError: + sequence does not fit an integral number of times in the matrix + + And here, an even number of rows is given...but the square + matrix has an even number of columns, too. As we saw + in the previous example, an odd number is required: + + >>> banded(4, {0: 2, 1: ones(2)}) # trying to make 4x4 and cols must be odd + Traceback (most recent call last): + ... + ValueError: + sequence does not fit an integral number of times in the matrix + + A way around having to count rows is to enclosing matrix elements + in a tuple and indicate the desired number of them to the right: + + >>> banded({0: 2, 2: (ones(2),)*3}) + Matrix([ + [2, 0, 1, 1, 0, 0, 0, 0], + [0, 2, 1, 1, 0, 0, 0, 0], + [0, 0, 2, 0, 1, 1, 0, 0], + [0, 0, 0, 2, 1, 1, 0, 0], + [0, 0, 0, 0, 2, 0, 1, 1], + [0, 0, 0, 0, 0, 2, 1, 1]]) + + An error will be raised if more than one value + is written to a given entry. Here, the ones overlap + with the main diagonal if they are placed on the + first diagonal: + + >>> banded({0: (2,)*5, 1: (ones(2),)*3}) + Traceback (most recent call last): + ... + ValueError: collision at (1, 1) + + By placing a 0 at the bottom left of the 2x2 matrix of + ones, the collision is avoided: + + >>> u2 = Matrix([ + ... [1, 1], + ... [0, 1]]) + >>> banded({0: [2]*5, 1: [u2]*3}) + Matrix([ + [2, 1, 1, 0, 0, 0, 0], + [0, 2, 1, 0, 0, 0, 0], + [0, 0, 2, 1, 1, 0, 0], + [0, 0, 0, 2, 1, 0, 0], + [0, 0, 0, 0, 2, 1, 1], + [0, 0, 0, 0, 0, 0, 1]]) + """ + try: + if len(args) not in (1, 2, 3): + raise TypeError + if not isinstance(args[-1], (dict, Dict)): + raise TypeError + if len(args) == 1: + rows = kwargs.get('rows', None) + cols = kwargs.get('cols', None) + if rows is not None: + rows = as_int(rows) + if cols is not None: + cols = as_int(cols) + elif len(args) == 2: + rows = cols = as_int(args[0]) + else: + rows, cols = map(as_int, args[:2]) + # fails with ValueError if any keys are not ints + _ = all(as_int(k) for k in args[-1]) + except (ValueError, TypeError): + raise TypeError(filldedent( + '''unrecognized input to banded: + expecting [[row,] col,] {int: value}''')) + def rc(d): + # return row,col coord of diagonal start + r = -d if d < 0 else 0 + c = 0 if r else d + return r, c + smat = {} + undone = [] + tba = Dummy() + # first handle objects with size + for d, v in args[-1].items(): + r, c = rc(d) + # note: only list and tuple are recognized since this + # will allow other Basic objects like Tuple + # into the matrix if so desired + if isinstance(v, (list, tuple)): + extra = 0 + for i, vi in enumerate(v): + i += extra + if is_sequence(vi): + vi = SparseMatrix(vi) + smat[r + i, c + i] = vi + extra += min(vi.shape) - 1 + else: + smat[r + i, c + i] = vi + elif is_sequence(v): + v = SparseMatrix(v) + rv, cv = v.shape + if rows and cols: + nr, xr = divmod(rows - r, rv) + nc, xc = divmod(cols - c, cv) + x = xr or xc + do = min(nr, nc) + elif rows: + do, x = divmod(rows - r, rv) + elif cols: + do, x = divmod(cols - c, cv) + else: + do = 1 + x = 0 + if x: + raise ValueError(filldedent(''' + sequence does not fit an integral number of times + in the matrix''')) + j = min(v.shape) + for i in range(do): + smat[r, c] = v + r += j + c += j + elif v: + smat[r, c] = tba + undone.append((d, v)) + s = SparseMatrix(None, smat) # to expand matrices + smat = s.todok() + # check for dim errors here + if rows is not None and rows < s.rows: + raise ValueError('Designated rows %s < needed %s' % (rows, s.rows)) + if cols is not None and cols < s.cols: + raise ValueError('Designated cols %s < needed %s' % (cols, s.cols)) + if rows is cols is None: + rows = s.rows + cols = s.cols + elif rows is not None and cols is None: + cols = max(rows, s.cols) + elif cols is not None and rows is None: + rows = max(cols, s.rows) + def update(i, j, v): + # update smat and make sure there are + # no collisions + if v: + if (i, j) in smat and smat[i, j] not in (tba, v): + raise ValueError('collision at %s' % ((i, j),)) + smat[i, j] = v + if undone: + for d, vi in undone: + r, c = rc(d) + v = vi if callable(vi) else lambda _: vi + i = 0 + while r + i < rows and c + i < cols: + update(r + i, c + i, v(i)) + i += 1 + return SparseMatrix(rows, cols, smat) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/subspaces.py b/venv/lib/python3.11/site-packages/sympy/matrices/subspaces.py new file mode 100644 index 0000000000000000000000000000000000000000..1ab0b71b4289ebaeb6394059c6a7cd49d3a148a1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/subspaces.py @@ -0,0 +1,174 @@ +from .utilities import _iszero + + +def _columnspace(M, simplify=False): + """Returns a list of vectors (Matrix objects) that span columnspace of ``M`` + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix(3, 3, [1, 3, 0, -2, -6, 0, 3, 9, 6]) + >>> M + Matrix([ + [ 1, 3, 0], + [-2, -6, 0], + [ 3, 9, 6]]) + >>> M.columnspace() + [Matrix([ + [ 1], + [-2], + [ 3]]), Matrix([ + [0], + [0], + [6]])] + + See Also + ======== + + nullspace + rowspace + """ + + reduced, pivots = M.echelon_form(simplify=simplify, with_pivots=True) + + return [M.col(i) for i in pivots] + + +def _nullspace(M, simplify=False, iszerofunc=_iszero): + """Returns list of vectors (Matrix objects) that span nullspace of ``M`` + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix(3, 3, [1, 3, 0, -2, -6, 0, 3, 9, 6]) + >>> M + Matrix([ + [ 1, 3, 0], + [-2, -6, 0], + [ 3, 9, 6]]) + >>> M.nullspace() + [Matrix([ + [-3], + [ 1], + [ 0]])] + + See Also + ======== + + columnspace + rowspace + """ + + reduced, pivots = M.rref(iszerofunc=iszerofunc, simplify=simplify) + + free_vars = [i for i in range(M.cols) if i not in pivots] + basis = [] + + for free_var in free_vars: + # for each free variable, we will set it to 1 and all others + # to 0. Then, we will use back substitution to solve the system + vec = [M.zero] * M.cols + vec[free_var] = M.one + + for piv_row, piv_col in enumerate(pivots): + vec[piv_col] -= reduced[piv_row, free_var] + + basis.append(vec) + + return [M._new(M.cols, 1, b) for b in basis] + + +def _rowspace(M, simplify=False): + """Returns a list of vectors that span the row space of ``M``. + + Examples + ======== + + >>> from sympy import Matrix + >>> M = Matrix(3, 3, [1, 3, 0, -2, -6, 0, 3, 9, 6]) + >>> M + Matrix([ + [ 1, 3, 0], + [-2, -6, 0], + [ 3, 9, 6]]) + >>> M.rowspace() + [Matrix([[1, 3, 0]]), Matrix([[0, 0, 6]])] + """ + + reduced, pivots = M.echelon_form(simplify=simplify, with_pivots=True) + + return [reduced.row(i) for i in range(len(pivots))] + + +def _orthogonalize(cls, *vecs, normalize=False, rankcheck=False): + """Apply the Gram-Schmidt orthogonalization procedure + to vectors supplied in ``vecs``. + + Parameters + ========== + + vecs + vectors to be made orthogonal + + normalize : bool + If ``True``, return an orthonormal basis. + + rankcheck : bool + If ``True``, the computation does not stop when encountering + linearly dependent vectors. + + If ``False``, it will raise ``ValueError`` when any zero + or linearly dependent vectors are found. + + Returns + ======= + + list + List of orthogonal (or orthonormal) basis vectors. + + Examples + ======== + + >>> from sympy import I, Matrix + >>> v = [Matrix([1, I]), Matrix([1, -I])] + >>> Matrix.orthogonalize(*v) + [Matrix([ + [1], + [I]]), Matrix([ + [ 1], + [-I]])] + + See Also + ======== + + MatrixBase.QRdecomposition + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process + """ + from .decompositions import _QRdecomposition_optional + + if not vecs: + return [] + + all_row_vecs = (vecs[0].rows == 1) + + vecs = [x.vec() for x in vecs] + M = cls.hstack(*vecs) + Q, R = _QRdecomposition_optional(M, normalize=normalize) + + if rankcheck and Q.cols < len(vecs): + raise ValueError("GramSchmidt: vector set not linearly independent") + + ret = [] + for i in range(Q.cols): + if all_row_vecs: + col = cls(Q[:, i].T) + else: + col = cls(Q[:, i]) + ret.append(col) + return ret diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_commonmatrix.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_commonmatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..6735adc1a9d4f9934a55c7ee70b087a19d3a48b4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_commonmatrix.py @@ -0,0 +1,1266 @@ +# +# Code for testing deprecated matrix classes. New test code should not be added +# here. Instead, add it to test_matrixbase.py. +# +# This entire test module and the corresponding sympy/matrices/common.py +# module will be removed in a future release. +# +from sympy.testing.pytest import raises, XFAIL, warns_deprecated_sympy + +from sympy.assumptions import Q +from sympy.core.expr import Expr +from sympy.core.add import Add +from sympy.core.function import Function +from sympy.core.kind import NumberKind, UndefinedKind +from sympy.core.numbers import I, Integer, oo, pi, Rational +from sympy.core.singleton import S +from sympy.core.symbol import Symbol, symbols +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import cos, sin +from sympy.matrices.exceptions import ShapeError, NonSquareMatrixError +from sympy.matrices.kind import MatrixKind +from sympy.matrices.common import ( + _MinimalMatrix, _CastableMatrix, MatrixShaping, MatrixProperties, + MatrixOperations, MatrixArithmetic, MatrixSpecial) +from sympy.matrices.matrices import MatrixCalculus +from sympy.matrices import (Matrix, diag, eye, + matrix_multiply_elementwise, ones, zeros, SparseMatrix, banded, + MutableDenseMatrix, MutableSparseMatrix, ImmutableDenseMatrix, + ImmutableSparseMatrix) +from sympy.polys.polytools import Poly +from sympy.utilities.iterables import flatten +from sympy.tensor.array.dense_ndim_array import ImmutableDenseNDimArray as Array + +from sympy.abc import x, y, z + + +def test_matrix_deprecated_isinstance(): + + # Test that e.g. isinstance(M, MatrixCommon) still gives True when M is a + # Matrix for each of the deprecated matrix classes. + + from sympy.matrices.common import ( + MatrixRequired, + MatrixShaping, + MatrixSpecial, + MatrixProperties, + MatrixOperations, + MatrixArithmetic, + MatrixCommon + ) + from sympy.matrices.matrices import ( + MatrixDeterminant, + MatrixReductions, + MatrixSubspaces, + MatrixEigen, + MatrixCalculus, + MatrixDeprecated + ) + from sympy import ( + Matrix, + ImmutableMatrix, + SparseMatrix, + ImmutableSparseMatrix + ) + all_mixins = ( + MatrixRequired, + MatrixShaping, + MatrixSpecial, + MatrixProperties, + MatrixOperations, + MatrixArithmetic, + MatrixCommon, + MatrixDeterminant, + MatrixReductions, + MatrixSubspaces, + MatrixEigen, + MatrixCalculus, + MatrixDeprecated + ) + all_matrices = ( + Matrix, + ImmutableMatrix, + SparseMatrix, + ImmutableSparseMatrix + ) + + Ms = [M([[1, 2], [3, 4]]) for M in all_matrices] + t = () + + for mixin in all_mixins: + for M in Ms: + with warns_deprecated_sympy(): + assert isinstance(M, mixin) is True + with warns_deprecated_sympy(): + assert isinstance(t, mixin) is False + + +# classes to test the deprecated matrix classes. We use warns_deprecated_sympy +# to suppress the deprecation warnings because subclassing the deprecated +# classes causes a warning to be raised. + +with warns_deprecated_sympy(): + class ShapingOnlyMatrix(_MinimalMatrix, _CastableMatrix, MatrixShaping): + pass + + +def eye_Shaping(n): + return ShapingOnlyMatrix(n, n, lambda i, j: int(i == j)) + + +def zeros_Shaping(n): + return ShapingOnlyMatrix(n, n, lambda i, j: 0) + + +with warns_deprecated_sympy(): + class PropertiesOnlyMatrix(_MinimalMatrix, _CastableMatrix, MatrixProperties): + pass + + +def eye_Properties(n): + return PropertiesOnlyMatrix(n, n, lambda i, j: int(i == j)) + + +def zeros_Properties(n): + return PropertiesOnlyMatrix(n, n, lambda i, j: 0) + + +with warns_deprecated_sympy(): + class OperationsOnlyMatrix(_MinimalMatrix, _CastableMatrix, MatrixOperations): + pass + + +def eye_Operations(n): + return OperationsOnlyMatrix(n, n, lambda i, j: int(i == j)) + + +def zeros_Operations(n): + return OperationsOnlyMatrix(n, n, lambda i, j: 0) + + +with warns_deprecated_sympy(): + class ArithmeticOnlyMatrix(_MinimalMatrix, _CastableMatrix, MatrixArithmetic): + pass + + +def eye_Arithmetic(n): + return ArithmeticOnlyMatrix(n, n, lambda i, j: int(i == j)) + + +def zeros_Arithmetic(n): + return ArithmeticOnlyMatrix(n, n, lambda i, j: 0) + + +with warns_deprecated_sympy(): + class SpecialOnlyMatrix(_MinimalMatrix, _CastableMatrix, MatrixSpecial): + pass + + +with warns_deprecated_sympy(): + class CalculusOnlyMatrix(_MinimalMatrix, _CastableMatrix, MatrixCalculus): + pass + + +def test__MinimalMatrix(): + x = _MinimalMatrix(2, 3, [1, 2, 3, 4, 5, 6]) + assert x.rows == 2 + assert x.cols == 3 + assert x[2] == 3 + assert x[1, 1] == 5 + assert list(x) == [1, 2, 3, 4, 5, 6] + assert list(x[1, :]) == [4, 5, 6] + assert list(x[:, 1]) == [2, 5] + assert list(x[:, :]) == list(x) + assert x[:, :] == x + assert _MinimalMatrix(x) == x + assert _MinimalMatrix([[1, 2, 3], [4, 5, 6]]) == x + assert _MinimalMatrix(([1, 2, 3], [4, 5, 6])) == x + assert _MinimalMatrix([(1, 2, 3), (4, 5, 6)]) == x + assert _MinimalMatrix(((1, 2, 3), (4, 5, 6))) == x + assert not (_MinimalMatrix([[1, 2], [3, 4], [5, 6]]) == x) + + +def test_kind(): + assert Matrix([[1, 2], [3, 4]]).kind == MatrixKind(NumberKind) + assert Matrix([[0, 0], [0, 0]]).kind == MatrixKind(NumberKind) + assert Matrix(0, 0, []).kind == MatrixKind(NumberKind) + assert Matrix([[x]]).kind == MatrixKind(NumberKind) + assert Matrix([[1, Matrix([[1]])]]).kind == MatrixKind(UndefinedKind) + assert SparseMatrix([[1]]).kind == MatrixKind(NumberKind) + assert SparseMatrix([[1, Matrix([[1]])]]).kind == MatrixKind(UndefinedKind) + + +# ShapingOnlyMatrix tests +def test_vec(): + m = ShapingOnlyMatrix(2, 2, [1, 3, 2, 4]) + m_vec = m.vec() + assert m_vec.cols == 1 + for i in range(4): + assert m_vec[i] == i + 1 + + +def test_todok(): + a, b, c, d = symbols('a:d') + m1 = MutableDenseMatrix([[a, b], [c, d]]) + m2 = ImmutableDenseMatrix([[a, b], [c, d]]) + m3 = MutableSparseMatrix([[a, b], [c, d]]) + m4 = ImmutableSparseMatrix([[a, b], [c, d]]) + assert m1.todok() == m2.todok() == m3.todok() == m4.todok() == \ + {(0, 0): a, (0, 1): b, (1, 0): c, (1, 1): d} + + +def test_tolist(): + lst = [[S.One, S.Half, x*y, S.Zero], [x, y, z, x**2], [y, -S.One, z*x, 3]] + flat_lst = [S.One, S.Half, x*y, S.Zero, x, y, z, x**2, y, -S.One, z*x, 3] + m = ShapingOnlyMatrix(3, 4, flat_lst) + assert m.tolist() == lst + +def test_todod(): + m = ShapingOnlyMatrix(3, 2, [[S.One, 0], [0, S.Half], [x, 0]]) + dict = {0: {0: S.One}, 1: {1: S.Half}, 2: {0: x}} + assert m.todod() == dict + +def test_row_col_del(): + e = ShapingOnlyMatrix(3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 9]) + raises(IndexError, lambda: e.row_del(5)) + raises(IndexError, lambda: e.row_del(-5)) + raises(IndexError, lambda: e.col_del(5)) + raises(IndexError, lambda: e.col_del(-5)) + + assert e.row_del(2) == e.row_del(-1) == Matrix([[1, 2, 3], [4, 5, 6]]) + assert e.col_del(2) == e.col_del(-1) == Matrix([[1, 2], [4, 5], [7, 8]]) + + assert e.row_del(1) == e.row_del(-2) == Matrix([[1, 2, 3], [7, 8, 9]]) + assert e.col_del(1) == e.col_del(-2) == Matrix([[1, 3], [4, 6], [7, 9]]) + + +def test_get_diag_blocks1(): + a = Matrix([[1, 2], [2, 3]]) + b = Matrix([[3, x], [y, 3]]) + c = Matrix([[3, x, 3], [y, 3, z], [x, y, z]]) + assert a.get_diag_blocks() == [a] + assert b.get_diag_blocks() == [b] + assert c.get_diag_blocks() == [c] + + +def test_get_diag_blocks2(): + a = Matrix([[1, 2], [2, 3]]) + b = Matrix([[3, x], [y, 3]]) + c = Matrix([[3, x, 3], [y, 3, z], [x, y, z]]) + A, B, C, D = diag(a, b, b), diag(a, b, c), diag(a, c, b), diag(c, c, b) + A = ShapingOnlyMatrix(A.rows, A.cols, A) + B = ShapingOnlyMatrix(B.rows, B.cols, B) + C = ShapingOnlyMatrix(C.rows, C.cols, C) + D = ShapingOnlyMatrix(D.rows, D.cols, D) + + assert A.get_diag_blocks() == [a, b, b] + assert B.get_diag_blocks() == [a, b, c] + assert C.get_diag_blocks() == [a, c, b] + assert D.get_diag_blocks() == [c, c, b] + + +def test_shape(): + m = ShapingOnlyMatrix(1, 2, [0, 0]) + assert m.shape == (1, 2) + + +def test_reshape(): + m0 = eye_Shaping(3) + assert m0.reshape(1, 9) == Matrix(1, 9, (1, 0, 0, 0, 1, 0, 0, 0, 1)) + m1 = ShapingOnlyMatrix(3, 4, lambda i, j: i + j) + assert m1.reshape( + 4, 3) == Matrix(((0, 1, 2), (3, 1, 2), (3, 4, 2), (3, 4, 5))) + assert m1.reshape(2, 6) == Matrix(((0, 1, 2, 3, 1, 2), (3, 4, 2, 3, 4, 5))) + + +def test_row_col(): + m = ShapingOnlyMatrix(3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 9]) + assert m.row(0) == Matrix(1, 3, [1, 2, 3]) + assert m.col(0) == Matrix(3, 1, [1, 4, 7]) + + +def test_row_join(): + assert eye_Shaping(3).row_join(Matrix([7, 7, 7])) == \ + Matrix([[1, 0, 0, 7], + [0, 1, 0, 7], + [0, 0, 1, 7]]) + + +def test_col_join(): + assert eye_Shaping(3).col_join(Matrix([[7, 7, 7]])) == \ + Matrix([[1, 0, 0], + [0, 1, 0], + [0, 0, 1], + [7, 7, 7]]) + + +def test_row_insert(): + r4 = Matrix([[4, 4, 4]]) + for i in range(-4, 5): + l = [1, 0, 0] + l.insert(i, 4) + assert flatten(eye_Shaping(3).row_insert(i, r4).col(0).tolist()) == l + + +def test_col_insert(): + c4 = Matrix([4, 4, 4]) + for i in range(-4, 5): + l = [0, 0, 0] + l.insert(i, 4) + assert flatten(zeros_Shaping(3).col_insert(i, c4).row(0).tolist()) == l + # issue 13643 + assert eye_Shaping(6).col_insert(3, Matrix([[2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2]])) == \ + Matrix([[1, 0, 0, 2, 2, 0, 0, 0], + [0, 1, 0, 2, 2, 0, 0, 0], + [0, 0, 1, 2, 2, 0, 0, 0], + [0, 0, 0, 2, 2, 1, 0, 0], + [0, 0, 0, 2, 2, 0, 1, 0], + [0, 0, 0, 2, 2, 0, 0, 1]]) + + +def test_extract(): + m = ShapingOnlyMatrix(4, 3, lambda i, j: i*3 + j) + assert m.extract([0, 1, 3], [0, 1]) == Matrix(3, 2, [0, 1, 3, 4, 9, 10]) + assert m.extract([0, 3], [0, 0, 2]) == Matrix(2, 3, [0, 0, 2, 9, 9, 11]) + assert m.extract(range(4), range(3)) == m + raises(IndexError, lambda: m.extract([4], [0])) + raises(IndexError, lambda: m.extract([0], [3])) + + +def test_hstack(): + m = ShapingOnlyMatrix(4, 3, lambda i, j: i*3 + j) + m2 = ShapingOnlyMatrix(3, 4, lambda i, j: i*3 + j) + assert m == m.hstack(m) + assert m.hstack(m, m, m) == ShapingOnlyMatrix.hstack(m, m, m) == Matrix([ + [0, 1, 2, 0, 1, 2, 0, 1, 2], + [3, 4, 5, 3, 4, 5, 3, 4, 5], + [6, 7, 8, 6, 7, 8, 6, 7, 8], + [9, 10, 11, 9, 10, 11, 9, 10, 11]]) + raises(ShapeError, lambda: m.hstack(m, m2)) + assert Matrix.hstack() == Matrix() + + # test regression #12938 + M1 = Matrix.zeros(0, 0) + M2 = Matrix.zeros(0, 1) + M3 = Matrix.zeros(0, 2) + M4 = Matrix.zeros(0, 3) + m = ShapingOnlyMatrix.hstack(M1, M2, M3, M4) + assert m.rows == 0 and m.cols == 6 + + +def test_vstack(): + m = ShapingOnlyMatrix(4, 3, lambda i, j: i*3 + j) + m2 = ShapingOnlyMatrix(3, 4, lambda i, j: i*3 + j) + assert m == m.vstack(m) + assert m.vstack(m, m, m) == ShapingOnlyMatrix.vstack(m, m, m) == Matrix([ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [9, 10, 11], + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [9, 10, 11], + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [9, 10, 11]]) + raises(ShapeError, lambda: m.vstack(m, m2)) + assert Matrix.vstack() == Matrix() + + +# PropertiesOnlyMatrix tests +def test_atoms(): + m = PropertiesOnlyMatrix(2, 2, [1, 2, x, 1 - 1/x]) + assert m.atoms() == {S.One, S(2), S.NegativeOne, x} + assert m.atoms(Symbol) == {x} + + +def test_free_symbols(): + assert PropertiesOnlyMatrix([[x], [0]]).free_symbols == {x} + + +def test_has(): + A = PropertiesOnlyMatrix(((x, y), (2, 3))) + assert A.has(x) + assert not A.has(z) + assert A.has(Symbol) + + A = PropertiesOnlyMatrix(((2, y), (2, 3))) + assert not A.has(x) + + +def test_is_anti_symmetric(): + x = symbols('x') + assert PropertiesOnlyMatrix(2, 1, [1, 2]).is_anti_symmetric() is False + m = PropertiesOnlyMatrix(3, 3, [0, x**2 + 2*x + 1, y, -(x + 1)**2, 0, x*y, -y, -x*y, 0]) + assert m.is_anti_symmetric() is True + assert m.is_anti_symmetric(simplify=False) is False + assert m.is_anti_symmetric(simplify=lambda x: x) is False + + m = PropertiesOnlyMatrix(3, 3, [x.expand() for x in m]) + assert m.is_anti_symmetric(simplify=False) is True + m = PropertiesOnlyMatrix(3, 3, [x.expand() for x in [S.One] + list(m)[1:]]) + assert m.is_anti_symmetric() is False + + +def test_diagonal_symmetrical(): + m = PropertiesOnlyMatrix(2, 2, [0, 1, 1, 0]) + assert not m.is_diagonal() + assert m.is_symmetric() + assert m.is_symmetric(simplify=False) + + m = PropertiesOnlyMatrix(2, 2, [1, 0, 0, 1]) + assert m.is_diagonal() + + m = PropertiesOnlyMatrix(3, 3, diag(1, 2, 3)) + assert m.is_diagonal() + assert m.is_symmetric() + + m = PropertiesOnlyMatrix(3, 3, [1, 0, 0, 0, 2, 0, 0, 0, 3]) + assert m == diag(1, 2, 3) + + m = PropertiesOnlyMatrix(2, 3, zeros(2, 3)) + assert not m.is_symmetric() + assert m.is_diagonal() + + m = PropertiesOnlyMatrix(((5, 0), (0, 6), (0, 0))) + assert m.is_diagonal() + + m = PropertiesOnlyMatrix(((5, 0, 0), (0, 6, 0))) + assert m.is_diagonal() + + m = Matrix(3, 3, [1, x**2 + 2*x + 1, y, (x + 1)**2, 2, 0, y, 0, 3]) + assert m.is_symmetric() + assert not m.is_symmetric(simplify=False) + assert m.expand().is_symmetric(simplify=False) + + +def test_is_hermitian(): + a = PropertiesOnlyMatrix([[1, I], [-I, 1]]) + assert a.is_hermitian + a = PropertiesOnlyMatrix([[2*I, I], [-I, 1]]) + assert a.is_hermitian is False + a = PropertiesOnlyMatrix([[x, I], [-I, 1]]) + assert a.is_hermitian is None + a = PropertiesOnlyMatrix([[x, 1], [-I, 1]]) + assert a.is_hermitian is False + + +def test_is_Identity(): + assert eye_Properties(3).is_Identity + assert not PropertiesOnlyMatrix(zeros(3)).is_Identity + assert not PropertiesOnlyMatrix(ones(3)).is_Identity + # issue 6242 + assert not PropertiesOnlyMatrix([[1, 0, 0]]).is_Identity + + +def test_is_symbolic(): + a = PropertiesOnlyMatrix([[x, x], [x, x]]) + assert a.is_symbolic() is True + a = PropertiesOnlyMatrix([[1, 2, 3, 4], [5, 6, 7, 8]]) + assert a.is_symbolic() is False + a = PropertiesOnlyMatrix([[1, 2, 3, 4], [5, 6, x, 8]]) + assert a.is_symbolic() is True + a = PropertiesOnlyMatrix([[1, x, 3]]) + assert a.is_symbolic() is True + a = PropertiesOnlyMatrix([[1, 2, 3]]) + assert a.is_symbolic() is False + a = PropertiesOnlyMatrix([[1], [x], [3]]) + assert a.is_symbolic() is True + a = PropertiesOnlyMatrix([[1], [2], [3]]) + assert a.is_symbolic() is False + + +def test_is_upper(): + a = PropertiesOnlyMatrix([[1, 2, 3]]) + assert a.is_upper is True + a = PropertiesOnlyMatrix([[1], [2], [3]]) + assert a.is_upper is False + + +def test_is_lower(): + a = PropertiesOnlyMatrix([[1, 2, 3]]) + assert a.is_lower is False + a = PropertiesOnlyMatrix([[1], [2], [3]]) + assert a.is_lower is True + + +def test_is_square(): + m = PropertiesOnlyMatrix([[1], [1]]) + m2 = PropertiesOnlyMatrix([[2, 2], [2, 2]]) + assert not m.is_square + assert m2.is_square + + +def test_is_symmetric(): + m = PropertiesOnlyMatrix(2, 2, [0, 1, 1, 0]) + assert m.is_symmetric() + m = PropertiesOnlyMatrix(2, 2, [0, 1, 0, 1]) + assert not m.is_symmetric() + + +def test_is_hessenberg(): + A = PropertiesOnlyMatrix([[3, 4, 1], [2, 4, 5], [0, 1, 2]]) + assert A.is_upper_hessenberg + A = PropertiesOnlyMatrix(3, 3, [3, 2, 0, 4, 4, 1, 1, 5, 2]) + assert A.is_lower_hessenberg + A = PropertiesOnlyMatrix(3, 3, [3, 2, -1, 4, 4, 1, 1, 5, 2]) + assert A.is_lower_hessenberg is False + assert A.is_upper_hessenberg is False + + A = PropertiesOnlyMatrix([[3, 4, 1], [2, 4, 5], [3, 1, 2]]) + assert not A.is_upper_hessenberg + + +def test_is_zero(): + assert PropertiesOnlyMatrix(0, 0, []).is_zero_matrix + assert PropertiesOnlyMatrix([[0, 0], [0, 0]]).is_zero_matrix + assert PropertiesOnlyMatrix(zeros(3, 4)).is_zero_matrix + assert not PropertiesOnlyMatrix(eye(3)).is_zero_matrix + assert PropertiesOnlyMatrix([[x, 0], [0, 0]]).is_zero_matrix == None + assert PropertiesOnlyMatrix([[x, 1], [0, 0]]).is_zero_matrix == False + a = Symbol('a', nonzero=True) + assert PropertiesOnlyMatrix([[a, 0], [0, 0]]).is_zero_matrix == False + + +def test_values(): + assert set(PropertiesOnlyMatrix(2, 2, [0, 1, 2, 3] + ).values()) == {1, 2, 3} + x = Symbol('x', real=True) + assert set(PropertiesOnlyMatrix(2, 2, [x, 0, 0, 1] + ).values()) == {x, 1} + + +# OperationsOnlyMatrix tests +def test_applyfunc(): + m0 = OperationsOnlyMatrix(eye(3)) + assert m0.applyfunc(lambda x: 2*x) == eye(3)*2 + assert m0.applyfunc(lambda x: 0) == zeros(3) + assert m0.applyfunc(lambda x: 1) == ones(3) + + +def test_adjoint(): + dat = [[0, I], [1, 0]] + ans = OperationsOnlyMatrix([[0, 1], [-I, 0]]) + assert ans.adjoint() == Matrix(dat) + + +def test_as_real_imag(): + m1 = OperationsOnlyMatrix(2, 2, [1, 2, 3, 4]) + m3 = OperationsOnlyMatrix(2, 2, + [1 + S.ImaginaryUnit, 2 + 2*S.ImaginaryUnit, + 3 + 3*S.ImaginaryUnit, 4 + 4*S.ImaginaryUnit]) + + a, b = m3.as_real_imag() + assert a == m1 + assert b == m1 + + +def test_conjugate(): + M = OperationsOnlyMatrix([[0, I, 5], + [1, 2, 0]]) + + assert M.T == Matrix([[0, 1], + [I, 2], + [5, 0]]) + + assert M.C == Matrix([[0, -I, 5], + [1, 2, 0]]) + assert M.C == M.conjugate() + + assert M.H == M.T.C + assert M.H == Matrix([[ 0, 1], + [-I, 2], + [ 5, 0]]) + + +def test_doit(): + a = OperationsOnlyMatrix([[Add(x, x, evaluate=False)]]) + assert a[0] != 2*x + assert a.doit() == Matrix([[2*x]]) + + +def test_evalf(): + a = OperationsOnlyMatrix(2, 1, [sqrt(5), 6]) + assert all(a.evalf()[i] == a[i].evalf() for i in range(2)) + assert all(a.evalf(2)[i] == a[i].evalf(2) for i in range(2)) + assert all(a.n(2)[i] == a[i].n(2) for i in range(2)) + + +def test_expand(): + m0 = OperationsOnlyMatrix([[x*(x + y), 2], [((x + y)*y)*x, x*(y + x*(x + y))]]) + # Test if expand() returns a matrix + m1 = m0.expand() + assert m1 == Matrix( + [[x*y + x**2, 2], [x*y**2 + y*x**2, x*y + y*x**2 + x**3]]) + + a = Symbol('a', real=True) + + assert OperationsOnlyMatrix(1, 1, [exp(I*a)]).expand(complex=True) == \ + Matrix([cos(a) + I*sin(a)]) + + +def test_refine(): + m0 = OperationsOnlyMatrix([[Abs(x)**2, sqrt(x**2)], + [sqrt(x**2)*Abs(y)**2, sqrt(y**2)*Abs(x)**2]]) + m1 = m0.refine(Q.real(x) & Q.real(y)) + assert m1 == Matrix([[x**2, Abs(x)], [y**2*Abs(x), x**2*Abs(y)]]) + + m1 = m0.refine(Q.positive(x) & Q.positive(y)) + assert m1 == Matrix([[x**2, x], [x*y**2, x**2*y]]) + + m1 = m0.refine(Q.negative(x) & Q.negative(y)) + assert m1 == Matrix([[x**2, -x], [-x*y**2, -x**2*y]]) + + +def test_replace(): + F, G = symbols('F, G', cls=Function) + K = OperationsOnlyMatrix(2, 2, lambda i, j: G(i+j)) + M = OperationsOnlyMatrix(2, 2, lambda i, j: F(i+j)) + N = M.replace(F, G) + assert N == K + + +def test_replace_map(): + F, G = symbols('F, G', cls=Function) + K = OperationsOnlyMatrix(2, 2, [(G(0), {F(0): G(0)}), (G(1), {F(1): G(1)}), (G(1), {F(1) \ + : G(1)}), (G(2), {F(2): G(2)})]) + M = OperationsOnlyMatrix(2, 2, lambda i, j: F(i+j)) + N = M.replace(F, G, True) + assert N == K + + +def test_rot90(): + A = Matrix([[1, 2], [3, 4]]) + assert A == A.rot90(0) == A.rot90(4) + assert A.rot90(2) == A.rot90(-2) == A.rot90(6) == Matrix(((4, 3), (2, 1))) + assert A.rot90(3) == A.rot90(-1) == A.rot90(7) == Matrix(((2, 4), (1, 3))) + assert A.rot90() == A.rot90(-7) == A.rot90(-3) == Matrix(((3, 1), (4, 2))) + +def test_simplify(): + n = Symbol('n') + f = Function('f') + + M = OperationsOnlyMatrix([[ 1/x + 1/y, (x + x*y) / x ], + [ (f(x) + y*f(x))/f(x), 2 * (1/n - cos(n * pi)/n) / pi ]]) + assert M.simplify() == Matrix([[ (x + y)/(x * y), 1 + y ], + [ 1 + y, 2*((1 - 1*cos(pi*n))/(pi*n)) ]]) + eq = (1 + x)**2 + M = OperationsOnlyMatrix([[eq]]) + assert M.simplify() == Matrix([[eq]]) + assert M.simplify(ratio=oo) == Matrix([[eq.simplify(ratio=oo)]]) + + # https://github.com/sympy/sympy/issues/19353 + m = Matrix([[30, 2], [3, 4]]) + assert (1/(m.trace())).simplify() == Rational(1, 34) + + +def test_subs(): + assert OperationsOnlyMatrix([[1, x], [x, 4]]).subs(x, 5) == Matrix([[1, 5], [5, 4]]) + assert OperationsOnlyMatrix([[x, 2], [x + y, 4]]).subs([[x, -1], [y, -2]]) == \ + Matrix([[-1, 2], [-3, 4]]) + assert OperationsOnlyMatrix([[x, 2], [x + y, 4]]).subs([(x, -1), (y, -2)]) == \ + Matrix([[-1, 2], [-3, 4]]) + assert OperationsOnlyMatrix([[x, 2], [x + y, 4]]).subs({x: -1, y: -2}) == \ + Matrix([[-1, 2], [-3, 4]]) + assert OperationsOnlyMatrix([[x*y]]).subs({x: y - 1, y: x - 1}, simultaneous=True) == \ + Matrix([[(x - 1)*(y - 1)]]) + + +def test_trace(): + M = OperationsOnlyMatrix([[1, 0, 0], + [0, 5, 0], + [0, 0, 8]]) + assert M.trace() == 14 + + +def test_xreplace(): + assert OperationsOnlyMatrix([[1, x], [x, 4]]).xreplace({x: 5}) == \ + Matrix([[1, 5], [5, 4]]) + assert OperationsOnlyMatrix([[x, 2], [x + y, 4]]).xreplace({x: -1, y: -2}) == \ + Matrix([[-1, 2], [-3, 4]]) + + +def test_permute(): + a = OperationsOnlyMatrix(3, 4, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) + + raises(IndexError, lambda: a.permute([[0, 5]])) + raises(ValueError, lambda: a.permute(Symbol('x'))) + b = a.permute_rows([[0, 2], [0, 1]]) + assert a.permute([[0, 2], [0, 1]]) == b == Matrix([ + [5, 6, 7, 8], + [9, 10, 11, 12], + [1, 2, 3, 4]]) + + b = a.permute_cols([[0, 2], [0, 1]]) + assert a.permute([[0, 2], [0, 1]], orientation='cols') == b ==\ + Matrix([ + [ 2, 3, 1, 4], + [ 6, 7, 5, 8], + [10, 11, 9, 12]]) + + b = a.permute_cols([[0, 2], [0, 1]], direction='backward') + assert a.permute([[0, 2], [0, 1]], orientation='cols', direction='backward') == b ==\ + Matrix([ + [ 3, 1, 2, 4], + [ 7, 5, 6, 8], + [11, 9, 10, 12]]) + + assert a.permute([1, 2, 0, 3]) == Matrix([ + [5, 6, 7, 8], + [9, 10, 11, 12], + [1, 2, 3, 4]]) + + from sympy.combinatorics import Permutation + assert a.permute(Permutation([1, 2, 0, 3])) == Matrix([ + [5, 6, 7, 8], + [9, 10, 11, 12], + [1, 2, 3, 4]]) + +def test_upper_triangular(): + + A = OperationsOnlyMatrix([ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ]) + + R = A.upper_triangular(2) + assert R == OperationsOnlyMatrix([ + [0, 0, 1, 1], + [0, 0, 0, 1], + [0, 0, 0, 0], + [0, 0, 0, 0] + ]) + + R = A.upper_triangular(-2) + assert R == OperationsOnlyMatrix([ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [0, 1, 1, 1] + ]) + + R = A.upper_triangular() + assert R == OperationsOnlyMatrix([ + [1, 1, 1, 1], + [0, 1, 1, 1], + [0, 0, 1, 1], + [0, 0, 0, 1] + ]) + +def test_lower_triangular(): + A = OperationsOnlyMatrix([ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ]) + + L = A.lower_triangular() + assert L == ArithmeticOnlyMatrix([ + [1, 0, 0, 0], + [1, 1, 0, 0], + [1, 1, 1, 0], + [1, 1, 1, 1]]) + + L = A.lower_triangular(2) + assert L == ArithmeticOnlyMatrix([ + [1, 1, 1, 0], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ]) + + L = A.lower_triangular(-2) + assert L == ArithmeticOnlyMatrix([ + [0, 0, 0, 0], + [0, 0, 0, 0], + [1, 0, 0, 0], + [1, 1, 0, 0] + ]) + + +# ArithmeticOnlyMatrix tests +def test_abs(): + m = ArithmeticOnlyMatrix([[1, -2], [x, y]]) + assert abs(m) == ArithmeticOnlyMatrix([[1, 2], [Abs(x), Abs(y)]]) + + +def test_add(): + m = ArithmeticOnlyMatrix([[1, 2, 3], [x, y, x], [2*y, -50, z*x]]) + assert m + m == ArithmeticOnlyMatrix([[2, 4, 6], [2*x, 2*y, 2*x], [4*y, -100, 2*z*x]]) + n = ArithmeticOnlyMatrix(1, 2, [1, 2]) + raises(ShapeError, lambda: m + n) + + +def test_multiplication(): + a = ArithmeticOnlyMatrix(( + (1, 2), + (3, 1), + (0, 6), + )) + + b = ArithmeticOnlyMatrix(( + (1, 2), + (3, 0), + )) + + raises(ShapeError, lambda: b*a) + raises(TypeError, lambda: a*{}) + + c = a*b + assert c[0, 0] == 7 + assert c[0, 1] == 2 + assert c[1, 0] == 6 + assert c[1, 1] == 6 + assert c[2, 0] == 18 + assert c[2, 1] == 0 + + try: + eval('c = a @ b') + except SyntaxError: + pass + else: + assert c[0, 0] == 7 + assert c[0, 1] == 2 + assert c[1, 0] == 6 + assert c[1, 1] == 6 + assert c[2, 0] == 18 + assert c[2, 1] == 0 + + h = a.multiply_elementwise(c) + assert h == matrix_multiply_elementwise(a, c) + assert h[0, 0] == 7 + assert h[0, 1] == 4 + assert h[1, 0] == 18 + assert h[1, 1] == 6 + assert h[2, 0] == 0 + assert h[2, 1] == 0 + raises(ShapeError, lambda: a.multiply_elementwise(b)) + + c = b * Symbol("x") + assert isinstance(c, ArithmeticOnlyMatrix) + assert c[0, 0] == x + assert c[0, 1] == 2*x + assert c[1, 0] == 3*x + assert c[1, 1] == 0 + + c2 = x * b + assert c == c2 + + c = 5 * b + assert isinstance(c, ArithmeticOnlyMatrix) + assert c[0, 0] == 5 + assert c[0, 1] == 2*5 + assert c[1, 0] == 3*5 + assert c[1, 1] == 0 + + try: + eval('c = 5 @ b') + except SyntaxError: + pass + else: + assert isinstance(c, ArithmeticOnlyMatrix) + assert c[0, 0] == 5 + assert c[0, 1] == 2*5 + assert c[1, 0] == 3*5 + assert c[1, 1] == 0 + + # https://github.com/sympy/sympy/issues/22353 + A = Matrix(ones(3, 1)) + _h = -Rational(1, 2) + B = Matrix([_h, _h, _h]) + assert A.multiply_elementwise(B) == Matrix([ + [_h], + [_h], + [_h]]) + + +def test_matmul(): + a = Matrix([[1, 2], [3, 4]]) + + assert a.__matmul__(2) == NotImplemented + + assert a.__rmatmul__(2) == NotImplemented + + #This is done this way because @ is only supported in Python 3.5+ + #To check 2@a case + try: + eval('2 @ a') + except SyntaxError: + pass + except TypeError: #TypeError is raised in case of NotImplemented is returned + pass + + #Check a@2 case + try: + eval('a @ 2') + except SyntaxError: + pass + except TypeError: #TypeError is raised in case of NotImplemented is returned + pass + + +def test_non_matmul(): + """ + Test that if explicitly specified as non-matrix, mul reverts + to scalar multiplication. + """ + class foo(Expr): + is_Matrix=False + is_MatrixLike=False + shape = (1, 1) + + A = Matrix([[1, 2], [3, 4]]) + b = foo() + assert b*A == Matrix([[b, 2*b], [3*b, 4*b]]) + assert A*b == Matrix([[b, 2*b], [3*b, 4*b]]) + + +def test_power(): + raises(NonSquareMatrixError, lambda: Matrix((1, 2))**2) + + A = ArithmeticOnlyMatrix([[2, 3], [4, 5]]) + assert (A**5)[:] == (6140, 8097, 10796, 14237) + A = ArithmeticOnlyMatrix([[2, 1, 3], [4, 2, 4], [6, 12, 1]]) + assert (A**3)[:] == (290, 262, 251, 448, 440, 368, 702, 954, 433) + assert A**0 == eye(3) + assert A**1 == A + assert (ArithmeticOnlyMatrix([[2]]) ** 100)[0, 0] == 2**100 + assert ArithmeticOnlyMatrix([[1, 2], [3, 4]])**Integer(2) == ArithmeticOnlyMatrix([[7, 10], [15, 22]]) + A = Matrix([[1,2],[4,5]]) + assert A.pow(20, method='cayley') == A.pow(20, method='multiply') + +def test_neg(): + n = ArithmeticOnlyMatrix(1, 2, [1, 2]) + assert -n == ArithmeticOnlyMatrix(1, 2, [-1, -2]) + + +def test_sub(): + n = ArithmeticOnlyMatrix(1, 2, [1, 2]) + assert n - n == ArithmeticOnlyMatrix(1, 2, [0, 0]) + + +def test_div(): + n = ArithmeticOnlyMatrix(1, 2, [1, 2]) + assert n/2 == ArithmeticOnlyMatrix(1, 2, [S.Half, S(2)/2]) + +# SpecialOnlyMatrix tests +def test_eye(): + assert list(SpecialOnlyMatrix.eye(2, 2)) == [1, 0, 0, 1] + assert list(SpecialOnlyMatrix.eye(2)) == [1, 0, 0, 1] + assert type(SpecialOnlyMatrix.eye(2)) == SpecialOnlyMatrix + assert type(SpecialOnlyMatrix.eye(2, cls=Matrix)) == Matrix + + +def test_ones(): + assert list(SpecialOnlyMatrix.ones(2, 2)) == [1, 1, 1, 1] + assert list(SpecialOnlyMatrix.ones(2)) == [1, 1, 1, 1] + assert SpecialOnlyMatrix.ones(2, 3) == Matrix([[1, 1, 1], [1, 1, 1]]) + assert type(SpecialOnlyMatrix.ones(2)) == SpecialOnlyMatrix + assert type(SpecialOnlyMatrix.ones(2, cls=Matrix)) == Matrix + + +def test_zeros(): + assert list(SpecialOnlyMatrix.zeros(2, 2)) == [0, 0, 0, 0] + assert list(SpecialOnlyMatrix.zeros(2)) == [0, 0, 0, 0] + assert SpecialOnlyMatrix.zeros(2, 3) == Matrix([[0, 0, 0], [0, 0, 0]]) + assert type(SpecialOnlyMatrix.zeros(2)) == SpecialOnlyMatrix + assert type(SpecialOnlyMatrix.zeros(2, cls=Matrix)) == Matrix + + +def test_diag_make(): + diag = SpecialOnlyMatrix.diag + a = Matrix([[1, 2], [2, 3]]) + b = Matrix([[3, x], [y, 3]]) + c = Matrix([[3, x, 3], [y, 3, z], [x, y, z]]) + assert diag(a, b, b) == Matrix([ + [1, 2, 0, 0, 0, 0], + [2, 3, 0, 0, 0, 0], + [0, 0, 3, x, 0, 0], + [0, 0, y, 3, 0, 0], + [0, 0, 0, 0, 3, x], + [0, 0, 0, 0, y, 3], + ]) + assert diag(a, b, c) == Matrix([ + [1, 2, 0, 0, 0, 0, 0], + [2, 3, 0, 0, 0, 0, 0], + [0, 0, 3, x, 0, 0, 0], + [0, 0, y, 3, 0, 0, 0], + [0, 0, 0, 0, 3, x, 3], + [0, 0, 0, 0, y, 3, z], + [0, 0, 0, 0, x, y, z], + ]) + assert diag(a, c, b) == Matrix([ + [1, 2, 0, 0, 0, 0, 0], + [2, 3, 0, 0, 0, 0, 0], + [0, 0, 3, x, 3, 0, 0], + [0, 0, y, 3, z, 0, 0], + [0, 0, x, y, z, 0, 0], + [0, 0, 0, 0, 0, 3, x], + [0, 0, 0, 0, 0, y, 3], + ]) + a = Matrix([x, y, z]) + b = Matrix([[1, 2], [3, 4]]) + c = Matrix([[5, 6]]) + # this "wandering diagonal" is what makes this + # a block diagonal where each block is independent + # of the others + assert diag(a, 7, b, c) == Matrix([ + [x, 0, 0, 0, 0, 0], + [y, 0, 0, 0, 0, 0], + [z, 0, 0, 0, 0, 0], + [0, 7, 0, 0, 0, 0], + [0, 0, 1, 2, 0, 0], + [0, 0, 3, 4, 0, 0], + [0, 0, 0, 0, 5, 6]]) + raises(ValueError, lambda: diag(a, 7, b, c, rows=5)) + assert diag(1) == Matrix([[1]]) + assert diag(1, rows=2) == Matrix([[1, 0], [0, 0]]) + assert diag(1, cols=2) == Matrix([[1, 0], [0, 0]]) + assert diag(1, rows=3, cols=2) == Matrix([[1, 0], [0, 0], [0, 0]]) + assert diag(*[2, 3]) == Matrix([ + [2, 0], + [0, 3]]) + assert diag(Matrix([2, 3])) == Matrix([ + [2], + [3]]) + assert diag([1, [2, 3], 4], unpack=False) == \ + diag([[1], [2, 3], [4]], unpack=False) == Matrix([ + [1, 0], + [2, 3], + [4, 0]]) + assert type(diag(1)) == SpecialOnlyMatrix + assert type(diag(1, cls=Matrix)) == Matrix + assert Matrix.diag([1, 2, 3]) == Matrix.diag(1, 2, 3) + assert Matrix.diag([1, 2, 3], unpack=False).shape == (3, 1) + assert Matrix.diag([[1, 2, 3]]).shape == (3, 1) + assert Matrix.diag([[1, 2, 3]], unpack=False).shape == (1, 3) + assert Matrix.diag([[[1, 2, 3]]]).shape == (1, 3) + # kerning can be used to move the starting point + assert Matrix.diag(ones(0, 2), 1, 2) == Matrix([ + [0, 0, 1, 0], + [0, 0, 0, 2]]) + assert Matrix.diag(ones(2, 0), 1, 2) == Matrix([ + [0, 0], + [0, 0], + [1, 0], + [0, 2]]) + + +def test_diagonal(): + m = Matrix(3, 3, range(9)) + d = m.diagonal() + assert d == m.diagonal(0) + assert tuple(d) == (0, 4, 8) + assert tuple(m.diagonal(1)) == (1, 5) + assert tuple(m.diagonal(-1)) == (3, 7) + assert tuple(m.diagonal(2)) == (2,) + assert type(m.diagonal()) == type(m) + s = SparseMatrix(3, 3, {(1, 1): 1}) + assert type(s.diagonal()) == type(s) + assert type(m) != type(s) + raises(ValueError, lambda: m.diagonal(3)) + raises(ValueError, lambda: m.diagonal(-3)) + raises(ValueError, lambda: m.diagonal(pi)) + M = ones(2, 3) + assert banded({i: list(M.diagonal(i)) + for i in range(1-M.rows, M.cols)}) == M + + +def test_jordan_block(): + assert SpecialOnlyMatrix.jordan_block(3, 2) == SpecialOnlyMatrix.jordan_block(3, eigenvalue=2) \ + == SpecialOnlyMatrix.jordan_block(size=3, eigenvalue=2) \ + == SpecialOnlyMatrix.jordan_block(3, 2, band='upper') \ + == SpecialOnlyMatrix.jordan_block( + size=3, eigenval=2, eigenvalue=2) \ + == Matrix([ + [2, 1, 0], + [0, 2, 1], + [0, 0, 2]]) + + assert SpecialOnlyMatrix.jordan_block(3, 2, band='lower') == Matrix([ + [2, 0, 0], + [1, 2, 0], + [0, 1, 2]]) + # missing eigenvalue + raises(ValueError, lambda: SpecialOnlyMatrix.jordan_block(2)) + # non-integral size + raises(ValueError, lambda: SpecialOnlyMatrix.jordan_block(3.5, 2)) + # size not specified + raises(ValueError, lambda: SpecialOnlyMatrix.jordan_block(eigenvalue=2)) + # inconsistent eigenvalue + raises(ValueError, + lambda: SpecialOnlyMatrix.jordan_block( + eigenvalue=2, eigenval=4)) + + # Using alias keyword + assert SpecialOnlyMatrix.jordan_block(size=3, eigenvalue=2) == \ + SpecialOnlyMatrix.jordan_block(size=3, eigenval=2) + + +def test_orthogonalize(): + m = Matrix([[1, 2], [3, 4]]) + assert m.orthogonalize(Matrix([[2], [1]])) == [Matrix([[2], [1]])] + assert m.orthogonalize(Matrix([[2], [1]]), normalize=True) == \ + [Matrix([[2*sqrt(5)/5], [sqrt(5)/5]])] + assert m.orthogonalize(Matrix([[1], [2]]), Matrix([[-1], [4]])) == \ + [Matrix([[1], [2]]), Matrix([[Rational(-12, 5)], [Rational(6, 5)]])] + assert m.orthogonalize(Matrix([[0], [0]]), Matrix([[-1], [4]])) == \ + [Matrix([[-1], [4]])] + assert m.orthogonalize(Matrix([[0], [0]])) == [] + + n = Matrix([[9, 1, 9], [3, 6, 10], [8, 5, 2]]) + vecs = [Matrix([[-5], [1]]), Matrix([[-5], [2]]), Matrix([[-5], [-2]])] + assert n.orthogonalize(*vecs) == \ + [Matrix([[-5], [1]]), Matrix([[Rational(5, 26)], [Rational(25, 26)]])] + + vecs = [Matrix([0, 0, 0]), Matrix([1, 2, 3]), Matrix([1, 4, 5])] + raises(ValueError, lambda: Matrix.orthogonalize(*vecs, rankcheck=True)) + + vecs = [Matrix([1, 2, 3]), Matrix([4, 5, 6]), Matrix([7, 8, 9])] + raises(ValueError, lambda: Matrix.orthogonalize(*vecs, rankcheck=True)) + +def test_wilkinson(): + + wminus, wplus = Matrix.wilkinson(1) + assert wminus == Matrix([ + [-1, 1, 0], + [1, 0, 1], + [0, 1, 1]]) + assert wplus == Matrix([ + [1, 1, 0], + [1, 0, 1], + [0, 1, 1]]) + + wminus, wplus = Matrix.wilkinson(3) + assert wminus == Matrix([ + [-3, 1, 0, 0, 0, 0, 0], + [1, -2, 1, 0, 0, 0, 0], + [0, 1, -1, 1, 0, 0, 0], + [0, 0, 1, 0, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 0, 1, 2, 1], + + [0, 0, 0, 0, 0, 1, 3]]) + + assert wplus == Matrix([ + [3, 1, 0, 0, 0, 0, 0], + [1, 2, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 0, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 0, 1, 2, 1], + [0, 0, 0, 0, 0, 1, 3]]) + + +# CalculusOnlyMatrix tests +@XFAIL +def test_diff(): + x, y = symbols('x y') + m = CalculusOnlyMatrix(2, 1, [x, y]) + # TODO: currently not working as ``_MinimalMatrix`` cannot be sympified: + assert m.diff(x) == Matrix(2, 1, [1, 0]) + + +def test_integrate(): + x, y = symbols('x y') + m = CalculusOnlyMatrix(2, 1, [x, y]) + assert m.integrate(x) == Matrix(2, 1, [x**2/2, y*x]) + + +def test_jacobian2(): + rho, phi = symbols("rho,phi") + X = CalculusOnlyMatrix(3, 1, [rho*cos(phi), rho*sin(phi), rho**2]) + Y = CalculusOnlyMatrix(2, 1, [rho, phi]) + J = Matrix([ + [cos(phi), -rho*sin(phi)], + [sin(phi), rho*cos(phi)], + [ 2*rho, 0], + ]) + assert X.jacobian(Y) == J + + m = CalculusOnlyMatrix(2, 2, [1, 2, 3, 4]) + m2 = CalculusOnlyMatrix(4, 1, [1, 2, 3, 4]) + raises(TypeError, lambda: m.jacobian(Matrix([1, 2]))) + raises(TypeError, lambda: m2.jacobian(m)) + + +def test_limit(): + x, y = symbols('x y') + m = CalculusOnlyMatrix(2, 1, [1/x, y]) + assert m.limit(x, 5) == Matrix(2, 1, [Rational(1, 5), y]) + + +def test_issue_13774(): + M = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + v = [1, 1, 1] + raises(TypeError, lambda: M*v) + raises(TypeError, lambda: v*M) + +def test_companion(): + x = Symbol('x') + y = Symbol('y') + raises(ValueError, lambda: Matrix.companion(1)) + raises(ValueError, lambda: Matrix.companion(Poly([1], x))) + raises(ValueError, lambda: Matrix.companion(Poly([2, 1], x))) + raises(ValueError, lambda: Matrix.companion(Poly(x*y, [x, y]))) + + c0, c1, c2 = symbols('c0:3') + assert Matrix.companion(Poly([1, c0], x)) == Matrix([-c0]) + assert Matrix.companion(Poly([1, c1, c0], x)) == \ + Matrix([[0, -c0], [1, -c1]]) + assert Matrix.companion(Poly([1, c2, c1, c0], x)) == \ + Matrix([[0, 0, -c0], [1, 0, -c1], [0, 1, -c2]]) + +def test_issue_10589(): + x, y, z = symbols("x, y z") + M1 = Matrix([x, y, z]) + M1 = M1.subs(zip([x, y, z], [1, 2, 3])) + assert M1 == Matrix([[1], [2], [3]]) + + M2 = Matrix([[x, x, x, x, x], [x, x, x, x, x], [x, x, x, x, x]]) + M2 = M2.subs(zip([x], [1])) + assert M2 == Matrix([[1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1]]) + +def test_rmul_pr19860(): + class Foo(ImmutableDenseMatrix): + _op_priority = MutableDenseMatrix._op_priority + 0.01 + + a = Matrix(2, 2, [1, 2, 3, 4]) + b = Foo(2, 2, [1, 2, 3, 4]) + + # This would throw a RecursionError: maximum recursion depth + # since b always has higher priority even after a.as_mutable() + c = a*b + + assert isinstance(c, Foo) + assert c == Matrix([[7, 10], [15, 22]]) + + +def test_issue_18956(): + A = Array([[1, 2], [3, 4]]) + B = Matrix([[1,2],[3,4]]) + raises(TypeError, lambda: B + A) + raises(TypeError, lambda: A + B) + + +def test__eq__(): + class My(object): + def __iter__(self): + yield 1 + yield 2 + return + def __getitem__(self, i): + return list(self)[i] + a = Matrix(2, 1, [1, 2]) + assert a != My() + class My_sympy(My): + def _sympy_(self): + return Matrix(self) + assert a == My_sympy() diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_decompositions.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_decompositions.py new file mode 100644 index 0000000000000000000000000000000000000000..d169ec3a8846fed786981e62d932fd860b6d4951 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_decompositions.py @@ -0,0 +1,474 @@ +from sympy.core.function import expand_mul +from sympy.core.numbers import I, Rational +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.complexes import Abs +from sympy.simplify.simplify import simplify +from sympy.matrices.exceptions import NonSquareMatrixError +from sympy.matrices import Matrix, zeros, eye, SparseMatrix +from sympy.abc import x, y, z +from sympy.testing.pytest import raises, slow +from sympy.testing.matrices import allclose + + +def test_LUdecomp(): + testmat = Matrix([[0, 2, 5, 3], + [3, 3, 7, 4], + [8, 4, 0, 2], + [-2, 6, 3, 4]]) + L, U, p = testmat.LUdecomposition() + assert L.is_lower + assert U.is_upper + assert (L*U).permute_rows(p, 'backward') - testmat == zeros(4) + + testmat = Matrix([[6, -2, 7, 4], + [0, 3, 6, 7], + [1, -2, 7, 4], + [-9, 2, 6, 3]]) + L, U, p = testmat.LUdecomposition() + assert L.is_lower + assert U.is_upper + assert (L*U).permute_rows(p, 'backward') - testmat == zeros(4) + + # non-square + testmat = Matrix([[1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 11, 12]]) + L, U, p = testmat.LUdecomposition(rankcheck=False) + assert L.is_lower + assert U.is_upper + assert (L*U).permute_rows(p, 'backward') - testmat == zeros(4, 3) + + # square and singular + testmat = Matrix([[1, 2, 3], + [2, 4, 6], + [4, 5, 6]]) + L, U, p = testmat.LUdecomposition(rankcheck=False) + assert L.is_lower + assert U.is_upper + assert (L*U).permute_rows(p, 'backward') - testmat == zeros(3) + + M = Matrix(((1, x, 1), (2, y, 0), (y, 0, z))) + L, U, p = M.LUdecomposition() + assert L.is_lower + assert U.is_upper + assert (L*U).permute_rows(p, 'backward') - M == zeros(3) + + mL = Matrix(( + (1, 0, 0), + (2, 3, 0), + )) + assert mL.is_lower is True + assert mL.is_upper is False + mU = Matrix(( + (1, 2, 3), + (0, 4, 5), + )) + assert mU.is_lower is False + assert mU.is_upper is True + + # test FF LUdecomp + M = Matrix([[1, 3, 3], + [3, 2, 6], + [3, 2, 2]]) + P, L, Dee, U = M.LUdecompositionFF() + assert P*M == L*Dee.inv()*U + + M = Matrix([[1, 2, 3, 4], + [3, -1, 2, 3], + [3, 1, 3, -2], + [6, -1, 0, 2]]) + P, L, Dee, U = M.LUdecompositionFF() + assert P*M == L*Dee.inv()*U + + M = Matrix([[0, 0, 1], + [2, 3, 0], + [3, 1, 4]]) + P, L, Dee, U = M.LUdecompositionFF() + assert P*M == L*Dee.inv()*U + + # issue 15794 + M = Matrix( + [[1, 2, 3], + [4, 5, 6], + [7, 8, 9]] + ) + raises(ValueError, lambda : M.LUdecomposition_Simple(rankcheck=True)) + +def test_singular_value_decompositionD(): + A = Matrix([[1, 2], [2, 1]]) + U, S, V = A.singular_value_decomposition() + assert U * S * V.T == A + assert U.T * U == eye(U.cols) + assert V.T * V == eye(V.cols) + + B = Matrix([[1, 2]]) + U, S, V = B.singular_value_decomposition() + + assert U * S * V.T == B + assert U.T * U == eye(U.cols) + assert V.T * V == eye(V.cols) + + C = Matrix([ + [1, 0, 0, 0, 2], + [0, 0, 3, 0, 0], + [0, 0, 0, 0, 0], + [0, 2, 0, 0, 0], + ]) + + U, S, V = C.singular_value_decomposition() + + assert U * S * V.T == C + assert U.T * U == eye(U.cols) + assert V.T * V == eye(V.cols) + + D = Matrix([[Rational(1, 3), sqrt(2)], [0, Rational(1, 4)]]) + U, S, V = D.singular_value_decomposition() + assert simplify(U.T * U) == eye(U.cols) + assert simplify(V.T * V) == eye(V.cols) + assert simplify(U * S * V.T) == D + + +def test_QR(): + A = Matrix([[1, 2], [2, 3]]) + Q, S = A.QRdecomposition() + R = Rational + assert Q == Matrix([ + [ 5**R(-1, 2), (R(2)/5)*(R(1)/5)**R(-1, 2)], + [2*5**R(-1, 2), (-R(1)/5)*(R(1)/5)**R(-1, 2)]]) + assert S == Matrix([[5**R(1, 2), 8*5**R(-1, 2)], [0, (R(1)/5)**R(1, 2)]]) + assert Q*S == A + assert Q.T * Q == eye(2) + + A = Matrix([[1, 1, 1], [1, 1, 3], [2, 3, 4]]) + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + A = Matrix([[12, 0, -51], [6, 0, 167], [-4, 0, 24]]) + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + x = Symbol('x') + A = Matrix([x]) + Q, R = A.QRdecomposition() + assert Q == Matrix([x / Abs(x)]) + assert R == Matrix([Abs(x)]) + + A = Matrix([[x, 0], [0, x]]) + Q, R = A.QRdecomposition() + assert Q == x / Abs(x) * Matrix([[1, 0], [0, 1]]) + assert R == Abs(x) * Matrix([[1, 0], [0, 1]]) + + +def test_QR_non_square(): + # Narrow (cols < rows) matrices + A = Matrix([[9, 0, 26], [12, 0, -7], [0, 4, 4], [0, -3, -3]]) + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + A = Matrix([[1, -1, 4], [1, 4, -2], [1, 4, 2], [1, -1, 0]]) + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + A = Matrix(2, 1, [1, 2]) + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + # Wide (cols > rows) matrices + A = Matrix([[1, 2, 3], [4, 5, 6]]) + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + A = Matrix([[1, 2, 3, 4], [1, 4, 9, 16], [1, 8, 27, 64]]) + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + A = Matrix(1, 2, [1, 2]) + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + +def test_QR_trivial(): + # Rank deficient matrices + A = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + A = Matrix([[1, 1, 1], [2, 2, 2], [3, 3, 3], [4, 4, 4]]) + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + A = Matrix([[1, 1, 1], [2, 2, 2], [3, 3, 3], [4, 4, 4]]).T + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + # Zero rank matrices + A = Matrix([[0, 0, 0]]) + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + A = Matrix([[0, 0, 0]]).T + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + A = Matrix([[0, 0, 0], [0, 0, 0]]) + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + A = Matrix([[0, 0, 0], [0, 0, 0]]).T + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + # Rank deficient matrices with zero norm from beginning columns + A = Matrix([[0, 0, 0], [1, 2, 3]]).T + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + A = Matrix([[0, 0, 0, 0], [1, 2, 3, 4], [0, 0, 0, 0]]).T + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + A = Matrix([[0, 0, 0, 0], [1, 2, 3, 4], [0, 0, 0, 0], [2, 4, 6, 8]]).T + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + A = Matrix([[0, 0, 0], [0, 0, 0], [0, 0, 0], [1, 2, 3]]).T + Q, R = A.QRdecomposition() + assert Q.T * Q == eye(Q.cols) + assert R.is_upper + assert A == Q*R + + +def test_QR_float(): + A = Matrix([[1, 1], [1, 1.01]]) + Q, R = A.QRdecomposition() + assert allclose(Q * R, A) + assert allclose(Q * Q.T, Matrix.eye(2)) + assert allclose(Q.T * Q, Matrix.eye(2)) + + A = Matrix([[1, 1], [1, 1.001]]) + Q, R = A.QRdecomposition() + assert allclose(Q * R, A) + assert allclose(Q * Q.T, Matrix.eye(2)) + assert allclose(Q.T * Q, Matrix.eye(2)) + + +def test_LUdecomposition_Simple_iszerofunc(): + # Test if callable passed to matrices.LUdecomposition_Simple() as iszerofunc keyword argument is used inside + # matrices.LUdecomposition_Simple() + magic_string = "I got passed in!" + def goofyiszero(value): + raise ValueError(magic_string) + + try: + lu, p = Matrix([[1, 0], [0, 1]]).LUdecomposition_Simple(iszerofunc=goofyiszero) + except ValueError as err: + assert magic_string == err.args[0] + return + + assert False + +def test_LUdecomposition_iszerofunc(): + # Test if callable passed to matrices.LUdecomposition() as iszerofunc keyword argument is used inside + # matrices.LUdecomposition_Simple() + magic_string = "I got passed in!" + def goofyiszero(value): + raise ValueError(magic_string) + + try: + l, u, p = Matrix([[1, 0], [0, 1]]).LUdecomposition(iszerofunc=goofyiszero) + except ValueError as err: + assert magic_string == err.args[0] + return + + assert False + +def test_LDLdecomposition(): + raises(NonSquareMatrixError, lambda: Matrix((1, 2)).LDLdecomposition()) + raises(ValueError, lambda: Matrix(((1, 2), (3, 4))).LDLdecomposition()) + raises(ValueError, lambda: Matrix(((5 + I, 0), (0, 1))).LDLdecomposition()) + raises(ValueError, lambda: Matrix(((1, 5), (5, 1))).LDLdecomposition()) + raises(ValueError, lambda: Matrix(((1, 2), (3, 4))).LDLdecomposition(hermitian=False)) + A = Matrix(((1, 5), (5, 1))) + L, D = A.LDLdecomposition(hermitian=False) + assert L * D * L.T == A + A = Matrix(((25, 15, -5), (15, 18, 0), (-5, 0, 11))) + L, D = A.LDLdecomposition() + assert L * D * L.T == A + assert L.is_lower + assert L == Matrix([[1, 0, 0], [ Rational(3, 5), 1, 0], [Rational(-1, 5), Rational(1, 3), 1]]) + assert D.is_diagonal() + assert D == Matrix([[25, 0, 0], [0, 9, 0], [0, 0, 9]]) + A = Matrix(((4, -2*I, 2 + 2*I), (2*I, 2, -1 + I), (2 - 2*I, -1 - I, 11))) + L, D = A.LDLdecomposition() + assert expand_mul(L * D * L.H) == A + assert L.expand() == Matrix([[1, 0, 0], [I/2, 1, 0], [S.Half - I/2, 0, 1]]) + assert D.expand() == Matrix(((4, 0, 0), (0, 1, 0), (0, 0, 9))) + + raises(NonSquareMatrixError, lambda: SparseMatrix((1, 2)).LDLdecomposition()) + raises(ValueError, lambda: SparseMatrix(((1, 2), (3, 4))).LDLdecomposition()) + raises(ValueError, lambda: SparseMatrix(((5 + I, 0), (0, 1))).LDLdecomposition()) + raises(ValueError, lambda: SparseMatrix(((1, 5), (5, 1))).LDLdecomposition()) + raises(ValueError, lambda: SparseMatrix(((1, 2), (3, 4))).LDLdecomposition(hermitian=False)) + A = SparseMatrix(((1, 5), (5, 1))) + L, D = A.LDLdecomposition(hermitian=False) + assert L * D * L.T == A + A = SparseMatrix(((25, 15, -5), (15, 18, 0), (-5, 0, 11))) + L, D = A.LDLdecomposition() + assert L * D * L.T == A + assert L.is_lower + assert L == Matrix([[1, 0, 0], [ Rational(3, 5), 1, 0], [Rational(-1, 5), Rational(1, 3), 1]]) + assert D.is_diagonal() + assert D == Matrix([[25, 0, 0], [0, 9, 0], [0, 0, 9]]) + A = SparseMatrix(((4, -2*I, 2 + 2*I), (2*I, 2, -1 + I), (2 - 2*I, -1 - I, 11))) + L, D = A.LDLdecomposition() + assert expand_mul(L * D * L.H) == A + assert L == Matrix(((1, 0, 0), (I/2, 1, 0), (S.Half - I/2, 0, 1))) + assert D == Matrix(((4, 0, 0), (0, 1, 0), (0, 0, 9))) + +def test_pinv_succeeds_with_rank_decomposition_method(): + # Test rank decomposition method of pseudoinverse succeeding + As = [Matrix([ + [61, 89, 55, 20, 71, 0], + [62, 96, 85, 85, 16, 0], + [69, 56, 17, 4, 54, 0], + [10, 54, 91, 41, 71, 0], + [ 7, 30, 10, 48, 90, 0], + [0,0,0,0,0,0]])] + for A in As: + A_pinv = A.pinv(method="RD") + AAp = A * A_pinv + ApA = A_pinv * A + assert simplify(AAp * A) == A + assert simplify(ApA * A_pinv) == A_pinv + assert AAp.H == AAp + assert ApA.H == ApA + +def test_rank_decomposition(): + a = Matrix(0, 0, []) + c, f = a.rank_decomposition() + assert f.is_echelon + assert c.cols == f.rows == a.rank() + assert c * f == a + + a = Matrix(1, 1, [5]) + c, f = a.rank_decomposition() + assert f.is_echelon + assert c.cols == f.rows == a.rank() + assert c * f == a + + a = Matrix(3, 3, [1, 2, 3, 1, 2, 3, 1, 2, 3]) + c, f = a.rank_decomposition() + assert f.is_echelon + assert c.cols == f.rows == a.rank() + assert c * f == a + + a = Matrix([ + [0, 0, 1, 2, 2, -5, 3], + [-1, 5, 2, 2, 1, -7, 5], + [0, 0, -2, -3, -3, 8, -5], + [-1, 5, 0, -1, -2, 1, 0]]) + c, f = a.rank_decomposition() + assert f.is_echelon + assert c.cols == f.rows == a.rank() + assert c * f == a + + +@slow +def test_upper_hessenberg_decomposition(): + A = Matrix([ + [1, 0, sqrt(3)], + [sqrt(2), Rational(1, 2), 2], + [1, Rational(1, 4), 3], + ]) + H, P = A.upper_hessenberg_decomposition() + assert simplify(P * P.H) == eye(P.cols) + assert simplify(P.H * P) == eye(P.cols) + assert H.is_upper_hessenberg + assert (simplify(P * H * P.H)) == A + + + B = Matrix([ + [1, 2, 10], + [8, 2, 5], + [3, 12, 34], + ]) + H, P = B.upper_hessenberg_decomposition() + assert simplify(P * P.H) == eye(P.cols) + assert simplify(P.H * P) == eye(P.cols) + assert H.is_upper_hessenberg + assert simplify(P * H * P.H) == B + + C = Matrix([ + [1, sqrt(2), 2, 3], + [0, 5, 3, 4], + [1, 1, 4, sqrt(5)], + [0, 2, 2, 3] + ]) + + H, P = C.upper_hessenberg_decomposition() + assert simplify(P * P.H) == eye(P.cols) + assert simplify(P.H * P) == eye(P.cols) + assert H.is_upper_hessenberg + assert simplify(P * H * P.H) == C + + D = Matrix([ + [1, 2, 3], + [-3, 5, 6], + [4, -8, 9], + ]) + H, P = D.upper_hessenberg_decomposition() + assert simplify(P * P.H) == eye(P.cols) + assert simplify(P.H * P) == eye(P.cols) + assert H.is_upper_hessenberg + assert simplify(P * H * P.H) == D + + E = Matrix([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [1, 1, 0, 1], + [1, 1, 1, 0] + ]) + + H, P = E.upper_hessenberg_decomposition() + assert simplify(P * P.H) == eye(P.cols) + assert simplify(P.H * P) == eye(P.cols) + assert H.is_upper_hessenberg + assert simplify(P * H * P.H) == E diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_determinant.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_determinant.py new file mode 100644 index 0000000000000000000000000000000000000000..82b42ccf67efa4757bf270782bdf1d65e0efa306 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_determinant.py @@ -0,0 +1,280 @@ +import random +import pytest +from sympy.core.numbers import I +from sympy.core.numbers import Rational +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.polys.polytools import Poly +from sympy.matrices import Matrix, eye, ones +from sympy.abc import x, y, z +from sympy.testing.pytest import raises +from sympy.matrices.exceptions import NonSquareMatrixError +from sympy.functions.combinatorial.factorials import factorial, subfactorial + + +@pytest.mark.parametrize("method", [ + # Evaluating these directly because they are never reached via M.det() + Matrix._eval_det_bareiss, Matrix._eval_det_berkowitz, + Matrix._eval_det_bird, Matrix._eval_det_laplace, Matrix._eval_det_lu +]) +@pytest.mark.parametrize("M, sol", [ + (Matrix(), 1), + (Matrix([[0]]), 0), + (Matrix([[5]]), 5), +]) +def test_eval_determinant(method, M, sol): + assert method(M) == sol + + +@pytest.mark.parametrize("method", [ + "domain-ge", "bareiss", "berkowitz", "bird", "laplace", "lu"]) +@pytest.mark.parametrize("M, sol", [ + (Matrix(( (-3, 2), + ( 8, -5) )), -1), + (Matrix(( (x, 1), + (y, 2*y) )), 2*x*y - y), + (Matrix(( (1, 1, 1), + (1, 2, 3), + (1, 3, 6) )), 1), + (Matrix(( ( 3, -2, 0, 5), + (-2, 1, -2, 2), + ( 0, -2, 5, 0), + ( 5, 0, 3, 4) )), -289), + (Matrix(( ( 1, 2, 3, 4), + ( 5, 6, 7, 8), + ( 9, 10, 11, 12), + (13, 14, 15, 16) )), 0), + (Matrix(( (3, 2, 0, 0, 0), + (0, 3, 2, 0, 0), + (0, 0, 3, 2, 0), + (0, 0, 0, 3, 2), + (2, 0, 0, 0, 3) )), 275), + (Matrix(( ( 3, 0, 0, 0), + (-2, 1, 0, 0), + ( 0, -2, 5, 0), + ( 5, 0, 3, 4) )), 60), + (Matrix(( ( 1, 0, 0, 0), + ( 5, 0, 0, 0), + ( 9, 10, 11, 0), + (13, 14, 15, 16) )), 0), + (Matrix(( (3, 2, 0, 0, 0), + (0, 3, 2, 0, 0), + (0, 0, 3, 2, 0), + (0, 0, 0, 3, 2), + (0, 0, 0, 0, 3) )), 243), + (Matrix(( (1, 0, 1, 2, 12), + (2, 0, 1, 1, 4), + (2, 1, 1, -1, 3), + (3, 2, -1, 1, 8), + (1, 1, 1, 0, 6) )), -55), + (Matrix(( (-5, 2, 3, 4, 5), + ( 1, -4, 3, 4, 5), + ( 1, 2, -3, 4, 5), + ( 1, 2, 3, -2, 5), + ( 1, 2, 3, 4, -1) )), 11664), + (Matrix(( ( 2, 7, -1, 3, 2), + ( 0, 0, 1, 0, 1), + (-2, 0, 7, 0, 2), + (-3, -2, 4, 5, 3), + ( 1, 0, 0, 0, 1) )), 123), + (Matrix(( (x, y, z), + (1, 0, 0), + (y, z, x) )), z**2 - x*y), +]) +def test_determinant(method, M, sol): + assert M.det(method=method) == sol + + +def test_issue_13835(): + a = symbols('a') + M = lambda n: Matrix([[i + a*j for i in range(n)] + for j in range(n)]) + assert M(5).det() == 0 + assert M(6).det() == 0 + assert M(7).det() == 0 + + +def test_issue_14517(): + M = Matrix([ + [ 0, 10*I, 10*I, 0], + [10*I, 0, 0, 10*I], + [10*I, 0, 5 + 2*I, 10*I], + [ 0, 10*I, 10*I, 5 + 2*I]]) + ev = M.eigenvals() + # test one random eigenvalue, the computation is a little slow + test_ev = random.choice(list(ev.keys())) + assert (M - test_ev*eye(4)).det() == 0 + + +@pytest.mark.parametrize("method", [ + "bareis", "det_lu", "det_LU", "Bareis", "BAREISS", "BERKOWITZ", "LU"]) +@pytest.mark.parametrize("M, sol", [ + (Matrix(( ( 3, -2, 0, 5), + (-2, 1, -2, 2), + ( 0, -2, 5, 0), + ( 5, 0, 3, 4) )), -289), + (Matrix(( (-5, 2, 3, 4, 5), + ( 1, -4, 3, 4, 5), + ( 1, 2, -3, 4, 5), + ( 1, 2, 3, -2, 5), + ( 1, 2, 3, 4, -1) )), 11664), +]) +def test_legacy_det(method, M, sol): + # Minimal support for legacy keys for 'method' in det() + # Partially copied from test_determinant() + assert M.det(method=method) == sol + + +def eye_Determinant(n): + return Matrix(n, n, lambda i, j: int(i == j)) + +def zeros_Determinant(n): + return Matrix(n, n, lambda i, j: 0) + +def test_det(): + a = Matrix(2, 3, [1, 2, 3, 4, 5, 6]) + raises(NonSquareMatrixError, lambda: a.det()) + + z = zeros_Determinant(2) + ey = eye_Determinant(2) + assert z.det() == 0 + assert ey.det() == 1 + + x = Symbol('x') + a = Matrix(0, 0, []) + b = Matrix(1, 1, [5]) + c = Matrix(2, 2, [1, 2, 3, 4]) + d = Matrix(3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 8]) + e = Matrix(4, 4, + [x, 1, 2, 3, 4, 5, 6, 7, 2, 9, 10, 11, 12, 13, 14, 14]) + from sympy.abc import i, j, k, l, m, n + f = Matrix(3, 3, [i, l, m, 0, j, n, 0, 0, k]) + g = Matrix(3, 3, [i, 0, 0, l, j, 0, m, n, k]) + h = Matrix(3, 3, [x**3, 0, 0, i, x**-1, 0, j, k, x**-2]) + # the method keyword for `det` doesn't kick in until 4x4 matrices, + # so there is no need to test all methods on smaller ones + + assert a.det() == 1 + assert b.det() == 5 + assert c.det() == -2 + assert d.det() == 3 + assert e.det() == 4*x - 24 + assert e.det(method="domain-ge") == 4*x - 24 + assert e.det(method='bareiss') == 4*x - 24 + assert e.det(method='berkowitz') == 4*x - 24 + assert f.det() == i*j*k + assert g.det() == i*j*k + assert h.det() == 1 + raises(ValueError, lambda: e.det(iszerofunc="test")) + +def test_permanent(): + M = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + assert M.per() == 450 + for i in range(1, 12): + assert ones(i, i).per() == ones(i, i).T.per() == factorial(i) + assert (ones(i, i)-eye(i)).per() == (ones(i, i)-eye(i)).T.per() == subfactorial(i) + + a1, a2, a3, a4, a5 = symbols('a_1 a_2 a_3 a_4 a_5') + M = Matrix([a1, a2, a3, a4, a5]) + assert M.per() == M.T.per() == a1 + a2 + a3 + a4 + a5 + +def test_adjugate(): + x = Symbol('x') + e = Matrix(4, 4, + [x, 1, 2, 3, 4, 5, 6, 7, 2, 9, 10, 11, 12, 13, 14, 14]) + + adj = Matrix([ + [ 4, -8, 4, 0], + [ 76, -14*x - 68, 14*x - 8, -4*x + 24], + [-122, 17*x + 142, -21*x + 4, 8*x - 48], + [ 48, -4*x - 72, 8*x, -4*x + 24]]) + assert e.adjugate() == adj + assert e.adjugate(method='bareiss') == adj + assert e.adjugate(method='berkowitz') == adj + assert e.adjugate(method='bird') == adj + assert e.adjugate(method='laplace') == adj + + a = Matrix(2, 3, [1, 2, 3, 4, 5, 6]) + raises(NonSquareMatrixError, lambda: a.adjugate()) + +def test_util(): + R = Rational + + v1 = Matrix(1, 3, [1, 2, 3]) + v2 = Matrix(1, 3, [3, 4, 5]) + assert v1.norm() == sqrt(14) + assert v1.project(v2) == Matrix(1, 3, [R(39)/25, R(52)/25, R(13)/5]) + assert Matrix.zeros(1, 2) == Matrix(1, 2, [0, 0]) + assert ones(1, 2) == Matrix(1, 2, [1, 1]) + assert v1.copy() == v1 + # cofactor + assert eye(3) == eye(3).cofactor_matrix() + test = Matrix([[1, 3, 2], [2, 6, 3], [2, 3, 6]]) + assert test.cofactor_matrix() == \ + Matrix([[27, -6, -6], [-12, 2, 3], [-3, 1, 0]]) + test = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + assert test.cofactor_matrix() == \ + Matrix([[-3, 6, -3], [6, -12, 6], [-3, 6, -3]]) + +def test_cofactor_and_minors(): + x = Symbol('x') + e = Matrix(4, 4, + [x, 1, 2, 3, 4, 5, 6, 7, 2, 9, 10, 11, 12, 13, 14, 14]) + + m = Matrix([ + [ x, 1, 3], + [ 2, 9, 11], + [12, 13, 14]]) + cm = Matrix([ + [ 4, 76, -122, 48], + [-8, -14*x - 68, 17*x + 142, -4*x - 72], + [ 4, 14*x - 8, -21*x + 4, 8*x], + [ 0, -4*x + 24, 8*x - 48, -4*x + 24]]) + sub = Matrix([ + [x, 1, 2], + [4, 5, 6], + [2, 9, 10]]) + + assert e.minor_submatrix(1, 2) == m + assert e.minor_submatrix(-1, -1) == sub + assert e.minor(1, 2) == -17*x - 142 + assert e.cofactor(1, 2) == 17*x + 142 + assert e.cofactor_matrix() == cm + assert e.cofactor_matrix(method="bareiss") == cm + assert e.cofactor_matrix(method="berkowitz") == cm + assert e.cofactor_matrix(method="bird") == cm + assert e.cofactor_matrix(method="laplace") == cm + + raises(ValueError, lambda: e.cofactor(4, 5)) + raises(ValueError, lambda: e.minor(4, 5)) + raises(ValueError, lambda: e.minor_submatrix(4, 5)) + + a = Matrix(2, 3, [1, 2, 3, 4, 5, 6]) + assert a.minor_submatrix(0, 0) == Matrix([[5, 6]]) + + raises(ValueError, lambda: + Matrix(0, 0, []).minor_submatrix(0, 0)) + raises(NonSquareMatrixError, lambda: a.cofactor(0, 0)) + raises(NonSquareMatrixError, lambda: a.minor(0, 0)) + raises(NonSquareMatrixError, lambda: a.cofactor_matrix()) + +def test_charpoly(): + x, y = Symbol('x'), Symbol('y') + z, t = Symbol('z'), Symbol('t') + + from sympy.abc import a,b,c + + m = Matrix(3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 9]) + + assert eye_Determinant(3).charpoly(x) == Poly((x - 1)**3, x) + assert eye_Determinant(3).charpoly(y) == Poly((y - 1)**3, y) + assert m.charpoly() == Poly(x**3 - 15*x**2 - 18*x, x) + raises(NonSquareMatrixError, lambda: Matrix([[1], [2]]).charpoly()) + n = Matrix(4, 4, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + assert n.charpoly() == Poly(x**4, x) + + n = Matrix(4, 4, [45, 0, 0, 0, 0, 23, 0, 0, 0, 0, 87, 0, 0, 0, 0, 12]) + assert n.charpoly() == Poly(x**4 - 167*x**3 + 8811*x**2 - 173457*x + 1080540, x) + + n = Matrix(3, 3, [x, 0, 0, a, y, 0, b, c, z]) + assert n.charpoly() == Poly(t**3 - (x+y+z)*t**2 + t*(x*y+y*z+x*z) - x*y*z, t) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_domains.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_domains.py new file mode 100644 index 0000000000000000000000000000000000000000..26a54b8879a5c65f3a01b4886d223c08309e733d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_domains.py @@ -0,0 +1,113 @@ +# Test Matrix/DomainMatrix interaction. + + +from sympy import GF, ZZ, QQ, EXRAW +from sympy.polys.matrices import DomainMatrix, DM + +from sympy import ( + Matrix, + MutableMatrix, + ImmutableMatrix, + SparseMatrix, + MutableDenseMatrix, + ImmutableDenseMatrix, + MutableSparseMatrix, + ImmutableSparseMatrix, +) +from sympy import symbols, S, sqrt + +from sympy.testing.pytest import raises + + +x, y = symbols('x y') + + +MATRIX_TYPES = ( + Matrix, + MutableMatrix, + ImmutableMatrix, + SparseMatrix, + MutableDenseMatrix, + ImmutableDenseMatrix, + MutableSparseMatrix, + ImmutableSparseMatrix, +) +IMMUTABLE = ( + ImmutableMatrix, + ImmutableDenseMatrix, + ImmutableSparseMatrix, +) + + +def DMs(items, domain): + return DM(items, domain).to_sparse() + + +def test_Matrix_rep_domain(): + + for Mat in MATRIX_TYPES: + + M = Mat([[1, 2], [3, 4]]) + assert M._rep == DMs([[1, 2], [3, 4]], ZZ) + assert (M / 2)._rep == DMs([[(1,2), 1], [(3,2), 2]], QQ) + if not isinstance(M, IMMUTABLE): + M[0, 0] = x + assert M._rep == DMs([[x, 2], [3, 4]], EXRAW) + + M = Mat([[S(1)/2, 2], [3, 4]]) + assert M._rep == DMs([[(1,2), 2], [3, 4]], QQ) + if not isinstance(M, IMMUTABLE): + M[0, 0] = x + assert M._rep == DMs([[x, 2], [3, 4]], EXRAW) + + dM = DMs([[1, 2], [3, 4]], ZZ) + assert Mat._fromrep(dM)._rep == dM + + # XXX: This is not intended. Perhaps it should be coerced to EXRAW? + # The private _fromrep method is never called like this but perhaps it + # should be guarded. + # + # It is not clear how to integrate domains other than ZZ, QQ and EXRAW with + # the rest of Matrix or if the public type for this needs to be something + # different from Matrix somehow. + K = QQ.algebraic_field(sqrt(2)) + dM = DM([[1, 2], [3, 4]], K) + assert Mat._fromrep(dM)._rep.domain == K + + +def test_Matrix_to_DM(): + + M = Matrix([[1, 2], [3, 4]]) + assert M.to_DM() == DMs([[1, 2], [3, 4]], ZZ) + assert M.to_DM() is not M._rep + assert M.to_DM(field=True) == DMs([[1, 2], [3, 4]], QQ) + assert M.to_DM(domain=QQ) == DMs([[1, 2], [3, 4]], QQ) + assert M.to_DM(domain=QQ[x]) == DMs([[1, 2], [3, 4]], QQ[x]) + assert M.to_DM(domain=GF(3)) == DMs([[1, 2], [0, 1]], GF(3)) + + M = Matrix([[1, 2], [3, 4]]) + M[0, 0] = x + assert M._rep.domain == EXRAW + M[0, 0] = 1 + assert M.to_DM() == DMs([[1, 2], [3, 4]], ZZ) + + M = Matrix([[S(1)/2, 2], [3, 4]]) + assert M.to_DM() == DMs([[QQ(1,2), 2], [3, 4]], QQ) + + M = Matrix([[x, 2], [3, 4]]) + assert M.to_DM() == DMs([[x, 2], [3, 4]], ZZ[x]) + assert M.to_DM(field=True) == DMs([[x, 2], [3, 4]], ZZ.frac_field(x)) + + M = Matrix([[1/x, 2], [3, 4]]) + assert M.to_DM() == DMs([[1/x, 2], [3, 4]], ZZ.frac_field(x)) + + M = Matrix([[1, sqrt(2)], [3, 4]]) + K = QQ.algebraic_field(sqrt(2)) + sqrt2 = K.from_sympy(sqrt(2)) # XXX: Maybe K(sqrt(2)) should work + M_K = DomainMatrix([[K(1), sqrt2], [K(3), K(4)]], (2, 2), K) + assert M.to_DM() == DMs([[1, sqrt(2)], [3, 4]], EXRAW) + assert M.to_DM(extension=True) == M_K.to_sparse() + + # Options cannot be used with the domain parameter + M = Matrix([[1, 2], [3, 4]]) + raises(TypeError, lambda: M.to_DM(domain=QQ, field=True)) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_eigen.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_eigen.py new file mode 100644 index 0000000000000000000000000000000000000000..fcf96325519879e0683d29e2ddc32db7bf83baa4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_eigen.py @@ -0,0 +1,712 @@ +from sympy.core.evalf import N +from sympy.core.numbers import (Float, I, Rational) +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.matrices import eye, Matrix +from sympy.core.singleton import S +from sympy.testing.pytest import raises, XFAIL +from sympy.matrices.exceptions import NonSquareMatrixError, MatrixError +from sympy.matrices.expressions.fourier import DFT +from sympy.simplify.simplify import simplify +from sympy.matrices.immutable import ImmutableMatrix +from sympy.testing.pytest import slow +from sympy.testing.matrices import allclose + + +def test_eigen(): + R = Rational + M = Matrix.eye(3) + assert M.eigenvals(multiple=False) == {S.One: 3} + assert M.eigenvals(multiple=True) == [1, 1, 1] + + assert M.eigenvects() == ( + [(1, 3, [Matrix([1, 0, 0]), + Matrix([0, 1, 0]), + Matrix([0, 0, 1])])]) + + assert M.left_eigenvects() == ( + [(1, 3, [Matrix([[1, 0, 0]]), + Matrix([[0, 1, 0]]), + Matrix([[0, 0, 1]])])]) + + M = Matrix([[0, 1, 1], + [1, 0, 0], + [1, 1, 1]]) + + assert M.eigenvals() == {2*S.One: 1, -S.One: 1, S.Zero: 1} + + assert M.eigenvects() == ( + [ + (-1, 1, [Matrix([-1, 1, 0])]), + ( 0, 1, [Matrix([0, -1, 1])]), + ( 2, 1, [Matrix([R(2, 3), R(1, 3), 1])]) + ]) + + assert M.left_eigenvects() == ( + [ + (-1, 1, [Matrix([[-2, 1, 1]])]), + (0, 1, [Matrix([[-1, -1, 1]])]), + (2, 1, [Matrix([[1, 1, 1]])]) + ]) + + a = Symbol('a') + M = Matrix([[a, 0], + [0, 1]]) + + assert M.eigenvals() == {a: 1, S.One: 1} + + M = Matrix([[1, -1], + [1, 3]]) + assert M.eigenvects() == ([(2, 2, [Matrix(2, 1, [-1, 1])])]) + assert M.left_eigenvects() == ([(2, 2, [Matrix([[1, 1]])])]) + + M = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + a = R(15, 2) + b = 3*33**R(1, 2) + c = R(13, 2) + d = (R(33, 8) + 3*b/8) + e = (R(33, 8) - 3*b/8) + + def NS(e, n): + return str(N(e, n)) + r = [ + (a - b/2, 1, [Matrix([(12 + 24/(c - b/2))/((c - b/2)*e) + 3/(c - b/2), + (6 + 12/(c - b/2))/e, 1])]), + ( 0, 1, [Matrix([1, -2, 1])]), + (a + b/2, 1, [Matrix([(12 + 24/(c + b/2))/((c + b/2)*d) + 3/(c + b/2), + (6 + 12/(c + b/2))/d, 1])]), + ] + r1 = [(NS(r[i][0], 2), NS(r[i][1], 2), + [NS(j, 2) for j in r[i][2][0]]) for i in range(len(r))] + r = M.eigenvects() + r2 = [(NS(r[i][0], 2), NS(r[i][1], 2), + [NS(j, 2) for j in r[i][2][0]]) for i in range(len(r))] + assert sorted(r1) == sorted(r2) + + eps = Symbol('eps', real=True) + + M = Matrix([[abs(eps), I*eps ], + [-I*eps, abs(eps) ]]) + + assert M.eigenvects() == ( + [ + ( 0, 1, [Matrix([[-I*eps/abs(eps)], [1]])]), + ( 2*abs(eps), 1, [ Matrix([[I*eps/abs(eps)], [1]]) ] ), + ]) + + assert M.left_eigenvects() == ( + [ + (0, 1, [Matrix([[I*eps/Abs(eps), 1]])]), + (2*Abs(eps), 1, [Matrix([[-I*eps/Abs(eps), 1]])]) + ]) + + M = Matrix(3, 3, [1, 2, 0, 0, 3, 0, 2, -4, 2]) + M._eigenvects = M.eigenvects(simplify=False) + assert max(i.q for i in M._eigenvects[0][2][0]) > 1 + M._eigenvects = M.eigenvects(simplify=True) + assert max(i.q for i in M._eigenvects[0][2][0]) == 1 + + M = Matrix([[Rational(1, 4), 1], [1, 1]]) + assert M.eigenvects() == [ + (Rational(5, 8) - sqrt(73)/8, 1, [Matrix([[-sqrt(73)/8 - Rational(3, 8)], [1]])]), + (Rational(5, 8) + sqrt(73)/8, 1, [Matrix([[Rational(-3, 8) + sqrt(73)/8], [1]])])] + + # issue 10719 + assert Matrix([]).eigenvals() == {} + assert Matrix([]).eigenvals(multiple=True) == [] + assert Matrix([]).eigenvects() == [] + + # issue 15119 + raises(NonSquareMatrixError, + lambda: Matrix([[1, 2], [0, 4], [0, 0]]).eigenvals()) + raises(NonSquareMatrixError, + lambda: Matrix([[1, 0], [3, 4], [5, 6]]).eigenvals()) + raises(NonSquareMatrixError, + lambda: Matrix([[1, 2, 3], [0, 5, 6]]).eigenvals()) + raises(NonSquareMatrixError, + lambda: Matrix([[1, 0, 0], [4, 5, 0]]).eigenvals()) + raises(NonSquareMatrixError, + lambda: Matrix([[1, 2, 3], [0, 5, 6]]).eigenvals( + error_when_incomplete = False)) + raises(NonSquareMatrixError, + lambda: Matrix([[1, 0, 0], [4, 5, 0]]).eigenvals( + error_when_incomplete = False)) + + m = Matrix([[1, 2], [3, 4]]) + assert isinstance(m.eigenvals(simplify=True, multiple=False), dict) + assert isinstance(m.eigenvals(simplify=True, multiple=True), list) + assert isinstance(m.eigenvals(simplify=lambda x: x, multiple=False), dict) + assert isinstance(m.eigenvals(simplify=lambda x: x, multiple=True), list) + + +def test_float_eigenvals(): + m = Matrix([[1, .6, .6], [.6, .9, .9], [.9, .6, .6]]) + evals = [ + Rational(5, 4) - sqrt(385)/20, + sqrt(385)/20 + Rational(5, 4), + S.Zero] + + n_evals = m.eigenvals(rational=True, multiple=True) + n_evals = sorted(n_evals) + s_evals = [x.evalf() for x in evals] + s_evals = sorted(s_evals) + + for x, y in zip(n_evals, s_evals): + assert abs(x-y) < 10**-9 + + +@XFAIL +def test_eigen_vects(): + m = Matrix(2, 2, [1, 0, 0, I]) + raises(NotImplementedError, lambda: m.is_diagonalizable(True)) + # !!! bug because of eigenvects() or roots(x**2 + (-1 - I)*x + I, x) + # see issue 5292 + assert not m.is_diagonalizable(True) + raises(MatrixError, lambda: m.diagonalize(True)) + (P, D) = m.diagonalize(True) + +def test_issue_8240(): + # Eigenvalues of large triangular matrices + x, y = symbols('x y') + n = 200 + + diagonal_variables = [Symbol('x%s' % i) for i in range(n)] + M = [[0 for i in range(n)] for j in range(n)] + for i in range(n): + M[i][i] = diagonal_variables[i] + M = Matrix(M) + + eigenvals = M.eigenvals() + assert len(eigenvals) == n + for i in range(n): + assert eigenvals[diagonal_variables[i]] == 1 + + eigenvals = M.eigenvals(multiple=True) + assert set(eigenvals) == set(diagonal_variables) + + # with multiplicity + M = Matrix([[x, 0, 0], [1, y, 0], [2, 3, x]]) + eigenvals = M.eigenvals() + assert eigenvals == {x: 2, y: 1} + + eigenvals = M.eigenvals(multiple=True) + assert len(eigenvals) == 3 + assert eigenvals.count(x) == 2 + assert eigenvals.count(y) == 1 + + +def test_eigenvals(): + M = Matrix([[0, 1, 1], + [1, 0, 0], + [1, 1, 1]]) + assert M.eigenvals() == {2*S.One: 1, -S.One: 1, S.Zero: 1} + + m = Matrix([ + [3, 0, 0, 0, -3], + [0, -3, -3, 0, 3], + [0, 3, 0, 3, 0], + [0, 0, 3, 0, 3], + [3, 0, 0, 3, 0]]) + + # XXX Used dry-run test because arbitrary symbol that appears in + # CRootOf may not be unique. + assert m.eigenvals() + + +def test_eigenvects(): + M = Matrix([[0, 1, 1], + [1, 0, 0], + [1, 1, 1]]) + vecs = M.eigenvects() + for val, mult, vec_list in vecs: + assert len(vec_list) == 1 + assert M*vec_list[0] == val*vec_list[0] + + +def test_left_eigenvects(): + M = Matrix([[0, 1, 1], + [1, 0, 0], + [1, 1, 1]]) + vecs = M.left_eigenvects() + for val, mult, vec_list in vecs: + assert len(vec_list) == 1 + assert vec_list[0]*M == val*vec_list[0] + + +@slow +def test_bidiagonalize(): + M = Matrix([[1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + assert M.bidiagonalize() == M + assert M.bidiagonalize(upper=False) == M + assert M.bidiagonalize() == M + assert M.bidiagonal_decomposition() == (M, M, M) + assert M.bidiagonal_decomposition(upper=False) == (M, M, M) + assert M.bidiagonalize() == M + + import random + #Real Tests + for real_test in range(2): + test_values = [] + row = 2 + col = 2 + for _ in range(row * col): + value = random.randint(-1000000000, 1000000000) + test_values = test_values + [value] + # L -> Lower Bidiagonalization + # M -> Mutable Matrix + # N -> Immutable Matrix + # 0 -> Bidiagonalized form + # 1,2,3 -> Bidiagonal_decomposition matrices + # 4 -> Product of 1 2 3 + M = Matrix(row, col, test_values) + N = ImmutableMatrix(M) + + N1, N2, N3 = N.bidiagonal_decomposition() + M1, M2, M3 = M.bidiagonal_decomposition() + M0 = M.bidiagonalize() + N0 = N.bidiagonalize() + + N4 = N1 * N2 * N3 + M4 = M1 * M2 * M3 + + N2.simplify() + N4.simplify() + N0.simplify() + + M0.simplify() + M2.simplify() + M4.simplify() + + LM0 = M.bidiagonalize(upper=False) + LM1, LM2, LM3 = M.bidiagonal_decomposition(upper=False) + LN0 = N.bidiagonalize(upper=False) + LN1, LN2, LN3 = N.bidiagonal_decomposition(upper=False) + + LN4 = LN1 * LN2 * LN3 + LM4 = LM1 * LM2 * LM3 + + LN2.simplify() + LN4.simplify() + LN0.simplify() + + LM0.simplify() + LM2.simplify() + LM4.simplify() + + assert M == M4 + assert M2 == M0 + assert N == N4 + assert N2 == N0 + assert M == LM4 + assert LM2 == LM0 + assert N == LN4 + assert LN2 == LN0 + + #Complex Tests + for complex_test in range(2): + test_values = [] + size = 2 + for _ in range(size * size): + real = random.randint(-1000000000, 1000000000) + comp = random.randint(-1000000000, 1000000000) + value = real + comp * I + test_values = test_values + [value] + M = Matrix(size, size, test_values) + N = ImmutableMatrix(M) + # L -> Lower Bidiagonalization + # M -> Mutable Matrix + # N -> Immutable Matrix + # 0 -> Bidiagonalized form + # 1,2,3 -> Bidiagonal_decomposition matrices + # 4 -> Product of 1 2 3 + N1, N2, N3 = N.bidiagonal_decomposition() + M1, M2, M3 = M.bidiagonal_decomposition() + M0 = M.bidiagonalize() + N0 = N.bidiagonalize() + + N4 = N1 * N2 * N3 + M4 = M1 * M2 * M3 + + N2.simplify() + N4.simplify() + N0.simplify() + + M0.simplify() + M2.simplify() + M4.simplify() + + LM0 = M.bidiagonalize(upper=False) + LM1, LM2, LM3 = M.bidiagonal_decomposition(upper=False) + LN0 = N.bidiagonalize(upper=False) + LN1, LN2, LN3 = N.bidiagonal_decomposition(upper=False) + + LN4 = LN1 * LN2 * LN3 + LM4 = LM1 * LM2 * LM3 + + LN2.simplify() + LN4.simplify() + LN0.simplify() + + LM0.simplify() + LM2.simplify() + LM4.simplify() + + assert M == M4 + assert M2 == M0 + assert N == N4 + assert N2 == N0 + assert M == LM4 + assert LM2 == LM0 + assert N == LN4 + assert LN2 == LN0 + + M = Matrix(18, 8, range(1, 145)) + M = M.applyfunc(lambda i: Float(i)) + assert M.bidiagonal_decomposition()[1] == M.bidiagonalize() + assert M.bidiagonal_decomposition(upper=False)[1] == M.bidiagonalize(upper=False) + a, b, c = M.bidiagonal_decomposition() + diff = a * b * c - M + assert abs(max(diff)) < 10**-12 + + +def test_diagonalize(): + m = Matrix(2, 2, [0, -1, 1, 0]) + raises(MatrixError, lambda: m.diagonalize(reals_only=True)) + P, D = m.diagonalize() + assert D.is_diagonal() + assert D == Matrix([ + [-I, 0], + [ 0, I]]) + + # make sure we use floats out if floats are passed in + m = Matrix(2, 2, [0, .5, .5, 0]) + P, D = m.diagonalize() + assert all(isinstance(e, Float) for e in D.values()) + assert all(isinstance(e, Float) for e in P.values()) + + _, D2 = m.diagonalize(reals_only=True) + assert D == D2 + + m = Matrix( + [[0, 1, 0, 0], [1, 0, 0, 0.002], [0.002, 0, 0, 1], [0, 0, 1, 0]]) + P, D = m.diagonalize() + assert allclose(P*D, m*P) + + +def test_is_diagonalizable(): + a, b, c = symbols('a b c') + m = Matrix(2, 2, [a, c, c, b]) + assert m.is_symmetric() + assert m.is_diagonalizable() + assert not Matrix(2, 2, [1, 1, 0, 1]).is_diagonalizable() + + m = Matrix(2, 2, [0, -1, 1, 0]) + assert m.is_diagonalizable() + assert not m.is_diagonalizable(reals_only=True) + + +def test_jordan_form(): + m = Matrix(3, 2, [-3, 1, -3, 20, 3, 10]) + raises(NonSquareMatrixError, lambda: m.jordan_form()) + + # the next two tests test the cases where the old + # algorithm failed due to the fact that the block structure can + # *NOT* be determined from algebraic and geometric multiplicity alone + # This can be seen most easily when one lets compute the J.c.f. of a matrix that + # is in J.c.f already. + m = Matrix(4, 4, [2, 1, 0, 0, + 0, 2, 1, 0, + 0, 0, 2, 0, + 0, 0, 0, 2 + ]) + P, J = m.jordan_form() + assert m == J + + m = Matrix(4, 4, [2, 1, 0, 0, + 0, 2, 0, 0, + 0, 0, 2, 1, + 0, 0, 0, 2 + ]) + P, J = m.jordan_form() + assert m == J + + A = Matrix([[ 2, 4, 1, 0], + [-4, 2, 0, 1], + [ 0, 0, 2, 4], + [ 0, 0, -4, 2]]) + P, J = A.jordan_form() + assert simplify(P*J*P.inv()) == A + + assert Matrix(1, 1, [1]).jordan_form() == (Matrix([1]), Matrix([1])) + assert Matrix(1, 1, [1]).jordan_form(calc_transform=False) == Matrix([1]) + + # If we have eigenvalues in CRootOf form, raise errors + m = Matrix([[3, 0, 0, 0, -3], [0, -3, -3, 0, 3], [0, 3, 0, 3, 0], [0, 0, 3, 0, 3], [3, 0, 0, 3, 0]]) + raises(MatrixError, lambda: m.jordan_form()) + + # make sure that if the input has floats, the output does too + m = Matrix([ + [ 0.6875, 0.125 + 0.1875*sqrt(3)], + [0.125 + 0.1875*sqrt(3), 0.3125]]) + P, J = m.jordan_form() + assert all(isinstance(x, Float) or x == 0 for x in P) + assert all(isinstance(x, Float) or x == 0 for x in J) + + +def test_singular_values(): + x = Symbol('x', real=True) + + A = Matrix([[0, 1*I], [2, 0]]) + # if singular values can be sorted, they should be in decreasing order + assert A.singular_values() == [2, 1] + + A = eye(3) + A[1, 1] = x + A[2, 2] = 5 + vals = A.singular_values() + # since Abs(x) cannot be sorted, test set equality + assert set(vals) == {5, 1, Abs(x)} + + A = Matrix([[sin(x), cos(x)], [-cos(x), sin(x)]]) + vals = [sv.trigsimp() for sv in A.singular_values()] + assert vals == [S.One, S.One] + + A = Matrix([ + [2, 4], + [1, 3], + [0, 0], + [0, 0] + ]) + assert A.singular_values() == \ + [sqrt(sqrt(221) + 15), sqrt(15 - sqrt(221))] + assert A.T.singular_values() == \ + [sqrt(sqrt(221) + 15), sqrt(15 - sqrt(221)), 0, 0] + +def test___eq__(): + assert (Matrix( + [[0, 1, 1], + [1, 0, 0], + [1, 1, 1]]) == {}) is False + + +def test_definite(): + # Examples from Gilbert Strang, "Introduction to Linear Algebra" + # Positive definite matrices + m = Matrix([[2, -1, 0], [-1, 2, -1], [0, -1, 2]]) + assert m.is_positive_definite == True + assert m.is_positive_semidefinite == True + assert m.is_negative_definite == False + assert m.is_negative_semidefinite == False + assert m.is_indefinite == False + + m = Matrix([[5, 4], [4, 5]]) + assert m.is_positive_definite == True + assert m.is_positive_semidefinite == True + assert m.is_negative_definite == False + assert m.is_negative_semidefinite == False + assert m.is_indefinite == False + + # Positive semidefinite matrices + m = Matrix([[2, -1, -1], [-1, 2, -1], [-1, -1, 2]]) + assert m.is_positive_definite == False + assert m.is_positive_semidefinite == True + assert m.is_negative_definite == False + assert m.is_negative_semidefinite == False + assert m.is_indefinite == False + + m = Matrix([[1, 2], [2, 4]]) + assert m.is_positive_definite == False + assert m.is_positive_semidefinite == True + assert m.is_negative_definite == False + assert m.is_negative_semidefinite == False + assert m.is_indefinite == False + + # Examples from Mathematica documentation + # Non-hermitian positive definite matrices + m = Matrix([[2, 3], [4, 8]]) + assert m.is_positive_definite == True + assert m.is_positive_semidefinite == True + assert m.is_negative_definite == False + assert m.is_negative_semidefinite == False + assert m.is_indefinite == False + + # Hermetian matrices + m = Matrix([[1, 2*I], [-I, 4]]) + assert m.is_positive_definite == True + assert m.is_positive_semidefinite == True + assert m.is_negative_definite == False + assert m.is_negative_semidefinite == False + assert m.is_indefinite == False + + # Symbolic matrices examples + a = Symbol('a', positive=True) + b = Symbol('b', negative=True) + m = Matrix([[a, 0, 0], [0, a, 0], [0, 0, a]]) + assert m.is_positive_definite == True + assert m.is_positive_semidefinite == True + assert m.is_negative_definite == False + assert m.is_negative_semidefinite == False + assert m.is_indefinite == False + + m = Matrix([[b, 0, 0], [0, b, 0], [0, 0, b]]) + assert m.is_positive_definite == False + assert m.is_positive_semidefinite == False + assert m.is_negative_definite == True + assert m.is_negative_semidefinite == True + assert m.is_indefinite == False + + m = Matrix([[a, 0], [0, b]]) + assert m.is_positive_definite == False + assert m.is_positive_semidefinite == False + assert m.is_negative_definite == False + assert m.is_negative_semidefinite == False + assert m.is_indefinite == True + + m = Matrix([ + [0.0228202735623867, 0.00518748979085398, + -0.0743036351048907, -0.00709135324903921], + [0.00518748979085398, 0.0349045359786350, + 0.0830317991056637, 0.00233147902806909], + [-0.0743036351048907, 0.0830317991056637, + 1.15859676366277, 0.340359081555988], + [-0.00709135324903921, 0.00233147902806909, + 0.340359081555988, 0.928147644848199] + ]) + assert m.is_positive_definite == True + assert m.is_positive_semidefinite == True + assert m.is_indefinite == False + + # test for issue 19547: https://github.com/sympy/sympy/issues/19547 + m = Matrix([ + [0, 0, 0], + [0, 1, 2], + [0, 2, 1] + ]) + assert not m.is_positive_definite + assert not m.is_positive_semidefinite + + +def test_positive_semidefinite_cholesky(): + from sympy.matrices.eigen import _is_positive_semidefinite_cholesky + + m = Matrix([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) + assert _is_positive_semidefinite_cholesky(m) == True + m = Matrix([[0, 0, 0], [0, 5, -10*I], [0, 10*I, 5]]) + assert _is_positive_semidefinite_cholesky(m) == False + m = Matrix([[1, 0, 0], [0, 0, 0], [0, 0, -1]]) + assert _is_positive_semidefinite_cholesky(m) == False + m = Matrix([[0, 1], [1, 0]]) + assert _is_positive_semidefinite_cholesky(m) == False + + # https://www.value-at-risk.net/cholesky-factorization/ + m = Matrix([[4, -2, -6], [-2, 10, 9], [-6, 9, 14]]) + assert _is_positive_semidefinite_cholesky(m) == True + m = Matrix([[9, -3, 3], [-3, 2, 1], [3, 1, 6]]) + assert _is_positive_semidefinite_cholesky(m) == True + m = Matrix([[4, -2, 2], [-2, 1, -1], [2, -1, 5]]) + assert _is_positive_semidefinite_cholesky(m) == True + m = Matrix([[1, 2, -1], [2, 5, 1], [-1, 1, 9]]) + assert _is_positive_semidefinite_cholesky(m) == False + + +def test_issue_20582(): + A = Matrix([ + [5, -5, -3, 2, -7], + [-2, -5, 0, 2, 1], + [-2, -7, -5, -2, -6], + [7, 10, 3, 9, -2], + [4, -10, 3, -8, -4] + ]) + # XXX Used dry-run test because arbitrary symbol that appears in + # CRootOf may not be unique. + assert A.eigenvects() + +def test_issue_19210(): + t = Symbol('t') + H = Matrix([[3, 0, 0, 0], [0, 1 , 2, 0], [0, 2, 2, 0], [0, 0, 0, 4]]) + A = (-I * H * t).jordan_form() + assert A == (Matrix([ + [0, 1, 0, 0], + [0, 0, -4/(-1 + sqrt(17)), 4/(1 + sqrt(17))], + [0, 0, 1, 1], + [1, 0, 0, 0]]), Matrix([ + [-4*I*t, 0, 0, 0], + [ 0, -3*I*t, 0, 0], + [ 0, 0, t*(-3*I/2 + sqrt(17)*I/2), 0], + [ 0, 0, 0, t*(-sqrt(17)*I/2 - 3*I/2)]])) + + +def test_issue_20275(): + # XXX We use complex expansions because complex exponentials are not + # recognized by polys.domains + A = DFT(3).as_explicit().expand(complex=True) + eigenvects = A.eigenvects() + assert eigenvects[0] == ( + -1, 1, + [Matrix([[1 - sqrt(3)], [1], [1]])] + ) + assert eigenvects[1] == ( + 1, 1, + [Matrix([[1 + sqrt(3)], [1], [1]])] + ) + assert eigenvects[2] == ( + -I, 1, + [Matrix([[0], [-1], [1]])] + ) + + A = DFT(4).as_explicit().expand(complex=True) + eigenvects = A.eigenvects() + assert eigenvects[0] == ( + -1, 1, + [Matrix([[-1], [1], [1], [1]])] + ) + assert eigenvects[1] == ( + 1, 2, + [Matrix([[1], [0], [1], [0]]), Matrix([[2], [1], [0], [1]])] + ) + assert eigenvects[2] == ( + -I, 1, + [Matrix([[0], [-1], [0], [1]])] + ) + + # XXX We skip test for some parts of eigenvectors which are very + # complicated and fragile under expression tree changes + A = DFT(5).as_explicit().expand(complex=True) + eigenvects = A.eigenvects() + assert eigenvects[0] == ( + -1, 1, + [Matrix([[1 - sqrt(5)], [1], [1], [1], [1]])] + ) + assert eigenvects[1] == ( + 1, 2, + [Matrix([[S(1)/2 + sqrt(5)/2], [0], [1], [1], [0]]), + Matrix([[S(1)/2 + sqrt(5)/2], [1], [0], [0], [1]])] + ) + + +def test_issue_20752(): + b = symbols('b', nonzero=True) + m = Matrix([[0, 0, 0], [0, b, 0], [0, 0, b]]) + assert m.is_positive_semidefinite is None + + +def test_issue_25282(): + dd = sd = [0] * 11 + [1] + ds = [2, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0] + ss = ds.copy() + ss[8] = 2 + + def rotate(x, i): + return x[i:] + x[:i] + + mat = [] + for i in range(12): + mat.append(rotate(ss, i) + rotate(sd, i)) + for i in range(12): + mat.append(rotate(ds, i) + rotate(dd, i)) + + assert sum(Matrix(mat).eigenvals().values()) == 24 diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_graph.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..0bf3c819a9477387f53560a034d7949fd76a654f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_graph.py @@ -0,0 +1,108 @@ +from sympy.combinatorics import Permutation +from sympy.core.symbol import symbols +from sympy.matrices import Matrix +from sympy.matrices.expressions import ( + PermutationMatrix, BlockDiagMatrix, BlockMatrix) + + +def test_connected_components(): + a, b, c, d, e, f, g, h, i, j, k, l, m = symbols('a:m') + + M = Matrix([ + [a, 0, 0, 0, b, 0, 0, 0, 0, 0, c, 0, 0], + [0, d, 0, 0, 0, e, 0, 0, 0, 0, 0, f, 0], + [0, 0, g, 0, 0, 0, h, 0, 0, 0, 0, 0, i], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [m, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, m, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, m, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [j, 0, 0, 0, k, 0, 0, 1, 0, 0, l, 0, 0], + [0, j, 0, 0, 0, k, 0, 0, 1, 0, 0, l, 0], + [0, 0, j, 0, 0, 0, k, 0, 0, 1, 0, 0, l], + [0, 0, 0, 0, d, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, d, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, d, 0, 0, 0, 0, 0, 1]]) + cc = M.connected_components() + assert cc == [[0, 4, 7, 10], [1, 5, 8, 11], [2, 6, 9, 12], [3]] + + P, B = M.connected_components_decomposition() + p = Permutation([0, 4, 7, 10, 1, 5, 8, 11, 2, 6, 9, 12, 3]) + assert P == PermutationMatrix(p) + + B0 = Matrix([ + [a, b, 0, c], + [m, 1, 0, 0], + [j, k, 1, l], + [0, d, 0, 1]]) + B1 = Matrix([ + [d, e, 0, f], + [m, 1, 0, 0], + [j, k, 1, l], + [0, d, 0, 1]]) + B2 = Matrix([ + [g, h, 0, i], + [m, 1, 0, 0], + [j, k, 1, l], + [0, d, 0, 1]]) + B3 = Matrix([[1]]) + assert B == BlockDiagMatrix(B0, B1, B2, B3) + + +def test_strongly_connected_components(): + M = Matrix([ + [11, 14, 10, 0, 15, 0], + [0, 44, 0, 0, 45, 0], + [1, 4, 0, 0, 5, 0], + [0, 0, 0, 22, 0, 23], + [0, 54, 0, 0, 55, 0], + [0, 0, 0, 32, 0, 33]]) + scc = M.strongly_connected_components() + assert scc == [[1, 4], [0, 2], [3, 5]] + + P, B = M.strongly_connected_components_decomposition() + p = Permutation([1, 4, 0, 2, 3, 5]) + assert P == PermutationMatrix(p) + assert B == BlockMatrix([ + [ + Matrix([[44, 45], [54, 55]]), + Matrix.zeros(2, 2), + Matrix.zeros(2, 2) + ], + [ + Matrix([[14, 15], [4, 5]]), + Matrix([[11, 10], [1, 0]]), + Matrix.zeros(2, 2) + ], + [ + Matrix.zeros(2, 2), + Matrix.zeros(2, 2), + Matrix([[22, 23], [32, 33]]) + ] + ]) + P = P.as_explicit() + B = B.as_explicit() + assert P.T * B * P == M + + P, B = M.strongly_connected_components_decomposition(lower=False) + p = Permutation([3, 5, 0, 2, 1, 4]) + assert P == PermutationMatrix(p) + assert B == BlockMatrix([ + [ + Matrix([[22, 23], [32, 33]]), + Matrix.zeros(2, 2), + Matrix.zeros(2, 2) + ], + [ + Matrix.zeros(2, 2), + Matrix([[11, 10], [1, 0]]), + Matrix([[14, 15], [4, 5]]) + ], + [ + Matrix.zeros(2, 2), + Matrix.zeros(2, 2), + Matrix([[44, 45], [54, 55]]) + ] + ]) + P = P.as_explicit() + B = B.as_explicit() + assert P.T * B * P == M diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_immutable.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_immutable.py new file mode 100644 index 0000000000000000000000000000000000000000..2b83c1f9fae7f83be9d5f7dd4b484781dc128faf --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_immutable.py @@ -0,0 +1,136 @@ +from itertools import product + +from sympy.core.relational import (Equality, Unequality) +from sympy.core.singleton import S +from sympy.core.sympify import sympify +from sympy.integrals.integrals import integrate +from sympy.matrices.dense import (Matrix, eye, zeros) +from sympy.matrices.immutable import ImmutableMatrix +from sympy.matrices import SparseMatrix +from sympy.matrices.immutable import \ + ImmutableDenseMatrix, ImmutableSparseMatrix +from sympy.abc import x, y +from sympy.testing.pytest import raises + +IM = ImmutableDenseMatrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) +ISM = ImmutableSparseMatrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) +ieye = ImmutableDenseMatrix(eye(3)) + + +def test_creation(): + assert IM.shape == ISM.shape == (3, 3) + assert IM[1, 2] == ISM[1, 2] == 6 + assert IM[2, 2] == ISM[2, 2] == 9 + + +def test_immutability(): + with raises(TypeError): + IM[2, 2] = 5 + with raises(TypeError): + ISM[2, 2] = 5 + + +def test_slicing(): + assert IM[1, :] == ImmutableDenseMatrix([[4, 5, 6]]) + assert IM[:2, :2] == ImmutableDenseMatrix([[1, 2], [4, 5]]) + assert ISM[1, :] == ImmutableSparseMatrix([[4, 5, 6]]) + assert ISM[:2, :2] == ImmutableSparseMatrix([[1, 2], [4, 5]]) + + +def test_subs(): + A = ImmutableMatrix([[1, 2], [3, 4]]) + B = ImmutableMatrix([[1, 2], [x, 4]]) + C = ImmutableMatrix([[-x, x*y], [-(x + y), y**2]]) + assert B.subs(x, 3) == A + assert (x*B).subs(x, 3) == 3*A + assert (x*eye(2) + B).subs(x, 3) == 3*eye(2) + A + assert C.subs([[x, -1], [y, -2]]) == A + assert C.subs([(x, -1), (y, -2)]) == A + assert C.subs({x: -1, y: -2}) == A + assert C.subs({x: y - 1, y: x - 1}, simultaneous=True) == \ + ImmutableMatrix([[1 - y, (x - 1)*(y - 1)], [2 - x - y, (x - 1)**2]]) + + +def test_as_immutable(): + data = [[1, 2], [3, 4]] + X = Matrix(data) + assert sympify(X) == X.as_immutable() == ImmutableMatrix(data) + + data = {(0, 0): 1, (0, 1): 2, (1, 0): 3, (1, 1): 4} + X = SparseMatrix(2, 2, data) + assert sympify(X) == X.as_immutable() == ImmutableSparseMatrix(2, 2, data) + + +def test_function_return_types(): + # Lets ensure that decompositions of immutable matrices remain immutable + # I.e. do MatrixBase methods return the correct class? + X = ImmutableMatrix([[1, 2], [3, 4]]) + Y = ImmutableMatrix([[1], [0]]) + q, r = X.QRdecomposition() + assert (type(q), type(r)) == (ImmutableMatrix, ImmutableMatrix) + + assert type(X.LUsolve(Y)) == ImmutableMatrix + assert type(X.QRsolve(Y)) == ImmutableMatrix + + X = ImmutableMatrix([[5, 2], [2, 7]]) + assert X.T == X + assert X.is_symmetric + assert type(X.cholesky()) == ImmutableMatrix + L, D = X.LDLdecomposition() + assert (type(L), type(D)) == (ImmutableMatrix, ImmutableMatrix) + + X = ImmutableMatrix([[1, 2], [2, 1]]) + assert X.is_diagonalizable() + assert X.det() == -3 + assert X.norm(2) == 3 + + assert type(X.eigenvects()[0][2][0]) == ImmutableMatrix + + assert type(zeros(3, 3).as_immutable().nullspace()[0]) == ImmutableMatrix + + X = ImmutableMatrix([[1, 0], [2, 1]]) + assert type(X.lower_triangular_solve(Y)) == ImmutableMatrix + assert type(X.T.upper_triangular_solve(Y)) == ImmutableMatrix + + assert type(X.minor_submatrix(0, 0)) == ImmutableMatrix + +# issue 6279 +# https://github.com/sympy/sympy/issues/6279 +# Test that Immutable _op_ Immutable => Immutable and not MatExpr + + +def test_immutable_evaluation(): + X = ImmutableMatrix(eye(3)) + A = ImmutableMatrix(3, 3, range(9)) + assert isinstance(X + A, ImmutableMatrix) + assert isinstance(X * A, ImmutableMatrix) + assert isinstance(X * 2, ImmutableMatrix) + assert isinstance(2 * X, ImmutableMatrix) + assert isinstance(A**2, ImmutableMatrix) + + +def test_deterimant(): + assert ImmutableMatrix(4, 4, lambda i, j: i + j).det() == 0 + + +def test_Equality(): + assert Equality(IM, IM) is S.true + assert Unequality(IM, IM) is S.false + assert Equality(IM, IM.subs(1, 2)) is S.false + assert Unequality(IM, IM.subs(1, 2)) is S.true + assert Equality(IM, 2) is S.false + assert Unequality(IM, 2) is S.true + M = ImmutableMatrix([x, y]) + assert Equality(M, IM) is S.false + assert Unequality(M, IM) is S.true + assert Equality(M, M.subs(x, 2)).subs(x, 2) is S.true + assert Unequality(M, M.subs(x, 2)).subs(x, 2) is S.false + assert Equality(M, M.subs(x, 2)).subs(x, 3) is S.false + assert Unequality(M, M.subs(x, 2)).subs(x, 3) is S.true + + +def test_integrate(): + intIM = integrate(IM, x) + assert intIM.shape == IM.shape + assert all(intIM[i, j] == (1 + j + 3*i)*x for i, j in + product(range(3), range(3))) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_interactions.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_interactions.py new file mode 100644 index 0000000000000000000000000000000000000000..f4fc3268368e8dd632fc0df187d57ea5e845120c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_interactions.py @@ -0,0 +1,77 @@ +""" +We have a few different kind of Matrices +Matrix, ImmutableMatrix, MatrixExpr + +Here we test the extent to which they cooperate +""" + +from sympy.core.symbol import symbols +from sympy.matrices import (Matrix, MatrixSymbol, eye, Identity, + ImmutableMatrix) +from sympy.matrices.expressions import MatrixExpr, MatAdd +from sympy.matrices.matrixbase import classof +from sympy.testing.pytest import raises + +SM = MatrixSymbol('X', 3, 3) +SV = MatrixSymbol('v', 3, 1) +MM = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) +IM = ImmutableMatrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) +meye = eye(3) +imeye = ImmutableMatrix(eye(3)) +ideye = Identity(3) +a, b, c = symbols('a,b,c') + + +def test_IM_MM(): + assert isinstance(MM + IM, ImmutableMatrix) + assert isinstance(IM + MM, ImmutableMatrix) + assert isinstance(2*IM + MM, ImmutableMatrix) + assert MM.equals(IM) + + +def test_ME_MM(): + assert isinstance(Identity(3) + MM, MatrixExpr) + assert isinstance(SM + MM, MatAdd) + assert isinstance(MM + SM, MatAdd) + assert (Identity(3) + MM)[1, 1] == 6 + + +def test_equality(): + a, b, c = Identity(3), eye(3), ImmutableMatrix(eye(3)) + for x in [a, b, c]: + for y in [a, b, c]: + assert x.equals(y) + + +def test_matrix_symbol_MM(): + X = MatrixSymbol('X', 3, 3) + Y = eye(3) + X + assert Y[1, 1] == 1 + X[1, 1] + + +def test_matrix_symbol_vector_matrix_multiplication(): + A = MM * SV + B = IM * SV + assert A == B + C = (SV.T * MM.T).T + assert B == C + D = (SV.T * IM.T).T + assert C == D + + +def test_indexing_interactions(): + assert (a * IM)[1, 1] == 5*a + assert (SM + IM)[1, 1] == SM[1, 1] + IM[1, 1] + assert (SM * IM)[1, 1] == SM[1, 0]*IM[0, 1] + SM[1, 1]*IM[1, 1] + \ + SM[1, 2]*IM[2, 1] + + +def test_classof(): + A = Matrix(3, 3, range(9)) + B = ImmutableMatrix(3, 3, range(9)) + C = MatrixSymbol('C', 3, 3) + assert classof(A, A) == Matrix + assert classof(B, B) == ImmutableMatrix + assert classof(A, B) == ImmutableMatrix + assert classof(B, A) == ImmutableMatrix + raises(TypeError, lambda: classof(A, C)) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_matrices.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_matrices.py new file mode 100644 index 0000000000000000000000000000000000000000..d9d97341de570e078d652dddce58fb8f5cb99e43 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_matrices.py @@ -0,0 +1,3487 @@ +# +# Code for testing deprecated matrix classes. New test code should not be added +# here. Instead, add it to test_matrixbase.py. +# +# This entire test module and the corresponding sympy/matrices/matrices.py +# module will be removed in a future release. +# +import random +import concurrent.futures +from collections.abc import Hashable + +from sympy.core.add import Add +from sympy.core.function import Function, diff, expand +from sympy.core.numbers import (E, Float, I, Integer, Rational, nan, oo, pi) +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.core.sympify import sympify +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import (Max, Min, sqrt) +from sympy.functions.elementary.trigonometric import (cos, sin, tan) +from sympy.integrals.integrals import integrate +from sympy.matrices.expressions.transpose import transpose +from sympy.physics.quantum.operator import HermitianOperator, Operator, Dagger +from sympy.polys.polytools import (Poly, PurePoly) +from sympy.polys.rootoftools import RootOf +from sympy.printing.str import sstr +from sympy.sets.sets import FiniteSet +from sympy.simplify.simplify import (signsimp, simplify) +from sympy.simplify.trigsimp import trigsimp +from sympy.matrices.exceptions import (ShapeError, MatrixError, + NonSquareMatrixError) +from sympy.matrices.matrixbase import DeferredVector +from sympy.matrices.determinant import _find_reasonable_pivot_naive +from sympy.matrices.utilities import _simplify +from sympy.matrices import ( + GramSchmidt, ImmutableMatrix, ImmutableSparseMatrix, Matrix, + SparseMatrix, casoratian, diag, eye, hessian, + matrix_multiply_elementwise, ones, randMatrix, rot_axis1, rot_axis2, + rot_axis3, wronskian, zeros, MutableDenseMatrix, ImmutableDenseMatrix, + MatrixSymbol, dotprodsimp, rot_ccw_axis1, rot_ccw_axis2, rot_ccw_axis3) +from sympy.matrices.utilities import _dotprodsimp_state +from sympy.core import Tuple, Wild +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.utilities.iterables import flatten, capture, iterable +from sympy.utilities.exceptions import ignore_warnings +from sympy.testing.pytest import (raises, XFAIL, slow, skip, skip_under_pyodide, + warns_deprecated_sympy) +from sympy.assumptions import Q +from sympy.tensor.array import Array +from sympy.tensor.array.array_derivatives import ArrayDerivative +from sympy.matrices.expressions import MatPow +from sympy.algebras import Quaternion + +from sympy import O + +from sympy.abc import a, b, c, d, x, y, z, t + + +# don't re-order this list +classes = (Matrix, SparseMatrix, ImmutableMatrix, ImmutableSparseMatrix) + + +# Test the deprecated matrixmixins +from sympy.matrices.common import _MinimalMatrix, _CastableMatrix +from sympy.matrices.matrices import MatrixSubspaces, MatrixReductions + + +with warns_deprecated_sympy(): + class SubspaceOnlyMatrix(_MinimalMatrix, _CastableMatrix, MatrixSubspaces): + pass + + +with warns_deprecated_sympy(): + class ReductionsOnlyMatrix(_MinimalMatrix, _CastableMatrix, MatrixReductions): + pass + + +def eye_Reductions(n): + return ReductionsOnlyMatrix(n, n, lambda i, j: int(i == j)) + + +def zeros_Reductions(n): + return ReductionsOnlyMatrix(n, n, lambda i, j: 0) + + +def test_args(): + for n, cls in enumerate(classes): + m = cls.zeros(3, 2) + # all should give back the same type of arguments, e.g. ints for shape + assert m.shape == (3, 2) and all(type(i) is int for i in m.shape) + assert m.rows == 3 and type(m.rows) is int + assert m.cols == 2 and type(m.cols) is int + if not n % 2: + assert type(m.flat()) in (list, tuple, Tuple) + else: + assert type(m.todok()) is dict + + +def test_deprecated_mat_smat(): + for cls in Matrix, ImmutableMatrix: + m = cls.zeros(3, 2) + with warns_deprecated_sympy(): + mat = m._mat + assert mat == m.flat() + for cls in SparseMatrix, ImmutableSparseMatrix: + m = cls.zeros(3, 2) + with warns_deprecated_sympy(): + smat = m._smat + assert smat == m.todok() + + +def test_division(): + v = Matrix(1, 2, [x, y]) + assert v/z == Matrix(1, 2, [x/z, y/z]) + + +def test_sum(): + m = Matrix([[1, 2, 3], [x, y, x], [2*y, -50, z*x]]) + assert m + m == Matrix([[2, 4, 6], [2*x, 2*y, 2*x], [4*y, -100, 2*z*x]]) + n = Matrix(1, 2, [1, 2]) + raises(ShapeError, lambda: m + n) + +def test_abs(): + m = Matrix(1, 2, [-3, x]) + n = Matrix(1, 2, [3, Abs(x)]) + assert abs(m) == n + +def test_addition(): + a = Matrix(( + (1, 2), + (3, 1), + )) + + b = Matrix(( + (1, 2), + (3, 0), + )) + + assert a + b == a.add(b) == Matrix([[2, 4], [6, 1]]) + + +def test_fancy_index_matrix(): + for M in (Matrix, SparseMatrix): + a = M(3, 3, range(9)) + assert a == a[:, :] + assert a[1, :] == Matrix(1, 3, [3, 4, 5]) + assert a[:, 1] == Matrix([1, 4, 7]) + assert a[[0, 1], :] == Matrix([[0, 1, 2], [3, 4, 5]]) + assert a[[0, 1], 2] == a[[0, 1], [2]] + assert a[2, [0, 1]] == a[[2], [0, 1]] + assert a[:, [0, 1]] == Matrix([[0, 1], [3, 4], [6, 7]]) + assert a[0, 0] == 0 + assert a[0:2, :] == Matrix([[0, 1, 2], [3, 4, 5]]) + assert a[:, 0:2] == Matrix([[0, 1], [3, 4], [6, 7]]) + assert a[::2, 1] == a[[0, 2], 1] + assert a[1, ::2] == a[1, [0, 2]] + a = M(3, 3, range(9)) + assert a[[0, 2, 1, 2, 1], :] == Matrix([ + [0, 1, 2], + [6, 7, 8], + [3, 4, 5], + [6, 7, 8], + [3, 4, 5]]) + assert a[:, [0,2,1,2,1]] == Matrix([ + [0, 2, 1, 2, 1], + [3, 5, 4, 5, 4], + [6, 8, 7, 8, 7]]) + + a = SparseMatrix.zeros(3) + a[1, 2] = 2 + a[0, 1] = 3 + a[2, 0] = 4 + assert a.extract([1, 1], [2]) == Matrix([ + [2], + [2]]) + assert a.extract([1, 0], [2, 2, 2]) == Matrix([ + [2, 2, 2], + [0, 0, 0]]) + assert a.extract([1, 0, 1, 2], [2, 0, 1, 0]) == Matrix([ + [2, 0, 0, 0], + [0, 0, 3, 0], + [2, 0, 0, 0], + [0, 4, 0, 4]]) + + +def test_multiplication(): + a = Matrix(( + (1, 2), + (3, 1), + (0, 6), + )) + + b = Matrix(( + (1, 2), + (3, 0), + )) + + c = a*b + assert c[0, 0] == 7 + assert c[0, 1] == 2 + assert c[1, 0] == 6 + assert c[1, 1] == 6 + assert c[2, 0] == 18 + assert c[2, 1] == 0 + + try: + eval('c = a @ b') + except SyntaxError: + pass + else: + assert c[0, 0] == 7 + assert c[0, 1] == 2 + assert c[1, 0] == 6 + assert c[1, 1] == 6 + assert c[2, 0] == 18 + assert c[2, 1] == 0 + + h = matrix_multiply_elementwise(a, c) + assert h == a.multiply_elementwise(c) + assert h[0, 0] == 7 + assert h[0, 1] == 4 + assert h[1, 0] == 18 + assert h[1, 1] == 6 + assert h[2, 0] == 0 + assert h[2, 1] == 0 + raises(ShapeError, lambda: matrix_multiply_elementwise(a, b)) + + c = b * Symbol("x") + assert isinstance(c, Matrix) + assert c[0, 0] == x + assert c[0, 1] == 2*x + assert c[1, 0] == 3*x + assert c[1, 1] == 0 + + c2 = x * b + assert c == c2 + + c = 5 * b + assert isinstance(c, Matrix) + assert c[0, 0] == 5 + assert c[0, 1] == 2*5 + assert c[1, 0] == 3*5 + assert c[1, 1] == 0 + + try: + eval('c = 5 @ b') + except SyntaxError: + pass + else: + assert isinstance(c, Matrix) + assert c[0, 0] == 5 + assert c[0, 1] == 2*5 + assert c[1, 0] == 3*5 + assert c[1, 1] == 0 + + +def test_multiplication_inf_zero(): + + M = Matrix([[oo, 0], [0, oo]]) + assert M ** 2 == M + + M = Matrix([[oo, oo], [0, 0]]) + assert M ** 2 == Matrix([[nan, nan], [nan, nan]]) + + A = Matrix([ + [0, 0, 0, -S(1)/2], + [0, 1, 0, 0], + [0, 0, 1, 0], + [-S(1)/2, 0, 0, 0]]) + + B = Matrix([ + [pi*x**2, 0, pi*b*x**4/8 + pi*a*x**4/8 + O(x**5), pi*x**4/2 + pi*b**2*x**6/32 + pi*a*b*x**6/48 + pi*a**2*x**6/32 + O(x**7)], + [0, pi*x**4/4, O(x**6), O(x**8)], + [pi*b*x**4/8 + pi*a*x**4/8 + O(x**5), O(x**6), pi*b**2*x**6/32 + pi*a*b*x**6/48 + pi*a**2*x**6/32 + O(x**7), pi*b*x**6/12 + pi*a*x**6/12 + O(x**7)], + [pi*x**4/2 + pi*b**2*x**6/32 + pi*a*b*x**6/48 + pi*a**2*x**6/32 + O(x**7), O(x**8), pi*b*x**6/12 + pi*a*x**6/12 + O(x**7), pi*x**6/3 + 3*pi*b**2*x**8/64 + pi*a*b*x**8/32 + 3*pi*a**2*x**8/64 + O(x**9)]]) + + C = Matrix([ + [-pi*x**4/4 - pi*b**2*x**6/64 - pi*a*b*x**6/96 - pi*a**2*x**6/64 + O(x**7), O(x**8), -pi*b*x**6/24 - pi*a*x**6/24 + O(x**7), -pi*x**6/6 - 3*pi*b**2*x**8/128 - pi*a*b*x**8/64 - 3*pi*a**2*x**8/128 + O(x**9)], + [ 0, pi*x**4/4, O(x**6), O(x**8)], + [ pi*b*x**4/8 + pi*a*x**4/8 + O(x**5), O(x**6), pi*b**2*x**6/32 + pi*a*b*x**6/48 + pi*a**2*x**6/32 + O(x**7), pi*b*x**6/12 + pi*a*x**6/12 + O(x**7)], + [ -pi*x**2/2, 0, -pi*b*x**4/16 - pi*a*x**4/16 + O(x**5), -pi*x**4/4 - pi*b**2*x**6/64 - pi*a*b*x**6/96 - pi*a**2*x**6/64 + O(x**7)]]) + + C2 = Matrix(4, 4, lambda i, j: Add(*(A[i,k]*B[k,j] for k in range(4)))) + + assert A*B == C == C2 + + +def test_power(): + raises(NonSquareMatrixError, lambda: Matrix((1, 2))**2) + + R = Rational + A = Matrix([[2, 3], [4, 5]]) + assert (A**-3)[:] == [R(-269)/8, R(153)/8, R(51)/2, R(-29)/2] + assert (A**5)[:] == [6140, 8097, 10796, 14237] + A = Matrix([[2, 1, 3], [4, 2, 4], [6, 12, 1]]) + assert (A**3)[:] == [290, 262, 251, 448, 440, 368, 702, 954, 433] + assert A**0 == eye(3) + assert A**1 == A + assert (Matrix([[2]]) ** 100)[0, 0] == 2**100 + assert eye(2)**10000000 == eye(2) + assert Matrix([[1, 2], [3, 4]])**Integer(2) == Matrix([[7, 10], [15, 22]]) + + A = Matrix([[33, 24], [48, 57]]) + assert (A**S.Half)[:] == [5, 2, 4, 7] + A = Matrix([[0, 4], [-1, 5]]) + assert (A**S.Half)**2 == A + + assert Matrix([[1, 0], [1, 1]])**S.Half == Matrix([[1, 0], [S.Half, 1]]) + assert Matrix([[1, 0], [1, 1]])**0.5 == Matrix([[1, 0], [0.5, 1]]) + from sympy.abc import n + assert Matrix([[1, a], [0, 1]])**n == Matrix([[1, a*n], [0, 1]]) + assert Matrix([[b, a], [0, b]])**n == Matrix([[b**n, a*b**(n-1)*n], [0, b**n]]) + assert Matrix([ + [a**n, a**(n - 1)*n, (a**n*n**2 - a**n*n)/(2*a**2)], + [ 0, a**n, a**(n - 1)*n], + [ 0, 0, a**n]]) + assert Matrix([[a, 1, 0], [0, a, 0], [0, 0, b]])**n == Matrix([ + [a**n, a**(n-1)*n, 0], + [0, a**n, 0], + [0, 0, b**n]]) + + A = Matrix([[1, 0], [1, 7]]) + assert A._matrix_pow_by_jordan_blocks(S(3)) == A._eval_pow_by_recursion(3) + A = Matrix([[2]]) + assert A**10 == Matrix([[2**10]]) == A._matrix_pow_by_jordan_blocks(S(10)) == \ + A._eval_pow_by_recursion(10) + + # testing a matrix that cannot be jordan blocked issue 11766 + m = Matrix([[3, 0, 0, 0, -3], [0, -3, -3, 0, 3], [0, 3, 0, 3, 0], [0, 0, 3, 0, 3], [3, 0, 0, 3, 0]]) + raises(MatrixError, lambda: m._matrix_pow_by_jordan_blocks(S(10))) + + # test issue 11964 + raises(MatrixError, lambda: Matrix([[1, 1], [3, 3]])._matrix_pow_by_jordan_blocks(S(-10))) + A = Matrix([[0, 1, 0], [0, 0, 1], [0, 0, 0]]) # Nilpotent jordan block size 3 + assert A**10.0 == Matrix([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) + raises(ValueError, lambda: A**2.1) + raises(ValueError, lambda: A**Rational(3, 2)) + A = Matrix([[8, 1], [3, 2]]) + assert A**10.0 == Matrix([[1760744107, 272388050], [817164150, 126415807]]) + A = Matrix([[0, 0, 1], [0, 0, 1], [0, 0, 1]]) # Nilpotent jordan block size 1 + assert A**10.0 == Matrix([[0, 0, 1], [0, 0, 1], [0, 0, 1]]) + A = Matrix([[0, 1, 0], [0, 0, 1], [0, 0, 1]]) # Nilpotent jordan block size 2 + assert A**10.0 == Matrix([[0, 0, 1], [0, 0, 1], [0, 0, 1]]) + n = Symbol('n', integer=True) + assert isinstance(A**n, MatPow) + n = Symbol('n', integer=True, negative=True) + raises(ValueError, lambda: A**n) + n = Symbol('n', integer=True, nonnegative=True) + assert A**n == Matrix([ + [KroneckerDelta(0, n), KroneckerDelta(1, n), -KroneckerDelta(0, n) - KroneckerDelta(1, n) + 1], + [ 0, KroneckerDelta(0, n), 1 - KroneckerDelta(0, n)], + [ 0, 0, 1]]) + assert A**(n + 2) == Matrix([[0, 0, 1], [0, 0, 1], [0, 0, 1]]) + raises(ValueError, lambda: A**Rational(3, 2)) + A = Matrix([[0, 0, 1], [3, 0, 1], [4, 3, 1]]) + assert A**5.0 == Matrix([[168, 72, 89], [291, 144, 161], [572, 267, 329]]) + assert A**5.0 == A**5 + A = Matrix([[0, 1, 0],[-1, 0, 0],[0, 0, 0]]) + n = Symbol("n") + An = A**n + assert An.subs(n, 2).doit() == A**2 + raises(ValueError, lambda: An.subs(n, -2).doit()) + assert An * An == A**(2*n) + + # concretizing behavior for non-integer and complex powers + A = Matrix([[0,0,0],[0,0,0],[0,0,0]]) + n = Symbol('n', integer=True, positive=True) + assert A**n == A + n = Symbol('n', integer=True, nonnegative=True) + assert A**n == diag(0**n, 0**n, 0**n) + assert (A**n).subs(n, 0) == eye(3) + assert (A**n).subs(n, 1) == zeros(3) + A = Matrix ([[2,0,0],[0,2,0],[0,0,2]]) + assert A**2.1 == diag (2**2.1, 2**2.1, 2**2.1) + assert A**I == diag (2**I, 2**I, 2**I) + A = Matrix([[0, 1, 0], [0, 0, 1], [0, 0, 1]]) + raises(ValueError, lambda: A**2.1) + raises(ValueError, lambda: A**I) + A = Matrix([[S.Half, S.Half], [S.Half, S.Half]]) + assert A**S.Half == A + A = Matrix([[1, 1],[3, 3]]) + assert A**S.Half == Matrix ([[S.Half, S.Half], [3*S.Half, 3*S.Half]]) + + +def test_issue_17247_expression_blowup_1(): + M = Matrix([[1+x, 1-x], [1-x, 1+x]]) + with dotprodsimp(True): + assert M.exp().expand() == Matrix([ + [ (exp(2*x) + exp(2))/2, (-exp(2*x) + exp(2))/2], + [(-exp(2*x) + exp(2))/2, (exp(2*x) + exp(2))/2]]) + +def test_issue_17247_expression_blowup_2(): + M = Matrix([[1+x, 1-x], [1-x, 1+x]]) + with dotprodsimp(True): + P, J = M.jordan_form () + assert P*J*P.inv() + +def test_issue_17247_expression_blowup_3(): + M = Matrix([[1+x, 1-x], [1-x, 1+x]]) + with dotprodsimp(True): + assert M**100 == Matrix([ + [633825300114114700748351602688*x**100 + 633825300114114700748351602688, 633825300114114700748351602688 - 633825300114114700748351602688*x**100], + [633825300114114700748351602688 - 633825300114114700748351602688*x**100, 633825300114114700748351602688*x**100 + 633825300114114700748351602688]]) + +def test_issue_17247_expression_blowup_4(): +# This matrix takes extremely long on current master even with intermediate simplification so an abbreviated version is used. It is left here for test in case of future optimizations. +# M = Matrix(S('''[ +# [ -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64, 1/4 - 5*I/16, 65/128 + 87*I/64, -9/32 - I/16, 183/256 - 97*I/128, 3/64 + 13*I/64, -23/32 - 59*I/256, 15/128 - 3*I/32, 19/256 + 551*I/1024], +# [-149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128, 85/256 - 33*I/16, 805/128 + 2415*I/512, -219/128 + 115*I/256, 6301/4096 - 6609*I/1024, 119/128 + 143*I/128, -10879/2048 + 4343*I/4096, 129/256 - 549*I/512, 42533/16384 + 29103*I/8192], +# [ 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64, 1/4 - 5*I/16, 65/128 + 87*I/64, -9/32 - I/16, 183/256 - 97*I/128, 3/64 + 13*I/64, -23/32 - 59*I/256], +# [ -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128, 85/256 - 33*I/16, 805/128 + 2415*I/512, -219/128 + 115*I/256, 6301/4096 - 6609*I/1024, 119/128 + 143*I/128, -10879/2048 + 4343*I/4096], +# [ 1 + I, -19/4 + 5*I/4, 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64, 1/4 - 5*I/16, 65/128 + 87*I/64, -9/32 - I/16, 183/256 - 97*I/128], +# [ 21/8 + I, -537/64 + 143*I/16, -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128, 85/256 - 33*I/16, 805/128 + 2415*I/512, -219/128 + 115*I/256, 6301/4096 - 6609*I/1024], +# [ -2, 17/4 - 13*I/2, 1 + I, -19/4 + 5*I/4, 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64, 1/4 - 5*I/16, 65/128 + 87*I/64], +# [ 1/4 + 13*I/4, -825/64 - 147*I/32, 21/8 + I, -537/64 + 143*I/16, -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128, 85/256 - 33*I/16, 805/128 + 2415*I/512], +# [ -4*I, 27/2 + 6*I, -2, 17/4 - 13*I/2, 1 + I, -19/4 + 5*I/4, 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64], +# [ 1/4 + 5*I/2, -23/8 - 57*I/16, 1/4 + 13*I/4, -825/64 - 147*I/32, 21/8 + I, -537/64 + 143*I/16, -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128], +# [ -4, 9 - 5*I, -4*I, 27/2 + 6*I, -2, 17/4 - 13*I/2, 1 + I, -19/4 + 5*I/4, 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16], +# [ -2*I, 119/8 + 29*I/4, 1/4 + 5*I/2, -23/8 - 57*I/16, 1/4 + 13*I/4, -825/64 - 147*I/32, 21/8 + I, -537/64 + 143*I/16, -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128]]''')) +# assert M**10 == Matrix([ +# [ 7*(-221393644768594642173548179825793834595 - 1861633166167425978847110897013541127952*I)/9671406556917033397649408, 15*(31670992489131684885307005100073928751695 + 10329090958303458811115024718207404523808*I)/77371252455336267181195264, 7*(-3710978679372178839237291049477017392703 + 1377706064483132637295566581525806894169*I)/19342813113834066795298816, (9727707023582419994616144751727760051598 - 59261571067013123836477348473611225724433*I)/9671406556917033397649408, (31896723509506857062605551443641668183707 + 54643444538699269118869436271152084599580*I)/38685626227668133590597632, (-2024044860947539028275487595741003997397402 + 130959428791783397562960461903698670485863*I)/309485009821345068724781056, 3*(26190251453797590396533756519358368860907 - 27221191754180839338002754608545400941638*I)/77371252455336267181195264, (1154643595139959842768960128434994698330461 + 3385496216250226964322872072260446072295634*I)/618970019642690137449562112, 3*(-31849347263064464698310044805285774295286 - 11877437776464148281991240541742691164309*I)/77371252455336267181195264, (4661330392283532534549306589669150228040221 - 4171259766019818631067810706563064103956871*I)/1237940039285380274899124224, (9598353794289061833850770474812760144506 + 358027153990999990968244906482319780943983*I)/309485009821345068724781056, (-9755135335127734571547571921702373498554177 - 4837981372692695195747379349593041939686540*I)/2475880078570760549798248448], +# [(-379516731607474268954110071392894274962069 - 422272153179747548473724096872271700878296*I)/77371252455336267181195264, (41324748029613152354787280677832014263339501 - 12715121258662668420833935373453570749288074*I)/1237940039285380274899124224, (-339216903907423793947110742819264306542397 + 494174755147303922029979279454787373566517*I)/77371252455336267181195264, (-18121350839962855576667529908850640619878381 - 37413012454129786092962531597292531089199003*I)/1237940039285380274899124224, (2489661087330511608618880408199633556675926 + 1137821536550153872137379935240732287260863*I)/309485009821345068724781056, (-136644109701594123227587016790354220062972119 + 110130123468183660555391413889600443583585272*I)/4951760157141521099596496896, (1488043981274920070468141664150073426459593 - 9691968079933445130866371609614474474327650*I)/1237940039285380274899124224, 27*(4636797403026872518131756991410164760195942 + 3369103221138229204457272860484005850416533*I)/4951760157141521099596496896, (-8534279107365915284081669381642269800472363 + 2241118846262661434336333368511372725482742*I)/1237940039285380274899124224, (60923350128174260992536531692058086830950875 - 263673488093551053385865699805250505661590126*I)/9903520314283042199192993792, (18520943561240714459282253753348921824172569 + 24846649186468656345966986622110971925703604*I)/4951760157141521099596496896, (-232781130692604829085973604213529649638644431 + 35981505277760667933017117949103953338570617*I)/9903520314283042199192993792], +# [ (8742968295129404279528270438201520488950 + 3061473358639249112126847237482570858327*I)/4835703278458516698824704, (-245657313712011778432792959787098074935273 + 253113767861878869678042729088355086740856*I)/38685626227668133590597632, (1947031161734702327107371192008011621193 - 19462330079296259148177542369999791122762*I)/9671406556917033397649408, (552856485625209001527688949522750288619217 + 392928441196156725372494335248099016686580*I)/77371252455336267181195264, (-44542866621905323121630214897126343414629 + 3265340021421335059323962377647649632959*I)/19342813113834066795298816, (136272594005759723105646069956434264218730 - 330975364731707309489523680957584684763587*I)/38685626227668133590597632, (27392593965554149283318732469825168894401 + 75157071243800133880129376047131061115278*I)/38685626227668133590597632, 7*(-357821652913266734749960136017214096276154 - 45509144466378076475315751988405961498243*I)/309485009821345068724781056, (104485001373574280824835174390219397141149 - 99041000529599568255829489765415726168162*I)/77371252455336267181195264, (1198066993119982409323525798509037696321291 + 4249784165667887866939369628840569844519936*I)/618970019642690137449562112, (-114985392587849953209115599084503853611014 - 52510376847189529234864487459476242883449*I)/77371252455336267181195264, (6094620517051332877965959223269600650951573 - 4683469779240530439185019982269137976201163*I)/1237940039285380274899124224], +# [ (611292255597977285752123848828590587708323 - 216821743518546668382662964473055912169502*I)/77371252455336267181195264, (-1144023204575811464652692396337616594307487 + 12295317806312398617498029126807758490062855*I)/309485009821345068724781056, (-374093027769390002505693378578475235158281 - 573533923565898290299607461660384634333639*I)/77371252455336267181195264, (47405570632186659000138546955372796986832987 - 2837476058950808941605000274055970055096534*I)/1237940039285380274899124224, (-571573207393621076306216726219753090535121 + 533381457185823100878764749236639320783831*I)/77371252455336267181195264, (-7096548151856165056213543560958582513797519 - 24035731898756040059329175131592138642195366*I)/618970019642690137449562112, (2396762128833271142000266170154694033849225 + 1448501087375679588770230529017516492953051*I)/309485009821345068724781056, (-150609293845161968447166237242456473262037053 + 92581148080922977153207018003184520294188436*I)/4951760157141521099596496896, 5*(270278244730804315149356082977618054486347 - 1997830155222496880429743815321662710091562*I)/1237940039285380274899124224, (62978424789588828258068912690172109324360330 + 44803641177219298311493356929537007630129097*I)/2475880078570760549798248448, 19*(-451431106327656743945775812536216598712236 + 114924966793632084379437683991151177407937*I)/1237940039285380274899124224, (63417747628891221594106738815256002143915995 - 261508229397507037136324178612212080871150958*I)/9903520314283042199192993792], +# [ (-2144231934021288786200752920446633703357 + 2305614436009705803670842248131563850246*I)/1208925819614629174706176, (-90720949337459896266067589013987007078153 - 221951119475096403601562347412753844534569*I)/19342813113834066795298816, (11590973613116630788176337262688659880376 + 6514520676308992726483494976339330626159*I)/4835703278458516698824704, 3*(-131776217149000326618649542018343107657237 + 79095042939612668486212006406818285287004*I)/38685626227668133590597632, (10100577916793945997239221374025741184951 - 28631383488085522003281589065994018550748*I)/9671406556917033397649408, 67*(10090295594251078955008130473573667572549 + 10449901522697161049513326446427839676762*I)/77371252455336267181195264, (-54270981296988368730689531355811033930513 - 3413683117592637309471893510944045467443*I)/19342813113834066795298816, (440372322928679910536575560069973699181278 - 736603803202303189048085196176918214409081*I)/77371252455336267181195264, (33220374714789391132887731139763250155295 + 92055083048787219934030779066298919603554*I)/38685626227668133590597632, 5*(-594638554579967244348856981610805281527116 - 82309245323128933521987392165716076704057*I)/309485009821345068724781056, (128056368815300084550013708313312073721955 - 114619107488668120303579745393765245911404*I)/77371252455336267181195264, 21*(59839959255173222962789517794121843393573 + 241507883613676387255359616163487405826334*I)/618970019642690137449562112], +# [ (-13454485022325376674626653802541391955147 + 184471402121905621396582628515905949793486*I)/19342813113834066795298816, (-6158730123400322562149780662133074862437105 - 3416173052604643794120262081623703514107476*I)/154742504910672534362390528, (770558003844914708453618983120686116100419 - 127758381209767638635199674005029818518766*I)/77371252455336267181195264, (-4693005771813492267479835161596671660631703 + 12703585094750991389845384539501921531449948*I)/309485009821345068724781056, (-295028157441149027913545676461260860036601 - 841544569970643160358138082317324743450770*I)/77371252455336267181195264, (56716442796929448856312202561538574275502893 + 7216818824772560379753073185990186711454778*I)/1237940039285380274899124224, 15*(-87061038932753366532685677510172566368387 + 61306141156647596310941396434445461895538*I)/154742504910672534362390528, (-3455315109680781412178133042301025723909347 - 24969329563196972466388460746447646686670670*I)/618970019642690137449562112, (2453418854160886481106557323699250865361849 + 1497886802326243014471854112161398141242514*I)/309485009821345068724781056, (-151343224544252091980004429001205664193082173 + 90471883264187337053549090899816228846836628*I)/4951760157141521099596496896, (1652018205533026103358164026239417416432989 - 9959733619236515024261775397109724431400162*I)/1237940039285380274899124224, 3*(40676374242956907656984876692623172736522006 + 31023357083037817469535762230872667581366205*I)/4951760157141521099596496896], +# [ (-1226990509403328460274658603410696548387 - 4131739423109992672186585941938392788458*I)/1208925819614629174706176, (162392818524418973411975140074368079662703 + 23706194236915374831230612374344230400704*I)/9671406556917033397649408, (-3935678233089814180000602553655565621193 + 2283744757287145199688061892165659502483*I)/1208925819614629174706176, (-2400210250844254483454290806930306285131 - 315571356806370996069052930302295432758205*I)/19342813113834066795298816, (13365917938215281056563183751673390817910 + 15911483133819801118348625831132324863881*I)/4835703278458516698824704, 3*(-215950551370668982657516660700301003897855 + 51684341999223632631602864028309400489378*I)/38685626227668133590597632, (20886089946811765149439844691320027184765 - 30806277083146786592790625980769214361844*I)/9671406556917033397649408, (562180634592713285745940856221105667874855 + 1031543963988260765153550559766662245114916*I)/77371252455336267181195264, (-65820625814810177122941758625652476012867 - 12429918324787060890804395323920477537595*I)/19342813113834066795298816, (319147848192012911298771180196635859221089 - 402403304933906769233365689834404519960394*I)/38685626227668133590597632, (23035615120921026080284733394359587955057 + 115351677687031786114651452775242461310624*I)/38685626227668133590597632, (-3426830634881892756966440108592579264936130 - 1022954961164128745603407283836365128598559*I)/309485009821345068724781056], +# [ (-192574788060137531023716449082856117537757 - 69222967328876859586831013062387845780692*I)/19342813113834066795298816, (2736383768828013152914815341491629299773262 - 2773252698016291897599353862072533475408743*I)/77371252455336267181195264, (-23280005281223837717773057436155921656805 + 214784953368021840006305033048142888879224*I)/19342813113834066795298816, (-3035247484028969580570400133318947903462326 - 2195168903335435855621328554626336958674325*I)/77371252455336267181195264, (984552428291526892214541708637840971548653 - 64006622534521425620714598573494988589378*I)/77371252455336267181195264, (-3070650452470333005276715136041262898509903 + 7286424705750810474140953092161794621989080*I)/154742504910672534362390528, (-147848877109756404594659513386972921139270 - 416306113044186424749331418059456047650861*I)/38685626227668133590597632, (55272118474097814260289392337160619494260781 + 7494019668394781211907115583302403519488058*I)/1237940039285380274899124224, (-581537886583682322424771088996959213068864 + 542191617758465339135308203815256798407429*I)/77371252455336267181195264, (-6422548983676355789975736799494791970390991 - 23524183982209004826464749309156698827737702*I)/618970019642690137449562112, 7*(180747195387024536886923192475064903482083 + 84352527693562434817771649853047924991804*I)/154742504910672534362390528, (-135485179036717001055310712747643466592387031 + 102346575226653028836678855697782273460527608*I)/4951760157141521099596496896], +# [ (3384238362616083147067025892852431152105 + 156724444932584900214919898954874618256*I)/604462909807314587353088, (-59558300950677430189587207338385764871866 + 114427143574375271097298201388331237478857*I)/4835703278458516698824704, (-1356835789870635633517710130971800616227 - 7023484098542340388800213478357340875410*I)/1208925819614629174706176, (234884918567993750975181728413524549575881 + 79757294640629983786895695752733890213506*I)/9671406556917033397649408, (-7632732774935120473359202657160313866419 + 2905452608512927560554702228553291839465*I)/1208925819614629174706176, (52291747908702842344842889809762246649489 - 520996778817151392090736149644507525892649*I)/19342813113834066795298816, (17472406829219127839967951180375981717322 + 23464704213841582137898905375041819568669*I)/4835703278458516698824704, (-911026971811893092350229536132730760943307 + 150799318130900944080399439626714846752360*I)/38685626227668133590597632, (26234457233977042811089020440646443590687 - 45650293039576452023692126463683727692890*I)/9671406556917033397649408, 3*(288348388717468992528382586652654351121357 + 454526517721403048270274049572136109264668*I)/77371252455336267181195264, (-91583492367747094223295011999405657956347 - 12704691128268298435362255538069612411331*I)/19342813113834066795298816, (411208730251327843849027957710164064354221 - 569898526380691606955496789378230959965898*I)/38685626227668133590597632], +# [ (27127513117071487872628354831658811211795 - 37765296987901990355760582016892124833857*I)/4835703278458516698824704, (1741779916057680444272938534338833170625435 + 3083041729779495966997526404685535449810378*I)/77371252455336267181195264, 3*(-60642236251815783728374561836962709533401 - 24630301165439580049891518846174101510744*I)/19342813113834066795298816, 3*(445885207364591681637745678755008757483408 - 350948497734812895032502179455610024541643*I)/38685626227668133590597632, (-47373295621391195484367368282471381775684 + 219122969294089357477027867028071400054973*I)/19342813113834066795298816, (-2801565819673198722993348253876353741520438 - 2250142129822658548391697042460298703335701*I)/77371252455336267181195264, (801448252275607253266997552356128790317119 - 50890367688077858227059515894356594900558*I)/77371252455336267181195264, (-5082187758525931944557763799137987573501207 + 11610432359082071866576699236013484487676124*I)/309485009821345068724781056, (-328925127096560623794883760398247685166830 - 643447969697471610060622160899409680422019*I)/77371252455336267181195264, 15*(2954944669454003684028194956846659916299765 + 33434406416888505837444969347824812608566*I)/1237940039285380274899124224, (-415749104352001509942256567958449835766827 + 479330966144175743357171151440020955412219*I)/77371252455336267181195264, 3*(-4639987285852134369449873547637372282914255 - 11994411888966030153196659207284951579243273*I)/1237940039285380274899124224], +# [ (-478846096206269117345024348666145495601 + 1249092488629201351470551186322814883283*I)/302231454903657293676544, (-17749319421930878799354766626365926894989 - 18264580106418628161818752318217357231971*I)/1208925819614629174706176, (2801110795431528876849623279389579072819 + 363258850073786330770713557775566973248*I)/604462909807314587353088, (-59053496693129013745775512127095650616252 + 78143588734197260279248498898321500167517*I)/4835703278458516698824704, (-283186724922498212468162690097101115349 - 6443437753863179883794497936345437398276*I)/1208925819614629174706176, (188799118826748909206887165661384998787543 + 84274736720556630026311383931055307398820*I)/9671406556917033397649408, (-5482217151670072904078758141270295025989 + 1818284338672191024475557065444481298568*I)/1208925819614629174706176, (56564463395350195513805521309731217952281 - 360208541416798112109946262159695452898431*I)/19342813113834066795298816, 11*(1259539805728870739006416869463689438068 + 1409136581547898074455004171305324917387*I)/4835703278458516698824704, 5*(-123701190701414554945251071190688818343325 + 30997157322590424677294553832111902279712*I)/38685626227668133590597632, (16130917381301373033736295883982414239781 - 32752041297570919727145380131926943374516*I)/9671406556917033397649408, (650301385108223834347093740500375498354925 + 899526407681131828596801223402866051809258*I)/77371252455336267181195264], +# [ (9011388245256140876590294262420614839483 + 8167917972423946282513000869327525382672*I)/1208925819614629174706176, (-426393174084720190126376382194036323028924 + 180692224825757525982858693158209545430621*I)/9671406556917033397649408, (24588556702197802674765733448108154175535 - 45091766022876486566421953254051868331066*I)/4835703278458516698824704, (1872113939365285277373877183750416985089691 + 3030392393733212574744122057679633775773130*I)/77371252455336267181195264, (-222173405538046189185754954524429864167549 - 75193157893478637039381059488387511299116*I)/19342813113834066795298816, (2670821320766222522963689317316937579844558 - 2645837121493554383087981511645435472169191*I)/77371252455336267181195264, 5*(-2100110309556476773796963197283876204940 + 41957457246479840487980315496957337371937*I)/19342813113834066795298816, (-5733743755499084165382383818991531258980593 - 3328949988392698205198574824396695027195732*I)/154742504910672534362390528, (707827994365259025461378911159398206329247 - 265730616623227695108042528694302299777294*I)/77371252455336267181195264, (-1442501604682933002895864804409322823788319 + 11504137805563265043376405214378288793343879*I)/309485009821345068724781056, (-56130472299445561499538726459719629522285 - 61117552419727805035810982426639329818864*I)/9671406556917033397649408, (39053692321126079849054272431599539429908717 - 10209127700342570953247177602860848130710666*I)/1237940039285380274899124224]]) + M = Matrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64, 1/4 - 5*I/16, 65/128 + 87*I/64], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128, 85/256 - 33*I/16, 805/128 + 2415*I/512], + [ 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64], + [ -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128], + [ 1 + I, -19/4 + 5*I/4, 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16], + [ 21/8 + I, -537/64 + 143*I/16, -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M**10 == Matrix(S('''[ + [ 7369525394972778926719607798014571861/604462909807314587353088 - 229284202061790301477392339912557559*I/151115727451828646838272, -19704281515163975949388435612632058035/1208925819614629174706176 + 14319858347987648723768698170712102887*I/302231454903657293676544, -3623281909451783042932142262164941211/604462909807314587353088 - 6039240602494288615094338643452320495*I/604462909807314587353088, 109260497799140408739847239685705357695/2417851639229258349412352 - 7427566006564572463236368211555511431*I/2417851639229258349412352, -16095803767674394244695716092817006641/2417851639229258349412352 + 10336681897356760057393429626719177583*I/1208925819614629174706176, -42207883340488041844332828574359769743/2417851639229258349412352 - 182332262671671273188016400290188468499*I/4835703278458516698824704], + [50566491050825573392726324995779608259/1208925819614629174706176 - 90047007594468146222002432884052362145*I/2417851639229258349412352, 74273703462900000967697427843983822011/1208925819614629174706176 + 265947522682943571171988741842776095421*I/1208925819614629174706176, -116900341394390200556829767923360888429/2417851639229258349412352 - 53153263356679268823910621474478756845*I/2417851639229258349412352, 195407378023867871243426523048612490249/1208925819614629174706176 - 1242417915995360200584837585002906728929*I/9671406556917033397649408, -863597594389821970177319682495878193/302231454903657293676544 + 476936100741548328800725360758734300481*I/9671406556917033397649408, -3154451590535653853562472176601754835575/19342813113834066795298816 - 232909875490506237386836489998407329215*I/2417851639229258349412352], + [ -1715444997702484578716037230949868543/302231454903657293676544 + 5009695651321306866158517287924120777*I/302231454903657293676544, -30551582497996879620371947949342101301/604462909807314587353088 - 7632518367986526187139161303331519629*I/151115727451828646838272, 312680739924495153190604170938220575/18889465931478580854784 - 108664334509328818765959789219208459*I/75557863725914323419136, -14693696966703036206178521686918865509/604462909807314587353088 + 72345386220900843930147151999899692401*I/1208925819614629174706176, -8218872496728882299722894680635296519/1208925819614629174706176 - 16776782833358893712645864791807664983*I/1208925819614629174706176, 143237839169380078671242929143670635137/2417851639229258349412352 + 2883817094806115974748882735218469447*I/2417851639229258349412352], + [ 3087979417831061365023111800749855987/151115727451828646838272 + 34441942370802869368851419102423997089*I/604462909807314587353088, -148309181940158040917731426845476175667/604462909807314587353088 - 263987151804109387844966835369350904919*I/9671406556917033397649408, 50259518594816377378747711930008883165/1208925819614629174706176 - 95713974916869240305450001443767979653*I/2417851639229258349412352, 153466447023875527996457943521467271119/2417851639229258349412352 + 517285524891117105834922278517084871349*I/2417851639229258349412352, -29184653615412989036678939366291205575/604462909807314587353088 - 27551322282526322041080173287022121083*I/1208925819614629174706176, 196404220110085511863671393922447671649/1208925819614629174706176 - 1204712019400186021982272049902206202145*I/9671406556917033397649408], + [ -2632581805949645784625606590600098779/151115727451828646838272 - 589957435912868015140272627522612771*I/37778931862957161709568, 26727850893953715274702844733506310247/302231454903657293676544 - 10825791956782128799168209600694020481*I/302231454903657293676544, -1036348763702366164044671908440791295/151115727451828646838272 + 3188624571414467767868303105288107375*I/151115727451828646838272, -36814959939970644875593411585393242449/604462909807314587353088 - 18457555789119782404850043842902832647*I/302231454903657293676544, 12454491297984637815063964572803058647/604462909807314587353088 - 340489532842249733975074349495329171*I/302231454903657293676544, -19547211751145597258386735573258916681/604462909807314587353088 + 87299583775782199663414539883938008933*I/1208925819614629174706176], + [ -40281994229560039213253423262678393183/604462909807314587353088 - 2939986850065527327299273003299736641*I/604462909807314587353088, 331940684638052085845743020267462794181/2417851639229258349412352 - 284574901963624403933361315517248458969*I/1208925819614629174706176, 6453843623051745485064693628073010961/302231454903657293676544 + 36062454107479732681350914931391590957*I/604462909807314587353088, -147665869053634695632880753646441962067/604462909807314587353088 - 305987938660447291246597544085345123927*I/9671406556917033397649408, 107821369195275772166593879711259469423/2417851639229258349412352 - 11645185518211204108659001435013326687*I/302231454903657293676544, 64121228424717666402009446088588091619/1208925819614629174706176 + 265557133337095047883844369272389762133*I/1208925819614629174706176]]''')) + +def test_issue_17247_expression_blowup_5(): + M = Matrix(6, 6, lambda i, j: 1 + (-1)**(i+j)*I) + with dotprodsimp(True): + assert M.charpoly('x') == PurePoly(x**6 + (-6 - 6*I)*x**5 + 36*I*x**4, x, domain='EX') + +def test_issue_17247_expression_blowup_6(): + M = Matrix(8, 8, [x+i for i in range (64)]) + with dotprodsimp(True): + assert M.det('bareiss') == 0 + +def test_issue_17247_expression_blowup_7(): + M = Matrix(6, 6, lambda i, j: 1 + (-1)**(i+j)*I) + with dotprodsimp(True): + assert M.det('berkowitz') == 0 + +def test_issue_17247_expression_blowup_8(): + M = Matrix(8, 8, [x+i for i in range (64)]) + with dotprodsimp(True): + assert M.det('lu') == 0 + +def test_issue_17247_expression_blowup_9(): + M = Matrix(8, 8, [x+i for i in range (64)]) + with dotprodsimp(True): + assert M.rref() == (Matrix([ + [1, 0, -1, -2, -3, -4, -5, -6], + [0, 1, 2, 3, 4, 5, 6, 7], + [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, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0]]), (0, 1)) + +def test_issue_17247_expression_blowup_10(): + M = Matrix(6, 6, lambda i, j: 1 + (-1)**(i+j)*I) + with dotprodsimp(True): + assert M.cofactor(0, 0) == 0 + +def test_issue_17247_expression_blowup_11(): + M = Matrix(6, 6, lambda i, j: 1 + (-1)**(i+j)*I) + with dotprodsimp(True): + assert M.cofactor_matrix() == Matrix(6, 6, [0]*36) + +def test_issue_17247_expression_blowup_12(): + M = Matrix(6, 6, lambda i, j: 1 + (-1)**(i+j)*I) + with dotprodsimp(True): + assert M.eigenvals() == {6: 1, 6*I: 1, 0: 4} + +def test_issue_17247_expression_blowup_13(): + M = Matrix([ + [ 0, 1 - x, x + 1, 1 - x], + [1 - x, x + 1, 0, x + 1], + [ 0, 1 - x, x + 1, 1 - x], + [ 0, 0, 1 - x, 0]]) + + ev = M.eigenvects() + assert ev[0] == (0, 2, [Matrix([0, -1, 0, 1])]) + assert ev[1][0] == x - sqrt(2)*(x - 1) + 1 + assert ev[1][1] == 1 + assert ev[1][2][0].expand(deep=False, numer=True) == Matrix([ + [(-x + sqrt(2)*(x - 1) - 1)/(x - 1)], + [-4*x/(x**2 - 2*x + 1) + (x + 1)*(x - sqrt(2)*(x - 1) + 1)/(x**2 - 2*x + 1)], + [(-x + sqrt(2)*(x - 1) - 1)/(x - 1)], + [1] + ]) + + assert ev[2][0] == x + sqrt(2)*(x - 1) + 1 + assert ev[2][1] == 1 + assert ev[2][2][0].expand(deep=False, numer=True) == Matrix([ + [(-x - sqrt(2)*(x - 1) - 1)/(x - 1)], + [-4*x/(x**2 - 2*x + 1) + (x + 1)*(x + sqrt(2)*(x - 1) + 1)/(x**2 - 2*x + 1)], + [(-x - sqrt(2)*(x - 1) - 1)/(x - 1)], + [1] + ]) + + +def test_issue_17247_expression_blowup_14(): + M = Matrix(8, 8, ([1+x, 1-x]*4 + [1-x, 1+x]*4)*4) + with dotprodsimp(True): + assert M.echelon_form() == Matrix([ + [x + 1, 1 - x, x + 1, 1 - x, x + 1, 1 - x, x + 1, 1 - x], + [ 0, 4*x, 0, 4*x, 0, 4*x, 0, 4*x], + [ 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, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0]]) + +def test_issue_17247_expression_blowup_15(): + M = Matrix(8, 8, ([1+x, 1-x]*4 + [1-x, 1+x]*4)*4) + with dotprodsimp(True): + assert M.rowspace() == [Matrix([[x + 1, 1 - x, x + 1, 1 - x, x + 1, 1 - x, x + 1, 1 - x]]), Matrix([[0, 4*x, 0, 4*x, 0, 4*x, 0, 4*x]])] + +def test_issue_17247_expression_blowup_16(): + M = Matrix(8, 8, ([1+x, 1-x]*4 + [1-x, 1+x]*4)*4) + with dotprodsimp(True): + assert M.columnspace() == [Matrix([[x + 1],[1 - x],[x + 1],[1 - x],[x + 1],[1 - x],[x + 1],[1 - x]]), Matrix([[1 - x],[x + 1],[1 - x],[x + 1],[1 - x],[x + 1],[1 - x],[x + 1]])] + +def test_issue_17247_expression_blowup_17(): + M = Matrix(8, 8, [x+i for i in range (64)]) + with dotprodsimp(True): + assert M.nullspace() == [ + Matrix([[1],[-2],[1],[0],[0],[0],[0],[0]]), + Matrix([[2],[-3],[0],[1],[0],[0],[0],[0]]), + Matrix([[3],[-4],[0],[0],[1],[0],[0],[0]]), + Matrix([[4],[-5],[0],[0],[0],[1],[0],[0]]), + Matrix([[5],[-6],[0],[0],[0],[0],[1],[0]]), + Matrix([[6],[-7],[0],[0],[0],[0],[0],[1]])] + +def test_issue_17247_expression_blowup_18(): + M = Matrix(6, 6, ([1+x, 1-x]*3 + [1-x, 1+x]*3)*3) + with dotprodsimp(True): + assert not M.is_nilpotent() + +def test_issue_17247_expression_blowup_19(): + M = Matrix(S('''[ + [ -3/4, 0, 1/4 + I/2, 0], + [ 0, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 1/2 - I, 0, 0, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert not M.is_diagonalizable() + +def test_issue_17247_expression_blowup_20(): + M = Matrix([ + [x + 1, 1 - x, 0, 0], + [1 - x, x + 1, 0, x + 1], + [ 0, 1 - x, x + 1, 0], + [ 0, 0, 0, x + 1]]) + with dotprodsimp(True): + assert M.diagonalize() == (Matrix([ + [1, 1, 0, (x + 1)/(x - 1)], + [1, -1, 0, 0], + [1, 1, 1, 0], + [0, 0, 0, 1]]), + Matrix([ + [2, 0, 0, 0], + [0, 2*x, 0, 0], + [0, 0, x + 1, 0], + [0, 0, 0, x + 1]])) + +def test_issue_17247_expression_blowup_21(): + M = Matrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 0, 0], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 0, 9/4 + 55*I/16, 2473/256 + 137*I/64, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.inv(method='GE') == Matrix(S('''[ + [-26194832/3470993 - 31733264*I/3470993, 156352/3470993 + 10325632*I/3470993, 0, -7741283181072/3306971225785 + 2999007604624*I/3306971225785], + [4408224/3470993 - 9675328*I/3470993, -2422272/3470993 + 1523712*I/3470993, 0, -1824666489984/3306971225785 - 1401091949952*I/3306971225785], + [-26406945676288/22270005630769 + 10245925485056*I/22270005630769, 7453523312640/22270005630769 + 1601616519168*I/22270005630769, 633088/6416033 - 140288*I/6416033, 872209227109521408/21217636514687010905 + 6066405081802389504*I/21217636514687010905], + [0, 0, 0, -11328/952745 + 87616*I/952745]]''')) + +def test_issue_17247_expression_blowup_22(): + M = Matrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 0, 0], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 0, 9/4 + 55*I/16, 2473/256 + 137*I/64, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.inv(method='LU') == Matrix(S('''[ + [-26194832/3470993 - 31733264*I/3470993, 156352/3470993 + 10325632*I/3470993, 0, -7741283181072/3306971225785 + 2999007604624*I/3306971225785], + [4408224/3470993 - 9675328*I/3470993, -2422272/3470993 + 1523712*I/3470993, 0, -1824666489984/3306971225785 - 1401091949952*I/3306971225785], + [-26406945676288/22270005630769 + 10245925485056*I/22270005630769, 7453523312640/22270005630769 + 1601616519168*I/22270005630769, 633088/6416033 - 140288*I/6416033, 872209227109521408/21217636514687010905 + 6066405081802389504*I/21217636514687010905], + [0, 0, 0, -11328/952745 + 87616*I/952745]]''')) + +def test_issue_17247_expression_blowup_23(): + M = Matrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 0, 0], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 0, 9/4 + 55*I/16, 2473/256 + 137*I/64, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.inv(method='ADJ').expand() == Matrix(S('''[ + [-26194832/3470993 - 31733264*I/3470993, 156352/3470993 + 10325632*I/3470993, 0, -7741283181072/3306971225785 + 2999007604624*I/3306971225785], + [4408224/3470993 - 9675328*I/3470993, -2422272/3470993 + 1523712*I/3470993, 0, -1824666489984/3306971225785 - 1401091949952*I/3306971225785], + [-26406945676288/22270005630769 + 10245925485056*I/22270005630769, 7453523312640/22270005630769 + 1601616519168*I/22270005630769, 633088/6416033 - 140288*I/6416033, 872209227109521408/21217636514687010905 + 6066405081802389504*I/21217636514687010905], + [0, 0, 0, -11328/952745 + 87616*I/952745]]''')) + +def test_issue_17247_expression_blowup_24(): + M = SparseMatrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 0, 0], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 0, 9/4 + 55*I/16, 2473/256 + 137*I/64, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.inv(method='CH') == Matrix(S('''[ + [-26194832/3470993 - 31733264*I/3470993, 156352/3470993 + 10325632*I/3470993, 0, -7741283181072/3306971225785 + 2999007604624*I/3306971225785], + [4408224/3470993 - 9675328*I/3470993, -2422272/3470993 + 1523712*I/3470993, 0, -1824666489984/3306971225785 - 1401091949952*I/3306971225785], + [-26406945676288/22270005630769 + 10245925485056*I/22270005630769, 7453523312640/22270005630769 + 1601616519168*I/22270005630769, 633088/6416033 - 140288*I/6416033, 872209227109521408/21217636514687010905 + 6066405081802389504*I/21217636514687010905], + [0, 0, 0, -11328/952745 + 87616*I/952745]]''')) + +def test_issue_17247_expression_blowup_25(): + M = SparseMatrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 0, 0], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 0, 9/4 + 55*I/16, 2473/256 + 137*I/64, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.inv(method='LDL') == Matrix(S('''[ + [-26194832/3470993 - 31733264*I/3470993, 156352/3470993 + 10325632*I/3470993, 0, -7741283181072/3306971225785 + 2999007604624*I/3306971225785], + [4408224/3470993 - 9675328*I/3470993, -2422272/3470993 + 1523712*I/3470993, 0, -1824666489984/3306971225785 - 1401091949952*I/3306971225785], + [-26406945676288/22270005630769 + 10245925485056*I/22270005630769, 7453523312640/22270005630769 + 1601616519168*I/22270005630769, 633088/6416033 - 140288*I/6416033, 872209227109521408/21217636514687010905 + 6066405081802389504*I/21217636514687010905], + [0, 0, 0, -11328/952745 + 87616*I/952745]]''')) + +def test_issue_17247_expression_blowup_26(): + M = Matrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64, 1/4 - 5*I/16, 65/128 + 87*I/64, -9/32 - I/16, 183/256 - 97*I/128], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128, 85/256 - 33*I/16, 805/128 + 2415*I/512, -219/128 + 115*I/256, 6301/4096 - 6609*I/1024], + [ 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64, 1/4 - 5*I/16, 65/128 + 87*I/64], + [ -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128, 85/256 - 33*I/16, 805/128 + 2415*I/512], + [ 1 + I, -19/4 + 5*I/4, 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64], + [ 21/8 + I, -537/64 + 143*I/16, -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128], + [ -2, 17/4 - 13*I/2, 1 + I, -19/4 + 5*I/4, 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16], + [ 1/4 + 13*I/4, -825/64 - 147*I/32, 21/8 + I, -537/64 + 143*I/16, -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.rank() == 4 + +def test_issue_17247_expression_blowup_27(): + M = Matrix([ + [ 0, 1 - x, x + 1, 1 - x], + [1 - x, x + 1, 0, x + 1], + [ 0, 1 - x, x + 1, 1 - x], + [ 0, 0, 1 - x, 0]]) + with dotprodsimp(True): + P, J = M.jordan_form() + assert P.expand() == Matrix(S('''[ + [ 0, 4*x/(x**2 - 2*x + 1), -(-17*x**4 + 12*sqrt(2)*x**4 - 4*sqrt(2)*x**3 + 6*x**3 - 6*x - 4*sqrt(2)*x + 12*sqrt(2) + 17)/(-7*x**4 + 5*sqrt(2)*x**4 - 6*sqrt(2)*x**3 + 8*x**3 - 2*x**2 + 8*x + 6*sqrt(2)*x - 5*sqrt(2) - 7), -(12*sqrt(2)*x**4 + 17*x**4 - 6*x**3 - 4*sqrt(2)*x**3 - 4*sqrt(2)*x + 6*x - 17 + 12*sqrt(2))/(7*x**4 + 5*sqrt(2)*x**4 - 6*sqrt(2)*x**3 - 8*x**3 + 2*x**2 - 8*x + 6*sqrt(2)*x - 5*sqrt(2) + 7)], + [x - 1, x/(x - 1) + 1/(x - 1), (-7*x**3 + 5*sqrt(2)*x**3 - x**2 + sqrt(2)*x**2 - sqrt(2)*x - x - 5*sqrt(2) - 7)/(-3*x**3 + 2*sqrt(2)*x**3 - 2*sqrt(2)*x**2 + 3*x**2 + 2*sqrt(2)*x + 3*x - 3 - 2*sqrt(2)), (7*x**3 + 5*sqrt(2)*x**3 + x**2 + sqrt(2)*x**2 - sqrt(2)*x + x - 5*sqrt(2) + 7)/(2*sqrt(2)*x**3 + 3*x**3 - 3*x**2 - 2*sqrt(2)*x**2 - 3*x + 2*sqrt(2)*x - 2*sqrt(2) + 3)], + [ 0, 1, -(-3*x**2 + 2*sqrt(2)*x**2 + 2*x - 3 - 2*sqrt(2))/(-x**2 + sqrt(2)*x**2 - 2*sqrt(2)*x + 1 + sqrt(2)), -(2*sqrt(2)*x**2 + 3*x**2 - 2*x - 2*sqrt(2) + 3)/(x**2 + sqrt(2)*x**2 - 2*sqrt(2)*x - 1 + sqrt(2))], + [1 - x, 0, 1, 1]]''')).expand() + assert J == Matrix(S('''[ + [0, 1, 0, 0], + [0, 0, 0, 0], + [0, 0, x - sqrt(2)*(x - 1) + 1, 0], + [0, 0, 0, x + sqrt(2)*(x - 1) + 1]]''')) + +def test_issue_17247_expression_blowup_28(): + M = Matrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 0, 0], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 0, 9/4 + 55*I/16, 2473/256 + 137*I/64, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.singular_values() == S('''[ + sqrt(14609315/131072 + sqrt(64789115132571/2147483648 - 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3) + 76627253330829751075/(35184372088832*sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))) - 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)))/2 + sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))/2), + sqrt(14609315/131072 - sqrt(64789115132571/2147483648 - 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3) + 76627253330829751075/(35184372088832*sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))) - 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)))/2 + sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))/2), + sqrt(14609315/131072 - sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))/2 + sqrt(64789115132571/2147483648 - 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3) - 76627253330829751075/(35184372088832*sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))) - 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)))/2), + sqrt(14609315/131072 - sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))/2 - sqrt(64789115132571/2147483648 - 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3) - 76627253330829751075/(35184372088832*sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))) - 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)))/2)]''') + + +def test_issue_16823(): + # This still needs to be fixed if not using dotprodsimp. + M = Matrix(S('''[ + [1+I,-19/4+5/4*I,1/2-I,9/4+55/16*I,-3/4,45/32-37/16*I,1/4+1/2*I,-129/64-9/64*I,1/4-5/16*I,65/128+87/64*I,-9/32-1/16*I,183/256-97/128*I,3/64+13/64*I,-23/32-59/256*I,15/128-3/32*I,19/256+551/1024*I], + [21/8+I,-537/64+143/16*I,-5/8-39/16*I,2473/256+137/64*I,-149/64+49/32*I,-177/128-1369/128*I,125/64+87/64*I,-2063/256+541/128*I,85/256-33/16*I,805/128+2415/512*I,-219/128+115/256*I,6301/4096-6609/1024*I,119/128+143/128*I,-10879/2048+4343/4096*I,129/256-549/512*I,42533/16384+29103/8192*I], + [-2,17/4-13/2*I,1+I,-19/4+5/4*I,1/2-I,9/4+55/16*I,-3/4,45/32-37/16*I,1/4+1/2*I,-129/64-9/64*I,1/4-5/16*I,65/128+87/64*I,-9/32-1/16*I,183/256-97/128*I,3/64+13/64*I,-23/32-59/256*I], + [1/4+13/4*I,-825/64-147/32*I,21/8+I,-537/64+143/16*I,-5/8-39/16*I,2473/256+137/64*I,-149/64+49/32*I,-177/128-1369/128*I,125/64+87/64*I,-2063/256+541/128*I,85/256-33/16*I,805/128+2415/512*I,-219/128+115/256*I,6301/4096-6609/1024*I,119/128+143/128*I,-10879/2048+4343/4096*I], + [-4*I,27/2+6*I,-2,17/4-13/2*I,1+I,-19/4+5/4*I,1/2-I,9/4+55/16*I,-3/4,45/32-37/16*I,1/4+1/2*I,-129/64-9/64*I,1/4-5/16*I,65/128+87/64*I,-9/32-1/16*I,183/256-97/128*I], + [1/4+5/2*I,-23/8-57/16*I,1/4+13/4*I,-825/64-147/32*I,21/8+I,-537/64+143/16*I,-5/8-39/16*I,2473/256+137/64*I,-149/64+49/32*I,-177/128-1369/128*I,125/64+87/64*I,-2063/256+541/128*I,85/256-33/16*I,805/128+2415/512*I,-219/128+115/256*I,6301/4096-6609/1024*I], + [-4,9-5*I,-4*I,27/2+6*I,-2,17/4-13/2*I,1+I,-19/4+5/4*I,1/2-I,9/4+55/16*I,-3/4,45/32-37/16*I,1/4+1/2*I,-129/64-9/64*I,1/4-5/16*I,65/128+87/64*I], + [-2*I,119/8+29/4*I,1/4+5/2*I,-23/8-57/16*I,1/4+13/4*I,-825/64-147/32*I,21/8+I,-537/64+143/16*I,-5/8-39/16*I,2473/256+137/64*I,-149/64+49/32*I,-177/128-1369/128*I,125/64+87/64*I,-2063/256+541/128*I,85/256-33/16*I,805/128+2415/512*I], + [0,-6,-4,9-5*I,-4*I,27/2+6*I,-2,17/4-13/2*I,1+I,-19/4+5/4*I,1/2-I,9/4+55/16*I,-3/4,45/32-37/16*I,1/4+1/2*I,-129/64-9/64*I], + [1,-9/4+3*I,-2*I,119/8+29/4*I,1/4+5/2*I,-23/8-57/16*I,1/4+13/4*I,-825/64-147/32*I,21/8+I,-537/64+143/16*I,-5/8-39/16*I,2473/256+137/64*I,-149/64+49/32*I,-177/128-1369/128*I,125/64+87/64*I,-2063/256+541/128*I], + [0,-4*I,0,-6,-4,9-5*I,-4*I,27/2+6*I,-2,17/4-13/2*I,1+I,-19/4+5/4*I,1/2-I,9/4+55/16*I,-3/4,45/32-37/16*I], + [0,1/4+1/2*I,1,-9/4+3*I,-2*I,119/8+29/4*I,1/4+5/2*I,-23/8-57/16*I,1/4+13/4*I,-825/64-147/32*I,21/8+I,-537/64+143/16*I,-5/8-39/16*I,2473/256+137/64*I,-149/64+49/32*I,-177/128-1369/128*I]]''')) + with dotprodsimp(True): + assert M.rank() == 8 + + +def test_issue_18531(): + # solve_linear_system still needs fixing but the rref works. + M = Matrix([ + [1, 1, 1, 1, 1, 0, 1, 0, 0], + [1 + sqrt(2), -1 + sqrt(2), 1 - sqrt(2), -sqrt(2) - 1, 1, 1, -1, 1, 1], + [-5 + 2*sqrt(2), -5 - 2*sqrt(2), -5 - 2*sqrt(2), -5 + 2*sqrt(2), -7, 2, -7, -2, 0], + [-3*sqrt(2) - 1, 1 - 3*sqrt(2), -1 + 3*sqrt(2), 1 + 3*sqrt(2), -7, -5, 7, -5, 3], + [7 - 4*sqrt(2), 4*sqrt(2) + 7, 4*sqrt(2) + 7, 7 - 4*sqrt(2), 7, -12, 7, 12, 0], + [-1 + 3*sqrt(2), 1 + 3*sqrt(2), -3*sqrt(2) - 1, 1 - 3*sqrt(2), 7, -5, -7, -5, 3], + [-3 + 2*sqrt(2), -3 - 2*sqrt(2), -3 - 2*sqrt(2), -3 + 2*sqrt(2), -1, 2, -1, -2, 0], + [1 - sqrt(2), -sqrt(2) - 1, 1 + sqrt(2), -1 + sqrt(2), -1, 1, 1, 1, 1] + ]) + with dotprodsimp(True): + assert M.rref() == (Matrix([ + [1, 0, 0, 0, 0, 0, 0, 0, S(1)/2], + [0, 1, 0, 0, 0, 0, 0, 0, -S(1)/2], + [0, 0, 1, 0, 0, 0, 0, 0, S(1)/2], + [0, 0, 0, 1, 0, 0, 0, 0, -S(1)/2], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, -S(1)/2], + [0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, -S(1)/2]]), (0, 1, 2, 3, 4, 5, 6, 7)) + + +def test_creation(): + raises(ValueError, lambda: Matrix(5, 5, range(20))) + raises(ValueError, lambda: Matrix(5, -1, [])) + raises(IndexError, lambda: Matrix((1, 2))[2]) + with raises(IndexError): + Matrix((1, 2))[3] = 5 + + assert Matrix() == Matrix([]) == Matrix(0, 0, []) + assert Matrix([[]]) == Matrix(1, 0, []) + assert Matrix([[], []]) == Matrix(2, 0, []) + + # anything used to be allowed in a matrix + with warns_deprecated_sympy(): + assert Matrix([[[1], (2,)]]).tolist() == [[[1], (2,)]] + with warns_deprecated_sympy(): + assert Matrix([[[1], (2,)]]).T.tolist() == [[[1]], [(2,)]] + M = Matrix([[0]]) + with warns_deprecated_sympy(): + M[0, 0] = S.EmptySet + + a = Matrix([[x, 0], [0, 0]]) + m = a + assert m.cols == m.rows + assert m.cols == 2 + assert m[:] == [x, 0, 0, 0] + + b = Matrix(2, 2, [x, 0, 0, 0]) + m = b + assert m.cols == m.rows + assert m.cols == 2 + assert m[:] == [x, 0, 0, 0] + + assert a == b + + assert Matrix(b) == b + + c23 = Matrix(2, 3, range(1, 7)) + c13 = Matrix(1, 3, range(7, 10)) + c = Matrix([c23, c13]) + assert c.cols == 3 + assert c.rows == 3 + assert c[:] == [1, 2, 3, 4, 5, 6, 7, 8, 9] + + assert Matrix(eye(2)) == eye(2) + assert ImmutableMatrix(ImmutableMatrix(eye(2))) == ImmutableMatrix(eye(2)) + assert ImmutableMatrix(c) == c.as_immutable() + assert Matrix(ImmutableMatrix(c)) == ImmutableMatrix(c).as_mutable() + + assert c is not Matrix(c) + + dat = [[ones(3,2), ones(3,3)*2], [ones(2,3)*3, ones(2,2)*4]] + M = Matrix(dat) + assert M == Matrix([ + [1, 1, 2, 2, 2], + [1, 1, 2, 2, 2], + [1, 1, 2, 2, 2], + [3, 3, 3, 4, 4], + [3, 3, 3, 4, 4]]) + assert M.tolist() != dat + # keep block form if evaluate=False + assert Matrix(dat, evaluate=False).tolist() == dat + A = MatrixSymbol("A", 2, 2) + dat = [ones(2), A] + assert Matrix(dat) == Matrix([ + [ 1, 1], + [ 1, 1], + [A[0, 0], A[0, 1]], + [A[1, 0], A[1, 1]]]) + with warns_deprecated_sympy(): + assert Matrix(dat, evaluate=False).tolist() == [[i] for i in dat] + + # 0-dim tolerance + assert Matrix([ones(2), ones(0)]) == Matrix([ones(2)]) + raises(ValueError, lambda: Matrix([ones(2), ones(0, 3)])) + raises(ValueError, lambda: Matrix([ones(2), ones(3, 0)])) + + # mix of Matrix and iterable + M = Matrix([[1, 2], [3, 4]]) + M2 = Matrix([M, (5, 6)]) + assert M2 == Matrix([[1, 2], [3, 4], [5, 6]]) + + +def test_irregular_block(): + assert Matrix.irregular(3, ones(2,1), ones(3,3)*2, ones(2,2)*3, + ones(1,1)*4, ones(2,2)*5, ones(1,2)*6, ones(1,2)*7) == Matrix([ + [1, 2, 2, 2, 3, 3], + [1, 2, 2, 2, 3, 3], + [4, 2, 2, 2, 5, 5], + [6, 6, 7, 7, 5, 5]]) + + +def test_tolist(): + lst = [[S.One, S.Half, x*y, S.Zero], [x, y, z, x**2], [y, -S.One, z*x, 3]] + m = Matrix(lst) + assert m.tolist() == lst + + +def test_as_mutable(): + assert zeros(0, 3).as_mutable() == zeros(0, 3) + assert zeros(0, 3).as_immutable() == ImmutableMatrix(zeros(0, 3)) + assert zeros(3, 0).as_immutable() == ImmutableMatrix(zeros(3, 0)) + + +def test_slicing(): + m0 = eye(4) + assert m0[:3, :3] == eye(3) + assert m0[2:4, 0:2] == zeros(2) + + m1 = Matrix(3, 3, lambda i, j: i + j) + assert m1[0, :] == Matrix(1, 3, (0, 1, 2)) + assert m1[1:3, 1] == Matrix(2, 1, (2, 3)) + + m2 = Matrix([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]]) + assert m2[:, -1] == Matrix(4, 1, [3, 7, 11, 15]) + assert m2[-2:, :] == Matrix([[8, 9, 10, 11], [12, 13, 14, 15]]) + + +def test_submatrix_assignment(): + m = zeros(4) + m[2:4, 2:4] = eye(2) + assert m == Matrix(((0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 1, 0), + (0, 0, 0, 1))) + m[:2, :2] = eye(2) + assert m == eye(4) + m[:, 0] = Matrix(4, 1, (1, 2, 3, 4)) + assert m == Matrix(((1, 0, 0, 0), + (2, 1, 0, 0), + (3, 0, 1, 0), + (4, 0, 0, 1))) + m[:, :] = zeros(4) + assert m == zeros(4) + m[:, :] = [(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12), (13, 14, 15, 16)] + assert m == Matrix(((1, 2, 3, 4), + (5, 6, 7, 8), + (9, 10, 11, 12), + (13, 14, 15, 16))) + m[:2, 0] = [0, 0] + assert m == Matrix(((0, 2, 3, 4), + (0, 6, 7, 8), + (9, 10, 11, 12), + (13, 14, 15, 16))) + + +def test_extract(): + m = Matrix(4, 3, lambda i, j: i*3 + j) + assert m.extract([0, 1, 3], [0, 1]) == Matrix(3, 2, [0, 1, 3, 4, 9, 10]) + assert m.extract([0, 3], [0, 0, 2]) == Matrix(2, 3, [0, 0, 2, 9, 9, 11]) + assert m.extract(range(4), range(3)) == m + raises(IndexError, lambda: m.extract([4], [0])) + raises(IndexError, lambda: m.extract([0], [3])) + + +def test_reshape(): + m0 = eye(3) + assert m0.reshape(1, 9) == Matrix(1, 9, (1, 0, 0, 0, 1, 0, 0, 0, 1)) + m1 = Matrix(3, 4, lambda i, j: i + j) + assert m1.reshape( + 4, 3) == Matrix(((0, 1, 2), (3, 1, 2), (3, 4, 2), (3, 4, 5))) + assert m1.reshape(2, 6) == Matrix(((0, 1, 2, 3, 1, 2), (3, 4, 2, 3, 4, 5))) + + +def test_applyfunc(): + m0 = eye(3) + assert m0.applyfunc(lambda x: 2*x) == eye(3)*2 + assert m0.applyfunc(lambda x: 0) == zeros(3) + + +def test_expand(): + m0 = Matrix([[x*(x + y), 2], [((x + y)*y)*x, x*(y + x*(x + y))]]) + # Test if expand() returns a matrix + m1 = m0.expand() + assert m1 == Matrix( + [[x*y + x**2, 2], [x*y**2 + y*x**2, x*y + y*x**2 + x**3]]) + + a = Symbol('a', real=True) + + assert Matrix([exp(I*a)]).expand(complex=True) == \ + Matrix([cos(a) + I*sin(a)]) + + assert Matrix([[0, 1, 2], [0, 0, -1], [0, 0, 0]]).exp() == Matrix([ + [1, 1, Rational(3, 2)], + [0, 1, -1], + [0, 0, 1]] + ) + +def test_refine(): + m0 = Matrix([[Abs(x)**2, sqrt(x**2)], + [sqrt(x**2)*Abs(y)**2, sqrt(y**2)*Abs(x)**2]]) + m1 = m0.refine(Q.real(x) & Q.real(y)) + assert m1 == Matrix([[x**2, Abs(x)], [y**2*Abs(x), x**2*Abs(y)]]) + + m1 = m0.refine(Q.positive(x) & Q.positive(y)) + assert m1 == Matrix([[x**2, x], [x*y**2, x**2*y]]) + + m1 = m0.refine(Q.negative(x) & Q.negative(y)) + assert m1 == Matrix([[x**2, -x], [-x*y**2, -x**2*y]]) + +def test_random(): + M = randMatrix(3, 3) + M = randMatrix(3, 3, seed=3) + assert M == randMatrix(3, 3, seed=3) + + M = randMatrix(3, 4, 0, 150) + M = randMatrix(3, seed=4, symmetric=True) + assert M == randMatrix(3, seed=4, symmetric=True) + + S = M.copy() + S.simplify() + assert S == M # doesn't fail when elements are Numbers, not int + + rng = random.Random(4) + assert M == randMatrix(3, symmetric=True, prng=rng) + + # Ensure symmetry + for size in (10, 11): # Test odd and even + for percent in (100, 70, 30): + M = randMatrix(size, symmetric=True, percent=percent, prng=rng) + assert M == M.T + + M = randMatrix(10, min=1, percent=70) + zero_count = 0 + for i in range(M.shape[0]): + for j in range(M.shape[1]): + if M[i, j] == 0: + zero_count += 1 + assert zero_count == 30 + +def test_inverse(): + A = eye(4) + assert A.inv() == eye(4) + assert A.inv(method="LU") == eye(4) + assert A.inv(method="ADJ") == eye(4) + assert A.inv(method="CH") == eye(4) + assert A.inv(method="LDL") == eye(4) + assert A.inv(method="QR") == eye(4) + A = Matrix([[2, 3, 5], + [3, 6, 2], + [8, 3, 6]]) + Ainv = A.inv() + assert A*Ainv == eye(3) + assert A.inv(method="LU") == Ainv + assert A.inv(method="ADJ") == Ainv + assert A.inv(method="CH") == Ainv + assert A.inv(method="LDL") == Ainv + assert A.inv(method="QR") == Ainv + + AA = Matrix([[0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], + [1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0], + [1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1], + [1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0], + [1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1], + [0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0], + [1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1], + [0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1], + [1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0], + [1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1], + [1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0], + [0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1], + [0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1], + [1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1], + [0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1], + [0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0], + [0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0]]) + assert AA.inv(method="BLOCK") * AA == eye(AA.shape[0]) + # test that immutability is not a problem + cls = ImmutableMatrix + m = cls([[48, 49, 31], + [ 9, 71, 94], + [59, 28, 65]]) + assert all(type(m.inv(s)) is cls for s in 'GE ADJ LU CH LDL QR'.split()) + cls = ImmutableSparseMatrix + m = cls([[48, 49, 31], + [ 9, 71, 94], + [59, 28, 65]]) + assert all(type(m.inv(s)) is cls for s in 'GE ADJ LU CH LDL QR'.split()) + + +def test_jacobian_hessian(): + L = Matrix(1, 2, [x**2*y, 2*y**2 + x*y]) + syms = [x, y] + assert L.jacobian(syms) == Matrix([[2*x*y, x**2], [y, 4*y + x]]) + + L = Matrix(1, 2, [x, x**2*y**3]) + assert L.jacobian(syms) == Matrix([[1, 0], [2*x*y**3, x**2*3*y**2]]) + + f = x**2*y + syms = [x, y] + assert hessian(f, syms) == Matrix([[2*y, 2*x], [2*x, 0]]) + + f = x**2*y**3 + assert hessian(f, syms) == \ + Matrix([[2*y**3, 6*x*y**2], [6*x*y**2, 6*x**2*y]]) + + f = z + x*y**2 + g = x**2 + 2*y**3 + ans = Matrix([[0, 2*y], + [2*y, 2*x]]) + assert ans == hessian(f, Matrix([x, y])) + assert ans == hessian(f, Matrix([x, y]).T) + assert hessian(f, (y, x), [g]) == Matrix([ + [ 0, 6*y**2, 2*x], + [6*y**2, 2*x, 2*y], + [ 2*x, 2*y, 0]]) + + +def test_wronskian(): + assert wronskian([cos(x), sin(x)], x) == cos(x)**2 + sin(x)**2 + assert wronskian([exp(x), exp(2*x)], x) == exp(3*x) + assert wronskian([exp(x), x], x) == exp(x) - x*exp(x) + assert wronskian([1, x, x**2], x) == 2 + w1 = -6*exp(x)*sin(x)*x + 6*cos(x)*exp(x)*x**2 - 6*exp(x)*cos(x)*x - \ + exp(x)*cos(x)*x**3 + exp(x)*sin(x)*x**3 + assert wronskian([exp(x), cos(x), x**3], x).expand() == w1 + assert wronskian([exp(x), cos(x), x**3], x, method='berkowitz').expand() \ + == w1 + w2 = -x**3*cos(x)**2 - x**3*sin(x)**2 - 6*x*cos(x)**2 - 6*x*sin(x)**2 + assert wronskian([sin(x), cos(x), x**3], x).expand() == w2 + assert wronskian([sin(x), cos(x), x**3], x, method='berkowitz').expand() \ + == w2 + assert wronskian([], x) == 1 + + +def test_subs(): + assert Matrix([[1, x], [x, 4]]).subs(x, 5) == Matrix([[1, 5], [5, 4]]) + assert Matrix([[x, 2], [x + y, 4]]).subs([[x, -1], [y, -2]]) == \ + Matrix([[-1, 2], [-3, 4]]) + assert Matrix([[x, 2], [x + y, 4]]).subs([(x, -1), (y, -2)]) == \ + Matrix([[-1, 2], [-3, 4]]) + assert Matrix([[x, 2], [x + y, 4]]).subs({x: -1, y: -2}) == \ + Matrix([[-1, 2], [-3, 4]]) + assert Matrix([x*y]).subs({x: y - 1, y: x - 1}, simultaneous=True) == \ + Matrix([(x - 1)*(y - 1)]) + + for cls in classes: + assert Matrix([[2, 0], [0, 2]]) == cls.eye(2).subs(1, 2) + +def test_xreplace(): + assert Matrix([[1, x], [x, 4]]).xreplace({x: 5}) == \ + Matrix([[1, 5], [5, 4]]) + assert Matrix([[x, 2], [x + y, 4]]).xreplace({x: -1, y: -2}) == \ + Matrix([[-1, 2], [-3, 4]]) + for cls in classes: + assert Matrix([[2, 0], [0, 2]]) == cls.eye(2).xreplace({1: 2}) + +def test_simplify(): + n = Symbol('n') + f = Function('f') + + M = Matrix([[ 1/x + 1/y, (x + x*y) / x ], + [ (f(x) + y*f(x))/f(x), 2 * (1/n - cos(n * pi)/n) / pi ]]) + M.simplify() + assert M == Matrix([[ (x + y)/(x * y), 1 + y ], + [ 1 + y, 2*((1 - 1*cos(pi*n))/(pi*n)) ]]) + eq = (1 + x)**2 + M = Matrix([[eq]]) + M.simplify() + assert M == Matrix([[eq]]) + M.simplify(ratio=oo) + assert M == Matrix([[eq.simplify(ratio=oo)]]) + + +def test_transpose(): + M = Matrix([[1, 2, 3, 4, 5, 6, 7, 8, 9, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]]) + assert M.T == Matrix( [ [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5], + [6, 6], + [7, 7], + [8, 8], + [9, 9], + [0, 0] ]) + assert M.T.T == M + assert M.T == M.transpose() + + +def test_conjugate(): + M = Matrix([[0, I, 5], + [1, 2, 0]]) + + assert M.T == Matrix([[0, 1], + [I, 2], + [5, 0]]) + + assert M.C == Matrix([[0, -I, 5], + [1, 2, 0]]) + assert M.C == M.conjugate() + + assert M.H == M.T.C + assert M.H == Matrix([[ 0, 1], + [-I, 2], + [ 5, 0]]) + + +def test_conj_dirac(): + raises(AttributeError, lambda: eye(3).D) + + M = Matrix([[1, I, I, I], + [0, 1, I, I], + [0, 0, 1, I], + [0, 0, 0, 1]]) + + assert M.D == Matrix([[ 1, 0, 0, 0], + [-I, 1, 0, 0], + [-I, -I, -1, 0], + [-I, -I, I, -1]]) + + +def test_trace(): + M = Matrix([[1, 0, 0], + [0, 5, 0], + [0, 0, 8]]) + assert M.trace() == 14 + + +def test_shape(): + M = Matrix([[x, 0, 0], + [0, y, 0]]) + assert M.shape == (2, 3) + + +def test_col_row_op(): + M = Matrix([[x, 0, 0], + [0, y, 0]]) + M.row_op(1, lambda r, j: r + j + 1) + assert M == Matrix([[x, 0, 0], + [1, y + 2, 3]]) + + M.col_op(0, lambda c, j: c + y**j) + assert M == Matrix([[x + 1, 0, 0], + [1 + y, y + 2, 3]]) + + # neither row nor slice give copies that allow the original matrix to + # be changed + assert M.row(0) == Matrix([[x + 1, 0, 0]]) + r1 = M.row(0) + r1[0] = 42 + assert M[0, 0] == x + 1 + r1 = M[0, :-1] # also testing negative slice + r1[0] = 42 + assert M[0, 0] == x + 1 + c1 = M.col(0) + assert c1 == Matrix([x + 1, 1 + y]) + c1[0] = 0 + assert M[0, 0] == x + 1 + c1 = M[:, 0] + c1[0] = 42 + assert M[0, 0] == x + 1 + + +def test_row_mult(): + M = Matrix([[1,2,3], + [4,5,6]]) + M.row_mult(1,3) + assert M[1,0] == 12 + assert M[0,0] == 1 + assert M[1,2] == 18 + + +def test_row_add(): + M = Matrix([[1,2,3], + [4,5,6], + [1,1,1]]) + M.row_add(2,0,5) + assert M[0,0] == 6 + assert M[1,0] == 4 + assert M[0,2] == 8 + + +def test_zip_row_op(): + for cls in classes[:2]: # XXX: immutable matrices don't support row ops + M = cls.eye(3) + M.zip_row_op(1, 0, lambda v, u: v + 2*u) + assert M == cls([[1, 0, 0], + [2, 1, 0], + [0, 0, 1]]) + + M = cls.eye(3)*2 + M[0, 1] = -1 + M.zip_row_op(1, 0, lambda v, u: v + 2*u); M + assert M == cls([[2, -1, 0], + [4, 0, 0], + [0, 0, 2]]) + +def test_issue_3950(): + m = Matrix([1, 2, 3]) + a = Matrix([1, 2, 3]) + b = Matrix([2, 2, 3]) + assert not (m in []) + assert not (m in [1]) + assert m != 1 + assert m == a + assert m != b + + +def test_issue_3981(): + class Index1: + def __index__(self): + return 1 + + class Index2: + def __index__(self): + return 2 + index1 = Index1() + index2 = Index2() + + m = Matrix([1, 2, 3]) + + assert m[index2] == 3 + + m[index2] = 5 + assert m[2] == 5 + + m = Matrix([[1, 2, 3], [4, 5, 6]]) + assert m[index1, index2] == 6 + assert m[1, index2] == 6 + assert m[index1, 2] == 6 + + m[index1, index2] = 4 + assert m[1, 2] == 4 + m[1, index2] = 6 + assert m[1, 2] == 6 + m[index1, 2] = 8 + assert m[1, 2] == 8 + + +def test_evalf(): + a = Matrix([sqrt(5), 6]) + assert all(a.evalf()[i] == a[i].evalf() for i in range(2)) + assert all(a.evalf(2)[i] == a[i].evalf(2) for i in range(2)) + assert all(a.n(2)[i] == a[i].n(2) for i in range(2)) + + +def test_is_symbolic(): + a = Matrix([[x, x], [x, x]]) + assert a.is_symbolic() is True + a = Matrix([[1, 2, 3, 4], [5, 6, 7, 8]]) + assert a.is_symbolic() is False + a = Matrix([[1, 2, 3, 4], [5, 6, x, 8]]) + assert a.is_symbolic() is True + a = Matrix([[1, x, 3]]) + assert a.is_symbolic() is True + a = Matrix([[1, 2, 3]]) + assert a.is_symbolic() is False + a = Matrix([[1], [x], [3]]) + assert a.is_symbolic() is True + a = Matrix([[1], [2], [3]]) + assert a.is_symbolic() is False + + +def test_is_upper(): + a = Matrix([[1, 2, 3]]) + assert a.is_upper is True + a = Matrix([[1], [2], [3]]) + assert a.is_upper is False + a = zeros(4, 2) + assert a.is_upper is True + + +def test_is_lower(): + a = Matrix([[1, 2, 3]]) + assert a.is_lower is False + a = Matrix([[1], [2], [3]]) + assert a.is_lower is True + + +def test_is_nilpotent(): + a = Matrix(4, 4, [0, 2, 1, 6, 0, 0, 1, 2, 0, 0, 0, 3, 0, 0, 0, 0]) + assert a.is_nilpotent() + a = Matrix([[1, 0], [0, 1]]) + assert not a.is_nilpotent() + a = Matrix([]) + assert a.is_nilpotent() + + +def test_zeros_ones_fill(): + n, m = 3, 5 + + a = zeros(n, m) + a.fill( 5 ) + + b = 5 * ones(n, m) + + assert a == b + assert a.rows == b.rows == 3 + assert a.cols == b.cols == 5 + assert a.shape == b.shape == (3, 5) + assert zeros(2) == zeros(2, 2) + assert ones(2) == ones(2, 2) + assert zeros(2, 3) == Matrix(2, 3, [0]*6) + assert ones(2, 3) == Matrix(2, 3, [1]*6) + + a.fill(0) + assert a == zeros(n, m) + + +def test_empty_zeros(): + a = zeros(0) + assert a == Matrix() + a = zeros(0, 2) + assert a.rows == 0 + assert a.cols == 2 + a = zeros(2, 0) + assert a.rows == 2 + assert a.cols == 0 + + +def test_issue_3749(): + a = Matrix([[x**2, x*y], [x*sin(y), x*cos(y)]]) + assert a.diff(x) == Matrix([[2*x, y], [sin(y), cos(y)]]) + assert Matrix([ + [x, -x, x**2], + [exp(x), 1/x - exp(-x), x + 1/x]]).limit(x, oo) == \ + Matrix([[oo, -oo, oo], [oo, 0, oo]]) + assert Matrix([ + [(exp(x) - 1)/x, 2*x + y*x, x**x ], + [1/x, abs(x), abs(sin(x + 1))]]).limit(x, 0) == \ + Matrix([[1, 0, 1], [oo, 0, sin(1)]]) + assert a.integrate(x) == Matrix([ + [Rational(1, 3)*x**3, y*x**2/2], + [x**2*sin(y)/2, x**2*cos(y)/2]]) + + +def test_inv_iszerofunc(): + A = eye(4) + A.col_swap(0, 1) + for method in "GE", "LU": + assert A.inv(method=method, iszerofunc=lambda x: x == 0) == \ + A.inv(method="ADJ") + + +def test_jacobian_metrics(): + rho, phi = symbols("rho,phi") + X = Matrix([rho*cos(phi), rho*sin(phi)]) + Y = Matrix([rho, phi]) + J = X.jacobian(Y) + assert J == X.jacobian(Y.T) + assert J == (X.T).jacobian(Y) + assert J == (X.T).jacobian(Y.T) + g = J.T*eye(J.shape[0])*J + g = g.applyfunc(trigsimp) + assert g == Matrix([[1, 0], [0, rho**2]]) + + +def test_jacobian2(): + rho, phi = symbols("rho,phi") + X = Matrix([rho*cos(phi), rho*sin(phi), rho**2]) + Y = Matrix([rho, phi]) + J = Matrix([ + [cos(phi), -rho*sin(phi)], + [sin(phi), rho*cos(phi)], + [ 2*rho, 0], + ]) + assert X.jacobian(Y) == J + + +def test_issue_4564(): + X = Matrix([exp(x + y + z), exp(x + y + z), exp(x + y + z)]) + Y = Matrix([x, y, z]) + for i in range(1, 3): + for j in range(1, 3): + X_slice = X[:i, :] + Y_slice = Y[:j, :] + J = X_slice.jacobian(Y_slice) + assert J.rows == i + assert J.cols == j + for k in range(j): + assert J[:, k] == X_slice + + +def test_nonvectorJacobian(): + X = Matrix([[exp(x + y + z), exp(x + y + z)], + [exp(x + y + z), exp(x + y + z)]]) + raises(TypeError, lambda: X.jacobian(Matrix([x, y, z]))) + X = X[0, :] + Y = Matrix([[x, y], [x, z]]) + raises(TypeError, lambda: X.jacobian(Y)) + raises(TypeError, lambda: X.jacobian(Matrix([ [x, y], [x, z] ]))) + + +def test_vec(): + m = Matrix([[1, 3], [2, 4]]) + m_vec = m.vec() + assert m_vec.cols == 1 + for i in range(4): + assert m_vec[i] == i + 1 + + +def test_vech(): + m = Matrix([[1, 2], [2, 3]]) + m_vech = m.vech() + assert m_vech.cols == 1 + for i in range(3): + assert m_vech[i] == i + 1 + m_vech = m.vech(diagonal=False) + assert m_vech[0] == 2 + + m = Matrix([[1, x*(x + y)], [y*x + x**2, 1]]) + m_vech = m.vech(diagonal=False) + assert m_vech[0] == y*x + x**2 + + m = Matrix([[1, x*(x + y)], [y*x, 1]]) + m_vech = m.vech(diagonal=False, check_symmetry=False) + assert m_vech[0] == y*x + + raises(ShapeError, lambda: Matrix([[1, 3]]).vech()) + raises(ValueError, lambda: Matrix([[1, 3], [2, 4]]).vech()) + raises(ShapeError, lambda: Matrix([[1, 3]]).vech()) + raises(ValueError, lambda: Matrix([[1, 3], [2, 4]]).vech()) + + +def test_diag(): + # mostly tested in testcommonmatrix.py + assert diag([1, 2, 3]) == Matrix([1, 2, 3]) + m = [1, 2, [3]] + raises(ValueError, lambda: diag(m)) + assert diag(m, strict=False) == Matrix([1, 2, 3]) + + +def test_get_diag_blocks1(): + a = Matrix([[1, 2], [2, 3]]) + b = Matrix([[3, x], [y, 3]]) + c = Matrix([[3, x, 3], [y, 3, z], [x, y, z]]) + assert a.get_diag_blocks() == [a] + assert b.get_diag_blocks() == [b] + assert c.get_diag_blocks() == [c] + + +def test_get_diag_blocks2(): + a = Matrix([[1, 2], [2, 3]]) + b = Matrix([[3, x], [y, 3]]) + c = Matrix([[3, x, 3], [y, 3, z], [x, y, z]]) + assert diag(a, b, b).get_diag_blocks() == [a, b, b] + assert diag(a, b, c).get_diag_blocks() == [a, b, c] + assert diag(a, c, b).get_diag_blocks() == [a, c, b] + assert diag(c, c, b).get_diag_blocks() == [c, c, b] + + +def test_inv_block(): + a = Matrix([[1, 2], [2, 3]]) + b = Matrix([[3, x], [y, 3]]) + c = Matrix([[3, x, 3], [y, 3, z], [x, y, z]]) + A = diag(a, b, b) + assert A.inv(try_block_diag=True) == diag(a.inv(), b.inv(), b.inv()) + A = diag(a, b, c) + assert A.inv(try_block_diag=True) == diag(a.inv(), b.inv(), c.inv()) + A = diag(a, c, b) + assert A.inv(try_block_diag=True) == diag(a.inv(), c.inv(), b.inv()) + A = diag(a, a, b, a, c, a) + assert A.inv(try_block_diag=True) == diag( + a.inv(), a.inv(), b.inv(), a.inv(), c.inv(), a.inv()) + assert A.inv(try_block_diag=True, method="ADJ") == diag( + a.inv(method="ADJ"), a.inv(method="ADJ"), b.inv(method="ADJ"), + a.inv(method="ADJ"), c.inv(method="ADJ"), a.inv(method="ADJ")) + + +def test_creation_args(): + """ + Check that matrix dimensions can be specified using any reasonable type + (see issue 4614). + """ + raises(ValueError, lambda: zeros(3, -1)) + raises(TypeError, lambda: zeros(1, 2, 3, 4)) + assert zeros(int(3)) == zeros(3) + assert zeros(Integer(3)) == zeros(3) + raises(ValueError, lambda: zeros(3.)) + assert eye(int(3)) == eye(3) + assert eye(Integer(3)) == eye(3) + raises(ValueError, lambda: eye(3.)) + assert ones(int(3), Integer(4)) == ones(3, 4) + raises(TypeError, lambda: Matrix(5)) + raises(TypeError, lambda: Matrix(1, 2)) + raises(ValueError, lambda: Matrix([1, [2]])) + + +def test_diagonal_symmetrical(): + m = Matrix(2, 2, [0, 1, 1, 0]) + assert not m.is_diagonal() + assert m.is_symmetric() + assert m.is_symmetric(simplify=False) + + m = Matrix(2, 2, [1, 0, 0, 1]) + assert m.is_diagonal() + + m = diag(1, 2, 3) + assert m.is_diagonal() + assert m.is_symmetric() + + m = Matrix(3, 3, [1, 0, 0, 0, 2, 0, 0, 0, 3]) + assert m == diag(1, 2, 3) + + m = Matrix(2, 3, zeros(2, 3)) + assert not m.is_symmetric() + assert m.is_diagonal() + + m = Matrix(((5, 0), (0, 6), (0, 0))) + assert m.is_diagonal() + + m = Matrix(((5, 0, 0), (0, 6, 0))) + assert m.is_diagonal() + + m = Matrix(3, 3, [1, x**2 + 2*x + 1, y, (x + 1)**2, 2, 0, y, 0, 3]) + assert m.is_symmetric() + assert not m.is_symmetric(simplify=False) + assert m.expand().is_symmetric(simplify=False) + + +def test_diagonalization(): + m = Matrix([[1, 2+I], [2-I, 3]]) + assert m.is_diagonalizable() + + m = Matrix(3, 2, [-3, 1, -3, 20, 3, 10]) + assert not m.is_diagonalizable() + assert not m.is_symmetric() + raises(NonSquareMatrixError, lambda: m.diagonalize()) + + # diagonalizable + m = diag(1, 2, 3) + (P, D) = m.diagonalize() + assert P == eye(3) + assert D == m + + m = Matrix(2, 2, [0, 1, 1, 0]) + assert m.is_symmetric() + assert m.is_diagonalizable() + (P, D) = m.diagonalize() + assert P.inv() * m * P == D + + m = Matrix(2, 2, [1, 0, 0, 3]) + assert m.is_symmetric() + assert m.is_diagonalizable() + (P, D) = m.diagonalize() + assert P.inv() * m * P == D + assert P == eye(2) + assert D == m + + m = Matrix(2, 2, [1, 1, 0, 0]) + assert m.is_diagonalizable() + (P, D) = m.diagonalize() + assert P.inv() * m * P == D + + m = Matrix(3, 3, [1, 2, 0, 0, 3, 0, 2, -4, 2]) + assert m.is_diagonalizable() + (P, D) = m.diagonalize() + assert P.inv() * m * P == D + for i in P: + assert i.as_numer_denom()[1] == 1 + + m = Matrix(2, 2, [1, 0, 0, 0]) + assert m.is_diagonal() + assert m.is_diagonalizable() + (P, D) = m.diagonalize() + assert P.inv() * m * P == D + assert P == Matrix([[0, 1], [1, 0]]) + + # diagonalizable, complex only + m = Matrix(2, 2, [0, 1, -1, 0]) + assert not m.is_diagonalizable(True) + raises(MatrixError, lambda: m.diagonalize(True)) + assert m.is_diagonalizable() + (P, D) = m.diagonalize() + assert P.inv() * m * P == D + + # not diagonalizable + m = Matrix(2, 2, [0, 1, 0, 0]) + assert not m.is_diagonalizable() + raises(MatrixError, lambda: m.diagonalize()) + + m = Matrix(3, 3, [-3, 1, -3, 20, 3, 10, 2, -2, 4]) + assert not m.is_diagonalizable() + raises(MatrixError, lambda: m.diagonalize()) + + # symbolic + a, b, c, d = symbols('a b c d') + m = Matrix(2, 2, [a, c, c, b]) + assert m.is_symmetric() + assert m.is_diagonalizable() + + +def test_issue_15887(): + # Mutable matrix should not use cache + a = MutableDenseMatrix([[0, 1], [1, 0]]) + assert a.is_diagonalizable() is True + a[1, 0] = 0 + assert a.is_diagonalizable() is False + + a = MutableDenseMatrix([[0, 1], [1, 0]]) + a.diagonalize() + a[1, 0] = 0 + raises(MatrixError, lambda: a.diagonalize()) + + +def test_jordan_form(): + + m = Matrix(3, 2, [-3, 1, -3, 20, 3, 10]) + raises(NonSquareMatrixError, lambda: m.jordan_form()) + + # diagonalizable + m = Matrix(3, 3, [7, -12, 6, 10, -19, 10, 12, -24, 13]) + Jmust = Matrix(3, 3, [-1, 0, 0, 0, 1, 0, 0, 0, 1]) + P, J = m.jordan_form() + assert Jmust == J + assert Jmust == m.diagonalize()[1] + + # m = Matrix(3, 3, [0, 6, 3, 1, 3, 1, -2, 2, 1]) + # m.jordan_form() # very long + # m.jordan_form() # + + # diagonalizable, complex only + + # Jordan cells + # complexity: one of eigenvalues is zero + m = Matrix(3, 3, [0, 1, 0, -4, 4, 0, -2, 1, 2]) + # The blocks are ordered according to the value of their eigenvalues, + # in order to make the matrix compatible with .diagonalize() + Jmust = Matrix(3, 3, [2, 1, 0, 0, 2, 0, 0, 0, 2]) + P, J = m.jordan_form() + assert Jmust == J + + # complexity: all of eigenvalues are equal + m = Matrix(3, 3, [2, 6, -15, 1, 1, -5, 1, 2, -6]) + # Jmust = Matrix(3, 3, [-1, 0, 0, 0, -1, 1, 0, 0, -1]) + # same here see 1456ff + Jmust = Matrix(3, 3, [-1, 1, 0, 0, -1, 0, 0, 0, -1]) + P, J = m.jordan_form() + assert Jmust == J + + # complexity: two of eigenvalues are zero + m = Matrix(3, 3, [4, -5, 2, 5, -7, 3, 6, -9, 4]) + Jmust = Matrix(3, 3, [0, 1, 0, 0, 0, 0, 0, 0, 1]) + P, J = m.jordan_form() + assert Jmust == J + + m = Matrix(4, 4, [6, 5, -2, -3, -3, -1, 3, 3, 2, 1, -2, -3, -1, 1, 5, 5]) + Jmust = Matrix(4, 4, [2, 1, 0, 0, + 0, 2, 0, 0, + 0, 0, 2, 1, + 0, 0, 0, 2] + ) + P, J = m.jordan_form() + assert Jmust == J + + m = Matrix(4, 4, [6, 2, -8, -6, -3, 2, 9, 6, 2, -2, -8, -6, -1, 0, 3, 4]) + # Jmust = Matrix(4, 4, [2, 0, 0, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 0, -2]) + # same here see 1456ff + Jmust = Matrix(4, 4, [-2, 0, 0, 0, + 0, 2, 1, 0, + 0, 0, 2, 0, + 0, 0, 0, 2]) + P, J = m.jordan_form() + assert Jmust == J + + m = Matrix(4, 4, [5, 4, 2, 1, 0, 1, -1, -1, -1, -1, 3, 0, 1, 1, -1, 2]) + assert not m.is_diagonalizable() + Jmust = Matrix(4, 4, [1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 4, 1, 0, 0, 0, 4]) + P, J = m.jordan_form() + assert Jmust == J + + # checking for maximum precision to remain unchanged + m = Matrix([[Float('1.0', precision=110), Float('2.0', precision=110)], + [Float('3.14159265358979323846264338327', precision=110), Float('4.0', precision=110)]]) + P, J = m.jordan_form() + for term in J.values(): + if isinstance(term, Float): + assert term._prec == 110 + + +def test_jordan_form_complex_issue_9274(): + A = Matrix([[ 2, 4, 1, 0], + [-4, 2, 0, 1], + [ 0, 0, 2, 4], + [ 0, 0, -4, 2]]) + p = 2 - 4*I + q = 2 + 4*I + Jmust1 = Matrix([[p, 1, 0, 0], + [0, p, 0, 0], + [0, 0, q, 1], + [0, 0, 0, q]]) + Jmust2 = Matrix([[q, 1, 0, 0], + [0, q, 0, 0], + [0, 0, p, 1], + [0, 0, 0, p]]) + P, J = A.jordan_form() + assert J == Jmust1 or J == Jmust2 + assert simplify(P*J*P.inv()) == A + +def test_issue_10220(): + # two non-orthogonal Jordan blocks with eigenvalue 1 + M = Matrix([[1, 0, 0, 1], + [0, 1, 1, 0], + [0, 0, 1, 1], + [0, 0, 0, 1]]) + P, J = M.jordan_form() + assert P == Matrix([[0, 1, 0, 1], + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0]]) + assert J == Matrix([ + [1, 1, 0, 0], + [0, 1, 1, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]]) + +def test_jordan_form_issue_15858(): + A = Matrix([ + [1, 1, 1, 0], + [-2, -1, 0, -1], + [0, 0, -1, -1], + [0, 0, 2, 1]]) + (P, J) = A.jordan_form() + assert P.expand() == Matrix([ + [ -I, -I/2, I, I/2], + [-1 + I, 0, -1 - I, 0], + [ 0, -S(1)/2 - I/2, 0, -S(1)/2 + I/2], + [ 0, 1, 0, 1]]) + assert J == Matrix([ + [-I, 1, 0, 0], + [0, -I, 0, 0], + [0, 0, I, 1], + [0, 0, 0, I]]) + +def test_Matrix_berkowitz_charpoly(): + UA, K_i, K_w = symbols('UA K_i K_w') + + A = Matrix([[-K_i - UA + K_i**2/(K_i + K_w), K_i*K_w/(K_i + K_w)], + [ K_i*K_w/(K_i + K_w), -K_w + K_w**2/(K_i + K_w)]]) + + charpoly = A.charpoly(x) + + assert charpoly == \ + Poly(x**2 + (K_i*UA + K_w*UA + 2*K_i*K_w)/(K_i + K_w)*x + + K_i*K_w*UA/(K_i + K_w), x, domain='ZZ(K_i,K_w,UA)') + + assert type(charpoly) is PurePoly + + A = Matrix([[1, 3], [2, 0]]) + assert A.charpoly() == A.charpoly(x) == PurePoly(x**2 - x - 6) + + A = Matrix([[1, 2], [x, 0]]) + p = A.charpoly(x) + assert p.gen != x + assert p.as_expr().subs(p.gen, x) == x**2 - 3*x + + +def test_exp_jordan_block(): + l = Symbol('lamda') + + m = Matrix.jordan_block(1, l) + assert m._eval_matrix_exp_jblock() == Matrix([[exp(l)]]) + + m = Matrix.jordan_block(3, l) + assert m._eval_matrix_exp_jblock() == \ + Matrix([ + [exp(l), exp(l), exp(l)/2], + [0, exp(l), exp(l)], + [0, 0, exp(l)]]) + + +def test_exp(): + m = Matrix([[3, 4], [0, -2]]) + m_exp = Matrix([[exp(3), -4*exp(-2)/5 + 4*exp(3)/5], [0, exp(-2)]]) + assert m.exp() == m_exp + assert exp(m) == m_exp + + m = Matrix([[1, 0], [0, 1]]) + assert m.exp() == Matrix([[E, 0], [0, E]]) + assert exp(m) == Matrix([[E, 0], [0, E]]) + + m = Matrix([[1, -1], [1, 1]]) + assert m.exp() == Matrix([[E*cos(1), -E*sin(1)], [E*sin(1), E*cos(1)]]) + + +def test_log(): + l = Symbol('lamda') + + m = Matrix.jordan_block(1, l) + assert m._eval_matrix_log_jblock() == Matrix([[log(l)]]) + + m = Matrix.jordan_block(4, l) + assert m._eval_matrix_log_jblock() == \ + Matrix( + [ + [log(l), 1/l, -1/(2*l**2), 1/(3*l**3)], + [0, log(l), 1/l, -1/(2*l**2)], + [0, 0, log(l), 1/l], + [0, 0, 0, log(l)] + ] + ) + + m = Matrix( + [[0, 0, 1], + [0, 0, 0], + [-1, 0, 0]] + ) + raises(MatrixError, lambda: m.log()) + + +def test_has(): + A = Matrix(((x, y), (2, 3))) + assert A.has(x) + assert not A.has(z) + assert A.has(Symbol) + + A = A.subs(x, 2) + assert not A.has(x) + + +def test_find_reasonable_pivot_naive_finds_guaranteed_nonzero1(): + # Test if matrices._find_reasonable_pivot_naive() + # finds a guaranteed non-zero pivot when the + # some of the candidate pivots are symbolic expressions. + # Keyword argument: simpfunc=None indicates that no simplifications + # should be performed during the search. + x = Symbol('x') + column = Matrix(3, 1, [x, cos(x)**2 + sin(x)**2, S.Half]) + pivot_offset, pivot_val, pivot_assumed_nonzero, simplified =\ + _find_reasonable_pivot_naive(column) + assert pivot_val == S.Half + +def test_find_reasonable_pivot_naive_finds_guaranteed_nonzero2(): + # Test if matrices._find_reasonable_pivot_naive() + # finds a guaranteed non-zero pivot when the + # some of the candidate pivots are symbolic expressions. + # Keyword argument: simpfunc=_simplify indicates that the search + # should attempt to simplify candidate pivots. + x = Symbol('x') + column = Matrix(3, 1, + [x, + cos(x)**2+sin(x)**2+x**2, + cos(x)**2+sin(x)**2]) + pivot_offset, pivot_val, pivot_assumed_nonzero, simplified =\ + _find_reasonable_pivot_naive(column, simpfunc=_simplify) + assert pivot_val == 1 + +def test_find_reasonable_pivot_naive_simplifies(): + # Test if matrices._find_reasonable_pivot_naive() + # simplifies candidate pivots, and reports + # their offsets correctly. + x = Symbol('x') + column = Matrix(3, 1, + [x, + cos(x)**2+sin(x)**2+x, + cos(x)**2+sin(x)**2]) + pivot_offset, pivot_val, pivot_assumed_nonzero, simplified =\ + _find_reasonable_pivot_naive(column, simpfunc=_simplify) + + assert len(simplified) == 2 + assert simplified[0][0] == 1 + assert simplified[0][1] == 1+x + assert simplified[1][0] == 2 + assert simplified[1][1] == 1 + +def test_errors(): + raises(ValueError, lambda: Matrix([[1, 2], [1]])) + raises(IndexError, lambda: Matrix([[1, 2]])[1.2, 5]) + raises(IndexError, lambda: Matrix([[1, 2]])[1, 5.2]) + raises(ValueError, lambda: randMatrix(3, c=4, symmetric=True)) + raises(ValueError, lambda: Matrix([1, 2]).reshape(4, 6)) + raises(ShapeError, + lambda: Matrix([[1, 2], [3, 4]]).copyin_matrix([1, 0], Matrix([1, 2]))) + raises(TypeError, lambda: Matrix([[1, 2], [3, 4]]).copyin_list([0, + 1], set())) + raises(NonSquareMatrixError, lambda: Matrix([[1, 2, 3], [2, 3, 0]]).inv()) + raises(ShapeError, + lambda: Matrix(1, 2, [1, 2]).row_join(Matrix([[1, 2], [3, 4]]))) + raises( + ShapeError, lambda: Matrix([1, 2]).col_join(Matrix([[1, 2], [3, 4]]))) + raises(ShapeError, lambda: Matrix([1]).row_insert(1, Matrix([[1, + 2], [3, 4]]))) + raises(ShapeError, lambda: Matrix([1]).col_insert(1, Matrix([[1, + 2], [3, 4]]))) + raises(NonSquareMatrixError, lambda: Matrix([1, 2]).trace()) + raises(TypeError, lambda: Matrix([1]).applyfunc(1)) + raises(ValueError, lambda: Matrix([[1, 2], [3, 4]]).minor(4, 5)) + raises(ValueError, lambda: Matrix([[1, 2], [3, 4]]).minor_submatrix(4, 5)) + raises(TypeError, lambda: Matrix([1, 2, 3]).cross(1)) + raises(TypeError, lambda: Matrix([1, 2, 3]).dot(1)) + raises(ShapeError, lambda: Matrix([1, 2, 3]).dot(Matrix([1, 2]))) + raises(ShapeError, lambda: Matrix([1, 2]).dot([])) + raises(TypeError, lambda: Matrix([1, 2]).dot('a')) + raises(ShapeError, lambda: Matrix([1, 2]).dot([1, 2, 3])) + raises(NonSquareMatrixError, lambda: Matrix([1, 2, 3]).exp()) + raises(ShapeError, lambda: Matrix([[1, 2], [3, 4]]).normalized()) + raises(ValueError, lambda: Matrix([1, 2]).inv(method='not a method')) + raises(NonSquareMatrixError, lambda: Matrix([1, 2]).inverse_GE()) + raises(ValueError, lambda: Matrix([[1, 2], [1, 2]]).inverse_GE()) + raises(NonSquareMatrixError, lambda: Matrix([1, 2]).inverse_ADJ()) + raises(ValueError, lambda: Matrix([[1, 2], [1, 2]]).inverse_ADJ()) + raises(NonSquareMatrixError, lambda: Matrix([1, 2]).inverse_LU()) + raises(NonSquareMatrixError, lambda: Matrix([1, 2]).is_nilpotent()) + raises(NonSquareMatrixError, lambda: Matrix([1, 2]).det()) + raises(ValueError, + lambda: Matrix([[1, 2], [3, 4]]).det(method='Not a real method')) + raises(ValueError, + lambda: Matrix([[1, 2, 3, 4], [5, 6, 7, 8], + [9, 10, 11, 12], [13, 14, 15, 16]]).det(iszerofunc="Not function")) + raises(ValueError, + lambda: Matrix([[1, 2, 3, 4], [5, 6, 7, 8], + [9, 10, 11, 12], [13, 14, 15, 16]]).det(iszerofunc=False)) + raises(ValueError, + lambda: hessian(Matrix([[1, 2], [3, 4]]), Matrix([[1, 2], [2, 1]]))) + raises(ValueError, lambda: hessian(Matrix([[1, 2], [3, 4]]), [])) + raises(ValueError, lambda: hessian(Symbol('x')**2, 'a')) + raises(IndexError, lambda: eye(3)[5, 2]) + raises(IndexError, lambda: eye(3)[2, 5]) + M = Matrix(((1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12), (13, 14, 15, 16))) + raises(ValueError, lambda: M.det('method=LU_decomposition()')) + V = Matrix([[10, 10, 10]]) + M = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + raises(ValueError, lambda: M.row_insert(4.7, V)) + M = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + raises(ValueError, lambda: M.col_insert(-4.2, V)) + +def test_len(): + assert len(Matrix()) == 0 + assert len(Matrix([[1, 2]])) == len(Matrix([[1], [2]])) == 2 + assert len(Matrix(0, 2, lambda i, j: 0)) == \ + len(Matrix(2, 0, lambda i, j: 0)) == 0 + assert len(Matrix([[0, 1, 2], [3, 4, 5]])) == 6 + assert Matrix([1]) == Matrix([[1]]) + assert not Matrix() + assert Matrix() == Matrix([]) + + +def test_integrate(): + A = Matrix(((1, 4, x), (y, 2, 4), (10, 5, x**2))) + assert A.integrate(x) == \ + Matrix(((x, 4*x, x**2/2), (x*y, 2*x, 4*x), (10*x, 5*x, x**3/3))) + assert A.integrate(y) == \ + Matrix(((y, 4*y, x*y), (y**2/2, 2*y, 4*y), (10*y, 5*y, y*x**2))) + + +def test_limit(): + A = Matrix(((1, 4, sin(x)/x), (y, 2, 4), (10, 5, x**2 + 1))) + assert A.limit(x, 0) == Matrix(((1, 4, 1), (y, 2, 4), (10, 5, 1))) + + +def test_diff(): + A = MutableDenseMatrix(((1, 4, x), (y, 2, 4), (10, 5, x**2 + 1))) + assert isinstance(A.diff(x), type(A)) + assert A.diff(x) == MutableDenseMatrix(((0, 0, 1), (0, 0, 0), (0, 0, 2*x))) + assert A.diff(y) == MutableDenseMatrix(((0, 0, 0), (1, 0, 0), (0, 0, 0))) + + assert diff(A, x) == MutableDenseMatrix(((0, 0, 1), (0, 0, 0), (0, 0, 2*x))) + assert diff(A, y) == MutableDenseMatrix(((0, 0, 0), (1, 0, 0), (0, 0, 0))) + + A_imm = A.as_immutable() + assert isinstance(A_imm.diff(x), type(A_imm)) + assert A_imm.diff(x) == ImmutableDenseMatrix(((0, 0, 1), (0, 0, 0), (0, 0, 2*x))) + assert A_imm.diff(y) == ImmutableDenseMatrix(((0, 0, 0), (1, 0, 0), (0, 0, 0))) + + assert diff(A_imm, x) == ImmutableDenseMatrix(((0, 0, 1), (0, 0, 0), (0, 0, 2*x))) + assert diff(A_imm, y) == ImmutableDenseMatrix(((0, 0, 0), (1, 0, 0), (0, 0, 0))) + + assert A.diff(x, evaluate=False) == ArrayDerivative(A, x, evaluate=False) + assert diff(A, x, evaluate=False) == ArrayDerivative(A, x, evaluate=False) + + +def test_diff_by_matrix(): + + # Derive matrix by matrix: + + A = MutableDenseMatrix([[x, y], [z, t]]) + assert A.diff(A) == Array([[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, 1]]]]) + assert diff(A, A) == Array([[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, 1]]]]) + + A_imm = A.as_immutable() + assert A_imm.diff(A_imm) == Array([[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, 1]]]]) + assert diff(A_imm, A_imm) == Array([[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, 1]]]]) + + # Derive a constant matrix: + assert A.diff(a) == MutableDenseMatrix([[0, 0], [0, 0]]) + + B = ImmutableDenseMatrix([a, b]) + assert A.diff(B) == Array.zeros(2, 1, 2, 2) + assert A.diff(A) == Array([[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, 1]]]]) + + # Test diff with tuples: + + dB = B.diff([[a, b]]) + assert dB.shape == (2, 2, 1) + assert dB == Array([[[1], [0]], [[0], [1]]]) + + f = Function("f") + fxyz = f(x, y, z) + assert fxyz.diff([[x, y, z]]) == Array([fxyz.diff(x), fxyz.diff(y), fxyz.diff(z)]) + assert fxyz.diff(([x, y, z], 2)) == Array([ + [fxyz.diff(x, 2), fxyz.diff(x, y), fxyz.diff(x, z)], + [fxyz.diff(x, y), fxyz.diff(y, 2), fxyz.diff(y, z)], + [fxyz.diff(x, z), fxyz.diff(z, y), fxyz.diff(z, 2)], + ]) + + expr = sin(x)*exp(y) + assert expr.diff([[x, y]]) == Array([cos(x)*exp(y), sin(x)*exp(y)]) + assert expr.diff(y, ((x, y),)) == Array([cos(x)*exp(y), sin(x)*exp(y)]) + assert expr.diff(x, ((x, y),)) == Array([-sin(x)*exp(y), cos(x)*exp(y)]) + assert expr.diff(((y, x),), [[x, y]]) == Array([[cos(x)*exp(y), -sin(x)*exp(y)], [sin(x)*exp(y), cos(x)*exp(y)]]) + + # Test different notations: + + assert fxyz.diff(x).diff(y).diff(x) == fxyz.diff(((x, y, z),), 3)[0, 1, 0] + assert fxyz.diff(z).diff(y).diff(x) == fxyz.diff(((x, y, z),), 3)[2, 1, 0] + assert fxyz.diff([[x, y, z]], ((z, y, x),)) == Array([[fxyz.diff(i).diff(j) for i in (x, y, z)] for j in (z, y, x)]) + + # Test scalar derived by matrix remains matrix: + res = x.diff(Matrix([[x, y]])) + assert isinstance(res, ImmutableDenseMatrix) + assert res == Matrix([[1, 0]]) + res = (x**3).diff(Matrix([[x, y]])) + assert isinstance(res, ImmutableDenseMatrix) + assert res == Matrix([[3*x**2, 0]]) + + +def test_getattr(): + A = Matrix(((1, 4, x), (y, 2, 4), (10, 5, x**2 + 1))) + raises(AttributeError, lambda: A.nonexistantattribute) + assert getattr(A, 'diff')(x) == Matrix(((0, 0, 1), (0, 0, 0), (0, 0, 2*x))) + + +def test_hessenberg(): + A = Matrix([[3, 4, 1], [2, 4, 5], [0, 1, 2]]) + assert A.is_upper_hessenberg + A = A.T + assert A.is_lower_hessenberg + A[0, -1] = 1 + assert A.is_lower_hessenberg is False + + A = Matrix([[3, 4, 1], [2, 4, 5], [3, 1, 2]]) + assert not A.is_upper_hessenberg + + A = zeros(5, 2) + assert A.is_upper_hessenberg + + +def test_cholesky(): + raises(NonSquareMatrixError, lambda: Matrix((1, 2)).cholesky()) + raises(ValueError, lambda: Matrix(((1, 2), (3, 4))).cholesky()) + raises(ValueError, lambda: Matrix(((5 + I, 0), (0, 1))).cholesky()) + raises(ValueError, lambda: Matrix(((1, 5), (5, 1))).cholesky()) + raises(ValueError, lambda: Matrix(((1, 2), (3, 4))).cholesky(hermitian=False)) + assert Matrix(((5 + I, 0), (0, 1))).cholesky(hermitian=False) == Matrix([ + [sqrt(5 + I), 0], [0, 1]]) + A = Matrix(((1, 5), (5, 1))) + L = A.cholesky(hermitian=False) + assert L == Matrix([[1, 0], [5, 2*sqrt(6)*I]]) + assert L*L.T == A + A = Matrix(((25, 15, -5), (15, 18, 0), (-5, 0, 11))) + L = A.cholesky() + assert L * L.T == A + assert L.is_lower + assert L == Matrix([[5, 0, 0], [3, 3, 0], [-1, 1, 3]]) + A = Matrix(((4, -2*I, 2 + 2*I), (2*I, 2, -1 + I), (2 - 2*I, -1 - I, 11))) + assert A.cholesky().expand() == Matrix(((2, 0, 0), (I, 1, 0), (1 - I, 0, 3))) + + raises(NonSquareMatrixError, lambda: SparseMatrix((1, 2)).cholesky()) + raises(ValueError, lambda: SparseMatrix(((1, 2), (3, 4))).cholesky()) + raises(ValueError, lambda: SparseMatrix(((5 + I, 0), (0, 1))).cholesky()) + raises(ValueError, lambda: SparseMatrix(((1, 5), (5, 1))).cholesky()) + raises(ValueError, lambda: SparseMatrix(((1, 2), (3, 4))).cholesky(hermitian=False)) + assert SparseMatrix(((5 + I, 0), (0, 1))).cholesky(hermitian=False) == Matrix([ + [sqrt(5 + I), 0], [0, 1]]) + A = SparseMatrix(((1, 5), (5, 1))) + L = A.cholesky(hermitian=False) + assert L == Matrix([[1, 0], [5, 2*sqrt(6)*I]]) + assert L*L.T == A + A = SparseMatrix(((25, 15, -5), (15, 18, 0), (-5, 0, 11))) + L = A.cholesky() + assert L * L.T == A + assert L.is_lower + assert L == Matrix([[5, 0, 0], [3, 3, 0], [-1, 1, 3]]) + A = SparseMatrix(((4, -2*I, 2 + 2*I), (2*I, 2, -1 + I), (2 - 2*I, -1 - I, 11))) + assert A.cholesky() == Matrix(((2, 0, 0), (I, 1, 0), (1 - I, 0, 3))) + + +def test_matrix_norm(): + # Vector Tests + # Test columns and symbols + x = Symbol('x', real=True) + v = Matrix([cos(x), sin(x)]) + assert trigsimp(v.norm(2)) == 1 + assert v.norm(10) == Pow(cos(x)**10 + sin(x)**10, Rational(1, 10)) + + # Test Rows + A = Matrix([[5, Rational(3, 2)]]) + assert A.norm() == Pow(25 + Rational(9, 4), S.Half) + assert A.norm(oo) == max(A) + assert A.norm(-oo) == min(A) + + # Matrix Tests + # Intuitive test + A = Matrix([[1, 1], [1, 1]]) + assert A.norm(2) == 2 + assert A.norm(-2) == 0 + assert A.norm('frobenius') == 2 + assert eye(10).norm(2) == eye(10).norm(-2) == 1 + assert A.norm(oo) == 2 + + # Test with Symbols and more complex entries + A = Matrix([[3, y, y], [x, S.Half, -pi]]) + assert (A.norm('fro') + == sqrt(Rational(37, 4) + 2*abs(y)**2 + pi**2 + x**2)) + + # Check non-square + A = Matrix([[1, 2, -3], [4, 5, Rational(13, 2)]]) + assert A.norm(2) == sqrt(Rational(389, 8) + sqrt(78665)/8) + assert A.norm(-2) is S.Zero + assert A.norm('frobenius') == sqrt(389)/2 + + # Test properties of matrix norms + # https://en.wikipedia.org/wiki/Matrix_norm#Definition + # Two matrices + A = Matrix([[1, 2], [3, 4]]) + B = Matrix([[5, 5], [-2, 2]]) + C = Matrix([[0, -I], [I, 0]]) + D = Matrix([[1, 0], [0, -1]]) + L = [A, B, C, D] + alpha = Symbol('alpha', real=True) + + for order in ['fro', 2, -2]: + # Zero Check + assert zeros(3).norm(order) is S.Zero + # Check Triangle Inequality for all Pairs of Matrices + for X in L: + for Y in L: + dif = (X.norm(order) + Y.norm(order) - + (X + Y).norm(order)) + assert (dif >= 0) + # Scalar multiplication linearity + for M in [A, B, C, D]: + dif = simplify((alpha*M).norm(order) - + abs(alpha) * M.norm(order)) + assert dif == 0 + + # Test Properties of Vector Norms + # https://en.wikipedia.org/wiki/Vector_norm + # Two column vectors + a = Matrix([1, 1 - 1*I, -3]) + b = Matrix([S.Half, 1*I, 1]) + c = Matrix([-1, -1, -1]) + d = Matrix([3, 2, I]) + e = Matrix([Integer(1e2), Rational(1, 1e2), 1]) + L = [a, b, c, d, e] + alpha = Symbol('alpha', real=True) + + for order in [1, 2, -1, -2, S.Infinity, S.NegativeInfinity, pi]: + # Zero Check + if order > 0: + assert Matrix([0, 0, 0]).norm(order) is S.Zero + # Triangle inequality on all pairs + if order >= 1: # Triangle InEq holds only for these norms + for X in L: + for Y in L: + dif = (X.norm(order) + Y.norm(order) - + (X + Y).norm(order)) + assert simplify(dif >= 0) is S.true + # Linear to scalar multiplication + if order in [1, 2, -1, -2, S.Infinity, S.NegativeInfinity]: + for X in L: + dif = simplify((alpha*X).norm(order) - + (abs(alpha) * X.norm(order))) + assert dif == 0 + + # ord=1 + M = Matrix(3, 3, [1, 3, 0, -2, -1, 0, 3, 9, 6]) + assert M.norm(1) == 13 + + +def test_condition_number(): + x = Symbol('x', real=True) + A = eye(3) + A[0, 0] = 10 + A[2, 2] = Rational(1, 10) + assert A.condition_number() == 100 + + A[1, 1] = x + assert A.condition_number() == Max(10, Abs(x)) / Min(Rational(1, 10), Abs(x)) + + M = Matrix([[cos(x), sin(x)], [-sin(x), cos(x)]]) + Mc = M.condition_number() + assert all(Float(1.).epsilon_eq(Mc.subs(x, val).evalf()) for val in + [Rational(1, 5), S.Half, Rational(1, 10), pi/2, pi, pi*Rational(7, 4) ]) + + #issue 10782 + assert Matrix([]).condition_number() == 0 + + +def test_equality(): + A = Matrix(((1, 2, 3), (4, 5, 6), (7, 8, 9))) + B = Matrix(((9, 8, 7), (6, 5, 4), (3, 2, 1))) + assert A == A[:, :] + assert not A != A[:, :] + assert not A == B + assert A != B + assert A != 10 + assert not A == 10 + + # A SparseMatrix can be equal to a Matrix + C = SparseMatrix(((1, 0, 0), (0, 1, 0), (0, 0, 1))) + D = Matrix(((1, 0, 0), (0, 1, 0), (0, 0, 1))) + assert C == D + assert not C != D + + +def test_col_join(): + assert eye(3).col_join(Matrix([[7, 7, 7]])) == \ + Matrix([[1, 0, 0], + [0, 1, 0], + [0, 0, 1], + [7, 7, 7]]) + + +def test_row_insert(): + r4 = Matrix([[4, 4, 4]]) + for i in range(-4, 5): + l = [1, 0, 0] + l.insert(i, 4) + assert flatten(eye(3).row_insert(i, r4).col(0).tolist()) == l + + +def test_col_insert(): + c4 = Matrix([4, 4, 4]) + for i in range(-4, 5): + l = [0, 0, 0] + l.insert(i, 4) + assert flatten(zeros(3).col_insert(i, c4).row(0).tolist()) == l + + +def test_normalized(): + assert Matrix([3, 4]).normalized() == \ + Matrix([Rational(3, 5), Rational(4, 5)]) + + # Zero vector trivial cases + assert Matrix([0, 0, 0]).normalized() == Matrix([0, 0, 0]) + + # Machine precision error truncation trivial cases + m = Matrix([0,0,1.e-100]) + assert m.normalized( + iszerofunc=lambda x: x.evalf(n=10, chop=True).is_zero + ) == Matrix([0, 0, 0]) + + +def test_print_nonzero(): + assert capture(lambda: eye(3).print_nonzero()) == \ + '[X ]\n[ X ]\n[ X]\n' + assert capture(lambda: eye(3).print_nonzero('.')) == \ + '[. ]\n[ . ]\n[ .]\n' + + +def test_zeros_eye(): + assert Matrix.eye(3) == eye(3) + assert Matrix.zeros(3) == zeros(3) + assert ones(3, 4) == Matrix(3, 4, [1]*12) + + i = Matrix([[1, 0], [0, 1]]) + z = Matrix([[0, 0], [0, 0]]) + for cls in classes: + m = cls.eye(2) + assert i == m # but m == i will fail if m is immutable + assert i == eye(2, cls=cls) + assert type(m) == cls + m = cls.zeros(2) + assert z == m + assert z == zeros(2, cls=cls) + assert type(m) == cls + + +def test_is_zero(): + assert Matrix().is_zero_matrix + assert Matrix([[0, 0], [0, 0]]).is_zero_matrix + assert zeros(3, 4).is_zero_matrix + assert not eye(3).is_zero_matrix + assert Matrix([[x, 0], [0, 0]]).is_zero_matrix == None + assert SparseMatrix([[x, 0], [0, 0]]).is_zero_matrix == None + assert ImmutableMatrix([[x, 0], [0, 0]]).is_zero_matrix == None + assert ImmutableSparseMatrix([[x, 0], [0, 0]]).is_zero_matrix == None + assert Matrix([[x, 1], [0, 0]]).is_zero_matrix == False + a = Symbol('a', nonzero=True) + assert Matrix([[a, 0], [0, 0]]).is_zero_matrix == False + + +def test_rotation_matrices(): + # This tests the rotation matrices by rotating about an axis and back. + theta = pi/3 + r3_plus = rot_axis3(theta) + r3_minus = rot_axis3(-theta) + r2_plus = rot_axis2(theta) + r2_minus = rot_axis2(-theta) + r1_plus = rot_axis1(theta) + r1_minus = rot_axis1(-theta) + assert r3_minus*r3_plus*eye(3) == eye(3) + assert r2_minus*r2_plus*eye(3) == eye(3) + assert r1_minus*r1_plus*eye(3) == eye(3) + + # Check the correctness of the trace of the rotation matrix + assert r1_plus.trace() == 1 + 2*cos(theta) + assert r2_plus.trace() == 1 + 2*cos(theta) + assert r3_plus.trace() == 1 + 2*cos(theta) + + # Check that a rotation with zero angle doesn't change anything. + assert rot_axis1(0) == eye(3) + assert rot_axis2(0) == eye(3) + assert rot_axis3(0) == eye(3) + + # Check left-hand convention + # see Issue #24529 + q1 = Quaternion.from_axis_angle([1, 0, 0], pi / 2) + q2 = Quaternion.from_axis_angle([0, 1, 0], pi / 2) + q3 = Quaternion.from_axis_angle([0, 0, 1], pi / 2) + assert rot_axis1(- pi / 2) == q1.to_rotation_matrix() + assert rot_axis2(- pi / 2) == q2.to_rotation_matrix() + assert rot_axis3(- pi / 2) == q3.to_rotation_matrix() + # Check right-hand convention + assert rot_ccw_axis1(+ pi / 2) == q1.to_rotation_matrix() + assert rot_ccw_axis2(+ pi / 2) == q2.to_rotation_matrix() + assert rot_ccw_axis3(+ pi / 2) == q3.to_rotation_matrix() + + +def test_DeferredVector(): + assert str(DeferredVector("vector")[4]) == "vector[4]" + assert sympify(DeferredVector("d")) == DeferredVector("d") + raises(IndexError, lambda: DeferredVector("d")[-1]) + assert str(DeferredVector("d")) == "d" + assert repr(DeferredVector("test")) == "DeferredVector('test')" + +def test_DeferredVector_not_iterable(): + assert not iterable(DeferredVector('X')) + +def test_DeferredVector_Matrix(): + raises(TypeError, lambda: Matrix(DeferredVector("V"))) + +def test_GramSchmidt(): + R = Rational + m1 = Matrix(1, 2, [1, 2]) + m2 = Matrix(1, 2, [2, 3]) + assert GramSchmidt([m1, m2]) == \ + [Matrix(1, 2, [1, 2]), Matrix(1, 2, [R(2)/5, R(-1)/5])] + assert GramSchmidt([m1.T, m2.T]) == \ + [Matrix(2, 1, [1, 2]), Matrix(2, 1, [R(2)/5, R(-1)/5])] + # from wikipedia + assert GramSchmidt([Matrix([3, 1]), Matrix([2, 2])], True) == [ + Matrix([3*sqrt(10)/10, sqrt(10)/10]), + Matrix([-sqrt(10)/10, 3*sqrt(10)/10])] + # https://github.com/sympy/sympy/issues/9488 + L = FiniteSet(Matrix([1])) + assert GramSchmidt(L) == [Matrix([[1]])] + + +def test_casoratian(): + assert casoratian([1, 2, 3, 4], 1) == 0 + assert casoratian([1, 2, 3, 4], 1, zero=False) == 0 + + +def test_zero_dimension_multiply(): + assert (Matrix()*zeros(0, 3)).shape == (0, 3) + assert zeros(3, 0)*zeros(0, 3) == zeros(3, 3) + assert zeros(0, 3)*zeros(3, 0) == Matrix() + + +def test_slice_issue_2884(): + m = Matrix(2, 2, range(4)) + assert m[1, :] == Matrix([[2, 3]]) + assert m[-1, :] == Matrix([[2, 3]]) + assert m[:, 1] == Matrix([[1, 3]]).T + assert m[:, -1] == Matrix([[1, 3]]).T + raises(IndexError, lambda: m[2, :]) + raises(IndexError, lambda: m[2, 2]) + + +def test_slice_issue_3401(): + assert zeros(0, 3)[:, -1].shape == (0, 1) + assert zeros(3, 0)[0, :] == Matrix(1, 0, []) + + +def test_copyin(): + s = zeros(3, 3) + s[3] = 1 + assert s[:, 0] == Matrix([0, 1, 0]) + assert s[3] == 1 + assert s[3: 4] == [1] + s[1, 1] = 42 + assert s[1, 1] == 42 + assert s[1, 1:] == Matrix([[42, 0]]) + s[1, 1:] = Matrix([[5, 6]]) + assert s[1, :] == Matrix([[1, 5, 6]]) + s[1, 1:] = [[42, 43]] + assert s[1, :] == Matrix([[1, 42, 43]]) + s[0, 0] = 17 + assert s[:, :1] == Matrix([17, 1, 0]) + s[0, 0] = [1, 1, 1] + assert s[:, 0] == Matrix([1, 1, 1]) + s[0, 0] = Matrix([1, 1, 1]) + assert s[:, 0] == Matrix([1, 1, 1]) + s[0, 0] = SparseMatrix([1, 1, 1]) + assert s[:, 0] == Matrix([1, 1, 1]) + + +def test_invertible_check(): + # sometimes a singular matrix will have a pivot vector shorter than + # the number of rows in a matrix... + assert Matrix([[1, 2], [1, 2]]).rref() == (Matrix([[1, 2], [0, 0]]), (0,)) + raises(ValueError, lambda: Matrix([[1, 2], [1, 2]]).inv()) + m = Matrix([ + [-1, -1, 0], + [ x, 1, 1], + [ 1, x, -1], + ]) + assert len(m.rref()[1]) != m.rows + # in addition, unless simplify=True in the call to rref, the identity + # matrix will be returned even though m is not invertible + assert m.rref()[0] != eye(3) + assert m.rref(simplify=signsimp)[0] != eye(3) + raises(ValueError, lambda: m.inv(method="ADJ")) + raises(ValueError, lambda: m.inv(method="GE")) + raises(ValueError, lambda: m.inv(method="LU")) + + +def test_issue_3959(): + x, y = symbols('x, y') + e = x*y + assert e.subs(x, Matrix([3, 5, 3])) == Matrix([3, 5, 3])*y + + +def test_issue_5964(): + assert str(Matrix([[1, 2], [3, 4]])) == 'Matrix([[1, 2], [3, 4]])' + + +def test_issue_7604(): + x, y = symbols("x y") + assert sstr(Matrix([[x, 2*y], [y**2, x + 3]])) == \ + 'Matrix([\n[ x, 2*y],\n[y**2, x + 3]])' + + +def test_is_Identity(): + assert eye(3).is_Identity + assert eye(3).as_immutable().is_Identity + assert not zeros(3).is_Identity + assert not ones(3).is_Identity + # issue 6242 + assert not Matrix([[1, 0, 0]]).is_Identity + # issue 8854 + assert SparseMatrix(3,3, {(0,0):1, (1,1):1, (2,2):1}).is_Identity + assert not SparseMatrix(2,3, range(6)).is_Identity + assert not SparseMatrix(3,3, {(0,0):1, (1,1):1}).is_Identity + assert not SparseMatrix(3,3, {(0,0):1, (1,1):1, (2,2):1, (0,1):2, (0,2):3}).is_Identity + + +def test_dot(): + assert ones(1, 3).dot(ones(3, 1)) == 3 + assert ones(1, 3).dot([1, 1, 1]) == 3 + assert Matrix([1, 2, 3]).dot(Matrix([1, 2, 3])) == 14 + assert Matrix([1, 2, 3*I]).dot(Matrix([I, 2, 3*I])) == -5 + I + assert Matrix([1, 2, 3*I]).dot(Matrix([I, 2, 3*I]), hermitian=False) == -5 + I + assert Matrix([1, 2, 3*I]).dot(Matrix([I, 2, 3*I]), hermitian=True) == 13 + I + assert Matrix([1, 2, 3*I]).dot(Matrix([I, 2, 3*I]), hermitian=True, conjugate_convention="physics") == 13 - I + assert Matrix([1, 2, 3*I]).dot(Matrix([4, 5*I, 6]), hermitian=True, conjugate_convention="right") == 4 + 8*I + assert Matrix([1, 2, 3*I]).dot(Matrix([4, 5*I, 6]), hermitian=True, conjugate_convention="left") == 4 - 8*I + assert Matrix([I, 2*I]).dot(Matrix([I, 2*I]), hermitian=False, conjugate_convention="left") == -5 + assert Matrix([I, 2*I]).dot(Matrix([I, 2*I]), conjugate_convention="left") == 5 + raises(ValueError, lambda: Matrix([1, 2]).dot(Matrix([3, 4]), hermitian=True, conjugate_convention="test")) + + +def test_dual(): + B_x, B_y, B_z, E_x, E_y, E_z = symbols( + 'B_x B_y B_z E_x E_y E_z', real=True) + F = Matrix(( + ( 0, E_x, E_y, E_z), + (-E_x, 0, B_z, -B_y), + (-E_y, -B_z, 0, B_x), + (-E_z, B_y, -B_x, 0) + )) + Fd = Matrix(( + ( 0, -B_x, -B_y, -B_z), + (B_x, 0, E_z, -E_y), + (B_y, -E_z, 0, E_x), + (B_z, E_y, -E_x, 0) + )) + assert F.dual().equals(Fd) + assert eye(3).dual().equals(zeros(3)) + assert F.dual().dual().equals(-F) + + +def test_anti_symmetric(): + assert Matrix([1, 2]).is_anti_symmetric() is False + m = Matrix(3, 3, [0, x**2 + 2*x + 1, y, -(x + 1)**2, 0, x*y, -y, -x*y, 0]) + assert m.is_anti_symmetric() is True + assert m.is_anti_symmetric(simplify=False) is None + assert m.is_anti_symmetric(simplify=lambda x: x) is None + + # tweak to fail + m[2, 1] = -m[2, 1] + assert m.is_anti_symmetric() is None + # untweak + m[2, 1] = -m[2, 1] + + m = m.expand() + assert m.is_anti_symmetric(simplify=False) is True + m[0, 0] = 1 + assert m.is_anti_symmetric() is False + + +def test_normalize_sort_diogonalization(): + A = Matrix(((1, 2), (2, 1))) + P, Q = A.diagonalize(normalize=True) + assert P*P.T == P.T*P == eye(P.cols) + P, Q = A.diagonalize(normalize=True, sort=True) + assert P*P.T == P.T*P == eye(P.cols) + assert P*Q*P.inv() == A + + +def test_issue_5321(): + raises(ValueError, lambda: Matrix([[1, 2, 3], Matrix(0, 1, [])])) + + +def test_issue_5320(): + assert Matrix.hstack(eye(2), 2*eye(2)) == Matrix([ + [1, 0, 2, 0], + [0, 1, 0, 2] + ]) + assert Matrix.vstack(eye(2), 2*eye(2)) == Matrix([ + [1, 0], + [0, 1], + [2, 0], + [0, 2] + ]) + cls = SparseMatrix + assert cls.hstack(cls(eye(2)), cls(2*eye(2))) == Matrix([ + [1, 0, 2, 0], + [0, 1, 0, 2] + ]) + +def test_issue_11944(): + A = Matrix([[1]]) + AIm = sympify(A) + assert Matrix.hstack(AIm, A) == Matrix([[1, 1]]) + assert Matrix.vstack(AIm, A) == Matrix([[1], [1]]) + +def test_cross(): + a = [1, 2, 3] + b = [3, 4, 5] + col = Matrix([-2, 4, -2]) + row = col.T + + def test(M, ans): + assert ans == M + assert type(M) == cls + for cls in classes: + A = cls(a) + B = cls(b) + test(A.cross(B), col) + test(A.cross(B.T), col) + test(A.T.cross(B.T), row) + test(A.T.cross(B), row) + raises(ShapeError, lambda: + Matrix(1, 2, [1, 1]).cross(Matrix(1, 2, [1, 1]))) + +def test_hat_vee(): + v1 = Matrix([x, y, z]) + v2 = Matrix([a, b, c]) + assert v1.hat() * v2 == v1.cross(v2) + assert v1.hat().is_anti_symmetric() + assert v1.hat().vee() == v1 + +def test_hash(): + for cls in classes[-2:]: + s = {cls.eye(1), cls.eye(1)} + assert len(s) == 1 and s.pop() == cls.eye(1) + # issue 3979 + for cls in classes[:2]: + assert not isinstance(cls.eye(1), Hashable) + + +@XFAIL +def test_issue_3979(): + # when this passes, delete this and change the [1:2] + # to [:2] in the test_hash above for issue 3979 + cls = classes[0] + raises(AttributeError, lambda: hash(cls.eye(1))) + + +def test_adjoint(): + dat = [[0, I], [1, 0]] + ans = Matrix([[0, 1], [-I, 0]]) + for cls in classes: + assert ans == cls(dat).adjoint() + + +def test_adjoint_with_operator(): + # Regression test for issue 25130: adjoint() should propagate to operators + import sympy.physics.quantum + a = sympy.physics.quantum.operator.Operator('a') + a_dag = sympy.physics.quantum.Dagger(a) + dat = [[0, I * a], [0, a_dag]] + ans = Matrix([[0, 0], [-I * a_dag, a]]) + for cls in classes: + assert ans == cls(dat).adjoint() + + +def test_simplify_immutable(): + assert simplify(ImmutableMatrix([[sin(x)**2 + cos(x)**2]])) == \ + ImmutableMatrix([[1]]) + +def test_replace(): + F, G = symbols('F, G', cls=Function) + K = Matrix(2, 2, lambda i, j: G(i+j)) + M = Matrix(2, 2, lambda i, j: F(i+j)) + N = M.replace(F, G) + assert N == K + + +def test_atoms(): + m = Matrix([[1, 2], [x, 1 - 1/x]]) + assert m.atoms() == {S.One,S(2),S.NegativeOne, x} + assert m.atoms(Symbol) == {x} + + +def test_pinv(): + # Pseudoinverse of an invertible matrix is the inverse. + A1 = Matrix([[a, b], [c, d]]) + assert simplify(A1.pinv(method="RD")) == simplify(A1.inv()) + + # Test the four properties of the pseudoinverse for various matrices. + As = [Matrix([[13, 104], [2212, 3], [-3, 5]]), + Matrix([[1, 7, 9], [11, 17, 19]]), + Matrix([a, b])] + + for A in As: + A_pinv = A.pinv(method="RD") + AAp = A * A_pinv + ApA = A_pinv * A + assert simplify(AAp * A) == A + assert simplify(ApA * A_pinv) == A_pinv + assert AAp.H == AAp + assert ApA.H == ApA + + # XXX Pinv with diagonalization makes expression too complicated. + for A in As: + A_pinv = simplify(A.pinv(method="ED")) + AAp = A * A_pinv + ApA = A_pinv * A + assert simplify(AAp * A) == A + assert simplify(ApA * A_pinv) == A_pinv + assert AAp.H == AAp + assert ApA.H == ApA + + # XXX Computing pinv using diagonalization makes an expression that + # is too complicated to simplify. + # A1 = Matrix([[a, b], [c, d]]) + # assert simplify(A1.pinv(method="ED")) == simplify(A1.inv()) + # so this is tested numerically at a fixed random point + + from sympy.core.numbers import comp + q = A1.pinv(method="ED") + w = A1.inv() + reps = {a: -73633, b: 11362, c: 55486, d: 62570} + assert all( + comp(i.n(), j.n()) + for i, j in zip(q.subs(reps), w.subs(reps)) + ) + + +@slow +def test_pinv_rank_deficient_when_diagonalization_fails(): + # Test the four properties of the pseudoinverse for matrices when + # diagonalization of A.H*A fails. + As = [ + Matrix([ + [61, 89, 55, 20, 71, 0], + [62, 96, 85, 85, 16, 0], + [69, 56, 17, 4, 54, 0], + [10, 54, 91, 41, 71, 0], + [ 7, 30, 10, 48, 90, 0], + [0, 0, 0, 0, 0, 0]]) + ] + for A in As: + A_pinv = A.pinv(method="ED") + AAp = A * A_pinv + ApA = A_pinv * A + assert AAp.H == AAp + + # Here ApA.H and ApA are equivalent expressions but they are very + # complicated expressions involving RootOfs. Using simplify would be + # too slow and so would evalf so we substitute approximate values for + # the RootOfs and then evalf which is less accurate but good enough to + # confirm that these two matrices are equivalent. + # + # assert ApA.H == ApA # <--- would fail (structural equality) + # assert simplify(ApA.H - ApA).is_zero_matrix # <--- too slow + # (ApA.H - ApA).evalf() # <--- too slow + + def allclose(M1, M2): + rootofs = M1.atoms(RootOf) + rootofs_approx = {r: r.evalf() for r in rootofs} + diff_approx = (M1 - M2).xreplace(rootofs_approx).evalf() + return all(abs(e) < 1e-10 for e in diff_approx) + + assert allclose(ApA.H, ApA) + + +def test_issue_7201(): + assert ones(0, 1) + ones(0, 1) == Matrix(0, 1, []) + assert ones(1, 0) + ones(1, 0) == Matrix(1, 0, []) + +def test_free_symbols(): + for M in ImmutableMatrix, ImmutableSparseMatrix, Matrix, SparseMatrix: + assert M([[x], [0]]).free_symbols == {x} + +def test_from_ndarray(): + """See issue 7465.""" + try: + from numpy import array + except ImportError: + skip('NumPy must be available to test creating matrices from ndarrays') + + assert Matrix(array([1, 2, 3])) == Matrix([1, 2, 3]) + assert Matrix(array([[1, 2, 3]])) == Matrix([[1, 2, 3]]) + assert Matrix(array([[1, 2, 3], [4, 5, 6]])) == \ + Matrix([[1, 2, 3], [4, 5, 6]]) + assert Matrix(array([x, y, z])) == Matrix([x, y, z]) + raises(NotImplementedError, + lambda: Matrix(array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]))) + assert Matrix([array([1, 2]), array([3, 4])]) == Matrix([[1, 2], [3, 4]]) + assert Matrix([array([1, 2]), [3, 4]]) == Matrix([[1, 2], [3, 4]]) + assert Matrix([array([]), array([])]) == Matrix(2, 0, []) != Matrix(0, 0, []) + +def test_17522_numpy(): + from sympy.matrices.common import _matrixify + try: + from numpy import array, matrix + except ImportError: + skip('NumPy must be available to test indexing matrixified NumPy ndarrays and matrices') + + m = _matrixify(array([[1, 2], [3, 4]])) + assert m[3] == 4 + assert list(m) == [1, 2, 3, 4] + + with ignore_warnings(PendingDeprecationWarning): + m = _matrixify(matrix([[1, 2], [3, 4]])) + assert m[3] == 4 + assert list(m) == [1, 2, 3, 4] + +def test_17522_mpmath(): + from sympy.matrices.common import _matrixify + try: + from mpmath import matrix + except ImportError: + skip('mpmath must be available to test indexing matrixified mpmath matrices') + + m = _matrixify(matrix([[1, 2], [3, 4]])) + assert m[3] == 4.0 + assert list(m) == [1.0, 2.0, 3.0, 4.0] + +def test_17522_scipy(): + from sympy.matrices.common import _matrixify + try: + from scipy.sparse import csr_matrix + except ImportError: + skip('SciPy must be available to test indexing matrixified SciPy sparse matrices') + + m = _matrixify(csr_matrix([[1, 2], [3, 4]])) + assert m[3] == 4 + assert list(m) == [1, 2, 3, 4] + +def test_hermitian(): + a = Matrix([[1, I], [-I, 1]]) + assert a.is_hermitian + a[0, 0] = 2*I + assert a.is_hermitian is False + a[0, 0] = x + assert a.is_hermitian is None + a[0, 1] = a[1, 0]*I + assert a.is_hermitian is False + b = HermitianOperator("b") + c = Operator("c") + assert Matrix([[b]]).is_hermitian is True + assert Matrix([[b, c], [Dagger(c), b]]).is_hermitian is True + assert Matrix([[b, c], [c, b]]).is_hermitian is False + assert Matrix([[b, c], [transpose(c), b]]).is_hermitian is False + +def test_doit(): + a = Matrix([[Add(x,x, evaluate=False)]]) + assert a[0] != 2*x + assert a.doit() == Matrix([[2*x]]) + +def test_issue_9457_9467_9876(): + # for row_del(index) + M = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + M.row_del(1) + assert M == Matrix([[1, 2, 3], [3, 4, 5]]) + N = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + N.row_del(-2) + assert N == Matrix([[1, 2, 3], [3, 4, 5]]) + O = Matrix([[1, 2, 3], [5, 6, 7], [9, 10, 11]]) + O.row_del(-1) + assert O == Matrix([[1, 2, 3], [5, 6, 7]]) + P = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + raises(IndexError, lambda: P.row_del(10)) + Q = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + raises(IndexError, lambda: Q.row_del(-10)) + + # for col_del(index) + M = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + M.col_del(1) + assert M == Matrix([[1, 3], [2, 4], [3, 5]]) + N = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + N.col_del(-2) + assert N == Matrix([[1, 3], [2, 4], [3, 5]]) + P = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + raises(IndexError, lambda: P.col_del(10)) + Q = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + raises(IndexError, lambda: Q.col_del(-10)) + +def test_issue_9422(): + x, y = symbols('x y', commutative=False) + a, b = symbols('a b') + M = eye(2) + M1 = Matrix(2, 2, [x, y, y, z]) + assert y*x*M != x*y*M + assert b*a*M == a*b*M + assert x*M1 != M1*x + assert a*M1 == M1*a + assert y*x*M == Matrix([[y*x, 0], [0, y*x]]) + + +def test_issue_10770(): + M = Matrix([]) + a = ['col_insert', 'row_join'], Matrix([9, 6, 3]) + b = ['row_insert', 'col_join'], a[1].T + c = ['row_insert', 'col_insert'], Matrix([[1, 2], [3, 4]]) + for ops, m in (a, b, c): + for op in ops: + f = getattr(M, op) + new = f(m) if 'join' in op else f(42, m) + assert new == m and id(new) != id(m) + + +def test_issue_10658(): + A = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + assert A.extract([0, 1, 2], [True, True, False]) == \ + Matrix([[1, 2], [4, 5], [7, 8]]) + assert A.extract([0, 1, 2], [True, False, False]) == Matrix([[1], [4], [7]]) + assert A.extract([True, False, False], [0, 1, 2]) == Matrix([[1, 2, 3]]) + assert A.extract([True, False, True], [0, 1, 2]) == \ + Matrix([[1, 2, 3], [7, 8, 9]]) + assert A.extract([0, 1, 2], [False, False, False]) == Matrix(3, 0, []) + assert A.extract([False, False, False], [0, 1, 2]) == Matrix(0, 3, []) + assert A.extract([True, False, True], [False, True, False]) == \ + Matrix([[2], [8]]) + +def test_opportunistic_simplification(): + # this test relates to issue #10718, #9480, #11434 + + # issue #9480 + m = Matrix([[-5 + 5*sqrt(2), -5], [-5*sqrt(2)/2 + 5, -5*sqrt(2)/2]]) + assert m.rank() == 1 + + # issue #10781 + m = Matrix([[3+3*sqrt(3)*I, -9],[4,-3+3*sqrt(3)*I]]) + assert simplify(m.rref()[0] - Matrix([[1, -9/(3 + 3*sqrt(3)*I)], [0, 0]])) == zeros(2, 2) + + # issue #11434 + ax,ay,bx,by,cx,cy,dx,dy,ex,ey,t0,t1 = symbols('a_x a_y b_x b_y c_x c_y d_x d_y e_x e_y t_0 t_1') + m = Matrix([[ax,ay,ax*t0,ay*t0,0],[bx,by,bx*t0,by*t0,0],[cx,cy,cx*t0,cy*t0,1],[dx,dy,dx*t0,dy*t0,1],[ex,ey,2*ex*t1-ex*t0,2*ey*t1-ey*t0,0]]) + assert m.rank() == 4 + +def test_partial_pivoting(): + # example from https://en.wikipedia.org/wiki/Pivot_element + # partial pivoting with back substitution gives a perfect result + # naive pivoting give an error ~1e-13, so anything better than + # 1e-15 is good + mm=Matrix([[0.003, 59.14, 59.17], [5.291, -6.13, 46.78]]) + assert (mm.rref()[0] - Matrix([[1.0, 0, 10.0], + [ 0, 1.0, 1.0]])).norm() < 1e-15 + + # issue #11549 + m_mixed = Matrix([[6e-17, 1.0, 4], + [ -1.0, 0, 8], + [ 0, 0, 1]]) + m_float = Matrix([[6e-17, 1.0, 4.], + [ -1.0, 0., 8.], + [ 0., 0., 1.]]) + m_inv = Matrix([[ 0, -1.0, 8.0], + [1.0, 6.0e-17, -4.0], + [ 0, 0, 1]]) + # this example is numerically unstable and involves a matrix with a norm >= 8, + # this comparing the difference of the results with 1e-15 is numerically sound. + assert (m_mixed.inv() - m_inv).norm() < 1e-15 + assert (m_float.inv() - m_inv).norm() < 1e-15 + +def test_iszero_substitution(): + """ When doing numerical computations, all elements that pass + the iszerofunc test should be set to numerically zero if they + aren't already. """ + + # Matrix from issue #9060 + m = Matrix([[0.9, -0.1, -0.2, 0],[-0.8, 0.9, -0.4, 0],[-0.1, -0.8, 0.6, 0]]) + m_rref = m.rref(iszerofunc=lambda x: abs(x)<6e-15)[0] + m_correct = Matrix([[1.0, 0, -0.301369863013699, 0],[ 0, 1.0, -0.712328767123288, 0],[ 0, 0, 0, 0]]) + m_diff = m_rref - m_correct + assert m_diff.norm() < 1e-15 + # if a zero-substitution wasn't made, this entry will be -1.11022302462516e-16 + assert m_rref[2,2] == 0 + +def test_issue_11238(): + from sympy.geometry.point import Point + xx = 8*tan(pi*Rational(13, 45))/(tan(pi*Rational(13, 45)) + sqrt(3)) + yy = (-8*sqrt(3)*tan(pi*Rational(13, 45))**2 + 24*tan(pi*Rational(13, 45)))/(-3 + tan(pi*Rational(13, 45))**2) + p1 = Point(0, 0) + p2 = Point(1, -sqrt(3)) + p0 = Point(xx,yy) + m1 = Matrix([p1 - simplify(p0), p2 - simplify(p0)]) + m2 = Matrix([p1 - p0, p2 - p0]) + m3 = Matrix([simplify(p1 - p0), simplify(p2 - p0)]) + + # This system has expressions which are zero and + # cannot be easily proved to be such, so without + # numerical testing, these assertions will fail. + Z = lambda x: abs(x.n()) < 1e-20 + assert m1.rank(simplify=True, iszerofunc=Z) == 1 + assert m2.rank(simplify=True, iszerofunc=Z) == 1 + assert m3.rank(simplify=True, iszerofunc=Z) == 1 + +def test_as_real_imag(): + m1 = Matrix(2,2,[1,2,3,4]) + m2 = m1*S.ImaginaryUnit + m3 = m1 + m2 + + for kls in classes: + a,b = kls(m3).as_real_imag() + assert list(a) == list(m1) + assert list(b) == list(m1) + +def test_deprecated(): + # Maintain tests for deprecated functions. We must capture + # the deprecation warnings. When the deprecated functionality is + # removed, the corresponding tests should be removed. + + m = Matrix(3, 3, [0, 1, 0, -4, 4, 0, -2, 1, 2]) + P, Jcells = m.jordan_cells() + assert Jcells[1] == Matrix(1, 1, [2]) + assert Jcells[0] == Matrix(2, 2, [2, 1, 0, 2]) + + +def test_issue_14489(): + from sympy.core.mod import Mod + A = Matrix([-1, 1, 2]) + B = Matrix([10, 20, -15]) + + assert Mod(A, 3) == Matrix([2, 1, 2]) + assert Mod(B, 4) == Matrix([2, 0, 1]) + +def test_issue_14943(): + # Test that __array__ accepts the optional dtype argument + try: + from numpy import array + except ImportError: + skip('NumPy must be available to test creating matrices from ndarrays') + + M = Matrix([[1,2], [3,4]]) + assert array(M, dtype=float).dtype.name == 'float64' + +def test_case_6913(): + m = MatrixSymbol('m', 1, 1) + a = Symbol("a") + a = m[0, 0]>0 + assert str(a) == 'm[0, 0] > 0' + +def test_issue_11948(): + A = MatrixSymbol('A', 3, 3) + a = Wild('a') + assert A.match(a) == {a: A} + +def test_gramschmidt_conjugate_dot(): + vecs = [Matrix([1, I]), Matrix([1, -I])] + assert Matrix.orthogonalize(*vecs) == \ + [Matrix([[1], [I]]), Matrix([[1], [-I]])] + + vecs = [Matrix([1, I, 0]), Matrix([I, 0, -I])] + assert Matrix.orthogonalize(*vecs) == \ + [Matrix([[1], [I], [0]]), Matrix([[I/2], [S(1)/2], [-I]])] + + mat = Matrix([[1, I], [1, -I]]) + Q, R = mat.QRdecomposition() + assert Q * Q.H == Matrix.eye(2) + +def test_issue_8207(): + a = Matrix(MatrixSymbol('a', 3, 1)) + b = Matrix(MatrixSymbol('b', 3, 1)) + c = a.dot(b) + d = diff(c, a[0, 0]) + e = diff(d, a[0, 0]) + assert d == b[0, 0] + assert e == 0 + +def test_func(): + from sympy.simplify.simplify import nthroot + + A = Matrix([[1, 2],[0, 3]]) + assert A.analytic_func(sin(x*t), x) == Matrix([[sin(t), sin(3*t) - sin(t)], [0, sin(3*t)]]) + + A = Matrix([[2, 1],[1, 2]]) + assert (pi * A / 6).analytic_func(cos(x), x) == Matrix([[sqrt(3)/4, -sqrt(3)/4], [-sqrt(3)/4, sqrt(3)/4]]) + + + raises(ValueError, lambda : zeros(5).analytic_func(log(x), x)) + raises(ValueError, lambda : (A*x).analytic_func(log(x), x)) + + A = Matrix([[0, -1, -2, 3], [0, -1, -2, 3], [0, 1, 0, -1], [0, 0, -1, 1]]) + assert A.analytic_func(exp(x), x) == A.exp() + raises(ValueError, lambda : A.analytic_func(sqrt(x), x)) + + A = Matrix([[41, 12],[12, 34]]) + assert simplify(A.analytic_func(sqrt(x), x)**2) == A + + A = Matrix([[3, -12, 4], [-1, 0, -2], [-1, 5, -1]]) + assert simplify(A.analytic_func(nthroot(x, 3), x)**3) == A + + A = Matrix([[2, 0, 0, 0], [1, 2, 0, 0], [0, 1, 3, 0], [0, 0, 1, 3]]) + assert A.analytic_func(exp(x), x) == A.exp() + + A = Matrix([[0, 2, 1, 6], [0, 0, 1, 2], [0, 0, 0, 3], [0, 0, 0, 0]]) + assert A.analytic_func(exp(x*t), x) == expand(simplify((A*t).exp())) + + +@skip_under_pyodide("Cannot create threads under pyodide.") +def test_issue_19809(): + + def f(): + assert _dotprodsimp_state.state == None + m = Matrix([[1]]) + m = m * m + return True + + with dotprodsimp(True): + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(f) + assert future.result() + + +def test_issue_23276(): + M = Matrix([x, y]) + assert integrate(M, (x, 0, 1), (y, 0, 1)) == Matrix([ + [S.Half], + [S.Half]]) + + +# SubspaceOnlyMatrix tests +def test_columnspace_one(): + m = SubspaceOnlyMatrix([[ 1, 2, 0, 2, 5], + [-2, -5, 1, -1, -8], + [ 0, -3, 3, 4, 1], + [ 3, 6, 0, -7, 2]]) + + basis = m.columnspace() + assert basis[0] == Matrix([1, -2, 0, 3]) + assert basis[1] == Matrix([2, -5, -3, 6]) + assert basis[2] == Matrix([2, -1, 4, -7]) + + assert len(basis) == 3 + assert Matrix.hstack(m, *basis).columnspace() == basis + + +def test_rowspace(): + m = SubspaceOnlyMatrix([[ 1, 2, 0, 2, 5], + [-2, -5, 1, -1, -8], + [ 0, -3, 3, 4, 1], + [ 3, 6, 0, -7, 2]]) + + basis = m.rowspace() + assert basis[0] == Matrix([[1, 2, 0, 2, 5]]) + assert basis[1] == Matrix([[0, -1, 1, 3, 2]]) + assert basis[2] == Matrix([[0, 0, 0, 5, 5]]) + + assert len(basis) == 3 + + +def test_nullspace_one(): + m = SubspaceOnlyMatrix([[ 1, 2, 0, 2, 5], + [-2, -5, 1, -1, -8], + [ 0, -3, 3, 4, 1], + [ 3, 6, 0, -7, 2]]) + + basis = m.nullspace() + assert basis[0] == Matrix([-2, 1, 1, 0, 0]) + assert basis[1] == Matrix([-1, -1, 0, -1, 1]) + # make sure the null space is really gets zeroed + assert all(e.is_zero for e in m*basis[0]) + assert all(e.is_zero for e in m*basis[1]) + + +# ReductionsOnlyMatrix tests +def test_row_op(): + e = eye_Reductions(3) + + raises(ValueError, lambda: e.elementary_row_op("abc")) + raises(ValueError, lambda: e.elementary_row_op()) + raises(ValueError, lambda: e.elementary_row_op('n->kn', row=5, k=5)) + raises(ValueError, lambda: e.elementary_row_op('n->kn', row=-5, k=5)) + raises(ValueError, lambda: e.elementary_row_op('n<->m', row1=1, row2=5)) + raises(ValueError, lambda: e.elementary_row_op('n<->m', row1=5, row2=1)) + raises(ValueError, lambda: e.elementary_row_op('n<->m', row1=-5, row2=1)) + raises(ValueError, lambda: e.elementary_row_op('n<->m', row1=1, row2=-5)) + raises(ValueError, lambda: e.elementary_row_op('n->n+km', row1=1, row2=5, k=5)) + raises(ValueError, lambda: e.elementary_row_op('n->n+km', row1=5, row2=1, k=5)) + raises(ValueError, lambda: e.elementary_row_op('n->n+km', row1=-5, row2=1, k=5)) + raises(ValueError, lambda: e.elementary_row_op('n->n+km', row1=1, row2=-5, k=5)) + raises(ValueError, lambda: e.elementary_row_op('n->n+km', row1=1, row2=1, k=5)) + + # test various ways to set arguments + assert e.elementary_row_op("n->kn", 0, 5) == Matrix([[5, 0, 0], [0, 1, 0], [0, 0, 1]]) + assert e.elementary_row_op("n->kn", 1, 5) == Matrix([[1, 0, 0], [0, 5, 0], [0, 0, 1]]) + assert e.elementary_row_op("n->kn", row=1, k=5) == Matrix([[1, 0, 0], [0, 5, 0], [0, 0, 1]]) + assert e.elementary_row_op("n->kn", row1=1, k=5) == Matrix([[1, 0, 0], [0, 5, 0], [0, 0, 1]]) + assert e.elementary_row_op("n<->m", 0, 1) == Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) + assert e.elementary_row_op("n<->m", row1=0, row2=1) == Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) + assert e.elementary_row_op("n<->m", row=0, row2=1) == Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) + assert e.elementary_row_op("n->n+km", 0, 5, 1) == Matrix([[1, 5, 0], [0, 1, 0], [0, 0, 1]]) + assert e.elementary_row_op("n->n+km", row=0, k=5, row2=1) == Matrix([[1, 5, 0], [0, 1, 0], [0, 0, 1]]) + assert e.elementary_row_op("n->n+km", row1=0, k=5, row2=1) == Matrix([[1, 5, 0], [0, 1, 0], [0, 0, 1]]) + + # make sure the matrix doesn't change size + a = ReductionsOnlyMatrix(2, 3, [0]*6) + assert a.elementary_row_op("n->kn", 1, 5) == Matrix(2, 3, [0]*6) + assert a.elementary_row_op("n<->m", 0, 1) == Matrix(2, 3, [0]*6) + assert a.elementary_row_op("n->n+km", 0, 5, 1) == Matrix(2, 3, [0]*6) + + +def test_col_op(): + e = eye_Reductions(3) + + raises(ValueError, lambda: e.elementary_col_op("abc")) + raises(ValueError, lambda: e.elementary_col_op()) + raises(ValueError, lambda: e.elementary_col_op('n->kn', col=5, k=5)) + raises(ValueError, lambda: e.elementary_col_op('n->kn', col=-5, k=5)) + raises(ValueError, lambda: e.elementary_col_op('n<->m', col1=1, col2=5)) + raises(ValueError, lambda: e.elementary_col_op('n<->m', col1=5, col2=1)) + raises(ValueError, lambda: e.elementary_col_op('n<->m', col1=-5, col2=1)) + raises(ValueError, lambda: e.elementary_col_op('n<->m', col1=1, col2=-5)) + raises(ValueError, lambda: e.elementary_col_op('n->n+km', col1=1, col2=5, k=5)) + raises(ValueError, lambda: e.elementary_col_op('n->n+km', col1=5, col2=1, k=5)) + raises(ValueError, lambda: e.elementary_col_op('n->n+km', col1=-5, col2=1, k=5)) + raises(ValueError, lambda: e.elementary_col_op('n->n+km', col1=1, col2=-5, k=5)) + raises(ValueError, lambda: e.elementary_col_op('n->n+km', col1=1, col2=1, k=5)) + + # test various ways to set arguments + assert e.elementary_col_op("n->kn", 0, 5) == Matrix([[5, 0, 0], [0, 1, 0], [0, 0, 1]]) + assert e.elementary_col_op("n->kn", 1, 5) == Matrix([[1, 0, 0], [0, 5, 0], [0, 0, 1]]) + assert e.elementary_col_op("n->kn", col=1, k=5) == Matrix([[1, 0, 0], [0, 5, 0], [0, 0, 1]]) + assert e.elementary_col_op("n->kn", col1=1, k=5) == Matrix([[1, 0, 0], [0, 5, 0], [0, 0, 1]]) + assert e.elementary_col_op("n<->m", 0, 1) == Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) + assert e.elementary_col_op("n<->m", col1=0, col2=1) == Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) + assert e.elementary_col_op("n<->m", col=0, col2=1) == Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) + assert e.elementary_col_op("n->n+km", 0, 5, 1) == Matrix([[1, 0, 0], [5, 1, 0], [0, 0, 1]]) + assert e.elementary_col_op("n->n+km", col=0, k=5, col2=1) == Matrix([[1, 0, 0], [5, 1, 0], [0, 0, 1]]) + assert e.elementary_col_op("n->n+km", col1=0, k=5, col2=1) == Matrix([[1, 0, 0], [5, 1, 0], [0, 0, 1]]) + + # make sure the matrix doesn't change size + a = ReductionsOnlyMatrix(2, 3, [0]*6) + assert a.elementary_col_op("n->kn", 1, 5) == Matrix(2, 3, [0]*6) + assert a.elementary_col_op("n<->m", 0, 1) == Matrix(2, 3, [0]*6) + assert a.elementary_col_op("n->n+km", 0, 5, 1) == Matrix(2, 3, [0]*6) + + +def test_is_echelon(): + zro = zeros_Reductions(3) + ident = eye_Reductions(3) + + assert zro.is_echelon + assert ident.is_echelon + + a = ReductionsOnlyMatrix(0, 0, []) + assert a.is_echelon + + a = ReductionsOnlyMatrix(2, 3, [3, 2, 1, 0, 0, 6]) + assert a.is_echelon + + a = ReductionsOnlyMatrix(2, 3, [0, 0, 6, 3, 2, 1]) + assert not a.is_echelon + + x = Symbol('x') + a = ReductionsOnlyMatrix(3, 1, [x, 0, 0]) + assert a.is_echelon + + a = ReductionsOnlyMatrix(3, 1, [x, x, 0]) + assert not a.is_echelon + + a = ReductionsOnlyMatrix(3, 3, [0, 0, 0, 1, 2, 3, 0, 0, 0]) + assert not a.is_echelon + + +def test_echelon_form(): + # echelon form is not unique, but the result + # must be row-equivalent to the original matrix + # and it must be in echelon form. + + a = zeros_Reductions(3) + e = eye_Reductions(3) + + # we can assume the zero matrix and the identity matrix shouldn't change + assert a.echelon_form() == a + assert e.echelon_form() == e + + a = ReductionsOnlyMatrix(0, 0, []) + assert a.echelon_form() == a + + a = ReductionsOnlyMatrix(1, 1, [5]) + assert a.echelon_form() == a + + # now we get to the real tests + + def verify_row_null_space(mat, rows, nulls): + for v in nulls: + assert all(t.is_zero for t in a_echelon*v) + for v in rows: + if not all(t.is_zero for t in v): + assert not all(t.is_zero for t in a_echelon*v.transpose()) + + a = ReductionsOnlyMatrix(3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 9]) + nulls = [Matrix([ + [ 1], + [-2], + [ 1]])] + rows = [a[i, :] for i in range(a.rows)] + a_echelon = a.echelon_form() + assert a_echelon.is_echelon + verify_row_null_space(a, rows, nulls) + + + a = ReductionsOnlyMatrix(3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 8]) + nulls = [] + rows = [a[i, :] for i in range(a.rows)] + a_echelon = a.echelon_form() + assert a_echelon.is_echelon + verify_row_null_space(a, rows, nulls) + + a = ReductionsOnlyMatrix(3, 3, [2, 1, 3, 0, 0, 0, 2, 1, 3]) + nulls = [Matrix([ + [Rational(-1, 2)], + [ 1], + [ 0]]), + Matrix([ + [Rational(-3, 2)], + [ 0], + [ 1]])] + rows = [a[i, :] for i in range(a.rows)] + a_echelon = a.echelon_form() + assert a_echelon.is_echelon + verify_row_null_space(a, rows, nulls) + + # this one requires a row swap + a = ReductionsOnlyMatrix(3, 3, [2, 1, 3, 0, 0, 0, 1, 1, 3]) + nulls = [Matrix([ + [ 0], + [ -3], + [ 1]])] + rows = [a[i, :] for i in range(a.rows)] + a_echelon = a.echelon_form() + assert a_echelon.is_echelon + verify_row_null_space(a, rows, nulls) + + a = ReductionsOnlyMatrix(3, 3, [0, 3, 3, 0, 2, 2, 0, 1, 1]) + nulls = [Matrix([ + [1], + [0], + [0]]), + Matrix([ + [ 0], + [-1], + [ 1]])] + rows = [a[i, :] for i in range(a.rows)] + a_echelon = a.echelon_form() + assert a_echelon.is_echelon + verify_row_null_space(a, rows, nulls) + + a = ReductionsOnlyMatrix(2, 3, [2, 2, 3, 3, 3, 0]) + nulls = [Matrix([ + [-1], + [1], + [0]])] + rows = [a[i, :] for i in range(a.rows)] + a_echelon = a.echelon_form() + assert a_echelon.is_echelon + verify_row_null_space(a, rows, nulls) + + +def test_rref(): + e = ReductionsOnlyMatrix(0, 0, []) + assert e.rref(pivots=False) == e + + e = ReductionsOnlyMatrix(1, 1, [1]) + a = ReductionsOnlyMatrix(1, 1, [5]) + assert e.rref(pivots=False) == a.rref(pivots=False) == e + + a = ReductionsOnlyMatrix(3, 1, [1, 2, 3]) + assert a.rref(pivots=False) == Matrix([[1], [0], [0]]) + + a = ReductionsOnlyMatrix(1, 3, [1, 2, 3]) + assert a.rref(pivots=False) == Matrix([[1, 2, 3]]) + + a = ReductionsOnlyMatrix(3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 9]) + assert a.rref(pivots=False) == Matrix([ + [1, 0, -1], + [0, 1, 2], + [0, 0, 0]]) + + a = ReductionsOnlyMatrix(3, 3, [1, 2, 3, 1, 2, 3, 1, 2, 3]) + b = ReductionsOnlyMatrix(3, 3, [1, 2, 3, 0, 0, 0, 0, 0, 0]) + c = ReductionsOnlyMatrix(3, 3, [0, 0, 0, 1, 2, 3, 0, 0, 0]) + d = ReductionsOnlyMatrix(3, 3, [0, 0, 0, 0, 0, 0, 1, 2, 3]) + assert a.rref(pivots=False) == \ + b.rref(pivots=False) == \ + c.rref(pivots=False) == \ + d.rref(pivots=False) == b + + e = eye_Reductions(3) + z = zeros_Reductions(3) + assert e.rref(pivots=False) == e + assert z.rref(pivots=False) == z + + a = ReductionsOnlyMatrix([ + [ 0, 0, 1, 2, 2, -5, 3], + [-1, 5, 2, 2, 1, -7, 5], + [ 0, 0, -2, -3, -3, 8, -5], + [-1, 5, 0, -1, -2, 1, 0]]) + mat, pivot_offsets = a.rref() + assert mat == Matrix([ + [1, -5, 0, 0, 1, 1, -1], + [0, 0, 1, 0, 0, -1, 1], + [0, 0, 0, 1, 1, -2, 1], + [0, 0, 0, 0, 0, 0, 0]]) + assert pivot_offsets == (0, 2, 3) + + a = ReductionsOnlyMatrix([[Rational(1, 19), Rational(1, 5), 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11], + [ 12, 13, 14, 15]]) + assert a.rref(pivots=False) == Matrix([ + [1, 0, 0, Rational(-76, 157)], + [0, 1, 0, Rational(-5, 157)], + [0, 0, 1, Rational(238, 157)], + [0, 0, 0, 0]]) + + x = Symbol('x') + a = ReductionsOnlyMatrix(2, 3, [x, 1, 1, sqrt(x), x, 1]) + for i, j in zip(a.rref(pivots=False), + [1, 0, sqrt(x)*(-x + 1)/(-x**Rational(5, 2) + x), + 0, 1, 1/(sqrt(x) + x + 1)]): + assert simplify(i - j).is_zero + + +def test_rref_rhs(): + a, b, c, d = symbols('a b c d') + A = Matrix([[0, 0], [0, 0], [1, 2], [3, 4]]) + B = Matrix([a, b, c, d]) + assert A.rref_rhs(B) == (Matrix([ + [1, 0], + [0, 1], + [0, 0], + [0, 0]]), Matrix([ + [ -2*c + d], + [3*c/2 - d/2], + [ a], + [ b]])) + + +def test_issue_17827(): + C = Matrix([ + [3, 4, -1, 1], + [9, 12, -3, 3], + [0, 2, 1, 3], + [2, 3, 0, -2], + [0, 3, 3, -5], + [8, 15, 0, 6] + ]) + # Tests for row/col within valid range + D = C.elementary_row_op('n<->m', row1=2, row2=5) + E = C.elementary_row_op('n->n+km', row1=5, row2=3, k=-4) + F = C.elementary_row_op('n->kn', row=5, k=2) + assert(D[5, :] == Matrix([[0, 2, 1, 3]])) + assert(E[5, :] == Matrix([[0, 3, 0, 14]])) + assert(F[5, :] == Matrix([[16, 30, 0, 12]])) + # Tests for row/col out of range + raises(ValueError, lambda: C.elementary_row_op('n<->m', row1=2, row2=6)) + raises(ValueError, lambda: C.elementary_row_op('n->kn', row=7, k=2)) + raises(ValueError, lambda: C.elementary_row_op('n->n+km', row1=-1, row2=5, k=2)) + +def test_rank(): + m = Matrix([[1, 2], [x, 1 - 1/x]]) + assert m.rank() == 2 + n = Matrix(3, 3, range(1, 10)) + assert n.rank() == 2 + p = zeros(3) + assert p.rank() == 0 + +def test_issue_11434(): + ax, ay, bx, by, cx, cy, dx, dy, ex, ey, t0, t1 = \ + symbols('a_x a_y b_x b_y c_x c_y d_x d_y e_x e_y t_0 t_1') + M = Matrix([[ax, ay, ax*t0, ay*t0, 0], + [bx, by, bx*t0, by*t0, 0], + [cx, cy, cx*t0, cy*t0, 1], + [dx, dy, dx*t0, dy*t0, 1], + [ex, ey, 2*ex*t1 - ex*t0, 2*ey*t1 - ey*t0, 0]]) + assert M.rank() == 4 + +def test_rank_regression_from_so(): + # see: + # https://stackoverflow.com/questions/19072700/why-does-sympy-give-me-the-wrong-answer-when-i-row-reduce-a-symbolic-matrix + + nu, lamb = symbols('nu, lambda') + A = Matrix([[-3*nu, 1, 0, 0], + [ 3*nu, -2*nu - 1, 2, 0], + [ 0, 2*nu, (-1*nu) - lamb - 2, 3], + [ 0, 0, nu + lamb, -3]]) + expected_reduced = Matrix([[1, 0, 0, 1/(nu**2*(-lamb - nu))], + [0, 1, 0, 3/(nu*(-lamb - nu))], + [0, 0, 1, 3/(-lamb - nu)], + [0, 0, 0, 0]]) + expected_pivots = (0, 1, 2) + + reduced, pivots = A.rref() + + assert simplify(expected_reduced - reduced) == zeros(*A.shape) + assert pivots == expected_pivots + +def test_issue_15872(): + A = Matrix([[1, 1, 1, 0], [-2, -1, 0, -1], [0, 0, -1, -1], [0, 0, 2, 1]]) + B = A - Matrix.eye(4) * I + assert B.rank() == 3 + assert (B**2).rank() == 2 + assert (B**3).rank() == 2 diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_matrixbase.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_matrixbase.py new file mode 100644 index 0000000000000000000000000000000000000000..a77f51596c6622dc427feeeb9383214592fab632 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_matrixbase.py @@ -0,0 +1,3795 @@ +import concurrent.futures +import random +from collections.abc import Hashable + +from sympy import ( + Abs, Add, Array, DeferredVector, E, Expr, FiniteSet, Float, Function, + GramSchmidt, I, ImmutableDenseMatrix, ImmutableMatrix, + ImmutableSparseMatrix, Integer, KroneckerDelta, MatPow, Matrix, + MatrixSymbol, Max, Min, MutableDenseMatrix, MutableSparseMatrix, Poly, Pow, + PurePoly, Q, Quaternion, Rational, RootOf, S, SparseMatrix, Symbol, Tuple, + Wild, banded, casoratian, cos, diag, diff, exp, expand, eye, floor, hessian, + integrate, log, matrix_multiply_elementwise, nan, ones, oo, pi, randMatrix, + rot_axis1, rot_axis2, rot_axis3, rot_ccw_axis1, rot_ccw_axis2, + rot_ccw_axis3, signsimp, simplify, sin, sqrt, sstr, symbols, sympify, tan, + trigsimp, wronskian, zeros, cancel) +from sympy.abc import a, b, c, d, t, x, y, z +from sympy.core.kind import NumberKind, UndefinedKind +from sympy.matrices.determinant import _find_reasonable_pivot_naive +from sympy.matrices.exceptions import ( + MatrixError, NonSquareMatrixError, ShapeError) +from sympy.matrices.kind import MatrixKind +from sympy.matrices.utilities import _dotprodsimp_state, _simplify, dotprodsimp +from sympy.tensor.array.array_derivatives import ArrayDerivative +from sympy.testing.pytest import ( + ignore_warnings, raises, skip, skip_under_pyodide, slow, + warns_deprecated_sympy) +from sympy.utilities.iterables import capture, iterable +from importlib.metadata import version + +all_classes = (Matrix, SparseMatrix, ImmutableMatrix, ImmutableSparseMatrix) +mutable_classes = (Matrix, SparseMatrix) +immutable_classes = (ImmutableMatrix, ImmutableSparseMatrix) + + +def test__MinimalMatrix(): + x = Matrix(2, 3, [1, 2, 3, 4, 5, 6]) + assert x.rows == 2 + assert x.cols == 3 + assert x[2] == 3 + assert x[1, 1] == 5 + assert list(x) == [1, 2, 3, 4, 5, 6] + assert list(x[1, :]) == [4, 5, 6] + assert list(x[:, 1]) == [2, 5] + assert list(x[:, :]) == list(x) + assert x[:, :] == x + assert Matrix(x) == x + assert Matrix([[1, 2, 3], [4, 5, 6]]) == x + assert Matrix(([1, 2, 3], [4, 5, 6])) == x + assert Matrix([(1, 2, 3), (4, 5, 6)]) == x + assert Matrix(((1, 2, 3), (4, 5, 6))) == x + assert not (Matrix([[1, 2], [3, 4], [5, 6]]) == x) + + +def test_kind(): + assert Matrix([[1, 2], [3, 4]]).kind == MatrixKind(NumberKind) + assert Matrix([[0, 0], [0, 0]]).kind == MatrixKind(NumberKind) + assert Matrix(0, 0, []).kind == MatrixKind(NumberKind) + assert Matrix([[x]]).kind == MatrixKind(NumberKind) + assert Matrix([[1, Matrix([[1]])]]).kind == MatrixKind(UndefinedKind) + assert SparseMatrix([[1]]).kind == MatrixKind(NumberKind) + assert SparseMatrix([[1, Matrix([[1]])]]).kind == MatrixKind(UndefinedKind) + + +def test_todok(): + a, b, c, d = symbols('a:d') + m1 = MutableDenseMatrix([[a, b], [c, d]]) + m2 = ImmutableDenseMatrix([[a, b], [c, d]]) + m3 = MutableSparseMatrix([[a, b], [c, d]]) + m4 = ImmutableSparseMatrix([[a, b], [c, d]]) + assert m1.todok() == m2.todok() == m3.todok() == m4.todok() == \ + {(0, 0): a, (0, 1): b, (1, 0): c, (1, 1): d} + + +def test_tolist(): + lst = [[S.One, S.Half, x*y, S.Zero], [x, y, z, x**2], [y, -S.One, z*x, 3]] + flat_lst = [S.One, S.Half, x*y, S.Zero, x, y, z, x**2, y, -S.One, z*x, 3] + m = Matrix(3, 4, flat_lst) + assert m.tolist() == lst + + +def test_todod(): + m = Matrix([[S.One, 0], [0, S.Half], [x, 0]]) + dict = {0: {0: S.One}, 1: {1: S.Half}, 2: {0: x}} + assert m.todod() == dict + + +def test_row_col_del(): + e = ImmutableMatrix(3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 9]) + raises(IndexError, lambda: e.row_del(5)) + raises(IndexError, lambda: e.row_del(-5)) + raises(IndexError, lambda: e.col_del(5)) + raises(IndexError, lambda: e.col_del(-5)) + + assert e.row_del(2) == e.row_del(-1) == Matrix([[1, 2, 3], [4, 5, 6]]) + assert e.col_del(2) == e.col_del(-1) == Matrix([[1, 2], [4, 5], [7, 8]]) + + assert e.row_del(1) == e.row_del(-2) == Matrix([[1, 2, 3], [7, 8, 9]]) + assert e.col_del(1) == e.col_del(-2) == Matrix([[1, 3], [4, 6], [7, 9]]) + + +def test_get_diag_blocks1(): + a = Matrix([[1, 2], [2, 3]]) + b = Matrix([[3, x], [y, 3]]) + c = Matrix([[3, x, 3], [y, 3, z], [x, y, z]]) + assert a.get_diag_blocks() == [a] + assert b.get_diag_blocks() == [b] + assert c.get_diag_blocks() == [c] + + +def test_get_diag_blocks2(): + a = Matrix([[1, 2], [2, 3]]) + b = Matrix([[3, x], [y, 3]]) + c = Matrix([[3, x, 3], [y, 3, z], [x, y, z]]) + A, B, C, D = diag(a, b, b), diag(a, b, c), diag(a, c, b), diag(c, c, b) + A = Matrix(A.rows, A.cols, A) + B = Matrix(B.rows, B.cols, B) + C = Matrix(C.rows, C.cols, C) + D = Matrix(D.rows, D.cols, D) + + assert A.get_diag_blocks() == [a, b, b] + assert B.get_diag_blocks() == [a, b, c] + assert C.get_diag_blocks() == [a, c, b] + assert D.get_diag_blocks() == [c, c, b] + + +def test_row_col(): + m = Matrix(3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 9]) + assert m.row(0) == Matrix(1, 3, [1, 2, 3]) + assert m.col(0) == Matrix(3, 1, [1, 4, 7]) + + +def test_row_join(): + assert eye(3).row_join(Matrix([7, 7, 7])) == \ + Matrix([[1, 0, 0, 7], + [0, 1, 0, 7], + [0, 0, 1, 7]]) + + +def test_col_join(): + assert eye(3).col_join(Matrix([[7, 7, 7]])) == \ + Matrix([[1, 0, 0], + [0, 1, 0], + [0, 0, 1], + [7, 7, 7]]) + + +def test_row_insert(): + r4 = Matrix([[4, 4, 4]]) + for i in range(-4, 5): + l = [1, 0, 0] + l.insert(i, 4) + assert eye(3).row_insert(i, r4).col(0).flat() == l + + +def test_col_insert(): + c4 = Matrix([4, 4, 4]) + for i in range(-4, 5): + l = [0, 0, 0] + l.insert(i, 4) + assert zeros(3).col_insert(i, c4).row(0).flat() == l + # issue 13643 + assert eye(6).col_insert(3, Matrix([[2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2]])) == \ + Matrix([[1, 0, 0, 2, 2, 0, 0, 0], + [0, 1, 0, 2, 2, 0, 0, 0], + [0, 0, 1, 2, 2, 0, 0, 0], + [0, 0, 0, 2, 2, 1, 0, 0], + [0, 0, 0, 2, 2, 0, 1, 0], + [0, 0, 0, 2, 2, 0, 0, 1]]) + + +def test_extract(): + m = Matrix(4, 3, lambda i, j: i*3 + j) + assert m.extract([0, 1, 3], [0, 1]) == Matrix(3, 2, [0, 1, 3, 4, 9, 10]) + assert m.extract([0, 3], [0, 0, 2]) == Matrix(2, 3, [0, 0, 2, 9, 9, 11]) + assert m.extract(range(4), range(3)) == m + raises(IndexError, lambda: m.extract([4], [0])) + raises(IndexError, lambda: m.extract([0], [3])) + + +def test_hstack(): + m = Matrix(4, 3, lambda i, j: i*3 + j) + m2 = Matrix(3, 4, lambda i, j: i*3 + j) + assert m == m.hstack(m) + assert m.hstack(m, m, m) == Matrix.hstack(m, m, m) == Matrix([ + [0, 1, 2, 0, 1, 2, 0, 1, 2], + [3, 4, 5, 3, 4, 5, 3, 4, 5], + [6, 7, 8, 6, 7, 8, 6, 7, 8], + [9, 10, 11, 9, 10, 11, 9, 10, 11]]) + raises(ShapeError, lambda: m.hstack(m, m2)) + assert Matrix.hstack() == Matrix() + + # test regression #12938 + M1 = Matrix.zeros(0, 0) + M2 = Matrix.zeros(0, 1) + M3 = Matrix.zeros(0, 2) + M4 = Matrix.zeros(0, 3) + m = Matrix.hstack(M1, M2, M3, M4) + assert m.rows == 0 and m.cols == 6 + + +def test_vstack(): + m = Matrix(4, 3, lambda i, j: i*3 + j) + m2 = Matrix(3, 4, lambda i, j: i*3 + j) + assert m == m.vstack(m) + assert m.vstack(m, m, m) == Matrix.vstack(m, m, m) == Matrix([ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [9, 10, 11], + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [9, 10, 11], + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [9, 10, 11]]) + raises(ShapeError, lambda: m.vstack(m, m2)) + assert Matrix.vstack() == Matrix() + + +def test_has(): + A = Matrix(((x, y), (2, 3))) + assert A.has(x) + assert not A.has(z) + assert A.has(Symbol) + + A = Matrix(((2, y), (2, 3))) + assert not A.has(x) + + +def test_is_anti_symmetric(): + x = symbols('x') + assert Matrix(2, 1, [1, 2]).is_anti_symmetric() is False + m = Matrix(3, 3, [0, x**2 + 2*x + 1, y, -(x + 1)**2, 0, x*y, -y, -x*y, 0]) + assert m.is_anti_symmetric() is True + assert m.is_anti_symmetric(simplify=False) is None + assert m.is_anti_symmetric(simplify=lambda x: x) is None + + m = Matrix(3, 3, [x.expand() for x in m]) + assert m.is_anti_symmetric(simplify=False) is True + m = Matrix(3, 3, [x.expand() for x in [S.One] + list(m)[1:]]) + assert m.is_anti_symmetric() is False + + +def test_is_hermitian(): + a = Matrix([[1, I], [-I, 1]]) + assert a.is_hermitian + a = Matrix([[2*I, I], [-I, 1]]) + assert a.is_hermitian is False + a = Matrix([[x, I], [-I, 1]]) + assert a.is_hermitian is None + a = Matrix([[x, 1], [-I, 1]]) + assert a.is_hermitian is False + + +def test_is_symbolic(): + a = Matrix([[x, x], [x, x]]) + assert a.is_symbolic() is True + a = Matrix([[1, 2, 3, 4], [5, 6, 7, 8]]) + assert a.is_symbolic() is False + a = Matrix([[1, 2, 3, 4], [5, 6, x, 8]]) + assert a.is_symbolic() is True + a = Matrix([[1, x, 3]]) + assert a.is_symbolic() is True + a = Matrix([[1, 2, 3]]) + assert a.is_symbolic() is False + a = Matrix([[1], [x], [3]]) + assert a.is_symbolic() is True + a = Matrix([[1], [2], [3]]) + assert a.is_symbolic() is False + + +def test_is_square(): + m = Matrix([[1], [1]]) + m2 = Matrix([[2, 2], [2, 2]]) + assert not m.is_square + assert m2.is_square + + +def test_is_symmetric(): + m = Matrix(2, 2, [0, 1, 1, 0]) + assert m.is_symmetric() + m = Matrix(2, 2, [0, 1, 0, 1]) + assert not m.is_symmetric() + + +def test_is_hessenberg(): + A = Matrix([[3, 4, 1], [2, 4, 5], [0, 1, 2]]) + assert A.is_upper_hessenberg + A = Matrix(3, 3, [3, 2, 0, 4, 4, 1, 1, 5, 2]) + assert A.is_lower_hessenberg + A = Matrix(3, 3, [3, 2, -1, 4, 4, 1, 1, 5, 2]) + assert A.is_lower_hessenberg is False + assert A.is_upper_hessenberg is False + + A = Matrix([[3, 4, 1], [2, 4, 5], [3, 1, 2]]) + assert not A.is_upper_hessenberg + + +def test_values(): + assert set(Matrix(2, 2, [0, 1, 2, 3] + ).values()) == {1, 2, 3} + x = Symbol('x', real=True) + assert set(Matrix(2, 2, [x, 0, 0, 1] + ).values()) == {x, 1} + + +def test_conjugate(): + M = Matrix([[0, I, 5], + [1, 2, 0]]) + + assert M.T == Matrix([[0, 1], + [I, 2], + [5, 0]]) + + assert M.C == Matrix([[0, -I, 5], + [1, 2, 0]]) + assert M.C == M.conjugate() + + assert M.H == M.T.C + assert M.H == Matrix([[ 0, 1], + [-I, 2], + [ 5, 0]]) + + +def test_doit(): + a = Matrix([[Add(x, x, evaluate=False)]]) + assert a[0] != 2*x + assert a.doit() == Matrix([[2*x]]) + + +def test_evalf(): + a = Matrix(2, 1, [sqrt(5), 6]) + assert all(a.evalf()[i] == a[i].evalf() for i in range(2)) + assert all(a.evalf(2)[i] == a[i].evalf(2) for i in range(2)) + assert all(a.n(2)[i] == a[i].n(2) for i in range(2)) + + +def test_replace(): + F, G = symbols('F, G', cls=Function) + K = Matrix(2, 2, lambda i, j: G(i+j)) + M = Matrix(2, 2, lambda i, j: F(i+j)) + N = M.replace(F, G) + assert N == K + + +def test_replace_map(): + F, G = symbols('F, G', cls=Function) + M = Matrix(2, 2, lambda i, j: F(i+j)) + N, d = M.replace(F, G, True) + assert N == Matrix(2, 2, lambda i, j: G(i+j)) + assert d == {F(0): G(0), F(1): G(1), F(2): G(2)} + +def test_numpy_conversion(): + try: + from numpy import array, array_equal + except ImportError: + skip('NumPy must be available to test creating matrices from ndarrays') + A = Matrix([[1,2], [3,4]]) + np_array = array([[1,2], [3,4]]) + assert array_equal(array(A), np_array) + assert array_equal(array(A, copy=True), np_array) + if(int(version('numpy').split('.')[0]) >= 2): #run this test only if numpy is new enough that copy variable is passed properly. + raises(TypeError, lambda: array(A, copy=False)) + +def test_rot90(): + A = Matrix([[1, 2], [3, 4]]) + assert A == A.rot90(0) == A.rot90(4) + assert A.rot90(2) == A.rot90(-2) == A.rot90(6) == Matrix(((4, 3), (2, 1))) + assert A.rot90(3) == A.rot90(-1) == A.rot90(7) == Matrix(((2, 4), (1, 3))) + assert A.rot90() == A.rot90(-7) == A.rot90(-3) == Matrix(((3, 1), (4, 2))) + + +def test_subs(): + assert Matrix([[1, x], [x, 4]]).subs(x, 5) == Matrix([[1, 5], [5, 4]]) + assert Matrix([[x, 2], [x + y, 4]]).subs([[x, -1], [y, -2]]) == \ + Matrix([[-1, 2], [-3, 4]]) + assert Matrix([[x, 2], [x + y, 4]]).subs([(x, -1), (y, -2)]) == \ + Matrix([[-1, 2], [-3, 4]]) + assert Matrix([[x, 2], [x + y, 4]]).subs({x: -1, y: -2}) == \ + Matrix([[-1, 2], [-3, 4]]) + assert Matrix([[x*y]]).subs({x: y - 1, y: x - 1}, simultaneous=True) == \ + Matrix([[(x - 1)*(y - 1)]]) + + +def test_permute(): + a = Matrix(3, 4, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) + + raises(IndexError, lambda: a.permute([[0, 5]])) + raises(ValueError, lambda: a.permute(Symbol('x'))) + b = a.permute_rows([[0, 2], [0, 1]]) + assert a.permute([[0, 2], [0, 1]]) == b == Matrix([ + [5, 6, 7, 8], + [9, 10, 11, 12], + [1, 2, 3, 4]]) + + b = a.permute_cols([[0, 2], [0, 1]]) + assert a.permute([[0, 2], [0, 1]], orientation='cols') == b ==\ + Matrix([ + [ 2, 3, 1, 4], + [ 6, 7, 5, 8], + [10, 11, 9, 12]]) + + b = a.permute_cols([[0, 2], [0, 1]], direction='backward') + assert a.permute([[0, 2], [0, 1]], orientation='cols', direction='backward') == b ==\ + Matrix([ + [ 3, 1, 2, 4], + [ 7, 5, 6, 8], + [11, 9, 10, 12]]) + + assert a.permute([1, 2, 0, 3]) == Matrix([ + [5, 6, 7, 8], + [9, 10, 11, 12], + [1, 2, 3, 4]]) + + from sympy.combinatorics import Permutation + assert a.permute(Permutation([1, 2, 0, 3])) == Matrix([ + [5, 6, 7, 8], + [9, 10, 11, 12], + [1, 2, 3, 4]]) + +def test_upper_triangular(): + + A = Matrix([ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ]) + + R = A.upper_triangular(2) + assert R == Matrix([ + [0, 0, 1, 1], + [0, 0, 0, 1], + [0, 0, 0, 0], + [0, 0, 0, 0] + ]) + + R = A.upper_triangular(-2) + assert R == Matrix([ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [0, 1, 1, 1] + ]) + + R = A.upper_triangular() + assert R == Matrix([ + [1, 1, 1, 1], + [0, 1, 1, 1], + [0, 0, 1, 1], + [0, 0, 0, 1] + ]) + + +def test_lower_triangular(): + A = Matrix([ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ]) + + L = A.lower_triangular() + assert L == Matrix([ + [1, 0, 0, 0], + [1, 1, 0, 0], + [1, 1, 1, 0], + [1, 1, 1, 1]]) + + L = A.lower_triangular(2) + assert L == Matrix([ + [1, 1, 1, 0], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1] + ]) + + L = A.lower_triangular(-2) + assert L == Matrix([ + [0, 0, 0, 0], + [0, 0, 0, 0], + [1, 0, 0, 0], + [1, 1, 0, 0] + ]) + + +def test_add(): + m = Matrix([[1, 2, 3], [x, y, x], [2*y, -50, z*x]]) + assert m + m == Matrix([[2, 4, 6], [2*x, 2*y, 2*x], [4*y, -100, 2*z*x]]) + n = Matrix(1, 2, [1, 2]) + raises(ShapeError, lambda: m + n) + + +def test_matmul(): + a = Matrix([[1, 2], [3, 4]]) + + assert a.__matmul__(2) == NotImplemented + + assert a.__rmatmul__(2) == NotImplemented + + #This is done this way because @ is only supported in Python 3.5+ + #To check 2@a case + try: + eval('2 @ a') + except SyntaxError: + pass + except TypeError: #TypeError is raised in case of NotImplemented is returned + pass + + #Check a@2 case + try: + eval('a @ 2') + except SyntaxError: + pass + except TypeError: #TypeError is raised in case of NotImplemented is returned + pass + + +def test_non_matmul(): + """ + Test that if explicitly specified as non-matrix, mul reverts + to scalar multiplication. + """ + class foo(Expr): + is_Matrix=False + is_MatrixLike=False + shape = (1, 1) + + A = Matrix([[1, 2], [3, 4]]) + b = foo() + assert b*A == Matrix([[b, 2*b], [3*b, 4*b]]) + assert A*b == Matrix([[b, 2*b], [3*b, 4*b]]) + + +def test_neg(): + n = Matrix(1, 2, [1, 2]) + assert -n == Matrix(1, 2, [-1, -2]) + + +def test_sub(): + n = Matrix(1, 2, [1, 2]) + assert n - n == Matrix(1, 2, [0, 0]) + + +def test_div(): + n = Matrix(1, 2, [1, 2]) + assert n/2 == Matrix(1, 2, [S.Half, S(2)/2]) + + +def test_eye(): + assert list(Matrix.eye(2, 2)) == [1, 0, 0, 1] + assert list(Matrix.eye(2)) == [1, 0, 0, 1] + assert type(Matrix.eye(2)) == Matrix + assert type(Matrix.eye(2, cls=Matrix)) == Matrix + + +def test_ones(): + assert list(Matrix.ones(2, 2)) == [1, 1, 1, 1] + assert list(Matrix.ones(2)) == [1, 1, 1, 1] + assert Matrix.ones(2, 3) == Matrix([[1, 1, 1], [1, 1, 1]]) + assert type(Matrix.ones(2)) == Matrix + assert type(Matrix.ones(2, cls=Matrix)) == Matrix + + +def test_zeros(): + assert list(Matrix.zeros(2, 2)) == [0, 0, 0, 0] + assert list(Matrix.zeros(2)) == [0, 0, 0, 0] + assert Matrix.zeros(2, 3) == Matrix([[0, 0, 0], [0, 0, 0]]) + assert type(Matrix.zeros(2)) == Matrix + assert type(Matrix.zeros(2, cls=Matrix)) == Matrix + + +def test_diag_make(): + diag = Matrix.diag + a = Matrix([[1, 2], [2, 3]]) + b = Matrix([[3, x], [y, 3]]) + c = Matrix([[3, x, 3], [y, 3, z], [x, y, z]]) + assert diag(a, b, b) == Matrix([ + [1, 2, 0, 0, 0, 0], + [2, 3, 0, 0, 0, 0], + [0, 0, 3, x, 0, 0], + [0, 0, y, 3, 0, 0], + [0, 0, 0, 0, 3, x], + [0, 0, 0, 0, y, 3], + ]) + assert diag(a, b, c) == Matrix([ + [1, 2, 0, 0, 0, 0, 0], + [2, 3, 0, 0, 0, 0, 0], + [0, 0, 3, x, 0, 0, 0], + [0, 0, y, 3, 0, 0, 0], + [0, 0, 0, 0, 3, x, 3], + [0, 0, 0, 0, y, 3, z], + [0, 0, 0, 0, x, y, z], + ]) + assert diag(a, c, b) == Matrix([ + [1, 2, 0, 0, 0, 0, 0], + [2, 3, 0, 0, 0, 0, 0], + [0, 0, 3, x, 3, 0, 0], + [0, 0, y, 3, z, 0, 0], + [0, 0, x, y, z, 0, 0], + [0, 0, 0, 0, 0, 3, x], + [0, 0, 0, 0, 0, y, 3], + ]) + a = Matrix([x, y, z]) + b = Matrix([[1, 2], [3, 4]]) + c = Matrix([[5, 6]]) + # this "wandering diagonal" is what makes this + # a block diagonal where each block is independent + # of the others + assert diag(a, 7, b, c) == Matrix([ + [x, 0, 0, 0, 0, 0], + [y, 0, 0, 0, 0, 0], + [z, 0, 0, 0, 0, 0], + [0, 7, 0, 0, 0, 0], + [0, 0, 1, 2, 0, 0], + [0, 0, 3, 4, 0, 0], + [0, 0, 0, 0, 5, 6]]) + raises(ValueError, lambda: diag(a, 7, b, c, rows=5)) + assert diag(1) == Matrix([[1]]) + assert diag(1, rows=2) == Matrix([[1, 0], [0, 0]]) + assert diag(1, cols=2) == Matrix([[1, 0], [0, 0]]) + assert diag(1, rows=3, cols=2) == Matrix([[1, 0], [0, 0], [0, 0]]) + assert diag(*[2, 3]) == Matrix([ + [2, 0], + [0, 3]]) + assert diag(Matrix([2, 3])) == Matrix([ + [2], + [3]]) + assert diag([1, [2, 3], 4], unpack=False) == \ + diag([[1], [2, 3], [4]], unpack=False) == Matrix([ + [1, 0], + [2, 3], + [4, 0]]) + assert type(diag(1)) == Matrix + assert type(diag(1, cls=Matrix)) == Matrix + assert Matrix.diag([1, 2, 3]) == Matrix.diag(1, 2, 3) + assert Matrix.diag([1, 2, 3], unpack=False).shape == (3, 1) + assert Matrix.diag([[1, 2, 3]]).shape == (3, 1) + assert Matrix.diag([[1, 2, 3]], unpack=False).shape == (1, 3) + assert Matrix.diag([[[1, 2, 3]]]).shape == (1, 3) + # kerning can be used to move the starting point + assert Matrix.diag(ones(0, 2), 1, 2) == Matrix([ + [0, 0, 1, 0], + [0, 0, 0, 2]]) + assert Matrix.diag(ones(2, 0), 1, 2) == Matrix([ + [0, 0], + [0, 0], + [1, 0], + [0, 2]]) + + +def test_diagonal(): + m = Matrix(3, 3, range(9)) + d = m.diagonal() + assert d == m.diagonal(0) + assert tuple(d) == (0, 4, 8) + assert tuple(m.diagonal(1)) == (1, 5) + assert tuple(m.diagonal(-1)) == (3, 7) + assert tuple(m.diagonal(2)) == (2,) + assert type(m.diagonal()) == type(m) + s = SparseMatrix(3, 3, {(1, 1): 1}) + assert type(s.diagonal()) == type(s) + assert type(m) != type(s) + raises(ValueError, lambda: m.diagonal(3)) + raises(ValueError, lambda: m.diagonal(-3)) + raises(ValueError, lambda: m.diagonal(pi)) + M = ones(2, 3) + assert banded({i: list(M.diagonal(i)) + for i in range(1-M.rows, M.cols)}) == M + + +def test_jordan_block(): + assert Matrix.jordan_block(3, 2) == Matrix.jordan_block(3, eigenvalue=2) \ + == Matrix.jordan_block(size=3, eigenvalue=2) \ + == Matrix.jordan_block(3, 2, band='upper') \ + == Matrix.jordan_block( + size=3, eigenval=2, eigenvalue=2) \ + == Matrix([ + [2, 1, 0], + [0, 2, 1], + [0, 0, 2]]) + + assert Matrix.jordan_block(3, 2, band='lower') == Matrix([ + [2, 0, 0], + [1, 2, 0], + [0, 1, 2]]) + # missing eigenvalue + raises(ValueError, lambda: Matrix.jordan_block(2)) + # non-integral size + raises(ValueError, lambda: Matrix.jordan_block(3.5, 2)) + # size not specified + raises(ValueError, lambda: Matrix.jordan_block(eigenvalue=2)) + # inconsistent eigenvalue + raises(ValueError, + lambda: Matrix.jordan_block( + eigenvalue=2, eigenval=4)) + + # Using alias keyword + assert Matrix.jordan_block(size=3, eigenvalue=2) == \ + Matrix.jordan_block(size=3, eigenval=2) + + +def test_orthogonalize(): + m = Matrix([[1, 2], [3, 4]]) + assert m.orthogonalize(Matrix([[2], [1]])) == [Matrix([[2], [1]])] + assert m.orthogonalize(Matrix([[2], [1]]), normalize=True) == \ + [Matrix([[2*sqrt(5)/5], [sqrt(5)/5]])] + assert m.orthogonalize(Matrix([[1], [2]]), Matrix([[-1], [4]])) == \ + [Matrix([[1], [2]]), Matrix([[Rational(-12, 5)], [Rational(6, 5)]])] + assert m.orthogonalize(Matrix([[0], [0]]), Matrix([[-1], [4]])) == \ + [Matrix([[-1], [4]])] + assert m.orthogonalize(Matrix([[0], [0]])) == [] + + n = Matrix([[9, 1, 9], [3, 6, 10], [8, 5, 2]]) + vecs = [Matrix([[-5], [1]]), Matrix([[-5], [2]]), Matrix([[-5], [-2]])] + assert n.orthogonalize(*vecs) == \ + [Matrix([[-5], [1]]), Matrix([[Rational(5, 26)], [Rational(25, 26)]])] + + vecs = [Matrix([0, 0, 0]), Matrix([1, 2, 3]), Matrix([1, 4, 5])] + raises(ValueError, lambda: Matrix.orthogonalize(*vecs, rankcheck=True)) + + vecs = [Matrix([1, 2, 3]), Matrix([4, 5, 6]), Matrix([7, 8, 9])] + raises(ValueError, lambda: Matrix.orthogonalize(*vecs, rankcheck=True)) + +def test_wilkinson(): + + wminus, wplus = Matrix.wilkinson(1) + assert wminus == Matrix([ + [-1, 1, 0], + [1, 0, 1], + [0, 1, 1]]) + assert wplus == Matrix([ + [1, 1, 0], + [1, 0, 1], + [0, 1, 1]]) + + wminus, wplus = Matrix.wilkinson(3) + assert wminus == Matrix([ + [-3, 1, 0, 0, 0, 0, 0], + [1, -2, 1, 0, 0, 0, 0], + [0, 1, -1, 1, 0, 0, 0], + [0, 0, 1, 0, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 0, 1, 2, 1], + + [0, 0, 0, 0, 0, 1, 3]]) + + assert wplus == Matrix([ + [3, 1, 0, 0, 0, 0, 0], + [1, 2, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 0, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 0, 1, 2, 1], + [0, 0, 0, 0, 0, 1, 3]]) + + +def test_limit(): + x, y = symbols('x y') + m = Matrix(2, 1, [1/x, y]) + assert m.limit(x, 5) == Matrix(2, 1, [Rational(1, 5), y]) + A = Matrix(((1, 4, sin(x)/x), (y, 2, 4), (10, 5, x**2 + 1))) + assert A.limit(x, 0) == Matrix(((1, 4, 1), (y, 2, 4), (10, 5, 1))) + + +def test_issue_13774(): + M = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + v = [1, 1, 1] + raises(TypeError, lambda: M*v) + raises(TypeError, lambda: v*M) + + +def test_companion(): + x = Symbol('x') + y = Symbol('y') + raises(ValueError, lambda: Matrix.companion(1)) + raises(ValueError, lambda: Matrix.companion(Poly([1], x))) + raises(ValueError, lambda: Matrix.companion(Poly([2, 1], x))) + raises(ValueError, lambda: Matrix.companion(Poly(x*y, [x, y]))) + + c0, c1, c2 = symbols('c0:3') + assert Matrix.companion(Poly([1, c0], x)) == Matrix([-c0]) + assert Matrix.companion(Poly([1, c1, c0], x)) == \ + Matrix([[0, -c0], [1, -c1]]) + assert Matrix.companion(Poly([1, c2, c1, c0], x)) == \ + Matrix([[0, 0, -c0], [1, 0, -c1], [0, 1, -c2]]) + + +def test_issue_10589(): + x, y, z = symbols("x, y z") + M1 = Matrix([x, y, z]) + M1 = M1.subs(zip([x, y, z], [1, 2, 3])) + assert M1 == Matrix([[1], [2], [3]]) + + M2 = Matrix([[x, x, x, x, x], [x, x, x, x, x], [x, x, x, x, x]]) + M2 = M2.subs(zip([x], [1])) + assert M2 == Matrix([[1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1]]) + + +def test_rmul_pr19860(): + class Foo(ImmutableDenseMatrix): + _op_priority = MutableDenseMatrix._op_priority + 0.01 + + a = Matrix(2, 2, [1, 2, 3, 4]) + b = Foo(2, 2, [1, 2, 3, 4]) + + # This would throw a RecursionError: maximum recursion depth + # since b always has higher priority even after a.as_mutable() + c = a*b + + assert isinstance(c, Foo) + assert c == Matrix([[7, 10], [15, 22]]) + + +def test_issue_18956(): + A = Array([[1, 2], [3, 4]]) + B = Matrix([[1,2],[3,4]]) + raises(TypeError, lambda: B + A) + raises(TypeError, lambda: A + B) + + +def test__eq__(): + class My(object): + def __iter__(self): + yield 1 + yield 2 + return + def __getitem__(self, i): + return list(self)[i] + a = Matrix(2, 1, [1, 2]) + assert a != My() + class My_sympy(My): + def _sympy_(self): + return Matrix(self) + assert a == My_sympy() + + +def test_args(): + for n, cls in enumerate(all_classes): + m = cls.zeros(3, 2) + # all should give back the same type of arguments, e.g. ints for shape + assert m.shape == (3, 2) and all(type(i) is int for i in m.shape) + assert m.rows == 3 and type(m.rows) is int + assert m.cols == 2 and type(m.cols) is int + if not n % 2: + assert type(m.flat()) in (list, tuple, Tuple) + else: + assert type(m.todok()) is dict + + +def test_deprecated_mat_smat(): + for cls in Matrix, ImmutableMatrix: + m = cls.zeros(3, 2) + with warns_deprecated_sympy(): + mat = m._mat + assert mat == m.flat() + for cls in SparseMatrix, ImmutableSparseMatrix: + m = cls.zeros(3, 2) + with warns_deprecated_sympy(): + smat = m._smat + assert smat == m.todok() + + +def test_division(): + v = Matrix(1, 2, [x, y]) + assert v/z == Matrix(1, 2, [x/z, y/z]) + + +def test_sum(): + m = Matrix([[1, 2, 3], [x, y, x], [2*y, -50, z*x]]) + assert m + m == Matrix([[2, 4, 6], [2*x, 2*y, 2*x], [4*y, -100, 2*z*x]]) + n = Matrix(1, 2, [1, 2]) + raises(ShapeError, lambda: m + n) + + +def test_abs(): + m = Matrix([[1, -2], [x, y]]) + assert abs(m) == Matrix([[1, 2], [Abs(x), Abs(y)]]) + m = Matrix(1, 2, [-3, x]) + n = Matrix(1, 2, [3, Abs(x)]) + assert abs(m) == n + + +def test_addition(): + a = Matrix(( + (1, 2), + (3, 1), + )) + + b = Matrix(( + (1, 2), + (3, 0), + )) + + assert a + b == a.add(b) == Matrix([[2, 4], [6, 1]]) + + +def test_fancy_index_matrix(): + for M in (Matrix, SparseMatrix): + a = M(3, 3, range(9)) + assert a == a[:, :] + assert a[1, :] == Matrix(1, 3, [3, 4, 5]) + assert a[:, 1] == Matrix([1, 4, 7]) + assert a[[0, 1], :] == Matrix([[0, 1, 2], [3, 4, 5]]) + assert a[[0, 1], 2] == a[[0, 1], [2]] + assert a[2, [0, 1]] == a[[2], [0, 1]] + assert a[:, [0, 1]] == Matrix([[0, 1], [3, 4], [6, 7]]) + assert a[0, 0] == 0 + assert a[0:2, :] == Matrix([[0, 1, 2], [3, 4, 5]]) + assert a[:, 0:2] == Matrix([[0, 1], [3, 4], [6, 7]]) + assert a[::2, 1] == a[[0, 2], 1] + assert a[1, ::2] == a[1, [0, 2]] + a = M(3, 3, range(9)) + assert a[[0, 2, 1, 2, 1], :] == Matrix([ + [0, 1, 2], + [6, 7, 8], + [3, 4, 5], + [6, 7, 8], + [3, 4, 5]]) + assert a[:, [0,2,1,2,1]] == Matrix([ + [0, 2, 1, 2, 1], + [3, 5, 4, 5, 4], + [6, 8, 7, 8, 7]]) + + a = SparseMatrix.zeros(3) + a[1, 2] = 2 + a[0, 1] = 3 + a[2, 0] = 4 + assert a.extract([1, 1], [2]) == Matrix([ + [2], + [2]]) + assert a.extract([1, 0], [2, 2, 2]) == Matrix([ + [2, 2, 2], + [0, 0, 0]]) + assert a.extract([1, 0, 1, 2], [2, 0, 1, 0]) == Matrix([ + [2, 0, 0, 0], + [0, 0, 3, 0], + [2, 0, 0, 0], + [0, 4, 0, 4]]) + + +def test_multiplication(): + a = Matrix(( + (1, 2), + (3, 1), + (0, 6), + )) + + b = Matrix(( + (1, 2), + (3, 0), + )) + + raises(ShapeError, lambda: b*a) + raises(TypeError, lambda: a*{}) + + c = a*b + assert c[0, 0] == 7 + assert c[0, 1] == 2 + assert c[1, 0] == 6 + assert c[1, 1] == 6 + assert c[2, 0] == 18 + assert c[2, 1] == 0 + + c = a @ b + assert c[0, 0] == 7 + assert c[0, 1] == 2 + assert c[1, 0] == 6 + assert c[1, 1] == 6 + assert c[2, 0] == 18 + assert c[2, 1] == 0 + + h = matrix_multiply_elementwise(a, c) + assert h == a.multiply_elementwise(c) + assert h[0, 0] == 7 + assert h[0, 1] == 4 + assert h[1, 0] == 18 + assert h[1, 1] == 6 + assert h[2, 0] == 0 + assert h[2, 1] == 0 + raises(ShapeError, lambda: matrix_multiply_elementwise(a, b)) + + c = b * Symbol("x") + assert isinstance(c, Matrix) + assert c[0, 0] == x + assert c[0, 1] == 2*x + assert c[1, 0] == 3*x + assert c[1, 1] == 0 + + c2 = x * b + assert c == c2 + + c = 5 * b + assert isinstance(c, Matrix) + assert c[0, 0] == 5 + assert c[0, 1] == 2*5 + assert c[1, 0] == 3*5 + assert c[1, 1] == 0 + + M = Matrix([[oo, 0], [0, oo]]) + assert M ** 2 == M + + M = Matrix([[oo, oo], [0, 0]]) + assert M ** 2 == Matrix([[nan, nan], [nan, nan]]) + + # https://github.com/sympy/sympy/issues/22353 + A = Matrix(ones(3, 1)) + _h = -Rational(1, 2) + B = Matrix([_h, _h, _h]) + assert A.multiply_elementwise(B) == Matrix([ + [_h], + [_h], + [_h]]) + + +def test_power(): + raises(NonSquareMatrixError, lambda: Matrix((1, 2))**2) + + A = Matrix([[2, 3], [4, 5]]) + assert A**5 == Matrix([[6140, 8097], [10796, 14237]]) + A = Matrix([[2, 1, 3], [4, 2, 4], [6, 12, 1]]) + assert A**3 == Matrix([[290, 262, 251], [448, 440, 368], [702, 954, 433]]) + assert A**0 == eye(3) + assert A**1 == A + assert (Matrix([[2]]) ** 100)[0, 0] == 2**100 + assert Matrix([[1, 2], [3, 4]])**Integer(2) == Matrix([[7, 10], [15, 22]]) + A = Matrix([[1,2],[4,5]]) + assert A.pow(20, method='cayley') == A.pow(20, method='multiply') + assert A**Integer(2) == Matrix([[9, 12], [24, 33]]) + assert eye(2)**10000000 == eye(2) + + A = Matrix([[33, 24], [48, 57]]) + assert (A**S.Half)[:] == [5, 2, 4, 7] + A = Matrix([[0, 4], [-1, 5]]) + assert (A**S.Half)**2 == A + + assert Matrix([[1, 0], [1, 1]])**S.Half == Matrix([[1, 0], [S.Half, 1]]) + assert Matrix([[1, 0], [1, 1]])**0.5 == Matrix([[1, 0], [0.5, 1]]) + from sympy.abc import n + assert Matrix([[1, a], [0, 1]])**n == Matrix([[1, a*n], [0, 1]]) + assert Matrix([[b, a], [0, b]])**n == Matrix([[b**n, a*b**(n-1)*n], [0, b**n]]) + assert Matrix([ + [a**n, a**(n - 1)*n, (a**n*n**2 - a**n*n)/(2*a**2)], + [ 0, a**n, a**(n - 1)*n], + [ 0, 0, a**n]]) + assert Matrix([[a, 1, 0], [0, a, 0], [0, 0, b]])**n == Matrix([ + [a**n, a**(n-1)*n, 0], + [0, a**n, 0], + [0, 0, b**n]]) + + A = Matrix([[1, 0], [1, 7]]) + assert A._matrix_pow_by_jordan_blocks(S(3)) == A._eval_pow_by_recursion(3) + A = Matrix([[2]]) + assert A**10 == Matrix([[2**10]]) == A._matrix_pow_by_jordan_blocks(S(10)) == \ + A._eval_pow_by_recursion(10) + + # testing a matrix that cannot be jordan blocked issue 11766 + m = Matrix([[3, 0, 0, 0, -3], [0, -3, -3, 0, 3], [0, 3, 0, 3, 0], [0, 0, 3, 0, 3], [3, 0, 0, 3, 0]]) + raises(MatrixError, lambda: m._matrix_pow_by_jordan_blocks(S(10))) + + # test issue 11964 + raises(MatrixError, lambda: Matrix([[1, 1], [3, 3]])._matrix_pow_by_jordan_blocks(S(-10))) + A = Matrix([[0, 1, 0], [0, 0, 1], [0, 0, 0]]) # Nilpotent jordan block size 3 + assert A**10.0 == Matrix([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) + raises(ValueError, lambda: A**2.1) + raises(ValueError, lambda: A**Rational(3, 2)) + A = Matrix([[8, 1], [3, 2]]) + assert A**10.0 == Matrix([[1760744107, 272388050], [817164150, 126415807]]) + A = Matrix([[0, 0, 1], [0, 0, 1], [0, 0, 1]]) # Nilpotent jordan block size 1 + assert A**10.0 == Matrix([[0, 0, 1], [0, 0, 1], [0, 0, 1]]) + A = Matrix([[0, 1, 0], [0, 0, 1], [0, 0, 1]]) # Nilpotent jordan block size 2 + assert A**10.0 == Matrix([[0, 0, 1], [0, 0, 1], [0, 0, 1]]) + n = Symbol('n', integer=True) + assert isinstance(A**n, MatPow) + n = Symbol('n', integer=True, negative=True) + raises(ValueError, lambda: A**n) + n = Symbol('n', integer=True, nonnegative=True) + assert A**n == Matrix([ + [KroneckerDelta(0, n), KroneckerDelta(1, n), -KroneckerDelta(0, n) - KroneckerDelta(1, n) + 1], + [ 0, KroneckerDelta(0, n), 1 - KroneckerDelta(0, n)], + [ 0, 0, 1]]) + assert A**(n + 2) == Matrix([[0, 0, 1], [0, 0, 1], [0, 0, 1]]) + raises(ValueError, lambda: A**Rational(3, 2)) + A = Matrix([[0, 0, 1], [3, 0, 1], [4, 3, 1]]) + assert A**5.0 == Matrix([[168, 72, 89], [291, 144, 161], [572, 267, 329]]) + assert A**5.0 == A**5 + A = Matrix([[0, 1, 0],[-1, 0, 0],[0, 0, 0]]) + n = Symbol("n") + An = A**n + assert An.subs(n, 2).doit() == A**2 + raises(ValueError, lambda: An.subs(n, -2).doit()) + assert An * An == A**(2*n) + + # concretizing behavior for non-integer and complex powers + A = Matrix([[0,0,0],[0,0,0],[0,0,0]]) + n = Symbol('n', integer=True, positive=True) + assert A**n == A + n = Symbol('n', integer=True, nonnegative=True) + assert A**n == diag(0**n, 0**n, 0**n) + assert (A**n).subs(n, 0) == eye(3) + assert (A**n).subs(n, 1) == zeros(3) + A = Matrix ([[2,0,0],[0,2,0],[0,0,2]]) + assert A**2.1 == diag (2**2.1, 2**2.1, 2**2.1) + assert A**I == diag (2**I, 2**I, 2**I) + A = Matrix([[0, 1, 0], [0, 0, 1], [0, 0, 1]]) + raises(ValueError, lambda: A**2.1) + raises(ValueError, lambda: A**I) + A = Matrix([[S.Half, S.Half], [S.Half, S.Half]]) + assert A**S.Half == A + A = Matrix([[1, 1],[3, 3]]) + assert A**S.Half == Matrix ([[S.Half, S.Half], [3*S.Half, 3*S.Half]]) + + +def test_issue_17247_expression_blowup_1(): + M = Matrix([[1+x, 1-x], [1-x, 1+x]]) + with dotprodsimp(True): + assert M.exp().expand() == Matrix([ + [ (exp(2*x) + exp(2))/2, (-exp(2*x) + exp(2))/2], + [(-exp(2*x) + exp(2))/2, (exp(2*x) + exp(2))/2]]) + + +def test_issue_17247_expression_blowup_2(): + M = Matrix([[1+x, 1-x], [1-x, 1+x]]) + with dotprodsimp(True): + P, J = M.jordan_form () + assert P*J*P.inv() + + +def test_issue_17247_expression_blowup_3(): + M = Matrix([[1+x, 1-x], [1-x, 1+x]]) + with dotprodsimp(True): + assert M**100 == Matrix([ + [633825300114114700748351602688*x**100 + 633825300114114700748351602688, 633825300114114700748351602688 - 633825300114114700748351602688*x**100], + [633825300114114700748351602688 - 633825300114114700748351602688*x**100, 633825300114114700748351602688*x**100 + 633825300114114700748351602688]]) + + +def test_issue_17247_expression_blowup_4(): +# This matrix takes extremely long on current master even with intermediate simplification so an abbreviated version is used. It is left here for test in case of future optimizations. +# M = Matrix(S('''[ +# [ -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64, 1/4 - 5*I/16, 65/128 + 87*I/64, -9/32 - I/16, 183/256 - 97*I/128, 3/64 + 13*I/64, -23/32 - 59*I/256, 15/128 - 3*I/32, 19/256 + 551*I/1024], +# [-149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128, 85/256 - 33*I/16, 805/128 + 2415*I/512, -219/128 + 115*I/256, 6301/4096 - 6609*I/1024, 119/128 + 143*I/128, -10879/2048 + 4343*I/4096, 129/256 - 549*I/512, 42533/16384 + 29103*I/8192], +# [ 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64, 1/4 - 5*I/16, 65/128 + 87*I/64, -9/32 - I/16, 183/256 - 97*I/128, 3/64 + 13*I/64, -23/32 - 59*I/256], +# [ -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128, 85/256 - 33*I/16, 805/128 + 2415*I/512, -219/128 + 115*I/256, 6301/4096 - 6609*I/1024, 119/128 + 143*I/128, -10879/2048 + 4343*I/4096], +# [ 1 + I, -19/4 + 5*I/4, 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64, 1/4 - 5*I/16, 65/128 + 87*I/64, -9/32 - I/16, 183/256 - 97*I/128], +# [ 21/8 + I, -537/64 + 143*I/16, -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128, 85/256 - 33*I/16, 805/128 + 2415*I/512, -219/128 + 115*I/256, 6301/4096 - 6609*I/1024], +# [ -2, 17/4 - 13*I/2, 1 + I, -19/4 + 5*I/4, 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64, 1/4 - 5*I/16, 65/128 + 87*I/64], +# [ 1/4 + 13*I/4, -825/64 - 147*I/32, 21/8 + I, -537/64 + 143*I/16, -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128, 85/256 - 33*I/16, 805/128 + 2415*I/512], +# [ -4*I, 27/2 + 6*I, -2, 17/4 - 13*I/2, 1 + I, -19/4 + 5*I/4, 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64], +# [ 1/4 + 5*I/2, -23/8 - 57*I/16, 1/4 + 13*I/4, -825/64 - 147*I/32, 21/8 + I, -537/64 + 143*I/16, -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128], +# [ -4, 9 - 5*I, -4*I, 27/2 + 6*I, -2, 17/4 - 13*I/2, 1 + I, -19/4 + 5*I/4, 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16], +# [ -2*I, 119/8 + 29*I/4, 1/4 + 5*I/2, -23/8 - 57*I/16, 1/4 + 13*I/4, -825/64 - 147*I/32, 21/8 + I, -537/64 + 143*I/16, -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128]]''')) +# assert M**10 == Matrix([ +# [ 7*(-221393644768594642173548179825793834595 - 1861633166167425978847110897013541127952*I)/9671406556917033397649408, 15*(31670992489131684885307005100073928751695 + 10329090958303458811115024718207404523808*I)/77371252455336267181195264, 7*(-3710978679372178839237291049477017392703 + 1377706064483132637295566581525806894169*I)/19342813113834066795298816, (9727707023582419994616144751727760051598 - 59261571067013123836477348473611225724433*I)/9671406556917033397649408, (31896723509506857062605551443641668183707 + 54643444538699269118869436271152084599580*I)/38685626227668133590597632, (-2024044860947539028275487595741003997397402 + 130959428791783397562960461903698670485863*I)/309485009821345068724781056, 3*(26190251453797590396533756519358368860907 - 27221191754180839338002754608545400941638*I)/77371252455336267181195264, (1154643595139959842768960128434994698330461 + 3385496216250226964322872072260446072295634*I)/618970019642690137449562112, 3*(-31849347263064464698310044805285774295286 - 11877437776464148281991240541742691164309*I)/77371252455336267181195264, (4661330392283532534549306589669150228040221 - 4171259766019818631067810706563064103956871*I)/1237940039285380274899124224, (9598353794289061833850770474812760144506 + 358027153990999990968244906482319780943983*I)/309485009821345068724781056, (-9755135335127734571547571921702373498554177 - 4837981372692695195747379349593041939686540*I)/2475880078570760549798248448], +# [(-379516731607474268954110071392894274962069 - 422272153179747548473724096872271700878296*I)/77371252455336267181195264, (41324748029613152354787280677832014263339501 - 12715121258662668420833935373453570749288074*I)/1237940039285380274899124224, (-339216903907423793947110742819264306542397 + 494174755147303922029979279454787373566517*I)/77371252455336267181195264, (-18121350839962855576667529908850640619878381 - 37413012454129786092962531597292531089199003*I)/1237940039285380274899124224, (2489661087330511608618880408199633556675926 + 1137821536550153872137379935240732287260863*I)/309485009821345068724781056, (-136644109701594123227587016790354220062972119 + 110130123468183660555391413889600443583585272*I)/4951760157141521099596496896, (1488043981274920070468141664150073426459593 - 9691968079933445130866371609614474474327650*I)/1237940039285380274899124224, 27*(4636797403026872518131756991410164760195942 + 3369103221138229204457272860484005850416533*I)/4951760157141521099596496896, (-8534279107365915284081669381642269800472363 + 2241118846262661434336333368511372725482742*I)/1237940039285380274899124224, (60923350128174260992536531692058086830950875 - 263673488093551053385865699805250505661590126*I)/9903520314283042199192993792, (18520943561240714459282253753348921824172569 + 24846649186468656345966986622110971925703604*I)/4951760157141521099596496896, (-232781130692604829085973604213529649638644431 + 35981505277760667933017117949103953338570617*I)/9903520314283042199192993792], +# [ (8742968295129404279528270438201520488950 + 3061473358639249112126847237482570858327*I)/4835703278458516698824704, (-245657313712011778432792959787098074935273 + 253113767861878869678042729088355086740856*I)/38685626227668133590597632, (1947031161734702327107371192008011621193 - 19462330079296259148177542369999791122762*I)/9671406556917033397649408, (552856485625209001527688949522750288619217 + 392928441196156725372494335248099016686580*I)/77371252455336267181195264, (-44542866621905323121630214897126343414629 + 3265340021421335059323962377647649632959*I)/19342813113834066795298816, (136272594005759723105646069956434264218730 - 330975364731707309489523680957584684763587*I)/38685626227668133590597632, (27392593965554149283318732469825168894401 + 75157071243800133880129376047131061115278*I)/38685626227668133590597632, 7*(-357821652913266734749960136017214096276154 - 45509144466378076475315751988405961498243*I)/309485009821345068724781056, (104485001373574280824835174390219397141149 - 99041000529599568255829489765415726168162*I)/77371252455336267181195264, (1198066993119982409323525798509037696321291 + 4249784165667887866939369628840569844519936*I)/618970019642690137449562112, (-114985392587849953209115599084503853611014 - 52510376847189529234864487459476242883449*I)/77371252455336267181195264, (6094620517051332877965959223269600650951573 - 4683469779240530439185019982269137976201163*I)/1237940039285380274899124224], +# [ (611292255597977285752123848828590587708323 - 216821743518546668382662964473055912169502*I)/77371252455336267181195264, (-1144023204575811464652692396337616594307487 + 12295317806312398617498029126807758490062855*I)/309485009821345068724781056, (-374093027769390002505693378578475235158281 - 573533923565898290299607461660384634333639*I)/77371252455336267181195264, (47405570632186659000138546955372796986832987 - 2837476058950808941605000274055970055096534*I)/1237940039285380274899124224, (-571573207393621076306216726219753090535121 + 533381457185823100878764749236639320783831*I)/77371252455336267181195264, (-7096548151856165056213543560958582513797519 - 24035731898756040059329175131592138642195366*I)/618970019642690137449562112, (2396762128833271142000266170154694033849225 + 1448501087375679588770230529017516492953051*I)/309485009821345068724781056, (-150609293845161968447166237242456473262037053 + 92581148080922977153207018003184520294188436*I)/4951760157141521099596496896, 5*(270278244730804315149356082977618054486347 - 1997830155222496880429743815321662710091562*I)/1237940039285380274899124224, (62978424789588828258068912690172109324360330 + 44803641177219298311493356929537007630129097*I)/2475880078570760549798248448, 19*(-451431106327656743945775812536216598712236 + 114924966793632084379437683991151177407937*I)/1237940039285380274899124224, (63417747628891221594106738815256002143915995 - 261508229397507037136324178612212080871150958*I)/9903520314283042199192993792], +# [ (-2144231934021288786200752920446633703357 + 2305614436009705803670842248131563850246*I)/1208925819614629174706176, (-90720949337459896266067589013987007078153 - 221951119475096403601562347412753844534569*I)/19342813113834066795298816, (11590973613116630788176337262688659880376 + 6514520676308992726483494976339330626159*I)/4835703278458516698824704, 3*(-131776217149000326618649542018343107657237 + 79095042939612668486212006406818285287004*I)/38685626227668133590597632, (10100577916793945997239221374025741184951 - 28631383488085522003281589065994018550748*I)/9671406556917033397649408, 67*(10090295594251078955008130473573667572549 + 10449901522697161049513326446427839676762*I)/77371252455336267181195264, (-54270981296988368730689531355811033930513 - 3413683117592637309471893510944045467443*I)/19342813113834066795298816, (440372322928679910536575560069973699181278 - 736603803202303189048085196176918214409081*I)/77371252455336267181195264, (33220374714789391132887731139763250155295 + 92055083048787219934030779066298919603554*I)/38685626227668133590597632, 5*(-594638554579967244348856981610805281527116 - 82309245323128933521987392165716076704057*I)/309485009821345068724781056, (128056368815300084550013708313312073721955 - 114619107488668120303579745393765245911404*I)/77371252455336267181195264, 21*(59839959255173222962789517794121843393573 + 241507883613676387255359616163487405826334*I)/618970019642690137449562112], +# [ (-13454485022325376674626653802541391955147 + 184471402121905621396582628515905949793486*I)/19342813113834066795298816, (-6158730123400322562149780662133074862437105 - 3416173052604643794120262081623703514107476*I)/154742504910672534362390528, (770558003844914708453618983120686116100419 - 127758381209767638635199674005029818518766*I)/77371252455336267181195264, (-4693005771813492267479835161596671660631703 + 12703585094750991389845384539501921531449948*I)/309485009821345068724781056, (-295028157441149027913545676461260860036601 - 841544569970643160358138082317324743450770*I)/77371252455336267181195264, (56716442796929448856312202561538574275502893 + 7216818824772560379753073185990186711454778*I)/1237940039285380274899124224, 15*(-87061038932753366532685677510172566368387 + 61306141156647596310941396434445461895538*I)/154742504910672534362390528, (-3455315109680781412178133042301025723909347 - 24969329563196972466388460746447646686670670*I)/618970019642690137449562112, (2453418854160886481106557323699250865361849 + 1497886802326243014471854112161398141242514*I)/309485009821345068724781056, (-151343224544252091980004429001205664193082173 + 90471883264187337053549090899816228846836628*I)/4951760157141521099596496896, (1652018205533026103358164026239417416432989 - 9959733619236515024261775397109724431400162*I)/1237940039285380274899124224, 3*(40676374242956907656984876692623172736522006 + 31023357083037817469535762230872667581366205*I)/4951760157141521099596496896], +# [ (-1226990509403328460274658603410696548387 - 4131739423109992672186585941938392788458*I)/1208925819614629174706176, (162392818524418973411975140074368079662703 + 23706194236915374831230612374344230400704*I)/9671406556917033397649408, (-3935678233089814180000602553655565621193 + 2283744757287145199688061892165659502483*I)/1208925819614629174706176, (-2400210250844254483454290806930306285131 - 315571356806370996069052930302295432758205*I)/19342813113834066795298816, (13365917938215281056563183751673390817910 + 15911483133819801118348625831132324863881*I)/4835703278458516698824704, 3*(-215950551370668982657516660700301003897855 + 51684341999223632631602864028309400489378*I)/38685626227668133590597632, (20886089946811765149439844691320027184765 - 30806277083146786592790625980769214361844*I)/9671406556917033397649408, (562180634592713285745940856221105667874855 + 1031543963988260765153550559766662245114916*I)/77371252455336267181195264, (-65820625814810177122941758625652476012867 - 12429918324787060890804395323920477537595*I)/19342813113834066795298816, (319147848192012911298771180196635859221089 - 402403304933906769233365689834404519960394*I)/38685626227668133590597632, (23035615120921026080284733394359587955057 + 115351677687031786114651452775242461310624*I)/38685626227668133590597632, (-3426830634881892756966440108592579264936130 - 1022954961164128745603407283836365128598559*I)/309485009821345068724781056], +# [ (-192574788060137531023716449082856117537757 - 69222967328876859586831013062387845780692*I)/19342813113834066795298816, (2736383768828013152914815341491629299773262 - 2773252698016291897599353862072533475408743*I)/77371252455336267181195264, (-23280005281223837717773057436155921656805 + 214784953368021840006305033048142888879224*I)/19342813113834066795298816, (-3035247484028969580570400133318947903462326 - 2195168903335435855621328554626336958674325*I)/77371252455336267181195264, (984552428291526892214541708637840971548653 - 64006622534521425620714598573494988589378*I)/77371252455336267181195264, (-3070650452470333005276715136041262898509903 + 7286424705750810474140953092161794621989080*I)/154742504910672534362390528, (-147848877109756404594659513386972921139270 - 416306113044186424749331418059456047650861*I)/38685626227668133590597632, (55272118474097814260289392337160619494260781 + 7494019668394781211907115583302403519488058*I)/1237940039285380274899124224, (-581537886583682322424771088996959213068864 + 542191617758465339135308203815256798407429*I)/77371252455336267181195264, (-6422548983676355789975736799494791970390991 - 23524183982209004826464749309156698827737702*I)/618970019642690137449562112, 7*(180747195387024536886923192475064903482083 + 84352527693562434817771649853047924991804*I)/154742504910672534362390528, (-135485179036717001055310712747643466592387031 + 102346575226653028836678855697782273460527608*I)/4951760157141521099596496896], +# [ (3384238362616083147067025892852431152105 + 156724444932584900214919898954874618256*I)/604462909807314587353088, (-59558300950677430189587207338385764871866 + 114427143574375271097298201388331237478857*I)/4835703278458516698824704, (-1356835789870635633517710130971800616227 - 7023484098542340388800213478357340875410*I)/1208925819614629174706176, (234884918567993750975181728413524549575881 + 79757294640629983786895695752733890213506*I)/9671406556917033397649408, (-7632732774935120473359202657160313866419 + 2905452608512927560554702228553291839465*I)/1208925819614629174706176, (52291747908702842344842889809762246649489 - 520996778817151392090736149644507525892649*I)/19342813113834066795298816, (17472406829219127839967951180375981717322 + 23464704213841582137898905375041819568669*I)/4835703278458516698824704, (-911026971811893092350229536132730760943307 + 150799318130900944080399439626714846752360*I)/38685626227668133590597632, (26234457233977042811089020440646443590687 - 45650293039576452023692126463683727692890*I)/9671406556917033397649408, 3*(288348388717468992528382586652654351121357 + 454526517721403048270274049572136109264668*I)/77371252455336267181195264, (-91583492367747094223295011999405657956347 - 12704691128268298435362255538069612411331*I)/19342813113834066795298816, (411208730251327843849027957710164064354221 - 569898526380691606955496789378230959965898*I)/38685626227668133590597632], +# [ (27127513117071487872628354831658811211795 - 37765296987901990355760582016892124833857*I)/4835703278458516698824704, (1741779916057680444272938534338833170625435 + 3083041729779495966997526404685535449810378*I)/77371252455336267181195264, 3*(-60642236251815783728374561836962709533401 - 24630301165439580049891518846174101510744*I)/19342813113834066795298816, 3*(445885207364591681637745678755008757483408 - 350948497734812895032502179455610024541643*I)/38685626227668133590597632, (-47373295621391195484367368282471381775684 + 219122969294089357477027867028071400054973*I)/19342813113834066795298816, (-2801565819673198722993348253876353741520438 - 2250142129822658548391697042460298703335701*I)/77371252455336267181195264, (801448252275607253266997552356128790317119 - 50890367688077858227059515894356594900558*I)/77371252455336267181195264, (-5082187758525931944557763799137987573501207 + 11610432359082071866576699236013484487676124*I)/309485009821345068724781056, (-328925127096560623794883760398247685166830 - 643447969697471610060622160899409680422019*I)/77371252455336267181195264, 15*(2954944669454003684028194956846659916299765 + 33434406416888505837444969347824812608566*I)/1237940039285380274899124224, (-415749104352001509942256567958449835766827 + 479330966144175743357171151440020955412219*I)/77371252455336267181195264, 3*(-4639987285852134369449873547637372282914255 - 11994411888966030153196659207284951579243273*I)/1237940039285380274899124224], +# [ (-478846096206269117345024348666145495601 + 1249092488629201351470551186322814883283*I)/302231454903657293676544, (-17749319421930878799354766626365926894989 - 18264580106418628161818752318217357231971*I)/1208925819614629174706176, (2801110795431528876849623279389579072819 + 363258850073786330770713557775566973248*I)/604462909807314587353088, (-59053496693129013745775512127095650616252 + 78143588734197260279248498898321500167517*I)/4835703278458516698824704, (-283186724922498212468162690097101115349 - 6443437753863179883794497936345437398276*I)/1208925819614629174706176, (188799118826748909206887165661384998787543 + 84274736720556630026311383931055307398820*I)/9671406556917033397649408, (-5482217151670072904078758141270295025989 + 1818284338672191024475557065444481298568*I)/1208925819614629174706176, (56564463395350195513805521309731217952281 - 360208541416798112109946262159695452898431*I)/19342813113834066795298816, 11*(1259539805728870739006416869463689438068 + 1409136581547898074455004171305324917387*I)/4835703278458516698824704, 5*(-123701190701414554945251071190688818343325 + 30997157322590424677294553832111902279712*I)/38685626227668133590597632, (16130917381301373033736295883982414239781 - 32752041297570919727145380131926943374516*I)/9671406556917033397649408, (650301385108223834347093740500375498354925 + 899526407681131828596801223402866051809258*I)/77371252455336267181195264], +# [ (9011388245256140876590294262420614839483 + 8167917972423946282513000869327525382672*I)/1208925819614629174706176, (-426393174084720190126376382194036323028924 + 180692224825757525982858693158209545430621*I)/9671406556917033397649408, (24588556702197802674765733448108154175535 - 45091766022876486566421953254051868331066*I)/4835703278458516698824704, (1872113939365285277373877183750416985089691 + 3030392393733212574744122057679633775773130*I)/77371252455336267181195264, (-222173405538046189185754954524429864167549 - 75193157893478637039381059488387511299116*I)/19342813113834066795298816, (2670821320766222522963689317316937579844558 - 2645837121493554383087981511645435472169191*I)/77371252455336267181195264, 5*(-2100110309556476773796963197283876204940 + 41957457246479840487980315496957337371937*I)/19342813113834066795298816, (-5733743755499084165382383818991531258980593 - 3328949988392698205198574824396695027195732*I)/154742504910672534362390528, (707827994365259025461378911159398206329247 - 265730616623227695108042528694302299777294*I)/77371252455336267181195264, (-1442501604682933002895864804409322823788319 + 11504137805563265043376405214378288793343879*I)/309485009821345068724781056, (-56130472299445561499538726459719629522285 - 61117552419727805035810982426639329818864*I)/9671406556917033397649408, (39053692321126079849054272431599539429908717 - 10209127700342570953247177602860848130710666*I)/1237940039285380274899124224]]) + M = Matrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64, 1/4 - 5*I/16, 65/128 + 87*I/64], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128, 85/256 - 33*I/16, 805/128 + 2415*I/512], + [ 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64], + [ -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128], + [ 1 + I, -19/4 + 5*I/4, 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16], + [ 21/8 + I, -537/64 + 143*I/16, -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M**10 == Matrix(S('''[ + [ 7369525394972778926719607798014571861/604462909807314587353088 - 229284202061790301477392339912557559*I/151115727451828646838272, -19704281515163975949388435612632058035/1208925819614629174706176 + 14319858347987648723768698170712102887*I/302231454903657293676544, -3623281909451783042932142262164941211/604462909807314587353088 - 6039240602494288615094338643452320495*I/604462909807314587353088, 109260497799140408739847239685705357695/2417851639229258349412352 - 7427566006564572463236368211555511431*I/2417851639229258349412352, -16095803767674394244695716092817006641/2417851639229258349412352 + 10336681897356760057393429626719177583*I/1208925819614629174706176, -42207883340488041844332828574359769743/2417851639229258349412352 - 182332262671671273188016400290188468499*I/4835703278458516698824704], + [50566491050825573392726324995779608259/1208925819614629174706176 - 90047007594468146222002432884052362145*I/2417851639229258349412352, 74273703462900000967697427843983822011/1208925819614629174706176 + 265947522682943571171988741842776095421*I/1208925819614629174706176, -116900341394390200556829767923360888429/2417851639229258349412352 - 53153263356679268823910621474478756845*I/2417851639229258349412352, 195407378023867871243426523048612490249/1208925819614629174706176 - 1242417915995360200584837585002906728929*I/9671406556917033397649408, -863597594389821970177319682495878193/302231454903657293676544 + 476936100741548328800725360758734300481*I/9671406556917033397649408, -3154451590535653853562472176601754835575/19342813113834066795298816 - 232909875490506237386836489998407329215*I/2417851639229258349412352], + [ -1715444997702484578716037230949868543/302231454903657293676544 + 5009695651321306866158517287924120777*I/302231454903657293676544, -30551582497996879620371947949342101301/604462909807314587353088 - 7632518367986526187139161303331519629*I/151115727451828646838272, 312680739924495153190604170938220575/18889465931478580854784 - 108664334509328818765959789219208459*I/75557863725914323419136, -14693696966703036206178521686918865509/604462909807314587353088 + 72345386220900843930147151999899692401*I/1208925819614629174706176, -8218872496728882299722894680635296519/1208925819614629174706176 - 16776782833358893712645864791807664983*I/1208925819614629174706176, 143237839169380078671242929143670635137/2417851639229258349412352 + 2883817094806115974748882735218469447*I/2417851639229258349412352], + [ 3087979417831061365023111800749855987/151115727451828646838272 + 34441942370802869368851419102423997089*I/604462909807314587353088, -148309181940158040917731426845476175667/604462909807314587353088 - 263987151804109387844966835369350904919*I/9671406556917033397649408, 50259518594816377378747711930008883165/1208925819614629174706176 - 95713974916869240305450001443767979653*I/2417851639229258349412352, 153466447023875527996457943521467271119/2417851639229258349412352 + 517285524891117105834922278517084871349*I/2417851639229258349412352, -29184653615412989036678939366291205575/604462909807314587353088 - 27551322282526322041080173287022121083*I/1208925819614629174706176, 196404220110085511863671393922447671649/1208925819614629174706176 - 1204712019400186021982272049902206202145*I/9671406556917033397649408], + [ -2632581805949645784625606590600098779/151115727451828646838272 - 589957435912868015140272627522612771*I/37778931862957161709568, 26727850893953715274702844733506310247/302231454903657293676544 - 10825791956782128799168209600694020481*I/302231454903657293676544, -1036348763702366164044671908440791295/151115727451828646838272 + 3188624571414467767868303105288107375*I/151115727451828646838272, -36814959939970644875593411585393242449/604462909807314587353088 - 18457555789119782404850043842902832647*I/302231454903657293676544, 12454491297984637815063964572803058647/604462909807314587353088 - 340489532842249733975074349495329171*I/302231454903657293676544, -19547211751145597258386735573258916681/604462909807314587353088 + 87299583775782199663414539883938008933*I/1208925819614629174706176], + [ -40281994229560039213253423262678393183/604462909807314587353088 - 2939986850065527327299273003299736641*I/604462909807314587353088, 331940684638052085845743020267462794181/2417851639229258349412352 - 284574901963624403933361315517248458969*I/1208925819614629174706176, 6453843623051745485064693628073010961/302231454903657293676544 + 36062454107479732681350914931391590957*I/604462909807314587353088, -147665869053634695632880753646441962067/604462909807314587353088 - 305987938660447291246597544085345123927*I/9671406556917033397649408, 107821369195275772166593879711259469423/2417851639229258349412352 - 11645185518211204108659001435013326687*I/302231454903657293676544, 64121228424717666402009446088588091619/1208925819614629174706176 + 265557133337095047883844369272389762133*I/1208925819614629174706176]]''')) + + +def test_issue_17247_expression_blowup_5(): + M = Matrix(6, 6, lambda i, j: 1 + (-1)**(i+j)*I) + with dotprodsimp(True): + assert M.charpoly('x') == PurePoly(x**6 + (-6 - 6*I)*x**5 + 36*I*x**4, x, domain='EX') + + +def test_issue_17247_expression_blowup_6(): + M = Matrix(8, 8, [x+i for i in range (64)]) + with dotprodsimp(True): + assert M.det('bareiss') == 0 + + +def test_issue_17247_expression_blowup_7(): + M = Matrix(6, 6, lambda i, j: 1 + (-1)**(i+j)*I) + with dotprodsimp(True): + assert M.det('berkowitz') == 0 + + +def test_issue_17247_expression_blowup_8(): + M = Matrix(8, 8, [x+i for i in range (64)]) + with dotprodsimp(True): + assert M.det('lu') == 0 + + +def test_issue_17247_expression_blowup_9(): + M = Matrix(8, 8, [x+i for i in range (64)]) + with dotprodsimp(True): + assert M.rref() == (Matrix([ + [1, 0, -1, -2, -3, -4, -5, -6], + [0, 1, 2, 3, 4, 5, 6, 7], + [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, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0]]), (0, 1)) + + +def test_issue_17247_expression_blowup_10(): + M = Matrix(6, 6, lambda i, j: 1 + (-1)**(i+j)*I) + with dotprodsimp(True): + assert M.cofactor(0, 0) == 0 + + +def test_issue_17247_expression_blowup_11(): + M = Matrix(6, 6, lambda i, j: 1 + (-1)**(i+j)*I) + with dotprodsimp(True): + assert M.cofactor_matrix() == Matrix(6, 6, [0]*36) + + +def test_issue_17247_expression_blowup_12(): + M = Matrix(6, 6, lambda i, j: 1 + (-1)**(i+j)*I) + with dotprodsimp(True): + assert M.eigenvals() == {6: 1, 6*I: 1, 0: 4} + + +def test_issue_17247_expression_blowup_13(): + M = Matrix([ + [ 0, 1 - x, x + 1, 1 - x], + [1 - x, x + 1, 0, x + 1], + [ 0, 1 - x, x + 1, 1 - x], + [ 0, 0, 1 - x, 0]]) + + ev = M.eigenvects() + assert ev[0] == (0, 2, [Matrix([0, -1, 0, 1])]) + assert ev[1][0] == x - sqrt(2)*(x - 1) + 1 + assert ev[1][1] == 1 + assert ev[1][2][0].expand(deep=False, numer=True) == Matrix([ + [(-x + sqrt(2)*(x - 1) - 1)/(x - 1)], + [-4*x/(x**2 - 2*x + 1) + (x + 1)*(x - sqrt(2)*(x - 1) + 1)/(x**2 - 2*x + 1)], + [(-x + sqrt(2)*(x - 1) - 1)/(x - 1)], + [1] + ]) + + assert ev[2][0] == x + sqrt(2)*(x - 1) + 1 + assert ev[2][1] == 1 + assert ev[2][2][0].expand(deep=False, numer=True) == Matrix([ + [(-x - sqrt(2)*(x - 1) - 1)/(x - 1)], + [-4*x/(x**2 - 2*x + 1) + (x + 1)*(x + sqrt(2)*(x - 1) + 1)/(x**2 - 2*x + 1)], + [(-x - sqrt(2)*(x - 1) - 1)/(x - 1)], + [1] + ]) + + +def test_issue_17247_expression_blowup_14(): + M = Matrix(8, 8, ([1+x, 1-x]*4 + [1-x, 1+x]*4)*4) + with dotprodsimp(True): + assert M.echelon_form() == Matrix([ + [x + 1, 1 - x, x + 1, 1 - x, x + 1, 1 - x, x + 1, 1 - x], + [ 0, 4*x, 0, 4*x, 0, 4*x, 0, 4*x], + [ 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, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0]]) + + +def test_issue_17247_expression_blowup_15(): + M = Matrix(8, 8, ([1+x, 1-x]*4 + [1-x, 1+x]*4)*4) + with dotprodsimp(True): + assert M.rowspace() == [Matrix([[x + 1, 1 - x, x + 1, 1 - x, x + 1, 1 - x, x + 1, 1 - x]]), Matrix([[0, 4*x, 0, 4*x, 0, 4*x, 0, 4*x]])] + + +def test_issue_17247_expression_blowup_16(): + M = Matrix(8, 8, ([1+x, 1-x]*4 + [1-x, 1+x]*4)*4) + with dotprodsimp(True): + assert M.columnspace() == [Matrix([[x + 1],[1 - x],[x + 1],[1 - x],[x + 1],[1 - x],[x + 1],[1 - x]]), Matrix([[1 - x],[x + 1],[1 - x],[x + 1],[1 - x],[x + 1],[1 - x],[x + 1]])] + + +def test_issue_17247_expression_blowup_17(): + M = Matrix(8, 8, [x+i for i in range (64)]) + with dotprodsimp(True): + assert M.nullspace() == [ + Matrix([[1],[-2],[1],[0],[0],[0],[0],[0]]), + Matrix([[2],[-3],[0],[1],[0],[0],[0],[0]]), + Matrix([[3],[-4],[0],[0],[1],[0],[0],[0]]), + Matrix([[4],[-5],[0],[0],[0],[1],[0],[0]]), + Matrix([[5],[-6],[0],[0],[0],[0],[1],[0]]), + Matrix([[6],[-7],[0],[0],[0],[0],[0],[1]])] + + +def test_issue_17247_expression_blowup_18(): + M = Matrix(6, 6, ([1+x, 1-x]*3 + [1-x, 1+x]*3)*3) + with dotprodsimp(True): + assert not M.is_nilpotent() + + +def test_issue_17247_expression_blowup_19(): + M = Matrix(S('''[ + [ -3/4, 0, 1/4 + I/2, 0], + [ 0, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 1/2 - I, 0, 0, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert not M.is_diagonalizable() + + +def test_issue_17247_expression_blowup_20(): + M = Matrix([ + [x + 1, 1 - x, 0, 0], + [1 - x, x + 1, 0, x + 1], + [ 0, 1 - x, x + 1, 0], + [ 0, 0, 0, x + 1]]) + with dotprodsimp(True): + assert M.diagonalize() == (Matrix([ + [1, 1, 0, (x + 1)/(x - 1)], + [1, -1, 0, 0], + [1, 1, 1, 0], + [0, 0, 0, 1]]), + Matrix([ + [2, 0, 0, 0], + [0, 2*x, 0, 0], + [0, 0, x + 1, 0], + [0, 0, 0, x + 1]])) + + +def test_issue_17247_expression_blowup_21(): + M = Matrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 0, 0], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 0, 9/4 + 55*I/16, 2473/256 + 137*I/64, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.inv(method='GE') == Matrix(S('''[ + [-26194832/3470993 - 31733264*I/3470993, 156352/3470993 + 10325632*I/3470993, 0, -7741283181072/3306971225785 + 2999007604624*I/3306971225785], + [4408224/3470993 - 9675328*I/3470993, -2422272/3470993 + 1523712*I/3470993, 0, -1824666489984/3306971225785 - 1401091949952*I/3306971225785], + [-26406945676288/22270005630769 + 10245925485056*I/22270005630769, 7453523312640/22270005630769 + 1601616519168*I/22270005630769, 633088/6416033 - 140288*I/6416033, 872209227109521408/21217636514687010905 + 6066405081802389504*I/21217636514687010905], + [0, 0, 0, -11328/952745 + 87616*I/952745]]''')) + + +def test_issue_17247_expression_blowup_22(): + M = Matrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 0, 0], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 0, 9/4 + 55*I/16, 2473/256 + 137*I/64, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.inv(method='LU') == Matrix(S('''[ + [-26194832/3470993 - 31733264*I/3470993, 156352/3470993 + 10325632*I/3470993, 0, -7741283181072/3306971225785 + 2999007604624*I/3306971225785], + [4408224/3470993 - 9675328*I/3470993, -2422272/3470993 + 1523712*I/3470993, 0, -1824666489984/3306971225785 - 1401091949952*I/3306971225785], + [-26406945676288/22270005630769 + 10245925485056*I/22270005630769, 7453523312640/22270005630769 + 1601616519168*I/22270005630769, 633088/6416033 - 140288*I/6416033, 872209227109521408/21217636514687010905 + 6066405081802389504*I/21217636514687010905], + [0, 0, 0, -11328/952745 + 87616*I/952745]]''')) + + +def test_issue_17247_expression_blowup_23(): + M = Matrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 0, 0], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 0, 9/4 + 55*I/16, 2473/256 + 137*I/64, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.inv(method='ADJ').expand() == Matrix(S('''[ + [-26194832/3470993 - 31733264*I/3470993, 156352/3470993 + 10325632*I/3470993, 0, -7741283181072/3306971225785 + 2999007604624*I/3306971225785], + [4408224/3470993 - 9675328*I/3470993, -2422272/3470993 + 1523712*I/3470993, 0, -1824666489984/3306971225785 - 1401091949952*I/3306971225785], + [-26406945676288/22270005630769 + 10245925485056*I/22270005630769, 7453523312640/22270005630769 + 1601616519168*I/22270005630769, 633088/6416033 - 140288*I/6416033, 872209227109521408/21217636514687010905 + 6066405081802389504*I/21217636514687010905], + [0, 0, 0, -11328/952745 + 87616*I/952745]]''')) + + +def test_issue_17247_expression_blowup_24(): + M = SparseMatrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 0, 0], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 0, 9/4 + 55*I/16, 2473/256 + 137*I/64, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.inv(method='CH') == Matrix(S('''[ + [-26194832/3470993 - 31733264*I/3470993, 156352/3470993 + 10325632*I/3470993, 0, -7741283181072/3306971225785 + 2999007604624*I/3306971225785], + [4408224/3470993 - 9675328*I/3470993, -2422272/3470993 + 1523712*I/3470993, 0, -1824666489984/3306971225785 - 1401091949952*I/3306971225785], + [-26406945676288/22270005630769 + 10245925485056*I/22270005630769, 7453523312640/22270005630769 + 1601616519168*I/22270005630769, 633088/6416033 - 140288*I/6416033, 872209227109521408/21217636514687010905 + 6066405081802389504*I/21217636514687010905], + [0, 0, 0, -11328/952745 + 87616*I/952745]]''')) + + +def test_issue_17247_expression_blowup_25(): + M = SparseMatrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 0, 0], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 0, 9/4 + 55*I/16, 2473/256 + 137*I/64, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.inv(method='LDL') == Matrix(S('''[ + [-26194832/3470993 - 31733264*I/3470993, 156352/3470993 + 10325632*I/3470993, 0, -7741283181072/3306971225785 + 2999007604624*I/3306971225785], + [4408224/3470993 - 9675328*I/3470993, -2422272/3470993 + 1523712*I/3470993, 0, -1824666489984/3306971225785 - 1401091949952*I/3306971225785], + [-26406945676288/22270005630769 + 10245925485056*I/22270005630769, 7453523312640/22270005630769 + 1601616519168*I/22270005630769, 633088/6416033 - 140288*I/6416033, 872209227109521408/21217636514687010905 + 6066405081802389504*I/21217636514687010905], + [0, 0, 0, -11328/952745 + 87616*I/952745]]''')) + + +def test_issue_17247_expression_blowup_26(): + M = Matrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64, 1/4 - 5*I/16, 65/128 + 87*I/64, -9/32 - I/16, 183/256 - 97*I/128], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128, 85/256 - 33*I/16, 805/128 + 2415*I/512, -219/128 + 115*I/256, 6301/4096 - 6609*I/1024], + [ 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64, 1/4 - 5*I/16, 65/128 + 87*I/64], + [ -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128, 85/256 - 33*I/16, 805/128 + 2415*I/512], + [ 1 + I, -19/4 + 5*I/4, 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16, 1/4 + I/2, -129/64 - 9*I/64], + [ 21/8 + I, -537/64 + 143*I/16, -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128, 125/64 + 87*I/64, -2063/256 + 541*I/128], + [ -2, 17/4 - 13*I/2, 1 + I, -19/4 + 5*I/4, 1/2 - I, 9/4 + 55*I/16, -3/4, 45/32 - 37*I/16], + [ 1/4 + 13*I/4, -825/64 - 147*I/32, 21/8 + I, -537/64 + 143*I/16, -5/8 - 39*I/16, 2473/256 + 137*I/64, -149/64 + 49*I/32, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.rank() == 4 + + +def test_issue_17247_expression_blowup_27(): + M = Matrix([ + [ 0, 1 - x, x + 1, 1 - x], + [1 - x, x + 1, 0, x + 1], + [ 0, 1 - x, x + 1, 1 - x], + [ 0, 0, 1 - x, 0]]) + with dotprodsimp(True): + P, J = M.jordan_form() + assert P.expand() == Matrix(S('''[ + [ 0, 4*x/(x**2 - 2*x + 1), -(-17*x**4 + 12*sqrt(2)*x**4 - 4*sqrt(2)*x**3 + 6*x**3 - 6*x - 4*sqrt(2)*x + 12*sqrt(2) + 17)/(-7*x**4 + 5*sqrt(2)*x**4 - 6*sqrt(2)*x**3 + 8*x**3 - 2*x**2 + 8*x + 6*sqrt(2)*x - 5*sqrt(2) - 7), -(12*sqrt(2)*x**4 + 17*x**4 - 6*x**3 - 4*sqrt(2)*x**3 - 4*sqrt(2)*x + 6*x - 17 + 12*sqrt(2))/(7*x**4 + 5*sqrt(2)*x**4 - 6*sqrt(2)*x**3 - 8*x**3 + 2*x**2 - 8*x + 6*sqrt(2)*x - 5*sqrt(2) + 7)], + [x - 1, x/(x - 1) + 1/(x - 1), (-7*x**3 + 5*sqrt(2)*x**3 - x**2 + sqrt(2)*x**2 - sqrt(2)*x - x - 5*sqrt(2) - 7)/(-3*x**3 + 2*sqrt(2)*x**3 - 2*sqrt(2)*x**2 + 3*x**2 + 2*sqrt(2)*x + 3*x - 3 - 2*sqrt(2)), (7*x**3 + 5*sqrt(2)*x**3 + x**2 + sqrt(2)*x**2 - sqrt(2)*x + x - 5*sqrt(2) + 7)/(2*sqrt(2)*x**3 + 3*x**3 - 3*x**2 - 2*sqrt(2)*x**2 - 3*x + 2*sqrt(2)*x - 2*sqrt(2) + 3)], + [ 0, 1, -(-3*x**2 + 2*sqrt(2)*x**2 + 2*x - 3 - 2*sqrt(2))/(-x**2 + sqrt(2)*x**2 - 2*sqrt(2)*x + 1 + sqrt(2)), -(2*sqrt(2)*x**2 + 3*x**2 - 2*x - 2*sqrt(2) + 3)/(x**2 + sqrt(2)*x**2 - 2*sqrt(2)*x - 1 + sqrt(2))], + [1 - x, 0, 1, 1]]''')).expand() + assert J == Matrix(S('''[ + [0, 1, 0, 0], + [0, 0, 0, 0], + [0, 0, x - sqrt(2)*(x - 1) + 1, 0], + [0, 0, 0, x + sqrt(2)*(x - 1) + 1]]''')) + + +def test_issue_17247_expression_blowup_28(): + M = Matrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 0, 0], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 0, 9/4 + 55*I/16, 2473/256 + 137*I/64, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.singular_values() == S('''[ + sqrt(14609315/131072 + sqrt(64789115132571/2147483648 - 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3) + 76627253330829751075/(35184372088832*sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))) - 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)))/2 + sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))/2), + sqrt(14609315/131072 - sqrt(64789115132571/2147483648 - 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3) + 76627253330829751075/(35184372088832*sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))) - 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)))/2 + sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))/2), + sqrt(14609315/131072 - sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))/2 + sqrt(64789115132571/2147483648 - 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3) - 76627253330829751075/(35184372088832*sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))) - 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)))/2), + sqrt(14609315/131072 - sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))/2 - sqrt(64789115132571/2147483648 - 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3) - 76627253330829751075/(35184372088832*sqrt(64789115132571/4294967296 + 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)) + 2*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3))) - 3546944054712886603889144627/(110680464442257309696*(25895222463957462655758224991455280215303/633825300114114700748351602688 + sqrt(1213909058710955930446995195883114969038524625997915131236390724543989220134670)*I/22282920707136844948184236032)**(1/3)))/2)]''') + + +def test_issue_16823(): + # This still needs to be fixed if not using dotprodsimp. + M = Matrix(S('''[ + [1+I,-19/4+5/4*I,1/2-I,9/4+55/16*I,-3/4,45/32-37/16*I,1/4+1/2*I,-129/64-9/64*I,1/4-5/16*I,65/128+87/64*I,-9/32-1/16*I,183/256-97/128*I,3/64+13/64*I,-23/32-59/256*I,15/128-3/32*I,19/256+551/1024*I], + [21/8+I,-537/64+143/16*I,-5/8-39/16*I,2473/256+137/64*I,-149/64+49/32*I,-177/128-1369/128*I,125/64+87/64*I,-2063/256+541/128*I,85/256-33/16*I,805/128+2415/512*I,-219/128+115/256*I,6301/4096-6609/1024*I,119/128+143/128*I,-10879/2048+4343/4096*I,129/256-549/512*I,42533/16384+29103/8192*I], + [-2,17/4-13/2*I,1+I,-19/4+5/4*I,1/2-I,9/4+55/16*I,-3/4,45/32-37/16*I,1/4+1/2*I,-129/64-9/64*I,1/4-5/16*I,65/128+87/64*I,-9/32-1/16*I,183/256-97/128*I,3/64+13/64*I,-23/32-59/256*I], + [1/4+13/4*I,-825/64-147/32*I,21/8+I,-537/64+143/16*I,-5/8-39/16*I,2473/256+137/64*I,-149/64+49/32*I,-177/128-1369/128*I,125/64+87/64*I,-2063/256+541/128*I,85/256-33/16*I,805/128+2415/512*I,-219/128+115/256*I,6301/4096-6609/1024*I,119/128+143/128*I,-10879/2048+4343/4096*I], + [-4*I,27/2+6*I,-2,17/4-13/2*I,1+I,-19/4+5/4*I,1/2-I,9/4+55/16*I,-3/4,45/32-37/16*I,1/4+1/2*I,-129/64-9/64*I,1/4-5/16*I,65/128+87/64*I,-9/32-1/16*I,183/256-97/128*I], + [1/4+5/2*I,-23/8-57/16*I,1/4+13/4*I,-825/64-147/32*I,21/8+I,-537/64+143/16*I,-5/8-39/16*I,2473/256+137/64*I,-149/64+49/32*I,-177/128-1369/128*I,125/64+87/64*I,-2063/256+541/128*I,85/256-33/16*I,805/128+2415/512*I,-219/128+115/256*I,6301/4096-6609/1024*I], + [-4,9-5*I,-4*I,27/2+6*I,-2,17/4-13/2*I,1+I,-19/4+5/4*I,1/2-I,9/4+55/16*I,-3/4,45/32-37/16*I,1/4+1/2*I,-129/64-9/64*I,1/4-5/16*I,65/128+87/64*I], + [-2*I,119/8+29/4*I,1/4+5/2*I,-23/8-57/16*I,1/4+13/4*I,-825/64-147/32*I,21/8+I,-537/64+143/16*I,-5/8-39/16*I,2473/256+137/64*I,-149/64+49/32*I,-177/128-1369/128*I,125/64+87/64*I,-2063/256+541/128*I,85/256-33/16*I,805/128+2415/512*I], + [0,-6,-4,9-5*I,-4*I,27/2+6*I,-2,17/4-13/2*I,1+I,-19/4+5/4*I,1/2-I,9/4+55/16*I,-3/4,45/32-37/16*I,1/4+1/2*I,-129/64-9/64*I], + [1,-9/4+3*I,-2*I,119/8+29/4*I,1/4+5/2*I,-23/8-57/16*I,1/4+13/4*I,-825/64-147/32*I,21/8+I,-537/64+143/16*I,-5/8-39/16*I,2473/256+137/64*I,-149/64+49/32*I,-177/128-1369/128*I,125/64+87/64*I,-2063/256+541/128*I], + [0,-4*I,0,-6,-4,9-5*I,-4*I,27/2+6*I,-2,17/4-13/2*I,1+I,-19/4+5/4*I,1/2-I,9/4+55/16*I,-3/4,45/32-37/16*I], + [0,1/4+1/2*I,1,-9/4+3*I,-2*I,119/8+29/4*I,1/4+5/2*I,-23/8-57/16*I,1/4+13/4*I,-825/64-147/32*I,21/8+I,-537/64+143/16*I,-5/8-39/16*I,2473/256+137/64*I,-149/64+49/32*I,-177/128-1369/128*I]]''')) + with dotprodsimp(True): + assert M.rank() == 8 + + +def test_issue_18531(): + # solve_linear_system still needs fixing but the rref works. + M = Matrix([ + [1, 1, 1, 1, 1, 0, 1, 0, 0], + [1 + sqrt(2), -1 + sqrt(2), 1 - sqrt(2), -sqrt(2) - 1, 1, 1, -1, 1, 1], + [-5 + 2*sqrt(2), -5 - 2*sqrt(2), -5 - 2*sqrt(2), -5 + 2*sqrt(2), -7, 2, -7, -2, 0], + [-3*sqrt(2) - 1, 1 - 3*sqrt(2), -1 + 3*sqrt(2), 1 + 3*sqrt(2), -7, -5, 7, -5, 3], + [7 - 4*sqrt(2), 4*sqrt(2) + 7, 4*sqrt(2) + 7, 7 - 4*sqrt(2), 7, -12, 7, 12, 0], + [-1 + 3*sqrt(2), 1 + 3*sqrt(2), -3*sqrt(2) - 1, 1 - 3*sqrt(2), 7, -5, -7, -5, 3], + [-3 + 2*sqrt(2), -3 - 2*sqrt(2), -3 - 2*sqrt(2), -3 + 2*sqrt(2), -1, 2, -1, -2, 0], + [1 - sqrt(2), -sqrt(2) - 1, 1 + sqrt(2), -1 + sqrt(2), -1, 1, 1, 1, 1] + ]) + with dotprodsimp(True): + assert M.rref() == (Matrix([ + [1, 0, 0, 0, 0, 0, 0, 0, S(1)/2], + [0, 1, 0, 0, 0, 0, 0, 0, -S(1)/2], + [0, 0, 1, 0, 0, 0, 0, 0, S(1)/2], + [0, 0, 0, 1, 0, 0, 0, 0, -S(1)/2], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, -S(1)/2], + [0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, -S(1)/2]]), (0, 1, 2, 3, 4, 5, 6, 7)) + + +def test_creation(): + raises(ValueError, lambda: Matrix(5, 5, range(20))) + raises(ValueError, lambda: Matrix(5, -1, [])) + raises(IndexError, lambda: Matrix((1, 2))[2]) + with raises(IndexError): + Matrix((1, 2))[3] = 5 + + assert Matrix() == Matrix([]) == Matrix(0, 0, []) + assert Matrix([[]]) == Matrix(1, 0, []) + assert Matrix([[], []]) == Matrix(2, 0, []) + + # anything used to be allowed in a matrix + with warns_deprecated_sympy(): + assert Matrix([[[1], (2,)]]).tolist() == [[[1], (2,)]] + with warns_deprecated_sympy(): + assert Matrix([[[1], (2,)]]).T.tolist() == [[[1]], [(2,)]] + M = Matrix([[0]]) + with warns_deprecated_sympy(): + M[0, 0] = S.EmptySet + + a = Matrix([[x, 0], [0, 0]]) + m = a + assert m.cols == m.rows + assert m.cols == 2 + assert m[:] == [x, 0, 0, 0] + + b = Matrix(2, 2, [x, 0, 0, 0]) + m = b + assert m.cols == m.rows + assert m.cols == 2 + assert m[:] == [x, 0, 0, 0] + + assert a == b + + assert Matrix(b) == b + + c23 = Matrix(2, 3, range(1, 7)) + c13 = Matrix(1, 3, range(7, 10)) + c = Matrix([c23, c13]) + assert c.cols == 3 + assert c.rows == 3 + assert c[:] == [1, 2, 3, 4, 5, 6, 7, 8, 9] + + assert Matrix(eye(2)) == eye(2) + assert ImmutableMatrix(ImmutableMatrix(eye(2))) == ImmutableMatrix(eye(2)) + assert ImmutableMatrix(c) == c.as_immutable() + assert Matrix(ImmutableMatrix(c)) == ImmutableMatrix(c).as_mutable() + + assert c is not Matrix(c) + + dat = [[ones(3,2), ones(3,3)*2], [ones(2,3)*3, ones(2,2)*4]] + M = Matrix(dat) + assert M == Matrix([ + [1, 1, 2, 2, 2], + [1, 1, 2, 2, 2], + [1, 1, 2, 2, 2], + [3, 3, 3, 4, 4], + [3, 3, 3, 4, 4]]) + assert M.tolist() != dat + # keep block form if evaluate=False + assert Matrix(dat, evaluate=False).tolist() == dat + A = MatrixSymbol("A", 2, 2) + dat = [ones(2), A] + assert Matrix(dat) == Matrix([ + [ 1, 1], + [ 1, 1], + [A[0, 0], A[0, 1]], + [A[1, 0], A[1, 1]]]) + with warns_deprecated_sympy(): + assert Matrix(dat, evaluate=False).tolist() == [[i] for i in dat] + + # 0-dim tolerance + assert Matrix([ones(2), ones(0)]) == Matrix([ones(2)]) + raises(ValueError, lambda: Matrix([ones(2), ones(0, 3)])) + raises(ValueError, lambda: Matrix([ones(2), ones(3, 0)])) + + # mix of Matrix and iterable + M = Matrix([[1, 2], [3, 4]]) + M2 = Matrix([M, (5, 6)]) + assert M2 == Matrix([[1, 2], [3, 4], [5, 6]]) + + +def test_irregular_block(): + assert Matrix.irregular(3, ones(2,1), ones(3,3)*2, ones(2,2)*3, + ones(1,1)*4, ones(2,2)*5, ones(1,2)*6, ones(1,2)*7) == Matrix([ + [1, 2, 2, 2, 3, 3], + [1, 2, 2, 2, 3, 3], + [4, 2, 2, 2, 5, 5], + [6, 6, 7, 7, 5, 5]]) + + +def test_slicing(): + m0 = eye(4) + assert m0[:3, :3] == eye(3) + assert m0[2:4, 0:2] == zeros(2) + + m1 = Matrix(3, 3, lambda i, j: i + j) + assert m1[0, :] == Matrix(1, 3, (0, 1, 2)) + assert m1[1:3, 1] == Matrix(2, 1, (2, 3)) + + m2 = Matrix([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]]) + assert m2[:, -1] == Matrix(4, 1, [3, 7, 11, 15]) + assert m2[-2:, :] == Matrix([[8, 9, 10, 11], [12, 13, 14, 15]]) + + +def test_submatrix_assignment(): + m = zeros(4) + m[2:4, 2:4] = eye(2) + assert m == Matrix(((0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 1, 0), + (0, 0, 0, 1))) + m[:2, :2] = eye(2) + assert m == eye(4) + m[:, 0] = Matrix(4, 1, (1, 2, 3, 4)) + assert m == Matrix(((1, 0, 0, 0), + (2, 1, 0, 0), + (3, 0, 1, 0), + (4, 0, 0, 1))) + m[:, :] = zeros(4) + assert m == zeros(4) + m[:, :] = [(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12), (13, 14, 15, 16)] + assert m == Matrix(((1, 2, 3, 4), + (5, 6, 7, 8), + (9, 10, 11, 12), + (13, 14, 15, 16))) + m[:2, 0] = [0, 0] + assert m == Matrix(((0, 2, 3, 4), + (0, 6, 7, 8), + (9, 10, 11, 12), + (13, 14, 15, 16))) + + +def test_reshape(): + m0 = eye(3) + assert m0.reshape(1, 9) == Matrix(1, 9, (1, 0, 0, 0, 1, 0, 0, 0, 1)) + m1 = Matrix(3, 4, lambda i, j: i + j) + assert m1.reshape( + 4, 3) == Matrix(((0, 1, 2), (3, 1, 2), (3, 4, 2), (3, 4, 5))) + assert m1.reshape(2, 6) == Matrix(((0, 1, 2, 3, 1, 2), (3, 4, 2, 3, 4, 5))) + + +def test_applyfunc(): + m0 = eye(3) + assert m0.applyfunc(lambda x: 2*x) == eye(3)*2 + assert m0.applyfunc(lambda x: 0) == zeros(3) + + +def test_expand(): + m0 = Matrix([[x*(x + y), 2], [((x + y)*y)*x, x*(y + x*(x + y))]]) + # Test if expand() returns a matrix + m1 = m0.expand() + assert m1 == Matrix( + [[x*y + x**2, 2], [x*y**2 + y*x**2, x*y + y*x**2 + x**3]]) + + a = Symbol('a', real=True) + + assert Matrix([exp(I*a)]).expand(complex=True) == \ + Matrix([cos(a) + I*sin(a)]) + + assert Matrix([[0, 1, 2], [0, 0, -1], [0, 0, 0]]).exp() == Matrix([ + [1, 1, Rational(3, 2)], + [0, 1, -1], + [0, 0, 1]] + ) + + +def test_refine(): + m0 = Matrix([[Abs(x)**2, sqrt(x**2)], + [sqrt(x**2)*Abs(y)**2, sqrt(y**2)*Abs(x)**2]]) + m1 = m0.refine(Q.real(x) & Q.real(y)) + assert m1 == Matrix([[x**2, Abs(x)], [y**2*Abs(x), x**2*Abs(y)]]) + + m1 = m0.refine(Q.positive(x) & Q.positive(y)) + assert m1 == Matrix([[x**2, x], [x*y**2, x**2*y]]) + + m1 = m0.refine(Q.negative(x) & Q.negative(y)) + assert m1 == Matrix([[x**2, -x], [-x*y**2, -x**2*y]]) + + +def test_random(): + M = randMatrix(3, 3) + M = randMatrix(3, 3, seed=3) + assert M == randMatrix(3, 3, seed=3) + + M = randMatrix(3, 4, 0, 150) + M = randMatrix(3, seed=4, symmetric=True) + assert M == randMatrix(3, seed=4, symmetric=True) + + S = M.copy() + S.simplify() + assert S == M # doesn't fail when elements are Numbers, not int + + rng = random.Random(4) + assert M == randMatrix(3, symmetric=True, prng=rng) + + # Ensure symmetry + for size in (10, 11): # Test odd and even + for percent in (100, 70, 30): + M = randMatrix(size, symmetric=True, percent=percent, prng=rng) + assert M == M.T + + M = randMatrix(10, min=1, percent=70) + zero_count = 0 + for i in range(M.shape[0]): + for j in range(M.shape[1]): + if M[i, j] == 0: + zero_count += 1 + assert zero_count == 30 + + +def test_inverse(): + A = eye(4) + assert A.inv() == eye(4) + assert A.inv(method="LU") == eye(4) + assert A.inv(method="ADJ") == eye(4) + assert A.inv(method="CH") == eye(4) + assert A.inv(method="LDL") == eye(4) + assert A.inv(method="QR") == eye(4) + A = Matrix([[2, 3, 5], + [3, 6, 2], + [8, 3, 6]]) + Ainv = A.inv() + assert A*Ainv == eye(3) + assert A.inv(method="LU") == Ainv + assert A.inv(method="ADJ") == Ainv + assert A.inv(method="CH") == Ainv + assert A.inv(method="LDL") == Ainv + assert A.inv(method="QR") == Ainv + + AA = Matrix([[0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], + [1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0], + [1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1], + [1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0], + [1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1], + [0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0], + [1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1], + [0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1], + [1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0], + [1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1], + [1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0], + [0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1], + [0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1], + [1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1], + [0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1], + [0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0], + [0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0]]) + assert AA.inv(method="BLOCK") * AA == eye(AA.shape[0]) + # test that immutability is not a problem + cls = ImmutableMatrix + m = cls([[48, 49, 31], + [ 9, 71, 94], + [59, 28, 65]]) + assert all(type(m.inv(s)) is cls for s in 'GE ADJ LU CH LDL QR'.split()) + cls = ImmutableSparseMatrix + m = cls([[48, 49, 31], + [ 9, 71, 94], + [59, 28, 65]]) + assert all(type(m.inv(s)) is cls for s in 'GE ADJ LU CH LDL QR'.split()) + + +def test_inverse_symbolic_float_issue_26821(): + Tau, Tau_syn_in, Tau_syn_ex, C_m, Tau_syn_gap = symbols("Tau Tau_syn_in Tau_syn_ex C_m Tau_syn_gap") + __h = symbols("__h") + + M = Matrix([ + [0,0,0,0,0,(1.0*Tau*__h-1.0*Tau_syn_in*__h)/(2.0*Tau-1.0*Tau_syn_in),-1.0*Tau*Tau_syn_in/(2.0*Tau-1.0*Tau_syn_in)], + [0,0,0,0,0,(-1.0*Tau*__h+1.0*Tau_syn_in*__h)/(2.0*Tau*Tau_syn_in-1.0*Tau_syn_in**2),1.0], + [0,(1.0*Tau*__h-1.0*Tau_syn_ex*__h)/(2.0*Tau-1.0*Tau_syn_ex),-1.0*Tau*Tau_syn_ex/(2.0*Tau-1.0*Tau_syn_ex),0,0,0,0], + [0,(-1.0*Tau*__h+1.0*Tau_syn_ex*__h)/(2.0*Tau*Tau_syn_ex-1.0*Tau_syn_ex**2),1.0,0,0,0,0], + [0,0,0,(1.0*Tau*__h-1.0*Tau_syn_gap*__h)/(2.0*Tau-1.0*Tau_syn_gap),-1.0*Tau*Tau_syn_gap/(2.0*Tau-1.0*Tau_syn_gap),0,0], + [0,0,0,(-1.0*Tau*__h+1.0*Tau_syn_gap*__h)/(2.0*Tau*Tau_syn_gap-1.0*Tau_syn_gap**2),1.0,0,0], + [1.0,-1.0*Tau*Tau_syn_ex*__h/(2.0*C_m*Tau-1.0*C_m*Tau_syn_ex),0,-1.0*Tau*Tau_syn_gap*__h/(2.0*C_m*Tau-1.0*C_m*Tau_syn_gap),0,-1.0*Tau*Tau_syn_in*__h/(2.0*C_m*Tau-1.0*C_m*Tau_syn_in),0] + ]) + + Mi = M.inv() + + assert (M*Mi - eye(7)).applyfunc(cancel) == zeros(7) + + # https://github.com/sympy/sympy/issues/26821 + # Previously very large floats were in the result. + assert max(abs(f) for f in Mi.atoms(Float)) < 1e3 + + +@slow +def test_matrix_exponential_issue_26821(): + # The symbol names matter in the original bug... + a, b, c, d, e = symbols("Tau, Tau_syn_in, Tau_syn_ex, C_m, Tau_syn_gap") + t = symbols("__h") + M = Matrix([ + [ 0, 1.0, 0, 0, 0, 0, 0], + [-1/b**2, -2/b, 0, 0, 0, 0, 0], + [ 0, 0, 0, 1.0, 0, 0, 0], + [ 0, 0, -1/c**2, -2/c, 0, 0, 0], + [ 0, 0, 0, 0, 0, 1, 0], + [ 0, 0, 0, 0, -1/e**2, -2/e, 0], + [ 1/d, 0, 1/d, 0, 1/d, 0, -1/a] + ]) + + Me = (t*M).exp() + assert (Me.diff(t) - M*Me).applyfunc(cancel) == zeros(7) + # https://github.com/sympy/sympy/issues/26821 + # Previously very large floats were in the result. + assert max(abs(f) for f in Me.atoms(Float)) < 1e3 + + +def test_jacobian_hessian(): + L = Matrix(1, 2, [x**2*y, 2*y**2 + x*y]) + syms = [x, y] + assert L.jacobian(syms) == Matrix([[2*x*y, x**2], [y, 4*y + x]]) + + L = Matrix(1, 2, [x, x**2*y**3]) + assert L.jacobian(syms) == Matrix([[1, 0], [2*x*y**3, x**2*3*y**2]]) + + f = x**2*y + syms = [x, y] + assert hessian(f, syms) == Matrix([[2*y, 2*x], [2*x, 0]]) + + f = x**2*y**3 + assert hessian(f, syms) == \ + Matrix([[2*y**3, 6*x*y**2], [6*x*y**2, 6*x**2*y]]) + + f = z + x*y**2 + g = x**2 + 2*y**3 + ans = Matrix([[0, 2*y], + [2*y, 2*x]]) + assert ans == hessian(f, Matrix([x, y])) + assert ans == hessian(f, Matrix([x, y]).T) + assert hessian(f, (y, x), [g]) == Matrix([ + [ 0, 6*y**2, 2*x], + [6*y**2, 2*x, 2*y], + [ 2*x, 2*y, 0]]) + + +def test_wronskian(): + assert wronskian([cos(x), sin(x)], x) == cos(x)**2 + sin(x)**2 + assert wronskian([exp(x), exp(2*x)], x) == exp(3*x) + assert wronskian([exp(x), x], x) == exp(x) - x*exp(x) + assert wronskian([1, x, x**2], x) == 2 + w1 = -6*exp(x)*sin(x)*x + 6*cos(x)*exp(x)*x**2 - 6*exp(x)*cos(x)*x - \ + exp(x)*cos(x)*x**3 + exp(x)*sin(x)*x**3 + assert wronskian([exp(x), cos(x), x**3], x).expand() == w1 + assert wronskian([exp(x), cos(x), x**3], x, method='berkowitz').expand() \ + == w1 + w2 = -x**3*cos(x)**2 - x**3*sin(x)**2 - 6*x*cos(x)**2 - 6*x*sin(x)**2 + assert wronskian([sin(x), cos(x), x**3], x).expand() == w2 + assert wronskian([sin(x), cos(x), x**3], x, method='berkowitz').expand() \ + == w2 + assert wronskian([], x) == 1 + + +def test_xreplace(): + assert Matrix([[1, x], [x, 4]]).xreplace({x: 5}) == \ + Matrix([[1, 5], [5, 4]]) + assert Matrix([[x, 2], [x + y, 4]]).xreplace({x: -1, y: -2}) == \ + Matrix([[-1, 2], [-3, 4]]) + for cls in all_classes: + assert Matrix([[2, 0], [0, 2]]) == cls.eye(2).xreplace({1: 2}) + + +def test_simplify(): + n = Symbol('n') + f = Function('f') + + M = Matrix([[ 1/x + 1/y, (x + x*y) / x ], + [ (f(x) + y*f(x))/f(x), 2 * (1/n - cos(n * pi)/n) / pi ]]) + M.simplify() + assert M == Matrix([[ (x + y)/(x * y), 1 + y ], + [ 1 + y, 2*((1 - 1*cos(pi*n))/(pi*n)) ]]) + eq = (1 + x)**2 + M = Matrix([[eq]]) + M.simplify() + assert M == Matrix([[eq]]) + M.simplify(ratio=oo) + assert M == Matrix([[eq.simplify(ratio=oo)]]) + + n = Symbol('n') + f = Function('f') + + M = ImmutableMatrix([ + [ 1/x + 1/y, (x + x*y) / x ], + [ (f(x) + y*f(x))/f(x), 2 * (1/n - cos(n * pi)/n) / pi ] + ]) + assert M.simplify() == Matrix([ + [ (x + y)/(x * y), 1 + y ], + [ 1 + y, 2*((1 - 1*cos(pi*n))/(pi*n)) ] + ]) + + eq = (1 + x)**2 + M = ImmutableMatrix([[eq]]) + assert M.simplify() == Matrix([[eq]]) + assert M.simplify(ratio=oo) == Matrix([[eq.simplify(ratio=oo)]]) + + assert simplify(ImmutableMatrix([[sin(x)**2 + cos(x)**2]])) == \ + ImmutableMatrix([[1]]) + + # https://github.com/sympy/sympy/issues/19353 + m = Matrix([[30, 2], [3, 4]]) + assert (1/(m.trace())).simplify() == Rational(1, 34) + +def test_transpose(): + M = Matrix([[1, 2, 3, 4, 5, 6, 7, 8, 9, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]]) + assert M.T == Matrix( [ [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5], + [6, 6], + [7, 7], + [8, 8], + [9, 9], + [0, 0] ]) + assert M.T.T == M + assert M.T == M.transpose() + + +def test_conj_dirac(): + raises(AttributeError, lambda: eye(3).D) + + M = Matrix([[1, I, I, I], + [0, 1, I, I], + [0, 0, 1, I], + [0, 0, 0, 1]]) + + assert M.D == Matrix([[ 1, 0, 0, 0], + [-I, 1, 0, 0], + [-I, -I, -1, 0], + [-I, -I, I, -1]]) + + +def test_trace(): + M = Matrix([[1, 0, 0], + [0, 5, 0], + [0, 0, 8]]) + assert M.trace() == 14 + + +def test_shape(): + m = Matrix(1, 2, [0, 0]) + assert m.shape == (1, 2) + M = Matrix([[x, 0, 0], + [0, y, 0]]) + assert M.shape == (2, 3) + + +def test_col_row_op(): + M = Matrix([[x, 0, 0], + [0, y, 0]]) + M.row_op(1, lambda r, j: r + j + 1) + assert M == Matrix([[x, 0, 0], + [1, y + 2, 3]]) + + M.col_op(0, lambda c, j: c + y**j) + assert M == Matrix([[x + 1, 0, 0], + [1 + y, y + 2, 3]]) + + # neither row nor slice give copies that allow the original matrix to + # be changed + assert M.row(0) == Matrix([[x + 1, 0, 0]]) + r1 = M.row(0) + r1[0] = 42 + assert M[0, 0] == x + 1 + r1 = M[0, :-1] # also testing negative slice + r1[0] = 42 + assert M[0, 0] == x + 1 + c1 = M.col(0) + assert c1 == Matrix([x + 1, 1 + y]) + c1[0] = 0 + assert M[0, 0] == x + 1 + c1 = M[:, 0] + c1[0] = 42 + assert M[0, 0] == x + 1 + + +def test_row_mult(): + M = Matrix([[1,2,3], + [4,5,6]]) + M.row_mult(1,3) + assert M[1,0] == 12 + assert M[0,0] == 1 + assert M[1,2] == 18 + + +def test_row_add(): + M = Matrix([[1,2,3], + [4,5,6], + [1,1,1]]) + M.row_add(2,0,5) + assert M[0,0] == 6 + assert M[1,0] == 4 + assert M[0,2] == 8 + + +def test_zip_row_op(): + for cls in mutable_classes: # XXX: immutable matrices don't support row ops + M = cls.eye(3) + M.zip_row_op(1, 0, lambda v, u: v + 2*u) + assert M == cls([[1, 0, 0], + [2, 1, 0], + [0, 0, 1]]) + + M = cls.eye(3)*2 + M[0, 1] = -1 + M.zip_row_op(1, 0, lambda v, u: v + 2*u); M + assert M == cls([[2, -1, 0], + [4, 0, 0], + [0, 0, 2]]) + + +def test_issue_3950(): + m = Matrix([1, 2, 3]) + a = Matrix([1, 2, 3]) + b = Matrix([2, 2, 3]) + assert not (m in []) + assert not (m in [1]) + assert m != 1 + assert m == a + assert m != b + + +def test_issue_3981(): + class Index1: + def __index__(self): + return 1 + + class Index2: + def __index__(self): + return 2 + index1 = Index1() + index2 = Index2() + + m = Matrix([1, 2, 3]) + + assert m[index2] == 3 + + m[index2] = 5 + assert m[2] == 5 + + m = Matrix([[1, 2, 3], [4, 5, 6]]) + assert m[index1, index2] == 6 + assert m[1, index2] == 6 + assert m[index1, 2] == 6 + + m[index1, index2] = 4 + assert m[1, 2] == 4 + m[1, index2] = 6 + assert m[1, 2] == 6 + m[index1, 2] = 8 + assert m[1, 2] == 8 + + +def test_is_upper(): + a = Matrix([[1, 2, 3]]) + assert a.is_upper is True + a = Matrix([[1], [2], [3]]) + assert a.is_upper is False + a = zeros(4, 2) + assert a.is_upper is True + + +def test_is_lower(): + a = Matrix([[1, 2, 3]]) + assert a.is_lower is False + a = Matrix([[1], [2], [3]]) + assert a.is_lower is True + + +def test_is_nilpotent(): + a = Matrix(4, 4, [0, 2, 1, 6, 0, 0, 1, 2, 0, 0, 0, 3, 0, 0, 0, 0]) + assert a.is_nilpotent() + a = Matrix([[1, 0], [0, 1]]) + assert not a.is_nilpotent() + a = Matrix([]) + assert a.is_nilpotent() + + +def test_zeros_ones_fill(): + n, m = 3, 5 + + a = zeros(n, m) + a.fill( 5 ) + + b = 5 * ones(n, m) + + assert a == b + assert a.rows == b.rows == 3 + assert a.cols == b.cols == 5 + assert a.shape == b.shape == (3, 5) + assert zeros(2) == zeros(2, 2) + assert ones(2) == ones(2, 2) + assert zeros(2, 3) == Matrix(2, 3, [0]*6) + assert ones(2, 3) == Matrix(2, 3, [1]*6) + + a.fill(0) + assert a == zeros(n, m) + + +def test_empty_zeros(): + a = zeros(0) + assert a == Matrix() + a = zeros(0, 2) + assert a.rows == 0 + assert a.cols == 2 + a = zeros(2, 0) + assert a.rows == 2 + assert a.cols == 0 + + +def test_issue_3749(): + a = Matrix([[x**2, x*y], [x*sin(y), x*cos(y)]]) + assert a.diff(x) == Matrix([[2*x, y], [sin(y), cos(y)]]) + assert Matrix([ + [x, -x, x**2], + [exp(x), 1/x - exp(-x), x + 1/x]]).limit(x, oo) == \ + Matrix([[oo, -oo, oo], [oo, 0, oo]]) + assert Matrix([ + [(exp(x) - 1)/x, 2*x + y*x, x**x ], + [1/x, abs(x), abs(sin(x + 1))]]).limit(x, 0) == \ + Matrix([[1, 0, 1], [oo, 0, sin(1)]]) + assert a.integrate(x) == Matrix([ + [Rational(1, 3)*x**3, y*x**2/2], + [x**2*sin(y)/2, x**2*cos(y)/2]]) + + +def test_inv_iszerofunc(): + A = eye(4) + A.col_swap(0, 1) + for method in "GE", "LU": + assert A.inv(method=method, iszerofunc=lambda x: x == 0) == \ + A.inv(method="ADJ") + + +def test_jacobian_metrics(): + rho, phi = symbols("rho,phi") + X = Matrix([rho*cos(phi), rho*sin(phi)]) + Y = Matrix([rho, phi]) + J = X.jacobian(Y) + assert J == X.jacobian(Y.T) + assert J == (X.T).jacobian(Y) + assert J == (X.T).jacobian(Y.T) + g = J.T*eye(J.shape[0])*J + g = g.applyfunc(trigsimp) + assert g == Matrix([[1, 0], [0, rho**2]]) + + +def test_jacobian2(): + rho, phi = symbols("rho,phi") + X = Matrix([rho*cos(phi), rho*sin(phi), rho**2]) + Y = Matrix([rho, phi]) + J = Matrix([ + [cos(phi), -rho*sin(phi)], + [sin(phi), rho*cos(phi)], + [ 2*rho, 0], + ]) + assert X.jacobian(Y) == J + + +def test_issue_4564(): + X = Matrix([exp(x + y + z), exp(x + y + z), exp(x + y + z)]) + Y = Matrix([x, y, z]) + for i in range(1, 3): + for j in range(1, 3): + X_slice = X[:i, :] + Y_slice = Y[:j, :] + J = X_slice.jacobian(Y_slice) + assert J.rows == i + assert J.cols == j + for k in range(j): + assert J[:, k] == X_slice + + +def test_nonvectorJacobian(): + X = Matrix([[exp(x + y + z), exp(x + y + z)], + [exp(x + y + z), exp(x + y + z)]]) + raises(TypeError, lambda: X.jacobian(Matrix([x, y, z]))) + X = X[0, :] + Y = Matrix([[x, y], [x, z]]) + raises(TypeError, lambda: X.jacobian(Y)) + raises(TypeError, lambda: X.jacobian(Matrix([ [x, y], [x, z] ]))) + + +def test_vec(): + m = Matrix([[1, 3], [2, 4]]) + m_vec = m.vec() + assert m_vec.cols == 1 + for i in range(4): + assert m_vec[i] == i + 1 + + +def test_vech(): + m = Matrix([[1, 2], [2, 3]]) + m_vech = m.vech() + assert m_vech.cols == 1 + for i in range(3): + assert m_vech[i] == i + 1 + m_vech = m.vech(diagonal=False) + assert m_vech[0] == 2 + + m = Matrix([[1, x*(x + y)], [y*x + x**2, 1]]) + m_vech = m.vech(diagonal=False) + assert m_vech[0] == y*x + x**2 + + m = Matrix([[1, x*(x + y)], [y*x, 1]]) + m_vech = m.vech(diagonal=False, check_symmetry=False) + assert m_vech[0] == y*x + + raises(ShapeError, lambda: Matrix([[1, 3]]).vech()) + raises(ValueError, lambda: Matrix([[1, 3], [2, 4]]).vech()) + raises(ShapeError, lambda: Matrix([[1, 3]]).vech()) + raises(ValueError, lambda: Matrix([[1, 3], [2, 4]]).vech()) + + +def test_diag(): + # mostly tested in testcommonmatrix.py + assert diag([1, 2, 3]) == Matrix([1, 2, 3]) + m = [1, 2, [3]] + raises(ValueError, lambda: diag(m)) + assert diag(m, strict=False) == Matrix([1, 2, 3]) + + +def test_inv_block(): + a = Matrix([[1, 2], [2, 3]]) + b = Matrix([[3, x], [y, 3]]) + c = Matrix([[3, x, 3], [y, 3, z], [x, y, z]]) + A = diag(a, b, b) + assert A.inv(try_block_diag=True) == diag(a.inv(), b.inv(), b.inv()) + A = diag(a, b, c) + assert A.inv(try_block_diag=True) == diag(a.inv(), b.inv(), c.inv()) + A = diag(a, c, b) + assert A.inv(try_block_diag=True) == diag(a.inv(), c.inv(), b.inv()) + A = diag(a, a, b, a, c, a) + assert A.inv(try_block_diag=True) == diag( + a.inv(), a.inv(), b.inv(), a.inv(), c.inv(), a.inv()) + assert A.inv(try_block_diag=True, method="ADJ") == diag( + a.inv(method="ADJ"), a.inv(method="ADJ"), b.inv(method="ADJ"), + a.inv(method="ADJ"), c.inv(method="ADJ"), a.inv(method="ADJ")) + + +def test_creation_args(): + """ + Check that matrix dimensions can be specified using any reasonable type + (see issue 4614). + """ + raises(ValueError, lambda: zeros(3, -1)) + raises(TypeError, lambda: zeros(1, 2, 3, 4)) + assert zeros(int(3)) == zeros(3) + assert zeros(Integer(3)) == zeros(3) + raises(ValueError, lambda: zeros(3.)) + assert eye(int(3)) == eye(3) + assert eye(Integer(3)) == eye(3) + raises(ValueError, lambda: eye(3.)) + assert ones(int(3), Integer(4)) == ones(3, 4) + raises(TypeError, lambda: Matrix(5)) + raises(TypeError, lambda: Matrix(1, 2)) + raises(ValueError, lambda: Matrix([1, [2]])) + + +def test_diagonal_symmetrical(): + m = Matrix(2, 2, [0, 1, 1, 0]) + assert not m.is_diagonal() + assert m.is_symmetric() + assert m.is_symmetric(simplify=False) + + m = Matrix(2, 2, [1, 0, 0, 1]) + assert m.is_diagonal() + + m = diag(1, 2, 3) + assert m.is_diagonal() + assert m.is_symmetric() + + m = Matrix(3, 3, [1, 0, 0, 0, 2, 0, 0, 0, 3]) + assert m == diag(1, 2, 3) + + m = Matrix(2, 3, zeros(2, 3)) + assert not m.is_symmetric() + assert m.is_diagonal() + + m = Matrix(((5, 0), (0, 6), (0, 0))) + assert m.is_diagonal() + + m = Matrix(((5, 0, 0), (0, 6, 0))) + assert m.is_diagonal() + + m = Matrix(3, 3, [1, x**2 + 2*x + 1, y, (x + 1)**2, 2, 0, y, 0, 3]) + assert m.is_symmetric() + assert not m.is_symmetric(simplify=False) + assert m.expand().is_symmetric(simplify=False) + + +def test_diagonalization(): + m = Matrix([[1, 2+I], [2-I, 3]]) + assert m.is_diagonalizable() + + m = Matrix(3, 2, [-3, 1, -3, 20, 3, 10]) + assert not m.is_diagonalizable() + assert not m.is_symmetric() + raises(NonSquareMatrixError, lambda: m.diagonalize()) + + # diagonalizable + m = diag(1, 2, 3) + (P, D) = m.diagonalize() + assert P == eye(3) + assert D == m + + m = Matrix(2, 2, [0, 1, 1, 0]) + assert m.is_symmetric() + assert m.is_diagonalizable() + (P, D) = m.diagonalize() + assert P.inv() * m * P == D + + m = Matrix(2, 2, [1, 0, 0, 3]) + assert m.is_symmetric() + assert m.is_diagonalizable() + (P, D) = m.diagonalize() + assert P.inv() * m * P == D + assert P == eye(2) + assert D == m + + m = Matrix(2, 2, [1, 1, 0, 0]) + assert m.is_diagonalizable() + (P, D) = m.diagonalize() + assert P.inv() * m * P == D + + m = Matrix(3, 3, [1, 2, 0, 0, 3, 0, 2, -4, 2]) + assert m.is_diagonalizable() + (P, D) = m.diagonalize() + assert P.inv() * m * P == D + for i in P: + assert i.as_numer_denom()[1] == 1 + + m = Matrix(2, 2, [1, 0, 0, 0]) + assert m.is_diagonal() + assert m.is_diagonalizable() + (P, D) = m.diagonalize() + assert P.inv() * m * P == D + assert P == Matrix([[0, 1], [1, 0]]) + + # diagonalizable, complex only + m = Matrix(2, 2, [0, 1, -1, 0]) + assert not m.is_diagonalizable(True) + raises(MatrixError, lambda: m.diagonalize(True)) + assert m.is_diagonalizable() + (P, D) = m.diagonalize() + assert P.inv() * m * P == D + + # not diagonalizable + m = Matrix(2, 2, [0, 1, 0, 0]) + assert not m.is_diagonalizable() + raises(MatrixError, lambda: m.diagonalize()) + + m = Matrix(3, 3, [-3, 1, -3, 20, 3, 10, 2, -2, 4]) + assert not m.is_diagonalizable() + raises(MatrixError, lambda: m.diagonalize()) + + # symbolic + a, b, c, d = symbols('a b c d') + m = Matrix(2, 2, [a, c, c, b]) + assert m.is_symmetric() + assert m.is_diagonalizable() + + +def test_issue_15887(): + # Mutable matrix should not use cache + a = MutableDenseMatrix([[0, 1], [1, 0]]) + assert a.is_diagonalizable() is True + a[1, 0] = 0 + assert a.is_diagonalizable() is False + + a = MutableDenseMatrix([[0, 1], [1, 0]]) + a.diagonalize() + a[1, 0] = 0 + raises(MatrixError, lambda: a.diagonalize()) + + +def test_jordan_form(): + + m = Matrix(3, 2, [-3, 1, -3, 20, 3, 10]) + raises(NonSquareMatrixError, lambda: m.jordan_form()) + + # diagonalizable + m = Matrix(3, 3, [7, -12, 6, 10, -19, 10, 12, -24, 13]) + Jmust = Matrix(3, 3, [-1, 0, 0, 0, 1, 0, 0, 0, 1]) + P, J = m.jordan_form() + assert Jmust == J + assert Jmust == m.diagonalize()[1] + + # m = Matrix(3, 3, [0, 6, 3, 1, 3, 1, -2, 2, 1]) + # m.jordan_form() # very long + # m.jordan_form() # + + # diagonalizable, complex only + + # Jordan cells + # complexity: one of eigenvalues is zero + m = Matrix(3, 3, [0, 1, 0, -4, 4, 0, -2, 1, 2]) + # The blocks are ordered according to the value of their eigenvalues, + # in order to make the matrix compatible with .diagonalize() + Jmust = Matrix(3, 3, [2, 1, 0, 0, 2, 0, 0, 0, 2]) + P, J = m.jordan_form() + assert Jmust == J + + # complexity: all of eigenvalues are equal + m = Matrix(3, 3, [2, 6, -15, 1, 1, -5, 1, 2, -6]) + # Jmust = Matrix(3, 3, [-1, 0, 0, 0, -1, 1, 0, 0, -1]) + # same here see 1456ff + Jmust = Matrix(3, 3, [-1, 1, 0, 0, -1, 0, 0, 0, -1]) + P, J = m.jordan_form() + assert Jmust == J + + # complexity: two of eigenvalues are zero + m = Matrix(3, 3, [4, -5, 2, 5, -7, 3, 6, -9, 4]) + Jmust = Matrix(3, 3, [0, 1, 0, 0, 0, 0, 0, 0, 1]) + P, J = m.jordan_form() + assert Jmust == J + + m = Matrix(4, 4, [6, 5, -2, -3, -3, -1, 3, 3, 2, 1, -2, -3, -1, 1, 5, 5]) + Jmust = Matrix(4, 4, [2, 1, 0, 0, + 0, 2, 0, 0, + 0, 0, 2, 1, + 0, 0, 0, 2] + ) + P, J = m.jordan_form() + assert Jmust == J + + m = Matrix(4, 4, [6, 2, -8, -6, -3, 2, 9, 6, 2, -2, -8, -6, -1, 0, 3, 4]) + # Jmust = Matrix(4, 4, [2, 0, 0, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 0, -2]) + # same here see 1456ff + Jmust = Matrix(4, 4, [-2, 0, 0, 0, + 0, 2, 1, 0, + 0, 0, 2, 0, + 0, 0, 0, 2]) + P, J = m.jordan_form() + assert Jmust == J + + m = Matrix(4, 4, [5, 4, 2, 1, 0, 1, -1, -1, -1, -1, 3, 0, 1, 1, -1, 2]) + assert not m.is_diagonalizable() + Jmust = Matrix(4, 4, [1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 4, 1, 0, 0, 0, 4]) + P, J = m.jordan_form() + assert Jmust == J + + # checking for maximum precision to remain unchanged + m = Matrix([[Float('1.0', precision=110), Float('2.0', precision=110)], + [Float('3.14159265358979323846264338327', precision=110), Float('4.0', precision=110)]]) + P, J = m.jordan_form() + for term in J.values(): + if isinstance(term, Float): + assert term._prec == 110 + + +def test_jordan_form_complex_issue_9274(): + A = Matrix([[ 2, 4, 1, 0], + [-4, 2, 0, 1], + [ 0, 0, 2, 4], + [ 0, 0, -4, 2]]) + p = 2 - 4*I + q = 2 + 4*I + Jmust1 = Matrix([[p, 1, 0, 0], + [0, p, 0, 0], + [0, 0, q, 1], + [0, 0, 0, q]]) + Jmust2 = Matrix([[q, 1, 0, 0], + [0, q, 0, 0], + [0, 0, p, 1], + [0, 0, 0, p]]) + P, J = A.jordan_form() + assert J == Jmust1 or J == Jmust2 + assert simplify(P*J*P.inv()) == A + + +def test_issue_10220(): + # two non-orthogonal Jordan blocks with eigenvalue 1 + M = Matrix([[1, 0, 0, 1], + [0, 1, 1, 0], + [0, 0, 1, 1], + [0, 0, 0, 1]]) + P, J = M.jordan_form() + assert P == Matrix([[0, 1, 0, 1], + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0]]) + assert J == Matrix([ + [1, 1, 0, 0], + [0, 1, 1, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]]) + + +def test_jordan_form_issue_15858(): + A = Matrix([ + [1, 1, 1, 0], + [-2, -1, 0, -1], + [0, 0, -1, -1], + [0, 0, 2, 1]]) + (P, J) = A.jordan_form() + assert P.expand() == Matrix([ + [ -I, -I/2, I, I/2], + [-1 + I, 0, -1 - I, 0], + [ 0, -S(1)/2 - I/2, 0, -S(1)/2 + I/2], + [ 0, 1, 0, 1]]) + assert J == Matrix([ + [-I, 1, 0, 0], + [0, -I, 0, 0], + [0, 0, I, 1], + [0, 0, 0, I]]) + + +def test_Matrix_berkowitz_charpoly(): + UA, K_i, K_w = symbols('UA K_i K_w') + + A = Matrix([[-K_i - UA + K_i**2/(K_i + K_w), K_i*K_w/(K_i + K_w)], + [ K_i*K_w/(K_i + K_w), -K_w + K_w**2/(K_i + K_w)]]) + + charpoly = A.charpoly(x) + + assert charpoly == \ + Poly(x**2 + (K_i*UA + K_w*UA + 2*K_i*K_w)/(K_i + K_w)*x + + K_i*K_w*UA/(K_i + K_w), x, domain='ZZ(K_i,K_w,UA)') + + assert type(charpoly) is PurePoly + + A = Matrix([[1, 3], [2, 0]]) + assert A.charpoly() == A.charpoly(x) == PurePoly(x**2 - x - 6) + + A = Matrix([[1, 2], [x, 0]]) + p = A.charpoly(x) + assert p.gen != x + assert p.as_expr().subs(p.gen, x) == x**2 - 3*x + + +def test_exp_jordan_block(): + l = Symbol('lamda') + + m = Matrix.jordan_block(1, l) + assert m._eval_matrix_exp_jblock() == Matrix([[exp(l)]]) + + m = Matrix.jordan_block(3, l) + assert m._eval_matrix_exp_jblock() == \ + Matrix([ + [exp(l), exp(l), exp(l)/2], + [0, exp(l), exp(l)], + [0, 0, exp(l)]]) + + +def test_exp(): + m = Matrix([[3, 4], [0, -2]]) + m_exp = Matrix([[exp(3), -4*exp(-2)/5 + 4*exp(3)/5], [0, exp(-2)]]) + assert m.exp() == m_exp + assert exp(m) == m_exp + + m = Matrix([[1, 0], [0, 1]]) + assert m.exp() == Matrix([[E, 0], [0, E]]) + assert exp(m) == Matrix([[E, 0], [0, E]]) + + m = Matrix([[1, -1], [1, 1]]) + assert m.exp() == Matrix([[E*cos(1), -E*sin(1)], [E*sin(1), E*cos(1)]]) + + +def test_log(): + l = Symbol('lamda') + + m = Matrix.jordan_block(1, l) + assert m._eval_matrix_log_jblock() == Matrix([[log(l)]]) + + m = Matrix.jordan_block(4, l) + assert m._eval_matrix_log_jblock() == \ + Matrix( + [ + [log(l), 1/l, -1/(2*l**2), 1/(3*l**3)], + [0, log(l), 1/l, -1/(2*l**2)], + [0, 0, log(l), 1/l], + [0, 0, 0, log(l)] + ] + ) + + m = Matrix( + [[0, 0, 1], + [0, 0, 0], + [-1, 0, 0]] + ) + raises(MatrixError, lambda: m.log()) + + +def test_find_reasonable_pivot_naive_finds_guaranteed_nonzero1(): + # Test if matrices._find_reasonable_pivot_naive() + # finds a guaranteed non-zero pivot when the + # some of the candidate pivots are symbolic expressions. + # Keyword argument: simpfunc=None indicates that no simplifications + # should be performed during the search. + x = Symbol('x') + column = Matrix(3, 1, [x, cos(x)**2 + sin(x)**2, S.Half]) + pivot_offset, pivot_val, pivot_assumed_nonzero, simplified =\ + _find_reasonable_pivot_naive(column) + assert pivot_val == S.Half + + +def test_find_reasonable_pivot_naive_finds_guaranteed_nonzero2(): + # Test if matrices._find_reasonable_pivot_naive() + # finds a guaranteed non-zero pivot when the + # some of the candidate pivots are symbolic expressions. + # Keyword argument: simpfunc=_simplify indicates that the search + # should attempt to simplify candidate pivots. + x = Symbol('x') + column = Matrix(3, 1, + [x, + cos(x)**2+sin(x)**2+x**2, + cos(x)**2+sin(x)**2]) + pivot_offset, pivot_val, pivot_assumed_nonzero, simplified =\ + _find_reasonable_pivot_naive(column, simpfunc=_simplify) + assert pivot_val == 1 + + +def test_find_reasonable_pivot_naive_simplifies(): + # Test if matrices._find_reasonable_pivot_naive() + # simplifies candidate pivots, and reports + # their offsets correctly. + x = Symbol('x') + column = Matrix(3, 1, + [x, + cos(x)**2+sin(x)**2+x, + cos(x)**2+sin(x)**2]) + pivot_offset, pivot_val, pivot_assumed_nonzero, simplified =\ + _find_reasonable_pivot_naive(column, simpfunc=_simplify) + + assert len(simplified) == 2 + assert simplified[0][0] == 1 + assert simplified[0][1] == 1+x + assert simplified[1][0] == 2 + assert simplified[1][1] == 1 + + +def test_errors(): + raises(ValueError, lambda: Matrix([[1, 2], [1]])) + raises(IndexError, lambda: Matrix([[1, 2]])[1.2, 5]) + raises(IndexError, lambda: Matrix([[1, 2]])[1, 5.2]) + raises(ValueError, lambda: randMatrix(3, c=4, symmetric=True)) + raises(ValueError, lambda: Matrix([1, 2]).reshape(4, 6)) + raises(ShapeError, + lambda: Matrix([[1, 2], [3, 4]]).copyin_matrix([1, 0], Matrix([1, 2]))) + raises(TypeError, lambda: Matrix([[1, 2], [3, 4]]).copyin_list([0, + 1], set())) + raises(NonSquareMatrixError, lambda: Matrix([[1, 2, 3], [2, 3, 0]]).inv()) + raises(ShapeError, + lambda: Matrix(1, 2, [1, 2]).row_join(Matrix([[1, 2], [3, 4]]))) + raises( + ShapeError, lambda: Matrix([1, 2]).col_join(Matrix([[1, 2], [3, 4]]))) + raises(ShapeError, lambda: Matrix([1]).row_insert(1, Matrix([[1, + 2], [3, 4]]))) + raises(ShapeError, lambda: Matrix([1]).col_insert(1, Matrix([[1, + 2], [3, 4]]))) + raises(NonSquareMatrixError, lambda: Matrix([1, 2]).trace()) + raises(TypeError, lambda: Matrix([1]).applyfunc(1)) + raises(ValueError, lambda: Matrix([[1, 2], [3, 4]]).minor(4, 5)) + raises(ValueError, lambda: Matrix([[1, 2], [3, 4]]).minor_submatrix(4, 5)) + raises(TypeError, lambda: Matrix([1, 2, 3]).cross(1)) + raises(TypeError, lambda: Matrix([1, 2, 3]).dot(1)) + raises(ShapeError, lambda: Matrix([1, 2, 3]).dot(Matrix([1, 2]))) + raises(ShapeError, lambda: Matrix([1, 2]).dot([])) + raises(TypeError, lambda: Matrix([1, 2]).dot('a')) + raises(ShapeError, lambda: Matrix([1, 2]).dot([1, 2, 3])) + raises(NonSquareMatrixError, lambda: Matrix([1, 2, 3]).exp()) + raises(ShapeError, lambda: Matrix([[1, 2], [3, 4]]).normalized()) + raises(ValueError, lambda: Matrix([1, 2]).inv(method='not a method')) + raises(NonSquareMatrixError, lambda: Matrix([1, 2]).inverse_GE()) + raises(ValueError, lambda: Matrix([[1, 2], [1, 2]]).inverse_GE()) + raises(NonSquareMatrixError, lambda: Matrix([1, 2]).inverse_ADJ()) + raises(ValueError, lambda: Matrix([[1, 2], [1, 2]]).inverse_ADJ()) + raises(NonSquareMatrixError, lambda: Matrix([1, 2]).inverse_LU()) + raises(NonSquareMatrixError, lambda: Matrix([1, 2]).is_nilpotent()) + raises(NonSquareMatrixError, lambda: Matrix([1, 2]).det()) + raises(ValueError, + lambda: Matrix([[1, 2], [3, 4]]).det(method='Not a real method')) + raises(ValueError, + lambda: Matrix([[1, 2, 3, 4], [5, 6, 7, 8], + [9, 10, 11, 12], [13, 14, 15, 16]]).det(iszerofunc="Not function")) + raises(ValueError, + lambda: Matrix([[1, 2, 3, 4], [5, 6, 7, 8], + [9, 10, 11, 12], [13, 14, 15, 16]]).det(iszerofunc=False)) + raises(ValueError, + lambda: hessian(Matrix([[1, 2], [3, 4]]), Matrix([[1, 2], [2, 1]]))) + raises(ValueError, lambda: hessian(Matrix([[1, 2], [3, 4]]), [])) + raises(ValueError, lambda: hessian(Symbol('x')**2, 'a')) + raises(IndexError, lambda: eye(3)[5, 2]) + raises(IndexError, lambda: eye(3)[2, 5]) + M = Matrix(((1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12), (13, 14, 15, 16))) + raises(ValueError, lambda: M.det('method=LU_decomposition()')) + V = Matrix([[10, 10, 10]]) + M = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + raises(ValueError, lambda: M.row_insert(4.7, V)) + M = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + raises(ValueError, lambda: M.col_insert(-4.2, V)) + + +def test_len(): + assert len(Matrix()) == 0 + assert len(Matrix([[1, 2]])) == len(Matrix([[1], [2]])) == 2 + assert len(Matrix(0, 2, lambda i, j: 0)) == \ + len(Matrix(2, 0, lambda i, j: 0)) == 0 + assert len(Matrix([[0, 1, 2], [3, 4, 5]])) == 6 + assert Matrix([1]) == Matrix([[1]]) + assert not Matrix() + assert Matrix() == Matrix([]) + + +def test_integrate(): + A = Matrix(((1, 4, x), (y, 2, 4), (10, 5, x**2))) + assert A.integrate(x) == \ + Matrix(((x, 4*x, x**2/2), (x*y, 2*x, 4*x), (10*x, 5*x, x**3/3))) + assert A.integrate(y) == \ + Matrix(((y, 4*y, x*y), (y**2/2, 2*y, 4*y), (10*y, 5*y, y*x**2))) + m = Matrix(2, 1, [x, y]) + assert m.integrate(x) == Matrix(2, 1, [x**2/2, y*x]) + + +def test_diff(): + A = MutableDenseMatrix(((1, 4, x), (y, 2, 4), (10, 5, x**2 + 1))) + assert isinstance(A.diff(x), type(A)) + assert A.diff(x) == MutableDenseMatrix(((0, 0, 1), (0, 0, 0), (0, 0, 2*x))) + assert A.diff(y) == MutableDenseMatrix(((0, 0, 0), (1, 0, 0), (0, 0, 0))) + + assert diff(A, x) == MutableDenseMatrix(((0, 0, 1), (0, 0, 0), (0, 0, 2*x))) + assert diff(A, y) == MutableDenseMatrix(((0, 0, 0), (1, 0, 0), (0, 0, 0))) + + A_imm = A.as_immutable() + assert isinstance(A_imm.diff(x), type(A_imm)) + assert A_imm.diff(x) == ImmutableDenseMatrix(((0, 0, 1), (0, 0, 0), (0, 0, 2*x))) + assert A_imm.diff(y) == ImmutableDenseMatrix(((0, 0, 0), (1, 0, 0), (0, 0, 0))) + + assert diff(A_imm, x) == ImmutableDenseMatrix(((0, 0, 1), (0, 0, 0), (0, 0, 2*x))) + assert diff(A_imm, y) == ImmutableDenseMatrix(((0, 0, 0), (1, 0, 0), (0, 0, 0))) + + assert A.diff(x, evaluate=False) == ArrayDerivative(A, x, evaluate=False) + assert diff(A, x, evaluate=False) == ArrayDerivative(A, x, evaluate=False) + + +def test_diff_by_matrix(): + + # Derive matrix by matrix: + + A = MutableDenseMatrix([[x, y], [z, t]]) + assert A.diff(A) == Array([[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, 1]]]]) + assert diff(A, A) == Array([[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, 1]]]]) + + A_imm = A.as_immutable() + assert A_imm.diff(A_imm) == Array([[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, 1]]]]) + assert diff(A_imm, A_imm) == Array([[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, 1]]]]) + + # Derive a constant matrix: + assert A.diff(a) == MutableDenseMatrix([[0, 0], [0, 0]]) + + B = ImmutableDenseMatrix([a, b]) + assert A.diff(B) == Array.zeros(2, 1, 2, 2) + assert A.diff(A) == Array([[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, 1]]]]) + + # Test diff with tuples: + + dB = B.diff([[a, b]]) + assert dB.shape == (2, 2, 1) + assert dB == Array([[[1], [0]], [[0], [1]]]) + + f = Function("f") + fxyz = f(x, y, z) + assert fxyz.diff([[x, y, z]]) == Array([fxyz.diff(x), fxyz.diff(y), fxyz.diff(z)]) + assert fxyz.diff(([x, y, z], 2)) == Array([ + [fxyz.diff(x, 2), fxyz.diff(x, y), fxyz.diff(x, z)], + [fxyz.diff(x, y), fxyz.diff(y, 2), fxyz.diff(y, z)], + [fxyz.diff(x, z), fxyz.diff(z, y), fxyz.diff(z, 2)], + ]) + + expr = sin(x)*exp(y) + assert expr.diff([[x, y]]) == Array([cos(x)*exp(y), sin(x)*exp(y)]) + assert expr.diff(y, ((x, y),)) == Array([cos(x)*exp(y), sin(x)*exp(y)]) + assert expr.diff(x, ((x, y),)) == Array([-sin(x)*exp(y), cos(x)*exp(y)]) + assert expr.diff(((y, x),), [[x, y]]) == Array([[cos(x)*exp(y), -sin(x)*exp(y)], [sin(x)*exp(y), cos(x)*exp(y)]]) + + # Test different notations: + + assert fxyz.diff(x).diff(y).diff(x) == fxyz.diff(((x, y, z),), 3)[0, 1, 0] + assert fxyz.diff(z).diff(y).diff(x) == fxyz.diff(((x, y, z),), 3)[2, 1, 0] + assert fxyz.diff([[x, y, z]], ((z, y, x),)) == Array([[fxyz.diff(i).diff(j) for i in (x, y, z)] for j in (z, y, x)]) + + # Test scalar derived by matrix remains matrix: + res = x.diff(Matrix([[x, y]])) + assert isinstance(res, ImmutableDenseMatrix) + assert res == Matrix([[1, 0]]) + res = (x**3).diff(Matrix([[x, y]])) + assert isinstance(res, ImmutableDenseMatrix) + assert res == Matrix([[3*x**2, 0]]) + + +def test_getattr(): + A = Matrix(((1, 4, x), (y, 2, 4), (10, 5, x**2 + 1))) + raises(AttributeError, lambda: A.nonexistantattribute) + assert getattr(A, 'diff')(x) == Matrix(((0, 0, 1), (0, 0, 0), (0, 0, 2*x))) + + +def test_hessenberg(): + A = Matrix([[3, 4, 1], [2, 4, 5], [0, 1, 2]]) + assert A.is_upper_hessenberg + A = A.T + assert A.is_lower_hessenberg + A[0, -1] = 1 + assert A.is_lower_hessenberg is False + + A = Matrix([[3, 4, 1], [2, 4, 5], [3, 1, 2]]) + assert not A.is_upper_hessenberg + + A = zeros(5, 2) + assert A.is_upper_hessenberg + + +def test_cholesky(): + raises(NonSquareMatrixError, lambda: Matrix((1, 2)).cholesky()) + raises(ValueError, lambda: Matrix(((1, 2), (3, 4))).cholesky()) + raises(ValueError, lambda: Matrix(((5 + I, 0), (0, 1))).cholesky()) + raises(ValueError, lambda: Matrix(((1, 5), (5, 1))).cholesky()) + raises(ValueError, lambda: Matrix(((1, 2), (3, 4))).cholesky(hermitian=False)) + assert Matrix(((5 + I, 0), (0, 1))).cholesky(hermitian=False) == Matrix([ + [sqrt(5 + I), 0], [0, 1]]) + A = Matrix(((1, 5), (5, 1))) + L = A.cholesky(hermitian=False) + assert L == Matrix([[1, 0], [5, 2*sqrt(6)*I]]) + assert L*L.T == A + A = Matrix(((25, 15, -5), (15, 18, 0), (-5, 0, 11))) + L = A.cholesky() + assert L * L.T == A + assert L.is_lower + assert L == Matrix([[5, 0, 0], [3, 3, 0], [-1, 1, 3]]) + A = Matrix(((4, -2*I, 2 + 2*I), (2*I, 2, -1 + I), (2 - 2*I, -1 - I, 11))) + assert A.cholesky().expand() == Matrix(((2, 0, 0), (I, 1, 0), (1 - I, 0, 3))) + + raises(NonSquareMatrixError, lambda: SparseMatrix((1, 2)).cholesky()) + raises(ValueError, lambda: SparseMatrix(((1, 2), (3, 4))).cholesky()) + raises(ValueError, lambda: SparseMatrix(((5 + I, 0), (0, 1))).cholesky()) + raises(ValueError, lambda: SparseMatrix(((1, 5), (5, 1))).cholesky()) + raises(ValueError, lambda: SparseMatrix(((1, 2), (3, 4))).cholesky(hermitian=False)) + assert SparseMatrix(((5 + I, 0), (0, 1))).cholesky(hermitian=False) == Matrix([ + [sqrt(5 + I), 0], [0, 1]]) + A = SparseMatrix(((1, 5), (5, 1))) + L = A.cholesky(hermitian=False) + assert L == Matrix([[1, 0], [5, 2*sqrt(6)*I]]) + assert L*L.T == A + A = SparseMatrix(((25, 15, -5), (15, 18, 0), (-5, 0, 11))) + L = A.cholesky() + assert L * L.T == A + assert L.is_lower + assert L == Matrix([[5, 0, 0], [3, 3, 0], [-1, 1, 3]]) + A = SparseMatrix(((4, -2*I, 2 + 2*I), (2*I, 2, -1 + I), (2 - 2*I, -1 - I, 11))) + assert A.cholesky() == Matrix(((2, 0, 0), (I, 1, 0), (1 - I, 0, 3))) + + +def test_matrix_norm(): + # Vector Tests + # Test columns and symbols + x = Symbol('x', real=True) + v = Matrix([cos(x), sin(x)]) + assert trigsimp(v.norm(2)) == 1 + assert v.norm(10) == Pow(cos(x)**10 + sin(x)**10, Rational(1, 10)) + + # Test Rows + A = Matrix([[5, Rational(3, 2)]]) + assert A.norm() == Pow(25 + Rational(9, 4), S.Half) + assert A.norm(oo) == max(A) + assert A.norm(-oo) == min(A) + + # Matrix Tests + # Intuitive test + A = Matrix([[1, 1], [1, 1]]) + assert A.norm(2) == 2 + assert A.norm(-2) == 0 + assert A.norm('frobenius') == 2 + assert eye(10).norm(2) == eye(10).norm(-2) == 1 + assert A.norm(oo) == 2 + + # Test with Symbols and more complex entries + A = Matrix([[3, y, y], [x, S.Half, -pi]]) + assert (A.norm('fro') + == sqrt(Rational(37, 4) + 2*abs(y)**2 + pi**2 + x**2)) + + # Check non-square + A = Matrix([[1, 2, -3], [4, 5, Rational(13, 2)]]) + assert A.norm(2) == sqrt(Rational(389, 8) + sqrt(78665)/8) + assert A.norm(-2) is S.Zero + assert A.norm('frobenius') == sqrt(389)/2 + + # Test properties of matrix norms + # https://en.wikipedia.org/wiki/Matrix_norm#Definition + # Two matrices + A = Matrix([[1, 2], [3, 4]]) + B = Matrix([[5, 5], [-2, 2]]) + C = Matrix([[0, -I], [I, 0]]) + D = Matrix([[1, 0], [0, -1]]) + L = [A, B, C, D] + alpha = Symbol('alpha', real=True) + + for order in ['fro', 2, -2]: + # Zero Check + assert zeros(3).norm(order) is S.Zero + # Check Triangle Inequality for all Pairs of Matrices + for X in L: + for Y in L: + dif = (X.norm(order) + Y.norm(order) - + (X + Y).norm(order)) + assert (dif >= 0) + # Scalar multiplication linearity + for M in [A, B, C, D]: + dif = simplify((alpha*M).norm(order) - + abs(alpha) * M.norm(order)) + assert dif == 0 + + # Test Properties of Vector Norms + # https://en.wikipedia.org/wiki/Vector_norm + # Two column vectors + a = Matrix([1, 1 - 1*I, -3]) + b = Matrix([S.Half, 1*I, 1]) + c = Matrix([-1, -1, -1]) + d = Matrix([3, 2, I]) + e = Matrix([Integer(1e2), Rational(1, 1e2), 1]) + L = [a, b, c, d, e] + alpha = Symbol('alpha', real=True) + + for order in [1, 2, -1, -2, S.Infinity, S.NegativeInfinity, pi]: + # Zero Check + if order > 0: + assert Matrix([0, 0, 0]).norm(order) is S.Zero + # Triangle inequality on all pairs + if order >= 1: # Triangle InEq holds only for these norms + for X in L: + for Y in L: + dif = (X.norm(order) + Y.norm(order) - + (X + Y).norm(order)) + assert simplify(dif >= 0) is S.true + # Linear to scalar multiplication + if order in [1, 2, -1, -2, S.Infinity, S.NegativeInfinity]: + for X in L: + dif = simplify((alpha*X).norm(order) - + (abs(alpha) * X.norm(order))) + assert dif == 0 + + # ord=1 + M = Matrix(3, 3, [1, 3, 0, -2, -1, 0, 3, 9, 6]) + assert M.norm(1) == 13 + + +def test_condition_number(): + x = Symbol('x', real=True) + A = eye(3) + A[0, 0] = 10 + A[2, 2] = Rational(1, 10) + assert A.condition_number() == 100 + + A[1, 1] = x + assert A.condition_number() == Max(10, Abs(x)) / Min(Rational(1, 10), Abs(x)) + + M = Matrix([[cos(x), sin(x)], [-sin(x), cos(x)]]) + Mc = M.condition_number() + assert all(Float(1.).epsilon_eq(Mc.subs(x, val).evalf()) for val in + [Rational(1, 5), S.Half, Rational(1, 10), pi/2, pi, pi*Rational(7, 4) ]) + + #issue 10782 + assert Matrix([]).condition_number() == 0 + + +def test_equality(): + A = Matrix(((1, 2, 3), (4, 5, 6), (7, 8, 9))) + B = Matrix(((9, 8, 7), (6, 5, 4), (3, 2, 1))) + assert A == A[:, :] + assert not A != A[:, :] + assert not A == B + assert A != B + assert A != 10 + assert not A == 10 + + # A SparseMatrix can be equal to a Matrix + C = SparseMatrix(((1, 0, 0), (0, 1, 0), (0, 0, 1))) + D = Matrix(((1, 0, 0), (0, 1, 0), (0, 0, 1))) + assert C == D + assert not C != D + + +def test_normalized(): + assert Matrix([3, 4]).normalized() == \ + Matrix([Rational(3, 5), Rational(4, 5)]) + + # Zero vector trivial cases + assert Matrix([0, 0, 0]).normalized() == Matrix([0, 0, 0]) + + # Machine precision error truncation trivial cases + m = Matrix([0,0,1.e-100]) + assert m.normalized( + iszerofunc=lambda x: x.evalf(n=10, chop=True).is_zero + ) == Matrix([0, 0, 0]) + + +def test_print_nonzero(): + assert capture(lambda: eye(3).print_nonzero()) == \ + '[X ]\n[ X ]\n[ X]\n' + assert capture(lambda: eye(3).print_nonzero('.')) == \ + '[. ]\n[ . ]\n[ .]\n' + + +def test_zeros_eye(): + assert Matrix.eye(3) == eye(3) + assert Matrix.zeros(3) == zeros(3) + assert ones(3, 4) == Matrix(3, 4, [1]*12) + + i = Matrix([[1, 0], [0, 1]]) + z = Matrix([[0, 0], [0, 0]]) + for cls in all_classes: + m = cls.eye(2) + assert i == m # but m == i will fail if m is immutable + assert i == eye(2, cls=cls) + assert type(m) == cls + m = cls.zeros(2) + assert z == m + assert z == zeros(2, cls=cls) + assert type(m) == cls + + +def test_is_zero(): + assert Matrix().is_zero_matrix + assert Matrix([[0, 0], [0, 0]]).is_zero_matrix + assert zeros(3, 4).is_zero_matrix + assert not eye(3).is_zero_matrix + assert Matrix([[x, 0], [0, 0]]).is_zero_matrix == None + assert SparseMatrix([[x, 0], [0, 0]]).is_zero_matrix == None + assert ImmutableMatrix([[x, 0], [0, 0]]).is_zero_matrix == None + assert ImmutableSparseMatrix([[x, 0], [0, 0]]).is_zero_matrix == None + assert Matrix([[x, 1], [0, 0]]).is_zero_matrix == False + a = Symbol('a', nonzero=True) + assert Matrix([[a, 0], [0, 0]]).is_zero_matrix == False + + +def test_rotation_matrices(): + # This tests the rotation matrices by rotating about an axis and back. + theta = pi/3 + r3_plus = rot_axis3(theta) + r3_minus = rot_axis3(-theta) + r2_plus = rot_axis2(theta) + r2_minus = rot_axis2(-theta) + r1_plus = rot_axis1(theta) + r1_minus = rot_axis1(-theta) + assert r3_minus*r3_plus*eye(3) == eye(3) + assert r2_minus*r2_plus*eye(3) == eye(3) + assert r1_minus*r1_plus*eye(3) == eye(3) + + # Check the correctness of the trace of the rotation matrix + assert r1_plus.trace() == 1 + 2*cos(theta) + assert r2_plus.trace() == 1 + 2*cos(theta) + assert r3_plus.trace() == 1 + 2*cos(theta) + + # Check that a rotation with zero angle doesn't change anything. + assert rot_axis1(0) == eye(3) + assert rot_axis2(0) == eye(3) + assert rot_axis3(0) == eye(3) + + # Check left-hand convention + # see Issue #24529 + q1 = Quaternion.from_axis_angle([1, 0, 0], pi / 2) + q2 = Quaternion.from_axis_angle([0, 1, 0], pi / 2) + q3 = Quaternion.from_axis_angle([0, 0, 1], pi / 2) + assert rot_axis1(- pi / 2) == q1.to_rotation_matrix() + assert rot_axis2(- pi / 2) == q2.to_rotation_matrix() + assert rot_axis3(- pi / 2) == q3.to_rotation_matrix() + # Check right-hand convention + assert rot_ccw_axis1(+ pi / 2) == q1.to_rotation_matrix() + assert rot_ccw_axis2(+ pi / 2) == q2.to_rotation_matrix() + assert rot_ccw_axis3(+ pi / 2) == q3.to_rotation_matrix() + + +def test_DeferredVector(): + assert str(DeferredVector("vector")[4]) == "vector[4]" + assert sympify(DeferredVector("d")) == DeferredVector("d") + raises(IndexError, lambda: DeferredVector("d")[-1]) + assert str(DeferredVector("d")) == "d" + assert repr(DeferredVector("test")) == "DeferredVector('test')" + + +def test_DeferredVector_not_iterable(): + assert not iterable(DeferredVector('X')) + + +def test_DeferredVector_Matrix(): + raises(TypeError, lambda: Matrix(DeferredVector("V"))) + + +def test_GramSchmidt(): + R = Rational + m1 = Matrix(1, 2, [1, 2]) + m2 = Matrix(1, 2, [2, 3]) + assert GramSchmidt([m1, m2]) == \ + [Matrix(1, 2, [1, 2]), Matrix(1, 2, [R(2)/5, R(-1)/5])] + assert GramSchmidt([m1.T, m2.T]) == \ + [Matrix(2, 1, [1, 2]), Matrix(2, 1, [R(2)/5, R(-1)/5])] + # from wikipedia + assert GramSchmidt([Matrix([3, 1]), Matrix([2, 2])], True) == [ + Matrix([3*sqrt(10)/10, sqrt(10)/10]), + Matrix([-sqrt(10)/10, 3*sqrt(10)/10])] + # https://github.com/sympy/sympy/issues/9488 + L = FiniteSet(Matrix([1])) + assert GramSchmidt(L) == [Matrix([[1]])] + + +def test_casoratian(): + assert casoratian([1, 2, 3, 4], 1) == 0 + assert casoratian([1, 2, 3, 4], 1, zero=False) == 0 + + +def test_zero_dimension_multiply(): + assert (Matrix()*zeros(0, 3)).shape == (0, 3) + assert zeros(3, 0)*zeros(0, 3) == zeros(3, 3) + assert zeros(0, 3)*zeros(3, 0) == Matrix() + + +def test_slice_issue_2884(): + m = Matrix(2, 2, range(4)) + assert m[1, :] == Matrix([[2, 3]]) + assert m[-1, :] == Matrix([[2, 3]]) + assert m[:, 1] == Matrix([[1, 3]]).T + assert m[:, -1] == Matrix([[1, 3]]).T + raises(IndexError, lambda: m[2, :]) + raises(IndexError, lambda: m[2, 2]) + + +def test_slice_issue_3401(): + assert zeros(0, 3)[:, -1].shape == (0, 1) + assert zeros(3, 0)[0, :] == Matrix(1, 0, []) + + +def test_copyin(): + s = zeros(3, 3) + s[3] = 1 + assert s[:, 0] == Matrix([0, 1, 0]) + assert s[3] == 1 + assert s[3: 4] == [1] + s[1, 1] = 42 + assert s[1, 1] == 42 + assert s[1, 1:] == Matrix([[42, 0]]) + s[1, 1:] = Matrix([[5, 6]]) + assert s[1, :] == Matrix([[1, 5, 6]]) + s[1, 1:] = [[42, 43]] + assert s[1, :] == Matrix([[1, 42, 43]]) + s[0, 0] = 17 + assert s[:, :1] == Matrix([17, 1, 0]) + s[0, 0] = [1, 1, 1] + assert s[:, 0] == Matrix([1, 1, 1]) + s[0, 0] = Matrix([1, 1, 1]) + assert s[:, 0] == Matrix([1, 1, 1]) + s[0, 0] = SparseMatrix([1, 1, 1]) + assert s[:, 0] == Matrix([1, 1, 1]) + + +def test_invertible_check(): + # sometimes a singular matrix will have a pivot vector shorter than + # the number of rows in a matrix... + assert Matrix([[1, 2], [1, 2]]).rref() == (Matrix([[1, 2], [0, 0]]), (0,)) + raises(ValueError, lambda: Matrix([[1, 2], [1, 2]]).inv()) + m = Matrix([ + [-1, -1, 0], + [ x, 1, 1], + [ 1, x, -1], + ]) + assert len(m.rref()[1]) != m.rows + # in addition, unless simplify=True in the call to rref, the identity + # matrix will be returned even though m is not invertible + assert m.rref()[0] != eye(3) + assert m.rref(simplify=signsimp)[0] != eye(3) + raises(ValueError, lambda: m.inv(method="ADJ")) + raises(ValueError, lambda: m.inv(method="GE")) + raises(ValueError, lambda: m.inv(method="LU")) + + +def test_issue_3959(): + x, y = symbols('x, y') + e = x*y + assert e.subs(x, Matrix([3, 5, 3])) == Matrix([3, 5, 3])*y + + +def test_issue_5964(): + assert str(Matrix([[1, 2], [3, 4]])) == 'Matrix([[1, 2], [3, 4]])' + + +def test_issue_7604(): + x, y = symbols("x y") + assert sstr(Matrix([[x, 2*y], [y**2, x + 3]])) == \ + 'Matrix([\n[ x, 2*y],\n[y**2, x + 3]])' + + +def test_is_Identity(): + assert eye(3).is_Identity + assert eye(3).as_immutable().is_Identity + assert not zeros(3).is_Identity + assert not ones(3).is_Identity + # issue 6242 + assert not Matrix([[1, 0, 0]]).is_Identity + # issue 8854 + assert SparseMatrix(3,3, {(0,0):1, (1,1):1, (2,2):1}).is_Identity + assert not SparseMatrix(2,3, range(6)).is_Identity + assert not SparseMatrix(3,3, {(0,0):1, (1,1):1}).is_Identity + assert not SparseMatrix(3,3, {(0,0):1, (1,1):1, (2,2):1, (0,1):2, (0,2):3}).is_Identity + + +def test_dot(): + assert ones(1, 3).dot(ones(3, 1)) == 3 + assert ones(1, 3).dot([1, 1, 1]) == 3 + assert Matrix([1, 2, 3]).dot(Matrix([1, 2, 3])) == 14 + assert Matrix([1, 2, 3*I]).dot(Matrix([I, 2, 3*I])) == -5 + I + assert Matrix([1, 2, 3*I]).dot(Matrix([I, 2, 3*I]), hermitian=False) == -5 + I + assert Matrix([1, 2, 3*I]).dot(Matrix([I, 2, 3*I]), hermitian=True) == 13 + I + assert Matrix([1, 2, 3*I]).dot(Matrix([I, 2, 3*I]), hermitian=True, conjugate_convention="physics") == 13 - I + assert Matrix([1, 2, 3*I]).dot(Matrix([4, 5*I, 6]), hermitian=True, conjugate_convention="right") == 4 + 8*I + assert Matrix([1, 2, 3*I]).dot(Matrix([4, 5*I, 6]), hermitian=True, conjugate_convention="left") == 4 - 8*I + assert Matrix([I, 2*I]).dot(Matrix([I, 2*I]), hermitian=False, conjugate_convention="left") == -5 + assert Matrix([I, 2*I]).dot(Matrix([I, 2*I]), conjugate_convention="left") == 5 + raises(ValueError, lambda: Matrix([1, 2]).dot(Matrix([3, 4]), hermitian=True, conjugate_convention="test")) + + +def test_dual(): + B_x, B_y, B_z, E_x, E_y, E_z = symbols( + 'B_x B_y B_z E_x E_y E_z', real=True) + F = Matrix(( + ( 0, E_x, E_y, E_z), + (-E_x, 0, B_z, -B_y), + (-E_y, -B_z, 0, B_x), + (-E_z, B_y, -B_x, 0) + )) + Fd = Matrix(( + ( 0, -B_x, -B_y, -B_z), + (B_x, 0, E_z, -E_y), + (B_y, -E_z, 0, E_x), + (B_z, E_y, -E_x, 0) + )) + assert F.dual().equals(Fd) + assert eye(3).dual().equals(zeros(3)) + assert F.dual().dual().equals(-F) + + +def test_anti_symmetric(): + assert Matrix([1, 2]).is_anti_symmetric() is False + m = Matrix(3, 3, [0, x**2 + 2*x + 1, y, -(x + 1)**2, 0, x*y, -y, -x*y, 0]) + assert m.is_anti_symmetric() is True + assert m.is_anti_symmetric(simplify=False) is None + assert m.is_anti_symmetric(simplify=lambda x: x) is None + + # tweak to fail + m[2, 1] = -m[2, 1] + assert m.is_anti_symmetric() is None + # untweak + m[2, 1] = -m[2, 1] + + m = m.expand() + assert m.is_anti_symmetric(simplify=False) is True + m[0, 0] = 1 + assert m.is_anti_symmetric() is False + + +def test_normalize_sort_diogonalization(): + A = Matrix(((1, 2), (2, 1))) + P, Q = A.diagonalize(normalize=True) + assert P*P.T == P.T*P == eye(P.cols) + P, Q = A.diagonalize(normalize=True, sort=True) + assert P*P.T == P.T*P == eye(P.cols) + assert P*Q*P.inv() == A + + +def test_issue_5321(): + raises(ValueError, lambda: Matrix([[1, 2, 3], Matrix(0, 1, [])])) + + +def test_issue_5320(): + assert Matrix.hstack(eye(2), 2*eye(2)) == Matrix([ + [1, 0, 2, 0], + [0, 1, 0, 2] + ]) + assert Matrix.vstack(eye(2), 2*eye(2)) == Matrix([ + [1, 0], + [0, 1], + [2, 0], + [0, 2] + ]) + cls = SparseMatrix + assert cls.hstack(cls(eye(2)), cls(2*eye(2))) == Matrix([ + [1, 0, 2, 0], + [0, 1, 0, 2] + ]) + + +def test_issue_11944(): + A = Matrix([[1]]) + AIm = sympify(A) + assert Matrix.hstack(AIm, A) == Matrix([[1, 1]]) + assert Matrix.vstack(AIm, A) == Matrix([[1], [1]]) + + +def test_cross(): + a = [1, 2, 3] + b = [3, 4, 5] + col = Matrix([-2, 4, -2]) + row = col.T + + def test(M, ans): + assert ans == M + assert type(M) == cls + for cls in all_classes: + A = cls(a) + B = cls(b) + test(A.cross(B), col) + test(A.cross(B.T), col) + test(A.T.cross(B.T), row) + test(A.T.cross(B), row) + raises(ShapeError, lambda: + Matrix(1, 2, [1, 1]).cross(Matrix(1, 2, [1, 1]))) + + +def test_hat_vee(): + v1 = Matrix([x, y, z]) + v2 = Matrix([a, b, c]) + assert v1.hat() * v2 == v1.cross(v2) + assert v1.hat().is_anti_symmetric() + assert v1.hat().vee() == v1 + + +def test_hash(): + for cls in immutable_classes: + s = {cls.eye(1), cls.eye(1)} + assert len(s) == 1 and s.pop() == cls.eye(1) + # issue 3979 + for cls in mutable_classes: + assert not isinstance(cls.eye(1), Hashable) + + +def test_adjoint(): + dat = [[0, I], [1, 0]] + ans = Matrix([[0, 1], [-I, 0]]) + for cls in all_classes: + assert ans == cls(dat).adjoint() + + +def test_atoms(): + m = Matrix([[1, 2], [x, 1 - 1/x]]) + assert m.atoms() == {S.One,S(2),S.NegativeOne, x} + assert m.atoms(Symbol) == {x} + + +def test_pinv(): + # Pseudoinverse of an invertible matrix is the inverse. + A1 = Matrix([[a, b], [c, d]]) + assert simplify(A1.pinv(method="RD")) == simplify(A1.inv()) + + # Test the four properties of the pseudoinverse for various matrices. + As = [Matrix([[13, 104], [2212, 3], [-3, 5]]), + Matrix([[1, 7, 9], [11, 17, 19]]), + Matrix([a, b])] + + for A in As: + A_pinv = A.pinv(method="RD") + AAp = A * A_pinv + ApA = A_pinv * A + assert simplify(AAp * A) == A + assert simplify(ApA * A_pinv) == A_pinv + assert AAp.H == AAp + assert ApA.H == ApA + + # XXX Pinv with diagonalization makes expression too complicated. + for A in As: + A_pinv = simplify(A.pinv(method="ED")) + AAp = A * A_pinv + ApA = A_pinv * A + assert simplify(AAp * A) == A + assert simplify(ApA * A_pinv) == A_pinv + assert AAp.H == AAp + assert ApA.H == ApA + + # XXX Computing pinv using diagonalization makes an expression that + # is too complicated to simplify. + # A1 = Matrix([[a, b], [c, d]]) + # assert simplify(A1.pinv(method="ED")) == simplify(A1.inv()) + # so this is tested numerically at a fixed random point + + from sympy.core.numbers import comp + q = A1.pinv(method="ED") + w = A1.inv() + reps = {a: -73633, b: 11362, c: 55486, d: 62570} + assert all( + comp(i.n(), j.n()) + for i, j in zip(q.subs(reps), w.subs(reps)) + ) + + +@slow +def test_pinv_rank_deficient_when_diagonalization_fails(): + # Test the four properties of the pseudoinverse for matrices when + # diagonalization of A.H*A fails. + As = [ + Matrix([ + [61, 89, 55, 20, 71, 0], + [62, 96, 85, 85, 16, 0], + [69, 56, 17, 4, 54, 0], + [10, 54, 91, 41, 71, 0], + [ 7, 30, 10, 48, 90, 0], + [0, 0, 0, 0, 0, 0]]) + ] + for A in As: + A_pinv = A.pinv(method="ED") + AAp = A * A_pinv + ApA = A_pinv * A + assert AAp.H == AAp + + # Here ApA.H and ApA are equivalent expressions but they are very + # complicated expressions involving RootOfs. Using simplify would be + # too slow and so would evalf so we substitute approximate values for + # the RootOfs and then evalf which is less accurate but good enough to + # confirm that these two matrices are equivalent. + # + # assert ApA.H == ApA # <--- would fail (structural equality) + # assert simplify(ApA.H - ApA).is_zero_matrix # <--- too slow + # (ApA.H - ApA).evalf() # <--- too slow + + def allclose(M1, M2): + rootofs = M1.atoms(RootOf) + rootofs_approx = {r: r.evalf() for r in rootofs} + diff_approx = (M1 - M2).xreplace(rootofs_approx).evalf() + return all(abs(e) < 1e-10 for e in diff_approx) + + assert allclose(ApA.H, ApA) + + +def test_issue_7201(): + assert ones(0, 1) + ones(0, 1) == Matrix(0, 1, []) + assert ones(1, 0) + ones(1, 0) == Matrix(1, 0, []) + + +def test_free_symbols(): + for M in ImmutableMatrix, ImmutableSparseMatrix, Matrix, SparseMatrix: + assert M([[x], [0]]).free_symbols == {x} + + +def test_from_ndarray(): + """See issue 7465.""" + try: + from numpy import array + except ImportError: + skip('NumPy must be available to test creating matrices from ndarrays') + + assert Matrix(array([1, 2, 3])) == Matrix([1, 2, 3]) + assert Matrix(array([[1, 2, 3]])) == Matrix([[1, 2, 3]]) + assert Matrix(array([[1, 2, 3], [4, 5, 6]])) == \ + Matrix([[1, 2, 3], [4, 5, 6]]) + assert Matrix(array([x, y, z])) == Matrix([x, y, z]) + raises(NotImplementedError, + lambda: Matrix(array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]))) + assert Matrix([array([1, 2]), array([3, 4])]) == Matrix([[1, 2], [3, 4]]) + assert Matrix([array([1, 2]), [3, 4]]) == Matrix([[1, 2], [3, 4]]) + assert Matrix([array([]), array([])]) == Matrix(2, 0, []) != Matrix([]) + + +def test_17522_numpy(): + from sympy.matrices.common import _matrixify + try: + from numpy import array, matrix + except ImportError: + skip('NumPy must be available to test indexing matrixified NumPy ndarrays and matrices') + + m = _matrixify(array([[1, 2], [3, 4]])) + assert m[3] == 4 + assert list(m) == [1, 2, 3, 4] + + with ignore_warnings(PendingDeprecationWarning): + m = _matrixify(matrix([[1, 2], [3, 4]])) + assert m[3] == 4 + assert list(m) == [1, 2, 3, 4] + + +def test_17522_mpmath(): + from sympy.matrices.common import _matrixify + try: + from mpmath import matrix + except ImportError: + skip('mpmath must be available to test indexing matrixified mpmath matrices') + + m = _matrixify(matrix([[1, 2], [3, 4]])) + assert m[3] == 4.0 + assert list(m) == [1.0, 2.0, 3.0, 4.0] + + +def test_17522_scipy(): + from sympy.matrices.common import _matrixify + try: + from scipy.sparse import csr_matrix + except ImportError: + skip('SciPy must be available to test indexing matrixified SciPy sparse matrices') + + m = _matrixify(csr_matrix([[1, 2], [3, 4]])) + assert m[3] == 4 + assert list(m) == [1, 2, 3, 4] + + +def test_hermitian(): + a = Matrix([[1, I], [-I, 1]]) + assert a.is_hermitian + a[0, 0] = 2*I + assert a.is_hermitian is False + a[0, 0] = x + assert a.is_hermitian is None + a[0, 1] = a[1, 0]*I + assert a.is_hermitian is False + + +def test_issue_9457_9467_9876(): + # for row_del(index) + M = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + M.row_del(1) + assert M == Matrix([[1, 2, 3], [3, 4, 5]]) + N = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + N.row_del(-2) + assert N == Matrix([[1, 2, 3], [3, 4, 5]]) + O = Matrix([[1, 2, 3], [5, 6, 7], [9, 10, 11]]) + O.row_del(-1) + assert O == Matrix([[1, 2, 3], [5, 6, 7]]) + P = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + raises(IndexError, lambda: P.row_del(10)) + Q = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + raises(IndexError, lambda: Q.row_del(-10)) + + # for col_del(index) + M = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + M.col_del(1) + assert M == Matrix([[1, 3], [2, 4], [3, 5]]) + N = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + N.col_del(-2) + assert N == Matrix([[1, 3], [2, 4], [3, 5]]) + P = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + raises(IndexError, lambda: P.col_del(10)) + Q = Matrix([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + raises(IndexError, lambda: Q.col_del(-10)) + + +def test_issue_9422(): + x, y = symbols('x y', commutative=False) + a, b = symbols('a b') + M = eye(2) + M1 = Matrix(2, 2, [x, y, y, z]) + assert y*x*M != x*y*M + assert b*a*M == a*b*M + assert x*M1 != M1*x + assert a*M1 == M1*a + assert y*x*M == Matrix([[y*x, 0], [0, y*x]]) + + +def test_issue_10770(): + M = Matrix([]) + a = ['col_insert', 'row_join'], Matrix([9, 6, 3]) + b = ['row_insert', 'col_join'], a[1].T + c = ['row_insert', 'col_insert'], Matrix([[1, 2], [3, 4]]) + for ops, m in (a, b, c): + for op in ops: + f = getattr(M, op) + new = f(m) if 'join' in op else f(42, m) + assert new == m and id(new) != id(m) + + +def test_issue_10658(): + A = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + assert A.extract([0, 1, 2], [True, True, False]) == \ + Matrix([[1, 2], [4, 5], [7, 8]]) + assert A.extract([0, 1, 2], [True, False, False]) == Matrix([[1], [4], [7]]) + assert A.extract([True, False, False], [0, 1, 2]) == Matrix([[1, 2, 3]]) + assert A.extract([True, False, True], [0, 1, 2]) == \ + Matrix([[1, 2, 3], [7, 8, 9]]) + assert A.extract([0, 1, 2], [False, False, False]) == Matrix(3, 0, []) + assert A.extract([False, False, False], [0, 1, 2]) == Matrix(0, 3, []) + assert A.extract([True, False, True], [False, True, False]) == \ + Matrix([[2], [8]]) + + +def test_opportunistic_simplification(): + # this test relates to issue #10718, #9480, #11434 + + # issue #9480 + m = Matrix([[-5 + 5*sqrt(2), -5], [-5*sqrt(2)/2 + 5, -5*sqrt(2)/2]]) + assert m.rank() == 1 + + # issue #10781 + m = Matrix([[3+3*sqrt(3)*I, -9],[4,-3+3*sqrt(3)*I]]) + assert simplify(m.rref()[0] - Matrix([[1, -9/(3 + 3*sqrt(3)*I)], [0, 0]])) == zeros(2, 2) + + # issue #11434 + ax,ay,bx,by,cx,cy,dx,dy,ex,ey,t0,t1 = symbols('a_x a_y b_x b_y c_x c_y d_x d_y e_x e_y t_0 t_1') + m = Matrix([[ax,ay,ax*t0,ay*t0,0],[bx,by,bx*t0,by*t0,0],[cx,cy,cx*t0,cy*t0,1],[dx,dy,dx*t0,dy*t0,1],[ex,ey,2*ex*t1-ex*t0,2*ey*t1-ey*t0,0]]) + assert m.rank() == 4 + + +def test_partial_pivoting(): + # example from https://en.wikipedia.org/wiki/Pivot_element + # partial pivoting with back substitution gives a perfect result + # naive pivoting give an error ~1e-13, so anything better than + # 1e-15 is good + mm=Matrix([[0.003, 59.14, 59.17], [5.291, -6.13, 46.78]]) + assert (mm.rref()[0] - Matrix([[1.0, 0, 10.0], + [ 0, 1.0, 1.0]])).norm() < 1e-15 + + # issue #11549 + m_mixed = Matrix([[6e-17, 1.0, 4], + [ -1.0, 0, 8], + [ 0, 0, 1]]) + m_float = Matrix([[6e-17, 1.0, 4.], + [ -1.0, 0., 8.], + [ 0., 0., 1.]]) + m_inv = Matrix([[ 0, -1.0, 8.0], + [1.0, 6.0e-17, -4.0], + [ 0, 0, 1]]) + # this example is numerically unstable and involves a matrix with a norm >= 8, + # this comparing the difference of the results with 1e-15 is numerically sound. + assert (m_mixed.inv() - m_inv).norm() < 1e-15 + assert (m_float.inv() - m_inv).norm() < 1e-15 + + +def test_iszero_substitution(): + """ When doing numerical computations, all elements that pass + the iszerofunc test should be set to numerically zero if they + aren't already. """ + + # Matrix from issue #9060 + m = Matrix([[0.9, -0.1, -0.2, 0],[-0.8, 0.9, -0.4, 0],[-0.1, -0.8, 0.6, 0]]) + m_rref = m.rref(iszerofunc=lambda x: abs(x)<6e-15)[0] + m_correct = Matrix([[1.0, 0, -0.301369863013699, 0],[ 0, 1.0, -0.712328767123288, 0],[ 0, 0, 0, 0]]) + m_diff = m_rref - m_correct + assert m_diff.norm() < 1e-15 + # if a zero-substitution wasn't made, this entry will be -1.11022302462516e-16 + assert m_rref[2,2] == 0 + + +def test_issue_11238(): + from sympy.geometry.point import Point + xx = 8*tan(pi*Rational(13, 45))/(tan(pi*Rational(13, 45)) + sqrt(3)) + yy = (-8*sqrt(3)*tan(pi*Rational(13, 45))**2 + 24*tan(pi*Rational(13, 45)))/(-3 + tan(pi*Rational(13, 45))**2) + p1 = Point(0, 0) + p2 = Point(1, -sqrt(3)) + p0 = Point(xx,yy) + m1 = Matrix([p1 - simplify(p0), p2 - simplify(p0)]) + m2 = Matrix([p1 - p0, p2 - p0]) + m3 = Matrix([simplify(p1 - p0), simplify(p2 - p0)]) + + # This system has expressions which are zero and + # cannot be easily proved to be such, so without + # numerical testing, these assertions will fail. + Z = lambda x: abs(x.n()) < 1e-20 + assert m1.rank(simplify=True, iszerofunc=Z) == 1 + assert m2.rank(simplify=True, iszerofunc=Z) == 1 + assert m3.rank(simplify=True, iszerofunc=Z) == 1 + + +def test_as_real_imag(): + m1 = Matrix(2,2,[1,2,3,4]) + m2 = m1*S.ImaginaryUnit + m3 = m1 + m2 + + for kls in all_classes: + a,b = kls(m3).as_real_imag() + assert list(a) == list(m1) + assert list(b) == list(m1) + + +def test_deprecated(): + # Maintain tests for deprecated functions. We must capture + # the deprecation warnings. When the deprecated functionality is + # removed, the corresponding tests should be removed. + + m = Matrix(3, 3, [0, 1, 0, -4, 4, 0, -2, 1, 2]) + P, Jcells = m.jordan_cells() + assert Jcells[1] == Matrix(1, 1, [2]) + assert Jcells[0] == Matrix(2, 2, [2, 1, 0, 2]) + + +def test_issue_14489(): + from sympy.core.mod import Mod + A = Matrix([-1, 1, 2]) + B = Matrix([10, 20, -15]) + + assert Mod(A, 3) == Matrix([2, 1, 2]) + assert Mod(B, 4) == Matrix([2, 0, 1]) + + +def test_issue_14943(): + # Test that __array__ accepts the optional dtype argument + try: + from numpy import array + except ImportError: + skip('NumPy must be available to test creating matrices from ndarrays') + + M = Matrix([[1,2], [3,4]]) + assert array(M, dtype=float).dtype.name == 'float64' + + +def test_case_6913(): + m = MatrixSymbol('m', 1, 1) + a = Symbol("a") + a = m[0, 0]>0 + assert str(a) == 'm[0, 0] > 0' + + +def test_issue_11948(): + A = MatrixSymbol('A', 3, 3) + a = Wild('a') + assert A.match(a) == {a: A} + + +def test_gramschmidt_conjugate_dot(): + vecs = [Matrix([1, I]), Matrix([1, -I])] + assert Matrix.orthogonalize(*vecs) == \ + [Matrix([[1], [I]]), Matrix([[1], [-I]])] + + vecs = [Matrix([1, I, 0]), Matrix([I, 0, -I])] + assert Matrix.orthogonalize(*vecs) == \ + [Matrix([[1], [I], [0]]), Matrix([[I/2], [S(1)/2], [-I]])] + + mat = Matrix([[1, I], [1, -I]]) + Q, R = mat.QRdecomposition() + assert Q * Q.H == Matrix.eye(2) + + +def test_issue_8207(): + a = Matrix(MatrixSymbol('a', 3, 1)) + b = Matrix(MatrixSymbol('b', 3, 1)) + c = a.dot(b) + d = diff(c, a[0, 0]) + e = diff(d, a[0, 0]) + assert d == b[0, 0] + assert e == 0 + + +def test_func(): + from sympy.simplify.simplify import nthroot + + A = Matrix([[1, 2],[0, 3]]) + assert A.analytic_func(sin(x*t), x) == Matrix([[sin(t), sin(3*t) - sin(t)], [0, sin(3*t)]]) + + A = Matrix([[2, 1],[1, 2]]) + assert (pi * A / 6).analytic_func(cos(x), x) == Matrix([[sqrt(3)/4, -sqrt(3)/4], [-sqrt(3)/4, sqrt(3)/4]]) + + + raises(ValueError, lambda : zeros(5).analytic_func(log(x), x)) + raises(ValueError, lambda : (A*x).analytic_func(log(x), x)) + + A = Matrix([[0, -1, -2, 3], [0, -1, -2, 3], [0, 1, 0, -1], [0, 0, -1, 1]]) + assert A.analytic_func(exp(x), x) == A.exp() + raises(ValueError, lambda : A.analytic_func(sqrt(x), x)) + + A = Matrix([[41, 12],[12, 34]]) + assert simplify(A.analytic_func(sqrt(x), x)**2) == A + + A = Matrix([[3, -12, 4], [-1, 0, -2], [-1, 5, -1]]) + assert simplify(A.analytic_func(nthroot(x, 3), x)**3) == A + + A = Matrix([[2, 0, 0, 0], [1, 2, 0, 0], [0, 1, 3, 0], [0, 0, 1, 3]]) + assert A.analytic_func(exp(x), x) == A.exp() + + A = Matrix([[0, 2, 1, 6], [0, 0, 1, 2], [0, 0, 0, 3], [0, 0, 0, 0]]) + assert A.analytic_func(exp(x*t), x) == expand(simplify((A*t).exp())) + + +@skip_under_pyodide("Cannot create threads under pyodide.") +def test_issue_19809(): + + def f(): + assert _dotprodsimp_state.state == None + m = Matrix([[1]]) + m = m * m + return True + + with dotprodsimp(True): + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(f) + assert future.result() + + +def test_issue_23276(): + M = Matrix([x, y]) + assert integrate(M, (x, 0, 1), (y, 0, 1)) == Matrix([ + [S.Half], + [S.Half]]) + + +def test_issue_27225(): + # https://github.com/sympy/sympy/issues/27225 + raises(TypeError, lambda : floor(Matrix([1, 1, 0]))) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_normalforms.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_normalforms.py new file mode 100644 index 0000000000000000000000000000000000000000..47ee52d73539f7fb79295443e1cf7e0a49e30a5e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_normalforms.py @@ -0,0 +1,111 @@ +from sympy.testing.pytest import warns_deprecated_sympy + +from sympy.core.symbol import Symbol +from sympy.polys.polytools import Poly +from sympy.matrices import Matrix, randMatrix +from sympy.matrices.normalforms import ( + invariant_factors, + smith_normal_form, + smith_normal_decomp, + hermite_normal_form, + is_smith_normal_form, +) +from sympy.polys.domains import ZZ, QQ +from sympy.core.numbers import Integer + +import random + + +def test_smith_normal(): + m = Matrix([[12,6,4,8],[3,9,6,12],[2,16,14,28],[20,10,10,20]]) + smf = Matrix([[1, 0, 0, 0], [0, 10, 0, 0], [0, 0, 30, 0], [0, 0, 0, 0]]) + assert smith_normal_form(m) == smf + + a, s, t = smith_normal_decomp(m) + assert a == s * m * t + + x = Symbol('x') + with warns_deprecated_sympy(): + m = Matrix([[Poly(x-1), Poly(1, x),Poly(-1,x)], + [0, Poly(x), Poly(-1,x)], + [Poly(0,x),Poly(-1,x),Poly(x)]]) + invs = 1, x - 1, x**2 - 1 + assert invariant_factors(m, domain=QQ[x]) == invs + + m = Matrix([[2, 4]]) + smf = Matrix([[2, 0]]) + assert smith_normal_form(m) == smf + + prng = random.Random(0) + for i in range(6): + for j in range(6): + for _ in range(10 if i*j else 1): + m = randMatrix(i, j, max=5, percent=50, prng=prng) + a, s, t = smith_normal_decomp(m) + assert a == s * m * t + assert is_smith_normal_form(a) + s.inv().to_DM(ZZ) + t.inv().to_DM(ZZ) + + a, s, t = smith_normal_decomp(m, QQ) + assert a == s * m * t + assert is_smith_normal_form(a) + s.inv() + t.inv() + + +def test_smith_normal_deprecated(): + from sympy.polys.solvers import RawMatrix as Matrix + + with warns_deprecated_sympy(): + m = Matrix([[12, 6, 4,8],[3,9,6,12],[2,16,14,28],[20,10,10,20]]) + setattr(m, 'ring', ZZ) + with warns_deprecated_sympy(): + smf = Matrix([[1, 0, 0, 0], [0, 10, 0, 0], [0, 0, 30, 0], [0, 0, 0, 0]]) + assert smith_normal_form(m) == smf + + x = Symbol('x') + with warns_deprecated_sympy(): + m = Matrix([[Poly(x-1), Poly(1, x),Poly(-1,x)], + [0, Poly(x), Poly(-1,x)], + [Poly(0,x),Poly(-1,x),Poly(x)]]) + setattr(m, 'ring', QQ[x]) + invs = (Poly(1, x, domain='QQ'), Poly(x - 1, domain='QQ'), Poly(x**2 - 1, domain='QQ')) + assert invariant_factors(m) == invs + + with warns_deprecated_sympy(): + m = Matrix([[2, 4]]) + setattr(m, 'ring', ZZ) + with warns_deprecated_sympy(): + smf = Matrix([[2, 0]]) + assert smith_normal_form(m) == smf + + +def test_hermite_normal(): + m = Matrix([[2, 7, 17, 29, 41], [3, 11, 19, 31, 43], [5, 13, 23, 37, 47]]) + hnf = Matrix([[1, 0, 0], [0, 2, 1], [0, 0, 1]]) + assert hermite_normal_form(m) == hnf + + tr_hnf = Matrix([[37, 0, 19], [222, -6, 113], [48, 0, 25], [0, 2, 1], [0, 0, 1]]) + assert hermite_normal_form(m.transpose()) == tr_hnf + + m = Matrix([[8, 28, 68, 116, 164], [3, 11, 19, 31, 43], [5, 13, 23, 37, 47]]) + hnf = Matrix([[4, 0, 0], [0, 2, 1], [0, 0, 1]]) + assert hermite_normal_form(m) == hnf + assert hermite_normal_form(m, D=8) == hnf + assert hermite_normal_form(m, D=ZZ(8)) == hnf + assert hermite_normal_form(m, D=Integer(8)) == hnf + + m = Matrix([[10, 8, 6, 30, 2], [45, 36, 27, 18, 9], [5, 4, 3, 2, 1]]) + hnf = Matrix([[26, 2], [0, 9], [0, 1]]) + assert hermite_normal_form(m) == hnf + + m = Matrix([[2, 7], [0, 0], [0, 0]]) + hnf = Matrix([[1], [0], [0]]) + assert hermite_normal_form(m) == hnf + + +def test_issue_23410(): + A = Matrix([[1, 12], [0, 8], [0, 5]]) + H = Matrix([[1, 0], [0, 8], [0, 5]]) + assert hermite_normal_form(A) == H diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_reductions.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_reductions.py new file mode 100644 index 0000000000000000000000000000000000000000..32c98c6f249b1afafc8193f4248dc9493bb803e0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_reductions.py @@ -0,0 +1,351 @@ +from sympy.core.numbers import I +from sympy.core.symbol import symbols +from sympy.testing.pytest import raises +from sympy.matrices import Matrix, zeros, eye +from sympy.core.symbol import Symbol +from sympy.core.numbers import Rational +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.simplify.simplify import simplify +from sympy.abc import x + + +# Matrix tests +def test_row_op(): + e = eye(3) + + raises(ValueError, lambda: e.elementary_row_op("abc")) + raises(ValueError, lambda: e.elementary_row_op()) + raises(ValueError, lambda: e.elementary_row_op('n->kn', row=5, k=5)) + raises(ValueError, lambda: e.elementary_row_op('n->kn', row=-5, k=5)) + raises(ValueError, lambda: e.elementary_row_op('n<->m', row1=1, row2=5)) + raises(ValueError, lambda: e.elementary_row_op('n<->m', row1=5, row2=1)) + raises(ValueError, lambda: e.elementary_row_op('n<->m', row1=-5, row2=1)) + raises(ValueError, lambda: e.elementary_row_op('n<->m', row1=1, row2=-5)) + raises(ValueError, lambda: e.elementary_row_op('n->n+km', row1=1, row2=5, k=5)) + raises(ValueError, lambda: e.elementary_row_op('n->n+km', row1=5, row2=1, k=5)) + raises(ValueError, lambda: e.elementary_row_op('n->n+km', row1=-5, row2=1, k=5)) + raises(ValueError, lambda: e.elementary_row_op('n->n+km', row1=1, row2=-5, k=5)) + raises(ValueError, lambda: e.elementary_row_op('n->n+km', row1=1, row2=1, k=5)) + + # test various ways to set arguments + assert e.elementary_row_op("n->kn", 0, 5) == Matrix([[5, 0, 0], [0, 1, 0], [0, 0, 1]]) + assert e.elementary_row_op("n->kn", 1, 5) == Matrix([[1, 0, 0], [0, 5, 0], [0, 0, 1]]) + assert e.elementary_row_op("n->kn", row=1, k=5) == Matrix([[1, 0, 0], [0, 5, 0], [0, 0, 1]]) + assert e.elementary_row_op("n->kn", row1=1, k=5) == Matrix([[1, 0, 0], [0, 5, 0], [0, 0, 1]]) + assert e.elementary_row_op("n<->m", 0, 1) == Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) + assert e.elementary_row_op("n<->m", row1=0, row2=1) == Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) + assert e.elementary_row_op("n<->m", row=0, row2=1) == Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) + assert e.elementary_row_op("n->n+km", 0, 5, 1) == Matrix([[1, 5, 0], [0, 1, 0], [0, 0, 1]]) + assert e.elementary_row_op("n->n+km", row=0, k=5, row2=1) == Matrix([[1, 5, 0], [0, 1, 0], [0, 0, 1]]) + assert e.elementary_row_op("n->n+km", row1=0, k=5, row2=1) == Matrix([[1, 5, 0], [0, 1, 0], [0, 0, 1]]) + + # make sure the matrix doesn't change size + a = Matrix(2, 3, [0]*6) + assert a.elementary_row_op("n->kn", 1, 5) == Matrix(2, 3, [0]*6) + assert a.elementary_row_op("n<->m", 0, 1) == Matrix(2, 3, [0]*6) + assert a.elementary_row_op("n->n+km", 0, 5, 1) == Matrix(2, 3, [0]*6) + + +def test_col_op(): + e = eye(3) + + raises(ValueError, lambda: e.elementary_col_op("abc")) + raises(ValueError, lambda: e.elementary_col_op()) + raises(ValueError, lambda: e.elementary_col_op('n->kn', col=5, k=5)) + raises(ValueError, lambda: e.elementary_col_op('n->kn', col=-5, k=5)) + raises(ValueError, lambda: e.elementary_col_op('n<->m', col1=1, col2=5)) + raises(ValueError, lambda: e.elementary_col_op('n<->m', col1=5, col2=1)) + raises(ValueError, lambda: e.elementary_col_op('n<->m', col1=-5, col2=1)) + raises(ValueError, lambda: e.elementary_col_op('n<->m', col1=1, col2=-5)) + raises(ValueError, lambda: e.elementary_col_op('n->n+km', col1=1, col2=5, k=5)) + raises(ValueError, lambda: e.elementary_col_op('n->n+km', col1=5, col2=1, k=5)) + raises(ValueError, lambda: e.elementary_col_op('n->n+km', col1=-5, col2=1, k=5)) + raises(ValueError, lambda: e.elementary_col_op('n->n+km', col1=1, col2=-5, k=5)) + raises(ValueError, lambda: e.elementary_col_op('n->n+km', col1=1, col2=1, k=5)) + + # test various ways to set arguments + assert e.elementary_col_op("n->kn", 0, 5) == Matrix([[5, 0, 0], [0, 1, 0], [0, 0, 1]]) + assert e.elementary_col_op("n->kn", 1, 5) == Matrix([[1, 0, 0], [0, 5, 0], [0, 0, 1]]) + assert e.elementary_col_op("n->kn", col=1, k=5) == Matrix([[1, 0, 0], [0, 5, 0], [0, 0, 1]]) + assert e.elementary_col_op("n->kn", col1=1, k=5) == Matrix([[1, 0, 0], [0, 5, 0], [0, 0, 1]]) + assert e.elementary_col_op("n<->m", 0, 1) == Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) + assert e.elementary_col_op("n<->m", col1=0, col2=1) == Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) + assert e.elementary_col_op("n<->m", col=0, col2=1) == Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) + assert e.elementary_col_op("n->n+km", 0, 5, 1) == Matrix([[1, 0, 0], [5, 1, 0], [0, 0, 1]]) + assert e.elementary_col_op("n->n+km", col=0, k=5, col2=1) == Matrix([[1, 0, 0], [5, 1, 0], [0, 0, 1]]) + assert e.elementary_col_op("n->n+km", col1=0, k=5, col2=1) == Matrix([[1, 0, 0], [5, 1, 0], [0, 0, 1]]) + + # make sure the matrix doesn't change size + a = Matrix(2, 3, [0]*6) + assert a.elementary_col_op("n->kn", 1, 5) == Matrix(2, 3, [0]*6) + assert a.elementary_col_op("n<->m", 0, 1) == Matrix(2, 3, [0]*6) + assert a.elementary_col_op("n->n+km", 0, 5, 1) == Matrix(2, 3, [0]*6) + + +def test_is_echelon(): + zro = zeros(3) + ident = eye(3) + + assert zro.is_echelon + assert ident.is_echelon + + a = Matrix(0, 0, []) + assert a.is_echelon + + a = Matrix(2, 3, [3, 2, 1, 0, 0, 6]) + assert a.is_echelon + + a = Matrix(2, 3, [0, 0, 6, 3, 2, 1]) + assert not a.is_echelon + + x = Symbol('x') + a = Matrix(3, 1, [x, 0, 0]) + assert a.is_echelon + + a = Matrix(3, 1, [x, x, 0]) + assert not a.is_echelon + + a = Matrix(3, 3, [0, 0, 0, 1, 2, 3, 0, 0, 0]) + assert not a.is_echelon + + +def test_echelon_form(): + # echelon form is not unique, but the result + # must be row-equivalent to the original matrix + # and it must be in echelon form. + + a = zeros(3) + e = eye(3) + + # we can assume the zero matrix and the identity matrix shouldn't change + assert a.echelon_form() == a + assert e.echelon_form() == e + + a = Matrix(0, 0, []) + assert a.echelon_form() == a + + a = Matrix(1, 1, [5]) + assert a.echelon_form() == a + + # now we get to the real tests + + def verify_row_null_space(mat, rows, nulls): + for v in nulls: + assert all(t.is_zero for t in a_echelon*v) + for v in rows: + if not all(t.is_zero for t in v): + assert not all(t.is_zero for t in a_echelon*v.transpose()) + + a = Matrix(3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 9]) + nulls = [Matrix([ + [ 1], + [-2], + [ 1]])] + rows = [a[i, :] for i in range(a.rows)] + a_echelon = a.echelon_form() + assert a_echelon.is_echelon + verify_row_null_space(a, rows, nulls) + + + a = Matrix(3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 8]) + nulls = [] + rows = [a[i, :] for i in range(a.rows)] + a_echelon = a.echelon_form() + assert a_echelon.is_echelon + verify_row_null_space(a, rows, nulls) + + a = Matrix(3, 3, [2, 1, 3, 0, 0, 0, 2, 1, 3]) + nulls = [Matrix([ + [Rational(-1, 2)], + [ 1], + [ 0]]), + Matrix([ + [Rational(-3, 2)], + [ 0], + [ 1]])] + rows = [a[i, :] for i in range(a.rows)] + a_echelon = a.echelon_form() + assert a_echelon.is_echelon + verify_row_null_space(a, rows, nulls) + + # this one requires a row swap + a = Matrix(3, 3, [2, 1, 3, 0, 0, 0, 1, 1, 3]) + nulls = [Matrix([ + [ 0], + [ -3], + [ 1]])] + rows = [a[i, :] for i in range(a.rows)] + a_echelon = a.echelon_form() + assert a_echelon.is_echelon + verify_row_null_space(a, rows, nulls) + + a = Matrix(3, 3, [0, 3, 3, 0, 2, 2, 0, 1, 1]) + nulls = [Matrix([ + [1], + [0], + [0]]), + Matrix([ + [ 0], + [-1], + [ 1]])] + rows = [a[i, :] for i in range(a.rows)] + a_echelon = a.echelon_form() + assert a_echelon.is_echelon + verify_row_null_space(a, rows, nulls) + + a = Matrix(2, 3, [2, 2, 3, 3, 3, 0]) + nulls = [Matrix([ + [-1], + [1], + [0]])] + rows = [a[i, :] for i in range(a.rows)] + a_echelon = a.echelon_form() + assert a_echelon.is_echelon + verify_row_null_space(a, rows, nulls) + + +def test_rref(): + e = Matrix(0, 0, []) + assert e.rref(pivots=False) == e + + e = Matrix(1, 1, [1]) + a = Matrix(1, 1, [5]) + assert e.rref(pivots=False) == a.rref(pivots=False) == e + + a = Matrix(3, 1, [1, 2, 3]) + assert a.rref(pivots=False) == Matrix([[1], [0], [0]]) + + a = Matrix(1, 3, [1, 2, 3]) + assert a.rref(pivots=False) == Matrix([[1, 2, 3]]) + + a = Matrix(3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 9]) + assert a.rref(pivots=False) == Matrix([ + [1, 0, -1], + [0, 1, 2], + [0, 0, 0]]) + + a = Matrix(3, 3, [1, 2, 3, 1, 2, 3, 1, 2, 3]) + b = Matrix(3, 3, [1, 2, 3, 0, 0, 0, 0, 0, 0]) + c = Matrix(3, 3, [0, 0, 0, 1, 2, 3, 0, 0, 0]) + d = Matrix(3, 3, [0, 0, 0, 0, 0, 0, 1, 2, 3]) + assert a.rref(pivots=False) == \ + b.rref(pivots=False) == \ + c.rref(pivots=False) == \ + d.rref(pivots=False) == b + + e = eye(3) + z = zeros(3) + assert e.rref(pivots=False) == e + assert z.rref(pivots=False) == z + + a = Matrix([ + [ 0, 0, 1, 2, 2, -5, 3], + [-1, 5, 2, 2, 1, -7, 5], + [ 0, 0, -2, -3, -3, 8, -5], + [-1, 5, 0, -1, -2, 1, 0]]) + mat, pivot_offsets = a.rref() + assert mat == Matrix([ + [1, -5, 0, 0, 1, 1, -1], + [0, 0, 1, 0, 0, -1, 1], + [0, 0, 0, 1, 1, -2, 1], + [0, 0, 0, 0, 0, 0, 0]]) + assert pivot_offsets == (0, 2, 3) + + a = Matrix([[Rational(1, 19), Rational(1, 5), 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11], + [ 12, 13, 14, 15]]) + assert a.rref(pivots=False) == Matrix([ + [1, 0, 0, Rational(-76, 157)], + [0, 1, 0, Rational(-5, 157)], + [0, 0, 1, Rational(238, 157)], + [0, 0, 0, 0]]) + + x = Symbol('x') + a = Matrix(2, 3, [x, 1, 1, sqrt(x), x, 1]) + for i, j in zip(a.rref(pivots=False), + [1, 0, sqrt(x)*(-x + 1)/(-x**Rational(5, 2) + x), + 0, 1, 1/(sqrt(x) + x + 1)]): + assert simplify(i - j).is_zero + + +def test_rref_rhs(): + a, b, c, d = symbols('a b c d') + A = Matrix([[0, 0], [0, 0], [1, 2], [3, 4]]) + B = Matrix([a, b, c, d]) + assert A.rref_rhs(B) == (Matrix([ + [1, 0], + [0, 1], + [0, 0], + [0, 0]]), Matrix([ + [ -2*c + d], + [3*c/2 - d/2], + [ a], + [ b]])) + + +def test_issue_17827(): + C = Matrix([ + [3, 4, -1, 1], + [9, 12, -3, 3], + [0, 2, 1, 3], + [2, 3, 0, -2], + [0, 3, 3, -5], + [8, 15, 0, 6] + ]) + # Tests for row/col within valid range + D = C.elementary_row_op('n<->m', row1=2, row2=5) + E = C.elementary_row_op('n->n+km', row1=5, row2=3, k=-4) + F = C.elementary_row_op('n->kn', row=5, k=2) + assert(D[5, :] == Matrix([[0, 2, 1, 3]])) + assert(E[5, :] == Matrix([[0, 3, 0, 14]])) + assert(F[5, :] == Matrix([[16, 30, 0, 12]])) + # Tests for row/col out of range + raises(ValueError, lambda: C.elementary_row_op('n<->m', row1=2, row2=6)) + raises(ValueError, lambda: C.elementary_row_op('n->kn', row=7, k=2)) + raises(ValueError, lambda: C.elementary_row_op('n->n+km', row1=-1, row2=5, k=2)) + +def test_rank(): + m = Matrix([[1, 2], [x, 1 - 1/x]]) + assert m.rank() == 2 + n = Matrix(3, 3, range(1, 10)) + assert n.rank() == 2 + p = zeros(3) + assert p.rank() == 0 + +def test_issue_11434(): + ax, ay, bx, by, cx, cy, dx, dy, ex, ey, t0, t1 = \ + symbols('a_x a_y b_x b_y c_x c_y d_x d_y e_x e_y t_0 t_1') + M = Matrix([[ax, ay, ax*t0, ay*t0, 0], + [bx, by, bx*t0, by*t0, 0], + [cx, cy, cx*t0, cy*t0, 1], + [dx, dy, dx*t0, dy*t0, 1], + [ex, ey, 2*ex*t1 - ex*t0, 2*ey*t1 - ey*t0, 0]]) + assert M.rank() == 4 + +def test_rank_regression_from_so(): + # see: + # https://stackoverflow.com/questions/19072700/why-does-sympy-give-me-the-wrong-answer-when-i-row-reduce-a-symbolic-matrix + + nu, lamb = symbols('nu, lambda') + A = Matrix([[-3*nu, 1, 0, 0], + [ 3*nu, -2*nu - 1, 2, 0], + [ 0, 2*nu, (-1*nu) - lamb - 2, 3], + [ 0, 0, nu + lamb, -3]]) + expected_reduced = Matrix([[1, 0, 0, 1/(nu**2*(-lamb - nu))], + [0, 1, 0, 3/(nu*(-lamb - nu))], + [0, 0, 1, 3/(-lamb - nu)], + [0, 0, 0, 0]]) + expected_pivots = (0, 1, 2) + + reduced, pivots = A.rref() + + assert simplify(expected_reduced - reduced) == zeros(*A.shape) + assert pivots == expected_pivots + +def test_issue_15872(): + A = Matrix([[1, 1, 1, 0], [-2, -1, 0, -1], [0, 0, -1, -1], [0, 0, 2, 1]]) + B = A - Matrix.eye(4) * I + assert B.rank() == 3 + assert (B**2).rank() == 2 + assert (B**3).rank() == 2 diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_repmatrix.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_repmatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..ee36de004705f29eaa49ea8e06fd65a8a2baa718 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_repmatrix.py @@ -0,0 +1,62 @@ +from sympy.testing.pytest import raises +from sympy.matrices.exceptions import NonSquareMatrixError, NonInvertibleMatrixError + +from sympy import Matrix, Rational + + +def test_lll(): + A = Matrix([[1, 0, 0, 0, -20160], + [0, 1, 0, 0, 33768], + [0, 0, 1, 0, 39578], + [0, 0, 0, 1, 47757]]) + L = Matrix([[ 10, -3, -2, 8, -4], + [ 3, -9, 8, 1, -11], + [ -3, 13, -9, -3, -9], + [-12, -7, -11, 9, -1]]) + T = Matrix([[ 10, -3, -2, 8], + [ 3, -9, 8, 1], + [ -3, 13, -9, -3], + [-12, -7, -11, 9]]) + assert A.lll() == L + assert A.lll_transform() == (L, T) + assert T * A == L + + +def test_matrix_inv_mod(): + A = Matrix(2, 1, [1, 0]) + raises(NonSquareMatrixError, lambda: A.inv_mod(2)) + A = Matrix(2, 2, [1, 0, 0, 0]) + raises(NonInvertibleMatrixError, lambda: A.inv_mod(2)) + A = Matrix(2, 2, [1, 2, 3, 4]) + Ai = Matrix(2, 2, [1, 1, 0, 1]) + assert A.inv_mod(3) == Ai + A = Matrix(2, 2, [1, 0, 0, 1]) + assert A.inv_mod(2) == A + A = Matrix(3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 9]) + raises(NonInvertibleMatrixError, lambda: A.inv_mod(5)) + A = Matrix(3, 3, [5, 1, 3, 2, 6, 0, 2, 1, 1]) + Ai = Matrix(3, 3, [6, 8, 0, 1, 5, 6, 5, 6, 4]) + assert A.inv_mod(9) == Ai + A = Matrix(3, 3, [1, 6, -3, 4, 1, -5, 3, -5, 5]) + Ai = Matrix(3, 3, [4, 3, 3, 1, 2, 5, 1, 5, 1]) + assert A.inv_mod(6) == Ai + A = Matrix(3, 3, [1, 6, 1, 4, 1, 5, 3, 2, 5]) + Ai = Matrix(3, 3, [6, 0, 3, 6, 6, 4, 1, 6, 1]) + assert A.inv_mod(7) == Ai + A = Matrix([[1, 2], [3, Rational(3,4)]]) + raises(ValueError, lambda: A.inv_mod(2)) + A = Matrix([[1, 2], [3, 4]]) + raises(TypeError, lambda: A.inv_mod(Rational(1, 2))) + # https://github.com/sympy/sympy/issues/27663 + M = Matrix([ + [2, 3, 1, 4], + [1, 5, 3, 2], + [3, 2, 4, 1], + [4, 1, 2, 5], + ]) + assert M.inv_mod(26) == Matrix([ + [7, 21, 10, 10], + [1, 7, 19, 3], + [14, 1, 15, 1], + [25, 23, 3, 12], + ]) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_solvers.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_solvers.py new file mode 100644 index 0000000000000000000000000000000000000000..c1347062c0482336affbeb4bb9a95aedfcc0ae53 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_solvers.py @@ -0,0 +1,615 @@ +import pytest +from sympy.core.function import expand_mul +from sympy.core.numbers import (I, Rational) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.core.sympify import sympify +from sympy.simplify.simplify import simplify +from sympy.matrices.exceptions import (ShapeError, NonSquareMatrixError) +from sympy.matrices import ( + ImmutableMatrix, Matrix, eye, ones, ImmutableDenseMatrix, dotprodsimp) +from sympy.matrices.determinant import _det_laplace +from sympy.testing.pytest import raises +from sympy.matrices.exceptions import NonInvertibleMatrixError +from sympy.polys.matrices.exceptions import DMShapeError +from sympy.solvers.solveset import linsolve +from sympy.abc import x, y + +def test_issue_17247_expression_blowup_29(): + M = Matrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 0, 0], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 0, 9/4 + 55*I/16, 2473/256 + 137*I/64, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.gauss_jordan_solve(ones(4, 1)) == (Matrix(S('''[ + [ -32549314808672/3306971225785 - 17397006745216*I/3306971225785], + [ 67439348256/3306971225785 - 9167503335872*I/3306971225785], + [-15091965363354518272/21217636514687010905 + 16890163109293858304*I/21217636514687010905], + [ -11328/952745 + 87616*I/952745]]''')), Matrix(0, 1, [])) + +def test_issue_17247_expression_blowup_30(): + M = Matrix(S('''[ + [ -3/4, 45/32 - 37*I/16, 0, 0], + [-149/64 + 49*I/32, -177/128 - 1369*I/128, 0, -2063/256 + 541*I/128], + [ 0, 9/4 + 55*I/16, 2473/256 + 137*I/64, 0], + [ 0, 0, 0, -177/128 - 1369*I/128]]''')) + with dotprodsimp(True): + assert M.cholesky_solve(ones(4, 1)) == Matrix(S('''[ + [ -32549314808672/3306971225785 - 17397006745216*I/3306971225785], + [ 67439348256/3306971225785 - 9167503335872*I/3306971225785], + [-15091965363354518272/21217636514687010905 + 16890163109293858304*I/21217636514687010905], + [ -11328/952745 + 87616*I/952745]]''')) + +# @XFAIL # This calculation hangs with dotprodsimp. +# def test_issue_17247_expression_blowup_31(): +# M = Matrix([ +# [x + 1, 1 - x, 0, 0], +# [1 - x, x + 1, 0, x + 1], +# [ 0, 1 - x, x + 1, 0], +# [ 0, 0, 0, x + 1]]) +# with dotprodsimp(True): +# assert M.LDLsolve(ones(4, 1)) == Matrix([ +# [(x + 1)/(4*x)], +# [(x - 1)/(4*x)], +# [(x + 1)/(4*x)], +# [ 1/(x + 1)]]) + + +def test_LUsolve_iszerofunc(): + # taken from https://github.com/sympy/sympy/issues/24679 + + M = Matrix([[(x + 1)**2 - (x**2 + 2*x + 1), x], [x, 0]]) + b = Matrix([1, 1]) + is_zero_func = lambda e: False if e._random() else True + + x_exp = Matrix([1/x, (1-(-x**2 - 2*x + (x+1)**2 - 1)/x)/x]) + + assert (x_exp - M.LUsolve(b, iszerofunc=is_zero_func)) == Matrix([0, 0]) + + +def test_issue_17247_expression_blowup_32(): + M = Matrix([ + [x + 1, 1 - x, 0, 0], + [1 - x, x + 1, 0, x + 1], + [ 0, 1 - x, x + 1, 0], + [ 0, 0, 0, x + 1]]) + with dotprodsimp(True): + assert M.LUsolve(ones(4, 1)) == Matrix([ + [(x + 1)/(4*x)], + [(x - 1)/(4*x)], + [(x + 1)/(4*x)], + [ 1/(x + 1)]]) + +def test_LUsolve(): + A = Matrix([[2, 3, 5], + [3, 6, 2], + [8, 3, 6]]) + x = Matrix(3, 1, [3, 7, 5]) + b = A*x + soln = A.LUsolve(b) + assert soln == x + A = Matrix([[0, -1, 2], + [5, 10, 7], + [8, 3, 4]]) + x = Matrix(3, 1, [-1, 2, 5]) + b = A*x + soln = A.LUsolve(b) + assert soln == x + A = Matrix([[2, 1], [1, 0], [1, 0]]) # issue 14548 + b = Matrix([3, 1, 1]) + assert A.LUsolve(b) == Matrix([1, 1]) + b = Matrix([3, 1, 2]) # inconsistent + raises(ValueError, lambda: A.LUsolve(b)) + A = Matrix([[0, -1, 2], + [5, 10, 7], + [8, 3, 4], + [2, 3, 5], + [3, 6, 2], + [8, 3, 6]]) + x = Matrix([2, 1, -4]) + b = A*x + soln = A.LUsolve(b) + assert soln == x + A = Matrix([[0, -1, 2], [5, 10, 7]]) # underdetermined + x = Matrix([-1, 2, 0]) + b = A*x + raises(NotImplementedError, lambda: A.LUsolve(b)) + + A = Matrix(4, 4, lambda i, j: 1/(i+j+1) if i != 3 else 0) + b = Matrix.zeros(4, 1) + raises(NonInvertibleMatrixError, lambda: A.LUsolve(b)) + + +def test_LUsolve_noncommutative(): + a0, a1, a2, a3 = symbols("a:4", commutative=False) + b0, b1 = symbols("b:2", commutative=False) + A = Matrix([[a0, a1], [a2, a3]]) + check = A * A.LUsolve(Matrix([b0, b1])) + assert check[0, 0].expand() == b0 + # Because sympy simplification is very limited with noncommutative expressions, + # perform an explicit check with the second element + assert check[1, 0] == ( + a2*a0**(-1)*(-a1*(-a2*a0**(-1)*a1 + a3)**(-1)*(-a2*a0**(-1)*b0 + b1) + b0) + + a3*(-a2*a0**(-1)*a1 + a3)**(-1)*(-a2*a0**(-1)*b0 + b1) + ) + + +def test_QRsolve(): + A = Matrix([[2, 3, 5], + [3, 6, 2], + [8, 3, 6]]) + x = Matrix(3, 1, [3, 7, 5]) + b = A*x + soln = A.QRsolve(b) + assert soln == x + x = Matrix([[1, 2], [3, 4], [5, 6]]) + b = A*x + soln = A.QRsolve(b) + assert soln == x + + A = Matrix([[0, -1, 2], + [5, 10, 7], + [8, 3, 4]]) + x = Matrix(3, 1, [-1, 2, 5]) + b = A*x + soln = A.QRsolve(b) + assert soln == x + x = Matrix([[7, 8], [9, 10], [11, 12]]) + b = A*x + soln = A.QRsolve(b) + assert soln == x + +def test_errors(): + raises(ShapeError, lambda: Matrix([1]).LUsolve(Matrix([[1, 2], [3, 4]]))) + +def test_cholesky_solve(): + A = Matrix([[2, 3, 5], + [3, 6, 2], + [8, 3, 6]]) + x = Matrix(3, 1, [3, 7, 5]) + b = A*x + soln = A.cholesky_solve(b) + assert soln == x + A = Matrix([[0, -1, 2], + [5, 10, 7], + [8, 3, 4]]) + x = Matrix(3, 1, [-1, 2, 5]) + b = A*x + soln = A.cholesky_solve(b) + assert soln == x + A = Matrix(((1, 5), (5, 1))) + x = Matrix((4, -3)) + b = A*x + soln = A.cholesky_solve(b) + assert soln == x + A = Matrix(((9, 3*I), (-3*I, 5))) + x = Matrix((-2, 1)) + b = A*x + soln = A.cholesky_solve(b) + assert expand_mul(soln) == x + A = Matrix(((9*I, 3), (-3 + I, 5))) + x = Matrix((2 + 3*I, -1)) + b = A*x + soln = A.cholesky_solve(b) + assert expand_mul(soln) == x + a00, a01, a11, b0, b1 = symbols('a00, a01, a11, b0, b1') + A = Matrix(((a00, a01), (a01, a11))) + b = Matrix((b0, b1)) + x = A.cholesky_solve(b) + assert simplify(A*x) == b + + +def test_LDLsolve(): + A = Matrix([[2, 3, 5], + [3, 6, 2], + [8, 3, 6]]) + x = Matrix(3, 1, [3, 7, 5]) + b = A*x + soln = A.LDLsolve(b) + assert soln == x + + A = Matrix([[0, -1, 2], + [5, 10, 7], + [8, 3, 4]]) + x = Matrix(3, 1, [-1, 2, 5]) + b = A*x + soln = A.LDLsolve(b) + assert soln == x + + A = Matrix(((9, 3*I), (-3*I, 5))) + x = Matrix((-2, 1)) + b = A*x + soln = A.LDLsolve(b) + assert expand_mul(soln) == x + + A = Matrix(((9*I, 3), (-3 + I, 5))) + x = Matrix((2 + 3*I, -1)) + b = A*x + soln = A.LDLsolve(b) + assert expand_mul(soln) == x + + A = Matrix(((9, 3), (3, 9))) + x = Matrix((1, 1)) + b = A * x + soln = A.LDLsolve(b) + assert expand_mul(soln) == x + + A = Matrix([[-5, -3, -4], [-3, -7, 7]]) + x = Matrix([[8], [7], [-2]]) + b = A * x + raises(NotImplementedError, lambda: A.LDLsolve(b)) + + +def test_lower_triangular_solve(): + + raises(NonSquareMatrixError, + lambda: Matrix([1, 0]).lower_triangular_solve(Matrix([0, 1]))) + raises(ShapeError, + lambda: Matrix([[1, 0], [0, 1]]).lower_triangular_solve(Matrix([1]))) + raises(ValueError, + lambda: Matrix([[2, 1], [1, 2]]).lower_triangular_solve( + Matrix([[1, 0], [0, 1]]))) + + A = Matrix([[1, 0], [0, 1]]) + B = Matrix([[x, y], [y, x]]) + C = Matrix([[4, 8], [2, 9]]) + + assert A.lower_triangular_solve(B) == B + assert A.lower_triangular_solve(C) == C + + +def test_upper_triangular_solve(): + + raises(NonSquareMatrixError, + lambda: Matrix([1, 0]).upper_triangular_solve(Matrix([0, 1]))) + raises(ShapeError, + lambda: Matrix([[1, 0], [0, 1]]).upper_triangular_solve(Matrix([1]))) + raises(TypeError, + lambda: Matrix([[2, 1], [1, 2]]).upper_triangular_solve( + Matrix([[1, 0], [0, 1]]))) + + A = Matrix([[1, 0], [0, 1]]) + B = Matrix([[x, y], [y, x]]) + C = Matrix([[2, 4], [3, 8]]) + + assert A.upper_triangular_solve(B) == B + assert A.upper_triangular_solve(C) == C + + +def test_diagonal_solve(): + raises(TypeError, lambda: Matrix([1, 1]).diagonal_solve(Matrix([1]))) + A = Matrix([[1, 0], [0, 1]])*2 + B = Matrix([[x, y], [y, x]]) + assert A.diagonal_solve(B) == B/2 + + A = Matrix([[1, 0], [1, 2]]) + raises(TypeError, lambda: A.diagonal_solve(B)) + +def test_pinv_solve(): + # Fully determined system (unique result, identical to other solvers). + A = Matrix([[1, 5], [7, 9]]) + B = Matrix([12, 13]) + assert A.pinv_solve(B) == A.cholesky_solve(B) + assert A.pinv_solve(B) == A.LDLsolve(B) + assert A.pinv_solve(B) == Matrix([sympify('-43/26'), sympify('71/26')]) + assert A * A.pinv() * B == B + # Fully determined, with two-dimensional B matrix. + B = Matrix([[12, 13, 14], [15, 16, 17]]) + assert A.pinv_solve(B) == A.cholesky_solve(B) + assert A.pinv_solve(B) == A.LDLsolve(B) + assert A.pinv_solve(B) == Matrix([[-33, -37, -41], [69, 75, 81]]) / 26 + assert A * A.pinv() * B == B + # Underdetermined system (infinite results). + A = Matrix([[1, 0, 1], [0, 1, 1]]) + B = Matrix([5, 7]) + solution = A.pinv_solve(B) + w = {} + for s in solution.atoms(Symbol): + # Extract dummy symbols used in the solution. + w[s.name] = s + assert solution == Matrix([[w['w0_0']/3 + w['w1_0']/3 - w['w2_0']/3 + 1], + [w['w0_0']/3 + w['w1_0']/3 - w['w2_0']/3 + 3], + [-w['w0_0']/3 - w['w1_0']/3 + w['w2_0']/3 + 4]]) + assert A * A.pinv() * B == B + # Overdetermined system (least squares results). + A = Matrix([[1, 0], [0, 0], [0, 1]]) + B = Matrix([3, 2, 1]) + assert A.pinv_solve(B) == Matrix([3, 1]) + # Proof the solution is not exact. + assert A * A.pinv() * B != B + +def test_pinv_rank_deficient(): + # Test the four properties of the pseudoinverse for various matrices. + As = [Matrix([[1, 1, 1], [2, 2, 2]]), + Matrix([[1, 0], [0, 0]]), + Matrix([[1, 2], [2, 4], [3, 6]])] + + for A in As: + A_pinv = A.pinv(method="RD") + AAp = A * A_pinv + ApA = A_pinv * A + assert simplify(AAp * A) == A + assert simplify(ApA * A_pinv) == A_pinv + assert AAp.H == AAp + assert ApA.H == ApA + + for A in As: + A_pinv = A.pinv(method="ED") + AAp = A * A_pinv + ApA = A_pinv * A + assert simplify(AAp * A) == A + assert simplify(ApA * A_pinv) == A_pinv + assert AAp.H == AAp + assert ApA.H == ApA + + # Test solving with rank-deficient matrices. + A = Matrix([[1, 0], [0, 0]]) + # Exact, non-unique solution. + B = Matrix([3, 0]) + solution = A.pinv_solve(B) + w1 = solution.atoms(Symbol).pop() + assert w1.name == 'w1_0' + assert solution == Matrix([3, w1]) + assert A * A.pinv() * B == B + # Least squares, non-unique solution. + B = Matrix([3, 1]) + solution = A.pinv_solve(B) + w1 = solution.atoms(Symbol).pop() + assert w1.name == 'w1_0' + assert solution == Matrix([3, w1]) + assert A * A.pinv() * B != B + +def test_gauss_jordan_solve(): + + # Square, full rank, unique solution + A = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 10]]) + b = Matrix([3, 6, 9]) + sol, params = A.gauss_jordan_solve(b) + assert sol == Matrix([[-1], [2], [0]]) + assert params == Matrix(0, 1, []) + + # Square, full rank, unique solution, B has more columns than rows + A = eye(3) + B = Matrix([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) + sol, params = A.gauss_jordan_solve(B) + assert sol == B + assert params == Matrix(0, 4, []) + + # Square, reduced rank, parametrized solution + A = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + b = Matrix([3, 6, 9]) + sol, params, freevar = A.gauss_jordan_solve(b, freevar=True) + w = {} + for s in sol.atoms(Symbol): + # Extract dummy symbols used in the solution. + w[s.name] = s + assert sol == Matrix([[w['tau0'] - 1], [-2*w['tau0'] + 2], [w['tau0']]]) + assert params == Matrix([[w['tau0']]]) + assert freevar == [2] + + # Square, reduced rank, parametrized solution, B has two columns + A = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + B = Matrix([[3, 4], [6, 8], [9, 12]]) + sol, params, freevar = A.gauss_jordan_solve(B, freevar=True) + w = {} + for s in sol.atoms(Symbol): + # Extract dummy symbols used in the solution. + w[s.name] = s + assert sol == Matrix([[w['tau0'] - 1, w['tau1'] - Rational(4, 3)], + [-2*w['tau0'] + 2, -2*w['tau1'] + Rational(8, 3)], + [w['tau0'], w['tau1']],]) + assert params == Matrix([[w['tau0'], w['tau1']]]) + assert freevar == [2] + + # Square, reduced rank, parametrized solution + A = Matrix([[1, 2, 3], [2, 4, 6], [3, 6, 9]]) + b = Matrix([0, 0, 0]) + sol, params = A.gauss_jordan_solve(b) + w = {} + for s in sol.atoms(Symbol): + w[s.name] = s + assert sol == Matrix([[-2*w['tau0'] - 3*w['tau1']], + [w['tau0']], [w['tau1']]]) + assert params == Matrix([[w['tau0']], [w['tau1']]]) + + # Square, reduced rank, parametrized solution + A = Matrix([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) + b = Matrix([0, 0, 0]) + sol, params = A.gauss_jordan_solve(b) + w = {} + for s in sol.atoms(Symbol): + w[s.name] = s + assert sol == Matrix([[w['tau0']], [w['tau1']], [w['tau2']]]) + assert params == Matrix([[w['tau0']], [w['tau1']], [w['tau2']]]) + + # Square, reduced rank, no solution + A = Matrix([[1, 2, 3], [2, 4, 6], [3, 6, 9]]) + b = Matrix([0, 0, 1]) + raises(ValueError, lambda: A.gauss_jordan_solve(b)) + + # Rectangular, tall, full rank, unique solution + A = Matrix([[1, 5, 3], [2, 1, 6], [1, 7, 9], [1, 4, 3]]) + b = Matrix([0, 0, 1, 0]) + sol, params = A.gauss_jordan_solve(b) + assert sol == Matrix([[Rational(-1, 2)], [0], [Rational(1, 6)]]) + assert params == Matrix(0, 1, []) + + # Rectangular, tall, full rank, unique solution, B has less columns than rows + A = Matrix([[1, 5, 3], [2, 1, 6], [1, 7, 9], [1, 4, 3]]) + B = Matrix([[0,0], [0, 0], [1, 2], [0, 0]]) + sol, params = A.gauss_jordan_solve(B) + assert sol == Matrix([[Rational(-1, 2), Rational(-2, 2)], [0, 0], [Rational(1, 6), Rational(2, 6)]]) + assert params == Matrix(0, 2, []) + + # Rectangular, tall, full rank, no solution + A = Matrix([[1, 5, 3], [2, 1, 6], [1, 7, 9], [1, 4, 3]]) + b = Matrix([0, 0, 0, 1]) + raises(ValueError, lambda: A.gauss_jordan_solve(b)) + + # Rectangular, tall, full rank, no solution, B has two columns (2nd has no solution) + A = Matrix([[1, 5, 3], [2, 1, 6], [1, 7, 9], [1, 4, 3]]) + B = Matrix([[0,0], [0, 0], [1, 0], [0, 1]]) + raises(ValueError, lambda: A.gauss_jordan_solve(B)) + + # Rectangular, tall, full rank, no solution, B has two columns (1st has no solution) + A = Matrix([[1, 5, 3], [2, 1, 6], [1, 7, 9], [1, 4, 3]]) + B = Matrix([[0,0], [0, 0], [0, 1], [1, 0]]) + raises(ValueError, lambda: A.gauss_jordan_solve(B)) + + # Rectangular, tall, reduced rank, parametrized solution + A = Matrix([[1, 5, 3], [2, 10, 6], [3, 15, 9], [1, 4, 3]]) + b = Matrix([0, 0, 0, 1]) + sol, params = A.gauss_jordan_solve(b) + w = {} + for s in sol.atoms(Symbol): + w[s.name] = s + assert sol == Matrix([[-3*w['tau0'] + 5], [-1], [w['tau0']]]) + assert params == Matrix([[w['tau0']]]) + + # Rectangular, tall, reduced rank, no solution + A = Matrix([[1, 5, 3], [2, 10, 6], [3, 15, 9], [1, 4, 3]]) + b = Matrix([0, 0, 1, 1]) + raises(ValueError, lambda: A.gauss_jordan_solve(b)) + + # Rectangular, wide, full rank, parametrized solution + A = Matrix([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 1, 12]]) + b = Matrix([1, 1, 1]) + sol, params = A.gauss_jordan_solve(b) + w = {} + for s in sol.atoms(Symbol): + w[s.name] = s + assert sol == Matrix([[2*w['tau0'] - 1], [-3*w['tau0'] + 1], [0], + [w['tau0']]]) + assert params == Matrix([[w['tau0']]]) + + # Rectangular, wide, reduced rank, parametrized solution + A = Matrix([[1, 2, 3, 4], [5, 6, 7, 8], [2, 4, 6, 8]]) + b = Matrix([0, 1, 0]) + sol, params = A.gauss_jordan_solve(b) + w = {} + for s in sol.atoms(Symbol): + w[s.name] = s + assert sol == Matrix([[w['tau0'] + 2*w['tau1'] + S.Half], + [-2*w['tau0'] - 3*w['tau1'] - Rational(1, 4)], + [w['tau0']], [w['tau1']]]) + assert params == Matrix([[w['tau0']], [w['tau1']]]) + # watch out for clashing symbols + x0, x1, x2, _x0 = symbols('_tau0 _tau1 _tau2 tau1') + M = Matrix([[0, 1, 0, 0, 0, 0], [0, 0, 0, 1, 0, _x0]]) + A = M[:, :-1] + b = M[:, -1:] + sol, params = A.gauss_jordan_solve(b) + assert params == Matrix(3, 1, [x0, x1, x2]) + assert sol == Matrix(5, 1, [x0, 0, x1, _x0, x2]) + + # Rectangular, wide, reduced rank, no solution + A = Matrix([[1, 2, 3, 4], [5, 6, 7, 8], [2, 4, 6, 8]]) + b = Matrix([1, 1, 1]) + raises(ValueError, lambda: A.gauss_jordan_solve(b)) + + # Test for immutable matrix + A = ImmutableMatrix([[1, 0], [0, 1]]) + B = ImmutableMatrix([1, 2]) + sol, params = A.gauss_jordan_solve(B) + assert sol == ImmutableMatrix([1, 2]) + assert params == ImmutableMatrix(0, 1, []) + assert sol.__class__ == ImmutableDenseMatrix + assert params.__class__ == ImmutableDenseMatrix + + # Test placement of free variables + A = Matrix([[1, 0, 0, 0], [0, 0, 0, 1]]) + b = Matrix([1, 1]) + sol, params = A.gauss_jordan_solve(b) + w = {} + for s in sol.atoms(Symbol): + w[s.name] = s + assert sol == Matrix([[1], [w['tau0']], [w['tau1']], [1]]) + assert params == Matrix([[w['tau0']], [w['tau1']]]) + + +def test_linsolve_underdetermined_AND_gauss_jordan_solve(): + #Test placement of free variables as per issue 19815 + A = Matrix([[1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1]]) + B = Matrix([1, 2, 1, 1, 1, 1, 1, 2]) + sol, params = A.gauss_jordan_solve(B) + w = {} + for s in sol.atoms(Symbol): + w[s.name] = s + assert params == Matrix([[w['tau0']], [w['tau1']], [w['tau2']], + [w['tau3']], [w['tau4']], [w['tau5']]]) + assert sol == Matrix([[1 - 1*w['tau2']], + [w['tau2']], + [1 - 1*w['tau0'] + w['tau1']], + [w['tau0']], + [w['tau3'] + w['tau4']], + [-1*w['tau3'] - 1*w['tau4'] - 1*w['tau1']], + [1 - 1*w['tau2']], + [w['tau1']], + [w['tau2']], + [w['tau3']], + [w['tau4']], + [1 - 1*w['tau5']], + [w['tau5']], + [1]]) + + from sympy.abc import j,f + # https://github.com/sympy/sympy/issues/20046 + A = Matrix([ + [1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, -1, 0, -1, 0, -1, 0, -1, -j], + [0, 0, 0, 0, 1, 1, 1, 1, f] + ]) + + sol_1=Matrix(list(linsolve(A))[0]) + + tau0, tau1, tau2, tau3, tau4 = symbols('tau:5') + + assert sol_1 == Matrix([[-f - j - tau0 + tau2 + tau4 + 1], + [j - tau1 - tau2 - tau4], + [tau0], + [tau1], + [f - tau2 - tau3 - tau4], + [tau2], + [tau3], + [tau4]]) + + # https://github.com/sympy/sympy/issues/19815 + sol_2 = A[:, : -1 ] * sol_1 - A[:, -1 ] + assert sol_2 == Matrix([[0], [0], [0]]) + + +@pytest.mark.parametrize("det_method", ["bird", "laplace"]) +@pytest.mark.parametrize("M, rhs", [ + (Matrix([[2, 3, 5], [3, 6, 2], [8, 3, 6]]), Matrix(3, 1, [3, 7, 5])), + (Matrix([[2, 3, 5], [3, 6, 2], [8, 3, 6]]), + Matrix([[1, 2], [3, 4], [5, 6]])), + (Matrix(2, 2, symbols("a:4")), Matrix(2, 1, symbols("b:2"))), +]) +def test_cramer_solve(det_method, M, rhs): + assert simplify(M.cramer_solve(rhs, det_method=det_method) - M.LUsolve(rhs) + ) == Matrix.zeros(M.rows, rhs.cols) + + +@pytest.mark.parametrize("det_method, error", [ + ("bird", DMShapeError), (_det_laplace, NonSquareMatrixError)]) +def test_cramer_solve_errors(det_method, error): + # Non-square matrix + A = Matrix([[0, -1, 2], [5, 10, 7]]) + b = Matrix([-2, 15]) + raises(error, lambda: A.cramer_solve(b, det_method=det_method)) + + +def test_solve(): + A = Matrix([[1,2], [2,4]]) + b = Matrix([[3], [4]]) + raises(ValueError, lambda: A.solve(b)) #no solution + b = Matrix([[ 4], [8]]) + raises(ValueError, lambda: A.solve(b)) #infinite solution diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_sparse.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_sparse.py new file mode 100644 index 0000000000000000000000000000000000000000..4d257c8062f220cc06bc0dabdc7ac40ce9dc4adc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_sparse.py @@ -0,0 +1,745 @@ +from sympy.core.numbers import (Float, I, Rational) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import Abs +from sympy.polys.polytools import PurePoly +from sympy.matrices import \ + Matrix, MutableSparseMatrix, ImmutableSparseMatrix, SparseMatrix, eye, \ + ones, zeros, ShapeError, NonSquareMatrixError +from sympy.testing.pytest import raises + + +def test_sparse_creation(): + a = SparseMatrix(2, 2, {(0, 0): [[1, 2], [3, 4]]}) + assert a == SparseMatrix([[1, 2], [3, 4]]) + a = SparseMatrix(2, 2, {(0, 0): [[1, 2]]}) + assert a == SparseMatrix([[1, 2], [0, 0]]) + a = SparseMatrix(2, 2, {(0, 0): [1, 2]}) + assert a == SparseMatrix([[1, 0], [2, 0]]) + + +def test_sparse_matrix(): + def sparse_eye(n): + return SparseMatrix.eye(n) + + def sparse_zeros(n): + return SparseMatrix.zeros(n) + + # creation args + raises(TypeError, lambda: SparseMatrix(1, 2)) + + a = SparseMatrix(( + (1, 0), + (0, 1) + )) + assert SparseMatrix(a) == a + + from sympy.matrices import MutableDenseMatrix + a = MutableSparseMatrix([]) + b = MutableDenseMatrix([1, 2]) + assert a.row_join(b) == b + assert a.col_join(b) == b + assert type(a.row_join(b)) == type(a) + assert type(a.col_join(b)) == type(a) + + # make sure 0 x n matrices get stacked correctly + sparse_matrices = [SparseMatrix.zeros(0, n) for n in range(4)] + assert SparseMatrix.hstack(*sparse_matrices) == Matrix(0, 6, []) + sparse_matrices = [SparseMatrix.zeros(n, 0) for n in range(4)] + assert SparseMatrix.vstack(*sparse_matrices) == Matrix(6, 0, []) + + # test element assignment + a = SparseMatrix(( + (1, 0), + (0, 1) + )) + + a[3] = 4 + assert a[1, 1] == 4 + a[3] = 1 + + a[0, 0] = 2 + assert a == SparseMatrix(( + (2, 0), + (0, 1) + )) + a[1, 0] = 5 + assert a == SparseMatrix(( + (2, 0), + (5, 1) + )) + a[1, 1] = 0 + assert a == SparseMatrix(( + (2, 0), + (5, 0) + )) + assert a.todok() == {(0, 0): 2, (1, 0): 5} + + # test_multiplication + a = SparseMatrix(( + (1, 2), + (3, 1), + (0, 6), + )) + + b = SparseMatrix(( + (1, 2), + (3, 0), + )) + + c = a*b + assert c[0, 0] == 7 + assert c[0, 1] == 2 + assert c[1, 0] == 6 + assert c[1, 1] == 6 + assert c[2, 0] == 18 + assert c[2, 1] == 0 + + try: + eval('c = a @ b') + except SyntaxError: + pass + else: + assert c[0, 0] == 7 + assert c[0, 1] == 2 + assert c[1, 0] == 6 + assert c[1, 1] == 6 + assert c[2, 0] == 18 + assert c[2, 1] == 0 + + x = Symbol("x") + + c = b * Symbol("x") + assert isinstance(c, SparseMatrix) + assert c[0, 0] == x + assert c[0, 1] == 2*x + assert c[1, 0] == 3*x + assert c[1, 1] == 0 + + c = 5 * b + assert isinstance(c, SparseMatrix) + assert c[0, 0] == 5 + assert c[0, 1] == 2*5 + assert c[1, 0] == 3*5 + assert c[1, 1] == 0 + + #test_power + A = SparseMatrix([[2, 3], [4, 5]]) + assert (A**5)[:] == [6140, 8097, 10796, 14237] + A = SparseMatrix([[2, 1, 3], [4, 2, 4], [6, 12, 1]]) + assert (A**3)[:] == [290, 262, 251, 448, 440, 368, 702, 954, 433] + + # test_creation + x = Symbol("x") + a = SparseMatrix([[x, 0], [0, 0]]) + m = a + assert m.cols == m.rows + assert m.cols == 2 + assert m[:] == [x, 0, 0, 0] + b = SparseMatrix(2, 2, [x, 0, 0, 0]) + m = b + assert m.cols == m.rows + assert m.cols == 2 + assert m[:] == [x, 0, 0, 0] + + assert a == b + S = sparse_eye(3) + S.row_del(1) + assert S == SparseMatrix([ + [1, 0, 0], + [0, 0, 1]]) + S = sparse_eye(3) + S.col_del(1) + assert S == SparseMatrix([ + [1, 0], + [0, 0], + [0, 1]]) + S = SparseMatrix.eye(3) + S[2, 1] = 2 + S.col_swap(1, 0) + assert S == SparseMatrix([ + [0, 1, 0], + [1, 0, 0], + [2, 0, 1]]) + S.row_swap(0, 1) + assert S == SparseMatrix([ + [1, 0, 0], + [0, 1, 0], + [2, 0, 1]]) + + a = SparseMatrix(1, 2, [1, 2]) + b = a.copy() + c = a.copy() + assert a[0] == 1 + a.row_del(0) + assert a == SparseMatrix(0, 2, []) + b.col_del(1) + assert b == SparseMatrix(1, 1, [1]) + + assert SparseMatrix([[1, 2, 3], [1, 2], [1]]) == Matrix([ + [1, 2, 3], + [1, 2, 0], + [1, 0, 0]]) + assert SparseMatrix(4, 4, {(1, 1): sparse_eye(2)}) == Matrix([ + [0, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 0]]) + raises(ValueError, lambda: SparseMatrix(1, 1, {(1, 1): 1})) + assert SparseMatrix(1, 2, [1, 2]).tolist() == [[1, 2]] + assert SparseMatrix(2, 2, [1, [2, 3]]).tolist() == [[1, 0], [2, 3]] + raises(ValueError, lambda: SparseMatrix(2, 2, [1])) + raises(ValueError, lambda: SparseMatrix(1, 1, [[1, 2]])) + assert SparseMatrix([.1]).has(Float) + # autosizing + assert SparseMatrix(None, {(0, 1): 0}).shape == (0, 0) + assert SparseMatrix(None, {(0, 1): 1}).shape == (1, 2) + assert SparseMatrix(None, None, {(0, 1): 1}).shape == (1, 2) + raises(ValueError, lambda: SparseMatrix(None, 1, [[1, 2]])) + raises(ValueError, lambda: SparseMatrix(1, None, [[1, 2]])) + raises(ValueError, lambda: SparseMatrix(3, 3, {(0, 0): ones(2), (1, 1): 2})) + + # test_determinant + x, y = Symbol('x'), Symbol('y') + + assert SparseMatrix(1, 1, [0]).det() == 0 + + assert SparseMatrix([[1]]).det() == 1 + + assert SparseMatrix(((-3, 2), (8, -5))).det() == -1 + + assert SparseMatrix(((x, 1), (y, 2*y))).det() == 2*x*y - y + + assert SparseMatrix(( (1, 1, 1), + (1, 2, 3), + (1, 3, 6) )).det() == 1 + + assert SparseMatrix(( ( 3, -2, 0, 5), + (-2, 1, -2, 2), + ( 0, -2, 5, 0), + ( 5, 0, 3, 4) )).det() == -289 + + assert SparseMatrix(( ( 1, 2, 3, 4), + ( 5, 6, 7, 8), + ( 9, 10, 11, 12), + (13, 14, 15, 16) )).det() == 0 + + assert SparseMatrix(( (3, 2, 0, 0, 0), + (0, 3, 2, 0, 0), + (0, 0, 3, 2, 0), + (0, 0, 0, 3, 2), + (2, 0, 0, 0, 3) )).det() == 275 + + assert SparseMatrix(( (1, 0, 1, 2, 12), + (2, 0, 1, 1, 4), + (2, 1, 1, -1, 3), + (3, 2, -1, 1, 8), + (1, 1, 1, 0, 6) )).det() == -55 + + assert SparseMatrix(( (-5, 2, 3, 4, 5), + ( 1, -4, 3, 4, 5), + ( 1, 2, -3, 4, 5), + ( 1, 2, 3, -2, 5), + ( 1, 2, 3, 4, -1) )).det() == 11664 + + assert SparseMatrix(( ( 3, 0, 0, 0), + (-2, 1, 0, 0), + ( 0, -2, 5, 0), + ( 5, 0, 3, 4) )).det() == 60 + + assert SparseMatrix(( ( 1, 0, 0, 0), + ( 5, 0, 0, 0), + ( 9, 10, 11, 0), + (13, 14, 15, 16) )).det() == 0 + + assert SparseMatrix(( (3, 2, 0, 0, 0), + (0, 3, 2, 0, 0), + (0, 0, 3, 2, 0), + (0, 0, 0, 3, 2), + (0, 0, 0, 0, 3) )).det() == 243 + + assert SparseMatrix(( ( 2, 7, -1, 3, 2), + ( 0, 0, 1, 0, 1), + (-2, 0, 7, 0, 2), + (-3, -2, 4, 5, 3), + ( 1, 0, 0, 0, 1) )).det() == 123 + + # test_slicing + m0 = sparse_eye(4) + assert m0[:3, :3] == sparse_eye(3) + assert m0[2:4, 0:2] == sparse_zeros(2) + + m1 = SparseMatrix(3, 3, lambda i, j: i + j) + assert m1[0, :] == SparseMatrix(1, 3, (0, 1, 2)) + assert m1[1:3, 1] == SparseMatrix(2, 1, (2, 3)) + + m2 = SparseMatrix( + [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]]) + assert m2[:, -1] == SparseMatrix(4, 1, [3, 7, 11, 15]) + assert m2[-2:, :] == SparseMatrix([[8, 9, 10, 11], [12, 13, 14, 15]]) + + assert SparseMatrix([[1, 2], [3, 4]])[[1], [1]] == Matrix([[4]]) + + # test_submatrix_assignment + m = sparse_zeros(4) + m[2:4, 2:4] = sparse_eye(2) + assert m == SparseMatrix([(0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 1, 0), + (0, 0, 0, 1)]) + assert len(m.todok()) == 2 + m[:2, :2] = sparse_eye(2) + assert m == sparse_eye(4) + m[:, 0] = SparseMatrix(4, 1, (1, 2, 3, 4)) + assert m == SparseMatrix([(1, 0, 0, 0), + (2, 1, 0, 0), + (3, 0, 1, 0), + (4, 0, 0, 1)]) + m[:, :] = sparse_zeros(4) + assert m == sparse_zeros(4) + m[:, :] = ((1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12), (13, 14, 15, 16)) + assert m == SparseMatrix((( 1, 2, 3, 4), + ( 5, 6, 7, 8), + ( 9, 10, 11, 12), + (13, 14, 15, 16))) + m[:2, 0] = [0, 0] + assert m == SparseMatrix((( 0, 2, 3, 4), + ( 0, 6, 7, 8), + ( 9, 10, 11, 12), + (13, 14, 15, 16))) + + # test_reshape + m0 = sparse_eye(3) + assert m0.reshape(1, 9) == SparseMatrix(1, 9, (1, 0, 0, 0, 1, 0, 0, 0, 1)) + m1 = SparseMatrix(3, 4, lambda i, j: i + j) + assert m1.reshape(4, 3) == \ + SparseMatrix([(0, 1, 2), (3, 1, 2), (3, 4, 2), (3, 4, 5)]) + assert m1.reshape(2, 6) == \ + SparseMatrix([(0, 1, 2, 3, 1, 2), (3, 4, 2, 3, 4, 5)]) + + # test_applyfunc + m0 = sparse_eye(3) + assert m0.applyfunc(lambda x: 2*x) == sparse_eye(3)*2 + assert m0.applyfunc(lambda x: 0 ) == sparse_zeros(3) + + # test__eval_Abs + assert abs(SparseMatrix(((x, 1), (y, 2*y)))) == SparseMatrix(((Abs(x), 1), (Abs(y), 2*Abs(y)))) + + # test_LUdecomp + testmat = SparseMatrix([[ 0, 2, 5, 3], + [ 3, 3, 7, 4], + [ 8, 4, 0, 2], + [-2, 6, 3, 4]]) + L, U, p = testmat.LUdecomposition() + assert L.is_lower + assert U.is_upper + assert (L*U).permute_rows(p, 'backward') - testmat == sparse_zeros(4) + + testmat = SparseMatrix([[ 6, -2, 7, 4], + [ 0, 3, 6, 7], + [ 1, -2, 7, 4], + [-9, 2, 6, 3]]) + L, U, p = testmat.LUdecomposition() + assert L.is_lower + assert U.is_upper + assert (L*U).permute_rows(p, 'backward') - testmat == sparse_zeros(4) + + x, y, z = Symbol('x'), Symbol('y'), Symbol('z') + M = Matrix(((1, x, 1), (2, y, 0), (y, 0, z))) + L, U, p = M.LUdecomposition() + assert L.is_lower + assert U.is_upper + assert (L*U).permute_rows(p, 'backward') - M == sparse_zeros(3) + + # test_LUsolve + A = SparseMatrix([[2, 3, 5], + [3, 6, 2], + [8, 3, 6]]) + x = SparseMatrix(3, 1, [3, 7, 5]) + b = A*x + soln = A.LUsolve(b) + assert soln == x + A = SparseMatrix([[0, -1, 2], + [5, 10, 7], + [8, 3, 4]]) + x = SparseMatrix(3, 1, [-1, 2, 5]) + b = A*x + soln = A.LUsolve(b) + assert soln == x + + # test_inverse + A = sparse_eye(4) + assert A.inv() == sparse_eye(4) + assert A.inv(method="CH") == sparse_eye(4) + assert A.inv(method="LDL") == sparse_eye(4) + + A = SparseMatrix([[2, 3, 5], + [3, 6, 2], + [7, 2, 6]]) + Ainv = SparseMatrix(Matrix(A).inv()) + assert A*Ainv == sparse_eye(3) + assert A.inv(method="CH") == Ainv + assert A.inv(method="LDL") == Ainv + + A = SparseMatrix([[2, 3, 5], + [3, 6, 2], + [5, 2, 6]]) + Ainv = SparseMatrix(Matrix(A).inv()) + assert A*Ainv == sparse_eye(3) + assert A.inv(method="CH") == Ainv + assert A.inv(method="LDL") == Ainv + + # test_cross + v1 = Matrix(1, 3, [1, 2, 3]) + v2 = Matrix(1, 3, [3, 4, 5]) + assert v1.cross(v2) == Matrix(1, 3, [-2, 4, -2]) + assert v1.norm(2)**2 == 14 + + # conjugate + a = SparseMatrix(((1, 2 + I), (3, 4))) + assert a.C == SparseMatrix([ + [1, 2 - I], + [3, 4] + ]) + + # mul + assert a*Matrix(2, 2, [1, 0, 0, 1]) == a + assert a + Matrix(2, 2, [1, 1, 1, 1]) == SparseMatrix([ + [2, 3 + I], + [4, 5] + ]) + + # col join + assert a.col_join(sparse_eye(2)) == SparseMatrix([ + [1, 2 + I], + [3, 4], + [1, 0], + [0, 1] + ]) + + # row insert + assert a.row_insert(2, sparse_eye(2)) == SparseMatrix([ + [1, 2 + I], + [3, 4], + [1, 0], + [0, 1] + ]) + + # col insert + assert a.col_insert(2, SparseMatrix.zeros(2, 1)) == SparseMatrix([ + [1, 2 + I, 0], + [3, 4, 0], + ]) + + # symmetric + assert not a.is_symmetric(simplify=False) + + # col op + M = SparseMatrix.eye(3)*2 + M[1, 0] = -1 + M.col_op(1, lambda v, i: v + 2*M[i, 0]) + assert M == SparseMatrix([ + [ 2, 4, 0], + [-1, 0, 0], + [ 0, 0, 2] + ]) + + # fill + M = SparseMatrix.eye(3) + M.fill(2) + assert M == SparseMatrix([ + [2, 2, 2], + [2, 2, 2], + [2, 2, 2], + ]) + + # test_cofactor + assert sparse_eye(3) == sparse_eye(3).cofactor_matrix() + test = SparseMatrix([[1, 3, 2], [2, 6, 3], [2, 3, 6]]) + assert test.cofactor_matrix() == \ + SparseMatrix([[27, -6, -6], [-12, 2, 3], [-3, 1, 0]]) + test = SparseMatrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + assert test.cofactor_matrix() == \ + SparseMatrix([[-3, 6, -3], [6, -12, 6], [-3, 6, -3]]) + + # test_jacobian + x = Symbol('x') + y = Symbol('y') + L = SparseMatrix(1, 2, [x**2*y, 2*y**2 + x*y]) + syms = [x, y] + assert L.jacobian(syms) == Matrix([[2*x*y, x**2], [y, 4*y + x]]) + + L = SparseMatrix(1, 2, [x, x**2*y**3]) + assert L.jacobian(syms) == SparseMatrix([[1, 0], [2*x*y**3, x**2*3*y**2]]) + + # test_QR + A = Matrix([[1, 2], [2, 3]]) + Q, S = A.QRdecomposition() + R = Rational + assert Q == Matrix([ + [ 5**R(-1, 2), (R(2)/5)*(R(1)/5)**R(-1, 2)], + [2*5**R(-1, 2), (-R(1)/5)*(R(1)/5)**R(-1, 2)]]) + assert S == Matrix([ + [5**R(1, 2), 8*5**R(-1, 2)], + [ 0, (R(1)/5)**R(1, 2)]]) + assert Q*S == A + assert Q.T * Q == sparse_eye(2) + + R = Rational + # test nullspace + # first test reduced row-ech form + + M = SparseMatrix([[5, 7, 2, 1], + [1, 6, 2, -1]]) + out, tmp = M.rref() + assert out == Matrix([[1, 0, -R(2)/23, R(13)/23], + [0, 1, R(8)/23, R(-6)/23]]) + + M = SparseMatrix([[ 1, 3, 0, 2, 6, 3, 1], + [-2, -6, 0, -2, -8, 3, 1], + [ 3, 9, 0, 0, 6, 6, 2], + [-1, -3, 0, 1, 0, 9, 3]]) + + out, tmp = M.rref() + assert out == Matrix([[1, 3, 0, 0, 2, 0, 0], + [0, 0, 0, 1, 2, 0, 0], + [0, 0, 0, 0, 0, 1, R(1)/3], + [0, 0, 0, 0, 0, 0, 0]]) + # now check the vectors + basis = M.nullspace() + assert basis[0] == Matrix([-3, 1, 0, 0, 0, 0, 0]) + assert basis[1] == Matrix([0, 0, 1, 0, 0, 0, 0]) + assert basis[2] == Matrix([-2, 0, 0, -2, 1, 0, 0]) + assert basis[3] == Matrix([0, 0, 0, 0, 0, R(-1)/3, 1]) + + # test eigen + x = Symbol('x') + y = Symbol('y') + sparse_eye3 = sparse_eye(3) + assert sparse_eye3.charpoly(x) == PurePoly((x - 1)**3) + assert sparse_eye3.charpoly(y) == PurePoly((y - 1)**3) + + # test values + M = Matrix([( 0, 1, -1), + ( 1, 1, 0), + (-1, 0, 1)]) + vals = M.eigenvals() + assert sorted(vals.keys()) == [-1, 1, 2] + + R = Rational + M = Matrix([[1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + assert M.eigenvects() == [(1, 3, [ + Matrix([1, 0, 0]), + Matrix([0, 1, 0]), + Matrix([0, 0, 1])])] + M = Matrix([[5, 0, 2], + [3, 2, 0], + [0, 0, 1]]) + assert M.eigenvects() == [(1, 1, [Matrix([R(-1)/2, R(3)/2, 1])]), + (2, 1, [Matrix([0, 1, 0])]), + (5, 1, [Matrix([1, 1, 0])])] + + assert M.zeros(3, 5) == SparseMatrix(3, 5, {}) + A = SparseMatrix(10, 10, {(0, 0): 18, (0, 9): 12, (1, 4): 18, (2, 7): 16, (3, 9): 12, (4, 2): 19, (5, 7): 16, (6, 2): 12, (9, 7): 18}) + assert A.row_list() == [(0, 0, 18), (0, 9, 12), (1, 4, 18), (2, 7, 16), (3, 9, 12), (4, 2, 19), (5, 7, 16), (6, 2, 12), (9, 7, 18)] + assert A.col_list() == [(0, 0, 18), (4, 2, 19), (6, 2, 12), (1, 4, 18), (2, 7, 16), (5, 7, 16), (9, 7, 18), (0, 9, 12), (3, 9, 12)] + assert SparseMatrix.eye(2).nnz() == 2 + + +def test_scalar_multiply(): + assert SparseMatrix([[1, 2]]).scalar_multiply(3) == SparseMatrix([[3, 6]]) + + +def test_transpose(): + assert SparseMatrix(((1, 2), (3, 4))).transpose() == \ + SparseMatrix(((1, 3), (2, 4))) + + +def test_trace(): + assert SparseMatrix(((1, 2), (3, 4))).trace() == 5 + assert SparseMatrix(((0, 0), (0, 4))).trace() == 4 + + +def test_CL_RL(): + assert SparseMatrix(((1, 2), (3, 4))).row_list() == \ + [(0, 0, 1), (0, 1, 2), (1, 0, 3), (1, 1, 4)] + assert SparseMatrix(((1, 2), (3, 4))).col_list() == \ + [(0, 0, 1), (1, 0, 3), (0, 1, 2), (1, 1, 4)] + + +def test_add(): + assert SparseMatrix(((1, 0), (0, 1))) + SparseMatrix(((0, 1), (1, 0))) == \ + SparseMatrix(((1, 1), (1, 1))) + a = SparseMatrix(100, 100, lambda i, j: int(j != 0 and i % j == 0)) + b = SparseMatrix(100, 100, lambda i, j: int(i != 0 and j % i == 0)) + assert (len(a.todok()) + len(b.todok()) - len((a + b).todok()) > 0) + + +def test_errors(): + raises(ValueError, lambda: SparseMatrix(1.4, 2, lambda i, j: 0)) + raises(TypeError, lambda: SparseMatrix([1, 2, 3], [1, 2])) + raises(ValueError, lambda: SparseMatrix([[1, 2], [3, 4]])[(1, 2, 3)]) + raises(IndexError, lambda: SparseMatrix([[1, 2], [3, 4]])[5]) + raises(ValueError, lambda: SparseMatrix([[1, 2], [3, 4]])[1, 2, 3]) + raises(TypeError, + lambda: SparseMatrix([[1, 2], [3, 4]]).copyin_list([0, 1], set())) + raises( + IndexError, lambda: SparseMatrix([[1, 2], [3, 4]])[1, 2]) + raises(TypeError, lambda: SparseMatrix([1, 2, 3]).cross(1)) + raises(IndexError, lambda: SparseMatrix(1, 2, [1, 2])[3]) + raises(ShapeError, + lambda: SparseMatrix(1, 2, [1, 2]) + SparseMatrix(2, 1, [2, 1])) + + +def test_len(): + assert not SparseMatrix() + assert SparseMatrix() == SparseMatrix([]) + assert SparseMatrix() == SparseMatrix([[]]) + + +def test_sparse_zeros_sparse_eye(): + assert SparseMatrix.eye(3) == eye(3, cls=SparseMatrix) + assert len(SparseMatrix.eye(3).todok()) == 3 + assert SparseMatrix.zeros(3) == zeros(3, cls=SparseMatrix) + assert len(SparseMatrix.zeros(3).todok()) == 0 + + +def test_copyin(): + s = SparseMatrix(3, 3, {}) + s[1, 0] = 1 + assert s[:, 0] == SparseMatrix(Matrix([0, 1, 0])) + assert s[3] == 1 + assert s[3: 4] == [1] + s[1, 1] = 42 + assert s[1, 1] == 42 + assert s[1, 1:] == SparseMatrix([[42, 0]]) + s[1, 1:] = Matrix([[5, 6]]) + assert s[1, :] == SparseMatrix([[1, 5, 6]]) + s[1, 1:] = [[42, 43]] + assert s[1, :] == SparseMatrix([[1, 42, 43]]) + s[0, 0] = 17 + assert s[:, :1] == SparseMatrix([17, 1, 0]) + s[0, 0] = [1, 1, 1] + assert s[:, 0] == SparseMatrix([1, 1, 1]) + s[0, 0] = Matrix([1, 1, 1]) + assert s[:, 0] == SparseMatrix([1, 1, 1]) + s[0, 0] = SparseMatrix([1, 1, 1]) + assert s[:, 0] == SparseMatrix([1, 1, 1]) + + +def test_sparse_solve(): + A = SparseMatrix(((25, 15, -5), (15, 18, 0), (-5, 0, 11))) + assert A.cholesky() == Matrix([ + [ 5, 0, 0], + [ 3, 3, 0], + [-1, 1, 3]]) + assert A.cholesky() * A.cholesky().T == Matrix([ + [25, 15, -5], + [15, 18, 0], + [-5, 0, 11]]) + + A = SparseMatrix(((25, 15, -5), (15, 18, 0), (-5, 0, 11))) + L, D = A.LDLdecomposition() + assert 15*L == Matrix([ + [15, 0, 0], + [ 9, 15, 0], + [-3, 5, 15]]) + assert D == Matrix([ + [25, 0, 0], + [ 0, 9, 0], + [ 0, 0, 9]]) + assert L * D * L.T == A + + A = SparseMatrix(((3, 0, 2), (0, 0, 1), (1, 2, 0))) + assert A.inv() * A == SparseMatrix(eye(3)) + + A = SparseMatrix([ + [ 2, -1, 0], + [-1, 2, -1], + [ 0, 0, 2]]) + ans = SparseMatrix([ + [Rational(2, 3), Rational(1, 3), Rational(1, 6)], + [Rational(1, 3), Rational(2, 3), Rational(1, 3)], + [ 0, 0, S.Half]]) + assert A.inv(method='CH') == ans + assert A.inv(method='LDL') == ans + assert A * ans == SparseMatrix(eye(3)) + + s = A.solve(A[:, 0], 'LDL') + assert A*s == A[:, 0] + s = A.solve(A[:, 0], 'CH') + assert A*s == A[:, 0] + A = A.col_join(A) + s = A.solve_least_squares(A[:, 0], 'CH') + assert A*s == A[:, 0] + s = A.solve_least_squares(A[:, 0], 'LDL') + assert A*s == A[:, 0] + + +def test_lower_triangular_solve(): + raises(NonSquareMatrixError, lambda: + SparseMatrix([[1, 2]]).lower_triangular_solve(Matrix([[1, 2]]))) + raises(ShapeError, lambda: + SparseMatrix([[1, 2], [0, 4]]).lower_triangular_solve(Matrix([1]))) + raises(ValueError, lambda: + SparseMatrix([[1, 2], [3, 4]]).lower_triangular_solve(Matrix([[1, 2], [3, 4]]))) + + a, b, c, d = symbols('a:d') + u, v, w, x = symbols('u:x') + + A = SparseMatrix([[a, 0], [c, d]]) + B = MutableSparseMatrix([[u, v], [w, x]]) + C = ImmutableSparseMatrix([[u, v], [w, x]]) + + sol = Matrix([[u/a, v/a], [(w - c*u/a)/d, (x - c*v/a)/d]]) + assert A.lower_triangular_solve(B) == sol + assert A.lower_triangular_solve(C) == sol + + +def test_upper_triangular_solve(): + raises(NonSquareMatrixError, lambda: + SparseMatrix([[1, 2]]).upper_triangular_solve(Matrix([[1, 2]]))) + raises(ShapeError, lambda: + SparseMatrix([[1, 2], [0, 4]]).upper_triangular_solve(Matrix([1]))) + raises(TypeError, lambda: + SparseMatrix([[1, 2], [3, 4]]).upper_triangular_solve(Matrix([[1, 2], [3, 4]]))) + + a, b, c, d = symbols('a:d') + u, v, w, x = symbols('u:x') + + A = SparseMatrix([[a, b], [0, d]]) + B = MutableSparseMatrix([[u, v], [w, x]]) + C = ImmutableSparseMatrix([[u, v], [w, x]]) + + sol = Matrix([[(u - b*w/d)/a, (v - b*x/d)/a], [w/d, x/d]]) + assert A.upper_triangular_solve(B) == sol + assert A.upper_triangular_solve(C) == sol + + +def test_diagonal_solve(): + a, d = symbols('a d') + u, v, w, x = symbols('u:x') + + A = SparseMatrix([[a, 0], [0, d]]) + B = MutableSparseMatrix([[u, v], [w, x]]) + C = ImmutableSparseMatrix([[u, v], [w, x]]) + + sol = Matrix([[u/a, v/a], [w/d, x/d]]) + assert A.diagonal_solve(B) == sol + assert A.diagonal_solve(C) == sol + + +def test_hermitian(): + x = Symbol('x') + a = SparseMatrix([[0, I], [-I, 0]]) + assert a.is_hermitian + a = SparseMatrix([[1, I], [-I, 1]]) + assert a.is_hermitian + a[0, 0] = 2*I + assert a.is_hermitian is False + a[0, 0] = x + assert a.is_hermitian is None + a[0, 1] = a[1, 0]*I + assert a.is_hermitian is False diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_sparsetools.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_sparsetools.py new file mode 100644 index 0000000000000000000000000000000000000000..244944c31da06460d4bc7beff8bce0f91fea9f14 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_sparsetools.py @@ -0,0 +1,132 @@ +from sympy.matrices.sparsetools import _doktocsr, _csrtodok, banded +from sympy.matrices.dense import (Matrix, eye, ones, zeros) +from sympy.matrices import SparseMatrix +from sympy.testing.pytest import raises + + +def test_doktocsr(): + a = SparseMatrix([[1, 2, 0, 0], [0, 3, 9, 0], [0, 1, 4, 0]]) + b = SparseMatrix(4, 6, [10, 20, 0, 0, 0, 0, 0, 30, 0, 40, 0, 0, 0, 0, 50, + 60, 70, 0, 0, 0, 0, 0, 0, 80]) + c = SparseMatrix(4, 4, [0, 0, 0, 0, 0, 12, 0, 2, 15, 0, 12, 0, 0, 0, 0, 4]) + d = SparseMatrix(10, 10, {(1, 1): 12, (3, 5): 7, (7, 8): 12}) + e = SparseMatrix([[0, 0, 0], [1, 0, 2], [3, 0, 0]]) + f = SparseMatrix(7, 8, {(2, 3): 5, (4, 5):12}) + assert _doktocsr(a) == [[1, 2, 3, 9, 1, 4], [0, 1, 1, 2, 1, 2], + [0, 2, 4, 6], [3, 4]] + assert _doktocsr(b) == [[10, 20, 30, 40, 50, 60, 70, 80], + [0, 1, 1, 3, 2, 3, 4, 5], [0, 2, 4, 7, 8], [4, 6]] + assert _doktocsr(c) == [[12, 2, 15, 12, 4], [1, 3, 0, 2, 3], + [0, 0, 2, 4, 5], [4, 4]] + assert _doktocsr(d) == [[12, 7, 12], [1, 5, 8], + [0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3], [10, 10]] + assert _doktocsr(e) == [[1, 2, 3], [0, 2, 0], [0, 0, 2, 3], [3, 3]] + assert _doktocsr(f) == [[5, 12], [3, 5], [0, 0, 0, 1, 1, 2, 2, 2], [7, 8]] + + +def test_csrtodok(): + h = [[5, 7, 5], [2, 1, 3], [0, 1, 1, 3], [3, 4]] + g = [[12, 5, 4], [2, 4, 2], [0, 1, 2, 3], [3, 7]] + i = [[1, 3, 12], [0, 2, 4], [0, 2, 3], [2, 5]] + j = [[11, 15, 12, 15], [2, 4, 1, 2], [0, 1, 1, 2, 3, 4], [5, 8]] + k = [[1, 3], [2, 1], [0, 1, 1, 2], [3, 3]] + m = _csrtodok(h) + assert isinstance(m, SparseMatrix) + assert m == SparseMatrix(3, 4, + {(0, 2): 5, (2, 1): 7, (2, 3): 5}) + assert _csrtodok(g) == SparseMatrix(3, 7, + {(0, 2): 12, (1, 4): 5, (2, 2): 4}) + assert _csrtodok(i) == SparseMatrix([[1, 0, 3, 0, 0], [0, 0, 0, 0, 12]]) + assert _csrtodok(j) == SparseMatrix(5, 8, + {(0, 2): 11, (2, 4): 15, (3, 1): 12, (4, 2): 15}) + assert _csrtodok(k) == SparseMatrix(3, 3, {(0, 2): 1, (2, 1): 3}) + + +def test_banded(): + raises(TypeError, lambda: banded()) + raises(TypeError, lambda: banded(1)) + raises(TypeError, lambda: banded(1, 2)) + raises(TypeError, lambda: banded(1, 2, 3)) + raises(TypeError, lambda: banded(1, 2, 3, 4)) + raises(ValueError, lambda: banded({0: (1, 2)}, rows=1)) + raises(ValueError, lambda: banded({0: (1, 2)}, cols=1)) + raises(ValueError, lambda: banded(1, {0: (1, 2)})) + raises(ValueError, lambda: banded(2, 1, {0: (1, 2)})) + raises(ValueError, lambda: banded(1, 2, {0: (1, 2)})) + + assert isinstance(banded(2, 4, {}), SparseMatrix) + assert banded(2, 4, {}) == zeros(2, 4) + assert banded({0: 0, 1: 0}) == zeros(0) + assert banded({0: Matrix([1, 2])}) == Matrix([1, 2]) + assert banded({1: [1, 2, 3, 0], -1: [4, 5, 6]}) == \ + banded({1: (1, 2, 3), -1: (4, 5, 6)}) == \ + Matrix([ + [0, 1, 0, 0], + [4, 0, 2, 0], + [0, 5, 0, 3], + [0, 0, 6, 0]]) + assert banded(3, 4, {-1: 1, 0: 2, 1: 3}) == \ + Matrix([ + [2, 3, 0, 0], + [1, 2, 3, 0], + [0, 1, 2, 3]]) + s = lambda d: (1 + d)**2 + assert banded(5, {0: s, 2: s}) == \ + Matrix([ + [1, 0, 1, 0, 0], + [0, 4, 0, 4, 0], + [0, 0, 9, 0, 9], + [0, 0, 0, 16, 0], + [0, 0, 0, 0, 25]]) + assert banded(2, {0: 1}) == \ + Matrix([ + [1, 0], + [0, 1]]) + assert banded(2, 3, {0: 1}) == \ + Matrix([ + [1, 0, 0], + [0, 1, 0]]) + vert = Matrix([1, 2, 3]) + assert banded({0: vert}, cols=3) == \ + Matrix([ + [1, 0, 0], + [2, 1, 0], + [3, 2, 1], + [0, 3, 2], + [0, 0, 3]]) + assert banded(4, {0: ones(2)}) == \ + Matrix([ + [1, 1, 0, 0], + [1, 1, 0, 0], + [0, 0, 1, 1], + [0, 0, 1, 1]]) + raises(ValueError, lambda: banded({0: 2, 1: ones(2)}, rows=5)) + assert banded({0: 2, 2: (ones(2),)*3}) == \ + Matrix([ + [2, 0, 1, 1, 0, 0, 0, 0], + [0, 2, 1, 1, 0, 0, 0, 0], + [0, 0, 2, 0, 1, 1, 0, 0], + [0, 0, 0, 2, 1, 1, 0, 0], + [0, 0, 0, 0, 2, 0, 1, 1], + [0, 0, 0, 0, 0, 2, 1, 1]]) + raises(ValueError, lambda: banded({0: (2,)*5, 1: (ones(2),)*3})) + u2 = Matrix([[1, 1], [0, 1]]) + assert banded({0: (2,)*5, 1: (u2,)*3}) == \ + Matrix([ + [2, 1, 1, 0, 0, 0, 0], + [0, 2, 1, 0, 0, 0, 0], + [0, 0, 2, 1, 1, 0, 0], + [0, 0, 0, 2, 1, 0, 0], + [0, 0, 0, 0, 2, 1, 1], + [0, 0, 0, 0, 0, 0, 1]]) + assert banded({0:(0, ones(2)), 2: 2}) == \ + Matrix([ + [0, 0, 2], + [0, 1, 1], + [0, 1, 1]]) + raises(ValueError, lambda: banded({0: (0, ones(2)), 1: 2})) + assert banded({0: 1}, cols=3) == banded({0: 1}, rows=3) == eye(3) + assert banded({1: 1}, rows=3) == Matrix([ + [0, 1, 0], + [0, 0, 1], + [0, 0, 0]]) diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_subspaces.py b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_subspaces.py new file mode 100644 index 0000000000000000000000000000000000000000..0bd853e321eb06f754c17e7bd0c11deb870506f5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/tests/test_subspaces.py @@ -0,0 +1,109 @@ +from sympy.matrices import Matrix +from sympy.core.numbers import Rational +from sympy.core.symbol import symbols +from sympy.solvers import solve + + +def test_columnspace_one(): + m = Matrix([[ 1, 2, 0, 2, 5], + [-2, -5, 1, -1, -8], + [ 0, -3, 3, 4, 1], + [ 3, 6, 0, -7, 2]]) + + basis = m.columnspace() + assert basis[0] == Matrix([1, -2, 0, 3]) + assert basis[1] == Matrix([2, -5, -3, 6]) + assert basis[2] == Matrix([2, -1, 4, -7]) + + assert len(basis) == 3 + assert Matrix.hstack(m, *basis).columnspace() == basis + + +def test_rowspace(): + m = Matrix([[ 1, 2, 0, 2, 5], + [-2, -5, 1, -1, -8], + [ 0, -3, 3, 4, 1], + [ 3, 6, 0, -7, 2]]) + + basis = m.rowspace() + assert basis[0] == Matrix([[1, 2, 0, 2, 5]]) + assert basis[1] == Matrix([[0, -1, 1, 3, 2]]) + assert basis[2] == Matrix([[0, 0, 0, 5, 5]]) + + assert len(basis) == 3 + + +def test_nullspace_one(): + m = Matrix([[ 1, 2, 0, 2, 5], + [-2, -5, 1, -1, -8], + [ 0, -3, 3, 4, 1], + [ 3, 6, 0, -7, 2]]) + + basis = m.nullspace() + assert basis[0] == Matrix([-2, 1, 1, 0, 0]) + assert basis[1] == Matrix([-1, -1, 0, -1, 1]) + # make sure the null space is really gets zeroed + assert all(e.is_zero for e in m*basis[0]) + assert all(e.is_zero for e in m*basis[1]) + +def test_nullspace_second(): + # first test reduced row-ech form + R = Rational + + M = Matrix([[5, 7, 2, 1], + [1, 6, 2, -1]]) + out, tmp = M.rref() + assert out == Matrix([[1, 0, -R(2)/23, R(13)/23], + [0, 1, R(8)/23, R(-6)/23]]) + + M = Matrix([[-5, -1, 4, -3, -1], + [ 1, -1, -1, 1, 0], + [-1, 0, 0, 0, 0], + [ 4, 1, -4, 3, 1], + [-2, 0, 2, -2, -1]]) + assert M*M.nullspace()[0] == Matrix(5, 1, [0]*5) + + M = Matrix([[ 1, 3, 0, 2, 6, 3, 1], + [-2, -6, 0, -2, -8, 3, 1], + [ 3, 9, 0, 0, 6, 6, 2], + [-1, -3, 0, 1, 0, 9, 3]]) + out, tmp = M.rref() + assert out == Matrix([[1, 3, 0, 0, 2, 0, 0], + [0, 0, 0, 1, 2, 0, 0], + [0, 0, 0, 0, 0, 1, R(1)/3], + [0, 0, 0, 0, 0, 0, 0]]) + + # now check the vectors + basis = M.nullspace() + assert basis[0] == Matrix([-3, 1, 0, 0, 0, 0, 0]) + assert basis[1] == Matrix([0, 0, 1, 0, 0, 0, 0]) + assert basis[2] == Matrix([-2, 0, 0, -2, 1, 0, 0]) + assert basis[3] == Matrix([0, 0, 0, 0, 0, R(-1)/3, 1]) + + # issue 4797; just see that we can do it when rows > cols + M = Matrix([[1, 2], [2, 4], [3, 6]]) + assert M.nullspace() + + +def test_columnspace_second(): + M = Matrix([[ 1, 2, 0, 2, 5], + [-2, -5, 1, -1, -8], + [ 0, -3, 3, 4, 1], + [ 3, 6, 0, -7, 2]]) + + # now check the vectors + basis = M.columnspace() + assert basis[0] == Matrix([1, -2, 0, 3]) + assert basis[1] == Matrix([2, -5, -3, 6]) + assert basis[2] == Matrix([2, -1, 4, -7]) + + #check by columnspace definition + a, b, c, d, e = symbols('a b c d e') + X = Matrix([a, b, c, d, e]) + for i in range(len(basis)): + eq=M*X-basis[i] + assert len(solve(eq, X)) != 0 + + #check if rank-nullity theorem holds + assert M.rank() == len(basis) + assert len(M.nullspace()) + len(M.columnspace()) == M.cols diff --git a/venv/lib/python3.11/site-packages/sympy/matrices/utilities.py b/venv/lib/python3.11/site-packages/sympy/matrices/utilities.py new file mode 100644 index 0000000000000000000000000000000000000000..b8a680b47e63615e210e561639a192ba47c642d3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/matrices/utilities.py @@ -0,0 +1,72 @@ +from contextlib import contextmanager +from threading import local + +from sympy.core.function import expand_mul + + +class DotProdSimpState(local): + def __init__(self): + self.state = None + +_dotprodsimp_state = DotProdSimpState() + +@contextmanager +def dotprodsimp(x): + old = _dotprodsimp_state.state + + try: + _dotprodsimp_state.state = x + yield + finally: + _dotprodsimp_state.state = old + + +def _dotprodsimp(expr, withsimp=False): + """Wrapper for simplify.dotprodsimp to avoid circular imports.""" + from sympy.simplify.simplify import dotprodsimp as dps + return dps(expr, withsimp=withsimp) + + +def _get_intermediate_simp(deffunc=lambda x: x, offfunc=lambda x: x, + onfunc=_dotprodsimp, dotprodsimp=None): + """Support function for controlling intermediate simplification. Returns a + simplification function according to the global setting of dotprodsimp + operation. + + ``deffunc`` - Function to be used by default. + ``offfunc`` - Function to be used if dotprodsimp has been turned off. + ``onfunc`` - Function to be used if dotprodsimp has been turned on. + ``dotprodsimp`` - True, False or None. Will be overridden by global + _dotprodsimp_state.state if that is not None. + """ + + if dotprodsimp is False or _dotprodsimp_state.state is False: + return offfunc + if dotprodsimp is True or _dotprodsimp_state.state is True: + return onfunc + + return deffunc # None, None + + +def _get_intermediate_simp_bool(default=False, dotprodsimp=None): + """Same as ``_get_intermediate_simp`` but returns bools instead of functions + by default.""" + + return _get_intermediate_simp(default, False, True, dotprodsimp) + + +def _iszero(x): + """Returns True if x is zero.""" + return getattr(x, 'is_zero', None) + + +def _is_zero_after_expand_mul(x): + """Tests by expand_mul only, suitable for polynomials and rational + functions.""" + return expand_mul(x) == 0 + + +def _simplify(expr): + """ Wrapper to avoid circular imports. """ + from sympy.simplify.simplify import simplify + return simplify(expr) diff --git a/venv/lib/python3.11/site-packages/sympy/multipledispatch/__init__.py b/venv/lib/python3.11/site-packages/sympy/multipledispatch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5447651645e3e2e92df3002822e87a773ade0df8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/multipledispatch/__init__.py @@ -0,0 +1,11 @@ +from .core import dispatch +from .dispatcher import (Dispatcher, halt_ordering, restart_ordering, + MDNotImplementedError) + +__version__ = '0.4.9' + +__all__ = [ + 'dispatch', + + 'Dispatcher', 'halt_ordering', 'restart_ordering', 'MDNotImplementedError', +] diff --git a/venv/lib/python3.11/site-packages/sympy/multipledispatch/conflict.py b/venv/lib/python3.11/site-packages/sympy/multipledispatch/conflict.py new file mode 100644 index 0000000000000000000000000000000000000000..98c6742c9c03860233ef0004b241ea3944ac6d4d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/multipledispatch/conflict.py @@ -0,0 +1,68 @@ +from .utils import _toposort, groupby + +class AmbiguityWarning(Warning): + pass + + +def supercedes(a, b): + """ A is consistent and strictly more specific than B """ + return len(a) == len(b) and all(map(issubclass, a, b)) + + +def consistent(a, b): + """ It is possible for an argument list to satisfy both A and B """ + return (len(a) == len(b) and + all(issubclass(aa, bb) or issubclass(bb, aa) + for aa, bb in zip(a, b))) + + +def ambiguous(a, b): + """ A is consistent with B but neither is strictly more specific """ + return consistent(a, b) and not (supercedes(a, b) or supercedes(b, a)) + + +def ambiguities(signatures): + """ All signature pairs such that A is ambiguous with B """ + signatures = list(map(tuple, signatures)) + return {(a, b) for a in signatures for b in signatures + if hash(a) < hash(b) + and ambiguous(a, b) + and not any(supercedes(c, a) and supercedes(c, b) + for c in signatures)} + + +def super_signature(signatures): + """ A signature that would break ambiguities """ + n = len(signatures[0]) + assert all(len(s) == n for s in signatures) + + return [max([type.mro(sig[i]) for sig in signatures], key=len)[0] + for i in range(n)] + + +def edge(a, b, tie_breaker=hash): + """ A should be checked before B + + Tie broken by tie_breaker, defaults to ``hash`` + """ + if supercedes(a, b): + if supercedes(b, a): + return tie_breaker(a) > tie_breaker(b) + else: + return True + return False + + +def ordering(signatures): + """ A sane ordering of signatures to check, first to last + + Topoological sort of edges as given by ``edge`` and ``supercedes`` + """ + signatures = list(map(tuple, signatures)) + edges = [(a, b) for a in signatures for b in signatures if edge(a, b)] + edges = groupby(lambda x: x[0], edges) + for s in signatures: + if s not in edges: + edges[s] = [] + edges = {k: [b for a, b in v] for k, v in edges.items()} + return _toposort(edges) diff --git a/venv/lib/python3.11/site-packages/sympy/multipledispatch/core.py b/venv/lib/python3.11/site-packages/sympy/multipledispatch/core.py new file mode 100644 index 0000000000000000000000000000000000000000..2856ff728c4eb97c5a59fffabddb4bf3c8b4baf2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/multipledispatch/core.py @@ -0,0 +1,83 @@ +from __future__ import annotations +from typing import Any + +import inspect + +from .dispatcher import Dispatcher, MethodDispatcher, ambiguity_warn + +# XXX: This parameter to dispatch isn't documented and isn't used anywhere in +# sympy. Maybe it should just be removed. +global_namespace: dict[str, Any] = {} + + +def dispatch(*types, namespace=global_namespace, on_ambiguity=ambiguity_warn): + """ Dispatch function on the types of the inputs + + Supports dispatch on all non-keyword arguments. + + Collects implementations based on the function name. Ignores namespaces. + + If ambiguous type signatures occur a warning is raised when the function is + defined suggesting the additional method to break the ambiguity. + + Examples + -------- + + >>> from sympy.multipledispatch import dispatch + >>> @dispatch(int) + ... def f(x): + ... return x + 1 + + >>> @dispatch(float) + ... def f(x): # noqa: F811 + ... return x - 1 + + >>> f(3) + 4 + >>> f(3.0) + 2.0 + + Specify an isolated namespace with the namespace keyword argument + + >>> my_namespace = dict() + >>> @dispatch(int, namespace=my_namespace) + ... def foo(x): + ... return x + 1 + + Dispatch on instance methods within classes + + >>> class MyClass(object): + ... @dispatch(list) + ... def __init__(self, data): + ... self.data = data + ... @dispatch(int) + ... def __init__(self, datum): # noqa: F811 + ... self.data = [datum] + """ + types = tuple(types) + + def _(func): + name = func.__name__ + + if ismethod(func): + dispatcher = inspect.currentframe().f_back.f_locals.get( + name, + MethodDispatcher(name)) + else: + if name not in namespace: + namespace[name] = Dispatcher(name) + dispatcher = namespace[name] + + dispatcher.add(types, func, on_ambiguity=on_ambiguity) + return dispatcher + return _ + + +def ismethod(func): + """ Is func a method? + + Note that this has to work as the method is defined but before the class is + defined. At this stage methods look like functions. + """ + signature = inspect.signature(func) + return signature.parameters.get('self', None) is not None diff --git a/venv/lib/python3.11/site-packages/sympy/multipledispatch/dispatcher.py b/venv/lib/python3.11/site-packages/sympy/multipledispatch/dispatcher.py new file mode 100644 index 0000000000000000000000000000000000000000..89471d678e1c330138a91ec6a41a324d29a037d7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/multipledispatch/dispatcher.py @@ -0,0 +1,413 @@ +from __future__ import annotations + +from warnings import warn +import inspect +from .conflict import ordering, ambiguities, super_signature, AmbiguityWarning +from .utils import expand_tuples +import itertools as itl + + +class MDNotImplementedError(NotImplementedError): + """ A NotImplementedError for multiple dispatch """ + + +### Functions for on_ambiguity + +def ambiguity_warn(dispatcher, ambiguities): + """ Raise warning when ambiguity is detected + + Parameters + ---------- + dispatcher : Dispatcher + The dispatcher on which the ambiguity was detected + ambiguities : set + Set of type signature pairs that are ambiguous within this dispatcher + + See Also: + Dispatcher.add + warning_text + """ + warn(warning_text(dispatcher.name, ambiguities), AmbiguityWarning) + + +class RaiseNotImplementedError: + """Raise ``NotImplementedError`` when called.""" + + def __init__(self, dispatcher): + self.dispatcher = dispatcher + + def __call__(self, *args, **kwargs): + types = tuple(type(a) for a in args) + raise NotImplementedError( + "Ambiguous signature for %s: <%s>" % ( + self.dispatcher.name, str_signature(types) + )) + +def ambiguity_register_error_ignore_dup(dispatcher, ambiguities): + """ + If super signature for ambiguous types is duplicate types, ignore it. + Else, register instance of ``RaiseNotImplementedError`` for ambiguous types. + + Parameters + ---------- + dispatcher : Dispatcher + The dispatcher on which the ambiguity was detected + ambiguities : set + Set of type signature pairs that are ambiguous within this dispatcher + + See Also: + Dispatcher.add + ambiguity_warn + """ + for amb in ambiguities: + signature = tuple(super_signature(amb)) + if len(set(signature)) == 1: + continue + dispatcher.add( + signature, RaiseNotImplementedError(dispatcher), + on_ambiguity=ambiguity_register_error_ignore_dup + ) + +### + + +_unresolved_dispatchers: set[Dispatcher] = set() +_resolve = [True] + + +def halt_ordering(): + _resolve[0] = False + + +def restart_ordering(on_ambiguity=ambiguity_warn): + _resolve[0] = True + while _unresolved_dispatchers: + dispatcher = _unresolved_dispatchers.pop() + dispatcher.reorder(on_ambiguity=on_ambiguity) + + +class Dispatcher: + """ Dispatch methods based on type signature + + Use ``dispatch`` to add implementations + + Examples + -------- + + >>> from sympy.multipledispatch import dispatch + >>> @dispatch(int) + ... def f(x): + ... return x + 1 + + >>> @dispatch(float) + ... def f(x): # noqa: F811 + ... return x - 1 + + >>> f(3) + 4 + >>> f(3.0) + 2.0 + """ + __slots__ = '__name__', 'name', 'funcs', 'ordering', '_cache', 'doc' + + def __init__(self, name, doc=None): + self.name = self.__name__ = name + self.funcs = {} + self._cache = {} + self.ordering = [] + self.doc = doc + + def register(self, *types, **kwargs): + """ Register dispatcher with new implementation + + >>> from sympy.multipledispatch.dispatcher import Dispatcher + >>> f = Dispatcher('f') + >>> @f.register(int) + ... def inc(x): + ... return x + 1 + + >>> @f.register(float) + ... def dec(x): + ... return x - 1 + + >>> @f.register(list) + ... @f.register(tuple) + ... def reverse(x): + ... return x[::-1] + + >>> f(1) + 2 + + >>> f(1.0) + 0.0 + + >>> f([1, 2, 3]) + [3, 2, 1] + """ + def _(func): + self.add(types, func, **kwargs) + return func + return _ + + @classmethod + def get_func_params(cls, func): + if hasattr(inspect, "signature"): + sig = inspect.signature(func) + return sig.parameters.values() + + @classmethod + def get_func_annotations(cls, func): + """ Get annotations of function positional parameters + """ + params = cls.get_func_params(func) + if params: + Parameter = inspect.Parameter + + params = (param for param in params + if param.kind in + (Parameter.POSITIONAL_ONLY, + Parameter.POSITIONAL_OR_KEYWORD)) + + annotations = tuple( + param.annotation + for param in params) + + if not any(ann is Parameter.empty for ann in annotations): + return annotations + + def add(self, signature, func, on_ambiguity=ambiguity_warn): + """ Add new types/method pair to dispatcher + + >>> from sympy.multipledispatch import Dispatcher + >>> D = Dispatcher('add') + >>> D.add((int, int), lambda x, y: x + y) + >>> D.add((float, float), lambda x, y: x + y) + + >>> D(1, 2) + 3 + >>> D(1, 2.0) + Traceback (most recent call last): + ... + NotImplementedError: Could not find signature for add: + + When ``add`` detects a warning it calls the ``on_ambiguity`` callback + with a dispatcher/itself, and a set of ambiguous type signature pairs + as inputs. See ``ambiguity_warn`` for an example. + """ + # Handle annotations + if not signature: + annotations = self.get_func_annotations(func) + if annotations: + signature = annotations + + # Handle union types + if any(isinstance(typ, tuple) for typ in signature): + for typs in expand_tuples(signature): + self.add(typs, func, on_ambiguity) + return + + for typ in signature: + if not isinstance(typ, type): + str_sig = ', '.join(c.__name__ if isinstance(c, type) + else str(c) for c in signature) + raise TypeError("Tried to dispatch on non-type: %s\n" + "In signature: <%s>\n" + "In function: %s" % + (typ, str_sig, self.name)) + + self.funcs[signature] = func + self.reorder(on_ambiguity=on_ambiguity) + self._cache.clear() + + def reorder(self, on_ambiguity=ambiguity_warn): + if _resolve[0]: + self.ordering = ordering(self.funcs) + amb = ambiguities(self.funcs) + if amb: + on_ambiguity(self, amb) + else: + _unresolved_dispatchers.add(self) + + def __call__(self, *args, **kwargs): + types = tuple([type(arg) for arg in args]) + try: + func = self._cache[types] + except KeyError: + func = self.dispatch(*types) + if not func: + raise NotImplementedError( + 'Could not find signature for %s: <%s>' % + (self.name, str_signature(types))) + self._cache[types] = func + try: + return func(*args, **kwargs) + + except MDNotImplementedError: + funcs = self.dispatch_iter(*types) + next(funcs) # burn first + for func in funcs: + try: + return func(*args, **kwargs) + except MDNotImplementedError: + pass + raise NotImplementedError("Matching functions for " + "%s: <%s> found, but none completed successfully" + % (self.name, str_signature(types))) + + def __str__(self): + return "" % self.name + __repr__ = __str__ + + def dispatch(self, *types): + """ Deterimine appropriate implementation for this type signature + + This method is internal. Users should call this object as a function. + Implementation resolution occurs within the ``__call__`` method. + + >>> from sympy.multipledispatch import dispatch + >>> @dispatch(int) + ... def inc(x): + ... return x + 1 + + >>> implementation = inc.dispatch(int) + >>> implementation(3) + 4 + + >>> print(inc.dispatch(float)) + None + + See Also: + ``sympy.multipledispatch.conflict`` - module to determine resolution order + """ + + if types in self.funcs: + return self.funcs[types] + + try: + return next(self.dispatch_iter(*types)) + except StopIteration: + return None + + def dispatch_iter(self, *types): + n = len(types) + for signature in self.ordering: + if len(signature) == n and all(map(issubclass, types, signature)): + result = self.funcs[signature] + yield result + + def resolve(self, types): + """ Deterimine appropriate implementation for this type signature + + .. deprecated:: 0.4.4 + Use ``dispatch(*types)`` instead + """ + warn("resolve() is deprecated, use dispatch(*types)", + DeprecationWarning) + + return self.dispatch(*types) + + def __getstate__(self): + return {'name': self.name, + 'funcs': self.funcs} + + def __setstate__(self, d): + self.name = d['name'] + self.funcs = d['funcs'] + self.ordering = ordering(self.funcs) + self._cache = {} + + @property + def __doc__(self): + docs = ["Multiply dispatched method: %s" % self.name] + + if self.doc: + docs.append(self.doc) + + other = [] + for sig in self.ordering[::-1]: + func = self.funcs[sig] + if func.__doc__: + s = 'Inputs: <%s>\n' % str_signature(sig) + s += '-' * len(s) + '\n' + s += func.__doc__.strip() + docs.append(s) + else: + other.append(str_signature(sig)) + + if other: + docs.append('Other signatures:\n ' + '\n '.join(other)) + + return '\n\n'.join(docs) + + def _help(self, *args): + return self.dispatch(*map(type, args)).__doc__ + + def help(self, *args, **kwargs): + """ Print docstring for the function corresponding to inputs """ + print(self._help(*args)) + + def _source(self, *args): + func = self.dispatch(*map(type, args)) + if not func: + raise TypeError("No function found") + return source(func) + + def source(self, *args, **kwargs): + """ Print source code for the function corresponding to inputs """ + print(self._source(*args)) + + +def source(func): + s = 'File: %s\n\n' % inspect.getsourcefile(func) + s = s + inspect.getsource(func) + return s + + +class MethodDispatcher(Dispatcher): + """ Dispatch methods based on type signature + + See Also: + Dispatcher + """ + + @classmethod + def get_func_params(cls, func): + if hasattr(inspect, "signature"): + sig = inspect.signature(func) + return itl.islice(sig.parameters.values(), 1, None) + + def __get__(self, instance, owner): + self.obj = instance + self.cls = owner + return self + + def __call__(self, *args, **kwargs): + types = tuple([type(arg) for arg in args]) + func = self.dispatch(*types) + if not func: + raise NotImplementedError('Could not find signature for %s: <%s>' % + (self.name, str_signature(types))) + return func(self.obj, *args, **kwargs) + + +def str_signature(sig): + """ String representation of type signature + + >>> from sympy.multipledispatch.dispatcher import str_signature + >>> str_signature((int, float)) + 'int, float' + """ + return ', '.join(cls.__name__ for cls in sig) + + +def warning_text(name, amb): + """ The text for ambiguity warnings """ + text = "\nAmbiguities exist in dispatched function %s\n\n" % (name) + text += "The following signatures may result in ambiguous behavior:\n" + for pair in amb: + text += "\t" + \ + ', '.join('[' + str_signature(s) + ']' for s in pair) + "\n" + text += "\n\nConsider making the following additions:\n\n" + text += '\n\n'.join(['@dispatch(' + str_signature(super_signature(s)) + + ')\ndef %s(...)' % name for s in amb]) + return text diff --git a/venv/lib/python3.11/site-packages/sympy/multipledispatch/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/multipledispatch/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/multipledispatch/tests/test_conflict.py b/venv/lib/python3.11/site-packages/sympy/multipledispatch/tests/test_conflict.py new file mode 100644 index 0000000000000000000000000000000000000000..5d2292c460585ae2a65a01795b38499e67706ff0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/multipledispatch/tests/test_conflict.py @@ -0,0 +1,62 @@ +from sympy.multipledispatch.conflict import (supercedes, ordering, ambiguities, + ambiguous, super_signature, consistent) + + +class A: pass +class B(A): pass +class C: pass + + +def test_supercedes(): + assert supercedes([B], [A]) + assert supercedes([B, A], [A, A]) + assert not supercedes([B, A], [A, B]) + assert not supercedes([A], [B]) + + +def test_consistent(): + assert consistent([A], [A]) + assert consistent([B], [B]) + assert not consistent([A], [C]) + assert consistent([A, B], [A, B]) + assert consistent([B, A], [A, B]) + assert not consistent([B, A], [B]) + assert not consistent([B, A], [B, C]) + + +def test_super_signature(): + assert super_signature([[A]]) == [A] + assert super_signature([[A], [B]]) == [B] + assert super_signature([[A, B], [B, A]]) == [B, B] + assert super_signature([[A, A, B], [A, B, A], [B, A, A]]) == [B, B, B] + + +def test_ambiguous(): + assert not ambiguous([A], [A]) + assert not ambiguous([A], [B]) + assert not ambiguous([B], [B]) + assert not ambiguous([A, B], [B, B]) + assert ambiguous([A, B], [B, A]) + + +def test_ambiguities(): + signatures = [[A], [B], [A, B], [B, A], [A, C]] + expected = {((A, B), (B, A))} + result = ambiguities(signatures) + assert set(map(frozenset, expected)) == set(map(frozenset, result)) + + signatures = [[A], [B], [A, B], [B, A], [A, C], [B, B]] + expected = set() + result = ambiguities(signatures) + assert set(map(frozenset, expected)) == set(map(frozenset, result)) + + +def test_ordering(): + signatures = [[A, A], [A, B], [B, A], [B, B], [A, C]] + ord = ordering(signatures) + assert ord[0] == (B, B) or ord[0] == (A, C) + assert ord[-1] == (A, A) or ord[-1] == (A, C) + + +def test_type_mro(): + assert super_signature([[object], [type]]) == [type] diff --git a/venv/lib/python3.11/site-packages/sympy/multipledispatch/tests/test_core.py b/venv/lib/python3.11/site-packages/sympy/multipledispatch/tests/test_core.py new file mode 100644 index 0000000000000000000000000000000000000000..016270fecc8cda644fc71b5c310b1430b50361f6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/multipledispatch/tests/test_core.py @@ -0,0 +1,213 @@ +from __future__ import annotations +from typing import Any + +from sympy.multipledispatch import dispatch +from sympy.multipledispatch.conflict import AmbiguityWarning +from sympy.testing.pytest import raises, warns +from functools import partial + +test_namespace: dict[str, Any] = {} + +orig_dispatch = dispatch +dispatch = partial(dispatch, namespace=test_namespace) + + +def test_singledispatch(): + @dispatch(int) + def f(x): # noqa:F811 + return x + 1 + + @dispatch(int) + def g(x): # noqa:F811 + return x + 2 + + @dispatch(float) # noqa:F811 + def f(x): # noqa:F811 + return x - 1 + + assert f(1) == 2 + assert g(1) == 3 + assert f(1.0) == 0 + + assert raises(NotImplementedError, lambda: f('hello')) + + +def test_multipledispatch(): + @dispatch(int, int) + def f(x, y): # noqa:F811 + return x + y + + @dispatch(float, float) # noqa:F811 + def f(x, y): # noqa:F811 + return x - y + + assert f(1, 2) == 3 + assert f(1.0, 2.0) == -1.0 + + +class A: pass +class B: pass +class C(A): pass +class D(C): pass +class E(C): pass + + +def test_inheritance(): + @dispatch(A) + def f(x): # noqa:F811 + return 'a' + + @dispatch(B) # noqa:F811 + def f(x): # noqa:F811 + return 'b' + + assert f(A()) == 'a' + assert f(B()) == 'b' + assert f(C()) == 'a' + + +def test_inheritance_and_multiple_dispatch(): + @dispatch(A, A) + def f(x, y): # noqa:F811 + return type(x), type(y) + + @dispatch(A, B) # noqa:F811 + def f(x, y): # noqa:F811 + return 0 + + assert f(A(), A()) == (A, A) + assert f(A(), C()) == (A, C) + assert f(A(), B()) == 0 + assert f(C(), B()) == 0 + assert raises(NotImplementedError, lambda: f(B(), B())) + + +def test_competing_solutions(): + @dispatch(A) + def h(x): # noqa:F811 + return 1 + + @dispatch(C) # noqa:F811 + def h(x): # noqa:F811 + return 2 + + assert h(D()) == 2 + + +def test_competing_multiple(): + @dispatch(A, B) + def h(x, y): # noqa:F811 + return 1 + + @dispatch(C, B) # noqa:F811 + def h(x, y): # noqa:F811 + return 2 + + assert h(D(), B()) == 2 + + +def test_competing_ambiguous(): + test_namespace = {} + dispatch = partial(orig_dispatch, namespace=test_namespace) + + @dispatch(A, C) + def f(x, y): # noqa:F811 + return 2 + + with warns(AmbiguityWarning, test_stacklevel=False): + @dispatch(C, A) # noqa:F811 + def f(x, y): # noqa:F811 + return 2 + + assert f(A(), C()) == f(C(), A()) == 2 + # assert raises(Warning, lambda : f(C(), C())) + + +def test_caching_correct_behavior(): + @dispatch(A) + def f(x): # noqa:F811 + return 1 + + assert f(C()) == 1 + + @dispatch(C) + def f(x): # noqa:F811 + return 2 + + assert f(C()) == 2 + + +def test_union_types(): + @dispatch((A, C)) + def f(x): # noqa:F811 + return 1 + + assert f(A()) == 1 + assert f(C()) == 1 + + +def test_namespaces(): + ns1 = {} + ns2 = {} + + def foo(x): + return 1 + foo1 = orig_dispatch(int, namespace=ns1)(foo) + + def foo(x): + return 2 + foo2 = orig_dispatch(int, namespace=ns2)(foo) + + assert foo1(0) == 1 + assert foo2(0) == 2 + + +""" +Fails +def test_dispatch_on_dispatch(): + @dispatch(A) + @dispatch(C) + def q(x): # noqa:F811 + return 1 + + assert q(A()) == 1 + assert q(C()) == 1 +""" + + +def test_methods(): + class Foo: + @dispatch(float) + def f(self, x): # noqa:F811 + return x - 1 + + @dispatch(int) # noqa:F811 + def f(self, x): # noqa:F811 + return x + 1 + + @dispatch(int) + def g(self, x): # noqa:F811 + return x + 3 + + + foo = Foo() + assert foo.f(1) == 2 + assert foo.f(1.0) == 0.0 + assert foo.g(1) == 4 + + +def test_methods_multiple_dispatch(): + class Foo: + @dispatch(A, A) + def f(x, y): # noqa:F811 + return 1 + + @dispatch(A, C) # noqa:F811 + def f(x, y): # noqa:F811 + return 2 + + + foo = Foo() + assert foo.f(A(), A()) == 1 + assert foo.f(A(), C()) == 2 + assert foo.f(C(), C()) == 2 diff --git a/venv/lib/python3.11/site-packages/sympy/multipledispatch/tests/test_dispatcher.py b/venv/lib/python3.11/site-packages/sympy/multipledispatch/tests/test_dispatcher.py new file mode 100644 index 0000000000000000000000000000000000000000..e31ca8a5486b87eb43fc5e6f887caf50d6bfbe20 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/multipledispatch/tests/test_dispatcher.py @@ -0,0 +1,284 @@ +from sympy.multipledispatch.dispatcher import (Dispatcher, MDNotImplementedError, + MethodDispatcher, halt_ordering, + restart_ordering, + ambiguity_register_error_ignore_dup) +from sympy.testing.pytest import raises, warns + + +def identity(x): + return x + + +def inc(x): + return x + 1 + + +def dec(x): + return x - 1 + + +def test_dispatcher(): + f = Dispatcher('f') + f.add((int,), inc) + f.add((float,), dec) + + with warns(DeprecationWarning, test_stacklevel=False): + assert f.resolve((int,)) == inc + assert f.dispatch(int) is inc + + assert f(1) == 2 + assert f(1.0) == 0.0 + + +def test_union_types(): + f = Dispatcher('f') + f.register((int, float))(inc) + + assert f(1) == 2 + assert f(1.0) == 2.0 + + +def test_dispatcher_as_decorator(): + f = Dispatcher('f') + + @f.register(int) + def inc(x): # noqa:F811 + return x + 1 + + @f.register(float) # noqa:F811 + def inc(x): # noqa:F811 + return x - 1 + + assert f(1) == 2 + assert f(1.0) == 0.0 + + +def test_register_instance_method(): + + class Test: + __init__ = MethodDispatcher('f') + + @__init__.register(list) + def _init_list(self, data): + self.data = data + + @__init__.register(object) + def _init_obj(self, datum): + self.data = [datum] + + a = Test(3) + b = Test([3]) + assert a.data == b.data + + +def test_on_ambiguity(): + f = Dispatcher('f') + + def identity(x): return x + + ambiguities = [False] + + def on_ambiguity(dispatcher, amb): + ambiguities[0] = True + + f.add((object, object), identity, on_ambiguity=on_ambiguity) + assert not ambiguities[0] + f.add((object, float), identity, on_ambiguity=on_ambiguity) + assert not ambiguities[0] + f.add((float, object), identity, on_ambiguity=on_ambiguity) + assert ambiguities[0] + + +def test_raise_error_on_non_class(): + f = Dispatcher('f') + assert raises(TypeError, lambda: f.add((1,), inc)) + + +def test_docstring(): + + def one(x, y): + """ Docstring number one """ + return x + y + + def two(x, y): + """ Docstring number two """ + return x + y + + def three(x, y): + return x + y + + master_doc = 'Doc of the multimethod itself' + + f = Dispatcher('f', doc=master_doc) + f.add((object, object), one) + f.add((int, int), two) + f.add((float, float), three) + + assert one.__doc__.strip() in f.__doc__ + assert two.__doc__.strip() in f.__doc__ + assert f.__doc__.find(one.__doc__.strip()) < \ + f.__doc__.find(two.__doc__.strip()) + assert 'object, object' in f.__doc__ + assert master_doc in f.__doc__ + + +def test_help(): + def one(x, y): + """ Docstring number one """ + return x + y + + def two(x, y): + """ Docstring number two """ + return x + y + + def three(x, y): + """ Docstring number three """ + return x + y + + master_doc = 'Doc of the multimethod itself' + + f = Dispatcher('f', doc=master_doc) + f.add((object, object), one) + f.add((int, int), two) + f.add((float, float), three) + + assert f._help(1, 1) == two.__doc__ + assert f._help(1.0, 2.0) == three.__doc__ + + +def test_source(): + def one(x, y): + """ Docstring number one """ + return x + y + + def two(x, y): + """ Docstring number two """ + return x - y + + master_doc = 'Doc of the multimethod itself' + + f = Dispatcher('f', doc=master_doc) + f.add((int, int), one) + f.add((float, float), two) + + assert 'x + y' in f._source(1, 1) + assert 'x - y' in f._source(1.0, 1.0) + + +def test_source_raises_on_missing_function(): + f = Dispatcher('f') + + assert raises(TypeError, lambda: f.source(1)) + + +def test_halt_method_resolution(): + g = [0] + + def on_ambiguity(a, b): + g[0] += 1 + + f = Dispatcher('f') + + halt_ordering() + + def func(*args): + pass + + f.add((int, object), func) + f.add((object, int), func) + + assert g == [0] + + restart_ordering(on_ambiguity=on_ambiguity) + + assert g == [1] + + assert set(f.ordering) == {(int, object), (object, int)} + + +def test_no_implementations(): + f = Dispatcher('f') + assert raises(NotImplementedError, lambda: f('hello')) + + +def test_register_stacking(): + f = Dispatcher('f') + + @f.register(list) + @f.register(tuple) + def rev(x): + return x[::-1] + + assert f((1, 2, 3)) == (3, 2, 1) + assert f([1, 2, 3]) == [3, 2, 1] + + assert raises(NotImplementedError, lambda: f('hello')) + assert rev('hello') == 'olleh' + + +def test_dispatch_method(): + f = Dispatcher('f') + + @f.register(list) + def rev(x): + return x[::-1] + + @f.register(int, int) + def add(x, y): + return x + y + + class MyList(list): + pass + + assert f.dispatch(list) is rev + assert f.dispatch(MyList) is rev + assert f.dispatch(int, int) is add + + +def test_not_implemented(): + f = Dispatcher('f') + + @f.register(object) + def _(x): + return 'default' + + @f.register(int) + def _(x): + if x % 2 == 0: + return 'even' + else: + raise MDNotImplementedError() + + assert f('hello') == 'default' # default behavior + assert f(2) == 'even' # specialized behavior + assert f(3) == 'default' # fall bac to default behavior + assert raises(NotImplementedError, lambda: f(1, 2)) + + +def test_not_implemented_error(): + f = Dispatcher('f') + + @f.register(float) + def _(a): + raise MDNotImplementedError() + + assert raises(NotImplementedError, lambda: f(1.0)) + +def test_ambiguity_register_error_ignore_dup(): + f = Dispatcher('f') + + class A: + pass + class B(A): + pass + class C(A): + pass + + # suppress warning for registering ambiguous signal + f.add((A, B), lambda x,y: None, ambiguity_register_error_ignore_dup) + f.add((B, A), lambda x,y: None, ambiguity_register_error_ignore_dup) + f.add((A, C), lambda x,y: None, ambiguity_register_error_ignore_dup) + f.add((C, A), lambda x,y: None, ambiguity_register_error_ignore_dup) + + # raises error if ambiguous signal is passed + assert raises(NotImplementedError, lambda: f(B(), C())) diff --git a/venv/lib/python3.11/site-packages/sympy/multipledispatch/utils.py b/venv/lib/python3.11/site-packages/sympy/multipledispatch/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..11f563772385124c2fc0d285f7aa6e0747b8b412 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/multipledispatch/utils.py @@ -0,0 +1,105 @@ +from collections import OrderedDict + + +def expand_tuples(L): + """ + >>> from sympy.multipledispatch.utils import expand_tuples + >>> expand_tuples([1, (2, 3)]) + [(1, 2), (1, 3)] + + >>> expand_tuples([1, 2]) + [(1, 2)] + """ + if not L: + return [()] + elif not isinstance(L[0], tuple): + rest = expand_tuples(L[1:]) + return [(L[0],) + t for t in rest] + else: + rest = expand_tuples(L[1:]) + return [(item,) + t for t in rest for item in L[0]] + + +# Taken from theano/theano/gof/sched.py +# Avoids licensing issues because this was written by Matthew Rocklin +def _toposort(edges): + """ Topological sort algorithm by Kahn [1] - O(nodes + vertices) + + inputs: + edges - a dict of the form {a: {b, c}} where b and c depend on a + outputs: + L - an ordered list of nodes that satisfy the dependencies of edges + + >>> from sympy.multipledispatch.utils import _toposort + >>> _toposort({1: (2, 3), 2: (3, )}) + [1, 2, 3] + + Closely follows the wikipedia page [2] + + [1] Kahn, Arthur B. (1962), "Topological sorting of large networks", + Communications of the ACM + [2] https://en.wikipedia.org/wiki/Toposort#Algorithms + """ + incoming_edges = reverse_dict(edges) + incoming_edges = {k: set(val) for k, val in incoming_edges.items()} + S = OrderedDict.fromkeys(v for v in edges if v not in incoming_edges) + L = [] + + while S: + n, _ = S.popitem() + L.append(n) + for m in edges.get(n, ()): + assert n in incoming_edges[m] + incoming_edges[m].remove(n) + if not incoming_edges[m]: + S[m] = None + if any(incoming_edges.get(v, None) for v in edges): + raise ValueError("Input has cycles") + return L + + +def reverse_dict(d): + """Reverses direction of dependence dict + + >>> d = {'a': (1, 2), 'b': (2, 3), 'c':()} + >>> reverse_dict(d) # doctest: +SKIP + {1: ('a',), 2: ('a', 'b'), 3: ('b',)} + + :note: dict order are not deterministic. As we iterate on the + input dict, it make the output of this function depend on the + dict order. So this function output order should be considered + as undeterministic. + + """ + result = {} + for key in d: + for val in d[key]: + result[val] = result.get(val, ()) + (key, ) + return result + + +# Taken from toolz +# Avoids licensing issues because this version was authored by Matthew Rocklin +def groupby(func, seq): + """ Group a collection by a key function + + >>> from sympy.multipledispatch.utils import groupby + >>> names = ['Alice', 'Bob', 'Charlie', 'Dan', 'Edith', 'Frank'] + >>> groupby(len, names) # doctest: +SKIP + {3: ['Bob', 'Dan'], 5: ['Alice', 'Edith', 'Frank'], 7: ['Charlie']} + + >>> iseven = lambda x: x % 2 == 0 + >>> groupby(iseven, [1, 2, 3, 4, 5, 6, 7, 8]) # doctest: +SKIP + {False: [1, 3, 5, 7], True: [2, 4, 6, 8]} + + See Also: + ``countby`` + """ + + d = {} + for item in seq: + key = func(item) + if key not in d: + d[key] = [] + d[key].append(item) + return d diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/__init__.py b/venv/lib/python3.11/site-packages/sympy/ntheory/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..19576c8935da455743d27f0a263caecca94f59f8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/__init__.py @@ -0,0 +1,67 @@ +""" +Number theory module (primes, etc) +""" + +from .generate import nextprime, prevprime, prime, primepi, primerange, \ + randprime, Sieve, sieve, primorial, cycle_length, composite, compositepi +from .primetest import isprime, is_gaussian_prime, is_mersenne_prime +from .factor_ import divisors, proper_divisors, factorint, multiplicity, \ + multiplicity_in_factorial, perfect_power, factor_cache, pollard_pm1, \ + pollard_rho, primefactors, totient, \ + divisor_count, proper_divisor_count, divisor_sigma, factorrat, \ + reduced_totient, primenu, primeomega, mersenne_prime_exponent, \ + is_perfect, is_abundant, is_deficient, is_amicable, is_carmichael, \ + abundance, dra, drm + +from .partitions_ import npartitions +from .residue_ntheory import is_primitive_root, is_quad_residue, \ + legendre_symbol, jacobi_symbol, n_order, sqrt_mod, quadratic_residues, \ + primitive_root, nthroot_mod, is_nthpow_residue, sqrt_mod_iter, mobius, \ + discrete_log, quadratic_congruence, polynomial_congruence +from .multinomial import binomial_coefficients, binomial_coefficients_list, \ + multinomial_coefficients +from .continued_fraction import continued_fraction_periodic, \ + continued_fraction_iterator, continued_fraction_reduce, \ + continued_fraction_convergents, continued_fraction +from .digits import count_digits, digits, is_palindromic +from .egyptian_fraction import egyptian_fraction +from .ecm import ecm +from .qs import qs, qs_factor +__all__ = [ + 'nextprime', 'prevprime', 'prime', 'primepi', 'primerange', 'randprime', + 'Sieve', 'sieve', 'primorial', 'cycle_length', 'composite', 'compositepi', + + 'isprime', 'is_gaussian_prime', 'is_mersenne_prime', + + + 'divisors', 'proper_divisors', 'factorint', 'multiplicity', 'perfect_power', + 'pollard_pm1', 'factor_cache', 'pollard_rho', 'primefactors', 'totient', + 'divisor_count', 'proper_divisor_count', 'divisor_sigma', 'factorrat', + 'reduced_totient', 'primenu', 'primeomega', 'mersenne_prime_exponent', + 'is_perfect', 'is_abundant', 'is_deficient', 'is_amicable', + 'is_carmichael', 'abundance', 'dra', 'drm', 'multiplicity_in_factorial', + + 'npartitions', + + 'is_primitive_root', 'is_quad_residue', 'legendre_symbol', + 'jacobi_symbol', 'n_order', 'sqrt_mod', 'quadratic_residues', + 'primitive_root', 'nthroot_mod', 'is_nthpow_residue', 'sqrt_mod_iter', + 'mobius', 'discrete_log', 'quadratic_congruence', 'polynomial_congruence', + + 'binomial_coefficients', 'binomial_coefficients_list', + 'multinomial_coefficients', + + 'continued_fraction_periodic', 'continued_fraction_iterator', + 'continued_fraction_reduce', 'continued_fraction_convergents', + 'continued_fraction', + + 'digits', + 'count_digits', + 'is_palindromic', + + 'egyptian_fraction', + + 'ecm', + + 'qs', 'qs_factor', +] diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/bbp_pi.py b/venv/lib/python3.11/site-packages/sympy/ntheory/bbp_pi.py new file mode 100644 index 0000000000000000000000000000000000000000..e2ff4b755d74d4e075ac7195f991c8182d175693 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/bbp_pi.py @@ -0,0 +1,190 @@ +''' +This implementation is a heavily modified fixed point implementation of +BBP_formula for calculating the nth position of pi. The original hosted +at: https://web.archive.org/web/20151116045029/http://en.literateprograms.org/Pi_with_the_BBP_formula_(Python) + +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sub-license, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Modifications: + +1.Once the nth digit and desired number of digits is selected, the +number of digits of working precision is calculated to ensure that +the hexadecimal digits returned are accurate. This is calculated as + + int(math.log(start + prec)/math.log(16) + prec + 3) + --------------------------------------- -------- + / / + number of hex digits additional digits + +This was checked by the following code which completed without +errors (and dig are the digits included in the test_bbp.py file): + + for i in range(0,1000): + for j in range(1,1000): + a, b = pi_hex_digits(i, j), dig[i:i+j] + if a != b: + print('%s\n%s'%(a,b)) + +Deceasing the additional digits by 1 generated errors, so '3' is +the smallest additional precision needed to calculate the above +loop without errors. The following trailing 10 digits were also +checked to be accurate (and the times were slightly faster with +some of the constant modifications that were made): + + >> from time import time + >> t=time();pi_hex_digits(10**2-10 + 1, 10), time()-t + ('e90c6cc0ac', 0.0) + >> t=time();pi_hex_digits(10**4-10 + 1, 10), time()-t + ('26aab49ec6', 0.17100000381469727) + >> t=time();pi_hex_digits(10**5-10 + 1, 10), time()-t + ('a22673c1a5', 4.7109999656677246) + >> t=time();pi_hex_digits(10**6-10 + 1, 10), time()-t + ('9ffd342362', 59.985999822616577) + >> t=time();pi_hex_digits(10**7-10 + 1, 10), time()-t + ('c1a42e06a1', 689.51800012588501) + +2. The while loop to evaluate whether the series has converged quits +when the addition amount `dt` has dropped to zero. + +3. the formatting string to convert the decimal to hexadecimal is +calculated for the given precision. + +4. pi_hex_digits(n) changed to have coefficient to the formula in an +array (perhaps just a matter of preference). + +''' + +from sympy.utilities.misc import as_int + + +def _series(j, n, prec=14): + + # Left sum from the bbp algorithm + s = 0 + D = _dn(n, prec) + D4 = 4 * D + d = j + for k in range(n + 1): + s += (pow(16, n - k, d) << D4) // d + d += 8 + + # Right sum iterates to infinity for full precision, but we + # stop at the point where one iteration is beyond the precision + # specified. + + t = 0 + k = n + 1 + e = D4 - 4 # 4*(D + n - k) + d = 8 * k + j + while True: + dt = (1 << e) // d + if not dt: + break + t += dt + # k += 1 + e -= 4 + d += 8 + total = s + t + + return total + + +def pi_hex_digits(n, prec=14): + """Returns a string containing ``prec`` (default 14) digits + starting at the nth digit of pi in hex. Counting of digits + starts at 0 and the decimal is not counted, so for n = 0 the + returned value starts with 3; n = 1 corresponds to the first + digit past the decimal point (which in hex is 2). + + Parameters + ========== + + n : non-negative integer + prec : non-negative integer. default = 14 + + Returns + ======= + + str : Returns a string containing ``prec`` digits + starting at the nth digit of pi in hex. + If ``prec`` = 0, returns empty string. + + Raises + ====== + + ValueError + If ``n`` < 0 or ``prec`` < 0. + Or ``n`` or ``prec`` is not an integer. + + Examples + ======== + + >>> from sympy.ntheory.bbp_pi import pi_hex_digits + >>> pi_hex_digits(0) + '3243f6a8885a30' + >>> pi_hex_digits(0, 3) + '324' + + These are consistent with the following results + + >>> import math + >>> hex(int(math.pi * 2**((14-1)*4))) + '0x3243f6a8885a30' + >>> hex(int(math.pi * 2**((3-1)*4))) + '0x324' + + References + ========== + + .. [1] http://www.numberworld.org/digits/Pi/ + """ + n, prec = as_int(n), as_int(prec) + if n < 0: + raise ValueError('n cannot be negative') + if prec < 0: + raise ValueError('prec cannot be negative') + if prec == 0: + return '' + + # main of implementation arrays holding formulae coefficients + n -= 1 + a = [4, 2, 1, 1] + j = [1, 4, 5, 6] + + #formulae + D = _dn(n, prec) + x = + (a[0]*_series(j[0], n, prec) + - a[1]*_series(j[1], n, prec) + - a[2]*_series(j[2], n, prec) + - a[3]*_series(j[3], n, prec)) & (16**D - 1) + + s = ("%0" + "%ix" % prec) % (x // 16**(D - prec)) + return s + + +def _dn(n, prec): + # controller for n dependence on precision + # n = starting digit index + # prec = the number of total digits to compute + n += 1 # because we subtract 1 for _series + + # assert int(math.log(n + prec)/math.log(16)) ==\ + # ((n + prec).bit_length() - 1) // 4 + return ((n + prec).bit_length() - 1) // 4 + prec + 3 diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/continued_fraction.py b/venv/lib/python3.11/site-packages/sympy/ntheory/continued_fraction.py new file mode 100644 index 0000000000000000000000000000000000000000..62f8e2d729ada3414a87d6f0583e06bee2a2b220 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/continued_fraction.py @@ -0,0 +1,369 @@ +from __future__ import annotations +import itertools +from sympy.core.exprtools import factor_terms +from sympy.core.numbers import Integer, Rational +from sympy.core.singleton import S +from sympy.core.symbol import Dummy +from sympy.core.sympify import _sympify +from sympy.utilities.misc import as_int + + +def continued_fraction(a) -> list: + """Return the continued fraction representation of a Rational or + quadratic irrational. + + Examples + ======== + + >>> from sympy.ntheory.continued_fraction import continued_fraction + >>> from sympy import sqrt + >>> continued_fraction((1 + 2*sqrt(3))/5) + [0, 1, [8, 3, 34, 3]] + + See Also + ======== + continued_fraction_periodic, continued_fraction_reduce, continued_fraction_convergents + """ + e = _sympify(a) + if all(i.is_Rational for i in e.atoms()): + if e.is_Integer: + return continued_fraction_periodic(e, 1, 0) + elif e.is_Rational: + return continued_fraction_periodic(e.p, e.q, 0) + elif e.is_Pow and e.exp is S.Half and e.base.is_Integer: + return continued_fraction_periodic(0, 1, e.base) + elif e.is_Mul and len(e.args) == 2 and ( + e.args[0].is_Rational and + e.args[1].is_Pow and + e.args[1].base.is_Integer and + e.args[1].exp is S.Half): + a, b = e.args + return continued_fraction_periodic(0, a.q, b.base, a.p) + else: + # this should not have to work very hard- no + # simplification, cancel, etc... which should be + # done by the user. e.g. This is a fancy 1 but + # the user should simplify it first: + # sqrt(2)*(1 + sqrt(2))/(sqrt(2) + 2) + p, d = e.expand().as_numer_denom() + if d.is_Integer: + if p.is_Rational: + return continued_fraction_periodic(p, d) + # look for a + b*c + # with c = sqrt(s) + if p.is_Add and len(p.args) == 2: + a, bc = p.args + else: + a = S.Zero + bc = p + if a.is_Integer: + b = S.NaN + if bc.is_Mul and len(bc.args) == 2: + b, c = bc.args + elif bc.is_Pow: + b = Integer(1) + c = bc + if b.is_Integer and ( + c.is_Pow and c.exp is S.Half and + c.base.is_Integer): + # (a + b*sqrt(c))/d + c = c.base + return continued_fraction_periodic(a, d, c, b) + raise ValueError( + 'expecting a rational or quadratic irrational, not %s' % e) + + +def continued_fraction_periodic(p, q, d=0, s=1) -> list: + r""" + Find the periodic continued fraction expansion of a quadratic irrational. + + Compute the continued fraction expansion of a rational or a + quadratic irrational number, i.e. `\frac{p + s\sqrt{d}}{q}`, where + `p`, `q \ne 0` and `d \ge 0` are integers. + + Returns the continued fraction representation (canonical form) as + a list of integers, optionally ending (for quadratic irrationals) + with list of integers representing the repeating digits. + + Parameters + ========== + + p : int + the rational part of the number's numerator + q : int + the denominator of the number + d : int, optional + the irrational part (discriminator) of the number's numerator + s : int, optional + the coefficient of the irrational part + + Examples + ======== + + >>> from sympy.ntheory.continued_fraction import continued_fraction_periodic + >>> continued_fraction_periodic(3, 2, 7) + [2, [1, 4, 1, 1]] + + Golden ratio has the simplest continued fraction expansion: + + >>> continued_fraction_periodic(1, 2, 5) + [[1]] + + If the discriminator is zero or a perfect square then the number will be a + rational number: + + >>> continued_fraction_periodic(4, 3, 0) + [1, 3] + >>> continued_fraction_periodic(4, 3, 49) + [3, 1, 2] + + See Also + ======== + + continued_fraction_iterator, continued_fraction_reduce + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Periodic_continued_fraction + .. [2] K. Rosen. Elementary Number theory and its applications. + Addison-Wesley, 3 Sub edition, pages 379-381, January 1992. + + """ + from sympy.functions import sqrt, floor + + p, q, d, s = list(map(as_int, [p, q, d, s])) + + if d < 0: + raise ValueError("expected non-negative for `d` but got %s" % d) + + if q == 0: + raise ValueError("The denominator cannot be 0.") + + if not s: + d = 0 + + # check for rational case + sd = sqrt(d) + if sd.is_Integer: + return list(continued_fraction_iterator(Rational(p + s*sd, q))) + + # irrational case with sd != Integer + if q < 0: + p, q, s = -p, -q, -s + + n = (p + s*sd)/q + if n < 0: + w = floor(-n) + f = -n - w + one_f = continued_fraction(1 - f) # 1-f < 1 so cf is [0 ... [...]] + one_f[0] -= w + 1 + return one_f + + d *= s**2 + sd *= s + + if (d - p**2)%q: + d *= q**2 + sd *= q + p *= q + q *= q + + terms: list[int] = [] + pq = {} + + while (p, q) not in pq: + pq[(p, q)] = len(terms) + terms.append((p + sd)//q) + p = terms[-1]*q - p + q = (d - p**2)//q + + i = pq[(p, q)] + return terms[:i] + [terms[i:]] # type: ignore + + +def continued_fraction_reduce(cf): + """ + Reduce a continued fraction to a rational or quadratic irrational. + + Compute the rational or quadratic irrational number from its + terminating or periodic continued fraction expansion. The + continued fraction expansion (cf) should be supplied as a + terminating iterator supplying the terms of the expansion. For + terminating continued fractions, this is equivalent to + ``list(continued_fraction_convergents(cf))[-1]``, only a little more + efficient. If the expansion has a repeating part, a list of the + repeating terms should be returned as the last element from the + iterator. This is the format returned by + continued_fraction_periodic. + + For quadratic irrationals, returns the largest solution found, + which is generally the one sought, if the fraction is in canonical + form (all terms positive except possibly the first). + + Examples + ======== + + >>> from sympy.ntheory.continued_fraction import continued_fraction_reduce + >>> continued_fraction_reduce([1, 2, 3, 4, 5]) + 225/157 + >>> continued_fraction_reduce([-2, 1, 9, 7, 1, 2]) + -256/233 + >>> continued_fraction_reduce([2, 1, 2, 1, 1, 4, 1, 1, 6, 1, 1, 8]).n(10) + 2.718281835 + >>> continued_fraction_reduce([1, 4, 2, [3, 1]]) + (sqrt(21) + 287)/238 + >>> continued_fraction_reduce([[1]]) + (1 + sqrt(5))/2 + >>> from sympy.ntheory.continued_fraction import continued_fraction_periodic + >>> continued_fraction_reduce(continued_fraction_periodic(8, 5, 13)) + (sqrt(13) + 8)/5 + + See Also + ======== + + continued_fraction_periodic + + """ + from sympy.solvers import solve + + period = [] + x = Dummy('x') + + def untillist(cf): + for nxt in cf: + if isinstance(nxt, list): + period.extend(nxt) + yield x + break + yield nxt + + a = S.Zero + for a in continued_fraction_convergents(untillist(cf)): + pass + + if period: + y = Dummy('y') + solns = solve(continued_fraction_reduce(period + [y]) - y, y) + solns.sort() + pure = solns[-1] + rv = a.subs(x, pure).radsimp() + else: + rv = a + if rv.is_Add: + rv = factor_terms(rv) + if rv.is_Mul and rv.args[0] == -1: + rv = rv.func(*rv.args) + return rv + + +def continued_fraction_iterator(x): + """ + Return continued fraction expansion of x as iterator. + + Examples + ======== + + >>> from sympy import Rational, pi + >>> from sympy.ntheory.continued_fraction import continued_fraction_iterator + + >>> list(continued_fraction_iterator(Rational(3, 8))) + [0, 2, 1, 2] + >>> list(continued_fraction_iterator(Rational(-3, 8))) + [-1, 1, 1, 1, 2] + + >>> for i, v in enumerate(continued_fraction_iterator(pi)): + ... if i > 7: + ... break + ... print(v) + 3 + 7 + 15 + 1 + 292 + 1 + 1 + 1 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Continued_fraction + + """ + from sympy.functions import floor + while True: + i = floor(x) + yield i + x -= i + if not x: + break + x = 1/x + + +def continued_fraction_convergents(cf): + """ + Return an iterator over the convergents of a continued fraction (cf). + + The parameter should be in either of the following to forms: + - A list of partial quotients, possibly with the last element being a list + of repeating partial quotients, such as might be returned by + continued_fraction and continued_fraction_periodic. + - An iterable returning successive partial quotients of the continued + fraction, such as might be returned by continued_fraction_iterator. + + In computing the convergents, the continued fraction need not be strictly + in canonical form (all integers, all but the first positive). + Rational and negative elements may be present in the expansion. + + Examples + ======== + + >>> from sympy.core import pi + >>> from sympy import S + >>> from sympy.ntheory.continued_fraction import \ + continued_fraction_convergents, continued_fraction_iterator + + >>> list(continued_fraction_convergents([0, 2, 1, 2])) + [0, 1/2, 1/3, 3/8] + + >>> list(continued_fraction_convergents([1, S('1/2'), -7, S('1/4')])) + [1, 3, 19/5, 7] + + >>> it = continued_fraction_convergents(continued_fraction_iterator(pi)) + >>> for n in range(7): + ... print(next(it)) + 3 + 22/7 + 333/106 + 355/113 + 103993/33102 + 104348/33215 + 208341/66317 + + >>> it = continued_fraction_convergents([1, [1, 2]]) # sqrt(3) + >>> for n in range(7): + ... print(next(it)) + 1 + 2 + 5/3 + 7/4 + 19/11 + 26/15 + 71/41 + + See Also + ======== + + continued_fraction_iterator, continued_fraction, continued_fraction_periodic + + """ + if isinstance(cf, list) and isinstance(cf[-1], list): + cf = itertools.chain(cf[:-1], itertools.cycle(cf[-1])) + p_2, q_2 = S.Zero, S.One + p_1, q_1 = S.One, S.Zero + for a in cf: + p, q = a*p_1 + p_2, a*q_1 + q_2 + p_2, q_2 = p_1, q_1 + p_1, q_1 = p, q + yield p/q diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/digits.py b/venv/lib/python3.11/site-packages/sympy/ntheory/digits.py new file mode 100644 index 0000000000000000000000000000000000000000..a0414815871f6f888ccd2823546ab2b0c2c9f515 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/digits.py @@ -0,0 +1,150 @@ +from collections import defaultdict + +from sympy.utilities.iterables import multiset, is_palindromic as _palindromic +from sympy.utilities.misc import as_int + + +def digits(n, b=10, digits=None): + """ + Return a list of the digits of ``n`` in base ``b``. The first + element in the list is ``b`` (or ``-b`` if ``n`` is negative). + + Examples + ======== + + >>> from sympy.ntheory.digits import digits + >>> digits(35) + [10, 3, 5] + + If the number is negative, the negative sign will be placed on the + base (which is the first element in the returned list): + + >>> digits(-35) + [-10, 3, 5] + + Bases other than 10 (and greater than 1) can be selected with ``b``: + + >>> digits(27, b=2) + [2, 1, 1, 0, 1, 1] + + Use the ``digits`` keyword if a certain number of digits is desired: + + >>> digits(35, digits=4) + [10, 0, 0, 3, 5] + + Parameters + ========== + + n: integer + The number whose digits are returned. + + b: integer + The base in which digits are computed. + + digits: integer (or None for all digits) + The number of digits to be returned (padded with zeros, if + necessary). + + See Also + ======== + sympy.core.intfunc.num_digits, count_digits + """ + + b = as_int(b) + n = as_int(n) + if b < 2: + raise ValueError("b must be greater than 1") + else: + x, y = abs(n), [] + while x >= b: + x, r = divmod(x, b) + y.append(r) + y.append(x) + y.append(-b if n < 0 else b) + y.reverse() + ndig = len(y) - 1 + if digits is not None: + if ndig > digits: + raise ValueError( + "For %s, at least %s digits are needed." % (n, ndig)) + elif ndig < digits: + y[1:1] = [0]*(digits - ndig) + return y + + +def count_digits(n, b=10): + """ + Return a dictionary whose keys are the digits of ``n`` in the + given base, ``b``, with keys indicating the digits appearing in the + number and values indicating how many times that digit appeared. + + Examples + ======== + + >>> from sympy.ntheory import count_digits + + >>> count_digits(1111339) + {1: 4, 3: 2, 9: 1} + + The digits returned are always represented in base-10 + but the number itself can be entered in any format that is + understood by Python; the base of the number can also be + given if it is different than 10: + + >>> n = 0xFA; n + 250 + >>> count_digits(_) + {0: 1, 2: 1, 5: 1} + >>> count_digits(n, 16) + {10: 1, 15: 1} + + The default dictionary will return a 0 for any digit that did + not appear in the number. For example, which digits appear 7 + times in ``77!``: + + >>> from sympy import factorial + >>> c77 = count_digits(factorial(77)) + >>> [i for i in range(10) if c77[i] == 7] + [1, 3, 7, 9] + + See Also + ======== + sympy.core.intfunc.num_digits, digits + """ + rv = defaultdict(int, multiset(digits(n, b)).items()) + rv.pop(b) if b in rv else rv.pop(-b) # b or -b is there + return rv + + +def is_palindromic(n, b=10): + """return True if ``n`` is the same when read from left to right + or right to left in the given base, ``b``. + + Examples + ======== + + >>> from sympy.ntheory import is_palindromic + + >>> all(is_palindromic(i) for i in (-11, 1, 22, 121)) + True + + The second argument allows you to test numbers in other + bases. For example, 88 is palindromic in base-10 but not + in base-8: + + >>> is_palindromic(88, 8) + False + + On the other hand, a number can be palindromic in base-8 but + not in base-10: + + >>> 0o121, is_palindromic(0o121) + (81, False) + + Or it might be palindromic in both bases: + + >>> oct(121), is_palindromic(121, 8) and is_palindromic(121) + ('0o171', True) + + """ + return _palindromic(digits(n, b), 1) diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/ecm.py b/venv/lib/python3.11/site-packages/sympy/ntheory/ecm.py new file mode 100644 index 0000000000000000000000000000000000000000..498c0c8fdf8478688465c4bae307818e9685b686 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/ecm.py @@ -0,0 +1,348 @@ +from math import log + +from sympy.core.random import _randint +from sympy.external.gmpy import gcd, invert, sqrt +from sympy.utilities.misc import as_int +from .generate import sieve, primerange +from .primetest import isprime + + +#----------------------------------------------------------------------------# +# # +# Lenstra's Elliptic Curve Factorization # +# # +#----------------------------------------------------------------------------# + + +class Point: + """Montgomery form of Points in an elliptic curve. + In this form, the addition and doubling of points + does not need any y-coordinate information thus + decreasing the number of operations. + Using Montgomery form we try to perform point addition + and doubling in least amount of multiplications. + + The elliptic curve used here is of the form + (E : b*y**2*z = x**3 + a*x**2*z + x*z**2). + The a_24 parameter is equal to (a + 2)/4. + + References + ========== + + .. [1] Kris Gaj, Soonhak Kwon, Patrick Baier, Paul Kohlbrenner, Hoang Le, Mohammed Khaleeluddin, Ramakrishna Bachimanchi, + Implementing the Elliptic Curve Method of Factoring in Reconfigurable Hardware, + Cryptographic Hardware and Embedded Systems - CHES 2006 (2006), pp. 119-133, + https://doi.org/10.1007/11894063_10 + https://www.hyperelliptic.org/tanja/SHARCS/talks06/Gaj.pdf + + """ + + def __init__(self, x_cord, z_cord, a_24, mod): + """ + Initial parameters for the Point class. + + Parameters + ========== + + x_cord : X coordinate of the Point + z_cord : Z coordinate of the Point + a_24 : Parameter of the elliptic curve in Montgomery form + mod : modulus + """ + self.x_cord = x_cord + self.z_cord = z_cord + self.a_24 = a_24 + self.mod = mod + + def __eq__(self, other): + """Two points are equal if X/Z of both points are equal + """ + if self.a_24 != other.a_24 or self.mod != other.mod: + return False + return self.x_cord * other.z_cord % self.mod ==\ + other.x_cord * self.z_cord % self.mod + + def add(self, Q, diff): + """ + Add two points self and Q where diff = self - Q. Moreover the assumption + is self.x_cord*Q.x_cord*(self.x_cord - Q.x_cord) != 0. This algorithm + requires 6 multiplications. Here the difference between the points + is already known and using this algorithm speeds up the addition + by reducing the number of multiplication required. Also in the + mont_ladder algorithm is constructed in a way so that the difference + between intermediate points is always equal to the initial point. + So, we always know what the difference between the point is. + + + Parameters + ========== + + Q : point on the curve in Montgomery form + diff : self - Q + + Examples + ======== + + >>> from sympy.ntheory.ecm import Point + >>> p1 = Point(11, 16, 7, 29) + >>> p2 = Point(13, 10, 7, 29) + >>> p3 = p2.add(p1, p1) + >>> p3.x_cord + 23 + >>> p3.z_cord + 17 + """ + u = (self.x_cord - self.z_cord)*(Q.x_cord + Q.z_cord) + v = (self.x_cord + self.z_cord)*(Q.x_cord - Q.z_cord) + add, subt = u + v, u - v + x_cord = diff.z_cord * add * add % self.mod + z_cord = diff.x_cord * subt * subt % self.mod + return Point(x_cord, z_cord, self.a_24, self.mod) + + def double(self): + """ + Doubles a point in an elliptic curve in Montgomery form. + This algorithm requires 5 multiplications. + + Examples + ======== + + >>> from sympy.ntheory.ecm import Point + >>> p1 = Point(11, 16, 7, 29) + >>> p2 = p1.double() + >>> p2.x_cord + 13 + >>> p2.z_cord + 10 + """ + u = pow(self.x_cord + self.z_cord, 2, self.mod) + v = pow(self.x_cord - self.z_cord, 2, self.mod) + diff = u - v + x_cord = u*v % self.mod + z_cord = diff*(v + self.a_24*diff) % self.mod + return Point(x_cord, z_cord, self.a_24, self.mod) + + def mont_ladder(self, k): + """ + Scalar multiplication of a point in Montgomery form + using Montgomery Ladder Algorithm. + A total of 11 multiplications are required in each step of this + algorithm. + + Parameters + ========== + + k : The positive integer multiplier + + Examples + ======== + + >>> from sympy.ntheory.ecm import Point + >>> p1 = Point(11, 16, 7, 29) + >>> p3 = p1.mont_ladder(3) + >>> p3.x_cord + 23 + >>> p3.z_cord + 17 + """ + Q = self + R = self.double() + for i in bin(k)[3:]: + if i == '1': + Q = R.add(Q, self) + R = R.double() + else: + R = Q.add(R, self) + Q = Q.double() + return Q + + +def _ecm_one_factor(n, B1=10000, B2=100000, max_curve=200, seed=None): + """Returns one factor of n using + Lenstra's 2 Stage Elliptic curve Factorization + with Suyama's Parameterization. Here Montgomery + arithmetic is used for fast computation of addition + and doubling of points in elliptic curve. + + Explanation + =========== + + This ECM method considers elliptic curves in Montgomery + form (E : b*y**2*z = x**3 + a*x**2*z + x*z**2) and involves + elliptic curve operations (mod N), where the elements in + Z are reduced (mod N). Since N is not a prime, E over FF(N) + is not really an elliptic curve but we can still do point additions + and doubling as if FF(N) was a field. + + Stage 1 : The basic algorithm involves taking a random point (P) on an + elliptic curve in FF(N). The compute k*P using Montgomery ladder algorithm. + Let q be an unknown factor of N. Then the order of the curve E, |E(FF(q))|, + might be a smooth number that divides k. Then we have k = l * |E(FF(q))| + for some l. For any point belonging to the curve E, |E(FF(q))|*P = O, + hence k*P = l*|E(FF(q))|*P. Thus kP.z_cord = 0 (mod q), and the unknownn + factor of N (q) can be recovered by taking gcd(kP.z_cord, N). + + Stage 2 : This is a continuation of Stage 1 if k*P != O. The idea utilize + the fact that even if kP != 0, the value of k might miss just one large + prime divisor of |E(FF(q))|. In this case we only need to compute the + scalar multiplication by p to get p*k*P = O. Here a second bound B2 + restrict the size of possible values of p. + + Parameters + ========== + + n : Number to be Factored. Assume that it is a composite number. + B1 : Stage 1 Bound. Must be an even number. + B2 : Stage 2 Bound. Must be an even number. + max_curve : Maximum number of curves generated + + Returns + ======= + + integer | None : a non-trivial divisor of ``n``. ``None`` if not found + + References + ========== + + .. [1] Carl Pomerance, Richard Crandall, Prime Numbers: A Computational Perspective, + 2nd Edition (2005), page 344, ISBN:978-0387252827 + """ + randint = _randint(seed) + + # When calculating T, if (B1 - 2*D) is negative, it cannot be calculated. + D = min(sqrt(B2), B1 // 2 - 1) + sieve.extend(D) + beta = [0] * D + S = [0] * D + k = 1 + for p in primerange(2, B1 + 1): + k *= pow(p, int(log(B1, p))) + + # Pre-calculate the prime numbers to be used in stage 2. + # Using the fact that the x-coordinates of point P and its + # inverse -P coincide, the number of primes to be checked + # in stage 2 can be reduced. + deltas_list = [] + for r in range(B1 + 2*D, B2 + 2*D, 4*D): + # d in deltas iff r+(2d+1) and/or r-(2d+1) is prime + deltas = {abs(q - r) >> 1 for q in primerange(r - 2*D, r + 2*D)} + deltas_list.append(list(deltas)) + + for _ in range(max_curve): + #Suyama's Parametrization + sigma = randint(6, n - 1) + u = (sigma**2 - 5) % n + v = (4*sigma) % n + u_3 = pow(u, 3, n) + + try: + # We use the elliptic curve y**2 = x**3 + a*x**2 + x + # where a = pow(v - u, 3, n)*(3*u + v)*invert(4*u_3*v, n) - 2 + # However, we do not declare a because it is more convenient + # to use a24 = (a + 2)*invert(4, n) in the calculation. + a24 = pow(v - u, 3, n)*(3*u + v)*invert(16*u_3*v, n) % n + except ZeroDivisionError: + #If the invert(16*u_3*v, n) doesn't exist (i.e., g != 1) + g = gcd(2*u_3*v, n) + #If g = n, try another curve + if g == n: + continue + return g + + Q = Point(u_3, pow(v, 3, n), a24, n) + Q = Q.mont_ladder(k) + g = gcd(Q.z_cord, n) + + #Stage 1 factor + if g != 1 and g != n: + return g + #Stage 1 failure. Q.z = 0, Try another curve + elif g == n: + continue + + #Stage 2 - Improved Standard Continuation + S[0] = Q + Q2 = Q.double() + S[1] = Q2.add(Q, Q) + beta[0] = (S[0].x_cord*S[0].z_cord) % n + beta[1] = (S[1].x_cord*S[1].z_cord) % n + for d in range(2, D): + S[d] = S[d - 1].add(Q2, S[d - 2]) + beta[d] = (S[d].x_cord*S[d].z_cord) % n + # i.e., S[i] = Q.mont_ladder(2*i + 1) + + g = 1 + W = Q.mont_ladder(4*D) + T = Q.mont_ladder(B1 - 2*D) + R = Q.mont_ladder(B1 + 2*D) + for deltas in deltas_list: + # R = Q.mont_ladder(r) where r in range(B1 + 2*D, B2 + 2*D, 4*D) + alpha = (R.x_cord*R.z_cord) % n + for delta in deltas: + # We want to calculate + # f = R.x_cord * S[delta].z_cord - S[delta].x_cord * R.z_cord + f = (R.x_cord - S[delta].x_cord)*\ + (R.z_cord + S[delta].z_cord) - alpha + beta[delta] + g = (g*f) % n + T, R = R, R.add(W, T) + g = gcd(n, g) + + #Stage 2 Factor found + if g != 1 and g != n: + return g + + +def ecm(n, B1=10000, B2=100000, max_curve=200, seed=1234): + """Performs factorization using Lenstra's Elliptic curve method. + + This function repeatedly calls ``_ecm_one_factor`` to compute the factors + of n. First all the small factors are taken out using trial division. + Then ``_ecm_one_factor`` is used to compute one factor at a time. + + Parameters + ========== + + n : Number to be Factored + B1 : Stage 1 Bound. Must be an even number. + B2 : Stage 2 Bound. Must be an even number. + max_curve : Maximum number of curves generated + seed : Initialize pseudorandom generator + + Examples + ======== + + >>> from sympy.ntheory import ecm + >>> ecm(25645121643901801) + {5394769, 4753701529} + >>> ecm(9804659461513846513) + {4641991, 2112166839943} + """ + from .factor_ import _perfect_power + n = as_int(n) + if B1 % 2 != 0 or B2 % 2 != 0: + raise ValueError("both bounds must be even") + TF_LIMIT = 100000 + factors = set() + for prime in sieve.primerange(2, TF_LIMIT): + if n % prime == 0: + factors.add(prime) + while(n % prime == 0): + n //= prime + + queue = [] + def check(m): + if isprime(m): + factors.add(m) + return + if result := _perfect_power(m, TF_LIMIT): + return check(result[0]) + queue.append(m) + check(n) + while queue: + n = queue.pop() + factor = _ecm_one_factor(n, B1, B2, max_curve, seed) + if factor is None: + raise ValueError("Increase the bounds") + check(factor) + check(n // factor) + return factors diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/egyptian_fraction.py b/venv/lib/python3.11/site-packages/sympy/ntheory/egyptian_fraction.py new file mode 100644 index 0000000000000000000000000000000000000000..8a42540b372042f596808684fef8e3fc57935b74 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/egyptian_fraction.py @@ -0,0 +1,223 @@ +from sympy.core.containers import Tuple +from sympy.core.numbers import (Integer, Rational) +from sympy.core.singleton import S +import sympy.polys + +from math import gcd + + +def egyptian_fraction(r, algorithm="Greedy"): + """ + Return the list of denominators of an Egyptian fraction + expansion [1]_ of the said rational `r`. + + Parameters + ========== + + r : Rational or (p, q) + a positive rational number, ``p/q``. + algorithm : { "Greedy", "Graham Jewett", "Takenouchi", "Golomb" }, optional + Denotes the algorithm to be used (the default is "Greedy"). + + Examples + ======== + + >>> from sympy import Rational + >>> from sympy.ntheory.egyptian_fraction import egyptian_fraction + >>> egyptian_fraction(Rational(3, 7)) + [3, 11, 231] + >>> egyptian_fraction((3, 7), "Graham Jewett") + [7, 8, 9, 56, 57, 72, 3192] + >>> egyptian_fraction((3, 7), "Takenouchi") + [4, 7, 28] + >>> egyptian_fraction((3, 7), "Golomb") + [3, 15, 35] + >>> egyptian_fraction((11, 5), "Golomb") + [1, 2, 3, 4, 9, 234, 1118, 2580] + + See Also + ======== + + sympy.core.numbers.Rational + + Notes + ===== + + Currently the following algorithms are supported: + + 1) Greedy Algorithm + + Also called the Fibonacci-Sylvester algorithm [2]_. + At each step, extract the largest unit fraction less + than the target and replace the target with the remainder. + + It has some distinct properties: + + a) Given `p/q` in lowest terms, generates an expansion of maximum + length `p`. Even as the numerators get large, the number of + terms is seldom more than a handful. + + b) Uses minimal memory. + + c) The terms can blow up (standard examples of this are 5/121 and + 31/311). The denominator is at most squared at each step + (doubly-exponential growth) and typically exhibits + singly-exponential growth. + + 2) Graham Jewett Algorithm + + The algorithm suggested by the result of Graham and Jewett. + Note that this has a tendency to blow up: the length of the + resulting expansion is always ``2**(x/gcd(x, y)) - 1``. See [3]_. + + 3) Takenouchi Algorithm + + The algorithm suggested by Takenouchi (1921). + Differs from the Graham-Jewett algorithm only in the handling + of duplicates. See [3]_. + + 4) Golomb's Algorithm + + A method given by Golumb (1962), using modular arithmetic and + inverses. It yields the same results as a method using continued + fractions proposed by Bleicher (1972). See [4]_. + + If the given rational is greater than or equal to 1, a greedy algorithm + of summing the harmonic sequence 1/1 + 1/2 + 1/3 + ... is used, taking + all the unit fractions of this sequence until adding one more would be + greater than the given number. This list of denominators is prefixed + to the result from the requested algorithm used on the remainder. For + example, if r is 8/3, using the Greedy algorithm, we get [1, 2, 3, 4, + 5, 6, 7, 14, 420], where the beginning of the sequence, [1, 2, 3, 4, 5, + 6, 7] is part of the harmonic sequence summing to 363/140, leaving a + remainder of 31/420, which yields [14, 420] by the Greedy algorithm. + The result of egyptian_fraction(Rational(8, 3), "Golomb") is [1, 2, 3, + 4, 5, 6, 7, 14, 574, 2788, 6460, 11590, 33062, 113820], and so on. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Egyptian_fraction + .. [2] https://en.wikipedia.org/wiki/Greedy_algorithm_for_Egyptian_fractions + .. [3] https://www.ics.uci.edu/~eppstein/numth/egypt/conflict.html + .. [4] https://web.archive.org/web/20180413004012/https://ami.ektf.hu/uploads/papers/finalpdf/AMI_42_from129to134.pdf + + """ + + if not isinstance(r, Rational): + if isinstance(r, (Tuple, tuple)) and len(r) == 2: + r = Rational(*r) + else: + raise ValueError("Value must be a Rational or tuple of ints") + if r <= 0: + raise ValueError("Value must be positive") + + # common cases that all methods agree on + x, y = r.as_numer_denom() + if y == 1 and x == 2: + return [Integer(i) for i in [1, 2, 3, 6]] + if x == y + 1: + return [S.One, y] + + prefix, rem = egypt_harmonic(r) + if rem == 0: + return prefix + # work in Python ints + x, y = rem.p, rem.q + # assert x < y and gcd(x, y) = 1 + + if algorithm == "Greedy": + postfix = egypt_greedy(x, y) + elif algorithm == "Graham Jewett": + postfix = egypt_graham_jewett(x, y) + elif algorithm == "Takenouchi": + postfix = egypt_takenouchi(x, y) + elif algorithm == "Golomb": + postfix = egypt_golomb(x, y) + else: + raise ValueError("Entered invalid algorithm") + return prefix + [Integer(i) for i in postfix] + + +def egypt_greedy(x, y): + # assumes gcd(x, y) == 1 + if x == 1: + return [y] + else: + a = (-y) % x + b = y*(y//x + 1) + c = gcd(a, b) + if c > 1: + num, denom = a//c, b//c + else: + num, denom = a, b + return [y//x + 1] + egypt_greedy(num, denom) + + +def egypt_graham_jewett(x, y): + # assumes gcd(x, y) == 1 + l = [y] * x + + # l is now a list of integers whose reciprocals sum to x/y. + # we shall now proceed to manipulate the elements of l without + # changing the reciprocated sum until all elements are unique. + + while len(l) != len(set(l)): + l.sort() # so the list has duplicates. find a smallest pair + for i in range(len(l) - 1): + if l[i] == l[i + 1]: + break + # we have now identified a pair of identical + # elements: l[i] and l[i + 1]. + # now comes the application of the result of graham and jewett: + l[i + 1] = l[i] + 1 + # and we just iterate that until the list has no duplicates. + l.append(l[i]*(l[i] + 1)) + return sorted(l) + + +def egypt_takenouchi(x, y): + # assumes gcd(x, y) == 1 + # special cases for 3/y + if x == 3: + if y % 2 == 0: + return [y//2, y] + i = (y - 1)//2 + j = i + 1 + k = j + i + return [j, k, j*k] + l = [y] * x + while len(l) != len(set(l)): + l.sort() + for i in range(len(l) - 1): + if l[i] == l[i + 1]: + break + k = l[i] + if k % 2 == 0: + l[i] = l[i] // 2 + del l[i + 1] + else: + l[i], l[i + 1] = (k + 1)//2, k*(k + 1)//2 + return sorted(l) + + +def egypt_golomb(x, y): + # assumes x < y and gcd(x, y) == 1 + if x == 1: + return [y] + xp = sympy.polys.ZZ.invert(int(x), int(y)) + rv = [xp*y] + rv.extend(egypt_golomb((x*xp - 1)//y, xp)) + return sorted(rv) + + +def egypt_harmonic(r): + # assumes r is Rational + rv = [] + d = S.One + acc = S.Zero + while acc + 1/d <= r: + acc += 1/d + rv.append(d) + d += 1 + return (rv, r - acc) diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/elliptic_curve.py b/venv/lib/python3.11/site-packages/sympy/ntheory/elliptic_curve.py new file mode 100644 index 0000000000000000000000000000000000000000..c969470a6c19a3d17e637529b6615eeba326e84a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/elliptic_curve.py @@ -0,0 +1,397 @@ +from sympy.core.numbers import oo +from sympy.core.symbol import symbols +from sympy.polys.domains import FiniteField, QQ, RationalField, FF +from sympy.polys.polytools import Poly +from sympy.solvers.solvers import solve +from sympy.utilities.iterables import is_sequence +from sympy.utilities.misc import as_int +from .factor_ import divisors +from .residue_ntheory import polynomial_congruence + + +class EllipticCurve: + """ + Create the following Elliptic Curve over domain. + + `y^{2} + a_{1} x y + a_{3} y = x^{3} + a_{2} x^{2} + a_{4} x + a_{6}` + + The default domain is ``QQ``. If no coefficient ``a1``, ``a2``, ``a3``, + is given then it creates a curve with the following form: + + `y^{2} = x^{3} + a_{4} x + a_{6}` + + Examples + ======== + + References + ========== + + .. [1] J. Silverman "A Friendly Introduction to Number Theory" Third Edition + .. [2] https://mathworld.wolfram.com/EllipticDiscriminant.html + .. [3] G. Hardy, E. Wright "An Introduction to the Theory of Numbers" Sixth Edition + + """ + + def __init__(self, a4, a6, a1=0, a2=0, a3=0, modulus=0): + if modulus == 0: + domain = QQ + else: + domain = FF(modulus) + a1, a2, a3, a4, a6 = map(domain.convert, (a1, a2, a3, a4, a6)) + self._domain = domain + self.modulus = modulus + # Calculate discriminant + b2 = a1**2 + 4 * a2 + b4 = 2 * a4 + a1 * a3 + b6 = a3**2 + 4 * a6 + b8 = a1**2 * a6 + 4 * a2 * a6 - a1 * a3 * a4 + a2 * a3**2 - a4**2 + self._b2, self._b4, self._b6, self._b8 = b2, b4, b6, b8 + self._discrim = -b2**2 * b8 - 8 * b4**3 - 27 * b6**2 + 9 * b2 * b4 * b6 + self._a1 = a1 + self._a2 = a2 + self._a3 = a3 + self._a4 = a4 + self._a6 = a6 + x, y, z = symbols('x y z') + self.x, self.y, self.z = x, y, z + self._poly = Poly(y**2*z + a1*x*y*z + a3*y*z**2 - x**3 - a2*x**2*z - a4*x*z**2 - a6*z**3, domain=domain) + if isinstance(self._domain, FiniteField): + self._rank = 0 + elif isinstance(self._domain, RationalField): + self._rank = None + + def __call__(self, x, y, z=1): + return EllipticCurvePoint(x, y, z, self) + + def __contains__(self, point): + if is_sequence(point): + if len(point) == 2: + z1 = 1 + else: + z1 = point[2] + x1, y1 = point[:2] + elif isinstance(point, EllipticCurvePoint): + x1, y1, z1 = point.x, point.y, point.z + else: + raise ValueError('Invalid point.') + if self.characteristic == 0 and z1 == 0: + return True + return self._poly.subs({self.x: x1, self.y: y1, self.z: z1}) == 0 + + def __repr__(self): + return self._poly.__repr__() + + def minimal(self): + """ + Return minimal Weierstrass equation. + + Examples + ======== + + >>> from sympy.ntheory.elliptic_curve import EllipticCurve + + >>> e1 = EllipticCurve(-10, -20, 0, -1, 1) + >>> e1.minimal() + Poly(-x**3 + 13392*x*z**2 + y**2*z + 1080432*z**3, x, y, z, domain='QQ') + + """ + char = self.characteristic + if char == 2: + return self + if char == 3: + return EllipticCurve(self._b4/2, self._b6/4, a2=self._b2/4, modulus=self.modulus) + c4 = self._b2**2 - 24*self._b4 + c6 = -self._b2**3 + 36*self._b2*self._b4 - 216*self._b6 + return EllipticCurve(-27*c4, -54*c6, modulus=self.modulus) + + def points(self): + """ + Return points of curve over Finite Field. + + Examples + ======== + + >>> from sympy.ntheory.elliptic_curve import EllipticCurve + >>> e2 = EllipticCurve(1, 1, 1, 1, 1, modulus=5) + >>> e2.points() + {(0, 2), (1, 4), (2, 0), (2, 2), (3, 0), (3, 1), (4, 0)} + + """ + + char = self.characteristic + all_pt = set() + if char >= 1: + for i in range(char): + congruence_eq = self._poly.subs({self.x: i, self.z: 1}).expr + sol = polynomial_congruence(congruence_eq, char) + all_pt.update((i, num) for num in sol) + return all_pt + else: + raise ValueError("Infinitely many points") + + def points_x(self, x): + """Returns points on the curve for the given x-coordinate.""" + pt = [] + if self._domain == QQ: + for y in solve(self._poly.subs(self.x, x)): + pt.append((x, y)) + else: + congruence_eq = self._poly.subs({self.x: x, self.z: 1}).expr + for y in polynomial_congruence(congruence_eq, self.characteristic): + pt.append((x, y)) + return pt + + def torsion_points(self): + """ + Return torsion points of curve over Rational number. + + Return point objects those are finite order. + According to Nagell-Lutz theorem, torsion point p(x, y) + x and y are integers, either y = 0 or y**2 is divisor + of discriminent. According to Mazur's theorem, there are + at most 15 points in torsion collection. + + Examples + ======== + + >>> from sympy.ntheory.elliptic_curve import EllipticCurve + >>> e2 = EllipticCurve(-43, 166) + >>> sorted(e2.torsion_points()) + [(-5, -16), (-5, 16), O, (3, -8), (3, 8), (11, -32), (11, 32)] + + """ + if self.characteristic > 0: + raise ValueError("No torsion point for Finite Field.") + l = [EllipticCurvePoint.point_at_infinity(self)] + for xx in solve(self._poly.subs({self.y: 0, self.z: 1})): + if xx.is_rational: + l.append(self(xx, 0)) + for i in divisors(self.discriminant, generator=True): + j = int(i**.5) + if j**2 == i: + for xx in solve(self._poly.subs({self.y: j, self.z: 1})): + if not xx.is_rational: + continue + p = self(xx, j) + if p.order() != oo: + l.extend([p, -p]) + return l + + @property + def characteristic(self): + """ + Return domain characteristic. + + Examples + ======== + + >>> from sympy.ntheory.elliptic_curve import EllipticCurve + >>> e2 = EllipticCurve(-43, 166) + >>> e2.characteristic + 0 + + """ + return self._domain.characteristic() + + @property + def discriminant(self): + """ + Return curve discriminant. + + Examples + ======== + + >>> from sympy.ntheory.elliptic_curve import EllipticCurve + >>> e2 = EllipticCurve(0, 17) + >>> e2.discriminant + -124848 + + """ + return int(self._discrim) + + @property + def is_singular(self): + """ + Return True if curve discriminant is equal to zero. + """ + return self.discriminant == 0 + + @property + def j_invariant(self): + """ + Return curve j-invariant. + + Examples + ======== + + >>> from sympy.ntheory.elliptic_curve import EllipticCurve + >>> e1 = EllipticCurve(-2, 0, 0, 1, 1) + >>> e1.j_invariant + 1404928/389 + + """ + c4 = self._b2**2 - 24*self._b4 + return self._domain.to_sympy(c4**3 / self._discrim) + + @property + def order(self): + """ + Number of points in Finite field. + + Examples + ======== + + >>> from sympy.ntheory.elliptic_curve import EllipticCurve + >>> e2 = EllipticCurve(1, 0, modulus=19) + >>> e2.order + 19 + + """ + if self.characteristic == 0: + raise NotImplementedError("Still not implemented") + return len(self.points()) + + @property + def rank(self): + """ + Number of independent points of infinite order. + + For Finite field, it must be 0. + """ + if self._rank is not None: + return self._rank + raise NotImplementedError("Still not implemented") + + +class EllipticCurvePoint: + """ + Point of Elliptic Curve + + Examples + ======== + + >>> from sympy.ntheory.elliptic_curve import EllipticCurve + >>> e1 = EllipticCurve(-17, 16) + >>> p1 = e1(0, -4, 1) + >>> p2 = e1(1, 0) + >>> p1 + p2 + (15, -56) + >>> e3 = EllipticCurve(-1, 9) + >>> e3(1, -3) * 3 + (664/169, 17811/2197) + >>> (e3(1, -3) * 3).order() + oo + >>> e2 = EllipticCurve(-2, 0, 0, 1, 1) + >>> p = e2(-1,1) + >>> q = e2(0, -1) + >>> p+q + (4, 8) + >>> p-q + (1, 0) + >>> 3*p-5*q + (328/361, -2800/6859) + """ + + @staticmethod + def point_at_infinity(curve): + return EllipticCurvePoint(0, 1, 0, curve) + + def __init__(self, x, y, z, curve): + dom = curve._domain.convert + self.x = dom(x) + self.y = dom(y) + self.z = dom(z) + self._curve = curve + self._domain = self._curve._domain + if not self._curve.__contains__(self): + raise ValueError("The curve does not contain this point") + + def __add__(self, p): + if self.z == 0: + return p + if p.z == 0: + return self + x1, y1 = self.x/self.z, self.y/self.z + x2, y2 = p.x/p.z, p.y/p.z + a1 = self._curve._a1 + a2 = self._curve._a2 + a3 = self._curve._a3 + a4 = self._curve._a4 + a6 = self._curve._a6 + if x1 != x2: + slope = (y1 - y2) / (x1 - x2) + yint = (y1 * x2 - y2 * x1) / (x2 - x1) + else: + if (y1 + y2) == 0: + return self.point_at_infinity(self._curve) + slope = (3 * x1**2 + 2*a2*x1 + a4 - a1*y1) / (a1 * x1 + a3 + 2 * y1) + yint = (-x1**3 + a4*x1 + 2*a6 - a3*y1) / (a1*x1 + a3 + 2*y1) + x3 = slope**2 + a1*slope - a2 - x1 - x2 + y3 = -(slope + a1) * x3 - yint - a3 + return self._curve(x3, y3, 1) + + def __lt__(self, other): + return (self.x, self.y, self.z) < (other.x, other.y, other.z) + + def __mul__(self, n): + n = as_int(n) + r = self.point_at_infinity(self._curve) + if n == 0: + return r + if n < 0: + return -self * -n + p = self + while n: + if n & 1: + r = r + p + n >>= 1 + p = p + p + return r + + def __rmul__(self, n): + return self * n + + def __neg__(self): + return EllipticCurvePoint(self.x, -self.y - self._curve._a1*self.x - self._curve._a3, self.z, self._curve) + + def __repr__(self): + if self.z == 0: + return 'O' + dom = self._curve._domain + try: + return '({}, {})'.format(dom.to_sympy(self.x), dom.to_sympy(self.y)) + except TypeError: + pass + return '({}, {})'.format(self.x, self.y) + + def __sub__(self, other): + return self.__add__(-other) + + def order(self): + """ + Return point order n where nP = 0. + + """ + if self.z == 0: + return 1 + if self.y == 0: # P = -P + return 2 + p = self * 2 + if p.y == -self.y: # 2P = -P + return 3 + i = 2 + if self._domain != QQ: + while int(p.x) == p.x and int(p.y) == p.y: + p = self + p + i += 1 + if p.z == 0: + return i + return oo + while p.x.numerator == p.x and p.y.numerator == p.y: + p = self + p + i += 1 + if i > 12: + return oo + if p.z == 0: + return i + return oo diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/factor_.py b/venv/lib/python3.11/site-packages/sympy/ntheory/factor_.py new file mode 100644 index 0000000000000000000000000000000000000000..2dc6ac81c237f000e55014f5e170b27b41335786 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/factor_.py @@ -0,0 +1,2841 @@ +""" +Integer factorization +""" +from __future__ import annotations + +from bisect import bisect_left +from collections import defaultdict, OrderedDict +from collections.abc import MutableMapping +import math + +from sympy.core.containers import Dict +from sympy.core.mul import Mul +from sympy.core.numbers import Rational, Integer +from sympy.core.intfunc import num_digits +from sympy.core.power import Pow +from sympy.core.random import _randint +from sympy.core.singleton import S +from sympy.external.gmpy import (SYMPY_INTS, gcd, sqrt as isqrt, + sqrtrem, iroot, bit_scan1, remove) +from .primetest import isprime, MERSENNE_PRIME_EXPONENTS, is_mersenne_prime +from .generate import sieve, primerange, nextprime +from .digits import digits +from sympy.utilities.decorator import deprecated +from sympy.utilities.iterables import flatten +from sympy.utilities.misc import as_int, filldedent +from .ecm import _ecm_one_factor + + +def smoothness(n): + """ + Return the B-smooth and B-power smooth values of n. + + The smoothness of n is the largest prime factor of n; the power- + smoothness is the largest divisor raised to its multiplicity. + + Examples + ======== + + >>> from sympy.ntheory.factor_ import smoothness + >>> smoothness(2**7*3**2) + (3, 128) + >>> smoothness(2**4*13) + (13, 16) + >>> smoothness(2) + (2, 2) + + See Also + ======== + + factorint, smoothness_p + """ + + if n == 1: + return (1, 1) # not prime, but otherwise this causes headaches + facs = factorint(n) + return max(facs), max(m**facs[m] for m in facs) + + +def smoothness_p(n, m=-1, power=0, visual=None): + """ + Return a list of [m, (p, (M, sm(p + m), psm(p + m)))...] + where: + + 1. p**M is the base-p divisor of n + 2. sm(p + m) is the smoothness of p + m (m = -1 by default) + 3. psm(p + m) is the power smoothness of p + m + + The list is sorted according to smoothness (default) or by power smoothness + if power=1. + + The smoothness of the numbers to the left (m = -1) or right (m = 1) of a + factor govern the results that are obtained from the p +/- 1 type factoring + methods. + + >>> from sympy.ntheory.factor_ import smoothness_p, factorint + >>> smoothness_p(10431, m=1) + (1, [(3, (2, 2, 4)), (19, (1, 5, 5)), (61, (1, 31, 31))]) + >>> smoothness_p(10431) + (-1, [(3, (2, 2, 2)), (19, (1, 3, 9)), (61, (1, 5, 5))]) + >>> smoothness_p(10431, power=1) + (-1, [(3, (2, 2, 2)), (61, (1, 5, 5)), (19, (1, 3, 9))]) + + If visual=True then an annotated string will be returned: + + >>> print(smoothness_p(21477639576571, visual=1)) + p**i=4410317**1 has p-1 B=1787, B-pow=1787 + p**i=4869863**1 has p-1 B=2434931, B-pow=2434931 + + This string can also be generated directly from a factorization dictionary + and vice versa: + + >>> factorint(17*9) + {3: 2, 17: 1} + >>> smoothness_p(_) + 'p**i=3**2 has p-1 B=2, B-pow=2\\np**i=17**1 has p-1 B=2, B-pow=16' + >>> smoothness_p(_) + {3: 2, 17: 1} + + The table of the output logic is: + + ====== ====== ======= ======= + | Visual + ------ ---------------------- + Input True False other + ====== ====== ======= ======= + dict str tuple str + str str tuple dict + tuple str tuple str + n str tuple tuple + mul str tuple tuple + ====== ====== ======= ======= + + See Also + ======== + + factorint, smoothness + """ + + # visual must be True, False or other (stored as None) + if visual in (1, 0): + visual = bool(visual) + elif visual not in (True, False): + visual = None + + if isinstance(n, str): + if visual: + return n + d = {} + for li in n.splitlines(): + k, v = [int(i) for i in + li.split('has')[0].split('=')[1].split('**')] + d[k] = v + if visual is not True and visual is not False: + return d + return smoothness_p(d, visual=False) + elif not isinstance(n, tuple): + facs = factorint(n, visual=False) + + if power: + k = -1 + else: + k = 1 + if isinstance(n, tuple): + rv = n + else: + rv = (m, sorted([(f, + tuple([M] + list(smoothness(f + m)))) + for f, M in list(facs.items())], + key=lambda x: (x[1][k], x[0]))) + + if visual is False or (visual is not True) and (type(n) in [int, Mul]): + return rv + lines = [] + for dat in rv[1]: + dat = flatten(dat) + dat.insert(2, m) + lines.append('p**i=%i**%i has p%+i B=%i, B-pow=%i' % tuple(dat)) + return '\n'.join(lines) + + +def multiplicity(p, n): + """ + Find the greatest integer m such that p**m divides n. + + Examples + ======== + + >>> from sympy import multiplicity, Rational + >>> [multiplicity(5, n) for n in [8, 5, 25, 125, 250]] + [0, 1, 2, 3, 3] + >>> multiplicity(3, Rational(1, 9)) + -2 + + Note: when checking for the multiplicity of a number in a + large factorial it is most efficient to send it as an unevaluated + factorial or to call ``multiplicity_in_factorial`` directly: + + >>> from sympy.ntheory import multiplicity_in_factorial + >>> from sympy import factorial + >>> p = factorial(25) + >>> n = 2**100 + >>> nfac = factorial(n, evaluate=False) + >>> multiplicity(p, nfac) + 52818775009509558395695966887 + >>> _ == multiplicity_in_factorial(p, n) + True + + See Also + ======== + + trailing + + """ + try: + p, n = as_int(p), as_int(n) + except ValueError: + from sympy.functions.combinatorial.factorials import factorial + if all(isinstance(i, (SYMPY_INTS, Rational)) for i in (p, n)): + p = Rational(p) + n = Rational(n) + if p.q == 1: + if n.p == 1: + return -multiplicity(p.p, n.q) + return multiplicity(p.p, n.p) - multiplicity(p.p, n.q) + elif p.p == 1: + return multiplicity(p.q, n.q) + else: + like = min( + multiplicity(p.p, n.p), + multiplicity(p.q, n.q)) + cross = min( + multiplicity(p.q, n.p), + multiplicity(p.p, n.q)) + return like - cross + elif (isinstance(p, (SYMPY_INTS, Integer)) and + isinstance(n, factorial) and + isinstance(n.args[0], Integer) and + n.args[0] >= 0): + return multiplicity_in_factorial(p, n.args[0]) + raise ValueError('expecting ints or fractions, got %s and %s' % (p, n)) + + if n == 0: + raise ValueError('no such integer exists: multiplicity of %s is not-defined' %(n)) + return remove(n, p)[1] + + +def multiplicity_in_factorial(p, n): + """return the largest integer ``m`` such that ``p**m`` divides ``n!`` + without calculating the factorial of ``n``. + + Parameters + ========== + + p : Integer + positive integer + n : Integer + non-negative integer + + Examples + ======== + + >>> from sympy.ntheory import multiplicity_in_factorial + >>> from sympy import factorial + + >>> multiplicity_in_factorial(2, 3) + 1 + + An instructive use of this is to tell how many trailing zeros + a given factorial has. For example, there are 6 in 25!: + + >>> factorial(25) + 15511210043330985984000000 + >>> multiplicity_in_factorial(10, 25) + 6 + + For large factorials, it is much faster/feasible to use + this function rather than computing the actual factorial: + + >>> multiplicity_in_factorial(factorial(25), 2**100) + 52818775009509558395695966887 + + See Also + ======== + + multiplicity + + """ + + p, n = as_int(p), as_int(n) + + if p <= 0: + raise ValueError('expecting positive integer got %s' % p ) + + if n < 0: + raise ValueError('expecting non-negative integer got %s' % n ) + + # keep only the largest of a given multiplicity since those + # of a given multiplicity will be goverened by the behavior + # of the largest factor + f = defaultdict(int) + for k, v in factorint(p).items(): + f[v] = max(k, f[v]) + # multiplicity of p in n! depends on multiplicity + # of prime `k` in p, so we floor divide by `v` + # and keep it if smaller than the multiplicity of p + # seen so far + return min((n + k - sum(digits(n, k)))//(k - 1)//v for v, k in f.items()) + + +def _perfect_power(n, next_p=2): + """ Return integers ``(b, e)`` such that ``n == b**e`` if ``n`` is a unique + perfect power with ``e > 1``, else ``False`` (e.g. 1 is not a perfect power). + + Explanation + =========== + + This is a low-level helper for ``perfect_power``, for internal use. + + Parameters + ========== + + n : int + assume that n is a nonnegative integer + next_p : int + Assume that n has no factor less than next_p. + i.e., all(n % p for p in range(2, next_p)) is True + + Examples + ======== + >>> from sympy.ntheory.factor_ import _perfect_power + >>> _perfect_power(16) + (2, 4) + >>> _perfect_power(17) + False + + """ + if n <= 3: + return False + + factors = {} + g = 0 + multi = 1 + + def done(n, factors, g, multi): + g = gcd(g, multi) + if g == 1: + return False + factors[n] = multi + return math.prod(p**(e//g) for p, e in factors.items()), g + + # If n is small, only trial factoring is faster + if n <= 1_000_000: + n = _factorint_small(factors, n, 1_000, 1_000, next_p)[0] + if n > 1: + return False + g = gcd(*factors.values()) + if g == 1: + return False + return math.prod(p**(e//g) for p, e in factors.items()), g + + # divide by 2 + if next_p < 3: + g = bit_scan1(n) + if g: + if g == 1: + return False + n >>= g + factors[2] = g + if n == 1: + return 2, g + else: + # If `m**g`, then we have found perfect power. + # Otherwise, there is no possibility of perfect power, especially if `g` is prime. + m, _exact = iroot(n, g) + if _exact: + return 2*m, g + elif isprime(g): + return False + next_p = 3 + + # square number? + while n & 7 == 1: # n % 8 == 1: + m, _exact = iroot(n, 2) + if _exact: + n = m + multi <<= 1 + else: + break + if n < next_p**3: + return done(n, factors, g, multi) + + # trial factoring + # Since the maximum value an exponent can take is `log_{next_p}(n)`, + # the number of exponents to be checked can be reduced by performing a trial factoring. + # The value of `tf_max` needs more consideration. + tf_max = n.bit_length()//27 + 24 + if next_p < tf_max: + for p in primerange(next_p, tf_max): + m, t = remove(n, p) + if t: + n = m + t *= multi + _g = gcd(g, t) + if _g == 1: + return False + factors[p] = t + if n == 1: + return math.prod(p**(e//_g) + for p, e in factors.items()), _g + elif g == 0 or _g < g: # If g is updated + g = _g + m, _exact = iroot(n**multi, g) + if _exact: + return m * math.prod(p**(e//g) + for p, e in factors.items()), g + elif isprime(g): + return False + next_p = tf_max + if n < next_p**3: + return done(n, factors, g, multi) + + # check iroot + if g: + # If g is non-zero, the exponent is a divisor of g. + # 2 can be omitted since it has already been checked. + prime_iter = sorted(factorint(g >> bit_scan1(g)).keys()) + else: + # The maximum possible value of the exponent is `log_{next_p}(n)`. + # To compensate for the presence of computational error, 2 is added. + prime_iter = primerange(3, int(math.log(n, next_p)) + 2) + logn = math.log2(n) + threshold = logn / 40 # Threshold for direct calculation + for p in prime_iter: + if threshold < p: + # If p is large, find the power root p directly without `iroot`. + while True: + b = pow(2, logn / p) + rb = int(b + 0.5) + if abs(rb - b) < 0.01 and rb**p == n: + n = rb + multi *= p + logn = math.log2(n) + else: + break + else: + while True: + m, _exact = iroot(n, p) + if _exact: + n = m + multi *= p + logn = math.log2(n) + else: + break + if n < next_p**(p + 2): + break + return done(n, factors, g, multi) + + +def perfect_power(n, candidates=None, big=True, factor=True): + """ + Return ``(b, e)`` such that ``n`` == ``b**e`` if ``n`` is a unique + perfect power with ``e > 1``, else ``False`` (e.g. 1 is not a + perfect power). A ValueError is raised if ``n`` is not Rational. + + By default, the base is recursively decomposed and the exponents + collected so the largest possible ``e`` is sought. If ``big=False`` + then the smallest possible ``e`` (thus prime) will be chosen. + + If ``factor=True`` then simultaneous factorization of ``n`` is + attempted since finding a factor indicates the only possible root + for ``n``. This is True by default since only a few small factors will + be tested in the course of searching for the perfect power. + + The use of ``candidates`` is primarily for internal use; if provided, + False will be returned if ``n`` cannot be written as a power with one + of the candidates as an exponent and factoring (beyond testing for + a factor of 2) will not be attempted. + + Examples + ======== + + >>> from sympy import perfect_power, Rational + >>> perfect_power(16) + (2, 4) + >>> perfect_power(16, big=False) + (4, 2) + + Negative numbers can only have odd perfect powers: + + >>> perfect_power(-4) + False + >>> perfect_power(-8) + (-2, 3) + + Rationals are also recognized: + + >>> perfect_power(Rational(1, 2)**3) + (1/2, 3) + >>> perfect_power(Rational(-3, 2)**3) + (-3/2, 3) + + Notes + ===== + + To know whether an integer is a perfect power of 2 use + + >>> is2pow = lambda n: bool(n and not n & (n - 1)) + >>> [(i, is2pow(i)) for i in range(5)] + [(0, False), (1, True), (2, True), (3, False), (4, True)] + + It is not necessary to provide ``candidates``. When provided + it will be assumed that they are ints. The first one that is + larger than the computed maximum possible exponent will signal + failure for the routine. + + >>> perfect_power(3**8, [9]) + False + >>> perfect_power(3**8, [2, 4, 8]) + (3, 8) + >>> perfect_power(3**8, [4, 8], big=False) + (9, 4) + + See Also + ======== + sympy.core.intfunc.integer_nthroot + sympy.ntheory.primetest.is_square + """ + # negative handling + if n < 0: + if candidates is None: + pp = perfect_power(-n, big=True, factor=factor) + if not pp: + return False + + b, e = pp + e2 = e & (-e) + b, e = b ** e2, e // e2 + + if e <= 1: + return False + + if big or isprime(e): + return -b, e + + for p in primerange(3, e + 1): + if e % p == 0: + return - b ** (e // p), p + + odd_candidates = {i for i in candidates if i % 2} + if not odd_candidates: + return False + + pp = perfect_power(-n, odd_candidates, big, factor) + if pp: + return -pp[0], pp[1] + + return False + + # non-integer handling + if isinstance(n, Rational) and not isinstance(n, Integer): + p, q = n.p, n.q + + if p == 1: + qq = perfect_power(q, candidates, big, factor) + return (S.One / qq[0], qq[1]) if qq is not False else False + + if not (pp:=perfect_power(p, factor=factor)): + return False + if not (qq:=perfect_power(q, factor=factor)): + return False + (num_base, num_exp), (den_base, den_exp) = pp, qq + + def compute_tuple(exponent): + """Helper to compute final result given an exponent""" + new_num = num_base ** (num_exp // exponent) + new_den = den_base ** (den_exp // exponent) + return n.func(new_num, new_den), exponent + + if candidates: + valid_candidates = [i for i in candidates + if num_exp % i == 0 and den_exp % i == 0] + if not valid_candidates: + return False + + e = max(valid_candidates) if big else min(valid_candidates) + return compute_tuple(e) + + g = math.gcd(num_exp, den_exp) + if g == 1: + return False + + if big: + return compute_tuple(g) + + e = next(p for p in primerange(2, g + 1) if g % p == 0) + return compute_tuple(e) + + if candidates is not None: + candidates = set(candidates) + + # positive integer handling + n = as_int(n) + + if candidates is None and big: + return _perfect_power(n) + + if n <= 3: + # no unique exponent for 0, 1 + # 2 and 3 have exponents of 1 + return False + logn = math.log2(n) + max_possible = int(logn) + 2 # only check values less than this + not_square = n % 10 in [2, 3, 7, 8] # squares cannot end in 2, 3, 7, 8 + min_possible = 2 + not_square + if not candidates: + candidates = primerange(min_possible, max_possible) + else: + candidates = sorted([i for i in candidates + if min_possible <= i < max_possible]) + if n%2 == 0: + e = bit_scan1(n) + candidates = [i for i in candidates if e%i == 0] + if big: + candidates = reversed(candidates) + for e in candidates: + r, ok = iroot(n, e) + if ok: + return int(r), e + return False + + def _factors(): + rv = 2 + n % 2 + while True: + yield rv + rv = nextprime(rv) + + for fac, e in zip(_factors(), candidates): + # see if there is a factor present + if factor and n % fac == 0: + # find what the potential power is + e = remove(n, fac)[1] + # if it's a trivial power we are done + if e == 1: + return False + + # maybe the e-th root of n is exact + r, exact = iroot(n, e) + if not exact: + # Having a factor, we know that e is the maximal + # possible value for a root of n. + # If n = fac**e*m can be written as a perfect + # power then see if m can be written as r**E where + # gcd(e, E) != 1 so n = (fac**(e//E)*r)**E + m = n//fac**e + rE = perfect_power(m, candidates=divisors(e, generator=True)) + if not rE: + return False + else: + r, E = rE + r, e = fac**(e//E)*r, E + if not big: + e0 = primefactors(e) + if e0[0] != e: + r, e = r**(e//e0[0]), e0[0] + return int(r), e + + # Weed out downright impossible candidates + if logn/e < 40: + b = 2.0**(logn/e) + if abs(int(b + 0.5) - b) > 0.01: + continue + + # now see if the plausible e makes a perfect power + r, exact = iroot(n, e) + if exact: + if big: + m = perfect_power(r, big=big, factor=factor) + if m: + r, e = m[0], e*m[1] + return int(r), e + + return False + + +class FactorCache(MutableMapping): + """ Provides a cache for prime factors. + ``factor_cache`` is pre-prepared as an instance of ``FactorCache``, + and ``factorint`` internally references it to speed up + the factorization of prime factors. + + While cache is automatically added during the execution of ``factorint``, + users can also manually add prime factors independently. + + >>> from sympy import factor_cache + >>> factor_cache[15] = 5 + + Furthermore, by customizing ``get_external``, + it is also possible to use external databases. + The following is an example using http://factordb.com . + + .. code-block:: python + + import requests + from sympy import factor_cache + + def get_external(self, n: int) -> list[int] | None: + res = requests.get("http://factordb.com/api", params={"query": str(n)}) + if res.status_code != requests.codes.ok: + return None + j = res.json() + if j.get("status") in ["FF", "P"]: + return list(int(p) for p, _ in j.get("factors")) + + factor_cache.get_external = get_external + + Be aware that writing this code will trigger internet access + to factordb.com when calling ``factorint``. + + """ + def __init__(self, maxsize: int | None = None): + self._cache: OrderedDict[int, int] = OrderedDict() + self.maxsize = maxsize + + def __len__(self) -> int: + return len(self._cache) + + def __contains__(self, n) -> bool: + return n in self._cache + + def __getitem__(self, n: int) -> int: + factor = self.get(n) + if factor is None: + raise KeyError(f"{n} does not exist.") + return factor + + def __setitem__(self, n: int, factor: int): + if not (1 < factor <= n and n % factor == 0 and isprime(factor)): + raise ValueError(f"{factor} is not a prime factor of {n}") + self._cache[n] = max(self._cache.get(n, 0), factor) + if self.maxsize is not None and len(self._cache) > self.maxsize: + self._cache.popitem(False) + + def __delitem__(self, n: int): + if n not in self._cache: + raise KeyError(f"{n} does not exist.") + del self._cache[n] + + def __iter__(self): + return self._cache.__iter__() + + def cache_clear(self) -> None: + """ Clear the cache """ + self._cache = OrderedDict() + + @property + def maxsize(self) -> int | None: + """ Returns the maximum cache size; if ``None``, it is unlimited. """ + return self._maxsize + + @maxsize.setter + def maxsize(self, value: int | None) -> None: + if value is not None and value <= 0: + raise ValueError("maxsize must be None or a non-negative integer.") + self._maxsize = value + if value is not None: + while len(self._cache) > value: + self._cache.popitem(False) + + def get(self, n: int, default=None): + """ Return the prime factor of ``n``. + If it does not exist in the cache, return the value of ``default``. + """ + if n <= sieve._list[-1]: + if sieve._list[bisect_left(sieve._list, n)] == n: + return n + if n in self._cache: + self._cache.move_to_end(n) + return self._cache[n] + if factors := self.get_external(n): + self.add(n, factors) + return self._cache[n] + return default + + def add(self, n: int, factors: list[int]) -> None: + for p in sorted(factors, reverse=True): + self[n] = p + n, _ = remove(n, p) + + def get_external(self, n: int) -> list[int] | None: + return None + + +factor_cache = FactorCache(maxsize=1000) + + +def pollard_rho(n, s=2, a=1, retries=5, seed=1234, max_steps=None, F=None): + r""" + Use Pollard's rho method to try to extract a nontrivial factor + of ``n``. The returned factor may be a composite number. If no + factor is found, ``None`` is returned. + + The algorithm generates pseudo-random values of x with a generator + function, replacing x with F(x). If F is not supplied then the + function x**2 + ``a`` is used. The first value supplied to F(x) is ``s``. + Upon failure (if ``retries`` is > 0) a new ``a`` and ``s`` will be + supplied; the ``a`` will be ignored if F was supplied. + + The sequence of numbers generated by such functions generally have a + a lead-up to some number and then loop around back to that number and + begin to repeat the sequence, e.g. 1, 2, 3, 4, 5, 3, 4, 5 -- this leader + and loop look a bit like the Greek letter rho, and thus the name, 'rho'. + + For a given function, very different leader-loop values can be obtained + so it is a good idea to allow for retries: + + >>> from sympy.ntheory.generate import cycle_length + >>> n = 16843009 + >>> F = lambda x:(2048*pow(x, 2, n) + 32767) % n + >>> for s in range(5): + ... print('loop length = %4i; leader length = %3i' % next(cycle_length(F, s))) + ... + loop length = 2489; leader length = 43 + loop length = 78; leader length = 121 + loop length = 1482; leader length = 100 + loop length = 1482; leader length = 286 + loop length = 1482; leader length = 101 + + Here is an explicit example where there is a three element leadup to + a sequence of 3 numbers (11, 14, 4) that then repeat: + + >>> x=2 + >>> for i in range(9): + ... print(x) + ... x=(x**2+12)%17 + ... + 2 + 16 + 13 + 11 + 14 + 4 + 11 + 14 + 4 + >>> next(cycle_length(lambda x: (x**2+12)%17, 2)) + (3, 3) + >>> list(cycle_length(lambda x: (x**2+12)%17, 2, values=True)) + [2, 16, 13, 11, 14, 4] + + Instead of checking the differences of all generated values for a gcd + with n, only the kth and 2*kth numbers are checked, e.g. 1st and 2nd, + 2nd and 4th, 3rd and 6th until it has been detected that the loop has been + traversed. Loops may be many thousands of steps long before rho finds a + factor or reports failure. If ``max_steps`` is specified, the iteration + is cancelled with a failure after the specified number of steps. + + Examples + ======== + + >>> from sympy import pollard_rho + >>> n=16843009 + >>> F=lambda x:(2048*pow(x,2,n) + 32767) % n + >>> pollard_rho(n, F=F) + 257 + + Use the default setting with a bad value of ``a`` and no retries: + + >>> pollard_rho(n, a=n-2, retries=0) + + If retries is > 0 then perhaps the problem will correct itself when + new values are generated for a: + + >>> pollard_rho(n, a=n-2, retries=1) + 257 + + References + ========== + + .. [1] Richard Crandall & Carl Pomerance (2005), "Prime Numbers: + A Computational Perspective", Springer, 2nd edition, 229-231 + + """ + n = int(n) + if n < 5: + raise ValueError('pollard_rho should receive n > 4') + randint = _randint(seed + retries) + V = s + for i in range(retries + 1): + U = V + if not F: + F = lambda x: (pow(x, 2, n) + a) % n + j = 0 + while 1: + if max_steps and (j > max_steps): + break + j += 1 + U = F(U) + V = F(F(V)) # V is 2x further along than U + g = gcd(U - V, n) + if g == 1: + continue + if g == n: + break + return int(g) + V = randint(0, n - 1) + a = randint(1, n - 3) # for x**2 + a, a%n should not be 0 or -2 + F = None + return None + + +def pollard_pm1(n, B=10, a=2, retries=0, seed=1234): + """ + Use Pollard's p-1 method to try to extract a nontrivial factor + of ``n``. Either a divisor (perhaps composite) or ``None`` is returned. + + The value of ``a`` is the base that is used in the test gcd(a**M - 1, n). + The default is 2. If ``retries`` > 0 then if no factor is found after the + first attempt, a new ``a`` will be generated randomly (using the ``seed``) + and the process repeated. + + Note: the value of M is lcm(1..B) = reduce(ilcm, range(2, B + 1)). + + A search is made for factors next to even numbers having a power smoothness + less than ``B``. Choosing a larger B increases the likelihood of finding a + larger factor but takes longer. Whether a factor of n is found or not + depends on ``a`` and the power smoothness of the even number just less than + the factor p (hence the name p - 1). + + Although some discussion of what constitutes a good ``a`` some + descriptions are hard to interpret. At the modular.math site referenced + below it is stated that if gcd(a**M - 1, n) = N then a**M % q**r is 1 + for every prime power divisor of N. But consider the following: + + >>> from sympy.ntheory.factor_ import smoothness_p, pollard_pm1 + >>> n=257*1009 + >>> smoothness_p(n) + (-1, [(257, (1, 2, 256)), (1009, (1, 7, 16))]) + + So we should (and can) find a root with B=16: + + >>> pollard_pm1(n, B=16, a=3) + 1009 + + If we attempt to increase B to 256 we find that it does not work: + + >>> pollard_pm1(n, B=256) + >>> + + But if the value of ``a`` is changed we find that only multiples of + 257 work, e.g.: + + >>> pollard_pm1(n, B=256, a=257) + 1009 + + Checking different ``a`` values shows that all the ones that did not + work had a gcd value not equal to ``n`` but equal to one of the + factors: + + >>> from sympy import ilcm, igcd, factorint, Pow + >>> M = 1 + >>> for i in range(2, 256): + ... M = ilcm(M, i) + ... + >>> set([igcd(pow(a, M, n) - 1, n) for a in range(2, 256) if + ... igcd(pow(a, M, n) - 1, n) != n]) + {1009} + + But does aM % d for every divisor of n give 1? + + >>> aM = pow(255, M, n) + >>> [(d, aM%Pow(*d.args)) for d in factorint(n, visual=True).args] + [(257**1, 1), (1009**1, 1)] + + No, only one of them. So perhaps the principle is that a root will + be found for a given value of B provided that: + + 1) the power smoothness of the p - 1 value next to the root + does not exceed B + 2) a**M % p != 1 for any of the divisors of n. + + By trying more than one ``a`` it is possible that one of them + will yield a factor. + + Examples + ======== + + With the default smoothness bound, this number cannot be cracked: + + >>> from sympy.ntheory import pollard_pm1 + >>> pollard_pm1(21477639576571) + + Increasing the smoothness bound helps: + + >>> pollard_pm1(21477639576571, B=2000) + 4410317 + + Looking at the smoothness of the factors of this number we find: + + >>> from sympy.ntheory.factor_ import smoothness_p, factorint + >>> print(smoothness_p(21477639576571, visual=1)) + p**i=4410317**1 has p-1 B=1787, B-pow=1787 + p**i=4869863**1 has p-1 B=2434931, B-pow=2434931 + + The B and B-pow are the same for the p - 1 factorizations of the divisors + because those factorizations had a very large prime factor: + + >>> factorint(4410317 - 1) + {2: 2, 617: 1, 1787: 1} + >>> factorint(4869863-1) + {2: 1, 2434931: 1} + + Note that until B reaches the B-pow value of 1787, the number is not cracked; + + >>> pollard_pm1(21477639576571, B=1786) + >>> pollard_pm1(21477639576571, B=1787) + 4410317 + + The B value has to do with the factors of the number next to the divisor, + not the divisors themselves. A worst case scenario is that the number next + to the factor p has a large prime divisisor or is a perfect power. If these + conditions apply then the power-smoothness will be about p/2 or p. The more + realistic is that there will be a large prime factor next to p requiring + a B value on the order of p/2. Although primes may have been searched for + up to this level, the p/2 is a factor of p - 1, something that we do not + know. The modular.math reference below states that 15% of numbers in the + range of 10**15 to 15**15 + 10**4 are 10**6 power smooth so a B of 10**6 + will fail 85% of the time in that range. From 10**8 to 10**8 + 10**3 the + percentages are nearly reversed...but in that range the simple trial + division is quite fast. + + References + ========== + + .. [1] Richard Crandall & Carl Pomerance (2005), "Prime Numbers: + A Computational Perspective", Springer, 2nd edition, 236-238 + .. [2] https://web.archive.org/web/20150716201437/http://modular.math.washington.edu/edu/2007/spring/ent/ent-html/node81.html + .. [3] https://www.cs.toronto.edu/~yuvalf/Factorization.pdf + """ + + n = int(n) + if n < 4 or B < 3: + raise ValueError('pollard_pm1 should receive n > 3 and B > 2') + randint = _randint(seed + B) + + # computing a**lcm(1,2,3,..B) % n for B > 2 + # it looks weird, but it's right: primes run [2, B] + # and the answer's not right until the loop is done. + for i in range(retries + 1): + aM = a + for p in sieve.primerange(2, B + 1): + e = int(math.log(B, p)) + aM = pow(aM, pow(p, e), n) + g = gcd(aM - 1, n) + if 1 < g < n: + return int(g) + + # get a new a: + # since the exponent, lcm(1..B), is even, if we allow 'a' to be 'n-1' + # then (n - 1)**even % n will be 1 which will give a g of 0 and 1 will + # give a zero, too, so we set the range as [2, n-2]. Some references + # say 'a' should be coprime to n, but either will detect factors. + a = randint(2, n - 2) + + +def _trial(factors, n, candidates, verbose=False): + """ + Helper function for integer factorization. Trial factors ``n` + against all integers given in the sequence ``candidates`` + and updates the dict ``factors`` in-place. Returns the reduced + value of ``n`` and a flag indicating whether any factors were found. + """ + if verbose: + factors0 = list(factors.keys()) + nfactors = len(factors) + for d in candidates: + if n % d == 0: + if n != d: + factor_cache[n] = d + n, m = remove(n // d, d) + factors[d] = m + 1 + if verbose: + for k in sorted(set(factors).difference(set(factors0))): + print(factor_msg % (k, factors[k])) + return int(n), len(factors) != nfactors + + +def _check_termination(factors, n, limit, use_trial, use_rho, use_pm1, + verbose, next_p): + """ + Helper function for integer factorization. Checks if ``n`` + is a prime or a perfect power, and in those cases updates the factorization. + """ + if verbose: + print('Check for termination') + if n == 1: + if verbose: + print(complete_msg) + return True + if n < next_p**2 or isprime(n): + factor_cache[n] = n + factors[int(n)] = 1 + if verbose: + print(complete_msg) + return True + + # since we've already been factoring there is no need to do + # simultaneous factoring with the power check + p = _perfect_power(n, next_p) + if not p: + return False + base, exp = p + if base < next_p**2 or isprime(base): + factor_cache[n] = base + factors[base] = exp + else: + facs = factorint(base, limit, use_trial, use_rho, use_pm1, + verbose=False) + for b, e in facs.items(): + if verbose: + print(factor_msg % (b, e)) + factors[b] = exp*e + if verbose: + print(complete_msg) + return True + + +trial_int_msg = "Trial division with ints [%i ... %i] and fail_max=%i" +trial_msg = "Trial division with primes [%i ... %i]" +rho_msg = "Pollard's rho with retries %i, max_steps %i and seed %i" +pm1_msg = "Pollard's p-1 with smoothness bound %i and seed %i" +ecm_msg = "Elliptic Curve with B1 bound %i, B2 bound %i, num_curves %i" +factor_msg = '\t%i ** %i' +fermat_msg = 'Close factors satisfying Fermat condition found.' +complete_msg = 'Factorization is complete.' + + +def _factorint_small(factors, n, limit, fail_max, next_p=2): + """ + Return the value of n and either a 0 (indicating that factorization up + to the limit was complete) or else the next near-prime that would have + been tested. + + Factoring stops if there are fail_max unsuccessful tests in a row. + + If factors of n were found they will be in the factors dictionary as + {factor: multiplicity} and the returned value of n will have had those + factors removed. The factors dictionary is modified in-place. + + """ + + def done(n, d): + """return n, d if the sqrt(n) was not reached yet, else + n, 0 indicating that factoring is done. + """ + if d*d <= n: + return n, d + return n, 0 + + limit2 = limit**2 + threshold2 = min(n, limit2) + + if next_p < 3: + if not n & 1: + m = bit_scan1(n) + factors[2] = m + n >>= m + threshold2 = min(n, limit2) + next_p = 3 + if threshold2 < 9: # next_p**2 = 9 + return done(n, next_p) + + if next_p < 5: + if not n % 3: + n //= 3 + m = 1 + while not n % 3: + n //= 3 + m += 1 + if m == 20: + n, mm = remove(n, 3) + m += mm + break + factors[3] = m + threshold2 = min(n, limit2) + next_p = 5 + if threshold2 < 25: # next_p**2 = 25 + return done(n, next_p) + + # Because of the order of checks, starting from `min_p = 6k+5`, + # useless checks are caused. + # We want to calculate + # next_p += [-1, -2, 3, 2, 1, 0][next_p % 6] + p6 = next_p % 6 + next_p += (-1 if p6 < 2 else 5) - p6 + + fails = 0 + while fails < fail_max: + # next_p % 6 == 5 + if n % next_p: + fails += 1 + else: + n //= next_p + m = 1 + while not n % next_p: + n //= next_p + m += 1 + if m == 20: + n, mm = remove(n, next_p) + m += mm + break + factors[next_p] = m + fails = 0 + threshold2 = min(n, limit2) + next_p += 2 + if threshold2 < next_p**2: + return done(n, next_p) + + # next_p % 6 == 1 + if n % next_p: + fails += 1 + else: + n //= next_p + m = 1 + while not n % next_p: + n //= next_p + m += 1 + if m == 20: + n, mm = remove(n, next_p) + m += mm + break + factors[next_p] = m + fails = 0 + threshold2 = min(n, limit2) + next_p += 4 + if threshold2 < next_p**2: + return done(n, next_p) + return done(n, next_p) + + +def factorint(n, limit=None, use_trial=True, use_rho=True, use_pm1=True, + use_ecm=True, verbose=False, visual=None, multiple=False): + r""" + Given a positive integer ``n``, ``factorint(n)`` returns a dict containing + the prime factors of ``n`` as keys and their respective multiplicities + as values. For example: + + >>> from sympy.ntheory import factorint + >>> factorint(2000) # 2000 = (2**4) * (5**3) + {2: 4, 5: 3} + >>> factorint(65537) # This number is prime + {65537: 1} + + For input less than 2, factorint behaves as follows: + + - ``factorint(1)`` returns the empty factorization, ``{}`` + - ``factorint(0)`` returns ``{0:1}`` + - ``factorint(-n)`` adds ``-1:1`` to the factors and then factors ``n`` + + Partial Factorization: + + If ``limit`` (> 3) is specified, the search is stopped after performing + trial division up to (and including) the limit (or taking a + corresponding number of rho/p-1 steps). This is useful if one has + a large number and only is interested in finding small factors (if + any). Note that setting a limit does not prevent larger factors + from being found early; it simply means that the largest factor may + be composite. Since checking for perfect power is relatively cheap, it is + done regardless of the limit setting. + + This number, for example, has two small factors and a huge + semi-prime factor that cannot be reduced easily: + + >>> from sympy.ntheory import isprime + >>> a = 1407633717262338957430697921446883 + >>> f = factorint(a, limit=10000) + >>> f == {991: 1, int(202916782076162456022877024859): 1, 7: 1} + True + >>> isprime(max(f)) + False + + This number has a small factor and a residual perfect power whose + base is greater than the limit: + + >>> factorint(3*101**7, limit=5) + {3: 1, 101: 7} + + List of Factors: + + If ``multiple`` is set to ``True`` then a list containing the + prime factors including multiplicities is returned. + + >>> factorint(24, multiple=True) + [2, 2, 2, 3] + + Visual Factorization: + + If ``visual`` is set to ``True``, then it will return a visual + factorization of the integer. For example: + + >>> from sympy import pprint + >>> pprint(factorint(4200, visual=True)) + 3 1 2 1 + 2 *3 *5 *7 + + Note that this is achieved by using the evaluate=False flag in Mul + and Pow. If you do other manipulations with an expression where + evaluate=False, it may evaluate. Therefore, you should use the + visual option only for visualization, and use the normal dictionary + returned by visual=False if you want to perform operations on the + factors. + + You can easily switch between the two forms by sending them back to + factorint: + + >>> from sympy import Mul + >>> regular = factorint(1764); regular + {2: 2, 3: 2, 7: 2} + >>> pprint(factorint(regular)) + 2 2 2 + 2 *3 *7 + + >>> visual = factorint(1764, visual=True); pprint(visual) + 2 2 2 + 2 *3 *7 + >>> print(factorint(visual)) + {2: 2, 3: 2, 7: 2} + + If you want to send a number to be factored in a partially factored form + you can do so with a dictionary or unevaluated expression: + + >>> factorint(factorint({4: 2, 12: 3})) # twice to toggle to dict form + {2: 10, 3: 3} + >>> factorint(Mul(4, 12, evaluate=False)) + {2: 4, 3: 1} + + The table of the output logic is: + + ====== ====== ======= ======= + Visual + ------ ---------------------- + Input True False other + ====== ====== ======= ======= + dict mul dict mul + n mul dict dict + mul mul dict dict + ====== ====== ======= ======= + + Notes + ===== + + Algorithm: + + The function switches between multiple algorithms. Trial division + quickly finds small factors (of the order 1-5 digits), and finds + all large factors if given enough time. The Pollard rho and p-1 + algorithms are used to find large factors ahead of time; they + will often find factors of the order of 10 digits within a few + seconds: + + >>> factors = factorint(12345678910111213141516) + >>> for base, exp in sorted(factors.items()): + ... print('%s %s' % (base, exp)) + ... + 2 2 + 2507191691 1 + 1231026625769 1 + + Any of these methods can optionally be disabled with the following + boolean parameters: + + - ``use_trial``: Toggle use of trial division + - ``use_rho``: Toggle use of Pollard's rho method + - ``use_pm1``: Toggle use of Pollard's p-1 method + + ``factorint`` also periodically checks if the remaining part is + a prime number or a perfect power, and in those cases stops. + + For unevaluated factorial, it uses Legendre's formula(theorem). + + + If ``verbose`` is set to ``True``, detailed progress is printed. + + See Also + ======== + + smoothness, smoothness_p, divisors + + """ + if isinstance(n, Dict): + n = dict(n) + if multiple: + fac = factorint(n, limit=limit, use_trial=use_trial, + use_rho=use_rho, use_pm1=use_pm1, + verbose=verbose, visual=False, multiple=False) + factorlist = sum(([p] * fac[p] if fac[p] > 0 else [S.One/p]*(-fac[p]) + for p in sorted(fac)), []) + return factorlist + + factordict = {} + if visual and not isinstance(n, (Mul, dict)): + factordict = factorint(n, limit=limit, use_trial=use_trial, + use_rho=use_rho, use_pm1=use_pm1, + verbose=verbose, visual=False) + elif isinstance(n, Mul): + factordict = {int(k): int(v) for k, v in + n.as_powers_dict().items()} + elif isinstance(n, dict): + factordict = n + if factordict and isinstance(n, (Mul, dict)): + # check it + for key in list(factordict.keys()): + if isprime(key): + continue + e = factordict.pop(key) + d = factorint(key, limit=limit, use_trial=use_trial, use_rho=use_rho, + use_pm1=use_pm1, verbose=verbose, visual=False) + for k, v in d.items(): + if k in factordict: + factordict[k] += v*e + else: + factordict[k] = v*e + if visual or (type(n) is dict and + visual is not True and + visual is not False): + if factordict == {}: + return S.One + if -1 in factordict: + factordict.pop(-1) + args = [S.NegativeOne] + else: + args = [] + args.extend([Pow(*i, evaluate=False) + for i in sorted(factordict.items())]) + return Mul(*args, evaluate=False) + elif isinstance(n, (dict, Mul)): + return factordict + + assert use_trial or use_rho or use_pm1 or use_ecm + + from sympy.functions.combinatorial.factorials import factorial + if isinstance(n, factorial): + x = as_int(n.args[0]) + if x >= 20: + factors = {} + m = 2 # to initialize the if condition below + for p in sieve.primerange(2, x + 1): + if m > 1: + m, q = 0, x // p + while q != 0: + m += q + q //= p + factors[p] = m + if factors and verbose: + for k in sorted(factors): + print(factor_msg % (k, factors[k])) + if verbose: + print(complete_msg) + return factors + else: + # if n < 20!, direct computation is faster + # since it uses a lookup table + n = n.func(x) + + n = as_int(n) + if limit: + limit = int(limit) + use_ecm = False + + # special cases + if n < 0: + factors = factorint( + -n, limit=limit, use_trial=use_trial, use_rho=use_rho, + use_pm1=use_pm1, verbose=verbose, visual=False) + factors[-1] = 1 + return factors + + if limit and limit < 2: + if n == 1: + return {} + return {n: 1} + elif n < 10: + # doing this we are assured of getting a limit > 2 + # when we have to compute it later + return [{0: 1}, {}, {2: 1}, {3: 1}, {2: 2}, {5: 1}, + {2: 1, 3: 1}, {7: 1}, {2: 3}, {3: 2}][n] + + factors = {} + + # do simplistic factorization + if verbose: + sn = str(n) + if len(sn) > 50: + print('Factoring %s' % sn[:5] + \ + '..(%i other digits)..' % (len(sn) - 10) + sn[-5:]) + else: + print('Factoring', n) + + # this is the preliminary factorization for small factors + # We want to guarantee that there are no small prime factors, + # so we run even if `use_trial` is False. + small = 2**15 + fail_max = 600 + small = min(small, limit or small) + if verbose: + print(trial_int_msg % (2, small, fail_max)) + n, next_p = _factorint_small(factors, n, small, fail_max) + if factors and verbose: + for k in sorted(factors): + print(factor_msg % (k, factors[k])) + if next_p == 0: + if n > 1: + factors[int(n)] = 1 + if verbose: + print(complete_msg) + return factors + # Check if it exists in the cache + while p := factor_cache.get(n): + n, e = remove(n, p) + factors[int(p)] = int(e) + # first check if the simplistic run didn't finish + # because of the limit and check for a perfect + # power before exiting + if limit and next_p > limit: + if verbose: + print('Exceeded limit:', limit) + if _check_termination(factors, n, limit, use_trial, + use_rho, use_pm1, verbose, next_p): + return factors + if n > 1: + factors[int(n)] = 1 + return factors + if _check_termination(factors, n, limit, use_trial, + use_rho, use_pm1, verbose, next_p): + return factors + + # continue with more advanced factorization methods + # ...do a Fermat test since it's so easy and we need the + # square root anyway. Finding 2 factors is easy if they are + # "close enough." This is the big root equivalent of dividing by + # 2, 3, 5. + sqrt_n = isqrt(n) + a = sqrt_n + 1 + # If `n % 4 == 1`, `a` must be odd for `a**2 - n` to be a square number. + if (n % 4 == 1) ^ (a & 1): + a += 1 + a2 = a**2 + b2 = a2 - n + for _ in range(3): + b, fermat = sqrtrem(b2) + if not fermat: + if verbose: + print(fermat_msg) + for r in [a - b, a + b]: + facs = factorint(r, limit=limit, use_trial=use_trial, + use_rho=use_rho, use_pm1=use_pm1, + verbose=verbose) + for k, v in facs.items(): + factors[k] = factors.get(k, 0) + v + factor_cache.add(n, facs) + if verbose: + print(complete_msg) + return factors + b2 += (a + 1) << 2 # equiv to (a + 2)**2 - n + a += 2 + + # these are the limits for trial division which will + # be attempted in parallel with pollard methods + low, high = next_p, 2*next_p + + # add 1 to make sure limit is reached in primerange calls + _limit = (limit or sqrt_n) + 1 + iteration = 0 + while 1: + high_ = min(high, _limit) + + # Trial division + if use_trial: + if verbose: + print(trial_msg % (low, high_)) + ps = sieve.primerange(low, high_) + n, found_trial = _trial(factors, n, ps, verbose) + next_p = high_ + if found_trial and _check_termination(factors, n, limit, use_trial, + use_rho, use_pm1, verbose, next_p): + return factors + else: + found_trial = False + + if high > _limit: + if verbose: + print('Exceeded limit:', _limit) + if n > 1: + factors[int(n)] = 1 + if verbose: + print(complete_msg) + return factors + + # Only used advanced methods when no small factors were found + if not found_trial: + # Pollard p-1 + if use_pm1: + if verbose: + print(pm1_msg % (low, high_)) + c = pollard_pm1(n, B=low, seed=high_) + if c: + if c < next_p**2 or isprime(c): + ps = [c] + else: + ps = factorint(c, limit=limit, + use_trial=use_trial, + use_rho=use_rho, + use_pm1=use_pm1, + use_ecm=use_ecm, + verbose=verbose) + n, _ = _trial(factors, n, ps, verbose=False) + if _check_termination(factors, n, limit, use_trial, + use_rho, use_pm1, verbose, next_p): + return factors + + # Pollard rho + if use_rho: + if verbose: + print(rho_msg % (1, low, high_)) + c = pollard_rho(n, retries=1, max_steps=low, seed=high_) + if c: + if c < next_p**2 or isprime(c): + ps = [c] + else: + ps = factorint(c, limit=limit, + use_trial=use_trial, + use_rho=use_rho, + use_pm1=use_pm1, + use_ecm=use_ecm, + verbose=verbose) + n, _ = _trial(factors, n, ps, verbose=False) + if _check_termination(factors, n, limit, use_trial, + use_rho, use_pm1, verbose, next_p): + return factors + # Use subexponential algorithms if use_ecm + # Use pollard algorithms for finding small factors for 3 iterations + # if after small factors the number of digits of n >= 25 then use ecm + iteration += 1 + if use_ecm and iteration >= 3 and num_digits(n) >= 24: + break + low, high = high, high*2 + + B1 = 10000 + B2 = 100*B1 + num_curves = 50 + while(1): + if verbose: + print(ecm_msg % (B1, B2, num_curves)) + factor = _ecm_one_factor(n, B1, B2, num_curves, seed=B1) + if factor: + if factor < next_p**2 or isprime(factor): + ps = [factor] + else: + ps = factorint(factor, limit=limit, + use_trial=use_trial, + use_rho=use_rho, + use_pm1=use_pm1, + use_ecm=use_ecm, + verbose=verbose) + n, _ = _trial(factors, n, ps, verbose=False) + if _check_termination(factors, n, limit, use_trial, + use_rho, use_pm1, verbose, next_p): + return factors + B1 *= 5 + B2 = 100*B1 + num_curves *= 4 + + +def factorrat(rat, limit=None, use_trial=True, use_rho=True, use_pm1=True, + verbose=False, visual=None, multiple=False): + r""" + Given a Rational ``r``, ``factorrat(r)`` returns a dict containing + the prime factors of ``r`` as keys and their respective multiplicities + as values. For example: + + >>> from sympy import factorrat, S + >>> factorrat(S(8)/9) # 8/9 = (2**3) * (3**-2) + {2: 3, 3: -2} + >>> factorrat(S(-1)/987) # -1/789 = -1 * (3**-1) * (7**-1) * (47**-1) + {-1: 1, 3: -1, 7: -1, 47: -1} + + Please see the docstring for ``factorint`` for detailed explanations + and examples of the following keywords: + + - ``limit``: Integer limit up to which trial division is done + - ``use_trial``: Toggle use of trial division + - ``use_rho``: Toggle use of Pollard's rho method + - ``use_pm1``: Toggle use of Pollard's p-1 method + - ``verbose``: Toggle detailed printing of progress + - ``multiple``: Toggle returning a list of factors or dict + - ``visual``: Toggle product form of output + """ + if multiple: + fac = factorrat(rat, limit=limit, use_trial=use_trial, + use_rho=use_rho, use_pm1=use_pm1, + verbose=verbose, visual=False, multiple=False) + factorlist = sum(([p] * fac[p] if fac[p] > 0 else [S.One/p]*(-fac[p]) + for p, _ in sorted(fac.items(), + key=lambda elem: elem[0] + if elem[1] > 0 + else 1/elem[0])), []) + return factorlist + + f = factorint(rat.p, limit=limit, use_trial=use_trial, + use_rho=use_rho, use_pm1=use_pm1, + verbose=verbose).copy() + f = defaultdict(int, f) + for p, e in factorint(rat.q, limit=limit, + use_trial=use_trial, + use_rho=use_rho, + use_pm1=use_pm1, + verbose=verbose).items(): + f[p] += -e + + if len(f) > 1 and 1 in f: + del f[1] + if not visual: + return dict(f) + else: + if -1 in f: + f.pop(-1) + args = [S.NegativeOne] + else: + args = [] + args.extend([Pow(*i, evaluate=False) + for i in sorted(f.items())]) + return Mul(*args, evaluate=False) + + +def primefactors(n, limit=None, verbose=False, **kwargs): + """Return a sorted list of n's prime factors, ignoring multiplicity + and any composite factor that remains if the limit was set too low + for complete factorization. Unlike factorint(), primefactors() does + not return -1 or 0. + + Parameters + ========== + + n : integer + limit, verbose, **kwargs : + Additional keyword arguments to be passed to ``factorint``. + Since ``kwargs`` is new in version 1.13, + ``limit`` and ``verbose`` are retained for compatibility purposes. + + Returns + ======= + + list(int) : List of prime numbers dividing ``n`` + + Examples + ======== + + >>> from sympy.ntheory import primefactors, factorint, isprime + >>> primefactors(6) + [2, 3] + >>> primefactors(-5) + [5] + + >>> sorted(factorint(123456).items()) + [(2, 6), (3, 1), (643, 1)] + >>> primefactors(123456) + [2, 3, 643] + + >>> sorted(factorint(10000000001, limit=200).items()) + [(101, 1), (99009901, 1)] + >>> isprime(99009901) + False + >>> primefactors(10000000001, limit=300) + [101] + + See Also + ======== + + factorint, divisors + + """ + n = int(n) + kwargs.update({"visual": None, "multiple": False, + "limit": limit, "verbose": verbose}) + factors = sorted(factorint(n=n, **kwargs).keys()) + # We want to calculate + # s = [f for f in factors if isprime(f)] + s = [f for f in factors[:-1:] if f not in [-1, 0, 1]] + if factors and isprime(factors[-1]): + s += [factors[-1]] + return s + + +def _divisors(n, proper=False): + """Helper function for divisors which generates the divisors. + + Parameters + ========== + + n : int + a nonnegative integer + proper: bool + If `True`, returns the generator that outputs only the proper divisor (i.e., excluding n). + + """ + if n <= 1: + if not proper and n: + yield 1 + return + + factordict = factorint(n) + ps = sorted(factordict.keys()) + + def rec_gen(n=0): + if n == len(ps): + yield 1 + else: + pows = [1] + for _ in range(factordict[ps[n]]): + pows.append(pows[-1] * ps[n]) + yield from (p * q for q in rec_gen(n + 1) for p in pows) + + if proper: + yield from (p for p in rec_gen() if p != n) + else: + yield from rec_gen() + + +def divisors(n, generator=False, proper=False): + r""" + Return all divisors of n sorted from 1..n by default. + If generator is ``True`` an unordered generator is returned. + + The number of divisors of n can be quite large if there are many + prime factors (counting repeated factors). If only the number of + factors is desired use divisor_count(n). + + Examples + ======== + + >>> from sympy import divisors, divisor_count + >>> divisors(24) + [1, 2, 3, 4, 6, 8, 12, 24] + >>> divisor_count(24) + 8 + + >>> list(divisors(120, generator=True)) + [1, 2, 4, 8, 3, 6, 12, 24, 5, 10, 20, 40, 15, 30, 60, 120] + + Notes + ===== + + This is a slightly modified version of Tim Peters referenced at: + https://stackoverflow.com/questions/1010381/python-factorization + + See Also + ======== + + primefactors, factorint, divisor_count + """ + rv = _divisors(as_int(abs(n)), proper) + return rv if generator else sorted(rv) + + +def divisor_count(n, modulus=1, proper=False): + """ + Return the number of divisors of ``n``. If ``modulus`` is not 1 then only + those that are divisible by ``modulus`` are counted. If ``proper`` is True + then the divisor of ``n`` will not be counted. + + Examples + ======== + + >>> from sympy import divisor_count + >>> divisor_count(6) + 4 + >>> divisor_count(6, 2) + 2 + >>> divisor_count(6, proper=True) + 3 + + See Also + ======== + + factorint, divisors, totient, proper_divisor_count + + """ + + if not modulus: + return 0 + elif modulus != 1: + n, r = divmod(n, modulus) + if r: + return 0 + if n == 0: + return 0 + n = Mul(*[v + 1 for k, v in factorint(n).items() if k > 1]) + if n and proper: + n -= 1 + return n + + +def proper_divisors(n, generator=False): + """ + Return all divisors of n except n, sorted by default. + If generator is ``True`` an unordered generator is returned. + + Examples + ======== + + >>> from sympy import proper_divisors, proper_divisor_count + >>> proper_divisors(24) + [1, 2, 3, 4, 6, 8, 12] + >>> proper_divisor_count(24) + 7 + >>> list(proper_divisors(120, generator=True)) + [1, 2, 4, 8, 3, 6, 12, 24, 5, 10, 20, 40, 15, 30, 60] + + See Also + ======== + + factorint, divisors, proper_divisor_count + + """ + return divisors(n, generator=generator, proper=True) + + +def proper_divisor_count(n, modulus=1): + """ + Return the number of proper divisors of ``n``. + + Examples + ======== + + >>> from sympy import proper_divisor_count + >>> proper_divisor_count(6) + 3 + >>> proper_divisor_count(6, modulus=2) + 1 + + See Also + ======== + + divisors, proper_divisors, divisor_count + + """ + return divisor_count(n, modulus=modulus, proper=True) + + +def _udivisors(n): + """Helper function for udivisors which generates the unitary divisors. + + Parameters + ========== + + n : int + a nonnegative integer + + """ + if n <= 1: + if n == 1: + yield 1 + return + + factorpows = [p**e for p, e in factorint(n).items()] + # We want to calculate + # yield from (math.prod(s) for s in powersets(factorpows)) + for i in range(2**len(factorpows)): + d = 1 + for k in range(i.bit_length()): + if i & 1: + d *= factorpows[k] + i >>= 1 + yield d + + +def udivisors(n, generator=False): + r""" + Return all unitary divisors of n sorted from 1..n by default. + If generator is ``True`` an unordered generator is returned. + + The number of unitary divisors of n can be quite large if there are many + prime factors. If only the number of unitary divisors is desired use + udivisor_count(n). + + Examples + ======== + + >>> from sympy.ntheory.factor_ import udivisors, udivisor_count + >>> udivisors(15) + [1, 3, 5, 15] + >>> udivisor_count(15) + 4 + + >>> sorted(udivisors(120, generator=True)) + [1, 3, 5, 8, 15, 24, 40, 120] + + See Also + ======== + + primefactors, factorint, divisors, divisor_count, udivisor_count + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Unitary_divisor + .. [2] https://mathworld.wolfram.com/UnitaryDivisor.html + + """ + rv = _udivisors(as_int(abs(n))) + return rv if generator else sorted(rv) + + +def udivisor_count(n): + """ + Return the number of unitary divisors of ``n``. + + Parameters + ========== + + n : integer + + Examples + ======== + + >>> from sympy.ntheory.factor_ import udivisor_count + >>> udivisor_count(120) + 8 + + See Also + ======== + + factorint, divisors, udivisors, divisor_count, totient + + References + ========== + + .. [1] https://mathworld.wolfram.com/UnitaryDivisorFunction.html + + """ + + if n == 0: + return 0 + return 2**len([p for p in factorint(n) if p > 1]) + + +def _antidivisors(n): + """Helper function for antidivisors which generates the antidivisors. + + Parameters + ========== + + n : int + a nonnegative integer + + """ + if n <= 2: + return + for d in _divisors(n): + y = 2*d + if n > y and n % y: + yield y + for d in _divisors(2*n-1): + if n > d >= 2 and n % d: + yield d + for d in _divisors(2*n+1): + if n > d >= 2 and n % d: + yield d + + +def antidivisors(n, generator=False): + r""" + Return all antidivisors of n sorted from 1..n by default. + + Antidivisors [1]_ of n are numbers that do not divide n by the largest + possible margin. If generator is True an unordered generator is returned. + + Examples + ======== + + >>> from sympy.ntheory.factor_ import antidivisors + >>> antidivisors(24) + [7, 16] + + >>> sorted(antidivisors(128, generator=True)) + [3, 5, 15, 17, 51, 85] + + See Also + ======== + + primefactors, factorint, divisors, divisor_count, antidivisor_count + + References + ========== + + .. [1] definition is described in https://oeis.org/A066272/a066272a.html + + """ + rv = _antidivisors(as_int(abs(n))) + return rv if generator else sorted(rv) + + +def antidivisor_count(n): + """ + Return the number of antidivisors [1]_ of ``n``. + + Parameters + ========== + + n : integer + + Examples + ======== + + >>> from sympy.ntheory.factor_ import antidivisor_count + >>> antidivisor_count(13) + 4 + >>> antidivisor_count(27) + 5 + + See Also + ======== + + factorint, divisors, antidivisors, divisor_count, totient + + References + ========== + + .. [1] formula from https://oeis.org/A066272 + + """ + + n = as_int(abs(n)) + if n <= 2: + return 0 + return divisor_count(2*n - 1) + divisor_count(2*n + 1) + \ + divisor_count(n) - divisor_count(n, 2) - 5 + +@deprecated("""\ +The `sympy.ntheory.factor_.totient` has been moved to `sympy.functions.combinatorial.numbers.totient`.""", +deprecated_since_version="1.13", +active_deprecations_target='deprecated-ntheory-symbolic-functions') +def totient(n): + r""" + Calculate the Euler totient function phi(n) + + .. deprecated:: 1.13 + + The ``totient`` function is deprecated. Use :class:`sympy.functions.combinatorial.numbers.totient` + instead. See its documentation for more information. See + :ref:`deprecated-ntheory-symbolic-functions` for details. + + ``totient(n)`` or `\phi(n)` is the number of positive integers `\leq` n + that are relatively prime to n. + + Parameters + ========== + + n : integer + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import totient + >>> totient(1) + 1 + >>> totient(25) + 20 + >>> totient(45) == totient(5)*totient(9) + True + + See Also + ======== + + divisor_count + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Euler%27s_totient_function + .. [2] https://mathworld.wolfram.com/TotientFunction.html + + """ + from sympy.functions.combinatorial.numbers import totient as _totient + return _totient(n) + + +@deprecated("""\ +The `sympy.ntheory.factor_.reduced_totient` has been moved to `sympy.functions.combinatorial.numbers.reduced_totient`.""", +deprecated_since_version="1.13", +active_deprecations_target='deprecated-ntheory-symbolic-functions') +def reduced_totient(n): + r""" + Calculate the Carmichael reduced totient function lambda(n) + + .. deprecated:: 1.13 + + The ``reduced_totient`` function is deprecated. Use :class:`sympy.functions.combinatorial.numbers.reduced_totient` + instead. See its documentation for more information. See + :ref:`deprecated-ntheory-symbolic-functions` for details. + + ``reduced_totient(n)`` or `\lambda(n)` is the smallest m > 0 such that + `k^m \equiv 1 \mod n` for all k relatively prime to n. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import reduced_totient + >>> reduced_totient(1) + 1 + >>> reduced_totient(8) + 2 + >>> reduced_totient(30) + 4 + + See Also + ======== + + totient + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Carmichael_function + .. [2] https://mathworld.wolfram.com/CarmichaelFunction.html + + """ + from sympy.functions.combinatorial.numbers import reduced_totient as _reduced_totient + return _reduced_totient(n) + + +@deprecated("""\ +The `sympy.ntheory.factor_.divisor_sigma` has been moved to `sympy.functions.combinatorial.numbers.divisor_sigma`.""", +deprecated_since_version="1.13", +active_deprecations_target='deprecated-ntheory-symbolic-functions') +def divisor_sigma(n, k=1): + r""" + Calculate the divisor function `\sigma_k(n)` for positive integer n + + .. deprecated:: 1.13 + + The ``divisor_sigma`` function is deprecated. Use :class:`sympy.functions.combinatorial.numbers.divisor_sigma` + instead. See its documentation for more information. See + :ref:`deprecated-ntheory-symbolic-functions` for details. + + ``divisor_sigma(n, k)`` is equal to ``sum([x**k for x in divisors(n)])`` + + If n's prime factorization is: + + .. math :: + n = \prod_{i=1}^\omega p_i^{m_i}, + + then + + .. math :: + \sigma_k(n) = \prod_{i=1}^\omega (1+p_i^k+p_i^{2k}+\cdots + + p_i^{m_ik}). + + Parameters + ========== + + n : integer + + k : integer, optional + power of divisors in the sum + + for k = 0, 1: + ``divisor_sigma(n, 0)`` is equal to ``divisor_count(n)`` + ``divisor_sigma(n, 1)`` is equal to ``sum(divisors(n))`` + + Default for k is 1. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import divisor_sigma + >>> divisor_sigma(18, 0) + 6 + >>> divisor_sigma(39, 1) + 56 + >>> divisor_sigma(12, 2) + 210 + >>> divisor_sigma(37) + 38 + + See Also + ======== + + divisor_count, totient, divisors, factorint + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Divisor_function + + """ + from sympy.functions.combinatorial.numbers import divisor_sigma as func_divisor_sigma + return func_divisor_sigma(n, k) + + +def _divisor_sigma(n:int, k:int=1) -> int: + r""" Calculate the divisor function `\sigma_k(n)` for positive integer n + + Parameters + ========== + + n : int + positive integer + k : int + nonnegative integer + + See Also + ======== + + sympy.functions.combinatorial.numbers.divisor_sigma + + """ + if k == 0: + return math.prod(e + 1 for e in factorint(n).values()) + return math.prod((p**(k*(e + 1)) - 1)//(p**k - 1) for p, e in factorint(n).items()) + + +def core(n, t=2): + r""" + Calculate core(n, t) = `core_t(n)` of a positive integer n + + ``core_2(n)`` is equal to the squarefree part of n + + If n's prime factorization is: + + .. math :: + n = \prod_{i=1}^\omega p_i^{m_i}, + + then + + .. math :: + core_t(n) = \prod_{i=1}^\omega p_i^{m_i \mod t}. + + Parameters + ========== + + n : integer + + t : integer + core(n, t) calculates the t-th power free part of n + + ``core(n, 2)`` is the squarefree part of ``n`` + ``core(n, 3)`` is the cubefree part of ``n`` + + Default for t is 2. + + Examples + ======== + + >>> from sympy.ntheory.factor_ import core + >>> core(24, 2) + 6 + >>> core(9424, 3) + 1178 + >>> core(379238) + 379238 + >>> core(15**11, 10) + 15 + + See Also + ======== + + factorint, sympy.solvers.diophantine.diophantine.square_factor + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Square-free_integer#Squarefree_core + + """ + + n = as_int(n) + t = as_int(t) + if n <= 0: + raise ValueError("n must be a positive integer") + elif t <= 1: + raise ValueError("t must be >= 2") + else: + y = 1 + for p, e in factorint(n).items(): + y *= p**(e % t) + return y + + +@deprecated("""\ +The `sympy.ntheory.factor_.udivisor_sigma` has been moved to `sympy.functions.combinatorial.numbers.udivisor_sigma`.""", +deprecated_since_version="1.13", +active_deprecations_target='deprecated-ntheory-symbolic-functions') +def udivisor_sigma(n, k=1): + r""" + Calculate the unitary divisor function `\sigma_k^*(n)` for positive integer n + + .. deprecated:: 1.13 + + The ``udivisor_sigma`` function is deprecated. Use :class:`sympy.functions.combinatorial.numbers.udivisor_sigma` + instead. See its documentation for more information. See + :ref:`deprecated-ntheory-symbolic-functions` for details. + + ``udivisor_sigma(n, k)`` is equal to ``sum([x**k for x in udivisors(n)])`` + + If n's prime factorization is: + + .. math :: + n = \prod_{i=1}^\omega p_i^{m_i}, + + then + + .. math :: + \sigma_k^*(n) = \prod_{i=1}^\omega (1+ p_i^{m_ik}). + + Parameters + ========== + + k : power of divisors in the sum + + for k = 0, 1: + ``udivisor_sigma(n, 0)`` is equal to ``udivisor_count(n)`` + ``udivisor_sigma(n, 1)`` is equal to ``sum(udivisors(n))`` + + Default for k is 1. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import udivisor_sigma + >>> udivisor_sigma(18, 0) + 4 + >>> udivisor_sigma(74, 1) + 114 + >>> udivisor_sigma(36, 3) + 47450 + >>> udivisor_sigma(111) + 152 + + See Also + ======== + + divisor_count, totient, divisors, udivisors, udivisor_count, divisor_sigma, + factorint + + References + ========== + + .. [1] https://mathworld.wolfram.com/UnitaryDivisorFunction.html + + """ + from sympy.functions.combinatorial.numbers import udivisor_sigma as _udivisor_sigma + return _udivisor_sigma(n, k) + + +@deprecated("""\ +The `sympy.ntheory.factor_.primenu` has been moved to `sympy.functions.combinatorial.numbers.primenu`.""", +deprecated_since_version="1.13", +active_deprecations_target='deprecated-ntheory-symbolic-functions') +def primenu(n): + r""" + Calculate the number of distinct prime factors for a positive integer n. + + .. deprecated:: 1.13 + + The ``primenu`` function is deprecated. Use :class:`sympy.functions.combinatorial.numbers.primenu` + instead. See its documentation for more information. See + :ref:`deprecated-ntheory-symbolic-functions` for details. + + If n's prime factorization is: + + .. math :: + n = \prod_{i=1}^k p_i^{m_i}, + + then ``primenu(n)`` or `\nu(n)` is: + + .. math :: + \nu(n) = k. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import primenu + >>> primenu(1) + 0 + >>> primenu(30) + 3 + + See Also + ======== + + factorint + + References + ========== + + .. [1] https://mathworld.wolfram.com/PrimeFactor.html + + """ + from sympy.functions.combinatorial.numbers import primenu as _primenu + return _primenu(n) + + +@deprecated("""\ +The `sympy.ntheory.factor_.primeomega` has been moved to `sympy.functions.combinatorial.numbers.primeomega`.""", +deprecated_since_version="1.13", +active_deprecations_target='deprecated-ntheory-symbolic-functions') +def primeomega(n): + r""" + Calculate the number of prime factors counting multiplicities for a + positive integer n. + + .. deprecated:: 1.13 + + The ``primeomega`` function is deprecated. Use :class:`sympy.functions.combinatorial.numbers.primeomega` + instead. See its documentation for more information. See + :ref:`deprecated-ntheory-symbolic-functions` for details. + + If n's prime factorization is: + + .. math :: + n = \prod_{i=1}^k p_i^{m_i}, + + then ``primeomega(n)`` or `\Omega(n)` is: + + .. math :: + \Omega(n) = \sum_{i=1}^k m_i. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import primeomega + >>> primeomega(1) + 0 + >>> primeomega(20) + 3 + + See Also + ======== + + factorint + + References + ========== + + .. [1] https://mathworld.wolfram.com/PrimeFactor.html + + """ + from sympy.functions.combinatorial.numbers import primeomega as _primeomega + return _primeomega(n) + + +def mersenne_prime_exponent(nth): + """Returns the exponent ``i`` for the nth Mersenne prime (which + has the form `2^i - 1`). + + Examples + ======== + + >>> from sympy.ntheory.factor_ import mersenne_prime_exponent + >>> mersenne_prime_exponent(1) + 2 + >>> mersenne_prime_exponent(20) + 4423 + """ + n = as_int(nth) + if n < 1: + raise ValueError("nth must be a positive integer; mersenne_prime_exponent(1) == 2") + if n > 51: + raise ValueError("There are only 51 perfect numbers; nth must be less than or equal to 51") + return MERSENNE_PRIME_EXPONENTS[n - 1] + + +def is_perfect(n): + """Returns True if ``n`` is a perfect number, else False. + + A perfect number is equal to the sum of its positive, proper divisors. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import divisor_sigma + >>> from sympy.ntheory.factor_ import is_perfect, divisors + >>> is_perfect(20) + False + >>> is_perfect(6) + True + >>> 6 == divisor_sigma(6) - 6 == sum(divisors(6)[:-1]) + True + + References + ========== + + .. [1] https://mathworld.wolfram.com/PerfectNumber.html + .. [2] https://en.wikipedia.org/wiki/Perfect_number + + """ + n = as_int(n) + if n < 1: + return False + if n % 2 == 0: + m = (n.bit_length() + 1) >> 1 + if (1 << (m - 1)) * ((1 << m) - 1) != n: + # Even perfect numbers must be of the form `2^{m-1}(2^m-1)` + return False + return m in MERSENNE_PRIME_EXPONENTS or is_mersenne_prime(2**m - 1) + + # n is an odd integer + if n < 10**2000: # https://www.lirmm.fr/~ochem/opn/ + return False + if n % 105 == 0: # not divis by 105 + return False + if all(n % m != r for m, r in [(12, 1), (468, 117), (324, 81)]): + return False + # there are many criteria that the factor structure of n + # must meet; since we will have to factor it to test the + # structure we will have the factors and can then check + # to see whether it is a perfect number or not. So we + # skip the structure checks and go straight to the final + # test below. + result = abundance(n) == 0 + if result: + raise ValueError(filldedent('''In 1888, Sylvester stated: " + ...a prolonged meditation on the subject has satisfied + me that the existence of any one such [odd perfect number] + -- its escape, so to say, from the complex web of conditions + which hem it in on all sides -- would be little short of a + miracle." I guess SymPy just found that miracle and it + factors like this: %s''' % factorint(n))) + return result + + +def abundance(n): + """Returns the difference between the sum of the positive + proper divisors of a number and the number. + + Examples + ======== + + >>> from sympy.ntheory import abundance, is_perfect, is_abundant + >>> abundance(6) + 0 + >>> is_perfect(6) + True + >>> abundance(10) + -2 + >>> is_abundant(10) + False + """ + return _divisor_sigma(n) - 2 * n + + +def is_abundant(n): + """Returns True if ``n`` is an abundant number, else False. + + A abundant number is smaller than the sum of its positive proper divisors. + + Examples + ======== + + >>> from sympy.ntheory.factor_ import is_abundant + >>> is_abundant(20) + True + >>> is_abundant(15) + False + + References + ========== + + .. [1] https://mathworld.wolfram.com/AbundantNumber.html + + """ + n = as_int(n) + if is_perfect(n): + return False + return n % 6 == 0 or bool(abundance(n) > 0) + + +def is_deficient(n): + """Returns True if ``n`` is a deficient number, else False. + + A deficient number is greater than the sum of its positive proper divisors. + + Examples + ======== + + >>> from sympy.ntheory.factor_ import is_deficient + >>> is_deficient(20) + False + >>> is_deficient(15) + True + + References + ========== + + .. [1] https://mathworld.wolfram.com/DeficientNumber.html + + """ + n = as_int(n) + if is_perfect(n): + return False + return bool(abundance(n) < 0) + + +def is_amicable(m, n): + """Returns True if the numbers `m` and `n` are "amicable", else False. + + Amicable numbers are two different numbers so related that the sum + of the proper divisors of each is equal to that of the other. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import divisor_sigma + >>> from sympy.ntheory.factor_ import is_amicable + >>> is_amicable(220, 284) + True + >>> divisor_sigma(220) == divisor_sigma(284) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Amicable_numbers + + """ + return m != n and m + n == _divisor_sigma(m) == _divisor_sigma(n) + + +def is_carmichael(n): + """ Returns True if the numbers `n` is Carmichael number, else False. + + Parameters + ========== + + n : Integer + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Carmichael_number + .. [2] https://oeis.org/A002997 + + """ + if n < 561: + return False + return n % 2 and not isprime(n) and \ + all(e == 1 and (n - 1) % (p - 1) == 0 for p, e in factorint(n).items()) + + +def find_carmichael_numbers_in_range(x, y): + """ Returns a list of the number of Carmichael in the range + + See Also + ======== + + is_carmichael + + """ + if 0 <= x <= y: + if x % 2 == 0: + return [i for i in range(x + 1, y, 2) if is_carmichael(i)] + else: + return [i for i in range(x, y, 2) if is_carmichael(i)] + else: + raise ValueError('The provided range is not valid. x and y must be non-negative integers and x <= y') + + +def find_first_n_carmichaels(n): + """ Returns the first n Carmichael numbers. + + Parameters + ========== + + n : Integer + + See Also + ======== + + is_carmichael + + """ + i = 561 + carmichaels = [] + + while len(carmichaels) < n: + if is_carmichael(i): + carmichaels.append(i) + i += 2 + + return carmichaels + + +def dra(n, b): + """ + Returns the additive digital root of a natural number ``n`` in base ``b`` + which is a single digit value obtained by an iterative process of summing + digits, on each iteration using the result from the previous iteration to + compute a digit sum. + + Examples + ======== + + >>> from sympy.ntheory.factor_ import dra + >>> dra(3110, 12) + 8 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Digital_root + + """ + + num = abs(as_int(n)) + b = as_int(b) + if b <= 1: + raise ValueError("Base should be an integer greater than 1") + + if num == 0: + return 0 + + return (1 + (num - 1) % (b - 1)) + + +def drm(n, b): + """ + Returns the multiplicative digital root of a natural number ``n`` in a given + base ``b`` which is a single digit value obtained by an iterative process of + multiplying digits, on each iteration using the result from the previous + iteration to compute the digit multiplication. + + Examples + ======== + + >>> from sympy.ntheory.factor_ import drm + >>> drm(9876, 10) + 0 + + >>> drm(49, 10) + 8 + + References + ========== + + .. [1] https://mathworld.wolfram.com/MultiplicativeDigitalRoot.html + + """ + + n = abs(as_int(n)) + b = as_int(b) + if b <= 1: + raise ValueError("Base should be an integer greater than 1") + while n > b: + mul = 1 + while n > 1: + n, r = divmod(n, b) + if r == 0: + return 0 + mul *= r + n = mul + return n diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/generate.py b/venv/lib/python3.11/site-packages/sympy/ntheory/generate.py new file mode 100644 index 0000000000000000000000000000000000000000..855bb44acfcb6241e6b0bcb81e7a2cfc8ced861f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/generate.py @@ -0,0 +1,1157 @@ +""" +Generating and counting primes. + +""" + +from bisect import bisect, bisect_left +from itertools import count +# Using arrays for sieving instead of lists greatly reduces +# memory consumption +from array import array as _array + +from sympy.core.random import randint +from sympy.external.gmpy import sqrt +from .primetest import isprime +from sympy.utilities.decorator import deprecated +from sympy.utilities.misc import as_int + + +def _as_int_ceiling(a): + """ Wrapping ceiling in as_int will raise an error if there was a problem + determining whether the expression was exactly an integer or not.""" + from sympy.functions.elementary.integers import ceiling + return as_int(ceiling(a)) + + +class Sieve: + """A list of prime numbers, implemented as a dynamically + growing sieve of Eratosthenes. When a lookup is requested involving + an odd number that has not been sieved, the sieve is automatically + extended up to that number. Implementation details limit the number of + primes to ``2^32-1``. + + Examples + ======== + + >>> from sympy import sieve + >>> sieve._reset() # this line for doctest only + >>> 25 in sieve + False + >>> sieve._list + array('L', [2, 3, 5, 7, 11, 13, 17, 19, 23]) + """ + + # data shared (and updated) by all Sieve instances + def __init__(self, sieve_interval=1_000_000): + """ Initial parameters for the Sieve class. + + Parameters + ========== + + sieve_interval (int): Amount of memory to be used + + Raises + ====== + + ValueError + If ``sieve_interval`` is not positive. + + """ + self._n = 6 + self._list = _array('L', [2, 3, 5, 7, 11, 13]) # primes + self._tlist = _array('L', [0, 1, 1, 2, 2, 4]) # totient + self._mlist = _array('i', [0, 1, -1, -1, 0, -1]) # mobius + if sieve_interval <= 0: + raise ValueError("sieve_interval should be a positive integer") + self.sieve_interval = sieve_interval + assert all(len(i) == self._n for i in (self._list, self._tlist, self._mlist)) + + def __repr__(self): + return ("<%s sieve (%i): %i, %i, %i, ... %i, %i\n" + "%s sieve (%i): %i, %i, %i, ... %i, %i\n" + "%s sieve (%i): %i, %i, %i, ... %i, %i>") % ( + 'prime', len(self._list), + self._list[0], self._list[1], self._list[2], + self._list[-2], self._list[-1], + 'totient', len(self._tlist), + self._tlist[0], self._tlist[1], + self._tlist[2], self._tlist[-2], self._tlist[-1], + 'mobius', len(self._mlist), + self._mlist[0], self._mlist[1], + self._mlist[2], self._mlist[-2], self._mlist[-1]) + + def _reset(self, prime=None, totient=None, mobius=None): + """Reset all caches (default). To reset one or more set the + desired keyword to True.""" + if all(i is None for i in (prime, totient, mobius)): + prime = totient = mobius = True + if prime: + self._list = self._list[:self._n] + if totient: + self._tlist = self._tlist[:self._n] + if mobius: + self._mlist = self._mlist[:self._n] + + def extend(self, n): + """Grow the sieve to cover all primes <= n. + + Examples + ======== + + >>> from sympy import sieve + >>> sieve._reset() # this line for doctest only + >>> sieve.extend(30) + >>> sieve[10] == 29 + True + """ + n = int(n) + # `num` is even at any point in the function. + # This satisfies the condition required by `self._primerange`. + num = self._list[-1] + 1 + if n < num: + return + num2 = num**2 + while num2 <= n: + self._list += _array('L', self._primerange(num, num2)) + num, num2 = num2, num2**2 + # Merge the sieves + self._list += _array('L', self._primerange(num, n + 1)) + + def _primerange(self, a, b): + """ Generate all prime numbers in the range (a, b). + + Parameters + ========== + + a, b : positive integers assuming the following conditions + * a is an even number + * 2 < self._list[-1] < a < b < nextprime(self._list[-1])**2 + + Yields + ====== + + p (int): prime numbers such that ``a < p < b`` + + Examples + ======== + + >>> from sympy.ntheory.generate import Sieve + >>> s = Sieve() + >>> s._list[-1] + 13 + >>> list(s._primerange(18, 31)) + [19, 23, 29] + + """ + if b % 2: + b -= 1 + while a < b: + block_size = min(self.sieve_interval, (b - a) // 2) + # Create the list such that block[x] iff (a + 2x + 1) is prime. + # Note that even numbers are not considered here. + block = [True] * block_size + for p in self._list[1:bisect(self._list, sqrt(a + 2 * block_size + 1))]: + for t in range((-(a + 1 + p) // 2) % p, block_size, p): + block[t] = False + for idx, p in enumerate(block): + if p: + yield a + 2 * idx + 1 + a += 2 * block_size + + def extend_to_no(self, i): + """Extend to include the ith prime number. + + Parameters + ========== + + i : integer + + Examples + ======== + + >>> from sympy import sieve + >>> sieve._reset() # this line for doctest only + >>> sieve.extend_to_no(9) + >>> sieve._list + array('L', [2, 3, 5, 7, 11, 13, 17, 19, 23]) + + Notes + ===== + + The list is extended by 50% if it is too short, so it is + likely that it will be longer than requested. + """ + i = as_int(i) + while len(self._list) < i: + self.extend(int(self._list[-1] * 1.5)) + + def primerange(self, a, b=None): + """Generate all prime numbers in the range [2, a) or [a, b). + + Examples + ======== + + >>> from sympy import sieve, prime + + All primes less than 19: + + >>> print([i for i in sieve.primerange(19)]) + [2, 3, 5, 7, 11, 13, 17] + + All primes greater than or equal to 7 and less than 19: + + >>> print([i for i in sieve.primerange(7, 19)]) + [7, 11, 13, 17] + + All primes through the 10th prime + + >>> list(sieve.primerange(prime(10) + 1)) + [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] + + """ + if b is None: + b = _as_int_ceiling(a) + a = 2 + else: + a = max(2, _as_int_ceiling(a)) + b = _as_int_ceiling(b) + if a >= b: + return + self.extend(b) + yield from self._list[bisect_left(self._list, a): + bisect_left(self._list, b)] + + def totientrange(self, a, b): + """Generate all totient numbers for the range [a, b). + + Examples + ======== + + >>> from sympy import sieve + >>> print([i for i in sieve.totientrange(7, 18)]) + [6, 4, 6, 4, 10, 4, 12, 6, 8, 8, 16] + """ + a = max(1, _as_int_ceiling(a)) + b = _as_int_ceiling(b) + n = len(self._tlist) + if a >= b: + return + elif b <= n: + for i in range(a, b): + yield self._tlist[i] + else: + self._tlist += _array('L', range(n, b)) + for i in range(1, n): + ti = self._tlist[i] + if ti == i - 1: + startindex = (n + i - 1) // i * i + for j in range(startindex, b, i): + self._tlist[j] -= self._tlist[j] // i + if i >= a: + yield ti + + for i in range(n, b): + ti = self._tlist[i] + if ti == i: + for j in range(i, b, i): + self._tlist[j] -= self._tlist[j] // i + if i >= a: + yield self._tlist[i] + + def mobiusrange(self, a, b): + """Generate all mobius numbers for the range [a, b). + + Parameters + ========== + + a : integer + First number in range + + b : integer + First number outside of range + + Examples + ======== + + >>> from sympy import sieve + >>> print([i for i in sieve.mobiusrange(7, 18)]) + [-1, 0, 0, 1, -1, 0, -1, 1, 1, 0, -1] + """ + a = max(1, _as_int_ceiling(a)) + b = _as_int_ceiling(b) + n = len(self._mlist) + if a >= b: + return + elif b <= n: + for i in range(a, b): + yield self._mlist[i] + else: + self._mlist += _array('i', [0]*(b - n)) + for i in range(1, n): + mi = self._mlist[i] + startindex = (n + i - 1) // i * i + for j in range(startindex, b, i): + self._mlist[j] -= mi + if i >= a: + yield mi + + for i in range(n, b): + mi = self._mlist[i] + for j in range(2 * i, b, i): + self._mlist[j] -= mi + if i >= a: + yield mi + + def search(self, n): + """Return the indices i, j of the primes that bound n. + + If n is prime then i == j. + + Although n can be an expression, if ceiling cannot convert + it to an integer then an n error will be raised. + + Examples + ======== + + >>> from sympy import sieve + >>> sieve.search(25) + (9, 10) + >>> sieve.search(23) + (9, 9) + """ + test = _as_int_ceiling(n) + n = as_int(n) + if n < 2: + raise ValueError("n should be >= 2 but got: %s" % n) + if n > self._list[-1]: + self.extend(n) + b = bisect(self._list, n) + if self._list[b - 1] == test: + return b, b + else: + return b, b + 1 + + def __contains__(self, n): + try: + n = as_int(n) + assert n >= 2 + except (ValueError, AssertionError): + return False + if n % 2 == 0: + return n == 2 + a, b = self.search(n) + return a == b + + def __iter__(self): + for n in count(1): + yield self[n] + + def __getitem__(self, n): + """Return the nth prime number""" + if isinstance(n, slice): + self.extend_to_no(n.stop) + start = n.start if n.start is not None else 0 + if start < 1: + # sieve[:5] would be empty (starting at -1), let's + # just be explicit and raise. + raise IndexError("Sieve indices start at 1.") + return self._list[start - 1:n.stop - 1:n.step] + else: + if n < 1: + # offset is one, so forbid explicit access to sieve[0] + # (would surprisingly return the last one). + raise IndexError("Sieve indices start at 1.") + n = as_int(n) + self.extend_to_no(n) + return self._list[n - 1] + +# Generate a global object for repeated use in trial division etc +sieve = Sieve() + +def prime(nth): + r""" + Return the nth prime number, where primes are indexed starting from 1: + prime(1) = 2, prime(2) = 3, etc. + + Parameters + ========== + + nth : int + The position of the prime number to return (must be a positive integer). + + Returns + ======= + + int + The nth prime number. + + Examples + ======== + + >>> from sympy import prime + >>> prime(10) + 29 + >>> prime(1) + 2 + >>> prime(100000) + 1299709 + + See Also + ======== + + sympy.ntheory.primetest.isprime : Test if a number is prime. + primerange : Generate all primes in a given range. + primepi : Return the number of primes less than or equal to a given number. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Prime_number_theorem + .. [2] https://en.wikipedia.org/wiki/Logarithmic_integral_function + .. [3] https://en.wikipedia.org/wiki/Skewes%27_number + """ + n = as_int(nth) + if n < 1: + raise ValueError("nth must be a positive integer; prime(1) == 2") + + # Check if n is within the sieve range + if n <= len(sieve._list): + return sieve[n] + + from sympy.functions.elementary.exponential import log + from sympy.functions.special.error_functions import li + + if n < 1000: + # Extend sieve up to 8*n as this is empirically sufficient + sieve.extend(8 * n) + return sieve[n] + + a = 2 + # Estimate an upper bound for the nth prime using the prime number theorem + b = int(n * (log(n).evalf() + log(log(n)).evalf())) + + # Binary search for the least m such that li(m) > n + while a < b: + mid = (a + b) >> 1 + if li(mid).evalf() > n: + b = mid + else: + a = mid + 1 + + return nextprime(a - 1, n - _primepi(a - 1)) + + +@deprecated("""\ +The `sympy.ntheory.generate.primepi` has been moved to `sympy.functions.combinatorial.numbers.primepi`.""", +deprecated_since_version="1.13", +active_deprecations_target='deprecated-ntheory-symbolic-functions') +def primepi(n): + r""" Represents the prime counting function pi(n) = the number + of prime numbers less than or equal to n. + + .. deprecated:: 1.13 + + The ``primepi`` function is deprecated. Use :class:`sympy.functions.combinatorial.numbers.primepi` + instead. See its documentation for more information. See + :ref:`deprecated-ntheory-symbolic-functions` for details. + + Algorithm Description: + + In sieve method, we remove all multiples of prime p + except p itself. + + Let phi(i,j) be the number of integers 2 <= k <= i + which remain after sieving from primes less than + or equal to j. + Clearly, pi(n) = phi(n, sqrt(n)) + + If j is not a prime, + phi(i,j) = phi(i, j - 1) + + if j is a prime, + We remove all numbers(except j) whose + smallest prime factor is j. + + Let $x= j \times a$ be such a number, where $2 \le a \le i / j$ + Now, after sieving from primes $\le j - 1$, + a must remain + (because x, and hence a has no prime factor $\le j - 1$) + Clearly, there are phi(i / j, j - 1) such a + which remain on sieving from primes $\le j - 1$ + + Now, if a is a prime less than equal to j - 1, + $x= j \times a$ has smallest prime factor = a, and + has already been removed(by sieving from a). + So, we do not need to remove it again. + (Note: there will be pi(j - 1) such x) + + Thus, number of x, that will be removed are: + phi(i / j, j - 1) - phi(j - 1, j - 1) + (Note that pi(j - 1) = phi(j - 1, j - 1)) + + $\Rightarrow$ phi(i,j) = phi(i, j - 1) - phi(i / j, j - 1) + phi(j - 1, j - 1) + + So,following recursion is used and implemented as dp: + + phi(a, b) = phi(a, b - 1), if b is not a prime + phi(a, b) = phi(a, b-1)-phi(a / b, b-1) + phi(b-1, b-1), if b is prime + + Clearly a is always of the form floor(n / k), + which can take at most $2\sqrt{n}$ values. + Two arrays arr1,arr2 are maintained + arr1[i] = phi(i, j), + arr2[i] = phi(n // i, j) + + Finally the answer is arr2[1] + + Examples + ======== + + >>> from sympy import primepi, prime, prevprime, isprime + >>> primepi(25) + 9 + + So there are 9 primes less than or equal to 25. Is 25 prime? + + >>> isprime(25) + False + + It is not. So the first prime less than 25 must be the + 9th prime: + + >>> prevprime(25) == prime(9) + True + + See Also + ======== + + sympy.ntheory.primetest.isprime : Test if n is prime + primerange : Generate all primes in a given range + prime : Return the nth prime + """ + from sympy.functions.combinatorial.numbers import primepi as func_primepi + return func_primepi(n) + + +def _primepi(n:int) -> int: + r""" Represents the prime counting function pi(n) = the number + of prime numbers less than or equal to n. + + Explanation + =========== + + In sieve method, we remove all multiples of prime p + except p itself. + + Let phi(i,j) be the number of integers 2 <= k <= i + which remain after sieving from primes less than + or equal to j. + Clearly, pi(n) = phi(n, sqrt(n)) + + If j is not a prime, + phi(i,j) = phi(i, j - 1) + + if j is a prime, + We remove all numbers(except j) whose + smallest prime factor is j. + + Let $x= j \times a$ be such a number, where $2 \le a \le i / j$ + Now, after sieving from primes $\le j - 1$, + a must remain + (because x, and hence a has no prime factor $\le j - 1$) + Clearly, there are phi(i / j, j - 1) such a + which remain on sieving from primes $\le j - 1$ + + Now, if a is a prime less than equal to j - 1, + $x= j \times a$ has smallest prime factor = a, and + has already been removed(by sieving from a). + So, we do not need to remove it again. + (Note: there will be pi(j - 1) such x) + + Thus, number of x, that will be removed are: + phi(i / j, j - 1) - phi(j - 1, j - 1) + (Note that pi(j - 1) = phi(j - 1, j - 1)) + + $\Rightarrow$ phi(i,j) = phi(i, j - 1) - phi(i / j, j - 1) + phi(j - 1, j - 1) + + So,following recursion is used and implemented as dp: + + phi(a, b) = phi(a, b - 1), if b is not a prime + phi(a, b) = phi(a, b-1)-phi(a / b, b-1) + phi(b-1, b-1), if b is prime + + Clearly a is always of the form floor(n / k), + which can take at most $2\sqrt{n}$ values. + Two arrays arr1,arr2 are maintained + arr1[i] = phi(i, j), + arr2[i] = phi(n // i, j) + + Finally the answer is arr2[1] + + Parameters + ========== + + n : int + + """ + if n < 2: + return 0 + if n <= sieve._list[-1]: + return sieve.search(n)[0] + lim = sqrt(n) + arr1 = [(i + 1) >> 1 for i in range(lim + 1)] + arr2 = [0] + [(n//i + 1) >> 1 for i in range(1, lim + 1)] + skip = [False] * (lim + 1) + for i in range(3, lim + 1, 2): + # Presently, arr1[k]=phi(k,i - 1), + # arr2[k] = phi(n // k,i - 1) # not all k's do this + if skip[i]: + # skip if i is a composite number + continue + p = arr1[i - 1] + for j in range(i, lim + 1, i): + skip[j] = True + # update arr2 + # phi(n/j, i) = phi(n/j, i-1) - phi(n/(i*j), i-1) + phi(i-1, i-1) + for j in range(1, min(n // (i * i), lim) + 1, 2): + # No need for arr2[j] in j such that skip[j] is True to + # compute the final required arr2[1]. + if skip[j]: + continue + st = i * j + if st <= lim: + arr2[j] -= arr2[st] - p + else: + arr2[j] -= arr1[n // st] - p + # update arr1 + # phi(j, i) = phi(j, i-1) - phi(j/i, i-1) + phi(i-1, i-1) + # where the range below i**2 is fixed and + # does not need to be calculated. + for j in range(lim, min(lim, i*i - 1), -1): + arr1[j] -= arr1[j // i] - p + return arr2[1] + + +def nextprime(n, ith=1): + """ Return the ith prime greater than n. + + Parameters + ========== + + n : integer + ith : positive integer + + Returns + ======= + + int : Return the ith prime greater than n + + Raises + ====== + + ValueError + If ``ith <= 0``. + If ``n`` or ``ith`` is not an integer. + + Notes + ===== + + Potential primes are located at 6*j +/- 1. This + property is used during searching. + + >>> from sympy import nextprime + >>> [(i, nextprime(i)) for i in range(10, 15)] + [(10, 11), (11, 13), (12, 13), (13, 17), (14, 17)] + >>> nextprime(2, ith=2) # the 2nd prime after 2 + 5 + + See Also + ======== + + prevprime : Return the largest prime smaller than n + primerange : Generate all primes in a given range + + """ + n = int(n) + i = as_int(ith) + if i <= 0: + raise ValueError("ith should be positive") + if n < 2: + n = 2 + i -= 1 + if n <= sieve._list[-2]: + l, _ = sieve.search(n) + if l + i - 1 < len(sieve._list): + return sieve._list[l + i - 1] + n = sieve._list[-1] + i += l - len(sieve._list) + nn = 6*(n//6) + if nn == n: + n += 1 + if isprime(n): + i -= 1 + if not i: + return n + n += 4 + elif n - nn == 5: + n += 2 + if isprime(n): + i -= 1 + if not i: + return n + n += 4 + else: + n = nn + 5 + while 1: + if isprime(n): + i -= 1 + if not i: + return n + n += 2 + if isprime(n): + i -= 1 + if not i: + return n + n += 4 + + +def prevprime(n): + """ Return the largest prime smaller than n. + + Notes + ===== + + Potential primes are located at 6*j +/- 1. This + property is used during searching. + + >>> from sympy import prevprime + >>> [(i, prevprime(i)) for i in range(10, 15)] + [(10, 7), (11, 7), (12, 11), (13, 11), (14, 13)] + + See Also + ======== + + nextprime : Return the ith prime greater than n + primerange : Generates all primes in a given range + """ + n = _as_int_ceiling(n) + if n < 3: + raise ValueError("no preceding primes") + if n < 8: + return {3: 2, 4: 3, 5: 3, 6: 5, 7: 5}[n] + if n <= sieve._list[-1]: + l, u = sieve.search(n) + if l == u: + return sieve[l-1] + else: + return sieve[l] + nn = 6*(n//6) + if n - nn <= 1: + n = nn - 1 + if isprime(n): + return n + n -= 4 + else: + n = nn + 1 + while 1: + if isprime(n): + return n + n -= 2 + if isprime(n): + return n + n -= 4 + + +def primerange(a, b=None): + """ Generate a list of all prime numbers in the range [2, a), + or [a, b). + + If the range exists in the default sieve, the values will + be returned from there; otherwise values will be returned + but will not modify the sieve. + + Examples + ======== + + >>> from sympy import primerange, prime + + All primes less than 19: + + >>> list(primerange(19)) + [2, 3, 5, 7, 11, 13, 17] + + All primes greater than or equal to 7 and less than 19: + + >>> list(primerange(7, 19)) + [7, 11, 13, 17] + + All primes through the 10th prime + + >>> list(primerange(prime(10) + 1)) + [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] + + The Sieve method, primerange, is generally faster but it will + occupy more memory as the sieve stores values. The default + instance of Sieve, named sieve, can be used: + + >>> from sympy import sieve + >>> list(sieve.primerange(1, 30)) + [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] + + Notes + ===== + + Some famous conjectures about the occurrence of primes in a given + range are [1]: + + - Twin primes: though often not, the following will give 2 primes + an infinite number of times: + primerange(6*n - 1, 6*n + 2) + - Legendre's: the following always yields at least one prime + primerange(n**2, (n+1)**2+1) + - Bertrand's (proven): there is always a prime in the range + primerange(n, 2*n) + - Brocard's: there are at least four primes in the range + primerange(prime(n)**2, prime(n+1)**2) + + The average gap between primes is log(n) [2]; the gap between + primes can be arbitrarily large since sequences of composite + numbers are arbitrarily large, e.g. the numbers in the sequence + n! + 2, n! + 3 ... n! + n are all composite. + + See Also + ======== + + prime : Return the nth prime + nextprime : Return the ith prime greater than n + prevprime : Return the largest prime smaller than n + randprime : Returns a random prime in a given range + primorial : Returns the product of primes based on condition + Sieve.primerange : return range from already computed primes + or extend the sieve to contain the requested + range. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Prime_number + .. [2] https://primes.utm.edu/notes/gaps.html + """ + if b is None: + a, b = 2, a + if a >= b: + return + # If we already have the range, return it. + largest_known_prime = sieve._list[-1] + if b <= largest_known_prime: + yield from sieve.primerange(a, b) + return + # If we know some of it, return it. + if a <= largest_known_prime: + yield from sieve._list[bisect_left(sieve._list, a):] + a = largest_known_prime + 1 + elif a % 2: + a -= 1 + tail = min(b, (largest_known_prime)**2) + if a < tail: + yield from sieve._primerange(a, tail) + a = tail + if b <= a: + return + # otherwise compute, without storing, the desired range. + while 1: + a = nextprime(a) + if a < b: + yield a + else: + return + + +def randprime(a, b): + """ Return a random prime number in the range [a, b). + + Bertrand's postulate assures that + randprime(a, 2*a) will always succeed for a > 1. + + Note that due to implementation difficulties, + the prime numbers chosen are not uniformly random. + For example, there are two primes in the range [112, 128), + ``113`` and ``127``, but ``randprime(112, 128)`` returns ``127`` + with a probability of 15/17. + + Examples + ======== + + >>> from sympy import randprime, isprime + >>> randprime(1, 30) #doctest: +SKIP + 13 + >>> isprime(randprime(1, 30)) + True + + See Also + ======== + + primerange : Generate all primes in a given range + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Bertrand's_postulate + + """ + if a >= b: + return + a, b = map(int, (a, b)) + n = randint(a - 1, b) + p = nextprime(n) + if p >= b: + p = prevprime(b) + if p < a: + raise ValueError("no primes exist in the specified range") + return p + + +def primorial(n, nth=True): + """ + Returns the product of the first n primes (default) or + the primes less than or equal to n (when ``nth=False``). + + Examples + ======== + + >>> from sympy.ntheory.generate import primorial, primerange + >>> from sympy import factorint, Mul, primefactors, sqrt + >>> primorial(4) # the first 4 primes are 2, 3, 5, 7 + 210 + >>> primorial(4, nth=False) # primes <= 4 are 2 and 3 + 6 + >>> primorial(1) + 2 + >>> primorial(1, nth=False) + 1 + >>> primorial(sqrt(101), nth=False) + 210 + + One can argue that the primes are infinite since if you take + a set of primes and multiply them together (e.g. the primorial) and + then add or subtract 1, the result cannot be divided by any of the + original factors, hence either 1 or more new primes must divide this + product of primes. + + In this case, the number itself is a new prime: + + >>> factorint(primorial(4) + 1) + {211: 1} + + In this case two new primes are the factors: + + >>> factorint(primorial(4) - 1) + {11: 1, 19: 1} + + Here, some primes smaller and larger than the primes multiplied together + are obtained: + + >>> p = list(primerange(10, 20)) + >>> sorted(set(primefactors(Mul(*p) + 1)).difference(set(p))) + [2, 5, 31, 149] + + See Also + ======== + + primerange : Generate all primes in a given range + + """ + if nth: + n = as_int(n) + else: + n = int(n) + if n < 1: + raise ValueError("primorial argument must be >= 1") + p = 1 + if nth: + for i in range(1, n + 1): + p *= prime(i) + else: + for i in primerange(2, n + 1): + p *= i + return p + + +def cycle_length(f, x0, nmax=None, values=False): + """For a given iterated sequence, return a generator that gives + the length of the iterated cycle (lambda) and the length of terms + before the cycle begins (mu); if ``values`` is True then the + terms of the sequence will be returned instead. The sequence is + started with value ``x0``. + + Note: more than the first lambda + mu terms may be returned and this + is the cost of cycle detection with Brent's method; there are, however, + generally less terms calculated than would have been calculated if the + proper ending point were determined, e.g. by using Floyd's method. + + >>> from sympy.ntheory.generate import cycle_length + + This will yield successive values of i <-- func(i): + + >>> def gen(func, i): + ... while 1: + ... yield i + ... i = func(i) + ... + + A function is defined: + + >>> func = lambda i: (i**2 + 1) % 51 + + and given a seed of 4 and the mu and lambda terms calculated: + + >>> next(cycle_length(func, 4)) + (6, 3) + + We can see what is meant by looking at the output: + + >>> iter = cycle_length(func, 4, values=True) + >>> list(iter) + [4, 17, 35, 2, 5, 26, 14, 44, 50, 2, 5, 26, 14] + + There are 6 repeating values after the first 3. + + If a sequence is suspected of being longer than you might wish, ``nmax`` + can be used to exit early (and mu will be returned as None): + + >>> next(cycle_length(func, 4, nmax = 4)) + (4, None) + >>> list(cycle_length(func, 4, nmax = 4, values=True)) + [4, 17, 35, 2] + + Code modified from: + https://en.wikipedia.org/wiki/Cycle_detection. + """ + + nmax = int(nmax or 0) + + # main phase: search successive powers of two + power = lam = 1 + tortoise, hare = x0, f(x0) # f(x0) is the element/node next to x0. + i = 1 + if values: + yield tortoise + while tortoise != hare and (not nmax or i < nmax): + i += 1 + if power == lam: # time to start a new power of two? + tortoise = hare + power *= 2 + lam = 0 + if values: + yield hare + hare = f(hare) + lam += 1 + if nmax and i == nmax: + if values: + return + else: + yield nmax, None + return + if not values: + # Find the position of the first repetition of length lambda + mu = 0 + tortoise = hare = x0 + for i in range(lam): + hare = f(hare) + while tortoise != hare: + tortoise = f(tortoise) + hare = f(hare) + mu += 1 + yield lam, mu + + +def composite(nth): + """ Return the nth composite number, with the composite numbers indexed as + composite(1) = 4, composite(2) = 6, etc.... + + Examples + ======== + + >>> from sympy import composite + >>> composite(36) + 52 + >>> composite(1) + 4 + >>> composite(17737) + 20000 + + See Also + ======== + + sympy.ntheory.primetest.isprime : Test if n is prime + primerange : Generate all primes in a given range + primepi : Return the number of primes less than or equal to n + prime : Return the nth prime + compositepi : Return the number of positive composite numbers less than or equal to n + """ + n = as_int(nth) + if n < 1: + raise ValueError("nth must be a positive integer; composite(1) == 4") + composite_arr = [4, 6, 8, 9, 10, 12, 14, 15, 16, 18] + if n <= 10: + return composite_arr[n - 1] + + a, b = 4, sieve._list[-1] + if n <= b - _primepi(b) - 1: + while a < b - 1: + mid = (a + b) >> 1 + if mid - _primepi(mid) - 1 > n: + b = mid + else: + a = mid + if isprime(a): + a -= 1 + return a + + from sympy.functions.elementary.exponential import log + from sympy.functions.special.error_functions import li + a = 4 # Lower bound for binary search + b = int(n*(log(n) + log(log(n)))) # Upper bound for the search. + + while a < b: + mid = (a + b) >> 1 + if mid - li(mid) - 1 > n: + b = mid + else: + a = mid + 1 + + n_composites = a - _primepi(a) - 1 + while n_composites > n: + if not isprime(a): + n_composites -= 1 + a -= 1 + if isprime(a): + a -= 1 + return a + + +def compositepi(n): + """ Return the number of positive composite numbers less than or equal to n. + The first positive composite is 4, i.e. compositepi(4) = 1. + + Examples + ======== + + >>> from sympy import compositepi + >>> compositepi(25) + 15 + >>> compositepi(1000) + 831 + + See Also + ======== + + sympy.ntheory.primetest.isprime : Test if n is prime + primerange : Generate all primes in a given range + prime : Return the nth prime + primepi : Return the number of primes less than or equal to n + composite : Return the nth composite number + """ + n = int(n) + if n < 4: + return 0 + return n - _primepi(n) - 1 diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/modular.py b/venv/lib/python3.11/site-packages/sympy/ntheory/modular.py new file mode 100644 index 0000000000000000000000000000000000000000..628a3d8c5a7fb4b6c51ad337df66d74f90282496 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/modular.py @@ -0,0 +1,291 @@ +from math import prod + +from sympy.external.gmpy import gcd, gcdext +from sympy.ntheory.primetest import isprime +from sympy.polys.domains import ZZ +from sympy.polys.galoistools import gf_crt, gf_crt1, gf_crt2 +from sympy.utilities.misc import as_int + + +def symmetric_residue(a, m): + """Return the residual mod m such that it is within half of the modulus. + + >>> from sympy.ntheory.modular import symmetric_residue + >>> symmetric_residue(1, 6) + 1 + >>> symmetric_residue(4, 6) + -2 + """ + if a <= m // 2: + return a + return a - m + + +def crt(m, v, symmetric=False, check=True): + r"""Chinese Remainder Theorem. + + The moduli in m are assumed to be pairwise coprime. The output + is then an integer f, such that f = v_i mod m_i for each pair out + of v and m. If ``symmetric`` is False a positive integer will be + returned, else \|f\| will be less than or equal to the LCM of the + moduli, and thus f may be negative. + + If the moduli are not co-prime the correct result will be returned + if/when the test of the result is found to be incorrect. This result + will be None if there is no solution. + + The keyword ``check`` can be set to False if it is known that the moduli + are coprime. + + Examples + ======== + + As an example consider a set of residues ``U = [49, 76, 65]`` + and a set of moduli ``M = [99, 97, 95]``. Then we have:: + + >>> from sympy.ntheory.modular import crt + + >>> crt([99, 97, 95], [49, 76, 65]) + (639985, 912285) + + This is the correct result because:: + + >>> [639985 % m for m in [99, 97, 95]] + [49, 76, 65] + + If the moduli are not co-prime, you may receive an incorrect result + if you use ``check=False``: + + >>> crt([12, 6, 17], [3, 4, 2], check=False) + (954, 1224) + >>> [954 % m for m in [12, 6, 17]] + [6, 0, 2] + >>> crt([12, 6, 17], [3, 4, 2]) is None + True + >>> crt([3, 6], [2, 5]) + (5, 6) + + Note: the order of gf_crt's arguments is reversed relative to crt, + and that solve_congruence takes residue, modulus pairs. + + Programmer's note: rather than checking that all pairs of moduli share + no GCD (an O(n**2) test) and rather than factoring all moduli and seeing + that there is no factor in common, a check that the result gives the + indicated residuals is performed -- an O(n) operation. + + See Also + ======== + + solve_congruence + sympy.polys.galoistools.gf_crt : low level crt routine used by this routine + """ + if check: + m = list(map(as_int, m)) + v = list(map(as_int, v)) + + result = gf_crt(v, m, ZZ) + mm = prod(m) + + if check: + if not all(v % m == result % m for v, m in zip(v, m)): + result = solve_congruence(*list(zip(v, m)), + check=False, symmetric=symmetric) + if result is None: + return result + result, mm = result + + if symmetric: + return int(symmetric_residue(result, mm)), int(mm) + return int(result), int(mm) + + +def crt1(m): + """First part of Chinese Remainder Theorem, for multiple application. + + Examples + ======== + + >>> from sympy.ntheory.modular import crt, crt1, crt2 + >>> m = [99, 97, 95] + >>> v = [49, 76, 65] + + The following two codes have the same result. + + >>> crt(m, v) + (639985, 912285) + + >>> mm, e, s = crt1(m) + >>> crt2(m, v, mm, e, s) + (639985, 912285) + + However, it is faster when we want to fix ``m`` and + compute for multiple ``v``, i.e. the following cases: + + >>> mm, e, s = crt1(m) + >>> vs = [[52, 21, 37], [19, 46, 76]] + >>> for v in vs: + ... print(crt2(m, v, mm, e, s)) + (397042, 912285) + (803206, 912285) + + See Also + ======== + + sympy.polys.galoistools.gf_crt1 : low level crt routine used by this routine + sympy.ntheory.modular.crt + sympy.ntheory.modular.crt2 + + """ + + return gf_crt1(m, ZZ) + + +def crt2(m, v, mm, e, s, symmetric=False): + """Second part of Chinese Remainder Theorem, for multiple application. + + See ``crt1`` for usage. + + Examples + ======== + + >>> from sympy.ntheory.modular import crt1, crt2 + >>> mm, e, s = crt1([18, 42, 6]) + >>> crt2([18, 42, 6], [0, 0, 0], mm, e, s) + (0, 4536) + + See Also + ======== + + sympy.polys.galoistools.gf_crt2 : low level crt routine used by this routine + sympy.ntheory.modular.crt + sympy.ntheory.modular.crt1 + + """ + + result = gf_crt2(v, m, mm, e, s, ZZ) + + if symmetric: + return int(symmetric_residue(result, mm)), int(mm) + return int(result), int(mm) + + +def solve_congruence(*remainder_modulus_pairs, **hint): + """Compute the integer ``n`` that has the residual ``ai`` when it is + divided by ``mi`` where the ``ai`` and ``mi`` are given as pairs to + this function: ((a1, m1), (a2, m2), ...). If there is no solution, + return None. Otherwise return ``n`` and its modulus. + + The ``mi`` values need not be co-prime. If it is known that the moduli are + not co-prime then the hint ``check`` can be set to False (default=True) and + the check for a quicker solution via crt() (valid when the moduli are + co-prime) will be skipped. + + If the hint ``symmetric`` is True (default is False), the value of ``n`` + will be within 1/2 of the modulus, possibly negative. + + Examples + ======== + + >>> from sympy.ntheory.modular import solve_congruence + + What number is 2 mod 3, 3 mod 5 and 2 mod 7? + + >>> solve_congruence((2, 3), (3, 5), (2, 7)) + (23, 105) + >>> [23 % m for m in [3, 5, 7]] + [2, 3, 2] + + If you prefer to work with all remainder in one list and + all moduli in another, send the arguments like this: + + >>> solve_congruence(*zip((2, 3, 2), (3, 5, 7))) + (23, 105) + + The moduli need not be co-prime; in this case there may or + may not be a solution: + + >>> solve_congruence((2, 3), (4, 6)) is None + True + + >>> solve_congruence((2, 3), (5, 6)) + (5, 6) + + The symmetric flag will make the result be within 1/2 of the modulus: + + >>> solve_congruence((2, 3), (5, 6), symmetric=True) + (-1, 6) + + See Also + ======== + + crt : high level routine implementing the Chinese Remainder Theorem + + """ + def combine(c1, c2): + """Return the tuple (a, m) which satisfies the requirement + that n = a + i*m satisfy n = a1 + j*m1 and n = a2 = k*m2. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Method_of_successive_substitution + """ + a1, m1 = c1 + a2, m2 = c2 + a, b, c = m1, a2 - a1, m2 + g = gcd(a, b, c) + a, b, c = [i//g for i in [a, b, c]] + if a != 1: + g, inv_a, _ = gcdext(a, c) + if g != 1: + return None + b *= inv_a + a, m = a1 + m1*b, m1*c + return a, m + + rm = remainder_modulus_pairs + symmetric = hint.get('symmetric', False) + + if hint.get('check', True): + rm = [(as_int(r), as_int(m)) for r, m in rm] + + # ignore redundant pairs but raise an error otherwise; also + # make sure that a unique set of bases is sent to gf_crt if + # they are all prime. + # + # The routine will work out less-trivial violations and + # return None, e.g. for the pairs (1,3) and (14,42) there + # is no answer because 14 mod 42 (having a gcd of 14) implies + # (14/2) mod (42/2), (14/7) mod (42/7) and (14/14) mod (42/14) + # which, being 0 mod 3, is inconsistent with 1 mod 3. But to + # preprocess the input beyond checking of another pair with 42 + # or 3 as the modulus (for this example) is not necessary. + uniq = {} + for r, m in rm: + r %= m + if m in uniq: + if r != uniq[m]: + return None + continue + uniq[m] = r + rm = [(r, m) for m, r in uniq.items()] + del uniq + + # if the moduli are co-prime, the crt will be significantly faster; + # checking all pairs for being co-prime gets to be slow but a prime + # test is a good trade-off + if all(isprime(m) for r, m in rm): + r, m = list(zip(*rm)) + return crt(m, r, symmetric=symmetric, check=False) + + rv = (0, 1) + for rmi in rm: + rv = combine(rv, rmi) + if rv is None: + break + n, m = rv + n = n % m + else: + if symmetric: + return symmetric_residue(n, m), m + return n, m diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/multinomial.py b/venv/lib/python3.11/site-packages/sympy/ntheory/multinomial.py new file mode 100644 index 0000000000000000000000000000000000000000..8ec50fdb533be547b9a8e60dc47568965bf89436 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/multinomial.py @@ -0,0 +1,188 @@ +from sympy.utilities.misc import as_int + + +def binomial_coefficients(n): + """Return a dictionary containing pairs :math:`{(k1,k2) : C_kn}` where + :math:`C_kn` are binomial coefficients and :math:`n=k1+k2`. + + Examples + ======== + + >>> from sympy.ntheory import binomial_coefficients + >>> binomial_coefficients(9) + {(0, 9): 1, (1, 8): 9, (2, 7): 36, (3, 6): 84, + (4, 5): 126, (5, 4): 126, (6, 3): 84, (7, 2): 36, (8, 1): 9, (9, 0): 1} + + See Also + ======== + + binomial_coefficients_list, multinomial_coefficients + """ + n = as_int(n) + d = {(0, n): 1, (n, 0): 1} + a = 1 + for k in range(1, n//2 + 1): + a = (a * (n - k + 1))//k + d[k, n - k] = d[n - k, k] = a + return d + + +def binomial_coefficients_list(n): + """ Return a list of binomial coefficients as rows of the Pascal's + triangle. + + Examples + ======== + + >>> from sympy.ntheory import binomial_coefficients_list + >>> binomial_coefficients_list(9) + [1, 9, 36, 84, 126, 126, 84, 36, 9, 1] + + See Also + ======== + + binomial_coefficients, multinomial_coefficients + """ + n = as_int(n) + d = [1] * (n + 1) + a = 1 + for k in range(1, n//2 + 1): + a = (a * (n - k + 1))//k + d[k] = d[n - k] = a + return d + + +def multinomial_coefficients(m, n): + r"""Return a dictionary containing pairs ``{(k1,k2,..,km) : C_kn}`` + where ``C_kn`` are multinomial coefficients such that + ``n=k1+k2+..+km``. + + Examples + ======== + + >>> from sympy.ntheory import multinomial_coefficients + >>> multinomial_coefficients(2, 5) # indirect doctest + {(0, 5): 1, (1, 4): 5, (2, 3): 10, (3, 2): 10, (4, 1): 5, (5, 0): 1} + + Notes + ===== + + The algorithm is based on the following result: + + .. math:: + \binom{n}{k_1, \ldots, k_m} = + \frac{k_1 + 1}{n - k_1} \sum_{i=2}^m \binom{n}{k_1 + 1, \ldots, k_i - 1, \ldots} + + Code contributed to Sage by Yann Laigle-Chapuy, copied with permission + of the author. + + See Also + ======== + + binomial_coefficients_list, binomial_coefficients + """ + m = as_int(m) + n = as_int(n) + if not m: + if n: + return {} + return {(): 1} + if m == 2: + return binomial_coefficients(n) + if m >= 2*n and n > 1: + return dict(multinomial_coefficients_iterator(m, n)) + t = [n] + [0] * (m - 1) + r = {tuple(t): 1} + if n: + j = 0 # j will be the leftmost nonzero position + else: + j = m + # enumerate tuples in co-lex order + while j < m - 1: + # compute next tuple + tj = t[j] + if j: + t[j] = 0 + t[0] = tj + if tj > 1: + t[j + 1] += 1 + j = 0 + start = 1 + v = 0 + else: + j += 1 + start = j + 1 + v = r[tuple(t)] + t[j] += 1 + # compute the value + # NB: the initialization of v was done above + for k in range(start, m): + if t[k]: + t[k] -= 1 + v += r[tuple(t)] + t[k] += 1 + t[0] -= 1 + r[tuple(t)] = (v * tj) // (n - t[0]) + return r + + +def multinomial_coefficients_iterator(m, n, _tuple=tuple): + """multinomial coefficient iterator + + This routine has been optimized for `m` large with respect to `n` by taking + advantage of the fact that when the monomial tuples `t` are stripped of + zeros, their coefficient is the same as that of the monomial tuples from + ``multinomial_coefficients(n, n)``. Therefore, the latter coefficients are + precomputed to save memory and time. + + >>> from sympy.ntheory.multinomial import multinomial_coefficients + >>> m53, m33 = multinomial_coefficients(5,3), multinomial_coefficients(3,3) + >>> m53[(0,0,0,1,2)] == m53[(0,0,1,0,2)] == m53[(1,0,2,0,0)] == m33[(0,1,2)] + True + + Examples + ======== + + >>> from sympy.ntheory.multinomial import multinomial_coefficients_iterator + >>> it = multinomial_coefficients_iterator(20,3) + >>> next(it) + ((3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 1) + """ + m = as_int(m) + n = as_int(n) + if m < 2*n or n == 1: + mc = multinomial_coefficients(m, n) + yield from mc.items() + else: + mc = multinomial_coefficients(n, n) + mc1 = {} + for k, v in mc.items(): + mc1[_tuple(filter(None, k))] = v + mc = mc1 + + t = [n] + [0] * (m - 1) + t1 = _tuple(t) + b = _tuple(filter(None, t1)) + yield (t1, mc[b]) + if n: + j = 0 # j will be the leftmost nonzero position + else: + j = m + # enumerate tuples in co-lex order + while j < m - 1: + # compute next tuple + tj = t[j] + if j: + t[j] = 0 + t[0] = tj + if tj > 1: + t[j + 1] += 1 + j = 0 + else: + j += 1 + t[j] += 1 + + t[0] -= 1 + t1 = _tuple(t) + b = _tuple(filter(None, t1)) + yield (t1, mc[b]) diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/partitions_.py b/venv/lib/python3.11/site-packages/sympy/ntheory/partitions_.py new file mode 100644 index 0000000000000000000000000000000000000000..953fa9e2fef146b0d3a9baad0ec5e1353ad6f237 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/partitions_.py @@ -0,0 +1,277 @@ +from mpmath.libmp import (fzero, from_int, from_rational, + fone, fhalf, bitcount, to_int, mpf_mul, mpf_div, mpf_sub, + mpf_add, mpf_sqrt, mpf_pi, mpf_cosh_sinh, mpf_cos, mpf_sin) +from .residue_ntheory import _sqrt_mod_prime_power, is_quad_residue +from sympy.utilities.decorator import deprecated +from sympy.utilities.memoization import recurrence_memo + +import math +from itertools import count + +def _pre(): + maxn = 10**5 + global _factor, _totient + _factor = [0]*maxn + _totient = [1]*maxn + lim = int(maxn**0.5) + 5 + for i in range(2, lim): + if _factor[i] == 0: + for j in range(i*i, maxn, i): + if _factor[j] == 0: + _factor[j] = i + for i in range(2, maxn): + if _factor[i] == 0: + _factor[i] = i + _totient[i] = i-1 + continue + x = _factor[i] + y = i//x + if y % x == 0: + _totient[i] = _totient[y]*x + else: + _totient[i] = _totient[y]*(x - 1) + +def _a(n, k, prec): + """ Compute the inner sum in HRR formula [1]_ + + References + ========== + + .. [1] https://msp.org/pjm/1956/6-1/pjm-v6-n1-p18-p.pdf + + """ + if k == 1: + return fone + + k1 = k + e = 0 + p = _factor[k] + while k1 % p == 0: + k1 //= p + e += 1 + k2 = k//k1 # k2 = p^e + v = 1 - 24*n + pi = mpf_pi(prec) + + if k1 == 1: + # k = p^e + if p == 2: + mod = 8*k + v = mod + v % mod + v = (v*pow(9, k - 1, mod)) % mod + m = _sqrt_mod_prime_power(v, 2, e + 3)[0] + arg = mpf_div(mpf_mul( + from_int(4*m), pi, prec), from_int(mod), prec) + return mpf_mul(mpf_mul( + from_int((-1)**e*(2 - (m % 4))), + mpf_sqrt(from_int(k), prec), prec), + mpf_sin(arg, prec), prec) + if p == 3: + mod = 3*k + v = mod + v % mod + if e > 1: + v = (v*pow(64, k//3 - 1, mod)) % mod + m = _sqrt_mod_prime_power(v, 3, e + 1)[0] + arg = mpf_div(mpf_mul(from_int(4*m), pi, prec), + from_int(mod), prec) + return mpf_mul(mpf_mul( + from_int(2*(-1)**(e + 1)*(3 - 2*(m % 3))), + mpf_sqrt(from_int(k//3), prec), prec), + mpf_sin(arg, prec), prec) + v = k + v % k + jacobi3 = -1 if k % 12 in [5, 7] else 1 + if v % p == 0: + if e == 1: + return mpf_mul( + from_int(jacobi3), + mpf_sqrt(from_int(k), prec), prec) + return fzero + if not is_quad_residue(v, p): + return fzero + _phi = p**(e - 1)*(p - 1) + v = (v*pow(576, _phi - 1, k)) + m = _sqrt_mod_prime_power(v, p, e)[0] + arg = mpf_div( + mpf_mul(from_int(4*m), pi, prec), + from_int(k), prec) + return mpf_mul(mpf_mul( + from_int(2*jacobi3), + mpf_sqrt(from_int(k), prec), prec), + mpf_cos(arg, prec), prec) + + if p != 2 or e >= 3: + d1, d2 = math.gcd(k1, 24), math.gcd(k2, 24) + e = 24//(d1*d2) + n1 = ((d2*e*n + (k2**2 - 1)//d1)* + pow(e*k2*k2*d2, _totient[k1] - 1, k1)) % k1 + n2 = ((d1*e*n + (k1**2 - 1)//d2)* + pow(e*k1*k1*d1, _totient[k2] - 1, k2)) % k2 + return mpf_mul(_a(n1, k1, prec), _a(n2, k2, prec), prec) + if e == 2: + n1 = ((8*n + 5)*pow(128, _totient[k1] - 1, k1)) % k1 + n2 = (4 + ((n - 2 - (k1**2 - 1)//8)*(k1**2)) % 4) % 4 + return mpf_mul(mpf_mul( + from_int(-1), + _a(n1, k1, prec), prec), + _a(n2, k2, prec)) + n1 = ((8*n + 1)*pow(32, _totient[k1] - 1, k1)) % k1 + n2 = (2 + (n - (k1**2 - 1)//8) % 2) % 2 + return mpf_mul(_a(n1, k1, prec), _a(n2, k2, prec), prec) + +def _d(n, j, prec, sq23pi, sqrt8): + """ + Compute the sinh term in the outer sum of the HRR formula. + The constants sqrt(2/3*pi) and sqrt(8) must be precomputed. + """ + j = from_int(j) + pi = mpf_pi(prec) + a = mpf_div(sq23pi, j, prec) + b = mpf_sub(from_int(n), from_rational(1, 24, prec), prec) + c = mpf_sqrt(b, prec) + ch, sh = mpf_cosh_sinh(mpf_mul(a, c), prec) + D = mpf_div( + mpf_sqrt(j, prec), + mpf_mul(mpf_mul(sqrt8, b), pi), prec) + E = mpf_sub(mpf_mul(a, ch), mpf_div(sh, c, prec), prec) + return mpf_mul(D, E) + + +@recurrence_memo([1, 1]) +def _partition_rec(n: int, prev) -> int: + """ Calculate the partition function P(n) + + Parameters + ========== + + n : int + nonnegative integer + + """ + v = 0 + penta = 0 # pentagonal number: 1, 5, 12, ... + for i in count(): + penta += 3*i + 1 + np = n - penta + if np < 0: + break + s = prev[np] + np -= i + 1 + # np = n - gp where gp = generalized pentagonal: 2, 7, 15, ... + if 0 <= np: + s += prev[np] + v += -s if i % 2 else s + return v + + +def _partition(n: int) -> int: + """ Calculate the partition function P(n) + + Parameters + ========== + + n : int + + """ + if n < 0: + return 0 + if (n <= 200_000 and n - _partition_rec.cache_length() < 70 or + _partition_rec.cache_length() == 2 and n < 14_400): + # There will be 2*10**5 elements created here + # and n elements created by partition, so in case we + # are going to be working with small n, we just + # use partition to calculate (and cache) the values + # since lookup is used there while summation, using + # _factor and _totient, will be used below. But we + # only do so if n is relatively close to the length + # of the cache since doing 1 calculation here is about + # the same as adding 70 elements to the cache. In addition, + # the startup here costs about the same as calculating the first + # 14,400 values via partition, so we delay startup here unless n + # is smaller than that. + return _partition_rec(n) + if '_factor' not in globals(): + _pre() + # Estimate number of bits in p(n). This formula could be tidied + pbits = int(( + math.pi*(2*n/3.)**0.5 - + math.log(4*n))/math.log(10) + 1) * \ + math.log2(10) + prec = p = int(pbits*1.1 + 100) + + # find the number of terms needed so rounded sum will be accurate + # using Rademacher's bound M(n, N) for the remainder after a partial + # sum of N terms (https://arxiv.org/pdf/1205.5991.pdf, (1.8)) + c1 = 44*math.pi**2/(225*math.sqrt(3)) + c2 = math.pi*math.sqrt(2)/75 + c3 = math.pi*math.sqrt(2/3) + def _M(n, N): + sqrt = math.sqrt + return c1/sqrt(N) + c2*sqrt(N/(n - 1))*math.sinh(c3*sqrt(n)/N) + big = max(9, math.ceil(n**0.5)) # should be too large (for n > 65, ceil should work) + assert _M(n, big) < 0.5 # else double big until too large + while big > 40 and _M(n, big) < 0.5: + big //= 2 + small = big + big = small*2 + while big - small > 1: + N = (big + small)//2 + if (er := _M(n, N)) < 0.5: + big = N + elif er >= 0.5: + small = N + M = big # done with function M; now have value + + # sanity check for expected size of answer + if M > 10**5: # i.e. M > maxn + raise ValueError("Input too big") # i.e. n > 149832547102 + + # calculate it + s = fzero + sq23pi = mpf_mul(mpf_sqrt(from_rational(2, 3, p), p), mpf_pi(p), p) + sqrt8 = mpf_sqrt(from_int(8), p) + for q in range(1, M): + a = _a(n, q, p) + d = _d(n, q, p, sq23pi, sqrt8) + s = mpf_add(s, mpf_mul(a, d), prec) + # On average, the terms decrease rapidly in magnitude. + # Dynamically reducing the precision greatly improves + # performance. + p = bitcount(abs(to_int(d))) + 50 + return int(to_int(mpf_add(s, fhalf, prec))) + + +@deprecated("""\ +The `sympy.ntheory.partitions_.npartitions` has been moved to `sympy.functions.combinatorial.numbers.partition`.""", +deprecated_since_version="1.13", +active_deprecations_target='deprecated-ntheory-symbolic-functions') +def npartitions(n, verbose=False): + """ + Calculate the partition function P(n), i.e. the number of ways that + n can be written as a sum of positive integers. + + .. deprecated:: 1.13 + + The ``npartitions`` function is deprecated. Use :class:`sympy.functions.combinatorial.numbers.partition` + instead. See its documentation for more information. See + :ref:`deprecated-ntheory-symbolic-functions` for details. + + P(n) is computed using the Hardy-Ramanujan-Rademacher formula [1]_. + + + The correctness of this implementation has been tested through $10^{10}$. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import partition + >>> partition(25) + 1958 + + References + ========== + + .. [1] https://mathworld.wolfram.com/PartitionFunctionP.html + + """ + from sympy.functions.combinatorial.numbers import partition as func_partition + return func_partition(n) diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/primetest.py b/venv/lib/python3.11/site-packages/sympy/ntheory/primetest.py new file mode 100644 index 0000000000000000000000000000000000000000..ff3cb82cc51bf57ca345a7d72ee715c861f62e2a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/primetest.py @@ -0,0 +1,830 @@ +""" +Primality testing + +""" + +from itertools import count + +from sympy.core.sympify import sympify +from sympy.external.gmpy import (gmpy as _gmpy, gcd, jacobi, + is_square as gmpy_is_square, + bit_scan1, is_fermat_prp, is_euler_prp, + is_selfridge_prp, is_strong_selfridge_prp, + is_strong_bpsw_prp) +from sympy.external.ntheory import _lucas_sequence +from sympy.utilities.misc import as_int, filldedent + +# Note: This list should be updated whenever new Mersenne primes are found. +# Refer: https://www.mersenne.org/ +MERSENNE_PRIME_EXPONENTS = (2, 3, 5, 7, 13, 17, 19, 31, 61, 89, 107, 127, 521, 607, 1279, 2203, + 2281, 3217, 4253, 4423, 9689, 9941, 11213, 19937, 21701, 23209, 44497, 86243, 110503, 132049, + 216091, 756839, 859433, 1257787, 1398269, 2976221, 3021377, 6972593, 13466917, 20996011, 24036583, + 25964951, 30402457, 32582657, 37156667, 42643801, 43112609, 57885161, 74207281, 77232917, 82589933, + 136279841) + + +def is_fermat_pseudoprime(n, a): + r"""Returns True if ``n`` is prime or is an odd composite integer that + is coprime to ``a`` and satisfy the modular arithmetic congruence relation: + + .. math :: + a^{n-1} \equiv 1 \pmod{n} + + (where mod refers to the modulo operation). + + Parameters + ========== + + n : Integer + ``n`` is a positive integer. + a : Integer + ``a`` is a positive integer. + ``a`` and ``n`` should be relatively prime. + + Returns + ======= + + bool : If ``n`` is prime, it always returns ``True``. + The composite number that returns ``True`` is called an Fermat pseudoprime. + + Examples + ======== + + >>> from sympy.ntheory.primetest import is_fermat_pseudoprime + >>> from sympy.ntheory.factor_ import isprime + >>> for n in range(1, 1000): + ... if is_fermat_pseudoprime(n, 2) and not isprime(n): + ... print(n) + 341 + 561 + 645 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Fermat_pseudoprime + """ + n, a = as_int(n), as_int(a) + if a == 1: + return n == 2 or bool(n % 2) + return is_fermat_prp(n, a) + + +def is_euler_pseudoprime(n, a): + r"""Returns True if ``n`` is prime or is an odd composite integer that + is coprime to ``a`` and satisfy the modular arithmetic congruence relation: + + .. math :: + a^{(n-1)/2} \equiv \pm 1 \pmod{n} + + (where mod refers to the modulo operation). + + Parameters + ========== + + n : Integer + ``n`` is a positive integer. + a : Integer + ``a`` is a positive integer. + ``a`` and ``n`` should be relatively prime. + + Returns + ======= + + bool : If ``n`` is prime, it always returns ``True``. + The composite number that returns ``True`` is called an Euler pseudoprime. + + Examples + ======== + + >>> from sympy.ntheory.primetest import is_euler_pseudoprime + >>> from sympy.ntheory.factor_ import isprime + >>> for n in range(1, 1000): + ... if is_euler_pseudoprime(n, 2) and not isprime(n): + ... print(n) + 341 + 561 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Euler_pseudoprime + """ + n, a = as_int(n), as_int(a) + if a < 1: + raise ValueError("a should be an integer greater than 0") + if n < 1: + raise ValueError("n should be an integer greater than 0") + if n == 1: + return False + if a == 1: + return n == 2 or bool(n % 2) # (prime or odd composite) + if n % 2 == 0: + return n == 2 + if gcd(n, a) != 1: + raise ValueError("The two numbers should be relatively prime") + return pow(a, (n - 1) // 2, n) in [1, n - 1] + + +def is_euler_jacobi_pseudoprime(n, a): + r"""Returns True if ``n`` is prime or is an odd composite integer that + is coprime to ``a`` and satisfy the modular arithmetic congruence relation: + + .. math :: + a^{(n-1)/2} \equiv \left(\frac{a}{n}\right) \pmod{n} + + (where mod refers to the modulo operation). + + Parameters + ========== + + n : Integer + ``n`` is a positive integer. + a : Integer + ``a`` is a positive integer. + ``a`` and ``n`` should be relatively prime. + + Returns + ======= + + bool : If ``n`` is prime, it always returns ``True``. + The composite number that returns ``True`` is called an Euler-Jacobi pseudoprime. + + Examples + ======== + + >>> from sympy.ntheory.primetest import is_euler_jacobi_pseudoprime + >>> from sympy.ntheory.factor_ import isprime + >>> for n in range(1, 1000): + ... if is_euler_jacobi_pseudoprime(n, 2) and not isprime(n): + ... print(n) + 561 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Euler%E2%80%93Jacobi_pseudoprime + """ + n, a = as_int(n), as_int(a) + if a == 1: + return n == 2 or bool(n % 2) + return is_euler_prp(n, a) + + +def is_square(n, prep=True): + """Return True if n == a * a for some integer a, else False. + If n is suspected of *not* being a square then this is a + quick method of confirming that it is not. + + Examples + ======== + + >>> from sympy.ntheory.primetest import is_square + >>> is_square(25) + True + >>> is_square(2) + False + + References + ========== + + .. [1] https://mersenneforum.org/showpost.php?p=110896 + + See Also + ======== + sympy.core.intfunc.isqrt + """ + if prep: + n = as_int(n) + if n < 0: + return False + if n in (0, 1): + return True + return gmpy_is_square(n) + + +def _test(n, base, s, t): + """Miller-Rabin strong pseudoprime test for one base. + Return False if n is definitely composite, True if n is + probably prime, with a probability greater than 3/4. + + """ + # do the Fermat test + b = pow(base, t, n) + if b == 1 or b == n - 1: + return True + for _ in range(s - 1): + b = pow(b, 2, n) + if b == n - 1: + return True + # see I. Niven et al. "An Introduction to Theory of Numbers", page 78 + if b == 1: + return False + return False + + +def mr(n, bases): + """Perform a Miller-Rabin strong pseudoprime test on n using a + given list of bases/witnesses. + + References + ========== + + .. [1] Richard Crandall & Carl Pomerance (2005), "Prime Numbers: + A Computational Perspective", Springer, 2nd edition, 135-138 + + A list of thresholds and the bases they require are here: + https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test#Deterministic_variants + + Examples + ======== + + >>> from sympy.ntheory.primetest import mr + >>> mr(1373651, [2, 3]) + False + >>> mr(479001599, [31, 73]) + True + + """ + from sympy.polys.domains import ZZ + + n = as_int(n) + if n < 2 or (n > 2 and n % 2 == 0): + return False + # remove powers of 2 from n-1 (= t * 2**s) + s = bit_scan1(n - 1) + t = n >> s + for base in bases: + # Bases >= n are wrapped, bases < 2 are invalid + if base >= n: + base %= n + if base >= 2: + base = ZZ(base) + if not _test(n, base, s, t): + return False + return True + + +def _lucas_extrastrong_params(n): + """Calculates the "extra strong" parameters (D, P, Q) for n. + + Parameters + ========== + + n : int + positive odd integer + + Returns + ======= + + D, P, Q: "extra strong" parameters. + ``(0, 0, 0)`` if we find a nontrivial divisor of ``n``. + + Examples + ======== + + >>> from sympy.ntheory.primetest import _lucas_extrastrong_params + >>> _lucas_extrastrong_params(101) + (12, 4, 1) + >>> _lucas_extrastrong_params(15) + (0, 0, 0) + + References + ========== + .. [1] OEIS A217719: Extra Strong Lucas Pseudoprimes + https://oeis.org/A217719 + .. [2] https://en.wikipedia.org/wiki/Lucas_pseudoprime + + """ + for P in count(3): + D = P**2 - 4 + j = jacobi(D, n) + if j == -1: + return (D, P, 1) + elif j == 0 and D % n: + return (0, 0, 0) + + +def is_lucas_prp(n): + """Standard Lucas compositeness test with Selfridge parameters. Returns + False if n is definitely composite, and True if n is a Lucas probable + prime. + + This is typically used in combination with the Miller-Rabin test. + + References + ========== + .. [1] Robert Baillie, Samuel S. Wagstaff, Lucas Pseudoprimes, + Math. Comp. Vol 35, Number 152 (1980), pp. 1391-1417, + https://doi.org/10.1090%2FS0025-5718-1980-0583518-6 + http://mpqs.free.fr/LucasPseudoprimes.pdf + .. [2] OEIS A217120: Lucas Pseudoprimes + https://oeis.org/A217120 + .. [3] https://en.wikipedia.org/wiki/Lucas_pseudoprime + + Examples + ======== + + >>> from sympy.ntheory.primetest import isprime, is_lucas_prp + >>> for i in range(10000): + ... if is_lucas_prp(i) and not isprime(i): + ... print(i) + 323 + 377 + 1159 + 1829 + 3827 + 5459 + 5777 + 9071 + 9179 + """ + n = as_int(n) + if n < 2: + return False + return is_selfridge_prp(n) + + +def is_strong_lucas_prp(n): + """Strong Lucas compositeness test with Selfridge parameters. Returns + False if n is definitely composite, and True if n is a strong Lucas + probable prime. + + This is often used in combination with the Miller-Rabin test, and + in particular, when combined with M-R base 2 creates the strong BPSW test. + + References + ========== + .. [1] Robert Baillie, Samuel S. Wagstaff, Lucas Pseudoprimes, + Math. Comp. Vol 35, Number 152 (1980), pp. 1391-1417, + https://doi.org/10.1090%2FS0025-5718-1980-0583518-6 + http://mpqs.free.fr/LucasPseudoprimes.pdf + .. [2] OEIS A217255: Strong Lucas Pseudoprimes + https://oeis.org/A217255 + .. [3] https://en.wikipedia.org/wiki/Lucas_pseudoprime + .. [4] https://en.wikipedia.org/wiki/Baillie-PSW_primality_test + + Examples + ======== + + >>> from sympy.ntheory.primetest import isprime, is_strong_lucas_prp + >>> for i in range(20000): + ... if is_strong_lucas_prp(i) and not isprime(i): + ... print(i) + 5459 + 5777 + 10877 + 16109 + 18971 + """ + n = as_int(n) + if n < 2: + return False + return is_strong_selfridge_prp(n) + + +def is_extra_strong_lucas_prp(n): + """Extra Strong Lucas compositeness test. Returns False if n is + definitely composite, and True if n is an "extra strong" Lucas probable + prime. + + The parameters are selected using P = 3, Q = 1, then incrementing P until + (D|n) == -1. The test itself is as defined in [1]_, from the + Mo and Jones preprint. The parameter selection and test are the same as + used in OEIS A217719, Perl's Math::Prime::Util, and the Lucas pseudoprime + page on Wikipedia. + + It is 20-50% faster than the strong test. + + Because of the different parameters selected, there is no relationship + between the strong Lucas pseudoprimes and extra strong Lucas pseudoprimes. + In particular, one is not a subset of the other. + + References + ========== + .. [1] Jon Grantham, Frobenius Pseudoprimes, + Math. Comp. Vol 70, Number 234 (2001), pp. 873-891, + https://doi.org/10.1090%2FS0025-5718-00-01197-2 + .. [2] OEIS A217719: Extra Strong Lucas Pseudoprimes + https://oeis.org/A217719 + .. [3] https://en.wikipedia.org/wiki/Lucas_pseudoprime + + Examples + ======== + + >>> from sympy.ntheory.primetest import isprime, is_extra_strong_lucas_prp + >>> for i in range(20000): + ... if is_extra_strong_lucas_prp(i) and not isprime(i): + ... print(i) + 989 + 3239 + 5777 + 10877 + """ + # Implementation notes: + # 1) the parameters differ from Thomas R. Nicely's. His parameter + # selection leads to pseudoprimes that overlap M-R tests, and + # contradict Baillie and Wagstaff's suggestion of (D|n) = -1. + # 2) The MathWorld page as of June 2013 specifies Q=-1. The Lucas + # sequence must have Q=1. See Grantham theorem 2.3, any of the + # references on the MathWorld page, or run it and see Q=-1 is wrong. + n = as_int(n) + if n == 2: + return True + if n < 2 or (n % 2) == 0: + return False + if gmpy_is_square(n): + return False + + D, P, Q = _lucas_extrastrong_params(n) + if D == 0: + return False + + # remove powers of 2 from n+1 (= k * 2**s) + s = bit_scan1(n + 1) + k = (n + 1) >> s + + U, V, _ = _lucas_sequence(n, P, Q, k) + + if U == 0 and (V == 2 or V == n - 2): + return True + for _ in range(1, s): + if V == 0: + return True + V = (V*V - 2) % n + return False + + +def proth_test(n): + r""" Test if the Proth number `n = k2^m + 1` is prime. where k is a positive odd number and `2^m > k`. + + Parameters + ========== + + n : Integer + ``n`` is Proth number + + Returns + ======= + + bool : If ``True``, then ``n`` is the Proth prime + + Raises + ====== + + ValueError + If ``n`` is not Proth number. + + Examples + ======== + + >>> from sympy.ntheory.primetest import proth_test + >>> proth_test(41) + True + >>> proth_test(57) + False + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Proth_prime + + """ + n = as_int(n) + if n < 3: + raise ValueError("n is not Proth number") + m = bit_scan1(n - 1) + k = n >> m + if m < k.bit_length(): + raise ValueError("n is not Proth number") + if n % 3 == 0: + return n == 3 + if k % 3: # n % 12 == 5 + return pow(3, n >> 1, n) == n - 1 + # If `n` is a square number, then `jacobi(a, n) = 1` for any `a` + if gmpy_is_square(n): + return False + # `a` may be chosen at random. + # In any case, we want to find `a` such that `jacobi(a, n) = -1`. + for a in range(5, n): + j = jacobi(a, n) + if j == -1: + return pow(a, n >> 1, n) == n - 1 + if j == 0: + return False + + +def _lucas_lehmer_primality_test(p): + r""" Test if the Mersenne number `M_p = 2^p-1` is prime. + + Parameters + ========== + + p : int + ``p`` is an odd prime number + + Returns + ======= + + bool : If ``True``, then `M_p` is the Mersenne prime + + Examples + ======== + + >>> from sympy.ntheory.primetest import _lucas_lehmer_primality_test + >>> _lucas_lehmer_primality_test(5) # 2**5 - 1 = 31 is prime + True + >>> _lucas_lehmer_primality_test(11) # 2**11 - 1 = 2047 is not prime + False + + See Also + ======== + + is_mersenne_prime + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Lucas%E2%80%93Lehmer_primality_test + + """ + v = 4 + m = 2**p - 1 + for _ in range(p - 2): + v = pow(v, 2, m) - 2 + return v == 0 + + +def is_mersenne_prime(n): + """Returns True if ``n`` is a Mersenne prime, else False. + + A Mersenne prime is a prime number having the form `2^i - 1`. + + Examples + ======== + + >>> from sympy.ntheory.factor_ import is_mersenne_prime + >>> is_mersenne_prime(6) + False + >>> is_mersenne_prime(127) + True + + References + ========== + + .. [1] https://mathworld.wolfram.com/MersennePrime.html + + """ + n = as_int(n) + if n < 1: + return False + if n & (n + 1): + # n is not Mersenne number + return False + p = n.bit_length() + if p in MERSENNE_PRIME_EXPONENTS: + return True + if p < 65_000_000 or not isprime(p): + # According to GIMPS, verification was completed on September 19, 2023 for p less than 65 million. + # https://www.mersenne.org/report_milestones/ + # If p is composite number, then n=2**p-1 is composite number. + return False + result = _lucas_lehmer_primality_test(p) + if result: + raise ValueError(filldedent(''' + This Mersenne Prime, 2^%s - 1, should + be added to SymPy's known values.''' % p)) + return result + + +_MR_BASES_32 = [15591, 2018, 166, 7429, 8064, 16045, 10503, 4399, 1949, 1295, + 2776, 3620, 560, 3128, 5212, 2657, 2300, 2021, 4652, 1471, + 9336, 4018, 2398, 20462, 10277, 8028, 2213, 6219, 620, 3763, + 4852, 5012, 3185, 1333, 6227,5298, 1074, 2391, 5113, 7061, + 803, 1269, 3875, 422, 751, 580, 4729, 10239, 746, 2951, 556, + 2206, 3778, 481, 1522, 3476, 481, 2487, 3266, 5633, 488, 3373, + 6441, 3344, 17, 15105, 1490, 4154, 2036, 1882, 1813, 467, + 3307, 14042, 6371, 658, 1005, 903, 737, 1887, 7447, 1888, + 2848, 1784, 7559, 3400, 951, 13969, 4304, 177, 41, 19875, + 3110, 13221, 8726, 571, 7043, 6943, 1199, 352, 6435, 165, + 1169, 3315, 978, 233, 3003, 2562, 2994, 10587, 10030, 2377, + 1902, 5354, 4447, 1555, 263, 27027, 2283, 305, 669, 1912, 601, + 6186, 429, 1930, 14873, 1784, 1661, 524, 3577, 236, 2360, + 6146, 2850, 55637, 1753, 4178, 8466, 222, 2579, 2743, 2031, + 2226, 2276, 374, 2132, 813, 23788, 1610, 4422, 5159, 1725, + 3597, 3366, 14336, 579, 165, 1375, 10018, 12616, 9816, 1371, + 536, 1867, 10864, 857, 2206, 5788, 434, 8085, 17618, 727, + 3639, 1595, 4944, 2129, 2029, 8195, 8344, 6232, 9183, 8126, + 1870, 3296, 7455, 8947, 25017, 541, 19115, 368, 566, 5674, + 411, 522, 1027, 8215, 2050, 6544, 10049, 614, 774, 2333, 3007, + 35201, 4706, 1152, 1785, 1028, 1540, 3743, 493, 4474, 2521, + 26845, 8354, 864, 18915, 5465, 2447, 42, 4511, 1660, 166, + 1249, 6259, 2553, 304, 272, 7286, 73, 6554, 899, 2816, 5197, + 13330, 7054, 2818, 3199, 811, 922, 350, 7514, 4452, 3449, + 2663, 4708, 418, 1621, 1171, 3471, 88, 11345, 412, 1559, 194] + + +def isprime(n): + """ + Test if n is a prime number (True) or not (False). For n < 2^64 the + answer is definitive; larger n values have a small probability of actually + being pseudoprimes. + + Negative numbers (e.g. -2) are not considered prime. + + The first step is looking for trivial factors, which if found enables + a quick return. Next, if the sieve is large enough, use bisection search + on the sieve. For small numbers, a set of deterministic Miller-Rabin + tests are performed with bases that are known to have no counterexamples + in their range. Finally if the number is larger than 2^64, a strong + BPSW test is performed. While this is a probable prime test and we + believe counterexamples exist, there are no known counterexamples. + + Examples + ======== + + >>> from sympy.ntheory import isprime + >>> isprime(13) + True + >>> isprime(15) + False + + Notes + ===== + + This routine is intended only for integer input, not numerical + expressions which may represent numbers. Floats are also + rejected as input because they represent numbers of limited + precision. While it is tempting to permit 7.0 to represent an + integer there are errors that may "pass silently" if this is + allowed: + + >>> from sympy import Float, S + >>> int(1e3) == 1e3 == 10**3 + True + >>> int(1e23) == 1e23 + True + >>> int(1e23) == 10**23 + False + + >>> near_int = 1 + S(1)/10**19 + >>> near_int == int(near_int) + False + >>> n = Float(near_int, 10) # truncated by precision + >>> n % 1 == 0 + True + >>> n = Float(near_int, 20) + >>> n % 1 == 0 + False + + See Also + ======== + + sympy.ntheory.generate.primerange : Generates all primes in a given range + sympy.functions.combinatorial.numbers.primepi : Return the number of primes less than or equal to n + sympy.ntheory.generate.prime : Return the nth prime + + References + ========== + .. [1] https://en.wikipedia.org/wiki/Strong_pseudoprime + .. [2] Robert Baillie, Samuel S. Wagstaff, Lucas Pseudoprimes, + Math. Comp. Vol 35, Number 152 (1980), pp. 1391-1417, + https://doi.org/10.1090%2FS0025-5718-1980-0583518-6 + http://mpqs.free.fr/LucasPseudoprimes.pdf + .. [3] https://en.wikipedia.org/wiki/Baillie-PSW_primality_test + """ + n = as_int(n) + + # Step 1, do quick composite testing via trial division. The individual + # modulo tests benchmark faster than one or two primorial igcds for me. + # The point here is just to speedily handle small numbers and many + # composites. Step 2 only requires that n <= 2 get handled here. + if n in [2, 3, 5]: + return True + if n < 2 or (n % 2) == 0 or (n % 3) == 0 or (n % 5) == 0: + return False + if n < 49: + return True + if (n % 7) == 0 or (n % 11) == 0 or (n % 13) == 0 or (n % 17) == 0 or \ + (n % 19) == 0 or (n % 23) == 0 or (n % 29) == 0 or (n % 31) == 0 or \ + (n % 37) == 0 or (n % 41) == 0 or (n % 43) == 0 or (n % 47) == 0: + return False + if n < 2809: + return True + if n < 65077: + # There are only five Euler pseudoprimes with a least prime factor greater than 47 + return pow(2, n >> 1, n) in [1, n - 1] and n not in [8321, 31621, 42799, 49141, 49981] + + # bisection search on the sieve if the sieve is large enough + from sympy.ntheory.generate import sieve as s + if n <= s._list[-1]: + l, u = s.search(n) + return l == u + from sympy.ntheory.factor_ import factor_cache + if (ret := factor_cache.get(n)) is not None: + return ret == n + + # If we have GMPY2, skip straight to step 3 and do a strong BPSW test. + # This should be a bit faster than our step 2, and for large values will + # be a lot faster than our step 3 (C+GMP vs. Python). + if _gmpy is not None: + return is_strong_bpsw_prp(n) + + + # Step 2: deterministic Miller-Rabin testing for numbers < 2^64. See: + # https://miller-rabin.appspot.com/ + # for lists. We have made sure the M-R routine will successfully handle + # bases larger than n, so we can use the minimal set. + # In September 2015 deterministic numbers were extended to over 2^81. + # https://arxiv.org/pdf/1509.00864.pdf + # https://oeis.org/A014233 + if n < 341531: + return mr(n, [9345883071009581737]) + if n < 4296595241: + # Michal Forisek and Jakub Jancina, + # Fast Primality Testing for Integers That Fit into a Machine Word + # https://ceur-ws.org/Vol-1326/020-Forisek.pdf + h = ((n >> 16) ^ n) * 0x45d9f3b + h = ((h >> 16) ^ h) * 0x45d9f3b + h = ((h >> 16) ^ h) & 255 + return mr(n, [_MR_BASES_32[h]]) + if n < 350269456337: + return mr(n, [4230279247111683200, 14694767155120705706, 16641139526367750375]) + if n < 55245642489451: + return mr(n, [2, 141889084524735, 1199124725622454117, 11096072698276303650]) + if n < 7999252175582851: + return mr(n, [2, 4130806001517, 149795463772692060, 186635894390467037, 3967304179347715805]) + if n < 585226005592931977: + return mr(n, [2, 123635709730000, 9233062284813009, 43835965440333360, 761179012939631437, 1263739024124850375]) + if n < 18446744073709551616: + return mr(n, [2, 325, 9375, 28178, 450775, 9780504, 1795265022]) + if n < 318665857834031151167461: + return mr(n, [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]) + if n < 3317044064679887385961981: + return mr(n, [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41]) + + # We could do this instead at any point: + #if n < 18446744073709551616: + # return mr(n, [2]) and is_extra_strong_lucas_prp(n) + + # Here are tests that are safe for MR routines that don't understand + # large bases. + #if n < 9080191: + # return mr(n, [31, 73]) + #if n < 19471033: + # return mr(n, [2, 299417]) + #if n < 38010307: + # return mr(n, [2, 9332593]) + #if n < 316349281: + # return mr(n, [11000544, 31481107]) + #if n < 4759123141: + # return mr(n, [2, 7, 61]) + #if n < 105936894253: + # return mr(n, [2, 1005905886, 1340600841]) + #if n < 31858317218647: + # return mr(n, [2, 642735, 553174392, 3046413974]) + #if n < 3071837692357849: + # return mr(n, [2, 75088, 642735, 203659041, 3613982119]) + #if n < 18446744073709551616: + # return mr(n, [2, 325, 9375, 28178, 450775, 9780504, 1795265022]) + + # Step 3: BPSW. + # + # Time for isprime(10**2000 + 4561), no gmpy or gmpy2 installed + # 44.0s old isprime using 46 bases + # 5.3s strong BPSW + one random base + # 4.3s extra strong BPSW + one random base + # 4.1s strong BPSW + # 3.2s extra strong BPSW + + # Classic BPSW from page 1401 of the paper. See alternate ideas below. + return is_strong_bpsw_prp(n) + + # Using extra strong test, which is somewhat faster + #return mr(n, [2]) and is_extra_strong_lucas_prp(n) + + # Add a random M-R base + #import random + #return mr(n, [2, random.randint(3, n-1)]) and is_strong_lucas_prp(n) + + +def is_gaussian_prime(num): + r"""Test if num is a Gaussian prime number. + + References + ========== + + .. [1] https://oeis.org/wiki/Gaussian_primes + """ + + num = sympify(num) + a, b = num.as_real_imag() + a = as_int(a, strict=False) + b = as_int(b, strict=False) + if a == 0: + b = abs(b) + return isprime(b) and b % 4 == 3 + elif b == 0: + a = abs(a) + return isprime(a) and a % 4 == 3 + return isprime(a**2 + b**2) diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/qs.py b/venv/lib/python3.11/site-packages/sympy/ntheory/qs.py new file mode 100644 index 0000000000000000000000000000000000000000..acc9a7b6e0151695538a99a738ef397166497ba5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/qs.py @@ -0,0 +1,451 @@ +from math import exp, log +from sympy.core.random import _randint +from sympy.external.gmpy import bit_scan1, gcd, invert, sqrt as isqrt +from sympy.ntheory.factor_ import _perfect_power +from sympy.ntheory.primetest import isprime +from sympy.ntheory.residue_ntheory import _sqrt_mod_prime_power + + +class SievePolynomial: + def __init__(self, a, b, N): + """This class denotes the sieve polynomial. + Provide methods to compute `(a*x + b)**2 - N` and + `a*x + b` when given `x`. + + Parameters + ========== + + a : parameter of the sieve polynomial + b : parameter of the sieve polynomial + N : number to be factored + + """ + self.a = a + self.b = b + self.a2 = a**2 + self.ab = 2*a*b + self.b2 = b**2 - N + + def eval_u(self, x): + return self.a*x + self.b + + def eval_v(self, x): + return (self.a2*x + self.ab)*x + self.b2 + + +class FactorBaseElem: + """This class stores an element of the `factor_base`. + """ + def __init__(self, prime, tmem_p, log_p): + """ + Initialization of factor_base_elem. + + Parameters + ========== + + prime : prime number of the factor_base + tmem_p : Integer square root of x**2 = n mod prime + log_p : Compute Natural Logarithm of the prime + """ + self.prime = prime + self.tmem_p = tmem_p + self.log_p = log_p + # `soln1` and `soln2` are solutions to + # the equation `(a*x + b)**2 - N = 0 (mod p)`. + self.soln1 = None + self.soln2 = None + self.b_ainv = None + + +def _generate_factor_base(prime_bound, n): + """Generate `factor_base` for Quadratic Sieve. The `factor_base` + consists of all the points whose ``legendre_symbol(n, p) == 1`` + and ``p < num_primes``. Along with the prime `factor_base` also stores + natural logarithm of prime and the residue n modulo p. + It also returns the of primes numbers in the `factor_base` which are + close to 1000 and 5000. + + Parameters + ========== + + prime_bound : upper prime bound of the factor_base + n : integer to be factored + """ + from sympy.ntheory.generate import sieve + factor_base = [] + idx_1000, idx_5000 = None, None + for prime in sieve.primerange(1, prime_bound): + if pow(n, (prime - 1) // 2, prime) == 1: + if prime > 1000 and idx_1000 is None: + idx_1000 = len(factor_base) - 1 + if prime > 5000 and idx_5000 is None: + idx_5000 = len(factor_base) - 1 + residue = _sqrt_mod_prime_power(n, prime, 1)[0] + log_p = round(log(prime)*2**10) + factor_base.append(FactorBaseElem(prime, residue, log_p)) + return idx_1000, idx_5000, factor_base + + +def _generate_polynomial(N, M, factor_base, idx_1000, idx_5000, randint): + """ Generate sieve polynomials indefinitely. + Information such as `soln1` in the `factor_base` associated with + the polynomial is modified in place. + + Parameters + ========== + + N : Number to be factored + M : sieve interval + factor_base : factor_base primes + idx_1000 : index of prime number in the factor_base near 1000 + idx_5000 : index of prime number in the factor_base near to 5000 + randint : A callable that takes two integers (a, b) and returns a random integer + n such that a <= n <= b, similar to `random.randint`. + """ + approx_val = log(2*N)/2 - log(M) + start = idx_1000 or 0 + end = idx_5000 or (len(factor_base) - 1) + while True: + # Choose `a` that is close to `sqrt(2*N) / M` + best_a, best_q, best_ratio = None, None, None + for _ in range(50): + a = 1 + q = [] + while log(a) < approx_val: + rand_p = 0 + while(rand_p == 0 or rand_p in q): + rand_p = randint(start, end) + p = factor_base[rand_p].prime + a *= p + q.append(rand_p) + ratio = exp(log(a) - approx_val) + if best_ratio is None or abs(ratio - 1) < abs(best_ratio - 1): + best_q = q + best_a = a + best_ratio = ratio + + # Set `b` using the Chinese remainder theorem + a = best_a + q = best_q + B = [] + for val in q: + q_l = factor_base[val].prime + gamma = factor_base[val].tmem_p * invert(a // q_l, q_l) % q_l + if 2*gamma > q_l: + gamma = q_l - gamma + B.append(a//q_l*gamma) + b = sum(B) + g = SievePolynomial(a, b, N) + for fb in factor_base: + if a % fb.prime == 0: + fb.soln1 = None + continue + a_inv = invert(a, fb.prime) + fb.b_ainv = [2*b_elem*a_inv % fb.prime for b_elem in B] + fb.soln1 = (a_inv*(fb.tmem_p - b)) % fb.prime + fb.soln2 = (a_inv*(-fb.tmem_p - b)) % fb.prime + yield g + + # Update `b` with Gray code + for i in range(1, 2**(len(B)-1)): + v = bit_scan1(i) + neg_pow = 2*((i >> (v + 1)) % 2) - 1 + b = g.b + 2*neg_pow*B[v] + a = g.a + g = SievePolynomial(a, b, N) + for fb in factor_base: + if fb.soln1 is None: + continue + fb.soln1 = (fb.soln1 - neg_pow*fb.b_ainv[v]) % fb.prime + fb.soln2 = (fb.soln2 - neg_pow*fb.b_ainv[v]) % fb.prime + yield g + + +def _gen_sieve_array(M, factor_base): + """Sieve Stage of the Quadratic Sieve. For every prime in the factor_base + that does not divide the coefficient `a` we add log_p over the sieve_array + such that ``-M <= soln1 + i*p <= M`` and ``-M <= soln2 + i*p <= M`` where `i` + is an integer. When p = 2 then log_p is only added using + ``-M <= soln1 + i*p <= M``. + + Parameters + ========== + + M : sieve interval + factor_base : factor_base primes + """ + sieve_array = [0]*(2*M + 1) + for factor in factor_base: + if factor.soln1 is None: #The prime does not divides a + continue + for idx in range((M + factor.soln1) % factor.prime, 2*M, factor.prime): + sieve_array[idx] += factor.log_p + if factor.prime == 2: + continue + #if prime is 2 then sieve only with soln_1_p + for idx in range((M + factor.soln2) % factor.prime, 2*M, factor.prime): + sieve_array[idx] += factor.log_p + return sieve_array + + +def _check_smoothness(num, factor_base): + r""" Check if `num` is smooth with respect to the given `factor_base` + and compute its factorization vector. + + Parameters + ========== + + num : integer whose smootheness is to be checked + factor_base : factor_base primes + """ + if num < 0: + num *= -1 + vec = 1 + else: + vec = 0 + for i, fb in enumerate(factor_base, 1): + if num % fb.prime: + continue + e = 1 + num //= fb.prime + while num % fb.prime == 0: + e += 1 + num //= fb.prime + if e % 2: + vec += 1 << i + return vec, num + + +def _trial_division_stage(N, M, factor_base, sieve_array, sieve_poly, partial_relations, ERROR_TERM): + """Trial division stage. Here we trial divide the values generetated + by sieve_poly in the sieve interval and if it is a smooth number then + it is stored in `smooth_relations`. Moreover, if we find two partial relations + with same large prime then they are combined to form a smooth relation. + First we iterate over sieve array and look for values which are greater + than accumulated_val, as these values have a high chance of being smooth + number. Then using these values we find smooth relations. + In general, let ``t**2 = u*p modN`` and ``r**2 = v*p modN`` be two partial relations + with the same large prime p. Then they can be combined ``(t*r/p)**2 = u*v modN`` + to form a smooth relation. + + Parameters + ========== + + N : Number to be factored + M : sieve interval + factor_base : factor_base primes + sieve_array : stores log_p values + sieve_poly : polynomial from which we find smooth relations + partial_relations : stores partial relations with one large prime + ERROR_TERM : error term for accumulated_val + """ + accumulated_val = (log(M) + log(N)/2 - ERROR_TERM) * 2**10 + smooth_relations = [] + proper_factor = set() + partial_relation_upper_bound = 128*factor_base[-1].prime + for x, val in enumerate(sieve_array, -M): + if val < accumulated_val: + continue + v = sieve_poly.eval_v(x) + vec, num = _check_smoothness(v, factor_base) + if num == 1: + smooth_relations.append((sieve_poly.eval_u(x), v, vec)) + elif num < partial_relation_upper_bound and isprime(num): + if N % num == 0: + proper_factor.add(num) + continue + u = sieve_poly.eval_u(x) + if num in partial_relations: + u_prev, v_prev, vec_prev = partial_relations.pop(num) + u = u*u_prev*invert(num, N) % N + v = v*v_prev // num**2 + vec ^= vec_prev + smooth_relations.append((u, v, vec)) + else: + partial_relations[num] = (u, v, vec) + return smooth_relations, proper_factor + + +def _find_factor(N, smooth_relations, col): + """ Finds proper factor of N using fast gaussian reduction for modulo 2 matrix. + + Parameters + ========== + + N : Number to be factored + smooth_relations : Smooth relations vectors matrix + col : Number of columns in the matrix + + Reference + ========== + + .. [1] A fast algorithm for gaussian elimination over GF(2) and + its implementation on the GAPP. Cetin K.Koc, Sarath N.Arachchige + """ + matrix = [s_relation[2] for s_relation in smooth_relations] + row = len(matrix) + mark = [False] * row + for pos in range(col): + m = 1 << pos + for i in range(row): + if p := matrix[i] & m: + add_col = p ^ matrix[i] + matrix[i] = m + mark[i] = True + for j in range(i + 1, row): + if matrix[j] & m: + matrix[j] ^= add_col + break + + for m, mat, rel in zip(mark, matrix, smooth_relations): + if m: + continue + u, v = rel[0], rel[1] + for m1, mat1, rel1 in zip(mark, matrix, smooth_relations): + if m1 and mat & mat1: + u *= rel1[0] + v *= rel1[1] + # assert is_square(v) + v = isqrt(v) + if 1 < (g := gcd(u - v, N)) < N: + yield g + + +def qs(N, prime_bound, M, ERROR_TERM=25, seed=1234): + """Performs factorization using Self-Initializing Quadratic Sieve. + In SIQS, let N be a number to be factored, and this N should not be a + perfect power. If we find two integers such that ``X**2 = Y**2 modN`` and + ``X != +-Y modN``, then `gcd(X + Y, N)` will reveal a proper factor of N. + In order to find these integers X and Y we try to find relations of form + t**2 = u modN where u is a product of small primes. If we have enough of + these relations then we can form ``(t1*t2...ti)**2 = u1*u2...ui modN`` such that + the right hand side is a square, thus we found a relation of ``X**2 = Y**2 modN``. + + Here, several optimizations are done like using multiple polynomials for + sieving, fast changing between polynomials and using partial relations. + The use of partial relations can speeds up the factoring by 2 times. + + Parameters + ========== + + N : Number to be Factored + prime_bound : upper bound for primes in the factor base + M : Sieve Interval + ERROR_TERM : Error term for checking smoothness + seed : seed of random number generator + + Returns + ======= + + set(int) : A set of factors of N without considering multiplicity. + Returns ``{N}`` if factorization fails. + + Examples + ======== + + >>> from sympy.ntheory import qs + >>> qs(25645121643901801, 2000, 10000) + {5394769, 4753701529} + >>> qs(9804659461513846513, 2000, 10000) + {4641991, 2112166839943} + + See Also + ======== + + qs_factor + + References + ========== + + .. [1] https://pdfs.semanticscholar.org/5c52/8a975c1405bd35c65993abf5a4edb667c1db.pdf + .. [2] https://www.rieselprime.de/ziki/Self-initializing_quadratic_sieve + """ + return set(qs_factor(N, prime_bound, M, ERROR_TERM, seed)) + + +def qs_factor(N, prime_bound, M, ERROR_TERM=25, seed=1234): + """ Performs factorization using Self-Initializing Quadratic Sieve. + + Parameters + ========== + + N : Number to be Factored + prime_bound : upper bound for primes in the factor base + M : Sieve Interval + ERROR_TERM : Error term for checking smoothness + seed : seed of random number generator + + Returns + ======= + + dict[int, int] : Factors of N. + Returns ``{N: 1}`` if factorization fails. + Note that the key is not always a prime number. + + Examples + ======== + + >>> from sympy.ntheory import qs_factor + >>> qs_factor(1009 * 100003, 2000, 10000) + {1009: 1, 100003: 1} + + See Also + ======== + + qs + + """ + if N < 2: + raise ValueError("N should be greater than 1") + factors = {} + smooth_relations = [] + partial_relations = {} + # Eliminate the possibility of even numbers, + # prime numbers, and perfect powers. + if N % 2 == 0: + e = 1 + N //= 2 + while N % 2 == 0: + N //= 2 + e += 1 + factors[2] = e + if isprime(N): + factors[N] = 1 + return factors + if result := _perfect_power(N, 3): + n, e = result + factors[n] = e + return factors + N_copy = N + randint = _randint(seed) + idx_1000, idx_5000, factor_base = _generate_factor_base(prime_bound, N) + threshold = len(factor_base) * 105//100 + for g in _generate_polynomial(N, M, factor_base, idx_1000, idx_5000, randint): + sieve_array = _gen_sieve_array(M, factor_base) + s_rel, p_f = _trial_division_stage(N, M, factor_base, sieve_array, g, partial_relations, ERROR_TERM) + smooth_relations += s_rel + for p in p_f: + if N_copy % p: + continue + e = 1 + N_copy //= p + while N_copy % p == 0: + N_copy //= p + e += 1 + factors[p] = e + if threshold <= len(smooth_relations): + break + + for factor in _find_factor(N, smooth_relations, len(factor_base) + 1): + if N_copy % factor == 0: + e = 1 + N_copy //= factor + while N_copy % factor == 0: + N_copy //= factor + e += 1 + factors[factor] = e + if N_copy == 1 or isprime(N_copy): + break + if N_copy != 1: + factors[N_copy] = 1 + return factors diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/residue_ntheory.py b/venv/lib/python3.11/site-packages/sympy/ntheory/residue_ntheory.py new file mode 100644 index 0000000000000000000000000000000000000000..eba024161194605aabebd10ee30bf09acb90270b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/residue_ntheory.py @@ -0,0 +1,1963 @@ +from __future__ import annotations + +from sympy.external.gmpy import (gcd, lcm, invert, sqrt, jacobi, + bit_scan1, remove) +from sympy.polys import Poly +from sympy.polys.domains import ZZ +from sympy.polys.galoistools import gf_crt1, gf_crt2, linear_congruence, gf_csolve +from .primetest import isprime +from .generate import primerange +from .factor_ import factorint, _perfect_power +from .modular import crt +from sympy.utilities.decorator import deprecated +from sympy.utilities.memoization import recurrence_memo +from sympy.utilities.misc import as_int +from sympy.utilities.iterables import iproduct +from sympy.core.random import _randint, randint + +from itertools import product + + +def n_order(a, n): + r""" Returns the order of ``a`` modulo ``n``. + + Explanation + =========== + + The order of ``a`` modulo ``n`` is the smallest integer + ``k`` such that `a^k` leaves a remainder of 1 with ``n``. + + Parameters + ========== + + a : integer + n : integer, n > 1. a and n should be relatively prime + + Returns + ======= + + int : the order of ``a`` modulo ``n`` + + Raises + ====== + + ValueError + If `n \le 1` or `\gcd(a, n) \neq 1`. + If ``a`` or ``n`` is not an integer. + + Examples + ======== + + >>> from sympy.ntheory import n_order + >>> n_order(3, 7) + 6 + >>> n_order(4, 7) + 3 + + See Also + ======== + + is_primitive_root + We say that ``a`` is a primitive root of ``n`` + when the order of ``a`` modulo ``n`` equals ``totient(n)`` + + """ + a, n = as_int(a), as_int(n) + if n <= 1: + raise ValueError("n should be an integer greater than 1") + a = a % n + # Trivial + if a == 1: + return 1 + if gcd(a, n) != 1: + raise ValueError("The two numbers should be relatively prime") + a_order = 1 + for p, e in factorint(n).items(): + pe = p**e + pe_order = (p - 1) * p**(e - 1) + factors = factorint(p - 1) + if e > 1: + factors[p] = e - 1 + order = 1 + for px, ex in factors.items(): + x = pow(a, pe_order // px**ex, pe) + while x != 1: + x = pow(x, px, pe) + order *= px + a_order = lcm(a_order, order) + return int(a_order) + + +def _primitive_root_prime_iter(p): + r""" Generates the primitive roots for a prime ``p``. + + Explanation + =========== + + The primitive roots generated are not necessarily sorted. + However, the first one is the smallest primitive root. + + Find the element whose order is ``p-1`` from the smaller one. + If we can find the first primitive root ``g``, we can use the following theorem. + + .. math :: + \operatorname{ord}(g^k) = \frac{\operatorname{ord}(g)}{\gcd(\operatorname{ord}(g), k)} + + From the assumption that `\operatorname{ord}(g)=p-1`, + it is a necessary and sufficient condition for + `\operatorname{ord}(g^k)=p-1` that `\gcd(p-1, k)=1`. + + Parameters + ========== + + p : odd prime + + Yields + ====== + + int + the primitive roots of ``p`` + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import _primitive_root_prime_iter + >>> sorted(_primitive_root_prime_iter(19)) + [2, 3, 10, 13, 14, 15] + + References + ========== + + .. [1] W. Stein "Elementary Number Theory" (2011), page 44 + + """ + if p == 3: + yield 2 + return + # Let p = +-1 (mod 4a). Legendre symbol (a/p) = 1, so `a` is not the primitive root. + # Corollary : If p = +-1 (mod 8), then 2 is not the primitive root of p. + g_min = 3 if p % 8 in [1, 7] else 2 + if p < 41: + # small case + g = 5 if p == 23 else g_min + else: + v = [(p - 1) // i for i in factorint(p - 1).keys()] + for g in range(g_min, p): + if all(pow(g, pw, p) != 1 for pw in v): + break + yield g + # g**k is the primitive root of p iff gcd(p - 1, k) = 1 + for k in range(3, p, 2): + if gcd(p - 1, k) == 1: + yield pow(g, k, p) + + +def _primitive_root_prime_power_iter(p, e): + r""" Generates the primitive roots of `p^e`. + + Explanation + =========== + + Let ``g`` be the primitive root of ``p``. + If `g^{p-1} \not\equiv 1 \pmod{p^2}`, then ``g`` is primitive root of `p^e`. + Thus, if we find a primitive root ``g`` of ``p``, + then `g, g+p, g+2p, \ldots, g+(p-1)p` are primitive roots of `p^2` except one. + That one satisfies `\hat{g}^{p-1} \equiv 1 \pmod{p^2}`. + If ``h`` is the primitive root of `p^2`, + then `h, h+p^2, h+2p^2, \ldots, h+(p^{e-2}-1)p^e` are primitive roots of `p^e`. + + Parameters + ========== + + p : odd prime + e : positive integer + + Yields + ====== + + int + the primitive roots of `p^e` + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import _primitive_root_prime_power_iter + >>> sorted(_primitive_root_prime_power_iter(5, 2)) + [2, 3, 8, 12, 13, 17, 22, 23] + + """ + if e == 1: + yield from _primitive_root_prime_iter(p) + else: + p2 = p**2 + for g in _primitive_root_prime_iter(p): + t = (g - pow(g, 2 - p, p2)) % p2 + for k in range(0, p2, p): + if k != t: + yield from (g + k + m for m in range(0, p**e, p2)) + + +def _primitive_root_prime_power2_iter(p, e): + r""" Generates the primitive roots of `2p^e`. + + Explanation + =========== + + If ``g`` is the primitive root of ``p**e``, + then the odd one of ``g`` and ``g+p**e`` is the primitive root of ``2*p**e``. + + Parameters + ========== + + p : odd prime + e : positive integer + + Yields + ====== + + int + the primitive roots of `2p^e` + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import _primitive_root_prime_power2_iter + >>> sorted(_primitive_root_prime_power2_iter(5, 2)) + [3, 13, 17, 23, 27, 33, 37, 47] + + """ + for g in _primitive_root_prime_power_iter(p, e): + if g % 2 == 1: + yield g + else: + yield g + p**e + + +def primitive_root(p, smallest=True): + r""" Returns a primitive root of ``p`` or None. + + Explanation + =========== + + For the definition of primitive root, + see the explanation of ``is_primitive_root``. + + The primitive root of ``p`` exist only for + `p = 2, 4, q^e, 2q^e` (``q`` is an odd prime). + Now, if we know the primitive root of ``q``, + we can calculate the primitive root of `q^e`, + and if we know the primitive root of `q^e`, + we can calculate the primitive root of `2q^e`. + When there is no need to find the smallest primitive root, + this property can be used to obtain a fast primitive root. + On the other hand, when we want the smallest primitive root, + we naively determine whether it is a primitive root or not. + + Parameters + ========== + + p : integer, p > 1 + smallest : if True the smallest primitive root is returned or None + + Returns + ======= + + int | None : + If the primitive root exists, return the primitive root of ``p``. + If not, return None. + + Raises + ====== + + ValueError + If `p \le 1` or ``p`` is not an integer. + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import primitive_root + >>> primitive_root(19) + 2 + >>> primitive_root(21) is None + True + >>> primitive_root(50, smallest=False) + 27 + + See Also + ======== + + is_primitive_root + + References + ========== + + .. [1] W. Stein "Elementary Number Theory" (2011), page 44 + .. [2] P. Hackman "Elementary Number Theory" (2009), Chapter C + + """ + p = as_int(p) + if p <= 1: + raise ValueError("p should be an integer greater than 1") + if p <= 4: + return p - 1 + p_even = p % 2 == 0 + if not p_even: + q = p # p is odd + elif p % 4: + q = p//2 # p had 1 factor of 2 + else: + return None # p had more than one factor of 2 + if isprime(q): + e = 1 + else: + m = _perfect_power(q, 3) + if not m: + return None + q, e = m + if not isprime(q): + return None + if not smallest: + if p_even: + return next(_primitive_root_prime_power2_iter(q, e)) + return next(_primitive_root_prime_power_iter(q, e)) + if p_even: + for i in range(3, p, 2): + if i % q and is_primitive_root(i, p): + return i + g = next(_primitive_root_prime_iter(q)) + if e == 1 or pow(g, q - 1, q**2) != 1: + return g + for i in range(g + 1, p): + if i % q and is_primitive_root(i, p): + return i + + +def is_primitive_root(a, p): + r""" Returns True if ``a`` is a primitive root of ``p``. + + Explanation + =========== + + ``a`` is said to be the primitive root of ``p`` if `\gcd(a, p) = 1` and + `\phi(p)` is the smallest positive number s.t. + + `a^{\phi(p)} \equiv 1 \pmod{p}`. + + where `\phi(p)` is Euler's totient function. + + The primitive root of ``p`` exist only for + `p = 2, 4, q^e, 2q^e` (``q`` is an odd prime). + Hence, if it is not such a ``p``, it returns False. + To determine the primitive root, we need to know + the prime factorization of ``q-1``. + The hardness of the determination depends on this complexity. + + Parameters + ========== + + a : integer + p : integer, ``p`` > 1. ``a`` and ``p`` should be relatively prime + + Returns + ======= + + bool : If True, ``a`` is the primitive root of ``p``. + + Raises + ====== + + ValueError + If `p \le 1` or `\gcd(a, p) \neq 1`. + If ``a`` or ``p`` is not an integer. + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import totient + >>> from sympy.ntheory import is_primitive_root, n_order + >>> is_primitive_root(3, 10) + True + >>> is_primitive_root(9, 10) + False + >>> n_order(3, 10) == totient(10) + True + >>> n_order(9, 10) == totient(10) + False + + See Also + ======== + + primitive_root + + """ + a, p = as_int(a), as_int(p) + if p <= 1: + raise ValueError("p should be an integer greater than 1") + a = a % p + if gcd(a, p) != 1: + raise ValueError("The two numbers should be relatively prime") + # Primitive root of p exist only for + # p = 2, 4, q**e, 2*q**e (q is odd prime) + if p <= 4: + # The primitive root is only p-1. + return a == p - 1 + if p % 2: + q = p # p is odd + elif p % 4: + q = p//2 # p had 1 factor of 2 + else: + return False # p had more than one factor of 2 + if isprime(q): + group_order = q - 1 + factors = factorint(q - 1).keys() + else: + m = _perfect_power(q, 3) + if not m: + return False + q, e = m + if not isprime(q): + return False + group_order = q**(e - 1)*(q - 1) + factors = set(factorint(q - 1).keys()) + factors.add(q) + return all(pow(a, group_order // prime, p) != 1 for prime in factors) + + +def _sqrt_mod_tonelli_shanks(a, p): + """ + Returns the square root in the case of ``p`` prime with ``p == 1 (mod 8)`` + + Assume that the root exists. + + Parameters + ========== + + a : int + p : int + prime number. should be ``p % 8 == 1`` + + Returns + ======= + + int : Generally, there are two roots, but only one is returned. + Which one is returned is random. + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import _sqrt_mod_tonelli_shanks + >>> _sqrt_mod_tonelli_shanks(2, 17) in [6, 11] + True + + References + ========== + + .. [1] Carl Pomerance, Richard Crandall, Prime Numbers: A Computational Perspective, + 2nd Edition (2005), page 101, ISBN:978-0387252827 + + """ + s = bit_scan1(p - 1) + t = p >> s + # find a non-quadratic residue + if p % 12 == 5: + # Legendre symbol (3/p) == -1 if p % 12 in [5, 7] + d = 3 + elif p % 5 in [2, 3]: + # Legendre symbol (5/p) == -1 if p % 5 in [2, 3] + d = 5 + else: + while 1: + d = randint(6, p - 1) + if jacobi(d, p) == -1: + break + #assert legendre_symbol(d, p) == -1 + A = pow(a, t, p) + D = pow(d, t, p) + m = 0 + for i in range(s): + adm = A*pow(D, m, p) % p + adm = pow(adm, 2**(s - 1 - i), p) + if adm % p == p - 1: + m += 2**i + #assert A*pow(D, m, p) % p == 1 + x = pow(a, (t + 1)//2, p)*pow(D, m//2, p) % p + return x + + +def sqrt_mod(a, p, all_roots=False): + """ + Find a root of ``x**2 = a mod p``. + + Parameters + ========== + + a : integer + p : positive integer + all_roots : if True the list of roots is returned or None + + Notes + ===== + + If there is no root it is returned None; else the returned root + is less or equal to ``p // 2``; in general is not the smallest one. + It is returned ``p // 2`` only if it is the only root. + + Use ``all_roots`` only when it is expected that all the roots fit + in memory; otherwise use ``sqrt_mod_iter``. + + Examples + ======== + + >>> from sympy.ntheory import sqrt_mod + >>> sqrt_mod(11, 43) + 21 + >>> sqrt_mod(17, 32, True) + [7, 9, 23, 25] + """ + if all_roots: + return sorted(sqrt_mod_iter(a, p)) + p = abs(as_int(p)) + halfp = p // 2 + x = None + for r in sqrt_mod_iter(a, p): + if r < halfp: + return r + elif r > halfp: + return p - r + else: + x = r + return x + + +def sqrt_mod_iter(a, p, domain=int): + """ + Iterate over solutions to ``x**2 = a mod p``. + + Parameters + ========== + + a : integer + p : positive integer + domain : integer domain, ``int``, ``ZZ`` or ``Integer`` + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import sqrt_mod_iter + >>> list(sqrt_mod_iter(11, 43)) + [21, 22] + + See Also + ======== + + sqrt_mod : Same functionality, but you want a sorted list or only one solution. + + """ + a, p = as_int(a), abs(as_int(p)) + v = [] + pv = [] + _product = product + for px, ex in factorint(p).items(): + if a % px: + # `len(rx)` is at most 4 + rx = _sqrt_mod_prime_power(a, px, ex) + else: + # `len(list(rx))` can be assumed to be large. + # The `itertools.product` is disadvantageous in terms of memory usage. + # It is also inferior to iproduct in speed if not all Cartesian products are needed. + rx = _sqrt_mod1(a, px, ex) + _product = iproduct + if not rx: + return + v.append(rx) + pv.append(px**ex) + if len(v) == 1: + yield from map(domain, v[0]) + else: + mm, e, s = gf_crt1(pv, ZZ) + for vx in _product(*v): + yield domain(gf_crt2(vx, pv, mm, e, s, ZZ)) + + +def _sqrt_mod_prime_power(a, p, k): + """ + Find the solutions to ``x**2 = a mod p**k`` when ``a % p != 0``. + If no solution exists, return ``None``. + Solutions are returned in an ascending list. + + Parameters + ========== + + a : integer + p : prime number + k : positive integer + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import _sqrt_mod_prime_power + >>> _sqrt_mod_prime_power(11, 43, 1) + [21, 22] + + References + ========== + + .. [1] P. Hackman "Elementary Number Theory" (2009), page 160 + .. [2] http://www.numbertheory.org/php/squareroot.html + .. [3] [Gathen99]_ + """ + pk = p**k + a = a % pk + + if p == 2: + # see Ref.[2] + if a % 8 != 1: + return None + # Trivial + if k <= 3: + return list(range(1, pk, 2)) + r = 1 + # r is one of the solutions to x**2 - a = 0 (mod 2**3). + # Hensel lift them to solutions of x**2 - a = 0 (mod 2**k) + # if r**2 - a = 0 mod 2**nx but not mod 2**(nx+1) + # then r + 2**(nx - 1) is a root mod 2**(nx+1) + for nx in range(3, k): + if ((r**2 - a) >> nx) % 2: + r += 1 << (nx - 1) + # r is a solution of x**2 - a = 0 (mod 2**k), and + # there exist other solutions -r, r+h, -(r+h), and these are all solutions. + h = 1 << (k - 1) + return sorted([r, pk - r, (r + h) % pk, -(r + h) % pk]) + + # If the Legendre symbol (a/p) is not 1, no solution exists. + if jacobi(a, p) != 1: + return None + if p % 4 == 3: + res = pow(a, (p + 1) // 4, p) + elif p % 8 == 5: + res = pow(a, (p + 3) // 8, p) + if pow(res, 2, p) != a % p: + res = res * pow(2, (p - 1) // 4, p) % p + else: + res = _sqrt_mod_tonelli_shanks(a, p) + if k > 1: + # Hensel lifting with Newton iteration, see Ref.[3] chapter 9 + # with f(x) = x**2 - a; one has f'(a) != 0 (mod p) for p != 2 + px = p + for _ in range(k.bit_length() - 1): + px = px**2 + frinv = invert(2*res, px) + res = (res - (res**2 - a)*frinv) % px + if k & (k - 1): # If k is not a power of 2 + frinv = invert(2*res, pk) + res = (res - (res**2 - a)*frinv) % pk + return sorted([res, pk - res]) + + +def _sqrt_mod1(a, p, n): + """ + Find solution to ``x**2 == a mod p**n`` when ``a % p == 0``. + If no solution exists, return ``None``. + + Parameters + ========== + + a : integer + p : prime number, p must divide a + n : positive integer + + References + ========== + + .. [1] http://www.numbertheory.org/php/squareroot.html + """ + pn = p**n + a = a % pn + if a == 0: + # case gcd(a, p**k) = p**n + return range(0, pn, p**((n + 1) // 2)) + # case gcd(a, p**k) = p**r, r < n + a, r = remove(a, p) + if r % 2 == 1: + return None + res = _sqrt_mod_prime_power(a, p, n - r) + if res is None: + return None + m = r // 2 + return (x for rx in res for x in range(rx*p**m, pn, p**(n - m))) + + +def is_quad_residue(a, p): + """ + Returns True if ``a`` (mod ``p``) is in the set of squares mod ``p``, + i.e a % p in set([i**2 % p for i in range(p)]). + + Parameters + ========== + + a : integer + p : positive integer + + Returns + ======= + + bool : If True, ``x**2 == a (mod p)`` has solution. + + Raises + ====== + + ValueError + If ``a``, ``p`` is not integer. + If ``p`` is not positive. + + Examples + ======== + + >>> from sympy.ntheory import is_quad_residue + >>> is_quad_residue(21, 100) + True + + Indeed, ``pow(39, 2, 100)`` would be 21. + + >>> is_quad_residue(21, 120) + False + + That is, for any integer ``x``, ``pow(x, 2, 120)`` is not 21. + + If ``p`` is an odd + prime, an iterative method is used to make the determination: + + >>> from sympy.ntheory import is_quad_residue + >>> sorted(set([i**2 % 7 for i in range(7)])) + [0, 1, 2, 4] + >>> [j for j in range(7) if is_quad_residue(j, 7)] + [0, 1, 2, 4] + + See Also + ======== + + legendre_symbol, jacobi_symbol, sqrt_mod + """ + a, p = as_int(a), as_int(p) + if p < 1: + raise ValueError('p must be > 0') + a %= p + if a < 2 or p < 3: + return True + # Since we want to compute the Jacobi symbol, + # we separate p into the odd part and the rest. + t = bit_scan1(p) + if t: + # The existence of a solution to a power of 2 is determined + # using the logic of `p==2` in `_sqrt_mod_prime_power` and `_sqrt_mod1`. + a_ = a % (1 << t) + if a_: + r = bit_scan1(a_) + if r % 2 or (a_ >> r) & 6: + return False + p >>= t + a %= p + if a < 2 or p < 3: + return True + # If Jacobi symbol is -1 or p is prime, can be determined by Jacobi symbol only + j = jacobi(a, p) + if j == -1 or isprime(p): + return j == 1 + # Checks if `x**2 = a (mod p)` has a solution + for px, ex in factorint(p).items(): + if a % px: + if jacobi(a, px) != 1: + return False + else: + a_ = a % px**ex + if a_ == 0: + continue + a_, r = remove(a_, px) + if r % 2 or jacobi(a_, px) != 1: + return False + return True + + +def is_nthpow_residue(a, n, m): + """ + Returns True if ``x**n == a (mod m)`` has solutions. + + References + ========== + + .. [1] P. Hackman "Elementary Number Theory" (2009), page 76 + + """ + a = a % m + a, n, m = as_int(a), as_int(n), as_int(m) + if m <= 0: + raise ValueError('m must be > 0') + if n < 0: + raise ValueError('n must be >= 0') + if n == 0: + if m == 1: + return False + return a == 1 + if a == 0: + return True + if n == 1: + return True + if n == 2: + return is_quad_residue(a, m) + return all(_is_nthpow_residue_bign_prime_power(a, n, p, e) + for p, e in factorint(m).items()) + + +def _is_nthpow_residue_bign_prime_power(a, n, p, k): + r""" + Returns True if `x^n = a \pmod{p^k}` has solutions for `n > 2`. + + Parameters + ========== + + a : positive integer + n : integer, n > 2 + p : prime number + k : positive integer + + """ + while a % p == 0: + a %= pow(p, k) + if not a: + return True + a, mu = remove(a, p) + if mu % n: + return False + k -= mu + if p != 2: + f = p**(k - 1)*(p - 1) # f = totient(p**k) + return pow(a, f // gcd(f, n), pow(p, k)) == 1 + if n & 1: + return True + c = min(bit_scan1(n) + 2, k) + return a % pow(2, c) == 1 + + +def _nthroot_mod1(s, q, p, all_roots): + """ + Root of ``x**q = s mod p``, ``p`` prime and ``q`` divides ``p - 1``. + Assume that the root exists. + + Parameters + ========== + + s : integer + q : integer, n > 2. ``q`` divides ``p - 1``. + p : prime number + all_roots : if False returns the smallest root, else the list of roots + + Returns + ======= + + list[int] | int : + Root of ``x**q = s mod p``. If ``all_roots == True``, + returned ascending list. otherwise, returned an int. + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import _nthroot_mod1 + >>> _nthroot_mod1(5, 3, 13, False) + 7 + >>> _nthroot_mod1(13, 4, 17, True) + [3, 5, 12, 14] + + References + ========== + + .. [1] A. M. Johnston, A Generalized qth Root Algorithm, + ACM-SIAM Symposium on Discrete Algorithms (1999), pp. 929-930 + + """ + g = next(_primitive_root_prime_iter(p)) + r = s + for qx, ex in factorint(q).items(): + f = (p - 1) // qx**ex + while f % qx == 0: + f //= qx + z = f*invert(-f, qx) + x = (1 + z) // qx + t = discrete_log(p, pow(r, f, p), pow(g, f*qx, p)) + for _ in range(ex): + # assert t == discrete_log(p, pow(r, f, p), pow(g, f*qx, p)) + r = pow(r, x, p)*pow(g, -z*t % (p - 1), p) % p + t //= qx + res = [r] + h = pow(g, (p - 1) // q, p) + #assert pow(h, q, p) == 1 + hx = r + for _ in range(q - 1): + hx = (hx*h) % p + res.append(hx) + if all_roots: + res.sort() + return res + return min(res) + + +def _nthroot_mod_prime_power(a, n, p, k): + """ Root of ``x**n = a mod p**k``. + + Parameters + ========== + + a : integer + n : integer, n > 2 + p : prime number + k : positive integer + + Returns + ======= + + list[int] : + Ascending list of roots of ``x**n = a mod p**k``. + If no solution exists, return ``[]``. + + """ + if not _is_nthpow_residue_bign_prime_power(a, n, p, k): + return [] + a_mod_p = a % p + if a_mod_p == 0: + base_roots = [0] + elif (p - 1) % n == 0: + base_roots = _nthroot_mod1(a_mod_p, n, p, all_roots=True) + else: + # The roots of ``x**n - a = 0 (mod p)`` are roots of + # ``gcd(x**n - a, x**(p - 1) - 1) = 0 (mod p)`` + pa = n + pb = p - 1 + b = 1 + if pa < pb: + a_mod_p, pa, b, pb = b, pb, a_mod_p, pa + # gcd(x**pa - a, x**pb - b) = gcd(x**pb - b, x**pc - c) + # where pc = pa % pb; c = b**-q * a mod p + while pb: + q, pc = divmod(pa, pb) + c = pow(b, -q, p) * a_mod_p % p + pa, pb = pb, pc + a_mod_p, b = b, c + if pa == 1: + base_roots = [a_mod_p] + elif pa == 2: + base_roots = sqrt_mod(a_mod_p, p, all_roots=True) + else: + base_roots = _nthroot_mod1(a_mod_p, pa, p, all_roots=True) + if k == 1: + return base_roots + a %= p**k + tot_roots = set() + for root in base_roots: + diff = pow(root, n - 1, p)*n % p + new_base = p + if diff != 0: + m_inv = invert(diff, p) + for _ in range(k - 1): + new_base *= p + tmp = pow(root, n, new_base) - a + tmp *= m_inv + root = (root - tmp) % new_base + tot_roots.add(root) + else: + roots_in_base = {root} + for _ in range(k - 1): + new_base *= p + new_roots = set() + for k_ in roots_in_base: + if pow(k_, n, new_base) != a % new_base: + continue + while k_ not in new_roots: + new_roots.add(k_) + k_ = (k_ + (new_base // p)) % new_base + roots_in_base = new_roots + tot_roots = tot_roots | roots_in_base + return sorted(tot_roots) + + +def nthroot_mod(a, n, p, all_roots=False): + """ + Find the solutions to ``x**n = a mod p``. + + Parameters + ========== + + a : integer + n : positive integer + p : positive integer + all_roots : if False returns the smallest root, else the list of roots + + Returns + ======= + + list[int] | int | None : + solutions to ``x**n = a mod p``. + The table of the output type is: + + ========== ========== ========== + all_roots has roots Returns + ========== ========== ========== + True Yes list[int] + True No [] + False Yes int + False No None + ========== ========== ========== + + Raises + ====== + + ValueError + If ``a``, ``n`` or ``p`` is not integer. + If ``n`` or ``p`` is not positive. + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import nthroot_mod + >>> nthroot_mod(11, 4, 19) + 8 + >>> nthroot_mod(11, 4, 19, True) + [8, 11] + >>> nthroot_mod(68, 3, 109) + 23 + + References + ========== + + .. [1] P. Hackman "Elementary Number Theory" (2009), page 76 + + """ + a = a % p + a, n, p = as_int(a), as_int(n), as_int(p) + + if n < 1: + raise ValueError("n should be positive") + if p < 1: + raise ValueError("p should be positive") + if n == 1: + return [a] if all_roots else a + if n == 2: + return sqrt_mod(a, p, all_roots) + base = [] + prime_power = [] + for q, e in factorint(p).items(): + tot_roots = _nthroot_mod_prime_power(a, n, q, e) + if not tot_roots: + return [] if all_roots else None + prime_power.append(q**e) + base.append(sorted(tot_roots)) + P, E, S = gf_crt1(prime_power, ZZ) + ret = sorted(map(int, {gf_crt2(c, prime_power, P, E, S, ZZ) + for c in product(*base)})) + if all_roots: + return ret + if ret: + return ret[0] + + +def quadratic_residues(p) -> list[int]: + """ + Returns the list of quadratic residues. + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import quadratic_residues + >>> quadratic_residues(7) + [0, 1, 2, 4] + """ + p = as_int(p) + r = {pow(i, 2, p) for i in range(p // 2 + 1)} + return sorted(r) + + +@deprecated("""\ +The `sympy.ntheory.residue_ntheory.legendre_symbol` has been moved to `sympy.functions.combinatorial.numbers.legendre_symbol`.""", +deprecated_since_version="1.13", +active_deprecations_target='deprecated-ntheory-symbolic-functions') +def legendre_symbol(a, p): + r""" + Returns the Legendre symbol `(a / p)`. + + .. deprecated:: 1.13 + + The ``legendre_symbol`` function is deprecated. Use :class:`sympy.functions.combinatorial.numbers.legendre_symbol` + instead. See its documentation for more information. See + :ref:`deprecated-ntheory-symbolic-functions` for details. + + For an integer ``a`` and an odd prime ``p``, the Legendre symbol is + defined as + + .. math :: + \genfrac(){}{}{a}{p} = \begin{cases} + 0 & \text{if } p \text{ divides } a\\ + 1 & \text{if } a \text{ is a quadratic residue modulo } p\\ + -1 & \text{if } a \text{ is a quadratic nonresidue modulo } p + \end{cases} + + Parameters + ========== + + a : integer + p : odd prime + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import legendre_symbol + >>> [legendre_symbol(i, 7) for i in range(7)] + [0, 1, 1, -1, 1, -1, -1] + >>> sorted(set([i**2 % 7 for i in range(7)])) + [0, 1, 2, 4] + + See Also + ======== + + is_quad_residue, jacobi_symbol + + """ + from sympy.functions.combinatorial.numbers import legendre_symbol as _legendre_symbol + return _legendre_symbol(a, p) + + +@deprecated("""\ +The `sympy.ntheory.residue_ntheory.jacobi_symbol` has been moved to `sympy.functions.combinatorial.numbers.jacobi_symbol`.""", +deprecated_since_version="1.13", +active_deprecations_target='deprecated-ntheory-symbolic-functions') +def jacobi_symbol(m, n): + r""" + Returns the Jacobi symbol `(m / n)`. + + .. deprecated:: 1.13 + + The ``jacobi_symbol`` function is deprecated. Use :class:`sympy.functions.combinatorial.numbers.jacobi_symbol` + instead. See its documentation for more information. See + :ref:`deprecated-ntheory-symbolic-functions` for details. + + For any integer ``m`` and any positive odd integer ``n`` the Jacobi symbol + is defined as the product of the Legendre symbols corresponding to the + prime factors of ``n``: + + .. math :: + \genfrac(){}{}{m}{n} = + \genfrac(){}{}{m}{p^{1}}^{\alpha_1} + \genfrac(){}{}{m}{p^{2}}^{\alpha_2} + ... + \genfrac(){}{}{m}{p^{k}}^{\alpha_k} + \text{ where } n = + p_1^{\alpha_1} + p_2^{\alpha_2} + ... + p_k^{\alpha_k} + + Like the Legendre symbol, if the Jacobi symbol `\genfrac(){}{}{m}{n} = -1` + then ``m`` is a quadratic nonresidue modulo ``n``. + + But, unlike the Legendre symbol, if the Jacobi symbol + `\genfrac(){}{}{m}{n} = 1` then ``m`` may or may not be a quadratic residue + modulo ``n``. + + Parameters + ========== + + m : integer + n : odd positive integer + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import jacobi_symbol, legendre_symbol + >>> from sympy import S + >>> jacobi_symbol(45, 77) + -1 + >>> jacobi_symbol(60, 121) + 1 + + The relationship between the ``jacobi_symbol`` and ``legendre_symbol`` can + be demonstrated as follows: + + >>> L = legendre_symbol + >>> S(45).factors() + {3: 2, 5: 1} + >>> jacobi_symbol(7, 45) == L(7, 3)**2 * L(7, 5)**1 + True + + See Also + ======== + + is_quad_residue, legendre_symbol + """ + from sympy.functions.combinatorial.numbers import jacobi_symbol as _jacobi_symbol + return _jacobi_symbol(m, n) + + +@deprecated("""\ +The `sympy.ntheory.residue_ntheory.mobius` has been moved to `sympy.functions.combinatorial.numbers.mobius`.""", +deprecated_since_version="1.13", +active_deprecations_target='deprecated-ntheory-symbolic-functions') +def mobius(n): + """ + Mobius function maps natural number to {-1, 0, 1} + + .. deprecated:: 1.13 + + The ``mobius`` function is deprecated. Use :class:`sympy.functions.combinatorial.numbers.mobius` + instead. See its documentation for more information. See + :ref:`deprecated-ntheory-symbolic-functions` for details. + + It is defined as follows: + 1) `1` if `n = 1`. + 2) `0` if `n` has a squared prime factor. + 3) `(-1)^k` if `n` is a square-free positive integer with `k` + number of prime factors. + + It is an important multiplicative function in number theory + and combinatorics. It has applications in mathematical series, + algebraic number theory and also physics (Fermion operator has very + concrete realization with Mobius Function model). + + Parameters + ========== + + n : positive integer + + Examples + ======== + + >>> from sympy.functions.combinatorial.numbers import mobius + >>> mobius(13*7) + 1 + >>> mobius(1) + 1 + >>> mobius(13*7*5) + -1 + >>> mobius(13**2) + 0 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/M%C3%B6bius_function + .. [2] Thomas Koshy "Elementary Number Theory with Applications" + + """ + from sympy.functions.combinatorial.numbers import mobius as _mobius + return _mobius(n) + + +def _discrete_log_trial_mul(n, a, b, order=None): + """ + Trial multiplication algorithm for computing the discrete logarithm of + ``a`` to the base ``b`` modulo ``n``. + + The algorithm finds the discrete logarithm using exhaustive search. This + naive method is used as fallback algorithm of ``discrete_log`` when the + group order is very small. The value ``n`` must be greater than 1. + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import _discrete_log_trial_mul + >>> _discrete_log_trial_mul(41, 15, 7) + 3 + + See Also + ======== + + discrete_log + + References + ========== + + .. [1] "Handbook of applied cryptography", Menezes, A. J., Van, O. P. C., & + Vanstone, S. A. (1997). + """ + a %= n + b %= n + if order is None: + order = n + x = 1 + for i in range(order): + if x == a: + return i + x = x * b % n + raise ValueError("Log does not exist") + + +def _discrete_log_shanks_steps(n, a, b, order=None): + """ + Baby-step giant-step algorithm for computing the discrete logarithm of + ``a`` to the base ``b`` modulo ``n``. + + The algorithm is a time-memory trade-off of the method of exhaustive + search. It uses `O(sqrt(m))` memory, where `m` is the group order. + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import _discrete_log_shanks_steps + >>> _discrete_log_shanks_steps(41, 15, 7) + 3 + + See Also + ======== + + discrete_log + + References + ========== + + .. [1] "Handbook of applied cryptography", Menezes, A. J., Van, O. P. C., & + Vanstone, S. A. (1997). + """ + a %= n + b %= n + if order is None: + order = n_order(b, n) + m = sqrt(order) + 1 + T = {} + x = 1 + for i in range(m): + T[x] = i + x = x * b % n + z = pow(b, -m, n) + x = a + for i in range(m): + if x in T: + return i * m + T[x] + x = x * z % n + raise ValueError("Log does not exist") + + +def _discrete_log_pollard_rho(n, a, b, order=None, retries=10, rseed=None): + """ + Pollard's Rho algorithm for computing the discrete logarithm of ``a`` to + the base ``b`` modulo ``n``. + + It is a randomized algorithm with the same expected running time as + ``_discrete_log_shanks_steps``, but requires a negligible amount of memory. + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import _discrete_log_pollard_rho + >>> _discrete_log_pollard_rho(227, 3**7, 3) + 7 + + See Also + ======== + + discrete_log + + References + ========== + + .. [1] "Handbook of applied cryptography", Menezes, A. J., Van, O. P. C., & + Vanstone, S. A. (1997). + """ + a %= n + b %= n + + if order is None: + order = n_order(b, n) + randint = _randint(rseed) + + for i in range(retries): + aa = randint(1, order - 1) + ba = randint(1, order - 1) + xa = pow(b, aa, n) * pow(a, ba, n) % n + + c = xa % 3 + if c == 0: + xb = a * xa % n + ab = aa + bb = (ba + 1) % order + elif c == 1: + xb = xa * xa % n + ab = (aa + aa) % order + bb = (ba + ba) % order + else: + xb = b * xa % n + ab = (aa + 1) % order + bb = ba + + for j in range(order): + c = xa % 3 + if c == 0: + xa = a * xa % n + ba = (ba + 1) % order + elif c == 1: + xa = xa * xa % n + aa = (aa + aa) % order + ba = (ba + ba) % order + else: + xa = b * xa % n + aa = (aa + 1) % order + + c = xb % 3 + if c == 0: + xb = a * xb % n + bb = (bb + 1) % order + elif c == 1: + xb = xb * xb % n + ab = (ab + ab) % order + bb = (bb + bb) % order + else: + xb = b * xb % n + ab = (ab + 1) % order + + c = xb % 3 + if c == 0: + xb = a * xb % n + bb = (bb + 1) % order + elif c == 1: + xb = xb * xb % n + ab = (ab + ab) % order + bb = (bb + bb) % order + else: + xb = b * xb % n + ab = (ab + 1) % order + + if xa == xb: + r = (ba - bb) % order + try: + e = invert(r, order) * (ab - aa) % order + if (pow(b, e, n) - a) % n == 0: + return e + except ZeroDivisionError: + pass + break + raise ValueError("Pollard's Rho failed to find logarithm") + + +def _discrete_log_is_smooth(n: int, factorbase: list): + """Try to factor n with respect to a given factorbase. + Upon success a list of exponents with respect to the factorbase is returned. + Otherwise None.""" + factors = [0]*len(factorbase) + for i, p in enumerate(factorbase): + while n % p == 0: # divide by p as many times as possible + factors[i] += 1 + n = n // p + if n != 1: + return None # the number factors if at the end nothing is left + return factors + + +def _discrete_log_index_calculus(n, a, b, order, rseed=None): + """ + Index Calculus algorithm for computing the discrete logarithm of ``a`` to + the base ``b`` modulo ``n``. + + The group order must be given and prime. It is not suitable for small orders + and the algorithm might fail to find a solution in such situations. + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import _discrete_log_index_calculus + >>> _discrete_log_index_calculus(24570203447, 23859756228, 2, 12285101723) + 4519867240 + + See Also + ======== + + discrete_log + + References + ========== + + .. [1] "Handbook of applied cryptography", Menezes, A. J., Van, O. P. C., & + Vanstone, S. A. (1997). + """ + randint = _randint(rseed) + from math import sqrt, exp, log + a %= n + b %= n + # assert isprime(order), "The order of the base must be prime." + # First choose a heuristic the bound B for the factorbase. + # We have added an extra term to the asymptotic value which + # is closer to the theoretical optimum for n up to 2^70. + B = int(exp(0.5 * sqrt( log(n) * log(log(n)) )*( 1 + 1/log(log(n)) ))) + max = 5 * B * B # expected number of tries to find a relation + factorbase = list(primerange(B)) # compute the factorbase + lf = len(factorbase) # length of the factorbase + ordermo = order-1 + abx = a + for x in range(order): + if abx == 1: + return (order - x) % order + relationa = _discrete_log_is_smooth(abx, factorbase) + if relationa: + relationa = [r % order for r in relationa] + [x] + break + abx = abx * b % n # abx = a*pow(b, x, n) % n + + else: + raise ValueError("Index Calculus failed") + + relations = [None] * lf + k = 1 # number of relations found + kk = 0 + while k < 3 * lf and kk < max: # find relations for all primes in our factor base + x = randint(1,ordermo) + relation = _discrete_log_is_smooth(pow(b,x,n), factorbase) + if relation is None: + kk += 1 + continue + k += 1 + kk = 0 + relation += [ x ] + index = lf # determine the index of the first nonzero entry + for i in range(lf): + ri = relation[i] % order + if ri> 0 and relations[i] is not None: # make this entry zero if we can + for j in range(lf+1): + relation[j] = (relation[j] - ri*relations[i][j]) % order + else: + relation[i] = ri + if relation[i] > 0 and index == lf: # is this the index of the first nonzero entry? + index = i + if index == lf or relations[index] is not None: # the relation contains no new information + continue + # the relation contains new information + rinv = pow(relation[index],-1,order) # normalize the first nonzero entry + for j in range(index,lf+1): + relation[j] = rinv * relation[j] % order + relations[index] = relation + for i in range(lf): # subtract the new relation from the one for a + if relationa[i] > 0 and relations[i] is not None: + rbi = relationa[i] + for j in range(lf+1): + relationa[j] = (relationa[j] - rbi*relations[i][j]) % order + if relationa[i] > 0: # the index of the first nonzero entry + break # we do not need to reduce further at this point + else: # all unknowns are gone + #print(f"Success after {k} relations out of {lf}") + x = (order -relationa[lf]) % order + if pow(b,x,n) == a: + return x + raise ValueError("Index Calculus failed") + raise ValueError("Index Calculus failed") + + +def _discrete_log_pohlig_hellman(n, a, b, order=None, order_factors=None): + """ + Pohlig-Hellman algorithm for computing the discrete logarithm of ``a`` to + the base ``b`` modulo ``n``. + + In order to compute the discrete logarithm, the algorithm takes advantage + of the factorization of the group order. It is more efficient when the + group order factors into many small primes. + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import _discrete_log_pohlig_hellman + >>> _discrete_log_pohlig_hellman(251, 210, 71) + 197 + + See Also + ======== + + discrete_log + + References + ========== + + .. [1] "Handbook of applied cryptography", Menezes, A. J., Van, O. P. C., & + Vanstone, S. A. (1997). + """ + from .modular import crt + a %= n + b %= n + + if order is None: + order = n_order(b, n) + if order_factors is None: + order_factors = factorint(order) + l = [0] * len(order_factors) + + for i, (pi, ri) in enumerate(order_factors.items()): + for j in range(ri): + aj = pow(a * pow(b, -l[i], n), order // pi**(j + 1), n) + bj = pow(b, order // pi, n) + cj = discrete_log(n, aj, bj, pi, True) + l[i] += cj * pi**j + + d, _ = crt([pi**ri for pi, ri in order_factors.items()], l) + return d + + +def discrete_log(n, a, b, order=None, prime_order=None): + """ + Compute the discrete logarithm of ``a`` to the base ``b`` modulo ``n``. + + This is a recursive function to reduce the discrete logarithm problem in + cyclic groups of composite order to the problem in cyclic groups of prime + order. + + It employs different algorithms depending on the problem (subgroup order + size, prime order or not): + + * Trial multiplication + * Baby-step giant-step + * Pollard's Rho + * Index Calculus + * Pohlig-Hellman + + Examples + ======== + + >>> from sympy.ntheory import discrete_log + >>> discrete_log(41, 15, 7) + 3 + + References + ========== + + .. [1] https://mathworld.wolfram.com/DiscreteLogarithm.html + .. [2] "Handbook of applied cryptography", Menezes, A. J., Van, O. P. C., & + Vanstone, S. A. (1997). + + """ + from math import sqrt, log + n, a, b = as_int(n), as_int(a), as_int(b) + + if n < 1: + raise ValueError("n should be positive") + if n == 1: + return 0 + + if order is None: + # Compute the order and its factoring in one pass + # order = totient(n), factors = factorint(order) + factors = {} + for px, kx in factorint(n).items(): + if kx > 1: + if px in factors: + factors[px] += kx - 1 + else: + factors[px] = kx - 1 + for py, ky in factorint(px - 1).items(): + if py in factors: + factors[py] += ky + else: + factors[py] = ky + order = 1 + for px, kx in factors.items(): + order *= px**kx + # Now the `order` is the order of the group and factors = factorint(order) + # The order of `b` divides the order of the group. + order_factors = {} + for p, e in factors.items(): + i = 0 + for _ in range(e): + if pow(b, order // p, n) == 1: + order //= p + i += 1 + else: + break + if i < e: + order_factors[p] = e - i + + if prime_order is None: + prime_order = isprime(order) + + if order < 1000: + return _discrete_log_trial_mul(n, a, b, order) + elif prime_order: + # Shanks and Pollard rho are O(sqrt(order)) while index calculus is O(exp(2*sqrt(log(n)log(log(n))))) + # we compare the expected running times to determine the algorithm which is expected to be faster + if 4*sqrt(log(n)*log(log(n))) < log(order) - 10: # the number 10 was determined experimental + return _discrete_log_index_calculus(n, a, b, order) + elif order < 1000000000000: + # Shanks seems typically faster, but uses O(sqrt(order)) memory + return _discrete_log_shanks_steps(n, a, b, order) + return _discrete_log_pollard_rho(n, a, b, order) + + return _discrete_log_pohlig_hellman(n, a, b, order, order_factors) + + + +def quadratic_congruence(a, b, c, n): + r""" + Find the solutions to `a x^2 + b x + c \equiv 0 \pmod{n}`. + + Parameters + ========== + + a : int + b : int + c : int + n : int + A positive integer. + + Returns + ======= + + list[int] : + A sorted list of solutions. If no solution exists, ``[]``. + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import quadratic_congruence + >>> quadratic_congruence(2, 5, 3, 7) # 2x^2 + 5x + 3 = 0 (mod 7) + [2, 6] + >>> quadratic_congruence(8, 6, 4, 15) # No solution + [] + + See Also + ======== + + polynomial_congruence : Solve the polynomial congruence + + """ + a = as_int(a) + b = as_int(b) + c = as_int(c) + n = as_int(n) + if n <= 1: + raise ValueError("n should be an integer greater than 1") + a %= n + b %= n + c %= n + + if a == 0: + return linear_congruence(b, -c, n) + if n == 2: + # assert a == 1 + roots = [] + if c == 0: + roots.append(0) + if (b + c) % 2: + roots.append(1) + return roots + if gcd(2*a, n) == 1: + inv_a = invert(a, n) + b *= inv_a + c *= inv_a + if b % 2: + b += n + b >>= 1 + return sorted((i - b) % n for i in sqrt_mod_iter(b**2 - c, n)) + res = set() + for i in sqrt_mod_iter(b**2 - 4*a*c, 4*a*n): + q, rem = divmod(i - b, 2*a) + if rem == 0: + res.add(q % n) + + return sorted(res) + + +def _valid_expr(expr): + """ + return coefficients of expr if it is a univariate polynomial + with integer coefficients else raise a ValueError. + """ + + if not expr.is_polynomial(): + raise ValueError("The expression should be a polynomial") + polynomial = Poly(expr) + if not polynomial.is_univariate: + raise ValueError("The expression should be univariate") + if not polynomial.domain == ZZ: + raise ValueError("The expression should should have integer coefficients") + return polynomial.all_coeffs() + + +def polynomial_congruence(expr, m): + """ + Find the solutions to a polynomial congruence equation modulo m. + + Parameters + ========== + + expr : integer coefficient polynomial + m : positive integer + + Examples + ======== + + >>> from sympy.ntheory import polynomial_congruence + >>> from sympy.abc import x + >>> expr = x**6 - 2*x**5 -35 + >>> polynomial_congruence(expr, 6125) + [3257] + + See Also + ======== + + sympy.polys.galoistools.gf_csolve : low level solving routine used by this routine + + """ + coefficients = _valid_expr(expr) + coefficients = [num % m for num in coefficients] + rank = len(coefficients) + if rank == 3: + return quadratic_congruence(*coefficients, m) + if rank == 2: + return quadratic_congruence(0, *coefficients, m) + if coefficients[0] == 1 and 1 + coefficients[-1] == sum(coefficients): + return nthroot_mod(-coefficients[-1], rank - 1, m, True) + return gf_csolve(coefficients, m) + + +def binomial_mod(n, m, k): + """Compute ``binomial(n, m) % k``. + + Explanation + =========== + + Returns ``binomial(n, m) % k`` using a generalization of Lucas' + Theorem for prime powers given by Granville [1]_, in conjunction with + the Chinese Remainder Theorem. The residue for each prime power + is calculated in time O(log^2(n) + q^4*log(n)log(p) + q^4*p*log^3(p)). + + Parameters + ========== + + n : an integer + m : an integer + k : a positive integer + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import binomial_mod + >>> binomial_mod(10, 2, 6) # binomial(10, 2) = 45 + 3 + >>> binomial_mod(17, 9, 10) # binomial(17, 9) = 24310 + 0 + + References + ========== + + .. [1] Binomial coefficients modulo prime powers, Andrew Granville, + Available: https://web.archive.org/web/20170202003812/http://www.dms.umontreal.ca/~andrew/PDF/BinCoeff.pdf + """ + if k < 1: raise ValueError('k is required to be positive') + # We decompose q into a product of prime powers and apply + # the generalization of Lucas' Theorem given by Granville + # to obtain binomial(n, k) mod p^e, and then use the Chinese + # Remainder Theorem to obtain the result mod q + if n < 0 or m < 0 or m > n: return 0 + factorisation = factorint(k) + residues = [_binomial_mod_prime_power(n, m, p, e) for p, e in factorisation.items()] + return crt([p**pw for p, pw in factorisation.items()], residues, check=False)[0] + + +def _binomial_mod_prime_power(n, m, p, q): + """Compute ``binomial(n, m) % p**q`` for a prime ``p``. + + Parameters + ========== + + n : positive integer + m : a nonnegative integer + p : a prime + q : a positive integer (the prime exponent) + + Examples + ======== + + >>> from sympy.ntheory.residue_ntheory import _binomial_mod_prime_power + >>> _binomial_mod_prime_power(10, 2, 3, 2) # binomial(10, 2) = 45 + 0 + >>> _binomial_mod_prime_power(17, 9, 2, 4) # binomial(17, 9) = 24310 + 6 + + References + ========== + + .. [1] Binomial coefficients modulo prime powers, Andrew Granville, + Available: https://web.archive.org/web/20170202003812/http://www.dms.umontreal.ca/~andrew/PDF/BinCoeff.pdf + """ + # Function/variable naming within this function follows Ref.[1] + # n!_p will be used to denote the product of integers <= n not divisible by + # p, with binomial(n, m)_p the same as binomial(n, m), but defined using + # n!_p in place of n! + modulo = pow(p, q) + + def up_factorial(u): + """Compute (u*p)!_p modulo p^q.""" + r = q // 2 + fac = prod = 1 + if r == 1 and p == 2 or 2*r + 1 in (p, p*p): + if q % 2 == 1: r += 1 + modulo, div = pow(p, 2*r), pow(p, 2*r - q) + else: + modulo, div = pow(p, 2*r + 1), pow(p, (2*r + 1) - q) + for j in range(1, r + 1): + for mul in range((j - 1)*p + 1, j*p): # ignore jp itself + fac *= mul + fac %= modulo + bj_ = bj(u, j, r) + prod *= pow(fac, bj_, modulo) + prod %= modulo + if p == 2: + sm = u // 2 + for j in range(1, r + 1): sm += j//2 * bj(u, j, r) + if sm % 2 == 1: prod *= -1 + prod %= modulo//div + return prod % modulo + + def bj(u, j, r): + """Compute the exponent of (j*p)!_p in the calculation of (u*p)!_p.""" + prod = u + for i in range(1, r + 1): + if i != j: prod *= u*u - i*i + for i in range(1, r + 1): + if i != j: prod //= j*j - i*i + return prod // j + + def up_plus_v_binom(u, v): + """Compute binomial(u*p + v, v)_p modulo p^q.""" + prod = 1 + div = invert(factorial(v), modulo) + for j in range(1, q): + b = div + for v_ in range(j*p + 1, j*p + v + 1): + b *= v_ + b %= modulo + aj = u + for i in range(1, q): + if i != j: aj *= u - i + for i in range(1, q): + if i != j: aj //= j - i + aj //= j + prod *= pow(b, aj, modulo) + prod %= modulo + return prod + + @recurrence_memo([1]) + def factorial(v, prev): + """Compute v! modulo p^q.""" + return v*prev[-1] % modulo + + def factorial_p(n): + """Compute n!_p modulo p^q.""" + u, v = divmod(n, p) + return (factorial(v) * up_factorial(u) * up_plus_v_binom(u, v)) % modulo + + prod = 1 + Nj, Mj, Rj = n, m, n - m + # e0 will be the p-adic valuation of binomial(n, m) at p + e0 = carry = eq_1 = j = 0 + while Nj: + numerator = factorial_p(Nj % modulo) + denominator = factorial_p(Mj % modulo) * factorial_p(Rj % modulo) % modulo + Nj, (Mj, mj), (Rj, rj) = Nj//p, divmod(Mj, p), divmod(Rj, p) + carry = (mj + rj + carry) // p + e0 += carry + if j >= q - 1: eq_1 += carry + prod *= numerator * invert(denominator, modulo) + prod %= modulo + j += 1 + + mul = pow(1 if p == 2 and q >= 3 else -1, eq_1, modulo) + return (pow(p, e0, modulo) * mul * prod) % modulo diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_bbp_pi.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_bbp_pi.py new file mode 100644 index 0000000000000000000000000000000000000000..69c24970239cc45eef4140bf19dfd7d4f6a7e150 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_bbp_pi.py @@ -0,0 +1,134 @@ +from sympy.core.random import randint + +from sympy.ntheory.bbp_pi import pi_hex_digits +from sympy.testing.pytest import raises + + +# http://www.herongyang.com/Cryptography/Blowfish-First-8366-Hex-Digits-of-PI.html +# There are actually 8336 listed there; with the prepended 3 there are 8337 +# below +dig=''.join(''' +3243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89452821e638d013 +77be5466cf34e90c6cc0ac29b7c97c50dd3f84d5b5b54709179216d5d98979fb1bd1310ba698dfb5 +ac2ffd72dbd01adfb7b8e1afed6a267e96ba7c9045f12c7f9924a19947b3916cf70801f2e2858efc +16636920d871574e69a458fea3f4933d7e0d95748f728eb658718bcd5882154aee7b54a41dc25a59 +b59c30d5392af26013c5d1b023286085f0ca417918b8db38ef8e79dcb0603a180e6c9e0e8bb01e8a +3ed71577c1bd314b2778af2fda55605c60e65525f3aa55ab945748986263e8144055ca396a2aab10 +b6b4cc5c341141e8cea15486af7c72e993b3ee1411636fbc2a2ba9c55d741831f6ce5c3e169b8793 +1eafd6ba336c24cf5c7a325381289586773b8f48986b4bb9afc4bfe81b6628219361d809ccfb21a9 +91487cac605dec8032ef845d5de98575b1dc262302eb651b8823893e81d396acc50f6d6ff383f442 +392e0b4482a484200469c8f04a9e1f9b5e21c66842f6e96c9a670c9c61abd388f06a51a0d2d8542f +68960fa728ab5133a36eef0b6c137a3be4ba3bf0507efb2a98a1f1651d39af017666ca593e82430e +888cee8619456f9fb47d84a5c33b8b5ebee06f75d885c12073401a449f56c16aa64ed3aa62363f77 +061bfedf72429b023d37d0d724d00a1248db0fead349f1c09b075372c980991b7b25d479d8f6e8de +f7e3fe501ab6794c3b976ce0bd04c006bac1a94fb6409f60c45e5c9ec2196a246368fb6faf3e6c53 +b51339b2eb3b52ec6f6dfc511f9b30952ccc814544af5ebd09bee3d004de334afd660f2807192e4b +b3c0cba85745c8740fd20b5f39b9d3fbdb5579c0bd1a60320ad6a100c6402c7279679f25fefb1fa3 +cc8ea5e9f8db3222f83c7516dffd616b152f501ec8ad0552ab323db5fafd23876053317b483e00df +829e5c57bbca6f8ca01a87562edf1769dbd542a8f6287effc3ac6732c68c4f5573695b27b0bbca58 +c8e1ffa35db8f011a010fa3d98fd2183b84afcb56c2dd1d35b9a53e479b6f84565d28e49bc4bfb97 +90e1ddf2daa4cb7e3362fb1341cee4c6e8ef20cada36774c01d07e9efe2bf11fb495dbda4dae9091 +98eaad8e716b93d5a0d08ed1d0afc725e08e3c5b2f8e7594b78ff6e2fbf2122b648888b812900df0 +1c4fad5ea0688fc31cd1cff191b3a8c1ad2f2f2218be0e1777ea752dfe8b021fa1e5a0cc0fb56f74 +e818acf3d6ce89e299b4a84fe0fd13e0b77cc43b81d2ada8d9165fa2668095770593cc7314211a14 +77e6ad206577b5fa86c75442f5fb9d35cfebcdaf0c7b3e89a0d6411bd3ae1e7e4900250e2d2071b3 +5e226800bb57b8e0af2464369bf009b91e5563911d59dfa6aa78c14389d95a537f207d5ba202e5b9 +c5832603766295cfa911c819684e734a41b3472dca7b14a94a1b5100529a532915d60f573fbc9bc6 +e42b60a47681e6740008ba6fb5571be91ff296ec6b2a0dd915b6636521e7b9f9b6ff34052ec58556 +6453b02d5da99f8fa108ba47996e85076a4b7a70e9b5b32944db75092ec4192623ad6ea6b049a7df +7d9cee60b88fedb266ecaa8c71699a17ff5664526cc2b19ee1193602a575094c29a0591340e4183a +3e3f54989a5b429d656b8fe4d699f73fd6a1d29c07efe830f54d2d38e6f0255dc14cdd20868470eb +266382e9c6021ecc5e09686b3f3ebaefc93c9718146b6a70a1687f358452a0e286b79c5305aa5007 +373e07841c7fdeae5c8e7d44ec5716f2b8b03ada37f0500c0df01c1f040200b3ffae0cf51a3cb574 +b225837a58dc0921bdd19113f97ca92ff69432477322f547013ae5e58137c2dadcc8b576349af3dd +a7a94461460fd0030eecc8c73ea4751e41e238cd993bea0e2f3280bba1183eb3314e548b384f6db9 +086f420d03f60a04bf2cb8129024977c795679b072bcaf89afde9a771fd9930810b38bae12dccf3f +2e5512721f2e6b7124501adde69f84cd877a5847187408da17bc9f9abce94b7d8cec7aec3adb851d +fa63094366c464c3d2ef1c18473215d908dd433b3724c2ba1612a14d432a65c45150940002133ae4 +dd71dff89e10314e5581ac77d65f11199b043556f1d7a3c76b3c11183b5924a509f28fe6ed97f1fb +fa9ebabf2c1e153c6e86e34570eae96fb1860e5e0a5a3e2ab3771fe71c4e3d06fa2965dcb999e71d +0f803e89d65266c8252e4cc9789c10b36ac6150eba94e2ea78a5fc3c531e0a2df4f2f74ea7361d2b +3d1939260f19c279605223a708f71312b6ebadfe6eeac31f66e3bc4595a67bc883b17f37d1018cff +28c332ddefbe6c5aa56558218568ab9802eecea50fdb2f953b2aef7dad5b6e2f841521b628290761 +70ecdd4775619f151013cca830eb61bd960334fe1eaa0363cfb5735c904c70a239d59e9e0bcbaade +14eecc86bc60622ca79cab5cabb2f3846e648b1eaf19bdf0caa02369b9655abb5040685a323c2ab4 +b3319ee9d5c021b8f79b540b19875fa09995f7997e623d7da8f837889a97e32d7711ed935f166812 +810e358829c7e61fd696dedfa17858ba9957f584a51b2272639b83c3ff1ac24696cdb30aeb532e30 +548fd948e46dbc312858ebf2ef34c6ffeafe28ed61ee7c3c735d4a14d9e864b7e342105d14203e13 +e045eee2b6a3aaabeadb6c4f15facb4fd0c742f442ef6abbb5654f3b1d41cd2105d81e799e86854d +c7e44b476a3d816250cf62a1f25b8d2646fc8883a0c1c7b6a37f1524c369cb749247848a0b5692b2 +85095bbf00ad19489d1462b17423820e0058428d2a0c55f5ea1dadf43e233f70613372f0928d937e +41d65fecf16c223bdb7cde3759cbee74604085f2a7ce77326ea607808419f8509ee8efd85561d997 +35a969a7aac50c06c25a04abfc800bcadc9e447a2ec3453484fdd567050e1e9ec9db73dbd3105588 +cd675fda79e3674340c5c43465713e38d83d28f89ef16dff20153e21e78fb03d4ae6e39f2bdb83ad +f7e93d5a68948140f7f64c261c94692934411520f77602d4f7bcf46b2ed4a20068d40824713320f4 +6a43b7d4b7500061af1e39f62e9724454614214f74bf8b88404d95fc1d96b591af70f4ddd366a02f +45bfbc09ec03bd97857fac6dd031cb850496eb27b355fd3941da2547e6abca0a9a28507825530429 +f40a2c86dae9b66dfb68dc1462d7486900680ec0a427a18dee4f3ffea2e887ad8cb58ce0067af4d6 +b6aace1e7cd3375fecce78a399406b2a4220fe9e35d9f385b9ee39d7ab3b124e8b1dc9faf74b6d18 +5626a36631eae397b23a6efa74dd5b43326841e7f7ca7820fbfb0af54ed8feb397454056acba4895 +2755533a3a20838d87fe6ba9b7d096954b55a867bca1159a58cca9296399e1db33a62a4a563f3125 +f95ef47e1c9029317cfdf8e80204272f7080bb155c05282ce395c11548e4c66d2248c1133fc70f86 +dc07f9c9ee41041f0f404779a45d886e17325f51ebd59bc0d1f2bcc18f41113564257b7834602a9c +60dff8e8a31f636c1b0e12b4c202e1329eaf664fd1cad181156b2395e0333e92e13b240b62eebeb9 +2285b2a20ee6ba0d99de720c8c2da2f728d012784595b794fd647d0862e7ccf5f05449a36f877d48 +fac39dfd27f33e8d1e0a476341992eff743a6f6eabf4f8fd37a812dc60a1ebddf8991be14cdb6e6b +0dc67b55106d672c372765d43bdcd0e804f1290dc7cc00ffa3b5390f92690fed0b667b9ffbcedb7d +9ca091cf0bd9155ea3bb132f88515bad247b9479bf763bd6eb37392eb3cc1159798026e297f42e31 +2d6842ada7c66a2b3b12754ccc782ef11c6a124237b79251e706a1bbe64bfb63501a6b101811caed +fa3d25bdd8e2e1c3c9444216590a121386d90cec6ed5abea2a64af674eda86a85fbebfe98864e4c3 +fe9dbc8057f0f7c08660787bf86003604dd1fd8346f6381fb07745ae04d736fccc83426b33f01eab +71b08041873c005e5f77a057bebde8ae2455464299bf582e614e58f48ff2ddfda2f474ef388789bd +c25366f9c3c8b38e74b475f25546fcd9b97aeb26618b1ddf84846a0e79915f95e2466e598e20b457 +708cd55591c902de4cb90bace1bb8205d011a862487574a99eb77f19b6e0a9dc09662d09a1c43246 +33e85a1f0209f0be8c4a99a0251d6efe101ab93d1d0ba5a4dfa186f20f2868f169dcb7da83573906 +fea1e2ce9b4fcd7f5250115e01a70683faa002b5c40de6d0279af88c27773f8641c3604c0661a806 +b5f0177a28c0f586e0006058aa30dc7d6211e69ed72338ea6353c2dd94c2c21634bbcbee5690bcb6 +deebfc7da1ce591d766f05e4094b7c018839720a3d7c927c2486e3725f724d9db91ac15bb4d39eb8 +fced54557808fca5b5d83d7cd34dad0fc41e50ef5eb161e6f8a28514d96c51133c6fd5c7e756e14e +c4362abfceddc6c837d79a323492638212670efa8e406000e03a39ce37d3faf5cfabc277375ac52d +1b5cb0679e4fa33742d382274099bc9bbed5118e9dbf0f7315d62d1c7ec700c47bb78c1b6b21a190 +45b26eb1be6a366eb45748ab2fbc946e79c6a376d26549c2c8530ff8ee468dde7dd5730a1d4cd04d +c62939bbdba9ba4650ac9526e8be5ee304a1fad5f06a2d519a63ef8ce29a86ee22c089c2b843242e +f6a51e03aa9cf2d0a483c061ba9be96a4d8fe51550ba645bd62826a2f9a73a3ae14ba99586ef5562 +e9c72fefd3f752f7da3f046f6977fa0a5980e4a91587b086019b09e6ad3b3ee593e990fd5a9e34d7 +972cf0b7d9022b8b5196d5ac3a017da67dd1cf3ed67c7d2d281f9f25cfadf2b89b5ad6b4725a88f5 +4ce029ac71e019a5e647b0acfded93fa9be8d3c48d283b57ccf8d5662979132e28785f0191ed7560 +55f7960e44e3d35e8c15056dd488f46dba03a161250564f0bdc3eb9e153c9057a297271aeca93a07 +2a1b3f6d9b1e6321f5f59c66fb26dcf3197533d928b155fdf5035634828aba3cbb28517711c20ad9 +f8abcc5167ccad925f4de817513830dc8e379d58629320f991ea7a90c2fb3e7bce5121ce64774fbe +32a8b6e37ec3293d4648de53696413e680a2ae0810dd6db22469852dfd09072166b39a460a6445c0 +dd586cdecf1c20c8ae5bbef7dd1b588d40ccd2017f6bb4e3bbdda26a7e3a59ff453e350a44bcb4cd +d572eacea8fa6484bb8d6612aebf3c6f47d29be463542f5d9eaec2771bf64e6370740e0d8de75b13 +57f8721671af537d5d4040cb084eb4e2cc34d2466a0115af84e1b0042895983a1d06b89fb4ce6ea0 +486f3f3b823520ab82011a1d4b277227f8611560b1e7933fdcbb3a792b344525bda08839e151ce79 +4b2f32c9b7a01fbac9e01cc87ebcc7d1f6cf0111c3a1e8aac71a908749d44fbd9ad0dadecbd50ada +380339c32ac69136678df9317ce0b12b4ff79e59b743f5bb3af2d519ff27d9459cbf97222c15e6fc +2a0f91fc719b941525fae59361ceb69cebc2a8645912baa8d1b6c1075ee3056a0c10d25065cb03a4 +42e0ec6e0e1698db3b4c98a0be3278e9649f1f9532e0d392dfd3a0342b8971f21e1b0a74414ba334 +8cc5be7120c37632d8df359f8d9b992f2ee60b6f470fe3f11de54cda541edad891ce6279cfcd3e7e +6f1618b166fd2c1d05848fd2c5f6fb2299f523f357a632762393a8353156cccd02acf081625a75eb +b56e16369788d273ccde96629281b949d04c50901b71c65614e6c6c7bd327a140a45e1d006c3f27b +9ac9aa53fd62a80f00bb25bfe235bdd2f671126905b2040222b6cbcf7ccd769c2b53113ec01640e3 +d338abbd602547adf0ba38209cf746ce7677afa1c52075606085cbfe4e8ae88dd87aaaf9b04cf9aa +7e1948c25c02fb8a8c01c36ae4d6ebe1f990d4f869a65cdea03f09252dc208e69fb74e6132ce77e2 +5b578fdfe33ac372e6'''.split()) + + +def test_hex_pi_nth_digits(): + assert pi_hex_digits(0) == '3243f6a8885a30' + assert pi_hex_digits(1) == '243f6a8885a308' + assert pi_hex_digits(10000) == '68ac8fcfb8016c' + assert pi_hex_digits(13) == '08d313198a2e03' + assert pi_hex_digits(0, 3) == '324' + assert pi_hex_digits(0, 0) == '' + raises(ValueError, lambda: pi_hex_digits(-1)) + raises(ValueError, lambda: pi_hex_digits(0, -1)) + raises(ValueError, lambda: pi_hex_digits(3.14)) + + # this will pick a random segment to compute every time + # it is run. If it ever fails, there is an error in the + # computation. + n = randint(0, len(dig)) + prec = randint(0, len(dig) - n) + assert pi_hex_digits(n, prec) == dig[n: n + prec] diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_continued_fraction.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_continued_fraction.py new file mode 100644 index 0000000000000000000000000000000000000000..8ca6088507f1d112e9146cd5249b1143f375c2cf --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_continued_fraction.py @@ -0,0 +1,77 @@ +import itertools +from sympy.core import GoldenRatio as phi +from sympy.core.numbers import (Rational, pi) +from sympy.core.singleton import S +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.ntheory.continued_fraction import \ + (continued_fraction_periodic as cf_p, + continued_fraction_iterator as cf_i, + continued_fraction_convergents as cf_c, + continued_fraction_reduce as cf_r, + continued_fraction as cf) +from sympy.testing.pytest import raises + + +def test_continued_fraction(): + assert cf_p(1, 1, 10, 0) == cf_p(1, 1, 0, 1) + assert cf_p(1, -1, 10, 1) == cf_p(-1, 1, 10, -1) + t = sqrt(2) + assert cf((1 + t)*(1 - t)) == cf(-1) + for n in [0, 2, Rational(2, 3), sqrt(2), 3*sqrt(2), 1 + 2*sqrt(3)/5, + (2 - 3*sqrt(5))/7, 1 + sqrt(2), (-5 + sqrt(17))/4]: + assert (cf_r(cf(n)) - n).expand() == 0 + assert (cf_r(cf(-n)) + n).expand() == 0 + raises(ValueError, lambda: cf(sqrt(2 + sqrt(3)))) + raises(ValueError, lambda: cf(sqrt(2) + sqrt(3))) + raises(ValueError, lambda: cf(pi)) + raises(ValueError, lambda: cf(.1)) + + raises(ValueError, lambda: cf_p(1, 0, 0)) + raises(ValueError, lambda: cf_p(1, 1, -1)) + assert cf_p(4, 3, 0) == [1, 3] + assert cf_p(0, 3, 5) == [0, 1, [2, 1, 12, 1, 2, 2]] + assert cf_p(1, 1, 0) == [1] + assert cf_p(3, 4, 0) == [0, 1, 3] + assert cf_p(4, 5, 0) == [0, 1, 4] + assert cf_p(5, 6, 0) == [0, 1, 5] + assert cf_p(11, 13, 0) == [0, 1, 5, 2] + assert cf_p(16, 19, 0) == [0, 1, 5, 3] + assert cf_p(27, 32, 0) == [0, 1, 5, 2, 2] + assert cf_p(1, 2, 5) == [[1]] + assert cf_p(0, 1, 2) == [1, [2]] + assert cf_p(6, 7, 49) == [1, 1, 6] + assert cf_p(3796, 1387, 0) == [2, 1, 2, 1, 4] + assert cf_p(3245, 10000) == [0, 3, 12, 4, 13] + assert cf_p(1932, 2568) == [0, 1, 3, 26, 2] + assert cf_p(6589, 2569) == [2, 1, 1, 3, 2, 1, 3, 1, 23] + + def take(iterator, n=7): + return list(itertools.islice(iterator, n)) + + assert take(cf_i(phi)) == [1, 1, 1, 1, 1, 1, 1] + assert take(cf_i(pi)) == [3, 7, 15, 1, 292, 1, 1] + + assert list(cf_i(Rational(17, 12))) == [1, 2, 2, 2] + assert list(cf_i(Rational(-17, 12))) == [-2, 1, 1, 2, 2] + + assert list(cf_c([1, 6, 1, 8])) == [S.One, Rational(7, 6), Rational(8, 7), Rational(71, 62)] + assert list(cf_c([2])) == [S(2)] + assert list(cf_c([1, 1, 1, 1, 1, 1, 1])) == [S.One, S(2), Rational(3, 2), Rational(5, 3), + Rational(8, 5), Rational(13, 8), Rational(21, 13)] + assert list(cf_c([1, 6, Rational(-1, 2), 4])) == [S.One, Rational(7, 6), Rational(5, 4), Rational(3, 2)] + assert take(cf_c([[1]])) == [S.One, S(2), Rational(3, 2), Rational(5, 3), Rational(8, 5), + Rational(13, 8), Rational(21, 13)] + assert take(cf_c([1, [1, 2]])) == [S.One, S(2), Rational(5, 3), Rational(7, 4), Rational(19, 11), + Rational(26, 15), Rational(71, 41)] + + cf_iter_e = (2 if i == 1 else i // 3 * 2 if i % 3 == 0 else 1 for i in itertools.count(1)) + assert take(cf_c(cf_iter_e)) == [S(2), S(3), Rational(8, 3), Rational(11, 4), Rational(19, 7), + Rational(87, 32), Rational(106, 39)] + + assert cf_r([1, 6, 1, 8]) == Rational(71, 62) + assert cf_r([3]) == S(3) + assert cf_r([-1, 5, 1, 4]) == Rational(-24, 29) + assert (cf_r([0, 1, 1, 7, [24, 8]]) - (sqrt(3) + 2)/7).expand() == 0 + assert cf_r([1, 5, 9]) == Rational(55, 46) + assert (cf_r([[1]]) - (sqrt(5) + 1)/2).expand() == 0 + assert cf_r([-3, 1, 1, [2]]) == -1 - sqrt(2) diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_digits.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_digits.py new file mode 100644 index 0000000000000000000000000000000000000000..4284805f4ffe5b9095eacb2e83f2cd8076db3ee4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_digits.py @@ -0,0 +1,55 @@ +from sympy.ntheory import count_digits, digits, is_palindromic +from sympy.core.intfunc import num_digits + +from sympy.testing.pytest import raises + + +def test_num_digits(): + # depending on whether one rounds up or down or uses log or log10, + # one or more of these will fail if you don't check for the off-by + # one condition + assert num_digits(2, 2) == 2 + assert num_digits(2**48 - 1, 2) == 48 + assert num_digits(1000, 10) == 4 + assert num_digits(125, 5) == 4 + assert num_digits(100, 16) == 2 + assert num_digits(-1000, 10) == 4 + # if changes are made to the function, this structured test over + # this range will expose problems + for base in range(2, 100): + for e in range(1, 100): + n = base**e + assert num_digits(n, base) == e + 1 + assert num_digits(n + 1, base) == e + 1 + assert num_digits(n - 1, base) == e + + +def test_digits(): + assert all(digits(n, 2)[1:] == [int(d) for d in format(n, 'b')] + for n in range(20)) + assert all(digits(n, 8)[1:] == [int(d) for d in format(n, 'o')] + for n in range(20)) + assert all(digits(n, 16)[1:] == [int(d, 16) for d in format(n, 'x')] + for n in range(20)) + assert digits(2345, 34) == [34, 2, 0, 33] + assert digits(384753, 71) == [71, 1, 5, 23, 4] + assert digits(93409, 10) == [10, 9, 3, 4, 0, 9] + assert digits(-92838, 11) == [-11, 6, 3, 8, 2, 9] + assert digits(35, 10) == [10, 3, 5] + assert digits(35, 10, 3) == [10, 0, 3, 5] + assert digits(-35, 10, 4) == [-10, 0, 0, 3, 5] + raises(ValueError, lambda: digits(2, 2, 1)) + + +def test_count_digits(): + assert count_digits(55, 2) == {1: 5, 0: 1} + assert count_digits(55, 10) == {5: 2} + n = count_digits(123) + assert n[4] == 0 and type(n[4]) is int + + +def test_is_palindromic(): + assert is_palindromic(-11) + assert is_palindromic(11) + assert is_palindromic(0o121, 8) + assert not is_palindromic(123) diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_ecm.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_ecm.py new file mode 100644 index 0000000000000000000000000000000000000000..7f134e4e1cf68231e9f89242d2b8476b9edeabb8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_ecm.py @@ -0,0 +1,63 @@ +from sympy.external.gmpy import invert +from sympy.ntheory.ecm import ecm, Point +from sympy.testing.pytest import slow + +@slow +def test_ecm(): + assert ecm(3146531246531241245132451321) == {3, 100327907731, 10454157497791297} + assert ecm(46167045131415113) == {43, 2634823, 407485517} + assert ecm(631211032315670776841) == {9312934919, 67777885039} + assert ecm(398883434337287) == {99476569, 4009823} + assert ecm(64211816600515193) == {281719, 359641, 633767} + assert ecm(4269021180054189416198169786894227) == {184039, 241603, 333331, 477973, 618619, 974123} + assert ecm(4516511326451341281684513) == {3, 39869, 131743543, 95542348571} + assert ecm(4132846513818654136451) == {47, 160343, 2802377, 195692803} + assert ecm(168541512131094651323) == {79, 113, 11011069, 1714635721} + #This takes ~10secs while factorint is not able to factorize this even in ~10mins + assert ecm(7060005655815754299976961394452809, B1=100000, B2=1000000) == {6988699669998001, 1010203040506070809} + + +def test_Point(): + #The curve is of the form y**2 = x**3 + a*x**2 + x + mod = 101 + a = 10 + a_24 = (a + 2)*invert(4, mod) + p1 = Point(10, 17, a_24, mod) + p2 = p1.double() + assert p2 == Point(68, 56, a_24, mod) + p4 = p2.double() + assert p4 == Point(22, 64, a_24, mod) + p8 = p4.double() + assert p8 == Point(71, 95, a_24, mod) + p16 = p8.double() + assert p16 == Point(5, 16, a_24, mod) + p32 = p16.double() + assert p32 == Point(33, 96, a_24, mod) + + # p3 = p2 + p1 + p3 = p2.add(p1, p1) + assert p3 == Point(1, 61, a_24, mod) + # p5 = p3 + p2 or p4 + p1 + p5 = p3.add(p2, p1) + assert p5 == Point(49, 90, a_24, mod) + assert p5 == p4.add(p1, p3) + # p6 = 2*p3 + p6 = p3.double() + assert p6 == Point(87, 43, a_24, mod) + assert p6 == p4.add(p2, p2) + # p7 = p5 + p2 + p7 = p5.add(p2, p3) + assert p7 == Point(69, 23, a_24, mod) + assert p7 == p4.add(p3, p1) + assert p7 == p6.add(p1, p5) + # p9 = p5 + p4 + p9 = p5.add(p4, p1) + assert p9 == Point(56, 99, a_24, mod) + assert p9 == p6.add(p3, p3) + assert p9 == p7.add(p2, p5) + assert p9 == p8.add(p1, p7) + + assert p5 == p1.mont_ladder(5) + assert p9 == p1.mont_ladder(9) + assert p16 == p1.mont_ladder(16) + assert p9 == p3.mont_ladder(3) diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_egyptian_fraction.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_egyptian_fraction.py new file mode 100644 index 0000000000000000000000000000000000000000..a9a9fac578d93a88a648bdcf8dc34550cf4a7573 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_egyptian_fraction.py @@ -0,0 +1,49 @@ +from sympy.core.numbers import Rational +from sympy.ntheory.egyptian_fraction import egyptian_fraction +from sympy.core.add import Add +from sympy.testing.pytest import raises +from sympy.core.random import random_complex_number + + +def test_egyptian_fraction(): + def test_equality(r, alg="Greedy"): + return r == Add(*[Rational(1, i) for i in egyptian_fraction(r, alg)]) + + r = random_complex_number(a=0, c=1, b=0, d=0, rational=True) + assert test_equality(r) + + assert egyptian_fraction(Rational(4, 17)) == [5, 29, 1233, 3039345] + assert egyptian_fraction(Rational(7, 13), "Greedy") == [2, 26] + assert egyptian_fraction(Rational(23, 101), "Greedy") == \ + [5, 37, 1438, 2985448, 40108045937720] + assert egyptian_fraction(Rational(18, 23), "Takenouchi") == \ + [2, 6, 12, 35, 276, 2415] + assert egyptian_fraction(Rational(5, 6), "Graham Jewett") == \ + [6, 7, 8, 9, 10, 42, 43, 44, 45, 56, 57, 58, 72, 73, 90, 1806, 1807, + 1808, 1892, 1893, 1980, 3192, 3193, 3306, 5256, 3263442, 3263443, + 3267056, 3581556, 10192056, 10650056950806] + assert egyptian_fraction(Rational(5, 6), "Golomb") == [2, 6, 12, 20, 30] + assert egyptian_fraction(Rational(5, 121), "Golomb") == [25, 1225, 3577, 7081, 11737] + raises(ValueError, lambda: egyptian_fraction(Rational(-4, 9))) + assert egyptian_fraction(Rational(8, 3), "Golomb") == [1, 2, 3, 4, 5, 6, 7, + 14, 574, 2788, 6460, + 11590, 33062, 113820] + assert egyptian_fraction(Rational(355, 113)) == [1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 27, 744, 893588, + 1251493536607, + 20361068938197002344405230] + + +def test_input(): + r = (2,3), Rational(2, 3), (Rational(2), Rational(3)) + for m in ["Greedy", "Graham Jewett", "Takenouchi", "Golomb"]: + for i in r: + d = egyptian_fraction(i, m) + assert all(i.is_Integer for i in d) + if m == "Graham Jewett": + assert d == [3, 4, 12] + else: + assert d == [2, 6] + # check prefix + d = egyptian_fraction(Rational(5, 3)) + assert d == [1, 2, 6] and all(i.is_Integer for i in d) diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_elliptic_curve.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_elliptic_curve.py new file mode 100644 index 0000000000000000000000000000000000000000..7d49d8eac72cc622fb92dfca8c54e5cc6c8dfb8f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_elliptic_curve.py @@ -0,0 +1,20 @@ +from sympy.ntheory.elliptic_curve import EllipticCurve + + +def test_elliptic_curve(): + # Point addition and multiplication + e3 = EllipticCurve(-1, 9) + p = e3(0, 3) + q = e3(-1, 3) + r = p + q + assert r.x == 1 and r.y == -3 + r = 2*p + q + assert r.x == 35 and r.y == 207 + r = -p + q + assert r.x == 37 and r.y == 225 + # Verify result in http://www.lmfdb.org/EllipticCurve/Q + # Discriminant + assert EllipticCurve(-1, 9).discriminant == -34928 + assert EllipticCurve(-2731, -55146, 1, 0, 1).discriminant == 25088 + # Torsion points + assert len(EllipticCurve(0, 1).torsion_points()) == 6 diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_factor_.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_factor_.py new file mode 100644 index 0000000000000000000000000000000000000000..5174b842c49ef0e14c1ad38d2d9ad550c2a2a388 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_factor_.py @@ -0,0 +1,702 @@ +from sympy.core.containers import Dict +from sympy.core.mul import Mul +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.functions.combinatorial.factorials import factorial as fac +from sympy.core.numbers import Integer, Rational +from sympy.external.gmpy import gcd + +from sympy.ntheory import (totient, + factorint, primefactors, divisors, nextprime, + pollard_rho, perfect_power, multiplicity, multiplicity_in_factorial, + divisor_count, primorial, pollard_pm1, divisor_sigma, + factorrat, reduced_totient) +from sympy.ntheory.factor_ import (smoothness, smoothness_p, proper_divisors, + antidivisors, antidivisor_count, _divisor_sigma, core, udivisors, udivisor_sigma, + udivisor_count, proper_divisor_count, primenu, primeomega, + mersenne_prime_exponent, is_perfect, is_abundant, + is_deficient, is_amicable, is_carmichael, find_carmichael_numbers_in_range, + find_first_n_carmichaels, dra, drm, _perfect_power, factor_cache) + +from sympy.testing.pytest import raises, slow + +from sympy.utilities.iterables import capture + + +def fac_multiplicity(n, p): + """Return the power of the prime number p in the + factorization of n!""" + if p > n: + return 0 + if p > n//2: + return 1 + q, m = n, 0 + while q >= p: + q //= p + m += q + return m + + +def multiproduct(seq=(), start=1): + """ + Return the product of a sequence of factors with multiplicities, + times the value of the parameter ``start``. The input may be a + sequence of (factor, exponent) pairs or a dict of such pairs. + + >>> multiproduct({3:7, 2:5}, 4) # = 3**7 * 2**5 * 4 + 279936 + + """ + if not seq: + return start + if isinstance(seq, dict): + seq = iter(seq.items()) + units = start + multi = [] + for base, exp in seq: + if not exp: + continue + elif exp == 1: + units *= base + else: + if exp % 2: + units *= base + multi.append((base, exp//2)) + return units * multiproduct(multi)**2 + + +def test_multiplicity(): + for b in range(2, 20): + for i in range(100): + assert multiplicity(b, b**i) == i + assert multiplicity(b, (b**i) * 23) == i + assert multiplicity(b, (b**i) * 1000249) == i + # Should be fast + assert multiplicity(10, 10**10023) == 10023 + # Should exit quickly + assert multiplicity(10**10, 10**10) == 1 + # Should raise errors for bad input + raises(ValueError, lambda: multiplicity(1, 1)) + raises(ValueError, lambda: multiplicity(1, 2)) + raises(ValueError, lambda: multiplicity(1.3, 2)) + raises(ValueError, lambda: multiplicity(2, 0)) + raises(ValueError, lambda: multiplicity(1.3, 0)) + + # handles Rationals + assert multiplicity(10, Rational(30, 7)) == 1 + assert multiplicity(Rational(2, 7), Rational(4, 7)) == 1 + assert multiplicity(Rational(1, 7), Rational(3, 49)) == 2 + assert multiplicity(Rational(2, 7), Rational(7, 2)) == -1 + assert multiplicity(3, Rational(1, 9)) == -2 + + +def test_multiplicity_in_factorial(): + n = fac(1000) + for i in (2, 4, 6, 12, 30, 36, 48, 60, 72, 96): + assert multiplicity(i, n) == multiplicity_in_factorial(i, 1000) + + +def test_private_perfect_power(): + assert _perfect_power(0) is False + assert _perfect_power(1) is False + assert _perfect_power(2) is False + assert _perfect_power(3) is False + for x in [2, 3, 5, 6, 7, 12, 15, 105, 100003]: + for y in range(2, 100): + assert _perfect_power(x**y) == (x, y) + if x & 1: + assert _perfect_power(x**y, next_p=3) == (x, y) + if x == 100003: + assert _perfect_power(x**y, next_p=100003) == (x, y) + assert _perfect_power(101*x**y) == False + # Catalan's conjecture + if x**y not in [8, 9]: + assert _perfect_power(x**y + 1) == False + assert _perfect_power(x**y - 1) == False + for x in range(1, 10): + for y in range(1, 10): + g = gcd(x, y) + if g == 1: + assert _perfect_power(5**x * 101**y) == False + else: + assert _perfect_power(5**x * 101**y) == (5**(x//g) * 101**(y//g), g) + + +def test_perfect_power(): + raises(ValueError, lambda: perfect_power(0.1)) + assert perfect_power(0) is False + assert perfect_power(1) is False + assert perfect_power(2) is False + assert perfect_power(3) is False + assert perfect_power(4) == (2, 2) + assert perfect_power(14) is False + assert perfect_power(25) == (5, 2) + assert perfect_power(22) is False + assert perfect_power(22, [2]) is False + assert perfect_power(137**(3*5*13)) == (137, 3*5*13) + assert perfect_power(137**(3*5*13) + 1) is False + assert perfect_power(137**(3*5*13) - 1) is False + assert perfect_power(103005006004**7) == (103005006004, 7) + assert perfect_power(103005006004**7 + 1) is False + assert perfect_power(103005006004**7 - 1) is False + assert perfect_power(103005006004**12) == (103005006004, 12) + assert perfect_power(103005006004**12 + 1) is False + assert perfect_power(103005006004**12 - 1) is False + assert perfect_power(2**10007) == (2, 10007) + assert perfect_power(2**10007 + 1) is False + assert perfect_power(2**10007 - 1) is False + assert perfect_power((9**99 + 1)**60) == (9**99 + 1, 60) + assert perfect_power((9**99 + 1)**60 + 1) is False + assert perfect_power((9**99 + 1)**60 - 1) is False + assert perfect_power((10**40000)**2, big=False) == (10**40000, 2) + assert perfect_power(10**100000) == (10, 100000) + assert perfect_power(10**100001) == (10, 100001) + assert perfect_power(13**4, [3, 5]) is False + assert perfect_power(3**4, [3, 10], factor=0) is False + assert perfect_power(3**3*5**3) == (15, 3) + assert perfect_power(2**3*5**5) is False + assert perfect_power(2*13**4) is False + assert perfect_power(2**5*3**3) is False + t = 2**24 + for d in divisors(24): + m = perfect_power(t*3**d) + assert m and m[1] == d or d == 1 + m = perfect_power(t*3**d, big=False) + assert m and m[1] == 2 or d == 1 or d == 3, (d, m) + + # negatives and non-integer rationals + assert perfect_power(-4) is False + assert perfect_power(-8) == (-2, 3) + assert perfect_power(-S(1)/8) == (-S(1)/2, 3) + assert perfect_power(S(1)/3) == False + assert perfect_power(-5**15) == (-5, 15) + assert perfect_power(-5**15, big=False) == (-3125, 3) + assert perfect_power(-5**15, [15]) == (-5, 15) + + n = -3 ** 60 + assert perfect_power(n) == (-81, 15) + assert perfect_power(n, big=False) == (-3486784401, 3) + assert perfect_power(n, [3, 5], big=True) == (-531441, 5) + assert perfect_power(n, [3, 5], big=False) == (-3486784401, 3) + assert perfect_power(n, [2]) == False + assert perfect_power(n, [2, 15]) == (-81, 15) + assert perfect_power(n, [2, 13]) == False + assert perfect_power(n, [17]) == False + assert perfect_power(n, [3]) == (-3486784401, 3) + assert perfect_power(n + 1) == False + + r = S(2) ** (2 * 5 * 7) / S(3) ** (2 * 7) + assert perfect_power(r) == (S(32) / 3, 14) + assert perfect_power(-r) == (-S(1024) / 9, 7) + assert perfect_power(r, big=False) == (S(34359738368) / 2187, 2) + assert perfect_power(r, [2, 5]) == (S(34359738368) / 2187, 2) + assert perfect_power(r, [5, 7]) == (S(1024) / 9, 7) + assert perfect_power(r, [5, 7], big=False) == (S(1024) / 9, 7) + assert perfect_power(r, [2, 5, 7], big=False) == (S(34359738368) / 2187, 2) + assert perfect_power(-r, [5, 7], big=False) == (-S(1024) / 9, 7) + + assert perfect_power(-S(1) / 8) == (-S(1) / 2, 3) + + assert perfect_power((-3)**60) == (3, 60) + assert perfect_power((-3)**61) == (-3, 61) + + assert perfect_power(S(2 ** 9) / 3 ** 12) == (S(8)/81, 3) + assert perfect_power(Rational(1, 2)**3) == (S.Half, 3) + assert perfect_power(Rational(-3, 2)**3) == (-3*S.Half, 3) + + +def test_factor_cache(): + factor_cache.cache_clear() + raises(ValueError, lambda: factor_cache.__setitem__(1, 5)) + raises(ValueError, lambda: factor_cache.__setitem__(10, 1)) + raises(ValueError, lambda: factor_cache.__setitem__(10, 10)) + raises(ValueError, lambda: factor_cache.__setitem__(10, 3)) + raises(ValueError, lambda: factor_cache.__setitem__(20, 4)) + factor_cache.maxsize = 3 + for i in range(2, 10): + factor_cache[5*i] = 5 + assert len(factor_cache) == 3 + factor_cache.maxsize = 5 + for i in range(2, 10): + factor_cache[5*i] = 5 + assert len(factor_cache) == 5 + factor_cache.maxsize = 2 + assert len(factor_cache) == 2 + factor_cache.maxsize =1000 + + factor_cache.cache_clear() + factor_cache[40] = 5 + assert factor_cache.get(40) == 5 + assert factor_cache.get(20) is None + assert factor_cache[40] == 5 + raises(KeyError, lambda: factor_cache[10]) + del factor_cache[40] + assert len(factor_cache) == 0 + raises(KeyError, lambda: factor_cache.__delitem__(40)) + factor_cache.add(100, [5, 2]) + assert len(factor_cache) == 2 + assert factor_cache[100] == 5 + + for n in [1000000007, 10000019*20000003]: + factorint(n) + assert n in factor_cache + + # Restore the initial state + factor_cache.cache_clear() + factor_cache.maxsize = 1000 + + +@slow +def test_factorint(): + assert primefactors(123456) == [2, 3, 643] + assert factorint(0) == {0: 1} + assert factorint(1) == {} + assert factorint(-1) == {-1: 1} + assert factorint(-2) == {-1: 1, 2: 1} + assert factorint(-16) == {-1: 1, 2: 4} + assert factorint(2) == {2: 1} + assert factorint(126) == {2: 1, 3: 2, 7: 1} + assert factorint(123456) == {2: 6, 3: 1, 643: 1} + assert factorint(5951757) == {3: 1, 7: 1, 29: 2, 337: 1} + assert factorint(64015937) == {7993: 1, 8009: 1} + assert factorint(2**(2**6) + 1) == {274177: 1, 67280421310721: 1} + #issue 19683 + assert factorint(10**38 - 1) == {3: 2, 11: 1, 909090909090909091: 1, 1111111111111111111: 1} + #issue 17676 + assert factorint(28300421052393658575) == {3: 1, 5: 2, 11: 2, 43: 1, 2063: 2, 4127: 1, 4129: 1} + assert factorint(2063**2 * 4127**1 * 4129**1) == {2063: 2, 4127: 1, 4129: 1} + assert factorint(2347**2 * 7039**1 * 7043**1) == {2347: 2, 7039: 1, 7043: 1} + + assert factorint(0, multiple=True) == [0] + assert factorint(1, multiple=True) == [] + assert factorint(-1, multiple=True) == [-1] + assert factorint(-2, multiple=True) == [-1, 2] + assert factorint(-16, multiple=True) == [-1, 2, 2, 2, 2] + assert factorint(2, multiple=True) == [2] + assert factorint(24, multiple=True) == [2, 2, 2, 3] + assert factorint(126, multiple=True) == [2, 3, 3, 7] + assert factorint(123456, multiple=True) == [2, 2, 2, 2, 2, 2, 3, 643] + assert factorint(5951757, multiple=True) == [3, 7, 29, 29, 337] + assert factorint(64015937, multiple=True) == [7993, 8009] + assert factorint(2**(2**6) + 1, multiple=True) == [274177, 67280421310721] + + assert factorint(fac(1, evaluate=False)) == {} + assert factorint(fac(7, evaluate=False)) == {2: 4, 3: 2, 5: 1, 7: 1} + assert factorint(fac(15, evaluate=False)) == \ + {2: 11, 3: 6, 5: 3, 7: 2, 11: 1, 13: 1} + assert factorint(fac(20, evaluate=False)) == \ + {2: 18, 3: 8, 5: 4, 7: 2, 11: 1, 13: 1, 17: 1, 19: 1} + assert factorint(fac(23, evaluate=False)) == \ + {2: 19, 3: 9, 5: 4, 7: 3, 11: 2, 13: 1, 17: 1, 19: 1, 23: 1} + + assert multiproduct(factorint(fac(200))) == fac(200) + assert multiproduct(factorint(fac(200, evaluate=False))) == fac(200) + for b, e in factorint(fac(150)).items(): + assert e == fac_multiplicity(150, b) + for b, e in factorint(fac(150, evaluate=False)).items(): + assert e == fac_multiplicity(150, b) + assert factorint(103005006059**7) == {103005006059: 7} + assert factorint(31337**191) == {31337: 191} + assert factorint(2**1000 * 3**500 * 257**127 * 383**60) == \ + {2: 1000, 3: 500, 257: 127, 383: 60} + assert len(factorint(fac(10000))) == 1229 + assert len(factorint(fac(10000, evaluate=False))) == 1229 + assert factorint(12932983746293756928584532764589230) == \ + {2: 1, 5: 1, 73: 1, 727719592270351: 1, 63564265087747: 1, 383: 1} + assert factorint(727719592270351) == {727719592270351: 1} + assert factorint(2**64 + 1, use_trial=False) == factorint(2**64 + 1) + for n in range(60000): + assert multiproduct(factorint(n)) == n + assert pollard_rho(2**64 + 1, seed=1) == 274177 + assert pollard_rho(19, seed=1) is None + assert factorint(3, limit=2) == {3: 1} + assert factorint(12345) == {3: 1, 5: 1, 823: 1} + assert factorint( + 12345, limit=3) == {4115: 1, 3: 1} # the 5 is greater than the limit + assert factorint(1, limit=1) == {} + assert factorint(0, 3) == {0: 1} + assert factorint(12, limit=1) == {12: 1} + assert factorint(30, limit=2) == {2: 1, 15: 1} + assert factorint(16, limit=2) == {2: 4} + assert factorint(124, limit=3) == {2: 2, 31: 1} + assert factorint(4*31**2, limit=3) == {2: 2, 31: 2} + p1 = nextprime(2**32) + p2 = nextprime(2**16) + p3 = nextprime(p2) + assert factorint(p1*p2*p3) == {p1: 1, p2: 1, p3: 1} + assert factorint(13*17*19, limit=15) == {13: 1, 17*19: 1} + assert factorint(1951*15013*15053, limit=2000) == {225990689: 1, 1951: 1} + assert factorint(primorial(17) + 1, use_pm1=0) == \ + {int(19026377261): 1, 3467: 1, 277: 1, 105229: 1} + # when prime b is closer than approx sqrt(8*p) to prime p then they are + # "close" and have a trivial factorization + a = nextprime(2**2**8) # 78 digits + b = nextprime(a + 2**2**4) + assert 'Fermat' in capture(lambda: factorint(a*b, verbose=1)) + + raises(ValueError, lambda: pollard_rho(4)) + raises(ValueError, lambda: pollard_pm1(3)) + raises(ValueError, lambda: pollard_pm1(10, B=2)) + # verbose coverage + n = nextprime(2**16)*nextprime(2**17)*nextprime(1901) + assert 'with primes' in capture(lambda: factorint(n, verbose=1)) + capture(lambda: factorint(nextprime(2**16)*1012, verbose=1)) + + n = nextprime(2**17) + capture(lambda: factorint(n**3, verbose=1)) # perfect power termination + capture(lambda: factorint(2*n, verbose=1)) # factoring complete msg + + # exceed 1st + n = nextprime(2**17) + n *= nextprime(n) + assert '1000' in capture(lambda: factorint(n, limit=1000, verbose=1)) + n *= nextprime(n) + assert len(factorint(n)) == 3 + assert len(factorint(n, limit=p1)) == 3 + n *= nextprime(2*n) + # exceed 2nd + assert '2001' in capture(lambda: factorint(n, limit=2000, verbose=1)) + assert capture( + lambda: factorint(n, limit=4000, verbose=1)).count('Pollard') == 2 + # non-prime pm1 result + n = nextprime(8069) + n *= nextprime(2*n)*nextprime(2*n, 2) + capture(lambda: factorint(n, verbose=1)) # non-prime pm1 result + # factor fermat composite + p1 = nextprime(2**17) + p2 = nextprime(2*p1) + assert factorint((p1*p2**2)**3) == {p1: 3, p2: 6} + # Test for non integer input + raises(ValueError, lambda: factorint(4.5)) + # test dict/Dict input + sans = '2**10*3**3' + n = {4: 2, 12: 3} + assert str(factorint(n)) == sans + assert str(factorint(Dict(n))) == sans + + +def test_divisors_and_divisor_count(): + assert divisors(-1) == [1] + assert divisors(0) == [] + assert divisors(1) == [1] + assert divisors(2) == [1, 2] + assert divisors(3) == [1, 3] + assert divisors(17) == [1, 17] + assert divisors(10) == [1, 2, 5, 10] + assert divisors(100) == [1, 2, 4, 5, 10, 20, 25, 50, 100] + assert divisors(101) == [1, 101] + assert type(divisors(2, generator=True)) is not list + + assert divisor_count(0) == 0 + assert divisor_count(-1) == 1 + assert divisor_count(1) == 1 + assert divisor_count(6) == 4 + assert divisor_count(12) == 6 + + assert divisor_count(180, 3) == divisor_count(180//3) + assert divisor_count(2*3*5, 7) == 0 + + +def test_proper_divisors_and_proper_divisor_count(): + assert proper_divisors(-1) == [] + assert proper_divisors(0) == [] + assert proper_divisors(1) == [] + assert proper_divisors(2) == [1] + assert proper_divisors(3) == [1] + assert proper_divisors(17) == [1] + assert proper_divisors(10) == [1, 2, 5] + assert proper_divisors(100) == [1, 2, 4, 5, 10, 20, 25, 50] + assert proper_divisors(1000000007) == [1] + assert type(proper_divisors(2, generator=True)) is not list + + assert proper_divisor_count(0) == 0 + assert proper_divisor_count(-1) == 0 + assert proper_divisor_count(1) == 0 + assert proper_divisor_count(36) == 8 + assert proper_divisor_count(2*3*5) == 7 + + +def test_udivisors_and_udivisor_count(): + assert udivisors(-1) == [1] + assert udivisors(0) == [] + assert udivisors(1) == [1] + assert udivisors(2) == [1, 2] + assert udivisors(3) == [1, 3] + assert udivisors(17) == [1, 17] + assert udivisors(10) == [1, 2, 5, 10] + assert udivisors(100) == [1, 4, 25, 100] + assert udivisors(101) == [1, 101] + assert udivisors(1000) == [1, 8, 125, 1000] + assert type(udivisors(2, generator=True)) is not list + + assert udivisor_count(0) == 0 + assert udivisor_count(-1) == 1 + assert udivisor_count(1) == 1 + assert udivisor_count(6) == 4 + assert udivisor_count(12) == 4 + + assert udivisor_count(180) == 8 + assert udivisor_count(2*3*5*7) == 16 + + +def test_issue_6981(): + S = set(divisors(4)).union(set(divisors(Integer(2)))) + assert S == {1,2,4} + + +def test_issue_4356(): + assert factorint(1030903) == {53: 2, 367: 1} + + +def test_divisors(): + assert divisors(28) == [1, 2, 4, 7, 14, 28] + assert list(divisors(3*5*7, 1)) == [1, 3, 5, 15, 7, 21, 35, 105] + assert divisors(0) == [] + + +def test_divisor_count(): + assert divisor_count(0) == 0 + assert divisor_count(6) == 4 + + +def test_proper_divisors(): + assert proper_divisors(-1) == [] + assert proper_divisors(28) == [1, 2, 4, 7, 14] + assert list(proper_divisors(3*5*7, True)) == [1, 3, 5, 15, 7, 21, 35] + + +def test_proper_divisor_count(): + assert proper_divisor_count(6) == 3 + assert proper_divisor_count(108) == 11 + + +def test_antidivisors(): + assert antidivisors(-1) == [] + assert antidivisors(-3) == [2] + assert antidivisors(14) == [3, 4, 9] + assert antidivisors(237) == [2, 5, 6, 11, 19, 25, 43, 95, 158] + assert antidivisors(12345) == [2, 6, 7, 10, 30, 1646, 3527, 4938, 8230] + assert antidivisors(393216) == [262144] + assert sorted(x for x in antidivisors(3*5*7, 1)) == \ + [2, 6, 10, 11, 14, 19, 30, 42, 70] + assert antidivisors(1) == [] + assert type(antidivisors(2, generator=True)) is not list + +def test_antidivisor_count(): + assert antidivisor_count(0) == 0 + assert antidivisor_count(-1) == 0 + assert antidivisor_count(-4) == 1 + assert antidivisor_count(20) == 3 + assert antidivisor_count(25) == 5 + assert antidivisor_count(38) == 7 + assert antidivisor_count(180) == 6 + assert antidivisor_count(2*3*5) == 3 + + +def test_smoothness_and_smoothness_p(): + assert smoothness(1) == (1, 1) + assert smoothness(2**4*3**2) == (3, 16) + + assert smoothness_p(10431, m=1) == \ + (1, [(3, (2, 2, 4)), (19, (1, 5, 5)), (61, (1, 31, 31))]) + assert smoothness_p(10431) == \ + (-1, [(3, (2, 2, 2)), (19, (1, 3, 9)), (61, (1, 5, 5))]) + assert smoothness_p(10431, power=1) == \ + (-1, [(3, (2, 2, 2)), (61, (1, 5, 5)), (19, (1, 3, 9))]) + assert smoothness_p(21477639576571, visual=1) == \ + 'p**i=4410317**1 has p-1 B=1787, B-pow=1787\n' + \ + 'p**i=4869863**1 has p-1 B=2434931, B-pow=2434931' + + +def test_visual_factorint(): + assert factorint(1, visual=1) == 1 + forty2 = factorint(42, visual=True) + assert type(forty2) == Mul + assert str(forty2) == '2**1*3**1*7**1' + assert factorint(1, visual=True) is S.One + no = {"evaluate": False} + assert factorint(42**2, visual=True) == Mul(Pow(2, 2, **no), + Pow(3, 2, **no), + Pow(7, 2, **no), **no) + assert -1 in factorint(-42, visual=True).args + + +def test_factorrat(): + assert str(factorrat(S(12)/1, visual=True)) == '2**2*3**1' + assert str(factorrat(Rational(1, 1), visual=True)) == '1' + assert str(factorrat(S(25)/14, visual=True)) == '5**2/(2*7)' + assert str(factorrat(Rational(25, 14), visual=True)) == '5**2/(2*7)' + assert str(factorrat(S(-25)/14/9, visual=True)) == '-1*5**2/(2*3**2*7)' + + assert factorrat(S(12)/1, multiple=True) == [2, 2, 3] + assert factorrat(Rational(1, 1), multiple=True) == [] + assert factorrat(S(25)/14, multiple=True) == [Rational(1, 7), S.Half, 5, 5] + assert factorrat(Rational(25, 14), multiple=True) == [Rational(1, 7), S.Half, 5, 5] + assert factorrat(Rational(12, 1), multiple=True) == [2, 2, 3] + assert factorrat(S(-25)/14/9, multiple=True) == \ + [-1, Rational(1, 7), Rational(1, 3), Rational(1, 3), S.Half, 5, 5] + + +def test_visual_io(): + sm = smoothness_p + fi = factorint + # with smoothness_p + n = 124 + d = fi(n) + m = fi(d, visual=True) + t = sm(n) + s = sm(t) + for th in [d, s, t, n, m]: + assert sm(th, visual=True) == s + assert sm(th, visual=1) == s + for th in [d, s, t, n, m]: + assert sm(th, visual=False) == t + assert [sm(th, visual=None) for th in [d, s, t, n, m]] == [s, d, s, t, t] + assert [sm(th, visual=2) for th in [d, s, t, n, m]] == [s, d, s, t, t] + + # with factorint + for th in [d, m, n]: + assert fi(th, visual=True) == m + assert fi(th, visual=1) == m + for th in [d, m, n]: + assert fi(th, visual=False) == d + assert [fi(th, visual=None) for th in [d, m, n]] == [m, d, d] + assert [fi(th, visual=0) for th in [d, m, n]] == [m, d, d] + + # test reevaluation + no = {"evaluate": False} + assert sm({4: 2}, visual=False) == sm(16) + assert sm(Mul(*[Pow(k, v, **no) for k, v in {4: 2, 2: 6}.items()], **no), + visual=False) == sm(2**10) + + assert fi({4: 2}, visual=False) == fi(16) + assert fi(Mul(*[Pow(k, v, **no) for k, v in {4: 2, 2: 6}.items()], **no), + visual=False) == fi(2**10) + + +def test_core(): + assert core(35**13, 10) == 42875 + assert core(210**2) == 1 + assert core(7776, 3) == 36 + assert core(10**27, 22) == 10**5 + assert core(537824) == 14 + assert core(1, 6) == 1 + + +def test__divisor_sigma(): + assert _divisor_sigma(23450) == 50592 + assert _divisor_sigma(23450, 0) == 24 + assert _divisor_sigma(23450, 1) == 50592 + assert _divisor_sigma(23450, 2) == 730747500 + assert _divisor_sigma(23450, 3) == 14666785333344 + A000005 = [1, 2, 2, 3, 2, 4, 2, 4, 3, 4, 2, 6, 2, 4, 4, 5, 2, 6, 2, 6, 4, + 4, 2, 8, 3, 4, 4, 6, 2, 8, 2, 6, 4, 4, 4, 9, 2, 4, 4, 8, 2, 8] + for n, val in enumerate(A000005, 1): + assert _divisor_sigma(n, 0) == val + A000203 = [1, 3, 4, 7, 6, 12, 8, 15, 13, 18, 12, 28, 14, 24, 24, 31, 18, + 39, 20, 42, 32, 36, 24, 60, 31, 42, 40, 56, 30, 72, 32, 63, 48] + for n, val in enumerate(A000203, 1): + assert _divisor_sigma(n, 1) == val + A001157 = [1, 5, 10, 21, 26, 50, 50, 85, 91, 130, 122, 210, 170, 250, 260, + 341, 290, 455, 362, 546, 500, 610, 530, 850, 651, 850, 820, 1050] + for n, val in enumerate(A001157, 1): + assert _divisor_sigma(n, 2) == val + + +def test_mersenne_prime_exponent(): + assert mersenne_prime_exponent(1) == 2 + assert mersenne_prime_exponent(4) == 7 + assert mersenne_prime_exponent(10) == 89 + assert mersenne_prime_exponent(25) == 21701 + raises(ValueError, lambda: mersenne_prime_exponent(52)) + raises(ValueError, lambda: mersenne_prime_exponent(0)) + + +def test_is_perfect(): + assert is_perfect(-6) is False + assert is_perfect(6) is True + assert is_perfect(15) is False + assert is_perfect(28) is True + assert is_perfect(400) is False + assert is_perfect(496) is True + assert is_perfect(8128) is True + assert is_perfect(10000) is False + + +def test_is_abundant(): + assert is_abundant(10) is False + assert is_abundant(12) is True + assert is_abundant(18) is True + assert is_abundant(21) is False + assert is_abundant(945) is True + + +def test_is_deficient(): + assert is_deficient(10) is True + assert is_deficient(22) is True + assert is_deficient(56) is False + assert is_deficient(20) is False + assert is_deficient(36) is False + + +def test_is_amicable(): + assert is_amicable(173, 129) is False + assert is_amicable(220, 284) is True + assert is_amicable(8756, 8756) is False + + +def test_is_carmichael(): + A002997 = [561, 1105, 1729, 2465, 2821, 6601, 8911, 10585, 15841, + 29341, 41041, 46657, 52633, 62745, 63973, 75361, 101101] + for n in range(1, 5000): + assert is_carmichael(n) == (n in A002997) + for n in A002997: + assert is_carmichael(n) + + +def test_find_carmichael_numbers_in_range(): + assert find_carmichael_numbers_in_range(0, 561) == [] + assert find_carmichael_numbers_in_range(561, 562) == [561] + assert find_carmichael_numbers_in_range(561, 1105) == find_carmichael_numbers_in_range(561, 562) + raises(ValueError, lambda: find_carmichael_numbers_in_range(-2, 2)) + raises(ValueError, lambda: find_carmichael_numbers_in_range(22, 2)) + + +def test_find_first_n_carmichaels(): + assert find_first_n_carmichaels(0) == [] + assert find_first_n_carmichaels(1) == [561] + assert find_first_n_carmichaels(2) == [561, 1105] + + +def test_dra(): + assert dra(19, 12) == 8 + assert dra(2718, 10) == 9 + assert dra(0, 22) == 0 + assert dra(23456789, 10) == 8 + raises(ValueError, lambda: dra(24, -2)) + raises(ValueError, lambda: dra(24.2, 5)) + +def test_drm(): + assert drm(19, 12) == 7 + assert drm(2718, 10) == 2 + assert drm(0, 15) == 0 + assert drm(234161, 10) == 6 + raises(ValueError, lambda: drm(24, -2)) + raises(ValueError, lambda: drm(11.6, 9)) + + +def test_deprecated_ntheory_symbolic_functions(): + from sympy.testing.pytest import warns_deprecated_sympy + + with warns_deprecated_sympy(): + assert primenu(3) == 1 + with warns_deprecated_sympy(): + assert primeomega(3) == 1 + with warns_deprecated_sympy(): + assert totient(3) == 2 + with warns_deprecated_sympy(): + assert reduced_totient(3) == 2 + with warns_deprecated_sympy(): + assert divisor_sigma(3) == 4 + with warns_deprecated_sympy(): + assert udivisor_sigma(3) == 4 diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_generate.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_generate.py new file mode 100644 index 0000000000000000000000000000000000000000..b0e5918ffefede2e86f3be2b07d6c3a01c02e6e0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_generate.py @@ -0,0 +1,285 @@ +from bisect import bisect, bisect_left + +from sympy.functions.combinatorial.numbers import mobius, totient +from sympy.ntheory.generate import (sieve, Sieve) + +from sympy.ntheory import isprime, randprime, nextprime, prevprime, \ + primerange, primepi, prime, primorial, composite, compositepi +from sympy.ntheory.generate import cycle_length, _primepi +from sympy.ntheory.primetest import mr +from sympy.testing.pytest import raises + +def test_prime(): + assert prime(1) == 2 + assert prime(2) == 3 + assert prime(5) == 11 + assert prime(11) == 31 + assert prime(57) == 269 + assert prime(296) == 1949 + assert prime(559) == 4051 + assert prime(3000) == 27449 + assert prime(4096) == 38873 + assert prime(9096) == 94321 + assert prime(25023) == 287341 + assert prime(10000000) == 179424673 # issue #20951 + assert prime(99999999) == 2038074739 + raises(ValueError, lambda: prime(0)) + sieve.extend(3000) + assert prime(401) == 2749 + raises(ValueError, lambda: prime(-1)) + + +def test__primepi(): + assert _primepi(-1) == 0 + assert _primepi(1) == 0 + assert _primepi(2) == 1 + assert _primepi(5) == 3 + assert _primepi(11) == 5 + assert _primepi(57) == 16 + assert _primepi(296) == 62 + assert _primepi(559) == 102 + assert _primepi(3000) == 430 + assert _primepi(4096) == 564 + assert _primepi(9096) == 1128 + assert _primepi(25023) == 2763 + assert _primepi(10**8) == 5761455 + assert _primepi(253425253) == 13856396 + assert _primepi(8769575643) == 401464322 + sieve.extend(3000) + assert _primepi(2000) == 303 + + +def test_composite(): + from sympy.ntheory.generate import sieve + sieve._reset() + assert composite(1) == 4 + assert composite(2) == 6 + assert composite(5) == 10 + assert composite(11) == 20 + assert composite(41) == 58 + assert composite(57) == 80 + assert composite(296) == 370 + assert composite(559) == 684 + assert composite(3000) == 3488 + assert composite(4096) == 4736 + assert composite(9096) == 10368 + assert composite(25023) == 28088 + sieve.extend(3000) + assert composite(1957) == 2300 + assert composite(2568) == 2998 + raises(ValueError, lambda: composite(0)) + + +def test_compositepi(): + assert compositepi(1) == 0 + assert compositepi(2) == 0 + assert compositepi(5) == 1 + assert compositepi(11) == 5 + assert compositepi(57) == 40 + assert compositepi(296) == 233 + assert compositepi(559) == 456 + assert compositepi(3000) == 2569 + assert compositepi(4096) == 3531 + assert compositepi(9096) == 7967 + assert compositepi(25023) == 22259 + assert compositepi(10**8) == 94238544 + assert compositepi(253425253) == 239568856 + assert compositepi(8769575643) == 8368111320 + sieve.extend(3000) + assert compositepi(2321) == 1976 + + +def test_generate(): + from sympy.ntheory.generate import sieve + sieve._reset() + assert nextprime(-4) == 2 + assert nextprime(2) == 3 + assert nextprime(5) == 7 + assert nextprime(12) == 13 + assert prevprime(3) == 2 + assert prevprime(7) == 5 + assert prevprime(13) == 11 + assert prevprime(19) == 17 + assert prevprime(20) == 19 + + sieve.extend_to_no(9) + assert sieve._list[-1] == 23 + + assert sieve._list[-1] < 31 + assert 31 in sieve + + assert nextprime(90) == 97 + assert nextprime(10**40) == (10**40 + 121) + primelist = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, + 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, + 79, 83, 89, 97, 101, 103, 107, 109, 113, + 127, 131, 137, 139, 149, 151, 157, 163, + 167, 173, 179, 181, 191, 193, 197, 199, + 211, 223, 227, 229, 233, 239, 241, 251, + 257, 263, 269, 271, 277, 281, 283, 293] + for i in range(len(primelist) - 2): + for j in range(2, len(primelist) - i): + assert nextprime(primelist[i], j) == primelist[i + j] + if 3 < i: + assert nextprime(primelist[i] - 1, j) == primelist[i + j - 1] + raises(ValueError, lambda: nextprime(2, 0)) + raises(ValueError, lambda: nextprime(2, -1)) + assert prevprime(97) == 89 + assert prevprime(10**40) == (10**40 - 17) + + raises(ValueError, lambda: Sieve(0)) + raises(ValueError, lambda: Sieve(-1)) + for sieve_interval in [1, 10, 11, 1_000_000]: + s = Sieve(sieve_interval=sieve_interval) + for head in range(s._list[-1] + 1, (s._list[-1] + 1)**2, 2): + for tail in range(head + 1, (s._list[-1] + 1)**2): + A = list(s._primerange(head, tail)) + B = primelist[bisect(primelist, head):bisect_left(primelist, tail)] + assert A == B + for k in range(s._list[-1], primelist[-1] - 1, 2): + s = Sieve(sieve_interval=sieve_interval) + s.extend(k) + assert list(s._list) == primelist[:bisect(primelist, k)] + s.extend(primelist[-1]) + assert list(s._list) == primelist + + assert list(sieve.primerange(10, 1)) == [] + assert list(sieve.primerange(5, 9)) == [5, 7] + sieve._reset(prime=True) + assert list(sieve.primerange(2, 13)) == [2, 3, 5, 7, 11] + assert list(sieve.primerange(13)) == [2, 3, 5, 7, 11] + assert list(sieve.primerange(8)) == [2, 3, 5, 7] + assert list(sieve.primerange(-2)) == [] + assert list(sieve.primerange(29)) == [2, 3, 5, 7, 11, 13, 17, 19, 23] + assert list(sieve.primerange(34)) == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31] + + assert list(sieve.totientrange(5, 15)) == [4, 2, 6, 4, 6, 4, 10, 4, 12, 6] + sieve._reset(totient=True) + assert list(sieve.totientrange(3, 13)) == [2, 2, 4, 2, 6, 4, 6, 4, 10, 4] + assert list(sieve.totientrange(900, 1000)) == [totient(x) for x in range(900, 1000)] + assert list(sieve.totientrange(0, 1)) == [] + assert list(sieve.totientrange(1, 2)) == [1] + + assert list(sieve.mobiusrange(5, 15)) == [-1, 1, -1, 0, 0, 1, -1, 0, -1, 1] + sieve._reset(mobius=True) + assert list(sieve.mobiusrange(3, 13)) == [-1, 0, -1, 1, -1, 0, 0, 1, -1, 0] + assert list(sieve.mobiusrange(1050, 1100)) == [mobius(x) for x in range(1050, 1100)] + assert list(sieve.mobiusrange(0, 1)) == [] + assert list(sieve.mobiusrange(1, 2)) == [1] + + assert list(primerange(10, 1)) == [] + assert list(primerange(2, 7)) == [2, 3, 5] + assert list(primerange(2, 10)) == [2, 3, 5, 7] + assert list(primerange(1050, 1100)) == [1051, 1061, + 1063, 1069, 1087, 1091, 1093, 1097] + s = Sieve() + for i in range(30, 2350, 376): + for j in range(2, 5096, 1139): + A = list(s.primerange(i, i + j)) + B = list(primerange(i, i + j)) + assert A == B + s = Sieve() + sieve._reset(prime=True) + sieve.extend(13) + for i in range(200): + for j in range(i, 200): + A = list(s.primerange(i, j)) + B = list(primerange(i, j)) + assert A == B + sieve.extend(1000) + for a, b in [(901, 1103), # a < 1000 < b < 1000**2 + (806, 1002007), # a < 1000 < 1000**2 < b + (2000, 30001), # 1000 < a < b < 1000**2 + (100005, 1010001), # 1000 < a < 1000**2 < b + (1003003, 1005000), # 1000**2 < a < b + ]: + assert list(primerange(a, b)) == list(s.primerange(a, b)) + sieve._reset(prime=True) + sieve.extend(100000) + assert len(sieve._list) == len(set(sieve._list)) + s = Sieve() + assert s[10] == 29 + + assert nextprime(2, 2) == 5 + + raises(ValueError, lambda: totient(0)) + + raises(ValueError, lambda: primorial(0)) + + assert mr(1, [2]) is False + + func = lambda i: (i**2 + 1) % 51 + assert next(cycle_length(func, 4)) == (6, 3) + assert list(cycle_length(func, 4, values=True)) == \ + [4, 17, 35, 2, 5, 26, 14, 44, 50, 2, 5, 26, 14] + assert next(cycle_length(func, 4, nmax=5)) == (5, None) + assert list(cycle_length(func, 4, nmax=5, values=True)) == \ + [4, 17, 35, 2, 5] + sieve.extend(3000) + assert nextprime(2968) == 2969 + assert prevprime(2930) == 2927 + raises(ValueError, lambda: prevprime(1)) + raises(ValueError, lambda: prevprime(-4)) + + +def test_randprime(): + assert randprime(10, 1) is None + assert randprime(3, -3) is None + assert randprime(2, 3) == 2 + assert randprime(1, 3) == 2 + assert randprime(3, 5) == 3 + raises(ValueError, lambda: randprime(-12, -2)) + raises(ValueError, lambda: randprime(-10, 0)) + raises(ValueError, lambda: randprime(20, 22)) + raises(ValueError, lambda: randprime(0, 2)) + raises(ValueError, lambda: randprime(1, 2)) + for a in [100, 300, 500, 250000]: + for b in [100, 300, 500, 250000]: + p = randprime(a, a + b) + assert a <= p < (a + b) and isprime(p) + + +def test_primorial(): + assert primorial(1) == 2 + assert primorial(1, nth=0) == 1 + assert primorial(2) == 6 + assert primorial(2, nth=0) == 2 + assert primorial(4, nth=0) == 6 + + +def test_search(): + assert 2 in sieve + assert 2.1 not in sieve + assert 1 not in sieve + assert 2**1000 not in sieve + raises(ValueError, lambda: sieve.search(1)) + + +def test_sieve_slice(): + assert sieve[5] == 11 + assert list(sieve[5:10]) == [sieve[x] for x in range(5, 10)] + assert list(sieve[5:10:2]) == [sieve[x] for x in range(5, 10, 2)] + assert list(sieve[1:5]) == [2, 3, 5, 7] + raises(IndexError, lambda: sieve[:5]) + raises(IndexError, lambda: sieve[0]) + raises(IndexError, lambda: sieve[0:5]) + +def test_sieve_iter(): + values = [] + for value in sieve: + if value > 7: + break + values.append(value) + assert values == list(sieve[1:5]) + + +def test_sieve_repr(): + assert "sieve" in repr(sieve) + assert "prime" in repr(sieve) + + +def test_deprecated_ntheory_symbolic_functions(): + from sympy.testing.pytest import warns_deprecated_sympy + + with warns_deprecated_sympy(): + assert primepi(0) == 0 diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_hypothesis.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_hypothesis.py new file mode 100644 index 0000000000000000000000000000000000000000..a8f4cbecdbb7a6b15b0e323700cda11039c968fb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_hypothesis.py @@ -0,0 +1,24 @@ +from hypothesis import given +from hypothesis import strategies as st +from sympy import divisors +from sympy.functions.combinatorial.numbers import divisor_sigma, totient +from sympy.ntheory.primetest import is_square + + +@given(n=st.integers(1, 10**10)) +def test_tau_hypothesis(n): + div = divisors(n) + tau_n = len(div) + assert is_square(n) == (tau_n % 2 == 1) + sigmas = [divisor_sigma(i) for i in div] + totients = [totient(n // i) for i in div] + mul = [a * b for a, b in zip(sigmas, totients)] + assert n * tau_n == sum(mul) + + +@given(n=st.integers(1, 10**10)) +def test_totient_hypothesis(n): + assert totient(n) <= n + div = divisors(n) + totients = [totient(i) for i in div] + assert n == sum(totients) diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_modular.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_modular.py new file mode 100644 index 0000000000000000000000000000000000000000..10ebb1d3d3bdf5f736a6229579ae4c42a805745e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_modular.py @@ -0,0 +1,34 @@ +from sympy.ntheory.modular import crt, crt1, crt2, solve_congruence +from sympy.testing.pytest import raises + + +def test_crt(): + def mcrt(m, v, r, symmetric=False): + assert crt(m, v, symmetric)[0] == r + mm, e, s = crt1(m) + assert crt2(m, v, mm, e, s, symmetric) == (r, mm) + + mcrt([2, 3, 5], [0, 0, 0], 0) + mcrt([2, 3, 5], [1, 1, 1], 1) + + mcrt([2, 3, 5], [-1, -1, -1], -1, True) + mcrt([2, 3, 5], [-1, -1, -1], 2*3*5 - 1, False) + + assert crt([656, 350], [811, 133], symmetric=True) == (-56917, 114800) + + +def test_modular(): + assert solve_congruence(*list(zip([3, 4, 2], [12, 35, 17]))) == (1719, 7140) + assert solve_congruence(*list(zip([3, 4, 2], [12, 6, 17]))) is None + assert solve_congruence(*list(zip([3, 4, 2], [13, 7, 17]))) == (172, 1547) + assert solve_congruence(*list(zip([-10, -3, -15], [13, 7, 17]))) == (172, 1547) + assert solve_congruence(*list(zip([-10, -3, 1, -15], [13, 7, 7, 17]))) is None + assert solve_congruence( + *list(zip([-10, -5, 2, -15], [13, 7, 7, 17]))) == (835, 1547) + assert solve_congruence( + *list(zip([-10, -5, 2, -15], [13, 7, 14, 17]))) == (2382, 3094) + assert solve_congruence( + *list(zip([-10, 2, 2, -15], [13, 7, 14, 17]))) == (2382, 3094) + assert solve_congruence(*list(zip((1, 1, 2), (3, 2, 4)))) is None + raises( + ValueError, lambda: solve_congruence(*list(zip([3, 4, 2], [12.1, 35, 17])))) diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_multinomial.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_multinomial.py new file mode 100644 index 0000000000000000000000000000000000000000..b455c5cc979b9ba9756c9da88c1694471115cd5d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_multinomial.py @@ -0,0 +1,48 @@ +from sympy.ntheory.multinomial import (binomial_coefficients, binomial_coefficients_list, multinomial_coefficients) +from sympy.ntheory.multinomial import multinomial_coefficients_iterator + + +def test_binomial_coefficients_list(): + assert binomial_coefficients_list(0) == [1] + assert binomial_coefficients_list(1) == [1, 1] + assert binomial_coefficients_list(2) == [1, 2, 1] + assert binomial_coefficients_list(3) == [1, 3, 3, 1] + assert binomial_coefficients_list(4) == [1, 4, 6, 4, 1] + assert binomial_coefficients_list(5) == [1, 5, 10, 10, 5, 1] + assert binomial_coefficients_list(6) == [1, 6, 15, 20, 15, 6, 1] + + +def test_binomial_coefficients(): + for n in range(15): + c = binomial_coefficients(n) + l = [c[k] for k in sorted(c)] + assert l == binomial_coefficients_list(n) + + +def test_multinomial_coefficients(): + assert multinomial_coefficients(1, 1) == {(1,): 1} + assert multinomial_coefficients(1, 2) == {(2,): 1} + assert multinomial_coefficients(1, 3) == {(3,): 1} + assert multinomial_coefficients(2, 0) == {(0, 0): 1} + assert multinomial_coefficients(2, 1) == {(0, 1): 1, (1, 0): 1} + assert multinomial_coefficients(2, 2) == {(2, 0): 1, (0, 2): 1, (1, 1): 2} + assert multinomial_coefficients(2, 3) == {(3, 0): 1, (1, 2): 3, (0, 3): 1, + (2, 1): 3} + assert multinomial_coefficients(3, 1) == {(1, 0, 0): 1, (0, 1, 0): 1, + (0, 0, 1): 1} + assert multinomial_coefficients(3, 2) == {(0, 1, 1): 2, (0, 0, 2): 1, + (1, 1, 0): 2, (0, 2, 0): 1, (1, 0, 1): 2, (2, 0, 0): 1} + mc = multinomial_coefficients(3, 3) + assert mc == {(2, 1, 0): 3, (0, 3, 0): 1, + (1, 0, 2): 3, (0, 2, 1): 3, (0, 1, 2): 3, (3, 0, 0): 1, + (2, 0, 1): 3, (1, 2, 0): 3, (1, 1, 1): 6, (0, 0, 3): 1} + assert dict(multinomial_coefficients_iterator(2, 0)) == {(0, 0): 1} + assert dict( + multinomial_coefficients_iterator(2, 1)) == {(0, 1): 1, (1, 0): 1} + assert dict(multinomial_coefficients_iterator(2, 2)) == \ + {(2, 0): 1, (0, 2): 1, (1, 1): 2} + assert dict(multinomial_coefficients_iterator(3, 3)) == mc + it = multinomial_coefficients_iterator(7, 2) + assert [next(it) for i in range(4)] == \ + [((2, 0, 0, 0, 0, 0, 0), 1), ((1, 1, 0, 0, 0, 0, 0), 2), + ((0, 2, 0, 0, 0, 0, 0), 1), ((1, 0, 1, 0, 0, 0, 0), 2)] diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_partitions.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_partitions.py new file mode 100644 index 0000000000000000000000000000000000000000..8eb7fad3445068ae7ae4033c76c808e3c87347b6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_partitions.py @@ -0,0 +1,28 @@ +from sympy.ntheory.partitions_ import npartitions, _partition_rec, _partition + + +def test__partition_rec(): + A000041 = [1, 1, 2, 3, 5, 7, 11, 15, 22, 30, 42, 56, 77, 101, 135, + 176, 231, 297, 385, 490, 627, 792, 1002, 1255, 1575] + for n, val in enumerate(A000041): + assert _partition_rec(n) == val + + +def test__partition(): + assert [_partition(k) for k in range(13)] == \ + [1, 1, 2, 3, 5, 7, 11, 15, 22, 30, 42, 56, 77] + assert _partition(100) == 190569292 + assert _partition(200) == 3972999029388 + assert _partition(1000) == 24061467864032622473692149727991 + assert _partition(1001) == 25032297938763929621013218349796 + assert _partition(2000) == 4720819175619413888601432406799959512200344166 + assert _partition(10000) % 10**10 == 6916435144 + assert _partition(100000) % 10**10 == 9421098519 + assert _partition(10000000) % 10**10 == 7677288980 + + +def test_deprecated_ntheory_symbolic_functions(): + from sympy.testing.pytest import warns_deprecated_sympy + + with warns_deprecated_sympy(): + assert npartitions(0) == 1 diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_primetest.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_primetest.py new file mode 100644 index 0000000000000000000000000000000000000000..8a56332941d9421bda4d6acc1e4b3406617cee2b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_primetest.py @@ -0,0 +1,235 @@ +from math import gcd + +from sympy.ntheory.generate import Sieve, sieve +from sympy.ntheory.primetest import (mr, _lucas_extrastrong_params, is_lucas_prp, is_square, + is_strong_lucas_prp, is_extra_strong_lucas_prp, + proth_test, isprime, is_euler_pseudoprime, + is_gaussian_prime, is_fermat_pseudoprime, is_euler_jacobi_pseudoprime, + MERSENNE_PRIME_EXPONENTS, _lucas_lehmer_primality_test, + is_mersenne_prime) + +from sympy.testing.pytest import slow, raises +from sympy.core.numbers import I, Float + + +def test_is_fermat_pseudoprime(): + assert is_fermat_pseudoprime(5, 1) + assert is_fermat_pseudoprime(9, 1) + + +def test_euler_pseudoprimes(): + assert is_euler_pseudoprime(13, 1) + assert is_euler_pseudoprime(15, 1) + assert is_euler_pseudoprime(17, 6) + assert is_euler_pseudoprime(101, 7) + assert is_euler_pseudoprime(1009, 10) + assert is_euler_pseudoprime(11287, 41) + + raises(ValueError, lambda: is_euler_pseudoprime(0, 4)) + raises(ValueError, lambda: is_euler_pseudoprime(3, 0)) + raises(ValueError, lambda: is_euler_pseudoprime(15, 6)) + + # A006970 + euler_prp = [341, 561, 1105, 1729, 1905, 2047, 2465, 3277, + 4033, 4681, 5461, 6601, 8321, 8481, 10261, 10585] + for p in euler_prp: + assert is_euler_pseudoprime(p, 2) + + # A048950 + euler_prp = [121, 703, 1729, 1891, 2821, 3281, 7381, 8401, 8911, 10585, + 12403, 15457, 15841, 16531, 18721, 19345, 23521, 24661, 28009] + for p in euler_prp: + assert is_euler_pseudoprime(p, 3) + + # A033181 + absolute_euler_prp = [1729, 2465, 15841, 41041, 46657, 75361, + 162401, 172081, 399001, 449065, 488881] + for p in absolute_euler_prp: + for a in range(2, p): + if gcd(a, p) != 1: + continue + assert is_euler_pseudoprime(p, a) + + +def test_is_euler_jacobi_pseudoprime(): + assert is_euler_jacobi_pseudoprime(11, 1) + assert is_euler_jacobi_pseudoprime(15, 1) + + +def test_lucas_extrastrong_params(): + assert _lucas_extrastrong_params(3) == (5, 3, 1) + assert _lucas_extrastrong_params(5) == (12, 4, 1) + assert _lucas_extrastrong_params(7) == (5, 3, 1) + assert _lucas_extrastrong_params(9) == (0, 0, 0) + assert _lucas_extrastrong_params(11) == (21, 5, 1) + assert _lucas_extrastrong_params(59) == (32, 6, 1) + assert _lucas_extrastrong_params(479) == (117, 11, 1) + + +def test_is_extra_strong_lucas_prp(): + assert is_extra_strong_lucas_prp(4) == False + assert is_extra_strong_lucas_prp(989) == True + assert is_extra_strong_lucas_prp(10877) == True + assert is_extra_strong_lucas_prp(9) == False + assert is_extra_strong_lucas_prp(16) == False + assert is_extra_strong_lucas_prp(169) == False + +@slow +def test_prps(): + oddcomposites = [n for n in range(1, 10**5) if + n % 2 and not isprime(n)] + # A checksum would be better. + assert sum(oddcomposites) == 2045603465 + assert [n for n in oddcomposites if mr(n, [2])] == [ + 2047, 3277, 4033, 4681, 8321, 15841, 29341, 42799, 49141, + 52633, 65281, 74665, 80581, 85489, 88357, 90751] + assert [n for n in oddcomposites if mr(n, [3])] == [ + 121, 703, 1891, 3281, 8401, 8911, 10585, 12403, 16531, + 18721, 19345, 23521, 31621, 44287, 47197, 55969, 63139, + 74593, 79003, 82513, 87913, 88573, 97567] + assert [n for n in oddcomposites if mr(n, [325])] == [ + 9, 25, 27, 49, 65, 81, 325, 341, 343, 697, 1141, 2059, + 2149, 3097, 3537, 4033, 4681, 4941, 5833, 6517, 7987, 8911, + 12403, 12913, 15043, 16021, 20017, 22261, 23221, 24649, + 24929, 31841, 35371, 38503, 43213, 44173, 47197, 50041, + 55909, 56033, 58969, 59089, 61337, 65441, 68823, 72641, + 76793, 78409, 85879] + assert not any(mr(n, [9345883071009581737]) for n in oddcomposites) + assert [n for n in oddcomposites if is_lucas_prp(n)] == [ + 323, 377, 1159, 1829, 3827, 5459, 5777, 9071, 9179, 10877, + 11419, 11663, 13919, 14839, 16109, 16211, 18407, 18971, + 19043, 22499, 23407, 24569, 25199, 25877, 26069, 27323, + 32759, 34943, 35207, 39059, 39203, 39689, 40309, 44099, + 46979, 47879, 50183, 51983, 53663, 56279, 58519, 60377, + 63881, 69509, 72389, 73919, 75077, 77219, 79547, 79799, + 82983, 84419, 86063, 90287, 94667, 97019, 97439] + assert [n for n in oddcomposites if is_strong_lucas_prp(n)] == [ + 5459, 5777, 10877, 16109, 18971, 22499, 24569, 25199, 40309, + 58519, 75077, 97439] + assert [n for n in oddcomposites if is_extra_strong_lucas_prp(n) + ] == [ + 989, 3239, 5777, 10877, 27971, 29681, 30739, 31631, 39059, + 72389, 73919, 75077] + + +def test_proth_test(): + # Proth number + A080075 = [3, 5, 9, 13, 17, 25, 33, 41, 49, 57, 65, + 81, 97, 113, 129, 145, 161, 177, 193] + # Proth prime + A080076 = [3, 5, 13, 17, 41, 97, 113, 193] + + for n in range(200): + if n in A080075: + assert proth_test(n) == (n in A080076) + else: + raises(ValueError, lambda: proth_test(n)) + + +def test_lucas_lehmer_primality_test(): + for p in sieve.primerange(3, 100): + assert _lucas_lehmer_primality_test(p) == (p in MERSENNE_PRIME_EXPONENTS) + + +def test_is_mersenne_prime(): + assert is_mersenne_prime(-3) is False + assert is_mersenne_prime(3) is True + assert is_mersenne_prime(10) is False + assert is_mersenne_prime(127) is True + assert is_mersenne_prime(511) is False + assert is_mersenne_prime(131071) is True + assert is_mersenne_prime(2147483647) is True + + +def test_isprime(): + s = Sieve() + s.extend(100000) + ps = set(s.primerange(2, 100001)) + for n in range(100001): + # if (n in ps) != isprime(n): print n + assert (n in ps) == isprime(n) + assert isprime(179424673) + assert isprime(20678048681) + assert isprime(1968188556461) + assert isprime(2614941710599) + assert isprime(65635624165761929287) + assert isprime(1162566711635022452267983) + assert isprime(77123077103005189615466924501) + assert isprime(3991617775553178702574451996736229) + assert isprime(273952953553395851092382714516720001799) + assert isprime(int(''' +531137992816767098689588206552468627329593117727031923199444138200403\ +559860852242739162502265229285668889329486246501015346579337652707239\ +409519978766587351943831270835393219031728127''')) + + # Some Mersenne primes + assert isprime(2**61 - 1) + assert isprime(2**89 - 1) + assert isprime(2**607 - 1) + # (but not all Mersenne's are primes + assert not isprime(2**601 - 1) + + # pseudoprimes + #------------- + # to some small bases + assert not isprime(2152302898747) + assert not isprime(3474749660383) + assert not isprime(341550071728321) + assert not isprime(3825123056546413051) + # passes the base set [2, 3, 7, 61, 24251] + assert not isprime(9188353522314541) + # large examples + assert not isprime(877777777777777777777777) + # conjectured psi_12 given at http://mathworld.wolfram.com/StrongPseudoprime.html + assert not isprime(318665857834031151167461) + # conjectured psi_17 given at http://mathworld.wolfram.com/StrongPseudoprime.html + assert not isprime(564132928021909221014087501701) + # Arnault's 1993 number; a factor of it is + # 400958216639499605418306452084546853005188166041132508774506\ + # 204738003217070119624271622319159721973358216316508535816696\ + # 9145233813917169287527980445796800452592031836601 + assert not isprime(int(''' +803837457453639491257079614341942108138837688287558145837488917522297\ +427376533365218650233616396004545791504202360320876656996676098728404\ +396540823292873879185086916685732826776177102938969773947016708230428\ +687109997439976544144845341155872450633409279022275296229414984230688\ +1685404326457534018329786111298960644845216191652872597534901''')) + # Arnault's 1995 number; can be factored as + # p1*(313*(p1 - 1) + 1)*(353*(p1 - 1) + 1) where p1 is + # 296744956686855105501541746429053327307719917998530433509950\ + # 755312768387531717701995942385964281211880336647542183455624\ + # 93168782883 + assert not isprime(int(''' +288714823805077121267142959713039399197760945927972270092651602419743\ +230379915273311632898314463922594197780311092934965557841894944174093\ +380561511397999942154241693397290542371100275104208013496673175515285\ +922696291677532547504444585610194940420003990443211677661994962953925\ +045269871932907037356403227370127845389912612030924484149472897688540\ +6024976768122077071687938121709811322297802059565867''')) + sieve.extend(3000) + assert isprime(2819) + assert not isprime(2931) + raises(ValueError, lambda: isprime(2.0)) + raises(ValueError, lambda: isprime(Float(2))) + + +def test_is_square(): + assert [i for i in range(25) if is_square(i)] == [0, 1, 4, 9, 16] + + # issue #17044 + assert not is_square(60 ** 3) + assert not is_square(60 ** 5) + assert not is_square(84 ** 7) + assert not is_square(105 ** 9) + assert not is_square(120 ** 3) + +def test_is_gaussianprime(): + assert is_gaussian_prime(7*I) + assert is_gaussian_prime(7) + assert is_gaussian_prime(2 + 3*I) + assert not is_gaussian_prime(2 + 2*I) + + +def test_issue_27145(): + #https://github.com/sympy/sympy/issues/27145 + assert [mr(i,[2,3,5,7]) for i in (1, 2, 6)] == [False, True, False] diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_qs.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_qs.py new file mode 100644 index 0000000000000000000000000000000000000000..16932dd61badf4a467e67fa52e0f473594fd057b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_qs.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +import math +from sympy.core.random import _randint +from sympy.ntheory import qs, qs_factor +from sympy.ntheory.qs import SievePolynomial, _generate_factor_base, \ + _generate_polynomial, \ + _gen_sieve_array, _check_smoothness, _trial_division_stage, _find_factor +from sympy.testing.pytest import slow + + +@slow +def test_qs_1(): + assert qs(10009202107, 100, 10000) == {100043, 100049} + assert qs(211107295182713951054568361, 1000, 10000) == \ + {13791315212531, 15307263442931} + assert qs(980835832582657*990377764891511, 2000, 10000) == \ + {980835832582657, 990377764891511} + assert qs(18640889198609*20991129234731, 1000, 50000) == \ + {18640889198609, 20991129234731} + + +def test_qs_2() -> None: + n = 10009202107 + M = 50 + sieve_poly = SievePolynomial(10, 80, n) + assert sieve_poly.eval_v(10) == sieve_poly.eval_u(10)**2 - n == -10009169707 + assert sieve_poly.eval_v(5) == sieve_poly.eval_u(5)**2 - n == -10009185207 + + idx_1000, idx_5000, factor_base = _generate_factor_base(2000, n) + assert idx_1000 == 82 + assert [factor_base[i].prime for i in range(15)] == \ + [2, 3, 7, 11, 17, 19, 29, 31, 43, 59, 61, 67, 71, 73, 79] + assert [factor_base[i].tmem_p for i in range(15)] == \ + [1, 1, 3, 5, 3, 6, 6, 14, 1, 16, 24, 22, 18, 22, 15] + assert [factor_base[i].log_p for i in range(5)] == \ + [710, 1125, 1993, 2455, 2901] + + it = _generate_polynomial( + n, M, factor_base, idx_1000, idx_5000, _randint(0)) + g = next(it) + assert g.a == 1133107 + assert g.b == 682543 + assert [factor_base[i].soln1 for i in range(15)] == \ + [0, 0, 3, 7, 13, 0, 8, 19, 9, 43, 27, 25, 63, 29, 19] + assert [factor_base[i].soln2 for i in range(15)] == \ + [0, 1, 1, 3, 12, 16, 15, 6, 15, 1, 56, 55, 61, 58, 16] + assert [factor_base[i].b_ainv for i in range(5)] == \ + [[0, 0], [0, 2], [3, 0], [3, 9], [13, 13]] + + g_1 = next(it) + assert g_1.a == 1133107 + assert g_1.b == 136765 + + sieve_array = _gen_sieve_array(M, factor_base) + assert sieve_array[0:5] == [8424, 13603, 1835, 5335, 710] + + assert _check_smoothness(9645, factor_base) == (36028797018963972, 5) + assert _check_smoothness(210313, factor_base) == (20992, 1) + + partial_relations: dict[int, tuple[int, int]] = {} + smooth_relation, proper_factor = _trial_division_stage( + n, M, factor_base, sieve_array, sieve_poly, partial_relations, + ERROR_TERM=25*2**10) + + assert partial_relations == { + 8699: (440, -10009008507, 75557863761098695507973), + 166741: (490, -10008962007, 524341), + 131449: (530, -10008921207, 664613997892457936451903530140172325), + 6653: (550, -10008899607, 19342813113834066795307021) + } + assert [smooth_relation[i][0] for i in range(5)] == [ + -250, 1064469, 72819, 231957, 44167] + assert [smooth_relation[i][1] for i in range(5)] == [ + -10009139607, 1133094251961, 5302606761, 53804049849, 1950723889] + assert smooth_relation[0][2] == 89213869829863962596973701078031812362502145 + assert proper_factor == set() + + +def test_qs_3(): + N = 1817 + smooth_relations = [ + (2455024, 637, 8), + (-27993000, 81536, 10), + (11461840, 12544, 0), + (149, 20384, 10), + (-31138074, 19208, 2) + ] + assert next(_find_factor(N, smooth_relations, 4)) == 23 + + +def test_qs_4(): + N = 10007**2 * 10009 * 10037**3 * 10039 + for factor in qs(N, 1000, 2000): + assert N % factor == 0 + N //= factor + + +def test_qs_factor(): + assert qs_factor(1009 * 100003, 2000, 10000) == {1009: 1, 100003: 1} + n = 1009**2 * 2003**2*30011*400009 + factors = qs_factor(n, 2000, 10000) + assert len(factors) > 1 + assert math.prod(p**e for p, e in factors.items()) == n + + +def test_issue_27616(): + #https://github.com/sympy/sympy/issues/27616 + N = 9804659461513846513 + 1 + assert qs(N, 5000, 20000) is not None diff --git a/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_residue.py b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_residue.py new file mode 100644 index 0000000000000000000000000000000000000000..4d530905f39b88d8d7cc0e861ac6eadb2fa6f98a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/ntheory/tests/test_residue.py @@ -0,0 +1,349 @@ +from collections import defaultdict +from sympy.core.containers import Tuple +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol) +from sympy.functions.combinatorial.numbers import totient +from sympy.ntheory import n_order, is_primitive_root, is_quad_residue, \ + legendre_symbol, jacobi_symbol, primerange, sqrt_mod, \ + primitive_root, quadratic_residues, is_nthpow_residue, nthroot_mod, \ + sqrt_mod_iter, mobius, discrete_log, quadratic_congruence, \ + polynomial_congruence, sieve +from sympy.ntheory.residue_ntheory import _primitive_root_prime_iter, \ + _primitive_root_prime_power_iter, _primitive_root_prime_power2_iter, \ + _nthroot_mod_prime_power, _discrete_log_trial_mul, _discrete_log_shanks_steps, \ + _discrete_log_pollard_rho, _discrete_log_index_calculus, _discrete_log_pohlig_hellman, \ + _binomial_mod_prime_power, binomial_mod +from sympy.polys.domains import ZZ +from sympy.testing.pytest import raises +from sympy.core.random import randint, choice + + +def test_residue(): + assert n_order(2, 13) == 12 + assert [n_order(a, 7) for a in range(1, 7)] == \ + [1, 3, 6, 3, 6, 2] + assert n_order(5, 17) == 16 + assert n_order(17, 11) == n_order(6, 11) + assert n_order(101, 119) == 6 + assert n_order(11, (10**50 + 151)**2) == 10000000000000000000000000000000000000000000000030100000000000000000000000000000000000000000000022650 + raises(ValueError, lambda: n_order(6, 9)) + + assert is_primitive_root(2, 7) is False + assert is_primitive_root(3, 8) is False + assert is_primitive_root(11, 14) is False + assert is_primitive_root(12, 17) == is_primitive_root(29, 17) + raises(ValueError, lambda: is_primitive_root(3, 6)) + + for p in primerange(3, 100): + li = list(_primitive_root_prime_iter(p)) + assert li[0] == min(li) + for g in li: + assert n_order(g, p) == p - 1 + assert len(li) == totient(totient(p)) + for e in range(1, 4): + li_power = list(_primitive_root_prime_power_iter(p, e)) + li_power2 = list(_primitive_root_prime_power2_iter(p, e)) + assert len(li_power) == len(li_power2) == totient(totient(p**e)) + assert primitive_root(97) == 5 + assert n_order(primitive_root(97, False), 97) == totient(97) + assert primitive_root(97**2) == 5 + assert n_order(primitive_root(97**2, False), 97**2) == totient(97**2) + assert primitive_root(40487) == 5 + assert n_order(primitive_root(40487, False), 40487) == totient(40487) + # note that primitive_root(40487) + 40487 = 40492 is a primitive root + # of 40487**2, but it is not the smallest + assert primitive_root(40487**2) == 10 + assert n_order(primitive_root(40487**2, False), 40487**2) == totient(40487**2) + assert primitive_root(82) == 7 + assert n_order(primitive_root(82, False), 82) == totient(82) + p = 10**50 + 151 + assert primitive_root(p) == 11 + assert n_order(primitive_root(p, False), p) == totient(p) + assert primitive_root(2*p) == 11 + assert n_order(primitive_root(2*p, False), 2*p) == totient(2*p) + assert primitive_root(p**2) == 11 + assert n_order(primitive_root(p**2, False), p**2) == totient(p**2) + assert primitive_root(4 * 11) is None and primitive_root(4 * 11, False) is None + assert primitive_root(15) is None and primitive_root(15, False) is None + raises(ValueError, lambda: primitive_root(-3)) + + assert is_quad_residue(3, 7) is False + assert is_quad_residue(10, 13) is True + assert is_quad_residue(12364, 139) == is_quad_residue(12364 % 139, 139) + assert is_quad_residue(207, 251) is True + assert is_quad_residue(0, 1) is True + assert is_quad_residue(1, 1) is True + assert is_quad_residue(0, 2) == is_quad_residue(1, 2) is True + assert is_quad_residue(1, 4) is True + assert is_quad_residue(2, 27) is False + assert is_quad_residue(13122380800, 13604889600) is True + assert [j for j in range(14) if is_quad_residue(j, 14)] == \ + [0, 1, 2, 4, 7, 8, 9, 11] + raises(ValueError, lambda: is_quad_residue(1.1, 2)) + raises(ValueError, lambda: is_quad_residue(2, 0)) + + assert quadratic_residues(S.One) == [0] + assert quadratic_residues(1) == [0] + assert quadratic_residues(12) == [0, 1, 4, 9] + assert quadratic_residues(13) == [0, 1, 3, 4, 9, 10, 12] + assert [len(quadratic_residues(i)) for i in range(1, 20)] == \ + [1, 2, 2, 2, 3, 4, 4, 3, 4, 6, 6, 4, 7, 8, 6, 4, 9, 8, 10] + + assert list(sqrt_mod_iter(6, 2)) == [0] + assert sqrt_mod(3, 13) == 4 + assert sqrt_mod(3, -13) == 4 + assert sqrt_mod(6, 23) == 11 + assert sqrt_mod(345, 690) == 345 + assert sqrt_mod(67, 101) == None + assert sqrt_mod(1020, 104729) == None + + for p in range(3, 100): + d = defaultdict(list) + for i in range(p): + d[pow(i, 2, p)].append(i) + for i in range(1, p): + it = sqrt_mod_iter(i, p) + v = sqrt_mod(i, p, True) + if v: + v = sorted(v) + assert d[i] == v + else: + assert not d[i] + + assert sqrt_mod(9, 27, True) == [3, 6, 12, 15, 21, 24] + assert sqrt_mod(9, 81, True) == [3, 24, 30, 51, 57, 78] + assert sqrt_mod(9, 3**5, True) == [3, 78, 84, 159, 165, 240] + assert sqrt_mod(81, 3**4, True) == [0, 9, 18, 27, 36, 45, 54, 63, 72] + assert sqrt_mod(81, 3**5, True) == [9, 18, 36, 45, 63, 72, 90, 99, 117,\ + 126, 144, 153, 171, 180, 198, 207, 225, 234] + assert sqrt_mod(81, 3**6, True) == [9, 72, 90, 153, 171, 234, 252, 315,\ + 333, 396, 414, 477, 495, 558, 576, 639, 657, 720] + assert sqrt_mod(81, 3**7, True) == [9, 234, 252, 477, 495, 720, 738, 963,\ + 981, 1206, 1224, 1449, 1467, 1692, 1710, 1935, 1953, 2178] + + for a, p in [(26214400, 32768000000), (26214400, 16384000000), + (262144, 1048576), (87169610025, 163443018796875), + (22315420166400, 167365651248000000)]: + assert pow(sqrt_mod(a, p), 2, p) == a + + n = 70 + a, p = 5**2*3**n*2**n, 5**6*3**(n+1)*2**(n+2) + it = sqrt_mod_iter(a, p) + for i in range(10): + assert pow(next(it), 2, p) == a + a, p = 5**2*3**n*2**n, 5**6*3**(n+1)*2**(n+3) + it = sqrt_mod_iter(a, p) + for i in range(2): + assert pow(next(it), 2, p) == a + n = 100 + a, p = 5**2*3**n*2**n, 5**6*3**(n+1)*2**(n+1) + it = sqrt_mod_iter(a, p) + for i in range(2): + assert pow(next(it), 2, p) == a + + assert type(next(sqrt_mod_iter(9, 27))) is int + assert type(next(sqrt_mod_iter(9, 27, ZZ))) is type(ZZ(1)) + assert type(next(sqrt_mod_iter(1, 7, ZZ))) is type(ZZ(1)) + + assert is_nthpow_residue(2, 1, 5) + + #issue 10816 + assert is_nthpow_residue(1, 0, 1) is False + assert is_nthpow_residue(1, 0, 2) is True + assert is_nthpow_residue(3, 0, 2) is True + assert is_nthpow_residue(0, 1, 8) is True + assert is_nthpow_residue(2, 3, 2) is True + assert is_nthpow_residue(2, 3, 9) is False + assert is_nthpow_residue(3, 5, 30) is True + assert is_nthpow_residue(21, 11, 20) is True + assert is_nthpow_residue(7, 10, 20) is False + assert is_nthpow_residue(5, 10, 20) is True + assert is_nthpow_residue(3, 10, 48) is False + assert is_nthpow_residue(1, 10, 40) is True + assert is_nthpow_residue(3, 10, 24) is False + assert is_nthpow_residue(1, 10, 24) is True + assert is_nthpow_residue(3, 10, 24) is False + assert is_nthpow_residue(2, 10, 48) is False + assert is_nthpow_residue(81, 3, 972) is False + assert is_nthpow_residue(243, 5, 5103) is True + assert is_nthpow_residue(243, 3, 1240029) is False + assert is_nthpow_residue(36010, 8, 87382) is True + assert is_nthpow_residue(28552, 6, 2218) is True + assert is_nthpow_residue(92712, 9, 50026) is True + x = {pow(i, 56, 1024) for i in range(1024)} + assert {a for a in range(1024) if is_nthpow_residue(a, 56, 1024)} == x + x = { pow(i, 256, 2048) for i in range(2048)} + assert {a for a in range(2048) if is_nthpow_residue(a, 256, 2048)} == x + x = { pow(i, 11, 324000) for i in range(1000)} + assert [ is_nthpow_residue(a, 11, 324000) for a in x] + x = { pow(i, 17, 22217575536) for i in range(1000)} + assert [ is_nthpow_residue(a, 17, 22217575536) for a in x] + assert is_nthpow_residue(676, 3, 5364) + assert is_nthpow_residue(9, 12, 36) + assert is_nthpow_residue(32, 10, 41) + assert is_nthpow_residue(4, 2, 64) + assert is_nthpow_residue(31, 4, 41) + assert not is_nthpow_residue(2, 2, 5) + assert is_nthpow_residue(8547, 12, 10007) + assert is_nthpow_residue(Dummy(even=True) + 3, 3, 2) == True + # _nthroot_mod_prime_power + for p in primerange(2, 10): + for a in range(3): + for n in range(3, 5): + ans = _nthroot_mod_prime_power(a, n, p, 1) + assert isinstance(ans, list) + if len(ans) == 0: + for b in range(p): + assert pow(b, n, p) != a % p + for k in range(2, 10): + assert _nthroot_mod_prime_power(a, n, p, k) == [] + else: + for b in range(p): + pred = pow(b, n, p) == a % p + assert not(pred ^ (b in ans)) + for k in range(2, 10): + ans = _nthroot_mod_prime_power(a, n, p, k) + if not ans: + break + for b in ans: + assert pow(b, n , p**k) == a + + assert nthroot_mod(Dummy(odd=True), 3, 2) == 1 + assert nthroot_mod(29, 31, 74) == 45 + assert nthroot_mod(1801, 11, 2663) == 44 + for a, q, p in [(51922, 2, 203017), (43, 3, 109), (1801, 11, 2663), + (26118163, 1303, 33333347), (1499, 7, 2663), (595, 6, 2663), + (1714, 12, 2663), (28477, 9, 33343)]: + r = nthroot_mod(a, q, p) + assert pow(r, q, p) == a + assert nthroot_mod(11, 3, 109) is None + assert nthroot_mod(16, 5, 36, True) == [4, 22] + assert nthroot_mod(9, 16, 36, True) == [3, 9, 15, 21, 27, 33] + assert nthroot_mod(4, 3, 3249000) is None + assert nthroot_mod(36010, 8, 87382, True) == [40208, 47174] + assert nthroot_mod(0, 12, 37, True) == [0] + assert nthroot_mod(0, 7, 100, True) == [0, 10, 20, 30, 40, 50, 60, 70, 80, 90] + assert nthroot_mod(4, 4, 27, True) == [5, 22] + assert nthroot_mod(4, 4, 121, True) == [19, 102] + assert nthroot_mod(2, 3, 7, True) == [] + for p in range(1, 20): + for a in range(p): + for n in range(1, p): + ans = nthroot_mod(a, n, p, True) + assert isinstance(ans, list) + for b in range(p): + pred = pow(b, n, p) == a + assert not(pred ^ (b in ans)) + ans2 = nthroot_mod(a, n, p, False) + if ans2 is None: + assert ans == [] + else: + assert ans2 in ans + + x = Symbol('x', positive=True) + i = Symbol('i', integer=True) + assert _discrete_log_trial_mul(587, 2**7, 2) == 7 + assert _discrete_log_trial_mul(941, 7**18, 7) == 18 + assert _discrete_log_trial_mul(389, 3**81, 3) == 81 + assert _discrete_log_trial_mul(191, 19**123, 19) == 123 + assert _discrete_log_shanks_steps(442879, 7**2, 7) == 2 + assert _discrete_log_shanks_steps(874323, 5**19, 5) == 19 + assert _discrete_log_shanks_steps(6876342, 7**71, 7) == 71 + assert _discrete_log_shanks_steps(2456747, 3**321, 3) == 321 + assert _discrete_log_pollard_rho(6013199, 2**6, 2, rseed=0) == 6 + assert _discrete_log_pollard_rho(6138719, 2**19, 2, rseed=0) == 19 + assert _discrete_log_pollard_rho(36721943, 2**40, 2, rseed=0) == 40 + assert _discrete_log_pollard_rho(24567899, 3**333, 3, rseed=0) == 333 + raises(ValueError, lambda: _discrete_log_pollard_rho(11, 7, 31, rseed=0)) + raises(ValueError, lambda: _discrete_log_pollard_rho(227, 3**7, 5, rseed=0)) + assert _discrete_log_index_calculus(983, 948, 2, 491) == 183 + assert _discrete_log_index_calculus(633383, 21794, 2, 316691) == 68048 + assert _discrete_log_index_calculus(941762639, 68822582, 2, 470881319) == 338029275 + assert _discrete_log_index_calculus(999231337607, 888188918786, 2, 499615668803) == 142811376514 + assert _discrete_log_index_calculus(47747730623, 19410045286, 43425105668, 645239603) == 590504662 + assert _discrete_log_pohlig_hellman(98376431, 11**9, 11) == 9 + assert _discrete_log_pohlig_hellman(78723213, 11**31, 11) == 31 + assert _discrete_log_pohlig_hellman(32942478, 11**98, 11) == 98 + assert _discrete_log_pohlig_hellman(14789363, 11**444, 11) == 444 + assert discrete_log(1, 0, 2) == 0 + raises(ValueError, lambda: discrete_log(-4, 1, 3)) + raises(ValueError, lambda: discrete_log(10, 3, 2)) + assert discrete_log(587, 2**9, 2) == 9 + assert discrete_log(2456747, 3**51, 3) == 51 + assert discrete_log(32942478, 11**127, 11) == 127 + assert discrete_log(432751500361, 7**324, 7) == 324 + assert discrete_log(265390227570863,184500076053622, 2) == 17835221372061 + assert discrete_log(22708823198678103974314518195029102158525052496759285596453269189798311427475159776411276642277139650833937, + 17463946429475485293747680247507700244427944625055089103624311227422110546803452417458985046168310373075327, + 123456) == 2068031853682195777930683306640554533145512201725884603914601918777510185469769997054750835368413389728895 + args = 5779, 3528, 6215 + assert discrete_log(*args) == 687 + assert discrete_log(*Tuple(*args)) == 687 + assert quadratic_congruence(400, 85, 125, 1600) == [295, 615, 935, 1255, 1575] + assert quadratic_congruence(3, 6, 5, 25) == [3, 20] + assert quadratic_congruence(120, 80, 175, 500) == [] + assert quadratic_congruence(15, 14, 7, 2) == [1] + assert quadratic_congruence(8, 15, 7, 29) == [10, 28] + assert quadratic_congruence(160, 200, 300, 461) == [144, 431] + assert quadratic_congruence(100000, 123456, 7415263, 48112959837082048697) == [30417843635344493501, 36001135160550533083] + assert quadratic_congruence(65, 121, 72, 277) == [249, 252] + assert quadratic_congruence(5, 10, 14, 2) == [0] + assert quadratic_congruence(10, 17, 19, 2) == [1] + assert quadratic_congruence(10, 14, 20, 2) == [0, 1] + assert quadratic_congruence(2**48-7, 2**48-1, 4, 2**48) == [8249717183797, 31960993774868] + assert polynomial_congruence(6*x**5 + 10*x**4 + 5*x**3 + x**2 + x + 1, + 972000) == [220999, 242999, 463999, 485999, 706999, 728999, 949999, 971999] + + assert polynomial_congruence(x**3 - 10*x**2 + 12*x - 82, 33075) == [30287] + assert polynomial_congruence(x**2 + x + 47, 2401) == [785, 1615] + assert polynomial_congruence(10*x**2 + 14*x + 20, 2) == [0, 1] + assert polynomial_congruence(x**3 + 3, 16) == [5] + assert polynomial_congruence(65*x**2 + 121*x + 72, 277) == [249, 252] + assert polynomial_congruence(x**4 - 4, 27) == [5, 22] + assert polynomial_congruence(35*x**3 - 6*x**2 - 567*x + 2308, 148225) == [86957, + 111157, 122531, 146731] + assert polynomial_congruence(x**16 - 9, 36) == [3, 9, 15, 21, 27, 33] + assert polynomial_congruence(x**6 - 2*x**5 - 35, 6125) == [3257] + raises(ValueError, lambda: polynomial_congruence(x**x, 6125)) + raises(ValueError, lambda: polynomial_congruence(x**i, 6125)) + raises(ValueError, lambda: polynomial_congruence(0.1*x**2 + 6, 100)) + + assert binomial_mod(-1, 1, 10) == 0 + assert binomial_mod(1, -1, 10) == 0 + raises(ValueError, lambda: binomial_mod(2, 1, -1)) + assert binomial_mod(51, 10, 10) == 0 + assert binomial_mod(10**3, 500, 3**6) == 567 + assert binomial_mod(10**18 - 1, 123456789, 4) == 0 + assert binomial_mod(10**18, 10**12, (10**5 + 3)**2) == 3744312326 + + +def test_binomial_p_pow(): + n, binomials, binomial = 1000, [1], 1 + for i in range(1, n + 1): + binomial *= n - i + 1 + binomial //= i + binomials.append(binomial) + + # Test powers of two, which the algorithm treats slightly differently + trials_2 = 100 + for _ in range(trials_2): + m, power = randint(0, n), randint(1, 20) + assert _binomial_mod_prime_power(n, m, 2, power) == binomials[m] % 2**power + + # Test against other prime powers + primes = list(sieve.primerange(2*n)) + trials = 1000 + for _ in range(trials): + m, prime, power = randint(0, n), choice(primes), randint(1, 10) + assert _binomial_mod_prime_power(n, m, prime, power) == binomials[m] % prime**power + + +def test_deprecated_ntheory_symbolic_functions(): + from sympy.testing.pytest import warns_deprecated_sympy + + with warns_deprecated_sympy(): + assert mobius(3) == -1 + with warns_deprecated_sympy(): + assert legendre_symbol(2, 3) == -1 + with warns_deprecated_sympy(): + assert jacobi_symbol(2, 3) == -1 diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/__init__.py b/venv/lib/python3.11/site-packages/sympy/parsing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b39d031bca26bc599eb9eb0e12dfe48f7e6db174 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/__init__.py @@ -0,0 +1,4 @@ +"""Used for translating a string into a SymPy expression. """ +__all__ = ['parse_expr'] + +from .sympy_parser import parse_expr diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/ast_parser.py b/venv/lib/python3.11/site-packages/sympy/parsing/ast_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..95a773d5bec6e130810b7b7925fdff57270aec17 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/ast_parser.py @@ -0,0 +1,79 @@ +""" +This module implements the functionality to take any Python expression as a +string and fix all numbers and other things before evaluating it, +thus + +1/2 + +returns + +Integer(1)/Integer(2) + +We use the ast module for this. It is well documented at docs.python.org. + +Some tips to understand how this works: use dump() to get a nice +representation of any node. Then write a string of what you want to get, +e.g. "Integer(1)", parse it, dump it and you'll see that you need to do +"Call(Name('Integer', Load()), [node], [], None, None)". You do not need +to bother with lineno and col_offset, just call fix_missing_locations() +before returning the node. +""" + +from sympy.core.basic import Basic +from sympy.core.sympify import SympifyError + +from ast import parse, NodeTransformer, Call, Name, Load, \ + fix_missing_locations, Constant, Tuple + +class Transform(NodeTransformer): + + def __init__(self, local_dict, global_dict): + NodeTransformer.__init__(self) + self.local_dict = local_dict + self.global_dict = global_dict + + def visit_Constant(self, node): + if isinstance(node.value, int): + return fix_missing_locations(Call(func=Name('Integer', Load()), + args=[node], keywords=[])) + elif isinstance(node.value, float): + return fix_missing_locations(Call(func=Name('Float', Load()), + args=[node], keywords=[])) + return node + + def visit_Name(self, node): + if node.id in self.local_dict: + return node + elif node.id in self.global_dict: + name_obj = self.global_dict[node.id] + + if isinstance(name_obj, (Basic, type)) or callable(name_obj): + return node + elif node.id in ['True', 'False']: + return node + return fix_missing_locations(Call(func=Name('Symbol', Load()), + args=[Constant(node.id)], keywords=[])) + + def visit_Lambda(self, node): + args = [self.visit(arg) for arg in node.args.args] + body = self.visit(node.body) + n = Call(func=Name('Lambda', Load()), + args=[Tuple(args, Load()), body], keywords=[]) + return fix_missing_locations(n) + +def parse_expr(s, local_dict): + """ + Converts the string "s" to a SymPy expression, in local_dict. + + It converts all numbers to Integers before feeding it to Python and + automatically creates Symbols. + """ + global_dict = {} + exec('from sympy import *', global_dict) + try: + a = parse(s.strip(), mode="eval") + except SyntaxError: + raise SympifyError("Cannot parse %s." % repr(s)) + a = Transform(local_dict, global_dict).visit(a) + e = compile(a, "", "eval") + return eval(e, global_dict, local_dict) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/Autolev.g4 b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/Autolev.g4 new file mode 100644 index 0000000000000000000000000000000000000000..94feea5fa4f49e9d1054eca2cd60c996aebff7c2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/Autolev.g4 @@ -0,0 +1,118 @@ +grammar Autolev; + +options { + language = Python3; +} + +prog: stat+; + +stat: varDecl + | functionCall + | codeCommands + | massDecl + | inertiaDecl + | assignment + | settings + ; + +assignment: vec equals expr #vecAssign + | ID '[' index ']' equals expr #indexAssign + | ID diff? equals expr #regularAssign; + +equals: ('='|'+='|'-='|':='|'*='|'/='|'^='); + +index: expr (',' expr)* ; + +diff: ('\'')+; + +functionCall: ID '(' (expr (',' expr)*)? ')' + | (Mass|Inertia) '(' (ID (',' ID)*)? ')'; + +varDecl: varType varDecl2 (',' varDecl2)*; + +varType: Newtonian|Frames|Bodies|Particles|Points|Constants + | Specifieds|Imaginary|Variables ('\'')*|MotionVariables ('\'')*; + +varDecl2: ID ('{' INT ',' INT '}')? (('{' INT ':' INT (',' INT ':' INT)* '}'))? ('{' INT '}')? ('+'|'-')? ('\'')* ('=' expr)?; + +ranges: ('{' INT ':' INT (',' INT ':' INT)* '}'); + +massDecl: Mass massDecl2 (',' massDecl2)*; + +massDecl2: ID '=' expr; + +inertiaDecl: Inertia ID ('(' ID ')')? (',' expr)+; + +matrix: '[' expr ((','|';') expr)* ']'; +matrixInOutput: (ID (ID '=' (FLOAT|INT)?))|FLOAT|INT; + +codeCommands: units + | inputs + | outputs + | codegen + | commands; + +settings: ID (EXP|ID|FLOAT|INT)?; + +units: UnitSystem ID (',' ID)*; +inputs: Input inputs2 (',' inputs2)*; +id_diff: ID diff?; +inputs2: id_diff '=' expr expr?; +outputs: Output outputs2 (',' outputs2)*; +outputs2: expr expr?; +codegen: ID functionCall ('['matrixInOutput (',' matrixInOutput)*']')? ID'.'ID; + +commands: Save ID'.'ID + | Encode ID (',' ID)*; + +vec: ID ('>')+ + | '0>' + | '1>>'; + +expr: expr '^' expr # Exponent + | expr ('*'|'/') expr # MulDiv + | expr ('+'|'-') expr # AddSub + | EXP # exp + | '-' expr # negativeOne + | FLOAT # float + | INT # int + | ID('\'')* # id + | vec # VectorOrDyadic + | ID '['expr (',' expr)* ']' # Indexing + | functionCall # function + | matrix # matrices + | '(' expr ')' # parens + | expr '=' expr # idEqualsExpr + | expr ':' expr # colon + | ID? ranges ('\'')* # rangess + ; + +// These are to take care of the case insensitivity of Autolev. +Mass: ('M'|'m')('A'|'a')('S'|'s')('S'|'s'); +Inertia: ('I'|'i')('N'|'n')('E'|'e')('R'|'r')('T'|'t')('I'|'i')('A'|'a'); +Input: ('I'|'i')('N'|'n')('P'|'p')('U'|'u')('T'|'t')('S'|'s')?; +Output: ('O'|'o')('U'|'u')('T'|'t')('P'|'p')('U'|'u')('T'|'t'); +Save: ('S'|'s')('A'|'a')('V'|'v')('E'|'e'); +UnitSystem: ('U'|'u')('N'|'n')('I'|'i')('T'|'t')('S'|'s')('Y'|'y')('S'|'s')('T'|'t')('E'|'e')('M'|'m'); +Encode: ('E'|'e')('N'|'n')('C'|'c')('O'|'o')('D'|'d')('E'|'e'); +Newtonian: ('N'|'n')('E'|'e')('W'|'w')('T'|'t')('O'|'o')('N'|'n')('I'|'i')('A'|'a')('N'|'n'); +Frames: ('F'|'f')('R'|'r')('A'|'a')('M'|'m')('E'|'e')('S'|'s')?; +Bodies: ('B'|'b')('O'|'o')('D'|'d')('I'|'i')('E'|'e')('S'|'s')?; +Particles: ('P'|'p')('A'|'a')('R'|'r')('T'|'t')('I'|'i')('C'|'c')('L'|'l')('E'|'e')('S'|'s')?; +Points: ('P'|'p')('O'|'o')('I'|'i')('N'|'n')('T'|'t')('S'|'s')?; +Constants: ('C'|'c')('O'|'o')('N'|'n')('S'|'s')('T'|'t')('A'|'a')('N'|'n')('T'|'t')('S'|'s')?; +Specifieds: ('S'|'s')('P'|'p')('E'|'e')('C'|'c')('I'|'i')('F'|'f')('I'|'i')('E'|'e')('D'|'d')('S'|'s')?; +Imaginary: ('I'|'i')('M'|'m')('A'|'a')('G'|'g')('I'|'i')('N'|'n')('A'|'a')('R'|'r')('Y'|'y'); +Variables: ('V'|'v')('A'|'a')('R'|'r')('I'|'i')('A'|'a')('B'|'b')('L'|'l')('E'|'e')('S'|'s')?; +MotionVariables: ('M'|'m')('O'|'o')('T'|'t')('I'|'i')('O'|'o')('N'|'n')('V'|'v')('A'|'a')('R'|'r')('I'|'i')('A'|'a')('B'|'b')('L'|'l')('E'|'e')('S'|'s')?; + +fragment DIFF: ('\'')*; +fragment DIGIT: [0-9]; +INT: [0-9]+ ; // match integers +FLOAT: DIGIT+ '.' DIGIT* + | '.' DIGIT+; +EXP: FLOAT 'E' INT +| FLOAT 'E' '-' INT; +LINE_COMMENT : '%' .*? '\r'? '\n' -> skip ; +ID: [a-zA-Z][a-zA-Z0-9_]*; +WS: [ \t\r\n&]+ -> skip ; // toss out whitespace diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/__init__.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ec81bb83325d68e1c11b43a1df5ec56846367e9f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/__init__.py @@ -0,0 +1,97 @@ +from sympy.external import import_module +from sympy.utilities.decorator import doctest_depends_on + +@doctest_depends_on(modules=('antlr4',)) +def parse_autolev(autolev_code, include_numeric=False): + """Parses Autolev code (version 4.1) to SymPy code. + + Parameters + ========= + autolev_code : Can be an str or any object with a readlines() method (such as a file handle or StringIO). + include_numeric : boolean, optional + If True NumPy, PyDy, or other numeric code is included for numeric evaluation lines in the Autolev code. + + Returns + ======= + sympy_code : str + Equivalent SymPy and/or numpy/pydy code as the input code. + + + Example (Double Pendulum) + ========================= + >>> my_al_text = ("MOTIONVARIABLES' Q{2}', U{2}'", + ... "CONSTANTS L,M,G", + ... "NEWTONIAN N", + ... "FRAMES A,B", + ... "SIMPROT(N, A, 3, Q1)", + ... "SIMPROT(N, B, 3, Q2)", + ... "W_A_N>=U1*N3>", + ... "W_B_N>=U2*N3>", + ... "POINT O", + ... "PARTICLES P,R", + ... "P_O_P> = L*A1>", + ... "P_P_R> = L*B1>", + ... "V_O_N> = 0>", + ... "V2PTS(N, A, O, P)", + ... "V2PTS(N, B, P, R)", + ... "MASS P=M, R=M", + ... "Q1' = U1", + ... "Q2' = U2", + ... "GRAVITY(G*N1>)", + ... "ZERO = FR() + FRSTAR()", + ... "KANE()", + ... "INPUT M=1,G=9.81,L=1", + ... "INPUT Q1=.1,Q2=.2,U1=0,U2=0", + ... "INPUT TFINAL=10, INTEGSTP=.01", + ... "CODE DYNAMICS() some_filename.c") + >>> my_al_text = '\\n'.join(my_al_text) + >>> from sympy.parsing.autolev import parse_autolev + >>> print(parse_autolev(my_al_text, include_numeric=True)) + import sympy.physics.mechanics as _me + import sympy as _sm + import math as m + import numpy as _np + + q1, q2, u1, u2 = _me.dynamicsymbols('q1 q2 u1 u2') + q1_d, q2_d, u1_d, u2_d = _me.dynamicsymbols('q1_ q2_ u1_ u2_', 1) + l, m, g = _sm.symbols('l m g', real=True) + frame_n = _me.ReferenceFrame('n') + frame_a = _me.ReferenceFrame('a') + frame_b = _me.ReferenceFrame('b') + frame_a.orient(frame_n, 'Axis', [q1, frame_n.z]) + frame_b.orient(frame_n, 'Axis', [q2, frame_n.z]) + frame_a.set_ang_vel(frame_n, u1*frame_n.z) + frame_b.set_ang_vel(frame_n, u2*frame_n.z) + point_o = _me.Point('o') + particle_p = _me.Particle('p', _me.Point('p_pt'), _sm.Symbol('m')) + particle_r = _me.Particle('r', _me.Point('r_pt'), _sm.Symbol('m')) + particle_p.point.set_pos(point_o, l*frame_a.x) + particle_r.point.set_pos(particle_p.point, l*frame_b.x) + point_o.set_vel(frame_n, 0) + particle_p.point.v2pt_theory(point_o,frame_n,frame_a) + particle_r.point.v2pt_theory(particle_p.point,frame_n,frame_b) + particle_p.mass = m + particle_r.mass = m + force_p = particle_p.mass*(g*frame_n.x) + force_r = particle_r.mass*(g*frame_n.x) + kd_eqs = [q1_d - u1, q2_d - u2] + forceList = [(particle_p.point,particle_p.mass*(g*frame_n.x)), (particle_r.point,particle_r.mass*(g*frame_n.x))] + kane = _me.KanesMethod(frame_n, q_ind=[q1,q2], u_ind=[u1, u2], kd_eqs = kd_eqs) + fr, frstar = kane.kanes_equations([particle_p, particle_r], forceList) + zero = fr+frstar + from pydy.system import System + sys = System(kane, constants = {l:1, m:1, g:9.81}, + specifieds={}, + initial_conditions={q1:.1, q2:.2, u1:0, u2:0}, + times = _np.linspace(0.0, 10, 10/.01)) + + y=sys.integrate() + + """ + + _autolev = import_module( + 'sympy.parsing.autolev._parse_autolev_antlr', + import_kwargs={'fromlist': ['X']}) + + if _autolev is not None: + return _autolev.parse_autolev(autolev_code, include_numeric) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_antlr/__init__.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_antlr/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9b71e9f51fd455558a9eb42dc840604c6c96e4b3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_antlr/__init__.py @@ -0,0 +1,5 @@ +# *** GENERATED BY `setup.py antlr`, DO NOT EDIT BY HAND *** +# +# Generated with antlr4 +# antlr4 is licensed under the BSD-3-Clause License +# https://github.com/antlr/antlr4/blob/master/LICENSE.txt diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_antlr/autolevlexer.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_antlr/autolevlexer.py new file mode 100644 index 0000000000000000000000000000000000000000..f3b3b1d27ade809a63d9fd328a1572c17625443e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_antlr/autolevlexer.py @@ -0,0 +1,253 @@ +# *** GENERATED BY `setup.py antlr`, DO NOT EDIT BY HAND *** +# +# Generated with antlr4 +# antlr4 is licensed under the BSD-3-Clause License +# https://github.com/antlr/antlr4/blob/master/LICENSE.txt +from antlr4 import * +from io import StringIO +import sys +if sys.version_info[1] > 5: + from typing import TextIO +else: + from typing.io import TextIO + + +def serializedATN(): + return [ + 4,0,49,393,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5, + 2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2, + 13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7, + 19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2, + 26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,31,7,31,2,32,7, + 32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38,7,38,2, + 39,7,39,2,40,7,40,2,41,7,41,2,42,7,42,2,43,7,43,2,44,7,44,2,45,7, + 45,2,46,7,46,2,47,7,47,2,48,7,48,2,49,7,49,2,50,7,50,1,0,1,0,1,1, + 1,1,1,2,1,2,1,3,1,3,1,3,1,4,1,4,1,4,1,5,1,5,1,5,1,6,1,6,1,6,1,7, + 1,7,1,7,1,8,1,8,1,8,1,9,1,9,1,10,1,10,1,11,1,11,1,12,1,12,1,13,1, + 13,1,14,1,14,1,15,1,15,1,16,1,16,1,17,1,17,1,18,1,18,1,19,1,19,1, + 20,1,20,1,21,1,21,1,21,1,22,1,22,1,22,1,22,1,23,1,23,1,24,1,24,1, + 25,1,25,1,26,1,26,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1, + 27,1,27,1,28,1,28,1,28,1,28,1,28,1,28,3,28,184,8,28,1,29,1,29,1, + 29,1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,30,1,30,1,31,1,31,1,31,1, + 31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,32,1,32,1,32,1,32,1,32,1, + 32,1,32,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,34,1, + 34,1,34,1,34,1,34,1,34,3,34,232,8,34,1,35,1,35,1,35,1,35,1,35,1, + 35,3,35,240,8,35,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,3, + 36,251,8,36,1,37,1,37,1,37,1,37,1,37,1,37,3,37,259,8,37,1,38,1,38, + 1,38,1,38,1,38,1,38,1,38,1,38,1,38,3,38,270,8,38,1,39,1,39,1,39, + 1,39,1,39,1,39,1,39,1,39,1,39,1,39,3,39,282,8,39,1,40,1,40,1,40, + 1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,41,1,41,1,41,1,41,1,41,1,41, + 1,41,1,41,1,41,3,41,303,8,41,1,42,1,42,1,42,1,42,1,42,1,42,1,42, + 1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,3,42,320,8,42,1,43,5,43, + 323,8,43,10,43,12,43,326,9,43,1,44,1,44,1,45,4,45,331,8,45,11,45, + 12,45,332,1,46,4,46,336,8,46,11,46,12,46,337,1,46,1,46,5,46,342, + 8,46,10,46,12,46,345,9,46,1,46,1,46,4,46,349,8,46,11,46,12,46,350, + 3,46,353,8,46,1,47,1,47,1,47,1,47,1,47,1,47,1,47,1,47,1,47,3,47, + 364,8,47,1,48,1,48,5,48,368,8,48,10,48,12,48,371,9,48,1,48,3,48, + 374,8,48,1,48,1,48,1,48,1,48,1,49,1,49,5,49,382,8,49,10,49,12,49, + 385,9,49,1,50,4,50,388,8,50,11,50,12,50,389,1,50,1,50,1,369,0,51, + 1,1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13, + 27,14,29,15,31,16,33,17,35,18,37,19,39,20,41,21,43,22,45,23,47,24, + 49,25,51,26,53,27,55,28,57,29,59,30,61,31,63,32,65,33,67,34,69,35, + 71,36,73,37,75,38,77,39,79,40,81,41,83,42,85,43,87,0,89,0,91,44, + 93,45,95,46,97,47,99,48,101,49,1,0,24,2,0,77,77,109,109,2,0,65,65, + 97,97,2,0,83,83,115,115,2,0,73,73,105,105,2,0,78,78,110,110,2,0, + 69,69,101,101,2,0,82,82,114,114,2,0,84,84,116,116,2,0,80,80,112, + 112,2,0,85,85,117,117,2,0,79,79,111,111,2,0,86,86,118,118,2,0,89, + 89,121,121,2,0,67,67,99,99,2,0,68,68,100,100,2,0,87,87,119,119,2, + 0,70,70,102,102,2,0,66,66,98,98,2,0,76,76,108,108,2,0,71,71,103, + 103,1,0,48,57,2,0,65,90,97,122,4,0,48,57,65,90,95,95,97,122,4,0, + 9,10,13,13,32,32,38,38,410,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0, + 7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17, + 1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27, + 1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37, + 1,0,0,0,0,39,1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47, + 1,0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,55,1,0,0,0,0,57, + 1,0,0,0,0,59,1,0,0,0,0,61,1,0,0,0,0,63,1,0,0,0,0,65,1,0,0,0,0,67, + 1,0,0,0,0,69,1,0,0,0,0,71,1,0,0,0,0,73,1,0,0,0,0,75,1,0,0,0,0,77, + 1,0,0,0,0,79,1,0,0,0,0,81,1,0,0,0,0,83,1,0,0,0,0,85,1,0,0,0,0,91, + 1,0,0,0,0,93,1,0,0,0,0,95,1,0,0,0,0,97,1,0,0,0,0,99,1,0,0,0,0,101, + 1,0,0,0,1,103,1,0,0,0,3,105,1,0,0,0,5,107,1,0,0,0,7,109,1,0,0,0, + 9,112,1,0,0,0,11,115,1,0,0,0,13,118,1,0,0,0,15,121,1,0,0,0,17,124, + 1,0,0,0,19,127,1,0,0,0,21,129,1,0,0,0,23,131,1,0,0,0,25,133,1,0, + 0,0,27,135,1,0,0,0,29,137,1,0,0,0,31,139,1,0,0,0,33,141,1,0,0,0, + 35,143,1,0,0,0,37,145,1,0,0,0,39,147,1,0,0,0,41,149,1,0,0,0,43,151, + 1,0,0,0,45,154,1,0,0,0,47,158,1,0,0,0,49,160,1,0,0,0,51,162,1,0, + 0,0,53,164,1,0,0,0,55,169,1,0,0,0,57,177,1,0,0,0,59,185,1,0,0,0, + 61,192,1,0,0,0,63,197,1,0,0,0,65,208,1,0,0,0,67,215,1,0,0,0,69,225, + 1,0,0,0,71,233,1,0,0,0,73,241,1,0,0,0,75,252,1,0,0,0,77,260,1,0, + 0,0,79,271,1,0,0,0,81,283,1,0,0,0,83,293,1,0,0,0,85,304,1,0,0,0, + 87,324,1,0,0,0,89,327,1,0,0,0,91,330,1,0,0,0,93,352,1,0,0,0,95,363, + 1,0,0,0,97,365,1,0,0,0,99,379,1,0,0,0,101,387,1,0,0,0,103,104,5, + 91,0,0,104,2,1,0,0,0,105,106,5,93,0,0,106,4,1,0,0,0,107,108,5,61, + 0,0,108,6,1,0,0,0,109,110,5,43,0,0,110,111,5,61,0,0,111,8,1,0,0, + 0,112,113,5,45,0,0,113,114,5,61,0,0,114,10,1,0,0,0,115,116,5,58, + 0,0,116,117,5,61,0,0,117,12,1,0,0,0,118,119,5,42,0,0,119,120,5,61, + 0,0,120,14,1,0,0,0,121,122,5,47,0,0,122,123,5,61,0,0,123,16,1,0, + 0,0,124,125,5,94,0,0,125,126,5,61,0,0,126,18,1,0,0,0,127,128,5,44, + 0,0,128,20,1,0,0,0,129,130,5,39,0,0,130,22,1,0,0,0,131,132,5,40, + 0,0,132,24,1,0,0,0,133,134,5,41,0,0,134,26,1,0,0,0,135,136,5,123, + 0,0,136,28,1,0,0,0,137,138,5,125,0,0,138,30,1,0,0,0,139,140,5,58, + 0,0,140,32,1,0,0,0,141,142,5,43,0,0,142,34,1,0,0,0,143,144,5,45, + 0,0,144,36,1,0,0,0,145,146,5,59,0,0,146,38,1,0,0,0,147,148,5,46, + 0,0,148,40,1,0,0,0,149,150,5,62,0,0,150,42,1,0,0,0,151,152,5,48, + 0,0,152,153,5,62,0,0,153,44,1,0,0,0,154,155,5,49,0,0,155,156,5,62, + 0,0,156,157,5,62,0,0,157,46,1,0,0,0,158,159,5,94,0,0,159,48,1,0, + 0,0,160,161,5,42,0,0,161,50,1,0,0,0,162,163,5,47,0,0,163,52,1,0, + 0,0,164,165,7,0,0,0,165,166,7,1,0,0,166,167,7,2,0,0,167,168,7,2, + 0,0,168,54,1,0,0,0,169,170,7,3,0,0,170,171,7,4,0,0,171,172,7,5,0, + 0,172,173,7,6,0,0,173,174,7,7,0,0,174,175,7,3,0,0,175,176,7,1,0, + 0,176,56,1,0,0,0,177,178,7,3,0,0,178,179,7,4,0,0,179,180,7,8,0,0, + 180,181,7,9,0,0,181,183,7,7,0,0,182,184,7,2,0,0,183,182,1,0,0,0, + 183,184,1,0,0,0,184,58,1,0,0,0,185,186,7,10,0,0,186,187,7,9,0,0, + 187,188,7,7,0,0,188,189,7,8,0,0,189,190,7,9,0,0,190,191,7,7,0,0, + 191,60,1,0,0,0,192,193,7,2,0,0,193,194,7,1,0,0,194,195,7,11,0,0, + 195,196,7,5,0,0,196,62,1,0,0,0,197,198,7,9,0,0,198,199,7,4,0,0,199, + 200,7,3,0,0,200,201,7,7,0,0,201,202,7,2,0,0,202,203,7,12,0,0,203, + 204,7,2,0,0,204,205,7,7,0,0,205,206,7,5,0,0,206,207,7,0,0,0,207, + 64,1,0,0,0,208,209,7,5,0,0,209,210,7,4,0,0,210,211,7,13,0,0,211, + 212,7,10,0,0,212,213,7,14,0,0,213,214,7,5,0,0,214,66,1,0,0,0,215, + 216,7,4,0,0,216,217,7,5,0,0,217,218,7,15,0,0,218,219,7,7,0,0,219, + 220,7,10,0,0,220,221,7,4,0,0,221,222,7,3,0,0,222,223,7,1,0,0,223, + 224,7,4,0,0,224,68,1,0,0,0,225,226,7,16,0,0,226,227,7,6,0,0,227, + 228,7,1,0,0,228,229,7,0,0,0,229,231,7,5,0,0,230,232,7,2,0,0,231, + 230,1,0,0,0,231,232,1,0,0,0,232,70,1,0,0,0,233,234,7,17,0,0,234, + 235,7,10,0,0,235,236,7,14,0,0,236,237,7,3,0,0,237,239,7,5,0,0,238, + 240,7,2,0,0,239,238,1,0,0,0,239,240,1,0,0,0,240,72,1,0,0,0,241,242, + 7,8,0,0,242,243,7,1,0,0,243,244,7,6,0,0,244,245,7,7,0,0,245,246, + 7,3,0,0,246,247,7,13,0,0,247,248,7,18,0,0,248,250,7,5,0,0,249,251, + 7,2,0,0,250,249,1,0,0,0,250,251,1,0,0,0,251,74,1,0,0,0,252,253,7, + 8,0,0,253,254,7,10,0,0,254,255,7,3,0,0,255,256,7,4,0,0,256,258,7, + 7,0,0,257,259,7,2,0,0,258,257,1,0,0,0,258,259,1,0,0,0,259,76,1,0, + 0,0,260,261,7,13,0,0,261,262,7,10,0,0,262,263,7,4,0,0,263,264,7, + 2,0,0,264,265,7,7,0,0,265,266,7,1,0,0,266,267,7,4,0,0,267,269,7, + 7,0,0,268,270,7,2,0,0,269,268,1,0,0,0,269,270,1,0,0,0,270,78,1,0, + 0,0,271,272,7,2,0,0,272,273,7,8,0,0,273,274,7,5,0,0,274,275,7,13, + 0,0,275,276,7,3,0,0,276,277,7,16,0,0,277,278,7,3,0,0,278,279,7,5, + 0,0,279,281,7,14,0,0,280,282,7,2,0,0,281,280,1,0,0,0,281,282,1,0, + 0,0,282,80,1,0,0,0,283,284,7,3,0,0,284,285,7,0,0,0,285,286,7,1,0, + 0,286,287,7,19,0,0,287,288,7,3,0,0,288,289,7,4,0,0,289,290,7,1,0, + 0,290,291,7,6,0,0,291,292,7,12,0,0,292,82,1,0,0,0,293,294,7,11,0, + 0,294,295,7,1,0,0,295,296,7,6,0,0,296,297,7,3,0,0,297,298,7,1,0, + 0,298,299,7,17,0,0,299,300,7,18,0,0,300,302,7,5,0,0,301,303,7,2, + 0,0,302,301,1,0,0,0,302,303,1,0,0,0,303,84,1,0,0,0,304,305,7,0,0, + 0,305,306,7,10,0,0,306,307,7,7,0,0,307,308,7,3,0,0,308,309,7,10, + 0,0,309,310,7,4,0,0,310,311,7,11,0,0,311,312,7,1,0,0,312,313,7,6, + 0,0,313,314,7,3,0,0,314,315,7,1,0,0,315,316,7,17,0,0,316,317,7,18, + 0,0,317,319,7,5,0,0,318,320,7,2,0,0,319,318,1,0,0,0,319,320,1,0, + 0,0,320,86,1,0,0,0,321,323,5,39,0,0,322,321,1,0,0,0,323,326,1,0, + 0,0,324,322,1,0,0,0,324,325,1,0,0,0,325,88,1,0,0,0,326,324,1,0,0, + 0,327,328,7,20,0,0,328,90,1,0,0,0,329,331,7,20,0,0,330,329,1,0,0, + 0,331,332,1,0,0,0,332,330,1,0,0,0,332,333,1,0,0,0,333,92,1,0,0,0, + 334,336,3,89,44,0,335,334,1,0,0,0,336,337,1,0,0,0,337,335,1,0,0, + 0,337,338,1,0,0,0,338,339,1,0,0,0,339,343,5,46,0,0,340,342,3,89, + 44,0,341,340,1,0,0,0,342,345,1,0,0,0,343,341,1,0,0,0,343,344,1,0, + 0,0,344,353,1,0,0,0,345,343,1,0,0,0,346,348,5,46,0,0,347,349,3,89, + 44,0,348,347,1,0,0,0,349,350,1,0,0,0,350,348,1,0,0,0,350,351,1,0, + 0,0,351,353,1,0,0,0,352,335,1,0,0,0,352,346,1,0,0,0,353,94,1,0,0, + 0,354,355,3,93,46,0,355,356,5,69,0,0,356,357,3,91,45,0,357,364,1, + 0,0,0,358,359,3,93,46,0,359,360,5,69,0,0,360,361,5,45,0,0,361,362, + 3,91,45,0,362,364,1,0,0,0,363,354,1,0,0,0,363,358,1,0,0,0,364,96, + 1,0,0,0,365,369,5,37,0,0,366,368,9,0,0,0,367,366,1,0,0,0,368,371, + 1,0,0,0,369,370,1,0,0,0,369,367,1,0,0,0,370,373,1,0,0,0,371,369, + 1,0,0,0,372,374,5,13,0,0,373,372,1,0,0,0,373,374,1,0,0,0,374,375, + 1,0,0,0,375,376,5,10,0,0,376,377,1,0,0,0,377,378,6,48,0,0,378,98, + 1,0,0,0,379,383,7,21,0,0,380,382,7,22,0,0,381,380,1,0,0,0,382,385, + 1,0,0,0,383,381,1,0,0,0,383,384,1,0,0,0,384,100,1,0,0,0,385,383, + 1,0,0,0,386,388,7,23,0,0,387,386,1,0,0,0,388,389,1,0,0,0,389,387, + 1,0,0,0,389,390,1,0,0,0,390,391,1,0,0,0,391,392,6,50,0,0,392,102, + 1,0,0,0,21,0,183,231,239,250,258,269,281,302,319,324,332,337,343, + 350,352,363,369,373,383,389,1,6,0,0 + ] + +class AutolevLexer(Lexer): + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + T__0 = 1 + T__1 = 2 + T__2 = 3 + T__3 = 4 + T__4 = 5 + T__5 = 6 + T__6 = 7 + T__7 = 8 + T__8 = 9 + T__9 = 10 + T__10 = 11 + T__11 = 12 + T__12 = 13 + T__13 = 14 + T__14 = 15 + T__15 = 16 + T__16 = 17 + T__17 = 18 + T__18 = 19 + T__19 = 20 + T__20 = 21 + T__21 = 22 + T__22 = 23 + T__23 = 24 + T__24 = 25 + T__25 = 26 + Mass = 27 + Inertia = 28 + Input = 29 + Output = 30 + Save = 31 + UnitSystem = 32 + Encode = 33 + Newtonian = 34 + Frames = 35 + Bodies = 36 + Particles = 37 + Points = 38 + Constants = 39 + Specifieds = 40 + Imaginary = 41 + Variables = 42 + MotionVariables = 43 + INT = 44 + FLOAT = 45 + EXP = 46 + LINE_COMMENT = 47 + ID = 48 + WS = 49 + + channelNames = [ u"DEFAULT_TOKEN_CHANNEL", u"HIDDEN" ] + + modeNames = [ "DEFAULT_MODE" ] + + literalNames = [ "", + "'['", "']'", "'='", "'+='", "'-='", "':='", "'*='", "'/='", + "'^='", "','", "'''", "'('", "')'", "'{'", "'}'", "':'", "'+'", + "'-'", "';'", "'.'", "'>'", "'0>'", "'1>>'", "'^'", "'*'", "'/'" ] + + symbolicNames = [ "", + "Mass", "Inertia", "Input", "Output", "Save", "UnitSystem", + "Encode", "Newtonian", "Frames", "Bodies", "Particles", "Points", + "Constants", "Specifieds", "Imaginary", "Variables", "MotionVariables", + "INT", "FLOAT", "EXP", "LINE_COMMENT", "ID", "WS" ] + + ruleNames = [ "T__0", "T__1", "T__2", "T__3", "T__4", "T__5", "T__6", + "T__7", "T__8", "T__9", "T__10", "T__11", "T__12", "T__13", + "T__14", "T__15", "T__16", "T__17", "T__18", "T__19", + "T__20", "T__21", "T__22", "T__23", "T__24", "T__25", + "Mass", "Inertia", "Input", "Output", "Save", "UnitSystem", + "Encode", "Newtonian", "Frames", "Bodies", "Particles", + "Points", "Constants", "Specifieds", "Imaginary", "Variables", + "MotionVariables", "DIFF", "DIGIT", "INT", "FLOAT", "EXP", + "LINE_COMMENT", "ID", "WS" ] + + grammarFileName = "Autolev.g4" + + def __init__(self, input=None, output:TextIO = sys.stdout): + super().__init__(input, output) + self.checkVersion("4.11.1") + self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache()) + self._actions = None + self._predicates = None + + diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_antlr/autolevlistener.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_antlr/autolevlistener.py new file mode 100644 index 0000000000000000000000000000000000000000..6f391a298a71ecf2d04cf921a919cbb68b181fab --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_antlr/autolevlistener.py @@ -0,0 +1,421 @@ +# *** GENERATED BY `setup.py antlr`, DO NOT EDIT BY HAND *** +# +# Generated with antlr4 +# antlr4 is licensed under the BSD-3-Clause License +# https://github.com/antlr/antlr4/blob/master/LICENSE.txt +from antlr4 import * +if __name__ is not None and "." in __name__: + from .autolevparser import AutolevParser +else: + from autolevparser import AutolevParser + +# This class defines a complete listener for a parse tree produced by AutolevParser. +class AutolevListener(ParseTreeListener): + + # Enter a parse tree produced by AutolevParser#prog. + def enterProg(self, ctx:AutolevParser.ProgContext): + pass + + # Exit a parse tree produced by AutolevParser#prog. + def exitProg(self, ctx:AutolevParser.ProgContext): + pass + + + # Enter a parse tree produced by AutolevParser#stat. + def enterStat(self, ctx:AutolevParser.StatContext): + pass + + # Exit a parse tree produced by AutolevParser#stat. + def exitStat(self, ctx:AutolevParser.StatContext): + pass + + + # Enter a parse tree produced by AutolevParser#vecAssign. + def enterVecAssign(self, ctx:AutolevParser.VecAssignContext): + pass + + # Exit a parse tree produced by AutolevParser#vecAssign. + def exitVecAssign(self, ctx:AutolevParser.VecAssignContext): + pass + + + # Enter a parse tree produced by AutolevParser#indexAssign. + def enterIndexAssign(self, ctx:AutolevParser.IndexAssignContext): + pass + + # Exit a parse tree produced by AutolevParser#indexAssign. + def exitIndexAssign(self, ctx:AutolevParser.IndexAssignContext): + pass + + + # Enter a parse tree produced by AutolevParser#regularAssign. + def enterRegularAssign(self, ctx:AutolevParser.RegularAssignContext): + pass + + # Exit a parse tree produced by AutolevParser#regularAssign. + def exitRegularAssign(self, ctx:AutolevParser.RegularAssignContext): + pass + + + # Enter a parse tree produced by AutolevParser#equals. + def enterEquals(self, ctx:AutolevParser.EqualsContext): + pass + + # Exit a parse tree produced by AutolevParser#equals. + def exitEquals(self, ctx:AutolevParser.EqualsContext): + pass + + + # Enter a parse tree produced by AutolevParser#index. + def enterIndex(self, ctx:AutolevParser.IndexContext): + pass + + # Exit a parse tree produced by AutolevParser#index. + def exitIndex(self, ctx:AutolevParser.IndexContext): + pass + + + # Enter a parse tree produced by AutolevParser#diff. + def enterDiff(self, ctx:AutolevParser.DiffContext): + pass + + # Exit a parse tree produced by AutolevParser#diff. + def exitDiff(self, ctx:AutolevParser.DiffContext): + pass + + + # Enter a parse tree produced by AutolevParser#functionCall. + def enterFunctionCall(self, ctx:AutolevParser.FunctionCallContext): + pass + + # Exit a parse tree produced by AutolevParser#functionCall. + def exitFunctionCall(self, ctx:AutolevParser.FunctionCallContext): + pass + + + # Enter a parse tree produced by AutolevParser#varDecl. + def enterVarDecl(self, ctx:AutolevParser.VarDeclContext): + pass + + # Exit a parse tree produced by AutolevParser#varDecl. + def exitVarDecl(self, ctx:AutolevParser.VarDeclContext): + pass + + + # Enter a parse tree produced by AutolevParser#varType. + def enterVarType(self, ctx:AutolevParser.VarTypeContext): + pass + + # Exit a parse tree produced by AutolevParser#varType. + def exitVarType(self, ctx:AutolevParser.VarTypeContext): + pass + + + # Enter a parse tree produced by AutolevParser#varDecl2. + def enterVarDecl2(self, ctx:AutolevParser.VarDecl2Context): + pass + + # Exit a parse tree produced by AutolevParser#varDecl2. + def exitVarDecl2(self, ctx:AutolevParser.VarDecl2Context): + pass + + + # Enter a parse tree produced by AutolevParser#ranges. + def enterRanges(self, ctx:AutolevParser.RangesContext): + pass + + # Exit a parse tree produced by AutolevParser#ranges. + def exitRanges(self, ctx:AutolevParser.RangesContext): + pass + + + # Enter a parse tree produced by AutolevParser#massDecl. + def enterMassDecl(self, ctx:AutolevParser.MassDeclContext): + pass + + # Exit a parse tree produced by AutolevParser#massDecl. + def exitMassDecl(self, ctx:AutolevParser.MassDeclContext): + pass + + + # Enter a parse tree produced by AutolevParser#massDecl2. + def enterMassDecl2(self, ctx:AutolevParser.MassDecl2Context): + pass + + # Exit a parse tree produced by AutolevParser#massDecl2. + def exitMassDecl2(self, ctx:AutolevParser.MassDecl2Context): + pass + + + # Enter a parse tree produced by AutolevParser#inertiaDecl. + def enterInertiaDecl(self, ctx:AutolevParser.InertiaDeclContext): + pass + + # Exit a parse tree produced by AutolevParser#inertiaDecl. + def exitInertiaDecl(self, ctx:AutolevParser.InertiaDeclContext): + pass + + + # Enter a parse tree produced by AutolevParser#matrix. + def enterMatrix(self, ctx:AutolevParser.MatrixContext): + pass + + # Exit a parse tree produced by AutolevParser#matrix. + def exitMatrix(self, ctx:AutolevParser.MatrixContext): + pass + + + # Enter a parse tree produced by AutolevParser#matrixInOutput. + def enterMatrixInOutput(self, ctx:AutolevParser.MatrixInOutputContext): + pass + + # Exit a parse tree produced by AutolevParser#matrixInOutput. + def exitMatrixInOutput(self, ctx:AutolevParser.MatrixInOutputContext): + pass + + + # Enter a parse tree produced by AutolevParser#codeCommands. + def enterCodeCommands(self, ctx:AutolevParser.CodeCommandsContext): + pass + + # Exit a parse tree produced by AutolevParser#codeCommands. + def exitCodeCommands(self, ctx:AutolevParser.CodeCommandsContext): + pass + + + # Enter a parse tree produced by AutolevParser#settings. + def enterSettings(self, ctx:AutolevParser.SettingsContext): + pass + + # Exit a parse tree produced by AutolevParser#settings. + def exitSettings(self, ctx:AutolevParser.SettingsContext): + pass + + + # Enter a parse tree produced by AutolevParser#units. + def enterUnits(self, ctx:AutolevParser.UnitsContext): + pass + + # Exit a parse tree produced by AutolevParser#units. + def exitUnits(self, ctx:AutolevParser.UnitsContext): + pass + + + # Enter a parse tree produced by AutolevParser#inputs. + def enterInputs(self, ctx:AutolevParser.InputsContext): + pass + + # Exit a parse tree produced by AutolevParser#inputs. + def exitInputs(self, ctx:AutolevParser.InputsContext): + pass + + + # Enter a parse tree produced by AutolevParser#id_diff. + def enterId_diff(self, ctx:AutolevParser.Id_diffContext): + pass + + # Exit a parse tree produced by AutolevParser#id_diff. + def exitId_diff(self, ctx:AutolevParser.Id_diffContext): + pass + + + # Enter a parse tree produced by AutolevParser#inputs2. + def enterInputs2(self, ctx:AutolevParser.Inputs2Context): + pass + + # Exit a parse tree produced by AutolevParser#inputs2. + def exitInputs2(self, ctx:AutolevParser.Inputs2Context): + pass + + + # Enter a parse tree produced by AutolevParser#outputs. + def enterOutputs(self, ctx:AutolevParser.OutputsContext): + pass + + # Exit a parse tree produced by AutolevParser#outputs. + def exitOutputs(self, ctx:AutolevParser.OutputsContext): + pass + + + # Enter a parse tree produced by AutolevParser#outputs2. + def enterOutputs2(self, ctx:AutolevParser.Outputs2Context): + pass + + # Exit a parse tree produced by AutolevParser#outputs2. + def exitOutputs2(self, ctx:AutolevParser.Outputs2Context): + pass + + + # Enter a parse tree produced by AutolevParser#codegen. + def enterCodegen(self, ctx:AutolevParser.CodegenContext): + pass + + # Exit a parse tree produced by AutolevParser#codegen. + def exitCodegen(self, ctx:AutolevParser.CodegenContext): + pass + + + # Enter a parse tree produced by AutolevParser#commands. + def enterCommands(self, ctx:AutolevParser.CommandsContext): + pass + + # Exit a parse tree produced by AutolevParser#commands. + def exitCommands(self, ctx:AutolevParser.CommandsContext): + pass + + + # Enter a parse tree produced by AutolevParser#vec. + def enterVec(self, ctx:AutolevParser.VecContext): + pass + + # Exit a parse tree produced by AutolevParser#vec. + def exitVec(self, ctx:AutolevParser.VecContext): + pass + + + # Enter a parse tree produced by AutolevParser#parens. + def enterParens(self, ctx:AutolevParser.ParensContext): + pass + + # Exit a parse tree produced by AutolevParser#parens. + def exitParens(self, ctx:AutolevParser.ParensContext): + pass + + + # Enter a parse tree produced by AutolevParser#VectorOrDyadic. + def enterVectorOrDyadic(self, ctx:AutolevParser.VectorOrDyadicContext): + pass + + # Exit a parse tree produced by AutolevParser#VectorOrDyadic. + def exitVectorOrDyadic(self, ctx:AutolevParser.VectorOrDyadicContext): + pass + + + # Enter a parse tree produced by AutolevParser#Exponent. + def enterExponent(self, ctx:AutolevParser.ExponentContext): + pass + + # Exit a parse tree produced by AutolevParser#Exponent. + def exitExponent(self, ctx:AutolevParser.ExponentContext): + pass + + + # Enter a parse tree produced by AutolevParser#MulDiv. + def enterMulDiv(self, ctx:AutolevParser.MulDivContext): + pass + + # Exit a parse tree produced by AutolevParser#MulDiv. + def exitMulDiv(self, ctx:AutolevParser.MulDivContext): + pass + + + # Enter a parse tree produced by AutolevParser#AddSub. + def enterAddSub(self, ctx:AutolevParser.AddSubContext): + pass + + # Exit a parse tree produced by AutolevParser#AddSub. + def exitAddSub(self, ctx:AutolevParser.AddSubContext): + pass + + + # Enter a parse tree produced by AutolevParser#float. + def enterFloat(self, ctx:AutolevParser.FloatContext): + pass + + # Exit a parse tree produced by AutolevParser#float. + def exitFloat(self, ctx:AutolevParser.FloatContext): + pass + + + # Enter a parse tree produced by AutolevParser#int. + def enterInt(self, ctx:AutolevParser.IntContext): + pass + + # Exit a parse tree produced by AutolevParser#int. + def exitInt(self, ctx:AutolevParser.IntContext): + pass + + + # Enter a parse tree produced by AutolevParser#idEqualsExpr. + def enterIdEqualsExpr(self, ctx:AutolevParser.IdEqualsExprContext): + pass + + # Exit a parse tree produced by AutolevParser#idEqualsExpr. + def exitIdEqualsExpr(self, ctx:AutolevParser.IdEqualsExprContext): + pass + + + # Enter a parse tree produced by AutolevParser#negativeOne. + def enterNegativeOne(self, ctx:AutolevParser.NegativeOneContext): + pass + + # Exit a parse tree produced by AutolevParser#negativeOne. + def exitNegativeOne(self, ctx:AutolevParser.NegativeOneContext): + pass + + + # Enter a parse tree produced by AutolevParser#function. + def enterFunction(self, ctx:AutolevParser.FunctionContext): + pass + + # Exit a parse tree produced by AutolevParser#function. + def exitFunction(self, ctx:AutolevParser.FunctionContext): + pass + + + # Enter a parse tree produced by AutolevParser#rangess. + def enterRangess(self, ctx:AutolevParser.RangessContext): + pass + + # Exit a parse tree produced by AutolevParser#rangess. + def exitRangess(self, ctx:AutolevParser.RangessContext): + pass + + + # Enter a parse tree produced by AutolevParser#colon. + def enterColon(self, ctx:AutolevParser.ColonContext): + pass + + # Exit a parse tree produced by AutolevParser#colon. + def exitColon(self, ctx:AutolevParser.ColonContext): + pass + + + # Enter a parse tree produced by AutolevParser#id. + def enterId(self, ctx:AutolevParser.IdContext): + pass + + # Exit a parse tree produced by AutolevParser#id. + def exitId(self, ctx:AutolevParser.IdContext): + pass + + + # Enter a parse tree produced by AutolevParser#exp. + def enterExp(self, ctx:AutolevParser.ExpContext): + pass + + # Exit a parse tree produced by AutolevParser#exp. + def exitExp(self, ctx:AutolevParser.ExpContext): + pass + + + # Enter a parse tree produced by AutolevParser#matrices. + def enterMatrices(self, ctx:AutolevParser.MatricesContext): + pass + + # Exit a parse tree produced by AutolevParser#matrices. + def exitMatrices(self, ctx:AutolevParser.MatricesContext): + pass + + + # Enter a parse tree produced by AutolevParser#Indexing. + def enterIndexing(self, ctx:AutolevParser.IndexingContext): + pass + + # Exit a parse tree produced by AutolevParser#Indexing. + def exitIndexing(self, ctx:AutolevParser.IndexingContext): + pass + + + +del AutolevParser diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_antlr/autolevparser.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_antlr/autolevparser.py new file mode 100644 index 0000000000000000000000000000000000000000..e63ef1c110812580d06291ee7c7ec40b6a076cea --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_antlr/autolevparser.py @@ -0,0 +1,3063 @@ +# *** GENERATED BY `setup.py antlr`, DO NOT EDIT BY HAND *** +# +# Generated with antlr4 +# antlr4 is licensed under the BSD-3-Clause License +# https://github.com/antlr/antlr4/blob/master/LICENSE.txt +from antlr4 import * +from io import StringIO +import sys +if sys.version_info[1] > 5: + from typing import TextIO +else: + from typing.io import TextIO + +def serializedATN(): + return [ + 4,1,49,431,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7, + 6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13, + 2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2,20, + 7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7,26, + 2,27,7,27,1,0,4,0,58,8,0,11,0,12,0,59,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,3,1,69,8,1,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2, + 3,2,84,8,2,1,2,1,2,1,2,3,2,89,8,2,1,3,1,3,1,4,1,4,1,4,5,4,96,8,4, + 10,4,12,4,99,9,4,1,5,4,5,102,8,5,11,5,12,5,103,1,6,1,6,1,6,1,6,1, + 6,5,6,111,8,6,10,6,12,6,114,9,6,3,6,116,8,6,1,6,1,6,1,6,1,6,1,6, + 1,6,5,6,124,8,6,10,6,12,6,127,9,6,3,6,129,8,6,1,6,3,6,132,8,6,1, + 7,1,7,1,7,1,7,5,7,138,8,7,10,7,12,7,141,9,7,1,8,1,8,1,8,1,8,1,8, + 1,8,1,8,1,8,1,8,1,8,5,8,153,8,8,10,8,12,8,156,9,8,1,8,1,8,5,8,160, + 8,8,10,8,12,8,163,9,8,3,8,165,8,8,1,9,1,9,1,9,1,9,1,9,1,9,3,9,173, + 8,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,5,9,183,8,9,10,9,12,9,186,9, + 9,1,9,3,9,189,8,9,1,9,1,9,1,9,3,9,194,8,9,1,9,3,9,197,8,9,1,9,5, + 9,200,8,9,10,9,12,9,203,9,9,1,9,1,9,3,9,207,8,9,1,10,1,10,1,10,1, + 10,1,10,1,10,1,10,1,10,5,10,217,8,10,10,10,12,10,220,9,10,1,10,1, + 10,1,11,1,11,1,11,1,11,5,11,228,8,11,10,11,12,11,231,9,11,1,12,1, + 12,1,12,1,12,1,13,1,13,1,13,1,13,1,13,3,13,242,8,13,1,13,1,13,4, + 13,246,8,13,11,13,12,13,247,1,14,1,14,1,14,1,14,5,14,254,8,14,10, + 14,12,14,257,9,14,1,14,1,14,1,15,1,15,1,15,1,15,3,15,265,8,15,1, + 15,1,15,3,15,269,8,15,1,16,1,16,1,16,1,16,1,16,3,16,276,8,16,1,17, + 1,17,3,17,280,8,17,1,18,1,18,1,18,1,18,5,18,286,8,18,10,18,12,18, + 289,9,18,1,19,1,19,1,19,1,19,5,19,295,8,19,10,19,12,19,298,9,19, + 1,20,1,20,3,20,302,8,20,1,21,1,21,1,21,1,21,3,21,308,8,21,1,22,1, + 22,1,22,1,22,5,22,314,8,22,10,22,12,22,317,9,22,1,23,1,23,3,23,321, + 8,23,1,24,1,24,1,24,1,24,1,24,1,24,5,24,329,8,24,10,24,12,24,332, + 9,24,1,24,1,24,3,24,336,8,24,1,24,1,24,1,24,1,24,1,25,1,25,1,25, + 1,25,1,25,1,25,1,25,1,25,5,25,350,8,25,10,25,12,25,353,9,25,3,25, + 355,8,25,1,26,1,26,4,26,359,8,26,11,26,12,26,360,1,26,1,26,3,26, + 365,8,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,5,27,375,8,27,10, + 27,12,27,378,9,27,1,27,1,27,1,27,1,27,1,27,1,27,5,27,386,8,27,10, + 27,12,27,389,9,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3, + 27,400,8,27,1,27,1,27,5,27,404,8,27,10,27,12,27,407,9,27,3,27,409, + 8,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27, + 1,27,1,27,1,27,5,27,426,8,27,10,27,12,27,429,9,27,1,27,0,1,54,28, + 0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44, + 46,48,50,52,54,0,7,1,0,3,9,1,0,27,28,1,0,17,18,2,0,10,10,19,19,1, + 0,44,45,2,0,44,46,48,48,1,0,25,26,483,0,57,1,0,0,0,2,68,1,0,0,0, + 4,88,1,0,0,0,6,90,1,0,0,0,8,92,1,0,0,0,10,101,1,0,0,0,12,131,1,0, + 0,0,14,133,1,0,0,0,16,164,1,0,0,0,18,166,1,0,0,0,20,208,1,0,0,0, + 22,223,1,0,0,0,24,232,1,0,0,0,26,236,1,0,0,0,28,249,1,0,0,0,30,268, + 1,0,0,0,32,275,1,0,0,0,34,277,1,0,0,0,36,281,1,0,0,0,38,290,1,0, + 0,0,40,299,1,0,0,0,42,303,1,0,0,0,44,309,1,0,0,0,46,318,1,0,0,0, + 48,322,1,0,0,0,50,354,1,0,0,0,52,364,1,0,0,0,54,408,1,0,0,0,56,58, + 3,2,1,0,57,56,1,0,0,0,58,59,1,0,0,0,59,57,1,0,0,0,59,60,1,0,0,0, + 60,1,1,0,0,0,61,69,3,14,7,0,62,69,3,12,6,0,63,69,3,32,16,0,64,69, + 3,22,11,0,65,69,3,26,13,0,66,69,3,4,2,0,67,69,3,34,17,0,68,61,1, + 0,0,0,68,62,1,0,0,0,68,63,1,0,0,0,68,64,1,0,0,0,68,65,1,0,0,0,68, + 66,1,0,0,0,68,67,1,0,0,0,69,3,1,0,0,0,70,71,3,52,26,0,71,72,3,6, + 3,0,72,73,3,54,27,0,73,89,1,0,0,0,74,75,5,48,0,0,75,76,5,1,0,0,76, + 77,3,8,4,0,77,78,5,2,0,0,78,79,3,6,3,0,79,80,3,54,27,0,80,89,1,0, + 0,0,81,83,5,48,0,0,82,84,3,10,5,0,83,82,1,0,0,0,83,84,1,0,0,0,84, + 85,1,0,0,0,85,86,3,6,3,0,86,87,3,54,27,0,87,89,1,0,0,0,88,70,1,0, + 0,0,88,74,1,0,0,0,88,81,1,0,0,0,89,5,1,0,0,0,90,91,7,0,0,0,91,7, + 1,0,0,0,92,97,3,54,27,0,93,94,5,10,0,0,94,96,3,54,27,0,95,93,1,0, + 0,0,96,99,1,0,0,0,97,95,1,0,0,0,97,98,1,0,0,0,98,9,1,0,0,0,99,97, + 1,0,0,0,100,102,5,11,0,0,101,100,1,0,0,0,102,103,1,0,0,0,103,101, + 1,0,0,0,103,104,1,0,0,0,104,11,1,0,0,0,105,106,5,48,0,0,106,115, + 5,12,0,0,107,112,3,54,27,0,108,109,5,10,0,0,109,111,3,54,27,0,110, + 108,1,0,0,0,111,114,1,0,0,0,112,110,1,0,0,0,112,113,1,0,0,0,113, + 116,1,0,0,0,114,112,1,0,0,0,115,107,1,0,0,0,115,116,1,0,0,0,116, + 117,1,0,0,0,117,132,5,13,0,0,118,119,7,1,0,0,119,128,5,12,0,0,120, + 125,5,48,0,0,121,122,5,10,0,0,122,124,5,48,0,0,123,121,1,0,0,0,124, + 127,1,0,0,0,125,123,1,0,0,0,125,126,1,0,0,0,126,129,1,0,0,0,127, + 125,1,0,0,0,128,120,1,0,0,0,128,129,1,0,0,0,129,130,1,0,0,0,130, + 132,5,13,0,0,131,105,1,0,0,0,131,118,1,0,0,0,132,13,1,0,0,0,133, + 134,3,16,8,0,134,139,3,18,9,0,135,136,5,10,0,0,136,138,3,18,9,0, + 137,135,1,0,0,0,138,141,1,0,0,0,139,137,1,0,0,0,139,140,1,0,0,0, + 140,15,1,0,0,0,141,139,1,0,0,0,142,165,5,34,0,0,143,165,5,35,0,0, + 144,165,5,36,0,0,145,165,5,37,0,0,146,165,5,38,0,0,147,165,5,39, + 0,0,148,165,5,40,0,0,149,165,5,41,0,0,150,154,5,42,0,0,151,153,5, + 11,0,0,152,151,1,0,0,0,153,156,1,0,0,0,154,152,1,0,0,0,154,155,1, + 0,0,0,155,165,1,0,0,0,156,154,1,0,0,0,157,161,5,43,0,0,158,160,5, + 11,0,0,159,158,1,0,0,0,160,163,1,0,0,0,161,159,1,0,0,0,161,162,1, + 0,0,0,162,165,1,0,0,0,163,161,1,0,0,0,164,142,1,0,0,0,164,143,1, + 0,0,0,164,144,1,0,0,0,164,145,1,0,0,0,164,146,1,0,0,0,164,147,1, + 0,0,0,164,148,1,0,0,0,164,149,1,0,0,0,164,150,1,0,0,0,164,157,1, + 0,0,0,165,17,1,0,0,0,166,172,5,48,0,0,167,168,5,14,0,0,168,169,5, + 44,0,0,169,170,5,10,0,0,170,171,5,44,0,0,171,173,5,15,0,0,172,167, + 1,0,0,0,172,173,1,0,0,0,173,188,1,0,0,0,174,175,5,14,0,0,175,176, + 5,44,0,0,176,177,5,16,0,0,177,184,5,44,0,0,178,179,5,10,0,0,179, + 180,5,44,0,0,180,181,5,16,0,0,181,183,5,44,0,0,182,178,1,0,0,0,183, + 186,1,0,0,0,184,182,1,0,0,0,184,185,1,0,0,0,185,187,1,0,0,0,186, + 184,1,0,0,0,187,189,5,15,0,0,188,174,1,0,0,0,188,189,1,0,0,0,189, + 193,1,0,0,0,190,191,5,14,0,0,191,192,5,44,0,0,192,194,5,15,0,0,193, + 190,1,0,0,0,193,194,1,0,0,0,194,196,1,0,0,0,195,197,7,2,0,0,196, + 195,1,0,0,0,196,197,1,0,0,0,197,201,1,0,0,0,198,200,5,11,0,0,199, + 198,1,0,0,0,200,203,1,0,0,0,201,199,1,0,0,0,201,202,1,0,0,0,202, + 206,1,0,0,0,203,201,1,0,0,0,204,205,5,3,0,0,205,207,3,54,27,0,206, + 204,1,0,0,0,206,207,1,0,0,0,207,19,1,0,0,0,208,209,5,14,0,0,209, + 210,5,44,0,0,210,211,5,16,0,0,211,218,5,44,0,0,212,213,5,10,0,0, + 213,214,5,44,0,0,214,215,5,16,0,0,215,217,5,44,0,0,216,212,1,0,0, + 0,217,220,1,0,0,0,218,216,1,0,0,0,218,219,1,0,0,0,219,221,1,0,0, + 0,220,218,1,0,0,0,221,222,5,15,0,0,222,21,1,0,0,0,223,224,5,27,0, + 0,224,229,3,24,12,0,225,226,5,10,0,0,226,228,3,24,12,0,227,225,1, + 0,0,0,228,231,1,0,0,0,229,227,1,0,0,0,229,230,1,0,0,0,230,23,1,0, + 0,0,231,229,1,0,0,0,232,233,5,48,0,0,233,234,5,3,0,0,234,235,3,54, + 27,0,235,25,1,0,0,0,236,237,5,28,0,0,237,241,5,48,0,0,238,239,5, + 12,0,0,239,240,5,48,0,0,240,242,5,13,0,0,241,238,1,0,0,0,241,242, + 1,0,0,0,242,245,1,0,0,0,243,244,5,10,0,0,244,246,3,54,27,0,245,243, + 1,0,0,0,246,247,1,0,0,0,247,245,1,0,0,0,247,248,1,0,0,0,248,27,1, + 0,0,0,249,250,5,1,0,0,250,255,3,54,27,0,251,252,7,3,0,0,252,254, + 3,54,27,0,253,251,1,0,0,0,254,257,1,0,0,0,255,253,1,0,0,0,255,256, + 1,0,0,0,256,258,1,0,0,0,257,255,1,0,0,0,258,259,5,2,0,0,259,29,1, + 0,0,0,260,261,5,48,0,0,261,262,5,48,0,0,262,264,5,3,0,0,263,265, + 7,4,0,0,264,263,1,0,0,0,264,265,1,0,0,0,265,269,1,0,0,0,266,269, + 5,45,0,0,267,269,5,44,0,0,268,260,1,0,0,0,268,266,1,0,0,0,268,267, + 1,0,0,0,269,31,1,0,0,0,270,276,3,36,18,0,271,276,3,38,19,0,272,276, + 3,44,22,0,273,276,3,48,24,0,274,276,3,50,25,0,275,270,1,0,0,0,275, + 271,1,0,0,0,275,272,1,0,0,0,275,273,1,0,0,0,275,274,1,0,0,0,276, + 33,1,0,0,0,277,279,5,48,0,0,278,280,7,5,0,0,279,278,1,0,0,0,279, + 280,1,0,0,0,280,35,1,0,0,0,281,282,5,32,0,0,282,287,5,48,0,0,283, + 284,5,10,0,0,284,286,5,48,0,0,285,283,1,0,0,0,286,289,1,0,0,0,287, + 285,1,0,0,0,287,288,1,0,0,0,288,37,1,0,0,0,289,287,1,0,0,0,290,291, + 5,29,0,0,291,296,3,42,21,0,292,293,5,10,0,0,293,295,3,42,21,0,294, + 292,1,0,0,0,295,298,1,0,0,0,296,294,1,0,0,0,296,297,1,0,0,0,297, + 39,1,0,0,0,298,296,1,0,0,0,299,301,5,48,0,0,300,302,3,10,5,0,301, + 300,1,0,0,0,301,302,1,0,0,0,302,41,1,0,0,0,303,304,3,40,20,0,304, + 305,5,3,0,0,305,307,3,54,27,0,306,308,3,54,27,0,307,306,1,0,0,0, + 307,308,1,0,0,0,308,43,1,0,0,0,309,310,5,30,0,0,310,315,3,46,23, + 0,311,312,5,10,0,0,312,314,3,46,23,0,313,311,1,0,0,0,314,317,1,0, + 0,0,315,313,1,0,0,0,315,316,1,0,0,0,316,45,1,0,0,0,317,315,1,0,0, + 0,318,320,3,54,27,0,319,321,3,54,27,0,320,319,1,0,0,0,320,321,1, + 0,0,0,321,47,1,0,0,0,322,323,5,48,0,0,323,335,3,12,6,0,324,325,5, + 1,0,0,325,330,3,30,15,0,326,327,5,10,0,0,327,329,3,30,15,0,328,326, + 1,0,0,0,329,332,1,0,0,0,330,328,1,0,0,0,330,331,1,0,0,0,331,333, + 1,0,0,0,332,330,1,0,0,0,333,334,5,2,0,0,334,336,1,0,0,0,335,324, + 1,0,0,0,335,336,1,0,0,0,336,337,1,0,0,0,337,338,5,48,0,0,338,339, + 5,20,0,0,339,340,5,48,0,0,340,49,1,0,0,0,341,342,5,31,0,0,342,343, + 5,48,0,0,343,344,5,20,0,0,344,355,5,48,0,0,345,346,5,33,0,0,346, + 351,5,48,0,0,347,348,5,10,0,0,348,350,5,48,0,0,349,347,1,0,0,0,350, + 353,1,0,0,0,351,349,1,0,0,0,351,352,1,0,0,0,352,355,1,0,0,0,353, + 351,1,0,0,0,354,341,1,0,0,0,354,345,1,0,0,0,355,51,1,0,0,0,356,358, + 5,48,0,0,357,359,5,21,0,0,358,357,1,0,0,0,359,360,1,0,0,0,360,358, + 1,0,0,0,360,361,1,0,0,0,361,365,1,0,0,0,362,365,5,22,0,0,363,365, + 5,23,0,0,364,356,1,0,0,0,364,362,1,0,0,0,364,363,1,0,0,0,365,53, + 1,0,0,0,366,367,6,27,-1,0,367,409,5,46,0,0,368,369,5,18,0,0,369, + 409,3,54,27,12,370,409,5,45,0,0,371,409,5,44,0,0,372,376,5,48,0, + 0,373,375,5,11,0,0,374,373,1,0,0,0,375,378,1,0,0,0,376,374,1,0,0, + 0,376,377,1,0,0,0,377,409,1,0,0,0,378,376,1,0,0,0,379,409,3,52,26, + 0,380,381,5,48,0,0,381,382,5,1,0,0,382,387,3,54,27,0,383,384,5,10, + 0,0,384,386,3,54,27,0,385,383,1,0,0,0,386,389,1,0,0,0,387,385,1, + 0,0,0,387,388,1,0,0,0,388,390,1,0,0,0,389,387,1,0,0,0,390,391,5, + 2,0,0,391,409,1,0,0,0,392,409,3,12,6,0,393,409,3,28,14,0,394,395, + 5,12,0,0,395,396,3,54,27,0,396,397,5,13,0,0,397,409,1,0,0,0,398, + 400,5,48,0,0,399,398,1,0,0,0,399,400,1,0,0,0,400,401,1,0,0,0,401, + 405,3,20,10,0,402,404,5,11,0,0,403,402,1,0,0,0,404,407,1,0,0,0,405, + 403,1,0,0,0,405,406,1,0,0,0,406,409,1,0,0,0,407,405,1,0,0,0,408, + 366,1,0,0,0,408,368,1,0,0,0,408,370,1,0,0,0,408,371,1,0,0,0,408, + 372,1,0,0,0,408,379,1,0,0,0,408,380,1,0,0,0,408,392,1,0,0,0,408, + 393,1,0,0,0,408,394,1,0,0,0,408,399,1,0,0,0,409,427,1,0,0,0,410, + 411,10,16,0,0,411,412,5,24,0,0,412,426,3,54,27,17,413,414,10,15, + 0,0,414,415,7,6,0,0,415,426,3,54,27,16,416,417,10,14,0,0,417,418, + 7,2,0,0,418,426,3,54,27,15,419,420,10,3,0,0,420,421,5,3,0,0,421, + 426,3,54,27,4,422,423,10,2,0,0,423,424,5,16,0,0,424,426,3,54,27, + 3,425,410,1,0,0,0,425,413,1,0,0,0,425,416,1,0,0,0,425,419,1,0,0, + 0,425,422,1,0,0,0,426,429,1,0,0,0,427,425,1,0,0,0,427,428,1,0,0, + 0,428,55,1,0,0,0,429,427,1,0,0,0,50,59,68,83,88,97,103,112,115,125, + 128,131,139,154,161,164,172,184,188,193,196,201,206,218,229,241, + 247,255,264,268,275,279,287,296,301,307,315,320,330,335,351,354, + 360,364,376,387,399,405,408,425,427 + ] + +class AutolevParser ( Parser ): + + grammarFileName = "Autolev.g4" + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + sharedContextCache = PredictionContextCache() + + literalNames = [ "", "'['", "']'", "'='", "'+='", "'-='", "':='", + "'*='", "'/='", "'^='", "','", "'''", "'('", "')'", + "'{'", "'}'", "':'", "'+'", "'-'", "';'", "'.'", "'>'", + "'0>'", "'1>>'", "'^'", "'*'", "'/'" ] + + symbolicNames = [ "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "Mass", "Inertia", + "Input", "Output", "Save", "UnitSystem", "Encode", + "Newtonian", "Frames", "Bodies", "Particles", "Points", + "Constants", "Specifieds", "Imaginary", "Variables", + "MotionVariables", "INT", "FLOAT", "EXP", "LINE_COMMENT", + "ID", "WS" ] + + RULE_prog = 0 + RULE_stat = 1 + RULE_assignment = 2 + RULE_equals = 3 + RULE_index = 4 + RULE_diff = 5 + RULE_functionCall = 6 + RULE_varDecl = 7 + RULE_varType = 8 + RULE_varDecl2 = 9 + RULE_ranges = 10 + RULE_massDecl = 11 + RULE_massDecl2 = 12 + RULE_inertiaDecl = 13 + RULE_matrix = 14 + RULE_matrixInOutput = 15 + RULE_codeCommands = 16 + RULE_settings = 17 + RULE_units = 18 + RULE_inputs = 19 + RULE_id_diff = 20 + RULE_inputs2 = 21 + RULE_outputs = 22 + RULE_outputs2 = 23 + RULE_codegen = 24 + RULE_commands = 25 + RULE_vec = 26 + RULE_expr = 27 + + ruleNames = [ "prog", "stat", "assignment", "equals", "index", "diff", + "functionCall", "varDecl", "varType", "varDecl2", "ranges", + "massDecl", "massDecl2", "inertiaDecl", "matrix", "matrixInOutput", + "codeCommands", "settings", "units", "inputs", "id_diff", + "inputs2", "outputs", "outputs2", "codegen", "commands", + "vec", "expr" ] + + EOF = Token.EOF + T__0=1 + T__1=2 + T__2=3 + T__3=4 + T__4=5 + T__5=6 + T__6=7 + T__7=8 + T__8=9 + T__9=10 + T__10=11 + T__11=12 + T__12=13 + T__13=14 + T__14=15 + T__15=16 + T__16=17 + T__17=18 + T__18=19 + T__19=20 + T__20=21 + T__21=22 + T__22=23 + T__23=24 + T__24=25 + T__25=26 + Mass=27 + Inertia=28 + Input=29 + Output=30 + Save=31 + UnitSystem=32 + Encode=33 + Newtonian=34 + Frames=35 + Bodies=36 + Particles=37 + Points=38 + Constants=39 + Specifieds=40 + Imaginary=41 + Variables=42 + MotionVariables=43 + INT=44 + FLOAT=45 + EXP=46 + LINE_COMMENT=47 + ID=48 + WS=49 + + def __init__(self, input:TokenStream, output:TextIO = sys.stdout): + super().__init__(input, output) + self.checkVersion("4.11.1") + self._interp = ParserATNSimulator(self, self.atn, self.decisionsToDFA, self.sharedContextCache) + self._predicates = None + + + + + class ProgContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def stat(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.StatContext) + else: + return self.getTypedRuleContext(AutolevParser.StatContext,i) + + + def getRuleIndex(self): + return AutolevParser.RULE_prog + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterProg" ): + listener.enterProg(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitProg" ): + listener.exitProg(self) + + + + + def prog(self): + + localctx = AutolevParser.ProgContext(self, self._ctx, self.state) + self.enterRule(localctx, 0, self.RULE_prog) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 57 + self._errHandler.sync(self) + _la = self._input.LA(1) + while True: + self.state = 56 + self.stat() + self.state = 59 + self._errHandler.sync(self) + _la = self._input.LA(1) + if not (((_la) & ~0x3f) == 0 and ((1 << _la) & 299067041120256) != 0): + break + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class StatContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def varDecl(self): + return self.getTypedRuleContext(AutolevParser.VarDeclContext,0) + + + def functionCall(self): + return self.getTypedRuleContext(AutolevParser.FunctionCallContext,0) + + + def codeCommands(self): + return self.getTypedRuleContext(AutolevParser.CodeCommandsContext,0) + + + def massDecl(self): + return self.getTypedRuleContext(AutolevParser.MassDeclContext,0) + + + def inertiaDecl(self): + return self.getTypedRuleContext(AutolevParser.InertiaDeclContext,0) + + + def assignment(self): + return self.getTypedRuleContext(AutolevParser.AssignmentContext,0) + + + def settings(self): + return self.getTypedRuleContext(AutolevParser.SettingsContext,0) + + + def getRuleIndex(self): + return AutolevParser.RULE_stat + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterStat" ): + listener.enterStat(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitStat" ): + listener.exitStat(self) + + + + + def stat(self): + + localctx = AutolevParser.StatContext(self, self._ctx, self.state) + self.enterRule(localctx, 2, self.RULE_stat) + try: + self.state = 68 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,1,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 61 + self.varDecl() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 62 + self.functionCall() + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 63 + self.codeCommands() + pass + + elif la_ == 4: + self.enterOuterAlt(localctx, 4) + self.state = 64 + self.massDecl() + pass + + elif la_ == 5: + self.enterOuterAlt(localctx, 5) + self.state = 65 + self.inertiaDecl() + pass + + elif la_ == 6: + self.enterOuterAlt(localctx, 6) + self.state = 66 + self.assignment() + pass + + elif la_ == 7: + self.enterOuterAlt(localctx, 7) + self.state = 67 + self.settings() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class AssignmentContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return AutolevParser.RULE_assignment + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class VecAssignContext(AssignmentContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.AssignmentContext + super().__init__(parser) + self.copyFrom(ctx) + + def vec(self): + return self.getTypedRuleContext(AutolevParser.VecContext,0) + + def equals(self): + return self.getTypedRuleContext(AutolevParser.EqualsContext,0) + + def expr(self): + return self.getTypedRuleContext(AutolevParser.ExprContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterVecAssign" ): + listener.enterVecAssign(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitVecAssign" ): + listener.exitVecAssign(self) + + + class RegularAssignContext(AssignmentContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.AssignmentContext + super().__init__(parser) + self.copyFrom(ctx) + + def ID(self): + return self.getToken(AutolevParser.ID, 0) + def equals(self): + return self.getTypedRuleContext(AutolevParser.EqualsContext,0) + + def expr(self): + return self.getTypedRuleContext(AutolevParser.ExprContext,0) + + def diff(self): + return self.getTypedRuleContext(AutolevParser.DiffContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRegularAssign" ): + listener.enterRegularAssign(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRegularAssign" ): + listener.exitRegularAssign(self) + + + class IndexAssignContext(AssignmentContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.AssignmentContext + super().__init__(parser) + self.copyFrom(ctx) + + def ID(self): + return self.getToken(AutolevParser.ID, 0) + def index(self): + return self.getTypedRuleContext(AutolevParser.IndexContext,0) + + def equals(self): + return self.getTypedRuleContext(AutolevParser.EqualsContext,0) + + def expr(self): + return self.getTypedRuleContext(AutolevParser.ExprContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterIndexAssign" ): + listener.enterIndexAssign(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitIndexAssign" ): + listener.exitIndexAssign(self) + + + + def assignment(self): + + localctx = AutolevParser.AssignmentContext(self, self._ctx, self.state) + self.enterRule(localctx, 4, self.RULE_assignment) + self._la = 0 # Token type + try: + self.state = 88 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,3,self._ctx) + if la_ == 1: + localctx = AutolevParser.VecAssignContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 70 + self.vec() + self.state = 71 + self.equals() + self.state = 72 + self.expr(0) + pass + + elif la_ == 2: + localctx = AutolevParser.IndexAssignContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 74 + self.match(AutolevParser.ID) + self.state = 75 + self.match(AutolevParser.T__0) + self.state = 76 + self.index() + self.state = 77 + self.match(AutolevParser.T__1) + self.state = 78 + self.equals() + self.state = 79 + self.expr(0) + pass + + elif la_ == 3: + localctx = AutolevParser.RegularAssignContext(self, localctx) + self.enterOuterAlt(localctx, 3) + self.state = 81 + self.match(AutolevParser.ID) + self.state = 83 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==11: + self.state = 82 + self.diff() + + + self.state = 85 + self.equals() + self.state = 86 + self.expr(0) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class EqualsContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return AutolevParser.RULE_equals + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterEquals" ): + listener.enterEquals(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitEquals" ): + listener.exitEquals(self) + + + + + def equals(self): + + localctx = AutolevParser.EqualsContext(self, self._ctx, self.state) + self.enterRule(localctx, 6, self.RULE_equals) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 90 + _la = self._input.LA(1) + if not(((_la) & ~0x3f) == 0 and ((1 << _la) & 1016) != 0): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class IndexContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.ExprContext) + else: + return self.getTypedRuleContext(AutolevParser.ExprContext,i) + + + def getRuleIndex(self): + return AutolevParser.RULE_index + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterIndex" ): + listener.enterIndex(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitIndex" ): + listener.exitIndex(self) + + + + + def index(self): + + localctx = AutolevParser.IndexContext(self, self._ctx, self.state) + self.enterRule(localctx, 8, self.RULE_index) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 92 + self.expr(0) + self.state = 97 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==10: + self.state = 93 + self.match(AutolevParser.T__9) + self.state = 94 + self.expr(0) + self.state = 99 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class DiffContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return AutolevParser.RULE_diff + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDiff" ): + listener.enterDiff(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDiff" ): + listener.exitDiff(self) + + + + + def diff(self): + + localctx = AutolevParser.DiffContext(self, self._ctx, self.state) + self.enterRule(localctx, 10, self.RULE_diff) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 101 + self._errHandler.sync(self) + _la = self._input.LA(1) + while True: + self.state = 100 + self.match(AutolevParser.T__10) + self.state = 103 + self._errHandler.sync(self) + _la = self._input.LA(1) + if not (_la==11): + break + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class FunctionCallContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ID(self, i:int=None): + if i is None: + return self.getTokens(AutolevParser.ID) + else: + return self.getToken(AutolevParser.ID, i) + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.ExprContext) + else: + return self.getTypedRuleContext(AutolevParser.ExprContext,i) + + + def Mass(self): + return self.getToken(AutolevParser.Mass, 0) + + def Inertia(self): + return self.getToken(AutolevParser.Inertia, 0) + + def getRuleIndex(self): + return AutolevParser.RULE_functionCall + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFunctionCall" ): + listener.enterFunctionCall(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFunctionCall" ): + listener.exitFunctionCall(self) + + + + + def functionCall(self): + + localctx = AutolevParser.FunctionCallContext(self, self._ctx, self.state) + self.enterRule(localctx, 12, self.RULE_functionCall) + self._la = 0 # Token type + try: + self.state = 131 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [48]: + self.enterOuterAlt(localctx, 1) + self.state = 105 + self.match(AutolevParser.ID) + self.state = 106 + self.match(AutolevParser.T__11) + self.state = 115 + self._errHandler.sync(self) + _la = self._input.LA(1) + if ((_la) & ~0x3f) == 0 and ((1 << _la) & 404620694540290) != 0: + self.state = 107 + self.expr(0) + self.state = 112 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==10: + self.state = 108 + self.match(AutolevParser.T__9) + self.state = 109 + self.expr(0) + self.state = 114 + self._errHandler.sync(self) + _la = self._input.LA(1) + + + + self.state = 117 + self.match(AutolevParser.T__12) + pass + elif token in [27, 28]: + self.enterOuterAlt(localctx, 2) + self.state = 118 + _la = self._input.LA(1) + if not(_la==27 or _la==28): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 119 + self.match(AutolevParser.T__11) + self.state = 128 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==48: + self.state = 120 + self.match(AutolevParser.ID) + self.state = 125 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==10: + self.state = 121 + self.match(AutolevParser.T__9) + self.state = 122 + self.match(AutolevParser.ID) + self.state = 127 + self._errHandler.sync(self) + _la = self._input.LA(1) + + + + self.state = 130 + self.match(AutolevParser.T__12) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class VarDeclContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def varType(self): + return self.getTypedRuleContext(AutolevParser.VarTypeContext,0) + + + def varDecl2(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.VarDecl2Context) + else: + return self.getTypedRuleContext(AutolevParser.VarDecl2Context,i) + + + def getRuleIndex(self): + return AutolevParser.RULE_varDecl + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterVarDecl" ): + listener.enterVarDecl(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitVarDecl" ): + listener.exitVarDecl(self) + + + + + def varDecl(self): + + localctx = AutolevParser.VarDeclContext(self, self._ctx, self.state) + self.enterRule(localctx, 14, self.RULE_varDecl) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 133 + self.varType() + self.state = 134 + self.varDecl2() + self.state = 139 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==10: + self.state = 135 + self.match(AutolevParser.T__9) + self.state = 136 + self.varDecl2() + self.state = 141 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class VarTypeContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def Newtonian(self): + return self.getToken(AutolevParser.Newtonian, 0) + + def Frames(self): + return self.getToken(AutolevParser.Frames, 0) + + def Bodies(self): + return self.getToken(AutolevParser.Bodies, 0) + + def Particles(self): + return self.getToken(AutolevParser.Particles, 0) + + def Points(self): + return self.getToken(AutolevParser.Points, 0) + + def Constants(self): + return self.getToken(AutolevParser.Constants, 0) + + def Specifieds(self): + return self.getToken(AutolevParser.Specifieds, 0) + + def Imaginary(self): + return self.getToken(AutolevParser.Imaginary, 0) + + def Variables(self): + return self.getToken(AutolevParser.Variables, 0) + + def MotionVariables(self): + return self.getToken(AutolevParser.MotionVariables, 0) + + def getRuleIndex(self): + return AutolevParser.RULE_varType + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterVarType" ): + listener.enterVarType(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitVarType" ): + listener.exitVarType(self) + + + + + def varType(self): + + localctx = AutolevParser.VarTypeContext(self, self._ctx, self.state) + self.enterRule(localctx, 16, self.RULE_varType) + self._la = 0 # Token type + try: + self.state = 164 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [34]: + self.enterOuterAlt(localctx, 1) + self.state = 142 + self.match(AutolevParser.Newtonian) + pass + elif token in [35]: + self.enterOuterAlt(localctx, 2) + self.state = 143 + self.match(AutolevParser.Frames) + pass + elif token in [36]: + self.enterOuterAlt(localctx, 3) + self.state = 144 + self.match(AutolevParser.Bodies) + pass + elif token in [37]: + self.enterOuterAlt(localctx, 4) + self.state = 145 + self.match(AutolevParser.Particles) + pass + elif token in [38]: + self.enterOuterAlt(localctx, 5) + self.state = 146 + self.match(AutolevParser.Points) + pass + elif token in [39]: + self.enterOuterAlt(localctx, 6) + self.state = 147 + self.match(AutolevParser.Constants) + pass + elif token in [40]: + self.enterOuterAlt(localctx, 7) + self.state = 148 + self.match(AutolevParser.Specifieds) + pass + elif token in [41]: + self.enterOuterAlt(localctx, 8) + self.state = 149 + self.match(AutolevParser.Imaginary) + pass + elif token in [42]: + self.enterOuterAlt(localctx, 9) + self.state = 150 + self.match(AutolevParser.Variables) + self.state = 154 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==11: + self.state = 151 + self.match(AutolevParser.T__10) + self.state = 156 + self._errHandler.sync(self) + _la = self._input.LA(1) + + pass + elif token in [43]: + self.enterOuterAlt(localctx, 10) + self.state = 157 + self.match(AutolevParser.MotionVariables) + self.state = 161 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==11: + self.state = 158 + self.match(AutolevParser.T__10) + self.state = 163 + self._errHandler.sync(self) + _la = self._input.LA(1) + + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class VarDecl2Context(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ID(self): + return self.getToken(AutolevParser.ID, 0) + + def INT(self, i:int=None): + if i is None: + return self.getTokens(AutolevParser.INT) + else: + return self.getToken(AutolevParser.INT, i) + + def expr(self): + return self.getTypedRuleContext(AutolevParser.ExprContext,0) + + + def getRuleIndex(self): + return AutolevParser.RULE_varDecl2 + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterVarDecl2" ): + listener.enterVarDecl2(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitVarDecl2" ): + listener.exitVarDecl2(self) + + + + + def varDecl2(self): + + localctx = AutolevParser.VarDecl2Context(self, self._ctx, self.state) + self.enterRule(localctx, 18, self.RULE_varDecl2) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 166 + self.match(AutolevParser.ID) + self.state = 172 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,15,self._ctx) + if la_ == 1: + self.state = 167 + self.match(AutolevParser.T__13) + self.state = 168 + self.match(AutolevParser.INT) + self.state = 169 + self.match(AutolevParser.T__9) + self.state = 170 + self.match(AutolevParser.INT) + self.state = 171 + self.match(AutolevParser.T__14) + + + self.state = 188 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,17,self._ctx) + if la_ == 1: + self.state = 174 + self.match(AutolevParser.T__13) + self.state = 175 + self.match(AutolevParser.INT) + self.state = 176 + self.match(AutolevParser.T__15) + self.state = 177 + self.match(AutolevParser.INT) + self.state = 184 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==10: + self.state = 178 + self.match(AutolevParser.T__9) + self.state = 179 + self.match(AutolevParser.INT) + self.state = 180 + self.match(AutolevParser.T__15) + self.state = 181 + self.match(AutolevParser.INT) + self.state = 186 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 187 + self.match(AutolevParser.T__14) + + + self.state = 193 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==14: + self.state = 190 + self.match(AutolevParser.T__13) + self.state = 191 + self.match(AutolevParser.INT) + self.state = 192 + self.match(AutolevParser.T__14) + + + self.state = 196 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==17 or _la==18: + self.state = 195 + _la = self._input.LA(1) + if not(_la==17 or _la==18): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + + + self.state = 201 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==11: + self.state = 198 + self.match(AutolevParser.T__10) + self.state = 203 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 206 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==3: + self.state = 204 + self.match(AutolevParser.T__2) + self.state = 205 + self.expr(0) + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class RangesContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def INT(self, i:int=None): + if i is None: + return self.getTokens(AutolevParser.INT) + else: + return self.getToken(AutolevParser.INT, i) + + def getRuleIndex(self): + return AutolevParser.RULE_ranges + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRanges" ): + listener.enterRanges(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRanges" ): + listener.exitRanges(self) + + + + + def ranges(self): + + localctx = AutolevParser.RangesContext(self, self._ctx, self.state) + self.enterRule(localctx, 20, self.RULE_ranges) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 208 + self.match(AutolevParser.T__13) + self.state = 209 + self.match(AutolevParser.INT) + self.state = 210 + self.match(AutolevParser.T__15) + self.state = 211 + self.match(AutolevParser.INT) + self.state = 218 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==10: + self.state = 212 + self.match(AutolevParser.T__9) + self.state = 213 + self.match(AutolevParser.INT) + self.state = 214 + self.match(AutolevParser.T__15) + self.state = 215 + self.match(AutolevParser.INT) + self.state = 220 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 221 + self.match(AutolevParser.T__14) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class MassDeclContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def Mass(self): + return self.getToken(AutolevParser.Mass, 0) + + def massDecl2(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.MassDecl2Context) + else: + return self.getTypedRuleContext(AutolevParser.MassDecl2Context,i) + + + def getRuleIndex(self): + return AutolevParser.RULE_massDecl + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMassDecl" ): + listener.enterMassDecl(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMassDecl" ): + listener.exitMassDecl(self) + + + + + def massDecl(self): + + localctx = AutolevParser.MassDeclContext(self, self._ctx, self.state) + self.enterRule(localctx, 22, self.RULE_massDecl) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 223 + self.match(AutolevParser.Mass) + self.state = 224 + self.massDecl2() + self.state = 229 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==10: + self.state = 225 + self.match(AutolevParser.T__9) + self.state = 226 + self.massDecl2() + self.state = 231 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class MassDecl2Context(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ID(self): + return self.getToken(AutolevParser.ID, 0) + + def expr(self): + return self.getTypedRuleContext(AutolevParser.ExprContext,0) + + + def getRuleIndex(self): + return AutolevParser.RULE_massDecl2 + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMassDecl2" ): + listener.enterMassDecl2(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMassDecl2" ): + listener.exitMassDecl2(self) + + + + + def massDecl2(self): + + localctx = AutolevParser.MassDecl2Context(self, self._ctx, self.state) + self.enterRule(localctx, 24, self.RULE_massDecl2) + try: + self.enterOuterAlt(localctx, 1) + self.state = 232 + self.match(AutolevParser.ID) + self.state = 233 + self.match(AutolevParser.T__2) + self.state = 234 + self.expr(0) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class InertiaDeclContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def Inertia(self): + return self.getToken(AutolevParser.Inertia, 0) + + def ID(self, i:int=None): + if i is None: + return self.getTokens(AutolevParser.ID) + else: + return self.getToken(AutolevParser.ID, i) + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.ExprContext) + else: + return self.getTypedRuleContext(AutolevParser.ExprContext,i) + + + def getRuleIndex(self): + return AutolevParser.RULE_inertiaDecl + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterInertiaDecl" ): + listener.enterInertiaDecl(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitInertiaDecl" ): + listener.exitInertiaDecl(self) + + + + + def inertiaDecl(self): + + localctx = AutolevParser.InertiaDeclContext(self, self._ctx, self.state) + self.enterRule(localctx, 26, self.RULE_inertiaDecl) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 236 + self.match(AutolevParser.Inertia) + self.state = 237 + self.match(AutolevParser.ID) + self.state = 241 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==12: + self.state = 238 + self.match(AutolevParser.T__11) + self.state = 239 + self.match(AutolevParser.ID) + self.state = 240 + self.match(AutolevParser.T__12) + + + self.state = 245 + self._errHandler.sync(self) + _la = self._input.LA(1) + while True: + self.state = 243 + self.match(AutolevParser.T__9) + self.state = 244 + self.expr(0) + self.state = 247 + self._errHandler.sync(self) + _la = self._input.LA(1) + if not (_la==10): + break + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class MatrixContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.ExprContext) + else: + return self.getTypedRuleContext(AutolevParser.ExprContext,i) + + + def getRuleIndex(self): + return AutolevParser.RULE_matrix + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMatrix" ): + listener.enterMatrix(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMatrix" ): + listener.exitMatrix(self) + + + + + def matrix(self): + + localctx = AutolevParser.MatrixContext(self, self._ctx, self.state) + self.enterRule(localctx, 28, self.RULE_matrix) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 249 + self.match(AutolevParser.T__0) + self.state = 250 + self.expr(0) + self.state = 255 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==10 or _la==19: + self.state = 251 + _la = self._input.LA(1) + if not(_la==10 or _la==19): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 252 + self.expr(0) + self.state = 257 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 258 + self.match(AutolevParser.T__1) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class MatrixInOutputContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ID(self, i:int=None): + if i is None: + return self.getTokens(AutolevParser.ID) + else: + return self.getToken(AutolevParser.ID, i) + + def FLOAT(self): + return self.getToken(AutolevParser.FLOAT, 0) + + def INT(self): + return self.getToken(AutolevParser.INT, 0) + + def getRuleIndex(self): + return AutolevParser.RULE_matrixInOutput + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMatrixInOutput" ): + listener.enterMatrixInOutput(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMatrixInOutput" ): + listener.exitMatrixInOutput(self) + + + + + def matrixInOutput(self): + + localctx = AutolevParser.MatrixInOutputContext(self, self._ctx, self.state) + self.enterRule(localctx, 30, self.RULE_matrixInOutput) + self._la = 0 # Token type + try: + self.state = 268 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [48]: + self.enterOuterAlt(localctx, 1) + self.state = 260 + self.match(AutolevParser.ID) + + self.state = 261 + self.match(AutolevParser.ID) + self.state = 262 + self.match(AutolevParser.T__2) + self.state = 264 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==44 or _la==45: + self.state = 263 + _la = self._input.LA(1) + if not(_la==44 or _la==45): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + + + pass + elif token in [45]: + self.enterOuterAlt(localctx, 2) + self.state = 266 + self.match(AutolevParser.FLOAT) + pass + elif token in [44]: + self.enterOuterAlt(localctx, 3) + self.state = 267 + self.match(AutolevParser.INT) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class CodeCommandsContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def units(self): + return self.getTypedRuleContext(AutolevParser.UnitsContext,0) + + + def inputs(self): + return self.getTypedRuleContext(AutolevParser.InputsContext,0) + + + def outputs(self): + return self.getTypedRuleContext(AutolevParser.OutputsContext,0) + + + def codegen(self): + return self.getTypedRuleContext(AutolevParser.CodegenContext,0) + + + def commands(self): + return self.getTypedRuleContext(AutolevParser.CommandsContext,0) + + + def getRuleIndex(self): + return AutolevParser.RULE_codeCommands + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCodeCommands" ): + listener.enterCodeCommands(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCodeCommands" ): + listener.exitCodeCommands(self) + + + + + def codeCommands(self): + + localctx = AutolevParser.CodeCommandsContext(self, self._ctx, self.state) + self.enterRule(localctx, 32, self.RULE_codeCommands) + try: + self.state = 275 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [32]: + self.enterOuterAlt(localctx, 1) + self.state = 270 + self.units() + pass + elif token in [29]: + self.enterOuterAlt(localctx, 2) + self.state = 271 + self.inputs() + pass + elif token in [30]: + self.enterOuterAlt(localctx, 3) + self.state = 272 + self.outputs() + pass + elif token in [48]: + self.enterOuterAlt(localctx, 4) + self.state = 273 + self.codegen() + pass + elif token in [31, 33]: + self.enterOuterAlt(localctx, 5) + self.state = 274 + self.commands() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SettingsContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ID(self, i:int=None): + if i is None: + return self.getTokens(AutolevParser.ID) + else: + return self.getToken(AutolevParser.ID, i) + + def EXP(self): + return self.getToken(AutolevParser.EXP, 0) + + def FLOAT(self): + return self.getToken(AutolevParser.FLOAT, 0) + + def INT(self): + return self.getToken(AutolevParser.INT, 0) + + def getRuleIndex(self): + return AutolevParser.RULE_settings + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSettings" ): + listener.enterSettings(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSettings" ): + listener.exitSettings(self) + + + + + def settings(self): + + localctx = AutolevParser.SettingsContext(self, self._ctx, self.state) + self.enterRule(localctx, 34, self.RULE_settings) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 277 + self.match(AutolevParser.ID) + self.state = 279 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,30,self._ctx) + if la_ == 1: + self.state = 278 + _la = self._input.LA(1) + if not(((_la) & ~0x3f) == 0 and ((1 << _la) & 404620279021568) != 0): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnitsContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def UnitSystem(self): + return self.getToken(AutolevParser.UnitSystem, 0) + + def ID(self, i:int=None): + if i is None: + return self.getTokens(AutolevParser.ID) + else: + return self.getToken(AutolevParser.ID, i) + + def getRuleIndex(self): + return AutolevParser.RULE_units + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnits" ): + listener.enterUnits(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnits" ): + listener.exitUnits(self) + + + + + def units(self): + + localctx = AutolevParser.UnitsContext(self, self._ctx, self.state) + self.enterRule(localctx, 36, self.RULE_units) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 281 + self.match(AutolevParser.UnitSystem) + self.state = 282 + self.match(AutolevParser.ID) + self.state = 287 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==10: + self.state = 283 + self.match(AutolevParser.T__9) + self.state = 284 + self.match(AutolevParser.ID) + self.state = 289 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class InputsContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def Input(self): + return self.getToken(AutolevParser.Input, 0) + + def inputs2(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.Inputs2Context) + else: + return self.getTypedRuleContext(AutolevParser.Inputs2Context,i) + + + def getRuleIndex(self): + return AutolevParser.RULE_inputs + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterInputs" ): + listener.enterInputs(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitInputs" ): + listener.exitInputs(self) + + + + + def inputs(self): + + localctx = AutolevParser.InputsContext(self, self._ctx, self.state) + self.enterRule(localctx, 38, self.RULE_inputs) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 290 + self.match(AutolevParser.Input) + self.state = 291 + self.inputs2() + self.state = 296 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==10: + self.state = 292 + self.match(AutolevParser.T__9) + self.state = 293 + self.inputs2() + self.state = 298 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Id_diffContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ID(self): + return self.getToken(AutolevParser.ID, 0) + + def diff(self): + return self.getTypedRuleContext(AutolevParser.DiffContext,0) + + + def getRuleIndex(self): + return AutolevParser.RULE_id_diff + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterId_diff" ): + listener.enterId_diff(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitId_diff" ): + listener.exitId_diff(self) + + + + + def id_diff(self): + + localctx = AutolevParser.Id_diffContext(self, self._ctx, self.state) + self.enterRule(localctx, 40, self.RULE_id_diff) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 299 + self.match(AutolevParser.ID) + self.state = 301 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==11: + self.state = 300 + self.diff() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Inputs2Context(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def id_diff(self): + return self.getTypedRuleContext(AutolevParser.Id_diffContext,0) + + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.ExprContext) + else: + return self.getTypedRuleContext(AutolevParser.ExprContext,i) + + + def getRuleIndex(self): + return AutolevParser.RULE_inputs2 + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterInputs2" ): + listener.enterInputs2(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitInputs2" ): + listener.exitInputs2(self) + + + + + def inputs2(self): + + localctx = AutolevParser.Inputs2Context(self, self._ctx, self.state) + self.enterRule(localctx, 42, self.RULE_inputs2) + try: + self.enterOuterAlt(localctx, 1) + self.state = 303 + self.id_diff() + self.state = 304 + self.match(AutolevParser.T__2) + self.state = 305 + self.expr(0) + self.state = 307 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,34,self._ctx) + if la_ == 1: + self.state = 306 + self.expr(0) + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OutputsContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def Output(self): + return self.getToken(AutolevParser.Output, 0) + + def outputs2(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.Outputs2Context) + else: + return self.getTypedRuleContext(AutolevParser.Outputs2Context,i) + + + def getRuleIndex(self): + return AutolevParser.RULE_outputs + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOutputs" ): + listener.enterOutputs(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOutputs" ): + listener.exitOutputs(self) + + + + + def outputs(self): + + localctx = AutolevParser.OutputsContext(self, self._ctx, self.state) + self.enterRule(localctx, 44, self.RULE_outputs) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 309 + self.match(AutolevParser.Output) + self.state = 310 + self.outputs2() + self.state = 315 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==10: + self.state = 311 + self.match(AutolevParser.T__9) + self.state = 312 + self.outputs2() + self.state = 317 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Outputs2Context(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.ExprContext) + else: + return self.getTypedRuleContext(AutolevParser.ExprContext,i) + + + def getRuleIndex(self): + return AutolevParser.RULE_outputs2 + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOutputs2" ): + listener.enterOutputs2(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOutputs2" ): + listener.exitOutputs2(self) + + + + + def outputs2(self): + + localctx = AutolevParser.Outputs2Context(self, self._ctx, self.state) + self.enterRule(localctx, 46, self.RULE_outputs2) + try: + self.enterOuterAlt(localctx, 1) + self.state = 318 + self.expr(0) + self.state = 320 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,36,self._ctx) + if la_ == 1: + self.state = 319 + self.expr(0) + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class CodegenContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ID(self, i:int=None): + if i is None: + return self.getTokens(AutolevParser.ID) + else: + return self.getToken(AutolevParser.ID, i) + + def functionCall(self): + return self.getTypedRuleContext(AutolevParser.FunctionCallContext,0) + + + def matrixInOutput(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.MatrixInOutputContext) + else: + return self.getTypedRuleContext(AutolevParser.MatrixInOutputContext,i) + + + def getRuleIndex(self): + return AutolevParser.RULE_codegen + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCodegen" ): + listener.enterCodegen(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCodegen" ): + listener.exitCodegen(self) + + + + + def codegen(self): + + localctx = AutolevParser.CodegenContext(self, self._ctx, self.state) + self.enterRule(localctx, 48, self.RULE_codegen) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 322 + self.match(AutolevParser.ID) + self.state = 323 + self.functionCall() + self.state = 335 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==1: + self.state = 324 + self.match(AutolevParser.T__0) + self.state = 325 + self.matrixInOutput() + self.state = 330 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==10: + self.state = 326 + self.match(AutolevParser.T__9) + self.state = 327 + self.matrixInOutput() + self.state = 332 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 333 + self.match(AutolevParser.T__1) + + + self.state = 337 + self.match(AutolevParser.ID) + self.state = 338 + self.match(AutolevParser.T__19) + self.state = 339 + self.match(AutolevParser.ID) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class CommandsContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def Save(self): + return self.getToken(AutolevParser.Save, 0) + + def ID(self, i:int=None): + if i is None: + return self.getTokens(AutolevParser.ID) + else: + return self.getToken(AutolevParser.ID, i) + + def Encode(self): + return self.getToken(AutolevParser.Encode, 0) + + def getRuleIndex(self): + return AutolevParser.RULE_commands + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCommands" ): + listener.enterCommands(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCommands" ): + listener.exitCommands(self) + + + + + def commands(self): + + localctx = AutolevParser.CommandsContext(self, self._ctx, self.state) + self.enterRule(localctx, 50, self.RULE_commands) + self._la = 0 # Token type + try: + self.state = 354 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [31]: + self.enterOuterAlt(localctx, 1) + self.state = 341 + self.match(AutolevParser.Save) + self.state = 342 + self.match(AutolevParser.ID) + self.state = 343 + self.match(AutolevParser.T__19) + self.state = 344 + self.match(AutolevParser.ID) + pass + elif token in [33]: + self.enterOuterAlt(localctx, 2) + self.state = 345 + self.match(AutolevParser.Encode) + self.state = 346 + self.match(AutolevParser.ID) + self.state = 351 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==10: + self.state = 347 + self.match(AutolevParser.T__9) + self.state = 348 + self.match(AutolevParser.ID) + self.state = 353 + self._errHandler.sync(self) + _la = self._input.LA(1) + + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class VecContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ID(self): + return self.getToken(AutolevParser.ID, 0) + + def getRuleIndex(self): + return AutolevParser.RULE_vec + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterVec" ): + listener.enterVec(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitVec" ): + listener.exitVec(self) + + + + + def vec(self): + + localctx = AutolevParser.VecContext(self, self._ctx, self.state) + self.enterRule(localctx, 52, self.RULE_vec) + try: + self.state = 364 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [48]: + self.enterOuterAlt(localctx, 1) + self.state = 356 + self.match(AutolevParser.ID) + self.state = 358 + self._errHandler.sync(self) + _alt = 1 + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt == 1: + self.state = 357 + self.match(AutolevParser.T__20) + + else: + raise NoViableAltException(self) + self.state = 360 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,41,self._ctx) + + pass + elif token in [22]: + self.enterOuterAlt(localctx, 2) + self.state = 362 + self.match(AutolevParser.T__21) + pass + elif token in [23]: + self.enterOuterAlt(localctx, 3) + self.state = 363 + self.match(AutolevParser.T__22) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ExprContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return AutolevParser.RULE_expr + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + class ParensContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def expr(self): + return self.getTypedRuleContext(AutolevParser.ExprContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterParens" ): + listener.enterParens(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitParens" ): + listener.exitParens(self) + + + class VectorOrDyadicContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def vec(self): + return self.getTypedRuleContext(AutolevParser.VecContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterVectorOrDyadic" ): + listener.enterVectorOrDyadic(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitVectorOrDyadic" ): + listener.exitVectorOrDyadic(self) + + + class ExponentContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.ExprContext) + else: + return self.getTypedRuleContext(AutolevParser.ExprContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterExponent" ): + listener.enterExponent(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitExponent" ): + listener.exitExponent(self) + + + class MulDivContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.ExprContext) + else: + return self.getTypedRuleContext(AutolevParser.ExprContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMulDiv" ): + listener.enterMulDiv(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMulDiv" ): + listener.exitMulDiv(self) + + + class AddSubContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.ExprContext) + else: + return self.getTypedRuleContext(AutolevParser.ExprContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAddSub" ): + listener.enterAddSub(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAddSub" ): + listener.exitAddSub(self) + + + class FloatContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def FLOAT(self): + return self.getToken(AutolevParser.FLOAT, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFloat" ): + listener.enterFloat(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFloat" ): + listener.exitFloat(self) + + + class IntContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def INT(self): + return self.getToken(AutolevParser.INT, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterInt" ): + listener.enterInt(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitInt" ): + listener.exitInt(self) + + + class IdEqualsExprContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.ExprContext) + else: + return self.getTypedRuleContext(AutolevParser.ExprContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterIdEqualsExpr" ): + listener.enterIdEqualsExpr(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitIdEqualsExpr" ): + listener.exitIdEqualsExpr(self) + + + class NegativeOneContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def expr(self): + return self.getTypedRuleContext(AutolevParser.ExprContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNegativeOne" ): + listener.enterNegativeOne(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNegativeOne" ): + listener.exitNegativeOne(self) + + + class FunctionContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def functionCall(self): + return self.getTypedRuleContext(AutolevParser.FunctionCallContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFunction" ): + listener.enterFunction(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFunction" ): + listener.exitFunction(self) + + + class RangessContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def ranges(self): + return self.getTypedRuleContext(AutolevParser.RangesContext,0) + + def ID(self): + return self.getToken(AutolevParser.ID, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRangess" ): + listener.enterRangess(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRangess" ): + listener.exitRangess(self) + + + class ColonContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.ExprContext) + else: + return self.getTypedRuleContext(AutolevParser.ExprContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterColon" ): + listener.enterColon(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitColon" ): + listener.exitColon(self) + + + class IdContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def ID(self): + return self.getToken(AutolevParser.ID, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterId" ): + listener.enterId(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitId" ): + listener.exitId(self) + + + class ExpContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def EXP(self): + return self.getToken(AutolevParser.EXP, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterExp" ): + listener.enterExp(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitExp" ): + listener.exitExp(self) + + + class MatricesContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def matrix(self): + return self.getTypedRuleContext(AutolevParser.MatrixContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMatrices" ): + listener.enterMatrices(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMatrices" ): + listener.exitMatrices(self) + + + class IndexingContext(ExprContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AutolevParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def ID(self): + return self.getToken(AutolevParser.ID, 0) + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AutolevParser.ExprContext) + else: + return self.getTypedRuleContext(AutolevParser.ExprContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterIndexing" ): + listener.enterIndexing(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitIndexing" ): + listener.exitIndexing(self) + + + + def expr(self, _p:int=0): + _parentctx = self._ctx + _parentState = self.state + localctx = AutolevParser.ExprContext(self, self._ctx, _parentState) + _prevctx = localctx + _startState = 54 + self.enterRecursionRule(localctx, 54, self.RULE_expr, _p) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 408 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,47,self._ctx) + if la_ == 1: + localctx = AutolevParser.ExpContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + + self.state = 367 + self.match(AutolevParser.EXP) + pass + + elif la_ == 2: + localctx = AutolevParser.NegativeOneContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 368 + self.match(AutolevParser.T__17) + self.state = 369 + self.expr(12) + pass + + elif la_ == 3: + localctx = AutolevParser.FloatContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 370 + self.match(AutolevParser.FLOAT) + pass + + elif la_ == 4: + localctx = AutolevParser.IntContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 371 + self.match(AutolevParser.INT) + pass + + elif la_ == 5: + localctx = AutolevParser.IdContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 372 + self.match(AutolevParser.ID) + self.state = 376 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,43,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 373 + self.match(AutolevParser.T__10) + self.state = 378 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,43,self._ctx) + + pass + + elif la_ == 6: + localctx = AutolevParser.VectorOrDyadicContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 379 + self.vec() + pass + + elif la_ == 7: + localctx = AutolevParser.IndexingContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 380 + self.match(AutolevParser.ID) + self.state = 381 + self.match(AutolevParser.T__0) + self.state = 382 + self.expr(0) + self.state = 387 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==10: + self.state = 383 + self.match(AutolevParser.T__9) + self.state = 384 + self.expr(0) + self.state = 389 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 390 + self.match(AutolevParser.T__1) + pass + + elif la_ == 8: + localctx = AutolevParser.FunctionContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 392 + self.functionCall() + pass + + elif la_ == 9: + localctx = AutolevParser.MatricesContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 393 + self.matrix() + pass + + elif la_ == 10: + localctx = AutolevParser.ParensContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 394 + self.match(AutolevParser.T__11) + self.state = 395 + self.expr(0) + self.state = 396 + self.match(AutolevParser.T__12) + pass + + elif la_ == 11: + localctx = AutolevParser.RangessContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 399 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==48: + self.state = 398 + self.match(AutolevParser.ID) + + + self.state = 401 + self.ranges() + self.state = 405 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,46,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 402 + self.match(AutolevParser.T__10) + self.state = 407 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,46,self._ctx) + + pass + + + self._ctx.stop = self._input.LT(-1) + self.state = 427 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,49,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + if self._parseListeners is not None: + self.triggerExitRuleEvent() + _prevctx = localctx + self.state = 425 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,48,self._ctx) + if la_ == 1: + localctx = AutolevParser.ExponentContext(self, AutolevParser.ExprContext(self, _parentctx, _parentState)) + self.pushNewRecursionContext(localctx, _startState, self.RULE_expr) + self.state = 410 + if not self.precpred(self._ctx, 16): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 16)") + self.state = 411 + self.match(AutolevParser.T__23) + self.state = 412 + self.expr(17) + pass + + elif la_ == 2: + localctx = AutolevParser.MulDivContext(self, AutolevParser.ExprContext(self, _parentctx, _parentState)) + self.pushNewRecursionContext(localctx, _startState, self.RULE_expr) + self.state = 413 + if not self.precpred(self._ctx, 15): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 15)") + self.state = 414 + _la = self._input.LA(1) + if not(_la==25 or _la==26): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 415 + self.expr(16) + pass + + elif la_ == 3: + localctx = AutolevParser.AddSubContext(self, AutolevParser.ExprContext(self, _parentctx, _parentState)) + self.pushNewRecursionContext(localctx, _startState, self.RULE_expr) + self.state = 416 + if not self.precpred(self._ctx, 14): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 14)") + self.state = 417 + _la = self._input.LA(1) + if not(_la==17 or _la==18): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 418 + self.expr(15) + pass + + elif la_ == 4: + localctx = AutolevParser.IdEqualsExprContext(self, AutolevParser.ExprContext(self, _parentctx, _parentState)) + self.pushNewRecursionContext(localctx, _startState, self.RULE_expr) + self.state = 419 + if not self.precpred(self._ctx, 3): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 3)") + self.state = 420 + self.match(AutolevParser.T__2) + self.state = 421 + self.expr(4) + pass + + elif la_ == 5: + localctx = AutolevParser.ColonContext(self, AutolevParser.ExprContext(self, _parentctx, _parentState)) + self.pushNewRecursionContext(localctx, _startState, self.RULE_expr) + self.state = 422 + if not self.precpred(self._ctx, 2): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 2)") + self.state = 423 + self.match(AutolevParser.T__15) + self.state = 424 + self.expr(3) + pass + + + self.state = 429 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,49,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.unrollRecursionContexts(_parentctx) + return localctx + + + + def sempred(self, localctx:RuleContext, ruleIndex:int, predIndex:int): + if self._predicates == None: + self._predicates = dict() + self._predicates[27] = self.expr_sempred + pred = self._predicates.get(ruleIndex, None) + if pred is None: + raise Exception("No predicate with index:" + str(ruleIndex)) + else: + return pred(localctx, predIndex) + + def expr_sempred(self, localctx:ExprContext, predIndex:int): + if predIndex == 0: + return self.precpred(self._ctx, 16) + + + if predIndex == 1: + return self.precpred(self._ctx, 15) + + + if predIndex == 2: + return self.precpred(self._ctx, 14) + + + if predIndex == 3: + return self.precpred(self._ctx, 3) + + + if predIndex == 4: + return self.precpred(self._ctx, 2) + + + + + diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_build_autolev_antlr.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_build_autolev_antlr.py new file mode 100644 index 0000000000000000000000000000000000000000..8314b2f546c0a18a8e281768b60d66556c852e3b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_build_autolev_antlr.py @@ -0,0 +1,86 @@ +import os +import subprocess +import glob + +from sympy.utilities.misc import debug + +here = os.path.dirname(__file__) +grammar_file = os.path.abspath(os.path.join(here, "Autolev.g4")) +dir_autolev_antlr = os.path.join(here, "_antlr") + +header = '''\ +# *** GENERATED BY `setup.py antlr`, DO NOT EDIT BY HAND *** +# +# Generated with antlr4 +# antlr4 is licensed under the BSD-3-Clause License +# https://github.com/antlr/antlr4/blob/master/LICENSE.txt +''' + + +def check_antlr_version(): + debug("Checking antlr4 version...") + + try: + debug(subprocess.check_output(["antlr4"]) + .decode('utf-8').split("\n")[0]) + return True + except (subprocess.CalledProcessError, FileNotFoundError): + debug("The 'antlr4' command line tool is not installed, " + "or not on your PATH.\n" + "> Please refer to the README.md file for more information.") + return False + + +def build_parser(output_dir=dir_autolev_antlr): + check_antlr_version() + + debug("Updating ANTLR-generated code in {}".format(output_dir)) + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + with open(os.path.join(output_dir, "__init__.py"), "w+") as fp: + fp.write(header) + + args = [ + "antlr4", + grammar_file, + "-o", output_dir, + "-no-visitor", + ] + + debug("Running code generation...\n\t$ {}".format(" ".join(args))) + subprocess.check_output(args, cwd=output_dir) + + debug("Applying headers, removing unnecessary files and renaming...") + # Handle case insensitive file systems. If the files are already + # generated, they will be written to autolev* but Autolev*.* won't match them. + for path in (glob.glob(os.path.join(output_dir, "Autolev*.*")) or + glob.glob(os.path.join(output_dir, "autolev*.*"))): + + # Remove files ending in .interp or .tokens as they are not needed. + if not path.endswith(".py"): + os.unlink(path) + continue + + new_path = os.path.join(output_dir, os.path.basename(path).lower()) + with open(path, 'r') as f: + lines = [line.rstrip().replace('AutolevParser import', 'autolevparser import') +'\n' + for line in f] + + os.unlink(path) + + with open(new_path, "w") as out_file: + offset = 0 + while lines[offset].startswith('#'): + offset += 1 + out_file.write(header) + out_file.writelines(lines[offset:]) + + debug("\t{}".format(new_path)) + + return True + + +if __name__ == "__main__": + build_parser() diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_listener_autolev_antlr.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_listener_autolev_antlr.py new file mode 100644 index 0000000000000000000000000000000000000000..9ca2f8af88de18036b90788fd29d02707f098213 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_listener_autolev_antlr.py @@ -0,0 +1,2083 @@ +import collections +import warnings + +from sympy.external import import_module + +autolevparser = import_module('sympy.parsing.autolev._antlr.autolevparser', + import_kwargs={'fromlist': ['AutolevParser']}) +autolevlexer = import_module('sympy.parsing.autolev._antlr.autolevlexer', + import_kwargs={'fromlist': ['AutolevLexer']}) +autolevlistener = import_module('sympy.parsing.autolev._antlr.autolevlistener', + import_kwargs={'fromlist': ['AutolevListener']}) + +AutolevParser = getattr(autolevparser, 'AutolevParser', None) +AutolevLexer = getattr(autolevlexer, 'AutolevLexer', None) +AutolevListener = getattr(autolevlistener, 'AutolevListener', None) + + +def strfunc(z): + if z == 0: + return "" + elif z == 1: + return "_d" + else: + return "_" + "d" * z + +def declare_phy_entities(self, ctx, phy_type, i, j=None): + if phy_type in ("frame", "newtonian"): + declare_frames(self, ctx, i, j) + elif phy_type == "particle": + declare_particles(self, ctx, i, j) + elif phy_type == "point": + declare_points(self, ctx, i, j) + elif phy_type == "bodies": + declare_bodies(self, ctx, i, j) + +def declare_frames(self, ctx, i, j=None): + if "{" in ctx.getText(): + if j: + name1 = ctx.ID().getText().lower() + str(i) + str(j) + else: + name1 = ctx.ID().getText().lower() + str(i) + else: + name1 = ctx.ID().getText().lower() + name2 = "frame_" + name1 + if self.getValue(ctx.parentCtx.varType()) == "newtonian": + self.newtonian = name2 + + self.symbol_table2.update({name1: name2}) + + self.symbol_table.update({name1 + "1>": name2 + ".x"}) + self.symbol_table.update({name1 + "2>": name2 + ".y"}) + self.symbol_table.update({name1 + "3>": name2 + ".z"}) + + self.type2.update({name1: "frame"}) + self.write(name2 + " = " + "_me.ReferenceFrame('" + name1 + "')\n") + +def declare_points(self, ctx, i, j=None): + if "{" in ctx.getText(): + if j: + name1 = ctx.ID().getText().lower() + str(i) + str(j) + else: + name1 = ctx.ID().getText().lower() + str(i) + else: + name1 = ctx.ID().getText().lower() + + name2 = "point_" + name1 + + self.symbol_table2.update({name1: name2}) + self.type2.update({name1: "point"}) + self.write(name2 + " = " + "_me.Point('" + name1 + "')\n") + +def declare_particles(self, ctx, i, j=None): + if "{" in ctx.getText(): + if j: + name1 = ctx.ID().getText().lower() + str(i) + str(j) + else: + name1 = ctx.ID().getText().lower() + str(i) + else: + name1 = ctx.ID().getText().lower() + + name2 = "particle_" + name1 + + self.symbol_table2.update({name1: name2}) + self.type2.update({name1: "particle"}) + self.bodies.update({name1: name2}) + self.write(name2 + " = " + "_me.Particle('" + name1 + "', " + "_me.Point('" + + name1 + "_pt" + "'), " + "_sm.Symbol('m'))\n") + +def declare_bodies(self, ctx, i, j=None): + if "{" in ctx.getText(): + if j: + name1 = ctx.ID().getText().lower() + str(i) + str(j) + else: + name1 = ctx.ID().getText().lower() + str(i) + else: + name1 = ctx.ID().getText().lower() + + name2 = "body_" + name1 + self.bodies.update({name1: name2}) + masscenter = name2 + "_cm" + refFrame = name2 + "_f" + + self.symbol_table2.update({name1: name2}) + self.symbol_table2.update({name1 + "o": masscenter}) + self.symbol_table.update({name1 + "1>": refFrame+".x"}) + self.symbol_table.update({name1 + "2>": refFrame+".y"}) + self.symbol_table.update({name1 + "3>": refFrame+".z"}) + + self.type2.update({name1: "bodies"}) + self.type2.update({name1+"o": "point"}) + + self.write(masscenter + " = " + "_me.Point('" + name1 + "_cm" + "')\n") + if self.newtonian: + self.write(masscenter + ".set_vel(" + self.newtonian + ", " + "0)\n") + self.write(refFrame + " = " + "_me.ReferenceFrame('" + name1 + "_f" + "')\n") + # We set a dummy mass and inertia here. + # They will be reset using the setters later in the code anyway. + self.write(name2 + " = " + "_me.RigidBody('" + name1 + "', " + masscenter + ", " + + refFrame + ", " + "_sm.symbols('m'), (_me.outer(" + refFrame + + ".x," + refFrame + ".x)," + masscenter + "))\n") + +def inertia_func(self, v1, v2, l, frame): + + if self.type2[v1] == "particle": + l.append("_me.inertia_of_point_mass(" + self.bodies[v1] + ".mass, " + self.bodies[v1] + + ".point.pos_from(" + self.symbol_table2[v2] + "), " + frame + ")") + + elif self.type2[v1] == "bodies": + # Inertia has been defined about center of mass. + if self.inertia_point[v1] == v1 + "o": + # Asking point is cm as well + if v2 == self.inertia_point[v1]: + l.append(self.symbol_table2[v1] + ".inertia[0]") + + # Asking point is not cm + else: + l.append(self.bodies[v1] + ".inertia[0]" + " + " + + "_me.inertia_of_point_mass(" + self.bodies[v1] + + ".mass, " + self.bodies[v1] + ".masscenter" + + ".pos_from(" + self.symbol_table2[v2] + + "), " + frame + ")") + + # Inertia has been defined about another point + else: + # Asking point is the defined point + if v2 == self.inertia_point[v1]: + l.append(self.symbol_table2[v1] + ".inertia[0]") + # Asking point is cm + elif v2 == v1 + "o": + l.append(self.bodies[v1] + ".inertia[0]" + " - " + + "_me.inertia_of_point_mass(" + self.bodies[v1] + + ".mass, " + self.bodies[v1] + ".masscenter" + + ".pos_from(" + self.symbol_table2[self.inertia_point[v1]] + + "), " + frame + ")") + # Asking point is some other point + else: + l.append(self.bodies[v1] + ".inertia[0]" + " - " + + "_me.inertia_of_point_mass(" + self.bodies[v1] + + ".mass, " + self.bodies[v1] + ".masscenter" + + ".pos_from(" + self.symbol_table2[self.inertia_point[v1]] + + "), " + frame + ")" + " + " + + "_me.inertia_of_point_mass(" + self.bodies[v1] + + ".mass, " + self.bodies[v1] + ".masscenter" + + ".pos_from(" + self.symbol_table2[v2] + + "), " + frame + ")") + + +def processConstants(self, ctx): + # Process constant declarations of the type: Constants F = 3, g = 9.81 + name = ctx.ID().getText().lower() + if "=" in ctx.getText(): + self.symbol_table.update({name: name}) + # self.inputs.update({self.symbol_table[name]: self.getValue(ctx.getChild(2))}) + self.write(self.symbol_table[name] + " = " + "_sm.S(" + self.getValue(ctx.getChild(2)) + ")\n") + self.type.update({name: "constants"}) + return + + # Constants declarations of the type: Constants A, B + else: + if "{" not in ctx.getText(): + self.symbol_table[name] = name + self.type[name] = "constants" + + # Process constant declarations of the type: Constants C+, D- + if ctx.getChildCount() == 2: + # This is set for declaring nonpositive=True and nonnegative=True + if ctx.getChild(1).getText() == "+": + self.sign[name] = "+" + elif ctx.getChild(1).getText() == "-": + self.sign[name] = "-" + else: + if "{" not in ctx.getText(): + self.sign[name] = "o" + + # Process constant declarations of the type: Constants K{4}, a{1:2, 1:2}, b{1:2} + if "{" in ctx.getText(): + if ":" in ctx.getText(): + num1 = int(ctx.INT(0).getText()) + num2 = int(ctx.INT(1).getText()) + 1 + else: + num1 = 1 + num2 = int(ctx.INT(0).getText()) + 1 + + if ":" in ctx.getText(): + if "," in ctx.getText(): + num3 = int(ctx.INT(2).getText()) + num4 = int(ctx.INT(3).getText()) + 1 + for i in range(num1, num2): + for j in range(num3, num4): + self.symbol_table[name + str(i) + str(j)] = name + str(i) + str(j) + self.type[name + str(i) + str(j)] = "constants" + self.var_list.append(name + str(i) + str(j)) + self.sign[name + str(i) + str(j)] = "o" + else: + for i in range(num1, num2): + self.symbol_table[name + str(i)] = name + str(i) + self.type[name + str(i)] = "constants" + self.var_list.append(name + str(i)) + self.sign[name + str(i)] = "o" + + elif "," in ctx.getText(): + for i in range(1, int(ctx.INT(0).getText()) + 1): + for j in range(1, int(ctx.INT(1).getText()) + 1): + self.symbol_table[name] = name + str(i) + str(j) + self.type[name + str(i) + str(j)] = "constants" + self.var_list.append(name + str(i) + str(j)) + self.sign[name + str(i) + str(j)] = "o" + + else: + for i in range(num1, num2): + self.symbol_table[name + str(i)] = name + str(i) + self.type[name + str(i)] = "constants" + self.var_list.append(name + str(i)) + self.sign[name + str(i)] = "o" + + if "{" not in ctx.getText(): + self.var_list.append(name) + + +def writeConstants(self, ctx): + l1 = list(filter(lambda x: self.sign[x] == "o", self.var_list)) + l2 = list(filter(lambda x: self.sign[x] == "+", self.var_list)) + l3 = list(filter(lambda x: self.sign[x] == "-", self.var_list)) + try: + if self.settings["complex"] == "on": + real = ", real=True" + elif self.settings["complex"] == "off": + real = "" + except Exception: + real = ", real=True" + + if l1: + a = ", ".join(l1) + " = " + "_sm.symbols(" + "'" +\ + " ".join(l1) + "'" + real + ")\n" + self.write(a) + if l2: + a = ", ".join(l2) + " = " + "_sm.symbols(" + "'" +\ + " ".join(l2) + "'" + real + ", nonnegative=True)\n" + self.write(a) + if l3: + a = ", ".join(l3) + " = " + "_sm.symbols(" + "'" + \ + " ".join(l3) + "'" + real + ", nonpositive=True)\n" + self.write(a) + self.var_list = [] + + +def processVariables(self, ctx): + # Specified F = x*N1> + y*N2> + name = ctx.ID().getText().lower() + if "=" in ctx.getText(): + text = name + "'"*(ctx.getChildCount()-3) + self.write(text + " = " + self.getValue(ctx.expr()) + "\n") + return + + # Process variables of the type: Variables qA, qB + if ctx.getChildCount() == 1: + self.symbol_table[name] = name + if self.getValue(ctx.parentCtx.getChild(0)) in ("variable", "specified", "motionvariable", "motionvariable'"): + self.type.update({name: self.getValue(ctx.parentCtx.getChild(0))}) + + self.var_list.append(name) + self.sign[name] = 0 + + # Process variables of the type: Variables x', y'' + elif "'" in ctx.getText() and "{" not in ctx.getText(): + if ctx.getText().count("'") > self.maxDegree: + self.maxDegree = ctx.getText().count("'") + for i in range(ctx.getChildCount()): + self.sign[name + strfunc(i)] = i + self.symbol_table[name + "'"*i] = name + strfunc(i) + if self.getValue(ctx.parentCtx.getChild(0)) in ("variable", "specified", "motionvariable", "motionvariable'"): + self.type.update({name + "'"*i: self.getValue(ctx.parentCtx.getChild(0))}) + self.var_list.append(name + strfunc(i)) + + elif "{" in ctx.getText(): + # Process variables of the type: Variables x{3}, y{2} + + if "'" in ctx.getText(): + dash_count = ctx.getText().count("'") + if dash_count > self.maxDegree: + self.maxDegree = dash_count + + if ":" in ctx.getText(): + # Variables C{1:2, 1:2} + if "," in ctx.getText(): + num1 = int(ctx.INT(0).getText()) + num2 = int(ctx.INT(1).getText()) + 1 + num3 = int(ctx.INT(2).getText()) + num4 = int(ctx.INT(3).getText()) + 1 + # Variables C{1:2} + else: + num1 = int(ctx.INT(0).getText()) + num2 = int(ctx.INT(1).getText()) + 1 + + # Variables C{1,3} + elif "," in ctx.getText(): + num1 = 1 + num2 = int(ctx.INT(0).getText()) + 1 + num3 = 1 + num4 = int(ctx.INT(1).getText()) + 1 + else: + num1 = 1 + num2 = int(ctx.INT(0).getText()) + 1 + + for i in range(num1, num2): + try: + for j in range(num3, num4): + try: + for z in range(dash_count+1): + self.symbol_table.update({name + str(i) + str(j) + "'"*z: name + str(i) + str(j) + strfunc(z)}) + if self.getValue(ctx.parentCtx.getChild(0)) in ("variable", "specified", "motionvariable", "motionvariable'"): + self.type.update({name + str(i) + str(j) + "'"*z: self.getValue(ctx.parentCtx.getChild(0))}) + self.var_list.append(name + str(i) + str(j) + strfunc(z)) + self.sign.update({name + str(i) + str(j) + strfunc(z): z}) + if dash_count > self.maxDegree: + self.maxDegree = dash_count + except Exception: + self.symbol_table.update({name + str(i) + str(j): name + str(i) + str(j)}) + if self.getValue(ctx.parentCtx.getChild(0)) in ("variable", "specified", "motionvariable", "motionvariable'"): + self.type.update({name + str(i) + str(j): self.getValue(ctx.parentCtx.getChild(0))}) + self.var_list.append(name + str(i) + str(j)) + self.sign.update({name + str(i) + str(j): 0}) + except Exception: + try: + for z in range(dash_count+1): + self.symbol_table.update({name + str(i) + "'"*z: name + str(i) + strfunc(z)}) + if self.getValue(ctx.parentCtx.getChild(0)) in ("variable", "specified", "motionvariable", "motionvariable'"): + self.type.update({name + str(i) + "'"*z: self.getValue(ctx.parentCtx.getChild(0))}) + self.var_list.append(name + str(i) + strfunc(z)) + self.sign.update({name + str(i) + strfunc(z): z}) + if dash_count > self.maxDegree: + self.maxDegree = dash_count + except Exception: + self.symbol_table.update({name + str(i): name + str(i)}) + if self.getValue(ctx.parentCtx.getChild(0)) in ("variable", "specified", "motionvariable", "motionvariable'"): + self.type.update({name + str(i): self.getValue(ctx.parentCtx.getChild(0))}) + self.var_list.append(name + str(i)) + self.sign.update({name + str(i): 0}) + +def writeVariables(self, ctx): + #print(self.sign) + #print(self.symbol_table) + if self.var_list: + for i in range(self.maxDegree+1): + if i == 0: + j = "" + t = "" + else: + j = str(i) + t = ", " + l = [] + for k in list(filter(lambda x: self.sign[x] == i, self.var_list)): + if i == 0: + l.append(k) + if i == 1: + l.append(k[:-1]) + if i > 1: + l.append(k[:-2]) + a = ", ".join(list(filter(lambda x: self.sign[x] == i, self.var_list))) + " = " +\ + "_me.dynamicsymbols(" + "'" + " ".join(l) + "'" + t + j + ")\n" + l = [] + self.write(a) + self.maxDegree = 0 + self.var_list = [] + +def processImaginary(self, ctx): + name = ctx.ID().getText().lower() + self.symbol_table[name] = name + self.type[name] = "imaginary" + self.var_list.append(name) + + +def writeImaginary(self, ctx): + a = ", ".join(self.var_list) + " = " + "_sm.symbols(" + "'" + \ + " ".join(self.var_list) + "')\n" + b = ", ".join(self.var_list) + " = " + "_sm.I\n" + self.write(a) + self.write(b) + self.var_list = [] + +if AutolevListener: + class MyListener(AutolevListener): # type: ignore + def __init__(self, include_numeric=False): + # Stores data in tree nodes(tree annotation). Especially useful for expr reconstruction. + self.tree_property = {} + + # Stores the declared variables, constants etc as they are declared in Autolev and SymPy + # {"": ""}. + self.symbol_table = collections.OrderedDict() + + # Similar to symbol_table. Used for storing Physical entities like Frames, Points, + # Particles, Bodies etc + self.symbol_table2 = collections.OrderedDict() + + # Used to store nonpositive, nonnegative etc for constants and number of "'"s (order of diff) + # in variables. + self.sign = {} + + # Simple list used as a store to pass around variables between the 'process' and 'write' + # methods. + self.var_list = [] + + # Stores the type of a declared variable (constants, variables, specifieds etc) + self.type = collections.OrderedDict() + + # Similar to self.type. Used for storing the type of Physical entities like Frames, Points, + # Particles, Bodies etc + self.type2 = collections.OrderedDict() + + # These lists are used to distinguish matrix, numeric and vector expressions. + self.matrix_expr = [] + self.numeric_expr = [] + self.vector_expr = [] + self.fr_expr = [] + + self.output_code = [] + + # Stores the variables and their rhs for substituting upon the Autolev command EXPLICIT. + self.explicit = collections.OrderedDict() + + # Write code to import common dependencies. + self.output_code.append("import sympy.physics.mechanics as _me\n") + self.output_code.append("import sympy as _sm\n") + self.output_code.append("import math as m\n") + self.output_code.append("import numpy as _np\n") + self.output_code.append("\n") + + # Just a store for the max degree variable in a line. + self.maxDegree = 0 + + # Stores the input parameters which are then used for codegen and numerical analysis. + self.inputs = collections.OrderedDict() + # Stores the variables which appear in Output Autolev commands. + self.outputs = [] + # Stores the settings specified by the user. Ex: Complex on/off, Degrees on/off + self.settings = {} + # Boolean which changes the behaviour of some expression reconstruction + # when parsing Input Autolev commands. + self.in_inputs = False + self.in_outputs = False + + # Stores for the physical entities. + self.newtonian = None + self.bodies = collections.OrderedDict() + self.constants = [] + self.forces = collections.OrderedDict() + self.q_ind = [] + self.q_dep = [] + self.u_ind = [] + self.u_dep = [] + self.kd_eqs = [] + self.dependent_variables = [] + self.kd_equivalents = collections.OrderedDict() + self.kd_equivalents2 = collections.OrderedDict() + self.kd_eqs_supplied = None + self.kane_type = "no_args" + self.inertia_point = collections.OrderedDict() + self.kane_parsed = False + self.t = False + + # PyDy ode code will be included only if this flag is set to True. + self.include_numeric = include_numeric + + def write(self, string): + self.output_code.append(string) + + def getValue(self, node): + return self.tree_property[node] + + def setValue(self, node, value): + self.tree_property[node] = value + + def getSymbolTable(self): + return self.symbol_table + + def getType(self): + return self.type + + def exitVarDecl(self, ctx): + # This event method handles variable declarations. The parse tree node varDecl contains + # one or more varDecl2 nodes. Eg varDecl for 'Constants a{1:2, 1:2}, b{1:2}' has two varDecl2 + # nodes(one for a{1:2, 1:2} and one for b{1:2}). + + # Variable declarations are processed and stored in the event method exitVarDecl2. + # This stored information is used to write the final SymPy output code in the exitVarDecl event method. + + # determine the type of declaration + if self.getValue(ctx.varType()) == "constant": + writeConstants(self, ctx) + elif self.getValue(ctx.varType()) in\ + ("variable", "motionvariable", "motionvariable'", "specified"): + writeVariables(self, ctx) + elif self.getValue(ctx.varType()) == "imaginary": + writeImaginary(self, ctx) + + def exitVarType(self, ctx): + # Annotate the varType tree node with the type of the variable declaration. + name = ctx.getChild(0).getText().lower() + if name[-1] == "s" and name != "bodies": + self.setValue(ctx, name[:-1]) + else: + self.setValue(ctx, name) + + def exitVarDecl2(self, ctx): + # Variable declarations are processed and stored in the event method exitVarDecl2. + # This stored information is used to write the final SymPy output code in the exitVarDecl event method. + # This is the case for constants, variables, specifieds etc. + + # This isn't the case for all types of declarations though. For instance + # Frames A, B, C, N cannot be defined on one line in SymPy. So we do not append A, B, C, N + # to a var_list or use exitVarDecl. exitVarDecl2 directly writes out to the file. + + # determine the type of declaration + if self.getValue(ctx.parentCtx.varType()) == "constant": + processConstants(self, ctx) + + elif self.getValue(ctx.parentCtx.varType()) in \ + ("variable", "motionvariable", "motionvariable'", "specified"): + processVariables(self, ctx) + + elif self.getValue(ctx.parentCtx.varType()) == "imaginary": + processImaginary(self, ctx) + + elif self.getValue(ctx.parentCtx.varType()) in ("frame", "newtonian", "point", "particle", "bodies"): + if "{" in ctx.getText(): + if ":" in ctx.getText() and "," not in ctx.getText(): + num1 = int(ctx.INT(0).getText()) + num2 = int(ctx.INT(1).getText()) + 1 + elif ":" not in ctx.getText() and "," in ctx.getText(): + num1 = 1 + num2 = int(ctx.INT(0).getText()) + 1 + num3 = 1 + num4 = int(ctx.INT(1).getText()) + 1 + elif ":" in ctx.getText() and "," in ctx.getText(): + num1 = int(ctx.INT(0).getText()) + num2 = int(ctx.INT(1).getText()) + 1 + num3 = int(ctx.INT(2).getText()) + num4 = int(ctx.INT(3).getText()) + 1 + else: + num1 = 1 + num2 = int(ctx.INT(0).getText()) + 1 + else: + num1 = 1 + num2 = 2 + for i in range(num1, num2): + try: + for j in range(num3, num4): + declare_phy_entities(self, ctx, self.getValue(ctx.parentCtx.varType()), i, j) + except Exception: + declare_phy_entities(self, ctx, self.getValue(ctx.parentCtx.varType()), i) + # ================== Subrules of parser rule expr (Start) ====================== # + + def exitId(self, ctx): + # Tree annotation for ID which is a labeled subrule of the parser rule expr. + # A_C + python_keywords = ["and", "as", "assert", "break", "class", "continue", "def", "del", "elif", "else", "except",\ + "exec", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "not", "or", "pass", "print",\ + "raise", "return", "try", "while", "with", "yield"] + + if ctx.ID().getText().lower() in python_keywords: + warnings.warn("Python keywords must not be used as identifiers. Please refer to the list of keywords at https://docs.python.org/2.5/ref/keywords.html", + SyntaxWarning) + + if "_" in ctx.ID().getText() and ctx.ID().getText().count('_') == 1: + e1, e2 = ctx.ID().getText().lower().split('_') + try: + if self.type2[e1] == "frame": + e1 = self.symbol_table2[e1] + elif self.type2[e1] == "bodies": + e1 = self.symbol_table2[e1] + "_f" + if self.type2[e2] == "frame": + e2 = self.symbol_table2[e2] + elif self.type2[e2] == "bodies": + e2 = self.symbol_table2[e2] + "_f" + + self.setValue(ctx, e1 + ".dcm(" + e2 + ")") + except Exception: + self.setValue(ctx, ctx.ID().getText().lower()) + else: + # Reserved constant Pi + if ctx.ID().getText().lower() == "pi": + self.setValue(ctx, "_sm.pi") + self.numeric_expr.append(ctx) + + # Reserved variable T (for time) + elif ctx.ID().getText().lower() == "t": + self.setValue(ctx, "_me.dynamicsymbols._t") + if not self.in_inputs and not self.in_outputs: + self.t = True + + else: + idText = ctx.ID().getText().lower() + "'"*(ctx.getChildCount() - 1) + if idText in self.type.keys() and self.type[idText] == "matrix": + self.matrix_expr.append(ctx) + if self.in_inputs: + try: + self.setValue(ctx, self.symbol_table[idText]) + except Exception: + self.setValue(ctx, idText.lower()) + else: + try: + self.setValue(ctx, self.symbol_table[idText]) + except Exception: + pass + + def exitInt(self, ctx): + # Tree annotation for int which is a labeled subrule of the parser rule expr. + int_text = ctx.INT().getText() + self.setValue(ctx, int_text) + self.numeric_expr.append(ctx) + + def exitFloat(self, ctx): + # Tree annotation for float which is a labeled subrule of the parser rule expr. + floatText = ctx.FLOAT().getText() + self.setValue(ctx, floatText) + self.numeric_expr.append(ctx) + + def exitAddSub(self, ctx): + # Tree annotation for AddSub which is a labeled subrule of the parser rule expr. + # The subrule is expr = expr (+|-) expr + if ctx.expr(0) in self.matrix_expr or ctx.expr(1) in self.matrix_expr: + self.matrix_expr.append(ctx) + if ctx.expr(0) in self.vector_expr or ctx.expr(1) in self.vector_expr: + self.vector_expr.append(ctx) + if ctx.expr(0) in self.numeric_expr and ctx.expr(1) in self.numeric_expr: + self.numeric_expr.append(ctx) + self.setValue(ctx, self.getValue(ctx.expr(0)) + ctx.getChild(1).getText() + + self.getValue(ctx.expr(1))) + + def exitMulDiv(self, ctx): + # Tree annotation for MulDiv which is a labeled subrule of the parser rule expr. + # The subrule is expr = expr (*|/) expr + try: + if ctx.expr(0) in self.vector_expr and ctx.expr(1) in self.vector_expr: + self.setValue(ctx, "_me.outer(" + self.getValue(ctx.expr(0)) + ", " + + self.getValue(ctx.expr(1)) + ")") + else: + if ctx.expr(0) in self.matrix_expr or ctx.expr(1) in self.matrix_expr: + self.matrix_expr.append(ctx) + if ctx.expr(0) in self.vector_expr or ctx.expr(1) in self.vector_expr: + self.vector_expr.append(ctx) + if ctx.expr(0) in self.numeric_expr and ctx.expr(1) in self.numeric_expr: + self.numeric_expr.append(ctx) + self.setValue(ctx, self.getValue(ctx.expr(0)) + ctx.getChild(1).getText() + + self.getValue(ctx.expr(1))) + except Exception: + pass + + def exitNegativeOne(self, ctx): + # Tree annotation for negativeOne which is a labeled subrule of the parser rule expr. + self.setValue(ctx, "-1*" + self.getValue(ctx.getChild(1))) + if ctx.getChild(1) in self.matrix_expr: + self.matrix_expr.append(ctx) + if ctx.getChild(1) in self.numeric_expr: + self.numeric_expr.append(ctx) + + def exitParens(self, ctx): + # Tree annotation for parens which is a labeled subrule of the parser rule expr. + # The subrule is expr = '(' expr ')' + if ctx.expr() in self.matrix_expr: + self.matrix_expr.append(ctx) + if ctx.expr() in self.vector_expr: + self.vector_expr.append(ctx) + if ctx.expr() in self.numeric_expr: + self.numeric_expr.append(ctx) + self.setValue(ctx, "(" + self.getValue(ctx.expr()) + ")") + + def exitExponent(self, ctx): + # Tree annotation for Exponent which is a labeled subrule of the parser rule expr. + # The subrule is expr = expr ^ expr + if ctx.expr(0) in self.matrix_expr or ctx.expr(1) in self.matrix_expr: + self.matrix_expr.append(ctx) + if ctx.expr(0) in self.vector_expr or ctx.expr(1) in self.vector_expr: + self.vector_expr.append(ctx) + if ctx.expr(0) in self.numeric_expr and ctx.expr(1) in self.numeric_expr: + self.numeric_expr.append(ctx) + self.setValue(ctx, self.getValue(ctx.expr(0)) + "**" + self.getValue(ctx.expr(1))) + + def exitExp(self, ctx): + s = ctx.EXP().getText()[ctx.EXP().getText().index('E')+1:] + if "-" in s: + s = s[0] + s[1:].lstrip("0") + else: + s = s.lstrip("0") + self.setValue(ctx, ctx.EXP().getText()[:ctx.EXP().getText().index('E')] + + "*10**(" + s + ")") + + def exitFunction(self, ctx): + # Tree annotation for function which is a labeled subrule of the parser rule expr. + + # The difference between this and FunctionCall is that this is used for non standalone functions + # appearing in expressions and assignments. + # Eg: + # When we come across a standalone function say Expand(E, n:m) then it is categorized as FunctionCall + # which is a parser rule in itself under rule stat. exitFunctionCall() takes care of it and writes to the file. + # + # On the other hand, while we come across E_diff = D(E, y), we annotate the tree node + # of the function D(E, y) with the SymPy equivalent in exitFunction(). + # In this case it is the method exitAssignment() that writes the code to the file and not exitFunction(). + + ch = ctx.getChild(0) + func_name = ch.getChild(0).getText().lower() + + # Expand(y, n:m) * + if func_name == "expand": + expr = self.getValue(ch.expr(0)) + if ch.expr(0) in self.matrix_expr or (expr in self.type.keys() and self.type[expr] == "matrix"): + self.matrix_expr.append(ctx) + # _sm.Matrix([i.expand() for i in z]).reshape(z.shape[0], z.shape[1]) + self.setValue(ctx, "_sm.Matrix([i.expand() for i in " + expr + "])" + + ".reshape((" + expr + ").shape[0], " + "(" + expr + ").shape[1])") + else: + self.setValue(ctx, "(" + expr + ")" + "." + "expand()") + + # Factor(y, x) * + elif func_name == "factor": + expr = self.getValue(ch.expr(0)) + if ch.expr(0) in self.matrix_expr or (expr in self.type.keys() and self.type[expr] == "matrix"): + self.matrix_expr.append(ctx) + self.setValue(ctx, "_sm.Matrix([_sm.factor(i, " + self.getValue(ch.expr(1)) + ") for i in " + + expr + "])" + ".reshape((" + expr + ").shape[0], " + "(" + expr + ").shape[1])") + else: + self.setValue(ctx, "_sm.factor(" + "(" + expr + ")" + + ", " + self.getValue(ch.expr(1)) + ")") + + # D(y, x) + elif func_name == "d": + expr = self.getValue(ch.expr(0)) + if ch.expr(0) in self.matrix_expr or (expr in self.type.keys() and self.type[expr] == "matrix"): + self.matrix_expr.append(ctx) + self.setValue(ctx, "_sm.Matrix([i.diff(" + self.getValue(ch.expr(1)) + ") for i in " + + expr + "])" + ".reshape((" + expr + ").shape[0], " + "(" + expr + ").shape[1])") + else: + if ch.getChildCount() == 8: + frame = self.symbol_table2[ch.expr(2).getText().lower()] + self.setValue(ctx, "(" + expr + ")" + "." + "diff(" + self.getValue(ch.expr(1)) + + ", " + frame + ")") + else: + self.setValue(ctx, "(" + expr + ")" + "." + "diff(" + + self.getValue(ch.expr(1)) + ")") + + # Dt(y) + elif func_name == "dt": + expr = self.getValue(ch.expr(0)) + if ch.expr(0) in self.vector_expr: + text = "dt(" + else: + text = "diff(_sm.Symbol('t')" + if ch.expr(0) in self.matrix_expr or (expr in self.type.keys() and self.type[expr] == "matrix"): + self.matrix_expr.append(ctx) + self.setValue(ctx, "_sm.Matrix([i." + text + + ") for i in " + expr + "])" + + ".reshape((" + expr + ").shape[0], " + "(" + expr + ").shape[1])") + else: + if ch.getChildCount() == 6: + frame = self.symbol_table2[ch.expr(1).getText().lower()] + self.setValue(ctx, "(" + expr + ")" + "." + "dt(" + + frame + ")") + else: + self.setValue(ctx, "(" + expr + ")" + "." + text + ")") + + # Explicit(EXPRESS(IMPLICIT>,C)) + elif func_name == "explicit": + if ch.expr(0) in self.vector_expr: + self.vector_expr.append(ctx) + expr = self.getValue(ch.expr(0)) + if self.explicit.keys(): + explicit_list = [] + for i in self.explicit.keys(): + explicit_list.append(i + ":" + self.explicit[i]) + self.setValue(ctx, "(" + expr + ")" + ".subs({" + ", ".join(explicit_list) + "})") + else: + self.setValue(ctx, expr) + + # Taylor(y, 0:2, w=a, x=0) + # TODO: Currently only works with symbols. Make it work for dynamicsymbols. + elif func_name == "taylor": + exp = self.getValue(ch.expr(0)) + order = self.getValue(ch.expr(1).expr(1)) + x = (ch.getChildCount()-6)//2 + l = [] + for i in range(x): + index = 2 + i + child = ch.expr(index) + l.append(".series(" + self.getValue(child.getChild(0)) + + ", " + self.getValue(child.getChild(2)) + + ", " + order + ").removeO()") + self.setValue(ctx, "(" + exp + ")" + "".join(l)) + + # Evaluate(y, a=x, b=2) + elif func_name == "evaluate": + expr = self.getValue(ch.expr(0)) + l = [] + x = (ch.getChildCount()-4)//2 + for i in range(x): + index = 1 + i + child = ch.expr(index) + l.append(self.getValue(child.getChild(0)) + ":" + + self.getValue(child.getChild(2))) + + if ch.expr(0) in self.matrix_expr or (expr in self.type.keys() and self.type[expr] == "matrix"): + self.matrix_expr.append(ctx) + self.setValue(ctx, "_sm.Matrix([i.subs({" + ",".join(l) + "}) for i in " + + expr + "])" + + ".reshape((" + expr + ").shape[0], " + "(" + expr + ").shape[1])") + else: + if self.explicit: + explicit_list = [] + for i in self.explicit.keys(): + explicit_list.append(i + ":" + self.explicit[i]) + self.setValue(ctx, "(" + expr + ")" + ".subs({" + ",".join(explicit_list) + + "}).subs({" + ",".join(l) + "})") + else: + self.setValue(ctx, "(" + expr + ")" + ".subs({" + ",".join(l) + "})") + + # Polynomial([a, b, c], x) + elif func_name == "polynomial": + self.setValue(ctx, "_sm.Poly(" + self.getValue(ch.expr(0)) + ", " + + self.getValue(ch.expr(1)) + ")") + + # Roots(Poly, x, 2) + # Roots([1; 2; 3; 4]) + elif func_name == "roots": + self.matrix_expr.append(ctx) + expr = self.getValue(ch.expr(0)) + if ch.expr(0) in self.matrix_expr or (expr in self.type.keys() and self.type[expr] == "matrix"): + self.setValue(ctx, "[i.evalf() for i in " + "_sm.solve(" + + "_sm.Poly(" + expr + ", " + "x),x)]") + else: + self.setValue(ctx, "[i.evalf() for i in " + "_sm.solve(" + + expr + ", " + self.getValue(ch.expr(1)) + ")]") + + # Transpose(A), Inv(A) + elif func_name in ("transpose", "inv", "inverse"): + self.matrix_expr.append(ctx) + if func_name == "transpose": + e = ".T" + elif func_name in ("inv", "inverse"): + e = "**(-1)" + self.setValue(ctx, "(" + self.getValue(ch.expr(0)) + ")" + e) + + # Eig(A) + elif func_name == "eig": + # "_sm.Matrix([i.evalf() for i in " + + self.setValue(ctx, "_sm.Matrix([i.evalf() for i in (" + + self.getValue(ch.expr(0)) + ").eigenvals().keys()])") + + # Diagmat(n, m, x) + # Diagmat(3, 1) + elif func_name == "diagmat": + self.matrix_expr.append(ctx) + if ch.getChildCount() == 6: + l = [] + for i in range(int(self.getValue(ch.expr(0)))): + l.append(self.getValue(ch.expr(1)) + ",") + + self.setValue(ctx, "_sm.diag(" + ("".join(l))[:-1] + ")") + + elif ch.getChildCount() == 8: + # _sm.Matrix([x if i==j else 0 for i in range(n) for j in range(m)]).reshape(n, m) + n = self.getValue(ch.expr(0)) + m = self.getValue(ch.expr(1)) + x = self.getValue(ch.expr(2)) + self.setValue(ctx, "_sm.Matrix([" + x + " if i==j else 0 for i in range(" + + n + ") for j in range(" + m + ")]).reshape(" + n + ", " + m + ")") + + # Cols(A) + # Cols(A, 1) + # Cols(A, 1, 2:4, 3) + elif func_name in ("cols", "rows"): + self.matrix_expr.append(ctx) + if func_name == "cols": + e1 = ".cols" + e2 = ".T." + else: + e1 = ".rows" + e2 = "." + if ch.getChildCount() == 4: + self.setValue(ctx, "(" + self.getValue(ch.expr(0)) + ")" + e1) + elif ch.getChildCount() == 6: + self.setValue(ctx, "(" + self.getValue(ch.expr(0)) + ")" + + e1[:-1] + "(" + str(int(self.getValue(ch.expr(1))) - 1) + ")") + else: + l = [] + for i in range(4, ch.getChildCount()): + try: + if ch.getChild(i).getChildCount() > 1 and ch.getChild(i).getChild(1).getText() == ":": + for j in range(int(ch.getChild(i).getChild(0).getText()), + int(ch.getChild(i).getChild(2).getText())+1): + l.append("(" + self.getValue(ch.getChild(2)) + ")" + e2 + + "row(" + str(j-1) + ")") + else: + l.append("(" + self.getValue(ch.getChild(2)) + ")" + e2 + + "row(" + str(int(ch.getChild(i).getText())-1) + ")") + except Exception: + pass + self.setValue(ctx, "_sm.Matrix([" + ",".join(l) + "])") + + # Det(A) Trace(A) + elif func_name in ["det", "trace"]: + self.setValue(ctx, "(" + self.getValue(ch.expr(0)) + ")" + "." + + func_name + "()") + + # Element(A, 2, 3) + elif func_name == "element": + self.setValue(ctx, "(" + self.getValue(ch.expr(0)) + ")" + "[" + + str(int(self.getValue(ch.expr(1)))-1) + "," + + str(int(self.getValue(ch.expr(2)))-1) + "]") + + elif func_name in \ + ["cos", "sin", "tan", "cosh", "sinh", "tanh", "acos", "asin", "atan", + "log", "exp", "sqrt", "factorial", "floor", "sign"]: + self.setValue(ctx, "_sm." + func_name + "(" + self.getValue(ch.expr(0)) + ")") + + elif func_name == "ceil": + self.setValue(ctx, "_sm.ceiling" + "(" + self.getValue(ch.expr(0)) + ")") + + elif func_name == "sqr": + self.setValue(ctx, "(" + self.getValue(ch.expr(0)) + + ")" + "**2") + + elif func_name == "log10": + self.setValue(ctx, "_sm.log" + + "(" + self.getValue(ch.expr(0)) + ", 10)") + + elif func_name == "atan2": + self.setValue(ctx, "_sm.atan2" + "(" + self.getValue(ch.expr(0)) + ", " + + self.getValue(ch.expr(1)) + ")") + + elif func_name in ["int", "round"]: + self.setValue(ctx, func_name + + "(" + self.getValue(ch.expr(0)) + ")") + + elif func_name == "abs": + self.setValue(ctx, "_sm.Abs(" + self.getValue(ch.expr(0)) + ")") + + elif func_name in ["max", "min"]: + # max(x, y, z) + l = [] + for i in range(1, ch.getChildCount()): + if ch.getChild(i) in self.tree_property.keys(): + l.append(self.getValue(ch.getChild(i))) + elif ch.getChild(i).getText() in [",", "(", ")"]: + l.append(ch.getChild(i).getText()) + self.setValue(ctx, "_sm." + ch.getChild(0).getText().capitalize() + "".join(l)) + + # Coef(y, x) + elif func_name == "coef": + #A41_A53=COEF([RHS(U4);RHS(U5)],[U1,U2,U3]) + if ch.expr(0) in self.matrix_expr and ch.expr(1) in self.matrix_expr: + icount = jcount = 0 + for i in range(ch.expr(0).getChild(0).getChildCount()): + try: + ch.expr(0).getChild(0).getChild(i).getRuleIndex() + icount+=1 + except Exception: + pass + for j in range(ch.expr(1).getChild(0).getChildCount()): + try: + ch.expr(1).getChild(0).getChild(j).getRuleIndex() + jcount+=1 + except Exception: + pass + l = [] + for i in range(icount): + for j in range(jcount): + # a41_a53[i,j] = u4.expand().coeff(u1) + l.append(self.getValue(ch.expr(0).getChild(0).expr(i)) + ".expand().coeff(" + + self.getValue(ch.expr(1).getChild(0).expr(j)) + ")") + self.setValue(ctx, "_sm.Matrix([" + ", ".join(l) + "]).reshape(" + str(icount) + ", " + str(jcount) + ")") + else: + self.setValue(ctx, "(" + self.getValue(ch.expr(0)) + + ")" + ".expand().coeff(" + self.getValue(ch.expr(1)) + ")") + + # Exclude(y, x) Include(y, x) + elif func_name in ("exclude", "include"): + if func_name == "exclude": + e = "0" + else: + e = "1" + expr = self.getValue(ch.expr(0)) + if ch.expr(0) in self.matrix_expr or (expr in self.type.keys() and self.type[expr] == "matrix"): + self.matrix_expr.append(ctx) + self.setValue(ctx, "_sm.Matrix([i.collect(" + self.getValue(ch.expr(1)) + "])" + + ".coeff(" + self.getValue(ch.expr(1)) + "," + e + ")" + "for i in " + expr + ")" + + ".reshape((" + expr + ").shape[0], " + "(" + expr + ").shape[1])") + else: + self.setValue(ctx, "(" + expr + + ")" + ".collect(" + self.getValue(ch.expr(1)) + ")" + + ".coeff(" + self.getValue(ch.expr(1)) + "," + e + ")") + + # RHS(y) + elif func_name == "rhs": + self.setValue(ctx, self.explicit[self.getValue(ch.expr(0))]) + + # Arrange(y, n, x) * + elif func_name == "arrange": + expr = self.getValue(ch.expr(0)) + if ch.expr(0) in self.matrix_expr or (expr in self.type.keys() and self.type[expr] == "matrix"): + self.matrix_expr.append(ctx) + self.setValue(ctx, "_sm.Matrix([i.collect(" + self.getValue(ch.expr(2)) + + ")" + "for i in " + expr + "])"+ + ".reshape((" + expr + ").shape[0], " + "(" + expr + ").shape[1])") + else: + self.setValue(ctx, "(" + expr + + ")" + ".collect(" + self.getValue(ch.expr(2)) + ")") + + # Replace(y, sin(x)=3) + elif func_name == "replace": + l = [] + for i in range(1, ch.getChildCount()): + try: + if ch.getChild(i).getChild(1).getText() == "=": + l.append(self.getValue(ch.getChild(i).getChild(0)) + + ":" + self.getValue(ch.getChild(i).getChild(2))) + except Exception: + pass + expr = self.getValue(ch.expr(0)) + if ch.expr(0) in self.matrix_expr or (expr in self.type.keys() and self.type[expr] == "matrix"): + self.matrix_expr.append(ctx) + self.setValue(ctx, "_sm.Matrix([i.subs({" + ",".join(l) + "}) for i in " + + expr + "])" + + ".reshape((" + expr + ").shape[0], " + "(" + expr + ").shape[1])") + else: + self.setValue(ctx, "(" + self.getValue(ch.expr(0)) + ")" + + ".subs({" + ",".join(l) + "})") + + # Dot(Loop>, N1>) + elif func_name == "dot": + l = [] + num = (ch.expr(1).getChild(0).getChildCount()-1)//2 + if ch.expr(1) in self.matrix_expr: + for i in range(num): + l.append("_me.dot(" + self.getValue(ch.expr(0)) + ", " + self.getValue(ch.expr(1).getChild(0).expr(i)) + ")") + self.setValue(ctx, "_sm.Matrix([" + ",".join(l) + "]).reshape(" + str(num) + ", " + "1)") + else: + self.setValue(ctx, "_me.dot(" + self.getValue(ch.expr(0)) + ", " + self.getValue(ch.expr(1)) + ")") + # Cross(w_A_N>, P_NA_AB>) + elif func_name == "cross": + self.vector_expr.append(ctx) + self.setValue(ctx, "_me.cross(" + self.getValue(ch.expr(0)) + ", " + self.getValue(ch.expr(1)) + ")") + + # Mag(P_O_Q>) + elif func_name == "mag": + self.setValue(ctx, self.getValue(ch.expr(0)) + "." + "magnitude()") + + # MATRIX(A, I_R>>) + elif func_name == "matrix": + if self.type2[ch.expr(0).getText().lower()] == "frame": + text = "" + elif self.type2[ch.expr(0).getText().lower()] == "bodies": + text = "_f" + self.setValue(ctx, "(" + self.getValue(ch.expr(1)) + ")" + ".to_matrix(" + + self.symbol_table2[ch.expr(0).getText().lower()] + text + ")") + + # VECTOR(A, ROWS(EIGVECS,1)) + elif func_name == "vector": + if self.type2[ch.expr(0).getText().lower()] == "frame": + text = "" + elif self.type2[ch.expr(0).getText().lower()] == "bodies": + text = "_f" + v = self.getValue(ch.expr(1)) + f = self.symbol_table2[ch.expr(0).getText().lower()] + text + self.setValue(ctx, v + "[0]*" + f + ".x +" + v + "[1]*" + f + ".y +" + + v + "[2]*" + f + ".z") + + # Express(A2>, B) + # Here I am dealing with all the Inertia commands as I expect the users to use Inertia + # commands only with Express because SymPy needs the Reference frame to be specified unlike Autolev. + elif func_name == "express": + self.vector_expr.append(ctx) + if self.type2[ch.expr(1).getText().lower()] == "frame": + frame = self.symbol_table2[ch.expr(1).getText().lower()] + else: + frame = self.symbol_table2[ch.expr(1).getText().lower()] + "_f" + if ch.expr(0).getText().lower() == "1>>": + self.setValue(ctx, "_me.inertia(" + frame + ", 1, 1, 1)") + + elif '_' in ch.expr(0).getText().lower() and ch.expr(0).getText().lower().count('_') == 2\ + and ch.expr(0).getText().lower()[0] == "i" and ch.expr(0).getText().lower()[-2:] == ">>": + v1 = ch.expr(0).getText().lower()[:-2].split('_')[1] + v2 = ch.expr(0).getText().lower()[:-2].split('_')[2] + l = [] + inertia_func(self, v1, v2, l, frame) + self.setValue(ctx, " + ".join(l)) + + elif ch.expr(0).getChild(0).getChild(0).getText().lower() == "inertia": + if ch.expr(0).getChild(0).getChildCount() == 4: + l = [] + v2 = ch.expr(0).getChild(0).ID(0).getText().lower() + for v1 in self.bodies: + inertia_func(self, v1, v2, l, frame) + self.setValue(ctx, " + ".join(l)) + + else: + l = [] + l2 = [] + v2 = ch.expr(0).getChild(0).ID(0).getText().lower() + for i in range(1, (ch.expr(0).getChild(0).getChildCount()-2)//2): + l2.append(ch.expr(0).getChild(0).ID(i).getText().lower()) + for v1 in l2: + inertia_func(self, v1, v2, l, frame) + self.setValue(ctx, " + ".join(l)) + + else: + self.setValue(ctx, "(" + self.getValue(ch.expr(0)) + ")" + ".express(" + + self.symbol_table2[ch.expr(1).getText().lower()] + ")") + # CM(P) + elif func_name == "cm": + if self.type2[ch.expr(0).getText().lower()] == "point": + text = "" + else: + text = ".point" + if ch.getChildCount() == 4: + self.setValue(ctx, "_me.functions.center_of_mass(" + self.symbol_table2[ch.expr(0).getText().lower()] + + text + "," + ", ".join(self.bodies.values()) + ")") + else: + bodies = [] + for i in range(1, (ch.getChildCount()-1)//2): + bodies.append(self.symbol_table2[ch.expr(i).getText().lower()]) + self.setValue(ctx, "_me.functions.center_of_mass(" + self.symbol_table2[ch.expr(0).getText().lower()] + + text + "," + ", ".join(bodies) + ")") + + # PARTIALS(V_P1_E>,U1) + elif func_name == "partials": + speeds = [] + for i in range(1, (ch.getChildCount()-1)//2): + if self.kd_equivalents2: + speeds.append(self.kd_equivalents2[self.symbol_table[ch.expr(i).getText().lower()]]) + else: + speeds.append(self.symbol_table[ch.expr(i).getText().lower()]) + v1, v2, v3 = ch.expr(0).getText().lower().replace(">","").split('_') + if self.type2[v2] == "point": + point = self.symbol_table2[v2] + elif self.type2[v2] == "particle": + point = self.symbol_table2[v2] + ".point" + frame = self.symbol_table2[v3] + self.setValue(ctx, point + ".partial_velocity(" + frame + ", " + ",".join(speeds) + ")") + + # UnitVec(A1>+A2>+A3>) + elif func_name == "unitvec": + self.setValue(ctx, "(" + self.getValue(ch.expr(0)) + ")" + ".normalize()") + + # Units(deg, rad) + elif func_name == "units": + if ch.expr(0).getText().lower() == "deg" and ch.expr(1).getText().lower() == "rad": + factor = 0.0174533 + elif ch.expr(0).getText().lower() == "rad" and ch.expr(1).getText().lower() == "deg": + factor = 57.2958 + self.setValue(ctx, str(factor)) + # Mass(A) + elif func_name == "mass": + l = [] + try: + ch.ID(0).getText().lower() + for i in range((ch.getChildCount()-1)//2): + l.append(self.symbol_table2[ch.ID(i).getText().lower()] + ".mass") + self.setValue(ctx, "+".join(l)) + except Exception: + for i in self.bodies.keys(): + l.append(self.bodies[i] + ".mass") + self.setValue(ctx, "+".join(l)) + + # Fr() FrStar() + # _me.KanesMethod(n, q_ind, u_ind, kd, velocity_constraints).kanes_equations(pl, fl)[0] + elif func_name in ["fr", "frstar"]: + if not self.kane_parsed: + if self.kd_eqs: + for i in self.kd_eqs: + self.q_ind.append(self.symbol_table[i.strip().split('-')[0].replace("'","")]) + self.u_ind.append(self.symbol_table[i.strip().split('-')[1].replace("'","")]) + + for i in range(len(self.kd_eqs)): + self.kd_eqs[i] = self.symbol_table[self.kd_eqs[i].strip().split('-')[0]] + " - " +\ + self.symbol_table[self.kd_eqs[i].strip().split('-')[1]] + + # Do all of this if kd_eqs are not specified + if not self.kd_eqs: + self.kd_eqs_supplied = False + self.matrix_expr.append(ctx) + for i in self.type.keys(): + if self.type[i] == "motionvariable": + if self.sign[self.symbol_table[i.lower()]] == 0: + self.q_ind.append(self.symbol_table[i.lower()]) + elif self.sign[self.symbol_table[i.lower()]] == 1: + name = "u_" + self.symbol_table[i.lower()] + self.symbol_table.update({name: name}) + self.write(name + " = " + "_me.dynamicsymbols('" + name + "')\n") + if self.symbol_table[i.lower()] not in self.dependent_variables: + self.u_ind.append(name) + self.kd_equivalents.update({name: self.symbol_table[i.lower()]}) + else: + self.u_dep.append(name) + self.kd_equivalents.update({name: self.symbol_table[i.lower()]}) + + for i in self.kd_equivalents.keys(): + self.kd_eqs.append(self.kd_equivalents[i] + "-" + i) + + if not self.u_ind and not self.kd_eqs: + self.u_ind = self.q_ind.copy() + self.q_ind = [] + + # deal with velocity constraints + if self.dependent_variables: + for i in self.dependent_variables: + self.u_dep.append(i) + if i in self.u_ind: + self.u_ind.remove(i) + + + self.u_dep[:] = [i for i in self.u_dep if i not in self.kd_equivalents.values()] + + force_list = [] + for i in self.forces.keys(): + force_list.append("(" + i + "," + self.forces[i] + ")") + if self.u_dep: + u_dep_text = ", u_dependent=[" + ", ".join(self.u_dep) + "]" + else: + u_dep_text = "" + if self.dependent_variables: + velocity_constraints_text = ", velocity_constraints = velocity_constraints" + else: + velocity_constraints_text = "" + if ctx.parentCtx not in self.fr_expr: + self.write("kd_eqs = [" + ", ".join(self.kd_eqs) + "]\n") + self.write("forceList = " + "[" + ", ".join(force_list) + "]\n") + self.write("kane = _me.KanesMethod(" + self.newtonian + ", " + "q_ind=[" + + ",".join(self.q_ind) + "], " + "u_ind=[" + + ", ".join(self.u_ind) + "]" + u_dep_text + ", " + + "kd_eqs = kd_eqs" + velocity_constraints_text + ")\n") + self.write("fr, frstar = kane." + "kanes_equations([" + + ", ".join(self.bodies.values()) + "], forceList)\n") + self.fr_expr.append(ctx.parentCtx) + self.kane_parsed = True + self.setValue(ctx, func_name) + + def exitMatrices(self, ctx): + # Tree annotation for Matrices which is a labeled subrule of the parser rule expr. + + # MO = [a, b; c, d] + # we generate _sm.Matrix([a, b, c, d]).reshape(2, 2) + # The reshape values are determined by counting the "," and ";" in the Autolev matrix + + # Eg: + # [1, 2, 3; 4, 5, 6; 7, 8, 9; 10, 11, 12] + # semicolon_count = 3 and rows = 3+1 = 4 + # comma_count = 8 and cols = 8/rows + 1 = 8/4 + 1 = 3 + + # TODO** Parse block matrices + self.matrix_expr.append(ctx) + l = [] + semicolon_count = 0 + comma_count = 0 + for i in range(ctx.matrix().getChildCount()): + child = ctx.matrix().getChild(i) + if child == AutolevParser.ExprContext: + l.append(self.getValue(child)) + elif child.getText() == ";": + semicolon_count += 1 + l.append(",") + elif child.getText() == ",": + comma_count += 1 + l.append(",") + else: + try: + try: + l.append(self.getValue(child)) + except Exception: + l.append(self.symbol_table[child.getText().lower()]) + except Exception: + l.append(child.getText().lower()) + num_of_rows = semicolon_count + 1 + num_of_cols = (comma_count//num_of_rows) + 1 + + self.setValue(ctx, "_sm.Matrix(" + "".join(l) + ")" + ".reshape(" + + str(num_of_rows) + ", " + str(num_of_cols) + ")") + + def exitVectorOrDyadic(self, ctx): + self.vector_expr.append(ctx) + ch = ctx.vec() + + if ch.getChild(0).getText() == "0>": + self.setValue(ctx, "0") + + elif ch.getChild(0).getText() == "1>>": + self.setValue(ctx, "1>>") + + elif "_" in ch.ID().getText() and ch.ID().getText().count('_') == 2: + vec_text = ch.getText().lower() + v1, v2, v3 = ch.ID().getText().lower().split('_') + + if v1 == "p": + if self.type2[v2] == "point": + e2 = self.symbol_table2[v2] + elif self.type2[v2] == "particle": + e2 = self.symbol_table2[v2] + ".point" + if self.type2[v3] == "point": + e3 = self.symbol_table2[v3] + elif self.type2[v3] == "particle": + e3 = self.symbol_table2[v3] + ".point" + get_vec = e3 + ".pos_from(" + e2 + ")" + self.setValue(ctx, get_vec) + + elif v1 in ("w", "alf"): + if v1 == "w": + text = ".ang_vel_in(" + elif v1 == "alf": + text = ".ang_acc_in(" + if self.type2[v2] == "bodies": + e2 = self.symbol_table2[v2] + "_f" + elif self.type2[v2] == "frame": + e2 = self.symbol_table2[v2] + if self.type2[v3] == "bodies": + e3 = self.symbol_table2[v3] + "_f" + elif self.type2[v3] == "frame": + e3 = self.symbol_table2[v3] + get_vec = e2 + text + e3 + ")" + self.setValue(ctx, get_vec) + + elif v1 in ("v", "a"): + if v1 == "v": + text = ".vel(" + elif v1 == "a": + text = ".acc(" + if self.type2[v2] == "point": + e2 = self.symbol_table2[v2] + elif self.type2[v2] == "particle": + e2 = self.symbol_table2[v2] + ".point" + get_vec = e2 + text + self.symbol_table2[v3] + ")" + self.setValue(ctx, get_vec) + + else: + self.setValue(ctx, vec_text.replace(">", "")) + + else: + vec_text = ch.getText().lower() + name = self.symbol_table[vec_text] + self.setValue(ctx, name) + + def exitIndexing(self, ctx): + if ctx.getChildCount() == 4: + try: + int_text = str(int(self.getValue(ctx.getChild(2))) - 1) + except Exception: + int_text = self.getValue(ctx.getChild(2)) + " - 1" + self.setValue(ctx, ctx.ID().getText().lower() + "[" + int_text + "]") + elif ctx.getChildCount() == 6: + try: + int_text1 = str(int(self.getValue(ctx.getChild(2))) - 1) + except Exception: + int_text1 = self.getValue(ctx.getChild(2)) + " - 1" + try: + int_text2 = str(int(self.getValue(ctx.getChild(4))) - 1) + except Exception: + int_text2 = self.getValue(ctx.getChild(2)) + " - 1" + self.setValue(ctx, ctx.ID().getText().lower() + "[" + int_text1 + ", " + int_text2 + "]") + + + # ================== Subrules of parser rule expr (End) ====================== # + + def exitRegularAssign(self, ctx): + # Handle assignments of type ID = expr + if ctx.equals().getText() in ["=", "+=", "-=", "*=", "/="]: + equals = ctx.equals().getText() + elif ctx.equals().getText() == ":=": + equals = " = " + elif ctx.equals().getText() == "^=": + equals = "**=" + + try: + a = ctx.ID().getText().lower() + "'"*ctx.diff().getText().count("'") + except Exception: + a = ctx.ID().getText().lower() + + if a in self.type.keys() and self.type[a] in ("motionvariable", "motionvariable'") and\ + self.type[ctx.expr().getText().lower()] in ("motionvariable", "motionvariable'"): + b = ctx.expr().getText().lower() + if "'" in b and "'" not in a: + a, b = b, a + if not self.kane_parsed: + self.kd_eqs.append(a + "-" + b) + self.kd_equivalents.update({self.symbol_table[a]: + self.symbol_table[b]}) + self.kd_equivalents2.update({self.symbol_table[b]: + self.symbol_table[a]}) + + if a in self.symbol_table.keys() and a in self.type.keys() and self.type[a] in ("variable", "motionvariable"): + self.explicit.update({self.symbol_table[a]: self.getValue(ctx.expr())}) + + else: + if ctx.expr() in self.matrix_expr: + self.type.update({a: "matrix"}) + + try: + b = self.symbol_table[a] + except KeyError: + self.symbol_table[a] = a + + if "_" in a and a.count("_") == 1: + e1, e2 = a.split('_') + if e1 in self.type2.keys() and self.type2[e1] in ("frame", "bodies")\ + and e2 in self.type2.keys() and self.type2[e2] in ("frame", "bodies"): + if self.type2[e1] == "bodies": + t1 = "_f" + else: + t1 = "" + if self.type2[e2] == "bodies": + t2 = "_f" + else: + t2 = "" + + self.write(self.symbol_table2[e2] + t2 + ".orient(" + self.symbol_table2[e1] + + t1 + ", 'DCM', " + self.getValue(ctx.expr()) + ")\n") + else: + self.write(self.symbol_table[a] + " " + equals + " " + + self.getValue(ctx.expr()) + "\n") + else: + self.write(self.symbol_table[a] + " " + equals + " " + + self.getValue(ctx.expr()) + "\n") + + def exitIndexAssign(self, ctx): + # Handle assignments of type ID[index] = expr + if ctx.equals().getText() in ["=", "+=", "-=", "*=", "/="]: + equals = ctx.equals().getText() + elif ctx.equals().getText() == ":=": + equals = " = " + elif ctx.equals().getText() == "^=": + equals = "**=" + + text = ctx.ID().getText().lower() + self.type.update({text: "matrix"}) + # Handle assignments of type ID[2] = expr + if ctx.index().getChildCount() == 1: + if ctx.index().getChild(0).getText() == "1": + self.type.update({text: "matrix"}) + self.symbol_table.update({text: text}) + self.write(text + " = " + "_sm.Matrix([[0]])\n") + self.write(text + "[0] = " + self.getValue(ctx.expr()) + "\n") + else: + # m = m.row_insert(m.shape[0], _sm.Matrix([[0]])) + self.write(text + " = " + text + + ".row_insert(" + text + ".shape[0]" + ", " + "_sm.Matrix([[0]])" + ")\n") + self.write(text + "[" + text + ".shape[0]-1" + "] = " + self.getValue(ctx.expr()) + "\n") + + # Handle assignments of type ID[2, 2] = expr + elif ctx.index().getChildCount() == 3: + l = [] + try: + l.append(str(int(self.getValue(ctx.index().getChild(0)))-1)) + except Exception: + l.append(self.getValue(ctx.index().getChild(0)) + "-1") + l.append(",") + try: + l.append(str(int(self.getValue(ctx.index().getChild(2)))-1)) + except Exception: + l.append(self.getValue(ctx.index().getChild(2)) + "-1") + self.write(self.symbol_table[ctx.ID().getText().lower()] + + "[" + "".join(l) + "]" + " " + equals + " " + self.getValue(ctx.expr()) + "\n") + + def exitVecAssign(self, ctx): + # Handle assignments of the type vec = expr + ch = ctx.vec() + vec_text = ch.getText().lower() + + if "_" in ch.ID().getText(): + num = ch.ID().getText().count('_') + + if num == 2: + v1, v2, v3 = ch.ID().getText().lower().split('_') + + if v1 == "p": + if self.type2[v2] == "point": + e2 = self.symbol_table2[v2] + elif self.type2[v2] == "particle": + e2 = self.symbol_table2[v2] + ".point" + if self.type2[v3] == "point": + e3 = self.symbol_table2[v3] + elif self.type2[v3] == "particle": + e3 = self.symbol_table2[v3] + ".point" + # ab.set_pos(na, la*a.x) + self.write(e3 + ".set_pos(" + e2 + ", " + self.getValue(ctx.expr()) + ")\n") + + elif v1 in ("w", "alf"): + if v1 == "w": + text = ".set_ang_vel(" + elif v1 == "alf": + text = ".set_ang_acc(" + # a.set_ang_vel(n, qad*a.z) + if self.type2[v2] == "bodies": + e2 = self.symbol_table2[v2] + "_f" + else: + e2 = self.symbol_table2[v2] + if self.type2[v3] == "bodies": + e3 = self.symbol_table2[v3] + "_f" + else: + e3 = self.symbol_table2[v3] + self.write(e2 + text + e3 + ", " + self.getValue(ctx.expr()) + ")\n") + + elif v1 in ("v", "a"): + if v1 == "v": + text = ".set_vel(" + elif v1 == "a": + text = ".set_acc(" + if self.type2[v2] == "point": + e2 = self.symbol_table2[v2] + elif self.type2[v2] == "particle": + e2 = self.symbol_table2[v2] + ".point" + self.write(e2 + text + self.symbol_table2[v3] + + ", " + self.getValue(ctx.expr()) + ")\n") + elif v1 == "i": + if v2 in self.type2.keys() and self.type2[v2] == "bodies": + self.write(self.symbol_table2[v2] + ".inertia = (" + self.getValue(ctx.expr()) + + ", " + self.symbol_table2[v3] + ")\n") + self.inertia_point.update({v2: v3}) + elif v2 in self.type2.keys() and self.type2[v2] == "particle": + self.write(ch.ID().getText().lower() + " = " + self.getValue(ctx.expr()) + "\n") + else: + self.write(ch.ID().getText().lower() + " = " + self.getValue(ctx.expr()) + "\n") + else: + self.write(ch.ID().getText().lower() + " = " + self.getValue(ctx.expr()) + "\n") + + elif num == 1: + v1, v2 = ch.ID().getText().lower().split('_') + + if v1 in ("force", "torque"): + if self.type2[v2] in ("point", "frame"): + e2 = self.symbol_table2[v2] + elif self.type2[v2] == "particle": + e2 = self.symbol_table2[v2] + ".point" + self.symbol_table.update({vec_text: ch.ID().getText().lower()}) + + if e2 in self.forces.keys(): + self.forces[e2] = self.forces[e2] + " + " + self.getValue(ctx.expr()) + else: + self.forces.update({e2: self.getValue(ctx.expr())}) + self.write(ch.ID().getText().lower() + " = " + self.forces[e2] + "\n") + + else: + name = ch.ID().getText().lower() + self.symbol_table.update({vec_text: name}) + self.write(ch.ID().getText().lower() + " = " + self.getValue(ctx.expr()) + "\n") + else: + name = ch.ID().getText().lower() + self.symbol_table.update({vec_text: name}) + self.write(name + " " + ctx.getChild(1).getText() + " " + self.getValue(ctx.expr()) + "\n") + else: + name = ch.ID().getText().lower() + self.symbol_table.update({vec_text: name}) + self.write(name + " " + ctx.getChild(1).getText() + " " + self.getValue(ctx.expr()) + "\n") + + def enterInputs2(self, ctx): + self.in_inputs = True + + # Inputs + def exitInputs2(self, ctx): + # Stores numerical values given by the input command which + # are used for codegen and numerical analysis. + if ctx.getChildCount() == 3: + try: + self.inputs.update({self.symbol_table[ctx.id_diff().getText().lower()]: self.getValue(ctx.expr(0))}) + except Exception: + self.inputs.update({ctx.id_diff().getText().lower(): self.getValue(ctx.expr(0))}) + elif ctx.getChildCount() == 4: + try: + self.inputs.update({self.symbol_table[ctx.id_diff().getText().lower()]: + (self.getValue(ctx.expr(0)), self.getValue(ctx.expr(1)))}) + except Exception: + self.inputs.update({ctx.id_diff().getText().lower(): + (self.getValue(ctx.expr(0)), self.getValue(ctx.expr(1)))}) + + self.in_inputs = False + + def enterOutputs(self, ctx): + self.in_outputs = True + def exitOutputs(self, ctx): + self.in_outputs = False + + def exitOutputs2(self, ctx): + try: + if "[" in ctx.expr(1).getText(): + self.outputs.append(self.symbol_table[ctx.expr(0).getText().lower()] + + ctx.expr(1).getText().lower()) + else: + self.outputs.append(self.symbol_table[ctx.expr(0).getText().lower()]) + + except Exception: + pass + + # Code commands + def exitCodegen(self, ctx): + # Handles the CODE() command ie the solvers and the codgen part. + # Uses linsolve for the algebraic solvers and nsolve for non linear solvers. + + if ctx.functionCall().getChild(0).getText().lower() == "algebraic": + matrix_name = self.getValue(ctx.functionCall().expr(0)) + e = [] + d = [] + for i in range(1, (ctx.functionCall().getChildCount()-2)//2): + a = self.getValue(ctx.functionCall().expr(i)) + e.append(a) + + for i in self.inputs.keys(): + d.append(i + ":" + self.inputs[i]) + self.write(matrix_name + "_list" + " = " + "[]\n") + self.write("for i in " + matrix_name + ": " + matrix_name + + "_list" + ".append(i.subs({" + ", ".join(d) + "}))\n") + self.write("print(_sm.linsolve(" + matrix_name + "_list" + ", " + ",".join(e) + "))\n") + + elif ctx.functionCall().getChild(0).getText().lower() == "nonlinear": + e = [] + d = [] + guess = [] + for i in range(1, (ctx.functionCall().getChildCount()-2)//2): + a = self.getValue(ctx.functionCall().expr(i)) + e.append(a) + #print(self.inputs) + for i in self.inputs.keys(): + if i in self.symbol_table.keys(): + if type(self.inputs[i]) is tuple: + j, z = self.inputs[i] + else: + j = self.inputs[i] + z = "" + if i not in e: + if z == "deg": + d.append(i + ":" + "_np.deg2rad(" + j + ")") + else: + d.append(i + ":" + j) + else: + if z == "deg": + guess.append("_np.deg2rad(" + j + ")") + else: + guess.append(j) + + self.write("matrix_list" + " = " + "[]\n") + self.write("for i in " + self.getValue(ctx.functionCall().expr(0)) + ":") + self.write("matrix_list" + ".append(i.subs({" + ", ".join(d) + "}))\n") + self.write("print(_sm.nsolve(matrix_list," + "(" + ",".join(e) + ")" + + ",(" + ",".join(guess) + ")" + "))\n") + + elif ctx.functionCall().getChild(0).getText().lower() in ["ode", "dynamics"] and self.include_numeric: + if self.kane_type == "no_args": + for i in self.symbol_table.keys(): + try: + if self.type[i] == "constants" or self.type[self.symbol_table[i]] == "constants": + self.constants.append(self.symbol_table[i]) + except Exception: + pass + q_add_u = self.q_ind + self.q_dep + self.u_ind + self.u_dep + x0 = [] + for i in q_add_u: + try: + if i in self.inputs.keys(): + if type(self.inputs[i]) is tuple: + if self.inputs[i][1] == "deg": + x0.append(i + ":" + "_np.deg2rad(" + self.inputs[i][0] + ")") + else: + x0.append(i + ":" + self.inputs[i][0]) + else: + x0.append(i + ":" + self.inputs[i]) + elif self.kd_equivalents[i] in self.inputs.keys(): + if type(self.inputs[self.kd_equivalents[i]]) is tuple: + x0.append(i + ":" + self.inputs[self.kd_equivalents[i]][0]) + else: + x0.append(i + ":" + self.inputs[self.kd_equivalents[i]]) + except Exception: + pass + + # numerical constants + numerical_constants = [] + for i in self.constants: + if i in self.inputs.keys(): + if type(self.inputs[i]) is tuple: + numerical_constants.append(self.inputs[i][0]) + else: + numerical_constants.append(self.inputs[i]) + + # t = linspace + t_final = self.inputs["tfinal"] + integ_stp = self.inputs["integstp"] + + self.write("from pydy.system import System\n") + const_list = [] + if numerical_constants: + for i in range(len(self.constants)): + const_list.append(self.constants[i] + ":" + numerical_constants[i]) + specifieds = [] + if self.t: + specifieds.append("_me.dynamicsymbols('t')" + ":" + "lambda x, t: t") + + for i in self.inputs: + if i in self.symbol_table.keys() and self.symbol_table[i] not in\ + self.constants + self.q_ind + self.q_dep + self.u_ind + self.u_dep: + specifieds.append(self.symbol_table[i] + ":" + self.inputs[i]) + + self.write("sys = System(kane, constants = {" + ", ".join(const_list) + "},\n" + + "specifieds={" + ", ".join(specifieds) + "},\n" + + "initial_conditions={" + ", ".join(x0) + "},\n" + + "times = _np.linspace(0.0, " + str(t_final) + ", " + str(t_final) + + "/" + str(integ_stp) + "))\n\ny=sys.integrate()\n") + + # For outputs other than qs and us. + other_outputs = [] + for i in self.outputs: + if i not in q_add_u: + if "[" in i: + other_outputs.append((i[:-3] + i[-2], i[:-3] + "[" + str(int(i[-2])-1) + "]")) + else: + other_outputs.append((i, i)) + + for i in other_outputs: + self.write(i[0] + "_out" + " = " + "[]\n") + if other_outputs: + self.write("for i in y:\n") + self.write(" q_u_dict = dict(zip(sys.coordinates+sys.speeds, i))\n") + for i in other_outputs: + self.write(" "*4 + i[0] + "_out" + ".append(" + i[1] + ".subs(q_u_dict)" + + ".subs(sys.constants).evalf())\n") + + # Standalone function calls (used for dual functions) + def exitFunctionCall(self, ctx): + # Basically deals with standalone function calls ie functions which are not a part of + # expressions and assignments. Autolev Dual functions can both appear in standalone + # function calls and also on the right hand side as part of expr or assignment. + + # Dual functions are indicated by a * in the comments below + + # Checks if the function is a statement on its own + if ctx.parentCtx.getRuleIndex() == AutolevParser.RULE_stat: + func_name = ctx.getChild(0).getText().lower() + # Expand(E, n:m) * + if func_name == "expand": + # If the first argument is a pre declared variable. + expr = self.getValue(ctx.expr(0)) + symbol = self.symbol_table[ctx.expr(0).getText().lower()] + if ctx.expr(0) in self.matrix_expr or (expr in self.type.keys() and self.type[expr] == "matrix"): + self.write(symbol + " = " + "_sm.Matrix([i.expand() for i in " + expr + "])" + + ".reshape((" + expr + ").shape[0], " + "(" + expr + ").shape[1])\n") + else: + self.write(symbol + " = " + symbol + "." + "expand()\n") + + # Factor(E, x) * + elif func_name == "factor": + expr = self.getValue(ctx.expr(0)) + symbol = self.symbol_table[ctx.expr(0).getText().lower()] + if ctx.expr(0) in self.matrix_expr or (expr in self.type.keys() and self.type[expr] == "matrix"): + self.write(symbol + " = " + "_sm.Matrix([_sm.factor(i," + self.getValue(ctx.expr(1)) + + ") for i in " + expr + "])" + + ".reshape((" + expr + ").shape[0], " + "(" + expr + ").shape[1])\n") + else: + self.write(expr + " = " + "_sm.factor(" + expr + ", " + + self.getValue(ctx.expr(1)) + ")\n") + + # Solve(Zero, x, y) + elif func_name == "solve": + l = [] + l2 = [] + num = 0 + for i in range(1, ctx.getChildCount()): + if ctx.getChild(i).getText() == ",": + num+=1 + try: + l.append(self.getValue(ctx.getChild(i))) + except Exception: + l.append(ctx.getChild(i).getText()) + + if i != 2: + try: + l2.append(self.getValue(ctx.getChild(i))) + except Exception: + pass + + for i in l2: + self.explicit.update({i: "_sm.solve" + "".join(l) + "[" + i + "]"}) + + self.write("print(_sm.solve" + "".join(l) + ")\n") + + # Arrange(y, n, x) * + elif func_name == "arrange": + expr = self.getValue(ctx.expr(0)) + symbol = self.symbol_table[ctx.expr(0).getText().lower()] + + if ctx.expr(0) in self.matrix_expr or (expr in self.type.keys() and self.type[expr] == "matrix"): + self.write(symbol + " = " + "_sm.Matrix([i.collect(" + self.getValue(ctx.expr(2)) + + ")" + "for i in " + expr + "])" + + ".reshape((" + expr + ").shape[0], " + "(" + expr + ").shape[1])\n") + else: + self.write(self.getValue(ctx.expr(0)) + ".collect(" + + self.getValue(ctx.expr(2)) + ")\n") + + # Eig(M, EigenValue, EigenVec) + elif func_name == "eig": + self.symbol_table.update({ctx.expr(1).getText().lower(): ctx.expr(1).getText().lower()}) + self.symbol_table.update({ctx.expr(2).getText().lower(): ctx.expr(2).getText().lower()}) + # _sm.Matrix([i.evalf() for i in (i_s_so).eigenvals().keys()]) + self.write(ctx.expr(1).getText().lower() + " = " + + "_sm.Matrix([i.evalf() for i in " + + "(" + self.getValue(ctx.expr(0)) + ")" + ".eigenvals().keys()])\n") + # _sm.Matrix([i[2][0].evalf() for i in (i_s_o).eigenvects()]).reshape(i_s_o.shape[0], i_s_o.shape[1]) + self.write(ctx.expr(2).getText().lower() + " = " + + "_sm.Matrix([i[2][0].evalf() for i in " + "(" + self.getValue(ctx.expr(0)) + ")" + + ".eigenvects()]).reshape(" + self.getValue(ctx.expr(0)) + ".shape[0], " + + self.getValue(ctx.expr(0)) + ".shape[1])\n") + + # Simprot(N, A, 3, qA) + elif func_name == "simprot": + # A.orient(N, 'Axis', qA, N.z) + if self.type2[ctx.expr(0).getText().lower()] == "frame": + frame1 = self.symbol_table2[ctx.expr(0).getText().lower()] + elif self.type2[ctx.expr(0).getText().lower()] == "bodies": + frame1 = self.symbol_table2[ctx.expr(0).getText().lower()] + "_f" + if self.type2[ctx.expr(1).getText().lower()] == "frame": + frame2 = self.symbol_table2[ctx.expr(1).getText().lower()] + elif self.type2[ctx.expr(1).getText().lower()] == "bodies": + frame2 = self.symbol_table2[ctx.expr(1).getText().lower()] + "_f" + e2 = "" + if ctx.expr(2).getText()[0] == "-": + e2 = "-1*" + if ctx.expr(2).getText() in ("1", "-1"): + e = frame1 + ".x" + elif ctx.expr(2).getText() in ("2", "-2"): + e = frame1 + ".y" + elif ctx.expr(2).getText() in ("3", "-3"): + e = frame1 + ".z" + else: + e = self.getValue(ctx.expr(2)) + e2 = "" + + if "degrees" in self.settings.keys() and self.settings["degrees"] == "off": + value = self.getValue(ctx.expr(3)) + else: + if ctx.expr(3) in self.numeric_expr: + value = "_np.deg2rad(" + self.getValue(ctx.expr(3)) + ")" + else: + value = self.getValue(ctx.expr(3)) + self.write(frame2 + ".orient(" + frame1 + + ", " + "'Axis'" + ", " + "[" + value + + ", " + e2 + e + "]" + ")\n") + + # Express(A2>, B) * + elif func_name == "express": + if self.type2[ctx.expr(1).getText().lower()] == "bodies": + f = "_f" + else: + f = "" + + if '_' in ctx.expr(0).getText().lower() and ctx.expr(0).getText().count('_') == 2: + vec = ctx.expr(0).getText().lower().replace(">", "").split('_') + v1 = self.symbol_table2[vec[1]] + v2 = self.symbol_table2[vec[2]] + if vec[0] == "p": + self.write(v2 + ".set_pos(" + v1 + ", " + "(" + self.getValue(ctx.expr(0)) + + ")" + ".express(" + self.symbol_table2[ctx.expr(1).getText().lower()] + f + "))\n") + elif vec[0] == "v": + self.write(v1 + ".set_vel(" + v2 + ", " + "(" + self.getValue(ctx.expr(0)) + + ")" + ".express(" + self.symbol_table2[ctx.expr(1).getText().lower()] + f + "))\n") + elif vec[0] == "a": + self.write(v1 + ".set_acc(" + v2 + ", " + "(" + self.getValue(ctx.expr(0)) + + ")" + ".express(" + self.symbol_table2[ctx.expr(1).getText().lower()] + f + "))\n") + else: + self.write(self.getValue(ctx.expr(0)) + " = " + "(" + self.getValue(ctx.expr(0)) + ")" + ".express(" + + self.symbol_table2[ctx.expr(1).getText().lower()] + f + ")\n") + else: + self.write(self.getValue(ctx.expr(0)) + " = " + "(" + self.getValue(ctx.expr(0)) + ")" + ".express(" + + self.symbol_table2[ctx.expr(1).getText().lower()] + f + ")\n") + + # Angvel(A, B) + elif func_name == "angvel": + self.write("print(" + self.symbol_table2[ctx.expr(1).getText().lower()] + + ".ang_vel_in(" + self.symbol_table2[ctx.expr(0).getText().lower()] + "))\n") + + # v2pts(N, A, O, P) + elif func_name in ("v2pts", "a2pts", "v2pt", "a1pt"): + if func_name == "v2pts": + text = ".v2pt_theory(" + elif func_name == "a2pts": + text = ".a2pt_theory(" + elif func_name == "v1pt": + text = ".v1pt_theory(" + elif func_name == "a1pt": + text = ".a1pt_theory(" + if self.type2[ctx.expr(1).getText().lower()] == "frame": + frame = self.symbol_table2[ctx.expr(1).getText().lower()] + elif self.type2[ctx.expr(1).getText().lower()] == "bodies": + frame = self.symbol_table2[ctx.expr(1).getText().lower()] + "_f" + expr_list = [] + for i in range(2, 4): + if self.type2[ctx.expr(i).getText().lower()] == "point": + expr_list.append(self.symbol_table2[ctx.expr(i).getText().lower()]) + elif self.type2[ctx.expr(i).getText().lower()] == "particle": + expr_list.append(self.symbol_table2[ctx.expr(i).getText().lower()] + ".point") + + self.write(expr_list[1] + text + expr_list[0] + + "," + self.symbol_table2[ctx.expr(0).getText().lower()] + "," + + frame + ")\n") + + # Gravity(g*N1>) + elif func_name == "gravity": + for i in self.bodies.keys(): + if self.type2[i] == "bodies": + e = self.symbol_table2[i] + ".masscenter" + elif self.type2[i] == "particle": + e = self.symbol_table2[i] + ".point" + if e in self.forces.keys(): + self.forces[e] = self.forces[e] + self.symbol_table2[i] +\ + ".mass*(" + self.getValue(ctx.expr(0)) + ")" + else: + self.forces.update({e: self.symbol_table2[i] + + ".mass*(" + self.getValue(ctx.expr(0)) + ")"}) + self.write("force_" + i + " = " + self.forces[e] + "\n") + + # Explicit(EXPRESS(IMPLICIT>,C)) + elif func_name == "explicit": + if ctx.expr(0) in self.vector_expr: + self.vector_expr.append(ctx) + expr = self.getValue(ctx.expr(0)) + if self.explicit.keys(): + explicit_list = [] + for i in self.explicit.keys(): + explicit_list.append(i + ":" + self.explicit[i]) + if '_' in ctx.expr(0).getText().lower() and ctx.expr(0).getText().count('_') == 2: + vec = ctx.expr(0).getText().lower().replace(">", "").split('_') + v1 = self.symbol_table2[vec[1]] + v2 = self.symbol_table2[vec[2]] + if vec[0] == "p": + self.write(v2 + ".set_pos(" + v1 + ", " + "(" + expr + + ")" + ".subs({" + ", ".join(explicit_list) + "}))\n") + elif vec[0] == "v": + self.write(v2 + ".set_vel(" + v1 + ", " + "(" + expr + + ")" + ".subs({" + ", ".join(explicit_list) + "}))\n") + elif vec[0] == "a": + self.write(v2 + ".set_acc(" + v1 + ", " + "(" + expr + + ")" + ".subs({" + ", ".join(explicit_list) + "}))\n") + else: + self.write(expr + " = " + "(" + expr + ")" + ".subs({" + ", ".join(explicit_list) + "})\n") + else: + self.write(expr + " = " + "(" + expr + ")" + ".subs({" + ", ".join(explicit_list) + "})\n") + + # Force(O/Q, -k*Stretch*Uvec>) + elif func_name in ("force", "torque"): + + if "/" in ctx.expr(0).getText().lower(): + p1 = ctx.expr(0).getText().lower().split('/')[0] + p2 = ctx.expr(0).getText().lower().split('/')[1] + if self.type2[p1] in ("point", "frame"): + pt1 = self.symbol_table2[p1] + elif self.type2[p1] == "particle": + pt1 = self.symbol_table2[p1] + ".point" + if self.type2[p2] in ("point", "frame"): + pt2 = self.symbol_table2[p2] + elif self.type2[p2] == "particle": + pt2 = self.symbol_table2[p2] + ".point" + if pt1 in self.forces.keys(): + self.forces[pt1] = self.forces[pt1] + " + -1*("+self.getValue(ctx.expr(1)) + ")" + self.write("force_" + p1 + " = " + self.forces[pt1] + "\n") + else: + self.forces.update({pt1: "-1*("+self.getValue(ctx.expr(1)) + ")"}) + self.write("force_" + p1 + " = " + self.forces[pt1] + "\n") + if pt2 in self.forces.keys(): + self.forces[pt2] = self.forces[pt2] + "+ " + self.getValue(ctx.expr(1)) + self.write("force_" + p2 + " = " + self.forces[pt2] + "\n") + else: + self.forces.update({pt2: self.getValue(ctx.expr(1))}) + self.write("force_" + p2 + " = " + self.forces[pt2] + "\n") + + elif ctx.expr(0).getChildCount() == 1: + p1 = ctx.expr(0).getText().lower() + if self.type2[p1] in ("point", "frame"): + pt1 = self.symbol_table2[p1] + elif self.type2[p1] == "particle": + pt1 = self.symbol_table2[p1] + ".point" + if pt1 in self.forces.keys(): + self.forces[pt1] = self.forces[pt1] + "+ -1*(" + self.getValue(ctx.expr(1)) + ")" + else: + self.forces.update({pt1: "-1*(" + self.getValue(ctx.expr(1)) + ")"}) + + # Constrain(Dependent[qB]) + elif func_name == "constrain": + if ctx.getChild(2).getChild(0).getText().lower() == "dependent": + self.write("velocity_constraints = [i for i in dependent]\n") + x = (ctx.expr(0).getChildCount()-2)//2 + for i in range(x): + self.dependent_variables.append(self.getValue(ctx.expr(0).expr(i))) + + # Kane() + elif func_name == "kane": + if ctx.getChildCount() == 3: + self.kane_type = "no_args" + + # Settings + def exitSettings(self, ctx): + # Stores settings like Complex on/off, Degrees on/off etc in self.settings. + try: + self.settings.update({ctx.getChild(0).getText().lower(): + ctx.getChild(1).getText().lower()}) + except Exception: + pass + + def exitMassDecl2(self, ctx): + # Used for declaring the masses of particles and rigidbodies. + particle = self.symbol_table2[ctx.getChild(0).getText().lower()] + if ctx.getText().count("=") == 2: + if ctx.expr().expr(1) in self.numeric_expr: + e = "_sm.S(" + self.getValue(ctx.expr().expr(1)) + ")" + else: + e = self.getValue(ctx.expr().expr(1)) + self.symbol_table.update({ctx.expr().expr(0).getText().lower(): ctx.expr().expr(0).getText().lower()}) + self.write(ctx.expr().expr(0).getText().lower() + " = " + e + "\n") + mass = ctx.expr().expr(0).getText().lower() + else: + try: + if ctx.expr() in self.numeric_expr: + mass = "_sm.S(" + self.getValue(ctx.expr()) + ")" + else: + mass = self.getValue(ctx.expr()) + except Exception: + a_text = ctx.expr().getText().lower() + self.symbol_table.update({a_text: a_text}) + self.type.update({a_text: "constants"}) + self.write(a_text + " = " + "_sm.symbols('" + a_text + "')\n") + mass = a_text + + self.write(particle + ".mass = " + mass + "\n") + + def exitInertiaDecl(self, ctx): + inertia_list = [] + try: + ctx.ID(1).getText() + num = 5 + except Exception: + num = 2 + for i in range((ctx.getChildCount()-num)//2): + try: + if ctx.expr(i) in self.numeric_expr: + inertia_list.append("_sm.S(" + self.getValue(ctx.expr(i)) + ")") + else: + inertia_list.append(self.getValue(ctx.expr(i))) + except Exception: + a_text = ctx.expr(i).getText().lower() + self.symbol_table.update({a_text: a_text}) + self.type.update({a_text: "constants"}) + self.write(a_text + " = " + "_sm.symbols('" + a_text + "')\n") + inertia_list.append(a_text) + + if len(inertia_list) < 6: + for i in range(6-len(inertia_list)): + inertia_list.append("0") + # body_a.inertia = (_me.inertia(body_a, I1, I2, I3, 0, 0, 0), body_a_cm) + try: + frame = self.symbol_table2[ctx.ID(1).getText().lower()] + point = self.symbol_table2[ctx.ID(0).getText().lower().split('_')[1]] + body = self.symbol_table2[ctx.ID(0).getText().lower().split('_')[0]] + self.inertia_point.update({ctx.ID(0).getText().lower().split('_')[0] + : ctx.ID(0).getText().lower().split('_')[1]}) + self.write(body + ".inertia" + " = " + "(_me.inertia(" + frame + ", " + + ", ".join(inertia_list) + "), " + point + ")\n") + + except Exception: + body_name = self.symbol_table2[ctx.ID(0).getText().lower()] + body_name_cm = body_name + "_cm" + self.inertia_point.update({ctx.ID(0).getText().lower(): ctx.ID(0).getText().lower() + "o"}) + self.write(body_name + ".inertia" + " = " + "(_me.inertia(" + body_name + "_f" + ", " + + ", ".join(inertia_list) + "), " + body_name_cm + ")\n") diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_parse_autolev_antlr.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_parse_autolev_antlr.py new file mode 100644 index 0000000000000000000000000000000000000000..e43924aac30903ade996b31921d3960afae90284 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/_parse_autolev_antlr.py @@ -0,0 +1,38 @@ +from importlib.metadata import version +from sympy.external import import_module + + +autolevparser = import_module('sympy.parsing.autolev._antlr.autolevparser', + import_kwargs={'fromlist': ['AutolevParser']}) +autolevlexer = import_module('sympy.parsing.autolev._antlr.autolevlexer', + import_kwargs={'fromlist': ['AutolevLexer']}) +autolevlistener = import_module('sympy.parsing.autolev._antlr.autolevlistener', + import_kwargs={'fromlist': ['AutolevListener']}) + +AutolevParser = getattr(autolevparser, 'AutolevParser', None) +AutolevLexer = getattr(autolevlexer, 'AutolevLexer', None) +AutolevListener = getattr(autolevlistener, 'AutolevListener', None) + + +def parse_autolev(autolev_code, include_numeric): + antlr4 = import_module('antlr4') + if not antlr4 or not version('antlr4-python3-runtime').startswith('4.11'): + raise ImportError("Autolev parsing requires the antlr4 Python package," + " provided by pip (antlr4-python3-runtime)" + " conda (antlr-python-runtime), version 4.11") + try: + l = autolev_code.readlines() + input_stream = antlr4.InputStream("".join(l)) + except Exception: + input_stream = antlr4.InputStream(autolev_code) + + if AutolevListener: + from ._listener_autolev_antlr import MyListener + lexer = AutolevLexer(input_stream) + token_stream = antlr4.CommonTokenStream(lexer) + parser = AutolevParser(token_stream) + tree = parser.prog() + my_listener = MyListener(include_numeric) + walker = antlr4.ParseTreeWalker() + walker.walk(my_listener, tree) + return "".join(my_listener.output_code) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/README.txt b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..946b006bac33544fadd2dc6d24c22240c8fbc8e4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/README.txt @@ -0,0 +1,9 @@ +# parsing/tests/test_autolev.py uses the .al files in this directory as inputs and checks +# the equivalence of the parser generated codes and the respective .py files. + +# By default, this directory contains tests for all rules of the parser. + +# Additional tests consisting of full physics examples shall be made available soon in +# the form of another repository. One shall be able to copy the contents of that repo +# to this folder and use those tests after uncommenting the respective code in +# parsing/tests/test_autolev.py. diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/chaos_pendulum.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/chaos_pendulum.al new file mode 100644 index 0000000000000000000000000000000000000000..3bbb4d51b853bfd759df38d666a42adc1cbea190 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/chaos_pendulum.al @@ -0,0 +1,33 @@ +CONSTANTS G,LB,W,H +MOTIONVARIABLES' THETA'',PHI'',OMEGA',ALPHA' +NEWTONIAN N +BODIES A,B +SIMPROT(N,A,2,THETA) +SIMPROT(A,B,3,PHI) +POINT O +LA = (LB-H/2)/2 +P_O_AO> = LA*A3> +P_O_BO> = LB*A3> +OMEGA = THETA' +ALPHA = PHI' +W_A_N> = OMEGA*N2> +W_B_A> = ALPHA*A3> +V_O_N> = 0> +V2PTS(N, A, O, AO) +V2PTS(N, A, O, BO) +MASS A=MA, B=MB +IAXX = 1/12*MA*(2*LA)^2 +IAYY = IAXX +IAZZ = 0 +IBXX = 1/12*MB*H^2 +IBYY = 1/12*MB*(W^2+H^2) +IBZZ = 1/12*MB*W^2 +INERTIA A, IAXX, IAYY, IAZZ +INERTIA B, IBXX, IBYY, IBZZ +GRAVITY(G*N3>) +ZERO = FR() + FRSTAR() +KANE() +INPUT LB=0.2,H=0.1,W=0.2,MA=0.01,MB=0.1,G=9.81 +INPUT THETA = 90 DEG, PHI = 0.5 DEG, OMEGA=0, ALPHA=0 +INPUT TFINAL=10, INTEGSTP=0.02 +CODE DYNAMICS() some_filename.c diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/chaos_pendulum.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/chaos_pendulum.py new file mode 100644 index 0000000000000000000000000000000000000000..4435635720bb38f40366f55bb3ace0f6f6899284 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/chaos_pendulum.py @@ -0,0 +1,55 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +g, lb, w, h = _sm.symbols('g lb w h', real=True) +theta, phi, omega, alpha = _me.dynamicsymbols('theta phi omega alpha') +theta_d, phi_d, omega_d, alpha_d = _me.dynamicsymbols('theta_ phi_ omega_ alpha_', 1) +theta_dd, phi_dd = _me.dynamicsymbols('theta_ phi_', 2) +frame_n = _me.ReferenceFrame('n') +body_a_cm = _me.Point('a_cm') +body_a_cm.set_vel(frame_n, 0) +body_a_f = _me.ReferenceFrame('a_f') +body_a = _me.RigidBody('a', body_a_cm, body_a_f, _sm.symbols('m'), (_me.outer(body_a_f.x,body_a_f.x),body_a_cm)) +body_b_cm = _me.Point('b_cm') +body_b_cm.set_vel(frame_n, 0) +body_b_f = _me.ReferenceFrame('b_f') +body_b = _me.RigidBody('b', body_b_cm, body_b_f, _sm.symbols('m'), (_me.outer(body_b_f.x,body_b_f.x),body_b_cm)) +body_a_f.orient(frame_n, 'Axis', [theta, frame_n.y]) +body_b_f.orient(body_a_f, 'Axis', [phi, body_a_f.z]) +point_o = _me.Point('o') +la = (lb-h/2)/2 +body_a_cm.set_pos(point_o, la*body_a_f.z) +body_b_cm.set_pos(point_o, lb*body_a_f.z) +body_a_f.set_ang_vel(frame_n, omega*frame_n.y) +body_b_f.set_ang_vel(body_a_f, alpha*body_a_f.z) +point_o.set_vel(frame_n, 0) +body_a_cm.v2pt_theory(point_o,frame_n,body_a_f) +body_b_cm.v2pt_theory(point_o,frame_n,body_a_f) +ma = _sm.symbols('ma') +body_a.mass = ma +mb = _sm.symbols('mb') +body_b.mass = mb +iaxx = 1/12*ma*(2*la)**2 +iayy = iaxx +iazz = 0 +ibxx = 1/12*mb*h**2 +ibyy = 1/12*mb*(w**2+h**2) +ibzz = 1/12*mb*w**2 +body_a.inertia = (_me.inertia(body_a_f, iaxx, iayy, iazz, 0, 0, 0), body_a_cm) +body_b.inertia = (_me.inertia(body_b_f, ibxx, ibyy, ibzz, 0, 0, 0), body_b_cm) +force_a = body_a.mass*(g*frame_n.z) +force_b = body_b.mass*(g*frame_n.z) +kd_eqs = [theta_d - omega, phi_d - alpha] +forceList = [(body_a.masscenter,body_a.mass*(g*frame_n.z)), (body_b.masscenter,body_b.mass*(g*frame_n.z))] +kane = _me.KanesMethod(frame_n, q_ind=[theta,phi], u_ind=[omega, alpha], kd_eqs = kd_eqs) +fr, frstar = kane.kanes_equations([body_a, body_b], forceList) +zero = fr+frstar +from pydy.system import System +sys = System(kane, constants = {g:9.81, lb:0.2, w:0.2, h:0.1, ma:0.01, mb:0.1}, +specifieds={}, +initial_conditions={theta:_np.deg2rad(90), phi:_np.deg2rad(0.5), omega:0, alpha:0}, +times = _np.linspace(0.0, 10, 10/0.02)) + +y=sys.integrate() diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/double_pendulum.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/double_pendulum.al new file mode 100644 index 0000000000000000000000000000000000000000..0b6d72a072e093a6cb048a0b7976041ee9c2f4f3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/double_pendulum.al @@ -0,0 +1,25 @@ +MOTIONVARIABLES' Q{2}', U{2}' +CONSTANTS L,M,G +NEWTONIAN N +FRAMES A,B +SIMPROT(N, A, 3, Q1) +SIMPROT(N, B, 3, Q2) +W_A_N>=U1*N3> +W_B_N>=U2*N3> +POINT O +PARTICLES P,R +P_O_P> = L*A1> +P_P_R> = L*B1> +V_O_N> = 0> +V2PTS(N, A, O, P) +V2PTS(N, B, P, R) +MASS P=M, R=M +Q1' = U1 +Q2' = U2 +GRAVITY(G*N1>) +ZERO = FR() + FRSTAR() +KANE() +INPUT M=1,G=9.81,L=1 +INPUT Q1=.1,Q2=.2,U1=0,U2=0 +INPUT TFINAL=10, INTEGSTP=.01 +CODE DYNAMICS() some_filename.c diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/double_pendulum.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/double_pendulum.py new file mode 100644 index 0000000000000000000000000000000000000000..12c73c3b4b198399f4c45f5e00d556c859caff74 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/double_pendulum.py @@ -0,0 +1,39 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +q1, q2, u1, u2 = _me.dynamicsymbols('q1 q2 u1 u2') +q1_d, q2_d, u1_d, u2_d = _me.dynamicsymbols('q1_ q2_ u1_ u2_', 1) +l, m, g = _sm.symbols('l m g', real=True) +frame_n = _me.ReferenceFrame('n') +frame_a = _me.ReferenceFrame('a') +frame_b = _me.ReferenceFrame('b') +frame_a.orient(frame_n, 'Axis', [q1, frame_n.z]) +frame_b.orient(frame_n, 'Axis', [q2, frame_n.z]) +frame_a.set_ang_vel(frame_n, u1*frame_n.z) +frame_b.set_ang_vel(frame_n, u2*frame_n.z) +point_o = _me.Point('o') +particle_p = _me.Particle('p', _me.Point('p_pt'), _sm.Symbol('m')) +particle_r = _me.Particle('r', _me.Point('r_pt'), _sm.Symbol('m')) +particle_p.point.set_pos(point_o, l*frame_a.x) +particle_r.point.set_pos(particle_p.point, l*frame_b.x) +point_o.set_vel(frame_n, 0) +particle_p.point.v2pt_theory(point_o,frame_n,frame_a) +particle_r.point.v2pt_theory(particle_p.point,frame_n,frame_b) +particle_p.mass = m +particle_r.mass = m +force_p = particle_p.mass*(g*frame_n.x) +force_r = particle_r.mass*(g*frame_n.x) +kd_eqs = [q1_d - u1, q2_d - u2] +forceList = [(particle_p.point,particle_p.mass*(g*frame_n.x)), (particle_r.point,particle_r.mass*(g*frame_n.x))] +kane = _me.KanesMethod(frame_n, q_ind=[q1,q2], u_ind=[u1, u2], kd_eqs = kd_eqs) +fr, frstar = kane.kanes_equations([particle_p, particle_r], forceList) +zero = fr+frstar +from pydy.system import System +sys = System(kane, constants = {l:1, m:1, g:9.81}, +specifieds={}, +initial_conditions={q1:.1, q2:.2, u1:0, u2:0}, +times = _np.linspace(0.0, 10, 10/.01)) + +y=sys.integrate() diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/mass_spring_damper.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/mass_spring_damper.al new file mode 100644 index 0000000000000000000000000000000000000000..4892e5ca8cb18cad6b14a2a37cbdc1f7fb8217ac --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/mass_spring_damper.al @@ -0,0 +1,19 @@ +CONSTANTS M,K,B,G +MOTIONVARIABLES' POSITION',SPEED' +VARIABLES O +FORCE = O*SIN(T) +NEWTONIAN CEILING +POINTS ORIGIN +V_ORIGIN_CEILING> = 0> +PARTICLES BLOCK +P_ORIGIN_BLOCK> = POSITION*CEILING1> +MASS BLOCK=M +V_BLOCK_CEILING>=SPEED*CEILING1> +POSITION' = SPEED +FORCE_MAGNITUDE = M*G-K*POSITION-B*SPEED+FORCE +FORCE_BLOCK>=EXPLICIT(FORCE_MAGNITUDE*CEILING1>) +ZERO = FR() + FRSTAR() +KANE() +INPUT TFINAL=10.0, INTEGSTP=0.01 +INPUT M=1.0, K=1.0, B=0.2, G=9.8, POSITION=0.1, SPEED=-1.0, O=2 +CODE DYNAMICS() dummy_file.c diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/mass_spring_damper.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/mass_spring_damper.py new file mode 100644 index 0000000000000000000000000000000000000000..8a5baab9642ff140e0ee81027a1e8f9152d7050c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/mass_spring_damper.py @@ -0,0 +1,31 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +m, k, b, g = _sm.symbols('m k b g', real=True) +position, speed = _me.dynamicsymbols('position speed') +position_d, speed_d = _me.dynamicsymbols('position_ speed_', 1) +o = _me.dynamicsymbols('o') +force = o*_sm.sin(_me.dynamicsymbols._t) +frame_ceiling = _me.ReferenceFrame('ceiling') +point_origin = _me.Point('origin') +point_origin.set_vel(frame_ceiling, 0) +particle_block = _me.Particle('block', _me.Point('block_pt'), _sm.Symbol('m')) +particle_block.point.set_pos(point_origin, position*frame_ceiling.x) +particle_block.mass = m +particle_block.point.set_vel(frame_ceiling, speed*frame_ceiling.x) +force_magnitude = m*g-k*position-b*speed+force +force_block = (force_magnitude*frame_ceiling.x).subs({position_d:speed}) +kd_eqs = [position_d - speed] +forceList = [(particle_block.point,(force_magnitude*frame_ceiling.x).subs({position_d:speed}))] +kane = _me.KanesMethod(frame_ceiling, q_ind=[position], u_ind=[speed], kd_eqs = kd_eqs) +fr, frstar = kane.kanes_equations([particle_block], forceList) +zero = fr+frstar +from pydy.system import System +sys = System(kane, constants = {m:1.0, k:1.0, b:0.2, g:9.8}, +specifieds={_me.dynamicsymbols('t'):lambda x, t: t, o:2}, +initial_conditions={position:0.1, speed:-1*1.0}, +times = _np.linspace(0.0, 10.0, 10.0/0.01)) + +y=sys.integrate() diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/non_min_pendulum.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/non_min_pendulum.al new file mode 100644 index 0000000000000000000000000000000000000000..74f5062d80926db7acd634a04759abce857087e5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/non_min_pendulum.al @@ -0,0 +1,20 @@ +MOTIONVARIABLES' Q{2}'' +CONSTANTS L,M,G +NEWTONIAN N +POINT PN +V_PN_N> = 0> +THETA1 = ATAN(Q2/Q1) +FRAMES A +SIMPROT(N, A, 3, THETA1) +PARTICLES P +P_PN_P> = Q1*N1>+Q2*N2> +MASS P=M +V_P_N>=DT(P_P_PN>, N) +F_V = DOT(EXPRESS(V_P_N>,A), A1>) +GRAVITY(G*N1>) +DEPENDENT[1] = F_V +CONSTRAIN(DEPENDENT[Q1']) +ZERO=FR()+FRSTAR() +F_C = MAG(P_P_PN>)-L +CONFIG[1]=F_C +ZERO[2]=CONFIG[1] diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/non_min_pendulum.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/non_min_pendulum.py new file mode 100644 index 0000000000000000000000000000000000000000..fc972ebd518e77da5e1902c149f2699979865e7f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/pydy-example-repo/non_min_pendulum.py @@ -0,0 +1,36 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +q1, q2 = _me.dynamicsymbols('q1 q2') +q1_d, q2_d = _me.dynamicsymbols('q1_ q2_', 1) +q1_dd, q2_dd = _me.dynamicsymbols('q1_ q2_', 2) +l, m, g = _sm.symbols('l m g', real=True) +frame_n = _me.ReferenceFrame('n') +point_pn = _me.Point('pn') +point_pn.set_vel(frame_n, 0) +theta1 = _sm.atan(q2/q1) +frame_a = _me.ReferenceFrame('a') +frame_a.orient(frame_n, 'Axis', [theta1, frame_n.z]) +particle_p = _me.Particle('p', _me.Point('p_pt'), _sm.Symbol('m')) +particle_p.point.set_pos(point_pn, q1*frame_n.x+q2*frame_n.y) +particle_p.mass = m +particle_p.point.set_vel(frame_n, (point_pn.pos_from(particle_p.point)).dt(frame_n)) +f_v = _me.dot((particle_p.point.vel(frame_n)).express(frame_a), frame_a.x) +force_p = particle_p.mass*(g*frame_n.x) +dependent = _sm.Matrix([[0]]) +dependent[0] = f_v +velocity_constraints = [i for i in dependent] +u_q1_d = _me.dynamicsymbols('u_q1_d') +u_q2_d = _me.dynamicsymbols('u_q2_d') +kd_eqs = [q1_d-u_q1_d, q2_d-u_q2_d] +forceList = [(particle_p.point,particle_p.mass*(g*frame_n.x))] +kane = _me.KanesMethod(frame_n, q_ind=[q1,q2], u_ind=[u_q2_d], u_dependent=[u_q1_d], kd_eqs = kd_eqs, velocity_constraints = velocity_constraints) +fr, frstar = kane.kanes_equations([particle_p], forceList) +zero = fr+frstar +f_c = point_pn.pos_from(particle_p.point).magnitude()-l +config = _sm.Matrix([[0]]) +config[0] = f_c +zero = zero.row_insert(zero.shape[0], _sm.Matrix([[0]])) +zero[zero.shape[0]-1] = config[0] diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest1.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest1.al new file mode 100644 index 0000000000000000000000000000000000000000..457e79fd646677c0decdc69f921bc05e9e0dcf51 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest1.al @@ -0,0 +1,8 @@ +% ruletest1.al +CONSTANTS F = 3, G = 9.81 +CONSTANTS A, B +CONSTANTS S, S1, S2+, S3+, S4- +CONSTANTS K{4}, L{1:3}, P{1:2,1:3} +CONSTANTS C{2,3} +E1 = A*F + S2 - G +E2 = F^2 + K3*K2*G diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest1.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest1.py new file mode 100644 index 0000000000000000000000000000000000000000..8466392ac930f13f2419c9c04eef9dcc2884e9bd --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest1.py @@ -0,0 +1,15 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +f = _sm.S(3) +g = _sm.S(9.81) +a, b = _sm.symbols('a b', real=True) +s, s1 = _sm.symbols('s s1', real=True) +s2, s3 = _sm.symbols('s2 s3', real=True, nonnegative=True) +s4 = _sm.symbols('s4', real=True, nonpositive=True) +k1, k2, k3, k4, l1, l2, l3, p11, p12, p13, p21, p22, p23 = _sm.symbols('k1 k2 k3 k4 l1 l2 l3 p11 p12 p13 p21 p22 p23', real=True) +c11, c12, c13, c21, c22, c23 = _sm.symbols('c11 c12 c13 c21 c22 c23', real=True) +e1 = a*f+s2-g +e2 = f**2+k3*k2*g diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest10.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest10.al new file mode 100644 index 0000000000000000000000000000000000000000..9d5f76f063c43bcb5e2a8d4f29619a6952abf9e5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest10.al @@ -0,0 +1,58 @@ +% ruletest10.al + +VARIABLES X,Y +COMPLEX ON +CONSTANTS A,B +E = A*(B*X+Y)^2 +M = [E;E] +EXPAND(E) +EXPAND(M) +FACTOR(E,X) +FACTOR(M,X) + +EQN[1] = A*X + B*Y +EQN[2] = 2*A*X - 3*B*Y +SOLVE(EQN, X, Y) +RHS_Y = RHS(Y) +E = (X+Y)^2 + 2*X^2 +ARRANGE(E, 2, X) + +CONSTANTS A,B,C +M = [A,B;C,0] +M2 = EVALUATE(M,A=1,B=2,C=3) +EIG(M2, EIGVALUE, EIGVEC) + +NEWTONIAN N +FRAMES A +SIMPROT(N, A, N1>, X) +DEGREES OFF +SIMPROT(N, A, N1>, PI/2) + +CONSTANTS C{3} +V> = C1*A1> + C2*A2> + C3*A3> +POINTS O, P +P_P_O> = C1*A1> +EXPRESS(V>,N) +EXPRESS(P_P_O>,N) +W_A_N> = C3*A3> +ANGVEL(A,N) + +V2PTS(N,A,O,P) +PARTICLES P{2} +V2PTS(N,A,P1,P2) +A2PTS(N,A,P1,P) + +BODIES B{2} +CONSTANT G +GRAVITY(G*N1>) + +VARIABLE Z +V> = X*A1> + Y*A3> +P_P_O> = X*A1> + Y*A2> +X = 2*Z +Y = Z +EXPLICIT(V>) +EXPLICIT(P_P_O>) + +FORCE(O/P1, X*Y*A1>) +FORCE(P2, X*Y*A1>) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest10.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest10.py new file mode 100644 index 0000000000000000000000000000000000000000..2b9674e47d5f6132c5a79a33b9d8d55a131942d6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest10.py @@ -0,0 +1,64 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +x, y = _me.dynamicsymbols('x y') +a, b = _sm.symbols('a b', real=True) +e = a*(b*x+y)**2 +m = _sm.Matrix([e,e]).reshape(2, 1) +e = e.expand() +m = _sm.Matrix([i.expand() for i in m]).reshape((m).shape[0], (m).shape[1]) +e = _sm.factor(e, x) +m = _sm.Matrix([_sm.factor(i,x) for i in m]).reshape((m).shape[0], (m).shape[1]) +eqn = _sm.Matrix([[0]]) +eqn[0] = a*x+b*y +eqn = eqn.row_insert(eqn.shape[0], _sm.Matrix([[0]])) +eqn[eqn.shape[0]-1] = 2*a*x-3*b*y +print(_sm.solve(eqn,x,y)) +rhs_y = _sm.solve(eqn,x,y)[y] +e = (x+y)**2+2*x**2 +e.collect(x) +a, b, c = _sm.symbols('a b c', real=True) +m = _sm.Matrix([a,b,c,0]).reshape(2, 2) +m2 = _sm.Matrix([i.subs({a:1,b:2,c:3}) for i in m]).reshape((m).shape[0], (m).shape[1]) +eigvalue = _sm.Matrix([i.evalf() for i in (m2).eigenvals().keys()]) +eigvec = _sm.Matrix([i[2][0].evalf() for i in (m2).eigenvects()]).reshape(m2.shape[0], m2.shape[1]) +frame_n = _me.ReferenceFrame('n') +frame_a = _me.ReferenceFrame('a') +frame_a.orient(frame_n, 'Axis', [x, frame_n.x]) +frame_a.orient(frame_n, 'Axis', [_sm.pi/2, frame_n.x]) +c1, c2, c3 = _sm.symbols('c1 c2 c3', real=True) +v = c1*frame_a.x+c2*frame_a.y+c3*frame_a.z +point_o = _me.Point('o') +point_p = _me.Point('p') +point_o.set_pos(point_p, c1*frame_a.x) +v = (v).express(frame_n) +point_o.set_pos(point_p, (point_o.pos_from(point_p)).express(frame_n)) +frame_a.set_ang_vel(frame_n, c3*frame_a.z) +print(frame_n.ang_vel_in(frame_a)) +point_p.v2pt_theory(point_o,frame_n,frame_a) +particle_p1 = _me.Particle('p1', _me.Point('p1_pt'), _sm.Symbol('m')) +particle_p2 = _me.Particle('p2', _me.Point('p2_pt'), _sm.Symbol('m')) +particle_p2.point.v2pt_theory(particle_p1.point,frame_n,frame_a) +point_p.a2pt_theory(particle_p1.point,frame_n,frame_a) +body_b1_cm = _me.Point('b1_cm') +body_b1_cm.set_vel(frame_n, 0) +body_b1_f = _me.ReferenceFrame('b1_f') +body_b1 = _me.RigidBody('b1', body_b1_cm, body_b1_f, _sm.symbols('m'), (_me.outer(body_b1_f.x,body_b1_f.x),body_b1_cm)) +body_b2_cm = _me.Point('b2_cm') +body_b2_cm.set_vel(frame_n, 0) +body_b2_f = _me.ReferenceFrame('b2_f') +body_b2 = _me.RigidBody('b2', body_b2_cm, body_b2_f, _sm.symbols('m'), (_me.outer(body_b2_f.x,body_b2_f.x),body_b2_cm)) +g = _sm.symbols('g', real=True) +force_p1 = particle_p1.mass*(g*frame_n.x) +force_p2 = particle_p2.mass*(g*frame_n.x) +force_b1 = body_b1.mass*(g*frame_n.x) +force_b2 = body_b2.mass*(g*frame_n.x) +z = _me.dynamicsymbols('z') +v = x*frame_a.x+y*frame_a.z +point_o.set_pos(point_p, x*frame_a.x+y*frame_a.y) +v = (v).subs({x:2*z, y:z}) +point_o.set_pos(point_p, (point_o.pos_from(point_p)).subs({x:2*z, y:z})) +force_o = -1*(x*y*frame_a.x) +force_p1 = particle_p1.mass*(g*frame_n.x)+ x*y*frame_a.x diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest11.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest11.al new file mode 100644 index 0000000000000000000000000000000000000000..60934c1ca563024828110bfe984a90d5686b89e4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest11.al @@ -0,0 +1,6 @@ +VARIABLES X, Y +CONSTANTS A{1:2, 1:2}, B{1:2} +EQN[1] = A11*x + A12*y - B1 +EQN[2] = A21*x + A22*y - B2 +INPUT A11=2, A12=5, A21=3, A22=4, B1=7, B2=6 +CODE ALGEBRAIC(EQN, X, Y) some_filename.c diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest11.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest11.py new file mode 100644 index 0000000000000000000000000000000000000000..4ec2397ea96261d7b582d1f699e3897caae88f20 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest11.py @@ -0,0 +1,14 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +x, y = _me.dynamicsymbols('x y') +a11, a12, a21, a22, b1, b2 = _sm.symbols('a11 a12 a21 a22 b1 b2', real=True) +eqn = _sm.Matrix([[0]]) +eqn[0] = a11*x+a12*y-b1 +eqn = eqn.row_insert(eqn.shape[0], _sm.Matrix([[0]])) +eqn[eqn.shape[0]-1] = a21*x+a22*y-b2 +eqn_list = [] +for i in eqn: eqn_list.append(i.subs({a11:2, a12:5, a21:3, a22:4, b1:7, b2:6})) +print(_sm.linsolve(eqn_list, x,y)) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest12.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest12.al new file mode 100644 index 0000000000000000000000000000000000000000..f147f55afd1438436767960e0487d5d9e7161c8f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest12.al @@ -0,0 +1,7 @@ +VARIABLES X,Y +CONSTANTS A,B,R +EQN[1] = A*X^3+B*Y^2-R +EQN[2] = A*SIN(X)^2 + B*COS(2*Y) - R^2 +INPUT A=2.0, B=3.0, R=1.0 +INPUT X = 30 DEG, Y = 3.14 +CODE NONLINEAR(EQN,X,Y) some_filename.c diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest12.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest12.py new file mode 100644 index 0000000000000000000000000000000000000000..3d7d996fa649f796a536dba20c1a36554acd8046 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest12.py @@ -0,0 +1,14 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +x, y = _me.dynamicsymbols('x y') +a, b, r = _sm.symbols('a b r', real=True) +eqn = _sm.Matrix([[0]]) +eqn[0] = a*x**3+b*y**2-r +eqn = eqn.row_insert(eqn.shape[0], _sm.Matrix([[0]])) +eqn[eqn.shape[0]-1] = a*_sm.sin(x)**2+b*_sm.cos(2*y)-r**2 +matrix_list = [] +for i in eqn:matrix_list.append(i.subs({a:2.0, b:3.0, r:1.0})) +print(_sm.nsolve(matrix_list,(x,y),(_np.deg2rad(30),3.14))) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest2.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest2.al new file mode 100644 index 0000000000000000000000000000000000000000..17937e58bd20a9fb82f44ccd05f0c081a1aa6c9b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest2.al @@ -0,0 +1,12 @@ +% ruletest2.al +VARIABLES X1,X2 +SPECIFIED F1 = X1*X2 + 3*X1^2 +SPECIFIED F2=X1*T+X2*T^2 +VARIABLE X', Y'' +MOTIONVARIABLES Q{3}, U{2} +VARIABLES P{2}' +VARIABLE W{3}', R{2}'' +VARIABLES C{1:2, 1:2} +VARIABLES D{1,3} +VARIABLES J{1:2} +IMAGINARY N diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest2.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest2.py new file mode 100644 index 0000000000000000000000000000000000000000..31c1d9974c2292466b805b91f8254bffaa94e2ac --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest2.py @@ -0,0 +1,22 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +x1, x2 = _me.dynamicsymbols('x1 x2') +f1 = x1*x2+3*x1**2 +f2 = x1*_me.dynamicsymbols._t+x2*_me.dynamicsymbols._t**2 +x, y = _me.dynamicsymbols('x y') +x_d, y_d = _me.dynamicsymbols('x_ y_', 1) +y_dd = _me.dynamicsymbols('y_', 2) +q1, q2, q3, u1, u2 = _me.dynamicsymbols('q1 q2 q3 u1 u2') +p1, p2 = _me.dynamicsymbols('p1 p2') +p1_d, p2_d = _me.dynamicsymbols('p1_ p2_', 1) +w1, w2, w3, r1, r2 = _me.dynamicsymbols('w1 w2 w3 r1 r2') +w1_d, w2_d, w3_d, r1_d, r2_d = _me.dynamicsymbols('w1_ w2_ w3_ r1_ r2_', 1) +r1_dd, r2_dd = _me.dynamicsymbols('r1_ r2_', 2) +c11, c12, c21, c22 = _me.dynamicsymbols('c11 c12 c21 c22') +d11, d12, d13 = _me.dynamicsymbols('d11 d12 d13') +j1, j2 = _me.dynamicsymbols('j1 j2') +n = _sm.symbols('n') +n = _sm.I diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest3.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest3.al new file mode 100644 index 0000000000000000000000000000000000000000..f263f1802ebca2725481dd5fdd3540bf8e9f11bf --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest3.al @@ -0,0 +1,25 @@ +% ruletest3.al +FRAMES A, B +NEWTONIAN N + +VARIABLES X{3} +CONSTANTS L + +V1> = X1*A1> + X2*A2> + X3*A3> +V2> = X1*B1> + X2*B2> + X3*B3> +V3> = X1*N1> + X2*N2> + X3*N3> + +V> = V1> + V2> + V3> + +POINTS C, D +POINTS PO{3} + +PARTICLES L +PARTICLES P{3} + +BODIES S +BODIES R{2} + +V4> = X1*S1> + X2*S2> + X3*S3> + +P_C_SO> = L*N1> diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest3.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest3.py new file mode 100644 index 0000000000000000000000000000000000000000..23f79aa571337f200b3ff4d56b5747f7704985c0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest3.py @@ -0,0 +1,37 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +frame_a = _me.ReferenceFrame('a') +frame_b = _me.ReferenceFrame('b') +frame_n = _me.ReferenceFrame('n') +x1, x2, x3 = _me.dynamicsymbols('x1 x2 x3') +l = _sm.symbols('l', real=True) +v1 = x1*frame_a.x+x2*frame_a.y+x3*frame_a.z +v2 = x1*frame_b.x+x2*frame_b.y+x3*frame_b.z +v3 = x1*frame_n.x+x2*frame_n.y+x3*frame_n.z +v = v1+v2+v3 +point_c = _me.Point('c') +point_d = _me.Point('d') +point_po1 = _me.Point('po1') +point_po2 = _me.Point('po2') +point_po3 = _me.Point('po3') +particle_l = _me.Particle('l', _me.Point('l_pt'), _sm.Symbol('m')) +particle_p1 = _me.Particle('p1', _me.Point('p1_pt'), _sm.Symbol('m')) +particle_p2 = _me.Particle('p2', _me.Point('p2_pt'), _sm.Symbol('m')) +particle_p3 = _me.Particle('p3', _me.Point('p3_pt'), _sm.Symbol('m')) +body_s_cm = _me.Point('s_cm') +body_s_cm.set_vel(frame_n, 0) +body_s_f = _me.ReferenceFrame('s_f') +body_s = _me.RigidBody('s', body_s_cm, body_s_f, _sm.symbols('m'), (_me.outer(body_s_f.x,body_s_f.x),body_s_cm)) +body_r1_cm = _me.Point('r1_cm') +body_r1_cm.set_vel(frame_n, 0) +body_r1_f = _me.ReferenceFrame('r1_f') +body_r1 = _me.RigidBody('r1', body_r1_cm, body_r1_f, _sm.symbols('m'), (_me.outer(body_r1_f.x,body_r1_f.x),body_r1_cm)) +body_r2_cm = _me.Point('r2_cm') +body_r2_cm.set_vel(frame_n, 0) +body_r2_f = _me.ReferenceFrame('r2_f') +body_r2 = _me.RigidBody('r2', body_r2_cm, body_r2_f, _sm.symbols('m'), (_me.outer(body_r2_f.x,body_r2_f.x),body_r2_cm)) +v4 = x1*body_s_f.x+x2*body_s_f.y+x3*body_s_f.z +body_s_cm.set_pos(point_c, l*frame_n.x) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest4.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest4.al new file mode 100644 index 0000000000000000000000000000000000000000..7302bd7724bad9b763c75fe4230faa42b5070408 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest4.al @@ -0,0 +1,20 @@ +% ruletest4.al + +FRAMES A, B +MOTIONVARIABLES Q{3} +SIMPROT(A, B, 1, Q3) +DCM = A_B +M = DCM*3 - A_B + +VARIABLES R +CIRCLE_AREA = PI*R^2 + +VARIABLES U, A +VARIABLES X, Y +S = U*T - 1/2*A*T^2 + +EXPR1 = 2*A*0.5 - 1.25 + 0.25 +EXPR2 = -X^2 + Y^2 + 0.25*(X+Y)^2 +EXPR3 = 0.5E-10 + +DYADIC>> = A1>*A1> + A2>*A2> + A3>*A3> diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest4.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest4.py new file mode 100644 index 0000000000000000000000000000000000000000..74b18543e04d6c9e42dd569d2152040c13ae0899 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest4.py @@ -0,0 +1,20 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +frame_a = _me.ReferenceFrame('a') +frame_b = _me.ReferenceFrame('b') +q1, q2, q3 = _me.dynamicsymbols('q1 q2 q3') +frame_b.orient(frame_a, 'Axis', [q3, frame_a.x]) +dcm = frame_a.dcm(frame_b) +m = dcm*3-frame_a.dcm(frame_b) +r = _me.dynamicsymbols('r') +circle_area = _sm.pi*r**2 +u, a = _me.dynamicsymbols('u a') +x, y = _me.dynamicsymbols('x y') +s = u*_me.dynamicsymbols._t-1/2*a*_me.dynamicsymbols._t**2 +expr1 = 2*a*0.5-1.25+0.25 +expr2 = -1*x**2+y**2+0.25*(x+y)**2 +expr3 = 0.5*10**(-10) +dyadic = _me.outer(frame_a.x, frame_a.x)+_me.outer(frame_a.y, frame_a.y)+_me.outer(frame_a.z, frame_a.z) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest5.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest5.al new file mode 100644 index 0000000000000000000000000000000000000000..a859dc8bb1f0251af14809681d995c59b31377ba --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest5.al @@ -0,0 +1,32 @@ +% ruletest5.al +VARIABLES X', Y' + +E1 = (X+Y)^2 + (X-Y)^3 +E2 = (X-Y)^2 +E3 = X^2 + Y^2 + 2*X*Y + +M1 = [E1;E2] +M2 = [(X+Y)^2,(X-Y)^2] +M3 = M1 + [X;Y] + +AM = EXPAND(M1) +CM = EXPAND([(X+Y)^2,(X-Y)^2]) +EM = EXPAND(M1 + [X;Y]) +F = EXPAND(E1) +G = EXPAND(E2) + +A = FACTOR(E3, X) +BM = FACTOR(M1, X) +CM = FACTOR(M1 + [X;Y], X) + +A = D(E3, X) +B = D(E3, Y) +CM = D(M2, X) +DM = D(M1 + [X;Y], X) +FRAMES A, B +A_B = [1,0,0;1,0,0;1,0,0] +V1> = X*A1> + Y*A2> + X*Y*A3> +E> = D(V1>, X, B) +FM = DT(M1) +GM = DT([(X+Y)^2,(X-Y)^2]) +H> = DT(V1>, B) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest5.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest5.py new file mode 100644 index 0000000000000000000000000000000000000000..93684435b402f5b56e2f4a5c3c81500208556423 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest5.py @@ -0,0 +1,33 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +x, y = _me.dynamicsymbols('x y') +x_d, y_d = _me.dynamicsymbols('x_ y_', 1) +e1 = (x+y)**2+(x-y)**3 +e2 = (x-y)**2 +e3 = x**2+y**2+2*x*y +m1 = _sm.Matrix([e1,e2]).reshape(2, 1) +m2 = _sm.Matrix([(x+y)**2,(x-y)**2]).reshape(1, 2) +m3 = m1+_sm.Matrix([x,y]).reshape(2, 1) +am = _sm.Matrix([i.expand() for i in m1]).reshape((m1).shape[0], (m1).shape[1]) +cm = _sm.Matrix([i.expand() for i in _sm.Matrix([(x+y)**2,(x-y)**2]).reshape(1, 2)]).reshape((_sm.Matrix([(x+y)**2,(x-y)**2]).reshape(1, 2)).shape[0], (_sm.Matrix([(x+y)**2,(x-y)**2]).reshape(1, 2)).shape[1]) +em = _sm.Matrix([i.expand() for i in m1+_sm.Matrix([x,y]).reshape(2, 1)]).reshape((m1+_sm.Matrix([x,y]).reshape(2, 1)).shape[0], (m1+_sm.Matrix([x,y]).reshape(2, 1)).shape[1]) +f = (e1).expand() +g = (e2).expand() +a = _sm.factor((e3), x) +bm = _sm.Matrix([_sm.factor(i, x) for i in m1]).reshape((m1).shape[0], (m1).shape[1]) +cm = _sm.Matrix([_sm.factor(i, x) for i in m1+_sm.Matrix([x,y]).reshape(2, 1)]).reshape((m1+_sm.Matrix([x,y]).reshape(2, 1)).shape[0], (m1+_sm.Matrix([x,y]).reshape(2, 1)).shape[1]) +a = (e3).diff(x) +b = (e3).diff(y) +cm = _sm.Matrix([i.diff(x) for i in m2]).reshape((m2).shape[0], (m2).shape[1]) +dm = _sm.Matrix([i.diff(x) for i in m1+_sm.Matrix([x,y]).reshape(2, 1)]).reshape((m1+_sm.Matrix([x,y]).reshape(2, 1)).shape[0], (m1+_sm.Matrix([x,y]).reshape(2, 1)).shape[1]) +frame_a = _me.ReferenceFrame('a') +frame_b = _me.ReferenceFrame('b') +frame_b.orient(frame_a, 'DCM', _sm.Matrix([1,0,0,1,0,0,1,0,0]).reshape(3, 3)) +v1 = x*frame_a.x+y*frame_a.y+x*y*frame_a.z +e = (v1).diff(x, frame_b) +fm = _sm.Matrix([i.diff(_sm.Symbol('t')) for i in m1]).reshape((m1).shape[0], (m1).shape[1]) +gm = _sm.Matrix([i.diff(_sm.Symbol('t')) for i in _sm.Matrix([(x+y)**2,(x-y)**2]).reshape(1, 2)]).reshape((_sm.Matrix([(x+y)**2,(x-y)**2]).reshape(1, 2)).shape[0], (_sm.Matrix([(x+y)**2,(x-y)**2]).reshape(1, 2)).shape[1]) +h = (v1).dt(frame_b) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest6.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest6.al new file mode 100644 index 0000000000000000000000000000000000000000..7ec3ba61590e77772ae631237df048b932fe778c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest6.al @@ -0,0 +1,41 @@ +% ruletest6.al +VARIABLES Q{2} +VARIABLES X,Y,Z +Q1 = X^2 + Y^2 +Q2 = X-Y +E = Q1 + Q2 +A = EXPLICIT(E) +E2 = COS(X) +E3 = COS(X*Y) +A = TAYLOR(E2, 0:2, X=0) +B = TAYLOR(E3, 0:2, X=0, Y=0) + +E = EXPAND((X+Y)^2) +A = EVALUATE(E, X=1, Y=Z) +BM = EVALUATE([E;2*E], X=1, Y=Z) + +E = Q1 + Q2 +A = EVALUATE(E, X=2, Y=Z^2) + +CONSTANTS J,K,L +P1 = POLYNOMIAL([J,K,L],X) +P2 = POLYNOMIAL(J*X+K,X,1) + +ROOT1 = ROOTS(P1, X, 2) +ROOT2 = ROOTS([1;2;3]) + +M = [1,2,3,4;5,6,7,8;9,10,11,12;13,14,15,16] + +AM = TRANSPOSE(M) + M +BM = EIG(M) +C1 = DIAGMAT(4, 1) +C2 = DIAGMAT(3, 4, 2) +DM = INV(M+C1) +E = DET(M+C1) + TRACE([1,0;0,1]) +F = ELEMENT(M, 2, 3) + +A = COLS(M) +BM = COLS(M, 1) +CM = COLS(M, 1, 2:4, 3) +DM = ROWS(M, 1) +EM = ROWS(M, 1, 2:4, 3) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest6.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest6.py new file mode 100644 index 0000000000000000000000000000000000000000..85f1a0b49518bb0ae5766cbe91b9c24a1b8e9c20 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest6.py @@ -0,0 +1,36 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +q1, q2 = _me.dynamicsymbols('q1 q2') +x, y, z = _me.dynamicsymbols('x y z') +e = q1+q2 +a = (e).subs({q1:x**2+y**2, q2:x-y}) +e2 = _sm.cos(x) +e3 = _sm.cos(x*y) +a = (e2).series(x, 0, 2).removeO() +b = (e3).series(x, 0, 2).removeO().series(y, 0, 2).removeO() +e = ((x+y)**2).expand() +a = (e).subs({q1:x**2+y**2,q2:x-y}).subs({x:1,y:z}) +bm = _sm.Matrix([i.subs({x:1,y:z}) for i in _sm.Matrix([e,2*e]).reshape(2, 1)]).reshape((_sm.Matrix([e,2*e]).reshape(2, 1)).shape[0], (_sm.Matrix([e,2*e]).reshape(2, 1)).shape[1]) +e = q1+q2 +a = (e).subs({q1:x**2+y**2,q2:x-y}).subs({x:2,y:z**2}) +j, k, l = _sm.symbols('j k l', real=True) +p1 = _sm.Poly(_sm.Matrix([j,k,l]).reshape(1, 3), x) +p2 = _sm.Poly(j*x+k, x) +root1 = [i.evalf() for i in _sm.solve(p1, x)] +root2 = [i.evalf() for i in _sm.solve(_sm.Poly(_sm.Matrix([1,2,3]).reshape(3, 1), x),x)] +m = _sm.Matrix([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]).reshape(4, 4) +am = (m).T+m +bm = _sm.Matrix([i.evalf() for i in (m).eigenvals().keys()]) +c1 = _sm.diag(1,1,1,1) +c2 = _sm.Matrix([2 if i==j else 0 for i in range(3) for j in range(4)]).reshape(3, 4) +dm = (m+c1)**(-1) +e = (m+c1).det()+(_sm.Matrix([1,0,0,1]).reshape(2, 2)).trace() +f = (m)[1,2] +a = (m).cols +bm = (m).col(0) +cm = _sm.Matrix([(m).T.row(0),(m).T.row(1),(m).T.row(2),(m).T.row(3),(m).T.row(2)]) +dm = (m).row(0) +em = _sm.Matrix([(m).row(0),(m).row(1),(m).row(2),(m).row(3),(m).row(2)]) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest7.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest7.al new file mode 100644 index 0000000000000000000000000000000000000000..2904a602f589645d22e1d3d378d077dd6a1ec27e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest7.al @@ -0,0 +1,39 @@ +% ruletest7.al +VARIABLES X', Y' +E = COS(X) + SIN(X) + TAN(X)& ++ COSH(X) + SINH(X) + TANH(X)& ++ ACOS(X) + ASIN(X) + ATAN(X)& ++ LOG(X) + EXP(X) + SQRT(X)& ++ FACTORIAL(X) + CEIL(X) +& +FLOOR(X) + SIGN(X) + +E = SQR(X) + LOG10(X) + +A = ABS(-1) + INT(1.5) + ROUND(1.9) + +E1 = 2*X + 3*Y +E2 = X + Y + +AM = COEF([E1;E2], [X,Y]) +B = COEF(E1, X) +C = COEF(E2, Y) +D1 = EXCLUDE(E1, X) +D2 = INCLUDE(E1, X) +FM = ARRANGE([E1,E2],2,X) +F = ARRANGE(E1, 2, Y) +G = REPLACE(E1, X=2*X) +GM = REPLACE([E1;E2], X=3) + +FRAMES A, B +VARIABLES THETA +SIMPROT(A,B,3,THETA) +V1> = 2*A1> - 3*A2> + A3> +V2> = B1> + B2> + B3> +A = DOT(V1>, V2>) +BM = DOT(V1>, [V2>;2*V2>]) +C> = CROSS(V1>,V2>) +D = MAG(2*V1>) + MAG(3*V1>) +DYADIC>> = 3*A1>*A1> + A2>*A2> + 2*A3>*A3> +AM = MATRIX(B, DYADIC>>) +M = [1;2;3] +V> = VECTOR(A, M) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest7.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest7.py new file mode 100644 index 0000000000000000000000000000000000000000..19147856dc3b0d451184a6bb539c1c331f61a6d2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest7.py @@ -0,0 +1,35 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +x, y = _me.dynamicsymbols('x y') +x_d, y_d = _me.dynamicsymbols('x_ y_', 1) +e = _sm.cos(x)+_sm.sin(x)+_sm.tan(x)+_sm.cosh(x)+_sm.sinh(x)+_sm.tanh(x)+_sm.acos(x)+_sm.asin(x)+_sm.atan(x)+_sm.log(x)+_sm.exp(x)+_sm.sqrt(x)+_sm.factorial(x)+_sm.ceiling(x)+_sm.floor(x)+_sm.sign(x) +e = (x)**2+_sm.log(x, 10) +a = _sm.Abs(-1*1)+int(1.5)+round(1.9) +e1 = 2*x+3*y +e2 = x+y +am = _sm.Matrix([e1.expand().coeff(x), e1.expand().coeff(y), e2.expand().coeff(x), e2.expand().coeff(y)]).reshape(2, 2) +b = (e1).expand().coeff(x) +c = (e2).expand().coeff(y) +d1 = (e1).collect(x).coeff(x,0) +d2 = (e1).collect(x).coeff(x,1) +fm = _sm.Matrix([i.collect(x)for i in _sm.Matrix([e1,e2]).reshape(1, 2)]).reshape((_sm.Matrix([e1,e2]).reshape(1, 2)).shape[0], (_sm.Matrix([e1,e2]).reshape(1, 2)).shape[1]) +f = (e1).collect(y) +g = (e1).subs({x:2*x}) +gm = _sm.Matrix([i.subs({x:3}) for i in _sm.Matrix([e1,e2]).reshape(2, 1)]).reshape((_sm.Matrix([e1,e2]).reshape(2, 1)).shape[0], (_sm.Matrix([e1,e2]).reshape(2, 1)).shape[1]) +frame_a = _me.ReferenceFrame('a') +frame_b = _me.ReferenceFrame('b') +theta = _me.dynamicsymbols('theta') +frame_b.orient(frame_a, 'Axis', [theta, frame_a.z]) +v1 = 2*frame_a.x-3*frame_a.y+frame_a.z +v2 = frame_b.x+frame_b.y+frame_b.z +a = _me.dot(v1, v2) +bm = _sm.Matrix([_me.dot(v1, v2),_me.dot(v1, 2*v2)]).reshape(2, 1) +c = _me.cross(v1, v2) +d = 2*v1.magnitude()+3*v1.magnitude() +dyadic = _me.outer(3*frame_a.x, frame_a.x)+_me.outer(frame_a.y, frame_a.y)+_me.outer(2*frame_a.z, frame_a.z) +am = (dyadic).to_matrix(frame_b) +m = _sm.Matrix([1,2,3]).reshape(3, 1) +v = m[0]*frame_a.x +m[1]*frame_a.y +m[2]*frame_a.z diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest8.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest8.al new file mode 100644 index 0000000000000000000000000000000000000000..4b2462c51e6730f46bf60b4b21ab6cfbf1993640 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest8.al @@ -0,0 +1,38 @@ +% ruletest8.al +FRAMES A +CONSTANTS C{3} +A>> = EXPRESS(1>>,A) +PARTICLES P1, P2 +BODIES R +R_A = [1,1,1;1,1,0;0,0,1] +POINT O +MASS P1=M1, P2=M2, R=MR +INERTIA R, I1, I2, I3 +P_P1_O> = C1*A1> +P_P2_O> = C2*A2> +P_RO_O> = C3*A3> +A>> = EXPRESS(I_P1_O>>, A) +A>> = EXPRESS(I_P2_O>>, A) +A>> = EXPRESS(I_R_O>>, A) +A>> = EXPRESS(INERTIA(O), A) +A>> = EXPRESS(INERTIA(O, P1, R), A) +A>> = EXPRESS(I_R_O>>, A) +A>> = EXPRESS(I_R_RO>>, A) + +P_P1_P2> = C1*A1> + C2*A2> +P_P1_RO> = C3*A1> +P_P2_RO> = C3*A2> + +B> = CM(O) +B> = CM(O, P1, R) +B> = CM(P1) + +MOTIONVARIABLES U{3} +V> = U1*A1> + U2*A2> + U3*A3> +U> = UNITVEC(V> + C1*A1>) +V_P1_A> = U1*A1> +A> = PARTIALS(V_P1_A>, U1) + +M = MASS(P1,R) +M = MASS(P2) +M = MASS() \ No newline at end of file diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest8.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest8.py new file mode 100644 index 0000000000000000000000000000000000000000..6809c47138e40027c700536e807ca7cfa5f468d7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest8.py @@ -0,0 +1,49 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +frame_a = _me.ReferenceFrame('a') +c1, c2, c3 = _sm.symbols('c1 c2 c3', real=True) +a = _me.inertia(frame_a, 1, 1, 1) +particle_p1 = _me.Particle('p1', _me.Point('p1_pt'), _sm.Symbol('m')) +particle_p2 = _me.Particle('p2', _me.Point('p2_pt'), _sm.Symbol('m')) +body_r_cm = _me.Point('r_cm') +body_r_f = _me.ReferenceFrame('r_f') +body_r = _me.RigidBody('r', body_r_cm, body_r_f, _sm.symbols('m'), (_me.outer(body_r_f.x,body_r_f.x),body_r_cm)) +frame_a.orient(body_r_f, 'DCM', _sm.Matrix([1,1,1,1,1,0,0,0,1]).reshape(3, 3)) +point_o = _me.Point('o') +m1 = _sm.symbols('m1') +particle_p1.mass = m1 +m2 = _sm.symbols('m2') +particle_p2.mass = m2 +mr = _sm.symbols('mr') +body_r.mass = mr +i1 = _sm.symbols('i1') +i2 = _sm.symbols('i2') +i3 = _sm.symbols('i3') +body_r.inertia = (_me.inertia(body_r_f, i1, i2, i3, 0, 0, 0), body_r_cm) +point_o.set_pos(particle_p1.point, c1*frame_a.x) +point_o.set_pos(particle_p2.point, c2*frame_a.y) +point_o.set_pos(body_r_cm, c3*frame_a.z) +a = _me.inertia_of_point_mass(particle_p1.mass, particle_p1.point.pos_from(point_o), frame_a) +a = _me.inertia_of_point_mass(particle_p2.mass, particle_p2.point.pos_from(point_o), frame_a) +a = body_r.inertia[0] + _me.inertia_of_point_mass(body_r.mass, body_r.masscenter.pos_from(point_o), frame_a) +a = _me.inertia_of_point_mass(particle_p1.mass, particle_p1.point.pos_from(point_o), frame_a) + _me.inertia_of_point_mass(particle_p2.mass, particle_p2.point.pos_from(point_o), frame_a) + body_r.inertia[0] + _me.inertia_of_point_mass(body_r.mass, body_r.masscenter.pos_from(point_o), frame_a) +a = _me.inertia_of_point_mass(particle_p1.mass, particle_p1.point.pos_from(point_o), frame_a) + body_r.inertia[0] + _me.inertia_of_point_mass(body_r.mass, body_r.masscenter.pos_from(point_o), frame_a) +a = body_r.inertia[0] + _me.inertia_of_point_mass(body_r.mass, body_r.masscenter.pos_from(point_o), frame_a) +a = body_r.inertia[0] +particle_p2.point.set_pos(particle_p1.point, c1*frame_a.x+c2*frame_a.y) +body_r_cm.set_pos(particle_p1.point, c3*frame_a.x) +body_r_cm.set_pos(particle_p2.point, c3*frame_a.y) +b = _me.functions.center_of_mass(point_o,particle_p1, particle_p2, body_r) +b = _me.functions.center_of_mass(point_o,particle_p1, body_r) +b = _me.functions.center_of_mass(particle_p1.point,particle_p1, particle_p2, body_r) +u1, u2, u3 = _me.dynamicsymbols('u1 u2 u3') +v = u1*frame_a.x+u2*frame_a.y+u3*frame_a.z +u = (v+c1*frame_a.x).normalize() +particle_p1.point.set_vel(frame_a, u1*frame_a.x) +a = particle_p1.point.partial_velocity(frame_a, u1) +m = particle_p1.mass+body_r.mass +m = particle_p2.mass +m = particle_p1.mass+particle_p2.mass+body_r.mass diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest9.al b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest9.al new file mode 100644 index 0000000000000000000000000000000000000000..df5c70f05b76fc215f829672e281491b0c96c6a6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest9.al @@ -0,0 +1,54 @@ +% ruletest9.al +NEWTONIAN N +FRAMES A +A> = 0> +D>> = EXPRESS(1>>, A) + +POINTS PO{2} +PARTICLES P{2} +MOTIONVARIABLES' C{3}' +BODIES R +P_P1_PO2> = C1*A1> +V> = 2*P_P1_PO2> + C2*A2> + +W_A_N> = C3*A3> +V> = 2*W_A_N> + C2*A2> +W_R_N> = C3*A3> +V> = 2*W_R_N> + C2*A2> + +ALF_A_N> = DT(W_A_N>, A) +V> = 2*ALF_A_N> + C2*A2> + +V_P1_A> = C1*A1> + C3*A2> +A_RO_N> = C2*A2> +V_A> = CROSS(A_RO_N>, V_P1_A>) + +X_B_C> = V_A> +X_B_D> = 2*X_B_C> +A_B_C_D_E> = X_B_D>*2 + +A_B_C = 2*C1*C2*C3 +A_B_C += 2*C1 +A_B_C := 3*C1 + +MOTIONVARIABLES' Q{2}', U{2}' +Q1' = U1 +Q2' = U2 + +VARIABLES X'', Y'' +SPECIFIED YY +Y'' = X*X'^2 + 1 +YY = X*X'^2 + 1 + +M[1] = 2*X +M[2] = 2*Y +A = 2*M[1] + +M = [1,2,3;4,5,6;7,8,9] +M[1, 2] = 5 +A = M[1, 2]*2 + +FORCE_RO> = Q1*N1> +TORQUE_A> = Q2*N3> +FORCE_RO> = Q2*N2> +F> = FORCE_RO>*2 diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest9.py b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest9.py new file mode 100644 index 0000000000000000000000000000000000000000..09d8ae4ee8385bde5c38b946458a43c8ffdaa9b8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/autolev/test-examples/ruletest9.py @@ -0,0 +1,55 @@ +import sympy.physics.mechanics as _me +import sympy as _sm +import math as m +import numpy as _np + +frame_n = _me.ReferenceFrame('n') +frame_a = _me.ReferenceFrame('a') +a = 0 +d = _me.inertia(frame_a, 1, 1, 1) +point_po1 = _me.Point('po1') +point_po2 = _me.Point('po2') +particle_p1 = _me.Particle('p1', _me.Point('p1_pt'), _sm.Symbol('m')) +particle_p2 = _me.Particle('p2', _me.Point('p2_pt'), _sm.Symbol('m')) +c1, c2, c3 = _me.dynamicsymbols('c1 c2 c3') +c1_d, c2_d, c3_d = _me.dynamicsymbols('c1_ c2_ c3_', 1) +body_r_cm = _me.Point('r_cm') +body_r_cm.set_vel(frame_n, 0) +body_r_f = _me.ReferenceFrame('r_f') +body_r = _me.RigidBody('r', body_r_cm, body_r_f, _sm.symbols('m'), (_me.outer(body_r_f.x,body_r_f.x),body_r_cm)) +point_po2.set_pos(particle_p1.point, c1*frame_a.x) +v = 2*point_po2.pos_from(particle_p1.point)+c2*frame_a.y +frame_a.set_ang_vel(frame_n, c3*frame_a.z) +v = 2*frame_a.ang_vel_in(frame_n)+c2*frame_a.y +body_r_f.set_ang_vel(frame_n, c3*frame_a.z) +v = 2*body_r_f.ang_vel_in(frame_n)+c2*frame_a.y +frame_a.set_ang_acc(frame_n, (frame_a.ang_vel_in(frame_n)).dt(frame_a)) +v = 2*frame_a.ang_acc_in(frame_n)+c2*frame_a.y +particle_p1.point.set_vel(frame_a, c1*frame_a.x+c3*frame_a.y) +body_r_cm.set_acc(frame_n, c2*frame_a.y) +v_a = _me.cross(body_r_cm.acc(frame_n), particle_p1.point.vel(frame_a)) +x_b_c = v_a +x_b_d = 2*x_b_c +a_b_c_d_e = x_b_d*2 +a_b_c = 2*c1*c2*c3 +a_b_c += 2*c1 +a_b_c = 3*c1 +q1, q2, u1, u2 = _me.dynamicsymbols('q1 q2 u1 u2') +q1_d, q2_d, u1_d, u2_d = _me.dynamicsymbols('q1_ q2_ u1_ u2_', 1) +x, y = _me.dynamicsymbols('x y') +x_d, y_d = _me.dynamicsymbols('x_ y_', 1) +x_dd, y_dd = _me.dynamicsymbols('x_ y_', 2) +yy = _me.dynamicsymbols('yy') +yy = x*x_d**2+1 +m = _sm.Matrix([[0]]) +m[0] = 2*x +m = m.row_insert(m.shape[0], _sm.Matrix([[0]])) +m[m.shape[0]-1] = 2*y +a = 2*m[0] +m = _sm.Matrix([1,2,3,4,5,6,7,8,9]).reshape(3, 3) +m[0,1] = 5 +a = m[0, 1]*2 +force_ro = q1*frame_n.x +torque_a = q2*frame_n.z +force_ro = q1*frame_n.x + q2*frame_n.y +f = force_ro*2 diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/c/__init__.py b/venv/lib/python3.11/site-packages/sympy/parsing/c/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..18d3d5301cb001c78fc4a9bc04b25aa36f282a93 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/c/__init__.py @@ -0,0 +1 @@ +"""Used for translating C source code into a SymPy expression""" diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/c/c_parser.py b/venv/lib/python3.11/site-packages/sympy/parsing/c/c_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..9e7223f8351205272e803773589649fcf1902f15 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/c/c_parser.py @@ -0,0 +1,1059 @@ +from sympy.external import import_module +import os + +cin = import_module('clang.cindex', import_kwargs = {'fromlist': ['cindex']}) + +""" +This module contains all the necessary Classes and Function used to Parse C and +C++ code into SymPy expression +The module serves as a backend for SymPyExpression to parse C code +It is also dependent on Clang's AST and SymPy's Codegen AST. +The module only supports the features currently supported by the Clang and +codegen AST which will be updated as the development of codegen AST and this +module progresses. +You might find unexpected bugs and exceptions while using the module, feel free +to report them to the SymPy Issue Tracker + +Features Supported +================== + +- Variable Declarations (integers and reals) +- Assignment (using integer & floating literal and function calls) +- Function Definitions and Declaration +- Function Calls +- Compound statements, Return statements + +Notes +===== + +The module is dependent on an external dependency which needs to be installed +to use the features of this module. + +Clang: The C and C++ compiler which is used to extract an AST from the provided +C source code. + +References +========== + +.. [1] https://github.com/sympy/sympy/issues +.. [2] https://clang.llvm.org/docs/ +.. [3] https://clang.llvm.org/docs/IntroductionToTheClangAST.html + +""" + +if cin: + from sympy.codegen.ast import (Variable, Integer, Float, + FunctionPrototype, FunctionDefinition, FunctionCall, + none, Return, Assignment, intc, int8, int16, int64, + uint8, uint16, uint32, uint64, float32, float64, float80, + aug_assign, bool_, While, CodeBlock) + from sympy.codegen.cnodes import (PreDecrement, PostDecrement, + PreIncrement, PostIncrement) + from sympy.core import Add, Mod, Mul, Pow, Rel + from sympy.logic.boolalg import And, as_Boolean, Not, Or + from sympy.core.symbol import Symbol + from sympy.core.sympify import sympify + from sympy.logic.boolalg import (false, true) + import sys + import tempfile + + class BaseParser: + """Base Class for the C parser""" + + def __init__(self): + """Initializes the Base parser creating a Clang AST index""" + self.index = cin.Index.create() + + def diagnostics(self, out): + """Diagostics function for the Clang AST""" + for diag in self.tu.diagnostics: + # tu = translation unit + print('%s %s (line %s, col %s) %s' % ( + { + 4: 'FATAL', + 3: 'ERROR', + 2: 'WARNING', + 1: 'NOTE', + 0: 'IGNORED', + }[diag.severity], + diag.location.file, + diag.location.line, + diag.location.column, + diag.spelling + ), file=out) + + class CCodeConverter(BaseParser): + """The Code Convereter for Clang AST + + The converter object takes the C source code or file as input and + converts them to SymPy Expressions. + """ + + def __init__(self): + """Initializes the code converter""" + super().__init__() + self._py_nodes = [] + self._data_types = { + "void": { + cin.TypeKind.VOID: none + }, + "bool": { + cin.TypeKind.BOOL: bool_ + }, + "int": { + cin.TypeKind.SCHAR: int8, + cin.TypeKind.SHORT: int16, + cin.TypeKind.INT: intc, + cin.TypeKind.LONG: int64, + cin.TypeKind.UCHAR: uint8, + cin.TypeKind.USHORT: uint16, + cin.TypeKind.UINT: uint32, + cin.TypeKind.ULONG: uint64 + }, + "float": { + cin.TypeKind.FLOAT: float32, + cin.TypeKind.DOUBLE: float64, + cin.TypeKind.LONGDOUBLE: float80 + } + } + + def parse(self, filename, flags): + """Function to parse a file with C source code + + It takes the filename as an attribute and creates a Clang AST + Translation Unit parsing the file. + Then the transformation function is called on the translation unit, + whose results are collected into a list which is returned by the + function. + + Parameters + ========== + + filename : string + Path to the C file to be parsed + + flags: list + Arguments to be passed to Clang while parsing the C code + + Returns + ======= + + py_nodes: list + A list of SymPy AST nodes + + """ + filepath = os.path.abspath(filename) + self.tu = self.index.parse( + filepath, + args=flags, + options=cin.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD + ) + for child in self.tu.cursor.get_children(): + if child.kind == cin.CursorKind.VAR_DECL or child.kind == cin.CursorKind.FUNCTION_DECL: + self._py_nodes.append(self.transform(child)) + return self._py_nodes + + def parse_str(self, source, flags): + """Function to parse a string with C source code + + It takes the source code as an attribute, stores it in a temporary + file and creates a Clang AST Translation Unit parsing the file. + Then the transformation function is called on the translation unit, + whose results are collected into a list which is returned by the + function. + + Parameters + ========== + + source : string + A string containing the C source code to be parsed + + flags: list + Arguments to be passed to Clang while parsing the C code + + Returns + ======= + + py_nodes: list + A list of SymPy AST nodes + + """ + file = tempfile.NamedTemporaryFile(mode = 'w+', suffix = '.cpp') + file.write(source) + file.flush() + file.seek(0) + self.tu = self.index.parse( + file.name, + args=flags, + options=cin.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD + ) + file.close() + for child in self.tu.cursor.get_children(): + if child.kind == cin.CursorKind.VAR_DECL or child.kind == cin.CursorKind.FUNCTION_DECL: + self._py_nodes.append(self.transform(child)) + return self._py_nodes + + def transform(self, node): + """Transformation Function for Clang AST nodes + + It determines the kind of node and calls the respective + transformation function for that node. + + Raises + ====== + + NotImplementedError : if the transformation for the provided node + is not implemented + + """ + handler = getattr(self, 'transform_%s' % node.kind.name.lower(), None) + + if handler is None: + print( + "Ignoring node of type %s (%s)" % ( + node.kind, + ' '.join( + t.spelling for t in node.get_tokens()) + ), + file=sys.stderr + ) + + return handler(node) + + def transform_var_decl(self, node): + """Transformation Function for Variable Declaration + + Used to create nodes for variable declarations and assignments with + values or function call for the respective nodes in the clang AST + + Returns + ======= + + A variable node as Declaration, with the initial value if given + + Raises + ====== + + NotImplementedError : if called for data types not currently + implemented + + Notes + ===== + + The function currently supports following data types: + + Boolean: + bool, _Bool + + Integer: + 8-bit: signed char and unsigned char + 16-bit: short, short int, signed short, + signed short int, unsigned short, unsigned short int + 32-bit: int, signed int, unsigned int + 64-bit: long, long int, signed long, + signed long int, unsigned long, unsigned long int + + Floating point: + Single Precision: float + Double Precision: double + Extended Precision: long double + + """ + if node.type.kind in self._data_types["int"]: + type = self._data_types["int"][node.type.kind] + elif node.type.kind in self._data_types["float"]: + type = self._data_types["float"][node.type.kind] + elif node.type.kind in self._data_types["bool"]: + type = self._data_types["bool"][node.type.kind] + else: + raise NotImplementedError("Only bool, int " + "and float are supported") + try: + children = node.get_children() + child = next(children) + + #ignoring namespace and type details for the variable + while child.kind == cin.CursorKind.NAMESPACE_REF or child.kind == cin.CursorKind.TYPE_REF: + child = next(children) + + val = self.transform(child) + + supported_rhs = [ + cin.CursorKind.INTEGER_LITERAL, + cin.CursorKind.FLOATING_LITERAL, + cin.CursorKind.UNEXPOSED_EXPR, + cin.CursorKind.BINARY_OPERATOR, + cin.CursorKind.PAREN_EXPR, + cin.CursorKind.UNARY_OPERATOR, + cin.CursorKind.CXX_BOOL_LITERAL_EXPR + ] + + if child.kind in supported_rhs: + if isinstance(val, str): + value = Symbol(val) + elif isinstance(val, bool): + if node.type.kind in self._data_types["int"]: + value = Integer(0) if val == False else Integer(1) + elif node.type.kind in self._data_types["float"]: + value = Float(0.0) if val == False else Float(1.0) + elif node.type.kind in self._data_types["bool"]: + value = sympify(val) + elif isinstance(val, (Integer, int, Float, float)): + if node.type.kind in self._data_types["int"]: + value = Integer(val) + elif node.type.kind in self._data_types["float"]: + value = Float(val) + elif node.type.kind in self._data_types["bool"]: + value = sympify(bool(val)) + else: + value = val + + return Variable( + node.spelling + ).as_Declaration( + type = type, + value = value + ) + + elif child.kind == cin.CursorKind.CALL_EXPR: + return Variable( + node.spelling + ).as_Declaration( + value = val + ) + + else: + raise NotImplementedError("Given " + "variable declaration \"{}\" " + "is not possible to parse yet!" + .format(" ".join( + t.spelling for t in node.get_tokens() + ) + )) + + except StopIteration: + return Variable( + node.spelling + ).as_Declaration( + type = type + ) + + def transform_function_decl(self, node): + """Transformation Function For Function Declaration + + Used to create nodes for function declarations and definitions for + the respective nodes in the clang AST + + Returns + ======= + + function : Codegen AST node + - FunctionPrototype node if function body is not present + - FunctionDefinition node if the function body is present + + + """ + + if node.result_type.kind in self._data_types["int"]: + ret_type = self._data_types["int"][node.result_type.kind] + elif node.result_type.kind in self._data_types["float"]: + ret_type = self._data_types["float"][node.result_type.kind] + elif node.result_type.kind in self._data_types["bool"]: + ret_type = self._data_types["bool"][node.result_type.kind] + elif node.result_type.kind in self._data_types["void"]: + ret_type = self._data_types["void"][node.result_type.kind] + else: + raise NotImplementedError("Only void, bool, int " + "and float are supported") + body = [] + param = [] + + # Subsequent nodes will be the parameters for the function. + for child in node.get_children(): + decl = self.transform(child) + if child.kind == cin.CursorKind.PARM_DECL: + param.append(decl) + elif child.kind == cin.CursorKind.COMPOUND_STMT: + for val in decl: + body.append(val) + else: + body.append(decl) + + if body == []: + function = FunctionPrototype( + return_type = ret_type, + name = node.spelling, + parameters = param + ) + else: + function = FunctionDefinition( + return_type = ret_type, + name = node.spelling, + parameters = param, + body = body + ) + return function + + def transform_parm_decl(self, node): + """Transformation function for Parameter Declaration + + Used to create parameter nodes for the required functions for the + respective nodes in the clang AST + + Returns + ======= + + param : Codegen AST Node + Variable node with the value and type of the variable + + Raises + ====== + + ValueError if multiple children encountered in the parameter node + + """ + if node.type.kind in self._data_types["int"]: + type = self._data_types["int"][node.type.kind] + elif node.type.kind in self._data_types["float"]: + type = self._data_types["float"][node.type.kind] + elif node.type.kind in self._data_types["bool"]: + type = self._data_types["bool"][node.type.kind] + else: + raise NotImplementedError("Only bool, int " + "and float are supported") + try: + children = node.get_children() + child = next(children) + + # Any namespace nodes can be ignored + while child.kind in [cin.CursorKind.NAMESPACE_REF, + cin.CursorKind.TYPE_REF, + cin.CursorKind.TEMPLATE_REF]: + child = next(children) + + # If there is a child, it is the default value of the parameter. + lit = self.transform(child) + if node.type.kind in self._data_types["int"]: + val = Integer(lit) + elif node.type.kind in self._data_types["float"]: + val = Float(lit) + elif node.type.kind in self._data_types["bool"]: + val = sympify(bool(lit)) + else: + raise NotImplementedError("Only bool, int " + "and float are supported") + + param = Variable( + node.spelling + ).as_Declaration( + type = type, + value = val + ) + except StopIteration: + param = Variable( + node.spelling + ).as_Declaration( + type = type + ) + + try: + self.transform(next(children)) + raise ValueError("Can't handle multiple children on parameter") + except StopIteration: + pass + + return param + + def transform_integer_literal(self, node): + """Transformation function for integer literal + + Used to get the value and type of the given integer literal. + + Returns + ======= + + val : list + List with two arguments type and Value + type contains the type of the integer + value contains the value stored in the variable + + Notes + ===== + + Only Base Integer type supported for now + + """ + try: + value = next(node.get_tokens()).spelling + except StopIteration: + # No tokens + value = node.literal + return int(value) + + def transform_floating_literal(self, node): + """Transformation function for floating literal + + Used to get the value and type of the given floating literal. + + Returns + ======= + + val : list + List with two arguments type and Value + type contains the type of float + value contains the value stored in the variable + + Notes + ===== + + Only Base Float type supported for now + + """ + try: + value = next(node.get_tokens()).spelling + except (StopIteration, ValueError): + # No tokens + value = node.literal + return float(value) + + def transform_string_literal(self, node): + #TODO: No string type in AST + #type = + #try: + # value = next(node.get_tokens()).spelling + #except (StopIteration, ValueError): + # No tokens + # value = node.literal + #val = [type, value] + #return val + pass + + def transform_character_literal(self, node): + """Transformation function for character literal + + Used to get the value of the given character literal. + + Returns + ======= + + val : int + val contains the ascii value of the character literal + + Notes + ===== + + Only for cases where character is assigned to a integer value, + since character literal is not in SymPy AST + + """ + try: + value = next(node.get_tokens()).spelling + except (StopIteration, ValueError): + # No tokens + value = node.literal + return ord(str(value[1])) + + def transform_cxx_bool_literal_expr(self, node): + """Transformation function for boolean literal + + Used to get the value of the given boolean literal. + + Returns + ======= + + value : bool + value contains the boolean value of the variable + + """ + try: + value = next(node.get_tokens()).spelling + except (StopIteration, ValueError): + value = node.literal + return True if value == 'true' else False + + def transform_unexposed_decl(self,node): + """Transformation function for unexposed declarations""" + pass + + def transform_unexposed_expr(self, node): + """Transformation function for unexposed expression + + Unexposed expressions are used to wrap float, double literals and + expressions + + Returns + ======= + + expr : Codegen AST Node + the result from the wrapped expression + + None : NoneType + No children are found for the node + + Raises + ====== + + ValueError if the expression contains multiple children + + """ + # Ignore unexposed nodes; pass whatever is the first + # (and should be only) child unaltered. + try: + children = node.get_children() + expr = self.transform(next(children)) + except StopIteration: + return None + + try: + next(children) + raise ValueError("Unexposed expression has > 1 children.") + except StopIteration: + pass + + return expr + + def transform_decl_ref_expr(self, node): + """Returns the name of the declaration reference""" + return node.spelling + + def transform_call_expr(self, node): + """Transformation function for a call expression + + Used to create function call nodes for the function calls present + in the C code + + Returns + ======= + + FunctionCall : Codegen AST Node + FunctionCall node with parameters if any parameters are present + + """ + param = [] + children = node.get_children() + child = next(children) + + while child.kind == cin.CursorKind.NAMESPACE_REF: + child = next(children) + while child.kind == cin.CursorKind.TYPE_REF: + child = next(children) + + first_child = self.transform(child) + try: + for child in children: + arg = self.transform(child) + if child.kind == cin.CursorKind.INTEGER_LITERAL: + param.append(Integer(arg)) + elif child.kind == cin.CursorKind.FLOATING_LITERAL: + param.append(Float(arg)) + else: + param.append(arg) + return FunctionCall(first_child, param) + + except StopIteration: + return FunctionCall(first_child) + + def transform_return_stmt(self, node): + """Returns the Return Node for a return statement""" + return Return(next(node.get_children()).spelling) + + def transform_compound_stmt(self, node): + """Transformation function for compound statements + + Returns + ======= + + expr : list + list of Nodes for the expressions present in the statement + + None : NoneType + if the compound statement is empty + + """ + expr = [] + children = node.get_children() + + for child in children: + expr.append(self.transform(child)) + return expr + + def transform_decl_stmt(self, node): + """Transformation function for declaration statements + + These statements are used to wrap different kinds of declararions + like variable or function declaration + The function calls the transformer function for the child of the + given node + + Returns + ======= + + statement : Codegen AST Node + contains the node returned by the children node for the type of + declaration + + Raises + ====== + + ValueError if multiple children present + + """ + try: + children = node.get_children() + statement = self.transform(next(children)) + except StopIteration: + pass + + try: + self.transform(next(children)) + raise ValueError("Don't know how to handle multiple statements") + except StopIteration: + pass + + return statement + + def transform_paren_expr(self, node): + """Transformation function for Parenthesized expressions + + Returns the result from its children nodes + + """ + return self.transform(next(node.get_children())) + + def transform_compound_assignment_operator(self, node): + """Transformation function for handling shorthand operators + + Returns + ======= + + augmented_assignment_expression: Codegen AST node + shorthand assignment expression represented as Codegen AST + + Raises + ====== + + NotImplementedError + If the shorthand operator for bitwise operators + (~=, ^=, &=, |=, <<=, >>=) is encountered + + """ + return self.transform_binary_operator(node) + + def transform_unary_operator(self, node): + """Transformation function for handling unary operators + + Returns + ======= + + unary_expression: Codegen AST node + simplified unary expression represented as Codegen AST + + Raises + ====== + + NotImplementedError + If dereferencing operator(*), address operator(&) or + bitwise NOT operator(~) is encountered + + """ + # supported operators list + operators_list = ['+', '-', '++', '--', '!'] + tokens = list(node.get_tokens()) + + # it can be either pre increment/decrement or any other operator from the list + if tokens[0].spelling in operators_list: + child = self.transform(next(node.get_children())) + # (decl_ref) e.g.; int a = ++b; or simply ++b; + if isinstance(child, str): + if tokens[0].spelling == '+': + return Symbol(child) + if tokens[0].spelling == '-': + return Mul(Symbol(child), -1) + if tokens[0].spelling == '++': + return PreIncrement(Symbol(child)) + if tokens[0].spelling == '--': + return PreDecrement(Symbol(child)) + if tokens[0].spelling == '!': + return Not(Symbol(child)) + # e.g.; int a = -1; or int b = -(1 + 2); + else: + if tokens[0].spelling == '+': + return child + if tokens[0].spelling == '-': + return Mul(child, -1) + if tokens[0].spelling == '!': + return Not(sympify(bool(child))) + + # it can be either post increment/decrement + # since variable name is obtained in token[0].spelling + elif tokens[1].spelling in ['++', '--']: + child = self.transform(next(node.get_children())) + if tokens[1].spelling == '++': + return PostIncrement(Symbol(child)) + if tokens[1].spelling == '--': + return PostDecrement(Symbol(child)) + else: + raise NotImplementedError("Dereferencing operator, " + "Address operator and bitwise NOT operator " + "have not been implemented yet!") + + def transform_binary_operator(self, node): + """Transformation function for handling binary operators + + Returns + ======= + + binary_expression: Codegen AST node + simplified binary expression represented as Codegen AST + + Raises + ====== + + NotImplementedError + If a bitwise operator or + unary operator(which is a child of any binary + operator in Clang AST) is encountered + + """ + # get all the tokens of assignment + # and store it in the tokens list + tokens = list(node.get_tokens()) + + # supported operators list + operators_list = ['+', '-', '*', '/', '%','=', + '>', '>=', '<', '<=', '==', '!=', '&&', '||', '+=', '-=', + '*=', '/=', '%='] + + # this stack will contain variable content + # and type of variable in the rhs + combined_variables_stack = [] + + # this stack will contain operators + # to be processed in the rhs + operators_stack = [] + + # iterate through every token + for token in tokens: + # token is either '(', ')' or + # any of the supported operators from the operator list + if token.kind == cin.TokenKind.PUNCTUATION: + + # push '(' to the operators stack + if token.spelling == '(': + operators_stack.append('(') + + elif token.spelling == ')': + # keep adding the expression to the + # combined variables stack unless + # '(' is found + while (operators_stack + and operators_stack[-1] != '('): + if len(combined_variables_stack) < 2: + raise NotImplementedError( + "Unary operators as a part of " + "binary operators is not " + "supported yet!") + rhs = combined_variables_stack.pop() + lhs = combined_variables_stack.pop() + operator = operators_stack.pop() + combined_variables_stack.append( + self.perform_operation( + lhs, rhs, operator)) + + # pop '(' + operators_stack.pop() + + # token is an operator (supported) + elif token.spelling in operators_list: + while (operators_stack + and self.priority_of(token.spelling) + <= self.priority_of( + operators_stack[-1])): + if len(combined_variables_stack) < 2: + raise NotImplementedError( + "Unary operators as a part of " + "binary operators is not " + "supported yet!") + rhs = combined_variables_stack.pop() + lhs = combined_variables_stack.pop() + operator = operators_stack.pop() + combined_variables_stack.append( + self.perform_operation( + lhs, rhs, operator)) + + # push current operator + operators_stack.append(token.spelling) + + # token is a bitwise operator + elif token.spelling in ['&', '|', '^', '<<', '>>']: + raise NotImplementedError( + "Bitwise operator has not been " + "implemented yet!") + + # token is a shorthand bitwise operator + elif token.spelling in ['&=', '|=', '^=', '<<=', + '>>=']: + raise NotImplementedError( + "Shorthand bitwise operator has not been " + "implemented yet!") + else: + raise NotImplementedError( + "Given token {} is not implemented yet!" + .format(token.spelling)) + + # token is an identifier(variable) + elif token.kind == cin.TokenKind.IDENTIFIER: + combined_variables_stack.append( + [token.spelling, 'identifier']) + + # token is a literal + elif token.kind == cin.TokenKind.LITERAL: + combined_variables_stack.append( + [token.spelling, 'literal']) + + # token is a keyword, either true or false + elif (token.kind == cin.TokenKind.KEYWORD + and token.spelling in ['true', 'false']): + combined_variables_stack.append( + [token.spelling, 'boolean']) + else: + raise NotImplementedError( + "Given token {} is not implemented yet!" + .format(token.spelling)) + + # process remaining operators + while operators_stack: + if len(combined_variables_stack) < 2: + raise NotImplementedError( + "Unary operators as a part of " + "binary operators is not " + "supported yet!") + rhs = combined_variables_stack.pop() + lhs = combined_variables_stack.pop() + operator = operators_stack.pop() + combined_variables_stack.append( + self.perform_operation(lhs, rhs, operator)) + + return combined_variables_stack[-1][0] + + def priority_of(self, op): + """To get the priority of given operator""" + if op in ['=', '+=', '-=', '*=', '/=', '%=']: + return 1 + if op in ['&&', '||']: + return 2 + if op in ['<', '<=', '>', '>=', '==', '!=']: + return 3 + if op in ['+', '-']: + return 4 + if op in ['*', '/', '%']: + return 5 + return 0 + + def perform_operation(self, lhs, rhs, op): + """Performs operation supported by the SymPy core + + Returns + ======= + + combined_variable: list + contains variable content and type of variable + + """ + lhs_value = self.get_expr_for_operand(lhs) + rhs_value = self.get_expr_for_operand(rhs) + if op == '+': + return [Add(lhs_value, rhs_value), 'expr'] + if op == '-': + return [Add(lhs_value, -rhs_value), 'expr'] + if op == '*': + return [Mul(lhs_value, rhs_value), 'expr'] + if op == '/': + return [Mul(lhs_value, Pow(rhs_value, Integer(-1))), 'expr'] + if op == '%': + return [Mod(lhs_value, rhs_value), 'expr'] + if op in ['<', '<=', '>', '>=', '==', '!=']: + return [Rel(lhs_value, rhs_value, op), 'expr'] + if op == '&&': + return [And(as_Boolean(lhs_value), as_Boolean(rhs_value)), 'expr'] + if op == '||': + return [Or(as_Boolean(lhs_value), as_Boolean(rhs_value)), 'expr'] + if op == '=': + return [Assignment(Variable(lhs_value), rhs_value), 'expr'] + if op in ['+=', '-=', '*=', '/=', '%=']: + return [aug_assign(Variable(lhs_value), op[0], rhs_value), 'expr'] + + def get_expr_for_operand(self, combined_variable): + """Gives out SymPy Codegen AST node + + AST node returned is corresponding to + combined variable passed.Combined variable contains + variable content and type of variable + + """ + if combined_variable[1] == 'identifier': + return Symbol(combined_variable[0]) + if combined_variable[1] == 'literal': + if '.' in combined_variable[0]: + return Float(float(combined_variable[0])) + else: + return Integer(int(combined_variable[0])) + if combined_variable[1] == 'expr': + return combined_variable[0] + if combined_variable[1] == 'boolean': + return true if combined_variable[0] == 'true' else false + + def transform_null_stmt(self, node): + """Handles Null Statement and returns None""" + return none + + def transform_while_stmt(self, node): + """Transformation function for handling while statement + + Returns + ======= + + while statement : Codegen AST Node + contains the while statement node having condition and + statement block + + """ + children = node.get_children() + + condition = self.transform(next(children)) + statements = self.transform(next(children)) + + if isinstance(statements, list): + statement_block = CodeBlock(*statements) + else: + statement_block = CodeBlock(statements) + + return While(condition, statement_block) + + + +else: + class CCodeConverter(): # type: ignore + def __init__(self, *args, **kwargs): + raise ImportError("Module not Installed") + + +def parse_c(source): + """Function for converting a C source code + + The function reads the source code present in the given file and parses it + to give out SymPy Expressions + + Returns + ======= + + src : list + List of Python expression strings + + """ + converter = CCodeConverter() + if os.path.exists(source): + src = converter.parse(source, flags = []) + else: + src = converter.parse_str(source, flags = []) + return src diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/fortran/__init__.py b/venv/lib/python3.11/site-packages/sympy/parsing/fortran/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c65e37cf3de2dddbcee0fa5c7eeac2fdc9f685db --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/fortran/__init__.py @@ -0,0 +1 @@ +"""Used for translating Fortran source code into a SymPy expression. """ diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/fortran/fortran_parser.py b/venv/lib/python3.11/site-packages/sympy/parsing/fortran/fortran_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..504249f6119a59a90d91c5e989f893cffe20e643 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/fortran/fortran_parser.py @@ -0,0 +1,347 @@ +from sympy.external import import_module + +lfortran = import_module('lfortran') + +if lfortran: + from sympy.codegen.ast import (Variable, IntBaseType, FloatBaseType, String, + Return, FunctionDefinition, Assignment) + from sympy.core import Add, Mul, Integer, Float + from sympy.core.symbol import Symbol + + asr_mod = lfortran.asr + asr = lfortran.asr.asr + src_to_ast = lfortran.ast.src_to_ast + ast_to_asr = lfortran.semantic.ast_to_asr.ast_to_asr + + """ + This module contains all the necessary Classes and Function used to Parse + Fortran code into SymPy expression + + The module and its API are currently under development and experimental. + It is also dependent on LFortran for the ASR that is converted to SymPy syntax + which is also under development. + The module only supports the features currently supported by the LFortran ASR + which will be updated as the development of LFortran and this module progresses + + You might find unexpected bugs and exceptions while using the module, feel free + to report them to the SymPy Issue Tracker + + The API for the module might also change while in development if better and + more effective ways are discovered for the process + + Features Supported + ================== + + - Variable Declarations (integers and reals) + - Function Definitions + - Assignments and Basic Binary Operations + + + Notes + ===== + + The module depends on an external dependency + + LFortran : Required to parse Fortran source code into ASR + + + References + ========== + + .. [1] https://github.com/sympy/sympy/issues + .. [2] https://gitlab.com/lfortran/lfortran + .. [3] https://docs.lfortran.org/ + + """ + + + class ASR2PyVisitor(asr.ASTVisitor): # type: ignore + """ + Visitor Class for LFortran ASR + + It is a Visitor class derived from asr.ASRVisitor which visits all the + nodes of the LFortran ASR and creates corresponding AST node for each + ASR node + + """ + + def __init__(self): + """Initialize the Parser""" + self._py_ast = [] + + def visit_TranslationUnit(self, node): + """ + Function to visit all the elements of the Translation Unit + created by LFortran ASR + """ + for s in node.global_scope.symbols: + sym = node.global_scope.symbols[s] + self.visit(sym) + for item in node.items: + self.visit(item) + + def visit_Assignment(self, node): + """Visitor Function for Assignment + + Visits each Assignment is the LFortran ASR and creates corresponding + assignment for SymPy. + + Notes + ===== + + The function currently only supports variable assignment and binary + operation assignments of varying multitudes. Any type of numberS or + array is not supported. + + Raises + ====== + + NotImplementedError() when called for Numeric assignments or Arrays + + """ + # TODO: Arithmetic Assignment + if isinstance(node.target, asr.Variable): + target = node.target + value = node.value + if isinstance(value, asr.Variable): + new_node = Assignment( + Variable( + target.name + ), + Variable( + value.name + ) + ) + elif (type(value) == asr.BinOp): + exp_ast = call_visitor(value) + for expr in exp_ast: + new_node = Assignment( + Variable(target.name), + expr + ) + else: + raise NotImplementedError("Numeric assignments not supported") + else: + raise NotImplementedError("Arrays not supported") + self._py_ast.append(new_node) + + def visit_BinOp(self, node): + """Visitor Function for Binary Operations + + Visits each binary operation present in the LFortran ASR like addition, + subtraction, multiplication, division and creates the corresponding + operation node in SymPy's AST + + In case of more than one binary operations, the function calls the + call_visitor() function on the child nodes of the binary operations + recursively until all the operations have been processed. + + Notes + ===== + + The function currently only supports binary operations with Variables + or other binary operations. Numerics are not supported as of yet. + + Raises + ====== + + NotImplementedError() when called for Numeric assignments + + """ + # TODO: Integer Binary Operations + op = node.op + lhs = node.left + rhs = node.right + + if (type(lhs) == asr.Variable): + left_value = Symbol(lhs.name) + elif(type(lhs) == asr.BinOp): + l_exp_ast = call_visitor(lhs) + for exp in l_exp_ast: + left_value = exp + else: + raise NotImplementedError("Numbers Currently not supported") + + if (type(rhs) == asr.Variable): + right_value = Symbol(rhs.name) + elif(type(rhs) == asr.BinOp): + r_exp_ast = call_visitor(rhs) + for exp in r_exp_ast: + right_value = exp + else: + raise NotImplementedError("Numbers Currently not supported") + + if isinstance(op, asr.Add): + new_node = Add(left_value, right_value) + elif isinstance(op, asr.Sub): + new_node = Add(left_value, -right_value) + elif isinstance(op, asr.Div): + new_node = Mul(left_value, 1/right_value) + elif isinstance(op, asr.Mul): + new_node = Mul(left_value, right_value) + + self._py_ast.append(new_node) + + def visit_Variable(self, node): + """Visitor Function for Variable Declaration + + Visits each variable declaration present in the ASR and creates a + Symbol declaration for each variable + + Notes + ===== + + The functions currently only support declaration of integer and + real variables. Other data types are still under development. + + Raises + ====== + + NotImplementedError() when called for unsupported data types + + """ + if isinstance(node.type, asr.Integer): + var_type = IntBaseType(String('integer')) + value = Integer(0) + elif isinstance(node.type, asr.Real): + var_type = FloatBaseType(String('real')) + value = Float(0.0) + else: + raise NotImplementedError("Data type not supported") + + if not (node.intent == 'in'): + new_node = Variable( + node.name + ).as_Declaration( + type = var_type, + value = value + ) + self._py_ast.append(new_node) + + def visit_Sequence(self, seq): + """Visitor Function for code sequence + + Visits a code sequence/ block and calls the visitor function on all the + children of the code block to create corresponding code in python + + """ + if seq is not None: + for node in seq: + self._py_ast.append(call_visitor(node)) + + def visit_Num(self, node): + """Visitor Function for Numbers in ASR + + This function is currently under development and will be updated + with improvements in the LFortran ASR + + """ + # TODO:Numbers when the LFortran ASR is updated + # self._py_ast.append(Integer(node.n)) + pass + + def visit_Function(self, node): + """Visitor Function for function Definitions + + Visits each function definition present in the ASR and creates a + function definition node in the Python AST with all the elements of the + given function + + The functions declare all the variables required as SymPy symbols in + the function before the function definition + + This function also the call_visior_function to parse the contents of + the function body + + """ + # TODO: Return statement, variable declaration + fn_args = [Variable(arg_iter.name) for arg_iter in node.args] + fn_body = [] + fn_name = node.name + for i in node.body: + fn_ast = call_visitor(i) + try: + fn_body_expr = fn_ast + except UnboundLocalError: + fn_body_expr = [] + for sym in node.symtab.symbols: + decl = call_visitor(node.symtab.symbols[sym]) + for symbols in decl: + fn_body.append(symbols) + for elem in fn_body_expr: + fn_body.append(elem) + fn_body.append( + Return( + Variable( + node.return_var.name + ) + ) + ) + if isinstance(node.return_var.type, asr.Integer): + ret_type = IntBaseType(String('integer')) + elif isinstance(node.return_var.type, asr.Real): + ret_type = FloatBaseType(String('real')) + else: + raise NotImplementedError("Data type not supported") + new_node = FunctionDefinition( + return_type = ret_type, + name = fn_name, + parameters = fn_args, + body = fn_body + ) + self._py_ast.append(new_node) + + def ret_ast(self): + """Returns the AST nodes""" + return self._py_ast +else: + class ASR2PyVisitor(): # type: ignore + def __init__(self, *args, **kwargs): + raise ImportError('lfortran not available') + +def call_visitor(fort_node): + """Calls the AST Visitor on the Module + + This function is used to call the AST visitor for a program or module + It imports all the required modules and calls the visit() function + on the given node + + Parameters + ========== + + fort_node : LFortran ASR object + Node for the operation for which the NodeVisitor is called + + Returns + ======= + + res_ast : list + list of SymPy AST Nodes + + """ + v = ASR2PyVisitor() + v.visit(fort_node) + res_ast = v.ret_ast() + return res_ast + + +def src_to_sympy(src): + """Wrapper function to convert the given Fortran source code to SymPy Expressions + + Parameters + ========== + + src : string + A string with the Fortran source code + + Returns + ======= + + py_src : string + A string with the Python source code compatible with SymPy + + """ + a_ast = src_to_ast(src, translation_unit=False) + a = ast_to_asr(a_ast) + py_src = call_visitor(a) + return py_src diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/latex/LICENSE.txt b/venv/lib/python3.11/site-packages/sympy/parsing/latex/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..6bbfda911b2afada41a568218e31a6502dc68f44 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/latex/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright 2016, latex2sympy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/latex/LaTeX.g4 b/venv/lib/python3.11/site-packages/sympy/parsing/latex/LaTeX.g4 new file mode 100644 index 0000000000000000000000000000000000000000..fc2c30f9817931e2060b549a39f98a6a4f9cb1f7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/latex/LaTeX.g4 @@ -0,0 +1,312 @@ +/* + ANTLR4 LaTeX Math Grammar + + Ported from latex2sympy by @augustt198 https://github.com/augustt198/latex2sympy See license in + LICENSE.txt + */ + +/* + After changing this file, it is necessary to run `python setup.py antlr` in the root directory of + the repository. This will regenerate the code in `sympy/parsing/latex/_antlr/*.py`. + */ + +grammar LaTeX; + +options { + language = Python3; +} + +WS: [ \t\r\n]+ -> skip; +THINSPACE: ('\\,' | '\\thinspace') -> skip; +MEDSPACE: ('\\:' | '\\medspace') -> skip; +THICKSPACE: ('\\;' | '\\thickspace') -> skip; +QUAD: '\\quad' -> skip; +QQUAD: '\\qquad' -> skip; +NEGTHINSPACE: ('\\!' | '\\negthinspace') -> skip; +NEGMEDSPACE: '\\negmedspace' -> skip; +NEGTHICKSPACE: '\\negthickspace' -> skip; +CMD_LEFT: '\\left' -> skip; +CMD_RIGHT: '\\right' -> skip; + +IGNORE: + ( + '\\vrule' + | '\\vcenter' + | '\\vbox' + | '\\vskip' + | '\\vspace' + | '\\hfil' + | '\\*' + | '\\-' + | '\\.' + | '\\/' + | '\\"' + | '\\(' + | '\\=' + ) -> skip; + +ADD: '+'; +SUB: '-'; +MUL: '*'; +DIV: '/'; + +L_PAREN: '('; +R_PAREN: ')'; +L_BRACE: '{'; +R_BRACE: '}'; +L_BRACE_LITERAL: '\\{'; +R_BRACE_LITERAL: '\\}'; +L_BRACKET: '['; +R_BRACKET: ']'; + +BAR: '|'; + +R_BAR: '\\right|'; +L_BAR: '\\left|'; + +L_ANGLE: '\\langle'; +R_ANGLE: '\\rangle'; +FUNC_LIM: '\\lim'; +LIM_APPROACH_SYM: + '\\to' + | '\\rightarrow' + | '\\Rightarrow' + | '\\longrightarrow' + | '\\Longrightarrow'; +FUNC_INT: + '\\int' + | '\\int\\limits'; +FUNC_SUM: '\\sum'; +FUNC_PROD: '\\prod'; + +FUNC_EXP: '\\exp'; +FUNC_LOG: '\\log'; +FUNC_LG: '\\lg'; +FUNC_LN: '\\ln'; +FUNC_SIN: '\\sin'; +FUNC_COS: '\\cos'; +FUNC_TAN: '\\tan'; +FUNC_CSC: '\\csc'; +FUNC_SEC: '\\sec'; +FUNC_COT: '\\cot'; + +FUNC_ARCSIN: '\\arcsin'; +FUNC_ARCCOS: '\\arccos'; +FUNC_ARCTAN: '\\arctan'; +FUNC_ARCCSC: '\\arccsc'; +FUNC_ARCSEC: '\\arcsec'; +FUNC_ARCCOT: '\\arccot'; + +FUNC_SINH: '\\sinh'; +FUNC_COSH: '\\cosh'; +FUNC_TANH: '\\tanh'; +FUNC_ARSINH: '\\arsinh'; +FUNC_ARCOSH: '\\arcosh'; +FUNC_ARTANH: '\\artanh'; + +L_FLOOR: '\\lfloor'; +R_FLOOR: '\\rfloor'; +L_CEIL: '\\lceil'; +R_CEIL: '\\rceil'; + +FUNC_SQRT: '\\sqrt'; +FUNC_OVERLINE: '\\overline'; + +CMD_TIMES: '\\times'; +CMD_CDOT: '\\cdot'; +CMD_DIV: '\\div'; +CMD_FRAC: + '\\frac' + | '\\dfrac' + | '\\tfrac'; +CMD_BINOM: '\\binom'; +CMD_DBINOM: '\\dbinom'; +CMD_TBINOM: '\\tbinom'; + +CMD_MATHIT: '\\mathit'; + +UNDERSCORE: '_'; +CARET: '^'; +COLON: ':'; + +fragment WS_CHAR: [ \t\r\n]; +DIFFERENTIAL: 'd' WS_CHAR*? ([a-zA-Z] | '\\' [a-zA-Z]+); + +LETTER: [a-zA-Z]; +DIGIT: [0-9]; + +EQUAL: (('&' WS_CHAR*?)? '=') | ('=' (WS_CHAR*? '&')?); +NEQ: '\\neq'; + +LT: '<'; +LTE: ('\\leq' | '\\le' | LTE_Q | LTE_S); +LTE_Q: '\\leqq'; +LTE_S: '\\leqslant'; + +GT: '>'; +GTE: ('\\geq' | '\\ge' | GTE_Q | GTE_S); +GTE_Q: '\\geqq'; +GTE_S: '\\geqslant'; + +BANG: '!'; + +SINGLE_QUOTES: '\''+; + +SYMBOL: '\\' [a-zA-Z]+; + +math: relation; + +relation: + relation (EQUAL | LT | LTE | GT | GTE | NEQ) relation + | expr; + +equality: expr EQUAL expr; + +expr: additive; + +additive: additive (ADD | SUB) additive | mp; + +// mult part +mp: + mp (MUL | CMD_TIMES | CMD_CDOT | DIV | CMD_DIV | COLON) mp + | unary; + +mp_nofunc: + mp_nofunc ( + MUL + | CMD_TIMES + | CMD_CDOT + | DIV + | CMD_DIV + | COLON + ) mp_nofunc + | unary_nofunc; + +unary: (ADD | SUB) unary | postfix+; + +unary_nofunc: + (ADD | SUB) unary_nofunc + | postfix postfix_nofunc*; + +postfix: exp postfix_op*; +postfix_nofunc: exp_nofunc postfix_op*; +postfix_op: BANG | eval_at; + +eval_at: + BAR (eval_at_sup | eval_at_sub | eval_at_sup eval_at_sub); + +eval_at_sub: UNDERSCORE L_BRACE (expr | equality) R_BRACE; + +eval_at_sup: CARET L_BRACE (expr | equality) R_BRACE; + +exp: exp CARET (atom | L_BRACE expr R_BRACE) subexpr? | comp; + +exp_nofunc: + exp_nofunc CARET (atom | L_BRACE expr R_BRACE) subexpr? + | comp_nofunc; + +comp: + group + | abs_group + | func + | atom + | floor + | ceil; + +comp_nofunc: + group + | abs_group + | atom + | floor + | ceil; + +group: + L_PAREN expr R_PAREN + | L_BRACKET expr R_BRACKET + | L_BRACE expr R_BRACE + | L_BRACE_LITERAL expr R_BRACE_LITERAL; + +abs_group: BAR expr BAR; + +number: DIGIT+ (',' DIGIT DIGIT DIGIT)* ('.' DIGIT+)?; + +atom: (LETTER | SYMBOL) (subexpr? SINGLE_QUOTES? | SINGLE_QUOTES? subexpr?) + | number + | DIFFERENTIAL + | mathit + | frac + | binom + | bra + | ket; + +bra: L_ANGLE expr (R_BAR | BAR); +ket: (L_BAR | BAR) expr R_ANGLE; + +mathit: CMD_MATHIT L_BRACE mathit_text R_BRACE; +mathit_text: LETTER*; + +frac: CMD_FRAC (upperd = DIGIT | L_BRACE upper = expr R_BRACE) + (lowerd = DIGIT | L_BRACE lower = expr R_BRACE); + +binom: + (CMD_BINOM | CMD_DBINOM | CMD_TBINOM) L_BRACE n = expr R_BRACE L_BRACE k = expr R_BRACE; + +floor: L_FLOOR val = expr R_FLOOR; +ceil: L_CEIL val = expr R_CEIL; + +func_normal: + FUNC_EXP + | FUNC_LOG + | FUNC_LG + | FUNC_LN + | FUNC_SIN + | FUNC_COS + | FUNC_TAN + | FUNC_CSC + | FUNC_SEC + | FUNC_COT + | FUNC_ARCSIN + | FUNC_ARCCOS + | FUNC_ARCTAN + | FUNC_ARCCSC + | FUNC_ARCSEC + | FUNC_ARCCOT + | FUNC_SINH + | FUNC_COSH + | FUNC_TANH + | FUNC_ARSINH + | FUNC_ARCOSH + | FUNC_ARTANH; + +func: + func_normal (subexpr? supexpr? | supexpr? subexpr?) ( + L_PAREN func_arg R_PAREN + | func_arg_noparens + ) + | (LETTER | SYMBOL) (subexpr? SINGLE_QUOTES? | SINGLE_QUOTES? subexpr?) // e.g. f(x), f_1'(x) + L_PAREN args R_PAREN + | FUNC_INT (subexpr supexpr | supexpr subexpr)? ( + additive? DIFFERENTIAL + | frac + | additive + ) + | FUNC_SQRT (L_BRACKET root = expr R_BRACKET)? L_BRACE base = expr R_BRACE + | FUNC_OVERLINE L_BRACE base = expr R_BRACE + | (FUNC_SUM | FUNC_PROD) (subeq supexpr | supexpr subeq) mp + | FUNC_LIM limit_sub mp; + +args: (expr ',' args) | expr; + +limit_sub: + UNDERSCORE L_BRACE (LETTER | SYMBOL) LIM_APPROACH_SYM expr ( + CARET ((L_BRACE (ADD | SUB) R_BRACE) | ADD | SUB) + )? R_BRACE; + +func_arg: expr | (expr ',' func_arg); +func_arg_noparens: mp_nofunc; + +subexpr: UNDERSCORE (atom | L_BRACE expr R_BRACE); +supexpr: CARET (atom | L_BRACE expr R_BRACE); + +subeq: UNDERSCORE L_BRACE equality R_BRACE; +supeq: UNDERSCORE L_BRACE equality R_BRACE; diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/latex/__init__.py b/venv/lib/python3.11/site-packages/sympy/parsing/latex/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9466d37b8b06f1f292c73f975e44d21c96da10d1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/latex/__init__.py @@ -0,0 +1,204 @@ +from sympy.external import import_module +from sympy.utilities.decorator import doctest_depends_on +from re import compile as rcompile + +from sympy.parsing.latex.lark import LarkLaTeXParser, TransformToSymPyExpr, parse_latex_lark # noqa + +from .errors import LaTeXParsingError # noqa + + +IGNORE_L = r"\s*[{]*\s*" +IGNORE_R = r"\s*[}]*\s*" +NO_LEFT = r"(? len(latex_str): + e = len(latex_str) + eellipsis = "" + + if x[3] in END_DELIM_REPR: + err = (f"Extra '{x[2]}' at index {x[0]} or " + "missing corresponding " + f"'{BEGIN_DELIM_REPR[MATRIX_DELIMS_INV[x[3]]]}' " + f"in LaTeX string: {sellipsis}{latex_str[s:e]}" + f"{eellipsis}") + raise LaTeXParsingError(err) + + if x[7] is None: + err = (f"Extra '{x[2]}' at index {x[0]} or " + "missing corresponding " + f"'{END_DELIM_REPR[MATRIX_DELIMS[x[3]]]}' " + f"in LaTeX string: {sellipsis}{latex_str[s:e]}" + f"{eellipsis}") + raise LaTeXParsingError(err) + + correct_end_regex = MATRIX_DELIMS[x[3]] + sellipsis = "..." if x[0] > 0 else "" + eellipsis = "..." if x[5] < len(latex_str) else "" + if x[7] != correct_end_regex: + err = ("Expected " + f"'{END_DELIM_REPR[correct_end_regex]}' " + f"to close the '{x[2]}' at index {x[0]} but " + f"found '{x[6]}' at index {x[4]} of LaTeX " + f"string instead: {sellipsis}{latex_str[x[0]:x[5]]}" + f"{eellipsis}") + raise LaTeXParsingError(err) + +__doctest_requires__ = {('parse_latex',): ['antlr4', 'lark']} + + +@doctest_depends_on(modules=('antlr4', 'lark')) +def parse_latex(s, strict=False, backend="antlr"): + r"""Converts the input LaTeX string ``s`` to a SymPy ``Expr``. + + Parameters + ========== + + s : str + The LaTeX string to parse. In Python source containing LaTeX, + *raw strings* (denoted with ``r"``, like this one) are preferred, + as LaTeX makes liberal use of the ``\`` character, which would + trigger escaping in normal Python strings. + backend : str, optional + Currently, there are two backends supported: ANTLR, and Lark. + The default setting is to use the ANTLR backend, which can be + changed to Lark if preferred. + + Use ``backend="antlr"`` for the ANTLR-based parser, and + ``backend="lark"`` for the Lark-based parser. + + The ``backend`` option is case-sensitive, and must be in + all lowercase. + strict : bool, optional + This option is only available with the ANTLR backend. + + If True, raise an exception if the string cannot be parsed as + valid LaTeX. If False, try to recover gracefully from common + mistakes. + + Examples + ======== + + >>> from sympy.parsing.latex import parse_latex + >>> expr = parse_latex(r"\frac {1 + \sqrt {\a}} {\b}") + >>> expr + (sqrt(a) + 1)/b + >>> expr.evalf(4, subs=dict(a=5, b=2)) + 1.618 + >>> func = parse_latex(r"\int_1^\alpha \dfrac{\mathrm{d}t}{t}", backend="lark") + >>> func.evalf(subs={"alpha": 2}) + 0.693147180559945 + """ + + check_matrix_delimiters(s) + + if backend == "antlr": + _latex = import_module( + 'sympy.parsing.latex._parse_latex_antlr', + import_kwargs={'fromlist': ['X']}) + + if _latex is not None: + return _latex.parse_latex(s, strict) + elif backend == "lark": + return parse_latex_lark(s) + else: + raise NotImplementedError(f"Using the '{backend}' backend in the LaTeX" \ + " parser is not supported.") diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/latex/_antlr/__init__.py b/venv/lib/python3.11/site-packages/sympy/parsing/latex/_antlr/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2d690e1eb8631ee7731fc1875769d3a4704a1743 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/latex/_antlr/__init__.py @@ -0,0 +1,9 @@ +# *** GENERATED BY `setup.py antlr`, DO NOT EDIT BY HAND *** +# +# Generated from ../LaTeX.g4, derived from latex2sympy +# latex2sympy is licensed under the MIT license +# https://github.com/augustt198/latex2sympy/blob/master/LICENSE.txt +# +# Generated with antlr4 +# antlr4 is licensed under the BSD-3-Clause License +# https://github.com/antlr/antlr4/blob/master/LICENSE.txt diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/latex/_antlr/latexlexer.py b/venv/lib/python3.11/site-packages/sympy/parsing/latex/_antlr/latexlexer.py new file mode 100644 index 0000000000000000000000000000000000000000..46ca959736c967782eef360b9b3268ccd0be0979 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/latex/_antlr/latexlexer.py @@ -0,0 +1,512 @@ +# *** GENERATED BY `setup.py antlr`, DO NOT EDIT BY HAND *** +# +# Generated from ../LaTeX.g4, derived from latex2sympy +# latex2sympy is licensed under the MIT license +# https://github.com/augustt198/latex2sympy/blob/master/LICENSE.txt +# +# Generated with antlr4 +# antlr4 is licensed under the BSD-3-Clause License +# https://github.com/antlr/antlr4/blob/master/LICENSE.txt +from antlr4 import * +from io import StringIO +import sys +if sys.version_info[1] > 5: + from typing import TextIO +else: + from typing.io import TextIO + + +def serializedATN(): + return [ + 4,0,91,911,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5, + 2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2, + 13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7, + 19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2, + 26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,31,7,31,2,32,7, + 32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38,7,38,2, + 39,7,39,2,40,7,40,2,41,7,41,2,42,7,42,2,43,7,43,2,44,7,44,2,45,7, + 45,2,46,7,46,2,47,7,47,2,48,7,48,2,49,7,49,2,50,7,50,2,51,7,51,2, + 52,7,52,2,53,7,53,2,54,7,54,2,55,7,55,2,56,7,56,2,57,7,57,2,58,7, + 58,2,59,7,59,2,60,7,60,2,61,7,61,2,62,7,62,2,63,7,63,2,64,7,64,2, + 65,7,65,2,66,7,66,2,67,7,67,2,68,7,68,2,69,7,69,2,70,7,70,2,71,7, + 71,2,72,7,72,2,73,7,73,2,74,7,74,2,75,7,75,2,76,7,76,2,77,7,77,2, + 78,7,78,2,79,7,79,2,80,7,80,2,81,7,81,2,82,7,82,2,83,7,83,2,84,7, + 84,2,85,7,85,2,86,7,86,2,87,7,87,2,88,7,88,2,89,7,89,2,90,7,90,2, + 91,7,91,1,0,1,0,1,1,1,1,1,2,4,2,191,8,2,11,2,12,2,192,1,2,1,2,1, + 3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,3,3,209,8,3,1,3,1, + 3,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,3,4,224,8,4,1,4,1, + 4,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,3,5,241,8, + 5,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,7,1,7,1,7,1,7,1,7,1, + 7,1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1, + 8,1,8,1,8,3,8,277,8,8,1,8,1,8,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1, + 9,1,9,1,9,1,9,1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,10, + 1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,11,1,11,1,11,1,11, + 1,11,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12, + 1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13, + 1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13, + 1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13, + 1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,3,13, + 381,8,13,1,13,1,13,1,14,1,14,1,15,1,15,1,16,1,16,1,17,1,17,1,18, + 1,18,1,19,1,19,1,20,1,20,1,21,1,21,1,22,1,22,1,22,1,23,1,23,1,23, + 1,24,1,24,1,25,1,25,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27, + 1,27,1,28,1,28,1,28,1,28,1,28,1,28,1,28,1,29,1,29,1,29,1,29,1,29, + 1,29,1,29,1,29,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,31,1,31, + 1,31,1,31,1,31,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32, + 1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32, + 1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32, + 1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32, + 1,32,1,32,1,32,1,32,1,32,1,32,3,32,504,8,32,1,33,1,33,1,33,1,33, + 1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,33,3,33,521, + 8,33,1,34,1,34,1,34,1,34,1,34,1,35,1,35,1,35,1,35,1,35,1,35,1,36, + 1,36,1,36,1,36,1,36,1,37,1,37,1,37,1,37,1,37,1,38,1,38,1,38,1,38, + 1,39,1,39,1,39,1,39,1,40,1,40,1,40,1,40,1,40,1,41,1,41,1,41,1,41, + 1,41,1,42,1,42,1,42,1,42,1,42,1,43,1,43,1,43,1,43,1,43,1,44,1,44, + 1,44,1,44,1,44,1,45,1,45,1,45,1,45,1,45,1,46,1,46,1,46,1,46,1,46, + 1,46,1,46,1,46,1,47,1,47,1,47,1,47,1,47,1,47,1,47,1,47,1,48,1,48, + 1,48,1,48,1,48,1,48,1,48,1,48,1,49,1,49,1,49,1,49,1,49,1,49,1,49, + 1,49,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,51,1,51,1,51,1,51, + 1,51,1,51,1,51,1,51,1,52,1,52,1,52,1,52,1,52,1,52,1,53,1,53,1,53, + 1,53,1,53,1,53,1,54,1,54,1,54,1,54,1,54,1,54,1,55,1,55,1,55,1,55, + 1,55,1,55,1,55,1,55,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,57, + 1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,58,1,58,1,58,1,58,1,58,1,58, + 1,58,1,58,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,60,1,60,1,60, + 1,60,1,60,1,60,1,60,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,62,1,62, + 1,62,1,62,1,62,1,62,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63, + 1,63,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,65,1,65,1,65,1,65,1,65, + 1,65,1,66,1,66,1,66,1,66,1,66,1,67,1,67,1,67,1,67,1,67,1,67,1,67, + 1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,3,67,753,8,67, + 1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,69,1,69,1,69,1,69,1,69,1,69, + 1,69,1,69,1,70,1,70,1,70,1,70,1,70,1,70,1,70,1,70,1,71,1,71,1,71, + 1,71,1,71,1,71,1,71,1,71,1,72,1,72,1,73,1,73,1,74,1,74,1,75,1,75, + 1,76,1,76,5,76,796,8,76,10,76,12,76,799,9,76,1,76,1,76,1,76,4,76, + 804,8,76,11,76,12,76,805,3,76,808,8,76,1,77,1,77,1,78,1,78,1,79, + 1,79,5,79,816,8,79,10,79,12,79,819,9,79,3,79,821,8,79,1,79,1,79, + 1,79,5,79,826,8,79,10,79,12,79,829,9,79,1,79,3,79,832,8,79,3,79, + 834,8,79,1,80,1,80,1,80,1,80,1,80,1,81,1,81,1,82,1,82,1,82,1,82, + 1,82,1,82,1,82,1,82,1,82,3,82,852,8,82,1,83,1,83,1,83,1,83,1,83, + 1,83,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,85,1,85, + 1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,3,86,881,8,86,1,87, + 1,87,1,87,1,87,1,87,1,87,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88, + 1,88,1,88,1,89,1,89,1,90,4,90,902,8,90,11,90,12,90,903,1,91,1,91, + 4,91,908,8,91,11,91,12,91,909,3,797,817,827,0,92,1,1,3,2,5,3,7,4, + 9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,16, + 33,17,35,18,37,19,39,20,41,21,43,22,45,23,47,24,49,25,51,26,53,27, + 55,28,57,29,59,30,61,31,63,32,65,33,67,34,69,35,71,36,73,37,75,38, + 77,39,79,40,81,41,83,42,85,43,87,44,89,45,91,46,93,47,95,48,97,49, + 99,50,101,51,103,52,105,53,107,54,109,55,111,56,113,57,115,58,117, + 59,119,60,121,61,123,62,125,63,127,64,129,65,131,66,133,67,135,68, + 137,69,139,70,141,71,143,72,145,73,147,74,149,75,151,0,153,76,155, + 77,157,78,159,79,161,80,163,81,165,82,167,83,169,84,171,85,173,86, + 175,87,177,88,179,89,181,90,183,91,1,0,3,3,0,9,10,13,13,32,32,2, + 0,65,90,97,122,1,0,48,57,949,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0, + 0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0, + 17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0, + 27,1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0, + 37,1,0,0,0,0,39,1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0, + 47,1,0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,55,1,0,0,0,0, + 57,1,0,0,0,0,59,1,0,0,0,0,61,1,0,0,0,0,63,1,0,0,0,0,65,1,0,0,0,0, + 67,1,0,0,0,0,69,1,0,0,0,0,71,1,0,0,0,0,73,1,0,0,0,0,75,1,0,0,0,0, + 77,1,0,0,0,0,79,1,0,0,0,0,81,1,0,0,0,0,83,1,0,0,0,0,85,1,0,0,0,0, + 87,1,0,0,0,0,89,1,0,0,0,0,91,1,0,0,0,0,93,1,0,0,0,0,95,1,0,0,0,0, + 97,1,0,0,0,0,99,1,0,0,0,0,101,1,0,0,0,0,103,1,0,0,0,0,105,1,0,0, + 0,0,107,1,0,0,0,0,109,1,0,0,0,0,111,1,0,0,0,0,113,1,0,0,0,0,115, + 1,0,0,0,0,117,1,0,0,0,0,119,1,0,0,0,0,121,1,0,0,0,0,123,1,0,0,0, + 0,125,1,0,0,0,0,127,1,0,0,0,0,129,1,0,0,0,0,131,1,0,0,0,0,133,1, + 0,0,0,0,135,1,0,0,0,0,137,1,0,0,0,0,139,1,0,0,0,0,141,1,0,0,0,0, + 143,1,0,0,0,0,145,1,0,0,0,0,147,1,0,0,0,0,149,1,0,0,0,0,153,1,0, + 0,0,0,155,1,0,0,0,0,157,1,0,0,0,0,159,1,0,0,0,0,161,1,0,0,0,0,163, + 1,0,0,0,0,165,1,0,0,0,0,167,1,0,0,0,0,169,1,0,0,0,0,171,1,0,0,0, + 0,173,1,0,0,0,0,175,1,0,0,0,0,177,1,0,0,0,0,179,1,0,0,0,0,181,1, + 0,0,0,0,183,1,0,0,0,1,185,1,0,0,0,3,187,1,0,0,0,5,190,1,0,0,0,7, + 208,1,0,0,0,9,223,1,0,0,0,11,240,1,0,0,0,13,244,1,0,0,0,15,252,1, + 0,0,0,17,276,1,0,0,0,19,280,1,0,0,0,21,295,1,0,0,0,23,312,1,0,0, + 0,25,320,1,0,0,0,27,380,1,0,0,0,29,384,1,0,0,0,31,386,1,0,0,0,33, + 388,1,0,0,0,35,390,1,0,0,0,37,392,1,0,0,0,39,394,1,0,0,0,41,396, + 1,0,0,0,43,398,1,0,0,0,45,400,1,0,0,0,47,403,1,0,0,0,49,406,1,0, + 0,0,51,408,1,0,0,0,53,410,1,0,0,0,55,412,1,0,0,0,57,420,1,0,0,0, + 59,427,1,0,0,0,61,435,1,0,0,0,63,443,1,0,0,0,65,503,1,0,0,0,67,520, + 1,0,0,0,69,522,1,0,0,0,71,527,1,0,0,0,73,533,1,0,0,0,75,538,1,0, + 0,0,77,543,1,0,0,0,79,547,1,0,0,0,81,551,1,0,0,0,83,556,1,0,0,0, + 85,561,1,0,0,0,87,566,1,0,0,0,89,571,1,0,0,0,91,576,1,0,0,0,93,581, + 1,0,0,0,95,589,1,0,0,0,97,597,1,0,0,0,99,605,1,0,0,0,101,613,1,0, + 0,0,103,621,1,0,0,0,105,629,1,0,0,0,107,635,1,0,0,0,109,641,1,0, + 0,0,111,647,1,0,0,0,113,655,1,0,0,0,115,663,1,0,0,0,117,671,1,0, + 0,0,119,679,1,0,0,0,121,687,1,0,0,0,123,694,1,0,0,0,125,701,1,0, + 0,0,127,707,1,0,0,0,129,717,1,0,0,0,131,724,1,0,0,0,133,730,1,0, + 0,0,135,752,1,0,0,0,137,754,1,0,0,0,139,761,1,0,0,0,141,769,1,0, + 0,0,143,777,1,0,0,0,145,785,1,0,0,0,147,787,1,0,0,0,149,789,1,0, + 0,0,151,791,1,0,0,0,153,793,1,0,0,0,155,809,1,0,0,0,157,811,1,0, + 0,0,159,833,1,0,0,0,161,835,1,0,0,0,163,840,1,0,0,0,165,851,1,0, + 0,0,167,853,1,0,0,0,169,859,1,0,0,0,171,869,1,0,0,0,173,880,1,0, + 0,0,175,882,1,0,0,0,177,888,1,0,0,0,179,898,1,0,0,0,181,901,1,0, + 0,0,183,905,1,0,0,0,185,186,5,44,0,0,186,2,1,0,0,0,187,188,5,46, + 0,0,188,4,1,0,0,0,189,191,7,0,0,0,190,189,1,0,0,0,191,192,1,0,0, + 0,192,190,1,0,0,0,192,193,1,0,0,0,193,194,1,0,0,0,194,195,6,2,0, + 0,195,6,1,0,0,0,196,197,5,92,0,0,197,209,5,44,0,0,198,199,5,92,0, + 0,199,200,5,116,0,0,200,201,5,104,0,0,201,202,5,105,0,0,202,203, + 5,110,0,0,203,204,5,115,0,0,204,205,5,112,0,0,205,206,5,97,0,0,206, + 207,5,99,0,0,207,209,5,101,0,0,208,196,1,0,0,0,208,198,1,0,0,0,209, + 210,1,0,0,0,210,211,6,3,0,0,211,8,1,0,0,0,212,213,5,92,0,0,213,224, + 5,58,0,0,214,215,5,92,0,0,215,216,5,109,0,0,216,217,5,101,0,0,217, + 218,5,100,0,0,218,219,5,115,0,0,219,220,5,112,0,0,220,221,5,97,0, + 0,221,222,5,99,0,0,222,224,5,101,0,0,223,212,1,0,0,0,223,214,1,0, + 0,0,224,225,1,0,0,0,225,226,6,4,0,0,226,10,1,0,0,0,227,228,5,92, + 0,0,228,241,5,59,0,0,229,230,5,92,0,0,230,231,5,116,0,0,231,232, + 5,104,0,0,232,233,5,105,0,0,233,234,5,99,0,0,234,235,5,107,0,0,235, + 236,5,115,0,0,236,237,5,112,0,0,237,238,5,97,0,0,238,239,5,99,0, + 0,239,241,5,101,0,0,240,227,1,0,0,0,240,229,1,0,0,0,241,242,1,0, + 0,0,242,243,6,5,0,0,243,12,1,0,0,0,244,245,5,92,0,0,245,246,5,113, + 0,0,246,247,5,117,0,0,247,248,5,97,0,0,248,249,5,100,0,0,249,250, + 1,0,0,0,250,251,6,6,0,0,251,14,1,0,0,0,252,253,5,92,0,0,253,254, + 5,113,0,0,254,255,5,113,0,0,255,256,5,117,0,0,256,257,5,97,0,0,257, + 258,5,100,0,0,258,259,1,0,0,0,259,260,6,7,0,0,260,16,1,0,0,0,261, + 262,5,92,0,0,262,277,5,33,0,0,263,264,5,92,0,0,264,265,5,110,0,0, + 265,266,5,101,0,0,266,267,5,103,0,0,267,268,5,116,0,0,268,269,5, + 104,0,0,269,270,5,105,0,0,270,271,5,110,0,0,271,272,5,115,0,0,272, + 273,5,112,0,0,273,274,5,97,0,0,274,275,5,99,0,0,275,277,5,101,0, + 0,276,261,1,0,0,0,276,263,1,0,0,0,277,278,1,0,0,0,278,279,6,8,0, + 0,279,18,1,0,0,0,280,281,5,92,0,0,281,282,5,110,0,0,282,283,5,101, + 0,0,283,284,5,103,0,0,284,285,5,109,0,0,285,286,5,101,0,0,286,287, + 5,100,0,0,287,288,5,115,0,0,288,289,5,112,0,0,289,290,5,97,0,0,290, + 291,5,99,0,0,291,292,5,101,0,0,292,293,1,0,0,0,293,294,6,9,0,0,294, + 20,1,0,0,0,295,296,5,92,0,0,296,297,5,110,0,0,297,298,5,101,0,0, + 298,299,5,103,0,0,299,300,5,116,0,0,300,301,5,104,0,0,301,302,5, + 105,0,0,302,303,5,99,0,0,303,304,5,107,0,0,304,305,5,115,0,0,305, + 306,5,112,0,0,306,307,5,97,0,0,307,308,5,99,0,0,308,309,5,101,0, + 0,309,310,1,0,0,0,310,311,6,10,0,0,311,22,1,0,0,0,312,313,5,92,0, + 0,313,314,5,108,0,0,314,315,5,101,0,0,315,316,5,102,0,0,316,317, + 5,116,0,0,317,318,1,0,0,0,318,319,6,11,0,0,319,24,1,0,0,0,320,321, + 5,92,0,0,321,322,5,114,0,0,322,323,5,105,0,0,323,324,5,103,0,0,324, + 325,5,104,0,0,325,326,5,116,0,0,326,327,1,0,0,0,327,328,6,12,0,0, + 328,26,1,0,0,0,329,330,5,92,0,0,330,331,5,118,0,0,331,332,5,114, + 0,0,332,333,5,117,0,0,333,334,5,108,0,0,334,381,5,101,0,0,335,336, + 5,92,0,0,336,337,5,118,0,0,337,338,5,99,0,0,338,339,5,101,0,0,339, + 340,5,110,0,0,340,341,5,116,0,0,341,342,5,101,0,0,342,381,5,114, + 0,0,343,344,5,92,0,0,344,345,5,118,0,0,345,346,5,98,0,0,346,347, + 5,111,0,0,347,381,5,120,0,0,348,349,5,92,0,0,349,350,5,118,0,0,350, + 351,5,115,0,0,351,352,5,107,0,0,352,353,5,105,0,0,353,381,5,112, + 0,0,354,355,5,92,0,0,355,356,5,118,0,0,356,357,5,115,0,0,357,358, + 5,112,0,0,358,359,5,97,0,0,359,360,5,99,0,0,360,381,5,101,0,0,361, + 362,5,92,0,0,362,363,5,104,0,0,363,364,5,102,0,0,364,365,5,105,0, + 0,365,381,5,108,0,0,366,367,5,92,0,0,367,381,5,42,0,0,368,369,5, + 92,0,0,369,381,5,45,0,0,370,371,5,92,0,0,371,381,5,46,0,0,372,373, + 5,92,0,0,373,381,5,47,0,0,374,375,5,92,0,0,375,381,5,34,0,0,376, + 377,5,92,0,0,377,381,5,40,0,0,378,379,5,92,0,0,379,381,5,61,0,0, + 380,329,1,0,0,0,380,335,1,0,0,0,380,343,1,0,0,0,380,348,1,0,0,0, + 380,354,1,0,0,0,380,361,1,0,0,0,380,366,1,0,0,0,380,368,1,0,0,0, + 380,370,1,0,0,0,380,372,1,0,0,0,380,374,1,0,0,0,380,376,1,0,0,0, + 380,378,1,0,0,0,381,382,1,0,0,0,382,383,6,13,0,0,383,28,1,0,0,0, + 384,385,5,43,0,0,385,30,1,0,0,0,386,387,5,45,0,0,387,32,1,0,0,0, + 388,389,5,42,0,0,389,34,1,0,0,0,390,391,5,47,0,0,391,36,1,0,0,0, + 392,393,5,40,0,0,393,38,1,0,0,0,394,395,5,41,0,0,395,40,1,0,0,0, + 396,397,5,123,0,0,397,42,1,0,0,0,398,399,5,125,0,0,399,44,1,0,0, + 0,400,401,5,92,0,0,401,402,5,123,0,0,402,46,1,0,0,0,403,404,5,92, + 0,0,404,405,5,125,0,0,405,48,1,0,0,0,406,407,5,91,0,0,407,50,1,0, + 0,0,408,409,5,93,0,0,409,52,1,0,0,0,410,411,5,124,0,0,411,54,1,0, + 0,0,412,413,5,92,0,0,413,414,5,114,0,0,414,415,5,105,0,0,415,416, + 5,103,0,0,416,417,5,104,0,0,417,418,5,116,0,0,418,419,5,124,0,0, + 419,56,1,0,0,0,420,421,5,92,0,0,421,422,5,108,0,0,422,423,5,101, + 0,0,423,424,5,102,0,0,424,425,5,116,0,0,425,426,5,124,0,0,426,58, + 1,0,0,0,427,428,5,92,0,0,428,429,5,108,0,0,429,430,5,97,0,0,430, + 431,5,110,0,0,431,432,5,103,0,0,432,433,5,108,0,0,433,434,5,101, + 0,0,434,60,1,0,0,0,435,436,5,92,0,0,436,437,5,114,0,0,437,438,5, + 97,0,0,438,439,5,110,0,0,439,440,5,103,0,0,440,441,5,108,0,0,441, + 442,5,101,0,0,442,62,1,0,0,0,443,444,5,92,0,0,444,445,5,108,0,0, + 445,446,5,105,0,0,446,447,5,109,0,0,447,64,1,0,0,0,448,449,5,92, + 0,0,449,450,5,116,0,0,450,504,5,111,0,0,451,452,5,92,0,0,452,453, + 5,114,0,0,453,454,5,105,0,0,454,455,5,103,0,0,455,456,5,104,0,0, + 456,457,5,116,0,0,457,458,5,97,0,0,458,459,5,114,0,0,459,460,5,114, + 0,0,460,461,5,111,0,0,461,504,5,119,0,0,462,463,5,92,0,0,463,464, + 5,82,0,0,464,465,5,105,0,0,465,466,5,103,0,0,466,467,5,104,0,0,467, + 468,5,116,0,0,468,469,5,97,0,0,469,470,5,114,0,0,470,471,5,114,0, + 0,471,472,5,111,0,0,472,504,5,119,0,0,473,474,5,92,0,0,474,475,5, + 108,0,0,475,476,5,111,0,0,476,477,5,110,0,0,477,478,5,103,0,0,478, + 479,5,114,0,0,479,480,5,105,0,0,480,481,5,103,0,0,481,482,5,104, + 0,0,482,483,5,116,0,0,483,484,5,97,0,0,484,485,5,114,0,0,485,486, + 5,114,0,0,486,487,5,111,0,0,487,504,5,119,0,0,488,489,5,92,0,0,489, + 490,5,76,0,0,490,491,5,111,0,0,491,492,5,110,0,0,492,493,5,103,0, + 0,493,494,5,114,0,0,494,495,5,105,0,0,495,496,5,103,0,0,496,497, + 5,104,0,0,497,498,5,116,0,0,498,499,5,97,0,0,499,500,5,114,0,0,500, + 501,5,114,0,0,501,502,5,111,0,0,502,504,5,119,0,0,503,448,1,0,0, + 0,503,451,1,0,0,0,503,462,1,0,0,0,503,473,1,0,0,0,503,488,1,0,0, + 0,504,66,1,0,0,0,505,506,5,92,0,0,506,507,5,105,0,0,507,508,5,110, + 0,0,508,521,5,116,0,0,509,510,5,92,0,0,510,511,5,105,0,0,511,512, + 5,110,0,0,512,513,5,116,0,0,513,514,5,92,0,0,514,515,5,108,0,0,515, + 516,5,105,0,0,516,517,5,109,0,0,517,518,5,105,0,0,518,519,5,116, + 0,0,519,521,5,115,0,0,520,505,1,0,0,0,520,509,1,0,0,0,521,68,1,0, + 0,0,522,523,5,92,0,0,523,524,5,115,0,0,524,525,5,117,0,0,525,526, + 5,109,0,0,526,70,1,0,0,0,527,528,5,92,0,0,528,529,5,112,0,0,529, + 530,5,114,0,0,530,531,5,111,0,0,531,532,5,100,0,0,532,72,1,0,0,0, + 533,534,5,92,0,0,534,535,5,101,0,0,535,536,5,120,0,0,536,537,5,112, + 0,0,537,74,1,0,0,0,538,539,5,92,0,0,539,540,5,108,0,0,540,541,5, + 111,0,0,541,542,5,103,0,0,542,76,1,0,0,0,543,544,5,92,0,0,544,545, + 5,108,0,0,545,546,5,103,0,0,546,78,1,0,0,0,547,548,5,92,0,0,548, + 549,5,108,0,0,549,550,5,110,0,0,550,80,1,0,0,0,551,552,5,92,0,0, + 552,553,5,115,0,0,553,554,5,105,0,0,554,555,5,110,0,0,555,82,1,0, + 0,0,556,557,5,92,0,0,557,558,5,99,0,0,558,559,5,111,0,0,559,560, + 5,115,0,0,560,84,1,0,0,0,561,562,5,92,0,0,562,563,5,116,0,0,563, + 564,5,97,0,0,564,565,5,110,0,0,565,86,1,0,0,0,566,567,5,92,0,0,567, + 568,5,99,0,0,568,569,5,115,0,0,569,570,5,99,0,0,570,88,1,0,0,0,571, + 572,5,92,0,0,572,573,5,115,0,0,573,574,5,101,0,0,574,575,5,99,0, + 0,575,90,1,0,0,0,576,577,5,92,0,0,577,578,5,99,0,0,578,579,5,111, + 0,0,579,580,5,116,0,0,580,92,1,0,0,0,581,582,5,92,0,0,582,583,5, + 97,0,0,583,584,5,114,0,0,584,585,5,99,0,0,585,586,5,115,0,0,586, + 587,5,105,0,0,587,588,5,110,0,0,588,94,1,0,0,0,589,590,5,92,0,0, + 590,591,5,97,0,0,591,592,5,114,0,0,592,593,5,99,0,0,593,594,5,99, + 0,0,594,595,5,111,0,0,595,596,5,115,0,0,596,96,1,0,0,0,597,598,5, + 92,0,0,598,599,5,97,0,0,599,600,5,114,0,0,600,601,5,99,0,0,601,602, + 5,116,0,0,602,603,5,97,0,0,603,604,5,110,0,0,604,98,1,0,0,0,605, + 606,5,92,0,0,606,607,5,97,0,0,607,608,5,114,0,0,608,609,5,99,0,0, + 609,610,5,99,0,0,610,611,5,115,0,0,611,612,5,99,0,0,612,100,1,0, + 0,0,613,614,5,92,0,0,614,615,5,97,0,0,615,616,5,114,0,0,616,617, + 5,99,0,0,617,618,5,115,0,0,618,619,5,101,0,0,619,620,5,99,0,0,620, + 102,1,0,0,0,621,622,5,92,0,0,622,623,5,97,0,0,623,624,5,114,0,0, + 624,625,5,99,0,0,625,626,5,99,0,0,626,627,5,111,0,0,627,628,5,116, + 0,0,628,104,1,0,0,0,629,630,5,92,0,0,630,631,5,115,0,0,631,632,5, + 105,0,0,632,633,5,110,0,0,633,634,5,104,0,0,634,106,1,0,0,0,635, + 636,5,92,0,0,636,637,5,99,0,0,637,638,5,111,0,0,638,639,5,115,0, + 0,639,640,5,104,0,0,640,108,1,0,0,0,641,642,5,92,0,0,642,643,5,116, + 0,0,643,644,5,97,0,0,644,645,5,110,0,0,645,646,5,104,0,0,646,110, + 1,0,0,0,647,648,5,92,0,0,648,649,5,97,0,0,649,650,5,114,0,0,650, + 651,5,115,0,0,651,652,5,105,0,0,652,653,5,110,0,0,653,654,5,104, + 0,0,654,112,1,0,0,0,655,656,5,92,0,0,656,657,5,97,0,0,657,658,5, + 114,0,0,658,659,5,99,0,0,659,660,5,111,0,0,660,661,5,115,0,0,661, + 662,5,104,0,0,662,114,1,0,0,0,663,664,5,92,0,0,664,665,5,97,0,0, + 665,666,5,114,0,0,666,667,5,116,0,0,667,668,5,97,0,0,668,669,5,110, + 0,0,669,670,5,104,0,0,670,116,1,0,0,0,671,672,5,92,0,0,672,673,5, + 108,0,0,673,674,5,102,0,0,674,675,5,108,0,0,675,676,5,111,0,0,676, + 677,5,111,0,0,677,678,5,114,0,0,678,118,1,0,0,0,679,680,5,92,0,0, + 680,681,5,114,0,0,681,682,5,102,0,0,682,683,5,108,0,0,683,684,5, + 111,0,0,684,685,5,111,0,0,685,686,5,114,0,0,686,120,1,0,0,0,687, + 688,5,92,0,0,688,689,5,108,0,0,689,690,5,99,0,0,690,691,5,101,0, + 0,691,692,5,105,0,0,692,693,5,108,0,0,693,122,1,0,0,0,694,695,5, + 92,0,0,695,696,5,114,0,0,696,697,5,99,0,0,697,698,5,101,0,0,698, + 699,5,105,0,0,699,700,5,108,0,0,700,124,1,0,0,0,701,702,5,92,0,0, + 702,703,5,115,0,0,703,704,5,113,0,0,704,705,5,114,0,0,705,706,5, + 116,0,0,706,126,1,0,0,0,707,708,5,92,0,0,708,709,5,111,0,0,709,710, + 5,118,0,0,710,711,5,101,0,0,711,712,5,114,0,0,712,713,5,108,0,0, + 713,714,5,105,0,0,714,715,5,110,0,0,715,716,5,101,0,0,716,128,1, + 0,0,0,717,718,5,92,0,0,718,719,5,116,0,0,719,720,5,105,0,0,720,721, + 5,109,0,0,721,722,5,101,0,0,722,723,5,115,0,0,723,130,1,0,0,0,724, + 725,5,92,0,0,725,726,5,99,0,0,726,727,5,100,0,0,727,728,5,111,0, + 0,728,729,5,116,0,0,729,132,1,0,0,0,730,731,5,92,0,0,731,732,5,100, + 0,0,732,733,5,105,0,0,733,734,5,118,0,0,734,134,1,0,0,0,735,736, + 5,92,0,0,736,737,5,102,0,0,737,738,5,114,0,0,738,739,5,97,0,0,739, + 753,5,99,0,0,740,741,5,92,0,0,741,742,5,100,0,0,742,743,5,102,0, + 0,743,744,5,114,0,0,744,745,5,97,0,0,745,753,5,99,0,0,746,747,5, + 92,0,0,747,748,5,116,0,0,748,749,5,102,0,0,749,750,5,114,0,0,750, + 751,5,97,0,0,751,753,5,99,0,0,752,735,1,0,0,0,752,740,1,0,0,0,752, + 746,1,0,0,0,753,136,1,0,0,0,754,755,5,92,0,0,755,756,5,98,0,0,756, + 757,5,105,0,0,757,758,5,110,0,0,758,759,5,111,0,0,759,760,5,109, + 0,0,760,138,1,0,0,0,761,762,5,92,0,0,762,763,5,100,0,0,763,764,5, + 98,0,0,764,765,5,105,0,0,765,766,5,110,0,0,766,767,5,111,0,0,767, + 768,5,109,0,0,768,140,1,0,0,0,769,770,5,92,0,0,770,771,5,116,0,0, + 771,772,5,98,0,0,772,773,5,105,0,0,773,774,5,110,0,0,774,775,5,111, + 0,0,775,776,5,109,0,0,776,142,1,0,0,0,777,778,5,92,0,0,778,779,5, + 109,0,0,779,780,5,97,0,0,780,781,5,116,0,0,781,782,5,104,0,0,782, + 783,5,105,0,0,783,784,5,116,0,0,784,144,1,0,0,0,785,786,5,95,0,0, + 786,146,1,0,0,0,787,788,5,94,0,0,788,148,1,0,0,0,789,790,5,58,0, + 0,790,150,1,0,0,0,791,792,7,0,0,0,792,152,1,0,0,0,793,797,5,100, + 0,0,794,796,3,151,75,0,795,794,1,0,0,0,796,799,1,0,0,0,797,798,1, + 0,0,0,797,795,1,0,0,0,798,807,1,0,0,0,799,797,1,0,0,0,800,808,7, + 1,0,0,801,803,5,92,0,0,802,804,7,1,0,0,803,802,1,0,0,0,804,805,1, + 0,0,0,805,803,1,0,0,0,805,806,1,0,0,0,806,808,1,0,0,0,807,800,1, + 0,0,0,807,801,1,0,0,0,808,154,1,0,0,0,809,810,7,1,0,0,810,156,1, + 0,0,0,811,812,7,2,0,0,812,158,1,0,0,0,813,817,5,38,0,0,814,816,3, + 151,75,0,815,814,1,0,0,0,816,819,1,0,0,0,817,818,1,0,0,0,817,815, + 1,0,0,0,818,821,1,0,0,0,819,817,1,0,0,0,820,813,1,0,0,0,820,821, + 1,0,0,0,821,822,1,0,0,0,822,834,5,61,0,0,823,831,5,61,0,0,824,826, + 3,151,75,0,825,824,1,0,0,0,826,829,1,0,0,0,827,828,1,0,0,0,827,825, + 1,0,0,0,828,830,1,0,0,0,829,827,1,0,0,0,830,832,5,38,0,0,831,827, + 1,0,0,0,831,832,1,0,0,0,832,834,1,0,0,0,833,820,1,0,0,0,833,823, + 1,0,0,0,834,160,1,0,0,0,835,836,5,92,0,0,836,837,5,110,0,0,837,838, + 5,101,0,0,838,839,5,113,0,0,839,162,1,0,0,0,840,841,5,60,0,0,841, + 164,1,0,0,0,842,843,5,92,0,0,843,844,5,108,0,0,844,845,5,101,0,0, + 845,852,5,113,0,0,846,847,5,92,0,0,847,848,5,108,0,0,848,852,5,101, + 0,0,849,852,3,167,83,0,850,852,3,169,84,0,851,842,1,0,0,0,851,846, + 1,0,0,0,851,849,1,0,0,0,851,850,1,0,0,0,852,166,1,0,0,0,853,854, + 5,92,0,0,854,855,5,108,0,0,855,856,5,101,0,0,856,857,5,113,0,0,857, + 858,5,113,0,0,858,168,1,0,0,0,859,860,5,92,0,0,860,861,5,108,0,0, + 861,862,5,101,0,0,862,863,5,113,0,0,863,864,5,115,0,0,864,865,5, + 108,0,0,865,866,5,97,0,0,866,867,5,110,0,0,867,868,5,116,0,0,868, + 170,1,0,0,0,869,870,5,62,0,0,870,172,1,0,0,0,871,872,5,92,0,0,872, + 873,5,103,0,0,873,874,5,101,0,0,874,881,5,113,0,0,875,876,5,92,0, + 0,876,877,5,103,0,0,877,881,5,101,0,0,878,881,3,175,87,0,879,881, + 3,177,88,0,880,871,1,0,0,0,880,875,1,0,0,0,880,878,1,0,0,0,880,879, + 1,0,0,0,881,174,1,0,0,0,882,883,5,92,0,0,883,884,5,103,0,0,884,885, + 5,101,0,0,885,886,5,113,0,0,886,887,5,113,0,0,887,176,1,0,0,0,888, + 889,5,92,0,0,889,890,5,103,0,0,890,891,5,101,0,0,891,892,5,113,0, + 0,892,893,5,115,0,0,893,894,5,108,0,0,894,895,5,97,0,0,895,896,5, + 110,0,0,896,897,5,116,0,0,897,178,1,0,0,0,898,899,5,33,0,0,899,180, + 1,0,0,0,900,902,5,39,0,0,901,900,1,0,0,0,902,903,1,0,0,0,903,901, + 1,0,0,0,903,904,1,0,0,0,904,182,1,0,0,0,905,907,5,92,0,0,906,908, + 7,1,0,0,907,906,1,0,0,0,908,909,1,0,0,0,909,907,1,0,0,0,909,910, + 1,0,0,0,910,184,1,0,0,0,22,0,192,208,223,240,276,380,503,520,752, + 797,805,807,817,820,827,831,833,851,880,903,909,1,6,0,0 + ] + +class LaTeXLexer(Lexer): + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + T__0 = 1 + T__1 = 2 + WS = 3 + THINSPACE = 4 + MEDSPACE = 5 + THICKSPACE = 6 + QUAD = 7 + QQUAD = 8 + NEGTHINSPACE = 9 + NEGMEDSPACE = 10 + NEGTHICKSPACE = 11 + CMD_LEFT = 12 + CMD_RIGHT = 13 + IGNORE = 14 + ADD = 15 + SUB = 16 + MUL = 17 + DIV = 18 + L_PAREN = 19 + R_PAREN = 20 + L_BRACE = 21 + R_BRACE = 22 + L_BRACE_LITERAL = 23 + R_BRACE_LITERAL = 24 + L_BRACKET = 25 + R_BRACKET = 26 + BAR = 27 + R_BAR = 28 + L_BAR = 29 + L_ANGLE = 30 + R_ANGLE = 31 + FUNC_LIM = 32 + LIM_APPROACH_SYM = 33 + FUNC_INT = 34 + FUNC_SUM = 35 + FUNC_PROD = 36 + FUNC_EXP = 37 + FUNC_LOG = 38 + FUNC_LG = 39 + FUNC_LN = 40 + FUNC_SIN = 41 + FUNC_COS = 42 + FUNC_TAN = 43 + FUNC_CSC = 44 + FUNC_SEC = 45 + FUNC_COT = 46 + FUNC_ARCSIN = 47 + FUNC_ARCCOS = 48 + FUNC_ARCTAN = 49 + FUNC_ARCCSC = 50 + FUNC_ARCSEC = 51 + FUNC_ARCCOT = 52 + FUNC_SINH = 53 + FUNC_COSH = 54 + FUNC_TANH = 55 + FUNC_ARSINH = 56 + FUNC_ARCOSH = 57 + FUNC_ARTANH = 58 + L_FLOOR = 59 + R_FLOOR = 60 + L_CEIL = 61 + R_CEIL = 62 + FUNC_SQRT = 63 + FUNC_OVERLINE = 64 + CMD_TIMES = 65 + CMD_CDOT = 66 + CMD_DIV = 67 + CMD_FRAC = 68 + CMD_BINOM = 69 + CMD_DBINOM = 70 + CMD_TBINOM = 71 + CMD_MATHIT = 72 + UNDERSCORE = 73 + CARET = 74 + COLON = 75 + DIFFERENTIAL = 76 + LETTER = 77 + DIGIT = 78 + EQUAL = 79 + NEQ = 80 + LT = 81 + LTE = 82 + LTE_Q = 83 + LTE_S = 84 + GT = 85 + GTE = 86 + GTE_Q = 87 + GTE_S = 88 + BANG = 89 + SINGLE_QUOTES = 90 + SYMBOL = 91 + + channelNames = [ u"DEFAULT_TOKEN_CHANNEL", u"HIDDEN" ] + + modeNames = [ "DEFAULT_MODE" ] + + literalNames = [ "", + "','", "'.'", "'\\quad'", "'\\qquad'", "'\\negmedspace'", "'\\negthickspace'", + "'\\left'", "'\\right'", "'+'", "'-'", "'*'", "'/'", "'('", + "')'", "'{'", "'}'", "'\\{'", "'\\}'", "'['", "']'", "'|'", + "'\\right|'", "'\\left|'", "'\\langle'", "'\\rangle'", "'\\lim'", + "'\\sum'", "'\\prod'", "'\\exp'", "'\\log'", "'\\lg'", "'\\ln'", + "'\\sin'", "'\\cos'", "'\\tan'", "'\\csc'", "'\\sec'", "'\\cot'", + "'\\arcsin'", "'\\arccos'", "'\\arctan'", "'\\arccsc'", "'\\arcsec'", + "'\\arccot'", "'\\sinh'", "'\\cosh'", "'\\tanh'", "'\\arsinh'", + "'\\arcosh'", "'\\artanh'", "'\\lfloor'", "'\\rfloor'", "'\\lceil'", + "'\\rceil'", "'\\sqrt'", "'\\overline'", "'\\times'", "'\\cdot'", + "'\\div'", "'\\binom'", "'\\dbinom'", "'\\tbinom'", "'\\mathit'", + "'_'", "'^'", "':'", "'\\neq'", "'<'", "'\\leqq'", "'\\leqslant'", + "'>'", "'\\geqq'", "'\\geqslant'", "'!'" ] + + symbolicNames = [ "", + "WS", "THINSPACE", "MEDSPACE", "THICKSPACE", "QUAD", "QQUAD", + "NEGTHINSPACE", "NEGMEDSPACE", "NEGTHICKSPACE", "CMD_LEFT", + "CMD_RIGHT", "IGNORE", "ADD", "SUB", "MUL", "DIV", "L_PAREN", + "R_PAREN", "L_BRACE", "R_BRACE", "L_BRACE_LITERAL", "R_BRACE_LITERAL", + "L_BRACKET", "R_BRACKET", "BAR", "R_BAR", "L_BAR", "L_ANGLE", + "R_ANGLE", "FUNC_LIM", "LIM_APPROACH_SYM", "FUNC_INT", "FUNC_SUM", + "FUNC_PROD", "FUNC_EXP", "FUNC_LOG", "FUNC_LG", "FUNC_LN", "FUNC_SIN", + "FUNC_COS", "FUNC_TAN", "FUNC_CSC", "FUNC_SEC", "FUNC_COT", + "FUNC_ARCSIN", "FUNC_ARCCOS", "FUNC_ARCTAN", "FUNC_ARCCSC", + "FUNC_ARCSEC", "FUNC_ARCCOT", "FUNC_SINH", "FUNC_COSH", "FUNC_TANH", + "FUNC_ARSINH", "FUNC_ARCOSH", "FUNC_ARTANH", "L_FLOOR", "R_FLOOR", + "L_CEIL", "R_CEIL", "FUNC_SQRT", "FUNC_OVERLINE", "CMD_TIMES", + "CMD_CDOT", "CMD_DIV", "CMD_FRAC", "CMD_BINOM", "CMD_DBINOM", + "CMD_TBINOM", "CMD_MATHIT", "UNDERSCORE", "CARET", "COLON", + "DIFFERENTIAL", "LETTER", "DIGIT", "EQUAL", "NEQ", "LT", "LTE", + "LTE_Q", "LTE_S", "GT", "GTE", "GTE_Q", "GTE_S", "BANG", "SINGLE_QUOTES", + "SYMBOL" ] + + ruleNames = [ "T__0", "T__1", "WS", "THINSPACE", "MEDSPACE", "THICKSPACE", + "QUAD", "QQUAD", "NEGTHINSPACE", "NEGMEDSPACE", "NEGTHICKSPACE", + "CMD_LEFT", "CMD_RIGHT", "IGNORE", "ADD", "SUB", "MUL", + "DIV", "L_PAREN", "R_PAREN", "L_BRACE", "R_BRACE", "L_BRACE_LITERAL", + "R_BRACE_LITERAL", "L_BRACKET", "R_BRACKET", "BAR", "R_BAR", + "L_BAR", "L_ANGLE", "R_ANGLE", "FUNC_LIM", "LIM_APPROACH_SYM", + "FUNC_INT", "FUNC_SUM", "FUNC_PROD", "FUNC_EXP", "FUNC_LOG", + "FUNC_LG", "FUNC_LN", "FUNC_SIN", "FUNC_COS", "FUNC_TAN", + "FUNC_CSC", "FUNC_SEC", "FUNC_COT", "FUNC_ARCSIN", "FUNC_ARCCOS", + "FUNC_ARCTAN", "FUNC_ARCCSC", "FUNC_ARCSEC", "FUNC_ARCCOT", + "FUNC_SINH", "FUNC_COSH", "FUNC_TANH", "FUNC_ARSINH", + "FUNC_ARCOSH", "FUNC_ARTANH", "L_FLOOR", "R_FLOOR", "L_CEIL", + "R_CEIL", "FUNC_SQRT", "FUNC_OVERLINE", "CMD_TIMES", "CMD_CDOT", + "CMD_DIV", "CMD_FRAC", "CMD_BINOM", "CMD_DBINOM", "CMD_TBINOM", + "CMD_MATHIT", "UNDERSCORE", "CARET", "COLON", "WS_CHAR", + "DIFFERENTIAL", "LETTER", "DIGIT", "EQUAL", "NEQ", "LT", + "LTE", "LTE_Q", "LTE_S", "GT", "GTE", "GTE_Q", "GTE_S", + "BANG", "SINGLE_QUOTES", "SYMBOL" ] + + grammarFileName = "LaTeX.g4" + + def __init__(self, input=None, output:TextIO = sys.stdout): + super().__init__(input, output) + self.checkVersion("4.11.1") + self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache()) + self._actions = None + self._predicates = None + + diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/latex/_antlr/latexparser.py b/venv/lib/python3.11/site-packages/sympy/parsing/latex/_antlr/latexparser.py new file mode 100644 index 0000000000000000000000000000000000000000..f6f58119055ded8f77380bbef52c77ddd6a01cfe --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/latex/_antlr/latexparser.py @@ -0,0 +1,3652 @@ +# *** GENERATED BY `setup.py antlr`, DO NOT EDIT BY HAND *** +# +# Generated from ../LaTeX.g4, derived from latex2sympy +# latex2sympy is licensed under the MIT license +# https://github.com/augustt198/latex2sympy/blob/master/LICENSE.txt +# +# Generated with antlr4 +# antlr4 is licensed under the BSD-3-Clause License +# https://github.com/antlr/antlr4/blob/master/LICENSE.txt +from antlr4 import * +from io import StringIO +import sys +if sys.version_info[1] > 5: + from typing import TextIO +else: + from typing.io import TextIO + +def serializedATN(): + return [ + 4,1,91,522,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7, + 6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13, + 2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2,20, + 7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7,26, + 2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,31,7,31,2,32,7,32,2,33, + 7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38,7,38,2,39,7,39, + 2,40,7,40,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,5,1,91,8,1,10,1,12,1,94, + 9,1,1,2,1,2,1,2,1,2,1,3,1,3,1,4,1,4,1,4,1,4,1,4,1,4,5,4,108,8,4, + 10,4,12,4,111,9,4,1,5,1,5,1,5,1,5,1,5,1,5,5,5,119,8,5,10,5,12,5, + 122,9,5,1,6,1,6,1,6,1,6,1,6,1,6,5,6,130,8,6,10,6,12,6,133,9,6,1, + 7,1,7,1,7,4,7,138,8,7,11,7,12,7,139,3,7,142,8,7,1,8,1,8,1,8,1,8, + 5,8,148,8,8,10,8,12,8,151,9,8,3,8,153,8,8,1,9,1,9,5,9,157,8,9,10, + 9,12,9,160,9,9,1,10,1,10,5,10,164,8,10,10,10,12,10,167,9,10,1,11, + 1,11,3,11,171,8,11,1,12,1,12,1,12,1,12,1,12,1,12,3,12,179,8,12,1, + 13,1,13,1,13,1,13,3,13,185,8,13,1,13,1,13,1,14,1,14,1,14,1,14,3, + 14,193,8,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,1,15,1,15,1, + 15,1,15,3,15,207,8,15,1,15,3,15,210,8,15,5,15,212,8,15,10,15,12, + 15,215,9,15,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,3, + 16,227,8,16,1,16,3,16,230,8,16,5,16,232,8,16,10,16,12,16,235,9,16, + 1,17,1,17,1,17,1,17,1,17,1,17,3,17,243,8,17,1,18,1,18,1,18,1,18, + 1,18,3,18,250,8,18,1,19,1,19,1,19,1,19,1,19,1,19,1,19,1,19,1,19, + 1,19,1,19,1,19,1,19,1,19,1,19,1,19,3,19,268,8,19,1,20,1,20,1,20, + 1,20,1,21,4,21,275,8,21,11,21,12,21,276,1,21,1,21,1,21,1,21,5,21, + 283,8,21,10,21,12,21,286,9,21,1,21,1,21,4,21,290,8,21,11,21,12,21, + 291,3,21,294,8,21,1,22,1,22,3,22,298,8,22,1,22,3,22,301,8,22,1,22, + 3,22,304,8,22,1,22,3,22,307,8,22,3,22,309,8,22,1,22,1,22,1,22,1, + 22,1,22,1,22,1,22,3,22,318,8,22,1,23,1,23,1,23,1,23,1,24,1,24,1, + 24,1,24,1,25,1,25,1,25,1,25,1,25,1,26,5,26,334,8,26,10,26,12,26, + 337,9,26,1,27,1,27,1,27,1,27,1,27,1,27,3,27,345,8,27,1,27,1,27,1, + 27,1,27,1,27,3,27,352,8,27,1,28,1,28,1,28,1,28,1,28,1,28,1,28,1, + 28,1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,30,1,31,1,31,1,32,1,32,3, + 32,374,8,32,1,32,3,32,377,8,32,1,32,3,32,380,8,32,1,32,3,32,383, + 8,32,3,32,385,8,32,1,32,1,32,1,32,1,32,1,32,3,32,392,8,32,1,32,1, + 32,3,32,396,8,32,1,32,3,32,399,8,32,1,32,3,32,402,8,32,1,32,3,32, + 405,8,32,3,32,407,8,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1, + 32,1,32,1,32,3,32,420,8,32,1,32,3,32,423,8,32,1,32,1,32,1,32,3,32, + 428,8,32,1,32,1,32,1,32,1,32,1,32,3,32,435,8,32,1,32,1,32,1,32,1, + 32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,3, + 32,453,8,32,1,32,1,32,1,32,1,32,1,32,1,32,3,32,461,8,32,1,33,1,33, + 1,33,1,33,1,33,3,33,468,8,33,1,34,1,34,1,34,1,34,1,34,1,34,1,34, + 1,34,1,34,1,34,1,34,3,34,481,8,34,3,34,483,8,34,1,34,1,34,1,35,1, + 35,1,35,1,35,1,35,3,35,492,8,35,1,36,1,36,1,37,1,37,1,37,1,37,1, + 37,1,37,3,37,502,8,37,1,38,1,38,1,38,1,38,1,38,1,38,3,38,510,8,38, + 1,39,1,39,1,39,1,39,1,39,1,40,1,40,1,40,1,40,1,40,1,40,0,6,2,8,10, + 12,30,32,41,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36, + 38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80, + 0,9,2,0,79,82,85,86,1,0,15,16,3,0,17,18,65,67,75,75,2,0,77,77,91, + 91,1,0,27,28,2,0,27,27,29,29,1,0,69,71,1,0,37,58,1,0,35,36,563,0, + 82,1,0,0,0,2,84,1,0,0,0,4,95,1,0,0,0,6,99,1,0,0,0,8,101,1,0,0,0, + 10,112,1,0,0,0,12,123,1,0,0,0,14,141,1,0,0,0,16,152,1,0,0,0,18,154, + 1,0,0,0,20,161,1,0,0,0,22,170,1,0,0,0,24,172,1,0,0,0,26,180,1,0, + 0,0,28,188,1,0,0,0,30,196,1,0,0,0,32,216,1,0,0,0,34,242,1,0,0,0, + 36,249,1,0,0,0,38,267,1,0,0,0,40,269,1,0,0,0,42,274,1,0,0,0,44,317, + 1,0,0,0,46,319,1,0,0,0,48,323,1,0,0,0,50,327,1,0,0,0,52,335,1,0, + 0,0,54,338,1,0,0,0,56,353,1,0,0,0,58,361,1,0,0,0,60,365,1,0,0,0, + 62,369,1,0,0,0,64,460,1,0,0,0,66,467,1,0,0,0,68,469,1,0,0,0,70,491, + 1,0,0,0,72,493,1,0,0,0,74,495,1,0,0,0,76,503,1,0,0,0,78,511,1,0, + 0,0,80,516,1,0,0,0,82,83,3,2,1,0,83,1,1,0,0,0,84,85,6,1,-1,0,85, + 86,3,6,3,0,86,92,1,0,0,0,87,88,10,2,0,0,88,89,7,0,0,0,89,91,3,2, + 1,3,90,87,1,0,0,0,91,94,1,0,0,0,92,90,1,0,0,0,92,93,1,0,0,0,93,3, + 1,0,0,0,94,92,1,0,0,0,95,96,3,6,3,0,96,97,5,79,0,0,97,98,3,6,3,0, + 98,5,1,0,0,0,99,100,3,8,4,0,100,7,1,0,0,0,101,102,6,4,-1,0,102,103, + 3,10,5,0,103,109,1,0,0,0,104,105,10,2,0,0,105,106,7,1,0,0,106,108, + 3,8,4,3,107,104,1,0,0,0,108,111,1,0,0,0,109,107,1,0,0,0,109,110, + 1,0,0,0,110,9,1,0,0,0,111,109,1,0,0,0,112,113,6,5,-1,0,113,114,3, + 14,7,0,114,120,1,0,0,0,115,116,10,2,0,0,116,117,7,2,0,0,117,119, + 3,10,5,3,118,115,1,0,0,0,119,122,1,0,0,0,120,118,1,0,0,0,120,121, + 1,0,0,0,121,11,1,0,0,0,122,120,1,0,0,0,123,124,6,6,-1,0,124,125, + 3,16,8,0,125,131,1,0,0,0,126,127,10,2,0,0,127,128,7,2,0,0,128,130, + 3,12,6,3,129,126,1,0,0,0,130,133,1,0,0,0,131,129,1,0,0,0,131,132, + 1,0,0,0,132,13,1,0,0,0,133,131,1,0,0,0,134,135,7,1,0,0,135,142,3, + 14,7,0,136,138,3,18,9,0,137,136,1,0,0,0,138,139,1,0,0,0,139,137, + 1,0,0,0,139,140,1,0,0,0,140,142,1,0,0,0,141,134,1,0,0,0,141,137, + 1,0,0,0,142,15,1,0,0,0,143,144,7,1,0,0,144,153,3,16,8,0,145,149, + 3,18,9,0,146,148,3,20,10,0,147,146,1,0,0,0,148,151,1,0,0,0,149,147, + 1,0,0,0,149,150,1,0,0,0,150,153,1,0,0,0,151,149,1,0,0,0,152,143, + 1,0,0,0,152,145,1,0,0,0,153,17,1,0,0,0,154,158,3,30,15,0,155,157, + 3,22,11,0,156,155,1,0,0,0,157,160,1,0,0,0,158,156,1,0,0,0,158,159, + 1,0,0,0,159,19,1,0,0,0,160,158,1,0,0,0,161,165,3,32,16,0,162,164, + 3,22,11,0,163,162,1,0,0,0,164,167,1,0,0,0,165,163,1,0,0,0,165,166, + 1,0,0,0,166,21,1,0,0,0,167,165,1,0,0,0,168,171,5,89,0,0,169,171, + 3,24,12,0,170,168,1,0,0,0,170,169,1,0,0,0,171,23,1,0,0,0,172,178, + 5,27,0,0,173,179,3,28,14,0,174,179,3,26,13,0,175,176,3,28,14,0,176, + 177,3,26,13,0,177,179,1,0,0,0,178,173,1,0,0,0,178,174,1,0,0,0,178, + 175,1,0,0,0,179,25,1,0,0,0,180,181,5,73,0,0,181,184,5,21,0,0,182, + 185,3,6,3,0,183,185,3,4,2,0,184,182,1,0,0,0,184,183,1,0,0,0,185, + 186,1,0,0,0,186,187,5,22,0,0,187,27,1,0,0,0,188,189,5,74,0,0,189, + 192,5,21,0,0,190,193,3,6,3,0,191,193,3,4,2,0,192,190,1,0,0,0,192, + 191,1,0,0,0,193,194,1,0,0,0,194,195,5,22,0,0,195,29,1,0,0,0,196, + 197,6,15,-1,0,197,198,3,34,17,0,198,213,1,0,0,0,199,200,10,2,0,0, + 200,206,5,74,0,0,201,207,3,44,22,0,202,203,5,21,0,0,203,204,3,6, + 3,0,204,205,5,22,0,0,205,207,1,0,0,0,206,201,1,0,0,0,206,202,1,0, + 0,0,207,209,1,0,0,0,208,210,3,74,37,0,209,208,1,0,0,0,209,210,1, + 0,0,0,210,212,1,0,0,0,211,199,1,0,0,0,212,215,1,0,0,0,213,211,1, + 0,0,0,213,214,1,0,0,0,214,31,1,0,0,0,215,213,1,0,0,0,216,217,6,16, + -1,0,217,218,3,36,18,0,218,233,1,0,0,0,219,220,10,2,0,0,220,226, + 5,74,0,0,221,227,3,44,22,0,222,223,5,21,0,0,223,224,3,6,3,0,224, + 225,5,22,0,0,225,227,1,0,0,0,226,221,1,0,0,0,226,222,1,0,0,0,227, + 229,1,0,0,0,228,230,3,74,37,0,229,228,1,0,0,0,229,230,1,0,0,0,230, + 232,1,0,0,0,231,219,1,0,0,0,232,235,1,0,0,0,233,231,1,0,0,0,233, + 234,1,0,0,0,234,33,1,0,0,0,235,233,1,0,0,0,236,243,3,38,19,0,237, + 243,3,40,20,0,238,243,3,64,32,0,239,243,3,44,22,0,240,243,3,58,29, + 0,241,243,3,60,30,0,242,236,1,0,0,0,242,237,1,0,0,0,242,238,1,0, + 0,0,242,239,1,0,0,0,242,240,1,0,0,0,242,241,1,0,0,0,243,35,1,0,0, + 0,244,250,3,38,19,0,245,250,3,40,20,0,246,250,3,44,22,0,247,250, + 3,58,29,0,248,250,3,60,30,0,249,244,1,0,0,0,249,245,1,0,0,0,249, + 246,1,0,0,0,249,247,1,0,0,0,249,248,1,0,0,0,250,37,1,0,0,0,251,252, + 5,19,0,0,252,253,3,6,3,0,253,254,5,20,0,0,254,268,1,0,0,0,255,256, + 5,25,0,0,256,257,3,6,3,0,257,258,5,26,0,0,258,268,1,0,0,0,259,260, + 5,21,0,0,260,261,3,6,3,0,261,262,5,22,0,0,262,268,1,0,0,0,263,264, + 5,23,0,0,264,265,3,6,3,0,265,266,5,24,0,0,266,268,1,0,0,0,267,251, + 1,0,0,0,267,255,1,0,0,0,267,259,1,0,0,0,267,263,1,0,0,0,268,39,1, + 0,0,0,269,270,5,27,0,0,270,271,3,6,3,0,271,272,5,27,0,0,272,41,1, + 0,0,0,273,275,5,78,0,0,274,273,1,0,0,0,275,276,1,0,0,0,276,274,1, + 0,0,0,276,277,1,0,0,0,277,284,1,0,0,0,278,279,5,1,0,0,279,280,5, + 78,0,0,280,281,5,78,0,0,281,283,5,78,0,0,282,278,1,0,0,0,283,286, + 1,0,0,0,284,282,1,0,0,0,284,285,1,0,0,0,285,293,1,0,0,0,286,284, + 1,0,0,0,287,289,5,2,0,0,288,290,5,78,0,0,289,288,1,0,0,0,290,291, + 1,0,0,0,291,289,1,0,0,0,291,292,1,0,0,0,292,294,1,0,0,0,293,287, + 1,0,0,0,293,294,1,0,0,0,294,43,1,0,0,0,295,308,7,3,0,0,296,298,3, + 74,37,0,297,296,1,0,0,0,297,298,1,0,0,0,298,300,1,0,0,0,299,301, + 5,90,0,0,300,299,1,0,0,0,300,301,1,0,0,0,301,309,1,0,0,0,302,304, + 5,90,0,0,303,302,1,0,0,0,303,304,1,0,0,0,304,306,1,0,0,0,305,307, + 3,74,37,0,306,305,1,0,0,0,306,307,1,0,0,0,307,309,1,0,0,0,308,297, + 1,0,0,0,308,303,1,0,0,0,309,318,1,0,0,0,310,318,3,42,21,0,311,318, + 5,76,0,0,312,318,3,50,25,0,313,318,3,54,27,0,314,318,3,56,28,0,315, + 318,3,46,23,0,316,318,3,48,24,0,317,295,1,0,0,0,317,310,1,0,0,0, + 317,311,1,0,0,0,317,312,1,0,0,0,317,313,1,0,0,0,317,314,1,0,0,0, + 317,315,1,0,0,0,317,316,1,0,0,0,318,45,1,0,0,0,319,320,5,30,0,0, + 320,321,3,6,3,0,321,322,7,4,0,0,322,47,1,0,0,0,323,324,7,5,0,0,324, + 325,3,6,3,0,325,326,5,31,0,0,326,49,1,0,0,0,327,328,5,72,0,0,328, + 329,5,21,0,0,329,330,3,52,26,0,330,331,5,22,0,0,331,51,1,0,0,0,332, + 334,5,77,0,0,333,332,1,0,0,0,334,337,1,0,0,0,335,333,1,0,0,0,335, + 336,1,0,0,0,336,53,1,0,0,0,337,335,1,0,0,0,338,344,5,68,0,0,339, + 345,5,78,0,0,340,341,5,21,0,0,341,342,3,6,3,0,342,343,5,22,0,0,343, + 345,1,0,0,0,344,339,1,0,0,0,344,340,1,0,0,0,345,351,1,0,0,0,346, + 352,5,78,0,0,347,348,5,21,0,0,348,349,3,6,3,0,349,350,5,22,0,0,350, + 352,1,0,0,0,351,346,1,0,0,0,351,347,1,0,0,0,352,55,1,0,0,0,353,354, + 7,6,0,0,354,355,5,21,0,0,355,356,3,6,3,0,356,357,5,22,0,0,357,358, + 5,21,0,0,358,359,3,6,3,0,359,360,5,22,0,0,360,57,1,0,0,0,361,362, + 5,59,0,0,362,363,3,6,3,0,363,364,5,60,0,0,364,59,1,0,0,0,365,366, + 5,61,0,0,366,367,3,6,3,0,367,368,5,62,0,0,368,61,1,0,0,0,369,370, + 7,7,0,0,370,63,1,0,0,0,371,384,3,62,31,0,372,374,3,74,37,0,373,372, + 1,0,0,0,373,374,1,0,0,0,374,376,1,0,0,0,375,377,3,76,38,0,376,375, + 1,0,0,0,376,377,1,0,0,0,377,385,1,0,0,0,378,380,3,76,38,0,379,378, + 1,0,0,0,379,380,1,0,0,0,380,382,1,0,0,0,381,383,3,74,37,0,382,381, + 1,0,0,0,382,383,1,0,0,0,383,385,1,0,0,0,384,373,1,0,0,0,384,379, + 1,0,0,0,385,391,1,0,0,0,386,387,5,19,0,0,387,388,3,70,35,0,388,389, + 5,20,0,0,389,392,1,0,0,0,390,392,3,72,36,0,391,386,1,0,0,0,391,390, + 1,0,0,0,392,461,1,0,0,0,393,406,7,3,0,0,394,396,3,74,37,0,395,394, + 1,0,0,0,395,396,1,0,0,0,396,398,1,0,0,0,397,399,5,90,0,0,398,397, + 1,0,0,0,398,399,1,0,0,0,399,407,1,0,0,0,400,402,5,90,0,0,401,400, + 1,0,0,0,401,402,1,0,0,0,402,404,1,0,0,0,403,405,3,74,37,0,404,403, + 1,0,0,0,404,405,1,0,0,0,405,407,1,0,0,0,406,395,1,0,0,0,406,401, + 1,0,0,0,407,408,1,0,0,0,408,409,5,19,0,0,409,410,3,66,33,0,410,411, + 5,20,0,0,411,461,1,0,0,0,412,419,5,34,0,0,413,414,3,74,37,0,414, + 415,3,76,38,0,415,420,1,0,0,0,416,417,3,76,38,0,417,418,3,74,37, + 0,418,420,1,0,0,0,419,413,1,0,0,0,419,416,1,0,0,0,419,420,1,0,0, + 0,420,427,1,0,0,0,421,423,3,8,4,0,422,421,1,0,0,0,422,423,1,0,0, + 0,423,424,1,0,0,0,424,428,5,76,0,0,425,428,3,54,27,0,426,428,3,8, + 4,0,427,422,1,0,0,0,427,425,1,0,0,0,427,426,1,0,0,0,428,461,1,0, + 0,0,429,434,5,63,0,0,430,431,5,25,0,0,431,432,3,6,3,0,432,433,5, + 26,0,0,433,435,1,0,0,0,434,430,1,0,0,0,434,435,1,0,0,0,435,436,1, + 0,0,0,436,437,5,21,0,0,437,438,3,6,3,0,438,439,5,22,0,0,439,461, + 1,0,0,0,440,441,5,64,0,0,441,442,5,21,0,0,442,443,3,6,3,0,443,444, + 5,22,0,0,444,461,1,0,0,0,445,452,7,8,0,0,446,447,3,78,39,0,447,448, + 3,76,38,0,448,453,1,0,0,0,449,450,3,76,38,0,450,451,3,78,39,0,451, + 453,1,0,0,0,452,446,1,0,0,0,452,449,1,0,0,0,453,454,1,0,0,0,454, + 455,3,10,5,0,455,461,1,0,0,0,456,457,5,32,0,0,457,458,3,68,34,0, + 458,459,3,10,5,0,459,461,1,0,0,0,460,371,1,0,0,0,460,393,1,0,0,0, + 460,412,1,0,0,0,460,429,1,0,0,0,460,440,1,0,0,0,460,445,1,0,0,0, + 460,456,1,0,0,0,461,65,1,0,0,0,462,463,3,6,3,0,463,464,5,1,0,0,464, + 465,3,66,33,0,465,468,1,0,0,0,466,468,3,6,3,0,467,462,1,0,0,0,467, + 466,1,0,0,0,468,67,1,0,0,0,469,470,5,73,0,0,470,471,5,21,0,0,471, + 472,7,3,0,0,472,473,5,33,0,0,473,482,3,6,3,0,474,480,5,74,0,0,475, + 476,5,21,0,0,476,477,7,1,0,0,477,481,5,22,0,0,478,481,5,15,0,0,479, + 481,5,16,0,0,480,475,1,0,0,0,480,478,1,0,0,0,480,479,1,0,0,0,481, + 483,1,0,0,0,482,474,1,0,0,0,482,483,1,0,0,0,483,484,1,0,0,0,484, + 485,5,22,0,0,485,69,1,0,0,0,486,492,3,6,3,0,487,488,3,6,3,0,488, + 489,5,1,0,0,489,490,3,70,35,0,490,492,1,0,0,0,491,486,1,0,0,0,491, + 487,1,0,0,0,492,71,1,0,0,0,493,494,3,12,6,0,494,73,1,0,0,0,495,501, + 5,73,0,0,496,502,3,44,22,0,497,498,5,21,0,0,498,499,3,6,3,0,499, + 500,5,22,0,0,500,502,1,0,0,0,501,496,1,0,0,0,501,497,1,0,0,0,502, + 75,1,0,0,0,503,509,5,74,0,0,504,510,3,44,22,0,505,506,5,21,0,0,506, + 507,3,6,3,0,507,508,5,22,0,0,508,510,1,0,0,0,509,504,1,0,0,0,509, + 505,1,0,0,0,510,77,1,0,0,0,511,512,5,73,0,0,512,513,5,21,0,0,513, + 514,3,4,2,0,514,515,5,22,0,0,515,79,1,0,0,0,516,517,5,73,0,0,517, + 518,5,21,0,0,518,519,3,4,2,0,519,520,5,22,0,0,520,81,1,0,0,0,59, + 92,109,120,131,139,141,149,152,158,165,170,178,184,192,206,209,213, + 226,229,233,242,249,267,276,284,291,293,297,300,303,306,308,317, + 335,344,351,373,376,379,382,384,391,395,398,401,404,406,419,422, + 427,434,452,460,467,480,482,491,501,509 + ] + +class LaTeXParser ( Parser ): + + grammarFileName = "LaTeX.g4" + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + sharedContextCache = PredictionContextCache() + + literalNames = [ "", "','", "'.'", "", "", + "", "", "'\\quad'", "'\\qquad'", + "", "'\\negmedspace'", "'\\negthickspace'", + "'\\left'", "'\\right'", "", "'+'", "'-'", + "'*'", "'/'", "'('", "')'", "'{'", "'}'", "'\\{'", + "'\\}'", "'['", "']'", "'|'", "'\\right|'", "'\\left|'", + "'\\langle'", "'\\rangle'", "'\\lim'", "", + "", "'\\sum'", "'\\prod'", "'\\exp'", "'\\log'", + "'\\lg'", "'\\ln'", "'\\sin'", "'\\cos'", "'\\tan'", + "'\\csc'", "'\\sec'", "'\\cot'", "'\\arcsin'", "'\\arccos'", + "'\\arctan'", "'\\arccsc'", "'\\arcsec'", "'\\arccot'", + "'\\sinh'", "'\\cosh'", "'\\tanh'", "'\\arsinh'", "'\\arcosh'", + "'\\artanh'", "'\\lfloor'", "'\\rfloor'", "'\\lceil'", + "'\\rceil'", "'\\sqrt'", "'\\overline'", "'\\times'", + "'\\cdot'", "'\\div'", "", "'\\binom'", "'\\dbinom'", + "'\\tbinom'", "'\\mathit'", "'_'", "'^'", "':'", "", + "", "", "", "'\\neq'", "'<'", + "", "'\\leqq'", "'\\leqslant'", "'>'", "", + "'\\geqq'", "'\\geqslant'", "'!'" ] + + symbolicNames = [ "", "", "", "WS", "THINSPACE", + "MEDSPACE", "THICKSPACE", "QUAD", "QQUAD", "NEGTHINSPACE", + "NEGMEDSPACE", "NEGTHICKSPACE", "CMD_LEFT", "CMD_RIGHT", + "IGNORE", "ADD", "SUB", "MUL", "DIV", "L_PAREN", "R_PAREN", + "L_BRACE", "R_BRACE", "L_BRACE_LITERAL", "R_BRACE_LITERAL", + "L_BRACKET", "R_BRACKET", "BAR", "R_BAR", "L_BAR", + "L_ANGLE", "R_ANGLE", "FUNC_LIM", "LIM_APPROACH_SYM", + "FUNC_INT", "FUNC_SUM", "FUNC_PROD", "FUNC_EXP", "FUNC_LOG", + "FUNC_LG", "FUNC_LN", "FUNC_SIN", "FUNC_COS", "FUNC_TAN", + "FUNC_CSC", "FUNC_SEC", "FUNC_COT", "FUNC_ARCSIN", + "FUNC_ARCCOS", "FUNC_ARCTAN", "FUNC_ARCCSC", "FUNC_ARCSEC", + "FUNC_ARCCOT", "FUNC_SINH", "FUNC_COSH", "FUNC_TANH", + "FUNC_ARSINH", "FUNC_ARCOSH", "FUNC_ARTANH", "L_FLOOR", + "R_FLOOR", "L_CEIL", "R_CEIL", "FUNC_SQRT", "FUNC_OVERLINE", + "CMD_TIMES", "CMD_CDOT", "CMD_DIV", "CMD_FRAC", "CMD_BINOM", + "CMD_DBINOM", "CMD_TBINOM", "CMD_MATHIT", "UNDERSCORE", + "CARET", "COLON", "DIFFERENTIAL", "LETTER", "DIGIT", + "EQUAL", "NEQ", "LT", "LTE", "LTE_Q", "LTE_S", "GT", + "GTE", "GTE_Q", "GTE_S", "BANG", "SINGLE_QUOTES", + "SYMBOL" ] + + RULE_math = 0 + RULE_relation = 1 + RULE_equality = 2 + RULE_expr = 3 + RULE_additive = 4 + RULE_mp = 5 + RULE_mp_nofunc = 6 + RULE_unary = 7 + RULE_unary_nofunc = 8 + RULE_postfix = 9 + RULE_postfix_nofunc = 10 + RULE_postfix_op = 11 + RULE_eval_at = 12 + RULE_eval_at_sub = 13 + RULE_eval_at_sup = 14 + RULE_exp = 15 + RULE_exp_nofunc = 16 + RULE_comp = 17 + RULE_comp_nofunc = 18 + RULE_group = 19 + RULE_abs_group = 20 + RULE_number = 21 + RULE_atom = 22 + RULE_bra = 23 + RULE_ket = 24 + RULE_mathit = 25 + RULE_mathit_text = 26 + RULE_frac = 27 + RULE_binom = 28 + RULE_floor = 29 + RULE_ceil = 30 + RULE_func_normal = 31 + RULE_func = 32 + RULE_args = 33 + RULE_limit_sub = 34 + RULE_func_arg = 35 + RULE_func_arg_noparens = 36 + RULE_subexpr = 37 + RULE_supexpr = 38 + RULE_subeq = 39 + RULE_supeq = 40 + + ruleNames = [ "math", "relation", "equality", "expr", "additive", "mp", + "mp_nofunc", "unary", "unary_nofunc", "postfix", "postfix_nofunc", + "postfix_op", "eval_at", "eval_at_sub", "eval_at_sup", + "exp", "exp_nofunc", "comp", "comp_nofunc", "group", + "abs_group", "number", "atom", "bra", "ket", "mathit", + "mathit_text", "frac", "binom", "floor", "ceil", "func_normal", + "func", "args", "limit_sub", "func_arg", "func_arg_noparens", + "subexpr", "supexpr", "subeq", "supeq" ] + + EOF = Token.EOF + T__0=1 + T__1=2 + WS=3 + THINSPACE=4 + MEDSPACE=5 + THICKSPACE=6 + QUAD=7 + QQUAD=8 + NEGTHINSPACE=9 + NEGMEDSPACE=10 + NEGTHICKSPACE=11 + CMD_LEFT=12 + CMD_RIGHT=13 + IGNORE=14 + ADD=15 + SUB=16 + MUL=17 + DIV=18 + L_PAREN=19 + R_PAREN=20 + L_BRACE=21 + R_BRACE=22 + L_BRACE_LITERAL=23 + R_BRACE_LITERAL=24 + L_BRACKET=25 + R_BRACKET=26 + BAR=27 + R_BAR=28 + L_BAR=29 + L_ANGLE=30 + R_ANGLE=31 + FUNC_LIM=32 + LIM_APPROACH_SYM=33 + FUNC_INT=34 + FUNC_SUM=35 + FUNC_PROD=36 + FUNC_EXP=37 + FUNC_LOG=38 + FUNC_LG=39 + FUNC_LN=40 + FUNC_SIN=41 + FUNC_COS=42 + FUNC_TAN=43 + FUNC_CSC=44 + FUNC_SEC=45 + FUNC_COT=46 + FUNC_ARCSIN=47 + FUNC_ARCCOS=48 + FUNC_ARCTAN=49 + FUNC_ARCCSC=50 + FUNC_ARCSEC=51 + FUNC_ARCCOT=52 + FUNC_SINH=53 + FUNC_COSH=54 + FUNC_TANH=55 + FUNC_ARSINH=56 + FUNC_ARCOSH=57 + FUNC_ARTANH=58 + L_FLOOR=59 + R_FLOOR=60 + L_CEIL=61 + R_CEIL=62 + FUNC_SQRT=63 + FUNC_OVERLINE=64 + CMD_TIMES=65 + CMD_CDOT=66 + CMD_DIV=67 + CMD_FRAC=68 + CMD_BINOM=69 + CMD_DBINOM=70 + CMD_TBINOM=71 + CMD_MATHIT=72 + UNDERSCORE=73 + CARET=74 + COLON=75 + DIFFERENTIAL=76 + LETTER=77 + DIGIT=78 + EQUAL=79 + NEQ=80 + LT=81 + LTE=82 + LTE_Q=83 + LTE_S=84 + GT=85 + GTE=86 + GTE_Q=87 + GTE_S=88 + BANG=89 + SINGLE_QUOTES=90 + SYMBOL=91 + + def __init__(self, input:TokenStream, output:TextIO = sys.stdout): + super().__init__(input, output) + self.checkVersion("4.11.1") + self._interp = ParserATNSimulator(self, self.atn, self.decisionsToDFA, self.sharedContextCache) + self._predicates = None + + + + + class MathContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def relation(self): + return self.getTypedRuleContext(LaTeXParser.RelationContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_math + + + + + def math(self): + + localctx = LaTeXParser.MathContext(self, self._ctx, self.state) + self.enterRule(localctx, 0, self.RULE_math) + try: + self.enterOuterAlt(localctx, 1) + self.state = 82 + self.relation(0) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class RelationContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def relation(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(LaTeXParser.RelationContext) + else: + return self.getTypedRuleContext(LaTeXParser.RelationContext,i) + + + def EQUAL(self): + return self.getToken(LaTeXParser.EQUAL, 0) + + def LT(self): + return self.getToken(LaTeXParser.LT, 0) + + def LTE(self): + return self.getToken(LaTeXParser.LTE, 0) + + def GT(self): + return self.getToken(LaTeXParser.GT, 0) + + def GTE(self): + return self.getToken(LaTeXParser.GTE, 0) + + def NEQ(self): + return self.getToken(LaTeXParser.NEQ, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_relation + + + + def relation(self, _p:int=0): + _parentctx = self._ctx + _parentState = self.state + localctx = LaTeXParser.RelationContext(self, self._ctx, _parentState) + _prevctx = localctx + _startState = 2 + self.enterRecursionRule(localctx, 2, self.RULE_relation, _p) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 85 + self.expr() + self._ctx.stop = self._input.LT(-1) + self.state = 92 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,0,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + if self._parseListeners is not None: + self.triggerExitRuleEvent() + _prevctx = localctx + localctx = LaTeXParser.RelationContext(self, _parentctx, _parentState) + self.pushNewRecursionContext(localctx, _startState, self.RULE_relation) + self.state = 87 + if not self.precpred(self._ctx, 2): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 2)") + self.state = 88 + _la = self._input.LA(1) + if not((((_la - 79)) & ~0x3f) == 0 and ((1 << (_la - 79)) & 207) != 0): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 89 + self.relation(3) + self.state = 94 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,0,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.unrollRecursionContexts(_parentctx) + return localctx + + + class EqualityContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(LaTeXParser.ExprContext) + else: + return self.getTypedRuleContext(LaTeXParser.ExprContext,i) + + + def EQUAL(self): + return self.getToken(LaTeXParser.EQUAL, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_equality + + + + + def equality(self): + + localctx = LaTeXParser.EqualityContext(self, self._ctx, self.state) + self.enterRule(localctx, 4, self.RULE_equality) + try: + self.enterOuterAlt(localctx, 1) + self.state = 95 + self.expr() + self.state = 96 + self.match(LaTeXParser.EQUAL) + self.state = 97 + self.expr() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ExprContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def additive(self): + return self.getTypedRuleContext(LaTeXParser.AdditiveContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_expr + + + + + def expr(self): + + localctx = LaTeXParser.ExprContext(self, self._ctx, self.state) + self.enterRule(localctx, 6, self.RULE_expr) + try: + self.enterOuterAlt(localctx, 1) + self.state = 99 + self.additive(0) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class AdditiveContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def mp(self): + return self.getTypedRuleContext(LaTeXParser.MpContext,0) + + + def additive(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(LaTeXParser.AdditiveContext) + else: + return self.getTypedRuleContext(LaTeXParser.AdditiveContext,i) + + + def ADD(self): + return self.getToken(LaTeXParser.ADD, 0) + + def SUB(self): + return self.getToken(LaTeXParser.SUB, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_additive + + + + def additive(self, _p:int=0): + _parentctx = self._ctx + _parentState = self.state + localctx = LaTeXParser.AdditiveContext(self, self._ctx, _parentState) + _prevctx = localctx + _startState = 8 + self.enterRecursionRule(localctx, 8, self.RULE_additive, _p) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 102 + self.mp(0) + self._ctx.stop = self._input.LT(-1) + self.state = 109 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,1,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + if self._parseListeners is not None: + self.triggerExitRuleEvent() + _prevctx = localctx + localctx = LaTeXParser.AdditiveContext(self, _parentctx, _parentState) + self.pushNewRecursionContext(localctx, _startState, self.RULE_additive) + self.state = 104 + if not self.precpred(self._ctx, 2): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 2)") + self.state = 105 + _la = self._input.LA(1) + if not(_la==15 or _la==16): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 106 + self.additive(3) + self.state = 111 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,1,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.unrollRecursionContexts(_parentctx) + return localctx + + + class MpContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def unary(self): + return self.getTypedRuleContext(LaTeXParser.UnaryContext,0) + + + def mp(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(LaTeXParser.MpContext) + else: + return self.getTypedRuleContext(LaTeXParser.MpContext,i) + + + def MUL(self): + return self.getToken(LaTeXParser.MUL, 0) + + def CMD_TIMES(self): + return self.getToken(LaTeXParser.CMD_TIMES, 0) + + def CMD_CDOT(self): + return self.getToken(LaTeXParser.CMD_CDOT, 0) + + def DIV(self): + return self.getToken(LaTeXParser.DIV, 0) + + def CMD_DIV(self): + return self.getToken(LaTeXParser.CMD_DIV, 0) + + def COLON(self): + return self.getToken(LaTeXParser.COLON, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_mp + + + + def mp(self, _p:int=0): + _parentctx = self._ctx + _parentState = self.state + localctx = LaTeXParser.MpContext(self, self._ctx, _parentState) + _prevctx = localctx + _startState = 10 + self.enterRecursionRule(localctx, 10, self.RULE_mp, _p) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 113 + self.unary() + self._ctx.stop = self._input.LT(-1) + self.state = 120 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,2,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + if self._parseListeners is not None: + self.triggerExitRuleEvent() + _prevctx = localctx + localctx = LaTeXParser.MpContext(self, _parentctx, _parentState) + self.pushNewRecursionContext(localctx, _startState, self.RULE_mp) + self.state = 115 + if not self.precpred(self._ctx, 2): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 2)") + self.state = 116 + _la = self._input.LA(1) + if not((((_la - 17)) & ~0x3f) == 0 and ((1 << (_la - 17)) & 290200700988686339) != 0): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 117 + self.mp(3) + self.state = 122 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,2,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.unrollRecursionContexts(_parentctx) + return localctx + + + class Mp_nofuncContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def unary_nofunc(self): + return self.getTypedRuleContext(LaTeXParser.Unary_nofuncContext,0) + + + def mp_nofunc(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(LaTeXParser.Mp_nofuncContext) + else: + return self.getTypedRuleContext(LaTeXParser.Mp_nofuncContext,i) + + + def MUL(self): + return self.getToken(LaTeXParser.MUL, 0) + + def CMD_TIMES(self): + return self.getToken(LaTeXParser.CMD_TIMES, 0) + + def CMD_CDOT(self): + return self.getToken(LaTeXParser.CMD_CDOT, 0) + + def DIV(self): + return self.getToken(LaTeXParser.DIV, 0) + + def CMD_DIV(self): + return self.getToken(LaTeXParser.CMD_DIV, 0) + + def COLON(self): + return self.getToken(LaTeXParser.COLON, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_mp_nofunc + + + + def mp_nofunc(self, _p:int=0): + _parentctx = self._ctx + _parentState = self.state + localctx = LaTeXParser.Mp_nofuncContext(self, self._ctx, _parentState) + _prevctx = localctx + _startState = 12 + self.enterRecursionRule(localctx, 12, self.RULE_mp_nofunc, _p) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 124 + self.unary_nofunc() + self._ctx.stop = self._input.LT(-1) + self.state = 131 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,3,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + if self._parseListeners is not None: + self.triggerExitRuleEvent() + _prevctx = localctx + localctx = LaTeXParser.Mp_nofuncContext(self, _parentctx, _parentState) + self.pushNewRecursionContext(localctx, _startState, self.RULE_mp_nofunc) + self.state = 126 + if not self.precpred(self._ctx, 2): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 2)") + self.state = 127 + _la = self._input.LA(1) + if not((((_la - 17)) & ~0x3f) == 0 and ((1 << (_la - 17)) & 290200700988686339) != 0): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 128 + self.mp_nofunc(3) + self.state = 133 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,3,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.unrollRecursionContexts(_parentctx) + return localctx + + + class UnaryContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def unary(self): + return self.getTypedRuleContext(LaTeXParser.UnaryContext,0) + + + def ADD(self): + return self.getToken(LaTeXParser.ADD, 0) + + def SUB(self): + return self.getToken(LaTeXParser.SUB, 0) + + def postfix(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(LaTeXParser.PostfixContext) + else: + return self.getTypedRuleContext(LaTeXParser.PostfixContext,i) + + + def getRuleIndex(self): + return LaTeXParser.RULE_unary + + + + + def unary(self): + + localctx = LaTeXParser.UnaryContext(self, self._ctx, self.state) + self.enterRule(localctx, 14, self.RULE_unary) + self._la = 0 # Token type + try: + self.state = 141 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [15, 16]: + self.enterOuterAlt(localctx, 1) + self.state = 134 + _la = self._input.LA(1) + if not(_la==15 or _la==16): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 135 + self.unary() + pass + elif token in [19, 21, 23, 25, 27, 29, 30, 32, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 61, 63, 64, 68, 69, 70, 71, 72, 76, 77, 78, 91]: + self.enterOuterAlt(localctx, 2) + self.state = 137 + self._errHandler.sync(self) + _alt = 1 + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt == 1: + self.state = 136 + self.postfix() + + else: + raise NoViableAltException(self) + self.state = 139 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,4,self._ctx) + + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Unary_nofuncContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def unary_nofunc(self): + return self.getTypedRuleContext(LaTeXParser.Unary_nofuncContext,0) + + + def ADD(self): + return self.getToken(LaTeXParser.ADD, 0) + + def SUB(self): + return self.getToken(LaTeXParser.SUB, 0) + + def postfix(self): + return self.getTypedRuleContext(LaTeXParser.PostfixContext,0) + + + def postfix_nofunc(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(LaTeXParser.Postfix_nofuncContext) + else: + return self.getTypedRuleContext(LaTeXParser.Postfix_nofuncContext,i) + + + def getRuleIndex(self): + return LaTeXParser.RULE_unary_nofunc + + + + + def unary_nofunc(self): + + localctx = LaTeXParser.Unary_nofuncContext(self, self._ctx, self.state) + self.enterRule(localctx, 16, self.RULE_unary_nofunc) + self._la = 0 # Token type + try: + self.state = 152 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [15, 16]: + self.enterOuterAlt(localctx, 1) + self.state = 143 + _la = self._input.LA(1) + if not(_la==15 or _la==16): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 144 + self.unary_nofunc() + pass + elif token in [19, 21, 23, 25, 27, 29, 30, 32, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 61, 63, 64, 68, 69, 70, 71, 72, 76, 77, 78, 91]: + self.enterOuterAlt(localctx, 2) + self.state = 145 + self.postfix() + self.state = 149 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,6,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 146 + self.postfix_nofunc() + self.state = 151 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,6,self._ctx) + + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PostfixContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def exp(self): + return self.getTypedRuleContext(LaTeXParser.ExpContext,0) + + + def postfix_op(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(LaTeXParser.Postfix_opContext) + else: + return self.getTypedRuleContext(LaTeXParser.Postfix_opContext,i) + + + def getRuleIndex(self): + return LaTeXParser.RULE_postfix + + + + + def postfix(self): + + localctx = LaTeXParser.PostfixContext(self, self._ctx, self.state) + self.enterRule(localctx, 18, self.RULE_postfix) + try: + self.enterOuterAlt(localctx, 1) + self.state = 154 + self.exp(0) + self.state = 158 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,8,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 155 + self.postfix_op() + self.state = 160 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,8,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Postfix_nofuncContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def exp_nofunc(self): + return self.getTypedRuleContext(LaTeXParser.Exp_nofuncContext,0) + + + def postfix_op(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(LaTeXParser.Postfix_opContext) + else: + return self.getTypedRuleContext(LaTeXParser.Postfix_opContext,i) + + + def getRuleIndex(self): + return LaTeXParser.RULE_postfix_nofunc + + + + + def postfix_nofunc(self): + + localctx = LaTeXParser.Postfix_nofuncContext(self, self._ctx, self.state) + self.enterRule(localctx, 20, self.RULE_postfix_nofunc) + try: + self.enterOuterAlt(localctx, 1) + self.state = 161 + self.exp_nofunc(0) + self.state = 165 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,9,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 162 + self.postfix_op() + self.state = 167 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,9,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Postfix_opContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def BANG(self): + return self.getToken(LaTeXParser.BANG, 0) + + def eval_at(self): + return self.getTypedRuleContext(LaTeXParser.Eval_atContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_postfix_op + + + + + def postfix_op(self): + + localctx = LaTeXParser.Postfix_opContext(self, self._ctx, self.state) + self.enterRule(localctx, 22, self.RULE_postfix_op) + try: + self.state = 170 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [89]: + self.enterOuterAlt(localctx, 1) + self.state = 168 + self.match(LaTeXParser.BANG) + pass + elif token in [27]: + self.enterOuterAlt(localctx, 2) + self.state = 169 + self.eval_at() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Eval_atContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def BAR(self): + return self.getToken(LaTeXParser.BAR, 0) + + def eval_at_sup(self): + return self.getTypedRuleContext(LaTeXParser.Eval_at_supContext,0) + + + def eval_at_sub(self): + return self.getTypedRuleContext(LaTeXParser.Eval_at_subContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_eval_at + + + + + def eval_at(self): + + localctx = LaTeXParser.Eval_atContext(self, self._ctx, self.state) + self.enterRule(localctx, 24, self.RULE_eval_at) + try: + self.enterOuterAlt(localctx, 1) + self.state = 172 + self.match(LaTeXParser.BAR) + self.state = 178 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,11,self._ctx) + if la_ == 1: + self.state = 173 + self.eval_at_sup() + pass + + elif la_ == 2: + self.state = 174 + self.eval_at_sub() + pass + + elif la_ == 3: + self.state = 175 + self.eval_at_sup() + self.state = 176 + self.eval_at_sub() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Eval_at_subContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def UNDERSCORE(self): + return self.getToken(LaTeXParser.UNDERSCORE, 0) + + def L_BRACE(self): + return self.getToken(LaTeXParser.L_BRACE, 0) + + def R_BRACE(self): + return self.getToken(LaTeXParser.R_BRACE, 0) + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def equality(self): + return self.getTypedRuleContext(LaTeXParser.EqualityContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_eval_at_sub + + + + + def eval_at_sub(self): + + localctx = LaTeXParser.Eval_at_subContext(self, self._ctx, self.state) + self.enterRule(localctx, 26, self.RULE_eval_at_sub) + try: + self.enterOuterAlt(localctx, 1) + self.state = 180 + self.match(LaTeXParser.UNDERSCORE) + self.state = 181 + self.match(LaTeXParser.L_BRACE) + self.state = 184 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,12,self._ctx) + if la_ == 1: + self.state = 182 + self.expr() + pass + + elif la_ == 2: + self.state = 183 + self.equality() + pass + + + self.state = 186 + self.match(LaTeXParser.R_BRACE) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Eval_at_supContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def CARET(self): + return self.getToken(LaTeXParser.CARET, 0) + + def L_BRACE(self): + return self.getToken(LaTeXParser.L_BRACE, 0) + + def R_BRACE(self): + return self.getToken(LaTeXParser.R_BRACE, 0) + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def equality(self): + return self.getTypedRuleContext(LaTeXParser.EqualityContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_eval_at_sup + + + + + def eval_at_sup(self): + + localctx = LaTeXParser.Eval_at_supContext(self, self._ctx, self.state) + self.enterRule(localctx, 28, self.RULE_eval_at_sup) + try: + self.enterOuterAlt(localctx, 1) + self.state = 188 + self.match(LaTeXParser.CARET) + self.state = 189 + self.match(LaTeXParser.L_BRACE) + self.state = 192 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,13,self._ctx) + if la_ == 1: + self.state = 190 + self.expr() + pass + + elif la_ == 2: + self.state = 191 + self.equality() + pass + + + self.state = 194 + self.match(LaTeXParser.R_BRACE) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ExpContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def comp(self): + return self.getTypedRuleContext(LaTeXParser.CompContext,0) + + + def exp(self): + return self.getTypedRuleContext(LaTeXParser.ExpContext,0) + + + def CARET(self): + return self.getToken(LaTeXParser.CARET, 0) + + def atom(self): + return self.getTypedRuleContext(LaTeXParser.AtomContext,0) + + + def L_BRACE(self): + return self.getToken(LaTeXParser.L_BRACE, 0) + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def R_BRACE(self): + return self.getToken(LaTeXParser.R_BRACE, 0) + + def subexpr(self): + return self.getTypedRuleContext(LaTeXParser.SubexprContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_exp + + + + def exp(self, _p:int=0): + _parentctx = self._ctx + _parentState = self.state + localctx = LaTeXParser.ExpContext(self, self._ctx, _parentState) + _prevctx = localctx + _startState = 30 + self.enterRecursionRule(localctx, 30, self.RULE_exp, _p) + try: + self.enterOuterAlt(localctx, 1) + self.state = 197 + self.comp() + self._ctx.stop = self._input.LT(-1) + self.state = 213 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,16,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + if self._parseListeners is not None: + self.triggerExitRuleEvent() + _prevctx = localctx + localctx = LaTeXParser.ExpContext(self, _parentctx, _parentState) + self.pushNewRecursionContext(localctx, _startState, self.RULE_exp) + self.state = 199 + if not self.precpred(self._ctx, 2): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 2)") + self.state = 200 + self.match(LaTeXParser.CARET) + self.state = 206 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [27, 29, 30, 68, 69, 70, 71, 72, 76, 77, 78, 91]: + self.state = 201 + self.atom() + pass + elif token in [21]: + self.state = 202 + self.match(LaTeXParser.L_BRACE) + self.state = 203 + self.expr() + self.state = 204 + self.match(LaTeXParser.R_BRACE) + pass + else: + raise NoViableAltException(self) + + self.state = 209 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,15,self._ctx) + if la_ == 1: + self.state = 208 + self.subexpr() + + + self.state = 215 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,16,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.unrollRecursionContexts(_parentctx) + return localctx + + + class Exp_nofuncContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def comp_nofunc(self): + return self.getTypedRuleContext(LaTeXParser.Comp_nofuncContext,0) + + + def exp_nofunc(self): + return self.getTypedRuleContext(LaTeXParser.Exp_nofuncContext,0) + + + def CARET(self): + return self.getToken(LaTeXParser.CARET, 0) + + def atom(self): + return self.getTypedRuleContext(LaTeXParser.AtomContext,0) + + + def L_BRACE(self): + return self.getToken(LaTeXParser.L_BRACE, 0) + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def R_BRACE(self): + return self.getToken(LaTeXParser.R_BRACE, 0) + + def subexpr(self): + return self.getTypedRuleContext(LaTeXParser.SubexprContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_exp_nofunc + + + + def exp_nofunc(self, _p:int=0): + _parentctx = self._ctx + _parentState = self.state + localctx = LaTeXParser.Exp_nofuncContext(self, self._ctx, _parentState) + _prevctx = localctx + _startState = 32 + self.enterRecursionRule(localctx, 32, self.RULE_exp_nofunc, _p) + try: + self.enterOuterAlt(localctx, 1) + self.state = 217 + self.comp_nofunc() + self._ctx.stop = self._input.LT(-1) + self.state = 233 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,19,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + if self._parseListeners is not None: + self.triggerExitRuleEvent() + _prevctx = localctx + localctx = LaTeXParser.Exp_nofuncContext(self, _parentctx, _parentState) + self.pushNewRecursionContext(localctx, _startState, self.RULE_exp_nofunc) + self.state = 219 + if not self.precpred(self._ctx, 2): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 2)") + self.state = 220 + self.match(LaTeXParser.CARET) + self.state = 226 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [27, 29, 30, 68, 69, 70, 71, 72, 76, 77, 78, 91]: + self.state = 221 + self.atom() + pass + elif token in [21]: + self.state = 222 + self.match(LaTeXParser.L_BRACE) + self.state = 223 + self.expr() + self.state = 224 + self.match(LaTeXParser.R_BRACE) + pass + else: + raise NoViableAltException(self) + + self.state = 229 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,18,self._ctx) + if la_ == 1: + self.state = 228 + self.subexpr() + + + self.state = 235 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,19,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.unrollRecursionContexts(_parentctx) + return localctx + + + class CompContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def group(self): + return self.getTypedRuleContext(LaTeXParser.GroupContext,0) + + + def abs_group(self): + return self.getTypedRuleContext(LaTeXParser.Abs_groupContext,0) + + + def func(self): + return self.getTypedRuleContext(LaTeXParser.FuncContext,0) + + + def atom(self): + return self.getTypedRuleContext(LaTeXParser.AtomContext,0) + + + def floor(self): + return self.getTypedRuleContext(LaTeXParser.FloorContext,0) + + + def ceil(self): + return self.getTypedRuleContext(LaTeXParser.CeilContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_comp + + + + + def comp(self): + + localctx = LaTeXParser.CompContext(self, self._ctx, self.state) + self.enterRule(localctx, 34, self.RULE_comp) + try: + self.state = 242 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,20,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 236 + self.group() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 237 + self.abs_group() + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 238 + self.func() + pass + + elif la_ == 4: + self.enterOuterAlt(localctx, 4) + self.state = 239 + self.atom() + pass + + elif la_ == 5: + self.enterOuterAlt(localctx, 5) + self.state = 240 + self.floor() + pass + + elif la_ == 6: + self.enterOuterAlt(localctx, 6) + self.state = 241 + self.ceil() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Comp_nofuncContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def group(self): + return self.getTypedRuleContext(LaTeXParser.GroupContext,0) + + + def abs_group(self): + return self.getTypedRuleContext(LaTeXParser.Abs_groupContext,0) + + + def atom(self): + return self.getTypedRuleContext(LaTeXParser.AtomContext,0) + + + def floor(self): + return self.getTypedRuleContext(LaTeXParser.FloorContext,0) + + + def ceil(self): + return self.getTypedRuleContext(LaTeXParser.CeilContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_comp_nofunc + + + + + def comp_nofunc(self): + + localctx = LaTeXParser.Comp_nofuncContext(self, self._ctx, self.state) + self.enterRule(localctx, 36, self.RULE_comp_nofunc) + try: + self.state = 249 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,21,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 244 + self.group() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 245 + self.abs_group() + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 246 + self.atom() + pass + + elif la_ == 4: + self.enterOuterAlt(localctx, 4) + self.state = 247 + self.floor() + pass + + elif la_ == 5: + self.enterOuterAlt(localctx, 5) + self.state = 248 + self.ceil() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class GroupContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def L_PAREN(self): + return self.getToken(LaTeXParser.L_PAREN, 0) + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def R_PAREN(self): + return self.getToken(LaTeXParser.R_PAREN, 0) + + def L_BRACKET(self): + return self.getToken(LaTeXParser.L_BRACKET, 0) + + def R_BRACKET(self): + return self.getToken(LaTeXParser.R_BRACKET, 0) + + def L_BRACE(self): + return self.getToken(LaTeXParser.L_BRACE, 0) + + def R_BRACE(self): + return self.getToken(LaTeXParser.R_BRACE, 0) + + def L_BRACE_LITERAL(self): + return self.getToken(LaTeXParser.L_BRACE_LITERAL, 0) + + def R_BRACE_LITERAL(self): + return self.getToken(LaTeXParser.R_BRACE_LITERAL, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_group + + + + + def group(self): + + localctx = LaTeXParser.GroupContext(self, self._ctx, self.state) + self.enterRule(localctx, 38, self.RULE_group) + try: + self.state = 267 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [19]: + self.enterOuterAlt(localctx, 1) + self.state = 251 + self.match(LaTeXParser.L_PAREN) + self.state = 252 + self.expr() + self.state = 253 + self.match(LaTeXParser.R_PAREN) + pass + elif token in [25]: + self.enterOuterAlt(localctx, 2) + self.state = 255 + self.match(LaTeXParser.L_BRACKET) + self.state = 256 + self.expr() + self.state = 257 + self.match(LaTeXParser.R_BRACKET) + pass + elif token in [21]: + self.enterOuterAlt(localctx, 3) + self.state = 259 + self.match(LaTeXParser.L_BRACE) + self.state = 260 + self.expr() + self.state = 261 + self.match(LaTeXParser.R_BRACE) + pass + elif token in [23]: + self.enterOuterAlt(localctx, 4) + self.state = 263 + self.match(LaTeXParser.L_BRACE_LITERAL) + self.state = 264 + self.expr() + self.state = 265 + self.match(LaTeXParser.R_BRACE_LITERAL) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Abs_groupContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def BAR(self, i:int=None): + if i is None: + return self.getTokens(LaTeXParser.BAR) + else: + return self.getToken(LaTeXParser.BAR, i) + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_abs_group + + + + + def abs_group(self): + + localctx = LaTeXParser.Abs_groupContext(self, self._ctx, self.state) + self.enterRule(localctx, 40, self.RULE_abs_group) + try: + self.enterOuterAlt(localctx, 1) + self.state = 269 + self.match(LaTeXParser.BAR) + self.state = 270 + self.expr() + self.state = 271 + self.match(LaTeXParser.BAR) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class NumberContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def DIGIT(self, i:int=None): + if i is None: + return self.getTokens(LaTeXParser.DIGIT) + else: + return self.getToken(LaTeXParser.DIGIT, i) + + def getRuleIndex(self): + return LaTeXParser.RULE_number + + + + + def number(self): + + localctx = LaTeXParser.NumberContext(self, self._ctx, self.state) + self.enterRule(localctx, 42, self.RULE_number) + try: + self.enterOuterAlt(localctx, 1) + self.state = 274 + self._errHandler.sync(self) + _alt = 1 + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt == 1: + self.state = 273 + self.match(LaTeXParser.DIGIT) + + else: + raise NoViableAltException(self) + self.state = 276 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,23,self._ctx) + + self.state = 284 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,24,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 278 + self.match(LaTeXParser.T__0) + self.state = 279 + self.match(LaTeXParser.DIGIT) + self.state = 280 + self.match(LaTeXParser.DIGIT) + self.state = 281 + self.match(LaTeXParser.DIGIT) + self.state = 286 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,24,self._ctx) + + self.state = 293 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,26,self._ctx) + if la_ == 1: + self.state = 287 + self.match(LaTeXParser.T__1) + self.state = 289 + self._errHandler.sync(self) + _alt = 1 + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt == 1: + self.state = 288 + self.match(LaTeXParser.DIGIT) + + else: + raise NoViableAltException(self) + self.state = 291 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,25,self._ctx) + + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class AtomContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def LETTER(self): + return self.getToken(LaTeXParser.LETTER, 0) + + def SYMBOL(self): + return self.getToken(LaTeXParser.SYMBOL, 0) + + def subexpr(self): + return self.getTypedRuleContext(LaTeXParser.SubexprContext,0) + + + def SINGLE_QUOTES(self): + return self.getToken(LaTeXParser.SINGLE_QUOTES, 0) + + def number(self): + return self.getTypedRuleContext(LaTeXParser.NumberContext,0) + + + def DIFFERENTIAL(self): + return self.getToken(LaTeXParser.DIFFERENTIAL, 0) + + def mathit(self): + return self.getTypedRuleContext(LaTeXParser.MathitContext,0) + + + def frac(self): + return self.getTypedRuleContext(LaTeXParser.FracContext,0) + + + def binom(self): + return self.getTypedRuleContext(LaTeXParser.BinomContext,0) + + + def bra(self): + return self.getTypedRuleContext(LaTeXParser.BraContext,0) + + + def ket(self): + return self.getTypedRuleContext(LaTeXParser.KetContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_atom + + + + + def atom(self): + + localctx = LaTeXParser.AtomContext(self, self._ctx, self.state) + self.enterRule(localctx, 44, self.RULE_atom) + self._la = 0 # Token type + try: + self.state = 317 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [77, 91]: + self.enterOuterAlt(localctx, 1) + self.state = 295 + _la = self._input.LA(1) + if not(_la==77 or _la==91): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 308 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,31,self._ctx) + if la_ == 1: + self.state = 297 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,27,self._ctx) + if la_ == 1: + self.state = 296 + self.subexpr() + + + self.state = 300 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,28,self._ctx) + if la_ == 1: + self.state = 299 + self.match(LaTeXParser.SINGLE_QUOTES) + + + pass + + elif la_ == 2: + self.state = 303 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,29,self._ctx) + if la_ == 1: + self.state = 302 + self.match(LaTeXParser.SINGLE_QUOTES) + + + self.state = 306 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,30,self._ctx) + if la_ == 1: + self.state = 305 + self.subexpr() + + + pass + + + pass + elif token in [78]: + self.enterOuterAlt(localctx, 2) + self.state = 310 + self.number() + pass + elif token in [76]: + self.enterOuterAlt(localctx, 3) + self.state = 311 + self.match(LaTeXParser.DIFFERENTIAL) + pass + elif token in [72]: + self.enterOuterAlt(localctx, 4) + self.state = 312 + self.mathit() + pass + elif token in [68]: + self.enterOuterAlt(localctx, 5) + self.state = 313 + self.frac() + pass + elif token in [69, 70, 71]: + self.enterOuterAlt(localctx, 6) + self.state = 314 + self.binom() + pass + elif token in [30]: + self.enterOuterAlt(localctx, 7) + self.state = 315 + self.bra() + pass + elif token in [27, 29]: + self.enterOuterAlt(localctx, 8) + self.state = 316 + self.ket() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class BraContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def L_ANGLE(self): + return self.getToken(LaTeXParser.L_ANGLE, 0) + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def R_BAR(self): + return self.getToken(LaTeXParser.R_BAR, 0) + + def BAR(self): + return self.getToken(LaTeXParser.BAR, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_bra + + + + + def bra(self): + + localctx = LaTeXParser.BraContext(self, self._ctx, self.state) + self.enterRule(localctx, 46, self.RULE_bra) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 319 + self.match(LaTeXParser.L_ANGLE) + self.state = 320 + self.expr() + self.state = 321 + _la = self._input.LA(1) + if not(_la==27 or _la==28): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class KetContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def R_ANGLE(self): + return self.getToken(LaTeXParser.R_ANGLE, 0) + + def L_BAR(self): + return self.getToken(LaTeXParser.L_BAR, 0) + + def BAR(self): + return self.getToken(LaTeXParser.BAR, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_ket + + + + + def ket(self): + + localctx = LaTeXParser.KetContext(self, self._ctx, self.state) + self.enterRule(localctx, 48, self.RULE_ket) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 323 + _la = self._input.LA(1) + if not(_la==27 or _la==29): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 324 + self.expr() + self.state = 325 + self.match(LaTeXParser.R_ANGLE) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class MathitContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def CMD_MATHIT(self): + return self.getToken(LaTeXParser.CMD_MATHIT, 0) + + def L_BRACE(self): + return self.getToken(LaTeXParser.L_BRACE, 0) + + def mathit_text(self): + return self.getTypedRuleContext(LaTeXParser.Mathit_textContext,0) + + + def R_BRACE(self): + return self.getToken(LaTeXParser.R_BRACE, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_mathit + + + + + def mathit(self): + + localctx = LaTeXParser.MathitContext(self, self._ctx, self.state) + self.enterRule(localctx, 50, self.RULE_mathit) + try: + self.enterOuterAlt(localctx, 1) + self.state = 327 + self.match(LaTeXParser.CMD_MATHIT) + self.state = 328 + self.match(LaTeXParser.L_BRACE) + self.state = 329 + self.mathit_text() + self.state = 330 + self.match(LaTeXParser.R_BRACE) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Mathit_textContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def LETTER(self, i:int=None): + if i is None: + return self.getTokens(LaTeXParser.LETTER) + else: + return self.getToken(LaTeXParser.LETTER, i) + + def getRuleIndex(self): + return LaTeXParser.RULE_mathit_text + + + + + def mathit_text(self): + + localctx = LaTeXParser.Mathit_textContext(self, self._ctx, self.state) + self.enterRule(localctx, 52, self.RULE_mathit_text) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 335 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==77: + self.state = 332 + self.match(LaTeXParser.LETTER) + self.state = 337 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class FracContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.upperd = None # Token + self.upper = None # ExprContext + self.lowerd = None # Token + self.lower = None # ExprContext + + def CMD_FRAC(self): + return self.getToken(LaTeXParser.CMD_FRAC, 0) + + def L_BRACE(self, i:int=None): + if i is None: + return self.getTokens(LaTeXParser.L_BRACE) + else: + return self.getToken(LaTeXParser.L_BRACE, i) + + def R_BRACE(self, i:int=None): + if i is None: + return self.getTokens(LaTeXParser.R_BRACE) + else: + return self.getToken(LaTeXParser.R_BRACE, i) + + def DIGIT(self, i:int=None): + if i is None: + return self.getTokens(LaTeXParser.DIGIT) + else: + return self.getToken(LaTeXParser.DIGIT, i) + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(LaTeXParser.ExprContext) + else: + return self.getTypedRuleContext(LaTeXParser.ExprContext,i) + + + def getRuleIndex(self): + return LaTeXParser.RULE_frac + + + + + def frac(self): + + localctx = LaTeXParser.FracContext(self, self._ctx, self.state) + self.enterRule(localctx, 54, self.RULE_frac) + try: + self.enterOuterAlt(localctx, 1) + self.state = 338 + self.match(LaTeXParser.CMD_FRAC) + self.state = 344 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [78]: + self.state = 339 + localctx.upperd = self.match(LaTeXParser.DIGIT) + pass + elif token in [21]: + self.state = 340 + self.match(LaTeXParser.L_BRACE) + self.state = 341 + localctx.upper = self.expr() + self.state = 342 + self.match(LaTeXParser.R_BRACE) + pass + else: + raise NoViableAltException(self) + + self.state = 351 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [78]: + self.state = 346 + localctx.lowerd = self.match(LaTeXParser.DIGIT) + pass + elif token in [21]: + self.state = 347 + self.match(LaTeXParser.L_BRACE) + self.state = 348 + localctx.lower = self.expr() + self.state = 349 + self.match(LaTeXParser.R_BRACE) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class BinomContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.n = None # ExprContext + self.k = None # ExprContext + + def L_BRACE(self, i:int=None): + if i is None: + return self.getTokens(LaTeXParser.L_BRACE) + else: + return self.getToken(LaTeXParser.L_BRACE, i) + + def R_BRACE(self, i:int=None): + if i is None: + return self.getTokens(LaTeXParser.R_BRACE) + else: + return self.getToken(LaTeXParser.R_BRACE, i) + + def CMD_BINOM(self): + return self.getToken(LaTeXParser.CMD_BINOM, 0) + + def CMD_DBINOM(self): + return self.getToken(LaTeXParser.CMD_DBINOM, 0) + + def CMD_TBINOM(self): + return self.getToken(LaTeXParser.CMD_TBINOM, 0) + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(LaTeXParser.ExprContext) + else: + return self.getTypedRuleContext(LaTeXParser.ExprContext,i) + + + def getRuleIndex(self): + return LaTeXParser.RULE_binom + + + + + def binom(self): + + localctx = LaTeXParser.BinomContext(self, self._ctx, self.state) + self.enterRule(localctx, 56, self.RULE_binom) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 353 + _la = self._input.LA(1) + if not((((_la - 69)) & ~0x3f) == 0 and ((1 << (_la - 69)) & 7) != 0): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 354 + self.match(LaTeXParser.L_BRACE) + self.state = 355 + localctx.n = self.expr() + self.state = 356 + self.match(LaTeXParser.R_BRACE) + self.state = 357 + self.match(LaTeXParser.L_BRACE) + self.state = 358 + localctx.k = self.expr() + self.state = 359 + self.match(LaTeXParser.R_BRACE) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class FloorContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.val = None # ExprContext + + def L_FLOOR(self): + return self.getToken(LaTeXParser.L_FLOOR, 0) + + def R_FLOOR(self): + return self.getToken(LaTeXParser.R_FLOOR, 0) + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_floor + + + + + def floor(self): + + localctx = LaTeXParser.FloorContext(self, self._ctx, self.state) + self.enterRule(localctx, 58, self.RULE_floor) + try: + self.enterOuterAlt(localctx, 1) + self.state = 361 + self.match(LaTeXParser.L_FLOOR) + self.state = 362 + localctx.val = self.expr() + self.state = 363 + self.match(LaTeXParser.R_FLOOR) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class CeilContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.val = None # ExprContext + + def L_CEIL(self): + return self.getToken(LaTeXParser.L_CEIL, 0) + + def R_CEIL(self): + return self.getToken(LaTeXParser.R_CEIL, 0) + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_ceil + + + + + def ceil(self): + + localctx = LaTeXParser.CeilContext(self, self._ctx, self.state) + self.enterRule(localctx, 60, self.RULE_ceil) + try: + self.enterOuterAlt(localctx, 1) + self.state = 365 + self.match(LaTeXParser.L_CEIL) + self.state = 366 + localctx.val = self.expr() + self.state = 367 + self.match(LaTeXParser.R_CEIL) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Func_normalContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def FUNC_EXP(self): + return self.getToken(LaTeXParser.FUNC_EXP, 0) + + def FUNC_LOG(self): + return self.getToken(LaTeXParser.FUNC_LOG, 0) + + def FUNC_LG(self): + return self.getToken(LaTeXParser.FUNC_LG, 0) + + def FUNC_LN(self): + return self.getToken(LaTeXParser.FUNC_LN, 0) + + def FUNC_SIN(self): + return self.getToken(LaTeXParser.FUNC_SIN, 0) + + def FUNC_COS(self): + return self.getToken(LaTeXParser.FUNC_COS, 0) + + def FUNC_TAN(self): + return self.getToken(LaTeXParser.FUNC_TAN, 0) + + def FUNC_CSC(self): + return self.getToken(LaTeXParser.FUNC_CSC, 0) + + def FUNC_SEC(self): + return self.getToken(LaTeXParser.FUNC_SEC, 0) + + def FUNC_COT(self): + return self.getToken(LaTeXParser.FUNC_COT, 0) + + def FUNC_ARCSIN(self): + return self.getToken(LaTeXParser.FUNC_ARCSIN, 0) + + def FUNC_ARCCOS(self): + return self.getToken(LaTeXParser.FUNC_ARCCOS, 0) + + def FUNC_ARCTAN(self): + return self.getToken(LaTeXParser.FUNC_ARCTAN, 0) + + def FUNC_ARCCSC(self): + return self.getToken(LaTeXParser.FUNC_ARCCSC, 0) + + def FUNC_ARCSEC(self): + return self.getToken(LaTeXParser.FUNC_ARCSEC, 0) + + def FUNC_ARCCOT(self): + return self.getToken(LaTeXParser.FUNC_ARCCOT, 0) + + def FUNC_SINH(self): + return self.getToken(LaTeXParser.FUNC_SINH, 0) + + def FUNC_COSH(self): + return self.getToken(LaTeXParser.FUNC_COSH, 0) + + def FUNC_TANH(self): + return self.getToken(LaTeXParser.FUNC_TANH, 0) + + def FUNC_ARSINH(self): + return self.getToken(LaTeXParser.FUNC_ARSINH, 0) + + def FUNC_ARCOSH(self): + return self.getToken(LaTeXParser.FUNC_ARCOSH, 0) + + def FUNC_ARTANH(self): + return self.getToken(LaTeXParser.FUNC_ARTANH, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_func_normal + + + + + def func_normal(self): + + localctx = LaTeXParser.Func_normalContext(self, self._ctx, self.state) + self.enterRule(localctx, 62, self.RULE_func_normal) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 369 + _la = self._input.LA(1) + if not(((_la) & ~0x3f) == 0 and ((1 << _la) & 576460614864470016) != 0): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class FuncContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.root = None # ExprContext + self.base = None # ExprContext + + def func_normal(self): + return self.getTypedRuleContext(LaTeXParser.Func_normalContext,0) + + + def L_PAREN(self): + return self.getToken(LaTeXParser.L_PAREN, 0) + + def func_arg(self): + return self.getTypedRuleContext(LaTeXParser.Func_argContext,0) + + + def R_PAREN(self): + return self.getToken(LaTeXParser.R_PAREN, 0) + + def func_arg_noparens(self): + return self.getTypedRuleContext(LaTeXParser.Func_arg_noparensContext,0) + + + def subexpr(self): + return self.getTypedRuleContext(LaTeXParser.SubexprContext,0) + + + def supexpr(self): + return self.getTypedRuleContext(LaTeXParser.SupexprContext,0) + + + def args(self): + return self.getTypedRuleContext(LaTeXParser.ArgsContext,0) + + + def LETTER(self): + return self.getToken(LaTeXParser.LETTER, 0) + + def SYMBOL(self): + return self.getToken(LaTeXParser.SYMBOL, 0) + + def SINGLE_QUOTES(self): + return self.getToken(LaTeXParser.SINGLE_QUOTES, 0) + + def FUNC_INT(self): + return self.getToken(LaTeXParser.FUNC_INT, 0) + + def DIFFERENTIAL(self): + return self.getToken(LaTeXParser.DIFFERENTIAL, 0) + + def frac(self): + return self.getTypedRuleContext(LaTeXParser.FracContext,0) + + + def additive(self): + return self.getTypedRuleContext(LaTeXParser.AdditiveContext,0) + + + def FUNC_SQRT(self): + return self.getToken(LaTeXParser.FUNC_SQRT, 0) + + def L_BRACE(self): + return self.getToken(LaTeXParser.L_BRACE, 0) + + def R_BRACE(self): + return self.getToken(LaTeXParser.R_BRACE, 0) + + def expr(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(LaTeXParser.ExprContext) + else: + return self.getTypedRuleContext(LaTeXParser.ExprContext,i) + + + def L_BRACKET(self): + return self.getToken(LaTeXParser.L_BRACKET, 0) + + def R_BRACKET(self): + return self.getToken(LaTeXParser.R_BRACKET, 0) + + def FUNC_OVERLINE(self): + return self.getToken(LaTeXParser.FUNC_OVERLINE, 0) + + def mp(self): + return self.getTypedRuleContext(LaTeXParser.MpContext,0) + + + def FUNC_SUM(self): + return self.getToken(LaTeXParser.FUNC_SUM, 0) + + def FUNC_PROD(self): + return self.getToken(LaTeXParser.FUNC_PROD, 0) + + def subeq(self): + return self.getTypedRuleContext(LaTeXParser.SubeqContext,0) + + + def FUNC_LIM(self): + return self.getToken(LaTeXParser.FUNC_LIM, 0) + + def limit_sub(self): + return self.getTypedRuleContext(LaTeXParser.Limit_subContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_func + + + + + def func(self): + + localctx = LaTeXParser.FuncContext(self, self._ctx, self.state) + self.enterRule(localctx, 64, self.RULE_func) + self._la = 0 # Token type + try: + self.state = 460 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58]: + self.enterOuterAlt(localctx, 1) + self.state = 371 + self.func_normal() + self.state = 384 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,40,self._ctx) + if la_ == 1: + self.state = 373 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==73: + self.state = 372 + self.subexpr() + + + self.state = 376 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==74: + self.state = 375 + self.supexpr() + + + pass + + elif la_ == 2: + self.state = 379 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==74: + self.state = 378 + self.supexpr() + + + self.state = 382 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==73: + self.state = 381 + self.subexpr() + + + pass + + + self.state = 391 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,41,self._ctx) + if la_ == 1: + self.state = 386 + self.match(LaTeXParser.L_PAREN) + self.state = 387 + self.func_arg() + self.state = 388 + self.match(LaTeXParser.R_PAREN) + pass + + elif la_ == 2: + self.state = 390 + self.func_arg_noparens() + pass + + + pass + elif token in [77, 91]: + self.enterOuterAlt(localctx, 2) + self.state = 393 + _la = self._input.LA(1) + if not(_la==77 or _la==91): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 406 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,46,self._ctx) + if la_ == 1: + self.state = 395 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==73: + self.state = 394 + self.subexpr() + + + self.state = 398 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==90: + self.state = 397 + self.match(LaTeXParser.SINGLE_QUOTES) + + + pass + + elif la_ == 2: + self.state = 401 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==90: + self.state = 400 + self.match(LaTeXParser.SINGLE_QUOTES) + + + self.state = 404 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==73: + self.state = 403 + self.subexpr() + + + pass + + + self.state = 408 + self.match(LaTeXParser.L_PAREN) + self.state = 409 + self.args() + self.state = 410 + self.match(LaTeXParser.R_PAREN) + pass + elif token in [34]: + self.enterOuterAlt(localctx, 3) + self.state = 412 + self.match(LaTeXParser.FUNC_INT) + self.state = 419 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [73]: + self.state = 413 + self.subexpr() + self.state = 414 + self.supexpr() + pass + elif token in [74]: + self.state = 416 + self.supexpr() + self.state = 417 + self.subexpr() + pass + elif token in [15, 16, 19, 21, 23, 25, 27, 29, 30, 32, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 61, 63, 64, 68, 69, 70, 71, 72, 76, 77, 78, 91]: + pass + else: + pass + self.state = 427 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,49,self._ctx) + if la_ == 1: + self.state = 422 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,48,self._ctx) + if la_ == 1: + self.state = 421 + self.additive(0) + + + self.state = 424 + self.match(LaTeXParser.DIFFERENTIAL) + pass + + elif la_ == 2: + self.state = 425 + self.frac() + pass + + elif la_ == 3: + self.state = 426 + self.additive(0) + pass + + + pass + elif token in [63]: + self.enterOuterAlt(localctx, 4) + self.state = 429 + self.match(LaTeXParser.FUNC_SQRT) + self.state = 434 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==25: + self.state = 430 + self.match(LaTeXParser.L_BRACKET) + self.state = 431 + localctx.root = self.expr() + self.state = 432 + self.match(LaTeXParser.R_BRACKET) + + + self.state = 436 + self.match(LaTeXParser.L_BRACE) + self.state = 437 + localctx.base = self.expr() + self.state = 438 + self.match(LaTeXParser.R_BRACE) + pass + elif token in [64]: + self.enterOuterAlt(localctx, 5) + self.state = 440 + self.match(LaTeXParser.FUNC_OVERLINE) + self.state = 441 + self.match(LaTeXParser.L_BRACE) + self.state = 442 + localctx.base = self.expr() + self.state = 443 + self.match(LaTeXParser.R_BRACE) + pass + elif token in [35, 36]: + self.enterOuterAlt(localctx, 6) + self.state = 445 + _la = self._input.LA(1) + if not(_la==35 or _la==36): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 452 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [73]: + self.state = 446 + self.subeq() + self.state = 447 + self.supexpr() + pass + elif token in [74]: + self.state = 449 + self.supexpr() + self.state = 450 + self.subeq() + pass + else: + raise NoViableAltException(self) + + self.state = 454 + self.mp(0) + pass + elif token in [32]: + self.enterOuterAlt(localctx, 7) + self.state = 456 + self.match(LaTeXParser.FUNC_LIM) + self.state = 457 + self.limit_sub() + self.state = 458 + self.mp(0) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ArgsContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def args(self): + return self.getTypedRuleContext(LaTeXParser.ArgsContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_args + + + + + def args(self): + + localctx = LaTeXParser.ArgsContext(self, self._ctx, self.state) + self.enterRule(localctx, 66, self.RULE_args) + try: + self.state = 467 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,53,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 462 + self.expr() + self.state = 463 + self.match(LaTeXParser.T__0) + self.state = 464 + self.args() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 466 + self.expr() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Limit_subContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def UNDERSCORE(self): + return self.getToken(LaTeXParser.UNDERSCORE, 0) + + def L_BRACE(self, i:int=None): + if i is None: + return self.getTokens(LaTeXParser.L_BRACE) + else: + return self.getToken(LaTeXParser.L_BRACE, i) + + def LIM_APPROACH_SYM(self): + return self.getToken(LaTeXParser.LIM_APPROACH_SYM, 0) + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def R_BRACE(self, i:int=None): + if i is None: + return self.getTokens(LaTeXParser.R_BRACE) + else: + return self.getToken(LaTeXParser.R_BRACE, i) + + def LETTER(self): + return self.getToken(LaTeXParser.LETTER, 0) + + def SYMBOL(self): + return self.getToken(LaTeXParser.SYMBOL, 0) + + def CARET(self): + return self.getToken(LaTeXParser.CARET, 0) + + def ADD(self): + return self.getToken(LaTeXParser.ADD, 0) + + def SUB(self): + return self.getToken(LaTeXParser.SUB, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_limit_sub + + + + + def limit_sub(self): + + localctx = LaTeXParser.Limit_subContext(self, self._ctx, self.state) + self.enterRule(localctx, 68, self.RULE_limit_sub) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 469 + self.match(LaTeXParser.UNDERSCORE) + self.state = 470 + self.match(LaTeXParser.L_BRACE) + self.state = 471 + _la = self._input.LA(1) + if not(_la==77 or _la==91): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 472 + self.match(LaTeXParser.LIM_APPROACH_SYM) + self.state = 473 + self.expr() + self.state = 482 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==74: + self.state = 474 + self.match(LaTeXParser.CARET) + self.state = 480 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [21]: + self.state = 475 + self.match(LaTeXParser.L_BRACE) + self.state = 476 + _la = self._input.LA(1) + if not(_la==15 or _la==16): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 477 + self.match(LaTeXParser.R_BRACE) + pass + elif token in [15]: + self.state = 478 + self.match(LaTeXParser.ADD) + pass + elif token in [16]: + self.state = 479 + self.match(LaTeXParser.SUB) + pass + else: + raise NoViableAltException(self) + + + + self.state = 484 + self.match(LaTeXParser.R_BRACE) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Func_argContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def func_arg(self): + return self.getTypedRuleContext(LaTeXParser.Func_argContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_func_arg + + + + + def func_arg(self): + + localctx = LaTeXParser.Func_argContext(self, self._ctx, self.state) + self.enterRule(localctx, 70, self.RULE_func_arg) + try: + self.state = 491 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,56,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 486 + self.expr() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 487 + self.expr() + self.state = 488 + self.match(LaTeXParser.T__0) + self.state = 489 + self.func_arg() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Func_arg_noparensContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def mp_nofunc(self): + return self.getTypedRuleContext(LaTeXParser.Mp_nofuncContext,0) + + + def getRuleIndex(self): + return LaTeXParser.RULE_func_arg_noparens + + + + + def func_arg_noparens(self): + + localctx = LaTeXParser.Func_arg_noparensContext(self, self._ctx, self.state) + self.enterRule(localctx, 72, self.RULE_func_arg_noparens) + try: + self.enterOuterAlt(localctx, 1) + self.state = 493 + self.mp_nofunc(0) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SubexprContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def UNDERSCORE(self): + return self.getToken(LaTeXParser.UNDERSCORE, 0) + + def atom(self): + return self.getTypedRuleContext(LaTeXParser.AtomContext,0) + + + def L_BRACE(self): + return self.getToken(LaTeXParser.L_BRACE, 0) + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def R_BRACE(self): + return self.getToken(LaTeXParser.R_BRACE, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_subexpr + + + + + def subexpr(self): + + localctx = LaTeXParser.SubexprContext(self, self._ctx, self.state) + self.enterRule(localctx, 74, self.RULE_subexpr) + try: + self.enterOuterAlt(localctx, 1) + self.state = 495 + self.match(LaTeXParser.UNDERSCORE) + self.state = 501 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [27, 29, 30, 68, 69, 70, 71, 72, 76, 77, 78, 91]: + self.state = 496 + self.atom() + pass + elif token in [21]: + self.state = 497 + self.match(LaTeXParser.L_BRACE) + self.state = 498 + self.expr() + self.state = 499 + self.match(LaTeXParser.R_BRACE) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SupexprContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def CARET(self): + return self.getToken(LaTeXParser.CARET, 0) + + def atom(self): + return self.getTypedRuleContext(LaTeXParser.AtomContext,0) + + + def L_BRACE(self): + return self.getToken(LaTeXParser.L_BRACE, 0) + + def expr(self): + return self.getTypedRuleContext(LaTeXParser.ExprContext,0) + + + def R_BRACE(self): + return self.getToken(LaTeXParser.R_BRACE, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_supexpr + + + + + def supexpr(self): + + localctx = LaTeXParser.SupexprContext(self, self._ctx, self.state) + self.enterRule(localctx, 76, self.RULE_supexpr) + try: + self.enterOuterAlt(localctx, 1) + self.state = 503 + self.match(LaTeXParser.CARET) + self.state = 509 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [27, 29, 30, 68, 69, 70, 71, 72, 76, 77, 78, 91]: + self.state = 504 + self.atom() + pass + elif token in [21]: + self.state = 505 + self.match(LaTeXParser.L_BRACE) + self.state = 506 + self.expr() + self.state = 507 + self.match(LaTeXParser.R_BRACE) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SubeqContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def UNDERSCORE(self): + return self.getToken(LaTeXParser.UNDERSCORE, 0) + + def L_BRACE(self): + return self.getToken(LaTeXParser.L_BRACE, 0) + + def equality(self): + return self.getTypedRuleContext(LaTeXParser.EqualityContext,0) + + + def R_BRACE(self): + return self.getToken(LaTeXParser.R_BRACE, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_subeq + + + + + def subeq(self): + + localctx = LaTeXParser.SubeqContext(self, self._ctx, self.state) + self.enterRule(localctx, 78, self.RULE_subeq) + try: + self.enterOuterAlt(localctx, 1) + self.state = 511 + self.match(LaTeXParser.UNDERSCORE) + self.state = 512 + self.match(LaTeXParser.L_BRACE) + self.state = 513 + self.equality() + self.state = 514 + self.match(LaTeXParser.R_BRACE) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SupeqContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def UNDERSCORE(self): + return self.getToken(LaTeXParser.UNDERSCORE, 0) + + def L_BRACE(self): + return self.getToken(LaTeXParser.L_BRACE, 0) + + def equality(self): + return self.getTypedRuleContext(LaTeXParser.EqualityContext,0) + + + def R_BRACE(self): + return self.getToken(LaTeXParser.R_BRACE, 0) + + def getRuleIndex(self): + return LaTeXParser.RULE_supeq + + + + + def supeq(self): + + localctx = LaTeXParser.SupeqContext(self, self._ctx, self.state) + self.enterRule(localctx, 80, self.RULE_supeq) + try: + self.enterOuterAlt(localctx, 1) + self.state = 516 + self.match(LaTeXParser.UNDERSCORE) + self.state = 517 + self.match(LaTeXParser.L_BRACE) + self.state = 518 + self.equality() + self.state = 519 + self.match(LaTeXParser.R_BRACE) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + + def sempred(self, localctx:RuleContext, ruleIndex:int, predIndex:int): + if self._predicates == None: + self._predicates = dict() + self._predicates[1] = self.relation_sempred + self._predicates[4] = self.additive_sempred + self._predicates[5] = self.mp_sempred + self._predicates[6] = self.mp_nofunc_sempred + self._predicates[15] = self.exp_sempred + self._predicates[16] = self.exp_nofunc_sempred + pred = self._predicates.get(ruleIndex, None) + if pred is None: + raise Exception("No predicate with index:" + str(ruleIndex)) + else: + return pred(localctx, predIndex) + + def relation_sempred(self, localctx:RelationContext, predIndex:int): + if predIndex == 0: + return self.precpred(self._ctx, 2) + + + def additive_sempred(self, localctx:AdditiveContext, predIndex:int): + if predIndex == 1: + return self.precpred(self._ctx, 2) + + + def mp_sempred(self, localctx:MpContext, predIndex:int): + if predIndex == 2: + return self.precpred(self._ctx, 2) + + + def mp_nofunc_sempred(self, localctx:Mp_nofuncContext, predIndex:int): + if predIndex == 3: + return self.precpred(self._ctx, 2) + + + def exp_sempred(self, localctx:ExpContext, predIndex:int): + if predIndex == 4: + return self.precpred(self._ctx, 2) + + + def exp_nofunc_sempred(self, localctx:Exp_nofuncContext, predIndex:int): + if predIndex == 5: + return self.precpred(self._ctx, 2) + + + + + diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/latex/_build_latex_antlr.py b/venv/lib/python3.11/site-packages/sympy/parsing/latex/_build_latex_antlr.py new file mode 100644 index 0000000000000000000000000000000000000000..ee50da5b7861154823812c7773360b53dfd29ff6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/latex/_build_latex_antlr.py @@ -0,0 +1,91 @@ +import os +import subprocess +import glob + +from sympy.utilities.misc import debug + +here = os.path.dirname(__file__) +grammar_file = os.path.abspath(os.path.join(here, "LaTeX.g4")) +dir_latex_antlr = os.path.join(here, "_antlr") + +header = '''\ +# *** GENERATED BY `setup.py antlr`, DO NOT EDIT BY HAND *** +# +# Generated from ../LaTeX.g4, derived from latex2sympy +# latex2sympy is licensed under the MIT license +# https://github.com/augustt198/latex2sympy/blob/master/LICENSE.txt +# +# Generated with antlr4 +# antlr4 is licensed under the BSD-3-Clause License +# https://github.com/antlr/antlr4/blob/master/LICENSE.txt +''' + + +def check_antlr_version(): + debug("Checking antlr4 version...") + + try: + debug(subprocess.check_output(["antlr4"]) + .decode('utf-8').split("\n")[0]) + return True + except (subprocess.CalledProcessError, FileNotFoundError): + debug("The 'antlr4' command line tool is not installed, " + "or not on your PATH.\n" + "> Please refer to the README.md file for more information.") + return False + + +def build_parser(output_dir=dir_latex_antlr): + check_antlr_version() + + debug("Updating ANTLR-generated code in {}".format(output_dir)) + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + with open(os.path.join(output_dir, "__init__.py"), "w+") as fp: + fp.write(header) + + args = [ + "antlr4", + grammar_file, + "-o", output_dir, + # for now, not generating these as latex2sympy did not use them + "-no-visitor", + "-no-listener", + ] + + debug("Running code generation...\n\t$ {}".format(" ".join(args))) + subprocess.check_output(args, cwd=output_dir) + + debug("Applying headers, removing unnecessary files and renaming...") + # Handle case insensitive file systems. If the files are already + # generated, they will be written to latex* but LaTeX*.* won't match them. + for path in (glob.glob(os.path.join(output_dir, "LaTeX*.*")) or + glob.glob(os.path.join(output_dir, "latex*.*"))): + + # Remove files ending in .interp or .tokens as they are not needed. + if not path.endswith(".py"): + os.unlink(path) + continue + + new_path = os.path.join(output_dir, os.path.basename(path).lower()) + with open(path, 'r') as f: + lines = [line.rstrip() + '\n' for line in f] + + os.unlink(path) + + with open(new_path, "w") as out_file: + offset = 0 + while lines[offset].startswith('#'): + offset += 1 + out_file.write(header) + out_file.writelines(lines[offset:]) + + debug("\t{}".format(new_path)) + + return True + + +if __name__ == "__main__": + build_parser() diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/latex/_parse_latex_antlr.py b/venv/lib/python3.11/site-packages/sympy/parsing/latex/_parse_latex_antlr.py new file mode 100644 index 0000000000000000000000000000000000000000..26604375b3a9622f8c1dacdb1d678d09c2c3ad41 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/latex/_parse_latex_antlr.py @@ -0,0 +1,607 @@ +# Ported from latex2sympy by @augustt198 +# https://github.com/augustt198/latex2sympy +# See license in LICENSE.txt +from importlib.metadata import version +import sympy +from sympy.external import import_module +from sympy.printing.str import StrPrinter +from sympy.physics.quantum.state import Bra, Ket + +from .errors import LaTeXParsingError + + +LaTeXParser = LaTeXLexer = MathErrorListener = None + +try: + LaTeXParser = import_module('sympy.parsing.latex._antlr.latexparser', + import_kwargs={'fromlist': ['LaTeXParser']}).LaTeXParser + LaTeXLexer = import_module('sympy.parsing.latex._antlr.latexlexer', + import_kwargs={'fromlist': ['LaTeXLexer']}).LaTeXLexer +except Exception: + pass + +ErrorListener = import_module('antlr4.error.ErrorListener', + warn_not_installed=True, + import_kwargs={'fromlist': ['ErrorListener']} + ) + + + +if ErrorListener: + class MathErrorListener(ErrorListener.ErrorListener): # type:ignore # noqa:F811 + def __init__(self, src): + super(ErrorListener.ErrorListener, self).__init__() + self.src = src + + def syntaxError(self, recog, symbol, line, col, msg, e): + fmt = "%s\n%s\n%s" + marker = "~" * col + "^" + + if msg.startswith("missing"): + err = fmt % (msg, self.src, marker) + elif msg.startswith("no viable"): + err = fmt % ("I expected something else here", self.src, marker) + elif msg.startswith("mismatched"): + names = LaTeXParser.literalNames + expected = [ + names[i] for i in e.getExpectedTokens() if i < len(names) + ] + if len(expected) < 10: + expected = " ".join(expected) + err = (fmt % ("I expected one of these: " + expected, self.src, + marker)) + else: + err = (fmt % ("I expected something else here", self.src, + marker)) + else: + err = fmt % ("I don't understand this", self.src, marker) + raise LaTeXParsingError(err) + + +def parse_latex(sympy, strict=False): + antlr4 = import_module('antlr4') + + if None in [antlr4, MathErrorListener] or \ + not version('antlr4-python3-runtime').startswith('4.11'): + raise ImportError("LaTeX parsing requires the antlr4 Python package," + " provided by pip (antlr4-python3-runtime) or" + " conda (antlr-python-runtime), version 4.11") + + sympy = sympy.strip() + matherror = MathErrorListener(sympy) + + stream = antlr4.InputStream(sympy) + lex = LaTeXLexer(stream) + lex.removeErrorListeners() + lex.addErrorListener(matherror) + + tokens = antlr4.CommonTokenStream(lex) + parser = LaTeXParser(tokens) + + # remove default console error listener + parser.removeErrorListeners() + parser.addErrorListener(matherror) + + relation = parser.math().relation() + if strict and (relation.start.start != 0 or relation.stop.stop != len(sympy) - 1): + raise LaTeXParsingError("Invalid LaTeX") + expr = convert_relation(relation) + + return expr + + +def convert_relation(rel): + if rel.expr(): + return convert_expr(rel.expr()) + + lh = convert_relation(rel.relation(0)) + rh = convert_relation(rel.relation(1)) + if rel.LT(): + return sympy.StrictLessThan(lh, rh) + elif rel.LTE(): + return sympy.LessThan(lh, rh) + elif rel.GT(): + return sympy.StrictGreaterThan(lh, rh) + elif rel.GTE(): + return sympy.GreaterThan(lh, rh) + elif rel.EQUAL(): + return sympy.Eq(lh, rh) + elif rel.NEQ(): + return sympy.Ne(lh, rh) + + +def convert_expr(expr): + return convert_add(expr.additive()) + + +def convert_add(add): + if add.ADD(): + lh = convert_add(add.additive(0)) + rh = convert_add(add.additive(1)) + return sympy.Add(lh, rh, evaluate=False) + elif add.SUB(): + lh = convert_add(add.additive(0)) + rh = convert_add(add.additive(1)) + if hasattr(rh, "is_Atom") and rh.is_Atom: + return sympy.Add(lh, -1 * rh, evaluate=False) + return sympy.Add(lh, sympy.Mul(-1, rh, evaluate=False), evaluate=False) + else: + return convert_mp(add.mp()) + + +def convert_mp(mp): + if hasattr(mp, 'mp'): + mp_left = mp.mp(0) + mp_right = mp.mp(1) + else: + mp_left = mp.mp_nofunc(0) + mp_right = mp.mp_nofunc(1) + + if mp.MUL() or mp.CMD_TIMES() or mp.CMD_CDOT(): + lh = convert_mp(mp_left) + rh = convert_mp(mp_right) + return sympy.Mul(lh, rh, evaluate=False) + elif mp.DIV() or mp.CMD_DIV() or mp.COLON(): + lh = convert_mp(mp_left) + rh = convert_mp(mp_right) + return sympy.Mul(lh, sympy.Pow(rh, -1, evaluate=False), evaluate=False) + else: + if hasattr(mp, 'unary'): + return convert_unary(mp.unary()) + else: + return convert_unary(mp.unary_nofunc()) + + +def convert_unary(unary): + if hasattr(unary, 'unary'): + nested_unary = unary.unary() + else: + nested_unary = unary.unary_nofunc() + if hasattr(unary, 'postfix_nofunc'): + first = unary.postfix() + tail = unary.postfix_nofunc() + postfix = [first] + tail + else: + postfix = unary.postfix() + + if unary.ADD(): + return convert_unary(nested_unary) + elif unary.SUB(): + numabs = convert_unary(nested_unary) + # Use Integer(-n) instead of Mul(-1, n) + return -numabs + elif postfix: + return convert_postfix_list(postfix) + + +def convert_postfix_list(arr, i=0): + if i >= len(arr): + raise LaTeXParsingError("Index out of bounds") + + res = convert_postfix(arr[i]) + if isinstance(res, sympy.Expr): + if i == len(arr) - 1: + return res # nothing to multiply by + else: + if i > 0: + left = convert_postfix(arr[i - 1]) + right = convert_postfix(arr[i + 1]) + if isinstance(left, sympy.Expr) and isinstance( + right, sympy.Expr): + left_syms = convert_postfix(arr[i - 1]).atoms(sympy.Symbol) + right_syms = convert_postfix(arr[i + 1]).atoms( + sympy.Symbol) + # if the left and right sides contain no variables and the + # symbol in between is 'x', treat as multiplication. + if not (left_syms or right_syms) and str(res) == 'x': + return convert_postfix_list(arr, i + 1) + # multiply by next + return sympy.Mul( + res, convert_postfix_list(arr, i + 1), evaluate=False) + else: # must be derivative + wrt = res[0] + if i == len(arr) - 1: + raise LaTeXParsingError("Expected expression for derivative") + else: + expr = convert_postfix_list(arr, i + 1) + return sympy.Derivative(expr, wrt) + + +def do_subs(expr, at): + if at.expr(): + at_expr = convert_expr(at.expr()) + syms = at_expr.atoms(sympy.Symbol) + if len(syms) == 0: + return expr + elif len(syms) > 0: + sym = next(iter(syms)) + return expr.subs(sym, at_expr) + elif at.equality(): + lh = convert_expr(at.equality().expr(0)) + rh = convert_expr(at.equality().expr(1)) + return expr.subs(lh, rh) + + +def convert_postfix(postfix): + if hasattr(postfix, 'exp'): + exp_nested = postfix.exp() + else: + exp_nested = postfix.exp_nofunc() + + exp = convert_exp(exp_nested) + for op in postfix.postfix_op(): + if op.BANG(): + if isinstance(exp, list): + raise LaTeXParsingError("Cannot apply postfix to derivative") + exp = sympy.factorial(exp, evaluate=False) + elif op.eval_at(): + ev = op.eval_at() + at_b = None + at_a = None + if ev.eval_at_sup(): + at_b = do_subs(exp, ev.eval_at_sup()) + if ev.eval_at_sub(): + at_a = do_subs(exp, ev.eval_at_sub()) + if at_b is not None and at_a is not None: + exp = sympy.Add(at_b, -1 * at_a, evaluate=False) + elif at_b is not None: + exp = at_b + elif at_a is not None: + exp = at_a + + return exp + + +def convert_exp(exp): + if hasattr(exp, 'exp'): + exp_nested = exp.exp() + else: + exp_nested = exp.exp_nofunc() + + if exp_nested: + base = convert_exp(exp_nested) + if isinstance(base, list): + raise LaTeXParsingError("Cannot raise derivative to power") + if exp.atom(): + exponent = convert_atom(exp.atom()) + elif exp.expr(): + exponent = convert_expr(exp.expr()) + return sympy.Pow(base, exponent, evaluate=False) + else: + if hasattr(exp, 'comp'): + return convert_comp(exp.comp()) + else: + return convert_comp(exp.comp_nofunc()) + + +def convert_comp(comp): + if comp.group(): + return convert_expr(comp.group().expr()) + elif comp.abs_group(): + return sympy.Abs(convert_expr(comp.abs_group().expr()), evaluate=False) + elif comp.atom(): + return convert_atom(comp.atom()) + elif comp.floor(): + return convert_floor(comp.floor()) + elif comp.ceil(): + return convert_ceil(comp.ceil()) + elif comp.func(): + return convert_func(comp.func()) + + +def convert_atom(atom): + if atom.LETTER(): + sname = atom.LETTER().getText() + if atom.subexpr(): + if atom.subexpr().expr(): # subscript is expr + subscript = convert_expr(atom.subexpr().expr()) + else: # subscript is atom + subscript = convert_atom(atom.subexpr().atom()) + sname += '_{' + StrPrinter().doprint(subscript) + '}' + if atom.SINGLE_QUOTES(): + sname += atom.SINGLE_QUOTES().getText() # put after subscript for easy identify + return sympy.Symbol(sname) + elif atom.SYMBOL(): + s = atom.SYMBOL().getText()[1:] + if s == "infty": + return sympy.oo + else: + if atom.subexpr(): + subscript = None + if atom.subexpr().expr(): # subscript is expr + subscript = convert_expr(atom.subexpr().expr()) + else: # subscript is atom + subscript = convert_atom(atom.subexpr().atom()) + subscriptName = StrPrinter().doprint(subscript) + s += '_{' + subscriptName + '}' + return sympy.Symbol(s) + elif atom.number(): + s = atom.number().getText().replace(",", "") + return sympy.Number(s) + elif atom.DIFFERENTIAL(): + var = get_differential_var(atom.DIFFERENTIAL()) + return sympy.Symbol('d' + var.name) + elif atom.mathit(): + text = rule2text(atom.mathit().mathit_text()) + return sympy.Symbol(text) + elif atom.frac(): + return convert_frac(atom.frac()) + elif atom.binom(): + return convert_binom(atom.binom()) + elif atom.bra(): + val = convert_expr(atom.bra().expr()) + return Bra(val) + elif atom.ket(): + val = convert_expr(atom.ket().expr()) + return Ket(val) + + +def rule2text(ctx): + stream = ctx.start.getInputStream() + # starting index of starting token + startIdx = ctx.start.start + # stopping index of stopping token + stopIdx = ctx.stop.stop + + return stream.getText(startIdx, stopIdx) + + +def convert_frac(frac): + diff_op = False + partial_op = False + if frac.lower and frac.upper: + lower_itv = frac.lower.getSourceInterval() + lower_itv_len = lower_itv[1] - lower_itv[0] + 1 + if (frac.lower.start == frac.lower.stop + and frac.lower.start.type == LaTeXLexer.DIFFERENTIAL): + wrt = get_differential_var_str(frac.lower.start.text) + diff_op = True + elif (lower_itv_len == 2 and frac.lower.start.type == LaTeXLexer.SYMBOL + and frac.lower.start.text == '\\partial' + and (frac.lower.stop.type == LaTeXLexer.LETTER + or frac.lower.stop.type == LaTeXLexer.SYMBOL)): + partial_op = True + wrt = frac.lower.stop.text + if frac.lower.stop.type == LaTeXLexer.SYMBOL: + wrt = wrt[1:] + + if diff_op or partial_op: + wrt = sympy.Symbol(wrt) + if (diff_op and frac.upper.start == frac.upper.stop + and frac.upper.start.type == LaTeXLexer.LETTER + and frac.upper.start.text == 'd'): + return [wrt] + elif (partial_op and frac.upper.start == frac.upper.stop + and frac.upper.start.type == LaTeXLexer.SYMBOL + and frac.upper.start.text == '\\partial'): + return [wrt] + upper_text = rule2text(frac.upper) + + expr_top = None + if diff_op and upper_text.startswith('d'): + expr_top = parse_latex(upper_text[1:]) + elif partial_op and frac.upper.start.text == '\\partial': + expr_top = parse_latex(upper_text[len('\\partial'):]) + if expr_top: + return sympy.Derivative(expr_top, wrt) + if frac.upper: + expr_top = convert_expr(frac.upper) + else: + expr_top = sympy.Number(frac.upperd.text) + if frac.lower: + expr_bot = convert_expr(frac.lower) + else: + expr_bot = sympy.Number(frac.lowerd.text) + inverse_denom = sympy.Pow(expr_bot, -1, evaluate=False) + if expr_top == 1: + return inverse_denom + else: + return sympy.Mul(expr_top, inverse_denom, evaluate=False) + +def convert_binom(binom): + expr_n = convert_expr(binom.n) + expr_k = convert_expr(binom.k) + return sympy.binomial(expr_n, expr_k, evaluate=False) + +def convert_floor(floor): + val = convert_expr(floor.val) + return sympy.floor(val, evaluate=False) + +def convert_ceil(ceil): + val = convert_expr(ceil.val) + return sympy.ceiling(val, evaluate=False) + +def convert_func(func): + if func.func_normal(): + if func.L_PAREN(): # function called with parenthesis + arg = convert_func_arg(func.func_arg()) + else: + arg = convert_func_arg(func.func_arg_noparens()) + + name = func.func_normal().start.text[1:] + + # change arc -> a + if name in [ + "arcsin", "arccos", "arctan", "arccsc", "arcsec", "arccot" + ]: + name = "a" + name[3:] + expr = getattr(sympy.functions, name)(arg, evaluate=False) + if name in ["arsinh", "arcosh", "artanh"]: + name = "a" + name[2:] + expr = getattr(sympy.functions, name)(arg, evaluate=False) + + if name == "exp": + expr = sympy.exp(arg, evaluate=False) + + if name in ("log", "lg", "ln"): + if func.subexpr(): + if func.subexpr().expr(): + base = convert_expr(func.subexpr().expr()) + else: + base = convert_atom(func.subexpr().atom()) + elif name == "lg": # ISO 80000-2:2019 + base = 10 + elif name in ("ln", "log"): # SymPy's latex printer prints ln as log by default + base = sympy.E + expr = sympy.log(arg, base, evaluate=False) + + func_pow = None + should_pow = True + if func.supexpr(): + if func.supexpr().expr(): + func_pow = convert_expr(func.supexpr().expr()) + else: + func_pow = convert_atom(func.supexpr().atom()) + + if name in [ + "sin", "cos", "tan", "csc", "sec", "cot", "sinh", "cosh", + "tanh" + ]: + if func_pow == -1: + name = "a" + name + should_pow = False + expr = getattr(sympy.functions, name)(arg, evaluate=False) + + if func_pow and should_pow: + expr = sympy.Pow(expr, func_pow, evaluate=False) + + return expr + elif func.LETTER() or func.SYMBOL(): + if func.LETTER(): + fname = func.LETTER().getText() + elif func.SYMBOL(): + fname = func.SYMBOL().getText()[1:] + fname = str(fname) # can't be unicode + if func.subexpr(): + if func.subexpr().expr(): # subscript is expr + subscript = convert_expr(func.subexpr().expr()) + else: # subscript is atom + subscript = convert_atom(func.subexpr().atom()) + subscriptName = StrPrinter().doprint(subscript) + fname += '_{' + subscriptName + '}' + if func.SINGLE_QUOTES(): + fname += func.SINGLE_QUOTES().getText() + input_args = func.args() + output_args = [] + while input_args.args(): # handle multiple arguments to function + output_args.append(convert_expr(input_args.expr())) + input_args = input_args.args() + output_args.append(convert_expr(input_args.expr())) + return sympy.Function(fname)(*output_args) + elif func.FUNC_INT(): + return handle_integral(func) + elif func.FUNC_SQRT(): + expr = convert_expr(func.base) + if func.root: + r = convert_expr(func.root) + return sympy.root(expr, r, evaluate=False) + else: + return sympy.sqrt(expr, evaluate=False) + elif func.FUNC_OVERLINE(): + expr = convert_expr(func.base) + return sympy.conjugate(expr, evaluate=False) + elif func.FUNC_SUM(): + return handle_sum_or_prod(func, "summation") + elif func.FUNC_PROD(): + return handle_sum_or_prod(func, "product") + elif func.FUNC_LIM(): + return handle_limit(func) + + +def convert_func_arg(arg): + if hasattr(arg, 'expr'): + return convert_expr(arg.expr()) + else: + return convert_mp(arg.mp_nofunc()) + + +def handle_integral(func): + if func.additive(): + integrand = convert_add(func.additive()) + elif func.frac(): + integrand = convert_frac(func.frac()) + else: + integrand = 1 + + int_var = None + if func.DIFFERENTIAL(): + int_var = get_differential_var(func.DIFFERENTIAL()) + else: + for sym in integrand.atoms(sympy.Symbol): + s = str(sym) + if len(s) > 1 and s[0] == 'd': + if s[1] == '\\': + int_var = sympy.Symbol(s[2:]) + else: + int_var = sympy.Symbol(s[1:]) + int_sym = sym + if int_var: + integrand = integrand.subs(int_sym, 1) + else: + # Assume dx by default + int_var = sympy.Symbol('x') + + if func.subexpr(): + if func.subexpr().atom(): + lower = convert_atom(func.subexpr().atom()) + else: + lower = convert_expr(func.subexpr().expr()) + if func.supexpr().atom(): + upper = convert_atom(func.supexpr().atom()) + else: + upper = convert_expr(func.supexpr().expr()) + return sympy.Integral(integrand, (int_var, lower, upper)) + else: + return sympy.Integral(integrand, int_var) + + +def handle_sum_or_prod(func, name): + val = convert_mp(func.mp()) + iter_var = convert_expr(func.subeq().equality().expr(0)) + start = convert_expr(func.subeq().equality().expr(1)) + if func.supexpr().expr(): # ^{expr} + end = convert_expr(func.supexpr().expr()) + else: # ^atom + end = convert_atom(func.supexpr().atom()) + + if name == "summation": + return sympy.Sum(val, (iter_var, start, end)) + elif name == "product": + return sympy.Product(val, (iter_var, start, end)) + + +def handle_limit(func): + sub = func.limit_sub() + if sub.LETTER(): + var = sympy.Symbol(sub.LETTER().getText()) + elif sub.SYMBOL(): + var = sympy.Symbol(sub.SYMBOL().getText()[1:]) + else: + var = sympy.Symbol('x') + if sub.SUB(): + direction = "-" + elif sub.ADD(): + direction = "+" + else: + direction = "+-" + approaching = convert_expr(sub.expr()) + content = convert_mp(func.mp()) + + return sympy.Limit(content, var, approaching, direction) + + +def get_differential_var(d): + text = get_differential_var_str(d.getText()) + return sympy.Symbol(text) + + +def get_differential_var_str(text): + for i in range(1, len(text)): + c = text[i] + if not (c == " " or c == "\r" or c == "\n" or c == "\t"): + idx = i + break + text = text[idx:] + if text[0] == "\\": + text = text[1:] + return text diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/latex/errors.py b/venv/lib/python3.11/site-packages/sympy/parsing/latex/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..d8c3ef9f06279df42d4b2054acc4cfe39b6682a5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/latex/errors.py @@ -0,0 +1,2 @@ +class LaTeXParsingError(Exception): + pass diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/__init__.py b/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..92e58d3172e100cc376d0b416b3835d164bd5647 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/__init__.py @@ -0,0 +1,2 @@ +from .latex_parser import parse_latex_lark, LarkLaTeXParser # noqa +from .transformer import TransformToSymPyExpr # noqa diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/grammar/greek_symbols.lark b/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/grammar/greek_symbols.lark new file mode 100644 index 0000000000000000000000000000000000000000..7439fab9dcac284dc3c9b5fbfa4fc6db8b29dfd2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/grammar/greek_symbols.lark @@ -0,0 +1,28 @@ +// Greek symbols +// TODO: Shouold we include the uppercase variants for the symbols where the uppercase variant doesn't have a separate meaning? +ALPHA: "\\alpha" +BETA: "\\beta" +GAMMA: "\\gamma" +DELTA: "\\delta" // TODO: Should this be included? Delta usually denotes other things. +EPSILON: "\\epsilon" | "\\varepsilon" +ZETA: "\\zeta" +ETA: "\\eta" +THETA: "\\theta" | "\\vartheta" +// TODO: Should I add iota to the list? +KAPPA: "\\kappa" +LAMBDA: "\\lambda" // TODO: What about the uppercase variant? +MU: "\\mu" +NU: "\\nu" +XI: "\\xi" +// TODO: Should there be a separate note for transforming \pi into sympy.pi? +RHO: "\\rho" | "\\varrho" +// TODO: What should we do about sigma? +TAU: "\\tau" +UPSILON: "\\upsilon" +PHI: "\\phi" | "\\varphi" +CHI: "\\chi" +PSI: "\\psi" +OMEGA: "\\omega" + +GREEK_SYMBOL: ALPHA | BETA | GAMMA | DELTA | EPSILON | ZETA | ETA | THETA | KAPPA + | LAMBDA | MU | NU | XI | RHO | TAU | UPSILON | PHI | CHI | PSI | OMEGA diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/grammar/latex.lark b/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/grammar/latex.lark new file mode 100644 index 0000000000000000000000000000000000000000..43e8d0e9105fa4da9bcdd2c0fa6111f6d523c9a9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/grammar/latex.lark @@ -0,0 +1,403 @@ +%ignore /[ \t\n\r]+/ + +%ignore "\\," | "\\thinspace" | "\\:" | "\\medspace" | "\\;" | "\\thickspace" +%ignore "\\quad" | "\\qquad" +%ignore "\\!" | "\\negthinspace" | "\\negmedspace" | "\\negthickspace" +%ignore "\\vrule" | "\\vcenter" | "\\vbox" | "\\vskip" | "\\vspace" | "\\hfill" +%ignore "\\*" | "\\-" | "\\." | "\\/" | "\\(" | "\\=" + +%ignore "\\left" | "\\right" +%ignore "\\limits" | "\\nolimits" +%ignore "\\displaystyle" + +///////////////////// tokens /////////////////////// + +// basic binary operators +ADD: "+" +SUB: "-" +MUL: "*" +DIV: "/" + +// tokens with distinct left and right symbols +L_BRACE: "{" +R_BRACE: "}" +L_BRACE_LITERAL: "\\{" +R_BRACE_LITERAL: "\\}" +L_BRACKET: "[" +R_BRACKET: "]" +L_CEIL: "\\lceil" +R_CEIL: "\\rceil" +L_FLOOR: "\\lfloor" +R_FLOOR: "\\rfloor" +L_PAREN: "(" +R_PAREN: ")" + +// limit, integral, sum, and product symbols +FUNC_LIM: "\\lim" +LIM_APPROACH_SYM: "\\to" | "\\rightarrow" | "\\Rightarrow" | "\\longrightarrow" | "\\Longrightarrow" +FUNC_INT: "\\int" | "\\intop" +FUNC_SUM: "\\sum" +FUNC_PROD: "\\prod" + +// common functions +FUNC_EXP: "\\exp" +FUNC_LOG: "\\log" +FUNC_LN: "\\ln" +FUNC_LG: "\\lg" +FUNC_MIN: "\\min" +FUNC_MAX: "\\max" + +// trigonometric functions +FUNC_SIN: "\\sin" +FUNC_COS: "\\cos" +FUNC_TAN: "\\tan" +FUNC_CSC: "\\csc" +FUNC_SEC: "\\sec" +FUNC_COT: "\\cot" + +// inverse trigonometric functions +FUNC_ARCSIN: "\\arcsin" +FUNC_ARCCOS: "\\arccos" +FUNC_ARCTAN: "\\arctan" +FUNC_ARCCSC: "\\arccsc" +FUNC_ARCSEC: "\\arcsec" +FUNC_ARCCOT: "\\arccot" + +// hyperbolic trigonometric functions +FUNC_SINH: "\\sinh" +FUNC_COSH: "\\cosh" +FUNC_TANH: "\\tanh" +FUNC_ARSINH: "\\arsinh" +FUNC_ARCOSH: "\\arcosh" +FUNC_ARTANH: "\\artanh" + +FUNC_SQRT: "\\sqrt" + +// miscellaneous symbols +CMD_TIMES: "\\times" +CMD_CDOT: "\\cdot" +CMD_DIV: "\\div" +CMD_FRAC: "\\frac" | "\\dfrac" | "\\tfrac" | "\\nicefrac" +CMD_BINOM: "\\binom" | "\\dbinom" | "\\tbinom" +CMD_OVERLINE: "\\overline" +CMD_LANGLE: "\\langle" +CMD_RANGLE: "\\rangle" + +CMD_MATHIT: "\\mathit" + +CMD_INFTY: "\\infty" + +BANG: "!" +BAR: "|" +CARET: "^" +COLON: ":" +UNDERSCORE: "_" + +// relational symbols +EQUAL: "=" +NOT_EQUAL: "\\neq" | "\\ne" +LT: "<" +LTE: "\\leq" | "\\le" | "\\leqslant" +GT: ">" +GTE: "\\geq" | "\\ge" | "\\geqslant" + +DIV_SYMBOL: CMD_DIV | DIV +MUL_SYMBOL: MUL | CMD_TIMES | CMD_CDOT + +%import .greek_symbols.GREEK_SYMBOL + +UPRIGHT_DIFFERENTIAL_SYMBOL: "\\text{d}" | "\\mathrm{d}" +DIFFERENTIAL_SYMBOL: "d" | UPRIGHT_DIFFERENTIAL_SYMBOL + +// disallow "d" as a variable name because we want to parse "d" as a differential symbol. +SYMBOL: /[a-zA-Z]'*/ +GREEK_SYMBOL_WITH_PRIMES: GREEK_SYMBOL "'"* +LATIN_SYMBOL_WITH_LATIN_SUBSCRIPT: /([a-zA-Z]'*)_(([A-Za-z0-9]|[a-zA-Z]+)|\{([A-Za-z0-9]|[a-zA-Z]+'*)\})/ +LATIN_SYMBOL_WITH_GREEK_SUBSCRIPT: /([a-zA-Z]'*)_/ GREEK_SYMBOL | /([a-zA-Z]'*)_/ L_BRACE GREEK_SYMBOL_WITH_PRIMES R_BRACE +// best to define the variant with braces like that instead of shoving it all into one case like in +// /([a-zA-Z])_/ L_BRACE? GREEK_SYMBOL R_BRACE? because then we can easily error out on input like +// r"h_{\theta" +GREEK_SYMBOL_WITH_LATIN_SUBSCRIPT: GREEK_SYMBOL_WITH_PRIMES /_(([A-Za-z0-9]|[a-zA-Z]+)|\{([A-Za-z0-9]|[a-zA-Z]+'*)\})/ +GREEK_SYMBOL_WITH_GREEK_SUBSCRIPT: GREEK_SYMBOL_WITH_PRIMES /_/ (GREEK_SYMBOL | L_BRACE GREEK_SYMBOL_WITH_PRIMES R_BRACE) +MULTI_LETTER_SYMBOL: /[a-zA-Z]+(\s+[a-zA-Z]+)*'*/ + +%import common.DIGIT -> DIGIT + +CMD_PRIME: "\\prime" +CMD_ASTERISK: "\\ast" + +PRIMES: "'"+ +STARS: "*"+ +PRIMES_VIA_CMD: CMD_PRIME+ +STARS_VIA_CMD: CMD_ASTERISK+ + +CMD_IMAGINARY_UNIT: "\\imaginaryunit" + +CMD_BEGIN: "\\begin" +CMD_END: "\\end" + +// matrices +IGNORE_L: /[ \t\n\r]*/ L_BRACE* /[ \t\n\r]*/ +IGNORE_R: /[ \t\n\r]*/ R_BRACE* /[ \t\n\r]*/ +ARRAY_MATRIX_BEGIN: L_BRACE "array" R_BRACE L_BRACE /[^}]*/ R_BRACE +ARRAY_MATRIX_END: L_BRACE "array" R_BRACE +AMSMATH_MATRIX: L_BRACE "matrix" R_BRACE +AMSMATH_PMATRIX: L_BRACE "pmatrix" R_BRACE +AMSMATH_BMATRIX: L_BRACE "bmatrix" R_BRACE +// Without the (L|R)_PARENs and (L|R)_BRACKETs, a matrix defined using +// \begin{array}...\end{array} or \begin{matrix}...\end{matrix} must +// not qualify as a complete matrix expression; this is done so that +// if we have \begin{array}...\end{array} or \begin{matrix}...\end{matrix} +// between BAR pairs, then they should be interpreted as determinants as +// opposed to sympy.Abs (absolute value) applied to a matrix. +CMD_BEGIN_AMSPMATRIX_AMSBMATRIX: CMD_BEGIN (AMSMATH_PMATRIX | AMSMATH_BMATRIX) +CMD_BEGIN_ARRAY_AMSMATRIX: (L_PAREN | L_BRACKET) IGNORE_L CMD_BEGIN (ARRAY_MATRIX_BEGIN | AMSMATH_MATRIX) +CMD_MATRIX_BEGIN: CMD_BEGIN_AMSPMATRIX_AMSBMATRIX | CMD_BEGIN_ARRAY_AMSMATRIX +CMD_END_AMSPMATRIX_AMSBMATRIX: CMD_END (AMSMATH_PMATRIX | AMSMATH_BMATRIX) +CMD_END_ARRAY_AMSMATRIX: CMD_END (ARRAY_MATRIX_END | AMSMATH_MATRIX) IGNORE_R "\\right"? (R_PAREN | R_BRACKET) +CMD_MATRIX_END: CMD_END_AMSPMATRIX_AMSBMATRIX | CMD_END_ARRAY_AMSMATRIX +MATRIX_COL_DELIM: "&" +MATRIX_ROW_DELIM: "\\\\" +FUNC_MATRIX_TRACE: "\\trace" +FUNC_MATRIX_ADJUGATE: "\\adjugate" + +// determinants +AMSMATH_VMATRIX: L_BRACE "vmatrix" R_BRACE +CMD_DETERMINANT_BEGIN_SIMPLE: CMD_BEGIN AMSMATH_VMATRIX +CMD_DETERMINANT_BEGIN_VARIANT: BAR IGNORE_L CMD_BEGIN (ARRAY_MATRIX_BEGIN | AMSMATH_MATRIX) +CMD_DETERMINANT_BEGIN: CMD_DETERMINANT_BEGIN_SIMPLE | CMD_DETERMINANT_BEGIN_VARIANT +CMD_DETERMINANT_END_SIMPLE: CMD_END AMSMATH_VMATRIX +CMD_DETERMINANT_END_VARIANT: CMD_END (ARRAY_MATRIX_END | AMSMATH_MATRIX) IGNORE_R "\\right"? BAR +CMD_DETERMINANT_END: CMD_DETERMINANT_END_SIMPLE | CMD_DETERMINANT_END_VARIANT +FUNC_DETERMINANT: "\\det" + +//////////////////// grammar ////////////////////// + +latex_string: _relation | _expression + +_one_letter_symbol: SYMBOL + | LATIN_SYMBOL_WITH_LATIN_SUBSCRIPT + | LATIN_SYMBOL_WITH_GREEK_SUBSCRIPT + | GREEK_SYMBOL_WITH_LATIN_SUBSCRIPT + | GREEK_SYMBOL_WITH_GREEK_SUBSCRIPT + | GREEK_SYMBOL_WITH_PRIMES +// LuaTeX-generated outputs of \mathit{foo'} and \mathit{foo}' +// seem to be the same on the surface. We allow both styles. +multi_letter_symbol: CMD_MATHIT L_BRACE MULTI_LETTER_SYMBOL R_BRACE + | CMD_MATHIT L_BRACE MULTI_LETTER_SYMBOL R_BRACE /'+/ +number: /\d+(\.\d*)?/ | CMD_IMAGINARY_UNIT + +_atomic_expr: _one_letter_symbol + | multi_letter_symbol + | number + | CMD_INFTY + +group_round_parentheses: L_PAREN _expression R_PAREN +group_square_brackets: L_BRACKET _expression R_BRACKET +group_curly_parentheses: L_BRACE _expression R_BRACE + +_relation: eq | ne | lt | lte | gt | gte + +eq: _expression EQUAL _expression +ne: _expression NOT_EQUAL _expression +lt: _expression LT _expression +lte: _expression LTE _expression +gt: _expression GT _expression +gte: _expression GTE _expression + +_expression_core: _atomic_expr | group_curly_parentheses + +add: _expression ADD _expression_mul + | ADD _expression_mul +sub: _expression SUB _expression_mul + | SUB _expression_mul +mul: _expression_mul MUL_SYMBOL _expression_power +div: _expression_mul DIV_SYMBOL _expression_power + +adjacent_expressions: (_one_letter_symbol | number) _expression_mul + | group_round_parentheses (group_round_parentheses | _one_letter_symbol) + | _function _function + | fraction _expression_mul + +_expression_func: _expression_core + | group_round_parentheses + | fraction + | binomial + | _function + | _integral// | derivative + | limit + | matrix + +_expression_power: _expression_func | superscript | matrix_prime | symbol_prime + +_expression_mul: _expression_power + | mul | div | adjacent_expressions + | summation | product + +_expression: _expression_mul | add | sub + +_limit_dir: "+" | "-" | L_BRACE ("+" | "-") R_BRACE + +limit_dir_expr: _expression CARET _limit_dir + +group_curly_parentheses_lim: L_BRACE _expression LIM_APPROACH_SYM (limit_dir_expr | _expression) R_BRACE + +limit: FUNC_LIM UNDERSCORE group_curly_parentheses_lim _expression + +differential: DIFFERENTIAL_SYMBOL _one_letter_symbol + +//_derivative_operator: CMD_FRAC L_BRACE DIFFERENTIAL_SYMBOL R_BRACE L_BRACE differential R_BRACE + +//derivative: _derivative_operator _expression + +_integral: normal_integral | integral_with_special_fraction + +normal_integral: FUNC_INT _expression DIFFERENTIAL_SYMBOL _one_letter_symbol + | FUNC_INT (CARET _expression_core UNDERSCORE _expression_core)? _expression? DIFFERENTIAL_SYMBOL _one_letter_symbol + | FUNC_INT (UNDERSCORE _expression_core CARET _expression_core)? _expression? DIFFERENTIAL_SYMBOL _one_letter_symbol + +group_curly_parentheses_int: L_BRACE _expression? differential R_BRACE + +special_fraction: CMD_FRAC group_curly_parentheses_int group_curly_parentheses + +integral_with_special_fraction: FUNC_INT special_fraction + | FUNC_INT (CARET _expression_core UNDERSCORE _expression_core)? special_fraction + | FUNC_INT (UNDERSCORE _expression_core CARET _expression_core)? special_fraction + +group_curly_parentheses_special: UNDERSCORE L_BRACE _atomic_expr EQUAL _atomic_expr R_BRACE CARET _expression_core + | CARET _expression_core UNDERSCORE L_BRACE _atomic_expr EQUAL _atomic_expr R_BRACE + +summation: FUNC_SUM group_curly_parentheses_special _expression + | FUNC_SUM group_curly_parentheses_special _expression + +product: FUNC_PROD group_curly_parentheses_special _expression + | FUNC_PROD group_curly_parentheses_special _expression + +superscript: _expression_func CARET (_expression_power | CMD_PRIME | CMD_ASTERISK) + | _expression_func CARET L_BRACE (PRIMES | STARS | PRIMES_VIA_CMD | STARS_VIA_CMD) R_BRACE + +matrix_prime: (matrix | group_round_parentheses) PRIMES + +symbol_prime: (LATIN_SYMBOL_WITH_LATIN_SUBSCRIPT + | LATIN_SYMBOL_WITH_GREEK_SUBSCRIPT + | GREEK_SYMBOL_WITH_LATIN_SUBSCRIPT + | GREEK_SYMBOL_WITH_GREEK_SUBSCRIPT) PRIMES + +fraction: _basic_fraction + | _simple_fraction + | _general_fraction + +_basic_fraction: CMD_FRAC DIGIT (DIGIT | SYMBOL | GREEK_SYMBOL_WITH_PRIMES) + +_simple_fraction: CMD_FRAC DIGIT group_curly_parentheses + | CMD_FRAC group_curly_parentheses (DIGIT | SYMBOL | GREEK_SYMBOL_WITH_PRIMES) + +_general_fraction: CMD_FRAC group_curly_parentheses group_curly_parentheses + +binomial: _basic_binomial + | _simple_binomial + | _general_binomial + +_basic_binomial: CMD_BINOM DIGIT (DIGIT | SYMBOL | GREEK_SYMBOL_WITH_PRIMES) + +_simple_binomial: CMD_BINOM DIGIT group_curly_parentheses + | CMD_BINOM group_curly_parentheses (DIGIT | SYMBOL | GREEK_SYMBOL_WITH_PRIMES) + +_general_binomial: CMD_BINOM group_curly_parentheses group_curly_parentheses + +list_of_expressions: _expression ("," _expression)* + +function_applied: _one_letter_symbol L_PAREN list_of_expressions R_PAREN + +min: FUNC_MIN L_PAREN list_of_expressions R_PAREN + +max: FUNC_MAX L_PAREN list_of_expressions R_PAREN + +bra: CMD_LANGLE _expression BAR + +ket: BAR _expression CMD_RANGLE + +inner_product: CMD_LANGLE _expression BAR _expression CMD_RANGLE + +_function: function_applied + | abs | floor | ceil + | _trigonometric_function | _inverse_trigonometric_function + | _trigonometric_function_power + | _hyperbolic_trigonometric_function | _inverse_hyperbolic_trigonometric_function + | exponential + | log + | square_root + | factorial + | conjugate + | max | min + | bra | ket | inner_product + | determinant + | trace + | adjugate + +exponential: FUNC_EXP _expression + +log: FUNC_LOG _expression + | FUNC_LN _expression + | FUNC_LG _expression + | FUNC_LOG UNDERSCORE (DIGIT | _one_letter_symbol) _expression + | FUNC_LOG UNDERSCORE group_curly_parentheses _expression + +square_root: FUNC_SQRT group_curly_parentheses + | FUNC_SQRT group_square_brackets group_curly_parentheses + +factorial: _expression_func BANG + +conjugate: CMD_OVERLINE group_curly_parentheses + | CMD_OVERLINE DIGIT + +_trigonometric_function: sin | cos | tan | csc | sec | cot + +sin: FUNC_SIN _expression +cos: FUNC_COS _expression +tan: FUNC_TAN _expression +csc: FUNC_CSC _expression +sec: FUNC_SEC _expression +cot: FUNC_COT _expression + +_trigonometric_function_power: sin_power | cos_power | tan_power | csc_power | sec_power | cot_power + +sin_power: FUNC_SIN CARET _expression_core _expression +cos_power: FUNC_COS CARET _expression_core _expression +tan_power: FUNC_TAN CARET _expression_core _expression +csc_power: FUNC_CSC CARET _expression_core _expression +sec_power: FUNC_SEC CARET _expression_core _expression +cot_power: FUNC_COT CARET _expression_core _expression + +_hyperbolic_trigonometric_function: sinh | cosh | tanh + +sinh: FUNC_SINH _expression +cosh: FUNC_COSH _expression +tanh: FUNC_TANH _expression + +_inverse_trigonometric_function: arcsin | arccos | arctan | arccsc | arcsec | arccot + +arcsin: FUNC_ARCSIN _expression +arccos: FUNC_ARCCOS _expression +arctan: FUNC_ARCTAN _expression +arccsc: FUNC_ARCCSC _expression +arcsec: FUNC_ARCSEC _expression +arccot: FUNC_ARCCOT _expression + +_inverse_hyperbolic_trigonometric_function: asinh | acosh | atanh + +asinh: FUNC_ARSINH _expression +acosh: FUNC_ARCOSH _expression +atanh: FUNC_ARTANH _expression + +abs: BAR _expression BAR +floor: L_FLOOR _expression R_FLOOR +ceil: L_CEIL _expression R_CEIL + +matrix: CMD_MATRIX_BEGIN matrix_body CMD_MATRIX_END +matrix_body: matrix_row (MATRIX_ROW_DELIM matrix_row)* (MATRIX_ROW_DELIM)? +matrix_row: _expression (MATRIX_COL_DELIM _expression)* +determinant: (CMD_DETERMINANT_BEGIN matrix_body CMD_DETERMINANT_END) + | FUNC_DETERMINANT _expression +trace: FUNC_MATRIX_TRACE _expression +adjugate: FUNC_MATRIX_ADJUGATE _expression diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/latex_parser.py b/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/latex_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..29f594b0de4bfd4648df1554d5863a37afff035f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/latex_parser.py @@ -0,0 +1,145 @@ +import os +import logging +import re +from pathlib import Path + +from sympy.external import import_module +from sympy.parsing.latex.lark.transformer import TransformToSymPyExpr + +_lark = import_module("lark") + + +class LarkLaTeXParser: + r"""Class for converting input `\mathrm{\LaTeX}` strings into SymPy Expressions. + It holds all the necessary internal data for doing so, and exposes hooks for + customizing its behavior. + + Parameters + ========== + + print_debug_output : bool, optional + + If set to ``True``, prints debug output to the logger. Defaults to ``False``. + + transform : bool, optional + + If set to ``True``, the class runs the Transformer class on the parse tree + generated by running ``Lark.parse`` on the input string. Defaults to ``True``. + + Setting it to ``False`` can help with debugging the `\mathrm{\LaTeX}` grammar. + + grammar_file : str, optional + + The path to the grammar file that the parser should use. If set to ``None``, + it uses the default grammar, which is in ``grammar/latex.lark``, relative to + the ``sympy/parsing/latex/lark/`` directory. + + transformer : str, optional + + The name of the Transformer class to use. If set to ``None``, it uses the + default transformer class, which is :py:func:`TransformToSymPyExpr`. + + """ + def __init__(self, print_debug_output=False, transform=True, grammar_file=None, transformer=None): + grammar_dir_path = os.path.join(os.path.dirname(__file__), "grammar/") + + if grammar_file is None: + latex_grammar = Path(os.path.join(grammar_dir_path, "latex.lark")).read_text(encoding="utf-8") + else: + latex_grammar = Path(grammar_file).read_text(encoding="utf-8") + + self.parser = _lark.Lark( + latex_grammar, + source_path=grammar_dir_path, + parser="earley", + start="latex_string", + lexer="auto", + ambiguity="explicit", + propagate_positions=False, + maybe_placeholders=False, + keep_all_tokens=True) + + self.print_debug_output = print_debug_output + self.transform_expr = transform + + if transformer is None: + self.transformer = TransformToSymPyExpr() + else: + self.transformer = transformer() + + def doparse(self, s: str): + if self.print_debug_output: + _lark.logger.setLevel(logging.DEBUG) + + parse_tree = self.parser.parse(s) + + if not self.transform_expr: + # exit early and return the parse tree + _lark.logger.debug("expression = %s", s) + _lark.logger.debug(parse_tree) + _lark.logger.debug(parse_tree.pretty()) + return parse_tree + + if self.print_debug_output: + # print this stuff before attempting to run the transformer + _lark.logger.debug("expression = %s", s) + # print the `parse_tree` variable + _lark.logger.debug(parse_tree.pretty()) + + sympy_expression = self.transformer.transform(parse_tree) + + if self.print_debug_output: + _lark.logger.debug("SymPy expression = %s", sympy_expression) + + return sympy_expression + + +if _lark is not None: + _lark_latex_parser = LarkLaTeXParser() + + +def parse_latex_lark(s: str): + """ + Experimental LaTeX parser using Lark. + + This function is still under development and its API may change with the + next releases of SymPy. + """ + if _lark is None: + raise ImportError("Lark is probably not installed") + return _lark_latex_parser.doparse(s) + + +def _pretty_print_lark_trees(tree, indent=0, show_expr=True): + if isinstance(tree, _lark.Token): + return tree.value + + data = str(tree.data) + + is_expr = data.startswith("expression") + + if is_expr: + data = re.sub(r"^expression", "E", data) + + is_ambig = (data == "_ambig") + + if is_ambig: + new_indent = indent + 2 + else: + new_indent = indent + + output = "" + show_node = not is_expr or show_expr + + if show_node: + output += str(data) + "(" + + if is_ambig: + output += "\n" + "\n".join([" " * new_indent + _pretty_print_lark_trees(i, new_indent, show_expr) for i in tree.children]) + else: + output += ",".join([_pretty_print_lark_trees(i, new_indent, show_expr) for i in tree.children]) + + if show_node: + output += ")" + + return output diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/transformer.py b/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/transformer.py new file mode 100644 index 0000000000000000000000000000000000000000..cbd514b6517336207a57de6d28bcce25858071dc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/latex/lark/transformer.py @@ -0,0 +1,730 @@ +import re + +import sympy +from sympy.external import import_module +from sympy.parsing.latex.errors import LaTeXParsingError + +lark = import_module("lark") + +if lark: + from lark import Transformer, Token, Tree # type: ignore +else: + class Transformer: # type: ignore + def transform(self, *args): + pass + + + class Token: # type: ignore + pass + + + class Tree: # type: ignore + pass + + +# noinspection PyPep8Naming,PyMethodMayBeStatic +class TransformToSymPyExpr(Transformer): + """Returns a SymPy expression that is generated by traversing the ``lark.Tree`` + passed to the ``.transform()`` function. + + Notes + ===== + + **This class is never supposed to be used directly.** + + In order to tweak the behavior of this class, it has to be subclassed and then after + the required modifications are made, the name of the new class should be passed to + the :py:class:`LarkLaTeXParser` class by using the ``transformer`` argument in the + constructor. + + Parameters + ========== + + visit_tokens : bool, optional + For information about what this option does, see `here + `_. + + Note that the option must be set to ``True`` for the default parser to work. + """ + + SYMBOL = sympy.Symbol + DIGIT = sympy.core.numbers.Integer + + def CMD_INFTY(self, tokens): + return sympy.oo + + def GREEK_SYMBOL_WITH_PRIMES(self, tokens): + # we omit the first character because it is a backslash. Also, if the variable name has "var" in it, + # like "varphi" or "varepsilon", we remove that too + variable_name = re.sub("var", "", tokens[1:]) + + return sympy.Symbol(variable_name) + + def LATIN_SYMBOL_WITH_LATIN_SUBSCRIPT(self, tokens): + base, sub = tokens.value.split("_") + if sub.startswith("{"): + return sympy.Symbol("%s_{%s}" % (base, sub[1:-1])) + else: + return sympy.Symbol("%s_{%s}" % (base, sub)) + + def GREEK_SYMBOL_WITH_LATIN_SUBSCRIPT(self, tokens): + base, sub = tokens.value.split("_") + greek_letter = re.sub("var", "", base[1:]) + + if sub.startswith("{"): + return sympy.Symbol("%s_{%s}" % (greek_letter, sub[1:-1])) + else: + return sympy.Symbol("%s_{%s}" % (greek_letter, sub)) + + def LATIN_SYMBOL_WITH_GREEK_SUBSCRIPT(self, tokens): + base, sub = tokens.value.split("_") + if sub.startswith("{"): + greek_letter = sub[2:-1] + else: + greek_letter = sub[1:] + + greek_letter = re.sub("var", "", greek_letter) + return sympy.Symbol("%s_{%s}" % (base, greek_letter)) + + + def GREEK_SYMBOL_WITH_GREEK_SUBSCRIPT(self, tokens): + base, sub = tokens.value.split("_") + greek_base = re.sub("var", "", base[1:]) + + if sub.startswith("{"): + greek_sub = sub[2:-1] + else: + greek_sub = sub[1:] + + greek_sub = re.sub("var", "", greek_sub) + return sympy.Symbol("%s_{%s}" % (greek_base, greek_sub)) + + def multi_letter_symbol(self, tokens): + if len(tokens) == 4: # no primes (single quotes) on symbol + return sympy.Symbol(tokens[2]) + if len(tokens) == 5: # there are primes on the symbol + return sympy.Symbol(tokens[2] + tokens[4]) + + def number(self, tokens): + if tokens[0].type == "CMD_IMAGINARY_UNIT": + return sympy.I + + if "." in tokens[0]: + return sympy.core.numbers.Float(tokens[0]) + else: + return sympy.core.numbers.Integer(tokens[0]) + + def latex_string(self, tokens): + return tokens[0] + + def group_round_parentheses(self, tokens): + return tokens[1] + + def group_square_brackets(self, tokens): + return tokens[1] + + def group_curly_parentheses(self, tokens): + return tokens[1] + + def eq(self, tokens): + return sympy.Eq(tokens[0], tokens[2]) + + def ne(self, tokens): + return sympy.Ne(tokens[0], tokens[2]) + + def lt(self, tokens): + return sympy.Lt(tokens[0], tokens[2]) + + def lte(self, tokens): + return sympy.Le(tokens[0], tokens[2]) + + def gt(self, tokens): + return sympy.Gt(tokens[0], tokens[2]) + + def gte(self, tokens): + return sympy.Ge(tokens[0], tokens[2]) + + def add(self, tokens): + if len(tokens) == 2: # +a + return tokens[1] + if len(tokens) == 3: # a + b + lh = tokens[0] + rh = tokens[2] + + if self._obj_is_sympy_Matrix(lh) or self._obj_is_sympy_Matrix(rh): + return sympy.MatAdd(lh, rh) + + return sympy.Add(lh, rh) + + def sub(self, tokens): + if len(tokens) == 2: # -a + x = tokens[1] + + if self._obj_is_sympy_Matrix(x): + return sympy.MatMul(-1, x) + + return -x + if len(tokens) == 3: # a - b + lh = tokens[0] + rh = tokens[2] + + if self._obj_is_sympy_Matrix(lh) or self._obj_is_sympy_Matrix(rh): + return sympy.MatAdd(lh, sympy.MatMul(-1, rh)) + + return sympy.Add(lh, -rh) + + def mul(self, tokens): + lh = tokens[0] + rh = tokens[2] + + if self._obj_is_sympy_Matrix(lh) or self._obj_is_sympy_Matrix(rh): + return sympy.MatMul(lh, rh) + + return sympy.Mul(lh, rh) + + def div(self, tokens): + return self._handle_division(tokens[0], tokens[2]) + + def adjacent_expressions(self, tokens): + # Most of the time, if two expressions are next to each other, it means implicit multiplication, + # but not always + from sympy.physics.quantum import Bra, Ket + if isinstance(tokens[0], Ket) and isinstance(tokens[1], Bra): + from sympy.physics.quantum import OuterProduct + return OuterProduct(tokens[0], tokens[1]) + elif tokens[0] == sympy.Symbol("d"): + # If the leftmost token is a "d", then it is highly likely that this is a differential + return tokens[0], tokens[1] + elif isinstance(tokens[0], tuple): + # then we have a derivative + return sympy.Derivative(tokens[1], tokens[0][1]) + else: + return sympy.Mul(tokens[0], tokens[1]) + + def superscript(self, tokens): + def isprime(x): + return isinstance(x, Token) and x.type == "PRIMES" + + def iscmdprime(x): + return isinstance(x, Token) and (x.type == "PRIMES_VIA_CMD" + or x.type == "CMD_PRIME") + + def isstar(x): + return isinstance(x, Token) and x.type == "STARS" + + def iscmdstar(x): + return isinstance(x, Token) and (x.type == "STARS_VIA_CMD" + or x.type == "CMD_ASTERISK") + + base = tokens[0] + if len(tokens) == 3: # a^b OR a^\prime OR a^\ast + sup = tokens[2] + if len(tokens) == 5: + # a^{'}, a^{''}, ... OR + # a^{*}, a^{**}, ... OR + # a^{\prime}, a^{\prime\prime}, ... OR + # a^{\ast}, a^{\ast\ast}, ... + sup = tokens[3] + + if self._obj_is_sympy_Matrix(base): + if sup == sympy.Symbol("T"): + return sympy.Transpose(base) + if sup == sympy.Symbol("H"): + return sympy.adjoint(base) + if isprime(sup): + sup = sup.value + if len(sup) % 2 == 0: + return base + return sympy.Transpose(base) + if iscmdprime(sup): + sup = sup.value + if (len(sup)/len(r"\prime")) % 2 == 0: + return base + return sympy.Transpose(base) + if isstar(sup): + sup = sup.value + # need .doit() in order to be consistent with + # sympy.adjoint() which returns the evaluated adjoint + # of a matrix + if len(sup) % 2 == 0: + return base.doit() + return sympy.adjoint(base) + if iscmdstar(sup): + sup = sup.value + # need .doit() for same reason as above + if (len(sup)/len(r"\ast")) % 2 == 0: + return base.doit() + return sympy.adjoint(base) + + if isprime(sup) or iscmdprime(sup) or isstar(sup) or iscmdstar(sup): + raise LaTeXParsingError(f"{base} with superscript {sup} is not understood.") + + return sympy.Pow(base, sup) + + def matrix_prime(self, tokens): + base = tokens[0] + primes = tokens[1].value + + if not self._obj_is_sympy_Matrix(base): + raise LaTeXParsingError(f"({base}){primes} is not understood.") + + if len(primes) % 2 == 0: + return base + + return sympy.Transpose(base) + + def symbol_prime(self, tokens): + base = tokens[0] + primes = tokens[1].value + + return sympy.Symbol(f"{base.name}{primes}") + + def fraction(self, tokens): + numerator = tokens[1] + if isinstance(tokens[2], tuple): + # we only need the variable w.r.t. which we are differentiating + _, variable = tokens[2] + + # we will pass this information upwards + return "derivative", variable + else: + denominator = tokens[2] + return self._handle_division(numerator, denominator) + + def binomial(self, tokens): + return sympy.binomial(tokens[1], tokens[2]) + + def normal_integral(self, tokens): + underscore_index = None + caret_index = None + + if "_" in tokens: + # we need to know the index because the next item in the list is the + # arguments for the lower bound of the integral + underscore_index = tokens.index("_") + + if "^" in tokens: + # we need to know the index because the next item in the list is the + # arguments for the upper bound of the integral + caret_index = tokens.index("^") + + lower_bound = tokens[underscore_index + 1] if underscore_index else None + upper_bound = tokens[caret_index + 1] if caret_index else None + + differential_symbol = self._extract_differential_symbol(tokens) + + if differential_symbol is None: + raise LaTeXParsingError("Differential symbol was not found in the expression." + "Valid differential symbols are \"d\", \"\\text{d}, and \"\\mathrm{d}\".") + + # else we can assume that a differential symbol was found + differential_variable_index = tokens.index(differential_symbol) + 1 + differential_variable = tokens[differential_variable_index] + + # we can't simply do something like `if (lower_bound and not upper_bound) ...` because this would + # evaluate to `True` if the `lower_bound` is 0 and upper bound is non-zero + if lower_bound is not None and upper_bound is None: + # then one was given and the other wasn't + raise LaTeXParsingError("Lower bound for the integral was found, but upper bound was not found.") + + if upper_bound is not None and lower_bound is None: + # then one was given and the other wasn't + raise LaTeXParsingError("Upper bound for the integral was found, but lower bound was not found.") + + # check if any expression was given or not. If it wasn't, then set the integrand to 1. + if underscore_index is not None and underscore_index == differential_variable_index - 3: + # The Token at differential_variable_index - 2 should be the integrand. However, if going one more step + # backwards after that gives us the underscore, then that means that there _was_ no integrand. + # Example: \int^7_0 dx + integrand = 1 + elif caret_index is not None and caret_index == differential_variable_index - 3: + # The Token at differential_variable_index - 2 should be the integrand. However, if going one more step + # backwards after that gives us the caret, then that means that there _was_ no integrand. + # Example: \int_0^7 dx + integrand = 1 + elif differential_variable_index == 2: + # this means we have something like "\int dx", because the "\int" symbol will always be + # at index 0 in `tokens` + integrand = 1 + else: + # The Token at differential_variable_index - 1 is the differential symbol itself, so we need to go one + # more step before that. + integrand = tokens[differential_variable_index - 2] + + if lower_bound is not None: + # then we have a definite integral + + # we can assume that either both the lower and upper bounds are given, or + # neither of them are + return sympy.Integral(integrand, (differential_variable, lower_bound, upper_bound)) + else: + # we have an indefinite integral + return sympy.Integral(integrand, differential_variable) + + def group_curly_parentheses_int(self, tokens): + # return signature is a tuple consisting of the expression in the numerator, along with the variable of + # integration + if len(tokens) == 3: + return 1, tokens[1] + elif len(tokens) == 4: + return tokens[1], tokens[2] + # there are no other possibilities + + def special_fraction(self, tokens): + numerator, variable = tokens[1] + denominator = tokens[2] + + # We pass the integrand, along with information about the variable of integration, upw + return sympy.Mul(numerator, sympy.Pow(denominator, -1)), variable + + def integral_with_special_fraction(self, tokens): + underscore_index = None + caret_index = None + + if "_" in tokens: + # we need to know the index because the next item in the list is the + # arguments for the lower bound of the integral + underscore_index = tokens.index("_") + + if "^" in tokens: + # we need to know the index because the next item in the list is the + # arguments for the upper bound of the integral + caret_index = tokens.index("^") + + lower_bound = tokens[underscore_index + 1] if underscore_index else None + upper_bound = tokens[caret_index + 1] if caret_index else None + + # we can't simply do something like `if (lower_bound and not upper_bound) ...` because this would + # evaluate to `True` if the `lower_bound` is 0 and upper bound is non-zero + if lower_bound is not None and upper_bound is None: + # then one was given and the other wasn't + raise LaTeXParsingError("Lower bound for the integral was found, but upper bound was not found.") + + if upper_bound is not None and lower_bound is None: + # then one was given and the other wasn't + raise LaTeXParsingError("Upper bound for the integral was found, but lower bound was not found.") + + integrand, differential_variable = tokens[-1] + + if lower_bound is not None: + # then we have a definite integral + + # we can assume that either both the lower and upper bounds are given, or + # neither of them are + return sympy.Integral(integrand, (differential_variable, lower_bound, upper_bound)) + else: + # we have an indefinite integral + return sympy.Integral(integrand, differential_variable) + + def group_curly_parentheses_special(self, tokens): + underscore_index = tokens.index("_") + caret_index = tokens.index("^") + + # given the type of expressions we are parsing, we can assume that the lower limit + # will always use braces around its arguments. This is because we don't support + # converting unconstrained sums into SymPy expressions. + + # first we isolate the bottom limit + left_brace_index = tokens.index("{", underscore_index) + right_brace_index = tokens.index("}", underscore_index) + + bottom_limit = tokens[left_brace_index + 1: right_brace_index] + + # next, we isolate the upper limit + top_limit = tokens[caret_index + 1:] + + # the code below will be useful for supporting things like `\sum_{n = 0}^{n = 5} n^2` + # if "{" in top_limit: + # left_brace_index = tokens.index("{", caret_index) + # if left_brace_index != -1: + # # then there's a left brace in the string, and we need to find the closing right brace + # right_brace_index = tokens.index("}", caret_index) + # top_limit = tokens[left_brace_index + 1: right_brace_index] + + # print(f"top limit = {top_limit}") + + index_variable = bottom_limit[0] + lower_limit = bottom_limit[-1] + upper_limit = top_limit[0] # for now, the index will always be 0 + + # print(f"return value = ({index_variable}, {lower_limit}, {upper_limit})") + + return index_variable, lower_limit, upper_limit + + def summation(self, tokens): + return sympy.Sum(tokens[2], tokens[1]) + + def product(self, tokens): + return sympy.Product(tokens[2], tokens[1]) + + def limit_dir_expr(self, tokens): + caret_index = tokens.index("^") + + if "{" in tokens: + left_curly_brace_index = tokens.index("{", caret_index) + direction = tokens[left_curly_brace_index + 1] + else: + direction = tokens[caret_index + 1] + + if direction == "+": + return tokens[0], "+" + elif direction == "-": + return tokens[0], "-" + else: + return tokens[0], "+-" + + def group_curly_parentheses_lim(self, tokens): + limit_variable = tokens[1] + if isinstance(tokens[3], tuple): + destination, direction = tokens[3] + else: + destination = tokens[3] + direction = "+-" + + return limit_variable, destination, direction + + def limit(self, tokens): + limit_variable, destination, direction = tokens[2] + + return sympy.Limit(tokens[-1], limit_variable, destination, direction) + + def differential(self, tokens): + return tokens[1] + + def derivative(self, tokens): + return sympy.Derivative(tokens[-1], tokens[5]) + + def list_of_expressions(self, tokens): + if len(tokens) == 1: + # we return it verbatim because the function_applied node expects + # a list + return tokens + else: + def remove_tokens(args): + if isinstance(args, Token): + if args.type != "COMMA": + # An unexpected token was encountered + raise LaTeXParsingError("A comma token was expected, but some other token was encountered.") + return False + return True + + return filter(remove_tokens, tokens) + + def function_applied(self, tokens): + return sympy.Function(tokens[0])(*tokens[2]) + + def min(self, tokens): + return sympy.Min(*tokens[2]) + + def max(self, tokens): + return sympy.Max(*tokens[2]) + + def bra(self, tokens): + from sympy.physics.quantum import Bra + return Bra(tokens[1]) + + def ket(self, tokens): + from sympy.physics.quantum import Ket + return Ket(tokens[1]) + + def inner_product(self, tokens): + from sympy.physics.quantum import Bra, Ket, InnerProduct + return InnerProduct(Bra(tokens[1]), Ket(tokens[3])) + + def sin(self, tokens): + return sympy.sin(tokens[1]) + + def cos(self, tokens): + return sympy.cos(tokens[1]) + + def tan(self, tokens): + return sympy.tan(tokens[1]) + + def csc(self, tokens): + return sympy.csc(tokens[1]) + + def sec(self, tokens): + return sympy.sec(tokens[1]) + + def cot(self, tokens): + return sympy.cot(tokens[1]) + + def sin_power(self, tokens): + exponent = tokens[2] + if exponent == -1: + return sympy.asin(tokens[-1]) + else: + return sympy.Pow(sympy.sin(tokens[-1]), exponent) + + def cos_power(self, tokens): + exponent = tokens[2] + if exponent == -1: + return sympy.acos(tokens[-1]) + else: + return sympy.Pow(sympy.cos(tokens[-1]), exponent) + + def tan_power(self, tokens): + exponent = tokens[2] + if exponent == -1: + return sympy.atan(tokens[-1]) + else: + return sympy.Pow(sympy.tan(tokens[-1]), exponent) + + def csc_power(self, tokens): + exponent = tokens[2] + if exponent == -1: + return sympy.acsc(tokens[-1]) + else: + return sympy.Pow(sympy.csc(tokens[-1]), exponent) + + def sec_power(self, tokens): + exponent = tokens[2] + if exponent == -1: + return sympy.asec(tokens[-1]) + else: + return sympy.Pow(sympy.sec(tokens[-1]), exponent) + + def cot_power(self, tokens): + exponent = tokens[2] + if exponent == -1: + return sympy.acot(tokens[-1]) + else: + return sympy.Pow(sympy.cot(tokens[-1]), exponent) + + def arcsin(self, tokens): + return sympy.asin(tokens[1]) + + def arccos(self, tokens): + return sympy.acos(tokens[1]) + + def arctan(self, tokens): + return sympy.atan(tokens[1]) + + def arccsc(self, tokens): + return sympy.acsc(tokens[1]) + + def arcsec(self, tokens): + return sympy.asec(tokens[1]) + + def arccot(self, tokens): + return sympy.acot(tokens[1]) + + def sinh(self, tokens): + return sympy.sinh(tokens[1]) + + def cosh(self, tokens): + return sympy.cosh(tokens[1]) + + def tanh(self, tokens): + return sympy.tanh(tokens[1]) + + def asinh(self, tokens): + return sympy.asinh(tokens[1]) + + def acosh(self, tokens): + return sympy.acosh(tokens[1]) + + def atanh(self, tokens): + return sympy.atanh(tokens[1]) + + def abs(self, tokens): + return sympy.Abs(tokens[1]) + + def floor(self, tokens): + return sympy.floor(tokens[1]) + + def ceil(self, tokens): + return sympy.ceiling(tokens[1]) + + def factorial(self, tokens): + return sympy.factorial(tokens[0]) + + def conjugate(self, tokens): + return sympy.conjugate(tokens[1]) + + def square_root(self, tokens): + if len(tokens) == 2: + # then there was no square bracket argument + return sympy.sqrt(tokens[1]) + elif len(tokens) == 3: + # then there _was_ a square bracket argument + return sympy.root(tokens[2], tokens[1]) + + def exponential(self, tokens): + return sympy.exp(tokens[1]) + + def log(self, tokens): + if tokens[0].type == "FUNC_LG": + # we don't need to check if there's an underscore or not because having one + # in this case would be meaningless + # TODO: ANTLR refers to ISO 80000-2:2019. should we keep base 10 or base 2? + return sympy.log(tokens[1], 10) + elif tokens[0].type == "FUNC_LN": + return sympy.log(tokens[1]) + elif tokens[0].type == "FUNC_LOG": + # we check if a base was specified or not + if "_" in tokens: + # then a base was specified + return sympy.log(tokens[3], tokens[2]) + else: + # a base was not specified + return sympy.log(tokens[1]) + + def _extract_differential_symbol(self, s: str): + differential_symbols = {"d", r"\text{d}", r"\mathrm{d}"} + + differential_symbol = next((symbol for symbol in differential_symbols if symbol in s), None) + + return differential_symbol + + def matrix(self, tokens): + def is_matrix_row(x): + return (isinstance(x, Tree) and x.data == "matrix_row") + + def is_not_col_delim(y): + return (not isinstance(y, Token) or y.type != "MATRIX_COL_DELIM") + + matrix_body = tokens[1].children + return sympy.Matrix([[y for y in x.children if is_not_col_delim(y)] + for x in matrix_body if is_matrix_row(x)]) + + def determinant(self, tokens): + if len(tokens) == 2: # \det A + if not self._obj_is_sympy_Matrix(tokens[1]): + raise LaTeXParsingError("Cannot take determinant of non-matrix.") + + return tokens[1].det() + + if len(tokens) == 3: # | A | + return self.matrix(tokens).det() + + def trace(self, tokens): + if not self._obj_is_sympy_Matrix(tokens[1]): + raise LaTeXParsingError("Cannot take trace of non-matrix.") + + return sympy.Trace(tokens[1]) + + def adjugate(self, tokens): + if not self._obj_is_sympy_Matrix(tokens[1]): + raise LaTeXParsingError("Cannot take adjugate of non-matrix.") + + # need .doit() since MatAdd does not support .adjugate() method + return tokens[1].doit().adjugate() + + def _obj_is_sympy_Matrix(self, obj): + if hasattr(obj, "is_Matrix"): + return obj.is_Matrix + + return isinstance(obj, sympy.Matrix) + + def _handle_division(self, numerator, denominator): + if self._obj_is_sympy_Matrix(denominator): + raise LaTeXParsingError("Cannot divide by matrices like this since " + "it is not clear if left or right multiplication " + "by the inverse is intended. Try explicitly " + "multiplying by the inverse instead.") + + if self._obj_is_sympy_Matrix(numerator): + return sympy.MatMul(numerator, sympy.Pow(denominator, -1)) + + return sympy.Mul(numerator, sympy.Pow(denominator, -1)) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/mathematica.py b/venv/lib/python3.11/site-packages/sympy/parsing/mathematica.py new file mode 100644 index 0000000000000000000000000000000000000000..b5824a8c33ee402d03e6c5617eeeea21d4a457d1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/mathematica.py @@ -0,0 +1,1085 @@ +from __future__ import annotations +import re +import typing +from itertools import product +from typing import Any, Callable + +import sympy +from sympy import Mul, Add, Pow, Rational, log, exp, sqrt, cos, sin, tan, asin, acos, acot, asec, acsc, sinh, cosh, tanh, asinh, \ + acosh, atanh, acoth, asech, acsch, expand, im, flatten, polylog, cancel, expand_trig, sign, simplify, \ + UnevaluatedExpr, S, atan, atan2, Mod, Max, Min, rf, Ei, Si, Ci, airyai, airyaiprime, airybi, primepi, prime, \ + isprime, cot, sec, csc, csch, sech, coth, Function, I, pi, Tuple, GreaterThan, StrictGreaterThan, StrictLessThan, \ + LessThan, Equality, Or, And, Lambda, Integer, Dummy, symbols +from sympy.core.sympify import sympify, _sympify +from sympy.functions.special.bessel import airybiprime +from sympy.functions.special.error_functions import li +from sympy.utilities.exceptions import sympy_deprecation_warning + + +def mathematica(s, additional_translations=None): + sympy_deprecation_warning( + """The ``mathematica`` function for the Mathematica parser is now +deprecated. Use ``parse_mathematica`` instead. +The parameter ``additional_translation`` can be replaced by SymPy's +.replace( ) or .subs( ) methods on the output expression instead.""", + deprecated_since_version="1.11", + active_deprecations_target="mathematica-parser-new", + ) + parser = MathematicaParser(additional_translations) + return sympify(parser._parse_old(s)) + + +def parse_mathematica(s): + """ + Translate a string containing a Wolfram Mathematica expression to a SymPy + expression. + + If the translator is unable to find a suitable SymPy expression, the + ``FullForm`` of the Mathematica expression will be output, using SymPy + ``Function`` objects as nodes of the syntax tree. + + Examples + ======== + + >>> from sympy.parsing.mathematica import parse_mathematica + >>> parse_mathematica("Sin[x]^2 Tan[y]") + sin(x)**2*tan(y) + >>> e = parse_mathematica("F[7,5,3]") + >>> e + F(7, 5, 3) + >>> from sympy import Function, Max, Min + >>> e.replace(Function("F"), lambda *x: Max(*x)*Min(*x)) + 21 + + Both standard input form and Mathematica full form are supported: + + >>> parse_mathematica("x*(a + b)") + x*(a + b) + >>> parse_mathematica("Times[x, Plus[a, b]]") + x*(a + b) + + To get a matrix from Wolfram's code: + + >>> m = parse_mathematica("{{a, b}, {c, d}}") + >>> m + ((a, b), (c, d)) + >>> from sympy import Matrix + >>> Matrix(m) + Matrix([ + [a, b], + [c, d]]) + + If the translation into equivalent SymPy expressions fails, an SymPy + expression equivalent to Wolfram Mathematica's "FullForm" will be created: + + >>> parse_mathematica("x_.") + Optional(Pattern(x, Blank())) + >>> parse_mathematica("Plus @@ {x, y, z}") + Apply(Plus, (x, y, z)) + >>> parse_mathematica("f[x_, 3] := x^3 /; x > 0") + SetDelayed(f(Pattern(x, Blank()), 3), Condition(x**3, x > 0)) + """ + parser = MathematicaParser() + return parser.parse(s) + + +def _parse_Function(*args): + if len(args) == 1: + arg = args[0] + Slot = Function("Slot") + slots = arg.atoms(Slot) + numbers = [a.args[0] for a in slots] + number_of_arguments = max(numbers) + if isinstance(number_of_arguments, Integer): + variables = symbols(f"dummy0:{number_of_arguments}", cls=Dummy) + return Lambda(variables, arg.xreplace({Slot(i+1): v for i, v in enumerate(variables)})) + return Lambda((), arg) + elif len(args) == 2: + variables = args[0] + body = args[1] + return Lambda(variables, body) + else: + raise SyntaxError("Function node expects 1 or 2 arguments") + + +def _deco(cls): + cls._initialize_class() + return cls + + +@_deco +class MathematicaParser: + """ + An instance of this class converts a string of a Wolfram Mathematica + expression to a SymPy expression. + + The main parser acts internally in three stages: + + 1. tokenizer: tokenizes the Mathematica expression and adds the missing * + operators. Handled by ``_from_mathematica_to_tokens(...)`` + 2. full form list: sort the list of strings output by the tokenizer into a + syntax tree of nested lists and strings, equivalent to Mathematica's + ``FullForm`` expression output. This is handled by the function + ``_from_tokens_to_fullformlist(...)``. + 3. SymPy expression: the syntax tree expressed as full form list is visited + and the nodes with equivalent classes in SymPy are replaced. Unknown + syntax tree nodes are cast to SymPy ``Function`` objects. This is + handled by ``_from_fullformlist_to_sympy(...)``. + + """ + + # left: Mathematica, right: SymPy + CORRESPONDENCES = { + 'Sqrt[x]': 'sqrt(x)', + 'Rational[x,y]': 'Rational(x,y)', + 'Exp[x]': 'exp(x)', + 'Log[x]': 'log(x)', + 'Log[x,y]': 'log(y,x)', + 'Log2[x]': 'log(x,2)', + 'Log10[x]': 'log(x,10)', + 'Mod[x,y]': 'Mod(x,y)', + 'Max[*x]': 'Max(*x)', + 'Min[*x]': 'Min(*x)', + 'Pochhammer[x,y]':'rf(x,y)', + 'ArcTan[x,y]':'atan2(y,x)', + 'ExpIntegralEi[x]': 'Ei(x)', + 'SinIntegral[x]': 'Si(x)', + 'CosIntegral[x]': 'Ci(x)', + 'AiryAi[x]': 'airyai(x)', + 'AiryAiPrime[x]': 'airyaiprime(x)', + 'AiryBi[x]' :'airybi(x)', + 'AiryBiPrime[x]' :'airybiprime(x)', + 'LogIntegral[x]':' li(x)', + 'PrimePi[x]': 'primepi(x)', + 'Prime[x]': 'prime(x)', + 'PrimeQ[x]': 'isprime(x)' + } + + # trigonometric, e.t.c. + for arc, tri, h in product(('', 'Arc'), ( + 'Sin', 'Cos', 'Tan', 'Cot', 'Sec', 'Csc'), ('', 'h')): + fm = arc + tri + h + '[x]' + if arc: # arc func + fs = 'a' + tri.lower() + h + '(x)' + else: # non-arc func + fs = tri.lower() + h + '(x)' + CORRESPONDENCES.update({fm: fs}) + + REPLACEMENTS = { + ' ': '', + '^': '**', + '{': '[', + '}': ']', + } + + RULES = { + # a single whitespace to '*' + 'whitespace': ( + re.compile(r''' + (?:(?<=[a-zA-Z\d])|(?<=\d\.)) # a letter or a number + \s+ # any number of whitespaces + (?:(?=[a-zA-Z\d])|(?=\.\d)) # a letter or a number + ''', re.VERBOSE), + '*'), + + # add omitted '*' character + 'add*_1': ( + re.compile(r''' + (?:(?<=[])\d])|(?<=\d\.)) # ], ) or a number + # '' + (?=[(a-zA-Z]) # ( or a single letter + ''', re.VERBOSE), + '*'), + + # add omitted '*' character (variable letter preceding) + 'add*_2': ( + re.compile(r''' + (?<=[a-zA-Z]) # a letter + \( # ( as a character + (?=.) # any characters + ''', re.VERBOSE), + '*('), + + # convert 'Pi' to 'pi' + 'Pi': ( + re.compile(r''' + (?: + \A|(?<=[^a-zA-Z]) + ) + Pi # 'Pi' is 3.14159... in Mathematica + (?=[^a-zA-Z]) + ''', re.VERBOSE), + 'pi'), + } + + # Mathematica function name pattern + FM_PATTERN = re.compile(r''' + (?: + \A|(?<=[^a-zA-Z]) # at the top or a non-letter + ) + [A-Z][a-zA-Z\d]* # Function + (?=\[) # [ as a character + ''', re.VERBOSE) + + # list or matrix pattern (for future usage) + ARG_MTRX_PATTERN = re.compile(r''' + \{.*\} + ''', re.VERBOSE) + + # regex string for function argument pattern + ARGS_PATTERN_TEMPLATE = r''' + (?: + \A|(?<=[^a-zA-Z]) + ) + {arguments} # model argument like x, y,... + (?=[^a-zA-Z]) + ''' + + # will contain transformed CORRESPONDENCES dictionary + TRANSLATIONS: dict[tuple[str, int], dict[str, Any]] = {} + + # cache for a raw users' translation dictionary + cache_original: dict[tuple[str, int], dict[str, Any]] = {} + + # cache for a compiled users' translation dictionary + cache_compiled: dict[tuple[str, int], dict[str, Any]] = {} + + @classmethod + def _initialize_class(cls): + # get a transformed CORRESPONDENCES dictionary + d = cls._compile_dictionary(cls.CORRESPONDENCES) + cls.TRANSLATIONS.update(d) + + def __init__(self, additional_translations=None): + self.translations = {} + + # update with TRANSLATIONS (class constant) + self.translations.update(self.TRANSLATIONS) + + if additional_translations is None: + additional_translations = {} + + # check the latest added translations + if self.__class__.cache_original != additional_translations: + if not isinstance(additional_translations, dict): + raise ValueError('The argument must be dict type') + + # get a transformed additional_translations dictionary + d = self._compile_dictionary(additional_translations) + + # update cache + self.__class__.cache_original = additional_translations + self.__class__.cache_compiled = d + + # merge user's own translations + self.translations.update(self.__class__.cache_compiled) + + @classmethod + def _compile_dictionary(cls, dic): + # for return + d = {} + + for fm, fs in dic.items(): + # check function form + cls._check_input(fm) + cls._check_input(fs) + + # uncover '*' hiding behind a whitespace + fm = cls._apply_rules(fm, 'whitespace') + fs = cls._apply_rules(fs, 'whitespace') + + # remove whitespace(s) + fm = cls._replace(fm, ' ') + fs = cls._replace(fs, ' ') + + # search Mathematica function name + m = cls.FM_PATTERN.search(fm) + + # if no-hit + if m is None: + err = "'{f}' function form is invalid.".format(f=fm) + raise ValueError(err) + + # get Mathematica function name like 'Log' + fm_name = m.group() + + # get arguments of Mathematica function + args, end = cls._get_args(m) + + # function side check. (e.g.) '2*Func[x]' is invalid. + if m.start() != 0 or end != len(fm): + err = "'{f}' function form is invalid.".format(f=fm) + raise ValueError(err) + + # check the last argument's 1st character + if args[-1][0] == '*': + key_arg = '*' + else: + key_arg = len(args) + + key = (fm_name, key_arg) + + # convert '*x' to '\\*x' for regex + re_args = [x if x[0] != '*' else '\\' + x for x in args] + + # for regex. Example: (?:(x|y|z)) + xyz = '(?:(' + '|'.join(re_args) + '))' + + # string for regex compile + patStr = cls.ARGS_PATTERN_TEMPLATE.format(arguments=xyz) + + pat = re.compile(patStr, re.VERBOSE) + + # update dictionary + d[key] = {} + d[key]['fs'] = fs # SymPy function template + d[key]['args'] = args # args are ['x', 'y'] for example + d[key]['pat'] = pat + + return d + + def _convert_function(self, s): + '''Parse Mathematica function to SymPy one''' + + # compiled regex object + pat = self.FM_PATTERN + + scanned = '' # converted string + cur = 0 # position cursor + while True: + m = pat.search(s) + + if m is None: + # append the rest of string + scanned += s + break + + # get Mathematica function name + fm = m.group() + + # get arguments, and the end position of fm function + args, end = self._get_args(m) + + # the start position of fm function + bgn = m.start() + + # convert Mathematica function to SymPy one + s = self._convert_one_function(s, fm, args, bgn, end) + + # update cursor + cur = bgn + + # append converted part + scanned += s[:cur] + + # shrink s + s = s[cur:] + + return scanned + + def _convert_one_function(self, s, fm, args, bgn, end): + # no variable-length argument + if (fm, len(args)) in self.translations: + key = (fm, len(args)) + + # x, y,... model arguments + x_args = self.translations[key]['args'] + + # make CORRESPONDENCES between model arguments and actual ones + d = dict(zip(x_args, args)) + + # with variable-length argument + elif (fm, '*') in self.translations: + key = (fm, '*') + + # x, y,..*args (model arguments) + x_args = self.translations[key]['args'] + + # make CORRESPONDENCES between model arguments and actual ones + d = {} + for i, x in enumerate(x_args): + if x[0] == '*': + d[x] = ','.join(args[i:]) + break + d[x] = args[i] + + # out of self.translations + else: + err = "'{f}' is out of the whitelist.".format(f=fm) + raise ValueError(err) + + # template string of converted function + template = self.translations[key]['fs'] + + # regex pattern for x_args + pat = self.translations[key]['pat'] + + scanned = '' + cur = 0 + while True: + m = pat.search(template) + + if m is None: + scanned += template + break + + # get model argument + x = m.group() + + # get a start position of the model argument + xbgn = m.start() + + # add the corresponding actual argument + scanned += template[:xbgn] + d[x] + + # update cursor to the end of the model argument + cur = m.end() + + # shrink template + template = template[cur:] + + # update to swapped string + s = s[:bgn] + scanned + s[end:] + + return s + + @classmethod + def _get_args(cls, m): + '''Get arguments of a Mathematica function''' + + s = m.string # whole string + anc = m.end() + 1 # pointing the first letter of arguments + square, curly = [], [] # stack for brackets + args = [] + + # current cursor + cur = anc + for i, c in enumerate(s[anc:], anc): + # extract one argument + if c == ',' and (not square) and (not curly): + args.append(s[cur:i]) # add an argument + cur = i + 1 # move cursor + + # handle list or matrix (for future usage) + if c == '{': + curly.append(c) + elif c == '}': + curly.pop() + + # seek corresponding ']' with skipping irrevant ones + if c == '[': + square.append(c) + elif c == ']': + if square: + square.pop() + else: # empty stack + args.append(s[cur:i]) + break + + # the next position to ']' bracket (the function end) + func_end = i + 1 + + return args, func_end + + @classmethod + def _replace(cls, s, bef): + aft = cls.REPLACEMENTS[bef] + s = s.replace(bef, aft) + return s + + @classmethod + def _apply_rules(cls, s, bef): + pat, aft = cls.RULES[bef] + return pat.sub(aft, s) + + @classmethod + def _check_input(cls, s): + for bracket in (('[', ']'), ('{', '}'), ('(', ')')): + if s.count(bracket[0]) != s.count(bracket[1]): + err = "'{f}' function form is invalid.".format(f=s) + raise ValueError(err) + + if '{' in s: + err = "Currently list is not supported." + raise ValueError(err) + + def _parse_old(self, s): + # input check + self._check_input(s) + + # uncover '*' hiding behind a whitespace + s = self._apply_rules(s, 'whitespace') + + # remove whitespace(s) + s = self._replace(s, ' ') + + # add omitted '*' character + s = self._apply_rules(s, 'add*_1') + s = self._apply_rules(s, 'add*_2') + + # translate function + s = self._convert_function(s) + + # '^' to '**' + s = self._replace(s, '^') + + # 'Pi' to 'pi' + s = self._apply_rules(s, 'Pi') + + # '{', '}' to '[', ']', respectively +# s = cls._replace(s, '{') # currently list is not taken into account +# s = cls._replace(s, '}') + + return s + + def parse(self, s): + s2 = self._from_mathematica_to_tokens(s) + s3 = self._from_tokens_to_fullformlist(s2) + s4 = self._from_fullformlist_to_sympy(s3) + return s4 + + INFIX = "Infix" + PREFIX = "Prefix" + POSTFIX = "Postfix" + FLAT = "Flat" + RIGHT = "Right" + LEFT = "Left" + + _mathematica_op_precedence: list[tuple[str, str | None, dict[str, str | Callable]]] = [ + (POSTFIX, None, {";": lambda x: x + ["Null"] if isinstance(x, list) and x and x[0] == "CompoundExpression" else ["CompoundExpression", x, "Null"]}), + (INFIX, FLAT, {";": "CompoundExpression"}), + (INFIX, RIGHT, {"=": "Set", ":=": "SetDelayed", "+=": "AddTo", "-=": "SubtractFrom", "*=": "TimesBy", "/=": "DivideBy"}), + (INFIX, LEFT, {"//": lambda x, y: [x, y]}), + (POSTFIX, None, {"&": "Function"}), + (INFIX, LEFT, {"/.": "ReplaceAll"}), + (INFIX, RIGHT, {"->": "Rule", ":>": "RuleDelayed"}), + (INFIX, LEFT, {"/;": "Condition"}), + (INFIX, FLAT, {"|": "Alternatives"}), + (POSTFIX, None, {"..": "Repeated", "...": "RepeatedNull"}), + (INFIX, FLAT, {"||": "Or"}), + (INFIX, FLAT, {"&&": "And"}), + (PREFIX, None, {"!": "Not"}), + (INFIX, FLAT, {"===": "SameQ", "=!=": "UnsameQ"}), + (INFIX, FLAT, {"==": "Equal", "!=": "Unequal", "<=": "LessEqual", "<": "Less", ">=": "GreaterEqual", ">": "Greater"}), + (INFIX, None, {";;": "Span"}), + (INFIX, FLAT, {"+": "Plus", "-": "Plus"}), + (INFIX, FLAT, {"*": "Times", "/": "Times"}), + (INFIX, FLAT, {".": "Dot"}), + (PREFIX, None, {"-": lambda x: MathematicaParser._get_neg(x), + "+": lambda x: x}), + (INFIX, RIGHT, {"^": "Power"}), + (INFIX, RIGHT, {"@@": "Apply", "/@": "Map", "//@": "MapAll", "@@@": lambda x, y: ["Apply", x, y, ["List", "1"]]}), + (POSTFIX, None, {"'": "Derivative", "!": "Factorial", "!!": "Factorial2", "--": "Decrement"}), + (INFIX, None, {"[": lambda x, y: [x, *y], "[[": lambda x, y: ["Part", x, *y]}), + (PREFIX, None, {"{": lambda x: ["List", *x], "(": lambda x: x[0]}), + (INFIX, None, {"?": "PatternTest"}), + (POSTFIX, None, { + "_": lambda x: ["Pattern", x, ["Blank"]], + "_.": lambda x: ["Optional", ["Pattern", x, ["Blank"]]], + "__": lambda x: ["Pattern", x, ["BlankSequence"]], + "___": lambda x: ["Pattern", x, ["BlankNullSequence"]], + }), + (INFIX, None, {"_": lambda x, y: ["Pattern", x, ["Blank", y]]}), + (PREFIX, None, {"#": "Slot", "##": "SlotSequence"}), + ] + + _missing_arguments_default = { + "#": lambda: ["Slot", "1"], + "##": lambda: ["SlotSequence", "1"], + } + + _literal = r"[A-Za-z][A-Za-z0-9]*" + _number = r"(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)" + + _enclosure_open = ["(", "[", "[[", "{"] + _enclosure_close = [")", "]", "]]", "}"] + + @classmethod + def _get_neg(cls, x): + return f"-{x}" if isinstance(x, str) and re.match(MathematicaParser._number, x) else ["Times", "-1", x] + + @classmethod + def _get_inv(cls, x): + return ["Power", x, "-1"] + + _regex_tokenizer = None + + def _get_tokenizer(self): + if self._regex_tokenizer is not None: + # Check if the regular expression has already been compiled: + return self._regex_tokenizer + tokens = [self._literal, self._number] + tokens_escape = self._enclosure_open[:] + self._enclosure_close[:] + for typ, strat, symdict in self._mathematica_op_precedence: + for k in symdict: + tokens_escape.append(k) + tokens_escape.sort(key=lambda x: -len(x)) + tokens.extend(map(re.escape, tokens_escape)) + tokens.append(",") + tokens.append("\n") + tokenizer = re.compile("(" + "|".join(tokens) + ")") + self._regex_tokenizer = tokenizer + return self._regex_tokenizer + + def _from_mathematica_to_tokens(self, code: str): + tokenizer = self._get_tokenizer() + + # Find strings: + code_splits: list[str | list] = [] + while True: + string_start = code.find("\"") + if string_start == -1: + if len(code) > 0: + code_splits.append(code) + break + match_end = re.search(r'(? 0: + code_splits.append(code[:string_start]) + code_splits.append(["_Str", code[string_start+1:string_end].replace('\\"', '"')]) + code = code[string_end+1:] + + # Remove comments: + for i, code_split in enumerate(code_splits): + if isinstance(code_split, list): + continue + while True: + pos_comment_start = code_split.find("(*") + if pos_comment_start == -1: + break + pos_comment_end = code_split.find("*)") + if pos_comment_end == -1 or pos_comment_end < pos_comment_start: + raise SyntaxError("mismatch in comment (* *) code") + code_split = code_split[:pos_comment_start] + code_split[pos_comment_end+2:] + code_splits[i] = code_split + + # Tokenize the input strings with a regular expression: + token_lists = [tokenizer.findall(i) if isinstance(i, str) and i.isascii() else [i] for i in code_splits] + tokens = [j for i in token_lists for j in i] + + # Remove newlines at the beginning + while tokens and tokens[0] == "\n": + tokens.pop(0) + # Remove newlines at the end + while tokens and tokens[-1] == "\n": + tokens.pop(-1) + + return tokens + + def _is_op(self, token: str | list) -> bool: + if isinstance(token, list): + return False + if re.match(self._literal, token): + return False + if re.match("-?" + self._number, token): + return False + return True + + def _is_valid_star1(self, token: str | list) -> bool: + if token in (")", "}"): + return True + return not self._is_op(token) + + def _is_valid_star2(self, token: str | list) -> bool: + if token in ("(", "{"): + return True + return not self._is_op(token) + + def _from_tokens_to_fullformlist(self, tokens: list): + stack: list[list] = [[]] + open_seq = [] + pointer: int = 0 + while pointer < len(tokens): + token = tokens[pointer] + if token in self._enclosure_open: + stack[-1].append(token) + open_seq.append(token) + stack.append([]) + elif token == ",": + if len(stack[-1]) == 0 and stack[-2][-1] == open_seq[-1]: + raise SyntaxError("%s cannot be followed by comma ," % open_seq[-1]) + stack[-1] = self._parse_after_braces(stack[-1]) + stack.append([]) + elif token in self._enclosure_close: + ind = self._enclosure_close.index(token) + if self._enclosure_open[ind] != open_seq[-1]: + unmatched_enclosure = SyntaxError("unmatched enclosure") + if token == "]]" and open_seq[-1] == "[": + if open_seq[-2] == "[": + # These two lines would be logically correct, but are + # unnecessary: + # token = "]" + # tokens[pointer] = "]" + tokens.insert(pointer+1, "]") + elif open_seq[-2] == "[[": + if tokens[pointer+1] == "]": + tokens[pointer+1] = "]]" + elif tokens[pointer+1] == "]]": + tokens[pointer+1] = "]]" + tokens.insert(pointer+2, "]") + else: + raise unmatched_enclosure + else: + raise unmatched_enclosure + if len(stack[-1]) == 0 and stack[-2][-1] == "(": + raise SyntaxError("( ) not valid syntax") + last_stack = self._parse_after_braces(stack[-1], True) + stack[-1] = last_stack + new_stack_element = [] + while stack[-1][-1] != open_seq[-1]: + new_stack_element.append(stack.pop()) + new_stack_element.reverse() + if open_seq[-1] == "(" and len(new_stack_element) != 1: + raise SyntaxError("( must be followed by one expression, %i detected" % len(new_stack_element)) + stack[-1].append(new_stack_element) + open_seq.pop(-1) + else: + stack[-1].append(token) + pointer += 1 + if len(stack) != 1: + raise RuntimeError("Stack should have only one element") + return self._parse_after_braces(stack[0]) + + def _util_remove_newlines(self, lines: list, tokens: list, inside_enclosure: bool): + pointer = 0 + size = len(tokens) + while pointer < size: + token = tokens[pointer] + if token == "\n": + if inside_enclosure: + # Ignore newlines inside enclosures + tokens.pop(pointer) + size -= 1 + continue + if pointer == 0: + tokens.pop(0) + size -= 1 + continue + if pointer > 1: + try: + prev_expr = self._parse_after_braces(tokens[:pointer], inside_enclosure) + except SyntaxError: + tokens.pop(pointer) + size -= 1 + continue + else: + prev_expr = tokens[0] + if len(prev_expr) > 0 and prev_expr[0] == "CompoundExpression": + lines.extend(prev_expr[1:]) + else: + lines.append(prev_expr) + for i in range(pointer): + tokens.pop(0) + size -= pointer + pointer = 0 + continue + pointer += 1 + + def _util_add_missing_asterisks(self, tokens: list): + size: int = len(tokens) + pointer: int = 0 + while pointer < size: + if (pointer > 0 and + self._is_valid_star1(tokens[pointer - 1]) and + self._is_valid_star2(tokens[pointer])): + # This is a trick to add missing * operators in the expression, + # `"*" in op_dict` makes sure the precedence level is the same as "*", + # while `not self._is_op( ... )` makes sure this and the previous + # expression are not operators. + if tokens[pointer] == "(": + # ( has already been processed by now, replace: + tokens[pointer] = "*" + tokens[pointer + 1] = tokens[pointer + 1][0] + else: + tokens.insert(pointer, "*") + pointer += 1 + size += 1 + pointer += 1 + + def _parse_after_braces(self, tokens: list, inside_enclosure: bool = False): + op_dict: dict + changed: bool = False + lines: list = [] + + self._util_remove_newlines(lines, tokens, inside_enclosure) + + for op_type, grouping_strat, op_dict in reversed(self._mathematica_op_precedence): + if "*" in op_dict: + self._util_add_missing_asterisks(tokens) + size: int = len(tokens) + pointer: int = 0 + while pointer < size: + token = tokens[pointer] + if isinstance(token, str) and token in op_dict: + op_name: str | Callable = op_dict[token] + node: list + first_index: int + if isinstance(op_name, str): + node = [op_name] + first_index = 1 + else: + node = [] + first_index = 0 + if token in ("+", "-") and op_type == self.PREFIX and pointer > 0 and not self._is_op(tokens[pointer - 1]): + # Make sure that PREFIX + - don't match expressions like a + b or a - b, + # the INFIX + - are supposed to match that expression: + pointer += 1 + continue + if op_type == self.INFIX: + if pointer == 0 or pointer == size - 1 or self._is_op(tokens[pointer - 1]) or self._is_op(tokens[pointer + 1]): + pointer += 1 + continue + changed = True + tokens[pointer] = node + if op_type == self.INFIX: + arg1 = tokens.pop(pointer-1) + arg2 = tokens.pop(pointer) + if token == "/": + arg2 = self._get_inv(arg2) + elif token == "-": + arg2 = self._get_neg(arg2) + pointer -= 1 + size -= 2 + node.append(arg1) + node_p = node + if grouping_strat == self.FLAT: + while pointer + 2 < size and self._check_op_compatible(tokens[pointer+1], token): + node_p.append(arg2) + other_op = tokens.pop(pointer+1) + arg2 = tokens.pop(pointer+1) + if other_op == "/": + arg2 = self._get_inv(arg2) + elif other_op == "-": + arg2 = self._get_neg(arg2) + size -= 2 + node_p.append(arg2) + elif grouping_strat == self.RIGHT: + while pointer + 2 < size and tokens[pointer+1] == token: + node_p.append([op_name, arg2]) + node_p = node_p[-1] + tokens.pop(pointer+1) + arg2 = tokens.pop(pointer+1) + size -= 2 + node_p.append(arg2) + elif grouping_strat == self.LEFT: + while pointer + 1 < size and tokens[pointer+1] == token: + if isinstance(op_name, str): + node_p[first_index] = [op_name, node_p[first_index], arg2] + else: + node_p[first_index] = op_name(node_p[first_index], arg2) + tokens.pop(pointer+1) + arg2 = tokens.pop(pointer+1) + size -= 2 + node_p.append(arg2) + else: + node.append(arg2) + elif op_type == self.PREFIX: + if grouping_strat is not None: + raise TypeError("'Prefix' op_type should not have a grouping strat") + if pointer == size - 1 or self._is_op(tokens[pointer + 1]): + tokens[pointer] = self._missing_arguments_default[token]() + else: + node.append(tokens.pop(pointer+1)) + size -= 1 + elif op_type == self.POSTFIX: + if grouping_strat is not None: + raise TypeError("'Prefix' op_type should not have a grouping strat") + if pointer == 0 or self._is_op(tokens[pointer - 1]): + tokens[pointer] = self._missing_arguments_default[token]() + else: + node.append(tokens.pop(pointer-1)) + pointer -= 1 + size -= 1 + if isinstance(op_name, Callable): # type: ignore + op_call: Callable = typing.cast(Callable, op_name) + new_node = op_call(*node) + node.clear() + if isinstance(new_node, list): + node.extend(new_node) + else: + tokens[pointer] = new_node + pointer += 1 + if len(tokens) > 1 or (len(lines) == 0 and len(tokens) == 0): + if changed: + # Trick to deal with cases in which an operator with lower + # precedence should be transformed before an operator of higher + # precedence. Such as in the case of `#&[x]` (that is + # equivalent to `Lambda(d_, d_)(x)` in SymPy). In this case the + # operator `&` has lower precedence than `[`, but needs to be + # evaluated first because otherwise `# (&[x])` is not a valid + # expression: + return self._parse_after_braces(tokens, inside_enclosure) + raise SyntaxError("unable to create a single AST for the expression") + if len(lines) > 0: + if tokens[0] and tokens[0][0] == "CompoundExpression": + tokens = tokens[0][1:] + compound_expression = ["CompoundExpression", *lines, *tokens] + return compound_expression + return tokens[0] + + def _check_op_compatible(self, op1: str, op2: str): + if op1 == op2: + return True + muldiv = {"*", "/"} + addsub = {"+", "-"} + if op1 in muldiv and op2 in muldiv: + return True + if op1 in addsub and op2 in addsub: + return True + return False + + def _from_fullform_to_fullformlist(self, wmexpr: str): + """ + Parses FullForm[Downvalues[]] generated by Mathematica + """ + out: list = [] + stack = [out] + generator = re.finditer(r'[\[\],]', wmexpr) + last_pos = 0 + for match in generator: + if match is None: + break + position = match.start() + last_expr = wmexpr[last_pos:position].replace(',', '').replace(']', '').replace('[', '').strip() + + if match.group() == ',': + if last_expr != '': + stack[-1].append(last_expr) + elif match.group() == ']': + if last_expr != '': + stack[-1].append(last_expr) + stack.pop() + elif match.group() == '[': + stack[-1].append([last_expr]) + stack.append(stack[-1][-1]) + last_pos = match.end() + return out[0] + + def _from_fullformlist_to_fullformsympy(self, pylist: list): + from sympy import Function, Symbol + + def converter(expr): + if isinstance(expr, list): + if len(expr) > 0: + head = expr[0] + args = [converter(arg) for arg in expr[1:]] + return Function(head)(*args) + else: + raise ValueError("Empty list of expressions") + elif isinstance(expr, str): + return Symbol(expr) + else: + return _sympify(expr) + + return converter(pylist) + + _node_conversions = { + "Times": Mul, + "Plus": Add, + "Power": Pow, + "Rational": Rational, + "Log": lambda *a: log(*reversed(a)), + "Log2": lambda x: log(x, 2), + "Log10": lambda x: log(x, 10), + "Exp": exp, + "Sqrt": sqrt, + + "Sin": sin, + "Cos": cos, + "Tan": tan, + "Cot": cot, + "Sec": sec, + "Csc": csc, + + "ArcSin": asin, + "ArcCos": acos, + "ArcTan": lambda *a: atan2(*reversed(a)) if len(a) == 2 else atan(*a), + "ArcCot": acot, + "ArcSec": asec, + "ArcCsc": acsc, + + "Sinh": sinh, + "Cosh": cosh, + "Tanh": tanh, + "Coth": coth, + "Sech": sech, + "Csch": csch, + + "ArcSinh": asinh, + "ArcCosh": acosh, + "ArcTanh": atanh, + "ArcCoth": acoth, + "ArcSech": asech, + "ArcCsch": acsch, + + "Expand": expand, + "Im": im, + "Re": sympy.re, + "Flatten": flatten, + "Polylog": polylog, + "Cancel": cancel, + # Gamma=gamma, + "TrigExpand": expand_trig, + "Sign": sign, + "Simplify": simplify, + "Defer": UnevaluatedExpr, + "Identity": S, + # Sum=Sum_doit, + # Module=With, + # Block=With, + "Null": lambda *a: S.Zero, + "Mod": Mod, + "Max": Max, + "Min": Min, + "Pochhammer": rf, + "ExpIntegralEi": Ei, + "SinIntegral": Si, + "CosIntegral": Ci, + "AiryAi": airyai, + "AiryAiPrime": airyaiprime, + "AiryBi": airybi, + "AiryBiPrime": airybiprime, + "LogIntegral": li, + "PrimePi": primepi, + "Prime": prime, + "PrimeQ": isprime, + + "List": Tuple, + "Greater": StrictGreaterThan, + "GreaterEqual": GreaterThan, + "Less": StrictLessThan, + "LessEqual": LessThan, + "Equal": Equality, + "Or": Or, + "And": And, + + "Function": _parse_Function, + } + + _atom_conversions = { + "I": I, + "Pi": pi, + } + + def _from_fullformlist_to_sympy(self, full_form_list): + + def recurse(expr): + if isinstance(expr, list): + if isinstance(expr[0], list): + head = recurse(expr[0]) + else: + head = self._node_conversions.get(expr[0], Function(expr[0])) + return head(*[recurse(arg) for arg in expr[1:]]) + else: + return self._atom_conversions.get(expr, sympify(expr)) + + return recurse(full_form_list) + + def _from_fullformsympy_to_sympy(self, mform): + + expr = mform + for mma_form, sympy_node in self._node_conversions.items(): + expr = expr.replace(Function(mma_form), sympy_node) + return expr diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/maxima.py b/venv/lib/python3.11/site-packages/sympy/parsing/maxima.py new file mode 100644 index 0000000000000000000000000000000000000000..7a8ee5b17bb03a36e338803cb10f9ebf22763c2c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/maxima.py @@ -0,0 +1,71 @@ +import re +from sympy.concrete.products import product +from sympy.concrete.summations import Sum +from sympy.core.sympify import sympify +from sympy.functions.elementary.trigonometric import (cos, sin) + + +class MaximaHelpers: + def maxima_expand(expr): + return expr.expand() + + def maxima_float(expr): + return expr.evalf() + + def maxima_trigexpand(expr): + return expr.expand(trig=True) + + def maxima_sum(a1, a2, a3, a4): + return Sum(a1, (a2, a3, a4)).doit() + + def maxima_product(a1, a2, a3, a4): + return product(a1, (a2, a3, a4)) + + def maxima_csc(expr): + return 1/sin(expr) + + def maxima_sec(expr): + return 1/cos(expr) + +sub_dict = { + 'pi': re.compile(r'%pi'), + 'E': re.compile(r'%e'), + 'I': re.compile(r'%i'), + '**': re.compile(r'\^'), + 'oo': re.compile(r'\binf\b'), + '-oo': re.compile(r'\bminf\b'), + "'-'": re.compile(r'\bminus\b'), + 'maxima_expand': re.compile(r'\bexpand\b'), + 'maxima_float': re.compile(r'\bfloat\b'), + 'maxima_trigexpand': re.compile(r'\btrigexpand'), + 'maxima_sum': re.compile(r'\bsum\b'), + 'maxima_product': re.compile(r'\bproduct\b'), + 'cancel': re.compile(r'\bratsimp\b'), + 'maxima_csc': re.compile(r'\bcsc\b'), + 'maxima_sec': re.compile(r'\bsec\b') +} + +var_name = re.compile(r'^\s*(\w+)\s*:') + + +def parse_maxima(str, globals=None, name_dict={}): + str = str.strip() + str = str.rstrip('; ') + + for k, v in sub_dict.items(): + str = v.sub(k, str) + + assign_var = None + var_match = var_name.search(str) + if var_match: + assign_var = var_match.group(1) + str = str[var_match.end():].strip() + + dct = MaximaHelpers.__dict__.copy() + dct.update(name_dict) + obj = sympify(str, locals=dct) + + if assign_var and globals: + globals[assign_var] = obj + + return obj diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/sym_expr.py b/venv/lib/python3.11/site-packages/sympy/parsing/sym_expr.py new file mode 100644 index 0000000000000000000000000000000000000000..9dbd0e94eb51147b51825fcf15cbec5ae18bb1b6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/sym_expr.py @@ -0,0 +1,279 @@ +from sympy.printing import pycode, ccode, fcode +from sympy.external import import_module +from sympy.utilities.decorator import doctest_depends_on + +lfortran = import_module('lfortran') +cin = import_module('clang.cindex', import_kwargs = {'fromlist': ['cindex']}) + +if lfortran: + from sympy.parsing.fortran.fortran_parser import src_to_sympy +if cin: + from sympy.parsing.c.c_parser import parse_c + +@doctest_depends_on(modules=['lfortran', 'clang.cindex']) +class SymPyExpression: # type: ignore + """Class to store and handle SymPy expressions + + This class will hold SymPy Expressions and handle the API for the + conversion to and from different languages. + + It works with the C and the Fortran Parser to generate SymPy expressions + which are stored here and which can be converted to multiple language's + source code. + + Notes + ===== + + The module and its API are currently under development and experimental + and can be changed during development. + + The Fortran parser does not support numeric assignments, so all the + variables have been Initialized to zero. + + The module also depends on external dependencies: + + - LFortran which is required to use the Fortran parser + - Clang which is required for the C parser + + Examples + ======== + + Example of parsing C code: + + >>> from sympy.parsing.sym_expr import SymPyExpression + >>> src = ''' + ... int a,b; + ... float c = 2, d =4; + ... ''' + >>> a = SymPyExpression(src, 'c') + >>> a.return_expr() + [Declaration(Variable(a, type=intc)), + Declaration(Variable(b, type=intc)), + Declaration(Variable(c, type=float32, value=2.0)), + Declaration(Variable(d, type=float32, value=4.0))] + + An example of variable definition: + + >>> from sympy.parsing.sym_expr import SymPyExpression + >>> src2 = ''' + ... integer :: a, b, c, d + ... real :: p, q, r, s + ... ''' + >>> p = SymPyExpression() + >>> p.convert_to_expr(src2, 'f') + >>> p.convert_to_c() + ['int a = 0', 'int b = 0', 'int c = 0', 'int d = 0', 'double p = 0.0', 'double q = 0.0', 'double r = 0.0', 'double s = 0.0'] + + An example of Assignment: + + >>> from sympy.parsing.sym_expr import SymPyExpression + >>> src3 = ''' + ... integer :: a, b, c, d, e + ... d = a + b - c + ... e = b * d + c * e / a + ... ''' + >>> p = SymPyExpression(src3, 'f') + >>> p.convert_to_python() + ['a = 0', 'b = 0', 'c = 0', 'd = 0', 'e = 0', 'd = a + b - c', 'e = b*d + c*e/a'] + + An example of function definition: + + >>> from sympy.parsing.sym_expr import SymPyExpression + >>> src = ''' + ... integer function f(a,b) + ... integer, intent(in) :: a, b + ... integer :: r + ... end function + ... ''' + >>> a = SymPyExpression(src, 'f') + >>> a.convert_to_python() + ['def f(a, b):\\n f = 0\\n r = 0\\n return f'] + + """ + + def __init__(self, source_code = None, mode = None): + """Constructor for SymPyExpression class""" + super().__init__() + if not(mode or source_code): + self._expr = [] + elif mode: + if source_code: + if mode.lower() == 'f': + if lfortran: + self._expr = src_to_sympy(source_code) + else: + raise ImportError("LFortran is not installed, cannot parse Fortran code") + elif mode.lower() == 'c': + if cin: + self._expr = parse_c(source_code) + else: + raise ImportError("Clang is not installed, cannot parse C code") + else: + raise NotImplementedError( + 'Parser for specified language is not implemented' + ) + else: + raise ValueError('Source code not present') + else: + raise ValueError('Please specify a mode for conversion') + + def convert_to_expr(self, src_code, mode): + """Converts the given source code to SymPy Expressions + + Attributes + ========== + + src_code : String + the source code or filename of the source code that is to be + converted + + mode: String + the mode to determine which parser is to be used according to + the language of the source code + f or F for Fortran + c or C for C/C++ + + Examples + ======== + + >>> from sympy.parsing.sym_expr import SymPyExpression + >>> src3 = ''' + ... integer function f(a,b) result(r) + ... integer, intent(in) :: a, b + ... integer :: x + ... r = a + b -x + ... end function + ... ''' + >>> p = SymPyExpression() + >>> p.convert_to_expr(src3, 'f') + >>> p.return_expr() + [FunctionDefinition(integer, name=f, parameters=(Variable(a), Variable(b)), body=CodeBlock( + Declaration(Variable(r, type=integer, value=0)), + Declaration(Variable(x, type=integer, value=0)), + Assignment(Variable(r), a + b - x), + Return(Variable(r)) + ))] + + + + + """ + if mode.lower() == 'f': + if lfortran: + self._expr = src_to_sympy(src_code) + else: + raise ImportError("LFortran is not installed, cannot parse Fortran code") + elif mode.lower() == 'c': + if cin: + self._expr = parse_c(src_code) + else: + raise ImportError("Clang is not installed, cannot parse C code") + else: + raise NotImplementedError( + "Parser for specified language has not been implemented" + ) + + def convert_to_python(self): + """Returns a list with Python code for the SymPy expressions + + Examples + ======== + + >>> from sympy.parsing.sym_expr import SymPyExpression + >>> src2 = ''' + ... integer :: a, b, c, d + ... real :: p, q, r, s + ... c = a/b + ... d = c/a + ... s = p/q + ... r = q/p + ... ''' + >>> p = SymPyExpression(src2, 'f') + >>> p.convert_to_python() + ['a = 0', 'b = 0', 'c = 0', 'd = 0', 'p = 0.0', 'q = 0.0', 'r = 0.0', 's = 0.0', 'c = a/b', 'd = c/a', 's = p/q', 'r = q/p'] + + """ + self._pycode = [] + for iter in self._expr: + self._pycode.append(pycode(iter)) + return self._pycode + + def convert_to_c(self): + """Returns a list with the c source code for the SymPy expressions + + + Examples + ======== + + >>> from sympy.parsing.sym_expr import SymPyExpression + >>> src2 = ''' + ... integer :: a, b, c, d + ... real :: p, q, r, s + ... c = a/b + ... d = c/a + ... s = p/q + ... r = q/p + ... ''' + >>> p = SymPyExpression() + >>> p.convert_to_expr(src2, 'f') + >>> p.convert_to_c() + ['int a = 0', 'int b = 0', 'int c = 0', 'int d = 0', 'double p = 0.0', 'double q = 0.0', 'double r = 0.0', 'double s = 0.0', 'c = a/b;', 'd = c/a;', 's = p/q;', 'r = q/p;'] + + """ + self._ccode = [] + for iter in self._expr: + self._ccode.append(ccode(iter)) + return self._ccode + + def convert_to_fortran(self): + """Returns a list with the fortran source code for the SymPy expressions + + Examples + ======== + + >>> from sympy.parsing.sym_expr import SymPyExpression + >>> src2 = ''' + ... integer :: a, b, c, d + ... real :: p, q, r, s + ... c = a/b + ... d = c/a + ... s = p/q + ... r = q/p + ... ''' + >>> p = SymPyExpression(src2, 'f') + >>> p.convert_to_fortran() + [' integer*4 a', ' integer*4 b', ' integer*4 c', ' integer*4 d', ' real*8 p', ' real*8 q', ' real*8 r', ' real*8 s', ' c = a/b', ' d = c/a', ' s = p/q', ' r = q/p'] + + """ + self._fcode = [] + for iter in self._expr: + self._fcode.append(fcode(iter)) + return self._fcode + + def return_expr(self): + """Returns the expression list + + Examples + ======== + + >>> from sympy.parsing.sym_expr import SymPyExpression + >>> src3 = ''' + ... integer function f(a,b) + ... integer, intent(in) :: a, b + ... integer :: r + ... r = a+b + ... f = r + ... end function + ... ''' + >>> p = SymPyExpression() + >>> p.convert_to_expr(src3, 'f') + >>> p.return_expr() + [FunctionDefinition(integer, name=f, parameters=(Variable(a), Variable(b)), body=CodeBlock( + Declaration(Variable(f, type=integer, value=0)), + Declaration(Variable(r, type=integer, value=0)), + Assignment(Variable(f), Variable(r)), + Return(Variable(f)) + ))] + + """ + return self._expr diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/sympy_parser.py b/venv/lib/python3.11/site-packages/sympy/parsing/sympy_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..9cfda9ce0f73ffa3773031c48b9e9c245f69fe0b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/sympy_parser.py @@ -0,0 +1,1270 @@ +"""Transform a string with Python-like source code into SymPy expression. """ +from __future__ import annotations +from tokenize import (generate_tokens, untokenize, TokenError, + NUMBER, STRING, NAME, OP, ENDMARKER, ERRORTOKEN, NEWLINE) + +from keyword import iskeyword + +import ast +import unicodedata +from io import StringIO +import builtins +import types +from typing import Any, Callable +from functools import reduce +from sympy.assumptions.ask import AssumptionKeys +from sympy.core.basic import Basic +from sympy.core import Symbol +from sympy.core.function import Function +from sympy.utilities.misc import func_name +from sympy.functions.elementary.miscellaneous import Max, Min + + +null = '' + +TOKEN = tuple[int, str] +DICT = dict[str, Any] +TRANS = Callable[[list[TOKEN], DICT, DICT], list[TOKEN]] + +def _token_splittable(token_name: str) -> bool: + """ + Predicate for whether a token name can be split into multiple tokens. + + A token is splittable if it does not contain an underscore character and + it is not the name of a Greek letter. This is used to implicitly convert + expressions like 'xyz' into 'x*y*z'. + """ + if '_' in token_name: + return False + try: + return not unicodedata.lookup('GREEK SMALL LETTER ' + token_name) + except KeyError: + return len(token_name) > 1 + + +def _token_callable(token: TOKEN, local_dict: DICT, global_dict: DICT, nextToken=None): + """ + Predicate for whether a token name represents a callable function. + + Essentially wraps ``callable``, but looks up the token name in the + locals and globals. + """ + func = local_dict.get(token[1]) + if not func: + func = global_dict.get(token[1]) + return callable(func) and not isinstance(func, Symbol) + + +def _add_factorial_tokens(name: str, result: list[TOKEN]) -> list[TOKEN]: + if result == [] or result[-1][1] == '(': + raise TokenError() + + beginning = [(NAME, name), (OP, '(')] + end = [(OP, ')')] + + diff = 0 + length = len(result) + + for index, token in enumerate(result[::-1]): + toknum, tokval = token + i = length - index - 1 + + if tokval == ')': + diff += 1 + elif tokval == '(': + diff -= 1 + + if diff == 0: + if i - 1 >= 0 and result[i - 1][0] == NAME: + return result[:i - 1] + beginning + result[i - 1:] + end + else: + return result[:i] + beginning + result[i:] + end + + return result + + +class ParenthesisGroup(list[TOKEN]): + """List of tokens representing an expression in parentheses.""" + pass + + +class AppliedFunction: + """ + A group of tokens representing a function and its arguments. + + `exponent` is for handling the shorthand sin^2, ln^2, etc. + """ + def __init__(self, function: TOKEN, args: ParenthesisGroup, exponent=None): + if exponent is None: + exponent = [] + self.function = function + self.args = args + self.exponent = exponent + self.items = ['function', 'args', 'exponent'] + + def expand(self) -> list[TOKEN]: + """Return a list of tokens representing the function""" + return [self.function, *self.args] + + def __getitem__(self, index): + return getattr(self, self.items[index]) + + def __repr__(self): + return "AppliedFunction(%s, %s, %s)" % (self.function, self.args, + self.exponent) + + +def _flatten(result: list[TOKEN | AppliedFunction]): + result2: list[TOKEN] = [] + for tok in result: + if isinstance(tok, AppliedFunction): + result2.extend(tok.expand()) + else: + result2.append(tok) + return result2 + + +def _group_parentheses(recursor: TRANS): + def _inner(tokens: list[TOKEN], local_dict: DICT, global_dict: DICT): + """Group tokens between parentheses with ParenthesisGroup. + + Also processes those tokens recursively. + + """ + result: list[TOKEN | ParenthesisGroup] = [] + stacks: list[ParenthesisGroup] = [] + stacklevel = 0 + for token in tokens: + if token[0] == OP: + if token[1] == '(': + stacks.append(ParenthesisGroup([])) + stacklevel += 1 + elif token[1] == ')': + stacks[-1].append(token) + stack = stacks.pop() + + if len(stacks) > 0: + # We don't recurse here since the upper-level stack + # would reprocess these tokens + stacks[-1].extend(stack) + else: + # Recurse here to handle nested parentheses + # Strip off the outer parentheses to avoid an infinite loop + inner = stack[1:-1] + inner = recursor(inner, + local_dict, + global_dict) + parenGroup = [stack[0]] + inner + [stack[-1]] + result.append(ParenthesisGroup(parenGroup)) + stacklevel -= 1 + continue + if stacklevel: + stacks[-1].append(token) + else: + result.append(token) + if stacklevel: + raise TokenError("Mismatched parentheses") + return result + return _inner + + +def _apply_functions(tokens: list[TOKEN | ParenthesisGroup], local_dict: DICT, global_dict: DICT): + """Convert a NAME token + ParenthesisGroup into an AppliedFunction. + + Note that ParenthesisGroups, if not applied to any function, are + converted back into lists of tokens. + + """ + result: list[TOKEN | AppliedFunction] = [] + symbol = None + for tok in tokens: + if isinstance(tok, ParenthesisGroup): + if symbol and _token_callable(symbol, local_dict, global_dict): + result[-1] = AppliedFunction(symbol, tok) + symbol = None + else: + result.extend(tok) + elif tok[0] == NAME: + symbol = tok + result.append(tok) + else: + symbol = None + result.append(tok) + return result + + +def _implicit_multiplication(tokens: list[TOKEN | AppliedFunction], local_dict: DICT, global_dict: DICT): + """Implicitly adds '*' tokens. + + Cases: + + - Two AppliedFunctions next to each other ("sin(x)cos(x)") + + - AppliedFunction next to an open parenthesis ("sin x (cos x + 1)") + + - A close parenthesis next to an AppliedFunction ("(x+2)sin x")\ + + - A close parenthesis next to an open parenthesis ("(x+2)(x+3)") + + - AppliedFunction next to an implicitly applied function ("sin(x)cos x") + + """ + result: list[TOKEN | AppliedFunction] = [] + skip = False + for tok, nextTok in zip(tokens, tokens[1:]): + result.append(tok) + if skip: + skip = False + continue + if tok[0] == OP and tok[1] == '.' and nextTok[0] == NAME: + # Dotted name. Do not do implicit multiplication + skip = True + continue + if isinstance(tok, AppliedFunction): + if isinstance(nextTok, AppliedFunction): + result.append((OP, '*')) + elif nextTok == (OP, '('): + # Applied function followed by an open parenthesis + if tok.function[1] == "Function": + tok.function = (tok.function[0], 'Symbol') + result.append((OP, '*')) + elif nextTok[0] == NAME: + # Applied function followed by implicitly applied function + result.append((OP, '*')) + else: + if tok == (OP, ')'): + if isinstance(nextTok, AppliedFunction): + # Close parenthesis followed by an applied function + result.append((OP, '*')) + elif nextTok[0] == NAME: + # Close parenthesis followed by an implicitly applied function + result.append((OP, '*')) + elif nextTok == (OP, '('): + # Close parenthesis followed by an open parenthesis + result.append((OP, '*')) + elif tok[0] == NAME and not _token_callable(tok, local_dict, global_dict): + if isinstance(nextTok, AppliedFunction) or \ + (nextTok[0] == NAME and _token_callable(nextTok, local_dict, global_dict)): + # Constant followed by (implicitly applied) function + result.append((OP, '*')) + elif nextTok == (OP, '('): + # Constant followed by parenthesis + result.append((OP, '*')) + elif nextTok[0] == NAME: + # Constant followed by constant + result.append((OP, '*')) + if tokens: + result.append(tokens[-1]) + return result + + +def _implicit_application(tokens: list[TOKEN | AppliedFunction], local_dict: DICT, global_dict: DICT): + """Adds parentheses as needed after functions.""" + result: list[TOKEN | AppliedFunction] = [] + appendParen = 0 # number of closing parentheses to add + skip = 0 # number of tokens to delay before adding a ')' (to + # capture **, ^, etc.) + exponentSkip = False # skipping tokens before inserting parentheses to + # work with function exponentiation + for tok, nextTok in zip(tokens, tokens[1:]): + result.append(tok) + if (tok[0] == NAME and nextTok[0] not in [OP, ENDMARKER, NEWLINE]): + if _token_callable(tok, local_dict, global_dict, nextTok): # type: ignore + result.append((OP, '(')) + appendParen += 1 + # name followed by exponent - function exponentiation + elif (tok[0] == NAME and nextTok[0] == OP and nextTok[1] == '**'): + if _token_callable(tok, local_dict, global_dict): # type: ignore + exponentSkip = True + elif exponentSkip: + # if the last token added was an applied function (i.e. the + # power of the function exponent) OR a multiplication (as + # implicit multiplication would have added an extraneous + # multiplication) + if (isinstance(tok, AppliedFunction) + or (tok[0] == OP and tok[1] == '*')): + # don't add anything if the next token is a multiplication + # or if there's already a parenthesis (if parenthesis, still + # stop skipping tokens) + if not (nextTok[0] == OP and nextTok[1] == '*'): + if not(nextTok[0] == OP and nextTok[1] == '('): + result.append((OP, '(')) + appendParen += 1 + exponentSkip = False + elif appendParen: + if nextTok[0] == OP and nextTok[1] in ('^', '**', '*'): + skip = 1 + continue + if skip: + skip -= 1 + continue + result.append((OP, ')')) + appendParen -= 1 + + if tokens: + result.append(tokens[-1]) + + if appendParen: + result.extend([(OP, ')')] * appendParen) + return result + + +def function_exponentiation(tokens: list[TOKEN], local_dict: DICT, global_dict: DICT): + """Allows functions to be exponentiated, e.g. ``cos**2(x)``. + + Examples + ======== + + >>> from sympy.parsing.sympy_parser import (parse_expr, + ... standard_transformations, function_exponentiation) + >>> transformations = standard_transformations + (function_exponentiation,) + >>> parse_expr('sin**4(x)', transformations=transformations) + sin(x)**4 + """ + result: list[TOKEN] = [] + exponent: list[TOKEN] = [] + consuming_exponent = False + level = 0 + for tok, nextTok in zip(tokens, tokens[1:]): + if tok[0] == NAME and nextTok[0] == OP and nextTok[1] == '**': + if _token_callable(tok, local_dict, global_dict): + consuming_exponent = True + elif consuming_exponent: + if tok[0] == NAME and tok[1] == 'Function': + tok = (NAME, 'Symbol') + exponent.append(tok) + + # only want to stop after hitting ) + if tok[0] == nextTok[0] == OP and tok[1] == ')' and nextTok[1] == '(': + consuming_exponent = False + # if implicit multiplication was used, we may have )*( instead + if tok[0] == nextTok[0] == OP and tok[1] == '*' and nextTok[1] == '(': + consuming_exponent = False + del exponent[-1] + continue + elif exponent and not consuming_exponent: + if tok[0] == OP: + if tok[1] == '(': + level += 1 + elif tok[1] == ')': + level -= 1 + if level == 0: + result.append(tok) + result.extend(exponent) + exponent = [] + continue + result.append(tok) + if tokens: + result.append(tokens[-1]) + if exponent: + result.extend(exponent) + return result + + +def split_symbols_custom(predicate: Callable[[str], bool]): + """Creates a transformation that splits symbol names. + + ``predicate`` should return True if the symbol name is to be split. + + For instance, to retain the default behavior but avoid splitting certain + symbol names, a predicate like this would work: + + + >>> from sympy.parsing.sympy_parser import (parse_expr, _token_splittable, + ... standard_transformations, implicit_multiplication, + ... split_symbols_custom) + >>> def can_split(symbol): + ... if symbol not in ('list', 'of', 'unsplittable', 'names'): + ... return _token_splittable(symbol) + ... return False + ... + >>> transformation = split_symbols_custom(can_split) + >>> parse_expr('unsplittable', transformations=standard_transformations + + ... (transformation, implicit_multiplication)) + unsplittable + """ + def _split_symbols(tokens: list[TOKEN], local_dict: DICT, global_dict: DICT): + result: list[TOKEN] = [] + split = False + split_previous=False + + for tok in tokens: + if split_previous: + # throw out closing parenthesis of Symbol that was split + split_previous=False + continue + split_previous=False + + if tok[0] == NAME and tok[1] in ['Symbol', 'Function']: + split = True + + elif split and tok[0] == NAME: + symbol = tok[1][1:-1] + + if predicate(symbol): + tok_type = result[-2][1] # Symbol or Function + del result[-2:] # Get rid of the call to Symbol + + i = 0 + while i < len(symbol): + char = symbol[i] + if char in local_dict or char in global_dict: + result.append((NAME, "%s" % char)) + elif char.isdigit(): + chars = [char] + for i in range(i + 1, len(symbol)): + if not symbol[i].isdigit(): + i -= 1 + break + chars.append(symbol[i]) + char = ''.join(chars) + result.extend([(NAME, 'Number'), (OP, '('), + (NAME, "'%s'" % char), (OP, ')')]) + else: + use = tok_type if i == len(symbol) else 'Symbol' + result.extend([(NAME, use), (OP, '('), + (NAME, "'%s'" % char), (OP, ')')]) + i += 1 + + # Set split_previous=True so will skip + # the closing parenthesis of the original Symbol + split = False + split_previous = True + continue + + else: + split = False + + result.append(tok) + + return result + + return _split_symbols + + +#: Splits symbol names for implicit multiplication. +#: +#: Intended to let expressions like ``xyz`` be parsed as ``x*y*z``. Does not +#: split Greek character names, so ``theta`` will *not* become +#: ``t*h*e*t*a``. Generally this should be used with +#: ``implicit_multiplication``. +split_symbols = split_symbols_custom(_token_splittable) + + +def implicit_multiplication(tokens: list[TOKEN], local_dict: DICT, + global_dict: DICT) -> list[TOKEN]: + """Makes the multiplication operator optional in most cases. + + Use this before :func:`implicit_application`, otherwise expressions like + ``sin 2x`` will be parsed as ``x * sin(2)`` rather than ``sin(2*x)``. + + Examples + ======== + + >>> from sympy.parsing.sympy_parser import (parse_expr, + ... standard_transformations, implicit_multiplication) + >>> transformations = standard_transformations + (implicit_multiplication,) + >>> parse_expr('3 x y', transformations=transformations) + 3*x*y + """ + # These are interdependent steps, so we don't expose them separately + res1 = _group_parentheses(implicit_multiplication)(tokens, local_dict, global_dict) + res2 = _apply_functions(res1, local_dict, global_dict) + res3 = _implicit_multiplication(res2, local_dict, global_dict) + result = _flatten(res3) + return result + + +def implicit_application(tokens: list[TOKEN], local_dict: DICT, + global_dict: DICT) -> list[TOKEN]: + """Makes parentheses optional in some cases for function calls. + + Use this after :func:`implicit_multiplication`, otherwise expressions + like ``sin 2x`` will be parsed as ``x * sin(2)`` rather than + ``sin(2*x)``. + + Examples + ======== + + >>> from sympy.parsing.sympy_parser import (parse_expr, + ... standard_transformations, implicit_application) + >>> transformations = standard_transformations + (implicit_application,) + >>> parse_expr('cot z + csc z', transformations=transformations) + cot(z) + csc(z) + """ + res1 = _group_parentheses(implicit_application)(tokens, local_dict, global_dict) + res2 = _apply_functions(res1, local_dict, global_dict) + res3 = _implicit_application(res2, local_dict, global_dict) + result = _flatten(res3) + return result + + +def implicit_multiplication_application(result: list[TOKEN], local_dict: DICT, + global_dict: DICT) -> list[TOKEN]: + """Allows a slightly relaxed syntax. + + - Parentheses for single-argument method calls are optional. + + - Multiplication is implicit. + + - Symbol names can be split (i.e. spaces are not needed between + symbols). + + - Functions can be exponentiated. + + Examples + ======== + + >>> from sympy.parsing.sympy_parser import (parse_expr, + ... standard_transformations, implicit_multiplication_application) + >>> parse_expr("10sin**2 x**2 + 3xyz + tan theta", + ... transformations=(standard_transformations + + ... (implicit_multiplication_application,))) + 3*x*y*z + 10*sin(x**2)**2 + tan(theta) + + """ + for step in (split_symbols, implicit_multiplication, + implicit_application, function_exponentiation): + result = step(result, local_dict, global_dict) + + return result + + +def auto_symbol(tokens: list[TOKEN], local_dict: DICT, global_dict: DICT): + """Inserts calls to ``Symbol``/``Function`` for undefined variables.""" + result: list[TOKEN] = [] + prevTok = (-1, '') + + tokens.append((-1, '')) # so zip traverses all tokens + for tok, nextTok in zip(tokens, tokens[1:]): + tokNum, tokVal = tok + nextTokNum, nextTokVal = nextTok + if tokNum == NAME: + name = tokVal + + if (name in ['True', 'False', 'None'] + or iskeyword(name) + # Don't convert attribute access + or (prevTok[0] == OP and prevTok[1] == '.') + # Don't convert keyword arguments + or (prevTok[0] == OP and prevTok[1] in ('(', ',') + and nextTokNum == OP and nextTokVal == '=') + # the name has already been defined + or name in local_dict and local_dict[name] is not null): + result.append((NAME, name)) + continue + elif name in local_dict: + local_dict.setdefault(null, set()).add(name) + if nextTokVal == '(': + local_dict[name] = Function(name) + else: + local_dict[name] = Symbol(name) + result.append((NAME, name)) + continue + elif name in global_dict: + obj = global_dict[name] + if isinstance(obj, (AssumptionKeys, Basic, type)) or callable(obj): + result.append((NAME, name)) + continue + + result.extend([ + (NAME, 'Symbol' if nextTokVal != '(' else 'Function'), + (OP, '('), + (NAME, repr(str(name))), + (OP, ')'), + ]) + else: + result.append((tokNum, tokVal)) + + prevTok = (tokNum, tokVal) + + return result + + +def lambda_notation(tokens: list[TOKEN], local_dict: DICT, global_dict: DICT): + """Substitutes "lambda" with its SymPy equivalent Lambda(). + However, the conversion does not take place if only "lambda" + is passed because that is a syntax error. + + """ + result: list[TOKEN] = [] + flag = False + toknum, tokval = tokens[0] + tokLen = len(tokens) + + if toknum == NAME and tokval == 'lambda': + if tokLen == 2 or tokLen == 3 and tokens[1][0] == NEWLINE: + # In Python 3.6.7+, inputs without a newline get NEWLINE added to + # the tokens + result.extend(tokens) + elif tokLen > 2: + result.extend([ + (NAME, 'Lambda'), + (OP, '('), + (OP, '('), + (OP, ')'), + (OP, ')'), + ]) + for tokNum, tokVal in tokens[1:]: + if tokNum == OP and tokVal == ':': + tokVal = ',' + flag = True + if not flag and tokNum == OP and tokVal in ('*', '**'): + raise TokenError("Starred arguments in lambda not supported") + if flag: + result.insert(-1, (tokNum, tokVal)) + else: + result.insert(-2, (tokNum, tokVal)) + else: + result.extend(tokens) + + return result + + +def factorial_notation(tokens: list[TOKEN], local_dict: DICT, global_dict: DICT): + """Allows standard notation for factorial.""" + result: list[TOKEN] = [] + nfactorial = 0 + for toknum, tokval in tokens: + if toknum == OP and tokval == "!": + # In Python 3.12 "!" are OP instead of ERRORTOKEN + nfactorial += 1 + elif toknum == ERRORTOKEN: + op = tokval + if op == '!': + nfactorial += 1 + else: + nfactorial = 0 + result.append((OP, op)) + else: + if nfactorial == 1: + result = _add_factorial_tokens('factorial', result) + elif nfactorial == 2: + result = _add_factorial_tokens('factorial2', result) + elif nfactorial > 2: + raise TokenError + nfactorial = 0 + result.append((toknum, tokval)) + return result + + +def convert_xor(tokens: list[TOKEN], local_dict: DICT, global_dict: DICT): + """Treats XOR, ``^``, as exponentiation, ``**``.""" + result: list[TOKEN] = [] + for toknum, tokval in tokens: + if toknum == OP: + if tokval == '^': + result.append((OP, '**')) + else: + result.append((toknum, tokval)) + else: + result.append((toknum, tokval)) + + return result + + +def repeated_decimals(tokens: list[TOKEN], local_dict: DICT, global_dict: DICT): + """ + Allows 0.2[1] notation to represent the repeated decimal 0.2111... (19/90) + + Run this before auto_number. + + """ + result: list[TOKEN] = [] + + def is_digit(s): + return all(i in '0123456789_' for i in s) + + # num will running match any DECIMAL [ INTEGER ] + num: list[TOKEN] = [] + for toknum, tokval in tokens: + if toknum == NUMBER: + if (not num and '.' in tokval and 'e' not in tokval.lower() and + 'j' not in tokval.lower()): + num.append((toknum, tokval)) + elif is_digit(tokval) and (len(num) == 2 or + len(num) == 3 and is_digit(num[-1][1])): + num.append((toknum, tokval)) + else: + num = [] + elif toknum == OP: + if tokval == '[' and len(num) == 1: + num.append((OP, tokval)) + elif tokval == ']' and len(num) >= 3: + num.append((OP, tokval)) + elif tokval == '.' and not num: + # handle .[1] + num.append((NUMBER, '0.')) + else: + num = [] + else: + num = [] + + result.append((toknum, tokval)) + + if num and num[-1][1] == ']': + # pre.post[repetend] = a + b/c + d/e where a = pre, b/c = post, + # and d/e = repetend + result = result[:-len(num)] + pre, post = num[0][1].split('.') + repetend = num[2][1] + if len(num) == 5: + repetend += num[3][1] + + pre = pre.replace('_', '') + post = post.replace('_', '') + repetend = repetend.replace('_', '') + + zeros = '0'*len(post) + post, repetends = [w.lstrip('0') for w in [post, repetend]] + # or else interpreted as octal + + a = pre or '0' + b, c = post or '0', '1' + zeros + d, e = repetends, ('9'*len(repetend)) + zeros + + seq = [ + (OP, '('), + (NAME, 'Integer'), + (OP, '('), + (NUMBER, a), + (OP, ')'), + (OP, '+'), + (NAME, 'Rational'), + (OP, '('), + (NUMBER, b), + (OP, ','), + (NUMBER, c), + (OP, ')'), + (OP, '+'), + (NAME, 'Rational'), + (OP, '('), + (NUMBER, d), + (OP, ','), + (NUMBER, e), + (OP, ')'), + (OP, ')'), + ] + result.extend(seq) + num = [] + + return result + + +def auto_number(tokens: list[TOKEN], local_dict: DICT, global_dict: DICT): + """ + Converts numeric literals to use SymPy equivalents. + + Complex numbers use ``I``, integer literals use ``Integer``, and float + literals use ``Float``. + + """ + result: list[TOKEN] = [] + + for toknum, tokval in tokens: + if toknum == NUMBER: + number = tokval + postfix = [] + + if number.endswith(('j', 'J')): + number = number[:-1] + postfix = [(OP, '*'), (NAME, 'I')] + + if '.' in number or (('e' in number or 'E' in number) and + not (number.startswith(('0x', '0X')))): + seq = [(NAME, 'Float'), (OP, '('), + (NUMBER, repr(str(number))), (OP, ')')] + else: + seq = [(NAME, 'Integer'), (OP, '('), ( + NUMBER, number), (OP, ')')] + + result.extend(seq + postfix) + else: + result.append((toknum, tokval)) + + return result + + +def rationalize(tokens: list[TOKEN], local_dict: DICT, global_dict: DICT): + """Converts floats into ``Rational``. Run AFTER ``auto_number``.""" + result: list[TOKEN] = [] + passed_float = False + for toknum, tokval in tokens: + if toknum == NAME: + if tokval == 'Float': + passed_float = True + tokval = 'Rational' + result.append((toknum, tokval)) + elif passed_float == True and toknum == NUMBER: + passed_float = False + result.append((STRING, tokval)) + else: + result.append((toknum, tokval)) + + return result + + +def _transform_equals_sign(tokens: list[TOKEN], local_dict: DICT, global_dict: DICT): + """Transforms the equals sign ``=`` to instances of Eq. + + This is a helper function for ``convert_equals_signs``. + Works with expressions containing one equals sign and no + nesting. Expressions like ``(1=2)=False`` will not work with this + and should be used with ``convert_equals_signs``. + + Examples: 1=2 to Eq(1,2) + 1*2=x to Eq(1*2, x) + + This does not deal with function arguments yet. + + """ + result: list[TOKEN] = [] + if (OP, "=") in tokens: + result.append((NAME, "Eq")) + result.append((OP, "(")) + for token in tokens: + if token == (OP, "="): + result.append((OP, ",")) + continue + result.append(token) + result.append((OP, ")")) + else: + result = tokens + return result + + +def convert_equals_signs(tokens: list[TOKEN], local_dict: DICT, + global_dict: DICT) -> list[TOKEN]: + """ Transforms all the equals signs ``=`` to instances of Eq. + + Parses the equals signs in the expression and replaces them with + appropriate Eq instances. Also works with nested equals signs. + + Does not yet play well with function arguments. + For example, the expression ``(x=y)`` is ambiguous and can be interpreted + as x being an argument to a function and ``convert_equals_signs`` will not + work for this. + + See also + ======== + convert_equality_operators + + Examples + ======== + + >>> from sympy.parsing.sympy_parser import (parse_expr, + ... standard_transformations, convert_equals_signs) + >>> parse_expr("1*2=x", transformations=( + ... standard_transformations + (convert_equals_signs,))) + Eq(2, x) + >>> parse_expr("(1*2=x)=False", transformations=( + ... standard_transformations + (convert_equals_signs,))) + Eq(Eq(2, x), False) + + """ + res1 = _group_parentheses(convert_equals_signs)(tokens, local_dict, global_dict) + res2 = _apply_functions(res1, local_dict, global_dict) + res3 = _transform_equals_sign(res2, local_dict, global_dict) + result = _flatten(res3) + return result + + +#: Standard transformations for :func:`parse_expr`. +#: Inserts calls to :class:`~.Symbol`, :class:`~.Integer`, and other SymPy +#: datatypes and allows the use of standard factorial notation (e.g. ``x!``). +standard_transformations: tuple[TRANS, ...] \ + = (lambda_notation, auto_symbol, repeated_decimals, auto_number, + factorial_notation) + + +def stringify_expr(s: str, local_dict: DICT, global_dict: DICT, + transformations: tuple[TRANS, ...]) -> str: + """ + Converts the string ``s`` to Python code, in ``local_dict`` + + Generally, ``parse_expr`` should be used. + """ + + tokens = [] + input_code = StringIO(s.strip()) + for toknum, tokval, _, _, _ in generate_tokens(input_code.readline): + tokens.append((toknum, tokval)) + + for transform in transformations: + tokens = transform(tokens, local_dict, global_dict) + + return untokenize(tokens) + + +def eval_expr(code, local_dict: DICT, global_dict: DICT): + """ + Evaluate Python code generated by ``stringify_expr``. + + Generally, ``parse_expr`` should be used. + """ + expr = eval( + code, global_dict, local_dict) # take local objects in preference + return expr + + +def parse_expr(s: str, local_dict: DICT | None = None, + transformations: tuple[TRANS, ...] | str \ + = standard_transformations, + global_dict: DICT | None = None, evaluate=True): + """Converts the string ``s`` to a SymPy expression, in ``local_dict``. + + .. warning:: + Note that this function uses ``eval``, and thus shouldn't be used on + unsanitized input. + + Parameters + ========== + + s : str + The string to parse. + + local_dict : dict, optional + A dictionary of local variables to use when parsing. + + global_dict : dict, optional + A dictionary of global variables. By default, this is initialized + with ``from sympy import *``; provide this parameter to override + this behavior (for instance, to parse ``"Q & S"``). + + transformations : tuple or str + A tuple of transformation functions used to modify the tokens of the + parsed expression before evaluation. The default transformations + convert numeric literals into their SymPy equivalents, convert + undefined variables into SymPy symbols, and allow the use of standard + mathematical factorial notation (e.g. ``x!``). Selection via + string is available (see below). + + evaluate : bool, optional + When False, the order of the arguments will remain as they were in the + string and automatic simplification that would normally occur is + suppressed. (see examples) + + Examples + ======== + + >>> from sympy.parsing.sympy_parser import parse_expr + >>> parse_expr("1/2") + 1/2 + >>> type(_) + + >>> from sympy.parsing.sympy_parser import standard_transformations,\\ + ... implicit_multiplication_application + >>> transformations = (standard_transformations + + ... (implicit_multiplication_application,)) + >>> parse_expr("2x", transformations=transformations) + 2*x + + When evaluate=False, some automatic simplifications will not occur: + + >>> parse_expr("2**3"), parse_expr("2**3", evaluate=False) + (8, 2**3) + + In addition the order of the arguments will not be made canonical. + This feature allows one to tell exactly how the expression was entered: + + >>> a = parse_expr('1 + x', evaluate=False) + >>> b = parse_expr('x + 1', evaluate=False) + >>> a == b + False + >>> a.args + (1, x) + >>> b.args + (x, 1) + + Note, however, that when these expressions are printed they will + appear the same: + + >>> assert str(a) == str(b) + + As a convenience, transformations can be seen by printing ``transformations``: + + >>> from sympy.parsing.sympy_parser import transformations + + >>> print(transformations) + 0: lambda_notation + 1: auto_symbol + 2: repeated_decimals + 3: auto_number + 4: factorial_notation + 5: implicit_multiplication_application + 6: convert_xor + 7: implicit_application + 8: implicit_multiplication + 9: convert_equals_signs + 10: function_exponentiation + 11: rationalize + + The ``T`` object provides a way to select these transformations: + + >>> from sympy.parsing.sympy_parser import T + + If you print it, you will see the same list as shown above. + + >>> str(T) == str(transformations) + True + + Standard slicing will return a tuple of transformations: + + >>> T[:5] == standard_transformations + True + + So ``T`` can be used to specify the parsing transformations: + + >>> parse_expr("2x", transformations=T[:5]) + Traceback (most recent call last): + ... + SyntaxError: invalid syntax + >>> parse_expr("2x", transformations=T[:6]) + 2*x + >>> parse_expr('.3', transformations=T[3, 11]) + 3/10 + >>> parse_expr('.3x', transformations=T[:]) + 3*x/10 + + As a further convenience, strings 'implicit' and 'all' can be used + to select 0-5 and all the transformations, respectively. + + >>> parse_expr('.3x', transformations='all') + 3*x/10 + + See Also + ======== + + stringify_expr, eval_expr, standard_transformations, + implicit_multiplication_application + + """ + + if local_dict is None: + local_dict = {} + elif not isinstance(local_dict, dict): + raise TypeError('expecting local_dict to be a dict') + elif null in local_dict: + raise ValueError('cannot use "" in local_dict') + + if global_dict is None: + global_dict = {} + exec('from sympy import *', global_dict) + + builtins_dict = vars(builtins) + for name, obj in builtins_dict.items(): + if isinstance(obj, types.BuiltinFunctionType): + global_dict[name] = obj + global_dict['max'] = Max + global_dict['min'] = Min + + elif not isinstance(global_dict, dict): + raise TypeError('expecting global_dict to be a dict') + + transformations = transformations or () + if isinstance(transformations, str): + if transformations == 'all': + _transformations = T[:] + elif transformations == 'implicit': + _transformations = T[:6] + else: + raise ValueError('unknown transformation group name') + else: + _transformations = transformations + + code = stringify_expr(s, local_dict, global_dict, _transformations) + + if not evaluate: + code = compile(evaluateFalse(code), '', 'eval') # type: ignore + + try: + rv = eval_expr(code, local_dict, global_dict) + # restore neutral definitions for names + for i in local_dict.pop(null, ()): + local_dict[i] = null + return rv + except Exception as e: + # restore neutral definitions for names + for i in local_dict.pop(null, ()): + local_dict[i] = null + raise e from ValueError(f"Error from parse_expr with transformed code: {code!r}") + + +def evaluateFalse(s: str): + """ + Replaces operators with the SymPy equivalent and sets evaluate=False. + """ + node = ast.parse(s) + transformed_node = EvaluateFalseTransformer().visit(node) + # node is a Module, we want an Expression + transformed_node = ast.Expression(transformed_node.body[0].value) + + return ast.fix_missing_locations(transformed_node) + + +class EvaluateFalseTransformer(ast.NodeTransformer): + operators = { + ast.Add: 'Add', + ast.Mult: 'Mul', + ast.Pow: 'Pow', + ast.Sub: 'Add', + ast.Div: 'Mul', + ast.BitOr: 'Or', + ast.BitAnd: 'And', + ast.BitXor: 'Not', + } + functions = ( + 'Abs', 'im', 're', 'sign', 'arg', 'conjugate', + 'acos', 'acot', 'acsc', 'asec', 'asin', 'atan', + 'acosh', 'acoth', 'acsch', 'asech', 'asinh', 'atanh', + 'cos', 'cot', 'csc', 'sec', 'sin', 'tan', + 'cosh', 'coth', 'csch', 'sech', 'sinh', 'tanh', + 'exp', 'ln', 'log', 'sqrt', 'cbrt', + ) + + relational_operators = { + ast.NotEq: 'Ne', + ast.Lt: 'Lt', + ast.LtE: 'Le', + ast.Gt: 'Gt', + ast.GtE: 'Ge', + ast.Eq: 'Eq' + } + def visit_Compare(self, node): + def reducer(acc, op_right): + result, left = acc + op, right = op_right + if op.__class__ not in self.relational_operators: + raise ValueError("Only equation or inequality operators are supported") + new = ast.Call( + func=ast.Name( + id=self.relational_operators[op.__class__], ctx=ast.Load() + ), + args=[self.visit(left), self.visit(right)], + keywords=[ast.keyword(arg="evaluate", value=ast.Constant(value=False))], + ) + return result + [new], right + + args, _ = reduce( + reducer, zip(node.ops, node.comparators), ([], node.left) + ) + if len(args) == 1: + return args[0] + return ast.Call( + func=ast.Name(id=self.operators[ast.BitAnd], ctx=ast.Load()), + args=args, + keywords=[ast.keyword(arg="evaluate", value=ast.Constant(value=False))], + ) + + def flatten(self, args, func): + result = [] + for arg in args: + if isinstance(arg, ast.Call): + arg_func = arg.func + if isinstance(arg_func, ast.Call): + arg_func = arg_func.func + if arg_func.id == func: + result.extend(self.flatten(arg.args, func)) + else: + result.append(arg) + else: + result.append(arg) + return result + + def visit_BinOp(self, node): + if node.op.__class__ in self.operators: + sympy_class = self.operators[node.op.__class__] + right = self.visit(node.right) + left = self.visit(node.left) + + rev = False + if isinstance(node.op, ast.Sub): + right = ast.Call( + func=ast.Name(id='Mul', ctx=ast.Load()), + args=[ast.UnaryOp(op=ast.USub(), operand=ast.Constant(1)), right], + keywords=[ast.keyword(arg='evaluate', value=ast.Constant(value=False))] + ) + elif isinstance(node.op, ast.Div): + if isinstance(node.left, ast.UnaryOp): + left, right = right, left + rev = True + left = ast.Call( + func=ast.Name(id='Pow', ctx=ast.Load()), + args=[left, ast.UnaryOp(op=ast.USub(), operand=ast.Constant(1))], + keywords=[ast.keyword(arg='evaluate', value=ast.Constant(value=False))] + ) + else: + right = ast.Call( + func=ast.Name(id='Pow', ctx=ast.Load()), + args=[right, ast.UnaryOp(op=ast.USub(), operand=ast.Constant(1))], + keywords=[ast.keyword(arg='evaluate', value=ast.Constant(value=False))] + ) + + if rev: # undo reversal + left, right = right, left + new_node = ast.Call( + func=ast.Name(id=sympy_class, ctx=ast.Load()), + args=[left, right], + keywords=[ast.keyword(arg='evaluate', value=ast.Constant(value=False))] + ) + + if sympy_class in ('Add', 'Mul'): + # Denest Add or Mul as appropriate + new_node.args = self.flatten(new_node.args, sympy_class) + + return new_node + return node + + def visit_Call(self, node): + new_node = self.generic_visit(node) + if isinstance(node.func, ast.Name) and node.func.id in self.functions: + new_node.keywords.append(ast.keyword(arg='evaluate', value=ast.Constant(value=False))) + return new_node + + +_transformation = { # items can be added but never re-ordered +0: lambda_notation, +1: auto_symbol, +2: repeated_decimals, +3: auto_number, +4: factorial_notation, +5: implicit_multiplication_application, +6: convert_xor, +7: implicit_application, +8: implicit_multiplication, +9: convert_equals_signs, +10: function_exponentiation, +11: rationalize} + +transformations = '\n'.join('%s: %s' % (i, func_name(f)) for i, f in _transformation.items()) + + +class _T(): + """class to retrieve transformations from a given slice + + EXAMPLES + ======== + + >>> from sympy.parsing.sympy_parser import T, standard_transformations + >>> assert T[:5] == standard_transformations + """ + def __init__(self): + self.N = len(_transformation) + + def __str__(self): + return transformations + + def __getitem__(self, t): + if not type(t) is tuple: + t = (t,) + i = [] + for ti in t: + if type(ti) is int: + i.append(range(self.N)[ti]) + elif type(ti) is slice: + i.extend(range(*ti.indices(self.N))) + else: + raise TypeError('unexpected slice arg') + return tuple([_transformation[_] for _ in i]) + +T = _T() diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/parsing/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_ast_parser.py b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_ast_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..24572190df72f9be11b5830355b0d6b9e3bb53ad --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_ast_parser.py @@ -0,0 +1,25 @@ +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.parsing.ast_parser import parse_expr +from sympy.testing.pytest import raises +from sympy.core.sympify import SympifyError +import warnings + +def test_parse_expr(): + a, b = symbols('a, b') + # tests issue_16393 + assert parse_expr('a + b', {}) == a + b + raises(SympifyError, lambda: parse_expr('a + ', {})) + + # tests Transform.visit_Constant + assert parse_expr('1 + 2', {}) == S(3) + assert parse_expr('1 + 2.0', {}) == S(3.0) + + # tests Transform.visit_Name + assert parse_expr('Rational(1, 2)', {}) == S(1)/2 + assert parse_expr('a', {'a': a}) == a + + # tests issue_23092 + with warnings.catch_warnings(): + warnings.simplefilter('error') + assert parse_expr('6 * 7', {}) == S(42) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_autolev.py b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_autolev.py new file mode 100644 index 0000000000000000000000000000000000000000..dfcaef13565c5e2187dc6e90113b407a7967c331 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_autolev.py @@ -0,0 +1,178 @@ +import os + +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.external import import_module +from sympy.testing.pytest import skip +from sympy.parsing.autolev import parse_autolev + +antlr4 = import_module("antlr4") + +if not antlr4: + disabled = True + +FILE_DIR = os.path.dirname( + os.path.dirname(os.path.abspath(os.path.realpath(__file__)))) + + +def _test_examples(in_filename, out_filename, test_name=""): + + in_file_path = os.path.join(FILE_DIR, 'autolev', 'test-examples', + in_filename) + correct_file_path = os.path.join(FILE_DIR, 'autolev', 'test-examples', + out_filename) + with open(in_file_path) as f: + generated_code = parse_autolev(f, include_numeric=True) + + with open(correct_file_path) as f: + for idx, line1 in enumerate(f): + if line1.startswith("#"): + break + try: + line2 = generated_code.split('\n')[idx] + assert line1.rstrip() == line2.rstrip() + except Exception: + msg = 'mismatch in ' + test_name + ' in line no: {0}' + raise AssertionError(msg.format(idx+1)) + + +def test_rule_tests(): + + l = ["ruletest1", "ruletest2", "ruletest3", "ruletest4", "ruletest5", + "ruletest6", "ruletest7", "ruletest8", "ruletest9", "ruletest10", + "ruletest11", "ruletest12"] + + for i in l: + in_filepath = i + ".al" + out_filepath = i + ".py" + _test_examples(in_filepath, out_filepath, i) + + +def test_pydy_examples(): + + l = ["mass_spring_damper", "chaos_pendulum", "double_pendulum", + "non_min_pendulum"] + + for i in l: + in_filepath = os.path.join("pydy-example-repo", i + ".al") + out_filepath = os.path.join("pydy-example-repo", i + ".py") + _test_examples(in_filepath, out_filepath, i) + + +def test_autolev_tutorial(): + + dir_path = os.path.join(FILE_DIR, 'autolev', 'test-examples', + 'autolev-tutorial') + + if os.path.isdir(dir_path): + l = ["tutor1", "tutor2", "tutor3", "tutor4", "tutor5", "tutor6", + "tutor7"] + for i in l: + in_filepath = os.path.join("autolev-tutorial", i + ".al") + out_filepath = os.path.join("autolev-tutorial", i + ".py") + _test_examples(in_filepath, out_filepath, i) + + +def test_dynamics_online(): + + dir_path = os.path.join(FILE_DIR, 'autolev', 'test-examples', + 'dynamics-online') + + if os.path.isdir(dir_path): + ch1 = ["1-4", "1-5", "1-6", "1-7", "1-8", "1-9_1", "1-9_2", "1-9_3"] + ch2 = ["2-1", "2-2", "2-3", "2-4", "2-5", "2-6", "2-7", "2-8", "2-9", + "circular"] + ch3 = ["3-1_1", "3-1_2", "3-2_1", "3-2_2", "3-2_3", "3-2_4", "3-2_5", + "3-3"] + ch4 = ["4-1_1", "4-2_1", "4-4_1", "4-4_2", "4-5_1", "4-5_2"] + chapters = [(ch1, "ch1"), (ch2, "ch2"), (ch3, "ch3"), (ch4, "ch4")] + for ch, name in chapters: + for i in ch: + in_filepath = os.path.join("dynamics-online", name, i + ".al") + out_filepath = os.path.join("dynamics-online", name, i + ".py") + _test_examples(in_filepath, out_filepath, i) + + +def test_output_01(): + """Autolev example calculates the position, velocity, and acceleration of a + point and expresses in a single reference frame:: + + (1) FRAMES C,D,F + (2) VARIABLES FD'',DC'' + (3) CONSTANTS R,L + (4) POINTS O,E + (5) SIMPROT(F,D,1,FD) + -> (6) F_D = [1, 0, 0; 0, COS(FD), -SIN(FD); 0, SIN(FD), COS(FD)] + (7) SIMPROT(D,C,2,DC) + -> (8) D_C = [COS(DC), 0, SIN(DC); 0, 1, 0; -SIN(DC), 0, COS(DC)] + (9) W_C_F> = EXPRESS(W_C_F>, F) + -> (10) W_C_F> = FD'*F1> + COS(FD)*DC'*F2> + SIN(FD)*DC'*F3> + (11) P_O_E>=R*D2>-L*C1> + (12) P_O_E>=EXPRESS(P_O_E>, D) + -> (13) P_O_E> = -L*COS(DC)*D1> + R*D2> + L*SIN(DC)*D3> + (14) V_E_F>=EXPRESS(DT(P_O_E>,F),D) + -> (15) V_E_F> = L*SIN(DC)*DC'*D1> - L*SIN(DC)*FD'*D2> + (R*FD'+L*COS(DC)*DC')*D3> + (16) A_E_F>=EXPRESS(DT(V_E_F>,F),D) + -> (17) A_E_F> = L*(COS(DC)*DC'^2+SIN(DC)*DC'')*D1> + (-R*FD'^2-2*L*COS(DC)*DC'*FD'-L*SIN(DC)*FD'')*D2> + (R*FD''+L*COS(DC)*DC''-L*SIN(DC)*DC'^2-L*SIN(DC)*FD'^2)*D3> + + """ + + if not antlr4: + skip('Test skipped: antlr4 is not installed.') + + autolev_input = """\ +FRAMES C,D,F +VARIABLES FD'',DC'' +CONSTANTS R,L +POINTS O,E +SIMPROT(F,D,1,FD) +SIMPROT(D,C,2,DC) +W_C_F>=EXPRESS(W_C_F>,F) +P_O_E>=R*D2>-L*C1> +P_O_E>=EXPRESS(P_O_E>,D) +V_E_F>=EXPRESS(DT(P_O_E>,F),D) +A_E_F>=EXPRESS(DT(V_E_F>,F),D)\ +""" + + sympy_input = parse_autolev(autolev_input) + + g = {} + l = {} + exec(sympy_input, g, l) + + w_c_f = l['frame_c'].ang_vel_in(l['frame_f']) + # P_O_E> means "the position of point E wrt to point O" + p_o_e = l['point_e'].pos_from(l['point_o']) + v_e_f = l['point_e'].vel(l['frame_f']) + a_e_f = l['point_e'].acc(l['frame_f']) + + # NOTE : The Autolev outputs above were manually transformed into + # equivalent SymPy physics vector expressions. Would be nice to automate + # this transformation. + expected_w_c_f = (l['fd'].diff()*l['frame_f'].x + + cos(l['fd'])*l['dc'].diff()*l['frame_f'].y + + sin(l['fd'])*l['dc'].diff()*l['frame_f'].z) + + assert (w_c_f - expected_w_c_f).simplify() == 0 + + expected_p_o_e = (-l['l']*cos(l['dc'])*l['frame_d'].x + + l['r']*l['frame_d'].y + + l['l']*sin(l['dc'])*l['frame_d'].z) + + assert (p_o_e - expected_p_o_e).simplify() == 0 + + expected_v_e_f = (l['l']*sin(l['dc'])*l['dc'].diff()*l['frame_d'].x - + l['l']*sin(l['dc'])*l['fd'].diff()*l['frame_d'].y + + (l['r']*l['fd'].diff() + + l['l']*cos(l['dc'])*l['dc'].diff())*l['frame_d'].z) + assert (v_e_f - expected_v_e_f).simplify() == 0 + + expected_a_e_f = (l['l']*(cos(l['dc'])*l['dc'].diff()**2 + + sin(l['dc'])*l['dc'].diff().diff())*l['frame_d'].x + + (-l['r']*l['fd'].diff()**2 - + 2*l['l']*cos(l['dc'])*l['dc'].diff()*l['fd'].diff() - + l['l']*sin(l['dc'])*l['fd'].diff().diff())*l['frame_d'].y + + (l['r']*l['fd'].diff().diff() + + l['l']*cos(l['dc'])*l['dc'].diff().diff() - + l['l']*sin(l['dc'])*l['dc'].diff()**2 - + l['l']*sin(l['dc'])*l['fd'].diff()**2)*l['frame_d'].z) + assert (a_e_f - expected_a_e_f).simplify() == 0 diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_c_parser.py b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_c_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..b74622e40030cba180cb4fc354216ccca119baec --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_c_parser.py @@ -0,0 +1,5248 @@ +from sympy.parsing.sym_expr import SymPyExpression +from sympy.testing.pytest import raises, XFAIL +from sympy.external import import_module + +cin = import_module('clang.cindex', import_kwargs = {'fromlist': ['cindex']}) + +if cin: + from sympy.codegen.ast import (Variable, String, Return, + FunctionDefinition, Integer, Float, Declaration, CodeBlock, + FunctionPrototype, FunctionCall, NoneToken, Assignment, Type, + IntBaseType, SignedIntType, UnsignedIntType, FloatType, + AddAugmentedAssignment, SubAugmentedAssignment, + MulAugmentedAssignment, DivAugmentedAssignment, + ModAugmentedAssignment, While) + from sympy.codegen.cnodes import (PreDecrement, PostDecrement, + PreIncrement, PostIncrement) + from sympy.core import (Add, Mul, Mod, Pow, Rational, + StrictLessThan, LessThan, StrictGreaterThan, GreaterThan, + Equality, Unequality) + from sympy.logic.boolalg import And, Not, Or + from sympy.core.symbol import Symbol + from sympy.logic.boolalg import (false, true) + import os + + def test_variable(): + c_src1 = ( + 'int a;' + '\n' + + 'int b;' + '\n' + ) + c_src2 = ( + 'float a;' + '\n' + + 'float b;' + '\n' + ) + c_src3 = ( + 'int a;' + '\n' + + 'float b;' + '\n' + + 'int c;' + ) + c_src4 = ( + 'int x = 1, y = 6.78;' + '\n' + + 'float p = 2, q = 9.67;' + ) + + res1 = SymPyExpression(c_src1, 'c').return_expr() + res2 = SymPyExpression(c_src2, 'c').return_expr() + res3 = SymPyExpression(c_src3, 'c').return_expr() + res4 = SymPyExpression(c_src4, 'c').return_expr() + + assert res1[0] == Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('intc')) + ) + ) + + assert res1[1] == Declaration( + Variable( + Symbol('b'), + type=IntBaseType(String('intc')) + ) + ) + + assert res2[0] == Declaration( + Variable( + Symbol('a'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ) + ) + assert res2[1] == Declaration( + Variable( + Symbol('b'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ) + ) + + assert res3[0] == Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('intc')) + ) + ) + + assert res3[1] == Declaration( + Variable( + Symbol('b'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ) + ) + + assert res3[2] == Declaration( + Variable( + Symbol('c'), + type=IntBaseType(String('intc')) + ) + ) + + assert res4[0] == Declaration( + Variable( + Symbol('x'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res4[1] == Declaration( + Variable( + Symbol('y'), + type=IntBaseType(String('intc')), + value=Integer(6) + ) + ) + + assert res4[2] == Declaration( + Variable( + Symbol('p'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('2.0', precision=53) + ) + ) + + assert res4[3] == Declaration( + Variable( + Symbol('q'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('9.67', precision=53) + ) + ) + + + def test_int(): + c_src1 = 'int a = 1;' + c_src2 = ( + 'int a = 1;' + '\n' + + 'int b = 2;' + '\n' + ) + c_src3 = 'int a = 2.345, b = 5.67;' + c_src4 = 'int p = 6, q = 23.45;' + c_src5 = "int x = '0', y = 'a';" + c_src6 = "int r = true, s = false;" + + # cin.TypeKind.UCHAR + c_src_type1 = ( + "signed char a = 1, b = 5.1;" + ) + + # cin.TypeKind.SHORT + c_src_type2 = ( + "short a = 1, b = 5.1;" + "signed short c = 1, d = 5.1;" + "short int e = 1, f = 5.1;" + "signed short int g = 1, h = 5.1;" + ) + + # cin.TypeKind.INT + c_src_type3 = ( + "signed int a = 1, b = 5.1;" + "int c = 1, d = 5.1;" + ) + + # cin.TypeKind.LONG + c_src_type4 = ( + "long a = 1, b = 5.1;" + "long int c = 1, d = 5.1;" + ) + + # cin.TypeKind.UCHAR + c_src_type5 = "unsigned char a = 1, b = 5.1;" + + # cin.TypeKind.USHORT + c_src_type6 = ( + "unsigned short a = 1, b = 5.1;" + "unsigned short int c = 1, d = 5.1;" + ) + + # cin.TypeKind.UINT + c_src_type7 = "unsigned int a = 1, b = 5.1;" + + # cin.TypeKind.ULONG + c_src_type8 = ( + "unsigned long a = 1, b = 5.1;" + "unsigned long int c = 1, d = 5.1;" + ) + + res1 = SymPyExpression(c_src1, 'c').return_expr() + res2 = SymPyExpression(c_src2, 'c').return_expr() + res3 = SymPyExpression(c_src3, 'c').return_expr() + res4 = SymPyExpression(c_src4, 'c').return_expr() + res5 = SymPyExpression(c_src5, 'c').return_expr() + res6 = SymPyExpression(c_src6, 'c').return_expr() + + res_type1 = SymPyExpression(c_src_type1, 'c').return_expr() + res_type2 = SymPyExpression(c_src_type2, 'c').return_expr() + res_type3 = SymPyExpression(c_src_type3, 'c').return_expr() + res_type4 = SymPyExpression(c_src_type4, 'c').return_expr() + res_type5 = SymPyExpression(c_src_type5, 'c').return_expr() + res_type6 = SymPyExpression(c_src_type6, 'c').return_expr() + res_type7 = SymPyExpression(c_src_type7, 'c').return_expr() + res_type8 = SymPyExpression(c_src_type8, 'c').return_expr() + + assert res1[0] == Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res2[0] == Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res2[1] == Declaration( + Variable( + Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(2) + ) + ) + + assert res3[0] == Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(2) + ) + ) + + assert res3[1] == Declaration( + Variable( + Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(5) + ) + ) + + assert res4[0] == Declaration( + Variable( + Symbol('p'), + type=IntBaseType(String('intc')), + value=Integer(6) + ) + ) + + assert res4[1] == Declaration( + Variable( + Symbol('q'), + type=IntBaseType(String('intc')), + value=Integer(23) + ) + ) + + assert res5[0] == Declaration( + Variable( + Symbol('x'), + type=IntBaseType(String('intc')), + value=Integer(48) + ) + ) + + assert res5[1] == Declaration( + Variable( + Symbol('y'), + type=IntBaseType(String('intc')), + value=Integer(97) + ) + ) + + assert res6[0] == Declaration( + Variable( + Symbol('r'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res6[1] == Declaration( + Variable( + Symbol('s'), + type=IntBaseType(String('intc')), + value=Integer(0) + ) + ) + + assert res_type1[0] == Declaration( + Variable( + Symbol('a'), + type=SignedIntType( + String('int8'), + nbits=Integer(8) + ), + value=Integer(1) + ) + ) + + assert res_type1[1] == Declaration( + Variable( + Symbol('b'), + type=SignedIntType( + String('int8'), + nbits=Integer(8) + ), + value=Integer(5) + ) + ) + + assert res_type2[0] == Declaration( + Variable( + Symbol('a'), + type=SignedIntType( + String('int16'), + nbits=Integer(16) + ), + value=Integer(1) + ) + ) + + assert res_type2[1] == Declaration( + Variable( + Symbol('b'), + type=SignedIntType( + String('int16'), + nbits=Integer(16) + ), + value=Integer(5) + ) + ) + + assert res_type2[2] == Declaration( + Variable(Symbol('c'), + type=SignedIntType( + String('int16'), + nbits=Integer(16) + ), + value=Integer(1) + ) + ) + + assert res_type2[3] == Declaration( + Variable( + Symbol('d'), + type=SignedIntType( + String('int16'), + nbits=Integer(16) + ), + value=Integer(5) + ) + ) + + assert res_type2[4] == Declaration( + Variable( + Symbol('e'), + type=SignedIntType( + String('int16'), + nbits=Integer(16) + ), + value=Integer(1) + ) + ) + + assert res_type2[5] == Declaration( + Variable( + Symbol('f'), + type=SignedIntType( + String('int16'), + nbits=Integer(16) + ), + value=Integer(5) + ) + ) + + assert res_type2[6] == Declaration( + Variable( + Symbol('g'), + type=SignedIntType( + String('int16'), + nbits=Integer(16) + ), + value=Integer(1) + ) + ) + + assert res_type2[7] == Declaration( + Variable( + Symbol('h'), + type=SignedIntType( + String('int16'), + nbits=Integer(16) + ), + value=Integer(5) + ) + ) + + assert res_type3[0] == Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res_type3[1] == Declaration( + Variable( + Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(5) + ) + ) + + assert res_type3[2] == Declaration( + Variable( + Symbol('c'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res_type3[3] == Declaration( + Variable( + Symbol('d'), + type=IntBaseType(String('intc')), + value=Integer(5) + ) + ) + + assert res_type4[0] == Declaration( + Variable( + Symbol('a'), + type=SignedIntType( + String('int64'), + nbits=Integer(64) + ), + value=Integer(1) + ) + ) + + assert res_type4[1] == Declaration( + Variable( + Symbol('b'), + type=SignedIntType( + String('int64'), + nbits=Integer(64) + ), + value=Integer(5) + ) + ) + + assert res_type4[2] == Declaration( + Variable( + Symbol('c'), + type=SignedIntType( + String('int64'), + nbits=Integer(64) + ), + value=Integer(1) + ) + ) + + assert res_type4[3] == Declaration( + Variable( + Symbol('d'), + type=SignedIntType( + String('int64'), + nbits=Integer(64) + ), + value=Integer(5) + ) + ) + + assert res_type5[0] == Declaration( + Variable( + Symbol('a'), + type=UnsignedIntType( + String('uint8'), + nbits=Integer(8) + ), + value=Integer(1) + ) + ) + + assert res_type5[1] == Declaration( + Variable( + Symbol('b'), + type=UnsignedIntType( + String('uint8'), + nbits=Integer(8) + ), + value=Integer(5) + ) + ) + + assert res_type6[0] == Declaration( + Variable( + Symbol('a'), + type=UnsignedIntType( + String('uint16'), + nbits=Integer(16) + ), + value=Integer(1) + ) + ) + + assert res_type6[1] == Declaration( + Variable( + Symbol('b'), + type=UnsignedIntType( + String('uint16'), + nbits=Integer(16) + ), + value=Integer(5) + ) + ) + + assert res_type6[2] == Declaration( + Variable( + Symbol('c'), + type=UnsignedIntType( + String('uint16'), + nbits=Integer(16) + ), + value=Integer(1) + ) + ) + + assert res_type6[3] == Declaration( + Variable( + Symbol('d'), + type=UnsignedIntType( + String('uint16'), + nbits=Integer(16) + ), + value=Integer(5) + ) + ) + + assert res_type7[0] == Declaration( + Variable( + Symbol('a'), + type=UnsignedIntType( + String('uint32'), + nbits=Integer(32) + ), + value=Integer(1) + ) + ) + + assert res_type7[1] == Declaration( + Variable( + Symbol('b'), + type=UnsignedIntType( + String('uint32'), + nbits=Integer(32) + ), + value=Integer(5) + ) + ) + + assert res_type8[0] == Declaration( + Variable( + Symbol('a'), + type=UnsignedIntType( + String('uint64'), + nbits=Integer(64) + ), + value=Integer(1) + ) + ) + + assert res_type8[1] == Declaration( + Variable( + Symbol('b'), + type=UnsignedIntType( + String('uint64'), + nbits=Integer(64) + ), + value=Integer(5) + ) + ) + + assert res_type8[2] == Declaration( + Variable( + Symbol('c'), + type=UnsignedIntType( + String('uint64'), + nbits=Integer(64) + ), + value=Integer(1) + ) + ) + + assert res_type8[3] == Declaration( + Variable( + Symbol('d'), + type=UnsignedIntType( + String('uint64'), + nbits=Integer(64) + ), + value=Integer(5) + ) + ) + + + def test_float(): + c_src1 = 'float a = 1.0;' + c_src2 = ( + 'float a = 1.25;' + '\n' + + 'float b = 2.39;' + '\n' + ) + c_src3 = 'float x = 1, y = 2;' + c_src4 = 'float p = 5, e = 7.89;' + c_src5 = 'float r = true, s = false;' + + # cin.TypeKind.FLOAT + c_src_type1 = 'float x = 1, y = 2.5;' + + # cin.TypeKind.DOUBLE + c_src_type2 = 'double x = 1, y = 2.5;' + + # cin.TypeKind.LONGDOUBLE + c_src_type3 = 'long double x = 1, y = 2.5;' + + res1 = SymPyExpression(c_src1, 'c').return_expr() + res2 = SymPyExpression(c_src2, 'c').return_expr() + res3 = SymPyExpression(c_src3, 'c').return_expr() + res4 = SymPyExpression(c_src4, 'c').return_expr() + res5 = SymPyExpression(c_src5, 'c').return_expr() + + res_type1 = SymPyExpression(c_src_type1, 'c').return_expr() + res_type2 = SymPyExpression(c_src_type2, 'c').return_expr() + res_type3 = SymPyExpression(c_src_type3, 'c').return_expr() + + assert res1[0] == Declaration( + Variable( + Symbol('a'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('1.0', precision=53) + ) + ) + + assert res2[0] == Declaration( + Variable( + Symbol('a'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('1.25', precision=53) + ) + ) + + assert res2[1] == Declaration( + Variable( + Symbol('b'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('2.3900000000000001', precision=53) + ) + ) + + assert res3[0] == Declaration( + Variable( + Symbol('x'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('1.0', precision=53) + ) + ) + + assert res3[1] == Declaration( + Variable( + Symbol('y'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('2.0', precision=53) + ) + ) + + assert res4[0] == Declaration( + Variable( + Symbol('p'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('5.0', precision=53) + ) + ) + + assert res4[1] == Declaration( + Variable( + Symbol('e'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('7.89', precision=53) + ) + ) + + assert res5[0] == Declaration( + Variable( + Symbol('r'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('1.0', precision=53) + ) + ) + + assert res5[1] == Declaration( + Variable( + Symbol('s'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('0.0', precision=53) + ) + ) + + assert res_type1[0] == Declaration( + Variable( + Symbol('x'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('1.0', precision=53) + ) + ) + + assert res_type1[1] == Declaration( + Variable( + Symbol('y'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('2.5', precision=53) + ) + ) + assert res_type2[0] == Declaration( + Variable( + Symbol('x'), + type=FloatType( + String('float64'), + nbits=Integer(64), + nmant=Integer(52), + nexp=Integer(11) + ), + value=Float('1.0', precision=53) + ) + ) + + assert res_type2[1] == Declaration( + Variable( + Symbol('y'), + type=FloatType( + String('float64'), + nbits=Integer(64), + nmant=Integer(52), + nexp=Integer(11) + ), + value=Float('2.5', precision=53) + ) + ) + + assert res_type3[0] == Declaration( + Variable( + Symbol('x'), + type=FloatType( + String('float80'), + nbits=Integer(80), + nmant=Integer(63), + nexp=Integer(15) + ), + value=Float('1.0', precision=53) + ) + ) + + assert res_type3[1] == Declaration( + Variable( + Symbol('y'), + type=FloatType( + String('float80'), + nbits=Integer(80), + nmant=Integer(63), + nexp=Integer(15) + ), + value=Float('2.5', precision=53) + ) + ) + + + def test_bool(): + c_src1 = ( + 'bool a = true, b = false;' + ) + + c_src2 = ( + 'bool a = 1, b = 0;' + ) + + c_src3 = ( + 'bool a = 10, b = 20;' + ) + + c_src4 = ( + 'bool a = 19.1, b = 9.0, c = 0.0;' + ) + + res1 = SymPyExpression(c_src1, 'c').return_expr() + res2 = SymPyExpression(c_src2, 'c').return_expr() + res3 = SymPyExpression(c_src3, 'c').return_expr() + res4 = SymPyExpression(c_src4, 'c').return_expr() + + assert res1[0] == Declaration( + Variable(Symbol('a'), + type=Type(String('bool')), + value=true + ) + ) + + assert res1[1] == Declaration( + Variable(Symbol('b'), + type=Type(String('bool')), + value=false + ) + ) + + assert res2[0] == Declaration( + Variable(Symbol('a'), + type=Type(String('bool')), + value=true) + ) + + assert res2[1] == Declaration( + Variable(Symbol('b'), + type=Type(String('bool')), + value=false + ) + ) + + assert res3[0] == Declaration( + Variable(Symbol('a'), + type=Type(String('bool')), + value=true + ) + ) + + assert res3[1] == Declaration( + Variable(Symbol('b'), + type=Type(String('bool')), + value=true + ) + ) + + assert res4[0] == Declaration( + Variable(Symbol('a'), + type=Type(String('bool')), + value=true) + ) + + assert res4[1] == Declaration( + Variable(Symbol('b'), + type=Type(String('bool')), + value=true + ) + ) + + assert res4[2] == Declaration( + Variable(Symbol('c'), + type=Type(String('bool')), + value=false + ) + ) + + @XFAIL # this is expected to fail because of a bug in the C parser. + def test_function(): + c_src1 = ( + 'void fun1()' + '\n' + + '{' + '\n' + + 'int a;' + '\n' + + '}' + ) + c_src2 = ( + 'int fun2()' + '\n' + + '{'+ '\n' + + 'int a;' + '\n' + + 'return a;' + '\n' + + '}' + ) + c_src3 = ( + 'float fun3()' + '\n' + + '{' + '\n' + + 'float b;' + '\n' + + 'return b;' + '\n' + + '}' + ) + c_src4 = ( + 'float fun4()' + '\n' + + '{}' + ) + + res1 = SymPyExpression(c_src1, 'c').return_expr() + res2 = SymPyExpression(c_src2, 'c').return_expr() + res3 = SymPyExpression(c_src3, 'c').return_expr() + res4 = SymPyExpression(c_src4, 'c').return_expr() + + assert res1[0] == FunctionDefinition( + NoneToken(), + name=String('fun1'), + parameters=(), + body=CodeBlock( + Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('intc')) + ) + ) + ) + ) + + assert res2[0] == FunctionDefinition( + IntBaseType(String('intc')), + name=String('fun2'), + parameters=(), + body=CodeBlock( + Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('intc')) + ) + ), + Return('a') + ) + ) + + assert res3[0] == FunctionDefinition( + FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + name=String('fun3'), + parameters=(), + body=CodeBlock( + Declaration( + Variable( + Symbol('b'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ) + ), + Return('b') + ) + ) + + assert res4[0] == FunctionPrototype( + FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + name=String('fun4'), + parameters=() + ) + + @XFAIL # this is expected to fail because of a bug in the C parser. + def test_parameters(): + c_src1 = ( + 'void fun1( int a)' + '\n' + + '{' + '\n' + + 'int i;' + '\n' + + '}' + ) + c_src2 = ( + 'int fun2(float x, float y)' + '\n' + + '{'+ '\n' + + 'int a;' + '\n' + + 'return a;' + '\n' + + '}' + ) + c_src3 = ( + 'float fun3(int p, float q, int r)' + '\n' + + '{' + '\n' + + 'float b;' + '\n' + + 'return b;' + '\n' + + '}' + ) + + res1 = SymPyExpression(c_src1, 'c').return_expr() + res2 = SymPyExpression(c_src2, 'c').return_expr() + res3 = SymPyExpression(c_src3, 'c').return_expr() + + assert res1[0] == FunctionDefinition( + NoneToken(), + name=String('fun1'), + parameters=( + Variable( + Symbol('a'), + type=IntBaseType(String('intc')) + ), + ), + body=CodeBlock( + Declaration( + Variable( + Symbol('i'), + type=IntBaseType(String('intc')) + ) + ) + ) + ) + + assert res2[0] == FunctionDefinition( + IntBaseType(String('intc')), + name=String('fun2'), + parameters=( + Variable( + Symbol('x'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ), + Variable( + Symbol('y'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ) + ), + body=CodeBlock( + Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('intc')) + ) + ), + Return('a') + ) + ) + + assert res3[0] == FunctionDefinition( + FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + name=String('fun3'), + parameters=( + Variable( + Symbol('p'), + type=IntBaseType(String('intc')) + ), + Variable( + Symbol('q'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ), + Variable( + Symbol('r'), + type=IntBaseType(String('intc')) + ) + ), + body=CodeBlock( + Declaration( + Variable( + Symbol('b'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ) + ), + Return('b') + ) + ) + + @XFAIL # this is expected to fail because of a bug in the C parser. + def test_function_call(): + c_src1 = ( + 'int fun1(int x)' + '\n' + + '{' + '\n' + + 'return x;' + '\n' + + '}' + '\n' + + 'void caller()' + '\n' + + '{' + '\n' + + 'int x = fun1(2);' + '\n' + + '}' + ) + + c_src2 = ( + 'int fun2(int a, int b, int c)' + '\n' + + '{' + '\n' + + 'return a;' + '\n' + + '}' + '\n' + + 'void caller()' + '\n' + + '{' + '\n' + + 'int y = fun2(2, 3, 4);' + '\n' + + '}' + ) + + c_src3 = ( + 'int fun3(int a, int b, int c)' + '\n' + + '{' + '\n' + + 'return b;' + '\n' + + '}' + '\n' + + 'void caller()' + '\n' + + '{' + '\n' + + 'int p;' + '\n' + + 'int q;' + '\n' + + 'int r;' + '\n' + + 'int z = fun3(p, q, r);' + '\n' + + '}' + ) + + c_src4 = ( + 'int fun4(float a, float b, int c)' + '\n' + + '{' + '\n' + + 'return c;' + '\n' + + '}' + '\n' + + 'void caller()' + '\n' + + '{' + '\n' + + 'float x;' + '\n' + + 'float y;' + '\n' + + 'int z;' + '\n' + + 'int i = fun4(x, y, z)' + '\n' + + '}' + ) + + c_src5 = ( + 'int fun()' + '\n' + + '{' + '\n' + + 'return 1;' + '\n' + + '}' + '\n' + + 'void caller()' + '\n' + + '{' + '\n' + + 'int a = fun()' + '\n' + + '}' + ) + + res1 = SymPyExpression(c_src1, 'c').return_expr() + res2 = SymPyExpression(c_src2, 'c').return_expr() + res3 = SymPyExpression(c_src3, 'c').return_expr() + res4 = SymPyExpression(c_src4, 'c').return_expr() + res5 = SymPyExpression(c_src5, 'c').return_expr() + + + assert res1[0] == FunctionDefinition( + IntBaseType(String('intc')), + name=String('fun1'), + parameters=(Variable(Symbol('x'), + type=IntBaseType(String('intc')) + ), + ), + body=CodeBlock( + Return('x') + ) + ) + + assert res1[1] == FunctionDefinition( + NoneToken(), + name=String('caller'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('x'), + value=FunctionCall(String('fun1'), + function_args=( + Integer(2), + ) + ) + ) + ) + ) + ) + + assert res2[0] == FunctionDefinition( + IntBaseType(String('intc')), + name=String('fun2'), + parameters=(Variable(Symbol('a'), + type=IntBaseType(String('intc')) + ), + Variable(Symbol('b'), + type=IntBaseType(String('intc')) + ), + Variable(Symbol('c'), + type=IntBaseType(String('intc')) + ) + ), + body=CodeBlock( + Return('a') + ) + ) + + assert res2[1] == FunctionDefinition( + NoneToken(), + name=String('caller'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('y'), + value=FunctionCall( + String('fun2'), + function_args=( + Integer(2), + Integer(3), + Integer(4) + ) + ) + ) + ) + ) + ) + + assert res3[0] == FunctionDefinition( + IntBaseType(String('intc')), + name=String('fun3'), + parameters=( + Variable(Symbol('a'), + type=IntBaseType(String('intc')) + ), + Variable(Symbol('b'), + type=IntBaseType(String('intc')) + ), + Variable(Symbol('c'), + type=IntBaseType(String('intc')) + ) + ), + body=CodeBlock( + Return('b') + ) + ) + + assert res3[1] == FunctionDefinition( + NoneToken(), + name=String('caller'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('p'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('q'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('r'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('z'), + value=FunctionCall( + String('fun3'), + function_args=( + Symbol('p'), + Symbol('q'), + Symbol('r') + ) + ) + ) + ) + ) + ) + + assert res4[0] == FunctionDefinition( + IntBaseType(String('intc')), + name=String('fun4'), + parameters=(Variable(Symbol('a'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ), + Variable(Symbol('b'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ), + Variable(Symbol('c'), + type=IntBaseType(String('intc')) + ) + ), + body=CodeBlock( + Return('c') + ) + ) + + assert res4[1] == FunctionDefinition( + NoneToken(), + name=String('caller'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('x'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ) + ), + Declaration( + Variable(Symbol('y'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ) + ), + Declaration( + Variable(Symbol('z'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('i'), + value=FunctionCall(String('fun4'), + function_args=( + Symbol('x'), + Symbol('y'), + Symbol('z') + ) + ) + ) + ) + ) + ) + + assert res5[0] == FunctionDefinition( + IntBaseType(String('intc')), + name=String('fun'), + parameters=(), + body=CodeBlock( + Return('') + ) + ) + + assert res5[1] == FunctionDefinition( + NoneToken(), + name=String('caller'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + value=FunctionCall(String('fun'), + function_args=() + ) + ) + ) + ) + ) + + + def test_parse(): + c_src1 = ( + 'int a;' + '\n' + + 'int b;' + '\n' + ) + c_src2 = ( + 'void fun1()' + '\n' + + '{' + '\n' + + 'int a;' + '\n' + + '}' + ) + + f1 = open('..a.h', 'w') + f2 = open('..b.h', 'w') + + f1.write(c_src1) + f2. write(c_src2) + + f1.close() + f2.close() + + res1 = SymPyExpression('..a.h', 'c').return_expr() + res2 = SymPyExpression('..b.h', 'c').return_expr() + + os.remove('..a.h') + os.remove('..b.h') + + assert res1[0] == Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('intc')) + ) + ) + assert res1[1] == Declaration( + Variable( + Symbol('b'), + type=IntBaseType(String('intc')) + ) + ) + assert res2[0] == FunctionDefinition( + NoneToken(), + name=String('fun1'), + parameters=(), + body=CodeBlock( + Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('intc')) + ) + ) + ) + ) + + + def test_binary_operators(): + c_src1 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'a = 1;' + '\n' + + '}' + ) + c_src2 = ( + 'void func()'+ + '{' + '\n' + + 'int a = 0;' + '\n' + + 'a = a + 1;' + '\n' + + 'a = 3*a - 10;' + '\n' + + '}' + ) + c_src3 = ( + 'void func()'+ + '{' + '\n' + + 'int a = 10;' + '\n' + + 'a = 1 + a - 3 * 6;' + '\n' + + '}' + ) + c_src4 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'int b;' + '\n' + + 'a = 100;' + '\n' + + 'b = a*a + a*a + a + 19*a + 1 + 24;' + '\n' + + '}' + ) + c_src5 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'int b;' + '\n' + + 'int c;' + '\n' + + 'int d;' + '\n' + + 'a = 1;' + '\n' + + 'b = 2;' + '\n' + + 'c = b;' + '\n' + + 'd = ((a+b)*(a+c))*((c-d)*(a+c));' + '\n' + + '}' + ) + c_src6 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'int b;' + '\n' + + 'int c;' + '\n' + + 'int d;' + '\n' + + 'a = 1;' + '\n' + + 'b = 2;' + '\n' + + 'c = 3;' + '\n' + + 'd = (a*a*a*a + 3*b*b + b + b + c*d);' + '\n' + + '}' + ) + c_src7 = ( + 'void func()'+ + '{' + '\n' + + 'float a;' + '\n' + + 'a = 1.01;' + '\n' + + '}' + ) + + c_src8 = ( + 'void func()'+ + '{' + '\n' + + 'float a;' + '\n' + + 'a = 10.0 + 2.5;' + '\n' + + '}' + ) + + c_src9 = ( + 'void func()'+ + '{' + '\n' + + 'float a;' + '\n' + + 'a = 10.0 / 2.5;' + '\n' + + '}' + ) + + c_src10 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'a = 100 / 4;' + '\n' + + '}' + ) + + c_src11 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'a = 20 - 100 / 4 * 5 + 10;' + '\n' + + '}' + ) + + c_src12 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'a = (20 - 100) / 4 * (5 + 10);' + '\n' + + '}' + ) + + c_src13 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'int b;' + '\n' + + 'float c;' + '\n' + + 'c = b/a;' + '\n' + + '}' + ) + + c_src14 = ( + 'void func()'+ + '{' + '\n' + + 'int a = 2;' + '\n' + + 'int d = 5;' + '\n' + + 'int n = 10;' + '\n' + + 'int s;' + '\n' + + 's = (a/2)*(2*a + (n-1)*d);' + '\n' + + '}' + ) + + c_src15 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'a = 1 % 2;' + '\n' + + '}' + ) + + c_src16 = ( + 'void func()'+ + '{' + '\n' + + 'int a = 2;' + '\n' + + 'int b;' + '\n' + + 'b = a % 3;' + '\n' + + '}' + ) + + c_src17 = ( + 'void func()'+ + '{' + '\n' + + 'int a = 100;' + '\n' + + 'int b = 3;' + '\n' + + 'int c;' + '\n' + + 'c = a % b;' + '\n' + + '}' + ) + + c_src18 = ( + 'void func()'+ + '{' + '\n' + + 'int a = 100;' + '\n' + + 'int b = 3;' + '\n' + + 'int mod = 1000000007;' + '\n' + + 'int c;' + '\n' + + 'c = (a + b * (100/a)) % mod;' + '\n' + + '}' + ) + + c_src19 = ( + 'void func()'+ + '{' + '\n' + + 'int a = 100;' + '\n' + + 'int b = 3;' + '\n' + + 'int mod = 1000000007;' + '\n' + + 'int c;' + '\n' + + 'c = ((a % mod + b % mod) % mod' \ + '* (a % mod - b % mod) % mod) % mod;' + '\n' + + '}' + ) + + c_src20 = ( + 'void func()'+ + '{' + '\n' + + 'bool a' + '\n' + + 'bool b;' + '\n' + + 'a = 1 == 2;' + '\n' + + 'b = 1 != 2;' + '\n' + + '}' + ) + + c_src21 = ( + 'void func()'+ + '{' + '\n' + + 'bool a;' + '\n' + + 'bool b;' + '\n' + + 'bool c;' + '\n' + + 'bool d;' + '\n' + + 'a = 1 == 2;' + '\n' + + 'b = 1 <= 2;' + '\n' + + 'c = 1 > 2;' + '\n' + + 'd = 1 >= 2;' + '\n' + + '}' + ) + + c_src22 = ( + 'void func()'+ + '{' + '\n' + + 'int a = 1;' + '\n' + + 'int b = 2;' + '\n' + + + 'bool c1;' + '\n' + + 'bool c2;' + '\n' + + 'bool c3;' + '\n' + + 'bool c4;' + '\n' + + 'bool c5;' + '\n' + + 'bool c6;' + '\n' + + 'bool c7;' + '\n' + + 'bool c8;' + '\n' + + + 'c1 = a == 1;' + '\n' + + 'c2 = b == 2;' + '\n' + + + 'c3 = 1 != a;' + '\n' + + 'c4 = 1 != b;' + '\n' + + + 'c5 = a < 0;' + '\n' + + 'c6 = b <= 10;' + '\n' + + 'c7 = a > 0;' + '\n' + + 'c8 = b >= 11;' + '\n' + + '}' + ) + + c_src23 = ( + 'void func()'+ + '{' + '\n' + + 'int a = 3;' + '\n' + + 'int b = 4;' + '\n' + + + 'bool c1;' + '\n' + + 'bool c2;' + '\n' + + 'bool c3;' + '\n' + + 'bool c4;' + '\n' + + 'bool c5;' + '\n' + + 'bool c6;' + '\n' + + + 'c1 = a == b;' + '\n' + + 'c2 = a != b;' + '\n' + + 'c3 = a < b;' + '\n' + + 'c4 = a <= b;' + '\n' + + 'c5 = a > b;' + '\n' + + 'c6 = a >= b;' + '\n' + + '}' + ) + + c_src24 = ( + 'void func()'+ + '{' + '\n' + + 'float a = 1.25' + 'float b = 2.5;' + '\n' + + + 'bool c1;' + '\n' + + 'bool c2;' + '\n' + + 'bool c3;' + '\n' + + 'bool c4;' + '\n' + + + 'c1 = a == 1.25;' + '\n' + + 'c2 = b == 2.54;' + '\n' + + + 'c3 = 1.2 != a;' + '\n' + + 'c4 = 1.5 != b;' + '\n' + + '}' + ) + + c_src25 = ( + 'void func()'+ + '{' + '\n' + + 'float a = 1.25' + '\n' + + 'float b = 2.5;' + '\n' + + + 'bool c1;' + '\n' + + 'bool c2;' + '\n' + + 'bool c3;' + '\n' + + 'bool c4;' + '\n' + + 'bool c5;' + '\n' + + 'bool c6;' + '\n' + + + 'c1 = a == b;' + '\n' + + 'c2 = a != b;' + '\n' + + 'c3 = a < b;' + '\n' + + 'c4 = a <= b;' + '\n' + + 'c5 = a > b;' + '\n' + + 'c6 = a >= b;' + '\n' + + '}' + ) + + c_src26 = ( + 'void func()'+ + '{' + '\n' + + 'bool c1;' + '\n' + + 'bool c2;' + '\n' + + 'bool c3;' + '\n' + + 'bool c4;' + '\n' + + 'bool c5;' + '\n' + + 'bool c6;' + '\n' + + + 'c1 = true == true;' + '\n' + + 'c2 = true == false;' + '\n' + + 'c3 = false == false;' + '\n' + + + 'c4 = true != true;' + '\n' + + 'c5 = true != false;' + '\n' + + 'c6 = false != false;' + '\n' + + '}' + ) + + c_src27 = ( + 'void func()'+ + '{' + '\n' + + 'bool c1;' + '\n' + + 'bool c2;' + '\n' + + 'bool c3;' + '\n' + + 'bool c4;' + '\n' + + 'bool c5;' + '\n' + + 'bool c6;' + '\n' + + + 'c1 = true && true;' + '\n' + + 'c2 = true && false;' + '\n' + + 'c3 = false && false;' + '\n' + + + 'c4 = true || true;' + '\n' + + 'c5 = true || false;' + '\n' + + 'c6 = false || false;' + '\n' + + '}' + ) + + c_src28 = ( + 'void func()'+ + '{' + '\n' + + 'bool a;' + '\n' + + 'bool c1;' + '\n' + + 'bool c2;' + '\n' + + 'bool c3;' + '\n' + + 'bool c4;' + '\n' + + + 'c1 = a && true;' + '\n' + + 'c2 = false && a;' + '\n' + + + 'c3 = true || a;' + '\n' + + 'c4 = a || false;' + '\n' + + '}' + ) + + c_src29 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'bool c1;' + '\n' + + 'bool c2;' + '\n' + + 'bool c3;' + '\n' + + 'bool c4;' + '\n' + + + 'c1 = a && 1;' + '\n' + + 'c2 = a && 0;' + '\n' + + + 'c3 = a || 1;' + '\n' + + 'c4 = 0 || a;' + '\n' + + '}' + ) + + c_src30 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'int b;' + '\n' + + 'bool c;'+ '\n' + + 'bool d;'+ '\n' + + + 'bool c1;' + '\n' + + 'bool c2;' + '\n' + + 'bool c3;' + '\n' + + 'bool c4;' + '\n' + + 'bool c5;' + '\n' + + 'bool c6;' + '\n' + + + 'c1 = a && b;' + '\n' + + 'c2 = a && c;' + '\n' + + 'c3 = c && d;' + '\n' + + + 'c4 = a || b;' + '\n' + + 'c5 = a || c;' + '\n' + + 'c6 = c || d;' + '\n' + + '}' + ) + + c_src_raise1 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'a = -1;' + '\n' + + '}' + ) + + c_src_raise2 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'a = -+1;' + '\n' + + '}' + ) + + c_src_raise3 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'a = 2*-2;' + '\n' + + '}' + ) + + c_src_raise4 = ( + 'void func()'+ + '{' + '\n' + + 'int a;' + '\n' + + 'a = (int)2.0;' + '\n' + + '}' + ) + + c_src_raise5 = ( + 'void func()'+ + '{' + '\n' + + 'int a=100;' + '\n' + + 'a = (a==100)?(1):(0);' + '\n' + + '}' + ) + + res1 = SymPyExpression(c_src1, 'c').return_expr() + res2 = SymPyExpression(c_src2, 'c').return_expr() + res3 = SymPyExpression(c_src3, 'c').return_expr() + res4 = SymPyExpression(c_src4, 'c').return_expr() + res5 = SymPyExpression(c_src5, 'c').return_expr() + res6 = SymPyExpression(c_src6, 'c').return_expr() + res7 = SymPyExpression(c_src7, 'c').return_expr() + res8 = SymPyExpression(c_src8, 'c').return_expr() + res9 = SymPyExpression(c_src9, 'c').return_expr() + res10 = SymPyExpression(c_src10, 'c').return_expr() + res11 = SymPyExpression(c_src11, 'c').return_expr() + res12 = SymPyExpression(c_src12, 'c').return_expr() + res13 = SymPyExpression(c_src13, 'c').return_expr() + res14 = SymPyExpression(c_src14, 'c').return_expr() + res15 = SymPyExpression(c_src15, 'c').return_expr() + res16 = SymPyExpression(c_src16, 'c').return_expr() + res17 = SymPyExpression(c_src17, 'c').return_expr() + res18 = SymPyExpression(c_src18, 'c').return_expr() + res19 = SymPyExpression(c_src19, 'c').return_expr() + res20 = SymPyExpression(c_src20, 'c').return_expr() + res21 = SymPyExpression(c_src21, 'c').return_expr() + res22 = SymPyExpression(c_src22, 'c').return_expr() + res23 = SymPyExpression(c_src23, 'c').return_expr() + res24 = SymPyExpression(c_src24, 'c').return_expr() + res25 = SymPyExpression(c_src25, 'c').return_expr() + res26 = SymPyExpression(c_src26, 'c').return_expr() + res27 = SymPyExpression(c_src27, 'c').return_expr() + res28 = SymPyExpression(c_src28, 'c').return_expr() + res29 = SymPyExpression(c_src29, 'c').return_expr() + res30 = SymPyExpression(c_src30, 'c').return_expr() + + assert res1[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')) + ) + ), + Assignment(Variable(Symbol('a')), Integer(1)) + ) + ) + + assert res2[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(0))), + Assignment( + Variable(Symbol('a')), + Add(Symbol('a'), + Integer(1)) + ), + Assignment(Variable(Symbol('a')), + Add( + Mul( + Integer(3), + Symbol('a')), + Integer(-10) + ) + ) + ) + ) + + assert res3[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(10) + ) + ), + Assignment( + Variable(Symbol('a')), + Add( + Symbol('a'), + Integer(-17) + ) + ) + ) + ) + + assert res4[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')) + ) + ), + Assignment( + Variable(Symbol('a')), + Integer(100)), + Assignment( + Variable(Symbol('b')), + Add( + Mul( + Integer(2), + Pow( + Symbol('a'), + Integer(2)) + ), + Mul( + Integer(20), + Symbol('a')), + Integer(25) + ) + ) + ) + ) + + assert res5[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('c'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('d'), + type=IntBaseType(String('intc')) + ) + ), + Assignment( + Variable(Symbol('a')), + Integer(1)), + Assignment( + Variable(Symbol('b')), + Integer(2) + ), + Assignment( + Variable(Symbol('c')), + Symbol('b')), + Assignment( + Variable(Symbol('d')), + Mul( + Add( + Symbol('a'), + Symbol('b')), + Pow( + Add( + Symbol('a'), + Symbol('c') + ), + Integer(2) + ), + Add( + Symbol('c'), + Mul( + Integer(-1), + Symbol('d') + ) + ) + ) + ) + ) + ) + + assert res6[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('c'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('d'), + type=IntBaseType(String('intc')) + ) + ), + Assignment( + Variable(Symbol('a')), + Integer(1) + ), + Assignment( + Variable(Symbol('b')), + Integer(2) + ), + Assignment( + Variable(Symbol('c')), + Integer(3) + ), + Assignment( + Variable(Symbol('d')), + Add( + Pow( + Symbol('a'), + Integer(4) + ), + Mul( + Integer(3), + Pow( + Symbol('b'), + Integer(2) + ) + ), + Mul( + Integer(2), + Symbol('b') + ), + Mul( + Symbol('c'), + Symbol('d') + ) + ) + ) + ) + ) + + assert res7[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ) + ), + Assignment( + Variable(Symbol('a')), + Float('1.01', precision=53) + ) + ) + ) + + assert res8[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ) + ), + Assignment( + Variable(Symbol('a')), + Float('12.5', precision=53) + ) + ) + ) + + assert res9[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ) + ), + Assignment( + Variable(Symbol('a')), + Float('4.0', precision=53) + ) + ) + ) + + assert res10[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')) + ) + ), + Assignment( + Variable(Symbol('a')), + Integer(25) + ) + ) + ) + + assert res11[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')) + ) + ), + Assignment( + Variable(Symbol('a')), + Integer(-95) + ) + ) + ) + + assert res12[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')) + ) + ), + Assignment( + Variable(Symbol('a')), + Integer(-300) + ) + ) + ) + + assert res13[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('c'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ) + ), + Assignment( + Variable(Symbol('c')), + Mul( + Pow( + Symbol('a'), + Integer(-1) + ), + Symbol('b') + ) + ) + ) + ) + + assert res14[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(2) + ) + ), + Declaration( + Variable(Symbol('d'), + type=IntBaseType(String('intc')), + value=Integer(5) + ) + ), + Declaration( + Variable(Symbol('n'), + type=IntBaseType(String('intc')), + value=Integer(10) + ) + ), + Declaration( + Variable(Symbol('s'), + type=IntBaseType(String('intc')) + ) + ), + Assignment( + Variable(Symbol('s')), + Mul( + Rational(1, 2), + Symbol('a'), + Add( + Mul( + Integer(2), + Symbol('a') + ), + Mul( + Symbol('d'), + Add( + Symbol('n'), + Integer(-1) + ) + ) + ) + ) + ) + ) + ) + + assert res15[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')) + ) + ), + Assignment( + Variable(Symbol('a')), + Integer(1) + ) + ) + ) + + assert res16[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(2) + ) + ), + Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')) + ) + ), + Assignment( + Variable(Symbol('b')), + Mod( + Symbol('a'), + Integer(3) + ) + ) + ) + ) + + assert res17[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(100) + ) + ), + Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(3) + ) + ), + Declaration( + Variable(Symbol('c'), + type=IntBaseType(String('intc')) + ) + ), + Assignment( + Variable(Symbol('c')), + Mod( + Symbol('a'), + Symbol('b') + ) + ) + ) + ) + + assert res18[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(100) + ) + ), + Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(3) + ) + ), + Declaration( + Variable(Symbol('mod'), + type=IntBaseType(String('intc')), + value=Integer(1000000007) + ) + ), + Declaration( + Variable(Symbol('c'), + type=IntBaseType(String('intc')) + ) + ), + Assignment( + Variable(Symbol('c')), + Mod( + Add( + Symbol('a'), + Mul( + Integer(100), + Pow( + Symbol('a'), + Integer(-1) + ), + Symbol('b') + ) + ), + Symbol('mod') + ) + ) + ) + ) + + assert res19[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(100) + ) + ), + Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(3) + ) + ), + Declaration( + Variable(Symbol('mod'), + type=IntBaseType(String('intc')), + value=Integer(1000000007) + ) + ), + Declaration( + Variable(Symbol('c'), + type=IntBaseType(String('intc')) + ) + ), + Assignment( + Variable(Symbol('c')), + Mod( + Mul( + Add( + Mod( + Symbol('a'), + Symbol('mod') + ), + Mul( + Integer(-1), + Mod( + Symbol('b'), + Symbol('mod') + ) + ) + ), + Mod( + Add( + Symbol('a'), + Symbol('b') + ), + Symbol('mod') + ) + ), + Symbol('mod') + ) + ) + ) + ) + + assert res20[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('b'), + type=Type(String('bool')) + ) + ), + Assignment( + Variable(Symbol('a')), + false + ), + Assignment( + Variable(Symbol('b')), + true + ) + ) + ) + + assert res21[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('b'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('d'), + type=Type(String('bool')) + ) + ), + Assignment( + Variable(Symbol('a')), + false + ), + Assignment( + Variable(Symbol('b')), + true + ), + Assignment( + Variable(Symbol('c')), + false + ), + Assignment( + Variable(Symbol('d')), + false + ) + ) + ) + + assert res22[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ), + Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(2) + ) + ), + Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c5'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c6'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c7'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c8'), + type=Type(String('bool')) + ) + ), + Assignment( + Variable(Symbol('c1')), + Equality( + Symbol('a'), + Integer(1) + ) + ), + Assignment( + Variable(Symbol('c2')), + Equality( + Symbol('b'), + Integer(2) + ) + ), + Assignment( + Variable(Symbol('c3')), + Unequality( + Integer(1), + Symbol('a') + ) + ), + Assignment( + Variable(Symbol('c4')), + Unequality( + Integer(1), + Symbol('b') + ) + ), + Assignment( + Variable(Symbol('c5')), + StrictLessThan( + Symbol('a'), + Integer(0) + ) + ), + Assignment( + Variable(Symbol('c6')), + LessThan( + Symbol('b'), + Integer(10) + ) + ), + Assignment( + Variable(Symbol('c7')), + StrictGreaterThan( + Symbol('a'), + Integer(0) + ) + ), + Assignment( + Variable(Symbol('c8')), + GreaterThan( + Symbol('b'), + Integer(11) + ) + ) + ) + ) + + assert res23[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(3) + ) + ), + Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(4) + ) + ), + Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c5'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c6'), + type=Type(String('bool')) + ) + ), + Assignment( + Variable(Symbol('c1')), + Equality( + Symbol('a'), + Symbol('b') + ) + ), + Assignment( + Variable(Symbol('c2')), + Unequality( + Symbol('a'), + Symbol('b') + ) + ), + Assignment( + Variable(Symbol('c3')), + StrictLessThan( + Symbol('a'), + Symbol('b') + ) + ), + Assignment( + Variable(Symbol('c4')), + LessThan( + Symbol('a'), + Symbol('b') + ) + ), + Assignment( + Variable(Symbol('c5')), + StrictGreaterThan( + Symbol('a'), + Symbol('b') + ) + ), + Assignment( + Variable(Symbol('c6')), + GreaterThan( + Symbol('a'), + Symbol('b') + ) + ) + ) + ) + + assert res24[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ) + ), + Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')) + ) + ), + Assignment( + Variable(Symbol('c1')), + Equality( + Symbol('a'), + Float('1.25', precision=53) + ) + ), + Assignment( + Variable(Symbol('c3')), + Unequality( + Float('1.2', precision=53), + Symbol('a') + ) + ) + ) + ) + + + assert res25[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('1.25', precision=53) + ) + ), + Declaration( + Variable(Symbol('b'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('2.5', precision=53) + ) + ), + Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c2'), + type=Type(String('bool') + ) + ) + ), + Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c5'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c6'), + type=Type(String('bool')) + ) + ), + Assignment( + Variable(Symbol('c1')), + Equality( + Symbol('a'), + Symbol('b') + ) + ), + Assignment( + Variable(Symbol('c2')), + Unequality( + Symbol('a'), + Symbol('b') + ) + ), + Assignment( + Variable(Symbol('c3')), + StrictLessThan( + Symbol('a'), + Symbol('b') + ) + ), + Assignment( + Variable(Symbol('c4')), + LessThan( + Symbol('a'), + Symbol('b') + ) + ), + Assignment( + Variable(Symbol('c5')), + StrictGreaterThan( + Symbol('a'), + Symbol('b') + ) + ), + Assignment( + Variable(Symbol('c6')), + GreaterThan( + Symbol('a'), + Symbol('b') + ) + ) + ) + ) + + assert res26[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), body=CodeBlock( + Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c5'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c6'), + type=Type(String('bool')) + ) + ), + Assignment( + Variable(Symbol('c1')), + true + ), + Assignment( + Variable(Symbol('c2')), + false + ), + Assignment( + Variable(Symbol('c3')), + true + ), + Assignment( + Variable(Symbol('c4')), + false + ), + Assignment( + Variable(Symbol('c5')), + true + ), + Assignment( + Variable(Symbol('c6')), + false + ) + ) + ) + + assert res27[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c5'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c6'), + type=Type(String('bool')) + ) + ), + Assignment( + Variable(Symbol('c1')), + true + ), + Assignment( + Variable(Symbol('c2')), + false + ), + Assignment( + Variable(Symbol('c3')), + false + ), + Assignment( + Variable(Symbol('c4')), + true + ), + Assignment( + Variable(Symbol('c5')), + true + ), + Assignment( + Variable(Symbol('c6')), + false) + ) + ) + + assert res28[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')) + ) + ), + Assignment( + Variable(Symbol('c1')), + Symbol('a') + ), + Assignment( + Variable(Symbol('c2')), + false + ), + Assignment( + Variable(Symbol('c3')), + true + ), + Assignment( + Variable(Symbol('c4')), + Symbol('a') + ) + ) + ) + + assert res29[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')) + ) + ), + Assignment( + Variable(Symbol('c1')), + Symbol('a') + ), + Assignment( + Variable(Symbol('c2')), + false + ), + Assignment( + Variable(Symbol('c3')), + true + ), + Assignment( + Variable(Symbol('c4')), + Symbol('a') + ) + ) + ) + + assert res30[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')) + ) + ), + Declaration( + Variable(Symbol('c'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('d'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c5'), + type=Type(String('bool')) + ) + ), + Declaration( + Variable(Symbol('c6'), + type=Type(String('bool')) + ) + ), + Assignment( + Variable(Symbol('c1')), + And( + Symbol('a'), + Symbol('b') + ) + ), + Assignment( + Variable(Symbol('c2')), + And( + Symbol('a'), + Symbol('c') + ) + ), + Assignment( + Variable(Symbol('c3')), + And( + Symbol('c'), + Symbol('d') + ) + ), + Assignment( + Variable(Symbol('c4')), + Or( + Symbol('a'), + Symbol('b') + ) + ), + Assignment( + Variable(Symbol('c5')), + Or( + Symbol('a'), + Symbol('c') + ) + ), + Assignment( + Variable(Symbol('c6')), + Or( + Symbol('c'), + Symbol('d') + ) + ) + ) + ) + + raises(NotImplementedError, lambda: SymPyExpression(c_src_raise1, 'c')) + raises(NotImplementedError, lambda: SymPyExpression(c_src_raise2, 'c')) + raises(NotImplementedError, lambda: SymPyExpression(c_src_raise3, 'c')) + raises(NotImplementedError, lambda: SymPyExpression(c_src_raise4, 'c')) + raises(NotImplementedError, lambda: SymPyExpression(c_src_raise5, 'c')) + + + @XFAIL + def test_var_decl(): + c_src1 = ( + 'int b = 100;' + '\n' + + 'int a = b;' + '\n' + ) + + c_src2 = ( + 'int a = 1;' + '\n' + + 'int b = a + 1;' + '\n' + ) + + c_src3 = ( + 'float a = 10.0 + 2.5;' + '\n' + + 'float b = a * 20.0;' + '\n' + ) + + c_src4 = ( + 'int a = 1 + 100 - 3 * 6;' + '\n' + ) + + c_src5 = ( + 'int a = (((1 + 100) * 12) - 3) * (6 - 10);' + '\n' + ) + + c_src6 = ( + 'int b = 2;' + '\n' + + 'int c = 3;' + '\n' + + 'int a = b + c * 4;' + '\n' + ) + + c_src7 = ( + 'int b = 1;' + '\n' + + 'int c = b + 2;' + '\n' + + 'int a = 10 * b * b * c;' + '\n' + ) + + c_src8 = ( + 'void func()'+ + '{' + '\n' + + 'int a = 1;' + '\n' + + 'int b = 2;' + '\n' + + 'int temp = a;' + '\n' + + 'a = b;' + '\n' + + 'b = temp;' + '\n' + + '}' + ) + + c_src9 = ( + 'int a = 1;' + '\n' + + 'int b = 2;' + '\n' + + 'int c = a;' + '\n' + + 'int d = a + b + c;' + '\n' + + 'int e = a*a*a + 3*a*a*b + 3*a*b*b + b*b*b;' + '\n' + 'int f = (a + b + c) * (a + b - c);' + '\n' + + 'int g = (a + b + c + d)*(a + b + c + d)*(a * (b - c));' + + '\n' + ) + + c_src10 = ( + 'float a = 10.0;' + '\n' + + 'float b = 2.5;' + '\n' + + 'float c = a*a + 2*a*b + b*b;' + '\n' + ) + + c_src11 = ( + 'float a = 10.0 / 2.5;' + '\n' + ) + + c_src12 = ( + 'int a = 100 / 4;' + '\n' + ) + + c_src13 = ( + 'int a = 20 - 100 / 4 * 5 + 10;' + '\n' + ) + + c_src14 = ( + 'int a = (20 - 100) / 4 * (5 + 10);' + '\n' + ) + + c_src15 = ( + 'int a = 4;' + '\n' + + 'int b = 2;' + '\n' + + 'float c = b/a;' + '\n' + ) + + c_src16 = ( + 'int a = 2;' + '\n' + + 'int d = 5;' + '\n' + + 'int n = 10;' + '\n' + + 'int s = (a/2)*(2*a + (n-1)*d);' + '\n' + ) + + c_src17 = ( + 'int a = 1 % 2;' + '\n' + ) + + c_src18 = ( + 'int a = 2;' + '\n' + + 'int b = a % 3;' + '\n' + ) + + c_src19 = ( + 'int a = 100;' + '\n' + + 'int b = 3;' + '\n' + + 'int c = a % b;' + '\n' + ) + + c_src20 = ( + 'int a = 100;' + '\n' + + 'int b = 3;' + '\n' + + 'int mod = 1000000007;' + '\n' + + 'int c = (a + b * (100/a)) % mod;' + '\n' + ) + + c_src21 = ( + 'int a = 100;' + '\n' + + 'int b = 3;' + '\n' + + 'int mod = 1000000007;' + '\n' + + 'int c = ((a % mod + b % mod) % mod *' \ + '(a % mod - b % mod) % mod) % mod;' + '\n' + ) + + c_src22 = ( + 'bool a = 1 == 2, b = 1 != 2;' + ) + + c_src23 = ( + 'bool a = 1 < 2, b = 1 <= 2, c = 1 > 2, d = 1 >= 2;' + ) + + c_src24 = ( + 'int a = 1, b = 2;' + '\n' + + + 'bool c1 = a == 1;' + '\n' + + 'bool c2 = b == 2;' + '\n' + + + 'bool c3 = 1 != a;' + '\n' + + 'bool c4 = 1 != b;' + '\n' + + + 'bool c5 = a < 0;' + '\n' + + 'bool c6 = b <= 10;' + '\n' + + 'bool c7 = a > 0;' + '\n' + + 'bool c8 = b >= 11;' + + ) + + c_src25 = ( + 'int a = 3, b = 4;' + '\n' + + + 'bool c1 = a == b;' + '\n' + + 'bool c2 = a != b;' + '\n' + + 'bool c3 = a < b;' + '\n' + + 'bool c4 = a <= b;' + '\n' + + 'bool c5 = a > b;' + '\n' + + 'bool c6 = a >= b;' + ) + + c_src26 = ( + 'float a = 1.25, b = 2.5;' + '\n' + + + 'bool c1 = a == 1.25;' + '\n' + + 'bool c2 = b == 2.54;' + '\n' + + + 'bool c3 = 1.2 != a;' + '\n' + + 'bool c4 = 1.5 != b;' + ) + + c_src27 = ( + 'float a = 1.25, b = 2.5;' + '\n' + + + 'bool c1 = a == b;' + '\n' + + 'bool c2 = a != b;' + '\n' + + 'bool c3 = a < b;' + '\n' + + 'bool c4 = a <= b;' + '\n' + + 'bool c5 = a > b;' + '\n' + + 'bool c6 = a >= b;' + ) + + c_src28 = ( + 'bool c1 = true == true;' + '\n' + + 'bool c2 = true == false;' + '\n' + + 'bool c3 = false == false;' + '\n' + + + 'bool c4 = true != true;' + '\n' + + 'bool c5 = true != false;' + '\n' + + 'bool c6 = false != false;' + ) + + c_src29 = ( + 'bool c1 = true && true;' + '\n' + + 'bool c2 = true && false;' + '\n' + + 'bool c3 = false && false;' + '\n' + + + 'bool c4 = true || true;' + '\n' + + 'bool c5 = true || false;' + '\n' + + 'bool c6 = false || false;' + ) + + c_src30 = ( + 'bool a = false;' + '\n' + + + 'bool c1 = a && true;' + '\n' + + 'bool c2 = false && a;' + '\n' + + + 'bool c3 = true || a;' + '\n' + + 'bool c4 = a || false;' + ) + + c_src31 = ( + 'int a = 1;' + '\n' + + + 'bool c1 = a && 1;' + '\n' + + 'bool c2 = a && 0;' + '\n' + + + 'bool c3 = a || 1;' + '\n' + + 'bool c4 = 0 || a;' + ) + + c_src32 = ( + 'int a = 1, b = 0;' + '\n' + + 'bool c = false, d = true;'+ '\n' + + + 'bool c1 = a && b;' + '\n' + + 'bool c2 = a && c;' + '\n' + + 'bool c3 = c && d;' + '\n' + + + 'bool c4 = a || b;' + '\n' + + 'bool c5 = a || c;' + '\n' + + 'bool c6 = c || d;' + ) + + c_src_raise1 = ( + "char a = 'b';" + ) + + c_src_raise2 = ( + 'int a[] = {10, 20};' + ) + + res1 = SymPyExpression(c_src1, 'c').return_expr() + res2 = SymPyExpression(c_src2, 'c').return_expr() + res3 = SymPyExpression(c_src3, 'c').return_expr() + res4 = SymPyExpression(c_src4, 'c').return_expr() + res5 = SymPyExpression(c_src5, 'c').return_expr() + res6 = SymPyExpression(c_src6, 'c').return_expr() + res7 = SymPyExpression(c_src7, 'c').return_expr() + res8 = SymPyExpression(c_src8, 'c').return_expr() + res9 = SymPyExpression(c_src9, 'c').return_expr() + res10 = SymPyExpression(c_src10, 'c').return_expr() + res11 = SymPyExpression(c_src11, 'c').return_expr() + res12 = SymPyExpression(c_src12, 'c').return_expr() + res13 = SymPyExpression(c_src13, 'c').return_expr() + res14 = SymPyExpression(c_src14, 'c').return_expr() + res15 = SymPyExpression(c_src15, 'c').return_expr() + res16 = SymPyExpression(c_src16, 'c').return_expr() + res17 = SymPyExpression(c_src17, 'c').return_expr() + res18 = SymPyExpression(c_src18, 'c').return_expr() + res19 = SymPyExpression(c_src19, 'c').return_expr() + res20 = SymPyExpression(c_src20, 'c').return_expr() + res21 = SymPyExpression(c_src21, 'c').return_expr() + res22 = SymPyExpression(c_src22, 'c').return_expr() + res23 = SymPyExpression(c_src23, 'c').return_expr() + res24 = SymPyExpression(c_src24, 'c').return_expr() + res25 = SymPyExpression(c_src25, 'c').return_expr() + res26 = SymPyExpression(c_src26, 'c').return_expr() + res27 = SymPyExpression(c_src27, 'c').return_expr() + res28 = SymPyExpression(c_src28, 'c').return_expr() + res29 = SymPyExpression(c_src29, 'c').return_expr() + res30 = SymPyExpression(c_src30, 'c').return_expr() + res31 = SymPyExpression(c_src31, 'c').return_expr() + res32 = SymPyExpression(c_src32, 'c').return_expr() + + assert res1[0] == Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(100) + ) + ) + + assert res1[1] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Symbol('b') + ) + ) + + assert res2[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res2[1] == Declaration(Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Add( + Symbol('a'), + Integer(1) + ) + ) + ) + + assert res3[0] == Declaration( + Variable(Symbol('a'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('12.5', precision=53) + ) + ) + + assert res3[1] == Declaration( + Variable(Symbol('b'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Mul( + Float('20.0', precision=53), + Symbol('a') + ) + ) + ) + + assert res4[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(83) + ) + ) + + assert res5[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(-4836) + ) + ) + + assert res6[0] == Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(2) + ) + ) + + assert res6[1] == Declaration( + Variable(Symbol('c'), + type=IntBaseType(String('intc')), + value=Integer(3) + ) + ) + + assert res6[2] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Add( + Symbol('b'), + Mul( + Integer(4), + Symbol('c') + ) + ) + ) + ) + + assert res7[0] == Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res7[1] == Declaration( + Variable(Symbol('c'), + type=IntBaseType(String('intc')), + value=Add( + Symbol('b'), + Integer(2) + ) + ) + ) + + assert res7[2] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Mul( + Integer(10), + Pow( + Symbol('b'), + Integer(2) + ), + Symbol('c') + ) + ) + ) + + assert res8[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ), + Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(2) + ) + ), + Declaration( + Variable(Symbol('temp'), + type=IntBaseType(String('intc')), + value=Symbol('a') + ) + ), + Assignment( + Variable(Symbol('a')), + Symbol('b') + ), + Assignment( + Variable(Symbol('b')), + Symbol('temp') + ) + ) + ) + + assert res9[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res9[1] == Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(2) + ) + ) + + assert res9[2] == Declaration( + Variable(Symbol('c'), + type=IntBaseType(String('intc')), + value=Symbol('a') + ) + ) + + assert res9[3] == Declaration( + Variable(Symbol('d'), + type=IntBaseType(String('intc')), + value=Add( + Symbol('a'), + Symbol('b'), + Symbol('c') + ) + ) + ) + + assert res9[4] == Declaration( + Variable(Symbol('e'), + type=IntBaseType(String('intc')), + value=Add( + Pow( + Symbol('a'), + Integer(3) + ), + Mul( + Integer(3), + Pow( + Symbol('a'), + Integer(2) + ), + Symbol('b') + ), + Mul( + Integer(3), + Symbol('a'), + Pow( + Symbol('b'), + Integer(2) + ) + ), + Pow( + Symbol('b'), + Integer(3) + ) + ) + ) + ) + + assert res9[5] == Declaration( + Variable(Symbol('f'), + type=IntBaseType(String('intc')), + value=Mul( + Add( + Symbol('a'), + Symbol('b'), + Mul( + Integer(-1), + Symbol('c') + ) + ), + Add( + Symbol('a'), + Symbol('b'), + Symbol('c') + ) + ) + ) + ) + + assert res9[6] == Declaration( + Variable(Symbol('g'), + type=IntBaseType(String('intc')), + value=Mul( + Symbol('a'), + Add( + Symbol('b'), + Mul( + Integer(-1), + Symbol('c') + ) + ), + Pow( + Add( + Symbol('a'), + Symbol('b'), + Symbol('c'), + Symbol('d') + ), + Integer(2) + ) + ) + ) + ) + + assert res10[0] == Declaration( + Variable(Symbol('a'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('10.0', precision=53) + ) + ) + + assert res10[1] == Declaration( + Variable(Symbol('b'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('2.5', precision=53) + ) + ) + + assert res10[2] == Declaration( + Variable(Symbol('c'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Add( + Pow( + Symbol('a'), + Integer(2) + ), + Mul( + Integer(2), + Symbol('a'), + Symbol('b') + ), + Pow( + Symbol('b'), + Integer(2) + ) + ) + ) + ) + + assert res11[0] == Declaration( + Variable(Symbol('a'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('4.0', precision=53) + ) + ) + + assert res12[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(25) + ) + ) + + assert res13[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(-95) + ) + ) + + assert res14[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(-300) + ) + ) + + assert res15[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(4) + ) + ) + + assert res15[1] == Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(2) + ) + ) + + assert res15[2] == Declaration( + Variable(Symbol('c'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Mul( + Pow( + Symbol('a'), + Integer(-1) + ), + Symbol('b') + ) + ) + ) + + assert res16[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(2) + ) + ) + + assert res16[1] == Declaration( + Variable(Symbol('d'), + type=IntBaseType(String('intc')), + value=Integer(5) + ) + ) + + assert res16[2] == Declaration( + Variable(Symbol('n'), + type=IntBaseType(String('intc')), + value=Integer(10) + ) + ) + + assert res16[3] == Declaration( + Variable(Symbol('s'), + type=IntBaseType(String('intc')), + value=Mul( + Rational(1, 2), + Symbol('a'), + Add( + Mul( + Integer(2), + Symbol('a') + ), + Mul( + Symbol('d'), + Add( + Symbol('n'), + Integer(-1) + ) + ) + ) + ) + ) + ) + + assert res17[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res18[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(2) + ) + ) + + assert res18[1] == Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Mod( + Symbol('a'), + Integer(3) + ) + ) + ) + + assert res19[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(100) + ) + ) + assert res19[1] == Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(3) + ) + ) + + assert res19[2] == Declaration( + Variable(Symbol('c'), + type=IntBaseType(String('intc')), + value=Mod( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res20[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(100) + ) + ) + + assert res20[1] == Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(3) + ) + ) + + assert res20[2] == Declaration( + Variable(Symbol('mod'), + type=IntBaseType(String('intc')), + value=Integer(1000000007) + ) + ) + + assert res20[3] == Declaration( + Variable(Symbol('c'), + type=IntBaseType(String('intc')), + value=Mod( + Add( + Symbol('a'), + Mul( + Integer(100), + Pow( + Symbol('a'), + Integer(-1) + ), + Symbol('b') + ) + ), + Symbol('mod') + ) + ) + ) + + assert res21[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(100) + ) + ) + + assert res21[1] == Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(3) + ) + ) + + assert res21[2] == Declaration( + Variable(Symbol('mod'), + type=IntBaseType(String('intc')), + value=Integer(1000000007) + ) + ) + + assert res21[3] == Declaration( + Variable(Symbol('c'), + type=IntBaseType(String('intc')), + value=Mod( + Mul( + Add( + Symbol('a'), + Mul( + Integer(-1), + Symbol('b') + ) + ), + Add( + Symbol('a'), + Symbol('b') + ) + ), + Symbol('mod') + ) + ) + ) + + assert res22[0] == Declaration( + Variable(Symbol('a'), + type=Type(String('bool')), + value=false + ) + ) + + assert res22[1] == Declaration( + Variable(Symbol('b'), + type=Type(String('bool')), + value=true + ) + ) + + assert res23[0] == Declaration( + Variable(Symbol('a'), + type=Type(String('bool')), + value=true + ) + ) + + assert res23[1] == Declaration( + Variable(Symbol('b'), + type=Type(String('bool')), + value=true + ) + ) + + assert res23[2] == Declaration( + Variable(Symbol('c'), + type=Type(String('bool')), + value=false + ) + ) + + assert res23[3] == Declaration( + Variable(Symbol('d'), + type=Type(String('bool')), + value=false + ) + ) + + assert res24[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res24[1] == Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(2) + ) + ) + + assert res24[2] == Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')), + value=Equality( + Symbol('a'), + Integer(1) + ) + ) + ) + + assert res24[3] == Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')), + value=Equality( + Symbol('b'), + Integer(2) + ) + ) + ) + + assert res24[4] == Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')), + value=Unequality( + Integer(1), + Symbol('a') + ) + ) + ) + + assert res24[5] == Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')), + value=Unequality( + Integer(1), + Symbol('b') + ) + ) + ) + + assert res24[6] == Declaration( + Variable(Symbol('c5'), + type=Type(String('bool')), + value=StrictLessThan(Symbol('a'), + Integer(0) + ) + ) + ) + + assert res24[7] == Declaration( + Variable(Symbol('c6'), + type=Type(String('bool')), + value=LessThan( + Symbol('b'), + Integer(10) + ) + ) + ) + + assert res24[8] == Declaration( + Variable(Symbol('c7'), + type=Type(String('bool')), + value=StrictGreaterThan( + Symbol('a'), + Integer(0) + ) + ) + ) + + assert res24[9] == Declaration( + Variable(Symbol('c8'), + type=Type(String('bool')), + value=GreaterThan( + Symbol('b'), + Integer(11) + ) + ) + ) + + assert res25[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(3) + ) + ) + + assert res25[1] == Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(4) + ) + ) + + assert res25[2] == Declaration(Variable(Symbol('c1'), + type=Type(String('bool')), + value=Equality( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res25[3] == Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')), + value=Unequality( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res25[4] == Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')), + value=StrictLessThan( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res25[5] == Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')), + value=LessThan( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res25[6] == Declaration( + Variable(Symbol('c5'), + type=Type(String('bool')), + value=StrictGreaterThan( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res25[7] == Declaration( + Variable(Symbol('c6'), + type=Type(String('bool')), + value=GreaterThan( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res26[0] == Declaration( + Variable(Symbol('a'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('1.25', precision=53) + ) + ) + + assert res26[1] == Declaration( + Variable(Symbol('b'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('2.5', precision=53) + ) + ) + + assert res26[2] == Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')), + value=Equality( + Symbol('a'), + Float('1.25', precision=53) + ) + ) + ) + + assert res26[3] == Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')), + value=Equality( + Symbol('b'), + Float('2.54', precision=53) + ) + ) + ) + + assert res26[4] == Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')), + value=Unequality( + Float('1.2', precision=53), + Symbol('a') + ) + ) + ) + + assert res26[5] == Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')), + value=Unequality( + Float('1.5', precision=53), + Symbol('b') + ) + ) + ) + + assert res27[0] == Declaration( + Variable(Symbol('a'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('1.25', precision=53) + ) + ) + + assert res27[1] == Declaration( + Variable(Symbol('b'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('2.5', precision=53) + ) + ) + + assert res27[2] == Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')), + value=Equality( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res27[3] == Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')), + value=Unequality( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res27[4] == Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')), + value=StrictLessThan( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res27[5] == Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')), + value=LessThan( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res27[6] == Declaration( + Variable(Symbol('c5'), + type=Type(String('bool')), + value=StrictGreaterThan( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res27[7] == Declaration( + Variable(Symbol('c6'), + type=Type(String('bool')), + value=GreaterThan( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res28[0] == Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')), + value=true + ) + ) + + assert res28[1] == Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')), + value=false + ) + ) + + assert res28[2] == Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')), + value=true + ) + ) + + assert res28[3] == Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')), + value=false + ) + ) + + assert res28[4] == Declaration( + Variable(Symbol('c5'), + type=Type(String('bool')), + value=true + ) + ) + + assert res28[5] == Declaration( + Variable(Symbol('c6'), + type=Type(String('bool')), + value=false + ) + ) + + assert res29[0] == Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')), + value=true + ) + ) + + assert res29[1] == Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')), + value=false + ) + ) + + assert res29[2] == Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')), + value=false + ) + ) + + assert res29[3] == Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')), + value=true + ) + ) + + assert res29[4] == Declaration( + Variable(Symbol('c5'), + type=Type(String('bool')), + value=true + ) + ) + + assert res29[5] == Declaration( + Variable(Symbol('c6'), + type=Type(String('bool')), + value=false + ) + ) + + assert res30[0] == Declaration( + Variable(Symbol('a'), + type=Type(String('bool')), + value=false + ) + ) + + assert res30[1] == Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')), + value=Symbol('a') + ) + ) + + assert res30[2] == Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')), + value=false + ) + ) + + assert res30[3] == Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')), + value=true + ) + ) + + assert res30[4] == Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')), + value=Symbol('a') + ) + ) + + assert res31[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res31[1] == Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')), + value=Symbol('a') + ) + ) + + assert res31[2] == Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')), + value=false + ) + ) + + assert res31[3] == Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')), + value=true + ) + ) + + assert res31[4] == Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')), + value=Symbol('a') + ) + ) + + assert res32[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res32[1] == Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(0) + ) + ) + + assert res32[2] == Declaration( + Variable(Symbol('c'), + type=Type(String('bool')), + value=false + ) + ) + + assert res32[3] == Declaration( + Variable(Symbol('d'), + type=Type(String('bool')), + value=true + ) + ) + + assert res32[4] == Declaration( + Variable(Symbol('c1'), + type=Type(String('bool')), + value=And( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res32[5] == Declaration( + Variable(Symbol('c2'), + type=Type(String('bool')), + value=And( + Symbol('a'), + Symbol('c') + ) + ) + ) + + assert res32[6] == Declaration( + Variable(Symbol('c3'), + type=Type(String('bool')), + value=And( + Symbol('c'), + Symbol('d') + ) + ) + ) + + assert res32[7] == Declaration( + Variable(Symbol('c4'), + type=Type(String('bool')), + value=Or( + Symbol('a'), + Symbol('b') + ) + ) + ) + + assert res32[8] == Declaration( + Variable(Symbol('c5'), + type=Type(String('bool')), + value=Or( + Symbol('a'), + Symbol('c') + ) + ) + ) + + assert res32[9] == Declaration( + Variable(Symbol('c6'), + type=Type(String('bool')), + value=Or( + Symbol('c'), + Symbol('d') + ) + ) + ) + + raises(NotImplementedError, lambda: SymPyExpression(c_src_raise1, 'c')) + raises(NotImplementedError, lambda: SymPyExpression(c_src_raise2, 'c')) + + + def test_paren_expr(): + c_src1 = ( + 'int a = (1);' + 'int b = (1 + 2 * 3);' + ) + + c_src2 = ( + 'int a = 1, b = 2, c = 3;' + 'int d = (a);' + 'int e = (a + 1);' + 'int f = (a + b * c - d / e);' + ) + + res1 = SymPyExpression(c_src1, 'c').return_expr() + res2 = SymPyExpression(c_src2, 'c').return_expr() + + assert res1[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res1[1] == Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(7) + ) + ) + + assert res2[0] == Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(1) + ) + ) + + assert res2[1] == Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(2) + ) + ) + + assert res2[2] == Declaration( + Variable(Symbol('c'), + type=IntBaseType(String('intc')), + value=Integer(3) + ) + ) + + assert res2[3] == Declaration( + Variable(Symbol('d'), + type=IntBaseType(String('intc')), + value=Symbol('a') + ) + ) + + assert res2[4] == Declaration( + Variable(Symbol('e'), + type=IntBaseType(String('intc')), + value=Add( + Symbol('a'), + Integer(1) + ) + ) + ) + + assert res2[5] == Declaration( + Variable(Symbol('f'), + type=IntBaseType(String('intc')), + value=Add( + Symbol('a'), + Mul( + Symbol('b'), + Symbol('c') + ), + Mul( + Integer(-1), + Symbol('d'), + Pow( + Symbol('e'), + Integer(-1) + ) + ) + ) + ) + ) + + + def test_unary_operators(): + c_src1 = ( + 'void func()'+ + '{' + '\n' + + 'int a = 10;' + '\n' + + 'int b = 20;' + '\n' + + '++a;' + '\n' + + '--b;' + '\n' + + 'a++;' + '\n' + + 'b--;' + '\n' + + '}' + ) + + c_src2 = ( + 'void func()'+ + '{' + '\n' + + 'int a = 10;' + '\n' + + 'int b = -100;' + '\n' + + 'int c = +19;' + '\n' + + 'int d = ++a;' + '\n' + + 'int e = --b;' + '\n' + + 'int f = a++;' + '\n' + + 'int g = b--;' + '\n' + + 'bool h = !false;' + '\n' + + 'bool i = !d;' + '\n' + + 'bool j = !0;' + '\n' + + 'bool k = !10.0;' + '\n' + + '}' + ) + + c_src_raise1 = ( + 'void func()'+ + '{' + '\n' + + 'int a = 10;' + '\n' + + 'int b = ~a;' + '\n' + + '}' + ) + + c_src_raise2 = ( + 'void func()'+ + '{' + '\n' + + 'int a = 10;' + '\n' + + 'int b = *&a;' + '\n' + + '}' + ) + + res1 = SymPyExpression(c_src1, 'c').return_expr() + res2 = SymPyExpression(c_src2, 'c').return_expr() + + assert res1[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(10) + ) + ), + Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(20) + ) + ), + PreIncrement(Symbol('a')), + PreDecrement(Symbol('b')), + PostIncrement(Symbol('a')), + PostDecrement(Symbol('b')) + ) + ) + + assert res2[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(10) + ) + ), + Declaration( + Variable(Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(-100) + ) + ), + Declaration( + Variable(Symbol('c'), + type=IntBaseType(String('intc')), + value=Integer(19) + ) + ), + Declaration( + Variable(Symbol('d'), + type=IntBaseType(String('intc')), + value=PreIncrement(Symbol('a')) + ) + ), + Declaration( + Variable(Symbol('e'), + type=IntBaseType(String('intc')), + value=PreDecrement(Symbol('b')) + ) + ), + Declaration( + Variable(Symbol('f'), + type=IntBaseType(String('intc')), + value=PostIncrement(Symbol('a')) + ) + ), + Declaration( + Variable(Symbol('g'), + type=IntBaseType(String('intc')), + value=PostDecrement(Symbol('b')) + ) + ), + Declaration( + Variable(Symbol('h'), + type=Type(String('bool')), + value=true + ) + ), + Declaration( + Variable(Symbol('i'), + type=Type(String('bool')), + value=Not(Symbol('d')) + ) + ), + Declaration( + Variable(Symbol('j'), + type=Type(String('bool')), + value=true + ) + ), + Declaration( + Variable(Symbol('k'), + type=Type(String('bool')), + value=false + ) + ) + ) + ) + + raises(NotImplementedError, lambda: SymPyExpression(c_src_raise1, 'c')) + raises(NotImplementedError, lambda: SymPyExpression(c_src_raise2, 'c')) + + + def test_compound_assignment_operator(): + c_src = ( + 'void func()'+ + '{' + '\n' + + 'int a = 100;' + '\n' + + 'a += 10;' + '\n' + + 'a -= 10;' + '\n' + + 'a *= 10;' + '\n' + + 'a /= 10;' + '\n' + + 'a %= 10;' + '\n' + + '}' + ) + + res = SymPyExpression(c_src, 'c').return_expr() + + assert res[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('intc')), + value=Integer(100) + ) + ), + AddAugmentedAssignment( + Variable(Symbol('a')), + Integer(10) + ), + SubAugmentedAssignment( + Variable(Symbol('a')), + Integer(10) + ), + MulAugmentedAssignment( + Variable(Symbol('a')), + Integer(10) + ), + DivAugmentedAssignment( + Variable(Symbol('a')), + Integer(10) + ), + ModAugmentedAssignment( + Variable(Symbol('a')), + Integer(10) + ) + ) + ) + + @XFAIL # this is expected to fail because of a bug in the C parser. + def test_while_stmt(): + c_src1 = ( + 'void func()'+ + '{' + '\n' + + 'int i = 0;' + '\n' + + 'while(i < 10)' + '\n' + + '{' + '\n' + + 'i++;' + '\n' + + '}' + '}' + ) + + c_src2 = ( + 'void func()'+ + '{' + '\n' + + 'int i = 0;' + '\n' + + 'while(i < 10)' + '\n' + + 'i++;' + '\n' + + '}' + ) + + c_src3 = ( + 'void func()'+ + '{' + '\n' + + 'int i = 10;' + '\n' + + 'int cnt = 0;' + '\n' + + 'while(i > 0)' + '\n' + + '{' + '\n' + + 'i--;' + '\n' + + 'cnt++;' + '\n' + + '}' + '\n' + + '}' + ) + + c_src4 = ( + 'int digit_sum(int n)'+ + '{' + '\n' + + 'int sum = 0;' + '\n' + + 'while(n > 0)' + '\n' + + '{' + '\n' + + 'sum += (n % 10);' + '\n' + + 'n /= 10;' + '\n' + + '}' + '\n' + + 'return sum;' + '\n' + + '}' + ) + + c_src5 = ( + 'void func()'+ + '{' + '\n' + + 'while(1);' + '\n' + + '}' + ) + + res1 = SymPyExpression(c_src1, 'c').return_expr() + res2 = SymPyExpression(c_src2, 'c').return_expr() + res3 = SymPyExpression(c_src3, 'c').return_expr() + res4 = SymPyExpression(c_src4, 'c').return_expr() + res5 = SymPyExpression(c_src5, 'c').return_expr() + + assert res1[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable(Symbol('i'), + type=IntBaseType(String('intc')), + value=Integer(0) + ) + ), + While( + StrictLessThan( + Symbol('i'), + Integer(10) + ), + body=CodeBlock( + PostIncrement( + Symbol('i') + ) + ) + ) + ) + ) + + assert res2[0] == res1[0] + + assert res3[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + Declaration( + Variable( + Symbol('i'), + type=IntBaseType(String('intc')), + value=Integer(10) + ) + ), + Declaration( + Variable( + Symbol('cnt'), + type=IntBaseType(String('intc')), + value=Integer(0) + ) + ), + While( + StrictGreaterThan( + Symbol('i'), + Integer(0) + ), + body=CodeBlock( + PostDecrement( + Symbol('i') + ), + PostIncrement( + Symbol('cnt') + ) + ) + ) + ) + ) + + assert res4[0] == FunctionDefinition( + IntBaseType(String('intc')), + name=String('digit_sum'), + parameters=( + Variable( + Symbol('n'), + type=IntBaseType(String('intc')) + ), + ), + body=CodeBlock( + Declaration( + Variable( + Symbol('sum'), + type=IntBaseType(String('intc')), + value=Integer(0) + ) + ), + While( + StrictGreaterThan( + Symbol('n'), + Integer(0) + ), + body=CodeBlock( + AddAugmentedAssignment( + Variable( + Symbol('sum') + ), + Mod( + Symbol('n'), + Integer(10) + ) + ), + DivAugmentedAssignment( + Variable( + Symbol('n') + ), + Integer(10) + ) + ) + ), + Return('sum') + ) + ) + + assert res5[0] == FunctionDefinition( + NoneToken(), + name=String('func'), + parameters=(), + body=CodeBlock( + While( + Integer(1), + body=CodeBlock( + NoneToken() + ) + ) + ) + ) + + +else: + def test_raise(): + from sympy.parsing.c.c_parser import CCodeConverter + raises(ImportError, lambda: CCodeConverter()) + raises(ImportError, lambda: SymPyExpression(' ', mode = 'c')) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_custom_latex.py b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_custom_latex.py new file mode 100644 index 0000000000000000000000000000000000000000..f5eff1c9ec79528c7f9e3a06cf9e2f84c86091ee --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_custom_latex.py @@ -0,0 +1,69 @@ +import os +import tempfile +from pathlib import Path + +import sympy +from sympy.testing.pytest import raises +from sympy.parsing.latex.lark import LarkLaTeXParser, TransformToSymPyExpr, parse_latex_lark +from sympy.external import import_module + +lark = import_module("lark") + +# disable tests if lark is not present +disabled = lark is None + +grammar_file = os.path.join(os.path.dirname(__file__), "../latex/lark/grammar/latex.lark") + +modification1 = """ +%override DIV_SYMBOL: DIV +%override MUL_SYMBOL: MUL | CMD_TIMES +""" + +modification2 = r""" +%override number: /\d+(,\d*)?/ +""" + +def init_custom_parser(modification, transformer=None): + latex_grammar = Path(grammar_file).read_text(encoding="utf-8") + latex_grammar += modification + + with tempfile.NamedTemporaryFile() as f: + f.write(bytes(latex_grammar, encoding="utf8")) + f.flush() + + parser = LarkLaTeXParser(grammar_file=f.name, transformer=transformer) + + return parser + +def test_custom1(): + # Removes the parser's ability to understand \cdot and \div. + + parser = init_custom_parser(modification1) + + with raises(lark.exceptions.UnexpectedCharacters): + parser.doparse(r"a \cdot b") + parser.doparse(r"x \div y") + +class CustomTransformer(TransformToSymPyExpr): + def number(self, tokens): + if "," in tokens[0]: + # The Float constructor expects a dot as the decimal separator + return sympy.core.numbers.Float(tokens[0].replace(",", ".")) + else: + return sympy.core.numbers.Integer(tokens[0]) + +def test_custom2(): + # Makes the parser parse commas as the decimal separator instead of dots + + parser = init_custom_parser(modification2, CustomTransformer) + + with raises(lark.exceptions.UnexpectedCharacters): + # Asserting that the default parser cannot parse numbers which have commas as + # the decimal separator + parse_latex_lark("100,1") + parse_latex_lark("0,009") + + parser.doparse("100,1") + parser.doparse("0,009") + parser.doparse("2,71828") + parser.doparse("3,14159") diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_fortran_parser.py b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_fortran_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..9bcd54533ef231dd0a116910453dff0e993bc727 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_fortran_parser.py @@ -0,0 +1,406 @@ +from sympy.testing.pytest import raises +from sympy.parsing.sym_expr import SymPyExpression +from sympy.external import import_module + +lfortran = import_module('lfortran') + +if lfortran: + from sympy.codegen.ast import (Variable, IntBaseType, FloatBaseType, String, + Return, FunctionDefinition, Assignment, + Declaration, CodeBlock) + from sympy.core import Integer, Float, Add + from sympy.core.symbol import Symbol + + + expr1 = SymPyExpression() + expr2 = SymPyExpression() + src = """\ + integer :: a, b, c, d + real :: p, q, r, s + """ + + + def test_sym_expr(): + src1 = ( + src + + """\ + d = a + b -c + """ + ) + expr3 = SymPyExpression(src,'f') + expr4 = SymPyExpression(src1,'f') + ls1 = expr3.return_expr() + ls2 = expr4.return_expr() + for i in range(0, 7): + assert isinstance(ls1[i], Declaration) + assert isinstance(ls2[i], Declaration) + assert isinstance(ls2[8], Assignment) + assert ls1[0] == Declaration( + Variable( + Symbol('a'), + type = IntBaseType(String('integer')), + value = Integer(0) + ) + ) + assert ls1[1] == Declaration( + Variable( + Symbol('b'), + type = IntBaseType(String('integer')), + value = Integer(0) + ) + ) + assert ls1[2] == Declaration( + Variable( + Symbol('c'), + type = IntBaseType(String('integer')), + value = Integer(0) + ) + ) + assert ls1[3] == Declaration( + Variable( + Symbol('d'), + type = IntBaseType(String('integer')), + value = Integer(0) + ) + ) + assert ls1[4] == Declaration( + Variable( + Symbol('p'), + type = FloatBaseType(String('real')), + value = Float(0.0) + ) + ) + assert ls1[5] == Declaration( + Variable( + Symbol('q'), + type = FloatBaseType(String('real')), + value = Float(0.0) + ) + ) + assert ls1[6] == Declaration( + Variable( + Symbol('r'), + type = FloatBaseType(String('real')), + value = Float(0.0) + ) + ) + assert ls1[7] == Declaration( + Variable( + Symbol('s'), + type = FloatBaseType(String('real')), + value = Float(0.0) + ) + ) + assert ls2[8] == Assignment( + Variable(Symbol('d')), + Symbol('a') + Symbol('b') - Symbol('c') + ) + + def test_assignment(): + src1 = ( + src + + """\ + a = b + c = d + p = q + r = s + """ + ) + expr1.convert_to_expr(src1, 'f') + ls1 = expr1.return_expr() + for iter in range(0, 12): + if iter < 8: + assert isinstance(ls1[iter], Declaration) + else: + assert isinstance(ls1[iter], Assignment) + assert ls1[8] == Assignment( + Variable(Symbol('a')), + Variable(Symbol('b')) + ) + assert ls1[9] == Assignment( + Variable(Symbol('c')), + Variable(Symbol('d')) + ) + assert ls1[10] == Assignment( + Variable(Symbol('p')), + Variable(Symbol('q')) + ) + assert ls1[11] == Assignment( + Variable(Symbol('r')), + Variable(Symbol('s')) + ) + + + def test_binop_add(): + src1 = ( + src + + """\ + c = a + b + d = a + c + s = p + q + r + """ + ) + expr1.convert_to_expr(src1, 'f') + ls1 = expr1.return_expr() + for iter in range(8, 11): + assert isinstance(ls1[iter], Assignment) + assert ls1[8] == Assignment( + Variable(Symbol('c')), + Symbol('a') + Symbol('b') + ) + assert ls1[9] == Assignment( + Variable(Symbol('d')), + Symbol('a') + Symbol('c') + ) + assert ls1[10] == Assignment( + Variable(Symbol('s')), + Symbol('p') + Symbol('q') + Symbol('r') + ) + + + def test_binop_sub(): + src1 = ( + src + + """\ + c = a - b + d = a - c + s = p - q - r + """ + ) + expr1.convert_to_expr(src1, 'f') + ls1 = expr1.return_expr() + for iter in range(8, 11): + assert isinstance(ls1[iter], Assignment) + assert ls1[8] == Assignment( + Variable(Symbol('c')), + Symbol('a') - Symbol('b') + ) + assert ls1[9] == Assignment( + Variable(Symbol('d')), + Symbol('a') - Symbol('c') + ) + assert ls1[10] == Assignment( + Variable(Symbol('s')), + Symbol('p') - Symbol('q') - Symbol('r') + ) + + + def test_binop_mul(): + src1 = ( + src + + """\ + c = a * b + d = a * c + s = p * q * r + """ + ) + expr1.convert_to_expr(src1, 'f') + ls1 = expr1.return_expr() + for iter in range(8, 11): + assert isinstance(ls1[iter], Assignment) + assert ls1[8] == Assignment( + Variable(Symbol('c')), + Symbol('a') * Symbol('b') + ) + assert ls1[9] == Assignment( + Variable(Symbol('d')), + Symbol('a') * Symbol('c') + ) + assert ls1[10] == Assignment( + Variable(Symbol('s')), + Symbol('p') * Symbol('q') * Symbol('r') + ) + + + def test_binop_div(): + src1 = ( + src + + """\ + c = a / b + d = a / c + s = p / q + r = q / p + """ + ) + expr1.convert_to_expr(src1, 'f') + ls1 = expr1.return_expr() + for iter in range(8, 12): + assert isinstance(ls1[iter], Assignment) + assert ls1[8] == Assignment( + Variable(Symbol('c')), + Symbol('a') / Symbol('b') + ) + assert ls1[9] == Assignment( + Variable(Symbol('d')), + Symbol('a') / Symbol('c') + ) + assert ls1[10] == Assignment( + Variable(Symbol('s')), + Symbol('p') / Symbol('q') + ) + assert ls1[11] == Assignment( + Variable(Symbol('r')), + Symbol('q') / Symbol('p') + ) + + def test_mul_binop(): + src1 = ( + src + + """\ + d = a + b - c + c = a * b + d + s = p * q / r + r = p * s + q / p + """ + ) + expr1.convert_to_expr(src1, 'f') + ls1 = expr1.return_expr() + for iter in range(8, 12): + assert isinstance(ls1[iter], Assignment) + assert ls1[8] == Assignment( + Variable(Symbol('d')), + Symbol('a') + Symbol('b') - Symbol('c') + ) + assert ls1[9] == Assignment( + Variable(Symbol('c')), + Symbol('a') * Symbol('b') + Symbol('d') + ) + assert ls1[10] == Assignment( + Variable(Symbol('s')), + Symbol('p') * Symbol('q') / Symbol('r') + ) + assert ls1[11] == Assignment( + Variable(Symbol('r')), + Symbol('p') * Symbol('s') + Symbol('q') / Symbol('p') + ) + + + def test_function(): + src1 = """\ + integer function f(a,b) + integer :: x, y + f = x + y + end function + """ + expr1.convert_to_expr(src1, 'f') + for iter in expr1.return_expr(): + assert isinstance(iter, FunctionDefinition) + assert iter == FunctionDefinition( + IntBaseType(String('integer')), + name=String('f'), + parameters=( + Variable(Symbol('a')), + Variable(Symbol('b')) + ), + body=CodeBlock( + Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('integer')), + value=Integer(0) + ) + ), + Declaration( + Variable( + Symbol('b'), + type=IntBaseType(String('integer')), + value=Integer(0) + ) + ), + Declaration( + Variable( + Symbol('f'), + type=IntBaseType(String('integer')), + value=Integer(0) + ) + ), + Declaration( + Variable( + Symbol('x'), + type=IntBaseType(String('integer')), + value=Integer(0) + ) + ), + Declaration( + Variable( + Symbol('y'), + type=IntBaseType(String('integer')), + value=Integer(0) + ) + ), + Assignment( + Variable(Symbol('f')), + Add(Symbol('x'), Symbol('y')) + ), + Return(Variable(Symbol('f'))) + ) + ) + + + def test_var(): + expr1.convert_to_expr(src, 'f') + ls = expr1.return_expr() + for iter in expr1.return_expr(): + assert isinstance(iter, Declaration) + assert ls[0] == Declaration( + Variable( + Symbol('a'), + type = IntBaseType(String('integer')), + value = Integer(0) + ) + ) + assert ls[1] == Declaration( + Variable( + Symbol('b'), + type = IntBaseType(String('integer')), + value = Integer(0) + ) + ) + assert ls[2] == Declaration( + Variable( + Symbol('c'), + type = IntBaseType(String('integer')), + value = Integer(0) + ) + ) + assert ls[3] == Declaration( + Variable( + Symbol('d'), + type = IntBaseType(String('integer')), + value = Integer(0) + ) + ) + assert ls[4] == Declaration( + Variable( + Symbol('p'), + type = FloatBaseType(String('real')), + value = Float(0.0) + ) + ) + assert ls[5] == Declaration( + Variable( + Symbol('q'), + type = FloatBaseType(String('real')), + value = Float(0.0) + ) + ) + assert ls[6] == Declaration( + Variable( + Symbol('r'), + type = FloatBaseType(String('real')), + value = Float(0.0) + ) + ) + assert ls[7] == Declaration( + Variable( + Symbol('s'), + type = FloatBaseType(String('real')), + value = Float(0.0) + ) + ) + +else: + def test_raise(): + from sympy.parsing.fortran.fortran_parser import ASR2PyVisitor + raises(ImportError, lambda: ASR2PyVisitor()) + raises(ImportError, lambda: SymPyExpression(' ', mode = 'f')) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_implicit_multiplication_application.py b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_implicit_multiplication_application.py new file mode 100644 index 0000000000000000000000000000000000000000..56df361e77b0c0f94bdb53b03e0dc30a8a10899f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_implicit_multiplication_application.py @@ -0,0 +1,195 @@ +import sympy +from sympy.parsing.sympy_parser import ( + parse_expr, + standard_transformations, + convert_xor, + implicit_multiplication_application, + implicit_multiplication, + implicit_application, + function_exponentiation, + split_symbols, + split_symbols_custom, + _token_splittable +) +from sympy.testing.pytest import raises + + +def test_implicit_multiplication(): + cases = { + '5x': '5*x', + 'abc': 'a*b*c', + '3sin(x)': '3*sin(x)', + '(x+1)(x+2)': '(x+1)*(x+2)', + '(5 x**2)sin(x)': '(5*x**2)*sin(x)', + '2 sin(x) cos(x)': '2*sin(x)*cos(x)', + 'pi x': 'pi*x', + 'x pi': 'x*pi', + 'E x': 'E*x', + 'EulerGamma y': 'EulerGamma*y', + 'E pi': 'E*pi', + 'pi (x + 2)': 'pi*(x+2)', + '(x + 2) pi': '(x+2)*pi', + 'pi sin(x)': 'pi*sin(x)', + } + transformations = standard_transformations + (convert_xor,) + transformations2 = transformations + (split_symbols, + implicit_multiplication) + for case in cases: + implicit = parse_expr(case, transformations=transformations2) + normal = parse_expr(cases[case], transformations=transformations) + assert(implicit == normal) + + application = ['sin x', 'cos 2*x', 'sin cos x'] + for case in application: + raises(SyntaxError, + lambda: parse_expr(case, transformations=transformations2)) + raises(TypeError, + lambda: parse_expr('sin**2(x)', transformations=transformations2)) + + +def test_implicit_application(): + cases = { + 'factorial': 'factorial', + 'sin x': 'sin(x)', + 'tan y**3': 'tan(y**3)', + 'cos 2*x': 'cos(2*x)', + '(cot)': 'cot', + 'sin cos tan x': 'sin(cos(tan(x)))' + } + transformations = standard_transformations + (convert_xor,) + transformations2 = transformations + (implicit_application,) + for case in cases: + implicit = parse_expr(case, transformations=transformations2) + normal = parse_expr(cases[case], transformations=transformations) + assert(implicit == normal), (implicit, normal) + + multiplication = ['x y', 'x sin x', '2x'] + for case in multiplication: + raises(SyntaxError, + lambda: parse_expr(case, transformations=transformations2)) + raises(TypeError, + lambda: parse_expr('sin**2(x)', transformations=transformations2)) + + +def test_function_exponentiation(): + cases = { + 'sin**2(x)': 'sin(x)**2', + 'exp^y(z)': 'exp(z)^y', + 'sin**2(E^(x))': 'sin(E^(x))**2' + } + transformations = standard_transformations + (convert_xor,) + transformations2 = transformations + (function_exponentiation,) + for case in cases: + implicit = parse_expr(case, transformations=transformations2) + normal = parse_expr(cases[case], transformations=transformations) + assert(implicit == normal) + + other_implicit = ['x y', 'x sin x', '2x', 'sin x', + 'cos 2*x', 'sin cos x'] + for case in other_implicit: + raises(SyntaxError, + lambda: parse_expr(case, transformations=transformations2)) + + assert parse_expr('x**2', local_dict={ 'x': sympy.Symbol('x') }, + transformations=transformations2) == parse_expr('x**2') + + +def test_symbol_splitting(): + # By default Greek letter names should not be split (lambda is a keyword + # so skip it) + transformations = standard_transformations + (split_symbols,) + greek_letters = ('alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', + 'eta', 'theta', 'iota', 'kappa', 'mu', 'nu', 'xi', + 'omicron', 'pi', 'rho', 'sigma', 'tau', 'upsilon', + 'phi', 'chi', 'psi', 'omega') + + for letter in greek_letters: + assert(parse_expr(letter, transformations=transformations) == + parse_expr(letter)) + + # Make sure symbol splitting resolves names + transformations += (implicit_multiplication,) + local_dict = { 'e': sympy.E } + cases = { + 'xe': 'E*x', + 'Iy': 'I*y', + 'ee': 'E*E', + } + for case, expected in cases.items(): + assert(parse_expr(case, local_dict=local_dict, + transformations=transformations) == + parse_expr(expected)) + + # Make sure custom splitting works + def can_split(symbol): + if symbol not in ('unsplittable', 'names'): + return _token_splittable(symbol) + return False + transformations = standard_transformations + transformations += (split_symbols_custom(can_split), + implicit_multiplication) + + assert(parse_expr('unsplittable', transformations=transformations) == + parse_expr('unsplittable')) + assert(parse_expr('names', transformations=transformations) == + parse_expr('names')) + assert(parse_expr('xy', transformations=transformations) == + parse_expr('x*y')) + for letter in greek_letters: + assert(parse_expr(letter, transformations=transformations) == + parse_expr(letter)) + + +def test_all_implicit_steps(): + cases = { + '2x': '2*x', # implicit multiplication + 'x y': 'x*y', + 'xy': 'x*y', + 'sin x': 'sin(x)', # add parentheses + '2sin x': '2*sin(x)', + 'x y z': 'x*y*z', + 'sin(2 * 3x)': 'sin(2 * 3 * x)', + 'sin(x) (1 + cos(x))': 'sin(x) * (1 + cos(x))', + '(x + 2) sin(x)': '(x + 2) * sin(x)', + '(x + 2) sin x': '(x + 2) * sin(x)', + 'sin(sin x)': 'sin(sin(x))', + 'sin x!': 'sin(factorial(x))', + 'sin x!!': 'sin(factorial2(x))', + 'factorial': 'factorial', # don't apply a bare function + 'x sin x': 'x * sin(x)', # both application and multiplication + 'xy sin x': 'x * y * sin(x)', + '(x+2)(x+3)': '(x + 2) * (x+3)', + 'x**2 + 2xy + y**2': 'x**2 + 2 * x * y + y**2', # split the xy + 'pi': 'pi', # don't mess with constants + 'None': 'None', + 'ln sin x': 'ln(sin(x))', # multiple implicit function applications + 'sin x**2': 'sin(x**2)', # implicit application to an exponential + 'alpha': 'Symbol("alpha")', # don't split Greek letters/subscripts + 'x_2': 'Symbol("x_2")', + 'sin^2 x**2': 'sin(x**2)**2', # function raised to a power + 'sin**3(x)': 'sin(x)**3', + '(factorial)': 'factorial', + 'tan 3x': 'tan(3*x)', + 'sin^2(3*E^(x))': 'sin(3*E**(x))**2', + 'sin**2(E^(3x))': 'sin(E**(3*x))**2', + 'sin^2 (3x*E^(x))': 'sin(3*x*E^x)**2', + 'pi sin x': 'pi*sin(x)', + } + transformations = standard_transformations + (convert_xor,) + transformations2 = transformations + (implicit_multiplication_application,) + for case in cases: + implicit = parse_expr(case, transformations=transformations2) + normal = parse_expr(cases[case], transformations=transformations) + assert(implicit == normal) + + +def test_no_methods_implicit_multiplication(): + # Issue 21020 + u = sympy.Symbol('u') + transformations = standard_transformations + \ + (implicit_multiplication,) + expr = parse_expr('x.is_polynomial(x)', transformations=transformations) + assert expr == True + expr = parse_expr('(exp(x) / (1 + exp(2x))).subs(exp(x), u)', + transformations=transformations) + assert expr == u/(u**2 + 1) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_latex.py b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_latex.py new file mode 100644 index 0000000000000000000000000000000000000000..49a48966eacaa1cd7a242dcd0e7699c992bb1268 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_latex.py @@ -0,0 +1,358 @@ +from sympy.testing.pytest import raises, XFAIL +from sympy.external import import_module + +from sympy.concrete.products import Product +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.function import (Derivative, Function) +from sympy.core.mul import Mul +from sympy.core.numbers import (E, oo) +from sympy.core.power import Pow +from sympy.core.relational import (GreaterThan, LessThan, StrictGreaterThan, StrictLessThan, Unequality) +from sympy.core.symbol import Symbol +from sympy.functions.combinatorial.factorials import (binomial, factorial) +from sympy.functions.elementary.complexes import (Abs, conjugate) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.integers import (ceiling, floor) +from sympy.functions.elementary.miscellaneous import (root, sqrt) +from sympy.functions.elementary.trigonometric import (asin, cos, csc, sec, sin, tan) +from sympy.integrals.integrals import Integral +from sympy.series.limits import Limit + +from sympy.core.relational import Eq, Ne, Lt, Le, Gt, Ge +from sympy.physics.quantum.state import Bra, Ket +from sympy.abc import x, y, z, a, b, c, t, k, n +antlr4 = import_module("antlr4") + +# disable tests if antlr4-python3-runtime is not present +disabled = antlr4 is None + +theta = Symbol('theta') +f = Function('f') + + +# shorthand definitions +def _Add(a, b): + return Add(a, b, evaluate=False) + + +def _Mul(a, b): + return Mul(a, b, evaluate=False) + + +def _Pow(a, b): + return Pow(a, b, evaluate=False) + + +def _Sqrt(a): + return sqrt(a, evaluate=False) + + +def _Conjugate(a): + return conjugate(a, evaluate=False) + + +def _Abs(a): + return Abs(a, evaluate=False) + + +def _factorial(a): + return factorial(a, evaluate=False) + + +def _exp(a): + return exp(a, evaluate=False) + + +def _log(a, b): + return log(a, b, evaluate=False) + + +def _binomial(n, k): + return binomial(n, k, evaluate=False) + + +def test_import(): + from sympy.parsing.latex._build_latex_antlr import ( + build_parser, + check_antlr_version, + dir_latex_antlr + ) + # XXX: It would be better to come up with a test for these... + del build_parser, check_antlr_version, dir_latex_antlr + + +# These LaTeX strings should parse to the corresponding SymPy expression +GOOD_PAIRS = [ + (r"0", 0), + (r"1", 1), + (r"-3.14", -3.14), + (r"(-7.13)(1.5)", _Mul(-7.13, 1.5)), + (r"x", x), + (r"2x", 2*x), + (r"x^2", x**2), + (r"x^\frac{1}{2}", _Pow(x, _Pow(2, -1))), + (r"x^{3 + 1}", x**_Add(3, 1)), + (r"-c", -c), + (r"a \cdot b", a * b), + (r"a / b", a / b), + (r"a \div b", a / b), + (r"a + b", a + b), + (r"a + b - a", _Add(a+b, -a)), + (r"a^2 + b^2 = c^2", Eq(a**2 + b**2, c**2)), + (r"(x + y) z", _Mul(_Add(x, y), z)), + (r"a'b+ab'", _Add(_Mul(Symbol("a'"), b), _Mul(a, Symbol("b'")))), + (r"y''_1", Symbol("y_{1}''")), + (r"y_1''", Symbol("y_{1}''")), + (r"\left(x + y\right) z", _Mul(_Add(x, y), z)), + (r"\left( x + y\right ) z", _Mul(_Add(x, y), z)), + (r"\left( x + y\right ) z", _Mul(_Add(x, y), z)), + (r"\left[x + y\right] z", _Mul(_Add(x, y), z)), + (r"\left\{x + y\right\} z", _Mul(_Add(x, y), z)), + (r"1+1", _Add(1, 1)), + (r"0+1", _Add(0, 1)), + (r"1*2", _Mul(1, 2)), + (r"0*1", _Mul(0, 1)), + (r"1 \times 2 ", _Mul(1, 2)), + (r"x = y", Eq(x, y)), + (r"x \neq y", Ne(x, y)), + (r"x < y", Lt(x, y)), + (r"x > y", Gt(x, y)), + (r"x \leq y", Le(x, y)), + (r"x \geq y", Ge(x, y)), + (r"x \le y", Le(x, y)), + (r"x \ge y", Ge(x, y)), + (r"\lfloor x \rfloor", floor(x)), + (r"\lceil x \rceil", ceiling(x)), + (r"\langle x |", Bra('x')), + (r"| x \rangle", Ket('x')), + (r"\sin \theta", sin(theta)), + (r"\sin(\theta)", sin(theta)), + (r"\sin^{-1} a", asin(a)), + (r"\sin a \cos b", _Mul(sin(a), cos(b))), + (r"\sin \cos \theta", sin(cos(theta))), + (r"\sin(\cos \theta)", sin(cos(theta))), + (r"\frac{a}{b}", a / b), + (r"\dfrac{a}{b}", a / b), + (r"\tfrac{a}{b}", a / b), + (r"\frac12", _Pow(2, -1)), + (r"\frac12y", _Mul(_Pow(2, -1), y)), + (r"\frac1234", _Mul(_Pow(2, -1), 34)), + (r"\frac2{3}", _Mul(2, _Pow(3, -1))), + (r"\frac{\sin{x}}2", _Mul(sin(x), _Pow(2, -1))), + (r"\frac{a + b}{c}", _Mul(a + b, _Pow(c, -1))), + (r"\frac{7}{3}", _Mul(7, _Pow(3, -1))), + (r"(\csc x)(\sec y)", csc(x)*sec(y)), + (r"\lim_{x \to 3} a", Limit(a, x, 3, dir='+-')), + (r"\lim_{x \rightarrow 3} a", Limit(a, x, 3, dir='+-')), + (r"\lim_{x \Rightarrow 3} a", Limit(a, x, 3, dir='+-')), + (r"\lim_{x \longrightarrow 3} a", Limit(a, x, 3, dir='+-')), + (r"\lim_{x \Longrightarrow 3} a", Limit(a, x, 3, dir='+-')), + (r"\lim_{x \to 3^{+}} a", Limit(a, x, 3, dir='+')), + (r"\lim_{x \to 3^{-}} a", Limit(a, x, 3, dir='-')), + (r"\lim_{x \to 3^+} a", Limit(a, x, 3, dir='+')), + (r"\lim_{x \to 3^-} a", Limit(a, x, 3, dir='-')), + (r"\infty", oo), + (r"\lim_{x \to \infty} \frac{1}{x}", Limit(_Pow(x, -1), x, oo)), + (r"\frac{d}{dx} x", Derivative(x, x)), + (r"\frac{d}{dt} x", Derivative(x, t)), + (r"f(x)", f(x)), + (r"f(x, y)", f(x, y)), + (r"f(x, y, z)", f(x, y, z)), + (r"f'_1(x)", Function("f_{1}'")(x)), + (r"f_{1}''(x+y)", Function("f_{1}''")(x+y)), + (r"\frac{d f(x)}{dx}", Derivative(f(x), x)), + (r"\frac{d\theta(x)}{dx}", Derivative(Function('theta')(x), x)), + (r"x \neq y", Unequality(x, y)), + (r"|x|", _Abs(x)), + (r"||x||", _Abs(Abs(x))), + (r"|x||y|", _Abs(x)*_Abs(y)), + (r"||x||y||", _Abs(_Abs(x)*_Abs(y))), + (r"\pi^{|xy|}", Symbol('pi')**_Abs(x*y)), + (r"\int x dx", Integral(x, x)), + (r"\int x d\theta", Integral(x, theta)), + (r"\int (x^2 - y)dx", Integral(x**2 - y, x)), + (r"\int x + a dx", Integral(_Add(x, a), x)), + (r"\int da", Integral(1, a)), + (r"\int_0^7 dx", Integral(1, (x, 0, 7))), + (r"\int\limits_{0}^{1} x dx", Integral(x, (x, 0, 1))), + (r"\int_a^b x dx", Integral(x, (x, a, b))), + (r"\int^b_a x dx", Integral(x, (x, a, b))), + (r"\int_{a}^b x dx", Integral(x, (x, a, b))), + (r"\int^{b}_a x dx", Integral(x, (x, a, b))), + (r"\int_{a}^{b} x dx", Integral(x, (x, a, b))), + (r"\int^{b}_{a} x dx", Integral(x, (x, a, b))), + (r"\int_{f(a)}^{f(b)} f(z) dz", Integral(f(z), (z, f(a), f(b)))), + (r"\int (x+a)", Integral(_Add(x, a), x)), + (r"\int a + b + c dx", Integral(_Add(_Add(a, b), c), x)), + (r"\int \frac{dz}{z}", Integral(Pow(z, -1), z)), + (r"\int \frac{3 dz}{z}", Integral(3*Pow(z, -1), z)), + (r"\int \frac{1}{x} dx", Integral(Pow(x, -1), x)), + (r"\int \frac{1}{a} + \frac{1}{b} dx", + Integral(_Add(_Pow(a, -1), Pow(b, -1)), x)), + (r"\int \frac{3 \cdot d\theta}{\theta}", + Integral(3*_Pow(theta, -1), theta)), + (r"\int \frac{1}{x} + 1 dx", Integral(_Add(_Pow(x, -1), 1), x)), + (r"x_0", Symbol('x_{0}')), + (r"x_{1}", Symbol('x_{1}')), + (r"x_a", Symbol('x_{a}')), + (r"x_{b}", Symbol('x_{b}')), + (r"h_\theta", Symbol('h_{theta}')), + (r"h_{\theta}", Symbol('h_{theta}')), + (r"h_{\theta}(x_0, x_1)", + Function('h_{theta}')(Symbol('x_{0}'), Symbol('x_{1}'))), + (r"x!", _factorial(x)), + (r"100!", _factorial(100)), + (r"\theta!", _factorial(theta)), + (r"(x + 1)!", _factorial(_Add(x, 1))), + (r"(x!)!", _factorial(_factorial(x))), + (r"x!!!", _factorial(_factorial(_factorial(x)))), + (r"5!7!", _Mul(_factorial(5), _factorial(7))), + (r"\sqrt{x}", sqrt(x)), + (r"\sqrt{x + b}", sqrt(_Add(x, b))), + (r"\sqrt[3]{\sin x}", root(sin(x), 3)), + (r"\sqrt[y]{\sin x}", root(sin(x), y)), + (r"\sqrt[\theta]{\sin x}", root(sin(x), theta)), + (r"\sqrt{\frac{12}{6}}", _Sqrt(_Mul(12, _Pow(6, -1)))), + (r"\overline{z}", _Conjugate(z)), + (r"\overline{\overline{z}}", _Conjugate(_Conjugate(z))), + (r"\overline{x + y}", _Conjugate(_Add(x, y))), + (r"\overline{x} + \overline{y}", _Conjugate(x) + _Conjugate(y)), + (r"x < y", StrictLessThan(x, y)), + (r"x \leq y", LessThan(x, y)), + (r"x > y", StrictGreaterThan(x, y)), + (r"x \geq y", GreaterThan(x, y)), + (r"\mathit{x}", Symbol('x')), + (r"\mathit{test}", Symbol('test')), + (r"\mathit{TEST}", Symbol('TEST')), + (r"\mathit{HELLO world}", Symbol('HELLO world')), + (r"\sum_{k = 1}^{3} c", Sum(c, (k, 1, 3))), + (r"\sum_{k = 1}^3 c", Sum(c, (k, 1, 3))), + (r"\sum^{3}_{k = 1} c", Sum(c, (k, 1, 3))), + (r"\sum^3_{k = 1} c", Sum(c, (k, 1, 3))), + (r"\sum_{k = 1}^{10} k^2", Sum(k**2, (k, 1, 10))), + (r"\sum_{n = 0}^{\infty} \frac{1}{n!}", + Sum(_Pow(_factorial(n), -1), (n, 0, oo))), + (r"\prod_{a = b}^{c} x", Product(x, (a, b, c))), + (r"\prod_{a = b}^c x", Product(x, (a, b, c))), + (r"\prod^{c}_{a = b} x", Product(x, (a, b, c))), + (r"\prod^c_{a = b} x", Product(x, (a, b, c))), + (r"\exp x", _exp(x)), + (r"\exp(x)", _exp(x)), + (r"\lg x", _log(x, 10)), + (r"\ln x", _log(x, E)), + (r"\ln xy", _log(x*y, E)), + (r"\log x", _log(x, E)), + (r"\log xy", _log(x*y, E)), + (r"\log_{2} x", _log(x, 2)), + (r"\log_{a} x", _log(x, a)), + (r"\log_{11} x", _log(x, 11)), + (r"\log_{a^2} x", _log(x, _Pow(a, 2))), + (r"[x]", x), + (r"[a + b]", _Add(a, b)), + (r"\frac{d}{dx} [ \tan x ]", Derivative(tan(x), x)), + (r"\binom{n}{k}", _binomial(n, k)), + (r"\tbinom{n}{k}", _binomial(n, k)), + (r"\dbinom{n}{k}", _binomial(n, k)), + (r"\binom{n}{0}", _binomial(n, 0)), + (r"x^\binom{n}{k}", _Pow(x, _binomial(n, k))), + (r"a \, b", _Mul(a, b)), + (r"a \thinspace b", _Mul(a, b)), + (r"a \: b", _Mul(a, b)), + (r"a \medspace b", _Mul(a, b)), + (r"a \; b", _Mul(a, b)), + (r"a \thickspace b", _Mul(a, b)), + (r"a \quad b", _Mul(a, b)), + (r"a \qquad b", _Mul(a, b)), + (r"a \! b", _Mul(a, b)), + (r"a \negthinspace b", _Mul(a, b)), + (r"a \negmedspace b", _Mul(a, b)), + (r"a \negthickspace b", _Mul(a, b)), + (r"\int x \, dx", Integral(x, x)), + (r"\log_2 x", _log(x, 2)), + (r"\log_a x", _log(x, a)), + (r"5^0 - 4^0", _Add(_Pow(5, 0), _Mul(-1, _Pow(4, 0)))), + (r"3x - 1", _Add(_Mul(3, x), -1)) +] + + +def test_parseable(): + from sympy.parsing.latex import parse_latex + for latex_str, sympy_expr in GOOD_PAIRS: + assert parse_latex(latex_str) == sympy_expr, latex_str + +# These bad LaTeX strings should raise a LaTeXParsingError when parsed +BAD_STRINGS = [ + r"(", + r")", + r"\frac{d}{dx}", + r"(\frac{d}{dx})", + r"\sqrt{}", + r"\sqrt", + r"\overline{}", + r"\overline", + r"{", + r"}", + r"\mathit{x + y}", + r"\mathit{21}", + r"\frac{2}{}", + r"\frac{}{2}", + r"\int", + r"!", + r"!0", + r"_", + r"^", + r"|", + r"||x|", + r"()", + r"((((((((((((((((()))))))))))))))))", + r"-", + r"\frac{d}{dx} + \frac{d}{dt}", + r"f(x,,y)", + r"f(x,y,", + r"\sin^x", + r"\cos^2", + r"@", + r"#", + r"$", + r"%", + r"&", + r"*", + r"" "\\", + r"~", + r"\frac{(2 + x}{1 - x)}", +] + +def test_not_parseable(): + from sympy.parsing.latex import parse_latex, LaTeXParsingError + for latex_str in BAD_STRINGS: + with raises(LaTeXParsingError): + parse_latex(latex_str) + +# At time of migration from latex2sympy, should fail but doesn't +FAILING_BAD_STRINGS = [ + r"\cos 1 \cos", + r"f(,", + r"f()", + r"a \div \div b", + r"a \cdot \cdot b", + r"a // b", + r"a +", + r"1.1.1", + r"1 +", + r"a / b /", +] + +@XFAIL +def test_failing_not_parseable(): + from sympy.parsing.latex import parse_latex, LaTeXParsingError + for latex_str in FAILING_BAD_STRINGS: + with raises(LaTeXParsingError): + parse_latex(latex_str) + +# In strict mode, FAILING_BAD_STRINGS would fail +def test_strict_mode(): + from sympy.parsing.latex import parse_latex, LaTeXParsingError + for latex_str in FAILING_BAD_STRINGS: + with raises(LaTeXParsingError): + parse_latex(latex_str, strict=True) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_latex_deps.py b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_latex_deps.py new file mode 100644 index 0000000000000000000000000000000000000000..7df44c2b19e34024db6e898f7c4eac962dcaa1c9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_latex_deps.py @@ -0,0 +1,16 @@ +from sympy.external import import_module +from sympy.testing.pytest import ignore_warnings, raises + +antlr4 = import_module("antlr4", warn_not_installed=False) + +# disable tests if antlr4-python3-runtime is not present +if antlr4: + disabled = True + + +def test_no_import(): + from sympy.parsing.latex import parse_latex + + with ignore_warnings(UserWarning): + with raises(ImportError): + parse_latex('1 + 1') diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_latex_lark.py b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_latex_lark.py new file mode 100644 index 0000000000000000000000000000000000000000..dd1f72a66c788ac41d923005ea988664d05a16c1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_latex_lark.py @@ -0,0 +1,872 @@ +from sympy.testing.pytest import XFAIL +from sympy.parsing.latex.lark import parse_latex_lark +from sympy.external import import_module + +from sympy.concrete.products import Product +from sympy.concrete.summations import Sum +from sympy.core.function import Derivative, Function +from sympy.core.numbers import E, oo, Rational +from sympy.core.power import Pow +from sympy.core.parameters import evaluate +from sympy.core.relational import GreaterThan, LessThan, StrictGreaterThan, StrictLessThan, Unequality +from sympy.core.symbol import Symbol +from sympy.functions.combinatorial.factorials import binomial, factorial +from sympy.functions.elementary.complexes import Abs, conjugate +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.integers import ceiling, floor +from sympy.functions.elementary.miscellaneous import root, sqrt, Min, Max +from sympy.functions.elementary.trigonometric import asin, cos, csc, sec, sin, tan +from sympy.integrals.integrals import Integral +from sympy.series.limits import Limit +from sympy import Matrix, MatAdd, MatMul, Transpose, Trace +from sympy import I + +from sympy.core.relational import Eq, Ne, Lt, Le, Gt, Ge +from sympy.physics.quantum import Bra, Ket, InnerProduct +from sympy.abc import x, y, z, a, b, c, d, t, k, n + +from .test_latex import theta, f, _Add, _Mul, _Pow, _Sqrt, _Conjugate, _Abs, _factorial, _exp, _binomial + +lark = import_module("lark") + +# disable tests if lark is not present +disabled = lark is None + +# shorthand definitions that are only needed for the Lark LaTeX parser +def _Min(*args): + return Min(*args, evaluate=False) + + +def _Max(*args): + return Max(*args, evaluate=False) + + +def _log(a, b=E): + if b == E: + return log(a, evaluate=False) + else: + return log(a, b, evaluate=False) + + +def _MatAdd(a, b): + return MatAdd(a, b, evaluate=False) + + +def _MatMul(a, b): + return MatMul(a, b, evaluate=False) + + +# These LaTeX strings should parse to the corresponding SymPy expression +SYMBOL_EXPRESSION_PAIRS = [ + (r"x_0", Symbol('x_{0}')), + (r"x_{1}", Symbol('x_{1}')), + (r"x_a", Symbol('x_{a}')), + (r"x_{b}", Symbol('x_{b}')), + (r"h_\theta", Symbol('h_{theta}')), + (r"h_{\theta}", Symbol('h_{theta}')), + (r"y''_1", Symbol("y''_{1}")), + (r"y_1''", Symbol("y_{1}''")), + (r"\mathit{x}", Symbol('x')), + (r"\mathit{test}", Symbol('test')), + (r"\mathit{TEST}", Symbol('TEST')), + (r"\mathit{HELLO world}", Symbol('HELLO world')), + (r"a'", Symbol("a'")), + (r"a''", Symbol("a''")), + (r"\alpha'", Symbol("alpha'")), + (r"\alpha''", Symbol("alpha''")), + (r"a_b", Symbol("a_{b}")), + (r"a_b'", Symbol("a_{b}'")), + (r"a'_b", Symbol("a'_{b}")), + (r"a'_b'", Symbol("a'_{b}'")), + (r"a_{b'}", Symbol("a_{b'}")), + (r"a_{b'}'", Symbol("a_{b'}'")), + (r"a'_{b'}", Symbol("a'_{b'}")), + (r"a'_{b'}'", Symbol("a'_{b'}'")), + (r"\mathit{foo}'", Symbol("foo'")), + (r"\mathit{foo'}", Symbol("foo'")), + (r"\mathit{foo'}'", Symbol("foo''")), + (r"a_b''", Symbol("a_{b}''")), + (r"a''_b", Symbol("a''_{b}")), + (r"a''_b'''", Symbol("a''_{b}'''")), + (r"a_{b''}", Symbol("a_{b''}")), + (r"a_{b''}''", Symbol("a_{b''}''")), + (r"a''_{b''}", Symbol("a''_{b''}")), + (r"a''_{b''}'''", Symbol("a''_{b''}'''")), + (r"\mathit{foo}''", Symbol("foo''")), + (r"\mathit{foo''}", Symbol("foo''")), + (r"\mathit{foo''}'''", Symbol("foo'''''")), + (r"a_\alpha", Symbol("a_{alpha}")), + (r"a_\alpha'", Symbol("a_{alpha}'")), + (r"a'_\alpha", Symbol("a'_{alpha}")), + (r"a'_\alpha'", Symbol("a'_{alpha}'")), + (r"a_{\alpha'}", Symbol("a_{alpha'}")), + (r"a_{\alpha'}'", Symbol("a_{alpha'}'")), + (r"a'_{\alpha'}", Symbol("a'_{alpha'}")), + (r"a'_{\alpha'}'", Symbol("a'_{alpha'}'")), + (r"a_\alpha''", Symbol("a_{alpha}''")), + (r"a''_\alpha", Symbol("a''_{alpha}")), + (r"a''_\alpha'''", Symbol("a''_{alpha}'''")), + (r"a_{\alpha''}", Symbol("a_{alpha''}")), + (r"a_{\alpha''}''", Symbol("a_{alpha''}''")), + (r"a''_{\alpha''}", Symbol("a''_{alpha''}")), + (r"a''_{\alpha''}'''", Symbol("a''_{alpha''}'''")), + (r"\alpha_b", Symbol("alpha_{b}")), + (r"\alpha_b'", Symbol("alpha_{b}'")), + (r"\alpha'_b", Symbol("alpha'_{b}")), + (r"\alpha'_b'", Symbol("alpha'_{b}'")), + (r"\alpha_{b'}", Symbol("alpha_{b'}")), + (r"\alpha_{b'}'", Symbol("alpha_{b'}'")), + (r"\alpha'_{b'}", Symbol("alpha'_{b'}")), + (r"\alpha'_{b'}'", Symbol("alpha'_{b'}'")), + (r"\alpha_b''", Symbol("alpha_{b}''")), + (r"\alpha''_b", Symbol("alpha''_{b}")), + (r"\alpha''_b'''", Symbol("alpha''_{b}'''")), + (r"\alpha_{b''}", Symbol("alpha_{b''}")), + (r"\alpha_{b''}''", Symbol("alpha_{b''}''")), + (r"\alpha''_{b''}", Symbol("alpha''_{b''}")), + (r"\alpha''_{b''}'''", Symbol("alpha''_{b''}'''")), + (r"\alpha_\beta", Symbol("alpha_{beta}")), + (r"\alpha_{\beta}", Symbol("alpha_{beta}")), + (r"\alpha_{\beta'}", Symbol("alpha_{beta'}")), + (r"\alpha_{\beta''}", Symbol("alpha_{beta''}")), + (r"\alpha'_\beta", Symbol("alpha'_{beta}")), + (r"\alpha'_{\beta}", Symbol("alpha'_{beta}")), + (r"\alpha'_{\beta'}", Symbol("alpha'_{beta'}")), + (r"\alpha'_{\beta''}", Symbol("alpha'_{beta''}")), + (r"\alpha''_\beta", Symbol("alpha''_{beta}")), + (r"\alpha''_{\beta}", Symbol("alpha''_{beta}")), + (r"\alpha''_{\beta'}", Symbol("alpha''_{beta'}")), + (r"\alpha''_{\beta''}", Symbol("alpha''_{beta''}")), + (r"\alpha_\beta'", Symbol("alpha_{beta}'")), + (r"\alpha_{\beta}'", Symbol("alpha_{beta}'")), + (r"\alpha_{\beta'}'", Symbol("alpha_{beta'}'")), + (r"\alpha_{\beta''}'", Symbol("alpha_{beta''}'")), + (r"\alpha'_\beta'", Symbol("alpha'_{beta}'")), + (r"\alpha'_{\beta}'", Symbol("alpha'_{beta}'")), + (r"\alpha'_{\beta'}'", Symbol("alpha'_{beta'}'")), + (r"\alpha'_{\beta''}'", Symbol("alpha'_{beta''}'")), + (r"\alpha''_\beta'", Symbol("alpha''_{beta}'")), + (r"\alpha''_{\beta}'", Symbol("alpha''_{beta}'")), + (r"\alpha''_{\beta'}'", Symbol("alpha''_{beta'}'")), + (r"\alpha''_{\beta''}'", Symbol("alpha''_{beta''}'")), + (r"\alpha_\beta''", Symbol("alpha_{beta}''")), + (r"\alpha_{\beta}''", Symbol("alpha_{beta}''")), + (r"\alpha_{\beta'}''", Symbol("alpha_{beta'}''")), + (r"\alpha_{\beta''}''", Symbol("alpha_{beta''}''")), + (r"\alpha'_\beta''", Symbol("alpha'_{beta}''")), + (r"\alpha'_{\beta}''", Symbol("alpha'_{beta}''")), + (r"\alpha'_{\beta'}''", Symbol("alpha'_{beta'}''")), + (r"\alpha'_{\beta''}''", Symbol("alpha'_{beta''}''")), + (r"\alpha''_\beta''", Symbol("alpha''_{beta}''")), + (r"\alpha''_{\beta}''", Symbol("alpha''_{beta}''")), + (r"\alpha''_{\beta'}''", Symbol("alpha''_{beta'}''")), + (r"\alpha''_{\beta''}''", Symbol("alpha''_{beta''}''")) + +] + +UNEVALUATED_SIMPLE_EXPRESSION_PAIRS = [ + (r"0", 0), + (r"1", 1), + (r"-3.14", -3.14), + (r"(-7.13)(1.5)", _Mul(-7.13, 1.5)), + (r"1+1", _Add(1, 1)), + (r"0+1", _Add(0, 1)), + (r"1*2", _Mul(1, 2)), + (r"0*1", _Mul(0, 1)), + (r"x", x), + (r"2x", 2 * x), + (r"3x - 1", _Add(_Mul(3, x), -1)), + (r"-c", -c), + (r"\infty", oo), + (r"a \cdot b", a * b), + (r"1 \times 2 ", _Mul(1, 2)), + (r"a / b", a / b), + (r"a \div b", a / b), + (r"a + b", a + b), + (r"a + b - a", _Add(a + b, -a)), + (r"(x + y) z", _Mul(_Add(x, y), z)), + (r"a'b+ab'", _Add(_Mul(Symbol("a'"), b), _Mul(a, Symbol("b'")))) +] + +EVALUATED_SIMPLE_EXPRESSION_PAIRS = [ + (r"(-7.13)(1.5)", -10.695), + (r"1+1", 2), + (r"0+1", 1), + (r"1*2", 2), + (r"0*1", 0), + (r"2x", 2 * x), + (r"3x - 1", 3 * x - 1), + (r"-c", -c), + (r"a \cdot b", a * b), + (r"1 \times 2 ", 2), + (r"a / b", a / b), + (r"a \div b", a / b), + (r"a + b", a + b), + (r"a + b - a", b), + (r"(x + y) z", (x + y) * z), +] + +UNEVALUATED_FRACTION_EXPRESSION_PAIRS = [ + (r"\frac{a}{b}", a / b), + (r"\dfrac{a}{b}", a / b), + (r"\tfrac{a}{b}", a / b), + (r"\frac12", _Mul(1, _Pow(2, -1))), + (r"\frac12y", _Mul(_Mul(1, _Pow(2, -1)), y)), + (r"\frac1234", _Mul(_Mul(1, _Pow(2, -1)), 34)), + (r"\frac2{3}", _Mul(2, _Pow(3, -1))), + (r"\frac{a + b}{c}", _Mul(a + b, _Pow(c, -1))), + (r"\frac{7}{3}", _Mul(7, _Pow(3, -1))) +] + +EVALUATED_FRACTION_EXPRESSION_PAIRS = [ + (r"\frac{a}{b}", a / b), + (r"\dfrac{a}{b}", a / b), + (r"\tfrac{a}{b}", a / b), + (r"\frac12", Rational(1, 2)), + (r"\frac12y", y / 2), + (r"\frac1234", 17), + (r"\frac2{3}", Rational(2, 3)), + (r"\frac{a + b}{c}", (a + b) / c), + (r"\frac{7}{3}", Rational(7, 3)) +] + +RELATION_EXPRESSION_PAIRS = [ + (r"x = y", Eq(x, y)), + (r"x \neq y", Ne(x, y)), + (r"x < y", Lt(x, y)), + (r"x > y", Gt(x, y)), + (r"x \leq y", Le(x, y)), + (r"x \geq y", Ge(x, y)), + (r"x \le y", Le(x, y)), + (r"x \ge y", Ge(x, y)), + (r"x < y", StrictLessThan(x, y)), + (r"x \leq y", LessThan(x, y)), + (r"x > y", StrictGreaterThan(x, y)), + (r"x \geq y", GreaterThan(x, y)), + (r"x \neq y", Unequality(x, y)), # same as 2nd one in the list + (r"a^2 + b^2 = c^2", Eq(a**2 + b**2, c**2)) +] + +UNEVALUATED_POWER_EXPRESSION_PAIRS = [ + (r"x^2", x ** 2), + (r"x^\frac{1}{2}", _Pow(x, _Mul(1, _Pow(2, -1)))), + (r"x^{3 + 1}", x ** _Add(3, 1)), + (r"\pi^{|xy|}", Symbol('pi') ** _Abs(x * y)), + (r"5^0 - 4^0", _Add(_Pow(5, 0), _Mul(-1, _Pow(4, 0)))) +] + +EVALUATED_POWER_EXPRESSION_PAIRS = [ + (r"x^2", x ** 2), + (r"x^\frac{1}{2}", sqrt(x)), + (r"x^{3 + 1}", x ** 4), + (r"\pi^{|xy|}", Symbol('pi') ** _Abs(x * y)), + (r"5^0 - 4^0", 0) +] + +UNEVALUATED_INTEGRAL_EXPRESSION_PAIRS = [ + (r"\int x dx", Integral(_Mul(1, x), x)), + (r"\int x \, dx", Integral(_Mul(1, x), x)), + (r"\int x d\theta", Integral(_Mul(1, x), theta)), + (r"\int (x^2 - y)dx", Integral(_Mul(1, x ** 2 - y), x)), + (r"\int x + a dx", Integral(_Mul(1, _Add(x, a)), x)), + (r"\int da", Integral(_Mul(1, 1), a)), + (r"\int_0^7 dx", Integral(_Mul(1, 1), (x, 0, 7))), + (r"\int\limits_{0}^{1} x dx", Integral(_Mul(1, x), (x, 0, 1))), + (r"\int_a^b x dx", Integral(_Mul(1, x), (x, a, b))), + (r"\int^b_a x dx", Integral(_Mul(1, x), (x, a, b))), + (r"\int_{a}^b x dx", Integral(_Mul(1, x), (x, a, b))), + (r"\int^{b}_a x dx", Integral(_Mul(1, x), (x, a, b))), + (r"\int_{a}^{b} x dx", Integral(_Mul(1, x), (x, a, b))), + (r"\int^{b}_{a} x dx", Integral(_Mul(1, x), (x, a, b))), + (r"\int_{f(a)}^{f(b)} f(z) dz", Integral(f(z), (z, f(a), f(b)))), + (r"\int a + b + c dx", Integral(_Mul(1, _Add(_Add(a, b), c)), x)), + (r"\int \frac{dz}{z}", Integral(_Mul(1, _Mul(1, Pow(z, -1))), z)), + (r"\int \frac{3 dz}{z}", Integral(_Mul(1, _Mul(3, _Pow(z, -1))), z)), + (r"\int \frac{1}{x} dx", Integral(_Mul(1, _Mul(1, Pow(x, -1))), x)), + (r"\int \frac{1}{a} + \frac{1}{b} dx", + Integral(_Mul(1, _Add(_Mul(1, _Pow(a, -1)), _Mul(1, Pow(b, -1)))), x)), + (r"\int \frac{1}{x} + 1 dx", Integral(_Mul(1, _Add(_Mul(1, _Pow(x, -1)), 1)), x)) +] + +EVALUATED_INTEGRAL_EXPRESSION_PAIRS = [ + (r"\int x dx", Integral(x, x)), + (r"\int x \, dx", Integral(x, x)), + (r"\int x d\theta", Integral(x, theta)), + (r"\int (x^2 - y)dx", Integral(x ** 2 - y, x)), + (r"\int x + a dx", Integral(x + a, x)), + (r"\int da", Integral(1, a)), + (r"\int_0^7 dx", Integral(1, (x, 0, 7))), + (r"\int\limits_{0}^{1} x dx", Integral(x, (x, 0, 1))), + (r"\int_a^b x dx", Integral(x, (x, a, b))), + (r"\int^b_a x dx", Integral(x, (x, a, b))), + (r"\int_{a}^b x dx", Integral(x, (x, a, b))), + (r"\int^{b}_a x dx", Integral(x, (x, a, b))), + (r"\int_{a}^{b} x dx", Integral(x, (x, a, b))), + (r"\int^{b}_{a} x dx", Integral(x, (x, a, b))), + (r"\int_{f(a)}^{f(b)} f(z) dz", Integral(f(z), (z, f(a), f(b)))), + (r"\int a + b + c dx", Integral(a + b + c, x)), + (r"\int \frac{dz}{z}", Integral(Pow(z, -1), z)), + (r"\int \frac{3 dz}{z}", Integral(3 * Pow(z, -1), z)), + (r"\int \frac{1}{x} dx", Integral(1 / x, x)), + (r"\int \frac{1}{a} + \frac{1}{b} dx", Integral(1 / a + 1 / b, x)), + (r"\int \frac{1}{a} - \frac{1}{b} dx", Integral(1 / a - 1 / b, x)), + (r"\int \frac{1}{x} + 1 dx", Integral(1 / x + 1, x)) +] + +DERIVATIVE_EXPRESSION_PAIRS = [ + (r"\frac{d}{dx} x", Derivative(x, x)), + (r"\frac{d}{dt} x", Derivative(x, t)), + (r"\frac{d}{dx} ( \tan x )", Derivative(tan(x), x)), + (r"\frac{d f(x)}{dx}", Derivative(f(x), x)), + (r"\frac{d\theta(x)}{dx}", Derivative(Function('theta')(x), x)) +] + +TRIGONOMETRIC_EXPRESSION_PAIRS = [ + (r"\sin \theta", sin(theta)), + (r"\sin(\theta)", sin(theta)), + (r"\sin^{-1} a", asin(a)), + (r"\sin a \cos b", _Mul(sin(a), cos(b))), + (r"\sin \cos \theta", sin(cos(theta))), + (r"\sin(\cos \theta)", sin(cos(theta))), + (r"(\csc x)(\sec y)", csc(x) * sec(y)), + (r"\frac{\sin{x}}2", _Mul(sin(x), _Pow(2, -1))) +] + +UNEVALUATED_LIMIT_EXPRESSION_PAIRS = [ + (r"\lim_{x \to 3} a", Limit(a, x, 3, dir="+-")), + (r"\lim_{x \rightarrow 3} a", Limit(a, x, 3, dir="+-")), + (r"\lim_{x \Rightarrow 3} a", Limit(a, x, 3, dir="+-")), + (r"\lim_{x \longrightarrow 3} a", Limit(a, x, 3, dir="+-")), + (r"\lim_{x \Longrightarrow 3} a", Limit(a, x, 3, dir="+-")), + (r"\lim_{x \to 3^{+}} a", Limit(a, x, 3, dir="+")), + (r"\lim_{x \to 3^{-}} a", Limit(a, x, 3, dir="-")), + (r"\lim_{x \to 3^+} a", Limit(a, x, 3, dir="+")), + (r"\lim_{x \to 3^-} a", Limit(a, x, 3, dir="-")), + (r"\lim_{x \to \infty} \frac{1}{x}", Limit(_Mul(1, _Pow(x, -1)), x, oo)) +] + +EVALUATED_LIMIT_EXPRESSION_PAIRS = [ + (r"\lim_{x \to \infty} \frac{1}{x}", Limit(1 / x, x, oo)) +] + +UNEVALUATED_SQRT_EXPRESSION_PAIRS = [ + (r"\sqrt{x}", sqrt(x)), + (r"\sqrt{x + b}", sqrt(_Add(x, b))), + (r"\sqrt[3]{\sin x}", _Pow(sin(x), _Pow(3, -1))), + # the above test needed to be handled differently than the ones below because root + # acts differently if its second argument is a number + (r"\sqrt[y]{\sin x}", root(sin(x), y)), + (r"\sqrt[\theta]{\sin x}", root(sin(x), theta)), + (r"\sqrt{\frac{12}{6}}", _Sqrt(_Mul(12, _Pow(6, -1)))) +] + +EVALUATED_SQRT_EXPRESSION_PAIRS = [ + (r"\sqrt{x}", sqrt(x)), + (r"\sqrt{x + b}", sqrt(x + b)), + (r"\sqrt[3]{\sin x}", root(sin(x), 3)), + (r"\sqrt[y]{\sin x}", root(sin(x), y)), + (r"\sqrt[\theta]{\sin x}", root(sin(x), theta)), + (r"\sqrt{\frac{12}{6}}", sqrt(2)) +] + +UNEVALUATED_FACTORIAL_EXPRESSION_PAIRS = [ + (r"x!", _factorial(x)), + (r"100!", _factorial(100)), + (r"\theta!", _factorial(theta)), + (r"(x + 1)!", _factorial(_Add(x, 1))), + (r"(x!)!", _factorial(_factorial(x))), + (r"x!!!", _factorial(_factorial(_factorial(x)))), + (r"5!7!", _Mul(_factorial(5), _factorial(7))) +] + +EVALUATED_FACTORIAL_EXPRESSION_PAIRS = [ + (r"x!", factorial(x)), + (r"100!", factorial(100)), + (r"\theta!", factorial(theta)), + (r"(x + 1)!", factorial(x + 1)), + (r"(x!)!", factorial(factorial(x))), + (r"x!!!", factorial(factorial(factorial(x)))), + (r"5!7!", factorial(5) * factorial(7)), + (r"24! \times 24!", factorial(24) * factorial(24)) +] + +UNEVALUATED_SUM_EXPRESSION_PAIRS = [ + (r"\sum_{k = 1}^{3} c", Sum(_Mul(1, c), (k, 1, 3))), + (r"\sum_{k = 1}^3 c", Sum(_Mul(1, c), (k, 1, 3))), + (r"\sum^{3}_{k = 1} c", Sum(_Mul(1, c), (k, 1, 3))), + (r"\sum^3_{k = 1} c", Sum(_Mul(1, c), (k, 1, 3))), + (r"\sum_{k = 1}^{10} k^2", Sum(_Mul(1, k ** 2), (k, 1, 10))), + (r"\sum_{n = 0}^{\infty} \frac{1}{n!}", + Sum(_Mul(1, _Mul(1, _Pow(_factorial(n), -1))), (n, 0, oo))) +] + +EVALUATED_SUM_EXPRESSION_PAIRS = [ + (r"\sum_{k = 1}^{3} c", Sum(c, (k, 1, 3))), + (r"\sum_{k = 1}^3 c", Sum(c, (k, 1, 3))), + (r"\sum^{3}_{k = 1} c", Sum(c, (k, 1, 3))), + (r"\sum^3_{k = 1} c", Sum(c, (k, 1, 3))), + (r"\sum_{k = 1}^{10} k^2", Sum(k ** 2, (k, 1, 10))), + (r"\sum_{n = 0}^{\infty} \frac{1}{n!}", Sum(1 / factorial(n), (n, 0, oo))) +] + +UNEVALUATED_PRODUCT_EXPRESSION_PAIRS = [ + (r"\prod_{a = b}^{c} x", Product(x, (a, b, c))), + (r"\prod_{a = b}^c x", Product(x, (a, b, c))), + (r"\prod^{c}_{a = b} x", Product(x, (a, b, c))), + (r"\prod^c_{a = b} x", Product(x, (a, b, c))) +] + +APPLIED_FUNCTION_EXPRESSION_PAIRS = [ + (r"f(x)", f(x)), + (r"f(x, y)", f(x, y)), + (r"f(x, y, z)", f(x, y, z)), + (r"f'_1(x)", Function("f_{1}'")(x)), + (r"f_{1}''(x+y)", Function("f_{1}''")(x + y)), + (r"h_{\theta}(x_0, x_1)", + Function('h_{theta}')(Symbol('x_{0}'), Symbol('x_{1}'))) +] + +UNEVALUATED_COMMON_FUNCTION_EXPRESSION_PAIRS = [ + (r"|x|", _Abs(x)), + (r"||x||", _Abs(Abs(x))), + (r"|x||y|", _Abs(x) * _Abs(y)), + (r"||x||y||", _Abs(_Abs(x) * _Abs(y))), + (r"\lfloor x \rfloor", floor(x)), + (r"\lceil x \rceil", ceiling(x)), + (r"\exp x", _exp(x)), + (r"\exp(x)", _exp(x)), + (r"\lg x", _log(x, 10)), + (r"\ln x", _log(x)), + (r"\ln xy", _log(x * y)), + (r"\log x", _log(x)), + (r"\log xy", _log(x * y)), + (r"\log_{2} x", _log(x, 2)), + (r"\log_{a} x", _log(x, a)), + (r"\log_{11} x", _log(x, 11)), + (r"\log_{a^2} x", _log(x, _Pow(a, 2))), + (r"\log_2 x", _log(x, 2)), + (r"\log_a x", _log(x, a)), + (r"\overline{z}", _Conjugate(z)), + (r"\overline{\overline{z}}", _Conjugate(_Conjugate(z))), + (r"\overline{x + y}", _Conjugate(_Add(x, y))), + (r"\overline{x} + \overline{y}", _Conjugate(x) + _Conjugate(y)), + (r"\min(a, b)", _Min(a, b)), + (r"\min(a, b, c - d, xy)", _Min(a, b, c - d, x * y)), + (r"\max(a, b)", _Max(a, b)), + (r"\max(a, b, c - d, xy)", _Max(a, b, c - d, x * y)), + # physics things don't have an `evaluate=False` variant + (r"\langle x |", Bra('x')), + (r"| x \rangle", Ket('x')), + (r"\langle x | y \rangle", InnerProduct(Bra('x'), Ket('y'))), +] + +EVALUATED_COMMON_FUNCTION_EXPRESSION_PAIRS = [ + (r"|x|", Abs(x)), + (r"||x||", Abs(Abs(x))), + (r"|x||y|", Abs(x) * Abs(y)), + (r"||x||y||", Abs(Abs(x) * Abs(y))), + (r"\lfloor x \rfloor", floor(x)), + (r"\lceil x \rceil", ceiling(x)), + (r"\exp x", exp(x)), + (r"\exp(x)", exp(x)), + (r"\lg x", log(x, 10)), + (r"\ln x", log(x)), + (r"\ln xy", log(x * y)), + (r"\log x", log(x)), + (r"\log xy", log(x * y)), + (r"\log_{2} x", log(x, 2)), + (r"\log_{a} x", log(x, a)), + (r"\log_{11} x", log(x, 11)), + (r"\log_{a^2} x", log(x, _Pow(a, 2))), + (r"\log_2 x", log(x, 2)), + (r"\log_a x", log(x, a)), + (r"\overline{z}", conjugate(z)), + (r"\overline{\overline{z}}", conjugate(conjugate(z))), + (r"\overline{x + y}", conjugate(x + y)), + (r"\overline{x} + \overline{y}", conjugate(x) + conjugate(y)), + (r"\min(a, b)", Min(a, b)), + (r"\min(a, b, c - d, xy)", Min(a, b, c - d, x * y)), + (r"\max(a, b)", Max(a, b)), + (r"\max(a, b, c - d, xy)", Max(a, b, c - d, x * y)), + (r"\langle x |", Bra('x')), + (r"| x \rangle", Ket('x')), + (r"\langle x | y \rangle", InnerProduct(Bra('x'), Ket('y'))), +] + +SPACING_RELATED_EXPRESSION_PAIRS = [ + (r"a \, b", _Mul(a, b)), + (r"a \thinspace b", _Mul(a, b)), + (r"a \: b", _Mul(a, b)), + (r"a \medspace b", _Mul(a, b)), + (r"a \; b", _Mul(a, b)), + (r"a \thickspace b", _Mul(a, b)), + (r"a \quad b", _Mul(a, b)), + (r"a \qquad b", _Mul(a, b)), + (r"a \! b", _Mul(a, b)), + (r"a \negthinspace b", _Mul(a, b)), + (r"a \negmedspace b", _Mul(a, b)), + (r"a \negthickspace b", _Mul(a, b)) +] + +UNEVALUATED_BINOMIAL_EXPRESSION_PAIRS = [ + (r"\binom{n}{k}", _binomial(n, k)), + (r"\tbinom{n}{k}", _binomial(n, k)), + (r"\dbinom{n}{k}", _binomial(n, k)), + (r"\binom{n}{0}", _binomial(n, 0)), + (r"x^\binom{n}{k}", _Pow(x, _binomial(n, k))) +] + +EVALUATED_BINOMIAL_EXPRESSION_PAIRS = [ + (r"\binom{n}{k}", binomial(n, k)), + (r"\tbinom{n}{k}", binomial(n, k)), + (r"\dbinom{n}{k}", binomial(n, k)), + (r"\binom{n}{0}", binomial(n, 0)), + (r"x^\binom{n}{k}", x ** binomial(n, k)) +] + +MISCELLANEOUS_EXPRESSION_PAIRS = [ + (r"\left(x + y\right) z", _Mul(_Add(x, y), z)), + (r"\left( x + y\right ) z", _Mul(_Add(x, y), z)), + (r"\left( x + y\right ) z", _Mul(_Add(x, y), z)), +] + +UNEVALUATED_LITERAL_COMPLEX_NUMBER_EXPRESSION_PAIRS = [ + (r"\imaginaryunit^2", _Pow(I, 2)), + (r"|\imaginaryunit|", _Abs(I)), + (r"\overline{\imaginaryunit}", _Conjugate(I)), + (r"\imaginaryunit+\imaginaryunit", _Add(I, I)), + (r"\imaginaryunit-\imaginaryunit", _Add(I, -I)), + (r"\imaginaryunit*\imaginaryunit", _Mul(I, I)), + (r"\imaginaryunit/\imaginaryunit", _Mul(I, _Pow(I, -1))), + (r"(1+\imaginaryunit)/|1+\imaginaryunit|", _Mul(_Add(1, I), _Pow(_Abs(_Add(1, I)), -1))) +] + +UNEVALUATED_MATRIX_EXPRESSION_PAIRS = [ + (r"\begin{pmatrix}a & b \\x & y\end{pmatrix}", + Matrix([[a, b], [x, y]])), + (r"\begin{pmatrix}a & b \\x & y\\\end{pmatrix}", + Matrix([[a, b], [x, y]])), + (r"\begin{bmatrix}a & b \\x & y\end{bmatrix}", + Matrix([[a, b], [x, y]])), + (r"\left(\begin{matrix}a & b \\x & y\end{matrix}\right)", + Matrix([[a, b], [x, y]])), + (r"\left[\begin{matrix}a & b \\x & y\end{matrix}\right]", + Matrix([[a, b], [x, y]])), + (r"\left[\begin{array}{cc}a & b \\x & y\end{array}\right]", + Matrix([[a, b], [x, y]])), + (r"\left(\begin{array}{cc}a & b \\x & y\end{array}\right)", + Matrix([[a, b], [x, y]])), + (r"\left( { \begin{array}{cc}a & b \\x & y\end{array} } \right)", + Matrix([[a, b], [x, y]])), + (r"+\begin{pmatrix}a & b \\x & y\end{pmatrix}", + Matrix([[a, b], [x, y]])), + ((r"\begin{pmatrix}x & y \\a & b\end{pmatrix}+" + r"\begin{pmatrix}a & b \\x & y\end{pmatrix}"), + _MatAdd(Matrix([[x, y], [a, b]]), Matrix([[a, b], [x, y]]))), + (r"-\begin{pmatrix}a & b \\x & y\end{pmatrix}", + _MatMul(-1, Matrix([[a, b], [x, y]]))), + ((r"\begin{pmatrix}x & y \\a & b\end{pmatrix}-" + r"\begin{pmatrix}a & b \\x & y\end{pmatrix}"), + _MatAdd(Matrix([[x, y], [a, b]]), _MatMul(-1, Matrix([[a, b], [x, y]])))), + ((r"\begin{pmatrix}a & b & c \\x & y & z \\a & b & c \end{pmatrix}*" + r"\begin{pmatrix}x & y & z \\a & b & c \\a & b & c \end{pmatrix}*" + r"\begin{pmatrix}a & b & c \\x & y & z \\x & y & z \end{pmatrix}"), + _MatMul(_MatMul(Matrix([[a, b, c], [x, y, z], [a, b, c]]), + Matrix([[x, y, z], [a, b, c], [a, b, c]])), + Matrix([[a, b, c], [x, y, z], [x, y, z]]))), + (r"\begin{pmatrix}a & b \\x & y\end{pmatrix}/2", + _MatMul(Matrix([[a, b], [x, y]]), _Pow(2, -1))), + (r"\begin{pmatrix}a & b \\x & y\end{pmatrix}^2", + _Pow(Matrix([[a, b], [x, y]]), 2)), + (r"\begin{pmatrix}a & b \\x & y\end{pmatrix}^{-1}", + _Pow(Matrix([[a, b], [x, y]]), -1)), + (r"\begin{pmatrix}a & b \\x & y\end{pmatrix}^T", + Transpose(Matrix([[a, b], [x, y]]))), + (r"\begin{pmatrix}a & b \\x & y\end{pmatrix}^{T}", + Transpose(Matrix([[a, b], [x, y]]))), + (r"\begin{pmatrix}a & b \\x & y\end{pmatrix}^\mathit{T}", + Transpose(Matrix([[a, b], [x, y]]))), + (r"\begin{pmatrix}1 & 2 \\3 & 4\end{pmatrix}^T", + Transpose(Matrix([[1, 2], [3, 4]]))), + ((r"(\begin{pmatrix}1 & 2 \\3 & 4\end{pmatrix}+" + r"\begin{pmatrix}1 & 2 \\3 & 4\end{pmatrix}^T)*" + r"\begin{bmatrix}1\\0\end{bmatrix}"), + _MatMul(_MatAdd(Matrix([[1, 2], [3, 4]]), + Transpose(Matrix([[1, 2], [3, 4]]))), + Matrix([[1], [0]]))), + ((r"(\begin{pmatrix}a & b \\x & y\end{pmatrix}+" + r"\begin{pmatrix}x & y \\a & b\end{pmatrix})^2"), + _Pow(_MatAdd(Matrix([[a, b], [x, y]]), + Matrix([[x, y], [a, b]])), 2)), + ((r"(\begin{pmatrix}a & b \\x & y\end{pmatrix}+" + r"\begin{pmatrix}x & y \\a & b\end{pmatrix})^T"), + Transpose(_MatAdd(Matrix([[a, b], [x, y]]), + Matrix([[x, y], [a, b]])))), + (r"\overline{\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}}", + _Conjugate(_MatAdd(Matrix([[I, 2], [3, 4]]), + Matrix([[I, 2], [3, 4]])))) +] + +EVALUATED_MATRIX_EXPRESSION_PAIRS = [ + (r"\det\left(\left[ { \begin{array}{cc}a&b\\x&y\end{array} } \right]\right)", + Matrix([[a, b], [x, y]]).det()), + (r"\det \begin{pmatrix}1&2\\3&4\end{pmatrix}", -2), + (r"\det{\begin{pmatrix}1&2\\3&4\end{pmatrix}}", -2), + (r"\det(\begin{pmatrix}1&2\\3&4\end{pmatrix})", -2), + (r"\det\left(\begin{pmatrix}1&2\\3&4\end{pmatrix}\right)", -2), + (r"\begin{pmatrix}a & b \\x & y\end{pmatrix}/\begin{vmatrix}a & b \\x & y\end{vmatrix}", + _MatMul(Matrix([[a, b], [x, y]]), _Pow(Matrix([[a, b], [x, y]]).det(), -1))), + (r"\begin{pmatrix}a & b \\x & y\end{pmatrix}/|\begin{matrix}a & b \\x & y\end{matrix}|", + _MatMul(Matrix([[a, b], [x, y]]), _Pow(Matrix([[a, b], [x, y]]).det(), -1))), + (r"\frac{\begin{pmatrix}a & b \\x & y\end{pmatrix}}{| { \begin{matrix}a & b \\x & y\end{matrix} } |}", + _MatMul(Matrix([[a, b], [x, y]]), _Pow(Matrix([[a, b], [x, y]]).det(), -1))), + (r"\overline{\begin{pmatrix}\imaginaryunit & 1+\imaginaryunit \\-\imaginaryunit & 4\end{pmatrix}}", + Matrix([[-I, 1-I], [I, 4]])), + (r"\begin{pmatrix}\imaginaryunit & 1+\imaginaryunit \\-\imaginaryunit & 4\end{pmatrix}^H", + Matrix([[-I, I], [1-I, 4]])), + (r"\trace(\begin{pmatrix}\imaginaryunit & 1+\imaginaryunit \\-\imaginaryunit & 4\end{pmatrix})", + Trace(Matrix([[I, 1+I], [-I, 4]]))), + (r"\adjugate(\begin{pmatrix}1 & 2 \\3 & 4\end{pmatrix})", + Matrix([[4, -2], [-3, 1]])), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^\ast", + Matrix([[-2*I, 6], [4, 8]])), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^{\ast}", + Matrix([[-2*I, 6], [4, 8]])), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^{\ast\ast}", + Matrix([[2*I, 4], [6, 8]])), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^{\ast\ast\ast}", + Matrix([[-2*I, 6], [4, 8]])), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^{*}", + Matrix([[-2*I, 6], [4, 8]])), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^{**}", + Matrix([[2*I, 4], [6, 8]])), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^{***}", + Matrix([[-2*I, 6], [4, 8]])), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^\prime", + Transpose(_MatAdd(Matrix([[I, 2], [3, 4]]), + Matrix([[I, 2], [3, 4]])))), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^{\prime}", + Transpose(_MatAdd(Matrix([[I, 2], [3, 4]]), + Matrix([[I, 2], [3, 4]])))), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^{\prime\prime}", + _MatAdd(Matrix([[I, 2], [3, 4]]), + Matrix([[I, 2], [3, 4]]))), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^{\prime\prime\prime}", + Transpose(_MatAdd(Matrix([[I, 2], [3, 4]]), + Matrix([[I, 2], [3, 4]])))), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^{'}", + Transpose(_MatAdd(Matrix([[I, 2], [3, 4]]), + Matrix([[I, 2], [3, 4]])))), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^{''}", + _MatAdd(Matrix([[I, 2], [3, 4]]), + Matrix([[I, 2], [3, 4]]))), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^{'''}", + Transpose(_MatAdd(Matrix([[I, 2], [3, 4]]), + Matrix([[I, 2], [3, 4]])))), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})'", + Transpose(_MatAdd(Matrix([[I, 2], [3, 4]]), + Matrix([[I, 2], [3, 4]])))), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})''", + _MatAdd(Matrix([[I, 2], [3, 4]]), + Matrix([[I, 2], [3, 4]]))), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})'''", + Transpose(_MatAdd(Matrix([[I, 2], [3, 4]]), + Matrix([[I, 2], [3, 4]])))), + (r"\det(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})", + (_MatAdd(Matrix([[I, 2], [3, 4]]), + Matrix([[I, 2], [3, 4]]))).det()), + (r"\trace(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})", + Trace(_MatAdd(Matrix([[I, 2], [3, 4]]), + Matrix([[I, 2], [3, 4]])))), + (r"\adjugate(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})", + (Matrix([[8, -4], [-6, 2*I]]))), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^T", + Transpose(_MatAdd(Matrix([[I, 2], [3, 4]]), + Matrix([[I, 2], [3, 4]])))), + (r"(\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix}+\begin{pmatrix}\imaginaryunit&2\\3&4\end{pmatrix})^H", + (Matrix([[-2*I, 6], [4, 8]]))) +] + + +def test_symbol_expressions(): + expected_failures = {6, 7} + for i, (latex_str, sympy_expr) in enumerate(SYMBOL_EXPRESSION_PAIRS): + if i in expected_failures: + continue + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_simple_expressions(): + expected_failures = {20} + for i, (latex_str, sympy_expr) in enumerate(UNEVALUATED_SIMPLE_EXPRESSION_PAIRS): + if i in expected_failures: + continue + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + for i, (latex_str, sympy_expr) in enumerate(EVALUATED_SIMPLE_EXPRESSION_PAIRS): + if i in expected_failures: + continue + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_fraction_expressions(): + for latex_str, sympy_expr in UNEVALUATED_FRACTION_EXPRESSION_PAIRS: + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + for latex_str, sympy_expr in EVALUATED_FRACTION_EXPRESSION_PAIRS: + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_relation_expressions(): + for latex_str, sympy_expr in RELATION_EXPRESSION_PAIRS: + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + +def test_power_expressions(): + expected_failures = {3} + for i, (latex_str, sympy_expr) in enumerate(UNEVALUATED_POWER_EXPRESSION_PAIRS): + if i in expected_failures: + continue + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + for i, (latex_str, sympy_expr) in enumerate(EVALUATED_POWER_EXPRESSION_PAIRS): + if i in expected_failures: + continue + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_integral_expressions(): + expected_failures = {14} + for i, (latex_str, sympy_expr) in enumerate(UNEVALUATED_INTEGRAL_EXPRESSION_PAIRS): + if i in expected_failures: + continue + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, i + + for i, (latex_str, sympy_expr) in enumerate(EVALUATED_INTEGRAL_EXPRESSION_PAIRS): + if i in expected_failures: + continue + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_derivative_expressions(): + expected_failures = {3, 4} + for i, (latex_str, sympy_expr) in enumerate(DERIVATIVE_EXPRESSION_PAIRS): + if i in expected_failures: + continue + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + for i, (latex_str, sympy_expr) in enumerate(DERIVATIVE_EXPRESSION_PAIRS): + if i in expected_failures: + continue + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_trigonometric_expressions(): + expected_failures = {3} + for i, (latex_str, sympy_expr) in enumerate(TRIGONOMETRIC_EXPRESSION_PAIRS): + if i in expected_failures: + continue + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_limit_expressions(): + for latex_str, sympy_expr in UNEVALUATED_LIMIT_EXPRESSION_PAIRS: + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_square_root_expressions(): + for latex_str, sympy_expr in UNEVALUATED_SQRT_EXPRESSION_PAIRS: + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + for latex_str, sympy_expr in EVALUATED_SQRT_EXPRESSION_PAIRS: + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_factorial_expressions(): + for latex_str, sympy_expr in UNEVALUATED_FACTORIAL_EXPRESSION_PAIRS: + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + for latex_str, sympy_expr in EVALUATED_FACTORIAL_EXPRESSION_PAIRS: + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_sum_expressions(): + for latex_str, sympy_expr in UNEVALUATED_SUM_EXPRESSION_PAIRS: + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + for latex_str, sympy_expr in EVALUATED_SUM_EXPRESSION_PAIRS: + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_product_expressions(): + for latex_str, sympy_expr in UNEVALUATED_PRODUCT_EXPRESSION_PAIRS: + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + +@XFAIL +def test_applied_function_expressions(): + expected_failures = {0, 3, 4} # 0 is ambiguous, and the others require not-yet-added features + # not sure why 1, and 2 are failing + for i, (latex_str, sympy_expr) in enumerate(APPLIED_FUNCTION_EXPRESSION_PAIRS): + if i in expected_failures: + continue + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_common_function_expressions(): + for latex_str, sympy_expr in UNEVALUATED_COMMON_FUNCTION_EXPRESSION_PAIRS: + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + for latex_str, sympy_expr in EVALUATED_COMMON_FUNCTION_EXPRESSION_PAIRS: + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +# unhandled bug causing these to fail +@XFAIL +def test_spacing(): + for latex_str, sympy_expr in SPACING_RELATED_EXPRESSION_PAIRS: + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_binomial_expressions(): + for latex_str, sympy_expr in UNEVALUATED_BINOMIAL_EXPRESSION_PAIRS: + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + for latex_str, sympy_expr in EVALUATED_BINOMIAL_EXPRESSION_PAIRS: + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_miscellaneous_expressions(): + for latex_str, sympy_expr in MISCELLANEOUS_EXPRESSION_PAIRS: + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_literal_complex_number_expressions(): + for latex_str, sympy_expr in UNEVALUATED_LITERAL_COMPLEX_NUMBER_EXPRESSION_PAIRS: + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + +def test_matrix_expressions(): + for latex_str, sympy_expr in UNEVALUATED_MATRIX_EXPRESSION_PAIRS: + with evaluate(False): + assert parse_latex_lark(latex_str) == sympy_expr, latex_str + + for latex_str, sympy_expr in EVALUATED_MATRIX_EXPRESSION_PAIRS: + assert parse_latex_lark(latex_str) == sympy_expr, latex_str diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_mathematica.py b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_mathematica.py new file mode 100644 index 0000000000000000000000000000000000000000..df193b6d61f9c82778d8e0a40b893cbe6cb8f06a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_mathematica.py @@ -0,0 +1,280 @@ +from sympy import sin, Function, symbols, Dummy, Lambda, cos +from sympy.parsing.mathematica import parse_mathematica, MathematicaParser +from sympy.core.sympify import sympify +from sympy.abc import n, w, x, y, z +from sympy.testing.pytest import raises + + +def test_mathematica(): + d = { + '- 6x': '-6*x', + 'Sin[x]^2': 'sin(x)**2', + '2(x-1)': '2*(x-1)', + '3y+8': '3*y+8', + 'ArcSin[2x+9(4-x)^2]/x': 'asin(2*x+9*(4-x)**2)/x', + 'x+y': 'x+y', + '355/113': '355/113', + '2.718281828': '2.718281828', + 'Cos(1/2 * π)': 'Cos(π/2)', + 'Sin[12]': 'sin(12)', + 'Exp[Log[4]]': 'exp(log(4))', + '(x+1)(x+3)': '(x+1)*(x+3)', + 'Cos[ArcCos[3.6]]': 'cos(acos(3.6))', + 'Cos[x]==Sin[y]': 'Eq(cos(x), sin(y))', + '2*Sin[x+y]': '2*sin(x+y)', + 'Sin[x]+Cos[y]': 'sin(x)+cos(y)', + 'Sin[Cos[x]]': 'sin(cos(x))', + '2*Sqrt[x+y]': '2*sqrt(x+y)', # Test case from the issue 4259 + '+Sqrt[2]': 'sqrt(2)', + '-Sqrt[2]': '-sqrt(2)', + '-1/Sqrt[2]': '-1/sqrt(2)', + '-(1/Sqrt[3])': '-(1/sqrt(3))', + '1/(2*Sqrt[5])': '1/(2*sqrt(5))', + 'Mod[5,3]': 'Mod(5,3)', + '-Mod[5,3]': '-Mod(5,3)', + '(x+1)y': '(x+1)*y', + 'x(y+1)': 'x*(y+1)', + 'Sin[x]Cos[y]': 'sin(x)*cos(y)', + 'Sin[x]^2Cos[y]^2': 'sin(x)**2*cos(y)**2', + 'Cos[x]^2(1 - Cos[y]^2)': 'cos(x)**2*(1-cos(y)**2)', + 'x y': 'x*y', + 'x y': 'x*y', + '2 x': '2*x', + 'x 8': 'x*8', + '2 8': '2*8', + '4.x': '4.*x', + '4. 3': '4.*3', + '4. 3.': '4.*3.', + '1 2 3': '1*2*3', + ' - 2 * Sqrt[ 2 3 * ( 1 + 5 ) ] ': '-2*sqrt(2*3*(1+5))', + 'Log[2,4]': 'log(4,2)', + 'Log[Log[2,4],4]': 'log(4,log(4,2))', + 'Exp[Sqrt[2]^2Log[2, 8]]': 'exp(sqrt(2)**2*log(8,2))', + 'ArcSin[Cos[0]]': 'asin(cos(0))', + 'Log2[16]': 'log(16,2)', + 'Max[1,-2,3,-4]': 'Max(1,-2,3,-4)', + 'Min[1,-2,3]': 'Min(1,-2,3)', + 'Exp[I Pi/2]': 'exp(I*pi/2)', + 'ArcTan[x,y]': 'atan2(y,x)', + 'Pochhammer[x,y]': 'rf(x,y)', + 'ExpIntegralEi[x]': 'Ei(x)', + 'SinIntegral[x]': 'Si(x)', + 'CosIntegral[x]': 'Ci(x)', + 'AiryAi[x]': 'airyai(x)', + 'AiryAiPrime[5]': 'airyaiprime(5)', + 'AiryBi[x]': 'airybi(x)', + 'AiryBiPrime[7]': 'airybiprime(7)', + 'LogIntegral[4]': ' li(4)', + 'PrimePi[7]': 'primepi(7)', + 'Prime[5]': 'prime(5)', + 'PrimeQ[5]': 'isprime(5)', + 'Rational[2,19]': 'Rational(2,19)', # test case for issue 25716 + } + + for e in d: + assert parse_mathematica(e) == sympify(d[e]) + + # The parsed form of this expression should not evaluate the Lambda object: + assert parse_mathematica("Sin[#]^2 + Cos[#]^2 &[x]") == sin(x)**2 + cos(x)**2 + + d1, d2, d3 = symbols("d1:4", cls=Dummy) + assert parse_mathematica("Sin[#] + Cos[#3] &").dummy_eq(Lambda((d1, d2, d3), sin(d1) + cos(d3))) + assert parse_mathematica("Sin[#^2] &").dummy_eq(Lambda(d1, sin(d1**2))) + assert parse_mathematica("Function[x, x^3]") == Lambda(x, x**3) + assert parse_mathematica("Function[{x, y}, x^2 + y^2]") == Lambda((x, y), x**2 + y**2) + + +def test_parser_mathematica_tokenizer(): + parser = MathematicaParser() + + chain = lambda expr: parser._from_tokens_to_fullformlist(parser._from_mathematica_to_tokens(expr)) + + # Basic patterns + assert chain("x") == "x" + assert chain("42") == "42" + assert chain(".2") == ".2" + assert chain("+x") == "x" + assert chain("-1") == "-1" + assert chain("- 3") == "-3" + assert chain("α") == "α" + assert chain("+Sin[x]") == ["Sin", "x"] + assert chain("-Sin[x]") == ["Times", "-1", ["Sin", "x"]] + assert chain("x(a+1)") == ["Times", "x", ["Plus", "a", "1"]] + assert chain("(x)") == "x" + assert chain("(+x)") == "x" + assert chain("-a") == ["Times", "-1", "a"] + assert chain("(-x)") == ["Times", "-1", "x"] + assert chain("(x + y)") == ["Plus", "x", "y"] + assert chain("3 + 4") == ["Plus", "3", "4"] + assert chain("a - 3") == ["Plus", "a", "-3"] + assert chain("a - b") == ["Plus", "a", ["Times", "-1", "b"]] + assert chain("7 * 8") == ["Times", "7", "8"] + assert chain("a + b*c") == ["Plus", "a", ["Times", "b", "c"]] + assert chain("a + b* c* d + 2 * e") == ["Plus", "a", ["Times", "b", "c", "d"], ["Times", "2", "e"]] + assert chain("a / b") == ["Times", "a", ["Power", "b", "-1"]] + + # Missing asterisk (*) patterns: + assert chain("x y") == ["Times", "x", "y"] + assert chain("3 4") == ["Times", "3", "4"] + assert chain("a[b] c") == ["Times", ["a", "b"], "c"] + assert chain("(x) (y)") == ["Times", "x", "y"] + assert chain("3 (a)") == ["Times", "3", "a"] + assert chain("(a) b") == ["Times", "a", "b"] + assert chain("4.2") == "4.2" + assert chain("4 2") == ["Times", "4", "2"] + assert chain("4 2") == ["Times", "4", "2"] + assert chain("3 . 4") == ["Dot", "3", "4"] + assert chain("4. 2") == ["Times", "4.", "2"] + assert chain("x.y") == ["Dot", "x", "y"] + assert chain("4.y") == ["Times", "4.", "y"] + assert chain("4 .y") == ["Dot", "4", "y"] + assert chain("x.4") == ["Times", "x", ".4"] + assert chain("x0.3") == ["Times", "x0", ".3"] + assert chain("x. 4") == ["Dot", "x", "4"] + + # Comments + assert chain("a (* +b *) + c") == ["Plus", "a", "c"] + assert chain("a (* + b *) + (**)c (* +d *) + e") == ["Plus", "a", "c", "e"] + assert chain("""a + (* + + b + *) c + (* d + *) e + """) == ["Plus", "a", "c", "e"] + + # Operators couples + and -, * and / are mutually associative: + # (i.e. expression gets flattened when mixing these operators) + assert chain("a*b/c") == ["Times", "a", "b", ["Power", "c", "-1"]] + assert chain("a/b*c") == ["Times", "a", ["Power", "b", "-1"], "c"] + assert chain("a+b-c") == ["Plus", "a", "b", ["Times", "-1", "c"]] + assert chain("a-b+c") == ["Plus", "a", ["Times", "-1", "b"], "c"] + assert chain("-a + b -c ") == ["Plus", ["Times", "-1", "a"], "b", ["Times", "-1", "c"]] + assert chain("a/b/c*d") == ["Times", "a", ["Power", "b", "-1"], ["Power", "c", "-1"], "d"] + assert chain("a/b/c") == ["Times", "a", ["Power", "b", "-1"], ["Power", "c", "-1"]] + assert chain("a-b-c") == ["Plus", "a", ["Times", "-1", "b"], ["Times", "-1", "c"]] + assert chain("1/a") == ["Times", "1", ["Power", "a", "-1"]] + assert chain("1/a/b") == ["Times", "1", ["Power", "a", "-1"], ["Power", "b", "-1"]] + assert chain("-1/a*b") == ["Times", "-1", ["Power", "a", "-1"], "b"] + + # Enclosures of various kinds, i.e. ( ) [ ] [[ ]] { } + assert chain("(a + b) + c") == ["Plus", ["Plus", "a", "b"], "c"] + assert chain(" a + (b + c) + d ") == ["Plus", "a", ["Plus", "b", "c"], "d"] + assert chain("a * (b + c)") == ["Times", "a", ["Plus", "b", "c"]] + assert chain("a b (c d)") == ["Times", "a", "b", ["Times", "c", "d"]] + assert chain("{a, b, 2, c}") == ["List", "a", "b", "2", "c"] + assert chain("{a, {b, c}}") == ["List", "a", ["List", "b", "c"]] + assert chain("{{a}}") == ["List", ["List", "a"]] + assert chain("a[b, c]") == ["a", "b", "c"] + assert chain("a[[b, c]]") == ["Part", "a", "b", "c"] + assert chain("a[b[c]]") == ["a", ["b", "c"]] + assert chain("a[[b, c[[d, {e,f}]]]]") == ["Part", "a", "b", ["Part", "c", "d", ["List", "e", "f"]]] + assert chain("a[b[[c,d]]]") == ["a", ["Part", "b", "c", "d"]] + assert chain("a[[b[c]]]") == ["Part", "a", ["b", "c"]] + assert chain("a[[b[[c]]]]") == ["Part", "a", ["Part", "b", "c"]] + assert chain("a[[b[c[[d]]]]]") == ["Part", "a", ["b", ["Part", "c", "d"]]] + assert chain("a[b[[c[d]]]]") == ["a", ["Part", "b", ["c", "d"]]] + assert chain("x[[a+1, b+2, c+3]]") == ["Part", "x", ["Plus", "a", "1"], ["Plus", "b", "2"], ["Plus", "c", "3"]] + assert chain("x[a+1, b+2, c+3]") == ["x", ["Plus", "a", "1"], ["Plus", "b", "2"], ["Plus", "c", "3"]] + assert chain("{a+1, b+2, c+3}") == ["List", ["Plus", "a", "1"], ["Plus", "b", "2"], ["Plus", "c", "3"]] + + # Flat operator: + assert chain("a*b*c*d*e") == ["Times", "a", "b", "c", "d", "e"] + assert chain("a +b + c+ d+e") == ["Plus", "a", "b", "c", "d", "e"] + + # Right priority operator: + assert chain("a^b") == ["Power", "a", "b"] + assert chain("a^b^c") == ["Power", "a", ["Power", "b", "c"]] + assert chain("a^b^c^d") == ["Power", "a", ["Power", "b", ["Power", "c", "d"]]] + + # Left priority operator: + assert chain("a/.b") == ["ReplaceAll", "a", "b"] + assert chain("a/.b/.c/.d") == ["ReplaceAll", ["ReplaceAll", ["ReplaceAll", "a", "b"], "c"], "d"] + + assert chain("a//b") == ["a", "b"] + assert chain("a//b//c") == [["a", "b"], "c"] + assert chain("a//b//c//d") == [[["a", "b"], "c"], "d"] + + # Compound expressions + assert chain("a;b") == ["CompoundExpression", "a", "b"] + assert chain("a;") == ["CompoundExpression", "a", "Null"] + assert chain("a;b;") == ["CompoundExpression", "a", "b", "Null"] + assert chain("a[b;c]") == ["a", ["CompoundExpression", "b", "c"]] + assert chain("a[b,c;d,e]") == ["a", "b", ["CompoundExpression", "c", "d"], "e"] + assert chain("a[b,c;,d]") == ["a", "b", ["CompoundExpression", "c", "Null"], "d"] + + # New lines + assert chain("a\nb\n") == ["CompoundExpression", "a", "b"] + assert chain("a\n\nb\n (c \nd) \n") == ["CompoundExpression", "a", "b", ["Times", "c", "d"]] + assert chain("\na; b\nc") == ["CompoundExpression", "a", "b", "c"] + assert chain("a + \nb\n") == ["Plus", "a", "b"] + assert chain("a\nb; c; d\n e; (f \n g); h + \n i") == ["CompoundExpression", "a", "b", "c", "d", "e", ["Times", "f", "g"], ["Plus", "h", "i"]] + assert chain("\n{\na\nb; c; d\n e (f \n g); h + \n i\n\n}\n") == ["List", ["CompoundExpression", ["Times", "a", "b"], "c", ["Times", "d", "e", ["Times", "f", "g"]], ["Plus", "h", "i"]]] + + # Patterns + assert chain("y_") == ["Pattern", "y", ["Blank"]] + assert chain("y_.") == ["Optional", ["Pattern", "y", ["Blank"]]] + assert chain("y__") == ["Pattern", "y", ["BlankSequence"]] + assert chain("y___") == ["Pattern", "y", ["BlankNullSequence"]] + assert chain("a[b_.,c_]") == ["a", ["Optional", ["Pattern", "b", ["Blank"]]], ["Pattern", "c", ["Blank"]]] + assert chain("b_. c") == ["Times", ["Optional", ["Pattern", "b", ["Blank"]]], "c"] + + # Slots for lambda functions + assert chain("#") == ["Slot", "1"] + assert chain("#3") == ["Slot", "3"] + assert chain("#n") == ["Slot", "n"] + assert chain("##") == ["SlotSequence", "1"] + assert chain("##a") == ["SlotSequence", "a"] + + # Lambda functions + assert chain("x&") == ["Function", "x"] + assert chain("#&") == ["Function", ["Slot", "1"]] + assert chain("#+3&") == ["Function", ["Plus", ["Slot", "1"], "3"]] + assert chain("#1 + #2&") == ["Function", ["Plus", ["Slot", "1"], ["Slot", "2"]]] + assert chain("# + #&") == ["Function", ["Plus", ["Slot", "1"], ["Slot", "1"]]] + assert chain("#&[x]") == [["Function", ["Slot", "1"]], "x"] + assert chain("#1 + #2 & [x, y]") == [["Function", ["Plus", ["Slot", "1"], ["Slot", "2"]]], "x", "y"] + assert chain("#1^2#2^3&") == ["Function", ["Times", ["Power", ["Slot", "1"], "2"], ["Power", ["Slot", "2"], "3"]]] + + # Strings inside Mathematica expressions: + assert chain('"abc"') == ["_Str", "abc"] + assert chain('"a\\"b"') == ["_Str", 'a"b'] + # This expression does not make sense mathematically, it's just testing the parser: + assert chain('x + "abc" ^ 3') == ["Plus", "x", ["Power", ["_Str", "abc"], "3"]] + assert chain('"a (* b *) c"') == ["_Str", "a (* b *) c"] + assert chain('"a" (* b *) ') == ["_Str", "a"] + assert chain('"a [ b] "') == ["_Str", "a [ b] "] + raises(SyntaxError, lambda: chain('"')) + raises(SyntaxError, lambda: chain('"\\"')) + raises(SyntaxError, lambda: chain('"abc')) + raises(SyntaxError, lambda: chain('"abc\\"def')) + + # Invalid expressions: + raises(SyntaxError, lambda: chain("(,")) + raises(SyntaxError, lambda: chain("()")) + raises(SyntaxError, lambda: chain("a (* b")) + + +def test_parser_mathematica_exp_alt(): + parser = MathematicaParser() + + convert_chain2 = lambda expr: parser._from_fullformlist_to_fullformsympy(parser._from_fullform_to_fullformlist(expr)) + convert_chain3 = lambda expr: parser._from_fullformsympy_to_sympy(convert_chain2(expr)) + + Sin, Times, Plus, Power = symbols("Sin Times Plus Power", cls=Function) + + full_form1 = "Sin[Times[x, y]]" + full_form2 = "Plus[Times[x, y], z]" + full_form3 = "Sin[Times[x, Plus[y, z], Power[w, n]]]]" + full_form4 = "Rational[Rational[x, y], z]" + + assert parser._from_fullform_to_fullformlist(full_form1) == ["Sin", ["Times", "x", "y"]] + assert parser._from_fullform_to_fullformlist(full_form2) == ["Plus", ["Times", "x", "y"], "z"] + assert parser._from_fullform_to_fullformlist(full_form3) == ["Sin", ["Times", "x", ["Plus", "y", "z"], ["Power", "w", "n"]]] + assert parser._from_fullform_to_fullformlist(full_form4) == ["Rational", ["Rational", "x", "y"], "z"] + + assert convert_chain2(full_form1) == Sin(Times(x, y)) + assert convert_chain2(full_form2) == Plus(Times(x, y), z) + assert convert_chain2(full_form3) == Sin(Times(x, Plus(y, z), Power(w, n))) + + assert convert_chain3(full_form1) == sin(x*y) + assert convert_chain3(full_form2) == x*y + z + assert convert_chain3(full_form3) == sin(x*(y + z)*w**n) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_maxima.py b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_maxima.py new file mode 100644 index 0000000000000000000000000000000000000000..c0bc1db8f1385ed52e8c677a1bcc759f5118d01e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_maxima.py @@ -0,0 +1,50 @@ +from sympy.parsing.maxima import parse_maxima +from sympy.core.numbers import (E, Rational, oo) +from sympy.core.symbol import Symbol +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.abc import x + +n = Symbol('n', integer=True) + + +def test_parser(): + assert Abs(parse_maxima('float(1/3)') - 0.333333333) < 10**(-5) + assert parse_maxima('13^26') == 91733330193268616658399616009 + assert parse_maxima('sin(%pi/2) + cos(%pi/3)') == Rational(3, 2) + assert parse_maxima('log(%e)') == 1 + + +def test_injection(): + parse_maxima('c: x+1', globals=globals()) + # c created by parse_maxima + assert c == x + 1 # noqa:F821 + + parse_maxima('g: sqrt(81)', globals=globals()) + # g created by parse_maxima + assert g == 9 # noqa:F821 + + +def test_maxima_functions(): + assert parse_maxima('expand( (x+1)^2)') == x**2 + 2*x + 1 + assert parse_maxima('factor( x**2 + 2*x + 1)') == (x + 1)**2 + assert parse_maxima('2*cos(x)^2 + sin(x)^2') == 2*cos(x)**2 + sin(x)**2 + assert parse_maxima('trigexpand(sin(2*x)+cos(2*x))') == \ + -1 + 2*cos(x)**2 + 2*cos(x)*sin(x) + assert parse_maxima('solve(x^2-4,x)') == [-2, 2] + assert parse_maxima('limit((1+1/x)^x,x,inf)') == E + assert parse_maxima('limit(sqrt(-x)/x,x,0,minus)') is -oo + assert parse_maxima('diff(x^x, x)') == x**x*(1 + log(x)) + assert parse_maxima('sum(k, k, 1, n)', name_dict={ + "n": Symbol('n', integer=True), + "k": Symbol('k', integer=True) + }) == (n**2 + n)/2 + assert parse_maxima('product(k, k, 1, n)', name_dict={ + "n": Symbol('n', integer=True), + "k": Symbol('k', integer=True) + }) == factorial(n) + assert parse_maxima('ratsimp((x^2-1)/(x+1))') == x - 1 + assert Abs( parse_maxima( + 'float(sec(%pi/3) + csc(%pi/3))') - 3.154700538379252) < 10**(-5) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_sym_expr.py b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_sym_expr.py new file mode 100644 index 0000000000000000000000000000000000000000..99912805db381b96e7f41a348fe6f90d71adf781 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_sym_expr.py @@ -0,0 +1,209 @@ +from sympy.parsing.sym_expr import SymPyExpression +from sympy.testing.pytest import raises +from sympy.external import import_module + +lfortran = import_module('lfortran') +cin = import_module('clang.cindex', import_kwargs = {'fromlist': ['cindex']}) + +if lfortran and cin: + from sympy.codegen.ast import (Variable, IntBaseType, FloatBaseType, String, + Declaration, FloatType) + from sympy.core import Integer, Float + from sympy.core.symbol import Symbol + + expr1 = SymPyExpression() + src = """\ + integer :: a, b, c, d + real :: p, q, r, s + """ + + def test_c_parse(): + src1 = """\ + int a, b = 4; + float c, d = 2.4; + """ + expr1.convert_to_expr(src1, 'c') + ls = expr1.return_expr() + + assert ls[0] == Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('intc')) + ) + ) + assert ls[1] == Declaration( + Variable( + Symbol('b'), + type=IntBaseType(String('intc')), + value=Integer(4) + ) + ) + assert ls[2] == Declaration( + Variable( + Symbol('c'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ) + ) + ) + assert ls[3] == Declaration( + Variable( + Symbol('d'), + type=FloatType( + String('float32'), + nbits=Integer(32), + nmant=Integer(23), + nexp=Integer(8) + ), + value=Float('2.3999999999999999', precision=53) + ) + ) + + + def test_fortran_parse(): + expr = SymPyExpression(src, 'f') + ls = expr.return_expr() + + assert ls[0] == Declaration( + Variable( + Symbol('a'), + type=IntBaseType(String('integer')), + value=Integer(0) + ) + ) + assert ls[1] == Declaration( + Variable( + Symbol('b'), + type=IntBaseType(String('integer')), + value=Integer(0) + ) + ) + assert ls[2] == Declaration( + Variable( + Symbol('c'), + type=IntBaseType(String('integer')), + value=Integer(0) + ) + ) + assert ls[3] == Declaration( + Variable( + Symbol('d'), + type=IntBaseType(String('integer')), + value=Integer(0) + ) + ) + assert ls[4] == Declaration( + Variable( + Symbol('p'), + type=FloatBaseType(String('real')), + value=Float('0.0', precision=53) + ) + ) + assert ls[5] == Declaration( + Variable( + Symbol('q'), + type=FloatBaseType(String('real')), + value=Float('0.0', precision=53) + ) + ) + assert ls[6] == Declaration( + Variable( + Symbol('r'), + type=FloatBaseType(String('real')), + value=Float('0.0', precision=53) + ) + ) + assert ls[7] == Declaration( + Variable( + Symbol('s'), + type=FloatBaseType(String('real')), + value=Float('0.0', precision=53) + ) + ) + + + def test_convert_py(): + src1 = ( + src + + """\ + a = b + c + s = p * q / r + """ + ) + expr1.convert_to_expr(src1, 'f') + exp_py = expr1.convert_to_python() + assert exp_py == [ + 'a = 0', + 'b = 0', + 'c = 0', + 'd = 0', + 'p = 0.0', + 'q = 0.0', + 'r = 0.0', + 's = 0.0', + 'a = b + c', + 's = p*q/r' + ] + + + def test_convert_fort(): + src1 = ( + src + + """\ + a = b + c + s = p * q / r + """ + ) + expr1.convert_to_expr(src1, 'f') + exp_fort = expr1.convert_to_fortran() + assert exp_fort == [ + ' integer*4 a', + ' integer*4 b', + ' integer*4 c', + ' integer*4 d', + ' real*8 p', + ' real*8 q', + ' real*8 r', + ' real*8 s', + ' a = b + c', + ' s = p*q/r' + ] + + + def test_convert_c(): + src1 = ( + src + + """\ + a = b + c + s = p * q / r + """ + ) + expr1.convert_to_expr(src1, 'f') + exp_c = expr1.convert_to_c() + assert exp_c == [ + 'int a = 0', + 'int b = 0', + 'int c = 0', + 'int d = 0', + 'double p = 0.0', + 'double q = 0.0', + 'double r = 0.0', + 'double s = 0.0', + 'a = b + c;', + 's = p*q/r;' + ] + + + def test_exceptions(): + src = 'int a;' + raises(ValueError, lambda: SymPyExpression(src)) + raises(ValueError, lambda: SymPyExpression(mode = 'c')) + raises(NotImplementedError, lambda: SymPyExpression(src, mode = 'd')) + +elif not lfortran and not cin: + def test_raise(): + raises(ImportError, lambda: SymPyExpression('int a;', 'c')) + raises(ImportError, lambda: SymPyExpression('integer :: a', 'f')) diff --git a/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_sympy_parser.py b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_sympy_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..43ecccbe262ffb4093248d891aa7423c8f62c628 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/parsing/tests/test_sympy_parser.py @@ -0,0 +1,371 @@ +# -*- coding: utf-8 -*- + + +import builtins +import types + +from sympy.assumptions import Q +from sympy.core import Symbol, Function, Float, Rational, Integer, I, Mul, Pow, Eq, Lt, Le, Gt, Ge, Ne +from sympy.functions import exp, factorial, factorial2, sin, Min, Max +from sympy.logic import And +from sympy.series import Limit +from sympy.testing.pytest import raises + +from sympy.parsing.sympy_parser import ( + parse_expr, standard_transformations, rationalize, TokenError, + split_symbols, implicit_multiplication, convert_equals_signs, + convert_xor, function_exponentiation, lambda_notation, auto_symbol, + repeated_decimals, implicit_multiplication_application, + auto_number, factorial_notation, implicit_application, + _transformation, T + ) + + +def test_sympy_parser(): + x = Symbol('x') + inputs = { + '2*x': 2 * x, + '3.00': Float(3), + '22/7': Rational(22, 7), + '2+3j': 2 + 3*I, + 'exp(x)': exp(x), + 'x!': factorial(x), + 'x!!': factorial2(x), + '(x + 1)! - 1': factorial(x + 1) - 1, + '3.[3]': Rational(10, 3), + '.0[3]': Rational(1, 30), + '3.2[3]': Rational(97, 30), + '1.3[12]': Rational(433, 330), + '1 + 3.[3]': Rational(13, 3), + '1 + .0[3]': Rational(31, 30), + '1 + 3.2[3]': Rational(127, 30), + '.[0011]': Rational(1, 909), + '0.1[00102] + 1': Rational(366697, 333330), + '1.[0191]': Rational(10190, 9999), + '10!': 3628800, + '-(2)': -Integer(2), + '[-1, -2, 3]': [Integer(-1), Integer(-2), Integer(3)], + 'Symbol("x").free_symbols': x.free_symbols, + "S('S(3).n(n=3)')": Float(3, 3), + 'factorint(12, visual=True)': Mul( + Pow(2, 2, evaluate=False), + Pow(3, 1, evaluate=False), + evaluate=False), + 'Limit(sin(x), x, 0, dir="-")': Limit(sin(x), x, 0, dir='-'), + 'Q.even(x)': Q.even(x), + + + } + for text, result in inputs.items(): + assert parse_expr(text) == result + + raises(TypeError, lambda: + parse_expr('x', standard_transformations)) + raises(TypeError, lambda: + parse_expr('x', transformations=lambda x,y: 1)) + raises(TypeError, lambda: + parse_expr('x', transformations=(lambda x,y: 1,))) + raises(TypeError, lambda: parse_expr('x', transformations=((),))) + raises(TypeError, lambda: parse_expr('x', {}, [], [])) + raises(TypeError, lambda: parse_expr('x', [], [], {})) + raises(TypeError, lambda: parse_expr('x', [], [], {})) + + +def test_rationalize(): + inputs = { + '0.123': Rational(123, 1000) + } + transformations = standard_transformations + (rationalize,) + for text, result in inputs.items(): + assert parse_expr(text, transformations=transformations) == result + + +def test_factorial_fail(): + inputs = ['x!!!', 'x!!!!', '(!)'] + + + for text in inputs: + try: + parse_expr(text) + assert False + except TokenError: + assert True + + +def test_repeated_fail(): + inputs = ['1[1]', '.1e1[1]', '0x1[1]', '1.1j[1]', '1.1[1 + 1]', + '0.1[[1]]', '0x1.1[1]'] + + + # All are valid Python, so only raise TypeError for invalid indexing + for text in inputs: + raises(TypeError, lambda: parse_expr(text)) + + + inputs = ['0.1[', '0.1[1', '0.1[]'] + for text in inputs: + raises((TokenError, SyntaxError), lambda: parse_expr(text)) + + +def test_repeated_dot_only(): + assert parse_expr('.[1]') == Rational(1, 9) + assert parse_expr('1 + .[1]') == Rational(10, 9) + + +def test_local_dict(): + local_dict = { + 'my_function': lambda x: x + 2 + } + inputs = { + 'my_function(2)': Integer(4) + } + for text, result in inputs.items(): + assert parse_expr(text, local_dict=local_dict) == result + + +def test_local_dict_split_implmult(): + t = standard_transformations + (split_symbols, implicit_multiplication,) + w = Symbol('w', real=True) + y = Symbol('y') + assert parse_expr('yx', local_dict={'x':w}, transformations=t) == y*w + + +def test_local_dict_symbol_to_fcn(): + x = Symbol('x') + d = {'foo': Function('bar')} + assert parse_expr('foo(x)', local_dict=d) == d['foo'](x) + d = {'foo': Symbol('baz')} + raises(TypeError, lambda: parse_expr('foo(x)', local_dict=d)) + + +def test_global_dict(): + global_dict = { + 'Symbol': Symbol + } + inputs = { + 'Q & S': And(Symbol('Q'), Symbol('S')) + } + for text, result in inputs.items(): + assert parse_expr(text, global_dict=global_dict) == result + + +def test_no_globals(): + + # Replicate creating the default global_dict: + default_globals = {} + exec('from sympy import *', default_globals) + builtins_dict = vars(builtins) + for name, obj in builtins_dict.items(): + if isinstance(obj, types.BuiltinFunctionType): + default_globals[name] = obj + default_globals['max'] = Max + default_globals['min'] = Min + + # Need to include Symbol or parse_expr will not work: + default_globals.pop('Symbol') + global_dict = {'Symbol':Symbol} + + for name in default_globals: + obj = parse_expr(name, global_dict=global_dict) + assert obj == Symbol(name) + + +def test_issue_2515(): + raises(TokenError, lambda: parse_expr('(()')) + raises(TokenError, lambda: parse_expr('"""')) + + +def test_issue_7663(): + x = Symbol('x') + e = '2*(x+1)' + assert parse_expr(e, evaluate=False) == parse_expr(e, evaluate=False) + assert parse_expr(e, evaluate=False).equals(2*(x+1)) + +def test_recursive_evaluate_false_10560(): + inputs = { + '4*-3' : '4*-3', + '-4*3' : '(-4)*3', + "-2*x*y": '(-2)*x*y', + "x*-4*x": "x*(-4)*x" + } + for text, result in inputs.items(): + assert parse_expr(text, evaluate=False) == parse_expr(result, evaluate=False) + + +def test_function_evaluate_false(): + inputs = [ + 'Abs(0)', 'im(0)', 're(0)', 'sign(0)', 'arg(0)', 'conjugate(0)', + 'acos(0)', 'acot(0)', 'acsc(0)', 'asec(0)', 'asin(0)', 'atan(0)', + 'acosh(0)', 'acoth(0)', 'acsch(0)', 'asech(0)', 'asinh(0)', 'atanh(0)', + 'cos(0)', 'cot(0)', 'csc(0)', 'sec(0)', 'sin(0)', 'tan(0)', + 'cosh(0)', 'coth(0)', 'csch(0)', 'sech(0)', 'sinh(0)', 'tanh(0)', + 'exp(0)', 'log(0)', 'sqrt(0)', + ] + for case in inputs: + expr = parse_expr(case, evaluate=False) + assert case == str(expr) != str(expr.doit()) + assert str(parse_expr('ln(0)', evaluate=False)) == 'log(0)' + assert str(parse_expr('cbrt(0)', evaluate=False)) == '0**(1/3)' + + +def test_issue_10773(): + inputs = { + '-10/5': '(-10)/5', + '-10/-5' : '(-10)/(-5)', + } + for text, result in inputs.items(): + assert parse_expr(text, evaluate=False) == parse_expr(result, evaluate=False) + + +def test_split_symbols(): + transformations = standard_transformations + \ + (split_symbols, implicit_multiplication,) + x = Symbol('x') + y = Symbol('y') + xy = Symbol('xy') + + + assert parse_expr("xy") == xy + assert parse_expr("xy", transformations=transformations) == x*y + + +def test_split_symbols_function(): + transformations = standard_transformations + \ + (split_symbols, implicit_multiplication,) + x = Symbol('x') + y = Symbol('y') + a = Symbol('a') + f = Function('f') + + + assert parse_expr("ay(x+1)", transformations=transformations) == a*y*(x+1) + assert parse_expr("af(x+1)", transformations=transformations, + local_dict={'f':f}) == a*f(x+1) + + +def test_functional_exponent(): + t = standard_transformations + (convert_xor, function_exponentiation) + x = Symbol('x') + y = Symbol('y') + a = Symbol('a') + yfcn = Function('y') + assert parse_expr("sin^2(x)", transformations=t) == (sin(x))**2 + assert parse_expr("sin^y(x)", transformations=t) == (sin(x))**y + assert parse_expr("exp^y(x)", transformations=t) == (exp(x))**y + assert parse_expr("E^y(x)", transformations=t) == exp(yfcn(x)) + assert parse_expr("a^y(x)", transformations=t) == a**(yfcn(x)) + + +def test_match_parentheses_implicit_multiplication(): + transformations = standard_transformations + \ + (implicit_multiplication,) + raises(TokenError, lambda: parse_expr('(1,2),(3,4]',transformations=transformations)) + + +def test_convert_equals_signs(): + transformations = standard_transformations + \ + (convert_equals_signs, ) + x = Symbol('x') + y = Symbol('y') + assert parse_expr("1*2=x", transformations=transformations) == Eq(2, x) + assert parse_expr("y = x", transformations=transformations) == Eq(y, x) + assert parse_expr("(2*y = x) = False", + transformations=transformations) == Eq(Eq(2*y, x), False) + + +def test_parse_function_issue_3539(): + x = Symbol('x') + f = Function('f') + assert parse_expr('f(x)') == f(x) + +def test_issue_24288(): + assert parse_expr("1 < 2", evaluate=False) == Lt(1, 2, evaluate=False) + assert parse_expr("1 <= 2", evaluate=False) == Le(1, 2, evaluate=False) + assert parse_expr("1 > 2", evaluate=False) == Gt(1, 2, evaluate=False) + assert parse_expr("1 >= 2", evaluate=False) == Ge(1, 2, evaluate=False) + assert parse_expr("1 != 2", evaluate=False) == Ne(1, 2, evaluate=False) + assert parse_expr("1 == 2", evaluate=False) == Eq(1, 2, evaluate=False) + assert parse_expr("1 < 2 < 3", evaluate=False) == And(Lt(1, 2, evaluate=False), Lt(2, 3, evaluate=False), evaluate=False) + assert parse_expr("1 <= 2 <= 3", evaluate=False) == And(Le(1, 2, evaluate=False), Le(2, 3, evaluate=False), evaluate=False) + assert parse_expr("1 < 2 <= 3 < 4", evaluate=False) == \ + And(Lt(1, 2, evaluate=False), Le(2, 3, evaluate=False), Lt(3, 4, evaluate=False), evaluate=False) + # Valid Python relational operators that SymPy does not decide how to handle them yet + raises(ValueError, lambda: parse_expr("1 in 2", evaluate=False)) + raises(ValueError, lambda: parse_expr("1 is 2", evaluate=False)) + raises(ValueError, lambda: parse_expr("1 not in 2", evaluate=False)) + raises(ValueError, lambda: parse_expr("1 is not 2", evaluate=False)) + +def test_split_symbols_numeric(): + transformations = ( + standard_transformations + + (implicit_multiplication_application,)) + + n = Symbol('n') + expr1 = parse_expr('2**n * 3**n') + expr2 = parse_expr('2**n3**n', transformations=transformations) + assert expr1 == expr2 == 2**n*3**n + + expr1 = parse_expr('n12n34', transformations=transformations) + assert expr1 == n*12*n*34 + + +def test_unicode_names(): + assert parse_expr('α') == Symbol('α') + + +def test_python3_features(): + assert parse_expr("123_456") == 123456 + assert parse_expr("1.2[3_4]") == parse_expr("1.2[34]") == Rational(611, 495) + assert parse_expr("1.2[012_012]") == parse_expr("1.2[012012]") == Rational(400, 333) + assert parse_expr('.[3_4]') == parse_expr('.[34]') == Rational(34, 99) + assert parse_expr('.1[3_4]') == parse_expr('.1[34]') == Rational(133, 990) + assert parse_expr('123_123.123_123[3_4]') == parse_expr('123123.123123[34]') == Rational(12189189189211, 99000000) + + +def test_issue_19501(): + x = Symbol('x') + eq = parse_expr('E**x(1+x)', local_dict={'x': x}, transformations=( + standard_transformations + + (implicit_multiplication_application,))) + assert eq.free_symbols == {x} + + +def test_parsing_definitions(): + from sympy.abc import x + assert len(_transformation) == 12 # if this changes, extend below + assert _transformation[0] == lambda_notation + assert _transformation[1] == auto_symbol + assert _transformation[2] == repeated_decimals + assert _transformation[3] == auto_number + assert _transformation[4] == factorial_notation + assert _transformation[5] == implicit_multiplication_application + assert _transformation[6] == convert_xor + assert _transformation[7] == implicit_application + assert _transformation[8] == implicit_multiplication + assert _transformation[9] == convert_equals_signs + assert _transformation[10] == function_exponentiation + assert _transformation[11] == rationalize + assert T[:5] == T[0,1,2,3,4] == standard_transformations + t = _transformation + assert T[-1, 0] == (t[len(t) - 1], t[0]) + assert T[:5, 8] == standard_transformations + (t[8],) + assert parse_expr('0.3x^2', transformations='all') == 3*x**2/10 + assert parse_expr('sin 3x', transformations='implicit') == sin(3*x) + + +def test_builtins(): + cases = [ + ('abs(x)', 'Abs(x)'), + ('max(x, y)', 'Max(x, y)'), + ('min(x, y)', 'Min(x, y)'), + ('pow(x, y)', 'Pow(x, y)'), + ] + for built_in_func_call, sympy_func_call in cases: + assert parse_expr(built_in_func_call) == parse_expr(sympy_func_call) + assert str(parse_expr('pow(38, -1, 97)')) == '23' + + +def test_issue_22822(): + raises(ValueError, lambda: parse_expr('x', {'': 1})) + data = {'some_parameter': None} + assert parse_expr('some_parameter is None', data) is True diff --git a/venv/lib/python3.11/site-packages/sympy/physics/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..60989896ae8b3f69efc7d2350add8f6f19d85669 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/__init__.py @@ -0,0 +1,12 @@ +""" +A module that helps solving problems in physics. +""" + +from . import units +from .matrices import mgamma, msigma, minkowski_tensor, mdft + +__all__ = [ + 'units', + + 'mgamma', 'msigma', 'minkowski_tensor', 'mdft', +] diff --git a/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3e0f687cc23c1862b65e55117841cfd7d2b8e3f0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/__init__.py @@ -0,0 +1,53 @@ +"""Biomechanics extension for SymPy. + +Includes biomechanics-related constructs which allows users to extend multibody +models created using `sympy.physics.mechanics` into biomechanical or +musculoskeletal models involding musculotendons and activation dynamics. + +""" + +from .activation import ( + ActivationBase, + FirstOrderActivationDeGroote2016, + ZerothOrderActivation, +) +from .curve import ( + CharacteristicCurveCollection, + CharacteristicCurveFunction, + FiberForceLengthActiveDeGroote2016, + FiberForceLengthPassiveDeGroote2016, + FiberForceLengthPassiveInverseDeGroote2016, + FiberForceVelocityDeGroote2016, + FiberForceVelocityInverseDeGroote2016, + TendonForceLengthDeGroote2016, + TendonForceLengthInverseDeGroote2016, +) +from .musculotendon import ( + MusculotendonBase, + MusculotendonDeGroote2016, + MusculotendonFormulation, +) + + +__all__ = [ + # Musculotendon characteristic curve functions + 'CharacteristicCurveCollection', + 'CharacteristicCurveFunction', + 'FiberForceLengthActiveDeGroote2016', + 'FiberForceLengthPassiveDeGroote2016', + 'FiberForceLengthPassiveInverseDeGroote2016', + 'FiberForceVelocityDeGroote2016', + 'FiberForceVelocityInverseDeGroote2016', + 'TendonForceLengthDeGroote2016', + 'TendonForceLengthInverseDeGroote2016', + + # Activation dynamics classes + 'ActivationBase', + 'FirstOrderActivationDeGroote2016', + 'ZerothOrderActivation', + + # Musculotendon classes + 'MusculotendonBase', + 'MusculotendonDeGroote2016', + 'MusculotendonFormulation', +] diff --git a/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/_mixin.py b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/_mixin.py new file mode 100644 index 0000000000000000000000000000000000000000..f6ff905100fb4d6f346aaf717cfe9a66b4c2cc9a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/_mixin.py @@ -0,0 +1,53 @@ +"""Mixin classes for sharing functionality between unrelated classes. + +This module is named with a leading underscore to signify to users that it's +"private" and only intended for internal use by the biomechanics module. + +""" + + +__all__ = ['_NamedMixin'] + + +class _NamedMixin: + """Mixin class for adding `name` properties. + + Valid names, as will typically be used by subclasses as a suffix when + naming automatically-instantiated symbol attributes, must be nonzero length + strings. + + Attributes + ========== + + name : str + The name identifier associated with the instance. Must be a string of + length at least 1. + + """ + + @property + def name(self) -> str: + """The name associated with the class instance.""" + return self._name + + @name.setter + def name(self, name: str) -> None: + if hasattr(self, '_name'): + msg = ( + f'Can\'t set attribute `name` to {repr(name)} as it is ' + f'immutable.' + ) + raise AttributeError(msg) + if not isinstance(name, str): + msg = ( + f'Name {repr(name)} passed to `name` was of type ' + f'{type(name)}, must be {str}.' + ) + raise TypeError(msg) + if name in {''}: + msg = ( + f'Name {repr(name)} is invalid, must be a nonzero length ' + f'{type(str)}.' + ) + raise ValueError(msg) + self._name = name diff --git a/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/activation.py b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/activation.py new file mode 100644 index 0000000000000000000000000000000000000000..908d9bd2e7b433f91ef6678426c2e4896ab82f27 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/activation.py @@ -0,0 +1,869 @@ +r"""Activation dynamics for musclotendon models. + +Musculotendon models are able to produce active force when they are activated, +which is when a chemical process has taken place within the muscle fibers +causing them to voluntarily contract. Biologically this chemical process (the +diffusion of :math:`\textrm{Ca}^{2+}` ions) is not the input in the system, +electrical signals from the nervous system are. These are termed excitations. +Activation dynamics, which relates the normalized excitation level to the +normalized activation level, can be modeled by the models present in this +module. + +""" + +from abc import ABC, abstractmethod +from functools import cached_property + +from sympy.core.symbol import Symbol +from sympy.core.numbers import Float, Integer, Rational +from sympy.functions.elementary.hyperbolic import tanh +from sympy.matrices.dense import MutableDenseMatrix as Matrix, zeros +from sympy.physics.biomechanics._mixin import _NamedMixin +from sympy.physics.mechanics import dynamicsymbols + + +__all__ = [ + 'ActivationBase', + 'FirstOrderActivationDeGroote2016', + 'ZerothOrderActivation', +] + + +class ActivationBase(ABC, _NamedMixin): + """Abstract base class for all activation dynamics classes to inherit from. + + Notes + ===== + + Instances of this class cannot be directly instantiated by users. However, + it can be used to created custom activation dynamics types through + subclassing. + + """ + + def __init__(self, name): + """Initializer for ``ActivationBase``.""" + self.name = str(name) + + # Symbols + self._e = dynamicsymbols(f"e_{name}") + self._a = dynamicsymbols(f"a_{name}") + + @classmethod + @abstractmethod + def with_defaults(cls, name): + """Alternate constructor that provides recommended defaults for + constants.""" + pass + + @property + def excitation(self): + """Dynamic symbol representing excitation. + + Explanation + =========== + + The alias ``e`` can also be used to access the same attribute. + + """ + return self._e + + @property + def e(self): + """Dynamic symbol representing excitation. + + Explanation + =========== + + The alias ``excitation`` can also be used to access the same attribute. + + """ + return self._e + + @property + def activation(self): + """Dynamic symbol representing activation. + + Explanation + =========== + + The alias ``a`` can also be used to access the same attribute. + + """ + return self._a + + @property + def a(self): + """Dynamic symbol representing activation. + + Explanation + =========== + + The alias ``activation`` can also be used to access the same attribute. + + """ + return self._a + + @property + @abstractmethod + def order(self): + """Order of the (differential) equation governing activation.""" + pass + + @property + @abstractmethod + def state_vars(self): + """Ordered column matrix of functions of time that represent the state + variables. + + Explanation + =========== + + The alias ``x`` can also be used to access the same attribute. + + """ + pass + + @property + @abstractmethod + def x(self): + """Ordered column matrix of functions of time that represent the state + variables. + + Explanation + =========== + + The alias ``state_vars`` can also be used to access the same attribute. + + """ + pass + + @property + @abstractmethod + def input_vars(self): + """Ordered column matrix of functions of time that represent the input + variables. + + Explanation + =========== + + The alias ``r`` can also be used to access the same attribute. + + """ + pass + + @property + @abstractmethod + def r(self): + """Ordered column matrix of functions of time that represent the input + variables. + + Explanation + =========== + + The alias ``input_vars`` can also be used to access the same attribute. + + """ + pass + + @property + @abstractmethod + def constants(self): + """Ordered column matrix of non-time varying symbols present in ``M`` + and ``F``. + + Only symbolic constants are returned. If a numeric type (e.g. ``Float``) + has been used instead of ``Symbol`` for a constant then that attribute + will not be included in the matrix returned by this property. This is + because the primary use of this property attribute is to provide an + ordered sequence of the still-free symbols that require numeric values + during code generation. + + Explanation + =========== + + The alias ``p`` can also be used to access the same attribute. + + """ + pass + + @property + @abstractmethod + def p(self): + """Ordered column matrix of non-time varying symbols present in ``M`` + and ``F``. + + Only symbolic constants are returned. If a numeric type (e.g. ``Float``) + has been used instead of ``Symbol`` for a constant then that attribute + will not be included in the matrix returned by this property. This is + because the primary use of this property attribute is to provide an + ordered sequence of the still-free symbols that require numeric values + during code generation. + + Explanation + =========== + + The alias ``constants`` can also be used to access the same attribute. + + """ + pass + + @property + @abstractmethod + def M(self): + """Ordered square matrix of coefficients on the LHS of ``M x' = F``. + + Explanation + =========== + + The square matrix that forms part of the LHS of the linear system of + ordinary differential equations governing the activation dynamics: + + ``M(x, r, t, p) x' = F(x, r, t, p)``. + + """ + pass + + @property + @abstractmethod + def F(self): + """Ordered column matrix of equations on the RHS of ``M x' = F``. + + Explanation + =========== + + The column matrix that forms the RHS of the linear system of ordinary + differential equations governing the activation dynamics: + + ``M(x, r, t, p) x' = F(x, r, t, p)``. + + """ + pass + + @abstractmethod + def rhs(self): + """ + + Explanation + =========== + + The solution to the linear system of ordinary differential equations + governing the activation dynamics: + + ``M(x, r, t, p) x' = F(x, r, t, p)``. + + """ + pass + + def __eq__(self, other): + """Equality check for activation dynamics.""" + if type(self) != type(other): + return False + if self.name != other.name: + return False + return True + + def __repr__(self): + """Default representation of activation dynamics.""" + return f'{self.__class__.__name__}({self.name!r})' + + +class ZerothOrderActivation(ActivationBase): + """Simple zeroth-order activation dynamics mapping excitation to + activation. + + Explanation + =========== + + Zeroth-order activation dynamics are useful in instances where you want to + reduce the complexity of your musculotendon dynamics as they simple map + exictation to activation. As a result, no additional state equations are + introduced to your system. They also remove a potential source of delay + between the input and dynamics of your system as no (ordinary) differential + equations are involved. + + """ + + def __init__(self, name): + """Initializer for ``ZerothOrderActivation``. + + Parameters + ========== + + name : str + The name identifier associated with the instance. Must be a string + of length at least 1. + + """ + super().__init__(name) + + # Zeroth-order activation dynamics has activation equal excitation so + # overwrite the symbol for activation with the excitation symbol. + self._a = self._e + + @classmethod + def with_defaults(cls, name): + """Alternate constructor that provides recommended defaults for + constants. + + Explanation + =========== + + As this concrete class doesn't implement any constants associated with + its dynamics, this ``classmethod`` simply creates a standard instance + of ``ZerothOrderActivation``. An implementation is provided to ensure + a consistent interface between all ``ActivationBase`` concrete classes. + + """ + return cls(name) + + @property + def order(self): + """Order of the (differential) equation governing activation.""" + return 0 + + @property + def state_vars(self): + """Ordered column matrix of functions of time that represent the state + variables. + + Explanation + =========== + + As zeroth-order activation dynamics simply maps excitation to + activation, this class has no associated state variables and so this + property return an empty column ``Matrix`` with shape (0, 1). + + The alias ``x`` can also be used to access the same attribute. + + """ + return zeros(0, 1) + + @property + def x(self): + """Ordered column matrix of functions of time that represent the state + variables. + + Explanation + =========== + + As zeroth-order activation dynamics simply maps excitation to + activation, this class has no associated state variables and so this + property return an empty column ``Matrix`` with shape (0, 1). + + The alias ``state_vars`` can also be used to access the same attribute. + + """ + return zeros(0, 1) + + @property + def input_vars(self): + """Ordered column matrix of functions of time that represent the input + variables. + + Explanation + =========== + + Excitation is the only input in zeroth-order activation dynamics and so + this property returns a column ``Matrix`` with one entry, ``e``, and + shape (1, 1). + + The alias ``r`` can also be used to access the same attribute. + + """ + return Matrix([self._e]) + + @property + def r(self): + """Ordered column matrix of functions of time that represent the input + variables. + + Explanation + =========== + + Excitation is the only input in zeroth-order activation dynamics and so + this property returns a column ``Matrix`` with one entry, ``e``, and + shape (1, 1). + + The alias ``input_vars`` can also be used to access the same attribute. + + """ + return Matrix([self._e]) + + @property + def constants(self): + """Ordered column matrix of non-time varying symbols present in ``M`` + and ``F``. + + Only symbolic constants are returned. If a numeric type (e.g. ``Float``) + has been used instead of ``Symbol`` for a constant then that attribute + will not be included in the matrix returned by this property. This is + because the primary use of this property attribute is to provide an + ordered sequence of the still-free symbols that require numeric values + during code generation. + + Explanation + =========== + + As zeroth-order activation dynamics simply maps excitation to + activation, this class has no associated constants and so this property + return an empty column ``Matrix`` with shape (0, 1). + + The alias ``p`` can also be used to access the same attribute. + + """ + return zeros(0, 1) + + @property + def p(self): + """Ordered column matrix of non-time varying symbols present in ``M`` + and ``F``. + + Only symbolic constants are returned. If a numeric type (e.g. ``Float``) + has been used instead of ``Symbol`` for a constant then that attribute + will not be included in the matrix returned by this property. This is + because the primary use of this property attribute is to provide an + ordered sequence of the still-free symbols that require numeric values + during code generation. + + Explanation + =========== + + As zeroth-order activation dynamics simply maps excitation to + activation, this class has no associated constants and so this property + return an empty column ``Matrix`` with shape (0, 1). + + The alias ``constants`` can also be used to access the same attribute. + + """ + return zeros(0, 1) + + @property + def M(self): + """Ordered square matrix of coefficients on the LHS of ``M x' = F``. + + Explanation + =========== + + The square matrix that forms part of the LHS of the linear system of + ordinary differential equations governing the activation dynamics: + + ``M(x, r, t, p) x' = F(x, r, t, p)``. + + As zeroth-order activation dynamics have no state variables, this + linear system has dimension 0 and therefore ``M`` is an empty square + ``Matrix`` with shape (0, 0). + + """ + return Matrix([]) + + @property + def F(self): + """Ordered column matrix of equations on the RHS of ``M x' = F``. + + Explanation + =========== + + The column matrix that forms the RHS of the linear system of ordinary + differential equations governing the activation dynamics: + + ``M(x, r, t, p) x' = F(x, r, t, p)``. + + As zeroth-order activation dynamics have no state variables, this + linear system has dimension 0 and therefore ``F`` is an empty column + ``Matrix`` with shape (0, 1). + + """ + return zeros(0, 1) + + def rhs(self): + """Ordered column matrix of equations for the solution of ``M x' = F``. + + Explanation + =========== + + The solution to the linear system of ordinary differential equations + governing the activation dynamics: + + ``M(x, r, t, p) x' = F(x, r, t, p)``. + + As zeroth-order activation dynamics have no state variables, this + linear has dimension 0 and therefore this method returns an empty + column ``Matrix`` with shape (0, 1). + + """ + return zeros(0, 1) + + +class FirstOrderActivationDeGroote2016(ActivationBase): + r"""First-order activation dynamics based on De Groote et al., 2016 [1]_. + + Explanation + =========== + + Gives the first-order activation dynamics equation for the rate of change + of activation with respect to time as a function of excitation and + activation. + + The function is defined by the equation: + + .. math:: + + \frac{da}{dt} = \left(\frac{\frac{1}{2} + a0}{\tau_a \left(\frac{1}{2} + + \frac{3a}{2}\right)} + \frac{\left(\frac{1}{2} + + \frac{3a}{2}\right) \left(\frac{1}{2} - a0\right)}{\tau_d}\right) + \left(e - a\right) + + where + + .. math:: + + a0 = \frac{\tanh{\left(b \left(e - a\right) \right)}}{2} + + with constant values of :math:`tau_a = 0.015`, :math:`tau_d = 0.060`, and + :math:`b = 10`. + + References + ========== + + .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation + of direct collocation optimal control problem formulations for + solving the muscle redundancy problem, Annals of biomedical + engineering, 44(10), (2016) pp. 2922-2936 + + """ + + def __init__(self, + name, + activation_time_constant=None, + deactivation_time_constant=None, + smoothing_rate=None, + ): + """Initializer for ``FirstOrderActivationDeGroote2016``. + + Parameters + ========== + activation time constant : Symbol | Number | None + The value of the activation time constant governing the delay + between excitation and activation when excitation exceeds + activation. + deactivation time constant : Symbol | Number | None + The value of the deactivation time constant governing the delay + between excitation and activation when activation exceeds + excitation. + smoothing_rate : Symbol | Number | None + The slope of the hyperbolic tangent function used to smooth between + the switching of the equations where excitation exceed activation + and where activation exceeds excitation. The recommended value to + use is ``10``, but values between ``0.1`` and ``100`` can be used. + + """ + super().__init__(name) + + # Symbols + self.activation_time_constant = activation_time_constant + self.deactivation_time_constant = deactivation_time_constant + self.smoothing_rate = smoothing_rate + + @classmethod + def with_defaults(cls, name): + r"""Alternate constructor that will use the published constants. + + Explanation + =========== + + Returns an instance of ``FirstOrderActivationDeGroote2016`` using the + three constant values specified in the original publication. + + These have the values: + + :math:`tau_a = 0.015` + :math:`tau_d = 0.060` + :math:`b = 10` + + """ + tau_a = Float('0.015') + tau_d = Float('0.060') + b = Float('10.0') + return cls(name, tau_a, tau_d, b) + + @property + def activation_time_constant(self): + """Delay constant for activation. + + Explanation + =========== + + The alias ```tau_a`` can also be used to access the same attribute. + + """ + return self._tau_a + + @activation_time_constant.setter + def activation_time_constant(self, tau_a): + if hasattr(self, '_tau_a'): + msg = ( + f'Can\'t set attribute `activation_time_constant` to ' + f'{repr(tau_a)} as it is immutable and already has value ' + f'{self._tau_a}.' + ) + raise AttributeError(msg) + self._tau_a = Symbol(f'tau_a_{self.name}') if tau_a is None else tau_a + + @property + def tau_a(self): + """Delay constant for activation. + + Explanation + =========== + + The alias ``activation_time_constant`` can also be used to access the + same attribute. + + """ + return self._tau_a + + @property + def deactivation_time_constant(self): + """Delay constant for deactivation. + + Explanation + =========== + + The alias ``tau_d`` can also be used to access the same attribute. + + """ + return self._tau_d + + @deactivation_time_constant.setter + def deactivation_time_constant(self, tau_d): + if hasattr(self, '_tau_d'): + msg = ( + f'Can\'t set attribute `deactivation_time_constant` to ' + f'{repr(tau_d)} as it is immutable and already has value ' + f'{self._tau_d}.' + ) + raise AttributeError(msg) + self._tau_d = Symbol(f'tau_d_{self.name}') if tau_d is None else tau_d + + @property + def tau_d(self): + """Delay constant for deactivation. + + Explanation + =========== + + The alias ``deactivation_time_constant`` can also be used to access the + same attribute. + + """ + return self._tau_d + + @property + def smoothing_rate(self): + """Smoothing constant for the hyperbolic tangent term. + + Explanation + =========== + + The alias ``b`` can also be used to access the same attribute. + + """ + return self._b + + @smoothing_rate.setter + def smoothing_rate(self, b): + if hasattr(self, '_b'): + msg = ( + f'Can\'t set attribute `smoothing_rate` to {b!r} as it is ' + f'immutable and already has value {self._b!r}.' + ) + raise AttributeError(msg) + self._b = Symbol(f'b_{self.name}') if b is None else b + + @property + def b(self): + """Smoothing constant for the hyperbolic tangent term. + + Explanation + =========== + + The alias ``smoothing_rate`` can also be used to access the same + attribute. + + """ + return self._b + + @property + def order(self): + """Order of the (differential) equation governing activation.""" + return 1 + + @property + def state_vars(self): + """Ordered column matrix of functions of time that represent the state + variables. + + Explanation + =========== + + The alias ``x`` can also be used to access the same attribute. + + """ + return Matrix([self._a]) + + @property + def x(self): + """Ordered column matrix of functions of time that represent the state + variables. + + Explanation + =========== + + The alias ``state_vars`` can also be used to access the same attribute. + + """ + return Matrix([self._a]) + + @property + def input_vars(self): + """Ordered column matrix of functions of time that represent the input + variables. + + Explanation + =========== + + The alias ``r`` can also be used to access the same attribute. + + """ + return Matrix([self._e]) + + @property + def r(self): + """Ordered column matrix of functions of time that represent the input + variables. + + Explanation + =========== + + The alias ``input_vars`` can also be used to access the same attribute. + + """ + return Matrix([self._e]) + + @property + def constants(self): + """Ordered column matrix of non-time varying symbols present in ``M`` + and ``F``. + + Only symbolic constants are returned. If a numeric type (e.g. ``Float``) + has been used instead of ``Symbol`` for a constant then that attribute + will not be included in the matrix returned by this property. This is + because the primary use of this property attribute is to provide an + ordered sequence of the still-free symbols that require numeric values + during code generation. + + Explanation + =========== + + The alias ``p`` can also be used to access the same attribute. + + """ + constants = [self._tau_a, self._tau_d, self._b] + symbolic_constants = [c for c in constants if not c.is_number] + return Matrix(symbolic_constants) if symbolic_constants else zeros(0, 1) + + @property + def p(self): + """Ordered column matrix of non-time varying symbols present in ``M`` + and ``F``. + + Explanation + =========== + + Only symbolic constants are returned. If a numeric type (e.g. ``Float``) + has been used instead of ``Symbol`` for a constant then that attribute + will not be included in the matrix returned by this property. This is + because the primary use of this property attribute is to provide an + ordered sequence of the still-free symbols that require numeric values + during code generation. + + The alias ``constants`` can also be used to access the same attribute. + + """ + constants = [self._tau_a, self._tau_d, self._b] + symbolic_constants = [c for c in constants if not c.is_number] + return Matrix(symbolic_constants) if symbolic_constants else zeros(0, 1) + + @property + def M(self): + """Ordered square matrix of coefficients on the LHS of ``M x' = F``. + + Explanation + =========== + + The square matrix that forms part of the LHS of the linear system of + ordinary differential equations governing the activation dynamics: + + ``M(x, r, t, p) x' = F(x, r, t, p)``. + + """ + return Matrix([Integer(1)]) + + @property + def F(self): + """Ordered column matrix of equations on the RHS of ``M x' = F``. + + Explanation + =========== + + The column matrix that forms the RHS of the linear system of ordinary + differential equations governing the activation dynamics: + + ``M(x, r, t, p) x' = F(x, r, t, p)``. + + """ + return Matrix([self._da_eqn]) + + def rhs(self): + """Ordered column matrix of equations for the solution of ``M x' = F``. + + Explanation + =========== + + The solution to the linear system of ordinary differential equations + governing the activation dynamics: + + ``M(x, r, t, p) x' = F(x, r, t, p)``. + + """ + return Matrix([self._da_eqn]) + + @cached_property + def _da_eqn(self): + HALF = Rational(1, 2) + a0 = HALF * tanh(self._b * (self._e - self._a)) + a1 = (HALF + Rational(3, 2) * self._a) + a2 = (HALF + a0) / (self._tau_a * a1) + a3 = a1 * (HALF - a0) / self._tau_d + activation_dynamics_equation = (a2 + a3) * (self._e - self._a) + return activation_dynamics_equation + + def __eq__(self, other): + """Equality check for ``FirstOrderActivationDeGroote2016``.""" + if type(self) != type(other): + return False + self_attrs = (self.name, self.tau_a, self.tau_d, self.b) + other_attrs = (other.name, other.tau_a, other.tau_d, other.b) + if self_attrs == other_attrs: + return True + return False + + def __repr__(self): + """Representation of ``FirstOrderActivationDeGroote2016``.""" + return ( + f'{self.__class__.__name__}({self.name!r}, ' + f'activation_time_constant={self.tau_a!r}, ' + f'deactivation_time_constant={self.tau_d!r}, ' + f'smoothing_rate={self.b!r})' + ) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/curve.py b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/curve.py new file mode 100644 index 0000000000000000000000000000000000000000..50535271f51493acc2183d257ce89ff0da4dde5e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/curve.py @@ -0,0 +1,1763 @@ +"""Implementations of characteristic curves for musculotendon models.""" + +from dataclasses import dataclass + +from sympy.core.expr import UnevaluatedExpr +from sympy.core.function import ArgumentIndexError, Function +from sympy.core.numbers import Float, Integer +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.hyperbolic import cosh, sinh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.printing.precedence import PRECEDENCE + + +__all__ = [ + 'CharacteristicCurveCollection', + 'CharacteristicCurveFunction', + 'FiberForceLengthActiveDeGroote2016', + 'FiberForceLengthPassiveDeGroote2016', + 'FiberForceLengthPassiveInverseDeGroote2016', + 'FiberForceVelocityDeGroote2016', + 'FiberForceVelocityInverseDeGroote2016', + 'TendonForceLengthDeGroote2016', + 'TendonForceLengthInverseDeGroote2016', +] + + +class CharacteristicCurveFunction(Function): + """Base class for all musculotendon characteristic curve functions.""" + + @classmethod + def eval(cls): + msg = ( + f'Cannot directly instantiate {cls.__name__!r}, instances of ' + f'characteristic curves must be of a concrete subclass.' + + ) + raise TypeError(msg) + + def _print_code(self, printer): + """Print code for the function defining the curve using a printer. + + Explanation + =========== + + The order of operations may need to be controlled as constant folding + the numeric terms within the equations of a musculotendon + characteristic curve can sometimes results in a numerically-unstable + expression. + + Parameters + ========== + + printer : Printer + The printer to be used to print a string representation of the + characteristic curve as valid code in the target language. + + """ + return printer._print(printer.parenthesize( + self.doit(deep=False, evaluate=False), PRECEDENCE['Atom'], + )) + + _ccode = _print_code + _cupycode = _print_code + _cxxcode = _print_code + _fcode = _print_code + _jaxcode = _print_code + _lambdacode = _print_code + _mpmathcode = _print_code + _octave = _print_code + _pythoncode = _print_code + _numpycode = _print_code + _scipycode = _print_code + + +class TendonForceLengthDeGroote2016(CharacteristicCurveFunction): + r"""Tendon force-length curve based on De Groote et al., 2016 [1]_. + + Explanation + =========== + + Gives the normalized tendon force produced as a function of normalized + tendon length. + + The function is defined by the equation: + + $fl^T = c_0 \exp{c_3 \left( \tilde{l}^T - c_1 \right)} - c_2$ + + with constant values of $c_0 = 0.2$, $c_1 = 0.995$, $c_2 = 0.25$, and + $c_3 = 33.93669377311689$. + + While it is possible to change the constant values, these were carefully + selected in the original publication to give the characteristic curve + specific and required properties. For example, the function produces no + force when the tendon is in an unstrained state. It also produces a force + of 1 normalized unit when the tendon is under a 5% strain. + + Examples + ======== + + The preferred way to instantiate :class:`TendonForceLengthDeGroote2016` is using + the :meth:`~.with_defaults` constructor because this will automatically + populate the constants within the characteristic curve equation with the + floating point values from the original publication. This constructor takes + a single argument corresponding to normalized tendon length. We'll create a + :class:`~.Symbol` called ``l_T_tilde`` to represent this. + + >>> from sympy import Symbol + >>> from sympy.physics.biomechanics import TendonForceLengthDeGroote2016 + >>> l_T_tilde = Symbol('l_T_tilde') + >>> fl_T = TendonForceLengthDeGroote2016.with_defaults(l_T_tilde) + >>> fl_T + TendonForceLengthDeGroote2016(l_T_tilde, 0.2, 0.995, 0.25, + 33.93669377311689) + + It's also possible to populate the four constants with your own values too. + + >>> from sympy import symbols + >>> c0, c1, c2, c3 = symbols('c0 c1 c2 c3') + >>> fl_T = TendonForceLengthDeGroote2016(l_T_tilde, c0, c1, c2, c3) + >>> fl_T + TendonForceLengthDeGroote2016(l_T_tilde, c0, c1, c2, c3) + + You don't just have to use symbols as the arguments, it's also possible to + use expressions. Let's create a new pair of symbols, ``l_T`` and + ``l_T_slack``, representing tendon length and tendon slack length + respectively. We can then represent ``l_T_tilde`` as an expression, the + ratio of these. + + >>> l_T, l_T_slack = symbols('l_T l_T_slack') + >>> l_T_tilde = l_T/l_T_slack + >>> fl_T = TendonForceLengthDeGroote2016.with_defaults(l_T_tilde) + >>> fl_T + TendonForceLengthDeGroote2016(l_T/l_T_slack, 0.2, 0.995, 0.25, + 33.93669377311689) + + To inspect the actual symbolic expression that this function represents, + we can call the :meth:`~.doit` method on an instance. We'll use the keyword + argument ``evaluate=False`` as this will keep the expression in its + canonical form and won't simplify any constants. + + >>> fl_T.doit(evaluate=False) + -0.25 + 0.2*exp(33.93669377311689*(l_T/l_T_slack - 0.995)) + + The function can also be differentiated. We'll differentiate with respect + to l_T using the ``diff`` method on an instance with the single positional + argument ``l_T``. + + >>> fl_T.diff(l_T) + 6.787338754623378*exp(33.93669377311689*(l_T/l_T_slack - 0.995))/l_T_slack + + References + ========== + + .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation + of direct collocation optimal control problem formulations for + solving the muscle redundancy problem, Annals of biomedical + engineering, 44(10), (2016) pp. 2922-2936 + + """ + + @classmethod + def with_defaults(cls, l_T_tilde): + r"""Recommended constructor that will use the published constants. + + Explanation + =========== + + Returns a new instance of the tendon force-length function using the + four constant values specified in the original publication. + + These have the values: + + $c_0 = 0.2$ + $c_1 = 0.995$ + $c_2 = 0.25$ + $c_3 = 33.93669377311689$ + + Parameters + ========== + + l_T_tilde : Any (sympifiable) + Normalized tendon length. + + """ + c0 = Float('0.2') + c1 = Float('0.995') + c2 = Float('0.25') + c3 = Float('33.93669377311689') + return cls(l_T_tilde, c0, c1, c2, c3) + + @classmethod + def eval(cls, l_T_tilde, c0, c1, c2, c3): + """Evaluation of basic inputs. + + Parameters + ========== + + l_T_tilde : Any (sympifiable) + Normalized tendon length. + c0 : Any (sympifiable) + The first constant in the characteristic equation. The published + value is ``0.2``. + c1 : Any (sympifiable) + The second constant in the characteristic equation. The published + value is ``0.995``. + c2 : Any (sympifiable) + The third constant in the characteristic equation. The published + value is ``0.25``. + c3 : Any (sympifiable) + The fourth constant in the characteristic equation. The published + value is ``33.93669377311689``. + + """ + pass + + def _eval_evalf(self, prec): + """Evaluate the expression numerically using ``evalf``.""" + return self.doit(deep=False, evaluate=False)._eval_evalf(prec) + + def doit(self, deep=True, evaluate=True, **hints): + """Evaluate the expression defining the function. + + Parameters + ========== + + deep : bool + Whether ``doit`` should be recursively called. Default is ``True``. + evaluate : bool. + Whether the SymPy expression should be evaluated as it is + constructed. If ``False``, then no constant folding will be + conducted which will leave the expression in a more numerically- + stable for values of ``l_T_tilde`` that correspond to a sensible + operating range for a musculotendon. Default is ``True``. + **kwargs : dict[str, Any] + Additional keyword argument pairs to be recursively passed to + ``doit``. + + """ + l_T_tilde, *constants = self.args + if deep: + hints['evaluate'] = evaluate + l_T_tilde = l_T_tilde.doit(deep=deep, **hints) + c0, c1, c2, c3 = [c.doit(deep=deep, **hints) for c in constants] + else: + c0, c1, c2, c3 = constants + + if evaluate: + return c0*exp(c3*(l_T_tilde - c1)) - c2 + + return c0*exp(c3*UnevaluatedExpr(l_T_tilde - c1)) - c2 + + def fdiff(self, argindex=1): + """Derivative of the function with respect to a single argument. + + Parameters + ========== + + argindex : int + The index of the function's arguments with respect to which the + derivative should be taken. Argument indexes start at ``1``. + Default is ``1``. + + """ + l_T_tilde, c0, c1, c2, c3 = self.args + if argindex == 1: + return c0*c3*exp(c3*UnevaluatedExpr(l_T_tilde - c1)) + elif argindex == 2: + return exp(c3*UnevaluatedExpr(l_T_tilde - c1)) + elif argindex == 3: + return -c0*c3*exp(c3*UnevaluatedExpr(l_T_tilde - c1)) + elif argindex == 4: + return Integer(-1) + elif argindex == 5: + return c0*(l_T_tilde - c1)*exp(c3*UnevaluatedExpr(l_T_tilde - c1)) + + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """Inverse function. + + Parameters + ========== + + argindex : int + Value to start indexing the arguments at. Default is ``1``. + + """ + return TendonForceLengthInverseDeGroote2016 + + def _latex(self, printer): + """Print a LaTeX representation of the function defining the curve. + + Parameters + ========== + + printer : Printer + The printer to be used to print the LaTeX string representation. + + """ + l_T_tilde = self.args[0] + _l_T_tilde = printer._print(l_T_tilde) + return r'\operatorname{fl}^T \left( %s \right)' % _l_T_tilde + + +class TendonForceLengthInverseDeGroote2016(CharacteristicCurveFunction): + r"""Inverse tendon force-length curve based on De Groote et al., 2016 [1]_. + + Explanation + =========== + + Gives the normalized tendon length that produces a specific normalized + tendon force. + + The function is defined by the equation: + + ${fl^T}^{-1} = frac{\log{\frac{fl^T + c_2}{c_0}}}{c_3} + c_1$ + + with constant values of $c_0 = 0.2$, $c_1 = 0.995$, $c_2 = 0.25$, and + $c_3 = 33.93669377311689$. This function is the exact analytical inverse + of the related tendon force-length curve ``TendonForceLengthDeGroote2016``. + + While it is possible to change the constant values, these were carefully + selected in the original publication to give the characteristic curve + specific and required properties. For example, the function produces no + force when the tendon is in an unstrained state. It also produces a force + of 1 normalized unit when the tendon is under a 5% strain. + + Examples + ======== + + The preferred way to instantiate :class:`TendonForceLengthInverseDeGroote2016` is + using the :meth:`~.with_defaults` constructor because this will automatically + populate the constants within the characteristic curve equation with the + floating point values from the original publication. This constructor takes + a single argument corresponding to normalized tendon force-length, which is + equal to the tendon force. We'll create a :class:`~.Symbol` called ``fl_T`` to + represent this. + + >>> from sympy import Symbol + >>> from sympy.physics.biomechanics import TendonForceLengthInverseDeGroote2016 + >>> fl_T = Symbol('fl_T') + >>> l_T_tilde = TendonForceLengthInverseDeGroote2016.with_defaults(fl_T) + >>> l_T_tilde + TendonForceLengthInverseDeGroote2016(fl_T, 0.2, 0.995, 0.25, + 33.93669377311689) + + It's also possible to populate the four constants with your own values too. + + >>> from sympy import symbols + >>> c0, c1, c2, c3 = symbols('c0 c1 c2 c3') + >>> l_T_tilde = TendonForceLengthInverseDeGroote2016(fl_T, c0, c1, c2, c3) + >>> l_T_tilde + TendonForceLengthInverseDeGroote2016(fl_T, c0, c1, c2, c3) + + To inspect the actual symbolic expression that this function represents, + we can call the :meth:`~.doit` method on an instance. We'll use the keyword + argument ``evaluate=False`` as this will keep the expression in its + canonical form and won't simplify any constants. + + >>> l_T_tilde.doit(evaluate=False) + c1 + log((c2 + fl_T)/c0)/c3 + + The function can also be differentiated. We'll differentiate with respect + to l_T using the ``diff`` method on an instance with the single positional + argument ``l_T``. + + >>> l_T_tilde.diff(fl_T) + 1/(c3*(c2 + fl_T)) + + References + ========== + + .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation + of direct collocation optimal control problem formulations for + solving the muscle redundancy problem, Annals of biomedical + engineering, 44(10), (2016) pp. 2922-2936 + + """ + + @classmethod + def with_defaults(cls, fl_T): + r"""Recommended constructor that will use the published constants. + + Explanation + =========== + + Returns a new instance of the inverse tendon force-length function + using the four constant values specified in the original publication. + + These have the values: + + $c_0 = 0.2$ + $c_1 = 0.995$ + $c_2 = 0.25$ + $c_3 = 33.93669377311689$ + + Parameters + ========== + + fl_T : Any (sympifiable) + Normalized tendon force as a function of tendon length. + + """ + c0 = Float('0.2') + c1 = Float('0.995') + c2 = Float('0.25') + c3 = Float('33.93669377311689') + return cls(fl_T, c0, c1, c2, c3) + + @classmethod + def eval(cls, fl_T, c0, c1, c2, c3): + """Evaluation of basic inputs. + + Parameters + ========== + + fl_T : Any (sympifiable) + Normalized tendon force as a function of tendon length. + c0 : Any (sympifiable) + The first constant in the characteristic equation. The published + value is ``0.2``. + c1 : Any (sympifiable) + The second constant in the characteristic equation. The published + value is ``0.995``. + c2 : Any (sympifiable) + The third constant in the characteristic equation. The published + value is ``0.25``. + c3 : Any (sympifiable) + The fourth constant in the characteristic equation. The published + value is ``33.93669377311689``. + + """ + pass + + def _eval_evalf(self, prec): + """Evaluate the expression numerically using ``evalf``.""" + return self.doit(deep=False, evaluate=False)._eval_evalf(prec) + + def doit(self, deep=True, evaluate=True, **hints): + """Evaluate the expression defining the function. + + Parameters + ========== + + deep : bool + Whether ``doit`` should be recursively called. Default is ``True``. + evaluate : bool. + Whether the SymPy expression should be evaluated as it is + constructed. If ``False``, then no constant folding will be + conducted which will leave the expression in a more numerically- + stable for values of ``l_T_tilde`` that correspond to a sensible + operating range for a musculotendon. Default is ``True``. + **kwargs : dict[str, Any] + Additional keyword argument pairs to be recursively passed to + ``doit``. + + """ + fl_T, *constants = self.args + if deep: + hints['evaluate'] = evaluate + fl_T = fl_T.doit(deep=deep, **hints) + c0, c1, c2, c3 = [c.doit(deep=deep, **hints) for c in constants] + else: + c0, c1, c2, c3 = constants + + if evaluate: + return log((fl_T + c2)/c0)/c3 + c1 + + return log(UnevaluatedExpr((fl_T + c2)/c0))/c3 + c1 + + def fdiff(self, argindex=1): + """Derivative of the function with respect to a single argument. + + Parameters + ========== + + argindex : int + The index of the function's arguments with respect to which the + derivative should be taken. Argument indexes start at ``1``. + Default is ``1``. + + """ + fl_T, c0, c1, c2, c3 = self.args + if argindex == 1: + return 1/(c3*(fl_T + c2)) + elif argindex == 2: + return -1/(c0*c3) + elif argindex == 3: + return Integer(1) + elif argindex == 4: + return 1/(c3*(fl_T + c2)) + elif argindex == 5: + return -log(UnevaluatedExpr((fl_T + c2)/c0))/c3**2 + + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """Inverse function. + + Parameters + ========== + + argindex : int + Value to start indexing the arguments at. Default is ``1``. + + """ + return TendonForceLengthDeGroote2016 + + def _latex(self, printer): + """Print a LaTeX representation of the function defining the curve. + + Parameters + ========== + + printer : Printer + The printer to be used to print the LaTeX string representation. + + """ + fl_T = self.args[0] + _fl_T = printer._print(fl_T) + return r'\left( \operatorname{fl}^T \right)^{-1} \left( %s \right)' % _fl_T + + +class FiberForceLengthPassiveDeGroote2016(CharacteristicCurveFunction): + r"""Passive muscle fiber force-length curve based on De Groote et al., 2016 + [1]_. + + Explanation + =========== + + The function is defined by the equation: + + $fl^M_{pas} = \frac{\frac{\exp{c_1 \left(\tilde{l^M} - 1\right)}}{c_0} - 1}{\exp{c_1} - 1}$ + + with constant values of $c_0 = 0.6$ and $c_1 = 4.0$. + + While it is possible to change the constant values, these were carefully + selected in the original publication to give the characteristic curve + specific and required properties. For example, the function produces a + passive fiber force very close to 0 for all normalized fiber lengths + between 0 and 1. + + Examples + ======== + + The preferred way to instantiate :class:`FiberForceLengthPassiveDeGroote2016` is + using the :meth:`~.with_defaults` constructor because this will automatically + populate the constants within the characteristic curve equation with the + floating point values from the original publication. This constructor takes + a single argument corresponding to normalized muscle fiber length. We'll + create a :class:`~.Symbol` called ``l_M_tilde`` to represent this. + + >>> from sympy import Symbol + >>> from sympy.physics.biomechanics import FiberForceLengthPassiveDeGroote2016 + >>> l_M_tilde = Symbol('l_M_tilde') + >>> fl_M = FiberForceLengthPassiveDeGroote2016.with_defaults(l_M_tilde) + >>> fl_M + FiberForceLengthPassiveDeGroote2016(l_M_tilde, 0.6, 4.0) + + It's also possible to populate the two constants with your own values too. + + >>> from sympy import symbols + >>> c0, c1 = symbols('c0 c1') + >>> fl_M = FiberForceLengthPassiveDeGroote2016(l_M_tilde, c0, c1) + >>> fl_M + FiberForceLengthPassiveDeGroote2016(l_M_tilde, c0, c1) + + You don't just have to use symbols as the arguments, it's also possible to + use expressions. Let's create a new pair of symbols, ``l_M`` and + ``l_M_opt``, representing muscle fiber length and optimal muscle fiber + length respectively. We can then represent ``l_M_tilde`` as an expression, + the ratio of these. + + >>> l_M, l_M_opt = symbols('l_M l_M_opt') + >>> l_M_tilde = l_M/l_M_opt + >>> fl_M = FiberForceLengthPassiveDeGroote2016.with_defaults(l_M_tilde) + >>> fl_M + FiberForceLengthPassiveDeGroote2016(l_M/l_M_opt, 0.6, 4.0) + + To inspect the actual symbolic expression that this function represents, + we can call the :meth:`~.doit` method on an instance. We'll use the keyword + argument ``evaluate=False`` as this will keep the expression in its + canonical form and won't simplify any constants. + + >>> fl_M.doit(evaluate=False) + 0.0186573603637741*(-1 + exp(6.66666666666667*(l_M/l_M_opt - 1))) + + The function can also be differentiated. We'll differentiate with respect + to l_M using the ``diff`` method on an instance with the single positional + argument ``l_M``. + + >>> fl_M.diff(l_M) + 0.12438240242516*exp(6.66666666666667*(l_M/l_M_opt - 1))/l_M_opt + + References + ========== + + .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation + of direct collocation optimal control problem formulations for + solving the muscle redundancy problem, Annals of biomedical + engineering, 44(10), (2016) pp. 2922-2936 + + """ + + @classmethod + def with_defaults(cls, l_M_tilde): + r"""Recommended constructor that will use the published constants. + + Explanation + =========== + + Returns a new instance of the muscle fiber passive force-length + function using the four constant values specified in the original + publication. + + These have the values: + + $c_0 = 0.6$ + $c_1 = 4.0$ + + Parameters + ========== + + l_M_tilde : Any (sympifiable) + Normalized muscle fiber length. + + """ + c0 = Float('0.6') + c1 = Float('4.0') + return cls(l_M_tilde, c0, c1) + + @classmethod + def eval(cls, l_M_tilde, c0, c1): + """Evaluation of basic inputs. + + Parameters + ========== + + l_M_tilde : Any (sympifiable) + Normalized muscle fiber length. + c0 : Any (sympifiable) + The first constant in the characteristic equation. The published + value is ``0.6``. + c1 : Any (sympifiable) + The second constant in the characteristic equation. The published + value is ``4.0``. + + """ + pass + + def _eval_evalf(self, prec): + """Evaluate the expression numerically using ``evalf``.""" + return self.doit(deep=False, evaluate=False)._eval_evalf(prec) + + def doit(self, deep=True, evaluate=True, **hints): + """Evaluate the expression defining the function. + + Parameters + ========== + + deep : bool + Whether ``doit`` should be recursively called. Default is ``True``. + evaluate : bool. + Whether the SymPy expression should be evaluated as it is + constructed. If ``False``, then no constant folding will be + conducted which will leave the expression in a more numerically- + stable for values of ``l_T_tilde`` that correspond to a sensible + operating range for a musculotendon. Default is ``True``. + **kwargs : dict[str, Any] + Additional keyword argument pairs to be recursively passed to + ``doit``. + + """ + l_M_tilde, *constants = self.args + if deep: + hints['evaluate'] = evaluate + l_M_tilde = l_M_tilde.doit(deep=deep, **hints) + c0, c1 = [c.doit(deep=deep, **hints) for c in constants] + else: + c0, c1 = constants + + if evaluate: + return (exp((c1*(l_M_tilde - 1))/c0) - 1)/(exp(c1) - 1) + + return (exp((c1*UnevaluatedExpr(l_M_tilde - 1))/c0) - 1)/(exp(c1) - 1) + + def fdiff(self, argindex=1): + """Derivative of the function with respect to a single argument. + + Parameters + ========== + + argindex : int + The index of the function's arguments with respect to which the + derivative should be taken. Argument indexes start at ``1``. + Default is ``1``. + + """ + l_M_tilde, c0, c1 = self.args + if argindex == 1: + return c1*exp(c1*UnevaluatedExpr(l_M_tilde - 1)/c0)/(c0*(exp(c1) - 1)) + elif argindex == 2: + return ( + -c1*exp(c1*UnevaluatedExpr(l_M_tilde - 1)/c0) + *UnevaluatedExpr(l_M_tilde - 1)/(c0**2*(exp(c1) - 1)) + ) + elif argindex == 3: + return ( + -exp(c1)*(-1 + exp(c1*UnevaluatedExpr(l_M_tilde - 1)/c0))/(exp(c1) - 1)**2 + + exp(c1*UnevaluatedExpr(l_M_tilde - 1)/c0)*(l_M_tilde - 1)/(c0*(exp(c1) - 1)) + ) + + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """Inverse function. + + Parameters + ========== + + argindex : int + Value to start indexing the arguments at. Default is ``1``. + + """ + return FiberForceLengthPassiveInverseDeGroote2016 + + def _latex(self, printer): + """Print a LaTeX representation of the function defining the curve. + + Parameters + ========== + + printer : Printer + The printer to be used to print the LaTeX string representation. + + """ + l_M_tilde = self.args[0] + _l_M_tilde = printer._print(l_M_tilde) + return r'\operatorname{fl}^M_{pas} \left( %s \right)' % _l_M_tilde + + +class FiberForceLengthPassiveInverseDeGroote2016(CharacteristicCurveFunction): + r"""Inverse passive muscle fiber force-length curve based on De Groote et + al., 2016 [1]_. + + Explanation + =========== + + Gives the normalized muscle fiber length that produces a specific normalized + passive muscle fiber force. + + The function is defined by the equation: + + ${fl^M_{pas}}^{-1} = \frac{c_0 \log{\left(\exp{c_1} - 1\right)fl^M_pas + 1}}{c_1} + 1$ + + with constant values of $c_0 = 0.6$ and $c_1 = 4.0$. This function is the + exact analytical inverse of the related tendon force-length curve + ``FiberForceLengthPassiveDeGroote2016``. + + While it is possible to change the constant values, these were carefully + selected in the original publication to give the characteristic curve + specific and required properties. For example, the function produces a + passive fiber force very close to 0 for all normalized fiber lengths + between 0 and 1. + + Examples + ======== + + The preferred way to instantiate + :class:`FiberForceLengthPassiveInverseDeGroote2016` is using the + :meth:`~.with_defaults` constructor because this will automatically populate the + constants within the characteristic curve equation with the floating point + values from the original publication. This constructor takes a single + argument corresponding to the normalized passive muscle fiber length-force + component of the muscle fiber force. We'll create a :class:`~.Symbol` called + ``fl_M_pas`` to represent this. + + >>> from sympy import Symbol + >>> from sympy.physics.biomechanics import FiberForceLengthPassiveInverseDeGroote2016 + >>> fl_M_pas = Symbol('fl_M_pas') + >>> l_M_tilde = FiberForceLengthPassiveInverseDeGroote2016.with_defaults(fl_M_pas) + >>> l_M_tilde + FiberForceLengthPassiveInverseDeGroote2016(fl_M_pas, 0.6, 4.0) + + It's also possible to populate the two constants with your own values too. + + >>> from sympy import symbols + >>> c0, c1 = symbols('c0 c1') + >>> l_M_tilde = FiberForceLengthPassiveInverseDeGroote2016(fl_M_pas, c0, c1) + >>> l_M_tilde + FiberForceLengthPassiveInverseDeGroote2016(fl_M_pas, c0, c1) + + To inspect the actual symbolic expression that this function represents, + we can call the :meth:`~.doit` method on an instance. We'll use the keyword + argument ``evaluate=False`` as this will keep the expression in its + canonical form and won't simplify any constants. + + >>> l_M_tilde.doit(evaluate=False) + c0*log(1 + fl_M_pas*(exp(c1) - 1))/c1 + 1 + + The function can also be differentiated. We'll differentiate with respect + to fl_M_pas using the ``diff`` method on an instance with the single positional + argument ``fl_M_pas``. + + >>> l_M_tilde.diff(fl_M_pas) + c0*(exp(c1) - 1)/(c1*(fl_M_pas*(exp(c1) - 1) + 1)) + + References + ========== + + .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation + of direct collocation optimal control problem formulations for + solving the muscle redundancy problem, Annals of biomedical + engineering, 44(10), (2016) pp. 2922-2936 + + """ + + @classmethod + def with_defaults(cls, fl_M_pas): + r"""Recommended constructor that will use the published constants. + + Explanation + =========== + + Returns a new instance of the inverse muscle fiber passive force-length + function using the four constant values specified in the original + publication. + + These have the values: + + $c_0 = 0.6$ + $c_1 = 4.0$ + + Parameters + ========== + + fl_M_pas : Any (sympifiable) + Normalized passive muscle fiber force as a function of muscle fiber + length. + + """ + c0 = Float('0.6') + c1 = Float('4.0') + return cls(fl_M_pas, c0, c1) + + @classmethod + def eval(cls, fl_M_pas, c0, c1): + """Evaluation of basic inputs. + + Parameters + ========== + + fl_M_pas : Any (sympifiable) + Normalized passive muscle fiber force. + c0 : Any (sympifiable) + The first constant in the characteristic equation. The published + value is ``0.6``. + c1 : Any (sympifiable) + The second constant in the characteristic equation. The published + value is ``4.0``. + + """ + pass + + def _eval_evalf(self, prec): + """Evaluate the expression numerically using ``evalf``.""" + return self.doit(deep=False, evaluate=False)._eval_evalf(prec) + + def doit(self, deep=True, evaluate=True, **hints): + """Evaluate the expression defining the function. + + Parameters + ========== + + deep : bool + Whether ``doit`` should be recursively called. Default is ``True``. + evaluate : bool. + Whether the SymPy expression should be evaluated as it is + constructed. If ``False``, then no constant folding will be + conducted which will leave the expression in a more numerically- + stable for values of ``l_T_tilde`` that correspond to a sensible + operating range for a musculotendon. Default is ``True``. + **kwargs : dict[str, Any] + Additional keyword argument pairs to be recursively passed to + ``doit``. + + """ + fl_M_pas, *constants = self.args + if deep: + hints['evaluate'] = evaluate + fl_M_pas = fl_M_pas.doit(deep=deep, **hints) + c0, c1 = [c.doit(deep=deep, **hints) for c in constants] + else: + c0, c1 = constants + + if evaluate: + return c0*log(fl_M_pas*(exp(c1) - 1) + 1)/c1 + 1 + + return c0*log(UnevaluatedExpr(fl_M_pas*(exp(c1) - 1)) + 1)/c1 + 1 + + def fdiff(self, argindex=1): + """Derivative of the function with respect to a single argument. + + Parameters + ========== + + argindex : int + The index of the function's arguments with respect to which the + derivative should be taken. Argument indexes start at ``1``. + Default is ``1``. + + """ + fl_M_pas, c0, c1 = self.args + if argindex == 1: + return c0*(exp(c1) - 1)/(c1*(fl_M_pas*(exp(c1) - 1) + 1)) + elif argindex == 2: + return log(fl_M_pas*(exp(c1) - 1) + 1)/c1 + elif argindex == 3: + return ( + c0*fl_M_pas*exp(c1)/(c1*(fl_M_pas*(exp(c1) - 1) + 1)) + - c0*log(fl_M_pas*(exp(c1) - 1) + 1)/c1**2 + ) + + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """Inverse function. + + Parameters + ========== + + argindex : int + Value to start indexing the arguments at. Default is ``1``. + + """ + return FiberForceLengthPassiveDeGroote2016 + + def _latex(self, printer): + """Print a LaTeX representation of the function defining the curve. + + Parameters + ========== + + printer : Printer + The printer to be used to print the LaTeX string representation. + + """ + fl_M_pas = self.args[0] + _fl_M_pas = printer._print(fl_M_pas) + return r'\left( \operatorname{fl}^M_{pas} \right)^{-1} \left( %s \right)' % _fl_M_pas + + +class FiberForceLengthActiveDeGroote2016(CharacteristicCurveFunction): + r"""Active muscle fiber force-length curve based on De Groote et al., 2016 + [1]_. + + Explanation + =========== + + The function is defined by the equation: + + $fl_{\text{act}}^M = c_0 \exp\left(-\frac{1}{2}\left(\frac{\tilde{l}^M - c_1}{c_2 + c_3 \tilde{l}^M}\right)^2\right) + + c_4 \exp\left(-\frac{1}{2}\left(\frac{\tilde{l}^M - c_5}{c_6 + c_7 \tilde{l}^M}\right)^2\right) + + c_8 \exp\left(-\frac{1}{2}\left(\frac{\tilde{l}^M - c_9}{c_{10} + c_{11} \tilde{l}^M}\right)^2\right)$ + + with constant values of $c0 = 0.814$, $c1 = 1.06$, $c2 = 0.162$, + $c3 = 0.0633$, $c4 = 0.433$, $c5 = 0.717$, $c6 = -0.0299$, $c7 = 0.2$, + $c8 = 0.1$, $c9 = 1.0$, $c10 = 0.354$, and $c11 = 0.0$. + + While it is possible to change the constant values, these were carefully + selected in the original publication to give the characteristic curve + specific and required properties. For example, the function produces a + active fiber force of 1 at a normalized fiber length of 1, and an active + fiber force of 0 at normalized fiber lengths of 0 and 2. + + Examples + ======== + + The preferred way to instantiate :class:`FiberForceLengthActiveDeGroote2016` is + using the :meth:`~.with_defaults` constructor because this will automatically + populate the constants within the characteristic curve equation with the + floating point values from the original publication. This constructor takes + a single argument corresponding to normalized muscle fiber length. We'll + create a :class:`~.Symbol` called ``l_M_tilde`` to represent this. + + >>> from sympy import Symbol + >>> from sympy.physics.biomechanics import FiberForceLengthActiveDeGroote2016 + >>> l_M_tilde = Symbol('l_M_tilde') + >>> fl_M = FiberForceLengthActiveDeGroote2016.with_defaults(l_M_tilde) + >>> fl_M + FiberForceLengthActiveDeGroote2016(l_M_tilde, 0.814, 1.06, 0.162, 0.0633, + 0.433, 0.717, -0.0299, 0.2, 0.1, 1.0, 0.354, 0.0) + + It's also possible to populate the two constants with your own values too. + + >>> from sympy import symbols + >>> c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11 = symbols('c0:12') + >>> fl_M = FiberForceLengthActiveDeGroote2016(l_M_tilde, c0, c1, c2, c3, + ... c4, c5, c6, c7, c8, c9, c10, c11) + >>> fl_M + FiberForceLengthActiveDeGroote2016(l_M_tilde, c0, c1, c2, c3, c4, c5, c6, + c7, c8, c9, c10, c11) + + You don't just have to use symbols as the arguments, it's also possible to + use expressions. Let's create a new pair of symbols, ``l_M`` and + ``l_M_opt``, representing muscle fiber length and optimal muscle fiber + length respectively. We can then represent ``l_M_tilde`` as an expression, + the ratio of these. + + >>> l_M, l_M_opt = symbols('l_M l_M_opt') + >>> l_M_tilde = l_M/l_M_opt + >>> fl_M = FiberForceLengthActiveDeGroote2016.with_defaults(l_M_tilde) + >>> fl_M + FiberForceLengthActiveDeGroote2016(l_M/l_M_opt, 0.814, 1.06, 0.162, 0.0633, + 0.433, 0.717, -0.0299, 0.2, 0.1, 1.0, 0.354, 0.0) + + To inspect the actual symbolic expression that this function represents, + we can call the :meth:`~.doit` method on an instance. We'll use the keyword + argument ``evaluate=False`` as this will keep the expression in its + canonical form and won't simplify any constants. + + >>> fl_M.doit(evaluate=False) + 0.814*exp(-(l_M/l_M_opt + - 1.06)**2/(2*(0.0633*l_M/l_M_opt + 0.162)**2)) + + 0.433*exp(-(l_M/l_M_opt - 0.717)**2/(2*(0.2*l_M/l_M_opt - 0.0299)**2)) + + 0.1*exp(-3.98991349867535*(l_M/l_M_opt - 1.0)**2) + + The function can also be differentiated. We'll differentiate with respect + to l_M using the ``diff`` method on an instance with the single positional + argument ``l_M``. + + >>> fl_M.diff(l_M) + ((-0.79798269973507*l_M/l_M_opt + + 0.79798269973507)*exp(-3.98991349867535*(l_M/l_M_opt - 1.0)**2) + + (0.433*(-l_M/l_M_opt + 0.717)/(0.2*l_M/l_M_opt - 0.0299)**2 + + 0.0866*(l_M/l_M_opt - 0.717)**2/(0.2*l_M/l_M_opt + - 0.0299)**3)*exp(-(l_M/l_M_opt - 0.717)**2/(2*(0.2*l_M/l_M_opt - 0.0299)**2)) + + (0.814*(-l_M/l_M_opt + 1.06)/(0.0633*l_M/l_M_opt + + 0.162)**2 + 0.0515262*(l_M/l_M_opt + - 1.06)**2/(0.0633*l_M/l_M_opt + + 0.162)**3)*exp(-(l_M/l_M_opt + - 1.06)**2/(2*(0.0633*l_M/l_M_opt + 0.162)**2)))/l_M_opt + + References + ========== + + .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation + of direct collocation optimal control problem formulations for + solving the muscle redundancy problem, Annals of biomedical + engineering, 44(10), (2016) pp. 2922-2936 + + """ + + @classmethod + def with_defaults(cls, l_M_tilde): + r"""Recommended constructor that will use the published constants. + + Explanation + =========== + + Returns a new instance of the inverse muscle fiber act force-length + function using the four constant values specified in the original + publication. + + These have the values: + + $c0 = 0.814$ + $c1 = 1.06$ + $c2 = 0.162$ + $c3 = 0.0633$ + $c4 = 0.433$ + $c5 = 0.717$ + $c6 = -0.0299$ + $c7 = 0.2$ + $c8 = 0.1$ + $c9 = 1.0$ + $c10 = 0.354$ + $c11 = 0.0$ + + Parameters + ========== + + fl_M_act : Any (sympifiable) + Normalized passive muscle fiber force as a function of muscle fiber + length. + + """ + c0 = Float('0.814') + c1 = Float('1.06') + c2 = Float('0.162') + c3 = Float('0.0633') + c4 = Float('0.433') + c5 = Float('0.717') + c6 = Float('-0.0299') + c7 = Float('0.2') + c8 = Float('0.1') + c9 = Float('1.0') + c10 = Float('0.354') + c11 = Float('0.0') + return cls(l_M_tilde, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) + + @classmethod + def eval(cls, l_M_tilde, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11): + """Evaluation of basic inputs. + + Parameters + ========== + + l_M_tilde : Any (sympifiable) + Normalized muscle fiber length. + c0 : Any (sympifiable) + The first constant in the characteristic equation. The published + value is ``0.814``. + c1 : Any (sympifiable) + The second constant in the characteristic equation. The published + value is ``1.06``. + c2 : Any (sympifiable) + The third constant in the characteristic equation. The published + value is ``0.162``. + c3 : Any (sympifiable) + The fourth constant in the characteristic equation. The published + value is ``0.0633``. + c4 : Any (sympifiable) + The fifth constant in the characteristic equation. The published + value is ``0.433``. + c5 : Any (sympifiable) + The sixth constant in the characteristic equation. The published + value is ``0.717``. + c6 : Any (sympifiable) + The seventh constant in the characteristic equation. The published + value is ``-0.0299``. + c7 : Any (sympifiable) + The eighth constant in the characteristic equation. The published + value is ``0.2``. + c8 : Any (sympifiable) + The ninth constant in the characteristic equation. The published + value is ``0.1``. + c9 : Any (sympifiable) + The tenth constant in the characteristic equation. The published + value is ``1.0``. + c10 : Any (sympifiable) + The eleventh constant in the characteristic equation. The published + value is ``0.354``. + c11 : Any (sympifiable) + The tweflth constant in the characteristic equation. The published + value is ``0.0``. + + """ + pass + + def _eval_evalf(self, prec): + """Evaluate the expression numerically using ``evalf``.""" + return self.doit(deep=False, evaluate=False)._eval_evalf(prec) + + def doit(self, deep=True, evaluate=True, **hints): + """Evaluate the expression defining the function. + + Parameters + ========== + + deep : bool + Whether ``doit`` should be recursively called. Default is ``True``. + evaluate : bool. + Whether the SymPy expression should be evaluated as it is + constructed. If ``False``, then no constant folding will be + conducted which will leave the expression in a more numerically- + stable for values of ``l_M_tilde`` that correspond to a sensible + operating range for a musculotendon. Default is ``True``. + **kwargs : dict[str, Any] + Additional keyword argument pairs to be recursively passed to + ``doit``. + + """ + l_M_tilde, *constants = self.args + if deep: + hints['evaluate'] = evaluate + l_M_tilde = l_M_tilde.doit(deep=deep, **hints) + constants = [c.doit(deep=deep, **hints) for c in constants] + c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11 = constants + + if evaluate: + return ( + c0*exp(-(((l_M_tilde - c1)/(c2 + c3*l_M_tilde))**2)/2) + + c4*exp(-(((l_M_tilde - c5)/(c6 + c7*l_M_tilde))**2)/2) + + c8*exp(-(((l_M_tilde - c9)/(c10 + c11*l_M_tilde))**2)/2) + ) + + return ( + c0*exp(-((UnevaluatedExpr(l_M_tilde - c1)/(c2 + c3*l_M_tilde))**2)/2) + + c4*exp(-((UnevaluatedExpr(l_M_tilde - c5)/(c6 + c7*l_M_tilde))**2)/2) + + c8*exp(-((UnevaluatedExpr(l_M_tilde - c9)/(c10 + c11*l_M_tilde))**2)/2) + ) + + def fdiff(self, argindex=1): + """Derivative of the function with respect to a single argument. + + Parameters + ========== + + argindex : int + The index of the function's arguments with respect to which the + derivative should be taken. Argument indexes start at ``1``. + Default is ``1``. + + """ + l_M_tilde, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11 = self.args + if argindex == 1: + return ( + c0*( + c3*(l_M_tilde - c1)**2/(c2 + c3*l_M_tilde)**3 + + (c1 - l_M_tilde)/((c2 + c3*l_M_tilde)**2) + )*exp(-(l_M_tilde - c1)**2/(2*(c2 + c3*l_M_tilde)**2)) + + c4*( + c7*(l_M_tilde - c5)**2/(c6 + c7*l_M_tilde)**3 + + (c5 - l_M_tilde)/((c6 + c7*l_M_tilde)**2) + )*exp(-(l_M_tilde - c5)**2/(2*(c6 + c7*l_M_tilde)**2)) + + c8*( + c11*(l_M_tilde - c9)**2/(c10 + c11*l_M_tilde)**3 + + (c9 - l_M_tilde)/((c10 + c11*l_M_tilde)**2) + )*exp(-(l_M_tilde - c9)**2/(2*(c10 + c11*l_M_tilde)**2)) + ) + elif argindex == 2: + return exp(-(l_M_tilde - c1)**2/(2*(c2 + c3*l_M_tilde)**2)) + elif argindex == 3: + return ( + c0*(l_M_tilde - c1)/(c2 + c3*l_M_tilde)**2 + *exp(-(l_M_tilde - c1)**2 /(2*(c2 + c3*l_M_tilde)**2)) + ) + elif argindex == 4: + return ( + c0*(l_M_tilde - c1)**2/(c2 + c3*l_M_tilde)**3 + *exp(-(l_M_tilde - c1)**2/(2*(c2 + c3*l_M_tilde)**2)) + ) + elif argindex == 5: + return ( + c0*l_M_tilde*(l_M_tilde - c1)**2/(c2 + c3*l_M_tilde)**3 + *exp(-(l_M_tilde - c1)**2/(2*(c2 + c3*l_M_tilde)**2)) + ) + elif argindex == 6: + return exp(-(l_M_tilde - c5)**2/(2*(c6 + c7*l_M_tilde)**2)) + elif argindex == 7: + return ( + c4*(l_M_tilde - c5)/(c6 + c7*l_M_tilde)**2 + *exp(-(l_M_tilde - c5)**2 /(2*(c6 + c7*l_M_tilde)**2)) + ) + elif argindex == 8: + return ( + c4*(l_M_tilde - c5)**2/(c6 + c7*l_M_tilde)**3 + *exp(-(l_M_tilde - c5)**2/(2*(c6 + c7*l_M_tilde)**2)) + ) + elif argindex == 9: + return ( + c4*l_M_tilde*(l_M_tilde - c5)**2/(c6 + c7*l_M_tilde)**3 + *exp(-(l_M_tilde - c5)**2/(2*(c6 + c7*l_M_tilde)**2)) + ) + elif argindex == 10: + return exp(-(l_M_tilde - c9)**2/(2*(c10 + c11*l_M_tilde)**2)) + elif argindex == 11: + return ( + c8*(l_M_tilde - c9)/(c10 + c11*l_M_tilde)**2 + *exp(-(l_M_tilde - c9)**2 /(2*(c10 + c11*l_M_tilde)**2)) + ) + elif argindex == 12: + return ( + c8*(l_M_tilde - c9)**2/(c10 + c11*l_M_tilde)**3 + *exp(-(l_M_tilde - c9)**2/(2*(c10 + c11*l_M_tilde)**2)) + ) + elif argindex == 13: + return ( + c8*l_M_tilde*(l_M_tilde - c9)**2/(c10 + c11*l_M_tilde)**3 + *exp(-(l_M_tilde - c9)**2/(2*(c10 + c11*l_M_tilde)**2)) + ) + + raise ArgumentIndexError(self, argindex) + + def _latex(self, printer): + """Print a LaTeX representation of the function defining the curve. + + Parameters + ========== + + printer : Printer + The printer to be used to print the LaTeX string representation. + + """ + l_M_tilde = self.args[0] + _l_M_tilde = printer._print(l_M_tilde) + return r'\operatorname{fl}^M_{act} \left( %s \right)' % _l_M_tilde + + +class FiberForceVelocityDeGroote2016(CharacteristicCurveFunction): + r"""Muscle fiber force-velocity curve based on De Groote et al., 2016 [1]_. + + Explanation + =========== + + Gives the normalized muscle fiber force produced as a function of + normalized tendon velocity. + + The function is defined by the equation: + + $fv^M = c_0 \log{\left(c_1 \tilde{v}_m + c_2\right) + \sqrt{\left(c_1 \tilde{v}_m + c_2\right)^2 + 1}} + c_3$ + + with constant values of $c_0 = -0.318$, $c_1 = -8.149$, $c_2 = -0.374$, and + $c_3 = 0.886$. + + While it is possible to change the constant values, these were carefully + selected in the original publication to give the characteristic curve + specific and required properties. For example, the function produces a + normalized muscle fiber force of 1 when the muscle fibers are contracting + isometrically (they have an extension rate of 0). + + Examples + ======== + + The preferred way to instantiate :class:`FiberForceVelocityDeGroote2016` is using + the :meth:`~.with_defaults` constructor because this will automatically populate + the constants within the characteristic curve equation with the floating + point values from the original publication. This constructor takes a single + argument corresponding to normalized muscle fiber extension velocity. We'll + create a :class:`~.Symbol` called ``v_M_tilde`` to represent this. + + >>> from sympy import Symbol + >>> from sympy.physics.biomechanics import FiberForceVelocityDeGroote2016 + >>> v_M_tilde = Symbol('v_M_tilde') + >>> fv_M = FiberForceVelocityDeGroote2016.with_defaults(v_M_tilde) + >>> fv_M + FiberForceVelocityDeGroote2016(v_M_tilde, -0.318, -8.149, -0.374, 0.886) + + It's also possible to populate the four constants with your own values too. + + >>> from sympy import symbols + >>> c0, c1, c2, c3 = symbols('c0 c1 c2 c3') + >>> fv_M = FiberForceVelocityDeGroote2016(v_M_tilde, c0, c1, c2, c3) + >>> fv_M + FiberForceVelocityDeGroote2016(v_M_tilde, c0, c1, c2, c3) + + You don't just have to use symbols as the arguments, it's also possible to + use expressions. Let's create a new pair of symbols, ``v_M`` and + ``v_M_max``, representing muscle fiber extension velocity and maximum + muscle fiber extension velocity respectively. We can then represent + ``v_M_tilde`` as an expression, the ratio of these. + + >>> v_M, v_M_max = symbols('v_M v_M_max') + >>> v_M_tilde = v_M/v_M_max + >>> fv_M = FiberForceVelocityDeGroote2016.with_defaults(v_M_tilde) + >>> fv_M + FiberForceVelocityDeGroote2016(v_M/v_M_max, -0.318, -8.149, -0.374, 0.886) + + To inspect the actual symbolic expression that this function represents, + we can call the :meth:`~.doit` method on an instance. We'll use the keyword + argument ``evaluate=False`` as this will keep the expression in its + canonical form and won't simplify any constants. + + >>> fv_M.doit(evaluate=False) + 0.886 - 0.318*log(-8.149*v_M/v_M_max - 0.374 + sqrt(1 + (-8.149*v_M/v_M_max + - 0.374)**2)) + + The function can also be differentiated. We'll differentiate with respect + to v_M using the ``diff`` method on an instance with the single positional + argument ``v_M``. + + >>> fv_M.diff(v_M) + 2.591382*(1 + (-8.149*v_M/v_M_max - 0.374)**2)**(-1/2)/v_M_max + + References + ========== + + .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation + of direct collocation optimal control problem formulations for + solving the muscle redundancy problem, Annals of biomedical + engineering, 44(10), (2016) pp. 2922-2936 + + """ + + @classmethod + def with_defaults(cls, v_M_tilde): + r"""Recommended constructor that will use the published constants. + + Explanation + =========== + + Returns a new instance of the muscle fiber force-velocity function + using the four constant values specified in the original publication. + + These have the values: + + $c_0 = -0.318$ + $c_1 = -8.149$ + $c_2 = -0.374$ + $c_3 = 0.886$ + + Parameters + ========== + + v_M_tilde : Any (sympifiable) + Normalized muscle fiber extension velocity. + + """ + c0 = Float('-0.318') + c1 = Float('-8.149') + c2 = Float('-0.374') + c3 = Float('0.886') + return cls(v_M_tilde, c0, c1, c2, c3) + + @classmethod + def eval(cls, v_M_tilde, c0, c1, c2, c3): + """Evaluation of basic inputs. + + Parameters + ========== + + v_M_tilde : Any (sympifiable) + Normalized muscle fiber extension velocity. + c0 : Any (sympifiable) + The first constant in the characteristic equation. The published + value is ``-0.318``. + c1 : Any (sympifiable) + The second constant in the characteristic equation. The published + value is ``-8.149``. + c2 : Any (sympifiable) + The third constant in the characteristic equation. The published + value is ``-0.374``. + c3 : Any (sympifiable) + The fourth constant in the characteristic equation. The published + value is ``0.886``. + + """ + pass + + def _eval_evalf(self, prec): + """Evaluate the expression numerically using ``evalf``.""" + return self.doit(deep=False, evaluate=False)._eval_evalf(prec) + + def doit(self, deep=True, evaluate=True, **hints): + """Evaluate the expression defining the function. + + Parameters + ========== + + deep : bool + Whether ``doit`` should be recursively called. Default is ``True``. + evaluate : bool. + Whether the SymPy expression should be evaluated as it is + constructed. If ``False``, then no constant folding will be + conducted which will leave the expression in a more numerically- + stable for values of ``v_M_tilde`` that correspond to a sensible + operating range for a musculotendon. Default is ``True``. + **kwargs : dict[str, Any] + Additional keyword argument pairs to be recursively passed to + ``doit``. + + """ + v_M_tilde, *constants = self.args + if deep: + hints['evaluate'] = evaluate + v_M_tilde = v_M_tilde.doit(deep=deep, **hints) + c0, c1, c2, c3 = [c.doit(deep=deep, **hints) for c in constants] + else: + c0, c1, c2, c3 = constants + + if evaluate: + return c0*log(c1*v_M_tilde + c2 + sqrt((c1*v_M_tilde + c2)**2 + 1)) + c3 + + return c0*log(c1*v_M_tilde + c2 + sqrt(UnevaluatedExpr(c1*v_M_tilde + c2)**2 + 1)) + c3 + + def fdiff(self, argindex=1): + """Derivative of the function with respect to a single argument. + + Parameters + ========== + + argindex : int + The index of the function's arguments with respect to which the + derivative should be taken. Argument indexes start at ``1``. + Default is ``1``. + + """ + v_M_tilde, c0, c1, c2, c3 = self.args + if argindex == 1: + return c0*c1/sqrt(UnevaluatedExpr(c1*v_M_tilde + c2)**2 + 1) + elif argindex == 2: + return log( + c1*v_M_tilde + c2 + + sqrt(UnevaluatedExpr(c1*v_M_tilde + c2)**2 + 1) + ) + elif argindex == 3: + return c0*v_M_tilde/sqrt(UnevaluatedExpr(c1*v_M_tilde + c2)**2 + 1) + elif argindex == 4: + return c0/sqrt(UnevaluatedExpr(c1*v_M_tilde + c2)**2 + 1) + elif argindex == 5: + return Integer(1) + + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """Inverse function. + + Parameters + ========== + + argindex : int + Value to start indexing the arguments at. Default is ``1``. + + """ + return FiberForceVelocityInverseDeGroote2016 + + def _latex(self, printer): + """Print a LaTeX representation of the function defining the curve. + + Parameters + ========== + + printer : Printer + The printer to be used to print the LaTeX string representation. + + """ + v_M_tilde = self.args[0] + _v_M_tilde = printer._print(v_M_tilde) + return r'\operatorname{fv}^M \left( %s \right)' % _v_M_tilde + + +class FiberForceVelocityInverseDeGroote2016(CharacteristicCurveFunction): + r"""Inverse muscle fiber force-velocity curve based on De Groote et al., + 2016 [1]_. + + Explanation + =========== + + Gives the normalized muscle fiber velocity that produces a specific + normalized muscle fiber force. + + The function is defined by the equation: + + ${fv^M}^{-1} = \frac{\sinh{\frac{fv^M - c_3}{c_0}} - c_2}{c_1}$ + + with constant values of $c_0 = -0.318$, $c_1 = -8.149$, $c_2 = -0.374$, and + $c_3 = 0.886$. This function is the exact analytical inverse of the related + muscle fiber force-velocity curve ``FiberForceVelocityDeGroote2016``. + + While it is possible to change the constant values, these were carefully + selected in the original publication to give the characteristic curve + specific and required properties. For example, the function produces a + normalized muscle fiber force of 1 when the muscle fibers are contracting + isometrically (they have an extension rate of 0). + + Examples + ======== + + The preferred way to instantiate :class:`FiberForceVelocityInverseDeGroote2016` + is using the :meth:`~.with_defaults` constructor because this will automatically + populate the constants within the characteristic curve equation with the + floating point values from the original publication. This constructor takes + a single argument corresponding to normalized muscle fiber force-velocity + component of the muscle fiber force. We'll create a :class:`~.Symbol` called + ``fv_M`` to represent this. + + >>> from sympy import Symbol + >>> from sympy.physics.biomechanics import FiberForceVelocityInverseDeGroote2016 + >>> fv_M = Symbol('fv_M') + >>> v_M_tilde = FiberForceVelocityInverseDeGroote2016.with_defaults(fv_M) + >>> v_M_tilde + FiberForceVelocityInverseDeGroote2016(fv_M, -0.318, -8.149, -0.374, 0.886) + + It's also possible to populate the four constants with your own values too. + + >>> from sympy import symbols + >>> c0, c1, c2, c3 = symbols('c0 c1 c2 c3') + >>> v_M_tilde = FiberForceVelocityInverseDeGroote2016(fv_M, c0, c1, c2, c3) + >>> v_M_tilde + FiberForceVelocityInverseDeGroote2016(fv_M, c0, c1, c2, c3) + + To inspect the actual symbolic expression that this function represents, + we can call the :meth:`~.doit` method on an instance. We'll use the keyword + argument ``evaluate=False`` as this will keep the expression in its + canonical form and won't simplify any constants. + + >>> v_M_tilde.doit(evaluate=False) + (-c2 + sinh((-c3 + fv_M)/c0))/c1 + + The function can also be differentiated. We'll differentiate with respect + to fv_M using the ``diff`` method on an instance with the single positional + argument ``fv_M``. + + >>> v_M_tilde.diff(fv_M) + cosh((-c3 + fv_M)/c0)/(c0*c1) + + References + ========== + + .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation + of direct collocation optimal control problem formulations for + solving the muscle redundancy problem, Annals of biomedical + engineering, 44(10), (2016) pp. 2922-2936 + + """ + + @classmethod + def with_defaults(cls, fv_M): + r"""Recommended constructor that will use the published constants. + + Explanation + =========== + + Returns a new instance of the inverse muscle fiber force-velocity + function using the four constant values specified in the original + publication. + + These have the values: + + $c_0 = -0.318$ + $c_1 = -8.149$ + $c_2 = -0.374$ + $c_3 = 0.886$ + + Parameters + ========== + + fv_M : Any (sympifiable) + Normalized muscle fiber extension velocity. + + """ + c0 = Float('-0.318') + c1 = Float('-8.149') + c2 = Float('-0.374') + c3 = Float('0.886') + return cls(fv_M, c0, c1, c2, c3) + + @classmethod + def eval(cls, fv_M, c0, c1, c2, c3): + """Evaluation of basic inputs. + + Parameters + ========== + + fv_M : Any (sympifiable) + Normalized muscle fiber force as a function of muscle fiber + extension velocity. + c0 : Any (sympifiable) + The first constant in the characteristic equation. The published + value is ``-0.318``. + c1 : Any (sympifiable) + The second constant in the characteristic equation. The published + value is ``-8.149``. + c2 : Any (sympifiable) + The third constant in the characteristic equation. The published + value is ``-0.374``. + c3 : Any (sympifiable) + The fourth constant in the characteristic equation. The published + value is ``0.886``. + + """ + pass + + def _eval_evalf(self, prec): + """Evaluate the expression numerically using ``evalf``.""" + return self.doit(deep=False, evaluate=False)._eval_evalf(prec) + + def doit(self, deep=True, evaluate=True, **hints): + """Evaluate the expression defining the function. + + Parameters + ========== + + deep : bool + Whether ``doit`` should be recursively called. Default is ``True``. + evaluate : bool. + Whether the SymPy expression should be evaluated as it is + constructed. If ``False``, then no constant folding will be + conducted which will leave the expression in a more numerically- + stable for values of ``fv_M`` that correspond to a sensible + operating range for a musculotendon. Default is ``True``. + **kwargs : dict[str, Any] + Additional keyword argument pairs to be recursively passed to + ``doit``. + + """ + fv_M, *constants = self.args + if deep: + hints['evaluate'] = evaluate + fv_M = fv_M.doit(deep=deep, **hints) + c0, c1, c2, c3 = [c.doit(deep=deep, **hints) for c in constants] + else: + c0, c1, c2, c3 = constants + + if evaluate: + return (sinh((fv_M - c3)/c0) - c2)/c1 + + return (sinh(UnevaluatedExpr(fv_M - c3)/c0) - c2)/c1 + + def fdiff(self, argindex=1): + """Derivative of the function with respect to a single argument. + + Parameters + ========== + + argindex : int + The index of the function's arguments with respect to which the + derivative should be taken. Argument indexes start at ``1``. + Default is ``1``. + + """ + fv_M, c0, c1, c2, c3 = self.args + if argindex == 1: + return cosh((fv_M - c3)/c0)/(c0*c1) + elif argindex == 2: + return (c3 - fv_M)*cosh((fv_M - c3)/c0)/(c0**2*c1) + elif argindex == 3: + return (c2 - sinh((fv_M - c3)/c0))/c1**2 + elif argindex == 4: + return -1/c1 + elif argindex == 5: + return -cosh((fv_M - c3)/c0)/(c0*c1) + + raise ArgumentIndexError(self, argindex) + + def inverse(self, argindex=1): + """Inverse function. + + Parameters + ========== + + argindex : int + Value to start indexing the arguments at. Default is ``1``. + + """ + return FiberForceVelocityDeGroote2016 + + def _latex(self, printer): + """Print a LaTeX representation of the function defining the curve. + + Parameters + ========== + + printer : Printer + The printer to be used to print the LaTeX string representation. + + """ + fv_M = self.args[0] + _fv_M = printer._print(fv_M) + return r'\left( \operatorname{fv}^M \right)^{-1} \left( %s \right)' % _fv_M + + +@dataclass(frozen=True) +class CharacteristicCurveCollection: + """Simple data container to group together related characteristic curves.""" + tendon_force_length: CharacteristicCurveFunction + tendon_force_length_inverse: CharacteristicCurveFunction + fiber_force_length_passive: CharacteristicCurveFunction + fiber_force_length_passive_inverse: CharacteristicCurveFunction + fiber_force_length_active: CharacteristicCurveFunction + fiber_force_velocity: CharacteristicCurveFunction + fiber_force_velocity_inverse: CharacteristicCurveFunction + + def __iter__(self): + """Iterator support for ``CharacteristicCurveCollection``.""" + yield self.tendon_force_length + yield self.tendon_force_length_inverse + yield self.fiber_force_length_passive + yield self.fiber_force_length_passive_inverse + yield self.fiber_force_length_active + yield self.fiber_force_velocity + yield self.fiber_force_velocity_inverse diff --git a/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/musculotendon.py b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/musculotendon.py new file mode 100644 index 0000000000000000000000000000000000000000..e16d66373da9107adee2e3b8418f657ee5879298 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/musculotendon.py @@ -0,0 +1,1424 @@ +"""Implementations of musculotendon models. + +Musculotendon models are a critical component of biomechanical models, one that +differentiates them from pure multibody systems. Musculotendon models produce a +force dependent on their level of activation, their length, and their +extension velocity. Length- and extension velocity-dependent force production +are governed by force-length and force-velocity characteristics. +These are normalized functions that are dependent on the musculotendon's state +and are specific to a given musculotendon model. + +""" + +from abc import abstractmethod +from enum import IntEnum, unique + +from sympy.core.numbers import Float, Integer +from sympy.core.symbol import Symbol, symbols +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import cos, sin +from sympy.matrices.dense import MutableDenseMatrix as Matrix, diag, eye, zeros +from sympy.physics.biomechanics.activation import ActivationBase +from sympy.physics.biomechanics.curve import ( + CharacteristicCurveCollection, + FiberForceLengthActiveDeGroote2016, + FiberForceLengthPassiveDeGroote2016, + FiberForceLengthPassiveInverseDeGroote2016, + FiberForceVelocityDeGroote2016, + FiberForceVelocityInverseDeGroote2016, + TendonForceLengthDeGroote2016, + TendonForceLengthInverseDeGroote2016, +) +from sympy.physics.biomechanics._mixin import _NamedMixin +from sympy.physics.mechanics.actuator import ForceActuator +from sympy.physics.vector.functions import dynamicsymbols + + +__all__ = [ + 'MusculotendonBase', + 'MusculotendonDeGroote2016', + 'MusculotendonFormulation', +] + + +@unique +class MusculotendonFormulation(IntEnum): + """Enumeration of types of musculotendon dynamics formulations. + + Explanation + =========== + + An (integer) enumeration is used as it allows for clearer selection of the + different formulations of musculotendon dynamics. + + Members + ======= + + RIGID_TENDON : 0 + A rigid tendon model. + FIBER_LENGTH_EXPLICIT : 1 + An explicit elastic tendon model with the muscle fiber length (l_M) as + the state variable. + TENDON_FORCE_EXPLICIT : 2 + An explicit elastic tendon model with the tendon force (F_T) as the + state variable. + FIBER_LENGTH_IMPLICIT : 3 + An implicit elastic tendon model with the muscle fiber length (l_M) as + the state variable and the muscle fiber velocity as an additional input + variable. + TENDON_FORCE_IMPLICIT : 4 + An implicit elastic tendon model with the tendon force (F_T) as the + state variable as the muscle fiber velocity as an additional input + variable. + + """ + + RIGID_TENDON = 0 + FIBER_LENGTH_EXPLICIT = 1 + TENDON_FORCE_EXPLICIT = 2 + FIBER_LENGTH_IMPLICIT = 3 + TENDON_FORCE_IMPLICIT = 4 + + def __str__(self): + """Returns a string representation of the enumeration value. + + Notes + ===== + + This hard coding is required due to an incompatibility between the + ``IntEnum`` implementations in Python 3.10 and Python 3.11 + (https://github.com/python/cpython/issues/84247). From Python 3.11 + onwards, the ``__str__`` method uses ``int.__str__``, whereas prior it + used ``Enum.__str__``. Once Python 3.11 becomes the minimum version + supported by SymPy, this method override can be removed. + + """ + return str(self.value) + + +_DEFAULT_MUSCULOTENDON_FORMULATION = MusculotendonFormulation.RIGID_TENDON + + +class MusculotendonBase(ForceActuator, _NamedMixin): + r"""Abstract base class for all musculotendon classes to inherit from. + + Explanation + =========== + + A musculotendon generates a contractile force based on its activation, + length, and shortening velocity. This abstract base class is to be inherited + by all musculotendon subclasses that implement different characteristic + musculotendon curves. Characteristic musculotendon curves are required for + the tendon force-length, passive fiber force-length, active fiber force- + length, and fiber force-velocity relationships. + + Parameters + ========== + + name : str + The name identifier associated with the musculotendon. This name is used + as a suffix when automatically generated symbols are instantiated. It + must be a string of nonzero length. + pathway : PathwayBase + The pathway that the actuator follows. This must be an instance of a + concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``. + activation_dynamics : ActivationBase + The activation dynamics that will be modeled within the musculotendon. + This must be an instance of a concrete subclass of ``ActivationBase``, + e.g. ``FirstOrderActivationDeGroote2016``. + musculotendon_dynamics : MusculotendonFormulation | int + The formulation of musculotendon dynamics that should be used + internally, i.e. rigid or elastic tendon model, the choice of + musculotendon state etc. This must be a member of the integer + enumeration ``MusculotendonFormulation`` or an integer that can be cast + to a member. To use a rigid tendon formulation, set this to + ``MusculotendonFormulation.RIGID_TENDON`` (or the integer value ``0``, + which will be cast to the enumeration member). There are four possible + formulations for an elastic tendon model. To use an explicit formulation + with the fiber length as the state, set this to + ``MusculotendonFormulation.FIBER_LENGTH_EXPLICIT`` (or the integer value + ``1``). To use an explicit formulation with the tendon force as the + state, set this to ``MusculotendonFormulation.TENDON_FORCE_EXPLICIT`` + (or the integer value ``2``). To use an implicit formulation with the + fiber length as the state, set this to + ``MusculotendonFormulation.FIBER_LENGTH_IMPLICIT`` (or the integer value + ``3``). To use an implicit formulation with the tendon force as the + state, set this to ``MusculotendonFormulation.TENDON_FORCE_IMPLICIT`` + (or the integer value ``4``). The default is + ``MusculotendonFormulation.RIGID_TENDON``, which corresponds to a rigid + tendon formulation. + tendon_slack_length : Expr | None + The length of the tendon when the musculotendon is in its unloaded + state. In a rigid tendon model the tendon length is the tendon slack + length. In all musculotendon models, tendon slack length is used to + normalize tendon length to give + :math:`\tilde{l}^T = \frac{l^T}{l^T_{slack}}`. + peak_isometric_force : Expr | None + The maximum force that the muscle fiber can produce when it is + undergoing an isometric contraction (no lengthening velocity). In all + musculotendon models, peak isometric force is used to normalized tendon + and muscle fiber force to give + :math:`\tilde{F}^T = \frac{F^T}{F^M_{max}}`. + optimal_fiber_length : Expr | None + The muscle fiber length at which the muscle fibers produce no passive + force and their maximum active force. In all musculotendon models, + optimal fiber length is used to normalize muscle fiber length to give + :math:`\tilde{l}^M = \frac{l^M}{l^M_{opt}}`. + maximal_fiber_velocity : Expr | None + The fiber velocity at which, during muscle fiber shortening, the muscle + fibers are unable to produce any active force. In all musculotendon + models, maximal fiber velocity is used to normalize muscle fiber + extension velocity to give :math:`\tilde{v}^M = \frac{v^M}{v^M_{max}}`. + optimal_pennation_angle : Expr | None + The pennation angle when muscle fiber length equals the optimal fiber + length. + fiber_damping_coefficient : Expr | None + The coefficient of damping to be used in the damping element in the + muscle fiber model. + with_defaults : bool + Whether ``with_defaults`` alternate constructors should be used when + automatically constructing child classes. Default is ``False``. + + """ + + def __init__( + self, + name, + pathway, + activation_dynamics, + *, + musculotendon_dynamics=_DEFAULT_MUSCULOTENDON_FORMULATION, + tendon_slack_length=None, + peak_isometric_force=None, + optimal_fiber_length=None, + maximal_fiber_velocity=None, + optimal_pennation_angle=None, + fiber_damping_coefficient=None, + with_defaults=False, + ): + self.name = name + + # Supply a placeholder force to the super initializer, this will be + # replaced later + super().__init__(Symbol('F'), pathway) + + # Activation dynamics + if not isinstance(activation_dynamics, ActivationBase): + msg = ( + f'Can\'t set attribute `activation_dynamics` to ' + f'{activation_dynamics} as it must be of type ' + f'`ActivationBase`, not {type(activation_dynamics)}.' + ) + raise TypeError(msg) + self._activation_dynamics = activation_dynamics + self._child_objects = (self._activation_dynamics, ) + + # Constants + if tendon_slack_length is not None: + self._l_T_slack = tendon_slack_length + else: + self._l_T_slack = Symbol(f'l_T_slack_{self.name}') + if peak_isometric_force is not None: + self._F_M_max = peak_isometric_force + else: + self._F_M_max = Symbol(f'F_M_max_{self.name}') + if optimal_fiber_length is not None: + self._l_M_opt = optimal_fiber_length + else: + self._l_M_opt = Symbol(f'l_M_opt_{self.name}') + if maximal_fiber_velocity is not None: + self._v_M_max = maximal_fiber_velocity + else: + self._v_M_max = Symbol(f'v_M_max_{self.name}') + if optimal_pennation_angle is not None: + self._alpha_opt = optimal_pennation_angle + else: + self._alpha_opt = Symbol(f'alpha_opt_{self.name}') + if fiber_damping_coefficient is not None: + self._beta = fiber_damping_coefficient + else: + self._beta = Symbol(f'beta_{self.name}') + + # Musculotendon dynamics + self._with_defaults = with_defaults + if musculotendon_dynamics == MusculotendonFormulation.RIGID_TENDON: + self._rigid_tendon_musculotendon_dynamics() + elif musculotendon_dynamics == MusculotendonFormulation.FIBER_LENGTH_EXPLICIT: + self._fiber_length_explicit_musculotendon_dynamics() + elif musculotendon_dynamics == MusculotendonFormulation.TENDON_FORCE_EXPLICIT: + self._tendon_force_explicit_musculotendon_dynamics() + elif musculotendon_dynamics == MusculotendonFormulation.FIBER_LENGTH_IMPLICIT: + self._fiber_length_implicit_musculotendon_dynamics() + elif musculotendon_dynamics == MusculotendonFormulation.TENDON_FORCE_IMPLICIT: + self._tendon_force_implicit_musculotendon_dynamics() + else: + msg = ( + f'Musculotendon dynamics {repr(musculotendon_dynamics)} ' + f'passed to `musculotendon_dynamics` was of type ' + f'{type(musculotendon_dynamics)}, must be ' + f'{MusculotendonFormulation}.' + ) + raise TypeError(msg) + self._musculotendon_dynamics = musculotendon_dynamics + + # Must override the placeholder value in `self._force` now that the + # actual force has been calculated by + # `self.__musculotendon_dynamics`. + # Note that `self._force` assumes forces are expansile, musculotendon + # forces are contractile hence the minus sign preceding `self._F_T` + # (the tendon force). + self._force = -self._F_T + + @classmethod + def with_defaults( + cls, + name, + pathway, + activation_dynamics, + *, + musculotendon_dynamics=_DEFAULT_MUSCULOTENDON_FORMULATION, + tendon_slack_length=None, + peak_isometric_force=None, + optimal_fiber_length=None, + maximal_fiber_velocity=Float('10.0'), + optimal_pennation_angle=Float('0.0'), + fiber_damping_coefficient=Float('0.1'), + ): + r"""Recommended constructor that will use the published constants. + + Explanation + =========== + + Returns a new instance of the musculotendon class using recommended + values for ``v_M_max``, ``alpha_opt``, and ``beta``. The values are: + + :math:`v^M_{max} = 10` + :math:`\alpha_{opt} = 0` + :math:`\beta = \frac{1}{10}` + + The musculotendon curves are also instantiated using the constants from + the original publication. + + Parameters + ========== + + name : str + The name identifier associated with the musculotendon. This name is + used as a suffix when automatically generated symbols are + instantiated. It must be a string of nonzero length. + pathway : PathwayBase + The pathway that the actuator follows. This must be an instance of a + concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``. + activation_dynamics : ActivationBase + The activation dynamics that will be modeled within the + musculotendon. This must be an instance of a concrete subclass of + ``ActivationBase``, e.g. ``FirstOrderActivationDeGroote2016``. + musculotendon_dynamics : MusculotendonFormulation | int + The formulation of musculotendon dynamics that should be used + internally, i.e. rigid or elastic tendon model, the choice of + musculotendon state etc. This must be a member of the integer + enumeration ``MusculotendonFormulation`` or an integer that can be + cast to a member. To use a rigid tendon formulation, set this to + ``MusculotendonFormulation.RIGID_TENDON`` (or the integer value + ``0``, which will be cast to the enumeration member). There are four + possible formulations for an elastic tendon model. To use an + explicit formulation with the fiber length as the state, set this to + ``MusculotendonFormulation.FIBER_LENGTH_EXPLICIT`` (or the integer + value ``1``). To use an explicit formulation with the tendon force + as the state, set this to + ``MusculotendonFormulation.TENDON_FORCE_EXPLICIT`` (or the integer + value ``2``). To use an implicit formulation with the fiber length + as the state, set this to + ``MusculotendonFormulation.FIBER_LENGTH_IMPLICIT`` (or the integer + value ``3``). To use an implicit formulation with the tendon force + as the state, set this to + ``MusculotendonFormulation.TENDON_FORCE_IMPLICIT`` (or the integer + value ``4``). The default is + ``MusculotendonFormulation.RIGID_TENDON``, which corresponds to a + rigid tendon formulation. + tendon_slack_length : Expr | None + The length of the tendon when the musculotendon is in its unloaded + state. In a rigid tendon model the tendon length is the tendon slack + length. In all musculotendon models, tendon slack length is used to + normalize tendon length to give + :math:`\tilde{l}^T = \frac{l^T}{l^T_{slack}}`. + peak_isometric_force : Expr | None + The maximum force that the muscle fiber can produce when it is + undergoing an isometric contraction (no lengthening velocity). In + all musculotendon models, peak isometric force is used to normalized + tendon and muscle fiber force to give + :math:`\tilde{F}^T = \frac{F^T}{F^M_{max}}`. + optimal_fiber_length : Expr | None + The muscle fiber length at which the muscle fibers produce no + passive force and their maximum active force. In all musculotendon + models, optimal fiber length is used to normalize muscle fiber + length to give :math:`\tilde{l}^M = \frac{l^M}{l^M_{opt}}`. + maximal_fiber_velocity : Expr | None + The fiber velocity at which, during muscle fiber shortening, the + muscle fibers are unable to produce any active force. In all + musculotendon models, maximal fiber velocity is used to normalize + muscle fiber extension velocity to give + :math:`\tilde{v}^M = \frac{v^M}{v^M_{max}}`. + optimal_pennation_angle : Expr | None + The pennation angle when muscle fiber length equals the optimal + fiber length. + fiber_damping_coefficient : Expr | None + The coefficient of damping to be used in the damping element in the + muscle fiber model. + + """ + return cls( + name, + pathway, + activation_dynamics=activation_dynamics, + musculotendon_dynamics=musculotendon_dynamics, + tendon_slack_length=tendon_slack_length, + peak_isometric_force=peak_isometric_force, + optimal_fiber_length=optimal_fiber_length, + maximal_fiber_velocity=maximal_fiber_velocity, + optimal_pennation_angle=optimal_pennation_angle, + fiber_damping_coefficient=fiber_damping_coefficient, + with_defaults=True, + ) + + @abstractmethod + def curves(cls): + """Return a ``CharacteristicCurveCollection`` of the curves related to + the specific model.""" + pass + + @property + def tendon_slack_length(self): + r"""Symbol or value corresponding to the tendon slack length constant. + + Explanation + =========== + + The length of the tendon when the musculotendon is in its unloaded + state. In a rigid tendon model the tendon length is the tendon slack + length. In all musculotendon models, tendon slack length is used to + normalize tendon length to give + :math:`\tilde{l}^T = \frac{l^T}{l^T_{slack}}`. + + The alias ``l_T_slack`` can also be used to access the same attribute. + + """ + return self._l_T_slack + + @property + def l_T_slack(self): + r"""Symbol or value corresponding to the tendon slack length constant. + + Explanation + =========== + + The length of the tendon when the musculotendon is in its unloaded + state. In a rigid tendon model the tendon length is the tendon slack + length. In all musculotendon models, tendon slack length is used to + normalize tendon length to give + :math:`\tilde{l}^T = \frac{l^T}{l^T_{slack}}`. + + The alias ``tendon_slack_length`` can also be used to access the same + attribute. + + """ + return self._l_T_slack + + @property + def peak_isometric_force(self): + r"""Symbol or value corresponding to the peak isometric force constant. + + Explanation + =========== + + The maximum force that the muscle fiber can produce when it is + undergoing an isometric contraction (no lengthening velocity). In all + musculotendon models, peak isometric force is used to normalized tendon + and muscle fiber force to give + :math:`\tilde{F}^T = \frac{F^T}{F^M_{max}}`. + + The alias ``F_M_max`` can also be used to access the same attribute. + + """ + return self._F_M_max + + @property + def F_M_max(self): + r"""Symbol or value corresponding to the peak isometric force constant. + + Explanation + =========== + + The maximum force that the muscle fiber can produce when it is + undergoing an isometric contraction (no lengthening velocity). In all + musculotendon models, peak isometric force is used to normalized tendon + and muscle fiber force to give + :math:`\tilde{F}^T = \frac{F^T}{F^M_{max}}`. + + The alias ``peak_isometric_force`` can also be used to access the same + attribute. + + """ + return self._F_M_max + + @property + def optimal_fiber_length(self): + r"""Symbol or value corresponding to the optimal fiber length constant. + + Explanation + =========== + + The muscle fiber length at which the muscle fibers produce no passive + force and their maximum active force. In all musculotendon models, + optimal fiber length is used to normalize muscle fiber length to give + :math:`\tilde{l}^M = \frac{l^M}{l^M_{opt}}`. + + The alias ``l_M_opt`` can also be used to access the same attribute. + + """ + return self._l_M_opt + + @property + def l_M_opt(self): + r"""Symbol or value corresponding to the optimal fiber length constant. + + Explanation + =========== + + The muscle fiber length at which the muscle fibers produce no passive + force and their maximum active force. In all musculotendon models, + optimal fiber length is used to normalize muscle fiber length to give + :math:`\tilde{l}^M = \frac{l^M}{l^M_{opt}}`. + + The alias ``optimal_fiber_length`` can also be used to access the same + attribute. + + """ + return self._l_M_opt + + @property + def maximal_fiber_velocity(self): + r"""Symbol or value corresponding to the maximal fiber velocity constant. + + Explanation + =========== + + The fiber velocity at which, during muscle fiber shortening, the muscle + fibers are unable to produce any active force. In all musculotendon + models, maximal fiber velocity is used to normalize muscle fiber + extension velocity to give :math:`\tilde{v}^M = \frac{v^M}{v^M_{max}}`. + + The alias ``v_M_max`` can also be used to access the same attribute. + + """ + return self._v_M_max + + @property + def v_M_max(self): + r"""Symbol or value corresponding to the maximal fiber velocity constant. + + Explanation + =========== + + The fiber velocity at which, during muscle fiber shortening, the muscle + fibers are unable to produce any active force. In all musculotendon + models, maximal fiber velocity is used to normalize muscle fiber + extension velocity to give :math:`\tilde{v}^M = \frac{v^M}{v^M_{max}}`. + + The alias ``maximal_fiber_velocity`` can also be used to access the same + attribute. + + """ + return self._v_M_max + + @property + def optimal_pennation_angle(self): + """Symbol or value corresponding to the optimal pennation angle + constant. + + Explanation + =========== + + The pennation angle when muscle fiber length equals the optimal fiber + length. + + The alias ``alpha_opt`` can also be used to access the same attribute. + + """ + return self._alpha_opt + + @property + def alpha_opt(self): + """Symbol or value corresponding to the optimal pennation angle + constant. + + Explanation + =========== + + The pennation angle when muscle fiber length equals the optimal fiber + length. + + The alias ``optimal_pennation_angle`` can also be used to access the + same attribute. + + """ + return self._alpha_opt + + @property + def fiber_damping_coefficient(self): + """Symbol or value corresponding to the fiber damping coefficient + constant. + + Explanation + =========== + + The coefficient of damping to be used in the damping element in the + muscle fiber model. + + The alias ``beta`` can also be used to access the same attribute. + + """ + return self._beta + + @property + def beta(self): + """Symbol or value corresponding to the fiber damping coefficient + constant. + + Explanation + =========== + + The coefficient of damping to be used in the damping element in the + muscle fiber model. + + The alias ``fiber_damping_coefficient`` can also be used to access the + same attribute. + + """ + return self._beta + + @property + def activation_dynamics(self): + """Activation dynamics model governing this musculotendon's activation. + + Explanation + =========== + + Returns the instance of a subclass of ``ActivationBase`` that governs + the relationship between excitation and activation that is used to + represent the activation dynamics of this musculotendon. + + """ + return self._activation_dynamics + + @property + def excitation(self): + """Dynamic symbol representing excitation. + + Explanation + =========== + + The alias ``e`` can also be used to access the same attribute. + + """ + return self._activation_dynamics._e + + @property + def e(self): + """Dynamic symbol representing excitation. + + Explanation + =========== + + The alias ``excitation`` can also be used to access the same attribute. + + """ + return self._activation_dynamics._e + + @property + def activation(self): + """Dynamic symbol representing activation. + + Explanation + =========== + + The alias ``a`` can also be used to access the same attribute. + + """ + return self._activation_dynamics._a + + @property + def a(self): + """Dynamic symbol representing activation. + + Explanation + =========== + + The alias ``activation`` can also be used to access the same attribute. + + """ + return self._activation_dynamics._a + + @property + def musculotendon_dynamics(self): + """The choice of rigid or type of elastic tendon musculotendon dynamics. + + Explanation + =========== + + The formulation of musculotendon dynamics that should be used + internally, i.e. rigid or elastic tendon model, the choice of + musculotendon state etc. This must be a member of the integer + enumeration ``MusculotendonFormulation`` or an integer that can be cast + to a member. To use a rigid tendon formulation, set this to + ``MusculotendonFormulation.RIGID_TENDON`` (or the integer value ``0``, + which will be cast to the enumeration member). There are four possible + formulations for an elastic tendon model. To use an explicit formulation + with the fiber length as the state, set this to + ``MusculotendonFormulation.FIBER_LENGTH_EXPLICIT`` (or the integer value + ``1``). To use an explicit formulation with the tendon force as the + state, set this to ``MusculotendonFormulation.TENDON_FORCE_EXPLICIT`` + (or the integer value ``2``). To use an implicit formulation with the + fiber length as the state, set this to + ``MusculotendonFormulation.FIBER_LENGTH_IMPLICIT`` (or the integer value + ``3``). To use an implicit formulation with the tendon force as the + state, set this to ``MusculotendonFormulation.TENDON_FORCE_IMPLICIT`` + (or the integer value ``4``). The default is + ``MusculotendonFormulation.RIGID_TENDON``, which corresponds to a rigid + tendon formulation. + + """ + return self._musculotendon_dynamics + + def _rigid_tendon_musculotendon_dynamics(self): + """Rigid tendon musculotendon.""" + self._l_MT = self.pathway.length + self._v_MT = self.pathway.extension_velocity + self._l_T = self._l_T_slack + self._l_T_tilde = Integer(1) + self._l_M = sqrt((self._l_MT - self._l_T)**2 + (self._l_M_opt*sin(self._alpha_opt))**2) + self._l_M_tilde = self._l_M/self._l_M_opt + self._v_M = self._v_MT*(self._l_MT - self._l_T_slack)/self._l_M + self._v_M_tilde = self._v_M/self._v_M_max + if self._with_defaults: + self._fl_T = self.curves.tendon_force_length.with_defaults(self._l_T_tilde) + self._fl_M_pas = self.curves.fiber_force_length_passive.with_defaults(self._l_M_tilde) + self._fl_M_act = self.curves.fiber_force_length_active.with_defaults(self._l_M_tilde) + self._fv_M = self.curves.fiber_force_velocity.with_defaults(self._v_M_tilde) + else: + fl_T_constants = symbols(f'c_0:4_fl_T_{self.name}') + self._fl_T = self.curves.tendon_force_length(self._l_T_tilde, *fl_T_constants) + fl_M_pas_constants = symbols(f'c_0:2_fl_M_pas_{self.name}') + self._fl_M_pas = self.curves.fiber_force_length_passive(self._l_M_tilde, *fl_M_pas_constants) + fl_M_act_constants = symbols(f'c_0:12_fl_M_act_{self.name}') + self._fl_M_act = self.curves.fiber_force_length_active(self._l_M_tilde, *fl_M_act_constants) + fv_M_constants = symbols(f'c_0:4_fv_M_{self.name}') + self._fv_M = self.curves.fiber_force_velocity(self._v_M_tilde, *fv_M_constants) + self._F_M_tilde = self.a*self._fl_M_act*self._fv_M + self._fl_M_pas + self._beta*self._v_M_tilde + self._F_T_tilde = self._F_M_tilde + self._F_M = self._F_M_tilde*self._F_M_max + self._cos_alpha = cos(self._alpha_opt) + self._F_T = self._F_M*self._cos_alpha + + # Containers + self._state_vars = zeros(0, 1) + self._input_vars = zeros(0, 1) + self._state_eqns = zeros(0, 1) + self._curve_constants = Matrix( + fl_T_constants + + fl_M_pas_constants + + fl_M_act_constants + + fv_M_constants + ) if not self._with_defaults else zeros(0, 1) + + def _fiber_length_explicit_musculotendon_dynamics(self): + """Elastic tendon musculotendon using `l_M_tilde` as a state.""" + self._l_M_tilde = dynamicsymbols(f'l_M_tilde_{self.name}') + self._l_MT = self.pathway.length + self._v_MT = self.pathway.extension_velocity + self._l_M = self._l_M_tilde*self._l_M_opt + self._l_T = self._l_MT - sqrt(self._l_M**2 - (self._l_M_opt*sin(self._alpha_opt))**2) + self._l_T_tilde = self._l_T/self._l_T_slack + self._cos_alpha = (self._l_MT - self._l_T)/self._l_M + if self._with_defaults: + self._fl_T = self.curves.tendon_force_length.with_defaults(self._l_T_tilde) + self._fl_M_pas = self.curves.fiber_force_length_passive.with_defaults(self._l_M_tilde) + self._fl_M_act = self.curves.fiber_force_length_active.with_defaults(self._l_M_tilde) + else: + fl_T_constants = symbols(f'c_0:4_fl_T_{self.name}') + self._fl_T = self.curves.tendon_force_length(self._l_T_tilde, *fl_T_constants) + fl_M_pas_constants = symbols(f'c_0:2_fl_M_pas_{self.name}') + self._fl_M_pas = self.curves.fiber_force_length_passive(self._l_M_tilde, *fl_M_pas_constants) + fl_M_act_constants = symbols(f'c_0:12_fl_M_act_{self.name}') + self._fl_M_act = self.curves.fiber_force_length_active(self._l_M_tilde, *fl_M_act_constants) + self._F_T_tilde = self._fl_T + self._F_T = self._F_T_tilde*self._F_M_max + self._F_M = self._F_T/self._cos_alpha + self._F_M_tilde = self._F_M/self._F_M_max + self._fv_M = (self._F_M_tilde - self._fl_M_pas)/(self.a*self._fl_M_act) + if self._with_defaults: + self._v_M_tilde = self.curves.fiber_force_velocity_inverse.with_defaults(self._fv_M) + else: + fv_M_constants = symbols(f'c_0:4_fv_M_{self.name}') + self._v_M_tilde = self.curves.fiber_force_velocity_inverse(self._fv_M, *fv_M_constants) + self._dl_M_tilde_dt = (self._v_M_max/self._l_M_opt)*self._v_M_tilde + + self._state_vars = Matrix([self._l_M_tilde]) + self._input_vars = zeros(0, 1) + self._state_eqns = Matrix([self._dl_M_tilde_dt]) + self._curve_constants = Matrix( + fl_T_constants + + fl_M_pas_constants + + fl_M_act_constants + + fv_M_constants + ) if not self._with_defaults else zeros(0, 1) + + def _tendon_force_explicit_musculotendon_dynamics(self): + """Elastic tendon musculotendon using `F_T_tilde` as a state.""" + self._F_T_tilde = dynamicsymbols(f'F_T_tilde_{self.name}') + self._l_MT = self.pathway.length + self._v_MT = self.pathway.extension_velocity + self._fl_T = self._F_T_tilde + if self._with_defaults: + self._fl_T_inv = self.curves.tendon_force_length_inverse.with_defaults(self._fl_T) + else: + fl_T_constants = symbols(f'c_0:4_fl_T_{self.name}') + self._fl_T_inv = self.curves.tendon_force_length_inverse(self._fl_T, *fl_T_constants) + self._l_T_tilde = self._fl_T_inv + self._l_T = self._l_T_tilde*self._l_T_slack + self._l_M = sqrt((self._l_MT - self._l_T)**2 + (self._l_M_opt*sin(self._alpha_opt))**2) + self._l_M_tilde = self._l_M/self._l_M_opt + if self._with_defaults: + self._fl_M_pas = self.curves.fiber_force_length_passive.with_defaults(self._l_M_tilde) + self._fl_M_act = self.curves.fiber_force_length_active.with_defaults(self._l_M_tilde) + else: + fl_M_pas_constants = symbols(f'c_0:2_fl_M_pas_{self.name}') + self._fl_M_pas = self.curves.fiber_force_length_passive(self._l_M_tilde, *fl_M_pas_constants) + fl_M_act_constants = symbols(f'c_0:12_fl_M_act_{self.name}') + self._fl_M_act = self.curves.fiber_force_length_active(self._l_M_tilde, *fl_M_act_constants) + self._cos_alpha = (self._l_MT - self._l_T)/self._l_M + self._F_T = self._F_T_tilde*self._F_M_max + self._F_M = self._F_T/self._cos_alpha + self._F_M_tilde = self._F_M/self._F_M_max + self._fv_M = (self._F_M_tilde - self._fl_M_pas)/(self.a*self._fl_M_act) + if self._with_defaults: + self._fv_M_inv = self.curves.fiber_force_velocity_inverse.with_defaults(self._fv_M) + else: + fv_M_constants = symbols(f'c_0:4_fv_M_{self.name}') + self._fv_M_inv = self.curves.fiber_force_velocity_inverse(self._fv_M, *fv_M_constants) + self._v_M_tilde = self._fv_M_inv + self._v_M = self._v_M_tilde*self._v_M_max + self._v_T = self._v_MT - (self._v_M/self._cos_alpha) + self._v_T_tilde = self._v_T/self._l_T_slack + if self._with_defaults: + self._fl_T = self.curves.tendon_force_length.with_defaults(self._l_T_tilde) + else: + self._fl_T = self.curves.tendon_force_length(self._l_T_tilde, *fl_T_constants) + self._dF_T_tilde_dt = self._fl_T.diff(dynamicsymbols._t).subs({self._l_T_tilde.diff(dynamicsymbols._t): self._v_T_tilde}) + + self._state_vars = Matrix([self._F_T_tilde]) + self._input_vars = zeros(0, 1) + self._state_eqns = Matrix([self._dF_T_tilde_dt]) + self._curve_constants = Matrix( + fl_T_constants + + fl_M_pas_constants + + fl_M_act_constants + + fv_M_constants + ) if not self._with_defaults else zeros(0, 1) + + def _fiber_length_implicit_musculotendon_dynamics(self): + raise NotImplementedError + + def _tendon_force_implicit_musculotendon_dynamics(self): + raise NotImplementedError + + @property + def state_vars(self): + """Ordered column matrix of functions of time that represent the state + variables. + + Explanation + =========== + + The alias ``x`` can also be used to access the same attribute. + + """ + state_vars = [self._state_vars] + for child in self._child_objects: + state_vars.append(child.state_vars) + return Matrix.vstack(*state_vars) + + @property + def x(self): + """Ordered column matrix of functions of time that represent the state + variables. + + Explanation + =========== + + The alias ``state_vars`` can also be used to access the same attribute. + + """ + state_vars = [self._state_vars] + for child in self._child_objects: + state_vars.append(child.state_vars) + return Matrix.vstack(*state_vars) + + @property + def input_vars(self): + """Ordered column matrix of functions of time that represent the input + variables. + + Explanation + =========== + + The alias ``r`` can also be used to access the same attribute. + + """ + input_vars = [self._input_vars] + for child in self._child_objects: + input_vars.append(child.input_vars) + return Matrix.vstack(*input_vars) + + @property + def r(self): + """Ordered column matrix of functions of time that represent the input + variables. + + Explanation + =========== + + The alias ``input_vars`` can also be used to access the same attribute. + + """ + input_vars = [self._input_vars] + for child in self._child_objects: + input_vars.append(child.input_vars) + return Matrix.vstack(*input_vars) + + @property + def constants(self): + """Ordered column matrix of non-time varying symbols present in ``M`` + and ``F``. + + Explanation + =========== + + Only symbolic constants are returned. If a numeric type (e.g. ``Float``) + has been used instead of ``Symbol`` for a constant then that attribute + will not be included in the matrix returned by this property. This is + because the primary use of this property attribute is to provide an + ordered sequence of the still-free symbols that require numeric values + during code generation. + + The alias ``p`` can also be used to access the same attribute. + + """ + musculotendon_constants = [ + self._l_T_slack, + self._F_M_max, + self._l_M_opt, + self._v_M_max, + self._alpha_opt, + self._beta, + ] + musculotendon_constants = [ + c for c in musculotendon_constants if not c.is_number + ] + constants = [ + Matrix(musculotendon_constants) + if musculotendon_constants + else zeros(0, 1) + ] + for child in self._child_objects: + constants.append(child.constants) + constants.append(self._curve_constants) + return Matrix.vstack(*constants) + + @property + def p(self): + """Ordered column matrix of non-time varying symbols present in ``M`` + and ``F``. + + Explanation + =========== + + Only symbolic constants are returned. If a numeric type (e.g. ``Float``) + has been used instead of ``Symbol`` for a constant then that attribute + will not be included in the matrix returned by this property. This is + because the primary use of this property attribute is to provide an + ordered sequence of the still-free symbols that require numeric values + during code generation. + + The alias ``constants`` can also be used to access the same attribute. + + """ + musculotendon_constants = [ + self._l_T_slack, + self._F_M_max, + self._l_M_opt, + self._v_M_max, + self._alpha_opt, + self._beta, + ] + musculotendon_constants = [ + c for c in musculotendon_constants if not c.is_number + ] + constants = [ + Matrix(musculotendon_constants) + if musculotendon_constants + else zeros(0, 1) + ] + for child in self._child_objects: + constants.append(child.constants) + constants.append(self._curve_constants) + return Matrix.vstack(*constants) + + @property + def M(self): + """Ordered square matrix of coefficients on the LHS of ``M x' = F``. + + Explanation + =========== + + The square matrix that forms part of the LHS of the linear system of + ordinary differential equations governing the activation dynamics: + + ``M(x, r, t, p) x' = F(x, r, t, p)``. + + As zeroth-order activation dynamics have no state variables, this + linear system has dimension 0 and therefore ``M`` is an empty square + ``Matrix`` with shape (0, 0). + + """ + M = [eye(len(self._state_vars))] + for child in self._child_objects: + M.append(child.M) + return diag(*M) + + @property + def F(self): + """Ordered column matrix of equations on the RHS of ``M x' = F``. + + Explanation + =========== + + The column matrix that forms the RHS of the linear system of ordinary + differential equations governing the activation dynamics: + + ``M(x, r, t, p) x' = F(x, r, t, p)``. + + As zeroth-order activation dynamics have no state variables, this + linear system has dimension 0 and therefore ``F`` is an empty column + ``Matrix`` with shape (0, 1). + + """ + F = [self._state_eqns] + for child in self._child_objects: + F.append(child.F) + return Matrix.vstack(*F) + + def rhs(self): + """Ordered column matrix of equations for the solution of ``M x' = F``. + + Explanation + =========== + + The solution to the linear system of ordinary differential equations + governing the activation dynamics: + + ``M(x, r, t, p) x' = F(x, r, t, p)``. + + As zeroth-order activation dynamics have no state variables, this + linear has dimension 0 and therefore this method returns an empty + column ``Matrix`` with shape (0, 1). + + """ + is_explicit = ( + MusculotendonFormulation.FIBER_LENGTH_EXPLICIT, + MusculotendonFormulation.TENDON_FORCE_EXPLICIT, + ) + if self.musculotendon_dynamics is MusculotendonFormulation.RIGID_TENDON: + child_rhs = [child.rhs() for child in self._child_objects] + return Matrix.vstack(*child_rhs) + elif self.musculotendon_dynamics in is_explicit: + rhs = self._state_eqns + child_rhs = [child.rhs() for child in self._child_objects] + return Matrix.vstack(rhs, *child_rhs) + return self.M.solve(self.F) + + def __repr__(self): + """Returns a string representation to reinstantiate the model.""" + return ( + f'{self.__class__.__name__}({self.name!r}, ' + f'pathway={self.pathway!r}, ' + f'activation_dynamics={self.activation_dynamics!r}, ' + f'musculotendon_dynamics={self.musculotendon_dynamics}, ' + f'tendon_slack_length={self._l_T_slack!r}, ' + f'peak_isometric_force={self._F_M_max!r}, ' + f'optimal_fiber_length={self._l_M_opt!r}, ' + f'maximal_fiber_velocity={self._v_M_max!r}, ' + f'optimal_pennation_angle={self._alpha_opt!r}, ' + f'fiber_damping_coefficient={self._beta!r})' + ) + + def __str__(self): + """Returns a string representation of the expression for musculotendon + force.""" + return str(self.force) + + +class MusculotendonDeGroote2016(MusculotendonBase): + r"""Musculotendon model using the curves of De Groote et al., 2016 [1]_. + + Examples + ======== + + This class models the musculotendon actuator parametrized by the + characteristic curves described in De Groote et al., 2016 [1]_. Like all + musculotendon models in SymPy's biomechanics module, it requires a pathway + to define its line of action. We'll begin by creating a simple + ``LinearPathway`` between two points that our musculotendon will follow. + We'll create a point ``O`` to represent the musculotendon's origin and + another ``I`` to represent its insertion. + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import (LinearPathway, Point, + ... ReferenceFrame, dynamicsymbols) + + >>> N = ReferenceFrame('N') + >>> O, I = O, P = symbols('O, I', cls=Point) + >>> q, u = dynamicsymbols('q, u', real=True) + >>> I.set_pos(O, q*N.x) + >>> O.set_vel(N, 0) + >>> I.set_vel(N, u*N.x) + >>> pathway = LinearPathway(O, I) + >>> pathway.attachments + (O, I) + >>> pathway.length + Abs(q(t)) + >>> pathway.extension_velocity + sign(q(t))*Derivative(q(t), t) + + A musculotendon also takes an instance of an activation dynamics model as + this will be used to provide symbols for the activation in the formulation + of the musculotendon dynamics. We'll use an instance of + ``FirstOrderActivationDeGroote2016`` to represent first-order activation + dynamics. Note that a single name argument needs to be provided as SymPy + will use this as a suffix. + + >>> from sympy.physics.biomechanics import FirstOrderActivationDeGroote2016 + + >>> activation = FirstOrderActivationDeGroote2016('muscle') + >>> activation.x + Matrix([[a_muscle(t)]]) + >>> activation.r + Matrix([[e_muscle(t)]]) + >>> activation.p + Matrix([ + [tau_a_muscle], + [tau_d_muscle], + [ b_muscle]]) + >>> activation.rhs() + Matrix([[((1/2 - tanh(b_muscle*(-a_muscle(t) + e_muscle(t)))/2)*(3*...]]) + + The musculotendon class requires symbols or values to be passed to represent + the constants in the musculotendon dynamics. We'll use SymPy's ``symbols`` + function to create symbols for the maximum isometric force ``F_M_max``, + optimal fiber length ``l_M_opt``, tendon slack length ``l_T_slack``, maximum + fiber velocity ``v_M_max``, optimal pennation angle ``alpha_opt, and fiber + damping coefficient ``beta``. + + >>> F_M_max = symbols('F_M_max', real=True) + >>> l_M_opt = symbols('l_M_opt', real=True) + >>> l_T_slack = symbols('l_T_slack', real=True) + >>> v_M_max = symbols('v_M_max', real=True) + >>> alpha_opt = symbols('alpha_opt', real=True) + >>> beta = symbols('beta', real=True) + + We can then import the class ``MusculotendonDeGroote2016`` from the + biomechanics module and create an instance by passing in the various objects + we have previously instantiated. By default, a musculotendon model with + rigid tendon musculotendon dynamics will be created. + + >>> from sympy.physics.biomechanics import MusculotendonDeGroote2016 + + >>> rigid_tendon_muscle = MusculotendonDeGroote2016( + ... 'muscle', + ... pathway, + ... activation, + ... tendon_slack_length=l_T_slack, + ... peak_isometric_force=F_M_max, + ... optimal_fiber_length=l_M_opt, + ... maximal_fiber_velocity=v_M_max, + ... optimal_pennation_angle=alpha_opt, + ... fiber_damping_coefficient=beta, + ... ) + + We can inspect the various properties of the musculotendon, including + getting the symbolic expression describing the force it produces using its + ``force`` attribute. + + >>> rigid_tendon_muscle.force + -F_M_max*(beta*(-l_T_slack + Abs(q(t)))*sign(q(t))*Derivative(q(t), t)... + + When we created the musculotendon object, we passed in an instance of an + activation dynamics object that governs the activation within the + musculotendon. SymPy makes a design choice here that the activation dynamics + instance will be treated as a child object of the musculotendon dynamics. + Therefore, if we want to inspect the state and input variables associated + with the musculotendon model, we will also be returned the state and input + variables associated with the child object, or the activation dynamics in + this case. As the musculotendon model that we created here uses rigid tendon + dynamics, no additional states or inputs relating to the musculotendon are + introduces. Consequently, the model has a single state associated with it, + the activation, and a single input associated with it, the excitation. The + states and inputs can be inspected using the ``x`` and ``r`` attributes + respectively. Note that both ``x`` and ``r`` have the alias attributes of + ``state_vars`` and ``input_vars``. + + >>> rigid_tendon_muscle.x + Matrix([[a_muscle(t)]]) + >>> rigid_tendon_muscle.r + Matrix([[e_muscle(t)]]) + + To see which constants are symbolic in the musculotendon model, we can use + the ``p`` or ``constants`` attribute. This returns a ``Matrix`` populated + by the constants that are represented by a ``Symbol`` rather than a numeric + value. + + >>> rigid_tendon_muscle.p + Matrix([ + [ l_T_slack], + [ F_M_max], + [ l_M_opt], + [ v_M_max], + [ alpha_opt], + [ beta], + [ tau_a_muscle], + [ tau_d_muscle], + [ b_muscle], + [ c_0_fl_T_muscle], + [ c_1_fl_T_muscle], + [ c_2_fl_T_muscle], + [ c_3_fl_T_muscle], + [ c_0_fl_M_pas_muscle], + [ c_1_fl_M_pas_muscle], + [ c_0_fl_M_act_muscle], + [ c_1_fl_M_act_muscle], + [ c_2_fl_M_act_muscle], + [ c_3_fl_M_act_muscle], + [ c_4_fl_M_act_muscle], + [ c_5_fl_M_act_muscle], + [ c_6_fl_M_act_muscle], + [ c_7_fl_M_act_muscle], + [ c_8_fl_M_act_muscle], + [ c_9_fl_M_act_muscle], + [c_10_fl_M_act_muscle], + [c_11_fl_M_act_muscle], + [ c_0_fv_M_muscle], + [ c_1_fv_M_muscle], + [ c_2_fv_M_muscle], + [ c_3_fv_M_muscle]]) + + Finally, we can call the ``rhs`` method to return a ``Matrix`` that + contains as its elements the righthand side of the ordinary differential + equations corresponding to each of the musculotendon's states. Like the + method with the same name on the ``Method`` classes in SymPy's mechanics + module, this returns a column vector where the number of rows corresponds to + the number of states. For our example here, we have a single state, the + dynamic symbol ``a_muscle(t)``, so the returned value is a 1-by-1 + ``Matrix``. + + >>> rigid_tendon_muscle.rhs() + Matrix([[((1/2 - tanh(b_muscle*(-a_muscle(t) + e_muscle(t)))/2)*(3*...]]) + + The musculotendon class supports elastic tendon musculotendon models in + addition to rigid tendon ones. You can choose to either use the fiber length + or tendon force as an additional state. You can also specify whether an + explicit or implicit formulation should be used. To select a formulation, + pass a member of the ``MusculotendonFormulation`` enumeration to the + ``musculotendon_dynamics`` parameter when calling the constructor. This + enumeration is an ``IntEnum``, so you can also pass an integer, however it + is recommended to use the enumeration as it is clearer which formulation you + are actually selecting. Below, we'll use the ``FIBER_LENGTH_EXPLICIT`` + member to create a musculotendon with an elastic tendon that will use the + (normalized) muscle fiber length as an additional state and will produce + the governing ordinary differential equation in explicit form. + + >>> from sympy.physics.biomechanics import MusculotendonFormulation + + >>> elastic_tendon_muscle = MusculotendonDeGroote2016( + ... 'muscle', + ... pathway, + ... activation, + ... musculotendon_dynamics=MusculotendonFormulation.FIBER_LENGTH_EXPLICIT, + ... tendon_slack_length=l_T_slack, + ... peak_isometric_force=F_M_max, + ... optimal_fiber_length=l_M_opt, + ... maximal_fiber_velocity=v_M_max, + ... optimal_pennation_angle=alpha_opt, + ... fiber_damping_coefficient=beta, + ... ) + + >>> elastic_tendon_muscle.force + -F_M_max*TendonForceLengthDeGroote2016((-sqrt(l_M_opt**2*... + >>> elastic_tendon_muscle.x + Matrix([ + [l_M_tilde_muscle(t)], + [ a_muscle(t)]]) + >>> elastic_tendon_muscle.r + Matrix([[e_muscle(t)]]) + >>> elastic_tendon_muscle.p + Matrix([ + [ l_T_slack], + [ F_M_max], + [ l_M_opt], + [ v_M_max], + [ alpha_opt], + [ beta], + [ tau_a_muscle], + [ tau_d_muscle], + [ b_muscle], + [ c_0_fl_T_muscle], + [ c_1_fl_T_muscle], + [ c_2_fl_T_muscle], + [ c_3_fl_T_muscle], + [ c_0_fl_M_pas_muscle], + [ c_1_fl_M_pas_muscle], + [ c_0_fl_M_act_muscle], + [ c_1_fl_M_act_muscle], + [ c_2_fl_M_act_muscle], + [ c_3_fl_M_act_muscle], + [ c_4_fl_M_act_muscle], + [ c_5_fl_M_act_muscle], + [ c_6_fl_M_act_muscle], + [ c_7_fl_M_act_muscle], + [ c_8_fl_M_act_muscle], + [ c_9_fl_M_act_muscle], + [c_10_fl_M_act_muscle], + [c_11_fl_M_act_muscle], + [ c_0_fv_M_muscle], + [ c_1_fv_M_muscle], + [ c_2_fv_M_muscle], + [ c_3_fv_M_muscle]]) + >>> elastic_tendon_muscle.rhs() + Matrix([ + [v_M_max*FiberForceVelocityInverseDeGroote2016((l_M_opt*...], + [ ((1/2 - tanh(b_muscle*(-a_muscle(t) + e_muscle(t)))/2)*(3*...]]) + + It is strongly recommended to use the alternate ``with_defaults`` + constructor when creating an instance because this will ensure that the + published constants are used in the musculotendon characteristic curves. + + >>> elastic_tendon_muscle = MusculotendonDeGroote2016.with_defaults( + ... 'muscle', + ... pathway, + ... activation, + ... musculotendon_dynamics=MusculotendonFormulation.FIBER_LENGTH_EXPLICIT, + ... tendon_slack_length=l_T_slack, + ... peak_isometric_force=F_M_max, + ... optimal_fiber_length=l_M_opt, + ... ) + + >>> elastic_tendon_muscle.x + Matrix([ + [l_M_tilde_muscle(t)], + [ a_muscle(t)]]) + >>> elastic_tendon_muscle.r + Matrix([[e_muscle(t)]]) + >>> elastic_tendon_muscle.p + Matrix([ + [ l_T_slack], + [ F_M_max], + [ l_M_opt], + [tau_a_muscle], + [tau_d_muscle], + [ b_muscle]]) + + Parameters + ========== + + name : str + The name identifier associated with the musculotendon. This name is used + as a suffix when automatically generated symbols are instantiated. It + must be a string of nonzero length. + pathway : PathwayBase + The pathway that the actuator follows. This must be an instance of a + concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``. + activation_dynamics : ActivationBase + The activation dynamics that will be modeled within the musculotendon. + This must be an instance of a concrete subclass of ``ActivationBase``, + e.g. ``FirstOrderActivationDeGroote2016``. + musculotendon_dynamics : MusculotendonFormulation | int + The formulation of musculotendon dynamics that should be used + internally, i.e. rigid or elastic tendon model, the choice of + musculotendon state etc. This must be a member of the integer + enumeration ``MusculotendonFormulation`` or an integer that can be cast + to a member. To use a rigid tendon formulation, set this to + ``MusculotendonFormulation.RIGID_TENDON`` (or the integer value ``0``, + which will be cast to the enumeration member). There are four possible + formulations for an elastic tendon model. To use an explicit formulation + with the fiber length as the state, set this to + ``MusculotendonFormulation.FIBER_LENGTH_EXPLICIT`` (or the integer value + ``1``). To use an explicit formulation with the tendon force as the + state, set this to ``MusculotendonFormulation.TENDON_FORCE_EXPLICIT`` + (or the integer value ``2``). To use an implicit formulation with the + fiber length as the state, set this to + ``MusculotendonFormulation.FIBER_LENGTH_IMPLICIT`` (or the integer value + ``3``). To use an implicit formulation with the tendon force as the + state, set this to ``MusculotendonFormulation.TENDON_FORCE_IMPLICIT`` + (or the integer value ``4``). The default is + ``MusculotendonFormulation.RIGID_TENDON``, which corresponds to a rigid + tendon formulation. + tendon_slack_length : Expr | None + The length of the tendon when the musculotendon is in its unloaded + state. In a rigid tendon model the tendon length is the tendon slack + length. In all musculotendon models, tendon slack length is used to + normalize tendon length to give + :math:`\tilde{l}^T = \frac{l^T}{l^T_{slack}}`. + peak_isometric_force : Expr | None + The maximum force that the muscle fiber can produce when it is + undergoing an isometric contraction (no lengthening velocity). In all + musculotendon models, peak isometric force is used to normalized tendon + and muscle fiber force to give + :math:`\tilde{F}^T = \frac{F^T}{F^M_{max}}`. + optimal_fiber_length : Expr | None + The muscle fiber length at which the muscle fibers produce no passive + force and their maximum active force. In all musculotendon models, + optimal fiber length is used to normalize muscle fiber length to give + :math:`\tilde{l}^M = \frac{l^M}{l^M_{opt}}`. + maximal_fiber_velocity : Expr | None + The fiber velocity at which, during muscle fiber shortening, the muscle + fibers are unable to produce any active force. In all musculotendon + models, maximal fiber velocity is used to normalize muscle fiber + extension velocity to give :math:`\tilde{v}^M = \frac{v^M}{v^M_{max}}`. + optimal_pennation_angle : Expr | None + The pennation angle when muscle fiber length equals the optimal fiber + length. + fiber_damping_coefficient : Expr | None + The coefficient of damping to be used in the damping element in the + muscle fiber model. + with_defaults : bool + Whether ``with_defaults`` alternate constructors should be used when + automatically constructing child classes. Default is ``False``. + + References + ========== + + .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation + of direct collocation optimal control problem formulations for + solving the muscle redundancy problem, Annals of biomedical + engineering, 44(10), (2016) pp. 2922-2936 + + """ + + curves = CharacteristicCurveCollection( + tendon_force_length=TendonForceLengthDeGroote2016, + tendon_force_length_inverse=TendonForceLengthInverseDeGroote2016, + fiber_force_length_passive=FiberForceLengthPassiveDeGroote2016, + fiber_force_length_passive_inverse=FiberForceLengthPassiveInverseDeGroote2016, + fiber_force_length_active=FiberForceLengthActiveDeGroote2016, + fiber_force_velocity=FiberForceVelocityDeGroote2016, + fiber_force_velocity_inverse=FiberForceVelocityInverseDeGroote2016, + ) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/tests/test_activation.py b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/tests/test_activation.py new file mode 100644 index 0000000000000000000000000000000000000000..a38742f0d42af48dff95295eae869b2c5ef269de --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/tests/test_activation.py @@ -0,0 +1,348 @@ +"""Tests for the ``sympy.physics.biomechanics.activation.py`` module.""" + +import pytest + +from sympy import Symbol +from sympy.core.numbers import Float, Integer, Rational +from sympy.functions.elementary.hyperbolic import tanh +from sympy.matrices import Matrix +from sympy.matrices.dense import zeros +from sympy.physics.mechanics import dynamicsymbols +from sympy.physics.biomechanics import ( + ActivationBase, + FirstOrderActivationDeGroote2016, + ZerothOrderActivation, +) +from sympy.physics.biomechanics._mixin import _NamedMixin +from sympy.simplify.simplify import simplify + + +class TestZerothOrderActivation: + + @staticmethod + def test_class(): + assert issubclass(ZerothOrderActivation, ActivationBase) + assert issubclass(ZerothOrderActivation, _NamedMixin) + assert ZerothOrderActivation.__name__ == 'ZerothOrderActivation' + + @pytest.fixture(autouse=True) + def _zeroth_order_activation_fixture(self): + self.name = 'name' + self.e = dynamicsymbols('e_name') + self.instance = ZerothOrderActivation(self.name) + + def test_instance(self): + instance = ZerothOrderActivation(self.name) + assert isinstance(instance, ZerothOrderActivation) + + def test_with_defaults(self): + instance = ZerothOrderActivation.with_defaults(self.name) + assert isinstance(instance, ZerothOrderActivation) + assert instance == ZerothOrderActivation(self.name) + + def test_name(self): + assert hasattr(self.instance, 'name') + assert self.instance.name == self.name + + def test_order(self): + assert hasattr(self.instance, 'order') + assert self.instance.order == 0 + + def test_excitation_attribute(self): + assert hasattr(self.instance, 'e') + assert hasattr(self.instance, 'excitation') + e_expected = dynamicsymbols('e_name') + assert self.instance.e == e_expected + assert self.instance.excitation == e_expected + assert self.instance.e is self.instance.excitation + + def test_activation_attribute(self): + assert hasattr(self.instance, 'a') + assert hasattr(self.instance, 'activation') + a_expected = dynamicsymbols('e_name') + assert self.instance.a == a_expected + assert self.instance.activation == a_expected + assert self.instance.a is self.instance.activation is self.instance.e + + def test_state_vars_attribute(self): + assert hasattr(self.instance, 'x') + assert hasattr(self.instance, 'state_vars') + assert self.instance.x == self.instance.state_vars + x_expected = zeros(0, 1) + assert self.instance.x == x_expected + assert self.instance.state_vars == x_expected + assert isinstance(self.instance.x, Matrix) + assert isinstance(self.instance.state_vars, Matrix) + assert self.instance.x.shape == (0, 1) + assert self.instance.state_vars.shape == (0, 1) + + def test_input_vars_attribute(self): + assert hasattr(self.instance, 'r') + assert hasattr(self.instance, 'input_vars') + assert self.instance.r == self.instance.input_vars + r_expected = Matrix([self.e]) + assert self.instance.r == r_expected + assert self.instance.input_vars == r_expected + assert isinstance(self.instance.r, Matrix) + assert isinstance(self.instance.input_vars, Matrix) + assert self.instance.r.shape == (1, 1) + assert self.instance.input_vars.shape == (1, 1) + + def test_constants_attribute(self): + assert hasattr(self.instance, 'p') + assert hasattr(self.instance, 'constants') + assert self.instance.p == self.instance.constants + p_expected = zeros(0, 1) + assert self.instance.p == p_expected + assert self.instance.constants == p_expected + assert isinstance(self.instance.p, Matrix) + assert isinstance(self.instance.constants, Matrix) + assert self.instance.p.shape == (0, 1) + assert self.instance.constants.shape == (0, 1) + + def test_M_attribute(self): + assert hasattr(self.instance, 'M') + M_expected = Matrix([]) + assert self.instance.M == M_expected + assert isinstance(self.instance.M, Matrix) + assert self.instance.M.shape == (0, 0) + + def test_F(self): + assert hasattr(self.instance, 'F') + F_expected = zeros(0, 1) + assert self.instance.F == F_expected + assert isinstance(self.instance.F, Matrix) + assert self.instance.F.shape == (0, 1) + + def test_rhs(self): + assert hasattr(self.instance, 'rhs') + rhs_expected = zeros(0, 1) + rhs = self.instance.rhs() + assert rhs == rhs_expected + assert isinstance(rhs, Matrix) + assert rhs.shape == (0, 1) + + def test_repr(self): + expected = 'ZerothOrderActivation(\'name\')' + assert repr(self.instance) == expected + + +class TestFirstOrderActivationDeGroote2016: + + @staticmethod + def test_class(): + assert issubclass(FirstOrderActivationDeGroote2016, ActivationBase) + assert issubclass(FirstOrderActivationDeGroote2016, _NamedMixin) + assert FirstOrderActivationDeGroote2016.__name__ == 'FirstOrderActivationDeGroote2016' + + @pytest.fixture(autouse=True) + def _first_order_activation_de_groote_2016_fixture(self): + self.name = 'name' + self.e = dynamicsymbols('e_name') + self.a = dynamicsymbols('a_name') + self.tau_a = Symbol('tau_a') + self.tau_d = Symbol('tau_d') + self.b = Symbol('b') + self.instance = FirstOrderActivationDeGroote2016( + self.name, + self.tau_a, + self.tau_d, + self.b, + ) + + def test_instance(self): + instance = FirstOrderActivationDeGroote2016(self.name) + assert isinstance(instance, FirstOrderActivationDeGroote2016) + + def test_with_defaults(self): + instance = FirstOrderActivationDeGroote2016.with_defaults(self.name) + assert isinstance(instance, FirstOrderActivationDeGroote2016) + assert instance.tau_a == Float('0.015') + assert instance.activation_time_constant == Float('0.015') + assert instance.tau_d == Float('0.060') + assert instance.deactivation_time_constant == Float('0.060') + assert instance.b == Float('10.0') + assert instance.smoothing_rate == Float('10.0') + + def test_name(self): + assert hasattr(self.instance, 'name') + assert self.instance.name == self.name + + def test_order(self): + assert hasattr(self.instance, 'order') + assert self.instance.order == 1 + + def test_excitation(self): + assert hasattr(self.instance, 'e') + assert hasattr(self.instance, 'excitation') + e_expected = dynamicsymbols('e_name') + assert self.instance.e == e_expected + assert self.instance.excitation == e_expected + assert self.instance.e is self.instance.excitation + + def test_excitation_is_immutable(self): + with pytest.raises(AttributeError): + self.instance.e = None + with pytest.raises(AttributeError): + self.instance.excitation = None + + def test_activation(self): + assert hasattr(self.instance, 'a') + assert hasattr(self.instance, 'activation') + a_expected = dynamicsymbols('a_name') + assert self.instance.a == a_expected + assert self.instance.activation == a_expected + + def test_activation_is_immutable(self): + with pytest.raises(AttributeError): + self.instance.a = None + with pytest.raises(AttributeError): + self.instance.activation = None + + @pytest.mark.parametrize( + 'tau_a, expected', + [ + (None, Symbol('tau_a_name')), + (Symbol('tau_a'), Symbol('tau_a')), + (Float('0.015'), Float('0.015')), + ] + ) + def test_activation_time_constant(self, tau_a, expected): + instance = FirstOrderActivationDeGroote2016( + 'name', activation_time_constant=tau_a, + ) + assert instance.tau_a == expected + assert instance.activation_time_constant == expected + assert instance.tau_a is instance.activation_time_constant + + def test_activation_time_constant_is_immutable(self): + with pytest.raises(AttributeError): + self.instance.tau_a = None + with pytest.raises(AttributeError): + self.instance.activation_time_constant = None + + @pytest.mark.parametrize( + 'tau_d, expected', + [ + (None, Symbol('tau_d_name')), + (Symbol('tau_d'), Symbol('tau_d')), + (Float('0.060'), Float('0.060')), + ] + ) + def test_deactivation_time_constant(self, tau_d, expected): + instance = FirstOrderActivationDeGroote2016( + 'name', deactivation_time_constant=tau_d, + ) + assert instance.tau_d == expected + assert instance.deactivation_time_constant == expected + assert instance.tau_d is instance.deactivation_time_constant + + def test_deactivation_time_constant_is_immutable(self): + with pytest.raises(AttributeError): + self.instance.tau_d = None + with pytest.raises(AttributeError): + self.instance.deactivation_time_constant = None + + @pytest.mark.parametrize( + 'b, expected', + [ + (None, Symbol('b_name')), + (Symbol('b'), Symbol('b')), + (Integer('10'), Integer('10')), + ] + ) + def test_smoothing_rate(self, b, expected): + instance = FirstOrderActivationDeGroote2016( + 'name', smoothing_rate=b, + ) + assert instance.b == expected + assert instance.smoothing_rate == expected + assert instance.b is instance.smoothing_rate + + def test_smoothing_rate_is_immutable(self): + with pytest.raises(AttributeError): + self.instance.b = None + with pytest.raises(AttributeError): + self.instance.smoothing_rate = None + + def test_state_vars(self): + assert hasattr(self.instance, 'x') + assert hasattr(self.instance, 'state_vars') + assert self.instance.x == self.instance.state_vars + x_expected = Matrix([self.a]) + assert self.instance.x == x_expected + assert self.instance.state_vars == x_expected + assert isinstance(self.instance.x, Matrix) + assert isinstance(self.instance.state_vars, Matrix) + assert self.instance.x.shape == (1, 1) + assert self.instance.state_vars.shape == (1, 1) + + def test_input_vars(self): + assert hasattr(self.instance, 'r') + assert hasattr(self.instance, 'input_vars') + assert self.instance.r == self.instance.input_vars + r_expected = Matrix([self.e]) + assert self.instance.r == r_expected + assert self.instance.input_vars == r_expected + assert isinstance(self.instance.r, Matrix) + assert isinstance(self.instance.input_vars, Matrix) + assert self.instance.r.shape == (1, 1) + assert self.instance.input_vars.shape == (1, 1) + + def test_constants(self): + assert hasattr(self.instance, 'p') + assert hasattr(self.instance, 'constants') + assert self.instance.p == self.instance.constants + p_expected = Matrix([self.tau_a, self.tau_d, self.b]) + assert self.instance.p == p_expected + assert self.instance.constants == p_expected + assert isinstance(self.instance.p, Matrix) + assert isinstance(self.instance.constants, Matrix) + assert self.instance.p.shape == (3, 1) + assert self.instance.constants.shape == (3, 1) + + def test_M(self): + assert hasattr(self.instance, 'M') + M_expected = Matrix([1]) + assert self.instance.M == M_expected + assert isinstance(self.instance.M, Matrix) + assert self.instance.M.shape == (1, 1) + + def test_F(self): + assert hasattr(self.instance, 'F') + da_expr = ( + ((1/(self.tau_a*(Rational(1, 2) + Rational(3, 2)*self.a))) + *(Rational(1, 2) + Rational(1, 2)*tanh(self.b*(self.e - self.a))) + + ((Rational(1, 2) + Rational(3, 2)*self.a)/self.tau_d) + *(Rational(1, 2) - Rational(1, 2)*tanh(self.b*(self.e - self.a)))) + *(self.e - self.a) + ) + F_expected = Matrix([da_expr]) + assert self.instance.F == F_expected + assert isinstance(self.instance.F, Matrix) + assert self.instance.F.shape == (1, 1) + + def test_rhs(self): + assert hasattr(self.instance, 'rhs') + da_expr = ( + ((1/(self.tau_a*(Rational(1, 2) + Rational(3, 2)*self.a))) + *(Rational(1, 2) + Rational(1, 2)*tanh(self.b*(self.e - self.a))) + + ((Rational(1, 2) + Rational(3, 2)*self.a)/self.tau_d) + *(Rational(1, 2) - Rational(1, 2)*tanh(self.b*(self.e - self.a)))) + *(self.e - self.a) + ) + rhs_expected = Matrix([da_expr]) + rhs = self.instance.rhs() + assert rhs == rhs_expected + assert isinstance(rhs, Matrix) + assert rhs.shape == (1, 1) + assert simplify(self.instance.M.solve(self.instance.F) - rhs) == zeros(1) + + def test_repr(self): + expected = ( + 'FirstOrderActivationDeGroote2016(\'name\', ' + 'activation_time_constant=tau_a, ' + 'deactivation_time_constant=tau_d, ' + 'smoothing_rate=b)' + ) + assert repr(self.instance) == expected diff --git a/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/tests/test_curve.py b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/tests/test_curve.py new file mode 100644 index 0000000000000000000000000000000000000000..6a8fcbccdb8b4190376b051093b376e936d9d5d3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/tests/test_curve.py @@ -0,0 +1,1695 @@ +"""Tests for the ``sympy.physics.biomechanics.characteristic.py`` module.""" + +import pytest + +from sympy.core.expr import UnevaluatedExpr +from sympy.core.function import Function +from sympy.core.numbers import Float, Integer +from sympy.core.symbol import Symbol, symbols +from sympy.external.importtools import import_module +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.hyperbolic import cosh, sinh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.physics.biomechanics.curve import ( + CharacteristicCurveCollection, + CharacteristicCurveFunction, + FiberForceLengthActiveDeGroote2016, + FiberForceLengthPassiveDeGroote2016, + FiberForceLengthPassiveInverseDeGroote2016, + FiberForceVelocityDeGroote2016, + FiberForceVelocityInverseDeGroote2016, + TendonForceLengthDeGroote2016, + TendonForceLengthInverseDeGroote2016, +) +from sympy.printing.c import C89CodePrinter, C99CodePrinter, C11CodePrinter +from sympy.printing.cxx import ( + CXX98CodePrinter, + CXX11CodePrinter, + CXX17CodePrinter, +) +from sympy.printing.fortran import FCodePrinter +from sympy.printing.lambdarepr import LambdaPrinter +from sympy.printing.latex import LatexPrinter +from sympy.printing.octave import OctaveCodePrinter +from sympy.printing.numpy import ( + CuPyPrinter, + JaxPrinter, + NumPyPrinter, + SciPyPrinter, +) +from sympy.printing.pycode import MpmathPrinter, PythonCodePrinter +from sympy.utilities.lambdify import lambdify + +jax = import_module('jax') +numpy = import_module('numpy') + +if jax: + jax.config.update('jax_enable_x64', True) + + +class TestCharacteristicCurveFunction: + + @staticmethod + @pytest.mark.parametrize( + 'code_printer, expected', + [ + (C89CodePrinter, '(a + b)*(c + d)*(e + f)'), + (C99CodePrinter, '(a + b)*(c + d)*(e + f)'), + (C11CodePrinter, '(a + b)*(c + d)*(e + f)'), + (CXX98CodePrinter, '(a + b)*(c + d)*(e + f)'), + (CXX11CodePrinter, '(a + b)*(c + d)*(e + f)'), + (CXX17CodePrinter, '(a + b)*(c + d)*(e + f)'), + (FCodePrinter, ' (a + b)*(c + d)*(e + f)'), + (OctaveCodePrinter, '(a + b).*(c + d).*(e + f)'), + (PythonCodePrinter, '(a + b)*(c + d)*(e + f)'), + (NumPyPrinter, '(a + b)*(c + d)*(e + f)'), + (SciPyPrinter, '(a + b)*(c + d)*(e + f)'), + (CuPyPrinter, '(a + b)*(c + d)*(e + f)'), + (JaxPrinter, '(a + b)*(c + d)*(e + f)'), + (MpmathPrinter, '(a + b)*(c + d)*(e + f)'), + (LambdaPrinter, '(a + b)*(c + d)*(e + f)'), + ] + ) + def test_print_code_parenthesize(code_printer, expected): + + class ExampleFunction(CharacteristicCurveFunction): + + @classmethod + def eval(cls, a, b): + pass + + def doit(self, **kwargs): + a, b = self.args + return a + b + + a, b, c, d, e, f = symbols('a, b, c, d, e, f') + f1 = ExampleFunction(a, b) + f2 = ExampleFunction(c, d) + f3 = ExampleFunction(e, f) + assert code_printer().doprint(f1*f2*f3) == expected + + +class TestTendonForceLengthDeGroote2016: + + @pytest.fixture(autouse=True) + def _tendon_force_length_arguments_fixture(self): + self.l_T_tilde = Symbol('l_T_tilde') + self.c0 = Symbol('c_0') + self.c1 = Symbol('c_1') + self.c2 = Symbol('c_2') + self.c3 = Symbol('c_3') + self.constants = (self.c0, self.c1, self.c2, self.c3) + + @staticmethod + def test_class(): + assert issubclass(TendonForceLengthDeGroote2016, Function) + assert issubclass(TendonForceLengthDeGroote2016, CharacteristicCurveFunction) + assert TendonForceLengthDeGroote2016.__name__ == 'TendonForceLengthDeGroote2016' + + def test_instance(self): + fl_T = TendonForceLengthDeGroote2016(self.l_T_tilde, *self.constants) + assert isinstance(fl_T, TendonForceLengthDeGroote2016) + assert str(fl_T) == 'TendonForceLengthDeGroote2016(l_T_tilde, c_0, c_1, c_2, c_3)' + + def test_doit(self): + fl_T = TendonForceLengthDeGroote2016(self.l_T_tilde, *self.constants).doit() + assert fl_T == self.c0*exp(self.c3*(self.l_T_tilde - self.c1)) - self.c2 + + def test_doit_evaluate_false(self): + fl_T = TendonForceLengthDeGroote2016(self.l_T_tilde, *self.constants).doit(evaluate=False) + assert fl_T == self.c0*exp(self.c3*UnevaluatedExpr(self.l_T_tilde - self.c1)) - self.c2 + + def test_with_defaults(self): + constants = ( + Float('0.2'), + Float('0.995'), + Float('0.25'), + Float('33.93669377311689'), + ) + fl_T_manual = TendonForceLengthDeGroote2016(self.l_T_tilde, *constants) + fl_T_constants = TendonForceLengthDeGroote2016.with_defaults(self.l_T_tilde) + assert fl_T_manual == fl_T_constants + + def test_differentiate_wrt_l_T_tilde(self): + fl_T = TendonForceLengthDeGroote2016(self.l_T_tilde, *self.constants) + expected = self.c0*self.c3*exp(self.c3*UnevaluatedExpr(-self.c1 + self.l_T_tilde)) + assert fl_T.diff(self.l_T_tilde) == expected + + def test_differentiate_wrt_c0(self): + fl_T = TendonForceLengthDeGroote2016(self.l_T_tilde, *self.constants) + expected = exp(self.c3*UnevaluatedExpr(-self.c1 + self.l_T_tilde)) + assert fl_T.diff(self.c0) == expected + + def test_differentiate_wrt_c1(self): + fl_T = TendonForceLengthDeGroote2016(self.l_T_tilde, *self.constants) + expected = -self.c0*self.c3*exp(self.c3*UnevaluatedExpr(self.l_T_tilde - self.c1)) + assert fl_T.diff(self.c1) == expected + + def test_differentiate_wrt_c2(self): + fl_T = TendonForceLengthDeGroote2016(self.l_T_tilde, *self.constants) + expected = Integer(-1) + assert fl_T.diff(self.c2) == expected + + def test_differentiate_wrt_c3(self): + fl_T = TendonForceLengthDeGroote2016(self.l_T_tilde, *self.constants) + expected = self.c0*(self.l_T_tilde - self.c1)*exp(self.c3*UnevaluatedExpr(self.l_T_tilde - self.c1)) + assert fl_T.diff(self.c3) == expected + + def test_inverse(self): + fl_T = TendonForceLengthDeGroote2016(self.l_T_tilde, *self.constants) + assert fl_T.inverse() is TendonForceLengthInverseDeGroote2016 + + def test_function_print_latex(self): + fl_T = TendonForceLengthDeGroote2016(self.l_T_tilde, *self.constants) + expected = r'\operatorname{fl}^T \left( l_{T tilde} \right)' + assert LatexPrinter().doprint(fl_T) == expected + + def test_expression_print_latex(self): + fl_T = TendonForceLengthDeGroote2016(self.l_T_tilde, *self.constants) + expected = r'c_{0} e^{c_{3} \left(- c_{1} + l_{T tilde}\right)} - c_{2}' + assert LatexPrinter().doprint(fl_T.doit()) == expected + + @pytest.mark.parametrize( + 'code_printer, expected', + [ + ( + C89CodePrinter, + '(-0.25 + 0.20000000000000001*exp(33.93669377311689*(l_T_tilde - 0.995)))', + ), + ( + C99CodePrinter, + '(-0.25 + 0.20000000000000001*exp(33.93669377311689*(l_T_tilde - 0.995)))', + ), + ( + C11CodePrinter, + '(-0.25 + 0.20000000000000001*exp(33.93669377311689*(l_T_tilde - 0.995)))', + ), + ( + CXX98CodePrinter, + '(-0.25 + 0.20000000000000001*exp(33.93669377311689*(l_T_tilde - 0.995)))', + ), + ( + CXX11CodePrinter, + '(-0.25 + 0.20000000000000001*std::exp(33.93669377311689*(l_T_tilde - 0.995)))', + ), + ( + CXX17CodePrinter, + '(-0.25 + 0.20000000000000001*std::exp(33.93669377311689*(l_T_tilde - 0.995)))', + ), + ( + FCodePrinter, + ' (-0.25d0 + 0.2d0*exp(33.93669377311689d0*(l_T_tilde - 0.995d0)))', + ), + ( + OctaveCodePrinter, + '(-0.25 + 0.2*exp(33.93669377311689*(l_T_tilde - 0.995)))', + ), + ( + PythonCodePrinter, + '(-0.25 + 0.2*math.exp(33.93669377311689*(l_T_tilde - 0.995)))', + ), + ( + NumPyPrinter, + '(-0.25 + 0.2*numpy.exp(33.93669377311689*(l_T_tilde - 0.995)))', + ), + ( + SciPyPrinter, + '(-0.25 + 0.2*numpy.exp(33.93669377311689*(l_T_tilde - 0.995)))', + ), + ( + CuPyPrinter, + '(-0.25 + 0.2*cupy.exp(33.93669377311689*(l_T_tilde - 0.995)))', + ), + ( + JaxPrinter, + '(-0.25 + 0.2*jax.numpy.exp(33.93669377311689*(l_T_tilde - 0.995)))', + ), + ( + MpmathPrinter, + '(mpmath.mpf((1, 1, -2, 1)) + mpmath.mpf((0, 3602879701896397, -54, 52))' + '*mpmath.exp(mpmath.mpf((0, 9552330089424741, -48, 54))*(l_T_tilde + ' + 'mpmath.mpf((1, 8962163258467287, -53, 53)))))', + ), + ( + LambdaPrinter, + '(-0.25 + 0.2*math.exp(33.93669377311689*(l_T_tilde - 0.995)))', + ), + ] + ) + def test_print_code(self, code_printer, expected): + fl_T = TendonForceLengthDeGroote2016.with_defaults(self.l_T_tilde) + assert code_printer().doprint(fl_T) == expected + + def test_derivative_print_code(self): + fl_T = TendonForceLengthDeGroote2016.with_defaults(self.l_T_tilde) + dfl_T_dl_T_tilde = fl_T.diff(self.l_T_tilde) + expected = '6.787338754623378*math.exp(33.93669377311689*(l_T_tilde - 0.995))' + assert PythonCodePrinter().doprint(dfl_T_dl_T_tilde) == expected + + def test_lambdify(self): + fl_T = TendonForceLengthDeGroote2016.with_defaults(self.l_T_tilde) + fl_T_callable = lambdify(self.l_T_tilde, fl_T) + assert fl_T_callable(1.0) == pytest.approx(-0.013014055039221595) + + @pytest.mark.skipif(numpy is None, reason='NumPy not installed') + def test_lambdify_numpy(self): + fl_T = TendonForceLengthDeGroote2016.with_defaults(self.l_T_tilde) + fl_T_callable = lambdify(self.l_T_tilde, fl_T, 'numpy') + l_T_tilde = numpy.array([0.95, 1.0, 1.01, 1.05]) + expected = numpy.array([ + -0.2065693181344816, + -0.0130140550392216, + 0.0827421191989246, + 1.04314889144172, + ]) + numpy.testing.assert_allclose(fl_T_callable(l_T_tilde), expected) + + @pytest.mark.skipif(jax is None, reason='JAX not installed') + def test_lambdify_jax(self): + fl_T = TendonForceLengthDeGroote2016.with_defaults(self.l_T_tilde) + fl_T_callable = jax.jit(lambdify(self.l_T_tilde, fl_T, 'jax')) + l_T_tilde = jax.numpy.array([0.95, 1.0, 1.01, 1.05]) + expected = jax.numpy.array([ + -0.2065693181344816, + -0.0130140550392216, + 0.0827421191989246, + 1.04314889144172, + ]) + numpy.testing.assert_allclose(fl_T_callable(l_T_tilde), expected) + + +class TestTendonForceLengthInverseDeGroote2016: + + @pytest.fixture(autouse=True) + def _tendon_force_length_inverse_arguments_fixture(self): + self.fl_T = Symbol('fl_T') + self.c0 = Symbol('c_0') + self.c1 = Symbol('c_1') + self.c2 = Symbol('c_2') + self.c3 = Symbol('c_3') + self.constants = (self.c0, self.c1, self.c2, self.c3) + + @staticmethod + def test_class(): + assert issubclass(TendonForceLengthInverseDeGroote2016, Function) + assert issubclass(TendonForceLengthInverseDeGroote2016, CharacteristicCurveFunction) + assert TendonForceLengthInverseDeGroote2016.__name__ == 'TendonForceLengthInverseDeGroote2016' + + def test_instance(self): + fl_T_inv = TendonForceLengthInverseDeGroote2016(self.fl_T, *self.constants) + assert isinstance(fl_T_inv, TendonForceLengthInverseDeGroote2016) + assert str(fl_T_inv) == 'TendonForceLengthInverseDeGroote2016(fl_T, c_0, c_1, c_2, c_3)' + + def test_doit(self): + fl_T_inv = TendonForceLengthInverseDeGroote2016(self.fl_T, *self.constants).doit() + assert fl_T_inv == log((self.fl_T + self.c2)/self.c0)/self.c3 + self.c1 + + def test_doit_evaluate_false(self): + fl_T_inv = TendonForceLengthInverseDeGroote2016(self.fl_T, *self.constants).doit(evaluate=False) + assert fl_T_inv == log(UnevaluatedExpr((self.fl_T + self.c2)/self.c0))/self.c3 + self.c1 + + def test_with_defaults(self): + constants = ( + Float('0.2'), + Float('0.995'), + Float('0.25'), + Float('33.93669377311689'), + ) + fl_T_inv_manual = TendonForceLengthInverseDeGroote2016(self.fl_T, *constants) + fl_T_inv_constants = TendonForceLengthInverseDeGroote2016.with_defaults(self.fl_T) + assert fl_T_inv_manual == fl_T_inv_constants + + def test_differentiate_wrt_fl_T(self): + fl_T_inv = TendonForceLengthInverseDeGroote2016(self.fl_T, *self.constants) + expected = 1/(self.c3*(self.fl_T + self.c2)) + assert fl_T_inv.diff(self.fl_T) == expected + + def test_differentiate_wrt_c0(self): + fl_T_inv = TendonForceLengthInverseDeGroote2016(self.fl_T, *self.constants) + expected = -1/(self.c0*self.c3) + assert fl_T_inv.diff(self.c0) == expected + + def test_differentiate_wrt_c1(self): + fl_T_inv = TendonForceLengthInverseDeGroote2016(self.fl_T, *self.constants) + expected = Integer(1) + assert fl_T_inv.diff(self.c1) == expected + + def test_differentiate_wrt_c2(self): + fl_T_inv = TendonForceLengthInverseDeGroote2016(self.fl_T, *self.constants) + expected = 1/(self.c3*(self.fl_T + self.c2)) + assert fl_T_inv.diff(self.c2) == expected + + def test_differentiate_wrt_c3(self): + fl_T_inv = TendonForceLengthInverseDeGroote2016(self.fl_T, *self.constants) + expected = -log(UnevaluatedExpr((self.fl_T + self.c2)/self.c0))/self.c3**2 + assert fl_T_inv.diff(self.c3) == expected + + def test_inverse(self): + fl_T_inv = TendonForceLengthInverseDeGroote2016(self.fl_T, *self.constants) + assert fl_T_inv.inverse() is TendonForceLengthDeGroote2016 + + def test_function_print_latex(self): + fl_T_inv = TendonForceLengthInverseDeGroote2016(self.fl_T, *self.constants) + expected = r'\left( \operatorname{fl}^T \right)^{-1} \left( fl_{T} \right)' + assert LatexPrinter().doprint(fl_T_inv) == expected + + def test_expression_print_latex(self): + fl_T = TendonForceLengthInverseDeGroote2016(self.fl_T, *self.constants) + expected = r'c_{1} + \frac{\log{\left(\frac{c_{2} + fl_{T}}{c_{0}} \right)}}{c_{3}}' + assert LatexPrinter().doprint(fl_T.doit()) == expected + + @pytest.mark.parametrize( + 'code_printer, expected', + [ + ( + C89CodePrinter, + '(0.995 + 0.029466630034306838*log(5.0*fl_T + 1.25))', + ), + ( + C99CodePrinter, + '(0.995 + 0.029466630034306838*log(5.0*fl_T + 1.25))', + ), + ( + C11CodePrinter, + '(0.995 + 0.029466630034306838*log(5.0*fl_T + 1.25))', + ), + ( + CXX98CodePrinter, + '(0.995 + 0.029466630034306838*log(5.0*fl_T + 1.25))', + ), + ( + CXX11CodePrinter, + '(0.995 + 0.029466630034306838*std::log(5.0*fl_T + 1.25))', + ), + ( + CXX17CodePrinter, + '(0.995 + 0.029466630034306838*std::log(5.0*fl_T + 1.25))', + ), + ( + FCodePrinter, + ' (0.995d0 + 0.02946663003430684d0*log(5.0d0*fl_T + 1.25d0))', + ), + ( + OctaveCodePrinter, + '(0.995 + 0.02946663003430684*log(5.0*fl_T + 1.25))', + ), + ( + PythonCodePrinter, + '(0.995 + 0.02946663003430684*math.log(5.0*fl_T + 1.25))', + ), + ( + NumPyPrinter, + '(0.995 + 0.02946663003430684*numpy.log(5.0*fl_T + 1.25))', + ), + ( + SciPyPrinter, + '(0.995 + 0.02946663003430684*numpy.log(5.0*fl_T + 1.25))', + ), + ( + CuPyPrinter, + '(0.995 + 0.02946663003430684*cupy.log(5.0*fl_T + 1.25))', + ), + ( + JaxPrinter, + '(0.995 + 0.02946663003430684*jax.numpy.log(5.0*fl_T + 1.25))', + ), + ( + MpmathPrinter, + '(mpmath.mpf((0, 8962163258467287, -53, 53))' + ' + mpmath.mpf((0, 33972711434846347, -60, 55))' + '*mpmath.log(mpmath.mpf((0, 5, 0, 3))*fl_T + mpmath.mpf((0, 5, -2, 3))))', + ), + ( + LambdaPrinter, + '(0.995 + 0.02946663003430684*math.log(5.0*fl_T + 1.25))', + ), + ] + ) + def test_print_code(self, code_printer, expected): + fl_T_inv = TendonForceLengthInverseDeGroote2016.with_defaults(self.fl_T) + assert code_printer().doprint(fl_T_inv) == expected + + def test_derivative_print_code(self): + fl_T_inv = TendonForceLengthInverseDeGroote2016.with_defaults(self.fl_T) + dfl_T_inv_dfl_T = fl_T_inv.diff(self.fl_T) + expected = '1/(33.93669377311689*fl_T + 8.484173443279222)' + assert PythonCodePrinter().doprint(dfl_T_inv_dfl_T) == expected + + def test_lambdify(self): + fl_T_inv = TendonForceLengthInverseDeGroote2016.with_defaults(self.fl_T) + fl_T_inv_callable = lambdify(self.fl_T, fl_T_inv) + assert fl_T_inv_callable(0.0) == pytest.approx(1.0015752885) + + @pytest.mark.skipif(numpy is None, reason='NumPy not installed') + def test_lambdify_numpy(self): + fl_T_inv = TendonForceLengthInverseDeGroote2016.with_defaults(self.fl_T) + fl_T_inv_callable = lambdify(self.fl_T, fl_T_inv, 'numpy') + fl_T = numpy.array([-0.2, -0.01, 0.0, 1.01, 1.02, 1.05]) + expected = numpy.array([ + 0.9541505769, + 1.0003724019, + 1.0015752885, + 1.0492347951, + 1.0494677341, + 1.0501557022, + ]) + numpy.testing.assert_allclose(fl_T_inv_callable(fl_T), expected) + + @pytest.mark.skipif(jax is None, reason='JAX not installed') + def test_lambdify_jax(self): + fl_T_inv = TendonForceLengthInverseDeGroote2016.with_defaults(self.fl_T) + fl_T_inv_callable = jax.jit(lambdify(self.fl_T, fl_T_inv, 'jax')) + fl_T = jax.numpy.array([-0.2, -0.01, 0.0, 1.01, 1.02, 1.05]) + expected = jax.numpy.array([ + 0.9541505769, + 1.0003724019, + 1.0015752885, + 1.0492347951, + 1.0494677341, + 1.0501557022, + ]) + numpy.testing.assert_allclose(fl_T_inv_callable(fl_T), expected) + + +class TestFiberForceLengthPassiveDeGroote2016: + + @pytest.fixture(autouse=True) + def _fiber_force_length_passive_arguments_fixture(self): + self.l_M_tilde = Symbol('l_M_tilde') + self.c0 = Symbol('c_0') + self.c1 = Symbol('c_1') + self.constants = (self.c0, self.c1) + + @staticmethod + def test_class(): + assert issubclass(FiberForceLengthPassiveDeGroote2016, Function) + assert issubclass(FiberForceLengthPassiveDeGroote2016, CharacteristicCurveFunction) + assert FiberForceLengthPassiveDeGroote2016.__name__ == 'FiberForceLengthPassiveDeGroote2016' + + def test_instance(self): + fl_M_pas = FiberForceLengthPassiveDeGroote2016(self.l_M_tilde, *self.constants) + assert isinstance(fl_M_pas, FiberForceLengthPassiveDeGroote2016) + assert str(fl_M_pas) == 'FiberForceLengthPassiveDeGroote2016(l_M_tilde, c_0, c_1)' + + def test_doit(self): + fl_M_pas = FiberForceLengthPassiveDeGroote2016(self.l_M_tilde, *self.constants).doit() + assert fl_M_pas == (exp((self.c1*(self.l_M_tilde - 1))/self.c0) - 1)/(exp(self.c1) - 1) + + def test_doit_evaluate_false(self): + fl_M_pas = FiberForceLengthPassiveDeGroote2016(self.l_M_tilde, *self.constants).doit(evaluate=False) + assert fl_M_pas == (exp((self.c1*UnevaluatedExpr(self.l_M_tilde - 1))/self.c0) - 1)/(exp(self.c1) - 1) + + def test_with_defaults(self): + constants = ( + Float('0.6'), + Float('4.0'), + ) + fl_M_pas_manual = FiberForceLengthPassiveDeGroote2016(self.l_M_tilde, *constants) + fl_M_pas_constants = FiberForceLengthPassiveDeGroote2016.with_defaults(self.l_M_tilde) + assert fl_M_pas_manual == fl_M_pas_constants + + def test_differentiate_wrt_l_M_tilde(self): + fl_M_pas = FiberForceLengthPassiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = self.c1*exp(self.c1*UnevaluatedExpr(self.l_M_tilde - 1)/self.c0)/(self.c0*(exp(self.c1) - 1)) + assert fl_M_pas.diff(self.l_M_tilde) == expected + + def test_differentiate_wrt_c0(self): + fl_M_pas = FiberForceLengthPassiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = ( + -self.c1*exp(self.c1*UnevaluatedExpr(self.l_M_tilde - 1)/self.c0) + *UnevaluatedExpr(self.l_M_tilde - 1)/(self.c0**2*(exp(self.c1) - 1)) + ) + assert fl_M_pas.diff(self.c0) == expected + + def test_differentiate_wrt_c1(self): + fl_M_pas = FiberForceLengthPassiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = ( + -exp(self.c1)*(-1 + exp(self.c1*UnevaluatedExpr(self.l_M_tilde - 1)/self.c0))/(exp(self.c1) - 1)**2 + + exp(self.c1*UnevaluatedExpr(self.l_M_tilde - 1)/self.c0)*(self.l_M_tilde - 1)/(self.c0*(exp(self.c1) - 1)) + ) + assert fl_M_pas.diff(self.c1) == expected + + def test_inverse(self): + fl_M_pas = FiberForceLengthPassiveDeGroote2016(self.l_M_tilde, *self.constants) + assert fl_M_pas.inverse() is FiberForceLengthPassiveInverseDeGroote2016 + + def test_function_print_latex(self): + fl_M_pas = FiberForceLengthPassiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = r'\operatorname{fl}^M_{pas} \left( l_{M tilde} \right)' + assert LatexPrinter().doprint(fl_M_pas) == expected + + def test_expression_print_latex(self): + fl_M_pas = FiberForceLengthPassiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = r'\frac{e^{\frac{c_{1} \left(l_{M tilde} - 1\right)}{c_{0}}} - 1}{e^{c_{1}} - 1}' + assert LatexPrinter().doprint(fl_M_pas.doit()) == expected + + @pytest.mark.parametrize( + 'code_printer, expected', + [ + ( + C89CodePrinter, + '(0.01865736036377405*(-1 + exp(6.666666666666667*(l_M_tilde - 1))))', + ), + ( + C99CodePrinter, + '(0.01865736036377405*(-1 + exp(6.666666666666667*(l_M_tilde - 1))))', + ), + ( + C11CodePrinter, + '(0.01865736036377405*(-1 + exp(6.666666666666667*(l_M_tilde - 1))))', + ), + ( + CXX98CodePrinter, + '(0.01865736036377405*(-1 + exp(6.666666666666667*(l_M_tilde - 1))))', + ), + ( + CXX11CodePrinter, + '(0.01865736036377405*(-1 + std::exp(6.666666666666667*(l_M_tilde - 1))))', + ), + ( + CXX17CodePrinter, + '(0.01865736036377405*(-1 + std::exp(6.666666666666667*(l_M_tilde - 1))))', + ), + ( + FCodePrinter, + ' (0.0186573603637741d0*(-1 + exp(6.666666666666667d0*(l_M_tilde - 1\n' + ' @ ))))', + ), + ( + OctaveCodePrinter, + '(0.0186573603637741*(-1 + exp(6.66666666666667*(l_M_tilde - 1))))', + ), + ( + PythonCodePrinter, + '(0.0186573603637741*(-1 + math.exp(6.66666666666667*(l_M_tilde - 1))))', + ), + ( + NumPyPrinter, + '(0.0186573603637741*(-1 + numpy.exp(6.66666666666667*(l_M_tilde - 1))))', + ), + ( + SciPyPrinter, + '(0.0186573603637741*(-1 + numpy.exp(6.66666666666667*(l_M_tilde - 1))))', + ), + ( + CuPyPrinter, + '(0.0186573603637741*(-1 + cupy.exp(6.66666666666667*(l_M_tilde - 1))))', + ), + ( + JaxPrinter, + '(0.0186573603637741*(-1 + jax.numpy.exp(6.66666666666667*(l_M_tilde - 1))))', + ), + ( + MpmathPrinter, + '(mpmath.mpf((0, 672202249456079, -55, 50))*(-1 + mpmath.exp(' + 'mpmath.mpf((0, 7505999378950827, -50, 53))*(l_M_tilde - 1))))', + ), + ( + LambdaPrinter, + '(0.0186573603637741*(-1 + math.exp(6.66666666666667*(l_M_tilde - 1))))', + ), + ] + ) + def test_print_code(self, code_printer, expected): + fl_M_pas = FiberForceLengthPassiveDeGroote2016.with_defaults(self.l_M_tilde) + assert code_printer().doprint(fl_M_pas) == expected + + def test_derivative_print_code(self): + fl_M_pas = FiberForceLengthPassiveDeGroote2016.with_defaults(self.l_M_tilde) + fl_M_pas_dl_M_tilde = fl_M_pas.diff(self.l_M_tilde) + expected = '0.12438240242516*math.exp(6.66666666666667*(l_M_tilde - 1))' + assert PythonCodePrinter().doprint(fl_M_pas_dl_M_tilde) == expected + + def test_lambdify(self): + fl_M_pas = FiberForceLengthPassiveDeGroote2016.with_defaults(self.l_M_tilde) + fl_M_pas_callable = lambdify(self.l_M_tilde, fl_M_pas) + assert fl_M_pas_callable(1.0) == pytest.approx(0.0) + + @pytest.mark.skipif(numpy is None, reason='NumPy not installed') + def test_lambdify_numpy(self): + fl_M_pas = FiberForceLengthPassiveDeGroote2016.with_defaults(self.l_M_tilde) + fl_M_pas_callable = lambdify(self.l_M_tilde, fl_M_pas, 'numpy') + l_M_tilde = numpy.array([0.5, 0.8, 0.9, 1.0, 1.1, 1.2, 1.5]) + expected = numpy.array([ + -0.0179917778, + -0.0137393336, + -0.0090783522, + 0.0, + 0.0176822155, + 0.0521224686, + 0.5043387669, + ]) + numpy.testing.assert_allclose(fl_M_pas_callable(l_M_tilde), expected) + + @pytest.mark.skipif(jax is None, reason='JAX not installed') + def test_lambdify_jax(self): + fl_M_pas = FiberForceLengthPassiveDeGroote2016.with_defaults(self.l_M_tilde) + fl_M_pas_callable = jax.jit(lambdify(self.l_M_tilde, fl_M_pas, 'jax')) + l_M_tilde = jax.numpy.array([0.5, 0.8, 0.9, 1.0, 1.1, 1.2, 1.5]) + expected = jax.numpy.array([ + -0.0179917778, + -0.0137393336, + -0.0090783522, + 0.0, + 0.0176822155, + 0.0521224686, + 0.5043387669, + ]) + numpy.testing.assert_allclose(fl_M_pas_callable(l_M_tilde), expected) + + +class TestFiberForceLengthPassiveInverseDeGroote2016: + + @pytest.fixture(autouse=True) + def _fiber_force_length_passive_arguments_fixture(self): + self.fl_M_pas = Symbol('fl_M_pas') + self.c0 = Symbol('c_0') + self.c1 = Symbol('c_1') + self.constants = (self.c0, self.c1) + + @staticmethod + def test_class(): + assert issubclass(FiberForceLengthPassiveInverseDeGroote2016, Function) + assert issubclass(FiberForceLengthPassiveInverseDeGroote2016, CharacteristicCurveFunction) + assert FiberForceLengthPassiveInverseDeGroote2016.__name__ == 'FiberForceLengthPassiveInverseDeGroote2016' + + def test_instance(self): + fl_M_pas_inv = FiberForceLengthPassiveInverseDeGroote2016(self.fl_M_pas, *self.constants) + assert isinstance(fl_M_pas_inv, FiberForceLengthPassiveInverseDeGroote2016) + assert str(fl_M_pas_inv) == 'FiberForceLengthPassiveInverseDeGroote2016(fl_M_pas, c_0, c_1)' + + def test_doit(self): + fl_M_pas_inv = FiberForceLengthPassiveInverseDeGroote2016(self.fl_M_pas, *self.constants).doit() + assert fl_M_pas_inv == self.c0*log(self.fl_M_pas*(exp(self.c1) - 1) + 1)/self.c1 + 1 + + def test_doit_evaluate_false(self): + fl_M_pas_inv = FiberForceLengthPassiveInverseDeGroote2016(self.fl_M_pas, *self.constants).doit(evaluate=False) + assert fl_M_pas_inv == self.c0*log(UnevaluatedExpr(self.fl_M_pas*(exp(self.c1) - 1)) + 1)/self.c1 + 1 + + def test_with_defaults(self): + constants = ( + Float('0.6'), + Float('4.0'), + ) + fl_M_pas_inv_manual = FiberForceLengthPassiveInverseDeGroote2016(self.fl_M_pas, *constants) + fl_M_pas_inv_constants = FiberForceLengthPassiveInverseDeGroote2016.with_defaults(self.fl_M_pas) + assert fl_M_pas_inv_manual == fl_M_pas_inv_constants + + def test_differentiate_wrt_fl_T(self): + fl_M_pas_inv = FiberForceLengthPassiveInverseDeGroote2016(self.fl_M_pas, *self.constants) + expected = self.c0*(exp(self.c1) - 1)/(self.c1*(self.fl_M_pas*(exp(self.c1) - 1) + 1)) + assert fl_M_pas_inv.diff(self.fl_M_pas) == expected + + def test_differentiate_wrt_c0(self): + fl_M_pas_inv = FiberForceLengthPassiveInverseDeGroote2016(self.fl_M_pas, *self.constants) + expected = log(self.fl_M_pas*(exp(self.c1) - 1) + 1)/self.c1 + assert fl_M_pas_inv.diff(self.c0) == expected + + def test_differentiate_wrt_c1(self): + fl_M_pas_inv = FiberForceLengthPassiveInverseDeGroote2016(self.fl_M_pas, *self.constants) + expected = ( + self.c0*self.fl_M_pas*exp(self.c1)/(self.c1*(self.fl_M_pas*(exp(self.c1) - 1) + 1)) + - self.c0*log(self.fl_M_pas*(exp(self.c1) - 1) + 1)/self.c1**2 + ) + assert fl_M_pas_inv.diff(self.c1) == expected + + def test_inverse(self): + fl_M_pas_inv = FiberForceLengthPassiveInverseDeGroote2016(self.fl_M_pas, *self.constants) + assert fl_M_pas_inv.inverse() is FiberForceLengthPassiveDeGroote2016 + + def test_function_print_latex(self): + fl_M_pas_inv = FiberForceLengthPassiveInverseDeGroote2016(self.fl_M_pas, *self.constants) + expected = r'\left( \operatorname{fl}^M_{pas} \right)^{-1} \left( fl_{M pas} \right)' + assert LatexPrinter().doprint(fl_M_pas_inv) == expected + + def test_expression_print_latex(self): + fl_T = FiberForceLengthPassiveInverseDeGroote2016(self.fl_M_pas, *self.constants) + expected = r'\frac{c_{0} \log{\left(fl_{M pas} \left(e^{c_{1}} - 1\right) + 1 \right)}}{c_{1}} + 1' + assert LatexPrinter().doprint(fl_T.doit()) == expected + + @pytest.mark.parametrize( + 'code_printer, expected', + [ + ( + C89CodePrinter, + '(1 + 0.14999999999999999*log(1 + 53.598150033144236*fl_M_pas))', + ), + ( + C99CodePrinter, + '(1 + 0.14999999999999999*log(1 + 53.598150033144236*fl_M_pas))', + ), + ( + C11CodePrinter, + '(1 + 0.14999999999999999*log(1 + 53.598150033144236*fl_M_pas))', + ), + ( + CXX98CodePrinter, + '(1 + 0.14999999999999999*log(1 + 53.598150033144236*fl_M_pas))', + ), + ( + CXX11CodePrinter, + '(1 + 0.14999999999999999*std::log(1 + 53.598150033144236*fl_M_pas))', + ), + ( + CXX17CodePrinter, + '(1 + 0.14999999999999999*std::log(1 + 53.598150033144236*fl_M_pas))', + ), + ( + FCodePrinter, + ' (1 + 0.15d0*log(1.0d0 + 53.5981500331442d0*fl_M_pas))', + ), + ( + OctaveCodePrinter, + '(1 + 0.15*log(1 + 53.5981500331442*fl_M_pas))', + ), + ( + PythonCodePrinter, + '(1 + 0.15*math.log(1 + 53.5981500331442*fl_M_pas))', + ), + ( + NumPyPrinter, + '(1 + 0.15*numpy.log(1 + 53.5981500331442*fl_M_pas))', + ), + ( + SciPyPrinter, + '(1 + 0.15*numpy.log(1 + 53.5981500331442*fl_M_pas))', + ), + ( + CuPyPrinter, + '(1 + 0.15*cupy.log(1 + 53.5981500331442*fl_M_pas))', + ), + ( + JaxPrinter, + '(1 + 0.15*jax.numpy.log(1 + 53.5981500331442*fl_M_pas))', + ), + ( + MpmathPrinter, + '(1 + mpmath.mpf((0, 5404319552844595, -55, 53))*mpmath.log(1 ' + '+ mpmath.mpf((0, 942908627019595, -44, 50))*fl_M_pas))', + ), + ( + LambdaPrinter, + '(1 + 0.15*math.log(1 + 53.5981500331442*fl_M_pas))', + ), + ] + ) + def test_print_code(self, code_printer, expected): + fl_M_pas_inv = FiberForceLengthPassiveInverseDeGroote2016.with_defaults(self.fl_M_pas) + assert code_printer().doprint(fl_M_pas_inv) == expected + + def test_derivative_print_code(self): + fl_M_pas_inv = FiberForceLengthPassiveInverseDeGroote2016.with_defaults(self.fl_M_pas) + dfl_M_pas_inv_dfl_T = fl_M_pas_inv.diff(self.fl_M_pas) + expected = '32.1588900198865/(214.392600132577*fl_M_pas + 4.0)' + assert PythonCodePrinter().doprint(dfl_M_pas_inv_dfl_T) == expected + + def test_lambdify(self): + fl_M_pas_inv = FiberForceLengthPassiveInverseDeGroote2016.with_defaults(self.fl_M_pas) + fl_M_pas_inv_callable = lambdify(self.fl_M_pas, fl_M_pas_inv) + assert fl_M_pas_inv_callable(0.0) == pytest.approx(1.0) + + @pytest.mark.skipif(numpy is None, reason='NumPy not installed') + def test_lambdify_numpy(self): + fl_M_pas_inv = FiberForceLengthPassiveInverseDeGroote2016.with_defaults(self.fl_M_pas) + fl_M_pas_inv_callable = lambdify(self.fl_M_pas, fl_M_pas_inv, 'numpy') + fl_M_pas = numpy.array([-0.01, 0.0, 0.01, 0.02, 0.05, 0.1]) + expected = numpy.array([ + 0.8848253714, + 1.0, + 1.0643754386, + 1.1092744701, + 1.1954331425, + 1.2774998934, + ]) + numpy.testing.assert_allclose(fl_M_pas_inv_callable(fl_M_pas), expected) + + @pytest.mark.skipif(jax is None, reason='JAX not installed') + def test_lambdify_jax(self): + fl_M_pas_inv = FiberForceLengthPassiveInverseDeGroote2016.with_defaults(self.fl_M_pas) + fl_M_pas_inv_callable = jax.jit(lambdify(self.fl_M_pas, fl_M_pas_inv, 'jax')) + fl_M_pas = jax.numpy.array([-0.01, 0.0, 0.01, 0.02, 0.05, 0.1]) + expected = jax.numpy.array([ + 0.8848253714, + 1.0, + 1.0643754386, + 1.1092744701, + 1.1954331425, + 1.2774998934, + ]) + numpy.testing.assert_allclose(fl_M_pas_inv_callable(fl_M_pas), expected) + + +class TestFiberForceLengthActiveDeGroote2016: + + @pytest.fixture(autouse=True) + def _fiber_force_length_active_arguments_fixture(self): + self.l_M_tilde = Symbol('l_M_tilde') + self.c0 = Symbol('c_0') + self.c1 = Symbol('c_1') + self.c2 = Symbol('c_2') + self.c3 = Symbol('c_3') + self.c4 = Symbol('c_4') + self.c5 = Symbol('c_5') + self.c6 = Symbol('c_6') + self.c7 = Symbol('c_7') + self.c8 = Symbol('c_8') + self.c9 = Symbol('c_9') + self.c10 = Symbol('c_10') + self.c11 = Symbol('c_11') + self.constants = ( + self.c0, self.c1, self.c2, self.c3, self.c4, self.c5, + self.c6, self.c7, self.c8, self.c9, self.c10, self.c11, + ) + + @staticmethod + def test_class(): + assert issubclass(FiberForceLengthActiveDeGroote2016, Function) + assert issubclass(FiberForceLengthActiveDeGroote2016, CharacteristicCurveFunction) + assert FiberForceLengthActiveDeGroote2016.__name__ == 'FiberForceLengthActiveDeGroote2016' + + def test_instance(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + assert isinstance(fl_M_act, FiberForceLengthActiveDeGroote2016) + assert str(fl_M_act) == ( + 'FiberForceLengthActiveDeGroote2016(l_M_tilde, c_0, c_1, c_2, c_3, ' + 'c_4, c_5, c_6, c_7, c_8, c_9, c_10, c_11)' + ) + + def test_doit(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants).doit() + assert fl_M_act == ( + self.c0*exp(-(((self.l_M_tilde - self.c1)/(self.c2 + self.c3*self.l_M_tilde))**2)/2) + + self.c4*exp(-(((self.l_M_tilde - self.c5)/(self.c6 + self.c7*self.l_M_tilde))**2)/2) + + self.c8*exp(-(((self.l_M_tilde - self.c9)/(self.c10 + self.c11*self.l_M_tilde))**2)/2) + ) + + def test_doit_evaluate_false(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants).doit(evaluate=False) + assert fl_M_act == ( + self.c0*exp(-((UnevaluatedExpr(self.l_M_tilde - self.c1)/(self.c2 + self.c3*self.l_M_tilde))**2)/2) + + self.c4*exp(-((UnevaluatedExpr(self.l_M_tilde - self.c5)/(self.c6 + self.c7*self.l_M_tilde))**2)/2) + + self.c8*exp(-((UnevaluatedExpr(self.l_M_tilde - self.c9)/(self.c10 + self.c11*self.l_M_tilde))**2)/2) + ) + + def test_with_defaults(self): + constants = ( + Float('0.814'), + Float('1.06'), + Float('0.162'), + Float('0.0633'), + Float('0.433'), + Float('0.717'), + Float('-0.0299'), + Float('0.2'), + Float('0.1'), + Float('1.0'), + Float('0.354'), + Float('0.0'), + ) + fl_M_act_manual = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *constants) + fl_M_act_constants = FiberForceLengthActiveDeGroote2016.with_defaults(self.l_M_tilde) + assert fl_M_act_manual == fl_M_act_constants + + def test_differentiate_wrt_l_M_tilde(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = ( + self.c0*( + self.c3*(self.l_M_tilde - self.c1)**2/(self.c2 + self.c3*self.l_M_tilde)**3 + + (self.c1 - self.l_M_tilde)/((self.c2 + self.c3*self.l_M_tilde)**2) + )*exp(-(self.l_M_tilde - self.c1)**2/(2*(self.c2 + self.c3*self.l_M_tilde)**2)) + + self.c4*( + self.c7*(self.l_M_tilde - self.c5)**2/(self.c6 + self.c7*self.l_M_tilde)**3 + + (self.c5 - self.l_M_tilde)/((self.c6 + self.c7*self.l_M_tilde)**2) + )*exp(-(self.l_M_tilde - self.c5)**2/(2*(self.c6 + self.c7*self.l_M_tilde)**2)) + + self.c8*( + self.c11*(self.l_M_tilde - self.c9)**2/(self.c10 + self.c11*self.l_M_tilde)**3 + + (self.c9 - self.l_M_tilde)/((self.c10 + self.c11*self.l_M_tilde)**2) + )*exp(-(self.l_M_tilde - self.c9)**2/(2*(self.c10 + self.c11*self.l_M_tilde)**2)) + ) + assert fl_M_act.diff(self.l_M_tilde) == expected + + def test_differentiate_wrt_c0(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = exp(-(self.l_M_tilde - self.c1)**2/(2*(self.c2 + self.c3*self.l_M_tilde)**2)) + assert fl_M_act.doit().diff(self.c0) == expected + + def test_differentiate_wrt_c1(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = ( + self.c0*(self.l_M_tilde - self.c1)/(self.c2 + self.c3*self.l_M_tilde)**2 + *exp(-(self.l_M_tilde - self.c1)**2/(2*(self.c2 + self.c3*self.l_M_tilde)**2)) + ) + assert fl_M_act.diff(self.c1) == expected + + def test_differentiate_wrt_c2(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = ( + self.c0*(self.l_M_tilde - self.c1)**2/(self.c2 + self.c3*self.l_M_tilde)**3 + *exp(-(self.l_M_tilde - self.c1)**2/(2*(self.c2 + self.c3*self.l_M_tilde)**2)) + ) + assert fl_M_act.diff(self.c2) == expected + + def test_differentiate_wrt_c3(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = ( + self.c0*self.l_M_tilde*(self.l_M_tilde - self.c1)**2/(self.c2 + self.c3*self.l_M_tilde)**3 + *exp(-(self.l_M_tilde - self.c1)**2/(2*(self.c2 + self.c3*self.l_M_tilde)**2)) + ) + assert fl_M_act.diff(self.c3) == expected + + def test_differentiate_wrt_c4(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = exp(-(self.l_M_tilde - self.c5)**2/(2*(self.c6 + self.c7*self.l_M_tilde)**2)) + assert fl_M_act.diff(self.c4) == expected + + def test_differentiate_wrt_c5(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = ( + self.c4*(self.l_M_tilde - self.c5)/(self.c6 + self.c7*self.l_M_tilde)**2 + *exp(-(self.l_M_tilde - self.c5)**2/(2*(self.c6 + self.c7*self.l_M_tilde)**2)) + ) + assert fl_M_act.diff(self.c5) == expected + + def test_differentiate_wrt_c6(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = ( + self.c4*(self.l_M_tilde - self.c5)**2/(self.c6 + self.c7*self.l_M_tilde)**3 + *exp(-(self.l_M_tilde - self.c5)**2/(2*(self.c6 + self.c7*self.l_M_tilde)**2)) + ) + assert fl_M_act.diff(self.c6) == expected + + def test_differentiate_wrt_c7(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = ( + self.c4*self.l_M_tilde*(self.l_M_tilde - self.c5)**2/(self.c6 + self.c7*self.l_M_tilde)**3 + *exp(-(self.l_M_tilde - self.c5)**2/(2*(self.c6 + self.c7*self.l_M_tilde)**2)) + ) + assert fl_M_act.diff(self.c7) == expected + + def test_differentiate_wrt_c8(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = exp(-(self.l_M_tilde - self.c9)**2/(2*(self.c10 + self.c11*self.l_M_tilde)**2)) + assert fl_M_act.diff(self.c8) == expected + + def test_differentiate_wrt_c9(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = ( + self.c8*(self.l_M_tilde - self.c9)/(self.c10 + self.c11*self.l_M_tilde)**2 + *exp(-(self.l_M_tilde - self.c9)**2/(2*(self.c10 + self.c11*self.l_M_tilde)**2)) + ) + assert fl_M_act.diff(self.c9) == expected + + def test_differentiate_wrt_c10(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = ( + self.c8*(self.l_M_tilde - self.c9)**2/(self.c10 + self.c11*self.l_M_tilde)**3 + *exp(-(self.l_M_tilde - self.c9)**2/(2*(self.c10 + self.c11*self.l_M_tilde)**2)) + ) + assert fl_M_act.diff(self.c10) == expected + + def test_differentiate_wrt_c11(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = ( + self.c8*self.l_M_tilde*(self.l_M_tilde - self.c9)**2/(self.c10 + self.c11*self.l_M_tilde)**3 + *exp(-(self.l_M_tilde - self.c9)**2/(2*(self.c10 + self.c11*self.l_M_tilde)**2)) + ) + assert fl_M_act.diff(self.c11) == expected + + def test_function_print_latex(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = r'\operatorname{fl}^M_{act} \left( l_{M tilde} \right)' + assert LatexPrinter().doprint(fl_M_act) == expected + + def test_expression_print_latex(self): + fl_M_act = FiberForceLengthActiveDeGroote2016(self.l_M_tilde, *self.constants) + expected = ( + r'c_{0} e^{- \frac{\left(- c_{1} + l_{M tilde}\right)^{2}}{2 \left(c_{2} + c_{3} l_{M tilde}\right)^{2}}} ' + r'+ c_{4} e^{- \frac{\left(- c_{5} + l_{M tilde}\right)^{2}}{2 \left(c_{6} + c_{7} l_{M tilde}\right)^{2}}} ' + r'+ c_{8} e^{- \frac{\left(- c_{9} + l_{M tilde}\right)^{2}}{2 \left(c_{10} + c_{11} l_{M tilde}\right)^{2}}}' + ) + assert LatexPrinter().doprint(fl_M_act.doit()) == expected + + @pytest.mark.parametrize( + 'code_printer, expected', + [ + ( + C89CodePrinter, + ( + '(0.81399999999999995*exp(-1.0/2.0*pow(l_M_tilde - 1.0600000000000001, 2)/pow(0.063299999999999995*l_M_tilde + 0.16200000000000001, 2)) + 0.433*exp(-1.0/2.0*pow(l_M_tilde - 0.71699999999999997, 2)/pow(0.20000000000000001*l_M_tilde - 0.029899999999999999, 2)) + 0.10000000000000001*exp(-3.9899134986753491*pow(l_M_tilde - 1.0, 2)))' + ), + ), + ( + C99CodePrinter, + ( + '(0.81399999999999995*exp(-1.0/2.0*pow(l_M_tilde - 1.0600000000000001, 2)/pow(0.063299999999999995*l_M_tilde + 0.16200000000000001, 2)) + 0.433*exp(-1.0/2.0*pow(l_M_tilde - 0.71699999999999997, 2)/pow(0.20000000000000001*l_M_tilde - 0.029899999999999999, 2)) + 0.10000000000000001*exp(-3.9899134986753491*pow(l_M_tilde - 1.0, 2)))' + ), + ), + ( + C11CodePrinter, + ( + '(0.81399999999999995*exp(-1.0/2.0*pow(l_M_tilde - 1.0600000000000001, 2)/pow(0.063299999999999995*l_M_tilde + 0.16200000000000001, 2)) + 0.433*exp(-1.0/2.0*pow(l_M_tilde - 0.71699999999999997, 2)/pow(0.20000000000000001*l_M_tilde - 0.029899999999999999, 2)) + 0.10000000000000001*exp(-3.9899134986753491*pow(l_M_tilde - 1.0, 2)))' + ), + ), + ( + CXX98CodePrinter, + ( + '(0.81399999999999995*exp(-1.0/2.0*std::pow(l_M_tilde - 1.0600000000000001, 2)/std::pow(0.063299999999999995*l_M_tilde + 0.16200000000000001, 2)) + 0.433*exp(-1.0/2.0*std::pow(l_M_tilde - 0.71699999999999997, 2)/std::pow(0.20000000000000001*l_M_tilde - 0.029899999999999999, 2)) + 0.10000000000000001*exp(-3.9899134986753491*std::pow(l_M_tilde - 1.0, 2)))' + ), + ), + ( + CXX11CodePrinter, + ( + '(0.81399999999999995*std::exp(-1.0/2.0*std::pow(l_M_tilde - 1.0600000000000001, 2)/std::pow(0.063299999999999995*l_M_tilde + 0.16200000000000001, 2)) + 0.433*std::exp(-1.0/2.0*std::pow(l_M_tilde - 0.71699999999999997, 2)/std::pow(0.20000000000000001*l_M_tilde - 0.029899999999999999, 2)) + 0.10000000000000001*std::exp(-3.9899134986753491*std::pow(l_M_tilde - 1.0, 2)))' + ), + ), + ( + CXX17CodePrinter, + ( + '(0.81399999999999995*std::exp(-1.0/2.0*std::pow(l_M_tilde - 1.0600000000000001, 2)/std::pow(0.063299999999999995*l_M_tilde + 0.16200000000000001, 2)) + 0.433*std::exp(-1.0/2.0*std::pow(l_M_tilde - 0.71699999999999997, 2)/std::pow(0.20000000000000001*l_M_tilde - 0.029899999999999999, 2)) + 0.10000000000000001*std::exp(-3.9899134986753491*std::pow(l_M_tilde - 1.0, 2)))' + ), + ), + ( + FCodePrinter, + ( + ' (0.814d0*exp(-0.5d0*(l_M_tilde - 1.06d0)**2/(\n' + ' @ 0.063299999999999995d0*l_M_tilde + 0.16200000000000001d0)**2) +\n' + ' @ 0.433d0*exp(-0.5d0*(l_M_tilde - 0.717d0)**2/(\n' + ' @ 0.20000000000000001d0*l_M_tilde - 0.029899999999999999d0)**2) +\n' + ' @ 0.1d0*exp(-3.9899134986753491d0*(l_M_tilde - 1.0d0)**2))' + ), + ), + ( + OctaveCodePrinter, + ( + '(0.814*exp(-(l_M_tilde - 1.06).^2./(2*(0.0633*l_M_tilde + 0.162).^2)) + 0.433*exp(-(l_M_tilde - 0.717).^2./(2*(0.2*l_M_tilde - 0.0299).^2)) + 0.1*exp(-3.98991349867535*(l_M_tilde - 1.0).^2))' + ), + ), + ( + PythonCodePrinter, + ( + '(0.814*math.exp(-1/2*(l_M_tilde - 1.06)**2/(0.0633*l_M_tilde + 0.162)**2) + 0.433*math.exp(-1/2*(l_M_tilde - 0.717)**2/(0.2*l_M_tilde - 0.0299)**2) + 0.1*math.exp(-3.98991349867535*(l_M_tilde - 1.0)**2))' + ), + ), + ( + NumPyPrinter, + ( + '(0.814*numpy.exp(-1/2*(l_M_tilde - 1.06)**2/(0.0633*l_M_tilde + 0.162)**2) + 0.433*numpy.exp(-1/2*(l_M_tilde - 0.717)**2/(0.2*l_M_tilde - 0.0299)**2) + 0.1*numpy.exp(-3.98991349867535*(l_M_tilde - 1.0)**2))' + ), + ), + ( + SciPyPrinter, + ( + '(0.814*numpy.exp(-1/2*(l_M_tilde - 1.06)**2/(0.0633*l_M_tilde + 0.162)**2) + 0.433*numpy.exp(-1/2*(l_M_tilde - 0.717)**2/(0.2*l_M_tilde - 0.0299)**2) + 0.1*numpy.exp(-3.98991349867535*(l_M_tilde - 1.0)**2))' + ), + ), + ( + CuPyPrinter, + ( + '(0.814*cupy.exp(-1/2*(l_M_tilde - 1.06)**2/(0.0633*l_M_tilde + 0.162)**2) + 0.433*cupy.exp(-1/2*(l_M_tilde - 0.717)**2/(0.2*l_M_tilde - 0.0299)**2) + 0.1*cupy.exp(-3.98991349867535*(l_M_tilde - 1.0)**2))' + ), + ), + ( + JaxPrinter, + ( + '(0.814*jax.numpy.exp(-1/2*(l_M_tilde - 1.06)**2/(0.0633*l_M_tilde + 0.162)**2) + 0.433*jax.numpy.exp(-1/2*(l_M_tilde - 0.717)**2/(0.2*l_M_tilde - 0.0299)**2) + 0.1*jax.numpy.exp(-3.98991349867535*(l_M_tilde - 1.0)**2))' + ), + ), + ( + MpmathPrinter, + ( + '(mpmath.mpf((0, 7331860193359167, -53, 53))*mpmath.exp(-mpmath.mpf(1)/mpmath.mpf(2)*(l_M_tilde + mpmath.mpf((1, 2386907802506363, -51, 52)))**2/(mpmath.mpf((0, 2280622851300419, -55, 52))*l_M_tilde + mpmath.mpf((0, 5836665117072163, -55, 53)))**2) + mpmath.mpf((0, 7800234554605699, -54, 53))*mpmath.exp(-mpmath.mpf(1)/mpmath.mpf(2)*(l_M_tilde + mpmath.mpf((1, 6458161865649291, -53, 53)))**2/(mpmath.mpf((0, 3602879701896397, -54, 52))*l_M_tilde + mpmath.mpf((1, 8618088246936181, -58, 53)))**2) + mpmath.mpf((0, 3602879701896397, -55, 52))*mpmath.exp(-mpmath.mpf((0, 8984486472937407, -51, 53))*(l_M_tilde + mpmath.mpf((1, 1, 0, 1)))**2))' + ), + ), + ( + LambdaPrinter, + ( + '(0.814*math.exp(-1/2*(l_M_tilde - 1.06)**2/(0.0633*l_M_tilde + 0.162)**2) + 0.433*math.exp(-1/2*(l_M_tilde - 0.717)**2/(0.2*l_M_tilde - 0.0299)**2) + 0.1*math.exp(-3.98991349867535*(l_M_tilde - 1.0)**2))' + ), + ), + ] + ) + def test_print_code(self, code_printer, expected): + fl_M_act = FiberForceLengthActiveDeGroote2016.with_defaults(self.l_M_tilde) + assert code_printer().doprint(fl_M_act) == expected + + def test_derivative_print_code(self): + fl_M_act = FiberForceLengthActiveDeGroote2016.with_defaults(self.l_M_tilde) + fl_M_act_dl_M_tilde = fl_M_act.diff(self.l_M_tilde) + expected = ( + '(0.79798269973507 - 0.79798269973507*l_M_tilde)*math.exp(-3.98991349867535*(l_M_tilde - 1.0)**2) + (0.433*(0.717 - l_M_tilde)/(0.2*l_M_tilde - 0.0299)**2 + 0.0866*(l_M_tilde - 0.717)**2/(0.2*l_M_tilde - 0.0299)**3)*math.exp(-1/2*(l_M_tilde - 0.717)**2/(0.2*l_M_tilde - 0.0299)**2) + (0.814*(1.06 - l_M_tilde)/(0.0633*l_M_tilde + 0.162)**2 + 0.0515262*(l_M_tilde - 1.06)**2/(0.0633*l_M_tilde + 0.162)**3)*math.exp(-1/2*(l_M_tilde - 1.06)**2/(0.0633*l_M_tilde + 0.162)**2)' + ) + assert PythonCodePrinter().doprint(fl_M_act_dl_M_tilde) == expected + + def test_lambdify(self): + fl_M_act = FiberForceLengthActiveDeGroote2016.with_defaults(self.l_M_tilde) + fl_M_act_callable = lambdify(self.l_M_tilde, fl_M_act) + assert fl_M_act_callable(1.0) == pytest.approx(0.9941398866) + + @pytest.mark.skipif(numpy is None, reason='NumPy not installed') + def test_lambdify_numpy(self): + fl_M_act = FiberForceLengthActiveDeGroote2016.with_defaults(self.l_M_tilde) + fl_M_act_callable = lambdify(self.l_M_tilde, fl_M_act, 'numpy') + l_M_tilde = numpy.array([0.0, 0.5, 1.0, 1.5, 2.0]) + expected = numpy.array([ + 0.0018501319, + 0.0529122812, + 0.9941398866, + 0.2312431531, + 0.0069595432, + ]) + numpy.testing.assert_allclose(fl_M_act_callable(l_M_tilde), expected) + + @pytest.mark.skipif(jax is None, reason='JAX not installed') + def test_lambdify_jax(self): + fl_M_act = FiberForceLengthActiveDeGroote2016.with_defaults(self.l_M_tilde) + fl_M_act_callable = jax.jit(lambdify(self.l_M_tilde, fl_M_act, 'jax')) + l_M_tilde = jax.numpy.array([0.0, 0.5, 1.0, 1.5, 2.0]) + expected = jax.numpy.array([ + 0.0018501319, + 0.0529122812, + 0.9941398866, + 0.2312431531, + 0.0069595432, + ]) + numpy.testing.assert_allclose(fl_M_act_callable(l_M_tilde), expected) + + +class TestFiberForceVelocityDeGroote2016: + + @pytest.fixture(autouse=True) + def _muscle_fiber_force_velocity_arguments_fixture(self): + self.v_M_tilde = Symbol('v_M_tilde') + self.c0 = Symbol('c_0') + self.c1 = Symbol('c_1') + self.c2 = Symbol('c_2') + self.c3 = Symbol('c_3') + self.constants = (self.c0, self.c1, self.c2, self.c3) + + @staticmethod + def test_class(): + assert issubclass(FiberForceVelocityDeGroote2016, Function) + assert issubclass(FiberForceVelocityDeGroote2016, CharacteristicCurveFunction) + assert FiberForceVelocityDeGroote2016.__name__ == 'FiberForceVelocityDeGroote2016' + + def test_instance(self): + fv_M = FiberForceVelocityDeGroote2016(self.v_M_tilde, *self.constants) + assert isinstance(fv_M, FiberForceVelocityDeGroote2016) + assert str(fv_M) == 'FiberForceVelocityDeGroote2016(v_M_tilde, c_0, c_1, c_2, c_3)' + + def test_doit(self): + fv_M = FiberForceVelocityDeGroote2016(self.v_M_tilde, *self.constants).doit() + expected = ( + self.c0 * log((self.c1 * self.v_M_tilde + self.c2) + + sqrt((self.c1 * self.v_M_tilde + self.c2)**2 + 1)) + self.c3 + ) + assert fv_M == expected + + def test_doit_evaluate_false(self): + fv_M = FiberForceVelocityDeGroote2016(self.v_M_tilde, *self.constants).doit(evaluate=False) + expected = ( + self.c0 * log((self.c1 * self.v_M_tilde + self.c2) + + sqrt(UnevaluatedExpr(self.c1 * self.v_M_tilde + self.c2)**2 + 1)) + self.c3 + ) + assert fv_M == expected + + def test_with_defaults(self): + constants = ( + Float('-0.318'), + Float('-8.149'), + Float('-0.374'), + Float('0.886'), + ) + fv_M_manual = FiberForceVelocityDeGroote2016(self.v_M_tilde, *constants) + fv_M_constants = FiberForceVelocityDeGroote2016.with_defaults(self.v_M_tilde) + assert fv_M_manual == fv_M_constants + + def test_differentiate_wrt_v_M_tilde(self): + fv_M = FiberForceVelocityDeGroote2016(self.v_M_tilde, *self.constants) + expected = ( + self.c0*self.c1 + /sqrt(UnevaluatedExpr(self.c1*self.v_M_tilde + self.c2)**2 + 1) + ) + assert fv_M.diff(self.v_M_tilde) == expected + + def test_differentiate_wrt_c0(self): + fv_M = FiberForceVelocityDeGroote2016(self.v_M_tilde, *self.constants) + expected = log( + self.c1*self.v_M_tilde + self.c2 + + sqrt(UnevaluatedExpr(self.c1*self.v_M_tilde + self.c2)**2 + 1) + ) + assert fv_M.diff(self.c0) == expected + + def test_differentiate_wrt_c1(self): + fv_M = FiberForceVelocityDeGroote2016(self.v_M_tilde, *self.constants) + expected = ( + self.c0*self.v_M_tilde + /sqrt(UnevaluatedExpr(self.c1*self.v_M_tilde + self.c2)**2 + 1) + ) + assert fv_M.diff(self.c1) == expected + + def test_differentiate_wrt_c2(self): + fv_M = FiberForceVelocityDeGroote2016(self.v_M_tilde, *self.constants) + expected = ( + self.c0 + /sqrt(UnevaluatedExpr(self.c1*self.v_M_tilde + self.c2)**2 + 1) + ) + assert fv_M.diff(self.c2) == expected + + def test_differentiate_wrt_c3(self): + fv_M = FiberForceVelocityDeGroote2016(self.v_M_tilde, *self.constants) + expected = Integer(1) + assert fv_M.diff(self.c3) == expected + + def test_inverse(self): + fv_M = FiberForceVelocityDeGroote2016(self.v_M_tilde, *self.constants) + assert fv_M.inverse() is FiberForceVelocityInverseDeGroote2016 + + def test_function_print_latex(self): + fv_M = FiberForceVelocityDeGroote2016(self.v_M_tilde, *self.constants) + expected = r'\operatorname{fv}^M \left( v_{M tilde} \right)' + assert LatexPrinter().doprint(fv_M) == expected + + def test_expression_print_latex(self): + fv_M = FiberForceVelocityDeGroote2016(self.v_M_tilde, *self.constants) + expected = ( + r'c_{0} \log{\left(c_{1} v_{M tilde} + c_{2} + \sqrt{\left(c_{1} ' + r'v_{M tilde} + c_{2}\right)^{2} + 1} \right)} + c_{3}' + ) + assert LatexPrinter().doprint(fv_M.doit()) == expected + + @pytest.mark.parametrize( + 'code_printer, expected', + [ + ( + C89CodePrinter, + '(0.88600000000000001 - 0.318*log(-8.1489999999999991*v_M_tilde ' + '- 0.374 + sqrt(1 + pow(-8.1489999999999991*v_M_tilde - 0.374, 2))))', + ), + ( + C99CodePrinter, + '(0.88600000000000001 - 0.318*log(-8.1489999999999991*v_M_tilde ' + '- 0.374 + sqrt(1 + pow(-8.1489999999999991*v_M_tilde - 0.374, 2))))', + ), + ( + C11CodePrinter, + '(0.88600000000000001 - 0.318*log(-8.1489999999999991*v_M_tilde ' + '- 0.374 + sqrt(1 + pow(-8.1489999999999991*v_M_tilde - 0.374, 2))))', + ), + ( + CXX98CodePrinter, + '(0.88600000000000001 - 0.318*log(-8.1489999999999991*v_M_tilde ' + '- 0.374 + std::sqrt(1 + std::pow(-8.1489999999999991*v_M_tilde - 0.374, 2))))', + ), + ( + CXX11CodePrinter, + '(0.88600000000000001 - 0.318*std::log(-8.1489999999999991*v_M_tilde ' + '- 0.374 + std::sqrt(1 + std::pow(-8.1489999999999991*v_M_tilde - 0.374, 2))))', + ), + ( + CXX17CodePrinter, + '(0.88600000000000001 - 0.318*std::log(-8.1489999999999991*v_M_tilde ' + '- 0.374 + std::sqrt(1 + std::pow(-8.1489999999999991*v_M_tilde - 0.374, 2))))', + ), + ( + FCodePrinter, + ' (0.886d0 - 0.318d0*log(-8.1489999999999991d0*v_M_tilde - 0.374d0 +\n' + ' @ sqrt(1.0d0 + (-8.149d0*v_M_tilde - 0.374d0)**2)))', + ), + ( + OctaveCodePrinter, + '(0.886 - 0.318*log(-8.149*v_M_tilde - 0.374 ' + '+ sqrt(1 + (-8.149*v_M_tilde - 0.374).^2)))', + ), + ( + PythonCodePrinter, + '(0.886 - 0.318*math.log(-8.149*v_M_tilde - 0.374 ' + '+ math.sqrt(1 + (-8.149*v_M_tilde - 0.374)**2)))', + ), + ( + NumPyPrinter, + '(0.886 - 0.318*numpy.log(-8.149*v_M_tilde - 0.374 ' + '+ numpy.sqrt(1 + (-8.149*v_M_tilde - 0.374)**2)))', + ), + ( + SciPyPrinter, + '(0.886 - 0.318*numpy.log(-8.149*v_M_tilde - 0.374 ' + '+ numpy.sqrt(1 + (-8.149*v_M_tilde - 0.374)**2)))', + ), + ( + CuPyPrinter, + '(0.886 - 0.318*cupy.log(-8.149*v_M_tilde - 0.374 ' + '+ cupy.sqrt(1 + (-8.149*v_M_tilde - 0.374)**2)))', + ), + ( + JaxPrinter, + '(0.886 - 0.318*jax.numpy.log(-8.149*v_M_tilde - 0.374 ' + '+ jax.numpy.sqrt(1 + (-8.149*v_M_tilde - 0.374)**2)))', + ), + ( + MpmathPrinter, + '(mpmath.mpf((0, 7980378539700519, -53, 53)) ' + '- mpmath.mpf((0, 5728578726015271, -54, 53))' + '*mpmath.log(-mpmath.mpf((0, 4587479170430271, -49, 53))*v_M_tilde ' + '+ mpmath.mpf((1, 3368692521273131, -53, 52)) ' + '+ mpmath.sqrt(1 + (-mpmath.mpf((0, 4587479170430271, -49, 53))*v_M_tilde ' + '+ mpmath.mpf((1, 3368692521273131, -53, 52)))**2)))', + ), + ( + LambdaPrinter, + '(0.886 - 0.318*math.log(-8.149*v_M_tilde - 0.374 ' + '+ sqrt(1 + (-8.149*v_M_tilde - 0.374)**2)))', + ), + ] + ) + def test_print_code(self, code_printer, expected): + fv_M = FiberForceVelocityDeGroote2016.with_defaults(self.v_M_tilde) + assert code_printer().doprint(fv_M) == expected + + def test_derivative_print_code(self): + fv_M = FiberForceVelocityDeGroote2016.with_defaults(self.v_M_tilde) + dfv_M_dv_M_tilde = fv_M.diff(self.v_M_tilde) + expected = '2.591382*(1 + (-8.149*v_M_tilde - 0.374)**2)**(-1/2)' + assert PythonCodePrinter().doprint(dfv_M_dv_M_tilde) == expected + + def test_lambdify(self): + fv_M = FiberForceVelocityDeGroote2016.with_defaults(self.v_M_tilde) + fv_M_callable = lambdify(self.v_M_tilde, fv_M) + assert fv_M_callable(0.0) == pytest.approx(1.002320622548512) + + @pytest.mark.skipif(numpy is None, reason='NumPy not installed') + def test_lambdify_numpy(self): + fv_M = FiberForceVelocityDeGroote2016.with_defaults(self.v_M_tilde) + fv_M_callable = lambdify(self.v_M_tilde, fv_M, 'numpy') + v_M_tilde = numpy.array([-1.0, -0.5, 0.0, 0.5]) + expected = numpy.array([ + 0.0120816781, + 0.2438336294, + 1.0023206225, + 1.5850003903, + ]) + numpy.testing.assert_allclose(fv_M_callable(v_M_tilde), expected) + + @pytest.mark.skipif(jax is None, reason='JAX not installed') + def test_lambdify_jax(self): + fv_M = FiberForceVelocityDeGroote2016.with_defaults(self.v_M_tilde) + fv_M_callable = jax.jit(lambdify(self.v_M_tilde, fv_M, 'jax')) + v_M_tilde = jax.numpy.array([-1.0, -0.5, 0.0, 0.5]) + expected = jax.numpy.array([ + 0.0120816781, + 0.2438336294, + 1.0023206225, + 1.5850003903, + ]) + numpy.testing.assert_allclose(fv_M_callable(v_M_tilde), expected) + + +class TestFiberForceVelocityInverseDeGroote2016: + + @pytest.fixture(autouse=True) + def _tendon_force_length_inverse_arguments_fixture(self): + self.fv_M = Symbol('fv_M') + self.c0 = Symbol('c_0') + self.c1 = Symbol('c_1') + self.c2 = Symbol('c_2') + self.c3 = Symbol('c_3') + self.constants = (self.c0, self.c1, self.c2, self.c3) + + @staticmethod + def test_class(): + assert issubclass(FiberForceVelocityInverseDeGroote2016, Function) + assert issubclass(FiberForceVelocityInverseDeGroote2016, CharacteristicCurveFunction) + assert FiberForceVelocityInverseDeGroote2016.__name__ == 'FiberForceVelocityInverseDeGroote2016' + + def test_instance(self): + fv_M_inv = FiberForceVelocityInverseDeGroote2016(self.fv_M, *self.constants) + assert isinstance(fv_M_inv, FiberForceVelocityInverseDeGroote2016) + assert str(fv_M_inv) == 'FiberForceVelocityInverseDeGroote2016(fv_M, c_0, c_1, c_2, c_3)' + + def test_doit(self): + fv_M_inv = FiberForceVelocityInverseDeGroote2016(self.fv_M, *self.constants).doit() + assert fv_M_inv == (sinh((self.fv_M - self.c3)/self.c0) - self.c2)/self.c1 + + def test_doit_evaluate_false(self): + fv_M_inv = FiberForceVelocityInverseDeGroote2016(self.fv_M, *self.constants).doit(evaluate=False) + assert fv_M_inv == (sinh(UnevaluatedExpr(self.fv_M - self.c3)/self.c0) - self.c2)/self.c1 + + def test_with_defaults(self): + constants = ( + Float('-0.318'), + Float('-8.149'), + Float('-0.374'), + Float('0.886'), + ) + fv_M_inv_manual = FiberForceVelocityInverseDeGroote2016(self.fv_M, *constants) + fv_M_inv_constants = FiberForceVelocityInverseDeGroote2016.with_defaults(self.fv_M) + assert fv_M_inv_manual == fv_M_inv_constants + + def test_differentiate_wrt_fv_M(self): + fv_M_inv = FiberForceVelocityInverseDeGroote2016(self.fv_M, *self.constants) + expected = cosh((self.fv_M - self.c3)/self.c0)/(self.c0*self.c1) + assert fv_M_inv.diff(self.fv_M) == expected + + def test_differentiate_wrt_c0(self): + fv_M_inv = FiberForceVelocityInverseDeGroote2016(self.fv_M, *self.constants) + expected = (self.c3 - self.fv_M)*cosh((self.fv_M - self.c3)/self.c0)/(self.c0**2*self.c1) + assert fv_M_inv.diff(self.c0) == expected + + def test_differentiate_wrt_c1(self): + fv_M_inv = FiberForceVelocityInverseDeGroote2016(self.fv_M, *self.constants) + expected = (self.c2 - sinh((self.fv_M - self.c3)/self.c0))/self.c1**2 + assert fv_M_inv.diff(self.c1) == expected + + def test_differentiate_wrt_c2(self): + fv_M_inv = FiberForceVelocityInverseDeGroote2016(self.fv_M, *self.constants) + expected = -1/self.c1 + assert fv_M_inv.diff(self.c2) == expected + + def test_differentiate_wrt_c3(self): + fv_M_inv = FiberForceVelocityInverseDeGroote2016(self.fv_M, *self.constants) + expected = -cosh((self.fv_M - self.c3)/self.c0)/(self.c0*self.c1) + assert fv_M_inv.diff(self.c3) == expected + + def test_inverse(self): + fv_M_inv = FiberForceVelocityInverseDeGroote2016(self.fv_M, *self.constants) + assert fv_M_inv.inverse() is FiberForceVelocityDeGroote2016 + + def test_function_print_latex(self): + fv_M_inv = FiberForceVelocityInverseDeGroote2016(self.fv_M, *self.constants) + expected = r'\left( \operatorname{fv}^M \right)^{-1} \left( fv_{M} \right)' + assert LatexPrinter().doprint(fv_M_inv) == expected + + def test_expression_print_latex(self): + fv_M = FiberForceVelocityInverseDeGroote2016(self.fv_M, *self.constants) + expected = r'\frac{- c_{2} + \sinh{\left(\frac{- c_{3} + fv_{M}}{c_{0}} \right)}}{c_{1}}' + assert LatexPrinter().doprint(fv_M.doit()) == expected + + @pytest.mark.parametrize( + 'code_printer, expected', + [ + ( + C89CodePrinter, + '(-0.12271444348999878*(0.374 - sinh(3.1446540880503142*(fv_M ' + '- 0.88600000000000001))))', + ), + ( + C99CodePrinter, + '(-0.12271444348999878*(0.374 - sinh(3.1446540880503142*(fv_M ' + '- 0.88600000000000001))))', + ), + ( + C11CodePrinter, + '(-0.12271444348999878*(0.374 - sinh(3.1446540880503142*(fv_M ' + '- 0.88600000000000001))))', + ), + ( + CXX98CodePrinter, + '(-0.12271444348999878*(0.374 - sinh(3.1446540880503142*(fv_M ' + '- 0.88600000000000001))))', + ), + ( + CXX11CodePrinter, + '(-0.12271444348999878*(0.374 - std::sinh(3.1446540880503142' + '*(fv_M - 0.88600000000000001))))', + ), + ( + CXX17CodePrinter, + '(-0.12271444348999878*(0.374 - std::sinh(3.1446540880503142' + '*(fv_M - 0.88600000000000001))))', + ), + ( + FCodePrinter, + ' (-0.122714443489999d0*(0.374d0 - sinh(3.1446540880503142d0*(fv_M -\n' + ' @ 0.886d0))))', + ), + ( + OctaveCodePrinter, + '(-0.122714443489999*(0.374 - sinh(3.14465408805031*(fv_M ' + '- 0.886))))', + ), + ( + PythonCodePrinter, + '(-0.122714443489999*(0.374 - math.sinh(3.14465408805031*(fv_M ' + '- 0.886))))', + ), + ( + NumPyPrinter, + '(-0.122714443489999*(0.374 - numpy.sinh(3.14465408805031' + '*(fv_M - 0.886))))', + ), + ( + SciPyPrinter, + '(-0.122714443489999*(0.374 - numpy.sinh(3.14465408805031' + '*(fv_M - 0.886))))', + ), + ( + CuPyPrinter, + '(-0.122714443489999*(0.374 - cupy.sinh(3.14465408805031*(fv_M ' + '- 0.886))))', + ), + ( + JaxPrinter, + '(-0.122714443489999*(0.374 - jax.numpy.sinh(3.14465408805031' + '*(fv_M - 0.886))))', + ), + ( + MpmathPrinter, + '(-mpmath.mpf((0, 8842507551592581, -56, 53))*(mpmath.mpf((0, ' + '3368692521273131, -53, 52)) - mpmath.sinh(mpmath.mpf((0, ' + '7081131489576251, -51, 53))*(fv_M + mpmath.mpf((1, ' + '7980378539700519, -53, 53))))))', + ), + ( + LambdaPrinter, + '(-0.122714443489999*(0.374 - math.sinh(3.14465408805031*(fv_M ' + '- 0.886))))', + ), + ] + ) + def test_print_code(self, code_printer, expected): + fv_M_inv = FiberForceVelocityInverseDeGroote2016.with_defaults(self.fv_M) + assert code_printer().doprint(fv_M_inv) == expected + + def test_derivative_print_code(self): + fv_M_inv = FiberForceVelocityInverseDeGroote2016.with_defaults(self.fv_M) + dfv_M_inv_dfv_M = fv_M_inv.diff(self.fv_M) + expected = ( + '0.385894476383644*math.cosh(3.14465408805031*fv_M ' + '- 2.78616352201258)' + ) + assert PythonCodePrinter().doprint(dfv_M_inv_dfv_M) == expected + + def test_lambdify(self): + fv_M_inv = FiberForceVelocityInverseDeGroote2016.with_defaults(self.fv_M) + fv_M_inv_callable = lambdify(self.fv_M, fv_M_inv) + assert fv_M_inv_callable(1.0) == pytest.approx(-0.0009548832444487479) + + @pytest.mark.skipif(numpy is None, reason='NumPy not installed') + def test_lambdify_numpy(self): + fv_M_inv = FiberForceVelocityInverseDeGroote2016.with_defaults(self.fv_M) + fv_M_inv_callable = lambdify(self.fv_M, fv_M_inv, 'numpy') + fv_M = numpy.array([0.8, 0.9, 1.0, 1.1, 1.2]) + expected = numpy.array([ + -0.0794881459, + -0.0404909338, + -0.0009548832, + 0.043061991, + 0.0959484397, + ]) + numpy.testing.assert_allclose(fv_M_inv_callable(fv_M), expected) + + @pytest.mark.skipif(jax is None, reason='JAX not installed') + def test_lambdify_jax(self): + fv_M_inv = FiberForceVelocityInverseDeGroote2016.with_defaults(self.fv_M) + fv_M_inv_callable = jax.jit(lambdify(self.fv_M, fv_M_inv, 'jax')) + fv_M = jax.numpy.array([0.8, 0.9, 1.0, 1.1, 1.2]) + expected = jax.numpy.array([ + -0.0794881459, + -0.0404909338, + -0.0009548832, + 0.043061991, + 0.0959484397, + ]) + numpy.testing.assert_allclose(fv_M_inv_callable(fv_M), expected) + + +class TestCharacteristicCurveCollection: + + @staticmethod + def test_valid_constructor(): + curves = CharacteristicCurveCollection( + tendon_force_length=TendonForceLengthDeGroote2016, + tendon_force_length_inverse=TendonForceLengthInverseDeGroote2016, + fiber_force_length_passive=FiberForceLengthPassiveDeGroote2016, + fiber_force_length_passive_inverse=FiberForceLengthPassiveInverseDeGroote2016, + fiber_force_length_active=FiberForceLengthActiveDeGroote2016, + fiber_force_velocity=FiberForceVelocityDeGroote2016, + fiber_force_velocity_inverse=FiberForceVelocityInverseDeGroote2016, + ) + assert curves.tendon_force_length is TendonForceLengthDeGroote2016 + assert curves.tendon_force_length_inverse is TendonForceLengthInverseDeGroote2016 + assert curves.fiber_force_length_passive is FiberForceLengthPassiveDeGroote2016 + assert curves.fiber_force_length_passive_inverse is FiberForceLengthPassiveInverseDeGroote2016 + assert curves.fiber_force_length_active is FiberForceLengthActiveDeGroote2016 + assert curves.fiber_force_velocity is FiberForceVelocityDeGroote2016 + assert curves.fiber_force_velocity_inverse is FiberForceVelocityInverseDeGroote2016 + + @staticmethod + @pytest.mark.skip(reason='kw_only dataclasses only valid in Python >3.10') + def test_invalid_constructor_keyword_only(): + with pytest.raises(TypeError): + _ = CharacteristicCurveCollection( + TendonForceLengthDeGroote2016, + TendonForceLengthInverseDeGroote2016, + FiberForceLengthPassiveDeGroote2016, + FiberForceLengthPassiveInverseDeGroote2016, + FiberForceLengthActiveDeGroote2016, + FiberForceVelocityDeGroote2016, + FiberForceVelocityInverseDeGroote2016, + ) + + @staticmethod + @pytest.mark.parametrize( + 'kwargs', + [ + {'tendon_force_length': TendonForceLengthDeGroote2016}, + { + 'tendon_force_length': TendonForceLengthDeGroote2016, + 'tendon_force_length_inverse': TendonForceLengthInverseDeGroote2016, + 'fiber_force_length_passive': FiberForceLengthPassiveDeGroote2016, + 'fiber_force_length_passive_inverse': FiberForceLengthPassiveInverseDeGroote2016, + 'fiber_force_length_active': FiberForceLengthActiveDeGroote2016, + 'fiber_force_velocity': FiberForceVelocityDeGroote2016, + 'fiber_force_velocity_inverse': FiberForceVelocityInverseDeGroote2016, + 'extra_kwarg': None, + }, + ] + ) + def test_invalid_constructor_wrong_number_args(kwargs): + with pytest.raises(TypeError): + _ = CharacteristicCurveCollection(**kwargs) + + @staticmethod + def test_instance_is_immutable(): + curves = CharacteristicCurveCollection( + tendon_force_length=TendonForceLengthDeGroote2016, + tendon_force_length_inverse=TendonForceLengthInverseDeGroote2016, + fiber_force_length_passive=FiberForceLengthPassiveDeGroote2016, + fiber_force_length_passive_inverse=FiberForceLengthPassiveInverseDeGroote2016, + fiber_force_length_active=FiberForceLengthActiveDeGroote2016, + fiber_force_velocity=FiberForceVelocityDeGroote2016, + fiber_force_velocity_inverse=FiberForceVelocityInverseDeGroote2016, + ) + with pytest.raises(AttributeError): + curves.tendon_force_length = None + with pytest.raises(AttributeError): + curves.tendon_force_length_inverse = None + with pytest.raises(AttributeError): + curves.fiber_force_length_passive = None + with pytest.raises(AttributeError): + curves.fiber_force_length_passive_inverse = None + with pytest.raises(AttributeError): + curves.fiber_force_length_active = None + with pytest.raises(AttributeError): + curves.fiber_force_velocity = None + with pytest.raises(AttributeError): + curves.fiber_force_velocity_inverse = None diff --git a/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/tests/test_mixin.py b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/tests/test_mixin.py new file mode 100644 index 0000000000000000000000000000000000000000..be079c195f3d961a88f52c94b695666f2a4f2bb5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/tests/test_mixin.py @@ -0,0 +1,48 @@ +"""Tests for the ``sympy.physics.biomechanics._mixin.py`` module.""" + +import pytest + +from sympy.physics.biomechanics._mixin import _NamedMixin + + +class TestNamedMixin: + + @staticmethod + def test_subclass(): + + class Subclass(_NamedMixin): + + def __init__(self, name): + self.name = name + + instance = Subclass('name') + assert instance.name == 'name' + + @pytest.fixture(autouse=True) + def _named_mixin_fixture(self): + + class Subclass(_NamedMixin): + + def __init__(self, name): + self.name = name + + self.Subclass = Subclass + + @pytest.mark.parametrize('name', ['a', 'name', 'long_name']) + def test_valid_name_argument(self, name): + instance = self.Subclass(name) + assert instance.name == name + + @pytest.mark.parametrize('invalid_name', [0, 0.0, None, False]) + def test_invalid_name_argument_not_str(self, invalid_name): + with pytest.raises(TypeError): + _ = self.Subclass(invalid_name) + + def test_invalid_name_argument_zero_length_str(self): + with pytest.raises(ValueError): + _ = self.Subclass('') + + def test_name_attribute_is_immutable(self): + instance = self.Subclass('name') + with pytest.raises(AttributeError): + instance.name = 'new_name' diff --git a/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/tests/test_musculotendon.py b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/tests/test_musculotendon.py new file mode 100644 index 0000000000000000000000000000000000000000..d0c5a1088214049aaaaa3666854e232d26f77786 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/biomechanics/tests/test_musculotendon.py @@ -0,0 +1,837 @@ +"""Tests for the ``sympy.physics.biomechanics.musculotendon.py`` module.""" + +import abc + +import pytest + +from sympy.core.expr import UnevaluatedExpr +from sympy.core.numbers import Float, Integer, Rational +from sympy.core.symbol import Symbol +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.hyperbolic import tanh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin +from sympy.matrices.dense import MutableDenseMatrix as Matrix, eye, zeros +from sympy.physics.biomechanics.activation import ( + FirstOrderActivationDeGroote2016 +) +from sympy.physics.biomechanics.curve import ( + CharacteristicCurveCollection, + FiberForceLengthActiveDeGroote2016, + FiberForceLengthPassiveDeGroote2016, + FiberForceLengthPassiveInverseDeGroote2016, + FiberForceVelocityDeGroote2016, + FiberForceVelocityInverseDeGroote2016, + TendonForceLengthDeGroote2016, + TendonForceLengthInverseDeGroote2016, +) +from sympy.physics.biomechanics.musculotendon import ( + MusculotendonBase, + MusculotendonDeGroote2016, + MusculotendonFormulation, +) +from sympy.physics.biomechanics._mixin import _NamedMixin +from sympy.physics.mechanics.actuator import ForceActuator +from sympy.physics.mechanics.pathway import LinearPathway +from sympy.physics.vector.frame import ReferenceFrame +from sympy.physics.vector.functions import dynamicsymbols +from sympy.physics.vector.point import Point +from sympy.simplify.simplify import simplify + + +class TestMusculotendonFormulation: + @staticmethod + def test_rigid_tendon_member(): + assert MusculotendonFormulation(0) == 0 + assert MusculotendonFormulation.RIGID_TENDON == 0 + + @staticmethod + def test_fiber_length_explicit_member(): + assert MusculotendonFormulation(1) == 1 + assert MusculotendonFormulation.FIBER_LENGTH_EXPLICIT == 1 + + @staticmethod + def test_tendon_force_explicit_member(): + assert MusculotendonFormulation(2) == 2 + assert MusculotendonFormulation.TENDON_FORCE_EXPLICIT == 2 + + @staticmethod + def test_fiber_length_implicit_member(): + assert MusculotendonFormulation(3) == 3 + assert MusculotendonFormulation.FIBER_LENGTH_IMPLICIT == 3 + + @staticmethod + def test_tendon_force_implicit_member(): + assert MusculotendonFormulation(4) == 4 + assert MusculotendonFormulation.TENDON_FORCE_IMPLICIT == 4 + + +class TestMusculotendonBase: + + @staticmethod + def test_is_abstract_base_class(): + assert issubclass(MusculotendonBase, abc.ABC) + + @staticmethod + def test_class(): + assert issubclass(MusculotendonBase, ForceActuator) + assert issubclass(MusculotendonBase, _NamedMixin) + assert MusculotendonBase.__name__ == 'MusculotendonBase' + + @staticmethod + def test_cannot_instantiate_directly(): + with pytest.raises(TypeError): + _ = MusculotendonBase() + + +@pytest.mark.parametrize('musculotendon_concrete', [MusculotendonDeGroote2016]) +class TestMusculotendonRigidTendon: + + @pytest.fixture(autouse=True) + def _musculotendon_rigid_tendon_fixture(self, musculotendon_concrete): + self.name = 'name' + self.N = ReferenceFrame('N') + self.q = dynamicsymbols('q') + self.origin = Point('pO') + self.insertion = Point('pI') + self.insertion.set_pos(self.origin, self.q*self.N.x) + self.pathway = LinearPathway(self.origin, self.insertion) + self.activation = FirstOrderActivationDeGroote2016(self.name) + self.e = self.activation.excitation + self.a = self.activation.activation + self.tau_a = self.activation.activation_time_constant + self.tau_d = self.activation.deactivation_time_constant + self.b = self.activation.smoothing_rate + self.formulation = MusculotendonFormulation.RIGID_TENDON + self.l_T_slack = Symbol('l_T_slack') + self.F_M_max = Symbol('F_M_max') + self.l_M_opt = Symbol('l_M_opt') + self.v_M_max = Symbol('v_M_max') + self.alpha_opt = Symbol('alpha_opt') + self.beta = Symbol('beta') + self.instance = musculotendon_concrete( + self.name, + self.pathway, + self.activation, + musculotendon_dynamics=self.formulation, + tendon_slack_length=self.l_T_slack, + peak_isometric_force=self.F_M_max, + optimal_fiber_length=self.l_M_opt, + maximal_fiber_velocity=self.v_M_max, + optimal_pennation_angle=self.alpha_opt, + fiber_damping_coefficient=self.beta, + ) + self.da_expr = ( + (1/(self.tau_a*(Rational(1, 2) + Rational(3, 2)*self.a))) + *(Rational(1, 2) + Rational(1, 2)*tanh(self.b*(self.e - self.a))) + + ((Rational(1, 2) + Rational(3, 2)*self.a)/self.tau_d) + *(Rational(1, 2) - Rational(1, 2)*tanh(self.b*(self.e - self.a))) + )*(self.e - self.a) + + def test_state_vars(self): + assert hasattr(self.instance, 'x') + assert hasattr(self.instance, 'state_vars') + assert self.instance.x == self.instance.state_vars + x_expected = Matrix([self.a]) + assert self.instance.x == x_expected + assert self.instance.state_vars == x_expected + assert isinstance(self.instance.x, Matrix) + assert isinstance(self.instance.state_vars, Matrix) + assert self.instance.x.shape == (1, 1) + assert self.instance.state_vars.shape == (1, 1) + + def test_input_vars(self): + assert hasattr(self.instance, 'r') + assert hasattr(self.instance, 'input_vars') + assert self.instance.r == self.instance.input_vars + r_expected = Matrix([self.e]) + assert self.instance.r == r_expected + assert self.instance.input_vars == r_expected + assert isinstance(self.instance.r, Matrix) + assert isinstance(self.instance.input_vars, Matrix) + assert self.instance.r.shape == (1, 1) + assert self.instance.input_vars.shape == (1, 1) + + def test_constants(self): + assert hasattr(self.instance, 'p') + assert hasattr(self.instance, 'constants') + assert self.instance.p == self.instance.constants + p_expected = Matrix( + [ + self.l_T_slack, + self.F_M_max, + self.l_M_opt, + self.v_M_max, + self.alpha_opt, + self.beta, + self.tau_a, + self.tau_d, + self.b, + Symbol('c_0_fl_T_name'), + Symbol('c_1_fl_T_name'), + Symbol('c_2_fl_T_name'), + Symbol('c_3_fl_T_name'), + Symbol('c_0_fl_M_pas_name'), + Symbol('c_1_fl_M_pas_name'), + Symbol('c_0_fl_M_act_name'), + Symbol('c_1_fl_M_act_name'), + Symbol('c_2_fl_M_act_name'), + Symbol('c_3_fl_M_act_name'), + Symbol('c_4_fl_M_act_name'), + Symbol('c_5_fl_M_act_name'), + Symbol('c_6_fl_M_act_name'), + Symbol('c_7_fl_M_act_name'), + Symbol('c_8_fl_M_act_name'), + Symbol('c_9_fl_M_act_name'), + Symbol('c_10_fl_M_act_name'), + Symbol('c_11_fl_M_act_name'), + Symbol('c_0_fv_M_name'), + Symbol('c_1_fv_M_name'), + Symbol('c_2_fv_M_name'), + Symbol('c_3_fv_M_name'), + ] + ) + assert self.instance.p == p_expected + assert self.instance.constants == p_expected + assert isinstance(self.instance.p, Matrix) + assert isinstance(self.instance.constants, Matrix) + assert self.instance.p.shape == (31, 1) + assert self.instance.constants.shape == (31, 1) + + def test_M(self): + assert hasattr(self.instance, 'M') + M_expected = Matrix([1]) + assert self.instance.M == M_expected + assert isinstance(self.instance.M, Matrix) + assert self.instance.M.shape == (1, 1) + + def test_F(self): + assert hasattr(self.instance, 'F') + F_expected = Matrix([self.da_expr]) + assert self.instance.F == F_expected + assert isinstance(self.instance.F, Matrix) + assert self.instance.F.shape == (1, 1) + + def test_rhs(self): + assert hasattr(self.instance, 'rhs') + rhs_expected = Matrix([self.da_expr]) + rhs = self.instance.rhs() + assert isinstance(rhs, Matrix) + assert rhs.shape == (1, 1) + assert simplify(rhs - rhs_expected) == zeros(1) + + +@pytest.mark.parametrize( + 'musculotendon_concrete, curve', + [ + ( + MusculotendonDeGroote2016, + CharacteristicCurveCollection( + tendon_force_length=TendonForceLengthDeGroote2016, + tendon_force_length_inverse=TendonForceLengthInverseDeGroote2016, + fiber_force_length_passive=FiberForceLengthPassiveDeGroote2016, + fiber_force_length_passive_inverse=FiberForceLengthPassiveInverseDeGroote2016, + fiber_force_length_active=FiberForceLengthActiveDeGroote2016, + fiber_force_velocity=FiberForceVelocityDeGroote2016, + fiber_force_velocity_inverse=FiberForceVelocityInverseDeGroote2016, + ), + ) + ], +) +class TestFiberLengthExplicit: + + @pytest.fixture(autouse=True) + def _musculotendon_fiber_length_explicit_fixture( + self, + musculotendon_concrete, + curve, + ): + self.name = 'name' + self.N = ReferenceFrame('N') + self.q = dynamicsymbols('q') + self.origin = Point('pO') + self.insertion = Point('pI') + self.insertion.set_pos(self.origin, self.q*self.N.x) + self.pathway = LinearPathway(self.origin, self.insertion) + self.activation = FirstOrderActivationDeGroote2016(self.name) + self.e = self.activation.excitation + self.a = self.activation.activation + self.tau_a = self.activation.activation_time_constant + self.tau_d = self.activation.deactivation_time_constant + self.b = self.activation.smoothing_rate + self.formulation = MusculotendonFormulation.FIBER_LENGTH_EXPLICIT + self.l_T_slack = Symbol('l_T_slack') + self.F_M_max = Symbol('F_M_max') + self.l_M_opt = Symbol('l_M_opt') + self.v_M_max = Symbol('v_M_max') + self.alpha_opt = Symbol('alpha_opt') + self.beta = Symbol('beta') + self.instance = musculotendon_concrete( + self.name, + self.pathway, + self.activation, + musculotendon_dynamics=self.formulation, + tendon_slack_length=self.l_T_slack, + peak_isometric_force=self.F_M_max, + optimal_fiber_length=self.l_M_opt, + maximal_fiber_velocity=self.v_M_max, + optimal_pennation_angle=self.alpha_opt, + fiber_damping_coefficient=self.beta, + with_defaults=True, + ) + self.l_M_tilde = dynamicsymbols('l_M_tilde_name') + l_MT = self.pathway.length + l_M = self.l_M_tilde*self.l_M_opt + l_T = l_MT - sqrt(l_M**2 - (self.l_M_opt*sin(self.alpha_opt))**2) + fl_T = curve.tendon_force_length.with_defaults(l_T/self.l_T_slack) + fl_M_pas = curve.fiber_force_length_passive.with_defaults(self.l_M_tilde) + fl_M_act = curve.fiber_force_length_active.with_defaults(self.l_M_tilde) + v_M_tilde = curve.fiber_force_velocity_inverse.with_defaults( + ((((fl_T*self.F_M_max)/((l_MT - l_T)/l_M))/self.F_M_max) - fl_M_pas) + /(self.a*fl_M_act) + ) + self.dl_M_tilde_expr = (self.v_M_max/self.l_M_opt)*v_M_tilde + self.da_expr = ( + (1/(self.tau_a*(Rational(1, 2) + Rational(3, 2)*self.a))) + *(Rational(1, 2) + Rational(1, 2)*tanh(self.b*(self.e - self.a))) + + ((Rational(1, 2) + Rational(3, 2)*self.a)/self.tau_d) + *(Rational(1, 2) - Rational(1, 2)*tanh(self.b*(self.e - self.a))) + )*(self.e - self.a) + + def test_state_vars(self): + assert hasattr(self.instance, 'x') + assert hasattr(self.instance, 'state_vars') + assert self.instance.x == self.instance.state_vars + x_expected = Matrix([self.l_M_tilde, self.a]) + assert self.instance.x == x_expected + assert self.instance.state_vars == x_expected + assert isinstance(self.instance.x, Matrix) + assert isinstance(self.instance.state_vars, Matrix) + assert self.instance.x.shape == (2, 1) + assert self.instance.state_vars.shape == (2, 1) + + def test_input_vars(self): + assert hasattr(self.instance, 'r') + assert hasattr(self.instance, 'input_vars') + assert self.instance.r == self.instance.input_vars + r_expected = Matrix([self.e]) + assert self.instance.r == r_expected + assert self.instance.input_vars == r_expected + assert isinstance(self.instance.r, Matrix) + assert isinstance(self.instance.input_vars, Matrix) + assert self.instance.r.shape == (1, 1) + assert self.instance.input_vars.shape == (1, 1) + + def test_constants(self): + assert hasattr(self.instance, 'p') + assert hasattr(self.instance, 'constants') + assert self.instance.p == self.instance.constants + p_expected = Matrix( + [ + self.l_T_slack, + self.F_M_max, + self.l_M_opt, + self.v_M_max, + self.alpha_opt, + self.beta, + self.tau_a, + self.tau_d, + self.b, + ] + ) + assert self.instance.p == p_expected + assert self.instance.constants == p_expected + assert isinstance(self.instance.p, Matrix) + assert isinstance(self.instance.constants, Matrix) + assert self.instance.p.shape == (9, 1) + assert self.instance.constants.shape == (9, 1) + + def test_M(self): + assert hasattr(self.instance, 'M') + M_expected = eye(2) + assert self.instance.M == M_expected + assert isinstance(self.instance.M, Matrix) + assert self.instance.M.shape == (2, 2) + + def test_F(self): + assert hasattr(self.instance, 'F') + F_expected = Matrix([self.dl_M_tilde_expr, self.da_expr]) + assert self.instance.F == F_expected + assert isinstance(self.instance.F, Matrix) + assert self.instance.F.shape == (2, 1) + + def test_rhs(self): + assert hasattr(self.instance, 'rhs') + rhs_expected = Matrix([self.dl_M_tilde_expr, self.da_expr]) + rhs = self.instance.rhs() + assert isinstance(rhs, Matrix) + assert rhs.shape == (2, 1) + assert simplify(rhs - rhs_expected) == zeros(2, 1) + + +@pytest.mark.parametrize( + 'musculotendon_concrete, curve', + [ + ( + MusculotendonDeGroote2016, + CharacteristicCurveCollection( + tendon_force_length=TendonForceLengthDeGroote2016, + tendon_force_length_inverse=TendonForceLengthInverseDeGroote2016, + fiber_force_length_passive=FiberForceLengthPassiveDeGroote2016, + fiber_force_length_passive_inverse=FiberForceLengthPassiveInverseDeGroote2016, + fiber_force_length_active=FiberForceLengthActiveDeGroote2016, + fiber_force_velocity=FiberForceVelocityDeGroote2016, + fiber_force_velocity_inverse=FiberForceVelocityInverseDeGroote2016, + ), + ) + ], +) +class TestTendonForceExplicit: + + @pytest.fixture(autouse=True) + def _musculotendon_tendon_force_explicit_fixture( + self, + musculotendon_concrete, + curve, + ): + self.name = 'name' + self.N = ReferenceFrame('N') + self.q = dynamicsymbols('q') + self.origin = Point('pO') + self.insertion = Point('pI') + self.insertion.set_pos(self.origin, self.q*self.N.x) + self.pathway = LinearPathway(self.origin, self.insertion) + self.activation = FirstOrderActivationDeGroote2016(self.name) + self.e = self.activation.excitation + self.a = self.activation.activation + self.tau_a = self.activation.activation_time_constant + self.tau_d = self.activation.deactivation_time_constant + self.b = self.activation.smoothing_rate + self.formulation = MusculotendonFormulation.TENDON_FORCE_EXPLICIT + self.l_T_slack = Symbol('l_T_slack') + self.F_M_max = Symbol('F_M_max') + self.l_M_opt = Symbol('l_M_opt') + self.v_M_max = Symbol('v_M_max') + self.alpha_opt = Symbol('alpha_opt') + self.beta = Symbol('beta') + self.instance = musculotendon_concrete( + self.name, + self.pathway, + self.activation, + musculotendon_dynamics=self.formulation, + tendon_slack_length=self.l_T_slack, + peak_isometric_force=self.F_M_max, + optimal_fiber_length=self.l_M_opt, + maximal_fiber_velocity=self.v_M_max, + optimal_pennation_angle=self.alpha_opt, + fiber_damping_coefficient=self.beta, + with_defaults=True, + ) + self.F_T_tilde = dynamicsymbols('F_T_tilde_name') + l_T_tilde = curve.tendon_force_length_inverse.with_defaults(self.F_T_tilde) + l_MT = self.pathway.length + v_MT = self.pathway.extension_velocity + l_T = l_T_tilde*self.l_T_slack + l_M = sqrt((l_MT - l_T)**2 + (self.l_M_opt*sin(self.alpha_opt))**2) + l_M_tilde = l_M/self.l_M_opt + cos_alpha = (l_MT - l_T)/l_M + F_T = self.F_T_tilde*self.F_M_max + F_M = F_T/cos_alpha + F_M_tilde = F_M/self.F_M_max + fl_M_pas = curve.fiber_force_length_passive.with_defaults(l_M_tilde) + fl_M_act = curve.fiber_force_length_active.with_defaults(l_M_tilde) + fv_M = (F_M_tilde - fl_M_pas)/(self.a*fl_M_act) + v_M_tilde = curve.fiber_force_velocity_inverse.with_defaults(fv_M) + v_M = v_M_tilde*self.v_M_max + v_T = v_MT - v_M/cos_alpha + v_T_tilde = v_T/self.l_T_slack + self.dF_T_tilde_expr = ( + Float('0.2')*Float('33.93669377311689')*exp( + Float('33.93669377311689')*UnevaluatedExpr(l_T_tilde - Float('0.995')) + )*v_T_tilde + ) + self.da_expr = ( + (1/(self.tau_a*(Rational(1, 2) + Rational(3, 2)*self.a))) + *(Rational(1, 2) + Rational(1, 2)*tanh(self.b*(self.e - self.a))) + + ((Rational(1, 2) + Rational(3, 2)*self.a)/self.tau_d) + *(Rational(1, 2) - Rational(1, 2)*tanh(self.b*(self.e - self.a))) + )*(self.e - self.a) + + def test_state_vars(self): + assert hasattr(self.instance, 'x') + assert hasattr(self.instance, 'state_vars') + assert self.instance.x == self.instance.state_vars + x_expected = Matrix([self.F_T_tilde, self.a]) + assert self.instance.x == x_expected + assert self.instance.state_vars == x_expected + assert isinstance(self.instance.x, Matrix) + assert isinstance(self.instance.state_vars, Matrix) + assert self.instance.x.shape == (2, 1) + assert self.instance.state_vars.shape == (2, 1) + + def test_input_vars(self): + assert hasattr(self.instance, 'r') + assert hasattr(self.instance, 'input_vars') + assert self.instance.r == self.instance.input_vars + r_expected = Matrix([self.e]) + assert self.instance.r == r_expected + assert self.instance.input_vars == r_expected + assert isinstance(self.instance.r, Matrix) + assert isinstance(self.instance.input_vars, Matrix) + assert self.instance.r.shape == (1, 1) + assert self.instance.input_vars.shape == (1, 1) + + def test_constants(self): + assert hasattr(self.instance, 'p') + assert hasattr(self.instance, 'constants') + assert self.instance.p == self.instance.constants + p_expected = Matrix( + [ + self.l_T_slack, + self.F_M_max, + self.l_M_opt, + self.v_M_max, + self.alpha_opt, + self.beta, + self.tau_a, + self.tau_d, + self.b, + ] + ) + assert self.instance.p == p_expected + assert self.instance.constants == p_expected + assert isinstance(self.instance.p, Matrix) + assert isinstance(self.instance.constants, Matrix) + assert self.instance.p.shape == (9, 1) + assert self.instance.constants.shape == (9, 1) + + def test_M(self): + assert hasattr(self.instance, 'M') + M_expected = eye(2) + assert self.instance.M == M_expected + assert isinstance(self.instance.M, Matrix) + assert self.instance.M.shape == (2, 2) + + def test_F(self): + assert hasattr(self.instance, 'F') + F_expected = Matrix([self.dF_T_tilde_expr, self.da_expr]) + assert self.instance.F == F_expected + assert isinstance(self.instance.F, Matrix) + assert self.instance.F.shape == (2, 1) + + def test_rhs(self): + assert hasattr(self.instance, 'rhs') + rhs_expected = Matrix([self.dF_T_tilde_expr, self.da_expr]) + rhs = self.instance.rhs() + assert isinstance(rhs, Matrix) + assert rhs.shape == (2, 1) + assert simplify(rhs - rhs_expected) == zeros(2, 1) + + +class TestMusculotendonDeGroote2016: + + @staticmethod + def test_class(): + assert issubclass(MusculotendonDeGroote2016, ForceActuator) + assert issubclass(MusculotendonDeGroote2016, _NamedMixin) + assert MusculotendonDeGroote2016.__name__ == 'MusculotendonDeGroote2016' + + @staticmethod + def test_instance(): + origin = Point('pO') + insertion = Point('pI') + insertion.set_pos(origin, dynamicsymbols('q')*ReferenceFrame('N').x) + pathway = LinearPathway(origin, insertion) + activation = FirstOrderActivationDeGroote2016('name') + l_T_slack = Symbol('l_T_slack') + F_M_max = Symbol('F_M_max') + l_M_opt = Symbol('l_M_opt') + v_M_max = Symbol('v_M_max') + alpha_opt = Symbol('alpha_opt') + beta = Symbol('beta') + instance = MusculotendonDeGroote2016( + 'name', + pathway, + activation, + musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON, + tendon_slack_length=l_T_slack, + peak_isometric_force=F_M_max, + optimal_fiber_length=l_M_opt, + maximal_fiber_velocity=v_M_max, + optimal_pennation_angle=alpha_opt, + fiber_damping_coefficient=beta, + ) + assert isinstance(instance, MusculotendonDeGroote2016) + + @pytest.fixture(autouse=True) + def _musculotendon_fixture(self): + self.name = 'name' + self.N = ReferenceFrame('N') + self.q = dynamicsymbols('q') + self.origin = Point('pO') + self.insertion = Point('pI') + self.insertion.set_pos(self.origin, self.q*self.N.x) + self.pathway = LinearPathway(self.origin, self.insertion) + self.activation = FirstOrderActivationDeGroote2016(self.name) + self.l_T_slack = Symbol('l_T_slack') + self.F_M_max = Symbol('F_M_max') + self.l_M_opt = Symbol('l_M_opt') + self.v_M_max = Symbol('v_M_max') + self.alpha_opt = Symbol('alpha_opt') + self.beta = Symbol('beta') + + def test_with_defaults(self): + origin = Point('pO') + insertion = Point('pI') + insertion.set_pos(origin, dynamicsymbols('q')*ReferenceFrame('N').x) + pathway = LinearPathway(origin, insertion) + activation = FirstOrderActivationDeGroote2016('name') + l_T_slack = Symbol('l_T_slack') + F_M_max = Symbol('F_M_max') + l_M_opt = Symbol('l_M_opt') + v_M_max = Float('10.0') + alpha_opt = Float('0.0') + beta = Float('0.1') + instance = MusculotendonDeGroote2016.with_defaults( + 'name', + pathway, + activation, + musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON, + tendon_slack_length=l_T_slack, + peak_isometric_force=F_M_max, + optimal_fiber_length=l_M_opt, + ) + assert instance.tendon_slack_length == l_T_slack + assert instance.peak_isometric_force == F_M_max + assert instance.optimal_fiber_length == l_M_opt + assert instance.maximal_fiber_velocity == v_M_max + assert instance.optimal_pennation_angle == alpha_opt + assert instance.fiber_damping_coefficient == beta + + @pytest.mark.parametrize( + 'l_T_slack, expected', + [ + (None, Symbol('l_T_slack_name')), + (Symbol('l_T_slack'), Symbol('l_T_slack')), + (Rational(1, 2), Rational(1, 2)), + (Float('0.5'), Float('0.5')), + ], + ) + def test_tendon_slack_length(self, l_T_slack, expected): + instance = MusculotendonDeGroote2016( + self.name, + self.pathway, + self.activation, + musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON, + tendon_slack_length=l_T_slack, + peak_isometric_force=self.F_M_max, + optimal_fiber_length=self.l_M_opt, + maximal_fiber_velocity=self.v_M_max, + optimal_pennation_angle=self.alpha_opt, + fiber_damping_coefficient=self.beta, + ) + assert instance.l_T_slack == expected + assert instance.tendon_slack_length == expected + + @pytest.mark.parametrize( + 'F_M_max, expected', + [ + (None, Symbol('F_M_max_name')), + (Symbol('F_M_max'), Symbol('F_M_max')), + (Integer(1000), Integer(1000)), + (Float('1000.0'), Float('1000.0')), + ], + ) + def test_peak_isometric_force(self, F_M_max, expected): + instance = MusculotendonDeGroote2016( + self.name, + self.pathway, + self.activation, + musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON, + tendon_slack_length=self.l_T_slack, + peak_isometric_force=F_M_max, + optimal_fiber_length=self.l_M_opt, + maximal_fiber_velocity=self.v_M_max, + optimal_pennation_angle=self.alpha_opt, + fiber_damping_coefficient=self.beta, + ) + assert instance.F_M_max == expected + assert instance.peak_isometric_force == expected + + @pytest.mark.parametrize( + 'l_M_opt, expected', + [ + (None, Symbol('l_M_opt_name')), + (Symbol('l_M_opt'), Symbol('l_M_opt')), + (Rational(1, 2), Rational(1, 2)), + (Float('0.5'), Float('0.5')), + ], + ) + def test_optimal_fiber_length(self, l_M_opt, expected): + instance = MusculotendonDeGroote2016( + self.name, + self.pathway, + self.activation, + musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON, + tendon_slack_length=self.l_T_slack, + peak_isometric_force=self.F_M_max, + optimal_fiber_length=l_M_opt, + maximal_fiber_velocity=self.v_M_max, + optimal_pennation_angle=self.alpha_opt, + fiber_damping_coefficient=self.beta, + ) + assert instance.l_M_opt == expected + assert instance.optimal_fiber_length == expected + + @pytest.mark.parametrize( + 'v_M_max, expected', + [ + (None, Symbol('v_M_max_name')), + (Symbol('v_M_max'), Symbol('v_M_max')), + (Integer(10), Integer(10)), + (Float('10.0'), Float('10.0')), + ], + ) + def test_maximal_fiber_velocity(self, v_M_max, expected): + instance = MusculotendonDeGroote2016( + self.name, + self.pathway, + self.activation, + musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON, + tendon_slack_length=self.l_T_slack, + peak_isometric_force=self.F_M_max, + optimal_fiber_length=self.l_M_opt, + maximal_fiber_velocity=v_M_max, + optimal_pennation_angle=self.alpha_opt, + fiber_damping_coefficient=self.beta, + ) + assert instance.v_M_max == expected + assert instance.maximal_fiber_velocity == expected + + @pytest.mark.parametrize( + 'alpha_opt, expected', + [ + (None, Symbol('alpha_opt_name')), + (Symbol('alpha_opt'), Symbol('alpha_opt')), + (Integer(0), Integer(0)), + (Float('0.1'), Float('0.1')), + ], + ) + def test_optimal_pennation_angle(self, alpha_opt, expected): + instance = MusculotendonDeGroote2016( + self.name, + self.pathway, + self.activation, + musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON, + tendon_slack_length=self.l_T_slack, + peak_isometric_force=self.F_M_max, + optimal_fiber_length=self.l_M_opt, + maximal_fiber_velocity=self.v_M_max, + optimal_pennation_angle=alpha_opt, + fiber_damping_coefficient=self.beta, + ) + assert instance.alpha_opt == expected + assert instance.optimal_pennation_angle == expected + + @pytest.mark.parametrize( + 'beta, expected', + [ + (None, Symbol('beta_name')), + (Symbol('beta'), Symbol('beta')), + (Integer(0), Integer(0)), + (Rational(1, 10), Rational(1, 10)), + (Float('0.1'), Float('0.1')), + ], + ) + def test_fiber_damping_coefficient(self, beta, expected): + instance = MusculotendonDeGroote2016( + self.name, + self.pathway, + self.activation, + musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON, + tendon_slack_length=self.l_T_slack, + peak_isometric_force=self.F_M_max, + optimal_fiber_length=self.l_M_opt, + maximal_fiber_velocity=self.v_M_max, + optimal_pennation_angle=self.alpha_opt, + fiber_damping_coefficient=beta, + ) + assert instance.beta == expected + assert instance.fiber_damping_coefficient == expected + + def test_excitation(self): + instance = MusculotendonDeGroote2016( + self.name, + self.pathway, + self.activation, + ) + assert hasattr(instance, 'e') + assert hasattr(instance, 'excitation') + e_expected = dynamicsymbols('e_name') + assert instance.e == e_expected + assert instance.excitation == e_expected + assert instance.e is instance.excitation + + def test_excitation_is_immutable(self): + instance = MusculotendonDeGroote2016( + self.name, + self.pathway, + self.activation, + ) + with pytest.raises(AttributeError): + instance.e = None + with pytest.raises(AttributeError): + instance.excitation = None + + def test_activation(self): + instance = MusculotendonDeGroote2016( + self.name, + self.pathway, + self.activation, + ) + assert hasattr(instance, 'a') + assert hasattr(instance, 'activation') + a_expected = dynamicsymbols('a_name') + assert instance.a == a_expected + assert instance.activation == a_expected + + def test_activation_is_immutable(self): + instance = MusculotendonDeGroote2016( + self.name, + self.pathway, + self.activation, + ) + with pytest.raises(AttributeError): + instance.a = None + with pytest.raises(AttributeError): + instance.activation = None + + def test_repr(self): + instance = MusculotendonDeGroote2016( + self.name, + self.pathway, + self.activation, + musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON, + tendon_slack_length=self.l_T_slack, + peak_isometric_force=self.F_M_max, + optimal_fiber_length=self.l_M_opt, + maximal_fiber_velocity=self.v_M_max, + optimal_pennation_angle=self.alpha_opt, + fiber_damping_coefficient=self.beta, + ) + expected = ( + 'MusculotendonDeGroote2016(\'name\', ' + 'pathway=LinearPathway(pO, pI), ' + 'activation_dynamics=FirstOrderActivationDeGroote2016(\'name\', ' + 'activation_time_constant=tau_a_name, ' + 'deactivation_time_constant=tau_d_name, ' + 'smoothing_rate=b_name), ' + 'musculotendon_dynamics=0, ' + 'tendon_slack_length=l_T_slack, ' + 'peak_isometric_force=F_M_max, ' + 'optimal_fiber_length=l_M_opt, ' + 'maximal_fiber_velocity=v_M_max, ' + 'optimal_pennation_angle=alpha_opt, ' + 'fiber_damping_coefficient=beta)' + ) + assert repr(instance) == expected diff --git a/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..781429110cab760f8990961c6536e7267a2a371a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/__init__.py @@ -0,0 +1,10 @@ +__all__ = ['Beam', + 'Truss', + 'Cable', + 'Arch' + ] + +from .beam import Beam +from .truss import Truss +from .cable import Cable +from .arch import Arch diff --git a/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/arch.py b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/arch.py new file mode 100644 index 0000000000000000000000000000000000000000..31e2b41e841638f6a8002da1a7c843a9f5b35555 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/arch.py @@ -0,0 +1,1025 @@ +""" +This module can be used to solve probelsm related to 2D parabolic arches +""" +from sympy.core.sympify import sympify +from sympy.core.symbol import Symbol,symbols +from sympy import diff, sqrt, cos , sin, atan, rad, Min +from sympy.core.relational import Eq +from sympy.solvers.solvers import solve +from sympy.functions import Piecewise +from sympy.plotting import plot +from sympy import limit +from sympy.utilities.decorator import doctest_depends_on +from sympy.external.importtools import import_module + +numpy = import_module('numpy', import_kwargs={'fromlist':['arange']}) + +class Arch: + """ + This class is used to solve problems related to a three hinged arch(determinate) structure.\n + An arch is a curved vertical structure spanning an open space underneath it.\n + Arches can be used to reduce the bending moments in long-span structures.\n + + Arches are used in structural engineering(over windows, door and even bridges)\n + because they can support a very large mass placed on top of them. + + Example + ======== + >>> from sympy.physics.continuum_mechanics.arch import Arch + >>> a = Arch((0,0),(10,0),crown_x=5,crown_y=5) + >>> a.get_shape_eqn + 5 - (x - 5)**2/5 + + >>> from sympy.physics.continuum_mechanics.arch import Arch + >>> a = Arch((0,0),(10,1),crown_x=6) + >>> a.get_shape_eqn + 9/5 - (x - 6)**2/20 + """ + def __init__(self,left_support,right_support,**kwargs): + self._shape_eqn = None + self._left_support = (sympify(left_support[0]),sympify(left_support[1])) + self._right_support = (sympify(right_support[0]),sympify(right_support[1])) + self._crown_x = None + self._crown_y = None + if 'crown_x' in kwargs: + self._crown_x = sympify(kwargs['crown_x']) + if 'crown_y' in kwargs: + self._crown_y = sympify(kwargs['crown_y']) + self._shape_eqn = self.get_shape_eqn + self._conc_loads = {} + self._distributed_loads = {} + self._loads = {'concentrated': self._conc_loads, 'distributed':self._distributed_loads} + self._loads_applied = {} + self._supports = {'left':'hinge', 'right':'hinge'} + self._member = None + self._member_force = None + self._reaction_force = {Symbol('R_A_x'):0, Symbol('R_A_y'):0, Symbol('R_B_x'):0, Symbol('R_B_y'):0} + self._points_disc_x = set() + self._points_disc_y = set() + self._moment_x = {} + self._moment_y = {} + self._load_x = {} + self._load_y = {} + self._moment_x_func = Piecewise((0,True)) + self._moment_y_func = Piecewise((0,True)) + self._load_x_func = Piecewise((0,True)) + self._load_y_func = Piecewise((0,True)) + self._bending_moment = None + self._shear_force = None + self._axial_force = None + # self._crown = (sympify(crown[0]),sympify(crown[1])) + + @property + def get_shape_eqn(self): + "returns the equation of the shape of arch developed" + if self._shape_eqn: + return self._shape_eqn + + x,y,c = symbols('x y c') + a = Symbol('a',positive=False) + if self._crown_x and self._crown_y: + x0 = self._crown_x + y0 = self._crown_y + parabola_eqn = a*(x-x0)**2 + y0 - y + eq1 = parabola_eqn.subs({x:self._left_support[0], y:self._left_support[1]}) + solution = solve((eq1),(a)) + parabola_eqn = solution[0]*(x-x0)**2 + y0 + if(parabola_eqn.subs({x:self._right_support[0]}) != self._right_support[1]): + raise ValueError("provided coordinates of crown and supports are not consistent with parabolic arch") + + elif self._crown_x: + x0 = self._crown_x + parabola_eqn = a*(x-x0)**2 + c - y + eq1 = parabola_eqn.subs({x:self._left_support[0], y:self._left_support[1]}) + eq2 = parabola_eqn.subs({x:self._right_support[0], y:self._right_support[1]}) + solution = solve((eq1,eq2),(a,c)) + if len(solution) <2 or solution[a] == 0: + raise ValueError("parabolic arch cannot be constructed with the provided coordinates, try providing crown_y") + parabola_eqn = solution[a]*(x-x0)**2+ solution[c] + self._crown_y = solution[c] + + else: + raise KeyError("please provide crown_x to construct arch") + + return parabola_eqn + + @property + def get_loads(self): + """ + return the position of the applied load and angle (for concentrated loads) + """ + return self._loads + + @property + def supports(self): + """ + Returns the type of support + """ + return self._supports + + @property + def left_support(self): + """ + Returns the position of the left support. + """ + return self._left_support + + @property + def right_support(self): + """ + Returns the position of the right support. + """ + return self._right_support + + @property + def reaction_force(self): + """ + return the reaction forces generated + """ + return self._reaction_force + + def apply_load(self,order,label,start,mag,end=None,angle=None): + """ + This method adds load to the Arch. + + Parameters + ========== + + order : Integer + Order of the applied load. + + - For point/concentrated loads, order = -1 + - For distributed load, order = 0 + + label : String or Symbol + The label of the load + - should not use 'A' or 'B' as it is used for supports. + + start : Float + + - For concentrated/point loads, start is the x coordinate + - For distributed loads, start is the starting position of distributed load + + mag : Sympifyable + Magnitude of the applied load. Must be positive + + end : Float + Required for distributed loads + + - For concentrated/point load , end is None(may not be given) + - For distributed loads, end is the end position of distributed load + + angle: Sympifyable + The angle in degrees, the load vector makes with the horizontal + in the counter-clockwise direction. + + Examples + ======== + For applying distributed load + + >>> from sympy.physics.continuum_mechanics.arch import Arch + >>> a = Arch((0,0),(10,0),crown_x=5,crown_y=5) + >>> a.apply_load(0,'C',start=3,end=5,mag=-10) + + For applying point/concentrated_loads + + >>> from sympy.physics.continuum_mechanics.arch import Arch + >>> a = Arch((0,0),(10,0),crown_x=5,crown_y=5) + >>> a.apply_load(-1,'C',start=2,mag=15,angle=45) + + """ + y = Symbol('y') + x = Symbol('x') + x0 = Symbol('x0') + # y0 = Symbol('y0') + order= sympify(order) + mag = sympify(mag) + angle = sympify(angle) + + if label in self._loads_applied: + raise ValueError("load with the given label already exists") + + if label in ['A','B']: + raise ValueError("cannot use the given label, reserved for supports") + + if order == 0: + if end is None or end>> from sympy.physics.continuum_mechanics.arch import Arch + >>> a = Arch((0,0),(10,0),crown_x=5,crown_y=5) + >>> a.apply_load(0,'C',start=3,end=5,mag=-10) + >>> a.remove_load('C') + removed load C: {'start': 3, 'end': 5, 'f_y': -10} + """ + y = Symbol('y') + x = Symbol('x') + x0 = Symbol('x0') + + if label in self._distributed_loads : + + self._loads_applied.pop(label) + start = self._distributed_loads[label]['start'] + end = self._distributed_loads[label]['end'] + mag = self._distributed_loads[label]['f_y'] + self._points_disc_y.remove(start) + self._load_y[start] -= mag*(Min(x,end)-start) + self._moment_y[start] += mag*(Min(x,end)-start)*(x0-(start+(Min(x,end)))/2) + val = self._distributed_loads.pop(label) + print(f"removed load {label}: {val}") + + elif label in self._conc_loads : + + self._loads_applied.pop(label) + start = self._conc_loads[label]['x'] + self._points_disc_x.remove(start) + self._points_disc_y.remove(start) + self._moment_y[start] += self._conc_loads[label]['f_y']*(x0-start) + self._moment_x[start] -= self._conc_loads[label]['f_x']*(y-self._conc_loads[label]['y']) + self._load_x[start] -= self._conc_loads[label]['f_x'] + self._load_y[start] -= self._conc_loads[label]['f_y'] + val = self._conc_loads.pop(label) + print(f"removed load {label}: {val}") + + else : + raise ValueError("label not found") + + def change_support_position(self, left_support=None, right_support=None): + """ + Change position of supports. + If not provided , defaults to the old value. + Parameters + ========== + + left_support: tuple (x, y) + x: float + x-coordinate value of the left_support + + y: float + y-coordinate value of the left_support + + right_support: tuple (x, y) + x: float + x-coordinate value of the right_support + + y: float + y-coordinate value of the right_support + """ + if left_support is not None: + self._left_support = (left_support[0],left_support[1]) + + if right_support is not None: + self._right_support = (right_support[0],right_support[1]) + + self._shape_eqn = None + self._shape_eqn = self.get_shape_eqn + + def change_crown_position(self,crown_x=None,crown_y=None): + """ + Change the position of the crown/hinge of the arch + + Parameters + ========== + + crown_x: Float + The x coordinate of the position of the hinge + - if not provided, defaults to old value + + crown_y: Float + The y coordinate of the position of the hinge + - if not provided defaults to None + """ + self._crown_x = crown_x + self._crown_y = crown_y + self._shape_eqn = None + self._shape_eqn = self.get_shape_eqn + + def change_support_type(self,left_support=None,right_support=None): + """ + Add the type for support at each end. + Can use roller or hinge support at each end. + + Parameters + ========== + + left_support, right_support : string + Type of support at respective end + + - For roller support , left_support/right_support = "roller" + - For hinged support, left_support/right_support = "hinge" + - defaults to hinge if value not provided + + Examples + ======== + + For applying roller support at right end + + >>> from sympy.physics.continuum_mechanics.arch import Arch + >>> a = Arch((0,0),(10,0),crown_x=5,crown_y=5) + >>> a.change_support_type(right_support="roller") + + """ + support_types = ['roller','hinge'] + if left_support: + if left_support not in support_types: + raise ValueError("supports must only be roller or hinge") + + self._supports['left'] = left_support + + if right_support: + if right_support not in support_types: + raise ValueError("supports must only be roller or hinge") + + self._supports['right'] = right_support + + def add_member(self,y): + """ + This method adds a member/rod at a particular height y. + A rod is used for stability of the structure in case of a roller support. + """ + if y>self._crown_y or y>> from sympy.physics.continuum_mechanics.arch import Arch + >>> a = Arch((0,0),(10,0),crown_x=5,crown_y=5) + >>> a.apply_load(0,'C',start=3,end=5,mag=-10) + >>> a.solve() + >>> a.reaction_force + {R_A_x: 8, R_A_y: 12, R_B_x: -8, R_B_y: 8} + + >>> from sympy import Symbol + >>> t = Symbol('t') + >>> from sympy.physics.continuum_mechanics.arch import Arch + >>> a = Arch((0,0),(16,0),crown_x=8,crown_y=5) + >>> a.apply_load(0,'C',start=3,end=5,mag=t) + >>> a.solve() + >>> a.reaction_force + {R_A_x: -4*t/5, R_A_y: -3*t/2, R_B_x: 4*t/5, R_B_y: -t/2} + + >>> a.bending_moment_at(4) + -5*t/2 + """ + y = Symbol('y') + x = Symbol('x') + x0 = Symbol('x0') + + discontinuity_points_x = sorted(self._points_disc_x) + discontinuity_points_y = sorted(self._points_disc_y) + + self._moment_x_func = Piecewise((0,True)) + self._moment_y_func = Piecewise((0,True)) + + self._load_x_func = Piecewise((0,True)) + self._load_y_func = Piecewise((0,True)) + + accumulated_x_moment = 0 + accumulated_y_moment = 0 + + accumulated_x_load = 0 + accumulated_y_load = 0 + + for point in discontinuity_points_x: + cond = (x >= point) + accumulated_x_load += self._load_x[point] + accumulated_x_moment += self._moment_x[point] + self._load_x_func = Piecewise((accumulated_x_load,cond),(self._load_x_func,True)) + self._moment_x_func = Piecewise((accumulated_x_moment,cond),(self._moment_x_func,True)) + + for point in discontinuity_points_y: + cond = (x >= point) + accumulated_y_moment += self._moment_y[point] + accumulated_y_load += self._load_y[point] + self._load_y_func = Piecewise((accumulated_y_load,cond),(self._load_y_func,True)) + self._moment_y_func = Piecewise((accumulated_y_moment,cond),(self._moment_y_func,True)) + + moment_A = self._moment_y_func.subs(x,self._right_support[0]).subs(x0,self._left_support[0]) +\ + self._moment_x_func.subs(x,self._right_support[0]).subs(y,self._left_support[1]) + + moment_hinge_left = self._moment_y_func.subs(x,self._crown_x).subs(x0,self._crown_x) +\ + self._moment_x_func.subs(x,self._crown_x).subs(y,self._crown_y) + + moment_hinge_right = self._moment_y_func.subs(x,self._right_support[0]).subs(x0,self._crown_x)- \ + self._moment_y_func.subs(x,self._crown_x).subs(x0,self._crown_x) +\ + self._moment_x_func.subs(x,self._right_support[0]).subs(y,self._crown_y) -\ + self._moment_x_func.subs(x,self._crown_x).subs(y,self._crown_y) + + net_x = self._load_x_func.subs(x,self._right_support[0]) + net_y = self._load_y_func.subs(x,self._right_support[0]) + + if (self._supports['left']=='roller' or self._supports['right']=='roller') and not self._member: + print("member must be added if any of the supports is roller") + return + + R_A_x, R_A_y, R_B_x, R_B_y, T = symbols('R_A_x R_A_y R_B_x R_B_y T') + + if self._supports['left'] == 'roller' and self._supports['right'] == 'roller': + + if self._member[2]>=max(self._left_support[1],self._right_support[1]): + + if net_x!=0: + raise ValueError("net force in x direction not possible under the specified conditions") + + else: + eq1 = Eq(R_A_x ,0) + eq2 = Eq(R_B_x, 0) + eq3 = Eq(R_A_y + R_B_y + net_y,0) + + eq4 = Eq(R_B_y*(self._right_support[0]-self._left_support[0])-\ + R_B_x*(self._right_support[1]-self._left_support[1])+moment_A,0) + + eq5 = Eq(moment_hinge_right + R_B_y*(self._right_support[0]-self._crown_x) +\ + T*(self._member[2]-self._crown_y),0) + solution = solve((eq1,eq2,eq3,eq4,eq5),(R_A_x,R_A_y,R_B_x,R_B_y,T)) + + elif self._member[2]>=self._left_support[1]: + eq1 = Eq(R_A_x ,0) + eq2 = Eq(R_B_x, 0) + eq3 = Eq(R_A_y + R_B_y + net_y,0) + eq4 = Eq(R_B_y*(self._right_support[0]-self._left_support[0])-\ + T*(self._member[2]-self._left_support[1])+moment_A,0) + eq5 = Eq(T+net_x,0) + solution = solve((eq1,eq2,eq3,eq4,eq5),(R_A_x,R_A_y,R_B_x,R_B_y,T)) + + elif self._member[2]>=self._right_support[1]: + eq1 = Eq(R_A_x ,0) + eq2 = Eq(R_B_x, 0) + eq3 = Eq(R_A_y + R_B_y + net_y,0) + eq4 = Eq(R_B_y*(self._right_support[0]-self._left_support[0])+\ + T*(self._member[2]-self._left_support[1])+moment_A,0) + eq5 = Eq(T-net_x,0) + solution = solve((eq1,eq2,eq3,eq4,eq5),(R_A_x,R_A_y,R_B_x,R_B_y,T)) + + elif self._supports['left'] == 'roller': + if self._member[2]>=max(self._left_support[1], self._right_support[1]): + eq1 = Eq(R_A_x ,0) + eq2 = Eq(R_B_x+net_x,0) + eq3 = Eq(R_A_y + R_B_y + net_y,0) + eq4 = Eq(R_B_y*(self._right_support[0]-self._left_support[0])-\ + R_B_x*(self._right_support[1]-self._left_support[1])+moment_A,0) + eq5 = Eq(moment_hinge_left + R_A_y*(self._left_support[0]-self._crown_x) -\ + T*(self._member[2]-self._crown_y),0) + solution = solve((eq1,eq2,eq3,eq4,eq5),(R_A_x,R_A_y,R_B_x,R_B_y,T)) + + elif self._member[2]>=self._left_support[1]: + eq1 = Eq(R_A_x ,0) + eq2 = Eq(R_B_x+ T +net_x,0) + eq3 = Eq(R_A_y + R_B_y + net_y,0) + eq4 = Eq(R_B_y*(self._right_support[0]-self._left_support[0])-\ + R_B_x*(self._right_support[1]-self._left_support[1])-\ + T*(self._member[2]-self._left_support[0])+moment_A,0) + eq5 = Eq(moment_hinge_left + R_A_y*(self._left_support[0]-self._crown_x)-\ + T*(self._member[2]-self._crown_y),0) + solution = solve((eq1,eq2,eq3,eq4,eq5),(R_A_x,R_A_y,R_B_x,R_B_y,T)) + + elif self._member[2]>=self._right_support[0]: + eq1 = Eq(R_A_x,0) + eq2 = Eq(R_B_x- T +net_x,0) + eq3 = Eq(R_A_y + R_B_y + net_y,0) + eq4 = Eq(moment_hinge_left+R_A_y*(self._left_support[0]-self._crown_x),0) + eq5 = Eq(moment_A+R_B_y*(self._right_support[0]-self._left_support[0])-\ + R_B_x*(self._right_support[1]-self._left_support[1])+\ + T*(self._member[2]-self._left_support[1]),0) + solution = solve((eq1,eq2,eq3,eq4,eq5),(R_A_x,R_A_y,R_B_x,R_B_y,T)) + + elif self._supports['right'] == 'roller': + if self._member[2]>=max(self._left_support[1], self._right_support[1]): + eq1 = Eq(R_B_x,0) + eq2 = Eq(R_A_x+net_x,0) + eq3 = Eq(R_A_y+R_B_y+net_y,0) + eq4 = Eq(moment_hinge_right+R_B_y*(self._right_support[0]-self._crown_x)+\ + T*(self._member[2]-self._crown_y),0) + eq5 = Eq(moment_A+R_B_y*(self._right_support[0]-self._left_support[0]),0) + solution = solve((eq1,eq2,eq3,eq4,eq5),(R_A_x,R_A_y,R_B_x,R_B_y,T)) + + elif self._member[2]>=self._left_support[1]: + eq1 = Eq(R_B_x,0) + eq2 = Eq(R_A_x+T+net_x,0) + eq3 = Eq(R_A_y+R_B_y+net_y,0) + eq4 = Eq(moment_hinge_right+R_B_y*(self._right_support[0]-self._crown_x),0) + eq5 = Eq(moment_A-T*(self._member[2]-self._left_support[1])+\ + R_B_y*(self._right_support[0]-self._left_support[0]),0) + solution = solve((eq1,eq2,eq3,eq4,eq5),(R_A_x,R_A_y,R_B_x,R_B_y,T)) + + elif self._member[2]>=self._right_support[1]: + eq1 = Eq(R_B_x,0) + eq2 = Eq(R_A_x-T+net_x,0) + eq3 = Eq(R_A_y+R_B_y+net_y,0) + eq4 = Eq(moment_hinge_right+R_B_y*(self._right_support[0]-self._crown_x)+\ + T*(self._member[2]-self._crown_y),0) + eq5 = Eq(moment_A+T*(self._member[2]-self._left_support[1])+\ + R_B_y*(self._right_support[0]-self._left_support[0])) + solution = solve((eq1,eq2,eq3,eq4,eq5),(R_A_x,R_A_y,R_B_x,R_B_y,T)) + else: + eq1 = Eq(R_A_x + R_B_x + net_x,0) + eq2 = Eq(R_A_y + R_B_y + net_y,0) + eq3 = Eq(R_B_y*(self._right_support[0]-self._left_support[0])-\ + R_B_x*(self._right_support[1]-self._left_support[1])+moment_A,0) + eq4 = Eq(moment_hinge_right + R_B_y*(self._right_support[0]-self._crown_x) -\ + R_B_x*(self._right_support[1]-self._crown_y),0) + solution = solve((eq1,eq2,eq3,eq4),(R_A_x,R_A_y,R_B_x,R_B_y)) + + for symb in self._reaction_force: + self._reaction_force[symb] = solution[symb] + + self._bending_moment = - (self._moment_x_func.subs(x,x0) + self._moment_y_func.subs(x,x0) -\ + solution[R_A_y]*(x0-self._left_support[0]) +\ + solution[R_A_x]*(self._shape_eqn.subs({x:x0})-self._left_support[1])) + + angle = atan(diff(self._shape_eqn,x)) + + fx = (self._load_x_func+solution[R_A_x]) + fy = (self._load_y_func+solution[R_A_y]) + + axial_force = fx*cos(angle) + fy*sin(angle) + shear_force = -fx*sin(angle) + fy*cos(angle) + + self._axial_force = axial_force + self._shear_force = shear_force + + @doctest_depends_on(modules=('numpy',)) + def draw(self): + """ + This method returns a plot object containing the diagram of the specified arch along with the supports + and forces applied to the structure. + + Examples + ======== + + >>> from sympy import Symbol + >>> t = Symbol('t') + >>> from sympy.physics.continuum_mechanics.arch import Arch + >>> a = Arch((0,0),(40,0),crown_x=20,crown_y=12) + >>> a.apply_load(-1,'C',8,150,angle=270) + >>> a.apply_load(0,'D',start=20,end=40,mag=-4) + >>> a.apply_load(-1,'E',10,t,angle=300) + >>> p = a.draw() + >>> p # doctest: +ELLIPSIS + Plot object containing: + [0]: cartesian line: 11.325 - 3*(x - 20)**2/100 for x over (0.0, 40.0) + [1]: cartesian line: 12 - 3*(x - 20)**2/100 for x over (0.0, 40.0) + ... + >>> p.show() + + """ + x = Symbol('x') + markers = [] + annotations = self._draw_loads() + rectangles = [] + supports = self._draw_supports() + markers+=supports + + xmax = self._right_support[0] + xmin = self._left_support[0] + ymin = min(self._left_support[1],self._right_support[1]) + ymax = self._crown_y + + lim = max(xmax*1.1-xmin*0.8+1, ymax*1.1-ymin*0.8+1) + + rectangles = self._draw_rectangles() + + filler = self._draw_filler() + rectangles+=filler + + if self._member is not None: + if(self._member[2]>=self._right_support[1]): + markers.append( + { + 'args':[[self._member[1]+0.005*lim],[self._member[2]]], + 'marker':'o', + 'markersize': 4, + 'color': 'white', + 'markerfacecolor':'none' + } + ) + + if(self._member[2]>=self._left_support[1]): + markers.append( + { + 'args':[[self._member[0]-0.005*lim],[self._member[2]]], + 'marker':'o', + 'markersize': 4, + 'color': 'white', + 'markerfacecolor':'none' + } + ) + + + + markers.append({ + 'args':[[self._crown_x],[self._crown_y-0.005*lim]], + 'marker':'o', + 'markersize': 5, + 'color':'white', + 'markerfacecolor':'none', + }) + + if lim==xmax*1.1-xmin*0.8+1: + + sing_plot = plot(self._shape_eqn-0.015*lim, + self._shape_eqn, + (x, self._left_support[0], self._right_support[0]), + markers=markers, + show=False, + annotations=annotations, + rectangles = rectangles, + xlim=(xmin-0.05*lim, xmax*1.1), + ylim=(xmin-0.05*lim, xmax*1.1), + axis=False, + line_color='brown') + + else: + sing_plot = plot(self._shape_eqn-0.015*lim, + self._shape_eqn, + (x, self._left_support[0], self._right_support[0]), + markers=markers, + show=False, + annotations=annotations, + rectangles = rectangles, + xlim=(ymin-0.05*lim, ymax*1.1), + ylim=(ymin-0.05*lim, ymax*1.1), + axis=False, + line_color='brown') + + return sing_plot + + + def _draw_supports(self): + support_markers = [] + + xmax = self._right_support[0] + xmin = self._left_support[0] + ymin = min(self._left_support[1],self._right_support[1]) + ymax = self._crown_y + + if abs(1.1*xmax-0.8*xmin)>abs(1.1*ymax-0.8*ymin): + max_diff = 1.1*xmax-0.8*xmin + else: + max_diff = 1.1*ymax-0.8*ymin + + if self._supports['left']=='roller': + support_markers.append( + { + 'args':[ + [self._left_support[0]], + [self._left_support[1]-0.02*max_diff] + ], + 'marker':'o', + 'markersize':11, + 'color':'black', + 'markerfacecolor':'none' + } + ) + else: + support_markers.append( + { + 'args':[ + [self._left_support[0]], + [self._left_support[1]-0.007*max_diff] + ], + 'marker':6, + 'markersize':15, + 'color':'black', + 'markerfacecolor':'none' + } + ) + + if self._supports['right']=='roller': + support_markers.append( + { + 'args':[ + [self._right_support[0]], + [self._right_support[1]-0.02*max_diff] + ], + 'marker':'o', + 'markersize':11, + 'color':'black', + 'markerfacecolor':'none' + } + ) + else: + support_markers.append( + { + 'args':[ + [self._right_support[0]], + [self._right_support[1]-0.007*max_diff] + ], + 'marker':6, + 'markersize':15, + 'color':'black', + 'markerfacecolor':'none' + } + ) + + support_markers.append( + { + 'args':[ + [self._right_support[0]], + [self._right_support[1]-0.036*max_diff] + ], + 'marker':'_', + 'markersize':15, + 'color':'black', + 'markerfacecolor':'none' + } + ) + + support_markers.append( + { + 'args':[ + [self._left_support[0]], + [self._left_support[1]-0.036*max_diff] + ], + 'marker':'_', + 'markersize':15, + 'color':'black', + 'markerfacecolor':'none' + } + ) + + return support_markers + + def _draw_rectangles(self): + member = [] + + xmax = self._right_support[0] + xmin = self._left_support[0] + ymin = min(self._left_support[1],self._right_support[1]) + ymax = self._crown_y + + if abs(1.1*xmax-0.8*xmin)>abs(1.1*ymax-0.8*ymin): + max_diff = 1.1*xmax-0.8*xmin + else: + max_diff = 1.1*ymax-0.8*ymin + + if self._member is not None: + if self._member[2]>= max(self._left_support[1],self._right_support[1]): + member.append( + { + 'xy':(self._member[0],self._member[2]-0.005*max_diff), + 'width':self._member[1]-self._member[0], + 'height': 0.01*max_diff, + 'angle': 0, + 'color':'brown', + } + ) + + elif self._member[2]>=self._left_support[1]: + member.append( + { + 'xy':(self._member[0],self._member[2]-0.005*max_diff), + 'width':self._right_support[0]-self._member[0], + 'height': 0.01*max_diff, + 'angle': 0, + 'color':'brown', + } + ) + + else: + member.append( + { + 'xy':(self._member[1],self._member[2]-0.005*max_diff), + 'width':abs(self._left_support[0]-self._member[1]), + 'height': 0.01*max_diff, + 'angle': 180, + 'color':'brown', + } + ) + + if self._distributed_loads: + for loads in self._distributed_loads: + + start = self._distributed_loads[loads]['start'] + end = self._distributed_loads[loads]['end'] + + member.append( + { + 'xy':(start,self._crown_y+max_diff*0.15), + 'width': (end-start), + 'height': max_diff*0.01, + 'color': 'orange' + } + ) + + + return member + + def _draw_loads(self): + load_annotations = [] + + xmax = self._right_support[0] + xmin = self._left_support[0] + ymin = min(self._left_support[1],self._right_support[1]) + ymax = self._crown_y + + if abs(1.1*xmax-0.8*xmin)>abs(1.1*ymax-0.8*ymin): + max_diff = 1.1*xmax-0.8*xmin + else: + max_diff = 1.1*ymax-0.8*ymin + + for load in self._conc_loads: + x = self._conc_loads[load]['x'] + y = self._conc_loads[load]['y'] + angle = self._conc_loads[load]['angle'] + mag = self._conc_loads[load]['mag'] + load_annotations.append( + { + 'text':'', + 'xy':( + x+cos(rad(angle))*max_diff*0.08, + y+sin(rad(angle))*max_diff*0.08 + ), + 'xytext':(x,y), + 'fontsize':10, + 'fontweight': 'bold', + 'arrowprops':{'width':1.5, 'headlength':5, 'headwidth':5, 'facecolor':'blue','edgecolor':'blue'} + } + ) + load_annotations.append( + { + 'text':f'{load}: {mag} N', + 'fontsize':10, + 'fontweight': 'bold', + 'xy': (x+cos(rad(angle))*max_diff*0.12,y+sin(rad(angle))*max_diff*0.12) + } + ) + + for load in self._distributed_loads: + start = self._distributed_loads[load]['start'] + end = self._distributed_loads[load]['end'] + mag = self._distributed_loads[load]['f_y'] + x_points = numpy.arange(start,end,(end-start)/(max_diff*0.25)) + x_points = numpy.append(x_points,end) + for point in x_points: + if(mag<0): + load_annotations.append( + { + 'text':'', + 'xy':(point,self._crown_y+max_diff*0.05), + 'xytext': (point,self._crown_y+max_diff*0.15), + 'arrowprops':{'width':1.5, 'headlength':5, 'headwidth':5, 'facecolor':'orange','edgecolor':'orange'} + } + ) + else: + load_annotations.append( + { + 'text':'', + 'xy':(point,self._crown_y+max_diff*0.2), + 'xytext': (point,self._crown_y+max_diff*0.15), + 'arrowprops':{'width':1.5, 'headlength':5, 'headwidth':5, 'facecolor':'orange','edgecolor':'orange'} + } + ) + if(mag<0): + load_annotations.append( + { + 'text':f'{load}: {abs(mag)} N/m', + 'fontsize':10, + 'fontweight': 'bold', + 'xy':((start+end)/2,self._crown_y+max_diff*0.175) + } + ) + else: + load_annotations.append( + { + 'text':f'{load}: {abs(mag)} N/m', + 'fontsize':10, + 'fontweight': 'bold', + 'xy':((start+end)/2,self._crown_y+max_diff*0.125) + } + ) + return load_annotations + + def _draw_filler(self): + x = Symbol('x') + filler = [] + xmax = self._right_support[0] + xmin = self._left_support[0] + ymin = min(self._left_support[1],self._right_support[1]) + ymax = self._crown_y + + if abs(1.1*xmax-0.8*xmin)>abs(1.1*ymax-0.8*ymin): + max_diff = 1.1*xmax-0.8*xmin + else: + max_diff = 1.1*ymax-0.8*ymin + + x_points = numpy.arange(self._left_support[0],self._right_support[0],(self._right_support[0]-self._left_support[0])/(max_diff*max_diff)) + + for point in x_points: + filler.append( + { + 'xy':(point,self._shape_eqn.subs(x,point)-max_diff*0.015), + 'width': (self._right_support[0]-self._left_support[0])/(max_diff*max_diff), + 'height': max_diff*0.015, + 'color': 'brown' + } + ) + + return filler diff --git a/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/beam.py b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/beam.py new file mode 100644 index 0000000000000000000000000000000000000000..dfdfc6d3594da6de44c7c42def3e3f5539cb988e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/beam.py @@ -0,0 +1,3903 @@ +""" +This module can be used to solve 2D beam bending problems with +singularity functions in mechanics. +""" + +from sympy.core import S, Symbol, diff, symbols +from sympy.core.add import Add +from sympy.core.expr import Expr +from sympy.core.function import (Derivative, Function) +from sympy.core.mul import Mul +from sympy.core.relational import Eq +from sympy.core.sympify import sympify +from sympy.solvers import linsolve +from sympy.solvers.ode.ode import dsolve +from sympy.solvers.solvers import solve +from sympy.printing import sstr +from sympy.functions import SingularityFunction, Piecewise, factorial +from sympy.integrals import integrate +from sympy.series import limit +from sympy.plotting import plot, PlotGrid +from sympy.geometry.entity import GeometryEntity +from sympy.external import import_module +from sympy.sets.sets import Interval +from sympy.utilities.lambdify import lambdify +from sympy.utilities.decorator import doctest_depends_on +from sympy.utilities.iterables import iterable +import warnings + + +__doctest_requires__ = { + ('Beam.draw', + 'Beam.plot_bending_moment', + 'Beam.plot_deflection', + 'Beam.plot_ild_moment', + 'Beam.plot_ild_shear', + 'Beam.plot_shear_force', + 'Beam.plot_shear_stress', + 'Beam.plot_slope'): ['matplotlib'], +} + + +numpy = import_module('numpy', import_kwargs={'fromlist':['arange']}) + + +class Beam: + """ + A Beam is a structural element that is capable of withstanding load + primarily by resisting against bending. Beams are characterized by + their cross sectional profile(Second moment of area), their length + and their material. + + .. note:: + A consistent sign convention must be used while solving a beam + bending problem; the results will + automatically follow the chosen sign convention. However, the + chosen sign convention must respect the rule that, on the positive + side of beam's axis (in respect to current section), a loading force + giving positive shear yields a negative moment, as below (the + curved arrow shows the positive moment and rotation): + + .. image:: allowed-sign-conventions.png + + Examples + ======== + There is a beam of length 4 meters. A constant distributed load of 6 N/m + is applied from half of the beam till the end. There are two simple supports + below the beam, one at the starting point and another at the ending point + of the beam. The deflection of the beam at the end is restricted. + + Using the sign convention of downwards forces being positive. + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols, Piecewise + >>> E, I = symbols('E, I') + >>> R1, R2 = symbols('R1, R2') + >>> b = Beam(4, E, I) + >>> b.apply_load(R1, 0, -1) + >>> b.apply_load(6, 2, 0) + >>> b.apply_load(R2, 4, -1) + >>> b.bc_deflection = [(0, 0), (4, 0)] + >>> b.boundary_conditions + {'bending_moment': [], 'deflection': [(0, 0), (4, 0)], 'shear_force': [], 'slope': []} + >>> b.load + R1*SingularityFunction(x, 0, -1) + R2*SingularityFunction(x, 4, -1) + 6*SingularityFunction(x, 2, 0) + >>> b.solve_for_reaction_loads(R1, R2) + >>> b.load + -3*SingularityFunction(x, 0, -1) + 6*SingularityFunction(x, 2, 0) - 9*SingularityFunction(x, 4, -1) + >>> b.shear_force() + 3*SingularityFunction(x, 0, 0) - 6*SingularityFunction(x, 2, 1) + 9*SingularityFunction(x, 4, 0) + >>> b.bending_moment() + 3*SingularityFunction(x, 0, 1) - 3*SingularityFunction(x, 2, 2) + 9*SingularityFunction(x, 4, 1) + >>> b.slope() + (-3*SingularityFunction(x, 0, 2)/2 + SingularityFunction(x, 2, 3) - 9*SingularityFunction(x, 4, 2)/2 + 7)/(E*I) + >>> b.deflection() + (7*x - SingularityFunction(x, 0, 3)/2 + SingularityFunction(x, 2, 4)/4 - 3*SingularityFunction(x, 4, 3)/2)/(E*I) + >>> b.deflection().rewrite(Piecewise) + (7*x - Piecewise((x**3, x >= 0), (0, True))/2 + - 3*Piecewise(((x - 4)**3, x >= 4), (0, True))/2 + + Piecewise(((x - 2)**4, x >= 2), (0, True))/4)/(E*I) + + Calculate the support reactions for a fully symbolic beam of length L. + There are two simple supports below the beam, one at the starting point + and another at the ending point of the beam. The deflection of the beam + at the end is restricted. The beam is loaded with: + + * a downward point load P1 applied at L/4 + * an upward point load P2 applied at L/8 + * a counterclockwise moment M1 applied at L/2 + * a clockwise moment M2 applied at 3*L/4 + * a distributed constant load q1, applied downward, starting from L/2 + up to 3*L/4 + * a distributed constant load q2, applied upward, starting from 3*L/4 + up to L + + No assumptions are needed for symbolic loads. However, defining a positive + length will help the algorithm to compute the solution. + + >>> E, I = symbols('E, I') + >>> L = symbols("L", positive=True) + >>> P1, P2, M1, M2, q1, q2 = symbols("P1, P2, M1, M2, q1, q2") + >>> R1, R2 = symbols('R1, R2') + >>> b = Beam(L, E, I) + >>> b.apply_load(R1, 0, -1) + >>> b.apply_load(R2, L, -1) + >>> b.apply_load(P1, L/4, -1) + >>> b.apply_load(-P2, L/8, -1) + >>> b.apply_load(M1, L/2, -2) + >>> b.apply_load(-M2, 3*L/4, -2) + >>> b.apply_load(q1, L/2, 0, 3*L/4) + >>> b.apply_load(-q2, 3*L/4, 0, L) + >>> b.bc_deflection = [(0, 0), (L, 0)] + >>> b.solve_for_reaction_loads(R1, R2) + >>> print(b.reaction_loads[R1]) + (-3*L**2*q1 + L**2*q2 - 24*L*P1 + 28*L*P2 - 32*M1 + 32*M2)/(32*L) + >>> print(b.reaction_loads[R2]) + (-5*L**2*q1 + 7*L**2*q2 - 8*L*P1 + 4*L*P2 + 32*M1 - 32*M2)/(32*L) + """ + + def __init__(self, length, elastic_modulus, second_moment, area=Symbol('A'), variable=Symbol('x'), base_char='C', ild_variable=Symbol('a')): + """Initializes the class. + + Parameters + ========== + + length : Sympifyable + A Symbol or value representing the Beam's length. + + elastic_modulus : Sympifyable + A SymPy expression representing the Beam's Modulus of Elasticity. + It is a measure of the stiffness of the Beam material. It can + also be a continuous function of position along the beam. + + second_moment : Sympifyable or Geometry object + Describes the cross-section of the beam via a SymPy expression + representing the Beam's second moment of area. It is a geometrical + property of an area which reflects how its points are distributed + with respect to its neutral axis. It can also be a continuous + function of position along the beam. Alternatively ``second_moment`` + can be a shape object such as a ``Polygon`` from the geometry module + representing the shape of the cross-section of the beam. In such cases, + it is assumed that the x-axis of the shape object is aligned with the + bending axis of the beam. The second moment of area will be computed + from the shape object internally. + + area : Symbol/float + Represents the cross-section area of beam + + variable : Symbol, optional + A Symbol object that will be used as the variable along the beam + while representing the load, shear, moment, slope and deflection + curve. By default, it is set to ``Symbol('x')``. + + base_char : String, optional + A String that will be used as base character to generate sequential + symbols for integration constants in cases where boundary conditions + are not sufficient to solve them. + + ild_variable : Symbol, optional + A Symbol object that will be used as the variable specifying the + location of the moving load in ILD calculations. By default, it + is set to ``Symbol('a')``. + """ + self.length = length + self.elastic_modulus = elastic_modulus + if isinstance(second_moment, GeometryEntity): + self.cross_section = second_moment + else: + self.cross_section = None + self.second_moment = second_moment + self.variable = variable + self.ild_variable = ild_variable + self._base_char = base_char + self._boundary_conditions = {'deflection': [], 'slope': [], 'bending_moment': [], 'shear_force': []} + self._load = 0 + self.area = area + self._applied_supports = [] + self._applied_rotation_hinges = [] + self._applied_sliding_hinges = [] + self._rotation_hinge_symbols = [] + self._sliding_hinge_symbols = [] + self._support_as_loads = [] + self._applied_loads = [] + self._reaction_loads = {} + self._ild_reactions = {} + self._ild_shear = 0 + self._ild_moment = 0 + # _original_load is a copy of _load equations with unsubstituted reaction + # forces. It is used for calculating reaction forces in case of I.L.D. + self._original_load = 0 + self._joined_beam = False + + def __str__(self): + shape_description = self._cross_section if self._cross_section else self._second_moment + str_sol = 'Beam({}, {}, {})'.format(sstr(self._length), sstr(self._elastic_modulus), sstr(shape_description)) + return str_sol + + @property + def reaction_loads(self): + """ Returns the reaction forces in a dictionary.""" + return self._reaction_loads + + @property + def rotation_jumps(self): + """ + Returns the value for the rotation jumps in rotation hinges in a dictionary. + The rotation jump is the rotation (in radian) in a rotation hinge. This can + be seen as a jump in the slope plot. + """ + return self._rotation_jumps + + @property + def deflection_jumps(self): + """ + Returns the deflection jumps in sliding hinges in a dictionary. + The deflection jump is the deflection (in meters) in a sliding hinge. + This can be seen as a jump in the deflection plot. + """ + return self._deflection_jumps + + @property + def ild_shear(self): + """ Returns the I.L.D. shear equation.""" + return self._ild_shear + + @property + def ild_reactions(self): + """ Returns the I.L.D. reaction forces in a dictionary.""" + return self._ild_reactions + + @property + def ild_rotation_jumps(self): + """ + Returns the I.L.D. rotation jumps in rotation hinges in a dictionary. + The rotation jump is the rotation (in radian) in a rotation hinge. This can + be seen as a jump in the slope plot. + """ + return self._ild_rotations_jumps + + @property + def ild_deflection_jumps(self): + """ + Returns the I.L.D. deflection jumps in sliding hinges in a dictionary. + The deflection jump is the deflection (in meters) in a sliding hinge. + This can be seen as a jump in the deflection plot. + """ + return self._ild_deflection_jumps + + @property + def ild_moment(self): + """ Returns the I.L.D. moment equation.""" + return self._ild_moment + + @property + def length(self): + """Length of the Beam.""" + return self._length + + @length.setter + def length(self, l): + self._length = sympify(l) + + @property + def area(self): + """Cross-sectional area of the Beam. """ + return self._area + + @area.setter + def area(self, a): + self._area = sympify(a) + + @property + def variable(self): + """ + A symbol that can be used as a variable along the length of the beam + while representing load distribution, shear force curve, bending + moment, slope curve and the deflection curve. By default, it is set + to ``Symbol('x')``, but this property is mutable. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> E, I, A = symbols('E, I, A') + >>> x, y, z = symbols('x, y, z') + >>> b = Beam(4, E, I) + >>> b.variable + x + >>> b.variable = y + >>> b.variable + y + >>> b = Beam(4, E, I, A, z) + >>> b.variable + z + """ + return self._variable + + @variable.setter + def variable(self, v): + if isinstance(v, Symbol): + self._variable = v + else: + raise TypeError("""The variable should be a Symbol object.""") + + @property + def elastic_modulus(self): + """Young's Modulus of the Beam. """ + return self._elastic_modulus + + @elastic_modulus.setter + def elastic_modulus(self, e): + self._elastic_modulus = sympify(e) + + @property + def second_moment(self): + """Second moment of area of the Beam. """ + return self._second_moment + + @second_moment.setter + def second_moment(self, i): + self._cross_section = None + if isinstance(i, GeometryEntity): + raise ValueError("To update cross-section geometry use `cross_section` attribute") + else: + self._second_moment = sympify(i) + + @property + def cross_section(self): + """Cross-section of the beam""" + return self._cross_section + + @cross_section.setter + def cross_section(self, s): + if s: + self._second_moment = s.second_moment_of_area()[0] + self._cross_section = s + + @property + def boundary_conditions(self): + """ + Returns a dictionary of boundary conditions applied on the beam. + The dictionary has three keywords namely moment, slope and deflection. + The value of each keyword is a list of tuple, where each tuple + contains location and value of a boundary condition in the format + (location, value). + + Examples + ======== + There is a beam of length 4 meters. The bending moment at 0 should be 4 + and at 4 it should be 0. The slope of the beam should be 1 at 0. The + deflection should be 2 at 0. + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> E, I = symbols('E, I') + >>> b = Beam(4, E, I) + >>> b.bc_deflection = [(0, 2)] + >>> b.bc_slope = [(0, 1)] + >>> b.boundary_conditions + {'bending_moment': [], 'deflection': [(0, 2)], 'shear_force': [], 'slope': [(0, 1)]} + + Here the deflection of the beam should be ``2`` at ``0``. + Similarly, the slope of the beam should be ``1`` at ``0``. + """ + return self._boundary_conditions + + @property + def bc_shear_force(self): + return self._boundary_conditions['shear_force'] + + @bc_shear_force.setter + def bc_shear_force(self, sf_bcs): + self._boundary_conditions['shear_force'] = sf_bcs + + @property + def bc_bending_moment(self): + return self._boundary_conditions['bending_moment'] + + @bc_bending_moment.setter + def bc_bending_moment(self, bm_bcs): + self._boundary_conditions['bending_moment'] = bm_bcs + + @property + def bc_slope(self): + return self._boundary_conditions['slope'] + + @bc_slope.setter + def bc_slope(self, s_bcs): + self._boundary_conditions['slope'] = s_bcs + + @property + def bc_deflection(self): + return self._boundary_conditions['deflection'] + + @bc_deflection.setter + def bc_deflection(self, d_bcs): + self._boundary_conditions['deflection'] = d_bcs + + def join(self, beam, via="fixed"): + """ + This method joins two beams to make a new composite beam system. + Passed Beam class instance is attached to the right end of calling + object. This method can be used to form beams having Discontinuous + values of Elastic modulus or Second moment. + + Parameters + ========== + beam : Beam class object + The Beam object which would be connected to the right of calling + object. + via : String + States the way two Beam object would get connected + - For axially fixed Beams, via="fixed" + - For Beams connected via rotation hinge, via="hinge" + + Examples + ======== + There is a cantilever beam of length 4 meters. For first 2 meters + its moment of inertia is `1.5*I` and `I` for the other end. + A pointload of magnitude 4 N is applied from the top at its free end. + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> E, I = symbols('E, I') + >>> R1, R2 = symbols('R1, R2') + >>> b1 = Beam(2, E, 1.5*I) + >>> b2 = Beam(2, E, I) + >>> b = b1.join(b2, "fixed") + >>> b.apply_load(20, 4, -1) + >>> b.apply_load(R1, 0, -1) + >>> b.apply_load(R2, 0, -2) + >>> b.bc_slope = [(0, 0)] + >>> b.bc_deflection = [(0, 0)] + >>> b.solve_for_reaction_loads(R1, R2) + >>> b.load + 80*SingularityFunction(x, 0, -2) - 20*SingularityFunction(x, 0, -1) + 20*SingularityFunction(x, 4, -1) + >>> b.slope() + (-((-80*SingularityFunction(x, 0, 1) + 10*SingularityFunction(x, 0, 2) - 10*SingularityFunction(x, 4, 2))/I + 120/I)/E + 80.0/(E*I))*SingularityFunction(x, 2, 0) + - 0.666666666666667*(-80*SingularityFunction(x, 0, 1) + 10*SingularityFunction(x, 0, 2) - 10*SingularityFunction(x, 4, 2))*SingularityFunction(x, 0, 0)/(E*I) + + 0.666666666666667*(-80*SingularityFunction(x, 0, 1) + 10*SingularityFunction(x, 0, 2) - 10*SingularityFunction(x, 4, 2))*SingularityFunction(x, 2, 0)/(E*I) + """ + x = self.variable + E = self.elastic_modulus + new_length = self.length + beam.length + if self.elastic_modulus != beam.elastic_modulus: + raise NotImplementedError('Joining beams with different Elastic modulus is not implemented.') + + if self.second_moment != beam.second_moment: + new_second_moment = Piecewise((self.second_moment, x<=self.length), + (beam.second_moment, x<=new_length)) + else: + new_second_moment = self.second_moment + + if via == "fixed": + new_beam = Beam(new_length, E, new_second_moment, x) + new_beam._joined_beam = True + return new_beam + + if via == "hinge": + new_beam = Beam(new_length, E, new_second_moment, x) + new_beam._joined_beam = True + new_beam.apply_rotation_hinge(self.length) + return new_beam + + def apply_support(self, loc, type="fixed"): + """ + This method applies support to a particular beam object and returns + the symbol of the unknown reaction load(s). + + Parameters + ========== + loc : Sympifyable + Location of point at which support is applied. + type : String + Determines type of Beam support applied. To apply support structure + with + - zero degree of freedom, type = "fixed" + - one degree of freedom, type = "pin" + - two degrees of freedom, type = "roller" + + Returns + ======= + Symbol or tuple of Symbol + The unknown reaction load as a symbol. + - Symbol(reaction_force) if type = "pin" or "roller" + - Symbol(reaction_force), Symbol(reaction_moment) if type = "fixed" + + Examples + ======== + There is a beam of length 20 meters. A moment of magnitude 100 Nm is + applied in the clockwise direction at the end of the beam. A pointload + of magnitude 8 N is applied from the top of the beam at a distance of 10 meters. + There is one fixed support at the start of the beam and a roller at the end. + + Using the sign convention of upward forces and clockwise moment + being positive. + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> E, I = symbols('E, I') + >>> b = Beam(20, E, I) + >>> p0, m0 = b.apply_support(0, 'fixed') + >>> p1 = b.apply_support(20, 'roller') + >>> b.apply_load(-8, 10, -1) + >>> b.apply_load(100, 20, -2) + >>> b.solve_for_reaction_loads(p0, m0, p1) + >>> b.reaction_loads + {M_0: 20, R_0: -2, R_20: 10} + >>> b.reaction_loads[p0] + -2 + >>> b.load + 20*SingularityFunction(x, 0, -2) - 2*SingularityFunction(x, 0, -1) + - 8*SingularityFunction(x, 10, -1) + 100*SingularityFunction(x, 20, -2) + + 10*SingularityFunction(x, 20, -1) + """ + loc = sympify(loc) + + self._applied_supports.append((loc, type)) + if type in ("pin", "roller"): + reaction_load = Symbol('R_'+str(loc)) + self.apply_load(reaction_load, loc, -1) + self.bc_deflection.append((loc, 0)) + else: + reaction_load = Symbol('R_'+str(loc)) + reaction_moment = Symbol('M_'+str(loc)) + self.apply_load(reaction_load, loc, -1) + self.apply_load(reaction_moment, loc, -2) + self.bc_deflection.append((loc, 0)) + self.bc_slope.append((loc, 0)) + self._support_as_loads.append((reaction_moment, loc, -2, None)) + + self._support_as_loads.append((reaction_load, loc, -1, None)) + + if type in ("pin", "roller"): + return reaction_load + else: + return reaction_load, reaction_moment + + def _get_I(self, loc): + """ + Helper function that returns the Second moment (I) at a location in the beam. + """ + I = self.second_moment + if not isinstance(I, Piecewise): + return I + else: + for i in range(len(I.args)): + if loc <= I.args[i][1].args[1]: + return I.args[i][0] + + def apply_rotation_hinge(self, loc): + """ + This method applies a rotation hinge at a single location on the beam. + + Parameters + ---------- + loc : Sympifyable + Location of point at which hinge is applied. + + Returns + ======= + Symbol + The unknown rotation jump multiplied by the elastic modulus and second moment as a symbol. + + Examples + ======== + There is a beam of length 15 meters. Pin supports are placed at distances + of 0 and 10 meters. There is a fixed support at the end. There are two rotation hinges + in the structure, one at 5 meters and one at 10 meters. A pointload of magnitude + 10 kN is applied on the hinge at 5 meters. A distributed load of 5 kN works on + the structure from 10 meters to the end. + + Using the sign convention of upward forces and clockwise moment + being positive. + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import Symbol + >>> E = Symbol('E') + >>> I = Symbol('I') + >>> b = Beam(15, E, I) + >>> r0 = b.apply_support(0, type='pin') + >>> r10 = b.apply_support(10, type='pin') + >>> r15, m15 = b.apply_support(15, type='fixed') + >>> p5 = b.apply_rotation_hinge(5) + >>> p12 = b.apply_rotation_hinge(12) + >>> b.apply_load(-10, 5, -1) + >>> b.apply_load(-5, 10, 0, 15) + >>> b.solve_for_reaction_loads(r0, r10, r15, m15) + >>> b.reaction_loads + {M_15: -75/2, R_0: 0, R_10: 40, R_15: -5} + >>> b.rotation_jumps + {P_12: -1875/(16*E*I), P_5: 9625/(24*E*I)} + >>> b.rotation_jumps[p12] + -1875/(16*E*I) + >>> b.bending_moment() + -9625*SingularityFunction(x, 5, -1)/24 + 10*SingularityFunction(x, 5, 1) + - 40*SingularityFunction(x, 10, 1) + 5*SingularityFunction(x, 10, 2)/2 + + 1875*SingularityFunction(x, 12, -1)/16 + 75*SingularityFunction(x, 15, 0)/2 + + 5*SingularityFunction(x, 15, 1) - 5*SingularityFunction(x, 15, 2)/2 + """ + loc = sympify(loc) + E = self.elastic_modulus + I = self._get_I(loc) + + rotation_jump = Symbol('P_'+str(loc)) + self._applied_rotation_hinges.append(loc) + self._rotation_hinge_symbols.append(rotation_jump) + self.apply_load(E * I * rotation_jump, loc, -3) + self.bc_bending_moment.append((loc, 0)) + return rotation_jump + + def apply_sliding_hinge(self, loc): + """ + This method applies a sliding hinge at a single location on the beam. + + Parameters + ---------- + loc : Sympifyable + Location of point at which hinge is applied. + + Returns + ======= + Symbol + The unknown deflection jump multiplied by the elastic modulus and second moment as a symbol. + + Examples + ======== + There is a beam of length 13 meters. A fixed support is placed at the beginning. + There is a pin support at the end. There is a sliding hinge at a location of 8 meters. + A pointload of magnitude 10 kN is applied on the hinge at 5 meters. + + Using the sign convention of upward forces and clockwise moment + being positive. + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> b = Beam(13, 20, 20) + >>> r0, m0 = b.apply_support(0, type="fixed") + >>> s8 = b.apply_sliding_hinge(8) + >>> r13 = b.apply_support(13, type="pin") + >>> b.apply_load(-10, 5, -1) + >>> b.solve_for_reaction_loads(r0, m0, r13) + >>> b.reaction_loads + {M_0: -50, R_0: 10, R_13: 0} + >>> b.deflection_jumps + {W_8: 85/24} + >>> b.deflection_jumps[s8] + 85/24 + >>> b.bending_moment() + 50*SingularityFunction(x, 0, 0) - 10*SingularityFunction(x, 0, 1) + + 10*SingularityFunction(x, 5, 1) - 4250*SingularityFunction(x, 8, -2)/3 + >>> b.deflection() + -SingularityFunction(x, 0, 2)/16 + SingularityFunction(x, 0, 3)/240 + - SingularityFunction(x, 5, 3)/240 + 85*SingularityFunction(x, 8, 0)/24 + """ + loc = sympify(loc) + E = self.elastic_modulus + I = self._get_I(loc) + + deflection_jump = Symbol('W_' + str(loc)) + self._applied_sliding_hinges.append(loc) + self._sliding_hinge_symbols.append(deflection_jump) + self.apply_load(E * I * deflection_jump, loc, -4) + self.bc_shear_force.append((loc, 0)) + return deflection_jump + + def apply_load(self, value, start, order, end=None): + """ + This method adds up the loads given to a particular beam object. + + Parameters + ========== + value : Sympifyable + The value inserted should have the units [Force/(Distance**(n+1)] + where n is the order of applied load. + Units for applied loads: + + - For moments, unit = kN*m + - For point loads, unit = kN + - For constant distributed load, unit = kN/m + - For ramp loads, unit = kN/m/m + - For parabolic ramp loads, unit = kN/m/m/m + - ... so on. + + start : Sympifyable + The starting point of the applied load. For point moments and + point forces this is the location of application. + order : Integer + The order of the applied load. + + - For moments, order = -2 + - For point loads, order =-1 + - For constant distributed load, order = 0 + - For ramp loads, order = 1 + - For parabolic ramp loads, order = 2 + - ... so on. + + end : Sympifyable, optional + An optional argument that can be used if the load has an end point + within the length of the beam. + + Examples + ======== + There is a beam of length 4 meters. A moment of magnitude 3 Nm is + applied in the clockwise direction at the starting point of the beam. + A point load of magnitude 4 N is applied from the top of the beam at + 2 meters from the starting point and a parabolic ramp load of magnitude + 2 N/m is applied below the beam starting from 2 meters to 3 meters + away from the starting point of the beam. + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> E, I = symbols('E, I') + >>> b = Beam(4, E, I) + >>> b.apply_load(-3, 0, -2) + >>> b.apply_load(4, 2, -1) + >>> b.apply_load(-2, 2, 2, end=3) + >>> b.load + -3*SingularityFunction(x, 0, -2) + 4*SingularityFunction(x, 2, -1) - 2*SingularityFunction(x, 2, 2) + 2*SingularityFunction(x, 3, 0) + 4*SingularityFunction(x, 3, 1) + 2*SingularityFunction(x, 3, 2) + + """ + x = self.variable + value = sympify(value) + start = sympify(start) + order = sympify(order) + + self._applied_loads.append((value, start, order, end)) + self._load += value*SingularityFunction(x, start, order) + self._original_load += value*SingularityFunction(x, start, order) + + if end: + # load has an end point within the length of the beam. + self._handle_end(x, value, start, order, end, type="apply") + + def remove_load(self, value, start, order, end=None): + """ + This method removes a particular load present on the beam object. + Returns a ValueError if the load passed as an argument is not + present on the beam. + + Parameters + ========== + value : Sympifyable + The magnitude of an applied load. + start : Sympifyable + The starting point of the applied load. For point moments and + point forces this is the location of application. + order : Integer + The order of the applied load. + - For moments, order= -2 + - For point loads, order=-1 + - For constant distributed load, order=0 + - For ramp loads, order=1 + - For parabolic ramp loads, order=2 + - ... so on. + end : Sympifyable, optional + An optional argument that can be used if the load has an end point + within the length of the beam. + + Examples + ======== + There is a beam of length 4 meters. A moment of magnitude 3 Nm is + applied in the clockwise direction at the starting point of the beam. + A pointload of magnitude 4 N is applied from the top of the beam at + 2 meters from the starting point and a parabolic ramp load of magnitude + 2 N/m is applied below the beam starting from 2 meters to 3 meters + away from the starting point of the beam. + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> E, I = symbols('E, I') + >>> b = Beam(4, E, I) + >>> b.apply_load(-3, 0, -2) + >>> b.apply_load(4, 2, -1) + >>> b.apply_load(-2, 2, 2, end=3) + >>> b.load + -3*SingularityFunction(x, 0, -2) + 4*SingularityFunction(x, 2, -1) - 2*SingularityFunction(x, 2, 2) + 2*SingularityFunction(x, 3, 0) + 4*SingularityFunction(x, 3, 1) + 2*SingularityFunction(x, 3, 2) + >>> b.remove_load(-2, 2, 2, end = 3) + >>> b.load + -3*SingularityFunction(x, 0, -2) + 4*SingularityFunction(x, 2, -1) + """ + x = self.variable + value = sympify(value) + start = sympify(start) + order = sympify(order) + + if (value, start, order, end) in self._applied_loads: + self._load -= value*SingularityFunction(x, start, order) + self._original_load -= value*SingularityFunction(x, start, order) + self._applied_loads.remove((value, start, order, end)) + else: + msg = "No such load distribution exists on the beam object." + raise ValueError(msg) + + if end: + # load has an end point within the length of the beam. + self._handle_end(x, value, start, order, end, type="remove") + + def _handle_end(self, x, value, start, order, end, type): + """ + This functions handles the optional `end` value in the + `apply_load` and `remove_load` functions. When the value + of end is not NULL, this function will be executed. + """ + if order.is_negative: + msg = ("If 'end' is provided the 'order' of the load cannot " + "be negative, i.e. 'end' is only valid for distributed " + "loads.") + raise ValueError(msg) + # NOTE : A Taylor series can be used to define the summation of + # singularity functions that subtract from the load past the end + # point such that it evaluates to zero past 'end'. + f = value*x**order + + if type == "apply": + # iterating for "apply_load" method + for i in range(0, order + 1): + self._load -= (f.diff(x, i).subs(x, end - start) * + SingularityFunction(x, end, i)/factorial(i)) + self._original_load -= (f.diff(x, i).subs(x, end - start) * + SingularityFunction(x, end, i)/factorial(i)) + elif type == "remove": + # iterating for "remove_load" method + for i in range(0, order + 1): + self._load += (f.diff(x, i).subs(x, end - start) * + SingularityFunction(x, end, i)/factorial(i)) + self._original_load += (f.diff(x, i).subs(x, end - start) * + SingularityFunction(x, end, i)/factorial(i)) + + + @property + def load(self): + """ + Returns a Singularity Function expression which represents + the load distribution curve of the Beam object. + + Examples + ======== + There is a beam of length 4 meters. A moment of magnitude 3 Nm is + applied in the clockwise direction at the starting point of the beam. + A point load of magnitude 4 N is applied from the top of the beam at + 2 meters from the starting point and a parabolic ramp load of magnitude + 2 N/m is applied below the beam starting from 3 meters away from the + starting point of the beam. + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> E, I = symbols('E, I') + >>> b = Beam(4, E, I) + >>> b.apply_load(-3, 0, -2) + >>> b.apply_load(4, 2, -1) + >>> b.apply_load(-2, 3, 2) + >>> b.load + -3*SingularityFunction(x, 0, -2) + 4*SingularityFunction(x, 2, -1) - 2*SingularityFunction(x, 3, 2) + """ + return self._load + + @property + def applied_loads(self): + """ + Returns a list of all loads applied on the beam object. + Each load in the list is a tuple of form (value, start, order, end). + + Examples + ======== + There is a beam of length 4 meters. A moment of magnitude 3 Nm is + applied in the clockwise direction at the starting point of the beam. + A pointload of magnitude 4 N is applied from the top of the beam at + 2 meters from the starting point. Another pointload of magnitude 5 N + is applied at same position. + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> E, I = symbols('E, I') + >>> b = Beam(4, E, I) + >>> b.apply_load(-3, 0, -2) + >>> b.apply_load(4, 2, -1) + >>> b.apply_load(5, 2, -1) + >>> b.load + -3*SingularityFunction(x, 0, -2) + 9*SingularityFunction(x, 2, -1) + >>> b.applied_loads + [(-3, 0, -2, None), (4, 2, -1, None), (5, 2, -1, None)] + """ + return self._applied_loads + + def solve_for_reaction_loads(self, *reactions): + """ + Solves for the reaction forces. + + Examples + ======== + There is a beam of length 30 meters. A moment of magnitude 120 Nm is + applied in the clockwise direction at the end of the beam. A pointload + of magnitude 8 N is applied from the top of the beam at the starting + point. There are two simple supports below the beam. One at the end + and another one at a distance of 10 meters from the start. The + deflection is restricted at both the supports. + + Using the sign convention of upward forces and clockwise moment + being positive. + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> E, I = symbols('E, I') + >>> R1, R2 = symbols('R1, R2') + >>> b = Beam(30, E, I) + >>> b.apply_load(-8, 0, -1) + >>> b.apply_load(R1, 10, -1) # Reaction force at x = 10 + >>> b.apply_load(R2, 30, -1) # Reaction force at x = 30 + >>> b.apply_load(120, 30, -2) + >>> b.bc_deflection = [(10, 0), (30, 0)] + >>> b.load + R1*SingularityFunction(x, 10, -1) + R2*SingularityFunction(x, 30, -1) + - 8*SingularityFunction(x, 0, -1) + 120*SingularityFunction(x, 30, -2) + >>> b.solve_for_reaction_loads(R1, R2) + >>> b.reaction_loads + {R1: 6, R2: 2} + >>> b.load + -8*SingularityFunction(x, 0, -1) + 6*SingularityFunction(x, 10, -1) + + 120*SingularityFunction(x, 30, -2) + 2*SingularityFunction(x, 30, -1) + """ + + x = self.variable + l = self.length + C3 = Symbol('C3') + C4 = Symbol('C4') + rotation_jumps = tuple(self._rotation_hinge_symbols) + deflection_jumps = tuple(self._sliding_hinge_symbols) + + shear_curve = limit(self.shear_force(), x, l) + moment_curve = limit(self.bending_moment(), x, l) + + shear_force_eqs = [] + bending_moment_eqs = [] + slope_eqs = [] + deflection_eqs = [] + + for position, value in self._boundary_conditions['shear_force']: + eqs = self.shear_force().subs(x, position) - value + new_eqs = sum(arg for arg in eqs.args if not any(num.is_infinite for num in arg.args)) + shear_force_eqs.append(new_eqs) + + for position, value in self._boundary_conditions['bending_moment']: + eqs = self.bending_moment().subs(x, position) - value + new_eqs = sum(arg for arg in eqs.args if not any(num.is_infinite for num in arg.args)) + bending_moment_eqs.append(new_eqs) + + slope_curve = integrate(self.bending_moment(), x) + C3 + for position, value in self._boundary_conditions['slope']: + eqs = slope_curve.subs(x, position) - value + slope_eqs.append(eqs) + + deflection_curve = integrate(slope_curve, x) + C4 + for position, value in self._boundary_conditions['deflection']: + eqs = deflection_curve.subs(x, position) - value + deflection_eqs.append(eqs) + + solution = list((linsolve([shear_curve, moment_curve] + shear_force_eqs + bending_moment_eqs + slope_eqs + + deflection_eqs, (C3, C4) + reactions + rotation_jumps + deflection_jumps).args)[0]) + reaction_index = 2+len(reactions) + rotation_index = reaction_index + len(rotation_jumps) + reaction_solution = solution[2:reaction_index] + rotation_solution = solution[reaction_index:rotation_index] + deflection_solution = solution[rotation_index:] + + self._reaction_loads = dict(zip(reactions, reaction_solution)) + self._rotation_jumps = dict(zip(rotation_jumps, rotation_solution)) + self._deflection_jumps = dict(zip(deflection_jumps, deflection_solution)) + self._load = self._load.subs(self._reaction_loads) + self._load = self._load.subs(self._rotation_jumps) + self._load = self._load.subs(self._deflection_jumps) + + def shear_force(self): + """ + Returns a Singularity Function expression which represents + the shear force curve of the Beam object. + + Examples + ======== + There is a beam of length 30 meters. A moment of magnitude 120 Nm is + applied in the clockwise direction at the end of the beam. A pointload + of magnitude 8 N is applied from the top of the beam at the starting + point. There are two simple supports below the beam. One at the end + and another one at a distance of 10 meters from the start. The + deflection is restricted at both the supports. + + Using the sign convention of upward forces and clockwise moment + being positive. + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> E, I = symbols('E, I') + >>> R1, R2 = symbols('R1, R2') + >>> b = Beam(30, E, I) + >>> b.apply_load(-8, 0, -1) + >>> b.apply_load(R1, 10, -1) + >>> b.apply_load(R2, 30, -1) + >>> b.apply_load(120, 30, -2) + >>> b.bc_deflection = [(10, 0), (30, 0)] + >>> b.solve_for_reaction_loads(R1, R2) + >>> b.shear_force() + 8*SingularityFunction(x, 0, 0) - 6*SingularityFunction(x, 10, 0) - 120*SingularityFunction(x, 30, -1) - 2*SingularityFunction(x, 30, 0) + """ + x = self.variable + return -integrate(self.load, x) + + def max_shear_force(self): + """Returns maximum Shear force and its coordinate + in the Beam object.""" + shear_curve = self.shear_force() + x = self.variable + + terms = shear_curve.args + singularity = [] # Points at which shear function changes + for term in terms: + if isinstance(term, Mul): + term = term.args[-1] # SingularityFunction in the term + singularity.append(term.args[1]) + singularity = list(set(singularity)) + singularity.sort() + + intervals = [] # List of Intervals with discrete value of shear force + shear_values = [] # List of values of shear force in each interval + for i, s in enumerate(singularity): + if s == 0: + continue + try: + shear_slope = Piecewise((float("nan"), x<=singularity[i-1]),(self._load.rewrite(Piecewise), x>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> E, I = symbols('E, I') + >>> R1, R2 = symbols('R1, R2') + >>> b = Beam(30, E, I) + >>> b.apply_load(-8, 0, -1) + >>> b.apply_load(R1, 10, -1) + >>> b.apply_load(R2, 30, -1) + >>> b.apply_load(120, 30, -2) + >>> b.bc_deflection = [(10, 0), (30, 0)] + >>> b.solve_for_reaction_loads(R1, R2) + >>> b.bending_moment() + 8*SingularityFunction(x, 0, 1) - 6*SingularityFunction(x, 10, 1) - 120*SingularityFunction(x, 30, 0) - 2*SingularityFunction(x, 30, 1) + """ + x = self.variable + return integrate(self.shear_force(), x) + + def max_bmoment(self): + """Returns maximum Shear force and its coordinate + in the Beam object.""" + bending_curve = self.bending_moment() + x = self.variable + + terms = bending_curve.args + singularity = [] # Points at which bending moment changes + for term in terms: + if isinstance(term, Mul): + term = term.args[-1] # SingularityFunction in the term + singularity.append(term.args[1]) + singularity = list(set(singularity)) + singularity.sort() + + intervals = [] # List of Intervals with discrete value of bending moment + moment_values = [] # List of values of bending moment in each interval + for i, s in enumerate(singularity): + if s == 0: + continue + try: + moment_slope = Piecewise( + (float("nan"), x <= singularity[i - 1]), + (self.shear_force().rewrite(Piecewise), x < s), + (float("nan"), True)) + points = solve(moment_slope, x) + val = [] + for point in points: + val.append(abs(bending_curve.subs(x, point))) + points.extend([singularity[i-1], s]) + val += [abs(limit(bending_curve, x, singularity[i-1], '+')), abs(limit(bending_curve, x, s, '-'))] + max_moment = max(val) + moment_values.append(max_moment) + intervals.append(points[val.index(max_moment)]) + + # If bending moment in a particular Interval has zero or constant + # slope, then above block gives NotImplementedError as solve + # can't represent Interval solutions. + except NotImplementedError: + initial_moment = limit(bending_curve, x, singularity[i-1], '+') + final_moment = limit(bending_curve, x, s, '-') + # If bending_curve has a constant slope(it is a line). + if bending_curve.subs(x, (singularity[i-1] + s)/2) == (initial_moment + final_moment)/2 and initial_moment != final_moment: + moment_values.extend([initial_moment, final_moment]) + intervals.extend([singularity[i-1], s]) + else: # bending_curve has same value in whole Interval + moment_values.append(final_moment) + intervals.append(Interval(singularity[i-1], s)) + + moment_values = list(map(abs, moment_values)) + maximum_moment = max(moment_values) + point = intervals[moment_values.index(maximum_moment)] + return (point, maximum_moment) + + def point_cflexure(self): + """ + Returns a Set of point(s) with zero bending moment and + where bending moment curve of the beam object changes + its sign from negative to positive or vice versa. + + Examples + ======== + There is is 10 meter long overhanging beam. There are + two simple supports below the beam. One at the start + and another one at a distance of 6 meters from the start. + Point loads of magnitude 10KN and 20KN are applied at + 2 meters and 4 meters from start respectively. A Uniformly + distribute load of magnitude of magnitude 3KN/m is also + applied on top starting from 6 meters away from starting + point till end. + Using the sign convention of upward forces and clockwise moment + being positive. + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> E, I = symbols('E, I') + >>> b = Beam(10, E, I) + >>> b.apply_load(-4, 0, -1) + >>> b.apply_load(-46, 6, -1) + >>> b.apply_load(10, 2, -1) + >>> b.apply_load(20, 4, -1) + >>> b.apply_load(3, 6, 0) + >>> b.point_cflexure() + [10/3] + """ + #Removes the singularity functions of order < 0 from the bending moment equation used in this method + non_singular_bending_moment = sum(arg for arg in self.bending_moment().args if not arg.args[1].args[2] < 0) + + # To restrict the range within length of the Beam + moment_curve = Piecewise((float("nan"), self.variable<=0), + (non_singular_bending_moment, self.variable>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> E, I = symbols('E, I') + >>> R1, R2 = symbols('R1, R2') + >>> b = Beam(30, E, I) + >>> b.apply_load(-8, 0, -1) + >>> b.apply_load(R1, 10, -1) + >>> b.apply_load(R2, 30, -1) + >>> b.apply_load(120, 30, -2) + >>> b.bc_deflection = [(10, 0), (30, 0)] + >>> b.solve_for_reaction_loads(R1, R2) + >>> b.slope() + (-4*SingularityFunction(x, 0, 2) + 3*SingularityFunction(x, 10, 2) + + 120*SingularityFunction(x, 30, 1) + SingularityFunction(x, 30, 2) + 4000/3)/(E*I) + """ + x = self.variable + E = self.elastic_modulus + I = self.second_moment + + if not self._boundary_conditions['slope']: + return diff(self.deflection(), x) + if isinstance(I, Piecewise) and self._joined_beam: + args = I.args + slope = 0 + prev_slope = 0 + prev_end = 0 + for i in range(len(args)): + if i != 0: + prev_end = args[i-1][1].args[1] + slope_value = -S.One/E*integrate(self.bending_moment()/args[i][0], (x, prev_end, x)) + if i != len(args) - 1: + slope += (prev_slope + slope_value)*SingularityFunction(x, prev_end, 0) - \ + (prev_slope + slope_value)*SingularityFunction(x, args[i][1].args[1], 0) + else: + slope += (prev_slope + slope_value)*SingularityFunction(x, prev_end, 0) + prev_slope = slope_value.subs(x, args[i][1].args[1]) + return slope + + C3 = Symbol('C3') + slope_curve = -integrate(S.One/(E*I)*self.bending_moment(), x) + C3 + + bc_eqs = [] + for position, value in self._boundary_conditions['slope']: + eqs = slope_curve.subs(x, position) - value + bc_eqs.append(eqs) + constants = list(linsolve(bc_eqs, C3)) + slope_curve = slope_curve.subs({C3: constants[0][0]}) + return slope_curve + + def deflection(self): + """ + Returns a Singularity Function expression which represents + the elastic curve or deflection of the Beam object. + + Examples + ======== + There is a beam of length 30 meters. A moment of magnitude 120 Nm is + applied in the clockwise direction at the end of the beam. A pointload + of magnitude 8 N is applied from the top of the beam at the starting + point. There are two simple supports below the beam. One at the end + and another one at a distance of 10 meters from the start. The + deflection is restricted at both the supports. + + Using the sign convention of upward forces and clockwise moment + being positive. + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> E, I = symbols('E, I') + >>> R1, R2 = symbols('R1, R2') + >>> b = Beam(30, E, I) + >>> b.apply_load(-8, 0, -1) + >>> b.apply_load(R1, 10, -1) + >>> b.apply_load(R2, 30, -1) + >>> b.apply_load(120, 30, -2) + >>> b.bc_deflection = [(10, 0), (30, 0)] + >>> b.solve_for_reaction_loads(R1, R2) + >>> b.deflection() + (4000*x/3 - 4*SingularityFunction(x, 0, 3)/3 + SingularityFunction(x, 10, 3) + + 60*SingularityFunction(x, 30, 2) + SingularityFunction(x, 30, 3)/3 - 12000)/(E*I) + """ + x = self.variable + E = self.elastic_modulus + I = self.second_moment + if not self._boundary_conditions['deflection'] and not self._boundary_conditions['slope']: + if isinstance(I, Piecewise) and self._joined_beam: + args = I.args + prev_slope = 0 + prev_def = 0 + prev_end = 0 + deflection = 0 + for i in range(len(args)): + if i != 0: + prev_end = args[i-1][1].args[1] + slope_value = -S.One/E*integrate(self.bending_moment()/args[i][0], (x, prev_end, x)) + recent_segment_slope = prev_slope + slope_value + deflection_value = integrate(recent_segment_slope, (x, prev_end, x)) + if i != len(args) - 1: + deflection += (prev_def + deflection_value)*SingularityFunction(x, prev_end, 0) \ + - (prev_def + deflection_value)*SingularityFunction(x, args[i][1].args[1], 0) + else: + deflection += (prev_def + deflection_value)*SingularityFunction(x, prev_end, 0) + prev_slope = slope_value.subs(x, args[i][1].args[1]) + prev_def = deflection_value.subs(x, args[i][1].args[1]) + return deflection + base_char = self._base_char + constants = symbols(base_char + '3:5') + return S.One/(E*I)*integrate(-integrate(self.bending_moment(), x), x) + constants[0]*x + constants[1] + elif not self._boundary_conditions['deflection']: + base_char = self._base_char + constant = symbols(base_char + '4') + return integrate(self.slope(), x) + constant + elif not self._boundary_conditions['slope'] and self._boundary_conditions['deflection']: + if isinstance(I, Piecewise) and self._joined_beam: + args = I.args + prev_slope = 0 + prev_def = 0 + prev_end = 0 + deflection = 0 + for i in range(len(args)): + if i != 0: + prev_end = args[i-1][1].args[1] + slope_value = -S.One/E*integrate(self.bending_moment()/args[i][0], (x, prev_end, x)) + recent_segment_slope = prev_slope + slope_value + deflection_value = integrate(recent_segment_slope, (x, prev_end, x)) + if i != len(args) - 1: + deflection += (prev_def + deflection_value)*SingularityFunction(x, prev_end, 0) \ + - (prev_def + deflection_value)*SingularityFunction(x, args[i][1].args[1], 0) + else: + deflection += (prev_def + deflection_value)*SingularityFunction(x, prev_end, 0) + prev_slope = slope_value.subs(x, args[i][1].args[1]) + prev_def = deflection_value.subs(x, args[i][1].args[1]) + return deflection + base_char = self._base_char + C3, C4 = symbols(base_char + '3:5') # Integration constants + slope_curve = -integrate(self.bending_moment(), x) + C3 + deflection_curve = integrate(slope_curve, x) + C4 + bc_eqs = [] + for position, value in self._boundary_conditions['deflection']: + eqs = deflection_curve.subs(x, position) - value + bc_eqs.append(eqs) + constants = list(linsolve(bc_eqs, (C3, C4))) + deflection_curve = deflection_curve.subs({C3: constants[0][0], C4: constants[0][1]}) + return S.One/(E*I)*deflection_curve + + if isinstance(I, Piecewise) and self._joined_beam: + args = I.args + prev_slope = 0 + prev_def = 0 + prev_end = 0 + deflection = 0 + for i in range(len(args)): + if i != 0: + prev_end = args[i-1][1].args[1] + slope_value = S.One/E*integrate(self.bending_moment()/args[i][0], (x, prev_end, x)) + recent_segment_slope = prev_slope + slope_value + deflection_value = integrate(recent_segment_slope, (x, prev_end, x)) + if i != len(args) - 1: + deflection += (prev_def + deflection_value)*SingularityFunction(x, prev_end, 0) \ + - (prev_def + deflection_value)*SingularityFunction(x, args[i][1].args[1], 0) + else: + deflection += (prev_def + deflection_value)*SingularityFunction(x, prev_end, 0) + prev_slope = slope_value.subs(x, args[i][1].args[1]) + prev_def = deflection_value.subs(x, args[i][1].args[1]) + return deflection + + C4 = Symbol('C4') + deflection_curve = integrate(self.slope(), x) + C4 + + bc_eqs = [] + for position, value in self._boundary_conditions['deflection']: + eqs = deflection_curve.subs(x, position) - value + bc_eqs.append(eqs) + + constants = list(linsolve(bc_eqs, C4)) + deflection_curve = deflection_curve.subs({C4: constants[0][0]}) + return deflection_curve + + def max_deflection(self): + """ + Returns point of max deflection and its corresponding deflection value + in a Beam object. + """ + + # To restrict the range within length of the Beam + slope_curve = Piecewise((float("nan"), self.variable<=0), + (self.slope(), self.variable>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> R1, R2 = symbols('R1, R2') + >>> b = Beam(8, 200*(10**9), 400*(10**-6), 2) + >>> b.apply_load(5000, 2, -1) + >>> b.apply_load(R1, 0, -1) + >>> b.apply_load(R2, 8, -1) + >>> b.apply_load(10000, 4, 0, end=8) + >>> b.bc_deflection = [(0, 0), (8, 0)] + >>> b.solve_for_reaction_loads(R1, R2) + >>> b.plot_shear_stress() + Plot object containing: + [0]: cartesian line: 6875*SingularityFunction(x, 0, 0) - 2500*SingularityFunction(x, 2, 0) + - 5000*SingularityFunction(x, 4, 1) + 15625*SingularityFunction(x, 8, 0) + + 5000*SingularityFunction(x, 8, 1) for x over (0.0, 8.0) + """ + + shear_stress = self.shear_stress() + x = self.variable + length = self.length + + if subs is None: + subs = {} + for sym in shear_stress.atoms(Symbol): + if sym != x and sym not in subs: + raise ValueError('value of %s was not passed.' %sym) + + if length in subs: + length = subs[length] + + # Returns Plot of Shear Stress + return plot (shear_stress.subs(subs), (x, 0, length), + title='Shear Stress', xlabel=r'$\mathrm{x}$', ylabel=r'$\tau$', + line_color='r') + + + def plot_shear_force(self, subs=None): + """ + + Returns a plot for Shear force present in the Beam object. + + Parameters + ========== + subs : dictionary + Python dictionary containing Symbols as key and their + corresponding values. + + Examples + ======== + There is a beam of length 8 meters. A constant distributed load of 10 KN/m + is applied from half of the beam till the end. There are two simple supports + below the beam, one at the starting point and another at the ending point + of the beam. A pointload of magnitude 5 KN is also applied from top of the + beam, at a distance of 4 meters from the starting point. + Take E = 200 GPa and I = 400*(10**-6) meter**4. + + Using the sign convention of downwards forces being positive. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> R1, R2 = symbols('R1, R2') + >>> b = Beam(8, 200*(10**9), 400*(10**-6)) + >>> b.apply_load(5000, 2, -1) + >>> b.apply_load(R1, 0, -1) + >>> b.apply_load(R2, 8, -1) + >>> b.apply_load(10000, 4, 0, end=8) + >>> b.bc_deflection = [(0, 0), (8, 0)] + >>> b.solve_for_reaction_loads(R1, R2) + >>> b.plot_shear_force() + Plot object containing: + [0]: cartesian line: 13750*SingularityFunction(x, 0, 0) - 5000*SingularityFunction(x, 2, 0) + - 10000*SingularityFunction(x, 4, 1) + 31250*SingularityFunction(x, 8, 0) + + 10000*SingularityFunction(x, 8, 1) for x over (0.0, 8.0) + """ + shear_force = self.shear_force() + if subs is None: + subs = {} + for sym in shear_force.atoms(Symbol): + if sym == self.variable: + continue + if sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + if self.length in subs: + length = subs[self.length] + else: + length = self.length + return plot(shear_force.subs(subs), (self.variable, 0, length), title='Shear Force', + xlabel=r'$\mathrm{x}$', ylabel=r'$\mathrm{V}$', line_color='g') + + def plot_bending_moment(self, subs=None): + """ + + Returns a plot for Bending moment present in the Beam object. + + Parameters + ========== + subs : dictionary + Python dictionary containing Symbols as key and their + corresponding values. + + Examples + ======== + There is a beam of length 8 meters. A constant distributed load of 10 KN/m + is applied from half of the beam till the end. There are two simple supports + below the beam, one at the starting point and another at the ending point + of the beam. A pointload of magnitude 5 KN is also applied from top of the + beam, at a distance of 4 meters from the starting point. + Take E = 200 GPa and I = 400*(10**-6) meter**4. + + Using the sign convention of downwards forces being positive. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> R1, R2 = symbols('R1, R2') + >>> b = Beam(8, 200*(10**9), 400*(10**-6)) + >>> b.apply_load(5000, 2, -1) + >>> b.apply_load(R1, 0, -1) + >>> b.apply_load(R2, 8, -1) + >>> b.apply_load(10000, 4, 0, end=8) + >>> b.bc_deflection = [(0, 0), (8, 0)] + >>> b.solve_for_reaction_loads(R1, R2) + >>> b.plot_bending_moment() + Plot object containing: + [0]: cartesian line: 13750*SingularityFunction(x, 0, 1) - 5000*SingularityFunction(x, 2, 1) + - 5000*SingularityFunction(x, 4, 2) + 31250*SingularityFunction(x, 8, 1) + + 5000*SingularityFunction(x, 8, 2) for x over (0.0, 8.0) + """ + bending_moment = self.bending_moment() + if subs is None: + subs = {} + for sym in bending_moment.atoms(Symbol): + if sym == self.variable: + continue + if sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + if self.length in subs: + length = subs[self.length] + else: + length = self.length + return plot(bending_moment.subs(subs), (self.variable, 0, length), title='Bending Moment', + xlabel=r'$\mathrm{x}$', ylabel=r'$\mathrm{M}$', line_color='b') + + def plot_slope(self, subs=None): + """ + + Returns a plot for slope of deflection curve of the Beam object. + + Parameters + ========== + subs : dictionary + Python dictionary containing Symbols as key and their + corresponding values. + + Examples + ======== + There is a beam of length 8 meters. A constant distributed load of 10 KN/m + is applied from half of the beam till the end. There are two simple supports + below the beam, one at the starting point and another at the ending point + of the beam. A pointload of magnitude 5 KN is also applied from top of the + beam, at a distance of 4 meters from the starting point. + Take E = 200 GPa and I = 400*(10**-6) meter**4. + + Using the sign convention of downwards forces being positive. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> R1, R2 = symbols('R1, R2') + >>> b = Beam(8, 200*(10**9), 400*(10**-6)) + >>> b.apply_load(5000, 2, -1) + >>> b.apply_load(R1, 0, -1) + >>> b.apply_load(R2, 8, -1) + >>> b.apply_load(10000, 4, 0, end=8) + >>> b.bc_deflection = [(0, 0), (8, 0)] + >>> b.solve_for_reaction_loads(R1, R2) + >>> b.plot_slope() + Plot object containing: + [0]: cartesian line: -8.59375e-5*SingularityFunction(x, 0, 2) + 3.125e-5*SingularityFunction(x, 2, 2) + + 2.08333333333333e-5*SingularityFunction(x, 4, 3) - 0.0001953125*SingularityFunction(x, 8, 2) + - 2.08333333333333e-5*SingularityFunction(x, 8, 3) + 0.00138541666666667 for x over (0.0, 8.0) + """ + slope = self.slope() + if subs is None: + subs = {} + for sym in slope.atoms(Symbol): + if sym == self.variable: + continue + if sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + if self.length in subs: + length = subs[self.length] + else: + length = self.length + return plot(slope.subs(subs), (self.variable, 0, length), title='Slope', + xlabel=r'$\mathrm{x}$', ylabel=r'$\theta$', line_color='m') + + def plot_deflection(self, subs=None): + """ + + Returns a plot for deflection curve of the Beam object. + + Parameters + ========== + subs : dictionary + Python dictionary containing Symbols as key and their + corresponding values. + + Examples + ======== + There is a beam of length 8 meters. A constant distributed load of 10 KN/m + is applied from half of the beam till the end. There are two simple supports + below the beam, one at the starting point and another at the ending point + of the beam. A pointload of magnitude 5 KN is also applied from top of the + beam, at a distance of 4 meters from the starting point. + Take E = 200 GPa and I = 400*(10**-6) meter**4. + + Using the sign convention of downwards forces being positive. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> R1, R2 = symbols('R1, R2') + >>> b = Beam(8, 200*(10**9), 400*(10**-6)) + >>> b.apply_load(5000, 2, -1) + >>> b.apply_load(R1, 0, -1) + >>> b.apply_load(R2, 8, -1) + >>> b.apply_load(10000, 4, 0, end=8) + >>> b.bc_deflection = [(0, 0), (8, 0)] + >>> b.solve_for_reaction_loads(R1, R2) + >>> b.plot_deflection() + Plot object containing: + [0]: cartesian line: 0.00138541666666667*x - 2.86458333333333e-5*SingularityFunction(x, 0, 3) + + 1.04166666666667e-5*SingularityFunction(x, 2, 3) + 5.20833333333333e-6*SingularityFunction(x, 4, 4) + - 6.51041666666667e-5*SingularityFunction(x, 8, 3) - 5.20833333333333e-6*SingularityFunction(x, 8, 4) + for x over (0.0, 8.0) + """ + deflection = self.deflection() + if subs is None: + subs = {} + for sym in deflection.atoms(Symbol): + if sym == self.variable: + continue + if sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + if self.length in subs: + length = subs[self.length] + else: + length = self.length + return plot(deflection.subs(subs), (self.variable, 0, length), + title='Deflection', xlabel=r'$\mathrm{x}$', ylabel=r'$\delta$', + line_color='r') + + + def plot_loading_results(self, subs=None): + """ + Returns a subplot of Shear Force, Bending Moment, + Slope and Deflection of the Beam object. + + Parameters + ========== + + subs : dictionary + Python dictionary containing Symbols as key and their + corresponding values. + + Examples + ======== + + There is a beam of length 8 meters. A constant distributed load of 10 KN/m + is applied from half of the beam till the end. There are two simple supports + below the beam, one at the starting point and another at the ending point + of the beam. A pointload of magnitude 5 KN is also applied from top of the + beam, at a distance of 4 meters from the starting point. + Take E = 200 GPa and I = 400*(10**-6) meter**4. + + Using the sign convention of downwards forces being positive. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> R1, R2 = symbols('R1, R2') + >>> b = Beam(8, 200*(10**9), 400*(10**-6)) + >>> b.apply_load(5000, 2, -1) + >>> b.apply_load(R1, 0, -1) + >>> b.apply_load(R2, 8, -1) + >>> b.apply_load(10000, 4, 0, end=8) + >>> b.bc_deflection = [(0, 0), (8, 0)] + >>> b.solve_for_reaction_loads(R1, R2) + >>> axes = b.plot_loading_results() + """ + length = self.length + variable = self.variable + if subs is None: + subs = {} + for sym in self.deflection().atoms(Symbol): + if sym == self.variable: + continue + if sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + if length in subs: + length = subs[length] + ax1 = plot(self.shear_force().subs(subs), (variable, 0, length), + title="Shear Force", xlabel=r'$\mathrm{x}$', ylabel=r'$\mathrm{V}$', + line_color='g', show=False) + ax2 = plot(self.bending_moment().subs(subs), (variable, 0, length), + title="Bending Moment", xlabel=r'$\mathrm{x}$', ylabel=r'$\mathrm{M}$', + line_color='b', show=False) + ax3 = plot(self.slope().subs(subs), (variable, 0, length), + title="Slope", xlabel=r'$\mathrm{x}$', ylabel=r'$\theta$', + line_color='m', show=False) + ax4 = plot(self.deflection().subs(subs), (variable, 0, length), + title="Deflection", xlabel=r'$\mathrm{x}$', ylabel=r'$\delta$', + line_color='r', show=False) + + return PlotGrid(4, 1, ax1, ax2, ax3, ax4) + + def _solve_for_ild_equations(self, value): + """ + + Helper function for I.L.D. It takes the unsubstituted + copy of the load equation and uses it to calculate shear force and bending + moment equations. + """ + x = self.variable + a = self.ild_variable + load = self._load + value * SingularityFunction(x, a, -1) + shear_force = -integrate(load, x) + bending_moment = integrate(shear_force, x) + + return shear_force, bending_moment + + def solve_for_ild_reactions(self, value, *reactions): + """ + + Determines the Influence Line Diagram equations for reaction + forces under the effect of a moving load. + + Parameters + ========== + value : Integer + Magnitude of moving load + reactions : + The reaction forces applied on the beam. + + Warning + ======= + This method creates equations that can give incorrect results when + substituting a = 0 or a = l, with l the length of the beam. + + Examples + ======== + + There is a beam of length 10 meters. There are two simple supports + below the beam, one at the starting point and another at the ending + point of the beam. Calculate the I.L.D. equations for reaction forces + under the effect of a moving load of magnitude 1kN. + + Using the sign convention of downwards forces being positive. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy import symbols + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> E, I = symbols('E, I') + >>> R_0, R_10 = symbols('R_0, R_10') + >>> b = Beam(10, E, I) + >>> p0 = b.apply_support(0, 'pin') + >>> p10 = b.apply_support(10, 'roller') + >>> b.solve_for_ild_reactions(1,R_0,R_10) + >>> b.ild_reactions + {R_0: -SingularityFunction(a, 0, 0) + SingularityFunction(a, 0, 1)/10 - SingularityFunction(a, 10, 1)/10, + R_10: -SingularityFunction(a, 0, 1)/10 + SingularityFunction(a, 10, 0) + SingularityFunction(a, 10, 1)/10} + + """ + shear_force, bending_moment = self._solve_for_ild_equations(value) + x = self.variable + l = self.length + a = self.ild_variable + + rotation_jumps = tuple(self._rotation_hinge_symbols) + deflection_jumps = tuple(self._sliding_hinge_symbols) + + C3 = Symbol('C3') + C4 = Symbol('C4') + + shear_curve = limit(shear_force, x, l) - value*(SingularityFunction(a, 0, 0) - SingularityFunction(a, l, 0)) + moment_curve = (limit(bending_moment, x, l) - value * (l * SingularityFunction(a, 0, 0) + - SingularityFunction(a, 0, 1) + + SingularityFunction(a, l, 1))) + + shear_force_eqs = [] + bending_moment_eqs = [] + slope_eqs = [] + deflection_eqs = [] + + for position, val in self._boundary_conditions['shear_force']: + eqs = self.shear_force().subs(x, position) - val + eqs_without_inf = sum(arg for arg in eqs.args if not any(num.is_infinite for num in arg.args)) + shear_sinc = value * (SingularityFunction(- a, - position, 0) - SingularityFunction(-a, 0, 0)) + eqs_with_shear_sinc = eqs_without_inf - shear_sinc + shear_force_eqs.append(eqs_with_shear_sinc) + + for position, val in self._boundary_conditions['bending_moment']: + eqs = self.bending_moment().subs(x, position) - val + eqs_without_inf = sum(arg for arg in eqs.args if not any(num.is_infinite for num in arg.args)) + moment_sinc = value * (position * SingularityFunction(a, 0, 0) + - SingularityFunction(a, 0, 1) + SingularityFunction(a, position, 1)) + eqs_with_moment_sinc = eqs_without_inf - moment_sinc + bending_moment_eqs.append(eqs_with_moment_sinc) + + slope_curve = integrate(bending_moment, x) + C3 + for position, val in self._boundary_conditions['slope']: + eqs = slope_curve.subs(x, position) - val + value * (SingularityFunction(-a, 0, 1) + position * SingularityFunction(-a, 0, 0))**2 / 2 + slope_eqs.append(eqs) + + deflection_curve = integrate(slope_curve, x) + C4 + for position, val in self._boundary_conditions['deflection']: + eqs = deflection_curve.subs(x, position) - val + value * (SingularityFunction(-a, 0, 1) + position * SingularityFunction(-a, 0, 0)) ** 3 / 6 + deflection_eqs.append(eqs) + + solution = list((linsolve([shear_curve, moment_curve] + shear_force_eqs + bending_moment_eqs + slope_eqs + + deflection_eqs, (C3, C4) + reactions + rotation_jumps + deflection_jumps).args)[0]) + + reaction_index = 2 + len(reactions) + rotation_index = reaction_index + len(rotation_jumps) + reaction_solution = solution[2:reaction_index] + rotation_solution = solution[reaction_index:rotation_index] + deflection_solution = solution[rotation_index:] + + self._ild_reactions = dict(zip(reactions, reaction_solution)) + self._ild_rotations_jumps = dict(zip(rotation_jumps, rotation_solution)) + self._ild_deflection_jumps = dict(zip(deflection_jumps, deflection_solution)) + + def plot_ild_reactions(self, subs=None): + """ + + Plots the Influence Line Diagram of Reaction Forces + under the effect of a moving load. This function + should be called after calling solve_for_ild_reactions(). + + Parameters + ========== + + subs : dictionary + Python dictionary containing Symbols as key and their + corresponding values. + + Warning + ======= + The values for a = 0 and a = l, with l the length of the beam, in + the plot can be incorrect. + + Examples + ======== + + There is a beam of length 10 meters. A point load of magnitude 5KN + is also applied from top of the beam, at a distance of 4 meters + from the starting point. There are two simple supports below the + beam, located at the starting point and at a distance of 7 meters + from the starting point. Plot the I.L.D. equations for reactions + at both support points under the effect of a moving load + of magnitude 1kN. + + Using the sign convention of downwards forces being positive. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy import symbols + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> E, I = symbols('E, I') + >>> R_0, R_7 = symbols('R_0, R_7') + >>> b = Beam(10, E, I) + >>> p0 = b.apply_support(0, 'roller') + >>> p7 = b.apply_support(7, 'roller') + >>> b.apply_load(5,4,-1) + >>> b.solve_for_ild_reactions(1,R_0,R_7) + >>> b.ild_reactions + {R_0: -SingularityFunction(a, 0, 0) + SingularityFunction(a, 0, 1)/7 + - 3*SingularityFunction(a, 10, 0)/7 - SingularityFunction(a, 10, 1)/7 - 15/7, + R_7: -SingularityFunction(a, 0, 1)/7 + 10*SingularityFunction(a, 10, 0)/7 + SingularityFunction(a, 10, 1)/7 - 20/7} + >>> b.plot_ild_reactions() + PlotGrid object containing: + Plot[0]:Plot object containing: + [0]: cartesian line: -SingularityFunction(a, 0, 0) + SingularityFunction(a, 0, 1)/7 + - 3*SingularityFunction(a, 10, 0)/7 - SingularityFunction(a, 10, 1)/7 - 15/7 for a over (0.0, 10.0) + Plot[1]:Plot object containing: + [0]: cartesian line: -SingularityFunction(a, 0, 1)/7 + 10*SingularityFunction(a, 10, 0)/7 + + SingularityFunction(a, 10, 1)/7 - 20/7 for a over (0.0, 10.0) + + """ + if not self._ild_reactions: + raise ValueError("I.L.D. reaction equations not found. Please use solve_for_ild_reactions() to generate the I.L.D. reaction equations.") + + a = self.ild_variable + ildplots = [] + + if subs is None: + subs = {} + + for reaction in self._ild_reactions: + for sym in self._ild_reactions[reaction].atoms(Symbol): + if sym != a and sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + + for sym in self._length.atoms(Symbol): + if sym != a and sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + + for reaction in self._ild_reactions: + ildplots.append(plot(self._ild_reactions[reaction].subs(subs), + (a, 0, self._length.subs(subs)), title='I.L.D. for Reactions', + xlabel=a, ylabel=reaction, line_color='blue', show=False)) + + return PlotGrid(len(ildplots), 1, *ildplots) + + def solve_for_ild_shear(self, distance, value, *reactions): + """ + + Determines the Influence Line Diagram equations for shear at a + specified point under the effect of a moving load. + + Parameters + ========== + distance : Integer + Distance of the point from the start of the beam + for which equations are to be determined + value : Integer + Magnitude of moving load + reactions : + The reaction forces applied on the beam. + + Warning + ======= + This method creates equations that can give incorrect results when + substituting a = 0 or a = l, with l the length of the beam. + + Examples + ======== + + There is a beam of length 12 meters. There are two simple supports + below the beam, one at the starting point and another at a distance + of 8 meters. Calculate the I.L.D. equations for Shear at a distance + of 4 meters under the effect of a moving load of magnitude 1kN. + + Using the sign convention of downwards forces being positive. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy import symbols + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> E, I = symbols('E, I') + >>> R_0, R_8 = symbols('R_0, R_8') + >>> b = Beam(12, E, I) + >>> p0 = b.apply_support(0, 'roller') + >>> p8 = b.apply_support(8, 'roller') + >>> b.solve_for_ild_reactions(1, R_0, R_8) + >>> b.solve_for_ild_shear(4, 1, R_0, R_8) + >>> b.ild_shear + -(-SingularityFunction(a, 0, 0) + SingularityFunction(a, 12, 0) + 2)*SingularityFunction(a, 4, 0) + - SingularityFunction(-a, 0, 0) - SingularityFunction(a, 0, 0) + SingularityFunction(a, 0, 1)/8 + + SingularityFunction(a, 12, 0)/2 - SingularityFunction(a, 12, 1)/8 + 1 + + """ + + x = self.variable + l = self.length + a = self.ild_variable + + shear_force, _ = self._solve_for_ild_equations(value) + + shear_curve1 = value - limit(shear_force, x, distance) + shear_curve2 = (limit(shear_force, x, l) - limit(shear_force, x, distance)) - value + + for reaction in reactions: + shear_curve1 = shear_curve1.subs(reaction,self._ild_reactions[reaction]) + shear_curve2 = shear_curve2.subs(reaction,self._ild_reactions[reaction]) + + shear_eq = (shear_curve1 - (shear_curve1 - shear_curve2) * SingularityFunction(a, distance, 0) + - value * SingularityFunction(-a, 0, 0) + value * SingularityFunction(a, l, 0)) + + self._ild_shear = shear_eq + + def plot_ild_shear(self,subs=None): + """ + + Plots the Influence Line Diagram for Shear under the effect + of a moving load. This function should be called after + calling solve_for_ild_shear(). + + Parameters + ========== + + subs : dictionary + Python dictionary containing Symbols as key and their + corresponding values. + + Warning + ======= + The values for a = 0 and a = l, with l the length of the beam, in + the plot can be incorrect. + + Examples + ======== + + There is a beam of length 12 meters. There are two simple supports + below the beam, one at the starting point and another at a distance + of 8 meters. Plot the I.L.D. for Shear at a distance + of 4 meters under the effect of a moving load of magnitude 1kN. + + Using the sign convention of downwards forces being positive. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy import symbols + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> E, I = symbols('E, I') + >>> R_0, R_8 = symbols('R_0, R_8') + >>> b = Beam(12, E, I) + >>> p0 = b.apply_support(0, 'roller') + >>> p8 = b.apply_support(8, 'roller') + >>> b.solve_for_ild_reactions(1, R_0, R_8) + >>> b.solve_for_ild_shear(4, 1, R_0, R_8) + >>> b.ild_shear + -(-SingularityFunction(a, 0, 0) + SingularityFunction(a, 12, 0) + 2)*SingularityFunction(a, 4, 0) + - SingularityFunction(-a, 0, 0) - SingularityFunction(a, 0, 0) + SingularityFunction(a, 0, 1)/8 + + SingularityFunction(a, 12, 0)/2 - SingularityFunction(a, 12, 1)/8 + 1 + >>> b.plot_ild_shear() + Plot object containing: + [0]: cartesian line: -(-SingularityFunction(a, 0, 0) + SingularityFunction(a, 12, 0) + 2)*SingularityFunction(a, 4, 0) + - SingularityFunction(-a, 0, 0) - SingularityFunction(a, 0, 0) + SingularityFunction(a, 0, 1)/8 + + SingularityFunction(a, 12, 0)/2 - SingularityFunction(a, 12, 1)/8 + 1 for a over (0.0, 12.0) + + """ + + if not self._ild_shear: + raise ValueError("I.L.D. shear equation not found. Please use solve_for_ild_shear() to generate the I.L.D. shear equations.") + + l = self._length + a = self.ild_variable + + if subs is None: + subs = {} + + for sym in self._ild_shear.atoms(Symbol): + if sym != a and sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + + for sym in self._length.atoms(Symbol): + if sym != a and sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + + return plot(self._ild_shear.subs(subs), (a, 0, l), title='I.L.D. for Shear', + xlabel=r'$\mathrm{a}$', ylabel=r'$\mathrm{V}$', line_color='blue',show=True) + + def solve_for_ild_moment(self, distance, value, *reactions): + """ + + Determines the Influence Line Diagram equations for moment at a + specified point under the effect of a moving load. + + Parameters + ========== + distance : Integer + Distance of the point from the start of the beam + for which equations are to be determined + value : Integer + Magnitude of moving load + reactions : + The reaction forces applied on the beam. + + Warning + ======= + This method creates equations that can give incorrect results when + substituting a = 0 or a = l, with l the length of the beam. + + Examples + ======== + + There is a beam of length 12 meters. There are two simple supports + below the beam, one at the starting point and another at a distance + of 8 meters. Calculate the I.L.D. equations for Moment at a distance + of 4 meters under the effect of a moving load of magnitude 1kN. + + Using the sign convention of downwards forces being positive. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy import symbols + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> E, I = symbols('E, I') + >>> R_0, R_8 = symbols('R_0, R_8') + >>> b = Beam(12, E, I) + >>> p0 = b.apply_support(0, 'roller') + >>> p8 = b.apply_support(8, 'roller') + >>> b.solve_for_ild_reactions(1, R_0, R_8) + >>> b.solve_for_ild_moment(4, 1, R_0, R_8) + >>> b.ild_moment + -(4*SingularityFunction(a, 0, 0) - SingularityFunction(a, 0, 1) + SingularityFunction(a, 4, 1))*SingularityFunction(a, 4, 0) + - SingularityFunction(a, 0, 1)/2 + SingularityFunction(a, 4, 1) - 2*SingularityFunction(a, 12, 0) + - SingularityFunction(a, 12, 1)/2 + + """ + + x = self.variable + l = self.length + a = self.ild_variable + + _, moment = self._solve_for_ild_equations(value) + + moment_curve1 = value*(distance * SingularityFunction(a, 0, 0) - SingularityFunction(a, 0, 1) + + SingularityFunction(a, distance, 1)) - limit(moment, x, distance) + moment_curve2 = (limit(moment, x, l)-limit(moment, x, distance) + - value * (l * SingularityFunction(a, 0, 0) - SingularityFunction(a, 0, 1) + + SingularityFunction(a, l, 1))) + + for reaction in reactions: + moment_curve1 = moment_curve1.subs(reaction, self._ild_reactions[reaction]) + moment_curve2 = moment_curve2.subs(reaction, self._ild_reactions[reaction]) + + moment_eq = moment_curve1 - (moment_curve1 - moment_curve2) * SingularityFunction(a, distance, 0) + + self._ild_moment = moment_eq + + def plot_ild_moment(self,subs=None): + """ + + Plots the Influence Line Diagram for Moment under the effect + of a moving load. This function should be called after + calling solve_for_ild_moment(). + + Parameters + ========== + + subs : dictionary + Python dictionary containing Symbols as key and their + corresponding values. + + Warning + ======= + The values for a = 0 and a = l, with l the length of the beam, in + the plot can be incorrect. + + Examples + ======== + + There is a beam of length 12 meters. There are two simple supports + below the beam, one at the starting point and another at a distance + of 8 meters. Plot the I.L.D. for Moment at a distance + of 4 meters under the effect of a moving load of magnitude 1kN. + + Using the sign convention of downwards forces being positive. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy import symbols + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> E, I = symbols('E, I') + >>> R_0, R_8 = symbols('R_0, R_8') + >>> b = Beam(12, E, I) + >>> p0 = b.apply_support(0, 'roller') + >>> p8 = b.apply_support(8, 'roller') + >>> b.solve_for_ild_reactions(1, R_0, R_8) + >>> b.solve_for_ild_moment(4, 1, R_0, R_8) + >>> b.ild_moment + -(4*SingularityFunction(a, 0, 0) - SingularityFunction(a, 0, 1) + SingularityFunction(a, 4, 1))*SingularityFunction(a, 4, 0) + - SingularityFunction(a, 0, 1)/2 + SingularityFunction(a, 4, 1) - 2*SingularityFunction(a, 12, 0) + - SingularityFunction(a, 12, 1)/2 + >>> b.plot_ild_moment() + Plot object containing: + [0]: cartesian line: -(4*SingularityFunction(a, 0, 0) - SingularityFunction(a, 0, 1) + + SingularityFunction(a, 4, 1))*SingularityFunction(a, 4, 0) - SingularityFunction(a, 0, 1)/2 + + SingularityFunction(a, 4, 1) - 2*SingularityFunction(a, 12, 0) - SingularityFunction(a, 12, 1)/2 for a over (0.0, 12.0) + + """ + + if not self._ild_moment: + raise ValueError("I.L.D. moment equation not found. Please use solve_for_ild_moment() to generate the I.L.D. moment equations.") + + a = self.ild_variable + + if subs is None: + subs = {} + + for sym in self._ild_moment.atoms(Symbol): + if sym != a and sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + + for sym in self._length.atoms(Symbol): + if sym != a and sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + return plot(self._ild_moment.subs(subs), (a, 0, self._length), title='I.L.D. for Moment', + xlabel=r'$\mathrm{a}$', ylabel=r'$\mathrm{M}$', line_color='blue', show=True) + + @doctest_depends_on(modules=('numpy',)) + def draw(self, pictorial=True): + """ + Returns a plot object representing the beam diagram of the beam. + In particular, the diagram might include: + + * the beam. + * vertical black arrows represent point loads and support reaction + forces (the latter if they have been added with the ``apply_load`` + method). + * circular arrows represent moments. + * shaded areas represent distributed loads. + * the support, if ``apply_support`` has been executed. + * if a composite beam has been created with the ``join`` method and + a hinge has been specified, it will be shown with a white disc. + + The diagram shows positive loads on the upper side of the beam, + and negative loads on the lower side. If two or more distributed + loads acts along the same direction over the same region, the + function will add them up together. + + .. note:: + The user must be careful while entering load values. + The draw function assumes a sign convention which is used + for plotting loads. + Given a right handed coordinate system with XYZ coordinates, + the beam's length is assumed to be along the positive X axis. + The draw function recognizes positive loads(with n>-2) as loads + acting along negative Y direction and positive moments acting + along positive Z direction. + + Parameters + ========== + + pictorial: Boolean (default=True) + Setting ``pictorial=True`` would simply create a pictorial (scaled) + view of the beam diagram. On the other hand, ``pictorial=False`` + would create a beam diagram with the exact dimensions on the plot. + + Examples + ======== + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.physics.continuum_mechanics.beam import Beam + >>> from sympy import symbols + >>> P1, P2, M = symbols('P1, P2, M') + >>> E, I = symbols('E, I') + >>> b = Beam(50, 20, 30) + >>> b.apply_load(-10, 2, -1) + >>> b.apply_load(15, 26, -1) + >>> b.apply_load(P1, 10, -1) + >>> b.apply_load(-P2, 40, -1) + >>> b.apply_load(90, 5, 0, 23) + >>> b.apply_load(10, 30, 1, 50) + >>> b.apply_load(M, 15, -2) + >>> b.apply_load(-M, 30, -2) + >>> p50 = b.apply_support(50, "pin") + >>> p0, m0 = b.apply_support(0, "fixed") + >>> p20 = b.apply_support(20, "roller") + >>> p = b.draw() # doctest: +SKIP + >>> p # doctest: +ELLIPSIS,+SKIP + Plot object containing: + [0]: cartesian line: 25*SingularityFunction(x, 5, 0) - 25*SingularityFunction(x, 23, 0) + + SingularityFunction(x, 30, 1) - 20*SingularityFunction(x, 50, 0) + - SingularityFunction(x, 50, 1) + 5 for x over (0.0, 50.0) + [1]: cartesian line: 5 for x over (0.0, 50.0) + ... + >>> p.show() # doctest: +SKIP + + """ + if not numpy: + raise ImportError("To use this function numpy module is required") + + loads = list(set(self.applied_loads) - set(self._support_as_loads)) + if (not pictorial) and any((len(l[0].free_symbols) > 0) and (l[2] >= 0) for l in loads): + raise ValueError("`pictorial=False` requires numerical " + "distributed loads. Instead, symbolic loads were found. " + "Cannot continue.") + + x = self.variable + + # checking whether length is an expression in terms of any Symbol. + if isinstance(self.length, Expr): + l = list(self.length.atoms(Symbol)) + # assigning every Symbol a default value of 10 + l = dict.fromkeys(l, 10) + length = self.length.subs(l) + else: + l = {} + length = self.length + height = length/10 + + rectangles = [] + rectangles.append({'xy':(0, 0), 'width':length, 'height': height, 'facecolor':"brown"}) + annotations, markers, load_eq,load_eq1, fill = self._draw_load(pictorial, length, l) + support_markers, support_rectangles = self._draw_supports(length, l) + + rectangles += support_rectangles + markers += support_markers + + for loc in self._applied_rotation_hinges: + ratio = loc / self.length + x_pos = float(ratio) * length + markers += [{'args':[[x_pos], [height / 2]], 'marker':'o', 'markersize':6, 'color':"white"}] + + for loc in self._applied_sliding_hinges: + ratio = loc / self.length + x_pos = float(ratio) * length + markers += [{'args': [[x_pos], [height / 2]], 'marker':'|', 'markersize':12, 'color':"white"}] + + ylim = (-length, 1.25*length) + if fill: + # when distributed loads are presents, they might get clipped out + # in the figure by the ylim settings. + # It might be necessary to compute new limits. + _min = min(min(fill["y2"]), min(r["xy"][1] for r in rectangles)) + _max = max(max(fill["y1"]), max(r["xy"][1] for r in rectangles)) + if (_min < ylim[0]) or (_max > ylim[1]): + offset = abs(_max - _min) * 0.1 + ylim = (_min - offset, _max + offset) + + sing_plot = plot(height + load_eq, height + load_eq1, (x, 0, length), + xlim=(-height, length + height), ylim=ylim, + annotations=annotations, markers=markers, rectangles=rectangles, + line_color='brown', fill=fill, axis=False, show=False) + + return sing_plot + + + def _is_load_negative(self, load): + """Try to determine if a load is negative or positive, using + expansion and doit if necessary. + + Returns + ======= + True: if the load is negative + False: if the load is positive + None: if it is indeterminate + + """ + rv = load.is_negative + if load.is_Atom or rv is not None: + return rv + return load.doit().expand().is_negative + + def _draw_load(self, pictorial, length, l): + loads = list(set(self.applied_loads) - set(self._support_as_loads)) + height = length/10 + x = self.variable + + annotations = [] + markers = [] + load_args = [] + scaled_load = 0 + load_args1 = [] + scaled_load1 = 0 + load_eq = S.Zero # For positive valued higher order loads + load_eq1 = S.Zero # For negative valued higher order loads + fill = None + + # schematic view should use the class convention as much as possible. + # However, users can add expressions as symbolic loads, for example + # P1 - P2: is this load positive or negative? We can't say. + # On these occasions it is better to inform users about the + # indeterminate state of those loads. + warning_head = "Please, note that this schematic view might not be " \ + "in agreement with the sign convention used by the Beam class " \ + "for load-related computations, because it was not possible " \ + "to determine the sign (hence, the direction) of the " \ + "following loads:\n" + warning_body = "" + + for load in loads: + # check if the position of load is in terms of the beam length. + if l: + pos = load[1].subs(l) + else: + pos = load[1] + + # point loads + if load[2] == -1: + iln = self._is_load_negative(load[0]) + if iln is None: + warning_body += "* Point load %s located at %s\n" % (load[0], load[1]) + if iln: + annotations.append({'text':'', 'xy':(pos, 0), 'xytext':(pos, height - 4*height), 'arrowprops':{'width': 1.5, 'headlength': 5, 'headwidth': 5, 'facecolor': 'black'}}) + else: + annotations.append({'text':'', 'xy':(pos, height), 'xytext':(pos, height*4), 'arrowprops':{"width": 1.5, "headlength": 4, "headwidth": 4, "facecolor": 'black'}}) + # moment loads + elif load[2] == -2: + iln = self._is_load_negative(load[0]) + if iln is None: + warning_body += "* Moment %s located at %s\n" % (load[0], load[1]) + if self._is_load_negative(load[0]): + markers.append({'args':[[pos], [height/2]], 'marker': r'$\circlearrowright$', 'markersize':15}) + else: + markers.append({'args':[[pos], [height/2]], 'marker': r'$\circlearrowleft$', 'markersize':15}) + # higher order loads + elif load[2] >= 0: + # `fill` will be assigned only when higher order loads are present + value, start, order, end = load + + iln = self._is_load_negative(value) + if iln is None: + warning_body += "* Distributed load %s from %s to %s\n" % (value, start, end) + + # Positive loads have their separate equations + if not iln: + # if pictorial is True we remake the load equation again with + # some constant magnitude values. + if pictorial: + # remake the load equation again with some constant + # magnitude values. + value = 10**(1-order) if order > 0 else length/2 + scaled_load += value*SingularityFunction(x, start, order) + if end: + f2 = value*x**order if order >= 0 else length/2*x**order + for i in range(0, order + 1): + scaled_load -= (f2.diff(x, i).subs(x, end - start)* + SingularityFunction(x, end, i)/factorial(i)) + + if isinstance(scaled_load, Add): + load_args = scaled_load.args + else: + # when the load equation consists of only a single term + load_args = (scaled_load,) + load_eq = Add(*[i.subs(l) for i in load_args]) + + # For loads with negative value + else: + if pictorial: + # remake the load equation again with some constant + # magnitude values. + value = 10**(1-order) if order > 0 else length/2 + scaled_load1 += abs(value)*SingularityFunction(x, start, order) + if end: + f2 = abs(value)*x**order if order >= 0 else length/2*x**order + for i in range(0, order + 1): + scaled_load1 -= (f2.diff(x, i).subs(x, end - start)* + SingularityFunction(x, end, i)/factorial(i)) + + if isinstance(scaled_load1, Add): + load_args1 = scaled_load1.args + else: + # when the load equation consists of only a single term + load_args1 = (scaled_load1,) + load_eq1 = [i.subs(l) for i in load_args1] + load_eq1 = -Add(*load_eq1) - height + + if len(warning_body) > 0: + warnings.warn(warning_head + warning_body) + + xx = numpy.arange(0, float(length), 0.001) + yy1 = lambdify([x], height + load_eq.rewrite(Piecewise))(xx) + yy2 = lambdify([x], height + load_eq1.rewrite(Piecewise))(xx) + if not isinstance(yy1, numpy.ndarray): + yy1 *= numpy.ones_like(xx) + if not isinstance(yy2, numpy.ndarray): + yy2 *= numpy.ones_like(xx) + fill = {'x': xx, 'y1': yy1, 'y2': yy2, + 'color':'darkkhaki', "zorder": -1} + return annotations, markers, load_eq, load_eq1, fill + + + def _draw_supports(self, length, l): + height = float(length/10) + + support_markers = [] + support_rectangles = [] + for support in self._applied_supports: + if l: + pos = support[0].subs(l) + else: + pos = support[0] + + if support[1] == "pin": + support_markers.append({'args':[pos, [0]], 'marker':6, 'markersize':13, 'color':"black"}) + + elif support[1] == "roller": + support_markers.append({'args':[pos, [-height/2.5]], 'marker':'o', 'markersize':11, 'color':"black"}) + + elif support[1] == "fixed": + if pos == 0: + support_rectangles.append({'xy':(0, -3*height), 'width':-length/20, 'height':6*height + height, 'fill':False, 'hatch':'/////'}) + else: + support_rectangles.append({'xy':(length, -3*height), 'width':length/20, 'height': 6*height + height, 'fill':False, 'hatch':'/////'}) + + return support_markers, support_rectangles + + +class Beam3D(Beam): + """ + This class handles loads applied in any direction of a 3D space along + with unequal values of Second moment along different axes. + + .. note:: + A consistent sign convention must be used while solving a beam + bending problem; the results will + automatically follow the chosen sign convention. + This class assumes that any kind of distributed load/moment is + applied through out the span of a beam. + + Examples + ======== + There is a beam of l meters long. A constant distributed load of magnitude q + is applied along y-axis from start till the end of beam. A constant distributed + moment of magnitude m is also applied along z-axis from start till the end of beam. + Beam is fixed at both of its end. So, deflection of the beam at the both ends + is restricted. + + >>> from sympy.physics.continuum_mechanics.beam import Beam3D + >>> from sympy import symbols, simplify, collect, factor + >>> l, E, G, I, A = symbols('l, E, G, I, A') + >>> b = Beam3D(l, E, G, I, A) + >>> x, q, m = symbols('x, q, m') + >>> b.apply_load(q, 0, 0, dir="y") + >>> b.apply_moment_load(m, 0, -1, dir="z") + >>> b.shear_force() + [0, -q*x, 0] + >>> b.bending_moment() + [0, 0, -m*x + q*x**2/2] + >>> b.bc_slope = [(0, [0, 0, 0]), (l, [0, 0, 0])] + >>> b.bc_deflection = [(0, [0, 0, 0]), (l, [0, 0, 0])] + >>> b.solve_slope_deflection() + >>> factor(b.slope()) + [0, 0, x*(-l + x)*(-A*G*l**3*q + 2*A*G*l**2*q*x - 12*E*I*l*q + - 72*E*I*m + 24*E*I*q*x)/(12*E*I*(A*G*l**2 + 12*E*I))] + >>> dx, dy, dz = b.deflection() + >>> dy = collect(simplify(dy), x) + >>> dx == dz == 0 + True + >>> dy == (x*(12*E*I*l*(A*G*l**2*q - 2*A*G*l*m + 12*E*I*q) + ... + x*(A*G*l*(3*l*(A*G*l**2*q - 2*A*G*l*m + 12*E*I*q) + x*(-2*A*G*l**2*q + 4*A*G*l*m - 24*E*I*q)) + ... + A*G*(A*G*l**2 + 12*E*I)*(-2*l**2*q + 6*l*m - 4*m*x + q*x**2) + ... - 12*E*I*q*(A*G*l**2 + 12*E*I)))/(24*A*E*G*I*(A*G*l**2 + 12*E*I))) + True + + References + ========== + + .. [1] https://homes.civil.aau.dk/jc/FemteSemester/Beams3D.pdf + + """ + + def __init__(self, length, elastic_modulus, shear_modulus, second_moment, + area, variable=Symbol('x')): + """Initializes the class. + + Parameters + ========== + length : Sympifyable + A Symbol or value representing the Beam's length. + elastic_modulus : Sympifyable + A SymPy expression representing the Beam's Modulus of Elasticity. + It is a measure of the stiffness of the Beam material. + shear_modulus : Sympifyable + A SymPy expression representing the Beam's Modulus of rigidity. + It is a measure of rigidity of the Beam material. + second_moment : Sympifyable or list + A list of two elements having SymPy expression representing the + Beam's Second moment of area. First value represent Second moment + across y-axis and second across z-axis. + Single SymPy expression can be passed if both values are same + area : Sympifyable + A SymPy expression representing the Beam's cross-sectional area + in a plane perpendicular to length of the Beam. + variable : Symbol, optional + A Symbol object that will be used as the variable along the beam + while representing the load, shear, moment, slope and deflection + curve. By default, it is set to ``Symbol('x')``. + """ + super().__init__(length, elastic_modulus, second_moment, variable) + self.shear_modulus = shear_modulus + self.area = area + self._load_vector = [0, 0, 0] + self._moment_load_vector = [0, 0, 0] + self._torsion_moment = {} + self._load_Singularity = [0, 0, 0] + self._slope = [0, 0, 0] + self._deflection = [0, 0, 0] + self._angular_deflection = 0 + + @property + def shear_modulus(self): + """Young's Modulus of the Beam. """ + return self._shear_modulus + + @shear_modulus.setter + def shear_modulus(self, e): + self._shear_modulus = sympify(e) + + @property + def second_moment(self): + """Second moment of area of the Beam. """ + return self._second_moment + + @second_moment.setter + def second_moment(self, i): + if isinstance(i, list): + i = [sympify(x) for x in i] + self._second_moment = i + else: + self._second_moment = sympify(i) + + @property + def area(self): + """Cross-sectional area of the Beam. """ + return self._area + + @area.setter + def area(self, a): + self._area = sympify(a) + + @property + def load_vector(self): + """ + Returns a three element list representing the load vector. + """ + return self._load_vector + + @property + def moment_load_vector(self): + """ + Returns a three element list representing moment loads on Beam. + """ + return self._moment_load_vector + + @property + def boundary_conditions(self): + """ + Returns a dictionary of boundary conditions applied on the beam. + The dictionary has two keywords namely slope and deflection. + The value of each keyword is a list of tuple, where each tuple + contains location and value of a boundary condition in the format + (location, value). Further each value is a list corresponding to + slope or deflection(s) values along three axes at that location. + + Examples + ======== + There is a beam of length 4 meters. The slope at 0 should be 4 along + the x-axis and 0 along others. At the other end of beam, deflection + along all the three axes should be zero. + + >>> from sympy.physics.continuum_mechanics.beam import Beam3D + >>> from sympy import symbols + >>> l, E, G, I, A, x = symbols('l, E, G, I, A, x') + >>> b = Beam3D(30, E, G, I, A, x) + >>> b.bc_slope = [(0, (4, 0, 0))] + >>> b.bc_deflection = [(4, [0, 0, 0])] + >>> b.boundary_conditions + {'bending_moment': [], 'deflection': [(4, [0, 0, 0])], 'shear_force': [], 'slope': [(0, (4, 0, 0))]} + + Here the deflection of the beam should be ``0`` along all the three axes at ``4``. + Similarly, the slope of the beam should be ``4`` along x-axis and ``0`` + along y and z axis at ``0``. + """ + return self._boundary_conditions + + def polar_moment(self): + """ + Returns the polar moment of area of the beam + about the X axis with respect to the centroid. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.beam import Beam3D + >>> from sympy import symbols + >>> l, E, G, I, A = symbols('l, E, G, I, A') + >>> b = Beam3D(l, E, G, I, A) + >>> b.polar_moment() + 2*I + >>> I1 = [9, 15] + >>> b = Beam3D(l, E, G, I1, A) + >>> b.polar_moment() + 24 + """ + if not iterable(self.second_moment): + return 2*self.second_moment + return sum(self.second_moment) + + def apply_load(self, value, start, order, dir="y"): + """ + This method adds up the force load to a particular beam object. + + Parameters + ========== + value : Sympifyable + The magnitude of an applied load. + dir : String + Axis along which load is applied. + order : Integer + The order of the applied load. + - For point loads, order=-1 + - For constant distributed load, order=0 + - For ramp loads, order=1 + - For parabolic ramp loads, order=2 + - ... so on. + """ + x = self.variable + value = sympify(value) + start = sympify(start) + order = sympify(order) + + if dir == "x": + if not order == -1: + self._load_vector[0] += value + self._load_Singularity[0] += value*SingularityFunction(x, start, order) + + elif dir == "y": + if not order == -1: + self._load_vector[1] += value + self._load_Singularity[1] += value*SingularityFunction(x, start, order) + + else: + if not order == -1: + self._load_vector[2] += value + self._load_Singularity[2] += value*SingularityFunction(x, start, order) + + def apply_moment_load(self, value, start, order, dir="y"): + """ + This method adds up the moment loads to a particular beam object. + + Parameters + ========== + value : Sympifyable + The magnitude of an applied moment. + dir : String + Axis along which moment is applied. + order : Integer + The order of the applied load. + - For point moments, order=-2 + - For constant distributed moment, order=-1 + - For ramp moments, order=0 + - For parabolic ramp moments, order=1 + - ... so on. + """ + x = self.variable + value = sympify(value) + start = sympify(start) + order = sympify(order) + + if dir == "x": + if not order == -2: + self._moment_load_vector[0] += value + else: + if start in list(self._torsion_moment): + self._torsion_moment[start] += value + else: + self._torsion_moment[start] = value + self._load_Singularity[0] += value*SingularityFunction(x, start, order) + elif dir == "y": + if not order == -2: + self._moment_load_vector[1] += value + self._load_Singularity[0] += value*SingularityFunction(x, start, order) + else: + if not order == -2: + self._moment_load_vector[2] += value + self._load_Singularity[0] += value*SingularityFunction(x, start, order) + + def apply_support(self, loc, type="fixed"): + if type in ("pin", "roller"): + reaction_load = Symbol('R_'+str(loc)) + self._reaction_loads[reaction_load] = reaction_load + self.bc_deflection.append((loc, [0, 0, 0])) + else: + reaction_load = Symbol('R_'+str(loc)) + reaction_moment = Symbol('M_'+str(loc)) + self._reaction_loads[reaction_load] = [reaction_load, reaction_moment] + self.bc_deflection.append((loc, [0, 0, 0])) + self.bc_slope.append((loc, [0, 0, 0])) + + def solve_for_reaction_loads(self, *reaction): + """ + Solves for the reaction forces. + + Examples + ======== + There is a beam of length 30 meters. It it supported by rollers at + of its end. A constant distributed load of magnitude 8 N is applied + from start till its end along y-axis. Another linear load having + slope equal to 9 is applied along z-axis. + + >>> from sympy.physics.continuum_mechanics.beam import Beam3D + >>> from sympy import symbols + >>> l, E, G, I, A, x = symbols('l, E, G, I, A, x') + >>> b = Beam3D(30, E, G, I, A, x) + >>> b.apply_load(8, start=0, order=0, dir="y") + >>> b.apply_load(9*x, start=0, order=0, dir="z") + >>> b.bc_deflection = [(0, [0, 0, 0]), (30, [0, 0, 0])] + >>> R1, R2, R3, R4 = symbols('R1, R2, R3, R4') + >>> b.apply_load(R1, start=0, order=-1, dir="y") + >>> b.apply_load(R2, start=30, order=-1, dir="y") + >>> b.apply_load(R3, start=0, order=-1, dir="z") + >>> b.apply_load(R4, start=30, order=-1, dir="z") + >>> b.solve_for_reaction_loads(R1, R2, R3, R4) + >>> b.reaction_loads + {R1: -120, R2: -120, R3: -1350, R4: -2700} + """ + x = self.variable + l = self.length + q = self._load_Singularity + shear_curves = [integrate(load, x) for load in q] + moment_curves = [integrate(shear, x) for shear in shear_curves] + for i in range(3): + react = [r for r in reaction if (shear_curves[i].has(r) or moment_curves[i].has(r))] + if len(react) == 0: + continue + shear_curve = limit(shear_curves[i], x, l) + moment_curve = limit(moment_curves[i], x, l) + sol = list((linsolve([shear_curve, moment_curve], react).args)[0]) + sol_dict = dict(zip(react, sol)) + reaction_loads = self._reaction_loads + # Check if any of the evaluated reaction exists in another direction + # and if it exists then it should have same value. + for key in sol_dict: + if key in reaction_loads and sol_dict[key] != reaction_loads[key]: + raise ValueError("Ambiguous solution for %s in different directions." % key) + self._reaction_loads.update(sol_dict) + + def shear_force(self): + """ + Returns a list of three expressions which represents the shear force + curve of the Beam object along all three axes. + """ + x = self.variable + q = self._load_vector + return [integrate(-q[0], x), integrate(-q[1], x), integrate(-q[2], x)] + + def axial_force(self): + """ + Returns expression of Axial shear force present inside the Beam object. + """ + return self.shear_force()[0] + + def shear_stress(self): + """ + Returns a list of three expressions which represents the shear stress + curve of the Beam object along all three axes. + """ + return [self.shear_force()[0]/self._area, self.shear_force()[1]/self._area, self.shear_force()[2]/self._area] + + def axial_stress(self): + """ + Returns expression of Axial stress present inside the Beam object. + """ + return self.axial_force()/self._area + + def bending_moment(self): + """ + Returns a list of three expressions which represents the bending moment + curve of the Beam object along all three axes. + """ + x = self.variable + m = self._moment_load_vector + shear = self.shear_force() + + return [integrate(-m[0], x), integrate(-m[1] + shear[2], x), + integrate(-m[2] - shear[1], x) ] + + def torsional_moment(self): + """ + Returns expression of Torsional moment present inside the Beam object. + """ + return self.bending_moment()[0] + + def solve_for_torsion(self): + """ + Solves for the angular deflection due to the torsional effects of + moments being applied in the x-direction i.e. out of or into the beam. + + Here, a positive torque means the direction of the torque is positive + i.e. out of the beam along the beam-axis. Likewise, a negative torque + signifies a torque into the beam cross-section. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.beam import Beam3D + >>> from sympy import symbols + >>> l, E, G, I, A, x = symbols('l, E, G, I, A, x') + >>> b = Beam3D(20, E, G, I, A, x) + >>> b.apply_moment_load(4, 4, -2, dir='x') + >>> b.apply_moment_load(4, 8, -2, dir='x') + >>> b.apply_moment_load(4, 8, -2, dir='x') + >>> b.solve_for_torsion() + >>> b.angular_deflection().subs(x, 3) + 18/(G*I) + """ + x = self.variable + sum_moments = 0 + for point in list(self._torsion_moment): + sum_moments += self._torsion_moment[point] + list(self._torsion_moment).sort() + pointsList = list(self._torsion_moment) + torque_diagram = Piecewise((sum_moments, x<=pointsList[0]), (0, x>=pointsList[0])) + for i in range(len(pointsList))[1:]: + sum_moments -= self._torsion_moment[pointsList[i-1]] + torque_diagram += Piecewise((0, x<=pointsList[i-1]), (sum_moments, x<=pointsList[i]), (0, x>=pointsList[i])) + integrated_torque_diagram = integrate(torque_diagram) + self._angular_deflection = integrated_torque_diagram/(self.shear_modulus*self.polar_moment()) + + def solve_slope_deflection(self): + x = self.variable + l = self.length + E = self.elastic_modulus + G = self.shear_modulus + I = self.second_moment + if isinstance(I, list): + I_y, I_z = I[0], I[1] + else: + I_y = I_z = I + A = self._area + load = self._load_vector + moment = self._moment_load_vector + defl = Function('defl') + theta = Function('theta') + + # Finding deflection along x-axis(and corresponding slope value by differentiating it) + # Equation used: Derivative(E*A*Derivative(def_x(x), x), x) + load_x = 0 + eq = Derivative(E*A*Derivative(defl(x), x), x) + load[0] + def_x = dsolve(Eq(eq, 0), defl(x)).args[1] + # Solving constants originated from dsolve + C1 = Symbol('C1') + C2 = Symbol('C2') + constants = list((linsolve([def_x.subs(x, 0), def_x.subs(x, l)], C1, C2).args)[0]) + def_x = def_x.subs({C1:constants[0], C2:constants[1]}) + slope_x = def_x.diff(x) + self._deflection[0] = def_x + self._slope[0] = slope_x + + # Finding deflection along y-axis and slope across z-axis. System of equation involved: + # 1: Derivative(E*I_z*Derivative(theta_z(x), x), x) + G*A*(Derivative(defl_y(x), x) - theta_z(x)) + moment_z = 0 + # 2: Derivative(G*A*(Derivative(defl_y(x), x) - theta_z(x)), x) + load_y = 0 + C_i = Symbol('C_i') + # Substitute value of `G*A*(Derivative(defl_y(x), x) - theta_z(x))` from (2) in (1) + eq1 = Derivative(E*I_z*Derivative(theta(x), x), x) + (integrate(-load[1], x) + C_i) + moment[2] + slope_z = dsolve(Eq(eq1, 0)).args[1] + + # Solve for constants originated from using dsolve on eq1 + constants = list((linsolve([slope_z.subs(x, 0), slope_z.subs(x, l)], C1, C2).args)[0]) + slope_z = slope_z.subs({C1:constants[0], C2:constants[1]}) + + # Put value of slope obtained back in (2) to solve for `C_i` and find deflection across y-axis + eq2 = G*A*(Derivative(defl(x), x)) + load[1]*x - C_i - G*A*slope_z + def_y = dsolve(Eq(eq2, 0), defl(x)).args[1] + # Solve for constants originated from using dsolve on eq2 + constants = list((linsolve([def_y.subs(x, 0), def_y.subs(x, l)], C1, C_i).args)[0]) + self._deflection[1] = def_y.subs({C1:constants[0], C_i:constants[1]}) + self._slope[2] = slope_z.subs(C_i, constants[1]) + + # Finding deflection along z-axis and slope across y-axis. System of equation involved: + # 1: Derivative(E*I_y*Derivative(theta_y(x), x), x) - G*A*(Derivative(defl_z(x), x) + theta_y(x)) + moment_y = 0 + # 2: Derivative(G*A*(Derivative(defl_z(x), x) + theta_y(x)), x) + load_z = 0 + + # Substitute value of `G*A*(Derivative(defl_y(x), x) + theta_z(x))` from (2) in (1) + eq1 = Derivative(E*I_y*Derivative(theta(x), x), x) + (integrate(load[2], x) - C_i) + moment[1] + slope_y = dsolve(Eq(eq1, 0)).args[1] + # Solve for constants originated from using dsolve on eq1 + constants = list((linsolve([slope_y.subs(x, 0), slope_y.subs(x, l)], C1, C2).args)[0]) + slope_y = slope_y.subs({C1:constants[0], C2:constants[1]}) + + # Put value of slope obtained back in (2) to solve for `C_i` and find deflection across z-axis + eq2 = G*A*(Derivative(defl(x), x)) + load[2]*x - C_i + G*A*slope_y + def_z = dsolve(Eq(eq2,0)).args[1] + # Solve for constants originated from using dsolve on eq2 + constants = list((linsolve([def_z.subs(x, 0), def_z.subs(x, l)], C1, C_i).args)[0]) + self._deflection[2] = def_z.subs({C1:constants[0], C_i:constants[1]}) + self._slope[1] = slope_y.subs(C_i, constants[1]) + + def slope(self): + """ + Returns a three element list representing slope of deflection curve + along all the three axes. + """ + return self._slope + + def deflection(self): + """ + Returns a three element list representing deflection curve along all + the three axes. + """ + return self._deflection + + def angular_deflection(self): + """ + Returns a function in x depicting how the angular deflection, due to moments + in the x-axis on the beam, varies with x. + """ + return self._angular_deflection + + def _plot_shear_force(self, dir, subs=None): + + shear_force = self.shear_force() + + if dir == 'x': + dir_num = 0 + color = 'r' + + elif dir == 'y': + dir_num = 1 + color = 'g' + + elif dir == 'z': + dir_num = 2 + color = 'b' + + if subs is None: + subs = {} + + for sym in shear_force[dir_num].atoms(Symbol): + if sym != self.variable and sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + if self.length in subs: + length = subs[self.length] + else: + length = self.length + + return plot(shear_force[dir_num].subs(subs), (self.variable, 0, length), show = False, title='Shear Force along %c direction'%dir, + xlabel=r'$\mathrm{X}$', ylabel=r'$\mathrm{V(%c)}$'%dir, line_color=color) + + def plot_shear_force(self, dir="all", subs=None): + + """ + + Returns a plot for Shear force along all three directions + present in the Beam object. + + Parameters + ========== + dir : string (default : "all") + Direction along which shear force plot is required. + If no direction is specified, all plots are displayed. + subs : dictionary + Python dictionary containing Symbols as key and their + corresponding values. + + Examples + ======== + There is a beam of length 20 meters. It is supported by rollers + at both of its ends. A linear load having slope equal to 12 is applied + along y-axis. A constant distributed load of magnitude 15 N is + applied from start till its end along z-axis. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.physics.continuum_mechanics.beam import Beam3D + >>> from sympy import symbols + >>> l, E, G, I, A, x = symbols('l, E, G, I, A, x') + >>> b = Beam3D(20, E, G, I, A, x) + >>> b.apply_load(15, start=0, order=0, dir="z") + >>> b.apply_load(12*x, start=0, order=0, dir="y") + >>> b.bc_deflection = [(0, [0, 0, 0]), (20, [0, 0, 0])] + >>> R1, R2, R3, R4 = symbols('R1, R2, R3, R4') + >>> b.apply_load(R1, start=0, order=-1, dir="z") + >>> b.apply_load(R2, start=20, order=-1, dir="z") + >>> b.apply_load(R3, start=0, order=-1, dir="y") + >>> b.apply_load(R4, start=20, order=-1, dir="y") + >>> b.solve_for_reaction_loads(R1, R2, R3, R4) + >>> b.plot_shear_force() + PlotGrid object containing: + Plot[0]:Plot object containing: + [0]: cartesian line: 0 for x over (0.0, 20.0) + Plot[1]:Plot object containing: + [0]: cartesian line: -6*x**2 for x over (0.0, 20.0) + Plot[2]:Plot object containing: + [0]: cartesian line: -15*x for x over (0.0, 20.0) + + """ + + dir = dir.lower() + # For shear force along x direction + if dir == "x": + Px = self._plot_shear_force('x', subs) + return Px.show() + # For shear force along y direction + elif dir == "y": + Py = self._plot_shear_force('y', subs) + return Py.show() + # For shear force along z direction + elif dir == "z": + Pz = self._plot_shear_force('z', subs) + return Pz.show() + # For shear force along all direction + else: + Px = self._plot_shear_force('x', subs) + Py = self._plot_shear_force('y', subs) + Pz = self._plot_shear_force('z', subs) + return PlotGrid(3, 1, Px, Py, Pz) + + def _plot_bending_moment(self, dir, subs=None): + + bending_moment = self.bending_moment() + + if dir == 'x': + dir_num = 0 + color = 'g' + + elif dir == 'y': + dir_num = 1 + color = 'c' + + elif dir == 'z': + dir_num = 2 + color = 'm' + + if subs is None: + subs = {} + + for sym in bending_moment[dir_num].atoms(Symbol): + if sym != self.variable and sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + if self.length in subs: + length = subs[self.length] + else: + length = self.length + + return plot(bending_moment[dir_num].subs(subs), (self.variable, 0, length), show = False, title='Bending Moment along %c direction'%dir, + xlabel=r'$\mathrm{X}$', ylabel=r'$\mathrm{M(%c)}$'%dir, line_color=color) + + def plot_bending_moment(self, dir="all", subs=None): + + """ + + Returns a plot for bending moment along all three directions + present in the Beam object. + + Parameters + ========== + dir : string (default : "all") + Direction along which bending moment plot is required. + If no direction is specified, all plots are displayed. + subs : dictionary + Python dictionary containing Symbols as key and their + corresponding values. + + Examples + ======== + There is a beam of length 20 meters. It is supported by rollers + at both of its ends. A linear load having slope equal to 12 is applied + along y-axis. A constant distributed load of magnitude 15 N is + applied from start till its end along z-axis. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.physics.continuum_mechanics.beam import Beam3D + >>> from sympy import symbols + >>> l, E, G, I, A, x = symbols('l, E, G, I, A, x') + >>> b = Beam3D(20, E, G, I, A, x) + >>> b.apply_load(15, start=0, order=0, dir="z") + >>> b.apply_load(12*x, start=0, order=0, dir="y") + >>> b.bc_deflection = [(0, [0, 0, 0]), (20, [0, 0, 0])] + >>> R1, R2, R3, R4 = symbols('R1, R2, R3, R4') + >>> b.apply_load(R1, start=0, order=-1, dir="z") + >>> b.apply_load(R2, start=20, order=-1, dir="z") + >>> b.apply_load(R3, start=0, order=-1, dir="y") + >>> b.apply_load(R4, start=20, order=-1, dir="y") + >>> b.solve_for_reaction_loads(R1, R2, R3, R4) + >>> b.plot_bending_moment() + PlotGrid object containing: + Plot[0]:Plot object containing: + [0]: cartesian line: 0 for x over (0.0, 20.0) + Plot[1]:Plot object containing: + [0]: cartesian line: -15*x**2/2 for x over (0.0, 20.0) + Plot[2]:Plot object containing: + [0]: cartesian line: 2*x**3 for x over (0.0, 20.0) + + """ + + dir = dir.lower() + # For bending moment along x direction + if dir == "x": + Px = self._plot_bending_moment('x', subs) + return Px.show() + # For bending moment along y direction + elif dir == "y": + Py = self._plot_bending_moment('y', subs) + return Py.show() + # For bending moment along z direction + elif dir == "z": + Pz = self._plot_bending_moment('z', subs) + return Pz.show() + # For bending moment along all direction + else: + Px = self._plot_bending_moment('x', subs) + Py = self._plot_bending_moment('y', subs) + Pz = self._plot_bending_moment('z', subs) + return PlotGrid(3, 1, Px, Py, Pz) + + def _plot_slope(self, dir, subs=None): + + slope = self.slope() + + if dir == 'x': + dir_num = 0 + color = 'b' + + elif dir == 'y': + dir_num = 1 + color = 'm' + + elif dir == 'z': + dir_num = 2 + color = 'g' + + if subs is None: + subs = {} + + for sym in slope[dir_num].atoms(Symbol): + if sym != self.variable and sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + if self.length in subs: + length = subs[self.length] + else: + length = self.length + + + return plot(slope[dir_num].subs(subs), (self.variable, 0, length), show = False, title='Slope along %c direction'%dir, + xlabel=r'$\mathrm{X}$', ylabel=r'$\mathrm{\theta(%c)}$'%dir, line_color=color) + + def plot_slope(self, dir="all", subs=None): + + """ + + Returns a plot for Slope along all three directions + present in the Beam object. + + Parameters + ========== + dir : string (default : "all") + Direction along which Slope plot is required. + If no direction is specified, all plots are displayed. + subs : dictionary + Python dictionary containing Symbols as keys and their + corresponding values. + + Examples + ======== + There is a beam of length 20 meters. It is supported by rollers + at both of its ends. A linear load having slope equal to 12 is applied + along y-axis. A constant distributed load of magnitude 15 N is + applied from start till its end along z-axis. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.physics.continuum_mechanics.beam import Beam3D + >>> from sympy import symbols + >>> l, E, G, I, A, x = symbols('l, E, G, I, A, x') + >>> b = Beam3D(20, 40, 21, 100, 25, x) + >>> b.apply_load(15, start=0, order=0, dir="z") + >>> b.apply_load(12*x, start=0, order=0, dir="y") + >>> b.bc_deflection = [(0, [0, 0, 0]), (20, [0, 0, 0])] + >>> R1, R2, R3, R4 = symbols('R1, R2, R3, R4') + >>> b.apply_load(R1, start=0, order=-1, dir="z") + >>> b.apply_load(R2, start=20, order=-1, dir="z") + >>> b.apply_load(R3, start=0, order=-1, dir="y") + >>> b.apply_load(R4, start=20, order=-1, dir="y") + >>> b.solve_for_reaction_loads(R1, R2, R3, R4) + >>> b.solve_slope_deflection() + >>> b.plot_slope() + PlotGrid object containing: + Plot[0]:Plot object containing: + [0]: cartesian line: 0 for x over (0.0, 20.0) + Plot[1]:Plot object containing: + [0]: cartesian line: -x**3/1600 + 3*x**2/160 - x/8 for x over (0.0, 20.0) + Plot[2]:Plot object containing: + [0]: cartesian line: x**4/8000 - 19*x**2/172 + 52*x/43 for x over (0.0, 20.0) + + """ + + dir = dir.lower() + # For Slope along x direction + if dir == "x": + Px = self._plot_slope('x', subs) + return Px.show() + # For Slope along y direction + elif dir == "y": + Py = self._plot_slope('y', subs) + return Py.show() + # For Slope along z direction + elif dir == "z": + Pz = self._plot_slope('z', subs) + return Pz.show() + # For Slope along all direction + else: + Px = self._plot_slope('x', subs) + Py = self._plot_slope('y', subs) + Pz = self._plot_slope('z', subs) + return PlotGrid(3, 1, Px, Py, Pz) + + def _plot_deflection(self, dir, subs=None): + + deflection = self.deflection() + + if dir == 'x': + dir_num = 0 + color = 'm' + + elif dir == 'y': + dir_num = 1 + color = 'r' + + elif dir == 'z': + dir_num = 2 + color = 'c' + + if subs is None: + subs = {} + + for sym in deflection[dir_num].atoms(Symbol): + if sym != self.variable and sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + if self.length in subs: + length = subs[self.length] + else: + length = self.length + + return plot(deflection[dir_num].subs(subs), (self.variable, 0, length), show = False, title='Deflection along %c direction'%dir, + xlabel=r'$\mathrm{X}$', ylabel=r'$\mathrm{\delta(%c)}$'%dir, line_color=color) + + def plot_deflection(self, dir="all", subs=None): + + """ + + Returns a plot for Deflection along all three directions + present in the Beam object. + + Parameters + ========== + dir : string (default : "all") + Direction along which deflection plot is required. + If no direction is specified, all plots are displayed. + subs : dictionary + Python dictionary containing Symbols as keys and their + corresponding values. + + Examples + ======== + There is a beam of length 20 meters. It is supported by rollers + at both of its ends. A linear load having slope equal to 12 is applied + along y-axis. A constant distributed load of magnitude 15 N is + applied from start till its end along z-axis. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.physics.continuum_mechanics.beam import Beam3D + >>> from sympy import symbols + >>> l, E, G, I, A, x = symbols('l, E, G, I, A, x') + >>> b = Beam3D(20, 40, 21, 100, 25, x) + >>> b.apply_load(15, start=0, order=0, dir="z") + >>> b.apply_load(12*x, start=0, order=0, dir="y") + >>> b.bc_deflection = [(0, [0, 0, 0]), (20, [0, 0, 0])] + >>> R1, R2, R3, R4 = symbols('R1, R2, R3, R4') + >>> b.apply_load(R1, start=0, order=-1, dir="z") + >>> b.apply_load(R2, start=20, order=-1, dir="z") + >>> b.apply_load(R3, start=0, order=-1, dir="y") + >>> b.apply_load(R4, start=20, order=-1, dir="y") + >>> b.solve_for_reaction_loads(R1, R2, R3, R4) + >>> b.solve_slope_deflection() + >>> b.plot_deflection() + PlotGrid object containing: + Plot[0]:Plot object containing: + [0]: cartesian line: 0 for x over (0.0, 20.0) + Plot[1]:Plot object containing: + [0]: cartesian line: x**5/40000 - 4013*x**3/90300 + 26*x**2/43 + 1520*x/903 for x over (0.0, 20.0) + Plot[2]:Plot object containing: + [0]: cartesian line: x**4/6400 - x**3/160 + 27*x**2/560 + 2*x/7 for x over (0.0, 20.0) + + + """ + + dir = dir.lower() + # For deflection along x direction + if dir == "x": + Px = self._plot_deflection('x', subs) + return Px.show() + # For deflection along y direction + elif dir == "y": + Py = self._plot_deflection('y', subs) + return Py.show() + # For deflection along z direction + elif dir == "z": + Pz = self._plot_deflection('z', subs) + return Pz.show() + # For deflection along all direction + else: + Px = self._plot_deflection('x', subs) + Py = self._plot_deflection('y', subs) + Pz = self._plot_deflection('z', subs) + return PlotGrid(3, 1, Px, Py, Pz) + + def plot_loading_results(self, dir='x', subs=None): + + """ + + Returns a subplot of Shear Force, Bending Moment, + Slope and Deflection of the Beam object along the direction specified. + + Parameters + ========== + + dir : string (default : "x") + Direction along which plots are required. + If no direction is specified, plots along x-axis are displayed. + subs : dictionary + Python dictionary containing Symbols as key and their + corresponding values. + + Examples + ======== + There is a beam of length 20 meters. It is supported by rollers + at both of its ends. A linear load having slope equal to 12 is applied + along y-axis. A constant distributed load of magnitude 15 N is + applied from start till its end along z-axis. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.physics.continuum_mechanics.beam import Beam3D + >>> from sympy import symbols + >>> l, E, G, I, A, x = symbols('l, E, G, I, A, x') + >>> b = Beam3D(20, E, G, I, A, x) + >>> subs = {E:40, G:21, I:100, A:25} + >>> b.apply_load(15, start=0, order=0, dir="z") + >>> b.apply_load(12*x, start=0, order=0, dir="y") + >>> b.bc_deflection = [(0, [0, 0, 0]), (20, [0, 0, 0])] + >>> R1, R2, R3, R4 = symbols('R1, R2, R3, R4') + >>> b.apply_load(R1, start=0, order=-1, dir="z") + >>> b.apply_load(R2, start=20, order=-1, dir="z") + >>> b.apply_load(R3, start=0, order=-1, dir="y") + >>> b.apply_load(R4, start=20, order=-1, dir="y") + >>> b.solve_for_reaction_loads(R1, R2, R3, R4) + >>> b.solve_slope_deflection() + >>> b.plot_loading_results('y',subs) + PlotGrid object containing: + Plot[0]:Plot object containing: + [0]: cartesian line: -6*x**2 for x over (0.0, 20.0) + Plot[1]:Plot object containing: + [0]: cartesian line: -15*x**2/2 for x over (0.0, 20.0) + Plot[2]:Plot object containing: + [0]: cartesian line: -x**3/1600 + 3*x**2/160 - x/8 for x over (0.0, 20.0) + Plot[3]:Plot object containing: + [0]: cartesian line: x**5/40000 - 4013*x**3/90300 + 26*x**2/43 + 1520*x/903 for x over (0.0, 20.0) + + """ + + dir = dir.lower() + if subs is None: + subs = {} + + ax1 = self._plot_shear_force(dir, subs) + ax2 = self._plot_bending_moment(dir, subs) + ax3 = self._plot_slope(dir, subs) + ax4 = self._plot_deflection(dir, subs) + + return PlotGrid(4, 1, ax1, ax2, ax3, ax4) + + def _plot_shear_stress(self, dir, subs=None): + + shear_stress = self.shear_stress() + + if dir == 'x': + dir_num = 0 + color = 'r' + + elif dir == 'y': + dir_num = 1 + color = 'g' + + elif dir == 'z': + dir_num = 2 + color = 'b' + + if subs is None: + subs = {} + + for sym in shear_stress[dir_num].atoms(Symbol): + if sym != self.variable and sym not in subs: + raise ValueError('Value of %s was not passed.' %sym) + if self.length in subs: + length = subs[self.length] + else: + length = self.length + + return plot(shear_stress[dir_num].subs(subs), (self.variable, 0, length), show = False, title='Shear stress along %c direction'%dir, + xlabel=r'$\mathrm{X}$', ylabel=r'$\tau(%c)$'%dir, line_color=color) + + def plot_shear_stress(self, dir="all", subs=None): + + """ + + Returns a plot for Shear Stress along all three directions + present in the Beam object. + + Parameters + ========== + dir : string (default : "all") + Direction along which shear stress plot is required. + If no direction is specified, all plots are displayed. + subs : dictionary + Python dictionary containing Symbols as key and their + corresponding values. + + Examples + ======== + There is a beam of length 20 meters and area of cross section 2 square + meters. It is supported by rollers at both of its ends. A linear load having + slope equal to 12 is applied along y-axis. A constant distributed load + of magnitude 15 N is applied from start till its end along z-axis. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.physics.continuum_mechanics.beam import Beam3D + >>> from sympy import symbols + >>> l, E, G, I, A, x = symbols('l, E, G, I, A, x') + >>> b = Beam3D(20, E, G, I, 2, x) + >>> b.apply_load(15, start=0, order=0, dir="z") + >>> b.apply_load(12*x, start=0, order=0, dir="y") + >>> b.bc_deflection = [(0, [0, 0, 0]), (20, [0, 0, 0])] + >>> R1, R2, R3, R4 = symbols('R1, R2, R3, R4') + >>> b.apply_load(R1, start=0, order=-1, dir="z") + >>> b.apply_load(R2, start=20, order=-1, dir="z") + >>> b.apply_load(R3, start=0, order=-1, dir="y") + >>> b.apply_load(R4, start=20, order=-1, dir="y") + >>> b.solve_for_reaction_loads(R1, R2, R3, R4) + >>> b.plot_shear_stress() + PlotGrid object containing: + Plot[0]:Plot object containing: + [0]: cartesian line: 0 for x over (0.0, 20.0) + Plot[1]:Plot object containing: + [0]: cartesian line: -3*x**2 for x over (0.0, 20.0) + Plot[2]:Plot object containing: + [0]: cartesian line: -15*x/2 for x over (0.0, 20.0) + + """ + + dir = dir.lower() + # For shear stress along x direction + if dir == "x": + Px = self._plot_shear_stress('x', subs) + return Px.show() + # For shear stress along y direction + elif dir == "y": + Py = self._plot_shear_stress('y', subs) + return Py.show() + # For shear stress along z direction + elif dir == "z": + Pz = self._plot_shear_stress('z', subs) + return Pz.show() + # For shear stress along all direction + else: + Px = self._plot_shear_stress('x', subs) + Py = self._plot_shear_stress('y', subs) + Pz = self._plot_shear_stress('z', subs) + return PlotGrid(3, 1, Px, Py, Pz) + + def _max_shear_force(self, dir): + """ + Helper function for max_shear_force(). + """ + + dir = dir.lower() + + if dir == 'x': + dir_num = 0 + + elif dir == 'y': + dir_num = 1 + + elif dir == 'z': + dir_num = 2 + + if not self.shear_force()[dir_num]: + return (0,0) + # To restrict the range within length of the Beam + load_curve = Piecewise((float("nan"), self.variable<=0), + (self._load_vector[dir_num], self.variable>> from sympy.physics.continuum_mechanics.beam import Beam3D + >>> from sympy import symbols + >>> l, E, G, I, A, x = symbols('l, E, G, I, A, x') + >>> b = Beam3D(20, 40, 21, 100, 25, x) + >>> b.apply_load(15, start=0, order=0, dir="z") + >>> b.apply_load(12*x, start=0, order=0, dir="y") + >>> b.bc_deflection = [(0, [0, 0, 0]), (20, [0, 0, 0])] + >>> R1, R2, R3, R4 = symbols('R1, R2, R3, R4') + >>> b.apply_load(R1, start=0, order=-1, dir="z") + >>> b.apply_load(R2, start=20, order=-1, dir="z") + >>> b.apply_load(R3, start=0, order=-1, dir="y") + >>> b.apply_load(R4, start=20, order=-1, dir="y") + >>> b.solve_for_reaction_loads(R1, R2, R3, R4) + >>> b.max_shear_force() + [(0, 0), (20, 2400), (20, 300)] + """ + + max_shear = [] + max_shear.append(self._max_shear_force('x')) + max_shear.append(self._max_shear_force('y')) + max_shear.append(self._max_shear_force('z')) + return max_shear + + def _max_bending_moment(self, dir): + """ + Helper function for max_bending_moment(). + """ + + dir = dir.lower() + + if dir == 'x': + dir_num = 0 + + elif dir == 'y': + dir_num = 1 + + elif dir == 'z': + dir_num = 2 + + if not self.bending_moment()[dir_num]: + return (0,0) + # To restrict the range within length of the Beam + shear_curve = Piecewise((float("nan"), self.variable<=0), + (self.shear_force()[dir_num], self.variable>> from sympy.physics.continuum_mechanics.beam import Beam3D + >>> from sympy import symbols + >>> l, E, G, I, A, x = symbols('l, E, G, I, A, x') + >>> b = Beam3D(20, 40, 21, 100, 25, x) + >>> b.apply_load(15, start=0, order=0, dir="z") + >>> b.apply_load(12*x, start=0, order=0, dir="y") + >>> b.bc_deflection = [(0, [0, 0, 0]), (20, [0, 0, 0])] + >>> R1, R2, R3, R4 = symbols('R1, R2, R3, R4') + >>> b.apply_load(R1, start=0, order=-1, dir="z") + >>> b.apply_load(R2, start=20, order=-1, dir="z") + >>> b.apply_load(R3, start=0, order=-1, dir="y") + >>> b.apply_load(R4, start=20, order=-1, dir="y") + >>> b.solve_for_reaction_loads(R1, R2, R3, R4) + >>> b.max_bending_moment() + [(0, 0), (20, 3000), (20, 16000)] + """ + + max_bmoment = [] + max_bmoment.append(self._max_bending_moment('x')) + max_bmoment.append(self._max_bending_moment('y')) + max_bmoment.append(self._max_bending_moment('z')) + return max_bmoment + + max_bmoment = max_bending_moment + + def _max_deflection(self, dir): + """ + Helper function for max_Deflection() + """ + + dir = dir.lower() + + if dir == 'x': + dir_num = 0 + + elif dir == 'y': + dir_num = 1 + + elif dir == 'z': + dir_num = 2 + + if not self.deflection()[dir_num]: + return (0,0) + # To restrict the range within length of the Beam + slope_curve = Piecewise((float("nan"), self.variable<=0), + (self.slope()[dir_num], self.variable>> from sympy.physics.continuum_mechanics.beam import Beam3D + >>> from sympy import symbols + >>> l, E, G, I, A, x = symbols('l, E, G, I, A, x') + >>> b = Beam3D(20, 40, 21, 100, 25, x) + >>> b.apply_load(15, start=0, order=0, dir="z") + >>> b.apply_load(12*x, start=0, order=0, dir="y") + >>> b.bc_deflection = [(0, [0, 0, 0]), (20, [0, 0, 0])] + >>> R1, R2, R3, R4 = symbols('R1, R2, R3, R4') + >>> b.apply_load(R1, start=0, order=-1, dir="z") + >>> b.apply_load(R2, start=20, order=-1, dir="z") + >>> b.apply_load(R3, start=0, order=-1, dir="y") + >>> b.apply_load(R4, start=20, order=-1, dir="y") + >>> b.solve_for_reaction_loads(R1, R2, R3, R4) + >>> b.solve_slope_deflection() + >>> b.max_deflection() + [(0, 0), (10, 495/14), (-10 + 10*sqrt(10793)/43, (10 - 10*sqrt(10793)/43)**3/160 - 20/7 + (10 - 10*sqrt(10793)/43)**4/6400 + 20*sqrt(10793)/301 + 27*(10 - 10*sqrt(10793)/43)**2/560)] + """ + + max_def = [] + max_def.append(self._max_deflection('x')) + max_def.append(self._max_deflection('y')) + max_def.append(self._max_deflection('z')) + return max_def diff --git a/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/cable.py b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/cable.py new file mode 100644 index 0000000000000000000000000000000000000000..e38c6601b0a12cad83bc7e87597e79937f4667a4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/cable.py @@ -0,0 +1,815 @@ +""" +This module can be used to solve problems related +to 2D Cables. +""" + +from sympy.core.sympify import sympify +from sympy.core.symbol import Symbol,symbols +from sympy import sin, cos, pi, atan, diff, Piecewise, solve, rad +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.solvers.solveset import linsolve +from sympy.matrices import Matrix +from sympy.plotting import plot + +class Cable: + """ + Cables are structures in engineering that support + the applied transverse loads through the tensile + resistance developed in its members. + + Cables are widely used in suspension bridges, tension + leg offshore platforms, transmission lines, and find + use in several other engineering applications. + + Examples + ======== + A cable is supported at (0, 10) and (10, 10). Two point loads + acting vertically downwards act on the cable, one with magnitude 3 kN + and acting 2 meters from the left support and 3 meters below it, while + the other with magnitude 2 kN is 6 meters from the left support and + 6 meters below it. + + >>> from sympy.physics.continuum_mechanics.cable import Cable + >>> c = Cable(('A', 0, 10), ('B', 10, 10)) + >>> c.apply_load(-1, ('P', 2, 7, 3, 270)) + >>> c.apply_load(-1, ('Q', 6, 4, 2, 270)) + >>> c.loads + {'distributed': {}, 'point_load': {'P': [3, 270], 'Q': [2, 270]}} + >>> c.loads_position + {'P': [2, 7], 'Q': [6, 4]} + """ + def __init__(self, support_1, support_2): + """ + Initializes the class. + + Parameters + ========== + + support_1 and support_2 are tuples of the form + (label, x, y), where + + label : String or symbol + The label of the support + + x : Sympifyable + The x coordinate of the position of the support + + y : Sympifyable + The y coordinate of the position of the support + """ + self._left_support = [] + self._right_support = [] + self._supports = {} + self._support_labels = [] + self._loads = {"distributed": {}, "point_load": {}} + self._loads_position = {} + self._length = 0 + self._reaction_loads = {} + self._tension = {} + self._lowest_x_global = sympify(0) + self._lowest_y_global = sympify(0) + self._cable_eqn = None + self._tension_func = None + if support_1[0] == support_2[0]: + raise ValueError("Supports can not have the same label") + + elif support_1[1] == support_2[1]: + raise ValueError("Supports can not be at the same location") + + x1 = sympify(support_1[1]) + y1 = sympify(support_1[2]) + self._supports[support_1[0]] = [x1, y1] + + x2 = sympify(support_2[1]) + y2 = sympify(support_2[2]) + self._supports[support_2[0]] = [x2, y2] + + if support_1[1] < support_2[1]: + self._left_support.append(x1) + self._left_support.append(y1) + self._right_support.append(x2) + self._right_support.append(y2) + self._support_labels.append(support_1[0]) + self._support_labels.append(support_2[0]) + + else: + self._left_support.append(x2) + self._left_support.append(y2) + self._right_support.append(x1) + self._right_support.append(y1) + self._support_labels.append(support_2[0]) + self._support_labels.append(support_1[0]) + + for i in self._support_labels: + self._reaction_loads[Symbol("R_"+ i +"_x")] = 0 + self._reaction_loads[Symbol("R_"+ i +"_y")] = 0 + + @property + def supports(self): + """ + Returns the supports of the cable along with their + positions. + """ + return self._supports + + @property + def left_support(self): + """ + Returns the position of the left support. + """ + return self._left_support + + @property + def right_support(self): + """ + Returns the position of the right support. + """ + return self._right_support + + @property + def loads(self): + """ + Returns the magnitude and direction of the loads + acting on the cable. + """ + return self._loads + + @property + def loads_position(self): + """ + Returns the position of the point loads acting on the + cable. + """ + return self._loads_position + + @property + def length(self): + """ + Returns the length of the cable. + """ + return self._length + + @property + def reaction_loads(self): + """ + Returns the reaction forces at the supports, which are + initialized to 0. + """ + return self._reaction_loads + + @property + def tension(self): + """ + Returns the tension developed in the cable due to the loads + applied. + """ + return self._tension + + def tension_at(self, x): + """ + Returns the tension at a given value of x developed due to + distributed load. + """ + if 'distributed' not in self._tension.keys(): + raise ValueError("No distributed load added or solve method not called") + + if x > self._right_support[0] or x < self._left_support[0]: + raise ValueError("The value of x should be between the two supports") + + A = self._tension['distributed'] + X = Symbol('X') + + return A.subs({X:(x-self._lowest_x_global)}) + + def apply_length(self, length): + """ + This method specifies the length of the cable + + Parameters + ========== + + length : Sympifyable + The length of the cable + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.cable import Cable + >>> c = Cable(('A', 0, 10), ('B', 10, 10)) + >>> c.apply_length(20) + >>> c.length + 20 + """ + dist = ((self._left_support[0] - self._right_support[0])**2 + - (self._left_support[1] - self._right_support[1])**2)**(1/2) + + if length < dist: + raise ValueError("length should not be less than the distance between the supports") + + self._length = length + + def change_support(self, label, new_support): + """ + This method changes the mentioned support with a new support. + + Parameters + ========== + label: String or symbol + The label of the support to be changed + + new_support: Tuple of the form (new_label, x, y) + new_label: String or symbol + The label of the new support + + x: Sympifyable + The x-coordinate of the position of the new support. + + y: Sympifyable + The y-coordinate of the position of the new support. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.cable import Cable + >>> c = Cable(('A', 0, 10), ('B', 10, 10)) + >>> c.supports + {'A': [0, 10], 'B': [10, 10]} + >>> c.change_support('B', ('C', 5, 6)) + >>> c.supports + {'A': [0, 10], 'C': [5, 6]} + """ + if label not in self._supports: + raise ValueError("No support exists with the given label") + + i = self._support_labels.index(label) + rem_label = self._support_labels[(i+1)%2] + x1 = self._supports[rem_label][0] + y1 = self._supports[rem_label][1] + + x = sympify(new_support[1]) + y = sympify(new_support[2]) + + for l in self._loads_position: + if l[0] >= max(x, x1) or l[0] <= min(x, x1): + raise ValueError("The change in support will throw an existing load out of range") + + self._supports.pop(label) + self._left_support.clear() + self._right_support.clear() + self._reaction_loads.clear() + self._support_labels.remove(label) + + self._supports[new_support[0]] = [x, y] + + if x1 < x: + self._left_support.append(x1) + self._left_support.append(y1) + self._right_support.append(x) + self._right_support.append(y) + self._support_labels.append(new_support[0]) + + else: + self._left_support.append(x) + self._left_support.append(y) + self._right_support.append(x1) + self._right_support.append(y1) + self._support_labels.insert(0, new_support[0]) + + for i in self._support_labels: + self._reaction_loads[Symbol("R_"+ i +"_x")] = 0 + self._reaction_loads[Symbol("R_"+ i +"_y")] = 0 + + def apply_load(self, order, load): + """ + This method adds load to the cable. + + Parameters + ========== + + order : Integer + The order of the applied load. + + - For point loads, order = -1 + - For distributed load, order = 0 + + load : tuple + + * For point loads, load is of the form (label, x, y, magnitude, direction), where: + + label : String or symbol + The label of the load + + x : Sympifyable + The x coordinate of the position of the load + + y : Sympifyable + The y coordinate of the position of the load + + magnitude : Sympifyable + The magnitude of the load. It must always be positive + + direction : Sympifyable + The angle, in degrees, that the load vector makes with the horizontal + in the counter-clockwise direction. It takes the values 0 to 360, + inclusive. + + + * For uniformly distributed load, load is of the form (label, magnitude) + + label : String or symbol + The label of the load + + magnitude : Sympifyable + The magnitude of the load. It must always be positive + + Examples + ======== + + For a point load of magnitude 12 units inclined at 30 degrees with the horizontal: + + >>> from sympy.physics.continuum_mechanics.cable import Cable + >>> c = Cable(('A', 0, 10), ('B', 10, 10)) + >>> c.apply_load(-1, ('Z', 5, 5, 12, 30)) + >>> c.loads + {'distributed': {}, 'point_load': {'Z': [12, 30]}} + >>> c.loads_position + {'Z': [5, 5]} + + + For a uniformly distributed load of magnitude 9 units: + + >>> from sympy.physics.continuum_mechanics.cable import Cable + >>> c = Cable(('A', 0, 10), ('B', 10, 10)) + >>> c.apply_load(0, ('X', 9)) + >>> c.loads + {'distributed': {'X': 9}, 'point_load': {}} + """ + if order == -1: + if len(self._loads["distributed"]) != 0: + raise ValueError("Distributed load already exists") + + label = load[0] + if label in self._loads["point_load"]: + raise ValueError("Label already exists") + + x = sympify(load[1]) + y = sympify(load[2]) + + if x > self._right_support[0] or x < self._left_support[0]: + raise ValueError("The load should be positioned between the supports") + + magnitude = sympify(load[3]) + direction = sympify(load[4]) + + self._loads["point_load"][label] = [magnitude, direction] + self._loads_position[label] = [x, y] + + elif order == 0: + if len(self._loads_position) != 0: + raise ValueError("Point load(s) already exist") + + label = load[0] + if label in self._loads["distributed"]: + raise ValueError("Label already exists") + + magnitude = sympify(load[1]) + + self._loads["distributed"][label] = magnitude + + else: + raise ValueError("Order should be either -1 or 0") + + def remove_loads(self, *args): + """ + This methods removes the specified loads. + + Parameters + ========== + This input takes multiple label(s) as input + label(s): String or symbol + The label(s) of the loads to be removed. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.cable import Cable + >>> c = Cable(('A', 0, 10), ('B', 10, 10)) + >>> c.apply_load(-1, ('Z', 5, 5, 12, 30)) + >>> c.loads + {'distributed': {}, 'point_load': {'Z': [12, 30]}} + >>> c.remove_loads('Z') + >>> c.loads + {'distributed': {}, 'point_load': {}} + """ + for i in args: + if len(self._loads_position) == 0: + if i not in self._loads['distributed']: + raise ValueError("Error removing load " + i + ": no such load exists") + + else: + self._loads['disrtibuted'].pop(i) + + else: + if i not in self._loads['point_load']: + raise ValueError("Error removing load " + i + ": no such load exists") + + else: + self._loads['point_load'].pop(i) + self._loads_position.pop(i) + + def solve(self, *args): + """ + This method solves for the reaction forces at the supports, the tension developed in + the cable, and updates the length of the cable. + + Parameters + ========== + This method requires no input when solving for point loads + For distributed load, the x and y coordinates of the lowest point of the cable are + required as + + x: Sympifyable + The x coordinate of the lowest point + + y: Sympifyable + The y coordinate of the lowest point + + Examples + ======== + For point loads, + + >>> from sympy.physics.continuum_mechanics.cable import Cable + >>> c = Cable(("A", 0, 10), ("B", 10, 10)) + >>> c.apply_load(-1, ('Z', 2, 7.26, 3, 270)) + >>> c.apply_load(-1, ('X', 4, 6, 8, 270)) + >>> c.solve() + >>> c.tension + {A_Z: 8.91403453669861, X_B: 19*sqrt(13)/10, Z_X: 4.79150773600774} + >>> c.reaction_loads + {R_A_x: -5.25547445255474, R_A_y: 7.2, R_B_x: 5.25547445255474, R_B_y: 3.8} + >>> c.length + 5.7560958484519 + 2*sqrt(13) + + For distributed load, + + >>> from sympy.physics.continuum_mechanics.cable import Cable + >>> c=Cable(("A", 0, 40),("B", 100, 20)) + >>> c.apply_load(0, ("X", 850)) + >>> c.solve(58.58) + >>> c.tension + {'distributed': 36465.0*sqrt(0.00054335718671383*X**2 + 1)} + >>> c.tension_at(0) + 61717.4130533677 + >>> c.reaction_loads + {R_A_x: 36465.0, R_A_y: -49793.0, R_B_x: 44399.9537590861, R_B_y: 42868.2071025955} + """ + + if len(self._loads_position) != 0: + sorted_position = sorted(self._loads_position.items(), key = lambda item : item[1][0]) + + sorted_position.append(self._support_labels[1]) + sorted_position.insert(0, self._support_labels[0]) + + self._tension.clear() + moment_sum_from_left_support = 0 + moment_sum_from_right_support = 0 + F_x = 0 + F_y = 0 + self._length = 0 + tension_func = [] + x = symbols('x') + for i in range(1, len(sorted_position)-1): + if i == 1: + self._length+=sqrt((self._left_support[0] - self._loads_position[sorted_position[i][0]][0])**2 + (self._left_support[1] - self._loads_position[sorted_position[i][0]][1])**2) + + else: + self._length+=sqrt((self._loads_position[sorted_position[i-1][0]][0] - self._loads_position[sorted_position[i][0]][0])**2 + (self._loads_position[sorted_position[i-1][0]][1] - self._loads_position[sorted_position[i][0]][1])**2) + + if i == len(sorted_position)-2: + self._length+=sqrt((self._right_support[0] - self._loads_position[sorted_position[i][0]][0])**2 + (self._right_support[1] - self._loads_position[sorted_position[i][0]][1])**2) + + moment_sum_from_left_support += self._loads['point_load'][sorted_position[i][0]][0] * cos(pi * self._loads['point_load'][sorted_position[i][0]][1] / 180) * abs(self._left_support[1] - self._loads_position[sorted_position[i][0]][1]) + moment_sum_from_left_support += self._loads['point_load'][sorted_position[i][0]][0] * sin(pi * self._loads['point_load'][sorted_position[i][0]][1] / 180) * abs(self._left_support[0] - self._loads_position[sorted_position[i][0]][0]) + + F_x += self._loads['point_load'][sorted_position[i][0]][0] * cos(pi * self._loads['point_load'][sorted_position[i][0]][1] / 180) + F_y += self._loads['point_load'][sorted_position[i][0]][0] * sin(pi * self._loads['point_load'][sorted_position[i][0]][1] / 180) + + label = Symbol(sorted_position[i][0]+"_"+sorted_position[i+1][0]) + y2 = self._loads_position[sorted_position[i][0]][1] + x2 = self._loads_position[sorted_position[i][0]][0] + y1 = 0 + x1 = 0 + + if i == len(sorted_position)-2: + x1 = self._right_support[0] + y1 = self._right_support[1] + + else: + x1 = self._loads_position[sorted_position[i+1][0]][0] + y1 = self._loads_position[sorted_position[i+1][0]][1] + + angle_with_horizontal = atan((y1 - y2)/(x1 - x2)) + + tension = -(moment_sum_from_left_support)/(abs(self._left_support[1] - self._loads_position[sorted_position[i][0]][1])*cos(angle_with_horizontal) + abs(self._left_support[0] - self._loads_position[sorted_position[i][0]][0])*sin(angle_with_horizontal)) + self._tension[label] = tension + tension_func.append((tension, x<=x1)) + moment_sum_from_right_support += self._loads['point_load'][sorted_position[i][0]][0] * cos(pi * self._loads['point_load'][sorted_position[i][0]][1] / 180) * abs(self._right_support[1] - self._loads_position[sorted_position[i][0]][1]) + moment_sum_from_right_support += self._loads['point_load'][sorted_position[i][0]][0] * sin(pi * self._loads['point_load'][sorted_position[i][0]][1] / 180) * abs(self._right_support[0] - self._loads_position[sorted_position[i][0]][0]) + + label = Symbol(sorted_position[0][0]+"_"+sorted_position[1][0]) + y2 = self._loads_position[sorted_position[1][0]][1] + x2 = self._loads_position[sorted_position[1][0]][0] + x1 = self._left_support[0] + y1 = self._left_support[1] + + angle_with_horizontal = -atan((y2 - y1)/(x2 - x1)) + tension = -(moment_sum_from_right_support)/(abs(self._right_support[1] - self._loads_position[sorted_position[1][0]][1])*cos(angle_with_horizontal) + abs(self._right_support[0] - self._loads_position[sorted_position[1][0]][0])*sin(angle_with_horizontal)) + self._tension[label] = tension + + tension_func.insert(0,(tension, x<=x2)) + self._tension_func = Piecewise(*tension_func) + angle_with_horizontal = pi/2 - angle_with_horizontal + label = self._support_labels[0] + self._reaction_loads[Symbol("R_"+label+"_x")] = -sin(angle_with_horizontal) * tension + F_x += -sin(angle_with_horizontal) * tension + self._reaction_loads[Symbol("R_"+label+"_y")] = cos(angle_with_horizontal) * tension + F_y += cos(angle_with_horizontal) * tension + + label = self._support_labels[1] + self._reaction_loads[Symbol("R_"+label+"_x")] = -F_x + self._reaction_loads[Symbol("R_"+label+"_y")] = -F_y + + elif len(self._loads['distributed']) != 0 : + + if len(args) == 0: + raise ValueError("Provide the lowest point of the cable") + + lowest_x = sympify(args[0]) + self._lowest_x_global = lowest_x + + a = Symbol('a', positive=True) + c = Symbol('c') + # augmented matrix form of linsolve + + M = Matrix( + [[(self._left_support[0]-lowest_x)**2, 1, self._left_support[1]], + [(self._right_support[0]-lowest_x)**2, 1, self._right_support[1]], + ]) + + coefficient_solution = list(linsolve(M, (a, c))) + if len(coefficient_solution) ==0 or coefficient_solution[0][0]== 0: + raise ValueError("The lowest point is inconsistent with the supports") + + A = coefficient_solution[0][0] + C = coefficient_solution[0][1] + coefficient_solution[0][0]*lowest_x**2 + B = -2*coefficient_solution[0][0]*lowest_x + self._lowest_y_global = coefficient_solution[0][1] + lowest_y = self._lowest_y_global + + # y = A*x**2 + B*x + C + # shifting origin to lowest point + X = Symbol('X') + Y = Symbol('Y') + Y = A*(X + lowest_x)**2 + B*(X + lowest_x) + C - lowest_y + + temp_list = list(self._loads['distributed'].values()) + applied_force = temp_list[0] + + horizontal_force_constant = (applied_force * (self._right_support[0] - lowest_x)**2) / (2 * (self._right_support[1] - lowest_y)) + + self._tension.clear() + tangent_slope_to_curve = diff(Y, X) + self._tension['distributed'] = horizontal_force_constant / (cos(atan(tangent_slope_to_curve))) + + label = self._support_labels[0] + self._reaction_loads[Symbol("R_"+label+"_x")] = self.tension_at(self._left_support[0]) * cos(atan(tangent_slope_to_curve.subs(X, self._left_support[0] - lowest_x))) + self._reaction_loads[Symbol("R_"+label+"_y")] = self.tension_at(self._left_support[0]) * sin(atan(tangent_slope_to_curve.subs(X, self._left_support[0] - lowest_x))) + + label = self._support_labels[1] + self._reaction_loads[Symbol("R_"+label+"_x")] = self.tension_at(self._left_support[0]) * cos(atan(tangent_slope_to_curve.subs(X, self._right_support[0] - lowest_x))) + self._reaction_loads[Symbol("R_"+label+"_y")] = self.tension_at(self._left_support[0]) * sin(atan(tangent_slope_to_curve.subs(X, self._right_support[0] - lowest_x))) + + def draw(self): + """ + This method is used to obtain a plot for the specified cable with its supports, + shape and loads. + + Examples + ======== + + For point loads, + + >>> from sympy.physics.continuum_mechanics.cable import Cable + >>> c = Cable(("A", 0, 10), ("B", 10, 10)) + >>> c.apply_load(-1, ('Z', 2, 7.26, 3, 270)) + >>> c.apply_load(-1, ('X', 4, 6, 8, 270)) + >>> c.solve() + >>> p = c.draw() + >>> p # doctest: +ELLIPSIS + Plot object containing: + [0]: cartesian line: Piecewise((10 - 1.37*x, x <= 2), (8.52 - 0.63*x, x <= 4), (2*x/3 + 10/3, x <= 10)) for x over (0.0, 10.0) + ... + >>> p.show() + + For uniformly distributed loads, + + >>> from sympy.physics.continuum_mechanics.cable import Cable + >>> c=Cable(("A", 0, 40),("B", 100, 20)) + >>> c.apply_load(0, ("X", 850)) + >>> c.solve(58.58) + >>> p = c.draw() + >>> p # doctest: +ELLIPSIS + Plot object containing: + [0]: cartesian line: 0.0116550116550117*(x - 58.58)**2 + 0.00447086247086247 for x over (0.0, 100.0) + [1]: cartesian line: -7.49552913752915 for x over (0.0, 100.0) + ... + >>> p.show() + """ + x = Symbol("x") + annotations = [] + support_rectangles = self._draw_supports() + + xy_min = min(self._left_support[0],self._lowest_y_global) + xy_max = max(self._right_support[0], max(self._right_support[1],self._left_support[1])) + max_diff = xy_max - xy_min + if len(self._loads_position) != 0: + self._cable_eqn = self._draw_cable(-1) + annotations += self._draw_loads(-1) + + elif len(self._loads['distributed']) != 0 : + self._cable_eqn = self._draw_cable(0) + annotations += self._draw_loads(0) + + if not self._cable_eqn: + raise ValueError("solve method not called and/or values provided for loads and supports not adequate") + + cab_plot = plot(*self._cable_eqn,(x,self._left_support[0],self._right_support[0]), + xlim=(xy_min-0.5*max_diff,xy_max+0.5*max_diff), + ylim=(xy_min-0.5*max_diff,xy_max+0.5*max_diff), + rectangles=support_rectangles,show= False,annotations=annotations, axis=False) + + return cab_plot + + def _draw_supports(self): + member_rectangles = [] + xy_min = min(self._left_support[0],self._lowest_y_global) + xy_max = max(self._right_support[0], max(self._right_support[1],self._left_support[1])) + max_diff = xy_max - xy_min + + supp_width = 0.075*max_diff + + member_rectangles.append( + { + 'xy': (self._left_support[0]-supp_width,self._left_support[1]), + 'width': supp_width, + 'height':supp_width, + 'color':'brown', + 'fill': False + } + ) + + member_rectangles.append( + { + 'xy': (self._right_support[0],self._right_support[1]), + 'width': supp_width, + 'height':supp_width, + 'color':'brown', + 'fill': False + } + ) + + return member_rectangles + + def _draw_cable(self,order): + xy_min = min(self._left_support[0],self._lowest_y_global) + xy_max = max(self._right_support[0], max(self._right_support[1],self._left_support[1])) + max_diff = xy_max - xy_min + if order == -1 : + x,y = symbols('x y') + line_func = [] + sorted_position = sorted(self._loads_position.items(), key = lambda item : item[1][0]) + + for i in range(len(sorted_position)): + if(i==0): + y = ((sorted_position[i][1][1] - self._left_support[1])*(x-self._left_support[0]))/(sorted_position[i][1][0]- self._left_support[0]) + self._left_support[1] + else: + y = ((sorted_position[i][1][1] - sorted_position[i-1][1][1] )*(x-sorted_position[i-1][1][0]))/(sorted_position[i][1][0]- sorted_position[i-1][1][0]) + sorted_position[i-1][1][1] + line_func.append((y,x<=sorted_position[i][1][0])) + + y = ((sorted_position[len(sorted_position)-1][1][1] - self._right_support[1])*(x-self._right_support[0]))/(sorted_position[i][1][0]- self._right_support[0]) + self._right_support[1] + line_func.append((y,x<=self._right_support[0])) + return [Piecewise(*line_func)] + + elif order == 0: + x0 = self._lowest_x_global + diff_force_height = max_diff*0.075 + + a,c,x,y = symbols('a c x y') + parabola_eqn = a*(x-x0)**2 + c - y + + points = [(self._left_support[0],self._left_support[1]),(self._right_support[0],self._right_support[1])] + equations = [] + for px, py in points: + equations.append(parabola_eqn.subs({x: px, y: py})) + solution = solve(equations, (a, c)) + parabola_eqn = solution[a]*(x-x0)**2 + solution[c] + return [parabola_eqn, self._lowest_y_global - diff_force_height] + + def _draw_loads(self,order): + xy_min = min(self._left_support[0],self._lowest_y_global) + xy_max = max(self._right_support[0], max(self._right_support[1],self._left_support[1])) + max_diff = xy_max - xy_min + if(order==-1): + arrow_length = max_diff*0.1 + force_arrows = [] + for key in self._loads['point_load']: + force_arrows.append( + { + 'text': '', + 'xy':(self._loads_position[key][0]+arrow_length*cos(rad(self._loads['point_load'][key][1])),\ + self._loads_position[key][1] + arrow_length*sin(rad(self._loads['point_load'][key][1]))), + 'xytext': (self._loads_position[key][0],self._loads_position[key][1]), + 'arrowprops': {'width': 1, 'headlength':3, 'headwidth':3 , 'facecolor': 'black', } + } + ) + mag = self._loads['point_load'][key][0] + force_arrows.append( + { + 'text':f'{mag}N', + 'xy': (self._loads_position[key][0]+arrow_length*1.6*cos(rad(self._loads['point_load'][key][1])),\ + self._loads_position[key][1] + arrow_length*1.6*sin(rad(self._loads['point_load'][key][1]))), + } + ) + return force_arrows + + elif (order == 0): + x = symbols('x') + force_arrows = [] + x_val = [self._left_support[0] + ((self._right_support[0]-self._left_support[0])/10)*i for i in range(1,10)] + for i in x_val: + force_arrows.append( + { + 'text':'', + 'xytext':( + i, + self._cable_eqn[0].subs(x,i) + ), + 'xy':( + i, + self._cable_eqn[1].subs(x,i) + ), + 'arrowprops':{'width':1, 'headlength':3.5, 'headwidth':3.5, 'facecolor':'black'} + } + ) + mag = 0 + for key in self._loads['distributed']: + mag += self._loads['distributed'][key] + + force_arrows.append( + { + 'text':f'{mag} N/m', + 'xy':((self._left_support[0]+self._right_support[0])/2,self._lowest_y_global - max_diff*0.15) + } + ) + return force_arrows + + def plot_tension(self): + """ + Returns the diagram/plot of the tension generated in the cable at various points. + + Examples + ======== + + For point loads, + + >>> from sympy.physics.continuum_mechanics.cable import Cable + >>> c = Cable(("A", 0, 10), ("B", 10, 10)) + >>> c.apply_load(-1, ('Z', 2, 7.26, 3, 270)) + >>> c.apply_load(-1, ('X', 4, 6, 8, 270)) + >>> c.solve() + >>> p = c.plot_tension() + >>> p + Plot object containing: + [0]: cartesian line: Piecewise((8.91403453669861, x <= 2), (4.79150773600774, x <= 4), (19*sqrt(13)/10, x <= 10)) for x over (0.0, 10.0) + >>> p.show() + + For uniformly distributed loads, + + >>> from sympy.physics.continuum_mechanics.cable import Cable + >>> c=Cable(("A", 0, 40),("B", 100, 20)) + >>> c.apply_load(0, ("X", 850)) + >>> c.solve(58.58) + >>> p = c.plot_tension() + >>> p + Plot object containing: + [0]: cartesian line: 36465.0*sqrt(0.00054335718671383*X**2 + 1) for X over (0.0, 100.0) + >>> p.show() + + """ + if len(self._loads_position) != 0: + x = symbols('x') + tension_plot = plot(self._tension_func, (x,self._left_support[0],self._right_support[0]), show=False) + else: + X = symbols('X') + tension_plot = plot(self._tension['distributed'], (X,self._left_support[0],self._right_support[0]), show=False) + return tension_plot diff --git a/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/tests/test_arch.py b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/tests/test_arch.py new file mode 100644 index 0000000000000000000000000000000000000000..3d77062702222d7381a89450e8230b52bac4028c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/tests/test_arch.py @@ -0,0 +1,61 @@ +from sympy.physics.continuum_mechanics.arch import Arch +from sympy import Symbol, simplify + +x = Symbol('x') +t = Symbol('t') + +def test_arch_init(): + a = Arch((0,0),(10,0),crown_x=5,crown_y=5) + assert a.get_loads == {'distributed': {}, 'concentrated': {}} + assert a.reaction_force == {Symbol('R_A_x'):0, Symbol('R_A_y'):0, Symbol('R_B_x'):0, Symbol('R_B_y'):0} + assert a.supports == {'left':'hinge', 'right':'hinge'} + assert a.left_support == (0,0) + assert a.right_support == (10,0) + assert a.get_shape_eqn == 5 - ((x-5)**2)/5 + + a = Arch((0,0),(10,1),crown_x=6) + a.change_support_type(left_support='roller') + a.add_member(0.5) + assert a.supports == {'left':'roller', 'right':'hinge'} + assert simplify(a.get_shape_eqn) == simplify(9/5 - (x - 6)**2/20) + +def test_arch_support(): + a = Arch((0,0),(40,0),crown_x=20,crown_y=12) + a.apply_load(-1,'C',8,150,angle=270) + a.apply_load(0,'D',start=20,end=40,mag=-4) + a.solve() + assert abs(a.reaction_force[Symbol("R_A_x")] - 83.33333333333333) < 10e-12 + assert abs(a.reaction_force[Symbol("R_B_y")] - 90.00000000000000) < 10e-12 + assert abs(a.reaction_force[Symbol("R_B_x")] + 83.33333333333333) < 10e-12 + assert abs(a.reaction_force[Symbol("R_A_y")] - 140.00000000000000) < 10e-12 + +def test_arch_member(): + a = Arch((0,0),(40,0),crown_x=20,crown_y=15) + a.change_support_type(right_support='roller') + a.add_member(0) + a.apply_load(-1,'D',start=12,mag=3,angle=270) + a.apply_load(-1,'E',start=6,mag=4,angle=270) + a.apply_load(-1,'C',start=30,mag=5,angle=270) + a.solve() + assert a.reaction_force[Symbol("R_A_x")] == 0 + assert abs(a.reaction_force[Symbol("R_A_y")] - 6.750000000000000) < 10e-12 + assert a.reaction_force[Symbol("R_B_x")] == 0 + assert abs(a.reaction_force[Symbol("R_B_y")] - 5.250000000000000) < 10e-12 + +def test_symbol_magnitude(): + a = Arch((0,0),(16,0),crown_x=8,crown_y=5) + a.apply_load(0,'C',start=3,end=5,mag=t) + a.solve() + assert a.reaction_force[Symbol("R_A_x")] == -(4*t)/5 + assert a.reaction_force[Symbol("R_A_y")] == -(3*t)/2 + assert a.reaction_force[Symbol("R_B_x")] == (4*t)/5 + assert a.reaction_force[Symbol("R_B_y")] == -t/2 + assert a.bending_moment_at(4) == -5*t/2 + +def test_forces(): + a = Arch((0,0),(40,0),crown_x=20,crown_y=12) + a.apply_load(-1,'C',8,150,angle=270) + a.apply_load(0,'D',start=20,end=40,mag=-4) + a.solve() + assert abs(a.axial_force_at(7.999999999999999)-149.430523405935) < 1e-12 + assert abs(a.shear_force_at(7.999999999999999)-64.9227473161196) < 1e-12 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/tests/test_beam.py b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/tests/test_beam.py new file mode 100644 index 0000000000000000000000000000000000000000..a6a36fb030f99d9d384e52d4a239351688c7626b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/tests/test_beam.py @@ -0,0 +1,1118 @@ +from sympy.core.function import expand +from sympy.core.numbers import (Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.sets.sets import Interval +from sympy.simplify.simplify import simplify +from sympy.physics.continuum_mechanics.beam import Beam +from sympy.functions import SingularityFunction, Piecewise, meijerg, Abs, log +from sympy.testing.pytest import raises +from sympy.physics.units import meter, newton, kilo, giga, milli +from sympy.physics.continuum_mechanics.beam import Beam3D +from sympy.geometry import Circle, Polygon, Point2D, Triangle +from sympy.core.sympify import sympify + +x = Symbol('x') +y = Symbol('y') +R1, R2 = symbols('R1, R2') + + +def test_Beam(): + E = Symbol('E') + E_1 = Symbol('E_1') + I = Symbol('I') + I_1 = Symbol('I_1') + A = Symbol('A') + + b = Beam(1, E, I) + assert b.length == 1 + assert b.elastic_modulus == E + assert b.second_moment == I + assert b.variable == x + + # Test the length setter + b.length = 4 + assert b.length == 4 + + # Test the E setter + b.elastic_modulus = E_1 + assert b.elastic_modulus == E_1 + + # Test the I setter + b.second_moment = I_1 + assert b.second_moment is I_1 + + # Test the variable setter + b.variable = y + assert b.variable is y + + # Test for all boundary conditions. + b.bc_deflection = [(0, 2)] + b.bc_slope = [(0, 1)] + b.bc_bending_moment = [(0, 5)] + b.bc_shear_force = [(2, 1)] + assert b.boundary_conditions == {'deflection': [(0, 2)], 'slope': [(0, 1)], + 'bending_moment': [(0, 5)], 'shear_force': [(2, 1)]} + + # Test for shear force boundary condition method + b.bc_shear_force.extend([(1, 1), (2, 3)]) + sf_bcs = b.bc_shear_force + assert sf_bcs == [(2, 1), (1, 1), (2, 3)] + + # Test for slope boundary condition method + b.bc_bending_moment.extend([(1, 3), (5, 3)]) + bm_bcs = b.bc_bending_moment + assert bm_bcs == [(0, 5), (1, 3), (5, 3)] + + # Test for slope boundary condition method + b.bc_slope.extend([(4, 3), (5, 0)]) + s_bcs = b.bc_slope + assert s_bcs == [(0, 1), (4, 3), (5, 0)] + + # Test for deflection boundary condition method + b.bc_deflection.extend([(4, 3), (5, 0)]) + d_bcs = b.bc_deflection + assert d_bcs == [(0, 2), (4, 3), (5, 0)] + + # Test for updated boundary conditions + bcs_new = b.boundary_conditions + assert bcs_new == { + 'deflection': [(0, 2), (4, 3), (5, 0)], + 'slope': [(0, 1), (4, 3), (5, 0)], + 'bending_moment': [(0, 5), (1, 3), (5, 3)], + 'shear_force': [(2, 1), (1, 1), (2, 3)]} + + b1 = Beam(30, E, I) + b1.apply_load(-8, 0, -1) + b1.apply_load(R1, 10, -1) + b1.apply_load(R2, 30, -1) + b1.apply_load(120, 30, -2) + b1.bc_deflection = [(10, 0), (30, 0)] + b1.solve_for_reaction_loads(R1, R2) + + # Test for finding reaction forces + p = b1.reaction_loads + q = {R1: 6, R2: 2} + assert p == q + + # Test for load distribution function. + p = b1.load + q = -8*SingularityFunction(x, 0, -1) + 6*SingularityFunction(x, 10, -1) \ + + 120*SingularityFunction(x, 30, -2) + 2*SingularityFunction(x, 30, -1) + assert p == q + + # Test for shear force distribution function + p = b1.shear_force() + q = 8*SingularityFunction(x, 0, 0) - 6*SingularityFunction(x, 10, 0) \ + - 120*SingularityFunction(x, 30, -1) - 2*SingularityFunction(x, 30, 0) + assert p == q + + # Test for shear stress distribution function + p = b1.shear_stress() + q = (8*SingularityFunction(x, 0, 0) - 6*SingularityFunction(x, 10, 0) \ + - 120*SingularityFunction(x, 30, -1) \ + - 2*SingularityFunction(x, 30, 0))/A + assert p==q + + # Test for bending moment distribution function + p = b1.bending_moment() + q = 8*SingularityFunction(x, 0, 1) - 6*SingularityFunction(x, 10, 1) \ + - 120*SingularityFunction(x, 30, 0) - 2*SingularityFunction(x, 30, 1) + assert p == q + + # Test for slope distribution function + p = b1.slope() + q = -4*SingularityFunction(x, 0, 2) + 3*SingularityFunction(x, 10, 2) \ + + 120*SingularityFunction(x, 30, 1) + SingularityFunction(x, 30, 2) \ + + Rational(4000, 3) + assert p == q/(E*I) + + # Test for deflection distribution function + p = b1.deflection() + q = x*Rational(4000, 3) - 4*SingularityFunction(x, 0, 3)/3 \ + + SingularityFunction(x, 10, 3) + 60*SingularityFunction(x, 30, 2) \ + + SingularityFunction(x, 30, 3)/3 - 12000 + assert p == q/(E*I) + + # Test using symbols + l = Symbol('l') + w0 = Symbol('w0') + w2 = Symbol('w2') + a1 = Symbol('a1') + c = Symbol('c') + c1 = Symbol('c1') + d = Symbol('d') + e = Symbol('e') + f = Symbol('f') + + b2 = Beam(l, E, I) + + b2.apply_load(w0, a1, 1) + b2.apply_load(w2, c1, -1) + + b2.bc_deflection = [(c, d)] + b2.bc_slope = [(e, f)] + + # Test for load distribution function. + p = b2.load + q = w0*SingularityFunction(x, a1, 1) + w2*SingularityFunction(x, c1, -1) + assert p == q + + # Test for shear force distribution function + p = b2.shear_force() + q = -w0*SingularityFunction(x, a1, 2)/2 \ + - w2*SingularityFunction(x, c1, 0) + assert p == q + + # Test for shear stress distribution function + p = b2.shear_stress() + q = (-w0*SingularityFunction(x, a1, 2)/2 \ + - w2*SingularityFunction(x, c1, 0))/A + assert p == q + + # Test for bending moment distribution function + p = b2.bending_moment() + q = -w0*SingularityFunction(x, a1, 3)/6 - w2*SingularityFunction(x, c1, 1) + assert p == q + + # Test for slope distribution function + p = b2.slope() + q = (w0*SingularityFunction(x, a1, 4)/24 + w2*SingularityFunction(x, c1, 2)/2)/(E*I) + (E*I*f - w0*SingularityFunction(e, a1, 4)/24 - w2*SingularityFunction(e, c1, 2)/2)/(E*I) + assert expand(p) == expand(q) + + # Test for deflection distribution function + p = b2.deflection() + q = x*(E*I*f - w0*SingularityFunction(e, a1, 4)/24 \ + - w2*SingularityFunction(e, c1, 2)/2)/(E*I) \ + + (w0*SingularityFunction(x, a1, 5)/120 \ + + w2*SingularityFunction(x, c1, 3)/6)/(E*I) \ + + (E*I*(-c*f + d) + c*w0*SingularityFunction(e, a1, 4)/24 \ + + c*w2*SingularityFunction(e, c1, 2)/2 \ + - w0*SingularityFunction(c, a1, 5)/120 \ + - w2*SingularityFunction(c, c1, 3)/6)/(E*I) + assert simplify(p - q) == 0 + + b3 = Beam(9, E, I, 2) + b3.apply_load(value=-2, start=2, order=2, end=3) + b3.bc_slope.append((0, 2)) + C3 = symbols('C3') + C4 = symbols('C4') + + p = b3.load + q = -2*SingularityFunction(x, 2, 2) + 2*SingularityFunction(x, 3, 0) \ + + 4*SingularityFunction(x, 3, 1) + 2*SingularityFunction(x, 3, 2) + assert p == q + + p = b3.shear_force() + q = 2*SingularityFunction(x, 2, 3)/3 - 2*SingularityFunction(x, 3, 1) \ + - 2*SingularityFunction(x, 3, 2) - 2*SingularityFunction(x, 3, 3)/3 + assert p == q + + p = b3.shear_stress() + q = SingularityFunction(x, 2, 3)/3 - 1*SingularityFunction(x, 3, 1) \ + - 1*SingularityFunction(x, 3, 2) - 1*SingularityFunction(x, 3, 3)/3 + assert p == q + + p = b3.slope() + q = 2 - (SingularityFunction(x, 2, 5)/30 - SingularityFunction(x, 3, 3)/3 \ + - SingularityFunction(x, 3, 4)/6 - SingularityFunction(x, 3, 5)/30)/(E*I) + assert p == q + + p = b3.deflection() + q = 2*x - (SingularityFunction(x, 2, 6)/180 \ + - SingularityFunction(x, 3, 4)/12 - SingularityFunction(x, 3, 5)/30 \ + - SingularityFunction(x, 3, 6)/180)/(E*I) + assert p == q + C4 + + b4 = Beam(4, E, I, 3) + b4.apply_load(-3, 0, 0, end=3) + + p = b4.load + q = -3*SingularityFunction(x, 0, 0) + 3*SingularityFunction(x, 3, 0) + assert p == q + + p = b4.shear_force() + q = 3*SingularityFunction(x, 0, 1) \ + - 3*SingularityFunction(x, 3, 1) + assert p == q + + p = b4.shear_stress() + q = SingularityFunction(x, 0, 1) - SingularityFunction(x, 3, 1) + assert p == q + + p = b4.slope() + q = -3*SingularityFunction(x, 0, 3)/6 + 3*SingularityFunction(x, 3, 3)/6 + assert p == q/(E*I) + C3 + + p = b4.deflection() + q = -3*SingularityFunction(x, 0, 4)/24 + 3*SingularityFunction(x, 3, 4)/24 + assert p == q/(E*I) + C3*x + C4 + + # can't use end with point loads + raises(ValueError, lambda: b4.apply_load(-3, 0, -1, end=3)) + with raises(TypeError): + b4.variable = 1 + + +def test_insufficient_bconditions(): + # Test cases when required number of boundary conditions + # are not provided to solve the integration constants. + L = symbols('L', positive=True) + E, I, P, a3, a4 = symbols('E I P a3 a4') + + b = Beam(L, E, I, base_char='a') + b.apply_load(R2, L, -1) + b.apply_load(R1, 0, -1) + b.apply_load(-P, L/2, -1) + b.solve_for_reaction_loads(R1, R2) + + p = b.slope() + q = P*SingularityFunction(x, 0, 2)/4 - P*SingularityFunction(x, L/2, 2)/2 + P*SingularityFunction(x, L, 2)/4 + assert p == q/(E*I) + a3 + + p = b.deflection() + q = P*SingularityFunction(x, 0, 3)/12 - P*SingularityFunction(x, L/2, 3)/6 + P*SingularityFunction(x, L, 3)/12 + assert p == q/(E*I) + a3*x + a4 + + b.bc_deflection = [(0, 0)] + p = b.deflection() + q = a3*x + P*SingularityFunction(x, 0, 3)/12 - P*SingularityFunction(x, L/2, 3)/6 + P*SingularityFunction(x, L, 3)/12 + assert p == q/(E*I) + + b.bc_deflection = [(0, 0), (L, 0)] + p = b.deflection() + q = -L**2*P*x/16 + P*SingularityFunction(x, 0, 3)/12 - P*SingularityFunction(x, L/2, 3)/6 + P*SingularityFunction(x, L, 3)/12 + assert p == q/(E*I) + + +def test_statically_indeterminate(): + E = Symbol('E') + I = Symbol('I') + M1, M2 = symbols('M1, M2') + F = Symbol('F') + l = Symbol('l', positive=True) + + b5 = Beam(l, E, I) + b5.bc_deflection = [(0, 0),(l, 0)] + b5.bc_slope = [(0, 0),(l, 0)] + + b5.apply_load(R1, 0, -1) + b5.apply_load(M1, 0, -2) + b5.apply_load(R2, l, -1) + b5.apply_load(M2, l, -2) + b5.apply_load(-F, l/2, -1) + + b5.solve_for_reaction_loads(R1, R2, M1, M2) + p = b5.reaction_loads + q = {R1: F/2, R2: F/2, M1: -F*l/8, M2: F*l/8} + assert p == q + + +def test_beam_units(): + E = Symbol('E') + I = Symbol('I') + R1, R2 = symbols('R1, R2') + + kN = kilo*newton + gN = giga*newton + + b = Beam(8*meter, 200*gN/meter**2, 400*1000000*(milli*meter)**4) + b.apply_load(5*kN, 2*meter, -1) + b.apply_load(R1, 0*meter, -1) + b.apply_load(R2, 8*meter, -1) + b.apply_load(10*kN/meter, 4*meter, 0, end=8*meter) + b.bc_deflection = [(0*meter, 0*meter), (8*meter, 0*meter)] + b.solve_for_reaction_loads(R1, R2) + assert b.reaction_loads == {R1: -13750*newton, R2: -31250*newton} + + b = Beam(3*meter, E*newton/meter**2, I*meter**4) + b.apply_load(8*kN, 1*meter, -1) + b.apply_load(R1, 0*meter, -1) + b.apply_load(R2, 3*meter, -1) + b.apply_load(12*kN*meter, 2*meter, -2) + b.bc_deflection = [(0*meter, 0*meter), (3*meter, 0*meter)] + b.solve_for_reaction_loads(R1, R2) + assert b.reaction_loads == {R1: newton*Rational(-28000, 3), R2: newton*Rational(4000, 3)} + assert b.deflection().subs(x, 1*meter) == 62000*meter/(9*E*I) + + +def test_variable_moment(): + E = Symbol('E') + I = Symbol('I') + + b = Beam(4, E, 2*(4 - x)) + b.apply_load(20, 4, -1) + R, M = symbols('R, M') + b.apply_load(R, 0, -1) + b.apply_load(M, 0, -2) + b.bc_deflection = [(0, 0)] + b.bc_slope = [(0, 0)] + b.solve_for_reaction_loads(R, M) + assert b.slope().expand() == ((10*x*SingularityFunction(x, 0, 0) + - 10*(x - 4)*SingularityFunction(x, 4, 0))/E).expand() + assert b.deflection().expand() == ((5*x**2*SingularityFunction(x, 0, 0) + - 10*Piecewise((0, Abs(x)/4 < 1), (x**2*meijerg(((-1, 1), ()), ((), (-2, 0)), x/4), True)) + + 40*SingularityFunction(x, 4, 1))/E).expand() + + b = Beam(4, E - x, I) + b.apply_load(20, 4, -1) + R, M = symbols('R, M') + b.apply_load(R, 0, -1) + b.apply_load(M, 0, -2) + b.bc_deflection = [(0, 0)] + b.bc_slope = [(0, 0)] + b.solve_for_reaction_loads(R, M) + assert b.slope().expand() == ((-80*(-log(-E) + log(-E + x))*SingularityFunction(x, 0, 0) + + 80*(-log(-E + 4) + log(-E + x))*SingularityFunction(x, 4, 0) + 20*(-E*log(-E) + + E*log(-E + x) + x)*SingularityFunction(x, 0, 0) - 20*(-E*log(-E + 4) + E*log(-E + x) + + x - 4)*SingularityFunction(x, 4, 0))/I).expand() + + +def test_composite_beam(): + E = Symbol('E') + I = Symbol('I') + b1 = Beam(2, E, 1.5*I) + b2 = Beam(2, E, I) + b = b1.join(b2, "fixed") + b.apply_load(-20, 0, -1) + b.apply_load(80, 0, -2) + b.apply_load(20, 4, -1) + b.bc_slope = [(0, 0)] + b.bc_deflection = [(0, 0)] + assert b.length == 4 + assert b.second_moment == Piecewise((1.5*I, x <= 2), (I, x <= 4)) + assert b.slope().subs(x, 4) == 120.0/(E*I) + assert b.slope().subs(x, 2) == 80.0/(E*I) + assert int(b.deflection().subs(x, 4).args[0]) == -302 # Coefficient of 1/(E*I) + + l = symbols('l', positive=True) + R1, M1, R2, R3, P = symbols('R1 M1 R2 R3 P') + b1 = Beam(2*l, E, I) + b2 = Beam(2*l, E, I) + b = b1.join(b2,"hinge") + b.apply_load(M1, 0, -2) + b.apply_load(R1, 0, -1) + b.apply_load(R2, l, -1) + b.apply_load(R3, 4*l, -1) + b.apply_load(P, 3*l, -1) + b.bc_slope = [(0, 0)] + b.bc_deflection = [(0, 0), (l, 0), (4*l, 0)] + b.solve_for_reaction_loads(M1, R1, R2, R3) + assert b.reaction_loads == {R3: -P/2, R2: P*Rational(-5, 4), M1: -P*l/4, R1: P*Rational(3, 4)} + assert b.slope().subs(x, 3*l) == -7*P*l**2/(48*E*I) + assert b.deflection().subs(x, 2*l) == 7*P*l**3/(24*E*I) + assert b.deflection().subs(x, 3*l) == 5*P*l**3/(16*E*I) + + # When beams having same second moment are joined. + b1 = Beam(2, 500, 10) + b2 = Beam(2, 500, 10) + b = b1.join(b2, "fixed") + b.apply_load(M1, 0, -2) + b.apply_load(R1, 0, -1) + b.apply_load(R2, 1, -1) + b.apply_load(R3, 4, -1) + b.apply_load(10, 3, -1) + b.bc_slope = [(0, 0)] + b.bc_deflection = [(0, 0), (1, 0), (4, 0)] + b.solve_for_reaction_loads(M1, R1, R2, R3) + assert b.slope() == -2*SingularityFunction(x, 0, 1)/5625 + SingularityFunction(x, 0, 2)/1875\ + - 133*SingularityFunction(x, 1, 2)/135000 + SingularityFunction(x, 3, 2)/1000\ + - 37*SingularityFunction(x, 4, 2)/67500 + assert b.deflection() == -SingularityFunction(x, 0, 2)/5625 + SingularityFunction(x, 0, 3)/5625\ + - 133*SingularityFunction(x, 1, 3)/405000 + SingularityFunction(x, 3, 3)/3000\ + - 37*SingularityFunction(x, 4, 3)/202500 + + +def test_point_cflexure(): + E = Symbol('E') + I = Symbol('I') + b = Beam(10, E, I) + b.apply_load(-4, 0, -1) + b.apply_load(-46, 6, -1) + b.apply_load(10, 2, -1) + b.apply_load(20, 4, -1) + b.apply_load(3, 6, 0) + assert b.point_cflexure() == [Rational(10, 3)] + + E = Symbol('E') + I = Symbol('I') + b = Beam(15, E, I) + r0 = b.apply_support(0, type='pin') + r10 = b.apply_support(10, type='pin') + r15, m15 = b.apply_support(15, type='fixed') + b.apply_rotation_hinge(12) + b.apply_load(-10, 5, -1) + b.apply_load(-5, 10, 0, 15) + b.solve_for_reaction_loads(r0, r10, r15, m15) + assert b.point_cflexure() == [Rational(1200, 163), 12, Rational(163, 12)] + + E = Symbol('E') + I = Symbol('I') + b = Beam(15, E, I) + r0 = b.apply_support(0, type='pin') + r10 = b.apply_support(10, type='pin') + r15, m15 = b.apply_support(15, type='fixed') + b.apply_rotation_hinge(5) + b.apply_rotation_hinge(12) + b.apply_load(-10, 5, -1) + b.apply_load(-5, 10, 0, 15) + b.solve_for_reaction_loads(r0, r10, r15, m15) + with raises(NotImplementedError): + b.point_cflexure() + +def test_remove_load(): + E = Symbol('E') + I = Symbol('I') + b = Beam(4, E, I) + + try: + b.remove_load(2, 1, -1) + # As no load is applied on beam, ValueError should be returned. + except ValueError: + assert True + else: + assert False + + b.apply_load(-3, 0, -2) + b.apply_load(4, 2, -1) + b.apply_load(-2, 2, 2, end = 3) + b.remove_load(-2, 2, 2, end = 3) + assert b.load == -3*SingularityFunction(x, 0, -2) + 4*SingularityFunction(x, 2, -1) + assert b.applied_loads == [(-3, 0, -2, None), (4, 2, -1, None)] + + try: + b.remove_load(1, 2, -1) + # As load of this magnitude was never applied at + # this position, method should return a ValueError. + except ValueError: + assert True + else: + assert False + + b.remove_load(-3, 0, -2) + b.remove_load(4, 2, -1) + assert b.load == 0 + assert b.applied_loads == [] + + +def test_apply_support(): + E = Symbol('E') + I = Symbol('I') + + b = Beam(4, E, I) + b.apply_support(0, "cantilever") + b.apply_load(20, 4, -1) + M_0, R_0 = symbols('M_0, R_0') + b.solve_for_reaction_loads(R_0, M_0) + assert simplify(b.slope()) == simplify((80*SingularityFunction(x, 0, 1) - 10*SingularityFunction(x, 0, 2) + + 10*SingularityFunction(x, 4, 2))/(E*I)) + assert simplify(b.deflection()) == simplify((40*SingularityFunction(x, 0, 2) - 10*SingularityFunction(x, 0, 3)/3 + + 10*SingularityFunction(x, 4, 3)/3)/(E*I)) + + b = Beam(30, E, I) + p0 = b.apply_support(10, "pin") + p1 = b.apply_support(30, "roller") + b.apply_load(-8, 0, -1) + b.apply_load(120, 30, -2) + b.solve_for_reaction_loads(p0, p1) + assert b.slope() == (-4*SingularityFunction(x, 0, 2) + 3*SingularityFunction(x, 10, 2) + + 120*SingularityFunction(x, 30, 1) + SingularityFunction(x, 30, 2) + Rational(4000, 3))/(E*I) + assert b.deflection() == (x*Rational(4000, 3) - 4*SingularityFunction(x, 0, 3)/3 + SingularityFunction(x, 10, 3) + + 60*SingularityFunction(x, 30, 2) + SingularityFunction(x, 30, 3)/3 - 12000)/(E*I) + R_10 = Symbol('R_10') + R_30 = Symbol('R_30') + assert p0 == R_10 + assert b.reaction_loads == {R_10: 6, R_30: 2} + assert b.reaction_loads[p0] == 6 + + b = Beam(8, E, I) + p0, m0 = b.apply_support(0, "fixed") + p1 = b.apply_support(8, "roller") + b.apply_load(-5, 0, 0, 8) + b.solve_for_reaction_loads(p0, m0, p1) + R_0 = Symbol('R_0') + M_0 = Symbol('M_0') + R_8 = Symbol('R_8') + assert p0 == R_0 + assert m0 == M_0 + assert p1 == R_8 + assert b.reaction_loads == {R_0: 25, M_0: -40, R_8: 15} + assert b.reaction_loads[m0] == -40 + + P = Symbol('P', positive=True) + L = Symbol('L', positive=True) + b = Beam(L, E, I) + b.apply_support(0, type='fixed') + b.apply_support(L, type='fixed') + b.apply_load(-P, L/2, -1) + R_0, R_L, M_0, M_L = symbols('R_0, R_L, M_0, M_L') + b.solve_for_reaction_loads(R_0, R_L, M_0, M_L) + assert b.reaction_loads == {R_0: P/2, R_L: P/2, M_0: -L*P/8, M_L: L*P/8} + +def test_apply_rotation_hinge(): + b = Beam(15, 20, 20) + r0, m0 = b.apply_support(0, type='fixed') + r10 = b.apply_support(10, type='pin') + r15 = b.apply_support(15, type='pin') + p7 = b.apply_rotation_hinge(7) + p12 = b.apply_rotation_hinge(12) + b.apply_load(-10, 7, -1) + b.apply_load(-2, 10, 0, 15) + b.solve_for_reaction_loads(r0, m0, r10, r15) + R_0, M_0, R_10, R_15, P_7, P_12 = symbols('R_0, M_0, R_10, R_15, P_7, P_12') + expected_reactions = {R_0: 20/3, M_0: -140/3, R_10: 31/3, R_15: 3} + expected_rotations = {P_7: 2281/2160, P_12: -5137/5184} + reaction_symbols = [r0, m0, r10, r15] + rotation_symbols = [p7, p12] + tolerance = 1e-6 + assert all(abs(b.reaction_loads[r] - expected_reactions[r]) < tolerance for r in reaction_symbols) + assert all(abs(b.rotation_jumps[r] - expected_rotations[r]) < tolerance for r in rotation_symbols) + expected_bending_moment = (140 * SingularityFunction(x, 0, 0) / 3 - 20 * SingularityFunction(x, 0, 1) / 3 + - 11405 * SingularityFunction(x, 7, -1) / 27 + 10 * SingularityFunction(x, 7, 1) + - 31 * SingularityFunction(x, 10, 1) / 3 + SingularityFunction(x, 10, 2) + + 128425 * SingularityFunction(x, 12, -1) / 324 - 3 * SingularityFunction(x, 15, 1) + - SingularityFunction(x, 15, 2)) + assert b.bending_moment().expand() == expected_bending_moment.expand() + expected_slope = (-7*SingularityFunction(x, 0, 1)/60 + SingularityFunction(x, 0, 2)/120 + + 2281*SingularityFunction(x, 7, 0)/2160 - SingularityFunction(x, 7, 2)/80 + + 31*SingularityFunction(x, 10, 2)/2400 - SingularityFunction(x, 10, 3)/1200 + - 5137*SingularityFunction(x, 12, 0)/5184 + 3*SingularityFunction(x, 15, 2)/800 + + SingularityFunction(x, 15, 3)/1200) + assert b.slope().expand() == expected_slope.expand() + expected_deflection = (-7 * SingularityFunction(x, 0, 2) / 120 + SingularityFunction(x, 0, 3) / 360 + + 2281 * SingularityFunction(x, 7, 1) / 2160 - SingularityFunction(x, 7, 3) / 240 + + 31 * SingularityFunction(x, 10, 3) / 7200 - SingularityFunction(x, 10, 4) / 4800 + - 5137 * SingularityFunction(x, 12, 1) / 5184 + SingularityFunction(x, 15, 3) / 800 + + SingularityFunction(x, 15, 4) / 4800) + assert b.deflection().expand() == expected_deflection.expand() + + E = Symbol('E') + I = Symbol('I') + F = Symbol('F') + b = Beam(10, E, I) + r0, m0 = b.apply_support(0, type="fixed") + r10 = b.apply_support(10, type="pin") + b.apply_rotation_hinge(6) + b.apply_load(F, 8, -1) + b.solve_for_reaction_loads(r0, m0, r10) + assert b.reaction_loads == {R_0: -F/2, M_0: 3*F, R_10: -F/2} + assert (b.bending_moment() == -3*F*SingularityFunction(x, 0, 0) + F*SingularityFunction(x, 0, 1)/2 + + 17*F*SingularityFunction(x, 6, -1) - F*SingularityFunction(x, 8, 1) + + F*SingularityFunction(x, 10, 1)/2) + expected_deflection = -(-3*F*SingularityFunction(x, 0, 2)/2 + F*SingularityFunction(x, 0, 3)/12 + + 17*F*SingularityFunction(x, 6, 1) - F*SingularityFunction(x, 8, 3)/6 + + F*SingularityFunction(x, 10, 3)/12)/(E*I) + assert b.deflection().expand() == expected_deflection.expand() + + E = Symbol('E') + I = Symbol('I') + F = Symbol('F') + l1 = Symbol('l1', positive=True) + l2 = Symbol('l2', positive=True) + l3 = Symbol('l3', positive=True) + L = l1 + l2 + l3 + b = Beam(L, E, I) + r0, m0 = b.apply_support(0, type="fixed") + r1 = b.apply_support(L, type="pin") + b.apply_rotation_hinge(l1) + b.apply_load(F, l1+l2, -1) + b.solve_for_reaction_loads(r0, m0, r1) + assert b.reaction_loads[r0] == -F*l3/(l2 + l3) + assert b.reaction_loads[m0] == F*l1*l3/(l2 + l3) + assert b.reaction_loads[r1] == -F*l2/(l2 + l3) + expected_bending_moment = (-F*l1*l3*SingularityFunction(x, 0, 0)/(l2 + l3) + + F*l2*SingularityFunction(x, l1 + l2 + l3, 1)/(l2 + l3) + + F*l3*SingularityFunction(x, 0, 1)/(l2 + l3) - F*SingularityFunction(x, l1 + l2, 1) + - (-2*F*l1**3*l3 - 3*F*l1**2*l2*l3 - 3*F*l1**2*l3**2 + F*l2**3*l3 + 3*F*l2**2*l3**2 + 2*F*l2*l3**3) + *SingularityFunction(x, l1, -1)/(6*l2**2 + 12*l2*l3 + 6*l3**2)) + assert simplify(b.bending_moment().expand()) == simplify(expected_bending_moment.expand()) + +def test_apply_sliding_hinge(): + b = Beam(13, 20, 20) + r0, m0 = b.apply_support(0, type="fixed") + w8 = b.apply_sliding_hinge(8) + r13 = b.apply_support(13, type="pin") + b.apply_load(-10, 5, -1) + b.solve_for_reaction_loads(r0, m0, r13) + R_0, M_0, R_13, W_8 = symbols('R_0, M_0, R_13 W_8') + assert b.reaction_loads == {R_0: 10, M_0: -50, R_13: 0} + tolerance = 1e-6 + assert abs(b.deflection_jumps[w8] - 85/24) < tolerance + assert (b.bending_moment() == 50*SingularityFunction(x, 0, 0) - 10*SingularityFunction(x, 0, 1) + + 10*SingularityFunction(x, 5, 1) - 4250*SingularityFunction(x, 8, -2)/3) + assert (b.deflection() == -SingularityFunction(x, 0, 2)/16 + SingularityFunction(x, 0, 3)/240 + - SingularityFunction(x, 5, 3)/240 + 85*SingularityFunction(x, 8, 0)/24) + + E = Symbol('E') + I = Symbol('I') + I2 = Symbol('I2') + b1 = Beam(5, E, I) + b2 = Beam(8, E, I2) + b = b1.join(b2) + r0, m0 = b.apply_support(0, type="fixed") + b.apply_sliding_hinge(8) + r13 = b.apply_support(13, type="pin") + b.apply_load(-10, 5, -1) + b.solve_for_reaction_loads(r0, m0, r13) + W_8 = Symbol('W_8') + assert b.deflection_jumps == {W_8: 4250/(3*E*I2)} + + E = Symbol('E') + I = Symbol('I') + q = Symbol('q') + l1 = Symbol('l1', positive=True) + l2 = Symbol('l2', positive=True) + l3 = Symbol('l3', positive=True) + L = l1 + l2 + l3 + b = Beam(L, E, I) + r0 = b.apply_support(0, type="pin") + r3 = b.apply_support(l1, type="pin") + b.apply_sliding_hinge(l1 + l2) + r10 = b.apply_support(L, type="pin") + b.apply_load(q, 0, 0, l1) + b.solve_for_reaction_loads(r0, r3, r10) + assert (b.bending_moment() == l1*q*SingularityFunction(x, 0, 1)/2 + l1*q*SingularityFunction(x, l1, 1)/2 + - q*SingularityFunction(x, 0, 2)/2 + q*SingularityFunction(x, l1, 2)/2 + + (-l1**3*l2*q/24 - l1**3*l3*q/24)*SingularityFunction(x, l1 + l2, -2)) + assert b.deflection() ==(l1**3*q*x/24 - l1*q*SingularityFunction(x, 0, 3)/12 + - l1*q*SingularityFunction(x, l1, 3)/12 + q*SingularityFunction(x, 0, 4)/24 + - q*SingularityFunction(x, l1, 4)/24 + + (l1**3*l2*q/24 + l1**3*l3*q/24)*SingularityFunction(x, l1 + l2, 0))/(E*I) + +def test_max_shear_force(): + E = Symbol('E') + I = Symbol('I') + + b = Beam(3, E, I) + R, M = symbols('R, M') + b.apply_load(R, 0, -1) + b.apply_load(M, 0, -2) + b.apply_load(2, 3, -1) + b.apply_load(4, 2, -1) + b.apply_load(2, 2, 0, end=3) + b.solve_for_reaction_loads(R, M) + assert b.max_shear_force() == (Interval(0, 2), 8) + + l = symbols('l', positive=True) + P = Symbol('P') + b = Beam(l, E, I) + R1, R2 = symbols('R1, R2') + b.apply_load(R1, 0, -1) + b.apply_load(R2, l, -1) + b.apply_load(P, 0, 0, end=l) + b.solve_for_reaction_loads(R1, R2) + max_shear = b.max_shear_force() + assert max_shear[0] == 0 + assert simplify(max_shear[1] - (l*Abs(P)/2)) == 0 + + +def test_max_bmoment(): + E = Symbol('E') + I = Symbol('I') + l, P = symbols('l, P', positive=True) + + b = Beam(l, E, I) + R1, R2 = symbols('R1, R2') + b.apply_load(R1, 0, -1) + b.apply_load(R2, l, -1) + b.apply_load(P, l/2, -1) + b.solve_for_reaction_loads(R1, R2) + b.reaction_loads + assert b.max_bmoment() == (l/2, P*l/4) + + b = Beam(l, E, I) + R1, R2 = symbols('R1, R2') + b.apply_load(R1, 0, -1) + b.apply_load(R2, l, -1) + b.apply_load(P, 0, 0, end=l) + b.solve_for_reaction_loads(R1, R2) + assert b.max_bmoment() == (l/2, P*l**2/8) + + +def test_max_deflection(): + E, I, l, F = symbols('E, I, l, F', positive=True) + b = Beam(l, E, I) + b.bc_deflection = [(0, 0),(l, 0)] + b.bc_slope = [(0, 0),(l, 0)] + b.apply_load(F/2, 0, -1) + b.apply_load(-F*l/8, 0, -2) + b.apply_load(F/2, l, -1) + b.apply_load(F*l/8, l, -2) + b.apply_load(-F, l/2, -1) + assert b.max_deflection() == (l/2, F*l**3/(192*E*I)) + +def test_solve_for_ild_reactions(): + E = Symbol('E') + I = Symbol('I') + b = Beam(10, E, I) + b.apply_support(0, type="pin") + b.apply_support(10, type="pin") + R_0, R_10 = symbols('R_0, R_10') + b.solve_for_ild_reactions(1, R_0, R_10) + a = b.ild_variable + assert b.ild_reactions == {R_0: -SingularityFunction(a, 0, 0) + SingularityFunction(a, 0, 1)/10 + - SingularityFunction(a, 10, 1)/10, + R_10: -SingularityFunction(a, 0, 1)/10 + SingularityFunction(a, 10, 0) + + SingularityFunction(a, 10, 1)/10} + + E = Symbol('E') + I = Symbol('I') + F = Symbol('F') + L = Symbol('L', positive=True) + b = Beam(L, E, I) + b.apply_support(L, type="fixed") + b.apply_load(F, 0, -1) + R_L, M_L = symbols('R_L, M_L') + b.solve_for_ild_reactions(F, R_L, M_L) + a = b.ild_variable + assert b.ild_reactions == {R_L: -F*SingularityFunction(a, 0, 0) + F*SingularityFunction(a, L, 0) - F, + M_L: -F*L*SingularityFunction(a, 0, 0) - F*L + F*SingularityFunction(a, 0, 1) + - F*SingularityFunction(a, L, 1)} + + E = Symbol('E') + I = Symbol('I') + b = Beam(20, E, I) + r0 = b.apply_support(0, type="pin") + r5 = b.apply_support(5, type="pin") + r10 = b.apply_support(10, type="pin") + r20, m20 = b.apply_support(20, type="fixed") + b.solve_for_ild_reactions(1, r0, r5, r10, r20, m20) + a = b.ild_variable + assert b.ild_reactions[r0].subs(a, 4) == -Rational(59, 475) + assert b.ild_reactions[r5].subs(a, 4) == -Rational(2296, 2375) + assert b.ild_reactions[r10].subs(a, 4) == Rational(243, 2375) + assert b.ild_reactions[r20].subs(a, 12) == -Rational(83, 475) + assert b.ild_reactions[m20].subs(a, 12) == -Rational(264, 475) + +def test_solve_for_ild_shear(): + E = Symbol('E') + I = Symbol('I') + F = Symbol('F') + L1 = Symbol('L1', positive=True) + L2 = Symbol('L2', positive=True) + b = Beam(L1 + L2, E, I) + r0 = b.apply_support(0, type="pin") + rL = b.apply_support(L1 + L2, type="pin") + b.solve_for_ild_reactions(F, r0, rL) + b.solve_for_ild_shear(L1, F, r0, rL) + a = b.ild_variable + expected_shear = (-F*L1*SingularityFunction(a, 0, 0)/(L1 + L2) - F*L2*SingularityFunction(a, 0, 0)/(L1 + L2) + - F*SingularityFunction(-a, 0, 0) + F*SingularityFunction(a, L1 + L2, 0) + F + + F*SingularityFunction(a, 0, 1)/(L1 + L2) - F*SingularityFunction(a, L1 + L2, 1)/(L1 + L2) + - (-F*L1*SingularityFunction(a, 0, 0)/(L1 + L2) + F*L1*SingularityFunction(a, L1 + L2, 0)/(L1 + L2) + - F*L2*SingularityFunction(a, 0, 0)/(L1 + L2) + F*L2*SingularityFunction(a, L1 + L2, 0)/(L1 + L2) + + 2*F)*SingularityFunction(a, L1, 0)) + assert b.ild_shear.expand() == expected_shear.expand() + + E = Symbol('E') + I = Symbol('I') + b = Beam(20, E, I) + r0 = b.apply_support(0, type="pin") + r5 = b.apply_support(5, type="pin") + r10 = b.apply_support(10, type="pin") + r20, m20 = b.apply_support(20, type="fixed") + b.solve_for_ild_reactions(1, r0, r5, r10, r20, m20) + b.solve_for_ild_shear(6, 1, r0, r5, r10, r20, m20) + a = b.ild_variable + assert b.ild_shear.subs(a, 12) == Rational(96, 475) + assert b.ild_shear.subs(a, 4) == -Rational(216, 2375) + +def test_solve_for_ild_moment(): + E = Symbol('E') + I = Symbol('I') + F = Symbol('F') + L1 = Symbol('L1', positive=True) + L2 = Symbol('L2', positive=True) + b = Beam(L1 + L2, E, I) + r0 = b.apply_support(0, type="pin") + rL = b.apply_support(L1 + L2, type="pin") + a = b.ild_variable + b.solve_for_ild_reactions(F, r0, rL) + b.solve_for_ild_moment(L1, F, r0, rL) + assert b.ild_moment.subs(a, 3).subs(L1, 5).subs(L2, 5) == -3*F/2 + + E = Symbol('E') + I = Symbol('I') + b = Beam(20, E, I) + r0 = b.apply_support(0, type="pin") + r5 = b.apply_support(5, type="pin") + r10 = b.apply_support(10, type="pin") + r20, m20 = b.apply_support(20, type="fixed") + b.solve_for_ild_reactions(1, r0, r5, r10, r20, m20) + b.solve_for_ild_moment(5, 1, r0, r5, r10, r20, m20) + assert b.ild_moment.subs(a, 12) == -Rational(96, 475) + assert b.ild_moment.subs(a, 4) == Rational(36, 95) + +def test_ild_with_rotation_hinge(): + E = Symbol('E') + I = Symbol('I') + F = Symbol('F') + L1 = Symbol('L1', positive=True) + L2 = Symbol('L2', positive=True) + L3 = Symbol('L3', positive=True) + b = Beam(L1 + L2 + L3, E, I) + r0 = b.apply_support(0, type="pin") + r1 = b.apply_support(L1 + L2, type="pin") + r2 = b.apply_support(L1 + L2 + L3, type="pin") + b.apply_rotation_hinge(L1 + L2) + b.solve_for_ild_reactions(F, r0, r1, r2) + a = b.ild_variable + assert b.ild_reactions[r0].subs(a, 4).subs(L1, 5).subs(L2, 5).subs(L3, 10) == -3*F/5 + assert b.ild_reactions[r0].subs(a, -10).subs(L1, 5).subs(L2, 5).subs(L3, 10) == 0 + assert b.ild_reactions[r0].subs(a, 25).subs(L1, 5).subs(L2, 5).subs(L3, 10) == 0 + assert b.ild_reactions[r1].subs(a, 4).subs(L1, 5).subs(L2, 5).subs(L3, 10) == -2*F/5 + assert b.ild_reactions[r2].subs(a, 18).subs(L1, 5).subs(L2, 5).subs(L3, 10) == -4*F/5 + b.solve_for_ild_shear(L1, F, r0, r1, r2) + assert b.ild_shear.subs(a, 7).subs(L1, 5).subs(L2, 5).subs(L3, 10) == -3*F/10 + assert b.ild_shear.subs(a, 70).subs(L1, 5).subs(L2, 5).subs(L3, 10) == 0 + b.solve_for_ild_moment(L1, F, r0, r1, r2) + assert b.ild_moment.subs(a, 1).subs(L1, 5).subs(L2, 5).subs(L3, 10) == -F/2 + assert b.ild_moment.subs(a, 8).subs(L1, 5).subs(L2, 5).subs(L3, 10) == -F + +def test_ild_with_sliding_hinge(): + b = Beam(13, 200, 200) + r0 = b.apply_support(0, type="pin") + r6 = b.apply_support(6, type="pin") + r13, m13 = b.apply_support(13, type="fixed") + w3 = b.apply_sliding_hinge(3) + b.solve_for_ild_reactions(1, r0, r6, r13, m13) + a = b.ild_variable + assert b.ild_reactions[r0].subs(a, 3) == -1 + assert b.ild_reactions[r6].subs(a, 3) == Rational(9, 14) + assert b.ild_reactions[r13].subs(a, 9) == -Rational(207, 343) + assert b.ild_reactions[m13].subs(a, 9) == -Rational(60, 49) + assert b.ild_reactions[m13].subs(a, 15) == 0 + assert b.ild_reactions[m13].subs(a, -3) == 0 + assert b.ild_deflection_jumps[w3].subs(a, 9) == -Rational(9, 35000) + b.solve_for_ild_shear(7, 1, r0, r6, r13, m13) + assert b.ild_shear.subs(a, 8) == -Rational(200, 343) + b.solve_for_ild_moment(8, 1, r0, r6, r13, m13) + assert b.ild_moment.subs(a, 3) == -Rational(12, 7) + +def test_Beam3D(): + l, E, G, I, A = symbols('l, E, G, I, A') + R1, R2, R3, R4 = symbols('R1, R2, R3, R4') + + b = Beam3D(l, E, G, I, A) + m, q = symbols('m, q') + b.apply_load(q, 0, 0, dir="y") + b.apply_moment_load(m, 0, 0, dir="z") + b.bc_slope = [(0, [0, 0, 0]), (l, [0, 0, 0])] + b.bc_deflection = [(0, [0, 0, 0]), (l, [0, 0, 0])] + b.solve_slope_deflection() + + assert b.polar_moment() == 2*I + assert b.shear_force() == [0, -q*x, 0] + assert b.shear_stress() == [0, -q*x/A, 0] + assert b.axial_stress() == 0 + assert b.bending_moment() == [0, 0, -m*x + q*x**2/2] + expected_deflection = (x*(A*G*q*x**3/4 + A*G*x**2*(-l*(A*G*l*(l*q - 2*m) + + 12*E*I*q)/(A*G*l**2 + 12*E*I)/2 - m) + 3*E*I*l*(A*G*l*(l*q - 2*m) + + 12*E*I*q)/(A*G*l**2 + 12*E*I) + x*(-A*G*l**2*q/2 + + 3*A*G*l**2*(A*G*l*(l*q - 2*m) + 12*E*I*q)/(A*G*l**2 + 12*E*I)/4 + + A*G*l*m*Rational(3, 2) - 3*E*I*q))/(6*A*E*G*I)) + dx, dy, dz = b.deflection() + assert dx == dz == 0 + assert simplify(dy - expected_deflection) == 0 + + b2 = Beam3D(30, E, G, I, A, x) + b2.apply_load(50, start=0, order=0, dir="y") + b2.bc_deflection = [(0, [0, 0, 0]), (30, [0, 0, 0])] + b2.apply_load(R1, start=0, order=-1, dir="y") + b2.apply_load(R2, start=30, order=-1, dir="y") + b2.solve_for_reaction_loads(R1, R2) + assert b2.reaction_loads == {R1: -750, R2: -750} + + b2.solve_slope_deflection() + assert b2.slope() == [0, 0, 25*x**3/(3*E*I) - 375*x**2/(E*I) + 3750*x/(E*I)] + expected_deflection = 25*x**4/(12*E*I) - 125*x**3/(E*I) + 1875*x**2/(E*I) - \ + 25*x**2/(A*G) + 750*x/(A*G) + dx, dy, dz = b2.deflection() + assert dx == dz == 0 + assert dy == expected_deflection + + # Test for solve_for_reaction_loads + b3 = Beam3D(30, E, G, I, A, x) + b3.apply_load(8, start=0, order=0, dir="y") + b3.apply_load(9*x, start=0, order=0, dir="z") + b3.apply_load(R1, start=0, order=-1, dir="y") + b3.apply_load(R2, start=30, order=-1, dir="y") + b3.apply_load(R3, start=0, order=-1, dir="z") + b3.apply_load(R4, start=30, order=-1, dir="z") + b3.solve_for_reaction_loads(R1, R2, R3, R4) + assert b3.reaction_loads == {R1: -120, R2: -120, R3: -1350, R4: -2700} + + +def test_polar_moment_Beam3D(): + l, E, G, A, I1, I2 = symbols('l, E, G, A, I1, I2') + I = [I1, I2] + + b = Beam3D(l, E, G, I, A) + assert b.polar_moment() == I1 + I2 + + +def test_parabolic_loads(): + + E, I, L = symbols('E, I, L', positive=True, real=True) + R, M, P = symbols('R, M, P', real=True) + + # cantilever beam fixed at x=0 and parabolic distributed loading across + # length of beam + beam = Beam(L, E, I) + + beam.bc_deflection.append((0, 0)) + beam.bc_slope.append((0, 0)) + beam.apply_load(R, 0, -1) + beam.apply_load(M, 0, -2) + + # parabolic load + beam.apply_load(1, 0, 2) + + beam.solve_for_reaction_loads(R, M) + + assert beam.reaction_loads[R] == -L**3/3 + + # cantilever beam fixed at x=0 and parabolic distributed loading across + # first half of beam + beam = Beam(2*L, E, I) + + beam.bc_deflection.append((0, 0)) + beam.bc_slope.append((0, 0)) + beam.apply_load(R, 0, -1) + beam.apply_load(M, 0, -2) + + # parabolic load from x=0 to x=L + beam.apply_load(1, 0, 2, end=L) + + beam.solve_for_reaction_loads(R, M) + + # result should be the same as the prior example + assert beam.reaction_loads[R] == -L**3/3 + + # check constant load + beam = Beam(2*L, E, I) + beam.apply_load(P, 0, 0, end=L) + loading = beam.load.xreplace({L: 10, E: 20, I: 30, P: 40}) + assert loading.xreplace({x: 5}) == 40 + assert loading.xreplace({x: 15}) == 0 + + # check ramp load + beam = Beam(2*L, E, I) + beam.apply_load(P, 0, 1, end=L) + assert beam.load == (P*SingularityFunction(x, 0, 1) - + P*SingularityFunction(x, L, 1) - + P*L*SingularityFunction(x, L, 0)) + + # check higher order load: x**8 load from x=0 to x=L + beam = Beam(2*L, E, I) + beam.apply_load(P, 0, 8, end=L) + loading = beam.load.xreplace({L: 10, E: 20, I: 30, P: 40}) + assert loading.xreplace({x: 5}) == 40*5**8 + assert loading.xreplace({x: 15}) == 0 + + +def test_cross_section(): + I = Symbol('I') + l = Symbol('l') + E = Symbol('E') + C3, C4 = symbols('C3, C4') + a, c, g, h, r, n = symbols('a, c, g, h, r, n') + + # test for second_moment and cross_section setter + b0 = Beam(l, E, I) + assert b0.second_moment == I + assert b0.cross_section == None + b0.cross_section = Circle((0, 0), 5) + assert b0.second_moment == pi*Rational(625, 4) + assert b0.cross_section == Circle((0, 0), 5) + b0.second_moment = 2*n - 6 + assert b0.second_moment == 2*n-6 + assert b0.cross_section == None + with raises(ValueError): + b0.second_moment = Circle((0, 0), 5) + + # beam with a circular cross-section + b1 = Beam(50, E, Circle((0, 0), r)) + assert b1.cross_section == Circle((0, 0), r) + assert b1.second_moment == pi*r*Abs(r)**3/4 + + b1.apply_load(-10, 0, -1) + b1.apply_load(R1, 5, -1) + b1.apply_load(R2, 50, -1) + b1.apply_load(90, 45, -2) + b1.solve_for_reaction_loads(R1, R2) + assert b1.load == (-10*SingularityFunction(x, 0, -1) + 82*SingularityFunction(x, 5, -1)/S(9) + + 90*SingularityFunction(x, 45, -2) + 8*SingularityFunction(x, 50, -1)/9) + assert b1.bending_moment() == (10*SingularityFunction(x, 0, 1) - 82*SingularityFunction(x, 5, 1)/9 + - 90*SingularityFunction(x, 45, 0) - 8*SingularityFunction(x, 50, 1)/9) + q = (-5*SingularityFunction(x, 0, 2) + 41*SingularityFunction(x, 5, 2)/S(9) + + 90*SingularityFunction(x, 45, 1) + 4*SingularityFunction(x, 50, 2)/S(9))/(pi*E*r*Abs(r)**3) + assert b1.slope() == C3 + 4*q + q = (-5*SingularityFunction(x, 0, 3)/3 + 41*SingularityFunction(x, 5, 3)/27 + 45*SingularityFunction(x, 45, 2) + + 4*SingularityFunction(x, 50, 3)/27)/(pi*E*r*Abs(r)**3) + assert b1.deflection() == C3*x + C4 + 4*q + + # beam with a recatangular cross-section + b2 = Beam(20, E, Polygon((0, 0), (a, 0), (a, c), (0, c))) + assert b2.cross_section == Polygon((0, 0), (a, 0), (a, c), (0, c)) + assert b2.second_moment == a*c**3/12 + # beam with a triangular cross-section + b3 = Beam(15, E, Triangle((0, 0), (g, 0), (g/2, h))) + assert b3.cross_section == Triangle(Point2D(0, 0), Point2D(g, 0), Point2D(g/2, h)) + assert b3.second_moment == g*h**3/36 + + # composite beam + b = b2.join(b3, "fixed") + b.apply_load(-30, 0, -1) + b.apply_load(65, 0, -2) + b.apply_load(40, 0, -1) + b.bc_slope = [(0, 0)] + b.bc_deflection = [(0, 0)] + + assert b.second_moment == Piecewise((a*c**3/12, x <= 20), (g*h**3/36, x <= 35)) + assert b.cross_section == None + assert b.length == 35 + assert b.slope().subs(x, 7) == 8400/(E*a*c**3) + assert b.slope().subs(x, 25) == 52200/(E*g*h**3) + 39600/(E*a*c**3) + assert b.deflection().subs(x, 30) == -537000/(E*g*h**3) - 712000/(E*a*c**3) + +def test_max_shear_force_Beam3D(): + x = symbols('x') + b = Beam3D(20, 40, 21, 100, 25) + b.apply_load(15, start=0, order=0, dir="z") + b.apply_load(12*x, start=0, order=0, dir="y") + b.bc_deflection = [(0, [0, 0, 0]), (20, [0, 0, 0])] + assert b.max_shear_force() == [(0, 0), (20, 2400), (20, 300)] + +def test_max_bending_moment_Beam3D(): + x = symbols('x') + b = Beam3D(20, 40, 21, 100, 25) + b.apply_load(15, start=0, order=0, dir="z") + b.apply_load(12*x, start=0, order=0, dir="y") + b.bc_deflection = [(0, [0, 0, 0]), (20, [0, 0, 0])] + assert b.max_bmoment() == [(0, 0), (20, 3000), (20, 16000)] + +def test_max_deflection_Beam3D(): + x = symbols('x') + b = Beam3D(20, 40, 21, 100, 25) + b.apply_load(15, start=0, order=0, dir="z") + b.apply_load(12*x, start=0, order=0, dir="y") + b.bc_deflection = [(0, [0, 0, 0]), (20, [0, 0, 0])] + b.solve_slope_deflection() + c = sympify("495/14") + p = sympify("-10 + 10*sqrt(10793)/43") + q = sympify("(10 - 10*sqrt(10793)/43)**3/160 - 20/7 + (10 - 10*sqrt(10793)/43)**4/6400 + 20*sqrt(10793)/301 + 27*(10 - 10*sqrt(10793)/43)**2/560") + assert b.max_deflection() == [(0, 0), (10, c), (p, q)] + +def test_torsion_Beam3D(): + x = symbols('x') + b = Beam3D(20, 40, 21, 100, 25) + b.apply_moment_load(15, 5, -2, dir='x') + b.apply_moment_load(25, 10, -2, dir='x') + b.apply_moment_load(-5, 20, -2, dir='x') + b.solve_for_torsion() + assert b.angular_deflection().subs(x, 3) == sympify("1/40") + assert b.angular_deflection().subs(x, 9) == sympify("17/280") + assert b.angular_deflection().subs(x, 12) == sympify("53/840") + assert b.angular_deflection().subs(x, 17) == sympify("2/35") + assert b.angular_deflection().subs(x, 20) == sympify("3/56") diff --git a/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/tests/test_cable.py b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/tests/test_cable.py new file mode 100644 index 0000000000000000000000000000000000000000..95ae7997af20f31cbd1b36df4a494f66968ecf53 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/tests/test_cable.py @@ -0,0 +1,83 @@ +from sympy.physics.continuum_mechanics.cable import Cable +from sympy.core.symbol import Symbol + + +def test_cable(): + c = Cable(('A', 0, 10), ('B', 10, 10)) + assert c.supports == {'A': [0, 10], 'B': [10, 10]} + assert c.left_support == [0, 10] + assert c.right_support == [10, 10] + assert c.loads == {'distributed': {}, 'point_load': {}} + assert c.loads_position == {} + assert c.length == 0 + assert c.reaction_loads == {Symbol("R_A_x"): 0, Symbol("R_A_y"): 0, Symbol("R_B_x"): 0, Symbol("R_B_y"): 0} + + # tests for change_support method + c.change_support('A', ('C', 12, 3)) + assert c.supports == {'B': [10, 10], 'C': [12, 3]} + assert c.left_support == [10, 10] + assert c.right_support == [12, 3] + assert c.reaction_loads == {Symbol("R_B_x"): 0, Symbol("R_B_y"): 0, Symbol("R_C_x"): 0, Symbol("R_C_y"): 0} + + c.change_support('C', ('A', 0, 10)) + + # tests for apply_load method for point loads + c.apply_load(-1, ('X', 2, 5, 3, 30)) + c.apply_load(-1, ('Y', 5, 8, 5, 60)) + assert c.loads == {'distributed': {}, 'point_load': {'X': [3, 30], 'Y': [5, 60]}} + assert c.loads_position == {'X': [2, 5], 'Y': [5, 8]} + assert c.length == 0 + assert c.reaction_loads == {Symbol("R_A_x"): 0, Symbol("R_A_y"): 0, Symbol("R_B_x"): 0, Symbol("R_B_y"): 0} + + # tests for remove_loads method + c.remove_loads('X') + assert c.loads == {'distributed': {}, 'point_load': {'Y': [5, 60]}} + assert c.loads_position == {'Y': [5, 8]} + assert c.length == 0 + assert c.reaction_loads == {Symbol("R_A_x"): 0, Symbol("R_A_y"): 0, Symbol("R_B_x"): 0, Symbol("R_B_y"): 0} + + c.remove_loads('Y') + + #tests for apply_load method for distributed load + c.apply_load(0, ('Z', 9)) + assert c.loads == {'distributed': {'Z': 9}, 'point_load': {}} + assert c.loads_position == {} + assert c.length == 0 + assert c.reaction_loads == {Symbol("R_A_x"): 0, Symbol("R_A_y"): 0, Symbol("R_B_x"): 0, Symbol("R_B_y"): 0} + + # tests for apply_length method + c.apply_length(20) + assert c.length == 20 + + del c + # tests for solve method + # for point loads + c = Cable(("A", 0, 10), ("B", 5.5, 8)) + c.apply_load(-1, ('Z', 2, 7.26, 3, 270)) + c.apply_load(-1, ('X', 4, 6, 8, 270)) + c.solve() + #assert c.tension == {Symbol("Z_X"): 4.79150773600774, Symbol("X_B"): 6.78571428571429, Symbol("A_Z"): 6.89488895397307} + assert abs(c.tension[Symbol("A_Z")] - 6.89488895397307) < 10e-12 + assert abs(c.tension[Symbol("Z_X")] - 4.79150773600774) < 10e-12 + assert abs(c.tension[Symbol("X_B")] - 6.78571428571429) < 10e-12 + #assert c.reaction_loads == {Symbol("R_A_x"): -4.06504065040650, Symbol("R_A_y"): 5.56910569105691, Symbol("R_B_x"): 4.06504065040650, Symbol("R_B_y"): 5.43089430894309} + assert abs(c.reaction_loads[Symbol("R_A_x")] + 4.06504065040650) < 10e-12 + assert abs(c.reaction_loads[Symbol("R_A_y")] - 5.56910569105691) < 10e-12 + assert abs(c.reaction_loads[Symbol("R_B_x")] - 4.06504065040650) < 10e-12 + assert abs(c.reaction_loads[Symbol("R_B_y")] - 5.43089430894309) < 10e-12 + assert abs(c.length - 8.25609584845190) < 10e-12 + + del c + # tests for solve method + # for distributed loads + c=Cable(("A", 0, 40),("B", 100, 20)) + c.apply_load(0, ("X", 850)) + c.solve(58.58, 0) + + # assert c.tension['distributed'] == 36456.8485*sqrt(0.000543529004799705*(X + 0.00135624381275735)**2 + 1) + assert abs(c.tension_at(0) - 61717.4130533677) < 10e-11 + assert abs(c.tension_at(40) - 39738.0809048449) < 10e-11 + assert abs(c.reaction_loads[Symbol("R_A_x")] - 36465.0000000000) < 10e-11 + assert abs(c.reaction_loads[Symbol("R_A_y")] + 49793.0000000000) < 10e-11 + assert abs(c.reaction_loads[Symbol("R_B_x")] - 44399.9537590861) < 10e-11 + assert abs(c.reaction_loads[Symbol("R_B_y")] - 42868.2071025955 ) < 10e-11 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/tests/test_truss.py b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/tests/test_truss.py new file mode 100644 index 0000000000000000000000000000000000000000..61c89c9e09386257c7c69909dfdb0f37cda8627d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/tests/test_truss.py @@ -0,0 +1,100 @@ +from sympy.core.symbol import Symbol, symbols +from sympy.physics.continuum_mechanics.truss import Truss +from sympy import sqrt + + +def test_truss(): + A = Symbol('A') + B = Symbol('B') + C = Symbol('C') + AB, BC, AC = symbols('AB, BC, AC') + P = Symbol('P') + + t = Truss() + assert t.nodes == [] + assert t.node_labels == [] + assert t.node_positions == [] + assert t.members == {} + assert t.loads == {} + assert t.supports == {} + assert t.reaction_loads == {} + assert t.internal_forces == {} + + # testing the add_node method + t.add_node((A, 0, 0), (B, 2, 2), (C, 3, 0)) + assert t.nodes == [(A, 0, 0), (B, 2, 2), (C, 3, 0)] + assert t.node_labels == [A, B, C] + assert t.node_positions == [(0, 0), (2, 2), (3, 0)] + assert t.loads == {} + assert t.supports == {} + assert t.reaction_loads == {} + + # testing the remove_node method + t.remove_node(C) + assert t.nodes == [(A, 0, 0), (B, 2, 2)] + assert t.node_labels == [A, B] + assert t.node_positions == [(0, 0), (2, 2)] + assert t.loads == {} + assert t.supports == {} + + t.add_node((C, 3, 0)) + + # testing the add_member method + t.add_member((AB, A, B), (BC, B, C), (AC, A, C)) + assert t.members == {AB: [A, B], BC: [B, C], AC: [A, C]} + assert t.internal_forces == {AB: 0, BC: 0, AC: 0} + + # testing the remove_member method + t.remove_member(BC) + assert t.members == {AB: [A, B], AC: [A, C]} + assert t.internal_forces == {AB: 0, AC: 0} + + t.add_member((BC, B, C)) + + D, CD = symbols('D, CD') + + # testing the change_label methods + t.change_node_label((B, D)) + assert t.nodes == [(A, 0, 0), (D, 2, 2), (C, 3, 0)] + assert t.node_labels == [A, D, C] + assert t.loads == {} + assert t.supports == {} + assert t.members == {AB: [A, D], BC: [D, C], AC: [A, C]} + + t.change_member_label((BC, CD)) + assert t.members == {AB: [A, D], CD: [D, C], AC: [A, C]} + assert t.internal_forces == {AB: 0, CD: 0, AC: 0} + + + # testing the apply_load method + t.apply_load((A, P, 90), (A, P/4, 90), (A, 2*P,45), (D, P/2, 90)) + assert t.loads == {A: [[P, 90], [P/4, 90], [2*P, 45]], D: [[P/2, 90]]} + assert t.loads[A] == [[P, 90], [P/4, 90], [2*P, 45]] + + # testing the remove_load method + t.remove_load((A, P/4, 90)) + assert t.loads == {A: [[P, 90], [2*P, 45]], D: [[P/2, 90]]} + assert t.loads[A] == [[P, 90], [2*P, 45]] + + # testing the apply_support method + t.apply_support((A, "pinned"), (D, "roller")) + assert t.supports == {A: 'pinned', D: 'roller'} + assert t.reaction_loads == {} + assert t.loads == {A: [[P, 90], [2*P, 45], [Symbol('R_A_x'), 0], [Symbol('R_A_y'), 90]], D: [[P/2, 90], [Symbol('R_D_y'), 90]]} + + # testing the remove_support method + t.remove_support(A) + assert t.supports == {D: 'roller'} + assert t.reaction_loads == {} + assert t.loads == {A: [[P, 90], [2*P, 45]], D: [[P/2, 90], [Symbol('R_D_y'), 90]]} + + t.apply_support((A, "pinned")) + + # testing the solve method + t.solve() + assert t.reaction_loads['R_A_x'] == -sqrt(2)*P + assert t.reaction_loads['R_A_y'] == -sqrt(2)*P - P + assert t.reaction_loads['R_D_y'] == -P/2 + assert t.internal_forces[AB]/P == 0 + assert t.internal_forces[CD] == 0 + assert t.internal_forces[AC] == 0 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/truss.py b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/truss.py new file mode 100644 index 0000000000000000000000000000000000000000..f7fd0ea3f5e18574f21e2f656477c7af987d8eb6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/continuum_mechanics/truss.py @@ -0,0 +1,1108 @@ +""" +This module can be used to solve problems related +to 2D Trusses. +""" + + +from cmath import atan, inf +from sympy.core.add import Add +from sympy.core.evalf import INF +from sympy.core.mul import Mul +from sympy.core.symbol import Symbol +from sympy.core.sympify import sympify +from sympy import Matrix, pi +from sympy.external.importtools import import_module +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.matrices.dense import zeros +import math +from sympy.physics.units.quantities import Quantity +from sympy.plotting import plot +from sympy.utilities.decorator import doctest_depends_on +from sympy import sin, cos + + +__doctest_requires__ = {('Truss.draw'): ['matplotlib']} + + +numpy = import_module('numpy', import_kwargs={'fromlist':['arange']}) + + +class Truss: + """ + A Truss is an assembly of members such as beams, + connected by nodes, that create a rigid structure. + In engineering, a truss is a structure that + consists of two-force members only. + + Trusses are extremely important in engineering applications + and can be seen in numerous real-world applications like bridges. + + Examples + ======== + + There is a Truss consisting of four nodes and five + members connecting the nodes. A force P acts + downward on the node D and there also exist pinned + and roller joints on the nodes A and B respectively. + + .. image:: truss_example.png + + >>> from sympy.physics.continuum_mechanics.truss import Truss + >>> t = Truss() + >>> t.add_node(("node_1", 0, 0), ("node_2", 6, 0), ("node_3", 2, 2), ("node_4", 2, 0)) + >>> t.add_member(("member_1", "node_1", "node_4"), ("member_2", "node_2", "node_4"), ("member_3", "node_1", "node_3")) + >>> t.add_member(("member_4", "node_2", "node_3"), ("member_5", "node_3", "node_4")) + >>> t.apply_load(("node_4", 10, 270)) + >>> t.apply_support(("node_1", "pinned"), ("node_2", "roller")) + """ + + def __init__(self): + """ + Initializes the class + """ + self._nodes = [] + self._members = {} + self._loads = {} + self._supports = {} + self._node_labels = [] + self._node_positions = [] + self._node_position_x = [] + self._node_position_y = [] + self._nodes_occupied = {} + self._member_lengths = {} + self._reaction_loads = {} + self._internal_forces = {} + self._node_coordinates = {} + + @property + def nodes(self): + """ + Returns the nodes of the truss along with their positions. + """ + return self._nodes + + @property + def node_labels(self): + """ + Returns the node labels of the truss. + """ + return self._node_labels + + @property + def node_positions(self): + """ + Returns the positions of the nodes of the truss. + """ + return self._node_positions + + @property + def members(self): + """ + Returns the members of the truss along with the start and end points. + """ + return self._members + + @property + def member_lengths(self): + """ + Returns the length of each member of the truss. + """ + return self._member_lengths + + @property + def supports(self): + """ + Returns the nodes with provided supports along with the kind of support provided i.e. + pinned or roller. + """ + return self._supports + + @property + def loads(self): + """ + Returns the loads acting on the truss. + """ + return self._loads + + @property + def reaction_loads(self): + """ + Returns the reaction forces for all supports which are all initialized to 0. + """ + return self._reaction_loads + + @property + def internal_forces(self): + """ + Returns the internal forces for all members which are all initialized to 0. + """ + return self._internal_forces + + def add_node(self, *args): + """ + This method adds a node to the truss along with its name/label and its location. + Multiple nodes can be added at the same time. + + Parameters + ========== + The input(s) for this method are tuples of the form (label, x, y). + + label: String or a Symbol + The label for a node. It is the only way to identify a particular node. + + x: Sympifyable + The x-coordinate of the position of the node. + + y: Sympifyable + The y-coordinate of the position of the node. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.truss import Truss + >>> t = Truss() + >>> t.add_node(('A', 0, 0)) + >>> t.nodes + [('A', 0, 0)] + >>> t.add_node(('B', 3, 0), ('C', 4, 1)) + >>> t.nodes + [('A', 0, 0), ('B', 3, 0), ('C', 4, 1)] + """ + + for i in args: + label = i[0] + x = i[1] + x = sympify(x) + y=i[2] + y = sympify(y) + if label in self._node_coordinates: + raise ValueError("Node needs to have a unique label") + + elif [x, y] in self._node_coordinates.values(): + raise ValueError("A node already exists at the given position") + + else : + self._nodes.append((label, x, y)) + self._node_labels.append(label) + self._node_positions.append((x, y)) + self._node_position_x.append(x) + self._node_position_y.append(y) + self._node_coordinates[label] = [x, y] + + + + def remove_node(self, *args): + """ + This method removes a node from the truss. + Multiple nodes can be removed at the same time. + + Parameters + ========== + The input(s) for this method are the labels of the nodes to be removed. + + label: String or Symbol + The label of the node to be removed. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.truss import Truss + >>> t = Truss() + >>> t.add_node(('A', 0, 0), ('B', 3, 0), ('C', 5, 0)) + >>> t.nodes + [('A', 0, 0), ('B', 3, 0), ('C', 5, 0)] + >>> t.remove_node('A', 'C') + >>> t.nodes + [('B', 3, 0)] + """ + for label in args: + for i in range(len(self.nodes)): + if self._node_labels[i] == label: + x = self._node_position_x[i] + y = self._node_position_y[i] + + if label not in self._node_coordinates: + raise ValueError("No such node exists in the truss") + + else: + members_duplicate = self._members.copy() + for member in members_duplicate: + if label == self._members[member][0] or label == self._members[member][1]: + raise ValueError("The given node already has member attached to it") + self._nodes.remove((label, x, y)) + self._node_labels.remove(label) + self._node_positions.remove((x, y)) + self._node_position_x.remove(x) + self._node_position_y.remove(y) + if label in self._loads: + self._loads.pop(label) + if label in self._supports: + self._supports.pop(label) + self._node_coordinates.pop(label) + + + + def add_member(self, *args): + """ + This method adds a member between any two nodes in the given truss. + + Parameters + ========== + The input(s) of the method are tuple(s) of the form (label, start, end). + + label: String or Symbol + The label for a member. It is the only way to identify a particular member. + + start: String or Symbol + The label of the starting point/node of the member. + + end: String or Symbol + The label of the ending point/node of the member. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.truss import Truss + >>> t = Truss() + >>> t.add_node(('A', 0, 0), ('B', 3, 0), ('C', 2, 2)) + >>> t.add_member(('AB', 'A', 'B'), ('BC', 'B', 'C')) + >>> t.members + {'AB': ['A', 'B'], 'BC': ['B', 'C']} + """ + for i in args: + label = i[0] + start = i[1] + end = i[2] + + if start not in self._node_coordinates or end not in self._node_coordinates or start==end: + raise ValueError("The start and end points of the member must be unique nodes") + + elif label in self._members: + raise ValueError("A member with the same label already exists for the truss") + + elif self._nodes_occupied.get((start, end)): + raise ValueError("A member already exists between the two nodes") + + else: + self._members[label] = [start, end] + self._member_lengths[label] = sqrt((self._node_coordinates[end][0]-self._node_coordinates[start][0])**2 + (self._node_coordinates[end][1]-self._node_coordinates[start][1])**2) + self._nodes_occupied[start, end] = True + self._nodes_occupied[end, start] = True + self._internal_forces[label] = 0 + + def remove_member(self, *args): + """ + This method removes members from the given truss. + + Parameters + ========== + labels: String or Symbol + The label for the member to be removed. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.truss import Truss + >>> t = Truss() + >>> t.add_node(('A', 0, 0), ('B', 3, 0), ('C', 2, 2)) + >>> t.add_member(('AB', 'A', 'B'), ('AC', 'A', 'C'), ('BC', 'B', 'C')) + >>> t.members + {'AB': ['A', 'B'], 'AC': ['A', 'C'], 'BC': ['B', 'C']} + >>> t.remove_member('AC', 'BC') + >>> t.members + {'AB': ['A', 'B']} + """ + for label in args: + if label not in self._members: + raise ValueError("No such member exists in the Truss") + + else: + self._nodes_occupied.pop((self._members[label][0], self._members[label][1])) + self._nodes_occupied.pop((self._members[label][1], self._members[label][0])) + self._members.pop(label) + self._member_lengths.pop(label) + self._internal_forces.pop(label) + + def change_node_label(self, *args): + """ + This method changes the label(s) of the specified node(s). + + Parameters + ========== + The input(s) of this method are tuple(s) of the form (label, new_label). + + label: String or Symbol + The label of the node for which the label has + to be changed. + + new_label: String or Symbol + The new label of the node. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.truss import Truss + >>> t = Truss() + >>> t.add_node(('A', 0, 0), ('B', 3, 0)) + >>> t.nodes + [('A', 0, 0), ('B', 3, 0)] + >>> t.change_node_label(('A', 'C'), ('B', 'D')) + >>> t.nodes + [('C', 0, 0), ('D', 3, 0)] + """ + for i in args: + label = i[0] + new_label = i[1] + if label not in self._node_coordinates: + raise ValueError("No such node exists for the Truss") + elif new_label in self._node_coordinates: + raise ValueError("A node with the given label already exists") + else: + for node in self._nodes: + if node[0] == label: + self._nodes[self._nodes.index((label, node[1], node[2]))] = (new_label, node[1], node[2]) + self._node_labels[self._node_labels.index(node[0])] = new_label + self._node_coordinates[new_label] = self._node_coordinates[label] + self._node_coordinates.pop(label) + if node[0] in self._supports: + self._supports[new_label] = self._supports[node[0]] + self._supports.pop(node[0]) + if new_label in self._supports: + if self._supports[new_label] == 'pinned': + if 'R_'+str(label)+'_x' in self._reaction_loads and 'R_'+str(label)+'_y' in self._reaction_loads: + self._reaction_loads['R_'+str(new_label)+'_x'] = self._reaction_loads['R_'+str(label)+'_x'] + self._reaction_loads['R_'+str(new_label)+'_y'] = self._reaction_loads['R_'+str(label)+'_y'] + self._reaction_loads.pop('R_'+str(label)+'_x') + self._reaction_loads.pop('R_'+str(label)+'_y') + self._loads[new_label] = self._loads[label] + for load in self._loads[new_label]: + if load[1] == 90: + load[0] -= Symbol('R_'+str(label)+'_y') + if load[0] == 0: + self._loads[label].remove(load) + break + for load in self._loads[new_label]: + if load[1] == 0: + load[0] -= Symbol('R_'+str(label)+'_x') + if load[0] == 0: + self._loads[label].remove(load) + break + self.apply_load(new_label, Symbol('R_'+str(new_label)+'_x'), 0) + self.apply_load(new_label, Symbol('R_'+str(new_label)+'_y'), 90) + self._loads.pop(label) + elif self._supports[new_label] == 'roller': + self._loads[new_label] = self._loads[label] + for load in self._loads[label]: + if load[1] == 90: + load[0] -= Symbol('R_'+str(label)+'_y') + if load[0] == 0: + self._loads[label].remove(load) + break + self.apply_load(new_label, Symbol('R_'+str(new_label)+'_y'), 90) + self._loads.pop(label) + else: + if label in self._loads: + self._loads[new_label] = self._loads[label] + self._loads.pop(label) + for member in self._members: + if self._members[member][0] == node[0]: + self._members[member][0] = new_label + self._nodes_occupied[(new_label, self._members[member][1])] = True + self._nodes_occupied[(self._members[member][1], new_label)] = True + self._nodes_occupied.pop((label, self._members[member][1])) + self._nodes_occupied.pop((self._members[member][1], label)) + elif self._members[member][1] == node[0]: + self._members[member][1] = new_label + self._nodes_occupied[(self._members[member][0], new_label)] = True + self._nodes_occupied[(new_label, self._members[member][0])] = True + self._nodes_occupied.pop((self._members[member][0], label)) + self._nodes_occupied.pop((label, self._members[member][0])) + + def change_member_label(self, *args): + """ + This method changes the label(s) of the specified member(s). + + Parameters + ========== + The input(s) of this method are tuple(s) of the form (label, new_label) + + label: String or Symbol + The label of the member for which the label has + to be changed. + + new_label: String or Symbol + The new label of the member. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.truss import Truss + >>> t = Truss() + >>> t.add_node(('A', 0, 0), ('B', 3, 0), ('D', 5, 0)) + >>> t.nodes + [('A', 0, 0), ('B', 3, 0), ('D', 5, 0)] + >>> t.change_node_label(('A', 'C')) + >>> t.nodes + [('C', 0, 0), ('B', 3, 0), ('D', 5, 0)] + >>> t.add_member(('BC', 'B', 'C'), ('BD', 'B', 'D')) + >>> t.members + {'BC': ['B', 'C'], 'BD': ['B', 'D']} + >>> t.change_member_label(('BC', 'BC_new'), ('BD', 'BD_new')) + >>> t.members + {'BC_new': ['B', 'C'], 'BD_new': ['B', 'D']} + """ + for i in args: + label = i[0] + new_label = i[1] + if label not in self._members: + raise ValueError("No such member exists for the Truss") + else: + members_duplicate = list(self._members).copy() + for member in members_duplicate: + if member == label: + self._members[new_label] = [self._members[member][0], self._members[member][1]] + self._members.pop(label) + self._member_lengths[new_label] = self._member_lengths[label] + self._member_lengths.pop(label) + self._internal_forces[new_label] = self._internal_forces[label] + self._internal_forces.pop(label) + + def apply_load(self, *args): + """ + This method applies external load(s) at the specified node(s). + + Parameters + ========== + The input(s) of the method are tuple(s) of the form (location, magnitude, direction). + + location: String or Symbol + Label of the Node at which load is applied. + + magnitude: Sympifyable + Magnitude of the load applied. It must always be positive and any changes in + the direction of the load are not reflected here. + + direction: Sympifyable + The angle, in degrees, that the load vector makes with the horizontal + in the counter-clockwise direction. It takes the values 0 to 360, + inclusive. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.truss import Truss + >>> from sympy import symbols + >>> t = Truss() + >>> t.add_node(('A', 0, 0), ('B', 3, 0)) + >>> P = symbols('P') + >>> t.apply_load(('A', P, 90), ('A', P/2, 45), ('A', P/4, 90)) + >>> t.loads + {'A': [[P, 90], [P/2, 45], [P/4, 90]]} + """ + for i in args: + location = i[0] + magnitude = i[1] + direction = i[2] + magnitude = sympify(magnitude) + direction = sympify(direction) + + if location not in self._node_coordinates: + raise ValueError("Load must be applied at a known node") + + else: + if location in self._loads: + self._loads[location].append([magnitude, direction]) + else: + self._loads[location] = [[magnitude, direction]] + + def remove_load(self, *args): + """ + This method removes already + present external load(s) at specified node(s). + + Parameters + ========== + The input(s) of this method are tuple(s) of the form (location, magnitude, direction). + + location: String or Symbol + Label of the Node at which load is applied and is to be removed. + + magnitude: Sympifyable + Magnitude of the load applied. + + direction: Sympifyable + The angle, in degrees, that the load vector makes with the horizontal + in the counter-clockwise direction. It takes the values 0 to 360, + inclusive. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.truss import Truss + >>> from sympy import symbols + >>> t = Truss() + >>> t.add_node(('A', 0, 0), ('B', 3, 0)) + >>> P = symbols('P') + >>> t.apply_load(('A', P, 90), ('A', P/2, 45), ('A', P/4, 90)) + >>> t.loads + {'A': [[P, 90], [P/2, 45], [P/4, 90]]} + >>> t.remove_load(('A', P/4, 90), ('A', P/2, 45)) + >>> t.loads + {'A': [[P, 90]]} + """ + for i in args: + location = i[0] + magnitude = i[1] + direction = i[2] + magnitude = sympify(magnitude) + direction = sympify(direction) + + if location not in self._node_coordinates: + raise ValueError("Load must be removed from a known node") + + else: + if [magnitude, direction] not in self._loads[location]: + raise ValueError("No load of this magnitude and direction has been applied at this node") + else: + self._loads[location].remove([magnitude, direction]) + if self._loads[location] == []: + self._loads.pop(location) + + def apply_support(self, *args): + """ + This method adds a pinned or roller support at specified node(s). + + Parameters + ========== + The input(s) of this method are of the form (location, type). + + location: String or Symbol + Label of the Node at which support is added. + + type: String + Type of the support being provided at the node. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.truss import Truss + >>> t = Truss() + >>> t.add_node(('A', 0, 0), ('B', 3, 0)) + >>> t.apply_support(('A', 'pinned'), ('B', 'roller')) + >>> t.supports + {'A': 'pinned', 'B': 'roller'} + """ + for i in args: + location = i[0] + type = i[1] + if location not in self._node_coordinates: + raise ValueError("Support must be added on a known node") + + else: + if location not in self._supports: + if type == 'pinned': + self.apply_load((location, Symbol('R_'+str(location)+'_x'), 0)) + self.apply_load((location, Symbol('R_'+str(location)+'_y'), 90)) + elif type == 'roller': + self.apply_load((location, Symbol('R_'+str(location)+'_y'), 90)) + elif self._supports[location] == 'pinned': + if type == 'roller': + self.remove_load((location, Symbol('R_'+str(location)+'_x'), 0)) + elif self._supports[location] == 'roller': + if type == 'pinned': + self.apply_load((location, Symbol('R_'+str(location)+'_x'), 0)) + self._supports[location] = type + + def remove_support(self, *args): + """ + This method removes support from specified node(s.) + + Parameters + ========== + + locations: String or Symbol + Label of the Node(s) at which support is to be removed. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.truss import Truss + >>> t = Truss() + >>> t.add_node(('A', 0, 0), ('B', 3, 0)) + >>> t.apply_support(('A', 'pinned'), ('B', 'roller')) + >>> t.supports + {'A': 'pinned', 'B': 'roller'} + >>> t.remove_support('A','B') + >>> t.supports + {} + """ + for location in args: + + if location not in self._node_coordinates: + raise ValueError("No such node exists in the Truss") + + elif location not in self._supports: + raise ValueError("No support has been added to the given node") + + else: + if self._supports[location] == 'pinned': + self.remove_load((location, Symbol('R_'+str(location)+'_x'), 0)) + self.remove_load((location, Symbol('R_'+str(location)+'_y'), 90)) + elif self._supports[location] == 'roller': + self.remove_load((location, Symbol('R_'+str(location)+'_y'), 90)) + self._supports.pop(location) + + def solve(self): + """ + This method solves for all reaction forces of all supports and all internal forces + of all the members in the truss, provided the Truss is solvable. + + A Truss is solvable if the following condition is met, + + 2n >= r + m + + Where n is the number of nodes, r is the number of reaction forces, where each pinned + support has 2 reaction forces and each roller has 1, and m is the number of members. + + The given condition is derived from the fact that a system of equations is solvable + only when the number of variables is lesser than or equal to the number of equations. + Equilibrium Equations in x and y directions give two equations per node giving 2n number + equations. However, the truss needs to be stable as well and may be unstable if 2n > r + m. + The number of variables is simply the sum of the number of reaction forces and member + forces. + + .. note:: + The sign convention for the internal forces present in a member revolves around whether each + force is compressive or tensile. While forming equations for each node, internal force due + to a member on the node is assumed to be away from the node i.e. each force is assumed to + be compressive by default. Hence, a positive value for an internal force implies the + presence of compressive force in the member and a negative value implies a tensile force. + + Examples + ======== + + >>> from sympy.physics.continuum_mechanics.truss import Truss + >>> t = Truss() + >>> t.add_node(("node_1", 0, 0), ("node_2", 6, 0), ("node_3", 2, 2), ("node_4", 2, 0)) + >>> t.add_member(("member_1", "node_1", "node_4"), ("member_2", "node_2", "node_4"), ("member_3", "node_1", "node_3")) + >>> t.add_member(("member_4", "node_2", "node_3"), ("member_5", "node_3", "node_4")) + >>> t.apply_load(("node_4", 10, 270)) + >>> t.apply_support(("node_1", "pinned"), ("node_2", "roller")) + >>> t.solve() + >>> t.reaction_loads + {'R_node_1_x': 0, 'R_node_1_y': 20/3, 'R_node_2_y': 10/3} + >>> t.internal_forces + {'member_1': 20/3, 'member_2': 20/3, 'member_3': -20*sqrt(2)/3, 'member_4': -10*sqrt(5)/3, 'member_5': 10} + """ + count_reaction_loads = 0 + for node in self._nodes: + if node[0] in self._supports: + if self._supports[node[0]]=='pinned': + count_reaction_loads += 2 + elif self._supports[node[0]]=='roller': + count_reaction_loads += 1 + if 2*len(self._nodes) != len(self._members) + count_reaction_loads: + raise ValueError("The given truss cannot be solved") + coefficients_matrix = [[0 for i in range(2*len(self._nodes))] for j in range(2*len(self._nodes))] + load_matrix = zeros(2*len(self.nodes), 1) + load_matrix_row = 0 + for node in self._nodes: + if node[0] in self._loads: + for load in self._loads[node[0]]: + if load[0]!=Symbol('R_'+str(node[0])+'_x') and load[0]!=Symbol('R_'+str(node[0])+'_y'): + load_matrix[load_matrix_row] -= load[0]*cos(pi*load[1]/180) + load_matrix[load_matrix_row + 1] -= load[0]*sin(pi*load[1]/180) + load_matrix_row += 2 + cols = 0 + row = 0 + for node in self._nodes: + if node[0] in self._supports: + if self._supports[node[0]]=='pinned': + coefficients_matrix[row][cols] += 1 + coefficients_matrix[row+1][cols+1] += 1 + cols += 2 + elif self._supports[node[0]]=='roller': + coefficients_matrix[row+1][cols] += 1 + cols += 1 + row += 2 + for member in self._members: + start = self._members[member][0] + end = self._members[member][1] + length = sqrt((self._node_coordinates[start][0]-self._node_coordinates[end][0])**2 + (self._node_coordinates[start][1]-self._node_coordinates[end][1])**2) + start_index = self._node_labels.index(start) + end_index = self._node_labels.index(end) + horizontal_component_start = (self._node_coordinates[end][0]-self._node_coordinates[start][0])/length + vertical_component_start = (self._node_coordinates[end][1]-self._node_coordinates[start][1])/length + horizontal_component_end = (self._node_coordinates[start][0]-self._node_coordinates[end][0])/length + vertical_component_end = (self._node_coordinates[start][1]-self._node_coordinates[end][1])/length + coefficients_matrix[start_index*2][cols] += horizontal_component_start + coefficients_matrix[start_index*2+1][cols] += vertical_component_start + coefficients_matrix[end_index*2][cols] += horizontal_component_end + coefficients_matrix[end_index*2+1][cols] += vertical_component_end + cols += 1 + forces_matrix = (Matrix(coefficients_matrix)**-1)*load_matrix + self._reaction_loads = {} + i = 0 + min_load = inf + for node in self._nodes: + if node[0] in self._loads: + for load in self._loads[node[0]]: + if type(load[0]) not in [Symbol, Mul, Add]: + min_load = min(min_load, load[0]) + for j in range(len(forces_matrix)): + if type(forces_matrix[j]) not in [Symbol, Mul, Add]: + if abs(forces_matrix[j]/min_load) <1E-10: + forces_matrix[j] = 0 + for node in self._nodes: + if node[0] in self._supports: + if self._supports[node[0]]=='pinned': + self._reaction_loads['R_'+str(node[0])+'_x'] = forces_matrix[i] + self._reaction_loads['R_'+str(node[0])+'_y'] = forces_matrix[i+1] + i += 2 + elif self._supports[node[0]]=='roller': + self._reaction_loads['R_'+str(node[0])+'_y'] = forces_matrix[i] + i += 1 + for member in self._members: + self._internal_forces[member] = forces_matrix[i] + i += 1 + return + + @doctest_depends_on(modules=('numpy',)) + def draw(self, subs_dict=None): + """ + Returns a plot object of the Truss with all its nodes, members, + supports and loads. + + .. note:: + The user must be careful while entering load values in their + directions. The draw function assumes a sign convention that + is used for plotting loads. + + Given a right-handed coordinate system with XYZ coordinates, + the supports are assumed to be such that the reaction forces of a + pinned support is in the +X and +Y direction while those of a + roller support is in the +Y direction. For the load, the range + of angles, one can input goes all the way to 360 degrees which, in the + the plot is the angle that the load vector makes with the positive x-axis in the anticlockwise direction. + + For example, for a 90-degree angle, the load will be a vertically + directed along +Y while a 270-degree angle denotes a vertical + load as well but along -Y. + + Examples + ======== + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.physics.continuum_mechanics.truss import Truss + >>> import math + >>> t = Truss() + >>> t.add_node(("A", -4, 0), ("B", 0, 0), ("C", 4, 0), ("D", 8, 0)) + >>> t.add_node(("E", 6, 2/math.sqrt(3))) + >>> t.add_node(("F", 2, 2*math.sqrt(3))) + >>> t.add_node(("G", -2, 2/math.sqrt(3))) + >>> t.add_member(("AB","A","B"), ("BC","B","C"), ("CD","C","D")) + >>> t.add_member(("AG","A","G"), ("GB","G","B"), ("GF","G","F")) + >>> t.add_member(("BF","B","F"), ("FC","F","C"), ("CE","C","E")) + >>> t.add_member(("FE","F","E"), ("DE","D","E")) + >>> t.apply_support(("A","pinned"), ("D","roller")) + >>> t.apply_load(("G", 3, 90), ("E", 3, 90), ("F", 2, 90)) + >>> p = t.draw() + >>> p # doctest: +ELLIPSIS + Plot object containing: + [0]: cartesian line: 1 for x over (1.0, 1.0) + ... + >>> p.show() + """ + if not numpy: + raise ImportError("To use this function numpy module is required") + + x = Symbol('x') + + markers = [] + annotations = [] + rectangles = [] + + node_markers = self._draw_nodes(subs_dict) + markers += node_markers + + member_rectangles = self._draw_members() + rectangles += member_rectangles + + support_markers = self._draw_supports() + markers += support_markers + + load_annotations = self._draw_loads() + annotations += load_annotations + + xmax = -INF + xmin = INF + ymax = -INF + ymin = INF + + for node in self._node_coordinates: + xmax = max(xmax, self._node_coordinates[node][0]) + xmin = min(xmin, self._node_coordinates[node][0]) + ymax = max(ymax, self._node_coordinates[node][1]) + ymin = min(ymin, self._node_coordinates[node][1]) + + lim = max(xmax*1.1-xmin*0.8+1, ymax*1.1-ymin*0.8+1) + + if lim==xmax*1.1-xmin*0.8+1: + sing_plot = plot(1, (x, 1, 1), markers=markers, show=False, annotations=annotations, xlim=(xmin-0.05*lim, xmax*1.1), ylim=(xmin-0.05*lim, xmax*1.1), axis=False, rectangles=rectangles) + + else: + sing_plot = plot(1, (x, 1, 1), markers=markers, show=False, annotations=annotations, xlim=(ymin-0.05*lim, ymax*1.1), ylim=(ymin-0.05*lim, ymax*1.1), axis=False, rectangles=rectangles) + + return sing_plot + + + def _draw_nodes(self, subs_dict): + node_markers = [] + + for node in self._node_coordinates: + if (type(self._node_coordinates[node][0]) in (Symbol, Quantity)): + if self._node_coordinates[node][0] in subs_dict: + self._node_coordinates[node][0] = subs_dict[self._node_coordinates[node][0]] + else: + raise ValueError("provided substituted dictionary is not adequate") + elif (type(self._node_coordinates[node][0]) == Mul): + objects = self._node_coordinates[node][0].as_coeff_Mul() + for object in objects: + if type(object) in (Symbol, Quantity): + if subs_dict==None or object not in subs_dict: + raise ValueError("provided substituted dictionary is not adequate") + else: + self._node_coordinates[node][0] /= object + self._node_coordinates[node][0] *= subs_dict[object] + + if (type(self._node_coordinates[node][1]) in (Symbol, Quantity)): + if self._node_coordinates[node][1] in subs_dict: + self._node_coordinates[node][1] = subs_dict[self._node_coordinates[node][1]] + else: + raise ValueError("provided substituted dictionary is not adequate") + elif (type(self._node_coordinates[node][1]) == Mul): + objects = self._node_coordinates[node][1].as_coeff_Mul() + for object in objects: + if type(object) in (Symbol, Quantity): + if subs_dict==None or object not in subs_dict: + raise ValueError("provided substituted dictionary is not adequate") + else: + self._node_coordinates[node][1] /= object + self._node_coordinates[node][1] *= subs_dict[object] + + for node in self._node_coordinates: + node_markers.append( + { + 'args':[[self._node_coordinates[node][0]], [self._node_coordinates[node][1]]], + 'marker':'o', + 'markersize':5, + 'color':'black' + } + ) + return node_markers + + def _draw_members(self): + + member_rectangles = [] + + xmax = -INF + xmin = INF + ymax = -INF + ymin = INF + + for node in self._node_coordinates: + xmax = max(xmax, self._node_coordinates[node][0]) + xmin = min(xmin, self._node_coordinates[node][0]) + ymax = max(ymax, self._node_coordinates[node][1]) + ymin = min(ymin, self._node_coordinates[node][1]) + + if abs(1.1*xmax-0.8*xmin)>abs(1.1*ymax-0.8*ymin): + max_diff = 1.1*xmax-0.8*xmin + else: + max_diff = 1.1*ymax-0.8*ymin + + for member in self._members: + x1 = self._node_coordinates[self._members[member][0]][0] + y1 = self._node_coordinates[self._members[member][0]][1] + x2 = self._node_coordinates[self._members[member][1]][0] + y2 = self._node_coordinates[self._members[member][1]][1] + if x2!=x1 and y2!=y1: + if x2>x1: + member_rectangles.append( + { + 'xy':(x1-0.005*max_diff*cos(pi/4+atan((y2-y1)/(x2-x1)))/2, y1-0.005*max_diff*sin(pi/4+atan((y2-y1)/(x2-x1)))/2), + 'width':sqrt((x1-x2)**2+(y1-y2)**2)+0.005*max_diff/math.sqrt(2), + 'height':0.005*max_diff, + 'angle':180*atan((y2-y1)/(x2-x1))/pi, + 'color':'brown' + } + ) + else: + member_rectangles.append( + { + 'xy':(x2-0.005*max_diff*cos(pi/4+atan((y2-y1)/(x2-x1)))/2, y2-0.005*max_diff*sin(pi/4+atan((y2-y1)/(x2-x1)))/2), + 'width':sqrt((x1-x2)**2+(y1-y2)**2)+0.005*max_diff/math.sqrt(2), + 'height':0.005*max_diff, + 'angle':180*atan((y2-y1)/(x2-x1))/pi, + 'color':'brown' + } + ) + elif y2==y1: + if x2>x1: + member_rectangles.append( + { + 'xy':(x1-0.005*max_diff/2, y1-0.005*max_diff/2), + 'width':sqrt((x1-x2)**2+(y1-y2)**2), + 'height':0.005*max_diff, + 'angle':90*(1-math.copysign(1, x2-x1)), + 'color':'brown' + } + ) + else: + member_rectangles.append( + { + 'xy':(x1-0.005*max_diff/2, y1-0.005*max_diff/2), + 'width':sqrt((x1-x2)**2+(y1-y2)**2), + 'height':-0.005*max_diff, + 'angle':90*(1-math.copysign(1, x2-x1)), + 'color':'brown' + } + ) + else: + if y1abs(1.1*ymax-0.8*ymin): + max_diff = 1.1*xmax-0.8*xmin + else: + max_diff = 1.1*ymax-0.8*ymin + + for node in self._supports: + if self._supports[node]=='pinned': + support_markers.append( + { + 'args':[ + [self._node_coordinates[node][0]], + [self._node_coordinates[node][1]] + ], + 'marker':6, + 'markersize':15, + 'color':'black', + 'markerfacecolor':'none' + } + ) + support_markers.append( + { + 'args':[ + [self._node_coordinates[node][0]], + [self._node_coordinates[node][1]-0.035*max_diff] + ], + 'marker':'_', + 'markersize':14, + 'color':'black' + } + ) + + elif self._supports[node]=='roller': + support_markers.append( + { + 'args':[ + [self._node_coordinates[node][0]], + [self._node_coordinates[node][1]-0.02*max_diff] + ], + 'marker':'o', + 'markersize':11, + 'color':'black', + 'markerfacecolor':'none' + } + ) + support_markers.append( + { + 'args':[ + [self._node_coordinates[node][0]], + [self._node_coordinates[node][1]-0.0375*max_diff] + ], + 'marker':'_', + 'markersize':14, + 'color':'black' + } + ) + return support_markers + + def _draw_loads(self): + load_annotations = [] + + xmax = -INF + xmin = INF + ymax = -INF + ymin = INF + + for node in self._node_coordinates: + xmax = max(xmax, self._node_coordinates[node][0]) + xmin = min(xmin, self._node_coordinates[node][0]) + ymax = max(ymax, self._node_coordinates[node][1]) + ymin = min(ymin, self._node_coordinates[node][1]) + + if abs(1.1*xmax-0.8*xmin)>abs(1.1*ymax-0.8*ymin): + max_diff = 1.1*xmax-0.8*xmin+5 + else: + max_diff = 1.1*ymax-0.8*ymin+5 + + for node in self._loads: + for load in self._loads[node]: + if load[0] in [Symbol('R_'+str(node)+'_x'), Symbol('R_'+str(node)+'_y')]: + continue + x = self._node_coordinates[node][0] + y = self._node_coordinates[node][1] + load_annotations.append( + { + 'text':'', + 'xy':( + x-math.cos(pi*load[1]/180)*(max_diff/100), + y-math.sin(pi*load[1]/180)*(max_diff/100) + ), + 'xytext':( + x-(max_diff/100+abs(xmax-xmin)+abs(ymax-ymin))*math.cos(pi*load[1]/180)/20, + y-(max_diff/100+abs(xmax-xmin)+abs(ymax-ymin))*math.sin(pi*load[1]/180)/20 + ), + 'arrowprops':{'width':1.5, 'headlength':5, 'headwidth':5, 'facecolor':'black'} + } + ) + return load_annotations diff --git a/venv/lib/python3.11/site-packages/sympy/physics/control/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/control/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c4d74895f2e68cb918f00fd7065ca048b32ef06d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/control/__init__.py @@ -0,0 +1,17 @@ +from .lti import (TransferFunction, PIDController, Series, MIMOSeries, Parallel, MIMOParallel, + Feedback, MIMOFeedback, TransferFunctionMatrix, StateSpace, gbt, bilinear, forward_diff, + backward_diff, phase_margin, gain_margin) +from .control_plots import (pole_zero_numerical_data, pole_zero_plot, step_response_numerical_data, + step_response_plot, impulse_response_numerical_data, impulse_response_plot, ramp_response_numerical_data, + ramp_response_plot, bode_magnitude_numerical_data, bode_phase_numerical_data, bode_magnitude_plot, + bode_phase_plot, bode_plot, nyquist_plot_expr, nyquist_plot, nichols_plot_expr, nichols_plot) + +__all__ = ['TransferFunction', 'PIDController', 'Series', 'MIMOSeries', 'Parallel', + 'MIMOParallel', 'Feedback', 'MIMOFeedback', 'TransferFunctionMatrix', 'StateSpace', + 'gbt', 'bilinear', 'forward_diff', 'backward_diff', 'phase_margin', 'gain_margin', + 'pole_zero_numerical_data', 'pole_zero_plot', 'step_response_numerical_data', + 'step_response_plot', 'impulse_response_numerical_data', 'impulse_response_plot', + 'ramp_response_numerical_data', 'ramp_response_plot', + 'bode_magnitude_numerical_data', 'bode_phase_numerical_data', + 'bode_magnitude_plot', 'bode_phase_plot', 'bode_plot', 'nyquist_plot_expr', 'nyquist_plot', + 'nichols_plot_expr', 'nichols_plot'] diff --git a/venv/lib/python3.11/site-packages/sympy/physics/control/control_plots.py b/venv/lib/python3.11/site-packages/sympy/physics/control/control_plots.py new file mode 100644 index 0000000000000000000000000000000000000000..1a83d3b833a064905619a4d6ba2a74e52ef72afa --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/control/control_plots.py @@ -0,0 +1,1135 @@ +from sympy.core.numbers import I, pi +from sympy.functions.elementary.exponential import (exp, log) +from sympy.polys.partfrac import apart +from sympy.core.symbol import Dummy +from sympy.external import import_module +from sympy.functions import arg, Abs +from sympy.integrals.laplace import _fast_inverse_laplace +from sympy.physics.control.lti import SISOLinearTimeInvariant +from sympy.plotting.series import LineOver1DRangeSeries +from sympy.plotting.plot import plot_parametric +from sympy.polys.domains import ZZ, QQ +from sympy.polys.polytools import Poly +from sympy.printing.latex import latex +from sympy.geometry.polygon import deg + +__all__ = ['pole_zero_numerical_data', 'pole_zero_plot', + 'step_response_numerical_data', 'step_response_plot', + 'impulse_response_numerical_data', 'impulse_response_plot', + 'ramp_response_numerical_data', 'ramp_response_plot', + 'bode_magnitude_numerical_data', 'bode_phase_numerical_data', + 'bode_magnitude_plot', 'bode_phase_plot', 'bode_plot', + 'nyquist_plot_expr', 'nyquist_plot', 'nichols_plot_expr', + 'nichols_plot'] + + +matplotlib = import_module( + 'matplotlib', import_kwargs={'fromlist': ['pyplot']}, + catch=(RuntimeError,)) + +if matplotlib: + plt = matplotlib.pyplot + + +def _check_system(system): + """Function to check whether the dynamical system passed for plots is + compatible or not.""" + if not isinstance(system, SISOLinearTimeInvariant): + raise NotImplementedError("Only SISO LTI systems are currently supported.") + sys = system.to_expr() + len_free_symbols = len(sys.free_symbols) + if len_free_symbols > 1: + raise ValueError("Extra degree of freedom found. Make sure" + " that there are no free symbols in the dynamical system other" + " than the variable of Laplace transform.") + if sys.has(exp): + # Should test that exp is not part of a constant, in which case + # no exception is required, compare exp(s) with s*exp(1) + raise NotImplementedError("Time delay terms are not supported.") + + +def _poly_roots(poly): + """Function to get the roots of a polynomial.""" + def _eval(l): + return [float(i) if i.is_real else complex(i) for i in l] + if poly.domain in (QQ, ZZ): + return _eval(poly.all_roots()) + # XXX: Use all_roots() for irrational coefficients when possible + # See https://github.com/sympy/sympy/issues/22943 + return _eval(poly.nroots()) + + +def pole_zero_numerical_data(system): + """ + Returns the numerical data of poles and zeros of the system. + It is internally used by ``pole_zero_plot`` to get the data + for plotting poles and zeros. Users can use this data to further + analyse the dynamics of the system or plot using a different + backend/plotting-module. + + Parameters + ========== + + system : SISOLinearTimeInvariant + The system for which the pole-zero data is to be computed. + + Returns + ======= + + tuple : (zeros, poles) + zeros = Zeros of the system as a list of Python float/complex. + poles = Poles of the system as a list of Python float/complex. + + Raises + ====== + + NotImplementedError + When a SISO LTI system is not passed. + + When time delay terms are present in the system. + + ValueError + When more than one free symbol is present in the system. + The only variable in the transfer function should be + the variable of the Laplace transform. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy.physics.control.control_plots import pole_zero_numerical_data + >>> tf1 = TransferFunction(s**2 + 1, s**4 + 4*s**3 + 6*s**2 + 5*s + 2, s) + >>> pole_zero_numerical_data(tf1) + ([-1j, 1j], [-2.0, -1.0, (-0.5-0.8660254037844386j), (-0.5+0.8660254037844386j)]) + + See Also + ======== + + pole_zero_plot + + """ + _check_system(system) + system = system.doit() # Get the equivalent TransferFunction object. + + num_poly = Poly(system.num, system.var) + den_poly = Poly(system.den, system.var) + + return _poly_roots(num_poly), _poly_roots(den_poly) + + +def pole_zero_plot(system, pole_color='blue', pole_markersize=10, + zero_color='orange', zero_markersize=7, grid=True, show_axes=True, + show=True, **kwargs): + r""" + Returns the Pole-Zero plot (also known as PZ Plot or PZ Map) of a system. + + A Pole-Zero plot is a graphical representation of a system's poles and + zeros. It is plotted on a complex plane, with circular markers representing + the system's zeros and 'x' shaped markers representing the system's poles. + + Parameters + ========== + + system : SISOLinearTimeInvariant type systems + The system for which the pole-zero plot is to be computed. + pole_color : str, tuple, optional + The color of the pole points on the plot. Default color + is blue. The color can be provided as a matplotlib color string, + or a 3-tuple of floats each in the 0-1 range. + pole_markersize : Number, optional + The size of the markers used to mark the poles in the plot. + Default pole markersize is 10. + zero_color : str, tuple, optional + The color of the zero points on the plot. Default color + is orange. The color can be provided as a matplotlib color string, + or a 3-tuple of floats each in the 0-1 range. + zero_markersize : Number, optional + The size of the markers used to mark the zeros in the plot. + Default zero markersize is 7. + grid : boolean, optional + If ``True``, the plot will have a grid. Defaults to True. + show_axes : boolean, optional + If ``True``, the coordinate axes will be shown. Defaults to False. + show : boolean, optional + If ``True``, the plot will be displayed otherwise + the equivalent matplotlib ``plot`` object will be returned. + Defaults to True. + + Examples + ======== + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy.physics.control.control_plots import pole_zero_plot + >>> tf1 = TransferFunction(s**2 + 1, s**4 + 4*s**3 + 6*s**2 + 5*s + 2, s) + >>> pole_zero_plot(tf1) # doctest: +SKIP + + See Also + ======== + + pole_zero_numerical_data + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Pole%E2%80%93zero_plot + + """ + zeros, poles = pole_zero_numerical_data(system) + + zero_real = [i.real for i in zeros] + zero_imag = [i.imag for i in zeros] + + pole_real = [i.real for i in poles] + pole_imag = [i.imag for i in poles] + + plt.plot(pole_real, pole_imag, 'x', mfc='none', + markersize=pole_markersize, color=pole_color) + plt.plot(zero_real, zero_imag, 'o', markersize=zero_markersize, + color=zero_color) + plt.xlabel('Real Axis') + plt.ylabel('Imaginary Axis') + plt.title(f'Poles and Zeros of ${latex(system)}$', pad=20) + + if grid: + plt.grid() + if show_axes: + plt.axhline(0, color='black') + plt.axvline(0, color='black') + if show: + plt.show() + return + + return plt + + +def step_response_numerical_data(system, prec=8, lower_limit=0, + upper_limit=10, **kwargs): + """ + Returns the numerical values of the points in the step response plot + of a SISO continuous-time system. By default, adaptive sampling + is used. If the user wants to instead get an uniformly + sampled response, then ``adaptive`` kwarg should be passed ``False`` + and ``n`` must be passed as additional kwargs. + Refer to the parameters of class :class:`sympy.plotting.series.LineOver1DRangeSeries` + for more details. + + Parameters + ========== + + system : SISOLinearTimeInvariant + The system for which the unit step response data is to be computed. + prec : int, optional + The decimal point precision for the point coordinate values. + Defaults to 8. + lower_limit : Number, optional + The lower limit of the plot range. Defaults to 0. + upper_limit : Number, optional + The upper limit of the plot range. Defaults to 10. + kwargs : + Additional keyword arguments are passed to the underlying + :class:`sympy.plotting.series.LineOver1DRangeSeries` class. + + Returns + ======= + + tuple : (x, y) + x = Time-axis values of the points in the step response. NumPy array. + y = Amplitude-axis values of the points in the step response. NumPy array. + + Raises + ====== + + NotImplementedError + When a SISO LTI system is not passed. + + When time delay terms are present in the system. + + ValueError + When more than one free symbol is present in the system. + The only variable in the transfer function should be + the variable of the Laplace transform. + + When ``lower_limit`` parameter is less than 0. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy.physics.control.control_plots import step_response_numerical_data + >>> tf1 = TransferFunction(s, s**2 + 5*s + 8, s) + >>> step_response_numerical_data(tf1) # doctest: +SKIP + ([0.0, 0.025413462339411542, 0.0484508722725343, ... , 9.670250533855183, 9.844291913708725, 10.0], + [0.0, 0.023844582399907256, 0.042894276802320226, ..., 6.828770759094287e-12, 6.456457160755703e-12]) + + See Also + ======== + + step_response_plot + + """ + if lower_limit < 0: + raise ValueError("Lower limit of time must be greater " + "than or equal to zero.") + _check_system(system) + _x = Dummy("x") + expr = system.to_expr()/(system.var) + expr = apart(expr, system.var, full=True) + _y = _fast_inverse_laplace(expr, system.var, _x).evalf(prec) + return LineOver1DRangeSeries(_y, (_x, lower_limit, upper_limit), + **kwargs).get_points() + + +def step_response_plot(system, color='b', prec=8, lower_limit=0, + upper_limit=10, show_axes=False, grid=True, show=True, **kwargs): + r""" + Returns the unit step response of a continuous-time system. It is + the response of the system when the input signal is a step function. + + Parameters + ========== + + system : SISOLinearTimeInvariant type + The LTI SISO system for which the Step Response is to be computed. + color : str, tuple, optional + The color of the line. Default is Blue. + show : boolean, optional + If ``True``, the plot will be displayed otherwise + the equivalent matplotlib ``plot`` object will be returned. + Defaults to True. + lower_limit : Number, optional + The lower limit of the plot range. Defaults to 0. + upper_limit : Number, optional + The upper limit of the plot range. Defaults to 10. + prec : int, optional + The decimal point precision for the point coordinate values. + Defaults to 8. + show_axes : boolean, optional + If ``True``, the coordinate axes will be shown. Defaults to False. + grid : boolean, optional + If ``True``, the plot will have a grid. Defaults to True. + + Examples + ======== + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy.physics.control.control_plots import step_response_plot + >>> tf1 = TransferFunction(8*s**2 + 18*s + 32, s**3 + 6*s**2 + 14*s + 24, s) + >>> step_response_plot(tf1) # doctest: +SKIP + + See Also + ======== + + impulse_response_plot, ramp_response_plot + + References + ========== + + .. [1] https://www.mathworks.com/help/control/ref/lti.step.html + + """ + x, y = step_response_numerical_data(system, prec=prec, + lower_limit=lower_limit, upper_limit=upper_limit, **kwargs) + plt.plot(x, y, color=color) + plt.xlabel('Time (s)') + plt.ylabel('Amplitude') + plt.title(f'Unit Step Response of ${latex(system)}$', pad=20) + + if grid: + plt.grid() + if show_axes: + plt.axhline(0, color='black') + plt.axvline(0, color='black') + if show: + plt.show() + return + + return plt + + +def impulse_response_numerical_data(system, prec=8, lower_limit=0, + upper_limit=10, **kwargs): + """ + Returns the numerical values of the points in the impulse response plot + of a SISO continuous-time system. By default, adaptive sampling + is used. If the user wants to instead get an uniformly + sampled response, then ``adaptive`` kwarg should be passed ``False`` + and ``n`` must be passed as additional kwargs. + Refer to the parameters of class :class:`sympy.plotting.series.LineOver1DRangeSeries` + for more details. + + Parameters + ========== + + system : SISOLinearTimeInvariant + The system for which the impulse response data is to be computed. + prec : int, optional + The decimal point precision for the point coordinate values. + Defaults to 8. + lower_limit : Number, optional + The lower limit of the plot range. Defaults to 0. + upper_limit : Number, optional + The upper limit of the plot range. Defaults to 10. + kwargs : + Additional keyword arguments are passed to the underlying + :class:`sympy.plotting.series.LineOver1DRangeSeries` class. + + Returns + ======= + + tuple : (x, y) + x = Time-axis values of the points in the impulse response. NumPy array. + y = Amplitude-axis values of the points in the impulse response. NumPy array. + + Raises + ====== + + NotImplementedError + When a SISO LTI system is not passed. + + When time delay terms are present in the system. + + ValueError + When more than one free symbol is present in the system. + The only variable in the transfer function should be + the variable of the Laplace transform. + + When ``lower_limit`` parameter is less than 0. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy.physics.control.control_plots import impulse_response_numerical_data + >>> tf1 = TransferFunction(s, s**2 + 5*s + 8, s) + >>> impulse_response_numerical_data(tf1) # doctest: +SKIP + ([0.0, 0.06616480200395854,... , 9.854500743565858, 10.0], + [0.9999999799999999, 0.7042848373025861,...,7.170748906965121e-13, -5.1901263495547205e-12]) + + See Also + ======== + + impulse_response_plot + + """ + if lower_limit < 0: + raise ValueError("Lower limit of time must be greater " + "than or equal to zero.") + _check_system(system) + _x = Dummy("x") + expr = system.to_expr() + expr = apart(expr, system.var, full=True) + _y = _fast_inverse_laplace(expr, system.var, _x).evalf(prec) + return LineOver1DRangeSeries(_y, (_x, lower_limit, upper_limit), + **kwargs).get_points() + + +def impulse_response_plot(system, color='b', prec=8, lower_limit=0, + upper_limit=10, show_axes=False, grid=True, show=True, **kwargs): + r""" + Returns the unit impulse response (Input is the Dirac-Delta Function) of a + continuous-time system. + + Parameters + ========== + + system : SISOLinearTimeInvariant type + The LTI SISO system for which the Impulse Response is to be computed. + color : str, tuple, optional + The color of the line. Default is Blue. + show : boolean, optional + If ``True``, the plot will be displayed otherwise + the equivalent matplotlib ``plot`` object will be returned. + Defaults to True. + lower_limit : Number, optional + The lower limit of the plot range. Defaults to 0. + upper_limit : Number, optional + The upper limit of the plot range. Defaults to 10. + prec : int, optional + The decimal point precision for the point coordinate values. + Defaults to 8. + show_axes : boolean, optional + If ``True``, the coordinate axes will be shown. Defaults to False. + grid : boolean, optional + If ``True``, the plot will have a grid. Defaults to True. + + Examples + ======== + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy.physics.control.control_plots import impulse_response_plot + >>> tf1 = TransferFunction(8*s**2 + 18*s + 32, s**3 + 6*s**2 + 14*s + 24, s) + >>> impulse_response_plot(tf1) # doctest: +SKIP + + See Also + ======== + + step_response_plot, ramp_response_plot + + References + ========== + + .. [1] https://www.mathworks.com/help/control/ref/dynamicsystem.impulse.html + + """ + x, y = impulse_response_numerical_data(system, prec=prec, + lower_limit=lower_limit, upper_limit=upper_limit, **kwargs) + plt.plot(x, y, color=color) + plt.xlabel('Time (s)') + plt.ylabel('Amplitude') + plt.title(f'Impulse Response of ${latex(system)}$', pad=20) + + if grid: + plt.grid() + if show_axes: + plt.axhline(0, color='black') + plt.axvline(0, color='black') + if show: + plt.show() + return + + return plt + + +def ramp_response_numerical_data(system, slope=1, prec=8, + lower_limit=0, upper_limit=10, **kwargs): + """ + Returns the numerical values of the points in the ramp response plot + of a SISO continuous-time system. By default, adaptive sampling + is used. If the user wants to instead get an uniformly + sampled response, then ``adaptive`` kwarg should be passed ``False`` + and ``n`` must be passed as additional kwargs. + Refer to the parameters of class :class:`sympy.plotting.series.LineOver1DRangeSeries` + for more details. + + Parameters + ========== + + system : SISOLinearTimeInvariant + The system for which the ramp response data is to be computed. + slope : Number, optional + The slope of the input ramp function. Defaults to 1. + prec : int, optional + The decimal point precision for the point coordinate values. + Defaults to 8. + lower_limit : Number, optional + The lower limit of the plot range. Defaults to 0. + upper_limit : Number, optional + The upper limit of the plot range. Defaults to 10. + kwargs : + Additional keyword arguments are passed to the underlying + :class:`sympy.plotting.series.LineOver1DRangeSeries` class. + + Returns + ======= + + tuple : (x, y) + x = Time-axis values of the points in the ramp response plot. NumPy array. + y = Amplitude-axis values of the points in the ramp response plot. NumPy array. + + Raises + ====== + + NotImplementedError + When a SISO LTI system is not passed. + + When time delay terms are present in the system. + + ValueError + When more than one free symbol is present in the system. + The only variable in the transfer function should be + the variable of the Laplace transform. + + When ``lower_limit`` parameter is less than 0. + + When ``slope`` is negative. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy.physics.control.control_plots import ramp_response_numerical_data + >>> tf1 = TransferFunction(s, s**2 + 5*s + 8, s) + >>> ramp_response_numerical_data(tf1) # doctest: +SKIP + (([0.0, 0.12166980856813935,..., 9.861246379582118, 10.0], + [1.4504508011325967e-09, 0.006046440489058766,..., 0.12499999999568202, 0.12499999999661349])) + + See Also + ======== + + ramp_response_plot + + """ + if slope < 0: + raise ValueError("Slope must be greater than or equal" + " to zero.") + if lower_limit < 0: + raise ValueError("Lower limit of time must be greater " + "than or equal to zero.") + _check_system(system) + _x = Dummy("x") + expr = (slope*system.to_expr())/((system.var)**2) + expr = apart(expr, system.var, full=True) + _y = _fast_inverse_laplace(expr, system.var, _x).evalf(prec) + return LineOver1DRangeSeries(_y, (_x, lower_limit, upper_limit), + **kwargs).get_points() + + +def ramp_response_plot(system, slope=1, color='b', prec=8, lower_limit=0, + upper_limit=10, show_axes=False, grid=True, show=True, **kwargs): + r""" + Returns the ramp response of a continuous-time system. + + Ramp function is defined as the straight line + passing through origin ($f(x) = mx$). The slope of + the ramp function can be varied by the user and + the default value is 1. + + Parameters + ========== + + system : SISOLinearTimeInvariant type + The LTI SISO system for which the Ramp Response is to be computed. + slope : Number, optional + The slope of the input ramp function. Defaults to 1. + color : str, tuple, optional + The color of the line. Default is Blue. + show : boolean, optional + If ``True``, the plot will be displayed otherwise + the equivalent matplotlib ``plot`` object will be returned. + Defaults to True. + lower_limit : Number, optional + The lower limit of the plot range. Defaults to 0. + upper_limit : Number, optional + The upper limit of the plot range. Defaults to 10. + prec : int, optional + The decimal point precision for the point coordinate values. + Defaults to 8. + show_axes : boolean, optional + If ``True``, the coordinate axes will be shown. Defaults to False. + grid : boolean, optional + If ``True``, the plot will have a grid. Defaults to True. + + Examples + ======== + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy.physics.control.control_plots import ramp_response_plot + >>> tf1 = TransferFunction(s, (s+4)*(s+8), s) + >>> ramp_response_plot(tf1, upper_limit=2) # doctest: +SKIP + + See Also + ======== + + step_response_plot, impulse_response_plot + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Ramp_function + + """ + x, y = ramp_response_numerical_data(system, slope=slope, prec=prec, + lower_limit=lower_limit, upper_limit=upper_limit, **kwargs) + plt.plot(x, y, color=color) + plt.xlabel('Time (s)') + plt.ylabel('Amplitude') + plt.title(f'Ramp Response of ${latex(system)}$ [Slope = {slope}]', pad=20) + + if grid: + plt.grid() + if show_axes: + plt.axhline(0, color='black') + plt.axvline(0, color='black') + if show: + plt.show() + return + + return plt + + +def bode_magnitude_numerical_data(system, initial_exp=-5, final_exp=5, freq_unit='rad/sec', **kwargs): + """ + Returns the numerical data of the Bode magnitude plot of the system. + It is internally used by ``bode_magnitude_plot`` to get the data + for plotting Bode magnitude plot. Users can use this data to further + analyse the dynamics of the system or plot using a different + backend/plotting-module. + + Parameters + ========== + + system : SISOLinearTimeInvariant + The system for which the data is to be computed. + initial_exp : Number, optional + The initial exponent of 10 of the semilog plot. Defaults to -5. + final_exp : Number, optional + The final exponent of 10 of the semilog plot. Defaults to 5. + freq_unit : string, optional + User can choose between ``'rad/sec'`` (radians/second) and ``'Hz'`` (Hertz) as frequency units. + + Returns + ======= + + tuple : (x, y) + x = x-axis values of the Bode magnitude plot. + y = y-axis values of the Bode magnitude plot. + + Raises + ====== + + NotImplementedError + When a SISO LTI system is not passed. + + When time delay terms are present in the system. + + ValueError + When more than one free symbol is present in the system. + The only variable in the transfer function should be + the variable of the Laplace transform. + + When incorrect frequency units are given as input. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy.physics.control.control_plots import bode_magnitude_numerical_data + >>> tf1 = TransferFunction(s**2 + 1, s**4 + 4*s**3 + 6*s**2 + 5*s + 2, s) + >>> bode_magnitude_numerical_data(tf1) # doctest: +SKIP + ([1e-05, 1.5148378120533502e-05,..., 68437.36188804005, 100000.0], + [-6.020599914256786, -6.0205999155219505,..., -193.4117304087953, -200.00000000260573]) + + See Also + ======== + + bode_magnitude_plot, bode_phase_numerical_data + + """ + _check_system(system) + expr = system.to_expr() + freq_units = ('rad/sec', 'Hz') + if freq_unit not in freq_units: + raise ValueError('Only "rad/sec" and "Hz" are accepted frequency units.') + + _w = Dummy("w", real=True) + if freq_unit == 'Hz': + repl = I*_w*2*pi + else: + repl = I*_w + w_expr = expr.subs({system.var: repl}) + + mag = 20*log(Abs(w_expr), 10) + + x, y = LineOver1DRangeSeries(mag, + (_w, 10**initial_exp, 10**final_exp), xscale='log', **kwargs).get_points() + + return x, y + + +def bode_magnitude_plot(system, initial_exp=-5, final_exp=5, + color='b', show_axes=False, grid=True, show=True, freq_unit='rad/sec', **kwargs): + r""" + Returns the Bode magnitude plot of a continuous-time system. + + See ``bode_plot`` for all the parameters. + """ + x, y = bode_magnitude_numerical_data(system, initial_exp=initial_exp, + final_exp=final_exp, freq_unit=freq_unit) + plt.plot(x, y, color=color, **kwargs) + plt.xscale('log') + + + plt.xlabel('Frequency (%s) [Log Scale]' % freq_unit) + plt.ylabel('Magnitude (dB)') + plt.title(f'Bode Plot (Magnitude) of ${latex(system)}$', pad=20) + + if grid: + plt.grid(True) + if show_axes: + plt.axhline(0, color='black') + plt.axvline(0, color='black') + if show: + plt.show() + return + + return plt + + +def bode_phase_numerical_data(system, initial_exp=-5, final_exp=5, freq_unit='rad/sec', phase_unit='rad', phase_unwrap = True, **kwargs): + """ + Returns the numerical data of the Bode phase plot of the system. + It is internally used by ``bode_phase_plot`` to get the data + for plotting Bode phase plot. Users can use this data to further + analyse the dynamics of the system or plot using a different + backend/plotting-module. + + Parameters + ========== + + system : SISOLinearTimeInvariant + The system for which the Bode phase plot data is to be computed. + initial_exp : Number, optional + The initial exponent of 10 of the semilog plot. Defaults to -5. + final_exp : Number, optional + The final exponent of 10 of the semilog plot. Defaults to 5. + freq_unit : string, optional + User can choose between ``'rad/sec'`` (radians/second) and '``'Hz'`` (Hertz) as frequency units. + phase_unit : string, optional + User can choose between ``'rad'`` (radians) and ``'deg'`` (degree) as phase units. + phase_unwrap : bool, optional + Set to ``True`` by default. + + Returns + ======= + + tuple : (x, y) + x = x-axis values of the Bode phase plot. + y = y-axis values of the Bode phase plot. + + Raises + ====== + + NotImplementedError + When a SISO LTI system is not passed. + + When time delay terms are present in the system. + + ValueError + When more than one free symbol is present in the system. + The only variable in the transfer function should be + the variable of the Laplace transform. + + When incorrect frequency or phase units are given as input. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy.physics.control.control_plots import bode_phase_numerical_data + >>> tf1 = TransferFunction(s**2 + 1, s**4 + 4*s**3 + 6*s**2 + 5*s + 2, s) + >>> bode_phase_numerical_data(tf1) # doctest: +SKIP + ([1e-05, 1.4472354033813751e-05, 2.035581932165858e-05,..., 47577.3248186011, 67884.09326036123, 100000.0], + [-2.5000000000291665e-05, -3.6180885085e-05, -5.08895483066e-05,...,-3.1415085799262523, -3.14155265358979]) + + See Also + ======== + + bode_magnitude_plot, bode_phase_numerical_data + + """ + _check_system(system) + expr = system.to_expr() + freq_units = ('rad/sec', 'Hz') + phase_units = ('rad', 'deg') + if freq_unit not in freq_units: + raise ValueError('Only "rad/sec" and "Hz" are accepted frequency units.') + if phase_unit not in phase_units: + raise ValueError('Only "rad" and "deg" are accepted phase units.') + + _w = Dummy("w", real=True) + if freq_unit == 'Hz': + repl = I*_w*2*pi + else: + repl = I*_w + w_expr = expr.subs({system.var: repl}) + + if phase_unit == 'deg': + phase = arg(w_expr)*180/pi + else: + phase = arg(w_expr) + + x, y = LineOver1DRangeSeries(phase, + (_w, 10**initial_exp, 10**final_exp), xscale='log', **kwargs).get_points() + + half = None + if phase_unwrap: + if(phase_unit == 'rad'): + half = pi + elif(phase_unit == 'deg'): + half = 180 + if half: + unit = 2*half + for i in range(1, len(y)): + diff = y[i] - y[i - 1] + if diff > half: # Jump from -half to half + y[i] = (y[i] - unit) + elif diff < -half: # Jump from half to -half + y[i] = (y[i] + unit) + + return x, y + + +def bode_phase_plot(system, initial_exp=-5, final_exp=5, + color='b', show_axes=False, grid=True, show=True, freq_unit='rad/sec', phase_unit='rad', phase_unwrap=True, **kwargs): + r""" + Returns the Bode phase plot of a continuous-time system. + + See ``bode_plot`` for all the parameters. + """ + x, y = bode_phase_numerical_data(system, initial_exp=initial_exp, + final_exp=final_exp, freq_unit=freq_unit, phase_unit=phase_unit, phase_unwrap=phase_unwrap) + plt.plot(x, y, color=color, **kwargs) + plt.xscale('log') + + plt.xlabel('Frequency (%s) [Log Scale]' % freq_unit) + plt.ylabel('Phase (%s)' % phase_unit) + plt.title(f'Bode Plot (Phase) of ${latex(system)}$', pad=20) + + if grid: + plt.grid(True) + if show_axes: + plt.axhline(0, color='black') + plt.axvline(0, color='black') + if show: + plt.show() + return + + return plt + + +def bode_plot(system, initial_exp=-5, final_exp=5, + grid=True, show_axes=False, show=True, freq_unit='rad/sec', phase_unit='rad', phase_unwrap=True, **kwargs): + r""" + Returns the Bode phase and magnitude plots of a continuous-time system. + + Parameters + ========== + + system : SISOLinearTimeInvariant type + The LTI SISO system for which the Bode Plot is to be computed. + initial_exp : Number, optional + The initial exponent of 10 of the semilog plot. Defaults to -5. + final_exp : Number, optional + The final exponent of 10 of the semilog plot. Defaults to 5. + show : boolean, optional + If ``True``, the plot will be displayed otherwise + the equivalent matplotlib ``plot`` object will be returned. + Defaults to True. + prec : int, optional + The decimal point precision for the point coordinate values. + Defaults to 8. + grid : boolean, optional + If ``True``, the plot will have a grid. Defaults to True. + show_axes : boolean, optional + If ``True``, the coordinate axes will be shown. Defaults to False. + freq_unit : string, optional + User can choose between ``'rad/sec'`` (radians/second) and ``'Hz'`` (Hertz) as frequency units. + phase_unit : string, optional + User can choose between ``'rad'`` (radians) and ``'deg'`` (degree) as phase units. + + Examples + ======== + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy.physics.control.control_plots import bode_plot + >>> tf1 = TransferFunction(1*s**2 + 0.1*s + 7.5, 1*s**4 + 0.12*s**3 + 9*s**2, s) + >>> bode_plot(tf1, initial_exp=0.2, final_exp=0.7) # doctest: +SKIP + + See Also + ======== + + bode_magnitude_plot, bode_phase_plot + + """ + plt.subplot(211) + mag = bode_magnitude_plot(system, initial_exp=initial_exp, final_exp=final_exp, + show=False, grid=grid, show_axes=show_axes, + freq_unit=freq_unit, **kwargs) + mag.title(f'Bode Plot of ${latex(system)}$', pad=20) + mag.xlabel(None) + plt.subplot(212) + bode_phase_plot(system, initial_exp=initial_exp, final_exp=final_exp, + show=False, grid=grid, show_axes=show_axes, freq_unit=freq_unit, phase_unit=phase_unit, phase_unwrap=phase_unwrap, **kwargs).title(None) + + if show: + plt.show() + return + + return plt + + +def nyquist_plot_expr(system): + """Function to get the expression for Nyquist plot.""" + s = system.var + w = Dummy('w', real=True) + repl = I * w + expr = system.to_expr() + w_expr = expr.subs({s: repl}) + w_expr = w_expr.as_real_imag() + real_expr = w_expr[0] + imag_expr = w_expr[1] + return real_expr, imag_expr, w + + +def nichols_plot_expr(system): + """Function to get the expression for Nichols plot.""" + s = system.var + w = Dummy('w', real=True) + sys_expr = system.to_expr() + H_jw = sys_expr.subs(s, I*w) + mag_expr = Abs(H_jw) + mag_dB_expr = 20*log(mag_expr, 10) + phase_expr = arg(H_jw) + phase_deg_expr = deg(phase_expr) + return mag_dB_expr, phase_deg_expr, w + + +def nyquist_plot(system, initial_omega=0.01, final_omega=100, show=True, + color='b', **kwargs): + r""" + Generates the Nyquist plot for a continuous-time system. + + Parameters + ========== + + system : SISOLinearTimeInvariant + The LTI SISO system for which the Nyquist plot is to be generated. + initial_omega : float, optional + The starting frequency value. Defaults to 0.01. + final_omega : float, optional + The ending frequency value. Defaults to 100. + show : bool, optional + If True, the plot is displayed. Default is True. + color : str, optional + The color of the Nyquist plot. Default is 'b' (blue). + grid : bool, optional + If True, grid lines are displayed. Default is False. + **kwargs + Additional keyword arguments for customization. + + Examples + ======== + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy.physics.control.control_plots import nyquist_plot + >>> tf1 = TransferFunction(2*s**2 + 5*s + 1, s**2 + 2*s + 3, s) + >>> nyquist_plot(tf1) # doctest: +SKIP + + See Also + ======== + + nichols_plot, bode_plot + + """ + _check_system(system) + real_expr, imag_expr, w = nyquist_plot_expr(system) + w_values = [(w, initial_omega, final_omega)] + p = plot_parametric( + (real_expr, imag_expr), # The curve + (real_expr, -imag_expr), # Its mirror image + *w_values, + show=False, + line_color=color, + adaptive=True, + title=f'Nyquist Plot of ${latex(system)}$', + xlabel='Real Axis', + ylabel='Imaginary Axis', + size=(6, 5), + kwargs=kwargs) + if show: + p.show() + return + return p + + +def nichols_plot(system, initial_omega=0.01, final_omega=100, show=True, color='b', **kwargs): + r""" + Generates the Nichols plot for a LTI system. + + Parameters + ========== + + system : SISOLinearTimeInvariant + The LTI SISO system for which the Nyquist plot is to be generated. + initial_omega : float, optional + The starting frequency value. Defaults to 0.01. + final_omega : float, optional + The ending frequency value. Defaults to 100. + show : bool, optional + If True, the plot is displayed. Default is True. + color : str, optional + The color of the Nyquist plot. Default is 'b' (blue). + grid : bool, optional + If True, grid lines are displayed. Default is False. + **kwargs + Additional keyword arguments for customization. + + Examples + ======== + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy.physics.control.control_plots import nichols_plot + >>> tf1 = TransferFunction(1.5, s**2+14*s+40.02, s) + >>> nichols_plot(tf1) # doctest: +SKIP + + See Also + ======== + + nyquist_plot, bode_plot + + """ + _check_system(system) + magnitude_dB_expr, phase_deg_expr, w = nichols_plot_expr(system) + w_values = [(w, initial_omega, final_omega)] + p = plot_parametric( + (phase_deg_expr, magnitude_dB_expr), + *w_values, + show=False, + line_color=color, + title=f'Nichols Plot of ${latex(system)}$', + xlabel='Phase [deg]', + ylabel='Magnitude [dB]', + size=(6,5), + kwargs=kwargs) + if show: + p.show() + return + return p diff --git a/venv/lib/python3.11/site-packages/sympy/physics/control/lti.py b/venv/lib/python3.11/site-packages/sympy/physics/control/lti.py new file mode 100644 index 0000000000000000000000000000000000000000..480a1ec71d8c4dd07a51d67304a0b6e20a90691e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/control/lti.py @@ -0,0 +1,5001 @@ +from typing import Type +from sympy import Interval, numer, Rational, solveset +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.evalf import EvalfMixin +from sympy.core.expr import Expr +from sympy.core.function import expand +from sympy.core.logic import fuzzy_and +from sympy.core.mul import Mul +from sympy.core.numbers import I, pi, oo +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import Dummy, Symbol +from sympy.functions import Abs +from sympy.core.sympify import sympify, _sympify +from sympy.matrices import Matrix, ImmutableMatrix, ImmutableDenseMatrix, eye, ShapeError, zeros +from sympy.functions.elementary.exponential import (exp, log) +from sympy.matrices.expressions import MatMul, MatAdd +from sympy.polys import Poly, rootof +from sympy.polys.polyroots import roots +from sympy.polys.polytools import (cancel, degree) +from sympy.series import limit +from sympy.utilities.misc import filldedent +from sympy.solvers.ode.systems import linodesolve +from sympy.solvers.solveset import linsolve, linear_eq_to_matrix + +from mpmath.libmp.libmpf import prec_to_dps + +__all__ = ['TransferFunction', 'PIDController', 'Series', 'MIMOSeries', 'Parallel', 'MIMOParallel', + 'Feedback', 'MIMOFeedback', 'TransferFunctionMatrix', 'StateSpace', 'gbt', 'bilinear', 'forward_diff', 'backward_diff', + 'phase_margin', 'gain_margin'] + +def _roots(poly, var): + """ like roots, but works on higher-order polynomials. """ + r = roots(poly, var, multiple=True) + n = degree(poly) + if len(r) != n: + r = [rootof(poly, var, k) for k in range(n)] + return r + +def gbt(tf, sample_per, alpha): + r""" + Returns falling coefficients of H(z) from numerator and denominator. + + Explanation + =========== + + Where H(z) is the corresponding discretized transfer function, + discretized with the generalised bilinear transformation method. + H(z) is obtained from the continuous transfer function H(s) + by substituting $s(z) = \frac{z-1}{T(\alpha z + (1-\alpha))}$ into H(s), where T is the + sample period. + Coefficients are falling, i.e. $H(z) = \frac{az+b}{cz+d}$ is returned + as [a, b], [c, d]. + + Examples + ======== + + >>> from sympy.physics.control.lti import TransferFunction, gbt + >>> from sympy.abc import s, L, R, T + + >>> tf = TransferFunction(1, s*L + R, s) + >>> numZ, denZ = gbt(tf, T, 0.5) + >>> numZ + [T/(2*(L + R*T/2)), T/(2*(L + R*T/2))] + >>> denZ + [1, (-L + R*T/2)/(L + R*T/2)] + + >>> numZ, denZ = gbt(tf, T, 0) + >>> numZ + [T/L] + >>> denZ + [1, (-L + R*T)/L] + + >>> numZ, denZ = gbt(tf, T, 1) + >>> numZ + [T/(L + R*T), 0] + >>> denZ + [1, -L/(L + R*T)] + + >>> numZ, denZ = gbt(tf, T, 0.3) + >>> numZ + [3*T/(10*(L + 3*R*T/10)), 7*T/(10*(L + 3*R*T/10))] + >>> denZ + [1, (-L + 7*R*T/10)/(L + 3*R*T/10)] + + References + ========== + + .. [1] https://www.polyu.edu.hk/ama/profile/gfzhang/Research/ZCC09_IJC.pdf + """ + if not tf.is_SISO: + raise NotImplementedError("Not implemented for MIMO systems.") + + T = sample_per # and sample period T + s = tf.var + z = s # dummy discrete variable z + + np = tf.num.as_poly(s).all_coeffs() + dp = tf.den.as_poly(s).all_coeffs() + alpha = Rational(alpha).limit_denominator(1000) + + # The next line results from multiplying H(z) with z^N/z^N + N = max(len(np), len(dp)) - 1 + num = Add(*[ T**(N-i) * c * (z-1)**i * (alpha * z + 1 - alpha)**(N-i) for c, i in zip(np[::-1], range(len(np))) ]) + den = Add(*[ T**(N-i) * c * (z-1)**i * (alpha * z + 1 - alpha)**(N-i) for c, i in zip(dp[::-1], range(len(dp))) ]) + + num_coefs = num.as_poly(z).all_coeffs() + den_coefs = den.as_poly(z).all_coeffs() + + para = den_coefs[0] + num_coefs = [coef/para for coef in num_coefs] + den_coefs = [coef/para for coef in den_coefs] + + return num_coefs, den_coefs + +def bilinear(tf, sample_per): + r""" + Returns falling coefficients of H(z) from numerator and denominator. + + Explanation + =========== + + Where H(z) is the corresponding discretized transfer function, + discretized with the bilinear transform method. + H(z) is obtained from the continuous transfer function H(s) + by substituting $s(z) = \frac{2}{T}\frac{z-1}{z+1}$ into H(s), where T is the + sample period. + Coefficients are falling, i.e. $H(z) = \frac{az+b}{cz+d}$ is returned + as [a, b], [c, d]. + + Examples + ======== + + >>> from sympy.physics.control.lti import TransferFunction, bilinear + >>> from sympy.abc import s, L, R, T + + >>> tf = TransferFunction(1, s*L + R, s) + >>> numZ, denZ = bilinear(tf, T) + >>> numZ + [T/(2*(L + R*T/2)), T/(2*(L + R*T/2))] + >>> denZ + [1, (-L + R*T/2)/(L + R*T/2)] + """ + return gbt(tf, sample_per, S.Half) + +def forward_diff(tf, sample_per): + r""" + Returns falling coefficients of H(z) from numerator and denominator. + + Explanation + =========== + + Where H(z) is the corresponding discretized transfer function, + discretized with the forward difference transform method. + H(z) is obtained from the continuous transfer function H(s) + by substituting $s(z) = \frac{z-1}{T}$ into H(s), where T is the + sample period. + Coefficients are falling, i.e. $H(z) = \frac{az+b}{cz+d}$ is returned + as [a, b], [c, d]. + + Examples + ======== + + >>> from sympy.physics.control.lti import TransferFunction, forward_diff + >>> from sympy.abc import s, L, R, T + + >>> tf = TransferFunction(1, s*L + R, s) + >>> numZ, denZ = forward_diff(tf, T) + >>> numZ + [T/L] + >>> denZ + [1, (-L + R*T)/L] + """ + return gbt(tf, sample_per, S.Zero) + +def backward_diff(tf, sample_per): + r""" + Returns falling coefficients of H(z) from numerator and denominator. + + Explanation + =========== + + Where H(z) is the corresponding discretized transfer function, + discretized with the backward difference transform method. + H(z) is obtained from the continuous transfer function H(s) + by substituting $s(z) = \frac{z-1}{Tz}$ into H(s), where T is the + sample period. + Coefficients are falling, i.e. $H(z) = \frac{az+b}{cz+d}$ is returned + as [a, b], [c, d]. + + Examples + ======== + + >>> from sympy.physics.control.lti import TransferFunction, backward_diff + >>> from sympy.abc import s, L, R, T + + >>> tf = TransferFunction(1, s*L + R, s) + >>> numZ, denZ = backward_diff(tf, T) + >>> numZ + [T/(L + R*T), 0] + >>> denZ + [1, -L/(L + R*T)] + """ + return gbt(tf, sample_per, S.One) + +def phase_margin(system): + r""" + Returns the phase margin of a continuous time system. + Only applicable to Transfer Functions which can generate valid bode plots. + + Raises + ====== + + NotImplementedError + When time delay terms are present in the system. + + ValueError + When a SISO LTI system is not passed. + + When more than one free symbol is present in the system. + The only variable in the transfer function should be + the variable of the Laplace transform. + + Examples + ======== + + >>> from sympy.physics.control import TransferFunction, phase_margin + >>> from sympy.abc import s + + >>> tf = TransferFunction(1, s**3 + 2*s**2 + s, s) + >>> phase_margin(tf) + 180*(-pi + atan((-1 + (-2*18**(1/3)/(9 + sqrt(93))**(1/3) + 12**(1/3)*(9 + sqrt(93))**(1/3))**2/36)/(-12**(1/3)*(9 + sqrt(93))**(1/3)/3 + 2*18**(1/3)/(3*(9 + sqrt(93))**(1/3)))))/pi + 180 + >>> phase_margin(tf).n() + 21.3863897518751 + + >>> tf1 = TransferFunction(s**3, s**2 + 5*s, s) + >>> phase_margin(tf1) + -180 + 180*(atan(sqrt(2)*(-51/10 - sqrt(101)/10)*sqrt(1 + sqrt(101))/(2*(sqrt(101)/2 + 51/2))) + pi)/pi + >>> phase_margin(tf1).n() + -25.1783920627277 + + >>> tf2 = TransferFunction(1, s + 1, s) + >>> phase_margin(tf2) + -180 + + See Also + ======== + + gain_margin + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Phase_margin + + """ + from sympy.functions import arg + + if not isinstance(system, SISOLinearTimeInvariant): + raise ValueError("Margins are only applicable for SISO LTI systems.") + + _w = Dummy("w", real=True) + repl = I*_w + expr = system.to_expr() + len_free_symbols = len(expr.free_symbols) + if expr.has(exp): + raise NotImplementedError("Margins for systems with Time delay terms are not supported.") + elif len_free_symbols > 1: + raise ValueError("Extra degree of freedom found. Make sure" + " that there are no free symbols in the dynamical system other" + " than the variable of Laplace transform.") + + w_expr = expr.subs({system.var: repl}) + + mag = 20*log(Abs(w_expr), 10) + mag_sol = list(solveset(mag, _w, Interval(0, oo, left_open=True))) + + if (len(mag_sol) == 0): + pm = S(-180) + else: + wcp = mag_sol[0] + pm = ((arg(w_expr)*S(180)/pi).subs({_w:wcp}) + S(180)) % 360 + + if(pm >= 180): + pm = pm - 360 + + return pm + +def gain_margin(system): + r""" + Returns the gain margin of a continuous time system. + Only applicable to Transfer Functions which can generate valid bode plots. + + Raises + ====== + + NotImplementedError + When time delay terms are present in the system. + + ValueError + When a SISO LTI system is not passed. + + When more than one free symbol is present in the system. + The only variable in the transfer function should be + the variable of the Laplace transform. + + Examples + ======== + + >>> from sympy.physics.control import TransferFunction, gain_margin + >>> from sympy.abc import s + + >>> tf = TransferFunction(1, s**3 + 2*s**2 + s, s) + >>> gain_margin(tf) + 20*log(2)/log(10) + >>> gain_margin(tf).n() + 6.02059991327962 + + >>> tf1 = TransferFunction(s**3, s**2 + 5*s, s) + >>> gain_margin(tf1) + oo + + See Also + ======== + + phase_margin + + References + ========== + + https://en.wikipedia.org/wiki/Bode_plot + + """ + if not isinstance(system, SISOLinearTimeInvariant): + raise ValueError("Margins are only applicable for SISO LTI systems.") + + _w = Dummy("w", real=True) + repl = I*_w + expr = system.to_expr() + len_free_symbols = len(expr.free_symbols) + if expr.has(exp): + raise NotImplementedError("Margins for systems with Time delay terms are not supported.") + elif len_free_symbols > 1: + raise ValueError("Extra degree of freedom found. Make sure" + " that there are no free symbols in the dynamical system other" + " than the variable of Laplace transform.") + + w_expr = expr.subs({system.var: repl}) + + mag = 20*log(Abs(w_expr), 10) + phase = w_expr + phase_sol = list(solveset(numer(phase.as_real_imag()[1].cancel()),_w, Interval(0, oo, left_open = True))) + + if (len(phase_sol) == 0): + gm = oo + else: + wcg = phase_sol[0] + gm = -mag.subs({_w:wcg}) + + return gm + +class LinearTimeInvariant(Basic, EvalfMixin): + """A common class for all the Linear Time-Invariant Dynamical Systems.""" + + _clstype: Type + + # Users should not directly interact with this class. + def __new__(cls, *system, **kwargs): + if cls is LinearTimeInvariant: + raise NotImplementedError('The LTICommon class is not meant to be used directly.') + return super(LinearTimeInvariant, cls).__new__(cls, *system, **kwargs) + + @classmethod + def _check_args(cls, args): + if not args: + raise ValueError("At least 1 argument must be passed.") + if not all(isinstance(arg, cls._clstype) for arg in args): + raise TypeError(f"All arguments must be of type {cls._clstype}.") + var_set = {arg.var for arg in args} + if len(var_set) != 1: + raise ValueError(filldedent(f""" + All transfer functions should use the same complex variable + of the Laplace transform. {len(var_set)} different + values found.""")) + + @property + def is_SISO(self): + """Returns `True` if the passed LTI system is SISO else returns False.""" + return self._is_SISO + + +class SISOLinearTimeInvariant(LinearTimeInvariant): + """A common class for all the SISO Linear Time-Invariant Dynamical Systems.""" + # Users should not directly interact with this class. + + @property + def num_inputs(self): + """Return the number of inputs for SISOLinearTimeInvariant.""" + return 1 + + @property + def num_outputs(self): + """Return the number of outputs for SISOLinearTimeInvariant.""" + return 1 + + _is_SISO = True + + +class MIMOLinearTimeInvariant(LinearTimeInvariant): + """A common class for all the MIMO Linear Time-Invariant Dynamical Systems.""" + # Users should not directly interact with this class. + _is_SISO = False + + +SISOLinearTimeInvariant._clstype = SISOLinearTimeInvariant +MIMOLinearTimeInvariant._clstype = MIMOLinearTimeInvariant + + +def _check_other_SISO(func): + def wrapper(*args, **kwargs): + if not isinstance(args[-1], SISOLinearTimeInvariant): + return NotImplemented + else: + return func(*args, **kwargs) + return wrapper + + +def _check_other_MIMO(func): + def wrapper(*args, **kwargs): + if not isinstance(args[-1], MIMOLinearTimeInvariant): + return NotImplemented + else: + return func(*args, **kwargs) + return wrapper + + +class TransferFunction(SISOLinearTimeInvariant): + r""" + A class for representing LTI (Linear, time-invariant) systems that can be strictly described + by ratio of polynomials in the Laplace transform complex variable. The arguments + are ``num``, ``den``, and ``var``, where ``num`` and ``den`` are numerator and + denominator polynomials of the ``TransferFunction`` respectively, and the third argument is + a complex variable of the Laplace transform used by these polynomials of the transfer function. + ``num`` and ``den`` can be either polynomials or numbers, whereas ``var`` + has to be a :py:class:`~.Symbol`. + + Explanation + =========== + + Generally, a dynamical system representing a physical model can be described in terms of Linear + Ordinary Differential Equations like - + + $b_{m}y^{\left(m\right)}+b_{m-1}y^{\left(m-1\right)}+\dots+b_{1}y^{\left(1\right)}+b_{0}y= + a_{n}x^{\left(n\right)}+a_{n-1}x^{\left(n-1\right)}+\dots+a_{1}x^{\left(1\right)}+a_{0}x$ + + Here, $x$ is the input signal and $y$ is the output signal and superscript on both is the order of derivative + (not exponent). Derivative is taken with respect to the independent variable, $t$. Also, generally $m$ is greater + than $n$. + + It is not feasible to analyse the properties of such systems in their native form therefore, we use + mathematical tools like Laplace transform to get a better perspective. Taking the Laplace transform + of both the sides in the equation (at zero initial conditions), we get - + + $\mathcal{L}[b_{m}y^{\left(m\right)}+b_{m-1}y^{\left(m-1\right)}+\dots+b_{1}y^{\left(1\right)}+b_{0}y]= + \mathcal{L}[a_{n}x^{\left(n\right)}+a_{n-1}x^{\left(n-1\right)}+\dots+a_{1}x^{\left(1\right)}+a_{0}x]$ + + Using the linearity property of Laplace transform and also considering zero initial conditions + (i.e. $y(0^{-}) = 0$, $y'(0^{-}) = 0$ and so on), the equation + above gets translated to - + + $b_{m}\mathcal{L}[y^{\left(m\right)}]+\dots+b_{1}\mathcal{L}[y^{\left(1\right)}]+b_{0}\mathcal{L}[y]= + a_{n}\mathcal{L}[x^{\left(n\right)}]+\dots+a_{1}\mathcal{L}[x^{\left(1\right)}]+a_{0}\mathcal{L}[x]$ + + Now, applying Derivative property of Laplace transform, + + $b_{m}s^{m}\mathcal{L}[y]+\dots+b_{1}s\mathcal{L}[y]+b_{0}\mathcal{L}[y]= + a_{n}s^{n}\mathcal{L}[x]+\dots+a_{1}s\mathcal{L}[x]+a_{0}\mathcal{L}[x]$ + + Here, the superscript on $s$ is **exponent**. Note that the zero initial conditions assumption, mentioned above, is very important + and cannot be ignored otherwise the dynamical system cannot be considered time-independent and the simplified equation above + cannot be reached. + + Collecting $\mathcal{L}[y]$ and $\mathcal{L}[x]$ terms from both the sides and taking the ratio + $\frac{ \mathcal{L}\left\{y\right\} }{ \mathcal{L}\left\{x\right\} }$, we get the typical rational form of transfer + function. + + The numerator of the transfer function is, therefore, the Laplace transform of the output signal + (The signals are represented as functions of time) and similarly, the denominator + of the transfer function is the Laplace transform of the input signal. It is also a convention + to denote the input and output signal's Laplace transform with capital alphabets like shown below. + + $H(s) = \frac{Y(s)}{X(s)} = \frac{ \mathcal{L}\left\{y(t)\right\} }{ \mathcal{L}\left\{x(t)\right\} }$ + + $s$, also known as complex frequency, is a complex variable in the Laplace domain. It corresponds to the + equivalent variable $t$, in the time domain. Transfer functions are sometimes also referred to as the Laplace + transform of the system's impulse response. Transfer function, $H$, is represented as a rational + function in $s$ like, + + $H(s) =\ \frac{a_{n}s^{n}+a_{n-1}s^{n-1}+\dots+a_{1}s+a_{0}}{b_{m}s^{m}+b_{m-1}s^{m-1}+\dots+b_{1}s+b_{0}}$ + + Parameters + ========== + + num : Expr, Number + The numerator polynomial of the transfer function. + den : Expr, Number + The denominator polynomial of the transfer function. + var : Symbol + Complex variable of the Laplace transform used by the + polynomials of the transfer function. + + Raises + ====== + + TypeError + When ``var`` is not a Symbol or when ``num`` or ``den`` is not a + number or a polynomial. + ValueError + When ``den`` is zero. + + Examples + ======== + + >>> from sympy.abc import s, p, a + >>> from sympy.physics.control.lti import TransferFunction + >>> tf1 = TransferFunction(s + a, s**2 + s + 1, s) + >>> tf1 + TransferFunction(a + s, s**2 + s + 1, s) + >>> tf1.num + a + s + >>> tf1.den + s**2 + s + 1 + >>> tf1.var + s + >>> tf1.args + (a + s, s**2 + s + 1, s) + + Any complex variable can be used for ``var``. + + >>> tf2 = TransferFunction(a*p**3 - a*p**2 + s*p, p + a**2, p) + >>> tf2 + TransferFunction(a*p**3 - a*p**2 + p*s, a**2 + p, p) + >>> tf3 = TransferFunction((p + 3)*(p - 1), (p - 1)*(p + 5), p) + >>> tf3 + TransferFunction((p - 1)*(p + 3), (p - 1)*(p + 5), p) + + To negate a transfer function the ``-`` operator can be prepended: + + >>> tf4 = TransferFunction(-a + s, p**2 + s, p) + >>> -tf4 + TransferFunction(a - s, p**2 + s, p) + >>> tf5 = TransferFunction(s**4 - 2*s**3 + 5*s + 4, s + 4, s) + >>> -tf5 + TransferFunction(-s**4 + 2*s**3 - 5*s - 4, s + 4, s) + + You can use a float or an integer (or other constants) as numerator and denominator: + + >>> tf6 = TransferFunction(1/2, 4, s) + >>> tf6.num + 0.500000000000000 + >>> tf6.den + 4 + >>> tf6.var + s + >>> tf6.args + (0.5, 4, s) + + You can take the integer power of a transfer function using the ``**`` operator: + + >>> tf7 = TransferFunction(s + a, s - a, s) + >>> tf7**3 + TransferFunction((a + s)**3, (-a + s)**3, s) + >>> tf7**0 + TransferFunction(1, 1, s) + >>> tf8 = TransferFunction(p + 4, p - 3, p) + >>> tf8**-1 + TransferFunction(p - 3, p + 4, p) + + Addition, subtraction, and multiplication of transfer functions can form + unevaluated ``Series`` or ``Parallel`` objects. + + >>> tf9 = TransferFunction(s + 1, s**2 + s + 1, s) + >>> tf10 = TransferFunction(s - p, s + 3, s) + >>> tf11 = TransferFunction(4*s**2 + 2*s - 4, s - 1, s) + >>> tf12 = TransferFunction(1 - s, s**2 + 4, s) + >>> tf9 + tf10 + Parallel(TransferFunction(s + 1, s**2 + s + 1, s), TransferFunction(-p + s, s + 3, s)) + >>> tf10 - tf11 + Parallel(TransferFunction(-p + s, s + 3, s), TransferFunction(-4*s**2 - 2*s + 4, s - 1, s)) + >>> tf9 * tf10 + Series(TransferFunction(s + 1, s**2 + s + 1, s), TransferFunction(-p + s, s + 3, s)) + >>> tf10 - (tf9 + tf12) + Parallel(TransferFunction(-p + s, s + 3, s), TransferFunction(-s - 1, s**2 + s + 1, s), TransferFunction(s - 1, s**2 + 4, s)) + >>> tf10 - (tf9 * tf12) + Parallel(TransferFunction(-p + s, s + 3, s), Series(TransferFunction(-1, 1, s), TransferFunction(s + 1, s**2 + s + 1, s), TransferFunction(1 - s, s**2 + 4, s))) + >>> tf11 * tf10 * tf9 + Series(TransferFunction(4*s**2 + 2*s - 4, s - 1, s), TransferFunction(-p + s, s + 3, s), TransferFunction(s + 1, s**2 + s + 1, s)) + >>> tf9 * tf11 + tf10 * tf12 + Parallel(Series(TransferFunction(s + 1, s**2 + s + 1, s), TransferFunction(4*s**2 + 2*s - 4, s - 1, s)), Series(TransferFunction(-p + s, s + 3, s), TransferFunction(1 - s, s**2 + 4, s))) + >>> (tf9 + tf12) * (tf10 + tf11) + Series(Parallel(TransferFunction(s + 1, s**2 + s + 1, s), TransferFunction(1 - s, s**2 + 4, s)), Parallel(TransferFunction(-p + s, s + 3, s), TransferFunction(4*s**2 + 2*s - 4, s - 1, s))) + + These unevaluated ``Series`` or ``Parallel`` objects can convert into the + resultant transfer function using ``.doit()`` method or by ``.rewrite(TransferFunction)``. + + >>> ((tf9 + tf10) * tf12).doit() + TransferFunction((1 - s)*((-p + s)*(s**2 + s + 1) + (s + 1)*(s + 3)), (s + 3)*(s**2 + 4)*(s**2 + s + 1), s) + >>> (tf9 * tf10 - tf11 * tf12).rewrite(TransferFunction) + TransferFunction(-(1 - s)*(s + 3)*(s**2 + s + 1)*(4*s**2 + 2*s - 4) + (-p + s)*(s - 1)*(s + 1)*(s**2 + 4), (s - 1)*(s + 3)*(s**2 + 4)*(s**2 + s + 1), s) + + See Also + ======== + + Feedback, Series, Parallel + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Transfer_function + .. [2] https://en.wikipedia.org/wiki/Laplace_transform + + """ + def __new__(cls, num, den, var): + num, den = _sympify(num), _sympify(den) + + if not isinstance(var, Symbol): + raise TypeError("Variable input must be a Symbol.") + + if den == 0: + raise ValueError("TransferFunction cannot have a zero denominator.") + + if (((isinstance(num, (Expr, TransferFunction, Series, Parallel)) and num.has(Symbol)) or num.is_number) and + ((isinstance(den, (Expr, TransferFunction, Series, Parallel)) and den.has(Symbol)) or den.is_number)): + cls.is_StateSpace_object = False + return super(TransferFunction, cls).__new__(cls, num, den, var) + + else: + raise TypeError("Unsupported type for numerator or denominator of TransferFunction.") + + @classmethod + def from_rational_expression(cls, expr, var=None): + r""" + Creates a new ``TransferFunction`` efficiently from a rational expression. + + Parameters + ========== + + expr : Expr, Number + The rational expression representing the ``TransferFunction``. + var : Symbol, optional + Complex variable of the Laplace transform used by the + polynomials of the transfer function. + + Raises + ====== + + ValueError + When ``expr`` is of type ``Number`` and optional parameter ``var`` + is not passed. + + When ``expr`` has more than one variables and an optional parameter + ``var`` is not passed. + ZeroDivisionError + When denominator of ``expr`` is zero or it has ``ComplexInfinity`` + in its numerator. + + Examples + ======== + + >>> from sympy.abc import s, p, a + >>> from sympy.physics.control.lti import TransferFunction + >>> expr1 = (s + 5)/(3*s**2 + 2*s + 1) + >>> tf1 = TransferFunction.from_rational_expression(expr1) + >>> tf1 + TransferFunction(s + 5, 3*s**2 + 2*s + 1, s) + >>> expr2 = (a*p**3 - a*p**2 + s*p)/(p + a**2) # Expr with more than one variables + >>> tf2 = TransferFunction.from_rational_expression(expr2, p) + >>> tf2 + TransferFunction(a*p**3 - a*p**2 + p*s, a**2 + p, p) + + In case of conflict between two or more variables in a expression, SymPy will + raise a ``ValueError``, if ``var`` is not passed by the user. + + >>> tf = TransferFunction.from_rational_expression((a + a*s)/(s**2 + s + 1)) + Traceback (most recent call last): + ... + ValueError: Conflicting values found for positional argument `var` ({a, s}). Specify it manually. + + This can be corrected by specifying the ``var`` parameter manually. + + >>> tf = TransferFunction.from_rational_expression((a + a*s)/(s**2 + s + 1), s) + >>> tf + TransferFunction(a*s + a, s**2 + s + 1, s) + + ``var`` also need to be specified when ``expr`` is a ``Number`` + + >>> tf3 = TransferFunction.from_rational_expression(10, s) + >>> tf3 + TransferFunction(10, 1, s) + + """ + expr = _sympify(expr) + if var is None: + _free_symbols = expr.free_symbols + _len_free_symbols = len(_free_symbols) + if _len_free_symbols == 1: + var = list(_free_symbols)[0] + elif _len_free_symbols == 0: + raise ValueError(filldedent(""" + Positional argument `var` not found in the + TransferFunction defined. Specify it manually.""")) + else: + raise ValueError(filldedent(""" + Conflicting values found for positional argument `var` ({}). + Specify it manually.""".format(_free_symbols))) + + _num, _den = expr.as_numer_denom() + if _den == 0 or _num.has(S.ComplexInfinity): + raise ZeroDivisionError("TransferFunction cannot have a zero denominator.") + return cls(_num, _den, var) + + @classmethod + def from_coeff_lists(cls, num_list, den_list, var): + r""" + Creates a new ``TransferFunction`` efficiently from a list of coefficients. + + Parameters + ========== + + num_list : Sequence + Sequence comprising of numerator coefficients. + den_list : Sequence + Sequence comprising of denominator coefficients. + var : Symbol + Complex variable of the Laplace transform used by the + polynomials of the transfer function. + + Raises + ====== + + ZeroDivisionError + When the constructed denominator is zero. + + Examples + ======== + + >>> from sympy.abc import s, p + >>> from sympy.physics.control.lti import TransferFunction + >>> num = [1, 0, 2] + >>> den = [3, 2, 2, 1] + >>> tf = TransferFunction.from_coeff_lists(num, den, s) + >>> tf + TransferFunction(s**2 + 2, 3*s**3 + 2*s**2 + 2*s + 1, s) + >>> #Create a Transfer Function with more than one variable + >>> tf1 = TransferFunction.from_coeff_lists([p, 1], [2*p, 0, 4], s) + >>> tf1 + TransferFunction(p*s + 1, 2*p*s**2 + 4, s) + + """ + num_list = num_list[::-1] + den_list = den_list[::-1] + num_var_powers = [var**i for i in range(len(num_list))] + den_var_powers = [var**i for i in range(len(den_list))] + + _num = sum(coeff * var_power for coeff, var_power in zip(num_list, num_var_powers)) + _den = sum(coeff * var_power for coeff, var_power in zip(den_list, den_var_powers)) + + if _den == 0: + raise ZeroDivisionError("TransferFunction cannot have a zero denominator.") + + return cls(_num, _den, var) + + @classmethod + def from_zpk(cls, zeros, poles, gain, var): + r""" + Creates a new ``TransferFunction`` from given zeros, poles and gain. + + Parameters + ========== + + zeros : Sequence + Sequence comprising of zeros of transfer function. + poles : Sequence + Sequence comprising of poles of transfer function. + gain : Number, Symbol, Expression + A scalar value specifying gain of the model. + var : Symbol + Complex variable of the Laplace transform used by the + polynomials of the transfer function. + + Examples + ======== + + >>> from sympy.abc import s, p, k + >>> from sympy.physics.control.lti import TransferFunction + >>> zeros = [1, 2, 3] + >>> poles = [6, 5, 4] + >>> gain = 7 + >>> tf = TransferFunction.from_zpk(zeros, poles, gain, s) + >>> tf + TransferFunction(7*(s - 3)*(s - 2)*(s - 1), (s - 6)*(s - 5)*(s - 4), s) + >>> #Create a Transfer Function with variable poles and zeros + >>> tf1 = TransferFunction.from_zpk([p, k], [p + k, p - k], 2, s) + >>> tf1 + TransferFunction(2*(-k + s)*(-p + s), (-k - p + s)*(k - p + s), s) + >>> #Complex poles or zeros are acceptable + >>> tf2 = TransferFunction.from_zpk([0], [1-1j, 1+1j, 2], -2, s) + >>> tf2 + TransferFunction(-2*s, (s - 2)*(s - 1.0 - 1.0*I)*(s - 1.0 + 1.0*I), s) + + """ + num_poly = 1 + den_poly = 1 + for zero in zeros: + num_poly *= var - zero + for pole in poles: + den_poly *= var - pole + + return cls(gain*num_poly, den_poly, var) + + @property + def num(self): + """ + Returns the numerator polynomial of the transfer function. + + Examples + ======== + + >>> from sympy.abc import s, p + >>> from sympy.physics.control.lti import TransferFunction + >>> G1 = TransferFunction(s**2 + p*s + 3, s - 4, s) + >>> G1.num + p*s + s**2 + 3 + >>> G2 = TransferFunction((p + 5)*(p - 3), (p - 3)*(p + 1), p) + >>> G2.num + (p - 3)*(p + 5) + + """ + return self.args[0] + + @property + def den(self): + """ + Returns the denominator polynomial of the transfer function. + + Examples + ======== + + >>> from sympy.abc import s, p + >>> from sympy.physics.control.lti import TransferFunction + >>> G1 = TransferFunction(s + 4, p**3 - 2*p + 4, s) + >>> G1.den + p**3 - 2*p + 4 + >>> G2 = TransferFunction(3, 4, s) + >>> G2.den + 4 + + """ + return self.args[1] + + @property + def var(self): + """ + Returns the complex variable of the Laplace transform used by the polynomials of + the transfer function. + + Examples + ======== + + >>> from sympy.abc import s, p + >>> from sympy.physics.control.lti import TransferFunction + >>> G1 = TransferFunction(p**2 + 2*p + 4, p - 6, p) + >>> G1.var + p + >>> G2 = TransferFunction(0, s - 5, s) + >>> G2.var + s + + """ + return self.args[2] + + def _eval_subs(self, old, new): + arg_num = self.num.subs(old, new) + arg_den = self.den.subs(old, new) + argnew = TransferFunction(arg_num, arg_den, self.var) + return self if old == self.var else argnew + + def _eval_evalf(self, prec): + return TransferFunction( + self.num._eval_evalf(prec), + self.den._eval_evalf(prec), + self.var) + + def _eval_simplify(self, **kwargs): + tf = cancel(Mul(self.num, 1/self.den, evaluate=False), expand=False).as_numer_denom() + num_, den_ = tf[0], tf[1] + return TransferFunction(num_, den_, self.var) + + def _eval_rewrite_as_StateSpace(self, *args): + """ + Returns the equivalent space model of the transfer function model. + The state space model will be returned in the controllable canonical form. + + Unlike the space state to transfer function model conversion, the transfer function + to state space model conversion is not unique. There can be multiple state space + representations of a given transfer function model. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy.physics.control import TransferFunction, StateSpace + >>> tf = TransferFunction(s**2 + 1, s**3 + 2*s + 10, s) + >>> tf.rewrite(StateSpace) + StateSpace(Matrix([ + [ 0, 1, 0], + [ 0, 0, 1], + [-10, -2, 0]]), Matrix([ + [0], + [0], + [1]]), Matrix([[1, 0, 1]]), Matrix([[0]])) + + """ + if not self.is_proper: + raise ValueError("Transfer Function must be proper.") + + num_poly = Poly(self.num, self.var) + den_poly = Poly(self.den, self.var) + n = den_poly.degree() + + num_coeffs = num_poly.all_coeffs() + den_coeffs = den_poly.all_coeffs() + diff = n - num_poly.degree() + num_coeffs = [0]*diff + num_coeffs + + a = den_coeffs[1:] + a_mat = Matrix([[(-1)*coefficient/den_coeffs[0] for coefficient in reversed(a)]]) + vert = zeros(n-1, 1) + mat = eye(n-1) + A = vert.row_join(mat) + A = A.col_join(a_mat) + + B = zeros(n, 1) + B[n-1] = 1 + + i = n + C = [] + while(i > 0): + C.append(num_coeffs[i] - den_coeffs[i]*num_coeffs[0]) + i -= 1 + C = Matrix([C]) + + D = Matrix([num_coeffs[0]]) + + return StateSpace(A, B, C, D) + + def expand(self): + """ + Returns the transfer function with numerator and denominator + in expanded form. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction + >>> G1 = TransferFunction((a - s)**2, (s**2 + a)**2, s) + >>> G1.expand() + TransferFunction(a**2 - 2*a*s + s**2, a**2 + 2*a*s**2 + s**4, s) + >>> G2 = TransferFunction((p + 3*b)*(p - b), (p - b)*(p + 2*b), p) + >>> G2.expand() + TransferFunction(-3*b**2 + 2*b*p + p**2, -2*b**2 + b*p + p**2, p) + + """ + return TransferFunction(expand(self.num), expand(self.den), self.var) + + def dc_gain(self): + """ + Computes the gain of the response as the frequency approaches zero. + + The DC gain is infinite for systems with pure integrators. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction + >>> tf1 = TransferFunction(s + 3, s**2 - 9, s) + >>> tf1.dc_gain() + -1/3 + >>> tf2 = TransferFunction(p**2, p - 3 + p**3, p) + >>> tf2.dc_gain() + 0 + >>> tf3 = TransferFunction(a*p**2 - b, s + b, s) + >>> tf3.dc_gain() + (a*p**2 - b)/b + >>> tf4 = TransferFunction(1, s, s) + >>> tf4.dc_gain() + oo + + """ + m = Mul(self.num, Pow(self.den, -1, evaluate=False), evaluate=False) + return limit(m, self.var, 0) + + def poles(self): + """ + Returns the poles of a transfer function. + + Examples + ======== + + >>> from sympy.abc import s, p, a + >>> from sympy.physics.control.lti import TransferFunction + >>> tf1 = TransferFunction((p + 3)*(p - 1), (p - 1)*(p + 5), p) + >>> tf1.poles() + [-5, 1] + >>> tf2 = TransferFunction((1 - s)**2, (s**2 + 1)**2, s) + >>> tf2.poles() + [I, I, -I, -I] + >>> tf3 = TransferFunction(s**2, a*s + p, s) + >>> tf3.poles() + [-p/a] + + """ + return _roots(Poly(self.den, self.var), self.var) + + def zeros(self): + """ + Returns the zeros of a transfer function. + + Examples + ======== + + >>> from sympy.abc import s, p, a + >>> from sympy.physics.control.lti import TransferFunction + >>> tf1 = TransferFunction((p + 3)*(p - 1), (p - 1)*(p + 5), p) + >>> tf1.zeros() + [-3, 1] + >>> tf2 = TransferFunction((1 - s)**2, (s**2 + 1)**2, s) + >>> tf2.zeros() + [1, 1] + >>> tf3 = TransferFunction(s**2, a*s + p, s) + >>> tf3.zeros() + [0, 0] + + """ + return _roots(Poly(self.num, self.var), self.var) + + def eval_frequency(self, other): + """ + Returns the system response at any point in the real or complex plane. + + Examples + ======== + + >>> from sympy.abc import s, p, a + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy import I + >>> tf1 = TransferFunction(1, s**2 + 2*s + 1, s) + >>> omega = 0.1 + >>> tf1.eval_frequency(I*omega) + 1/(0.99 + 0.2*I) + >>> tf2 = TransferFunction(s**2, a*s + p, s) + >>> tf2.eval_frequency(2) + 4/(2*a + p) + >>> tf2.eval_frequency(I*2) + -4/(2*I*a + p) + """ + arg_num = self.num.subs(self.var, other) + arg_den = self.den.subs(self.var, other) + argnew = TransferFunction(arg_num, arg_den, self.var).to_expr() + return argnew.expand() + + def is_stable(self): + """ + Returns True if the transfer function is asymptotically stable; else False. + + This would not check the marginal or conditional stability of the system. + + Examples + ======== + + >>> from sympy.abc import s, p, a + >>> from sympy import symbols + >>> from sympy.physics.control.lti import TransferFunction + >>> q, r = symbols('q, r', negative=True) + >>> tf1 = TransferFunction((1 - s)**2, (s + 1)**2, s) + >>> tf1.is_stable() + True + >>> tf2 = TransferFunction((1 - p)**2, (s**2 + 1)**2, s) + >>> tf2.is_stable() + False + >>> tf3 = TransferFunction(4, q*s - r, s) + >>> tf3.is_stable() + False + >>> tf4 = TransferFunction(p + 1, a*p - s**2, p) + >>> tf4.is_stable() is None # Not enough info about the symbols to determine stability + True + + """ + return fuzzy_and(pole.as_real_imag()[0].is_negative for pole in self.poles()) + + def __add__(self, other): + if hasattr(other, "is_StateSpace_object") and other.is_StateSpace_object: + return Parallel(self, other) + elif isinstance(other, (TransferFunction, Series, Feedback)): + if not self.var == other.var: + raise ValueError(filldedent(""" + All the transfer functions should use the same complex variable + of the Laplace transform.""")) + return Parallel(self, other) + elif isinstance(other, Parallel): + if not self.var == other.var: + raise ValueError(filldedent(""" + All the transfer functions should use the same complex variable + of the Laplace transform.""")) + arg_list = list(other.args) + return Parallel(self, *arg_list) + else: + raise ValueError("TransferFunction cannot be added with {}.". + format(type(other))) + + def __radd__(self, other): + return self + other + + def __sub__(self, other): + if hasattr(other, "is_StateSpace_object") and other.is_StateSpace_object: + return Parallel(self, -other) + elif isinstance(other, (TransferFunction, Series)): + if not self.var == other.var: + raise ValueError(filldedent(""" + All the transfer functions should use the same complex variable + of the Laplace transform.""")) + return Parallel(self, -other) + elif isinstance(other, Parallel): + if not self.var == other.var: + raise ValueError(filldedent(""" + All the transfer functions should use the same complex variable + of the Laplace transform.""")) + arg_list = [-i for i in list(other.args)] + return Parallel(self, *arg_list) + else: + raise ValueError("{} cannot be subtracted from a TransferFunction." + .format(type(other))) + + def __rsub__(self, other): + return -self + other + + def __mul__(self, other): + if hasattr(other, "is_StateSpace_object") and other.is_StateSpace_object: + return Series(self, other) + elif isinstance(other, (TransferFunction, Parallel, Feedback)): + if not self.var == other.var: + raise ValueError(filldedent(""" + All the transfer functions should use the same complex variable + of the Laplace transform.""")) + return Series(self, other) + elif isinstance(other, Series): + if not self.var == other.var: + raise ValueError(filldedent(""" + All the transfer functions should use the same complex variable + of the Laplace transform.""")) + arg_list = list(other.args) + return Series(self, *arg_list) + else: + raise ValueError("TransferFunction cannot be multiplied with {}." + .format(type(other))) + + __rmul__ = __mul__ + + def __truediv__(self, other): + if isinstance(other, TransferFunction): + if not self.var == other.var: + raise ValueError(filldedent(""" + All the transfer functions should use the same complex variable + of the Laplace transform.""")) + return Series(self, TransferFunction(other.den, other.num, self.var)) + elif (isinstance(other, Parallel) and len(other.args + ) == 2 and isinstance(other.args[0], TransferFunction) + and isinstance(other.args[1], (Series, TransferFunction))): + + if not self.var == other.var: + raise ValueError(filldedent(""" + Both TransferFunction and Parallel should use the + same complex variable of the Laplace transform.""")) + if other.args[1] == self: + # plant and controller with unit feedback. + return Feedback(self, other.args[0]) + other_arg_list = list(other.args[1].args) if isinstance( + other.args[1], Series) else other.args[1] + if other_arg_list == other.args[1]: + return Feedback(self, other_arg_list) + elif self in other_arg_list: + other_arg_list.remove(self) + else: + return Feedback(self, Series(*other_arg_list)) + + if len(other_arg_list) == 1: + return Feedback(self, *other_arg_list) + else: + return Feedback(self, Series(*other_arg_list)) + else: + raise ValueError("TransferFunction cannot be divided by {}.". + format(type(other))) + + __rtruediv__ = __truediv__ + + def __pow__(self, p): + p = sympify(p) + if not p.is_Integer: + raise ValueError("Exponent must be an integer.") + if p is S.Zero: + return TransferFunction(1, 1, self.var) + elif p > 0: + num_, den_ = self.num**p, self.den**p + else: + p = abs(p) + num_, den_ = self.den**p, self.num**p + + return TransferFunction(num_, den_, self.var) + + def __neg__(self): + return TransferFunction(-self.num, self.den, self.var) + + @property + def is_proper(self): + """ + Returns True if degree of the numerator polynomial is less than + or equal to degree of the denominator polynomial, else False. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction + >>> tf1 = TransferFunction(b*s**2 + p**2 - a*p + s, b - p**2, s) + >>> tf1.is_proper + False + >>> tf2 = TransferFunction(p**2 - 4*p, p**3 + 3*p + 2, p) + >>> tf2.is_proper + True + + """ + return degree(self.num, self.var) <= degree(self.den, self.var) + + @property + def is_strictly_proper(self): + """ + Returns True if degree of the numerator polynomial is strictly less + than degree of the denominator polynomial, else False. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction + >>> tf1 = TransferFunction(a*p**2 + b*s, s - p, s) + >>> tf1.is_strictly_proper + False + >>> tf2 = TransferFunction(s**3 - 2, s**4 + 5*s + 6, s) + >>> tf2.is_strictly_proper + True + + """ + return degree(self.num, self.var) < degree(self.den, self.var) + + @property + def is_biproper(self): + """ + Returns True if degree of the numerator polynomial is equal to + degree of the denominator polynomial, else False. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction + >>> tf1 = TransferFunction(a*p**2 + b*s, s - p, s) + >>> tf1.is_biproper + True + >>> tf2 = TransferFunction(p**2, p + a, p) + >>> tf2.is_biproper + False + + """ + return degree(self.num, self.var) == degree(self.den, self.var) + + def to_expr(self): + """ + Converts a ``TransferFunction`` object to SymPy Expr. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction + >>> from sympy import Expr + >>> tf1 = TransferFunction(s, a*s**2 + 1, s) + >>> tf1.to_expr() + s/(a*s**2 + 1) + >>> isinstance(_, Expr) + True + >>> tf2 = TransferFunction(1, (p + 3*b)*(b - p), p) + >>> tf2.to_expr() + 1/((b - p)*(3*b + p)) + >>> tf3 = TransferFunction((s - 2)*(s - 3), (s - 1)*(s - 2)*(s - 3), s) + >>> tf3.to_expr() + ((s - 3)*(s - 2))/(((s - 3)*(s - 2)*(s - 1))) + + """ + + if self.num != 1: + return Mul(self.num, Pow(self.den, -1, evaluate=False), evaluate=False) + else: + return Pow(self.den, -1, evaluate=False) + + +class PIDController(TransferFunction): + r""" + A class for representing PID (Proportional-Integral-Derivative) + controllers in control systems. The PIDController class is a subclass + of TransferFunction, representing the controller's transfer function + in the Laplace domain. The arguments are ``kp``, ``ki``, ``kd``, + ``tf``, and ``var``, where ``kp``, ``ki``, and ``kd`` are the + proportional, integral, and derivative gains respectively.``tf`` + is the derivative filter time constant, which can be used to + filter out the noise and ``var`` is the complex variable used in + the transfer function. + + Parameters + ========== + + kp : Expr, Number + Proportional gain. Defaults to ``Symbol('kp')`` if not specified. + ki : Expr, Number + Integral gain. Defaults to ``Symbol('ki')`` if not specified. + kd : Expr, Number + Derivative gain. Defaults to ``Symbol('kd')`` if not specified. + tf : Expr, Number + Derivative filter time constant. Defaults to ``0`` if not specified. + var : Symbol + The complex frequency variable. Defaults to ``s`` if not specified. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.control.lti import PIDController + >>> kp, ki, kd = symbols('kp ki kd') + >>> p1 = PIDController(kp, ki, kd) + >>> print(p1) + PIDController(kp, ki, kd, 0, s) + >>> p1.doit() + TransferFunction(kd*s**2 + ki + kp*s, s, s) + >>> p1.kp + kp + >>> p1.ki + ki + >>> p1.kd + kd + >>> p1.tf + 0 + >>> p1.var + s + >>> p1.to_expr() + (kd*s**2 + ki + kp*s)/s + + See Also + ======== + + TransferFunction + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/PID_controller + .. [2] https://in.mathworks.com/help/control/ug/proportional-integral-derivative-pid-controllers.html + + """ + def __new__(cls, kp=Symbol('kp'), ki=Symbol('ki'), kd=Symbol('kd'), tf=0, var=Symbol('s')): + kp, ki, kd, tf = _sympify(kp), _sympify(ki), _sympify(kd), _sympify(tf) + num = kp*tf*var**2 + kp*var + ki*tf*var + ki + kd*var**2 + den = tf*var**2 + var + obj = TransferFunction.__new__(cls, num, den, var) + obj._kp, obj._ki, obj._kd, obj._tf = kp, ki, kd, tf + return obj + + def __repr__(self): + return f"PIDController({self.kp}, {self.ki}, {self.kd}, {self.tf}, {self.var})" + + __str__ = __repr__ + + @property + def kp(self): + """ + Returns the Proportional gain (kp) of the PIDController. + """ + return self._kp + + @property + def ki(self): + """ + Returns the Integral gain (ki) of the PIDController. + """ + return self._ki + + @property + def kd(self): + """ + Returns the Derivative gain (kd) of the PIDController. + """ + return self._kd + + @property + def tf(self): + """ + Returns the Derivative filter time constant (tf) of the PIDController. + """ + return self._tf + + def doit(self): + """ + Convert the PIDController into TransferFunction. + """ + return TransferFunction(self.num, self.den, self.var) + + +def _flatten_args(args, _cls): + temp_args = [] + for arg in args: + if isinstance(arg, _cls): + temp_args.extend(arg.args) + else: + temp_args.append(arg) + return tuple(temp_args) + + +def _dummify_args(_arg, var): + dummy_dict = {} + dummy_arg_list = [] + + for arg in _arg: + _s = Dummy() + dummy_dict[_s] = var + dummy_arg = arg.subs({var: _s}) + dummy_arg_list.append(dummy_arg) + + return dummy_arg_list, dummy_dict + + +class Series(SISOLinearTimeInvariant): + r""" + A class for representing a series configuration of SISO systems. + + Parameters + ========== + + args : SISOLinearTimeInvariant + SISO systems in a series configuration. + evaluate : Boolean, Keyword + When passed ``True``, returns the equivalent + ``Series(*args).doit()``. Set to ``False`` by default. + + Raises + ====== + + ValueError + When no argument is passed. + + ``var`` attribute is not same for every system. + TypeError + Any of the passed ``*args`` has unsupported type + + A combination of SISO and MIMO systems is + passed. There should be homogeneity in the + type of systems passed, SISO in this case. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy import Matrix + >>> from sympy.physics.control.lti import TransferFunction, Series, Parallel, StateSpace + >>> tf1 = TransferFunction(a*p**2 + b*s, s - p, s) + >>> tf2 = TransferFunction(s**3 - 2, s**4 + 5*s + 6, s) + >>> tf3 = TransferFunction(p**2, p + s, s) + >>> S1 = Series(tf1, tf2) + >>> S1 + Series(TransferFunction(a*p**2 + b*s, -p + s, s), TransferFunction(s**3 - 2, s**4 + 5*s + 6, s)) + >>> S1.var + s + >>> S2 = Series(tf2, Parallel(tf3, -tf1)) + >>> S2 + Series(TransferFunction(s**3 - 2, s**4 + 5*s + 6, s), Parallel(TransferFunction(p**2, p + s, s), TransferFunction(-a*p**2 - b*s, -p + s, s))) + >>> S2.var + s + >>> S3 = Series(Parallel(tf1, tf2), Parallel(tf2, tf3)) + >>> S3 + Series(Parallel(TransferFunction(a*p**2 + b*s, -p + s, s), TransferFunction(s**3 - 2, s**4 + 5*s + 6, s)), Parallel(TransferFunction(s**3 - 2, s**4 + 5*s + 6, s), TransferFunction(p**2, p + s, s))) + >>> S3.var + s + + You can get the resultant transfer function by using ``.doit()`` method: + + >>> S3 = Series(tf1, tf2, -tf3) + >>> S3.doit() + TransferFunction(-p**2*(s**3 - 2)*(a*p**2 + b*s), (-p + s)*(p + s)*(s**4 + 5*s + 6), s) + >>> S4 = Series(tf2, Parallel(tf1, -tf3)) + >>> S4.doit() + TransferFunction((s**3 - 2)*(-p**2*(-p + s) + (p + s)*(a*p**2 + b*s)), (-p + s)*(p + s)*(s**4 + 5*s + 6), s) + + You can also connect StateSpace which results in SISO + + >>> A1 = Matrix([[-1]]) + >>> B1 = Matrix([[1]]) + >>> C1 = Matrix([[-1]]) + >>> D1 = Matrix([1]) + >>> A2 = Matrix([[0]]) + >>> B2 = Matrix([[1]]) + >>> C2 = Matrix([[1]]) + >>> D2 = Matrix([[0]]) + >>> ss1 = StateSpace(A1, B1, C1, D1) + >>> ss2 = StateSpace(A2, B2, C2, D2) + >>> S5 = Series(ss1, ss2) + >>> S5 + Series(StateSpace(Matrix([[-1]]), Matrix([[1]]), Matrix([[-1]]), Matrix([[1]])), StateSpace(Matrix([[0]]), Matrix([[1]]), Matrix([[1]]), Matrix([[0]]))) + >>> S5.doit() + StateSpace(Matrix([ + [-1, 0], + [-1, 0]]), Matrix([ + [1], + [1]]), Matrix([[0, 1]]), Matrix([[0]])) + + Notes + ===== + + All the transfer functions should use the same complex variable + ``var`` of the Laplace transform. + + See Also + ======== + + MIMOSeries, Parallel, TransferFunction, Feedback + + """ + def __new__(cls, *args, evaluate=False): + + args = _flatten_args(args, Series) + # For StateSpace series connection + if args and any(isinstance(arg, StateSpace) or (hasattr(arg, 'is_StateSpace_object') + and arg.is_StateSpace_object)for arg in args): + # Check for SISO + if (args[0].num_inputs == 1) and (args[-1].num_outputs == 1): + # Check the interconnection + for i in range(1, len(args)): + if args[i].num_inputs != args[i-1].num_outputs: + raise ValueError(filldedent("""Systems with incompatible inputs and outputs + cannot be connected in Series.""")) + cls._is_series_StateSpace = True + else: + raise ValueError("To use Series connection for MIMO systems use MIMOSeries instead.") + else: + cls._is_series_StateSpace = False + cls._check_args(args) + + obj = super().__new__(cls, *args) + + return obj.doit() if evaluate else obj + + def __repr__(self): + systems_repr = ', '.join(repr(system) for system in self.args) + return f"Series({systems_repr})" + + __str__ = __repr__ + + @property + def var(self): + """ + Returns the complex variable used by all the transfer functions. + + Examples + ======== + + >>> from sympy.abc import p + >>> from sympy.physics.control.lti import TransferFunction, Series, Parallel + >>> G1 = TransferFunction(p**2 + 2*p + 4, p - 6, p) + >>> G2 = TransferFunction(p, 4 - p, p) + >>> G3 = TransferFunction(0, p**4 - 1, p) + >>> Series(G1, G2).var + p + >>> Series(-G3, Parallel(G1, G2)).var + p + + """ + return self.args[0].var + + def doit(self, **hints): + """ + Returns the resultant transfer function or StateSpace obtained after evaluating + the series interconnection. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction, Series + >>> tf1 = TransferFunction(a*p**2 + b*s, s - p, s) + >>> tf2 = TransferFunction(s**3 - 2, s**4 + 5*s + 6, s) + >>> Series(tf2, tf1).doit() + TransferFunction((s**3 - 2)*(a*p**2 + b*s), (-p + s)*(s**4 + 5*s + 6), s) + >>> Series(-tf1, -tf2).doit() + TransferFunction((2 - s**3)*(-a*p**2 - b*s), (-p + s)*(s**4 + 5*s + 6), s) + + Notes + ===== + + If a series connection contains only TransferFunction components, the equivalent system returned + will be a TransferFunction. However, if a StateSpace object is used in any of the arguments, + the output will be a StateSpace object. + + """ + # Check if the system is a StateSpace + if self._is_series_StateSpace: + # Return the equivalent StateSpace model + res = self.args[0] + if not isinstance(res, StateSpace): + res = res.doit().rewrite(StateSpace) + for arg in self.args[1:]: + if not isinstance(arg, StateSpace): + arg = arg.doit().rewrite(StateSpace) + else: + arg = arg.doit() + arg = arg.doit() + res = arg * res + return res + + _num_arg = (arg.doit().num for arg in self.args) + _den_arg = (arg.doit().den for arg in self.args) + res_num = Mul(*_num_arg, evaluate=True) + res_den = Mul(*_den_arg, evaluate=True) + return TransferFunction(res_num, res_den, self.var) + + def _eval_rewrite_as_TransferFunction(self, *args, **kwargs): + if self._is_series_StateSpace: + return self.doit().rewrite(TransferFunction)[0][0] + return self.doit() + + @_check_other_SISO + def __add__(self, other): + + if isinstance(other, Parallel): + arg_list = list(other.args) + return Parallel(self, *arg_list) + + return Parallel(self, other) + + __radd__ = __add__ + + @_check_other_SISO + def __sub__(self, other): + return self + (-other) + + def __rsub__(self, other): + return -self + other + + @_check_other_SISO + def __mul__(self, other): + + arg_list = list(self.args) + return Series(*arg_list, other) + + def __truediv__(self, other): + if isinstance(other, TransferFunction): + return Series(*self.args, TransferFunction(other.den, other.num, other.var)) + elif isinstance(other, Series): + tf_self = self.rewrite(TransferFunction) + tf_other = other.rewrite(TransferFunction) + return tf_self / tf_other + elif (isinstance(other, Parallel) and len(other.args) == 2 + and isinstance(other.args[0], TransferFunction) and isinstance(other.args[1], Series)): + + if not self.var == other.var: + raise ValueError(filldedent(""" + All the transfer functions should use the same complex variable + of the Laplace transform.""")) + self_arg_list = set(self.args) + other_arg_list = set(other.args[1].args) + res = list(self_arg_list ^ other_arg_list) + if len(res) == 0: + return Feedback(self, other.args[0]) + elif len(res) == 1: + return Feedback(self, *res) + else: + return Feedback(self, Series(*res)) + else: + raise ValueError("This transfer function expression is invalid.") + + def __neg__(self): + return Series(TransferFunction(-1, 1, self.var), self) + + def to_expr(self): + """Returns the equivalent ``Expr`` object.""" + return Mul(*(arg.to_expr() for arg in self.args), evaluate=False) + + @property + def is_proper(self): + """ + Returns True if degree of the numerator polynomial of the resultant transfer + function is less than or equal to degree of the denominator polynomial of + the same, else False. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction, Series + >>> tf1 = TransferFunction(b*s**2 + p**2 - a*p + s, b - p**2, s) + >>> tf2 = TransferFunction(p**2 - 4*p, p**3 + 3*s + 2, s) + >>> tf3 = TransferFunction(s, s**2 + s + 1, s) + >>> S1 = Series(-tf2, tf1) + >>> S1.is_proper + False + >>> S2 = Series(tf1, tf2, tf3) + >>> S2.is_proper + True + + """ + return self.doit().is_proper + + @property + def is_strictly_proper(self): + """ + Returns True if degree of the numerator polynomial of the resultant transfer + function is strictly less than degree of the denominator polynomial of + the same, else False. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction, Series + >>> tf1 = TransferFunction(a*p**2 + b*s, s - p, s) + >>> tf2 = TransferFunction(s**3 - 2, s**2 + 5*s + 6, s) + >>> tf3 = TransferFunction(1, s**2 + s + 1, s) + >>> S1 = Series(tf1, tf2) + >>> S1.is_strictly_proper + False + >>> S2 = Series(tf1, tf2, tf3) + >>> S2.is_strictly_proper + True + + """ + return self.doit().is_strictly_proper + + @property + def is_biproper(self): + r""" + Returns True if degree of the numerator polynomial of the resultant transfer + function is equal to degree of the denominator polynomial of + the same, else False. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction, Series + >>> tf1 = TransferFunction(a*p**2 + b*s, s - p, s) + >>> tf2 = TransferFunction(p, s**2, s) + >>> tf3 = TransferFunction(s**2, 1, s) + >>> S1 = Series(tf1, -tf2) + >>> S1.is_biproper + False + >>> S2 = Series(tf2, tf3) + >>> S2.is_biproper + True + + """ + return self.doit().is_biproper + + @property + def is_StateSpace_object(self): + return self._is_series_StateSpace + +def _mat_mul_compatible(*args): + """To check whether shapes are compatible for matrix mul.""" + return all(args[i].num_outputs == args[i+1].num_inputs for i in range(len(args)-1)) + + +class MIMOSeries(MIMOLinearTimeInvariant): + r""" + A class for representing a series configuration of MIMO systems. + + Parameters + ========== + + args : MIMOLinearTimeInvariant + MIMO systems in a series configuration. + evaluate : Boolean, Keyword + When passed ``True``, returns the equivalent + ``MIMOSeries(*args).doit()``. Set to ``False`` by default. + + Raises + ====== + + ValueError + When no argument is passed. + + ``var`` attribute is not same for every system. + + ``num_outputs`` of the MIMO system is not equal to the + ``num_inputs`` of its adjacent MIMO system. (Matrix + multiplication constraint, basically) + TypeError + Any of the passed ``*args`` has unsupported type + + A combination of SISO and MIMO systems is + passed. There should be homogeneity in the + type of systems passed, MIMO in this case. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import MIMOSeries, TransferFunctionMatrix, StateSpace + >>> from sympy import Matrix, pprint + >>> mat_a = Matrix([[5*s], [5]]) # 2 Outputs 1 Input + >>> mat_b = Matrix([[5, 1/(6*s**2)]]) # 1 Output 2 Inputs + >>> mat_c = Matrix([[1, s], [5/s, 1]]) # 2 Outputs 2 Inputs + >>> tfm_a = TransferFunctionMatrix.from_Matrix(mat_a, s) + >>> tfm_b = TransferFunctionMatrix.from_Matrix(mat_b, s) + >>> tfm_c = TransferFunctionMatrix.from_Matrix(mat_c, s) + >>> MIMOSeries(tfm_c, tfm_b, tfm_a) + MIMOSeries(TransferFunctionMatrix(((TransferFunction(1, 1, s), TransferFunction(s, 1, s)), (TransferFunction(5, s, s), TransferFunction(1, 1, s)))), TransferFunctionMatrix(((TransferFunction(5, 1, s), TransferFunction(1, 6*s**2, s)),)), TransferFunctionMatrix(((TransferFunction(5*s, 1, s),), (TransferFunction(5, 1, s),)))) + >>> pprint(_, use_unicode=False) # For Better Visualization + [5*s] [1 s] + [---] [5 1 ] [- -] + [ 1 ] [- ----] [1 1] + [ ] *[1 2] *[ ] + [ 5 ] [ 6*s ]{t} [5 1] + [ - ] [- -] + [ 1 ]{t} [s 1]{t} + >>> MIMOSeries(tfm_c, tfm_b, tfm_a).doit() + TransferFunctionMatrix(((TransferFunction(150*s**4 + 25*s, 6*s**3, s), TransferFunction(150*s**4 + 5*s, 6*s**2, s)), (TransferFunction(150*s**3 + 25, 6*s**3, s), TransferFunction(150*s**3 + 5, 6*s**2, s)))) + >>> pprint(_, use_unicode=False) # (2 Inputs -A-> 2 Outputs) -> (2 Inputs -B-> 1 Output) -> (1 Input -C-> 2 Outputs) is equivalent to (2 Inputs -Series Equivalent-> 2 Outputs). + [ 4 4 ] + [150*s + 25*s 150*s + 5*s] + [------------- ------------] + [ 3 2 ] + [ 6*s 6*s ] + [ ] + [ 3 3 ] + [ 150*s + 25 150*s + 5 ] + [ ----------- ---------- ] + [ 3 2 ] + [ 6*s 6*s ]{t} + >>> a1 = Matrix([[4, 1], [2, -3]]) + >>> b1 = Matrix([[5, 2], [-3, -3]]) + >>> c1 = Matrix([[2, -4], [0, 1]]) + >>> d1 = Matrix([[3, 2], [1, -1]]) + >>> a2 = Matrix([[-3, 4, 2], [-1, -3, 0], [2, 5, 3]]) + >>> b2 = Matrix([[1, 4], [-3, -3], [-2, 1]]) + >>> c2 = Matrix([[4, 2, -3], [1, 4, 3]]) + >>> d2 = Matrix([[-2, 4], [0, 1]]) + >>> ss1 = StateSpace(a1, b1, c1, d1) #2 inputs, 2 outputs + >>> ss2 = StateSpace(a2, b2, c2, d2) #2 inputs, 2 outputs + >>> S1 = MIMOSeries(ss1, ss2) #(2 inputs, 2 outputs) -> (2 inputs, 2 outputs) + >>> S1 + MIMOSeries(StateSpace(Matrix([ + [4, 1], + [2, -3]]), Matrix([ + [ 5, 2], + [-3, -3]]), Matrix([ + [2, -4], + [0, 1]]), Matrix([ + [3, 2], + [1, -1]])), StateSpace(Matrix([ + [-3, 4, 2], + [-1, -3, 0], + [ 2, 5, 3]]), Matrix([ + [ 1, 4], + [-3, -3], + [-2, 1]]), Matrix([ + [4, 2, -3], + [1, 4, 3]]), Matrix([ + [-2, 4], + [ 0, 1]]))) + >>> S1.doit() + StateSpace(Matrix([ + [ 4, 1, 0, 0, 0], + [ 2, -3, 0, 0, 0], + [ 2, 0, -3, 4, 2], + [-6, 9, -1, -3, 0], + [-4, 9, 2, 5, 3]]), Matrix([ + [ 5, 2], + [ -3, -3], + [ 7, -2], + [-12, -3], + [ -5, -5]]), Matrix([ + [-4, 12, 4, 2, -3], + [ 0, 1, 1, 4, 3]]), Matrix([ + [-2, -8], + [ 1, -1]])) + + Notes + ===== + + All the transfer function matrices should use the same complex variable ``var`` of the Laplace transform. + + ``MIMOSeries(A, B)`` is not equivalent to ``A*B``. It is always in the reverse order, that is ``B*A``. + + See Also + ======== + + Series, MIMOParallel + + """ + def __new__(cls, *args, evaluate=False): + + if args and any(isinstance(arg, StateSpace) or (hasattr(arg, 'is_StateSpace_object') + and arg.is_StateSpace_object) for arg in args): + # Check compatibility + for i in range(1, len(args)): + if args[i].num_inputs != args[i - 1].num_outputs: + raise ValueError(filldedent("""Systems with incompatible inputs and outputs + cannot be connected in MIMOSeries.""")) + obj = super().__new__(cls, *args) + cls._is_series_StateSpace = True + else: + cls._check_args(args) + cls._is_series_StateSpace = False + + if _mat_mul_compatible(*args): + obj = super().__new__(cls, *args) + + else: + raise ValueError(filldedent(""" + Number of input signals do not match the number + of output signals of adjacent systems for some args.""")) + + return obj.doit() if evaluate else obj + + @property + def var(self): + """ + Returns the complex variable used by all the transfer functions. + + Examples + ======== + + >>> from sympy.abc import p + >>> from sympy.physics.control.lti import TransferFunction, MIMOSeries, TransferFunctionMatrix + >>> G1 = TransferFunction(p**2 + 2*p + 4, p - 6, p) + >>> G2 = TransferFunction(p, 4 - p, p) + >>> G3 = TransferFunction(0, p**4 - 1, p) + >>> tfm_1 = TransferFunctionMatrix([[G1, G2, G3]]) + >>> tfm_2 = TransferFunctionMatrix([[G1], [G2], [G3]]) + >>> MIMOSeries(tfm_2, tfm_1).var + p + + """ + return self.args[0].var + + @property + def num_inputs(self): + """Returns the number of input signals of the series system.""" + return self.args[0].num_inputs + + @property + def num_outputs(self): + """Returns the number of output signals of the series system.""" + return self.args[-1].num_outputs + + @property + def shape(self): + """Returns the shape of the equivalent MIMO system.""" + return self.num_outputs, self.num_inputs + + @property + def is_StateSpace_object(self): + return self._is_series_StateSpace + + def doit(self, cancel=False, **kwargs): + """ + Returns the resultant obtained after evaluating the MIMO systems arranged + in a series configuration. For TransferFunction systems it returns a TransferFunctionMatrix + and for StateSpace systems it returns the resultant StateSpace system. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction, MIMOSeries, TransferFunctionMatrix + >>> tf1 = TransferFunction(a*p**2 + b*s, s - p, s) + >>> tf2 = TransferFunction(s**3 - 2, s**4 + 5*s + 6, s) + >>> tfm1 = TransferFunctionMatrix([[tf1, tf2], [tf2, tf2]]) + >>> tfm2 = TransferFunctionMatrix([[tf2, tf1], [tf1, tf1]]) + >>> MIMOSeries(tfm2, tfm1).doit() + TransferFunctionMatrix(((TransferFunction(2*(-p + s)*(s**3 - 2)*(a*p**2 + b*s)*(s**4 + 5*s + 6), (-p + s)**2*(s**4 + 5*s + 6)**2, s), TransferFunction((-p + s)**2*(s**3 - 2)*(a*p**2 + b*s) + (-p + s)*(a*p**2 + b*s)**2*(s**4 + 5*s + 6), (-p + s)**3*(s**4 + 5*s + 6), s)), (TransferFunction((-p + s)*(s**3 - 2)**2*(s**4 + 5*s + 6) + (s**3 - 2)*(a*p**2 + b*s)*(s**4 + 5*s + 6)**2, (-p + s)*(s**4 + 5*s + 6)**3, s), TransferFunction(2*(s**3 - 2)*(a*p**2 + b*s), (-p + s)*(s**4 + 5*s + 6), s)))) + + """ + if self._is_series_StateSpace: + # Return the equivalent StateSpace model + res = self.args[0] + if not isinstance(res, StateSpace): + res = res.doit().rewrite(StateSpace) + for arg in self.args[1:]: + if not isinstance(arg, StateSpace): + arg = arg.doit().rewrite(StateSpace) + else: + arg = arg.doit() + res = arg * res + return res + + _arg = (arg.doit()._expr_mat for arg in reversed(self.args)) + + if cancel: + res = MatMul(*_arg, evaluate=True) + return TransferFunctionMatrix.from_Matrix(res, self.var) + + _dummy_args, _dummy_dict = _dummify_args(_arg, self.var) + res = MatMul(*_dummy_args, evaluate=True) + temp_tfm = TransferFunctionMatrix.from_Matrix(res, self.var) + return temp_tfm.subs(_dummy_dict) + + def _eval_rewrite_as_TransferFunctionMatrix(self, *args, **kwargs): + if self._is_series_StateSpace: + return self.doit().rewrite(TransferFunction) + return self.doit() + + @_check_other_MIMO + def __add__(self, other): + + if isinstance(other, MIMOParallel): + arg_list = list(other.args) + return MIMOParallel(self, *arg_list) + + return MIMOParallel(self, other) + + __radd__ = __add__ + + @_check_other_MIMO + def __sub__(self, other): + return self + (-other) + + def __rsub__(self, other): + return -self + other + + @_check_other_MIMO + def __mul__(self, other): + + if isinstance(other, MIMOSeries): + self_arg_list = list(self.args) + other_arg_list = list(other.args) + return MIMOSeries(*other_arg_list, *self_arg_list) # A*B = MIMOSeries(B, A) + + arg_list = list(self.args) + return MIMOSeries(other, *arg_list) + + def __neg__(self): + arg_list = list(self.args) + arg_list[0] = -arg_list[0] + return MIMOSeries(*arg_list) + + +class Parallel(SISOLinearTimeInvariant): + r""" + A class for representing a parallel configuration of SISO systems. + + Parameters + ========== + + args : SISOLinearTimeInvariant + SISO systems in a parallel arrangement. + evaluate : Boolean, Keyword + When passed ``True``, returns the equivalent + ``Parallel(*args).doit()``. Set to ``False`` by default. + + Raises + ====== + + ValueError + When no argument is passed. + + ``var`` attribute is not same for every system. + TypeError + Any of the passed ``*args`` has unsupported type + + A combination of SISO and MIMO systems is + passed. There should be homogeneity in the + type of systems passed. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction, Parallel, Series, StateSpace + >>> tf1 = TransferFunction(a*p**2 + b*s, s - p, s) + >>> tf2 = TransferFunction(s**3 - 2, s**4 + 5*s + 6, s) + >>> tf3 = TransferFunction(p**2, p + s, s) + >>> P1 = Parallel(tf1, tf2) + >>> P1 + Parallel(TransferFunction(a*p**2 + b*s, -p + s, s), TransferFunction(s**3 - 2, s**4 + 5*s + 6, s)) + >>> P1.var + s + >>> P2 = Parallel(tf2, Series(tf3, -tf1)) + >>> P2 + Parallel(TransferFunction(s**3 - 2, s**4 + 5*s + 6, s), Series(TransferFunction(p**2, p + s, s), TransferFunction(-a*p**2 - b*s, -p + s, s))) + >>> P2.var + s + >>> P3 = Parallel(Series(tf1, tf2), Series(tf2, tf3)) + >>> P3 + Parallel(Series(TransferFunction(a*p**2 + b*s, -p + s, s), TransferFunction(s**3 - 2, s**4 + 5*s + 6, s)), Series(TransferFunction(s**3 - 2, s**4 + 5*s + 6, s), TransferFunction(p**2, p + s, s))) + >>> P3.var + s + + You can get the resultant transfer function by using ``.doit()`` method: + + >>> Parallel(tf1, tf2, -tf3).doit() + TransferFunction(-p**2*(-p + s)*(s**4 + 5*s + 6) + (-p + s)*(p + s)*(s**3 - 2) + (p + s)*(a*p**2 + b*s)*(s**4 + 5*s + 6), (-p + s)*(p + s)*(s**4 + 5*s + 6), s) + >>> Parallel(tf2, Series(tf1, -tf3)).doit() + TransferFunction(-p**2*(a*p**2 + b*s)*(s**4 + 5*s + 6) + (-p + s)*(p + s)*(s**3 - 2), (-p + s)*(p + s)*(s**4 + 5*s + 6), s) + + Parallel can be used to connect SISO ``StateSpace`` systems together. + + >>> A1 = Matrix([[-1]]) + >>> B1 = Matrix([[1]]) + >>> C1 = Matrix([[-1]]) + >>> D1 = Matrix([1]) + >>> A2 = Matrix([[0]]) + >>> B2 = Matrix([[1]]) + >>> C2 = Matrix([[1]]) + >>> D2 = Matrix([[0]]) + >>> ss1 = StateSpace(A1, B1, C1, D1) + >>> ss2 = StateSpace(A2, B2, C2, D2) + >>> P4 = Parallel(ss1, ss2) + >>> P4 + Parallel(StateSpace(Matrix([[-1]]), Matrix([[1]]), Matrix([[-1]]), Matrix([[1]])), StateSpace(Matrix([[0]]), Matrix([[1]]), Matrix([[1]]), Matrix([[0]]))) + + ``doit()`` can be used to find ``StateSpace`` equivalent for the system containing ``StateSpace`` objects. + + >>> P4.doit() + StateSpace(Matrix([ + [-1, 0], + [ 0, 0]]), Matrix([ + [1], + [1]]), Matrix([[-1, 1]]), Matrix([[1]])) + >>> P4.rewrite(TransferFunction) + TransferFunction(s*(s + 1) + 1, s*(s + 1), s) + + Notes + ===== + + All the transfer functions should use the same complex variable + ``var`` of the Laplace transform. + + See Also + ======== + + Series, TransferFunction, Feedback + + """ + + def __new__(cls, *args, evaluate=False): + + args = _flatten_args(args, Parallel) + # For StateSpace parallel connection + if args and any(isinstance(arg, StateSpace) or (hasattr(arg, 'is_StateSpace_object') + and arg.is_StateSpace_object) for arg in args): + # Check for SISO + if all(arg.is_SISO for arg in args): + cls._is_parallel_StateSpace = True + else: + raise ValueError("To use Parallel connection for MIMO systems use MIMOParallel instead.") + else: + cls._is_parallel_StateSpace = False + cls._check_args(args) + obj = super().__new__(cls, *args) + + return obj.doit() if evaluate else obj + + def __repr__(self): + systems_repr = ', '.join(repr(system) for system in self.args) + return f"Parallel({systems_repr})" + + __str__ = __repr__ + + @property + def var(self): + """ + Returns the complex variable used by all the transfer functions. + + Examples + ======== + + >>> from sympy.abc import p + >>> from sympy.physics.control.lti import TransferFunction, Parallel, Series + >>> G1 = TransferFunction(p**2 + 2*p + 4, p - 6, p) + >>> G2 = TransferFunction(p, 4 - p, p) + >>> G3 = TransferFunction(0, p**4 - 1, p) + >>> Parallel(G1, G2).var + p + >>> Parallel(-G3, Series(G1, G2)).var + p + + """ + return self.args[0].var + + def doit(self, **hints): + """ + Returns the resultant transfer function or state space obtained by + parallel connection of transfer functions or state space objects. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction, Parallel + >>> tf1 = TransferFunction(a*p**2 + b*s, s - p, s) + >>> tf2 = TransferFunction(s**3 - 2, s**4 + 5*s + 6, s) + >>> Parallel(tf2, tf1).doit() + TransferFunction((-p + s)*(s**3 - 2) + (a*p**2 + b*s)*(s**4 + 5*s + 6), (-p + s)*(s**4 + 5*s + 6), s) + >>> Parallel(-tf1, -tf2).doit() + TransferFunction((2 - s**3)*(-p + s) + (-a*p**2 - b*s)*(s**4 + 5*s + 6), (-p + s)*(s**4 + 5*s + 6), s) + + """ + if self._is_parallel_StateSpace: + # Return the equivalent StateSpace model + res = self.args[0].doit() + if not isinstance(res, StateSpace): + res = res.rewrite(StateSpace) + for arg in self.args[1:]: + if not isinstance(arg, StateSpace): + arg = arg.doit().rewrite(StateSpace) + res += arg + return res + + _arg = (arg.doit().to_expr() for arg in self.args) + res = Add(*_arg).as_numer_denom() + return TransferFunction(*res, self.var) + + def _eval_rewrite_as_TransferFunction(self, *args, **kwargs): + if self._is_parallel_StateSpace: + return self.doit().rewrite(TransferFunction)[0][0] + return self.doit() + + @_check_other_SISO + def __add__(self, other): + + self_arg_list = list(self.args) + return Parallel(*self_arg_list, other) + + __radd__ = __add__ + + @_check_other_SISO + def __sub__(self, other): + return self + (-other) + + def __rsub__(self, other): + return -self + other + + @_check_other_SISO + def __mul__(self, other): + + if isinstance(other, Series): + arg_list = list(other.args) + return Series(self, *arg_list) + + return Series(self, other) + + def __neg__(self): + return Series(TransferFunction(-1, 1, self.var), self) + + def to_expr(self): + """Returns the equivalent ``Expr`` object.""" + return Add(*(arg.to_expr() for arg in self.args), evaluate=False) + + @property + def is_proper(self): + """ + Returns True if degree of the numerator polynomial of the resultant transfer + function is less than or equal to degree of the denominator polynomial of + the same, else False. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction, Parallel + >>> tf1 = TransferFunction(b*s**2 + p**2 - a*p + s, b - p**2, s) + >>> tf2 = TransferFunction(p**2 - 4*p, p**3 + 3*s + 2, s) + >>> tf3 = TransferFunction(s, s**2 + s + 1, s) + >>> P1 = Parallel(-tf2, tf1) + >>> P1.is_proper + False + >>> P2 = Parallel(tf2, tf3) + >>> P2.is_proper + True + + """ + return self.doit().is_proper + + @property + def is_strictly_proper(self): + """ + Returns True if degree of the numerator polynomial of the resultant transfer + function is strictly less than degree of the denominator polynomial of + the same, else False. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction, Parallel + >>> tf1 = TransferFunction(a*p**2 + b*s, s - p, s) + >>> tf2 = TransferFunction(s**3 - 2, s**4 + 5*s + 6, s) + >>> tf3 = TransferFunction(s, s**2 + s + 1, s) + >>> P1 = Parallel(tf1, tf2) + >>> P1.is_strictly_proper + False + >>> P2 = Parallel(tf2, tf3) + >>> P2.is_strictly_proper + True + + """ + return self.doit().is_strictly_proper + + @property + def is_biproper(self): + """ + Returns True if degree of the numerator polynomial of the resultant transfer + function is equal to degree of the denominator polynomial of + the same, else False. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction, Parallel + >>> tf1 = TransferFunction(a*p**2 + b*s, s - p, s) + >>> tf2 = TransferFunction(p**2, p + s, s) + >>> tf3 = TransferFunction(s, s**2 + s + 1, s) + >>> P1 = Parallel(tf1, -tf2) + >>> P1.is_biproper + True + >>> P2 = Parallel(tf2, tf3) + >>> P2.is_biproper + False + + """ + return self.doit().is_biproper + + @property + def is_StateSpace_object(self): + return self._is_parallel_StateSpace + + +class MIMOParallel(MIMOLinearTimeInvariant): + r""" + A class for representing a parallel configuration of MIMO systems. + + Parameters + ========== + + args : MIMOLinearTimeInvariant + MIMO Systems in a parallel arrangement. + evaluate : Boolean, Keyword + When passed ``True``, returns the equivalent + ``MIMOParallel(*args).doit()``. Set to ``False`` by default. + + Raises + ====== + + ValueError + When no argument is passed. + + ``var`` attribute is not same for every system. + + All MIMO systems passed do not have same shape. + TypeError + Any of the passed ``*args`` has unsupported type + + A combination of SISO and MIMO systems is + passed. There should be homogeneity in the + type of systems passed, MIMO in this case. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunctionMatrix, MIMOParallel, StateSpace + >>> from sympy import Matrix, pprint + >>> expr_1 = 1/s + >>> expr_2 = s/(s**2-1) + >>> expr_3 = (2 + s)/(s**2 - 1) + >>> expr_4 = 5 + >>> tfm_a = TransferFunctionMatrix.from_Matrix(Matrix([[expr_1, expr_2], [expr_3, expr_4]]), s) + >>> tfm_b = TransferFunctionMatrix.from_Matrix(Matrix([[expr_2, expr_1], [expr_4, expr_3]]), s) + >>> tfm_c = TransferFunctionMatrix.from_Matrix(Matrix([[expr_3, expr_4], [expr_1, expr_2]]), s) + >>> MIMOParallel(tfm_a, tfm_b, tfm_c) + MIMOParallel(TransferFunctionMatrix(((TransferFunction(1, s, s), TransferFunction(s, s**2 - 1, s)), (TransferFunction(s + 2, s**2 - 1, s), TransferFunction(5, 1, s)))), TransferFunctionMatrix(((TransferFunction(s, s**2 - 1, s), TransferFunction(1, s, s)), (TransferFunction(5, 1, s), TransferFunction(s + 2, s**2 - 1, s)))), TransferFunctionMatrix(((TransferFunction(s + 2, s**2 - 1, s), TransferFunction(5, 1, s)), (TransferFunction(1, s, s), TransferFunction(s, s**2 - 1, s))))) + >>> pprint(_, use_unicode=False) # For Better Visualization + [ 1 s ] [ s 1 ] [s + 2 5 ] + [ - ------] [------ - ] [------ - ] + [ s 2 ] [ 2 s ] [ 2 1 ] + [ s - 1] [s - 1 ] [s - 1 ] + [ ] + [ ] + [ ] + [s + 2 5 ] [ 5 s + 2 ] [ 1 s ] + [------ - ] [ - ------] [ - ------] + [ 2 1 ] [ 1 2 ] [ s 2 ] + [s - 1 ]{t} [ s - 1]{t} [ s - 1]{t} + >>> MIMOParallel(tfm_a, tfm_b, tfm_c).doit() + TransferFunctionMatrix(((TransferFunction(s**2 + s*(2*s + 2) - 1, s*(s**2 - 1), s), TransferFunction(2*s**2 + 5*s*(s**2 - 1) - 1, s*(s**2 - 1), s)), (TransferFunction(s**2 + s*(s + 2) + 5*s*(s**2 - 1) - 1, s*(s**2 - 1), s), TransferFunction(5*s**2 + 2*s - 3, s**2 - 1, s)))) + >>> pprint(_, use_unicode=False) + [ 2 2 / 2 \ ] + [ s + s*(2*s + 2) - 1 2*s + 5*s*\s - 1/ - 1] + [ -------------------- -----------------------] + [ / 2 \ / 2 \ ] + [ s*\s - 1/ s*\s - 1/ ] + [ ] + [ 2 / 2 \ 2 ] + [s + s*(s + 2) + 5*s*\s - 1/ - 1 5*s + 2*s - 3 ] + [--------------------------------- -------------- ] + [ / 2 \ 2 ] + [ s*\s - 1/ s - 1 ]{t} + + ``MIMOParallel`` can also be used to connect MIMO ``StateSpace`` systems. + + >>> A1 = Matrix([[4, 1], [2, -3]]) + >>> B1 = Matrix([[5, 2], [-3, -3]]) + >>> C1 = Matrix([[2, -4], [0, 1]]) + >>> D1 = Matrix([[3, 2], [1, -1]]) + >>> A2 = Matrix([[-3, 4, 2], [-1, -3, 0], [2, 5, 3]]) + >>> B2 = Matrix([[1, 4], [-3, -3], [-2, 1]]) + >>> C2 = Matrix([[4, 2, -3], [1, 4, 3]]) + >>> D2 = Matrix([[-2, 4], [0, 1]]) + >>> ss1 = StateSpace(A1, B1, C1, D1) + >>> ss2 = StateSpace(A2, B2, C2, D2) + >>> p1 = MIMOParallel(ss1, ss2) + >>> p1 + MIMOParallel(StateSpace(Matrix([ + [4, 1], + [2, -3]]), Matrix([ + [ 5, 2], + [-3, -3]]), Matrix([ + [2, -4], + [0, 1]]), Matrix([ + [3, 2], + [1, -1]])), StateSpace(Matrix([ + [-3, 4, 2], + [-1, -3, 0], + [ 2, 5, 3]]), Matrix([ + [ 1, 4], + [-3, -3], + [-2, 1]]), Matrix([ + [4, 2, -3], + [1, 4, 3]]), Matrix([ + [-2, 4], + [ 0, 1]]))) + + ``doit()`` can be used to find ``StateSpace`` equivalent for the system containing ``StateSpace`` objects. + + >>> p1.doit() + StateSpace(Matrix([ + [4, 1, 0, 0, 0], + [2, -3, 0, 0, 0], + [0, 0, -3, 4, 2], + [0, 0, -1, -3, 0], + [0, 0, 2, 5, 3]]), Matrix([ + [ 5, 2], + [-3, -3], + [ 1, 4], + [-3, -3], + [-2, 1]]), Matrix([ + [2, -4, 4, 2, -3], + [0, 1, 1, 4, 3]]), Matrix([ + [1, 6], + [1, 0]])) + + Notes + ===== + + All the transfer function matrices should use the same complex variable + ``var`` of the Laplace transform. + + See Also + ======== + + Parallel, MIMOSeries + + """ + + def __new__(cls, *args, evaluate=False): + + args = _flatten_args(args, MIMOParallel) + + # For StateSpace Parallel connection + if args and any(isinstance(arg, StateSpace) or (hasattr(arg, 'is_StateSpace_object') + and arg.is_StateSpace_object) for arg in args): + if any(arg.num_inputs != args[0].num_inputs or arg.num_outputs != args[0].num_outputs + for arg in args[1:]): + raise ShapeError("Systems with incompatible inputs and outputs cannot be " + "connected in MIMOParallel.") + cls._is_parallel_StateSpace = True + else: + cls._check_args(args) + if any(arg.shape != args[0].shape for arg in args): + raise TypeError("Shape of all the args is not equal.") + cls._is_parallel_StateSpace = False + obj = super().__new__(cls, *args) + + return obj.doit() if evaluate else obj + + @property + def var(self): + """ + Returns the complex variable used by all the systems. + + Examples + ======== + + >>> from sympy.abc import p + >>> from sympy.physics.control.lti import TransferFunction, TransferFunctionMatrix, MIMOParallel + >>> G1 = TransferFunction(p**2 + 2*p + 4, p - 6, p) + >>> G2 = TransferFunction(p, 4 - p, p) + >>> G3 = TransferFunction(0, p**4 - 1, p) + >>> G4 = TransferFunction(p**2, p**2 - 1, p) + >>> tfm_a = TransferFunctionMatrix([[G1, G2], [G3, G4]]) + >>> tfm_b = TransferFunctionMatrix([[G2, G1], [G4, G3]]) + >>> MIMOParallel(tfm_a, tfm_b).var + p + + """ + return self.args[0].var + + @property + def num_inputs(self): + """Returns the number of input signals of the parallel system.""" + return self.args[0].num_inputs + + @property + def num_outputs(self): + """Returns the number of output signals of the parallel system.""" + return self.args[0].num_outputs + + @property + def shape(self): + """Returns the shape of the equivalent MIMO system.""" + return self.num_outputs, self.num_inputs + + @property + def is_StateSpace_object(self): + return self._is_parallel_StateSpace + + def doit(self, **hints): + """ + Returns the resultant transfer function matrix or StateSpace obtained after evaluating + the MIMO systems arranged in a parallel configuration. + + Examples + ======== + + >>> from sympy.abc import s, p, a, b + >>> from sympy.physics.control.lti import TransferFunction, MIMOParallel, TransferFunctionMatrix + >>> tf1 = TransferFunction(a*p**2 + b*s, s - p, s) + >>> tf2 = TransferFunction(s**3 - 2, s**4 + 5*s + 6, s) + >>> tfm_1 = TransferFunctionMatrix([[tf1, tf2], [tf2, tf1]]) + >>> tfm_2 = TransferFunctionMatrix([[tf2, tf1], [tf1, tf2]]) + >>> MIMOParallel(tfm_1, tfm_2).doit() + TransferFunctionMatrix(((TransferFunction((-p + s)*(s**3 - 2) + (a*p**2 + b*s)*(s**4 + 5*s + 6), (-p + s)*(s**4 + 5*s + 6), s), TransferFunction((-p + s)*(s**3 - 2) + (a*p**2 + b*s)*(s**4 + 5*s + 6), (-p + s)*(s**4 + 5*s + 6), s)), (TransferFunction((-p + s)*(s**3 - 2) + (a*p**2 + b*s)*(s**4 + 5*s + 6), (-p + s)*(s**4 + 5*s + 6), s), TransferFunction((-p + s)*(s**3 - 2) + (a*p**2 + b*s)*(s**4 + 5*s + 6), (-p + s)*(s**4 + 5*s + 6), s)))) + + """ + if self._is_parallel_StateSpace: + # Return the equivalent StateSpace model. + res = self.args[0] + if not isinstance(res, StateSpace): + res = res.doit().rewrite(StateSpace) + for arg in self.args[1:]: + if not isinstance(arg, StateSpace): + arg = arg.doit().rewrite(StateSpace) + else: + arg = arg.doit() + res += arg + return res + _arg = (arg.doit()._expr_mat for arg in self.args) + res = MatAdd(*_arg, evaluate=True) + return TransferFunctionMatrix.from_Matrix(res, self.var) + + def _eval_rewrite_as_TransferFunctionMatrix(self, *args, **kwargs): + if self._is_parallel_StateSpace: + return self.doit().rewrite(TransferFunction) + return self.doit() + + @_check_other_MIMO + def __add__(self, other): + + self_arg_list = list(self.args) + return MIMOParallel(*self_arg_list, other) + + __radd__ = __add__ + + @_check_other_MIMO + def __sub__(self, other): + return self + (-other) + + def __rsub__(self, other): + return -self + other + + @_check_other_MIMO + def __mul__(self, other): + + if isinstance(other, MIMOSeries): + arg_list = list(other.args) + return MIMOSeries(*arg_list, self) + + return MIMOSeries(other, self) + + def __neg__(self): + arg_list = [-arg for arg in list(self.args)] + return MIMOParallel(*arg_list) + + +class Feedback(SISOLinearTimeInvariant): + r""" + A class for representing closed-loop feedback interconnection between two + SISO input/output systems. + + The first argument, ``sys1``, is the feedforward part of the closed-loop + system or in simple words, the dynamical model representing the process + to be controlled. The second argument, ``sys2``, is the feedback system + and controls the fed back signal to ``sys1``. Both ``sys1`` and ``sys2`` + can either be ``Series``, ``StateSpace`` or ``TransferFunction`` objects. + + Parameters + ========== + + sys1 : Series, StateSpace, TransferFunction + The feedforward path system. + sys2 : Series, StateSpace, TransferFunction, optional + The feedback path system (often a feedback controller). + It is the model sitting on the feedback path. + + If not specified explicitly, the sys2 is + assumed to be unit (1.0) transfer function. + sign : int, optional + The sign of feedback. Can either be ``1`` + (for positive feedback) or ``-1`` (for negative feedback). + Default value is `-1`. + + Raises + ====== + + ValueError + When ``sys1`` and ``sys2`` are not using the + same complex variable of the Laplace transform. + + When a combination of ``sys1`` and ``sys2`` yields + zero denominator. + + TypeError + When either ``sys1`` or ``sys2`` is not a ``Series``, ``StateSpace`` or + ``TransferFunction`` object. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import StateSpace, TransferFunction, Feedback + >>> plant = TransferFunction(3*s**2 + 7*s - 3, s**2 - 4*s + 2, s) + >>> controller = TransferFunction(5*s - 10, s + 7, s) + >>> F1 = Feedback(plant, controller) + >>> F1 + Feedback(TransferFunction(3*s**2 + 7*s - 3, s**2 - 4*s + 2, s), TransferFunction(5*s - 10, s + 7, s), -1) + >>> F1.var + s + >>> F1.args + (TransferFunction(3*s**2 + 7*s - 3, s**2 - 4*s + 2, s), TransferFunction(5*s - 10, s + 7, s), -1) + + You can get the feedforward and feedback path systems by using ``.sys1`` and ``.sys2`` respectively. + + >>> F1.sys1 + TransferFunction(3*s**2 + 7*s - 3, s**2 - 4*s + 2, s) + >>> F1.sys2 + TransferFunction(5*s - 10, s + 7, s) + + You can get the resultant closed loop transfer function obtained by negative feedback + interconnection using ``.doit()`` method. + + >>> F1.doit() + TransferFunction((s + 7)*(s**2 - 4*s + 2)*(3*s**2 + 7*s - 3), ((s + 7)*(s**2 - 4*s + 2) + (5*s - 10)*(3*s**2 + 7*s - 3))*(s**2 - 4*s + 2), s) + >>> G = TransferFunction(2*s**2 + 5*s + 1, s**2 + 2*s + 3, s) + >>> C = TransferFunction(5*s + 10, s + 10, s) + >>> F2 = Feedback(G*C, TransferFunction(1, 1, s)) + >>> F2.doit() + TransferFunction((s + 10)*(5*s + 10)*(s**2 + 2*s + 3)*(2*s**2 + 5*s + 1), (s + 10)*((s + 10)*(s**2 + 2*s + 3) + (5*s + 10)*(2*s**2 + 5*s + 1))*(s**2 + 2*s + 3), s) + + To negate a ``Feedback`` object, the ``-`` operator can be prepended: + + >>> -F1 + Feedback(TransferFunction(-3*s**2 - 7*s + 3, s**2 - 4*s + 2, s), TransferFunction(10 - 5*s, s + 7, s), -1) + >>> -F2 + Feedback(Series(TransferFunction(-1, 1, s), TransferFunction(2*s**2 + 5*s + 1, s**2 + 2*s + 3, s), TransferFunction(5*s + 10, s + 10, s)), TransferFunction(-1, 1, s), -1) + + ``Feedback`` can also be used to connect SISO ``StateSpace`` systems together. + + >>> A1 = Matrix([[-1]]) + >>> B1 = Matrix([[1]]) + >>> C1 = Matrix([[-1]]) + >>> D1 = Matrix([1]) + >>> A2 = Matrix([[0]]) + >>> B2 = Matrix([[1]]) + >>> C2 = Matrix([[1]]) + >>> D2 = Matrix([[0]]) + >>> ss1 = StateSpace(A1, B1, C1, D1) + >>> ss2 = StateSpace(A2, B2, C2, D2) + >>> F3 = Feedback(ss1, ss2) + >>> F3 + Feedback(StateSpace(Matrix([[-1]]), Matrix([[1]]), Matrix([[-1]]), Matrix([[1]])), StateSpace(Matrix([[0]]), Matrix([[1]]), Matrix([[1]]), Matrix([[0]])), -1) + + ``doit()`` can be used to find ``StateSpace`` equivalent for the system containing ``StateSpace`` objects. + + >>> F3.doit() + StateSpace(Matrix([ + [-1, -1], + [-1, -1]]), Matrix([ + [1], + [1]]), Matrix([[-1, -1]]), Matrix([[1]])) + + We can also find the equivalent ``TransferFunction`` by using ``rewrite(TransferFunction)`` method. + + >>> F3.rewrite(TransferFunction) + TransferFunction(s, s + 2, s) + + See Also + ======== + + MIMOFeedback, Series, Parallel + + """ + def __new__(cls, sys1, sys2=None, sign=-1): + if not sys2: + sys2 = TransferFunction(1, 1, sys1.var) + + if not isinstance(sys1, (TransferFunction, Series, StateSpace, Feedback)): + raise TypeError("Unsupported type for `sys1` in Feedback.") + + if not isinstance(sys2, (TransferFunction, Series, StateSpace, Feedback)): + raise TypeError("Unsupported type for `sys2` in Feedback.") + + if not (sys1.num_inputs == sys1.num_outputs == sys2.num_inputs == + sys2.num_outputs == 1): + raise ValueError("""To use Feedback connection for MIMO systems + use MIMOFeedback instead.""") + + if sign not in [-1, 1]: + raise ValueError(filldedent(""" + Unsupported type for feedback. `sign` arg should + either be 1 (positive feedback loop) or -1 + (negative feedback loop).""")) + + if sys1.is_StateSpace_object or sys2.is_StateSpace_object: + cls.is_StateSpace_object = True + else: + if Mul(sys1.to_expr(), sys2.to_expr()).simplify() == sign: + raise ValueError("The equivalent system will have zero denominator.") + if sys1.var != sys2.var: + raise ValueError(filldedent("""Both `sys1` and `sys2` should be using the + same complex variable.""")) + cls.is_StateSpace_object = False + + return super(SISOLinearTimeInvariant, cls).__new__(cls, sys1, sys2, _sympify(sign)) + + def __repr__(self): + return f"Feedback({self.sys1}, {self.sys2}, {self.sign})" + + __str__ = __repr__ + + @property + def sys1(self): + """ + Returns the feedforward system of the feedback interconnection. + + Examples + ======== + + >>> from sympy.abc import s, p + >>> from sympy.physics.control.lti import TransferFunction, Feedback + >>> plant = TransferFunction(3*s**2 + 7*s - 3, s**2 - 4*s + 2, s) + >>> controller = TransferFunction(5*s - 10, s + 7, s) + >>> F1 = Feedback(plant, controller) + >>> F1.sys1 + TransferFunction(3*s**2 + 7*s - 3, s**2 - 4*s + 2, s) + >>> G = TransferFunction(2*s**2 + 5*s + 1, p**2 + 2*p + 3, p) + >>> C = TransferFunction(5*p + 10, p + 10, p) + >>> P = TransferFunction(1 - s, p + 2, p) + >>> F2 = Feedback(TransferFunction(1, 1, p), G*C*P) + >>> F2.sys1 + TransferFunction(1, 1, p) + + """ + return self.args[0] + + @property + def sys2(self): + """ + Returns the feedback controller of the feedback interconnection. + + Examples + ======== + + >>> from sympy.abc import s, p + >>> from sympy.physics.control.lti import TransferFunction, Feedback + >>> plant = TransferFunction(3*s**2 + 7*s - 3, s**2 - 4*s + 2, s) + >>> controller = TransferFunction(5*s - 10, s + 7, s) + >>> F1 = Feedback(plant, controller) + >>> F1.sys2 + TransferFunction(5*s - 10, s + 7, s) + >>> G = TransferFunction(2*s**2 + 5*s + 1, p**2 + 2*p + 3, p) + >>> C = TransferFunction(5*p + 10, p + 10, p) + >>> P = TransferFunction(1 - s, p + 2, p) + >>> F2 = Feedback(TransferFunction(1, 1, p), G*C*P) + >>> F2.sys2 + Series(TransferFunction(2*s**2 + 5*s + 1, p**2 + 2*p + 3, p), TransferFunction(5*p + 10, p + 10, p), TransferFunction(1 - s, p + 2, p)) + + """ + return self.args[1] + + @property + def var(self): + """ + Returns the complex variable of the Laplace transform used by all + the transfer functions involved in the feedback interconnection. + + Examples + ======== + + >>> from sympy.abc import s, p + >>> from sympy.physics.control.lti import TransferFunction, Feedback + >>> plant = TransferFunction(3*s**2 + 7*s - 3, s**2 - 4*s + 2, s) + >>> controller = TransferFunction(5*s - 10, s + 7, s) + >>> F1 = Feedback(plant, controller) + >>> F1.var + s + >>> G = TransferFunction(2*s**2 + 5*s + 1, p**2 + 2*p + 3, p) + >>> C = TransferFunction(5*p + 10, p + 10, p) + >>> P = TransferFunction(1 - s, p + 2, p) + >>> F2 = Feedback(TransferFunction(1, 1, p), G*C*P) + >>> F2.var + p + + """ + return self.sys1.var + + @property + def sign(self): + """ + Returns the type of MIMO Feedback model. ``1`` + for Positive and ``-1`` for Negative. + """ + return self.args[2] + + @property + def num(self): + """ + Returns the numerator of the closed loop feedback system. + """ + return self.sys1 + + @property + def den(self): + """ + Returns the denominator of the closed loop feedback model. + """ + unit = TransferFunction(1, 1, self.var) + arg_list = list(self.sys1.args) if isinstance(self.sys1, Series) else [self.sys1] + if self.sign == 1: + return Parallel(unit, -Series(self.sys2, *arg_list)) + return Parallel(unit, Series(self.sys2, *arg_list)) + + @property + def sensitivity(self): + """ + Returns the sensitivity function of the feedback loop. + + Sensitivity of a Feedback system is the ratio + of change in the open loop gain to the change in + the closed loop gain. + + .. note:: + This method would not return the complementary + sensitivity function. + + Examples + ======== + + >>> from sympy.abc import p + >>> from sympy.physics.control.lti import TransferFunction, Feedback + >>> C = TransferFunction(5*p + 10, p + 10, p) + >>> P = TransferFunction(1 - p, p + 2, p) + >>> F_1 = Feedback(P, C) + >>> F_1.sensitivity + 1/((1 - p)*(5*p + 10)/((p + 2)*(p + 10)) + 1) + + """ + + return 1/(1 - self.sign*self.sys1.to_expr()*self.sys2.to_expr()) + + def doit(self, cancel=False, expand=False, **hints): + """ + Returns the resultant transfer function or state space obtained by + feedback connection of transfer functions or state space objects. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy import Matrix + >>> from sympy.physics.control.lti import TransferFunction, Feedback, StateSpace + >>> plant = TransferFunction(3*s**2 + 7*s - 3, s**2 - 4*s + 2, s) + >>> controller = TransferFunction(5*s - 10, s + 7, s) + >>> F1 = Feedback(plant, controller) + >>> F1.doit() + TransferFunction((s + 7)*(s**2 - 4*s + 2)*(3*s**2 + 7*s - 3), ((s + 7)*(s**2 - 4*s + 2) + (5*s - 10)*(3*s**2 + 7*s - 3))*(s**2 - 4*s + 2), s) + >>> G = TransferFunction(2*s**2 + 5*s + 1, s**2 + 2*s + 3, s) + >>> F2 = Feedback(G, TransferFunction(1, 1, s)) + >>> F2.doit() + TransferFunction((s**2 + 2*s + 3)*(2*s**2 + 5*s + 1), (s**2 + 2*s + 3)*(3*s**2 + 7*s + 4), s) + + Use kwarg ``expand=True`` to expand the resultant transfer function. + Use ``cancel=True`` to cancel out the common terms in numerator and + denominator. + + >>> F2.doit(cancel=True, expand=True) + TransferFunction(2*s**2 + 5*s + 1, 3*s**2 + 7*s + 4, s) + >>> F2.doit(expand=True) + TransferFunction(2*s**4 + 9*s**3 + 17*s**2 + 17*s + 3, 3*s**4 + 13*s**3 + 27*s**2 + 29*s + 12, s) + + If the connection contain any ``StateSpace`` object then ``doit()`` + will return the equivalent ``StateSpace`` object. + + >>> A1 = Matrix([[-1.5, -2], [1, 0]]) + >>> B1 = Matrix([0.5, 0]) + >>> C1 = Matrix([[0, 1]]) + >>> A2 = Matrix([[0, 1], [-5, -2]]) + >>> B2 = Matrix([0, 3]) + >>> C2 = Matrix([[0, 1]]) + >>> ss1 = StateSpace(A1, B1, C1) + >>> ss2 = StateSpace(A2, B2, C2) + >>> F3 = Feedback(ss1, ss2) + >>> F3.doit() + StateSpace(Matrix([ + [-1.5, -2, 0, -0.5], + [ 1, 0, 0, 0], + [ 0, 0, 0, 1], + [ 0, 3, -5, -2]]), Matrix([ + [0.5], + [ 0], + [ 0], + [ 0]]), Matrix([[0, 1, 0, 0]]), Matrix([[0]])) + + """ + if self.is_StateSpace_object: + sys1_ss = self.sys1.doit().rewrite(StateSpace) + sys2_ss = self.sys2.doit().rewrite(StateSpace) + A1, B1, C1, D1 = sys1_ss.A, sys1_ss.B, sys1_ss.C, sys1_ss.D + A2, B2, C2, D2 = sys2_ss.A, sys2_ss.B, sys2_ss.C, sys2_ss.D + + # Create identity matrices + I_inputs = eye(self.num_inputs) + I_outputs = eye(self.num_outputs) + + # Compute F and its inverse + F = I_inputs - self.sign * D2 * D1 + E = F.inv() + + # Compute intermediate matrices + E_D2 = E * D2 + E_C2 = E * C2 + T1 = I_outputs + self.sign * D1 * E_D2 + T2 = I_inputs + self.sign * E_D2 * D1 + A = Matrix.vstack( + Matrix.hstack(A1 + self.sign * B1 * E_D2 * C1, self.sign * B1 * E_C2), + Matrix.hstack(B2 * T1 * C1, A2 + self.sign * B2 * D1 * E_C2) + ) + B = Matrix.vstack(B1 * T2, B2 * D1 * T2) + C = Matrix.hstack(T1 * C1, self.sign * D1 * E_C2) + D = D1 * T2 + return StateSpace(A, B, C, D) + + arg_list = list(self.sys1.args) if isinstance(self.sys1, Series) else [self.sys1] + # F_n and F_d are resultant TFs of num and den of Feedback. + F_n, unit = self.sys1.doit(), TransferFunction(1, 1, self.sys1.var) + if self.sign == -1: + F_d = Parallel(unit, Series(self.sys2, *arg_list)).doit() + else: + F_d = Parallel(unit, -Series(self.sys2, *arg_list)).doit() + + _resultant_tf = TransferFunction(F_n.num * F_d.den, F_n.den * F_d.num, F_n.var) + + if cancel: + _resultant_tf = _resultant_tf.simplify() + + if expand: + _resultant_tf = _resultant_tf.expand() + + return _resultant_tf + + def _eval_rewrite_as_TransferFunction(self, num, den, sign, **kwargs): + if self.is_StateSpace_object: + return self.doit().rewrite(TransferFunction)[0][0] + return self.doit() + + def to_expr(self): + """ + Converts a ``Feedback`` object to SymPy Expr. + + Examples + ======== + + >>> from sympy.abc import s, a, b + >>> from sympy.physics.control.lti import TransferFunction, Feedback + >>> from sympy import Expr + >>> tf1 = TransferFunction(a+s, 1, s) + >>> tf2 = TransferFunction(b+s, 1, s) + >>> fd1 = Feedback(tf1, tf2) + >>> fd1.to_expr() + (a + s)/((a + s)*(b + s) + 1) + >>> isinstance(_, Expr) + True + """ + + return self.doit().to_expr() + + def __neg__(self): + return Feedback(-self.sys1, -self.sys2, self.sign) + + +def _is_invertible(a, b, sign): + """ + Checks whether a given pair of MIMO + systems passed is invertible or not. + """ + _mat = eye(a.num_outputs) - sign*(a.doit()._expr_mat)*(b.doit()._expr_mat) + _det = _mat.det() + + return _det != 0 + + +class MIMOFeedback(MIMOLinearTimeInvariant): + r""" + A class for representing closed-loop feedback interconnection between two + MIMO input/output systems. + + Parameters + ========== + + sys1 : MIMOSeries, TransferFunctionMatrix, StateSpace + The MIMO system placed on the feedforward path. + sys2 : MIMOSeries, TransferFunctionMatrix, StateSpace + The system placed on the feedback path + (often a feedback controller). + sign : int, optional + The sign of feedback. Can either be ``1`` + (for positive feedback) or ``-1`` (for negative feedback). + Default value is `-1`. + + Raises + ====== + + ValueError + When ``sys1`` and ``sys2`` are not using the + same complex variable of the Laplace transform. + + Forward path model should have an equal number of inputs/outputs + to the feedback path outputs/inputs. + + When product of ``sys1`` and ``sys2`` is not a square matrix. + + When the equivalent MIMO system is not invertible. + + TypeError + When either ``sys1`` or ``sys2`` is not a ``MIMOSeries``, + ``TransferFunctionMatrix`` or a ``StateSpace`` object. + + Examples + ======== + + >>> from sympy import Matrix, pprint + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import StateSpace, TransferFunctionMatrix, MIMOFeedback + >>> plant_mat = Matrix([[1, 1/s], [0, 1]]) + >>> controller_mat = Matrix([[10, 0], [0, 10]]) # Constant Gain + >>> plant = TransferFunctionMatrix.from_Matrix(plant_mat, s) + >>> controller = TransferFunctionMatrix.from_Matrix(controller_mat, s) + >>> feedback = MIMOFeedback(plant, controller) # Negative Feedback (default) + >>> pprint(feedback, use_unicode=False) + / [1 1] [10 0 ] \-1 [1 1] + | [- -] [-- - ] | [- -] + | [1 s] [1 1 ] | [1 s] + |I + [ ] *[ ] | * [ ] + | [0 1] [0 10] | [0 1] + | [- -] [- --] | [- -] + \ [1 1]{t} [1 1 ]{t}/ [1 1]{t} + + To get the equivalent system matrix, use either ``doit`` or ``rewrite`` method. + + >>> pprint(feedback.doit(), use_unicode=False) + [1 1 ] + [-- -----] + [11 121*s] + [ ] + [0 1 ] + [- -- ] + [1 11 ]{t} + + To negate the ``MIMOFeedback`` object, use ``-`` operator. + + >>> neg_feedback = -feedback + >>> pprint(neg_feedback.doit(), use_unicode=False) + [-1 -1 ] + [--- -----] + [11 121*s] + [ ] + [ 0 -1 ] + [ - --- ] + [ 1 11 ]{t} + + ``MIMOFeedback`` can also be used to connect MIMO ``StateSpace`` systems. + + >>> A1 = Matrix([[4, 1], [2, -3]]) + >>> B1 = Matrix([[5, 2], [-3, -3]]) + >>> C1 = Matrix([[2, -4], [0, 1]]) + >>> D1 = Matrix([[3, 2], [1, -1]]) + >>> A2 = Matrix([[-3, 4, 2], [-1, -3, 0], [2, 5, 3]]) + >>> B2 = Matrix([[1, 4], [-3, -3], [-2, 1]]) + >>> C2 = Matrix([[4, 2, -3], [1, 4, 3]]) + >>> D2 = Matrix([[-2, 4], [0, 1]]) + >>> ss1 = StateSpace(A1, B1, C1, D1) + >>> ss2 = StateSpace(A2, B2, C2, D2) + >>> F1 = MIMOFeedback(ss1, ss2) + >>> F1 + MIMOFeedback(StateSpace(Matrix([ + [4, 1], + [2, -3]]), Matrix([ + [ 5, 2], + [-3, -3]]), Matrix([ + [2, -4], + [0, 1]]), Matrix([ + [3, 2], + [1, -1]])), StateSpace(Matrix([ + [-3, 4, 2], + [-1, -3, 0], + [ 2, 5, 3]]), Matrix([ + [ 1, 4], + [-3, -3], + [-2, 1]]), Matrix([ + [4, 2, -3], + [1, 4, 3]]), Matrix([ + [-2, 4], + [ 0, 1]])), -1) + + ``doit()`` can be used to find ``StateSpace`` equivalent for the system containing ``StateSpace`` objects. + + >>> F1.doit() + StateSpace(Matrix([ + [ 3, -3/4, -15/4, -37/2, -15], + [ 7/2, -39/8, 9/8, 39/4, 9], + [ 3, -41/4, -45/4, -51/2, -19], + [-9/2, 129/8, 73/8, 171/4, 36], + [-3/2, 47/8, 31/8, 85/4, 18]]), Matrix([ + [-1/4, 19/4], + [ 3/8, -21/8], + [ 1/4, 29/4], + [ 3/8, -93/8], + [ 5/8, -35/8]]), Matrix([ + [ 1, -15/4, -7/4, -21/2, -9], + [1/2, -13/8, -13/8, -19/4, -3]]), Matrix([ + [-1/4, 11/4], + [ 1/8, 9/8]])) + + See Also + ======== + + Feedback, MIMOSeries, MIMOParallel + + """ + def __new__(cls, sys1, sys2, sign=-1): + if not isinstance(sys1, (TransferFunctionMatrix, MIMOSeries, StateSpace)): + raise TypeError("Unsupported type for `sys1` in MIMO Feedback.") + + if not isinstance(sys2, (TransferFunctionMatrix, MIMOSeries, StateSpace)): + raise TypeError("Unsupported type for `sys2` in MIMO Feedback.") + + if sys1.num_inputs != sys2.num_outputs or \ + sys1.num_outputs != sys2.num_inputs: + raise ValueError(filldedent(""" + Product of `sys1` and `sys2` must + yield a square matrix.""")) + + if sign not in (-1, 1): + raise ValueError(filldedent(""" + Unsupported type for feedback. `sign` arg should + either be 1 (positive feedback loop) or -1 + (negative feedback loop).""")) + + if sys1.is_StateSpace_object or sys2.is_StateSpace_object: + cls.is_StateSpace_object = True + else: + if not _is_invertible(sys1, sys2, sign): + raise ValueError("Non-Invertible system inputted.") + cls.is_StateSpace_object = False + + if not cls.is_StateSpace_object and sys1.var != sys2.var: + raise ValueError(filldedent(""" + Both `sys1` and `sys2` should be using the + same complex variable.""")) + + return super().__new__(cls, sys1, sys2, _sympify(sign)) + + @property + def sys1(self): + r""" + Returns the system placed on the feedforward path of the MIMO feedback interconnection. + + Examples + ======== + + >>> from sympy import pprint + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction, TransferFunctionMatrix, MIMOFeedback + >>> tf1 = TransferFunction(s**2 + s + 1, s**2 - s + 1, s) + >>> tf2 = TransferFunction(1, s, s) + >>> tf3 = TransferFunction(1, 1, s) + >>> sys1 = TransferFunctionMatrix([[tf1, tf2], [tf2, tf1]]) + >>> sys2 = TransferFunctionMatrix([[tf3, tf3], [tf3, tf2]]) + >>> F_1 = MIMOFeedback(sys1, sys2, 1) + >>> F_1.sys1 + TransferFunctionMatrix(((TransferFunction(s**2 + s + 1, s**2 - s + 1, s), TransferFunction(1, s, s)), (TransferFunction(1, s, s), TransferFunction(s**2 + s + 1, s**2 - s + 1, s)))) + >>> pprint(_, use_unicode=False) + [ 2 ] + [s + s + 1 1 ] + [---------- - ] + [ 2 s ] + [s - s + 1 ] + [ ] + [ 2 ] + [ 1 s + s + 1] + [ - ----------] + [ s 2 ] + [ s - s + 1]{t} + + """ + return self.args[0] + + @property + def sys2(self): + r""" + Returns the feedback controller of the MIMO feedback interconnection. + + Examples + ======== + + >>> from sympy import pprint + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction, TransferFunctionMatrix, MIMOFeedback + >>> tf1 = TransferFunction(s**2, s**3 - s + 1, s) + >>> tf2 = TransferFunction(1, s, s) + >>> tf3 = TransferFunction(1, 1, s) + >>> sys1 = TransferFunctionMatrix([[tf1, tf2], [tf2, tf1]]) + >>> sys2 = TransferFunctionMatrix([[tf1, tf3], [tf3, tf2]]) + >>> F_1 = MIMOFeedback(sys1, sys2) + >>> F_1.sys2 + TransferFunctionMatrix(((TransferFunction(s**2, s**3 - s + 1, s), TransferFunction(1, 1, s)), (TransferFunction(1, 1, s), TransferFunction(1, s, s)))) + >>> pprint(_, use_unicode=False) + [ 2 ] + [ s 1] + [---------- -] + [ 3 1] + [s - s + 1 ] + [ ] + [ 1 1] + [ - -] + [ 1 s]{t} + + """ + return self.args[1] + + @property + def var(self): + r""" + Returns the complex variable of the Laplace transform used by all + the transfer functions involved in the MIMO feedback loop. + + Examples + ======== + + >>> from sympy.abc import p + >>> from sympy.physics.control.lti import TransferFunction, TransferFunctionMatrix, MIMOFeedback + >>> tf1 = TransferFunction(p, 1 - p, p) + >>> tf2 = TransferFunction(1, p, p) + >>> tf3 = TransferFunction(1, 1, p) + >>> sys1 = TransferFunctionMatrix([[tf1, tf2], [tf2, tf1]]) + >>> sys2 = TransferFunctionMatrix([[tf1, tf3], [tf3, tf2]]) + >>> F_1 = MIMOFeedback(sys1, sys2, 1) # Positive feedback + >>> F_1.var + p + + """ + return self.sys1.var + + @property + def sign(self): + r""" + Returns the type of feedback interconnection of two models. ``1`` + for Positive and ``-1`` for Negative. + """ + return self.args[2] + + @property + def sensitivity(self): + r""" + Returns the sensitivity function matrix of the feedback loop. + + Sensitivity of a closed-loop system is the ratio of change + in the open loop gain to the change in the closed loop gain. + + .. note:: + This method would not return the complementary + sensitivity function. + + Examples + ======== + + >>> from sympy import pprint + >>> from sympy.abc import p + >>> from sympy.physics.control.lti import TransferFunction, TransferFunctionMatrix, MIMOFeedback + >>> tf1 = TransferFunction(p, 1 - p, p) + >>> tf2 = TransferFunction(1, p, p) + >>> tf3 = TransferFunction(1, 1, p) + >>> sys1 = TransferFunctionMatrix([[tf1, tf2], [tf2, tf1]]) + >>> sys2 = TransferFunctionMatrix([[tf1, tf3], [tf3, tf2]]) + >>> F_1 = MIMOFeedback(sys1, sys2, 1) # Positive feedback + >>> F_2 = MIMOFeedback(sys1, sys2) # Negative feedback + >>> pprint(F_1.sensitivity, use_unicode=False) + [ 4 3 2 5 4 2 ] + [- p + 3*p - 4*p + 3*p - 1 p - 2*p + 3*p - 3*p + 1 ] + [---------------------------- -----------------------------] + [ 4 3 2 5 4 3 2 ] + [ p + 3*p - 8*p + 8*p - 3 p + 3*p - 8*p + 8*p - 3*p] + [ ] + [ 4 3 2 3 2 ] + [ p - p - p + p 3*p - 6*p + 4*p - 1 ] + [ -------------------------- -------------------------- ] + [ 4 3 2 4 3 2 ] + [ p + 3*p - 8*p + 8*p - 3 p + 3*p - 8*p + 8*p - 3 ] + >>> pprint(F_2.sensitivity, use_unicode=False) + [ 4 3 2 5 4 2 ] + [p - 3*p + 2*p + p - 1 p - 2*p + 3*p - 3*p + 1] + [------------------------ --------------------------] + [ 4 3 5 4 2 ] + [ p - 3*p + 2*p - 1 p - 3*p + 2*p - p ] + [ ] + [ 4 3 2 4 3 ] + [ p - p - p + p 2*p - 3*p + 2*p - 1 ] + [ ------------------- --------------------- ] + [ 4 3 4 3 ] + [ p - 3*p + 2*p - 1 p - 3*p + 2*p - 1 ] + + """ + _sys1_mat = self.sys1.doit()._expr_mat + _sys2_mat = self.sys2.doit()._expr_mat + + return (eye(self.sys1.num_inputs) - \ + self.sign*_sys1_mat*_sys2_mat).inv() + + @property + def num_inputs(self): + """Returns the number of inputs of the system.""" + return self.sys1.num_inputs + + @property + def num_outputs(self): + """Returns the number of outputs of the system.""" + return self.sys1.num_outputs + + def doit(self, cancel=True, expand=False, **hints): + r""" + Returns the resultant transfer function matrix obtained by the + feedback interconnection. + + Examples + ======== + + >>> from sympy import pprint + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction, TransferFunctionMatrix, MIMOFeedback + >>> tf1 = TransferFunction(s, 1 - s, s) + >>> tf2 = TransferFunction(1, s, s) + >>> tf3 = TransferFunction(5, 1, s) + >>> tf4 = TransferFunction(s - 1, s, s) + >>> tf5 = TransferFunction(0, 1, s) + >>> sys1 = TransferFunctionMatrix([[tf1, tf2], [tf3, tf4]]) + >>> sys2 = TransferFunctionMatrix([[tf3, tf5], [tf5, tf5]]) + >>> F_1 = MIMOFeedback(sys1, sys2, 1) + >>> pprint(F_1, use_unicode=False) + / [ s 1 ] [5 0] \-1 [ s 1 ] + | [----- - ] [- -] | [----- - ] + | [1 - s s ] [1 1] | [1 - s s ] + |I - [ ] *[ ] | * [ ] + | [ 5 s - 1] [0 0] | [ 5 s - 1] + | [ - -----] [- -] | [ - -----] + \ [ 1 s ]{t} [1 1]{t}/ [ 1 s ]{t} + >>> pprint(F_1.doit(), use_unicode=False) + [ -s s - 1 ] + [------- ----------- ] + [6*s - 1 s*(6*s - 1) ] + [ ] + [5*s - 5 (s - 1)*(6*s + 24)] + [------- ------------------] + [6*s - 1 s*(6*s - 1) ]{t} + + If the user wants the resultant ``TransferFunctionMatrix`` object without + canceling the common factors then the ``cancel`` kwarg should be passed ``False``. + + >>> pprint(F_1.doit(cancel=False), use_unicode=False) + [ s*(s - 1) s - 1 ] + [ ----------------- ----------- ] + [ (1 - s)*(6*s - 1) s*(6*s - 1) ] + [ ] + [s*(25*s - 25) + 5*(1 - s)*(6*s - 1) s*(s - 1)*(6*s - 1) + s*(25*s - 25)] + [----------------------------------- -----------------------------------] + [ (1 - s)*(6*s - 1) 2 ] + [ s *(6*s - 1) ]{t} + + If the user wants the expanded form of the resultant transfer function matrix, + the ``expand`` kwarg should be passed as ``True``. + + >>> pprint(F_1.doit(expand=True), use_unicode=False) + [ -s s - 1 ] + [------- -------- ] + [6*s - 1 2 ] + [ 6*s - s ] + [ ] + [ 2 ] + [5*s - 5 6*s + 18*s - 24] + [------- ----------------] + [6*s - 1 2 ] + [ 6*s - s ]{t} + + """ + if self.is_StateSpace_object: + sys1_ss = self.sys1.doit().rewrite(StateSpace) + sys2_ss = self.sys2.doit().rewrite(StateSpace) + A1, B1, C1, D1 = sys1_ss.A, sys1_ss.B, sys1_ss.C, sys1_ss.D + A2, B2, C2, D2 = sys2_ss.A, sys2_ss.B, sys2_ss.C, sys2_ss.D + + # Create identity matrices + I_inputs = eye(self.num_inputs) + I_outputs = eye(self.num_outputs) + + # Compute F and its inverse + F = I_inputs - self.sign * D2 * D1 + E = F.inv() + + # Compute intermediate matrices + E_D2 = E * D2 + E_C2 = E * C2 + T1 = I_outputs + self.sign * D1 * E_D2 + T2 = I_inputs + self.sign * E_D2 * D1 + A = Matrix.vstack( + Matrix.hstack(A1 + self.sign * B1 * E_D2 * C1, self.sign * B1 * E_C2), + Matrix.hstack(B2 * T1 * C1, A2 + self.sign * B2 * D1 * E_C2) + ) + B = Matrix.vstack(B1 * T2, B2 * D1 * T2) + C = Matrix.hstack(T1 * C1, self.sign * D1 * E_C2) + D = D1 * T2 + return StateSpace(A, B, C, D) + + _mat = self.sensitivity * self.sys1.doit()._expr_mat + + _resultant_tfm = _to_TFM(_mat, self.var) + + if cancel: + _resultant_tfm = _resultant_tfm.simplify() + + if expand: + _resultant_tfm = _resultant_tfm.expand() + + return _resultant_tfm + + def _eval_rewrite_as_TransferFunctionMatrix(self, sys1, sys2, sign, **kwargs): + return self.doit() + + def __neg__(self): + return MIMOFeedback(-self.sys1, -self.sys2, self.sign) + + +def _to_TFM(mat, var): + """Private method to convert ImmutableMatrix to TransferFunctionMatrix efficiently""" + to_tf = lambda expr: TransferFunction.from_rational_expression(expr, var) + arg = [[to_tf(expr) for expr in row] for row in mat.tolist()] + return TransferFunctionMatrix(arg) + + +class TransferFunctionMatrix(MIMOLinearTimeInvariant): + r""" + A class for representing the MIMO (multiple-input and multiple-output) + generalization of the SISO (single-input and single-output) transfer function. + + It is a matrix of transfer functions (``TransferFunction``, SISO-``Series`` or SISO-``Parallel``). + There is only one argument, ``arg`` which is also the compulsory argument. + ``arg`` is expected to be strictly of the type list of lists + which holds the transfer functions or reducible to transfer functions. + + Parameters + ========== + + arg : Nested ``List`` (strictly). + Users are expected to input a nested list of ``TransferFunction``, ``Series`` + and/or ``Parallel`` objects. + + Examples + ======== + + .. note:: + ``pprint()`` can be used for better visualization of ``TransferFunctionMatrix`` objects. + + >>> from sympy.abc import s, p, a + >>> from sympy import pprint + >>> from sympy.physics.control.lti import TransferFunction, TransferFunctionMatrix, Series, Parallel + >>> tf_1 = TransferFunction(s + a, s**2 + s + 1, s) + >>> tf_2 = TransferFunction(p**4 - 3*p + 2, s + p, s) + >>> tf_3 = TransferFunction(3, s + 2, s) + >>> tf_4 = TransferFunction(-a + p, 9*s - 9, s) + >>> tfm_1 = TransferFunctionMatrix([[tf_1], [tf_2], [tf_3]]) + >>> tfm_1 + TransferFunctionMatrix(((TransferFunction(a + s, s**2 + s + 1, s),), (TransferFunction(p**4 - 3*p + 2, p + s, s),), (TransferFunction(3, s + 2, s),))) + >>> tfm_1.var + s + >>> tfm_1.num_inputs + 1 + >>> tfm_1.num_outputs + 3 + >>> tfm_1.shape + (3, 1) + >>> tfm_1.args + (((TransferFunction(a + s, s**2 + s + 1, s),), (TransferFunction(p**4 - 3*p + 2, p + s, s),), (TransferFunction(3, s + 2, s),)),) + >>> tfm_2 = TransferFunctionMatrix([[tf_1, -tf_3], [tf_2, -tf_1], [tf_3, -tf_2]]) + >>> tfm_2 + TransferFunctionMatrix(((TransferFunction(a + s, s**2 + s + 1, s), TransferFunction(-3, s + 2, s)), (TransferFunction(p**4 - 3*p + 2, p + s, s), TransferFunction(-a - s, s**2 + s + 1, s)), (TransferFunction(3, s + 2, s), TransferFunction(-p**4 + 3*p - 2, p + s, s)))) + >>> pprint(tfm_2, use_unicode=False) # pretty-printing for better visualization + [ a + s -3 ] + [ ---------- ----- ] + [ 2 s + 2 ] + [ s + s + 1 ] + [ ] + [ 4 ] + [p - 3*p + 2 -a - s ] + [------------ ---------- ] + [ p + s 2 ] + [ s + s + 1 ] + [ ] + [ 4 ] + [ 3 - p + 3*p - 2] + [ ----- --------------] + [ s + 2 p + s ]{t} + + TransferFunctionMatrix can be transposed, if user wants to switch the input and output transfer functions + + >>> tfm_2.transpose() + TransferFunctionMatrix(((TransferFunction(a + s, s**2 + s + 1, s), TransferFunction(p**4 - 3*p + 2, p + s, s), TransferFunction(3, s + 2, s)), (TransferFunction(-3, s + 2, s), TransferFunction(-a - s, s**2 + s + 1, s), TransferFunction(-p**4 + 3*p - 2, p + s, s)))) + >>> pprint(_, use_unicode=False) + [ 4 ] + [ a + s p - 3*p + 2 3 ] + [---------- ------------ ----- ] + [ 2 p + s s + 2 ] + [s + s + 1 ] + [ ] + [ 4 ] + [ -3 -a - s - p + 3*p - 2] + [ ----- ---------- --------------] + [ s + 2 2 p + s ] + [ s + s + 1 ]{t} + + >>> tf_5 = TransferFunction(5, s, s) + >>> tf_6 = TransferFunction(5*s, (2 + s**2), s) + >>> tf_7 = TransferFunction(5, (s*(2 + s**2)), s) + >>> tf_8 = TransferFunction(5, 1, s) + >>> tfm_3 = TransferFunctionMatrix([[tf_5, tf_6], [tf_7, tf_8]]) + >>> tfm_3 + TransferFunctionMatrix(((TransferFunction(5, s, s), TransferFunction(5*s, s**2 + 2, s)), (TransferFunction(5, s*(s**2 + 2), s), TransferFunction(5, 1, s)))) + >>> pprint(tfm_3, use_unicode=False) + [ 5 5*s ] + [ - ------] + [ s 2 ] + [ s + 2] + [ ] + [ 5 5 ] + [---------- - ] + [ / 2 \ 1 ] + [s*\s + 2/ ]{t} + >>> tfm_3.var + s + >>> tfm_3.shape + (2, 2) + >>> tfm_3.num_outputs + 2 + >>> tfm_3.num_inputs + 2 + >>> tfm_3.args + (((TransferFunction(5, s, s), TransferFunction(5*s, s**2 + 2, s)), (TransferFunction(5, s*(s**2 + 2), s), TransferFunction(5, 1, s))),) + + To access the ``TransferFunction`` at any index in the ``TransferFunctionMatrix``, use the index notation. + + >>> tfm_3[1, 0] # gives the TransferFunction present at 2nd Row and 1st Col. Similar to that in Matrix classes + TransferFunction(5, s*(s**2 + 2), s) + >>> tfm_3[0, 0] # gives the TransferFunction present at 1st Row and 1st Col. + TransferFunction(5, s, s) + >>> tfm_3[:, 0] # gives the first column + TransferFunctionMatrix(((TransferFunction(5, s, s),), (TransferFunction(5, s*(s**2 + 2), s),))) + >>> pprint(_, use_unicode=False) + [ 5 ] + [ - ] + [ s ] + [ ] + [ 5 ] + [----------] + [ / 2 \] + [s*\s + 2/]{t} + >>> tfm_3[0, :] # gives the first row + TransferFunctionMatrix(((TransferFunction(5, s, s), TransferFunction(5*s, s**2 + 2, s)),)) + >>> pprint(_, use_unicode=False) + [5 5*s ] + [- ------] + [s 2 ] + [ s + 2]{t} + + To negate a transfer function matrix, ``-`` operator can be prepended: + + >>> tfm_4 = TransferFunctionMatrix([[tf_2], [-tf_1], [tf_3]]) + >>> -tfm_4 + TransferFunctionMatrix(((TransferFunction(-p**4 + 3*p - 2, p + s, s),), (TransferFunction(a + s, s**2 + s + 1, s),), (TransferFunction(-3, s + 2, s),))) + >>> tfm_5 = TransferFunctionMatrix([[tf_1, tf_2], [tf_3, -tf_1]]) + >>> -tfm_5 + TransferFunctionMatrix(((TransferFunction(-a - s, s**2 + s + 1, s), TransferFunction(-p**4 + 3*p - 2, p + s, s)), (TransferFunction(-3, s + 2, s), TransferFunction(a + s, s**2 + s + 1, s)))) + + ``subs()`` returns the ``TransferFunctionMatrix`` object with the value substituted in the expression. This will not + mutate your original ``TransferFunctionMatrix``. + + >>> tfm_2.subs(p, 2) # substituting p everywhere in tfm_2 with 2. + TransferFunctionMatrix(((TransferFunction(a + s, s**2 + s + 1, s), TransferFunction(-3, s + 2, s)), (TransferFunction(12, s + 2, s), TransferFunction(-a - s, s**2 + s + 1, s)), (TransferFunction(3, s + 2, s), TransferFunction(-12, s + 2, s)))) + >>> pprint(_, use_unicode=False) + [ a + s -3 ] + [---------- ----- ] + [ 2 s + 2 ] + [s + s + 1 ] + [ ] + [ 12 -a - s ] + [ ----- ----------] + [ s + 2 2 ] + [ s + s + 1] + [ ] + [ 3 -12 ] + [ ----- ----- ] + [ s + 2 s + 2 ]{t} + >>> pprint(tfm_2, use_unicode=False) # State of tfm_2 is unchanged after substitution + [ a + s -3 ] + [ ---------- ----- ] + [ 2 s + 2 ] + [ s + s + 1 ] + [ ] + [ 4 ] + [p - 3*p + 2 -a - s ] + [------------ ---------- ] + [ p + s 2 ] + [ s + s + 1 ] + [ ] + [ 4 ] + [ 3 - p + 3*p - 2] + [ ----- --------------] + [ s + 2 p + s ]{t} + + ``subs()`` also supports multiple substitutions. + + >>> tfm_2.subs({p: 2, a: 1}) # substituting p with 2 and a with 1 + TransferFunctionMatrix(((TransferFunction(s + 1, s**2 + s + 1, s), TransferFunction(-3, s + 2, s)), (TransferFunction(12, s + 2, s), TransferFunction(-s - 1, s**2 + s + 1, s)), (TransferFunction(3, s + 2, s), TransferFunction(-12, s + 2, s)))) + >>> pprint(_, use_unicode=False) + [ s + 1 -3 ] + [---------- ----- ] + [ 2 s + 2 ] + [s + s + 1 ] + [ ] + [ 12 -s - 1 ] + [ ----- ----------] + [ s + 2 2 ] + [ s + s + 1] + [ ] + [ 3 -12 ] + [ ----- ----- ] + [ s + 2 s + 2 ]{t} + + Users can reduce the ``Series`` and ``Parallel`` elements of the matrix to ``TransferFunction`` by using + ``doit()``. + + >>> tfm_6 = TransferFunctionMatrix([[Series(tf_3, tf_4), Parallel(tf_3, tf_4)]]) + >>> tfm_6 + TransferFunctionMatrix(((Series(TransferFunction(3, s + 2, s), TransferFunction(-a + p, 9*s - 9, s)), Parallel(TransferFunction(3, s + 2, s), TransferFunction(-a + p, 9*s - 9, s))),)) + >>> pprint(tfm_6, use_unicode=False) + [-a + p 3 -a + p 3 ] + [-------*----- ------- + -----] + [9*s - 9 s + 2 9*s - 9 s + 2]{t} + >>> tfm_6.doit() + TransferFunctionMatrix(((TransferFunction(-3*a + 3*p, (s + 2)*(9*s - 9), s), TransferFunction(27*s + (-a + p)*(s + 2) - 27, (s + 2)*(9*s - 9), s)),)) + >>> pprint(_, use_unicode=False) + [ -3*a + 3*p 27*s + (-a + p)*(s + 2) - 27] + [----------------- ----------------------------] + [(s + 2)*(9*s - 9) (s + 2)*(9*s - 9) ]{t} + >>> tf_9 = TransferFunction(1, s, s) + >>> tf_10 = TransferFunction(1, s**2, s) + >>> tfm_7 = TransferFunctionMatrix([[Series(tf_9, tf_10), tf_9], [tf_10, Parallel(tf_9, tf_10)]]) + >>> tfm_7 + TransferFunctionMatrix(((Series(TransferFunction(1, s, s), TransferFunction(1, s**2, s)), TransferFunction(1, s, s)), (TransferFunction(1, s**2, s), Parallel(TransferFunction(1, s, s), TransferFunction(1, s**2, s))))) + >>> pprint(tfm_7, use_unicode=False) + [ 1 1 ] + [---- - ] + [ 2 s ] + [s*s ] + [ ] + [ 1 1 1] + [ -- -- + -] + [ 2 2 s] + [ s s ]{t} + >>> tfm_7.doit() + TransferFunctionMatrix(((TransferFunction(1, s**3, s), TransferFunction(1, s, s)), (TransferFunction(1, s**2, s), TransferFunction(s**2 + s, s**3, s)))) + >>> pprint(_, use_unicode=False) + [1 1 ] + [-- - ] + [ 3 s ] + [s ] + [ ] + [ 2 ] + [1 s + s] + [-- ------] + [ 2 3 ] + [s s ]{t} + + Addition, subtraction, and multiplication of transfer function matrices can form + unevaluated ``Series`` or ``Parallel`` objects. + + - For addition and subtraction: + All the transfer function matrices must have the same shape. + + - For multiplication (C = A * B): + The number of inputs of the first transfer function matrix (A) must be equal to the + number of outputs of the second transfer function matrix (B). + + Also, use pretty-printing (``pprint``) to analyse better. + + >>> tfm_8 = TransferFunctionMatrix([[tf_3], [tf_2], [-tf_1]]) + >>> tfm_9 = TransferFunctionMatrix([[-tf_3]]) + >>> tfm_10 = TransferFunctionMatrix([[tf_1], [tf_2], [tf_4]]) + >>> tfm_11 = TransferFunctionMatrix([[tf_4], [-tf_1]]) + >>> tfm_12 = TransferFunctionMatrix([[tf_4, -tf_1, tf_3], [-tf_2, -tf_4, -tf_3]]) + >>> tfm_8 + tfm_10 + MIMOParallel(TransferFunctionMatrix(((TransferFunction(3, s + 2, s),), (TransferFunction(p**4 - 3*p + 2, p + s, s),), (TransferFunction(-a - s, s**2 + s + 1, s),))), TransferFunctionMatrix(((TransferFunction(a + s, s**2 + s + 1, s),), (TransferFunction(p**4 - 3*p + 2, p + s, s),), (TransferFunction(-a + p, 9*s - 9, s),)))) + >>> pprint(_, use_unicode=False) + [ 3 ] [ a + s ] + [ ----- ] [ ---------- ] + [ s + 2 ] [ 2 ] + [ ] [ s + s + 1 ] + [ 4 ] [ ] + [p - 3*p + 2] [ 4 ] + [------------] + [p - 3*p + 2] + [ p + s ] [------------] + [ ] [ p + s ] + [ -a - s ] [ ] + [ ---------- ] [ -a + p ] + [ 2 ] [ ------- ] + [ s + s + 1 ]{t} [ 9*s - 9 ]{t} + >>> -tfm_10 - tfm_8 + MIMOParallel(TransferFunctionMatrix(((TransferFunction(-a - s, s**2 + s + 1, s),), (TransferFunction(-p**4 + 3*p - 2, p + s, s),), (TransferFunction(a - p, 9*s - 9, s),))), TransferFunctionMatrix(((TransferFunction(-3, s + 2, s),), (TransferFunction(-p**4 + 3*p - 2, p + s, s),), (TransferFunction(a + s, s**2 + s + 1, s),)))) + >>> pprint(_, use_unicode=False) + [ -a - s ] [ -3 ] + [ ---------- ] [ ----- ] + [ 2 ] [ s + 2 ] + [ s + s + 1 ] [ ] + [ ] [ 4 ] + [ 4 ] [- p + 3*p - 2] + [- p + 3*p - 2] + [--------------] + [--------------] [ p + s ] + [ p + s ] [ ] + [ ] [ a + s ] + [ a - p ] [ ---------- ] + [ ------- ] [ 2 ] + [ 9*s - 9 ]{t} [ s + s + 1 ]{t} + >>> tfm_12 * tfm_8 + MIMOSeries(TransferFunctionMatrix(((TransferFunction(3, s + 2, s),), (TransferFunction(p**4 - 3*p + 2, p + s, s),), (TransferFunction(-a - s, s**2 + s + 1, s),))), TransferFunctionMatrix(((TransferFunction(-a + p, 9*s - 9, s), TransferFunction(-a - s, s**2 + s + 1, s), TransferFunction(3, s + 2, s)), (TransferFunction(-p**4 + 3*p - 2, p + s, s), TransferFunction(a - p, 9*s - 9, s), TransferFunction(-3, s + 2, s))))) + >>> pprint(_, use_unicode=False) + [ 3 ] + [ ----- ] + [ -a + p -a - s 3 ] [ s + 2 ] + [ ------- ---------- -----] [ ] + [ 9*s - 9 2 s + 2] [ 4 ] + [ s + s + 1 ] [p - 3*p + 2] + [ ] *[------------] + [ 4 ] [ p + s ] + [- p + 3*p - 2 a - p -3 ] [ ] + [-------------- ------- -----] [ -a - s ] + [ p + s 9*s - 9 s + 2]{t} [ ---------- ] + [ 2 ] + [ s + s + 1 ]{t} + >>> tfm_12 * tfm_8 * tfm_9 + MIMOSeries(TransferFunctionMatrix(((TransferFunction(-3, s + 2, s),),)), TransferFunctionMatrix(((TransferFunction(3, s + 2, s),), (TransferFunction(p**4 - 3*p + 2, p + s, s),), (TransferFunction(-a - s, s**2 + s + 1, s),))), TransferFunctionMatrix(((TransferFunction(-a + p, 9*s - 9, s), TransferFunction(-a - s, s**2 + s + 1, s), TransferFunction(3, s + 2, s)), (TransferFunction(-p**4 + 3*p - 2, p + s, s), TransferFunction(a - p, 9*s - 9, s), TransferFunction(-3, s + 2, s))))) + >>> pprint(_, use_unicode=False) + [ 3 ] + [ ----- ] + [ -a + p -a - s 3 ] [ s + 2 ] + [ ------- ---------- -----] [ ] + [ 9*s - 9 2 s + 2] [ 4 ] + [ s + s + 1 ] [p - 3*p + 2] [ -3 ] + [ ] *[------------] *[-----] + [ 4 ] [ p + s ] [s + 2]{t} + [- p + 3*p - 2 a - p -3 ] [ ] + [-------------- ------- -----] [ -a - s ] + [ p + s 9*s - 9 s + 2]{t} [ ---------- ] + [ 2 ] + [ s + s + 1 ]{t} + >>> tfm_10 + tfm_8*tfm_9 + MIMOParallel(TransferFunctionMatrix(((TransferFunction(a + s, s**2 + s + 1, s),), (TransferFunction(p**4 - 3*p + 2, p + s, s),), (TransferFunction(-a + p, 9*s - 9, s),))), MIMOSeries(TransferFunctionMatrix(((TransferFunction(-3, s + 2, s),),)), TransferFunctionMatrix(((TransferFunction(3, s + 2, s),), (TransferFunction(p**4 - 3*p + 2, p + s, s),), (TransferFunction(-a - s, s**2 + s + 1, s),))))) + >>> pprint(_, use_unicode=False) + [ a + s ] [ 3 ] + [ ---------- ] [ ----- ] + [ 2 ] [ s + 2 ] + [ s + s + 1 ] [ ] + [ ] [ 4 ] + [ 4 ] [p - 3*p + 2] [ -3 ] + [p - 3*p + 2] + [------------] *[-----] + [------------] [ p + s ] [s + 2]{t} + [ p + s ] [ ] + [ ] [ -a - s ] + [ -a + p ] [ ---------- ] + [ ------- ] [ 2 ] + [ 9*s - 9 ]{t} [ s + s + 1 ]{t} + + These unevaluated ``Series`` or ``Parallel`` objects can convert into the + resultant transfer function matrix using ``.doit()`` method or by + ``.rewrite(TransferFunctionMatrix)``. + + >>> (-tfm_8 + tfm_10 + tfm_8*tfm_9).doit() + TransferFunctionMatrix(((TransferFunction((a + s)*(s + 2)**3 - 3*(s + 2)**2*(s**2 + s + 1) - 9*(s + 2)*(s**2 + s + 1), (s + 2)**3*(s**2 + s + 1), s),), (TransferFunction((p + s)*(-3*p**4 + 9*p - 6), (p + s)**2*(s + 2), s),), (TransferFunction((-a + p)*(s + 2)*(s**2 + s + 1)**2 + (a + s)*(s + 2)*(9*s - 9)*(s**2 + s + 1) + (3*a + 3*s)*(9*s - 9)*(s**2 + s + 1), (s + 2)*(9*s - 9)*(s**2 + s + 1)**2, s),))) + >>> (-tfm_12 * -tfm_8 * -tfm_9).rewrite(TransferFunctionMatrix) + TransferFunctionMatrix(((TransferFunction(3*(-3*a + 3*p)*(p + s)*(s + 2)*(s**2 + s + 1)**2 + 3*(-3*a - 3*s)*(p + s)*(s + 2)*(9*s - 9)*(s**2 + s + 1) + 3*(a + s)*(s + 2)**2*(9*s - 9)*(-p**4 + 3*p - 2)*(s**2 + s + 1), (p + s)*(s + 2)**3*(9*s - 9)*(s**2 + s + 1)**2, s),), (TransferFunction(3*(-a + p)*(p + s)*(s + 2)**2*(-p**4 + 3*p - 2)*(s**2 + s + 1) + 3*(3*a + 3*s)*(p + s)**2*(s + 2)*(9*s - 9) + 3*(p + s)*(s + 2)*(9*s - 9)*(-3*p**4 + 9*p - 6)*(s**2 + s + 1), (p + s)**2*(s + 2)**3*(9*s - 9)*(s**2 + s + 1), s),))) + + See Also + ======== + + TransferFunction, MIMOSeries, MIMOParallel, Feedback + + """ + def __new__(cls, arg): + + expr_mat_arg = [] + try: + var = arg[0][0].var + except TypeError: + raise ValueError(filldedent(""" + `arg` param in TransferFunctionMatrix should + strictly be a nested list containing TransferFunction + objects.""")) + for row in arg: + temp = [] + for element in row: + if not isinstance(element, SISOLinearTimeInvariant): + raise TypeError(filldedent(""" + Each element is expected to be of + type `SISOLinearTimeInvariant`.""")) + + if var != element.var: + raise ValueError(filldedent(""" + Conflicting value(s) found for `var`. All TransferFunction + instances in TransferFunctionMatrix should use the same + complex variable in Laplace domain.""")) + + temp.append(element.to_expr()) + expr_mat_arg.append(temp) + + if isinstance(arg, (tuple, list, Tuple)): + # Making nested Tuple (sympy.core.containers.Tuple) from nested list or nested Python tuple + arg = Tuple(*(Tuple(*r, sympify=False) for r in arg), sympify=False) + + obj = super(TransferFunctionMatrix, cls).__new__(cls, arg) + obj._expr_mat = ImmutableMatrix(expr_mat_arg) + obj.is_StateSpace_object = False + return obj + + @classmethod + def from_Matrix(cls, matrix, var): + """ + Creates a new ``TransferFunctionMatrix`` efficiently from a SymPy Matrix of ``Expr`` objects. + + Parameters + ========== + + matrix : ``ImmutableMatrix`` having ``Expr``/``Number`` elements. + var : Symbol + Complex variable of the Laplace transform which will be used by the + all the ``TransferFunction`` objects in the ``TransferFunctionMatrix``. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunctionMatrix + >>> from sympy import Matrix, pprint + >>> M = Matrix([[s, 1/s], [1/(s+1), s]]) + >>> M_tf = TransferFunctionMatrix.from_Matrix(M, s) + >>> pprint(M_tf, use_unicode=False) + [ s 1] + [ - -] + [ 1 s] + [ ] + [ 1 s] + [----- -] + [s + 1 1]{t} + >>> M_tf.elem_poles() + [[[], [0]], [[-1], []]] + >>> M_tf.elem_zeros() + [[[0], []], [[], [0]]] + + """ + return _to_TFM(matrix, var) + + @property + def var(self): + """ + Returns the complex variable used by all the transfer functions or + ``Series``/``Parallel`` objects in a transfer function matrix. + + Examples + ======== + + >>> from sympy.abc import p, s + >>> from sympy.physics.control.lti import TransferFunction, TransferFunctionMatrix, Series, Parallel + >>> G1 = TransferFunction(p**2 + 2*p + 4, p - 6, p) + >>> G2 = TransferFunction(p, 4 - p, p) + >>> G3 = TransferFunction(0, p**4 - 1, p) + >>> G4 = TransferFunction(s + 1, s**2 + s + 1, s) + >>> S1 = Series(G1, G2) + >>> S2 = Series(-G3, Parallel(G2, -G1)) + >>> tfm1 = TransferFunctionMatrix([[G1], [G2], [G3]]) + >>> tfm1.var + p + >>> tfm2 = TransferFunctionMatrix([[-S1, -S2], [S1, S2]]) + >>> tfm2.var + p + >>> tfm3 = TransferFunctionMatrix([[G4]]) + >>> tfm3.var + s + + """ + return self.args[0][0][0].var + + @property + def num_inputs(self): + """ + Returns the number of inputs of the system. + + Examples + ======== + + >>> from sympy.abc import s, p + >>> from sympy.physics.control.lti import TransferFunction, TransferFunctionMatrix + >>> G1 = TransferFunction(s + 3, s**2 - 3, s) + >>> G2 = TransferFunction(4, s**2, s) + >>> G3 = TransferFunction(p**2 + s**2, p - 3, s) + >>> tfm_1 = TransferFunctionMatrix([[G2, -G1, G3], [-G2, -G1, -G3]]) + >>> tfm_1.num_inputs + 3 + + See Also + ======== + + num_outputs + + """ + return self._expr_mat.shape[1] + + @property + def num_outputs(self): + """ + Returns the number of outputs of the system. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunctionMatrix + >>> from sympy import Matrix + >>> M_1 = Matrix([[s], [1/s]]) + >>> TFM = TransferFunctionMatrix.from_Matrix(M_1, s) + >>> print(TFM) + TransferFunctionMatrix(((TransferFunction(s, 1, s),), (TransferFunction(1, s, s),))) + >>> TFM.num_outputs + 2 + + See Also + ======== + + num_inputs + + """ + return self._expr_mat.shape[0] + + @property + def shape(self): + """ + Returns the shape of the transfer function matrix, that is, ``(# of outputs, # of inputs)``. + + Examples + ======== + + >>> from sympy.abc import s, p + >>> from sympy.physics.control.lti import TransferFunction, TransferFunctionMatrix + >>> tf1 = TransferFunction(p**2 - 1, s**4 + s**3 - p, p) + >>> tf2 = TransferFunction(1 - p, p**2 - 3*p + 7, p) + >>> tf3 = TransferFunction(3, 4, p) + >>> tfm1 = TransferFunctionMatrix([[tf1, -tf2]]) + >>> tfm1.shape + (1, 2) + >>> tfm2 = TransferFunctionMatrix([[-tf2, tf3], [tf1, -tf1]]) + >>> tfm2.shape + (2, 2) + + """ + return self._expr_mat.shape + + def __neg__(self): + neg = -self._expr_mat + return _to_TFM(neg, self.var) + + @_check_other_MIMO + def __add__(self, other): + + if not isinstance(other, MIMOParallel): + return MIMOParallel(self, other) + other_arg_list = list(other.args) + return MIMOParallel(self, *other_arg_list) + + @_check_other_MIMO + def __sub__(self, other): + return self + (-other) + + @_check_other_MIMO + def __mul__(self, other): + + if not isinstance(other, MIMOSeries): + return MIMOSeries(other, self) + other_arg_list = list(other.args) + return MIMOSeries(*other_arg_list, self) + + def __getitem__(self, key): + trunc = self._expr_mat.__getitem__(key) + if isinstance(trunc, ImmutableMatrix): + return _to_TFM(trunc, self.var) + return TransferFunction.from_rational_expression(trunc, self.var) + + def transpose(self): + """Returns the transpose of the ``TransferFunctionMatrix`` (switched input and output layers).""" + transposed_mat = self._expr_mat.transpose() + return _to_TFM(transposed_mat, self.var) + + def elem_poles(self): + """ + Returns the poles of each element of the ``TransferFunctionMatrix``. + + .. note:: + Actual poles of a MIMO system are NOT the poles of individual elements. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction, TransferFunctionMatrix + >>> tf_1 = TransferFunction(3, (s + 1), s) + >>> tf_2 = TransferFunction(s + 6, (s + 1)*(s + 2), s) + >>> tf_3 = TransferFunction(s + 3, s**2 + 3*s + 2, s) + >>> tf_4 = TransferFunction(s + 2, s**2 + 5*s - 10, s) + >>> tfm_1 = TransferFunctionMatrix([[tf_1, tf_2], [tf_3, tf_4]]) + >>> tfm_1 + TransferFunctionMatrix(((TransferFunction(3, s + 1, s), TransferFunction(s + 6, (s + 1)*(s + 2), s)), (TransferFunction(s + 3, s**2 + 3*s + 2, s), TransferFunction(s + 2, s**2 + 5*s - 10, s)))) + >>> tfm_1.elem_poles() + [[[-1], [-2, -1]], [[-2, -1], [-5/2 + sqrt(65)/2, -sqrt(65)/2 - 5/2]]] + + See Also + ======== + + elem_zeros + + """ + return [[element.poles() for element in row] for row in self.doit().args[0]] + + def elem_zeros(self): + """ + Returns the zeros of each element of the ``TransferFunctionMatrix``. + + .. note:: + Actual zeros of a MIMO system are NOT the zeros of individual elements. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction, TransferFunctionMatrix + >>> tf_1 = TransferFunction(3, (s + 1), s) + >>> tf_2 = TransferFunction(s + 6, (s + 1)*(s + 2), s) + >>> tf_3 = TransferFunction(s + 3, s**2 + 3*s + 2, s) + >>> tf_4 = TransferFunction(s**2 - 9*s + 20, s**2 + 5*s - 10, s) + >>> tfm_1 = TransferFunctionMatrix([[tf_1, tf_2], [tf_3, tf_4]]) + >>> tfm_1 + TransferFunctionMatrix(((TransferFunction(3, s + 1, s), TransferFunction(s + 6, (s + 1)*(s + 2), s)), (TransferFunction(s + 3, s**2 + 3*s + 2, s), TransferFunction(s**2 - 9*s + 20, s**2 + 5*s - 10, s)))) + >>> tfm_1.elem_zeros() + [[[], [-6]], [[-3], [4, 5]]] + + See Also + ======== + + elem_poles + + """ + return [[element.zeros() for element in row] for row in self.doit().args[0]] + + def eval_frequency(self, other): + """ + Evaluates system response of each transfer function in the ``TransferFunctionMatrix`` at any point in the real or complex plane. + + Examples + ======== + + >>> from sympy.abc import s + >>> from sympy.physics.control.lti import TransferFunction, TransferFunctionMatrix + >>> from sympy import I + >>> tf_1 = TransferFunction(3, (s + 1), s) + >>> tf_2 = TransferFunction(s + 6, (s + 1)*(s + 2), s) + >>> tf_3 = TransferFunction(s + 3, s**2 + 3*s + 2, s) + >>> tf_4 = TransferFunction(s**2 - 9*s + 20, s**2 + 5*s - 10, s) + >>> tfm_1 = TransferFunctionMatrix([[tf_1, tf_2], [tf_3, tf_4]]) + >>> tfm_1 + TransferFunctionMatrix(((TransferFunction(3, s + 1, s), TransferFunction(s + 6, (s + 1)*(s + 2), s)), (TransferFunction(s + 3, s**2 + 3*s + 2, s), TransferFunction(s**2 - 9*s + 20, s**2 + 5*s - 10, s)))) + >>> tfm_1.eval_frequency(2) + Matrix([ + [ 1, 2/3], + [5/12, 3/2]]) + >>> tfm_1.eval_frequency(I*2) + Matrix([ + [ 3/5 - 6*I/5, -I], + [3/20 - 11*I/20, -101/74 + 23*I/74]]) + """ + mat = self._expr_mat.subs(self.var, other) + return mat.expand() + + def _flat(self): + """Returns flattened list of args in TransferFunctionMatrix""" + return [elem for tup in self.args[0] for elem in tup] + + def _eval_evalf(self, prec): + """Calls evalf() on each transfer function in the transfer function matrix""" + dps = prec_to_dps(prec) + mat = self._expr_mat.applyfunc(lambda a: a.evalf(n=dps)) + return _to_TFM(mat, self.var) + + def _eval_simplify(self, **kwargs): + """Simplifies the transfer function matrix""" + simp_mat = self._expr_mat.applyfunc(lambda a: cancel(a, expand=False)) + return _to_TFM(simp_mat, self.var) + + def expand(self, **hints): + """Expands the transfer function matrix""" + expand_mat = self._expr_mat.expand(**hints) + return _to_TFM(expand_mat, self.var) + +class StateSpace(LinearTimeInvariant): + r""" + State space model (ssm) of a linear, time invariant control system. + + Represents the standard state-space model with A, B, C, D as state-space matrices. + This makes the linear control system: + + (1) x'(t) = A * x(t) + B * u(t); x in R^n , u in R^k + (2) y(t) = C * x(t) + D * u(t); y in R^m + + where u(t) is any input signal, y(t) the corresponding output, and x(t) the system's state. + + Parameters + ========== + + A : Matrix + The State matrix of the state space model. + B : Matrix + The Input-to-State matrix of the state space model. + C : Matrix + The State-to-Output matrix of the state space model. + D : Matrix + The Feedthrough matrix of the state space model. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + + The easiest way to create a StateSpaceModel is via four matrices: + + >>> A = Matrix([[1, 2], [1, 0]]) + >>> B = Matrix([1, 1]) + >>> C = Matrix([[0, 1]]) + >>> D = Matrix([0]) + >>> StateSpace(A, B, C, D) + StateSpace(Matrix([ + [1, 2], + [1, 0]]), Matrix([ + [1], + [1]]), Matrix([[0, 1]]), Matrix([[0]])) + + One can use less matrices. The rest will be filled with a minimum of zeros: + + >>> StateSpace(A, B) + StateSpace(Matrix([ + [1, 2], + [1, 0]]), Matrix([ + [1], + [1]]), Matrix([[0, 0]]), Matrix([[0]])) + + See Also + ======== + + TransferFunction, TransferFunctionMatrix + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/State-space_representation + .. [2] https://in.mathworks.com/help/control/ref/ss.html + + """ + def __new__(cls, A=None, B=None, C=None, D=None): + if A is None: + A = zeros(1) + if B is None: + B = zeros(A.rows, 1) + if C is None: + C = zeros(1, A.cols) + if D is None: + D = zeros(C.rows, B.cols) + + A = _sympify(A) + B = _sympify(B) + C = _sympify(C) + D = _sympify(D) + + if (isinstance(A, ImmutableDenseMatrix) and isinstance(B, ImmutableDenseMatrix) and + isinstance(C, ImmutableDenseMatrix) and isinstance(D, ImmutableDenseMatrix)): + # Check State Matrix is square + if A.rows != A.cols: + raise ShapeError("Matrix A must be a square matrix.") + + # Check State and Input matrices have same rows + if A.rows != B.rows: + raise ShapeError("Matrices A and B must have the same number of rows.") + + # Check Output and Feedthrough matrices have same rows + if C.rows != D.rows: + raise ShapeError("Matrices C and D must have the same number of rows.") + + # Check State and Output matrices have same columns + if A.cols != C.cols: + raise ShapeError("Matrices A and C must have the same number of columns.") + + # Check Input and Feedthrough matrices have same columns + if B.cols != D.cols: + raise ShapeError("Matrices B and D must have the same number of columns.") + + obj = super(StateSpace, cls).__new__(cls, A, B, C, D) + obj._A = A + obj._B = B + obj._C = C + obj._D = D + + # Determine if the system is SISO or MIMO + num_outputs = D.rows + num_inputs = D.cols + if num_inputs == 1 and num_outputs == 1: + obj._is_SISO = True + obj._clstype = SISOLinearTimeInvariant + else: + obj._is_SISO = False + obj._clstype = MIMOLinearTimeInvariant + obj.is_StateSpace_object = True + return obj + + else: + raise TypeError("A, B, C and D inputs must all be sympy Matrices.") + + @property + def state_matrix(self): + """ + Returns the state matrix of the model. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[1, 2], [1, 0]]) + >>> B = Matrix([1, 1]) + >>> C = Matrix([[0, 1]]) + >>> D = Matrix([0]) + >>> ss = StateSpace(A, B, C, D) + >>> ss.state_matrix + Matrix([ + [1, 2], + [1, 0]]) + + """ + return self._A + + @property + def input_matrix(self): + """ + Returns the input matrix of the model. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[1, 2], [1, 0]]) + >>> B = Matrix([1, 1]) + >>> C = Matrix([[0, 1]]) + >>> D = Matrix([0]) + >>> ss = StateSpace(A, B, C, D) + >>> ss.input_matrix + Matrix([ + [1], + [1]]) + + """ + return self._B + + @property + def output_matrix(self): + """ + Returns the output matrix of the model. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[1, 2], [1, 0]]) + >>> B = Matrix([1, 1]) + >>> C = Matrix([[0, 1]]) + >>> D = Matrix([0]) + >>> ss = StateSpace(A, B, C, D) + >>> ss.output_matrix + Matrix([[0, 1]]) + + """ + return self._C + + @property + def feedforward_matrix(self): + """ + Returns the feedforward matrix of the model. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[1, 2], [1, 0]]) + >>> B = Matrix([1, 1]) + >>> C = Matrix([[0, 1]]) + >>> D = Matrix([0]) + >>> ss = StateSpace(A, B, C, D) + >>> ss.feedforward_matrix + Matrix([[0]]) + + """ + return self._D + + A = state_matrix + B = input_matrix + C = output_matrix + D = feedforward_matrix + + @property + def num_states(self): + """ + Returns the number of states of the model. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[1, 2], [1, 0]]) + >>> B = Matrix([1, 1]) + >>> C = Matrix([[0, 1]]) + >>> D = Matrix([0]) + >>> ss = StateSpace(A, B, C, D) + >>> ss.num_states + 2 + + """ + return self._A.rows + + @property + def num_inputs(self): + """ + Returns the number of inputs of the model. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[1, 2], [1, 0]]) + >>> B = Matrix([1, 1]) + >>> C = Matrix([[0, 1]]) + >>> D = Matrix([0]) + >>> ss = StateSpace(A, B, C, D) + >>> ss.num_inputs + 1 + + """ + return self._D.cols + + @property + def num_outputs(self): + """ + Returns the number of outputs of the model. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[1, 2], [1, 0]]) + >>> B = Matrix([1, 1]) + >>> C = Matrix([[0, 1]]) + >>> D = Matrix([0]) + >>> ss = StateSpace(A, B, C, D) + >>> ss.num_outputs + 1 + + """ + return self._D.rows + + + @property + def shape(self): + """Returns the shape of the equivalent StateSpace system.""" + return self.num_outputs, self.num_inputs + + def dsolve(self, initial_conditions=None, input_vector=None, var=Symbol('t')): + r""" + Returns `y(t)` or output of StateSpace given by the solution of equations: + x'(t) = A * x(t) + B * u(t) + y(t) = C * x(t) + D * u(t) + + Parameters + ============ + + initial_conditions : Matrix + The initial conditions of `x` state vector. If not provided, it defaults to a zero vector. + input_vector : Matrix + The input vector for state space. If not provided, it defaults to a zero vector. + var : Symbol + The symbol representing time. If not provided, it defaults to `t`. + + Examples + ========== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[-2, 0], [1, -1]]) + >>> B = Matrix([[1], [0]]) + >>> C = Matrix([[2, 1]]) + >>> ip = Matrix([5]) + >>> i = Matrix([0, 0]) + >>> ss = StateSpace(A, B, C) + >>> ss.dsolve(input_vector=ip, initial_conditions=i).simplify() + Matrix([[15/2 - 5*exp(-t) - 5*exp(-2*t)/2]]) + + If no input is provided it defaults to solving the system with zero initial conditions and zero input. + + >>> ss.dsolve() + Matrix([[0]]) + + References + ========== + .. [1] https://web.mit.edu/2.14/www/Handouts/StateSpaceResponse.pdf + .. [2] https://docs.sympy.org/latest/modules/solvers/ode.html#sympy.solvers.ode.systems.linodesolve + + """ + + if not isinstance(var, Symbol): + raise ValueError("Variable for representing time must be a Symbol.") + if not initial_conditions: + initial_conditions = zeros(self._A.shape[0], 1) + elif initial_conditions.shape != (self._A.shape[0], 1): + raise ShapeError("Initial condition vector should have the same number of " + "rows as the state matrix.") + if not input_vector: + input_vector = zeros(self._B.shape[1], 1) + elif input_vector.shape != (self._B.shape[1], 1): + raise ShapeError("Input vector should have the same number of " + "columns as the input matrix.") + sol = linodesolve(A=self._A, t=var, b=self._B*input_vector, type='type2', doit=True) + mat1 = Matrix(sol) + mat2 = mat1.replace(var, 0) + free1 = self._A.free_symbols | self._B.free_symbols | input_vector.free_symbols + free2 = mat2.free_symbols + # Get all the free symbols form the matrix + dummy_symbols = list(free2-free1) + # Convert the matrix to a Coefficient matrix + r1, r2 = linear_eq_to_matrix(mat2, dummy_symbols) + s = linsolve((r1, initial_conditions+r2)) + res_tuple = next(iter(s)) + for ind, v in enumerate(res_tuple): + mat1 = mat1.replace(dummy_symbols[ind], v) + res = self._C*mat1 + self._D*input_vector + return res + + def _eval_evalf(self, prec): + """ + Returns state space model where numerical expressions are evaluated into floating point numbers. + """ + dps = prec_to_dps(prec) + return StateSpace( + self._A.evalf(n = dps), + self._B.evalf(n = dps), + self._C.evalf(n = dps), + self._D.evalf(n = dps)) + + def _eval_rewrite_as_TransferFunction(self, *args): + """ + Returns the equivalent Transfer Function of the state space model. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import TransferFunction, StateSpace + >>> A = Matrix([[-5, -1], [3, -1]]) + >>> B = Matrix([2, 5]) + >>> C = Matrix([[1, 2]]) + >>> D = Matrix([0]) + >>> ss = StateSpace(A, B, C, D) + >>> ss.rewrite(TransferFunction) + [[TransferFunction(12*s + 59, s**2 + 6*s + 8, s)]] + + """ + s = Symbol('s') + n = self._A.shape[0] + I = eye(n) + G = self._C*(s*I - self._A).solve(self._B) + self._D + G = G.simplify() + to_tf = lambda expr: TransferFunction.from_rational_expression(expr, s) + tf_mat = [[to_tf(expr) for expr in sublist] for sublist in G.tolist()] + return tf_mat + + def __add__(self, other): + """ + Add two State Space systems (parallel connection). + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A1 = Matrix([[1]]) + >>> B1 = Matrix([[2]]) + >>> C1 = Matrix([[-1]]) + >>> D1 = Matrix([[-2]]) + >>> A2 = Matrix([[-1]]) + >>> B2 = Matrix([[-2]]) + >>> C2 = Matrix([[1]]) + >>> D2 = Matrix([[2]]) + >>> ss1 = StateSpace(A1, B1, C1, D1) + >>> ss2 = StateSpace(A2, B2, C2, D2) + >>> ss1 + ss2 + StateSpace(Matrix([ + [1, 0], + [0, -1]]), Matrix([ + [ 2], + [-2]]), Matrix([[-1, 1]]), Matrix([[0]])) + + """ + # Check for scalars + if isinstance(other, (int, float, complex, Symbol)): + A = self._A + B = self._B + C = self._C + D = self._D.applyfunc(lambda element: element + other) + + else: + # Check nature of system + if not isinstance(other, StateSpace): + raise ValueError("Addition is only supported for 2 State Space models.") + # Check dimensions of system + elif ((self.num_inputs != other.num_inputs) or (self.num_outputs != other.num_outputs)): + raise ShapeError("Systems with incompatible inputs and outputs cannot be added.") + + m1 = (self._A).row_join(zeros(self._A.shape[0], other._A.shape[-1])) + m2 = zeros(other._A.shape[0], self._A.shape[-1]).row_join(other._A) + + A = m1.col_join(m2) + B = self._B.col_join(other._B) + C = self._C.row_join(other._C) + D = self._D + other._D + + return StateSpace(A, B, C, D) + + def __radd__(self, other): + """ + Right add two State Space systems. + + Examples + ======== + + >>> from sympy.physics.control import StateSpace + >>> s = StateSpace() + >>> 5 + s + StateSpace(Matrix([[0]]), Matrix([[0]]), Matrix([[0]]), Matrix([[5]])) + + """ + return self + other + + def __sub__(self, other): + """ + Subtract two State Space systems. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A1 = Matrix([[1]]) + >>> B1 = Matrix([[2]]) + >>> C1 = Matrix([[-1]]) + >>> D1 = Matrix([[-2]]) + >>> A2 = Matrix([[-1]]) + >>> B2 = Matrix([[-2]]) + >>> C2 = Matrix([[1]]) + >>> D2 = Matrix([[2]]) + >>> ss1 = StateSpace(A1, B1, C1, D1) + >>> ss2 = StateSpace(A2, B2, C2, D2) + >>> ss1 - ss2 + StateSpace(Matrix([ + [1, 0], + [0, -1]]), Matrix([ + [ 2], + [-2]]), Matrix([[-1, -1]]), Matrix([[-4]])) + + """ + return self + (-other) + + def __rsub__(self, other): + """ + Right subtract two tate Space systems. + + Examples + ======== + + >>> from sympy.physics.control import StateSpace + >>> s = StateSpace() + >>> 5 - s + StateSpace(Matrix([[0]]), Matrix([[0]]), Matrix([[0]]), Matrix([[5]])) + + """ + return other + (-self) + + def __neg__(self): + """ + Returns the negation of the state space model. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[-5, -1], [3, -1]]) + >>> B = Matrix([2, 5]) + >>> C = Matrix([[1, 2]]) + >>> D = Matrix([0]) + >>> ss = StateSpace(A, B, C, D) + >>> -ss + StateSpace(Matrix([ + [-5, -1], + [ 3, -1]]), Matrix([ + [2], + [5]]), Matrix([[-1, -2]]), Matrix([[0]])) + + """ + return StateSpace(self._A, self._B, -self._C, -self._D) + + def __mul__(self, other): + """ + Multiplication of two State Space systems (serial connection). + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[-5, -1], [3, -1]]) + >>> B = Matrix([2, 5]) + >>> C = Matrix([[1, 2]]) + >>> D = Matrix([0]) + >>> ss = StateSpace(A, B, C, D) + >>> ss*5 + StateSpace(Matrix([ + [-5, -1], + [ 3, -1]]), Matrix([ + [2], + [5]]), Matrix([[5, 10]]), Matrix([[0]])) + + """ + # Check for scalars + if isinstance(other, (int, float, complex, Symbol)): + A = self._A + B = self._B + C = self._C.applyfunc(lambda element: element*other) + D = self._D.applyfunc(lambda element: element*other) + + else: + # Check nature of system + if not isinstance(other, StateSpace): + raise ValueError("Multiplication is only supported for 2 State Space models.") + # Check dimensions of system + elif self.num_inputs != other.num_outputs: + raise ShapeError("Systems with incompatible inputs and outputs cannot be multiplied.") + + m1 = (other._A).row_join(zeros(other._A.shape[0], self._A.shape[1])) + m2 = (self._B * other._C).row_join(self._A) + + A = m1.col_join(m2) + B = (other._B).col_join(self._B * other._D) + C = (self._D * other._C).row_join(self._C) + D = self._D * other._D + + return StateSpace(A, B, C, D) + + def __rmul__(self, other): + """ + Right multiply two tate Space systems. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[-5, -1], [3, -1]]) + >>> B = Matrix([2, 5]) + >>> C = Matrix([[1, 2]]) + >>> D = Matrix([0]) + >>> ss = StateSpace(A, B, C, D) + >>> 5*ss + StateSpace(Matrix([ + [-5, -1], + [ 3, -1]]), Matrix([ + [10], + [25]]), Matrix([[1, 2]]), Matrix([[0]])) + + """ + if isinstance(other, (int, float, complex, Symbol)): + A = self._A + C = self._C + B = self._B.applyfunc(lambda element: element*other) + D = self._D.applyfunc(lambda element: element*other) + return StateSpace(A, B, C, D) + else: + return self*other + + def __repr__(self): + A_str = self._A.__repr__() + B_str = self._B.__repr__() + C_str = self._C.__repr__() + D_str = self._D.__repr__() + + return f"StateSpace(\n{A_str},\n\n{B_str},\n\n{C_str},\n\n{D_str})" + + + def append(self, other): + """ + Returns the first model appended with the second model. The order is preserved. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A1 = Matrix([[1]]) + >>> B1 = Matrix([[2]]) + >>> C1 = Matrix([[-1]]) + >>> D1 = Matrix([[-2]]) + >>> A2 = Matrix([[-1]]) + >>> B2 = Matrix([[-2]]) + >>> C2 = Matrix([[1]]) + >>> D2 = Matrix([[2]]) + >>> ss1 = StateSpace(A1, B1, C1, D1) + >>> ss2 = StateSpace(A2, B2, C2, D2) + >>> ss1.append(ss2) + StateSpace(Matrix([ + [1, 0], + [0, -1]]), Matrix([ + [2, 0], + [0, -2]]), Matrix([ + [-1, 0], + [ 0, 1]]), Matrix([ + [-2, 0], + [ 0, 2]])) + + """ + n = self.num_states + other.num_states + m = self.num_inputs + other.num_inputs + p = self.num_outputs + other.num_outputs + + A = zeros(n, n) + B = zeros(n, m) + C = zeros(p, n) + D = zeros(p, m) + + A[:self.num_states, :self.num_states] = self._A + A[self.num_states:, self.num_states:] = other._A + B[:self.num_states, :self.num_inputs] = self._B + B[self.num_states:, self.num_inputs:] = other._B + C[:self.num_outputs, :self.num_states] = self._C + C[self.num_outputs:, self.num_states:] = other._C + D[:self.num_outputs, :self.num_inputs] = self._D + D[self.num_outputs:, self.num_inputs:] = other._D + return StateSpace(A, B, C, D) + + def observability_matrix(self): + """ + Returns the observability matrix of the state space model: + [C, C * A^1, C * A^2, .. , C * A^(n-1)]; A in R^(n x n), C in R^(m x k) + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[-1.5, -2], [1, 0]]) + >>> B = Matrix([0.5, 0]) + >>> C = Matrix([[0, 1]]) + >>> D = Matrix([1]) + >>> ss = StateSpace(A, B, C, D) + >>> ob = ss.observability_matrix() + >>> ob + Matrix([ + [0, 1], + [1, 0]]) + + References + ========== + .. [1] https://in.mathworks.com/help/control/ref/statespacemodel.obsv.html + + """ + n = self.num_states + ob = self._C + for i in range(1,n): + ob = ob.col_join(self._C * self._A**i) + + return ob + + def observable_subspace(self): + """ + Returns the observable subspace of the state space model. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[-1.5, -2], [1, 0]]) + >>> B = Matrix([0.5, 0]) + >>> C = Matrix([[0, 1]]) + >>> D = Matrix([1]) + >>> ss = StateSpace(A, B, C, D) + >>> ob_subspace = ss.observable_subspace() + >>> ob_subspace + [Matrix([ + [0], + [1]]), Matrix([ + [1], + [0]])] + + """ + return self.observability_matrix().columnspace() + + def is_observable(self): + """ + Returns if the state space model is observable. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[-1.5, -2], [1, 0]]) + >>> B = Matrix([0.5, 0]) + >>> C = Matrix([[0, 1]]) + >>> D = Matrix([1]) + >>> ss = StateSpace(A, B, C, D) + >>> ss.is_observable() + True + + """ + return self.observability_matrix().rank() == self.num_states + + def controllability_matrix(self): + """ + Returns the controllability matrix of the system: + [B, A * B, A^2 * B, .. , A^(n-1) * B]; A in R^(n x n), B in R^(n x m) + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[-1.5, -2], [1, 0]]) + >>> B = Matrix([0.5, 0]) + >>> C = Matrix([[0, 1]]) + >>> D = Matrix([1]) + >>> ss = StateSpace(A, B, C, D) + >>> ss.controllability_matrix() + Matrix([ + [0.5, -0.75], + [ 0, 0.5]]) + + References + ========== + .. [1] https://in.mathworks.com/help/control/ref/statespacemodel.ctrb.html + + """ + co = self._B + n = self._A.shape[0] + for i in range(1, n): + co = co.row_join(((self._A)**i) * self._B) + + return co + + def controllable_subspace(self): + """ + Returns the controllable subspace of the state space model. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[-1.5, -2], [1, 0]]) + >>> B = Matrix([0.5, 0]) + >>> C = Matrix([[0, 1]]) + >>> D = Matrix([1]) + >>> ss = StateSpace(A, B, C, D) + >>> co_subspace = ss.controllable_subspace() + >>> co_subspace + [Matrix([ + [0.5], + [ 0]]), Matrix([ + [-0.75], + [ 0.5]])] + + """ + return self.controllability_matrix().columnspace() + + def is_controllable(self): + """ + Returns if the state space model is controllable. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.physics.control import StateSpace + >>> A = Matrix([[-1.5, -2], [1, 0]]) + >>> B = Matrix([0.5, 0]) + >>> C = Matrix([[0, 1]]) + >>> D = Matrix([1]) + >>> ss = StateSpace(A, B, C, D) + >>> ss.is_controllable() + True + + """ + return self.controllability_matrix().rank() == self.num_states diff --git a/venv/lib/python3.11/site-packages/sympy/physics/control/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/control/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/control/tests/test_control_plots.py b/venv/lib/python3.11/site-packages/sympy/physics/control/tests/test_control_plots.py new file mode 100644 index 0000000000000000000000000000000000000000..05836c806f93c4a8ff375efe2b8bd5f993db7502 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/control/tests/test_control_plots.py @@ -0,0 +1,332 @@ +from math import isclose +from sympy.core.numbers import I, all_close +from sympy.core.symbol import Dummy +from sympy.functions.elementary.complexes import (Abs, arg) +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.abc import s, p, a +from sympy import pi +from sympy.external import import_module +from sympy.physics.control.control_plots import \ + (pole_zero_numerical_data, pole_zero_plot, step_response_numerical_data, + step_response_plot, impulse_response_numerical_data, + impulse_response_plot, ramp_response_numerical_data, + ramp_response_plot, bode_magnitude_numerical_data, + bode_phase_numerical_data, bode_plot, nyquist_plot_expr, + nichols_plot_expr) + +from sympy.physics.control.lti import (TransferFunction, + Series, Parallel, TransferFunctionMatrix) +from sympy.testing.pytest import raises, skip + +matplotlib = import_module( + 'matplotlib', import_kwargs={'fromlist': ['pyplot']}, + catch=(RuntimeError,)) + +numpy = import_module('numpy') + +tf1 = TransferFunction(1, p**2 + 0.5*p + 2, p) +tf2 = TransferFunction(p, 6*p**2 + 3*p + 1, p) +tf3 = TransferFunction(p, p**3 - 1, p) +tf4 = TransferFunction(10, p**3, p) +tf5 = TransferFunction(5, s**2 + 2*s + 10, s) +tf6 = TransferFunction(1, 1, s) +tf7 = TransferFunction(4*s*3 + 9*s**2 + 0.1*s + 11, 8*s**6 + 9*s**4 + 11, s) +tf8 = TransferFunction(5, s**2 + (2+I)*s + 10, s) + +ser1 = Series(tf4, TransferFunction(1, p - 5, p)) +ser2 = Series(tf3, TransferFunction(p, p + 2, p)) + +par1 = Parallel(tf1, tf2) + + +def _to_tuple(a, b): + return tuple(a), tuple(b) + +def _trim_tuple(a, b): + a, b = _to_tuple(a, b) + return tuple(a[0: 2] + a[len(a)//2 : len(a)//2 + 1] + a[-2:]), \ + tuple(b[0: 2] + b[len(b)//2 : len(b)//2 + 1] + b[-2:]) + +def y_coordinate_equality(plot_data_func, evalf_func, system): + """Checks whether the y-coordinate value of the plotted + data point is equal to the value of the function at a + particular x.""" + x, y = plot_data_func(system) + x, y = _trim_tuple(x, y) + y_exp = tuple(evalf_func(system, x_i) for x_i in x) + return all(Abs(y_exp_i - y_i) < 1e-8 for y_exp_i, y_i in zip(y_exp, y)) + + +def test_errors(): + if not matplotlib: + skip("Matplotlib not the default backend") + + # Invalid `system` check + tfm = TransferFunctionMatrix([[tf6, tf5], [tf5, tf6]]) + expr = 1/(s**2 - 1) + raises(NotImplementedError, lambda: pole_zero_plot(tfm)) + raises(NotImplementedError, lambda: pole_zero_numerical_data(expr)) + raises(NotImplementedError, lambda: impulse_response_plot(expr)) + raises(NotImplementedError, lambda: impulse_response_numerical_data(tfm)) + raises(NotImplementedError, lambda: step_response_plot(tfm)) + raises(NotImplementedError, lambda: step_response_numerical_data(expr)) + raises(NotImplementedError, lambda: ramp_response_plot(expr)) + raises(NotImplementedError, lambda: ramp_response_numerical_data(tfm)) + raises(NotImplementedError, lambda: bode_plot(tfm)) + + # More than 1 variables + tf_a = TransferFunction(a, s + 1, s) + raises(ValueError, lambda: pole_zero_plot(tf_a)) + raises(ValueError, lambda: pole_zero_numerical_data(tf_a)) + raises(ValueError, lambda: impulse_response_plot(tf_a)) + raises(ValueError, lambda: impulse_response_numerical_data(tf_a)) + raises(ValueError, lambda: step_response_plot(tf_a)) + raises(ValueError, lambda: step_response_numerical_data(tf_a)) + raises(ValueError, lambda: ramp_response_plot(tf_a)) + raises(ValueError, lambda: ramp_response_numerical_data(tf_a)) + raises(ValueError, lambda: bode_plot(tf_a)) + + # lower_limit > 0 for response plots + raises(ValueError, lambda: impulse_response_plot(tf1, lower_limit=-1)) + raises(ValueError, lambda: step_response_plot(tf1, lower_limit=-0.1)) + raises(ValueError, lambda: ramp_response_plot(tf1, lower_limit=-4/3)) + + # slope in ramp_response_plot() is negative + raises(ValueError, lambda: ramp_response_plot(tf1, slope=-0.1)) + + # incorrect frequency or phase unit + raises(ValueError, lambda: bode_plot(tf1,freq_unit = 'hz')) + raises(ValueError, lambda: bode_plot(tf1,phase_unit = 'degree')) + + +def test_pole_zero(): + + def pz_tester(sys, expected_value): + _z, _p = pole_zero_numerical_data(sys) + z_check = all_close(_z, expected_value[0]) + p_check = all_close(_p, expected_value[1]) + return p_check and z_check + + exp1 = [[], [-0.24999999999999994-1.3919410907075054j, -0.24999999999999994+1.3919410907075054j]] + exp2 = [[0.0], [-0.25-0.3227486121839514j, -0.25+0.3227486121839514j]] + exp3 = [[0.0], [0.9999999999999998+0j, -0.5000000000000004-0.8660254037844395j, + -0.5000000000000004+0.8660254037844395j]] + exp4 = [[], [0.0, 0.0, 0.0, 5.0]] + exp5 = [[-5.645751311064592, -0.5000000000000008, -0.3542486889354093], + [-0.24999999999999986-0.322748612183951348j, + -0.2499999999999998+0.32274861218395134j, + -0.24999999999999986-1.3919410907075052j, + -0.2499999999999998+1.3919410907075052j]] + exp6 = [[], [-1.1641600331447917-3.545808351896439j, + -0.8358399668552097+2.5458083518964383j]] + + assert pz_tester(tf1, exp1) + assert pz_tester(tf2, exp2) + assert pz_tester(tf3, exp3) + assert pz_tester(ser1, exp4) + assert pz_tester(par1, exp5) + assert pz_tester(tf8, exp6) + + +def test_bode(): + if not numpy: + skip("NumPy is required for this test") + + def bode_phase_evalf(system, point): + expr = system.to_expr() + _w = Dummy("w", real=True) + w_expr = expr.subs({system.var: I*_w}) + return arg(w_expr).subs({_w: point}).evalf() + + def bode_mag_evalf(system, point): + expr = system.to_expr() + _w = Dummy("w", real=True) + w_expr = expr.subs({system.var: I*_w}) + return 20*log(Abs(w_expr), 10).subs({_w: point}).evalf() + + def test_bode_data(sys): + return y_coordinate_equality(bode_magnitude_numerical_data, bode_mag_evalf, sys) \ + and y_coordinate_equality(bode_phase_numerical_data, bode_phase_evalf, sys) + + assert test_bode_data(tf1) + assert test_bode_data(tf2) + assert test_bode_data(tf3) + assert test_bode_data(tf4) + assert test_bode_data(tf5) + + +def check_point_accuracy(a, b): + return all(isclose(*_, rel_tol=1e-1, abs_tol=1e-6 + ) for _ in zip(a, b)) + + +def test_impulse_response(): + if not numpy: + skip("NumPy is required for this test") + + def impulse_res_tester(sys, expected_value): + x, y = _to_tuple(*impulse_response_numerical_data(sys, + adaptive=False, n=10)) + x_check = check_point_accuracy(x, expected_value[0]) + y_check = check_point_accuracy(y, expected_value[1]) + return x_check and y_check + + exp1 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, + 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), + (0.0, 0.544019738507865, 0.01993849743234938, -0.31140243360893216, -0.022852779906491996, 0.1778306498155759, + 0.01962941084328499, -0.1013115194573652, -0.014975541213105696, 0.0575789724730714)) + exp2 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555, + 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.1666666675, 0.08389223412935855, + 0.02338051973475047, -0.014966807776379383, -0.034645954223054234, -0.040560075735512804, + -0.037658628907103885, -0.030149507719590022, -0.021162090730736834, -0.012721292737437523)) + exp3 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555, + 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (4.369893391586999e-09, 1.1750333000630964, + 3.2922404058312473, 9.432290008148343, 28.37098083007151, 86.18577464367974, 261.90356653762115, + 795.6538758627842, 2416.9920942096983, 7342.159505206647)) + exp4 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555, + 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, 6.17283950617284, 24.69135802469136, + 55.555555555555564, 98.76543209876544, 154.320987654321, 222.22222222222226, 302.46913580246917, + 395.0617283950618, 500.0)) + exp5 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555, + 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, -0.10455606138085417, + 0.06757671513476461, -0.03234567568833768, 0.013582514927757873, -0.005273419510705473, + 0.0019364083003354075, -0.000680070134067832, 0.00022969845960406913, -7.476094359583917e-05)) + exp6 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, + 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), + (-6.016699583000218e-09, 0.35039802056107394, 3.3728423827689884, 12.119846079276684, + 25.86101014293389, 29.352480635282088, -30.49475907497664, -273.8717189554019, -863.2381702029659, + -1747.0262164682233)) + exp7 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, + 4.444444444444445, 5.555555555555555, 6.666666666666667, 7.777777777777779, + 8.88888888888889, 10.0), (0.0, 18.934638095560974, 5346.93244680907, 1384609.8718249386, + 358161126.65801865, 92645770015.70108, 23964739753087.42, 6198974342083139.0, 1.603492601616059e+18, + 4.147764422869658e+20)) + + assert impulse_res_tester(tf1, exp1) + assert impulse_res_tester(tf2, exp2) + assert impulse_res_tester(tf3, exp3) + assert impulse_res_tester(tf4, exp4) + assert impulse_res_tester(tf5, exp5) + assert impulse_res_tester(tf7, exp6) + assert impulse_res_tester(ser1, exp7) + + +def test_step_response(): + if not numpy: + skip("NumPy is required for this test") + + def step_res_tester(sys, expected_value): + x, y = _to_tuple(*step_response_numerical_data(sys, + adaptive=False, n=10)) + x_check = check_point_accuracy(x, expected_value[0]) + y_check = check_point_accuracy(y, expected_value[1]) + return x_check and y_check + + exp1 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, + 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), + (-1.9193285738516863e-08, 0.42283495488246126, 0.7840485977945262, 0.5546841805655717, + 0.33903033806932087, 0.4627251747410237, 0.5909907598988051, 0.5247213989553071, + 0.4486997874319281, 0.4839358435839171)) + exp2 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, + 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), + (0.0, 0.13728409095645816, 0.19474559355325086, 0.1974909129243011, 0.16841657696573073, + 0.12559777736159378, 0.08153828016664713, 0.04360471317348958, 0.015072994568868221, + -0.003636420058445484)) + exp3 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, + 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), + (0.0, 0.6314542141914303, 2.9356520038101035, 9.37731009663807, 28.452300356688376, + 86.25721933273988, 261.9236645044672, 795.6435410577224, 2416.9786984578764, 7342.154119725917)) + exp4 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, + 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), + (0.0, 2.286236899862826, 18.28989519890261, 61.72839629629631, 146.31916159122088, 285.7796124828532, + 493.8271703703705, 784.1792566529494, 1170.553292729767, 1666.6667)) + exp5 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, + 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), + (-3.999999997894577e-09, 0.6720357068882895, 0.4429938256137113, 0.5182010838004518, + 0.4944139147159695, 0.5016379853883338, 0.4995466896527733, 0.5001154784851325, + 0.49997448824584123, 0.5000039745919259)) + exp6 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, + 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), + (-1.5433688493882158e-09, 0.3428705539937336, 1.1253619102202777, 3.1849962651016517, + 9.47532757182671, 28.727231099148135, 87.29426924860557, 265.2138681048606, 805.6636260007757, + 2447.387582370878)) + + assert step_res_tester(tf1, exp1) + assert step_res_tester(tf2, exp2) + assert step_res_tester(tf3, exp3) + assert step_res_tester(tf4, exp4) + assert step_res_tester(tf5, exp5) + assert step_res_tester(ser2, exp6) + + +def test_ramp_response(): + if not numpy: + skip("NumPy is required for this test") + + def ramp_res_tester(sys, num_points, expected_value, slope=1): + x, y = _to_tuple(*ramp_response_numerical_data(sys, + slope=slope, adaptive=False, n=num_points)) + x_check = check_point_accuracy(x, expected_value[0]) + y_check = check_point_accuracy(y, expected_value[1]) + return x_check and y_check + + exp1 = ((0.0, 2.0, 4.0, 6.0, 8.0, 10.0), (0.0, 0.7324667795033895, 1.9909720978650398, + 2.7956587704217783, 3.9224897567931514, 4.85022655284895)) + exp2 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, + 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), + (2.4360213402019326e-08, 0.10175320182493253, 0.33057612497658406, 0.5967937263298935, + 0.8431511866718248, 1.0398805391471613, 1.1776043125035738, 1.2600994825747305, 1.2981042689274653, + 1.304684417610106)) + exp3 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555, + 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (-3.9329040468771836e-08, + 0.34686634635794555, 2.9998828170537903, 12.33303690737476, 40.993913948137795, 127.84145222317912, + 391.41713691996, 1192.0006858708389, 3623.9808672503405, 11011.728034546572)) + exp4 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555, + 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, 1.9051973784484078, 30.483158055174524, + 154.32098765432104, 487.7305288827924, 1190.7483615302544, 2469.1358024691367, 4574.3789056546275, + 7803.688462124678, 12500.0)) + exp5 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555, + 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, 3.8844361856975635, 9.141792069209865, + 14.096349157657231, 19.09783068994694, 24.10179770390321, 29.09907319114121, 34.10040420185154, + 39.09983919254265, 44.10006013058409)) + exp6 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555, + 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, 1.1111111111111112, 2.2222222222222223, + 3.3333333333333335, 4.444444444444445, 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0)) + + assert ramp_res_tester(tf1, 6, exp1) + assert ramp_res_tester(tf2, 10, exp2, 1.2) + assert ramp_res_tester(tf3, 10, exp3, 1.5) + assert ramp_res_tester(tf4, 10, exp4, 3) + assert ramp_res_tester(tf5, 10, exp5, 9) + assert ramp_res_tester(tf6, 10, exp6) + + +def test_nyquist_plot_expr(): + r1, i1, w1 = nyquist_plot_expr(tf1) + r2, i2, w2 = nyquist_plot_expr(tf2) + r3, i3, w3 = nyquist_plot_expr(tf3) + r4, i4, w4 = nyquist_plot_expr(tf4) + assert r1 == (2 - w1**2)/(0.25*w1**2 + (2 - w1**2)**2) + assert i1 == -0.5*w1/(0.25*w1**2 + (2 - w1**2)**2) + assert r2 == 3*w2**2/(9*w2**2 + (1 - 6*w2**2)**2) + assert i2 == w2*(1 - 6*w2**2)/(9*w2**2 + (1 - 6*w2**2)**2) + assert r3 == -w3**4/(w3**6 + 1) + assert i3 == -w3/(w3**6 + 1) + assert r4 == 0 + assert i4 == 10/w4**3 + + +def test_nichols_expr(): + m1, p1, w1 = nichols_plot_expr(tf1) + m2, p2, w2 = nichols_plot_expr(tf2) + m3, p3, w3 = nichols_plot_expr(tf3) + m4, p4, w4 = nichols_plot_expr(tf4) + assert m1 == 20*log(1/sqrt(w1**4 - 3.75*w1**2 + 4))/log(10) + assert p1 == 180*arg(1/(-w1**2 + 0.5*w1*I + 2))/pi + assert m2 == 20*log(Abs(w2)/sqrt(36*w2**4 - 3*w2**2 + 1))/log(10) + assert p2 == 180*arg(w2*I/(-6*w2**2 + 3*w2*I + 1))/pi + assert m3 == 20*log(Abs(w3)/sqrt(w3**6 + 1))/log(10) + assert p3 == 180*arg(-w3*I/(w3**3*I + 1))/pi + assert m4 == 20*log(10/(w4**2*Abs(w4)))/log(10) + assert p4 == 180*arg(I/w4**3)/pi diff --git a/venv/lib/python3.11/site-packages/sympy/physics/control/tests/test_lti.py b/venv/lib/python3.11/site-packages/sympy/physics/control/tests/test_lti.py new file mode 100644 index 0000000000000000000000000000000000000000..a78a4c9b893d11f5e9e94705637080e2a722796a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/control/tests/test_lti.py @@ -0,0 +1,2273 @@ +from sympy.core.add import Add +from sympy.core.function import Function +from sympy.core.mul import Mul +from sympy.core.numbers import (I, pi, Rational, oo) +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.special.delta_functions import Heaviside +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import atan +from sympy.matrices.dense import eye +from sympy.physics.control.lti import SISOLinearTimeInvariant +from sympy.polys.polytools import factor +from sympy.polys.rootoftools import CRootOf +from sympy.simplify.simplify import simplify +from sympy.core.containers import Tuple +from sympy.matrices import ImmutableMatrix, Matrix, ShapeError +from sympy.functions.elementary.trigonometric import sin, cos +from sympy.physics.control import (TransferFunction, PIDController, Series, Parallel, + Feedback, TransferFunctionMatrix, MIMOSeries, MIMOParallel, MIMOFeedback, + StateSpace, gbt, bilinear, forward_diff, backward_diff, phase_margin, gain_margin) +from sympy.testing.pytest import raises + +a, x, b, c, s, g, d, p, k, tau, zeta, wn, T = symbols('a, x, b, c, s, g, d, p, k,\ + tau, zeta, wn, T') +a0, a1, a2, a3, b0, b1, b2, b3, c0, c1, c2, c3, d0, d1, d2, d3 = symbols('a0:4,\ + b0:4, c0:4, d0:4') +TF1 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s) +TF2 = TransferFunction(k, 1, s) +TF3 = TransferFunction(a2*p - s, a2*s + p, s) + + +def test_TransferFunction_construction(): + tf = TransferFunction(s + 1, s**2 + s + 1, s) + assert tf.num == (s + 1) + assert tf.den == (s**2 + s + 1) + assert tf.args == (s + 1, s**2 + s + 1, s) + + tf1 = TransferFunction(s + 4, s - 5, s) + assert tf1.num == (s + 4) + assert tf1.den == (s - 5) + assert tf1.args == (s + 4, s - 5, s) + + # using different polynomial variables. + tf2 = TransferFunction(p + 3, p**2 - 9, p) + assert tf2.num == (p + 3) + assert tf2.den == (p**2 - 9) + assert tf2.args == (p + 3, p**2 - 9, p) + + tf3 = TransferFunction(p**3 + 5*p**2 + 4, p**4 + 3*p + 1, p) + assert tf3.args == (p**3 + 5*p**2 + 4, p**4 + 3*p + 1, p) + + # no pole-zero cancellation on its own. + tf4 = TransferFunction((s + 3)*(s - 1), (s - 1)*(s + 5), s) + assert tf4.den == (s - 1)*(s + 5) + assert tf4.args == ((s + 3)*(s - 1), (s - 1)*(s + 5), s) + + tf4_ = TransferFunction(p + 2, p + 2, p) + assert tf4_.args == (p + 2, p + 2, p) + + tf5 = TransferFunction(s - 1, 4 - p, s) + assert tf5.args == (s - 1, 4 - p, s) + + tf5_ = TransferFunction(s - 1, s - 1, s) + assert tf5_.args == (s - 1, s - 1, s) + + tf6 = TransferFunction(5, 6, s) + assert tf6.num == 5 + assert tf6.den == 6 + assert tf6.args == (5, 6, s) + + tf6_ = TransferFunction(1/2, 4, s) + assert tf6_.num == 0.5 + assert tf6_.den == 4 + assert tf6_.args == (0.500000000000000, 4, s) + + tf7 = TransferFunction(3*s**2 + 2*p + 4*s, 8*p**2 + 7*s, s) + tf8 = TransferFunction(3*s**2 + 2*p + 4*s, 8*p**2 + 7*s, p) + assert not tf7 == tf8 + + tf7_ = TransferFunction(a0*s + a1*s**2 + a2*s**3, b0*p - b1*s, s) + tf8_ = TransferFunction(a0*s + a1*s**2 + a2*s**3, b0*p - b1*s, s) + assert tf7_ == tf8_ + assert -(-tf7_) == tf7_ == -(-(-(-tf7_))) + + tf9 = TransferFunction(a*s**3 + b*s**2 + g*s + d, d*p + g*p**2 + g*s, s) + assert tf9.args == (a*s**3 + b*s**2 + d + g*s, d*p + g*p**2 + g*s, s) + + tf10 = TransferFunction(p**3 + d, g*s**2 + d*s + a, p) + tf10_ = TransferFunction(p**3 + d, g*s**2 + d*s + a, p) + assert tf10.args == (d + p**3, a + d*s + g*s**2, p) + assert tf10_ == tf10 + + tf11 = TransferFunction(a1*s + a0, b2*s**2 + b1*s + b0, s) + assert tf11.num == (a0 + a1*s) + assert tf11.den == (b0 + b1*s + b2*s**2) + assert tf11.args == (a0 + a1*s, b0 + b1*s + b2*s**2, s) + + # when just the numerator is 0, leave the denominator alone. + tf12 = TransferFunction(0, p**2 - p + 1, p) + assert tf12.args == (0, p**2 - p + 1, p) + + tf13 = TransferFunction(0, 1, s) + assert tf13.args == (0, 1, s) + + # float exponents + tf14 = TransferFunction(a0*s**0.5 + a2*s**0.6 - a1, a1*p**(-8.7), s) + assert tf14.args == (a0*s**0.5 - a1 + a2*s**0.6, a1*p**(-8.7), s) + + tf15 = TransferFunction(a2**2*p**(1/4) + a1*s**(-4/5), a0*s - p, p) + assert tf15.args == (a1*s**(-0.8) + a2**2*p**0.25, a0*s - p, p) + + omega_o, k_p, k_o, k_i = symbols('omega_o, k_p, k_o, k_i') + tf18 = TransferFunction((k_p + k_o*s + k_i/s), s**2 + 2*omega_o*s + omega_o**2, s) + assert tf18.num == k_i/s + k_o*s + k_p + assert tf18.args == (k_i/s + k_o*s + k_p, omega_o**2 + 2*omega_o*s + s**2, s) + + # ValueError when denominator is zero. + raises(ValueError, lambda: TransferFunction(4, 0, s)) + raises(ValueError, lambda: TransferFunction(s, 0, s)) + raises(ValueError, lambda: TransferFunction(0, 0, s)) + + raises(TypeError, lambda: TransferFunction(Matrix([1, 2, 3]), s, s)) + + raises(TypeError, lambda: TransferFunction(s**2 + 2*s - 1, s + 3, 3)) + raises(TypeError, lambda: TransferFunction(p + 1, 5 - p, 4)) + raises(TypeError, lambda: TransferFunction(3, 4, 8)) + + +def test_TransferFunction_functions(): + # classmethod from_rational_expression + expr_1 = Mul(0, Pow(s, -1, evaluate=False), evaluate=False) + expr_2 = s/0 + expr_3 = (p*s**2 + 5*s)/(s + 1)**3 + expr_4 = 6 + expr_5 = ((2 + 3*s)*(5 + 2*s))/((9 + 3*s)*(5 + 2*s**2)) + expr_6 = (9*s**4 + 4*s**2 + 8)/((s + 1)*(s + 9)) + tf = TransferFunction(s + 1, s**2 + 2, s) + delay = exp(-s/tau) + expr_7 = delay*tf.to_expr() + H1 = TransferFunction.from_rational_expression(expr_7, s) + H2 = TransferFunction(s + 1, (s**2 + 2)*exp(s/tau), s) + expr_8 = Add(2, 3*s/(s**2 + 1), evaluate=False) + + assert TransferFunction.from_rational_expression(expr_1) == TransferFunction(0, s, s) + raises(ZeroDivisionError, lambda: TransferFunction.from_rational_expression(expr_2)) + raises(ValueError, lambda: TransferFunction.from_rational_expression(expr_3)) + assert TransferFunction.from_rational_expression(expr_3, s) == TransferFunction((p*s**2 + 5*s), (s + 1)**3, s) + assert TransferFunction.from_rational_expression(expr_3, p) == TransferFunction((p*s**2 + 5*s), (s + 1)**3, p) + raises(ValueError, lambda: TransferFunction.from_rational_expression(expr_4)) + assert TransferFunction.from_rational_expression(expr_4, s) == TransferFunction(6, 1, s) + assert TransferFunction.from_rational_expression(expr_5, s) == \ + TransferFunction((2 + 3*s)*(5 + 2*s), (9 + 3*s)*(5 + 2*s**2), s) + assert TransferFunction.from_rational_expression(expr_6, s) == \ + TransferFunction((9*s**4 + 4*s**2 + 8), (s + 1)*(s + 9), s) + assert H1 == H2 + assert TransferFunction.from_rational_expression(expr_8, s) == \ + TransferFunction(2*s**2 + 3*s + 2, s**2 + 1, s) + + # classmethod from_coeff_lists + tf1 = TransferFunction.from_coeff_lists([1, 2], [3, 4, 5], s) + num2 = [p**2, 2*p] + den2 = [p**3, p + 1, 4] + tf2 = TransferFunction.from_coeff_lists(num2, den2, s) + num3 = [1, 2, 3] + den3 = [0, 0] + + assert tf1 == TransferFunction(s + 2, 3*s**2 + 4*s + 5, s) + assert tf2 == TransferFunction(p**2*s + 2*p, p**3*s**2 + s*(p + 1) + 4, s) + raises(ZeroDivisionError, lambda: TransferFunction.from_coeff_lists(num3, den3, s)) + + # classmethod from_zpk + zeros = [4] + poles = [-1+2j, -1-2j] + gain = 3 + tf1 = TransferFunction.from_zpk(zeros, poles, gain, s) + + assert tf1 == TransferFunction(3*s - 12, (s + 1.0 - 2.0*I)*(s + 1.0 + 2.0*I), s) + + # explicitly cancel poles and zeros. + tf0 = TransferFunction(s**5 + s**3 + s, s - s**2, s) + a = TransferFunction(-(s**4 + s**2 + 1), s - 1, s) + assert tf0.simplify() == simplify(tf0) == a + + tf1 = TransferFunction((p + 3)*(p - 1), (p - 1)*(p + 5), p) + b = TransferFunction(p + 3, p + 5, p) + assert tf1.simplify() == simplify(tf1) == b + + # expand the numerator and the denominator. + G1 = TransferFunction((1 - s)**2, (s**2 + 1)**2, s) + G2 = TransferFunction(1, -3, p) + c = (a2*s**p + a1*s**s + a0*p**p)*(p**s + s**p) + d = (b0*s**s + b1*p**s)*(b2*s*p + p**p) + e = a0*p**p*p**s + a0*p**p*s**p + a1*p**s*s**s + a1*s**p*s**s + a2*p**s*s**p + a2*s**(2*p) + f = b0*b2*p*s*s**s + b0*p**p*s**s + b1*b2*p*p**s*s + b1*p**p*p**s + g = a1*a2*s*s**p + a1*p*s + a2*b1*p*s*s**p + b1*p**2*s + G3 = TransferFunction(c, d, s) + G4 = TransferFunction(a0*s**s - b0*p**p, (a1*s + b1*s*p)*(a2*s**p + p), p) + + assert G1.expand() == TransferFunction(s**2 - 2*s + 1, s**4 + 2*s**2 + 1, s) + assert tf1.expand() == TransferFunction(p**2 + 2*p - 3, p**2 + 4*p - 5, p) + assert G2.expand() == G2 + assert G3.expand() == TransferFunction(e, f, s) + assert G4.expand() == TransferFunction(a0*s**s - b0*p**p, g, p) + + # purely symbolic polynomials. + p1 = a1*s + a0 + p2 = b2*s**2 + b1*s + b0 + SP1 = TransferFunction(p1, p2, s) + expect1 = TransferFunction(2.0*s + 1.0, 5.0*s**2 + 4.0*s + 3.0, s) + expect1_ = TransferFunction(2*s + 1, 5*s**2 + 4*s + 3, s) + assert SP1.subs({a0: 1, a1: 2, b0: 3, b1: 4, b2: 5}) == expect1_ + assert SP1.subs({a0: 1, a1: 2, b0: 3, b1: 4, b2: 5}).evalf() == expect1 + assert expect1_.evalf() == expect1 + + c1, d0, d1, d2 = symbols('c1, d0:3') + p3, p4 = c1*p, d2*p**3 + d1*p**2 - d0 + SP2 = TransferFunction(p3, p4, p) + expect2 = TransferFunction(2.0*p, 5.0*p**3 + 2.0*p**2 - 3.0, p) + expect2_ = TransferFunction(2*p, 5*p**3 + 2*p**2 - 3, p) + assert SP2.subs({c1: 2, d0: 3, d1: 2, d2: 5}) == expect2_ + assert SP2.subs({c1: 2, d0: 3, d1: 2, d2: 5}).evalf() == expect2 + assert expect2_.evalf() == expect2 + + SP3 = TransferFunction(a0*p**3 + a1*s**2 - b0*s + b1, a1*s + p, s) + expect3 = TransferFunction(2.0*p**3 + 4.0*s**2 - s + 5.0, p + 4.0*s, s) + expect3_ = TransferFunction(2*p**3 + 4*s**2 - s + 5, p + 4*s, s) + assert SP3.subs({a0: 2, a1: 4, b0: 1, b1: 5}) == expect3_ + assert SP3.subs({a0: 2, a1: 4, b0: 1, b1: 5}).evalf() == expect3 + assert expect3_.evalf() == expect3 + + SP4 = TransferFunction(s - a1*p**3, a0*s + p, p) + expect4 = TransferFunction(7.0*p**3 + s, p - s, p) + expect4_ = TransferFunction(7*p**3 + s, p - s, p) + assert SP4.subs({a0: -1, a1: -7}) == expect4_ + assert SP4.subs({a0: -1, a1: -7}).evalf() == expect4 + assert expect4_.evalf() == expect4 + + # evaluate the transfer function at particular frequencies. + assert tf1.eval_frequency(wn) == wn**2/(wn**2 + 4*wn - 5) + 2*wn/(wn**2 + 4*wn - 5) - 3/(wn**2 + 4*wn - 5) + assert G1.eval_frequency(1 + I) == S(3)/25 + S(4)*I/25 + assert G4.eval_frequency(S(5)/3) == \ + a0*s**s/(a1*a2*s**(S(8)/3) + S(5)*a1*s/3 + 5*a2*b1*s**(S(8)/3)/3 + S(25)*b1*s/9) - 5*3**(S(1)/3)*5**(S(2)/3)*b0/(9*a1*a2*s**(S(8)/3) + 15*a1*s + 15*a2*b1*s**(S(8)/3) + 25*b1*s) + + # Low-frequency (or DC) gain. + assert tf0.dc_gain() == 1 + assert tf1.dc_gain() == Rational(3, 5) + assert SP2.dc_gain() == 0 + assert expect4.dc_gain() == -1 + assert expect2_.dc_gain() == 0 + assert TransferFunction(1, s, s).dc_gain() == oo + + # Poles of a transfer function. + tf_ = TransferFunction(x**3 - k, k, x) + _tf = TransferFunction(k, x**4 - k, x) + TF_ = TransferFunction(x**2, x**10 + x + x**2, x) + _TF = TransferFunction(x**10 + x + x**2, x**2, x) + assert G1.poles() == [I, I, -I, -I] + assert G2.poles() == [] + assert tf1.poles() == [-5, 1] + assert expect4_.poles() == [s] + assert SP4.poles() == [-a0*s] + assert expect3.poles() == [-0.25*p] + assert str(expect2.poles()) == str([0.729001428685125, -0.564500714342563 - 0.710198984796332*I, -0.564500714342563 + 0.710198984796332*I]) + assert str(expect1.poles()) == str([-0.4 - 0.66332495807108*I, -0.4 + 0.66332495807108*I]) + assert _tf.poles() == [k**(Rational(1, 4)), -k**(Rational(1, 4)), I*k**(Rational(1, 4)), -I*k**(Rational(1, 4))] + assert TF_.poles() == [CRootOf(x**9 + x + 1, 0), 0, CRootOf(x**9 + x + 1, 1), CRootOf(x**9 + x + 1, 2), + CRootOf(x**9 + x + 1, 3), CRootOf(x**9 + x + 1, 4), CRootOf(x**9 + x + 1, 5), CRootOf(x**9 + x + 1, 6), + CRootOf(x**9 + x + 1, 7), CRootOf(x**9 + x + 1, 8)] + raises(NotImplementedError, lambda: TransferFunction(x**2, a0*x**10 + x + x**2, x).poles()) + + # Stability of a transfer function. + q, r = symbols('q, r', negative=True) + t = symbols('t', positive=True) + TF_ = TransferFunction(s**2 + a0 - a1*p, q*s - r, s) + stable_tf = TransferFunction(s**2 + a0 - a1*p, q*s - 1, s) + stable_tf_ = TransferFunction(s**2 + a0 - a1*p, q*s - t, s) + + assert G1.is_stable() is False + assert G2.is_stable() is True + assert tf1.is_stable() is False # as one pole is +ve, and the other is -ve. + assert expect2.is_stable() is False + assert expect1.is_stable() is True + assert stable_tf.is_stable() is True + assert stable_tf_.is_stable() is True + assert TF_.is_stable() is False + assert expect4_.is_stable() is None # no assumption provided for the only pole 's'. + assert SP4.is_stable() is None + + # Zeros of a transfer function. + assert G1.zeros() == [1, 1] + assert G2.zeros() == [] + assert tf1.zeros() == [-3, 1] + assert expect4_.zeros() == [7**(Rational(2, 3))*(-s)**(Rational(1, 3))/7, -7**(Rational(2, 3))*(-s)**(Rational(1, 3))/14 - + sqrt(3)*7**(Rational(2, 3))*I*(-s)**(Rational(1, 3))/14, -7**(Rational(2, 3))*(-s)**(Rational(1, 3))/14 + sqrt(3)*7**(Rational(2, 3))*I*(-s)**(Rational(1, 3))/14] + assert SP4.zeros() == [(s/a1)**(Rational(1, 3)), -(s/a1)**(Rational(1, 3))/2 - sqrt(3)*I*(s/a1)**(Rational(1, 3))/2, + -(s/a1)**(Rational(1, 3))/2 + sqrt(3)*I*(s/a1)**(Rational(1, 3))/2] + assert str(expect3.zeros()) == str([0.125 - 1.11102430216445*sqrt(-0.405063291139241*p**3 - 1.0), + 1.11102430216445*sqrt(-0.405063291139241*p**3 - 1.0) + 0.125]) + assert tf_.zeros() == [k**(Rational(1, 3)), -k**(Rational(1, 3))/2 - sqrt(3)*I*k**(Rational(1, 3))/2, + -k**(Rational(1, 3))/2 + sqrt(3)*I*k**(Rational(1, 3))/2] + assert _TF.zeros() == [CRootOf(x**9 + x + 1, 0), 0, CRootOf(x**9 + x + 1, 1), CRootOf(x**9 + x + 1, 2), + CRootOf(x**9 + x + 1, 3), CRootOf(x**9 + x + 1, 4), CRootOf(x**9 + x + 1, 5), CRootOf(x**9 + x + 1, 6), + CRootOf(x**9 + x + 1, 7), CRootOf(x**9 + x + 1, 8)] + raises(NotImplementedError, lambda: TransferFunction(a0*x**10 + x + x**2, x**2, x).zeros()) + + # negation of TF. + tf2 = TransferFunction(s + 3, s**2 - s**3 + 9, s) + tf3 = TransferFunction(-3*p + 3, 1 - p, p) + assert -tf2 == TransferFunction(-s - 3, s**2 - s**3 + 9, s) + assert -tf3 == TransferFunction(3*p - 3, 1 - p, p) + + # taking power of a TF. + tf4 = TransferFunction(p + 4, p - 3, p) + tf5 = TransferFunction(s**2 + 1, 1 - s, s) + expect2 = TransferFunction((s**2 + 1)**3, (1 - s)**3, s) + expect1 = TransferFunction((p + 4)**2, (p - 3)**2, p) + assert (tf4*tf4).doit() == tf4**2 == pow(tf4, 2) == expect1 + assert (tf5*tf5*tf5).doit() == tf5**3 == pow(tf5, 3) == expect2 + assert tf5**0 == pow(tf5, 0) == TransferFunction(1, 1, s) + assert Series(tf4).doit()**-1 == tf4**-1 == pow(tf4, -1) == TransferFunction(p - 3, p + 4, p) + assert (tf5*tf5).doit()**-1 == tf5**-2 == pow(tf5, -2) == TransferFunction((1 - s)**2, (s**2 + 1)**2, s) + + raises(ValueError, lambda: tf4**(s**2 + s - 1)) + raises(ValueError, lambda: tf5**s) + raises(ValueError, lambda: tf4**tf5) + + # SymPy's own functions. + tf = TransferFunction(s - 1, s**2 - 2*s + 1, s) + tf6 = TransferFunction(s + p, p**2 - 5, s) + assert factor(tf) == TransferFunction(s - 1, (s - 1)**2, s) + assert tf.num.subs(s, 2) == tf.den.subs(s, 2) == 1 + # subs & xreplace + assert tf.subs(s, 2) == TransferFunction(s - 1, s**2 - 2*s + 1, s) + assert tf6.subs(p, 3) == TransferFunction(s + 3, 4, s) + assert tf3.xreplace({p: s}) == TransferFunction(-3*s + 3, 1 - s, s) + raises(TypeError, lambda: tf3.xreplace({p: exp(2)})) + assert tf3.subs(p, exp(2)) == tf3 + + tf7 = TransferFunction(a0*s**p + a1*p**s, a2*p - s, s) + assert tf7.xreplace({s: k}) == TransferFunction(a0*k**p + a1*p**k, a2*p - k, k) + assert tf7.subs(s, k) == TransferFunction(a0*s**p + a1*p**s, a2*p - s, s) + + # Conversion to Expr with to_expr() + tf8 = TransferFunction(a0*s**5 + 5*s**2 + 3, s**6 - 3, s) + tf9 = TransferFunction((5 + s), (5 + s)*(6 + s), s) + tf10 = TransferFunction(0, 1, s) + tf11 = TransferFunction(1, 1, s) + assert tf8.to_expr() == Mul((a0*s**5 + 5*s**2 + 3), Pow((s**6 - 3), -1, evaluate=False), evaluate=False) + assert tf9.to_expr() == Mul((s + 5), Pow((5 + s)*(6 + s), -1, evaluate=False), evaluate=False) + assert tf10.to_expr() == Mul(S(0), Pow(1, -1, evaluate=False), evaluate=False) + assert tf11.to_expr() == Pow(1, -1, evaluate=False) + + +def test_TransferFunction_addition_and_subtraction(): + tf1 = TransferFunction(s + 6, s - 5, s) + tf2 = TransferFunction(s + 3, s + 1, s) + tf3 = TransferFunction(s + 1, s**2 + s + 1, s) + tf4 = TransferFunction(p, 2 - p, p) + + # addition + assert tf1 + tf2 == Parallel(tf1, tf2) + assert tf3 + tf1 == Parallel(tf3, tf1) + assert -tf1 + tf2 + tf3 == Parallel(-tf1, tf2, tf3) + assert tf1 + (tf2 + tf3) == Parallel(tf1, tf2, tf3) + + c = symbols("c", commutative=False) + raises(ValueError, lambda: tf1 + Matrix([1, 2, 3])) + raises(ValueError, lambda: tf2 + c) + raises(ValueError, lambda: tf3 + tf4) + raises(ValueError, lambda: tf1 + (s - 1)) + raises(ValueError, lambda: tf1 + 8) + raises(ValueError, lambda: (1 - p**3) + tf1) + + # subtraction + assert tf1 - tf2 == Parallel(tf1, -tf2) + assert tf3 - tf2 == Parallel(tf3, -tf2) + assert -tf1 - tf3 == Parallel(-tf1, -tf3) + assert tf1 - tf2 + tf3 == Parallel(tf1, -tf2, tf3) + + raises(ValueError, lambda: tf1 - Matrix([1, 2, 3])) + raises(ValueError, lambda: tf3 - tf4) + raises(ValueError, lambda: tf1 - (s - 1)) + raises(ValueError, lambda: tf1 - 8) + raises(ValueError, lambda: (s + 5) - tf2) + raises(ValueError, lambda: (1 + p**4) - tf1) + + +def test_TransferFunction_multiplication_and_division(): + G1 = TransferFunction(s + 3, -s**3 + 9, s) + G2 = TransferFunction(s + 1, s - 5, s) + G3 = TransferFunction(p, p**4 - 6, p) + G4 = TransferFunction(p + 4, p - 5, p) + G5 = TransferFunction(s + 6, s - 5, s) + G6 = TransferFunction(s + 3, s + 1, s) + G7 = TransferFunction(1, 1, s) + + # multiplication + assert G1*G2 == Series(G1, G2) + assert -G1*G5 == Series(-G1, G5) + assert -G2*G5*-G6 == Series(-G2, G5, -G6) + assert -G1*-G2*-G5*-G6 == Series(-G1, -G2, -G5, -G6) + assert G3*G4 == Series(G3, G4) + assert (G1*G2)*-(G5*G6) == \ + Series(G1, G2, TransferFunction(-1, 1, s), Series(G5, G6)) + assert G1*G2*(G5 + G6) == Series(G1, G2, Parallel(G5, G6)) + + # division - See ``test_Feedback_functions()`` for division by Parallel objects. + assert G5/G6 == Series(G5, pow(G6, -1)) + assert -G3/G4 == Series(-G3, pow(G4, -1)) + assert (G5*G6)/G7 == Series(G5, G6, pow(G7, -1)) + + c = symbols("c", commutative=False) + raises(ValueError, lambda: G3 * Matrix([1, 2, 3])) + raises(ValueError, lambda: G1 * c) + raises(ValueError, lambda: G3 * G5) + raises(ValueError, lambda: G5 * (s - 1)) + raises(ValueError, lambda: 9 * G5) + + raises(ValueError, lambda: G3 / Matrix([1, 2, 3])) + raises(ValueError, lambda: G6 / 0) + raises(ValueError, lambda: G3 / G5) + raises(ValueError, lambda: G5 / 2) + raises(ValueError, lambda: G5 / s**2) + raises(ValueError, lambda: (s - 4*s**2) / G2) + raises(ValueError, lambda: 0 / G4) + raises(ValueError, lambda: G7 / (1 + G6)) + raises(ValueError, lambda: G7 / (G5 * G6)) + raises(ValueError, lambda: G7 / (G7 + (G5 + G6))) + + +def test_TransferFunction_is_proper(): + omega_o, zeta, tau = symbols('omega_o, zeta, tau') + G1 = TransferFunction(omega_o**2, s**2 + p*omega_o*zeta*s + omega_o**2, omega_o) + G2 = TransferFunction(tau - s**3, tau + p**4, tau) + G3 = TransferFunction(a*b*s**3 + s**2 - a*p + s, b - s*p**2, p) + G4 = TransferFunction(b*s**2 + p**2 - a*p + s, b - p**2, s) + assert G1.is_proper + assert G2.is_proper + assert G3.is_proper + assert not G4.is_proper + + +def test_TransferFunction_is_strictly_proper(): + omega_o, zeta, tau = symbols('omega_o, zeta, tau') + tf1 = TransferFunction(omega_o**2, s**2 + p*omega_o*zeta*s + omega_o**2, omega_o) + tf2 = TransferFunction(tau - s**3, tau + p**4, tau) + tf3 = TransferFunction(a*b*s**3 + s**2 - a*p + s, b - s*p**2, p) + tf4 = TransferFunction(b*s**2 + p**2 - a*p + s, b - p**2, s) + assert not tf1.is_strictly_proper + assert not tf2.is_strictly_proper + assert tf3.is_strictly_proper + assert not tf4.is_strictly_proper + + +def test_TransferFunction_is_biproper(): + tau, omega_o, zeta = symbols('tau, omega_o, zeta') + tf1 = TransferFunction(omega_o**2, s**2 + p*omega_o*zeta*s + omega_o**2, omega_o) + tf2 = TransferFunction(tau - s**3, tau + p**4, tau) + tf3 = TransferFunction(a*b*s**3 + s**2 - a*p + s, b - s*p**2, p) + tf4 = TransferFunction(b*s**2 + p**2 - a*p + s, b - p**2, s) + assert tf1.is_biproper + assert tf2.is_biproper + assert not tf3.is_biproper + assert not tf4.is_biproper + + +def test_PIDController(): + kp, ki, kd, tf = symbols("kp ki kd tf") + p1 = PIDController(kp, ki, kd, tf) + p2 = PIDController() + + # Type Checking + assert isinstance(p1, PIDController) + assert isinstance(p1, TransferFunction) + + # Properties checking + assert p1 == PIDController(kp, ki, kd, tf, s) + assert p2 == PIDController(kp, ki, kd, 0, s) + assert p1.num == kd*s**2 + ki*s*tf + ki + kp*s**2*tf + kp*s + assert p1.den == s**2*tf + s + assert p1.var == s + assert p1.kp == kp + assert p1.ki == ki + assert p1.kd == kd + assert p1.tf == tf + + # Functionality checking + assert p1.doit() == TransferFunction(kd*s**2 + ki*s*tf + ki + kp*s**2*tf + kp*s, s**2*tf + s, s) + assert p1.is_proper == True + assert p1.is_biproper == True + assert p1.is_strictly_proper == False + assert p2.doit() == TransferFunction(kd*s**2 + ki + kp*s, s, s) + + # Using PIDController with TransferFunction + tf1 = TransferFunction(s, s + 1, s) + par1 = Parallel(p1, tf1) + ser1 = Series(p1, tf1) + fed1 = Feedback(p1, tf1) + assert par1 == Parallel(PIDController(kp, ki, kd, tf, s), TransferFunction(s, s + 1, s)) + assert ser1 == Series(PIDController(kp, ki, kd, tf, s), TransferFunction(s, s + 1, s)) + assert fed1 == Feedback(PIDController(kp, ki, kd, tf, s), TransferFunction(s, s + 1, s)) + assert par1.doit() == TransferFunction(s*(s**2*tf + s) + (s + 1)*(kd*s**2 + ki*s*tf + ki + kp*s**2*tf + kp*s), + (s + 1)*(s**2*tf + s), s) + assert ser1.doit() == TransferFunction(s*(kd*s**2 + ki*s*tf + ki + kp*s**2*tf + kp*s), + (s + 1)*(s**2*tf + s), s) + assert fed1.doit() == TransferFunction((s + 1)*(s**2*tf + s)*(kd*s**2 + ki*s*tf + ki + kp*s**2*tf + kp*s), + (s*(kd*s**2 + ki*s*tf + ki + kp*s**2*tf + kp*s) + (s + 1)*(s**2*tf + s))*(s**2*tf + s), s) + + +def test_Series_construction(): + tf = TransferFunction(a0*s**3 + a1*s**2 - a2*s, b0*p**4 + b1*p**3 - b2*s*p, s) + tf2 = TransferFunction(a2*p - s, a2*s + p, s) + tf3 = TransferFunction(a0*p + p**a1 - s, p, p) + tf4 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s) + inp = Function('X_d')(s) + out = Function('X')(s) + + s0 = Series(tf, tf2) + assert s0.args == (tf, tf2) + assert s0.var == s + + s1 = Series(Parallel(tf, -tf2), tf2) + assert s1.args == (Parallel(tf, -tf2), tf2) + assert s1.var == s + + tf3_ = TransferFunction(inp, 1, s) + tf4_ = TransferFunction(-out, 1, s) + s2 = Series(tf, Parallel(tf3_, tf4_), tf2) + assert s2.args == (tf, Parallel(tf3_, tf4_), tf2) + + s3 = Series(tf, tf2, tf4) + assert s3.args == (tf, tf2, tf4) + + s4 = Series(tf3_, tf4_) + assert s4.args == (tf3_, tf4_) + assert s4.var == s + + s6 = Series(tf2, tf4, Parallel(tf2, -tf), tf4) + assert s6.args == (tf2, tf4, Parallel(tf2, -tf), tf4) + + s7 = Series(tf, tf2) + assert s0 == s7 + assert not s0 == s2 + + raises(ValueError, lambda: Series(tf, tf3)) + raises(ValueError, lambda: Series(tf, tf2, tf3, tf4)) + raises(ValueError, lambda: Series(-tf3, tf2)) + raises(TypeError, lambda: Series(2, tf, tf4)) + raises(TypeError, lambda: Series(s**2 + p*s, tf3, tf2)) + raises(TypeError, lambda: Series(tf3, Matrix([1, 2, 3, 4]))) + + +def test_MIMOSeries_construction(): + tf_1 = TransferFunction(a0*s**3 + a1*s**2 - a2*s, b0*p**4 + b1*p**3 - b2*s*p, s) + tf_2 = TransferFunction(a2*p - s, a2*s + p, s) + tf_3 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s) + + tfm_1 = TransferFunctionMatrix([[tf_1, tf_2, tf_3], [-tf_3, -tf_2, tf_1]]) + tfm_2 = TransferFunctionMatrix([[-tf_2], [-tf_2], [-tf_3]]) + tfm_3 = TransferFunctionMatrix([[-tf_3]]) + tfm_4 = TransferFunctionMatrix([[TF3], [TF2], [-TF1]]) + tfm_5 = TransferFunctionMatrix.from_Matrix(Matrix([1/p]), p) + + s8 = MIMOSeries(tfm_2, tfm_1) + assert s8.args == (tfm_2, tfm_1) + assert s8.var == s + assert s8.shape == (s8.num_outputs, s8.num_inputs) == (2, 1) + + s9 = MIMOSeries(tfm_3, tfm_2, tfm_1) + assert s9.args == (tfm_3, tfm_2, tfm_1) + assert s9.var == s + assert s9.shape == (s9.num_outputs, s9.num_inputs) == (2, 1) + + s11 = MIMOSeries(tfm_3, MIMOParallel(-tfm_2, -tfm_4), tfm_1) + assert s11.args == (tfm_3, MIMOParallel(-tfm_2, -tfm_4), tfm_1) + assert s11.shape == (s11.num_outputs, s11.num_inputs) == (2, 1) + + # arg cannot be empty tuple. + raises(ValueError, lambda: MIMOSeries()) + + # arg cannot contain SISO as well as MIMO systems. + raises(TypeError, lambda: MIMOSeries(tfm_1, tf_1)) + + # for all the adjacent transfer function matrices: + # no. of inputs of first TFM must be equal to the no. of outputs of the second TFM. + raises(ValueError, lambda: MIMOSeries(tfm_1, tfm_2, -tfm_1)) + + # all the TFMs must use the same complex variable. + raises(ValueError, lambda: MIMOSeries(tfm_3, tfm_5)) + + # Number or expression not allowed in the arguments. + raises(TypeError, lambda: MIMOSeries(2, tfm_2, tfm_3)) + raises(TypeError, lambda: MIMOSeries(s**2 + p*s, -tfm_2, tfm_3)) + raises(TypeError, lambda: MIMOSeries(Matrix([1/p]), tfm_3)) + + +def test_Series_functions(): + tf1 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s) + tf2 = TransferFunction(k, 1, s) + tf3 = TransferFunction(a2*p - s, a2*s + p, s) + tf4 = TransferFunction(a0*p + p**a1 - s, p, p) + tf5 = TransferFunction(a1*s**2 + a2*s - a0, s + a0, s) + + assert tf1*tf2*tf3 == Series(tf1, tf2, tf3) == Series(Series(tf1, tf2), tf3) \ + == Series(tf1, Series(tf2, tf3)) + assert tf1*(tf2 + tf3) == Series(tf1, Parallel(tf2, tf3)) + assert tf1*tf2 + tf5 == Parallel(Series(tf1, tf2), tf5) + assert tf1*tf2 - tf5 == Parallel(Series(tf1, tf2), -tf5) + assert tf1*tf2 + tf3 + tf5 == Parallel(Series(tf1, tf2), tf3, tf5) + assert tf1*tf2 - tf3 - tf5 == Parallel(Series(tf1, tf2), -tf3, -tf5) + assert tf1*tf2 - tf3 + tf5 == Parallel(Series(tf1, tf2), -tf3, tf5) + assert tf1*tf2 + tf3*tf5 == Parallel(Series(tf1, tf2), Series(tf3, tf5)) + assert tf1*tf2 - tf3*tf5 == Parallel(Series(tf1, tf2), Series(TransferFunction(-1, 1, s), Series(tf3, tf5))) + assert tf2*tf3*(tf2 - tf1)*tf3 == Series(tf2, tf3, Parallel(tf2, -tf1), tf3) + assert -tf1*tf2 == Series(-tf1, tf2) + assert -(tf1*tf2) == Series(TransferFunction(-1, 1, s), Series(tf1, tf2)) + raises(ValueError, lambda: tf1*tf2*tf4) + raises(ValueError, lambda: tf1*(tf2 - tf4)) + raises(ValueError, lambda: tf3*Matrix([1, 2, 3])) + + # evaluate=True -> doit() + assert Series(tf1, tf2, evaluate=True) == Series(tf1, tf2).doit() == \ + TransferFunction(k, s**2 + 2*s*wn*zeta + wn**2, s) + assert Series(tf1, tf2, Parallel(tf1, -tf3), evaluate=True) == Series(tf1, tf2, Parallel(tf1, -tf3)).doit() == \ + TransferFunction(k*(a2*s + p + (-a2*p + s)*(s**2 + 2*s*wn*zeta + wn**2)), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2)**2, s) + assert Series(tf2, tf1, -tf3, evaluate=True) == Series(tf2, tf1, -tf3).doit() == \ + TransferFunction(k*(-a2*p + s), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s) + assert not Series(tf1, -tf2, evaluate=False) == Series(tf1, -tf2).doit() + + assert Series(Parallel(tf1, tf2), Parallel(tf2, -tf3)).doit() == \ + TransferFunction((k*(s**2 + 2*s*wn*zeta + wn**2) + 1)*(-a2*p + k*(a2*s + p) + s), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s) + assert Series(-tf1, -tf2, -tf3).doit() == \ + TransferFunction(k*(-a2*p + s), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s) + assert -Series(tf1, tf2, tf3).doit() == \ + TransferFunction(-k*(a2*p - s), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s) + assert Series(tf2, tf3, Parallel(tf2, -tf1), tf3).doit() == \ + TransferFunction(k*(a2*p - s)**2*(k*(s**2 + 2*s*wn*zeta + wn**2) - 1), (a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2), s) + + assert Series(tf1, tf2).rewrite(TransferFunction) == TransferFunction(k, s**2 + 2*s*wn*zeta + wn**2, s) + assert Series(tf2, tf1, -tf3).rewrite(TransferFunction) == \ + TransferFunction(k*(-a2*p + s), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s) + + S1 = Series(Parallel(tf1, tf2), Parallel(tf2, -tf3)) + assert S1.is_proper + assert not S1.is_strictly_proper + assert S1.is_biproper + + S2 = Series(tf1, tf2, tf3) + assert S2.is_proper + assert S2.is_strictly_proper + assert not S2.is_biproper + + S3 = Series(tf1, -tf2, Parallel(tf1, -tf3)) + assert S3.is_proper + assert S3.is_strictly_proper + assert not S3.is_biproper + + +def test_MIMOSeries_functions(): + tfm1 = TransferFunctionMatrix([[TF1, TF2, TF3], [-TF3, -TF2, TF1]]) + tfm2 = TransferFunctionMatrix([[-TF1], [-TF2], [-TF3]]) + tfm3 = TransferFunctionMatrix([[-TF1]]) + tfm4 = TransferFunctionMatrix([[-TF2, -TF3], [-TF1, TF2]]) + tfm5 = TransferFunctionMatrix([[TF2, -TF2], [-TF3, -TF2]]) + tfm6 = TransferFunctionMatrix([[-TF3], [TF1]]) + tfm7 = TransferFunctionMatrix([[TF1], [-TF2]]) + + assert tfm1*tfm2 + tfm6 == MIMOParallel(MIMOSeries(tfm2, tfm1), tfm6) + assert tfm1*tfm2 + tfm7 + tfm6 == MIMOParallel(MIMOSeries(tfm2, tfm1), tfm7, tfm6) + assert tfm1*tfm2 - tfm6 - tfm7 == MIMOParallel(MIMOSeries(tfm2, tfm1), -tfm6, -tfm7) + assert tfm4*tfm5 + (tfm4 - tfm5) == MIMOParallel(MIMOSeries(tfm5, tfm4), tfm4, -tfm5) + assert tfm4*-tfm6 + (-tfm4*tfm6) == MIMOParallel(MIMOSeries(-tfm6, tfm4), MIMOSeries(tfm6, -tfm4)) + + raises(ValueError, lambda: tfm1*tfm2 + TF1) + raises(TypeError, lambda: tfm1*tfm2 + a0) + raises(TypeError, lambda: tfm4*tfm6 - (s - 1)) + raises(TypeError, lambda: tfm4*-tfm6 - 8) + raises(TypeError, lambda: (-1 + p**5) + tfm1*tfm2) + + # Shape criteria. + + raises(TypeError, lambda: -tfm1*tfm2 + tfm4) + raises(TypeError, lambda: tfm1*tfm2 - tfm4 + tfm5) + raises(TypeError, lambda: tfm1*tfm2 - tfm4*tfm5) + + assert tfm1*tfm2*-tfm3 == MIMOSeries(-tfm3, tfm2, tfm1) + assert (tfm1*-tfm2)*tfm3 == MIMOSeries(tfm3, -tfm2, tfm1) + + # Multiplication of a Series object with a SISO TF not allowed. + + raises(ValueError, lambda: tfm4*tfm5*TF1) + raises(TypeError, lambda: tfm4*tfm5*a1) + raises(TypeError, lambda: tfm4*-tfm5*(s - 2)) + raises(TypeError, lambda: tfm5*tfm4*9) + raises(TypeError, lambda: (-p**3 + 1)*tfm5*tfm4) + + # Transfer function matrix in the arguments. + assert (MIMOSeries(tfm2, tfm1, evaluate=True) == MIMOSeries(tfm2, tfm1).doit() + == TransferFunctionMatrix(((TransferFunction(-k**2*(a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**2 + (-a2*p + s)*(a2*p - s)*(s**2 + 2*s*wn*zeta + wn**2)**2 - (a2*s + p)**2, + (a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**2, s),), + (TransferFunction(k**2*(a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**2 + (-a2*p + s)*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2) + (a2*p - s)*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), + (a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**2, s),)))) + + # doit() should not cancel poles and zeros. + mat_1 = Matrix([[1/(1+s), (1+s)/(1+s**2+2*s)**3]]) + mat_2 = Matrix([[(1+s)], [(1+s**2+2*s)**3/(1+s)]]) + tm_1, tm_2 = TransferFunctionMatrix.from_Matrix(mat_1, s), TransferFunctionMatrix.from_Matrix(mat_2, s) + assert (MIMOSeries(tm_2, tm_1).doit() + == TransferFunctionMatrix(((TransferFunction(2*(s + 1)**2*(s**2 + 2*s + 1)**3, (s + 1)**2*(s**2 + 2*s + 1)**3, s),),))) + assert MIMOSeries(tm_2, tm_1).doit().simplify() == TransferFunctionMatrix(((TransferFunction(2, 1, s),),)) + + # calling doit() will expand the internal Series and Parallel objects. + assert (MIMOSeries(-tfm3, -tfm2, tfm1, evaluate=True) + == MIMOSeries(-tfm3, -tfm2, tfm1).doit() + == TransferFunctionMatrix(((TransferFunction(k**2*(a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**2 + (a2*p - s)**2*(s**2 + 2*s*wn*zeta + wn**2)**2 + (a2*s + p)**2, + (a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**3, s),), + (TransferFunction(-k**2*(a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**2 + (-a2*p + s)*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2) + (a2*p - s)*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), + (a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**3, s),)))) + assert (MIMOSeries(MIMOParallel(tfm4, tfm5), tfm5, evaluate=True) + == MIMOSeries(MIMOParallel(tfm4, tfm5), tfm5).doit() + == TransferFunctionMatrix(((TransferFunction(-k*(-a2*s - p + (-a2*p + s)*(s**2 + 2*s*wn*zeta + wn**2)), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s), TransferFunction(k*(-a2*p - \ + k*(a2*s + p) + s), a2*s + p, s)), (TransferFunction(-k*(-a2*s - p + (-a2*p + s)*(s**2 + 2*s*wn*zeta + wn**2)), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s), \ + TransferFunction((-a2*p + s)*(-a2*p - k*(a2*s + p) + s), (a2*s + p)**2, s)))) == MIMOSeries(MIMOParallel(tfm4, tfm5), tfm5).rewrite(TransferFunctionMatrix)) + + +def test_Parallel_construction(): + tf = TransferFunction(a0*s**3 + a1*s**2 - a2*s, b0*p**4 + b1*p**3 - b2*s*p, s) + tf2 = TransferFunction(a2*p - s, a2*s + p, s) + tf3 = TransferFunction(a0*p + p**a1 - s, p, p) + tf4 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s) + inp = Function('X_d')(s) + out = Function('X')(s) + + p0 = Parallel(tf, tf2) + assert p0.args == (tf, tf2) + assert p0.var == s + + p1 = Parallel(Series(tf, -tf2), tf2) + assert p1.args == (Series(tf, -tf2), tf2) + assert p1.var == s + + tf3_ = TransferFunction(inp, 1, s) + tf4_ = TransferFunction(-out, 1, s) + p2 = Parallel(tf, Series(tf3_, -tf4_), tf2) + assert p2.args == (tf, Series(tf3_, -tf4_), tf2) + + p3 = Parallel(tf, tf2, tf4) + assert p3.args == (tf, tf2, tf4) + + p4 = Parallel(tf3_, tf4_) + assert p4.args == (tf3_, tf4_) + assert p4.var == s + + p5 = Parallel(tf, tf2) + assert p0 == p5 + assert not p0 == p1 + + p6 = Parallel(tf2, tf4, Series(tf2, -tf4)) + assert p6.args == (tf2, tf4, Series(tf2, -tf4)) + + p7 = Parallel(tf2, tf4, Series(tf2, -tf), tf4) + assert p7.args == (tf2, tf4, Series(tf2, -tf), tf4) + + raises(ValueError, lambda: Parallel(tf, tf3)) + raises(ValueError, lambda: Parallel(tf, tf2, tf3, tf4)) + raises(ValueError, lambda: Parallel(-tf3, tf4)) + raises(TypeError, lambda: Parallel(2, tf, tf4)) + raises(TypeError, lambda: Parallel(s**2 + p*s, tf3, tf2)) + raises(TypeError, lambda: Parallel(tf3, Matrix([1, 2, 3, 4]))) + + +def test_MIMOParallel_construction(): + tfm1 = TransferFunctionMatrix([[TF1], [TF2], [TF3]]) + tfm2 = TransferFunctionMatrix([[-TF3], [TF2], [TF1]]) + tfm3 = TransferFunctionMatrix([[TF1]]) + tfm4 = TransferFunctionMatrix([[TF2], [TF1], [TF3]]) + tfm5 = TransferFunctionMatrix([[TF1, TF2], [TF2, TF1]]) + tfm6 = TransferFunctionMatrix([[TF2, TF1], [TF1, TF2]]) + tfm7 = TransferFunctionMatrix.from_Matrix(Matrix([[1/p]]), p) + + p8 = MIMOParallel(tfm1, tfm2) + assert p8.args == (tfm1, tfm2) + assert p8.var == s + assert p8.shape == (p8.num_outputs, p8.num_inputs) == (3, 1) + + p9 = MIMOParallel(MIMOSeries(tfm3, tfm1), tfm2) + assert p9.args == (MIMOSeries(tfm3, tfm1), tfm2) + assert p9.var == s + assert p9.shape == (p9.num_outputs, p9.num_inputs) == (3, 1) + + p10 = MIMOParallel(tfm1, MIMOSeries(tfm3, tfm4), tfm2) + assert p10.args == (tfm1, MIMOSeries(tfm3, tfm4), tfm2) + assert p10.var == s + assert p10.shape == (p10.num_outputs, p10.num_inputs) == (3, 1) + + p11 = MIMOParallel(tfm2, tfm1, tfm4) + assert p11.args == (tfm2, tfm1, tfm4) + assert p11.shape == (p11.num_outputs, p11.num_inputs) == (3, 1) + + p12 = MIMOParallel(tfm6, tfm5) + assert p12.args == (tfm6, tfm5) + assert p12.shape == (p12.num_outputs, p12.num_inputs) == (2, 2) + + p13 = MIMOParallel(tfm2, tfm4, MIMOSeries(-tfm3, tfm4), -tfm4) + assert p13.args == (tfm2, tfm4, MIMOSeries(-tfm3, tfm4), -tfm4) + assert p13.shape == (p13.num_outputs, p13.num_inputs) == (3, 1) + + # arg cannot be empty tuple. + raises(TypeError, lambda: MIMOParallel(())) + + # arg cannot contain SISO as well as MIMO systems. + raises(TypeError, lambda: MIMOParallel(tfm1, tfm2, TF1)) + + # all TFMs must have same shapes. + raises(TypeError, lambda: MIMOParallel(tfm1, tfm3, tfm4)) + + # all TFMs must be using the same complex variable. + raises(ValueError, lambda: MIMOParallel(tfm3, tfm7)) + + # Number or expression not allowed in the arguments. + raises(TypeError, lambda: MIMOParallel(2, tfm1, tfm4)) + raises(TypeError, lambda: MIMOParallel(s**2 + p*s, -tfm4, tfm2)) + + +def test_Parallel_functions(): + tf1 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s) + tf2 = TransferFunction(k, 1, s) + tf3 = TransferFunction(a2*p - s, a2*s + p, s) + tf4 = TransferFunction(a0*p + p**a1 - s, p, p) + tf5 = TransferFunction(a1*s**2 + a2*s - a0, s + a0, s) + + assert tf1 + tf2 + tf3 == Parallel(tf1, tf2, tf3) + assert tf1 + tf2 + tf3 + tf5 == Parallel(tf1, tf2, tf3, tf5) + assert tf1 + tf2 - tf3 - tf5 == Parallel(tf1, tf2, -tf3, -tf5) + assert tf1 + tf2*tf3 == Parallel(tf1, Series(tf2, tf3)) + assert tf1 - tf2*tf3 == Parallel(tf1, -Series(tf2,tf3)) + assert -tf1 - tf2 == Parallel(-tf1, -tf2) + assert -(tf1 + tf2) == Series(TransferFunction(-1, 1, s), Parallel(tf1, tf2)) + assert (tf2 + tf3)*tf1 == Series(Parallel(tf2, tf3), tf1) + assert (tf1 + tf2)*(tf3*tf5) == Series(Parallel(tf1, tf2), tf3, tf5) + assert -(tf2 + tf3)*-tf5 == Series(TransferFunction(-1, 1, s), Parallel(tf2, tf3), -tf5) + assert tf2 + tf3 + tf2*tf1 + tf5 == Parallel(tf2, tf3, Series(tf2, tf1), tf5) + assert tf2 + tf3 + tf2*tf1 - tf3 == Parallel(tf2, tf3, Series(tf2, tf1), -tf3) + assert (tf1 + tf2 + tf5)*(tf3 + tf5) == Series(Parallel(tf1, tf2, tf5), Parallel(tf3, tf5)) + raises(ValueError, lambda: tf1 + tf2 + tf4) + raises(ValueError, lambda: tf1 - tf2*tf4) + raises(ValueError, lambda: tf3 + Matrix([1, 2, 3])) + + # evaluate=True -> doit() + assert Parallel(tf1, tf2, evaluate=True) == Parallel(tf1, tf2).doit() == \ + TransferFunction(k*(s**2 + 2*s*wn*zeta + wn**2) + 1, s**2 + 2*s*wn*zeta + wn**2, s) + assert Parallel(tf1, tf2, Series(-tf1, tf3), evaluate=True) == \ + Parallel(tf1, tf2, Series(-tf1, tf3)).doit() == TransferFunction(k*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2)**2 + \ + (-a2*p + s)*(s**2 + 2*s*wn*zeta + wn**2) + (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), (a2*s + p)*(s**2 + \ + 2*s*wn*zeta + wn**2)**2, s) + assert Parallel(tf2, tf1, -tf3, evaluate=True) == Parallel(tf2, tf1, -tf3).doit() == \ + TransferFunction(a2*s + k*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2) + p + (-a2*p + s)*(s**2 + 2*s*wn*zeta + wn**2) \ + , (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s) + assert not Parallel(tf1, -tf2, evaluate=False) == Parallel(tf1, -tf2).doit() + + assert Parallel(Series(tf1, tf2), Series(tf2, tf3)).doit() == \ + TransferFunction(k*(a2*p - s)*(s**2 + 2*s*wn*zeta + wn**2) + k*(a2*s + p), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s) + assert Parallel(-tf1, -tf2, -tf3).doit() == \ + TransferFunction(-a2*s - k*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2) - p + (-a2*p + s)*(s**2 + 2*s*wn*zeta + wn**2), \ + (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s) + assert -Parallel(tf1, tf2, tf3).doit() == \ + TransferFunction(-a2*s - k*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2) - p - (a2*p - s)*(s**2 + 2*s*wn*zeta + wn**2), \ + (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s) + assert Parallel(tf2, tf3, Series(tf2, -tf1), tf3).doit() == \ + TransferFunction(k*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2) - k*(a2*s + p) + (2*a2*p - 2*s)*(s**2 + 2*s*wn*zeta \ + + wn**2), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s) + + assert Parallel(tf1, tf2).rewrite(TransferFunction) == \ + TransferFunction(k*(s**2 + 2*s*wn*zeta + wn**2) + 1, s**2 + 2*s*wn*zeta + wn**2, s) + assert Parallel(tf2, tf1, -tf3).rewrite(TransferFunction) == \ + TransferFunction(a2*s + k*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2) + p + (-a2*p + s)*(s**2 + 2*s*wn*zeta + \ + wn**2), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s) + + assert Parallel(tf1, Parallel(tf2, tf3)) == Parallel(tf1, tf2, tf3) == Parallel(Parallel(tf1, tf2), tf3) + + P1 = Parallel(Series(tf1, tf2), Series(tf2, tf3)) + assert P1.is_proper + assert not P1.is_strictly_proper + assert P1.is_biproper + + P2 = Parallel(tf1, -tf2, -tf3) + assert P2.is_proper + assert not P2.is_strictly_proper + assert P2.is_biproper + + P3 = Parallel(tf1, -tf2, Series(tf1, tf3)) + assert P3.is_proper + assert not P3.is_strictly_proper + assert P3.is_biproper + + +def test_MIMOParallel_functions(): + tf4 = TransferFunction(a0*p + p**a1 - s, p, p) + tf5 = TransferFunction(a1*s**2 + a2*s - a0, s + a0, s) + + tfm1 = TransferFunctionMatrix([[TF1], [TF2], [TF3]]) + tfm2 = TransferFunctionMatrix([[-TF2], [tf5], [-TF1]]) + tfm3 = TransferFunctionMatrix([[tf5], [-tf5], [TF2]]) + tfm4 = TransferFunctionMatrix([[TF2, -tf5], [TF1, tf5]]) + tfm5 = TransferFunctionMatrix([[TF1, TF2], [TF3, -tf5]]) + tfm6 = TransferFunctionMatrix([[-TF2]]) + tfm7 = TransferFunctionMatrix([[tf4], [-tf4], [tf4]]) + + assert tfm1 + tfm2 + tfm3 == MIMOParallel(tfm1, tfm2, tfm3) == MIMOParallel(MIMOParallel(tfm1, tfm2), tfm3) + assert tfm2 - tfm1 - tfm3 == MIMOParallel(tfm2, -tfm1, -tfm3) + assert tfm2 - tfm3 + (-tfm1*tfm6*-tfm6) == MIMOParallel(tfm2, -tfm3, MIMOSeries(-tfm6, tfm6, -tfm1)) + assert tfm1 + tfm1 - (-tfm1*tfm6) == MIMOParallel(tfm1, tfm1, -MIMOSeries(tfm6, -tfm1)) + assert tfm2 - tfm3 - tfm1 + tfm2 == MIMOParallel(tfm2, -tfm3, -tfm1, tfm2) + assert tfm1 + tfm2 - tfm3 - tfm1 == MIMOParallel(tfm1, tfm2, -tfm3, -tfm1) + raises(ValueError, lambda: tfm1 + tfm2 + TF2) + raises(TypeError, lambda: tfm1 - tfm2 - a1) + raises(TypeError, lambda: tfm2 - tfm3 - (s - 1)) + raises(TypeError, lambda: -tfm3 - tfm2 - 9) + raises(TypeError, lambda: (1 - p**3) - tfm3 - tfm2) + # All TFMs must use the same complex var. tfm7 uses 'p'. + raises(ValueError, lambda: tfm3 - tfm2 - tfm7) + raises(ValueError, lambda: tfm2 - tfm1 + tfm7) + # (tfm1 +/- tfm2) has (3, 1) shape while tfm4 has (2, 2) shape. + raises(TypeError, lambda: tfm1 + tfm2 + tfm4) + raises(TypeError, lambda: (tfm1 - tfm2) - tfm4) + + assert (tfm1 + tfm2)*tfm6 == MIMOSeries(tfm6, MIMOParallel(tfm1, tfm2)) + assert (tfm2 - tfm3)*tfm6*-tfm6 == MIMOSeries(-tfm6, tfm6, MIMOParallel(tfm2, -tfm3)) + assert (tfm2 - tfm1 - tfm3)*(tfm6 + tfm6) == MIMOSeries(MIMOParallel(tfm6, tfm6), MIMOParallel(tfm2, -tfm1, -tfm3)) + raises(ValueError, lambda: (tfm4 + tfm5)*TF1) + raises(TypeError, lambda: (tfm2 - tfm3)*a2) + raises(TypeError, lambda: (tfm3 + tfm2)*(s - 6)) + raises(TypeError, lambda: (tfm1 + tfm2 + tfm3)*0) + raises(TypeError, lambda: (1 - p**3)*(tfm1 + tfm3)) + + # (tfm3 - tfm2) has (3, 1) shape while tfm4*tfm5 has (2, 2) shape. + raises(ValueError, lambda: (tfm3 - tfm2)*tfm4*tfm5) + # (tfm1 - tfm2) has (3, 1) shape while tfm5 has (2, 2) shape. + raises(ValueError, lambda: (tfm1 - tfm2)*tfm5) + + # TFM in the arguments. + assert (MIMOParallel(tfm1, tfm2, evaluate=True) == MIMOParallel(tfm1, tfm2).doit() + == MIMOParallel(tfm1, tfm2).rewrite(TransferFunctionMatrix) + == TransferFunctionMatrix(((TransferFunction(-k*(s**2 + 2*s*wn*zeta + wn**2) + 1, s**2 + 2*s*wn*zeta + wn**2, s),), \ + (TransferFunction(-a0 + a1*s**2 + a2*s + k*(a0 + s), a0 + s, s),), (TransferFunction(-a2*s - p + (a2*p - s)* \ + (s**2 + 2*s*wn*zeta + wn**2), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s),)))) + + +def test_Feedback_construction(): + tf1 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s) + tf2 = TransferFunction(k, 1, s) + tf3 = TransferFunction(a2*p - s, a2*s + p, s) + tf4 = TransferFunction(a0*p + p**a1 - s, p, p) + tf5 = TransferFunction(a1*s**2 + a2*s - a0, s + a0, s) + tf6 = TransferFunction(s - p, p + s, p) + + f1 = Feedback(TransferFunction(1, 1, s), tf1*tf2*tf3) + assert f1.args == (TransferFunction(1, 1, s), Series(tf1, tf2, tf3), -1) + assert f1.sys1 == TransferFunction(1, 1, s) + assert f1.sys2 == Series(tf1, tf2, tf3) + assert f1.var == s + + f2 = Feedback(tf1, tf2*tf3) + assert f2.args == (tf1, Series(tf2, tf3), -1) + assert f2.sys1 == tf1 + assert f2.sys2 == Series(tf2, tf3) + assert f2.var == s + + f3 = Feedback(tf1*tf2, tf5) + assert f3.args == (Series(tf1, tf2), tf5, -1) + assert f3.sys1 == Series(tf1, tf2) + + f4 = Feedback(tf4, tf6) + assert f4.args == (tf4, tf6, -1) + assert f4.sys1 == tf4 + assert f4.var == p + + f5 = Feedback(tf5, TransferFunction(1, 1, s)) + assert f5.args == (tf5, TransferFunction(1, 1, s), -1) + assert f5.var == s + assert f5 == Feedback(tf5) # When sys2 is not passed explicitly, it is assumed to be unit tf. + + f6 = Feedback(TransferFunction(1, 1, p), tf4) + assert f6.args == (TransferFunction(1, 1, p), tf4, -1) + assert f6.var == p + + f7 = -Feedback(tf4*tf6, TransferFunction(1, 1, p)) + assert f7.args == (Series(TransferFunction(-1, 1, p), Series(tf4, tf6)), -TransferFunction(1, 1, p), -1) + assert f7.sys1 == Series(TransferFunction(-1, 1, p), Series(tf4, tf6)) + + # denominator can't be a Parallel instance + raises(TypeError, lambda: Feedback(tf1, tf2 + tf3)) + raises(TypeError, lambda: Feedback(tf1, Matrix([1, 2, 3]))) + raises(TypeError, lambda: Feedback(TransferFunction(1, 1, s), s - 1)) + raises(TypeError, lambda: Feedback(1, 1)) + # raises(ValueError, lambda: Feedback(TransferFunction(1, 1, s), TransferFunction(1, 1, s))) + raises(ValueError, lambda: Feedback(tf2, tf4*tf5)) + raises(ValueError, lambda: Feedback(tf2, tf1, 1.5)) # `sign` can only be -1 or 1 + raises(ValueError, lambda: Feedback(tf1, -tf1**-1)) # denominator can't be zero + raises(ValueError, lambda: Feedback(tf4, tf5)) # Both systems should use the same `var` + + +def test_Feedback_functions(): + tf = TransferFunction(1, 1, s) + tf1 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s) + tf2 = TransferFunction(k, 1, s) + tf3 = TransferFunction(a2*p - s, a2*s + p, s) + tf4 = TransferFunction(a0*p + p**a1 - s, p, p) + tf5 = TransferFunction(a1*s**2 + a2*s - a0, s + a0, s) + tf6 = TransferFunction(s - p, p + s, p) + + assert (tf1*tf2*tf3 / tf3*tf5) == Series(tf1, tf2, tf3, pow(tf3, -1), tf5) + assert (tf1*tf2*tf3) / (tf3*tf5) == Series((tf1*tf2*tf3).doit(), pow((tf3*tf5).doit(),-1)) + assert tf / (tf + tf1) == Feedback(tf, tf1) + assert tf / (tf + tf1*tf2*tf3) == Feedback(tf, tf1*tf2*tf3) + assert tf1 / (tf + tf1*tf2*tf3) == Feedback(tf1, tf2*tf3) + assert (tf1*tf2) / (tf + tf1*tf2) == Feedback(tf1*tf2, tf) + assert (tf1*tf2) / (tf + tf1*tf2*tf5) == Feedback(tf1*tf2, tf5) + assert (tf1*tf2) / (tf + tf1*tf2*tf5*tf3) in (Feedback(tf1*tf2, tf5*tf3), Feedback(tf1*tf2, tf3*tf5)) + assert tf4 / (TransferFunction(1, 1, p) + tf4*tf6) == Feedback(tf4, tf6) + assert tf5 / (tf + tf5) == Feedback(tf5, tf) + + raises(TypeError, lambda: tf1*tf2*tf3 / (1 + tf1*tf2*tf3)) + raises(ValueError, lambda: tf2*tf3 / (tf + tf2*tf3*tf4)) + + assert Feedback(tf, tf1*tf2*tf3).doit() == \ + TransferFunction((a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), k*(a2*p - s) + \ + (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s) + assert Feedback(tf, tf1*tf2*tf3).sensitivity == \ + 1/(k*(a2*p - s)/((a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2)) + 1) + assert Feedback(tf1, tf2*tf3).doit() == \ + TransferFunction((a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), (k*(a2*p - s) + \ + (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2))*(s**2 + 2*s*wn*zeta + wn**2), s) + assert Feedback(tf1, tf2*tf3).sensitivity == \ + 1/(k*(a2*p - s)/((a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2)) + 1) + assert Feedback(tf1*tf2, tf5).doit() == \ + TransferFunction(k*(a0 + s)*(s**2 + 2*s*wn*zeta + wn**2), (k*(-a0 + a1*s**2 + a2*s) + \ + (a0 + s)*(s**2 + 2*s*wn*zeta + wn**2))*(s**2 + 2*s*wn*zeta + wn**2), s) + assert Feedback(tf1*tf2, tf5, 1).sensitivity == \ + 1/(-k*(-a0 + a1*s**2 + a2*s)/((a0 + s)*(s**2 + 2*s*wn*zeta + wn**2)) + 1) + assert Feedback(tf4, tf6).doit() == \ + TransferFunction(p*(p + s)*(a0*p + p**a1 - s), p*(p*(p + s) + (-p + s)*(a0*p + p**a1 - s)), p) + assert -Feedback(tf4*tf6, TransferFunction(1, 1, p)).doit() == \ + TransferFunction(-p*(-p + s)*(p + s)*(a0*p + p**a1 - s), p*(p + s)*(p*(p + s) + (-p + s)*(a0*p + p**a1 - s)), p) + assert Feedback(tf, tf).doit() == TransferFunction(1, 2, s) + + assert Feedback(tf1, tf2*tf5).rewrite(TransferFunction) == \ + TransferFunction((a0 + s)*(s**2 + 2*s*wn*zeta + wn**2), (k*(-a0 + a1*s**2 + a2*s) + \ + (a0 + s)*(s**2 + 2*s*wn*zeta + wn**2))*(s**2 + 2*s*wn*zeta + wn**2), s) + assert Feedback(TransferFunction(1, 1, p), tf4).rewrite(TransferFunction) == \ + TransferFunction(p, a0*p + p + p**a1 - s, p) + + +def test_Feedback_with_Series(): + # Solves issue https://github.com/sympy/sympy/issues/26161 + tf1 = TransferFunction(s+1, 1, s) + tf2 = TransferFunction(s+2, 1, s) + fd1 = Feedback(tf1, tf2, -1) # Negative Feedback system + fd2 = Feedback(tf1, tf2, 1) # Positive Feedback system + unit = TransferFunction(1, 1, s) + + # Checking the type + assert isinstance(fd1, SISOLinearTimeInvariant) + assert isinstance(fd1, Feedback) + + # Testing the numerator and denominator + assert fd1.num == tf1 + assert fd2.num == tf1 + assert fd1.den == Parallel(unit, Series(tf2, tf1)) + assert fd2.den == Parallel(unit, -Series(tf2, tf1)) + + # Testing the Series and Parallel Combination with Feedback and TransferFunction + s1 = Series(tf1, fd1) + p1 = Parallel(tf1, fd1) + assert tf1 * fd1 == s1 + assert tf1 + fd1 == p1 + assert s1.doit() == TransferFunction((s + 1)**2, (s + 1)*(s + 2) + 1, s) + assert p1.doit() == TransferFunction(s + (s + 1)*((s + 1)*(s + 2) + 1) + 1, (s + 1)*(s + 2) + 1, s) + + # Testing the use of Feedback and TransferFunction with Feedback + fd3 = Feedback(tf1*fd1, tf2, -1) + assert fd3 == Feedback(Series(tf1, fd1), tf2) + assert fd3.num == tf1 * fd1 + assert fd3.den == Parallel(unit, Series(tf2, Series(tf1, fd1))) + + # Testing the use of Feedback and TransferFunction with TransferFunction + tf3 = TransferFunction(tf1*fd1, tf2, s) + assert tf3 == TransferFunction(Series(tf1, fd1), tf2, s) + assert tf3.num == tf1*fd1 + + +def test_issue_26161(): + # Issue https://github.com/sympy/sympy/issues/26161 + Ib, Is, m, h, l2, l1 = symbols('I_b, I_s, m, h, l2, l1', + real=True, nonnegative=True) + KD, KP, v = symbols('K_D, K_P, v', real=True) + + tau1_sq = (Ib + m * h ** 2) / m / g / h + tau2 = l2 / v + tau3 = v / (l1 + l2) + K = v ** 2 / g / (l1 + l2) + + Gtheta = TransferFunction(-K * (tau2 * s + 1), tau1_sq * s ** 2 - 1, s) + Gdelta = TransferFunction(1, Is * s ** 2 + c * s, s) + Gpsi = TransferFunction(1, tau3 * s, s) + Dcont = TransferFunction(KD * s, 1, s) + PIcont = TransferFunction(KP, s, s) + Gunity = TransferFunction(1, 1, s) + + Ginner = Feedback(Dcont * Gdelta, Gtheta) + Gouter = Feedback(PIcont * Ginner * Gpsi, Gunity) + assert Gouter == Feedback(Series(PIcont, Series(Ginner, Gpsi)), Gunity) + assert Gouter.num == Series(PIcont, Series(Ginner, Gpsi)) + assert Gouter.den == Parallel(Gunity, Series(Gunity, Series(PIcont, Series(Ginner, Gpsi)))) + expr = (KD*KP*g*s**3*v**2*(l1 + l2)*(Is*s**2 + c*s)**2*(-g*h*m + s**2*(Ib + h**2*m))*(-KD*g*h*m*s*v**2*(l2*s + v) + \ + g*v*(l1 + l2)*(Is*s**2 + c*s)*(-g*h*m + s**2*(Ib + h**2*m))))/((s**2*v*(Is*s**2 + c*s)*(-KD*g*h*m*s*v**2* \ + (l2*s + v) + g*v*(l1 + l2)*(Is*s**2 + c*s)*(-g*h*m + s**2*(Ib + h**2*m)))*(KD*KP*g*s*v*(l1 + l2)**2* \ + (Is*s**2 + c*s)*(-g*h*m + s**2*(Ib + h**2*m)) + s**2*v*(Is*s**2 + c*s)*(-KD*g*h*m*s*v**2*(l2*s + v) + \ + g*v*(l1 + l2)*(Is*s**2 + c*s)*(-g*h*m + s**2*(Ib + h**2*m))))/(l1 + l2))) + + assert (Gouter.to_expr() - expr).simplify() == 0 + + +def test_MIMOFeedback_construction(): + tf1 = TransferFunction(1, s, s) + tf2 = TransferFunction(s, s**3 - 1, s) + tf3 = TransferFunction(s, s + 1, s) + tf4 = TransferFunction(s, s**2 + 1, s) + + tfm_1 = TransferFunctionMatrix([[tf1, tf2], [tf3, tf4]]) + tfm_2 = TransferFunctionMatrix([[tf2, tf3], [tf4, tf1]]) + tfm_3 = TransferFunctionMatrix([[tf3, tf4], [tf1, tf2]]) + + f1 = MIMOFeedback(tfm_1, tfm_2) + assert f1.args == (tfm_1, tfm_2, -1) + assert f1.sys1 == tfm_1 + assert f1.sys2 == tfm_2 + assert f1.var == s + assert f1.sign == -1 + assert -(-f1) == f1 + + f2 = MIMOFeedback(tfm_2, tfm_1, 1) + assert f2.args == (tfm_2, tfm_1, 1) + assert f2.sys1 == tfm_2 + assert f2.sys2 == tfm_1 + assert f2.var == s + assert f2.sign == 1 + + f3 = MIMOFeedback(tfm_1, MIMOSeries(tfm_3, tfm_2)) + assert f3.args == (tfm_1, MIMOSeries(tfm_3, tfm_2), -1) + assert f3.sys1 == tfm_1 + assert f3.sys2 == MIMOSeries(tfm_3, tfm_2) + assert f3.var == s + assert f3.sign == -1 + + mat = Matrix([[1, 1/s], [0, 1]]) + sys1 = controller = TransferFunctionMatrix.from_Matrix(mat, s) + f4 = MIMOFeedback(sys1, controller) + assert f4.args == (sys1, controller, -1) + assert f4.sys1 == f4.sys2 == sys1 + + +def test_MIMOFeedback_errors(): + tf1 = TransferFunction(1, s, s) + tf2 = TransferFunction(s, s**3 - 1, s) + tf3 = TransferFunction(s, s - 1, s) + tf4 = TransferFunction(s, s**2 + 1, s) + tf5 = TransferFunction(1, 1, s) + tf6 = TransferFunction(-1, s - 1, s) + + tfm_1 = TransferFunctionMatrix([[tf1, tf2], [tf3, tf4]]) + tfm_2 = TransferFunctionMatrix([[tf2, tf3], [tf4, tf1]]) + tfm_3 = TransferFunctionMatrix.from_Matrix(eye(2), var=s) + tfm_4 = TransferFunctionMatrix([[tf1, tf5], [tf5, tf5]]) + tfm_5 = TransferFunctionMatrix([[-tf3, tf3], [tf3, tf6]]) + # tfm_4 is inverse of tfm_5. Therefore tfm_5*tfm_4 = I + tfm_6 = TransferFunctionMatrix([[-tf3]]) + tfm_7 = TransferFunctionMatrix([[tf3, tf4]]) + + # Unsupported Types + raises(TypeError, lambda: MIMOFeedback(tf1, tf2)) + raises(TypeError, lambda: MIMOFeedback(MIMOParallel(tfm_1, tfm_2), tfm_3)) + # Shape Errors + raises(ValueError, lambda: MIMOFeedback(tfm_1, tfm_6, 1)) + raises(ValueError, lambda: MIMOFeedback(tfm_7, tfm_7)) + # sign not 1/-1 + raises(ValueError, lambda: MIMOFeedback(tfm_1, tfm_2, -2)) + # Non-Invertible Systems + raises(ValueError, lambda: MIMOFeedback(tfm_5, tfm_4, 1)) + raises(ValueError, lambda: MIMOFeedback(tfm_4, -tfm_5)) + raises(ValueError, lambda: MIMOFeedback(tfm_3, tfm_3, 1)) + # Variable not same in both the systems + tfm_8 = TransferFunctionMatrix.from_Matrix(eye(2), var=p) + raises(ValueError, lambda: MIMOFeedback(tfm_1, tfm_8, 1)) + + +def test_MIMOFeedback_functions(): + tf1 = TransferFunction(1, s, s) + tf2 = TransferFunction(s, s - 1, s) + tf3 = TransferFunction(1, 1, s) + tf4 = TransferFunction(-1, s - 1, s) + + tfm_1 = TransferFunctionMatrix.from_Matrix(eye(2), var=s) + tfm_2 = TransferFunctionMatrix([[tf1, tf3], [tf3, tf3]]) + tfm_3 = TransferFunctionMatrix([[-tf2, tf2], [tf2, tf4]]) + tfm_4 = TransferFunctionMatrix([[tf1, tf2], [-tf2, tf1]]) + + # sensitivity, doit(), rewrite() + F_1 = MIMOFeedback(tfm_2, tfm_3) + F_2 = MIMOFeedback(tfm_2, MIMOSeries(tfm_4, -tfm_1), 1) + + assert F_1.sensitivity == Matrix([[S.Half, 0], [0, S.Half]]) + assert F_2.sensitivity == Matrix([[(-2*s**4 + s**2)/(s**2 - s + 1), + (2*s**3 - s**2)/(s**2 - s + 1)], [-s**2, s]]) + + assert F_1.doit() == \ + TransferFunctionMatrix(((TransferFunction(1, 2*s, s), + TransferFunction(1, 2, s)), (TransferFunction(1, 2, s), + TransferFunction(1, 2, s)))) == F_1.rewrite(TransferFunctionMatrix) + assert F_2.doit(cancel=False, expand=True) == \ + TransferFunctionMatrix(((TransferFunction(-s**5 + 2*s**4 - 2*s**3 + s**2, s**5 - 2*s**4 + 3*s**3 - 2*s**2 + s, s), + TransferFunction(-2*s**4 + 2*s**3, s**2 - s + 1, s)), (TransferFunction(0, 1, s), TransferFunction(-s**2 + s, 1, s)))) + assert F_2.doit(cancel=False) == \ + TransferFunctionMatrix(((TransferFunction(s*(2*s**3 - s**2)*(s**2 - s + 1) + \ + (-2*s**4 + s**2)*(s**2 - s + 1), s*(s**2 - s + 1)**2, s), TransferFunction(-2*s**4 + 2*s**3, s**2 - s + 1, s)), + (TransferFunction(0, 1, s), TransferFunction(-s**2 + s, 1, s)))) + assert F_2.doit() == \ + TransferFunctionMatrix(((TransferFunction(s*(-2*s**2 + s*(2*s - 1) + 1), s**2 - s + 1, s), + TransferFunction(-2*s**3*(s - 1), s**2 - s + 1, s)), (TransferFunction(0, 1, s), TransferFunction(s*(1 - s), 1, s)))) + assert F_2.doit(expand=True) == \ + TransferFunctionMatrix(((TransferFunction(-s**2 + s, s**2 - s + 1, s), TransferFunction(-2*s**4 + 2*s**3, s**2 - s + 1, s)), + (TransferFunction(0, 1, s), TransferFunction(-s**2 + s, 1, s)))) + + assert -(F_1.doit()) == (-F_1).doit() # First negating then calculating vs calculating then negating. + + +def test_TransferFunctionMatrix_construction(): + tf5 = TransferFunction(a1*s**2 + a2*s - a0, s + a0, s) + tf4 = TransferFunction(a0*p + p**a1 - s, p, p) + + tfm3_ = TransferFunctionMatrix([[-TF3]]) + assert tfm3_.shape == (tfm3_.num_outputs, tfm3_.num_inputs) == (1, 1) + assert tfm3_.args == Tuple(Tuple(Tuple(-TF3))) + assert tfm3_.var == s + + tfm5 = TransferFunctionMatrix([[TF1, -TF2], [TF3, tf5]]) + assert tfm5.shape == (tfm5.num_outputs, tfm5.num_inputs) == (2, 2) + assert tfm5.args == Tuple(Tuple(Tuple(TF1, -TF2), Tuple(TF3, tf5))) + assert tfm5.var == s + + tfm7 = TransferFunctionMatrix([[TF1, TF2], [TF3, -tf5], [-tf5, TF2]]) + assert tfm7.shape == (tfm7.num_outputs, tfm7.num_inputs) == (3, 2) + assert tfm7.args == Tuple(Tuple(Tuple(TF1, TF2), Tuple(TF3, -tf5), Tuple(-tf5, TF2))) + assert tfm7.var == s + + # all transfer functions will use the same complex variable. tf4 uses 'p'. + raises(ValueError, lambda: TransferFunctionMatrix([[TF1], [TF2], [tf4]])) + raises(ValueError, lambda: TransferFunctionMatrix([[TF1, tf4], [TF3, tf5]])) + + # length of all the lists in the TFM should be equal. + raises(ValueError, lambda: TransferFunctionMatrix([[TF1], [TF3, tf5]])) + raises(ValueError, lambda: TransferFunctionMatrix([[TF1, TF3], [tf5]])) + + # lists should only support transfer functions in them. + raises(TypeError, lambda: TransferFunctionMatrix([[TF1, TF2], [TF3, Matrix([1, 2])]])) + raises(TypeError, lambda: TransferFunctionMatrix([[TF1, Matrix([1, 2])], [TF3, TF2]])) + + # `arg` should strictly be nested list of TransferFunction + raises(ValueError, lambda: TransferFunctionMatrix([TF1, TF2, tf5])) + raises(ValueError, lambda: TransferFunctionMatrix([TF1])) + +def test_TransferFunctionMatrix_functions(): + tf5 = TransferFunction(a1*s**2 + a2*s - a0, s + a0, s) + + # Classmethod (from_matrix) + + mat_1 = ImmutableMatrix([ + [s*(s + 1)*(s - 3)/(s**4 + 1), 2], + [p, p*(s + 1)/(s*(s**1 + 1))] + ]) + mat_2 = ImmutableMatrix([[(2*s + 1)/(s**2 - 9)]]) + mat_3 = ImmutableMatrix([[1, 2], [3, 4]]) + assert TransferFunctionMatrix.from_Matrix(mat_1, s) == \ + TransferFunctionMatrix([[TransferFunction(s*(s - 3)*(s + 1), s**4 + 1, s), TransferFunction(2, 1, s)], + [TransferFunction(p, 1, s), TransferFunction(p, s, s)]]) + assert TransferFunctionMatrix.from_Matrix(mat_2, s) == \ + TransferFunctionMatrix([[TransferFunction(2*s + 1, s**2 - 9, s)]]) + assert TransferFunctionMatrix.from_Matrix(mat_3, p) == \ + TransferFunctionMatrix([[TransferFunction(1, 1, p), TransferFunction(2, 1, p)], + [TransferFunction(3, 1, p), TransferFunction(4, 1, p)]]) + + # Negating a TFM + + tfm1 = TransferFunctionMatrix([[TF1], [TF2]]) + assert -tfm1 == TransferFunctionMatrix([[-TF1], [-TF2]]) + + tfm2 = TransferFunctionMatrix([[TF1, TF2, TF3], [tf5, -TF1, -TF3]]) + assert -tfm2 == TransferFunctionMatrix([[-TF1, -TF2, -TF3], [-tf5, TF1, TF3]]) + + # subs() + + H_1 = TransferFunctionMatrix.from_Matrix(mat_1, s) + H_2 = TransferFunctionMatrix([[TransferFunction(a*p*s, k*s**2, s), TransferFunction(p*s, k*(s**2 - a), s)]]) + assert H_1.subs(p, 1) == TransferFunctionMatrix([[TransferFunction(s*(s - 3)*(s + 1), s**4 + 1, s), TransferFunction(2, 1, s)], [TransferFunction(1, 1, s), TransferFunction(1, s, s)]]) + assert H_1.subs({p: 1}) == TransferFunctionMatrix([[TransferFunction(s*(s - 3)*(s + 1), s**4 + 1, s), TransferFunction(2, 1, s)], [TransferFunction(1, 1, s), TransferFunction(1, s, s)]]) + assert H_1.subs({p: 1, s: 1}) == TransferFunctionMatrix([[TransferFunction(s*(s - 3)*(s + 1), s**4 + 1, s), TransferFunction(2, 1, s)], [TransferFunction(1, 1, s), TransferFunction(1, s, s)]]) # This should ignore `s` as it is `var` + assert H_2.subs(p, 2) == TransferFunctionMatrix([[TransferFunction(2*a*s, k*s**2, s), TransferFunction(2*s, k*(-a + s**2), s)]]) + assert H_2.subs(k, 1) == TransferFunctionMatrix([[TransferFunction(a*p*s, s**2, s), TransferFunction(p*s, -a + s**2, s)]]) + assert H_2.subs(a, 0) == TransferFunctionMatrix([[TransferFunction(0, k*s**2, s), TransferFunction(p*s, k*s**2, s)]]) + assert H_2.subs({p: 1, k: 1, a: a0}) == TransferFunctionMatrix([[TransferFunction(a0*s, s**2, s), TransferFunction(s, -a0 + s**2, s)]]) + + # eval_frequency() + assert H_2.eval_frequency(S(1)/2 + I) == Matrix([[2*a*p/(5*k) - 4*I*a*p/(5*k), I*p/(-a*k - 3*k/4 + I*k) + p/(-2*a*k - 3*k/2 + 2*I*k)]]) + + # transpose() + + assert H_1.transpose() == TransferFunctionMatrix([[TransferFunction(s*(s - 3)*(s + 1), s**4 + 1, s), TransferFunction(p, 1, s)], [TransferFunction(2, 1, s), TransferFunction(p, s, s)]]) + assert H_2.transpose() == TransferFunctionMatrix([[TransferFunction(a*p*s, k*s**2, s)], [TransferFunction(p*s, k*(-a + s**2), s)]]) + assert H_1.transpose().transpose() == H_1 + assert H_2.transpose().transpose() == H_2 + + # elem_poles() + + assert H_1.elem_poles() == [[[-sqrt(2)/2 - sqrt(2)*I/2, -sqrt(2)/2 + sqrt(2)*I/2, sqrt(2)/2 - sqrt(2)*I/2, sqrt(2)/2 + sqrt(2)*I/2], []], + [[], [0]]] + assert H_2.elem_poles() == [[[0, 0], [sqrt(a), -sqrt(a)]]] + assert tfm2.elem_poles() == [[[wn*(-zeta + sqrt((zeta - 1)*(zeta + 1))), wn*(-zeta - sqrt((zeta - 1)*(zeta + 1)))], [], [-p/a2]], + [[-a0], [wn*(-zeta + sqrt((zeta - 1)*(zeta + 1))), wn*(-zeta - sqrt((zeta - 1)*(zeta + 1)))], [-p/a2]]] + + # elem_zeros() + + assert H_1.elem_zeros() == [[[-1, 0, 3], []], [[], []]] + assert H_2.elem_zeros() == [[[0], [0]]] + assert tfm2.elem_zeros() == [[[], [], [a2*p]], + [[-a2/(2*a1) - sqrt(4*a0*a1 + a2**2)/(2*a1), -a2/(2*a1) + sqrt(4*a0*a1 + a2**2)/(2*a1)], [], [a2*p]]] + + # doit() + + H_3 = TransferFunctionMatrix([[Series(TransferFunction(1, s**3 - 3, s), TransferFunction(s**2 - 2*s + 5, 1, s), TransferFunction(1, s, s))]]) + H_4 = TransferFunctionMatrix([[Parallel(TransferFunction(s**3 - 3, 4*s**4 - s**2 - 2*s + 5, s), TransferFunction(4 - s**3, 4*s**4 - s**2 - 2*s + 5, s))]]) + + assert H_3.doit() == TransferFunctionMatrix([[TransferFunction(s**2 - 2*s + 5, s*(s**3 - 3), s)]]) + assert H_4.doit() == TransferFunctionMatrix([[TransferFunction(1, 4*s**4 - s**2 - 2*s + 5, s)]]) + + # _flat() + + assert H_1._flat() == [TransferFunction(s*(s - 3)*(s + 1), s**4 + 1, s), TransferFunction(2, 1, s), TransferFunction(p, 1, s), TransferFunction(p, s, s)] + assert H_2._flat() == [TransferFunction(a*p*s, k*s**2, s), TransferFunction(p*s, k*(-a + s**2), s)] + assert H_3._flat() == [Series(TransferFunction(1, s**3 - 3, s), TransferFunction(s**2 - 2*s + 5, 1, s), TransferFunction(1, s, s))] + assert H_4._flat() == [Parallel(TransferFunction(s**3 - 3, 4*s**4 - s**2 - 2*s + 5, s), TransferFunction(4 - s**3, 4*s**4 - s**2 - 2*s + 5, s))] + + # evalf() + + assert H_1.evalf() == \ + TransferFunctionMatrix(((TransferFunction(s*(s - 3.0)*(s + 1.0), s**4 + 1.0, s), TransferFunction(2.0, 1, s)), (TransferFunction(1.0*p, 1, s), TransferFunction(p, s, s)))) + assert H_2.subs({a:3.141, p:2.88, k:2}).evalf() == \ + TransferFunctionMatrix(((TransferFunction(4.5230399999999999494093572138808667659759521484375, s, s), + TransferFunction(2.87999999999999989341858963598497211933135986328125*s, 2.0*s**2 - 6.282000000000000028421709430404007434844970703125, s)),)) + + # simplify() + + H_5 = TransferFunctionMatrix([[TransferFunction(s**5 + s**3 + s, s - s**2, s), + TransferFunction((s + 3)*(s - 1), (s - 1)*(s + 5), s)]]) + + assert H_5.simplify() == simplify(H_5) == \ + TransferFunctionMatrix(((TransferFunction(-s**4 - s**2 - 1, s - 1, s), TransferFunction(s + 3, s + 5, s)),)) + + # expand() + + assert (H_1.expand() + == TransferFunctionMatrix(((TransferFunction(s**3 - 2*s**2 - 3*s, s**4 + 1, s), TransferFunction(2, 1, s)), + (TransferFunction(p, 1, s), TransferFunction(p, s, s))))) + assert H_5.expand() == \ + TransferFunctionMatrix(((TransferFunction(s**5 + s**3 + s, -s**2 + s, s), TransferFunction(s**2 + 2*s - 3, s**2 + 4*s - 5, s)),)) + +def test_TransferFunction_gbt(): + # simple transfer function, e.g. ohms law + tf = TransferFunction(1, a*s+b, s) + numZ, denZ = gbt(tf, T, 0.5) + # discretized transfer function with coefs from tf.gbt() + tf_test_bilinear = TransferFunction(s * numZ[0] + numZ[1], s * denZ[0] + denZ[1], s) + # corresponding tf with manually calculated coefs + tf_test_manual = TransferFunction(s * T/(2*(a + b*T/2)) + T/(2*(a + b*T/2)), s + (-a + b*T/2)/(a + b*T/2), s) + + assert S.Zero == (tf_test_bilinear.simplify()-tf_test_manual.simplify()).simplify().num + + tf = TransferFunction(1, a*s+b, s) + numZ, denZ = gbt(tf, T, 0) + # discretized transfer function with coefs from tf.gbt() + tf_test_forward = TransferFunction(numZ[0], s*denZ[0]+denZ[1], s) + # corresponding tf with manually calculated coefs + tf_test_manual = TransferFunction(T/a, s + (-a + b*T)/a, s) + + assert S.Zero == (tf_test_forward.simplify()-tf_test_manual.simplify()).simplify().num + + tf = TransferFunction(1, a*s+b, s) + numZ, denZ = gbt(tf, T, 1) + # discretized transfer function with coefs from tf.gbt() + tf_test_backward = TransferFunction(s*numZ[0], s*denZ[0]+denZ[1], s) + # corresponding tf with manually calculated coefs + tf_test_manual = TransferFunction(s * T/(a + b*T), s - a/(a + b*T), s) + + assert S.Zero == (tf_test_backward.simplify()-tf_test_manual.simplify()).simplify().num + + tf = TransferFunction(1, a*s+b, s) + numZ, denZ = gbt(tf, T, 0.3) + # discretized transfer function with coefs from tf.gbt() + tf_test_gbt = TransferFunction(s*numZ[0]+numZ[1], s*denZ[0]+denZ[1], s) + # corresponding tf with manually calculated coefs + tf_test_manual = TransferFunction(s*3*T/(10*(a + 3*b*T/10)) + 7*T/(10*(a + 3*b*T/10)), s + (-a + 7*b*T/10)/(a + 3*b*T/10), s) + + assert S.Zero == (tf_test_gbt.simplify()-tf_test_manual.simplify()).simplify().num + +def test_TransferFunction_bilinear(): + # simple transfer function, e.g. ohms law + tf = TransferFunction(1, a*s+b, s) + numZ, denZ = bilinear(tf, T) + # discretized transfer function with coefs from tf.bilinear() + tf_test_bilinear = TransferFunction(s*numZ[0]+numZ[1], s*denZ[0]+denZ[1], s) + # corresponding tf with manually calculated coefs + tf_test_manual = TransferFunction(s * T/(2*(a + b*T/2)) + T/(2*(a + b*T/2)), s + (-a + b*T/2)/(a + b*T/2), s) + + assert S.Zero == (tf_test_bilinear.simplify()-tf_test_manual.simplify()).simplify().num + +def test_TransferFunction_forward_diff(): + # simple transfer function, e.g. ohms law + tf = TransferFunction(1, a*s+b, s) + numZ, denZ = forward_diff(tf, T) + # discretized transfer function with coefs from tf.forward_diff() + tf_test_forward = TransferFunction(numZ[0], s*denZ[0]+denZ[1], s) + # corresponding tf with manually calculated coefs + tf_test_manual = TransferFunction(T/a, s + (-a + b*T)/a, s) + + assert S.Zero == (tf_test_forward.simplify()-tf_test_manual.simplify()).simplify().num + +def test_TransferFunction_backward_diff(): + # simple transfer function, e.g. ohms law + tf = TransferFunction(1, a*s+b, s) + numZ, denZ = backward_diff(tf, T) + # discretized transfer function with coefs from tf.backward_diff() + tf_test_backward = TransferFunction(s*numZ[0]+numZ[1], s*denZ[0]+denZ[1], s) + # corresponding tf with manually calculated coefs + tf_test_manual = TransferFunction(s * T/(a + b*T), s - a/(a + b*T), s) + + assert S.Zero == (tf_test_backward.simplify()-tf_test_manual.simplify()).simplify().num + +def test_TransferFunction_phase_margin(): + # Test for phase margin + tf1 = TransferFunction(10, p**3 + 1, p) + tf2 = TransferFunction(s**2, 10, s) + tf3 = TransferFunction(1, a*s+b, s) + tf4 = TransferFunction((s + 1)*exp(s/tau), s**2 + 2, s) + tf_m = TransferFunctionMatrix([[tf2],[tf3]]) + + assert phase_margin(tf1) == -180 + 180*atan(3*sqrt(11))/pi + assert phase_margin(tf2) == 0 + + raises(NotImplementedError, lambda: phase_margin(tf4)) + raises(ValueError, lambda: phase_margin(tf3)) + raises(ValueError, lambda: phase_margin(MIMOSeries(tf_m))) + +def test_TransferFunction_gain_margin(): + # Test for gain margin + tf1 = TransferFunction(s**2, 5*(s+1)*(s-5)*(s-10), s) + tf2 = TransferFunction(s**2 + 2*s + 1, 1, s) + tf3 = TransferFunction(1, a*s+b, s) + tf4 = TransferFunction((s + 1)*exp(s/tau), s**2 + 2, s) + tf_m = TransferFunctionMatrix([[tf2],[tf3]]) + + assert gain_margin(tf1) == -20*log(S(7)/540)/log(10) + assert gain_margin(tf2) == oo + + raises(NotImplementedError, lambda: gain_margin(tf4)) + raises(ValueError, lambda: gain_margin(tf3)) + raises(ValueError, lambda: gain_margin(MIMOSeries(tf_m))) + + +def test_StateSpace_construction(): + # using different numbers for a SISO system. + A1 = Matrix([[0, 1], [1, 0]]) + B1 = Matrix([1, 0]) + C1 = Matrix([[0, 1]]) + D1 = Matrix([0]) + ss1 = StateSpace(A1, B1, C1, D1) + + assert ss1.state_matrix == Matrix([[0, 1], [1, 0]]) + assert ss1.input_matrix == Matrix([1, 0]) + assert ss1.output_matrix == Matrix([[0, 1]]) + assert ss1.feedforward_matrix == Matrix([0]) + assert ss1.args == (Matrix([[0, 1], [1, 0]]), Matrix([[1], [0]]), Matrix([[0, 1]]), Matrix([[0]])) + + # using different symbols for a SISO system. + ss2 = StateSpace(Matrix([a0]), Matrix([a1]), + Matrix([a2]), Matrix([a3])) + + assert ss2.state_matrix == Matrix([[a0]]) + assert ss2.input_matrix == Matrix([[a1]]) + assert ss2.output_matrix == Matrix([[a2]]) + assert ss2.feedforward_matrix == Matrix([[a3]]) + assert ss2.args == (Matrix([[a0]]), Matrix([[a1]]), Matrix([[a2]]), Matrix([[a3]])) + + # using different numbers for a MIMO system. + ss3 = StateSpace(Matrix([[-1.5, -2], [1, 0]]), + Matrix([[0.5, 0], [0, 1]]), + Matrix([[0, 1], [0, 2]]), + Matrix([[2, 2], [1, 1]])) + + assert ss3.state_matrix == Matrix([[-1.5, -2], [1, 0]]) + assert ss3.input_matrix == Matrix([[0.5, 0], [0, 1]]) + assert ss3.output_matrix == Matrix([[0, 1], [0, 2]]) + assert ss3.feedforward_matrix == Matrix([[2, 2], [1, 1]]) + assert ss3.args == (Matrix([[-1.5, -2], + [1, 0]]), + Matrix([[0.5, 0], + [0, 1]]), + Matrix([[0, 1], + [0, 2]]), + Matrix([[2, 2], + [1, 1]])) + + # using different symbols for a MIMO system. + A4 = Matrix([[a0, a1], [a2, a3]]) + B4 = Matrix([[b0, b1], [b2, b3]]) + C4 = Matrix([[c0, c1], [c2, c3]]) + D4 = Matrix([[d0, d1], [d2, d3]]) + ss4 = StateSpace(A4, B4, C4, D4) + + assert ss4.state_matrix == Matrix([[a0, a1], [a2, a3]]) + assert ss4.input_matrix == Matrix([[b0, b1], [b2, b3]]) + assert ss4.output_matrix == Matrix([[c0, c1], [c2, c3]]) + assert ss4.feedforward_matrix == Matrix([[d0, d1], [d2, d3]]) + assert ss4.args == (Matrix([[a0, a1], + [a2, a3]]), + Matrix([[b0, b1], + [b2, b3]]), + Matrix([[c0, c1], + [c2, c3]]), + Matrix([[d0, d1], + [d2, d3]])) + + # using less matrices. Rest will be filled with a minimum of zeros. + ss5 = StateSpace() + assert ss5.args == (Matrix([[0]]), Matrix([[0]]), Matrix([[0]]), Matrix([[0]])) + + A6 = Matrix([[0, 1], [1, 0]]) + B6 = Matrix([1, 1]) + ss6 = StateSpace(A6, B6) + + assert ss6.state_matrix == Matrix([[0, 1], [1, 0]]) + assert ss6.input_matrix == Matrix([1, 1]) + assert ss6.output_matrix == Matrix([[0, 0]]) + assert ss6.feedforward_matrix == Matrix([[0]]) + assert ss6.args == (Matrix([[0, 1], + [1, 0]]), + Matrix([[1], + [1]]), + Matrix([[0, 0]]), + Matrix([[0]])) + + # Check if the system is SISO or MIMO. + # If system is not SISO, then it is definitely MIMO. + + assert ss1.is_SISO == True + assert ss2.is_SISO == True + assert ss3.is_SISO == False + assert ss4.is_SISO == False + assert ss5.is_SISO == True + assert ss6.is_SISO == True + + # ShapeError if matrices do not fit. + raises(ShapeError, lambda: StateSpace(Matrix([s, (s+1)**2]), Matrix([s+1]), + Matrix([s**2 - 1]), Matrix([2*s]))) + raises(ShapeError, lambda: StateSpace(Matrix([s]), Matrix([s+1, s**3 + 1]), + Matrix([s**2 - 1]), Matrix([2*s]))) + raises(ShapeError, lambda: StateSpace(Matrix([s]), Matrix([s+1]), + Matrix([[s**2 - 1], [s**2 + 2*s + 1]]), Matrix([2*s]))) + raises(ShapeError, lambda: StateSpace(Matrix([[-s, -s], [s, 0]]), + Matrix([[s/2, 0], [0, s]]), + Matrix([[0, s]]), + Matrix([[2*s, 2*s], [s, s]]))) + + # TypeError if arguments are not sympy matrices. + raises(TypeError, lambda: StateSpace(s**2, s+1, 2*s, 1)) + raises(TypeError, lambda: StateSpace(Matrix([2, 0.5]), Matrix([-1]), + Matrix([1]), 0)) +def test_StateSpace_add(): + A1 = Matrix([[4, 1],[2, -3]]) + B1 = Matrix([[5, 2],[-3, -3]]) + C1 = Matrix([[2, -4],[0, 1]]) + D1 = Matrix([[3, 2],[1, -1]]) + ss1 = StateSpace(A1, B1, C1, D1) + + A2 = Matrix([[-3, 4, 2],[-1, -3, 0],[2, 5, 3]]) + B2 = Matrix([[1, 4],[-3, -3],[-2, 1]]) + C2 = Matrix([[4, 2, -3],[1, 4, 3]]) + D2 = Matrix([[-2, 4],[0, 1]]) + ss2 = StateSpace(A2, B2, C2, D2) + ss3 = StateSpace() + ss4 = StateSpace(Matrix([1]), Matrix([2]), Matrix([3]), Matrix([4])) + + expected_add = \ + StateSpace( + Matrix([ + [4, 1, 0, 0, 0], + [2, -3, 0, 0, 0], + [0, 0, -3, 4, 2], + [0, 0, -1, -3, 0], + [0, 0, 2, 5, 3]]), + Matrix([ + [ 5, 2], + [-3, -3], + [ 1, 4], + [-3, -3], + [-2, 1]]), + Matrix([ + [2, -4, 4, 2, -3], + [0, 1, 1, 4, 3]]), + Matrix([ + [1, 6], + [1, 0]])) + + expected_mul = \ + StateSpace( + Matrix([ + [ -3, 4, 2, 0, 0], + [ -1, -3, 0, 0, 0], + [ 2, 5, 3, 0, 0], + [ 22, 18, -9, 4, 1], + [-15, -18, 0, 2, -3]]), + Matrix([ + [ 1, 4], + [ -3, -3], + [ -2, 1], + [-10, 22], + [ 6, -15]]), + Matrix([ + [14, 14, -3, 2, -4], + [ 3, -2, -6, 0, 1]]), + Matrix([ + [-6, 14], + [-2, 3]])) + + assert ss1 + ss2 == expected_add + assert ss1*ss2 == expected_mul + assert ss3 + 1/2 == StateSpace(Matrix([[0]]), Matrix([[0]]), Matrix([[0]]), Matrix([[0.5]])) + assert ss4*1.5 == StateSpace(Matrix([[1]]), Matrix([[2]]), Matrix([[4.5]]), Matrix([[6.0]])) + assert 1.5*ss4 == StateSpace(Matrix([[1]]), Matrix([[3.0]]), Matrix([[3]]), Matrix([[6.0]])) + raises(ShapeError, lambda: ss1 + ss3) + raises(ShapeError, lambda: ss2*ss4) + +def test_StateSpace_negation(): + A = Matrix([[a0, a1], [a2, a3]]) + B = Matrix([[b0, b1], [b2, b3]]) + C = Matrix([[c0, c1], [c1, c2], [c2, c3]]) + D = Matrix([[d0, d1], [d1, d2], [d2, d3]]) + SS = StateSpace(A, B, C, D) + SS_neg = -SS + + state_mat = Matrix([[-1, 1], [1, -1]]) + input_mat = Matrix([1, -1]) + output_mat = Matrix([[-1, 1]]) + feedforward_mat = Matrix([1]) + system = StateSpace(state_mat, input_mat, output_mat, feedforward_mat) + + assert SS_neg == \ + StateSpace(Matrix([[a0, a1], + [a2, a3]]), + Matrix([[b0, b1], + [b2, b3]]), + Matrix([[-c0, -c1], + [-c1, -c2], + [-c2, -c3]]), + Matrix([[-d0, -d1], + [-d1, -d2], + [-d2, -d3]])) + assert -system == \ + StateSpace(Matrix([[-1, 1], + [ 1, -1]]), + Matrix([[ 1],[-1]]), + Matrix([[1, -1]]), + Matrix([[-1]])) + assert -SS_neg == SS + assert -(-(-(-system))) == system + +def test_SymPy_substitution_functions(): + # subs + ss1 = StateSpace(Matrix([s]), Matrix([(s + 1)**2]), Matrix([s**2 - 1]), Matrix([2*s])) + ss2 = StateSpace(Matrix([s + p]), Matrix([(s + 1)*(p - 1)]), Matrix([p**3 - s**3]), Matrix([s - p])) + + assert ss1.subs({s:5}) == StateSpace(Matrix([[5]]), Matrix([[36]]), Matrix([[24]]), Matrix([[10]])) + assert ss2.subs({p:1}) == StateSpace(Matrix([[s + 1]]), Matrix([[0]]), Matrix([[1 - s**3]]), Matrix([[s - 1]])) + + # xreplace + assert ss1.xreplace({s:p}) == \ + StateSpace(Matrix([[p]]), Matrix([[(p + 1)**2]]), Matrix([[p**2 - 1]]), Matrix([[2*p]])) + assert ss2.xreplace({s:a, p:b}) == \ + StateSpace(Matrix([[a + b]]), Matrix([[(a + 1)*(b - 1)]]), Matrix([[-a**3 + b**3]]), Matrix([[a - b]])) + + # evalf + p1 = a1*s + a0 + p2 = b2*s**2 + b1*s + b0 + G = StateSpace(Matrix([p1]), Matrix([p2])) + expect = StateSpace(Matrix([[2*s + 1]]), Matrix([[5*s**2 + 4*s + 3]]), Matrix([[0]]), Matrix([[0]])) + expect_ = StateSpace(Matrix([[2.0*s + 1.0]]), Matrix([[5.0*s**2 + 4.0*s + 3.0]]), Matrix([[0]]), Matrix([[0]])) + assert G.subs({a0: 1, a1: 2, b0: 3, b1: 4, b2: 5}) == expect + assert G.subs({a0: 1, a1: 2, b0: 3, b1: 4, b2: 5}).evalf() == expect_ + assert expect.evalf() == expect_ + +def test_conversion(): + # StateSpace to TransferFunction for SISO + A1 = Matrix([[-5, -1], [3, -1]]) + B1 = Matrix([2, 5]) + C1 = Matrix([[1, 2]]) + D1 = Matrix([0]) + H1 = StateSpace(A1, B1, C1, D1) + H3 = StateSpace(Matrix([[a0, a1], [a2, a3]]), B = Matrix([[b1], [b2]]), C = Matrix([[c1, c2]])) + tm1 = H1.rewrite(TransferFunction) + tm2 = (-H1).rewrite(TransferFunction) + + tf1 = tm1[0][0] + tf2 = tm2[0][0] + + assert tf1 == TransferFunction(12*s + 59, s**2 + 6*s + 8, s) + assert tf2.num == -tf1.num + assert tf2.den == tf1.den + + # StateSpace to TransferFunction for MIMO + A2 = Matrix([[-1.5, -2, 3], [1, 0, 1], [2, 1, 1]]) + B2 = Matrix([[0.5, 0, 1], [0, 1, 2], [2, 2, 3]]) + C2 = Matrix([[0, 1, 0], [0, 2, 1], [1, 0, 2]]) + D2 = Matrix([[2, 2, 0], [1, 1, 1], [3, 2, 1]]) + H2 = StateSpace(A2, B2, C2, D2) + tm3 = H2.rewrite(TransferFunction) + + # outputs for input i obtained at Index i-1. Consider input 1 + assert tm3[0][0] == TransferFunction(2.0*s**3 + 1.0*s**2 - 10.5*s + 4.5, 1.0*s**3 + 0.5*s**2 - 6.5*s - 2.5, s) + assert tm3[0][1] == TransferFunction(2.0*s**3 + 2.0*s**2 - 10.5*s - 3.5, 1.0*s**3 + 0.5*s**2 - 6.5*s - 2.5, s) + assert tm3[0][2] == TransferFunction(2.0*s**2 + 5.0*s - 0.5, 1.0*s**3 + 0.5*s**2 - 6.5*s - 2.5, s) + assert H3.rewrite(TransferFunction) == [[TransferFunction(-c1*(a1*b2 - a3*b1 + b1*s) - c2*(-a0*b2 + a2*b1 + b2*s), + -a0*a3 + a0*s + a1*a2 + a3*s - s**2, s)]] + # TransferFunction to StateSpace + SS = TF1.rewrite(StateSpace) + assert SS == \ + StateSpace(Matrix([[ 0, 1], + [-wn**2, -2*wn*zeta]]), + Matrix([[0], + [1]]), + Matrix([[1, 0]]), + Matrix([[0]])) + assert SS.rewrite(TransferFunction)[0][0] == TF1 + + # Transfer function has to be proper + raises(ValueError, lambda: TransferFunction(b*s**2 + p**2 - a*p + s, b - p**2, s).rewrite(StateSpace)) + + +def test_StateSpace_dsolve(): + # https://web.mit.edu/2.14/www/Handouts/StateSpaceResponse.pdf + # https://lpsa.swarthmore.edu/Transient/TransMethSS.html + A1 = Matrix([[0, 1], [-2, -3]]) + B1 = Matrix([[0], [1]]) + C1 = Matrix([[1, -1]]) + D1 = Matrix([0]) + I1 = Matrix([[1], [2]]) + t = symbols('t') + ss1 = StateSpace(A1, B1, C1, D1) + + # Zero input and Zero initial conditions + assert ss1.dsolve() == Matrix([[0]]) + assert ss1.dsolve(initial_conditions=I1) == Matrix([[8*exp(-t) - 9*exp(-2*t)]]) + + A2 = Matrix([[-2, 0], [1, -1]]) + C2 = eye(2,2) + I2 = Matrix([2, 3]) + ss2 = StateSpace(A=A2, C=C2) + assert ss2.dsolve(initial_conditions=I2) == Matrix([[2*exp(-2*t)], [5*exp(-t) - 2*exp(-2*t)]]) + + A3 = Matrix([[-1, 1], [-4, -4]]) + B3 = Matrix([[0], [4]]) + C3 = Matrix([[0, 1]]) + D3 = Matrix([0]) + U3 = Matrix([10]) + ss3 = StateSpace(A3, B3, C3, D3) + op = ss3.dsolve(input_vector=U3, var=t) + assert str(op.simplify().expand().evalf()[0]) == str(5.0 + 20.7880460155075*exp(-5*t/2)*sin(sqrt(7)*t/2) + - 5.0*exp(-5*t/2)*cos(sqrt(7)*t/2)) + + # Test with Heaviside as input + A4 = Matrix([[-1, 1], [-4, -4]]) + B4 = Matrix([[0], [4]]) + C4 = Matrix([[0, 1]]) + U4 = Matrix([[10*Heaviside(t)]]) + ss4 = StateSpace(A4, B4, C4) + op4 = str(ss4.dsolve(var=t, input_vector=U4)[0].simplify().expand().evalf()) + assert op4 == str(5.0*Heaviside(t) + 20.7880460155075*exp(-5*t/2)*sin(sqrt(7)*t/2)*Heaviside(t) + - 5.0*exp(-5*t/2)*cos(sqrt(7)*t/2)*Heaviside(t)) + + # Test with Symbolic Matrices + m, a, x0 = symbols('m a x_0') + A5 = Matrix([[0, 1], [0, 0]]) + B5 = Matrix([[0], [1 / m]]) + C5 = Matrix([[1, 0]]) + I5 = Matrix([[x0], [0]]) + U5 = Matrix([[exp(-a * t)]]) + ss5 = StateSpace(A5, B5, C5) + op5 = ss5.dsolve(initial_conditions=I5, input_vector=U5, var=t).simplify() + assert op5[0].args[0][0] == x0 + t/(a*m) - 1/(a**2*m) + exp(-a*t)/(a**2*m) + a11, a12, a21, a22, b1, b2, c1, c2, i1, i2 = symbols('a_11 a_12 a_21 a_22 b_1 b_2 c_1 c_2 i_1 i_2') + A6 = Matrix([[a11, a12], [a21, a22]]) + B6 = Matrix([b1, b2]) + C6 = Matrix([[c1, c2]]) + I6 = Matrix([i1, i2]) + ss6 = StateSpace(A6, B6, C6) + expr6 = ss6.dsolve(initial_conditions=I6)[0] + expr6 = expr6.subs([(a11, 0), (a12, 1), (a21, -2), (a22, -3), (b1, 0), (b2, 1), (c1, 1), (c2, -1), (i1, 1), (i2, 2)]) + assert expr6 == 8*exp(-t) - 9*exp(-2*t) + + +def test_StateSpace_functions(): + # https://in.mathworks.com/help/control/ref/statespacemodel.obsv.html + + A_mat = Matrix([[-1.5, -2], [1, 0]]) + B_mat = Matrix([0.5, 0]) + C_mat = Matrix([[0, 1]]) + D_mat = Matrix([1]) + SS1 = StateSpace(A_mat, B_mat, C_mat, D_mat) + SS2 = StateSpace(Matrix([[1, 1], [4, -2]]),Matrix([[0, 1], [0, 2]]),Matrix([[-1, 1], [1, -1]])) + SS3 = StateSpace(Matrix([[1, 1], [4, -2]]),Matrix([[1, -1], [1, -1]])) + SS4 = StateSpace(Matrix([[a0, a1], [a2, a3]]), Matrix([[b1], [b2]]), Matrix([[c1, c2]])) + + # Observability + assert SS1.is_observable() == True + assert SS2.is_observable() == False + assert SS1.observability_matrix() == Matrix([[0, 1], [1, 0]]) + assert SS2.observability_matrix() == Matrix([[-1, 1], [ 1, -1], [ 3, -3], [-3, 3]]) + assert SS1.observable_subspace() == [Matrix([[0], [1]]), Matrix([[1], [0]])] + assert SS2.observable_subspace() == [Matrix([[-1], [ 1], [ 3], [-3]])] + Qo = SS4.observability_matrix().subs([(a0, 0), (a1, -6), (a2, 1), (a3, -5), (c1, 0), (c2, 1)]) + assert Qo == Matrix([[0, 1], [1, -5]]) + + # Controllability + assert SS1.is_controllable() == True + assert SS3.is_controllable() == False + assert SS1.controllability_matrix() == Matrix([[0.5, -0.75], [ 0, 0.5]]) + assert SS3.controllability_matrix() == Matrix([[1, -1, 2, -2], [1, -1, 2, -2]]) + assert SS1.controllable_subspace() == [Matrix([[0.5], [ 0]]), Matrix([[-0.75], [ 0.5]])] + assert SS3.controllable_subspace() == [Matrix([[1], [1]])] + assert SS4.controllable_subspace() == [Matrix([ + [b1], + [b2]]), Matrix([ + [a0*b1 + a1*b2], + [a2*b1 + a3*b2]])] + Qc = SS4.controllability_matrix().subs([(a0, 0), (a1, 1), (a2, -6), (a3, -5), (b1, 0), (b2, 1)]) + assert Qc == Matrix([[0, 1], [1, -5]]) + + # Append + A1 = Matrix([[0, 1], [1, 0]]) + B1 = Matrix([[0], [1]]) + C1 = Matrix([[0, 1]]) + D1 = Matrix([[0]]) + ss1 = StateSpace(A1, B1, C1, D1) + ss2 = StateSpace(Matrix([[1, 0], [0, 1]]), Matrix([[1], [0]]), Matrix([[1, 0]]), Matrix([[1]])) + ss3 = ss1.append(ss2) + ss4 = SS4.append(ss1) + + assert ss3.num_states == ss1.num_states + ss2.num_states + assert ss3.num_inputs == ss1.num_inputs + ss2.num_inputs + assert ss3.num_outputs == ss1.num_outputs + ss2.num_outputs + assert ss3.state_matrix == Matrix([[0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + assert ss3.input_matrix == Matrix([[0, 0], [1, 0], [0, 1], [0, 0]]) + assert ss3.output_matrix == Matrix([[0, 1, 0, 0], [0, 0, 1, 0]]) + assert ss3.feedforward_matrix == Matrix([[0, 0], [0, 1]]) + + # Using symbolic matrices + assert ss4.num_states == SS4.num_states + ss1.num_states + assert ss4.num_inputs == SS4.num_inputs + ss1.num_inputs + assert ss4.num_outputs == SS4.num_outputs + ss1.num_outputs + assert ss4.state_matrix == Matrix([[a0, a1, 0, 0], [a2, a3, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) + assert ss4.input_matrix == Matrix([[b1, 0], [b2, 0], [0, 0], [0, 1]]) + assert ss4.output_matrix == Matrix([[c1, c2, 0, 0], [0, 0, 0, 1]]) + assert ss4.feedforward_matrix == Matrix([[0, 0], [0, 0]]) + + +def test_StateSpace_series(): + # For SISO Systems + a1 = Matrix([[0, 1], [1, 0]]) + b1 = Matrix([[0], [1]]) + c1 = Matrix([[0, 1]]) + d1 = Matrix([[0]]) + a2 = Matrix([[1, 0], [0, 1]]) + b2 = Matrix([[1], [0]]) + c2 = Matrix([[1, 0]]) + d2 = Matrix([[1]]) + + ss1 = StateSpace(a1, b1, c1, d1) + ss2 = StateSpace(a2, b2, c2, d2) + tf1 = TransferFunction(s, s+1, s) + ser1 = Series(ss1, ss2) + assert ser1 == Series(StateSpace(Matrix([ + [0, 1], + [1, 0]]), Matrix([ + [0], + [1]]), Matrix([[0, 1]]), Matrix([[0]])), StateSpace(Matrix([ + [1, 0], + [0, 1]]), Matrix([ + [1], + [0]]), Matrix([[1, 0]]), Matrix([[1]]))) + assert ser1.doit() == StateSpace( + Matrix([ + [0, 1, 0, 0], + [1, 0, 0, 0], + [0, 1, 1, 0], + [0, 0, 0, 1]]), + Matrix([ + [0], + [1], + [0], + [0]]), + Matrix([[0, 1, 1, 0]]), + Matrix([[0]])) + + assert ser1.num_inputs == 1 + assert ser1.num_outputs == 1 + assert ser1.rewrite(TransferFunction) == TransferFunction(s**2, s**3 - s**2 - s + 1, s) + ser2 = Series(ss1) + ser3 = Series(ser2, ss2) + assert ser3.doit() == ser1.doit() + + # TransferFunction interconnection with StateSpace + ser_tf = Series(tf1, ss1) + assert ser_tf == Series(TransferFunction(s, s + 1, s), StateSpace(Matrix([ + [0, 1], + [1, 0]]), Matrix([ + [0], + [1]]), Matrix([[0, 1]]), Matrix([[0]]))) + assert ser_tf.doit() == StateSpace( + Matrix([ + [-1, 0, 0], + [0, 0, 1], + [-1, 1, 0]]), + Matrix([ + [1], + [0], + [1]]), + Matrix([[0, 0, 1]]), + Matrix([[0]])) + assert ser_tf.rewrite(TransferFunction) == TransferFunction(s**2, s**3 + s**2 - s - 1, s) + + # For MIMO Systems + a3 = Matrix([[4, 1], [2, -3]]) + b3 = Matrix([[5, 2], [-3, -3]]) + c3 = Matrix([[2, -4], [0, 1]]) + d3 = Matrix([[3, 2], [1, -1]]) + a4 = Matrix([[-3, 4, 2], [-1, -3, 0], [2, 5, 3]]) + b4 = Matrix([[1, 4], [-3, -3], [-2, 1]]) + c4 = Matrix([[4, 2, -3], [1, 4, 3]]) + d4 = Matrix([[-2, 4], [0, 1]]) + ss3 = StateSpace(a3, b3, c3, d3) + ss4 = StateSpace(a4, b4, c4, d4) + ser4 = MIMOSeries(ss3, ss4) + assert ser4 == MIMOSeries(StateSpace(Matrix([ + [4, 1], + [2, -3]]), Matrix([ + [ 5, 2], + [-3, -3]]), Matrix([ + [2, -4], + [0, 1]]), Matrix([ + [3, 2], + [1, -1]])), StateSpace(Matrix([ + [-3, 4, 2], + [-1, -3, 0], + [ 2, 5, 3]]), Matrix([ + [ 1, 4], + [-3, -3], + [-2, 1]]), Matrix([ + [4, 2, -3], + [1, 4, 3]]), Matrix([ + [-2, 4], + [ 0, 1]]))) + assert ser4.doit() == StateSpace( + Matrix([ + [4, 1, 0, 0, 0], + [2, -3, 0, 0, 0], + [2, 0, -3, 4, 2], + [-6, 9, -1, -3, 0], + [-4, 9, 2, 5, 3]]), + Matrix([ + [5, 2], + [-3, -3], + [7, -2], + [-12, -3], + [-5, -5]]), + Matrix([ + [-4, 12, 4, 2, -3], + [0, 1, 1, 4, 3]]), + Matrix([ + [-2, -8], + [1, -1]])) + assert ser4.num_inputs == ss3.num_inputs + assert ser4.num_outputs == ss4.num_outputs + ser5 = MIMOSeries(ss3) + ser6 = MIMOSeries(ser5, ss4) + assert ser6.doit() == ser4.doit() + assert ser6.rewrite(TransferFunctionMatrix) == ser4.rewrite(TransferFunctionMatrix) + tf2 = TransferFunction(1, s, s) + tf3 = TransferFunction(1, s+1, s) + tf4 = TransferFunction(s, s+2, s) + tfm = TransferFunctionMatrix([[tf1, tf2], [tf3, tf4]]) + ser6 = MIMOSeries(ss3, tfm) + assert ser6 == MIMOSeries(StateSpace(Matrix([ + [4, 1], + [2, -3]]), Matrix([ + [ 5, 2], + [-3, -3]]), Matrix([ + [2, -4], + [0, 1]]), Matrix([ + [3, 2], + [1, -1]])), TransferFunctionMatrix(( + (TransferFunction(s, s + 1, s), TransferFunction(1, s, s)), + (TransferFunction(1, s + 1, s), TransferFunction(s, s + 2, s))))) + + +def test_StateSpace_parallel(): + # For SISO system + a1 = Matrix([[0, 1], [1, 0]]) + b1 = Matrix([[0], [1]]) + c1 = Matrix([[0, 1]]) + d1 = Matrix([[0]]) + a2 = Matrix([[1, 0], [0, 1]]) + b2 = Matrix([[1], [0]]) + c2 = Matrix([[1, 0]]) + d2 = Matrix([[1]]) + ss1 = StateSpace(a1, b1, c1, d1) + ss2 = StateSpace(a2, b2, c2, d2) + p1 = Parallel(ss1, ss2) + assert p1 == Parallel(StateSpace(Matrix([[0, 1], [1, 0]]), Matrix([[0], [1]]), Matrix([[0, 1]]), Matrix([[0]])), + StateSpace(Matrix([[1, 0],[0, 1]]), Matrix([[1],[0]]), Matrix([[1, 0]]), Matrix([[1]]))) + assert p1.doit() == StateSpace(Matrix([ + [0, 1, 0, 0], + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]]), + Matrix([ + [0], + [1], + [1], + [0]]), + Matrix([[0, 1, 1, 0]]), + Matrix([[1]])) + assert p1.rewrite(TransferFunction) == TransferFunction(s*(s + 2), s**2 - 1, s) + + # Connecting StateSpace with TransferFunction + tf1 = TransferFunction(s, s+1, s) + p2 = Parallel(ss1, tf1) + assert p2 == Parallel(StateSpace(Matrix([ + [0, 1], + [1, 0]]), Matrix([ + [0], + [1]]), Matrix([[0, 1]]), Matrix([[0]])), TransferFunction(s, s + 1, s)) + assert p2.doit() == StateSpace( + Matrix([ + [0, 1, 0], + [1, 0, 0], + [0, 0, -1]]), + Matrix([ + [0], + [1], + [1]]), + Matrix([[0, 1, -1]]), + Matrix([[1]])) + assert p2.rewrite(TransferFunction) == TransferFunction(s**2, s**2 - 1, s) + + # For MIMO + a3 = Matrix([[4, 1], [2, -3]]) + b3 = Matrix([[5, 2], [-3, -3]]) + c3 = Matrix([[2, -4], [0, 1]]) + d3 = Matrix([[3, 2], [1, -1]]) + a4 = Matrix([[-3, 4, 2], [-1, -3, 0], [2, 5, 3]]) + b4 = Matrix([[1, 4], [-3, -3], [-2, 1]]) + c4 = Matrix([[4, 2, -3], [1, 4, 3]]) + d4 = Matrix([[-2, 4], [0, 1]]) + ss3 = StateSpace(a3, b3, c3, d3) + ss4 = StateSpace(a4, b4, c4, d4) + p3 = MIMOParallel(ss3, ss4) + assert p3 == MIMOParallel(StateSpace(Matrix([ + [4, 1], + [2, -3]]), Matrix([ + [ 5, 2], + [-3, -3]]), Matrix([ + [2, -4], + [0, 1]]), Matrix([ + [3, 2], + [1, -1]])), StateSpace(Matrix([ + [-3, 4, 2], + [-1, -3, 0], + [ 2, 5, 3]]), Matrix([ + [ 1, 4], + [-3, -3], + [-2, 1]]), Matrix([ + [4, 2, -3], + [1, 4, 3]]), Matrix([ + [-2, 4], + [ 0, 1]]))) + assert p3.doit() == StateSpace(Matrix([ + [4, 1, 0, 0, 0], + [2, -3, 0, 0, 0], + [0, 0, -3, 4, 2], + [0, 0, -1, -3, 0], + [0, 0, 2, 5, 3]]), + Matrix([ + [5, 2], + [-3, -3], + [1, 4], + [-3, -3], + [-2, 1]]), + Matrix([ + [2, -4, 4, 2, -3], + [0, 1, 1, 4, 3]]), + Matrix([ + [1, 6], + [1, 0]])) + + # Using StateSpace with MIMOParallel. + tf2 = TransferFunction(1, s, s) + tf3 = TransferFunction(1, s + 1, s) + tf4 = TransferFunction(s, s + 2, s) + tfm = TransferFunctionMatrix([[tf1, tf2], [tf3, tf4]]) + p4 = MIMOParallel(tfm, ss3) + assert p4 == MIMOParallel(TransferFunctionMatrix(( + (TransferFunction(s, s + 1, s), TransferFunction(1, s, s)), + (TransferFunction(1, s + 1, s), TransferFunction(s, s + 2, s)))), + StateSpace(Matrix([ + [4, 1], + [2, -3]]), Matrix([ + [5, 2], + [-3, -3]]), Matrix([ + [2, -4], + [0, 1]]), Matrix([ + [3, 2], + [1, -1]]))) + + +def test_StateSpace_feedback(): + # For SISO + a1 = Matrix([[0, 1], [1, 0]]) + b1 = Matrix([[0], [1]]) + c1 = Matrix([[0, 1]]) + d1 = Matrix([[0]]) + a2 = Matrix([[1, 0], [0, 1]]) + b2 = Matrix([[1], [0]]) + c2 = Matrix([[1, 0]]) + d2 = Matrix([[1]]) + ss1 = StateSpace(a1, b1, c1, d1) + ss2 = StateSpace(a2, b2, c2, d2) + fd1 = Feedback(ss1, ss2) + + # Negative feedback + assert fd1 == Feedback(StateSpace(Matrix([[0, 1], [1, 0]]), Matrix([[0], [1]]), Matrix([[0, 1]]), Matrix([[0]])), + StateSpace(Matrix([[1, 0],[0, 1]]), Matrix([[1],[0]]), Matrix([[1, 0]]), Matrix([[1]])), -1) + assert fd1.doit() == StateSpace(Matrix([ + [0, 1, 0, 0], + [1, -1, -1, 0], + [0, 1, 1, 0], + [0, 0, 0, 1]]), Matrix([ + [0], + [1], + [0], + [0]]), Matrix( + [[0, 1, 0, 0]]), Matrix( + [[0]])) + assert fd1.rewrite(TransferFunction) == TransferFunction(s*(s - 1), s**3 - s + 1, s) + + # Positive Feedback + fd2 = Feedback(ss1, ss2, 1) + assert fd2.doit() == StateSpace(Matrix([ + [0, 1, 0, 0], + [1, 1, 1, 0], + [0, 1, 1, 0], + [0, 0, 0, 1]]), Matrix([ + [0], + [1], + [0], + [0]]), Matrix( + [[0, 1, 0, 0]]), Matrix( + [[0]])) + assert fd2.rewrite(TransferFunction) == TransferFunction(s*(s - 1), s**3 - 2*s**2 - s + 1, s) + + # Connection with TransferFunction + tf1 = TransferFunction(s, s+1, s) + fd3 = Feedback(ss1, tf1) + assert fd3 == Feedback(StateSpace(Matrix([ + [0, 1], + [1, 0]]), Matrix([ + [0], + [1]]), Matrix([[0, 1]]), Matrix([[0]])), + TransferFunction(s, s + 1, s), -1) + assert fd3.doit() == StateSpace (Matrix([ + [0, 1, 0], + [1, -1, 1], + [0, 1, -1]]), Matrix([ + [0], + [1], + [0]]), Matrix( + [[0, 1, 0]]), Matrix( + [[0]])) + + # For MIMO + a3 = Matrix([[4, 1], [2, -3]]) + b3 = Matrix([[5, 2], [-3, -3]]) + c3 = Matrix([[2, -4], [0, 1]]) + d3 = Matrix([[3, 2], [1, -1]]) + a4 = Matrix([[-3, 4, 2], [-1, -3, 0], [2, 5, 3]]) + b4 = Matrix([[1, 4], [-3, -3], [-2, 1]]) + c4 = Matrix([[4, 2, -3], [1, 4, 3]]) + d4 = Matrix([[-2, 4], [0, 1]]) + ss3 = StateSpace(a3, b3, c3, d3) + ss4 = StateSpace(a4, b4, c4, d4) + + # Negative Feedback + fd4 = MIMOFeedback(ss3, ss4) + assert fd4 == MIMOFeedback(StateSpace(Matrix([ + [4, 1], + [2, -3]]), Matrix([ + [ 5, 2], + [-3, -3]]), Matrix([ + [2, -4], + [0, 1]]), Matrix([ + [3, 2], + [1, -1]])), StateSpace(Matrix([ + [-3, 4, 2], + [-1, -3, 0], + [ 2, 5, 3]]), Matrix([ + [ 1, 4], + [-3, -3], + [-2, 1]]), Matrix([ + [4, 2, -3], + [1, 4, 3]]), Matrix([ + [-2, 4], + [ 0, 1]])), -1) + assert fd4.doit() == StateSpace(Matrix([ + [Rational(3), Rational(-3, 4), Rational(-15, 4), Rational(-37, 2), Rational(-15)], + [Rational(7, 2), Rational(-39, 8), Rational(9, 8), Rational(39, 4), Rational(9)], + [Rational(3), Rational(-41, 4), Rational(-45, 4), Rational(-51, 2), Rational(-19)], + [Rational(-9, 2), Rational(129, 8), Rational(73, 8), Rational(171, 4), Rational(36)], + [Rational(-3, 2), Rational(47, 8), Rational(31, 8), Rational(85, 4), Rational(18)]]), Matrix([ + [Rational(-1, 4), Rational(19, 4)], + [Rational(3, 8), Rational(-21, 8)], + [Rational(1, 4), Rational(29, 4)], + [Rational(3, 8), Rational(-93, 8)], + [Rational(5, 8), Rational(-35, 8)]]), Matrix([ + [Rational(1), Rational(-15, 4), Rational(-7, 4), Rational(-21, 2), Rational(-9)], + [Rational(1, 2), Rational(-13, 8), Rational(-13, 8), Rational(-19, 4), Rational(-3)]]), Matrix([ + [Rational(-1, 4), Rational(11, 4)], + [Rational(1, 8), Rational(9, 8)]])) + + # Positive Feedback + fd5 = MIMOFeedback(ss3, ss4, 1) + assert fd5.doit() == StateSpace(Matrix([ + [Rational(4, 7), Rational(62, 7), Rational(1), Rational(-8), Rational(-69, 7)], + [Rational(32, 7), Rational(-135, 14), Rational(-3, 2), Rational(3), Rational(36, 7)], + [Rational(-10, 7), Rational(41, 7), Rational(-4), Rational(-12), Rational(-97, 7)], + [Rational(12, 7), Rational(-111, 14), Rational(-5, 2), Rational(18), Rational(171, 7)], + [Rational(2, 7), Rational(-29, 14), Rational(-1, 2), Rational(10), Rational(81, 7)]]), Matrix([ + [Rational(6, 7), Rational(-17, 7)], + [Rational(-9, 14), Rational(15, 14)], + [Rational(6, 7), Rational(-31, 7)], + [Rational(-27, 14), Rational(87, 14)], + [Rational(-15, 14), Rational(25, 14)]]), Matrix([ + [Rational(-2, 7), Rational(11, 7), Rational(1), Rational(-4), Rational(-39, 7)], + [Rational(-2, 7), Rational(15, 14), Rational(-1, 2), Rational(-3), Rational(-18, 7)]]), Matrix([ + [Rational(4, 7), Rational(-9, 7)], + [Rational(1, 14), Rational(-11, 14)]])) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/hep/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/hep/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/hep/gamma_matrices.py b/venv/lib/python3.11/site-packages/sympy/physics/hep/gamma_matrices.py new file mode 100644 index 0000000000000000000000000000000000000000..40c3d0754438902f304d01c2df354dd09f9ea257 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/hep/gamma_matrices.py @@ -0,0 +1,716 @@ +""" + Module to handle gamma matrices expressed as tensor objects. + + Examples + ======== + + >>> from sympy.physics.hep.gamma_matrices import GammaMatrix as G, LorentzIndex + >>> from sympy.tensor.tensor import tensor_indices + >>> i = tensor_indices('i', LorentzIndex) + >>> G(i) + GammaMatrix(i) + + Note that there is already an instance of GammaMatrixHead in four dimensions: + GammaMatrix, which is simply declare as + + >>> from sympy.physics.hep.gamma_matrices import GammaMatrix + >>> from sympy.tensor.tensor import tensor_indices + >>> i = tensor_indices('i', LorentzIndex) + >>> GammaMatrix(i) + GammaMatrix(i) + + To access the metric tensor + + >>> LorentzIndex.metric + metric(LorentzIndex,LorentzIndex) + +""" +from sympy.core.mul import Mul +from sympy.core.singleton import S +from sympy.matrices.dense import eye +from sympy.matrices.expressions.trace import trace +from sympy.tensor.tensor import TensorIndexType, TensorIndex,\ + TensMul, TensAdd, tensor_mul, Tensor, TensorHead, TensorSymmetry + + +# DiracSpinorIndex = TensorIndexType('DiracSpinorIndex', dim=4, dummy_name="S") + + +LorentzIndex = TensorIndexType('LorentzIndex', dim=4, dummy_name="L") + + +GammaMatrix = TensorHead("GammaMatrix", [LorentzIndex], + TensorSymmetry.no_symmetry(1), comm=None) + + +def extract_type_tens(expression, component): + """ + Extract from a ``TensExpr`` all tensors with `component`. + + Returns two tensor expressions: + + * the first contains all ``Tensor`` of having `component`. + * the second contains all remaining. + + + """ + if isinstance(expression, Tensor): + sp = [expression] + elif isinstance(expression, TensMul): + sp = expression.args + else: + raise ValueError('wrong type') + + # Collect all gamma matrices of the same dimension + new_expr = S.One + residual_expr = S.One + for i in sp: + if isinstance(i, Tensor) and i.component == component: + new_expr *= i + else: + residual_expr *= i + return new_expr, residual_expr + + +def simplify_gamma_expression(expression): + extracted_expr, residual_expr = extract_type_tens(expression, GammaMatrix) + res_expr = _simplify_single_line(extracted_expr) + return res_expr * residual_expr + + +def simplify_gpgp(ex, sort=True): + """ + simplify products ``G(i)*p(-i)*G(j)*p(-j) -> p(i)*p(-i)`` + + Examples + ======== + + >>> from sympy.physics.hep.gamma_matrices import GammaMatrix as G, \ + LorentzIndex, simplify_gpgp + >>> from sympy.tensor.tensor import tensor_indices, tensor_heads + >>> p, q = tensor_heads('p, q', [LorentzIndex]) + >>> i0,i1,i2,i3,i4,i5 = tensor_indices('i0:6', LorentzIndex) + >>> ps = p(i0)*G(-i0) + >>> qs = q(i0)*G(-i0) + >>> simplify_gpgp(ps*qs*qs) + GammaMatrix(-L_0)*p(L_0)*q(L_1)*q(-L_1) + """ + def _simplify_gpgp(ex): + components = ex.components + a = [] + comp_map = [] + for i, comp in enumerate(components): + comp_map.extend([i]*comp.rank) + dum = [(i[0], i[1], comp_map[i[0]], comp_map[i[1]]) for i in ex.dum] + for i in range(len(components)): + if components[i] != GammaMatrix: + continue + for dx in dum: + if dx[2] == i: + p_pos1 = dx[3] + elif dx[3] == i: + p_pos1 = dx[2] + else: + continue + comp1 = components[p_pos1] + if comp1.comm == 0 and comp1.rank == 1: + a.append((i, p_pos1)) + if not a: + return ex + elim = set() + tv = [] + hit = True + coeff = S.One + ta = None + while hit: + hit = False + for i, ai in enumerate(a[:-1]): + if ai[0] in elim: + continue + if ai[0] != a[i + 1][0] - 1: + continue + if components[ai[1]] != components[a[i + 1][1]]: + continue + elim.add(ai[0]) + elim.add(ai[1]) + elim.add(a[i + 1][0]) + elim.add(a[i + 1][1]) + if not ta: + ta = ex.split() + mu = TensorIndex('mu', LorentzIndex) + hit = True + if i == 0: + coeff = ex.coeff + tx = components[ai[1]](mu)*components[ai[1]](-mu) + if len(a) == 2: + tx *= 4 # eye(4) + tv.append(tx) + break + + if tv: + a = [x for j, x in enumerate(ta) if j not in elim] + a.extend(tv) + t = tensor_mul(*a)*coeff + # t = t.replace(lambda x: x.is_Matrix, lambda x: 1) + return t + else: + return ex + + if sort: + ex = ex.sorted_components() + # this would be better off with pattern matching + while 1: + t = _simplify_gpgp(ex) + if t != ex: + ex = t + else: + return t + + +def gamma_trace(t): + """ + trace of a single line of gamma matrices + + Examples + ======== + + >>> from sympy.physics.hep.gamma_matrices import GammaMatrix as G, \ + gamma_trace, LorentzIndex + >>> from sympy.tensor.tensor import tensor_indices, tensor_heads + >>> p, q = tensor_heads('p, q', [LorentzIndex]) + >>> i0,i1,i2,i3,i4,i5 = tensor_indices('i0:6', LorentzIndex) + >>> ps = p(i0)*G(-i0) + >>> qs = q(i0)*G(-i0) + >>> gamma_trace(G(i0)*G(i1)) + 4*metric(i0, i1) + >>> gamma_trace(ps*ps) - 4*p(i0)*p(-i0) + 0 + >>> gamma_trace(ps*qs + ps*ps) - 4*p(i0)*p(-i0) - 4*p(i0)*q(-i0) + 0 + + """ + if isinstance(t, TensAdd): + res = TensAdd(*[gamma_trace(x) for x in t.args]) + return res + t = _simplify_single_line(t) + res = _trace_single_line(t) + return res + + +def _simplify_single_line(expression): + """ + Simplify single-line product of gamma matrices. + + Examples + ======== + + >>> from sympy.physics.hep.gamma_matrices import GammaMatrix as G, \ + LorentzIndex, _simplify_single_line + >>> from sympy.tensor.tensor import tensor_indices, TensorHead + >>> p = TensorHead('p', [LorentzIndex]) + >>> i0,i1 = tensor_indices('i0:2', LorentzIndex) + >>> _simplify_single_line(G(i0)*G(i1)*p(-i1)*G(-i0)) + 2*G(i0)*p(-i0) + 0 + + """ + t1, t2 = extract_type_tens(expression, GammaMatrix) + if t1 != 1: + t1 = kahane_simplify(t1) + res = t1*t2 + return res + + +def _trace_single_line(t): + """ + Evaluate the trace of a single gamma matrix line inside a ``TensExpr``. + + Notes + ===== + + If there are ``DiracSpinorIndex.auto_left`` and ``DiracSpinorIndex.auto_right`` + indices trace over them; otherwise traces are not implied (explain) + + + Examples + ======== + + >>> from sympy.physics.hep.gamma_matrices import GammaMatrix as G, \ + LorentzIndex, _trace_single_line + >>> from sympy.tensor.tensor import tensor_indices, TensorHead + >>> p = TensorHead('p', [LorentzIndex]) + >>> i0,i1,i2,i3,i4,i5 = tensor_indices('i0:6', LorentzIndex) + >>> _trace_single_line(G(i0)*G(i1)) + 4*metric(i0, i1) + >>> _trace_single_line(G(i0)*p(-i0)*G(i1)*p(-i1)) - 4*p(i0)*p(-i0) + 0 + + """ + def _trace_single_line1(t): + t = t.sorted_components() + components = t.components + ncomps = len(components) + g = LorentzIndex.metric + # gamma matirices are in a[i:j] + hit = 0 + for i in range(ncomps): + if components[i] == GammaMatrix: + hit = 1 + break + + for j in range(i + hit, ncomps): + if components[j] != GammaMatrix: + break + else: + j = ncomps + numG = j - i + if numG == 0: + tcoeff = t.coeff + return t.nocoeff if tcoeff else t + if numG % 2 == 1: + return TensMul.from_data(S.Zero, [], [], []) + elif numG > 4: + # find the open matrix indices and connect them: + a = t.split() + ind1 = a[i].get_indices()[0] + ind2 = a[i + 1].get_indices()[0] + aa = a[:i] + a[i + 2:] + t1 = tensor_mul(*aa)*g(ind1, ind2) + t1 = t1.contract_metric(g) + args = [t1] + sign = 1 + for k in range(i + 2, j): + sign = -sign + ind2 = a[k].get_indices()[0] + aa = a[:i] + a[i + 1:k] + a[k + 1:] + t2 = sign*tensor_mul(*aa)*g(ind1, ind2) + t2 = t2.contract_metric(g) + t2 = simplify_gpgp(t2, False) + args.append(t2) + t3 = TensAdd(*args) + t3 = _trace_single_line(t3) + return t3 + else: + a = t.split() + t1 = _gamma_trace1(*a[i:j]) + a2 = a[:i] + a[j:] + t2 = tensor_mul(*a2) + t3 = t1*t2 + if not t3: + return t3 + t3 = t3.contract_metric(g) + return t3 + + t = t.expand() + if isinstance(t, TensAdd): + a = [_trace_single_line1(x)*x.coeff for x in t.args] + return TensAdd(*a) + elif isinstance(t, (Tensor, TensMul)): + r = t.coeff*_trace_single_line1(t) + return r + else: + return trace(t) + + +def _gamma_trace1(*a): + gctr = 4 # FIXME specific for d=4 + g = LorentzIndex.metric + if not a: + return gctr + n = len(a) + if n%2 == 1: + #return TensMul.from_data(S.Zero, [], [], []) + return S.Zero + if n == 2: + ind0 = a[0].get_indices()[0] + ind1 = a[1].get_indices()[0] + return gctr*g(ind0, ind1) + if n == 4: + ind0 = a[0].get_indices()[0] + ind1 = a[1].get_indices()[0] + ind2 = a[2].get_indices()[0] + ind3 = a[3].get_indices()[0] + + return gctr*(g(ind0, ind1)*g(ind2, ind3) - \ + g(ind0, ind2)*g(ind1, ind3) + g(ind0, ind3)*g(ind1, ind2)) + + +def kahane_simplify(expression): + r""" + This function cancels contracted elements in a product of four + dimensional gamma matrices, resulting in an expression equal to the given + one, without the contracted gamma matrices. + + Parameters + ========== + + `expression` the tensor expression containing the gamma matrices to simplify. + + Notes + ===== + + If spinor indices are given, the matrices must be given in + the order given in the product. + + Algorithm + ========= + + The idea behind the algorithm is to use some well-known identities, + i.e., for contractions enclosing an even number of `\gamma` matrices + + `\gamma^\mu \gamma_{a_1} \cdots \gamma_{a_{2N}} \gamma_\mu = 2 (\gamma_{a_{2N}} \gamma_{a_1} \cdots \gamma_{a_{2N-1}} + \gamma_{a_{2N-1}} \cdots \gamma_{a_1} \gamma_{a_{2N}} )` + + for an odd number of `\gamma` matrices + + `\gamma^\mu \gamma_{a_1} \cdots \gamma_{a_{2N+1}} \gamma_\mu = -2 \gamma_{a_{2N+1}} \gamma_{a_{2N}} \cdots \gamma_{a_{1}}` + + Instead of repeatedly applying these identities to cancel out all contracted indices, + it is possible to recognize the links that would result from such an operation, + the problem is thus reduced to a simple rearrangement of free gamma matrices. + + Examples + ======== + + When using, always remember that the original expression coefficient + has to be handled separately + + >>> from sympy.physics.hep.gamma_matrices import GammaMatrix as G, LorentzIndex + >>> from sympy.physics.hep.gamma_matrices import kahane_simplify + >>> from sympy.tensor.tensor import tensor_indices + >>> i0, i1, i2 = tensor_indices('i0:3', LorentzIndex) + >>> ta = G(i0)*G(-i0) + >>> kahane_simplify(ta) + Matrix([ + [4, 0, 0, 0], + [0, 4, 0, 0], + [0, 0, 4, 0], + [0, 0, 0, 4]]) + >>> tb = G(i0)*G(i1)*G(-i0) + >>> kahane_simplify(tb) + -2*GammaMatrix(i1) + >>> t = G(i0)*G(-i0) + >>> kahane_simplify(t) + Matrix([ + [4, 0, 0, 0], + [0, 4, 0, 0], + [0, 0, 4, 0], + [0, 0, 0, 4]]) + >>> t = G(i0)*G(-i0) + >>> kahane_simplify(t) + Matrix([ + [4, 0, 0, 0], + [0, 4, 0, 0], + [0, 0, 4, 0], + [0, 0, 0, 4]]) + + If there are no contractions, the same expression is returned + + >>> tc = G(i0)*G(i1) + >>> kahane_simplify(tc) + GammaMatrix(i0)*GammaMatrix(i1) + + References + ========== + + [1] Algorithm for Reducing Contracted Products of gamma Matrices, + Joseph Kahane, Journal of Mathematical Physics, Vol. 9, No. 10, October 1968. + """ + + if isinstance(expression, Mul): + return expression + if isinstance(expression, TensAdd): + return TensAdd(*[kahane_simplify(arg) for arg in expression.args]) + + if isinstance(expression, Tensor): + return expression + + assert isinstance(expression, TensMul) + + gammas = expression.args + + for gamma in gammas: + assert gamma.component == GammaMatrix + + free = expression.free + # spinor_free = [_ for _ in expression.free_in_args if _[1] != 0] + + # if len(spinor_free) == 2: + # spinor_free.sort(key=lambda x: x[2]) + # assert spinor_free[0][1] == 1 and spinor_free[-1][1] == 2 + # assert spinor_free[0][2] == 0 + # elif spinor_free: + # raise ValueError('spinor indices do not match') + + dum = [] + for dum_pair in expression.dum: + if expression.index_types[dum_pair[0]] == LorentzIndex: + dum.append((dum_pair[0], dum_pair[1])) + + dum = sorted(dum) + + if len(dum) == 0: # or GammaMatrixHead: + # no contractions in `expression`, just return it. + return expression + + # find the `first_dum_pos`, i.e. the position of the first contracted + # gamma matrix, Kahane's algorithm as described in his paper requires the + # gamma matrix expression to start with a contracted gamma matrix, this is + # a workaround which ignores possible initial free indices, and re-adds + # them later. + + first_dum_pos = min(map(min, dum)) + + # for p1, p2, a1, a2 in expression.dum_in_args: + # if p1 != 0 or p2 != 0: + # # only Lorentz indices, skip Dirac indices: + # continue + # first_dum_pos = min(p1, p2) + # break + + total_number = len(free) + len(dum)*2 + number_of_contractions = len(dum) + + free_pos = [None]*total_number + for i in free: + free_pos[i[1]] = i[0] + + # `index_is_free` is a list of booleans, to identify index position + # and whether that index is free or dummy. + index_is_free = [False]*total_number + + for i, indx in enumerate(free): + index_is_free[indx[1]] = True + + # `links` is a dictionary containing the graph described in Kahane's paper, + # to every key correspond one or two values, representing the linked indices. + # All values in `links` are integers, negative numbers are used in the case + # where it is necessary to insert gamma matrices between free indices, in + # order to make Kahane's algorithm work (see paper). + links = {i: [] for i in range(first_dum_pos, total_number)} + + # `cum_sign` is a step variable to mark the sign of every index, see paper. + cum_sign = -1 + # `cum_sign_list` keeps storage for all `cum_sign` (every index). + cum_sign_list = [None]*total_number + block_free_count = 0 + + # multiply `resulting_coeff` by the coefficient parameter, the rest + # of the algorithm ignores a scalar coefficient. + resulting_coeff = S.One + + # initialize a list of lists of indices. The outer list will contain all + # additive tensor expressions, while the inner list will contain the + # free indices (rearranged according to the algorithm). + resulting_indices = [[]] + + # start to count the `connected_components`, which together with the number + # of contractions, determines a -1 or +1 factor to be multiplied. + connected_components = 1 + + # First loop: here we fill `cum_sign_list`, and draw the links + # among consecutive indices (they are stored in `links`). Links among + # non-consecutive indices will be drawn later. + for i, is_free in enumerate(index_is_free): + # if `expression` starts with free indices, they are ignored here; + # they are later added as they are to the beginning of all + # `resulting_indices` list of lists of indices. + if i < first_dum_pos: + continue + + if is_free: + block_free_count += 1 + # if previous index was free as well, draw an arch in `links`. + if block_free_count > 1: + links[i - 1].append(i) + links[i].append(i - 1) + else: + # Change the sign of the index (`cum_sign`) if the number of free + # indices preceding it is even. + cum_sign *= 1 if (block_free_count % 2) else -1 + if block_free_count == 0 and i != first_dum_pos: + # check if there are two consecutive dummy indices: + # in this case create virtual indices with negative position, + # these "virtual" indices represent the insertion of two + # gamma^0 matrices to separate consecutive dummy indices, as + # Kahane's algorithm requires dummy indices to be separated by + # free indices. The product of two gamma^0 matrices is unity, + # so the new expression being examined is the same as the + # original one. + if cum_sign == -1: + links[-1-i] = [-1-i+1] + links[-1-i+1] = [-1-i] + if (i - cum_sign) in links: + if i != first_dum_pos: + links[i].append(i - cum_sign) + if block_free_count != 0: + if i - cum_sign < len(index_is_free): + if index_is_free[i - cum_sign]: + links[i - cum_sign].append(i) + block_free_count = 0 + + cum_sign_list[i] = cum_sign + + # The previous loop has only created links between consecutive free indices, + # it is necessary to properly create links among dummy (contracted) indices, + # according to the rules described in Kahane's paper. There is only one exception + # to Kahane's rules: the negative indices, which handle the case of some + # consecutive free indices (Kahane's paper just describes dummy indices + # separated by free indices, hinting that free indices can be added without + # altering the expression result). + for i in dum: + # get the positions of the two contracted indices: + pos1 = i[0] + pos2 = i[1] + + # create Kahane's upper links, i.e. the upper arcs between dummy + # (i.e. contracted) indices: + links[pos1].append(pos2) + links[pos2].append(pos1) + + # create Kahane's lower links, this corresponds to the arcs below + # the line described in the paper: + + # first we move `pos1` and `pos2` according to the sign of the indices: + linkpos1 = pos1 + cum_sign_list[pos1] + linkpos2 = pos2 + cum_sign_list[pos2] + + # otherwise, perform some checks before creating the lower arcs: + + # make sure we are not exceeding the total number of indices: + if linkpos1 >= total_number: + continue + if linkpos2 >= total_number: + continue + + # make sure we are not below the first dummy index in `expression`: + if linkpos1 < first_dum_pos: + continue + if linkpos2 < first_dum_pos: + continue + + # check if the previous loop created "virtual" indices between dummy + # indices, in such a case relink `linkpos1` and `linkpos2`: + if (-1-linkpos1) in links: + linkpos1 = -1-linkpos1 + if (-1-linkpos2) in links: + linkpos2 = -1-linkpos2 + + # move only if not next to free index: + if linkpos1 >= 0 and not index_is_free[linkpos1]: + linkpos1 = pos1 + + if linkpos2 >=0 and not index_is_free[linkpos2]: + linkpos2 = pos2 + + # create the lower arcs: + if linkpos2 not in links[linkpos1]: + links[linkpos1].append(linkpos2) + if linkpos1 not in links[linkpos2]: + links[linkpos2].append(linkpos1) + + # This loop starts from the `first_dum_pos` index (first dummy index) + # walks through the graph deleting the visited indices from `links`, + # it adds a gamma matrix for every free index in encounters, while it + # completely ignores dummy indices and virtual indices. + pointer = first_dum_pos + previous_pointer = 0 + while True: + if pointer in links: + next_ones = links.pop(pointer) + else: + break + + if previous_pointer in next_ones: + next_ones.remove(previous_pointer) + + previous_pointer = pointer + + if next_ones: + pointer = next_ones[0] + else: + break + + if pointer == previous_pointer: + break + if pointer >=0 and free_pos[pointer] is not None: + for ri in resulting_indices: + ri.append(free_pos[pointer]) + + # The following loop removes the remaining connected components in `links`. + # If there are free indices inside a connected component, it gives a + # contribution to the resulting expression given by the factor + # `gamma_a gamma_b ... gamma_z + gamma_z ... gamma_b gamma_a`, in Kahanes's + # paper represented as {gamma_a, gamma_b, ... , gamma_z}, + # virtual indices are ignored. The variable `connected_components` is + # increased by one for every connected component this loop encounters. + + # If the connected component has virtual and dummy indices only + # (no free indices), it contributes to `resulting_indices` by a factor of two. + # The multiplication by two is a result of the + # factor {gamma^0, gamma^0} = 2 I, as it appears in Kahane's paper. + # Note: curly brackets are meant as in the paper, as a generalized + # multi-element anticommutator! + + while links: + connected_components += 1 + pointer = min(links.keys()) + previous_pointer = pointer + # the inner loop erases the visited indices from `links`, and it adds + # all free indices to `prepend_indices` list, virtual indices are + # ignored. + prepend_indices = [] + while True: + if pointer in links: + next_ones = links.pop(pointer) + else: + break + + if previous_pointer in next_ones: + if len(next_ones) > 1: + next_ones.remove(previous_pointer) + + previous_pointer = pointer + + if next_ones: + pointer = next_ones[0] + + if pointer >= first_dum_pos and free_pos[pointer] is not None: + prepend_indices.insert(0, free_pos[pointer]) + # if `prepend_indices` is void, it means there are no free indices + # in the loop (and it can be shown that there must be a virtual index), + # loops of virtual indices only contribute by a factor of two: + if len(prepend_indices) == 0: + resulting_coeff *= 2 + # otherwise, add the free indices in `prepend_indices` to + # the `resulting_indices`: + else: + expr1 = prepend_indices + expr2 = list(reversed(prepend_indices)) + resulting_indices = [expri + ri for ri in resulting_indices for expri in (expr1, expr2)] + + # sign correction, as described in Kahane's paper: + resulting_coeff *= -1 if (number_of_contractions - connected_components + 1) % 2 else 1 + # power of two factor, as described in Kahane's paper: + resulting_coeff *= 2**(number_of_contractions) + + # If `first_dum_pos` is not zero, it means that there are trailing free gamma + # matrices in front of `expression`, so multiply by them: + resulting_indices = [ free_pos[0:first_dum_pos] + ri for ri in resulting_indices ] + + resulting_expr = S.Zero + for i in resulting_indices: + temp_expr = S.One + for j in i: + temp_expr *= GammaMatrix(j) + resulting_expr += temp_expr + + t = resulting_coeff * resulting_expr + t1 = None + if isinstance(t, TensAdd): + t1 = t.args[0] + elif isinstance(t, TensMul): + t1 = t + if t1: + pass + else: + t = eye(4)*t + return t diff --git a/venv/lib/python3.11/site-packages/sympy/physics/hep/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/hep/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/hep/tests/test_gamma_matrices.py b/venv/lib/python3.11/site-packages/sympy/physics/hep/tests/test_gamma_matrices.py new file mode 100644 index 0000000000000000000000000000000000000000..1552cf0d19be222ba249a7e32c65c8c3abc54ac2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/hep/tests/test_gamma_matrices.py @@ -0,0 +1,427 @@ +from sympy.matrices.dense import eye, Matrix +from sympy.tensor.tensor import tensor_indices, TensorHead, tensor_heads, \ + TensExpr, canon_bp +from sympy.physics.hep.gamma_matrices import GammaMatrix as G, LorentzIndex, \ + kahane_simplify, gamma_trace, _simplify_single_line, simplify_gamma_expression +from sympy import Symbol + + +def _is_tensor_eq(arg1, arg2): + arg1 = canon_bp(arg1) + arg2 = canon_bp(arg2) + if isinstance(arg1, TensExpr): + return arg1.equals(arg2) + elif isinstance(arg2, TensExpr): + return arg2.equals(arg1) + return arg1 == arg2 + +def execute_gamma_simplify_tests_for_function(tfunc, D): + """ + Perform tests to check if sfunc is able to simplify gamma matrix expressions. + + Parameters + ========== + + `sfunc` a function to simplify a `TIDS`, shall return the simplified `TIDS`. + `D` the number of dimension (in most cases `D=4`). + + """ + + mu, nu, rho, sigma = tensor_indices("mu, nu, rho, sigma", LorentzIndex) + a1, a2, a3, a4, a5, a6 = tensor_indices("a1:7", LorentzIndex) + mu11, mu12, mu21, mu31, mu32, mu41, mu51, mu52 = tensor_indices("mu11, mu12, mu21, mu31, mu32, mu41, mu51, mu52", LorentzIndex) + mu61, mu71, mu72 = tensor_indices("mu61, mu71, mu72", LorentzIndex) + m0, m1, m2, m3, m4, m5, m6 = tensor_indices("m0:7", LorentzIndex) + + def g(xx, yy): + return (G(xx)*G(yy) + G(yy)*G(xx))/2 + + # Some examples taken from Kahane's paper, 4 dim only: + if D == 4: + t = (G(a1)*G(mu11)*G(a2)*G(mu21)*G(-a1)*G(mu31)*G(-a2)) + assert _is_tensor_eq(tfunc(t), -4*G(mu11)*G(mu31)*G(mu21) - 4*G(mu31)*G(mu11)*G(mu21)) + + t = (G(a1)*G(mu11)*G(mu12)*\ + G(a2)*G(mu21)*\ + G(a3)*G(mu31)*G(mu32)*\ + G(a4)*G(mu41)*\ + G(-a2)*G(mu51)*G(mu52)*\ + G(-a1)*G(mu61)*\ + G(-a3)*G(mu71)*G(mu72)*\ + G(-a4)) + assert _is_tensor_eq(tfunc(t), \ + 16*G(mu31)*G(mu32)*G(mu72)*G(mu71)*G(mu11)*G(mu52)*G(mu51)*G(mu12)*G(mu61)*G(mu21)*G(mu41) + 16*G(mu31)*G(mu32)*G(mu72)*G(mu71)*G(mu12)*G(mu51)*G(mu52)*G(mu11)*G(mu61)*G(mu21)*G(mu41) + 16*G(mu71)*G(mu72)*G(mu32)*G(mu31)*G(mu11)*G(mu52)*G(mu51)*G(mu12)*G(mu61)*G(mu21)*G(mu41) + 16*G(mu71)*G(mu72)*G(mu32)*G(mu31)*G(mu12)*G(mu51)*G(mu52)*G(mu11)*G(mu61)*G(mu21)*G(mu41)) + + # Fully Lorentz-contracted expressions, these return scalars: + + def add_delta(ne): + return ne * eye(4) # DiracSpinorIndex.delta(DiracSpinorIndex.auto_left, -DiracSpinorIndex.auto_right) + + t = (G(mu)*G(-mu)) + ts = add_delta(D) + assert _is_tensor_eq(tfunc(t), ts) + + t = (G(mu)*G(nu)*G(-mu)*G(-nu)) + ts = add_delta(2*D - D**2) # -8 + assert _is_tensor_eq(tfunc(t), ts) + + t = (G(mu)*G(nu)*G(-nu)*G(-mu)) + ts = add_delta(D**2) # 16 + assert _is_tensor_eq(tfunc(t), ts) + + t = (G(mu)*G(nu)*G(-rho)*G(-nu)*G(-mu)*G(rho)) + ts = add_delta(4*D - 4*D**2 + D**3) # 16 + assert _is_tensor_eq(tfunc(t), ts) + + t = (G(mu)*G(nu)*G(rho)*G(-rho)*G(-nu)*G(-mu)) + ts = add_delta(D**3) # 64 + assert _is_tensor_eq(tfunc(t), ts) + + t = (G(a1)*G(a2)*G(a3)*G(a4)*G(-a3)*G(-a1)*G(-a2)*G(-a4)) + ts = add_delta(-8*D + 16*D**2 - 8*D**3 + D**4) # -32 + assert _is_tensor_eq(tfunc(t), ts) + + t = (G(-mu)*G(-nu)*G(-rho)*G(-sigma)*G(nu)*G(mu)*G(sigma)*G(rho)) + ts = add_delta(-16*D + 24*D**2 - 8*D**3 + D**4) # 64 + assert _is_tensor_eq(tfunc(t), ts) + + t = (G(-mu)*G(nu)*G(-rho)*G(sigma)*G(rho)*G(-nu)*G(mu)*G(-sigma)) + ts = add_delta(8*D - 12*D**2 + 6*D**3 - D**4) # -32 + assert _is_tensor_eq(tfunc(t), ts) + + t = (G(a1)*G(a2)*G(a3)*G(a4)*G(a5)*G(-a3)*G(-a2)*G(-a1)*G(-a5)*G(-a4)) + ts = add_delta(64*D - 112*D**2 + 60*D**3 - 12*D**4 + D**5) # 256 + assert _is_tensor_eq(tfunc(t), ts) + + t = (G(a1)*G(a2)*G(a3)*G(a4)*G(a5)*G(-a3)*G(-a1)*G(-a2)*G(-a4)*G(-a5)) + ts = add_delta(64*D - 120*D**2 + 72*D**3 - 16*D**4 + D**5) # -128 + assert _is_tensor_eq(tfunc(t), ts) + + t = (G(a1)*G(a2)*G(a3)*G(a4)*G(a5)*G(a6)*G(-a3)*G(-a2)*G(-a1)*G(-a6)*G(-a5)*G(-a4)) + ts = add_delta(416*D - 816*D**2 + 528*D**3 - 144*D**4 + 18*D**5 - D**6) # -128 + assert _is_tensor_eq(tfunc(t), ts) + + t = (G(a1)*G(a2)*G(a3)*G(a4)*G(a5)*G(a6)*G(-a2)*G(-a3)*G(-a1)*G(-a6)*G(-a4)*G(-a5)) + ts = add_delta(416*D - 848*D**2 + 584*D**3 - 172*D**4 + 22*D**5 - D**6) # -128 + assert _is_tensor_eq(tfunc(t), ts) + + # Expressions with free indices: + + t = (G(mu)*G(nu)*G(rho)*G(sigma)*G(-mu)) + assert _is_tensor_eq(tfunc(t), (-2*G(sigma)*G(rho)*G(nu) + (4-D)*G(nu)*G(rho)*G(sigma))) + + t = (G(mu)*G(nu)*G(-mu)) + assert _is_tensor_eq(tfunc(t), (2-D)*G(nu)) + + t = (G(mu)*G(nu)*G(rho)*G(-mu)) + assert _is_tensor_eq(tfunc(t), 2*G(nu)*G(rho) + 2*G(rho)*G(nu) - (4-D)*G(nu)*G(rho)) + + t = 2*G(m2)*G(m0)*G(m1)*G(-m0)*G(-m1) + st = tfunc(t) + assert _is_tensor_eq(st, (D*(-2*D + 4))*G(m2)) + + t = G(m2)*G(m0)*G(m1)*G(-m0)*G(-m2) + st = tfunc(t) + assert _is_tensor_eq(st, ((-D + 2)**2)*G(m1)) + + t = G(m0)*G(m1)*G(m2)*G(m3)*G(-m1) + st = tfunc(t) + assert _is_tensor_eq(st, (D - 4)*G(m0)*G(m2)*G(m3) + 4*G(m0)*g(m2, m3)) + + t = G(m0)*G(m1)*G(m2)*G(m3)*G(-m1)*G(-m0) + st = tfunc(t) + assert _is_tensor_eq(st, ((D - 4)**2)*G(m2)*G(m3) + (8*D - 16)*g(m2, m3)) + + t = G(m2)*G(m0)*G(m1)*G(-m2)*G(-m0) + st = tfunc(t) + assert _is_tensor_eq(st, ((-D + 2)*(D - 4) + 4)*G(m1)) + + t = G(m3)*G(m1)*G(m0)*G(m2)*G(-m3)*G(-m0)*G(-m2) + st = tfunc(t) + assert _is_tensor_eq(st, (-4*D + (-D + 2)**2*(D - 4) + 8)*G(m1)) + + t = 2*G(m0)*G(m1)*G(m2)*G(m3)*G(-m0) + st = tfunc(t) + assert _is_tensor_eq(st, ((-2*D + 8)*G(m1)*G(m2)*G(m3) - 4*G(m3)*G(m2)*G(m1))) + + t = G(m5)*G(m0)*G(m1)*G(m4)*G(m2)*G(-m4)*G(m3)*G(-m0) + st = tfunc(t) + assert _is_tensor_eq(st, (((-D + 2)*(-D + 4))*G(m5)*G(m1)*G(m2)*G(m3) + (2*D - 4)*G(m5)*G(m3)*G(m2)*G(m1))) + + t = -G(m0)*G(m1)*G(m2)*G(m3)*G(-m0)*G(m4) + st = tfunc(t) + assert _is_tensor_eq(st, ((D - 4)*G(m1)*G(m2)*G(m3)*G(m4) + 2*G(m3)*G(m2)*G(m1)*G(m4))) + + t = G(-m5)*G(m0)*G(m1)*G(m2)*G(m3)*G(m4)*G(-m0)*G(m5) + st = tfunc(t) + + result1 = ((-D + 4)**2 + 4)*G(m1)*G(m2)*G(m3)*G(m4) +\ + (4*D - 16)*G(m3)*G(m2)*G(m1)*G(m4) + (4*D - 16)*G(m4)*G(m1)*G(m2)*G(m3)\ + + 4*G(m2)*G(m1)*G(m4)*G(m3) + 4*G(m3)*G(m4)*G(m1)*G(m2) +\ + 4*G(m4)*G(m3)*G(m2)*G(m1) + + # Kahane's algorithm yields this result, which is equivalent to `result1` + # in four dimensions, but is not automatically recognized as equal: + result2 = 8*G(m1)*G(m2)*G(m3)*G(m4) + 8*G(m4)*G(m3)*G(m2)*G(m1) + + if D == 4: + assert _is_tensor_eq(st, (result1)) or _is_tensor_eq(st, (result2)) + else: + assert _is_tensor_eq(st, (result1)) + + # and a few very simple cases, with no contracted indices: + + t = G(m0) + st = tfunc(t) + assert _is_tensor_eq(st, t) + + t = -7*G(m0) + st = tfunc(t) + assert _is_tensor_eq(st, t) + + t = 224*G(m0)*G(m1)*G(-m2)*G(m3) + st = tfunc(t) + assert _is_tensor_eq(st, t) + + +def test_kahane_algorithm(): + # Wrap this function to convert to and from TIDS: + + def tfunc(e): + return _simplify_single_line(e) + + execute_gamma_simplify_tests_for_function(tfunc, D=4) + + +def test_kahane_simplify1(): + i0,i1,i2,i3,i4,i5,i6,i7,i8,i9,i10,i11,i12,i13,i14,i15 = tensor_indices('i0:16', LorentzIndex) + mu, nu, rho, sigma = tensor_indices("mu, nu, rho, sigma", LorentzIndex) + D = 4 + t = G(i0)*G(i1) + r = kahane_simplify(t) + assert r.equals(t) + + t = G(i0)*G(i1)*G(-i0) + r = kahane_simplify(t) + assert r.equals(-2*G(i1)) + t = G(i0)*G(i1)*G(-i0) + r = kahane_simplify(t) + assert r.equals(-2*G(i1)) + + t = G(i0)*G(i1) + r = kahane_simplify(t) + assert r.equals(t) + t = G(i0)*G(i1) + r = kahane_simplify(t) + assert r.equals(t) + t = G(i0)*G(-i0) + r = kahane_simplify(t) + assert r.equals(4*eye(4)) + t = G(i0)*G(-i0) + r = kahane_simplify(t) + assert r.equals(4*eye(4)) + t = G(i0)*G(-i0) + r = kahane_simplify(t) + assert r.equals(4*eye(4)) + t = G(i0)*G(i1)*G(-i0) + r = kahane_simplify(t) + assert r.equals(-2*G(i1)) + t = G(i0)*G(i1)*G(-i0)*G(-i1) + r = kahane_simplify(t) + assert r.equals((2*D - D**2)*eye(4)) + t = G(i0)*G(i1)*G(-i0)*G(-i1) + r = kahane_simplify(t) + assert r.equals((2*D - D**2)*eye(4)) + t = G(i0)*G(-i0)*G(i1)*G(-i1) + r = kahane_simplify(t) + assert r.equals(16*eye(4)) + t = (G(mu)*G(nu)*G(-nu)*G(-mu)) + r = kahane_simplify(t) + assert r.equals(D**2*eye(4)) + t = (G(mu)*G(nu)*G(-nu)*G(-mu)) + r = kahane_simplify(t) + assert r.equals(D**2*eye(4)) + t = (G(mu)*G(nu)*G(-nu)*G(-mu)) + r = kahane_simplify(t) + assert r.equals(D**2*eye(4)) + t = (G(mu)*G(nu)*G(-rho)*G(-nu)*G(-mu)*G(rho)) + r = kahane_simplify(t) + assert r.equals((4*D - 4*D**2 + D**3)*eye(4)) + t = (G(-mu)*G(-nu)*G(-rho)*G(-sigma)*G(nu)*G(mu)*G(sigma)*G(rho)) + r = kahane_simplify(t) + assert r.equals((-16*D + 24*D**2 - 8*D**3 + D**4)*eye(4)) + t = (G(-mu)*G(nu)*G(-rho)*G(sigma)*G(rho)*G(-nu)*G(mu)*G(-sigma)) + r = kahane_simplify(t) + assert r.equals((8*D - 12*D**2 + 6*D**3 - D**4)*eye(4)) + + # Expressions with free indices: + t = (G(mu)*G(nu)*G(rho)*G(sigma)*G(-mu)) + r = kahane_simplify(t) + assert r.equals(-2*G(sigma)*G(rho)*G(nu)) + t = (G(mu)*G(-mu)*G(rho)*G(sigma)) + r = kahane_simplify(t) + assert r.equals(4*G(rho)*G(sigma)) + t = (G(rho)*G(sigma)*G(mu)*G(-mu)) + r = kahane_simplify(t) + assert r.equals(4*G(rho)*G(sigma)) + +def test_gamma_matrix_class(): + i, j, k = tensor_indices('i,j,k', LorentzIndex) + + # define another type of TensorHead to see if exprs are correctly handled: + A = TensorHead('A', [LorentzIndex]) + + t = A(k)*G(i)*G(-i) + ts = simplify_gamma_expression(t) + assert _is_tensor_eq(ts, Matrix([ + [4, 0, 0, 0], + [0, 4, 0, 0], + [0, 0, 4, 0], + [0, 0, 0, 4]])*A(k)) + + t = G(i)*A(k)*G(j) + ts = simplify_gamma_expression(t) + assert _is_tensor_eq(ts, A(k)*G(i)*G(j)) + + execute_gamma_simplify_tests_for_function(simplify_gamma_expression, D=4) + + +def test_gamma_matrix_trace(): + g = LorentzIndex.metric + + m0, m1, m2, m3, m4, m5, m6 = tensor_indices('m0:7', LorentzIndex) + n0, n1, n2, n3, n4, n5 = tensor_indices('n0:6', LorentzIndex) + + # working in D=4 dimensions + D = 4 + + # traces of odd number of gamma matrices are zero: + t = G(m0) + t1 = gamma_trace(t) + assert t1.equals(0) + + t = G(m0)*G(m1)*G(m2) + t1 = gamma_trace(t) + assert t1.equals(0) + + t = G(m0)*G(m1)*G(-m0) + t1 = gamma_trace(t) + assert t1.equals(0) + + t = G(m0)*G(m1)*G(m2)*G(m3)*G(m4) + t1 = gamma_trace(t) + assert t1.equals(0) + + # traces without internal contractions: + t = G(m0)*G(m1) + t1 = gamma_trace(t) + assert _is_tensor_eq(t1, 4*g(m0, m1)) + + t = G(m0)*G(m1)*G(m2)*G(m3) + t1 = gamma_trace(t) + t2 = -4*g(m0, m2)*g(m1, m3) + 4*g(m0, m1)*g(m2, m3) + 4*g(m0, m3)*g(m1, m2) + assert _is_tensor_eq(t1, t2) + + t = G(m0)*G(m1)*G(m2)*G(m3)*G(m4)*G(m5) + t1 = gamma_trace(t) + t2 = t1*g(-m0, -m5) + t2 = t2.contract_metric(g) + assert _is_tensor_eq(t2, D*gamma_trace(G(m1)*G(m2)*G(m3)*G(m4))) + + # traces of expressions with internal contractions: + t = G(m0)*G(-m0) + t1 = gamma_trace(t) + assert t1.equals(4*D) + + t = G(m0)*G(m1)*G(-m0)*G(-m1) + t1 = gamma_trace(t) + assert t1.equals(8*D - 4*D**2) + + t = G(m0)*G(m1)*G(m2)*G(m3)*G(m4)*G(-m0) + t1 = gamma_trace(t) + t2 = (-4*D)*g(m1, m3)*g(m2, m4) + (4*D)*g(m1, m2)*g(m3, m4) + \ + (4*D)*g(m1, m4)*g(m2, m3) + assert _is_tensor_eq(t1, t2) + + t = G(-m5)*G(m0)*G(m1)*G(m2)*G(m3)*G(m4)*G(-m0)*G(m5) + t1 = gamma_trace(t) + t2 = (32*D + 4*(-D + 4)**2 - 64)*(g(m1, m2)*g(m3, m4) - \ + g(m1, m3)*g(m2, m4) + g(m1, m4)*g(m2, m3)) + assert _is_tensor_eq(t1, t2) + + t = G(m0)*G(m1)*G(-m0)*G(m3) + t1 = gamma_trace(t) + assert t1.equals((-4*D + 8)*g(m1, m3)) + +# p, q = S1('p,q') +# ps = p(m0)*G(-m0) +# qs = q(m0)*G(-m0) +# t = ps*qs*ps*qs +# t1 = gamma_trace(t) +# assert t1 == 8*p(m0)*q(-m0)*p(m1)*q(-m1) - 4*p(m0)*p(-m0)*q(m1)*q(-m1) + + t = G(m0)*G(m1)*G(m2)*G(m3)*G(m4)*G(m5)*G(-m0)*G(-m1)*G(-m2)*G(-m3)*G(-m4)*G(-m5) + t1 = gamma_trace(t) + assert t1.equals(-4*D**6 + 120*D**5 - 1040*D**4 + 3360*D**3 - 4480*D**2 + 2048*D) + + t = G(m0)*G(m1)*G(n1)*G(m2)*G(n2)*G(m3)*G(m4)*G(-n2)*G(-n1)*G(-m0)*G(-m1)*G(-m2)*G(-m3)*G(-m4) + t1 = gamma_trace(t) + tresu = -7168*D + 16768*D**2 - 14400*D**3 + 5920*D**4 - 1232*D**5 + 120*D**6 - 4*D**7 + assert t1.equals(tresu) + + # checked with Mathematica + # In[1]:= <>> from sympy.physics.hydrogen import R_nl + >>> from sympy.abc import r, Z + >>> R_nl(1, 0, r, Z) + 2*sqrt(Z**3)*exp(-Z*r) + >>> R_nl(2, 0, r, Z) + sqrt(2)*(-Z*r + 2)*sqrt(Z**3)*exp(-Z*r/2)/4 + >>> R_nl(2, 1, r, Z) + sqrt(6)*Z*r*sqrt(Z**3)*exp(-Z*r/2)/12 + + For Hydrogen atom, you can just use the default value of Z=1: + + >>> R_nl(1, 0, r) + 2*exp(-r) + >>> R_nl(2, 0, r) + sqrt(2)*(2 - r)*exp(-r/2)/4 + >>> R_nl(3, 0, r) + 2*sqrt(3)*(2*r**2/9 - 2*r + 3)*exp(-r/3)/27 + + For Silver atom, you would use Z=47: + + >>> R_nl(1, 0, r, Z=47) + 94*sqrt(47)*exp(-47*r) + >>> R_nl(2, 0, r, Z=47) + 47*sqrt(94)*(2 - 47*r)*exp(-47*r/2)/4 + >>> R_nl(3, 0, r, Z=47) + 94*sqrt(141)*(4418*r**2/9 - 94*r + 3)*exp(-47*r/3)/27 + + The normalization of the radial wavefunction is: + + >>> from sympy import integrate, oo + >>> integrate(R_nl(1, 0, r)**2 * r**2, (r, 0, oo)) + 1 + >>> integrate(R_nl(2, 0, r)**2 * r**2, (r, 0, oo)) + 1 + >>> integrate(R_nl(2, 1, r)**2 * r**2, (r, 0, oo)) + 1 + + It holds for any atomic number: + + >>> integrate(R_nl(1, 0, r, Z=2)**2 * r**2, (r, 0, oo)) + 1 + >>> integrate(R_nl(2, 0, r, Z=3)**2 * r**2, (r, 0, oo)) + 1 + >>> integrate(R_nl(2, 1, r, Z=4)**2 * r**2, (r, 0, oo)) + 1 + + """ + # sympify arguments + n, l, r, Z = map(S, [n, l, r, Z]) + # radial quantum number + n_r = n - l - 1 + # rescaled "r" + a = 1/Z # Bohr radius + r0 = 2 * r / (n * a) + # normalization coefficient + C = sqrt((S(2)/(n*a))**3 * factorial(n_r) / (2*n*factorial(n + l))) + # This is an equivalent normalization coefficient, that can be found in + # some books. Both coefficients seem to be the same fast: + # C = S(2)/n**2 * sqrt(1/a**3 * factorial(n_r) / (factorial(n+l))) + return C * r0**l * assoc_laguerre(n_r, 2*l + 1, r0).expand() * exp(-r0/2) + + +def Psi_nlm(n, l, m, r, phi, theta, Z=1): + """ + Returns the Hydrogen wave function psi_{nlm}. It's the product of + the radial wavefunction R_{nl} and the spherical harmonic Y_{l}^{m}. + + Parameters + ========== + + n : integer + Principal Quantum Number which is + an integer with possible values as 1, 2, 3, 4,... + l : integer + ``l`` is the Angular Momentum Quantum Number with + values ranging from 0 to ``n-1``. + m : integer + ``m`` is the Magnetic Quantum Number with values + ranging from ``-l`` to ``l``. + r : + radial coordinate + phi : + azimuthal angle + theta : + polar angle + Z : + atomic number (1 for Hydrogen, 2 for Helium, ...) + + Everything is in Hartree atomic units. + + Examples + ======== + + >>> from sympy.physics.hydrogen import Psi_nlm + >>> from sympy import Symbol + >>> r=Symbol("r", positive=True) + >>> phi=Symbol("phi", real=True) + >>> theta=Symbol("theta", real=True) + >>> Z=Symbol("Z", positive=True, integer=True, nonzero=True) + >>> Psi_nlm(1,0,0,r,phi,theta,Z) + Z**(3/2)*exp(-Z*r)/sqrt(pi) + >>> Psi_nlm(2,1,1,r,phi,theta,Z) + -Z**(5/2)*r*exp(I*phi)*exp(-Z*r/2)*sin(theta)/(8*sqrt(pi)) + + Integrating the absolute square of a hydrogen wavefunction psi_{nlm} + over the whole space leads 1. + + The normalization of the hydrogen wavefunctions Psi_nlm is: + + >>> from sympy import integrate, conjugate, pi, oo, sin + >>> wf=Psi_nlm(2,1,1,r,phi,theta,Z) + >>> abs_sqrd=wf*conjugate(wf) + >>> jacobi=r**2*sin(theta) + >>> integrate(abs_sqrd*jacobi, (r,0,oo), (phi,0,2*pi), (theta,0,pi)) + 1 + """ + + # sympify arguments + n, l, m, r, phi, theta, Z = map(S, [n, l, m, r, phi, theta, Z]) + # check if values for n,l,m make physically sense + if n.is_integer and n < 1: + raise ValueError("'n' must be positive integer") + if l.is_integer and not (n > l): + raise ValueError("'n' must be greater than 'l'") + if m.is_integer and not (abs(m) <= l): + raise ValueError("|'m'| must be less or equal 'l'") + # return the hydrogen wave function + return R_nl(n, l, r, Z)*Ynm(l, m, theta, phi).expand(func=True) + + +def E_nl(n, Z=1): + """ + Returns the energy of the state (n, l) in Hartree atomic units. + + The energy does not depend on "l". + + Parameters + ========== + + n : integer + Principal Quantum Number which is + an integer with possible values as 1, 2, 3, 4,... + Z : + Atomic number (1 for Hydrogen, 2 for Helium, ...) + + Examples + ======== + + >>> from sympy.physics.hydrogen import E_nl + >>> from sympy.abc import n, Z + >>> E_nl(n, Z) + -Z**2/(2*n**2) + >>> E_nl(1) + -1/2 + >>> E_nl(2) + -1/8 + >>> E_nl(3) + -1/18 + >>> E_nl(3, 47) + -2209/18 + + """ + n, Z = S(n), S(Z) + if n.is_integer and (n < 1): + raise ValueError("'n' must be positive integer") + return -Z**2/(2*n**2) + + +def E_nl_dirac(n, l, spin_up=True, Z=1, c=Float("137.035999037")): + """ + Returns the relativistic energy of the state (n, l, spin) in Hartree atomic + units. + + The energy is calculated from the Dirac equation. The rest mass energy is + *not* included. + + Parameters + ========== + + n : integer + Principal Quantum Number which is + an integer with possible values as 1, 2, 3, 4,... + l : integer + ``l`` is the Angular Momentum Quantum Number with + values ranging from 0 to ``n-1``. + spin_up : + True if the electron spin is up (default), otherwise down + Z : + Atomic number (1 for Hydrogen, 2 for Helium, ...) + c : + Speed of light in atomic units. Default value is 137.035999037, + taken from https://arxiv.org/abs/1012.3627 + + Examples + ======== + + >>> from sympy.physics.hydrogen import E_nl_dirac + >>> E_nl_dirac(1, 0) + -0.500006656595360 + + >>> E_nl_dirac(2, 0) + -0.125002080189006 + >>> E_nl_dirac(2, 1) + -0.125000416028342 + >>> E_nl_dirac(2, 1, False) + -0.125002080189006 + + >>> E_nl_dirac(3, 0) + -0.0555562951740285 + >>> E_nl_dirac(3, 1) + -0.0555558020932949 + >>> E_nl_dirac(3, 1, False) + -0.0555562951740285 + >>> E_nl_dirac(3, 2) + -0.0555556377366884 + >>> E_nl_dirac(3, 2, False) + -0.0555558020932949 + + """ + n, l, Z, c = map(S, [n, l, Z, c]) + if not (l >= 0): + raise ValueError("'l' must be positive or zero") + if not (n > l): + raise ValueError("'n' must be greater than 'l'") + if (l == 0 and spin_up is False): + raise ValueError("Spin must be up for l==0.") + # skappa is sign*kappa, where sign contains the correct sign + if spin_up: + skappa = -l - 1 + else: + skappa = -l + beta = sqrt(skappa**2 - Z**2/c**2) + return c**2/sqrt(1 + Z**2/(n + skappa + beta)**2/c**2) - c**2 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/matrices.py b/venv/lib/python3.11/site-packages/sympy/physics/matrices.py new file mode 100644 index 0000000000000000000000000000000000000000..d91466220d63956053b91bd76b948ee677e7c191 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/matrices.py @@ -0,0 +1,176 @@ +"""Known matrices related to physics""" + +from sympy.core.numbers import I +from sympy.matrices.dense import MutableDenseMatrix as Matrix +from sympy.utilities.decorator import deprecated + + +def msigma(i): + r"""Returns a Pauli matrix `\sigma_i` with `i=1,2,3`. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Pauli_matrices + + Examples + ======== + + >>> from sympy.physics.matrices import msigma + >>> msigma(1) + Matrix([ + [0, 1], + [1, 0]]) + """ + if i == 1: + mat = ( + (0, 1), + (1, 0) + ) + elif i == 2: + mat = ( + (0, -I), + (I, 0) + ) + elif i == 3: + mat = ( + (1, 0), + (0, -1) + ) + else: + raise IndexError("Invalid Pauli index") + return Matrix(mat) + + +def pat_matrix(m, dx, dy, dz): + """Returns the Parallel Axis Theorem matrix to translate the inertia + matrix a distance of `(dx, dy, dz)` for a body of mass m. + + Examples + ======== + + To translate a body having a mass of 2 units a distance of 1 unit along + the `x`-axis we get: + + >>> from sympy.physics.matrices import pat_matrix + >>> pat_matrix(2, 1, 0, 0) + Matrix([ + [0, 0, 0], + [0, 2, 0], + [0, 0, 2]]) + + """ + dxdy = -dx*dy + dydz = -dy*dz + dzdx = -dz*dx + dxdx = dx**2 + dydy = dy**2 + dzdz = dz**2 + mat = ((dydy + dzdz, dxdy, dzdx), + (dxdy, dxdx + dzdz, dydz), + (dzdx, dydz, dydy + dxdx)) + return m*Matrix(mat) + + +def mgamma(mu, lower=False): + r"""Returns a Dirac gamma matrix `\gamma^\mu` in the standard + (Dirac) representation. + + Explanation + =========== + + If you want `\gamma_\mu`, use ``gamma(mu, True)``. + + We use a convention: + + `\gamma^5 = i \cdot \gamma^0 \cdot \gamma^1 \cdot \gamma^2 \cdot \gamma^3` + + `\gamma_5 = i \cdot \gamma_0 \cdot \gamma_1 \cdot \gamma_2 \cdot \gamma_3 = - \gamma^5` + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gamma_matrices + + Examples + ======== + + >>> from sympy.physics.matrices import mgamma + >>> mgamma(1) + Matrix([ + [ 0, 0, 0, 1], + [ 0, 0, 1, 0], + [ 0, -1, 0, 0], + [-1, 0, 0, 0]]) + """ + if mu not in (0, 1, 2, 3, 5): + raise IndexError("Invalid Dirac index") + if mu == 0: + mat = ( + (1, 0, 0, 0), + (0, 1, 0, 0), + (0, 0, -1, 0), + (0, 0, 0, -1) + ) + elif mu == 1: + mat = ( + (0, 0, 0, 1), + (0, 0, 1, 0), + (0, -1, 0, 0), + (-1, 0, 0, 0) + ) + elif mu == 2: + mat = ( + (0, 0, 0, -I), + (0, 0, I, 0), + (0, I, 0, 0), + (-I, 0, 0, 0) + ) + elif mu == 3: + mat = ( + (0, 0, 1, 0), + (0, 0, 0, -1), + (-1, 0, 0, 0), + (0, 1, 0, 0) + ) + elif mu == 5: + mat = ( + (0, 0, 1, 0), + (0, 0, 0, 1), + (1, 0, 0, 0), + (0, 1, 0, 0) + ) + m = Matrix(mat) + if lower: + if mu in (1, 2, 3, 5): + m = -m + return m + +#Minkowski tensor using the convention (+,-,-,-) used in the Quantum Field +#Theory +minkowski_tensor = Matrix( ( + (1, 0, 0, 0), + (0, -1, 0, 0), + (0, 0, -1, 0), + (0, 0, 0, -1) +)) + + +@deprecated( + """ + The sympy.physics.matrices.mdft method is deprecated. Use + sympy.DFT(n).as_explicit() instead. + """, + deprecated_since_version="1.9", + active_deprecations_target="deprecated-physics-mdft", +) +def mdft(n): + r""" + .. deprecated:: 1.9 + + Use DFT from sympy.matrices.expressions.fourier instead. + + To get identical behavior to ``mdft(n)``, use ``DFT(n).as_explicit()``. + """ + from sympy.matrices.expressions.fourier import DFT + return DFT(n).as_mutable() diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..afd8c071a2af4fd201d5b2371594b19e4a68edda --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/__init__.py @@ -0,0 +1,90 @@ +__all__ = [ + 'vector', + + 'CoordinateSym', 'ReferenceFrame', 'Dyadic', 'Vector', 'Point', 'cross', + 'dot', 'express', 'time_derivative', 'outer', 'kinematic_equations', + 'get_motion_params', 'partial_velocity', 'dynamicsymbols', 'vprint', + 'vsstrrepr', 'vsprint', 'vpprint', 'vlatex', 'init_vprinting', 'curl', + 'divergence', 'gradient', 'is_conservative', 'is_solenoidal', + 'scalar_potential', 'scalar_potential_difference', + + 'KanesMethod', + + 'RigidBody', + + 'linear_momentum', 'angular_momentum', 'kinetic_energy', 'potential_energy', + 'Lagrangian', 'mechanics_printing', 'mprint', 'msprint', 'mpprint', + 'mlatex', 'msubs', 'find_dynamicsymbols', + + 'inertia', 'inertia_of_point_mass', 'Inertia', + + 'Force', 'Torque', + + 'Particle', + + 'LagrangesMethod', + + 'Linearizer', + + 'Body', + + 'SymbolicSystem', 'System', + + 'PinJoint', 'PrismaticJoint', 'CylindricalJoint', 'PlanarJoint', + 'SphericalJoint', 'WeldJoint', + + 'JointsMethod', + + 'WrappingCylinder', 'WrappingGeometryBase', 'WrappingSphere', + + 'PathwayBase', 'LinearPathway', 'ObstacleSetPathway', 'WrappingPathway', + + 'ActuatorBase', 'ForceActuator', 'LinearDamper', 'LinearSpring', + 'TorqueActuator', 'DuffingSpring', 'CoulombKineticFriction', +] + +from sympy.physics import vector + +from sympy.physics.vector import (CoordinateSym, ReferenceFrame, Dyadic, Vector, Point, + cross, dot, express, time_derivative, outer, kinematic_equations, + get_motion_params, partial_velocity, dynamicsymbols, vprint, + vsstrrepr, vsprint, vpprint, vlatex, init_vprinting, curl, divergence, + gradient, is_conservative, is_solenoidal, scalar_potential, + scalar_potential_difference) + +from .kane import KanesMethod + +from .rigidbody import RigidBody + +from .functions import (linear_momentum, angular_momentum, kinetic_energy, + potential_energy, Lagrangian, mechanics_printing, + mprint, msprint, mpprint, mlatex, msubs, + find_dynamicsymbols) + +from .inertia import inertia, inertia_of_point_mass, Inertia + +from .loads import Force, Torque + +from .particle import Particle + +from .lagrange import LagrangesMethod + +from .linearize import Linearizer + +from .body import Body + +from .system import SymbolicSystem, System + +from .jointsmethod import JointsMethod + +from .joint import (PinJoint, PrismaticJoint, CylindricalJoint, PlanarJoint, + SphericalJoint, WeldJoint) + +from .wrapping_geometry import (WrappingCylinder, WrappingGeometryBase, + WrappingSphere) + +from .pathway import (PathwayBase, LinearPathway, ObstacleSetPathway, + WrappingPathway) + +from .actuator import (ActuatorBase, ForceActuator, LinearDamper, LinearSpring, + TorqueActuator, DuffingSpring, CoulombKineticFriction) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/actuator.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/actuator.py new file mode 100644 index 0000000000000000000000000000000000000000..625b3e55019e7545c6dfed073d388acba91a324c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/actuator.py @@ -0,0 +1,1147 @@ +"""Implementations of actuators for linked force and torque application.""" + +from abc import ABC, abstractmethod + +from sympy import S, sympify, exp, sign +from sympy.physics.mechanics.joint import PinJoint +from sympy.physics.mechanics.loads import Torque +from sympy.physics.mechanics.pathway import PathwayBase +from sympy.physics.mechanics.rigidbody import RigidBody +from sympy.physics.vector import ReferenceFrame, Vector + + +__all__ = [ + 'ActuatorBase', + 'ForceActuator', + 'LinearDamper', + 'LinearSpring', + 'TorqueActuator', + 'DuffingSpring', + 'CoulombKineticFriction', +] + + +class ActuatorBase(ABC): + """Abstract base class for all actuator classes to inherit from. + + Notes + ===== + + Instances of this class cannot be directly instantiated by users. However, + it can be used to created custom actuator types through subclassing. + + """ + + def __init__(self): + """Initializer for ``ActuatorBase``.""" + pass + + @abstractmethod + def to_loads(self): + """Loads required by the equations of motion method classes. + + Explanation + =========== + + ``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be + passed to the ``loads`` parameters of its ``kanes_equations`` method + when constructing the equations of motion. This method acts as a + utility to produce the correctly-structred pairs of points and vectors + required so that these can be easily concatenated with other items in + the list of loads and passed to ``KanesMethod.kanes_equations``. These + loads are also in the correct form to also be passed to the other + equations of motion method classes, e.g. ``LagrangesMethod``. + + """ + pass + + def __repr__(self): + """Default representation of an actuator.""" + return f'{self.__class__.__name__}()' + + +class ForceActuator(ActuatorBase): + """Force-producing actuator. + + Explanation + =========== + + A ``ForceActuator`` is an actuator that produces a (expansile) force along + its length. + + A force actuator uses a pathway instance to determine the direction and + number of forces that it applies to a system. Consider the simplest case + where a ``LinearPathway`` instance is used. This pathway is made up of two + points that can move relative to each other, and results in a pair of equal + and opposite forces acting on the endpoints. If the positive time-varying + Euclidean distance between the two points is defined, then the "extension + velocity" is the time derivative of this distance. The extension velocity + is positive when the two points are moving away from each other and + negative when moving closer to each other. The direction for the force + acting on either point is determined by constructing a unit vector directed + from the other point to this point. This establishes a sign convention such + that a positive force magnitude tends to push the points apart, this is the + meaning of "expansile" in this context. The following diagram shows the + positive force sense and the distance between the points:: + + P Q + o<--- F --->o + | | + |<--l(t)--->| + + Examples + ======== + + To construct an actuator, an expression (or symbol) must be supplied to + represent the force it can produce, alongside a pathway specifying its line + of action. Let's also create a global reference frame and spatially fix one + of the points in it while setting the other to be positioned such that it + can freely move in the frame's x direction specified by the coordinate + ``q``. + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import (ForceActuator, LinearPathway, + ... Point, ReferenceFrame) + >>> from sympy.physics.vector import dynamicsymbols + >>> N = ReferenceFrame('N') + >>> q = dynamicsymbols('q') + >>> force = symbols('F') + >>> pA, pB = Point('pA'), Point('pB') + >>> pA.set_vel(N, 0) + >>> pB.set_pos(pA, q*N.x) + >>> pB.pos_from(pA) + q(t)*N.x + >>> linear_pathway = LinearPathway(pA, pB) + >>> actuator = ForceActuator(force, linear_pathway) + >>> actuator + ForceActuator(F, LinearPathway(pA, pB)) + + Parameters + ========== + + force : Expr + The scalar expression defining the (expansile) force that the actuator + produces. + pathway : PathwayBase + The pathway that the actuator follows. This must be an instance of a + concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``. + + """ + + def __init__(self, force, pathway): + """Initializer for ``ForceActuator``. + + Parameters + ========== + + force : Expr + The scalar expression defining the (expansile) force that the + actuator produces. + pathway : PathwayBase + The pathway that the actuator follows. This must be an instance of + a concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``. + + """ + self.force = force + self.pathway = pathway + + @property + def force(self): + """The magnitude of the force produced by the actuator.""" + return self._force + + @force.setter + def force(self, force): + if hasattr(self, '_force'): + msg = ( + f'Can\'t set attribute `force` to {repr(force)} as it is ' + f'immutable.' + ) + raise AttributeError(msg) + self._force = sympify(force, strict=True) + + @property + def pathway(self): + """The ``Pathway`` defining the actuator's line of action.""" + return self._pathway + + @pathway.setter + def pathway(self, pathway): + if hasattr(self, '_pathway'): + msg = ( + f'Can\'t set attribute `pathway` to {repr(pathway)} as it is ' + f'immutable.' + ) + raise AttributeError(msg) + if not isinstance(pathway, PathwayBase): + msg = ( + f'Value {repr(pathway)} passed to `pathway` was of type ' + f'{type(pathway)}, must be {PathwayBase}.' + ) + raise TypeError(msg) + self._pathway = pathway + + def to_loads(self): + """Loads required by the equations of motion method classes. + + Explanation + =========== + + ``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be + passed to the ``loads`` parameters of its ``kanes_equations`` method + when constructing the equations of motion. This method acts as a + utility to produce the correctly-structred pairs of points and vectors + required so that these can be easily concatenated with other items in + the list of loads and passed to ``KanesMethod.kanes_equations``. These + loads are also in the correct form to also be passed to the other + equations of motion method classes, e.g. ``LagrangesMethod``. + + Examples + ======== + + The below example shows how to generate the loads produced by a force + actuator that follows a linear pathway. In this example we'll assume + that the force actuator is being used to model a simple linear spring. + First, create a linear pathway between two points separated by the + coordinate ``q`` in the ``x`` direction of the global frame ``N``. + + >>> from sympy.physics.mechanics import (LinearPathway, Point, + ... ReferenceFrame) + >>> from sympy.physics.vector import dynamicsymbols + >>> q = dynamicsymbols('q') + >>> N = ReferenceFrame('N') + >>> pA, pB = Point('pA'), Point('pB') + >>> pB.set_pos(pA, q*N.x) + >>> pathway = LinearPathway(pA, pB) + + Now create a symbol ``k`` to describe the spring's stiffness and + instantiate a force actuator that produces a (contractile) force + proportional to both the spring's stiffness and the pathway's length. + Note that actuator classes use the sign convention that expansile + forces are positive, so for a spring to produce a contractile force the + spring force needs to be calculated as the negative for the stiffness + multiplied by the length. + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import ForceActuator + >>> stiffness = symbols('k') + >>> spring_force = -stiffness*pathway.length + >>> spring = ForceActuator(spring_force, pathway) + + The forces produced by the spring can be generated in the list of loads + form that ``KanesMethod`` (and other equations of motion methods) + requires by calling the ``to_loads`` method. + + >>> spring.to_loads() + [(pA, k*q(t)*N.x), (pB, - k*q(t)*N.x)] + + A simple linear damper can be modeled in a similar way. Create another + symbol ``c`` to describe the dampers damping coefficient. This time + instantiate a force actuator that produces a force proportional to both + the damper's damping coefficient and the pathway's extension velocity. + Note that the damping force is negative as it acts in the opposite + direction to which the damper is changing in length. + + >>> damping_coefficient = symbols('c') + >>> damping_force = -damping_coefficient*pathway.extension_velocity + >>> damper = ForceActuator(damping_force, pathway) + + Again, the forces produces by the damper can be generated by calling + the ``to_loads`` method. + + >>> damper.to_loads() + [(pA, c*Derivative(q(t), t)*N.x), (pB, - c*Derivative(q(t), t)*N.x)] + + """ + return self.pathway.to_loads(self.force) + + def __repr__(self): + """Representation of a ``ForceActuator``.""" + return f'{self.__class__.__name__}({self.force}, {self.pathway})' + + +class LinearSpring(ForceActuator): + """A spring with its spring force as a linear function of its length. + + Explanation + =========== + + Note that the "linear" in the name ``LinearSpring`` refers to the fact that + the spring force is a linear function of the springs length. I.e. for a + linear spring with stiffness ``k``, distance between its ends of ``x``, and + an equilibrium length of ``0``, the spring force will be ``-k*x``, which is + a linear function in ``x``. To create a spring that follows a linear, or + straight, pathway between its two ends, a ``LinearPathway`` instance needs + to be passed to the ``pathway`` parameter. + + A ``LinearSpring`` is a subclass of ``ForceActuator`` and so follows the + same sign conventions for length, extension velocity, and the direction of + the forces it applies to its points of attachment on bodies. The sign + convention for the direction of forces is such that, for the case where a + linear spring is instantiated with a ``LinearPathway`` instance as its + pathway, they act to push the two ends of the spring away from one another. + Because springs produces a contractile force and acts to pull the two ends + together towards the equilibrium length when stretched, the scalar portion + of the forces on the endpoint are negative in order to flip the sign of the + forces on the endpoints when converted into vector quantities. The + following diagram shows the positive force sense and the distance between + the points:: + + P Q + o<--- F --->o + | | + |<--l(t)--->| + + Examples + ======== + + To construct a linear spring, an expression (or symbol) must be supplied to + represent the stiffness (spring constant) of the spring, alongside a + pathway specifying its line of action. Let's also create a global reference + frame and spatially fix one of the points in it while setting the other to + be positioned such that it can freely move in the frame's x direction + specified by the coordinate ``q``. + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import (LinearPathway, LinearSpring, + ... Point, ReferenceFrame) + >>> from sympy.physics.vector import dynamicsymbols + >>> N = ReferenceFrame('N') + >>> q = dynamicsymbols('q') + >>> stiffness = symbols('k') + >>> pA, pB = Point('pA'), Point('pB') + >>> pA.set_vel(N, 0) + >>> pB.set_pos(pA, q*N.x) + >>> pB.pos_from(pA) + q(t)*N.x + >>> linear_pathway = LinearPathway(pA, pB) + >>> spring = LinearSpring(stiffness, linear_pathway) + >>> spring + LinearSpring(k, LinearPathway(pA, pB)) + + This spring will produce a force that is proportional to both its stiffness + and the pathway's length. Note that this force is negative as SymPy's sign + convention for actuators is that negative forces are contractile. + + >>> spring.force + -k*sqrt(q(t)**2) + + To create a linear spring with a non-zero equilibrium length, an expression + (or symbol) can be passed to the ``equilibrium_length`` parameter on + construction on a ``LinearSpring`` instance. Let's create a symbol ``l`` + to denote a non-zero equilibrium length and create another linear spring. + + >>> l = symbols('l') + >>> spring = LinearSpring(stiffness, linear_pathway, equilibrium_length=l) + >>> spring + LinearSpring(k, LinearPathway(pA, pB), equilibrium_length=l) + + The spring force of this new spring is again proportional to both its + stiffness and the pathway's length. However, the spring will not produce + any force when ``q(t)`` equals ``l``. Note that the force will become + expansile when ``q(t)`` is less than ``l``, as expected. + + >>> spring.force + -k*(-l + sqrt(q(t)**2)) + + Parameters + ========== + + stiffness : Expr + The spring constant. + pathway : PathwayBase + The pathway that the actuator follows. This must be an instance of a + concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``. + equilibrium_length : Expr, optional + The length at which the spring is in equilibrium, i.e. it produces no + force. The default value is 0, i.e. the spring force is a linear + function of the pathway's length with no constant offset. + + See Also + ======== + + ForceActuator: force-producing actuator (superclass of ``LinearSpring``). + LinearPathway: straight-line pathway between a pair of points. + + """ + + def __init__(self, stiffness, pathway, equilibrium_length=S.Zero): + """Initializer for ``LinearSpring``. + + Parameters + ========== + + stiffness : Expr + The spring constant. + pathway : PathwayBase + The pathway that the actuator follows. This must be an instance of + a concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``. + equilibrium_length : Expr, optional + The length at which the spring is in equilibrium, i.e. it produces + no force. The default value is 0, i.e. the spring force is a linear + function of the pathway's length with no constant offset. + + """ + self.stiffness = stiffness + self.pathway = pathway + self.equilibrium_length = equilibrium_length + + @property + def force(self): + """The spring force produced by the linear spring.""" + return -self.stiffness*(self.pathway.length - self.equilibrium_length) + + @force.setter + def force(self, force): + raise AttributeError('Can\'t set computed attribute `force`.') + + @property + def stiffness(self): + """The spring constant for the linear spring.""" + return self._stiffness + + @stiffness.setter + def stiffness(self, stiffness): + if hasattr(self, '_stiffness'): + msg = ( + f'Can\'t set attribute `stiffness` to {repr(stiffness)} as it ' + f'is immutable.' + ) + raise AttributeError(msg) + self._stiffness = sympify(stiffness, strict=True) + + @property + def equilibrium_length(self): + """The length of the spring at which it produces no force.""" + return self._equilibrium_length + + @equilibrium_length.setter + def equilibrium_length(self, equilibrium_length): + if hasattr(self, '_equilibrium_length'): + msg = ( + f'Can\'t set attribute `equilibrium_length` to ' + f'{repr(equilibrium_length)} as it is immutable.' + ) + raise AttributeError(msg) + self._equilibrium_length = sympify(equilibrium_length, strict=True) + + def __repr__(self): + """Representation of a ``LinearSpring``.""" + string = f'{self.__class__.__name__}({self.stiffness}, {self.pathway}' + if self.equilibrium_length == S.Zero: + string += ')' + else: + string += f', equilibrium_length={self.equilibrium_length})' + return string + + +class LinearDamper(ForceActuator): + """A damper whose force is a linear function of its extension velocity. + + Explanation + =========== + + Note that the "linear" in the name ``LinearDamper`` refers to the fact that + the damping force is a linear function of the damper's rate of change in + its length. I.e. for a linear damper with damping ``c`` and extension + velocity ``v``, the damping force will be ``-c*v``, which is a linear + function in ``v``. To create a damper that follows a linear, or straight, + pathway between its two ends, a ``LinearPathway`` instance needs to be + passed to the ``pathway`` parameter. + + A ``LinearDamper`` is a subclass of ``ForceActuator`` and so follows the + same sign conventions for length, extension velocity, and the direction of + the forces it applies to its points of attachment on bodies. The sign + convention for the direction of forces is such that, for the case where a + linear damper is instantiated with a ``LinearPathway`` instance as its + pathway, they act to push the two ends of the damper away from one another. + Because dampers produce a force that opposes the direction of change in + length, when extension velocity is positive the scalar portions of the + forces applied at the two endpoints are negative in order to flip the sign + of the forces on the endpoints wen converted into vector quantities. When + extension velocity is negative (i.e. when the damper is shortening), the + scalar portions of the fofces applied are also negative so that the signs + cancel producing forces on the endpoints that are in the same direction as + the positive sign convention for the forces at the endpoints of the pathway + (i.e. they act to push the endpoints away from one another). The following + diagram shows the positive force sense and the distance between the + points:: + + P Q + o<--- F --->o + | | + |<--l(t)--->| + + Examples + ======== + + To construct a linear damper, an expression (or symbol) must be supplied to + represent the damping coefficient of the damper (we'll use the symbol + ``c``), alongside a pathway specifying its line of action. Let's also + create a global reference frame and spatially fix one of the points in it + while setting the other to be positioned such that it can freely move in + the frame's x direction specified by the coordinate ``q``. The velocity + that the two points move away from one another can be specified by the + coordinate ``u`` where ``u`` is the first time derivative of ``q`` + (i.e., ``u = Derivative(q(t), t)``). + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import (LinearDamper, LinearPathway, + ... Point, ReferenceFrame) + >>> from sympy.physics.vector import dynamicsymbols + >>> N = ReferenceFrame('N') + >>> q = dynamicsymbols('q') + >>> damping = symbols('c') + >>> pA, pB = Point('pA'), Point('pB') + >>> pA.set_vel(N, 0) + >>> pB.set_pos(pA, q*N.x) + >>> pB.pos_from(pA) + q(t)*N.x + >>> pB.vel(N) + Derivative(q(t), t)*N.x + >>> linear_pathway = LinearPathway(pA, pB) + >>> damper = LinearDamper(damping, linear_pathway) + >>> damper + LinearDamper(c, LinearPathway(pA, pB)) + + This damper will produce a force that is proportional to both its damping + coefficient and the pathway's extension length. Note that this force is + negative as SymPy's sign convention for actuators is that negative forces + are contractile and the damping force of the damper will oppose the + direction of length change. + + >>> damper.force + -c*sqrt(q(t)**2)*Derivative(q(t), t)/q(t) + + Parameters + ========== + + damping : Expr + The damping constant. + pathway : PathwayBase + The pathway that the actuator follows. This must be an instance of a + concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``. + + See Also + ======== + + ForceActuator: force-producing actuator (superclass of ``LinearDamper``). + LinearPathway: straight-line pathway between a pair of points. + + """ + + def __init__(self, damping, pathway): + """Initializer for ``LinearDamper``. + + Parameters + ========== + + damping : Expr + The damping constant. + pathway : PathwayBase + The pathway that the actuator follows. This must be an instance of + a concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``. + + """ + self.damping = damping + self.pathway = pathway + + @property + def force(self): + """The damping force produced by the linear damper.""" + return -self.damping*self.pathway.extension_velocity + + @force.setter + def force(self, force): + raise AttributeError('Can\'t set computed attribute `force`.') + + @property + def damping(self): + """The damping constant for the linear damper.""" + return self._damping + + @damping.setter + def damping(self, damping): + if hasattr(self, '_damping'): + msg = ( + f'Can\'t set attribute `damping` to {repr(damping)} as it is ' + f'immutable.' + ) + raise AttributeError(msg) + self._damping = sympify(damping, strict=True) + + def __repr__(self): + """Representation of a ``LinearDamper``.""" + return f'{self.__class__.__name__}({self.damping}, {self.pathway})' + + +class TorqueActuator(ActuatorBase): + """Torque-producing actuator. + + Explanation + =========== + + A ``TorqueActuator`` is an actuator that produces a pair of equal and + opposite torques on a pair of bodies. + + Examples + ======== + + To construct a torque actuator, an expression (or symbol) must be supplied + to represent the torque it can produce, alongside a vector specifying the + axis about which the torque will act, and a pair of frames on which the + torque will act. + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import (ReferenceFrame, RigidBody, + ... TorqueActuator) + >>> N = ReferenceFrame('N') + >>> A = ReferenceFrame('A') + >>> torque = symbols('T') + >>> axis = N.z + >>> parent = RigidBody('parent', frame=N) + >>> child = RigidBody('child', frame=A) + >>> bodies = (child, parent) + >>> actuator = TorqueActuator(torque, axis, *bodies) + >>> actuator + TorqueActuator(T, axis=N.z, target_frame=A, reaction_frame=N) + + Note that because torques actually act on frames, not bodies, + ``TorqueActuator`` will extract the frame associated with a ``RigidBody`` + when one is passed instead of a ``ReferenceFrame``. + + Parameters + ========== + + torque : Expr + The scalar expression defining the torque that the actuator produces. + axis : Vector + The axis about which the actuator applies torques. + target_frame : ReferenceFrame | RigidBody + The primary frame on which the actuator will apply the torque. + reaction_frame : ReferenceFrame | RigidBody | None + The secondary frame on which the actuator will apply the torque. Note + that the (equal and opposite) reaction torque is applied to this frame. + + """ + + def __init__(self, torque, axis, target_frame, reaction_frame=None): + """Initializer for ``TorqueActuator``. + + Parameters + ========== + + torque : Expr + The scalar expression defining the torque that the actuator + produces. + axis : Vector + The axis about which the actuator applies torques. + target_frame : ReferenceFrame | RigidBody + The primary frame on which the actuator will apply the torque. + reaction_frame : ReferenceFrame | RigidBody | None + The secondary frame on which the actuator will apply the torque. + Note that the (equal and opposite) reaction torque is applied to + this frame. + + """ + self.torque = torque + self.axis = axis + self.target_frame = target_frame + self.reaction_frame = reaction_frame + + @classmethod + def at_pin_joint(cls, torque, pin_joint): + """Alternate constructor to instantiate from a ``PinJoint`` instance. + + Examples + ======== + + To create a pin joint the ``PinJoint`` class requires a name, parent + body, and child body to be passed to its constructor. It is also + possible to control the joint axis using the ``joint_axis`` keyword + argument. In this example let's use the parent body's reference frame's + z-axis as the joint axis. + + >>> from sympy.physics.mechanics import (PinJoint, ReferenceFrame, + ... RigidBody, TorqueActuator) + >>> N = ReferenceFrame('N') + >>> A = ReferenceFrame('A') + >>> parent = RigidBody('parent', frame=N) + >>> child = RigidBody('child', frame=A) + >>> pin_joint = PinJoint( + ... 'pin', + ... parent, + ... child, + ... joint_axis=N.z, + ... ) + + Let's also create a symbol ``T`` that will represent the torque applied + by the torque actuator. + + >>> from sympy import symbols + >>> torque = symbols('T') + + To create the torque actuator from the ``torque`` and ``pin_joint`` + variables previously instantiated, these can be passed to the alternate + constructor class method ``at_pin_joint`` of the ``TorqueActuator`` + class. It should be noted that a positive torque will cause a positive + displacement of the joint coordinate or that the torque is applied on + the child body with a reaction torque on the parent. + + >>> actuator = TorqueActuator.at_pin_joint(torque, pin_joint) + >>> actuator + TorqueActuator(T, axis=N.z, target_frame=A, reaction_frame=N) + + Parameters + ========== + + torque : Expr + The scalar expression defining the torque that the actuator + produces. + pin_joint : PinJoint + The pin joint, and by association the parent and child bodies, on + which the torque actuator will act. The pair of bodies acted upon + by the torque actuator are the parent and child bodies of the pin + joint, with the child acting as the reaction body. The pin joint's + axis is used as the axis about which the torque actuator will apply + its torque. + + """ + if not isinstance(pin_joint, PinJoint): + msg = ( + f'Value {repr(pin_joint)} passed to `pin_joint` was of type ' + f'{type(pin_joint)}, must be {PinJoint}.' + ) + raise TypeError(msg) + return cls( + torque, + pin_joint.joint_axis, + pin_joint.child_interframe, + pin_joint.parent_interframe, + ) + + @property + def torque(self): + """The magnitude of the torque produced by the actuator.""" + return self._torque + + @torque.setter + def torque(self, torque): + if hasattr(self, '_torque'): + msg = ( + f'Can\'t set attribute `torque` to {repr(torque)} as it is ' + f'immutable.' + ) + raise AttributeError(msg) + self._torque = sympify(torque, strict=True) + + @property + def axis(self): + """The axis about which the torque acts.""" + return self._axis + + @axis.setter + def axis(self, axis): + if hasattr(self, '_axis'): + msg = ( + f'Can\'t set attribute `axis` to {repr(axis)} as it is ' + f'immutable.' + ) + raise AttributeError(msg) + if not isinstance(axis, Vector): + msg = ( + f'Value {repr(axis)} passed to `axis` was of type ' + f'{type(axis)}, must be {Vector}.' + ) + raise TypeError(msg) + self._axis = axis + + @property + def target_frame(self): + """The primary reference frames on which the torque will act.""" + return self._target_frame + + @target_frame.setter + def target_frame(self, target_frame): + if hasattr(self, '_target_frame'): + msg = ( + f'Can\'t set attribute `target_frame` to {repr(target_frame)} ' + f'as it is immutable.' + ) + raise AttributeError(msg) + if isinstance(target_frame, RigidBody): + target_frame = target_frame.frame + elif not isinstance(target_frame, ReferenceFrame): + msg = ( + f'Value {repr(target_frame)} passed to `target_frame` was of ' + f'type {type(target_frame)}, must be {ReferenceFrame}.' + ) + raise TypeError(msg) + self._target_frame = target_frame + + @property + def reaction_frame(self): + """The primary reference frames on which the torque will act.""" + return self._reaction_frame + + @reaction_frame.setter + def reaction_frame(self, reaction_frame): + if hasattr(self, '_reaction_frame'): + msg = ( + f'Can\'t set attribute `reaction_frame` to ' + f'{repr(reaction_frame)} as it is immutable.' + ) + raise AttributeError(msg) + if isinstance(reaction_frame, RigidBody): + reaction_frame = reaction_frame.frame + elif ( + not isinstance(reaction_frame, ReferenceFrame) + and reaction_frame is not None + ): + msg = ( + f'Value {repr(reaction_frame)} passed to `reaction_frame` was ' + f'of type {type(reaction_frame)}, must be {ReferenceFrame}.' + ) + raise TypeError(msg) + self._reaction_frame = reaction_frame + + def to_loads(self): + """Loads required by the equations of motion method classes. + + Explanation + =========== + + ``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be + passed to the ``loads`` parameters of its ``kanes_equations`` method + when constructing the equations of motion. This method acts as a + utility to produce the correctly-structred pairs of points and vectors + required so that these can be easily concatenated with other items in + the list of loads and passed to ``KanesMethod.kanes_equations``. These + loads are also in the correct form to also be passed to the other + equations of motion method classes, e.g. ``LagrangesMethod``. + + Examples + ======== + + The below example shows how to generate the loads produced by a torque + actuator that acts on a pair of bodies attached by a pin joint. + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import (PinJoint, ReferenceFrame, + ... RigidBody, TorqueActuator) + >>> torque = symbols('T') + >>> N = ReferenceFrame('N') + >>> A = ReferenceFrame('A') + >>> parent = RigidBody('parent', frame=N) + >>> child = RigidBody('child', frame=A) + >>> pin_joint = PinJoint( + ... 'pin', + ... parent, + ... child, + ... joint_axis=N.z, + ... ) + >>> actuator = TorqueActuator.at_pin_joint(torque, pin_joint) + + The forces produces by the damper can be generated by calling the + ``to_loads`` method. + + >>> actuator.to_loads() + [(A, T*N.z), (N, - T*N.z)] + + Alternatively, if a torque actuator is created without a reaction frame + then the loads returned by the ``to_loads`` method will contain just + the single load acting on the target frame. + + >>> actuator = TorqueActuator(torque, N.z, N) + >>> actuator.to_loads() + [(N, T*N.z)] + + """ + loads = [ + Torque(self.target_frame, self.torque*self.axis), + ] + if self.reaction_frame is not None: + loads.append(Torque(self.reaction_frame, -self.torque*self.axis)) + return loads + + def __repr__(self): + """Representation of a ``TorqueActuator``.""" + string = ( + f'{self.__class__.__name__}({self.torque}, axis={self.axis}, ' + f'target_frame={self.target_frame}' + ) + if self.reaction_frame is not None: + string += f', reaction_frame={self.reaction_frame})' + else: + string += ')' + return string + + +class DuffingSpring(ForceActuator): + """A nonlinear spring based on the Duffing equation. + + Explanation + =========== + + Here, ``DuffingSpring`` represents the force exerted by a nonlinear spring based on the Duffing equation: + F = -beta*x-alpha*x**3, where x is the displacement from the equilibrium position, beta is the linear spring constant, + and alpha is the coefficient for the nonlinear cubic term. + + Parameters + ========== + + linear_stiffness : Expr + The linear stiffness coefficient (beta). + nonlinear_stiffness : Expr + The nonlinear stiffness coefficient (alpha). + pathway : PathwayBase + The pathway that the actuator follows. + equilibrium_length : Expr, optional + The length at which the spring is in equilibrium (x). + """ + + def __init__(self, linear_stiffness, nonlinear_stiffness, pathway, equilibrium_length=S.Zero): + self.linear_stiffness = sympify(linear_stiffness, strict=True) + self.nonlinear_stiffness = sympify(nonlinear_stiffness, strict=True) + self.equilibrium_length = sympify(equilibrium_length, strict=True) + + if not isinstance(pathway, PathwayBase): + raise TypeError("pathway must be an instance of PathwayBase.") + self._pathway = pathway + + @property + def linear_stiffness(self): + return self._linear_stiffness + + @linear_stiffness.setter + def linear_stiffness(self, linear_stiffness): + if hasattr(self, '_linear_stiffness'): + msg = ( + f'Can\'t set attribute `linear_stiffness` to ' + f'{repr(linear_stiffness)} as it is immutable.' + ) + raise AttributeError(msg) + self._linear_stiffness = sympify(linear_stiffness, strict=True) + + @property + def nonlinear_stiffness(self): + return self._nonlinear_stiffness + + @nonlinear_stiffness.setter + def nonlinear_stiffness(self, nonlinear_stiffness): + if hasattr(self, '_nonlinear_stiffness'): + msg = ( + f'Can\'t set attribute `nonlinear_stiffness` to ' + f'{repr(nonlinear_stiffness)} as it is immutable.' + ) + raise AttributeError(msg) + self._nonlinear_stiffness = sympify(nonlinear_stiffness, strict=True) + + @property + def pathway(self): + return self._pathway + + @pathway.setter + def pathway(self, pathway): + if hasattr(self, '_pathway'): + msg = ( + f'Can\'t set attribute `pathway` to {repr(pathway)} as it is ' + f'immutable.' + ) + raise AttributeError(msg) + if not isinstance(pathway, PathwayBase): + msg = ( + f'Value {repr(pathway)} passed to `pathway` was of type ' + f'{type(pathway)}, must be {PathwayBase}.' + ) + raise TypeError(msg) + self._pathway = pathway + + @property + def equilibrium_length(self): + return self._equilibrium_length + + @equilibrium_length.setter + def equilibrium_length(self, equilibrium_length): + if hasattr(self, '_equilibrium_length'): + msg = ( + f'Can\'t set attribute `equilibrium_length` to ' + f'{repr(equilibrium_length)} as it is immutable.' + ) + raise AttributeError(msg) + self._equilibrium_length = sympify(equilibrium_length, strict=True) + + @property + def force(self): + """The force produced by the Duffing spring.""" + displacement = self.pathway.length - self.equilibrium_length + return -self.linear_stiffness * displacement - self.nonlinear_stiffness * displacement**3 + + @force.setter + def force(self, force): + if hasattr(self, '_force'): + msg = ( + f'Can\'t set attribute `force` to {repr(force)} as it is ' + f'immutable.' + ) + raise AttributeError(msg) + self._force = sympify(force, strict=True) + + def __repr__(self): + return (f"{self.__class__.__name__}(" + f"{self.linear_stiffness}, {self.nonlinear_stiffness}, {self.pathway}, " + f"equilibrium_length={self.equilibrium_length})") + +class CoulombKineticFriction(ForceActuator): + r"""Coulomb kinetic friction with Stribeck and viscous effects. + + Explanation + =========== + + This represents a Coulomb kinetic friction with the Stribeck and viscous effect, + described by the function: + + .. math:: + F = (\mu_k f_n + (\mu_s - \mu_k) f_n e^{-(\frac{v}{v_s})^2}) \text{sign}(v) + \sigma v + + where :math:`\mu_k` is the coefficient of kinetic friction, :math:`\mu_s` is the + coefficient of static friction, :math:`f_n` is the normal force, :math:`v` is the + relative velocity, :math:`v_s` is the Stribeck friction coefficient, and + :math:`\sigma` is the viscous friction constant. + + The default friction force is :math:`F = \mu_k f_n`. + When specified, the actuator includes: + + - Stribeck effect: :math:`(\mu_s - \mu_k) f_n e^{-(\frac{v}{v_s})^2}` + - Viscous effect: :math:`\sigma v` + + Notes + ===== + + The actuator makes the following assumptions: + + - The actuator assumes relative motion is non-zero. + - The normal force is assumed to be a non-negative scalar. + - The resultant friction force is opposite to the velocity direction. + - Each point in the pathway is fixed within separate objects that are sliding relative to each other. In other words, these two points are fixed in the mutually sliding objects. + + This actuator has been tested for straightforward motions, like a block sliding + on a surface. + + The friction force is defined to always oppose the direction of relative velocity :math:`v`. + Specifically: + + - The default Coulomb friction force :math:`\mu_k f_n \text{sign}(v)` is opposite to :math:`v`. + - The Stribeck effect :math:`(\mu_s - \mu_k) f_n e^{-(\frac{v}{v_s})^2} \text{sign}(v)` is also opposite to :math:`v`. + - The viscous friction term :math:`\sigma v` is opposite to :math:`v`. + + Examples + ======== + + The below example shows how to generate the loads produced by a Coulomb kinetic + friction actuator in a mass-spring system with friction. + + >>> import sympy as sm + >>> from sympy.physics.mechanics import (dynamicsymbols, ReferenceFrame, Point, + ... LinearPathway, CoulombKineticFriction, LinearSpring, KanesMethod, Particle) + + >>> x, v = dynamicsymbols('x, v', real=True) + >>> m, g, k, mu_k, mu_s, v_s, sigma = sm.symbols('m, g, k, mu_k, mu_s, v_s, sigma') + + >>> N = ReferenceFrame('N') + >>> O, P = Point('O'), Point('P') + >>> O.set_vel(N, 0) + >>> P.set_pos(O, x*N.x) + + >>> pathway = LinearPathway(O, P) + >>> friction = CoulombKineticFriction(mu_k, m*g, pathway, v_s=v_s, sigma=sigma, mu_s=mu_k) + >>> spring = LinearSpring(k, pathway) + >>> block = Particle('block', point=P, mass=m) + + >>> kane = KanesMethod(N, (x,), (v,), kd_eqs=(x.diff() - v,)) + >>> friction.to_loads() + [(O, (g*m*mu_k*sign(sign(x(t))*Derivative(x(t), t)) + sigma*sign(x(t))*Derivative(x(t), t))*x(t)/Abs(x(t))*N.x), (P, (-g*m*mu_k*sign(sign(x(t))*Derivative(x(t), t)) - sigma*sign(x(t))*Derivative(x(t), t))*x(t)/Abs(x(t))*N.x)] + >>> loads = friction.to_loads() + spring.to_loads() + >>> fr, frstar = kane.kanes_equations([block], loads) + >>> eom = fr + frstar + >>> eom + Matrix([[-k*x(t) - m*Derivative(v(t), t) + (-g*m*mu_k*sign(v(t)*sign(x(t))) - sigma*v(t)*sign(x(t)))*x(t)/Abs(x(t))]]) + + Parameters + ========== + + f_n : sympifiable + The normal force between the surfaces. It should always be a non-negative scalar. + mu_k : sympifiable + The coefficient of kinetic friction. + pathway : PathwayBase + The pathway that the actuator follows. + v_s : sympifiable, optional + The Stribeck friction coefficient. + sigma : sympifiable, optional + The viscous friction coefficient. + mu_s : sympifiable, optional + The coefficient of static friction. Defaults to mu_k, meaning the Stribeck effect evaluates to 0 by default. + + References + ========== + + .. [Moore2022] https://moorepants.github.io/learn-multibody-dynamics/loads.html#friction. + .. [Flores2023] Paulo Flores, Jorge Ambrosio, Hamid M. Lankarani, + "Contact-impact events with friction in multibody dynamics: Back to basics", + Mechanism and Machine Theory, vol. 184, 2023. https://doi.org/10.1016/j.mechmachtheory.2023.105305. + .. [Rogner2017] I. Rogner, "Friction modelling for robotic applications with planar motion", + Chalmers University of Technology, Department of Electrical Engineering, 2017. + + """ + + def __init__(self, mu_k, f_n, pathway, *, v_s=None, sigma=None, mu_s=None): + self._mu_k = sympify(mu_k, strict=True) if mu_k is not None else 1 + self._mu_s = sympify(mu_s, strict=True) if mu_s is not None else self._mu_k + self._f_n = sympify(f_n, strict=True) + self._sigma = sympify(sigma, strict=True) if sigma is not None else 0 + self._v_s = sympify(v_s, strict=True) if v_s is not None or v_s == 0 else 0.01 + self.pathway = pathway + + @property + def mu_k(self): + """The coefficient of kinetic friction.""" + return self._mu_k + + @property + def mu_s(self): + """The coefficient of static friction.""" + return self._mu_s + + @property + def f_n(self): + """The normal force between the surfaces.""" + return self._f_n + + @property + def sigma(self): + """The viscous friction coefficient.""" + return self._sigma + + @property + def v_s(self): + """The Stribeck friction coefficient.""" + return self._v_s + + @property + def force(self): + v = self.pathway.extension_velocity + f_c = self.mu_k * self.f_n + f_max = self.mu_s * self.f_n + stribeck_term = (f_max - f_c) * exp(-(v / self.v_s)**2) if self.v_s is not None else 0 + viscous_term = self.sigma * v if self.sigma is not None else 0 + return (f_c + stribeck_term) * -sign(v) - viscous_term + + @force.setter + def force(self, force): + raise AttributeError('Can\'t set computed attribute `force`.') + + def __repr__(self): + return (f'{self.__class__.__name__}({self._mu_k}, {self._mu_s} ' + f'{self._f_n}, {self.pathway}, {self._v_s}, ' + f'{self._sigma})') diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/body.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/body.py new file mode 100644 index 0000000000000000000000000000000000000000..efc367158bbf51e7d9929318ac9286ba5c3fb3ac --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/body.py @@ -0,0 +1,710 @@ +from sympy import Symbol +from sympy.physics.vector import Point, Vector, ReferenceFrame, Dyadic +from sympy.physics.mechanics import RigidBody, Particle, Inertia +from sympy.physics.mechanics.body_base import BodyBase +from sympy.utilities.exceptions import sympy_deprecation_warning + +__all__ = ['Body'] + + +# XXX: We use type:ignore because the classes RigidBody and Particle have +# inconsistent parallel axis methods that take different numbers of arguments. +class Body(RigidBody, Particle): # type: ignore + """ + Body is a common representation of either a RigidBody or a Particle SymPy + object depending on what is passed in during initialization. If a mass is + passed in and central_inertia is left as None, the Particle object is + created. Otherwise a RigidBody object will be created. + + .. deprecated:: 1.13 + The Body class is deprecated. Its functionality is captured by + :class:`~.RigidBody` and :class:`~.Particle`. + + Explanation + =========== + + The attributes that Body possesses will be the same as a Particle instance + or a Rigid Body instance depending on which was created. Additional + attributes are listed below. + + Attributes + ========== + + name : string + The body's name + masscenter : Point + The point which represents the center of mass of the rigid body + frame : ReferenceFrame + The reference frame which the body is fixed in + mass : Sympifyable + The body's mass + inertia : (Dyadic, Point) + The body's inertia around its center of mass. This attribute is specific + to the rigid body form of Body and is left undefined for the Particle + form + loads : iterable + This list contains information on the different loads acting on the + Body. Forces are listed as a (point, vector) tuple and torques are + listed as (reference frame, vector) tuples. + + Parameters + ========== + + name : String + Defines the name of the body. It is used as the base for defining + body specific properties. + masscenter : Point, optional + A point that represents the center of mass of the body or particle. + If no point is given, a point is generated. + mass : Sympifyable, optional + A Sympifyable object which represents the mass of the body. If no + mass is passed, one is generated. + frame : ReferenceFrame, optional + The ReferenceFrame that represents the reference frame of the body. + If no frame is given, a frame is generated. + central_inertia : Dyadic, optional + Central inertia dyadic of the body. If none is passed while creating + RigidBody, a default inertia is generated. + + Examples + ======== + + As Body has been deprecated, the following examples are for illustrative + purposes only. The functionality of Body is fully captured by + :class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation + warning we can use the ignore_warnings context manager. + + >>> from sympy.utilities.exceptions import ignore_warnings + + Default behaviour. This results in the creation of a RigidBody object for + which the mass, mass center, frame and inertia attributes are given default + values. :: + + >>> from sympy.physics.mechanics import Body + >>> with ignore_warnings(DeprecationWarning): + ... body = Body('name_of_body') + + This next example demonstrates the code required to specify all of the + values of the Body object. Note this will also create a RigidBody version of + the Body object. :: + + >>> from sympy import Symbol + >>> from sympy.physics.mechanics import ReferenceFrame, Point, inertia + >>> from sympy.physics.mechanics import Body + >>> mass = Symbol('mass') + >>> masscenter = Point('masscenter') + >>> frame = ReferenceFrame('frame') + >>> ixx = Symbol('ixx') + >>> body_inertia = inertia(frame, ixx, 0, 0) + >>> with ignore_warnings(DeprecationWarning): + ... body = Body('name_of_body', masscenter, mass, frame, body_inertia) + + The minimal code required to create a Particle version of the Body object + involves simply passing in a name and a mass. :: + + >>> from sympy import Symbol + >>> from sympy.physics.mechanics import Body + >>> mass = Symbol('mass') + >>> with ignore_warnings(DeprecationWarning): + ... body = Body('name_of_body', mass=mass) + + The Particle version of the Body object can also receive a masscenter point + and a reference frame, just not an inertia. + """ + + def __init__(self, name, masscenter=None, mass=None, frame=None, + central_inertia=None): + sympy_deprecation_warning( + """ + Support for the Body class has been removed, as its functionality is + fully captured by RigidBody and Particle. + """, + deprecated_since_version="1.13", + active_deprecations_target="deprecated-mechanics-body-class" + ) + + self._loads = [] + + if frame is None: + frame = ReferenceFrame(name + '_frame') + + if masscenter is None: + masscenter = Point(name + '_masscenter') + + if central_inertia is None and mass is None: + ixx = Symbol(name + '_ixx') + iyy = Symbol(name + '_iyy') + izz = Symbol(name + '_izz') + izx = Symbol(name + '_izx') + ixy = Symbol(name + '_ixy') + iyz = Symbol(name + '_iyz') + _inertia = Inertia.from_inertia_scalars(masscenter, frame, ixx, iyy, + izz, ixy, iyz, izx) + else: + _inertia = (central_inertia, masscenter) + + if mass is None: + _mass = Symbol(name + '_mass') + else: + _mass = mass + + masscenter.set_vel(frame, 0) + + # If user passes masscenter and mass then a particle is created + # otherwise a rigidbody. As a result a body may or may not have inertia. + # Note: BodyBase.__init__ is used to prevent problems with super() calls in + # Particle and RigidBody arising due to multiple inheritance. + if central_inertia is None and mass is not None: + BodyBase.__init__(self, name, masscenter, _mass) + self.frame = frame + self._central_inertia = Dyadic(0) + else: + BodyBase.__init__(self, name, masscenter, _mass) + self.frame = frame + self.inertia = _inertia + + def __repr__(self): + if self.is_rigidbody: + return RigidBody.__repr__(self) + return Particle.__repr__(self) + + @property + def loads(self): + return self._loads + + @property + def x(self): + """The basis Vector for the Body, in the x direction.""" + return self.frame.x + + @property + def y(self): + """The basis Vector for the Body, in the y direction.""" + return self.frame.y + + @property + def z(self): + """The basis Vector for the Body, in the z direction.""" + return self.frame.z + + @property + def inertia(self): + """The body's inertia about a point; stored as (Dyadic, Point).""" + if self.is_rigidbody: + return RigidBody.inertia.fget(self) + return (self.central_inertia, self.masscenter) + + @inertia.setter + def inertia(self, I): + RigidBody.inertia.fset(self, I) + + @property + def is_rigidbody(self): + if hasattr(self, '_inertia'): + return True + return False + + def kinetic_energy(self, frame): + """Kinetic energy of the body. + + Parameters + ========== + + frame : ReferenceFrame or Body + The Body's angular velocity and the velocity of it's mass + center are typically defined with respect to an inertial frame but + any relevant frame in which the velocities are known can be supplied. + + Examples + ======== + + As Body has been deprecated, the following examples are for illustrative + purposes only. The functionality of Body is fully captured by + :class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation + warning we can use the ignore_warnings context manager. + + >>> from sympy.utilities.exceptions import ignore_warnings + >>> from sympy.physics.mechanics import Body, ReferenceFrame, Point + >>> from sympy import symbols + >>> m, v, r, omega = symbols('m v r omega') + >>> N = ReferenceFrame('N') + >>> O = Point('O') + >>> with ignore_warnings(DeprecationWarning): + ... P = Body('P', masscenter=O, mass=m) + >>> P.masscenter.set_vel(N, v * N.y) + >>> P.kinetic_energy(N) + m*v**2/2 + + >>> N = ReferenceFrame('N') + >>> b = ReferenceFrame('b') + >>> b.set_ang_vel(N, omega * b.x) + >>> P = Point('P') + >>> P.set_vel(N, v * N.x) + >>> with ignore_warnings(DeprecationWarning): + ... B = Body('B', masscenter=P, frame=b) + >>> B.kinetic_energy(N) + B_ixx*omega**2/2 + B_mass*v**2/2 + + See Also + ======== + + sympy.physics.mechanics : Particle, RigidBody + + """ + if isinstance(frame, Body): + frame = Body.frame + if self.is_rigidbody: + return RigidBody(self.name, self.masscenter, self.frame, self.mass, + (self.central_inertia, self.masscenter)).kinetic_energy(frame) + return Particle(self.name, self.masscenter, self.mass).kinetic_energy(frame) + + def apply_force(self, force, point=None, reaction_body=None, reaction_point=None): + """Add force to the body(s). + + Explanation + =========== + + Applies the force on self or equal and opposite forces on + self and other body if both are given on the desired point on the bodies. + The force applied on other body is taken opposite of self, i.e, -force. + + Parameters + ========== + + force: Vector + The force to be applied. + point: Point, optional + The point on self on which force is applied. + By default self's masscenter. + reaction_body: Body, optional + Second body on which equal and opposite force + is to be applied. + reaction_point : Point, optional + The point on other body on which equal and opposite + force is applied. By default masscenter of other body. + + Example + ======= + + As Body has been deprecated, the following examples are for illustrative + purposes only. The functionality of Body is fully captured by + :class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation + warning we can use the ignore_warnings context manager. + + >>> from sympy.utilities.exceptions import ignore_warnings + >>> from sympy import symbols + >>> from sympy.physics.mechanics import Body, Point, dynamicsymbols + >>> m, g = symbols('m g') + >>> with ignore_warnings(DeprecationWarning): + ... B = Body('B') + >>> force1 = m*g*B.z + >>> B.apply_force(force1) #Applying force on B's masscenter + >>> B.loads + [(B_masscenter, g*m*B_frame.z)] + + We can also remove some part of force from any point on the body by + adding the opposite force to the body on that point. + + >>> f1, f2 = dynamicsymbols('f1 f2') + >>> P = Point('P') #Considering point P on body B + >>> B.apply_force(f1*B.x + f2*B.y, P) + >>> B.loads + [(B_masscenter, g*m*B_frame.z), (P, f1(t)*B_frame.x + f2(t)*B_frame.y)] + + Let's remove f1 from point P on body B. + + >>> B.apply_force(-f1*B.x, P) + >>> B.loads + [(B_masscenter, g*m*B_frame.z), (P, f2(t)*B_frame.y)] + + To further demonstrate the use of ``apply_force`` attribute, + consider two bodies connected through a spring. + + >>> from sympy.physics.mechanics import Body, dynamicsymbols + >>> with ignore_warnings(DeprecationWarning): + ... N = Body('N') #Newtonion Frame + >>> x = dynamicsymbols('x') + >>> with ignore_warnings(DeprecationWarning): + ... B1 = Body('B1') + ... B2 = Body('B2') + >>> spring_force = x*N.x + + Now let's apply equal and opposite spring force to the bodies. + + >>> P1 = Point('P1') + >>> P2 = Point('P2') + >>> B1.apply_force(spring_force, point=P1, reaction_body=B2, reaction_point=P2) + + We can check the loads(forces) applied to bodies now. + + >>> B1.loads + [(P1, x(t)*N_frame.x)] + >>> B2.loads + [(P2, - x(t)*N_frame.x)] + + Notes + ===== + + If a new force is applied to a body on a point which already has some + force applied on it, then the new force is added to the already applied + force on that point. + + """ + + if not isinstance(point, Point): + if point is None: + point = self.masscenter # masscenter + else: + raise TypeError("Force must be applied to a point on the body.") + if not isinstance(force, Vector): + raise TypeError("Force must be a vector.") + + if reaction_body is not None: + reaction_body.apply_force(-force, point=reaction_point) + + for load in self._loads: + if point in load: + force += load[1] + self._loads.remove(load) + break + + self._loads.append((point, force)) + + def apply_torque(self, torque, reaction_body=None): + """Add torque to the body(s). + + Explanation + =========== + + Applies the torque on self or equal and opposite torques on + self and other body if both are given. + The torque applied on other body is taken opposite of self, + i.e, -torque. + + Parameters + ========== + + torque: Vector + The torque to be applied. + reaction_body: Body, optional + Second body on which equal and opposite torque + is to be applied. + + Example + ======= + + As Body has been deprecated, the following examples are for illustrative + purposes only. The functionality of Body is fully captured by + :class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation + warning we can use the ignore_warnings context manager. + + >>> from sympy.utilities.exceptions import ignore_warnings + >>> from sympy import symbols + >>> from sympy.physics.mechanics import Body, dynamicsymbols + >>> t = symbols('t') + >>> with ignore_warnings(DeprecationWarning): + ... B = Body('B') + >>> torque1 = t*B.z + >>> B.apply_torque(torque1) + >>> B.loads + [(B_frame, t*B_frame.z)] + + We can also remove some part of torque from the body by + adding the opposite torque to the body. + + >>> t1, t2 = dynamicsymbols('t1 t2') + >>> B.apply_torque(t1*B.x + t2*B.y) + >>> B.loads + [(B_frame, t1(t)*B_frame.x + t2(t)*B_frame.y + t*B_frame.z)] + + Let's remove t1 from Body B. + + >>> B.apply_torque(-t1*B.x) + >>> B.loads + [(B_frame, t2(t)*B_frame.y + t*B_frame.z)] + + To further demonstrate the use, let us consider two bodies such that + a torque `T` is acting on one body, and `-T` on the other. + + >>> from sympy.physics.mechanics import Body, dynamicsymbols + >>> with ignore_warnings(DeprecationWarning): + ... N = Body('N') #Newtonion frame + ... B1 = Body('B1') + ... B2 = Body('B2') + >>> v = dynamicsymbols('v') + >>> T = v*N.y #Torque + + Now let's apply equal and opposite torque to the bodies. + + >>> B1.apply_torque(T, B2) + + We can check the loads (torques) applied to bodies now. + + >>> B1.loads + [(B1_frame, v(t)*N_frame.y)] + >>> B2.loads + [(B2_frame, - v(t)*N_frame.y)] + + Notes + ===== + + If a new torque is applied on body which already has some torque applied on it, + then the new torque is added to the previous torque about the body's frame. + + """ + + if not isinstance(torque, Vector): + raise TypeError("A Vector must be supplied to add torque.") + + if reaction_body is not None: + reaction_body.apply_torque(-torque) + + for load in self._loads: + if self.frame in load: + torque += load[1] + self._loads.remove(load) + break + self._loads.append((self.frame, torque)) + + def clear_loads(self): + """ + Clears the Body's loads list. + + Example + ======= + + As Body has been deprecated, the following examples are for illustrative + purposes only. The functionality of Body is fully captured by + :class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation + warning we can use the ignore_warnings context manager. + + >>> from sympy.utilities.exceptions import ignore_warnings + >>> from sympy.physics.mechanics import Body + >>> with ignore_warnings(DeprecationWarning): + ... B = Body('B') + >>> force = B.x + B.y + >>> B.apply_force(force) + >>> B.loads + [(B_masscenter, B_frame.x + B_frame.y)] + >>> B.clear_loads() + >>> B.loads + [] + + """ + + self._loads = [] + + def remove_load(self, about=None): + """ + Remove load about a point or frame. + + Parameters + ========== + + about : Point or ReferenceFrame, optional + The point about which force is applied, + and is to be removed. + If about is None, then the torque about + self's frame is removed. + + Example + ======= + + As Body has been deprecated, the following examples are for illustrative + purposes only. The functionality of Body is fully captured by + :class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation + warning we can use the ignore_warnings context manager. + + >>> from sympy.utilities.exceptions import ignore_warnings + >>> from sympy.physics.mechanics import Body, Point + >>> with ignore_warnings(DeprecationWarning): + ... B = Body('B') + >>> P = Point('P') + >>> f1 = B.x + >>> f2 = B.y + >>> B.apply_force(f1) + >>> B.apply_force(f2, P) + >>> B.loads + [(B_masscenter, B_frame.x), (P, B_frame.y)] + + >>> B.remove_load(P) + >>> B.loads + [(B_masscenter, B_frame.x)] + + """ + + if about is not None: + if not isinstance(about, Point): + raise TypeError('Load is applied about Point or ReferenceFrame.') + else: + about = self.frame + + for load in self._loads: + if about in load: + self._loads.remove(load) + break + + def masscenter_vel(self, body): + """ + Returns the velocity of the mass center with respect to the provided + rigid body or reference frame. + + Parameters + ========== + + body: Body or ReferenceFrame + The rigid body or reference frame to calculate the velocity in. + + Example + ======= + + As Body has been deprecated, the following examples are for illustrative + purposes only. The functionality of Body is fully captured by + :class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation + warning we can use the ignore_warnings context manager. + + >>> from sympy.utilities.exceptions import ignore_warnings + >>> from sympy.physics.mechanics import Body + >>> with ignore_warnings(DeprecationWarning): + ... A = Body('A') + ... B = Body('B') + >>> A.masscenter.set_vel(B.frame, 5*B.frame.x) + >>> A.masscenter_vel(B) + 5*B_frame.x + >>> A.masscenter_vel(B.frame) + 5*B_frame.x + + """ + + if isinstance(body, ReferenceFrame): + frame=body + elif isinstance(body, Body): + frame = body.frame + return self.masscenter.vel(frame) + + def ang_vel_in(self, body): + """ + Returns this body's angular velocity with respect to the provided + rigid body or reference frame. + + Parameters + ========== + + body: Body or ReferenceFrame + The rigid body or reference frame to calculate the angular velocity in. + + Example + ======= + + As Body has been deprecated, the following examples are for illustrative + purposes only. The functionality of Body is fully captured by + :class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation + warning we can use the ignore_warnings context manager. + + >>> from sympy.utilities.exceptions import ignore_warnings + >>> from sympy.physics.mechanics import Body, ReferenceFrame + >>> with ignore_warnings(DeprecationWarning): + ... A = Body('A') + >>> N = ReferenceFrame('N') + >>> with ignore_warnings(DeprecationWarning): + ... B = Body('B', frame=N) + >>> A.frame.set_ang_vel(N, 5*N.x) + >>> A.ang_vel_in(B) + 5*N.x + >>> A.ang_vel_in(N) + 5*N.x + + """ + + if isinstance(body, ReferenceFrame): + frame=body + elif isinstance(body, Body): + frame = body.frame + return self.frame.ang_vel_in(frame) + + def dcm(self, body): + """ + Returns the direction cosine matrix of this body relative to the + provided rigid body or reference frame. + + Parameters + ========== + + body: Body or ReferenceFrame + The rigid body or reference frame to calculate the dcm. + + Example + ======= + + As Body has been deprecated, the following examples are for illustrative + purposes only. The functionality of Body is fully captured by + :class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation + warning we can use the ignore_warnings context manager. + + >>> from sympy.utilities.exceptions import ignore_warnings + >>> from sympy.physics.mechanics import Body + >>> with ignore_warnings(DeprecationWarning): + ... A = Body('A') + ... B = Body('B') + >>> A.frame.orient_axis(B.frame, B.frame.x, 5) + >>> A.dcm(B) + Matrix([ + [1, 0, 0], + [0, cos(5), sin(5)], + [0, -sin(5), cos(5)]]) + >>> A.dcm(B.frame) + Matrix([ + [1, 0, 0], + [0, cos(5), sin(5)], + [0, -sin(5), cos(5)]]) + + """ + + if isinstance(body, ReferenceFrame): + frame=body + elif isinstance(body, Body): + frame = body.frame + return self.frame.dcm(frame) + + def parallel_axis(self, point, frame=None): + """Returns the inertia dyadic of the body with respect to another + point. + + Parameters + ========== + + point : sympy.physics.vector.Point + The point to express the inertia dyadic about. + frame : sympy.physics.vector.ReferenceFrame + The reference frame used to construct the dyadic. + + Returns + ======= + + inertia : sympy.physics.vector.Dyadic + The inertia dyadic of the rigid body expressed about the provided + point. + + Example + ======= + + As Body has been deprecated, the following examples are for illustrative + purposes only. The functionality of Body is fully captured by + :class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation + warning we can use the ignore_warnings context manager. + + >>> from sympy.utilities.exceptions import ignore_warnings + >>> from sympy.physics.mechanics import Body + >>> with ignore_warnings(DeprecationWarning): + ... A = Body('A') + >>> P = A.masscenter.locatenew('point', 3 * A.x + 5 * A.y) + >>> A.parallel_axis(P).to_matrix(A.frame) + Matrix([ + [A_ixx + 25*A_mass, A_ixy - 15*A_mass, A_izx], + [A_ixy - 15*A_mass, A_iyy + 9*A_mass, A_iyz], + [ A_izx, A_iyz, A_izz + 34*A_mass]]) + + """ + if self.is_rigidbody: + return RigidBody.parallel_axis(self, point, frame) + return Particle.parallel_axis(self, point, frame) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/body_base.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/body_base.py new file mode 100644 index 0000000000000000000000000000000000000000..d2546faf685f579d2aea10ed7f139a4beced7dd0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/body_base.py @@ -0,0 +1,94 @@ +from abc import ABC, abstractmethod +from sympy import Symbol, sympify +from sympy.physics.vector import Point + +__all__ = ['BodyBase'] + + +class BodyBase(ABC): + """Abstract class for body type objects.""" + def __init__(self, name, masscenter=None, mass=None): + # Note: If frame=None, no auto-generated frame is created, because a + # Particle does not need to have a frame by default. + if not isinstance(name, str): + raise TypeError('Supply a valid name.') + self._name = name + if mass is None: + mass = Symbol(f'{name}_mass') + if masscenter is None: + masscenter = Point(f'{name}_masscenter') + self.mass = mass + self.masscenter = masscenter + self.potential_energy = 0 + self.points = [] + + def __str__(self): + return self.name + + def __repr__(self): + return (f'{self.__class__.__name__}({repr(self.name)}, masscenter=' + f'{repr(self.masscenter)}, mass={repr(self.mass)})') + + @property + def name(self): + """The name of the body.""" + return self._name + + @property + def masscenter(self): + """The body's center of mass.""" + return self._masscenter + + @masscenter.setter + def masscenter(self, point): + if not isinstance(point, Point): + raise TypeError("The body's center of mass must be a Point object.") + self._masscenter = point + + @property + def mass(self): + """The body's mass.""" + return self._mass + + @mass.setter + def mass(self, mass): + self._mass = sympify(mass) + + @property + def potential_energy(self): + """The potential energy of the body. + + Examples + ======== + + >>> from sympy.physics.mechanics import Particle, Point + >>> from sympy import symbols + >>> m, g, h = symbols('m g h') + >>> O = Point('O') + >>> P = Particle('P', O, m) + >>> P.potential_energy = m * g * h + >>> P.potential_energy + g*h*m + + """ + return self._potential_energy + + @potential_energy.setter + def potential_energy(self, scalar): + self._potential_energy = sympify(scalar) + + @abstractmethod + def kinetic_energy(self, frame): + pass + + @abstractmethod + def linear_momentum(self, frame): + pass + + @abstractmethod + def angular_momentum(self, point, frame): + pass + + @abstractmethod + def parallel_axis(self, point, frame): + pass diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/functions.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/functions.py new file mode 100644 index 0000000000000000000000000000000000000000..42abe2b7fe608b4602cdab518f209b446b2dbe03 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/functions.py @@ -0,0 +1,735 @@ +from sympy.utilities import dict_merge +from sympy.utilities.iterables import iterable +from sympy.physics.vector import (Dyadic, Vector, ReferenceFrame, + Point, dynamicsymbols) +from sympy.physics.vector.printing import (vprint, vsprint, vpprint, vlatex, + init_vprinting) +from sympy.physics.mechanics.particle import Particle +from sympy.physics.mechanics.rigidbody import RigidBody +from sympy.simplify.simplify import simplify +from sympy import Matrix, Mul, Derivative, sin, cos, tan, S +from sympy.core.function import AppliedUndef +from sympy.physics.mechanics.inertia import (inertia as _inertia, + inertia_of_point_mass as _inertia_of_point_mass) +from sympy.utilities.exceptions import sympy_deprecation_warning + +__all__ = ['linear_momentum', + 'angular_momentum', + 'kinetic_energy', + 'potential_energy', + 'Lagrangian', + 'mechanics_printing', + 'mprint', + 'msprint', + 'mpprint', + 'mlatex', + 'msubs', + 'find_dynamicsymbols'] + +# These are functions that we've moved and renamed during extracting the +# basic vector calculus code from the mechanics packages. + +mprint = vprint +msprint = vsprint +mpprint = vpprint +mlatex = vlatex + + +def mechanics_printing(**kwargs): + """ + Initializes time derivative printing for all SymPy objects in + mechanics module. + """ + + init_vprinting(**kwargs) + +mechanics_printing.__doc__ = init_vprinting.__doc__ + + +def inertia(frame, ixx, iyy, izz, ixy=0, iyz=0, izx=0): + sympy_deprecation_warning( + """ + The inertia function has been moved. + Import it from "sympy.physics.mechanics". + """, + deprecated_since_version="1.13", + active_deprecations_target="moved-mechanics-functions" + ) + return _inertia(frame, ixx, iyy, izz, ixy, iyz, izx) + + +def inertia_of_point_mass(mass, pos_vec, frame): + sympy_deprecation_warning( + """ + The inertia_of_point_mass function has been moved. + Import it from "sympy.physics.mechanics". + """, + deprecated_since_version="1.13", + active_deprecations_target="moved-mechanics-functions" + ) + return _inertia_of_point_mass(mass, pos_vec, frame) + + +def linear_momentum(frame, *body): + """Linear momentum of the system. + + Explanation + =========== + + This function returns the linear momentum of a system of Particle's and/or + RigidBody's. The linear momentum of a system is equal to the vector sum of + the linear momentum of its constituents. Consider a system, S, comprised of + a rigid body, A, and a particle, P. The linear momentum of the system, L, + is equal to the vector sum of the linear momentum of the particle, L1, and + the linear momentum of the rigid body, L2, i.e. + + L = L1 + L2 + + Parameters + ========== + + frame : ReferenceFrame + The frame in which linear momentum is desired. + body1, body2, body3... : Particle and/or RigidBody + The body (or bodies) whose linear momentum is required. + + Examples + ======== + + >>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame + >>> from sympy.physics.mechanics import RigidBody, outer, linear_momentum + >>> N = ReferenceFrame('N') + >>> P = Point('P') + >>> P.set_vel(N, 10 * N.x) + >>> Pa = Particle('Pa', P, 1) + >>> Ac = Point('Ac') + >>> Ac.set_vel(N, 25 * N.y) + >>> I = outer(N.x, N.x) + >>> A = RigidBody('A', Ac, N, 20, (I, Ac)) + >>> linear_momentum(N, A, Pa) + 10*N.x + 500*N.y + + """ + + if not isinstance(frame, ReferenceFrame): + raise TypeError('Please specify a valid ReferenceFrame') + else: + linear_momentum_sys = Vector(0) + for e in body: + if isinstance(e, (RigidBody, Particle)): + linear_momentum_sys += e.linear_momentum(frame) + else: + raise TypeError('*body must have only Particle or RigidBody') + return linear_momentum_sys + + +def angular_momentum(point, frame, *body): + """Angular momentum of a system. + + Explanation + =========== + + This function returns the angular momentum of a system of Particle's and/or + RigidBody's. The angular momentum of such a system is equal to the vector + sum of the angular momentum of its constituents. Consider a system, S, + comprised of a rigid body, A, and a particle, P. The angular momentum of + the system, H, is equal to the vector sum of the angular momentum of the + particle, H1, and the angular momentum of the rigid body, H2, i.e. + + H = H1 + H2 + + Parameters + ========== + + point : Point + The point about which angular momentum of the system is desired. + frame : ReferenceFrame + The frame in which angular momentum is desired. + body1, body2, body3... : Particle and/or RigidBody + The body (or bodies) whose angular momentum is required. + + Examples + ======== + + >>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame + >>> from sympy.physics.mechanics import RigidBody, outer, angular_momentum + >>> N = ReferenceFrame('N') + >>> O = Point('O') + >>> O.set_vel(N, 0 * N.x) + >>> P = O.locatenew('P', 1 * N.x) + >>> P.set_vel(N, 10 * N.x) + >>> Pa = Particle('Pa', P, 1) + >>> Ac = O.locatenew('Ac', 2 * N.y) + >>> Ac.set_vel(N, 5 * N.y) + >>> a = ReferenceFrame('a') + >>> a.set_ang_vel(N, 10 * N.z) + >>> I = outer(N.z, N.z) + >>> A = RigidBody('A', Ac, a, 20, (I, Ac)) + >>> angular_momentum(O, N, Pa, A) + 10*N.z + + """ + + if not isinstance(frame, ReferenceFrame): + raise TypeError('Please enter a valid ReferenceFrame') + if not isinstance(point, Point): + raise TypeError('Please specify a valid Point') + else: + angular_momentum_sys = Vector(0) + for e in body: + if isinstance(e, (RigidBody, Particle)): + angular_momentum_sys += e.angular_momentum(point, frame) + else: + raise TypeError('*body must have only Particle or RigidBody') + return angular_momentum_sys + + +def kinetic_energy(frame, *body): + """Kinetic energy of a multibody system. + + Explanation + =========== + + This function returns the kinetic energy of a system of Particle's and/or + RigidBody's. The kinetic energy of such a system is equal to the sum of + the kinetic energies of its constituents. Consider a system, S, comprising + a rigid body, A, and a particle, P. The kinetic energy of the system, T, + is equal to the vector sum of the kinetic energy of the particle, T1, and + the kinetic energy of the rigid body, T2, i.e. + + T = T1 + T2 + + Kinetic energy is a scalar. + + Parameters + ========== + + frame : ReferenceFrame + The frame in which the velocity or angular velocity of the body is + defined. + body1, body2, body3... : Particle and/or RigidBody + The body (or bodies) whose kinetic energy is required. + + Examples + ======== + + >>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame + >>> from sympy.physics.mechanics import RigidBody, outer, kinetic_energy + >>> N = ReferenceFrame('N') + >>> O = Point('O') + >>> O.set_vel(N, 0 * N.x) + >>> P = O.locatenew('P', 1 * N.x) + >>> P.set_vel(N, 10 * N.x) + >>> Pa = Particle('Pa', P, 1) + >>> Ac = O.locatenew('Ac', 2 * N.y) + >>> Ac.set_vel(N, 5 * N.y) + >>> a = ReferenceFrame('a') + >>> a.set_ang_vel(N, 10 * N.z) + >>> I = outer(N.z, N.z) + >>> A = RigidBody('A', Ac, a, 20, (I, Ac)) + >>> kinetic_energy(N, Pa, A) + 350 + + """ + + if not isinstance(frame, ReferenceFrame): + raise TypeError('Please enter a valid ReferenceFrame') + ke_sys = S.Zero + for e in body: + if isinstance(e, (RigidBody, Particle)): + ke_sys += e.kinetic_energy(frame) + else: + raise TypeError('*body must have only Particle or RigidBody') + return ke_sys + + +def potential_energy(*body): + """Potential energy of a multibody system. + + Explanation + =========== + + This function returns the potential energy of a system of Particle's and/or + RigidBody's. The potential energy of such a system is equal to the sum of + the potential energy of its constituents. Consider a system, S, comprising + a rigid body, A, and a particle, P. The potential energy of the system, V, + is equal to the vector sum of the potential energy of the particle, V1, and + the potential energy of the rigid body, V2, i.e. + + V = V1 + V2 + + Potential energy is a scalar. + + Parameters + ========== + + body1, body2, body3... : Particle and/or RigidBody + The body (or bodies) whose potential energy is required. + + Examples + ======== + + >>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame + >>> from sympy.physics.mechanics import RigidBody, outer, potential_energy + >>> from sympy import symbols + >>> M, m, g, h = symbols('M m g h') + >>> N = ReferenceFrame('N') + >>> O = Point('O') + >>> O.set_vel(N, 0 * N.x) + >>> P = O.locatenew('P', 1 * N.x) + >>> Pa = Particle('Pa', P, m) + >>> Ac = O.locatenew('Ac', 2 * N.y) + >>> a = ReferenceFrame('a') + >>> I = outer(N.z, N.z) + >>> A = RigidBody('A', Ac, a, M, (I, Ac)) + >>> Pa.potential_energy = m * g * h + >>> A.potential_energy = M * g * h + >>> potential_energy(Pa, A) + M*g*h + g*h*m + + """ + + pe_sys = S.Zero + for e in body: + if isinstance(e, (RigidBody, Particle)): + pe_sys += e.potential_energy + else: + raise TypeError('*body must have only Particle or RigidBody') + return pe_sys + + +def gravity(acceleration, *bodies): + from sympy.physics.mechanics.loads import gravity as _gravity + sympy_deprecation_warning( + """ + The gravity function has been moved. + Import it from "sympy.physics.mechanics.loads". + """, + deprecated_since_version="1.13", + active_deprecations_target="moved-mechanics-functions" + ) + return _gravity(acceleration, *bodies) + + +def center_of_mass(point, *bodies): + """ + Returns the position vector from the given point to the center of mass + of the given bodies(particles or rigidbodies). + + Example + ======= + + >>> from sympy import symbols, S + >>> from sympy.physics.vector import Point + >>> from sympy.physics.mechanics import Particle, ReferenceFrame, RigidBody, outer + >>> from sympy.physics.mechanics.functions import center_of_mass + >>> a = ReferenceFrame('a') + >>> m = symbols('m', real=True) + >>> p1 = Particle('p1', Point('p1_pt'), S(1)) + >>> p2 = Particle('p2', Point('p2_pt'), S(2)) + >>> p3 = Particle('p3', Point('p3_pt'), S(3)) + >>> p4 = Particle('p4', Point('p4_pt'), m) + >>> b_f = ReferenceFrame('b_f') + >>> b_cm = Point('b_cm') + >>> mb = symbols('mb') + >>> b = RigidBody('b', b_cm, b_f, mb, (outer(b_f.x, b_f.x), b_cm)) + >>> p2.point.set_pos(p1.point, a.x) + >>> p3.point.set_pos(p1.point, a.x + a.y) + >>> p4.point.set_pos(p1.point, a.y) + >>> b.masscenter.set_pos(p1.point, a.y + a.z) + >>> point_o=Point('o') + >>> point_o.set_pos(p1.point, center_of_mass(p1.point, p1, p2, p3, p4, b)) + >>> expr = 5/(m + mb + 6)*a.x + (m + mb + 3)/(m + mb + 6)*a.y + mb/(m + mb + 6)*a.z + >>> point_o.pos_from(p1.point) + 5/(m + mb + 6)*a.x + (m + mb + 3)/(m + mb + 6)*a.y + mb/(m + mb + 6)*a.z + + """ + if not bodies: + raise TypeError("No bodies(instances of Particle or Rigidbody) were passed.") + + total_mass = 0 + vec = Vector(0) + for i in bodies: + total_mass += i.mass + + masscenter = getattr(i, 'masscenter', None) + if masscenter is None: + masscenter = i.point + vec += i.mass*masscenter.pos_from(point) + + return vec/total_mass + + +def Lagrangian(frame, *body): + """Lagrangian of a multibody system. + + Explanation + =========== + + This function returns the Lagrangian of a system of Particle's and/or + RigidBody's. The Lagrangian of such a system is equal to the difference + between the kinetic energies and potential energies of its constituents. If + T and V are the kinetic and potential energies of a system then it's + Lagrangian, L, is defined as + + L = T - V + + The Lagrangian is a scalar. + + Parameters + ========== + + frame : ReferenceFrame + The frame in which the velocity or angular velocity of the body is + defined to determine the kinetic energy. + + body1, body2, body3... : Particle and/or RigidBody + The body (or bodies) whose Lagrangian is required. + + Examples + ======== + + >>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame + >>> from sympy.physics.mechanics import RigidBody, outer, Lagrangian + >>> from sympy import symbols + >>> M, m, g, h = symbols('M m g h') + >>> N = ReferenceFrame('N') + >>> O = Point('O') + >>> O.set_vel(N, 0 * N.x) + >>> P = O.locatenew('P', 1 * N.x) + >>> P.set_vel(N, 10 * N.x) + >>> Pa = Particle('Pa', P, 1) + >>> Ac = O.locatenew('Ac', 2 * N.y) + >>> Ac.set_vel(N, 5 * N.y) + >>> a = ReferenceFrame('a') + >>> a.set_ang_vel(N, 10 * N.z) + >>> I = outer(N.z, N.z) + >>> A = RigidBody('A', Ac, a, 20, (I, Ac)) + >>> Pa.potential_energy = m * g * h + >>> A.potential_energy = M * g * h + >>> Lagrangian(N, Pa, A) + -M*g*h - g*h*m + 350 + + """ + + if not isinstance(frame, ReferenceFrame): + raise TypeError('Please supply a valid ReferenceFrame') + for e in body: + if not isinstance(e, (RigidBody, Particle)): + raise TypeError('*body must have only Particle or RigidBody') + return kinetic_energy(frame, *body) - potential_energy(*body) + + +def find_dynamicsymbols(expression, exclude=None, reference_frame=None): + """Find all dynamicsymbols in expression. + + Explanation + =========== + + If the optional ``exclude`` kwarg is used, only dynamicsymbols + not in the iterable ``exclude`` are returned. + If we intend to apply this function on a vector, the optional + ``reference_frame`` is also used to inform about the corresponding frame + with respect to which the dynamic symbols of the given vector is to be + determined. + + Parameters + ========== + + expression : SymPy expression + + exclude : iterable of dynamicsymbols, optional + + reference_frame : ReferenceFrame, optional + The frame with respect to which the dynamic symbols of the + given vector is to be determined. + + Examples + ======== + + >>> from sympy.physics.mechanics import dynamicsymbols, find_dynamicsymbols + >>> from sympy.physics.mechanics import ReferenceFrame + >>> x, y = dynamicsymbols('x, y') + >>> expr = x + x.diff()*y + >>> find_dynamicsymbols(expr) + {x(t), y(t), Derivative(x(t), t)} + >>> find_dynamicsymbols(expr, exclude=[x, y]) + {Derivative(x(t), t)} + >>> a, b, c = dynamicsymbols('a, b, c') + >>> A = ReferenceFrame('A') + >>> v = a * A.x + b * A.y + c * A.z + >>> find_dynamicsymbols(v, reference_frame=A) + {a(t), b(t), c(t)} + + """ + t_set = {dynamicsymbols._t} + if exclude: + if iterable(exclude): + exclude_set = set(exclude) + else: + raise TypeError("exclude kwarg must be iterable") + else: + exclude_set = set() + if isinstance(expression, Vector): + if reference_frame is None: + raise ValueError("You must provide reference_frame when passing a " + "vector expression, got %s." % reference_frame) + else: + expression = expression.to_matrix(reference_frame) + return {i for i in expression.atoms(AppliedUndef, Derivative) if + i.free_symbols == t_set} - exclude_set + + +def msubs(expr, *sub_dicts, smart=False, **kwargs): + """A custom subs for use on expressions derived in physics.mechanics. + + Traverses the expression tree once, performing the subs found in sub_dicts. + Terms inside ``Derivative`` expressions are ignored: + + Examples + ======== + + >>> from sympy.physics.mechanics import dynamicsymbols, msubs + >>> x = dynamicsymbols('x') + >>> msubs(x.diff() + x, {x: 1}) + Derivative(x(t), t) + 1 + + Note that sub_dicts can be a single dictionary, or several dictionaries: + + >>> x, y, z = dynamicsymbols('x, y, z') + >>> sub1 = {x: 1, y: 2} + >>> sub2 = {z: 3, x.diff(): 4} + >>> msubs(x.diff() + x + y + z, sub1, sub2) + 10 + + If smart=True (default False), also checks for conditions that may result + in ``nan``, but if simplified would yield a valid expression. For example: + + >>> from sympy import sin, tan + >>> (sin(x)/tan(x)).subs(x, 0) + nan + >>> msubs(sin(x)/tan(x), {x: 0}, smart=True) + 1 + + It does this by first replacing all ``tan`` with ``sin/cos``. Then each + node is traversed. If the node is a fraction, subs is first evaluated on + the denominator. If this results in 0, simplification of the entire + fraction is attempted. Using this selective simplification, only + subexpressions that result in 1/0 are targeted, resulting in faster + performance. + + """ + + sub_dict = dict_merge(*sub_dicts) + if smart: + func = _smart_subs + elif hasattr(expr, 'msubs'): + return expr.msubs(sub_dict) + else: + func = lambda expr, sub_dict: _crawl(expr, _sub_func, sub_dict) + if isinstance(expr, (Matrix, Vector, Dyadic)): + return expr.applyfunc(lambda x: func(x, sub_dict)) + else: + return func(expr, sub_dict) + + +def _crawl(expr, func, *args, **kwargs): + """Crawl the expression tree, and apply func to every node.""" + val = func(expr, *args, **kwargs) + if val is not None: + return val + new_args = (_crawl(arg, func, *args, **kwargs) for arg in expr.args) + return expr.func(*new_args) + + +def _sub_func(expr, sub_dict): + """Perform direct matching substitution, ignoring derivatives.""" + if expr in sub_dict: + return sub_dict[expr] + elif not expr.args or expr.is_Derivative: + return expr + + +def _tan_repl_func(expr): + """Replace tan with sin/cos.""" + if isinstance(expr, tan): + return sin(*expr.args) / cos(*expr.args) + elif not expr.args or expr.is_Derivative: + return expr + + +def _smart_subs(expr, sub_dict): + """Performs subs, checking for conditions that may result in `nan` or + `oo`, and attempts to simplify them out. + + The expression tree is traversed twice, and the following steps are + performed on each expression node: + - First traverse: + Replace all `tan` with `sin/cos`. + - Second traverse: + If node is a fraction, check if the denominator evaluates to 0. + If so, attempt to simplify it out. Then if node is in sub_dict, + sub in the corresponding value. + + """ + expr = _crawl(expr, _tan_repl_func) + + def _recurser(expr, sub_dict): + # Decompose the expression into num, den + num, den = _fraction_decomp(expr) + if den != 1: + # If there is a non trivial denominator, we need to handle it + denom_subbed = _recurser(den, sub_dict) + if denom_subbed.evalf() == 0: + # If denom is 0 after this, attempt to simplify the bad expr + expr = simplify(expr) + else: + # Expression won't result in nan, find numerator + num_subbed = _recurser(num, sub_dict) + return num_subbed / denom_subbed + # We have to crawl the tree manually, because `expr` may have been + # modified in the simplify step. First, perform subs as normal: + val = _sub_func(expr, sub_dict) + if val is not None: + return val + new_args = (_recurser(arg, sub_dict) for arg in expr.args) + return expr.func(*new_args) + return _recurser(expr, sub_dict) + + +def _fraction_decomp(expr): + """Return num, den such that expr = num/den.""" + if not isinstance(expr, Mul): + return expr, 1 + num = [] + den = [] + for a in expr.args: + if a.is_Pow and a.args[1] < 0: + den.append(1 / a) + else: + num.append(a) + if not den: + return expr, 1 + num = Mul(*num) + den = Mul(*den) + return num, den + + +def _f_list_parser(fl, ref_frame): + """Parses the provided forcelist composed of items + of the form (obj, force). + Returns a tuple containing: + vel_list: The velocity (ang_vel for Frames, vel for Points) in + the provided reference frame. + f_list: The forces. + + Used internally in the KanesMethod and LagrangesMethod classes. + + """ + def flist_iter(): + for pair in fl: + obj, force = pair + if isinstance(obj, ReferenceFrame): + yield obj.ang_vel_in(ref_frame), force + elif isinstance(obj, Point): + yield obj.vel(ref_frame), force + else: + raise TypeError('First entry in each forcelist pair must ' + 'be a point or frame.') + + if not fl: + vel_list, f_list = (), () + else: + unzip = lambda l: list(zip(*l)) if l[0] else [(), ()] + vel_list, f_list = unzip(list(flist_iter())) + return vel_list, f_list + + +def _validate_coordinates(coordinates=None, speeds=None, check_duplicates=True, + is_dynamicsymbols=True, u_auxiliary=None): + """Validate the generalized coordinates and generalized speeds. + + Parameters + ========== + coordinates : iterable, optional + Generalized coordinates to be validated. + speeds : iterable, optional + Generalized speeds to be validated. + check_duplicates : bool, optional + Checks if there are duplicates in the generalized coordinates and + generalized speeds. If so it will raise a ValueError. The default is + True. + is_dynamicsymbols : iterable, optional + Checks if all the generalized coordinates and generalized speeds are + dynamicsymbols. If any is not a dynamicsymbol, a ValueError will be + raised. The default is True. + u_auxiliary : iterable, optional + Auxiliary generalized speeds to be validated. + + """ + t_set = {dynamicsymbols._t} + # Convert input to iterables + if coordinates is None: + coordinates = [] + elif not iterable(coordinates): + coordinates = [coordinates] + if speeds is None: + speeds = [] + elif not iterable(speeds): + speeds = [speeds] + if u_auxiliary is None: + u_auxiliary = [] + elif not iterable(u_auxiliary): + u_auxiliary = [u_auxiliary] + + msgs = [] + if check_duplicates: # Check for duplicates + seen = set() + coord_duplicates = {x for x in coordinates if x in seen or seen.add(x)} + seen = set() + speed_duplicates = {x for x in speeds if x in seen or seen.add(x)} + seen = set() + aux_duplicates = {x for x in u_auxiliary if x in seen or seen.add(x)} + overlap_coords = set(coordinates).intersection(speeds) + overlap_aux = set(coordinates).union(speeds).intersection(u_auxiliary) + if coord_duplicates: + msgs.append(f'The generalized coordinates {coord_duplicates} are ' + f'duplicated, all generalized coordinates should be ' + f'unique.') + if speed_duplicates: + msgs.append(f'The generalized speeds {speed_duplicates} are ' + f'duplicated, all generalized speeds should be unique.') + if aux_duplicates: + msgs.append(f'The auxiliary speeds {aux_duplicates} are duplicated,' + f' all auxiliary speeds should be unique.') + if overlap_coords: + msgs.append(f'{overlap_coords} are defined as both generalized ' + f'coordinates and generalized speeds.') + if overlap_aux: + msgs.append(f'The auxiliary speeds {overlap_aux} are also defined ' + f'as generalized coordinates or generalized speeds.') + if is_dynamicsymbols: # Check whether all coordinates are dynamicsymbols + for coordinate in coordinates: + if not (isinstance(coordinate, (AppliedUndef, Derivative)) and + coordinate.free_symbols == t_set): + msgs.append(f'Generalized coordinate "{coordinate}" is not a ' + f'dynamicsymbol.') + for speed in speeds: + if not (isinstance(speed, (AppliedUndef, Derivative)) and + speed.free_symbols == t_set): + msgs.append( + f'Generalized speed "{speed}" is not a dynamicsymbol.') + for aux in u_auxiliary: + if not (isinstance(aux, (AppliedUndef, Derivative)) and + aux.free_symbols == t_set): + msgs.append( + f'Auxiliary speed "{aux}" is not a dynamicsymbol.') + if msgs: + raise ValueError('\n'.join(msgs)) + + +def _parse_linear_solver(linear_solver): + """Helper function to retrieve a specified linear solver.""" + if callable(linear_solver): + return linear_solver + return lambda A, b: Matrix.solve(A, b, method=linear_solver) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/inertia.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/inertia.py new file mode 100644 index 0000000000000000000000000000000000000000..683c1f630f3cedb82d02a9c5ba2309ae438b7fff --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/inertia.py @@ -0,0 +1,199 @@ +from sympy import sympify +from sympy.physics.vector import Point, Dyadic, ReferenceFrame, outer +from collections import namedtuple + +__all__ = ['inertia', 'inertia_of_point_mass', 'Inertia'] + + +def inertia(frame, ixx, iyy, izz, ixy=0, iyz=0, izx=0): + """Simple way to create inertia Dyadic object. + + Explanation + =========== + + Creates an inertia Dyadic based on the given tensor values and a body-fixed + reference frame. + + Parameters + ========== + + frame : ReferenceFrame + The frame the inertia is defined in. + ixx : Sympifyable + The xx element in the inertia dyadic. + iyy : Sympifyable + The yy element in the inertia dyadic. + izz : Sympifyable + The zz element in the inertia dyadic. + ixy : Sympifyable + The xy element in the inertia dyadic. + iyz : Sympifyable + The yz element in the inertia dyadic. + izx : Sympifyable + The zx element in the inertia dyadic. + + Examples + ======== + + >>> from sympy.physics.mechanics import ReferenceFrame, inertia + >>> N = ReferenceFrame('N') + >>> inertia(N, 1, 2, 3) + (N.x|N.x) + 2*(N.y|N.y) + 3*(N.z|N.z) + + """ + + if not isinstance(frame, ReferenceFrame): + raise TypeError('Need to define the inertia in a frame') + ixx, iyy, izz = sympify(ixx), sympify(iyy), sympify(izz) + ixy, iyz, izx = sympify(ixy), sympify(iyz), sympify(izx) + return (ixx*outer(frame.x, frame.x) + ixy*outer(frame.x, frame.y) + + izx*outer(frame.x, frame.z) + ixy*outer(frame.y, frame.x) + + iyy*outer(frame.y, frame.y) + iyz*outer(frame.y, frame.z) + + izx*outer(frame.z, frame.x) + iyz*outer(frame.z, frame.y) + + izz*outer(frame.z, frame.z)) + + +def inertia_of_point_mass(mass, pos_vec, frame): + """Inertia dyadic of a point mass relative to point O. + + Parameters + ========== + + mass : Sympifyable + Mass of the point mass + pos_vec : Vector + Position from point O to point mass + frame : ReferenceFrame + Reference frame to express the dyadic in + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import ReferenceFrame, inertia_of_point_mass + >>> N = ReferenceFrame('N') + >>> r, m = symbols('r m') + >>> px = r * N.x + >>> inertia_of_point_mass(m, px, N) + m*r**2*(N.y|N.y) + m*r**2*(N.z|N.z) + + """ + + return mass*( + (outer(frame.x, frame.x) + + outer(frame.y, frame.y) + + outer(frame.z, frame.z)) * + (pos_vec.dot(pos_vec)) - outer(pos_vec, pos_vec)) + + +class Inertia(namedtuple('Inertia', ['dyadic', 'point'])): + """Inertia object consisting of a Dyadic and a Point of reference. + + Explanation + =========== + + This is a simple class to store the Point and Dyadic, belonging to an + inertia. + + Attributes + ========== + + dyadic : Dyadic + The dyadic of the inertia. + point : Point + The reference point of the inertia. + + Examples + ======== + + >>> from sympy.physics.mechanics import ReferenceFrame, Point, Inertia + >>> N = ReferenceFrame('N') + >>> Po = Point('Po') + >>> Inertia(N.x.outer(N.x) + N.y.outer(N.y) + N.z.outer(N.z), Po) + ((N.x|N.x) + (N.y|N.y) + (N.z|N.z), Po) + + In the example above the Dyadic was created manually, one can however also + use the ``inertia`` function for this or the class method ``from_tensor`` as + shown below. + + >>> Inertia.from_inertia_scalars(Po, N, 1, 1, 1) + ((N.x|N.x) + (N.y|N.y) + (N.z|N.z), Po) + + """ + __slots__ = () + + def __new__(cls, dyadic, point): + # Switch order if given in the wrong order + if isinstance(dyadic, Point) and isinstance(point, Dyadic): + point, dyadic = dyadic, point + if not isinstance(point, Point): + raise TypeError('Reference point should be of type Point') + if not isinstance(dyadic, Dyadic): + raise TypeError('Inertia value should be expressed as a Dyadic') + return super().__new__(cls, dyadic, point) + + @classmethod + def from_inertia_scalars(cls, point, frame, ixx, iyy, izz, ixy=0, iyz=0, + izx=0): + """Simple way to create an Inertia object based on the tensor values. + + Explanation + =========== + + This class method uses the :func`~.inertia` to create the Dyadic based + on the tensor values. + + Parameters + ========== + + point : Point + The reference point of the inertia. + frame : ReferenceFrame + The frame the inertia is defined in. + ixx : Sympifyable + The xx element in the inertia dyadic. + iyy : Sympifyable + The yy element in the inertia dyadic. + izz : Sympifyable + The zz element in the inertia dyadic. + ixy : Sympifyable + The xy element in the inertia dyadic. + iyz : Sympifyable + The yz element in the inertia dyadic. + izx : Sympifyable + The zx element in the inertia dyadic. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import ReferenceFrame, Point, Inertia + >>> ixx, iyy, izz, ixy, iyz, izx = symbols('ixx iyy izz ixy iyz izx') + >>> N = ReferenceFrame('N') + >>> P = Point('P') + >>> I = Inertia.from_inertia_scalars(P, N, ixx, iyy, izz, ixy, iyz, izx) + + The tensor values can easily be seen when converting the dyadic to a + matrix. + + >>> I.dyadic.to_matrix(N) + Matrix([ + [ixx, ixy, izx], + [ixy, iyy, iyz], + [izx, iyz, izz]]) + + """ + return cls(inertia(frame, ixx, iyy, izz, ixy, iyz, izx), point) + + def __add__(self, other): + raise TypeError(f"unsupported operand type(s) for +: " + f"'{self.__class__.__name__}' and " + f"'{other.__class__.__name__}'") + + def __mul__(self, other): + raise TypeError(f"unsupported operand type(s) for *: " + f"'{self.__class__.__name__}' and " + f"'{other.__class__.__name__}'") + + __radd__ = __add__ + __rmul__ = __mul__ diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/joint.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/joint.py new file mode 100644 index 0000000000000000000000000000000000000000..6f3fe661532cff6bf8dda4ab4383fc09f75e9e44 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/joint.py @@ -0,0 +1,2188 @@ +# coding=utf-8 + +from abc import ABC, abstractmethod + +from sympy import pi, Derivative, Matrix +from sympy.core.function import AppliedUndef +from sympy.physics.mechanics.body_base import BodyBase +from sympy.physics.mechanics.functions import _validate_coordinates +from sympy.physics.vector import (Vector, dynamicsymbols, cross, Point, + ReferenceFrame) +from sympy.utilities.iterables import iterable +from sympy.utilities.exceptions import sympy_deprecation_warning + +__all__ = ['Joint', 'PinJoint', 'PrismaticJoint', 'CylindricalJoint', + 'PlanarJoint', 'SphericalJoint', 'WeldJoint'] + + +class Joint(ABC): + """Abstract base class for all specific joints. + + Explanation + =========== + + A joint subtracts degrees of freedom from a body. This is the base class + for all specific joints and holds all common methods acting as an interface + for all joints. Custom joint can be created by inheriting Joint class and + defining all abstract functions. + + The abstract methods are: + + - ``_generate_coordinates`` + - ``_generate_speeds`` + - ``_orient_frames`` + - ``_set_angular_velocity`` + - ``_set_linear_velocity`` + + Parameters + ========== + + name : string + A unique name for the joint. + parent : Particle or RigidBody + The parent body of joint. + child : Particle or RigidBody + The child body of joint. + coordinates : iterable of dynamicsymbols, optional + Generalized coordinates of the joint. + speeds : iterable of dynamicsymbols, optional + Generalized speeds of joint. + parent_point : Point or Vector, optional + Attachment point where the joint is fixed to the parent body. If a + vector is provided, then the attachment point is computed by adding the + vector to the body's mass center. The default value is the parent's mass + center. + child_point : Point or Vector, optional + Attachment point where the joint is fixed to the child body. If a + vector is provided, then the attachment point is computed by adding the + vector to the body's mass center. The default value is the child's mass + center. + parent_axis : Vector, optional + .. deprecated:: 1.12 + Axis fixed in the parent body which aligns with an axis fixed in the + child body. The default is the x axis of parent's reference frame. + For more information on this deprecation, see + :ref:`deprecated-mechanics-joint-axis`. + child_axis : Vector, optional + .. deprecated:: 1.12 + Axis fixed in the child body which aligns with an axis fixed in the + parent body. The default is the x axis of child's reference frame. + For more information on this deprecation, see + :ref:`deprecated-mechanics-joint-axis`. + parent_interframe : ReferenceFrame, optional + Intermediate frame of the parent body with respect to which the joint + transformation is formulated. If a Vector is provided then an interframe + is created which aligns its X axis with the given vector. The default + value is the parent's own frame. + child_interframe : ReferenceFrame, optional + Intermediate frame of the child body with respect to which the joint + transformation is formulated. If a Vector is provided then an interframe + is created which aligns its X axis with the given vector. The default + value is the child's own frame. + parent_joint_pos : Point or Vector, optional + .. deprecated:: 1.12 + This argument is replaced by parent_point and will be removed in a + future version. + See :ref:`deprecated-mechanics-joint-pos` for more information. + child_joint_pos : Point or Vector, optional + .. deprecated:: 1.12 + This argument is replaced by child_point and will be removed in a + future version. + See :ref:`deprecated-mechanics-joint-pos` for more information. + + Attributes + ========== + + name : string + The joint's name. + parent : Particle or RigidBody + The joint's parent body. + child : Particle or RigidBody + The joint's child body. + coordinates : Matrix + Matrix of the joint's generalized coordinates. + speeds : Matrix + Matrix of the joint's generalized speeds. + parent_point : Point + Attachment point where the joint is fixed to the parent body. + child_point : Point + Attachment point where the joint is fixed to the child body. + parent_axis : Vector + The axis fixed in the parent frame that represents the joint. + child_axis : Vector + The axis fixed in the child frame that represents the joint. + parent_interframe : ReferenceFrame + Intermediate frame of the parent body with respect to which the joint + transformation is formulated. + child_interframe : ReferenceFrame + Intermediate frame of the child body with respect to which the joint + transformation is formulated. + kdes : Matrix + Kinematical differential equations of the joint. + + Notes + ===== + + When providing a vector as the intermediate frame, a new intermediate frame + is created which aligns its X axis with the provided vector. This is done + with a single fixed rotation about a rotation axis. This rotation axis is + determined by taking the cross product of the ``body.x`` axis with the + provided vector. In the case where the provided vector is in the ``-body.x`` + direction, the rotation is done about the ``body.y`` axis. + + """ + + def __init__(self, name, parent, child, coordinates=None, speeds=None, + parent_point=None, child_point=None, parent_interframe=None, + child_interframe=None, parent_axis=None, child_axis=None, + parent_joint_pos=None, child_joint_pos=None): + + if not isinstance(name, str): + raise TypeError('Supply a valid name.') + self._name = name + + if not isinstance(parent, BodyBase): + raise TypeError('Parent must be a body.') + self._parent = parent + + if not isinstance(child, BodyBase): + raise TypeError('Child must be a body.') + self._child = child + + if parent_axis is not None or child_axis is not None: + sympy_deprecation_warning( + """ + The parent_axis and child_axis arguments for the Joint classes + are deprecated. Instead use parent_interframe, child_interframe. + """, + deprecated_since_version="1.12", + active_deprecations_target="deprecated-mechanics-joint-axis", + stacklevel=4 + ) + if parent_interframe is None: + parent_interframe = parent_axis + if child_interframe is None: + child_interframe = child_axis + + # Set parent and child frame attributes + if hasattr(self._parent, 'frame'): + self._parent_frame = self._parent.frame + else: + if isinstance(parent_interframe, ReferenceFrame): + self._parent_frame = parent_interframe + else: + self._parent_frame = ReferenceFrame( + f'{self.name}_{self._parent.name}_frame') + if hasattr(self._child, 'frame'): + self._child_frame = self._child.frame + else: + if isinstance(child_interframe, ReferenceFrame): + self._child_frame = child_interframe + else: + self._child_frame = ReferenceFrame( + f'{self.name}_{self._child.name}_frame') + + self._parent_interframe = self._locate_joint_frame( + self._parent, parent_interframe, self._parent_frame) + self._child_interframe = self._locate_joint_frame( + self._child, child_interframe, self._child_frame) + self._parent_axis = self._axis(parent_axis, self._parent_frame) + self._child_axis = self._axis(child_axis, self._child_frame) + + if parent_joint_pos is not None or child_joint_pos is not None: + sympy_deprecation_warning( + """ + The parent_joint_pos and child_joint_pos arguments for the Joint + classes are deprecated. Instead use parent_point and child_point. + """, + deprecated_since_version="1.12", + active_deprecations_target="deprecated-mechanics-joint-pos", + stacklevel=4 + ) + if parent_point is None: + parent_point = parent_joint_pos + if child_point is None: + child_point = child_joint_pos + self._parent_point = self._locate_joint_pos( + self._parent, parent_point, self._parent_frame) + self._child_point = self._locate_joint_pos( + self._child, child_point, self._child_frame) + + self._coordinates = self._generate_coordinates(coordinates) + self._speeds = self._generate_speeds(speeds) + _validate_coordinates(self.coordinates, self.speeds) + self._kdes = self._generate_kdes() + + self._orient_frames() + self._set_angular_velocity() + self._set_linear_velocity() + + def __str__(self): + return self.name + + def __repr__(self): + return self.__str__() + + @property + def name(self): + """Name of the joint.""" + return self._name + + @property + def parent(self): + """Parent body of Joint.""" + return self._parent + + @property + def child(self): + """Child body of Joint.""" + return self._child + + @property + def coordinates(self): + """Matrix of the joint's generalized coordinates.""" + return self._coordinates + + @property + def speeds(self): + """Matrix of the joint's generalized speeds.""" + return self._speeds + + @property + def kdes(self): + """Kinematical differential equations of the joint.""" + return self._kdes + + @property + def parent_axis(self): + """The axis of parent frame.""" + # Will be removed with `deprecated-mechanics-joint-axis` + return self._parent_axis + + @property + def child_axis(self): + """The axis of child frame.""" + # Will be removed with `deprecated-mechanics-joint-axis` + return self._child_axis + + @property + def parent_point(self): + """Attachment point where the joint is fixed to the parent body.""" + return self._parent_point + + @property + def child_point(self): + """Attachment point where the joint is fixed to the child body.""" + return self._child_point + + @property + def parent_interframe(self): + return self._parent_interframe + + @property + def child_interframe(self): + return self._child_interframe + + @abstractmethod + def _generate_coordinates(self, coordinates): + """Generate Matrix of the joint's generalized coordinates.""" + pass + + @abstractmethod + def _generate_speeds(self, speeds): + """Generate Matrix of the joint's generalized speeds.""" + pass + + @abstractmethod + def _orient_frames(self): + """Orient frames as per the joint.""" + pass + + @abstractmethod + def _set_angular_velocity(self): + """Set angular velocity of the joint related frames.""" + pass + + @abstractmethod + def _set_linear_velocity(self): + """Set velocity of related points to the joint.""" + pass + + @staticmethod + def _to_vector(matrix, frame): + """Converts a matrix to a vector in the given frame.""" + return Vector([(matrix, frame)]) + + @staticmethod + def _axis(ax, *frames): + """Check whether an axis is fixed in one of the frames.""" + if ax is None: + ax = frames[0].x + return ax + if not isinstance(ax, Vector): + raise TypeError("Axis must be a Vector.") + ref_frame = None # Find a body in which the axis can be expressed + for frame in frames: + try: + ax.to_matrix(frame) + except ValueError: + pass + else: + ref_frame = frame + break + if ref_frame is None: + raise ValueError("Axis cannot be expressed in one of the body's " + "frames.") + if not ax.dt(ref_frame) == 0: + raise ValueError('Axis cannot be time-varying when viewed from the ' + 'associated body.') + return ax + + @staticmethod + def _choose_rotation_axis(frame, axis): + components = axis.to_matrix(frame) + x, y, z = components[0], components[1], components[2] + + if x != 0: + if y != 0: + if z != 0: + return cross(axis, frame.x) + if z != 0: + return frame.y + return frame.z + else: + if y != 0: + return frame.x + return frame.y + + @staticmethod + def _create_aligned_interframe(frame, align_axis, frame_axis=None, + frame_name=None): + """ + Returns an intermediate frame, where the ``frame_axis`` defined in + ``frame`` is aligned with ``axis``. By default this means that the X + axis will be aligned with ``axis``. + + Parameters + ========== + + frame : BodyBase or ReferenceFrame + The body or reference frame with respect to which the intermediate + frame is oriented. + align_axis : Vector + The vector with respect to which the intermediate frame will be + aligned. + frame_axis : Vector + The vector of the frame which should get aligned with ``axis``. The + default is the X axis of the frame. + frame_name : string + Name of the to be created intermediate frame. The default adds + "_int_frame" to the name of ``frame``. + + Example + ======= + + An intermediate frame, where the X axis of the parent becomes aligned + with ``parent.y + parent.z`` can be created as follows: + + >>> from sympy.physics.mechanics.joint import Joint + >>> from sympy.physics.mechanics import RigidBody + >>> parent = RigidBody('parent') + >>> parent_interframe = Joint._create_aligned_interframe( + ... parent, parent.y + parent.z) + >>> parent_interframe + parent_int_frame + >>> parent.frame.dcm(parent_interframe) + Matrix([ + [ 0, -sqrt(2)/2, -sqrt(2)/2], + [sqrt(2)/2, 1/2, -1/2], + [sqrt(2)/2, -1/2, 1/2]]) + >>> (parent.y + parent.z).express(parent_interframe) + sqrt(2)*parent_int_frame.x + + Notes + ===== + + The direction cosine matrix between the given frame and intermediate + frame is formed using a simple rotation about an axis that is normal to + both ``align_axis`` and ``frame_axis``. In general, the normal axis is + formed by crossing the ``frame_axis`` with the ``align_axis``. The + exception is if the axes are parallel with opposite directions, in which + case the rotation vector is chosen using the rules in the following + table with the vectors expressed in the given frame: + + .. list-table:: + :header-rows: 1 + + * - ``align_axis`` + - ``frame_axis`` + - ``rotation_axis`` + * - ``-x`` + - ``x`` + - ``z`` + * - ``-y`` + - ``y`` + - ``x`` + * - ``-z`` + - ``z`` + - ``y`` + * - ``-x-y`` + - ``x+y`` + - ``z`` + * - ``-y-z`` + - ``y+z`` + - ``x`` + * - ``-x-z`` + - ``x+z`` + - ``y`` + * - ``-x-y-z`` + - ``x+y+z`` + - ``(x+y+z) × x`` + + """ + if isinstance(frame, BodyBase): + frame = frame.frame + if frame_axis is None: + frame_axis = frame.x + if frame_name is None: + if frame.name[-6:] == '_frame': + frame_name = f'{frame.name[:-6]}_int_frame' + else: + frame_name = f'{frame.name}_int_frame' + angle = frame_axis.angle_between(align_axis) + rotation_axis = cross(frame_axis, align_axis) + if rotation_axis == Vector(0) and angle == 0: + return frame + if angle == pi: + rotation_axis = Joint._choose_rotation_axis(frame, align_axis) + + int_frame = ReferenceFrame(frame_name) + int_frame.orient_axis(frame, rotation_axis, angle) + int_frame.set_ang_vel(frame, 0 * rotation_axis) + return int_frame + + def _generate_kdes(self): + """Generate kinematical differential equations.""" + kdes = [] + t = dynamicsymbols._t + for i in range(len(self.coordinates)): + kdes.append(-self.coordinates[i].diff(t) + self.speeds[i]) + return Matrix(kdes) + + def _locate_joint_pos(self, body, joint_pos, body_frame=None): + """Returns the attachment point of a body.""" + if body_frame is None: + body_frame = body.frame + if joint_pos is None: + return body.masscenter + if not isinstance(joint_pos, (Point, Vector)): + raise TypeError('Attachment point must be a Point or Vector.') + if isinstance(joint_pos, Vector): + point_name = f'{self.name}_{body.name}_joint' + joint_pos = body.masscenter.locatenew(point_name, joint_pos) + if not joint_pos.pos_from(body.masscenter).dt(body_frame) == 0: + raise ValueError('Attachment point must be fixed to the associated ' + 'body.') + return joint_pos + + def _locate_joint_frame(self, body, interframe, body_frame=None): + """Returns the attachment frame of a body.""" + if body_frame is None: + body_frame = body.frame + if interframe is None: + return body_frame + if isinstance(interframe, Vector): + interframe = Joint._create_aligned_interframe( + body_frame, interframe, + frame_name=f'{self.name}_{body.name}_int_frame') + elif not isinstance(interframe, ReferenceFrame): + raise TypeError('Interframe must be a ReferenceFrame.') + if not interframe.ang_vel_in(body_frame) == 0: + raise ValueError(f'Interframe {interframe} is not fixed to body ' + f'{body}.') + body.masscenter.set_vel(interframe, 0) # Fixate interframe to body + return interframe + + def _fill_coordinate_list(self, coordinates, n_coords, label='q', offset=0, + number_single=False): + """Helper method for _generate_coordinates and _generate_speeds. + + Parameters + ========== + + coordinates : iterable + Iterable of coordinates or speeds that have been provided. + n_coords : Integer + Number of coordinates that should be returned. + label : String, optional + Coordinate type either 'q' (coordinates) or 'u' (speeds). The + Default is 'q'. + offset : Integer + Count offset when creating new dynamicsymbols. The default is 0. + number_single : Boolean + Boolean whether if n_coords == 1, number should still be used. The + default is False. + + """ + + def create_symbol(number): + if n_coords == 1 and not number_single: + return dynamicsymbols(f'{label}_{self.name}') + return dynamicsymbols(f'{label}{number}_{self.name}') + + name = 'generalized coordinate' if label == 'q' else 'generalized speed' + generated_coordinates = [] + if coordinates is None: + coordinates = [] + elif not iterable(coordinates): + coordinates = [coordinates] + if not (len(coordinates) == 0 or len(coordinates) == n_coords): + raise ValueError(f'Expected {n_coords} {name}s, instead got ' + f'{len(coordinates)} {name}s.') + # Supports more iterables, also Matrix + for i, coord in enumerate(coordinates): + if coord is None: + generated_coordinates.append(create_symbol(i + offset)) + elif isinstance(coord, (AppliedUndef, Derivative)): + generated_coordinates.append(coord) + else: + raise TypeError(f'The {name} {coord} should have been a ' + f'dynamicsymbol.') + for i in range(len(coordinates) + offset, n_coords + offset): + generated_coordinates.append(create_symbol(i)) + return Matrix(generated_coordinates) + + +class PinJoint(Joint): + """Pin (Revolute) Joint. + + .. raw:: html + :file: ../../../doc/src/explanation/modules/physics/mechanics/PinJoint.svg + + Explanation + =========== + + A pin joint is defined such that the joint rotation axis is fixed in both + the child and parent and the location of the joint is relative to the mass + center of each body. The child rotates an angle, θ, from the parent about + the rotation axis and has a simple angular speed, ω, relative to the + parent. The direction cosine matrix between the child interframe and + parent interframe is formed using a simple rotation about the joint axis. + The page on the joints framework gives a more detailed explanation of the + intermediate frames. + + Parameters + ========== + + name : string + A unique name for the joint. + parent : Particle or RigidBody + The parent body of joint. + child : Particle or RigidBody + The child body of joint. + coordinates : dynamicsymbol, optional + Generalized coordinates of the joint. + speeds : dynamicsymbol, optional + Generalized speeds of joint. + parent_point : Point or Vector, optional + Attachment point where the joint is fixed to the parent body. If a + vector is provided, then the attachment point is computed by adding the + vector to the body's mass center. The default value is the parent's mass + center. + child_point : Point or Vector, optional + Attachment point where the joint is fixed to the child body. If a + vector is provided, then the attachment point is computed by adding the + vector to the body's mass center. The default value is the child's mass + center. + parent_axis : Vector, optional + .. deprecated:: 1.12 + Axis fixed in the parent body which aligns with an axis fixed in the + child body. The default is the x axis of parent's reference frame. + For more information on this deprecation, see + :ref:`deprecated-mechanics-joint-axis`. + child_axis : Vector, optional + .. deprecated:: 1.12 + Axis fixed in the child body which aligns with an axis fixed in the + parent body. The default is the x axis of child's reference frame. + For more information on this deprecation, see + :ref:`deprecated-mechanics-joint-axis`. + parent_interframe : ReferenceFrame, optional + Intermediate frame of the parent body with respect to which the joint + transformation is formulated. If a Vector is provided then an interframe + is created which aligns its X axis with the given vector. The default + value is the parent's own frame. + child_interframe : ReferenceFrame, optional + Intermediate frame of the child body with respect to which the joint + transformation is formulated. If a Vector is provided then an interframe + is created which aligns its X axis with the given vector. The default + value is the child's own frame. + joint_axis : Vector + The axis about which the rotation occurs. Note that the components + of this axis are the same in the parent_interframe and child_interframe. + parent_joint_pos : Point or Vector, optional + .. deprecated:: 1.12 + This argument is replaced by parent_point and will be removed in a + future version. + See :ref:`deprecated-mechanics-joint-pos` for more information. + child_joint_pos : Point or Vector, optional + .. deprecated:: 1.12 + This argument is replaced by child_point and will be removed in a + future version. + See :ref:`deprecated-mechanics-joint-pos` for more information. + + Attributes + ========== + + name : string + The joint's name. + parent : Particle or RigidBody + The joint's parent body. + child : Particle or RigidBody + The joint's child body. + coordinates : Matrix + Matrix of the joint's generalized coordinates. The default value is + ``dynamicsymbols(f'q_{joint.name}')``. + speeds : Matrix + Matrix of the joint's generalized speeds. The default value is + ``dynamicsymbols(f'u_{joint.name}')``. + parent_point : Point + Attachment point where the joint is fixed to the parent body. + child_point : Point + Attachment point where the joint is fixed to the child body. + parent_axis : Vector + The axis fixed in the parent frame that represents the joint. + child_axis : Vector + The axis fixed in the child frame that represents the joint. + parent_interframe : ReferenceFrame + Intermediate frame of the parent body with respect to which the joint + transformation is formulated. + child_interframe : ReferenceFrame + Intermediate frame of the child body with respect to which the joint + transformation is formulated. + joint_axis : Vector + The axis about which the rotation occurs. Note that the components of + this axis are the same in the parent_interframe and child_interframe. + kdes : Matrix + Kinematical differential equations of the joint. + + Examples + ========= + + A single pin joint is created from two bodies and has the following basic + attributes: + + >>> from sympy.physics.mechanics import RigidBody, PinJoint + >>> parent = RigidBody('P') + >>> parent + P + >>> child = RigidBody('C') + >>> child + C + >>> joint = PinJoint('PC', parent, child) + >>> joint + PinJoint: PC parent: P child: C + >>> joint.name + 'PC' + >>> joint.parent + P + >>> joint.child + C + >>> joint.parent_point + P_masscenter + >>> joint.child_point + C_masscenter + >>> joint.parent_axis + P_frame.x + >>> joint.child_axis + C_frame.x + >>> joint.coordinates + Matrix([[q_PC(t)]]) + >>> joint.speeds + Matrix([[u_PC(t)]]) + >>> child.frame.ang_vel_in(parent.frame) + u_PC(t)*P_frame.x + >>> child.frame.dcm(parent.frame) + Matrix([ + [1, 0, 0], + [0, cos(q_PC(t)), sin(q_PC(t))], + [0, -sin(q_PC(t)), cos(q_PC(t))]]) + >>> joint.child_point.pos_from(joint.parent_point) + 0 + + To further demonstrate the use of the pin joint, the kinematics of simple + double pendulum that rotates about the Z axis of each connected body can be + created as follows. + + >>> from sympy import symbols, trigsimp + >>> from sympy.physics.mechanics import RigidBody, PinJoint + >>> l1, l2 = symbols('l1 l2') + + First create bodies to represent the fixed ceiling and one to represent + each pendulum bob. + + >>> ceiling = RigidBody('C') + >>> upper_bob = RigidBody('U') + >>> lower_bob = RigidBody('L') + + The first joint will connect the upper bob to the ceiling by a distance of + ``l1`` and the joint axis will be about the Z axis for each body. + + >>> ceiling_joint = PinJoint('P1', ceiling, upper_bob, + ... child_point=-l1*upper_bob.frame.x, + ... joint_axis=ceiling.frame.z) + + The second joint will connect the lower bob to the upper bob by a distance + of ``l2`` and the joint axis will also be about the Z axis for each body. + + >>> pendulum_joint = PinJoint('P2', upper_bob, lower_bob, + ... child_point=-l2*lower_bob.frame.x, + ... joint_axis=upper_bob.frame.z) + + Once the joints are established the kinematics of the connected bodies can + be accessed. First the direction cosine matrices of pendulum link relative + to the ceiling are found: + + >>> upper_bob.frame.dcm(ceiling.frame) + Matrix([ + [ cos(q_P1(t)), sin(q_P1(t)), 0], + [-sin(q_P1(t)), cos(q_P1(t)), 0], + [ 0, 0, 1]]) + >>> trigsimp(lower_bob.frame.dcm(ceiling.frame)) + Matrix([ + [ cos(q_P1(t) + q_P2(t)), sin(q_P1(t) + q_P2(t)), 0], + [-sin(q_P1(t) + q_P2(t)), cos(q_P1(t) + q_P2(t)), 0], + [ 0, 0, 1]]) + + The position of the lower bob's masscenter is found with: + + >>> lower_bob.masscenter.pos_from(ceiling.masscenter) + l1*U_frame.x + l2*L_frame.x + + The angular velocities of the two pendulum links can be computed with + respect to the ceiling. + + >>> upper_bob.frame.ang_vel_in(ceiling.frame) + u_P1(t)*C_frame.z + >>> lower_bob.frame.ang_vel_in(ceiling.frame) + u_P1(t)*C_frame.z + u_P2(t)*U_frame.z + + And finally, the linear velocities of the two pendulum bobs can be computed + with respect to the ceiling. + + >>> upper_bob.masscenter.vel(ceiling.frame) + l1*u_P1(t)*U_frame.y + >>> lower_bob.masscenter.vel(ceiling.frame) + l1*u_P1(t)*U_frame.y + l2*(u_P1(t) + u_P2(t))*L_frame.y + + """ + + def __init__(self, name, parent, child, coordinates=None, speeds=None, + parent_point=None, child_point=None, parent_interframe=None, + child_interframe=None, parent_axis=None, child_axis=None, + joint_axis=None, parent_joint_pos=None, child_joint_pos=None): + + self._joint_axis = joint_axis + super().__init__(name, parent, child, coordinates, speeds, parent_point, + child_point, parent_interframe, child_interframe, + parent_axis, child_axis, parent_joint_pos, + child_joint_pos) + + def __str__(self): + return (f'PinJoint: {self.name} parent: {self.parent} ' + f'child: {self.child}') + + @property + def joint_axis(self): + """Axis about which the child rotates with respect to the parent.""" + return self._joint_axis + + def _generate_coordinates(self, coordinate): + return self._fill_coordinate_list(coordinate, 1, 'q') + + def _generate_speeds(self, speed): + return self._fill_coordinate_list(speed, 1, 'u') + + def _orient_frames(self): + self._joint_axis = self._axis(self.joint_axis, self.parent_interframe) + self.child_interframe.orient_axis( + self.parent_interframe, self.joint_axis, self.coordinates[0]) + + def _set_angular_velocity(self): + self.child_interframe.set_ang_vel(self.parent_interframe, self.speeds[ + 0] * self.joint_axis.normalize()) + + def _set_linear_velocity(self): + self.child_point.set_pos(self.parent_point, 0) + self.parent_point.set_vel(self._parent_frame, 0) + self.child_point.set_vel(self._child_frame, 0) + self.child.masscenter.v2pt_theory(self.parent_point, + self._parent_frame, self._child_frame) + + +class PrismaticJoint(Joint): + """Prismatic (Sliding) Joint. + + .. image:: PrismaticJoint.svg + + Explanation + =========== + + It is defined such that the child body translates with respect to the parent + body along the body-fixed joint axis. The location of the joint is defined + by two points, one in each body, which coincide when the generalized + coordinate is zero. The direction cosine matrix between the + parent_interframe and child_interframe is the identity matrix. Therefore, + the direction cosine matrix between the parent and child frames is fully + defined by the definition of the intermediate frames. The page on the joints + framework gives a more detailed explanation of the intermediate frames. + + Parameters + ========== + + name : string + A unique name for the joint. + parent : Particle or RigidBody + The parent body of joint. + child : Particle or RigidBody + The child body of joint. + coordinates : dynamicsymbol, optional + Generalized coordinates of the joint. The default value is + ``dynamicsymbols(f'q_{joint.name}')``. + speeds : dynamicsymbol, optional + Generalized speeds of joint. The default value is + ``dynamicsymbols(f'u_{joint.name}')``. + parent_point : Point or Vector, optional + Attachment point where the joint is fixed to the parent body. If a + vector is provided, then the attachment point is computed by adding the + vector to the body's mass center. The default value is the parent's mass + center. + child_point : Point or Vector, optional + Attachment point where the joint is fixed to the child body. If a + vector is provided, then the attachment point is computed by adding the + vector to the body's mass center. The default value is the child's mass + center. + parent_axis : Vector, optional + .. deprecated:: 1.12 + Axis fixed in the parent body which aligns with an axis fixed in the + child body. The default is the x axis of parent's reference frame. + For more information on this deprecation, see + :ref:`deprecated-mechanics-joint-axis`. + child_axis : Vector, optional + .. deprecated:: 1.12 + Axis fixed in the child body which aligns with an axis fixed in the + parent body. The default is the x axis of child's reference frame. + For more information on this deprecation, see + :ref:`deprecated-mechanics-joint-axis`. + parent_interframe : ReferenceFrame, optional + Intermediate frame of the parent body with respect to which the joint + transformation is formulated. If a Vector is provided then an interframe + is created which aligns its X axis with the given vector. The default + value is the parent's own frame. + child_interframe : ReferenceFrame, optional + Intermediate frame of the child body with respect to which the joint + transformation is formulated. If a Vector is provided then an interframe + is created which aligns its X axis with the given vector. The default + value is the child's own frame. + joint_axis : Vector + The axis along which the translation occurs. Note that the components + of this axis are the same in the parent_interframe and child_interframe. + parent_joint_pos : Point or Vector, optional + .. deprecated:: 1.12 + This argument is replaced by parent_point and will be removed in a + future version. + See :ref:`deprecated-mechanics-joint-pos` for more information. + child_joint_pos : Point or Vector, optional + .. deprecated:: 1.12 + This argument is replaced by child_point and will be removed in a + future version. + See :ref:`deprecated-mechanics-joint-pos` for more information. + + Attributes + ========== + + name : string + The joint's name. + parent : Particle or RigidBody + The joint's parent body. + child : Particle or RigidBody + The joint's child body. + coordinates : Matrix + Matrix of the joint's generalized coordinates. + speeds : Matrix + Matrix of the joint's generalized speeds. + parent_point : Point + Attachment point where the joint is fixed to the parent body. + child_point : Point + Attachment point where the joint is fixed to the child body. + parent_axis : Vector + The axis fixed in the parent frame that represents the joint. + child_axis : Vector + The axis fixed in the child frame that represents the joint. + parent_interframe : ReferenceFrame + Intermediate frame of the parent body with respect to which the joint + transformation is formulated. + child_interframe : ReferenceFrame + Intermediate frame of the child body with respect to which the joint + transformation is formulated. + kdes : Matrix + Kinematical differential equations of the joint. + + Examples + ========= + + A single prismatic joint is created from two bodies and has the following + basic attributes: + + >>> from sympy.physics.mechanics import RigidBody, PrismaticJoint + >>> parent = RigidBody('P') + >>> parent + P + >>> child = RigidBody('C') + >>> child + C + >>> joint = PrismaticJoint('PC', parent, child) + >>> joint + PrismaticJoint: PC parent: P child: C + >>> joint.name + 'PC' + >>> joint.parent + P + >>> joint.child + C + >>> joint.parent_point + P_masscenter + >>> joint.child_point + C_masscenter + >>> joint.parent_axis + P_frame.x + >>> joint.child_axis + C_frame.x + >>> joint.coordinates + Matrix([[q_PC(t)]]) + >>> joint.speeds + Matrix([[u_PC(t)]]) + >>> child.frame.ang_vel_in(parent.frame) + 0 + >>> child.frame.dcm(parent.frame) + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + >>> joint.child_point.pos_from(joint.parent_point) + q_PC(t)*P_frame.x + + To further demonstrate the use of the prismatic joint, the kinematics of two + masses sliding, one moving relative to a fixed body and the other relative + to the moving body. about the X axis of each connected body can be created + as follows. + + >>> from sympy.physics.mechanics import PrismaticJoint, RigidBody + + First create bodies to represent the fixed ceiling and one to represent + a particle. + + >>> wall = RigidBody('W') + >>> Part1 = RigidBody('P1') + >>> Part2 = RigidBody('P2') + + The first joint will connect the particle to the ceiling and the + joint axis will be about the X axis for each body. + + >>> J1 = PrismaticJoint('J1', wall, Part1) + + The second joint will connect the second particle to the first particle + and the joint axis will also be about the X axis for each body. + + >>> J2 = PrismaticJoint('J2', Part1, Part2) + + Once the joint is established the kinematics of the connected bodies can + be accessed. First the direction cosine matrices of Part relative + to the ceiling are found: + + >>> Part1.frame.dcm(wall.frame) + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + + >>> Part2.frame.dcm(wall.frame) + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + + The position of the particles' masscenter is found with: + + >>> Part1.masscenter.pos_from(wall.masscenter) + q_J1(t)*W_frame.x + + >>> Part2.masscenter.pos_from(wall.masscenter) + q_J1(t)*W_frame.x + q_J2(t)*P1_frame.x + + The angular velocities of the two particle links can be computed with + respect to the ceiling. + + >>> Part1.frame.ang_vel_in(wall.frame) + 0 + + >>> Part2.frame.ang_vel_in(wall.frame) + 0 + + And finally, the linear velocities of the two particles can be computed + with respect to the ceiling. + + >>> Part1.masscenter.vel(wall.frame) + u_J1(t)*W_frame.x + + >>> Part2.masscenter.vel(wall.frame) + u_J1(t)*W_frame.x + Derivative(q_J2(t), t)*P1_frame.x + + """ + + def __init__(self, name, parent, child, coordinates=None, speeds=None, + parent_point=None, child_point=None, parent_interframe=None, + child_interframe=None, parent_axis=None, child_axis=None, + joint_axis=None, parent_joint_pos=None, child_joint_pos=None): + + self._joint_axis = joint_axis + super().__init__(name, parent, child, coordinates, speeds, parent_point, + child_point, parent_interframe, child_interframe, + parent_axis, child_axis, parent_joint_pos, + child_joint_pos) + + def __str__(self): + return (f'PrismaticJoint: {self.name} parent: {self.parent} ' + f'child: {self.child}') + + @property + def joint_axis(self): + """Axis along which the child translates with respect to the parent.""" + return self._joint_axis + + def _generate_coordinates(self, coordinate): + return self._fill_coordinate_list(coordinate, 1, 'q') + + def _generate_speeds(self, speed): + return self._fill_coordinate_list(speed, 1, 'u') + + def _orient_frames(self): + self._joint_axis = self._axis(self.joint_axis, self.parent_interframe) + self.child_interframe.orient_axis( + self.parent_interframe, self.joint_axis, 0) + + def _set_angular_velocity(self): + self.child_interframe.set_ang_vel(self.parent_interframe, 0) + + def _set_linear_velocity(self): + axis = self.joint_axis.normalize() + self.child_point.set_pos(self.parent_point, self.coordinates[0] * axis) + self.parent_point.set_vel(self._parent_frame, 0) + self.child_point.set_vel(self._child_frame, 0) + self.child_point.set_vel(self._parent_frame, self.speeds[0] * axis) + self.child.masscenter.set_vel(self._parent_frame, self.speeds[0] * axis) + + +class CylindricalJoint(Joint): + """Cylindrical Joint. + + .. image:: CylindricalJoint.svg + :align: center + :width: 600 + + Explanation + =========== + + A cylindrical joint is defined such that the child body both rotates about + and translates along the body-fixed joint axis with respect to the parent + body. The joint axis is both the rotation axis and translation axis. The + location of the joint is defined by two points, one in each body, which + coincide when the generalized coordinate corresponding to the translation is + zero. The direction cosine matrix between the child interframe and parent + interframe is formed using a simple rotation about the joint axis. The page + on the joints framework gives a more detailed explanation of the + intermediate frames. + + Parameters + ========== + + name : string + A unique name for the joint. + parent : Particle or RigidBody + The parent body of joint. + child : Particle or RigidBody + The child body of joint. + rotation_coordinate : dynamicsymbol, optional + Generalized coordinate corresponding to the rotation angle. The default + value is ``dynamicsymbols(f'q0_{joint.name}')``. + translation_coordinate : dynamicsymbol, optional + Generalized coordinate corresponding to the translation distance. The + default value is ``dynamicsymbols(f'q1_{joint.name}')``. + rotation_speed : dynamicsymbol, optional + Generalized speed corresponding to the angular velocity. The default + value is ``dynamicsymbols(f'u0_{joint.name}')``. + translation_speed : dynamicsymbol, optional + Generalized speed corresponding to the translation velocity. The default + value is ``dynamicsymbols(f'u1_{joint.name}')``. + parent_point : Point or Vector, optional + Attachment point where the joint is fixed to the parent body. If a + vector is provided, then the attachment point is computed by adding the + vector to the body's mass center. The default value is the parent's mass + center. + child_point : Point or Vector, optional + Attachment point where the joint is fixed to the child body. If a + vector is provided, then the attachment point is computed by adding the + vector to the body's mass center. The default value is the child's mass + center. + parent_interframe : ReferenceFrame, optional + Intermediate frame of the parent body with respect to which the joint + transformation is formulated. If a Vector is provided then an interframe + is created which aligns its X axis with the given vector. The default + value is the parent's own frame. + child_interframe : ReferenceFrame, optional + Intermediate frame of the child body with respect to which the joint + transformation is formulated. If a Vector is provided then an interframe + is created which aligns its X axis with the given vector. The default + value is the child's own frame. + joint_axis : Vector, optional + The rotation as well as translation axis. Note that the components of + this axis are the same in the parent_interframe and child_interframe. + + Attributes + ========== + + name : string + The joint's name. + parent : Particle or RigidBody + The joint's parent body. + child : Particle or RigidBody + The joint's child body. + rotation_coordinate : dynamicsymbol + Generalized coordinate corresponding to the rotation angle. + translation_coordinate : dynamicsymbol + Generalized coordinate corresponding to the translation distance. + rotation_speed : dynamicsymbol + Generalized speed corresponding to the angular velocity. + translation_speed : dynamicsymbol + Generalized speed corresponding to the translation velocity. + coordinates : Matrix + Matrix of the joint's generalized coordinates. + speeds : Matrix + Matrix of the joint's generalized speeds. + parent_point : Point + Attachment point where the joint is fixed to the parent body. + child_point : Point + Attachment point where the joint is fixed to the child body. + parent_interframe : ReferenceFrame + Intermediate frame of the parent body with respect to which the joint + transformation is formulated. + child_interframe : ReferenceFrame + Intermediate frame of the child body with respect to which the joint + transformation is formulated. + kdes : Matrix + Kinematical differential equations of the joint. + joint_axis : Vector + The axis of rotation and translation. + + Examples + ========= + + A single cylindrical joint is created between two bodies and has the + following basic attributes: + + >>> from sympy.physics.mechanics import RigidBody, CylindricalJoint + >>> parent = RigidBody('P') + >>> parent + P + >>> child = RigidBody('C') + >>> child + C + >>> joint = CylindricalJoint('PC', parent, child) + >>> joint + CylindricalJoint: PC parent: P child: C + >>> joint.name + 'PC' + >>> joint.parent + P + >>> joint.child + C + >>> joint.parent_point + P_masscenter + >>> joint.child_point + C_masscenter + >>> joint.parent_axis + P_frame.x + >>> joint.child_axis + C_frame.x + >>> joint.coordinates + Matrix([ + [q0_PC(t)], + [q1_PC(t)]]) + >>> joint.speeds + Matrix([ + [u0_PC(t)], + [u1_PC(t)]]) + >>> child.frame.ang_vel_in(parent.frame) + u0_PC(t)*P_frame.x + >>> child.frame.dcm(parent.frame) + Matrix([ + [1, 0, 0], + [0, cos(q0_PC(t)), sin(q0_PC(t))], + [0, -sin(q0_PC(t)), cos(q0_PC(t))]]) + >>> joint.child_point.pos_from(joint.parent_point) + q1_PC(t)*P_frame.x + >>> child.masscenter.vel(parent.frame) + u1_PC(t)*P_frame.x + + To further demonstrate the use of the cylindrical joint, the kinematics of + two cylindrical joints perpendicular to each other can be created as follows. + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import RigidBody, CylindricalJoint + >>> r, l, w = symbols('r l w') + + First create bodies to represent the fixed floor with a fixed pole on it. + The second body represents a freely moving tube around that pole. The third + body represents a solid flag freely translating along and rotating around + the Y axis of the tube. + + >>> floor = RigidBody('floor') + >>> tube = RigidBody('tube') + >>> flag = RigidBody('flag') + + The first joint will connect the first tube to the floor with it translating + along and rotating around the Z axis of both bodies. + + >>> floor_joint = CylindricalJoint('C1', floor, tube, joint_axis=floor.z) + + The second joint will connect the tube perpendicular to the flag along the Y + axis of both the tube and the flag, with the joint located at a distance + ``r`` from the tube's center of mass and a combination of the distances + ``l`` and ``w`` from the flag's center of mass. + + >>> flag_joint = CylindricalJoint('C2', tube, flag, + ... parent_point=r * tube.y, + ... child_point=-w * flag.y + l * flag.z, + ... joint_axis=tube.y) + + Once the joints are established the kinematics of the connected bodies can + be accessed. First the direction cosine matrices of both the body and the + flag relative to the floor are found: + + >>> tube.frame.dcm(floor.frame) + Matrix([ + [ cos(q0_C1(t)), sin(q0_C1(t)), 0], + [-sin(q0_C1(t)), cos(q0_C1(t)), 0], + [ 0, 0, 1]]) + >>> flag.frame.dcm(floor.frame) + Matrix([ + [cos(q0_C1(t))*cos(q0_C2(t)), sin(q0_C1(t))*cos(q0_C2(t)), -sin(q0_C2(t))], + [ -sin(q0_C1(t)), cos(q0_C1(t)), 0], + [sin(q0_C2(t))*cos(q0_C1(t)), sin(q0_C1(t))*sin(q0_C2(t)), cos(q0_C2(t))]]) + + The position of the flag's center of mass is found with: + + >>> flag.masscenter.pos_from(floor.masscenter) + q1_C1(t)*floor_frame.z + (r + q1_C2(t))*tube_frame.y + w*flag_frame.y - l*flag_frame.z + + The angular velocities of the two tubes can be computed with respect to the + floor. + + >>> tube.frame.ang_vel_in(floor.frame) + u0_C1(t)*floor_frame.z + >>> flag.frame.ang_vel_in(floor.frame) + u0_C1(t)*floor_frame.z + u0_C2(t)*tube_frame.y + + Finally, the linear velocities of the two tube centers of mass can be + computed with respect to the floor, while expressed in the tube's frame. + + >>> tube.masscenter.vel(floor.frame).to_matrix(tube.frame) + Matrix([ + [ 0], + [ 0], + [u1_C1(t)]]) + >>> flag.masscenter.vel(floor.frame).to_matrix(tube.frame).simplify() + Matrix([ + [-l*u0_C2(t)*cos(q0_C2(t)) - r*u0_C1(t) - w*u0_C1(t) - q1_C2(t)*u0_C1(t)], + [ -l*u0_C1(t)*sin(q0_C2(t)) + Derivative(q1_C2(t), t)], + [ l*u0_C2(t)*sin(q0_C2(t)) + u1_C1(t)]]) + + """ + + def __init__(self, name, parent, child, rotation_coordinate=None, + translation_coordinate=None, rotation_speed=None, + translation_speed=None, parent_point=None, child_point=None, + parent_interframe=None, child_interframe=None, + joint_axis=None): + self._joint_axis = joint_axis + coordinates = (rotation_coordinate, translation_coordinate) + speeds = (rotation_speed, translation_speed) + super().__init__(name, parent, child, coordinates, speeds, + parent_point, child_point, + parent_interframe=parent_interframe, + child_interframe=child_interframe) + + def __str__(self): + return (f'CylindricalJoint: {self.name} parent: {self.parent} ' + f'child: {self.child}') + + @property + def joint_axis(self): + """Axis about and along which the rotation and translation occurs.""" + return self._joint_axis + + @property + def rotation_coordinate(self): + """Generalized coordinate corresponding to the rotation angle.""" + return self.coordinates[0] + + @property + def translation_coordinate(self): + """Generalized coordinate corresponding to the translation distance.""" + return self.coordinates[1] + + @property + def rotation_speed(self): + """Generalized speed corresponding to the angular velocity.""" + return self.speeds[0] + + @property + def translation_speed(self): + """Generalized speed corresponding to the translation velocity.""" + return self.speeds[1] + + def _generate_coordinates(self, coordinates): + return self._fill_coordinate_list(coordinates, 2, 'q') + + def _generate_speeds(self, speeds): + return self._fill_coordinate_list(speeds, 2, 'u') + + def _orient_frames(self): + self._joint_axis = self._axis(self.joint_axis, self.parent_interframe) + self.child_interframe.orient_axis( + self.parent_interframe, self.joint_axis, self.rotation_coordinate) + + def _set_angular_velocity(self): + self.child_interframe.set_ang_vel( + self.parent_interframe, + self.rotation_speed * self.joint_axis.normalize()) + + def _set_linear_velocity(self): + self.child_point.set_pos( + self.parent_point, + self.translation_coordinate * self.joint_axis.normalize()) + self.parent_point.set_vel(self._parent_frame, 0) + self.child_point.set_vel(self._child_frame, 0) + self.child_point.set_vel( + self._parent_frame, + self.translation_speed * self.joint_axis.normalize()) + self.child.masscenter.v2pt_theory(self.child_point, self._parent_frame, + self.child_interframe) + + +class PlanarJoint(Joint): + """Planar Joint. + + .. raw:: html + :file: ../../../doc/src/modules/physics/mechanics/api/PlanarJoint.svg + + Explanation + =========== + + A planar joint is defined such that the child body translates over a fixed + plane of the parent body as well as rotate about the rotation axis, which + is perpendicular to that plane. The origin of this plane is the + ``parent_point`` and the plane is spanned by two nonparallel planar vectors. + The location of the ``child_point`` is based on the planar vectors + ($\\vec{v}_1$, $\\vec{v}_2$) and generalized coordinates ($q_1$, $q_2$), + i.e. $\\vec{r} = q_1 \\hat{v}_1 + q_2 \\hat{v}_2$. The direction cosine + matrix between the ``child_interframe`` and ``parent_interframe`` is formed + using a simple rotation ($q_0$) about the rotation axis. + + In order to simplify the definition of the ``PlanarJoint``, the + ``rotation_axis`` and ``planar_vectors`` are set to be the unit vectors of + the ``parent_interframe`` according to the table below. This ensures that + you can only define these vectors by creating a separate frame and supplying + that as the interframe. If you however would only like to supply the normals + of the plane with respect to the parent and child bodies, then you can also + supply those to the ``parent_interframe`` and ``child_interframe`` + arguments. An example of both of these cases is in the examples section + below and the page on the joints framework provides a more detailed + explanation of the intermediate frames. + + .. list-table:: + + * - ``rotation_axis`` + - ``parent_interframe.x`` + * - ``planar_vectors[0]`` + - ``parent_interframe.y`` + * - ``planar_vectors[1]`` + - ``parent_interframe.z`` + + Parameters + ========== + + name : string + A unique name for the joint. + parent : Particle or RigidBody + The parent body of joint. + child : Particle or RigidBody + The child body of joint. + rotation_coordinate : dynamicsymbol, optional + Generalized coordinate corresponding to the rotation angle. The default + value is ``dynamicsymbols(f'q0_{joint.name}')``. + planar_coordinates : iterable of dynamicsymbols, optional + Two generalized coordinates used for the planar translation. The default + value is ``dynamicsymbols(f'q1_{joint.name} q2_{joint.name}')``. + rotation_speed : dynamicsymbol, optional + Generalized speed corresponding to the angular velocity. The default + value is ``dynamicsymbols(f'u0_{joint.name}')``. + planar_speeds : dynamicsymbols, optional + Two generalized speeds used for the planar translation velocity. The + default value is ``dynamicsymbols(f'u1_{joint.name} u2_{joint.name}')``. + parent_point : Point or Vector, optional + Attachment point where the joint is fixed to the parent body. If a + vector is provided, then the attachment point is computed by adding the + vector to the body's mass center. The default value is the parent's mass + center. + child_point : Point or Vector, optional + Attachment point where the joint is fixed to the child body. If a + vector is provided, then the attachment point is computed by adding the + vector to the body's mass center. The default value is the child's mass + center. + parent_interframe : ReferenceFrame, optional + Intermediate frame of the parent body with respect to which the joint + transformation is formulated. If a Vector is provided then an interframe + is created which aligns its X axis with the given vector. The default + value is the parent's own frame. + child_interframe : ReferenceFrame, optional + Intermediate frame of the child body with respect to which the joint + transformation is formulated. If a Vector is provided then an interframe + is created which aligns its X axis with the given vector. The default + value is the child's own frame. + + Attributes + ========== + + name : string + The joint's name. + parent : Particle or RigidBody + The joint's parent body. + child : Particle or RigidBody + The joint's child body. + rotation_coordinate : dynamicsymbol + Generalized coordinate corresponding to the rotation angle. + planar_coordinates : Matrix + Two generalized coordinates used for the planar translation. + rotation_speed : dynamicsymbol + Generalized speed corresponding to the angular velocity. + planar_speeds : Matrix + Two generalized speeds used for the planar translation velocity. + coordinates : Matrix + Matrix of the joint's generalized coordinates. + speeds : Matrix + Matrix of the joint's generalized speeds. + parent_point : Point + Attachment point where the joint is fixed to the parent body. + child_point : Point + Attachment point where the joint is fixed to the child body. + parent_interframe : ReferenceFrame + Intermediate frame of the parent body with respect to which the joint + transformation is formulated. + child_interframe : ReferenceFrame + Intermediate frame of the child body with respect to which the joint + transformation is formulated. + kdes : Matrix + Kinematical differential equations of the joint. + rotation_axis : Vector + The axis about which the rotation occurs. + planar_vectors : list + The vectors that describe the planar translation directions. + + Examples + ========= + + A single planar joint is created between two bodies and has the following + basic attributes: + + >>> from sympy.physics.mechanics import RigidBody, PlanarJoint + >>> parent = RigidBody('P') + >>> parent + P + >>> child = RigidBody('C') + >>> child + C + >>> joint = PlanarJoint('PC', parent, child) + >>> joint + PlanarJoint: PC parent: P child: C + >>> joint.name + 'PC' + >>> joint.parent + P + >>> joint.child + C + >>> joint.parent_point + P_masscenter + >>> joint.child_point + C_masscenter + >>> joint.rotation_axis + P_frame.x + >>> joint.planar_vectors + [P_frame.y, P_frame.z] + >>> joint.rotation_coordinate + q0_PC(t) + >>> joint.planar_coordinates + Matrix([ + [q1_PC(t)], + [q2_PC(t)]]) + >>> joint.coordinates + Matrix([ + [q0_PC(t)], + [q1_PC(t)], + [q2_PC(t)]]) + >>> joint.rotation_speed + u0_PC(t) + >>> joint.planar_speeds + Matrix([ + [u1_PC(t)], + [u2_PC(t)]]) + >>> joint.speeds + Matrix([ + [u0_PC(t)], + [u1_PC(t)], + [u2_PC(t)]]) + >>> child.frame.ang_vel_in(parent.frame) + u0_PC(t)*P_frame.x + >>> child.frame.dcm(parent.frame) + Matrix([ + [1, 0, 0], + [0, cos(q0_PC(t)), sin(q0_PC(t))], + [0, -sin(q0_PC(t)), cos(q0_PC(t))]]) + >>> joint.child_point.pos_from(joint.parent_point) + q1_PC(t)*P_frame.y + q2_PC(t)*P_frame.z + >>> child.masscenter.vel(parent.frame) + u1_PC(t)*P_frame.y + u2_PC(t)*P_frame.z + + To further demonstrate the use of the planar joint, the kinematics of a + block sliding on a slope, can be created as follows. + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import PlanarJoint, RigidBody, ReferenceFrame + >>> a, d, h = symbols('a d h') + + First create bodies to represent the slope and the block. + + >>> ground = RigidBody('G') + >>> block = RigidBody('B') + + To define the slope you can either define the plane by specifying the + ``planar_vectors`` or/and the ``rotation_axis``. However it is advisable to + create a rotated intermediate frame, so that the ``parent_vectors`` and + ``rotation_axis`` will be the unit vectors of this intermediate frame. + + >>> slope = ReferenceFrame('A') + >>> slope.orient_axis(ground.frame, ground.y, a) + + The planar joint can be created using these bodies and intermediate frame. + We can specify the origin of the slope to be ``d`` above the slope's center + of mass and the block's center of mass to be a distance ``h`` above the + slope's surface. Note that we can specify the normal of the plane using the + rotation axis argument. + + >>> joint = PlanarJoint('PC', ground, block, parent_point=d * ground.x, + ... child_point=-h * block.x, parent_interframe=slope) + + Once the joint is established the kinematics of the bodies can be accessed. + First the ``rotation_axis``, which is normal to the plane and the + ``plane_vectors``, can be found. + + >>> joint.rotation_axis + A.x + >>> joint.planar_vectors + [A.y, A.z] + + The direction cosine matrix of the block with respect to the ground can be + found with: + + >>> block.frame.dcm(ground.frame) + Matrix([ + [ cos(a), 0, -sin(a)], + [sin(a)*sin(q0_PC(t)), cos(q0_PC(t)), sin(q0_PC(t))*cos(a)], + [sin(a)*cos(q0_PC(t)), -sin(q0_PC(t)), cos(a)*cos(q0_PC(t))]]) + + The angular velocity of the block can be computed with respect to the + ground. + + >>> block.frame.ang_vel_in(ground.frame) + u0_PC(t)*A.x + + The position of the block's center of mass can be found with: + + >>> block.masscenter.pos_from(ground.masscenter) + d*G_frame.x + h*B_frame.x + q1_PC(t)*A.y + q2_PC(t)*A.z + + Finally, the linear velocity of the block's center of mass can be + computed with respect to the ground. + + >>> block.masscenter.vel(ground.frame) + u1_PC(t)*A.y + u2_PC(t)*A.z + + In some cases it could be your preference to only define the normals of the + plane with respect to both bodies. This can most easily be done by supplying + vectors to the ``interframe`` arguments. What will happen in this case is + that an interframe will be created with its ``x`` axis aligned with the + provided vector. For a further explanation of how this is done see the notes + of the ``Joint`` class. In the code below, the above example (with the block + on the slope) is recreated by supplying vectors to the interframe arguments. + Note that the previously described option is however more computationally + efficient, because the algorithm now has to compute the rotation angle + between the provided vector and the 'x' axis. + + >>> from sympy import symbols, cos, sin + >>> from sympy.physics.mechanics import PlanarJoint, RigidBody + >>> a, d, h = symbols('a d h') + >>> ground = RigidBody('G') + >>> block = RigidBody('B') + >>> joint = PlanarJoint( + ... 'PC', ground, block, parent_point=d * ground.x, + ... child_point=-h * block.x, child_interframe=block.x, + ... parent_interframe=cos(a) * ground.x + sin(a) * ground.z) + >>> block.frame.dcm(ground.frame).simplify() + Matrix([ + [ cos(a), 0, sin(a)], + [-sin(a)*sin(q0_PC(t)), cos(q0_PC(t)), sin(q0_PC(t))*cos(a)], + [-sin(a)*cos(q0_PC(t)), -sin(q0_PC(t)), cos(a)*cos(q0_PC(t))]]) + + """ + + def __init__(self, name, parent, child, rotation_coordinate=None, + planar_coordinates=None, rotation_speed=None, + planar_speeds=None, parent_point=None, child_point=None, + parent_interframe=None, child_interframe=None): + # A ready to merge implementation of setting the planar_vectors and + # rotation_axis was added and removed in PR #24046 + coordinates = (rotation_coordinate, planar_coordinates) + speeds = (rotation_speed, planar_speeds) + super().__init__(name, parent, child, coordinates, speeds, + parent_point, child_point, + parent_interframe=parent_interframe, + child_interframe=child_interframe) + + def __str__(self): + return (f'PlanarJoint: {self.name} parent: {self.parent} ' + f'child: {self.child}') + + @property + def rotation_coordinate(self): + """Generalized coordinate corresponding to the rotation angle.""" + return self.coordinates[0] + + @property + def planar_coordinates(self): + """Two generalized coordinates used for the planar translation.""" + return self.coordinates[1:, 0] + + @property + def rotation_speed(self): + """Generalized speed corresponding to the angular velocity.""" + return self.speeds[0] + + @property + def planar_speeds(self): + """Two generalized speeds used for the planar translation velocity.""" + return self.speeds[1:, 0] + + @property + def rotation_axis(self): + """The axis about which the rotation occurs.""" + return self.parent_interframe.x + + @property + def planar_vectors(self): + """The vectors that describe the planar translation directions.""" + return [self.parent_interframe.y, self.parent_interframe.z] + + def _generate_coordinates(self, coordinates): + rotation_speed = self._fill_coordinate_list(coordinates[0], 1, 'q', + number_single=True) + planar_speeds = self._fill_coordinate_list(coordinates[1], 2, 'q', 1) + return rotation_speed.col_join(planar_speeds) + + def _generate_speeds(self, speeds): + rotation_speed = self._fill_coordinate_list(speeds[0], 1, 'u', + number_single=True) + planar_speeds = self._fill_coordinate_list(speeds[1], 2, 'u', 1) + return rotation_speed.col_join(planar_speeds) + + def _orient_frames(self): + self.child_interframe.orient_axis( + self.parent_interframe, self.rotation_axis, + self.rotation_coordinate) + + def _set_angular_velocity(self): + self.child_interframe.set_ang_vel( + self.parent_interframe, + self.rotation_speed * self.rotation_axis) + + def _set_linear_velocity(self): + self.child_point.set_pos( + self.parent_point, + self.planar_coordinates[0] * self.planar_vectors[0] + + self.planar_coordinates[1] * self.planar_vectors[1]) + self.parent_point.set_vel(self.parent_interframe, 0) + self.child_point.set_vel(self.child_interframe, 0) + self.child_point.set_vel( + self._parent_frame, self.planar_speeds[0] * self.planar_vectors[0] + + self.planar_speeds[1] * self.planar_vectors[1]) + self.child.masscenter.v2pt_theory(self.child_point, self._parent_frame, + self._child_frame) + + +class SphericalJoint(Joint): + """Spherical (Ball-and-Socket) Joint. + + .. image:: SphericalJoint.svg + :align: center + :width: 600 + + Explanation + =========== + + A spherical joint is defined such that the child body is free to rotate in + any direction, without allowing a translation of the ``child_point``. As can + also be seen in the image, the ``parent_point`` and ``child_point`` are + fixed on top of each other, i.e. the ``joint_point``. This rotation is + defined using the :func:`parent_interframe.orient(child_interframe, + rot_type, amounts, rot_order) + ` method. The default + rotation consists of three relative rotations, i.e. body-fixed rotations. + Based on the direction cosine matrix following from these rotations, the + angular velocity is computed based on the generalized coordinates and + generalized speeds. + + Parameters + ========== + + name : string + A unique name for the joint. + parent : Particle or RigidBody + The parent body of joint. + child : Particle or RigidBody + The child body of joint. + coordinates: iterable of dynamicsymbols, optional + Generalized coordinates of the joint. + speeds : iterable of dynamicsymbols, optional + Generalized speeds of joint. + parent_point : Point or Vector, optional + Attachment point where the joint is fixed to the parent body. If a + vector is provided, then the attachment point is computed by adding the + vector to the body's mass center. The default value is the parent's mass + center. + child_point : Point or Vector, optional + Attachment point where the joint is fixed to the child body. If a + vector is provided, then the attachment point is computed by adding the + vector to the body's mass center. The default value is the child's mass + center. + parent_interframe : ReferenceFrame, optional + Intermediate frame of the parent body with respect to which the joint + transformation is formulated. If a Vector is provided then an interframe + is created which aligns its X axis with the given vector. The default + value is the parent's own frame. + child_interframe : ReferenceFrame, optional + Intermediate frame of the child body with respect to which the joint + transformation is formulated. If a Vector is provided then an interframe + is created which aligns its X axis with the given vector. The default + value is the child's own frame. + rot_type : str, optional + The method used to generate the direction cosine matrix. Supported + methods are: + + - ``'Body'``: three successive rotations about new intermediate axes, + also called "Euler and Tait-Bryan angles" + - ``'Space'``: three successive rotations about the parent frames' unit + vectors + + The default method is ``'Body'``. + amounts : + Expressions defining the rotation angles or direction cosine matrix. + These must match the ``rot_type``. See examples below for details. The + input types are: + + - ``'Body'``: 3-tuple of expressions, symbols, or functions + - ``'Space'``: 3-tuple of expressions, symbols, or functions + + The default amounts are the given ``coordinates``. + rot_order : str or int, optional + If applicable, the order of the successive of rotations. The string + ``'123'`` and integer ``123`` are equivalent, for example. Required for + ``'Body'`` and ``'Space'``. The default value is ``123``. + + Attributes + ========== + + name : string + The joint's name. + parent : Particle or RigidBody + The joint's parent body. + child : Particle or RigidBody + The joint's child body. + coordinates : Matrix + Matrix of the joint's generalized coordinates. + speeds : Matrix + Matrix of the joint's generalized speeds. + parent_point : Point + Attachment point where the joint is fixed to the parent body. + child_point : Point + Attachment point where the joint is fixed to the child body. + parent_interframe : ReferenceFrame + Intermediate frame of the parent body with respect to which the joint + transformation is formulated. + child_interframe : ReferenceFrame + Intermediate frame of the child body with respect to which the joint + transformation is formulated. + kdes : Matrix + Kinematical differential equations of the joint. + + Examples + ========= + + A single spherical joint is created from two bodies and has the following + basic attributes: + + >>> from sympy.physics.mechanics import RigidBody, SphericalJoint + >>> parent = RigidBody('P') + >>> parent + P + >>> child = RigidBody('C') + >>> child + C + >>> joint = SphericalJoint('PC', parent, child) + >>> joint + SphericalJoint: PC parent: P child: C + >>> joint.name + 'PC' + >>> joint.parent + P + >>> joint.child + C + >>> joint.parent_point + P_masscenter + >>> joint.child_point + C_masscenter + >>> joint.parent_interframe + P_frame + >>> joint.child_interframe + C_frame + >>> joint.coordinates + Matrix([ + [q0_PC(t)], + [q1_PC(t)], + [q2_PC(t)]]) + >>> joint.speeds + Matrix([ + [u0_PC(t)], + [u1_PC(t)], + [u2_PC(t)]]) + >>> child.frame.ang_vel_in(parent.frame).to_matrix(child.frame) + Matrix([ + [ u0_PC(t)*cos(q1_PC(t))*cos(q2_PC(t)) + u1_PC(t)*sin(q2_PC(t))], + [-u0_PC(t)*sin(q2_PC(t))*cos(q1_PC(t)) + u1_PC(t)*cos(q2_PC(t))], + [ u0_PC(t)*sin(q1_PC(t)) + u2_PC(t)]]) + >>> child.frame.x.to_matrix(parent.frame) + Matrix([ + [ cos(q1_PC(t))*cos(q2_PC(t))], + [sin(q0_PC(t))*sin(q1_PC(t))*cos(q2_PC(t)) + sin(q2_PC(t))*cos(q0_PC(t))], + [sin(q0_PC(t))*sin(q2_PC(t)) - sin(q1_PC(t))*cos(q0_PC(t))*cos(q2_PC(t))]]) + >>> joint.child_point.pos_from(joint.parent_point) + 0 + + To further demonstrate the use of the spherical joint, the kinematics of a + spherical joint with a ZXZ rotation can be created as follows. + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import RigidBody, SphericalJoint + >>> l1 = symbols('l1') + + First create bodies to represent the fixed floor and a pendulum bob. + + >>> floor = RigidBody('F') + >>> bob = RigidBody('B') + + The joint will connect the bob to the floor, with the joint located at a + distance of ``l1`` from the child's center of mass and the rotation set to a + body-fixed ZXZ rotation. + + >>> joint = SphericalJoint('S', floor, bob, child_point=l1 * bob.y, + ... rot_type='body', rot_order='ZXZ') + + Now that the joint is established, the kinematics of the connected body can + be accessed. + + The position of the bob's masscenter is found with: + + >>> bob.masscenter.pos_from(floor.masscenter) + - l1*B_frame.y + + The angular velocities of the pendulum link can be computed with respect to + the floor. + + >>> bob.frame.ang_vel_in(floor.frame).to_matrix( + ... floor.frame).simplify() + Matrix([ + [u1_S(t)*cos(q0_S(t)) + u2_S(t)*sin(q0_S(t))*sin(q1_S(t))], + [u1_S(t)*sin(q0_S(t)) - u2_S(t)*sin(q1_S(t))*cos(q0_S(t))], + [ u0_S(t) + u2_S(t)*cos(q1_S(t))]]) + + Finally, the linear velocity of the bob's center of mass can be computed. + + >>> bob.masscenter.vel(floor.frame).to_matrix(bob.frame) + Matrix([ + [ l1*(u0_S(t)*cos(q1_S(t)) + u2_S(t))], + [ 0], + [-l1*(u0_S(t)*sin(q1_S(t))*sin(q2_S(t)) + u1_S(t)*cos(q2_S(t)))]]) + + """ + def __init__(self, name, parent, child, coordinates=None, speeds=None, + parent_point=None, child_point=None, parent_interframe=None, + child_interframe=None, rot_type='BODY', amounts=None, + rot_order=123): + self._rot_type = rot_type + self._amounts = amounts + self._rot_order = rot_order + super().__init__(name, parent, child, coordinates, speeds, + parent_point, child_point, + parent_interframe=parent_interframe, + child_interframe=child_interframe) + + def __str__(self): + return (f'SphericalJoint: {self.name} parent: {self.parent} ' + f'child: {self.child}') + + def _generate_coordinates(self, coordinates): + return self._fill_coordinate_list(coordinates, 3, 'q') + + def _generate_speeds(self, speeds): + return self._fill_coordinate_list(speeds, len(self.coordinates), 'u') + + def _orient_frames(self): + supported_rot_types = ('BODY', 'SPACE') + if self._rot_type.upper() not in supported_rot_types: + raise NotImplementedError( + f'Rotation type "{self._rot_type}" is not implemented. ' + f'Implemented rotation types are: {supported_rot_types}') + amounts = self.coordinates if self._amounts is None else self._amounts + self.child_interframe.orient(self.parent_interframe, self._rot_type, + amounts, self._rot_order) + + def _set_angular_velocity(self): + t = dynamicsymbols._t + vel = self.child_interframe.ang_vel_in(self.parent_interframe).xreplace( + {q.diff(t): u for q, u in zip(self.coordinates, self.speeds)} + ) + self.child_interframe.set_ang_vel(self.parent_interframe, vel) + + def _set_linear_velocity(self): + self.child_point.set_pos(self.parent_point, 0) + self.parent_point.set_vel(self._parent_frame, 0) + self.child_point.set_vel(self._child_frame, 0) + self.child.masscenter.v2pt_theory(self.parent_point, self._parent_frame, + self._child_frame) + + +class WeldJoint(Joint): + """Weld Joint. + + .. raw:: html + :file: ../../../doc/src/modules/physics/mechanics/api/WeldJoint.svg + + Explanation + =========== + + A weld joint is defined such that there is no relative motion between the + child and parent bodies. The direction cosine matrix between the attachment + frame (``parent_interframe`` and ``child_interframe``) is the identity + matrix and the attachment points (``parent_point`` and ``child_point``) are + coincident. The page on the joints framework gives a more detailed + explanation of the intermediate frames. + + Parameters + ========== + + name : string + A unique name for the joint. + parent : Particle or RigidBody + The parent body of joint. + child : Particle or RigidBody + The child body of joint. + parent_point : Point or Vector, optional + Attachment point where the joint is fixed to the parent body. If a + vector is provided, then the attachment point is computed by adding the + vector to the body's mass center. The default value is the parent's mass + center. + child_point : Point or Vector, optional + Attachment point where the joint is fixed to the child body. If a + vector is provided, then the attachment point is computed by adding the + vector to the body's mass center. The default value is the child's mass + center. + parent_interframe : ReferenceFrame, optional + Intermediate frame of the parent body with respect to which the joint + transformation is formulated. If a Vector is provided then an interframe + is created which aligns its X axis with the given vector. The default + value is the parent's own frame. + child_interframe : ReferenceFrame, optional + Intermediate frame of the child body with respect to which the joint + transformation is formulated. If a Vector is provided then an interframe + is created which aligns its X axis with the given vector. The default + value is the child's own frame. + + Attributes + ========== + + name : string + The joint's name. + parent : Particle or RigidBody + The joint's parent body. + child : Particle or RigidBody + The joint's child body. + coordinates : Matrix + Matrix of the joint's generalized coordinates. The default value is + ``dynamicsymbols(f'q_{joint.name}')``. + speeds : Matrix + Matrix of the joint's generalized speeds. The default value is + ``dynamicsymbols(f'u_{joint.name}')``. + parent_point : Point + Attachment point where the joint is fixed to the parent body. + child_point : Point + Attachment point where the joint is fixed to the child body. + parent_interframe : ReferenceFrame + Intermediate frame of the parent body with respect to which the joint + transformation is formulated. + child_interframe : ReferenceFrame + Intermediate frame of the child body with respect to which the joint + transformation is formulated. + kdes : Matrix + Kinematical differential equations of the joint. + + Examples + ========= + + A single weld joint is created from two bodies and has the following basic + attributes: + + >>> from sympy.physics.mechanics import RigidBody, WeldJoint + >>> parent = RigidBody('P') + >>> parent + P + >>> child = RigidBody('C') + >>> child + C + >>> joint = WeldJoint('PC', parent, child) + >>> joint + WeldJoint: PC parent: P child: C + >>> joint.name + 'PC' + >>> joint.parent + P + >>> joint.child + C + >>> joint.parent_point + P_masscenter + >>> joint.child_point + C_masscenter + >>> joint.coordinates + Matrix(0, 0, []) + >>> joint.speeds + Matrix(0, 0, []) + >>> child.frame.ang_vel_in(parent.frame) + 0 + >>> child.frame.dcm(parent.frame) + Matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + >>> joint.child_point.pos_from(joint.parent_point) + 0 + + To further demonstrate the use of the weld joint, two relatively-fixed + bodies rotated by a quarter turn about the Y axis can be created as follows: + + >>> from sympy import symbols, pi + >>> from sympy.physics.mechanics import ReferenceFrame, RigidBody, WeldJoint + >>> l1, l2 = symbols('l1 l2') + + First create the bodies to represent the parent and rotated child body. + + >>> parent = RigidBody('P') + >>> child = RigidBody('C') + + Next the intermediate frame specifying the fixed rotation with respect to + the parent can be created. + + >>> rotated_frame = ReferenceFrame('Pr') + >>> rotated_frame.orient_axis(parent.frame, parent.y, pi / 2) + + The weld between the parent body and child body is located at a distance + ``l1`` from the parent's center of mass in the X direction and ``l2`` from + the child's center of mass in the child's negative X direction. + + >>> weld = WeldJoint('weld', parent, child, parent_point=l1 * parent.x, + ... child_point=-l2 * child.x, + ... parent_interframe=rotated_frame) + + Now that the joint has been established, the kinematics of the bodies can be + accessed. The direction cosine matrix of the child body with respect to the + parent can be found: + + >>> child.frame.dcm(parent.frame) + Matrix([ + [0, 0, -1], + [0, 1, 0], + [1, 0, 0]]) + + As can also been seen from the direction cosine matrix, the parent X axis is + aligned with the child's Z axis: + >>> parent.x == child.z + True + + The position of the child's center of mass with respect to the parent's + center of mass can be found with: + + >>> child.masscenter.pos_from(parent.masscenter) + l1*P_frame.x + l2*C_frame.x + + The angular velocity of the child with respect to the parent is 0 as one + would expect. + + >>> child.frame.ang_vel_in(parent.frame) + 0 + + """ + + def __init__(self, name, parent, child, parent_point=None, child_point=None, + parent_interframe=None, child_interframe=None): + super().__init__(name, parent, child, [], [], parent_point, + child_point, parent_interframe=parent_interframe, + child_interframe=child_interframe) + self._kdes = Matrix(1, 0, []).T # Removes stackability problems #10770 + + def __str__(self): + return (f'WeldJoint: {self.name} parent: {self.parent} ' + f'child: {self.child}') + + def _generate_coordinates(self, coordinate): + return Matrix() + + def _generate_speeds(self, speed): + return Matrix() + + def _orient_frames(self): + self.child_interframe.orient_axis(self.parent_interframe, + self.parent_interframe.x, 0) + + def _set_angular_velocity(self): + self.child_interframe.set_ang_vel(self.parent_interframe, 0) + + def _set_linear_velocity(self): + self.child_point.set_pos(self.parent_point, 0) + self.parent_point.set_vel(self._parent_frame, 0) + self.child_point.set_vel(self._child_frame, 0) + self.child.masscenter.set_vel(self._parent_frame, 0) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/jointsmethod.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/jointsmethod.py new file mode 100644 index 0000000000000000000000000000000000000000..df7bd56360072feb57a65e5f78c2d116f0d4842d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/jointsmethod.py @@ -0,0 +1,318 @@ +from sympy.physics.mechanics import (Body, Lagrangian, KanesMethod, LagrangesMethod, + RigidBody, Particle) +from sympy.physics.mechanics.body_base import BodyBase +from sympy.physics.mechanics.method import _Methods +from sympy import Matrix +from sympy.utilities.exceptions import sympy_deprecation_warning + +__all__ = ['JointsMethod'] + + +class JointsMethod(_Methods): + """Method for formulating the equations of motion using a set of interconnected bodies with joints. + + .. deprecated:: 1.13 + The JointsMethod class is deprecated. Its functionality has been + replaced by the new :class:`~.System` class. + + Parameters + ========== + + newtonion : Body or ReferenceFrame + The newtonion(inertial) frame. + *joints : Joint + The joints in the system + + Attributes + ========== + + q, u : iterable + Iterable of the generalized coordinates and speeds + bodies : iterable + Iterable of Body objects in the system. + loads : iterable + Iterable of (Point, vector) or (ReferenceFrame, vector) tuples + describing the forces on the system. + mass_matrix : Matrix, shape(n, n) + The system's mass matrix + forcing : Matrix, shape(n, 1) + The system's forcing vector + mass_matrix_full : Matrix, shape(2*n, 2*n) + The "mass matrix" for the u's and q's + forcing_full : Matrix, shape(2*n, 1) + The "forcing vector" for the u's and q's + method : KanesMethod or Lagrange's method + Method's object. + kdes : iterable + Iterable of kde in they system. + + Examples + ======== + + As Body and JointsMethod have been deprecated, the following examples are + for illustrative purposes only. The functionality of Body is fully captured + by :class:`~.RigidBody` and :class:`~.Particle` and the functionality of + JointsMethod is fully captured by :class:`~.System`. To ignore the + deprecation warning we can use the ignore_warnings context manager. + + >>> from sympy.utilities.exceptions import ignore_warnings + + This is a simple example for a one degree of freedom translational + spring-mass-damper. + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import Body, JointsMethod, PrismaticJoint + >>> from sympy.physics.vector import dynamicsymbols + >>> c, k = symbols('c k') + >>> x, v = dynamicsymbols('x v') + >>> with ignore_warnings(DeprecationWarning): + ... wall = Body('W') + ... body = Body('B') + >>> J = PrismaticJoint('J', wall, body, coordinates=x, speeds=v) + >>> wall.apply_force(c*v*wall.x, reaction_body=body) + >>> wall.apply_force(k*x*wall.x, reaction_body=body) + >>> with ignore_warnings(DeprecationWarning): + ... method = JointsMethod(wall, J) + >>> method.form_eoms() + Matrix([[-B_mass*Derivative(v(t), t) - c*v(t) - k*x(t)]]) + >>> M = method.mass_matrix_full + >>> F = method.forcing_full + >>> rhs = M.LUsolve(F) + >>> rhs + Matrix([ + [ v(t)], + [(-c*v(t) - k*x(t))/B_mass]]) + + Notes + ===== + + ``JointsMethod`` currently only works with systems that do not have any + configuration or motion constraints. + + """ + + def __init__(self, newtonion, *joints): + sympy_deprecation_warning( + """ + The JointsMethod class is deprecated. + Its functionality has been replaced by the new System class. + """, + deprecated_since_version="1.13", + active_deprecations_target="deprecated-mechanics-jointsmethod" + ) + if isinstance(newtonion, BodyBase): + self.frame = newtonion.frame + else: + self.frame = newtonion + + self._joints = joints + self._bodies = self._generate_bodylist() + self._loads = self._generate_loadlist() + self._q = self._generate_q() + self._u = self._generate_u() + self._kdes = self._generate_kdes() + + self._method = None + + @property + def bodies(self): + """List of bodies in they system.""" + return self._bodies + + @property + def loads(self): + """List of loads on the system.""" + return self._loads + + @property + def q(self): + """List of the generalized coordinates.""" + return self._q + + @property + def u(self): + """List of the generalized speeds.""" + return self._u + + @property + def kdes(self): + """List of the generalized coordinates.""" + return self._kdes + + @property + def forcing_full(self): + """The "forcing vector" for the u's and q's.""" + return self.method.forcing_full + + @property + def mass_matrix_full(self): + """The "mass matrix" for the u's and q's.""" + return self.method.mass_matrix_full + + @property + def mass_matrix(self): + """The system's mass matrix.""" + return self.method.mass_matrix + + @property + def forcing(self): + """The system's forcing vector.""" + return self.method.forcing + + @property + def method(self): + """Object of method used to form equations of systems.""" + return self._method + + def _generate_bodylist(self): + bodies = [] + for joint in self._joints: + if joint.child not in bodies: + bodies.append(joint.child) + if joint.parent not in bodies: + bodies.append(joint.parent) + return bodies + + def _generate_loadlist(self): + load_list = [] + for body in self.bodies: + if isinstance(body, Body): + load_list.extend(body.loads) + return load_list + + def _generate_q(self): + q_ind = [] + for joint in self._joints: + for coordinate in joint.coordinates: + if coordinate in q_ind: + raise ValueError('Coordinates of joints should be unique.') + q_ind.append(coordinate) + return Matrix(q_ind) + + def _generate_u(self): + u_ind = [] + for joint in self._joints: + for speed in joint.speeds: + if speed in u_ind: + raise ValueError('Speeds of joints should be unique.') + u_ind.append(speed) + return Matrix(u_ind) + + def _generate_kdes(self): + kd_ind = Matrix(1, 0, []).T + for joint in self._joints: + kd_ind = kd_ind.col_join(joint.kdes) + return kd_ind + + def _convert_bodies(self): + # Convert `Body` to `Particle` and `RigidBody` + bodylist = [] + for body in self.bodies: + if not isinstance(body, Body): + bodylist.append(body) + continue + if body.is_rigidbody: + rb = RigidBody(body.name, body.masscenter, body.frame, body.mass, + (body.central_inertia, body.masscenter)) + rb.potential_energy = body.potential_energy + bodylist.append(rb) + else: + part = Particle(body.name, body.masscenter, body.mass) + part.potential_energy = body.potential_energy + bodylist.append(part) + return bodylist + + def form_eoms(self, method=KanesMethod): + """Method to form system's equation of motions. + + Parameters + ========== + + method : Class + Class name of method. + + Returns + ======== + + Matrix + Vector of equations of motions. + + Examples + ======== + + As Body and JointsMethod have been deprecated, the following examples + are for illustrative purposes only. The functionality of Body is fully + captured by :class:`~.RigidBody` and :class:`~.Particle` and the + functionality of JointsMethod is fully captured by :class:`~.System`. To + ignore the deprecation warning we can use the ignore_warnings context + manager. + + >>> from sympy.utilities.exceptions import ignore_warnings + + This is a simple example for a one degree of freedom translational + spring-mass-damper. + + >>> from sympy import S, symbols + >>> from sympy.physics.mechanics import LagrangesMethod, dynamicsymbols, Body + >>> from sympy.physics.mechanics import PrismaticJoint, JointsMethod + >>> q = dynamicsymbols('q') + >>> qd = dynamicsymbols('q', 1) + >>> m, k, b = symbols('m k b') + >>> with ignore_warnings(DeprecationWarning): + ... wall = Body('W') + ... part = Body('P', mass=m) + >>> part.potential_energy = k * q**2 / S(2) + >>> J = PrismaticJoint('J', wall, part, coordinates=q, speeds=qd) + >>> wall.apply_force(b * qd * wall.x, reaction_body=part) + >>> with ignore_warnings(DeprecationWarning): + ... method = JointsMethod(wall, J) + >>> method.form_eoms(LagrangesMethod) + Matrix([[b*Derivative(q(t), t) + k*q(t) + m*Derivative(q(t), (t, 2))]]) + + We can also solve for the states using the 'rhs' method. + + >>> method.rhs() + Matrix([ + [ Derivative(q(t), t)], + [(-b*Derivative(q(t), t) - k*q(t))/m]]) + + """ + + bodylist = self._convert_bodies() + if issubclass(method, LagrangesMethod): #LagrangesMethod or similar + L = Lagrangian(self.frame, *bodylist) + self._method = method(L, self.q, self.loads, bodylist, self.frame) + else: #KanesMethod or similar + self._method = method(self.frame, q_ind=self.q, u_ind=self.u, kd_eqs=self.kdes, + forcelist=self.loads, bodies=bodylist) + soln = self.method._form_eoms() + return soln + + def rhs(self, inv_method=None): + """Returns equations that can be solved numerically. + + Parameters + ========== + + inv_method : str + The specific sympy inverse matrix calculation method to use. For a + list of valid methods, see + :meth:`~sympy.matrices.matrixbase.MatrixBase.inv` + + Returns + ======== + + Matrix + Numerically solvable equations. + + See Also + ======== + + sympy.physics.mechanics.kane.KanesMethod.rhs: + KanesMethod's rhs function. + sympy.physics.mechanics.lagrange.LagrangesMethod.rhs: + LagrangesMethod's rhs function. + + """ + + return self.method.rhs(inv_method=inv_method) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/kane.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/kane.py new file mode 100644 index 0000000000000000000000000000000000000000..805587a4fe9d7696f45c5815ee5406b103150698 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/kane.py @@ -0,0 +1,859 @@ +from sympy import zeros, Matrix, diff, eye, linear_eq_to_matrix +from sympy.core.sorting import default_sort_key +from sympy.physics.vector import (ReferenceFrame, dynamicsymbols, + partial_velocity) +from sympy.physics.mechanics.method import _Methods +from sympy.physics.mechanics.particle import Particle +from sympy.physics.mechanics.rigidbody import RigidBody +from sympy.physics.mechanics.functions import (msubs, find_dynamicsymbols, + _f_list_parser, + _validate_coordinates, + _parse_linear_solver) +from sympy.physics.mechanics.linearize import Linearizer +from sympy.utilities.iterables import iterable + + +__all__ = ['KanesMethod'] + + +class KanesMethod(_Methods): + r"""Kane's method object. + + Explanation + =========== + + This object is used to do the "book-keeping" as you go through and form + equations of motion in the way Kane presents in: + Kane, T., Levinson, D. Dynamics Theory and Applications. 1985 McGraw-Hill + + The attributes are for equations in the form [M] udot = forcing. + + Attributes + ========== + + q, u : Matrix + Matrices of the generalized coordinates and speeds + bodies : iterable + Iterable of Particle and RigidBody objects in the system. + loads : iterable + Iterable of (Point, vector) or (ReferenceFrame, vector) tuples + describing the forces on the system. + auxiliary_eqs : Matrix + If applicable, the set of auxiliary Kane's + equations used to solve for non-contributing + forces. + mass_matrix : Matrix + The system's dynamics mass matrix: [k_d; k_dnh] + forcing : Matrix + The system's dynamics forcing vector: -[f_d; f_dnh] + mass_matrix_kin : Matrix + The "mass matrix" for kinematic differential equations: k_kqdot + forcing_kin : Matrix + The forcing vector for kinematic differential equations: -(k_ku*u + f_k) + mass_matrix_full : Matrix + The "mass matrix" for the u's and q's with dynamics and kinematics + forcing_full : Matrix + The "forcing vector" for the u's and q's with dynamics and kinematics + + Parameters + ========== + + frame : ReferenceFrame + The inertial reference frame for the system. + q_ind : iterable of dynamicsymbols + Independent generalized coordinates. + u_ind : iterable of dynamicsymbols + Independent generalized speeds. + kd_eqs : iterable of Expr, optional + Kinematic differential equations, which linearly relate the generalized + speeds to the time-derivatives of the generalized coordinates. + q_dependent : iterable of dynamicsymbols, optional + Dependent generalized coordinates. + configuration_constraints : iterable of Expr, optional + Constraints on the system's configuration, i.e. holonomic constraints. + u_dependent : iterable of dynamicsymbols, optional + Dependent generalized speeds. + velocity_constraints : iterable of Expr, optional + Constraints on the system's velocity, i.e. the combination of the + nonholonomic constraints and the time-derivative of the holonomic + constraints. + acceleration_constraints : iterable of Expr, optional + Constraints on the system's acceleration, by default these are the + time-derivative of the velocity constraints. + u_auxiliary : iterable of dynamicsymbols, optional + Auxiliary generalized speeds. + bodies : iterable of Particle and/or RigidBody, optional + The particles and rigid bodies in the system. + forcelist : iterable of tuple[Point | ReferenceFrame, Vector], optional + Forces and torques applied on the system. + explicit_kinematics : bool + Boolean whether the mass matrices and forcing vectors should use the + explicit form (default) or implicit form for kinematics. + See the notes for more details. + kd_eqs_solver : str, callable + Method used to solve the kinematic differential equations. If a string + is supplied, it should be a valid method that can be used with the + :meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is + supplied, it should have the format ``f(A, rhs)``, where it solves the + equations and returns the solution. The default utilizes LU solve. See + the notes for more information. + constraint_solver : str, callable + Method used to solve the velocity constraints. If a string is + supplied, it should be a valid method that can be used with the + :meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is + supplied, it should have the format ``f(A, rhs)``, where it solves the + equations and returns the solution. The default utilizes LU solve. See + the notes for more information. + + Notes + ===== + + The mass matrices and forcing vectors related to kinematic equations + are given in the explicit form by default. In other words, the kinematic + mass matrix is $\mathbf{k_{k\dot{q}}} = \mathbf{I}$. + In order to get the implicit form of those matrices/vectors, you can set the + ``explicit_kinematics`` attribute to ``False``. So $\mathbf{k_{k\dot{q}}}$ + is not necessarily an identity matrix. This can provide more compact + equations for non-simple kinematics. + + Two linear solvers can be supplied to ``KanesMethod``: one for solving the + kinematic differential equations and one to solve the velocity constraints. + Both of these sets of equations can be expressed as a linear system ``Ax = rhs``, + which have to be solved in order to obtain the equations of motion. + + The default solver ``'LU'``, which stands for LU solve, results relatively low + number of operations. The weakness of this method is that it can result in zero + division errors. + + If zero divisions are encountered, a possible solver which may solve the problem + is ``"CRAMER"``. This method uses Cramer's rule to solve the system. This method + is slower and results in more operations than the default solver. However it only + uses a single division by default per entry of the solution. + + While a valid list of solvers can be found at + :meth:`sympy.matrices.matrixbase.MatrixBase.solve`, it is also possible to supply a + `callable`. This way it is possible to use a different solver routine. If the + kinematic differential equations are not too complex it can be worth it to simplify + the solution by using ``lambda A, b: simplify(Matrix.LUsolve(A, b))``. Another + option solver one may use is :func:`sympy.solvers.solveset.linsolve`. This can be + done using `lambda A, b: tuple(linsolve((A, b)))[0]`, where we select the first + solution as our system should have only one unique solution. + + Examples + ======== + + This is a simple example for a one degree of freedom translational + spring-mass-damper. + + In this example, we first need to do the kinematics. + This involves creating generalized speeds and coordinates and their + derivatives. + Then we create a point and set its velocity in a frame. + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import dynamicsymbols, ReferenceFrame + >>> from sympy.physics.mechanics import Point, Particle, KanesMethod + >>> q, u = dynamicsymbols('q u') + >>> qd, ud = dynamicsymbols('q u', 1) + >>> m, c, k = symbols('m c k') + >>> N = ReferenceFrame('N') + >>> P = Point('P') + >>> P.set_vel(N, u * N.x) + + Next we need to arrange/store information in the way that KanesMethod + requires. The kinematic differential equations should be an iterable of + expressions. A list of forces/torques must be constructed, where each entry + in the list is a (Point, Vector) or (ReferenceFrame, Vector) tuple, where + the Vectors represent the Force or Torque. + Next a particle needs to be created, and it needs to have a point and mass + assigned to it. + Finally, a list of all bodies and particles needs to be created. + + >>> kd = [qd - u] + >>> FL = [(P, (-k * q - c * u) * N.x)] + >>> pa = Particle('pa', P, m) + >>> BL = [pa] + + Finally we can generate the equations of motion. + First we create the KanesMethod object and supply an inertial frame, + coordinates, generalized speeds, and the kinematic differential equations. + Additional quantities such as configuration and motion constraints, + dependent coordinates and speeds, and auxiliary speeds are also supplied + here (see the online documentation). + Next we form FR* and FR to complete: Fr + Fr* = 0. + We have the equations of motion at this point. + It makes sense to rearrange them though, so we calculate the mass matrix and + the forcing terms, for E.o.M. in the form: [MM] udot = forcing, where MM is + the mass matrix, udot is a vector of the time derivatives of the + generalized speeds, and forcing is a vector representing "forcing" terms. + + >>> KM = KanesMethod(N, q_ind=[q], u_ind=[u], kd_eqs=kd) + >>> (fr, frstar) = KM.kanes_equations(BL, FL) + >>> MM = KM.mass_matrix + >>> forcing = KM.forcing + >>> rhs = MM.inv() * forcing + >>> rhs + Matrix([[(-c*u(t) - k*q(t))/m]]) + >>> KM.linearize(A_and_B=True)[0] + Matrix([ + [ 0, 1], + [-k/m, -c/m]]) + + Please look at the documentation pages for more information on how to + perform linearization and how to deal with dependent coordinates & speeds, + and how do deal with bringing non-contributing forces into evidence. + + """ + + def __init__(self, frame, q_ind, u_ind, kd_eqs=None, q_dependent=None, + configuration_constraints=None, u_dependent=None, + velocity_constraints=None, acceleration_constraints=None, + u_auxiliary=None, bodies=None, forcelist=None, + explicit_kinematics=True, kd_eqs_solver='LU', + constraint_solver='LU'): + + """Please read the online documentation. """ + if not q_ind: + q_ind = [dynamicsymbols('dummy_q')] + kd_eqs = [dynamicsymbols('dummy_kd')] + + if not isinstance(frame, ReferenceFrame): + raise TypeError('An inertial ReferenceFrame must be supplied') + self._inertial = frame + + self._fr = None + self._frstar = None + + self._forcelist = forcelist + self._bodylist = bodies + + self.explicit_kinematics = explicit_kinematics + self._constraint_solver = constraint_solver + self._initialize_vectors(q_ind, q_dependent, u_ind, u_dependent, + u_auxiliary) + _validate_coordinates(self.q, self.u) + self._initialize_kindiffeq_matrices(kd_eqs, kd_eqs_solver) + self._initialize_constraint_matrices( + configuration_constraints, velocity_constraints, + acceleration_constraints, constraint_solver) + + def _initialize_vectors(self, q_ind, q_dep, u_ind, u_dep, u_aux): + """Initialize the coordinate and speed vectors.""" + + none_handler = lambda x: Matrix(x) if x else Matrix() + + # Initialize generalized coordinates + q_dep = none_handler(q_dep) + if not iterable(q_ind): + raise TypeError('Generalized coordinates must be an iterable.') + if not iterable(q_dep): + raise TypeError('Dependent coordinates must be an iterable.') + q_ind = Matrix(q_ind) + self._qdep = q_dep + self._q = Matrix([q_ind, q_dep]) + self._qdot = self.q.diff(dynamicsymbols._t) + + # Initialize generalized speeds + u_dep = none_handler(u_dep) + if not iterable(u_ind): + raise TypeError('Generalized speeds must be an iterable.') + if not iterable(u_dep): + raise TypeError('Dependent speeds must be an iterable.') + u_ind = Matrix(u_ind) + self._udep = u_dep + self._u = Matrix([u_ind, u_dep]) + self._udot = self.u.diff(dynamicsymbols._t) + self._uaux = none_handler(u_aux) + + def _initialize_constraint_matrices(self, config, vel, acc, linear_solver='LU'): + """Initializes constraint matrices.""" + linear_solver = _parse_linear_solver(linear_solver) + # Define vector dimensions + o = len(self.u) + m = len(self._udep) + p = o - m + none_handler = lambda x: Matrix(x) if x else Matrix() + + # Initialize configuration constraints + config = none_handler(config) + if len(self._qdep) != len(config): + raise ValueError('There must be an equal number of dependent ' + 'coordinates and configuration constraints.') + self._f_h = none_handler(config) + + # Initialize velocity and acceleration constraints + vel = none_handler(vel) + acc = none_handler(acc) + if len(vel) != m: + raise ValueError('There must be an equal number of dependent ' + 'speeds and velocity constraints.') + if acc and (len(acc) != m): + raise ValueError('There must be an equal number of dependent ' + 'speeds and acceleration constraints.') + if vel: + + # When calling kanes_equations, another class instance will be + # created if auxiliary u's are present. In this case, the + # computation of kinetic differential equation matrices will be + # skipped as this was computed during the original KanesMethod + # object, and the qd_u_map will not be available. + if self._qdot_u_map is not None: + vel = msubs(vel, self._qdot_u_map) + self._k_nh, f_nh_neg = linear_eq_to_matrix(vel, self.u[:]) + self._f_nh = -f_nh_neg + + # If no acceleration constraints given, calculate them. + if not acc: + _f_dnh = (self._k_nh.diff(dynamicsymbols._t) * self.u + + self._f_nh.diff(dynamicsymbols._t)) + if self._qdot_u_map is not None: + _f_dnh = msubs(_f_dnh, self._qdot_u_map) + self._f_dnh = _f_dnh + self._k_dnh = self._k_nh + else: + if self._qdot_u_map is not None: + acc = msubs(acc, self._qdot_u_map) + + self._k_dnh, f_dnh_neg = linear_eq_to_matrix(acc, self._udot[:]) + self._f_dnh = -f_dnh_neg + # Form of non-holonomic constraints is B*u + C = 0. + # We partition B into independent and dependent columns: + # Ars is then -B_dep.inv() * B_ind, and it relates dependent speeds + # to independent speeds as: udep = Ars*uind, neglecting the C term. + B_ind = self._k_nh[:, :p] + B_dep = self._k_nh[:, p:o] + self._Ars = -linear_solver(B_dep, B_ind) + else: + self._f_nh = Matrix() + self._k_nh = Matrix() + self._f_dnh = Matrix() + self._k_dnh = Matrix() + self._Ars = Matrix() + + def _initialize_kindiffeq_matrices(self, kdeqs, linear_solver='LU'): + """Initialize the kinematic differential equation matrices. + + Parameters + ========== + kdeqs : sequence of sympy expressions + Kinematic differential equations in the form of f(u,q',q,t) where + f() = 0. The equations have to be linear in the time-derivatives of + the generalized coordinates and in the generalized speeds. + + """ + linear_solver = _parse_linear_solver(linear_solver) + if kdeqs: + if len(self.q) != len(kdeqs): + raise ValueError('There must be an equal number of kinematic ' + 'differential equations and coordinates.') + + u = self.u + qdot = self._qdot + + kdeqs = Matrix(kdeqs) + + u_zero = dict.fromkeys(u, 0) + uaux_zero = dict.fromkeys(self._uaux, 0) + qdot_zero = dict.fromkeys(qdot, 0) + + # Extract the linear coefficient matrices as per the following + # equation: + # + # k_ku(q,t)*u(t) + k_kqdot(q,t)*q'(t) + f_k(q,t) = 0 + # + k_ku = kdeqs.jacobian(u) + k_kqdot = kdeqs.jacobian(qdot) + f_k = kdeqs.xreplace(u_zero).xreplace(qdot_zero) + + # The kinematic differential equations should be linear in both q' + # and u so check for u and q' in the components. + dy_syms = find_dynamicsymbols(k_ku.row_join(k_kqdot).row_join(f_k)) + nonlin_vars = [vari for vari in u[:] + qdot[:] if vari in dy_syms] + if nonlin_vars: + msg = ('The provided kinematic differential equations are ' + 'nonlinear in {}. They must be linear in the ' + 'generalized speeds and derivatives of the generalized ' + 'coordinates.') + raise ValueError(msg.format(nonlin_vars)) + + self._f_k_implicit = f_k.xreplace(uaux_zero) + self._k_ku_implicit = k_ku.xreplace(uaux_zero) + self._k_kqdot_implicit = k_kqdot + + # Solve for q'(t) such that the coefficient matrices are now in + # this form: + # + # k_kqdot^-1*k_ku*u(t) + I*q'(t) + k_kqdot^-1*f_k = 0 + # + # NOTE : Solving the kinematic differential equations here is not + # necessary and prevents the equations from being provided in fully + # implicit form. + f_k_explicit = linear_solver(k_kqdot, f_k) + k_ku_explicit = linear_solver(k_kqdot, k_ku) + self._qdot_u_map = dict(zip(qdot, -(k_ku_explicit*u + f_k_explicit))) + + self._f_k = f_k_explicit.xreplace(uaux_zero) + self._k_ku = k_ku_explicit.xreplace(uaux_zero) + self._k_kqdot = eye(len(qdot)) + + else: + self._qdot_u_map = None + self._f_k_implicit = self._f_k = Matrix() + self._k_ku_implicit = self._k_ku = Matrix() + self._k_kqdot_implicit = self._k_kqdot = Matrix() + + def _form_fr(self, fl): + """Form the generalized active force.""" + if fl is not None and (len(fl) == 0 or not iterable(fl)): + raise ValueError('Force pairs must be supplied in an ' + 'non-empty iterable or None.') + + N = self._inertial + # pull out relevant velocities for constructing partial velocities + vel_list, f_list = _f_list_parser(fl, N) + vel_list = [msubs(i, self._qdot_u_map) for i in vel_list] + f_list = [msubs(i, self._qdot_u_map) for i in f_list] + + # Fill Fr with dot product of partial velocities and forces + o = len(self.u) + b = len(f_list) + FR = zeros(o, 1) + partials = partial_velocity(vel_list, self.u, N) + for i in range(o): + FR[i] = sum(partials[j][i].dot(f_list[j]) for j in range(b)) + + # In case there are dependent speeds + if self._udep: + p = o - len(self._udep) + FRtilde = FR[:p, 0] + FRold = FR[p:o, 0] + FRtilde += self._Ars.T * FRold + FR = FRtilde + + self._forcelist = fl + self._fr = FR + return FR + + def _form_frstar(self, bl): + """Form the generalized inertia force.""" + + if not iterable(bl): + raise TypeError('Bodies must be supplied in an iterable.') + + t = dynamicsymbols._t + N = self._inertial + # Dicts setting things to zero + udot_zero = dict.fromkeys(self._udot, 0) + uaux_zero = dict.fromkeys(self._uaux, 0) + uauxdot = [diff(i, t) for i in self._uaux] + uauxdot_zero = dict.fromkeys(uauxdot, 0) + # Dictionary of q' and q'' to u and u' + q_ddot_u_map = {k.diff(t): v.diff(t).xreplace( + self._qdot_u_map) for (k, v) in self._qdot_u_map.items()} + q_ddot_u_map.update(self._qdot_u_map) + + # Fill up the list of partials: format is a list with num elements + # equal to number of entries in body list. Each of these elements is a + # list - either of length 1 for the translational components of + # particles or of length 2 for the translational and rotational + # components of rigid bodies. The inner most list is the list of + # partial velocities. + def get_partial_velocity(body): + if isinstance(body, RigidBody): + vlist = [body.masscenter.vel(N), body.frame.ang_vel_in(N)] + elif isinstance(body, Particle): + vlist = [body.point.vel(N),] + else: + raise TypeError('The body list may only contain either ' + 'RigidBody or Particle as list elements.') + v = [msubs(vel, self._qdot_u_map) for vel in vlist] + return partial_velocity(v, self.u, N) + partials = [get_partial_velocity(body) for body in bl] + + # Compute fr_star in two components: + # fr_star = -(MM*u' + nonMM) + o = len(self.u) + MM = zeros(o, o) + nonMM = zeros(o, 1) + zero_uaux = lambda expr: msubs(expr, uaux_zero) + zero_udot_uaux = lambda expr: msubs(msubs(expr, udot_zero), uaux_zero) + for i, body in enumerate(bl): + if isinstance(body, RigidBody): + M = zero_uaux(body.mass) + I = zero_uaux(body.central_inertia) + vel = zero_uaux(body.masscenter.vel(N)) + omega = zero_uaux(body.frame.ang_vel_in(N)) + acc = zero_udot_uaux(body.masscenter.acc(N)) + inertial_force = (M.diff(t) * vel + M * acc) + inertial_torque = zero_uaux((I.dt(body.frame).dot(omega)) + + msubs(I.dot(body.frame.ang_acc_in(N)), udot_zero) + + (omega.cross(I.dot(omega)))) + for j in range(o): + tmp_vel = zero_uaux(partials[i][0][j]) + tmp_ang = zero_uaux(I.dot(partials[i][1][j])) + for k in range(o): + # translational + MM[j, k] += M*tmp_vel.dot(partials[i][0][k]) + # rotational + MM[j, k] += tmp_ang.dot(partials[i][1][k]) + nonMM[j] += inertial_force.dot(partials[i][0][j]) + nonMM[j] += inertial_torque.dot(partials[i][1][j]) + else: + M = zero_uaux(body.mass) + vel = zero_uaux(body.point.vel(N)) + acc = zero_udot_uaux(body.point.acc(N)) + inertial_force = (M.diff(t) * vel + M * acc) + for j in range(o): + temp = zero_uaux(partials[i][0][j]) + for k in range(o): + MM[j, k] += M*temp.dot(partials[i][0][k]) + nonMM[j] += inertial_force.dot(partials[i][0][j]) + # Compose fr_star out of MM and nonMM + MM = zero_uaux(msubs(MM, q_ddot_u_map)) + nonMM = msubs(msubs(nonMM, q_ddot_u_map), + udot_zero, uauxdot_zero, uaux_zero) + fr_star = -(MM * msubs(Matrix(self._udot), uauxdot_zero) + nonMM) + + # If there are dependent speeds, we need to find fr_star_tilde + if self._udep: + p = o - len(self._udep) + fr_star_ind = fr_star[:p, 0] + fr_star_dep = fr_star[p:o, 0] + fr_star = fr_star_ind + (self._Ars.T * fr_star_dep) + # Apply the same to MM + MMi = MM[:p, :] + MMd = MM[p:o, :] + MM = MMi + (self._Ars.T * MMd) + # Apply the same to nonMM + nonMM = nonMM[:p, :] + (self._Ars.T * nonMM[p:o, :]) + + self._bodylist = bl + self._frstar = fr_star + self._k_d = MM + self._f_d = -(self._fr - nonMM) + return fr_star + + def to_linearizer(self, linear_solver='LU'): + """Returns an instance of the Linearizer class, initiated from the + data in the KanesMethod class. This may be more desirable than using + the linearize class method, as the Linearizer object will allow more + efficient recalculation (i.e. about varying operating points). + + Parameters + ========== + linear_solver : str, callable + Method used to solve the several symbolic linear systems of the + form ``A*x=b`` in the linearization process. If a string is + supplied, it should be a valid method that can be used with the + :meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is + supplied, it should have the format ``x = f(A, b)``, where it + solves the equations and returns the solution. The default is + ``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``. + ``LUsolve()`` is fast to compute but will often result in + divide-by-zero and thus ``nan`` results. + + Returns + ======= + Linearizer + An instantiated + :class:`sympy.physics.mechanics.linearize.Linearizer`. + + """ + + if (self._fr is None) or (self._frstar is None): + raise ValueError('Need to compute Fr, Fr* first.') + + # Get required equation components. The Kane's method class breaks + # these into pieces. Need to reassemble + f_c = self._f_h + if self._f_nh and self._k_nh: + f_v = self._f_nh + self._k_nh*Matrix(self.u) + else: + f_v = Matrix() + if self._f_dnh and self._k_dnh: + f_a = self._f_dnh + self._k_dnh*Matrix(self._udot) + else: + f_a = Matrix() + # Dicts to sub to zero, for splitting up expressions + u_zero = dict.fromkeys(self.u, 0) + ud_zero = dict.fromkeys(self._udot, 0) + qd_zero = dict.fromkeys(self._qdot, 0) + qd_u_zero = dict.fromkeys(Matrix([self._qdot, self.u]), 0) + # Break the kinematic differential eqs apart into f_0 and f_1 + f_0 = msubs(self._f_k, u_zero) + self._k_kqdot*Matrix(self._qdot) + f_1 = msubs(self._f_k, qd_zero) + self._k_ku*Matrix(self.u) + # Break the dynamic differential eqs into f_2 and f_3 + f_2 = msubs(self._frstar, qd_u_zero) + f_3 = msubs(self._frstar, ud_zero) + self._fr + f_4 = zeros(len(f_2), 1) + + # Get the required vector components + q = self.q + u = self.u + if self._qdep: + q_i = q[:-len(self._qdep)] + else: + q_i = q + q_d = self._qdep + if self._udep: + u_i = u[:-len(self._udep)] + else: + u_i = u + u_d = self._udep + + # Form dictionary to set auxiliary speeds & their derivatives to 0. + uaux = self._uaux + uauxdot = uaux.diff(dynamicsymbols._t) + uaux_zero = dict.fromkeys(Matrix([uaux, uauxdot]), 0) + + # Checking for dynamic symbols outside the dynamic differential + # equations; throws error if there is. + sym_list = set(Matrix([q, self._qdot, u, self._udot, uaux, uauxdot])) + if any(find_dynamicsymbols(i, sym_list) for i in [self._k_kqdot, + self._k_ku, self._f_k, self._k_dnh, self._f_dnh, self._k_d]): + raise ValueError('Cannot have dynamicsymbols outside dynamic \ + forcing vector.') + + # Find all other dynamic symbols, forming the forcing vector r. + # Sort r to make it canonical. + r = list(find_dynamicsymbols(msubs(self._f_d, uaux_zero), sym_list)) + r.sort(key=default_sort_key) + + # Check for any derivatives of variables in r that are also found in r. + for i in r: + if diff(i, dynamicsymbols._t) in r: + raise ValueError('Cannot have derivatives of specified \ + quantities when linearizing forcing terms.') + return Linearizer(f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a, q, u, q_i, + q_d, u_i, u_d, r, linear_solver=linear_solver) + + # TODO : Remove `new_method` after 1.1 has been released. + def linearize(self, *, new_method=None, linear_solver='LU', **kwargs): + """ Linearize the equations of motion about a symbolic operating point. + + Parameters + ========== + new_method + Deprecated, does nothing and will be removed. + linear_solver : str, callable + Method used to solve the several symbolic linear systems of the + form ``A*x=b`` in the linearization process. If a string is + supplied, it should be a valid method that can be used with the + :meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is + supplied, it should have the format ``x = f(A, b)``, where it + solves the equations and returns the solution. The default is + ``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``. + ``LUsolve()`` is fast to compute but will often result in + divide-by-zero and thus ``nan`` results. + **kwargs + Extra keyword arguments are passed to + :meth:`sympy.physics.mechanics.linearize.Linearizer.linearize`. + + Explanation + =========== + + If kwarg A_and_B is False (default), returns M, A, B, r for the + linearized form, M*[q', u']^T = A*[q_ind, u_ind]^T + B*r. + + If kwarg A_and_B is True, returns A, B, r for the linearized form + dx = A*x + B*r, where x = [q_ind, u_ind]^T. Note that this is + computationally intensive if there are many symbolic parameters. For + this reason, it may be more desirable to use the default A_and_B=False, + returning M, A, and B. Values may then be substituted in to these + matrices, and the state space form found as + A = P.T*M.inv()*A, B = P.T*M.inv()*B, where P = Linearizer.perm_mat. + + In both cases, r is found as all dynamicsymbols in the equations of + motion that are not part of q, u, q', or u'. They are sorted in + canonical form. + + The operating points may be also entered using the ``op_point`` kwarg. + This takes a dictionary of {symbol: value}, or a an iterable of such + dictionaries. The values may be numeric or symbolic. The more values + you can specify beforehand, the faster this computation will run. + + For more documentation, please see the ``Linearizer`` class. + + """ + + linearizer = self.to_linearizer(linear_solver=linear_solver) + result = linearizer.linearize(**kwargs) + return result + (linearizer.r,) + + def kanes_equations(self, bodies=None, loads=None): + """ Method to form Kane's equations, Fr + Fr* = 0. + + Explanation + =========== + + Returns (Fr, Fr*). In the case where auxiliary generalized speeds are + present (say, s auxiliary speeds, o generalized speeds, and m motion + constraints) the length of the returned vectors will be o - m + s in + length. The first o - m equations will be the constrained Kane's + equations, then the s auxiliary Kane's equations. These auxiliary + equations can be accessed with the auxiliary_eqs property. + + Parameters + ========== + + bodies : iterable + An iterable of all RigidBody's and Particle's in the system. + A system must have at least one body. + loads : iterable + Takes in an iterable of (Particle, Vector) or (ReferenceFrame, Vector) + tuples which represent the force at a point or torque on a frame. + Must be either a non-empty iterable of tuples or None which corresponds + to a system with no constraints. + """ + if bodies is None: + bodies = self.bodies + if loads is None and self._forcelist is not None: + loads = self._forcelist + if loads == []: + loads = None + if not self._k_kqdot: + raise AttributeError('Create an instance of KanesMethod with ' + 'kinematic differential equations to use this method.') + fr = self._form_fr(loads) + frstar = self._form_frstar(bodies) + if self._uaux: + if not self._udep: + km = KanesMethod(self._inertial, self.q, self._uaux, + u_auxiliary=self._uaux, constraint_solver=self._constraint_solver) + else: + km = KanesMethod(self._inertial, self.q, self._uaux, + u_auxiliary=self._uaux, u_dependent=self._udep, + velocity_constraints=(self._k_nh * self.u + + self._f_nh), + acceleration_constraints=(self._k_dnh * self._udot + + self._f_dnh), + constraint_solver=self._constraint_solver + ) + km._qdot_u_map = self._qdot_u_map + self._km = km + fraux = km._form_fr(loads) + frstaraux = km._form_frstar(bodies) + self._aux_eq = fraux + frstaraux + self._fr = fr.col_join(fraux) + self._frstar = frstar.col_join(frstaraux) + return (self._fr, self._frstar) + + def _form_eoms(self): + fr, frstar = self.kanes_equations(self.bodylist, self.forcelist) + return fr + frstar + + def rhs(self, inv_method=None): + """Returns the system's equations of motion in first order form. The + output is the right hand side of:: + + x' = |q'| =: f(q, u, r, p, t) + |u'| + + The right hand side is what is needed by most numerical ODE + integrators. + + Parameters + ========== + + inv_method : str + The specific sympy inverse matrix calculation method to use. For a + list of valid methods, see + :meth:`~sympy.matrices.matrixbase.MatrixBase.inv` + + """ + rhs = zeros(len(self.q) + len(self.u), 1) + kdes = self.kindiffdict() + for i, q_i in enumerate(self.q): + rhs[i] = kdes[q_i.diff()] + + if inv_method is None: + rhs[len(self.q):, 0] = self.mass_matrix.LUsolve(self.forcing) + else: + rhs[len(self.q):, 0] = (self.mass_matrix.inv(inv_method, + try_block_diag=True) * + self.forcing) + + return rhs + + def kindiffdict(self): + """Returns a dictionary mapping q' to u.""" + if not self._qdot_u_map: + raise AttributeError('Create an instance of KanesMethod with ' + 'kinematic differential equations to use this method.') + return self._qdot_u_map + + @property + def auxiliary_eqs(self): + """A matrix containing the auxiliary equations.""" + if not self._fr or not self._frstar: + raise ValueError('Need to compute Fr, Fr* first.') + if not self._uaux: + raise ValueError('No auxiliary speeds have been declared.') + return self._aux_eq + + @property + def mass_matrix_kin(self): + r"""The kinematic "mass matrix" $\mathbf{k_{k\dot{q}}}$ of the system.""" + return self._k_kqdot if self.explicit_kinematics else self._k_kqdot_implicit + + @property + def forcing_kin(self): + """The kinematic "forcing vector" of the system.""" + if self.explicit_kinematics: + return -(self._k_ku * Matrix(self.u) + self._f_k) + else: + return -(self._k_ku_implicit * Matrix(self.u) + self._f_k_implicit) + + @property + def mass_matrix(self): + """The mass matrix of the system.""" + if not self._fr or not self._frstar: + raise ValueError('Need to compute Fr, Fr* first.') + return Matrix([self._k_d, self._k_dnh]) + + @property + def forcing(self): + """The forcing vector of the system.""" + if not self._fr or not self._frstar: + raise ValueError('Need to compute Fr, Fr* first.') + return -Matrix([self._f_d, self._f_dnh]) + + @property + def mass_matrix_full(self): + """The mass matrix of the system, augmented by the kinematic + differential equations in explicit or implicit form.""" + if not self._fr or not self._frstar: + raise ValueError('Need to compute Fr, Fr* first.') + o, n = len(self.u), len(self.q) + return (self.mass_matrix_kin.row_join(zeros(n, o))).col_join( + zeros(o, n).row_join(self.mass_matrix)) + + @property + def forcing_full(self): + """The forcing vector of the system, augmented by the kinematic + differential equations in explicit or implicit form.""" + return Matrix([self.forcing_kin, self.forcing]) + + @property + def q(self): + return self._q + + @property + def u(self): + return self._u + + @property + def bodylist(self): + return self._bodylist + + @property + def forcelist(self): + return self._forcelist + + @property + def bodies(self): + return self._bodylist + + @property + def loads(self): + return self._forcelist diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/lagrange.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/lagrange.py new file mode 100644 index 0000000000000000000000000000000000000000..282176a404f77762abc3ee8c6a575519b2de1f02 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/lagrange.py @@ -0,0 +1,512 @@ +from sympy import diff, zeros, Matrix, eye, sympify +from sympy.core.sorting import default_sort_key +from sympy.physics.vector import dynamicsymbols, ReferenceFrame +from sympy.physics.mechanics.method import _Methods +from sympy.physics.mechanics.functions import ( + find_dynamicsymbols, msubs, _f_list_parser, _validate_coordinates) +from sympy.physics.mechanics.linearize import Linearizer +from sympy.utilities.iterables import iterable + +__all__ = ['LagrangesMethod'] + + +class LagrangesMethod(_Methods): + """Lagrange's method object. + + Explanation + =========== + + This object generates the equations of motion in a two step procedure. The + first step involves the initialization of LagrangesMethod by supplying the + Lagrangian and the generalized coordinates, at the bare minimum. If there + are any constraint equations, they can be supplied as keyword arguments. + The Lagrange multipliers are automatically generated and are equal in + number to the constraint equations. Similarly any non-conservative forces + can be supplied in an iterable (as described below and also shown in the + example) along with a ReferenceFrame. This is also discussed further in the + __init__ method. + + Attributes + ========== + + q, u : Matrix + Matrices of the generalized coordinates and speeds + loads : iterable + Iterable of (Point, vector) or (ReferenceFrame, vector) tuples + describing the forces on the system. + bodies : iterable + Iterable containing the rigid bodies and particles of the system. + mass_matrix : Matrix + The system's mass matrix + forcing : Matrix + The system's forcing vector + mass_matrix_full : Matrix + The "mass matrix" for the qdot's, qdoubledot's, and the + lagrange multipliers (lam) + forcing_full : Matrix + The forcing vector for the qdot's, qdoubledot's and + lagrange multipliers (lam) + + Examples + ======== + + This is a simple example for a one degree of freedom translational + spring-mass-damper. + + In this example, we first need to do the kinematics. + This involves creating generalized coordinates and their derivatives. + Then we create a point and set its velocity in a frame. + + >>> from sympy.physics.mechanics import LagrangesMethod, Lagrangian + >>> from sympy.physics.mechanics import ReferenceFrame, Particle, Point + >>> from sympy.physics.mechanics import dynamicsymbols + >>> from sympy import symbols + >>> q = dynamicsymbols('q') + >>> qd = dynamicsymbols('q', 1) + >>> m, k, b = symbols('m k b') + >>> N = ReferenceFrame('N') + >>> P = Point('P') + >>> P.set_vel(N, qd * N.x) + + We need to then prepare the information as required by LagrangesMethod to + generate equations of motion. + First we create the Particle, which has a point attached to it. + Following this the lagrangian is created from the kinetic and potential + energies. + Then, an iterable of nonconservative forces/torques must be constructed, + where each item is a (Point, Vector) or (ReferenceFrame, Vector) tuple, + with the Vectors representing the nonconservative forces or torques. + + >>> Pa = Particle('Pa', P, m) + >>> Pa.potential_energy = k * q**2 / 2.0 + >>> L = Lagrangian(N, Pa) + >>> fl = [(P, -b * qd * N.x)] + + Finally we can generate the equations of motion. + First we create the LagrangesMethod object. To do this one must supply + the Lagrangian, and the generalized coordinates. The constraint equations, + the forcelist, and the inertial frame may also be provided, if relevant. + Next we generate Lagrange's equations of motion, such that: + Lagrange's equations of motion = 0. + We have the equations of motion at this point. + + >>> l = LagrangesMethod(L, [q], forcelist = fl, frame = N) + >>> print(l.form_lagranges_equations()) + Matrix([[b*Derivative(q(t), t) + 1.0*k*q(t) + m*Derivative(q(t), (t, 2))]]) + + We can also solve for the states using the 'rhs' method. + + >>> print(l.rhs()) + Matrix([[Derivative(q(t), t)], [(-b*Derivative(q(t), t) - 1.0*k*q(t))/m]]) + + Please refer to the docstrings on each method for more details. + """ + + def __init__(self, Lagrangian, qs, forcelist=None, bodies=None, frame=None, + hol_coneqs=None, nonhol_coneqs=None): + """Supply the following for the initialization of LagrangesMethod. + + Lagrangian : Sympifyable + + qs : array_like + The generalized coordinates + + hol_coneqs : array_like, optional + The holonomic constraint equations + + nonhol_coneqs : array_like, optional + The nonholonomic constraint equations + + forcelist : iterable, optional + Takes an iterable of (Point, Vector) or (ReferenceFrame, Vector) + tuples which represent the force at a point or torque on a frame. + This feature is primarily to account for the nonconservative forces + and/or moments. + + bodies : iterable, optional + Takes an iterable containing the rigid bodies and particles of the + system. + + frame : ReferenceFrame, optional + Supply the inertial frame. This is used to determine the + generalized forces due to non-conservative forces. + """ + + self._L = Matrix([sympify(Lagrangian)]) + self.eom = None + self._m_cd = Matrix() # Mass Matrix of differentiated coneqs + self._m_d = Matrix() # Mass Matrix of dynamic equations + self._f_cd = Matrix() # Forcing part of the diff coneqs + self._f_d = Matrix() # Forcing part of the dynamic equations + self.lam_coeffs = Matrix() # The coeffecients of the multipliers + + forcelist = forcelist if forcelist else [] + if not iterable(forcelist): + raise TypeError('Force pairs must be supplied in an iterable.') + self._forcelist = forcelist + if frame and not isinstance(frame, ReferenceFrame): + raise TypeError('frame must be a valid ReferenceFrame') + self._bodies = bodies + self.inertial = frame + + self.lam_vec = Matrix() + + self._term1 = Matrix() + self._term2 = Matrix() + self._term3 = Matrix() + self._term4 = Matrix() + + # Creating the qs, qdots and qdoubledots + if not iterable(qs): + raise TypeError('Generalized coordinates must be an iterable') + self._q = Matrix(qs) + self._qdots = self.q.diff(dynamicsymbols._t) + self._qdoubledots = self._qdots.diff(dynamicsymbols._t) + _validate_coordinates(self.q) + + mat_build = lambda x: Matrix(x) if x else Matrix() + hol_coneqs = mat_build(hol_coneqs) + nonhol_coneqs = mat_build(nonhol_coneqs) + self.coneqs = Matrix([hol_coneqs.diff(dynamicsymbols._t), + nonhol_coneqs]) + self._hol_coneqs = hol_coneqs + + def form_lagranges_equations(self): + """Method to form Lagrange's equations of motion. + + Returns a vector of equations of motion using Lagrange's equations of + the second kind. + """ + + qds = self._qdots + qdd_zero = dict.fromkeys(self._qdoubledots, 0) + n = len(self.q) + + # Internally we represent the EOM as four terms: + # EOM = term1 - term2 - term3 - term4 = 0 + + # First term + self._term1 = self._L.jacobian(qds) + self._term1 = self._term1.diff(dynamicsymbols._t).T + + # Second term + self._term2 = self._L.jacobian(self.q).T + + # Third term + if self.coneqs: + coneqs = self.coneqs + m = len(coneqs) + # Creating the multipliers + self.lam_vec = Matrix(dynamicsymbols('lam1:' + str(m + 1))) + self.lam_coeffs = -coneqs.jacobian(qds) + self._term3 = self.lam_coeffs.T * self.lam_vec + # Extracting the coeffecients of the qdds from the diff coneqs + diffconeqs = coneqs.diff(dynamicsymbols._t) + self._m_cd = diffconeqs.jacobian(self._qdoubledots) + # The remaining terms i.e. the 'forcing' terms in diff coneqs + self._f_cd = -diffconeqs.subs(qdd_zero) + else: + self._term3 = zeros(n, 1) + + # Fourth term + if self.forcelist: + N = self.inertial + self._term4 = zeros(n, 1) + for i, qd in enumerate(qds): + flist = zip(*_f_list_parser(self.forcelist, N)) + self._term4[i] = sum(v.diff(qd, N).dot(f) for (v, f) in flist) + else: + self._term4 = zeros(n, 1) + + # Form the dynamic mass and forcing matrices + without_lam = self._term1 - self._term2 - self._term4 + self._m_d = without_lam.jacobian(self._qdoubledots) + self._f_d = -without_lam.subs(qdd_zero) + + # Form the EOM + self.eom = without_lam - self._term3 + return self.eom + + def _form_eoms(self): + return self.form_lagranges_equations() + + @property + def mass_matrix(self): + """Returns the mass matrix, which is augmented by the Lagrange + multipliers, if necessary. + + Explanation + =========== + + If the system is described by 'n' generalized coordinates and there are + no constraint equations then an n X n matrix is returned. + + If there are 'n' generalized coordinates and 'm' constraint equations + have been supplied during initialization then an n X (n+m) matrix is + returned. The (n + m - 1)th and (n + m)th columns contain the + coefficients of the Lagrange multipliers. + """ + + if self.eom is None: + raise ValueError('Need to compute the equations of motion first') + if self.coneqs: + return (self._m_d).row_join(self.lam_coeffs.T) + else: + return self._m_d + + @property + def mass_matrix_full(self): + """Augments the coefficients of qdots to the mass_matrix.""" + + if self.eom is None: + raise ValueError('Need to compute the equations of motion first') + n = len(self.q) + m = len(self.coneqs) + row1 = eye(n).row_join(zeros(n, n + m)) + row2 = zeros(n, n).row_join(self.mass_matrix) + if self.coneqs: + row3 = zeros(m, n).row_join(self._m_cd).row_join(zeros(m, m)) + return row1.col_join(row2).col_join(row3) + else: + return row1.col_join(row2) + + @property + def forcing(self): + """Returns the forcing vector from 'lagranges_equations' method.""" + + if self.eom is None: + raise ValueError('Need to compute the equations of motion first') + return self._f_d + + @property + def forcing_full(self): + """Augments qdots to the forcing vector above.""" + + if self.eom is None: + raise ValueError('Need to compute the equations of motion first') + if self.coneqs: + return self._qdots.col_join(self.forcing).col_join(self._f_cd) + else: + return self._qdots.col_join(self.forcing) + + def to_linearizer(self, q_ind=None, qd_ind=None, q_dep=None, qd_dep=None, + linear_solver='LU'): + """Returns an instance of the Linearizer class, initiated from the data + in the LagrangesMethod class. This may be more desirable than using the + linearize class method, as the Linearizer object will allow more + efficient recalculation (i.e. about varying operating points). + + Parameters + ========== + + q_ind, qd_ind : array_like, optional + The independent generalized coordinates and speeds. + q_dep, qd_dep : array_like, optional + The dependent generalized coordinates and speeds. + linear_solver : str, callable + Method used to solve the several symbolic linear systems of the + form ``A*x=b`` in the linearization process. If a string is + supplied, it should be a valid method that can be used with the + :meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is + supplied, it should have the format ``x = f(A, b)``, where it + solves the equations and returns the solution. The default is + ``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``. + ``LUsolve()`` is fast to compute but will often result in + divide-by-zero and thus ``nan`` results. + + Returns + ======= + Linearizer + An instantiated + :class:`sympy.physics.mechanics.linearize.Linearizer`. + + """ + + # Compose vectors + t = dynamicsymbols._t + q = self.q + u = self._qdots + ud = u.diff(t) + # Get vector of lagrange multipliers + lams = self.lam_vec + + mat_build = lambda x: Matrix(x) if x else Matrix() + q_i = mat_build(q_ind) + q_d = mat_build(q_dep) + u_i = mat_build(qd_ind) + u_d = mat_build(qd_dep) + + # Compose general form equations + f_c = self._hol_coneqs + f_v = self.coneqs + f_a = f_v.diff(t) + f_0 = u + f_1 = -u + f_2 = self._term1 + f_3 = -(self._term2 + self._term4) + f_4 = -self._term3 + + # Check that there are an appropriate number of independent and + # dependent coordinates + if len(q_d) != len(f_c) or len(u_d) != len(f_v): + raise ValueError(("Must supply {:} dependent coordinates, and " + + "{:} dependent speeds").format(len(f_c), len(f_v))) + if set(Matrix([q_i, q_d])) != set(q): + raise ValueError("Must partition q into q_ind and q_dep, with " + + "no extra or missing symbols.") + if set(Matrix([u_i, u_d])) != set(u): + raise ValueError("Must partition qd into qd_ind and qd_dep, " + + "with no extra or missing symbols.") + + # Find all other dynamic symbols, forming the forcing vector r. + # Sort r to make it canonical. + insyms = set(Matrix([q, u, ud, lams])) + r = list(find_dynamicsymbols(f_3, insyms)) + r.sort(key=default_sort_key) + # Check for any derivatives of variables in r that are also found in r. + for i in r: + if diff(i, dynamicsymbols._t) in r: + raise ValueError('Cannot have derivatives of specified \ + quantities when linearizing forcing terms.') + + return Linearizer(f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a, q, u, q_i, + q_d, u_i, u_d, r, lams, linear_solver=linear_solver) + + def linearize(self, q_ind=None, qd_ind=None, q_dep=None, qd_dep=None, + linear_solver='LU', **kwargs): + """Linearize the equations of motion about a symbolic operating point. + + Parameters + ========== + linear_solver : str, callable + Method used to solve the several symbolic linear systems of the + form ``A*x=b`` in the linearization process. If a string is + supplied, it should be a valid method that can be used with the + :meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is + supplied, it should have the format ``x = f(A, b)``, where it + solves the equations and returns the solution. The default is + ``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``. + ``LUsolve()`` is fast to compute but will often result in + divide-by-zero and thus ``nan`` results. + **kwargs + Extra keyword arguments are passed to + :meth:`sympy.physics.mechanics.linearize.Linearizer.linearize`. + + Explanation + =========== + + If kwarg A_and_B is False (default), returns M, A, B, r for the + linearized form, M*[q', u']^T = A*[q_ind, u_ind]^T + B*r. + + If kwarg A_and_B is True, returns A, B, r for the linearized form + dx = A*x + B*r, where x = [q_ind, u_ind]^T. Note that this is + computationally intensive if there are many symbolic parameters. For + this reason, it may be more desirable to use the default A_and_B=False, + returning M, A, and B. Values may then be substituted in to these + matrices, and the state space form found as + A = P.T*M.inv()*A, B = P.T*M.inv()*B, where P = Linearizer.perm_mat. + + In both cases, r is found as all dynamicsymbols in the equations of + motion that are not part of q, u, q', or u'. They are sorted in + canonical form. + + The operating points may be also entered using the ``op_point`` kwarg. + This takes a dictionary of {symbol: value}, or a an iterable of such + dictionaries. The values may be numeric or symbolic. The more values + you can specify beforehand, the faster this computation will run. + + For more documentation, please see the ``Linearizer`` class.""" + + linearizer = self.to_linearizer(q_ind, qd_ind, q_dep, qd_dep, + linear_solver=linear_solver) + result = linearizer.linearize(**kwargs) + return result + (linearizer.r,) + + def solve_multipliers(self, op_point=None, sol_type='dict'): + """Solves for the values of the lagrange multipliers symbolically at + the specified operating point. + + Parameters + ========== + + op_point : dict or iterable of dicts, optional + Point at which to solve at. The operating point is specified as + a dictionary or iterable of dictionaries of {symbol: value}. The + value may be numeric or symbolic itself. + + sol_type : str, optional + Solution return type. Valid options are: + - 'dict': A dict of {symbol : value} (default) + - 'Matrix': An ordered column matrix of the solution + """ + + # Determine number of multipliers + k = len(self.lam_vec) + if k == 0: + raise ValueError("System has no lagrange multipliers to solve for.") + # Compose dict of operating conditions + if isinstance(op_point, dict): + op_point_dict = op_point + elif iterable(op_point): + op_point_dict = {} + for op in op_point: + op_point_dict.update(op) + elif op_point is None: + op_point_dict = {} + else: + raise TypeError("op_point must be either a dictionary or an " + "iterable of dictionaries.") + # Compose the system to be solved + mass_matrix = self.mass_matrix.col_join(-self.lam_coeffs.row_join( + zeros(k, k))) + force_matrix = self.forcing.col_join(self._f_cd) + # Sub in the operating point + mass_matrix = msubs(mass_matrix, op_point_dict) + force_matrix = msubs(force_matrix, op_point_dict) + # Solve for the multipliers + sol_list = mass_matrix.LUsolve(-force_matrix)[-k:] + if sol_type == 'dict': + return dict(zip(self.lam_vec, sol_list)) + elif sol_type == 'Matrix': + return Matrix(sol_list) + else: + raise ValueError("Unknown sol_type {:}.".format(sol_type)) + + def rhs(self, inv_method=None, **kwargs): + """Returns equations that can be solved numerically. + + Parameters + ========== + + inv_method : str + The specific sympy inverse matrix calculation method to use. For a + list of valid methods, see + :meth:`~sympy.matrices.matrixbase.MatrixBase.inv` + """ + + if inv_method is None: + self._rhs = self.mass_matrix_full.LUsolve(self.forcing_full) + else: + self._rhs = (self.mass_matrix_full.inv(inv_method, + try_block_diag=True) * self.forcing_full) + return self._rhs + + @property + def q(self): + return self._q + + @property + def u(self): + return self._qdots + + @property + def bodies(self): + return self._bodies + + @property + def forcelist(self): + return self._forcelist + + @property + def loads(self): + return self._forcelist diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/linearize.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/linearize.py new file mode 100644 index 0000000000000000000000000000000000000000..b94ddb865a7236a5ac6f1a41ba96679eb8b2cd8f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/linearize.py @@ -0,0 +1,474 @@ +__all__ = ['Linearizer'] + +from sympy import Matrix, eye, zeros +from sympy.core.symbol import Dummy +from sympy.utilities.iterables import flatten +from sympy.physics.vector import dynamicsymbols +from sympy.physics.mechanics.functions import msubs, _parse_linear_solver + +from collections import namedtuple +from collections.abc import Iterable + + +class Linearizer: + """This object holds the general model form for a dynamic system. This + model is used for computing the linearized form of the system, while + properly dealing with constraints leading to dependent coordinates and + speeds. The notation and method is described in [1]_. + + Attributes + ========== + + f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a : Matrix + Matrices holding the general system form. + q, u, r : Matrix + Matrices holding the generalized coordinates, speeds, and + input vectors. + q_i, u_i : Matrix + Matrices of the independent generalized coordinates and speeds. + q_d, u_d : Matrix + Matrices of the dependent generalized coordinates and speeds. + perm_mat : Matrix + Permutation matrix such that [q_ind, u_ind]^T = perm_mat*[q, u]^T + + References + ========== + + .. [1] D. L. Peterson, G. Gede, and M. Hubbard, "Symbolic linearization of + equations of motion of constrained multibody systems," Multibody + Syst Dyn, vol. 33, no. 2, pp. 143-161, Feb. 2015, doi: + 10.1007/s11044-014-9436-5. + + """ + + def __init__(self, f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a, q, u, q_i=None, + q_d=None, u_i=None, u_d=None, r=None, lams=None, + linear_solver='LU'): + """ + Parameters + ========== + + f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a : array_like + System of equations holding the general system form. + Supply empty array or Matrix if the parameter + does not exist. + q : array_like + The generalized coordinates. + u : array_like + The generalized speeds + q_i, u_i : array_like, optional + The independent generalized coordinates and speeds. + q_d, u_d : array_like, optional + The dependent generalized coordinates and speeds. + r : array_like, optional + The input variables. + lams : array_like, optional + The lagrange multipliers + linear_solver : str, callable + Method used to solve the several symbolic linear systems of the + form ``A*x=b`` in the linearization process. If a string is + supplied, it should be a valid method that can be used with the + :meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is + supplied, it should have the format ``x = f(A, b)``, where it + solves the equations and returns the solution. The default is + ``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``. + ``LUsolve()`` is fast to compute but will often result in + divide-by-zero and thus ``nan`` results. + + """ + self.linear_solver = _parse_linear_solver(linear_solver) + + # Generalized equation form + self.f_0 = Matrix(f_0) + self.f_1 = Matrix(f_1) + self.f_2 = Matrix(f_2) + self.f_3 = Matrix(f_3) + self.f_4 = Matrix(f_4) + self.f_c = Matrix(f_c) + self.f_v = Matrix(f_v) + self.f_a = Matrix(f_a) + + # Generalized equation variables + self.q = Matrix(q) + self.u = Matrix(u) + none_handler = lambda x: Matrix(x) if x else Matrix() + self.q_i = none_handler(q_i) + self.q_d = none_handler(q_d) + self.u_i = none_handler(u_i) + self.u_d = none_handler(u_d) + self.r = none_handler(r) + self.lams = none_handler(lams) + + # Derivatives of generalized equation variables + self._qd = self.q.diff(dynamicsymbols._t) + self._ud = self.u.diff(dynamicsymbols._t) + # If the user doesn't actually use generalized variables, and the + # qd and u vectors have any intersecting variables, this can cause + # problems. We'll fix this with some hackery, and Dummy variables + dup_vars = set(self._qd).intersection(self.u) + self._qd_dup = Matrix([var if var not in dup_vars else Dummy() for var + in self._qd]) + + # Derive dimension terms + l = len(self.f_c) + m = len(self.f_v) + n = len(self.q) + o = len(self.u) + s = len(self.r) + k = len(self.lams) + dims = namedtuple('dims', ['l', 'm', 'n', 'o', 's', 'k']) + self._dims = dims(l, m, n, o, s, k) + + self._Pq = None + self._Pqi = None + self._Pqd = None + self._Pu = None + self._Pui = None + self._Pud = None + self._C_0 = None + self._C_1 = None + self._C_2 = None + self.perm_mat = None + + self._setup_done = False + + def _setup(self): + # Calculations here only need to be run once. They are moved out of + # the __init__ method to increase the speed of Linearizer creation. + self._form_permutation_matrices() + self._form_block_matrices() + self._form_coefficient_matrices() + self._setup_done = True + + def _form_permutation_matrices(self): + """Form the permutation matrices Pq and Pu.""" + + # Extract dimension variables + l, m, n, o, s, k = self._dims + # Compute permutation matrices + if n != 0: + self._Pq = permutation_matrix(self.q, Matrix([self.q_i, self.q_d])) + if l > 0: + self._Pqi = self._Pq[:, :-l] + self._Pqd = self._Pq[:, -l:] + else: + self._Pqi = self._Pq + self._Pqd = Matrix() + if o != 0: + self._Pu = permutation_matrix(self.u, Matrix([self.u_i, self.u_d])) + if m > 0: + self._Pui = self._Pu[:, :-m] + self._Pud = self._Pu[:, -m:] + else: + self._Pui = self._Pu + self._Pud = Matrix() + # Compute combination permutation matrix for computing A and B + P_col1 = Matrix([self._Pqi, zeros(o + k, n - l)]) + P_col2 = Matrix([zeros(n, o - m), self._Pui, zeros(k, o - m)]) + if P_col1: + if P_col2: + self.perm_mat = P_col1.row_join(P_col2) + else: + self.perm_mat = P_col1 + else: + self.perm_mat = P_col2 + + def _form_coefficient_matrices(self): + """Form the coefficient matrices C_0, C_1, and C_2.""" + + # Extract dimension variables + l, m, n, o, s, k = self._dims + # Build up the coefficient matrices C_0, C_1, and C_2 + # If there are configuration constraints (l > 0), form C_0 as normal. + # If not, C_0 is I_(nxn). Note that this works even if n=0 + if l > 0: + f_c_jac_q = self.f_c.jacobian(self.q) + self._C_0 = (eye(n) - self._Pqd * + self.linear_solver(f_c_jac_q*self._Pqd, + f_c_jac_q))*self._Pqi + else: + self._C_0 = eye(n) + # If there are motion constraints (m > 0), form C_1 and C_2 as normal. + # If not, C_1 is 0, and C_2 is I_(oxo). Note that this works even if + # o = 0. + if m > 0: + f_v_jac_u = self.f_v.jacobian(self.u) + temp = f_v_jac_u * self._Pud + if n != 0: + f_v_jac_q = self.f_v.jacobian(self.q) + self._C_1 = -self._Pud * self.linear_solver(temp, f_v_jac_q) + else: + self._C_1 = zeros(o, n) + self._C_2 = (eye(o) - self._Pud * + self.linear_solver(temp, f_v_jac_u))*self._Pui + else: + self._C_1 = zeros(o, n) + self._C_2 = eye(o) + + def _form_block_matrices(self): + """Form the block matrices for composing M, A, and B.""" + + # Extract dimension variables + l, m, n, o, s, k = self._dims + # Block Matrix Definitions. These are only defined if under certain + # conditions. If undefined, an empty matrix is used instead + if n != 0: + self._M_qq = self.f_0.jacobian(self._qd) + self._A_qq = -(self.f_0 + self.f_1).jacobian(self.q) + else: + self._M_qq = Matrix() + self._A_qq = Matrix() + if n != 0 and m != 0: + self._M_uqc = self.f_a.jacobian(self._qd_dup) + self._A_uqc = -self.f_a.jacobian(self.q) + else: + self._M_uqc = Matrix() + self._A_uqc = Matrix() + if n != 0 and o - m + k != 0: + self._M_uqd = self.f_3.jacobian(self._qd_dup) + self._A_uqd = -(self.f_2 + self.f_3 + self.f_4).jacobian(self.q) + else: + self._M_uqd = Matrix() + self._A_uqd = Matrix() + if o != 0 and m != 0: + self._M_uuc = self.f_a.jacobian(self._ud) + self._A_uuc = -self.f_a.jacobian(self.u) + else: + self._M_uuc = Matrix() + self._A_uuc = Matrix() + if o != 0 and o - m + k != 0: + self._M_uud = self.f_2.jacobian(self._ud) + self._A_uud = -(self.f_2 + self.f_3).jacobian(self.u) + else: + self._M_uud = Matrix() + self._A_uud = Matrix() + if o != 0 and n != 0: + self._A_qu = -self.f_1.jacobian(self.u) + else: + self._A_qu = Matrix() + if k != 0 and o - m + k != 0: + self._M_uld = self.f_4.jacobian(self.lams) + else: + self._M_uld = Matrix() + if s != 0 and o - m + k != 0: + self._B_u = -self.f_3.jacobian(self.r) + else: + self._B_u = Matrix() + + def linearize(self, op_point=None, A_and_B=False, simplify=False): + """Linearize the system about the operating point. Note that + q_op, u_op, qd_op, ud_op must satisfy the equations of motion. + These may be either symbolic or numeric. + + Parameters + ========== + op_point : dict or iterable of dicts, optional + Dictionary or iterable of dictionaries containing the operating + point conditions for all or a subset of the generalized + coordinates, generalized speeds, and time derivatives of the + generalized speeds. These will be substituted into the linearized + system before the linearization is complete. Leave set to ``None`` + if you want the operating point to be an arbitrary set of symbols. + Note that any reduction in symbols (whether substituted for numbers + or expressions with a common parameter) will result in faster + runtime. + A_and_B : bool, optional + If A_and_B=False (default), (M, A, B) is returned and of + A_and_B=True, (A, B) is returned. See below. + simplify : bool, optional + Determines if returned values are simplified before return. + For large expressions this may be time consuming. Default is False. + + Returns + ======= + M, A, B : Matrices, ``A_and_B=False`` + Matrices from the implicit form: + ``[M]*[q', u']^T = [A]*[q_ind, u_ind]^T + [B]*r`` + A, B : Matrices, ``A_and_B=True`` + Matrices from the explicit form: + ``[q_ind', u_ind']^T = [A]*[q_ind, u_ind]^T + [B]*r`` + + Notes + ===== + + Note that the process of solving with A_and_B=True is computationally + intensive if there are many symbolic parameters. For this reason, it + may be more desirable to use the default A_and_B=False, returning M, A, + and B. More values may then be substituted in to these matrices later + on. The state space form can then be found as A = P.T*M.LUsolve(A), B = + P.T*M.LUsolve(B), where P = Linearizer.perm_mat. + + """ + + # Run the setup if needed: + if not self._setup_done: + self._setup() + + # Compose dict of operating conditions + if isinstance(op_point, dict): + op_point_dict = op_point + elif isinstance(op_point, Iterable): + op_point_dict = {} + for op in op_point: + op_point_dict.update(op) + else: + op_point_dict = {} + + # Extract dimension variables + l, m, n, o, s, k = self._dims + + # Rename terms to shorten expressions + M_qq = self._M_qq + M_uqc = self._M_uqc + M_uqd = self._M_uqd + M_uuc = self._M_uuc + M_uud = self._M_uud + M_uld = self._M_uld + A_qq = self._A_qq + A_uqc = self._A_uqc + A_uqd = self._A_uqd + A_qu = self._A_qu + A_uuc = self._A_uuc + A_uud = self._A_uud + B_u = self._B_u + C_0 = self._C_0 + C_1 = self._C_1 + C_2 = self._C_2 + + # Build up Mass Matrix + # |M_qq 0_nxo 0_nxk| + # M = |M_uqc M_uuc 0_mxk| + # |M_uqd M_uud M_uld| + if o != 0: + col2 = Matrix([zeros(n, o), M_uuc, M_uud]) + if k != 0: + col3 = Matrix([zeros(n + m, k), M_uld]) + if n != 0: + col1 = Matrix([M_qq, M_uqc, M_uqd]) + if o != 0 and k != 0: + M = col1.row_join(col2).row_join(col3) + elif o != 0: + M = col1.row_join(col2) + else: + M = col1 + elif k != 0: + M = col2.row_join(col3) + else: + M = col2 + M_eq = msubs(M, op_point_dict) + + # Build up state coefficient matrix A + # |(A_qq + A_qu*C_1)*C_0 A_qu*C_2| + # A = |(A_uqc + A_uuc*C_1)*C_0 A_uuc*C_2| + # |(A_uqd + A_uud*C_1)*C_0 A_uud*C_2| + # Col 1 is only defined if n != 0 + if n != 0: + r1c1 = A_qq + if o != 0: + r1c1 += (A_qu * C_1) + r1c1 = r1c1 * C_0 + if m != 0: + r2c1 = A_uqc + if o != 0: + r2c1 += (A_uuc * C_1) + r2c1 = r2c1 * C_0 + else: + r2c1 = Matrix() + if o - m + k != 0: + r3c1 = A_uqd + if o != 0: + r3c1 += (A_uud * C_1) + r3c1 = r3c1 * C_0 + else: + r3c1 = Matrix() + col1 = Matrix([r1c1, r2c1, r3c1]) + else: + col1 = Matrix() + # Col 2 is only defined if o != 0 + if o != 0: + if n != 0: + r1c2 = A_qu * C_2 + else: + r1c2 = Matrix() + if m != 0: + r2c2 = A_uuc * C_2 + else: + r2c2 = Matrix() + if o - m + k != 0: + r3c2 = A_uud * C_2 + else: + r3c2 = Matrix() + col2 = Matrix([r1c2, r2c2, r3c2]) + else: + col2 = Matrix() + if col1: + if col2: + Amat = col1.row_join(col2) + else: + Amat = col1 + else: + Amat = col2 + Amat_eq = msubs(Amat, op_point_dict) + + # Build up the B matrix if there are forcing variables + # |0_(n + m)xs| + # B = |B_u | + if s != 0 and o - m + k != 0: + Bmat = zeros(n + m, s).col_join(B_u) + Bmat_eq = msubs(Bmat, op_point_dict) + else: + Bmat_eq = Matrix() + + # kwarg A_and_B indicates to return A, B for forming the equation + # dx = [A]x + [B]r, where x = [q_indnd, u_indnd]^T, + if A_and_B: + A_cont = self.perm_mat.T * self.linear_solver(M_eq, Amat_eq) + if Bmat_eq: + B_cont = self.perm_mat.T * self.linear_solver(M_eq, Bmat_eq) + else: + # Bmat = Matrix([]), so no need to sub + B_cont = Bmat_eq + if simplify: + A_cont.simplify() + B_cont.simplify() + return A_cont, B_cont + # Otherwise return M, A, B for forming the equation + # [M]dx = [A]x + [B]r, where x = [q, u]^T + else: + if simplify: + M_eq.simplify() + Amat_eq.simplify() + Bmat_eq.simplify() + return M_eq, Amat_eq, Bmat_eq + + +def permutation_matrix(orig_vec, per_vec): + """Compute the permutation matrix to change order of + orig_vec into order of per_vec. + + Parameters + ========== + + orig_vec : array_like + Symbols in original ordering. + per_vec : array_like + Symbols in new ordering. + + Returns + ======= + + p_matrix : Matrix + Permutation matrix such that orig_vec == (p_matrix * per_vec). + """ + if not isinstance(orig_vec, (list, tuple)): + orig_vec = flatten(orig_vec) + if not isinstance(per_vec, (list, tuple)): + per_vec = flatten(per_vec) + if set(orig_vec) != set(per_vec): + raise ValueError("orig_vec and per_vec must be the same length, " + "and contain the same symbols.") + ind_list = [orig_vec.index(i) for i in per_vec] + p_matrix = zeros(len(orig_vec)) + for i, j in enumerate(ind_list): + p_matrix[i, j] = 1 + return p_matrix diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/loads.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/loads.py new file mode 100644 index 0000000000000000000000000000000000000000..3b9db763ffd6f99905e9d17fdc07f4171de4801b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/loads.py @@ -0,0 +1,177 @@ +from abc import ABC +from collections import namedtuple +from sympy.physics.mechanics.body_base import BodyBase +from sympy.physics.vector import Vector, ReferenceFrame, Point + +__all__ = ['LoadBase', 'Force', 'Torque'] + + +class LoadBase(ABC, namedtuple('LoadBase', ['location', 'vector'])): + """Abstract base class for the various loading types.""" + + def __add__(self, other): + raise TypeError(f"unsupported operand type(s) for +: " + f"'{self.__class__.__name__}' and " + f"'{other.__class__.__name__}'") + + def __mul__(self, other): + raise TypeError(f"unsupported operand type(s) for *: " + f"'{self.__class__.__name__}' and " + f"'{other.__class__.__name__}'") + + __radd__ = __add__ + __rmul__ = __mul__ + + +class Force(LoadBase): + """Force acting upon a point. + + Explanation + =========== + + A force is a vector that is bound to a line of action. This class stores + both a point, which lies on the line of action, and the vector. A tuple can + also be used, with the location as the first entry and the vector as second + entry. + + Examples + ======== + + A force of magnitude 2 along N.x acting on a point Po can be created as + follows: + + >>> from sympy.physics.mechanics import Point, ReferenceFrame, Force + >>> N = ReferenceFrame('N') + >>> Po = Point('Po') + >>> Force(Po, 2 * N.x) + (Po, 2*N.x) + + If a body is supplied, then the center of mass of that body is used. + + >>> from sympy.physics.mechanics import Particle + >>> P = Particle('P', point=Po) + >>> Force(P, 2 * N.x) + (Po, 2*N.x) + + """ + + def __new__(cls, point, force): + if isinstance(point, BodyBase): + point = point.masscenter + if not isinstance(point, Point): + raise TypeError('Force location should be a Point.') + if not isinstance(force, Vector): + raise TypeError('Force vector should be a Vector.') + return super().__new__(cls, point, force) + + def __repr__(self): + return (f'{self.__class__.__name__}(point={self.point}, ' + f'force={self.force})') + + @property + def point(self): + return self.location + + @property + def force(self): + return self.vector + + +class Torque(LoadBase): + """Torque acting upon a frame. + + Explanation + =========== + + A torque is a free vector that is acting on a reference frame, which is + associated with a rigid body. This class stores both the frame and the + vector. A tuple can also be used, with the location as the first item and + the vector as second item. + + Examples + ======== + + A torque of magnitude 2 about N.x acting on a frame N can be created as + follows: + + >>> from sympy.physics.mechanics import ReferenceFrame, Torque + >>> N = ReferenceFrame('N') + >>> Torque(N, 2 * N.x) + (N, 2*N.x) + + If a body is supplied, then the frame fixed to that body is used. + + >>> from sympy.physics.mechanics import RigidBody + >>> rb = RigidBody('rb', frame=N) + >>> Torque(rb, 2 * N.x) + (N, 2*N.x) + + """ + + def __new__(cls, frame, torque): + if isinstance(frame, BodyBase): + frame = frame.frame + if not isinstance(frame, ReferenceFrame): + raise TypeError('Torque location should be a ReferenceFrame.') + if not isinstance(torque, Vector): + raise TypeError('Torque vector should be a Vector.') + return super().__new__(cls, frame, torque) + + def __repr__(self): + return (f'{self.__class__.__name__}(frame={self.frame}, ' + f'torque={self.torque})') + + @property + def frame(self): + return self.location + + @property + def torque(self): + return self.vector + + +def gravity(acceleration, *bodies): + """ + Returns a list of gravity forces given the acceleration + due to gravity and any number of particles or rigidbodies. + + Example + ======= + + >>> from sympy.physics.mechanics import ReferenceFrame, Particle, RigidBody + >>> from sympy.physics.mechanics.loads import gravity + >>> from sympy import symbols + >>> N = ReferenceFrame('N') + >>> g = symbols('g') + >>> P = Particle('P') + >>> B = RigidBody('B') + >>> gravity(g*N.y, P, B) + [(P_masscenter, P_mass*g*N.y), + (B_masscenter, B_mass*g*N.y)] + + """ + + gravity_force = [] + for body in bodies: + if not isinstance(body, BodyBase): + raise TypeError(f'{type(body)} is not a body type') + gravity_force.append(Force(body.masscenter, body.mass * acceleration)) + return gravity_force + + +def _parse_load(load): + """Helper function to parse loads and convert tuples to load objects.""" + if isinstance(load, LoadBase): + return load + elif isinstance(load, tuple): + if len(load) != 2: + raise ValueError(f'Load {load} should have a length of 2.') + if isinstance(load[0], Point): + return Force(load[0], load[1]) + elif isinstance(load[0], ReferenceFrame): + return Torque(load[0], load[1]) + else: + raise ValueError(f'Load not recognized. The load location {load[0]}' + f' should either be a Point or a ReferenceFrame.') + raise TypeError(f'Load type {type(load)} not recognized as a load. It ' + f'should be a Force, Torque or tuple.') diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/method.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/method.py new file mode 100644 index 0000000000000000000000000000000000000000..5c2c4a5f388e56e37bd9ecdf6daffc08ffa51070 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/method.py @@ -0,0 +1,39 @@ +from abc import ABC, abstractmethod + +class _Methods(ABC): + """Abstract Base Class for all methods.""" + + @abstractmethod + def q(self): + pass + + @abstractmethod + def u(self): + pass + + @abstractmethod + def bodies(self): + pass + + @abstractmethod + def loads(self): + pass + + @abstractmethod + def mass_matrix(self): + pass + + @abstractmethod + def forcing(self): + pass + + @abstractmethod + def mass_matrix_full(self): + pass + + @abstractmethod + def forcing_full(self): + pass + + def _form_eoms(self): + raise NotImplementedError("Subclasses must implement this.") diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/models.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/models.py new file mode 100644 index 0000000000000000000000000000000000000000..a89b929ffd540a07787f6f94714850b348c90781 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/models.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python +"""This module contains some sample symbolic models used for testing and +examples.""" + +# Internal imports +from sympy.core import backend as sm +import sympy.physics.mechanics as me + + +def multi_mass_spring_damper(n=1, apply_gravity=False, + apply_external_forces=False): + r"""Returns a system containing the symbolic equations of motion and + associated variables for a simple multi-degree of freedom point mass, + spring, damper system with optional gravitational and external + specified forces. For example, a two mass system under the influence of + gravity and external forces looks like: + + :: + + ---------------- + | | | | g + \ | | | V + k0 / --- c0 | + | | | x0, v0 + --------- V + | m0 | ----- + --------- | + | | | | + \ v | | | + k1 / f0 --- c1 | + | | | x1, v1 + --------- V + | m1 | ----- + --------- + | f1 + V + + Parameters + ========== + + n : integer + The number of masses in the serial chain. + apply_gravity : boolean + If true, gravity will be applied to each mass. + apply_external_forces : boolean + If true, a time varying external force will be applied to each mass. + + Returns + ======= + + kane : sympy.physics.mechanics.kane.KanesMethod + A KanesMethod object. + + """ + + mass = sm.symbols('m:{}'.format(n)) + stiffness = sm.symbols('k:{}'.format(n)) + damping = sm.symbols('c:{}'.format(n)) + + acceleration_due_to_gravity = sm.symbols('g') + + coordinates = me.dynamicsymbols('x:{}'.format(n)) + speeds = me.dynamicsymbols('v:{}'.format(n)) + specifieds = me.dynamicsymbols('f:{}'.format(n)) + + ceiling = me.ReferenceFrame('N') + origin = me.Point('origin') + origin.set_vel(ceiling, 0) + + points = [origin] + kinematic_equations = [] + particles = [] + forces = [] + + for i in range(n): + + center = points[-1].locatenew('center{}'.format(i), + coordinates[i] * ceiling.x) + center.set_vel(ceiling, points[-1].vel(ceiling) + + speeds[i] * ceiling.x) + points.append(center) + + block = me.Particle('block{}'.format(i), center, mass[i]) + + kinematic_equations.append(speeds[i] - coordinates[i].diff()) + + total_force = (-stiffness[i] * coordinates[i] - + damping[i] * speeds[i]) + try: + total_force += (stiffness[i + 1] * coordinates[i + 1] + + damping[i + 1] * speeds[i + 1]) + except IndexError: # no force from below on last mass + pass + + if apply_gravity: + total_force += mass[i] * acceleration_due_to_gravity + + if apply_external_forces: + total_force += specifieds[i] + + forces.append((center, total_force * ceiling.x)) + + particles.append(block) + + kane = me.KanesMethod(ceiling, q_ind=coordinates, u_ind=speeds, + kd_eqs=kinematic_equations) + kane.kanes_equations(particles, forces) + + return kane + + +def n_link_pendulum_on_cart(n=1, cart_force=True, joint_torques=False): + r"""Returns the system containing the symbolic first order equations of + motion for a 2D n-link pendulum on a sliding cart under the influence of + gravity. + + :: + + | + o y v + \ 0 ^ g + \ | + --\-|---- + | \| | + F-> | o --|---> x + | | + --------- + o o + + Parameters + ========== + + n : integer + The number of links in the pendulum. + cart_force : boolean, default=True + If true an external specified lateral force is applied to the cart. + joint_torques : boolean, default=False + If true joint torques will be added as specified inputs at each + joint. + + Returns + ======= + + kane : sympy.physics.mechanics.kane.KanesMethod + A KanesMethod object. + + Notes + ===== + + The degrees of freedom of the system are n + 1, i.e. one for each + pendulum link and one for the lateral motion of the cart. + + M x' = F, where x = [u0, ..., un+1, q0, ..., qn+1] + + The joint angles are all defined relative to the ground where the x axis + defines the ground line and the y axis points up. The joint torques are + applied between each adjacent link and the between the cart and the + lower link where a positive torque corresponds to positive angle. + + """ + if n <= 0: + raise ValueError('The number of links must be a positive integer.') + + q = me.dynamicsymbols('q:{}'.format(n + 1)) + u = me.dynamicsymbols('u:{}'.format(n + 1)) + + if joint_torques is True: + T = me.dynamicsymbols('T1:{}'.format(n + 1)) + + m = sm.symbols('m:{}'.format(n + 1)) + l = sm.symbols('l:{}'.format(n)) + g, t = sm.symbols('g t') + + I = me.ReferenceFrame('I') + O = me.Point('O') + O.set_vel(I, 0) + + P0 = me.Point('P0') + P0.set_pos(O, q[0] * I.x) + P0.set_vel(I, u[0] * I.x) + Pa0 = me.Particle('Pa0', P0, m[0]) + + frames = [I] + points = [P0] + particles = [Pa0] + forces = [(P0, -m[0] * g * I.y)] + kindiffs = [q[0].diff(t) - u[0]] + + if cart_force is True or joint_torques is True: + specified = [] + else: + specified = None + + for i in range(n): + Bi = I.orientnew('B{}'.format(i), 'Axis', [q[i + 1], I.z]) + Bi.set_ang_vel(I, u[i + 1] * I.z) + frames.append(Bi) + + Pi = points[-1].locatenew('P{}'.format(i + 1), l[i] * Bi.y) + Pi.v2pt_theory(points[-1], I, Bi) + points.append(Pi) + + Pai = me.Particle('Pa' + str(i + 1), Pi, m[i + 1]) + particles.append(Pai) + + forces.append((Pi, -m[i + 1] * g * I.y)) + + if joint_torques is True: + + specified.append(T[i]) + + if i == 0: + forces.append((I, -T[i] * I.z)) + + if i == n - 1: + forces.append((Bi, T[i] * I.z)) + else: + forces.append((Bi, T[i] * I.z - T[i + 1] * I.z)) + + kindiffs.append(q[i + 1].diff(t) - u[i + 1]) + + if cart_force is True: + F = me.dynamicsymbols('F') + forces.append((P0, F * I.x)) + specified.append(F) + + kane = me.KanesMethod(I, q_ind=q, u_ind=u, kd_eqs=kindiffs) + kane.kanes_equations(particles, forces) + + return kane diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/particle.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/particle.py new file mode 100644 index 0000000000000000000000000000000000000000..5d49d4f811b8d1c7fff16c71991f5e01da6ded02 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/particle.py @@ -0,0 +1,209 @@ +from sympy import S +from sympy.physics.vector import cross, dot +from sympy.physics.mechanics.body_base import BodyBase +from sympy.physics.mechanics.inertia import inertia_of_point_mass +from sympy.utilities.exceptions import sympy_deprecation_warning + +__all__ = ['Particle'] + + +class Particle(BodyBase): + """A particle. + + Explanation + =========== + + Particles have a non-zero mass and lack spatial extension; they take up no + space. + + Values need to be supplied on initialization, but can be changed later. + + Parameters + ========== + + name : str + Name of particle + point : Point + A physics/mechanics Point which represents the position, velocity, and + acceleration of this Particle + mass : Sympifyable + A SymPy expression representing the Particle's mass + potential_energy : Sympifyable + The potential energy of the Particle. + + Examples + ======== + + >>> from sympy.physics.mechanics import Particle, Point + >>> from sympy import Symbol + >>> po = Point('po') + >>> m = Symbol('m') + >>> pa = Particle('pa', po, m) + >>> # Or you could change these later + >>> pa.mass = m + >>> pa.point = po + + """ + point = BodyBase.masscenter + + def __init__(self, name, point=None, mass=None): + super().__init__(name, point, mass) + + def linear_momentum(self, frame): + """Linear momentum of the particle. + + Explanation + =========== + + The linear momentum L, of a particle P, with respect to frame N is + given by: + + L = m * v + + where m is the mass of the particle, and v is the velocity of the + particle in the frame N. + + Parameters + ========== + + frame : ReferenceFrame + The frame in which linear momentum is desired. + + Examples + ======== + + >>> from sympy.physics.mechanics import Particle, Point, ReferenceFrame + >>> from sympy.physics.mechanics import dynamicsymbols + >>> from sympy.physics.vector import init_vprinting + >>> init_vprinting(pretty_print=False) + >>> m, v = dynamicsymbols('m v') + >>> N = ReferenceFrame('N') + >>> P = Point('P') + >>> A = Particle('A', P, m) + >>> P.set_vel(N, v * N.x) + >>> A.linear_momentum(N) + m*v*N.x + + """ + + return self.mass * self.point.vel(frame) + + def angular_momentum(self, point, frame): + """Angular momentum of the particle about the point. + + Explanation + =========== + + The angular momentum H, about some point O of a particle, P, is given + by: + + ``H = cross(r, m * v)`` + + where r is the position vector from point O to the particle P, m is + the mass of the particle, and v is the velocity of the particle in + the inertial frame, N. + + Parameters + ========== + + point : Point + The point about which angular momentum of the particle is desired. + + frame : ReferenceFrame + The frame in which angular momentum is desired. + + Examples + ======== + + >>> from sympy.physics.mechanics import Particle, Point, ReferenceFrame + >>> from sympy.physics.mechanics import dynamicsymbols + >>> from sympy.physics.vector import init_vprinting + >>> init_vprinting(pretty_print=False) + >>> m, v, r = dynamicsymbols('m v r') + >>> N = ReferenceFrame('N') + >>> O = Point('O') + >>> A = O.locatenew('A', r * N.x) + >>> P = Particle('P', A, m) + >>> P.point.set_vel(N, v * N.y) + >>> P.angular_momentum(O, N) + m*r*v*N.z + + """ + + return cross(self.point.pos_from(point), + self.mass * self.point.vel(frame)) + + def kinetic_energy(self, frame): + """Kinetic energy of the particle. + + Explanation + =========== + + The kinetic energy, T, of a particle, P, is given by: + + ``T = 1/2 (dot(m * v, v))`` + + where m is the mass of particle P, and v is the velocity of the + particle in the supplied ReferenceFrame. + + Parameters + ========== + + frame : ReferenceFrame + The Particle's velocity is typically defined with respect to + an inertial frame but any relevant frame in which the velocity is + known can be supplied. + + Examples + ======== + + >>> from sympy.physics.mechanics import Particle, Point, ReferenceFrame + >>> from sympy import symbols + >>> m, v, r = symbols('m v r') + >>> N = ReferenceFrame('N') + >>> O = Point('O') + >>> P = Particle('P', O, m) + >>> P.point.set_vel(N, v * N.y) + >>> P.kinetic_energy(N) + m*v**2/2 + + """ + + return S.Half * self.mass * dot(self.point.vel(frame), + self.point.vel(frame)) + + def set_potential_energy(self, scalar): + sympy_deprecation_warning( + """ +The sympy.physics.mechanics.Particle.set_potential_energy() +method is deprecated. Instead use + + P.potential_energy = scalar + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-set-potential-energy", + ) + self.potential_energy = scalar + + def parallel_axis(self, point, frame): + """Returns an inertia dyadic of the particle with respect to another + point and frame. + + Parameters + ========== + + point : sympy.physics.vector.Point + The point to express the inertia dyadic about. + frame : sympy.physics.vector.ReferenceFrame + The reference frame used to construct the dyadic. + + Returns + ======= + + inertia : sympy.physics.vector.Dyadic + The inertia dyadic of the particle expressed about the provided + point and frame. + + """ + return inertia_of_point_mass(self.mass, self.point.pos_from(point), + frame) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/pathway.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/pathway.py new file mode 100644 index 0000000000000000000000000000000000000000..b86ba85b1d9d1434c51de3fd7cc429442fdbedb0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/pathway.py @@ -0,0 +1,688 @@ +"""Implementations of pathways for use by actuators.""" + +from abc import ABC, abstractmethod + +from sympy.core.singleton import S +from sympy.physics.mechanics.loads import Force +from sympy.physics.mechanics.wrapping_geometry import WrappingGeometryBase +from sympy.physics.vector import Point, dynamicsymbols + + +__all__ = ['PathwayBase', 'LinearPathway', 'ObstacleSetPathway', + 'WrappingPathway'] + + +class PathwayBase(ABC): + """Abstract base class for all pathway classes to inherit from. + + Notes + ===== + + Instances of this class cannot be directly instantiated by users. However, + it can be used to created custom pathway types through subclassing. + + """ + + def __init__(self, *attachments): + """Initializer for ``PathwayBase``.""" + self.attachments = attachments + + @property + def attachments(self): + """The pair of points defining a pathway's ends.""" + return self._attachments + + @attachments.setter + def attachments(self, attachments): + if hasattr(self, '_attachments'): + msg = ( + f'Can\'t set attribute `attachments` to {repr(attachments)} ' + f'as it is immutable.' + ) + raise AttributeError(msg) + if len(attachments) != 2: + msg = ( + f'Value {repr(attachments)} passed to `attachments` was an ' + f'iterable of length {len(attachments)}, must be an iterable ' + f'of length 2.' + ) + raise ValueError(msg) + for i, point in enumerate(attachments): + if not isinstance(point, Point): + msg = ( + f'Value {repr(point)} passed to `attachments` at index ' + f'{i} was of type {type(point)}, must be {Point}.' + ) + raise TypeError(msg) + self._attachments = tuple(attachments) + + @property + @abstractmethod + def length(self): + """An expression representing the pathway's length.""" + pass + + @property + @abstractmethod + def extension_velocity(self): + """An expression representing the pathway's extension velocity.""" + pass + + @abstractmethod + def to_loads(self, force): + """Loads required by the equations of motion method classes. + + Explanation + =========== + + ``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be + passed to the ``loads`` parameters of its ``kanes_equations`` method + when constructing the equations of motion. This method acts as a + utility to produce the correctly-structred pairs of points and vectors + required so that these can be easily concatenated with other items in + the list of loads and passed to ``KanesMethod.kanes_equations``. These + loads are also in the correct form to also be passed to the other + equations of motion method classes, e.g. ``LagrangesMethod``. + + """ + pass + + def __repr__(self): + """Default representation of a pathway.""" + attachments = ', '.join(str(a) for a in self.attachments) + return f'{self.__class__.__name__}({attachments})' + + +class LinearPathway(PathwayBase): + """Linear pathway between a pair of attachment points. + + Explanation + =========== + + A linear pathway forms a straight-line segment between two points and is + the simplest pathway that can be formed. It will not interact with any + other objects in the system, i.e. a ``LinearPathway`` will intersect other + objects to ensure that the path between its two ends (its attachments) is + the shortest possible. + + A linear pathway is made up of two points that can move relative to each + other, and a pair of equal and opposite forces acting on the points. If the + positive time-varying Euclidean distance between the two points is defined, + then the "extension velocity" is the time derivative of this distance. The + extension velocity is positive when the two points are moving away from + each other and negative when moving closer to each other. The direction for + the force acting on either point is determined by constructing a unit + vector directed from the other point to this point. This establishes a sign + convention such that a positive force magnitude tends to push the points + apart. The following diagram shows the positive force sense and the + distance between the points:: + + P Q + o<--- F --->o + | | + |<--l(t)--->| + + Examples + ======== + + >>> from sympy.physics.mechanics import LinearPathway + + To construct a pathway, two points are required to be passed to the + ``attachments`` parameter as a ``tuple``. + + >>> from sympy.physics.mechanics import Point + >>> pA, pB = Point('pA'), Point('pB') + >>> linear_pathway = LinearPathway(pA, pB) + >>> linear_pathway + LinearPathway(pA, pB) + + The pathway created above isn't very interesting without the positions and + velocities of its attachment points being described. Without this its not + possible to describe how the pathway moves, i.e. its length or its + extension velocity. + + >>> from sympy.physics.mechanics import ReferenceFrame + >>> from sympy.physics.vector import dynamicsymbols + >>> N = ReferenceFrame('N') + >>> q = dynamicsymbols('q') + >>> pB.set_pos(pA, q*N.x) + >>> pB.pos_from(pA) + q(t)*N.x + + A pathway's length can be accessed via its ``length`` attribute. + + >>> linear_pathway.length + sqrt(q(t)**2) + + Note how what appears to be an overly-complex expression is returned. This + is actually required as it ensures that a pathway's length is always + positive. + + A pathway's extension velocity can be accessed similarly via its + ``extension_velocity`` attribute. + + >>> linear_pathway.extension_velocity + sqrt(q(t)**2)*Derivative(q(t), t)/q(t) + + Parameters + ========== + + attachments : tuple[Point, Point] + Pair of ``Point`` objects between which the linear pathway spans. + Constructor expects two points to be passed, e.g. + ``LinearPathway(Point('pA'), Point('pB'))``. More or fewer points will + cause an error to be thrown. + + """ + + def __init__(self, *attachments): + """Initializer for ``LinearPathway``. + + Parameters + ========== + + attachments : Point + Pair of ``Point`` objects between which the linear pathway spans. + Constructor expects two points to be passed, e.g. + ``LinearPathway(Point('pA'), Point('pB'))``. More or fewer points + will cause an error to be thrown. + + """ + super().__init__(*attachments) + + @property + def length(self): + """Exact analytical expression for the pathway's length.""" + return _point_pair_length(*self.attachments) + + @property + def extension_velocity(self): + """Exact analytical expression for the pathway's extension velocity.""" + return _point_pair_extension_velocity(*self.attachments) + + def to_loads(self, force): + """Loads required by the equations of motion method classes. + + Explanation + =========== + + ``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be + passed to the ``loads`` parameters of its ``kanes_equations`` method + when constructing the equations of motion. This method acts as a + utility to produce the correctly-structred pairs of points and vectors + required so that these can be easily concatenated with other items in + the list of loads and passed to ``KanesMethod.kanes_equations``. These + loads are also in the correct form to also be passed to the other + equations of motion method classes, e.g. ``LagrangesMethod``. + + Examples + ======== + + The below example shows how to generate the loads produced in a linear + actuator that produces an expansile force ``F``. First, create a linear + actuator between two points separated by the coordinate ``q`` in the + ``x`` direction of the global frame ``N``. + + >>> from sympy.physics.mechanics import (LinearPathway, Point, + ... ReferenceFrame) + >>> from sympy.physics.vector import dynamicsymbols + >>> q = dynamicsymbols('q') + >>> N = ReferenceFrame('N') + >>> pA, pB = Point('pA'), Point('pB') + >>> pB.set_pos(pA, q*N.x) + >>> linear_pathway = LinearPathway(pA, pB) + + Now create a symbol ``F`` to describe the magnitude of the (expansile) + force that will be produced along the pathway. The list of loads that + ``KanesMethod`` requires can be produced by calling the pathway's + ``to_loads`` method with ``F`` passed as the only argument. + + >>> from sympy import symbols + >>> F = symbols('F') + >>> linear_pathway.to_loads(F) + [(pA, - F*q(t)/sqrt(q(t)**2)*N.x), (pB, F*q(t)/sqrt(q(t)**2)*N.x)] + + Parameters + ========== + + force : Expr + Magnitude of the force acting along the length of the pathway. As + per the sign conventions for the pathway length, pathway extension + velocity, and pair of point forces, if this ``Expr`` is positive + then the force will act to push the pair of points away from one + another (it is expansile). + + """ + relative_position = _point_pair_relative_position(*self.attachments) + loads = [ + Force(self.attachments[0], -force*relative_position/self.length), + Force(self.attachments[-1], force*relative_position/self.length), + ] + return loads + + +class ObstacleSetPathway(PathwayBase): + """Obstacle-set pathway between a set of attachment points. + + Explanation + =========== + + An obstacle-set pathway forms a series of straight-line segment between + pairs of consecutive points in a set of points. It is similar to multiple + linear pathways joined end-to-end. It will not interact with any other + objects in the system, i.e. an ``ObstacleSetPathway`` will intersect other + objects to ensure that the path between its pairs of points (its + attachments) is the shortest possible. + + Examples + ======== + + To construct an obstacle-set pathway, three or more points are required to + be passed to the ``attachments`` parameter as a ``tuple``. + + >>> from sympy.physics.mechanics import ObstacleSetPathway, Point + >>> pA, pB, pC, pD = Point('pA'), Point('pB'), Point('pC'), Point('pD') + >>> obstacle_set_pathway = ObstacleSetPathway(pA, pB, pC, pD) + >>> obstacle_set_pathway + ObstacleSetPathway(pA, pB, pC, pD) + + The pathway created above isn't very interesting without the positions and + velocities of its attachment points being described. Without this its not + possible to describe how the pathway moves, i.e. its length or its + extension velocity. + + >>> from sympy import cos, sin + >>> from sympy.physics.mechanics import ReferenceFrame + >>> from sympy.physics.vector import dynamicsymbols + >>> N = ReferenceFrame('N') + >>> q = dynamicsymbols('q') + >>> pO = Point('pO') + >>> pA.set_pos(pO, N.y) + >>> pB.set_pos(pO, -N.x) + >>> pC.set_pos(pA, cos(q) * N.x - (sin(q) + 1) * N.y) + >>> pD.set_pos(pA, sin(q) * N.x + (cos(q) - 1) * N.y) + >>> pB.pos_from(pA) + - N.x - N.y + >>> pC.pos_from(pA) + cos(q(t))*N.x + (-sin(q(t)) - 1)*N.y + >>> pD.pos_from(pA) + sin(q(t))*N.x + (cos(q(t)) - 1)*N.y + + A pathway's length can be accessed via its ``length`` attribute. + + >>> obstacle_set_pathway.length.simplify() + sqrt(2)*(sqrt(cos(q(t)) + 1) + 2) + + A pathway's extension velocity can be accessed similarly via its + ``extension_velocity`` attribute. + + >>> obstacle_set_pathway.extension_velocity.simplify() + -sqrt(2)*sin(q(t))*Derivative(q(t), t)/(2*sqrt(cos(q(t)) + 1)) + + Parameters + ========== + + attachments : tuple[Point, ...] + The set of ``Point`` objects that define the segmented obstacle-set + pathway. + + """ + + def __init__(self, *attachments): + """Initializer for ``ObstacleSetPathway``. + + Parameters + ========== + + attachments : tuple[Point, ...] + The set of ``Point`` objects that define the segmented obstacle-set + pathway. + + """ + super().__init__(*attachments) + + @property + def attachments(self): + """The set of points defining a pathway's segmented path.""" + return self._attachments + + @attachments.setter + def attachments(self, attachments): + if hasattr(self, '_attachments'): + msg = ( + f'Can\'t set attribute `attachments` to {repr(attachments)} ' + f'as it is immutable.' + ) + raise AttributeError(msg) + if len(attachments) <= 2: + msg = ( + f'Value {repr(attachments)} passed to `attachments` was an ' + f'iterable of length {len(attachments)}, must be an iterable ' + f'of length 3 or greater.' + ) + raise ValueError(msg) + for i, point in enumerate(attachments): + if not isinstance(point, Point): + msg = ( + f'Value {repr(point)} passed to `attachments` at index ' + f'{i} was of type {type(point)}, must be {Point}.' + ) + raise TypeError(msg) + self._attachments = tuple(attachments) + + @property + def length(self): + """Exact analytical expression for the pathway's length.""" + length = S.Zero + attachment_pairs = zip(self.attachments[:-1], self.attachments[1:]) + for attachment_pair in attachment_pairs: + length += _point_pair_length(*attachment_pair) + return length + + @property + def extension_velocity(self): + """Exact analytical expression for the pathway's extension velocity.""" + extension_velocity = S.Zero + attachment_pairs = zip(self.attachments[:-1], self.attachments[1:]) + for attachment_pair in attachment_pairs: + extension_velocity += _point_pair_extension_velocity(*attachment_pair) + return extension_velocity + + def to_loads(self, force): + """Loads required by the equations of motion method classes. + + Explanation + =========== + + ``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be + passed to the ``loads`` parameters of its ``kanes_equations`` method + when constructing the equations of motion. This method acts as a + utility to produce the correctly-structred pairs of points and vectors + required so that these can be easily concatenated with other items in + the list of loads and passed to ``KanesMethod.kanes_equations``. These + loads are also in the correct form to also be passed to the other + equations of motion method classes, e.g. ``LagrangesMethod``. + + Examples + ======== + + The below example shows how to generate the loads produced in an + actuator that follows an obstacle-set pathway between four points and + produces an expansile force ``F``. First, create a pair of reference + frames, ``A`` and ``B``, in which the four points ``pA``, ``pB``, + ``pC``, and ``pD`` will be located. The first two points in frame ``A`` + and the second two in frame ``B``. Frame ``B`` will also be oriented + such that it relates to ``A`` via a rotation of ``q`` about an axis + ``N.z`` in a global frame (``N.z``, ``A.z``, and ``B.z`` are parallel). + + >>> from sympy.physics.mechanics import (ObstacleSetPathway, Point, + ... ReferenceFrame) + >>> from sympy.physics.vector import dynamicsymbols + >>> q = dynamicsymbols('q') + >>> N = ReferenceFrame('N') + >>> N = ReferenceFrame('N') + >>> A = N.orientnew('A', 'axis', (0, N.x)) + >>> B = A.orientnew('B', 'axis', (q, N.z)) + >>> pO = Point('pO') + >>> pA, pB, pC, pD = Point('pA'), Point('pB'), Point('pC'), Point('pD') + >>> pA.set_pos(pO, A.x) + >>> pB.set_pos(pO, -A.y) + >>> pC.set_pos(pO, B.y) + >>> pD.set_pos(pO, B.x) + >>> obstacle_set_pathway = ObstacleSetPathway(pA, pB, pC, pD) + + Now create a symbol ``F`` to describe the magnitude of the (expansile) + force that will be produced along the pathway. The list of loads that + ``KanesMethod`` requires can be produced by calling the pathway's + ``to_loads`` method with ``F`` passed as the only argument. + + >>> from sympy import Symbol + >>> F = Symbol('F') + >>> obstacle_set_pathway.to_loads(F) + [(pA, sqrt(2)*F/2*A.x + sqrt(2)*F/2*A.y), + (pB, - sqrt(2)*F/2*A.x - sqrt(2)*F/2*A.y), + (pB, - F/sqrt(2*cos(q(t)) + 2)*A.y - F/sqrt(2*cos(q(t)) + 2)*B.y), + (pC, F/sqrt(2*cos(q(t)) + 2)*A.y + F/sqrt(2*cos(q(t)) + 2)*B.y), + (pC, - sqrt(2)*F/2*B.x + sqrt(2)*F/2*B.y), + (pD, sqrt(2)*F/2*B.x - sqrt(2)*F/2*B.y)] + + Parameters + ========== + + force : Expr + The force acting along the length of the pathway. It is assumed + that this ``Expr`` represents an expansile force. + + """ + loads = [] + attachment_pairs = zip(self.attachments[:-1], self.attachments[1:]) + for attachment_pair in attachment_pairs: + relative_position = _point_pair_relative_position(*attachment_pair) + length = _point_pair_length(*attachment_pair) + loads.extend([ + Force(attachment_pair[0], -force*relative_position/length), + Force(attachment_pair[1], force*relative_position/length), + ]) + return loads + + +class WrappingPathway(PathwayBase): + """Pathway that wraps a geometry object. + + Explanation + =========== + + A wrapping pathway interacts with a geometry object and forms a path that + wraps smoothly along its surface. The wrapping pathway along the geometry + object will be the geodesic that the geometry object defines based on the + two points. It will not interact with any other objects in the system, i.e. + a ``WrappingPathway`` will intersect other objects to ensure that the path + between its two ends (its attachments) is the shortest possible. + + To explain the sign conventions used for pathway length, extension + velocity, and direction of applied forces, we can ignore the geometry with + which the wrapping pathway interacts. A wrapping pathway is made up of two + points that can move relative to each other, and a pair of equal and + opposite forces acting on the points. If the positive time-varying + Euclidean distance between the two points is defined, then the "extension + velocity" is the time derivative of this distance. The extension velocity + is positive when the two points are moving away from each other and + negative when moving closer to each other. The direction for the force + acting on either point is determined by constructing a unit vector directed + from the other point to this point. This establishes a sign convention such + that a positive force magnitude tends to push the points apart. The + following diagram shows the positive force sense and the distance between + the points:: + + P Q + o<--- F --->o + | | + |<--l(t)--->| + + Examples + ======== + + >>> from sympy.physics.mechanics import WrappingPathway + + To construct a wrapping pathway, like other pathways, a pair of points must + be passed, followed by an instance of a wrapping geometry class as a + keyword argument. We'll use a cylinder with radius ``r`` and its axis + parallel to ``N.x`` passing through a point ``pO``. + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import Point, ReferenceFrame, WrappingCylinder + >>> r = symbols('r') + >>> N = ReferenceFrame('N') + >>> pA, pB, pO = Point('pA'), Point('pB'), Point('pO') + >>> cylinder = WrappingCylinder(r, pO, N.x) + >>> wrapping_pathway = WrappingPathway(pA, pB, cylinder) + >>> wrapping_pathway + WrappingPathway(pA, pB, geometry=WrappingCylinder(radius=r, point=pO, + axis=N.x)) + + Parameters + ========== + + attachment_1 : Point + First of the pair of ``Point`` objects between which the wrapping + pathway spans. + attachment_2 : Point + Second of the pair of ``Point`` objects between which the wrapping + pathway spans. + geometry : WrappingGeometryBase + Geometry about which the pathway wraps. + + """ + + def __init__(self, attachment_1, attachment_2, geometry): + """Initializer for ``WrappingPathway``. + + Parameters + ========== + + attachment_1 : Point + First of the pair of ``Point`` objects between which the wrapping + pathway spans. + attachment_2 : Point + Second of the pair of ``Point`` objects between which the wrapping + pathway spans. + geometry : WrappingGeometryBase + Geometry about which the pathway wraps. + The geometry about which the pathway wraps. + + """ + super().__init__(attachment_1, attachment_2) + self.geometry = geometry + + @property + def geometry(self): + """Geometry around which the pathway wraps.""" + return self._geometry + + @geometry.setter + def geometry(self, geometry): + if hasattr(self, '_geometry'): + msg = ( + f'Can\'t set attribute `geometry` to {repr(geometry)} as it ' + f'is immutable.' + ) + raise AttributeError(msg) + if not isinstance(geometry, WrappingGeometryBase): + msg = ( + f'Value {repr(geometry)} passed to `geometry` was of type ' + f'{type(geometry)}, must be {WrappingGeometryBase}.' + ) + raise TypeError(msg) + self._geometry = geometry + + @property + def length(self): + """Exact analytical expression for the pathway's length.""" + return self.geometry.geodesic_length(*self.attachments) + + @property + def extension_velocity(self): + """Exact analytical expression for the pathway's extension velocity.""" + return self.length.diff(dynamicsymbols._t) + + def to_loads(self, force): + """Loads required by the equations of motion method classes. + + Explanation + =========== + + ``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be + passed to the ``loads`` parameters of its ``kanes_equations`` method + when constructing the equations of motion. This method acts as a + utility to produce the correctly-structred pairs of points and vectors + required so that these can be easily concatenated with other items in + the list of loads and passed to ``KanesMethod.kanes_equations``. These + loads are also in the correct form to also be passed to the other + equations of motion method classes, e.g. ``LagrangesMethod``. + + Examples + ======== + + The below example shows how to generate the loads produced in an + actuator that produces an expansile force ``F`` while wrapping around a + cylinder. First, create a cylinder with radius ``r`` and an axis + parallel to the ``N.z`` direction of the global frame ``N`` that also + passes through a point ``pO``. + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import (Point, ReferenceFrame, + ... WrappingCylinder) + >>> N = ReferenceFrame('N') + >>> r = symbols('r', positive=True) + >>> pO = Point('pO') + >>> cylinder = WrappingCylinder(r, pO, N.z) + + Create the pathway of the actuator using the ``WrappingPathway`` class, + defined to span between two points ``pA`` and ``pB``. Both points lie + on the surface of the cylinder and the location of ``pB`` is defined + relative to ``pA`` by the dynamics symbol ``q``. + + >>> from sympy import cos, sin + >>> from sympy.physics.mechanics import WrappingPathway, dynamicsymbols + >>> q = dynamicsymbols('q') + >>> pA = Point('pA') + >>> pB = Point('pB') + >>> pA.set_pos(pO, r*N.x) + >>> pB.set_pos(pO, r*(cos(q)*N.x + sin(q)*N.y)) + >>> pB.pos_from(pA) + (r*cos(q(t)) - r)*N.x + r*sin(q(t))*N.y + >>> pathway = WrappingPathway(pA, pB, cylinder) + + Now create a symbol ``F`` to describe the magnitude of the (expansile) + force that will be produced along the pathway. The list of loads that + ``KanesMethod`` requires can be produced by calling the pathway's + ``to_loads`` method with ``F`` passed as the only argument. + + >>> F = symbols('F') + >>> loads = pathway.to_loads(F) + >>> [load.__class__(load.location, load.vector.simplify()) for load in loads] + [(pA, F*N.y), (pB, F*sin(q(t))*N.x - F*cos(q(t))*N.y), + (pO, - F*sin(q(t))*N.x + F*(cos(q(t)) - 1)*N.y)] + + Parameters + ========== + + force : Expr + Magnitude of the force acting along the length of the pathway. It + is assumed that this ``Expr`` represents an expansile force. + + """ + pA, pB = self.attachments + pO = self.geometry.point + pA_force, pB_force = self.geometry.geodesic_end_vectors(pA, pB) + pO_force = -(pA_force + pB_force) + + loads = [ + Force(pA, force * pA_force), + Force(pB, force * pB_force), + Force(pO, force * pO_force), + ] + return loads + + def __repr__(self): + """Representation of a ``WrappingPathway``.""" + attachments = ', '.join(str(a) for a in self.attachments) + return ( + f'{self.__class__.__name__}({attachments}, ' + f'geometry={self.geometry})' + ) + + +def _point_pair_relative_position(point_1, point_2): + """The relative position between a pair of points.""" + return point_2.pos_from(point_1) + + +def _point_pair_length(point_1, point_2): + """The length of the direct linear path between two points.""" + return _point_pair_relative_position(point_1, point_2).magnitude() + + +def _point_pair_extension_velocity(point_1, point_2): + """The extension velocity of the direct linear path between two points.""" + return _point_pair_length(point_1, point_2).diff(dynamicsymbols._t) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/rigidbody.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/rigidbody.py new file mode 100644 index 0000000000000000000000000000000000000000..7cc61ff468f7f26d98209a48ca59ffa12a570490 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/rigidbody.py @@ -0,0 +1,314 @@ +from sympy import Symbol, S +from sympy.physics.vector import ReferenceFrame, Dyadic, Point, dot +from sympy.physics.mechanics.body_base import BodyBase +from sympy.physics.mechanics.inertia import inertia_of_point_mass, Inertia +from sympy.utilities.exceptions import sympy_deprecation_warning + +__all__ = ['RigidBody'] + + +class RigidBody(BodyBase): + """An idealized rigid body. + + Explanation + =========== + + This is essentially a container which holds the various components which + describe a rigid body: a name, mass, center of mass, reference frame, and + inertia. + + All of these need to be supplied on creation, but can be changed + afterwards. + + Attributes + ========== + + name : string + The body's name. + masscenter : Point + The point which represents the center of mass of the rigid body. + frame : ReferenceFrame + The ReferenceFrame which the rigid body is fixed in. + mass : Sympifyable + The body's mass. + inertia : (Dyadic, Point) + The body's inertia about a point; stored in a tuple as shown above. + potential_energy : Sympifyable + The potential energy of the RigidBody. + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.mechanics import ReferenceFrame, Point, RigidBody + >>> from sympy.physics.mechanics import outer + >>> m = Symbol('m') + >>> A = ReferenceFrame('A') + >>> P = Point('P') + >>> I = outer (A.x, A.x) + >>> inertia_tuple = (I, P) + >>> B = RigidBody('B', P, A, m, inertia_tuple) + >>> # Or you could change them afterwards + >>> m2 = Symbol('m2') + >>> B.mass = m2 + + """ + + def __init__(self, name, masscenter=None, frame=None, mass=None, + inertia=None): + super().__init__(name, masscenter, mass) + if frame is None: + frame = ReferenceFrame(f'{name}_frame') + self.frame = frame + if inertia is None: + ixx = Symbol(f'{name}_ixx') + iyy = Symbol(f'{name}_iyy') + izz = Symbol(f'{name}_izz') + izx = Symbol(f'{name}_izx') + ixy = Symbol(f'{name}_ixy') + iyz = Symbol(f'{name}_iyz') + inertia = Inertia.from_inertia_scalars(self.masscenter, self.frame, + ixx, iyy, izz, ixy, iyz, izx) + self.inertia = inertia + + def __repr__(self): + return (f'{self.__class__.__name__}({repr(self.name)}, masscenter=' + f'{repr(self.masscenter)}, frame={repr(self.frame)}, mass=' + f'{repr(self.mass)}, inertia={repr(self.inertia)})') + + @property + def frame(self): + """The ReferenceFrame fixed to the body.""" + return self._frame + + @frame.setter + def frame(self, F): + if not isinstance(F, ReferenceFrame): + raise TypeError("RigidBody frame must be a ReferenceFrame object.") + self._frame = F + + @property + def x(self): + """The basis Vector for the body, in the x direction. """ + return self.frame.x + + @property + def y(self): + """The basis Vector for the body, in the y direction. """ + return self.frame.y + + @property + def z(self): + """The basis Vector for the body, in the z direction. """ + return self.frame.z + + @property + def inertia(self): + """The body's inertia about a point; stored as (Dyadic, Point).""" + return self._inertia + + @inertia.setter + def inertia(self, I): + # check if I is of the form (Dyadic, Point) + if len(I) != 2 or not isinstance(I[0], Dyadic) or not isinstance(I[1], Point): + raise TypeError("RigidBody inertia must be a tuple of the form (Dyadic, Point).") + + self._inertia = Inertia(I[0], I[1]) + # have I S/O, want I S/S* + # I S/O = I S/S* + I S*/O; I S/S* = I S/O - I S*/O + # I_S/S* = I_S/O - I_S*/O + I_Ss_O = inertia_of_point_mass(self.mass, + self.masscenter.pos_from(I[1]), + self.frame) + self._central_inertia = I[0] - I_Ss_O + + @property + def central_inertia(self): + """The body's central inertia dyadic.""" + return self._central_inertia + + @central_inertia.setter + def central_inertia(self, I): + if not isinstance(I, Dyadic): + raise TypeError("RigidBody inertia must be a Dyadic object.") + self.inertia = Inertia(I, self.masscenter) + + def linear_momentum(self, frame): + """ Linear momentum of the rigid body. + + Explanation + =========== + + The linear momentum L, of a rigid body B, with respect to frame N is + given by: + + ``L = m * v`` + + where m is the mass of the rigid body, and v is the velocity of the mass + center of B in the frame N. + + Parameters + ========== + + frame : ReferenceFrame + The frame in which linear momentum is desired. + + Examples + ======== + + >>> from sympy.physics.mechanics import Point, ReferenceFrame, outer + >>> from sympy.physics.mechanics import RigidBody, dynamicsymbols + >>> from sympy.physics.vector import init_vprinting + >>> init_vprinting(pretty_print=False) + >>> m, v = dynamicsymbols('m v') + >>> N = ReferenceFrame('N') + >>> P = Point('P') + >>> P.set_vel(N, v * N.x) + >>> I = outer (N.x, N.x) + >>> Inertia_tuple = (I, P) + >>> B = RigidBody('B', P, N, m, Inertia_tuple) + >>> B.linear_momentum(N) + m*v*N.x + + """ + + return self.mass * self.masscenter.vel(frame) + + def angular_momentum(self, point, frame): + """Returns the angular momentum of the rigid body about a point in the + given frame. + + Explanation + =========== + + The angular momentum H of a rigid body B about some point O in a frame N + is given by: + + ``H = dot(I, w) + cross(r, m * v)`` + + where I and m are the central inertia dyadic and mass of rigid body B, w + is the angular velocity of body B in the frame N, r is the position + vector from point O to the mass center of B, and v is the velocity of + the mass center in the frame N. + + Parameters + ========== + + point : Point + The point about which angular momentum is desired. + frame : ReferenceFrame + The frame in which angular momentum is desired. + + Examples + ======== + + >>> from sympy.physics.mechanics import Point, ReferenceFrame, outer + >>> from sympy.physics.mechanics import RigidBody, dynamicsymbols + >>> from sympy.physics.vector import init_vprinting + >>> init_vprinting(pretty_print=False) + >>> m, v, r, omega = dynamicsymbols('m v r omega') + >>> N = ReferenceFrame('N') + >>> b = ReferenceFrame('b') + >>> b.set_ang_vel(N, omega * b.x) + >>> P = Point('P') + >>> P.set_vel(N, 1 * N.x) + >>> I = outer(b.x, b.x) + >>> B = RigidBody('B', P, b, m, (I, P)) + >>> B.angular_momentum(P, N) + omega*b.x + + """ + I = self.central_inertia + w = self.frame.ang_vel_in(frame) + m = self.mass + r = self.masscenter.pos_from(point) + v = self.masscenter.vel(frame) + + return I.dot(w) + r.cross(m * v) + + def kinetic_energy(self, frame): + """Kinetic energy of the rigid body. + + Explanation + =========== + + The kinetic energy, T, of a rigid body, B, is given by: + + ``T = 1/2 * (dot(dot(I, w), w) + dot(m * v, v))`` + + where I and m are the central inertia dyadic and mass of rigid body B + respectively, w is the body's angular velocity, and v is the velocity of + the body's mass center in the supplied ReferenceFrame. + + Parameters + ========== + + frame : ReferenceFrame + The RigidBody's angular velocity and the velocity of it's mass + center are typically defined with respect to an inertial frame but + any relevant frame in which the velocities are known can be + supplied. + + Examples + ======== + + >>> from sympy.physics.mechanics import Point, ReferenceFrame, outer + >>> from sympy.physics.mechanics import RigidBody + >>> from sympy import symbols + >>> m, v, r, omega = symbols('m v r omega') + >>> N = ReferenceFrame('N') + >>> b = ReferenceFrame('b') + >>> b.set_ang_vel(N, omega * b.x) + >>> P = Point('P') + >>> P.set_vel(N, v * N.x) + >>> I = outer (b.x, b.x) + >>> inertia_tuple = (I, P) + >>> B = RigidBody('B', P, b, m, inertia_tuple) + >>> B.kinetic_energy(N) + m*v**2/2 + omega**2/2 + + """ + + rotational_KE = S.Half * dot( + self.frame.ang_vel_in(frame), + dot(self.central_inertia, self.frame.ang_vel_in(frame))) + translational_KE = S.Half * self.mass * dot(self.masscenter.vel(frame), + self.masscenter.vel(frame)) + return rotational_KE + translational_KE + + def set_potential_energy(self, scalar): + sympy_deprecation_warning( + """ +The sympy.physics.mechanics.RigidBody.set_potential_energy() +method is deprecated. Instead use + + B.potential_energy = scalar + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-set-potential-energy", + ) + self.potential_energy = scalar + + def parallel_axis(self, point, frame=None): + """Returns the inertia dyadic of the body with respect to another point. + + Parameters + ========== + + point : sympy.physics.vector.Point + The point to express the inertia dyadic about. + frame : sympy.physics.vector.ReferenceFrame + The reference frame used to construct the dyadic. + + Returns + ======= + + inertia : sympy.physics.vector.Dyadic + The inertia dyadic of the rigid body expressed about the provided + point. + + """ + if frame is None: + frame = self.frame + return self.central_inertia + inertia_of_point_mass( + self.mass, self.masscenter.pos_from(point), frame) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/system.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/system.py new file mode 100644 index 0000000000000000000000000000000000000000..c8e0657d7da54ca5aaad9b37b816235641968470 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/system.py @@ -0,0 +1,1553 @@ +from functools import wraps + +from sympy.core.basic import Basic +from sympy.matrices.immutable import ImmutableMatrix +from sympy.matrices.dense import Matrix, eye, zeros +from sympy.core.containers import OrderedSet +from sympy.physics.mechanics.actuator import ActuatorBase +from sympy.physics.mechanics.body_base import BodyBase +from sympy.physics.mechanics.functions import ( + Lagrangian, _validate_coordinates, find_dynamicsymbols) +from sympy.physics.mechanics.joint import Joint +from sympy.physics.mechanics.kane import KanesMethod +from sympy.physics.mechanics.lagrange import LagrangesMethod +from sympy.physics.mechanics.loads import _parse_load, gravity +from sympy.physics.mechanics.method import _Methods +from sympy.physics.mechanics.particle import Particle +from sympy.physics.vector import Point, ReferenceFrame, dynamicsymbols +from sympy.utilities.iterables import iterable +from sympy.utilities.misc import filldedent + +__all__ = ['SymbolicSystem', 'System'] + + +def _reset_eom_method(method): + """Decorator to reset the eom_method if a property is changed.""" + + @wraps(method) + def wrapper(self, *args, **kwargs): + self._eom_method = None + return method(self, *args, **kwargs) + + return wrapper + + +class System(_Methods): + """Class to define a multibody system and form its equations of motion. + + Explanation + =========== + + A ``System`` instance stores the different objects associated with a model, + including bodies, joints, constraints, and other relevant information. With + all the relationships between components defined, the ``System`` can be used + to form the equations of motion using a backend, such as ``KanesMethod``. + The ``System`` has been designed to be compatible with third-party + libraries for greater flexibility and integration with other tools. + + Attributes + ========== + + frame : ReferenceFrame + Inertial reference frame of the system. + fixed_point : Point + A fixed point in the inertial reference frame. + x : Vector + Unit vector fixed in the inertial reference frame. + y : Vector + Unit vector fixed in the inertial reference frame. + z : Vector + Unit vector fixed in the inertial reference frame. + q : ImmutableMatrix + Matrix of all the generalized coordinates, i.e. the independent + generalized coordinates stacked upon the dependent. + u : ImmutableMatrix + Matrix of all the generalized speeds, i.e. the independent generealized + speeds stacked upon the dependent. + q_ind : ImmutableMatrix + Matrix of the independent generalized coordinates. + q_dep : ImmutableMatrix + Matrix of the dependent generalized coordinates. + u_ind : ImmutableMatrix + Matrix of the independent generalized speeds. + u_dep : ImmutableMatrix + Matrix of the dependent generalized speeds. + u_aux : ImmutableMatrix + Matrix of auxiliary generalized speeds. + kdes : ImmutableMatrix + Matrix of the kinematical differential equations as expressions equated + to the zero matrix. + bodies : tuple of BodyBase subclasses + Tuple of all bodies that make up the system. + joints : tuple of Joint + Tuple of all joints that connect bodies in the system. + loads : tuple of LoadBase subclasses + Tuple of all loads that have been applied to the system. + actuators : tuple of ActuatorBase subclasses + Tuple of all actuators present in the system. + holonomic_constraints : ImmutableMatrix + Matrix with the holonomic constraints as expressions equated to the zero + matrix. + nonholonomic_constraints : ImmutableMatrix + Matrix with the nonholonomic constraints as expressions equated to the + zero matrix. + velocity_constraints : ImmutableMatrix + Matrix with the velocity constraints as expressions equated to the zero + matrix. These are by default derived as the time derivatives of the + holonomic constraints extended with the nonholonomic constraints. + eom_method : subclass of KanesMethod or LagrangesMethod + Backend for forming the equations of motion. + + Examples + ======== + + In the example below a cart with a pendulum is created. The cart moves along + the x axis of the rail and the pendulum rotates about the z axis. The length + of the pendulum is ``l`` with the pendulum represented as a particle. To + move the cart a time dependent force ``F`` is applied to the cart. + + We first need to import some functions and create some of our variables. + + >>> from sympy import symbols, simplify + >>> from sympy.physics.mechanics import ( + ... mechanics_printing, dynamicsymbols, RigidBody, Particle, + ... ReferenceFrame, PrismaticJoint, PinJoint, System) + >>> mechanics_printing(pretty_print=False) + >>> g, l = symbols('g l') + >>> F = dynamicsymbols('F') + + The next step is to create bodies. It is also useful to create a frame for + locating the particle with respect to the pin joint later on, as a particle + does not have a body-fixed frame. + + >>> rail = RigidBody('rail') + >>> cart = RigidBody('cart') + >>> bob = Particle('bob') + >>> bob_frame = ReferenceFrame('bob_frame') + + Initialize the system, with the rail as the Newtonian reference. The body is + also automatically added to the system. + + >>> system = System.from_newtonian(rail) + >>> print(system.bodies[0]) + rail + + Create the joints, while immediately also adding them to the system. + + >>> system.add_joints( + ... PrismaticJoint('slider', rail, cart, joint_axis=rail.x), + ... PinJoint('pin', cart, bob, joint_axis=cart.z, + ... child_interframe=bob_frame, + ... child_point=l * bob_frame.y) + ... ) + >>> system.joints + (PrismaticJoint: slider parent: rail child: cart, + PinJoint: pin parent: cart child: bob) + + While adding the joints, the associated generalized coordinates, generalized + speeds, kinematic differential equations and bodies are also added to the + system. + + >>> system.q + Matrix([ + [q_slider], + [ q_pin]]) + >>> system.u + Matrix([ + [u_slider], + [ u_pin]]) + >>> system.kdes + Matrix([ + [u_slider - q_slider'], + [ u_pin - q_pin']]) + >>> [body.name for body in system.bodies] + ['rail', 'cart', 'bob'] + + With the kinematics established, we can now apply gravity and the cart force + ``F``. + + >>> system.apply_uniform_gravity(-g * system.y) + >>> system.add_loads((cart.masscenter, F * rail.x)) + >>> system.loads + ((rail_masscenter, - g*rail_mass*rail_frame.y), + (cart_masscenter, - cart_mass*g*rail_frame.y), + (bob_masscenter, - bob_mass*g*rail_frame.y), + (cart_masscenter, F*rail_frame.x)) + + With the entire system defined, we can now form the equations of motion. + Before forming the equations of motion, one can also run some checks that + will try to identify some common errors. + + >>> system.validate_system() + >>> system.form_eoms() + Matrix([ + [bob_mass*l*u_pin**2*sin(q_pin) - bob_mass*l*cos(q_pin)*u_pin' + - (bob_mass + cart_mass)*u_slider' + F], + [ -bob_mass*g*l*sin(q_pin) - bob_mass*l**2*u_pin' + - bob_mass*l*cos(q_pin)*u_slider']]) + >>> simplify(system.mass_matrix) + Matrix([ + [ bob_mass + cart_mass, bob_mass*l*cos(q_pin)], + [bob_mass*l*cos(q_pin), bob_mass*l**2]]) + >>> system.forcing + Matrix([ + [bob_mass*l*u_pin**2*sin(q_pin) + F], + [ -bob_mass*g*l*sin(q_pin)]]) + + The complexity of the above example can be increased if we add a constraint + to prevent the particle from moving in the horizontal (x) direction. This + can be done by adding a holonomic constraint. After which we should also + redefine what our (in)dependent generalized coordinates and speeds are. + + >>> system.add_holonomic_constraints( + ... bob.masscenter.pos_from(rail.masscenter).dot(system.x) + ... ) + >>> system.q_ind = system.get_joint('pin').coordinates + >>> system.q_dep = system.get_joint('slider').coordinates + >>> system.u_ind = system.get_joint('pin').speeds + >>> system.u_dep = system.get_joint('slider').speeds + + With the updated system the equations of motion can be formed again. + + >>> system.validate_system() + >>> system.form_eoms() + Matrix([[-bob_mass*g*l*sin(q_pin) + - bob_mass*l**2*u_pin' + - bob_mass*l*cos(q_pin)*u_slider' + - l*(bob_mass*l*u_pin**2*sin(q_pin) + - bob_mass*l*cos(q_pin)*u_pin' + - (bob_mass + cart_mass)*u_slider')*cos(q_pin) + - l*F*cos(q_pin)]]) + >>> simplify(system.mass_matrix) + Matrix([ + [bob_mass*l**2*sin(q_pin)**2, -cart_mass*l*cos(q_pin)], + [ l*cos(q_pin), 1]]) + >>> simplify(system.forcing) + Matrix([ + [-l*(bob_mass*g*sin(q_pin) + bob_mass*l*u_pin**2*sin(2*q_pin)/2 + + F*cos(q_pin))], + [ + l*u_pin**2*sin(q_pin)]]) + + """ + + def __init__(self, frame=None, fixed_point=None): + """Initialize the system. + + Parameters + ========== + + frame : ReferenceFrame, optional + The inertial frame of the system. If none is supplied, a new frame + will be created. + fixed_point : Point, optional + A fixed point in the inertial reference frame. If none is supplied, + a new fixed_point will be created. + + """ + if frame is None: + frame = ReferenceFrame('inertial_frame') + elif not isinstance(frame, ReferenceFrame): + raise TypeError('Frame must be an instance of ReferenceFrame.') + self._frame = frame + if fixed_point is None: + fixed_point = Point('inertial_point') + elif not isinstance(fixed_point, Point): + raise TypeError('Fixed point must be an instance of Point.') + self._fixed_point = fixed_point + self._fixed_point.set_vel(self._frame, 0) + self._q_ind = ImmutableMatrix(1, 0, []).T + self._q_dep = ImmutableMatrix(1, 0, []).T + self._u_ind = ImmutableMatrix(1, 0, []).T + self._u_dep = ImmutableMatrix(1, 0, []).T + self._u_aux = ImmutableMatrix(1, 0, []).T + self._kdes = ImmutableMatrix(1, 0, []).T + self._hol_coneqs = ImmutableMatrix(1, 0, []).T + self._nonhol_coneqs = ImmutableMatrix(1, 0, []).T + self._vel_constrs = None + self._bodies = [] + self._joints = [] + self._loads = [] + self._actuators = [] + self._eom_method = None + + @classmethod + def from_newtonian(cls, newtonian): + """Constructs the system with respect to a Newtonian body.""" + if isinstance(newtonian, Particle): + raise TypeError('A Particle has no frame so cannot act as ' + 'the Newtonian.') + system = cls(frame=newtonian.frame, fixed_point=newtonian.masscenter) + system.add_bodies(newtonian) + return system + + @property + def fixed_point(self): + """Fixed point in the inertial reference frame.""" + return self._fixed_point + + @property + def frame(self): + """Inertial reference frame of the system.""" + return self._frame + + @property + def x(self): + """Unit vector fixed in the inertial reference frame.""" + return self._frame.x + + @property + def y(self): + """Unit vector fixed in the inertial reference frame.""" + return self._frame.y + + @property + def z(self): + """Unit vector fixed in the inertial reference frame.""" + return self._frame.z + + @property + def bodies(self): + """Tuple of all bodies that have been added to the system.""" + return tuple(self._bodies) + + @bodies.setter + @_reset_eom_method + def bodies(self, bodies): + bodies = self._objects_to_list(bodies) + self._check_objects(bodies, [], BodyBase, 'Bodies', 'bodies') + self._bodies = bodies + + @property + def joints(self): + """Tuple of all joints that have been added to the system.""" + return tuple(self._joints) + + @joints.setter + @_reset_eom_method + def joints(self, joints): + joints = self._objects_to_list(joints) + self._check_objects(joints, [], Joint, 'Joints', 'joints') + self._joints = [] + self.add_joints(*joints) + + @property + def loads(self): + """Tuple of loads that have been applied on the system.""" + return tuple(self._loads) + + @loads.setter + @_reset_eom_method + def loads(self, loads): + loads = self._objects_to_list(loads) + self._loads = [_parse_load(load) for load in loads] + + @property + def actuators(self): + """Tuple of actuators present in the system.""" + return tuple(self._actuators) + + @actuators.setter + @_reset_eom_method + def actuators(self, actuators): + actuators = self._objects_to_list(actuators) + self._check_objects(actuators, [], ActuatorBase, 'Actuators', + 'actuators') + self._actuators = actuators + + @property + def q(self): + """Matrix of all the generalized coordinates with the independent + stacked upon the dependent.""" + return self._q_ind.col_join(self._q_dep) + + @property + def u(self): + """Matrix of all the generalized speeds with the independent stacked + upon the dependent.""" + return self._u_ind.col_join(self._u_dep) + + @property + def q_ind(self): + """Matrix of the independent generalized coordinates.""" + return self._q_ind + + @q_ind.setter + @_reset_eom_method + def q_ind(self, q_ind): + self._q_ind, self._q_dep = self._parse_coordinates( + self._objects_to_list(q_ind), True, [], self.q_dep, 'coordinates') + + @property + def q_dep(self): + """Matrix of the dependent generalized coordinates.""" + return self._q_dep + + @q_dep.setter + @_reset_eom_method + def q_dep(self, q_dep): + self._q_ind, self._q_dep = self._parse_coordinates( + self._objects_to_list(q_dep), False, self.q_ind, [], 'coordinates') + + @property + def u_ind(self): + """Matrix of the independent generalized speeds.""" + return self._u_ind + + @u_ind.setter + @_reset_eom_method + def u_ind(self, u_ind): + self._u_ind, self._u_dep = self._parse_coordinates( + self._objects_to_list(u_ind), True, [], self.u_dep, 'speeds') + + @property + def u_dep(self): + """Matrix of the dependent generalized speeds.""" + return self._u_dep + + @u_dep.setter + @_reset_eom_method + def u_dep(self, u_dep): + self._u_ind, self._u_dep = self._parse_coordinates( + self._objects_to_list(u_dep), False, self.u_ind, [], 'speeds') + + @property + def u_aux(self): + """Matrix of auxiliary generalized speeds.""" + return self._u_aux + + @u_aux.setter + @_reset_eom_method + def u_aux(self, u_aux): + self._u_aux = self._parse_coordinates( + self._objects_to_list(u_aux), True, [], [], 'u_auxiliary')[0] + + @property + def kdes(self): + """Kinematical differential equations as expressions equated to the zero + matrix. These equations describe the coupling between the generalized + coordinates and the generalized speeds.""" + return self._kdes + + @kdes.setter + @_reset_eom_method + def kdes(self, kdes): + kdes = self._objects_to_list(kdes) + self._kdes = self._parse_expressions( + kdes, [], 'kinematic differential equations') + + @property + def holonomic_constraints(self): + """Matrix with the holonomic constraints as expressions equated to the + zero matrix.""" + return self._hol_coneqs + + @holonomic_constraints.setter + @_reset_eom_method + def holonomic_constraints(self, constraints): + constraints = self._objects_to_list(constraints) + self._hol_coneqs = self._parse_expressions( + constraints, [], 'holonomic constraints') + + @property + def nonholonomic_constraints(self): + """Matrix with the nonholonomic constraints as expressions equated to + the zero matrix.""" + return self._nonhol_coneqs + + @nonholonomic_constraints.setter + @_reset_eom_method + def nonholonomic_constraints(self, constraints): + constraints = self._objects_to_list(constraints) + self._nonhol_coneqs = self._parse_expressions( + constraints, [], 'nonholonomic constraints') + + @property + def velocity_constraints(self): + """Matrix with the velocity constraints as expressions equated to the + zero matrix. The velocity constraints are by default derived from the + holonomic and nonholonomic constraints unless they are explicitly set. + """ + if self._vel_constrs is None: + return self.holonomic_constraints.diff(dynamicsymbols._t).col_join( + self.nonholonomic_constraints) + return self._vel_constrs + + @velocity_constraints.setter + @_reset_eom_method + def velocity_constraints(self, constraints): + if constraints is None: + self._vel_constrs = None + return + constraints = self._objects_to_list(constraints) + self._vel_constrs = self._parse_expressions( + constraints, [], 'velocity constraints') + + @property + def eom_method(self): + """Backend for forming the equations of motion.""" + return self._eom_method + + @staticmethod + def _objects_to_list(lst): + """Helper to convert passed objects to a list.""" + if not iterable(lst): # Only one object + return [lst] + return list(lst[:]) # converts Matrix and tuple to flattened list + + @staticmethod + def _check_objects(objects, obj_lst, expected_type, obj_name, type_name): + """Helper to check the objects that are being added to the system. + + Explanation + =========== + This method checks that the objects that are being added to the system + are of the correct type and have not already been added. If any of the + objects are not of the correct type or have already been added, then + an error is raised. + + Parameters + ========== + objects : iterable + The objects that would be added to the system. + obj_lst : list + The list of objects that are already in the system. + expected_type : type + The type that the objects should be. + obj_name : str + The name of the category of objects. This string is used to + formulate the error message for the user. + type_name : str + The name of the type that the objects should be. This string is used + to formulate the error message for the user. + + """ + seen = set(obj_lst) + duplicates = set() + wrong_types = set() + for obj in objects: + if not isinstance(obj, expected_type): + wrong_types.add(obj) + if obj in seen: + duplicates.add(obj) + else: + seen.add(obj) + if wrong_types: + raise TypeError(f'{obj_name} {wrong_types} are not {type_name}.') + if duplicates: + raise ValueError(f'{obj_name} {duplicates} have already been added ' + f'to the system.') + + def _parse_coordinates(self, new_coords, independent, old_coords_ind, + old_coords_dep, coord_type='coordinates'): + """Helper to parse coordinates and speeds.""" + # Construct lists of the independent and dependent coordinates + coords_ind, coords_dep = old_coords_ind[:], old_coords_dep[:] + if not iterable(independent): + independent = [independent] * len(new_coords) + for coord, indep in zip(new_coords, independent): + if indep: + coords_ind.append(coord) + else: + coords_dep.append(coord) + # Check types and duplicates + current = {'coordinates': self.q_ind[:] + self.q_dep[:], + 'speeds': self.u_ind[:] + self.u_dep[:], + 'u_auxiliary': self._u_aux[:], + coord_type: coords_ind + coords_dep} + _validate_coordinates(**current) + return (ImmutableMatrix(1, len(coords_ind), coords_ind).T, + ImmutableMatrix(1, len(coords_dep), coords_dep).T) + + @staticmethod + def _parse_expressions(new_expressions, old_expressions, name, + check_negatives=False): + """Helper to parse expressions like constraints.""" + old_expressions = old_expressions[:] + new_expressions = list(new_expressions) # Converts a possible tuple + if check_negatives: + check_exprs = old_expressions + [-expr for expr in old_expressions] + else: + check_exprs = old_expressions + System._check_objects(new_expressions, check_exprs, Basic, name, + 'expressions') + for expr in new_expressions: + if expr == 0: + raise ValueError(f'Parsed {name} are zero.') + return ImmutableMatrix(1, len(old_expressions) + len(new_expressions), + old_expressions + new_expressions).T + + @_reset_eom_method + def add_coordinates(self, *coordinates, independent=True): + """Add generalized coordinate(s) to the system. + + Parameters + ========== + + *coordinates : dynamicsymbols + One or more generalized coordinates to be added to the system. + independent : bool or list of bool, optional + Boolean whether a coordinate is dependent or independent. The + default is True, so the coordinates are added as independent by + default. + + """ + self._q_ind, self._q_dep = self._parse_coordinates( + coordinates, independent, self.q_ind, self.q_dep, 'coordinates') + + @_reset_eom_method + def add_speeds(self, *speeds, independent=True): + """Add generalized speed(s) to the system. + + Parameters + ========== + + *speeds : dynamicsymbols + One or more generalized speeds to be added to the system. + independent : bool or list of bool, optional + Boolean whether a speed is dependent or independent. The default is + True, so the speeds are added as independent by default. + + """ + self._u_ind, self._u_dep = self._parse_coordinates( + speeds, independent, self.u_ind, self.u_dep, 'speeds') + + @_reset_eom_method + def add_auxiliary_speeds(self, *speeds): + """Add auxiliary speed(s) to the system. + + Parameters + ========== + + *speeds : dynamicsymbols + One or more auxiliary speeds to be added to the system. + + """ + self._u_aux = self._parse_coordinates( + speeds, True, self._u_aux, [], 'u_auxiliary')[0] + + @_reset_eom_method + def add_kdes(self, *kdes): + """Add kinematic differential equation(s) to the system. + + Parameters + ========== + + *kdes : Expr + One or more kinematic differential equations. + + """ + self._kdes = self._parse_expressions( + kdes, self.kdes, 'kinematic differential equations', + check_negatives=True) + + @_reset_eom_method + def add_holonomic_constraints(self, *constraints): + """Add holonomic constraint(s) to the system. + + Parameters + ========== + + *constraints : Expr + One or more holonomic constraints, which are expressions that should + be zero. + + """ + self._hol_coneqs = self._parse_expressions( + constraints, self._hol_coneqs, 'holonomic constraints', + check_negatives=True) + + @_reset_eom_method + def add_nonholonomic_constraints(self, *constraints): + """Add nonholonomic constraint(s) to the system. + + Parameters + ========== + + *constraints : Expr + One or more nonholonomic constraints, which are expressions that + should be zero. + + """ + self._nonhol_coneqs = self._parse_expressions( + constraints, self._nonhol_coneqs, 'nonholonomic constraints', + check_negatives=True) + + @_reset_eom_method + def add_bodies(self, *bodies): + """Add body(ies) to the system. + + Parameters + ========== + + bodies : Particle or RigidBody + One or more bodies. + + """ + self._check_objects(bodies, self.bodies, BodyBase, 'Bodies', 'bodies') + self._bodies.extend(bodies) + + @_reset_eom_method + def add_loads(self, *loads): + """Add load(s) to the system. + + Parameters + ========== + + *loads : Force or Torque + One or more loads. + + """ + loads = [_parse_load(load) for load in loads] # Checks the loads + self._loads.extend(loads) + + @_reset_eom_method + def apply_uniform_gravity(self, acceleration): + """Apply uniform gravity to all bodies in the system by adding loads. + + Parameters + ========== + + acceleration : Vector + The acceleration due to gravity. + + """ + self.add_loads(*gravity(acceleration, *self.bodies)) + + @_reset_eom_method + def add_actuators(self, *actuators): + """Add actuator(s) to the system. + + Parameters + ========== + + *actuators : subclass of ActuatorBase + One or more actuators. + + """ + self._check_objects(actuators, self.actuators, ActuatorBase, + 'Actuators', 'actuators') + self._actuators.extend(actuators) + + @_reset_eom_method + def add_joints(self, *joints): + """Add joint(s) to the system. + + Explanation + =========== + + This methods adds one or more joints to the system including its + associated objects, i.e. generalized coordinates, generalized speeds, + kinematic differential equations and the bodies. + + Parameters + ========== + + *joints : subclass of Joint + One or more joints. + + Notes + ===== + + For the generalized coordinates, generalized speeds and bodies it is + checked whether they are already known by the system instance. If they + are, then they are not added. The kinematic differential equations are + however always added to the system, so you should not also manually add + those on beforehand. + + """ + self._check_objects(joints, self.joints, Joint, 'Joints', 'joints') + self._joints.extend(joints) + coordinates, speeds, kdes, bodies = (OrderedSet() for _ in range(4)) + for joint in joints: + coordinates.update(joint.coordinates) + speeds.update(joint.speeds) + kdes.update(joint.kdes) + bodies.update((joint.parent, joint.child)) + coordinates = coordinates.difference(self.q) + speeds = speeds.difference(self.u) + kdes = kdes.difference(self.kdes[:] + (-self.kdes)[:]) + bodies = bodies.difference(self.bodies) + self.add_coordinates(*tuple(coordinates)) + self.add_speeds(*tuple(speeds)) + self.add_kdes(*(kde for kde in tuple(kdes) if not kde == 0)) + self.add_bodies(*tuple(bodies)) + + def get_body(self, name): + """Retrieve a body from the system by name. + + Parameters + ========== + + name : str + The name of the body to retrieve. + + Returns + ======= + + RigidBody or Particle + The body with the given name, or None if no such body exists. + + """ + for body in self._bodies: + if body.name == name: + return body + + def get_joint(self, name): + """Retrieve a joint from the system by name. + + Parameters + ========== + + name : str + The name of the joint to retrieve. + + Returns + ======= + + subclass of Joint + The joint with the given name, or None if no such joint exists. + + """ + for joint in self._joints: + if joint.name == name: + return joint + + def _form_eoms(self): + return self.form_eoms() + + def form_eoms(self, eom_method=KanesMethod, **kwargs): + """Form the equations of motion of the system. + + Parameters + ========== + + eom_method : subclass of KanesMethod or LagrangesMethod + Backend class to be used for forming the equations of motion. The + default is ``KanesMethod``. + + Returns + ======== + + ImmutableMatrix + Vector of equations of motions. + + Examples + ======== + + This is a simple example for a one degree of freedom translational + spring-mass-damper. + + >>> from sympy import S, symbols + >>> from sympy.physics.mechanics import ( + ... LagrangesMethod, dynamicsymbols, PrismaticJoint, Particle, + ... RigidBody, System) + >>> q = dynamicsymbols('q') + >>> qd = dynamicsymbols('q', 1) + >>> m, k, b = symbols('m k b') + >>> wall = RigidBody('W') + >>> system = System.from_newtonian(wall) + >>> bob = Particle('P', mass=m) + >>> bob.potential_energy = S.Half * k * q**2 + >>> system.add_joints(PrismaticJoint('J', wall, bob, q, qd)) + >>> system.add_loads((bob.masscenter, b * qd * system.x)) + >>> system.form_eoms(LagrangesMethod) + Matrix([[-b*Derivative(q(t), t) + k*q(t) + m*Derivative(q(t), (t, 2))]]) + + We can also solve for the states using the 'rhs' method. + + >>> system.rhs() + Matrix([ + [ Derivative(q(t), t)], + [(b*Derivative(q(t), t) - k*q(t))/m]]) + + """ + # KanesMethod does not accept empty iterables + loads = self.loads + tuple( + load for act in self.actuators for load in act.to_loads()) + loads = loads if loads else None + if issubclass(eom_method, KanesMethod): + disallowed_kwargs = { + "frame", "q_ind", "u_ind", "kd_eqs", "q_dependent", + "u_dependent", "u_auxiliary", "configuration_constraints", + "velocity_constraints", "forcelist", "bodies"} + wrong_kwargs = disallowed_kwargs.intersection(kwargs) + if wrong_kwargs: + raise ValueError( + f"The following keyword arguments are not allowed to be " + f"overwritten in {eom_method.__name__}: {wrong_kwargs}.") + kwargs = {"frame": self.frame, "q_ind": self.q_ind, + "u_ind": self.u_ind, "kd_eqs": self.kdes, + "q_dependent": self.q_dep, "u_dependent": self.u_dep, + "configuration_constraints": self.holonomic_constraints, + "velocity_constraints": self.velocity_constraints, + "u_auxiliary": self.u_aux, + "forcelist": loads, "bodies": self.bodies, + "explicit_kinematics": False, **kwargs} + self._eom_method = eom_method(**kwargs) + elif issubclass(eom_method, LagrangesMethod): + disallowed_kwargs = { + "frame", "qs", "forcelist", "bodies", "hol_coneqs", + "nonhol_coneqs", "Lagrangian"} + wrong_kwargs = disallowed_kwargs.intersection(kwargs) + if wrong_kwargs: + raise ValueError( + f"The following keyword arguments are not allowed to be " + f"overwritten in {eom_method.__name__}: {wrong_kwargs}.") + kwargs = {"frame": self.frame, "qs": self.q, "forcelist": loads, + "bodies": self.bodies, + "hol_coneqs": self.holonomic_constraints, + "nonhol_coneqs": self.nonholonomic_constraints, **kwargs} + if "Lagrangian" not in kwargs: + kwargs["Lagrangian"] = Lagrangian(kwargs["frame"], + *kwargs["bodies"]) + self._eom_method = eom_method(**kwargs) + else: + raise NotImplementedError(f'{eom_method} has not been implemented.') + return self.eom_method._form_eoms() + + def rhs(self, inv_method=None): + """Compute the equations of motion in the explicit form. + + Parameters + ========== + + inv_method : str + The specific sympy inverse matrix calculation method to use. For a + list of valid methods, see + :meth:`~sympy.matrices.matrixbase.MatrixBase.inv` + + Returns + ======== + + ImmutableMatrix + Equations of motion in the explicit form. + + See Also + ======== + + sympy.physics.mechanics.kane.KanesMethod.rhs: + KanesMethod's ``rhs`` function. + sympy.physics.mechanics.lagrange.LagrangesMethod.rhs: + LagrangesMethod's ``rhs`` function. + + """ + return self.eom_method.rhs(inv_method=inv_method) + + @property + def mass_matrix(self): + r"""The mass matrix of the system. + + Explanation + =========== + + The mass matrix $M_d$ and the forcing vector $f_d$ of a system describe + the system's dynamics according to the following equations: + + .. math:: + M_d \dot{u} = f_d + + where $\dot{u}$ is the time derivative of the generalized speeds. + + """ + return self.eom_method.mass_matrix + + @property + def mass_matrix_full(self): + r"""The mass matrix of the system, augmented by the kinematic + differential equations in explicit or implicit form. + + Explanation + =========== + + The full mass matrix $M_m$ and the full forcing vector $f_m$ of a system + describe the dynamics and kinematics according to the following + equation: + + .. math:: + M_m \dot{x} = f_m + + where $x$ is the state vector stacking $q$ and $u$. + + """ + return self.eom_method.mass_matrix_full + + @property + def forcing(self): + """The forcing vector of the system.""" + return self.eom_method.forcing + + @property + def forcing_full(self): + """The forcing vector of the system, augmented by the kinematic + differential equations in explicit or implicit form.""" + return self.eom_method.forcing_full + + def validate_system(self, eom_method=KanesMethod, check_duplicates=False): + """Validates the system using some basic checks. + + Explanation + =========== + + This method validates the system based on the following checks: + + - The number of dependent generalized coordinates should equal the + number of holonomic constraints. + - All generalized coordinates defined by the joints should also be known + to the system. + - If ``KanesMethod`` is used as a ``eom_method``: + - All generalized speeds and kinematic differential equations + defined by the joints should also be known to the system. + - The number of dependent generalized speeds should equal the number + of velocity constraints. + - The number of generalized coordinates should be less than or equal + to the number of generalized speeds. + - The number of generalized coordinates should equal the number of + kinematic differential equations. + - If ``LagrangesMethod`` is used as ``eom_method``: + - There should not be any generalized speeds that are not + derivatives of the generalized coordinates (this includes the + generalized speeds defined by the joints). + + Parameters + ========== + + eom_method : subclass of KanesMethod or LagrangesMethod + Backend class that will be used for forming the equations of motion. + There are different checks for the different backends. The default + is ``KanesMethod``. + check_duplicates : bool + Boolean whether the system should be checked for duplicate + definitions. The default is False, because duplicates are already + checked when adding objects to the system. + + Notes + ===== + + This method is not guaranteed to be backwards compatible as it may + improve over time. The method can become both more and less strict in + certain areas. However a well-defined system should always pass all + these tests. + + """ + msgs = [] + # Save some data in variables + n_hc = self.holonomic_constraints.shape[0] + n_vc = self.velocity_constraints.shape[0] + n_q_dep, n_u_dep = self.q_dep.shape[0], self.u_dep.shape[0] + q_set, u_set = set(self.q), set(self.u) + n_q, n_u = len(q_set), len(u_set) + # Check number of holonomic constraints + if n_q_dep != n_hc: + msgs.append(filldedent(f""" + The number of dependent generalized coordinates {n_q_dep} should be + equal to the number of holonomic constraints {n_hc}.""")) + # Check if all joint coordinates and speeds are present + missing_q = set() + for joint in self.joints: + missing_q.update(set(joint.coordinates).difference(q_set)) + if missing_q: + msgs.append(filldedent(f""" + The generalized coordinates {missing_q} used in joints are not added + to the system.""")) + # Method dependent checks + if issubclass(eom_method, KanesMethod): + n_kdes = len(self.kdes) + missing_kdes, missing_u = set(), set() + for joint in self.joints: + missing_u.update(set(joint.speeds).difference(u_set)) + missing_kdes.update(set(joint.kdes).difference( + self.kdes[:] + (-self.kdes)[:])) + if missing_u: + msgs.append(filldedent(f""" + The generalized speeds {missing_u} used in joints are not added + to the system.""")) + if missing_kdes: + msgs.append(filldedent(f""" + The kinematic differential equations {missing_kdes} used in + joints are not added to the system.""")) + if n_u_dep != n_vc: + msgs.append(filldedent(f""" + The number of dependent generalized speeds {n_u_dep} should be + equal to the number of velocity constraints {n_vc}.""")) + if n_q > n_u: + msgs.append(filldedent(f""" + The number of generalized coordinates {n_q} should be less than + or equal to the number of generalized speeds {n_u}.""")) + if n_u != n_kdes: + msgs.append(filldedent(f""" + The number of generalized speeds {n_u} should be equal to the + number of kinematic differential equations {n_kdes}.""")) + elif issubclass(eom_method, LagrangesMethod): + not_qdots = set(self.u).difference(self.q.diff(dynamicsymbols._t)) + for joint in self.joints: + not_qdots.update(set( + joint.speeds).difference(self.q.diff(dynamicsymbols._t))) + if not_qdots: + msgs.append(filldedent(f""" + The generalized speeds {not_qdots} are not supported by this + method. Only derivatives of the generalized coordinates are + supported. If these symbols are used in your expressions, then + this will result in wrong equations of motion.""")) + if self.u_aux: + msgs.append(filldedent(f""" + This method does not support auxiliary speeds. If these symbols + are used in your expressions, then this will result in wrong + equations of motion. The auxiliary speeds are {self.u_aux}.""")) + else: + raise NotImplementedError(f'{eom_method} has not been implemented.') + if check_duplicates: # Should be redundant + duplicates_to_check = [('generalized coordinates', self.q), + ('generalized speeds', self.u), + ('auxiliary speeds', self.u_aux), + ('bodies', self.bodies), + ('joints', self.joints)] + for name, lst in duplicates_to_check: + seen = set() + duplicates = {x for x in lst if x in seen or seen.add(x)} + if duplicates: + msgs.append(filldedent(f""" + The {name} {duplicates} exist multiple times within the + system.""")) + if msgs: + raise ValueError('\n'.join(msgs)) + + +class SymbolicSystem: + """SymbolicSystem is a class that contains all the information about a + system in a symbolic format such as the equations of motions and the bodies + and loads in the system. + + There are three ways that the equations of motion can be described for + Symbolic System: + + + [1] Explicit form where the kinematics and dynamics are combined + x' = F_1(x, t, r, p) + + [2] Implicit form where the kinematics and dynamics are combined + M_2(x, p) x' = F_2(x, t, r, p) + + [3] Implicit form where the kinematics and dynamics are separate + M_3(q, p) u' = F_3(q, u, t, r, p) + q' = G(q, u, t, r, p) + + where + + x : states, e.g. [q, u] + t : time + r : specified (exogenous) inputs + p : constants + q : generalized coordinates + u : generalized speeds + F_1 : right hand side of the combined equations in explicit form + F_2 : right hand side of the combined equations in implicit form + F_3 : right hand side of the dynamical equations in implicit form + M_2 : mass matrix of the combined equations in implicit form + M_3 : mass matrix of the dynamical equations in implicit form + G : right hand side of the kinematical differential equations + + Parameters + ========== + + coord_states : ordered iterable of functions of time + This input will either be a collection of the coordinates or states + of the system depending on whether or not the speeds are also + given. If speeds are specified this input will be assumed to + be the coordinates otherwise this input will be assumed to + be the states. + + right_hand_side : Matrix + This variable is the right hand side of the equations of motion in + any of the forms. The specific form will be assumed depending on + whether a mass matrix or coordinate derivatives are given. + + speeds : ordered iterable of functions of time, optional + This is a collection of the generalized speeds of the system. If + given it will be assumed that the first argument (coord_states) + will represent the generalized coordinates of the system. + + mass_matrix : Matrix, optional + The matrix of the implicit forms of the equations of motion (forms + [2] and [3]). The distinction between the forms is determined by + whether or not the coordinate derivatives are passed in. If + they are given form [3] will be assumed otherwise form [2] is + assumed. + + coordinate_derivatives : Matrix, optional + The right hand side of the kinematical equations in explicit form. + If given it will be assumed that the equations of motion are being + entered in form [3]. + + alg_con : Iterable, optional + The indexes of the rows in the equations of motion that contain + algebraic constraints instead of differential equations. If the + equations are input in form [3], it will be assumed the indexes are + referencing the mass_matrix/right_hand_side combination and not the + coordinate_derivatives. + + output_eqns : Dictionary, optional + Any output equations that are desired to be tracked are stored in a + dictionary where the key corresponds to the name given for the + specific equation and the value is the equation itself in symbolic + form + + coord_idxs : Iterable, optional + If coord_states corresponds to the states rather than the + coordinates this variable will tell SymbolicSystem which indexes of + the states correspond to generalized coordinates. + + speed_idxs : Iterable, optional + If coord_states corresponds to the states rather than the + coordinates this variable will tell SymbolicSystem which indexes of + the states correspond to generalized speeds. + + bodies : iterable of Body/Rigidbody objects, optional + Iterable containing the bodies of the system + + loads : iterable of load instances (described below), optional + Iterable containing the loads of the system where forces are given + by (point of application, force vector) and torques are given by + (reference frame acting upon, torque vector). Ex [(point, force), + (ref_frame, torque)] + + Attributes + ========== + + coordinates : Matrix, shape(n, 1) + This is a matrix containing the generalized coordinates of the system + + speeds : Matrix, shape(m, 1) + This is a matrix containing the generalized speeds of the system + + states : Matrix, shape(o, 1) + This is a matrix containing the state variables of the system + + alg_con : List + This list contains the indices of the algebraic constraints in the + combined equations of motion. The presence of these constraints + requires that a DAE solver be used instead of an ODE solver. + If the system is given in form [3] the alg_con variable will be + adjusted such that it is a representation of the combined kinematics + and dynamics thus make sure it always matches the mass matrix + entered. + + dyn_implicit_mat : Matrix, shape(m, m) + This is the M matrix in form [3] of the equations of motion (the mass + matrix or generalized inertia matrix of the dynamical equations of + motion in implicit form). + + dyn_implicit_rhs : Matrix, shape(m, 1) + This is the F vector in form [3] of the equations of motion (the right + hand side of the dynamical equations of motion in implicit form). + + comb_implicit_mat : Matrix, shape(o, o) + This is the M matrix in form [2] of the equations of motion. + This matrix contains a block diagonal structure where the top + left block (the first rows) represent the matrix in the + implicit form of the kinematical equations and the bottom right + block (the last rows) represent the matrix in the implicit form + of the dynamical equations. + + comb_implicit_rhs : Matrix, shape(o, 1) + This is the F vector in form [2] of the equations of motion. The top + part of the vector represents the right hand side of the implicit form + of the kinemaical equations and the bottom of the vector represents the + right hand side of the implicit form of the dynamical equations of + motion. + + comb_explicit_rhs : Matrix, shape(o, 1) + This vector represents the right hand side of the combined equations of + motion in explicit form (form [1] from above). + + kin_explicit_rhs : Matrix, shape(m, 1) + This is the right hand side of the explicit form of the kinematical + equations of motion as can be seen in form [3] (the G matrix). + + output_eqns : Dictionary + If output equations were given they are stored in a dictionary where + the key corresponds to the name given for the specific equation and + the value is the equation itself in symbolic form + + bodies : Tuple + If the bodies in the system were given they are stored in a tuple for + future access + + loads : Tuple + If the loads in the system were given they are stored in a tuple for + future access. This includes forces and torques where forces are given + by (point of application, force vector) and torques are given by + (reference frame acted upon, torque vector). + + Example + ======= + + As a simple example, the dynamics of a simple pendulum will be input into a + SymbolicSystem object manually. First some imports will be needed and then + symbols will be set up for the length of the pendulum (l), mass at the end + of the pendulum (m), and a constant for gravity (g). :: + + >>> from sympy import Matrix, sin, symbols + >>> from sympy.physics.mechanics import dynamicsymbols, SymbolicSystem + >>> l, m, g = symbols('l m g') + + The system will be defined by an angle of theta from the vertical and a + generalized speed of omega will be used where omega = theta_dot. :: + + >>> theta, omega = dynamicsymbols('theta omega') + + Now the equations of motion are ready to be formed and passed to the + SymbolicSystem object. :: + + >>> kin_explicit_rhs = Matrix([omega]) + >>> dyn_implicit_mat = Matrix([l**2 * m]) + >>> dyn_implicit_rhs = Matrix([-g * l * m * sin(theta)]) + >>> symsystem = SymbolicSystem([theta], dyn_implicit_rhs, [omega], + ... dyn_implicit_mat) + + Notes + ===== + + m : number of generalized speeds + n : number of generalized coordinates + o : number of states + + """ + + def __init__(self, coord_states, right_hand_side, speeds=None, + mass_matrix=None, coordinate_derivatives=None, alg_con=None, + output_eqns={}, coord_idxs=None, speed_idxs=None, bodies=None, + loads=None): + """Initializes a SymbolicSystem object""" + + # Extract information on speeds, coordinates and states + if speeds is None: + self._states = Matrix(coord_states) + + if coord_idxs is None: + self._coordinates = None + else: + coords = [coord_states[i] for i in coord_idxs] + self._coordinates = Matrix(coords) + + if speed_idxs is None: + self._speeds = None + else: + speeds_inter = [coord_states[i] for i in speed_idxs] + self._speeds = Matrix(speeds_inter) + else: + self._coordinates = Matrix(coord_states) + self._speeds = Matrix(speeds) + self._states = self._coordinates.col_join(self._speeds) + + # Extract equations of motion form + if coordinate_derivatives is not None: + self._kin_explicit_rhs = coordinate_derivatives + self._dyn_implicit_rhs = right_hand_side + self._dyn_implicit_mat = mass_matrix + self._comb_implicit_rhs = None + self._comb_implicit_mat = None + self._comb_explicit_rhs = None + elif mass_matrix is not None: + self._kin_explicit_rhs = None + self._dyn_implicit_rhs = None + self._dyn_implicit_mat = None + self._comb_implicit_rhs = right_hand_side + self._comb_implicit_mat = mass_matrix + self._comb_explicit_rhs = None + else: + self._kin_explicit_rhs = None + self._dyn_implicit_rhs = None + self._dyn_implicit_mat = None + self._comb_implicit_rhs = None + self._comb_implicit_mat = None + self._comb_explicit_rhs = right_hand_side + + # Set the remainder of the inputs as instance attributes + if alg_con is not None and coordinate_derivatives is not None: + alg_con = [i + len(coordinate_derivatives) for i in alg_con] + self._alg_con = alg_con + self.output_eqns = output_eqns + + # Change the body and loads iterables to tuples if they are not tuples + # already + if not isinstance(bodies, tuple) and bodies is not None: + bodies = tuple(bodies) + if not isinstance(loads, tuple) and loads is not None: + loads = tuple(loads) + self._bodies = bodies + self._loads = loads + + @property + def coordinates(self): + """Returns the column matrix of the generalized coordinates""" + if self._coordinates is None: + raise AttributeError("The coordinates were not specified.") + else: + return self._coordinates + + @property + def speeds(self): + """Returns the column matrix of generalized speeds""" + if self._speeds is None: + raise AttributeError("The speeds were not specified.") + else: + return self._speeds + + @property + def states(self): + """Returns the column matrix of the state variables""" + return self._states + + @property + def alg_con(self): + """Returns a list with the indices of the rows containing algebraic + constraints in the combined form of the equations of motion""" + return self._alg_con + + @property + def dyn_implicit_mat(self): + """Returns the matrix, M, corresponding to the dynamic equations in + implicit form, M x' = F, where the kinematical equations are not + included""" + if self._dyn_implicit_mat is None: + raise AttributeError("dyn_implicit_mat is not specified for " + "equations of motion form [1] or [2].") + else: + return self._dyn_implicit_mat + + @property + def dyn_implicit_rhs(self): + """Returns the column matrix, F, corresponding to the dynamic equations + in implicit form, M x' = F, where the kinematical equations are not + included""" + if self._dyn_implicit_rhs is None: + raise AttributeError("dyn_implicit_rhs is not specified for " + "equations of motion form [1] or [2].") + else: + return self._dyn_implicit_rhs + + @property + def comb_implicit_mat(self): + """Returns the matrix, M, corresponding to the equations of motion in + implicit form (form [2]), M x' = F, where the kinematical equations are + included""" + if self._comb_implicit_mat is None: + if self._dyn_implicit_mat is not None: + num_kin_eqns = len(self._kin_explicit_rhs) + num_dyn_eqns = len(self._dyn_implicit_rhs) + zeros1 = zeros(num_kin_eqns, num_dyn_eqns) + zeros2 = zeros(num_dyn_eqns, num_kin_eqns) + inter1 = eye(num_kin_eqns).row_join(zeros1) + inter2 = zeros2.row_join(self._dyn_implicit_mat) + self._comb_implicit_mat = inter1.col_join(inter2) + return self._comb_implicit_mat + else: + raise AttributeError("comb_implicit_mat is not specified for " + "equations of motion form [1].") + else: + return self._comb_implicit_mat + + @property + def comb_implicit_rhs(self): + """Returns the column matrix, F, corresponding to the equations of + motion in implicit form (form [2]), M x' = F, where the kinematical + equations are included""" + if self._comb_implicit_rhs is None: + if self._dyn_implicit_rhs is not None: + kin_inter = self._kin_explicit_rhs + dyn_inter = self._dyn_implicit_rhs + self._comb_implicit_rhs = kin_inter.col_join(dyn_inter) + return self._comb_implicit_rhs + else: + raise AttributeError("comb_implicit_mat is not specified for " + "equations of motion in form [1].") + else: + return self._comb_implicit_rhs + + def compute_explicit_form(self): + """If the explicit right hand side of the combined equations of motion + is to provided upon initialization, this method will calculate it. This + calculation can potentially take awhile to compute.""" + if self._comb_explicit_rhs is not None: + raise AttributeError("comb_explicit_rhs is already formed.") + + inter1 = getattr(self, 'kin_explicit_rhs', None) + if inter1 is not None: + inter2 = self._dyn_implicit_mat.LUsolve(self._dyn_implicit_rhs) + out = inter1.col_join(inter2) + else: + out = self._comb_implicit_mat.LUsolve(self._comb_implicit_rhs) + + self._comb_explicit_rhs = out + + @property + def comb_explicit_rhs(self): + """Returns the right hand side of the equations of motion in explicit + form, x' = F, where the kinematical equations are included""" + if self._comb_explicit_rhs is None: + raise AttributeError("Please run .combute_explicit_form before " + "attempting to access comb_explicit_rhs.") + else: + return self._comb_explicit_rhs + + @property + def kin_explicit_rhs(self): + """Returns the right hand side of the kinematical equations in explicit + form, q' = G""" + if self._kin_explicit_rhs is None: + raise AttributeError("kin_explicit_rhs is not specified for " + "equations of motion form [1] or [2].") + else: + return self._kin_explicit_rhs + + def dynamic_symbols(self): + """Returns a column matrix containing all of the symbols in the system + that depend on time""" + # Create a list of all of the expressions in the equations of motion + if self._comb_explicit_rhs is None: + eom_expressions = (self.comb_implicit_mat[:] + + self.comb_implicit_rhs[:]) + else: + eom_expressions = (self._comb_explicit_rhs[:]) + + functions_of_time = set() + for expr in eom_expressions: + functions_of_time = functions_of_time.union( + find_dynamicsymbols(expr)) + functions_of_time = functions_of_time.union(self._states) + + return tuple(functions_of_time) + + def constant_symbols(self): + """Returns a column matrix containing all of the symbols in the system + that do not depend on time""" + # Create a list of all of the expressions in the equations of motion + if self._comb_explicit_rhs is None: + eom_expressions = (self.comb_implicit_mat[:] + + self.comb_implicit_rhs[:]) + else: + eom_expressions = (self._comb_explicit_rhs[:]) + + constants = set() + for expr in eom_expressions: + constants = constants.union(expr.free_symbols) + constants.remove(dynamicsymbols._t) + + return tuple(constants) + + @property + def bodies(self): + """Returns the bodies in the system""" + if self._bodies is None: + raise AttributeError("bodies were not specified for the system.") + else: + return self._bodies + + @property + def loads(self): + """Returns the loads in the system""" + if self._loads is None: + raise AttributeError("loads were not specified for the system.") + else: + return self._loads diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_actuator.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_actuator.py new file mode 100644 index 0000000000000000000000000000000000000000..5d69bccfe7a86d7555242a64923d77cb52cade88 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_actuator.py @@ -0,0 +1,1084 @@ +"""Tests for the ``sympy.physics.mechanics.actuator.py`` module.""" + +import pytest + +from sympy import ( + S, + Matrix, + Symbol, + SympifyError, + sqrt, + Abs, + symbols, + exp, + sign, +) +from sympy.physics.mechanics import ( + ActuatorBase, + Force, + ForceActuator, + KanesMethod, + LinearDamper, + LinearPathway, + LinearSpring, + Particle, + PinJoint, + Point, + ReferenceFrame, + RigidBody, + TorqueActuator, + Vector, + dynamicsymbols, + DuffingSpring, + CoulombKineticFriction, +) + +from sympy.core.expr import Expr as ExprType + +target = RigidBody('target') +reaction = RigidBody('reaction') + + +class TestForceActuator: + + @pytest.fixture(autouse=True) + def _linear_pathway_fixture(self): + self.force = Symbol('F') + self.pA = Point('pA') + self.pB = Point('pB') + self.pathway = LinearPathway(self.pA, self.pB) + self.q1 = dynamicsymbols('q1') + self.q2 = dynamicsymbols('q2') + self.q3 = dynamicsymbols('q3') + self.q1d = dynamicsymbols('q1', 1) + self.q2d = dynamicsymbols('q2', 1) + self.q3d = dynamicsymbols('q3', 1) + self.N = ReferenceFrame('N') + + def test_is_actuator_base_subclass(self): + assert issubclass(ForceActuator, ActuatorBase) + + @pytest.mark.parametrize( + 'force, expected_force', + [ + (1, S.One), + (S.One, S.One), + (Symbol('F'), Symbol('F')), + (dynamicsymbols('F'), dynamicsymbols('F')), + (Symbol('F')**2 + Symbol('F'), Symbol('F')**2 + Symbol('F')), + ] + ) + def test_valid_constructor_force(self, force, expected_force): + instance = ForceActuator(force, self.pathway) + assert isinstance(instance, ForceActuator) + assert hasattr(instance, 'force') + assert isinstance(instance.force, ExprType) + assert instance.force == expected_force + + @pytest.mark.parametrize('force', [None, 'F']) + def test_invalid_constructor_force_not_sympifyable(self, force): + with pytest.raises(SympifyError): + _ = ForceActuator(force, self.pathway) + + @pytest.mark.parametrize( + 'pathway', + [ + LinearPathway(Point('pA'), Point('pB')), + ] + ) + def test_valid_constructor_pathway(self, pathway): + instance = ForceActuator(self.force, pathway) + assert isinstance(instance, ForceActuator) + assert hasattr(instance, 'pathway') + assert isinstance(instance.pathway, LinearPathway) + assert instance.pathway == pathway + + def test_invalid_constructor_pathway_not_pathway_base(self): + with pytest.raises(TypeError): + _ = ForceActuator(self.force, None) + + @pytest.mark.parametrize( + 'property_name, fixture_attr_name', + [ + ('force', 'force'), + ('pathway', 'pathway'), + ] + ) + def test_properties_are_immutable(self, property_name, fixture_attr_name): + instance = ForceActuator(self.force, self.pathway) + value = getattr(self, fixture_attr_name) + with pytest.raises(AttributeError): + setattr(instance, property_name, value) + + def test_repr(self): + actuator = ForceActuator(self.force, self.pathway) + expected = "ForceActuator(F, LinearPathway(pA, pB))" + assert repr(actuator) == expected + + def test_to_loads_static_pathway(self): + self.pB.set_pos(self.pA, 2*self.N.x) + actuator = ForceActuator(self.force, self.pathway) + expected = [ + (self.pA, - self.force*self.N.x), + (self.pB, self.force*self.N.x), + ] + assert actuator.to_loads() == expected + + def test_to_loads_2D_pathway(self): + self.pB.set_pos(self.pA, 2*self.q1*self.N.x) + actuator = ForceActuator(self.force, self.pathway) + expected = [ + (self.pA, - self.force*(self.q1/sqrt(self.q1**2))*self.N.x), + (self.pB, self.force*(self.q1/sqrt(self.q1**2))*self.N.x), + ] + assert actuator.to_loads() == expected + + def test_to_loads_3D_pathway(self): + self.pB.set_pos( + self.pA, + self.q1*self.N.x - self.q2*self.N.y + 2*self.q3*self.N.z, + ) + actuator = ForceActuator(self.force, self.pathway) + length = sqrt(self.q1**2 + self.q2**2 + 4*self.q3**2) + pO_force = ( + - self.force*self.q1*self.N.x/length + + self.force*self.q2*self.N.y/length + - 2*self.force*self.q3*self.N.z/length + ) + pI_force = ( + self.force*self.q1*self.N.x/length + - self.force*self.q2*self.N.y/length + + 2*self.force*self.q3*self.N.z/length + ) + expected = [ + (self.pA, pO_force), + (self.pB, pI_force), + ] + assert actuator.to_loads() == expected + + +class TestLinearSpring: + + @pytest.fixture(autouse=True) + def _linear_spring_fixture(self): + self.stiffness = Symbol('k') + self.l = Symbol('l') + self.pA = Point('pA') + self.pB = Point('pB') + self.pathway = LinearPathway(self.pA, self.pB) + self.q = dynamicsymbols('q') + self.N = ReferenceFrame('N') + + def test_is_force_actuator_subclass(self): + assert issubclass(LinearSpring, ForceActuator) + + def test_is_actuator_base_subclass(self): + assert issubclass(LinearSpring, ActuatorBase) + + @pytest.mark.parametrize( + ( + 'stiffness, ' + 'expected_stiffness, ' + 'equilibrium_length, ' + 'expected_equilibrium_length, ' + 'force' + ), + [ + ( + 1, + S.One, + 0, + S.Zero, + -sqrt(dynamicsymbols('q')**2), + ), + ( + Symbol('k'), + Symbol('k'), + 0, + S.Zero, + -Symbol('k')*sqrt(dynamicsymbols('q')**2), + ), + ( + Symbol('k'), + Symbol('k'), + S.Zero, + S.Zero, + -Symbol('k')*sqrt(dynamicsymbols('q')**2), + ), + ( + Symbol('k'), + Symbol('k'), + Symbol('l'), + Symbol('l'), + -Symbol('k')*(sqrt(dynamicsymbols('q')**2) - Symbol('l')), + ), + ] + ) + def test_valid_constructor( + self, + stiffness, + expected_stiffness, + equilibrium_length, + expected_equilibrium_length, + force, + ): + self.pB.set_pos(self.pA, self.q*self.N.x) + spring = LinearSpring(stiffness, self.pathway, equilibrium_length) + + assert isinstance(spring, LinearSpring) + + assert hasattr(spring, 'stiffness') + assert isinstance(spring.stiffness, ExprType) + assert spring.stiffness == expected_stiffness + + assert hasattr(spring, 'pathway') + assert isinstance(spring.pathway, LinearPathway) + assert spring.pathway == self.pathway + + assert hasattr(spring, 'equilibrium_length') + assert isinstance(spring.equilibrium_length, ExprType) + assert spring.equilibrium_length == expected_equilibrium_length + + assert hasattr(spring, 'force') + assert isinstance(spring.force, ExprType) + assert spring.force == force + + @pytest.mark.parametrize('stiffness', [None, 'k']) + def test_invalid_constructor_stiffness_not_sympifyable(self, stiffness): + with pytest.raises(SympifyError): + _ = LinearSpring(stiffness, self.pathway, self.l) + + def test_invalid_constructor_pathway_not_pathway_base(self): + with pytest.raises(TypeError): + _ = LinearSpring(self.stiffness, None, self.l) + + @pytest.mark.parametrize('equilibrium_length', [None, 'l']) + def test_invalid_constructor_equilibrium_length_not_sympifyable( + self, + equilibrium_length, + ): + with pytest.raises(SympifyError): + _ = LinearSpring(self.stiffness, self.pathway, equilibrium_length) + + @pytest.mark.parametrize( + 'property_name, fixture_attr_name', + [ + ('stiffness', 'stiffness'), + ('pathway', 'pathway'), + ('equilibrium_length', 'l'), + ] + ) + def test_properties_are_immutable(self, property_name, fixture_attr_name): + spring = LinearSpring(self.stiffness, self.pathway, self.l) + value = getattr(self, fixture_attr_name) + with pytest.raises(AttributeError): + setattr(spring, property_name, value) + + @pytest.mark.parametrize( + 'equilibrium_length, expected', + [ + (S.Zero, 'LinearSpring(k, LinearPathway(pA, pB))'), + ( + Symbol('l'), + 'LinearSpring(k, LinearPathway(pA, pB), equilibrium_length=l)', + ), + ] + ) + def test_repr(self, equilibrium_length, expected): + self.pB.set_pos(self.pA, self.q*self.N.x) + spring = LinearSpring(self.stiffness, self.pathway, equilibrium_length) + assert repr(spring) == expected + + def test_to_loads(self): + self.pB.set_pos(self.pA, self.q*self.N.x) + spring = LinearSpring(self.stiffness, self.pathway, self.l) + normal = self.q/sqrt(self.q**2)*self.N.x + pA_force = self.stiffness*(sqrt(self.q**2) - self.l)*normal + pB_force = -self.stiffness*(sqrt(self.q**2) - self.l)*normal + expected = [Force(self.pA, pA_force), Force(self.pB, pB_force)] + loads = spring.to_loads() + + for load, (point, vector) in zip(loads, expected): + assert isinstance(load, Force) + assert load.point == point + assert (load.vector - vector).simplify() == 0 + + +class TestLinearDamper: + + @pytest.fixture(autouse=True) + def _linear_damper_fixture(self): + self.damping = Symbol('c') + self.l = Symbol('l') + self.pA = Point('pA') + self.pB = Point('pB') + self.pathway = LinearPathway(self.pA, self.pB) + self.q = dynamicsymbols('q') + self.dq = dynamicsymbols('q', 1) + self.u = dynamicsymbols('u') + self.N = ReferenceFrame('N') + + def test_is_force_actuator_subclass(self): + assert issubclass(LinearDamper, ForceActuator) + + def test_is_actuator_base_subclass(self): + assert issubclass(LinearDamper, ActuatorBase) + + def test_valid_constructor(self): + self.pB.set_pos(self.pA, self.q*self.N.x) + damper = LinearDamper(self.damping, self.pathway) + + assert isinstance(damper, LinearDamper) + + assert hasattr(damper, 'damping') + assert isinstance(damper.damping, ExprType) + assert damper.damping == self.damping + + assert hasattr(damper, 'pathway') + assert isinstance(damper.pathway, LinearPathway) + assert damper.pathway == self.pathway + + def test_valid_constructor_force(self): + self.pB.set_pos(self.pA, self.q*self.N.x) + damper = LinearDamper(self.damping, self.pathway) + + expected_force = -self.damping*sqrt(self.q**2)*self.dq/self.q + assert hasattr(damper, 'force') + assert isinstance(damper.force, ExprType) + assert damper.force == expected_force + + @pytest.mark.parametrize('damping', [None, 'c']) + def test_invalid_constructor_damping_not_sympifyable(self, damping): + with pytest.raises(SympifyError): + _ = LinearDamper(damping, self.pathway) + + def test_invalid_constructor_pathway_not_pathway_base(self): + with pytest.raises(TypeError): + _ = LinearDamper(self.damping, None) + + @pytest.mark.parametrize( + 'property_name, fixture_attr_name', + [ + ('damping', 'damping'), + ('pathway', 'pathway'), + ] + ) + def test_properties_are_immutable(self, property_name, fixture_attr_name): + damper = LinearDamper(self.damping, self.pathway) + value = getattr(self, fixture_attr_name) + with pytest.raises(AttributeError): + setattr(damper, property_name, value) + + def test_repr(self): + self.pB.set_pos(self.pA, self.q*self.N.x) + damper = LinearDamper(self.damping, self.pathway) + expected = 'LinearDamper(c, LinearPathway(pA, pB))' + assert repr(damper) == expected + + def test_to_loads(self): + self.pB.set_pos(self.pA, self.q*self.N.x) + damper = LinearDamper(self.damping, self.pathway) + direction = self.q**2/self.q**2*self.N.x + pA_force = self.damping*self.dq*direction + pB_force = -self.damping*self.dq*direction + expected = [Force(self.pA, pA_force), Force(self.pB, pB_force)] + assert damper.to_loads() == expected + + +class TestForcedMassSpringDamperModel(): + r"""A single degree of freedom translational forced mass-spring-damper. + + Notes + ===== + + This system is well known to have the governing equation: + + .. math:: + m \ddot{x} = F - k x - c \dot{x} + + where $F$ is an externally applied force, $m$ is the mass of the particle + to which the spring and damper are attached, $k$ is the spring's stiffness, + $c$ is the dampers damping coefficient, and $x$ is the generalized + coordinate representing the system's single (translational) degree of + freedom. + + """ + + @pytest.fixture(autouse=True) + def _force_mass_spring_damper_model_fixture(self): + self.m = Symbol('m') + self.k = Symbol('k') + self.c = Symbol('c') + self.F = Symbol('F') + + self.q = dynamicsymbols('q') + self.dq = dynamicsymbols('q', 1) + self.u = dynamicsymbols('u') + + self.frame = ReferenceFrame('N') + self.origin = Point('pO') + self.origin.set_vel(self.frame, 0) + + self.attachment = Point('pA') + self.attachment.set_pos(self.origin, self.q*self.frame.x) + + self.mass = Particle('mass', self.attachment, self.m) + self.pathway = LinearPathway(self.origin, self.attachment) + + self.kanes_method = KanesMethod( + self.frame, + q_ind=[self.q], + u_ind=[self.u], + kd_eqs=[self.dq - self.u], + ) + self.bodies = [self.mass] + + self.mass_matrix = Matrix([[self.m]]) + self.forcing = Matrix([[self.F - self.c*self.u - self.k*self.q]]) + + def test_force_acuator(self): + stiffness = -self.k*self.pathway.length + spring = ForceActuator(stiffness, self.pathway) + damping = -self.c*self.pathway.extension_velocity + damper = ForceActuator(damping, self.pathway) + + loads = [ + (self.attachment, self.F*self.frame.x), + *spring.to_loads(), + *damper.to_loads(), + ] + self.kanes_method.kanes_equations(self.bodies, loads) + + assert self.kanes_method.mass_matrix == self.mass_matrix + assert self.kanes_method.forcing == self.forcing + + def test_linear_spring_linear_damper(self): + spring = LinearSpring(self.k, self.pathway) + damper = LinearDamper(self.c, self.pathway) + + loads = [ + (self.attachment, self.F*self.frame.x), + *spring.to_loads(), + *damper.to_loads(), + ] + self.kanes_method.kanes_equations(self.bodies, loads) + + assert self.kanes_method.mass_matrix == self.mass_matrix + assert self.kanes_method.forcing == self.forcing + + +class TestTorqueActuator: + + @pytest.fixture(autouse=True) + def _torque_actuator_fixture(self): + self.torque = Symbol('T') + self.N = ReferenceFrame('N') + self.A = ReferenceFrame('A') + self.axis = self.N.z + self.target = RigidBody('target', frame=self.N) + self.reaction = RigidBody('reaction', frame=self.A) + + def test_is_actuator_base_subclass(self): + assert issubclass(TorqueActuator, ActuatorBase) + + @pytest.mark.parametrize( + 'torque', + [ + Symbol('T'), + dynamicsymbols('T'), + Symbol('T')**2 + Symbol('T'), + ] + ) + @pytest.mark.parametrize( + 'target_frame, reaction_frame', + [ + (target.frame, reaction.frame), + (target, reaction.frame), + (target.frame, reaction), + (target, reaction), + ] + ) + def test_valid_constructor_with_reaction( + self, + torque, + target_frame, + reaction_frame, + ): + instance = TorqueActuator( + torque, + self.axis, + target_frame, + reaction_frame, + ) + assert isinstance(instance, TorqueActuator) + + assert hasattr(instance, 'torque') + assert isinstance(instance.torque, ExprType) + assert instance.torque == torque + + assert hasattr(instance, 'axis') + assert isinstance(instance.axis, Vector) + assert instance.axis == self.axis + + assert hasattr(instance, 'target_frame') + assert isinstance(instance.target_frame, ReferenceFrame) + assert instance.target_frame == target.frame + + assert hasattr(instance, 'reaction_frame') + assert isinstance(instance.reaction_frame, ReferenceFrame) + assert instance.reaction_frame == reaction.frame + + @pytest.mark.parametrize( + 'torque', + [ + Symbol('T'), + dynamicsymbols('T'), + Symbol('T')**2 + Symbol('T'), + ] + ) + @pytest.mark.parametrize('target_frame', [target.frame, target]) + def test_valid_constructor_without_reaction(self, torque, target_frame): + instance = TorqueActuator(torque, self.axis, target_frame) + assert isinstance(instance, TorqueActuator) + + assert hasattr(instance, 'torque') + assert isinstance(instance.torque, ExprType) + assert instance.torque == torque + + assert hasattr(instance, 'axis') + assert isinstance(instance.axis, Vector) + assert instance.axis == self.axis + + assert hasattr(instance, 'target_frame') + assert isinstance(instance.target_frame, ReferenceFrame) + assert instance.target_frame == target.frame + + assert hasattr(instance, 'reaction_frame') + assert instance.reaction_frame is None + + @pytest.mark.parametrize('torque', [None, 'T']) + def test_invalid_constructor_torque_not_sympifyable(self, torque): + with pytest.raises(SympifyError): + _ = TorqueActuator(torque, self.axis, self.target) + + @pytest.mark.parametrize('axis', [Symbol('a'), dynamicsymbols('a')]) + def test_invalid_constructor_axis_not_vector(self, axis): + with pytest.raises(TypeError): + _ = TorqueActuator(self.torque, axis, self.target, self.reaction) + + @pytest.mark.parametrize( + 'frames', + [ + (None, ReferenceFrame('child')), + (ReferenceFrame('parent'), True), + (None, RigidBody('child')), + (RigidBody('parent'), True), + ] + ) + def test_invalid_constructor_frames_not_frame(self, frames): + with pytest.raises(TypeError): + _ = TorqueActuator(self.torque, self.axis, *frames) + + @pytest.mark.parametrize( + 'property_name, fixture_attr_name', + [ + ('torque', 'torque'), + ('axis', 'axis'), + ('target_frame', 'target'), + ('reaction_frame', 'reaction'), + ] + ) + def test_properties_are_immutable(self, property_name, fixture_attr_name): + actuator = TorqueActuator( + self.torque, + self.axis, + self.target, + self.reaction, + ) + value = getattr(self, fixture_attr_name) + with pytest.raises(AttributeError): + setattr(actuator, property_name, value) + + def test_repr_without_reaction(self): + actuator = TorqueActuator(self.torque, self.axis, self.target) + expected = 'TorqueActuator(T, axis=N.z, target_frame=N)' + assert repr(actuator) == expected + + def test_repr_with_reaction(self): + actuator = TorqueActuator( + self.torque, + self.axis, + self.target, + self.reaction, + ) + expected = 'TorqueActuator(T, axis=N.z, target_frame=N, reaction_frame=A)' + assert repr(actuator) == expected + + def test_at_pin_joint_constructor(self): + pin_joint = PinJoint( + 'pin', + self.target, + self.reaction, + coordinates=dynamicsymbols('q'), + speeds=dynamicsymbols('u'), + parent_interframe=self.N, + joint_axis=self.axis, + ) + instance = TorqueActuator.at_pin_joint(self.torque, pin_joint) + assert isinstance(instance, TorqueActuator) + + assert hasattr(instance, 'torque') + assert isinstance(instance.torque, ExprType) + assert instance.torque == self.torque + + assert hasattr(instance, 'axis') + assert isinstance(instance.axis, Vector) + assert instance.axis == self.axis + + assert hasattr(instance, 'target_frame') + assert isinstance(instance.target_frame, ReferenceFrame) + assert instance.target_frame == self.A + + assert hasattr(instance, 'reaction_frame') + assert isinstance(instance.reaction_frame, ReferenceFrame) + assert instance.reaction_frame == self.N + + def test_at_pin_joint_pin_joint_not_pin_joint_invalid(self): + with pytest.raises(TypeError): + _ = TorqueActuator.at_pin_joint(self.torque, Symbol('pin')) + + def test_to_loads_without_reaction(self): + actuator = TorqueActuator(self.torque, self.axis, self.target) + expected = [ + (self.N, self.torque*self.axis), + ] + assert actuator.to_loads() == expected + + def test_to_loads_with_reaction(self): + actuator = TorqueActuator( + self.torque, + self.axis, + self.target, + self.reaction, + ) + expected = [ + (self.N, self.torque*self.axis), + (self.A, - self.torque*self.axis), + ] + assert actuator.to_loads() == expected + + +class NonSympifyable: + pass + + +class TestDuffingSpring: + @pytest.fixture(autouse=True) + # Set up common variables that will be used in multiple tests + def _duffing_spring_fixture(self): + self.linear_stiffness = Symbol('beta') + self.nonlinear_stiffness = Symbol('alpha') + self.equilibrium_length = Symbol('l') + self.pA = Point('pA') + self.pB = Point('pB') + self.pathway = LinearPathway(self.pA, self.pB) + self.q = dynamicsymbols('q') + self.N = ReferenceFrame('N') + + # Simples tests to check that DuffingSpring is a subclass of ForceActuator and ActuatorBase + def test_is_force_actuator_subclass(self): + assert issubclass(DuffingSpring, ForceActuator) + + def test_is_actuator_base_subclass(self): + assert issubclass(DuffingSpring, ActuatorBase) + + @pytest.mark.parametrize( + # Create parametrized tests that allows running the same test function multiple times with different sets of arguments + ( + 'linear_stiffness, ' + 'expected_linear_stiffness, ' + 'nonlinear_stiffness, ' + 'expected_nonlinear_stiffness, ' + 'equilibrium_length, ' + 'expected_equilibrium_length, ' + 'force' + ), + [ + ( + 1, + S.One, + 1, + S.One, + 0, + S.Zero, + -sqrt(dynamicsymbols('q')**2)-(sqrt(dynamicsymbols('q')**2))**3, + ), + ( + Symbol('beta'), + Symbol('beta'), + Symbol('alpha'), + Symbol('alpha'), + 0, + S.Zero, + -Symbol('beta')*sqrt(dynamicsymbols('q')**2)-Symbol('alpha')*(sqrt(dynamicsymbols('q')**2))**3, + ), + ( + Symbol('beta'), + Symbol('beta'), + Symbol('alpha'), + Symbol('alpha'), + S.Zero, + S.Zero, + -Symbol('beta')*sqrt(dynamicsymbols('q')**2)-Symbol('alpha')*(sqrt(dynamicsymbols('q')**2))**3, + ), + ( + Symbol('beta'), + Symbol('beta'), + Symbol('alpha'), + Symbol('alpha'), + Symbol('l'), + Symbol('l'), + -Symbol('beta') * (sqrt(dynamicsymbols('q')**2) - Symbol('l')) - Symbol('alpha') * (sqrt(dynamicsymbols('q')**2) - Symbol('l'))**3, + ), + ] + ) + + # Check if DuffingSpring correctly initializes its attributes + # It tests various combinations of linear & nonlinear stiffness, equilibriun length, and the resulting force expression + def test_valid_constructor( + self, + linear_stiffness, + expected_linear_stiffness, + nonlinear_stiffness, + expected_nonlinear_stiffness, + equilibrium_length, + expected_equilibrium_length, + force, + ): + self.pB.set_pos(self.pA, self.q*self.N.x) + spring = DuffingSpring(linear_stiffness, nonlinear_stiffness, self.pathway, equilibrium_length) + + assert isinstance(spring, DuffingSpring) + + assert hasattr(spring, 'linear_stiffness') + assert isinstance(spring.linear_stiffness, ExprType) + assert spring.linear_stiffness == expected_linear_stiffness + + assert hasattr(spring, 'nonlinear_stiffness') + assert isinstance(spring.nonlinear_stiffness, ExprType) + assert spring.nonlinear_stiffness == expected_nonlinear_stiffness + + assert hasattr(spring, 'pathway') + assert isinstance(spring.pathway, LinearPathway) + assert spring.pathway == self.pathway + + assert hasattr(spring, 'equilibrium_length') + assert isinstance(spring.equilibrium_length, ExprType) + assert spring.equilibrium_length == expected_equilibrium_length + + assert hasattr(spring, 'force') + assert isinstance(spring.force, ExprType) + assert spring.force == force + + @pytest.mark.parametrize('linear_stiffness', [None, NonSympifyable()]) + def test_invalid_constructor_linear_stiffness_not_sympifyable(self, linear_stiffness): + with pytest.raises(SympifyError): + _ = DuffingSpring(linear_stiffness, self.nonlinear_stiffness, self.pathway, self.equilibrium_length) + + @pytest.mark.parametrize('nonlinear_stiffness', [None, NonSympifyable()]) + def test_invalid_constructor_nonlinear_stiffness_not_sympifyable(self, nonlinear_stiffness): + with pytest.raises(SympifyError): + _ = DuffingSpring(self.linear_stiffness, nonlinear_stiffness, self.pathway, self.equilibrium_length) + + def test_invalid_constructor_pathway_not_pathway_base(self): + with pytest.raises(TypeError): + _ = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, NonSympifyable(), self.equilibrium_length) + + @pytest.mark.parametrize('equilibrium_length', [None, NonSympifyable()]) + def test_invalid_constructor_equilibrium_length_not_sympifyable(self, equilibrium_length): + with pytest.raises(SympifyError): + _ = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, equilibrium_length) + + @pytest.mark.parametrize( + 'property_name, fixture_attr_name', + [ + ('linear_stiffness', 'linear_stiffness'), + ('nonlinear_stiffness', 'nonlinear_stiffness'), + ('pathway', 'pathway'), + ('equilibrium_length', 'equilibrium_length') + ] + ) + # Check if certain properties of DuffingSpring object are immutable after initialization + # Ensure that once DuffingSpring is created, its key properties cannot be changed + def test_properties_are_immutable(self, property_name, fixture_attr_name): + spring = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, self.equilibrium_length) + with pytest.raises(AttributeError): + setattr(spring, property_name, getattr(self, fixture_attr_name)) + + @pytest.mark.parametrize( + 'equilibrium_length, expected', + [ + (0, 'DuffingSpring(beta, alpha, LinearPathway(pA, pB), equilibrium_length=0)'), + (Symbol('l'), 'DuffingSpring(beta, alpha, LinearPathway(pA, pB), equilibrium_length=l)'), + ] + ) + # Check the __repr__ method of DuffingSpring class + # Check if the actual string representation of DuffingSpring instance matches the expected string for each provided parameter values + def test_repr(self, equilibrium_length, expected): + spring = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, equilibrium_length) + assert repr(spring) == expected + + def test_to_loads(self): + self.pB.set_pos(self.pA, self.q*self.N.x) + spring = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, self.equilibrium_length) + + # Calculate the displacement from the equilibrium length + displacement = self.q - self.equilibrium_length + + # Make sure this matches the computation in DuffingSpring class + force = -self.linear_stiffness * displacement - self.nonlinear_stiffness * displacement**3 + + # The expected loads on pA and pB due to the spring + expected_loads = [Force(self.pA, force * self.N.x), Force(self.pB, -force * self.N.x)] + + # Compare expected loads to what is returned from DuffingSpring.to_loads() + calculated_loads = spring.to_loads() + for calculated, expected in zip(calculated_loads, expected_loads): + assert calculated.point == expected.point + for dim in self.N: # Assuming self.N is the reference frame + calculated_component = calculated.vector.dot(dim) + expected_component = expected.vector.dot(dim) + # Substitute all symbols with numeric values + substitutions = {self.q: 1, Symbol('l'): 1, Symbol('alpha'): 1, Symbol('beta'): 1} # Add other necessary symbols as needed + diff = (calculated_component - expected_component).subs(substitutions).evalf() + # Check if the absolute value of the difference is below a threshold + assert Abs(diff) < 1e-9, f"The forces do not match. Difference: {diff}" + +class TestCoulombKineticFriction: + @pytest.fixture(autouse=True) + def _block_on_surface(self): + """A block sliding on a surface. + + Notes + ===== + This test validates the correctness of the CoulombKineticFriction by simulating + a block sliding on a surface with the Coulomb kinetic friction force. + The test covers scenarios with both positive and negative velocities. + + """ + + # Mass, gravity constant, friction coefficient, coefficient of Stribeck friction, viscous_coefficient + self.m, self.g, self.mu_k, self.mu_s, self.v_s, self.sigma, self.F = symbols('m g mu_k mu_s v_s sigma F', real=True) + + def test_block_on_surface_default(self): + # General Case + q = dynamicsymbols('q') + + N = ReferenceFrame('N') + O = Point('O') + P = O.locatenew('P', q * N.x) + O.set_vel(N, 0) + P.set_vel(N, q.diff() * N.x) + + pathway = LinearPathway(O, P) + friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway) + expected_general = [Force(point=O, force=self.g * self.m * self.mu_k * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2) * N.x), + Force(point=P, force=-self.g * self.m * self.mu_k * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2) * N.x)] + + assert friction.to_loads() == expected_general + + # Positive + q = dynamicsymbols('q', positive=True) + + N = ReferenceFrame('N') + O = Point('O') + P = O.locatenew('P', q * N.x) + O.set_vel(N, 0) + P.set_vel(N, q.diff() * N.x) + + pathway = LinearPathway(O, P) + friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway) + expected_positive = [Force(point=O, force=self.g * self.m * self.mu_k * sign(q.diff()) * N.x), + Force(point=P, force=-self.g * self.m * self.mu_k * sign(q.diff()) * N.x)] + + assert friction.to_loads() == expected_positive + + # Negative + q = dynamicsymbols('q', positive=False) + + N = ReferenceFrame('N') + O = Point('O') + P = O.locatenew('P', q * N.x) + O.set_vel(N, 0) + P.set_vel(N, q.diff() * N.x) + + pathway = LinearPathway(O, P) + friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway) + expected_negative = [Force(point=O, force=self.g * self.m * self.mu_k * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2)*N.x), + Force(point=P, force=-self.g * self.m * self.mu_k * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2)*N.x)] + + assert friction.to_loads() == expected_negative + + def test_block_on_surface_viscous(self): + # General Case + q = dynamicsymbols('q') + + N = ReferenceFrame('N') + O = Point('O') + P = O.locatenew('P', q * N.x) + O.set_vel(N, 0) + P.set_vel(N, q.diff() * N.x) + + pathway = LinearPathway(O, P) + friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, sigma=self.sigma) + expected_general = [Force(point=O, force=(self.g * self.m * self.mu_k * sign(sqrt(q**2) * q.diff()/q) + self.sigma * sqrt(q**2) * q.diff()/q) * q/sqrt(q**2) * N.x), + Force(point=P, force=(-self.g * self.m * self.mu_k * sign(sqrt(q**2) * q.diff()/q) - self.sigma * sqrt(q**2) * q.diff()/q) * q/sqrt(q**2) * N.x)] + + assert friction.to_loads() == expected_general + + # Positive + q = dynamicsymbols('q', positive=True) + + N = ReferenceFrame('N') + O = Point('O') + P = O.locatenew('P', q * N.x) + O.set_vel(N, 0) + P.set_vel(N, q.diff() * N.x) + + pathway = LinearPathway(O, P) + friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, sigma=self.sigma) + expected_positive = [Force(point=O, force=(self.g * self.m * self.mu_k * sign(q.diff()) + self.sigma * q.diff()) * N.x), + Force(point=P, force=(-self.g * self.m * self.mu_k * sign(q.diff()) - self.sigma * q.diff()) * N.x)] + + assert friction.to_loads() == expected_positive + + # Negative + q = dynamicsymbols('q', positive=False) + + N = ReferenceFrame('N') + O = Point('O') + P = O.locatenew('P', q * N.x) + O.set_vel(N, 0) + P.set_vel(N, q.diff() * N.x) + + pathway = LinearPathway(O, P) + friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, sigma=self.sigma) + expected_negative = [Force(point=O, force=(self.g * self.m * self.mu_k * sign(sqrt(q**2) * q.diff()/q) + self.sigma * sqrt(q**2) * q.diff()/q) * q/sqrt(q**2) * N.x), + Force(point=P, force=(-self.g * self.m * self.mu_k * sign(sqrt(q**2) * q.diff()/q) - self.sigma * sqrt(q**2) * q.diff()/q) * q/sqrt(q**2) * N.x)] + + assert friction.to_loads() == expected_negative + + def test_block_on_surface_stribeck(self): + # General Case + q = dynamicsymbols('q') + + N = ReferenceFrame('N') + O = Point('O') + P = O.locatenew('P', q * N.x) + O.set_vel(N, 0) + P.set_vel(N, q.diff() * N.x) + + pathway = LinearPathway(O, P) + friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, v_s=self.v_s, mu_s=self.mu_s) + expected_general = [Force(point=O, force=(self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2) * N.x), + Force(point=P, force=- (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2) * N.x)] + + assert friction.to_loads() == expected_general + + # Positive + q = dynamicsymbols('q', positive=True) + + N = ReferenceFrame('N') + O = Point('O') + P = O.locatenew('P', q * N.x) + O.set_vel(N, 0) + P.set_vel(N, q.diff() * N.x) + + pathway = LinearPathway(O, P) + friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, v_s=self.v_s, mu_s=self.mu_s) + expected_positive = [Force(point=O, force=(self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(q.diff()) * N.x), + Force(point=P, force=- (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(q.diff()) * N.x)] + + assert friction.to_loads() == expected_positive + + # Negative + q = dynamicsymbols('q', positive=False) + + N = ReferenceFrame('N') + O = Point('O') + P = O.locatenew('P', q * N.x) + O.set_vel(N, 0) + P.set_vel(N, q.diff() * N.x) + + pathway = LinearPathway(O, P) + friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, v_s=self.v_s, mu_s=self.mu_s) + expected_negative = [Force(point=O, force=(self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2) * N.x), + Force(point=P, force=- (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2) * N.x)] + + assert friction.to_loads() == expected_negative + + def test_block_on_surface_all(self): + # General Case + q = dynamicsymbols('q') + + N = ReferenceFrame('N') + O = Point('O') + P = O.locatenew('P', q * N.x) + O.set_vel(N, 0) + P.set_vel(N, q.diff() * N.x) + + pathway = LinearPathway(O, P) + friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, v_s=self.v_s, sigma=self.sigma, mu_s=self.mu_s) + expected_general = [Force(point=O, force=(self.sigma * sqrt(q**2) * q.diff()/q + (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(sqrt(q**2) * q.diff()/q)) * q/sqrt(q**2) * N.x), + Force(point=P, force=(-self.sigma * sqrt(q**2) * q.diff()/q - (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(sqrt(q**2) * q.diff()/q)) * q/sqrt(q**2) * N.x)] + + assert friction.to_loads() == expected_general + + # Positive + q = dynamicsymbols('q', positive=True) + + N = ReferenceFrame('N') + O = Point('O') + P = O.locatenew('P', q * N.x) + O.set_vel(N, 0) + P.set_vel(N, q.diff() * N.x) + + pathway = LinearPathway(O, P) + friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, v_s=self.v_s, sigma=self.sigma, mu_s=self.mu_s) + expected_positive = [Force(point=O, force=(self.sigma * q.diff() + (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(q.diff())) * N.x), + Force(point=P, force=(-self.sigma * q.diff() - (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(q.diff())) * N.x)] + + assert friction.to_loads() == expected_positive + + # Negative + q = dynamicsymbols('q', positive=False) + + N = ReferenceFrame('N') + O = Point('O') + P = O.locatenew('P', q * N.x) + O.set_vel(N, 0) + P.set_vel(N, q.diff() * N.x) + + pathway = LinearPathway(O, P) + friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, v_s=self.v_s, sigma=self.sigma, mu_s=self.mu_s) + expected_negative = [Force(point=O, force=(self.sigma * sqrt(q**2) * q.diff()/q + (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(sqrt(q**2) * q.diff()/q)) * q/sqrt(q**2) * N.x), + Force(point=P, force=(-self.sigma * sqrt(q**2) * q.diff()/q - (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(sqrt(q**2) * q.diff()/q)) * q/sqrt(q**2) * N.x)] + + assert friction.to_loads() == expected_negative + + def test_normal_force_zero(self): + q = dynamicsymbols('q') + + N = ReferenceFrame('N') + O = Point('O') + P = O.locatenew('P', q * N.x) + O.set_vel(N, 0) + P.set_vel(N, q.diff() * N.x) + + pathway = LinearPathway(O, P) + friction = CoulombKineticFriction( + self.mu_k, + 0, + pathway + ) + assert friction.force == 0 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_body.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_body.py new file mode 100644 index 0000000000000000000000000000000000000000..2d59d747400652a0cbb081f4afc5ae4ebaa4db85 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_body.py @@ -0,0 +1,340 @@ +from sympy import (Symbol, symbols, sin, cos, Matrix, zeros, + simplify) +from sympy.physics.vector import Point, ReferenceFrame, dynamicsymbols, Dyadic +from sympy.physics.mechanics import inertia, Body +from sympy.testing.pytest import raises, warns_deprecated_sympy + + +def test_default(): + with warns_deprecated_sympy(): + body = Body('body') + assert body.name == 'body' + assert body.loads == [] + point = Point('body_masscenter') + point.set_vel(body.frame, 0) + com = body.masscenter + frame = body.frame + assert com.vel(frame) == point.vel(frame) + assert body.mass == Symbol('body_mass') + ixx, iyy, izz = symbols('body_ixx body_iyy body_izz') + ixy, iyz, izx = symbols('body_ixy body_iyz body_izx') + assert body.inertia == (inertia(body.frame, ixx, iyy, izz, ixy, iyz, izx), + body.masscenter) + + +def test_custom_rigid_body(): + # Body with RigidBody. + rigidbody_masscenter = Point('rigidbody_masscenter') + rigidbody_mass = Symbol('rigidbody_mass') + rigidbody_frame = ReferenceFrame('rigidbody_frame') + body_inertia = inertia(rigidbody_frame, 1, 0, 0) + with warns_deprecated_sympy(): + rigid_body = Body('rigidbody_body', rigidbody_masscenter, + rigidbody_mass, rigidbody_frame, body_inertia) + com = rigid_body.masscenter + frame = rigid_body.frame + rigidbody_masscenter.set_vel(rigidbody_frame, 0) + assert com.vel(frame) == rigidbody_masscenter.vel(frame) + assert com.pos_from(com) == rigidbody_masscenter.pos_from(com) + + assert rigid_body.mass == rigidbody_mass + assert rigid_body.inertia == (body_inertia, rigidbody_masscenter) + + assert rigid_body.is_rigidbody + + assert hasattr(rigid_body, 'masscenter') + assert hasattr(rigid_body, 'mass') + assert hasattr(rigid_body, 'frame') + assert hasattr(rigid_body, 'inertia') + + +def test_particle_body(): + # Body with Particle + particle_masscenter = Point('particle_masscenter') + particle_mass = Symbol('particle_mass') + particle_frame = ReferenceFrame('particle_frame') + with warns_deprecated_sympy(): + particle_body = Body('particle_body', particle_masscenter, + particle_mass, particle_frame) + com = particle_body.masscenter + frame = particle_body.frame + particle_masscenter.set_vel(particle_frame, 0) + assert com.vel(frame) == particle_masscenter.vel(frame) + assert com.pos_from(com) == particle_masscenter.pos_from(com) + + assert particle_body.mass == particle_mass + assert not hasattr(particle_body, "_inertia") + assert hasattr(particle_body, 'frame') + assert hasattr(particle_body, 'masscenter') + assert hasattr(particle_body, 'mass') + assert particle_body.inertia == (Dyadic(0), particle_body.masscenter) + assert particle_body.central_inertia == Dyadic(0) + assert not particle_body.is_rigidbody + + particle_body.central_inertia = inertia(particle_frame, 1, 1, 1) + assert particle_body.central_inertia == inertia(particle_frame, 1, 1, 1) + assert particle_body.is_rigidbody + + with warns_deprecated_sympy(): + particle_body = Body('particle_body', mass=particle_mass) + assert not particle_body.is_rigidbody + point = particle_body.masscenter.locatenew('point', particle_body.x) + point_inertia = particle_mass * inertia(particle_body.frame, 0, 1, 1) + particle_body.inertia = (point_inertia, point) + assert particle_body.inertia == (point_inertia, point) + assert particle_body.central_inertia == Dyadic(0) + assert particle_body.is_rigidbody + + +def test_particle_body_add_force(): + # Body with Particle + particle_masscenter = Point('particle_masscenter') + particle_mass = Symbol('particle_mass') + particle_frame = ReferenceFrame('particle_frame') + with warns_deprecated_sympy(): + particle_body = Body('particle_body', particle_masscenter, + particle_mass, particle_frame) + + a = Symbol('a') + force_vector = a * particle_body.frame.x + particle_body.apply_force(force_vector, particle_body.masscenter) + assert len(particle_body.loads) == 1 + point = particle_body.masscenter.locatenew( + particle_body._name + '_point0', 0) + point.set_vel(particle_body.frame, 0) + force_point = particle_body.loads[0][0] + + frame = particle_body.frame + assert force_point.vel(frame) == point.vel(frame) + assert force_point.pos_from(force_point) == point.pos_from(force_point) + + assert particle_body.loads[0][1] == force_vector + + +def test_body_add_force(): + # Body with RigidBody. + rigidbody_masscenter = Point('rigidbody_masscenter') + rigidbody_mass = Symbol('rigidbody_mass') + rigidbody_frame = ReferenceFrame('rigidbody_frame') + body_inertia = inertia(rigidbody_frame, 1, 0, 0) + with warns_deprecated_sympy(): + rigid_body = Body('rigidbody_body', rigidbody_masscenter, + rigidbody_mass, rigidbody_frame, body_inertia) + + l = Symbol('l') + Fa = Symbol('Fa') + point = rigid_body.masscenter.locatenew( + 'rigidbody_body_point0', + l * rigid_body.frame.x) + point.set_vel(rigid_body.frame, 0) + force_vector = Fa * rigid_body.frame.z + # apply_force with point + rigid_body.apply_force(force_vector, point) + assert len(rigid_body.loads) == 1 + force_point = rigid_body.loads[0][0] + frame = rigid_body.frame + assert force_point.vel(frame) == point.vel(frame) + assert force_point.pos_from(force_point) == point.pos_from(force_point) + assert rigid_body.loads[0][1] == force_vector + # apply_force without point + rigid_body.apply_force(force_vector) + assert len(rigid_body.loads) == 2 + assert rigid_body.loads[1][1] == force_vector + # passing something else than point + raises(TypeError, lambda: rigid_body.apply_force(force_vector, 0)) + raises(TypeError, lambda: rigid_body.apply_force(0)) + +def test_body_add_torque(): + with warns_deprecated_sympy(): + body = Body('body') + torque_vector = body.frame.x + body.apply_torque(torque_vector) + + assert len(body.loads) == 1 + assert body.loads[0] == (body.frame, torque_vector) + raises(TypeError, lambda: body.apply_torque(0)) + +def test_body_masscenter_vel(): + with warns_deprecated_sympy(): + A = Body('A') + N = ReferenceFrame('N') + with warns_deprecated_sympy(): + B = Body('B', frame=N) + A.masscenter.set_vel(N, N.z) + assert A.masscenter_vel(B) == N.z + assert A.masscenter_vel(N) == N.z + +def test_body_ang_vel(): + with warns_deprecated_sympy(): + A = Body('A') + N = ReferenceFrame('N') + with warns_deprecated_sympy(): + B = Body('B', frame=N) + A.frame.set_ang_vel(N, N.y) + assert A.ang_vel_in(B) == N.y + assert B.ang_vel_in(A) == -N.y + assert A.ang_vel_in(N) == N.y + +def test_body_dcm(): + with warns_deprecated_sympy(): + A = Body('A') + B = Body('B') + A.frame.orient_axis(B.frame, B.frame.z, 10) + assert A.dcm(B) == Matrix([[cos(10), sin(10), 0], [-sin(10), cos(10), 0], [0, 0, 1]]) + assert A.dcm(B.frame) == Matrix([[cos(10), sin(10), 0], [-sin(10), cos(10), 0], [0, 0, 1]]) + +def test_body_axis(): + N = ReferenceFrame('N') + with warns_deprecated_sympy(): + B = Body('B', frame=N) + assert B.x == N.x + assert B.y == N.y + assert B.z == N.z + +def test_apply_force_multiple_one_point(): + a, b = symbols('a b') + P = Point('P') + with warns_deprecated_sympy(): + B = Body('B') + f1 = a*B.x + f2 = b*B.y + B.apply_force(f1, P) + assert B.loads == [(P, f1)] + B.apply_force(f2, P) + assert B.loads == [(P, f1+f2)] + +def test_apply_force(): + f, g = symbols('f g') + q, x, v1, v2 = dynamicsymbols('q x v1 v2') + P1 = Point('P1') + P2 = Point('P2') + with warns_deprecated_sympy(): + B1 = Body('B1') + B2 = Body('B2') + N = ReferenceFrame('N') + + P1.set_vel(B1.frame, v1*B1.x) + P2.set_vel(B2.frame, v2*B2.x) + force = f*q*N.z # time varying force + + B1.apply_force(force, P1, B2, P2) #applying equal and opposite force on moving points + assert B1.loads == [(P1, force)] + assert B2.loads == [(P2, -force)] + + g1 = B1.mass*g*N.y + g2 = B2.mass*g*N.y + + B1.apply_force(g1) #applying gravity on B1 masscenter + B2.apply_force(g2) #applying gravity on B2 masscenter + + assert B1.loads == [(P1,force), (B1.masscenter, g1)] + assert B2.loads == [(P2, -force), (B2.masscenter, g2)] + + force2 = x*N.x + + B1.apply_force(force2, reaction_body=B2) #Applying time varying force on masscenter + + assert B1.loads == [(P1, force), (B1.masscenter, force2+g1)] + assert B2.loads == [(P2, -force), (B2.masscenter, -force2+g2)] + +def test_apply_torque(): + t = symbols('t') + q = dynamicsymbols('q') + with warns_deprecated_sympy(): + B1 = Body('B1') + B2 = Body('B2') + N = ReferenceFrame('N') + torque = t*q*N.x + + B1.apply_torque(torque, B2) #Applying equal and opposite torque + assert B1.loads == [(B1.frame, torque)] + assert B2.loads == [(B2.frame, -torque)] + + torque2 = t*N.y + B1.apply_torque(torque2) + assert B1.loads == [(B1.frame, torque+torque2)] + +def test_clear_load(): + a = symbols('a') + P = Point('P') + with warns_deprecated_sympy(): + B = Body('B') + force = a*B.z + B.apply_force(force, P) + assert B.loads == [(P, force)] + B.clear_loads() + assert B.loads == [] + +def test_remove_load(): + P1 = Point('P1') + P2 = Point('P2') + with warns_deprecated_sympy(): + B = Body('B') + f1 = B.x + f2 = B.y + B.apply_force(f1, P1) + B.apply_force(f2, P2) + assert B.loads == [(P1, f1), (P2, f2)] + B.remove_load(P2) + assert B.loads == [(P1, f1)] + B.apply_torque(f1.cross(f2)) + assert B.loads == [(P1, f1), (B.frame, f1.cross(f2))] + B.remove_load() + assert B.loads == [(P1, f1)] + +def test_apply_loads_on_multi_degree_freedom_holonomic_system(): + """Example based on: https://pydy.readthedocs.io/en/latest/examples/multidof-holonomic.html""" + with warns_deprecated_sympy(): + W = Body('W') #Wall + B = Body('B') #Block + P = Body('P') #Pendulum + b = Body('b') #bob + q1, q2 = dynamicsymbols('q1 q2') #generalized coordinates + k, c, g, kT = symbols('k c g kT') #constants + F, T = dynamicsymbols('F T') #Specified forces + + #Applying forces + B.apply_force(F*W.x) + W.apply_force(k*q1*W.x, reaction_body=B) #Spring force + W.apply_force(c*q1.diff()*W.x, reaction_body=B) #dampner + P.apply_force(P.mass*g*W.y) + b.apply_force(b.mass*g*W.y) + + #Applying torques + P.apply_torque(kT*q2*W.z, reaction_body=b) + P.apply_torque(T*W.z) + + assert B.loads == [(B.masscenter, (F - k*q1 - c*q1.diff())*W.x)] + assert P.loads == [(P.masscenter, P.mass*g*W.y), (P.frame, (T + kT*q2)*W.z)] + assert b.loads == [(b.masscenter, b.mass*g*W.y), (b.frame, -kT*q2*W.z)] + assert W.loads == [(W.masscenter, (c*q1.diff() + k*q1)*W.x)] + + +def test_parallel_axis(): + N = ReferenceFrame('N') + m, Ix, Iy, Iz, a, b = symbols('m, I_x, I_y, I_z, a, b') + Io = inertia(N, Ix, Iy, Iz) + # Test RigidBody + o = Point('o') + p = o.locatenew('p', a * N.x + b * N.y) + with warns_deprecated_sympy(): + R = Body('R', masscenter=o, frame=N, mass=m, central_inertia=Io) + Ip = R.parallel_axis(p) + Ip_expected = inertia(N, Ix + m * b**2, Iy + m * a**2, + Iz + m * (a**2 + b**2), ixy=-m * a * b) + assert Ip == Ip_expected + # Reference frame from which the parallel axis is viewed should not matter + A = ReferenceFrame('A') + A.orient_axis(N, N.z, 1) + assert simplify( + (R.parallel_axis(p, A) - Ip_expected).to_matrix(A)) == zeros(3, 3) + # Test Particle + o = Point('o') + p = o.locatenew('p', a * N.x + b * N.y) + with warns_deprecated_sympy(): + P = Body('P', masscenter=o, mass=m, frame=N) + Ip = P.parallel_axis(p, N) + Ip_expected = inertia(N, m * b ** 2, m * a ** 2, m * (a ** 2 + b ** 2), + ixy=-m * a * b) + assert not P.is_rigidbody + assert Ip == Ip_expected diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_functions.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..bae6b19b2807dca1632942bd3717e29d214eb269 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_functions.py @@ -0,0 +1,262 @@ +from sympy import sin, cos, tan, pi, symbols, Matrix, S, Function +from sympy.physics.mechanics import (Particle, Point, ReferenceFrame, + RigidBody) +from sympy.physics.mechanics import (angular_momentum, dynamicsymbols, + kinetic_energy, linear_momentum, + outer, potential_energy, msubs, + find_dynamicsymbols, Lagrangian) + +from sympy.physics.mechanics.functions import ( + center_of_mass, _validate_coordinates, _parse_linear_solver) +from sympy.testing.pytest import raises, warns_deprecated_sympy + + +q1, q2, q3, q4, q5 = symbols('q1 q2 q3 q4 q5') +N = ReferenceFrame('N') +A = N.orientnew('A', 'Axis', [q1, N.z]) +B = A.orientnew('B', 'Axis', [q2, A.x]) +C = B.orientnew('C', 'Axis', [q3, B.y]) + + +def test_linear_momentum(): + N = ReferenceFrame('N') + Ac = Point('Ac') + Ac.set_vel(N, 25 * N.y) + I = outer(N.x, N.x) + A = RigidBody('A', Ac, N, 20, (I, Ac)) + P = Point('P') + Pa = Particle('Pa', P, 1) + Pa.point.set_vel(N, 10 * N.x) + raises(TypeError, lambda: linear_momentum(A, A, Pa)) + raises(TypeError, lambda: linear_momentum(N, N, Pa)) + assert linear_momentum(N, A, Pa) == 10 * N.x + 500 * N.y + + +def test_angular_momentum_and_linear_momentum(): + """A rod with length 2l, centroidal inertia I, and mass M along with a + particle of mass m fixed to the end of the rod rotate with an angular rate + of omega about point O which is fixed to the non-particle end of the rod. + The rod's reference frame is A and the inertial frame is N.""" + m, M, l, I = symbols('m, M, l, I') + omega = dynamicsymbols('omega') + N = ReferenceFrame('N') + a = ReferenceFrame('a') + O = Point('O') + Ac = O.locatenew('Ac', l * N.x) + P = Ac.locatenew('P', l * N.x) + O.set_vel(N, 0 * N.x) + a.set_ang_vel(N, omega * N.z) + Ac.v2pt_theory(O, N, a) + P.v2pt_theory(O, N, a) + Pa = Particle('Pa', P, m) + A = RigidBody('A', Ac, a, M, (I * outer(N.z, N.z), Ac)) + expected = 2 * m * omega * l * N.y + M * l * omega * N.y + assert linear_momentum(N, A, Pa) == expected + raises(TypeError, lambda: angular_momentum(N, N, A, Pa)) + raises(TypeError, lambda: angular_momentum(O, O, A, Pa)) + raises(TypeError, lambda: angular_momentum(O, N, O, Pa)) + expected = (I + M * l**2 + 4 * m * l**2) * omega * N.z + assert angular_momentum(O, N, A, Pa) == expected + + +def test_kinetic_energy(): + m, M, l1 = symbols('m M l1') + omega = dynamicsymbols('omega') + N = ReferenceFrame('N') + O = Point('O') + O.set_vel(N, 0 * N.x) + Ac = O.locatenew('Ac', l1 * N.x) + P = Ac.locatenew('P', l1 * N.x) + a = ReferenceFrame('a') + a.set_ang_vel(N, omega * N.z) + Ac.v2pt_theory(O, N, a) + P.v2pt_theory(O, N, a) + Pa = Particle('Pa', P, m) + I = outer(N.z, N.z) + A = RigidBody('A', Ac, a, M, (I, Ac)) + raises(TypeError, lambda: kinetic_energy(Pa, Pa, A)) + raises(TypeError, lambda: kinetic_energy(N, N, A)) + assert 0 == (kinetic_energy(N, Pa, A) - (M*l1**2*omega**2/2 + + 2*l1**2*m*omega**2 + omega**2/2)).expand() + + +def test_potential_energy(): + m, M, l1, g, h, H = symbols('m M l1 g h H') + omega = dynamicsymbols('omega') + N = ReferenceFrame('N') + O = Point('O') + O.set_vel(N, 0 * N.x) + Ac = O.locatenew('Ac', l1 * N.x) + P = Ac.locatenew('P', l1 * N.x) + a = ReferenceFrame('a') + a.set_ang_vel(N, omega * N.z) + Ac.v2pt_theory(O, N, a) + P.v2pt_theory(O, N, a) + Pa = Particle('Pa', P, m) + I = outer(N.z, N.z) + A = RigidBody('A', Ac, a, M, (I, Ac)) + Pa.potential_energy = m * g * h + A.potential_energy = M * g * H + assert potential_energy(A, Pa) == m * g * h + M * g * H + + +def test_Lagrangian(): + M, m, g, h = symbols('M m g h') + N = ReferenceFrame('N') + O = Point('O') + O.set_vel(N, 0 * N.x) + P = O.locatenew('P', 1 * N.x) + P.set_vel(N, 10 * N.x) + Pa = Particle('Pa', P, 1) + Ac = O.locatenew('Ac', 2 * N.y) + Ac.set_vel(N, 5 * N.y) + a = ReferenceFrame('a') + a.set_ang_vel(N, 10 * N.z) + I = outer(N.z, N.z) + A = RigidBody('A', Ac, a, 20, (I, Ac)) + Pa.potential_energy = m * g * h + A.potential_energy = M * g * h + raises(TypeError, lambda: Lagrangian(A, A, Pa)) + raises(TypeError, lambda: Lagrangian(N, N, Pa)) + + +def test_msubs(): + a, b = symbols('a, b') + x, y, z = dynamicsymbols('x, y, z') + # Test simple substitution + expr = Matrix([[a*x + b, x*y.diff() + y], + [x.diff().diff(), z + sin(z.diff())]]) + sol = Matrix([[a + b, y], + [x.diff().diff(), 1]]) + sd = {x: 1, z: 1, z.diff(): 0, y.diff(): 0} + assert msubs(expr, sd) == sol + # Test smart substitution + expr = cos(x + y)*tan(x + y) + b*x.diff() + sd = {x: 0, y: pi/2, x.diff(): 1} + assert msubs(expr, sd, smart=True) == b + 1 + N = ReferenceFrame('N') + v = x*N.x + y*N.y + d = x*(N.x|N.x) + y*(N.y|N.y) + v_sol = 1*N.y + d_sol = 1*(N.y|N.y) + sd = {x: 0, y: 1} + assert msubs(v, sd) == v_sol + assert msubs(d, sd) == d_sol + + +def test_find_dynamicsymbols(): + a, b = symbols('a, b') + x, y, z = dynamicsymbols('x, y, z') + expr = Matrix([[a*x + b, x*y.diff() + y], + [x.diff().diff(), z + sin(z.diff())]]) + # Test finding all dynamicsymbols + sol = {x, y.diff(), y, x.diff().diff(), z, z.diff()} + assert find_dynamicsymbols(expr) == sol + # Test finding all but those in sym_list + exclude_list = [x, y, z] + sol = {y.diff(), x.diff().diff(), z.diff()} + assert find_dynamicsymbols(expr, exclude=exclude_list) == sol + # Test finding all dynamicsymbols in a vector with a given reference frame + d, e, f = dynamicsymbols('d, e, f') + A = ReferenceFrame('A') + v = d * A.x + e * A.y + f * A.z + sol = {d, e, f} + assert find_dynamicsymbols(v, reference_frame=A) == sol + # Test if a ValueError is raised on supplying only a vector as input + raises(ValueError, lambda: find_dynamicsymbols(v)) + + +# This function tests the center_of_mass() function +# that was added in PR #14758 to compute the center of +# mass of a system of bodies. +def test_center_of_mass(): + a = ReferenceFrame('a') + m = symbols('m', real=True) + p1 = Particle('p1', Point('p1_pt'), S.One) + p2 = Particle('p2', Point('p2_pt'), S(2)) + p3 = Particle('p3', Point('p3_pt'), S(3)) + p4 = Particle('p4', Point('p4_pt'), m) + b_f = ReferenceFrame('b_f') + b_cm = Point('b_cm') + mb = symbols('mb') + b = RigidBody('b', b_cm, b_f, mb, (outer(b_f.x, b_f.x), b_cm)) + p2.point.set_pos(p1.point, a.x) + p3.point.set_pos(p1.point, a.x + a.y) + p4.point.set_pos(p1.point, a.y) + b.masscenter.set_pos(p1.point, a.y + a.z) + point_o=Point('o') + point_o.set_pos(p1.point, center_of_mass(p1.point, p1, p2, p3, p4, b)) + expr = 5/(m + mb + 6)*a.x + (m + mb + 3)/(m + mb + 6)*a.y + mb/(m + mb + 6)*a.z + assert point_o.pos_from(p1.point)-expr == 0 + + +def test_validate_coordinates(): + q1, q2, q3, u1, u2, u3, ua1, ua2, ua3 = dynamicsymbols('q1:4 u1:4 ua1:4') + s1, s2, s3 = symbols('s1:4') + # Test normal + _validate_coordinates([q1, q2, q3], [u1, u2, u3], + u_auxiliary=[ua1, ua2, ua3]) + # Test not equal number of coordinates and speeds + _validate_coordinates([q1, q2]) + _validate_coordinates([q1, q2], [u1]) + _validate_coordinates(speeds=[u1, u2]) + # Test duplicate + _validate_coordinates([q1, q2, q2], [u1, u2, u3], check_duplicates=False) + raises(ValueError, lambda: _validate_coordinates( + [q1, q2, q2], [u1, u2, u3])) + _validate_coordinates([q1, q2, q3], [u1, u2, u2], check_duplicates=False) + raises(ValueError, lambda: _validate_coordinates( + [q1, q2, q3], [u1, u2, u2], check_duplicates=True)) + raises(ValueError, lambda: _validate_coordinates( + [q1, q2, q3], [q1, u2, u3], check_duplicates=True)) + _validate_coordinates([q1, q2, q3], [u1, u2, u3], check_duplicates=False, + u_auxiliary=[u1, ua2, ua2]) + raises(ValueError, lambda: _validate_coordinates( + [q1, q2, q3], [u1, u2, u3], u_auxiliary=[u1, ua2, ua3])) + raises(ValueError, lambda: _validate_coordinates( + [q1, q2, q3], [u1, u2, u3], u_auxiliary=[q1, ua2, ua3])) + raises(ValueError, lambda: _validate_coordinates( + [q1, q2, q3], [u1, u2, u3], u_auxiliary=[ua1, ua2, ua2])) + # Test is_dynamicsymbols + _validate_coordinates([q1 + q2, q3], is_dynamicsymbols=False) + raises(ValueError, lambda: _validate_coordinates([q1 + q2, q3])) + _validate_coordinates([s1, q1, q2], [0, u1, u2], is_dynamicsymbols=False) + raises(ValueError, lambda: _validate_coordinates( + [s1, q1, q2], [0, u1, u2], is_dynamicsymbols=True)) + _validate_coordinates([s1 + s2 + s3, q1], [0, u1], is_dynamicsymbols=False) + raises(ValueError, lambda: _validate_coordinates( + [s1 + s2 + s3, q1], [0, u1], is_dynamicsymbols=True)) + _validate_coordinates(u_auxiliary=[s1, ua1], is_dynamicsymbols=False) + raises(ValueError, lambda: _validate_coordinates(u_auxiliary=[s1, ua1])) + # Test normal function + t = dynamicsymbols._t + a = symbols('a') + f1, f2 = symbols('f1:3', cls=Function) + _validate_coordinates([f1(a), f2(a)], is_dynamicsymbols=False) + raises(ValueError, lambda: _validate_coordinates([f1(a), f2(a)])) + raises(ValueError, lambda: _validate_coordinates(speeds=[f1(a), f2(a)])) + dynamicsymbols._t = a + _validate_coordinates([f1(a), f2(a)]) + raises(ValueError, lambda: _validate_coordinates([f1(t), f2(t)])) + dynamicsymbols._t = t + + +def test_parse_linear_solver(): + A, b = Matrix(3, 3, symbols('a:9')), Matrix(3, 2, symbols('b:6')) + assert _parse_linear_solver(Matrix.LUsolve) == Matrix.LUsolve # Test callable + assert _parse_linear_solver('LU')(A, b) == Matrix.LUsolve(A, b) + + +def test_deprecated_moved_functions(): + from sympy.physics.mechanics.functions import ( + inertia, inertia_of_point_mass, gravity) + N = ReferenceFrame('N') + with warns_deprecated_sympy(): + assert inertia(N, 0, 1, 0, 1) == (N.x | N.y) + (N.y | N.x) + (N.y | N.y) + with warns_deprecated_sympy(): + assert inertia_of_point_mass(1, N.x + N.y, N) == ( + (N.x | N.x) + (N.y | N.y) + 2 * (N.z | N.z) - + (N.x | N.y) - (N.y | N.x)) + p = Particle('P') + with warns_deprecated_sympy(): + assert gravity(-2 * N.z, p) == [(p.masscenter, -2 * p.mass * N.z)] diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_inertia.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_inertia.py new file mode 100644 index 0000000000000000000000000000000000000000..8d29e5f31868e539c4b50575af5180e5eb96f2cd --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_inertia.py @@ -0,0 +1,71 @@ +from sympy import symbols +from sympy.testing.pytest import raises +from sympy.physics.mechanics import (inertia, inertia_of_point_mass, + Inertia, ReferenceFrame, Point) + + +def test_inertia_dyadic(): + N = ReferenceFrame('N') + ixx, iyy, izz = symbols('ixx iyy izz') + ixy, iyz, izx = symbols('ixy iyz izx') + assert inertia(N, ixx, iyy, izz) == (ixx * (N.x | N.x) + iyy * + (N.y | N.y) + izz * (N.z | N.z)) + assert inertia(N, 0, 0, 0) == 0 * (N.x | N.x) + raises(TypeError, lambda: inertia(0, 0, 0, 0)) + assert inertia(N, ixx, iyy, izz, ixy, iyz, izx) == (ixx * (N.x | N.x) + + ixy * (N.x | N.y) + izx * (N.x | N.z) + ixy * (N.y | N.x) + iyy * + (N.y | N.y) + iyz * (N.y | N.z) + izx * (N.z | N.x) + iyz * (N.z | + N.y) + izz * (N.z | N.z)) + + +def test_inertia_of_point_mass(): + r, s, t, m = symbols('r s t m') + N = ReferenceFrame('N') + + px = r * N.x + I = inertia_of_point_mass(m, px, N) + assert I == m * r**2 * (N.y | N.y) + m * r**2 * (N.z | N.z) + + py = s * N.y + I = inertia_of_point_mass(m, py, N) + assert I == m * s**2 * (N.x | N.x) + m * s**2 * (N.z | N.z) + + pz = t * N.z + I = inertia_of_point_mass(m, pz, N) + assert I == m * t**2 * (N.x | N.x) + m * t**2 * (N.y | N.y) + + p = px + py + pz + I = inertia_of_point_mass(m, p, N) + assert I == (m * (s**2 + t**2) * (N.x | N.x) - + m * r * s * (N.x | N.y) - + m * r * t * (N.x | N.z) - + m * r * s * (N.y | N.x) + + m * (r**2 + t**2) * (N.y | N.y) - + m * s * t * (N.y | N.z) - + m * r * t * (N.z | N.x) - + m * s * t * (N.z | N.y) + + m * (r**2 + s**2) * (N.z | N.z)) + + +def test_inertia_object(): + N = ReferenceFrame('N') + O = Point('O') + ixx, iyy, izz = symbols('ixx iyy izz') + I_dyadic = ixx * (N.x | N.x) + iyy * (N.y | N.y) + izz * (N.z | N.z) + I = Inertia(inertia(N, ixx, iyy, izz), O) + assert isinstance(I, tuple) + assert I.__repr__() == ('Inertia(dyadic=ixx*(N.x|N.x) + iyy*(N.y|N.y) + ' + 'izz*(N.z|N.z), point=O)') + assert I.dyadic == I_dyadic + assert I.point == O + assert I[0] == I_dyadic + assert I[1] == O + assert I == (I_dyadic, O) # Test tuple equal + raises(TypeError, lambda: I != (O, I_dyadic)) # Incorrect tuple order + assert I == Inertia(O, I_dyadic) # Parse changed argument order + assert I == Inertia.from_inertia_scalars(O, N, ixx, iyy, izz) + # Test invalid tuple operations + raises(TypeError, lambda: I + (1, 2)) + raises(TypeError, lambda: (1, 2) + I) + raises(TypeError, lambda: I * 2) + raises(TypeError, lambda: 2 * I) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_joint.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_joint.py new file mode 100644 index 0000000000000000000000000000000000000000..271801b5b7290a4479ee61e1414741e4c4d6f966 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_joint.py @@ -0,0 +1,1240 @@ +from sympy.core.function import expand_mul +from sympy.core.numbers import pi +from sympy.core.singleton import S +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy import Matrix, simplify, eye, zeros +from sympy.core.symbol import symbols +from sympy.physics.mechanics import ( + dynamicsymbols, RigidBody, Particle, JointsMethod, PinJoint, PrismaticJoint, + CylindricalJoint, PlanarJoint, SphericalJoint, WeldJoint, Body) +from sympy.physics.mechanics.joint import Joint +from sympy.physics.vector import Vector, ReferenceFrame, Point +from sympy.testing.pytest import raises, warns_deprecated_sympy + + +t = dynamicsymbols._t # type: ignore + + +def _generate_body(interframe=False): + N = ReferenceFrame('N') + A = ReferenceFrame('A') + P = RigidBody('P', frame=N) + C = RigidBody('C', frame=A) + if interframe: + Pint, Cint = ReferenceFrame('P_int'), ReferenceFrame('C_int') + Pint.orient_axis(N, N.x, pi) + Cint.orient_axis(A, A.y, -pi / 2) + return N, A, P, C, Pint, Cint + return N, A, P, C + + +def test_Joint(): + parent = RigidBody('parent') + child = RigidBody('child') + raises(TypeError, lambda: Joint('J', parent, child)) + + +def test_coordinate_generation(): + q, u, qj, uj = dynamicsymbols('q u q_J u_J') + q0j, q1j, q2j, q3j, u0j, u1j, u2j, u3j = dynamicsymbols('q0:4_J u0:4_J') + q0, q1, q2, q3, u0, u1, u2, u3 = dynamicsymbols('q0:4 u0:4') + _, _, P, C = _generate_body() + # Using PinJoint to access Joint's coordinate generation method + J = PinJoint('J', P, C) + # Test single given + assert J._fill_coordinate_list(q, 1) == Matrix([q]) + assert J._fill_coordinate_list([u], 1) == Matrix([u]) + assert J._fill_coordinate_list([u], 1, offset=2) == Matrix([u]) + # Test None + assert J._fill_coordinate_list(None, 1) == Matrix([qj]) + assert J._fill_coordinate_list([None], 1) == Matrix([qj]) + assert J._fill_coordinate_list([q0, None, None], 3) == Matrix( + [q0, q1j, q2j]) + # Test autofill + assert J._fill_coordinate_list(None, 3) == Matrix([q0j, q1j, q2j]) + assert J._fill_coordinate_list([], 3) == Matrix([q0j, q1j, q2j]) + # Test offset + assert J._fill_coordinate_list([], 3, offset=1) == Matrix([q1j, q2j, q3j]) + assert J._fill_coordinate_list([q1, None, q3], 3, offset=1) == Matrix( + [q1, q2j, q3]) + assert J._fill_coordinate_list(None, 2, offset=2) == Matrix([q2j, q3j]) + # Test label + assert J._fill_coordinate_list(None, 1, 'u') == Matrix([uj]) + assert J._fill_coordinate_list([], 3, 'u') == Matrix([u0j, u1j, u2j]) + # Test single numbering + assert J._fill_coordinate_list(None, 1, number_single=True) == Matrix([q0j]) + assert J._fill_coordinate_list([], 1, 'u', 2, True) == Matrix([u2j]) + assert J._fill_coordinate_list([], 3, 'q') == Matrix([q0j, q1j, q2j]) + # Test invalid number of coordinates supplied + raises(ValueError, lambda: J._fill_coordinate_list([q0, q1], 1)) + raises(ValueError, lambda: J._fill_coordinate_list([u0, u1, None], 2, 'u')) + raises(ValueError, lambda: J._fill_coordinate_list([q0, q1], 3)) + # Test incorrect coordinate type + raises(TypeError, lambda: J._fill_coordinate_list([q0, symbols('q1')], 2)) + raises(TypeError, lambda: J._fill_coordinate_list([q0 + q1, q1], 2)) + # Test if derivative as generalized speed is allowed + _, _, P, C = _generate_body() + PinJoint('J', P, C, q1, q1.diff(t)) + # Test duplicate coordinates + _, _, P, C = _generate_body() + raises(ValueError, lambda: SphericalJoint('J', P, C, [q1j, None, None])) + raises(ValueError, lambda: SphericalJoint('J', P, C, speeds=[u0, u0, u1])) + + +def test_pin_joint(): + P = RigidBody('P') + C = RigidBody('C') + l, m = symbols('l m') + q, u = dynamicsymbols('q_J, u_J') + Pj = PinJoint('J', P, C) + assert Pj.name == 'J' + assert Pj.parent == P + assert Pj.child == C + assert Pj.coordinates == Matrix([q]) + assert Pj.speeds == Matrix([u]) + assert Pj.kdes == Matrix([u - q.diff(t)]) + assert Pj.joint_axis == P.frame.x + assert Pj.child_point.pos_from(C.masscenter) == Vector(0) + assert Pj.parent_point.pos_from(P.masscenter) == Vector(0) + assert Pj.parent_point.pos_from(Pj._child_point) == Vector(0) + assert C.masscenter.pos_from(P.masscenter) == Vector(0) + assert Pj.parent_interframe == P.frame + assert Pj.child_interframe == C.frame + assert Pj.__str__() == 'PinJoint: J parent: P child: C' + + P1 = RigidBody('P1') + C1 = RigidBody('C1') + Pint = ReferenceFrame('P_int') + Pint.orient_axis(P1.frame, P1.y, pi / 2) + J1 = PinJoint('J1', P1, C1, parent_point=l*P1.frame.x, + child_point=m*C1.frame.y, joint_axis=P1.frame.z, + parent_interframe=Pint) + assert J1._joint_axis == P1.frame.z + assert J1._child_point.pos_from(C1.masscenter) == m * C1.frame.y + assert J1._parent_point.pos_from(P1.masscenter) == l * P1.frame.x + assert J1._parent_point.pos_from(J1._child_point) == Vector(0) + assert (P1.masscenter.pos_from(C1.masscenter) == + -l*P1.frame.x + m*C1.frame.y) + assert J1.parent_interframe == Pint + assert J1.child_interframe == C1.frame + + q, u = dynamicsymbols('q, u') + N, A, P, C, Pint, Cint = _generate_body(True) + parent_point = P.masscenter.locatenew('parent_point', N.x + N.y) + child_point = C.masscenter.locatenew('child_point', C.y + C.z) + J = PinJoint('J', P, C, q, u, parent_point=parent_point, + child_point=child_point, parent_interframe=Pint, + child_interframe=Cint, joint_axis=N.z) + assert J.joint_axis == N.z + assert J.parent_point.vel(N) == 0 + assert J.parent_point == parent_point + assert J.child_point == child_point + assert J.child_point.pos_from(P.masscenter) == N.x + N.y + assert J.parent_point.pos_from(C.masscenter) == C.y + C.z + assert C.masscenter.pos_from(P.masscenter) == N.x + N.y - C.y - C.z + assert C.masscenter.vel(N).express(N) == (u * sin(q) - u * cos(q)) * N.x + ( + -u * sin(q) - u * cos(q)) * N.y + assert J.parent_interframe == Pint + assert J.child_interframe == Cint + + +def test_particle_compatibility(): + m, l = symbols('m l') + C_frame = ReferenceFrame('C') + P = Particle('P') + C = Particle('C', mass=m) + q, u = dynamicsymbols('q, u') + J = PinJoint('J', P, C, q, u, child_interframe=C_frame, + child_point=l * C_frame.y) + assert J.child_interframe == C_frame + assert J.parent_interframe.name == 'J_P_frame' + assert C.masscenter.pos_from(P.masscenter) == -l * C_frame.y + assert C_frame.dcm(J.parent_interframe) == Matrix([[1, 0, 0], + [0, cos(q), sin(q)], + [0, -sin(q), cos(q)]]) + assert C.masscenter.vel(J.parent_interframe) == -l * u * C_frame.z + # Test with specified joint axis + P_frame = ReferenceFrame('P') + C_frame = ReferenceFrame('C') + P = Particle('P') + C = Particle('C', mass=m) + q, u = dynamicsymbols('q, u') + J = PinJoint('J', P, C, q, u, parent_interframe=P_frame, + child_interframe=C_frame, child_point=l * C_frame.y, + joint_axis=P_frame.z) + assert J.joint_axis == J.parent_interframe.z + assert C_frame.dcm(J.parent_interframe) == Matrix([[cos(q), sin(q), 0], + [-sin(q), cos(q), 0], + [0, 0, 1]]) + assert P.masscenter.vel(J.parent_interframe) == 0 + assert C.masscenter.vel(J.parent_interframe) == l * u * C_frame.x + q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1:4 u1:4') + qdot_to_u = {qi.diff(t): ui for qi, ui in ((q1, u1), (q2, u2), (q3, u3))} + # Test compatibility for prismatic joint + P, C = Particle('P'), Particle('C') + J = PrismaticJoint('J', P, C, q, u) + assert J.parent_interframe.dcm(J.child_interframe) == eye(3) + assert C.masscenter.pos_from(P.masscenter) == q * J.parent_interframe.x + assert P.masscenter.vel(J.parent_interframe) == 0 + assert C.masscenter.vel(J.parent_interframe) == u * J.parent_interframe.x + # Test compatibility for cylindrical joint + P, C = Particle('P'), Particle('C') + P_frame = ReferenceFrame('P_frame') + J = CylindricalJoint('J', P, C, q1, q2, u1, u2, parent_interframe=P_frame, + parent_point=l * P_frame.x, joint_axis=P_frame.y) + assert J.parent_interframe.dcm(J.child_interframe) == Matrix([ + [cos(q1), 0, sin(q1)], [0, 1, 0], [-sin(q1), 0, cos(q1)]]) + assert C.masscenter.pos_from(P.masscenter) == l * P_frame.x + q2 * P_frame.y + assert C.masscenter.vel(J.parent_interframe) == u2 * P_frame.y + assert P.masscenter.vel(J.child_interframe).xreplace(qdot_to_u) == ( + -u2 * P_frame.y - l * u1 * P_frame.z) + # Test compatibility for planar joint + P, C = Particle('P'), Particle('C') + C_frame = ReferenceFrame('C_frame') + J = PlanarJoint('J', P, C, q1, [q2, q3], u1, [u2, u3], + child_interframe=C_frame, child_point=l * C_frame.z) + P_frame = J.parent_interframe + assert J.parent_interframe.dcm(J.child_interframe) == Matrix([ + [1, 0, 0], [0, cos(q1), -sin(q1)], [0, sin(q1), cos(q1)]]) + assert C.masscenter.pos_from(P.masscenter) == ( + -l * C_frame.z + q2 * P_frame.y + q3 * P_frame.z) + assert C.masscenter.vel(J.parent_interframe) == ( + l * u1 * C_frame.y + u2 * P_frame.y + u3 * P_frame.z) + # Test compatibility for weld joint + P, C = Particle('P'), Particle('C') + C_frame, P_frame = ReferenceFrame('C_frame'), ReferenceFrame('P_frame') + J = WeldJoint('J', P, C, parent_interframe=P_frame, + child_interframe=C_frame, parent_point=l * P_frame.x, + child_point=l * C_frame.y) + assert P_frame.dcm(C_frame) == eye(3) + assert C.masscenter.pos_from(P.masscenter) == l * P_frame.x - l * C_frame.y + assert C.masscenter.vel(J.parent_interframe) == 0 + + +def test_body_compatibility(): + m, l = symbols('m l') + C_frame = ReferenceFrame('C') + with warns_deprecated_sympy(): + P = Body('P') + C = Body('C', mass=m, frame=C_frame) + q, u = dynamicsymbols('q, u') + PinJoint('J', P, C, q, u, child_point=l * C_frame.y) + assert C.frame == C_frame + assert P.frame.name == 'P_frame' + assert C.masscenter.pos_from(P.masscenter) == -l * C.y + assert C.frame.dcm(P.frame) == Matrix([[1, 0, 0], + [0, cos(q), sin(q)], + [0, -sin(q), cos(q)]]) + assert C.masscenter.vel(P.frame) == -l * u * C.z + + +def test_pin_joint_double_pendulum(): + q1, q2 = dynamicsymbols('q1 q2') + u1, u2 = dynamicsymbols('u1 u2') + m, l = symbols('m l') + N = ReferenceFrame('N') + A = ReferenceFrame('A') + B = ReferenceFrame('B') + C = RigidBody('C', frame=N) # ceiling + PartP = RigidBody('P', frame=A, mass=m) + PartR = RigidBody('R', frame=B, mass=m) + + J1 = PinJoint('J1', C, PartP, speeds=u1, coordinates=q1, + child_point=-l*A.x, joint_axis=C.frame.z) + J2 = PinJoint('J2', PartP, PartR, speeds=u2, coordinates=q2, + child_point=-l*B.x, joint_axis=PartP.frame.z) + + # Check orientation + assert N.dcm(A) == Matrix([[cos(q1), -sin(q1), 0], + [sin(q1), cos(q1), 0], [0, 0, 1]]) + assert A.dcm(B) == Matrix([[cos(q2), -sin(q2), 0], + [sin(q2), cos(q2), 0], [0, 0, 1]]) + assert simplify(N.dcm(B)) == Matrix([[cos(q1 + q2), -sin(q1 + q2), 0], + [sin(q1 + q2), cos(q1 + q2), 0], + [0, 0, 1]]) + + # Check Angular Velocity + assert A.ang_vel_in(N) == u1 * N.z + assert B.ang_vel_in(A) == u2 * A.z + assert B.ang_vel_in(N) == u1 * N.z + u2 * A.z + + # Check kde + assert J1.kdes == Matrix([u1 - q1.diff(t)]) + assert J2.kdes == Matrix([u2 - q2.diff(t)]) + + # Check Linear Velocity + assert PartP.masscenter.vel(N) == l*u1*A.y + assert PartR.masscenter.vel(A) == l*u2*B.y + assert PartR.masscenter.vel(N) == l*u1*A.y + l*(u1 + u2)*B.y + + +def test_pin_joint_chaos_pendulum(): + mA, mB, lA, lB, h = symbols('mA, mB, lA, lB, h') + theta, phi, omega, alpha = dynamicsymbols('theta phi omega alpha') + N = ReferenceFrame('N') + A = ReferenceFrame('A') + B = ReferenceFrame('B') + lA = (lB - h / 2) / 2 + lC = (lB/2 + h/4) + rod = RigidBody('rod', frame=A, mass=mA) + plate = RigidBody('plate', mass=mB, frame=B) + C = RigidBody('C', frame=N) + J1 = PinJoint('J1', C, rod, coordinates=theta, speeds=omega, + child_point=lA*A.z, joint_axis=N.y) + J2 = PinJoint('J2', rod, plate, coordinates=phi, speeds=alpha, + parent_point=lC*A.z, joint_axis=A.z) + + # Check orientation + assert A.dcm(N) == Matrix([[cos(theta), 0, -sin(theta)], + [0, 1, 0], + [sin(theta), 0, cos(theta)]]) + assert A.dcm(B) == Matrix([[cos(phi), -sin(phi), 0], + [sin(phi), cos(phi), 0], + [0, 0, 1]]) + assert B.dcm(N) == Matrix([ + [cos(phi)*cos(theta), sin(phi), -sin(theta)*cos(phi)], + [-sin(phi)*cos(theta), cos(phi), sin(phi)*sin(theta)], + [sin(theta), 0, cos(theta)]]) + + # Check Angular Velocity + assert A.ang_vel_in(N) == omega*N.y + assert A.ang_vel_in(B) == -alpha*A.z + assert N.ang_vel_in(B) == -omega*N.y - alpha*A.z + + # Check kde + assert J1.kdes == Matrix([omega - theta.diff(t)]) + assert J2.kdes == Matrix([alpha - phi.diff(t)]) + + # Check pos of masscenters + assert C.masscenter.pos_from(rod.masscenter) == lA*A.z + assert rod.masscenter.pos_from(plate.masscenter) == - lC * A.z + + # Check Linear Velocities + assert rod.masscenter.vel(N) == (h/4 - lB/2)*omega*A.x + assert plate.masscenter.vel(N) == ((h/4 - lB/2)*omega + + (h/4 + lB/2)*omega)*A.x + + +def test_pin_joint_interframe(): + q, u = dynamicsymbols('q, u') + # Check not connected + N, A, P, C = _generate_body() + Pint, Cint = ReferenceFrame('Pint'), ReferenceFrame('Cint') + raises(ValueError, lambda: PinJoint('J', P, C, parent_interframe=Pint)) + raises(ValueError, lambda: PinJoint('J', P, C, child_interframe=Cint)) + # Check not fixed interframe + Pint.orient_axis(N, N.z, q) + Cint.orient_axis(A, A.z, q) + raises(ValueError, lambda: PinJoint('J', P, C, parent_interframe=Pint)) + raises(ValueError, lambda: PinJoint('J', P, C, child_interframe=Cint)) + # Check only parent_interframe + N, A, P, C = _generate_body() + Pint = ReferenceFrame('Pint') + Pint.orient_body_fixed(N, (pi / 4, pi, pi / 3), 'xyz') + PinJoint('J', P, C, q, u, parent_point=N.x, child_point=-C.y, + parent_interframe=Pint, joint_axis=Pint.x) + assert simplify(N.dcm(A)) - Matrix([ + [-1 / 2, sqrt(3) * cos(q) / 2, -sqrt(3) * sin(q) / 2], + [sqrt(6) / 4, sqrt(2) * (2 * sin(q) + cos(q)) / 4, + sqrt(2) * (-sin(q) + 2 * cos(q)) / 4], + [sqrt(6) / 4, sqrt(2) * (-2 * sin(q) + cos(q)) / 4, + -sqrt(2) * (sin(q) + 2 * cos(q)) / 4]]) == zeros(3) + assert A.ang_vel_in(N) == u * Pint.x + assert C.masscenter.pos_from(P.masscenter) == N.x + A.y + assert C.masscenter.vel(N) == u * A.z + assert P.masscenter.vel(Pint) == Vector(0) + assert C.masscenter.vel(Pint) == u * A.z + # Check only child_interframe + N, A, P, C = _generate_body() + Cint = ReferenceFrame('Cint') + Cint.orient_body_fixed(A, (2 * pi / 3, -pi, pi / 2), 'xyz') + PinJoint('J', P, C, q, u, parent_point=-N.z, child_point=C.x, + child_interframe=Cint, joint_axis=P.x + P.z) + assert simplify(N.dcm(A)) == Matrix([ + [-sqrt(2) * sin(q) / 2, + -sqrt(3) * (cos(q) - 1) / 4 - cos(q) / 4 - S(1) / 4, + sqrt(3) * (cos(q) + 1) / 4 - cos(q) / 4 + S(1) / 4], + [cos(q), (sqrt(2) + sqrt(6)) * -sin(q) / 4, + (-sqrt(2) + sqrt(6)) * sin(q) / 4], + [sqrt(2) * sin(q) / 2, + sqrt(3) * (cos(q) + 1) / 4 + cos(q) / 4 - S(1) / 4, + sqrt(3) * (1 - cos(q)) / 4 + cos(q) / 4 + S(1) / 4]]) + assert A.ang_vel_in(N) == sqrt(2) * u / 2 * N.x + sqrt(2) * u / 2 * N.z + assert C.masscenter.pos_from(P.masscenter) == - N.z - A.x + assert C.masscenter.vel(N).simplify() == ( + -sqrt(6) - sqrt(2)) * u / 4 * A.y + ( + -sqrt(2) + sqrt(6)) * u / 4 * A.z + assert C.masscenter.vel(Cint) == Vector(0) + # Check combination + N, A, P, C = _generate_body() + Pint, Cint = ReferenceFrame('Pint'), ReferenceFrame('Cint') + Pint.orient_body_fixed(N, (-pi / 2, pi, pi / 2), 'xyz') + Cint.orient_body_fixed(A, (2 * pi / 3, -pi, pi / 2), 'xyz') + PinJoint('J', P, C, q, u, parent_point=N.x - N.y, child_point=-C.z, + parent_interframe=Pint, child_interframe=Cint, + joint_axis=Pint.x + Pint.z) + assert simplify(N.dcm(A)) == Matrix([ + [cos(q), (sqrt(2) + sqrt(6)) * -sin(q) / 4, + (-sqrt(2) + sqrt(6)) * sin(q) / 4], + [-sqrt(2) * sin(q) / 2, + -sqrt(3) * (cos(q) + 1) / 4 - cos(q) / 4 + S(1) / 4, + sqrt(3) * (cos(q) - 1) / 4 - cos(q) / 4 - S(1) / 4], + [sqrt(2) * sin(q) / 2, + sqrt(3) * (cos(q) - 1) / 4 + cos(q) / 4 + S(1) / 4, + -sqrt(3) * (cos(q) + 1) / 4 + cos(q) / 4 - S(1) / 4]]) + assert A.ang_vel_in(N) == sqrt(2) * u / 2 * Pint.x + sqrt( + 2) * u / 2 * Pint.z + assert C.masscenter.pos_from(P.masscenter) == N.x - N.y + A.z + N_v_C = (-sqrt(2) + sqrt(6)) * u / 4 * A.x + assert C.masscenter.vel(N).simplify() == N_v_C + assert C.masscenter.vel(Pint).simplify() == N_v_C + assert C.masscenter.vel(Cint) == Vector(0) + + +def test_pin_joint_joint_axis(): + q, u = dynamicsymbols('q, u') + # Check parent as reference + N, A, P, C, Pint, Cint = _generate_body(True) + pin = PinJoint('J', P, C, q, u, parent_interframe=Pint, + child_interframe=Cint, joint_axis=P.y) + assert pin.joint_axis == P.y + assert N.dcm(A) == Matrix([[sin(q), 0, cos(q)], [0, -1, 0], + [cos(q), 0, -sin(q)]]) + # Check parent_interframe as reference + N, A, P, C, Pint, Cint = _generate_body(True) + pin = PinJoint('J', P, C, q, u, parent_interframe=Pint, + child_interframe=Cint, joint_axis=Pint.y) + assert pin.joint_axis == Pint.y + assert N.dcm(A) == Matrix([[-sin(q), 0, cos(q)], [0, -1, 0], + [cos(q), 0, sin(q)]]) + # Check combination of joint_axis with interframes supplied as vectors (2x) + N, A, P, C = _generate_body() + pin = PinJoint('J', P, C, q, u, parent_interframe=N.z, + child_interframe=-C.z, joint_axis=N.z) + assert pin.joint_axis == N.z + assert N.dcm(A) == Matrix([[-cos(q), -sin(q), 0], [-sin(q), cos(q), 0], + [0, 0, -1]]) + N, A, P, C = _generate_body() + pin = PinJoint('J', P, C, q, u, parent_interframe=N.z, + child_interframe=-C.z, joint_axis=N.x) + assert pin.joint_axis == N.x + assert N.dcm(A) == Matrix([[-1, 0, 0], [0, cos(q), sin(q)], + [0, sin(q), -cos(q)]]) + # Check time varying axis + N, A, P, C, Pint, Cint = _generate_body(True) + raises(ValueError, lambda: PinJoint('J', P, C, + joint_axis=cos(q) * N.x + sin(q) * N.y)) + # Check joint_axis provided in child frame + raises(ValueError, lambda: PinJoint('J', P, C, joint_axis=C.x)) + # Check some invalid combinations + raises(ValueError, lambda: PinJoint('J', P, C, joint_axis=P.x + C.y)) + raises(ValueError, lambda: PinJoint( + 'J', P, C, parent_interframe=Pint, child_interframe=Cint, + joint_axis=Pint.x + C.y)) + raises(ValueError, lambda: PinJoint( + 'J', P, C, parent_interframe=Pint, child_interframe=Cint, + joint_axis=P.x + Cint.y)) + # Check valid special combination + N, A, P, C, Pint, Cint = _generate_body(True) + PinJoint('J', P, C, parent_interframe=Pint, child_interframe=Cint, + joint_axis=Pint.x + P.y) + # Check invalid zero vector + raises(Exception, lambda: PinJoint( + 'J', P, C, parent_interframe=Pint, child_interframe=Cint, + joint_axis=Vector(0))) + raises(Exception, lambda: PinJoint( + 'J', P, C, parent_interframe=Pint, child_interframe=Cint, + joint_axis=P.y + Pint.y)) + + +def test_pin_joint_arbitrary_axis(): + q, u = dynamicsymbols('q_J, u_J') + + # When the bodies are attached though masscenters but axes are opposite. + N, A, P, C = _generate_body() + PinJoint('J', P, C, child_interframe=-A.x) + + assert (-A.x).angle_between(N.x) == 0 + assert -A.x.express(N) == N.x + assert A.dcm(N) == Matrix([[-1, 0, 0], + [0, -cos(q), -sin(q)], + [0, -sin(q), cos(q)]]) + assert A.ang_vel_in(N) == u*N.x + assert A.ang_vel_in(N).magnitude() == sqrt(u**2) + assert C.masscenter.pos_from(P.masscenter) == 0 + assert C.masscenter.pos_from(P.masscenter).express(N).simplify() == 0 + assert C.masscenter.vel(N) == 0 + + # When axes are different and parent joint is at masscenter but child joint + # is at a unit vector from child masscenter. + N, A, P, C = _generate_body() + PinJoint('J', P, C, child_interframe=A.y, child_point=A.x) + + assert A.y.angle_between(N.x) == 0 # Axis are aligned + assert A.y.express(N) == N.x + assert A.dcm(N) == Matrix([[0, -cos(q), -sin(q)], + [1, 0, 0], + [0, -sin(q), cos(q)]]) + assert A.ang_vel_in(N) == u*N.x + assert A.ang_vel_in(N).express(A) == u * A.y + assert A.ang_vel_in(N).magnitude() == sqrt(u**2) + assert A.ang_vel_in(N).cross(A.y) == 0 + assert C.masscenter.vel(N) == u*A.z + assert C.masscenter.pos_from(P.masscenter) == -A.x + assert (C.masscenter.pos_from(P.masscenter).express(N).simplify() == + cos(q)*N.y + sin(q)*N.z) + assert C.masscenter.vel(N).angle_between(A.x) == pi/2 + + # Similar to previous case but wrt parent body + N, A, P, C = _generate_body() + PinJoint('J', P, C, parent_interframe=N.y, parent_point=N.x) + + assert N.y.angle_between(A.x) == 0 # Axis are aligned + assert N.y.express(A) == A.x + assert A.dcm(N) == Matrix([[0, 1, 0], + [-cos(q), 0, sin(q)], + [sin(q), 0, cos(q)]]) + assert A.ang_vel_in(N) == u*N.y + assert A.ang_vel_in(N).express(A) == u*A.x + assert A.ang_vel_in(N).magnitude() == sqrt(u**2) + angle = A.ang_vel_in(N).angle_between(A.x) + assert angle.xreplace({u: 1}) == 0 + assert C.masscenter.vel(N) == 0 + assert C.masscenter.pos_from(P.masscenter) == N.x + + # Both joint pos id defined but different axes + N, A, P, C = _generate_body() + PinJoint('J', P, C, parent_point=N.x, child_point=A.x, + child_interframe=A.x + A.y) + assert expand_mul(N.x.angle_between(A.x + A.y)) == 0 # Axis are aligned + assert (A.x + A.y).express(N).simplify() == sqrt(2)*N.x + assert simplify(A.dcm(N)) == Matrix([ + [sqrt(2)/2, -sqrt(2)*cos(q)/2, -sqrt(2)*sin(q)/2], + [sqrt(2)/2, sqrt(2)*cos(q)/2, sqrt(2)*sin(q)/2], + [0, -sin(q), cos(q)]]) + assert A.ang_vel_in(N) == u*N.x + assert (A.ang_vel_in(N).express(A).simplify() == + (u*A.x + u*A.y)/sqrt(2)) + assert A.ang_vel_in(N).magnitude() == sqrt(u**2) + angle = A.ang_vel_in(N).angle_between(A.x + A.y) + assert angle.xreplace({u: 1}) == 0 + assert C.masscenter.vel(N).simplify() == (u * A.z)/sqrt(2) + assert C.masscenter.pos_from(P.masscenter) == N.x - A.x + assert (C.masscenter.pos_from(P.masscenter).express(N).simplify() == + (1 - sqrt(2)/2)*N.x + sqrt(2)*cos(q)/2*N.y + + sqrt(2)*sin(q)/2*N.z) + assert (C.masscenter.vel(N).express(N).simplify() == + -sqrt(2)*u*sin(q)/2*N.y + sqrt(2)*u*cos(q)/2*N.z) + assert C.masscenter.vel(N).angle_between(A.x) == pi/2 + + N, A, P, C = _generate_body() + PinJoint('J', P, C, parent_point=N.x, child_point=A.x, + child_interframe=A.x + A.y - A.z) + assert expand_mul(N.x.angle_between(A.x + A.y - A.z)) == 0 # Axis aligned + assert (A.x + A.y - A.z).express(N).simplify() == sqrt(3)*N.x + assert simplify(A.dcm(N)) == Matrix([ + [sqrt(3)/3, -sqrt(6)*sin(q + pi/4)/3, + sqrt(6)*cos(q + pi/4)/3], + [sqrt(3)/3, sqrt(6)*cos(q + pi/12)/3, + sqrt(6)*sin(q + pi/12)/3], + [-sqrt(3)/3, sqrt(6)*cos(q + 5*pi/12)/3, + sqrt(6)*sin(q + 5*pi/12)/3]]) + assert A.ang_vel_in(N) == u*N.x + assert A.ang_vel_in(N).express(A).simplify() == (u*A.x + u*A.y - + u*A.z)/sqrt(3) + assert A.ang_vel_in(N).magnitude() == sqrt(u**2) + angle = A.ang_vel_in(N).angle_between(A.x + A.y-A.z) + assert angle.xreplace({u: 1}).simplify() == 0 + assert C.masscenter.vel(N).simplify() == (u*A.y + u*A.z)/sqrt(3) + assert C.masscenter.pos_from(P.masscenter) == N.x - A.x + assert (C.masscenter.pos_from(P.masscenter).express(N).simplify() == + (1 - sqrt(3)/3)*N.x + sqrt(6)*sin(q + pi/4)/3*N.y - + sqrt(6)*cos(q + pi/4)/3*N.z) + assert (C.masscenter.vel(N).express(N).simplify() == + sqrt(6)*u*cos(q + pi/4)/3*N.y + + sqrt(6)*u*sin(q + pi/4)/3*N.z) + assert C.masscenter.vel(N).angle_between(A.x) == pi/2 + + N, A, P, C = _generate_body() + m, n = symbols('m n') + PinJoint('J', P, C, parent_point=m * N.x, child_point=n * A.x, + child_interframe=A.x + A.y - A.z, + parent_interframe=N.x - N.y + N.z) + angle = (N.x - N.y + N.z).angle_between(A.x + A.y - A.z) + assert expand_mul(angle) == 0 # Axis are aligned + assert ((A.x-A.y+A.z).express(N).simplify() == + (-4*cos(q)/3 - S(1)/3)*N.x + (S(1)/3 - 4*sin(q + pi/6)/3)*N.y + + (4*cos(q + pi/3)/3 - S(1)/3)*N.z) + assert simplify(A.dcm(N)) == Matrix([ + [S(1)/3 - 2*cos(q)/3, -2*sin(q + pi/6)/3 - S(1)/3, + 2*cos(q + pi/3)/3 + S(1)/3], + [2*cos(q + pi/3)/3 + S(1)/3, 2*cos(q)/3 - S(1)/3, + 2*sin(q + pi/6)/3 + S(1)/3], + [-2*sin(q + pi/6)/3 - S(1)/3, 2*cos(q + pi/3)/3 + S(1)/3, + 2*cos(q)/3 - S(1)/3]]) + assert (A.ang_vel_in(N) - (u*N.x - u*N.y + u*N.z)/sqrt(3)).simplify() + assert A.ang_vel_in(N).express(A).simplify() == (u*A.x + u*A.y - + u*A.z)/sqrt(3) + assert A.ang_vel_in(N).magnitude() == sqrt(u**2) + angle = A.ang_vel_in(N).angle_between(A.x+A.y-A.z) + assert angle.xreplace({u: 1}).simplify() == 0 + assert (C.masscenter.vel(N).simplify() == + sqrt(3)*n*u/3*A.y + sqrt(3)*n*u/3*A.z) + assert C.masscenter.pos_from(P.masscenter) == m*N.x - n*A.x + assert (C.masscenter.pos_from(P.masscenter).express(N).simplify() == + (m + n*(2*cos(q) - 1)/3)*N.x + n*(2*sin(q + pi/6) + + 1)/3*N.y - n*(2*cos(q + pi/3) + 1)/3*N.z) + assert (C.masscenter.vel(N).express(N).simplify() == + - 2*n*u*sin(q)/3*N.x + 2*n*u*cos(q + pi/6)/3*N.y + + 2*n*u*sin(q + pi/3)/3*N.z) + assert C.masscenter.vel(N).dot(N.x - N.y + N.z).simplify() == 0 + + +def test_create_aligned_frame_pi(): + N, A, P, C = _generate_body() + f = Joint._create_aligned_interframe(P, -P.x, P.x) + assert f.z == P.z + f = Joint._create_aligned_interframe(P, -P.y, P.y) + assert f.x == P.x + f = Joint._create_aligned_interframe(P, -P.z, P.z) + assert f.y == P.y + f = Joint._create_aligned_interframe(P, -P.x - P.y, P.x + P.y) + assert f.z == P.z + f = Joint._create_aligned_interframe(P, -P.y - P.z, P.y + P.z) + assert f.x == P.x + f = Joint._create_aligned_interframe(P, -P.x - P.z, P.x + P.z) + assert f.y == P.y + f = Joint._create_aligned_interframe(P, -P.x - P.y - P.z, P.x + P.y + P.z) + assert f.y - f.z == P.y - P.z + + +def test_pin_joint_axis(): + q, u = dynamicsymbols('q u') + # Test default joint axis + N, A, P, C, Pint, Cint = _generate_body(True) + J = PinJoint('J', P, C, q, u, parent_interframe=Pint, child_interframe=Cint) + assert J.joint_axis == Pint.x + # Test for the same joint axis expressed in different frames + N_R_A = Matrix([[0, sin(q), cos(q)], + [0, -cos(q), sin(q)], + [1, 0, 0]]) + N, A, P, C, Pint, Cint = _generate_body(True) + PinJoint('J', P, C, q, u, parent_interframe=Pint, child_interframe=Cint, + joint_axis=N.z) + assert N.dcm(A) == N_R_A + N, A, P, C, Pint, Cint = _generate_body(True) + PinJoint('J', P, C, q, u, parent_interframe=Pint, child_interframe=Cint, + joint_axis=-Pint.z) + assert N.dcm(A) == N_R_A + # Test time varying joint axis + N, A, P, C, Pint, Cint = _generate_body(True) + raises(ValueError, lambda: PinJoint('J', P, C, joint_axis=q * N.z)) + + +def test_locate_joint_pos(): + # Test Vector and default + N, A, P, C = _generate_body() + joint = PinJoint('J', P, C, parent_point=N.y + N.z) + assert joint.parent_point.name == 'J_P_joint' + assert joint.parent_point.pos_from(P.masscenter) == N.y + N.z + assert joint.child_point == C.masscenter + # Test Point objects + N, A, P, C = _generate_body() + parent_point = P.masscenter.locatenew('p', N.y + N.z) + joint = PinJoint('J', P, C, parent_point=parent_point, + child_point=C.masscenter) + assert joint.parent_point == parent_point + assert joint.child_point == C.masscenter + # Check invalid type + N, A, P, C = _generate_body() + raises(TypeError, + lambda: PinJoint('J', P, C, parent_point=N.x.to_matrix(N))) + # Test time varying positions + q = dynamicsymbols('q') + N, A, P, C = _generate_body() + raises(ValueError, lambda: PinJoint('J', P, C, parent_point=q * N.x)) + N, A, P, C = _generate_body() + child_point = C.masscenter.locatenew('p', q * A.y) + raises(ValueError, lambda: PinJoint('J', P, C, child_point=child_point)) + # Test undefined position + child_point = Point('p') + raises(ValueError, lambda: PinJoint('J', P, C, child_point=child_point)) + + +def test_locate_joint_frame(): + # Test rotated frame and default + N, A, P, C = _generate_body() + parent_interframe = ReferenceFrame('int_frame') + parent_interframe.orient_axis(N, N.z, 1) + joint = PinJoint('J', P, C, parent_interframe=parent_interframe) + assert joint.parent_interframe == parent_interframe + assert joint.parent_interframe.ang_vel_in(N) == 0 + assert joint.child_interframe == A + # Test time varying orientations + q = dynamicsymbols('q') + N, A, P, C = _generate_body() + parent_interframe = ReferenceFrame('int_frame') + parent_interframe.orient_axis(N, N.z, q) + raises(ValueError, + lambda: PinJoint('J', P, C, parent_interframe=parent_interframe)) + # Test undefined frame + N, A, P, C = _generate_body() + child_interframe = ReferenceFrame('int_frame') + child_interframe.orient_axis(N, N.z, 1) # Defined with respect to parent + raises(ValueError, + lambda: PinJoint('J', P, C, child_interframe=child_interframe)) + + +def test_prismatic_joint(): + _, _, P, C = _generate_body() + q, u = dynamicsymbols('q_S, u_S') + S = PrismaticJoint('S', P, C) + assert S.name == 'S' + assert S.parent == P + assert S.child == C + assert S.coordinates == Matrix([q]) + assert S.speeds == Matrix([u]) + assert S.kdes == Matrix([u - q.diff(t)]) + assert S.joint_axis == P.frame.x + assert S.child_point.pos_from(C.masscenter) == Vector(0) + assert S.parent_point.pos_from(P.masscenter) == Vector(0) + assert S.parent_point.pos_from(S.child_point) == - q * P.frame.x + assert P.masscenter.pos_from(C.masscenter) == - q * P.frame.x + assert C.masscenter.vel(P.frame) == u * P.frame.x + assert P.frame.ang_vel_in(C.frame) == 0 + assert C.frame.ang_vel_in(P.frame) == 0 + assert S.__str__() == 'PrismaticJoint: S parent: P child: C' + + N, A, P, C = _generate_body() + l, m = symbols('l m') + Pint = ReferenceFrame('P_int') + Pint.orient_axis(P.frame, P.y, pi / 2) + S = PrismaticJoint('S', P, C, parent_point=l * P.frame.x, + child_point=m * C.frame.y, joint_axis=P.frame.z, + parent_interframe=Pint) + + assert S.joint_axis == P.frame.z + assert S.child_point.pos_from(C.masscenter) == m * C.frame.y + assert S.parent_point.pos_from(P.masscenter) == l * P.frame.x + assert S.parent_point.pos_from(S.child_point) == - q * P.frame.z + assert P.masscenter.pos_from(C.masscenter) == - l * N.x - q * N.z + m * A.y + assert C.masscenter.vel(P.frame) == u * P.frame.z + assert P.masscenter.vel(Pint) == Vector(0) + assert C.frame.ang_vel_in(P.frame) == 0 + assert P.frame.ang_vel_in(C.frame) == 0 + + _, _, P, C = _generate_body() + Pint = ReferenceFrame('P_int') + Pint.orient_axis(P.frame, P.y, pi / 2) + S = PrismaticJoint('S', P, C, parent_point=l * P.frame.z, + child_point=m * C.frame.x, joint_axis=P.frame.z, + parent_interframe=Pint) + assert S.joint_axis == P.frame.z + assert S.child_point.pos_from(C.masscenter) == m * C.frame.x + assert S.parent_point.pos_from(P.masscenter) == l * P.frame.z + assert S.parent_point.pos_from(S.child_point) == - q * P.frame.z + assert P.masscenter.pos_from(C.masscenter) == (-l - q)*P.frame.z + m*C.frame.x + assert C.masscenter.vel(P.frame) == u * P.frame.z + assert C.frame.ang_vel_in(P.frame) == 0 + assert P.frame.ang_vel_in(C.frame) == 0 + + +def test_prismatic_joint_arbitrary_axis(): + q, u = dynamicsymbols('q_S, u_S') + + N, A, P, C = _generate_body() + PrismaticJoint('S', P, C, child_interframe=-A.x) + + assert (-A.x).angle_between(N.x) == 0 + assert -A.x.express(N) == N.x + assert A.dcm(N) == Matrix([[-1, 0, 0], [0, -1, 0], [0, 0, 1]]) + assert C.masscenter.pos_from(P.masscenter) == q * N.x + assert C.masscenter.pos_from(P.masscenter).express(A).simplify() == -q * A.x + assert C.masscenter.vel(N) == u * N.x + assert C.masscenter.vel(N).express(A) == -u * A.x + assert A.ang_vel_in(N) == 0 + assert N.ang_vel_in(A) == 0 + + #When axes are different and parent joint is at masscenter but child joint is at a unit vector from + #child masscenter. + N, A, P, C = _generate_body() + PrismaticJoint('S', P, C, child_interframe=A.y, child_point=A.x) + + assert A.y.angle_between(N.x) == 0 #Axis are aligned + assert A.y.express(N) == N.x + assert A.dcm(N) == Matrix([[0, -1, 0], [1, 0, 0], [0, 0, 1]]) + assert C.masscenter.vel(N) == u * N.x + assert C.masscenter.vel(N).express(A) == u * A.y + assert C.masscenter.pos_from(P.masscenter) == q*N.x - A.x + assert C.masscenter.pos_from(P.masscenter).express(N).simplify() == q*N.x + N.y + assert A.ang_vel_in(N) == 0 + assert N.ang_vel_in(A) == 0 + + #Similar to previous case but wrt parent body + N, A, P, C = _generate_body() + PrismaticJoint('S', P, C, parent_interframe=N.y, parent_point=N.x) + + assert N.y.angle_between(A.x) == 0 #Axis are aligned + assert N.y.express(A) == A.x + assert A.dcm(N) == Matrix([[0, 1, 0], [-1, 0, 0], [0, 0, 1]]) + assert C.masscenter.vel(N) == u * N.y + assert C.masscenter.vel(N).express(A) == u * A.x + assert C.masscenter.pos_from(P.masscenter) == N.x + q*N.y + assert A.ang_vel_in(N) == 0 + assert N.ang_vel_in(A) == 0 + + #Both joint pos is defined but different axes + N, A, P, C = _generate_body() + PrismaticJoint('S', P, C, parent_point=N.x, child_point=A.x, + child_interframe=A.x + A.y) + assert N.x.angle_between(A.x + A.y) == 0 #Axis are aligned + assert (A.x + A.y).express(N) == sqrt(2)*N.x + assert A.dcm(N) == Matrix([[sqrt(2)/2, -sqrt(2)/2, 0], [sqrt(2)/2, sqrt(2)/2, 0], [0, 0, 1]]) + assert C.masscenter.pos_from(P.masscenter) == (q + 1)*N.x - A.x + assert C.masscenter.pos_from(P.masscenter).express(N) == (q - sqrt(2)/2 + 1)*N.x + sqrt(2)/2*N.y + assert C.masscenter.vel(N).express(A) == u * (A.x + A.y)/sqrt(2) + assert C.masscenter.vel(N) == u*N.x + assert A.ang_vel_in(N) == 0 + assert N.ang_vel_in(A) == 0 + + N, A, P, C = _generate_body() + PrismaticJoint('S', P, C, parent_point=N.x, child_point=A.x, + child_interframe=A.x + A.y - A.z) + assert N.x.angle_between(A.x + A.y - A.z).simplify() == 0 #Axis are aligned + assert ((A.x + A.y - A.z).express(N) - sqrt(3)*N.x).simplify() == 0 + assert simplify(A.dcm(N)) == Matrix([[sqrt(3)/3, -sqrt(3)/3, sqrt(3)/3], + [sqrt(3)/3, sqrt(3)/6 + S(1)/2, S(1)/2 - sqrt(3)/6], + [-sqrt(3)/3, S(1)/2 - sqrt(3)/6, sqrt(3)/6 + S(1)/2]]) + assert C.masscenter.pos_from(P.masscenter) == (q + 1)*N.x - A.x + assert (C.masscenter.pos_from(P.masscenter).express(N) - + ((q - sqrt(3)/3 + 1)*N.x + sqrt(3)/3*N.y - sqrt(3)/3*N.z)).simplify() == 0 + assert C.masscenter.vel(N) == u*N.x + assert (C.masscenter.vel(N).express(A) - ( + sqrt(3)*u/3*A.x + sqrt(3)*u/3*A.y - sqrt(3)*u/3*A.z)).simplify() + assert A.ang_vel_in(N) == 0 + assert N.ang_vel_in(A) == 0 + + N, A, P, C = _generate_body() + m, n = symbols('m n') + PrismaticJoint('S', P, C, parent_point=m*N.x, child_point=n*A.x, + child_interframe=A.x + A.y - A.z, + parent_interframe=N.x - N.y + N.z) + # 0 angle means that the axis are aligned + assert (N.x-N.y+N.z).angle_between(A.x+A.y-A.z).simplify() == 0 + assert ((A.x+A.y-A.z).express(N) - (N.x - N.y + N.z)).simplify() == 0 + assert simplify(A.dcm(N)) == Matrix([[-S(1)/3, -S(2)/3, S(2)/3], + [S(2)/3, S(1)/3, S(2)/3], + [-S(2)/3, S(2)/3, S(1)/3]]) + assert (C.masscenter.pos_from(P.masscenter) - ( + (m + sqrt(3)*q/3)*N.x - sqrt(3)*q/3*N.y + sqrt(3)*q/3*N.z - n*A.x) + ).express(N).simplify() == 0 + assert (C.masscenter.pos_from(P.masscenter).express(N) - ( + (m + n/3 + sqrt(3)*q/3)*N.x + (2*n/3 - sqrt(3)*q/3)*N.y + + (-2*n/3 + sqrt(3)*q/3)*N.z)).simplify() == 0 + assert (C.masscenter.vel(N).express(N) - ( + sqrt(3)*u/3*N.x - sqrt(3)*u/3*N.y + sqrt(3)*u/3*N.z)).simplify() == 0 + assert (C.masscenter.vel(N).express(A) - + (sqrt(3)*u/3*A.x + sqrt(3)*u/3*A.y - sqrt(3)*u/3*A.z)).simplify() == 0 + assert A.ang_vel_in(N) == 0 + assert N.ang_vel_in(A) == 0 + + +def test_cylindrical_joint(): + N, A, P, C = _generate_body() + q0_def, q1_def, u0_def, u1_def = dynamicsymbols('q0:2_J, u0:2_J') + Cj = CylindricalJoint('J', P, C) + assert Cj.name == 'J' + assert Cj.parent == P + assert Cj.child == C + assert Cj.coordinates == Matrix([q0_def, q1_def]) + assert Cj.speeds == Matrix([u0_def, u1_def]) + assert Cj.rotation_coordinate == q0_def + assert Cj.translation_coordinate == q1_def + assert Cj.rotation_speed == u0_def + assert Cj.translation_speed == u1_def + assert Cj.kdes == Matrix([u0_def - q0_def.diff(t), u1_def - q1_def.diff(t)]) + assert Cj.joint_axis == N.x + assert Cj.child_point.pos_from(C.masscenter) == Vector(0) + assert Cj.parent_point.pos_from(P.masscenter) == Vector(0) + assert Cj.parent_point.pos_from(Cj._child_point) == -q1_def * N.x + assert C.masscenter.pos_from(P.masscenter) == q1_def * N.x + assert Cj.child_point.vel(N) == u1_def * N.x + assert A.ang_vel_in(N) == u0_def * N.x + assert Cj.parent_interframe == N + assert Cj.child_interframe == A + assert Cj.__str__() == 'CylindricalJoint: J parent: P child: C' + + q0, q1, u0, u1 = dynamicsymbols('q0:2, u0:2') + l, m = symbols('l, m') + N, A, P, C, Pint, Cint = _generate_body(True) + Cj = CylindricalJoint('J', P, C, rotation_coordinate=q0, rotation_speed=u0, + translation_speed=u1, parent_point=m * N.x, + child_point=l * A.y, parent_interframe=Pint, + child_interframe=Cint, joint_axis=2 * N.z) + assert Cj.coordinates == Matrix([q0, q1_def]) + assert Cj.speeds == Matrix([u0, u1]) + assert Cj.rotation_coordinate == q0 + assert Cj.translation_coordinate == q1_def + assert Cj.rotation_speed == u0 + assert Cj.translation_speed == u1 + assert Cj.kdes == Matrix([u0 - q0.diff(t), u1 - q1_def.diff(t)]) + assert Cj.joint_axis == 2 * N.z + assert Cj.child_point.pos_from(C.masscenter) == l * A.y + assert Cj.parent_point.pos_from(P.masscenter) == m * N.x + assert Cj.parent_point.pos_from(Cj._child_point) == -q1_def * N.z + assert C.masscenter.pos_from( + P.masscenter) == m * N.x + q1_def * N.z - l * A.y + assert C.masscenter.vel(N) == u1 * N.z - u0 * l * A.z + assert A.ang_vel_in(N) == u0 * N.z + + +def test_planar_joint(): + N, A, P, C = _generate_body() + q0_def, q1_def, q2_def = dynamicsymbols('q0:3_J') + u0_def, u1_def, u2_def = dynamicsymbols('u0:3_J') + Cj = PlanarJoint('J', P, C) + assert Cj.name == 'J' + assert Cj.parent == P + assert Cj.child == C + assert Cj.coordinates == Matrix([q0_def, q1_def, q2_def]) + assert Cj.speeds == Matrix([u0_def, u1_def, u2_def]) + assert Cj.rotation_coordinate == q0_def + assert Cj.planar_coordinates == Matrix([q1_def, q2_def]) + assert Cj.rotation_speed == u0_def + assert Cj.planar_speeds == Matrix([u1_def, u2_def]) + assert Cj.kdes == Matrix([u0_def - q0_def.diff(t), u1_def - q1_def.diff(t), + u2_def - q2_def.diff(t)]) + assert Cj.rotation_axis == N.x + assert Cj.planar_vectors == [N.y, N.z] + assert Cj.child_point.pos_from(C.masscenter) == Vector(0) + assert Cj.parent_point.pos_from(P.masscenter) == Vector(0) + r_P_C = q1_def * N.y + q2_def * N.z + assert Cj.parent_point.pos_from(Cj.child_point) == -r_P_C + assert C.masscenter.pos_from(P.masscenter) == r_P_C + assert Cj.child_point.vel(N) == u1_def * N.y + u2_def * N.z + assert A.ang_vel_in(N) == u0_def * N.x + assert Cj.parent_interframe == N + assert Cj.child_interframe == A + assert Cj.__str__() == 'PlanarJoint: J parent: P child: C' + + q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3, u0:3') + l, m = symbols('l, m') + N, A, P, C, Pint, Cint = _generate_body(True) + Cj = PlanarJoint('J', P, C, rotation_coordinate=q0, + planar_coordinates=[q1, q2], planar_speeds=[u1, u2], + parent_point=m * N.x, child_point=l * A.y, + parent_interframe=Pint, child_interframe=Cint) + assert Cj.coordinates == Matrix([q0, q1, q2]) + assert Cj.speeds == Matrix([u0_def, u1, u2]) + assert Cj.rotation_coordinate == q0 + assert Cj.planar_coordinates == Matrix([q1, q2]) + assert Cj.rotation_speed == u0_def + assert Cj.planar_speeds == Matrix([u1, u2]) + assert Cj.kdes == Matrix([u0_def - q0.diff(t), u1 - q1.diff(t), + u2 - q2.diff(t)]) + assert Cj.rotation_axis == Pint.x + assert Cj.planar_vectors == [Pint.y, Pint.z] + assert Cj.child_point.pos_from(C.masscenter) == l * A.y + assert Cj.parent_point.pos_from(P.masscenter) == m * N.x + assert Cj.parent_point.pos_from(Cj.child_point) == q1 * N.y + q2 * N.z + assert C.masscenter.pos_from( + P.masscenter) == m * N.x - q1 * N.y - q2 * N.z - l * A.y + assert C.masscenter.vel(N) == -u1 * N.y - u2 * N.z + u0_def * l * A.x + assert A.ang_vel_in(N) == u0_def * N.x + + +def test_planar_joint_advanced(): + # Tests whether someone is able to just specify two normals, which will form + # the rotation axis seen from the parent and child body. + # This specific example is a block on a slope, which has that same slope of + # 30 degrees, so in the zero configuration the frames of the parent and + # child are actually aligned. + q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3, u0:3') + l1, l2 = symbols('l1:3') + N, A, P, C = _generate_body() + J = PlanarJoint('J', P, C, q0, [q1, q2], u0, [u1, u2], + parent_point=l1 * N.z, + child_point=-l2 * C.z, + parent_interframe=N.z + N.y / sqrt(3), + child_interframe=A.z + A.y / sqrt(3)) + assert J.rotation_axis.express(N) == (N.z + N.y / sqrt(3)).normalize() + assert J.rotation_axis.express(A) == (A.z + A.y / sqrt(3)).normalize() + assert J.rotation_axis.angle_between(N.z) == pi / 6 + assert N.dcm(A).xreplace({q0: 0, q1: 0, q2: 0}) == eye(3) + N_R_A = Matrix([ + [cos(q0), -sqrt(3) * sin(q0) / 2, sin(q0) / 2], + [sqrt(3) * sin(q0) / 2, 3 * cos(q0) / 4 + 1 / 4, + sqrt(3) * (1 - cos(q0)) / 4], + [-sin(q0) / 2, sqrt(3) * (1 - cos(q0)) / 4, cos(q0) / 4 + 3 / 4]]) + # N.dcm(A) == N_R_A did not work + assert simplify(N.dcm(A) - N_R_A) == zeros(3) + + +def test_spherical_joint(): + N, A, P, C = _generate_body() + q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3_S, u0:3_S') + S = SphericalJoint('S', P, C) + assert S.name == 'S' + assert S.parent == P + assert S.child == C + assert S.coordinates == Matrix([q0, q1, q2]) + assert S.speeds == Matrix([u0, u1, u2]) + assert S.kdes == Matrix([u0 - q0.diff(t), u1 - q1.diff(t), u2 - q2.diff(t)]) + assert S.child_point.pos_from(C.masscenter) == Vector(0) + assert S.parent_point.pos_from(P.masscenter) == Vector(0) + assert S.parent_point.pos_from(S.child_point) == Vector(0) + assert P.masscenter.pos_from(C.masscenter) == Vector(0) + assert C.masscenter.vel(N) == Vector(0) + assert N.ang_vel_in(A) == (-u0 * cos(q1) * cos(q2) - u1 * sin(q2)) * A.x + ( + u0 * sin(q2) * cos(q1) - u1 * cos(q2)) * A.y + ( + -u0 * sin(q1) - u2) * A.z + assert A.ang_vel_in(N) == (u0 * cos(q1) * cos(q2) + u1 * sin(q2)) * A.x + ( + -u0 * sin(q2) * cos(q1) + u1 * cos(q2)) * A.y + ( + u0 * sin(q1) + u2) * A.z + assert S.__str__() == 'SphericalJoint: S parent: P child: C' + assert S._rot_type == 'BODY' + assert S._rot_order == 123 + assert S._amounts is None + + +def test_spherical_joint_speeds_as_derivative_terms(): + # This tests checks whether the system remains valid if the user chooses to + # pass the derivative of the generalized coordinates as generalized speeds + q0, q1, q2 = dynamicsymbols('q0:3') + u0, u1, u2 = dynamicsymbols('q0:3', 1) + N, A, P, C = _generate_body() + S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2]) + assert S.coordinates == Matrix([q0, q1, q2]) + assert S.speeds == Matrix([u0, u1, u2]) + assert S.kdes == Matrix([0, 0, 0]) + assert N.ang_vel_in(A) == (-u0 * cos(q1) * cos(q2) - u1 * sin(q2)) * A.x + ( + u0 * sin(q2) * cos(q1) - u1 * cos(q2)) * A.y + ( + -u0 * sin(q1) - u2) * A.z + + +def test_spherical_joint_coords(): + q0s, q1s, q2s, u0s, u1s, u2s = dynamicsymbols('q0:3_S, u0:3_S') + q0, q1, q2, q3, u0, u1, u2, u4 = dynamicsymbols('q0:4, u0:4') + # Test coordinates as list + N, A, P, C = _generate_body() + S = SphericalJoint('S', P, C, [q0, q1, q2], [u0, u1, u2]) + assert S.coordinates == Matrix([q0, q1, q2]) + assert S.speeds == Matrix([u0, u1, u2]) + # Test coordinates as Matrix + N, A, P, C = _generate_body() + S = SphericalJoint('S', P, C, Matrix([q0, q1, q2]), + Matrix([u0, u1, u2])) + assert S.coordinates == Matrix([q0, q1, q2]) + assert S.speeds == Matrix([u0, u1, u2]) + # Test too few generalized coordinates + N, A, P, C = _generate_body() + raises(ValueError, + lambda: SphericalJoint('S', P, C, Matrix([q0, q1]), Matrix([u0]))) + # Test too many generalized coordinates + raises(ValueError, lambda: SphericalJoint( + 'S', P, C, Matrix([q0, q1, q2, q3]), Matrix([u0, u1, u2]))) + raises(ValueError, lambda: SphericalJoint( + 'S', P, C, Matrix([q0, q1, q2]), Matrix([u0, u1, u2, u4]))) + + +def test_spherical_joint_orient_body(): + q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3, u0:3') + N_R_A = Matrix([ + [-sin(q1), -sin(q2) * cos(q1), cos(q1) * cos(q2)], + [-sin(q0) * cos(q1), sin(q0) * sin(q1) * sin(q2) - cos(q0) * cos(q2), + -sin(q0) * sin(q1) * cos(q2) - sin(q2) * cos(q0)], + [cos(q0) * cos(q1), -sin(q0) * cos(q2) - sin(q1) * sin(q2) * cos(q0), + -sin(q0) * sin(q2) + sin(q1) * cos(q0) * cos(q2)]]) + N_w_A = Matrix([[-u0 * sin(q1) - u2], + [-u0 * sin(q2) * cos(q1) + u1 * cos(q2)], + [u0 * cos(q1) * cos(q2) + u1 * sin(q2)]]) + N_v_Co = Matrix([ + [-sqrt(2) * (u0 * cos(q2 + pi / 4) * cos(q1) + u1 * sin(q2 + pi / 4))], + [-u0 * sin(q1) - u2], [-u0 * sin(q1) - u2]]) + # Test default rot_type='BODY', rot_order=123 + N, A, P, C, Pint, Cint = _generate_body(True) + S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2], + parent_point=N.x + N.y, child_point=-A.y + A.z, + parent_interframe=Pint, child_interframe=Cint, + rot_type='body', rot_order=123) + assert S._rot_type.upper() == 'BODY' + assert S._rot_order == 123 + assert simplify(N.dcm(A) - N_R_A) == zeros(3) + assert simplify(A.ang_vel_in(N).to_matrix(A) - N_w_A) == zeros(3, 1) + assert simplify(C.masscenter.vel(N).to_matrix(A)) == N_v_Co + # Test change of amounts + N, A, P, C, Pint, Cint = _generate_body(True) + S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2], + parent_point=N.x + N.y, child_point=-A.y + A.z, + parent_interframe=Pint, child_interframe=Cint, + rot_type='BODY', amounts=(q1, q0, q2), rot_order=123) + switch_order = lambda expr: expr.xreplace( + {q0: q1, q1: q0, q2: q2, u0: u1, u1: u0, u2: u2}) + assert S._rot_type.upper() == 'BODY' + assert S._rot_order == 123 + assert simplify(N.dcm(A) - switch_order(N_R_A)) == zeros(3) + assert simplify(A.ang_vel_in(N).to_matrix(A) - switch_order(N_w_A) + ) == zeros(3, 1) + assert simplify(C.masscenter.vel(N).to_matrix(A)) == switch_order(N_v_Co) + # Test different rot_order + N, A, P, C, Pint, Cint = _generate_body(True) + S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2], + parent_point=N.x + N.y, child_point=-A.y + A.z, + parent_interframe=Pint, child_interframe=Cint, + rot_type='BodY', rot_order='yxz') + assert S._rot_type.upper() == 'BODY' + assert S._rot_order == 'yxz' + assert simplify(N.dcm(A) - Matrix([ + [-sin(q0) * cos(q1), sin(q0) * sin(q1) * cos(q2) - sin(q2) * cos(q0), + sin(q0) * sin(q1) * sin(q2) + cos(q0) * cos(q2)], + [-sin(q1), -cos(q1) * cos(q2), -sin(q2) * cos(q1)], + [cos(q0) * cos(q1), -sin(q0) * sin(q2) - sin(q1) * cos(q0) * cos(q2), + sin(q0) * cos(q2) - sin(q1) * sin(q2) * cos(q0)]])) == zeros(3) + assert simplify(A.ang_vel_in(N).to_matrix(A) - Matrix([ + [u0 * sin(q1) - u2], [u0 * cos(q1) * cos(q2) - u1 * sin(q2)], + [u0 * sin(q2) * cos(q1) + u1 * cos(q2)]])) == zeros(3, 1) + assert simplify(C.masscenter.vel(N).to_matrix(A)) == Matrix([ + [-sqrt(2) * (u0 * sin(q2 + pi / 4) * cos(q1) + u1 * cos(q2 + pi / 4))], + [u0 * sin(q1) - u2], [u0 * sin(q1) - u2]]) + + +def test_spherical_joint_orient_space(): + q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3, u0:3') + N_R_A = Matrix([ + [-sin(q0) * sin(q2) - sin(q1) * cos(q0) * cos(q2), + sin(q0) * sin(q1) * cos(q2) - sin(q2) * cos(q0), cos(q1) * cos(q2)], + [-sin(q0) * cos(q2) + sin(q1) * sin(q2) * cos(q0), + -sin(q0) * sin(q1) * sin(q2) - cos(q0) * cos(q2), -sin(q2) * cos(q1)], + [cos(q0) * cos(q1), -sin(q0) * cos(q1), sin(q1)]]) + N_w_A = Matrix([ + [u1 * sin(q0) - u2 * cos(q0) * cos(q1)], + [u1 * cos(q0) + u2 * sin(q0) * cos(q1)], [u0 - u2 * sin(q1)]]) + N_v_Co = Matrix([ + [u0 - u2 * sin(q1)], [u0 - u2 * sin(q1)], + [sqrt(2) * (-u1 * sin(q0 + pi / 4) + u2 * cos(q0 + pi / 4) * cos(q1))]]) + # Test default rot_type='BODY', rot_order=123 + N, A, P, C, Pint, Cint = _generate_body(True) + S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2], + parent_point=N.x + N.z, child_point=-A.x + A.y, + parent_interframe=Pint, child_interframe=Cint, + rot_type='space', rot_order=123) + assert S._rot_type.upper() == 'SPACE' + assert S._rot_order == 123 + assert simplify(N.dcm(A) - N_R_A) == zeros(3) + assert simplify(A.ang_vel_in(N).to_matrix(A)) == N_w_A + assert simplify(C.masscenter.vel(N).to_matrix(A)) == N_v_Co + # Test change of amounts + switch_order = lambda expr: expr.xreplace( + {q0: q1, q1: q0, q2: q2, u0: u1, u1: u0, u2: u2}) + N, A, P, C, Pint, Cint = _generate_body(True) + S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2], + parent_point=N.x + N.z, child_point=-A.x + A.y, + parent_interframe=Pint, child_interframe=Cint, + rot_type='SPACE', amounts=(q1, q0, q2), rot_order=123) + assert S._rot_type.upper() == 'SPACE' + assert S._rot_order == 123 + assert simplify(N.dcm(A) - switch_order(N_R_A)) == zeros(3) + assert simplify(A.ang_vel_in(N).to_matrix(A)) == switch_order(N_w_A) + assert simplify(C.masscenter.vel(N).to_matrix(A)) == switch_order(N_v_Co) + # Test different rot_order + N, A, P, C, Pint, Cint = _generate_body(True) + S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2], + parent_point=N.x + N.z, child_point=-A.x + A.y, + parent_interframe=Pint, child_interframe=Cint, + rot_type='SPaCe', rot_order='zxy') + assert S._rot_type.upper() == 'SPACE' + assert S._rot_order == 'zxy' + assert simplify(N.dcm(A) - Matrix([ + [-sin(q2) * cos(q1), -sin(q0) * cos(q2) + sin(q1) * sin(q2) * cos(q0), + sin(q0) * sin(q1) * sin(q2) + cos(q0) * cos(q2)], + [-sin(q1), -cos(q0) * cos(q1), -sin(q0) * cos(q1)], + [cos(q1) * cos(q2), -sin(q0) * sin(q2) - sin(q1) * cos(q0) * cos(q2), + -sin(q0) * sin(q1) * cos(q2) + sin(q2) * cos(q0)]])) + assert simplify(A.ang_vel_in(N).to_matrix(A) - Matrix([ + [-u0 + u2 * sin(q1)], [-u1 * sin(q0) + u2 * cos(q0) * cos(q1)], + [u1 * cos(q0) + u2 * sin(q0) * cos(q1)]])) == zeros(3, 1) + assert simplify(C.masscenter.vel(N).to_matrix(A) - Matrix([ + [u1 * cos(q0) + u2 * sin(q0) * cos(q1)], + [u1 * cos(q0) + u2 * sin(q0) * cos(q1)], + [u0 + u1 * sin(q0) - u2 * sin(q1) - + u2 * cos(q0) * cos(q1)]])) == zeros(3, 1) + + +def test_weld_joint(): + _, _, P, C = _generate_body() + W = WeldJoint('W', P, C) + assert W.name == 'W' + assert W.parent == P + assert W.child == C + assert W.coordinates == Matrix() + assert W.speeds == Matrix() + assert W.kdes == Matrix(1, 0, []).T + assert P.frame.dcm(C.frame) == eye(3) + assert W.child_point.pos_from(C.masscenter) == Vector(0) + assert W.parent_point.pos_from(P.masscenter) == Vector(0) + assert W.parent_point.pos_from(W.child_point) == Vector(0) + assert P.masscenter.pos_from(C.masscenter) == Vector(0) + assert C.masscenter.vel(P.frame) == Vector(0) + assert P.frame.ang_vel_in(C.frame) == 0 + assert C.frame.ang_vel_in(P.frame) == 0 + assert W.__str__() == 'WeldJoint: W parent: P child: C' + + N, A, P, C = _generate_body() + l, m = symbols('l m') + Pint = ReferenceFrame('P_int') + Pint.orient_axis(P.frame, P.y, pi / 2) + W = WeldJoint('W', P, C, parent_point=l * P.frame.x, + child_point=m * C.frame.y, parent_interframe=Pint) + + assert W.child_point.pos_from(C.masscenter) == m * C.frame.y + assert W.parent_point.pos_from(P.masscenter) == l * P.frame.x + assert W.parent_point.pos_from(W.child_point) == Vector(0) + assert P.masscenter.pos_from(C.masscenter) == - l * N.x + m * A.y + assert C.masscenter.vel(P.frame) == Vector(0) + assert P.masscenter.vel(Pint) == Vector(0) + assert C.frame.ang_vel_in(P.frame) == 0 + assert P.frame.ang_vel_in(C.frame) == 0 + assert P.x == A.z + + with warns_deprecated_sympy(): + JointsMethod(P, W) # Tests #10770 + + +def test_deprecated_parent_child_axis(): + q, u = dynamicsymbols('q_J, u_J') + N, A, P, C = _generate_body() + with warns_deprecated_sympy(): + PinJoint('J', P, C, child_axis=-A.x) + assert (-A.x).angle_between(N.x) == 0 + assert -A.x.express(N) == N.x + assert A.dcm(N) == Matrix([[-1, 0, 0], + [0, -cos(q), -sin(q)], + [0, -sin(q), cos(q)]]) + assert A.ang_vel_in(N) == u * N.x + assert A.ang_vel_in(N).magnitude() == sqrt(u ** 2) + + N, A, P, C = _generate_body() + with warns_deprecated_sympy(): + PrismaticJoint('J', P, C, parent_axis=P.x + P.y) + assert (A.x).angle_between(N.x + N.y) == 0 + assert A.x.express(N) == (N.x + N.y) / sqrt(2) + assert A.dcm(N) == Matrix([[sqrt(2) / 2, sqrt(2) / 2, 0], + [-sqrt(2) / 2, sqrt(2) / 2, 0], [0, 0, 1]]) + assert A.ang_vel_in(N) == Vector(0) + + +def test_deprecated_joint_pos(): + N, A, P, C = _generate_body() + with warns_deprecated_sympy(): + pin = PinJoint('J', P, C, parent_joint_pos=N.x + N.y, + child_joint_pos=C.y - C.z) + assert pin.parent_point.pos_from(P.masscenter) == N.x + N.y + assert pin.child_point.pos_from(C.masscenter) == C.y - C.z + + N, A, P, C = _generate_body() + with warns_deprecated_sympy(): + slider = PrismaticJoint('J', P, C, parent_joint_pos=N.z + N.y, + child_joint_pos=C.y - C.x) + assert slider.parent_point.pos_from(P.masscenter) == N.z + N.y + assert slider.child_point.pos_from(C.masscenter) == C.y - C.x diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_jointsmethod.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_jointsmethod.py new file mode 100644 index 0000000000000000000000000000000000000000..1b48eae06dadc627442fd4e42445450be0393e33 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_jointsmethod.py @@ -0,0 +1,249 @@ +from sympy.core.function import expand +from sympy.core.symbol import symbols +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.matrices.dense import Matrix +from sympy.simplify.trigsimp import trigsimp +from sympy.physics.mechanics import ( + PinJoint, JointsMethod, RigidBody, Particle, Body, KanesMethod, + PrismaticJoint, LagrangesMethod, inertia) +from sympy.physics.vector import dynamicsymbols, ReferenceFrame +from sympy.testing.pytest import raises, warns_deprecated_sympy +from sympy import zeros +from sympy.utilities.lambdify import lambdify +from sympy.solvers.solvers import solve + + +t = dynamicsymbols._t # type: ignore + + +def test_jointsmethod(): + with warns_deprecated_sympy(): + P = Body('P') + C = Body('C') + Pin = PinJoint('P1', P, C) + C_ixx, g = symbols('C_ixx g') + q, u = dynamicsymbols('q_P1, u_P1') + P.apply_force(g*P.y) + with warns_deprecated_sympy(): + method = JointsMethod(P, Pin) + assert method.frame == P.frame + assert method.bodies == [C, P] + assert method.loads == [(P.masscenter, g*P.frame.y)] + assert method.q == Matrix([q]) + assert method.u == Matrix([u]) + assert method.kdes == Matrix([u - q.diff()]) + soln = method.form_eoms() + assert soln == Matrix([[-C_ixx*u.diff()]]) + assert method.forcing_full == Matrix([[u], [0]]) + assert method.mass_matrix_full == Matrix([[1, 0], [0, C_ixx]]) + assert isinstance(method.method, KanesMethod) + + +def test_rigid_body_particle_compatibility(): + l, m, g = symbols('l m g') + C = RigidBody('C') + b = Particle('b', mass=m) + b_frame = ReferenceFrame('b_frame') + q, u = dynamicsymbols('q u') + P = PinJoint('P', C, b, coordinates=q, speeds=u, child_interframe=b_frame, + child_point=-l * b_frame.x, joint_axis=C.z) + with warns_deprecated_sympy(): + method = JointsMethod(C, P) + method.loads.append((b.masscenter, m * g * C.x)) + method.form_eoms() + rhs = method.rhs() + assert rhs[1] == -g*sin(q)/l + + +def test_jointmethod_duplicate_coordinates_speeds(): + with warns_deprecated_sympy(): + P = Body('P') + C = Body('C') + T = Body('T') + q, u = dynamicsymbols('q u') + P1 = PinJoint('P1', P, C, q) + P2 = PrismaticJoint('P2', C, T, q) + with warns_deprecated_sympy(): + raises(ValueError, lambda: JointsMethod(P, P1, P2)) + + P1 = PinJoint('P1', P, C, speeds=u) + P2 = PrismaticJoint('P2', C, T, speeds=u) + with warns_deprecated_sympy(): + raises(ValueError, lambda: JointsMethod(P, P1, P2)) + + P1 = PinJoint('P1', P, C, q, u) + P2 = PrismaticJoint('P2', C, T, q, u) + with warns_deprecated_sympy(): + raises(ValueError, lambda: JointsMethod(P, P1, P2)) + +def test_complete_simple_double_pendulum(): + q1, q2 = dynamicsymbols('q1 q2') + u1, u2 = dynamicsymbols('u1 u2') + m, l, g = symbols('m l g') + with warns_deprecated_sympy(): + C = Body('C') # ceiling + PartP = Body('P', mass=m) + PartR = Body('R', mass=m) + J1 = PinJoint('J1', C, PartP, speeds=u1, coordinates=q1, + child_point=-l*PartP.x, joint_axis=C.z) + J2 = PinJoint('J2', PartP, PartR, speeds=u2, coordinates=q2, + child_point=-l*PartR.x, joint_axis=PartP.z) + + PartP.apply_force(m*g*C.x) + PartR.apply_force(m*g*C.x) + + with warns_deprecated_sympy(): + method = JointsMethod(C, J1, J2) + method.form_eoms() + + assert expand(method.mass_matrix_full) == Matrix([[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 2*l**2*m*cos(q2) + 3*l**2*m, l**2*m*cos(q2) + l**2*m], + [0, 0, l**2*m*cos(q2) + l**2*m, l**2*m]]) + assert trigsimp(method.forcing_full) == trigsimp(Matrix([[u1], [u2], [-g*l*m*(sin(q1 + q2) + sin(q1)) - + g*l*m*sin(q1) + l**2*m*(2*u1 + u2)*u2*sin(q2)], + [-g*l*m*sin(q1 + q2) - l**2*m*u1**2*sin(q2)]])) + +def test_two_dof_joints(): + q1, q2, u1, u2 = dynamicsymbols('q1 q2 u1 u2') + m, c1, c2, k1, k2 = symbols('m c1 c2 k1 k2') + with warns_deprecated_sympy(): + W = Body('W') + B1 = Body('B1', mass=m) + B2 = Body('B2', mass=m) + J1 = PrismaticJoint('J1', W, B1, coordinates=q1, speeds=u1) + J2 = PrismaticJoint('J2', B1, B2, coordinates=q2, speeds=u2) + W.apply_force(k1*q1*W.x, reaction_body=B1) + W.apply_force(c1*u1*W.x, reaction_body=B1) + B1.apply_force(k2*q2*W.x, reaction_body=B2) + B1.apply_force(c2*u2*W.x, reaction_body=B2) + with warns_deprecated_sympy(): + method = JointsMethod(W, J1, J2) + method.form_eoms() + MM = method.mass_matrix + forcing = method.forcing + rhs = MM.LUsolve(forcing) + assert expand(rhs[0]) == expand((-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2)/m) + assert expand(rhs[1]) == expand((k1 * q1 + c1 * u1 - 2 * k2 * q2 - 2 * + c2 * u2) / m) + +def test_simple_pedulum(): + l, m, g = symbols('l m g') + with warns_deprecated_sympy(): + C = Body('C') + b = Body('b', mass=m) + q = dynamicsymbols('q') + P = PinJoint('P', C, b, speeds=q.diff(t), coordinates=q, + child_point=-l * b.x, joint_axis=C.z) + b.potential_energy = - m * g * l * cos(q) + with warns_deprecated_sympy(): + method = JointsMethod(C, P) + method.form_eoms(LagrangesMethod) + rhs = method.rhs() + assert rhs[1] == -g*sin(q)/l + +def test_chaos_pendulum(): + #https://www.pydy.org/examples/chaos_pendulum.html + mA, mB, lA, lB, IAxx, IBxx, IByy, IBzz, g = symbols('mA, mB, lA, lB, IAxx, IBxx, IByy, IBzz, g') + theta, phi, omega, alpha = dynamicsymbols('theta phi omega alpha') + + A = ReferenceFrame('A') + B = ReferenceFrame('B') + + with warns_deprecated_sympy(): + rod = Body('rod', mass=mA, frame=A, + central_inertia=inertia(A, IAxx, IAxx, 0)) + plate = Body('plate', mass=mB, frame=B, + central_inertia=inertia(B, IBxx, IByy, IBzz)) + C = Body('C') + J1 = PinJoint('J1', C, rod, coordinates=theta, speeds=omega, + child_point=-lA * rod.z, joint_axis=C.y) + J2 = PinJoint('J2', rod, plate, coordinates=phi, speeds=alpha, + parent_point=(lB - lA) * rod.z, joint_axis=rod.z) + + rod.apply_force(mA*g*C.z) + plate.apply_force(mB*g*C.z) + + with warns_deprecated_sympy(): + method = JointsMethod(C, J1, J2) + method.form_eoms() + + MM = method.mass_matrix + forcing = method.forcing + rhs = MM.LUsolve(forcing) + xd = (-2 * IBxx * alpha * omega * sin(phi) * cos(phi) + 2 * IByy * alpha * omega * sin(phi) * + cos(phi) - g * lA * mA * sin(theta) - g * lB * mB * sin(theta)) / (IAxx + IBxx * + sin(phi)**2 + IByy * cos(phi)**2 + lA**2 * mA + lB**2 * mB) + assert (rhs[0] - xd).simplify() == 0 + xd = (IBxx - IByy) * omega**2 * sin(phi) * cos(phi) / IBzz + assert (rhs[1] - xd).simplify() == 0 + +def test_four_bar_linkage_with_manual_constraints(): + q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1:4, u1:4') + l1, l2, l3, l4, rho = symbols('l1:5, rho') + + N = ReferenceFrame('N') + inertias = [inertia(N, 0, 0, rho * l ** 3 / 12) for l in (l1, l2, l3, l4)] + with warns_deprecated_sympy(): + link1 = Body('Link1', frame=N, mass=rho * l1, + central_inertia=inertias[0]) + link2 = Body('Link2', mass=rho * l2, central_inertia=inertias[1]) + link3 = Body('Link3', mass=rho * l3, central_inertia=inertias[2]) + link4 = Body('Link4', mass=rho * l4, central_inertia=inertias[3]) + + joint1 = PinJoint( + 'J1', link1, link2, coordinates=q1, speeds=u1, joint_axis=link1.z, + parent_point=l1 / 2 * link1.x, child_point=-l2 / 2 * link2.x) + joint2 = PinJoint( + 'J2', link2, link3, coordinates=q2, speeds=u2, joint_axis=link2.z, + parent_point=l2 / 2 * link2.x, child_point=-l3 / 2 * link3.x) + joint3 = PinJoint( + 'J3', link3, link4, coordinates=q3, speeds=u3, joint_axis=link3.z, + parent_point=l3 / 2 * link3.x, child_point=-l4 / 2 * link4.x) + + loop = link4.masscenter.pos_from(link1.masscenter) \ + + l1 / 2 * link1.x + l4 / 2 * link4.x + + fh = Matrix([loop.dot(link1.x), loop.dot(link1.y)]) + + with warns_deprecated_sympy(): + method = JointsMethod(link1, joint1, joint2, joint3) + + t = dynamicsymbols._t + qdots = solve(method.kdes, [q1.diff(t), q2.diff(t), q3.diff(t)]) + fhd = fh.diff(t).subs(qdots) + + kane = KanesMethod(method.frame, q_ind=[q1], u_ind=[u1], + q_dependent=[q2, q3], u_dependent=[u2, u3], + kd_eqs=method.kdes, configuration_constraints=fh, + velocity_constraints=fhd, forcelist=method.loads, + bodies=method.bodies) + fr, frs = kane.kanes_equations() + assert fr == zeros(1) + + # Numerically check the mass- and forcing-matrix + p = Matrix([l1, l2, l3, l4, rho]) + q = Matrix([q1, q2, q3]) + u = Matrix([u1, u2, u3]) + eval_m = lambdify((q, p), kane.mass_matrix) + eval_f = lambdify((q, u, p), kane.forcing) + eval_fhd = lambdify((q, u, p), fhd) + + p_vals = [0.13, 0.24, 0.21, 0.34, 997] + q_vals = [2.1, 0.6655470375077588, 2.527408138024188] # Satisfies fh + u_vals = [0.2, -0.17963733938852067, 0.1309060540601612] # Satisfies fhd + mass_check = Matrix([[3.452709815256506e+01, 7.003948798374735e+00, + -4.939690970641498e+00], + [-2.203792703880936e-14, 2.071702479957077e-01, + 2.842917573033711e-01], + [-1.300000000000123e-01, -8.836934896046506e-03, + 1.864891330060847e-01]]) + forcing_check = Matrix([[-0.031211821321648], + [-0.00066022608181], + [0.001813559741243]]) + eps = 1e-10 + assert all(abs(x) < eps for x in eval_fhd(q_vals, u_vals, p_vals)) + assert all(abs(x) < eps for x in + (Matrix(eval_m(q_vals, p_vals)) - mass_check)) + assert all(abs(x) < eps for x in + (Matrix(eval_f(q_vals, u_vals, p_vals)) - forcing_check)) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane.py new file mode 100644 index 0000000000000000000000000000000000000000..5f9310aae6d720c32615a86df5e488f46a513c76 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane.py @@ -0,0 +1,553 @@ +from sympy import solve +from sympy import (cos, expand, Matrix, sin, symbols, tan, sqrt, S, + zeros, eye) +from sympy.simplify.simplify import simplify +from sympy.physics.mechanics import (dynamicsymbols, ReferenceFrame, Point, + RigidBody, KanesMethod, inertia, Particle, + dot, find_dynamicsymbols) +from sympy.testing.pytest import raises + + +def test_invalid_coordinates(): + # Simple pendulum, but use symbols instead of dynamicsymbols + l, m, g = symbols('l m g') + q, u = symbols('q u') # Generalized coordinate + kd = [q.diff(dynamicsymbols._t) - u] + N, O = ReferenceFrame('N'), Point('O') + O.set_vel(N, 0) + P = Particle('P', Point('P'), m) + P.point.set_pos(O, l * (sin(q) * N.x - cos(q) * N.y)) + F = (P.point, -m * g * N.y) + raises(ValueError, lambda: KanesMethod(N, [q], [u], kd, bodies=[P], + forcelist=[F])) + + +def test_one_dof(): + # This is for a 1 dof spring-mass-damper case. + # It is described in more detail in the KanesMethod docstring. + q, u = dynamicsymbols('q u') + qd, ud = dynamicsymbols('q u', 1) + m, c, k = symbols('m c k') + N = ReferenceFrame('N') + P = Point('P') + P.set_vel(N, u * N.x) + + kd = [qd - u] + FL = [(P, (-k * q - c * u) * N.x)] + pa = Particle('pa', P, m) + BL = [pa] + + KM = KanesMethod(N, [q], [u], kd) + KM.kanes_equations(BL, FL) + + assert KM.bodies == BL + assert KM.loads == FL + + MM = KM.mass_matrix + forcing = KM.forcing + rhs = MM.inv() * forcing + assert expand(rhs[0]) == expand(-(q * k + u * c) / m) + + assert simplify(KM.rhs() - + KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(2, 1) + + assert (KM.linearize(A_and_B=True, )[0] == Matrix([[0, 1], [-k/m, -c/m]])) + + +def test_two_dof(): + # This is for a 2 d.o.f., 2 particle spring-mass-damper. + # The first coordinate is the displacement of the first particle, and the + # second is the relative displacement between the first and second + # particles. Speeds are defined as the time derivatives of the particles. + q1, q2, u1, u2 = dynamicsymbols('q1 q2 u1 u2') + q1d, q2d, u1d, u2d = dynamicsymbols('q1 q2 u1 u2', 1) + m, c1, c2, k1, k2 = symbols('m c1 c2 k1 k2') + N = ReferenceFrame('N') + P1 = Point('P1') + P2 = Point('P2') + P1.set_vel(N, u1 * N.x) + P2.set_vel(N, (u1 + u2) * N.x) + # Note we multiply the kinematic equation by an arbitrary factor + # to test the implicit vs explicit kinematics attribute + kd = [q1d/2 - u1/2, 2*q2d - 2*u2] + + # Now we create the list of forces, then assign properties to each + # particle, then create a list of all particles. + FL = [(P1, (-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2) * N.x), (P2, (-k2 * + q2 - c2 * u2) * N.x)] + pa1 = Particle('pa1', P1, m) + pa2 = Particle('pa2', P2, m) + BL = [pa1, pa2] + + # Finally we create the KanesMethod object, specify the inertial frame, + # pass relevant information, and form Fr & Fr*. Then we calculate the mass + # matrix and forcing terms, and finally solve for the udots. + KM = KanesMethod(N, q_ind=[q1, q2], u_ind=[u1, u2], kd_eqs=kd) + KM.kanes_equations(BL, FL) + MM = KM.mass_matrix + forcing = KM.forcing + rhs = MM.inv() * forcing + assert expand(rhs[0]) == expand((-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2)/m) + assert expand(rhs[1]) == expand((k1 * q1 + c1 * u1 - 2 * k2 * q2 - 2 * + c2 * u2) / m) + + # Check that the explicit form is the default and kinematic mass matrix is identity + assert KM.explicit_kinematics + assert KM.mass_matrix_kin == eye(2) + + # Check that for the implicit form the mass matrix is not identity + KM.explicit_kinematics = False + assert KM.mass_matrix_kin == Matrix([[S(1)/2, 0], [0, 2]]) + + # Check that whether using implicit or explicit kinematics the RHS + # equations are consistent with the matrix form + for explicit_kinematics in [False, True]: + KM.explicit_kinematics = explicit_kinematics + assert simplify(KM.rhs() - + KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(4, 1) + + # Make sure an error is raised if nonlinear kinematic differential + # equations are supplied. + kd = [q1d - u1**2, sin(q2d) - cos(u2)] + raises(ValueError, lambda: KanesMethod(N, q_ind=[q1, q2], + u_ind=[u1, u2], kd_eqs=kd)) + +def test_pend(): + q, u = dynamicsymbols('q u') + qd, ud = dynamicsymbols('q u', 1) + m, l, g = symbols('m l g') + N = ReferenceFrame('N') + P = Point('P') + P.set_vel(N, -l * u * sin(q) * N.x + l * u * cos(q) * N.y) + kd = [qd - u] + + FL = [(P, m * g * N.x)] + pa = Particle('pa', P, m) + BL = [pa] + + KM = KanesMethod(N, [q], [u], kd) + KM.kanes_equations(BL, FL) + MM = KM.mass_matrix + forcing = KM.forcing + rhs = MM.inv() * forcing + rhs.simplify() + assert expand(rhs[0]) == expand(-g / l * sin(q)) + assert simplify(KM.rhs() - + KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(2, 1) + + +def test_rolling_disc(): + # Rolling Disc Example + # Here the rolling disc is formed from the contact point up, removing the + # need to introduce generalized speeds. Only 3 configuration and three + # speed variables are need to describe this system, along with the disc's + # mass and radius, and the local gravity (note that mass will drop out). + q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1 q2 q3 u1 u2 u3') + q1d, q2d, q3d, u1d, u2d, u3d = dynamicsymbols('q1 q2 q3 u1 u2 u3', 1) + r, m, g = symbols('r m g') + + # The kinematics are formed by a series of simple rotations. Each simple + # rotation creates a new frame, and the next rotation is defined by the new + # frame's basis vectors. This example uses a 3-1-2 series of rotations, or + # Z, X, Y series of rotations. Angular velocity for this is defined using + # the second frame's basis (the lean frame). + N = ReferenceFrame('N') + Y = N.orientnew('Y', 'Axis', [q1, N.z]) + L = Y.orientnew('L', 'Axis', [q2, Y.x]) + R = L.orientnew('R', 'Axis', [q3, L.y]) + w_R_N_qd = R.ang_vel_in(N) + R.set_ang_vel(N, u1 * L.x + u2 * L.y + u3 * L.z) + + # This is the translational kinematics. We create a point with no velocity + # in N; this is the contact point between the disc and ground. Next we form + # the position vector from the contact point to the disc's center of mass. + # Finally we form the velocity and acceleration of the disc. + C = Point('C') + C.set_vel(N, 0) + Dmc = C.locatenew('Dmc', r * L.z) + Dmc.v2pt_theory(C, N, R) + + # This is a simple way to form the inertia dyadic. + I = inertia(L, m / 4 * r**2, m / 2 * r**2, m / 4 * r**2) + + # Kinematic differential equations; how the generalized coordinate time + # derivatives relate to generalized speeds. + kd = [dot(R.ang_vel_in(N) - w_R_N_qd, uv) for uv in L] + + # Creation of the force list; it is the gravitational force at the mass + # center of the disc. Then we create the disc by assigning a Point to the + # center of mass attribute, a ReferenceFrame to the frame attribute, and mass + # and inertia. Then we form the body list. + ForceList = [(Dmc, - m * g * Y.z)] + BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc)) + BodyList = [BodyD] + + # Finally we form the equations of motion, using the same steps we did + # before. Specify inertial frame, supply generalized speeds, supply + # kinematic differential equation dictionary, compute Fr from the force + # list and Fr* from the body list, compute the mass matrix and forcing + # terms, then solve for the u dots (time derivatives of the generalized + # speeds). + KM = KanesMethod(N, q_ind=[q1, q2, q3], u_ind=[u1, u2, u3], kd_eqs=kd) + KM.kanes_equations(BodyList, ForceList) + MM = KM.mass_matrix + forcing = KM.forcing + rhs = MM.inv() * forcing + kdd = KM.kindiffdict() + rhs = rhs.subs(kdd) + rhs.simplify() + assert rhs.expand() == Matrix([(6*u2*u3*r - u3**2*r*tan(q2) + + 4*g*sin(q2))/(5*r), -2*u1*u3/3, u1*(-2*u2 + u3*tan(q2))]).expand() + assert simplify(KM.rhs() - + KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(6, 1) + + # This code tests our output vs. benchmark values. When r=g=m=1, the + # critical speed (where all eigenvalues of the linearized equations are 0) + # is 1 / sqrt(3) for the upright case. + A = KM.linearize(A_and_B=True)[0] + A_upright = A.subs({r: 1, g: 1, m: 1}).subs({q1: 0, q2: 0, q3: 0, u1: 0, u3: 0}) + import sympy + assert sympy.sympify(A_upright.subs({u2: 1 / sqrt(3)})).eigenvals() == {S.Zero: 6} + + +def test_aux(): + # Same as above, except we have 2 auxiliary speeds for the ground contact + # point, which is known to be zero. In one case, we go through then + # substitute the aux. speeds in at the end (they are zero, as well as their + # derivative), in the other case, we use the built-in auxiliary speed part + # of KanesMethod. The equations from each should be the same. + q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1 q2 q3 u1 u2 u3') + q1d, q2d, q3d, u1d, u2d, u3d = dynamicsymbols('q1 q2 q3 u1 u2 u3', 1) + u4, u5, f1, f2 = dynamicsymbols('u4, u5, f1, f2') + u4d, u5d = dynamicsymbols('u4, u5', 1) + r, m, g = symbols('r m g') + + N = ReferenceFrame('N') + Y = N.orientnew('Y', 'Axis', [q1, N.z]) + L = Y.orientnew('L', 'Axis', [q2, Y.x]) + R = L.orientnew('R', 'Axis', [q3, L.y]) + w_R_N_qd = R.ang_vel_in(N) + R.set_ang_vel(N, u1 * L.x + u2 * L.y + u3 * L.z) + + C = Point('C') + C.set_vel(N, u4 * L.x + u5 * (Y.z ^ L.x)) + Dmc = C.locatenew('Dmc', r * L.z) + Dmc.v2pt_theory(C, N, R) + Dmc.a2pt_theory(C, N, R) + + I = inertia(L, m / 4 * r**2, m / 2 * r**2, m / 4 * r**2) + + kd = [dot(R.ang_vel_in(N) - w_R_N_qd, uv) for uv in L] + + ForceList = [(Dmc, - m * g * Y.z), (C, f1 * L.x + f2 * (Y.z ^ L.x))] + BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc)) + BodyList = [BodyD] + + KM = KanesMethod(N, q_ind=[q1, q2, q3], u_ind=[u1, u2, u3, u4, u5], + kd_eqs=kd) + (fr, frstar) = KM.kanes_equations(BodyList, ForceList) + fr = fr.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0}) + frstar = frstar.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0}) + + KM2 = KanesMethod(N, q_ind=[q1, q2, q3], u_ind=[u1, u2, u3], kd_eqs=kd, + u_auxiliary=[u4, u5]) + (fr2, frstar2) = KM2.kanes_equations(BodyList, ForceList) + fr2 = fr2.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0}) + frstar2 = frstar2.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0}) + + frstar.simplify() + frstar2.simplify() + + assert (fr - fr2).expand() == Matrix([0, 0, 0, 0, 0]) + assert (frstar - frstar2).expand() == Matrix([0, 0, 0, 0, 0]) + + +def test_parallel_axis(): + # This is for a 2 dof inverted pendulum on a cart. + # This tests the parallel axis code in KanesMethod. The inertia of the + # pendulum is defined about the hinge, not about the center of mass. + + # Defining the constants and knowns of the system + gravity = symbols('g') + k, ls = symbols('k ls') + a, mA, mC = symbols('a mA mC') + F = dynamicsymbols('F') + Ix, Iy, Iz = symbols('Ix Iy Iz') + + # Declaring the Generalized coordinates and speeds + q1, q2 = dynamicsymbols('q1 q2') + q1d, q2d = dynamicsymbols('q1 q2', 1) + u1, u2 = dynamicsymbols('u1 u2') + u1d, u2d = dynamicsymbols('u1 u2', 1) + + # Creating reference frames + N = ReferenceFrame('N') + A = ReferenceFrame('A') + + A.orient(N, 'Axis', [-q2, N.z]) + A.set_ang_vel(N, -u2 * N.z) + + # Origin of Newtonian reference frame + O = Point('O') + + # Creating and Locating the positions of the cart, C, and the + # center of mass of the pendulum, A + C = O.locatenew('C', q1 * N.x) + Ao = C.locatenew('Ao', a * A.y) + + # Defining velocities of the points + O.set_vel(N, 0) + C.set_vel(N, u1 * N.x) + Ao.v2pt_theory(C, N, A) + Cart = Particle('Cart', C, mC) + Pendulum = RigidBody('Pendulum', Ao, A, mA, (inertia(A, Ix, Iy, Iz), C)) + + # kinematical differential equations + + kindiffs = [q1d - u1, q2d - u2] + + bodyList = [Cart, Pendulum] + + forceList = [(Ao, -N.y * gravity * mA), + (C, -N.y * gravity * mC), + (C, -N.x * k * (q1 - ls)), + (C, N.x * F)] + + km = KanesMethod(N, [q1, q2], [u1, u2], kindiffs) + (fr, frstar) = km.kanes_equations(bodyList, forceList) + mm = km.mass_matrix_full + assert mm[3, 3] == Iz + +def test_input_format(): + # 1 dof problem from test_one_dof + q, u = dynamicsymbols('q u') + qd, ud = dynamicsymbols('q u', 1) + m, c, k = symbols('m c k') + N = ReferenceFrame('N') + P = Point('P') + P.set_vel(N, u * N.x) + + kd = [qd - u] + FL = [(P, (-k * q - c * u) * N.x)] + pa = Particle('pa', P, m) + BL = [pa] + + KM = KanesMethod(N, [q], [u], kd) + # test for input format kane.kanes_equations((body1, body2, particle1)) + assert KM.kanes_equations(BL)[0] == Matrix([0]) + # test for input format kane.kanes_equations(bodies=(body1, body 2), loads=(load1,load2)) + assert KM.kanes_equations(bodies=BL, loads=None)[0] == Matrix([0]) + # test for input format kane.kanes_equations(bodies=(body1, body 2), loads=None) + assert KM.kanes_equations(BL, loads=None)[0] == Matrix([0]) + # test for input format kane.kanes_equations(bodies=(body1, body 2)) + assert KM.kanes_equations(BL)[0] == Matrix([0]) + # test for input format kane.kanes_equations(bodies=(body1, body2), loads=[]) + assert KM.kanes_equations(BL, [])[0] == Matrix([0]) + # test for error raised when a wrong force list (in this case a string) is provided + raises(ValueError, lambda: KM._form_fr('bad input')) + + # 1 dof problem from test_one_dof with FL & BL in instance + KM = KanesMethod(N, [q], [u], kd, bodies=BL, forcelist=FL) + assert KM.kanes_equations()[0] == Matrix([-c*u - k*q]) + + # 2 dof problem from test_two_dof + q1, q2, u1, u2 = dynamicsymbols('q1 q2 u1 u2') + q1d, q2d, u1d, u2d = dynamicsymbols('q1 q2 u1 u2', 1) + m, c1, c2, k1, k2 = symbols('m c1 c2 k1 k2') + N = ReferenceFrame('N') + P1 = Point('P1') + P2 = Point('P2') + P1.set_vel(N, u1 * N.x) + P2.set_vel(N, (u1 + u2) * N.x) + kd = [q1d - u1, q2d - u2] + + FL = ((P1, (-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2) * N.x), (P2, (-k2 * + q2 - c2 * u2) * N.x)) + pa1 = Particle('pa1', P1, m) + pa2 = Particle('pa2', P2, m) + BL = (pa1, pa2) + + KM = KanesMethod(N, q_ind=[q1, q2], u_ind=[u1, u2], kd_eqs=kd) + # test for input format + # kane.kanes_equations((body1, body2), (load1, load2)) + KM.kanes_equations(BL, FL) + MM = KM.mass_matrix + forcing = KM.forcing + rhs = MM.inv() * forcing + assert expand(rhs[0]) == expand((-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2)/m) + assert expand(rhs[1]) == expand((k1 * q1 + c1 * u1 - 2 * k2 * q2 - 2 * + c2 * u2) / m) + + +def test_implicit_kinematics(): + # Test that implicit kinematics can handle complicated + # equations that explicit form struggles with + # See https://github.com/sympy/sympy/issues/22626 + + # Inertial frame + NED = ReferenceFrame('NED') + NED_o = Point('NED_o') + NED_o.set_vel(NED, 0) + + # body frame + q_att = dynamicsymbols('lambda_0:4', real=True) + B = NED.orientnew('B', 'Quaternion', q_att) + + # Generalized coordinates + q_pos = dynamicsymbols('B_x:z') + B_cm = NED_o.locatenew('B_cm', q_pos[0]*B.x + q_pos[1]*B.y + q_pos[2]*B.z) + + q_ind = q_att[1:] + q_pos + q_dep = [q_att[0]] + + kinematic_eqs = [] + + # Generalized velocities + B_ang_vel = B.ang_vel_in(NED) + P, Q, R = dynamicsymbols('P Q R') + B.set_ang_vel(NED, P*B.x + Q*B.y + R*B.z) + + B_ang_vel_kd = (B.ang_vel_in(NED) - B_ang_vel).simplify() + + # Equating the two gives us the kinematic equation + kinematic_eqs += [ + B_ang_vel_kd & B.x, + B_ang_vel_kd & B.y, + B_ang_vel_kd & B.z + ] + + B_cm_vel = B_cm.vel(NED) + U, V, W = dynamicsymbols('U V W') + B_cm.set_vel(NED, U*B.x + V*B.y + W*B.z) + + # Compute the velocity of the point using the two methods + B_ref_vel_kd = (B_cm.vel(NED) - B_cm_vel) + + # taking dot product with unit vectors to get kinematic equations + # relating body coordinates and velocities + + # Note, there is a choice to dot with NED.xyz here. That makes + # the implicit form have some bigger terms but is still fine, the + # explicit form still struggles though + kinematic_eqs += [ + B_ref_vel_kd & B.x, + B_ref_vel_kd & B.y, + B_ref_vel_kd & B.z, + ] + + u_ind = [U, V, W, P, Q, R] + + # constraints + q_att_vec = Matrix(q_att) + config_cons = [(q_att_vec.T*q_att_vec)[0] - 1] #unit norm + kinematic_eqs = kinematic_eqs + [(q_att_vec.T * q_att_vec.diff())[0]] + + try: + KM = KanesMethod(NED, q_ind, u_ind, + q_dependent= q_dep, + kd_eqs = kinematic_eqs, + configuration_constraints = config_cons, + velocity_constraints= [], + u_dependent= [], #no dependent speeds + u_auxiliary = [], # No auxiliary speeds + explicit_kinematics = False # implicit kinematics + ) + except Exception as e: + raise e + + # mass and inertia dyadic relative to CM + M_B = symbols('M_B') + J_B = inertia(B, *[S(f'J_B_{ax}')*(1 if ax[0] == ax[1] else -1) + for ax in ['xx', 'yy', 'zz', 'xy', 'yz', 'xz']]) + J_B = J_B.subs({S('J_B_xy'): 0, S('J_B_yz'): 0}) + RB = RigidBody('RB', B_cm, B, M_B, (J_B, B_cm)) + + rigid_bodies = [RB] + # Forces + force_list = [ + #gravity pointing down + (RB.masscenter, RB.mass*S('g')*NED.z), + #generic forces and torques in body frame(inputs) + (RB.frame, dynamicsymbols('T_z')*B.z), + (RB.masscenter, dynamicsymbols('F_z')*B.z) + ] + + KM.kanes_equations(rigid_bodies, force_list) + + # Expecting implicit form to be less than 5% of the flops + n_ops_implicit = sum( + [x.count_ops() for x in KM.forcing_full] + + [x.count_ops() for x in KM.mass_matrix_full] + ) + # Save implicit kinematic matrices to use later + mass_matrix_kin_implicit = KM.mass_matrix_kin + forcing_kin_implicit = KM.forcing_kin + + KM.explicit_kinematics = True + n_ops_explicit = sum( + [x.count_ops() for x in KM.forcing_full] + + [x.count_ops() for x in KM.mass_matrix_full] + ) + forcing_kin_explicit = KM.forcing_kin + + assert n_ops_implicit / n_ops_explicit < .05 + + # Ideally we would check that implicit and explicit equations give the same result as done in test_one_dof + # But the whole raison-d'etre of the implicit equations is to deal with problems such + # as this one where the explicit form is too complicated to handle, especially the angular part + # (i.e. tests would be too slow) + # Instead, we check that the kinematic equations are correct using more fundamental tests: + # + # (1) that we recover the kinematic equations we have provided + assert (mass_matrix_kin_implicit * KM.q.diff() - forcing_kin_implicit) == Matrix(kinematic_eqs) + + # (2) that rate of quaternions matches what 'textbook' solutions give + # Note that we just use the explicit kinematics for the linear velocities + # as they are not as complicated as the angular ones + qdot_candidate = forcing_kin_explicit + + quat_dot_textbook = Matrix([ + [0, -P, -Q, -R], + [P, 0, R, -Q], + [Q, -R, 0, P], + [R, Q, -P, 0], + ]) * q_att_vec / 2 + + # Again, if we don't use this "textbook" solution + # sympy will struggle to deal with the terms related to quaternion rates + # due to the number of operations involved + qdot_candidate[-1] = quat_dot_textbook[0] # lambda_0, note the [-1] as sympy's Kane puts the dependent coordinate last + qdot_candidate[0] = quat_dot_textbook[1] # lambda_1 + qdot_candidate[1] = quat_dot_textbook[2] # lambda_2 + qdot_candidate[2] = quat_dot_textbook[3] # lambda_3 + + # sub the config constraint in the candidate solution and compare to the implicit rhs + lambda_0_sol = solve(config_cons[0], q_att_vec[0])[1] + lhs_candidate = simplify(mass_matrix_kin_implicit * qdot_candidate).subs({q_att_vec[0]: lambda_0_sol}) + assert lhs_candidate == forcing_kin_implicit + +def test_issue_24887(): + # Spherical pendulum + g, l, m, c = symbols('g l m c') + q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1:4 u1:4') + N = ReferenceFrame('N') + A = ReferenceFrame('A') + A.orient_body_fixed(N, (q1, q2, q3), 'zxy') + N_w_A = A.ang_vel_in(N) + # A.set_ang_vel(N, u1 * A.x + u2 * A.y + u3 * A.z) + kdes = [N_w_A.dot(A.x) - u1, N_w_A.dot(A.y) - u2, N_w_A.dot(A.z) - u3] + O = Point('O') + O.set_vel(N, 0) + Po = O.locatenew('Po', -l * A.y) + Po.set_vel(A, 0) + P = Particle('P', Po, m) + kane = KanesMethod(N, [q1, q2, q3], [u1, u2, u3], kdes, bodies=[P], + forcelist=[(Po, -m * g * N.y)]) + kane.kanes_equations() + expected_md = m * l ** 2 * Matrix([[1, 0, 0], [0, 0, 0], [0, 0, 1]]) + expected_fd = Matrix([ + [l*m*(g*(sin(q1)*sin(q3) - sin(q2)*cos(q1)*cos(q3)) - l*u2*u3)], + [0], [l*m*(-g*(sin(q1)*cos(q3) + sin(q2)*sin(q3)*cos(q1)) + l*u1*u2)]]) + assert find_dynamicsymbols(kane.forcing).issubset({q1, q2, q3, u1, u2, u3}) + assert simplify(kane.mass_matrix - expected_md) == zeros(3, 3) + assert simplify(kane.forcing - expected_fd) == zeros(3, 1) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane2.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane2.py new file mode 100644 index 0000000000000000000000000000000000000000..e55866672aec0adcd951e772964a4ed205b56405 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane2.py @@ -0,0 +1,464 @@ +from sympy import cos, Matrix, sin, zeros, tan, pi, symbols +from sympy.simplify.simplify import simplify +from sympy.simplify.trigsimp import trigsimp +from sympy.solvers.solvers import solve +from sympy.physics.mechanics import (cross, dot, dynamicsymbols, + find_dynamicsymbols, KanesMethod, inertia, + inertia_of_point_mass, Point, + ReferenceFrame, RigidBody) + + +def test_aux_dep(): + # This test is about rolling disc dynamics, comparing the results found + # with KanesMethod to those found when deriving the equations "manually" + # with SymPy. + # The terms Fr, Fr*, and Fr*_steady are all compared between the two + # methods. Here, Fr*_steady refers to the generalized inertia forces for an + # equilibrium configuration. + # Note: comparing to the test of test_rolling_disc() in test_kane.py, this + # test also tests auxiliary speeds and configuration and motion constraints + #, seen in the generalized dependent coordinates q[3], and depend speeds + # u[3], u[4] and u[5]. + + + # First, manual derivation of Fr, Fr_star, Fr_star_steady. + + # Symbols for time and constant parameters. + # Symbols for contact forces: Fx, Fy, Fz. + t, r, m, g, I, J = symbols('t r m g I J') + Fx, Fy, Fz = symbols('Fx Fy Fz') + + # Configuration variables and their time derivatives: + # q[0] -- yaw + # q[1] -- lean + # q[2] -- spin + # q[3] -- dot(-r*B.z, A.z) -- distance from ground plane to disc center in + # A.z direction + # Generalized speeds and their time derivatives: + # u[0] -- disc angular velocity component, disc fixed x direction + # u[1] -- disc angular velocity component, disc fixed y direction + # u[2] -- disc angular velocity component, disc fixed z direction + # u[3] -- disc velocity component, A.x direction + # u[4] -- disc velocity component, A.y direction + # u[5] -- disc velocity component, A.z direction + # Auxiliary generalized speeds: + # ua[0] -- contact point auxiliary generalized speed, A.x direction + # ua[1] -- contact point auxiliary generalized speed, A.y direction + # ua[2] -- contact point auxiliary generalized speed, A.z direction + q = dynamicsymbols('q:4') + qd = [qi.diff(t) for qi in q] + u = dynamicsymbols('u:6') + ud = [ui.diff(t) for ui in u] + ud_zero = dict(zip(ud, [0.]*len(ud))) + ua = dynamicsymbols('ua:3') + ua_zero = dict(zip(ua, [0.]*len(ua))) # noqa:F841 + + # Reference frames: + # Yaw intermediate frame: A. + # Lean intermediate frame: B. + # Disc fixed frame: C. + N = ReferenceFrame('N') + A = N.orientnew('A', 'Axis', [q[0], N.z]) + B = A.orientnew('B', 'Axis', [q[1], A.x]) + C = B.orientnew('C', 'Axis', [q[2], B.y]) + + # Angular velocity and angular acceleration of disc fixed frame + # u[0], u[1] and u[2] are generalized independent speeds. + C.set_ang_vel(N, u[0]*B.x + u[1]*B.y + u[2]*B.z) + C.set_ang_acc(N, C.ang_vel_in(N).diff(t, B) + + cross(B.ang_vel_in(N), C.ang_vel_in(N))) + + # Velocity and acceleration of points: + # Disc-ground contact point: P. + # Center of disc: O, defined from point P with depend coordinate: q[3] + # u[3], u[4] and u[5] are generalized dependent speeds. + P = Point('P') + P.set_vel(N, ua[0]*A.x + ua[1]*A.y + ua[2]*A.z) + O = P.locatenew('O', q[3]*A.z + r*sin(q[1])*A.y) + O.set_vel(N, u[3]*A.x + u[4]*A.y + u[5]*A.z) + O.set_acc(N, O.vel(N).diff(t, A) + cross(A.ang_vel_in(N), O.vel(N))) + + # Kinematic differential equations: + # Two equalities: one is w_c_n_qd = C.ang_vel_in(N) in three coordinates + # directions of B, for qd0, qd1 and qd2. + # the other is v_o_n_qd = O.vel(N) in A.z direction for qd3. + # Then, solve for dq/dt's in terms of u's: qd_kd. + w_c_n_qd = qd[0]*A.z + qd[1]*B.x + qd[2]*B.y + v_o_n_qd = O.pos_from(P).diff(t, A) + cross(A.ang_vel_in(N), O.pos_from(P)) + kindiffs = Matrix([dot(w_c_n_qd - C.ang_vel_in(N), uv) for uv in B] + + [dot(v_o_n_qd - O.vel(N), A.z)]) + qd_kd = solve(kindiffs, qd) # noqa:F841 + + # Values of generalized speeds during a steady turn for later substitution + # into the Fr_star_steady. + steady_conditions = solve(kindiffs.subs({qd[1] : 0, qd[3] : 0}), u) + steady_conditions.update({qd[1] : 0, qd[3] : 0}) + + # Partial angular velocities and velocities. + partial_w_C = [C.ang_vel_in(N).diff(ui, N) for ui in u + ua] + partial_v_O = [O.vel(N).diff(ui, N) for ui in u + ua] + partial_v_P = [P.vel(N).diff(ui, N) for ui in u + ua] + + # Configuration constraint: f_c, the projection of radius r in A.z direction + # is q[3]. + # Velocity constraints: f_v, for u3, u4 and u5. + # Acceleration constraints: f_a. + f_c = Matrix([dot(-r*B.z, A.z) - q[3]]) + f_v = Matrix([dot(O.vel(N) - (P.vel(N) + cross(C.ang_vel_in(N), + O.pos_from(P))), ai).expand() for ai in A]) + v_o_n = cross(C.ang_vel_in(N), O.pos_from(P)) + a_o_n = v_o_n.diff(t, A) + cross(A.ang_vel_in(N), v_o_n) + f_a = Matrix([dot(O.acc(N) - a_o_n, ai) for ai in A]) # noqa:F841 + + # Solve for constraint equations in the form of + # u_dependent = A_rs * [u_i; u_aux]. + # First, obtain constraint coefficient matrix: M_v * [u; ua] = 0; + # Second, taking u[0], u[1], u[2] as independent, + # taking u[3], u[4], u[5] as dependent, + # rearranging the matrix of M_v to be A_rs for u_dependent. + # Third, u_aux ==0 for u_dep, and resulting dictionary of u_dep_dict. + M_v = zeros(3, 9) + for i in range(3): + for j, ui in enumerate(u + ua): + M_v[i, j] = f_v[i].diff(ui) + + M_v_i = M_v[:, :3] + M_v_d = M_v[:, 3:6] + M_v_aux = M_v[:, 6:] + M_v_i_aux = M_v_i.row_join(M_v_aux) + A_rs = - M_v_d.inv() * M_v_i_aux + + u_dep = A_rs[:, :3] * Matrix(u[:3]) + u_dep_dict = dict(zip(u[3:], u_dep)) + + # Active forces: F_O acting on point O; F_P acting on point P. + # Generalized active forces (unconstrained): Fr_u = F_point * pv_point. + F_O = m*g*A.z + F_P = Fx * A.x + Fy * A.y + Fz * A.z + Fr_u = Matrix([dot(F_O, pv_o) + dot(F_P, pv_p) for pv_o, pv_p in + zip(partial_v_O, partial_v_P)]) + + # Inertia force: R_star_O. + # Inertia of disc: I_C_O, where J is a inertia component about principal axis. + # Inertia torque: T_star_C. + # Generalized inertia forces (unconstrained): Fr_star_u. + R_star_O = -m*O.acc(N) + I_C_O = inertia(B, I, J, I) + T_star_C = -(dot(I_C_O, C.ang_acc_in(N)) \ + + cross(C.ang_vel_in(N), dot(I_C_O, C.ang_vel_in(N)))) + Fr_star_u = Matrix([dot(R_star_O, pv) + dot(T_star_C, pav) for pv, pav in + zip(partial_v_O, partial_w_C)]) + + # Form nonholonomic Fr: Fr_c, and nonholonomic Fr_star: Fr_star_c. + # Also, nonholonomic Fr_star in steady turning condition: Fr_star_steady. + Fr_c = Fr_u[:3, :].col_join(Fr_u[6:, :]) + A_rs.T * Fr_u[3:6, :] + Fr_star_c = Fr_star_u[:3, :].col_join(Fr_star_u[6:, :])\ + + A_rs.T * Fr_star_u[3:6, :] + Fr_star_steady = Fr_star_c.subs(ud_zero).subs(u_dep_dict)\ + .subs(steady_conditions).subs({q[3]: -r*cos(q[1])}).expand() + + + # Second, using KaneMethod in mechanics for fr, frstar and frstar_steady. + + # Rigid Bodies: disc, with inertia I_C_O. + iner_tuple = (I_C_O, O) + disc = RigidBody('disc', O, C, m, iner_tuple) + bodyList = [disc] + + # Generalized forces: Gravity: F_o; Auxiliary forces: F_p. + F_o = (O, F_O) + F_p = (P, F_P) + forceList = [F_o, F_p] + + # KanesMethod. + kane = KanesMethod( + N, q_ind= q[:3], u_ind= u[:3], kd_eqs=kindiffs, + q_dependent=q[3:], configuration_constraints = f_c, + u_dependent=u[3:], velocity_constraints= f_v, + u_auxiliary=ua + ) + + # fr, frstar, frstar_steady and kdd(kinematic differential equations). + (fr, frstar)= kane.kanes_equations(bodyList, forceList) + frstar_steady = frstar.subs(ud_zero).subs(u_dep_dict).subs(steady_conditions)\ + .subs({q[3]: -r*cos(q[1])}).expand() + kdd = kane.kindiffdict() + + assert Matrix(Fr_c).expand() == fr.expand() + assert Matrix(Fr_star_c.subs(kdd)).expand() == frstar.expand() + # These Matrices have some Integer(0) and some Float(0). Running under + # SymEngine gives different types of zero. + assert (simplify(Matrix(Fr_star_steady).expand()).xreplace({0:0.0}) == + simplify(frstar_steady.expand()).xreplace({0:0.0})) + + syms_in_forcing = find_dynamicsymbols(kane.forcing) + for qdi in qd: + assert qdi not in syms_in_forcing + + +def test_non_central_inertia(): + # This tests that the calculation of Fr* does not depend the point + # about which the inertia of a rigid body is defined. This test solves + # exercises 8.12, 8.17 from Kane 1985. + + # Declare symbols + q1, q2, q3 = dynamicsymbols('q1:4') + q1d, q2d, q3d = dynamicsymbols('q1:4', level=1) + u1, u2, u3, u4, u5 = dynamicsymbols('u1:6') + u_prime, R, M, g, e, f, theta = symbols('u\' R, M, g, e, f, theta') + a, b, mA, mB, IA, J, K, t = symbols('a b mA mB IA J K t') + Q1, Q2, Q3 = symbols('Q1, Q2 Q3') + IA22, IA23, IA33 = symbols('IA22 IA23 IA33') + + # Reference Frames + F = ReferenceFrame('F') + P = F.orientnew('P', 'axis', [-theta, F.y]) + A = P.orientnew('A', 'axis', [q1, P.x]) + A.set_ang_vel(F, u1*A.x + u3*A.z) + # define frames for wheels + B = A.orientnew('B', 'axis', [q2, A.z]) + C = A.orientnew('C', 'axis', [q3, A.z]) + B.set_ang_vel(A, u4 * A.z) + C.set_ang_vel(A, u5 * A.z) + + # define points D, S*, Q on frame A and their velocities + pD = Point('D') + pD.set_vel(A, 0) + # u3 will not change v_D_F since wheels are still assumed to roll without slip. + pD.set_vel(F, u2 * A.y) + + pS_star = pD.locatenew('S*', e*A.y) + pQ = pD.locatenew('Q', f*A.y - R*A.x) + for p in [pS_star, pQ]: + p.v2pt_theory(pD, F, A) + + # masscenters of bodies A, B, C + pA_star = pD.locatenew('A*', a*A.y) + pB_star = pD.locatenew('B*', b*A.z) + pC_star = pD.locatenew('C*', -b*A.z) + for p in [pA_star, pB_star, pC_star]: + p.v2pt_theory(pD, F, A) + + # points of B, C touching the plane P + pB_hat = pB_star.locatenew('B^', -R*A.x) + pC_hat = pC_star.locatenew('C^', -R*A.x) + pB_hat.v2pt_theory(pB_star, F, B) + pC_hat.v2pt_theory(pC_star, F, C) + + # the velocities of B^, C^ are zero since B, C are assumed to roll without slip + kde = [q1d - u1, q2d - u4, q3d - u5] + vc = [dot(p.vel(F), A.y) for p in [pB_hat, pC_hat]] + + # inertias of bodies A, B, C + # IA22, IA23, IA33 are not specified in the problem statement, but are + # necessary to define an inertia object. Although the values of + # IA22, IA23, IA33 are not known in terms of the variables given in the + # problem statement, they do not appear in the general inertia terms. + inertia_A = inertia(A, IA, IA22, IA33, 0, IA23, 0) + inertia_B = inertia(B, K, K, J) + inertia_C = inertia(C, K, K, J) + + # define the rigid bodies A, B, C + rbA = RigidBody('rbA', pA_star, A, mA, (inertia_A, pA_star)) + rbB = RigidBody('rbB', pB_star, B, mB, (inertia_B, pB_star)) + rbC = RigidBody('rbC', pC_star, C, mB, (inertia_C, pC_star)) + + km = KanesMethod(F, q_ind=[q1, q2, q3], u_ind=[u1, u2], kd_eqs=kde, + u_dependent=[u4, u5], velocity_constraints=vc, + u_auxiliary=[u3]) + + forces = [(pS_star, -M*g*F.x), (pQ, Q1*A.x + Q2*A.y + Q3*A.z)] + bodies = [rbA, rbB, rbC] + fr, fr_star = km.kanes_equations(bodies, forces) + vc_map = solve(vc, [u4, u5]) + + # KanesMethod returns the negative of Fr, Fr* as defined in Kane1985. + fr_star_expected = Matrix([ + -(IA + 2*J*b**2/R**2 + 2*K + + mA*a**2 + 2*mB*b**2) * u1.diff(t) - mA*a*u1*u2, + -(mA + 2*mB +2*J/R**2) * u2.diff(t) + mA*a*u1**2, + 0]) + t = trigsimp(fr_star.subs(vc_map).subs({u3: 0})).doit().expand() + assert ((fr_star_expected - t).expand() == zeros(3, 1)) + + # define inertias of rigid bodies A, B, C about point D + # I_S/O = I_S/S* + I_S*/O + bodies2 = [] + for rb, I_star in zip([rbA, rbB, rbC], [inertia_A, inertia_B, inertia_C]): + I = I_star + inertia_of_point_mass(rb.mass, + rb.masscenter.pos_from(pD), + rb.frame) + bodies2.append(RigidBody('', rb.masscenter, rb.frame, rb.mass, + (I, pD))) + fr2, fr_star2 = km.kanes_equations(bodies2, forces) + + t = trigsimp(fr_star2.subs(vc_map).subs({u3: 0})).doit() + assert (fr_star_expected - t).expand() == zeros(3, 1) + +def test_sub_qdot(): + # This test solves exercises 8.12, 8.17 from Kane 1985 and defines + # some velocities in terms of q, qdot. + + ## --- Declare symbols --- + q1, q2, q3 = dynamicsymbols('q1:4') + q1d, q2d, q3d = dynamicsymbols('q1:4', level=1) + u1, u2, u3 = dynamicsymbols('u1:4') + u_prime, R, M, g, e, f, theta = symbols('u\' R, M, g, e, f, theta') + a, b, mA, mB, IA, J, K, t = symbols('a b mA mB IA J K t') + IA22, IA23, IA33 = symbols('IA22 IA23 IA33') + Q1, Q2, Q3 = symbols('Q1 Q2 Q3') + + # --- Reference Frames --- + F = ReferenceFrame('F') + P = F.orientnew('P', 'axis', [-theta, F.y]) + A = P.orientnew('A', 'axis', [q1, P.x]) + A.set_ang_vel(F, u1*A.x + u3*A.z) + # define frames for wheels + B = A.orientnew('B', 'axis', [q2, A.z]) + C = A.orientnew('C', 'axis', [q3, A.z]) + + ## --- define points D, S*, Q on frame A and their velocities --- + pD = Point('D') + pD.set_vel(A, 0) + # u3 will not change v_D_F since wheels are still assumed to roll w/o slip + pD.set_vel(F, u2 * A.y) + + pS_star = pD.locatenew('S*', e*A.y) + pQ = pD.locatenew('Q', f*A.y - R*A.x) + # masscenters of bodies A, B, C + pA_star = pD.locatenew('A*', a*A.y) + pB_star = pD.locatenew('B*', b*A.z) + pC_star = pD.locatenew('C*', -b*A.z) + for p in [pS_star, pQ, pA_star, pB_star, pC_star]: + p.v2pt_theory(pD, F, A) + + # points of B, C touching the plane P + pB_hat = pB_star.locatenew('B^', -R*A.x) + pC_hat = pC_star.locatenew('C^', -R*A.x) + pB_hat.v2pt_theory(pB_star, F, B) + pC_hat.v2pt_theory(pC_star, F, C) + + # --- relate qdot, u --- + # the velocities of B^, C^ are zero since B, C are assumed to roll w/o slip + kde = [dot(p.vel(F), A.y) for p in [pB_hat, pC_hat]] + kde += [u1 - q1d] + kde_map = solve(kde, [q1d, q2d, q3d]) + for k, v in list(kde_map.items()): + kde_map[k.diff(t)] = v.diff(t) + + # inertias of bodies A, B, C + # IA22, IA23, IA33 are not specified in the problem statement, but are + # necessary to define an inertia object. Although the values of + # IA22, IA23, IA33 are not known in terms of the variables given in the + # problem statement, they do not appear in the general inertia terms. + inertia_A = inertia(A, IA, IA22, IA33, 0, IA23, 0) + inertia_B = inertia(B, K, K, J) + inertia_C = inertia(C, K, K, J) + + # define the rigid bodies A, B, C + rbA = RigidBody('rbA', pA_star, A, mA, (inertia_A, pA_star)) + rbB = RigidBody('rbB', pB_star, B, mB, (inertia_B, pB_star)) + rbC = RigidBody('rbC', pC_star, C, mB, (inertia_C, pC_star)) + + ## --- use kanes method --- + km = KanesMethod(F, [q1, q2, q3], [u1, u2], kd_eqs=kde, u_auxiliary=[u3]) + + forces = [(pS_star, -M*g*F.x), (pQ, Q1*A.x + Q2*A.y + Q3*A.z)] + bodies = [rbA, rbB, rbC] + + # Q2 = -u_prime * u2 * Q1 / sqrt(u2**2 + f**2 * u1**2) + # -u_prime * R * u2 / sqrt(u2**2 + f**2 * u1**2) = R / Q1 * Q2 + fr_expected = Matrix([ + f*Q3 + M*g*e*sin(theta)*cos(q1), + Q2 + M*g*sin(theta)*sin(q1), + e*M*g*cos(theta) - Q1*f - Q2*R]) + #Q1 * (f - u_prime * R * u2 / sqrt(u2**2 + f**2 * u1**2)))]) + fr_star_expected = Matrix([ + -(IA + 2*J*b**2/R**2 + 2*K + + mA*a**2 + 2*mB*b**2) * u1.diff(t) - mA*a*u1*u2, + -(mA + 2*mB +2*J/R**2) * u2.diff(t) + mA*a*u1**2, + 0]) + + fr, fr_star = km.kanes_equations(bodies, forces) + assert (fr.expand() == fr_expected.expand()) + assert ((fr_star_expected - trigsimp(fr_star)).expand() == zeros(3, 1)) + +def test_sub_qdot2(): + # This test solves exercises 8.3 from Kane 1985 and defines + # all velocities in terms of q, qdot. We check that the generalized active + # forces are correctly computed if u terms are only defined in the + # kinematic differential equations. + # + # This functionality was added in PR 8948. Without qdot/u substitution, the + # KanesMethod constructor will fail during the constraint initialization as + # the B matrix will be poorly formed and inversion of the dependent part + # will fail. + + g, m, Px, Py, Pz, R, t = symbols('g m Px Py Pz R t') + q = dynamicsymbols('q:5') + qd = dynamicsymbols('q:5', level=1) + u = dynamicsymbols('u:5') + + ## Define inertial, intermediate, and rigid body reference frames + A = ReferenceFrame('A') + B_prime = A.orientnew('B_prime', 'Axis', [q[0], A.z]) + B = B_prime.orientnew('B', 'Axis', [pi/2 - q[1], B_prime.x]) + C = B.orientnew('C', 'Axis', [q[2], B.z]) + + ## Define points of interest and their velocities + pO = Point('O') + pO.set_vel(A, 0) + + # R is the point in plane H that comes into contact with disk C. + pR = pO.locatenew('R', q[3]*A.x + q[4]*A.y) + pR.set_vel(A, pR.pos_from(pO).diff(t, A)) + pR.set_vel(B, 0) + + # C^ is the point in disk C that comes into contact with plane H. + pC_hat = pR.locatenew('C^', 0) + pC_hat.set_vel(C, 0) + + # C* is the point at the center of disk C. + pCs = pC_hat.locatenew('C*', R*B.y) + pCs.set_vel(C, 0) + pCs.set_vel(B, 0) + + # calculate velocites of points C* and C^ in frame A + pCs.v2pt_theory(pR, A, B) # points C* and R are fixed in frame B + pC_hat.v2pt_theory(pCs, A, C) # points C* and C^ are fixed in frame C + + ## Define forces on each point of the system + R_C_hat = Px*A.x + Py*A.y + Pz*A.z + R_Cs = -m*g*A.z + forces = [(pC_hat, R_C_hat), (pCs, R_Cs)] + + ## Define kinematic differential equations + # let ui = omega_C_A & bi (i = 1, 2, 3) + # u4 = qd4, u5 = qd5 + u_expr = [C.ang_vel_in(A) & uv for uv in B] + u_expr += qd[3:] + kde = [ui - e for ui, e in zip(u, u_expr)] + km1 = KanesMethod(A, q, u, kde) + fr1, _ = km1.kanes_equations([], forces) + + ## Calculate generalized active forces if we impose the condition that the + # disk C is rolling without slipping + u_indep = u[:3] + u_dep = list(set(u) - set(u_indep)) + vc = [pC_hat.vel(A) & uv for uv in [A.x, A.y]] + km2 = KanesMethod(A, q, u_indep, kde, + u_dependent=u_dep, velocity_constraints=vc) + fr2, _ = km2.kanes_equations([], forces) + + fr1_expected = Matrix([ + -R*g*m*sin(q[1]), + -R*(Px*cos(q[0]) + Py*sin(q[0]))*tan(q[1]), + R*(Px*cos(q[0]) + Py*sin(q[0])), + Px, + Py]) + fr2_expected = Matrix([ + -R*g*m*sin(q[1]), + 0, + 0]) + assert (trigsimp(fr1.expand()) == trigsimp(fr1_expected.expand())) + assert (trigsimp(fr2.expand()) == trigsimp(fr2_expected.expand())) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane3.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane3.py new file mode 100644 index 0000000000000000000000000000000000000000..438759451cfb142c488b9b5c67ac269b668cac68 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane3.py @@ -0,0 +1,315 @@ +from sympy.core.numbers import pi +from sympy.core.symbol import symbols +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import acos, sin, cos +from sympy.matrices.dense import Matrix +from sympy.physics.mechanics import (ReferenceFrame, dynamicsymbols, + KanesMethod, inertia, Point, RigidBody, + dot) +from sympy.testing.pytest import slow + + +@slow +def test_bicycle(): + # Code to get equations of motion for a bicycle modeled as in: + # J.P Meijaard, Jim M Papadopoulos, Andy Ruina and A.L Schwab. Linearized + # dynamics equations for the balance and steer of a bicycle: a benchmark + # and review. Proceedings of The Royal Society (2007) 463, 1955-1982 + # doi: 10.1098/rspa.2007.1857 + + # Note that this code has been crudely ported from Autolev, which is the + # reason for some of the unusual naming conventions. It was purposefully as + # similar as possible in order to aide debugging. + + # Declare Coordinates & Speeds + # Simple definitions for qdots - qd = u + # Speeds are: + # - u1: yaw frame ang. rate + # - u2: roll frame ang. rate + # - u3: rear wheel frame ang. rate (spinning motion) + # - u4: frame ang. rate (pitching motion) + # - u5: steering frame ang. rate + # - u6: front wheel ang. rate (spinning motion) + # Wheel positions are ignorable coordinates, so they are not introduced. + q1, q2, q4, q5 = dynamicsymbols('q1 q2 q4 q5') + q1d, q2d, q4d, q5d = dynamicsymbols('q1 q2 q4 q5', 1) + u1, u2, u3, u4, u5, u6 = dynamicsymbols('u1 u2 u3 u4 u5 u6') + u1d, u2d, u3d, u4d, u5d, u6d = dynamicsymbols('u1 u2 u3 u4 u5 u6', 1) + + # Declare System's Parameters + WFrad, WRrad, htangle, forkoffset = symbols('WFrad WRrad htangle forkoffset') + forklength, framelength, forkcg1 = symbols('forklength framelength forkcg1') + forkcg3, framecg1, framecg3, Iwr11 = symbols('forkcg3 framecg1 framecg3 Iwr11') + Iwr22, Iwf11, Iwf22, Iframe11 = symbols('Iwr22 Iwf11 Iwf22 Iframe11') + Iframe22, Iframe33, Iframe31, Ifork11 = symbols('Iframe22 Iframe33 Iframe31 Ifork11') + Ifork22, Ifork33, Ifork31, g = symbols('Ifork22 Ifork33 Ifork31 g') + mframe, mfork, mwf, mwr = symbols('mframe mfork mwf mwr') + + # Set up reference frames for the system + # N - inertial + # Y - yaw + # R - roll + # WR - rear wheel, rotation angle is ignorable coordinate so not oriented + # Frame - bicycle frame + # TempFrame - statically rotated frame for easier reference inertia definition + # Fork - bicycle fork + # TempFork - statically rotated frame for easier reference inertia definition + # WF - front wheel, again posses a ignorable coordinate + N = ReferenceFrame('N') + Y = N.orientnew('Y', 'Axis', [q1, N.z]) + R = Y.orientnew('R', 'Axis', [q2, Y.x]) + Frame = R.orientnew('Frame', 'Axis', [q4 + htangle, R.y]) + WR = ReferenceFrame('WR') + TempFrame = Frame.orientnew('TempFrame', 'Axis', [-htangle, Frame.y]) + Fork = Frame.orientnew('Fork', 'Axis', [q5, Frame.x]) + TempFork = Fork.orientnew('TempFork', 'Axis', [-htangle, Fork.y]) + WF = ReferenceFrame('WF') + + # Kinematics of the Bicycle First block of code is forming the positions of + # the relevant points + # rear wheel contact -> rear wheel mass center -> frame mass center + + # frame/fork connection -> fork mass center + front wheel mass center -> + # front wheel contact point + WR_cont = Point('WR_cont') + WR_mc = WR_cont.locatenew('WR_mc', WRrad * R.z) + Steer = WR_mc.locatenew('Steer', framelength * Frame.z) + Frame_mc = WR_mc.locatenew('Frame_mc', - framecg1 * Frame.x + + framecg3 * Frame.z) + Fork_mc = Steer.locatenew('Fork_mc', - forkcg1 * Fork.x + + forkcg3 * Fork.z) + WF_mc = Steer.locatenew('WF_mc', forklength * Fork.x + forkoffset * Fork.z) + WF_cont = WF_mc.locatenew('WF_cont', WFrad * (dot(Fork.y, Y.z) * Fork.y - + Y.z).normalize()) + + # Set the angular velocity of each frame. + # Angular accelerations end up being calculated automatically by + # differentiating the angular velocities when first needed. + # u1 is yaw rate + # u2 is roll rate + # u3 is rear wheel rate + # u4 is frame pitch rate + # u5 is fork steer rate + # u6 is front wheel rate + Y.set_ang_vel(N, u1 * Y.z) + R.set_ang_vel(Y, u2 * R.x) + WR.set_ang_vel(Frame, u3 * Frame.y) + Frame.set_ang_vel(R, u4 * Frame.y) + Fork.set_ang_vel(Frame, u5 * Fork.x) + WF.set_ang_vel(Fork, u6 * Fork.y) + + # Form the velocities of the previously defined points, using the 2 - point + # theorem (written out by hand here). Accelerations again are calculated + # automatically when first needed. + WR_cont.set_vel(N, 0) + WR_mc.v2pt_theory(WR_cont, N, WR) + Steer.v2pt_theory(WR_mc, N, Frame) + Frame_mc.v2pt_theory(WR_mc, N, Frame) + Fork_mc.v2pt_theory(Steer, N, Fork) + WF_mc.v2pt_theory(Steer, N, Fork) + WF_cont.v2pt_theory(WF_mc, N, WF) + + # Sets the inertias of each body. Uses the inertia frame to construct the + # inertia dyadics. Wheel inertias are only defined by principle moments of + # inertia, and are in fact constant in the frame and fork reference frames; + # it is for this reason that the orientations of the wheels does not need + # to be defined. The frame and fork inertias are defined in the 'Temp' + # frames which are fixed to the appropriate body frames; this is to allow + # easier input of the reference values of the benchmark paper. Note that + # due to slightly different orientations, the products of inertia need to + # have their signs flipped; this is done later when entering the numerical + # value. + + Frame_I = (inertia(TempFrame, Iframe11, Iframe22, Iframe33, 0, 0, Iframe31), Frame_mc) + Fork_I = (inertia(TempFork, Ifork11, Ifork22, Ifork33, 0, 0, Ifork31), Fork_mc) + WR_I = (inertia(Frame, Iwr11, Iwr22, Iwr11), WR_mc) + WF_I = (inertia(Fork, Iwf11, Iwf22, Iwf11), WF_mc) + + # Declaration of the RigidBody containers. :: + + BodyFrame = RigidBody('BodyFrame', Frame_mc, Frame, mframe, Frame_I) + BodyFork = RigidBody('BodyFork', Fork_mc, Fork, mfork, Fork_I) + BodyWR = RigidBody('BodyWR', WR_mc, WR, mwr, WR_I) + BodyWF = RigidBody('BodyWF', WF_mc, WF, mwf, WF_I) + + # The kinematic differential equations; they are defined quite simply. Each + # entry in this list is equal to zero. + kd = [q1d - u1, q2d - u2, q4d - u4, q5d - u5] + + # The nonholonomic constraints are the velocity of the front wheel contact + # point dotted into the X, Y, and Z directions; the yaw frame is used as it + # is "closer" to the front wheel (1 less DCM connecting them). These + # constraints force the velocity of the front wheel contact point to be 0 + # in the inertial frame; the X and Y direction constraints enforce a + # "no-slip" condition, and the Z direction constraint forces the front + # wheel contact point to not move away from the ground frame, essentially + # replicating the holonomic constraint which does not allow the frame pitch + # to change in an invalid fashion. + + conlist_speed = [WF_cont.vel(N) & Y.x, WF_cont.vel(N) & Y.y, WF_cont.vel(N) & Y.z] + + # The holonomic constraint is that the position from the rear wheel contact + # point to the front wheel contact point when dotted into the + # normal-to-ground plane direction must be zero; effectively that the front + # and rear wheel contact points are always touching the ground plane. This + # is actually not part of the dynamic equations, but instead is necessary + # for the lineraization process. + + conlist_coord = [WF_cont.pos_from(WR_cont) & Y.z] + + # The force list; each body has the appropriate gravitational force applied + # at its mass center. + FL = [(Frame_mc, -mframe * g * Y.z), + (Fork_mc, -mfork * g * Y.z), + (WF_mc, -mwf * g * Y.z), + (WR_mc, -mwr * g * Y.z)] + BL = [BodyFrame, BodyFork, BodyWR, BodyWF] + + + # The N frame is the inertial frame, coordinates are supplied in the order + # of independent, dependent coordinates, as are the speeds. The kinematic + # differential equation are also entered here. Here the dependent speeds + # are specified, in the same order they were provided in earlier, along + # with the non-holonomic constraints. The dependent coordinate is also + # provided, with the holonomic constraint. Again, this is only provided + # for the linearization process. + + KM = KanesMethod(N, q_ind=[q1, q2, q5], + q_dependent=[q4], configuration_constraints=conlist_coord, + u_ind=[u2, u3, u5], + u_dependent=[u1, u4, u6], velocity_constraints=conlist_speed, + kd_eqs=kd, + constraint_solver="CRAMER") + (fr, frstar) = KM.kanes_equations(BL, FL) + + # This is the start of entering in the numerical values from the benchmark + # paper to validate the eigen values of the linearized equations from this + # model to the reference eigen values. Look at the aforementioned paper for + # more information. Some of these are intermediate values, used to + # transform values from the paper into the coordinate systems used in this + # model. + PaperRadRear = 0.3 + PaperRadFront = 0.35 + HTA = (pi / 2 - pi / 10).evalf() + TrailPaper = 0.08 + rake = (-(TrailPaper*sin(HTA)-(PaperRadFront*cos(HTA)))).evalf() + PaperWb = 1.02 + PaperFrameCgX = 0.3 + PaperFrameCgZ = 0.9 + PaperForkCgX = 0.9 + PaperForkCgZ = 0.7 + FrameLength = (PaperWb*sin(HTA)-(rake-(PaperRadFront-PaperRadRear)*cos(HTA))).evalf() + FrameCGNorm = ((PaperFrameCgZ - PaperRadRear-(PaperFrameCgX/sin(HTA))*cos(HTA))*sin(HTA)).evalf() + FrameCGPar = (PaperFrameCgX / sin(HTA) + (PaperFrameCgZ - PaperRadRear - PaperFrameCgX / sin(HTA) * cos(HTA)) * cos(HTA)).evalf() + tempa = (PaperForkCgZ - PaperRadFront) + tempb = (PaperWb-PaperForkCgX) + tempc = (sqrt(tempa**2+tempb**2)).evalf() + PaperForkL = (PaperWb*cos(HTA)-(PaperRadFront-PaperRadRear)*sin(HTA)).evalf() + ForkCGNorm = (rake+(tempc * sin(pi/2-HTA-acos(tempa/tempc)))).evalf() + ForkCGPar = (tempc * cos((pi/2-HTA)-acos(tempa/tempc))-PaperForkL).evalf() + + # Here is the final assembly of the numerical values. The symbol 'v' is the + # forward speed of the bicycle (a concept which only makes sense in the + # upright, static equilibrium case?). These are in a dictionary which will + # later be substituted in. Again the sign on the *product* of inertia + # values is flipped here, due to different orientations of coordinate + # systems. + v = symbols('v') + val_dict = {WFrad: PaperRadFront, + WRrad: PaperRadRear, + htangle: HTA, + forkoffset: rake, + forklength: PaperForkL, + framelength: FrameLength, + forkcg1: ForkCGPar, + forkcg3: ForkCGNorm, + framecg1: FrameCGNorm, + framecg3: FrameCGPar, + Iwr11: 0.0603, + Iwr22: 0.12, + Iwf11: 0.1405, + Iwf22: 0.28, + Ifork11: 0.05892, + Ifork22: 0.06, + Ifork33: 0.00708, + Ifork31: 0.00756, + Iframe11: 9.2, + Iframe22: 11, + Iframe33: 2.8, + Iframe31: -2.4, + mfork: 4, + mframe: 85, + mwf: 3, + mwr: 2, + g: 9.81, + q1: 0, + q2: 0, + q4: 0, + q5: 0, + u1: 0, + u2: 0, + u3: v / PaperRadRear, + u4: 0, + u5: 0, + u6: v / PaperRadFront} + + # Linearizes the forcing vector; the equations are set up as MM udot = + # forcing, where MM is the mass matrix, udot is the vector representing the + # time derivatives of the generalized speeds, and forcing is a vector which + # contains both external forcing terms and internal forcing terms, such as + # centripital or coriolis forces. This actually returns a matrix with as + # many rows as *total* coordinates and speeds, but only as many columns as + # independent coordinates and speeds. + + A, B, _ = KM.linearize( + A_and_B=True, + op_point={ + # Operating points for the accelerations are required for the + # linearizer to eliminate u' terms showing up in the coefficient + # matrices. + u1.diff(): 0, + u2.diff(): 0, + u3.diff(): 0, + u4.diff(): 0, + u5.diff(): 0, + u6.diff(): 0, + u1: 0, + u2: 0, + u3: v / PaperRadRear, + u4: 0, + u5: 0, + u6: v / PaperRadFront, + q1: 0, + q2: 0, + q4: 0, + q5: 0, + }, + linear_solver="CRAMER", + ) + # As mentioned above, the size of the linearized forcing terms is expanded + # to include both q's and u's, so the mass matrix must have this done as + # well. This will likely be changed to be part of the linearized process, + # for future reference. + A_s = A.xreplace(val_dict) + B_s = B.xreplace(val_dict) + + A_s = A_s.evalf() + B_s = B_s.evalf() + + # Finally, we construct an "A" matrix for the form xdot = A x (x being the + # state vector, although in this case, the sizes are a little off). The + # following line extracts only the minimum entries required for eigenvalue + # analysis, which correspond to rows and columns for lean, steer, lean + # rate, and steer rate. + A = A_s.extract([1, 2, 3, 5], [1, 2, 3, 5]) + + # Precomputed for comparison + Res = Matrix([[ 0, 0, 1.0, 0], + [ 0, 0, 0, 1.0], + [9.48977444677355, -0.891197738059089*v**2 - 0.571523173729245, -0.105522449805691*v, -0.330515398992311*v], + [11.7194768719633, -1.97171508499972*v**2 + 30.9087533932407, 3.67680523332152*v, -3.08486552743311*v]]) + + # Actual eigenvalue comparison + eps = 1.e-12 + for i in range(6): + error = Res.subs(v, i) - A.subs(v, i) + assert all(abs(x) < eps for x in error) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane4.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane4.py new file mode 100644 index 0000000000000000000000000000000000000000..a44dd2d407056ea36669268d478780fc581def51 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane4.py @@ -0,0 +1,115 @@ +from sympy import (cos, sin, Matrix, symbols) +from sympy.physics.mechanics import (dynamicsymbols, ReferenceFrame, Point, + KanesMethod, Particle) + +def test_replace_qdots_in_force(): + # Test PR 16700 "Replaces qdots with us in force-list in kanes.py" + # The new functionality allows one to specify forces in qdots which will + # automatically be replaced with u:s which are defined by the kde supplied + # to KanesMethod. The test case is the double pendulum with interacting + # forces in the example of chapter 4.7 "CONTRIBUTING INTERACTION FORCES" + # in Ref. [1]. Reference list at end test function. + + q1, q2 = dynamicsymbols('q1, q2') + qd1, qd2 = dynamicsymbols('q1, q2', level=1) + u1, u2 = dynamicsymbols('u1, u2') + + l, m = symbols('l, m') + + N = ReferenceFrame('N') # Inertial frame + A = N.orientnew('A', 'Axis', (q1, N.z)) # Rod A frame + B = A.orientnew('B', 'Axis', (q2, N.z)) # Rod B frame + + O = Point('O') # Origo + O.set_vel(N, 0) + + P = O.locatenew('P', ( l * A.x )) # Point @ end of rod A + P.v2pt_theory(O, N, A) + + Q = P.locatenew('Q', ( l * B.x )) # Point @ end of rod B + Q.v2pt_theory(P, N, B) + + Ap = Particle('Ap', P, m) + Bp = Particle('Bp', Q, m) + + # The forces are specified below. sigma is the torsional spring stiffness + # and delta is the viscous damping coefficient acting between the two + # bodies. Here, we specify the viscous damper as function of qdots prior + # forming the kde. In more complex systems it not might be obvious which + # kde is most efficient, why it is convenient to specify viscous forces in + # qdots independently of the kde. + sig, delta = symbols('sigma, delta') + Ta = (sig * q2 + delta * qd2) * N.z + forces = [(A, Ta), (B, -Ta)] + + # Try different kdes. + kde1 = [u1 - qd1, u2 - qd2] + kde2 = [u1 - qd1, u2 - (qd1 + qd2)] + + KM1 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde1) + fr1, fstar1 = KM1.kanes_equations([Ap, Bp], forces) + + KM2 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde2) + fr2, fstar2 = KM2.kanes_equations([Ap, Bp], forces) + + # Check EOM for KM2: + # Mass and force matrix from p.6 in Ref. [2] with added forces from + # example of chapter 4.7 in [1] and without gravity. + forcing_matrix_expected = Matrix( [ [ m * l**2 * sin(q2) * u2**2 + sig * q2 + + delta * (u2 - u1)], + [ m * l**2 * sin(q2) * -u1**2 - sig * q2 + - delta * (u2 - u1)] ] ) + mass_matrix_expected = Matrix( [ [ 2 * m * l**2, m * l**2 * cos(q2) ], + [ m * l**2 * cos(q2), m * l**2 ] ] ) + + assert (KM2.mass_matrix.expand() == mass_matrix_expected.expand()) + assert (KM2.forcing.expand() == forcing_matrix_expected.expand()) + + # Check fr1 with reference fr_expected from [1] with u:s instead of qdots. + fr1_expected = Matrix([ 0, -(sig*q2 + delta * u2) ]) + assert fr1.expand() == fr1_expected.expand() + + # Check fr2 + fr2_expected = Matrix([sig * q2 + delta * (u2 - u1), + - sig * q2 - delta * (u2 - u1)]) + assert fr2.expand() == fr2_expected.expand() + + # Specifying forces in u:s should stay the same: + Ta = (sig * q2 + delta * u2) * N.z + forces = [(A, Ta), (B, -Ta)] + KM1 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde1) + fr1, fstar1 = KM1.kanes_equations([Ap, Bp], forces) + + assert fr1.expand() == fr1_expected.expand() + + Ta = (sig * q2 + delta * (u2-u1)) * N.z + forces = [(A, Ta), (B, -Ta)] + KM2 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde2) + fr2, fstar2 = KM2.kanes_equations([Ap, Bp], forces) + + assert fr2.expand() == fr2_expected.expand() + + # Test if we have a qubic qdot force: + Ta = (sig * q2 + delta * qd2**3) * N.z + forces = [(A, Ta), (B, -Ta)] + + KM1 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde1) + fr1, fstar1 = KM1.kanes_equations([Ap, Bp], forces) + + fr1_cubic_expected = Matrix([ 0, -(sig*q2 + delta * u2**3) ]) + + assert fr1.expand() == fr1_cubic_expected.expand() + + KM2 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde2) + fr2, fstar2 = KM2.kanes_equations([Ap, Bp], forces) + + fr2_cubic_expected = Matrix([sig * q2 + delta * (u2 - u1)**3, + - sig * q2 - delta * (u2 - u1)**3]) + + assert fr2.expand() == fr2_cubic_expected.expand() + + # References: + # [1] T.R. Kane, D. a Levinson, Dynamics Theory and Applications, 2005. + # [2] Arun K Banerjee, Flexible Multibody Dynamics:Efficient Formulations + # and Applications, John Wiley and Sons, Ltd, 2016. + # doi:http://dx.doi.org/10.1002/9781119015635. diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane5.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane5.py new file mode 100644 index 0000000000000000000000000000000000000000..1d0f863e8fa0f46bcd8ae729a1a8852b702bdafa --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_kane5.py @@ -0,0 +1,128 @@ +from sympy import (zeros, Matrix, symbols, lambdify, sqrt, pi, + simplify) +from sympy.physics.mechanics import (dynamicsymbols, cross, inertia, RigidBody, + ReferenceFrame, KanesMethod) + + +def _create_rolling_disc(): + # Define symbols and coordinates + t = dynamicsymbols._t + q1, q2, q3, q4, q5, u1, u2, u3, u4, u5 = dynamicsymbols('q1:6 u1:6') + g, r, m = symbols('g r m') + # Define bodies and frames + ground = RigidBody('ground') + disc = RigidBody('disk', mass=m) + disc.inertia = (m * r ** 2 / 4 * inertia(disc.frame, 1, 2, 1), + disc.masscenter) + ground.masscenter.set_vel(ground.frame, 0) + disc.masscenter.set_vel(disc.frame, 0) + int_frame = ReferenceFrame('int_frame') + # Orient frames + int_frame.orient_body_fixed(ground.frame, (q1, q2, 0), 'zxy') + disc.frame.orient_axis(int_frame, int_frame.y, q3) + g_w_d = disc.frame.ang_vel_in(ground.frame) + disc.frame.set_ang_vel(ground.frame, + u1 * disc.x + u2 * disc.y + u3 * disc.z) + # Define points + cp = ground.masscenter.locatenew('contact_point', + q4 * ground.x + q5 * ground.y) + cp.set_vel(ground.frame, u4 * ground.x + u5 * ground.y) + disc.masscenter.set_pos(cp, r * int_frame.z) + disc.masscenter.set_vel(ground.frame, cross( + disc.frame.ang_vel_in(ground.frame), disc.masscenter.pos_from(cp))) + # Define kinematic differential equations + kdes = [g_w_d.dot(disc.x) - u1, g_w_d.dot(disc.y) - u2, + g_w_d.dot(disc.z) - u3, q4.diff(t) - u4, q5.diff(t) - u5] + # Define nonholonomic constraints + v0 = cp.vel(ground.frame) + cross( + disc.frame.ang_vel_in(int_frame), cp.pos_from(disc.masscenter)) + fnh = [v0.dot(ground.x), v0.dot(ground.y)] + # Define loads + loads = [(disc.masscenter, -disc.mass * g * ground.z)] + bodies = [disc] + return { + 'frame': ground.frame, + 'q_ind': [q1, q2, q3, q4, q5], + 'u_ind': [u1, u2, u3], + 'u_dep': [u4, u5], + 'kdes': kdes, + 'fnh': fnh, + 'bodies': bodies, + 'loads': loads + } + + +def _verify_rolling_disc_numerically(kane, all_zero=False): + q, u, p = dynamicsymbols('q1:6'), dynamicsymbols('u1:6'), symbols('g r m') + eval_sys = lambdify((q, u, p), (kane.mass_matrix_full, kane.forcing_full), + cse=True) + solve_sys = lambda q, u, p: Matrix.LUsolve( + *(Matrix(mat) for mat in eval_sys(q, u, p))) + solve_u_dep = lambdify((q, u[:3], p), kane._Ars * Matrix(u[:3]), cse=True) + eps = 1e-10 + p_vals = (9.81, 0.26, 3.43) + # First numeric test + q_vals = (0.3, 0.1, 1.97, -0.35, 2.27) + u_vals = [-0.2, 1.3, 0.15] + u_vals.extend(solve_u_dep(q_vals, u_vals, p_vals)[:2, 0]) + expected = Matrix([ + 0.126603940595934, 0.215942571601660, 1.28736069604936, + 0.319764288376543, 0.0989146857254898, -0.925848952664489, + -0.0181350656532944, 2.91695398184589, -0.00992793421754526, + 0.0412861634829171]) + assert all(abs(x) < eps for x in + (solve_sys(q_vals, u_vals, p_vals) - expected)) + # Second numeric test + q_vals = (3.97, -0.28, 8.2, -0.35, 2.27) + u_vals = [-0.25, -2.2, 0.62] + u_vals.extend(solve_u_dep(q_vals, u_vals, p_vals)[:2, 0]) + expected = Matrix([ + 0.0259159090798597, 0.668041660387416, -2.19283799213811, + 0.385441810852219, 0.420109283790573, 1.45030568179066, + -0.0110924422400793, -8.35617840186040, -0.154098542632173, + -0.146102664410010]) + assert all(abs(x) < eps for x in + (solve_sys(q_vals, u_vals, p_vals) - expected)) + if all_zero: + q_vals = (0, 0, 0, 0, 0) + u_vals = (0, 0, 0, 0, 0) + assert solve_sys(q_vals, u_vals, p_vals) == zeros(10, 1) + + +def test_kane_rolling_disc_lu(): + props = _create_rolling_disc() + kane = KanesMethod(props['frame'], props['q_ind'], props['u_ind'], + props['kdes'], u_dependent=props['u_dep'], + velocity_constraints=props['fnh'], + bodies=props['bodies'], forcelist=props['loads'], + explicit_kinematics=False, constraint_solver='LU') + kane.kanes_equations() + _verify_rolling_disc_numerically(kane) + + +def test_kane_rolling_disc_kdes_callable(): + props = _create_rolling_disc() + kane = KanesMethod( + props['frame'], props['q_ind'], props['u_ind'], props['kdes'], + u_dependent=props['u_dep'], velocity_constraints=props['fnh'], + bodies=props['bodies'], forcelist=props['loads'], + explicit_kinematics=False, + kd_eqs_solver=lambda A, b: simplify(A.LUsolve(b))) + q, u, p = dynamicsymbols('q1:6'), dynamicsymbols('u1:6'), symbols('g r m') + qd = dynamicsymbols('q1:6', 1) + eval_kdes = lambdify((q, qd, u, p), tuple(kane.kindiffdict().items())) + eps = 1e-10 + # Test with only zeros. If 'LU' would be used this would result in nan. + p_vals = (9.81, 0.25, 3.5) + zero_vals = (0, 0, 0, 0, 0) + assert all(abs(qdi - fui) < eps for qdi, fui in + eval_kdes(zero_vals, zero_vals, zero_vals, p_vals)) + # Test with some arbitrary values + q_vals = tuple(map(float, (pi / 6, pi / 3, pi / 2, 0.42, 0.62))) + qd_vals = tuple(map(float, (4, 1 / 3, 4 - 2 * sqrt(3), + 0.25 * (2 * sqrt(3) - 3), + 0.25 * (2 - sqrt(3))))) + u_vals = tuple(map(float, (-2, 4, 1 / 3, 0.25 * (-3 + 2 * sqrt(3)), + 0.25 * (-sqrt(3) + 2)))) + assert all(abs(qdi - fui) < eps for qdi, fui in + eval_kdes(q_vals, qd_vals, u_vals, p_vals)) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_lagrange.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_lagrange.py new file mode 100644 index 0000000000000000000000000000000000000000..81552bc7a4d0f6766dc46dcd47b7c7b1b0151b3f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_lagrange.py @@ -0,0 +1,247 @@ +from sympy.physics.mechanics import (dynamicsymbols, ReferenceFrame, Point, + RigidBody, LagrangesMethod, Particle, + inertia, Lagrangian) +from sympy.core.function import (Derivative, Function) +from sympy.core.numbers import pi +from sympy.core.symbol import symbols +from sympy.functions.elementary.trigonometric import (cos, sin, tan) +from sympy.matrices.dense import Matrix +from sympy.simplify.simplify import simplify +from sympy.testing.pytest import raises + + +def test_invalid_coordinates(): + # Simple pendulum, but use symbol instead of dynamicsymbol + l, m, g = symbols('l m g') + q = symbols('q') # Generalized coordinate + N, O = ReferenceFrame('N'), Point('O') + O.set_vel(N, 0) + P = Particle('P', Point('P'), m) + P.point.set_pos(O, l * (sin(q) * N.x - cos(q) * N.y)) + P.potential_energy = m * g * P.point.pos_from(O).dot(N.y) + L = Lagrangian(N, P) + raises(ValueError, lambda: LagrangesMethod(L, [q], bodies=P)) + + +def test_disc_on_an_incline_plane(): + # Disc rolling on an inclined plane + # First the generalized coordinates are created. The mass center of the + # disc is located from top vertex of the inclined plane by the generalized + # coordinate 'y'. The orientation of the disc is defined by the angle + # 'theta'. The mass of the disc is 'm' and its radius is 'R'. The length of + # the inclined path is 'l', the angle of inclination is 'alpha'. 'g' is the + # gravitational constant. + y, theta = dynamicsymbols('y theta') + yd, thetad = dynamicsymbols('y theta', 1) + m, g, R, l, alpha = symbols('m g R l alpha') + + # Next, we create the inertial reference frame 'N'. A reference frame 'A' + # is attached to the inclined plane. Finally a frame is created which is attached to the disk. + N = ReferenceFrame('N') + A = N.orientnew('A', 'Axis', [pi/2 - alpha, N.z]) + B = A.orientnew('B', 'Axis', [-theta, A.z]) + + # Creating the disc 'D'; we create the point that represents the mass + # center of the disc and set its velocity. The inertia dyadic of the disc + # is created. Finally, we create the disc. + Do = Point('Do') + Do.set_vel(N, yd * A.x) + I = m * R**2/2 * B.z | B.z + D = RigidBody('D', Do, B, m, (I, Do)) + + # To construct the Lagrangian, 'L', of the disc, we determine its kinetic + # and potential energies, T and U, respectively. L is defined as the + # difference between T and U. + D.potential_energy = m * g * (l - y) * sin(alpha) + L = Lagrangian(N, D) + + # We then create the list of generalized coordinates and constraint + # equations. The constraint arises due to the disc rolling without slip on + # on the inclined path. We then invoke the 'LagrangesMethod' class and + # supply it the necessary arguments and generate the equations of motion. + # The'rhs' method solves for the q_double_dots (i.e. the second derivative + # with respect to time of the generalized coordinates and the lagrange + # multipliers. + q = [y, theta] + hol_coneqs = [y - R * theta] + m = LagrangesMethod(L, q, hol_coneqs=hol_coneqs) + m.form_lagranges_equations() + rhs = m.rhs() + rhs.simplify() + assert rhs[2] == 2*g*sin(alpha)/3 + + +def test_simp_pen(): + # This tests that the equations generated by LagrangesMethod are identical + # to those obtained by hand calculations. The system under consideration is + # the simple pendulum. + # We begin by creating the generalized coordinates as per the requirements + # of LagrangesMethod. Also we created the associate symbols + # that characterize the system: 'm' is the mass of the bob, l is the length + # of the massless rigid rod connecting the bob to a point O fixed in the + # inertial frame. + q, u = dynamicsymbols('q u') + qd, ud = dynamicsymbols('q u ', 1) + l, m, g = symbols('l m g') + + # We then create the inertial frame and a frame attached to the massless + # string following which we define the inertial angular velocity of the + # string. + N = ReferenceFrame('N') + A = N.orientnew('A', 'Axis', [q, N.z]) + A.set_ang_vel(N, qd * N.z) + + # Next, we create the point O and fix it in the inertial frame. We then + # locate the point P to which the bob is attached. Its corresponding + # velocity is then determined by the 'two point formula'. + O = Point('O') + O.set_vel(N, 0) + P = O.locatenew('P', l * A.x) + P.v2pt_theory(O, N, A) + + # The 'Particle' which represents the bob is then created and its + # Lagrangian generated. + Pa = Particle('Pa', P, m) + Pa.potential_energy = - m * g * l * cos(q) + L = Lagrangian(N, Pa) + + # The 'LagrangesMethod' class is invoked to obtain equations of motion. + lm = LagrangesMethod(L, [q]) + lm.form_lagranges_equations() + RHS = lm.rhs() + assert RHS[1] == -g*sin(q)/l + + +def test_nonminimal_pendulum(): + q1, q2 = dynamicsymbols('q1:3') + q1d, q2d = dynamicsymbols('q1:3', level=1) + L, m, t = symbols('L, m, t') + g = 9.8 + # Compose World Frame + N = ReferenceFrame('N') + pN = Point('N*') + pN.set_vel(N, 0) + # Create point P, the pendulum mass + P = pN.locatenew('P1', q1*N.x + q2*N.y) + P.set_vel(N, P.pos_from(pN).dt(N)) + pP = Particle('pP', P, m) + # Constraint Equations + f_c = Matrix([q1**2 + q2**2 - L**2]) + # Calculate the lagrangian, and form the equations of motion + Lag = Lagrangian(N, pP) + LM = LagrangesMethod(Lag, [q1, q2], hol_coneqs=f_c, + forcelist=[(P, m*g*N.x)], frame=N) + LM.form_lagranges_equations() + # Check solution + lam1 = LM.lam_vec[0, 0] + eom_sol = Matrix([[m*Derivative(q1, t, t) - 9.8*m + 2*lam1*q1], + [m*Derivative(q2, t, t) + 2*lam1*q2]]) + assert LM.eom == eom_sol + # Check multiplier solution + lam_sol = Matrix([(19.6*q1 + 2*q1d**2 + 2*q2d**2)/(4*q1**2/m + 4*q2**2/m)]) + assert simplify(LM.solve_multipliers(sol_type='Matrix')) == simplify(lam_sol) + + +def test_dub_pen(): + + # The system considered is the double pendulum. Like in the + # test of the simple pendulum above, we begin by creating the generalized + # coordinates and the simple generalized speeds and accelerations which + # will be used later. Following this we create frames and points necessary + # for the kinematics. The procedure isn't explicitly explained as this is + # similar to the simple pendulum. Also this is documented on the pydy.org + # website. + q1, q2 = dynamicsymbols('q1 q2') + q1d, q2d = dynamicsymbols('q1 q2', 1) + q1dd, q2dd = dynamicsymbols('q1 q2', 2) + u1, u2 = dynamicsymbols('u1 u2') + u1d, u2d = dynamicsymbols('u1 u2', 1) + l, m, g = symbols('l m g') + + N = ReferenceFrame('N') + A = N.orientnew('A', 'Axis', [q1, N.z]) + B = N.orientnew('B', 'Axis', [q2, N.z]) + + A.set_ang_vel(N, q1d * A.z) + B.set_ang_vel(N, q2d * A.z) + + O = Point('O') + P = O.locatenew('P', l * A.x) + R = P.locatenew('R', l * B.x) + + O.set_vel(N, 0) + P.v2pt_theory(O, N, A) + R.v2pt_theory(P, N, B) + + ParP = Particle('ParP', P, m) + ParR = Particle('ParR', R, m) + + ParP.potential_energy = - m * g * l * cos(q1) + ParR.potential_energy = - m * g * l * cos(q1) - m * g * l * cos(q2) + L = Lagrangian(N, ParP, ParR) + lm = LagrangesMethod(L, [q1, q2], bodies=[ParP, ParR]) + lm.form_lagranges_equations() + + assert simplify(l*m*(2*g*sin(q1) + l*sin(q1)*sin(q2)*q2dd + + l*sin(q1)*cos(q2)*q2d**2 - l*sin(q2)*cos(q1)*q2d**2 + + l*cos(q1)*cos(q2)*q2dd + 2*l*q1dd) - lm.eom[0]) == 0 + assert simplify(l*m*(g*sin(q2) + l*sin(q1)*sin(q2)*q1dd + - l*sin(q1)*cos(q2)*q1d**2 + l*sin(q2)*cos(q1)*q1d**2 + + l*cos(q1)*cos(q2)*q1dd + l*q2dd) - lm.eom[1]) == 0 + assert lm.bodies == [ParP, ParR] + + +def test_rolling_disc(): + # Rolling Disc Example + # Here the rolling disc is formed from the contact point up, removing the + # need to introduce generalized speeds. Only 3 configuration and 3 + # speed variables are need to describe this system, along with the + # disc's mass and radius, and the local gravity. + q1, q2, q3 = dynamicsymbols('q1 q2 q3') + q1d, q2d, q3d = dynamicsymbols('q1 q2 q3', 1) + r, m, g = symbols('r m g') + + # The kinematics are formed by a series of simple rotations. Each simple + # rotation creates a new frame, and the next rotation is defined by the new + # frame's basis vectors. This example uses a 3-1-2 series of rotations, or + # Z, X, Y series of rotations. Angular velocity for this is defined using + # the second frame's basis (the lean frame). + N = ReferenceFrame('N') + Y = N.orientnew('Y', 'Axis', [q1, N.z]) + L = Y.orientnew('L', 'Axis', [q2, Y.x]) + R = L.orientnew('R', 'Axis', [q3, L.y]) + + # This is the translational kinematics. We create a point with no velocity + # in N; this is the contact point between the disc and ground. Next we form + # the position vector from the contact point to the disc's center of mass. + # Finally we form the velocity and acceleration of the disc. + C = Point('C') + C.set_vel(N, 0) + Dmc = C.locatenew('Dmc', r * L.z) + Dmc.v2pt_theory(C, N, R) + + # Forming the inertia dyadic. + I = inertia(L, m/4 * r**2, m/2 * r**2, m/4 * r**2) + BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc)) + + # Finally we form the equations of motion, using the same steps we did + # before. Supply the Lagrangian, the generalized speeds. + BodyD.potential_energy = - m * g * r * cos(q2) + Lag = Lagrangian(N, BodyD) + q = [q1, q2, q3] + q1 = Function('q1') + q2 = Function('q2') + q3 = Function('q3') + l = LagrangesMethod(Lag, q) + l.form_lagranges_equations() + RHS = l.rhs() + RHS.simplify() + t = symbols('t') + + assert (l.mass_matrix[3:6] == [0, 5*m*r**2/4, 0]) + assert RHS[4].simplify() == ( + (-8*g*sin(q2(t)) + r*(5*sin(2*q2(t))*Derivative(q1(t), t) + + 12*cos(q2(t))*Derivative(q3(t), t))*Derivative(q1(t), t))/(10*r)) + assert RHS[5] == (-5*cos(q2(t))*Derivative(q1(t), t) + 6*tan(q2(t) + )*Derivative(q3(t), t) + 4*Derivative(q1(t), t)/cos(q2(t)) + )*Derivative(q2(t), t) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_lagrange2.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_lagrange2.py new file mode 100644 index 0000000000000000000000000000000000000000..7602df157e9beb13db1dbb68a2980765cdc49bf2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_lagrange2.py @@ -0,0 +1,46 @@ +from sympy import symbols +from sympy.physics.mechanics import dynamicsymbols +from sympy.physics.mechanics import ReferenceFrame, Point, Particle +from sympy.physics.mechanics import LagrangesMethod, Lagrangian + +### This test asserts that a system with more than one external forces +### is accurately formed with Lagrange method (see issue #8626) + +def test_lagrange_2forces(): + ### Equations for two damped springs in series with two forces + + ### generalized coordinates + q1, q2 = dynamicsymbols('q1, q2') + ### generalized speeds + q1d, q2d = dynamicsymbols('q1, q2', 1) + + ### Mass, spring strength, friction coefficient + m, k, nu = symbols('m, k, nu') + + N = ReferenceFrame('N') + O = Point('O') + + ### Two points + P1 = O.locatenew('P1', q1 * N.x) + P1.set_vel(N, q1d * N.x) + P2 = O.locatenew('P1', q2 * N.x) + P2.set_vel(N, q2d * N.x) + + pP1 = Particle('pP1', P1, m) + pP1.potential_energy = k * q1**2 / 2 + + pP2 = Particle('pP2', P2, m) + pP2.potential_energy = k * (q1 - q2)**2 / 2 + + #### Friction forces + forcelist = [(P1, - nu * q1d * N.x), + (P2, - nu * q2d * N.x)] + lag = Lagrangian(N, pP1, pP2) + + l_method = LagrangesMethod(lag, (q1, q2), forcelist=forcelist, frame=N) + l_method.form_lagranges_equations() + + eq1 = l_method.eom[0] + assert eq1.diff(q1d) == nu + eq2 = l_method.eom[1] + assert eq2.diff(q2d) == nu diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_linearity_of_velocity_constraints.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_linearity_of_velocity_constraints.py new file mode 100644 index 0000000000000000000000000000000000000000..33c9e7ec070a3e6db2a6e26697d670964b0a32b9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_linearity_of_velocity_constraints.py @@ -0,0 +1,41 @@ +from sympy import symbols, sin, cos +from sympy.physics.mechanics import (dynamicsymbols, ReferenceFrame, Point, + KanesMethod) +from sympy.testing import pytest +from sympy.solvers.solveset import NonlinearError + +def test_linearity_of_motion_constraints(): + # Test that an error is raised by KanesMethod if nonlinear velocity + # constraints are supplied. + # It is a simple pendulum. + t = dynamicsymbols._t + N, A = ReferenceFrame('N'), ReferenceFrame('A') + O, P = Point('O'), Point('P') + O.set_vel(N, 0) + + l = symbols('l') + q, x, y, u, ux, uy = dynamicsymbols('q x y u ux uy') + + A.orient_axis(N, q, N.z) + A.set_ang_vel(N, u * N.z) + P.set_pos(O, -l * A.y) + P.v2pt_theory(O, N, A) + + kd = [u - q.diff(t), ux - x.diff(t), uy - y.diff(t)] + config_constr = [x - l * sin(q), y - l * cos(q)] + + q_ind = [q] + q_dep = [x, y] + u_ind = [u] + u_dep = [ux, uy] + + # Make sure an error is raised if nonlinear velocity constraints are + # supplied. + speed_constr = [ux - l * q.diff(t) * cos(q), sin(uy) + + l * q.diff(t) * sin(q)] + + with pytest.raises(NonlinearError): + KanesMethod(N, q_ind=q_ind, q_dependent=q_dep, u_ind=u_ind, + u_dependent=u_dep, kd_eqs=kd, + configuration_constraints=config_constr, + velocity_constraints=speed_constr) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_linearize.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_linearize.py new file mode 100644 index 0000000000000000000000000000000000000000..ec62b960b71d7fce5a5504478431ca23eb371fe0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_linearize.py @@ -0,0 +1,372 @@ +from sympy import symbols, Matrix, cos, sin, atan, sqrt, Rational +from sympy.core.sympify import sympify +from sympy.simplify.simplify import simplify +from sympy.solvers.solvers import solve +from sympy.physics.mechanics import dynamicsymbols, ReferenceFrame, Point,\ + dot, cross, inertia, KanesMethod, Particle, RigidBody, Lagrangian,\ + LagrangesMethod +from sympy.testing.pytest import slow + + +@slow +def test_linearize_rolling_disc_kane(): + # Symbols for time and constant parameters + t, r, m, g, v = symbols('t r m g v') + + # Configuration variables and their time derivatives + q1, q2, q3, q4, q5, q6 = q = dynamicsymbols('q1:7') + q1d, q2d, q3d, q4d, q5d, q6d = qd = [qi.diff(t) for qi in q] + + # Generalized speeds and their time derivatives + u = dynamicsymbols('u:6') + u1, u2, u3, u4, u5, u6 = u = dynamicsymbols('u1:7') + u1d, u2d, u3d, u4d, u5d, u6d = [ui.diff(t) for ui in u] + + # Reference frames + N = ReferenceFrame('N') # Inertial frame + NO = Point('NO') # Inertial origin + A = N.orientnew('A', 'Axis', [q1, N.z]) # Yaw intermediate frame + B = A.orientnew('B', 'Axis', [q2, A.x]) # Lean intermediate frame + C = B.orientnew('C', 'Axis', [q3, B.y]) # Disc fixed frame + CO = NO.locatenew('CO', q4*N.x + q5*N.y + q6*N.z) # Disc center + + # Disc angular velocity in N expressed using time derivatives of coordinates + w_c_n_qd = C.ang_vel_in(N) + w_b_n_qd = B.ang_vel_in(N) + + # Inertial angular velocity and angular acceleration of disc fixed frame + C.set_ang_vel(N, u1*B.x + u2*B.y + u3*B.z) + + # Disc center velocity in N expressed using time derivatives of coordinates + v_co_n_qd = CO.pos_from(NO).dt(N) + + # Disc center velocity in N expressed using generalized speeds + CO.set_vel(N, u4*C.x + u5*C.y + u6*C.z) + + # Disc Ground Contact Point + P = CO.locatenew('P', r*B.z) + P.v2pt_theory(CO, N, C) + + # Configuration constraint + f_c = Matrix([q6 - dot(CO.pos_from(P), N.z)]) + + # Velocity level constraints + f_v = Matrix([dot(P.vel(N), uv) for uv in C]) + + # Kinematic differential equations + kindiffs = Matrix([dot(w_c_n_qd - C.ang_vel_in(N), uv) for uv in B] + + [dot(v_co_n_qd - CO.vel(N), uv) for uv in N]) + qdots = solve(kindiffs, qd) + + # Set angular velocity of remaining frames + B.set_ang_vel(N, w_b_n_qd.subs(qdots)) + C.set_ang_acc(N, C.ang_vel_in(N).dt(B) + cross(B.ang_vel_in(N), C.ang_vel_in(N))) + + # Active forces + F_CO = m*g*A.z + + # Create inertia dyadic of disc C about point CO + I = (m * r**2) / 4 + J = (m * r**2) / 2 + I_C_CO = inertia(C, I, J, I) + + Disc = RigidBody('Disc', CO, C, m, (I_C_CO, CO)) + BL = [Disc] + FL = [(CO, F_CO)] + KM = KanesMethod(N, [q1, q2, q3, q4, q5], [u1, u2, u3], kd_eqs=kindiffs, + q_dependent=[q6], configuration_constraints=f_c, + u_dependent=[u4, u5, u6], velocity_constraints=f_v) + (fr, fr_star) = KM.kanes_equations(BL, FL) + + # Test generalized form equations + linearizer = KM.to_linearizer() + assert linearizer.f_c == f_c + assert linearizer.f_v == f_v + assert linearizer.f_a == f_v.diff(t).subs(KM.kindiffdict()) + sol = solve(linearizer.f_0 + linearizer.f_1, qd) + for qi in qdots.keys(): + assert sol[qi] == qdots[qi] + assert simplify(linearizer.f_2 + linearizer.f_3 - fr - fr_star) == Matrix([0, 0, 0]) + + # Perform the linearization + # Precomputed operating point + q_op = {q6: -r*cos(q2)} + u_op = {u1: 0, + u2: sin(q2)*q1d + q3d, + u3: cos(q2)*q1d, + u4: -r*(sin(q2)*q1d + q3d)*cos(q3), + u5: 0, + u6: -r*(sin(q2)*q1d + q3d)*sin(q3)} + qd_op = {q2d: 0, + q4d: -r*(sin(q2)*q1d + q3d)*cos(q1), + q5d: -r*(sin(q2)*q1d + q3d)*sin(q1), + q6d: 0} + ud_op = {u1d: 4*g*sin(q2)/(5*r) + sin(2*q2)*q1d**2/2 + 6*cos(q2)*q1d*q3d/5, + u2d: 0, + u3d: 0, + u4d: r*(sin(q2)*sin(q3)*q1d*q3d + sin(q3)*q3d**2), + u5d: r*(4*g*sin(q2)/(5*r) + sin(2*q2)*q1d**2/2 + 6*cos(q2)*q1d*q3d/5), + u6d: -r*(sin(q2)*cos(q3)*q1d*q3d + cos(q3)*q3d**2)} + + A, B = linearizer.linearize(op_point=[q_op, u_op, qd_op, ud_op], A_and_B=True, simplify=True) + + upright_nominal = {q1d: 0, q2: 0, m: 1, r: 1, g: 1} + + # Precomputed solution + A_sol = Matrix([[0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [sin(q1)*q3d, 0, 0, 0, 0, -sin(q1), -cos(q1), 0], + [-cos(q1)*q3d, 0, 0, 0, 0, cos(q1), -sin(q1), 0], + [0, Rational(4, 5), 0, 0, 0, 0, 0, 6*q3d/5], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, -2*q3d, 0, 0]]) + B_sol = Matrix([]) + + # Check that linearization is correct + assert A.subs(upright_nominal) == A_sol + assert B.subs(upright_nominal) == B_sol + + # Check eigenvalues at critical speed are all zero: + assert sympify(A.subs(upright_nominal).subs(q3d, 1/sqrt(3))).eigenvals() == {0: 8} + + # Check whether alternative solvers work + # symengine doesn't support method='GJ' + linearizer = KM.to_linearizer(linear_solver='GJ') + A, B = linearizer.linearize(op_point=[q_op, u_op, qd_op, ud_op], + A_and_B=True, simplify=True) + assert A.subs(upright_nominal) == A_sol + assert B.subs(upright_nominal) == B_sol + +def test_linearize_pendulum_kane_minimal(): + q1 = dynamicsymbols('q1') # angle of pendulum + u1 = dynamicsymbols('u1') # Angular velocity + q1d = dynamicsymbols('q1', 1) # Angular velocity + L, m, t = symbols('L, m, t') + g = 9.8 + + # Compose world frame + N = ReferenceFrame('N') + pN = Point('N*') + pN.set_vel(N, 0) + + # A.x is along the pendulum + A = N.orientnew('A', 'axis', [q1, N.z]) + A.set_ang_vel(N, u1*N.z) + + # Locate point P relative to the origin N* + P = pN.locatenew('P', L*A.x) + P.v2pt_theory(pN, N, A) + pP = Particle('pP', P, m) + + # Create Kinematic Differential Equations + kde = Matrix([q1d - u1]) + + # Input the force resultant at P + R = m*g*N.x + + # Solve for eom with kanes method + KM = KanesMethod(N, q_ind=[q1], u_ind=[u1], kd_eqs=kde) + (fr, frstar) = KM.kanes_equations([pP], [(P, R)]) + + # Linearize + A, B, inp_vec = KM.linearize(A_and_B=True, simplify=True) + + assert A == Matrix([[0, 1], [-9.8*cos(q1)/L, 0]]) + assert B == Matrix([]) + +def test_linearize_pendulum_kane_nonminimal(): + # Create generalized coordinates and speeds for this non-minimal realization + # q1, q2 = N.x and N.y coordinates of pendulum + # u1, u2 = N.x and N.y velocities of pendulum + q1, q2 = dynamicsymbols('q1:3') + q1d, q2d = dynamicsymbols('q1:3', level=1) + u1, u2 = dynamicsymbols('u1:3') + u1d, u2d = dynamicsymbols('u1:3', level=1) + L, m, t = symbols('L, m, t') + g = 9.8 + + # Compose world frame + N = ReferenceFrame('N') + pN = Point('N*') + pN.set_vel(N, 0) + + # A.x is along the pendulum + theta1 = atan(q2/q1) + A = N.orientnew('A', 'axis', [theta1, N.z]) + + # Locate the pendulum mass + P = pN.locatenew('P1', q1*N.x + q2*N.y) + pP = Particle('pP', P, m) + + # Calculate the kinematic differential equations + kde = Matrix([q1d - u1, + q2d - u2]) + dq_dict = solve(kde, [q1d, q2d]) + + # Set velocity of point P + P.set_vel(N, P.pos_from(pN).dt(N).subs(dq_dict)) + + # Configuration constraint is length of pendulum + f_c = Matrix([P.pos_from(pN).magnitude() - L]) + + # Velocity constraint is that the velocity in the A.x direction is + # always zero (the pendulum is never getting longer). + f_v = Matrix([P.vel(N).express(A).dot(A.x)]) + f_v.simplify() + + # Acceleration constraints is the time derivative of the velocity constraint + f_a = f_v.diff(t) + f_a.simplify() + + # Input the force resultant at P + R = m*g*N.x + + # Derive the equations of motion using the KanesMethod class. + KM = KanesMethod(N, q_ind=[q2], u_ind=[u2], q_dependent=[q1], + u_dependent=[u1], configuration_constraints=f_c, + velocity_constraints=f_v, acceleration_constraints=f_a, kd_eqs=kde) + (fr, frstar) = KM.kanes_equations([pP], [(P, R)]) + + # Set the operating point to be straight down, and non-moving + q_op = {q1: L, q2: 0} + u_op = {u1: 0, u2: 0} + ud_op = {u1d: 0, u2d: 0} + + A, B, inp_vec = KM.linearize(op_point=[q_op, u_op, ud_op], A_and_B=True, + simplify=True) + + assert A.expand() == Matrix([[0, 1], [-9.8/L, 0]]) + assert B == Matrix([]) + + + # symengine doesn't support method='GJ' + A, B, inp_vec = KM.linearize(op_point=[q_op, u_op, ud_op], A_and_B=True, + simplify=True, linear_solver='GJ') + + assert A.expand() == Matrix([[0, 1], [-9.8/L, 0]]) + assert B == Matrix([]) + + A, B, inp_vec = KM.linearize(op_point=[q_op, u_op, ud_op], + A_and_B=True, + simplify=True, + linear_solver=lambda A, b: A.LUsolve(b)) + + assert A.expand() == Matrix([[0, 1], [-9.8/L, 0]]) + assert B == Matrix([]) + + +def test_linearize_pendulum_lagrange_minimal(): + q1 = dynamicsymbols('q1') # angle of pendulum + q1d = dynamicsymbols('q1', 1) # Angular velocity + L, m, t = symbols('L, m, t') + g = 9.8 + + # Compose world frame + N = ReferenceFrame('N') + pN = Point('N*') + pN.set_vel(N, 0) + + # A.x is along the pendulum + A = N.orientnew('A', 'axis', [q1, N.z]) + A.set_ang_vel(N, q1d*N.z) + + # Locate point P relative to the origin N* + P = pN.locatenew('P', L*A.x) + P.v2pt_theory(pN, N, A) + pP = Particle('pP', P, m) + + # Solve for eom with Lagranges method + Lag = Lagrangian(N, pP) + LM = LagrangesMethod(Lag, [q1], forcelist=[(P, m*g*N.x)], frame=N) + LM.form_lagranges_equations() + + # Linearize + A, B, inp_vec = LM.linearize([q1], [q1d], A_and_B=True) + + assert simplify(A) == Matrix([[0, 1], [-9.8*cos(q1)/L, 0]]) + assert B == Matrix([]) + + # Check an alternative solver + A, B, inp_vec = LM.linearize([q1], [q1d], A_and_B=True, linear_solver='GJ') + + assert simplify(A) == Matrix([[0, 1], [-9.8*cos(q1)/L, 0]]) + assert B == Matrix([]) + + +def test_linearize_pendulum_lagrange_nonminimal(): + q1, q2 = dynamicsymbols('q1:3') + q1d, q2d = dynamicsymbols('q1:3', level=1) + L, m, t = symbols('L, m, t') + g = 9.8 + # Compose World Frame + N = ReferenceFrame('N') + pN = Point('N*') + pN.set_vel(N, 0) + # A.x is along the pendulum + theta1 = atan(q2/q1) + A = N.orientnew('A', 'axis', [theta1, N.z]) + # Create point P, the pendulum mass + P = pN.locatenew('P1', q1*N.x + q2*N.y) + P.set_vel(N, P.pos_from(pN).dt(N)) + pP = Particle('pP', P, m) + # Constraint Equations + f_c = Matrix([q1**2 + q2**2 - L**2]) + # Calculate the lagrangian, and form the equations of motion + Lag = Lagrangian(N, pP) + LM = LagrangesMethod(Lag, [q1, q2], hol_coneqs=f_c, forcelist=[(P, m*g*N.x)], frame=N) + LM.form_lagranges_equations() + # Compose operating point + op_point = {q1: L, q2: 0, q1d: 0, q2d: 0, q1d.diff(t): 0, q2d.diff(t): 0} + # Solve for multiplier operating point + lam_op = LM.solve_multipliers(op_point=op_point) + op_point.update(lam_op) + # Perform the Linearization + A, B, inp_vec = LM.linearize([q2], [q2d], [q1], [q1d], + op_point=op_point, A_and_B=True) + assert simplify(A) == Matrix([[0, 1], [-9.8/L, 0]]) + assert B == Matrix([]) + + # Check if passing a function to linear_solver works + A, B, inp_vec = LM.linearize([q2], [q2d], [q1], [q1d], op_point=op_point, + A_and_B=True, linear_solver=lambda A, b: + A.LUsolve(b)) + assert simplify(A) == Matrix([[0, 1], [-9.8/L, 0]]) + assert B == Matrix([]) + +def test_linearize_rolling_disc_lagrange(): + q1, q2, q3 = q = dynamicsymbols('q1 q2 q3') + q1d, q2d, q3d = qd = dynamicsymbols('q1 q2 q3', 1) + r, m, g = symbols('r m g') + + N = ReferenceFrame('N') + Y = N.orientnew('Y', 'Axis', [q1, N.z]) + L = Y.orientnew('L', 'Axis', [q2, Y.x]) + R = L.orientnew('R', 'Axis', [q3, L.y]) + + C = Point('C') + C.set_vel(N, 0) + Dmc = C.locatenew('Dmc', r * L.z) + Dmc.v2pt_theory(C, N, R) + + I = inertia(L, m / 4 * r**2, m / 2 * r**2, m / 4 * r**2) + BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc)) + BodyD.potential_energy = - m * g * r * cos(q2) + + Lag = Lagrangian(N, BodyD) + l = LagrangesMethod(Lag, q) + l.form_lagranges_equations() + + # Linearize about steady-state upright rolling + op_point = {q1: 0, q2: 0, q3: 0, + q1d: 0, q2d: 0, + q1d.diff(): 0, q2d.diff(): 0, q3d.diff(): 0} + A = l.linearize(q_ind=q, qd_ind=qd, op_point=op_point, A_and_B=True)[0] + sol = Matrix([[0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, -6*q3d, 0], + [0, -4*g/(5*r), 0, 6*q3d/5, 0, 0], + [0, 0, 0, 0, 0, 0]]) + + assert A == sol diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_loads.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_loads.py new file mode 100644 index 0000000000000000000000000000000000000000..8aa0cec14887f0778fc1e60e7ff33830ceef72d3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_loads.py @@ -0,0 +1,86 @@ +from pytest import raises + +from sympy import symbols +from sympy.physics.mechanics import (RigidBody, Particle, ReferenceFrame, Point, + outer, dynamicsymbols, Force, Torque) +from sympy.physics.mechanics.loads import gravity, _parse_load + + +def test_force_default(): + N = ReferenceFrame('N') + Po = Point('Po') + f1 = Force(Po, N.x) + assert f1.point == Po + assert f1.force == N.x + assert f1.__repr__() == 'Force(point=Po, force=N.x)' + # Test tuple behaviour + assert isinstance(f1, tuple) + assert f1[0] == Po + assert f1[1] == N.x + assert f1 == (Po, N.x) + assert f1 != (N.x, Po) + assert f1 != (Po, N.x + N.y) + assert f1 != (Point('Co'), N.x) + # Test body as input + P = Particle('P', Po) + f2 = Force(P, N.x) + assert f1 == f2 + + +def test_torque_default(): + N = ReferenceFrame('N') + f1 = Torque(N, N.x) + assert f1.frame == N + assert f1.torque == N.x + assert f1.__repr__() == 'Torque(frame=N, torque=N.x)' + # Test tuple behaviour + assert isinstance(f1, tuple) + assert f1[0] == N + assert f1[1] == N.x + assert f1 == (N, N.x) + assert f1 != (N.x, N) + assert f1 != (N, N.x + N.y) + assert f1 != (ReferenceFrame('A'), N.x) + # Test body as input + rb = RigidBody('P', frame=N) + f2 = Torque(rb, N.x) + assert f1 == f2 + + +def test_gravity(): + N = ReferenceFrame('N') + m, M, g = symbols('m M g') + F1, F2 = dynamicsymbols('F1 F2') + po = Point('po') + pa = Particle('pa', po, m) + A = ReferenceFrame('A') + P = Point('P') + I = outer(A.x, A.x) + B = RigidBody('B', P, A, M, (I, P)) + forceList = [(po, F1), (P, F2)] + forceList.extend(gravity(g * N.y, pa, B)) + l = [(po, F1), (P, F2), (po, g * m * N.y), (P, g * M * N.y)] + + for i in range(len(l)): + for j in range(len(l[i])): + assert forceList[i][j] == l[i][j] + + +def test_parse_loads(): + N = ReferenceFrame('N') + po = Point('po') + assert _parse_load(Force(po, N.z)) == (po, N.z) + assert _parse_load(Torque(N, N.x)) == (N, N.x) + f1 = _parse_load((po, N.x)) # Test whether a force is recognized + assert isinstance(f1, Force) + assert f1 == Force(po, N.x) + t1 = _parse_load((N, N.y)) # Test whether a torque is recognized + assert isinstance(t1, Torque) + assert t1 == Torque(N, N.y) + # Bodies should be undetermined (even in case of a Particle) + raises(ValueError, lambda: _parse_load((Particle('pa', po), N.x))) + raises(ValueError, lambda: _parse_load((RigidBody('pa', po, N), N.x))) + # Invalid tuple length + raises(ValueError, lambda: _parse_load((po, N.x, po, N.x))) + # Invalid type + raises(TypeError, lambda: _parse_load([po, N.x])) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_method.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_method.py new file mode 100644 index 0000000000000000000000000000000000000000..4a8fd5fb50c3178f5a5cdab1e80423df8b52f525 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_method.py @@ -0,0 +1,5 @@ +from sympy.physics.mechanics.method import _Methods +from sympy.testing.pytest import raises + +def test_method(): + raises(TypeError, lambda: _Methods()) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_models.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_models.py new file mode 100644 index 0000000000000000000000000000000000000000..2b3d3ae89b44d774ead1a3ea641a8274ba951638 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_models.py @@ -0,0 +1,117 @@ +import sympy.physics.mechanics.models as models +from sympy import (cos, sin, Matrix, symbols, zeros) +from sympy.simplify.simplify import simplify +from sympy.physics.mechanics import (dynamicsymbols) + + +def test_multi_mass_spring_damper_inputs(): + + c0, k0, m0 = symbols("c0 k0 m0") + g = symbols("g") + v0, x0, f0 = dynamicsymbols("v0 x0 f0") + + kane1 = models.multi_mass_spring_damper(1) + massmatrix1 = Matrix([[m0]]) + forcing1 = Matrix([[-c0*v0 - k0*x0]]) + assert simplify(massmatrix1 - kane1.mass_matrix) == Matrix([0]) + assert simplify(forcing1 - kane1.forcing) == Matrix([0]) + + kane2 = models.multi_mass_spring_damper(1, True) + massmatrix2 = Matrix([[m0]]) + forcing2 = Matrix([[-c0*v0 + g*m0 - k0*x0]]) + assert simplify(massmatrix2 - kane2.mass_matrix) == Matrix([0]) + assert simplify(forcing2 - kane2.forcing) == Matrix([0]) + + kane3 = models.multi_mass_spring_damper(1, True, True) + massmatrix3 = Matrix([[m0]]) + forcing3 = Matrix([[-c0*v0 + g*m0 - k0*x0 + f0]]) + assert simplify(massmatrix3 - kane3.mass_matrix) == Matrix([0]) + assert simplify(forcing3 - kane3.forcing) == Matrix([0]) + + kane4 = models.multi_mass_spring_damper(1, False, True) + massmatrix4 = Matrix([[m0]]) + forcing4 = Matrix([[-c0*v0 - k0*x0 + f0]]) + assert simplify(massmatrix4 - kane4.mass_matrix) == Matrix([0]) + assert simplify(forcing4 - kane4.forcing) == Matrix([0]) + + +def test_multi_mass_spring_damper_higher_order(): + c0, k0, m0 = symbols("c0 k0 m0") + c1, k1, m1 = symbols("c1 k1 m1") + c2, k2, m2 = symbols("c2 k2 m2") + v0, x0 = dynamicsymbols("v0 x0") + v1, x1 = dynamicsymbols("v1 x1") + v2, x2 = dynamicsymbols("v2 x2") + + kane1 = models.multi_mass_spring_damper(3) + massmatrix1 = Matrix([[m0 + m1 + m2, m1 + m2, m2], + [m1 + m2, m1 + m2, m2], + [m2, m2, m2]]) + forcing1 = Matrix([[-c0*v0 - k0*x0], + [-c1*v1 - k1*x1], + [-c2*v2 - k2*x2]]) + assert simplify(massmatrix1 - kane1.mass_matrix) == zeros(3) + assert simplify(forcing1 - kane1.forcing) == Matrix([0, 0, 0]) + + +def test_n_link_pendulum_on_cart_inputs(): + l0, m0 = symbols("l0 m0") + m1 = symbols("m1") + g = symbols("g") + q0, q1, F, T1 = dynamicsymbols("q0 q1 F T1") + u0, u1 = dynamicsymbols("u0 u1") + + kane1 = models.n_link_pendulum_on_cart(1) + massmatrix1 = Matrix([[m0 + m1, -l0*m1*cos(q1)], + [-l0*m1*cos(q1), l0**2*m1]]) + forcing1 = Matrix([[-l0*m1*u1**2*sin(q1) + F], [g*l0*m1*sin(q1)]]) + assert simplify(massmatrix1 - kane1.mass_matrix) == zeros(2) + assert simplify(forcing1 - kane1.forcing) == Matrix([0, 0]) + + kane2 = models.n_link_pendulum_on_cart(1, False) + massmatrix2 = Matrix([[m0 + m1, -l0*m1*cos(q1)], + [-l0*m1*cos(q1), l0**2*m1]]) + forcing2 = Matrix([[-l0*m1*u1**2*sin(q1)], [g*l0*m1*sin(q1)]]) + assert simplify(massmatrix2 - kane2.mass_matrix) == zeros(2) + assert simplify(forcing2 - kane2.forcing) == Matrix([0, 0]) + + kane3 = models.n_link_pendulum_on_cart(1, False, True) + massmatrix3 = Matrix([[m0 + m1, -l0*m1*cos(q1)], + [-l0*m1*cos(q1), l0**2*m1]]) + forcing3 = Matrix([[-l0*m1*u1**2*sin(q1)], [g*l0*m1*sin(q1) + T1]]) + assert simplify(massmatrix3 - kane3.mass_matrix) == zeros(2) + assert simplify(forcing3 - kane3.forcing) == Matrix([0, 0]) + + kane4 = models.n_link_pendulum_on_cart(1, True, False) + massmatrix4 = Matrix([[m0 + m1, -l0*m1*cos(q1)], + [-l0*m1*cos(q1), l0**2*m1]]) + forcing4 = Matrix([[-l0*m1*u1**2*sin(q1) + F], [g*l0*m1*sin(q1)]]) + assert simplify(massmatrix4 - kane4.mass_matrix) == zeros(2) + assert simplify(forcing4 - kane4.forcing) == Matrix([0, 0]) + + +def test_n_link_pendulum_on_cart_higher_order(): + l0, m0 = symbols("l0 m0") + l1, m1 = symbols("l1 m1") + m2 = symbols("m2") + g = symbols("g") + q0, q1, q2 = dynamicsymbols("q0 q1 q2") + u0, u1, u2 = dynamicsymbols("u0 u1 u2") + F, T1 = dynamicsymbols("F T1") + + kane1 = models.n_link_pendulum_on_cart(2) + massmatrix1 = Matrix([[m0 + m1 + m2, -l0*m1*cos(q1) - l0*m2*cos(q1), + -l1*m2*cos(q2)], + [-l0*m1*cos(q1) - l0*m2*cos(q1), l0**2*m1 + l0**2*m2, + l0*l1*m2*(sin(q1)*sin(q2) + cos(q1)*cos(q2))], + [-l1*m2*cos(q2), + l0*l1*m2*(sin(q1)*sin(q2) + cos(q1)*cos(q2)), + l1**2*m2]]) + forcing1 = Matrix([[-l0*m1*u1**2*sin(q1) - l0*m2*u1**2*sin(q1) - + l1*m2*u2**2*sin(q2) + F], + [g*l0*m1*sin(q1) + g*l0*m2*sin(q1) - + l0*l1*m2*(sin(q1)*cos(q2) - sin(q2)*cos(q1))*u2**2], + [g*l1*m2*sin(q2) - l0*l1*m2*(-sin(q1)*cos(q2) + + sin(q2)*cos(q1))*u1**2]]) + assert simplify(massmatrix1 - kane1.mass_matrix) == zeros(3) + assert simplify(forcing1 - kane1.forcing) == Matrix([0, 0, 0]) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_particle.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_particle.py new file mode 100644 index 0000000000000000000000000000000000000000..8eec80275b532055eacaf2339a276c0fd19b330a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_particle.py @@ -0,0 +1,78 @@ +from sympy import symbols +from sympy.physics.mechanics import Point, Particle, ReferenceFrame, inertia +from sympy.physics.mechanics.body_base import BodyBase +from sympy.testing.pytest import raises, warns_deprecated_sympy + + +def test_particle_default(): + # Test default + p = Particle('P') + assert p.name == 'P' + assert p.mass == symbols('P_mass') + assert p.masscenter.name == 'P_masscenter' + assert p.potential_energy == 0 + assert p.__str__() == 'P' + assert p.__repr__() == ("Particle('P', masscenter=P_masscenter, " + "mass=P_mass)") + raises(AttributeError, lambda: p.frame) + + +def test_particle(): + # Test initializing with parameters + m, m2, v1, v2, v3, r, g, h = symbols('m m2 v1 v2 v3 r g h') + P = Point('P') + P2 = Point('P2') + p = Particle('pa', P, m) + assert isinstance(p, BodyBase) + assert p.mass == m + assert p.point == P + # Test the mass setter + p.mass = m2 + assert p.mass == m2 + # Test the point setter + p.point = P2 + assert p.point == P2 + # Test the linear momentum function + N = ReferenceFrame('N') + O = Point('O') + P2.set_pos(O, r * N.y) + P2.set_vel(N, v1 * N.x) + raises(TypeError, lambda: Particle(P, P, m)) + raises(TypeError, lambda: Particle('pa', m, m)) + assert p.linear_momentum(N) == m2 * v1 * N.x + assert p.angular_momentum(O, N) == -m2 * r * v1 * N.z + P2.set_vel(N, v2 * N.y) + assert p.linear_momentum(N) == m2 * v2 * N.y + assert p.angular_momentum(O, N) == 0 + P2.set_vel(N, v3 * N.z) + assert p.linear_momentum(N) == m2 * v3 * N.z + assert p.angular_momentum(O, N) == m2 * r * v3 * N.x + P2.set_vel(N, v1 * N.x + v2 * N.y + v3 * N.z) + assert p.linear_momentum(N) == m2 * (v1 * N.x + v2 * N.y + v3 * N.z) + assert p.angular_momentum(O, N) == m2 * r * (v3 * N.x - v1 * N.z) + p.potential_energy = m * g * h + assert p.potential_energy == m * g * h + # TODO make the result not be system-dependent + assert p.kinetic_energy( + N) in [m2 * (v1 ** 2 + v2 ** 2 + v3 ** 2) / 2, + m2 * v1 ** 2 / 2 + m2 * v2 ** 2 / 2 + m2 * v3 ** 2 / 2] + + +def test_parallel_axis(): + N = ReferenceFrame('N') + m, a, b = symbols('m, a, b') + o = Point('o') + p = o.locatenew('p', a * N.x + b * N.y) + P = Particle('P', o, m) + Ip = P.parallel_axis(p, N) + Ip_expected = inertia(N, m * b ** 2, m * a ** 2, m * (a ** 2 + b ** 2), + ixy=-m * a * b) + assert Ip == Ip_expected + + +def test_deprecated_set_potential_energy(): + m, g, h = symbols('m g h') + P = Point('P') + p = Particle('pa', P, m) + with warns_deprecated_sympy(): + p.set_potential_energy(m * g * h) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_pathway.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_pathway.py new file mode 100644 index 0000000000000000000000000000000000000000..49dc4bd4d61300745833f9d32f3a91d9054c4839 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_pathway.py @@ -0,0 +1,691 @@ +"""Tests for the ``sympy.physics.mechanics.pathway.py`` module.""" + +import pytest + +from sympy import ( + Rational, + Symbol, + cos, + pi, + sin, + sqrt, +) +from sympy.physics.mechanics import ( + Force, + LinearPathway, + ObstacleSetPathway, + PathwayBase, + Point, + ReferenceFrame, + WrappingCylinder, + WrappingGeometryBase, + WrappingPathway, + WrappingSphere, + dynamicsymbols, +) +from sympy.simplify.simplify import simplify + + +def _simplify_loads(loads): + return [ + load.__class__(load.location, load.vector.simplify()) + for load in loads + ] + + +class TestLinearPathway: + + def test_is_pathway_base_subclass(self): + assert issubclass(LinearPathway, PathwayBase) + + @staticmethod + @pytest.mark.parametrize( + 'args, kwargs', + [ + ((Point('pA'), Point('pB')), {}), + ] + ) + def test_valid_constructor(args, kwargs): + pointA, pointB = args + instance = LinearPathway(*args, **kwargs) + assert isinstance(instance, LinearPathway) + assert hasattr(instance, 'attachments') + assert len(instance.attachments) == 2 + assert instance.attachments[0] is pointA + assert instance.attachments[1] is pointB + assert isinstance(instance.attachments[0], Point) + assert instance.attachments[0].name == 'pA' + assert isinstance(instance.attachments[1], Point) + assert instance.attachments[1].name == 'pB' + + @staticmethod + @pytest.mark.parametrize( + 'attachments', + [ + (Point('pA'), ), + (Point('pA'), Point('pB'), Point('pZ')), + ] + ) + def test_invalid_attachments_incorrect_number(attachments): + with pytest.raises(ValueError): + _ = LinearPathway(*attachments) + + @staticmethod + @pytest.mark.parametrize( + 'attachments', + [ + (None, Point('pB')), + (Point('pA'), None), + ] + ) + def test_invalid_attachments_not_point(attachments): + with pytest.raises(TypeError): + _ = LinearPathway(*attachments) + + @pytest.fixture(autouse=True) + def _linear_pathway_fixture(self): + self.N = ReferenceFrame('N') + self.pA = Point('pA') + self.pB = Point('pB') + self.pathway = LinearPathway(self.pA, self.pB) + self.q1 = dynamicsymbols('q1') + self.q2 = dynamicsymbols('q2') + self.q3 = dynamicsymbols('q3') + self.q1d = dynamicsymbols('q1', 1) + self.q2d = dynamicsymbols('q2', 1) + self.q3d = dynamicsymbols('q3', 1) + self.F = Symbol('F') + + def test_properties_are_immutable(self): + instance = LinearPathway(self.pA, self.pB) + with pytest.raises(AttributeError): + instance.attachments = None + with pytest.raises(TypeError): + instance.attachments[0] = None + with pytest.raises(TypeError): + instance.attachments[1] = None + + def test_repr(self): + pathway = LinearPathway(self.pA, self.pB) + expected = 'LinearPathway(pA, pB)' + assert repr(pathway) == expected + + def test_static_pathway_length(self): + self.pB.set_pos(self.pA, 2*self.N.x) + assert self.pathway.length == 2 + + def test_static_pathway_extension_velocity(self): + self.pB.set_pos(self.pA, 2*self.N.x) + assert self.pathway.extension_velocity == 0 + + def test_static_pathway_to_loads(self): + self.pB.set_pos(self.pA, 2*self.N.x) + expected = [ + (self.pA, - self.F*self.N.x), + (self.pB, self.F*self.N.x), + ] + assert self.pathway.to_loads(self.F) == expected + + def test_2D_pathway_length(self): + self.pB.set_pos(self.pA, 2*self.q1*self.N.x) + expected = 2*sqrt(self.q1**2) + assert self.pathway.length == expected + + def test_2D_pathway_extension_velocity(self): + self.pB.set_pos(self.pA, 2*self.q1*self.N.x) + expected = 2*sqrt(self.q1**2)*self.q1d/self.q1 + assert self.pathway.extension_velocity == expected + + def test_2D_pathway_to_loads(self): + self.pB.set_pos(self.pA, 2*self.q1*self.N.x) + expected = [ + (self.pA, - self.F*(self.q1 / sqrt(self.q1**2))*self.N.x), + (self.pB, self.F*(self.q1 / sqrt(self.q1**2))*self.N.x), + ] + assert self.pathway.to_loads(self.F) == expected + + def test_3D_pathway_length(self): + self.pB.set_pos( + self.pA, + self.q1*self.N.x - self.q2*self.N.y + 2*self.q3*self.N.z, + ) + expected = sqrt(self.q1**2 + self.q2**2 + 4*self.q3**2) + assert simplify(self.pathway.length - expected) == 0 + + def test_3D_pathway_extension_velocity(self): + self.pB.set_pos( + self.pA, + self.q1*self.N.x - self.q2*self.N.y + 2*self.q3*self.N.z, + ) + length = sqrt(self.q1**2 + self.q2**2 + 4*self.q3**2) + expected = ( + self.q1*self.q1d/length + + self.q2*self.q2d/length + + 4*self.q3*self.q3d/length + ) + assert simplify(self.pathway.extension_velocity - expected) == 0 + + def test_3D_pathway_to_loads(self): + self.pB.set_pos( + self.pA, + self.q1*self.N.x - self.q2*self.N.y + 2*self.q3*self.N.z, + ) + length = sqrt(self.q1**2 + self.q2**2 + 4*self.q3**2) + pO_force = ( + - self.F*self.q1*self.N.x/length + + self.F*self.q2*self.N.y/length + - 2*self.F*self.q3*self.N.z/length + ) + pI_force = ( + self.F*self.q1*self.N.x/length + - self.F*self.q2*self.N.y/length + + 2*self.F*self.q3*self.N.z/length + ) + expected = [ + (self.pA, pO_force), + (self.pB, pI_force), + ] + assert self.pathway.to_loads(self.F) == expected + + +class TestObstacleSetPathway: + + def test_is_pathway_base_subclass(self): + assert issubclass(ObstacleSetPathway, PathwayBase) + + @staticmethod + @pytest.mark.parametrize( + 'num_attachments, attachments', + [ + (3, [Point(name) for name in ('pO', 'pA', 'pI')]), + (4, [Point(name) for name in ('pO', 'pA', 'pB', 'pI')]), + (5, [Point(name) for name in ('pO', 'pA', 'pB', 'pC', 'pI')]), + (6, [Point(name) for name in ('pO', 'pA', 'pB', 'pC', 'pD', 'pI')]), + ] + ) + def test_valid_constructor(num_attachments, attachments): + instance = ObstacleSetPathway(*attachments) + assert isinstance(instance, ObstacleSetPathway) + assert hasattr(instance, 'attachments') + assert len(instance.attachments) == num_attachments + for attachment in instance.attachments: + assert isinstance(attachment, Point) + + @staticmethod + @pytest.mark.parametrize( + 'attachments', + [[Point('pO')], [Point('pO'), Point('pI')]], + ) + def test_invalid_constructor_attachments_incorrect_number(attachments): + with pytest.raises(ValueError): + _ = ObstacleSetPathway(*attachments) + + @staticmethod + @pytest.mark.parametrize( + 'attachments', + [ + (None, Point('pA'), Point('pI')), + (Point('pO'), None, Point('pI')), + (Point('pO'), Point('pA'), None), + ] + ) + def test_invalid_constructor_attachments_not_point(attachments): + with pytest.raises(TypeError): + _ = WrappingPathway(*attachments) # type: ignore + + def test_properties_are_immutable(self): + pathway = ObstacleSetPathway(Point('pO'), Point('pA'), Point('pI')) + with pytest.raises(AttributeError): + pathway.attachments = None # type: ignore + with pytest.raises(TypeError): + pathway.attachments[0] = None # type: ignore + with pytest.raises(TypeError): + pathway.attachments[1] = None # type: ignore + with pytest.raises(TypeError): + pathway.attachments[-1] = None # type: ignore + + @staticmethod + @pytest.mark.parametrize( + 'attachments, expected', + [ + ( + [Point(name) for name in ('pO', 'pA', 'pI')], + 'ObstacleSetPathway(pO, pA, pI)' + ), + ( + [Point(name) for name in ('pO', 'pA', 'pB', 'pI')], + 'ObstacleSetPathway(pO, pA, pB, pI)' + ), + ( + [Point(name) for name in ('pO', 'pA', 'pB', 'pC', 'pI')], + 'ObstacleSetPathway(pO, pA, pB, pC, pI)' + ), + ] + ) + def test_repr(attachments, expected): + pathway = ObstacleSetPathway(*attachments) + assert repr(pathway) == expected + + @pytest.fixture(autouse=True) + def _obstacle_set_pathway_fixture(self): + self.N = ReferenceFrame('N') + self.pO = Point('pO') + self.pI = Point('pI') + self.pA = Point('pA') + self.pB = Point('pB') + self.q = dynamicsymbols('q') + self.qd = dynamicsymbols('q', 1) + self.F = Symbol('F') + + def test_static_pathway_length(self): + self.pA.set_pos(self.pO, self.N.x) + self.pB.set_pos(self.pO, self.N.y) + self.pI.set_pos(self.pO, self.N.z) + pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI) + assert pathway.length == 1 + 2 * sqrt(2) + + def test_static_pathway_extension_velocity(self): + self.pA.set_pos(self.pO, self.N.x) + self.pB.set_pos(self.pO, self.N.y) + self.pI.set_pos(self.pO, self.N.z) + pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI) + assert pathway.extension_velocity == 0 + + def test_static_pathway_to_loads(self): + self.pA.set_pos(self.pO, self.N.x) + self.pB.set_pos(self.pO, self.N.y) + self.pI.set_pos(self.pO, self.N.z) + pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI) + expected = [ + Force(self.pO, -self.F * self.N.x), + Force(self.pA, self.F * self.N.x), + Force(self.pA, self.F * sqrt(2) / 2 * (self.N.x - self.N.y)), + Force(self.pB, self.F * sqrt(2) / 2 * (self.N.y - self.N.x)), + Force(self.pB, self.F * sqrt(2) / 2 * (self.N.y - self.N.z)), + Force(self.pI, self.F * sqrt(2) / 2 * (self.N.z - self.N.y)), + ] + assert pathway.to_loads(self.F) == expected + + def test_2D_pathway_length(self): + self.pA.set_pos(self.pO, -(self.N.x + self.N.y)) + self.pB.set_pos( + self.pO, cos(self.q) * self.N.x - (sin(self.q) + 1) * self.N.y + ) + self.pI.set_pos( + self.pO, sin(self.q) * self.N.x + (cos(self.q) - 1) * self.N.y + ) + pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI) + expected = 2 * sqrt(2) + sqrt(2 + 2*cos(self.q)) + assert (pathway.length - expected).simplify() == 0 + + def test_2D_pathway_extension_velocity(self): + self.pA.set_pos(self.pO, -(self.N.x + self.N.y)) + self.pB.set_pos( + self.pO, cos(self.q) * self.N.x - (sin(self.q) + 1) * self.N.y + ) + self.pI.set_pos( + self.pO, sin(self.q) * self.N.x + (cos(self.q) - 1) * self.N.y + ) + pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI) + expected = - (sqrt(2) * sin(self.q) * self.qd) / (2 * sqrt(cos(self.q) + 1)) + assert (pathway.extension_velocity - expected).simplify() == 0 + + def test_2D_pathway_to_loads(self): + self.pA.set_pos(self.pO, -(self.N.x + self.N.y)) + self.pB.set_pos( + self.pO, cos(self.q) * self.N.x - (sin(self.q) + 1) * self.N.y + ) + self.pI.set_pos( + self.pO, sin(self.q) * self.N.x + (cos(self.q) - 1) * self.N.y + ) + pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI) + pO_pA_force_vec = sqrt(2) / 2 * (self.N.x + self.N.y) + pA_pB_force_vec = ( + - sqrt(2 * cos(self.q) + 2) / 2 * self.N.x + + sqrt(2) * sin(self.q) / (2 * sqrt(cos(self.q) + 1)) * self.N.y + ) + pB_pI_force_vec = cos(self.q + pi/4) * self.N.x - sin(self.q + pi/4) * self.N.y + expected = [ + Force(self.pO, self.F * pO_pA_force_vec), + Force(self.pA, -self.F * pO_pA_force_vec), + Force(self.pA, self.F * pA_pB_force_vec), + Force(self.pB, -self.F * pA_pB_force_vec), + Force(self.pB, self.F * pB_pI_force_vec), + Force(self.pI, -self.F * pB_pI_force_vec), + ] + assert _simplify_loads(pathway.to_loads(self.F)) == expected + + +class TestWrappingPathway: + + def test_is_pathway_base_subclass(self): + assert issubclass(WrappingPathway, PathwayBase) + + @pytest.fixture(autouse=True) + def _wrapping_pathway_fixture(self): + self.pA = Point('pA') + self.pB = Point('pB') + self.r = Symbol('r', positive=True) + self.pO = Point('pO') + self.N = ReferenceFrame('N') + self.ax = self.N.z + self.sphere = WrappingSphere(self.r, self.pO) + self.cylinder = WrappingCylinder(self.r, self.pO, self.ax) + self.pathway = WrappingPathway(self.pA, self.pB, self.cylinder) + self.F = Symbol('F') + + def test_valid_constructor(self): + instance = WrappingPathway(self.pA, self.pB, self.cylinder) + assert isinstance(instance, WrappingPathway) + assert hasattr(instance, 'attachments') + assert len(instance.attachments) == 2 + assert isinstance(instance.attachments[0], Point) + assert instance.attachments[0] == self.pA + assert isinstance(instance.attachments[1], Point) + assert instance.attachments[1] == self.pB + assert hasattr(instance, 'geometry') + assert isinstance(instance.geometry, WrappingGeometryBase) + assert instance.geometry == self.cylinder + + @pytest.mark.parametrize( + 'attachments', + [ + (Point('pA'), ), + (Point('pA'), Point('pB'), Point('pZ')), + ] + ) + def test_invalid_constructor_attachments_incorrect_number(self, attachments): + with pytest.raises(TypeError): + _ = WrappingPathway(*attachments, self.cylinder) + + @staticmethod + @pytest.mark.parametrize( + 'attachments', + [ + (None, Point('pB')), + (Point('pA'), None), + ] + ) + def test_invalid_constructor_attachments_not_point(attachments): + with pytest.raises(TypeError): + _ = WrappingPathway(*attachments) + + def test_invalid_constructor_geometry_is_not_supplied(self): + with pytest.raises(TypeError): + _ = WrappingPathway(self.pA, self.pB) + + @pytest.mark.parametrize( + 'geometry', + [ + Symbol('r'), + dynamicsymbols('q'), + ReferenceFrame('N'), + ReferenceFrame('N').x, + ] + ) + def test_invalid_geometry_not_geometry(self, geometry): + with pytest.raises(TypeError): + _ = WrappingPathway(self.pA, self.pB, geometry) + + def test_attachments_property_is_immutable(self): + with pytest.raises(TypeError): + self.pathway.attachments[0] = self.pB + with pytest.raises(TypeError): + self.pathway.attachments[1] = self.pA + + def test_geometry_property_is_immutable(self): + with pytest.raises(AttributeError): + self.pathway.geometry = None + + def test_repr(self): + expected = ( + f'WrappingPathway(pA, pB, ' + f'geometry={self.cylinder!r})' + ) + assert repr(self.pathway) == expected + + @staticmethod + def _expand_pos_to_vec(pos, frame): + return sum(mag*unit for (mag, unit) in zip(pos, frame)) + + @pytest.mark.parametrize( + 'pA_vec, pB_vec, factor', + [ + ((1, 0, 0), (0, 1, 0), pi/2), + ((0, 1, 0), (sqrt(2)/2, -sqrt(2)/2, 0), 3*pi/4), + ((1, 0, 0), (Rational(1, 2), sqrt(3)/2, 0), pi/3), + ] + ) + def test_static_pathway_on_sphere_length(self, pA_vec, pB_vec, factor): + pA_vec = self._expand_pos_to_vec(pA_vec, self.N) + pB_vec = self._expand_pos_to_vec(pB_vec, self.N) + self.pA.set_pos(self.pO, self.r*pA_vec) + self.pB.set_pos(self.pO, self.r*pB_vec) + pathway = WrappingPathway(self.pA, self.pB, self.sphere) + expected = factor*self.r + assert simplify(pathway.length - expected) == 0 + + @pytest.mark.parametrize( + 'pA_vec, pB_vec, factor', + [ + ((1, 0, 0), (0, 1, 0), Rational(1, 2)*pi), + ((1, 0, 0), (-1, 0, 0), pi), + ((-1, 0, 0), (1, 0, 0), pi), + ((0, 1, 0), (sqrt(2)/2, -sqrt(2)/2, 0), 5*pi/4), + ((1, 0, 0), (Rational(1, 2), sqrt(3)/2, 0), pi/3), + ( + (0, 1, 0), + (sqrt(2)*Rational(1, 2), -sqrt(2)*Rational(1, 2), 1), + sqrt(1 + (Rational(5, 4)*pi)**2), + ), + ( + (1, 0, 0), + (Rational(1, 2), sqrt(3)*Rational(1, 2), 1), + sqrt(1 + (Rational(1, 3)*pi)**2), + ), + ] + ) + def test_static_pathway_on_cylinder_length(self, pA_vec, pB_vec, factor): + pA_vec = self._expand_pos_to_vec(pA_vec, self.N) + pB_vec = self._expand_pos_to_vec(pB_vec, self.N) + self.pA.set_pos(self.pO, self.r*pA_vec) + self.pB.set_pos(self.pO, self.r*pB_vec) + pathway = WrappingPathway(self.pA, self.pB, self.cylinder) + expected = factor*sqrt(self.r**2) + assert simplify(pathway.length - expected) == 0 + + @pytest.mark.parametrize( + 'pA_vec, pB_vec', + [ + ((1, 0, 0), (0, 1, 0)), + ((0, 1, 0), (sqrt(2)*Rational(1, 2), -sqrt(2)*Rational(1, 2), 0)), + ((1, 0, 0), (Rational(1, 2), sqrt(3)*Rational(1, 2), 0)), + ] + ) + def test_static_pathway_on_sphere_extension_velocity(self, pA_vec, pB_vec): + pA_vec = self._expand_pos_to_vec(pA_vec, self.N) + pB_vec = self._expand_pos_to_vec(pB_vec, self.N) + self.pA.set_pos(self.pO, self.r*pA_vec) + self.pB.set_pos(self.pO, self.r*pB_vec) + pathway = WrappingPathway(self.pA, self.pB, self.sphere) + assert pathway.extension_velocity == 0 + + @pytest.mark.parametrize( + 'pA_vec, pB_vec', + [ + ((1, 0, 0), (0, 1, 0)), + ((1, 0, 0), (-1, 0, 0)), + ((-1, 0, 0), (1, 0, 0)), + ((0, 1, 0), (sqrt(2)/2, -sqrt(2)/2, 0)), + ((1, 0, 0), (Rational(1, 2), sqrt(3)/2, 0)), + ((0, 1, 0), (sqrt(2)*Rational(1, 2), -sqrt(2)/2, 1)), + ((1, 0, 0), (Rational(1, 2), sqrt(3)/2, 1)), + ] + ) + def test_static_pathway_on_cylinder_extension_velocity(self, pA_vec, pB_vec): + pA_vec = self._expand_pos_to_vec(pA_vec, self.N) + pB_vec = self._expand_pos_to_vec(pB_vec, self.N) + self.pA.set_pos(self.pO, self.r*pA_vec) + self.pB.set_pos(self.pO, self.r*pB_vec) + pathway = WrappingPathway(self.pA, self.pB, self.cylinder) + assert pathway.extension_velocity == 0 + + @pytest.mark.parametrize( + 'pA_vec, pB_vec, pA_vec_expected, pB_vec_expected, pO_vec_expected', + ( + ((1, 0, 0), (0, 1, 0), (0, 1, 0), (1, 0, 0), (-1, -1, 0)), + ( + (0, 1, 0), + (sqrt(2)/2, -sqrt(2)/2, 0), + (1, 0, 0), + (sqrt(2)/2, sqrt(2)/2, 0), + (-1 - sqrt(2)/2, -sqrt(2)/2, 0) + ), + ( + (1, 0, 0), + (Rational(1, 2), sqrt(3)/2, 0), + (0, 1, 0), + (sqrt(3)/2, -Rational(1, 2), 0), + (-sqrt(3)/2, Rational(1, 2) - 1, 0), + ), + ) + ) + def test_static_pathway_on_sphere_to_loads( + self, + pA_vec, + pB_vec, + pA_vec_expected, + pB_vec_expected, + pO_vec_expected, + ): + pA_vec = self._expand_pos_to_vec(pA_vec, self.N) + pB_vec = self._expand_pos_to_vec(pB_vec, self.N) + self.pA.set_pos(self.pO, self.r*pA_vec) + self.pB.set_pos(self.pO, self.r*pB_vec) + pathway = WrappingPathway(self.pA, self.pB, self.sphere) + + pA_vec_expected = sum( + mag*unit for (mag, unit) in zip(pA_vec_expected, self.N) + ) + pB_vec_expected = sum( + mag*unit for (mag, unit) in zip(pB_vec_expected, self.N) + ) + pO_vec_expected = sum( + mag*unit for (mag, unit) in zip(pO_vec_expected, self.N) + ) + expected = [ + Force(self.pA, self.F*(self.r**3/sqrt(self.r**6))*pA_vec_expected), + Force(self.pB, self.F*(self.r**3/sqrt(self.r**6))*pB_vec_expected), + Force(self.pO, self.F*(self.r**3/sqrt(self.r**6))*pO_vec_expected), + ] + assert pathway.to_loads(self.F) == expected + + @pytest.mark.parametrize( + 'pA_vec, pB_vec, pA_vec_expected, pB_vec_expected, pO_vec_expected', + ( + ((1, 0, 0), (0, 1, 0), (0, 1, 0), (1, 0, 0), (-1, -1, 0)), + ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, 1, 0), (0, -2, 0)), + ((-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, -1, 0), (0, 2, 0)), + ( + (0, 1, 0), + (sqrt(2)/2, -sqrt(2)/2, 0), + (-1, 0, 0), + (-sqrt(2)/2, -sqrt(2)/2, 0), + (1 + sqrt(2)/2, sqrt(2)/2, 0) + ), + ( + (1, 0, 0), + (Rational(1, 2), sqrt(3)/2, 0), + (0, 1, 0), + (sqrt(3)/2, -Rational(1, 2), 0), + (-sqrt(3)/2, Rational(1, 2) - 1, 0), + ), + ( + (1, 0, 0), + (sqrt(2)/2, sqrt(2)/2, 0), + (0, 1, 0), + (sqrt(2)/2, -sqrt(2)/2, 0), + (-sqrt(2)/2, sqrt(2)/2 - 1, 0), + ), + ((0, 1, 0), (0, 1, 1), (0, 0, 1), (0, 0, -1), (0, 0, 0)), + ( + (0, 1, 0), + (sqrt(2)/2, -sqrt(2)/2, 1), + (-5*pi/sqrt(16 + 25*pi**2), 0, 4/sqrt(16 + 25*pi**2)), + ( + -5*sqrt(2)*pi/(2*sqrt(16 + 25*pi**2)), + -5*sqrt(2)*pi/(2*sqrt(16 + 25*pi**2)), + -4/sqrt(16 + 25*pi**2), + ), + ( + 5*(sqrt(2) + 2)*pi/(2*sqrt(16 + 25*pi**2)), + 5*sqrt(2)*pi/(2*sqrt(16 + 25*pi**2)), + 0, + ), + ), + ) + ) + def test_static_pathway_on_cylinder_to_loads( + self, + pA_vec, + pB_vec, + pA_vec_expected, + pB_vec_expected, + pO_vec_expected, + ): + pA_vec = self._expand_pos_to_vec(pA_vec, self.N) + pB_vec = self._expand_pos_to_vec(pB_vec, self.N) + self.pA.set_pos(self.pO, self.r*pA_vec) + self.pB.set_pos(self.pO, self.r*pB_vec) + pathway = WrappingPathway(self.pA, self.pB, self.cylinder) + + pA_force_expected = self.F*self._expand_pos_to_vec(pA_vec_expected, + self.N) + pB_force_expected = self.F*self._expand_pos_to_vec(pB_vec_expected, + self.N) + pO_force_expected = self.F*self._expand_pos_to_vec(pO_vec_expected, + self.N) + expected = [ + Force(self.pA, pA_force_expected), + Force(self.pB, pB_force_expected), + Force(self.pO, pO_force_expected), + ] + assert _simplify_loads(pathway.to_loads(self.F)) == expected + + def test_2D_pathway_on_cylinder_length(self): + q = dynamicsymbols('q') + pA_pos = self.r*self.N.x + pB_pos = self.r*(cos(q)*self.N.x + sin(q)*self.N.y) + self.pA.set_pos(self.pO, pA_pos) + self.pB.set_pos(self.pO, pB_pos) + expected = self.r*sqrt(q**2) + assert simplify(self.pathway.length - expected) == 0 + + def test_2D_pathway_on_cylinder_extension_velocity(self): + q = dynamicsymbols('q') + qd = dynamicsymbols('q', 1) + pA_pos = self.r*self.N.x + pB_pos = self.r*(cos(q)*self.N.x + sin(q)*self.N.y) + self.pA.set_pos(self.pO, pA_pos) + self.pB.set_pos(self.pO, pB_pos) + expected = self.r*(sqrt(q**2)/q)*qd + assert simplify(self.pathway.extension_velocity - expected) == 0 + + def test_2D_pathway_on_cylinder_to_loads(self): + q = dynamicsymbols('q') + pA_pos = self.r*self.N.x + pB_pos = self.r*(cos(q)*self.N.x + sin(q)*self.N.y) + self.pA.set_pos(self.pO, pA_pos) + self.pB.set_pos(self.pO, pB_pos) + + pA_force = self.F*self.N.y + pB_force = self.F*(sin(q)*self.N.x - cos(q)*self.N.y) + pO_force = self.F*(-sin(q)*self.N.x + (cos(q) - 1)*self.N.y) + expected = [ + Force(self.pA, pA_force), + Force(self.pB, pB_force), + Force(self.pO, pO_force), + ] + + loads = _simplify_loads(self.pathway.to_loads(self.F)) + assert loads == expected diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_rigidbody.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_rigidbody.py new file mode 100644 index 0000000000000000000000000000000000000000..78161e0c9fc33be6e3d274034b67278c8ceee8fd --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_rigidbody.py @@ -0,0 +1,184 @@ +from sympy.physics.mechanics import Point, ReferenceFrame, Dyadic, RigidBody +from sympy.physics.mechanics import dynamicsymbols, outer, inertia, Inertia +from sympy.physics.mechanics import inertia_of_point_mass +from sympy import expand, zeros, simplify, symbols +from sympy.testing.pytest import raises, warns_deprecated_sympy + + +def test_rigidbody_default(): + # Test default + b = RigidBody('B') + I = inertia(b.frame, *symbols('B_ixx B_iyy B_izz B_ixy B_iyz B_izx')) + assert b.name == 'B' + assert b.mass == symbols('B_mass') + assert b.masscenter.name == 'B_masscenter' + assert b.inertia == (I, b.masscenter) + assert b.central_inertia == I + assert b.frame.name == 'B_frame' + assert b.__str__() == 'B' + assert b.__repr__() == ( + "RigidBody('B', masscenter=B_masscenter, frame=B_frame, mass=B_mass, " + "inertia=Inertia(dyadic=B_ixx*(B_frame.x|B_frame.x) + " + "B_ixy*(B_frame.x|B_frame.y) + B_izx*(B_frame.x|B_frame.z) + " + "B_ixy*(B_frame.y|B_frame.x) + B_iyy*(B_frame.y|B_frame.y) + " + "B_iyz*(B_frame.y|B_frame.z) + B_izx*(B_frame.z|B_frame.x) + " + "B_iyz*(B_frame.z|B_frame.y) + B_izz*(B_frame.z|B_frame.z), " + "point=B_masscenter))") + + +def test_rigidbody(): + m, m2, v1, v2, v3, omega = symbols('m m2 v1 v2 v3 omega') + A = ReferenceFrame('A') + A2 = ReferenceFrame('A2') + P = Point('P') + P2 = Point('P2') + I = Dyadic(0) + I2 = Dyadic(0) + B = RigidBody('B', P, A, m, (I, P)) + assert B.mass == m + assert B.frame == A + assert B.masscenter == P + assert B.inertia == (I, B.masscenter) + + B.mass = m2 + B.frame = A2 + B.masscenter = P2 + B.inertia = (I2, B.masscenter) + raises(TypeError, lambda: RigidBody(P, P, A, m, (I, P))) + raises(TypeError, lambda: RigidBody('B', P, P, m, (I, P))) + raises(TypeError, lambda: RigidBody('B', P, A, m, (P, P))) + raises(TypeError, lambda: RigidBody('B', P, A, m, (I, I))) + assert B.__str__() == 'B' + assert B.mass == m2 + assert B.frame == A2 + assert B.masscenter == P2 + assert B.inertia == (I2, B.masscenter) + assert isinstance(B.inertia, Inertia) + + # Testing linear momentum function assuming A2 is the inertial frame + N = ReferenceFrame('N') + P2.set_vel(N, v1 * N.x + v2 * N.y + v3 * N.z) + assert B.linear_momentum(N) == m2 * (v1 * N.x + v2 * N.y + v3 * N.z) + + +def test_rigidbody2(): + M, v, r, omega, g, h = dynamicsymbols('M v r omega g h') + N = ReferenceFrame('N') + b = ReferenceFrame('b') + b.set_ang_vel(N, omega * b.x) + P = Point('P') + I = outer(b.x, b.x) + Inertia_tuple = (I, P) + B = RigidBody('B', P, b, M, Inertia_tuple) + P.set_vel(N, v * b.x) + assert B.angular_momentum(P, N) == omega * b.x + O = Point('O') + O.set_vel(N, v * b.x) + P.set_pos(O, r * b.y) + assert B.angular_momentum(O, N) == omega * b.x - M*v*r*b.z + B.potential_energy = M * g * h + assert B.potential_energy == M * g * h + assert expand(2 * B.kinetic_energy(N)) == omega**2 + M * v**2 + + +def test_rigidbody3(): + q1, q2, q3, q4 = dynamicsymbols('q1:5') + p1, p2, p3 = symbols('p1:4') + m = symbols('m') + + A = ReferenceFrame('A') + B = A.orientnew('B', 'axis', [q1, A.x]) + O = Point('O') + O.set_vel(A, q2*A.x + q3*A.y + q4*A.z) + P = O.locatenew('P', p1*B.x + p2*B.y + p3*B.z) + P.v2pt_theory(O, A, B) + I = outer(B.x, B.x) + + rb1 = RigidBody('rb1', P, B, m, (I, P)) + # I_S/O = I_S/S* + I_S*/O + rb2 = RigidBody('rb2', P, B, m, + (I + inertia_of_point_mass(m, P.pos_from(O), B), O)) + + assert rb1.central_inertia == rb2.central_inertia + assert rb1.angular_momentum(O, A) == rb2.angular_momentum(O, A) + + +def test_pendulum_angular_momentum(): + """Consider a pendulum of length OA = 2a, of mass m as a rigid body of + center of mass G (OG = a) which turn around (O,z). The angle between the + reference frame R and the rod is q. The inertia of the body is I = + (G,0,ma^2/3,ma^2/3). """ + + m, a = symbols('m, a') + q = dynamicsymbols('q') + + R = ReferenceFrame('R') + R1 = R.orientnew('R1', 'Axis', [q, R.z]) + R1.set_ang_vel(R, q.diff() * R.z) + + I = inertia(R1, 0, m * a**2 / 3, m * a**2 / 3) + + O = Point('O') + + A = O.locatenew('A', 2*a * R1.x) + G = O.locatenew('G', a * R1.x) + + S = RigidBody('S', G, R1, m, (I, G)) + + O.set_vel(R, 0) + A.v2pt_theory(O, R, R1) + G.v2pt_theory(O, R, R1) + + assert (4 * m * a**2 / 3 * q.diff() * R.z - + S.angular_momentum(O, R).express(R)) == 0 + + +def test_rigidbody_inertia(): + N = ReferenceFrame('N') + m, Ix, Iy, Iz, a, b = symbols('m, I_x, I_y, I_z, a, b') + Io = inertia(N, Ix, Iy, Iz) + o = Point('o') + p = o.locatenew('p', a * N.x + b * N.y) + R = RigidBody('R', o, N, m, (Io, p)) + I_check = inertia(N, Ix - b ** 2 * m, Iy - a ** 2 * m, + Iz - m * (a ** 2 + b ** 2), m * a * b) + assert isinstance(R.inertia, Inertia) + assert R.inertia == (Io, p) + assert R.central_inertia == I_check + R.central_inertia = Io + assert R.inertia == (Io, o) + assert R.central_inertia == Io + R.inertia = (Io, p) + assert R.inertia == (Io, p) + assert R.central_inertia == I_check + # parse Inertia object + R.inertia = Inertia(Io, o) + assert R.inertia == (Io, o) + + +def test_parallel_axis(): + N = ReferenceFrame('N') + m, Ix, Iy, Iz, a, b = symbols('m, I_x, I_y, I_z, a, b') + Io = inertia(N, Ix, Iy, Iz) + o = Point('o') + p = o.locatenew('p', a * N.x + b * N.y) + R = RigidBody('R', o, N, m, (Io, o)) + Ip = R.parallel_axis(p) + Ip_expected = inertia(N, Ix + m * b**2, Iy + m * a**2, + Iz + m * (a**2 + b**2), ixy=-m * a * b) + assert Ip == Ip_expected + # Reference frame from which the parallel axis is viewed should not matter + A = ReferenceFrame('A') + A.orient_axis(N, N.z, 1) + assert simplify( + (R.parallel_axis(p, A) - Ip_expected).to_matrix(A)) == zeros(3, 3) + + +def test_deprecated_set_potential_energy(): + m, g, h = symbols('m g h') + A = ReferenceFrame('A') + P = Point('P') + I = Dyadic(0) + B = RigidBody('B', P, A, m, (I, P)) + with warns_deprecated_sympy(): + B.set_potential_energy(m*g*h) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_system.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_system.py new file mode 100644 index 0000000000000000000000000000000000000000..6fdac1ea10e9f71f8cf999cc5069da7567f67adf --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_system.py @@ -0,0 +1,245 @@ +from sympy import symbols, Matrix, atan, zeros +from sympy.simplify.simplify import simplify +from sympy.physics.mechanics import (dynamicsymbols, Particle, Point, + ReferenceFrame, SymbolicSystem) +from sympy.testing.pytest import raises + +# This class is going to be tested using a simple pendulum set up in x and y +# coordinates +x, y, u, v, lam = dynamicsymbols('x y u v lambda') +m, l, g = symbols('m l g') + +# Set up the different forms the equations can take +# [1] Explicit form where the kinematics and dynamics are combined +# x' = F(x, t, r, p) +# +# [2] Implicit form where the kinematics and dynamics are combined +# M(x, p) x' = F(x, t, r, p) +# +# [3] Implicit form where the kinematics and dynamics are separate +# M(q, p) u' = F(q, u, t, r, p) +# q' = G(q, u, t, r, p) +dyn_implicit_mat = Matrix([[1, 0, -x/m], + [0, 1, -y/m], + [0, 0, l**2/m]]) + +dyn_implicit_rhs = Matrix([0, 0, u**2 + v**2 - g*y]) + +comb_implicit_mat = Matrix([[1, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, -x/m], + [0, 0, 0, 1, -y/m], + [0, 0, 0, 0, l**2/m]]) + +comb_implicit_rhs = Matrix([u, v, 0, 0, u**2 + v**2 - g*y]) + +kin_explicit_rhs = Matrix([u, v]) + +comb_explicit_rhs = comb_implicit_mat.LUsolve(comb_implicit_rhs) + +# Set up a body and load to pass into the system +theta = atan(x/y) +N = ReferenceFrame('N') +A = N.orientnew('A', 'Axis', [theta, N.z]) +O = Point('O') +P = O.locatenew('P', l * A.x) + +Pa = Particle('Pa', P, m) + +bodies = [Pa] +loads = [(P, g * m * N.x)] + +# Set up some output equations to be given to SymbolicSystem +# Change to make these fit the pendulum +PE = symbols("PE") +out_eqns = {PE: m*g*(l+y)} + +# Set up remaining arguments that can be passed to SymbolicSystem +alg_con = [2] +alg_con_full = [4] +coordinates = (x, y, lam) +speeds = (u, v) +states = (x, y, u, v, lam) +coord_idxs = (0, 1) +speed_idxs = (2, 3) + + +def test_form_1(): + symsystem1 = SymbolicSystem(states, comb_explicit_rhs, + alg_con=alg_con_full, output_eqns=out_eqns, + coord_idxs=coord_idxs, speed_idxs=speed_idxs, + bodies=bodies, loads=loads) + + assert symsystem1.coordinates == Matrix([x, y]) + assert symsystem1.speeds == Matrix([u, v]) + assert symsystem1.states == Matrix([x, y, u, v, lam]) + + assert symsystem1.alg_con == [4] + + inter = comb_explicit_rhs + assert simplify(symsystem1.comb_explicit_rhs - inter) == zeros(5, 1) + + assert set(symsystem1.dynamic_symbols()) == {y, v, lam, u, x} + assert type(symsystem1.dynamic_symbols()) == tuple + assert set(symsystem1.constant_symbols()) == {l, g, m} + assert type(symsystem1.constant_symbols()) == tuple + + assert symsystem1.output_eqns == out_eqns + + assert symsystem1.bodies == (Pa,) + assert symsystem1.loads == ((P, g * m * N.x),) + + +def test_form_2(): + symsystem2 = SymbolicSystem(coordinates, comb_implicit_rhs, speeds=speeds, + mass_matrix=comb_implicit_mat, + alg_con=alg_con_full, output_eqns=out_eqns, + bodies=bodies, loads=loads) + + assert symsystem2.coordinates == Matrix([x, y, lam]) + assert symsystem2.speeds == Matrix([u, v]) + assert symsystem2.states == Matrix([x, y, lam, u, v]) + + assert symsystem2.alg_con == [4] + + inter = comb_implicit_rhs + assert simplify(symsystem2.comb_implicit_rhs - inter) == zeros(5, 1) + assert simplify(symsystem2.comb_implicit_mat-comb_implicit_mat) == zeros(5) + + assert set(symsystem2.dynamic_symbols()) == {y, v, lam, u, x} + assert type(symsystem2.dynamic_symbols()) == tuple + assert set(symsystem2.constant_symbols()) == {l, g, m} + assert type(symsystem2.constant_symbols()) == tuple + + inter = comb_explicit_rhs + symsystem2.compute_explicit_form() + assert simplify(symsystem2.comb_explicit_rhs - inter) == zeros(5, 1) + + + assert symsystem2.output_eqns == out_eqns + + assert symsystem2.bodies == (Pa,) + assert symsystem2.loads == ((P, g * m * N.x),) + + +def test_form_3(): + symsystem3 = SymbolicSystem(states, dyn_implicit_rhs, + mass_matrix=dyn_implicit_mat, + coordinate_derivatives=kin_explicit_rhs, + alg_con=alg_con, coord_idxs=coord_idxs, + speed_idxs=speed_idxs, bodies=bodies, + loads=loads) + + assert symsystem3.coordinates == Matrix([x, y]) + assert symsystem3.speeds == Matrix([u, v]) + assert symsystem3.states == Matrix([x, y, u, v, lam]) + + assert symsystem3.alg_con == [4] + + inter1 = kin_explicit_rhs + inter2 = dyn_implicit_rhs + assert simplify(symsystem3.kin_explicit_rhs - inter1) == zeros(2, 1) + assert simplify(symsystem3.dyn_implicit_mat - dyn_implicit_mat) == zeros(3) + assert simplify(symsystem3.dyn_implicit_rhs - inter2) == zeros(3, 1) + + inter = comb_implicit_rhs + assert simplify(symsystem3.comb_implicit_rhs - inter) == zeros(5, 1) + assert simplify(symsystem3.comb_implicit_mat-comb_implicit_mat) == zeros(5) + + inter = comb_explicit_rhs + symsystem3.compute_explicit_form() + assert simplify(symsystem3.comb_explicit_rhs - inter) == zeros(5, 1) + + assert set(symsystem3.dynamic_symbols()) == {y, v, lam, u, x} + assert type(symsystem3.dynamic_symbols()) == tuple + assert set(symsystem3.constant_symbols()) == {l, g, m} + assert type(symsystem3.constant_symbols()) == tuple + + assert symsystem3.output_eqns == {} + + assert symsystem3.bodies == (Pa,) + assert symsystem3.loads == ((P, g * m * N.x),) + + +def test_property_attributes(): + symsystem = SymbolicSystem(states, comb_explicit_rhs, + alg_con=alg_con_full, output_eqns=out_eqns, + coord_idxs=coord_idxs, speed_idxs=speed_idxs, + bodies=bodies, loads=loads) + + with raises(AttributeError): + symsystem.bodies = 42 + with raises(AttributeError): + symsystem.coordinates = 42 + with raises(AttributeError): + symsystem.dyn_implicit_rhs = 42 + with raises(AttributeError): + symsystem.comb_implicit_rhs = 42 + with raises(AttributeError): + symsystem.loads = 42 + with raises(AttributeError): + symsystem.dyn_implicit_mat = 42 + with raises(AttributeError): + symsystem.comb_implicit_mat = 42 + with raises(AttributeError): + symsystem.kin_explicit_rhs = 42 + with raises(AttributeError): + symsystem.comb_explicit_rhs = 42 + with raises(AttributeError): + symsystem.speeds = 42 + with raises(AttributeError): + symsystem.states = 42 + with raises(AttributeError): + symsystem.alg_con = 42 + + +def test_not_specified_errors(): + """This test will cover errors that arise from trying to access attributes + that were not specified upon object creation or were specified on creation + and the user tries to recalculate them.""" + # Trying to access form 2 when form 1 given + # Trying to access form 3 when form 2 given + + symsystem1 = SymbolicSystem(states, comb_explicit_rhs) + + with raises(AttributeError): + symsystem1.comb_implicit_mat + with raises(AttributeError): + symsystem1.comb_implicit_rhs + with raises(AttributeError): + symsystem1.dyn_implicit_mat + with raises(AttributeError): + symsystem1.dyn_implicit_rhs + with raises(AttributeError): + symsystem1.kin_explicit_rhs + with raises(AttributeError): + symsystem1.compute_explicit_form() + + symsystem2 = SymbolicSystem(coordinates, comb_implicit_rhs, speeds=speeds, + mass_matrix=comb_implicit_mat) + + with raises(AttributeError): + symsystem2.dyn_implicit_mat + with raises(AttributeError): + symsystem2.dyn_implicit_rhs + with raises(AttributeError): + symsystem2.kin_explicit_rhs + + # Attribute error when trying to access coordinates and speeds when only the + # states were given. + with raises(AttributeError): + symsystem1.coordinates + with raises(AttributeError): + symsystem1.speeds + + # Attribute error when trying to access bodies and loads when they are not + # given + with raises(AttributeError): + symsystem1.bodies + with raises(AttributeError): + symsystem1.loads + + # Attribute error when trying to access comb_explicit_rhs before it was + # calculated + with raises(AttributeError): + symsystem2.comb_explicit_rhs diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_system_class.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_system_class.py new file mode 100644 index 0000000000000000000000000000000000000000..924cb8272c27c4f978aa4c3b1999f6ac56e47335 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_system_class.py @@ -0,0 +1,831 @@ +import pytest + +from sympy.core.symbol import symbols +from sympy.core.sympify import sympify +from sympy.functions.elementary.trigonometric import cos, sin +from sympy.matrices.dense import eye, zeros +from sympy.matrices.immutable import ImmutableMatrix +from sympy.physics.mechanics import ( + Force, KanesMethod, LagrangesMethod, Particle, PinJoint, Point, + PrismaticJoint, ReferenceFrame, RigidBody, Torque, TorqueActuator, System, + dynamicsymbols) +from sympy.simplify.simplify import simplify +from sympy.solvers.solvers import solve + +t = dynamicsymbols._t # type: ignore +q = dynamicsymbols('q:6') # type: ignore +qd = dynamicsymbols('q:6', 1) # type: ignore +u = dynamicsymbols('u:6') # type: ignore +ua = dynamicsymbols('ua:3') # type: ignore + + +class TestSystemBase: + @pytest.fixture() + def _empty_system_setup(self): + self.system = System(ReferenceFrame('frame'), Point('fixed_point')) + + def _empty_system_check(self, exclude=()): + matrices = ('q_ind', 'q_dep', 'q', 'u_ind', 'u_dep', 'u', 'u_aux', + 'kdes', 'holonomic_constraints', 'nonholonomic_constraints') + tuples = ('loads', 'bodies', 'joints', 'actuators') + for attr in matrices: + if attr not in exclude: + assert getattr(self.system, attr)[:] == [] + for attr in tuples: + if attr not in exclude: + assert getattr(self.system, attr) == () + if 'eom_method' not in exclude: + assert self.system.eom_method is None + + def _create_filled_system(self, with_speeds=True): + self.system = System(ReferenceFrame('frame'), Point('fixed_point')) + u = dynamicsymbols('u:6') if with_speeds else qd + self.bodies = symbols('rb1:5', cls=RigidBody) + self.joints = ( + PinJoint('J1', self.bodies[0], self.bodies[1], q[0], u[0]), + PrismaticJoint('J2', self.bodies[1], self.bodies[2], q[1], u[1]), + PinJoint('J3', self.bodies[2], self.bodies[3], q[2], u[2]) + ) + self.system.add_joints(*self.joints) + self.system.add_coordinates(q[3], independent=[False]) + self.system.add_speeds(u[3], independent=False) + if with_speeds: + self.system.add_kdes(u[3] - qd[3]) + self.system.add_auxiliary_speeds(ua[0], ua[1]) + self.system.add_holonomic_constraints(q[2] - q[0] + q[1]) + self.system.add_nonholonomic_constraints(u[3] - qd[1] + u[2]) + self.system.u_ind = u[:2] + self.system.u_dep = u[2:4] + self.q_ind, self.q_dep = self.system.q_ind[:], self.system.q_dep[:] + self.u_ind, self.u_dep = self.system.u_ind[:], self.system.u_dep[:] + self.kdes = self.system.kdes[:] + self.hc = self.system.holonomic_constraints[:] + self.vc = self.system.velocity_constraints[:] + self.nhc = self.system.nonholonomic_constraints[:] + + @pytest.fixture() + def _filled_system_setup(self): + self._create_filled_system(with_speeds=True) + + @pytest.fixture() + def _filled_system_setup_no_speeds(self): + self._create_filled_system(with_speeds=False) + + def _filled_system_check(self, exclude=()): + assert 'q_ind' in exclude or self.system.q_ind[:] == q[:3] + assert 'q_dep' in exclude or self.system.q_dep[:] == [q[3]] + assert 'q' in exclude or self.system.q[:] == q[:4] + assert 'u_ind' in exclude or self.system.u_ind[:] == u[:2] + assert 'u_dep' in exclude or self.system.u_dep[:] == u[2:4] + assert 'u' in exclude or self.system.u[:] == u[:4] + assert 'u_aux' in exclude or self.system.u_aux[:] == ua[:2] + assert 'kdes' in exclude or self.system.kdes[:] == [ + ui - qdi for ui, qdi in zip(u[:4], qd[:4])] + assert ('holonomic_constraints' in exclude or + self.system.holonomic_constraints[:] == [q[2] - q[0] + q[1]]) + assert ('nonholonomic_constraints' in exclude or + self.system.nonholonomic_constraints[:] == [u[3] - qd[1] + u[2]] + ) + assert ('velocity_constraints' in exclude or + self.system.velocity_constraints[:] == [ + qd[2] - qd[0] + qd[1], u[3] - qd[1] + u[2]]) + assert ('bodies' in exclude or + self.system.bodies == tuple(self.bodies)) + assert ('joints' in exclude or + self.system.joints == tuple(self.joints)) + + @pytest.fixture() + def _moving_point_mass(self, _empty_system_setup): + self.system.q_ind = q[0] + self.system.u_ind = u[0] + self.system.kdes = u[0] - q[0].diff(t) + p = Particle('p', mass=symbols('m')) + self.system.add_bodies(p) + p.masscenter.set_pos(self.system.fixed_point, q[0] * self.system.x) + + +class TestSystem(TestSystemBase): + def test_empty_system(self, _empty_system_setup): + self._empty_system_check() + self.system.validate_system() + + def test_filled_system(self, _filled_system_setup): + self._filled_system_check() + self.system.validate_system() + + @pytest.mark.parametrize('frame', [None, ReferenceFrame('frame')]) + @pytest.mark.parametrize('fixed_point', [None, Point('fixed_point')]) + def test_init(self, frame, fixed_point): + if fixed_point is None and frame is None: + self.system = System() + else: + self.system = System(frame, fixed_point) + if fixed_point is None: + assert self.system.fixed_point.name == 'inertial_point' + else: + assert self.system.fixed_point == fixed_point + if frame is None: + assert self.system.frame.name == 'inertial_frame' + else: + assert self.system.frame == frame + self._empty_system_check() + assert isinstance(self.system.q_ind, ImmutableMatrix) + assert isinstance(self.system.q_dep, ImmutableMatrix) + assert isinstance(self.system.q, ImmutableMatrix) + assert isinstance(self.system.u_ind, ImmutableMatrix) + assert isinstance(self.system.u_dep, ImmutableMatrix) + assert isinstance(self.system.u, ImmutableMatrix) + assert isinstance(self.system.kdes, ImmutableMatrix) + assert isinstance(self.system.holonomic_constraints, ImmutableMatrix) + assert isinstance(self.system.nonholonomic_constraints, ImmutableMatrix) + + def test_from_newtonian_rigid_body(self): + rb = RigidBody('body') + self.system = System.from_newtonian(rb) + assert self.system.fixed_point == rb.masscenter + assert self.system.frame == rb.frame + self._empty_system_check(exclude=('bodies',)) + self.system.bodies = (rb,) + + def test_from_newtonian_particle(self): + pt = Particle('particle') + with pytest.raises(TypeError): + System.from_newtonian(pt) + + @pytest.mark.parametrize('args, kwargs, exp_q_ind, exp_q_dep, exp_q', [ + (q[:3], {}, q[:3], [], q[:3]), + (q[:3], {'independent': True}, q[:3], [], q[:3]), + (q[:3], {'independent': False}, [], q[:3], q[:3]), + (q[:3], {'independent': [True, False, True]}, [q[0], q[2]], [q[1]], + [q[0], q[2], q[1]]), + ]) + def test_coordinates(self, _empty_system_setup, args, kwargs, + exp_q_ind, exp_q_dep, exp_q): + # Test add_coordinates + self.system.add_coordinates(*args, **kwargs) + assert self.system.q_ind[:] == exp_q_ind + assert self.system.q_dep[:] == exp_q_dep + assert self.system.q[:] == exp_q + self._empty_system_check(exclude=('q_ind', 'q_dep', 'q')) + # Test setter for q_ind and q_dep + self.system.q_ind = exp_q_ind + self.system.q_dep = exp_q_dep + assert self.system.q_ind[:] == exp_q_ind + assert self.system.q_dep[:] == exp_q_dep + assert self.system.q[:] == exp_q + self._empty_system_check(exclude=('q_ind', 'q_dep', 'q')) + + @pytest.mark.parametrize('func', ['add_coordinates', 'add_speeds']) + @pytest.mark.parametrize('args, kwargs', [ + ((q[0], q[5]), {}), + ((u[0], u[5]), {}), + ((q[0],), {'independent': False}), + ((u[0],), {'independent': False}), + ((u[0], q[5]), {}), + ((symbols('a'), q[5]), {}), + ]) + def test_coordinates_speeds_invalid(self, _filled_system_setup, func, args, + kwargs): + with pytest.raises(ValueError): + getattr(self.system, func)(*args, **kwargs) + self._filled_system_check() + + @pytest.mark.parametrize('args, kwargs, exp_u_ind, exp_u_dep, exp_u', [ + (u[:3], {}, u[:3], [], u[:3]), + (u[:3], {'independent': True}, u[:3], [], u[:3]), + (u[:3], {'independent': False}, [], u[:3], u[:3]), + (u[:3], {'independent': [True, False, True]}, [u[0], u[2]], [u[1]], + [u[0], u[2], u[1]]), + ]) + def test_speeds(self, _empty_system_setup, args, kwargs, exp_u_ind, + exp_u_dep, exp_u): + # Test add_speeds + self.system.add_speeds(*args, **kwargs) + assert self.system.u_ind[:] == exp_u_ind + assert self.system.u_dep[:] == exp_u_dep + assert self.system.u[:] == exp_u + self._empty_system_check(exclude=('u_ind', 'u_dep', 'u')) + # Test setter for u_ind and u_dep + self.system.u_ind = exp_u_ind + self.system.u_dep = exp_u_dep + assert self.system.u_ind[:] == exp_u_ind + assert self.system.u_dep[:] == exp_u_dep + assert self.system.u[:] == exp_u + self._empty_system_check(exclude=('u_ind', 'u_dep', 'u')) + + @pytest.mark.parametrize('args, kwargs, exp_u_aux', [ + (ua[:3], {}, ua[:3]), + ]) + def test_auxiliary_speeds(self, _empty_system_setup, args, kwargs, + exp_u_aux): + # Test add_speeds + self.system.add_auxiliary_speeds(*args, **kwargs) + assert self.system.u_aux[:] == exp_u_aux + self._empty_system_check(exclude=('u_aux',)) + # Test setter for u_ind and u_dep + self.system.u_aux = exp_u_aux + assert self.system.u_aux[:] == exp_u_aux + self._empty_system_check(exclude=('u_aux',)) + + @pytest.mark.parametrize('args, kwargs', [ + ((ua[2], q[0]), {}), + ((ua[2], u[1]), {}), + ((ua[0], ua[2]), {}), + ((symbols('a'), ua[2]), {}), + ]) + def test_auxiliary_invalid(self, _filled_system_setup, args, kwargs): + with pytest.raises(ValueError): + self.system.add_auxiliary_speeds(*args, **kwargs) + self._filled_system_check() + + @pytest.mark.parametrize('prop, add_func, args, kwargs', [ + ('q_ind', 'add_coordinates', (q[0],), {}), + ('q_dep', 'add_coordinates', (q[3],), {'independent': False}), + ('u_ind', 'add_speeds', (u[0],), {}), + ('u_dep', 'add_speeds', (u[3],), {'independent': False}), + ('u_aux', 'add_auxiliary_speeds', (ua[2],), {}), + ('kdes', 'add_kdes', (qd[0] - u[0],), {}), + ('holonomic_constraints', 'add_holonomic_constraints', + (q[0] - q[1],), {}), + ('nonholonomic_constraints', 'add_nonholonomic_constraints', + (u[0] - u[1],), {}), + ('bodies', 'add_bodies', (RigidBody('body'),), {}), + ('loads', 'add_loads', (Force(Point('P'), ReferenceFrame('N').x),), {}), + ('actuators', 'add_actuators', (TorqueActuator( + symbols('T'), ReferenceFrame('N').x, ReferenceFrame('A')),), {}), + ]) + def test_add_after_reset(self, _filled_system_setup, prop, add_func, args, + kwargs): + setattr(self.system, prop, ()) + exclude = (prop, 'q', 'u') + if prop in ('holonomic_constraints', 'nonholonomic_constraints'): + exclude += ('velocity_constraints',) + self._filled_system_check(exclude=exclude) + assert list(getattr(self.system, prop)[:]) == [] + getattr(self.system, add_func)(*args, **kwargs) + assert list(getattr(self.system, prop)[:]) == list(args) + + @pytest.mark.parametrize('prop, add_func, value, error', [ + ('q_ind', 'add_coordinates', symbols('a'), ValueError), + ('q_dep', 'add_coordinates', symbols('a'), ValueError), + ('u_ind', 'add_speeds', symbols('a'), ValueError), + ('u_dep', 'add_speeds', symbols('a'), ValueError), + ('u_aux', 'add_auxiliary_speeds', symbols('a'), ValueError), + ('kdes', 'add_kdes', 7, TypeError), + ('holonomic_constraints', 'add_holonomic_constraints', 7, TypeError), + ('nonholonomic_constraints', 'add_nonholonomic_constraints', 7, + TypeError), + ('bodies', 'add_bodies', symbols('a'), TypeError), + ('loads', 'add_loads', symbols('a'), TypeError), + ('actuators', 'add_actuators', symbols('a'), TypeError), + ]) + def test_type_error(self, _filled_system_setup, prop, add_func, value, + error): + with pytest.raises(error): + getattr(self.system, add_func)(value) + with pytest.raises(error): + setattr(self.system, prop, value) + self._filled_system_check() + + @pytest.mark.parametrize('args, kwargs, exp_kdes', [ + ((), {}, [ui - qdi for ui, qdi in zip(u[:4], qd[:4])]), + ((u[4] - qd[4], u[5] - qd[5]), {}, + [ui - qdi for ui, qdi in zip(u[:6], qd[:6])]), + ]) + def test_kdes(self, _filled_system_setup, args, kwargs, exp_kdes): + # Test add_speeds + self.system.add_kdes(*args, **kwargs) + self._filled_system_check(exclude=('kdes',)) + assert self.system.kdes[:] == exp_kdes + # Test setter for kdes + self.system.kdes = exp_kdes + self._filled_system_check(exclude=('kdes',)) + assert self.system.kdes[:] == exp_kdes + + @pytest.mark.parametrize('args, kwargs', [ + ((u[0] - qd[0], u[4] - qd[4]), {}), + ((-(u[0] - qd[0]), u[4] - qd[4]), {}), + (([u[0] - u[0], u[4] - qd[4]]), {}), + ]) + def test_kdes_invalid(self, _filled_system_setup, args, kwargs): + with pytest.raises(ValueError): + self.system.add_kdes(*args, **kwargs) + self._filled_system_check() + + @pytest.mark.parametrize('args, kwargs, exp_con', [ + ((), {}, [q[2] - q[0] + q[1]]), + ((q[4] - q[5], q[5] + q[3]), {}, + [q[2] - q[0] + q[1], q[4] - q[5], q[5] + q[3]]), + ]) + def test_holonomic_constraints(self, _filled_system_setup, args, kwargs, + exp_con): + exclude = ('holonomic_constraints', 'velocity_constraints') + exp_vel_con = [c.diff(t) for c in exp_con] + self.nhc + # Test add_holonomic_constraints + self.system.add_holonomic_constraints(*args, **kwargs) + self._filled_system_check(exclude=exclude) + assert self.system.holonomic_constraints[:] == exp_con + assert self.system.velocity_constraints[:] == exp_vel_con + # Test setter for holonomic_constraints + self.system.holonomic_constraints = exp_con + self._filled_system_check(exclude=exclude) + assert self.system.holonomic_constraints[:] == exp_con + assert self.system.velocity_constraints[:] == exp_vel_con + + @pytest.mark.parametrize('args, kwargs', [ + ((q[2] - q[0] + q[1], q[4] - q[3]), {}), + ((-(q[2] - q[0] + q[1]), q[4] - q[3]), {}), + ((q[0] - q[0], q[4] - q[3]), {}), + ]) + def test_holonomic_constraints_invalid(self, _filled_system_setup, args, + kwargs): + with pytest.raises(ValueError): + self.system.add_holonomic_constraints(*args, **kwargs) + self._filled_system_check() + + @pytest.mark.parametrize('args, kwargs, exp_con', [ + ((), {}, [u[3] - qd[1] + u[2]]), + ((u[4] - u[5], u[5] + u[3]), {}, + [u[3] - qd[1] + u[2], u[4] - u[5], u[5] + u[3]]), + ]) + def test_nonholonomic_constraints(self, _filled_system_setup, args, kwargs, + exp_con): + exclude = ('nonholonomic_constraints', 'velocity_constraints') + exp_vel_con = self.vc[:len(self.hc)] + exp_con + # Test add_nonholonomic_constraints + self.system.add_nonholonomic_constraints(*args, **kwargs) + self._filled_system_check(exclude=exclude) + assert self.system.nonholonomic_constraints[:] == exp_con + assert self.system.velocity_constraints[:] == exp_vel_con + # Test setter for nonholonomic_constraints + self.system.nonholonomic_constraints = exp_con + self._filled_system_check(exclude=exclude) + assert self.system.nonholonomic_constraints[:] == exp_con + assert self.system.velocity_constraints[:] == exp_vel_con + + @pytest.mark.parametrize('args, kwargs', [ + ((u[3] - qd[1] + u[2], u[4] - u[3]), {}), + ((-(u[3] - qd[1] + u[2]), u[4] - u[3]), {}), + ((u[0] - u[0], u[4] - u[3]), {}), + (([u[0] - u[0], u[4] - u[3]]), {}), + ]) + def test_nonholonomic_constraints_invalid(self, _filled_system_setup, args, + kwargs): + with pytest.raises(ValueError): + self.system.add_nonholonomic_constraints(*args, **kwargs) + self._filled_system_check() + + @pytest.mark.parametrize('constraints, expected', [ + ([], []), + (qd[2] - qd[0] + qd[1], [qd[2] - qd[0] + qd[1]]), + ([qd[2] + qd[1], u[2] - u[1]], [qd[2] + qd[1], u[2] - u[1]]), + ]) + def test_velocity_constraints_overwrite(self, _filled_system_setup, + constraints, expected): + self.system.velocity_constraints = constraints + self._filled_system_check(exclude=('velocity_constraints',)) + assert self.system.velocity_constraints[:] == expected + + def test_velocity_constraints_back_to_auto(self, _filled_system_setup): + self.system.velocity_constraints = qd[3] - qd[2] + self._filled_system_check(exclude=('velocity_constraints',)) + assert self.system.velocity_constraints[:] == [qd[3] - qd[2]] + self.system.velocity_constraints = None + self._filled_system_check() + + def test_bodies(self, _filled_system_setup): + rb1, rb2 = RigidBody('rb1'), RigidBody('rb2') + p1, p2 = Particle('p1'), Particle('p2') + self.system.add_bodies(rb1, p1) + assert self.system.bodies == (*self.bodies, rb1, p1) + self.system.add_bodies(p2) + assert self.system.bodies == (*self.bodies, rb1, p1, p2) + self.system.bodies = [] + assert self.system.bodies == () + self.system.bodies = p2 + assert self.system.bodies == (p2,) + symb = symbols('symb') + pytest.raises(TypeError, lambda: self.system.add_bodies(symb)) + pytest.raises(ValueError, lambda: self.system.add_bodies(p2)) + with pytest.raises(TypeError): + self.system.bodies = (rb1, rb2, p1, p2, symb) + assert self.system.bodies == (p2,) + + def test_add_loads(self): + system = System() + N, A = ReferenceFrame('N'), ReferenceFrame('A') + rb1 = RigidBody('rb1', frame=N) + mc1 = Point('mc1') + p1 = Particle('p1', mc1) + system.add_loads(Torque(rb1, N.x), (mc1, A.x), Force(p1, A.x)) + assert system.loads == ((N, N.x), (mc1, A.x), (mc1, A.x)) + system.loads = [(A, A.x)] + assert system.loads == ((A, A.x),) + pytest.raises(ValueError, lambda: system.add_loads((N, N.x, N.y))) + with pytest.raises(TypeError): + system.loads = (N, N.x) + assert system.loads == ((A, A.x),) + + def test_add_actuators(self): + system = System() + N, A = ReferenceFrame('N'), ReferenceFrame('A') + act1 = TorqueActuator(symbols('T1'), N.x, N) + act2 = TorqueActuator(symbols('T2'), N.y, N, A) + system.add_actuators(act1) + assert system.actuators == (act1,) + assert system.loads == () + system.actuators = (act2,) + assert system.actuators == (act2,) + + def test_add_joints(self): + q1, q2, q3, q4, u1, u2, u3 = dynamicsymbols('q1:5 u1:4') + rb1, rb2, rb3, rb4, rb5 = symbols('rb1:6', cls=RigidBody) + J1 = PinJoint('J1', rb1, rb2, q1, u1) + J2 = PrismaticJoint('J2', rb2, rb3, q2, u2) + J3 = PinJoint('J3', rb3, rb4, q3, u3) + J_lag = PinJoint('J_lag', rb4, rb5, q4, q4.diff(t)) + system = System() + system.add_joints(J1) + assert system.joints == (J1,) + assert system.bodies == (rb1, rb2) + assert system.q_ind == ImmutableMatrix([q1]) + assert system.u_ind == ImmutableMatrix([u1]) + assert system.kdes == ImmutableMatrix([u1 - q1.diff(t)]) + system.add_bodies(rb4) + system.add_coordinates(q3) + system.add_kdes(u3 - q3.diff(t)) + system.add_joints(J3) + assert system.joints == (J1, J3) + assert system.bodies == (rb1, rb2, rb4, rb3) + assert system.q_ind == ImmutableMatrix([q1, q3]) + assert system.u_ind == ImmutableMatrix([u1, u3]) + assert system.kdes == ImmutableMatrix( + [u1 - q1.diff(t), u3 - q3.diff(t)]) + system.add_kdes(-(u2 - q2.diff(t))) + system.add_joints(J2) + assert system.joints == (J1, J3, J2) + assert system.bodies == (rb1, rb2, rb4, rb3) + assert system.q_ind == ImmutableMatrix([q1, q3, q2]) + assert system.u_ind == ImmutableMatrix([u1, u3, u2]) + assert system.kdes == ImmutableMatrix([u1 - q1.diff(t), u3 - q3.diff(t), + -(u2 - q2.diff(t))]) + system.add_joints(J_lag) + assert system.joints == (J1, J3, J2, J_lag) + assert system.bodies == (rb1, rb2, rb4, rb3, rb5) + assert system.q_ind == ImmutableMatrix([q1, q3, q2, q4]) + assert system.u_ind == ImmutableMatrix([u1, u3, u2, q4.diff(t)]) + assert system.kdes == ImmutableMatrix([u1 - q1.diff(t), u3 - q3.diff(t), + -(u2 - q2.diff(t))]) + assert system.q_dep[:] == [] + assert system.u_dep[:] == [] + pytest.raises(ValueError, lambda: system.add_joints(J2)) + pytest.raises(TypeError, lambda: system.add_joints(rb1)) + + def test_joints_setter(self, _filled_system_setup): + self.system.joints = self.joints[1:] + assert self.system.joints == self.joints[1:] + self._filled_system_check(exclude=('joints',)) + self.system.q_ind = () + self.system.u_ind = () + self.system.joints = self.joints + self._filled_system_check() + + @pytest.mark.parametrize('name, joint_index', [ + ('J1', 0), + ('J2', 1), + ('not_existing', None), + ]) + def test_get_joint(self, _filled_system_setup, name, joint_index): + joint = self.system.get_joint(name) + if joint_index is None: + assert joint is None + else: + assert joint == self.joints[joint_index] + + @pytest.mark.parametrize('name, body_index', [ + ('rb1', 0), + ('rb3', 2), + ('not_existing', None), + ]) + def test_get_body(self, _filled_system_setup, name, body_index): + body = self.system.get_body(name) + if body_index is None: + assert body is None + else: + assert body == self.bodies[body_index] + + @pytest.mark.parametrize('eom_method', [KanesMethod, LagrangesMethod]) + def test_form_eoms_calls_subclass(self, _moving_point_mass, eom_method): + class MyMethod(eom_method): + pass + + self.system.form_eoms(eom_method=MyMethod) + assert isinstance(self.system.eom_method, MyMethod) + + @pytest.mark.parametrize('kwargs, expected', [ + ({}, ImmutableMatrix([[-1, 0], [0, symbols('m')]])), + ({'explicit_kinematics': True}, ImmutableMatrix([[1, 0], + [0, symbols('m')]])), + ]) + def test_system_kane_form_eoms_kwargs(self, _moving_point_mass, kwargs, + expected): + self.system.form_eoms(**kwargs) + assert self.system.mass_matrix_full == expected + + @pytest.mark.parametrize('kwargs, mm, gm', [ + ({}, ImmutableMatrix([[1, 0], [0, symbols('m')]]), + ImmutableMatrix([q[0].diff(t), 0])), + ]) + def test_system_lagrange_form_eoms_kwargs(self, _moving_point_mass, kwargs, + mm, gm): + self.system.form_eoms(eom_method=LagrangesMethod, **kwargs) + assert self.system.mass_matrix_full == mm + assert self.system.forcing_full == gm + + @pytest.mark.parametrize('eom_method, kwargs, error', [ + (KanesMethod, {'non_existing_kwarg': 1}, TypeError), + (LagrangesMethod, {'non_existing_kwarg': 1}, TypeError), + (KanesMethod, {'bodies': []}, ValueError), + (KanesMethod, {'kd_eqs': []}, ValueError), + (LagrangesMethod, {'bodies': []}, ValueError), + (LagrangesMethod, {'Lagrangian': 1}, ValueError), + ]) + def test_form_eoms_kwargs_errors(self, _empty_system_setup, eom_method, + kwargs, error): + self.system.q_ind = q[0] + p = Particle('p', mass=symbols('m')) + self.system.add_bodies(p) + p.masscenter.set_pos(self.system.fixed_point, q[0] * self.system.x) + with pytest.raises(error): + self.system.form_eoms(eom_method=eom_method, **kwargs) + + +class TestValidateSystem(TestSystemBase): + @pytest.mark.parametrize('valid_method, invalid_method, with_speeds', [ + (KanesMethod, LagrangesMethod, True), + (LagrangesMethod, KanesMethod, False) + ]) + def test_only_valid(self, valid_method, invalid_method, with_speeds): + self._create_filled_system(with_speeds=with_speeds) + self.system.validate_system(valid_method) + # Test Lagrange should fail due to the usage of generalized speeds + with pytest.raises(ValueError): + self.system.validate_system(invalid_method) + + @pytest.mark.parametrize('method, with_speeds', [ + (KanesMethod, True), (LagrangesMethod, False)]) + def test_missing_joint_coordinate(self, method, with_speeds): + self._create_filled_system(with_speeds=with_speeds) + self.system.q_ind = self.q_ind[1:] + self.system.u_ind = self.u_ind[:-1] + self.system.kdes = self.kdes[:-1] + pytest.raises(ValueError, lambda: self.system.validate_system(method)) + + def test_missing_joint_speed(self, _filled_system_setup): + self.system.q_ind = self.q_ind[:-1] + self.system.u_ind = self.u_ind[1:] + self.system.kdes = self.kdes[:-1] + pytest.raises(ValueError, lambda: self.system.validate_system()) + + def test_missing_joint_kdes(self, _filled_system_setup): + self.system.kdes = self.kdes[1:] + pytest.raises(ValueError, lambda: self.system.validate_system()) + + def test_negative_joint_kdes(self, _filled_system_setup): + self.system.kdes = [-self.kdes[0]] + self.kdes[1:] + self.system.validate_system() + + @pytest.mark.parametrize('method, with_speeds', [ + (KanesMethod, True), (LagrangesMethod, False)]) + def test_missing_holonomic_constraint(self, method, with_speeds): + self._create_filled_system(with_speeds=with_speeds) + self.system.holonomic_constraints = [] + self.system.nonholonomic_constraints = self.nhc + [ + self.u_ind[1] - self.u_dep[0] + self.u_ind[0]] + pytest.raises(ValueError, lambda: self.system.validate_system(method)) + self.system.q_dep = [] + self.system.q_ind = self.q_ind + self.q_dep + self.system.validate_system(method) + + def test_missing_nonholonomic_constraint(self, _filled_system_setup): + self.system.nonholonomic_constraints = [] + pytest.raises(ValueError, lambda: self.system.validate_system()) + self.system.u_dep = self.u_dep[1] + self.system.u_ind = self.u_ind + [self.u_dep[0]] + self.system.validate_system() + + def test_number_of_coordinates_speeds(self, _filled_system_setup): + # Test more speeds than coordinates + self.system.u_ind = self.u_ind + [u[5]] + self.system.kdes = self.kdes + [u[5] - qd[5]] + self.system.validate_system() + # Test more coordinates than speeds + self.system.q_ind = self.q_ind + self.system.u_ind = self.u_ind[:-1] + self.system.kdes = self.kdes[:-1] + pytest.raises(ValueError, lambda: self.system.validate_system()) + + def test_number_of_kdes(self, _filled_system_setup): + # Test wrong number of kdes + self.system.kdes = self.kdes[:-1] + pytest.raises(ValueError, lambda: self.system.validate_system()) + self.system.kdes = self.kdes + [u[2] + u[1] - qd[2]] + pytest.raises(ValueError, lambda: self.system.validate_system()) + + def test_duplicates(self, _filled_system_setup): + # This is basically a redundant feature, which should never fail + self.system.validate_system(check_duplicates=True) + + def test_speeds_in_lagrange(self, _filled_system_setup_no_speeds): + self.system.u_ind = u[:len(self.u_ind)] + with pytest.raises(ValueError): + self.system.validate_system(LagrangesMethod) + self.system.u_ind = [] + self.system.validate_system(LagrangesMethod) + self.system.u_aux = ua + with pytest.raises(ValueError): + self.system.validate_system(LagrangesMethod) + self.system.u_aux = [] + self.system.validate_system(LagrangesMethod) + self.system.add_joints( + PinJoint('Ju', RigidBody('rbu1'), RigidBody('rbu2'))) + self.system.u_ind = [] + with pytest.raises(ValueError): + self.system.validate_system(LagrangesMethod) + + +class TestSystemExamples: + def test_cart_pendulum_kanes(self): + # This example is the same as in the top documentation of System + # Added a spring to the cart + g, l, mc, mp, k = symbols('g l mc mp k') + F, qp, qc, up, uc = dynamicsymbols('F qp qc up uc') + rail = RigidBody('rail') + cart = RigidBody('cart', mass=mc) + bob = Particle('bob', mass=mp) + bob_frame = ReferenceFrame('bob_frame') + system = System.from_newtonian(rail) + assert system.bodies == (rail,) + assert system.frame == rail.frame + assert system.fixed_point == rail.masscenter + slider = PrismaticJoint('slider', rail, cart, qc, uc, joint_axis=rail.x) + pin = PinJoint('pin', cart, bob, qp, up, joint_axis=cart.z, + child_interframe=bob_frame, child_point=l * bob_frame.y) + system.add_joints(slider, pin) + assert system.joints == (slider, pin) + assert system.get_joint('slider') == slider + assert system.get_body('bob') == bob + system.apply_uniform_gravity(-g * system.y) + system.add_loads((cart.masscenter, F * rail.x)) + system.add_actuators(TorqueActuator(k * qp, cart.z, bob_frame, cart)) + system.validate_system() + system.form_eoms() + assert isinstance(system.eom_method, KanesMethod) + assert (simplify(system.mass_matrix - ImmutableMatrix( + [[mp + mc, mp * l * cos(qp)], [mp * l * cos(qp), mp * l ** 2]])) + == zeros(2, 2)) + assert (simplify(system.forcing - ImmutableMatrix([ + [mp * l * up ** 2 * sin(qp) + F], + [-mp * g * l * sin(qp) + k * qp]])) == zeros(2, 1)) + + system.add_holonomic_constraints( + sympify(bob.masscenter.pos_from(rail.masscenter).dot(system.x))) + assert system.eom_method is None + system.q_ind, system.q_dep = qp, qc + system.u_ind, system.u_dep = up, uc + system.validate_system() + + # Computed solution based on manually solving the constraints + subs = {qc: -l * sin(qp), + uc: -l * cos(qp) * up, + uc.diff(t): l * (up ** 2 * sin(qp) - up.diff(t) * cos(qp))} + upd_expected = ( + (-g * mp * sin(qp) + k * qp / l + l * mc * sin(2 * qp) * up ** 2 / 2 + - l * mp * sin(2 * qp) * up ** 2 / 2 - F * cos(qp)) / + (l * (mc * cos(qp) ** 2 + mp * sin(qp) ** 2))) + upd_sol = tuple(solve(system.form_eoms().xreplace(subs), + up.diff(t)).values())[0] + assert simplify(upd_sol - upd_expected) == 0 + assert isinstance(system.eom_method, KanesMethod) + + # Test other output + Mk = -ImmutableMatrix([[0, 1], [1, 0]]) + gk = -ImmutableMatrix([uc, up]) + Md = ImmutableMatrix([[-l ** 2 * mp * cos(qp) ** 2 + l ** 2 * mp, + l * mp * cos(qp) - l * (mc + mp) * cos(qp)], + [l * cos(qp), 1]]) + gd = ImmutableMatrix( + [[-g * l * mp * sin(qp) + k * qp - l ** 2 * mp * up ** 2 * sin(qp) * + cos(qp) - l * F * cos(qp)], [l * up ** 2 * sin(qp)]]) + Mm = (Mk.row_join(zeros(2, 2))).col_join(zeros(2, 2).row_join(Md)) + gm = gk.col_join(gd) + assert simplify(system.mass_matrix - Md) == zeros(2, 2) + assert simplify(system.forcing - gd) == zeros(2, 1) + assert simplify(system.mass_matrix_full - Mm) == zeros(4, 4) + assert simplify(system.forcing_full - gm) == zeros(4, 1) + + def test_cart_pendulum_lagrange(self): + # Lagrange version of test_cart_pendulus_kanes + # Added a spring to the cart + g, l, mc, mp, k = symbols('g l mc mp k') + F, qp, qc = dynamicsymbols('F qp qc') + qpd, qcd = dynamicsymbols('qp qc', 1) + rail = RigidBody('rail') + cart = RigidBody('cart', mass=mc) + bob = Particle('bob', mass=mp) + bob_frame = ReferenceFrame('bob_frame') + system = System.from_newtonian(rail) + assert system.bodies == (rail,) + assert system.frame == rail.frame + assert system.fixed_point == rail.masscenter + slider = PrismaticJoint('slider', rail, cart, qc, qcd, + joint_axis=rail.x) + pin = PinJoint('pin', cart, bob, qp, qpd, joint_axis=cart.z, + child_interframe=bob_frame, child_point=l * bob_frame.y) + system.add_joints(slider, pin) + assert system.joints == (slider, pin) + assert system.get_joint('slider') == slider + assert system.get_body('bob') == bob + for body in system.bodies: + body.potential_energy = body.mass * g * body.masscenter.pos_from( + system.fixed_point).dot(system.y) + system.add_loads((cart.masscenter, F * rail.x)) + system.add_actuators(TorqueActuator(k * qp, cart.z, bob_frame, cart)) + system.validate_system(LagrangesMethod) + system.form_eoms(LagrangesMethod) + assert (simplify(system.mass_matrix - ImmutableMatrix( + [[mp + mc, mp * l * cos(qp)], [mp * l * cos(qp), mp * l ** 2]])) + == zeros(2, 2)) + assert (simplify(system.forcing - ImmutableMatrix([ + [mp * l * qpd ** 2 * sin(qp) + F], [-mp * g * l * sin(qp) + k * qp]] + )) == zeros(2, 1)) + + system.add_holonomic_constraints( + sympify(bob.masscenter.pos_from(rail.masscenter).dot(system.x))) + assert system.eom_method is None + system.q_ind, system.q_dep = qp, qc + + # Computed solution based on manually solving the constraints + subs = {qc: -l * sin(qp), + qcd: -l * cos(qp) * qpd, + qcd.diff(t): l * (qpd ** 2 * sin(qp) - qpd.diff(t) * cos(qp))} + qpdd_expected = ( + (-g * mp * sin(qp) + k * qp / l + l * mc * sin(2 * qp) * qpd ** 2 / + 2 - l * mp * sin(2 * qp) * qpd ** 2 / 2 - F * cos(qp)) / + (l * (mc * cos(qp) ** 2 + mp * sin(qp) ** 2))) + eoms = system.form_eoms(LagrangesMethod) + lam1 = system.eom_method.lam_vec[0] + lam1_sol = system.eom_method.solve_multipliers()[lam1] + qpdd_sol = solve(eoms[0].xreplace({lam1: lam1_sol}).xreplace(subs), + qpd.diff(t))[0] + assert simplify(qpdd_sol - qpdd_expected) == 0 + assert isinstance(system.eom_method, LagrangesMethod) + + # Test other output + Md = ImmutableMatrix([[l ** 2 * mp, l * mp * cos(qp), -l * cos(qp)], + [l * mp * cos(qp), mc + mp, -1]]) + gd = ImmutableMatrix( + [[-g * l * mp * sin(qp) + k * qp], + [l * mp * sin(qp) * qpd ** 2 + F]]) + Mm = (eye(2).row_join(zeros(2, 3))).col_join(zeros(3, 2).row_join( + Md.col_join(ImmutableMatrix([l * cos(qp), 1, 0]).T))) + gm = ImmutableMatrix([qpd, qcd] + gd[:] + [l * sin(qp) * qpd ** 2]) + assert simplify(system.mass_matrix - Md) == zeros(2, 3) + assert simplify(system.forcing - gd) == zeros(2, 1) + assert simplify(system.mass_matrix_full - Mm) == zeros(5, 5) + assert simplify(system.forcing_full - gm) == zeros(5, 1) + + def test_box_on_ground(self): + # Particle sliding on ground with friction. The applied force is assumed + # to be positive and to be higher than the friction force. + g, m, mu = symbols('g m mu') + q, u, ua = dynamicsymbols('q u ua') + N, F = dynamicsymbols('N F', positive=True) + P = Particle("P", mass=m) + system = System() + system.add_bodies(P) + P.masscenter.set_pos(system.fixed_point, q * system.x) + P.masscenter.set_vel(system.frame, u * system.x + ua * system.y) + system.q_ind, system.u_ind, system.u_aux = [q], [u], [ua] + system.kdes = [q.diff(t) - u] + system.apply_uniform_gravity(-g * system.y) + system.add_loads( + Force(P, N * system.y), + Force(P, F * system.x - mu * N * system.x)) + system.validate_system() + system.form_eoms() + + # Test other output + Mk = ImmutableMatrix([1]) + gk = ImmutableMatrix([u]) + Md = ImmutableMatrix([m]) + gd = ImmutableMatrix([F - mu * N]) + Mm = (Mk.row_join(zeros(1, 1))).col_join(zeros(1, 1).row_join(Md)) + gm = gk.col_join(gd) + aux_eqs = ImmutableMatrix([N - m * g]) + assert simplify(system.mass_matrix - Md) == zeros(1, 1) + assert simplify(system.forcing - gd) == zeros(1, 1) + assert simplify(system.mass_matrix_full - Mm) == zeros(2, 2) + assert simplify(system.forcing_full - gm) == zeros(2, 1) + assert simplify(system.eom_method.auxiliary_eqs - aux_eqs + ) == zeros(1, 1) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_wrapping_geometry.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_wrapping_geometry.py new file mode 100644 index 0000000000000000000000000000000000000000..30c3ae71db5da75238ebb3d4cc53e11a29a72e5d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/tests/test_wrapping_geometry.py @@ -0,0 +1,363 @@ +"""Tests for the ``sympy.physics.mechanics.wrapping_geometry.py`` module.""" + +import pytest + +from sympy import ( + Integer, + Rational, + S, + Symbol, + acos, + cos, + pi, + sin, + sqrt, +) +from sympy.core.relational import Eq +from sympy.physics.mechanics import ( + Point, + ReferenceFrame, + WrappingCylinder, + WrappingSphere, + dynamicsymbols, +) +from sympy.simplify.simplify import simplify + + +r = Symbol('r', positive=True) +x = Symbol('x') +q = dynamicsymbols('q') +N = ReferenceFrame('N') + + +class TestWrappingSphere: + + @staticmethod + def test_valid_constructor(): + r = Symbol('r', positive=True) + pO = Point('pO') + sphere = WrappingSphere(r, pO) + assert isinstance(sphere, WrappingSphere) + assert hasattr(sphere, 'radius') + assert sphere.radius == r + assert hasattr(sphere, 'point') + assert sphere.point == pO + + @staticmethod + @pytest.mark.parametrize('position', [S.Zero, Integer(2)*r*N.x]) + def test_geodesic_length_point_not_on_surface_invalid(position): + r = Symbol('r', positive=True) + pO = Point('pO') + sphere = WrappingSphere(r, pO) + + p1 = Point('p1') + p1.set_pos(pO, position) + p2 = Point('p2') + p2.set_pos(pO, position) + + error_msg = r'point .* does not lie on the surface of' + with pytest.raises(ValueError, match=error_msg): + sphere.geodesic_length(p1, p2) + + @staticmethod + @pytest.mark.parametrize( + 'position_1, position_2, expected', + [ + (r*N.x, r*N.x, S.Zero), + (r*N.x, r*N.y, S.Half*pi*r), + (r*N.x, r*-N.x, pi*r), + (r*-N.x, r*N.x, pi*r), + (r*N.x, r*sqrt(2)*S.Half*(N.x + N.y), Rational(1, 4)*pi*r), + ( + r*sqrt(2)*S.Half*(N.x + N.y), + r*sqrt(3)*Rational(1, 3)*(N.x + N.y + N.z), + r*acos(sqrt(6)*Rational(1, 3)), + ), + ] + ) + def test_geodesic_length(position_1, position_2, expected): + r = Symbol('r', positive=True) + pO = Point('pO') + sphere = WrappingSphere(r, pO) + + p1 = Point('p1') + p1.set_pos(pO, position_1) + p2 = Point('p2') + p2.set_pos(pO, position_2) + + assert simplify(Eq(sphere.geodesic_length(p1, p2), expected)) + + @staticmethod + @pytest.mark.parametrize( + 'position_1, position_2, vector_1, vector_2', + [ + (r * N.x, r * N.y, N.y, N.x), + (r * N.x, -r * N.y, -N.y, N.x), + ( + r * N.y, + sqrt(2)/2 * r * N.x - sqrt(2)/2 * r * N.y, + N.x, + sqrt(2)/2 * N.x + sqrt(2)/2 * N.y, + ), + ( + r * N.x, + r / 2 * N.x + sqrt(3)/2 * r * N.y, + N.y, + sqrt(3)/2 * N.x - 1/2 * N.y, + ), + ( + r * N.x, + sqrt(2)/2 * r * N.x + sqrt(2)/2 * r * N.y, + N.y, + sqrt(2)/2 * N.x - sqrt(2)/2 * N.y, + ), + ] + ) + def test_geodesic_end_vectors(position_1, position_2, vector_1, vector_2): + r = Symbol('r', positive=True) + pO = Point('pO') + sphere = WrappingSphere(r, pO) + + p1 = Point('p1') + p1.set_pos(pO, position_1) + p2 = Point('p2') + p2.set_pos(pO, position_2) + + expected = (vector_1, vector_2) + + assert sphere.geodesic_end_vectors(p1, p2) == expected + + @staticmethod + @pytest.mark.parametrize( + 'position', + [r * N.x, r * cos(q) * N.x + r * sin(q) * N.y] + ) + def test_geodesic_end_vectors_invalid_coincident(position): + r = Symbol('r', positive=True) + pO = Point('pO') + sphere = WrappingSphere(r, pO) + + p1 = Point('p1') + p1.set_pos(pO, position) + p2 = Point('p2') + p2.set_pos(pO, position) + + with pytest.raises(ValueError): + _ = sphere.geodesic_end_vectors(p1, p2) + + @staticmethod + @pytest.mark.parametrize( + 'position_1, position_2', + [ + (r * N.x, -r * N.x), + (-r * N.y, r * N.y), + ( + r * cos(q) * N.x + r * sin(q) * N.y, + -r * cos(q) * N.x - r * sin(q) * N.y, + ) + ] + ) + def test_geodesic_end_vectors_invalid_diametrically_opposite( + position_1, + position_2, + ): + r = Symbol('r', positive=True) + pO = Point('pO') + sphere = WrappingSphere(r, pO) + + p1 = Point('p1') + p1.set_pos(pO, position_1) + p2 = Point('p2') + p2.set_pos(pO, position_2) + + with pytest.raises(ValueError): + _ = sphere.geodesic_end_vectors(p1, p2) + + +class TestWrappingCylinder: + + @staticmethod + def test_valid_constructor(): + N = ReferenceFrame('N') + r = Symbol('r', positive=True) + pO = Point('pO') + cylinder = WrappingCylinder(r, pO, N.x) + assert isinstance(cylinder, WrappingCylinder) + assert hasattr(cylinder, 'radius') + assert cylinder.radius == r + assert hasattr(cylinder, 'point') + assert cylinder.point == pO + assert hasattr(cylinder, 'axis') + assert cylinder.axis == N.x + + @staticmethod + @pytest.mark.parametrize( + 'position, expected', + [ + (S.Zero, False), + (r*N.y, True), + (r*N.z, True), + (r*(N.y + N.z).normalize(), True), + (Integer(2)*r*N.y, False), + (r*(N.x + N.y), True), + (r*(Integer(2)*N.x + N.y), True), + (Integer(2)*N.x + r*(Integer(2)*N.y + N.z).normalize(), True), + (r*(cos(q)*N.y + sin(q)*N.z), True) + ] + ) + def test_point_is_on_surface(position, expected): + r = Symbol('r', positive=True) + pO = Point('pO') + cylinder = WrappingCylinder(r, pO, N.x) + + p1 = Point('p1') + p1.set_pos(pO, position) + + assert cylinder.point_on_surface(p1) is expected + + @staticmethod + @pytest.mark.parametrize('position', [S.Zero, Integer(2)*r*N.y]) + def test_geodesic_length_point_not_on_surface_invalid(position): + r = Symbol('r', positive=True) + pO = Point('pO') + cylinder = WrappingCylinder(r, pO, N.x) + + p1 = Point('p1') + p1.set_pos(pO, position) + p2 = Point('p2') + p2.set_pos(pO, position) + + error_msg = r'point .* does not lie on the surface of' + with pytest.raises(ValueError, match=error_msg): + cylinder.geodesic_length(p1, p2) + + @staticmethod + @pytest.mark.parametrize( + 'axis, position_1, position_2, expected', + [ + (N.x, r*N.y, r*N.y, S.Zero), + (N.x, r*N.y, N.x + r*N.y, S.One), + (N.x, r*N.y, -x*N.x + r*N.y, sqrt(x**2)), + (-N.x, r*N.y, x*N.x + r*N.y, sqrt(x**2)), + (N.x, r*N.y, r*N.z, S.Half*pi*sqrt(r**2)), + (-N.x, r*N.y, r*N.z, Integer(3)*S.Half*pi*sqrt(r**2)), + (N.x, r*N.z, r*N.y, Integer(3)*S.Half*pi*sqrt(r**2)), + (-N.x, r*N.z, r*N.y, S.Half*pi*sqrt(r**2)), + (N.x, r*N.y, r*(cos(q)*N.y + sin(q)*N.z), sqrt(r**2*q**2)), + ( + -N.x, r*N.y, + r*(cos(q)*N.y + sin(q)*N.z), + sqrt(r**2*(Integer(2)*pi - q)**2), + ), + ] + ) + def test_geodesic_length(axis, position_1, position_2, expected): + r = Symbol('r', positive=True) + pO = Point('pO') + cylinder = WrappingCylinder(r, pO, axis) + + p1 = Point('p1') + p1.set_pos(pO, position_1) + p2 = Point('p2') + p2.set_pos(pO, position_2) + + assert simplify(Eq(cylinder.geodesic_length(p1, p2), expected)) + + @staticmethod + @pytest.mark.parametrize( + 'axis, position_1, position_2, vector_1, vector_2', + [ + (N.z, r * N.x, r * N.y, N.y, N.x), + (N.z, r * N.x, -r * N.x, N.y, N.y), + (N.z, -r * N.x, r * N.x, -N.y, -N.y), + (-N.z, r * N.x, -r * N.x, -N.y, -N.y), + (-N.z, -r * N.x, r * N.x, N.y, N.y), + (N.z, r * N.x, -r * N.y, N.y, -N.x), + ( + N.z, + r * N.y, + sqrt(2)/2 * r * N.x - sqrt(2)/2 * r * N.y, + - N.x, + - sqrt(2)/2 * N.x - sqrt(2)/2 * N.y, + ), + ( + N.z, + r * N.x, + r / 2 * N.x + sqrt(3)/2 * r * N.y, + N.y, + sqrt(3)/2 * N.x - 1/2 * N.y, + ), + ( + N.z, + r * N.x, + sqrt(2)/2 * r * N.x + sqrt(2)/2 * r * N.y, + N.y, + sqrt(2)/2 * N.x - sqrt(2)/2 * N.y, + ), + ( + N.z, + r * N.x, + r * N.x + N.z, + N.z, + -N.z, + ), + ( + N.z, + r * N.x, + r * N.y + pi/2 * r * N.z, + sqrt(2)/2 * N.y + sqrt(2)/2 * N.z, + sqrt(2)/2 * N.x - sqrt(2)/2 * N.z, + ), + ( + N.z, + r * N.x, + r * cos(q) * N.x + r * sin(q) * N.y, + N.y, + sin(q) * N.x - cos(q) * N.y, + ), + ] + ) + def test_geodesic_end_vectors( + axis, + position_1, + position_2, + vector_1, + vector_2, + ): + r = Symbol('r', positive=True) + pO = Point('pO') + cylinder = WrappingCylinder(r, pO, axis) + + p1 = Point('p1') + p1.set_pos(pO, position_1) + p2 = Point('p2') + p2.set_pos(pO, position_2) + + expected = (vector_1, vector_2) + end_vectors = tuple( + end_vector.simplify() + for end_vector in cylinder.geodesic_end_vectors(p1, p2) + ) + + assert end_vectors == expected + + @staticmethod + @pytest.mark.parametrize( + 'axis, position', + [ + (N.z, r * N.x), + (N.z, r * cos(q) * N.x + r * sin(q) * N.y + N.z), + ] + ) + def test_geodesic_end_vectors_invalid_coincident(axis, position): + r = Symbol('r', positive=True) + pO = Point('pO') + cylinder = WrappingCylinder(r, pO, axis) + + p1 = Point('p1') + p1.set_pos(pO, position) + p2 = Point('p2') + p2.set_pos(pO, position) + + with pytest.raises(ValueError): + _ = cylinder.geodesic_end_vectors(p1, p2) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/mechanics/wrapping_geometry.py b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/wrapping_geometry.py new file mode 100644 index 0000000000000000000000000000000000000000..47ed3c1c463499b024afb9e31cfa2ecd77534132 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/mechanics/wrapping_geometry.py @@ -0,0 +1,641 @@ +"""Geometry objects for use by wrapping pathways.""" + +from abc import ABC, abstractmethod + +from sympy import Integer, acos, pi, sqrt, sympify, tan +from sympy.core.relational import Eq +from sympy.functions.elementary.trigonometric import atan2 +from sympy.polys.polytools import cancel +from sympy.physics.vector import Vector, dot +from sympy.simplify.simplify import trigsimp + + +__all__ = [ + 'WrappingGeometryBase', + 'WrappingCylinder', + 'WrappingSphere', +] + + +class WrappingGeometryBase(ABC): + """Abstract base class for all geometry classes to inherit from. + + Notes + ===== + + Instances of this class cannot be directly instantiated by users. However, + it can be used to created custom geometry types through subclassing. + + """ + + @property + @abstractmethod + def point(cls): + """The point with which the geometry is associated.""" + pass + + @abstractmethod + def point_on_surface(self, point): + """Returns ``True`` if a point is on the geometry's surface. + + Parameters + ========== + point : Point + The point for which it's to be ascertained if it's on the + geometry's surface or not. + + """ + pass + + @abstractmethod + def geodesic_length(self, point_1, point_2): + """Returns the shortest distance between two points on a geometry's + surface. + + Parameters + ========== + + point_1 : Point + The point from which the geodesic length should be calculated. + point_2 : Point + The point to which the geodesic length should be calculated. + + """ + pass + + @abstractmethod + def geodesic_end_vectors(self, point_1, point_2): + """The vectors parallel to the geodesic at the two end points. + + Parameters + ========== + + point_1 : Point + The point from which the geodesic originates. + point_2 : Point + The point at which the geodesic terminates. + + """ + pass + + def __repr__(self): + """Default representation of a geometry object.""" + return f'{self.__class__.__name__}()' + + +class WrappingSphere(WrappingGeometryBase): + """A solid spherical object. + + Explanation + =========== + + A wrapping geometry that allows for circular arcs to be defined between + pairs of points. These paths are always geodetic (the shortest possible). + + Examples + ======== + + To create a ``WrappingSphere`` instance, a ``Symbol`` denoting its radius + and ``Point`` at which its center will be located are needed: + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import Point, WrappingSphere + >>> r = symbols('r') + >>> pO = Point('pO') + + A sphere with radius ``r`` centered on ``pO`` can be instantiated with: + + >>> WrappingSphere(r, pO) + WrappingSphere(radius=r, point=pO) + + Parameters + ========== + + radius : Symbol + Radius of the sphere. This symbol must represent a value that is + positive and constant, i.e. it cannot be a dynamic symbol, nor can it + be an expression. + point : Point + A point at which the sphere is centered. + + See Also + ======== + + WrappingCylinder: Cylindrical geometry where the wrapping direction can be + defined. + + """ + + def __init__(self, radius, point): + """Initializer for ``WrappingSphere``. + + Parameters + ========== + + radius : Symbol + The radius of the sphere. + point : Point + A point on which the sphere is centered. + + """ + self.radius = radius + self.point = point + + @property + def radius(self): + """Radius of the sphere.""" + return self._radius + + @radius.setter + def radius(self, radius): + self._radius = radius + + @property + def point(self): + """A point on which the sphere is centered.""" + return self._point + + @point.setter + def point(self, point): + self._point = point + + def point_on_surface(self, point): + """Returns ``True`` if a point is on the sphere's surface. + + Parameters + ========== + + point : Point + The point for which it's to be ascertained if it's on the sphere's + surface or not. This point's position relative to the sphere's + center must be a simple expression involving the radius of the + sphere, otherwise this check will likely not work. + + """ + point_vector = point.pos_from(self.point) + if isinstance(point_vector, Vector): + point_radius_squared = dot(point_vector, point_vector) + else: + point_radius_squared = point_vector**2 + return Eq(point_radius_squared, self.radius**2) == True + + def geodesic_length(self, point_1, point_2): + r"""Returns the shortest distance between two points on the sphere's + surface. + + Explanation + =========== + + The geodesic length, i.e. the shortest arc along the surface of a + sphere, connecting two points can be calculated using the formula: + + .. math:: + + l = \arccos\left(\mathbf{v}_1 \cdot \mathbf{v}_2\right) + + where $\mathbf{v}_1$ and $\mathbf{v}_2$ are the unit vectors from the + sphere's center to the first and second points on the sphere's surface + respectively. Note that the actual path that the geodesic will take is + undefined when the two points are directly opposite one another. + + Examples + ======== + + A geodesic length can only be calculated between two points on the + sphere's surface. Firstly, a ``WrappingSphere`` instance must be + created along with two points that will lie on its surface: + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import (Point, ReferenceFrame, + ... WrappingSphere) + >>> N = ReferenceFrame('N') + >>> r = symbols('r') + >>> pO = Point('pO') + >>> pO.set_vel(N, 0) + >>> sphere = WrappingSphere(r, pO) + >>> p1 = Point('p1') + >>> p2 = Point('p2') + + Let's assume that ``p1`` lies at a distance of ``r`` in the ``N.x`` + direction from ``pO`` and that ``p2`` is located on the sphere's + surface in the ``N.y + N.z`` direction from ``pO``. These positions can + be set with: + + >>> p1.set_pos(pO, r*N.x) + >>> p1.pos_from(pO) + r*N.x + >>> p2.set_pos(pO, r*(N.y + N.z).normalize()) + >>> p2.pos_from(pO) + sqrt(2)*r/2*N.y + sqrt(2)*r/2*N.z + + The geodesic length, which is in this case is a quarter of the sphere's + circumference, can be calculated using the ``geodesic_length`` method: + + >>> sphere.geodesic_length(p1, p2) + pi*r/2 + + If the ``geodesic_length`` method is passed an argument, the ``Point`` + that doesn't lie on the sphere's surface then a ``ValueError`` is + raised because it's not possible to calculate a value in this case. + + Parameters + ========== + + point_1 : Point + Point from which the geodesic length should be calculated. + point_2 : Point + Point to which the geodesic length should be calculated. + + """ + for point in (point_1, point_2): + if not self.point_on_surface(point): + msg = ( + f'Geodesic length cannot be calculated as point {point} ' + f'with radius {point.pos_from(self.point).magnitude()} ' + f'from the sphere\'s center {self.point} does not lie on ' + f'the surface of {self} with radius {self.radius}.' + ) + raise ValueError(msg) + point_1_vector = point_1.pos_from(self.point).normalize() + point_2_vector = point_2.pos_from(self.point).normalize() + central_angle = acos(point_2_vector.dot(point_1_vector)) + geodesic_length = self.radius*central_angle + return geodesic_length + + def geodesic_end_vectors(self, point_1, point_2): + """The vectors parallel to the geodesic at the two end points. + + Parameters + ========== + + point_1 : Point + The point from which the geodesic originates. + point_2 : Point + The point at which the geodesic terminates. + + """ + pA, pB = point_1, point_2 + pO = self.point + pA_vec = pA.pos_from(pO) + pB_vec = pB.pos_from(pO) + + if pA_vec.cross(pB_vec) == 0: + msg = ( + f'Can\'t compute geodesic end vectors for the pair of points ' + f'{pA} and {pB} on a sphere {self} as they are diametrically ' + f'opposed, thus the geodesic is not defined.' + ) + raise ValueError(msg) + + return ( + pA_vec.cross(pB.pos_from(pA)).cross(pA_vec).normalize(), + pB_vec.cross(pA.pos_from(pB)).cross(pB_vec).normalize(), + ) + + def __repr__(self): + """Representation of a ``WrappingSphere``.""" + return ( + f'{self.__class__.__name__}(radius={self.radius}, ' + f'point={self.point})' + ) + + +class WrappingCylinder(WrappingGeometryBase): + """A solid (infinite) cylindrical object. + + Explanation + =========== + + A wrapping geometry that allows for circular arcs to be defined between + pairs of points. These paths are always geodetic (the shortest possible) in + the sense that they will be a straight line on the unwrapped cylinder's + surface. However, it is also possible for a direction to be specified, i.e. + paths can be influenced such that they either wrap along the shortest side + or the longest side of the cylinder. To define these directions, rotations + are in the positive direction following the right-hand rule. + + Examples + ======== + + To create a ``WrappingCylinder`` instance, a ``Symbol`` denoting its + radius, a ``Vector`` defining its axis, and a ``Point`` through which its + axis passes are needed: + + >>> from sympy import symbols + >>> from sympy.physics.mechanics import (Point, ReferenceFrame, + ... WrappingCylinder) + >>> N = ReferenceFrame('N') + >>> r = symbols('r') + >>> pO = Point('pO') + >>> ax = N.x + + A cylinder with radius ``r``, and axis parallel to ``N.x`` passing through + ``pO`` can be instantiated with: + + >>> WrappingCylinder(r, pO, ax) + WrappingCylinder(radius=r, point=pO, axis=N.x) + + Parameters + ========== + + radius : Symbol + The radius of the cylinder. + point : Point + A point through which the cylinder's axis passes. + axis : Vector + The axis along which the cylinder is aligned. + + See Also + ======== + + WrappingSphere: Spherical geometry where the wrapping direction is always + geodetic. + + """ + + def __init__(self, radius, point, axis): + """Initializer for ``WrappingCylinder``. + + Parameters + ========== + + radius : Symbol + The radius of the cylinder. This symbol must represent a value that + is positive and constant, i.e. it cannot be a dynamic symbol. + point : Point + A point through which the cylinder's axis passes. + axis : Vector + The axis along which the cylinder is aligned. + + """ + self.radius = radius + self.point = point + self.axis = axis + + @property + def radius(self): + """Radius of the cylinder.""" + return self._radius + + @radius.setter + def radius(self, radius): + self._radius = radius + + @property + def point(self): + """A point through which the cylinder's axis passes.""" + return self._point + + @point.setter + def point(self, point): + self._point = point + + @property + def axis(self): + """Axis along which the cylinder is aligned.""" + return self._axis + + @axis.setter + def axis(self, axis): + self._axis = axis.normalize() + + def point_on_surface(self, point): + """Returns ``True`` if a point is on the cylinder's surface. + + Parameters + ========== + + point : Point + The point for which it's to be ascertained if it's on the + cylinder's surface or not. This point's position relative to the + cylinder's axis must be a simple expression involving the radius of + the sphere, otherwise this check will likely not work. + + """ + relative_position = point.pos_from(self.point) + parallel = relative_position.dot(self.axis) * self.axis + point_vector = relative_position - parallel + if isinstance(point_vector, Vector): + point_radius_squared = dot(point_vector, point_vector) + else: + point_radius_squared = point_vector**2 + return Eq(trigsimp(point_radius_squared), self.radius**2) == True + + def geodesic_length(self, point_1, point_2): + """The shortest distance between two points on a geometry's surface. + + Explanation + =========== + + The geodesic length, i.e. the shortest arc along the surface of a + cylinder, connecting two points. It can be calculated using Pythagoras' + theorem. The first short side is the distance between the two points on + the cylinder's surface parallel to the cylinder's axis. The second + short side is the arc of a circle between the two points of the + cylinder's surface perpendicular to the cylinder's axis. The resulting + hypotenuse is the geodesic length. + + Examples + ======== + + A geodesic length can only be calculated between two points on the + cylinder's surface. Firstly, a ``WrappingCylinder`` instance must be + created along with two points that will lie on its surface: + + >>> from sympy import symbols, cos, sin + >>> from sympy.physics.mechanics import (Point, ReferenceFrame, + ... WrappingCylinder, dynamicsymbols) + >>> N = ReferenceFrame('N') + >>> r = symbols('r') + >>> pO = Point('pO') + >>> pO.set_vel(N, 0) + >>> cylinder = WrappingCylinder(r, pO, N.x) + >>> p1 = Point('p1') + >>> p2 = Point('p2') + + Let's assume that ``p1`` is located at ``N.x + r*N.y`` relative to + ``pO`` and that ``p2`` is located at ``r*(cos(q)*N.y + sin(q)*N.z)`` + relative to ``pO``, where ``q(t)`` is a generalized coordinate + specifying the angle rotated around the ``N.x`` axis according to the + right-hand rule where ``N.y`` is zero. These positions can be set with: + + >>> q = dynamicsymbols('q') + >>> p1.set_pos(pO, N.x + r*N.y) + >>> p1.pos_from(pO) + N.x + r*N.y + >>> p2.set_pos(pO, r*(cos(q)*N.y + sin(q)*N.z).normalize()) + >>> p2.pos_from(pO).simplify() + r*cos(q(t))*N.y + r*sin(q(t))*N.z + + The geodesic length, which is in this case a is the hypotenuse of a + right triangle where the other two side lengths are ``1`` (parallel to + the cylinder's axis) and ``r*q(t)`` (parallel to the cylinder's cross + section), can be calculated using the ``geodesic_length`` method: + + >>> cylinder.geodesic_length(p1, p2).simplify() + sqrt(r**2*q(t)**2 + 1) + + If the ``geodesic_length`` method is passed an argument ``Point`` that + doesn't lie on the sphere's surface then a ``ValueError`` is raised + because it's not possible to calculate a value in this case. + + Parameters + ========== + + point_1 : Point + Point from which the geodesic length should be calculated. + point_2 : Point + Point to which the geodesic length should be calculated. + + """ + for point in (point_1, point_2): + if not self.point_on_surface(point): + msg = ( + f'Geodesic length cannot be calculated as point {point} ' + f'with radius {point.pos_from(self.point).magnitude()} ' + f'from the cylinder\'s center {self.point} does not lie on ' + f'the surface of {self} with radius {self.radius} and axis ' + f'{self.axis}.' + ) + raise ValueError(msg) + + relative_position = point_2.pos_from(point_1) + parallel_length = relative_position.dot(self.axis) + + point_1_relative_position = point_1.pos_from(self.point) + point_1_perpendicular_vector = ( + point_1_relative_position + - point_1_relative_position.dot(self.axis)*self.axis + ).normalize() + + point_2_relative_position = point_2.pos_from(self.point) + point_2_perpendicular_vector = ( + point_2_relative_position + - point_2_relative_position.dot(self.axis)*self.axis + ).normalize() + + central_angle = _directional_atan( + cancel(point_1_perpendicular_vector + .cross(point_2_perpendicular_vector) + .dot(self.axis)), + cancel(point_1_perpendicular_vector.dot(point_2_perpendicular_vector)), + ) + + planar_arc_length = self.radius*central_angle + geodesic_length = sqrt(parallel_length**2 + planar_arc_length**2) + return geodesic_length + + def geodesic_end_vectors(self, point_1, point_2): + """The vectors parallel to the geodesic at the two end points. + + Parameters + ========== + + point_1 : Point + The point from which the geodesic originates. + point_2 : Point + The point at which the geodesic terminates. + + """ + point_1_from_origin_point = point_1.pos_from(self.point) + point_2_from_origin_point = point_2.pos_from(self.point) + + if point_1_from_origin_point == point_2_from_origin_point: + msg = ( + f'Cannot compute geodesic end vectors for coincident points ' + f'{point_1} and {point_2} as no geodesic exists.' + ) + raise ValueError(msg) + + point_1_parallel = point_1_from_origin_point.dot(self.axis) * self.axis + point_2_parallel = point_2_from_origin_point.dot(self.axis) * self.axis + point_1_normal = (point_1_from_origin_point - point_1_parallel) + point_2_normal = (point_2_from_origin_point - point_2_parallel) + + if point_1_normal == point_2_normal: + point_1_perpendicular = Vector(0) + point_2_perpendicular = Vector(0) + else: + point_1_perpendicular = self.axis.cross(point_1_normal).normalize() + point_2_perpendicular = -self.axis.cross(point_2_normal).normalize() + + geodesic_length = self.geodesic_length(point_1, point_2) + relative_position = point_2.pos_from(point_1) + parallel_length = relative_position.dot(self.axis) + planar_arc_length = sqrt(geodesic_length**2 - parallel_length**2) + + point_1_vector = ( + planar_arc_length * point_1_perpendicular + + parallel_length * self.axis + ).normalize() + point_2_vector = ( + planar_arc_length * point_2_perpendicular + - parallel_length * self.axis + ).normalize() + + return (point_1_vector, point_2_vector) + + def __repr__(self): + """Representation of a ``WrappingCylinder``.""" + return ( + f'{self.__class__.__name__}(radius={self.radius}, ' + f'point={self.point}, axis={self.axis})' + ) + + +def _directional_atan(numerator, denominator): + """Compute atan in a directional sense as required for geodesics. + + Explanation + =========== + + To be able to control the direction of the geodesic length along the + surface of a cylinder a dedicated arctangent function is needed that + properly handles the directionality of different case. This function + ensures that the central angle is always positive but shifting the case + where ``atan2`` would return a negative angle to be centered around + ``2*pi``. + + Notes + ===== + + This function only handles very specific cases, i.e. the ones that are + expected to be encountered when calculating symbolic geodesics on uniformly + curved surfaces. As such, ``NotImplemented`` errors can be raised in many + cases. This function is named with a leader underscore to indicate that it + only aims to provide very specific functionality within the private scope + of this module. + + """ + + if numerator.is_number and denominator.is_number: + angle = atan2(numerator, denominator) + if angle < 0: + angle += 2 * pi + elif numerator.is_number: + msg = ( + f'Cannot compute a directional atan when the numerator {numerator} ' + f'is numeric and the denominator {denominator} is symbolic.' + ) + raise NotImplementedError(msg) + elif denominator.is_number: + msg = ( + f'Cannot compute a directional atan when the numerator {numerator} ' + f'is symbolic and the denominator {denominator} is numeric.' + ) + raise NotImplementedError(msg) + else: + ratio = sympify(trigsimp(numerator / denominator)) + if isinstance(ratio, tan): + angle = ratio.args[0] + elif ( + ratio.is_Mul + and ratio.args[0] == Integer(-1) + and isinstance(ratio.args[1], tan) + ): + angle = 2 * pi - ratio.args[1].args[0] + else: + msg = f'Cannot compute a directional atan for the value {ratio}.' + raise NotImplementedError(msg) + + return angle diff --git a/venv/lib/python3.11/site-packages/sympy/physics/optics/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/optics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d2d83d452fd30e718546c0eac26fe03bbef59c06 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/optics/__init__.py @@ -0,0 +1,38 @@ +__all__ = [ + 'TWave', + + 'RayTransferMatrix', 'FreeSpace', 'FlatRefraction', 'CurvedRefraction', + 'FlatMirror', 'CurvedMirror', 'ThinLens', 'GeometricRay', 'BeamParameter', + 'waist2rayleigh', 'rayleigh2waist', 'geometric_conj_ab', + 'geometric_conj_af', 'geometric_conj_bf', 'gaussian_conj', + 'conjugate_gauss_beams', + + 'Medium', + + 'refraction_angle', 'deviation', 'fresnel_coefficients', 'brewster_angle', + 'critical_angle', 'lens_makers_formula', 'mirror_formula', 'lens_formula', + 'hyperfocal_distance', 'transverse_magnification', + + 'jones_vector', 'stokes_vector', 'jones_2_stokes', 'linear_polarizer', + 'phase_retarder', 'half_wave_retarder', 'quarter_wave_retarder', + 'transmissive_filter', 'reflective_filter', 'mueller_matrix', + 'polarizing_beam_splitter', +] +from .waves import TWave + +from .gaussopt import (RayTransferMatrix, FreeSpace, FlatRefraction, + CurvedRefraction, FlatMirror, CurvedMirror, ThinLens, GeometricRay, + BeamParameter, waist2rayleigh, rayleigh2waist, geometric_conj_ab, + geometric_conj_af, geometric_conj_bf, gaussian_conj, + conjugate_gauss_beams) + +from .medium import Medium + +from .utils import (refraction_angle, deviation, fresnel_coefficients, + brewster_angle, critical_angle, lens_makers_formula, mirror_formula, + lens_formula, hyperfocal_distance, transverse_magnification) + +from .polarization import (jones_vector, stokes_vector, jones_2_stokes, + linear_polarizer, phase_retarder, half_wave_retarder, + quarter_wave_retarder, transmissive_filter, reflective_filter, + mueller_matrix, polarizing_beam_splitter) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/optics/gaussopt.py b/venv/lib/python3.11/site-packages/sympy/physics/optics/gaussopt.py new file mode 100644 index 0000000000000000000000000000000000000000..d9e8ef555d60e3204341cdc65cdd05fb02b2f196 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/optics/gaussopt.py @@ -0,0 +1,923 @@ +""" +Gaussian optics. + +The module implements: + +- Ray transfer matrices for geometrical and gaussian optics. + + See RayTransferMatrix, GeometricRay and BeamParameter + +- Conjugation relations for geometrical and gaussian optics. + + See geometric_conj*, gauss_conj and conjugate_gauss_beams + +The conventions for the distances are as follows: + +focal distance + positive for convergent lenses +object distance + positive for real objects +image distance + positive for real images +""" + +__all__ = [ + 'RayTransferMatrix', + 'FreeSpace', + 'FlatRefraction', + 'CurvedRefraction', + 'FlatMirror', + 'CurvedMirror', + 'ThinLens', + 'GeometricRay', + 'BeamParameter', + 'waist2rayleigh', + 'rayleigh2waist', + 'geometric_conj_ab', + 'geometric_conj_af', + 'geometric_conj_bf', + 'gaussian_conj', + 'conjugate_gauss_beams', +] + + +from sympy.core.expr import Expr +from sympy.core.numbers import (I, pi) +from sympy.core.sympify import sympify +from sympy.functions.elementary.complexes import (im, re) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import atan2 +from sympy.matrices.dense import Matrix, MutableDenseMatrix +from sympy.polys.rationaltools import together +from sympy.utilities.misc import filldedent + +### +# A, B, C, D matrices +### + + +class RayTransferMatrix(MutableDenseMatrix): + """ + Base class for a Ray Transfer Matrix. + + It should be used if there is not already a more specific subclass mentioned + in See Also. + + Parameters + ========== + + parameters : + A, B, C and D or 2x2 matrix (Matrix(2, 2, [A, B, C, D])) + + Examples + ======== + + >>> from sympy.physics.optics import RayTransferMatrix, ThinLens + >>> from sympy import Symbol, Matrix + + >>> mat = RayTransferMatrix(1, 2, 3, 4) + >>> mat + Matrix([ + [1, 2], + [3, 4]]) + + >>> RayTransferMatrix(Matrix([[1, 2], [3, 4]])) + Matrix([ + [1, 2], + [3, 4]]) + + >>> mat.A + 1 + + >>> f = Symbol('f') + >>> lens = ThinLens(f) + >>> lens + Matrix([ + [ 1, 0], + [-1/f, 1]]) + + >>> lens.C + -1/f + + See Also + ======== + + GeometricRay, BeamParameter, + FreeSpace, FlatRefraction, CurvedRefraction, + FlatMirror, CurvedMirror, ThinLens + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Ray_transfer_matrix_analysis + """ + + def __new__(cls, *args): + + if len(args) == 4: + temp = ((args[0], args[1]), (args[2], args[3])) + elif len(args) == 1 \ + and isinstance(args[0], Matrix) \ + and args[0].shape == (2, 2): + temp = args[0] + else: + raise ValueError(filldedent(''' + Expecting 2x2 Matrix or the 4 elements of + the Matrix but got %s''' % str(args))) + return Matrix.__new__(cls, temp) + + def __mul__(self, other): + if isinstance(other, RayTransferMatrix): + return RayTransferMatrix(Matrix(self)*Matrix(other)) + elif isinstance(other, GeometricRay): + return GeometricRay(Matrix(self)*Matrix(other)) + elif isinstance(other, BeamParameter): + temp = Matrix(self)*Matrix(((other.q,), (1,))) + q = (temp[0]/temp[1]).expand(complex=True) + return BeamParameter(other.wavelen, + together(re(q)), + z_r=together(im(q))) + else: + return Matrix.__mul__(self, other) + + @property + def A(self): + """ + The A parameter of the Matrix. + + Examples + ======== + + >>> from sympy.physics.optics import RayTransferMatrix + >>> mat = RayTransferMatrix(1, 2, 3, 4) + >>> mat.A + 1 + """ + return self[0, 0] + + @property + def B(self): + """ + The B parameter of the Matrix. + + Examples + ======== + + >>> from sympy.physics.optics import RayTransferMatrix + >>> mat = RayTransferMatrix(1, 2, 3, 4) + >>> mat.B + 2 + """ + return self[0, 1] + + @property + def C(self): + """ + The C parameter of the Matrix. + + Examples + ======== + + >>> from sympy.physics.optics import RayTransferMatrix + >>> mat = RayTransferMatrix(1, 2, 3, 4) + >>> mat.C + 3 + """ + return self[1, 0] + + @property + def D(self): + """ + The D parameter of the Matrix. + + Examples + ======== + + >>> from sympy.physics.optics import RayTransferMatrix + >>> mat = RayTransferMatrix(1, 2, 3, 4) + >>> mat.D + 4 + """ + return self[1, 1] + + +class FreeSpace(RayTransferMatrix): + """ + Ray Transfer Matrix for free space. + + Parameters + ========== + + distance + + See Also + ======== + + RayTransferMatrix + + Examples + ======== + + >>> from sympy.physics.optics import FreeSpace + >>> from sympy import symbols + >>> d = symbols('d') + >>> FreeSpace(d) + Matrix([ + [1, d], + [0, 1]]) + """ + def __new__(cls, d): + return RayTransferMatrix.__new__(cls, 1, d, 0, 1) + + +class FlatRefraction(RayTransferMatrix): + """ + Ray Transfer Matrix for refraction. + + Parameters + ========== + + n1 : + Refractive index of one medium. + n2 : + Refractive index of other medium. + + See Also + ======== + + RayTransferMatrix + + Examples + ======== + + >>> from sympy.physics.optics import FlatRefraction + >>> from sympy import symbols + >>> n1, n2 = symbols('n1 n2') + >>> FlatRefraction(n1, n2) + Matrix([ + [1, 0], + [0, n1/n2]]) + """ + def __new__(cls, n1, n2): + n1, n2 = map(sympify, (n1, n2)) + return RayTransferMatrix.__new__(cls, 1, 0, 0, n1/n2) + + +class CurvedRefraction(RayTransferMatrix): + """ + Ray Transfer Matrix for refraction on curved interface. + + Parameters + ========== + + R : + Radius of curvature (positive for concave). + n1 : + Refractive index of one medium. + n2 : + Refractive index of other medium. + + See Also + ======== + + RayTransferMatrix + + Examples + ======== + + >>> from sympy.physics.optics import CurvedRefraction + >>> from sympy import symbols + >>> R, n1, n2 = symbols('R n1 n2') + >>> CurvedRefraction(R, n1, n2) + Matrix([ + [ 1, 0], + [(n1 - n2)/(R*n2), n1/n2]]) + """ + def __new__(cls, R, n1, n2): + R, n1, n2 = map(sympify, (R, n1, n2)) + return RayTransferMatrix.__new__(cls, 1, 0, (n1 - n2)/R/n2, n1/n2) + + +class FlatMirror(RayTransferMatrix): + """ + Ray Transfer Matrix for reflection. + + See Also + ======== + + RayTransferMatrix + + Examples + ======== + + >>> from sympy.physics.optics import FlatMirror + >>> FlatMirror() + Matrix([ + [1, 0], + [0, 1]]) + """ + def __new__(cls): + return RayTransferMatrix.__new__(cls, 1, 0, 0, 1) + + +class CurvedMirror(RayTransferMatrix): + """ + Ray Transfer Matrix for reflection from curved surface. + + Parameters + ========== + + R : radius of curvature (positive for concave) + + See Also + ======== + + RayTransferMatrix + + Examples + ======== + + >>> from sympy.physics.optics import CurvedMirror + >>> from sympy import symbols + >>> R = symbols('R') + >>> CurvedMirror(R) + Matrix([ + [ 1, 0], + [-2/R, 1]]) + """ + def __new__(cls, R): + R = sympify(R) + return RayTransferMatrix.__new__(cls, 1, 0, -2/R, 1) + + +class ThinLens(RayTransferMatrix): + """ + Ray Transfer Matrix for a thin lens. + + Parameters + ========== + + f : + The focal distance. + + See Also + ======== + + RayTransferMatrix + + Examples + ======== + + >>> from sympy.physics.optics import ThinLens + >>> from sympy import symbols + >>> f = symbols('f') + >>> ThinLens(f) + Matrix([ + [ 1, 0], + [-1/f, 1]]) + """ + def __new__(cls, f): + f = sympify(f) + return RayTransferMatrix.__new__(cls, 1, 0, -1/f, 1) + + +### +# Representation for geometric ray +### + +class GeometricRay(MutableDenseMatrix): + """ + Representation for a geometric ray in the Ray Transfer Matrix formalism. + + Parameters + ========== + + h : height, and + angle : angle, or + matrix : a 2x1 matrix (Matrix(2, 1, [height, angle])) + + Examples + ======== + + >>> from sympy.physics.optics import GeometricRay, FreeSpace + >>> from sympy import symbols, Matrix + >>> d, h, angle = symbols('d, h, angle') + + >>> GeometricRay(h, angle) + Matrix([ + [ h], + [angle]]) + + >>> FreeSpace(d)*GeometricRay(h, angle) + Matrix([ + [angle*d + h], + [ angle]]) + + >>> GeometricRay( Matrix( ((h,), (angle,)) ) ) + Matrix([ + [ h], + [angle]]) + + See Also + ======== + + RayTransferMatrix + + """ + + def __new__(cls, *args): + if len(args) == 1 and isinstance(args[0], Matrix) \ + and args[0].shape == (2, 1): + temp = args[0] + elif len(args) == 2: + temp = ((args[0],), (args[1],)) + else: + raise ValueError(filldedent(''' + Expecting 2x1 Matrix or the 2 elements of + the Matrix but got %s''' % str(args))) + return Matrix.__new__(cls, temp) + + @property + def height(self): + """ + The distance from the optical axis. + + Examples + ======== + + >>> from sympy.physics.optics import GeometricRay + >>> from sympy import symbols + >>> h, angle = symbols('h, angle') + >>> gRay = GeometricRay(h, angle) + >>> gRay.height + h + """ + return self[0] + + @property + def angle(self): + """ + The angle with the optical axis. + + Examples + ======== + + >>> from sympy.physics.optics import GeometricRay + >>> from sympy import symbols + >>> h, angle = symbols('h, angle') + >>> gRay = GeometricRay(h, angle) + >>> gRay.angle + angle + """ + return self[1] + + +### +# Representation for gauss beam +### + +class BeamParameter(Expr): + """ + Representation for a gaussian ray in the Ray Transfer Matrix formalism. + + Parameters + ========== + + wavelen : the wavelength, + z : the distance to waist, and + w : the waist, or + z_r : the rayleigh range. + n : the refractive index of medium. + + Examples + ======== + + >>> from sympy.physics.optics import BeamParameter + >>> p = BeamParameter(530e-9, 1, w=1e-3) + >>> p.q + 1 + 1.88679245283019*I*pi + + >>> p.q.n() + 1.0 + 5.92753330865999*I + >>> p.w_0.n() + 0.00100000000000000 + >>> p.z_r.n() + 5.92753330865999 + + >>> from sympy.physics.optics import FreeSpace + >>> fs = FreeSpace(10) + >>> p1 = fs*p + >>> p.w.n() + 0.00101413072159615 + >>> p1.w.n() + 0.00210803120913829 + + See Also + ======== + + RayTransferMatrix + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Complex_beam_parameter + .. [2] https://en.wikipedia.org/wiki/Gaussian_beam + """ + #TODO A class Complex may be implemented. The BeamParameter may + # subclass it. See: + # https://groups.google.com/d/topic/sympy/7XkU07NRBEs/discussion + + def __new__(cls, wavelen, z, z_r=None, w=None, n=1): + wavelen = sympify(wavelen) + z = sympify(z) + n = sympify(n) + + if z_r is not None and w is None: + z_r = sympify(z_r) + elif w is not None and z_r is None: + z_r = waist2rayleigh(sympify(w), wavelen, n) + elif z_r is None and w is None: + raise ValueError('Must specify one of w and z_r.') + + return Expr.__new__(cls, wavelen, z, z_r, n) + + @property + def wavelen(self): + return self.args[0] + + @property + def z(self): + return self.args[1] + + @property + def z_r(self): + return self.args[2] + + @property + def n(self): + return self.args[3] + + @property + def q(self): + """ + The complex parameter representing the beam. + + Examples + ======== + + >>> from sympy.physics.optics import BeamParameter + >>> p = BeamParameter(530e-9, 1, w=1e-3) + >>> p.q + 1 + 1.88679245283019*I*pi + """ + return self.z + I*self.z_r + + @property + def radius(self): + """ + The radius of curvature of the phase front. + + Examples + ======== + + >>> from sympy.physics.optics import BeamParameter + >>> p = BeamParameter(530e-9, 1, w=1e-3) + >>> p.radius + 1 + 3.55998576005696*pi**2 + """ + return self.z*(1 + (self.z_r/self.z)**2) + + @property + def w(self): + """ + The radius of the beam w(z), at any position z along the beam. + The beam radius at `1/e^2` intensity (axial value). + + See Also + ======== + + w_0 : + The minimal radius of beam. + + Examples + ======== + + >>> from sympy.physics.optics import BeamParameter + >>> p = BeamParameter(530e-9, 1, w=1e-3) + >>> p.w + 0.001*sqrt(0.2809/pi**2 + 1) + """ + return self.w_0*sqrt(1 + (self.z/self.z_r)**2) + + @property + def w_0(self): + """ + The minimal radius of beam at `1/e^2` intensity (peak value). + + See Also + ======== + + w : the beam radius at `1/e^2` intensity (axial value). + + Examples + ======== + + >>> from sympy.physics.optics import BeamParameter + >>> p = BeamParameter(530e-9, 1, w=1e-3) + >>> p.w_0 + 0.00100000000000000 + """ + return sqrt(self.z_r/(pi*self.n)*self.wavelen) + + @property + def divergence(self): + """ + Half of the total angular spread. + + Examples + ======== + + >>> from sympy.physics.optics import BeamParameter + >>> p = BeamParameter(530e-9, 1, w=1e-3) + >>> p.divergence + 0.00053/pi + """ + return self.wavelen/pi/self.w_0 + + @property + def gouy(self): + """ + The Gouy phase. + + Examples + ======== + + >>> from sympy.physics.optics import BeamParameter + >>> p = BeamParameter(530e-9, 1, w=1e-3) + >>> p.gouy + atan(0.53/pi) + """ + return atan2(self.z, self.z_r) + + @property + def waist_approximation_limit(self): + """ + The minimal waist for which the gauss beam approximation is valid. + + Explanation + =========== + + The gauss beam is a solution to the paraxial equation. For curvatures + that are too great it is not a valid approximation. + + Examples + ======== + + >>> from sympy.physics.optics import BeamParameter + >>> p = BeamParameter(530e-9, 1, w=1e-3) + >>> p.waist_approximation_limit + 1.06e-6/pi + """ + return 2*self.wavelen/pi + + +### +# Utilities +### + +def waist2rayleigh(w, wavelen, n=1): + """ + Calculate the rayleigh range from the waist of a gaussian beam. + + See Also + ======== + + rayleigh2waist, BeamParameter + + Examples + ======== + + >>> from sympy.physics.optics import waist2rayleigh + >>> from sympy import symbols + >>> w, wavelen = symbols('w wavelen') + >>> waist2rayleigh(w, wavelen) + pi*w**2/wavelen + """ + w, wavelen = map(sympify, (w, wavelen)) + return w**2*n*pi/wavelen + + +def rayleigh2waist(z_r, wavelen): + """Calculate the waist from the rayleigh range of a gaussian beam. + + See Also + ======== + + waist2rayleigh, BeamParameter + + Examples + ======== + + >>> from sympy.physics.optics import rayleigh2waist + >>> from sympy import symbols + >>> z_r, wavelen = symbols('z_r wavelen') + >>> rayleigh2waist(z_r, wavelen) + sqrt(wavelen*z_r)/sqrt(pi) + """ + z_r, wavelen = map(sympify, (z_r, wavelen)) + return sqrt(z_r/pi*wavelen) + + +def geometric_conj_ab(a, b): + """ + Conjugation relation for geometrical beams under paraxial conditions. + + Explanation + =========== + + Takes the distances to the optical element and returns the needed + focal distance. + + See Also + ======== + + geometric_conj_af, geometric_conj_bf + + Examples + ======== + + >>> from sympy.physics.optics import geometric_conj_ab + >>> from sympy import symbols + >>> a, b = symbols('a b') + >>> geometric_conj_ab(a, b) + a*b/(a + b) + """ + a, b = map(sympify, (a, b)) + if a.is_infinite or b.is_infinite: + return a if b.is_infinite else b + else: + return a*b/(a + b) + + +def geometric_conj_af(a, f): + """ + Conjugation relation for geometrical beams under paraxial conditions. + + Explanation + =========== + + Takes the object distance (for geometric_conj_af) or the image distance + (for geometric_conj_bf) to the optical element and the focal distance. + Then it returns the other distance needed for conjugation. + + See Also + ======== + + geometric_conj_ab + + Examples + ======== + + >>> from sympy.physics.optics.gaussopt import geometric_conj_af, geometric_conj_bf + >>> from sympy import symbols + >>> a, b, f = symbols('a b f') + >>> geometric_conj_af(a, f) + a*f/(a - f) + >>> geometric_conj_bf(b, f) + b*f/(b - f) + """ + a, f = map(sympify, (a, f)) + return -geometric_conj_ab(a, -f) + +geometric_conj_bf = geometric_conj_af + + +def gaussian_conj(s_in, z_r_in, f): + """ + Conjugation relation for gaussian beams. + + Parameters + ========== + + s_in : + The distance to optical element from the waist. + z_r_in : + The rayleigh range of the incident beam. + f : + The focal length of the optical element. + + Returns + ======= + + a tuple containing (s_out, z_r_out, m) + s_out : + The distance between the new waist and the optical element. + z_r_out : + The rayleigh range of the emergent beam. + m : + The ration between the new and the old waists. + + Examples + ======== + + >>> from sympy.physics.optics import gaussian_conj + >>> from sympy import symbols + >>> s_in, z_r_in, f = symbols('s_in z_r_in f') + + >>> gaussian_conj(s_in, z_r_in, f)[0] + 1/(-1/(s_in + z_r_in**2/(-f + s_in)) + 1/f) + + >>> gaussian_conj(s_in, z_r_in, f)[1] + z_r_in/(1 - s_in**2/f**2 + z_r_in**2/f**2) + + >>> gaussian_conj(s_in, z_r_in, f)[2] + 1/sqrt(1 - s_in**2/f**2 + z_r_in**2/f**2) + """ + s_in, z_r_in, f = map(sympify, (s_in, z_r_in, f)) + s_out = 1 / ( -1/(s_in + z_r_in**2/(s_in - f)) + 1/f ) + m = 1/sqrt((1 - (s_in/f)**2) + (z_r_in/f)**2) + z_r_out = z_r_in / ((1 - (s_in/f)**2) + (z_r_in/f)**2) + return (s_out, z_r_out, m) + + +def conjugate_gauss_beams(wavelen, waist_in, waist_out, **kwargs): + """ + Find the optical setup conjugating the object/image waists. + + Parameters + ========== + + wavelen : + The wavelength of the beam. + waist_in and waist_out : + The waists to be conjugated. + f : + The focal distance of the element used in the conjugation. + + Returns + ======= + + a tuple containing (s_in, s_out, f) + s_in : + The distance before the optical element. + s_out : + The distance after the optical element. + f : + The focal distance of the optical element. + + Examples + ======== + + >>> from sympy.physics.optics import conjugate_gauss_beams + >>> from sympy import symbols, factor + >>> l, w_i, w_o, f = symbols('l w_i w_o f') + + >>> conjugate_gauss_beams(l, w_i, w_o, f=f)[0] + f*(1 - sqrt(w_i**2/w_o**2 - pi**2*w_i**4/(f**2*l**2))) + + >>> factor(conjugate_gauss_beams(l, w_i, w_o, f=f)[1]) + f*w_o**2*(w_i**2/w_o**2 - sqrt(w_i**2/w_o**2 - + pi**2*w_i**4/(f**2*l**2)))/w_i**2 + + >>> conjugate_gauss_beams(l, w_i, w_o, f=f)[2] + f + """ + #TODO add the other possible arguments + wavelen, waist_in, waist_out = map(sympify, (wavelen, waist_in, waist_out)) + m = waist_out / waist_in + z = waist2rayleigh(waist_in, wavelen) + if len(kwargs) != 1: + raise ValueError("The function expects only one named argument") + elif 'dist' in kwargs: + raise NotImplementedError(filldedent(''' + Currently only focal length is supported as a parameter''')) + elif 'f' in kwargs: + f = sympify(kwargs['f']) + s_in = f * (1 - sqrt(1/m**2 - z**2/f**2)) + s_out = gaussian_conj(s_in, z, f)[0] + elif 's_in' in kwargs: + raise NotImplementedError(filldedent(''' + Currently only focal length is supported as a parameter''')) + else: + raise ValueError(filldedent(''' + The functions expects the focal length as a named argument''')) + return (s_in, s_out, f) + +#TODO +#def plot_beam(): +# """Plot the beam radius as it propagates in space.""" +# pass + +#TODO +#def plot_beam_conjugation(): +# """ +# Plot the intersection of two beams. +# +# Represents the conjugation relation. +# +# See Also +# ======== +# +# conjugate_gauss_beams +# """ +# pass diff --git a/venv/lib/python3.11/site-packages/sympy/physics/optics/medium.py b/venv/lib/python3.11/site-packages/sympy/physics/optics/medium.py new file mode 100644 index 0000000000000000000000000000000000000000..764b68caad5865b8f3cee028a14cfa304796b4c0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/optics/medium.py @@ -0,0 +1,253 @@ +""" +**Contains** + +* Medium +""" +from sympy.physics.units import second, meter, kilogram, ampere + +__all__ = ['Medium'] + +from sympy.core.basic import Basic +from sympy.core.symbol import Str +from sympy.core.sympify import _sympify +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.physics.units import speed_of_light, u0, e0 + + +c = speed_of_light.convert_to(meter/second) +_e0mksa = e0.convert_to(ampere**2*second**4/(kilogram*meter**3)) +_u0mksa = u0.convert_to(meter*kilogram/(ampere**2*second**2)) + + +class Medium(Basic): + + """ + This class represents an optical medium. The prime reason to implement this is + to facilitate refraction, Fermat's principle, etc. + + Explanation + =========== + + An optical medium is a material through which electromagnetic waves propagate. + The permittivity and permeability of the medium define how electromagnetic + waves propagate in it. + + + Parameters + ========== + + name: string + The display name of the Medium. + + permittivity: Sympifyable + Electric permittivity of the space. + + permeability: Sympifyable + Magnetic permeability of the space. + + n: Sympifyable + Index of refraction of the medium. + + + Examples + ======== + + >>> from sympy.abc import epsilon, mu + >>> from sympy.physics.optics import Medium + >>> m1 = Medium('m1') + >>> m2 = Medium('m2', epsilon, mu) + >>> m1.intrinsic_impedance + 149896229*pi*kilogram*meter**2/(1250000*ampere**2*second**3) + >>> m2.refractive_index + 299792458*meter*sqrt(epsilon*mu)/second + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Optical_medium + + """ + + def __new__(cls, name, permittivity=None, permeability=None, n=None): + if not isinstance(name, Str): + name = Str(name) + + permittivity = _sympify(permittivity) if permittivity is not None else permittivity + permeability = _sympify(permeability) if permeability is not None else permeability + n = _sympify(n) if n is not None else n + + if n is not None: + if permittivity is not None and permeability is None: + permeability = n**2/(c**2*permittivity) + return MediumPP(name, permittivity, permeability) + elif permeability is not None and permittivity is None: + permittivity = n**2/(c**2*permeability) + return MediumPP(name, permittivity, permeability) + elif permittivity is not None and permittivity is not None: + raise ValueError("Specifying all of permittivity, permeability, and n is not allowed") + else: + return MediumN(name, n) + elif permittivity is not None and permeability is not None: + return MediumPP(name, permittivity, permeability) + elif permittivity is None and permeability is None: + return MediumPP(name, _e0mksa, _u0mksa) + else: + raise ValueError("Arguments are underspecified. Either specify n or any two of permittivity, " + "permeability, and n") + + @property + def name(self): + return self.args[0] + + @property + def speed(self): + """ + Returns speed of the electromagnetic wave travelling in the medium. + + Examples + ======== + + >>> from sympy.physics.optics import Medium + >>> m = Medium('m') + >>> m.speed + 299792458*meter/second + >>> m2 = Medium('m2', n=1) + >>> m.speed == m2.speed + True + + """ + return c / self.n + + @property + def refractive_index(self): + """ + Returns refractive index of the medium. + + Examples + ======== + + >>> from sympy.physics.optics import Medium + >>> m = Medium('m') + >>> m.refractive_index + 1 + + """ + return (c/self.speed) + + +class MediumN(Medium): + + """ + Represents an optical medium for which only the refractive index is known. + Useful for simple ray optics. + + This class should never be instantiated directly. + Instead it should be instantiated indirectly by instantiating Medium with + only n specified. + + Examples + ======== + >>> from sympy.physics.optics import Medium + >>> m = Medium('m', n=2) + >>> m + MediumN(Str('m'), 2) + """ + + def __new__(cls, name, n): + obj = super(Medium, cls).__new__(cls, name, n) + return obj + + @property + def n(self): + return self.args[1] + + +class MediumPP(Medium): + """ + Represents an optical medium for which the permittivity and permeability are known. + + This class should never be instantiated directly. Instead it should be + instantiated indirectly by instantiating Medium with any two of + permittivity, permeability, and n specified, or by not specifying any + of permittivity, permeability, or n, in which case default values for + permittivity and permeability will be used. + + Examples + ======== + >>> from sympy.physics.optics import Medium + >>> from sympy.abc import epsilon, mu + >>> m1 = Medium('m1', permittivity=epsilon, permeability=mu) + >>> m1 + MediumPP(Str('m1'), epsilon, mu) + >>> m2 = Medium('m2') + >>> m2 + MediumPP(Str('m2'), 625000*ampere**2*second**4/(22468879468420441*pi*kilogram*meter**3), pi*kilogram*meter/(2500000*ampere**2*second**2)) + """ + + + def __new__(cls, name, permittivity, permeability): + obj = super(Medium, cls).__new__(cls, name, permittivity, permeability) + return obj + + @property + def intrinsic_impedance(self): + """ + Returns intrinsic impedance of the medium. + + Explanation + =========== + + The intrinsic impedance of a medium is the ratio of the + transverse components of the electric and magnetic fields + of the electromagnetic wave travelling in the medium. + In a region with no electrical conductivity it simplifies + to the square root of ratio of magnetic permeability to + electric permittivity. + + Examples + ======== + + >>> from sympy.physics.optics import Medium + >>> m = Medium('m') + >>> m.intrinsic_impedance + 149896229*pi*kilogram*meter**2/(1250000*ampere**2*second**3) + + """ + return sqrt(self.permeability / self.permittivity) + + @property + def permittivity(self): + """ + Returns electric permittivity of the medium. + + Examples + ======== + + >>> from sympy.physics.optics import Medium + >>> m = Medium('m') + >>> m.permittivity + 625000*ampere**2*second**4/(22468879468420441*pi*kilogram*meter**3) + + """ + return self.args[1] + + @property + def permeability(self): + """ + Returns magnetic permeability of the medium. + + Examples + ======== + + >>> from sympy.physics.optics import Medium + >>> m = Medium('m') + >>> m.permeability + pi*kilogram*meter/(2500000*ampere**2*second**2) + + """ + return self.args[2] + + @property + def n(self): + return c*sqrt(self.permittivity*self.permeability) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/optics/polarization.py b/venv/lib/python3.11/site-packages/sympy/physics/optics/polarization.py new file mode 100644 index 0000000000000000000000000000000000000000..0bdb546548ad082ef38f5f0c159d7eadd38f6d30 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/optics/polarization.py @@ -0,0 +1,732 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +The module implements routines to model the polarization of optical fields +and can be used to calculate the effects of polarization optical elements on +the fields. + +- Jones vectors. + +- Stokes vectors. + +- Jones matrices. + +- Mueller matrices. + +Examples +======== + +We calculate a generic Jones vector: + +>>> from sympy import symbols, pprint, zeros, simplify +>>> from sympy.physics.optics.polarization import (jones_vector, stokes_vector, +... half_wave_retarder, polarizing_beam_splitter, jones_2_stokes) + +>>> psi, chi, p, I0 = symbols("psi, chi, p, I0", real=True) +>>> x0 = jones_vector(psi, chi) +>>> pprint(x0, use_unicode=True) +⎡-ⅈ⋅sin(χ)⋅sin(ψ) + cos(χ)⋅cos(ψ)⎤ +⎢ ⎥ +⎣ⅈ⋅sin(χ)⋅cos(ψ) + sin(ψ)⋅cos(χ) ⎦ + +And the more general Stokes vector: + +>>> s0 = stokes_vector(psi, chi, p, I0) +>>> pprint(s0, use_unicode=True) +⎡ I₀ ⎤ +⎢ ⎥ +⎢I₀⋅p⋅cos(2⋅χ)⋅cos(2⋅ψ)⎥ +⎢ ⎥ +⎢I₀⋅p⋅sin(2⋅ψ)⋅cos(2⋅χ)⎥ +⎢ ⎥ +⎣ I₀⋅p⋅sin(2⋅χ) ⎦ + +We calculate how the Jones vector is modified by a half-wave plate: + +>>> alpha = symbols("alpha", real=True) +>>> HWP = half_wave_retarder(alpha) +>>> x1 = simplify(HWP*x0) + +We calculate the very common operation of passing a beam through a half-wave +plate and then through a polarizing beam-splitter. We do this by putting this +Jones vector as the first entry of a two-Jones-vector state that is transformed +by a 4x4 Jones matrix modelling the polarizing beam-splitter to get the +transmitted and reflected Jones vectors: + +>>> PBS = polarizing_beam_splitter() +>>> X1 = zeros(4, 1) +>>> X1[:2, :] = x1 +>>> X2 = PBS*X1 +>>> transmitted_port = X2[:2, :] +>>> reflected_port = X2[2:, :] + +This allows us to calculate how the power in both ports depends on the initial +polarization: + +>>> transmitted_power = jones_2_stokes(transmitted_port)[0] +>>> reflected_power = jones_2_stokes(reflected_port)[0] +>>> print(transmitted_power) +cos(-2*alpha + chi + psi)**2/2 + cos(2*alpha + chi - psi)**2/2 + + +>>> print(reflected_power) +sin(-2*alpha + chi + psi)**2/2 + sin(2*alpha + chi - psi)**2/2 + +Please see the description of the individual functions for further +details and examples. + +References +========== + +.. [1] https://en.wikipedia.org/wiki/Jones_calculus +.. [2] https://en.wikipedia.org/wiki/Mueller_calculus +.. [3] https://en.wikipedia.org/wiki/Stokes_parameters + +""" + +from sympy.core.numbers import (I, pi) +from sympy.functions.elementary.complexes import (Abs, im, re) +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.matrices.dense import Matrix +from sympy.simplify.simplify import simplify +from sympy.physics.quantum import TensorProduct + + +def jones_vector(psi, chi): + """A Jones vector corresponding to a polarization ellipse with `psi` tilt, + and `chi` circularity. + + Parameters + ========== + + psi : numeric type or SymPy Symbol + The tilt of the polarization relative to the `x` axis. + + chi : numeric type or SymPy Symbol + The angle adjacent to the mayor axis of the polarization ellipse. + + + Returns + ======= + + Matrix : + A Jones vector. + + Examples + ======== + + The axes on the Poincaré sphere. + + >>> from sympy import pprint, symbols, pi + >>> from sympy.physics.optics.polarization import jones_vector + >>> psi, chi = symbols("psi, chi", real=True) + + A general Jones vector. + + >>> pprint(jones_vector(psi, chi), use_unicode=True) + ⎡-ⅈ⋅sin(χ)⋅sin(ψ) + cos(χ)⋅cos(ψ)⎤ + ⎢ ⎥ + ⎣ⅈ⋅sin(χ)⋅cos(ψ) + sin(ψ)⋅cos(χ) ⎦ + + Horizontal polarization. + + >>> pprint(jones_vector(0, 0), use_unicode=True) + ⎡1⎤ + ⎢ ⎥ + ⎣0⎦ + + Vertical polarization. + + >>> pprint(jones_vector(pi/2, 0), use_unicode=True) + ⎡0⎤ + ⎢ ⎥ + ⎣1⎦ + + Diagonal polarization. + + >>> pprint(jones_vector(pi/4, 0), use_unicode=True) + ⎡√2⎤ + ⎢──⎥ + ⎢2 ⎥ + ⎢ ⎥ + ⎢√2⎥ + ⎢──⎥ + ⎣2 ⎦ + + Anti-diagonal polarization. + + >>> pprint(jones_vector(-pi/4, 0), use_unicode=True) + ⎡ √2 ⎤ + ⎢ ── ⎥ + ⎢ 2 ⎥ + ⎢ ⎥ + ⎢-√2 ⎥ + ⎢────⎥ + ⎣ 2 ⎦ + + Right-hand circular polarization. + + >>> pprint(jones_vector(0, pi/4), use_unicode=True) + ⎡ √2 ⎤ + ⎢ ── ⎥ + ⎢ 2 ⎥ + ⎢ ⎥ + ⎢√2⋅ⅈ⎥ + ⎢────⎥ + ⎣ 2 ⎦ + + Left-hand circular polarization. + + >>> pprint(jones_vector(0, -pi/4), use_unicode=True) + ⎡ √2 ⎤ + ⎢ ── ⎥ + ⎢ 2 ⎥ + ⎢ ⎥ + ⎢-√2⋅ⅈ ⎥ + ⎢──────⎥ + ⎣ 2 ⎦ + + """ + return Matrix([-I*sin(chi)*sin(psi) + cos(chi)*cos(psi), + I*sin(chi)*cos(psi) + sin(psi)*cos(chi)]) + + +def stokes_vector(psi, chi, p=1, I=1): + """A Stokes vector corresponding to a polarization ellipse with ``psi`` + tilt, and ``chi`` circularity. + + Parameters + ========== + + psi : numeric type or SymPy Symbol + The tilt of the polarization relative to the ``x`` axis. + chi : numeric type or SymPy Symbol + The angle adjacent to the mayor axis of the polarization ellipse. + p : numeric type or SymPy Symbol + The degree of polarization. + I : numeric type or SymPy Symbol + The intensity of the field. + + + Returns + ======= + + Matrix : + A Stokes vector. + + Examples + ======== + + The axes on the Poincaré sphere. + + >>> from sympy import pprint, symbols, pi + >>> from sympy.physics.optics.polarization import stokes_vector + >>> psi, chi, p, I = symbols("psi, chi, p, I", real=True) + >>> pprint(stokes_vector(psi, chi, p, I), use_unicode=True) + ⎡ I ⎤ + ⎢ ⎥ + ⎢I⋅p⋅cos(2⋅χ)⋅cos(2⋅ψ)⎥ + ⎢ ⎥ + ⎢I⋅p⋅sin(2⋅ψ)⋅cos(2⋅χ)⎥ + ⎢ ⎥ + ⎣ I⋅p⋅sin(2⋅χ) ⎦ + + + Horizontal polarization + + >>> pprint(stokes_vector(0, 0), use_unicode=True) + ⎡1⎤ + ⎢ ⎥ + ⎢1⎥ + ⎢ ⎥ + ⎢0⎥ + ⎢ ⎥ + ⎣0⎦ + + Vertical polarization + + >>> pprint(stokes_vector(pi/2, 0), use_unicode=True) + ⎡1 ⎤ + ⎢ ⎥ + ⎢-1⎥ + ⎢ ⎥ + ⎢0 ⎥ + ⎢ ⎥ + ⎣0 ⎦ + + Diagonal polarization + + >>> pprint(stokes_vector(pi/4, 0), use_unicode=True) + ⎡1⎤ + ⎢ ⎥ + ⎢0⎥ + ⎢ ⎥ + ⎢1⎥ + ⎢ ⎥ + ⎣0⎦ + + Anti-diagonal polarization + + >>> pprint(stokes_vector(-pi/4, 0), use_unicode=True) + ⎡1 ⎤ + ⎢ ⎥ + ⎢0 ⎥ + ⎢ ⎥ + ⎢-1⎥ + ⎢ ⎥ + ⎣0 ⎦ + + Right-hand circular polarization + + >>> pprint(stokes_vector(0, pi/4), use_unicode=True) + ⎡1⎤ + ⎢ ⎥ + ⎢0⎥ + ⎢ ⎥ + ⎢0⎥ + ⎢ ⎥ + ⎣1⎦ + + Left-hand circular polarization + + >>> pprint(stokes_vector(0, -pi/4), use_unicode=True) + ⎡1 ⎤ + ⎢ ⎥ + ⎢0 ⎥ + ⎢ ⎥ + ⎢0 ⎥ + ⎢ ⎥ + ⎣-1⎦ + + Unpolarized light + + >>> pprint(stokes_vector(0, 0, 0), use_unicode=True) + ⎡1⎤ + ⎢ ⎥ + ⎢0⎥ + ⎢ ⎥ + ⎢0⎥ + ⎢ ⎥ + ⎣0⎦ + + """ + S0 = I + S1 = I*p*cos(2*psi)*cos(2*chi) + S2 = I*p*sin(2*psi)*cos(2*chi) + S3 = I*p*sin(2*chi) + return Matrix([S0, S1, S2, S3]) + + +def jones_2_stokes(e): + """Return the Stokes vector for a Jones vector ``e``. + + Parameters + ========== + + e : SymPy Matrix + A Jones vector. + + Returns + ======= + + SymPy Matrix + A Jones vector. + + Examples + ======== + + The axes on the Poincaré sphere. + + >>> from sympy import pprint, pi + >>> from sympy.physics.optics.polarization import jones_vector + >>> from sympy.physics.optics.polarization import jones_2_stokes + >>> H = jones_vector(0, 0) + >>> V = jones_vector(pi/2, 0) + >>> D = jones_vector(pi/4, 0) + >>> A = jones_vector(-pi/4, 0) + >>> R = jones_vector(0, pi/4) + >>> L = jones_vector(0, -pi/4) + >>> pprint([jones_2_stokes(e) for e in [H, V, D, A, R, L]], + ... use_unicode=True) + ⎡⎡1⎤ ⎡1 ⎤ ⎡1⎤ ⎡1 ⎤ ⎡1⎤ ⎡1 ⎤⎤ + ⎢⎢ ⎥ ⎢ ⎥ ⎢ ⎥ ⎢ ⎥ ⎢ ⎥ ⎢ ⎥⎥ + ⎢⎢1⎥ ⎢-1⎥ ⎢0⎥ ⎢0 ⎥ ⎢0⎥ ⎢0 ⎥⎥ + ⎢⎢ ⎥, ⎢ ⎥, ⎢ ⎥, ⎢ ⎥, ⎢ ⎥, ⎢ ⎥⎥ + ⎢⎢0⎥ ⎢0 ⎥ ⎢1⎥ ⎢-1⎥ ⎢0⎥ ⎢0 ⎥⎥ + ⎢⎢ ⎥ ⎢ ⎥ ⎢ ⎥ ⎢ ⎥ ⎢ ⎥ ⎢ ⎥⎥ + ⎣⎣0⎦ ⎣0 ⎦ ⎣0⎦ ⎣0 ⎦ ⎣1⎦ ⎣-1⎦⎦ + + """ + ex, ey = e + return Matrix([Abs(ex)**2 + Abs(ey)**2, + Abs(ex)**2 - Abs(ey)**2, + 2*re(ex*ey.conjugate()), + -2*im(ex*ey.conjugate())]) + + +def linear_polarizer(theta=0): + """A linear polarizer Jones matrix with transmission axis at + an angle ``theta``. + + Parameters + ========== + + theta : numeric type or SymPy Symbol + The angle of the transmission axis relative to the horizontal plane. + + Returns + ======= + + SymPy Matrix + A Jones matrix representing the polarizer. + + Examples + ======== + + A generic polarizer. + + >>> from sympy import pprint, symbols + >>> from sympy.physics.optics.polarization import linear_polarizer + >>> theta = symbols("theta", real=True) + >>> J = linear_polarizer(theta) + >>> pprint(J, use_unicode=True) + ⎡ 2 ⎤ + ⎢ cos (θ) sin(θ)⋅cos(θ)⎥ + ⎢ ⎥ + ⎢ 2 ⎥ + ⎣sin(θ)⋅cos(θ) sin (θ) ⎦ + + + """ + M = Matrix([[cos(theta)**2, sin(theta)*cos(theta)], + [sin(theta)*cos(theta), sin(theta)**2]]) + return M + + +def phase_retarder(theta=0, delta=0): + """A phase retarder Jones matrix with retardance ``delta`` at angle ``theta``. + + Parameters + ========== + + theta : numeric type or SymPy Symbol + The angle of the fast axis relative to the horizontal plane. + delta : numeric type or SymPy Symbol + The phase difference between the fast and slow axes of the + transmitted light. + + Returns + ======= + + SymPy Matrix : + A Jones matrix representing the retarder. + + Examples + ======== + + A generic retarder. + + >>> from sympy import pprint, symbols + >>> from sympy.physics.optics.polarization import phase_retarder + >>> theta, delta = symbols("theta, delta", real=True) + >>> R = phase_retarder(theta, delta) + >>> pprint(R, use_unicode=True) + ⎡ -ⅈ⋅δ -ⅈ⋅δ ⎤ + ⎢ ───── ───── ⎥ + ⎢⎛ ⅈ⋅δ 2 2 ⎞ 2 ⎛ ⅈ⋅δ⎞ 2 ⎥ + ⎢⎝ℯ ⋅sin (θ) + cos (θ)⎠⋅ℯ ⎝1 - ℯ ⎠⋅ℯ ⋅sin(θ)⋅cos(θ)⎥ + ⎢ ⎥ + ⎢ -ⅈ⋅δ -ⅈ⋅δ ⎥ + ⎢ ───── ─────⎥ + ⎢⎛ ⅈ⋅δ⎞ 2 ⎛ ⅈ⋅δ 2 2 ⎞ 2 ⎥ + ⎣⎝1 - ℯ ⎠⋅ℯ ⋅sin(θ)⋅cos(θ) ⎝ℯ ⋅cos (θ) + sin (θ)⎠⋅ℯ ⎦ + + """ + R = Matrix([[cos(theta)**2 + exp(I*delta)*sin(theta)**2, + (1-exp(I*delta))*cos(theta)*sin(theta)], + [(1-exp(I*delta))*cos(theta)*sin(theta), + sin(theta)**2 + exp(I*delta)*cos(theta)**2]]) + return R*exp(-I*delta/2) + + +def half_wave_retarder(theta): + """A half-wave retarder Jones matrix at angle ``theta``. + + Parameters + ========== + + theta : numeric type or SymPy Symbol + The angle of the fast axis relative to the horizontal plane. + + Returns + ======= + + SymPy Matrix + A Jones matrix representing the retarder. + + Examples + ======== + + A generic half-wave plate. + + >>> from sympy import pprint, symbols + >>> from sympy.physics.optics.polarization import half_wave_retarder + >>> theta= symbols("theta", real=True) + >>> HWP = half_wave_retarder(theta) + >>> pprint(HWP, use_unicode=True) + ⎡ ⎛ 2 2 ⎞ ⎤ + ⎢-ⅈ⋅⎝- sin (θ) + cos (θ)⎠ -2⋅ⅈ⋅sin(θ)⋅cos(θ) ⎥ + ⎢ ⎥ + ⎢ ⎛ 2 2 ⎞⎥ + ⎣ -2⋅ⅈ⋅sin(θ)⋅cos(θ) -ⅈ⋅⎝sin (θ) - cos (θ)⎠⎦ + + """ + return phase_retarder(theta, pi) + + +def quarter_wave_retarder(theta): + """A quarter-wave retarder Jones matrix at angle ``theta``. + + Parameters + ========== + + theta : numeric type or SymPy Symbol + The angle of the fast axis relative to the horizontal plane. + + Returns + ======= + + SymPy Matrix + A Jones matrix representing the retarder. + + Examples + ======== + + A generic quarter-wave plate. + + >>> from sympy import pprint, symbols + >>> from sympy.physics.optics.polarization import quarter_wave_retarder + >>> theta= symbols("theta", real=True) + >>> QWP = quarter_wave_retarder(theta) + >>> pprint(QWP, use_unicode=True) + ⎡ -ⅈ⋅π -ⅈ⋅π ⎤ + ⎢ ───── ───── ⎥ + ⎢⎛ 2 2 ⎞ 4 4 ⎥ + ⎢⎝ⅈ⋅sin (θ) + cos (θ)⎠⋅ℯ (1 - ⅈ)⋅ℯ ⋅sin(θ)⋅cos(θ)⎥ + ⎢ ⎥ + ⎢ -ⅈ⋅π -ⅈ⋅π ⎥ + ⎢ ───── ─────⎥ + ⎢ 4 ⎛ 2 2 ⎞ 4 ⎥ + ⎣(1 - ⅈ)⋅ℯ ⋅sin(θ)⋅cos(θ) ⎝sin (θ) + ⅈ⋅cos (θ)⎠⋅ℯ ⎦ + + """ + return phase_retarder(theta, pi/2) + + +def transmissive_filter(T): + """An attenuator Jones matrix with transmittance ``T``. + + Parameters + ========== + + T : numeric type or SymPy Symbol + The transmittance of the attenuator. + + Returns + ======= + + SymPy Matrix + A Jones matrix representing the filter. + + Examples + ======== + + A generic filter. + + >>> from sympy import pprint, symbols + >>> from sympy.physics.optics.polarization import transmissive_filter + >>> T = symbols("T", real=True) + >>> NDF = transmissive_filter(T) + >>> pprint(NDF, use_unicode=True) + ⎡√T 0 ⎤ + ⎢ ⎥ + ⎣0 √T⎦ + + """ + return Matrix([[sqrt(T), 0], [0, sqrt(T)]]) + + +def reflective_filter(R): + """A reflective filter Jones matrix with reflectance ``R``. + + Parameters + ========== + + R : numeric type or SymPy Symbol + The reflectance of the filter. + + Returns + ======= + + SymPy Matrix + A Jones matrix representing the filter. + + Examples + ======== + + A generic filter. + + >>> from sympy import pprint, symbols + >>> from sympy.physics.optics.polarization import reflective_filter + >>> R = symbols("R", real=True) + >>> pprint(reflective_filter(R), use_unicode=True) + ⎡√R 0 ⎤ + ⎢ ⎥ + ⎣0 -√R⎦ + + """ + return Matrix([[sqrt(R), 0], [0, -sqrt(R)]]) + + +def mueller_matrix(J): + """The Mueller matrix corresponding to Jones matrix `J`. + + Parameters + ========== + + J : SymPy Matrix + A Jones matrix. + + Returns + ======= + + SymPy Matrix + The corresponding Mueller matrix. + + Examples + ======== + + Generic optical components. + + >>> from sympy import pprint, symbols + >>> from sympy.physics.optics.polarization import (mueller_matrix, + ... linear_polarizer, half_wave_retarder, quarter_wave_retarder) + >>> theta = symbols("theta", real=True) + + A linear_polarizer + + >>> pprint(mueller_matrix(linear_polarizer(theta)), use_unicode=True) + ⎡ cos(2⋅θ) sin(2⋅θ) ⎤ + ⎢ 1/2 ──────── ──────── 0⎥ + ⎢ 2 2 ⎥ + ⎢ ⎥ + ⎢cos(2⋅θ) cos(4⋅θ) 1 sin(4⋅θ) ⎥ + ⎢──────── ──────── + ─ ──────── 0⎥ + ⎢ 2 4 4 4 ⎥ + ⎢ ⎥ + ⎢sin(2⋅θ) sin(4⋅θ) 1 cos(4⋅θ) ⎥ + ⎢──────── ──────── ─ - ──────── 0⎥ + ⎢ 2 4 4 4 ⎥ + ⎢ ⎥ + ⎣ 0 0 0 0⎦ + + A half-wave plate + + >>> pprint(mueller_matrix(half_wave_retarder(theta)), use_unicode=True) + ⎡1 0 0 0 ⎤ + ⎢ ⎥ + ⎢ 4 2 ⎥ + ⎢0 8⋅sin (θ) - 8⋅sin (θ) + 1 sin(4⋅θ) 0 ⎥ + ⎢ ⎥ + ⎢ 4 2 ⎥ + ⎢0 sin(4⋅θ) - 8⋅sin (θ) + 8⋅sin (θ) - 1 0 ⎥ + ⎢ ⎥ + ⎣0 0 0 -1⎦ + + A quarter-wave plate + + >>> pprint(mueller_matrix(quarter_wave_retarder(theta)), use_unicode=True) + ⎡1 0 0 0 ⎤ + ⎢ ⎥ + ⎢ cos(4⋅θ) 1 sin(4⋅θ) ⎥ + ⎢0 ──────── + ─ ──────── -sin(2⋅θ)⎥ + ⎢ 2 2 2 ⎥ + ⎢ ⎥ + ⎢ sin(4⋅θ) 1 cos(4⋅θ) ⎥ + ⎢0 ──────── ─ - ──────── cos(2⋅θ) ⎥ + ⎢ 2 2 2 ⎥ + ⎢ ⎥ + ⎣0 sin(2⋅θ) -cos(2⋅θ) 0 ⎦ + + """ + A = Matrix([[1, 0, 0, 1], + [1, 0, 0, -1], + [0, 1, 1, 0], + [0, -I, I, 0]]) + + return simplify(A*TensorProduct(J, J.conjugate())*A.inv()) + + +def polarizing_beam_splitter(Tp=1, Rs=1, Ts=0, Rp=0, phia=0, phib=0): + r"""A polarizing beam splitter Jones matrix at angle `theta`. + + Parameters + ========== + + J : SymPy Matrix + A Jones matrix. + Tp : numeric type or SymPy Symbol + The transmissivity of the P-polarized component. + Rs : numeric type or SymPy Symbol + The reflectivity of the S-polarized component. + Ts : numeric type or SymPy Symbol + The transmissivity of the S-polarized component. + Rp : numeric type or SymPy Symbol + The reflectivity of the P-polarized component. + phia : numeric type or SymPy Symbol + The phase difference between transmitted and reflected component for + output mode a. + phib : numeric type or SymPy Symbol + The phase difference between transmitted and reflected component for + output mode b. + + + Returns + ======= + + SymPy Matrix + A 4x4 matrix representing the PBS. This matrix acts on a 4x1 vector + whose first two entries are the Jones vector on one of the PBS ports, + and the last two entries the Jones vector on the other port. + + Examples + ======== + + Generic polarizing beam-splitter. + + >>> from sympy import pprint, symbols + >>> from sympy.physics.optics.polarization import polarizing_beam_splitter + >>> Ts, Rs, Tp, Rp = symbols(r"Ts, Rs, Tp, Rp", positive=True) + >>> phia, phib = symbols("phi_a, phi_b", real=True) + >>> PBS = polarizing_beam_splitter(Tp, Rs, Ts, Rp, phia, phib) + >>> pprint(PBS, use_unicode=False) + [ ____ ____ ] + [ \/ Tp 0 I*\/ Rp 0 ] + [ ] + [ ____ ____ I*phi_a] + [ 0 \/ Ts 0 -I*\/ Rs *e ] + [ ] + [ ____ ____ ] + [I*\/ Rp 0 \/ Tp 0 ] + [ ] + [ ____ I*phi_b ____ ] + [ 0 -I*\/ Rs *e 0 \/ Ts ] + + """ + PBS = Matrix([[sqrt(Tp), 0, I*sqrt(Rp), 0], + [0, sqrt(Ts), 0, -I*sqrt(Rs)*exp(I*phia)], + [I*sqrt(Rp), 0, sqrt(Tp), 0], + [0, -I*sqrt(Rs)*exp(I*phib), 0, sqrt(Ts)]]) + return PBS diff --git a/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_gaussopt.py b/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_gaussopt.py new file mode 100644 index 0000000000000000000000000000000000000000..5271f3cbb69cf5de861ff332d36418b79daeb1b5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_gaussopt.py @@ -0,0 +1,102 @@ +from sympy.core.evalf import N +from sympy.core.numbers import (Float, I, oo, pi) +from sympy.core.symbol import symbols +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import atan2 +from sympy.matrices.dense import Matrix +from sympy.polys.polytools import factor + +from sympy.physics.optics import (BeamParameter, CurvedMirror, + CurvedRefraction, FlatMirror, FlatRefraction, FreeSpace, GeometricRay, + RayTransferMatrix, ThinLens, conjugate_gauss_beams, + gaussian_conj, geometric_conj_ab, geometric_conj_af, geometric_conj_bf, + rayleigh2waist, waist2rayleigh) + + +def streq(a, b): + return str(a) == str(b) + + +def test_gauss_opt(): + mat = RayTransferMatrix(1, 2, 3, 4) + assert mat == Matrix([[1, 2], [3, 4]]) + assert mat == RayTransferMatrix( Matrix([[1, 2], [3, 4]]) ) + assert [mat.A, mat.B, mat.C, mat.D] == [1, 2, 3, 4] + + d, f, h, n1, n2, R = symbols('d f h n1 n2 R') + lens = ThinLens(f) + assert lens == Matrix([[ 1, 0], [-1/f, 1]]) + assert lens.C == -1/f + assert FreeSpace(d) == Matrix([[ 1, d], [0, 1]]) + assert FlatRefraction(n1, n2) == Matrix([[1, 0], [0, n1/n2]]) + assert CurvedRefraction( + R, n1, n2) == Matrix([[1, 0], [(n1 - n2)/(R*n2), n1/n2]]) + assert FlatMirror() == Matrix([[1, 0], [0, 1]]) + assert CurvedMirror(R) == Matrix([[ 1, 0], [-2/R, 1]]) + assert ThinLens(f) == Matrix([[ 1, 0], [-1/f, 1]]) + + mul = CurvedMirror(R)*FreeSpace(d) + mul_mat = Matrix([[ 1, 0], [-2/R, 1]])*Matrix([[ 1, d], [0, 1]]) + assert mul.A == mul_mat[0, 0] + assert mul.B == mul_mat[0, 1] + assert mul.C == mul_mat[1, 0] + assert mul.D == mul_mat[1, 1] + + angle = symbols('angle') + assert GeometricRay(h, angle) == Matrix([[ h], [angle]]) + assert FreeSpace( + d)*GeometricRay(h, angle) == Matrix([[angle*d + h], [angle]]) + assert GeometricRay( Matrix( ((h,), (angle,)) ) ) == Matrix([[h], [angle]]) + assert (FreeSpace(d)*GeometricRay(h, angle)).height == angle*d + h + assert (FreeSpace(d)*GeometricRay(h, angle)).angle == angle + + p = BeamParameter(530e-9, 1, w=1e-3) + assert streq(p.q, 1 + 1.88679245283019*I*pi) + assert streq(N(p.q), 1.0 + 5.92753330865999*I) + assert streq(N(p.w_0), Float(0.00100000000000000)) + assert streq(N(p.z_r), Float(5.92753330865999)) + fs = FreeSpace(10) + p1 = fs*p + assert streq(N(p.w), Float(0.00101413072159615)) + assert streq(N(p1.w), Float(0.00210803120913829)) + + w, wavelen = symbols('w wavelen') + assert waist2rayleigh(w, wavelen) == pi*w**2/wavelen + z_r, wavelen = symbols('z_r wavelen') + assert rayleigh2waist(z_r, wavelen) == sqrt(wavelen*z_r)/sqrt(pi) + + a, b, f = symbols('a b f') + assert geometric_conj_ab(a, b) == a*b/(a + b) + assert geometric_conj_af(a, f) == a*f/(a - f) + assert geometric_conj_bf(b, f) == b*f/(b - f) + assert geometric_conj_ab(oo, b) == b + assert geometric_conj_ab(a, oo) == a + + s_in, z_r_in, f = symbols('s_in z_r_in f') + assert gaussian_conj( + s_in, z_r_in, f)[0] == 1/(-1/(s_in + z_r_in**2/(-f + s_in)) + 1/f) + assert gaussian_conj( + s_in, z_r_in, f)[1] == z_r_in/(1 - s_in**2/f**2 + z_r_in**2/f**2) + assert gaussian_conj( + s_in, z_r_in, f)[2] == 1/sqrt(1 - s_in**2/f**2 + z_r_in**2/f**2) + + l, w_i, w_o, f = symbols('l w_i w_o f') + assert conjugate_gauss_beams(l, w_i, w_o, f=f)[0] == f*( + -sqrt(w_i**2/w_o**2 - pi**2*w_i**4/(f**2*l**2)) + 1) + assert factor(conjugate_gauss_beams(l, w_i, w_o, f=f)[1]) == f*w_o**2*( + w_i**2/w_o**2 - sqrt(w_i**2/w_o**2 - pi**2*w_i**4/(f**2*l**2)))/w_i**2 + assert conjugate_gauss_beams(l, w_i, w_o, f=f)[2] == f + + z, l, w_0 = symbols('z l w_0', positive=True) + p = BeamParameter(l, z, w=w_0) + assert p.radius == z*(pi**2*w_0**4/(l**2*z**2) + 1) + assert p.w == w_0*sqrt(l**2*z**2/(pi**2*w_0**4) + 1) + assert p.w_0 == w_0 + assert p.divergence == l/(pi*w_0) + assert p.gouy == atan2(z, pi*w_0**2/l) + assert p.waist_approximation_limit == 2*l/pi + + p = BeamParameter(530e-9, 1, w=1e-3, n=2) + assert streq(p.q, 1 + 3.77358490566038*I*pi) + assert streq(N(p.z_r), Float(11.8550666173200)) + assert streq(N(p.w_0), Float(0.00100000000000000)) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_medium.py b/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_medium.py new file mode 100644 index 0000000000000000000000000000000000000000..dfbb485f5b8e401f38c7f1cfa573f960a2479d7b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_medium.py @@ -0,0 +1,48 @@ +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.physics.optics import Medium +from sympy.abc import epsilon, mu, n +from sympy.physics.units import speed_of_light, u0, e0, m, kg, s, A + +from sympy.testing.pytest import raises + +c = speed_of_light.convert_to(m/s) +e0 = e0.convert_to(A**2*s**4/(kg*m**3)) +u0 = u0.convert_to(m*kg/(A**2*s**2)) + + +def test_medium(): + m1 = Medium('m1') + assert m1.intrinsic_impedance == sqrt(u0/e0) + assert m1.speed == 1/sqrt(e0*u0) + assert m1.refractive_index == c*sqrt(e0*u0) + assert m1.permittivity == e0 + assert m1.permeability == u0 + m2 = Medium('m2', epsilon, mu) + assert m2.intrinsic_impedance == sqrt(mu/epsilon) + assert m2.speed == 1/sqrt(epsilon*mu) + assert m2.refractive_index == c*sqrt(epsilon*mu) + assert m2.permittivity == epsilon + assert m2.permeability == mu + # Increasing electric permittivity and magnetic permeability + # by small amount from its value in vacuum. + m3 = Medium('m3', 9.0*10**(-12)*s**4*A**2/(m**3*kg), 1.45*10**(-6)*kg*m/(A**2*s**2)) + assert m3.refractive_index > m1.refractive_index + assert m3 != m1 + # Decreasing electric permittivity and magnetic permeability + # by small amount from its value in vacuum. + m4 = Medium('m4', 7.0*10**(-12)*s**4*A**2/(m**3*kg), 1.15*10**(-6)*kg*m/(A**2*s**2)) + assert m4.refractive_index < m1.refractive_index + m5 = Medium('m5', permittivity=710*10**(-12)*s**4*A**2/(m**3*kg), n=1.33) + assert abs(m5.intrinsic_impedance - 6.24845417765552*kg*m**2/(A**2*s**3)) \ + < 1e-12*kg*m**2/(A**2*s**3) + assert abs(m5.speed - 225407863.157895*m/s) < 1e-6*m/s + assert abs(m5.refractive_index - 1.33000000000000) < 1e-12 + assert abs(m5.permittivity - 7.1e-10*A**2*s**4/(kg*m**3)) \ + < 1e-20*A**2*s**4/(kg*m**3) + assert abs(m5.permeability - 2.77206575232851e-8*kg*m/(A**2*s**2)) \ + < 1e-20*kg*m/(A**2*s**2) + m6 = Medium('m6', None, mu, n) + assert m6.permittivity == n**2/(c**2*mu) + # test for equality of refractive indices + assert Medium('m7').refractive_index == Medium('m8', e0, u0).refractive_index + raises(ValueError, lambda:Medium('m9', e0, u0, 2)) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_polarization.py b/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_polarization.py new file mode 100644 index 0000000000000000000000000000000000000000..99c595d82a4a296066d5075f6182895a8de54d91 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_polarization.py @@ -0,0 +1,57 @@ +from sympy.physics.optics.polarization import (jones_vector, stokes_vector, + jones_2_stokes, linear_polarizer, phase_retarder, half_wave_retarder, + quarter_wave_retarder, transmissive_filter, reflective_filter, + mueller_matrix, polarizing_beam_splitter) +from sympy.core.numbers import (I, pi) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import exp +from sympy.matrices.dense import Matrix + + +def test_polarization(): + assert jones_vector(0, 0) == Matrix([1, 0]) + assert jones_vector(pi/2, 0) == Matrix([0, 1]) + ################################################################# + assert stokes_vector(0, 0) == Matrix([1, 1, 0, 0]) + assert stokes_vector(pi/2, 0) == Matrix([1, -1, 0, 0]) + ################################################################# + H = jones_vector(0, 0) + V = jones_vector(pi/2, 0) + D = jones_vector(pi/4, 0) + A = jones_vector(-pi/4, 0) + R = jones_vector(0, pi/4) + L = jones_vector(0, -pi/4) + + res = [Matrix([1, 1, 0, 0]), + Matrix([1, -1, 0, 0]), + Matrix([1, 0, 1, 0]), + Matrix([1, 0, -1, 0]), + Matrix([1, 0, 0, 1]), + Matrix([1, 0, 0, -1])] + + assert [jones_2_stokes(e) for e in [H, V, D, A, R, L]] == res + ################################################################# + assert linear_polarizer(0) == Matrix([[1, 0], [0, 0]]) + ################################################################# + delta = symbols("delta", real=True) + res = Matrix([[exp(-I*delta/2), 0], [0, exp(I*delta/2)]]) + assert phase_retarder(0, delta) == res + ################################################################# + assert half_wave_retarder(0) == Matrix([[-I, 0], [0, I]]) + ################################################################# + res = Matrix([[exp(-I*pi/4), 0], [0, I*exp(-I*pi/4)]]) + assert quarter_wave_retarder(0) == res + ################################################################# + assert transmissive_filter(1) == Matrix([[1, 0], [0, 1]]) + ################################################################# + assert reflective_filter(1) == Matrix([[1, 0], [0, -1]]) + + res = Matrix([[S(1)/2, S(1)/2, 0, 0], + [S(1)/2, S(1)/2, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]]) + assert mueller_matrix(linear_polarizer(0)) == res + ################################################################# + res = Matrix([[1, 0, 0, 0], [0, 0, 0, -I], [0, 0, 1, 0], [0, -I, 0, 0]]) + assert polarizing_beam_splitter() == res diff --git a/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_utils.py b/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..6c93883a081d3614a604aeadc8a4b617181de669 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_utils.py @@ -0,0 +1,202 @@ +from sympy.core.numbers import comp, Rational +from sympy.physics.optics.utils import (refraction_angle, fresnel_coefficients, + deviation, brewster_angle, critical_angle, lens_makers_formula, + mirror_formula, lens_formula, hyperfocal_distance, + transverse_magnification) +from sympy.physics.optics.medium import Medium +from sympy.physics.units import e0 + +from sympy.core.numbers import oo +from sympy.core.symbol import symbols +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.matrices.dense import Matrix +from sympy.geometry.point import Point3D +from sympy.geometry.line import Ray3D +from sympy.geometry.plane import Plane + +from sympy.testing.pytest import raises + + +ae = lambda a, b, n: comp(a, b, 10**-n) + + +def test_refraction_angle(): + n1, n2 = symbols('n1, n2') + m1 = Medium('m1') + m2 = Medium('m2') + r1 = Ray3D(Point3D(-1, -1, 1), Point3D(0, 0, 0)) + i = Matrix([1, 1, 1]) + n = Matrix([0, 0, 1]) + normal_ray = Ray3D(Point3D(0, 0, 0), Point3D(0, 0, 1)) + P = Plane(Point3D(0, 0, 0), normal_vector=[0, 0, 1]) + assert refraction_angle(r1, 1, 1, n) == Matrix([ + [ 1], + [ 1], + [-1]]) + assert refraction_angle([1, 1, 1], 1, 1, n) == Matrix([ + [ 1], + [ 1], + [-1]]) + assert refraction_angle((1, 1, 1), 1, 1, n) == Matrix([ + [ 1], + [ 1], + [-1]]) + assert refraction_angle(i, 1, 1, [0, 0, 1]) == Matrix([ + [ 1], + [ 1], + [-1]]) + assert refraction_angle(i, 1, 1, (0, 0, 1)) == Matrix([ + [ 1], + [ 1], + [-1]]) + assert refraction_angle(i, 1, 1, normal_ray) == Matrix([ + [ 1], + [ 1], + [-1]]) + assert refraction_angle(i, 1, 1, plane=P) == Matrix([ + [ 1], + [ 1], + [-1]]) + assert refraction_angle(r1, 1, 1, plane=P) == \ + Ray3D(Point3D(0, 0, 0), Point3D(1, 1, -1)) + assert refraction_angle(r1, m1, 1.33, plane=P) == \ + Ray3D(Point3D(0, 0, 0), Point3D(Rational(100, 133), Rational(100, 133), -789378201649271*sqrt(3)/1000000000000000)) + assert refraction_angle(r1, 1, m2, plane=P) == \ + Ray3D(Point3D(0, 0, 0), Point3D(1, 1, -1)) + assert refraction_angle(r1, n1, n2, plane=P) == \ + Ray3D(Point3D(0, 0, 0), Point3D(n1/n2, n1/n2, -sqrt(3)*sqrt(-2*n1**2/(3*n2**2) + 1))) + assert refraction_angle(r1, 1.33, 1, plane=P) == 0 # TIR + assert refraction_angle(r1, 1, 1, normal_ray) == \ + Ray3D(Point3D(0, 0, 0), direction_ratio=[1, 1, -1]) + assert ae(refraction_angle(0.5, 1, 2), 0.24207, 5) + assert ae(refraction_angle(0.5, 2, 1), 1.28293, 5) + raises(ValueError, lambda: refraction_angle(r1, m1, m2, normal_ray, P)) + raises(TypeError, lambda: refraction_angle(m1, m1, m2)) # can add other values for arg[0] + raises(TypeError, lambda: refraction_angle(r1, m1, m2, None, i)) + raises(TypeError, lambda: refraction_angle(r1, m1, m2, m2)) + + +def test_fresnel_coefficients(): + assert all(ae(i, j, 5) for i, j in zip( + fresnel_coefficients(0.5, 1, 1.33), + [0.11163, -0.17138, 0.83581, 0.82862])) + assert all(ae(i, j, 5) for i, j in zip( + fresnel_coefficients(0.5, 1.33, 1), + [-0.07726, 0.20482, 1.22724, 1.20482])) + m1 = Medium('m1') + m2 = Medium('m2', n=2) + assert all(ae(i, j, 5) for i, j in zip( + fresnel_coefficients(0.3, m1, m2), + [0.31784, -0.34865, 0.65892, 0.65135])) + ans = [[-0.23563, -0.97184], [0.81648, -0.57738]] + got = fresnel_coefficients(0.6, m2, m1) + for i, j in zip(got, ans): + for a, b in zip(i.as_real_imag(), j): + assert ae(a, b, 5) + + +def test_deviation(): + n1, n2 = symbols('n1, n2') + r1 = Ray3D(Point3D(-1, -1, 1), Point3D(0, 0, 0)) + n = Matrix([0, 0, 1]) + i = Matrix([-1, -1, -1]) + normal_ray = Ray3D(Point3D(0, 0, 0), Point3D(0, 0, 1)) + P = Plane(Point3D(0, 0, 0), normal_vector=[0, 0, 1]) + assert deviation(r1, 1, 1, normal=n) == 0 + assert deviation(r1, 1, 1, plane=P) == 0 + assert deviation(r1, 1, 1.1, plane=P).evalf(3) + 0.119 < 1e-3 + assert deviation(i, 1, 1.1, normal=normal_ray).evalf(3) + 0.119 < 1e-3 + assert deviation(r1, 1.33, 1, plane=P) is None # TIR + assert deviation(r1, 1, 1, normal=[0, 0, 1]) == 0 + assert deviation([-1, -1, -1], 1, 1, normal=[0, 0, 1]) == 0 + assert ae(deviation(0.5, 1, 2), -0.25793, 5) + assert ae(deviation(0.5, 2, 1), 0.78293, 5) + + +def test_brewster_angle(): + m1 = Medium('m1', n=1) + m2 = Medium('m2', n=1.33) + assert ae(brewster_angle(m1, m2), 0.93, 2) + m1 = Medium('m1', permittivity=e0, n=1) + m2 = Medium('m2', permittivity=e0, n=1.33) + assert ae(brewster_angle(m1, m2), 0.93, 2) + assert ae(brewster_angle(1, 1.33), 0.93, 2) + + +def test_critical_angle(): + m1 = Medium('m1', n=1) + m2 = Medium('m2', n=1.33) + assert ae(critical_angle(m2, m1), 0.85, 2) + + +def test_lens_makers_formula(): + n1, n2 = symbols('n1, n2') + m1 = Medium('m1', permittivity=e0, n=1) + m2 = Medium('m2', permittivity=e0, n=1.33) + assert lens_makers_formula(n1, n2, 10, -10) == 5.0*n2/(n1 - n2) + assert ae(lens_makers_formula(m1, m2, 10, -10), -20.15, 2) + assert ae(lens_makers_formula(1.33, 1, 10, -10), 15.15, 2) + + +def test_mirror_formula(): + u, v, f = symbols('u, v, f') + assert mirror_formula(focal_length=f, u=u) == f*u/(-f + u) + assert mirror_formula(focal_length=f, v=v) == f*v/(-f + v) + assert mirror_formula(u=u, v=v) == u*v/(u + v) + assert mirror_formula(u=oo, v=v) == v + assert mirror_formula(u=oo, v=oo) is oo + assert mirror_formula(focal_length=oo, u=u) == -u + assert mirror_formula(u=u, v=oo) == u + assert mirror_formula(focal_length=oo, v=oo) is oo + assert mirror_formula(focal_length=f, v=oo) == f + assert mirror_formula(focal_length=oo, v=v) == -v + assert mirror_formula(focal_length=oo, u=oo) is oo + assert mirror_formula(focal_length=f, u=oo) == f + assert mirror_formula(focal_length=oo, u=u) == -u + raises(ValueError, lambda: mirror_formula(focal_length=f, u=u, v=v)) + + +def test_lens_formula(): + u, v, f = symbols('u, v, f') + assert lens_formula(focal_length=f, u=u) == f*u/(f + u) + assert lens_formula(focal_length=f, v=v) == f*v/(f - v) + assert lens_formula(u=u, v=v) == u*v/(u - v) + assert lens_formula(u=oo, v=v) == v + assert lens_formula(u=oo, v=oo) is oo + assert lens_formula(focal_length=oo, u=u) == u + assert lens_formula(u=u, v=oo) == -u + assert lens_formula(focal_length=oo, v=oo) is -oo + assert lens_formula(focal_length=oo, v=v) == v + assert lens_formula(focal_length=f, v=oo) == -f + assert lens_formula(focal_length=oo, u=oo) is oo + assert lens_formula(focal_length=oo, u=u) == u + assert lens_formula(focal_length=f, u=oo) == f + raises(ValueError, lambda: lens_formula(focal_length=f, u=u, v=v)) + + +def test_hyperfocal_distance(): + f, N, c = symbols('f, N, c') + assert hyperfocal_distance(f=f, N=N, c=c) == f**2/(N*c) + assert ae(hyperfocal_distance(f=0.5, N=8, c=0.0033), 9.47, 2) + + +def test_transverse_magnification(): + si, so = symbols('si, so') + assert transverse_magnification(si, so) == -si/so + assert transverse_magnification(30, 15) == -2 + + +def test_lens_makers_formula_thick_lens(): + n1, n2 = symbols('n1, n2') + m1 = Medium('m1', permittivity=e0, n=1) + m2 = Medium('m2', permittivity=e0, n=1.33) + assert ae(lens_makers_formula(m1, m2, 10, -10, d=1), -19.82, 2) + assert lens_makers_formula(n1, n2, 1, -1, d=0.1) == n2/((2.0 - (0.1*n1 - 0.1*n2)/n1)*(n1 - n2)) + + +def test_lens_makers_formula_plano_lens(): + n1, n2 = symbols('n1, n2') + m1 = Medium('m1', permittivity=e0, n=1) + m2 = Medium('m2', permittivity=e0, n=1.33) + assert ae(lens_makers_formula(m1, m2, 10, oo), -40.30, 2) + assert lens_makers_formula(n1, n2, 10, oo) == 10.0*n2/(n1 - n2) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_waves.py b/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_waves.py new file mode 100644 index 0000000000000000000000000000000000000000..3cb8f804fb5be86d6174cb7c7b15fd8979c85ff8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/optics/tests/test_waves.py @@ -0,0 +1,82 @@ +from sympy.core.function import (Derivative, Function) +from sympy.core.numbers import (I, pi) +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (atan2, cos, sin) +from sympy.simplify.simplify import simplify +from sympy.abc import epsilon, mu +from sympy.functions.elementary.exponential import exp +from sympy.physics.units import speed_of_light, m, s +from sympy.physics.optics import TWave + +from sympy.testing.pytest import raises + +c = speed_of_light.convert_to(m/s) + +def test_twave(): + A1, phi1, A2, phi2, f = symbols('A1, phi1, A2, phi2, f') + n = Symbol('n') # Refractive index + t = Symbol('t') # Time + x = Symbol('x') # Spatial variable + E = Function('E') + w1 = TWave(A1, f, phi1) + w2 = TWave(A2, f, phi2) + assert w1.amplitude == A1 + assert w1.frequency == f + assert w1.phase == phi1 + assert w1.wavelength == c/(f*n) + assert w1.time_period == 1/f + assert w1.angular_velocity == 2*pi*f + assert w1.wavenumber == 2*pi*f*n/c + assert w1.speed == c/n + + w3 = w1 + w2 + assert w3.amplitude == sqrt(A1**2 + 2*A1*A2*cos(phi1 - phi2) + A2**2) + assert w3.frequency == f + assert w3.phase == atan2(A1*sin(phi1) + A2*sin(phi2), A1*cos(phi1) + A2*cos(phi2)) + assert w3.wavelength == c/(f*n) + assert w3.time_period == 1/f + assert w3.angular_velocity == 2*pi*f + assert w3.wavenumber == 2*pi*f*n/c + assert w3.speed == c/n + assert simplify(w3.rewrite(sin) - w2.rewrite(sin) - w1.rewrite(sin)) == 0 + assert w3.rewrite('pde') == epsilon*mu*Derivative(E(x, t), t, t) + Derivative(E(x, t), x, x) + assert w3.rewrite(cos) == sqrt(A1**2 + 2*A1*A2*cos(phi1 - phi2) + + A2**2)*cos(pi*f*n*x*s/(149896229*m) - 2*pi*f*t + atan2(A1*sin(phi1) + + A2*sin(phi2), A1*cos(phi1) + A2*cos(phi2))) + assert w3.rewrite(exp) == sqrt(A1**2 + 2*A1*A2*cos(phi1 - phi2) + + A2**2)*exp(I*(-2*pi*f*t + atan2(A1*sin(phi1) + A2*sin(phi2), A1*cos(phi1) + + A2*cos(phi2)) + pi*s*f*n*x/(149896229*m))) + + w4 = TWave(A1, None, 0, 1/f) + assert w4.frequency == f + + w5 = w1 - w2 + assert w5.amplitude == sqrt(A1**2 - 2*A1*A2*cos(phi1 - phi2) + A2**2) + assert w5.frequency == f + assert w5.phase == atan2(A1*sin(phi1) - A2*sin(phi2), A1*cos(phi1) - A2*cos(phi2)) + assert w5.wavelength == c/(f*n) + assert w5.time_period == 1/f + assert w5.angular_velocity == 2*pi*f + assert w5.wavenumber == 2*pi*f*n/c + assert w5.speed == c/n + assert simplify(w5.rewrite(sin) - w1.rewrite(sin) + w2.rewrite(sin)) == 0 + assert w5.rewrite('pde') == epsilon*mu*Derivative(E(x, t), t, t) + Derivative(E(x, t), x, x) + assert w5.rewrite(cos) == sqrt(A1**2 - 2*A1*A2*cos(phi1 - phi2) + + A2**2)*cos(-2*pi*f*t + atan2(A1*sin(phi1) - A2*sin(phi2), A1*cos(phi1) + - A2*cos(phi2)) + pi*s*f*n*x/(149896229*m)) + assert w5.rewrite(exp) == sqrt(A1**2 - 2*A1*A2*cos(phi1 - phi2) + + A2**2)*exp(I*(-2*pi*f*t + atan2(A1*sin(phi1) - A2*sin(phi2), A1*cos(phi1) + - A2*cos(phi2)) + pi*s*f*n*x/(149896229*m))) + + w6 = 2*w1 + assert w6.amplitude == 2*A1 + assert w6.frequency == f + assert w6.phase == phi1 + w7 = -w6 + assert w7.amplitude == -2*A1 + assert w7.frequency == f + assert w7.phase == phi1 + + raises(ValueError, lambda:TWave(A1)) + raises(ValueError, lambda:TWave(A1, f, phi1, t)) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/optics/utils.py b/venv/lib/python3.11/site-packages/sympy/physics/optics/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..72c3b78bd4b09eb069757fb3f8d3632f09ec4b80 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/optics/utils.py @@ -0,0 +1,698 @@ +""" +**Contains** + +* refraction_angle +* fresnel_coefficients +* deviation +* brewster_angle +* critical_angle +* lens_makers_formula +* mirror_formula +* lens_formula +* hyperfocal_distance +* transverse_magnification +""" + +__all__ = ['refraction_angle', + 'deviation', + 'fresnel_coefficients', + 'brewster_angle', + 'critical_angle', + 'lens_makers_formula', + 'mirror_formula', + 'lens_formula', + 'hyperfocal_distance', + 'transverse_magnification' + ] + +from sympy.core.numbers import (Float, I, oo, pi, zoo) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.core.sympify import sympify +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (acos, asin, atan2, cos, sin, tan) +from sympy.matrices.dense import Matrix +from sympy.polys.polytools import cancel +from sympy.series.limits import Limit +from sympy.geometry.line import Ray3D +from sympy.geometry.util import intersection +from sympy.geometry.plane import Plane +from sympy.utilities.iterables import is_sequence +from .medium import Medium + + +def refractive_index_of_medium(medium): + """ + Helper function that returns refractive index, given a medium + """ + if isinstance(medium, Medium): + n = medium.refractive_index + else: + n = sympify(medium) + return n + + +def refraction_angle(incident, medium1, medium2, normal=None, plane=None): + """ + This function calculates transmitted vector after refraction at planar + surface. ``medium1`` and ``medium2`` can be ``Medium`` or any sympifiable object. + If ``incident`` is a number then treated as angle of incidence (in radians) + in which case refraction angle is returned. + + If ``incident`` is an object of `Ray3D`, `normal` also has to be an instance + of `Ray3D` in order to get the output as a `Ray3D`. Please note that if + plane of separation is not provided and normal is an instance of `Ray3D`, + ``normal`` will be assumed to be intersecting incident ray at the plane of + separation. This will not be the case when `normal` is a `Matrix` or + any other sequence. + If ``incident`` is an instance of `Ray3D` and `plane` has not been provided + and ``normal`` is not `Ray3D`, output will be a `Matrix`. + + Parameters + ========== + + incident : Matrix, Ray3D, sequence or a number + Incident vector or angle of incidence + medium1 : sympy.physics.optics.medium.Medium or sympifiable + Medium 1 or its refractive index + medium2 : sympy.physics.optics.medium.Medium or sympifiable + Medium 2 or its refractive index + normal : Matrix, Ray3D, or sequence + Normal vector + plane : Plane + Plane of separation of the two media. + + Returns + ======= + + Returns an angle of refraction or a refracted ray depending on inputs. + + Examples + ======== + + >>> from sympy.physics.optics import refraction_angle + >>> from sympy.geometry import Point3D, Ray3D, Plane + >>> from sympy.matrices import Matrix + >>> from sympy import symbols, pi + >>> n = Matrix([0, 0, 1]) + >>> P = Plane(Point3D(0, 0, 0), normal_vector=[0, 0, 1]) + >>> r1 = Ray3D(Point3D(-1, -1, 1), Point3D(0, 0, 0)) + >>> refraction_angle(r1, 1, 1, n) + Matrix([ + [ 1], + [ 1], + [-1]]) + >>> refraction_angle(r1, 1, 1, plane=P) + Ray3D(Point3D(0, 0, 0), Point3D(1, 1, -1)) + + With different index of refraction of the two media + + >>> n1, n2 = symbols('n1, n2') + >>> refraction_angle(r1, n1, n2, n) + Matrix([ + [ n1/n2], + [ n1/n2], + [-sqrt(3)*sqrt(-2*n1**2/(3*n2**2) + 1)]]) + >>> refraction_angle(r1, n1, n2, plane=P) + Ray3D(Point3D(0, 0, 0), Point3D(n1/n2, n1/n2, -sqrt(3)*sqrt(-2*n1**2/(3*n2**2) + 1))) + >>> round(refraction_angle(pi/6, 1.2, 1.5), 5) + 0.41152 + """ + + n1 = refractive_index_of_medium(medium1) + n2 = refractive_index_of_medium(medium2) + + # check if an incidence angle was supplied instead of a ray + try: + angle_of_incidence = float(incident) + except TypeError: + angle_of_incidence = None + + try: + critical_angle_ = critical_angle(medium1, medium2) + except (ValueError, TypeError): + critical_angle_ = None + + if angle_of_incidence is not None: + if normal is not None or plane is not None: + raise ValueError('Normal/plane not allowed if incident is an angle') + + if not 0.0 <= angle_of_incidence < pi*0.5: + raise ValueError('Angle of incidence not in range [0:pi/2)') + + if critical_angle_ and angle_of_incidence > critical_angle_: + raise ValueError('Ray undergoes total internal reflection') + return asin(n1*sin(angle_of_incidence)/n2) + + # Treat the incident as ray below + # A flag to check whether to return Ray3D or not + return_ray = False + + if plane is not None and normal is not None: + raise ValueError("Either plane or normal is acceptable.") + + if not isinstance(incident, Matrix): + if is_sequence(incident): + _incident = Matrix(incident) + elif isinstance(incident, Ray3D): + _incident = Matrix(incident.direction_ratio) + else: + raise TypeError( + "incident should be a Matrix, Ray3D, or sequence") + else: + _incident = incident + + # If plane is provided, get direction ratios of the normal + # to the plane from the plane else go with `normal` param. + if plane is not None: + if not isinstance(plane, Plane): + raise TypeError("plane should be an instance of geometry.plane.Plane") + # If we have the plane, we can get the intersection + # point of incident ray and the plane and thus return + # an instance of Ray3D. + if isinstance(incident, Ray3D): + return_ray = True + intersection_pt = plane.intersection(incident)[0] + _normal = Matrix(plane.normal_vector) + else: + if not isinstance(normal, Matrix): + if is_sequence(normal): + _normal = Matrix(normal) + elif isinstance(normal, Ray3D): + _normal = Matrix(normal.direction_ratio) + if isinstance(incident, Ray3D): + intersection_pt = intersection(incident, normal) + if len(intersection_pt) == 0: + raise ValueError( + "Normal isn't concurrent with the incident ray.") + else: + return_ray = True + intersection_pt = intersection_pt[0] + else: + raise TypeError( + "Normal should be a Matrix, Ray3D, or sequence") + else: + _normal = normal + + eta = n1/n2 # Relative index of refraction + # Calculating magnitude of the vectors + mag_incident = sqrt(sum(i**2 for i in _incident)) + mag_normal = sqrt(sum(i**2 for i in _normal)) + # Converting vectors to unit vectors by dividing + # them with their magnitudes + _incident /= mag_incident + _normal /= mag_normal + c1 = -_incident.dot(_normal) # cos(angle_of_incidence) + cs2 = 1 - eta**2*(1 - c1**2) # cos(angle_of_refraction)**2 + if cs2.is_negative: # This is the case of total internal reflection(TIR). + return S.Zero + drs = eta*_incident + (eta*c1 - sqrt(cs2))*_normal + # Multiplying unit vector by its magnitude + drs = drs*mag_incident + if not return_ray: + return drs + else: + return Ray3D(intersection_pt, direction_ratio=drs) + + +def fresnel_coefficients(angle_of_incidence, medium1, medium2): + """ + This function uses Fresnel equations to calculate reflection and + transmission coefficients. Those are obtained for both polarisations + when the electric field vector is in the plane of incidence (labelled 'p') + and when the electric field vector is perpendicular to the plane of + incidence (labelled 's'). There are four real coefficients unless the + incident ray reflects in total internal in which case there are two complex + ones. Angle of incidence is the angle between the incident ray and the + surface normal. ``medium1`` and ``medium2`` can be ``Medium`` or any + sympifiable object. + + Parameters + ========== + + angle_of_incidence : sympifiable + + medium1 : Medium or sympifiable + Medium 1 or its refractive index + + medium2 : Medium or sympifiable + Medium 2 or its refractive index + + Returns + ======= + + Returns a list with four real Fresnel coefficients: + [reflection p (TM), reflection s (TE), + transmission p (TM), transmission s (TE)] + If the ray is undergoes total internal reflection then returns a + list of two complex Fresnel coefficients: + [reflection p (TM), reflection s (TE)] + + Examples + ======== + + >>> from sympy.physics.optics import fresnel_coefficients + >>> fresnel_coefficients(0.3, 1, 2) + [0.317843553417859, -0.348645229818821, + 0.658921776708929, 0.651354770181179] + >>> fresnel_coefficients(0.6, 2, 1) + [-0.235625382192159 - 0.971843958291041*I, + 0.816477005968898 - 0.577377951366403*I] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Fresnel_equations + """ + if not 0 <= 2*angle_of_incidence < pi: + raise ValueError('Angle of incidence not in range [0:pi/2)') + + n1 = refractive_index_of_medium(medium1) + n2 = refractive_index_of_medium(medium2) + + angle_of_refraction = asin(n1*sin(angle_of_incidence)/n2) + try: + angle_of_total_internal_reflection_onset = critical_angle(n1, n2) + except ValueError: + angle_of_total_internal_reflection_onset = None + + if angle_of_total_internal_reflection_onset is None or\ + angle_of_total_internal_reflection_onset > angle_of_incidence: + R_s = -sin(angle_of_incidence - angle_of_refraction)\ + /sin(angle_of_incidence + angle_of_refraction) + R_p = tan(angle_of_incidence - angle_of_refraction)\ + /tan(angle_of_incidence + angle_of_refraction) + T_s = 2*sin(angle_of_refraction)*cos(angle_of_incidence)\ + /sin(angle_of_incidence + angle_of_refraction) + T_p = 2*sin(angle_of_refraction)*cos(angle_of_incidence)\ + /(sin(angle_of_incidence + angle_of_refraction)\ + *cos(angle_of_incidence - angle_of_refraction)) + return [R_p, R_s, T_p, T_s] + else: + n = n2/n1 + R_s = cancel((cos(angle_of_incidence)-\ + I*sqrt(sin(angle_of_incidence)**2 - n**2))\ + /(cos(angle_of_incidence)+\ + I*sqrt(sin(angle_of_incidence)**2 - n**2))) + R_p = cancel((n**2*cos(angle_of_incidence)-\ + I*sqrt(sin(angle_of_incidence)**2 - n**2))\ + /(n**2*cos(angle_of_incidence)+\ + I*sqrt(sin(angle_of_incidence)**2 - n**2))) + return [R_p, R_s] + + +def deviation(incident, medium1, medium2, normal=None, plane=None): + """ + This function calculates the angle of deviation of a ray + due to refraction at planar surface. + + Parameters + ========== + + incident : Matrix, Ray3D, sequence or float + Incident vector or angle of incidence + medium1 : sympy.physics.optics.medium.Medium or sympifiable + Medium 1 or its refractive index + medium2 : sympy.physics.optics.medium.Medium or sympifiable + Medium 2 or its refractive index + normal : Matrix, Ray3D, or sequence + Normal vector + plane : Plane + Plane of separation of the two media. + + Returns angular deviation between incident and refracted rays + + Examples + ======== + + >>> from sympy.physics.optics import deviation + >>> from sympy.geometry import Point3D, Ray3D, Plane + >>> from sympy.matrices import Matrix + >>> from sympy import symbols + >>> n1, n2 = symbols('n1, n2') + >>> n = Matrix([0, 0, 1]) + >>> P = Plane(Point3D(0, 0, 0), normal_vector=[0, 0, 1]) + >>> r1 = Ray3D(Point3D(-1, -1, 1), Point3D(0, 0, 0)) + >>> deviation(r1, 1, 1, n) + 0 + >>> deviation(r1, n1, n2, plane=P) + -acos(-sqrt(-2*n1**2/(3*n2**2) + 1)) + acos(-sqrt(3)/3) + >>> round(deviation(0.1, 1.2, 1.5), 5) + -0.02005 + """ + refracted = refraction_angle(incident, + medium1, + medium2, + normal=normal, + plane=plane) + try: + angle_of_incidence = Float(incident) + except TypeError: + angle_of_incidence = None + + if angle_of_incidence is not None: + return float(refracted) - angle_of_incidence + + if refracted != 0: + if isinstance(refracted, Ray3D): + refracted = Matrix(refracted.direction_ratio) + + if not isinstance(incident, Matrix): + if is_sequence(incident): + _incident = Matrix(incident) + elif isinstance(incident, Ray3D): + _incident = Matrix(incident.direction_ratio) + else: + raise TypeError( + "incident should be a Matrix, Ray3D, or sequence") + else: + _incident = incident + + if plane is None: + if not isinstance(normal, Matrix): + if is_sequence(normal): + _normal = Matrix(normal) + elif isinstance(normal, Ray3D): + _normal = Matrix(normal.direction_ratio) + else: + raise TypeError( + "normal should be a Matrix, Ray3D, or sequence") + else: + _normal = normal + else: + _normal = Matrix(plane.normal_vector) + + mag_incident = sqrt(sum(i**2 for i in _incident)) + mag_normal = sqrt(sum(i**2 for i in _normal)) + mag_refracted = sqrt(sum(i**2 for i in refracted)) + _incident /= mag_incident + _normal /= mag_normal + refracted /= mag_refracted + i = acos(_incident.dot(_normal)) + r = acos(refracted.dot(_normal)) + return i - r + + +def brewster_angle(medium1, medium2): + """ + This function calculates the Brewster's angle of incidence to Medium 2 from + Medium 1 in radians. + + Parameters + ========== + + medium 1 : Medium or sympifiable + Refractive index of Medium 1 + medium 2 : Medium or sympifiable + Refractive index of Medium 1 + + Examples + ======== + + >>> from sympy.physics.optics import brewster_angle + >>> brewster_angle(1, 1.33) + 0.926093295503462 + + """ + + n1 = refractive_index_of_medium(medium1) + n2 = refractive_index_of_medium(medium2) + + return atan2(n2, n1) + +def critical_angle(medium1, medium2): + """ + This function calculates the critical angle of incidence (marking the onset + of total internal) to Medium 2 from Medium 1 in radians. + + Parameters + ========== + + medium 1 : Medium or sympifiable + Refractive index of Medium 1. + medium 2 : Medium or sympifiable + Refractive index of Medium 1. + + Examples + ======== + + >>> from sympy.physics.optics import critical_angle + >>> critical_angle(1.33, 1) + 0.850908514477849 + + """ + + n1 = refractive_index_of_medium(medium1) + n2 = refractive_index_of_medium(medium2) + + if n2 > n1: + raise ValueError('Total internal reflection impossible for n1 < n2') + else: + return asin(n2/n1) + + + +def lens_makers_formula(n_lens, n_surr, r1, r2, d=0): + """ + This function calculates focal length of a lens. + It follows cartesian sign convention. + + Parameters + ========== + + n_lens : Medium or sympifiable + Index of refraction of lens. + n_surr : Medium or sympifiable + Index of reflection of surrounding. + r1 : sympifiable + Radius of curvature of first surface. + r2 : sympifiable + Radius of curvature of second surface. + d : sympifiable, optional + Thickness of lens, default value is 0. + + Examples + ======== + + >>> from sympy.physics.optics import lens_makers_formula + >>> from sympy import S + >>> lens_makers_formula(1.33, 1, 10, -10) + 15.1515151515151 + >>> lens_makers_formula(1.2, 1, 10, S.Infinity) + 50.0000000000000 + >>> lens_makers_formula(1.33, 1, 10, -10, d=1) + 15.3418463277618 + + """ + + if isinstance(n_lens, Medium): + n_lens = n_lens.refractive_index + else: + n_lens = sympify(n_lens) + if isinstance(n_surr, Medium): + n_surr = n_surr.refractive_index + else: + n_surr = sympify(n_surr) + d = sympify(d) + + focal_length = 1/((n_lens - n_surr) / n_surr*(1/r1 - 1/r2 + (((n_lens - n_surr) * d) / (n_lens * r1 * r2)))) + + if focal_length == zoo: + return S.Infinity + return focal_length + + +def mirror_formula(focal_length=None, u=None, v=None): + """ + This function provides one of the three parameters + when two of them are supplied. + This is valid only for paraxial rays. + + Parameters + ========== + + focal_length : sympifiable + Focal length of the mirror. + u : sympifiable + Distance of object from the pole on + the principal axis. + v : sympifiable + Distance of the image from the pole + on the principal axis. + + Examples + ======== + + >>> from sympy.physics.optics import mirror_formula + >>> from sympy.abc import f, u, v + >>> mirror_formula(focal_length=f, u=u) + f*u/(-f + u) + >>> mirror_formula(focal_length=f, v=v) + f*v/(-f + v) + >>> mirror_formula(u=u, v=v) + u*v/(u + v) + + """ + if focal_length and u and v: + raise ValueError("Please provide only two parameters") + + focal_length = sympify(focal_length) + u = sympify(u) + v = sympify(v) + if u is oo: + _u = Symbol('u') + if v is oo: + _v = Symbol('v') + if focal_length is oo: + _f = Symbol('f') + if focal_length is None: + if u is oo and v is oo: + return Limit(Limit(_v*_u/(_v + _u), _u, oo), _v, oo).doit() + if u is oo: + return Limit(v*_u/(v + _u), _u, oo).doit() + if v is oo: + return Limit(_v*u/(_v + u), _v, oo).doit() + return v*u/(v + u) + if u is None: + if v is oo and focal_length is oo: + return Limit(Limit(_v*_f/(_v - _f), _v, oo), _f, oo).doit() + if v is oo: + return Limit(_v*focal_length/(_v - focal_length), _v, oo).doit() + if focal_length is oo: + return Limit(v*_f/(v - _f), _f, oo).doit() + return v*focal_length/(v - focal_length) + if v is None: + if u is oo and focal_length is oo: + return Limit(Limit(_u*_f/(_u - _f), _u, oo), _f, oo).doit() + if u is oo: + return Limit(_u*focal_length/(_u - focal_length), _u, oo).doit() + if focal_length is oo: + return Limit(u*_f/(u - _f), _f, oo).doit() + return u*focal_length/(u - focal_length) + + +def lens_formula(focal_length=None, u=None, v=None): + """ + This function provides one of the three parameters + when two of them are supplied. + This is valid only for paraxial rays. + + Parameters + ========== + + focal_length : sympifiable + Focal length of the mirror. + u : sympifiable + Distance of object from the optical center on + the principal axis. + v : sympifiable + Distance of the image from the optical center + on the principal axis. + + Examples + ======== + + >>> from sympy.physics.optics import lens_formula + >>> from sympy.abc import f, u, v + >>> lens_formula(focal_length=f, u=u) + f*u/(f + u) + >>> lens_formula(focal_length=f, v=v) + f*v/(f - v) + >>> lens_formula(u=u, v=v) + u*v/(u - v) + + """ + if focal_length and u and v: + raise ValueError("Please provide only two parameters") + + focal_length = sympify(focal_length) + u = sympify(u) + v = sympify(v) + if u is oo: + _u = Symbol('u') + if v is oo: + _v = Symbol('v') + if focal_length is oo: + _f = Symbol('f') + if focal_length is None: + if u is oo and v is oo: + return Limit(Limit(_v*_u/(_u - _v), _u, oo), _v, oo).doit() + if u is oo: + return Limit(v*_u/(_u - v), _u, oo).doit() + if v is oo: + return Limit(_v*u/(u - _v), _v, oo).doit() + return v*u/(u - v) + if u is None: + if v is oo and focal_length is oo: + return Limit(Limit(_v*_f/(_f - _v), _v, oo), _f, oo).doit() + if v is oo: + return Limit(_v*focal_length/(focal_length - _v), _v, oo).doit() + if focal_length is oo: + return Limit(v*_f/(_f - v), _f, oo).doit() + return v*focal_length/(focal_length - v) + if v is None: + if u is oo and focal_length is oo: + return Limit(Limit(_u*_f/(_u + _f), _u, oo), _f, oo).doit() + if u is oo: + return Limit(_u*focal_length/(_u + focal_length), _u, oo).doit() + if focal_length is oo: + return Limit(u*_f/(u + _f), _f, oo).doit() + return u*focal_length/(u + focal_length) + +def hyperfocal_distance(f, N, c): + """ + + Parameters + ========== + + f: sympifiable + Focal length of a given lens. + + N: sympifiable + F-number of a given lens. + + c: sympifiable + Circle of Confusion (CoC) of a given image format. + + Example + ======= + + >>> from sympy.physics.optics import hyperfocal_distance + >>> round(hyperfocal_distance(f = 0.5, N = 8, c = 0.0033), 2) + 9.47 + """ + + f = sympify(f) + N = sympify(N) + c = sympify(c) + + return (1/(N * c))*(f**2) + +def transverse_magnification(si, so): + """ + + Calculates the transverse magnification upon reflection in a mirror, + which is the ratio of the image size to the object size. + + Parameters + ========== + + so: sympifiable + Lens-object distance. + + si: sympifiable + Lens-image distance. + + Example + ======= + + >>> from sympy.physics.optics import transverse_magnification + >>> transverse_magnification(30, 15) + -2 + + """ + + si = sympify(si) + so = sympify(so) + + return (-(si/so)) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/optics/waves.py b/venv/lib/python3.11/site-packages/sympy/physics/optics/waves.py new file mode 100644 index 0000000000000000000000000000000000000000..61e2ff4db578543f9f2694f239f03439bfab2c41 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/optics/waves.py @@ -0,0 +1,340 @@ +""" +This module has all the classes and functions related to waves in optics. + +**Contains** + +* TWave +""" + +__all__ = ['TWave'] + +from sympy.core.basic import Basic +from sympy.core.expr import Expr +from sympy.core.function import Derivative, Function +from sympy.core.numbers import (Number, pi, I) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.core.sympify import _sympify, sympify +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (atan2, cos, sin) +from sympy.physics.units import speed_of_light, meter, second + + +c = speed_of_light.convert_to(meter/second) + + +class TWave(Expr): + + r""" + This is a simple transverse sine wave travelling in a one-dimensional space. + Basic properties are required at the time of creation of the object, + but they can be changed later with respective methods provided. + + Explanation + =========== + + It is represented as :math:`A \times cos(k*x - \omega \times t + \phi )`, + where :math:`A` is the amplitude, :math:`\omega` is the angular frequency, + :math:`k` is the wavenumber (spatial frequency), :math:`x` is a spatial variable + to represent the position on the dimension on which the wave propagates, + and :math:`\phi` is the phase angle of the wave. + + + Arguments + ========= + + amplitude : Sympifyable + Amplitude of the wave. + frequency : Sympifyable + Frequency of the wave. + phase : Sympifyable + Phase angle of the wave. + time_period : Sympifyable + Time period of the wave. + n : Sympifyable + Refractive index of the medium. + + Raises + ======= + + ValueError : When neither frequency nor time period is provided + or they are not consistent. + TypeError : When anything other than TWave objects is added. + + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.optics import TWave + >>> A1, phi1, A2, phi2, f = symbols('A1, phi1, A2, phi2, f') + >>> w1 = TWave(A1, f, phi1) + >>> w2 = TWave(A2, f, phi2) + >>> w3 = w1 + w2 # Superposition of two waves + >>> w3 + TWave(sqrt(A1**2 + 2*A1*A2*cos(phi1 - phi2) + A2**2), f, + atan2(A1*sin(phi1) + A2*sin(phi2), A1*cos(phi1) + A2*cos(phi2)), 1/f, n) + >>> w3.amplitude + sqrt(A1**2 + 2*A1*A2*cos(phi1 - phi2) + A2**2) + >>> w3.phase + atan2(A1*sin(phi1) + A2*sin(phi2), A1*cos(phi1) + A2*cos(phi2)) + >>> w3.speed + 299792458*meter/(second*n) + >>> w3.angular_velocity + 2*pi*f + + """ + + def __new__( + cls, + amplitude, + frequency=None, + phase=S.Zero, + time_period=None, + n=Symbol('n')): + if time_period is not None: + time_period = _sympify(time_period) + _frequency = S.One/time_period + if frequency is not None: + frequency = _sympify(frequency) + _time_period = S.One/frequency + if time_period is not None: + if frequency != S.One/time_period: + raise ValueError("frequency and time_period should be consistent.") + if frequency is None and time_period is None: + raise ValueError("Either frequency or time period is needed.") + if frequency is None: + frequency = _frequency + if time_period is None: + time_period = _time_period + + amplitude = _sympify(amplitude) + phase = _sympify(phase) + n = sympify(n) + obj = Basic.__new__(cls, amplitude, frequency, phase, time_period, n) + return obj + + @property + def amplitude(self): + """ + Returns the amplitude of the wave. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.optics import TWave + >>> A, phi, f = symbols('A, phi, f') + >>> w = TWave(A, f, phi) + >>> w.amplitude + A + """ + return self.args[0] + + @property + def frequency(self): + """ + Returns the frequency of the wave, + in cycles per second. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.optics import TWave + >>> A, phi, f = symbols('A, phi, f') + >>> w = TWave(A, f, phi) + >>> w.frequency + f + """ + return self.args[1] + + @property + def phase(self): + """ + Returns the phase angle of the wave, + in radians. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.optics import TWave + >>> A, phi, f = symbols('A, phi, f') + >>> w = TWave(A, f, phi) + >>> w.phase + phi + """ + return self.args[2] + + @property + def time_period(self): + """ + Returns the temporal period of the wave, + in seconds per cycle. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.optics import TWave + >>> A, phi, f = symbols('A, phi, f') + >>> w = TWave(A, f, phi) + >>> w.time_period + 1/f + """ + return self.args[3] + + @property + def n(self): + """ + Returns the refractive index of the medium + """ + return self.args[4] + + @property + def wavelength(self): + """ + Returns the wavelength (spatial period) of the wave, + in meters per cycle. + It depends on the medium of the wave. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.optics import TWave + >>> A, phi, f = symbols('A, phi, f') + >>> w = TWave(A, f, phi) + >>> w.wavelength + 299792458*meter/(second*f*n) + """ + return c/(self.frequency*self.n) + + + @property + def speed(self): + """ + Returns the propagation speed of the wave, + in meters per second. + It is dependent on the propagation medium. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.optics import TWave + >>> A, phi, f = symbols('A, phi, f') + >>> w = TWave(A, f, phi) + >>> w.speed + 299792458*meter/(second*n) + """ + return self.wavelength*self.frequency + + @property + def angular_velocity(self): + """ + Returns the angular velocity of the wave, + in radians per second. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.optics import TWave + >>> A, phi, f = symbols('A, phi, f') + >>> w = TWave(A, f, phi) + >>> w.angular_velocity + 2*pi*f + """ + return 2*pi*self.frequency + + @property + def wavenumber(self): + """ + Returns the wavenumber of the wave, + in radians per meter. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.optics import TWave + >>> A, phi, f = symbols('A, phi, f') + >>> w = TWave(A, f, phi) + >>> w.wavenumber + pi*second*f*n/(149896229*meter) + """ + return 2*pi/self.wavelength + + def __str__(self): + """String representation of a TWave.""" + from sympy.printing import sstr + return type(self).__name__ + sstr(self.args) + + __repr__ = __str__ + + def __add__(self, other): + """ + Addition of two waves will result in their superposition. + The type of interference will depend on their phase angles. + """ + if isinstance(other, TWave): + if self.frequency == other.frequency and self.wavelength == other.wavelength: + return TWave(sqrt(self.amplitude**2 + other.amplitude**2 + 2 * + self.amplitude*other.amplitude*cos( + self.phase - other.phase)), + self.frequency, + atan2(self.amplitude*sin(self.phase) + + other.amplitude*sin(other.phase), + self.amplitude*cos(self.phase) + + other.amplitude*cos(other.phase)) + ) + else: + raise NotImplementedError("Interference of waves with different frequencies" + " has not been implemented.") + else: + raise TypeError(type(other).__name__ + " and TWave objects cannot be added.") + + def __mul__(self, other): + """ + Multiplying a wave by a scalar rescales the amplitude of the wave. + """ + other = sympify(other) + if isinstance(other, Number): + return TWave(self.amplitude*other, *self.args[1:]) + else: + raise TypeError(type(other).__name__ + " and TWave objects cannot be multiplied.") + + def __sub__(self, other): + return self.__add__(-1*other) + + def __neg__(self): + return self.__mul__(-1) + + def __radd__(self, other): + return self.__add__(other) + + def __rmul__(self, other): + return self.__mul__(other) + + def __rsub__(self, other): + return (-self).__radd__(other) + + def _eval_rewrite_as_sin(self, *args, **kwargs): + return self.amplitude*sin(self.wavenumber*Symbol('x') + - self.angular_velocity*Symbol('t') + self.phase + pi/2, evaluate=False) + + def _eval_rewrite_as_cos(self, *args, **kwargs): + return self.amplitude*cos(self.wavenumber*Symbol('x') + - self.angular_velocity*Symbol('t') + self.phase) + + def _eval_rewrite_as_pde(self, *args, **kwargs): + mu, epsilon, x, t = symbols('mu, epsilon, x, t') + E = Function('E') + return Derivative(E(x, t), x, 2) + mu*epsilon*Derivative(E(x, t), t, 2) + + def _eval_rewrite_as_exp(self, *args, **kwargs): + return self.amplitude*exp(I*(self.wavenumber*Symbol('x') + - self.angular_velocity*Symbol('t') + self.phase)) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/paulialgebra.py b/venv/lib/python3.11/site-packages/sympy/physics/paulialgebra.py new file mode 100644 index 0000000000000000000000000000000000000000..300957354ff34907035aa1d1a48b00276230a1e5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/paulialgebra.py @@ -0,0 +1,231 @@ +""" +This module implements Pauli algebra by subclassing Symbol. Only algebraic +properties of Pauli matrices are used (we do not use the Matrix class). + +See the documentation to the class Pauli for examples. + +References +========== + +.. [1] https://en.wikipedia.org/wiki/Pauli_matrices +""" + +from sympy.core.add import Add +from sympy.core.mul import Mul +from sympy.core.numbers import I +from sympy.core.power import Pow +from sympy.core.symbol import Symbol +from sympy.physics.quantum import TensorProduct + +__all__ = ['evaluate_pauli_product'] + + +def delta(i, j): + """ + Returns 1 if ``i == j``, else 0. + + This is used in the multiplication of Pauli matrices. + + Examples + ======== + + >>> from sympy.physics.paulialgebra import delta + >>> delta(1, 1) + 1 + >>> delta(2, 3) + 0 + """ + if i == j: + return 1 + else: + return 0 + + +def epsilon(i, j, k): + """ + Return 1 if i,j,k is equal to (1,2,3), (2,3,1), or (3,1,2); + -1 if ``i``,``j``,``k`` is equal to (1,3,2), (3,2,1), or (2,1,3); + else return 0. + + This is used in the multiplication of Pauli matrices. + + Examples + ======== + + >>> from sympy.physics.paulialgebra import epsilon + >>> epsilon(1, 2, 3) + 1 + >>> epsilon(1, 3, 2) + -1 + """ + if (i, j, k) in ((1, 2, 3), (2, 3, 1), (3, 1, 2)): + return 1 + elif (i, j, k) in ((1, 3, 2), (3, 2, 1), (2, 1, 3)): + return -1 + else: + return 0 + + +class Pauli(Symbol): + """ + The class representing algebraic properties of Pauli matrices. + + Explanation + =========== + + The symbol used to display the Pauli matrices can be changed with an + optional parameter ``label="sigma"``. Pauli matrices with different + ``label`` attributes cannot multiply together. + + If the left multiplication of symbol or number with Pauli matrix is needed, + please use parentheses to separate Pauli and symbolic multiplication + (for example: 2*I*(Pauli(3)*Pauli(2))). + + Another variant is to use evaluate_pauli_product function to evaluate + the product of Pauli matrices and other symbols (with commutative + multiply rules). + + See Also + ======== + + evaluate_pauli_product + + Examples + ======== + + >>> from sympy.physics.paulialgebra import Pauli + >>> Pauli(1) + sigma1 + >>> Pauli(1)*Pauli(2) + I*sigma3 + >>> Pauli(1)*Pauli(1) + 1 + >>> Pauli(3)**4 + 1 + >>> Pauli(1)*Pauli(2)*Pauli(3) + I + + >>> from sympy.physics.paulialgebra import Pauli + >>> Pauli(1, label="tau") + tau1 + >>> Pauli(1)*Pauli(2, label="tau") + sigma1*tau2 + >>> Pauli(1, label="tau")*Pauli(2, label="tau") + I*tau3 + + >>> from sympy import I + >>> I*(Pauli(2)*Pauli(3)) + -sigma1 + + >>> from sympy.physics.paulialgebra import evaluate_pauli_product + >>> f = I*Pauli(2)*Pauli(3) + >>> f + I*sigma2*sigma3 + >>> evaluate_pauli_product(f) + -sigma1 + """ + + __slots__ = ("i", "label") + + def __new__(cls, i, label="sigma"): + if i not in [1, 2, 3]: + raise IndexError("Invalid Pauli index") + obj = Symbol.__new__(cls, "%s%d" %(label,i), commutative=False, hermitian=True) + obj.i = i + obj.label = label + return obj + + def __getnewargs_ex__(self): + return (self.i, self.label), {} + + def _hashable_content(self): + return (self.i, self.label) + + # FIXME don't work for -I*Pauli(2)*Pauli(3) + def __mul__(self, other): + if isinstance(other, Pauli): + j = self.i + k = other.i + jlab = self.label + klab = other.label + + if jlab == klab: + return delta(j, k) \ + + I*epsilon(j, k, 1)*Pauli(1,jlab) \ + + I*epsilon(j, k, 2)*Pauli(2,jlab) \ + + I*epsilon(j, k, 3)*Pauli(3,jlab) + return super().__mul__(other) + + def _eval_power(b, e): + if e.is_Integer and e.is_positive: + return super().__pow__(int(e) % 2) + + +def evaluate_pauli_product(arg): + '''Help function to evaluate Pauli matrices product + with symbolic objects. + + Parameters + ========== + + arg: symbolic expression that contains Paulimatrices + + Examples + ======== + + >>> from sympy.physics.paulialgebra import Pauli, evaluate_pauli_product + >>> from sympy import I + >>> evaluate_pauli_product(I*Pauli(1)*Pauli(2)) + -sigma3 + + >>> from sympy.abc import x + >>> evaluate_pauli_product(x**2*Pauli(2)*Pauli(1)) + -I*x**2*sigma3 + ''' + start = arg + end = arg + + if isinstance(arg, Pow) and isinstance(arg.args[0], Pauli): + if arg.args[1].is_odd: + return arg.args[0] + else: + return 1 + + if isinstance(arg, Add): + return Add(*[evaluate_pauli_product(part) for part in arg.args]) + + if isinstance(arg, TensorProduct): + return TensorProduct(*[evaluate_pauli_product(part) for part in arg.args]) + + elif not(isinstance(arg, Mul)): + return arg + + while not start == end or start == arg and end == arg: + start = end + + tmp = start.as_coeff_mul() + sigma_product = 1 + com_product = 1 + keeper = 1 + + for el in tmp[1]: + if isinstance(el, Pauli): + sigma_product *= el + elif not el.is_commutative: + if isinstance(el, Pow) and isinstance(el.args[0], Pauli): + if el.args[1].is_odd: + sigma_product *= el.args[0] + elif isinstance(el, TensorProduct): + keeper = keeper*sigma_product*\ + TensorProduct( + *[evaluate_pauli_product(part) for part in el.args] + ) + sigma_product = 1 + else: + keeper = keeper*sigma_product*el + sigma_product = 1 + else: + com_product *= el + end = tmp[0]*keeper*sigma_product*com_product + if end == arg: break + return end diff --git a/venv/lib/python3.11/site-packages/sympy/physics/pring.py b/venv/lib/python3.11/site-packages/sympy/physics/pring.py new file mode 100644 index 0000000000000000000000000000000000000000..325f4ff98a8c9fc428b4e332153af533f4d199ca --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/pring.py @@ -0,0 +1,94 @@ +from sympy.core.numbers import (I, pi) +from sympy.core.singleton import S +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.physics.quantum.constants import hbar + + +def wavefunction(n, x): + """ + Returns the wavefunction for particle on ring. + + Parameters + ========== + + n : The quantum number. + Here ``n`` can be positive as well as negative + which can be used to describe the direction of motion of particle. + x : + The angle. + + Examples + ======== + + >>> from sympy.physics.pring import wavefunction + >>> from sympy import Symbol, integrate, pi + >>> x=Symbol("x") + >>> wavefunction(1, x) + sqrt(2)*exp(I*x)/(2*sqrt(pi)) + >>> wavefunction(2, x) + sqrt(2)*exp(2*I*x)/(2*sqrt(pi)) + >>> wavefunction(3, x) + sqrt(2)*exp(3*I*x)/(2*sqrt(pi)) + + The normalization of the wavefunction is: + + >>> integrate(wavefunction(2, x)*wavefunction(-2, x), (x, 0, 2*pi)) + 1 + >>> integrate(wavefunction(4, x)*wavefunction(-4, x), (x, 0, 2*pi)) + 1 + + References + ========== + + .. [1] Atkins, Peter W.; Friedman, Ronald (2005). Molecular Quantum + Mechanics (4th ed.). Pages 71-73. + + """ + # sympify arguments + n, x = S(n), S(x) + return exp(n * I * x) / sqrt(2 * pi) + + +def energy(n, m, r): + """ + Returns the energy of the state corresponding to quantum number ``n``. + + E=(n**2 * (hcross)**2) / (2 * m * r**2) + + Parameters + ========== + + n : + The quantum number. + m : + Mass of the particle. + r : + Radius of circle. + + Examples + ======== + + >>> from sympy.physics.pring import energy + >>> from sympy import Symbol + >>> m=Symbol("m") + >>> r=Symbol("r") + >>> energy(1, m, r) + hbar**2/(2*m*r**2) + >>> energy(2, m, r) + 2*hbar**2/(m*r**2) + >>> energy(-2, 2.0, 3.0) + 0.111111111111111*hbar**2 + + References + ========== + + .. [1] Atkins, Peter W.; Friedman, Ronald (2005). Molecular Quantum + Mechanics (4th ed.). Pages 71-73. + + """ + n, m, r = S(n), S(m), S(r) + if n.is_integer: + return (n**2 * hbar**2) / (2 * m * r**2) + else: + raise ValueError("'n' must be integer") diff --git a/venv/lib/python3.11/site-packages/sympy/physics/qho_1d.py b/venv/lib/python3.11/site-packages/sympy/physics/qho_1d.py new file mode 100644 index 0000000000000000000000000000000000000000..f418e0e954656923fbfa64cea2145581ddf65aea --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/qho_1d.py @@ -0,0 +1,88 @@ +from sympy.core import S, pi, Rational +from sympy.functions import hermite, sqrt, exp, factorial, Abs +from sympy.physics.quantum.constants import hbar + + +def psi_n(n, x, m, omega): + """ + Returns the wavefunction psi_{n} for the One-dimensional harmonic oscillator. + + Parameters + ========== + + n : + the "nodal" quantum number. Corresponds to the number of nodes in the + wavefunction. ``n >= 0`` + x : + x coordinate. + m : + Mass of the particle. + omega : + Angular frequency of the oscillator. + + Examples + ======== + + >>> from sympy.physics.qho_1d import psi_n + >>> from sympy.abc import m, x, omega + >>> psi_n(0, x, m, omega) + (m*omega)**(1/4)*exp(-m*omega*x**2/(2*hbar))/(hbar**(1/4)*pi**(1/4)) + + """ + + # sympify arguments + n, x, m, omega = map(S, [n, x, m, omega]) + nu = m * omega / hbar + # normalization coefficient + C = (nu/pi)**Rational(1, 4) * sqrt(1/(2**n*factorial(n))) + + return C * exp(-nu* x**2 /2) * hermite(n, sqrt(nu)*x) + + +def E_n(n, omega): + """ + Returns the Energy of the One-dimensional harmonic oscillator. + + Parameters + ========== + + n : + The "nodal" quantum number. + omega : + The harmonic oscillator angular frequency. + + Notes + ===== + + The unit of the returned value matches the unit of hw, since the energy is + calculated as: + + E_n = hbar * omega*(n + 1/2) + + Examples + ======== + + >>> from sympy.physics.qho_1d import E_n + >>> from sympy.abc import x, omega + >>> E_n(x, omega) + hbar*omega*(x + 1/2) + """ + + return hbar * omega * (n + S.Half) + + +def coherent_state(n, alpha): + """ + Returns for the coherent states of 1D harmonic oscillator. + See https://en.wikipedia.org/wiki/Coherent_states + + Parameters + ========== + + n : + The "nodal" quantum number. + alpha : + The eigen value of annihilation operator. + """ + + return exp(- Abs(alpha)**2/2)*(alpha**n)/sqrt(factorial(n)) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..36203f1a48c4c53832ce44942878ddc7b89f8091 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/__init__.py @@ -0,0 +1,65 @@ +# Names exposed by 'from sympy.physics.quantum import *' + +__all__ = [ + 'AntiCommutator', + + 'qapply', + + 'Commutator', + + 'Dagger', + + 'HilbertSpaceError', 'HilbertSpace', 'TensorProductHilbertSpace', + 'TensorPowerHilbertSpace', 'DirectSumHilbertSpace', 'ComplexSpace', 'L2', + 'FockSpace', + + 'InnerProduct', + + 'Operator', 'HermitianOperator', 'UnitaryOperator', 'IdentityOperator', + 'OuterProduct', 'DifferentialOperator', + + 'represent', 'rep_innerproduct', 'rep_expectation', 'integrate_result', + 'get_basis', 'enumerate_states', + + 'KetBase', 'BraBase', 'StateBase', 'State', 'Ket', 'Bra', 'TimeDepState', + 'TimeDepBra', 'TimeDepKet', 'OrthogonalKet', 'OrthogonalBra', + 'OrthogonalState', 'Wavefunction', + + 'TensorProduct', 'tensor_product_simp', + + 'hbar', 'HBar', + + '_postprocess_state_mul', '_postprocess_state_pow' +] + +from .anticommutator import AntiCommutator + +from .qapply import qapply + +from .commutator import Commutator + +from .dagger import Dagger + +from .hilbert import (HilbertSpaceError, HilbertSpace, + TensorProductHilbertSpace, TensorPowerHilbertSpace, + DirectSumHilbertSpace, ComplexSpace, L2, FockSpace) + +from .innerproduct import InnerProduct + +from .operator import (Operator, HermitianOperator, UnitaryOperator, + IdentityOperator, OuterProduct, DifferentialOperator) + +from .represent import (represent, rep_innerproduct, rep_expectation, + integrate_result, get_basis, enumerate_states) + +from .state import (KetBase, BraBase, StateBase, State, Ket, Bra, + TimeDepState, TimeDepBra, TimeDepKet, OrthogonalKet, + OrthogonalBra, OrthogonalState, Wavefunction) + +from .tensorproduct import TensorProduct, tensor_product_simp + +from .constants import hbar, HBar + +# These are private, but need to be imported so they are registered +# as postprocessing transformers with Mul and Pow. +from .transforms import _postprocess_state_mul, _postprocess_state_pow diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/anticommutator.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/anticommutator.py new file mode 100644 index 0000000000000000000000000000000000000000..cbd26eade640b60a48eaac8c8b0abaf236478ca9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/anticommutator.py @@ -0,0 +1,166 @@ +"""The anti-commutator: ``{A,B} = A*B + B*A``.""" + +from sympy.core.expr import Expr +from sympy.core.kind import KindDispatcher +from sympy.core.mul import Mul +from sympy.core.numbers import Integer +from sympy.core.singleton import S +from sympy.printing.pretty.stringpict import prettyForm + +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.kind import _OperatorKind, OperatorKind + +__all__ = [ + 'AntiCommutator' +] + +#----------------------------------------------------------------------------- +# Anti-commutator +#----------------------------------------------------------------------------- + + +class AntiCommutator(Expr): + """The standard anticommutator, in an unevaluated state. + + Explanation + =========== + + Evaluating an anticommutator is defined [1]_ as: ``{A, B} = A*B + B*A``. + This class returns the anticommutator in an unevaluated form. To evaluate + the anticommutator, use the ``.doit()`` method. + + Canonical ordering of an anticommutator is ``{A, B}`` for ``A < B``. The + arguments of the anticommutator are put into canonical order using + ``__cmp__``. If ``B < A``, then ``{A, B}`` is returned as ``{B, A}``. + + Parameters + ========== + + A : Expr + The first argument of the anticommutator {A,B}. + B : Expr + The second argument of the anticommutator {A,B}. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.quantum import AntiCommutator + >>> from sympy.physics.quantum import Operator, Dagger + >>> x, y = symbols('x,y') + >>> A = Operator('A') + >>> B = Operator('B') + + Create an anticommutator and use ``doit()`` to multiply them out. + + >>> ac = AntiCommutator(A,B); ac + {A,B} + >>> ac.doit() + A*B + B*A + + The commutator orders it arguments in canonical order: + + >>> ac = AntiCommutator(B,A); ac + {A,B} + + Commutative constants are factored out: + + >>> AntiCommutator(3*x*A,x*y*B) + 3*x**2*y*{A,B} + + Adjoint operations applied to the anticommutator are properly applied to + the arguments: + + >>> Dagger(AntiCommutator(A,B)) + {Dagger(A),Dagger(B)} + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Commutator + """ + is_commutative = False + + _kind_dispatcher = KindDispatcher("AntiCommutator_kind_dispatcher", commutative=True) + + @property + def kind(self): + arg_kinds = (a.kind for a in self.args) + return self._kind_dispatcher(*arg_kinds) + + def __new__(cls, A, B): + r = cls.eval(A, B) + if r is not None: + return r + obj = Expr.__new__(cls, A, B) + return obj + + @classmethod + def eval(cls, a, b): + if not (a and b): + return S.Zero + if a == b: + return Integer(2)*a**2 + if a.is_commutative or b.is_commutative: + return Integer(2)*a*b + + # [xA,yB] -> xy*[A,B] + ca, nca = a.args_cnc() + cb, ncb = b.args_cnc() + c_part = ca + cb + if c_part: + return Mul(Mul(*c_part), cls(Mul._from_args(nca), Mul._from_args(ncb))) + + # Canonical ordering of arguments + #The Commutator [A,B] is on canonical form if A < B. + if a.compare(b) == 1: + return cls(b, a) + + def doit(self, **hints): + """ Evaluate anticommutator """ + # Keep the import of Operator here to avoid problems with + # circular imports. + from sympy.physics.quantum.operator import Operator + A = self.args[0] + B = self.args[1] + if isinstance(A, Operator) and isinstance(B, Operator): + try: + comm = A._eval_anticommutator(B, **hints) + except NotImplementedError: + try: + comm = B._eval_anticommutator(A, **hints) + except NotImplementedError: + comm = None + if comm is not None: + return comm.doit(**hints) + return (A*B + B*A).doit(**hints) + + def _eval_adjoint(self): + return AntiCommutator(Dagger(self.args[0]), Dagger(self.args[1])) + + def _sympyrepr(self, printer, *args): + return "%s(%s,%s)" % ( + self.__class__.__name__, printer._print( + self.args[0]), printer._print(self.args[1]) + ) + + def _sympystr(self, printer, *args): + return "{%s,%s}" % ( + printer._print(self.args[0]), printer._print(self.args[1])) + + def _pretty(self, printer, *args): + pform = printer._print(self.args[0], *args) + pform = prettyForm(*pform.right(prettyForm(','))) + pform = prettyForm(*pform.right(printer._print(self.args[1], *args))) + pform = prettyForm(*pform.parens(left='{', right='}')) + return pform + + def _latex(self, printer, *args): + return "\\left\\{%s,%s\\right\\}" % tuple([ + printer._print(arg, *args) for arg in self.args]) + + +@AntiCommutator._kind_dispatcher.register(_OperatorKind, _OperatorKind) +def find_op_kind(e1, e2): + """Find the kind of an anticommutator of two OperatorKinds.""" + return OperatorKind diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/boson.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/boson.py new file mode 100644 index 0000000000000000000000000000000000000000..0f24cae2a7ad2f438234fcf00dadb2a4a9d76fe8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/boson.py @@ -0,0 +1,243 @@ +"""Bosonic quantum operators.""" + +from sympy.core.numbers import Integer +from sympy.core.singleton import S +from sympy.functions.elementary.complexes import conjugate +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.physics.quantum import Operator +from sympy.physics.quantum import HilbertSpace, FockSpace, Ket, Bra +from sympy.functions.special.tensor_functions import KroneckerDelta + + +__all__ = [ + 'BosonOp', + 'BosonFockKet', + 'BosonFockBra', + 'BosonCoherentKet', + 'BosonCoherentBra' +] + + +class BosonOp(Operator): + """A bosonic operator that satisfies [a, Dagger(a)] == 1. + + Parameters + ========== + + name : str + A string that labels the bosonic mode. + + annihilation : bool + A bool that indicates if the bosonic operator is an annihilation (True, + default value) or creation operator (False) + + Examples + ======== + + >>> from sympy.physics.quantum import Dagger, Commutator + >>> from sympy.physics.quantum.boson import BosonOp + >>> a = BosonOp("a") + >>> Commutator(a, Dagger(a)).doit() + 1 + """ + + @property + def name(self): + return self.args[0] + + @property + def is_annihilation(self): + return bool(self.args[1]) + + @classmethod + def default_args(self): + return ("a", True) + + def __new__(cls, *args, **hints): + if not len(args) in [1, 2]: + raise ValueError('1 or 2 parameters expected, got %s' % args) + + if len(args) == 1: + args = (args[0], S.One) + + if len(args) == 2: + args = (args[0], Integer(args[1])) + + return Operator.__new__(cls, *args) + + def _eval_commutator_BosonOp(self, other, **hints): + if self.name == other.name: + # [a^\dagger, a] = -1 + if not self.is_annihilation and other.is_annihilation: + return S.NegativeOne + + elif 'independent' in hints and hints['independent']: + # [a, b] = 0 + return S.Zero + + return None + + def _eval_commutator_FermionOp(self, other, **hints): + return S.Zero + + def _eval_anticommutator_BosonOp(self, other, **hints): + if 'independent' in hints and hints['independent']: + # {a, b} = 2 * a * b, because [a, b] = 0 + return 2 * self * other + + return None + + def _eval_adjoint(self): + return BosonOp(str(self.name), not self.is_annihilation) + + def _print_contents_latex(self, printer, *args): + if self.is_annihilation: + return r'{%s}' % str(self.name) + else: + return r'{{%s}^\dagger}' % str(self.name) + + def _print_contents(self, printer, *args): + if self.is_annihilation: + return r'%s' % str(self.name) + else: + return r'Dagger(%s)' % str(self.name) + + def _print_contents_pretty(self, printer, *args): + from sympy.printing.pretty.stringpict import prettyForm + pform = printer._print(self.args[0], *args) + if self.is_annihilation: + return pform + else: + return pform**prettyForm('\N{DAGGER}') + + +class BosonFockKet(Ket): + """Fock state ket for a bosonic mode. + + Parameters + ========== + + n : Number + The Fock state number. + + """ + + def __new__(cls, n): + return Ket.__new__(cls, n) + + @property + def n(self): + return self.label[0] + + @classmethod + def dual_class(self): + return BosonFockBra + + @classmethod + def _eval_hilbert_space(cls, label): + return FockSpace() + + def _eval_innerproduct_BosonFockBra(self, bra, **hints): + return KroneckerDelta(self.n, bra.n) + + def _apply_from_right_to_BosonOp(self, op, **options): + if op.is_annihilation: + return sqrt(self.n) * BosonFockKet(self.n - 1) + else: + return sqrt(self.n + 1) * BosonFockKet(self.n + 1) + + +class BosonFockBra(Bra): + """Fock state bra for a bosonic mode. + + Parameters + ========== + + n : Number + The Fock state number. + + """ + + def __new__(cls, n): + return Bra.__new__(cls, n) + + @property + def n(self): + return self.label[0] + + @classmethod + def dual_class(self): + return BosonFockKet + + @classmethod + def _eval_hilbert_space(cls, label): + return FockSpace() + + +class BosonCoherentKet(Ket): + """Coherent state ket for a bosonic mode. + + Parameters + ========== + + alpha : Number, Symbol + The complex amplitude of the coherent state. + + """ + + def __new__(cls, alpha): + return Ket.__new__(cls, alpha) + + @property + def alpha(self): + return self.label[0] + + @classmethod + def dual_class(self): + return BosonCoherentBra + + @classmethod + def _eval_hilbert_space(cls, label): + return HilbertSpace() + + def _eval_innerproduct_BosonCoherentBra(self, bra, **hints): + if self.alpha == bra.alpha: + return S.One + else: + return exp(-(abs(self.alpha)**2 + abs(bra.alpha)**2 - 2 * conjugate(bra.alpha) * self.alpha)/2) + + def _apply_from_right_to_BosonOp(self, op, **options): + if op.is_annihilation: + return self.alpha * self + else: + return None + + +class BosonCoherentBra(Bra): + """Coherent state bra for a bosonic mode. + + Parameters + ========== + + alpha : Number, Symbol + The complex amplitude of the coherent state. + + """ + + def __new__(cls, alpha): + return Bra.__new__(cls, alpha) + + @property + def alpha(self): + return self.label[0] + + @classmethod + def dual_class(self): + return BosonCoherentKet + + def _apply_operator_BosonOp(self, op, **options): + if not op.is_annihilation: + return self.alpha * self + else: + return None diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/cartesian.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/cartesian.py new file mode 100644 index 0000000000000000000000000000000000000000..f3af1856f22c8fe4535b24be30bf99d0b3541a50 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/cartesian.py @@ -0,0 +1,341 @@ +"""Operators and states for 1D cartesian position and momentum. + +TODO: + +* Add 3D classes to mappings in operatorset.py + +""" + +from sympy.core.numbers import (I, pi) +from sympy.core.singleton import S +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.special.delta_functions import DiracDelta +from sympy.sets.sets import Interval + +from sympy.physics.quantum.constants import hbar +from sympy.physics.quantum.hilbert import L2 +from sympy.physics.quantum.operator import DifferentialOperator, HermitianOperator +from sympy.physics.quantum.state import Ket, Bra, State + +__all__ = [ + 'XOp', + 'YOp', + 'ZOp', + 'PxOp', + 'X', + 'Y', + 'Z', + 'Px', + 'XKet', + 'XBra', + 'PxKet', + 'PxBra', + 'PositionState3D', + 'PositionKet3D', + 'PositionBra3D' +] + +#------------------------------------------------------------------------- +# Position operators +#------------------------------------------------------------------------- + + +class XOp(HermitianOperator): + """1D cartesian position operator.""" + + @classmethod + def default_args(self): + return ("X",) + + @classmethod + def _eval_hilbert_space(self, args): + return L2(Interval(S.NegativeInfinity, S.Infinity)) + + def _eval_commutator_PxOp(self, other): + return I*hbar + + def _apply_operator_XKet(self, ket, **options): + return ket.position*ket + + def _apply_operator_PositionKet3D(self, ket, **options): + return ket.position_x*ket + + def _represent_PxKet(self, basis, *, index=1, **options): + states = basis._enumerate_state(2, start_index=index) + coord1 = states[0].momentum + coord2 = states[1].momentum + d = DifferentialOperator(coord1) + delta = DiracDelta(coord1 - coord2) + + return I*hbar*(d*delta) + + +class YOp(HermitianOperator): + """ Y cartesian coordinate operator (for 2D or 3D systems) """ + + @classmethod + def default_args(self): + return ("Y",) + + @classmethod + def _eval_hilbert_space(self, args): + return L2(Interval(S.NegativeInfinity, S.Infinity)) + + def _apply_operator_PositionKet3D(self, ket, **options): + return ket.position_y*ket + + +class ZOp(HermitianOperator): + """ Z cartesian coordinate operator (for 3D systems) """ + + @classmethod + def default_args(self): + return ("Z",) + + @classmethod + def _eval_hilbert_space(self, args): + return L2(Interval(S.NegativeInfinity, S.Infinity)) + + def _apply_operator_PositionKet3D(self, ket, **options): + return ket.position_z*ket + +#------------------------------------------------------------------------- +# Momentum operators +#------------------------------------------------------------------------- + + +class PxOp(HermitianOperator): + """1D cartesian momentum operator.""" + + @classmethod + def default_args(self): + return ("Px",) + + @classmethod + def _eval_hilbert_space(self, args): + return L2(Interval(S.NegativeInfinity, S.Infinity)) + + def _apply_operator_PxKet(self, ket, **options): + return ket.momentum*ket + + def _represent_XKet(self, basis, *, index=1, **options): + states = basis._enumerate_state(2, start_index=index) + coord1 = states[0].position + coord2 = states[1].position + d = DifferentialOperator(coord1) + delta = DiracDelta(coord1 - coord2) + + return -I*hbar*(d*delta) + +X = XOp('X') +Y = YOp('Y') +Z = ZOp('Z') +Px = PxOp('Px') + +#------------------------------------------------------------------------- +# Position eigenstates +#------------------------------------------------------------------------- + + +class XKet(Ket): + """1D cartesian position eigenket.""" + + @classmethod + def _operators_to_state(self, op, **options): + return self.__new__(self, *_lowercase_labels(op), **options) + + def _state_to_operators(self, op_class, **options): + return op_class.__new__(op_class, + *_uppercase_labels(self), **options) + + @classmethod + def default_args(self): + return ("x",) + + @classmethod + def dual_class(self): + return XBra + + @property + def position(self): + """The position of the state.""" + return self.label[0] + + def _enumerate_state(self, num_states, **options): + return _enumerate_continuous_1D(self, num_states, **options) + + def _eval_innerproduct_XBra(self, bra, **hints): + return DiracDelta(self.position - bra.position) + + def _eval_innerproduct_PxBra(self, bra, **hints): + return exp(-I*self.position*bra.momentum/hbar)/sqrt(2*pi*hbar) + + +class XBra(Bra): + """1D cartesian position eigenbra.""" + + @classmethod + def default_args(self): + return ("x",) + + @classmethod + def dual_class(self): + return XKet + + @property + def position(self): + """The position of the state.""" + return self.label[0] + + +class PositionState3D(State): + """ Base class for 3D cartesian position eigenstates """ + + @classmethod + def _operators_to_state(self, op, **options): + return self.__new__(self, *_lowercase_labels(op), **options) + + def _state_to_operators(self, op_class, **options): + return op_class.__new__(op_class, + *_uppercase_labels(self), **options) + + @classmethod + def default_args(self): + return ("x", "y", "z") + + @property + def position_x(self): + """ The x coordinate of the state """ + return self.label[0] + + @property + def position_y(self): + """ The y coordinate of the state """ + return self.label[1] + + @property + def position_z(self): + """ The z coordinate of the state """ + return self.label[2] + + +class PositionKet3D(Ket, PositionState3D): + """ 3D cartesian position eigenket """ + + def _eval_innerproduct_PositionBra3D(self, bra, **options): + x_diff = self.position_x - bra.position_x + y_diff = self.position_y - bra.position_y + z_diff = self.position_z - bra.position_z + + return DiracDelta(x_diff)*DiracDelta(y_diff)*DiracDelta(z_diff) + + @classmethod + def dual_class(self): + return PositionBra3D + + +# XXX: The type:ignore here is because mypy gives Definition of +# "_state_to_operators" in base class "PositionState3D" is incompatible with +# definition in base class "BraBase" +class PositionBra3D(Bra, PositionState3D): # type: ignore + """ 3D cartesian position eigenbra """ + + @classmethod + def dual_class(self): + return PositionKet3D + +#------------------------------------------------------------------------- +# Momentum eigenstates +#------------------------------------------------------------------------- + + +class PxKet(Ket): + """1D cartesian momentum eigenket.""" + + @classmethod + def _operators_to_state(self, op, **options): + return self.__new__(self, *_lowercase_labels(op), **options) + + def _state_to_operators(self, op_class, **options): + return op_class.__new__(op_class, + *_uppercase_labels(self), **options) + + @classmethod + def default_args(self): + return ("px",) + + @classmethod + def dual_class(self): + return PxBra + + @property + def momentum(self): + """The momentum of the state.""" + return self.label[0] + + def _enumerate_state(self, *args, **options): + return _enumerate_continuous_1D(self, *args, **options) + + def _eval_innerproduct_XBra(self, bra, **hints): + return exp(I*self.momentum*bra.position/hbar)/sqrt(2*pi*hbar) + + def _eval_innerproduct_PxBra(self, bra, **hints): + return DiracDelta(self.momentum - bra.momentum) + + +class PxBra(Bra): + """1D cartesian momentum eigenbra.""" + + @classmethod + def default_args(self): + return ("px",) + + @classmethod + def dual_class(self): + return PxKet + + @property + def momentum(self): + """The momentum of the state.""" + return self.label[0] + +#------------------------------------------------------------------------- +# Global helper functions +#------------------------------------------------------------------------- + + +def _enumerate_continuous_1D(*args, **options): + state = args[0] + num_states = args[1] + state_class = state.__class__ + index_list = options.pop('index_list', []) + + if len(index_list) == 0: + start_index = options.pop('start_index', 1) + index_list = list(range(start_index, start_index + num_states)) + + enum_states = [0 for i in range(len(index_list))] + + for i, ind in enumerate(index_list): + label = state.args[0] + enum_states[i] = state_class(str(label) + "_" + str(ind), **options) + + return enum_states + + +def _lowercase_labels(ops): + if not isinstance(ops, set): + ops = [ops] + + return [str(arg.label[0]).lower() for arg in ops] + + +def _uppercase_labels(ops): + if not isinstance(ops, set): + ops = [ops] + + new_args = [str(arg.label[0])[0].upper() + + str(arg.label[0])[1:] for arg in ops] + + return new_args diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/cg.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/cg.py new file mode 100644 index 0000000000000000000000000000000000000000..0f285cd39413a953246777c42fb6763c22a5716b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/cg.py @@ -0,0 +1,754 @@ +#TODO: +# -Implement Clebsch-Gordan symmetries +# -Improve simplification method +# -Implement new simplifications +"""Clebsch-Gordon Coefficients.""" + +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.expr import Expr +from sympy.core.function import expand +from sympy.core.mul import Mul +from sympy.core.power import Pow +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Wild, symbols) +from sympy.core.sympify import sympify +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.printing.pretty.stringpict import prettyForm, stringPict + +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.physics.wigner import clebsch_gordan, wigner_3j, wigner_6j, wigner_9j +from sympy.printing.precedence import PRECEDENCE + +__all__ = [ + 'CG', + 'Wigner3j', + 'Wigner6j', + 'Wigner9j', + 'cg_simp' +] + +#----------------------------------------------------------------------------- +# CG Coefficients +#----------------------------------------------------------------------------- + + +class Wigner3j(Expr): + """Class for the Wigner-3j symbols. + + Explanation + =========== + + Wigner 3j-symbols are coefficients determined by the coupling of + two angular momenta. When created, they are expressed as symbolic + quantities that, for numerical parameters, can be evaluated using the + ``.doit()`` method [1]_. + + Parameters + ========== + + j1, m1, j2, m2, j3, m3 : Number, Symbol + Terms determining the angular momentum of coupled angular momentum + systems. + + Examples + ======== + + Declare a Wigner-3j coefficient and calculate its value + + >>> from sympy.physics.quantum.cg import Wigner3j + >>> w3j = Wigner3j(6,0,4,0,2,0) + >>> w3j + Wigner3j(6, 0, 4, 0, 2, 0) + >>> w3j.doit() + sqrt(715)/143 + + See Also + ======== + + CG: Clebsch-Gordan coefficients + + References + ========== + + .. [1] Varshalovich, D A, Quantum Theory of Angular Momentum. 1988. + """ + + is_commutative = True + + def __new__(cls, j1, m1, j2, m2, j3, m3): + args = map(sympify, (j1, m1, j2, m2, j3, m3)) + return Expr.__new__(cls, *args) + + @property + def j1(self): + return self.args[0] + + @property + def m1(self): + return self.args[1] + + @property + def j2(self): + return self.args[2] + + @property + def m2(self): + return self.args[3] + + @property + def j3(self): + return self.args[4] + + @property + def m3(self): + return self.args[5] + + @property + def is_symbolic(self): + return not all(arg.is_number for arg in self.args) + + # This is modified from the _print_Matrix method + def _pretty(self, printer, *args): + m = ((printer._print(self.j1), printer._print(self.m1)), + (printer._print(self.j2), printer._print(self.m2)), + (printer._print(self.j3), printer._print(self.m3))) + hsep = 2 + vsep = 1 + maxw = [-1]*3 + for j in range(3): + maxw[j] = max(m[j][i].width() for i in range(2)) + D = None + for i in range(2): + D_row = None + for j in range(3): + s = m[j][i] + wdelta = maxw[j] - s.width() + wleft = wdelta //2 + wright = wdelta - wleft + + s = prettyForm(*s.right(' '*wright)) + s = prettyForm(*s.left(' '*wleft)) + + if D_row is None: + D_row = s + continue + D_row = prettyForm(*D_row.right(' '*hsep)) + D_row = prettyForm(*D_row.right(s)) + if D is None: + D = D_row + continue + for _ in range(vsep): + D = prettyForm(*D.below(' ')) + D = prettyForm(*D.below(D_row)) + D = prettyForm(*D.parens()) + return D + + def _latex(self, printer, *args): + label = map(printer._print, (self.j1, self.j2, self.j3, + self.m1, self.m2, self.m3)) + return r'\left(\begin{array}{ccc} %s & %s & %s \\ %s & %s & %s \end{array}\right)' % \ + tuple(label) + + def doit(self, **hints): + if self.is_symbolic: + raise ValueError("Coefficients must be numerical") + return wigner_3j(self.j1, self.j2, self.j3, self.m1, self.m2, self.m3) + + +class CG(Wigner3j): + r"""Class for Clebsch-Gordan coefficient. + + Explanation + =========== + + Clebsch-Gordan coefficients describe the angular momentum coupling between + two systems. The coefficients give the expansion of a coupled total angular + momentum state and an uncoupled tensor product state. The Clebsch-Gordan + coefficients are defined as [1]_: + + .. math :: + C^{j_3,m_3}_{j_1,m_1,j_2,m_2} = \left\langle j_1,m_1;j_2,m_2 | j_3,m_3\right\rangle + + Parameters + ========== + + j1, m1, j2, m2 : Number, Symbol + Angular momenta of states 1 and 2. + + j3, m3: Number, Symbol + Total angular momentum of the coupled system. + + Examples + ======== + + Define a Clebsch-Gordan coefficient and evaluate its value + + >>> from sympy.physics.quantum.cg import CG + >>> from sympy import S + >>> cg = CG(S(3)/2, S(3)/2, S(1)/2, -S(1)/2, 1, 1) + >>> cg + CG(3/2, 3/2, 1/2, -1/2, 1, 1) + >>> cg.doit() + sqrt(3)/2 + >>> CG(j1=S(1)/2, m1=-S(1)/2, j2=S(1)/2, m2=+S(1)/2, j3=1, m3=0).doit() + sqrt(2)/2 + + + Compare [2]_. + + See Also + ======== + + Wigner3j: Wigner-3j symbols + + References + ========== + + .. [1] Varshalovich, D A, Quantum Theory of Angular Momentum. 1988. + .. [2] `Clebsch-Gordan Coefficients, Spherical Harmonics, and d Functions + `_ + in P.A. Zyla *et al.* (Particle Data Group), Prog. Theor. Exp. Phys. + 2020, 083C01 (2020). + """ + precedence = PRECEDENCE["Pow"] - 1 + + def doit(self, **hints): + if self.is_symbolic: + raise ValueError("Coefficients must be numerical") + return clebsch_gordan(self.j1, self.j2, self.j3, self.m1, self.m2, self.m3) + + def _pretty(self, printer, *args): + bot = printer._print_seq( + (self.j1, self.m1, self.j2, self.m2), delimiter=',') + top = printer._print_seq((self.j3, self.m3), delimiter=',') + + pad = max(top.width(), bot.width()) + bot = prettyForm(*bot.left(' ')) + top = prettyForm(*top.left(' ')) + + if not pad == bot.width(): + bot = prettyForm(*bot.right(' '*(pad - bot.width()))) + if not pad == top.width(): + top = prettyForm(*top.right(' '*(pad - top.width()))) + s = stringPict('C' + ' '*pad) + s = prettyForm(*s.below(bot)) + s = prettyForm(*s.above(top)) + return s + + def _latex(self, printer, *args): + label = map(printer._print, (self.j3, self.m3, self.j1, + self.m1, self.j2, self.m2)) + return r'C^{%s,%s}_{%s,%s,%s,%s}' % tuple(label) + + +class Wigner6j(Expr): + """Class for the Wigner-6j symbols + + See Also + ======== + + Wigner3j: Wigner-3j symbols + + """ + def __new__(cls, j1, j2, j12, j3, j, j23): + args = map(sympify, (j1, j2, j12, j3, j, j23)) + return Expr.__new__(cls, *args) + + @property + def j1(self): + return self.args[0] + + @property + def j2(self): + return self.args[1] + + @property + def j12(self): + return self.args[2] + + @property + def j3(self): + return self.args[3] + + @property + def j(self): + return self.args[4] + + @property + def j23(self): + return self.args[5] + + @property + def is_symbolic(self): + return not all(arg.is_number for arg in self.args) + + # This is modified from the _print_Matrix method + def _pretty(self, printer, *args): + m = ((printer._print(self.j1), printer._print(self.j3)), + (printer._print(self.j2), printer._print(self.j)), + (printer._print(self.j12), printer._print(self.j23))) + hsep = 2 + vsep = 1 + maxw = [-1]*3 + for j in range(3): + maxw[j] = max(m[j][i].width() for i in range(2)) + D = None + for i in range(2): + D_row = None + for j in range(3): + s = m[j][i] + wdelta = maxw[j] - s.width() + wleft = wdelta //2 + wright = wdelta - wleft + + s = prettyForm(*s.right(' '*wright)) + s = prettyForm(*s.left(' '*wleft)) + + if D_row is None: + D_row = s + continue + D_row = prettyForm(*D_row.right(' '*hsep)) + D_row = prettyForm(*D_row.right(s)) + if D is None: + D = D_row + continue + for _ in range(vsep): + D = prettyForm(*D.below(' ')) + D = prettyForm(*D.below(D_row)) + D = prettyForm(*D.parens(left='{', right='}')) + return D + + def _latex(self, printer, *args): + label = map(printer._print, (self.j1, self.j2, self.j12, + self.j3, self.j, self.j23)) + return r'\left\{\begin{array}{ccc} %s & %s & %s \\ %s & %s & %s \end{array}\right\}' % \ + tuple(label) + + def doit(self, **hints): + if self.is_symbolic: + raise ValueError("Coefficients must be numerical") + return wigner_6j(self.j1, self.j2, self.j12, self.j3, self.j, self.j23) + + +class Wigner9j(Expr): + """Class for the Wigner-9j symbols + + See Also + ======== + + Wigner3j: Wigner-3j symbols + + """ + def __new__(cls, j1, j2, j12, j3, j4, j34, j13, j24, j): + args = map(sympify, (j1, j2, j12, j3, j4, j34, j13, j24, j)) + return Expr.__new__(cls, *args) + + @property + def j1(self): + return self.args[0] + + @property + def j2(self): + return self.args[1] + + @property + def j12(self): + return self.args[2] + + @property + def j3(self): + return self.args[3] + + @property + def j4(self): + return self.args[4] + + @property + def j34(self): + return self.args[5] + + @property + def j13(self): + return self.args[6] + + @property + def j24(self): + return self.args[7] + + @property + def j(self): + return self.args[8] + + @property + def is_symbolic(self): + return not all(arg.is_number for arg in self.args) + + # This is modified from the _print_Matrix method + def _pretty(self, printer, *args): + m = ( + (printer._print( + self.j1), printer._print(self.j3), printer._print(self.j13)), + (printer._print( + self.j2), printer._print(self.j4), printer._print(self.j24)), + (printer._print(self.j12), printer._print(self.j34), printer._print(self.j))) + hsep = 2 + vsep = 1 + maxw = [-1]*3 + for j in range(3): + maxw[j] = max(m[j][i].width() for i in range(3)) + D = None + for i in range(3): + D_row = None + for j in range(3): + s = m[j][i] + wdelta = maxw[j] - s.width() + wleft = wdelta //2 + wright = wdelta - wleft + + s = prettyForm(*s.right(' '*wright)) + s = prettyForm(*s.left(' '*wleft)) + + if D_row is None: + D_row = s + continue + D_row = prettyForm(*D_row.right(' '*hsep)) + D_row = prettyForm(*D_row.right(s)) + if D is None: + D = D_row + continue + for _ in range(vsep): + D = prettyForm(*D.below(' ')) + D = prettyForm(*D.below(D_row)) + D = prettyForm(*D.parens(left='{', right='}')) + return D + + def _latex(self, printer, *args): + label = map(printer._print, (self.j1, self.j2, self.j12, self.j3, + self.j4, self.j34, self.j13, self.j24, self.j)) + return r'\left\{\begin{array}{ccc} %s & %s & %s \\ %s & %s & %s \\ %s & %s & %s \end{array}\right\}' % \ + tuple(label) + + def doit(self, **hints): + if self.is_symbolic: + raise ValueError("Coefficients must be numerical") + return wigner_9j(self.j1, self.j2, self.j12, self.j3, self.j4, self.j34, self.j13, self.j24, self.j) + + +def cg_simp(e): + """Simplify and combine CG coefficients. + + Explanation + =========== + + This function uses various symmetry and properties of sums and + products of Clebsch-Gordan coefficients to simplify statements + involving these terms [1]_. + + Examples + ======== + + Simplify the sum over CG(a,alpha,0,0,a,alpha) for all alpha to + 2*a+1 + + >>> from sympy.physics.quantum.cg import CG, cg_simp + >>> a = CG(1,1,0,0,1,1) + >>> b = CG(1,0,0,0,1,0) + >>> c = CG(1,-1,0,0,1,-1) + >>> cg_simp(a+b+c) + 3 + + See Also + ======== + + CG: Clebsh-Gordan coefficients + + References + ========== + + .. [1] Varshalovich, D A, Quantum Theory of Angular Momentum. 1988. + """ + if isinstance(e, Add): + return _cg_simp_add(e) + elif isinstance(e, Sum): + return _cg_simp_sum(e) + elif isinstance(e, Mul): + return Mul(*[cg_simp(arg) for arg in e.args]) + elif isinstance(e, Pow): + return Pow(cg_simp(e.base), e.exp) + else: + return e + + +def _cg_simp_add(e): + #TODO: Improve simplification method + """Takes a sum of terms involving Clebsch-Gordan coefficients and + simplifies the terms. + + Explanation + =========== + + First, we create two lists, cg_part, which is all the terms involving CG + coefficients, and other_part, which is all other terms. The cg_part list + is then passed to the simplification methods, which return the new cg_part + and any additional terms that are added to other_part + """ + cg_part = [] + other_part = [] + + e = expand(e) + for arg in e.args: + if arg.has(CG): + if isinstance(arg, Sum): + other_part.append(_cg_simp_sum(arg)) + elif isinstance(arg, Mul): + terms = 1 + for term in arg.args: + if isinstance(term, Sum): + terms *= _cg_simp_sum(term) + else: + terms *= term + if terms.has(CG): + cg_part.append(terms) + else: + other_part.append(terms) + else: + cg_part.append(arg) + else: + other_part.append(arg) + + cg_part, other = _check_varsh_871_1(cg_part) + other_part.append(other) + cg_part, other = _check_varsh_871_2(cg_part) + other_part.append(other) + cg_part, other = _check_varsh_872_9(cg_part) + other_part.append(other) + return Add(*cg_part) + Add(*other_part) + + +def _check_varsh_871_1(term_list): + # Sum( CG(a,alpha,b,0,a,alpha), (alpha, -a, a)) == KroneckerDelta(b,0) + a, alpha, b, lt = map(Wild, ('a', 'alpha', 'b', 'lt')) + expr = lt*CG(a, alpha, b, 0, a, alpha) + simp = (2*a + 1)*KroneckerDelta(b, 0) + sign = lt/abs(lt) + build_expr = 2*a + 1 + index_expr = a + alpha + return _check_cg_simp(expr, simp, sign, lt, term_list, (a, alpha, b, lt), (a, b), build_expr, index_expr) + + +def _check_varsh_871_2(term_list): + # Sum((-1)**(a-alpha)*CG(a,alpha,a,-alpha,c,0),(alpha,-a,a)) + a, alpha, c, lt = map(Wild, ('a', 'alpha', 'c', 'lt')) + expr = lt*CG(a, alpha, a, -alpha, c, 0) + simp = sqrt(2*a + 1)*KroneckerDelta(c, 0) + sign = (-1)**(a - alpha)*lt/abs(lt) + build_expr = 2*a + 1 + index_expr = a + alpha + return _check_cg_simp(expr, simp, sign, lt, term_list, (a, alpha, c, lt), (a, c), build_expr, index_expr) + + +def _check_varsh_872_9(term_list): + # Sum( CG(a,alpha,b,beta,c,gamma)*CG(a,alpha',b,beta',c,gamma), (gamma, -c, c), (c, abs(a-b), a+b)) + a, alpha, alphap, b, beta, betap, c, gamma, lt = map(Wild, ( + 'a', 'alpha', 'alphap', 'b', 'beta', 'betap', 'c', 'gamma', 'lt')) + # Case alpha==alphap, beta==betap + + # For numerical alpha,beta + expr = lt*CG(a, alpha, b, beta, c, gamma)**2 + simp = S.One + sign = lt/abs(lt) + x = abs(a - b) + y = abs(alpha + beta) + build_expr = a + b + 1 - Piecewise((x, x > y), (0, Eq(x, y)), (y, y > x)) + index_expr = a + b - c + term_list, other1 = _check_cg_simp(expr, simp, sign, lt, term_list, (a, alpha, b, beta, c, gamma, lt), (a, alpha, b, beta), build_expr, index_expr) + + # For symbolic alpha,beta + x = abs(a - b) + y = a + b + build_expr = (y + 1 - x)*(x + y + 1) + index_expr = (c - x)*(x + c) + c + gamma + term_list, other2 = _check_cg_simp(expr, simp, sign, lt, term_list, (a, alpha, b, beta, c, gamma, lt), (a, alpha, b, beta), build_expr, index_expr) + + # Case alpha!=alphap or beta!=betap + # Note: this only works with leading term of 1, pattern matching is unable to match when there is a Wild leading term + # For numerical alpha,alphap,beta,betap + expr = CG(a, alpha, b, beta, c, gamma)*CG(a, alphap, b, betap, c, gamma) + simp = KroneckerDelta(alpha, alphap)*KroneckerDelta(beta, betap) + sign = S.One + x = abs(a - b) + y = abs(alpha + beta) + build_expr = a + b + 1 - Piecewise((x, x > y), (0, Eq(x, y)), (y, y > x)) + index_expr = a + b - c + term_list, other3 = _check_cg_simp(expr, simp, sign, S.One, term_list, (a, alpha, alphap, b, beta, betap, c, gamma), (a, alpha, alphap, b, beta, betap), build_expr, index_expr) + + # For symbolic alpha,alphap,beta,betap + x = abs(a - b) + y = a + b + build_expr = (y + 1 - x)*(x + y + 1) + index_expr = (c - x)*(x + c) + c + gamma + term_list, other4 = _check_cg_simp(expr, simp, sign, S.One, term_list, (a, alpha, alphap, b, beta, betap, c, gamma), (a, alpha, alphap, b, beta, betap), build_expr, index_expr) + + return term_list, other1 + other2 + other4 + + +def _check_cg_simp(expr, simp, sign, lt, term_list, variables, dep_variables, build_index_expr, index_expr): + """ Checks for simplifications that can be made, returning a tuple of the + simplified list of terms and any terms generated by simplification. + + Parameters + ========== + + expr: expression + The expression with Wild terms that will be matched to the terms in + the sum + + simp: expression + The expression with Wild terms that is substituted in place of the CG + terms in the case of simplification + + sign: expression + The expression with Wild terms denoting the sign that is on expr that + must match + + lt: expression + The expression with Wild terms that gives the leading term of the + matched expr + + term_list: list + A list of all of the terms is the sum to be simplified + + variables: list + A list of all the variables that appears in expr + + dep_variables: list + A list of the variables that must match for all the terms in the sum, + i.e. the dependent variables + + build_index_expr: expression + Expression with Wild terms giving the number of elements in cg_index + + index_expr: expression + Expression with Wild terms giving the index terms have when storing + them to cg_index + + """ + other_part = 0 + i = 0 + while i < len(term_list): + sub_1 = _check_cg(term_list[i], expr, len(variables)) + if sub_1 is None: + i += 1 + continue + if not build_index_expr.subs(sub_1).is_number: + i += 1 + continue + sub_dep = [(x, sub_1[x]) for x in dep_variables] + cg_index = [None]*build_index_expr.subs(sub_1) + for j in range(i, len(term_list)): + sub_2 = _check_cg(term_list[j], expr.subs(sub_dep), len(variables) - len(dep_variables), sign=(sign.subs(sub_1), sign.subs(sub_dep))) + if sub_2 is None: + continue + if not index_expr.subs(sub_dep).subs(sub_2).is_number: + continue + cg_index[index_expr.subs(sub_dep).subs(sub_2)] = j, expr.subs(lt, 1).subs(sub_dep).subs(sub_2), lt.subs(sub_2), sign.subs(sub_dep).subs(sub_2) + if not any(i is None for i in cg_index): + min_lt = min(*[ abs(term[2]) for term in cg_index ]) + indices = [ term[0] for term in cg_index] + indices.sort() + indices.reverse() + [ term_list.pop(j) for j in indices ] + for term in cg_index: + if abs(term[2]) > min_lt: + term_list.append( (term[2] - min_lt*term[3])*term[1] ) + other_part += min_lt*(sign*simp).subs(sub_1) + else: + i += 1 + return term_list, other_part + + +def _check_cg(cg_term, expr, length, sign=None): + """Checks whether a term matches the given expression""" + # TODO: Check for symmetries + matches = cg_term.match(expr) + if matches is None: + return + if sign is not None: + if not isinstance(sign, tuple): + raise TypeError('sign must be a tuple') + if not sign[0] == (sign[1]).subs(matches): + return + if len(matches) == length: + return matches + + +def _cg_simp_sum(e): + e = _check_varsh_sum_871_1(e) + e = _check_varsh_sum_871_2(e) + e = _check_varsh_sum_872_4(e) + return e + + +def _check_varsh_sum_871_1(e): + a = Wild('a') + alpha = symbols('alpha') + b = Wild('b') + match = e.match(Sum(CG(a, alpha, b, 0, a, alpha), (alpha, -a, a))) + if match is not None and len(match) == 2: + return ((2*a + 1)*KroneckerDelta(b, 0)).subs(match) + return e + + +def _check_varsh_sum_871_2(e): + a = Wild('a') + alpha = symbols('alpha') + c = Wild('c') + match = e.match( + Sum((-1)**(a - alpha)*CG(a, alpha, a, -alpha, c, 0), (alpha, -a, a))) + if match is not None and len(match) == 2: + return (sqrt(2*a + 1)*KroneckerDelta(c, 0)).subs(match) + return e + + +def _check_varsh_sum_872_4(e): + alpha = symbols('alpha') + beta = symbols('beta') + a = Wild('a') + b = Wild('b') + c = Wild('c') + cp = Wild('cp') + gamma = Wild('gamma') + gammap = Wild('gammap') + cg1 = CG(a, alpha, b, beta, c, gamma) + cg2 = CG(a, alpha, b, beta, cp, gammap) + match1 = e.match(Sum(cg1*cg2, (alpha, -a, a), (beta, -b, b))) + if match1 is not None and len(match1) == 6: + return (KroneckerDelta(c, cp)*KroneckerDelta(gamma, gammap)).subs(match1) + match2 = e.match(Sum(cg1**2, (alpha, -a, a), (beta, -b, b))) + if match2 is not None and len(match2) == 4: + return S.One + return e + + +def _cg_list(term): + if isinstance(term, CG): + return (term,), 1, 1 + cg = [] + coeff = 1 + if not isinstance(term, (Mul, Pow)): + raise NotImplementedError('term must be CG, Add, Mul or Pow') + if isinstance(term, Pow) and term.exp.is_number: + if term.exp.is_number: + [ cg.append(term.base) for _ in range(term.exp) ] + else: + return (term,), 1, 1 + if isinstance(term, Mul): + for arg in term.args: + if isinstance(arg, CG): + cg.append(arg) + else: + coeff *= arg + return cg, coeff, coeff/abs(coeff) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/circuitplot.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/circuitplot.py new file mode 100644 index 0000000000000000000000000000000000000000..316a4be613b2e275565999130c06ea678acd8b96 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/circuitplot.py @@ -0,0 +1,370 @@ +"""Matplotlib based plotting of quantum circuits. + +Todo: + +* Optimize printing of large circuits. +* Get this to work with single gates. +* Do a better job checking the form of circuits to make sure it is a Mul of + Gates. +* Get multi-target gates plotting. +* Get initial and final states to plot. +* Get measurements to plot. Might need to rethink measurement as a gate + issue. +* Get scale and figsize to be handled in a better way. +* Write some tests/examples! +""" + +from __future__ import annotations + +from sympy.core.mul import Mul +from sympy.external import import_module +from sympy.physics.quantum.gate import Gate, OneQubitGate, CGate, CGateS + + +__all__ = [ + 'CircuitPlot', + 'circuit_plot', + 'labeller', + 'Mz', + 'Mx', + 'CreateOneQubitGate', + 'CreateCGate', +] + +np = import_module('numpy') +matplotlib = import_module( + 'matplotlib', import_kwargs={'fromlist': ['pyplot']}, + catch=(RuntimeError,)) # This is raised in environments that have no display. + +if np and matplotlib: + pyplot = matplotlib.pyplot + Line2D = matplotlib.lines.Line2D + Circle = matplotlib.patches.Circle + +#from matplotlib import rc +#rc('text',usetex=True) + +class CircuitPlot: + """A class for managing a circuit plot.""" + + scale = 1.0 + fontsize = 20.0 + linewidth = 1.0 + control_radius = 0.05 + not_radius = 0.15 + swap_delta = 0.05 + labels: list[str] = [] + inits: dict[str, str] = {} + label_buffer = 0.5 + + def __init__(self, c, nqubits, **kwargs): + if not np or not matplotlib: + raise ImportError('numpy or matplotlib not available.') + self.circuit = c + self.ngates = len(self.circuit.args) + self.nqubits = nqubits + self.update(kwargs) + self._create_grid() + self._create_figure() + self._plot_wires() + self._plot_gates() + self._finish() + + def update(self, kwargs): + """Load the kwargs into the instance dict.""" + self.__dict__.update(kwargs) + + def _create_grid(self): + """Create the grid of wires.""" + scale = self.scale + wire_grid = np.arange(0.0, self.nqubits*scale, scale, dtype=float) + gate_grid = np.arange(0.0, self.ngates*scale, scale, dtype=float) + self._wire_grid = wire_grid + self._gate_grid = gate_grid + + def _create_figure(self): + """Create the main matplotlib figure.""" + self._figure = pyplot.figure( + figsize=(self.ngates*self.scale, self.nqubits*self.scale), + facecolor='w', + edgecolor='w' + ) + ax = self._figure.add_subplot( + 1, 1, 1, + frameon=True + ) + ax.set_axis_off() + offset = 0.5*self.scale + ax.set_xlim(self._gate_grid[0] - offset, self._gate_grid[-1] + offset) + ax.set_ylim(self._wire_grid[0] - offset, self._wire_grid[-1] + offset) + ax.set_aspect('equal') + self._axes = ax + + def _plot_wires(self): + """Plot the wires of the circuit diagram.""" + xstart = self._gate_grid[0] + xstop = self._gate_grid[-1] + xdata = (xstart - self.scale, xstop + self.scale) + for i in range(self.nqubits): + ydata = (self._wire_grid[i], self._wire_grid[i]) + line = Line2D( + xdata, ydata, + color='k', + lw=self.linewidth + ) + self._axes.add_line(line) + if self.labels: + init_label_buffer = 0 + if self.inits.get(self.labels[i]): init_label_buffer = 0.25 + self._axes.text( + xdata[0]-self.label_buffer-init_label_buffer,ydata[0], + render_label(self.labels[i],self.inits), + size=self.fontsize, + color='k',ha='center',va='center') + self._plot_measured_wires() + + def _plot_measured_wires(self): + ismeasured = self._measurements() + xstop = self._gate_grid[-1] + dy = 0.04 # amount to shift wires when doubled + # Plot doubled wires after they are measured + for im in ismeasured: + xdata = (self._gate_grid[ismeasured[im]],xstop+self.scale) + ydata = (self._wire_grid[im]+dy,self._wire_grid[im]+dy) + line = Line2D( + xdata, ydata, + color='k', + lw=self.linewidth + ) + self._axes.add_line(line) + # Also double any controlled lines off these wires + for i,g in enumerate(self._gates()): + if isinstance(g, (CGate, CGateS)): + wires = g.controls + g.targets + for wire in wires: + if wire in ismeasured and \ + self._gate_grid[i] > self._gate_grid[ismeasured[wire]]: + ydata = min(wires), max(wires) + xdata = self._gate_grid[i]-dy, self._gate_grid[i]-dy + line = Line2D( + xdata, ydata, + color='k', + lw=self.linewidth + ) + self._axes.add_line(line) + def _gates(self): + """Create a list of all gates in the circuit plot.""" + gates = [] + if isinstance(self.circuit, Mul): + for g in reversed(self.circuit.args): + if isinstance(g, Gate): + gates.append(g) + elif isinstance(self.circuit, Gate): + gates.append(self.circuit) + return gates + + def _plot_gates(self): + """Iterate through the gates and plot each of them.""" + for i, gate in enumerate(self._gates()): + gate.plot_gate(self, i) + + def _measurements(self): + """Return a dict ``{i:j}`` where i is the index of the wire that has + been measured, and j is the gate where the wire is measured. + """ + ismeasured = {} + for i,g in enumerate(self._gates()): + if getattr(g,'measurement',False): + for target in g.targets: + if target in ismeasured: + if ismeasured[target] > i: + ismeasured[target] = i + else: + ismeasured[target] = i + return ismeasured + + def _finish(self): + # Disable clipping to make panning work well for large circuits. + for o in self._figure.findobj(): + o.set_clip_on(False) + + def one_qubit_box(self, t, gate_idx, wire_idx): + """Draw a box for a single qubit gate.""" + x = self._gate_grid[gate_idx] + y = self._wire_grid[wire_idx] + self._axes.text( + x, y, t, + color='k', + ha='center', + va='center', + bbox={"ec": 'k', "fc": 'w', "fill": True, "lw": self.linewidth}, + size=self.fontsize + ) + + def two_qubit_box(self, t, gate_idx, wire_idx): + """Draw a box for a two qubit gate. Does not work yet. + """ + # x = self._gate_grid[gate_idx] + # y = self._wire_grid[wire_idx]+0.5 + print(self._gate_grid) + print(self._wire_grid) + # unused: + # obj = self._axes.text( + # x, y, t, + # color='k', + # ha='center', + # va='center', + # bbox=dict(ec='k', fc='w', fill=True, lw=self.linewidth), + # size=self.fontsize + # ) + + def control_line(self, gate_idx, min_wire, max_wire): + """Draw a vertical control line.""" + xdata = (self._gate_grid[gate_idx], self._gate_grid[gate_idx]) + ydata = (self._wire_grid[min_wire], self._wire_grid[max_wire]) + line = Line2D( + xdata, ydata, + color='k', + lw=self.linewidth + ) + self._axes.add_line(line) + + def control_point(self, gate_idx, wire_idx): + """Draw a control point.""" + x = self._gate_grid[gate_idx] + y = self._wire_grid[wire_idx] + radius = self.control_radius + c = Circle( + (x, y), + radius*self.scale, + ec='k', + fc='k', + fill=True, + lw=self.linewidth + ) + self._axes.add_patch(c) + + def not_point(self, gate_idx, wire_idx): + """Draw a NOT gates as the circle with plus in the middle.""" + x = self._gate_grid[gate_idx] + y = self._wire_grid[wire_idx] + radius = self.not_radius + c = Circle( + (x, y), + radius, + ec='k', + fc='w', + fill=False, + lw=self.linewidth + ) + self._axes.add_patch(c) + l = Line2D( + (x, x), (y - radius, y + radius), + color='k', + lw=self.linewidth + ) + self._axes.add_line(l) + + def swap_point(self, gate_idx, wire_idx): + """Draw a swap point as a cross.""" + x = self._gate_grid[gate_idx] + y = self._wire_grid[wire_idx] + d = self.swap_delta + l1 = Line2D( + (x - d, x + d), + (y - d, y + d), + color='k', + lw=self.linewidth + ) + l2 = Line2D( + (x - d, x + d), + (y + d, y - d), + color='k', + lw=self.linewidth + ) + self._axes.add_line(l1) + self._axes.add_line(l2) + +def circuit_plot(c, nqubits, **kwargs): + """Draw the circuit diagram for the circuit with nqubits. + + Parameters + ========== + + c : circuit + The circuit to plot. Should be a product of Gate instances. + nqubits : int + The number of qubits to include in the circuit. Must be at least + as big as the largest ``min_qubits`` of the gates. + """ + return CircuitPlot(c, nqubits, **kwargs) + +def render_label(label, inits={}): + """Slightly more flexible way to render labels. + + >>> from sympy.physics.quantum.circuitplot import render_label + >>> render_label('q0') + '$\\\\left|q0\\\\right\\\\rangle$' + >>> render_label('q0', {'q0':'0'}) + '$\\\\left|q0\\\\right\\\\rangle=\\\\left|0\\\\right\\\\rangle$' + """ + init = inits.get(label) + if init: + return r'$\left|%s\right\rangle=\left|%s\right\rangle$' % (label, init) + return r'$\left|%s\right\rangle$' % label + +def labeller(n, symbol='q'): + """Autogenerate labels for wires of quantum circuits. + + Parameters + ========== + + n : int + number of qubits in the circuit. + symbol : string + A character string to precede all gate labels. E.g. 'q_0', 'q_1', etc. + + >>> from sympy.physics.quantum.circuitplot import labeller + >>> labeller(2) + ['q_1', 'q_0'] + >>> labeller(3,'j') + ['j_2', 'j_1', 'j_0'] + """ + return ['%s_%d' % (symbol,n-i-1) for i in range(n)] + +class Mz(OneQubitGate): + """Mock-up of a z measurement gate. + + This is in circuitplot rather than gate.py because it's not a real + gate, it just draws one. + """ + measurement = True + gate_name='Mz' + gate_name_latex='M_z' + +class Mx(OneQubitGate): + """Mock-up of an x measurement gate. + + This is in circuitplot rather than gate.py because it's not a real + gate, it just draws one. + """ + measurement = True + gate_name='Mx' + gate_name_latex='M_x' + +class CreateOneQubitGate(type): + def __new__(mcl, name, latexname=None): + if not latexname: + latexname = name + return type(name + "Gate", (OneQubitGate,), + {'gate_name': name, 'gate_name_latex': latexname}) + +def CreateCGate(name, latexname=None): + """Use a lexical closure to make a controlled gate. + """ + if not latexname: + latexname = name + onequbitgate = CreateOneQubitGate(name, latexname) + def ControlledGate(ctrls,target): + return CGate(tuple(ctrls),onequbitgate(target)) + return ControlledGate diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/circuitutils.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/circuitutils.py new file mode 100644 index 0000000000000000000000000000000000000000..84955d3d724a2658f2dc3b26738133bd46f1aa57 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/circuitutils.py @@ -0,0 +1,488 @@ +"""Primitive circuit operations on quantum circuits.""" + +from functools import reduce + +from sympy.core.sorting import default_sort_key +from sympy.core.containers import Tuple +from sympy.core.mul import Mul +from sympy.core.symbol import Symbol +from sympy.core.sympify import sympify +from sympy.utilities import numbered_symbols +from sympy.physics.quantum.gate import Gate + +__all__ = [ + 'kmp_table', + 'find_subcircuit', + 'replace_subcircuit', + 'convert_to_symbolic_indices', + 'convert_to_real_indices', + 'random_reduce', + 'random_insert' +] + + +def kmp_table(word): + """Build the 'partial match' table of the Knuth-Morris-Pratt algorithm. + + Note: This is applicable to strings or + quantum circuits represented as tuples. + """ + + # Current position in subcircuit + pos = 2 + # Beginning position of candidate substring that + # may reappear later in word + cnd = 0 + # The 'partial match' table that helps one determine + # the next location to start substring search + table = [] + table.append(-1) + table.append(0) + + while pos < len(word): + if word[pos - 1] == word[cnd]: + cnd = cnd + 1 + table.append(cnd) + pos = pos + 1 + elif cnd > 0: + cnd = table[cnd] + else: + table.append(0) + pos = pos + 1 + + return table + + +def find_subcircuit(circuit, subcircuit, start=0, end=0): + """Finds the subcircuit in circuit, if it exists. + + Explanation + =========== + + If the subcircuit exists, the index of the start of + the subcircuit in circuit is returned; otherwise, + -1 is returned. The algorithm that is implemented + is the Knuth-Morris-Pratt algorithm. + + Parameters + ========== + + circuit : tuple, Gate or Mul + A tuple of Gates or Mul representing a quantum circuit + subcircuit : tuple, Gate or Mul + A tuple of Gates or Mul to find in circuit + start : int + The location to start looking for subcircuit. + If start is the same or past end, -1 is returned. + end : int + The last place to look for a subcircuit. If end + is less than 1 (one), then the length of circuit + is taken to be end. + + Examples + ======== + + Find the first instance of a subcircuit: + + >>> from sympy.physics.quantum.circuitutils import find_subcircuit + >>> from sympy.physics.quantum.gate import X, Y, Z, H + >>> circuit = X(0)*Z(0)*Y(0)*H(0) + >>> subcircuit = Z(0)*Y(0) + >>> find_subcircuit(circuit, subcircuit) + 1 + + Find the first instance starting at a specific position: + + >>> find_subcircuit(circuit, subcircuit, start=1) + 1 + + >>> find_subcircuit(circuit, subcircuit, start=2) + -1 + + >>> circuit = circuit*subcircuit + >>> find_subcircuit(circuit, subcircuit, start=2) + 4 + + Find the subcircuit within some interval: + + >>> find_subcircuit(circuit, subcircuit, start=2, end=2) + -1 + """ + + if isinstance(circuit, Mul): + circuit = circuit.args + + if isinstance(subcircuit, Mul): + subcircuit = subcircuit.args + + if len(subcircuit) == 0 or len(subcircuit) > len(circuit): + return -1 + + if end < 1: + end = len(circuit) + + # Location in circuit + pos = start + # Location in the subcircuit + index = 0 + # 'Partial match' table + table = kmp_table(subcircuit) + + while (pos + index) < end: + if subcircuit[index] == circuit[pos + index]: + index = index + 1 + else: + pos = pos + index - table[index] + index = table[index] if table[index] > -1 else 0 + + if index == len(subcircuit): + return pos + + return -1 + + +def replace_subcircuit(circuit, subcircuit, replace=None, pos=0): + """Replaces a subcircuit with another subcircuit in circuit, + if it exists. + + Explanation + =========== + + If multiple instances of subcircuit exists, the first instance is + replaced. The position to being searching from (if different from + 0) may be optionally given. If subcircuit cannot be found, circuit + is returned. + + Parameters + ========== + + circuit : tuple, Gate or Mul + A quantum circuit. + subcircuit : tuple, Gate or Mul + The circuit to be replaced. + replace : tuple, Gate or Mul + The replacement circuit. + pos : int + The location to start search and replace + subcircuit, if it exists. This may be used + if it is known beforehand that multiple + instances exist, and it is desirable to + replace a specific instance. If a negative number + is given, pos will be defaulted to 0. + + Examples + ======== + + Find and remove the subcircuit: + + >>> from sympy.physics.quantum.circuitutils import replace_subcircuit + >>> from sympy.physics.quantum.gate import X, Y, Z, H + >>> circuit = X(0)*Z(0)*Y(0)*H(0)*X(0)*H(0)*Y(0) + >>> subcircuit = Z(0)*Y(0) + >>> replace_subcircuit(circuit, subcircuit) + (X(0), H(0), X(0), H(0), Y(0)) + + Remove the subcircuit given a starting search point: + + >>> replace_subcircuit(circuit, subcircuit, pos=1) + (X(0), H(0), X(0), H(0), Y(0)) + + >>> replace_subcircuit(circuit, subcircuit, pos=2) + (X(0), Z(0), Y(0), H(0), X(0), H(0), Y(0)) + + Replace the subcircuit: + + >>> replacement = H(0)*Z(0) + >>> replace_subcircuit(circuit, subcircuit, replace=replacement) + (X(0), H(0), Z(0), H(0), X(0), H(0), Y(0)) + """ + + if pos < 0: + pos = 0 + + if isinstance(circuit, Mul): + circuit = circuit.args + + if isinstance(subcircuit, Mul): + subcircuit = subcircuit.args + + if isinstance(replace, Mul): + replace = replace.args + elif replace is None: + replace = () + + # Look for the subcircuit starting at pos + loc = find_subcircuit(circuit, subcircuit, start=pos) + + # If subcircuit was found + if loc > -1: + # Get the gates to the left of subcircuit + left = circuit[0:loc] + # Get the gates to the right of subcircuit + right = circuit[loc + len(subcircuit):len(circuit)] + # Recombine the left and right side gates into a circuit + circuit = left + replace + right + + return circuit + + +def _sympify_qubit_map(mapping): + new_map = {} + for key in mapping: + new_map[key] = sympify(mapping[key]) + return new_map + + +def convert_to_symbolic_indices(seq, start=None, gen=None, qubit_map=None): + """Returns the circuit with symbolic indices and the + dictionary mapping symbolic indices to real indices. + + The mapping is 1 to 1 and onto (bijective). + + Parameters + ========== + + seq : tuple, Gate/Integer/tuple or Mul + A tuple of Gate, Integer, or tuple objects, or a Mul + start : Symbol + An optional starting symbolic index + gen : object + An optional numbered symbol generator + qubit_map : dict + An existing mapping of symbolic indices to real indices + + All symbolic indices have the format 'i#', where # is + some number >= 0. + """ + + if isinstance(seq, Mul): + seq = seq.args + + # A numbered symbol generator + index_gen = numbered_symbols(prefix='i', start=-1) + cur_ndx = next(index_gen) + + # keys are symbolic indices; values are real indices + ndx_map = {} + + def create_inverse_map(symb_to_real_map): + rev_items = lambda item: (item[1], item[0]) + return dict(map(rev_items, symb_to_real_map.items())) + + if start is not None: + if not isinstance(start, Symbol): + msg = 'Expected Symbol for starting index, got %r.' % start + raise TypeError(msg) + cur_ndx = start + + if gen is not None: + if not isinstance(gen, numbered_symbols().__class__): + msg = 'Expected a generator, got %r.' % gen + raise TypeError(msg) + index_gen = gen + + if qubit_map is not None: + if not isinstance(qubit_map, dict): + msg = ('Expected dict for existing map, got ' + + '%r.' % qubit_map) + raise TypeError(msg) + ndx_map = qubit_map + + ndx_map = _sympify_qubit_map(ndx_map) + # keys are real indices; keys are symbolic indices + inv_map = create_inverse_map(ndx_map) + + sym_seq = () + for item in seq: + # Nested items, so recurse + if isinstance(item, Gate): + result = convert_to_symbolic_indices(item.args, + qubit_map=ndx_map, + start=cur_ndx, + gen=index_gen) + sym_item, new_map, cur_ndx, index_gen = result + ndx_map.update(new_map) + inv_map = create_inverse_map(ndx_map) + + elif isinstance(item, (tuple, Tuple)): + result = convert_to_symbolic_indices(item, + qubit_map=ndx_map, + start=cur_ndx, + gen=index_gen) + sym_item, new_map, cur_ndx, index_gen = result + ndx_map.update(new_map) + inv_map = create_inverse_map(ndx_map) + + elif item in inv_map: + sym_item = inv_map[item] + + else: + cur_ndx = next(gen) + ndx_map[cur_ndx] = item + inv_map[item] = cur_ndx + sym_item = cur_ndx + + if isinstance(item, Gate): + sym_item = item.__class__(*sym_item) + + sym_seq = sym_seq + (sym_item,) + + return sym_seq, ndx_map, cur_ndx, index_gen + + +def convert_to_real_indices(seq, qubit_map): + """Returns the circuit with real indices. + + Parameters + ========== + + seq : tuple, Gate/Integer/tuple or Mul + A tuple of Gate, Integer, or tuple objects or a Mul + qubit_map : dict + A dictionary mapping symbolic indices to real indices. + + Examples + ======== + + Change the symbolic indices to real integers: + + >>> from sympy import symbols + >>> from sympy.physics.quantum.circuitutils import convert_to_real_indices + >>> from sympy.physics.quantum.gate import X, Y, H + >>> i0, i1 = symbols('i:2') + >>> index_map = {i0 : 0, i1 : 1} + >>> convert_to_real_indices(X(i0)*Y(i1)*H(i0)*X(i1), index_map) + (X(0), Y(1), H(0), X(1)) + """ + + if isinstance(seq, Mul): + seq = seq.args + + if not isinstance(qubit_map, dict): + msg = 'Expected dict for qubit_map, got %r.' % qubit_map + raise TypeError(msg) + + qubit_map = _sympify_qubit_map(qubit_map) + real_seq = () + for item in seq: + # Nested items, so recurse + if isinstance(item, Gate): + real_item = convert_to_real_indices(item.args, qubit_map) + + elif isinstance(item, (tuple, Tuple)): + real_item = convert_to_real_indices(item, qubit_map) + + else: + real_item = qubit_map[item] + + if isinstance(item, Gate): + real_item = item.__class__(*real_item) + + real_seq = real_seq + (real_item,) + + return real_seq + + +def random_reduce(circuit, gate_ids, seed=None): + """Shorten the length of a quantum circuit. + + Explanation + =========== + + random_reduce looks for circuit identities in circuit, randomly chooses + one to remove, and returns a shorter yet equivalent circuit. If no + identities are found, the same circuit is returned. + + Parameters + ========== + + circuit : Gate tuple of Mul + A tuple of Gates representing a quantum circuit + gate_ids : list, GateIdentity + List of gate identities to find in circuit + seed : int or list + seed used for _randrange; to override the random selection, provide a + list of integers: the elements of gate_ids will be tested in the order + given by the list + + """ + from sympy.core.random import _randrange + + if not gate_ids: + return circuit + + if isinstance(circuit, Mul): + circuit = circuit.args + + ids = flatten_ids(gate_ids) + + # Create the random integer generator with the seed + randrange = _randrange(seed) + + # Look for an identity in the circuit + while ids: + i = randrange(len(ids)) + id = ids.pop(i) + if find_subcircuit(circuit, id) != -1: + break + else: + # no identity was found + return circuit + + # return circuit with the identity removed + return replace_subcircuit(circuit, id) + + +def random_insert(circuit, choices, seed=None): + """Insert a circuit into another quantum circuit. + + Explanation + =========== + + random_insert randomly chooses a location in the circuit to insert + a randomly selected circuit from amongst the given choices. + + Parameters + ========== + + circuit : Gate tuple or Mul + A tuple or Mul of Gates representing a quantum circuit + choices : list + Set of circuit choices + seed : int or list + seed used for _randrange; to override the random selections, give + a list two integers, [i, j] where i is the circuit location where + choice[j] will be inserted. + + Notes + ===== + + Indices for insertion should be [0, n] if n is the length of the + circuit. + """ + from sympy.core.random import _randrange + + if not choices: + return circuit + + if isinstance(circuit, Mul): + circuit = circuit.args + + # get the location in the circuit and the element to insert from choices + randrange = _randrange(seed) + loc = randrange(len(circuit) + 1) + choice = choices[randrange(len(choices))] + + circuit = list(circuit) + circuit[loc: loc] = choice + return tuple(circuit) + +# Flatten the GateIdentity objects (with gate rules) into one single list + + +def flatten_ids(ids): + collapse = lambda acc, an_id: acc + sorted(an_id.equivalent_ids, + key=default_sort_key) + ids = reduce(collapse, ids, []) + ids.sort(key=default_sort_key) + return ids diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/commutator.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/commutator.py new file mode 100644 index 0000000000000000000000000000000000000000..a2d97a679e27387077429a9973de21ad868e84ac --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/commutator.py @@ -0,0 +1,256 @@ +"""The commutator: [A,B] = A*B - B*A.""" + +from sympy.core.add import Add +from sympy.core.expr import Expr +from sympy.core.kind import KindDispatcher +from sympy.core.mul import Mul +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.printing.pretty.stringpict import prettyForm + +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.kind import _OperatorKind, OperatorKind + + +__all__ = [ + 'Commutator' +] + +#----------------------------------------------------------------------------- +# Commutator +#----------------------------------------------------------------------------- + + +class Commutator(Expr): + """The standard commutator, in an unevaluated state. + + Explanation + =========== + + Evaluating a commutator is defined [1]_ as: ``[A, B] = A*B - B*A``. This + class returns the commutator in an unevaluated form. To evaluate the + commutator, use the ``.doit()`` method. + + Canonical ordering of a commutator is ``[A, B]`` for ``A < B``. The + arguments of the commutator are put into canonical order using ``__cmp__``. + If ``B < A``, then ``[B, A]`` is returned as ``-[A, B]``. + + Parameters + ========== + + A : Expr + The first argument of the commutator [A,B]. + B : Expr + The second argument of the commutator [A,B]. + + Examples + ======== + + >>> from sympy.physics.quantum import Commutator, Dagger, Operator + >>> from sympy.abc import x, y + >>> A = Operator('A') + >>> B = Operator('B') + >>> C = Operator('C') + + Create a commutator and use ``.doit()`` to evaluate it: + + >>> comm = Commutator(A, B) + >>> comm + [A,B] + >>> comm.doit() + A*B - B*A + + The commutator orders it arguments in canonical order: + + >>> comm = Commutator(B, A); comm + -[A,B] + + Commutative constants are factored out: + + >>> Commutator(3*x*A, x*y*B) + 3*x**2*y*[A,B] + + Using ``.expand(commutator=True)``, the standard commutator expansion rules + can be applied: + + >>> Commutator(A+B, C).expand(commutator=True) + [A,C] + [B,C] + >>> Commutator(A, B+C).expand(commutator=True) + [A,B] + [A,C] + >>> Commutator(A*B, C).expand(commutator=True) + [A,C]*B + A*[B,C] + >>> Commutator(A, B*C).expand(commutator=True) + [A,B]*C + B*[A,C] + + Adjoint operations applied to the commutator are properly applied to the + arguments: + + >>> Dagger(Commutator(A, B)) + -[Dagger(A),Dagger(B)] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Commutator + """ + is_commutative = False + + _kind_dispatcher = KindDispatcher("Commutator_kind_dispatcher", commutative=True) + + @property + def kind(self): + arg_kinds = (a.kind for a in self.args) + return self._kind_dispatcher(*arg_kinds) + + def __new__(cls, A, B): + r = cls.eval(A, B) + if r is not None: + return r + obj = Expr.__new__(cls, A, B) + return obj + + @classmethod + def eval(cls, a, b): + if not (a and b): + return S.Zero + if a == b: + return S.Zero + if a.is_commutative or b.is_commutative: + return S.Zero + + # [xA,yB] -> xy*[A,B] + ca, nca = a.args_cnc() + cb, ncb = b.args_cnc() + c_part = ca + cb + if c_part: + return Mul(Mul(*c_part), cls(Mul._from_args(nca), Mul._from_args(ncb))) + + # Canonical ordering of arguments + # The Commutator [A, B] is in canonical form if A < B. + if a.compare(b) == 1: + return S.NegativeOne*cls(b, a) + + def _expand_pow(self, A, B, sign): + exp = A.exp + if not exp.is_integer or not exp.is_constant() or abs(exp) <= 1: + # nothing to do + return self + base = A.base + if exp.is_negative: + base = A.base**-1 + exp = -exp + comm = Commutator(base, B).expand(commutator=True) + + result = base**(exp - 1) * comm + for i in range(1, exp): + result += base**(exp - 1 - i) * comm * base**i + return sign*result.expand() + + def _eval_expand_commutator(self, **hints): + A = self.args[0] + B = self.args[1] + + if isinstance(A, Add): + # [A + B, C] -> [A, C] + [B, C] + sargs = [] + for term in A.args: + comm = Commutator(term, B) + if isinstance(comm, Commutator): + comm = comm._eval_expand_commutator() + sargs.append(comm) + return Add(*sargs) + elif isinstance(B, Add): + # [A, B + C] -> [A, B] + [A, C] + sargs = [] + for term in B.args: + comm = Commutator(A, term) + if isinstance(comm, Commutator): + comm = comm._eval_expand_commutator() + sargs.append(comm) + return Add(*sargs) + elif isinstance(A, Mul): + # [A*B, C] -> A*[B, C] + [A, C]*B + a = A.args[0] + b = Mul(*A.args[1:]) + c = B + comm1 = Commutator(b, c) + comm2 = Commutator(a, c) + if isinstance(comm1, Commutator): + comm1 = comm1._eval_expand_commutator() + if isinstance(comm2, Commutator): + comm2 = comm2._eval_expand_commutator() + first = Mul(a, comm1) + second = Mul(comm2, b) + return Add(first, second) + elif isinstance(B, Mul): + # [A, B*C] -> [A, B]*C + B*[A, C] + a = A + b = B.args[0] + c = Mul(*B.args[1:]) + comm1 = Commutator(a, b) + comm2 = Commutator(a, c) + if isinstance(comm1, Commutator): + comm1 = comm1._eval_expand_commutator() + if isinstance(comm2, Commutator): + comm2 = comm2._eval_expand_commutator() + first = Mul(comm1, c) + second = Mul(b, comm2) + return Add(first, second) + elif isinstance(A, Pow): + # [A**n, C] -> A**(n - 1)*[A, C] + A**(n - 2)*[A, C]*A + ... + [A, C]*A**(n-1) + return self._expand_pow(A, B, 1) + elif isinstance(B, Pow): + # [A, C**n] -> C**(n - 1)*[C, A] + C**(n - 2)*[C, A]*C + ... + [C, A]*C**(n-1) + return self._expand_pow(B, A, -1) + + # No changes, so return self + return self + + def doit(self, **hints): + """ Evaluate commutator """ + # Keep the import of Operator here to avoid problems with + # circular imports. + from sympy.physics.quantum.operator import Operator + A = self.args[0] + B = self.args[1] + if isinstance(A, Operator) and isinstance(B, Operator): + try: + comm = A._eval_commutator(B, **hints) + except NotImplementedError: + try: + comm = -1*B._eval_commutator(A, **hints) + except NotImplementedError: + comm = None + if comm is not None: + return comm.doit(**hints) + return (A*B - B*A).doit(**hints) + + def _eval_adjoint(self): + return Commutator(Dagger(self.args[1]), Dagger(self.args[0])) + + def _sympyrepr(self, printer, *args): + return "%s(%s,%s)" % ( + self.__class__.__name__, printer._print( + self.args[0]), printer._print(self.args[1]) + ) + + def _sympystr(self, printer, *args): + return "[%s,%s]" % ( + printer._print(self.args[0]), printer._print(self.args[1])) + + def _pretty(self, printer, *args): + pform = printer._print(self.args[0], *args) + pform = prettyForm(*pform.right(prettyForm(','))) + pform = prettyForm(*pform.right(printer._print(self.args[1], *args))) + pform = prettyForm(*pform.parens(left='[', right=']')) + return pform + + def _latex(self, printer, *args): + return "\\left[%s,%s\\right]" % tuple([ + printer._print(arg, *args) for arg in self.args]) + + +@Commutator._kind_dispatcher.register(_OperatorKind, _OperatorKind) +def find_op_kind(e1, e2): + """Find the kind of an anticommutator of two OperatorKinds.""" + return OperatorKind diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/constants.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..3e848bf24e95e3bd612169128a1845202066c6e9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/constants.py @@ -0,0 +1,59 @@ +"""Constants (like hbar) related to quantum mechanics.""" + +from sympy.core.numbers import NumberSymbol +from sympy.core.singleton import Singleton +from sympy.printing.pretty.stringpict import prettyForm +import mpmath.libmp as mlib + +#----------------------------------------------------------------------------- +# Constants +#----------------------------------------------------------------------------- + +__all__ = [ + 'hbar', + 'HBar', +] + + +class HBar(NumberSymbol, metaclass=Singleton): + """Reduced Plank's constant in numerical and symbolic form [1]_. + + Examples + ======== + + >>> from sympy.physics.quantum.constants import hbar + >>> hbar.evalf() + 1.05457162000000e-34 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Planck_constant + """ + + is_real = True + is_positive = True + is_negative = False + is_irrational = True + + __slots__ = () + + def _as_mpf_val(self, prec): + return mlib.from_float(1.05457162e-34, prec) + + def _sympyrepr(self, printer, *args): + return 'HBar()' + + def _sympystr(self, printer, *args): + return 'hbar' + + def _pretty(self, printer, *args): + if printer._use_unicode: + return prettyForm('\N{PLANCK CONSTANT OVER TWO PI}') + return prettyForm('hbar') + + def _latex(self, printer, *args): + return r'\hbar' + +# Create an instance for everyone to use. +hbar = HBar() diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/dagger.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/dagger.py new file mode 100644 index 0000000000000000000000000000000000000000..f96f01e3b9ac86ae30b03e3b97293bbafceaed8a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/dagger.py @@ -0,0 +1,95 @@ +"""Hermitian conjugation.""" + +from sympy.core import Expr, sympify +from sympy.functions.elementary.complexes import adjoint + +__all__ = [ + 'Dagger' +] + + +class Dagger(adjoint): + """General Hermitian conjugate operation. + + Explanation + =========== + + Take the Hermetian conjugate of an argument [1]_. For matrices this + operation is equivalent to transpose and complex conjugate [2]_. + + Parameters + ========== + + arg : Expr + The SymPy expression that we want to take the dagger of. + evaluate : bool + Whether the resulting expression should be directly evaluated. + + Examples + ======== + + Daggering various quantum objects: + + >>> from sympy.physics.quantum.dagger import Dagger + >>> from sympy.physics.quantum.state import Ket, Bra + >>> from sympy.physics.quantum.operator import Operator + >>> Dagger(Ket('psi')) + >> Dagger(Bra('phi')) + |phi> + >>> Dagger(Operator('A')) + Dagger(A) + + Inner and outer products:: + + >>> from sympy.physics.quantum import InnerProduct, OuterProduct + >>> Dagger(InnerProduct(Bra('a'), Ket('b'))) + + >>> Dagger(OuterProduct(Ket('a'), Bra('b'))) + |b>>> A = Operator('A') + >>> B = Operator('B') + >>> Dagger(A*B) + Dagger(B)*Dagger(A) + >>> Dagger(A+B) + Dagger(A) + Dagger(B) + >>> Dagger(A**2) + Dagger(A)**2 + + Dagger also seamlessly handles complex numbers and matrices:: + + >>> from sympy import Matrix, I + >>> m = Matrix([[1,I],[2,I]]) + >>> m + Matrix([ + [1, I], + [2, I]]) + >>> Dagger(m) + Matrix([ + [ 1, 2], + [-I, -I]]) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hermitian_adjoint + .. [2] https://en.wikipedia.org/wiki/Hermitian_transpose + """ + + @property + def kind(self): + """Find the kind of a dagger of something (just the kind of the something).""" + return self.args[0].kind + + def __new__(cls, arg, evaluate=True): + if hasattr(arg, 'adjoint') and evaluate: + return arg.adjoint() + elif hasattr(arg, 'conjugate') and hasattr(arg, 'transpose') and evaluate: + return arg.conjugate().transpose() + return Expr.__new__(cls, sympify(arg)) + +adjoint.__name__ = "Dagger" +adjoint._sympyrepr = lambda a, b: "Dagger(%s)" % b._print(a.args[0]) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/density.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/density.py new file mode 100644 index 0000000000000000000000000000000000000000..941373e8105dd0c725626396dfd9cd794b19d3f5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/density.py @@ -0,0 +1,315 @@ +from itertools import product + +from sympy.core.add import Add +from sympy.core.containers import Tuple +from sympy.core.function import expand +from sympy.core.mul import Mul +from sympy.core.singleton import S +from sympy.functions.elementary.exponential import log +from sympy.matrices.dense import MutableDenseMatrix as Matrix +from sympy.printing.pretty.stringpict import prettyForm +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.operator import HermitianOperator +from sympy.physics.quantum.represent import represent +from sympy.physics.quantum.matrixutils import numpy_ndarray, scipy_sparse_matrix, to_numpy +from sympy.physics.quantum.trace import Tr + + +class Density(HermitianOperator): + """Density operator for representing mixed states. + + TODO: Density operator support for Qubits + + Parameters + ========== + + values : tuples/lists + Each tuple/list should be of form (state, prob) or [state,prob] + + Examples + ======== + + Create a density operator with 2 states represented by Kets. + + >>> from sympy.physics.quantum.state import Ket + >>> from sympy.physics.quantum.density import Density + >>> d = Density([Ket(0), 0.5], [Ket(1),0.5]) + >>> d + Density((|0>, 0.5),(|1>, 0.5)) + + """ + @classmethod + def _eval_args(cls, args): + # call this to qsympify the args + args = super()._eval_args(args) + + for arg in args: + # Check if arg is a tuple + if not (isinstance(arg, Tuple) and len(arg) == 2): + raise ValueError("Each argument should be of form [state,prob]" + " or ( state, prob )") + + return args + + def states(self): + """Return list of all states. + + Examples + ======== + + >>> from sympy.physics.quantum.state import Ket + >>> from sympy.physics.quantum.density import Density + >>> d = Density([Ket(0), 0.5], [Ket(1),0.5]) + >>> d.states() + (|0>, |1>) + + """ + return Tuple(*[arg[0] for arg in self.args]) + + def probs(self): + """Return list of all probabilities. + + Examples + ======== + + >>> from sympy.physics.quantum.state import Ket + >>> from sympy.physics.quantum.density import Density + >>> d = Density([Ket(0), 0.5], [Ket(1),0.5]) + >>> d.probs() + (0.5, 0.5) + + """ + return Tuple(*[arg[1] for arg in self.args]) + + def get_state(self, index): + """Return specific state by index. + + Parameters + ========== + + index : index of state to be returned + + Examples + ======== + + >>> from sympy.physics.quantum.state import Ket + >>> from sympy.physics.quantum.density import Density + >>> d = Density([Ket(0), 0.5], [Ket(1),0.5]) + >>> d.states()[1] + |1> + + """ + state = self.args[index][0] + return state + + def get_prob(self, index): + """Return probability of specific state by index. + + Parameters + =========== + + index : index of states whose probability is returned. + + Examples + ======== + + >>> from sympy.physics.quantum.state import Ket + >>> from sympy.physics.quantum.density import Density + >>> d = Density([Ket(0), 0.5], [Ket(1),0.5]) + >>> d.probs()[1] + 0.500000000000000 + + """ + prob = self.args[index][1] + return prob + + def apply_op(self, op): + """op will operate on each individual state. + + Parameters + ========== + + op : Operator + + Examples + ======== + + >>> from sympy.physics.quantum.state import Ket + >>> from sympy.physics.quantum.density import Density + >>> from sympy.physics.quantum.operator import Operator + >>> A = Operator('A') + >>> d = Density([Ket(0), 0.5], [Ket(1),0.5]) + >>> d.apply_op(A) + Density((A*|0>, 0.5),(A*|1>, 0.5)) + + """ + new_args = [(op*state, prob) for (state, prob) in self.args] + return Density(*new_args) + + def doit(self, **hints): + """Expand the density operator into an outer product format. + + Examples + ======== + + >>> from sympy.physics.quantum.state import Ket + >>> from sympy.physics.quantum.density import Density + >>> from sympy.physics.quantum.operator import Operator + >>> A = Operator('A') + >>> d = Density([Ket(0), 0.5], [Ket(1),0.5]) + >>> d.doit() + 0.5*|0><0| + 0.5*|1><1| + + """ + + terms = [] + for (state, prob) in self.args: + state = state.expand() # needed to break up (a+b)*c + if (isinstance(state, Add)): + for arg in product(state.args, repeat=2): + terms.append(prob*self._generate_outer_prod(arg[0], + arg[1])) + else: + terms.append(prob*self._generate_outer_prod(state, state)) + + return Add(*terms) + + def _generate_outer_prod(self, arg1, arg2): + c_part1, nc_part1 = arg1.args_cnc() + c_part2, nc_part2 = arg2.args_cnc() + + if (len(nc_part1) == 0 or len(nc_part2) == 0): + raise ValueError('Atleast one-pair of' + ' Non-commutative instance required' + ' for outer product.') + + # We were able to remove some tensor product simplifications that + # used to be here as those transformations are not automatically + # applied by transforms.py. + op = Mul(*nc_part1)*Dagger(Mul(*nc_part2)) + + return Mul(*c_part1)*Mul(*c_part2) * op + + def _represent(self, **options): + return represent(self.doit(), **options) + + def _print_operator_name_latex(self, printer, *args): + return r'\rho' + + def _print_operator_name_pretty(self, printer, *args): + return prettyForm('\N{GREEK SMALL LETTER RHO}') + + def _eval_trace(self, **kwargs): + indices = kwargs.get('indices', []) + return Tr(self.doit(), indices).doit() + + def entropy(self): + """ Compute the entropy of a density matrix. + + Refer to density.entropy() method for examples. + """ + return entropy(self) + + +def entropy(density): + """Compute the entropy of a matrix/density object. + + This computes -Tr(density*ln(density)) using the eigenvalue decomposition + of density, which is given as either a Density instance or a matrix + (numpy.ndarray, sympy.Matrix or scipy.sparse). + + Parameters + ========== + + density : density matrix of type Density, SymPy matrix, + scipy.sparse or numpy.ndarray + + Examples + ======== + + >>> from sympy.physics.quantum.density import Density, entropy + >>> from sympy.physics.quantum.spin import JzKet + >>> from sympy import S + >>> up = JzKet(S(1)/2,S(1)/2) + >>> down = JzKet(S(1)/2,-S(1)/2) + >>> d = Density((up,S(1)/2),(down,S(1)/2)) + >>> entropy(d) + log(2)/2 + + """ + if isinstance(density, Density): + density = represent(density) # represent in Matrix + + if isinstance(density, scipy_sparse_matrix): + density = to_numpy(density) + + if isinstance(density, Matrix): + eigvals = density.eigenvals().keys() + return expand(-sum(e*log(e) for e in eigvals)) + elif isinstance(density, numpy_ndarray): + import numpy as np + eigvals = np.linalg.eigvals(density) + return -np.sum(eigvals*np.log(eigvals)) + else: + raise ValueError( + "numpy.ndarray, scipy.sparse or SymPy matrix expected") + + +def fidelity(state1, state2): + """ Computes the fidelity [1]_ between two quantum states + + The arguments provided to this function should be a square matrix or a + Density object. If it is a square matrix, it is assumed to be diagonalizable. + + Parameters + ========== + + state1, state2 : a density matrix or Matrix + + + Examples + ======== + + >>> from sympy import S, sqrt + >>> from sympy.physics.quantum.dagger import Dagger + >>> from sympy.physics.quantum.spin import JzKet + >>> from sympy.physics.quantum.density import fidelity + >>> from sympy.physics.quantum.represent import represent + >>> + >>> up = JzKet(S(1)/2,S(1)/2) + >>> down = JzKet(S(1)/2,-S(1)/2) + >>> amp = 1/sqrt(2) + >>> updown = (amp*up) + (amp*down) + >>> + >>> # represent turns Kets into matrices + >>> up_dm = represent(up*Dagger(up)) + >>> down_dm = represent(down*Dagger(down)) + >>> updown_dm = represent(updown*Dagger(updown)) + >>> + >>> fidelity(up_dm, up_dm) + 1 + >>> fidelity(up_dm, down_dm) #orthogonal states + 0 + >>> fidelity(up_dm, updown_dm).evalf().round(3) + 0.707 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Fidelity_of_quantum_states + + """ + state1 = represent(state1) if isinstance(state1, Density) else state1 + state2 = represent(state2) if isinstance(state2, Density) else state2 + + if not isinstance(state1, Matrix) or not isinstance(state2, Matrix): + raise ValueError("state1 and state2 must be of type Density or Matrix " + "received type=%s for state1 and type=%s for state2" % + (type(state1), type(state2))) + + if state1.shape != state2.shape and state1.is_square: + raise ValueError("The dimensions of both args should be equal and the " + "matrix obtained should be a square matrix") + + sqrt_state1 = state1**S.Half + return Tr((sqrt_state1*state2*sqrt_state1)**S.Half).doit() diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/fermion.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/fermion.py new file mode 100644 index 0000000000000000000000000000000000000000..8080bd3b0904b837652fdae7be0bd526da2d508f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/fermion.py @@ -0,0 +1,191 @@ +"""Fermionic quantum operators.""" + +from sympy.core.numbers import Integer +from sympy.core.singleton import S +from sympy.physics.quantum import Operator +from sympy.physics.quantum import HilbertSpace, Ket, Bra +from sympy.functions.special.tensor_functions import KroneckerDelta + + +__all__ = [ + 'FermionOp', + 'FermionFockKet', + 'FermionFockBra' +] + + +class FermionOp(Operator): + """A fermionic operator that satisfies {c, Dagger(c)} == 1. + + Parameters + ========== + + name : str + A string that labels the fermionic mode. + + annihilation : bool + A bool that indicates if the fermionic operator is an annihilation + (True, default value) or creation operator (False) + + Examples + ======== + + >>> from sympy.physics.quantum import Dagger, AntiCommutator + >>> from sympy.physics.quantum.fermion import FermionOp + >>> c = FermionOp("c") + >>> AntiCommutator(c, Dagger(c)).doit() + 1 + """ + @property + def name(self): + return self.args[0] + + @property + def is_annihilation(self): + return bool(self.args[1]) + + @classmethod + def default_args(self): + return ("c", True) + + def __new__(cls, *args, **hints): + if not len(args) in [1, 2]: + raise ValueError('1 or 2 parameters expected, got %s' % args) + + if len(args) == 1: + args = (args[0], S.One) + + if len(args) == 2: + args = (args[0], Integer(args[1])) + + return Operator.__new__(cls, *args) + + def _eval_commutator_FermionOp(self, other, **hints): + if 'independent' in hints and hints['independent']: + # [c, d] = 0 + return S.Zero + + return None + + def _eval_anticommutator_FermionOp(self, other, **hints): + if self.name == other.name: + # {a^\dagger, a} = 1 + if not self.is_annihilation and other.is_annihilation: + return S.One + + elif 'independent' in hints and hints['independent']: + # {c, d} = 2 * c * d, because [c, d] = 0 for independent operators + return 2 * self * other + + return None + + def _eval_anticommutator_BosonOp(self, other, **hints): + # because fermions and bosons commute + return 2 * self * other + + def _eval_commutator_BosonOp(self, other, **hints): + return S.Zero + + def _eval_adjoint(self): + return FermionOp(str(self.name), not self.is_annihilation) + + def _print_contents_latex(self, printer, *args): + if self.is_annihilation: + return r'{%s}' % str(self.name) + else: + return r'{{%s}^\dagger}' % str(self.name) + + def _print_contents(self, printer, *args): + if self.is_annihilation: + return r'%s' % str(self.name) + else: + return r'Dagger(%s)' % str(self.name) + + def _print_contents_pretty(self, printer, *args): + from sympy.printing.pretty.stringpict import prettyForm + pform = printer._print(self.args[0], *args) + if self.is_annihilation: + return pform + else: + return pform**prettyForm('\N{DAGGER}') + + def _eval_power(self, exp): + from sympy.core.singleton import S + if exp == 0: + return S.One + elif exp == 1: + return self + elif (exp > 1) == True and exp.is_integer == True: + return S.Zero + elif (exp < 0) == True or exp.is_integer == False: + raise ValueError("Fermionic operators can only be raised to a" + " positive integer power") + return Operator._eval_power(self, exp) + +class FermionFockKet(Ket): + """Fock state ket for a fermionic mode. + + Parameters + ========== + + n : Number + The Fock state number. + + """ + + def __new__(cls, n): + if n not in (0, 1): + raise ValueError("n must be 0 or 1") + return Ket.__new__(cls, n) + + @property + def n(self): + return self.label[0] + + @classmethod + def dual_class(self): + return FermionFockBra + + @classmethod + def _eval_hilbert_space(cls, label): + return HilbertSpace() + + def _eval_innerproduct_FermionFockBra(self, bra, **hints): + return KroneckerDelta(self.n, bra.n) + + def _apply_from_right_to_FermionOp(self, op, **options): + if op.is_annihilation: + if self.n == 1: + return FermionFockKet(0) + else: + return S.Zero + else: + if self.n == 0: + return FermionFockKet(1) + else: + return S.Zero + + +class FermionFockBra(Bra): + """Fock state bra for a fermionic mode. + + Parameters + ========== + + n : Number + The Fock state number. + + """ + + def __new__(cls, n): + if n not in (0, 1): + raise ValueError("n must be 0 or 1") + return Bra.__new__(cls, n) + + @property + def n(self): + return self.label[0] + + @classmethod + def dual_class(self): + return FermionFockKet diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/gate.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/gate.py new file mode 100644 index 0000000000000000000000000000000000000000..f8bcf5cd3611173cd9ebd6308dbbc896f5257f20 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/gate.py @@ -0,0 +1,1309 @@ +"""An implementation of gates that act on qubits. + +Gates are unitary operators that act on the space of qubits. + +Medium Term Todo: + +* Optimize Gate._apply_operators_Qubit to remove the creation of many + intermediate Qubit objects. +* Add commutation relationships to all operators and use this in gate_sort. +* Fix gate_sort and gate_simp. +* Get multi-target UGates plotting properly. +* Get UGate to work with either sympy/numpy matrices and output either + format. This should also use the matrix slots. +""" + +from itertools import chain +import random + +from sympy.core.add import Add +from sympy.core.containers import Tuple +from sympy.core.mul import Mul +from sympy.core.numbers import (I, Integer) +from sympy.core.power import Pow +from sympy.core.numbers import Number +from sympy.core.singleton import S as _S +from sympy.core.sorting import default_sort_key +from sympy.core.sympify import _sympify +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.printing.pretty.stringpict import prettyForm, stringPict + +from sympy.physics.quantum.anticommutator import AntiCommutator +from sympy.physics.quantum.commutator import Commutator +from sympy.physics.quantum.qexpr import QuantumError +from sympy.physics.quantum.hilbert import ComplexSpace +from sympy.physics.quantum.operator import (UnitaryOperator, Operator, + HermitianOperator) +from sympy.physics.quantum.matrixutils import matrix_tensor_product, matrix_eye +from sympy.physics.quantum.matrixcache import matrix_cache + +from sympy.matrices.matrixbase import MatrixBase + +from sympy.utilities.iterables import is_sequence + +__all__ = [ + 'Gate', + 'CGate', + 'UGate', + 'OneQubitGate', + 'TwoQubitGate', + 'IdentityGate', + 'HadamardGate', + 'XGate', + 'YGate', + 'ZGate', + 'TGate', + 'PhaseGate', + 'SwapGate', + 'CNotGate', + # Aliased gate names + 'CNOT', + 'SWAP', + 'H', + 'X', + 'Y', + 'Z', + 'T', + 'S', + 'Phase', + 'normalized', + 'gate_sort', + 'gate_simp', + 'random_circuit', + 'CPHASE', + 'CGateS', +] + +#----------------------------------------------------------------------------- +# Gate Super-Classes +#----------------------------------------------------------------------------- + +_normalized = True + + +def _max(*args, **kwargs): + if "key" not in kwargs: + kwargs["key"] = default_sort_key + return max(*args, **kwargs) + + +def _min(*args, **kwargs): + if "key" not in kwargs: + kwargs["key"] = default_sort_key + return min(*args, **kwargs) + + +def normalized(normalize): + r"""Set flag controlling normalization of Hadamard gates by `1/\sqrt{2}`. + + This is a global setting that can be used to simplify the look of various + expressions, by leaving off the leading `1/\sqrt{2}` of the Hadamard gate. + + Parameters + ---------- + normalize : bool + Should the Hadamard gate include the `1/\sqrt{2}` normalization factor? + When True, the Hadamard gate will have the `1/\sqrt{2}`. When False, the + Hadamard gate will not have this factor. + """ + global _normalized + _normalized = normalize + + +def _validate_targets_controls(tandc): + tandc = list(tandc) + # Check for integers + for bit in tandc: + if not bit.is_Integer and not bit.is_Symbol: + raise TypeError('Integer expected, got: %r' % tandc[bit]) + # Detect duplicates + if len(set(tandc)) != len(tandc): + raise QuantumError( + 'Target/control qubits in a gate cannot be duplicated' + ) + + +class Gate(UnitaryOperator): + """Non-controlled unitary gate operator that acts on qubits. + + This is a general abstract gate that needs to be subclassed to do anything + useful. + + Parameters + ---------- + label : tuple, int + A list of the target qubits (as ints) that the gate will apply to. + + Examples + ======== + + + """ + + _label_separator = ',' + + gate_name = 'G' + gate_name_latex = 'G' + + #------------------------------------------------------------------------- + # Initialization/creation + #------------------------------------------------------------------------- + + @classmethod + def _eval_args(cls, args): + args = Tuple(*UnitaryOperator._eval_args(args)) + _validate_targets_controls(args) + return args + + @classmethod + def _eval_hilbert_space(cls, args): + """This returns the smallest possible Hilbert space.""" + return ComplexSpace(2)**(_max(args) + 1) + + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + + @property + def nqubits(self): + """The total number of qubits this gate acts on. + + For controlled gate subclasses this includes both target and control + qubits, so that, for examples the CNOT gate acts on 2 qubits. + """ + return len(self.targets) + + @property + def min_qubits(self): + """The minimum number of qubits this gate needs to act on.""" + return _max(self.targets) + 1 + + @property + def targets(self): + """A tuple of target qubits.""" + return self.label + + @property + def gate_name_plot(self): + return r'$%s$' % self.gate_name_latex + + #------------------------------------------------------------------------- + # Gate methods + #------------------------------------------------------------------------- + + def get_target_matrix(self, format='sympy'): + """The matrix representation of the target part of the gate. + + Parameters + ---------- + format : str + The format string ('sympy','numpy', etc.) + """ + raise NotImplementedError( + 'get_target_matrix is not implemented in Gate.') + + #------------------------------------------------------------------------- + # Apply + #------------------------------------------------------------------------- + + def _apply_operator_IntQubit(self, qubits, **options): + """Redirect an apply from IntQubit to Qubit""" + return self._apply_operator_Qubit(qubits, **options) + + def _apply_operator_Qubit(self, qubits, **options): + """Apply this gate to a Qubit.""" + + # Check number of qubits this gate acts on. + if qubits.nqubits < self.min_qubits: + raise QuantumError( + 'Gate needs a minimum of %r qubits to act on, got: %r' % + (self.min_qubits, qubits.nqubits) + ) + + # If the controls are not met, just return + if isinstance(self, CGate): + if not self.eval_controls(qubits): + return qubits + + targets = self.targets + target_matrix = self.get_target_matrix(format='sympy') + + # Find which column of the target matrix this applies to. + column_index = 0 + n = 1 + for target in targets: + column_index += n*qubits[target] + n = n << 1 + column = target_matrix[:, int(column_index)] + + # Now apply each column element to the qubit. + result = 0 + for index in range(column.rows): + # TODO: This can be optimized to reduce the number of Qubit + # creations. We should simply manipulate the raw list of qubit + # values and then build the new Qubit object once. + # Make a copy of the incoming qubits. + new_qubit = qubits.__class__(*qubits.args) + # Flip the bits that need to be flipped. + for bit, target in enumerate(targets): + if new_qubit[target] != (index >> bit) & 1: + new_qubit = new_qubit.flip(target) + # The value in that row and column times the flipped-bit qubit + # is the result for that part. + result += column[index]*new_qubit + return result + + #------------------------------------------------------------------------- + # Represent + #------------------------------------------------------------------------- + + def _represent_default_basis(self, **options): + return self._represent_ZGate(None, **options) + + def _represent_ZGate(self, basis, **options): + format = options.get('format', 'sympy') + nqubits = options.get('nqubits', 0) + if nqubits == 0: + raise QuantumError( + 'The number of qubits must be given as nqubits.') + + # Make sure we have enough qubits for the gate. + if nqubits < self.min_qubits: + raise QuantumError( + 'The number of qubits %r is too small for the gate.' % nqubits + ) + + target_matrix = self.get_target_matrix(format) + targets = self.targets + if isinstance(self, CGate): + controls = self.controls + else: + controls = [] + m = represent_zbasis( + controls, targets, target_matrix, nqubits, format + ) + return m + + #------------------------------------------------------------------------- + # Print methods + #------------------------------------------------------------------------- + + def _sympystr(self, printer, *args): + label = self._print_label(printer, *args) + return '%s(%s)' % (self.gate_name, label) + + def _pretty(self, printer, *args): + a = stringPict(self.gate_name) + b = self._print_label_pretty(printer, *args) + return self._print_subscript_pretty(a, b) + + def _latex(self, printer, *args): + label = self._print_label(printer, *args) + return '%s_{%s}' % (self.gate_name_latex, label) + + def plot_gate(self, axes, gate_idx, gate_grid, wire_grid): + raise NotImplementedError('plot_gate is not implemented.') + + +class CGate(Gate): + """A general unitary gate with control qubits. + + A general control gate applies a target gate to a set of targets if all + of the control qubits have a particular values (set by + ``CGate.control_value``). + + Parameters + ---------- + label : tuple + The label in this case has the form (controls, gate), where controls + is a tuple/list of control qubits (as ints) and gate is a ``Gate`` + instance that is the target operator. + + Examples + ======== + + """ + + gate_name = 'C' + gate_name_latex = 'C' + + # The values this class controls for. + control_value = _S.One + + simplify_cgate = False + + #------------------------------------------------------------------------- + # Initialization + #------------------------------------------------------------------------- + + @classmethod + def _eval_args(cls, args): + # _eval_args has the right logic for the controls argument. + controls = args[0] + gate = args[1] + if not is_sequence(controls): + controls = (controls,) + controls = UnitaryOperator._eval_args(controls) + _validate_targets_controls(chain(controls, gate.targets)) + return (Tuple(*controls), gate) + + @classmethod + def _eval_hilbert_space(cls, args): + """This returns the smallest possible Hilbert space.""" + return ComplexSpace(2)**_max(_max(args[0]) + 1, args[1].min_qubits) + + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + + @property + def nqubits(self): + """The total number of qubits this gate acts on. + + For controlled gate subclasses this includes both target and control + qubits, so that, for examples the CNOT gate acts on 2 qubits. + """ + return len(self.targets) + len(self.controls) + + @property + def min_qubits(self): + """The minimum number of qubits this gate needs to act on.""" + return _max(_max(self.controls), _max(self.targets)) + 1 + + @property + def targets(self): + """A tuple of target qubits.""" + return self.gate.targets + + @property + def controls(self): + """A tuple of control qubits.""" + return tuple(self.label[0]) + + @property + def gate(self): + """The non-controlled gate that will be applied to the targets.""" + return self.label[1] + + #------------------------------------------------------------------------- + # Gate methods + #------------------------------------------------------------------------- + + def get_target_matrix(self, format='sympy'): + return self.gate.get_target_matrix(format) + + def eval_controls(self, qubit): + """Return True/False to indicate if the controls are satisfied.""" + return all(qubit[bit] == self.control_value for bit in self.controls) + + def decompose(self, **options): + """Decompose the controlled gate into CNOT and single qubits gates.""" + if len(self.controls) == 1: + c = self.controls[0] + t = self.gate.targets[0] + if isinstance(self.gate, YGate): + g1 = PhaseGate(t) + g2 = CNotGate(c, t) + g3 = PhaseGate(t) + g4 = ZGate(t) + return g1*g2*g3*g4 + if isinstance(self.gate, ZGate): + g1 = HadamardGate(t) + g2 = CNotGate(c, t) + g3 = HadamardGate(t) + return g1*g2*g3 + else: + return self + + #------------------------------------------------------------------------- + # Print methods + #------------------------------------------------------------------------- + + def _print_label(self, printer, *args): + controls = self._print_sequence(self.controls, ',', printer, *args) + gate = printer._print(self.gate, *args) + return '(%s),%s' % (controls, gate) + + def _pretty(self, printer, *args): + controls = self._print_sequence_pretty( + self.controls, ',', printer, *args) + gate = printer._print(self.gate) + gate_name = stringPict(self.gate_name) + first = self._print_subscript_pretty(gate_name, controls) + gate = self._print_parens_pretty(gate) + final = prettyForm(*first.right(gate)) + return final + + def _latex(self, printer, *args): + controls = self._print_sequence(self.controls, ',', printer, *args) + gate = printer._print(self.gate, *args) + return r'%s_{%s}{\left(%s\right)}' % \ + (self.gate_name_latex, controls, gate) + + def plot_gate(self, circ_plot, gate_idx): + """ + Plot the controlled gate. If *simplify_cgate* is true, simplify + C-X and C-Z gates into their more familiar forms. + """ + min_wire = int(_min(chain(self.controls, self.targets))) + max_wire = int(_max(chain(self.controls, self.targets))) + circ_plot.control_line(gate_idx, min_wire, max_wire) + for c in self.controls: + circ_plot.control_point(gate_idx, int(c)) + if self.simplify_cgate: + if self.gate.gate_name == 'X': + self.gate.plot_gate_plus(circ_plot, gate_idx) + elif self.gate.gate_name == 'Z': + circ_plot.control_point(gate_idx, self.targets[0]) + else: + self.gate.plot_gate(circ_plot, gate_idx) + else: + self.gate.plot_gate(circ_plot, gate_idx) + + #------------------------------------------------------------------------- + # Miscellaneous + #------------------------------------------------------------------------- + + def _eval_dagger(self): + if isinstance(self.gate, HermitianOperator): + return self + else: + return Gate._eval_dagger(self) + + def _eval_inverse(self): + if isinstance(self.gate, HermitianOperator): + return self + else: + return Gate._eval_inverse(self) + + def _eval_power(self, exp): + if isinstance(self.gate, HermitianOperator): + if exp == -1: + return Gate._eval_power(self, exp) + elif abs(exp) % 2 == 0: + return self*(Gate._eval_inverse(self)) + else: + return self + else: + return Gate._eval_power(self, exp) + +class CGateS(CGate): + """Version of CGate that allows gate simplifications. + I.e. cnot looks like an oplus, cphase has dots, etc. + """ + simplify_cgate=True + + +class UGate(Gate): + """General gate specified by a set of targets and a target matrix. + + Parameters + ---------- + label : tuple + A tuple of the form (targets, U), where targets is a tuple of the + target qubits and U is a unitary matrix with dimension of + len(targets). + """ + gate_name = 'U' + gate_name_latex = 'U' + + #------------------------------------------------------------------------- + # Initialization + #------------------------------------------------------------------------- + + @classmethod + def _eval_args(cls, args): + targets = args[0] + if not is_sequence(targets): + targets = (targets,) + targets = Gate._eval_args(targets) + _validate_targets_controls(targets) + mat = args[1] + if not isinstance(mat, MatrixBase): + raise TypeError('Matrix expected, got: %r' % mat) + #make sure this matrix is of a Basic type + mat = _sympify(mat) + dim = 2**len(targets) + if not all(dim == shape for shape in mat.shape): + raise IndexError( + 'Number of targets must match the matrix size: %r %r' % + (targets, mat) + ) + return (targets, mat) + + @classmethod + def _eval_hilbert_space(cls, args): + """This returns the smallest possible Hilbert space.""" + return ComplexSpace(2)**(_max(args[0]) + 1) + + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + + @property + def targets(self): + """A tuple of target qubits.""" + return tuple(self.label[0]) + + #------------------------------------------------------------------------- + # Gate methods + #------------------------------------------------------------------------- + + def get_target_matrix(self, format='sympy'): + """The matrix rep. of the target part of the gate. + + Parameters + ---------- + format : str + The format string ('sympy','numpy', etc.) + """ + return self.label[1] + + #------------------------------------------------------------------------- + # Print methods + #------------------------------------------------------------------------- + def _pretty(self, printer, *args): + targets = self._print_sequence_pretty( + self.targets, ',', printer, *args) + gate_name = stringPict(self.gate_name) + return self._print_subscript_pretty(gate_name, targets) + + def _latex(self, printer, *args): + targets = self._print_sequence(self.targets, ',', printer, *args) + return r'%s_{%s}' % (self.gate_name_latex, targets) + + def plot_gate(self, circ_plot, gate_idx): + circ_plot.one_qubit_box( + self.gate_name_plot, + gate_idx, int(self.targets[0]) + ) + + +class OneQubitGate(Gate): + """A single qubit unitary gate base class.""" + + nqubits = _S.One + + def plot_gate(self, circ_plot, gate_idx): + circ_plot.one_qubit_box( + self.gate_name_plot, + gate_idx, int(self.targets[0]) + ) + + def _eval_commutator(self, other, **hints): + if isinstance(other, OneQubitGate): + if self.targets != other.targets or self.__class__ == other.__class__: + return _S.Zero + return Operator._eval_commutator(self, other, **hints) + + def _eval_anticommutator(self, other, **hints): + if isinstance(other, OneQubitGate): + if self.targets != other.targets or self.__class__ == other.__class__: + return Integer(2)*self*other + return Operator._eval_anticommutator(self, other, **hints) + + +class TwoQubitGate(Gate): + """A two qubit unitary gate base class.""" + + nqubits = Integer(2) + +#----------------------------------------------------------------------------- +# Single Qubit Gates +#----------------------------------------------------------------------------- + + +class IdentityGate(OneQubitGate): + """The single qubit identity gate. + + Parameters + ---------- + target : int + The target qubit this gate will apply to. + + Examples + ======== + + """ + is_hermitian = True + gate_name = '1' + gate_name_latex = '1' + + # Short cut version of gate._apply_operator_Qubit + def _apply_operator_Qubit(self, qubits, **options): + # Check number of qubits this gate acts on (see gate._apply_operator_Qubit) + if qubits.nqubits < self.min_qubits: + raise QuantumError( + 'Gate needs a minimum of %r qubits to act on, got: %r' % + (self.min_qubits, qubits.nqubits) + ) + return qubits # no computation required for IdentityGate + + def get_target_matrix(self, format='sympy'): + return matrix_cache.get_matrix('eye2', format) + + def _eval_commutator(self, other, **hints): + return _S.Zero + + def _eval_anticommutator(self, other, **hints): + return Integer(2)*other + + +class HadamardGate(HermitianOperator, OneQubitGate): + """The single qubit Hadamard gate. + + Parameters + ---------- + target : int + The target qubit this gate will apply to. + + Examples + ======== + + >>> from sympy import sqrt + >>> from sympy.physics.quantum.qubit import Qubit + >>> from sympy.physics.quantum.gate import HadamardGate + >>> from sympy.physics.quantum.qapply import qapply + >>> qapply(HadamardGate(0)*Qubit('1')) + sqrt(2)*|0>/2 - sqrt(2)*|1>/2 + >>> # Hadamard on bell state, applied on 2 qubits. + >>> psi = 1/sqrt(2)*(Qubit('00')+Qubit('11')) + >>> qapply(HadamardGate(0)*HadamardGate(1)*psi) + sqrt(2)*|00>/2 + sqrt(2)*|11>/2 + + """ + gate_name = 'H' + gate_name_latex = 'H' + + def get_target_matrix(self, format='sympy'): + if _normalized: + return matrix_cache.get_matrix('H', format) + else: + return matrix_cache.get_matrix('Hsqrt2', format) + + def _eval_commutator_XGate(self, other, **hints): + return I*sqrt(2)*YGate(self.targets[0]) + + def _eval_commutator_YGate(self, other, **hints): + return I*sqrt(2)*(ZGate(self.targets[0]) - XGate(self.targets[0])) + + def _eval_commutator_ZGate(self, other, **hints): + return -I*sqrt(2)*YGate(self.targets[0]) + + def _eval_anticommutator_XGate(self, other, **hints): + return sqrt(2)*IdentityGate(self.targets[0]) + + def _eval_anticommutator_YGate(self, other, **hints): + return _S.Zero + + def _eval_anticommutator_ZGate(self, other, **hints): + return sqrt(2)*IdentityGate(self.targets[0]) + + +class XGate(HermitianOperator, OneQubitGate): + """The single qubit X, or NOT, gate. + + Parameters + ---------- + target : int + The target qubit this gate will apply to. + + Examples + ======== + + """ + gate_name = 'X' + gate_name_latex = 'X' + + def get_target_matrix(self, format='sympy'): + return matrix_cache.get_matrix('X', format) + + def plot_gate(self, circ_plot, gate_idx): + OneQubitGate.plot_gate(self,circ_plot,gate_idx) + + def plot_gate_plus(self, circ_plot, gate_idx): + circ_plot.not_point( + gate_idx, int(self.label[0]) + ) + + def _eval_commutator_YGate(self, other, **hints): + return Integer(2)*I*ZGate(self.targets[0]) + + def _eval_anticommutator_XGate(self, other, **hints): + return Integer(2)*IdentityGate(self.targets[0]) + + def _eval_anticommutator_YGate(self, other, **hints): + return _S.Zero + + def _eval_anticommutator_ZGate(self, other, **hints): + return _S.Zero + + +class YGate(HermitianOperator, OneQubitGate): + """The single qubit Y gate. + + Parameters + ---------- + target : int + The target qubit this gate will apply to. + + Examples + ======== + + """ + gate_name = 'Y' + gate_name_latex = 'Y' + + def get_target_matrix(self, format='sympy'): + return matrix_cache.get_matrix('Y', format) + + def _eval_commutator_ZGate(self, other, **hints): + return Integer(2)*I*XGate(self.targets[0]) + + def _eval_anticommutator_YGate(self, other, **hints): + return Integer(2)*IdentityGate(self.targets[0]) + + def _eval_anticommutator_ZGate(self, other, **hints): + return _S.Zero + + +class ZGate(HermitianOperator, OneQubitGate): + """The single qubit Z gate. + + Parameters + ---------- + target : int + The target qubit this gate will apply to. + + Examples + ======== + + """ + gate_name = 'Z' + gate_name_latex = 'Z' + + def get_target_matrix(self, format='sympy'): + return matrix_cache.get_matrix('Z', format) + + def _eval_commutator_XGate(self, other, **hints): + return Integer(2)*I*YGate(self.targets[0]) + + def _eval_anticommutator_YGate(self, other, **hints): + return _S.Zero + + +class PhaseGate(OneQubitGate): + """The single qubit phase, or S, gate. + + This gate rotates the phase of the state by pi/2 if the state is ``|1>`` and + does nothing if the state is ``|0>``. + + Parameters + ---------- + target : int + The target qubit this gate will apply to. + + Examples + ======== + + """ + is_hermitian = False + gate_name = 'S' + gate_name_latex = 'S' + + def get_target_matrix(self, format='sympy'): + return matrix_cache.get_matrix('S', format) + + def _eval_commutator_ZGate(self, other, **hints): + return _S.Zero + + def _eval_commutator_TGate(self, other, **hints): + return _S.Zero + + +class TGate(OneQubitGate): + """The single qubit pi/8 gate. + + This gate rotates the phase of the state by pi/4 if the state is ``|1>`` and + does nothing if the state is ``|0>``. + + Parameters + ---------- + target : int + The target qubit this gate will apply to. + + Examples + ======== + + """ + is_hermitian = False + gate_name = 'T' + gate_name_latex = 'T' + + def get_target_matrix(self, format='sympy'): + return matrix_cache.get_matrix('T', format) + + def _eval_commutator_ZGate(self, other, **hints): + return _S.Zero + + def _eval_commutator_PhaseGate(self, other, **hints): + return _S.Zero + + +# Aliases for gate names. +H = HadamardGate +X = XGate +Y = YGate +Z = ZGate +T = TGate +Phase = S = PhaseGate + + +#----------------------------------------------------------------------------- +# 2 Qubit Gates +#----------------------------------------------------------------------------- + + +class CNotGate(HermitianOperator, CGate, TwoQubitGate): + """Two qubit controlled-NOT. + + This gate performs the NOT or X gate on the target qubit if the control + qubits all have the value 1. + + Parameters + ---------- + label : tuple + A tuple of the form (control, target). + + Examples + ======== + + >>> from sympy.physics.quantum.gate import CNOT + >>> from sympy.physics.quantum.qapply import qapply + >>> from sympy.physics.quantum.qubit import Qubit + >>> c = CNOT(1,0) + >>> qapply(c*Qubit('10')) # note that qubits are indexed from right to left + |11> + + """ + gate_name = 'CNOT' + gate_name_latex = r'\text{CNOT}' + simplify_cgate = True + + #------------------------------------------------------------------------- + # Initialization + #------------------------------------------------------------------------- + + @classmethod + def _eval_args(cls, args): + args = Gate._eval_args(args) + return args + + @classmethod + def _eval_hilbert_space(cls, args): + """This returns the smallest possible Hilbert space.""" + return ComplexSpace(2)**(_max(args) + 1) + + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + + @property + def min_qubits(self): + """The minimum number of qubits this gate needs to act on.""" + return _max(self.label) + 1 + + @property + def targets(self): + """A tuple of target qubits.""" + return (self.label[1],) + + @property + def controls(self): + """A tuple of control qubits.""" + return (self.label[0],) + + @property + def gate(self): + """The non-controlled gate that will be applied to the targets.""" + return XGate(self.label[1]) + + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + + # The default printing of Gate works better than those of CGate, so we + # go around the overridden methods in CGate. + + def _print_label(self, printer, *args): + return Gate._print_label(self, printer, *args) + + def _pretty(self, printer, *args): + return Gate._pretty(self, printer, *args) + + def _latex(self, printer, *args): + return Gate._latex(self, printer, *args) + + #------------------------------------------------------------------------- + # Commutator/AntiCommutator + #------------------------------------------------------------------------- + + def _eval_commutator_ZGate(self, other, **hints): + """[CNOT(i, j), Z(i)] == 0.""" + if self.controls[0] == other.targets[0]: + return _S.Zero + else: + raise NotImplementedError('Commutator not implemented: %r' % other) + + def _eval_commutator_TGate(self, other, **hints): + """[CNOT(i, j), T(i)] == 0.""" + return self._eval_commutator_ZGate(other, **hints) + + def _eval_commutator_PhaseGate(self, other, **hints): + """[CNOT(i, j), S(i)] == 0.""" + return self._eval_commutator_ZGate(other, **hints) + + def _eval_commutator_XGate(self, other, **hints): + """[CNOT(i, j), X(j)] == 0.""" + if self.targets[0] == other.targets[0]: + return _S.Zero + else: + raise NotImplementedError('Commutator not implemented: %r' % other) + + def _eval_commutator_CNotGate(self, other, **hints): + """[CNOT(i, j), CNOT(i,k)] == 0.""" + if self.controls[0] == other.controls[0]: + return _S.Zero + else: + raise NotImplementedError('Commutator not implemented: %r' % other) + + +class SwapGate(TwoQubitGate): + """Two qubit SWAP gate. + + This gate swap the values of the two qubits. + + Parameters + ---------- + label : tuple + A tuple of the form (target1, target2). + + Examples + ======== + + """ + is_hermitian = True + gate_name = 'SWAP' + gate_name_latex = r'\text{SWAP}' + + def get_target_matrix(self, format='sympy'): + return matrix_cache.get_matrix('SWAP', format) + + def decompose(self, **options): + """Decompose the SWAP gate into CNOT gates.""" + i, j = self.targets[0], self.targets[1] + g1 = CNotGate(i, j) + g2 = CNotGate(j, i) + return g1*g2*g1 + + def plot_gate(self, circ_plot, gate_idx): + min_wire = int(_min(self.targets)) + max_wire = int(_max(self.targets)) + circ_plot.control_line(gate_idx, min_wire, max_wire) + circ_plot.swap_point(gate_idx, min_wire) + circ_plot.swap_point(gate_idx, max_wire) + + def _represent_ZGate(self, basis, **options): + """Represent the SWAP gate in the computational basis. + + The following representation is used to compute this: + + SWAP = |1><1|x|1><1| + |0><0|x|0><0| + |1><0|x|0><1| + |0><1|x|1><0| + """ + format = options.get('format', 'sympy') + targets = [int(t) for t in self.targets] + min_target = _min(targets) + max_target = _max(targets) + nqubits = options.get('nqubits', self.min_qubits) + + op01 = matrix_cache.get_matrix('op01', format) + op10 = matrix_cache.get_matrix('op10', format) + op11 = matrix_cache.get_matrix('op11', format) + op00 = matrix_cache.get_matrix('op00', format) + eye2 = matrix_cache.get_matrix('eye2', format) + + result = None + for i, j in ((op01, op10), (op10, op01), (op00, op00), (op11, op11)): + product = nqubits*[eye2] + product[nqubits - min_target - 1] = i + product[nqubits - max_target - 1] = j + new_result = matrix_tensor_product(*product) + if result is None: + result = new_result + else: + result = result + new_result + + return result + + +# Aliases for gate names. +CNOT = CNotGate +SWAP = SwapGate +def CPHASE(a,b): return CGateS((a,),Z(b)) + + +#----------------------------------------------------------------------------- +# Represent +#----------------------------------------------------------------------------- + + +def represent_zbasis(controls, targets, target_matrix, nqubits, format='sympy'): + """Represent a gate with controls, targets and target_matrix. + + This function does the low-level work of representing gates as matrices + in the standard computational basis (ZGate). Currently, we support two + main cases: + + 1. One target qubit and no control qubits. + 2. One target qubits and multiple control qubits. + + For the base of multiple controls, we use the following expression [1]: + + 1_{2**n} + (|1><1|)^{(n-1)} x (target-matrix - 1_{2}) + + Parameters + ---------- + controls : list, tuple + A sequence of control qubits. + targets : list, tuple + A sequence of target qubits. + target_matrix : sympy.Matrix, numpy.matrix, scipy.sparse + The matrix form of the transformation to be performed on the target + qubits. The format of this matrix must match that passed into + the `format` argument. + nqubits : int + The total number of qubits used for the representation. + format : str + The format of the final matrix ('sympy', 'numpy', 'scipy.sparse'). + + Examples + ======== + + References + ---------- + [1] http://www.johnlapeyre.com/qinf/qinf_html/node6.html. + """ + controls = [int(x) for x in controls] + targets = [int(x) for x in targets] + nqubits = int(nqubits) + + # This checks for the format as well. + op11 = matrix_cache.get_matrix('op11', format) + eye2 = matrix_cache.get_matrix('eye2', format) + + # Plain single qubit case + if len(controls) == 0 and len(targets) == 1: + product = [] + bit = targets[0] + # Fill product with [I1,Gate,I2] such that the unitaries, + # I, cause the gate to be applied to the correct Qubit + if bit != nqubits - 1: + product.append(matrix_eye(2**(nqubits - bit - 1), format=format)) + product.append(target_matrix) + if bit != 0: + product.append(matrix_eye(2**bit, format=format)) + return matrix_tensor_product(*product) + + # Single target, multiple controls. + elif len(targets) == 1 and len(controls) >= 1: + target = targets[0] + + # Build the non-trivial part. + product2 = [] + for i in range(nqubits): + product2.append(matrix_eye(2, format=format)) + for control in controls: + product2[nqubits - 1 - control] = op11 + product2[nqubits - 1 - target] = target_matrix - eye2 + + return matrix_eye(2**nqubits, format=format) + \ + matrix_tensor_product(*product2) + + # Multi-target, multi-control is not yet implemented. + else: + raise NotImplementedError( + 'The representation of multi-target, multi-control gates ' + 'is not implemented.' + ) + + +#----------------------------------------------------------------------------- +# Gate manipulation functions. +#----------------------------------------------------------------------------- + + +def gate_simp(circuit): + """Simplifies gates symbolically + + It first sorts gates using gate_sort. It then applies basic + simplification rules to the circuit, e.g., XGate**2 = Identity + """ + + # Bubble sort out gates that commute. + circuit = gate_sort(circuit) + + # Do simplifications by subing a simplification into the first element + # which can be simplified. We recursively call gate_simp with new circuit + # as input more simplifications exist. + if isinstance(circuit, Add): + return sum(gate_simp(t) for t in circuit.args) + elif isinstance(circuit, Mul): + circuit_args = circuit.args + elif isinstance(circuit, Pow): + b, e = circuit.as_base_exp() + circuit_args = (gate_simp(b)**e,) + else: + return circuit + + # Iterate through each element in circuit, simplify if possible. + for i in range(len(circuit_args)): + # H,X,Y or Z squared is 1. + # T**2 = S, S**2 = Z + if isinstance(circuit_args[i], Pow): + if isinstance(circuit_args[i].base, + (HadamardGate, XGate, YGate, ZGate)) \ + and isinstance(circuit_args[i].exp, Number): + # Build a new circuit taking replacing the + # H,X,Y,Z squared with one. + newargs = (circuit_args[:i] + + (circuit_args[i].base**(circuit_args[i].exp % 2),) + + circuit_args[i + 1:]) + # Recursively simplify the new circuit. + circuit = gate_simp(Mul(*newargs)) + break + elif isinstance(circuit_args[i].base, PhaseGate): + # Build a new circuit taking old circuit but splicing + # in simplification. + newargs = circuit_args[:i] + # Replace PhaseGate**2 with ZGate. + newargs = newargs + (ZGate(circuit_args[i].base.args[0])** + (Integer(circuit_args[i].exp/2)), circuit_args[i].base** + (circuit_args[i].exp % 2)) + # Append the last elements. + newargs = newargs + circuit_args[i + 1:] + # Recursively simplify the new circuit. + circuit = gate_simp(Mul(*newargs)) + break + elif isinstance(circuit_args[i].base, TGate): + # Build a new circuit taking all the old elements. + newargs = circuit_args[:i] + + # Put an Phasegate in place of any TGate**2. + newargs = newargs + (PhaseGate(circuit_args[i].base.args[0])** + Integer(circuit_args[i].exp/2), circuit_args[i].base** + (circuit_args[i].exp % 2)) + + # Append the last elements. + newargs = newargs + circuit_args[i + 1:] + # Recursively simplify the new circuit. + circuit = gate_simp(Mul(*newargs)) + break + return circuit + + +def gate_sort(circuit): + """Sorts the gates while keeping track of commutation relations + + This function uses a bubble sort to rearrange the order of gate + application. Keeps track of Quantum computations special commutation + relations (e.g. things that apply to the same Qubit do not commute with + each other) + + circuit is the Mul of gates that are to be sorted. + """ + # Make sure we have an Add or Mul. + if isinstance(circuit, Add): + return sum(gate_sort(t) for t in circuit.args) + if isinstance(circuit, Pow): + return gate_sort(circuit.base)**circuit.exp + elif isinstance(circuit, Gate): + return circuit + if not isinstance(circuit, Mul): + return circuit + + changes = True + while changes: + changes = False + circ_array = circuit.args + for i in range(len(circ_array) - 1): + # Go through each element and switch ones that are in wrong order + if isinstance(circ_array[i], (Gate, Pow)) and \ + isinstance(circ_array[i + 1], (Gate, Pow)): + # If we have a Pow object, look at only the base + first_base, first_exp = circ_array[i].as_base_exp() + second_base, second_exp = circ_array[i + 1].as_base_exp() + + # Use SymPy's hash based sorting. This is not mathematical + # sorting, but is rather based on comparing hashes of objects. + # See Basic.compare for details. + if first_base.compare(second_base) > 0: + if Commutator(first_base, second_base).doit() == 0: + new_args = (circuit.args[:i] + (circuit.args[i + 1],) + + (circuit.args[i],) + circuit.args[i + 2:]) + circuit = Mul(*new_args) + changes = True + break + if AntiCommutator(first_base, second_base).doit() == 0: + new_args = (circuit.args[:i] + (circuit.args[i + 1],) + + (circuit.args[i],) + circuit.args[i + 2:]) + sign = _S.NegativeOne**(first_exp*second_exp) + circuit = sign*Mul(*new_args) + changes = True + break + return circuit + + +#----------------------------------------------------------------------------- +# Utility functions +#----------------------------------------------------------------------------- + + +def random_circuit(ngates, nqubits, gate_space=(X, Y, Z, S, T, H, CNOT, SWAP)): + """Return a random circuit of ngates and nqubits. + + This uses an equally weighted sample of (X, Y, Z, S, T, H, CNOT, SWAP) + gates. + + Parameters + ---------- + ngates : int + The number of gates in the circuit. + nqubits : int + The number of qubits in the circuit. + gate_space : tuple + A tuple of the gate classes that will be used in the circuit. + Repeating gate classes multiple times in this tuple will increase + the frequency they appear in the random circuit. + """ + qubit_space = range(nqubits) + result = [] + for i in range(ngates): + g = random.choice(gate_space) + if g == CNotGate or g == SwapGate: + qubits = random.sample(qubit_space, 2) + g = g(*qubits) + else: + qubit = random.choice(qubit_space) + g = g(qubit) + result.append(g) + return Mul(*result) + + +def zx_basis_transform(self, format='sympy'): + """Transformation matrix from Z to X basis.""" + return matrix_cache.get_matrix('ZX', format) + + +def zy_basis_transform(self, format='sympy'): + """Transformation matrix from Z to Y basis.""" + return matrix_cache.get_matrix('ZY', format) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/grover.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/grover.py new file mode 100644 index 0000000000000000000000000000000000000000..a03bd3a61a6e0960ab66d55bcc0fc7f25936199e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/grover.py @@ -0,0 +1,345 @@ +"""Grover's algorithm and helper functions. + +Todo: + +* W gate construction (or perhaps -W gate based on Mermin's book) +* Generalize the algorithm for an unknown function that returns 1 on multiple + qubit states, not just one. +* Implement _represent_ZGate in OracleGate +""" + +from sympy.core.numbers import pi +from sympy.core.sympify import sympify +from sympy.core.basic import Atom +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.matrices.dense import eye +from sympy.core.numbers import NegativeOne +from sympy.physics.quantum.qapply import qapply +from sympy.physics.quantum.qexpr import QuantumError +from sympy.physics.quantum.hilbert import ComplexSpace +from sympy.physics.quantum.operator import UnitaryOperator +from sympy.physics.quantum.gate import Gate +from sympy.physics.quantum.qubit import IntQubit + +__all__ = [ + 'OracleGate', + 'WGate', + 'superposition_basis', + 'grover_iteration', + 'apply_grover' +] + + +def superposition_basis(nqubits): + """Creates an equal superposition of the computational basis. + + Parameters + ========== + + nqubits : int + The number of qubits. + + Returns + ======= + + state : Qubit + An equal superposition of the computational basis with nqubits. + + Examples + ======== + + Create an equal superposition of 2 qubits:: + + >>> from sympy.physics.quantum.grover import superposition_basis + >>> superposition_basis(2) + |0>/2 + |1>/2 + |2>/2 + |3>/2 + """ + + amp = 1/sqrt(2**nqubits) + return sum(amp*IntQubit(n, nqubits=nqubits) for n in range(2**nqubits)) + +class OracleGateFunction(Atom): + """Wrapper for python functions used in `OracleGate`s""" + + def __new__(cls, function): + if not callable(function): + raise TypeError('Callable expected, got: %r' % function) + obj = Atom.__new__(cls) + obj.function = function + return obj + + def _hashable_content(self): + return type(self), self.function + + def __call__(self, *args): + return self.function(*args) + + +class OracleGate(Gate): + """A black box gate. + + The gate marks the desired qubits of an unknown function by flipping + the sign of the qubits. The unknown function returns true when it + finds its desired qubits and false otherwise. + + Parameters + ========== + + qubits : int + Number of qubits. + + oracle : callable + A callable function that returns a boolean on a computational basis. + + Examples + ======== + + Apply an Oracle gate that flips the sign of ``|2>`` on different qubits:: + + >>> from sympy.physics.quantum.qubit import IntQubit + >>> from sympy.physics.quantum.qapply import qapply + >>> from sympy.physics.quantum.grover import OracleGate + >>> f = lambda qubits: qubits == IntQubit(2) + >>> v = OracleGate(2, f) + >>> qapply(v*IntQubit(2)) + -|2> + >>> qapply(v*IntQubit(3)) + |3> + """ + + gate_name = 'V' + gate_name_latex = 'V' + + #------------------------------------------------------------------------- + # Initialization/creation + #------------------------------------------------------------------------- + + @classmethod + def _eval_args(cls, args): + if len(args) != 2: + raise QuantumError( + 'Insufficient/excessive arguments to Oracle. Please ' + + 'supply the number of qubits and an unknown function.' + ) + sub_args = (args[0],) + sub_args = UnitaryOperator._eval_args(sub_args) + if not sub_args[0].is_Integer: + raise TypeError('Integer expected, got: %r' % sub_args[0]) + + function = args[1] + if not isinstance(function, OracleGateFunction): + function = OracleGateFunction(function) + + return (sub_args[0], function) + + @classmethod + def _eval_hilbert_space(cls, args): + """This returns the smallest possible Hilbert space.""" + return ComplexSpace(2)**args[0] + + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + + @property + def search_function(self): + """The unknown function that helps find the sought after qubits.""" + return self.label[1] + + @property + def targets(self): + """A tuple of target qubits.""" + return sympify(tuple(range(self.args[0]))) + + #------------------------------------------------------------------------- + # Apply + #------------------------------------------------------------------------- + + def _apply_operator_Qubit(self, qubits, **options): + """Apply this operator to a Qubit subclass. + + Parameters + ========== + + qubits : Qubit + The qubit subclass to apply this operator to. + + Returns + ======= + + state : Expr + The resulting quantum state. + """ + if qubits.nqubits != self.nqubits: + raise QuantumError( + 'OracleGate operates on %r qubits, got: %r' + % (self.nqubits, qubits.nqubits) + ) + # If function returns 1 on qubits + # return the negative of the qubits (flip the sign) + if self.search_function(qubits): + return -qubits + else: + return qubits + + #------------------------------------------------------------------------- + # Represent + #------------------------------------------------------------------------- + + def _represent_ZGate(self, basis, **options): + """ + Represent the OracleGate in the computational basis. + """ + nbasis = 2**self.nqubits # compute it only once + matrixOracle = eye(nbasis) + # Flip the sign given the output of the oracle function + for i in range(nbasis): + if self.search_function(IntQubit(i, nqubits=self.nqubits)): + matrixOracle[i, i] = NegativeOne() + return matrixOracle + + +class WGate(Gate): + """General n qubit W Gate in Grover's algorithm. + + The gate performs the operation ``2|phi> = (tensor product of n Hadamards)*(|0> with n qubits)`` + + Parameters + ========== + + nqubits : int + The number of qubits to operate on + + """ + + gate_name = 'W' + gate_name_latex = 'W' + + @classmethod + def _eval_args(cls, args): + if len(args) != 1: + raise QuantumError( + 'Insufficient/excessive arguments to W gate. Please ' + + 'supply the number of qubits to operate on.' + ) + args = UnitaryOperator._eval_args(args) + if not args[0].is_Integer: + raise TypeError('Integer expected, got: %r' % args[0]) + return args + + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + + @property + def targets(self): + return sympify(tuple(reversed(range(self.args[0])))) + + #------------------------------------------------------------------------- + # Apply + #------------------------------------------------------------------------- + + def _apply_operator_Qubit(self, qubits, **options): + """ + qubits: a set of qubits (Qubit) + Returns: quantum object (quantum expression - QExpr) + """ + if qubits.nqubits != self.nqubits: + raise QuantumError( + 'WGate operates on %r qubits, got: %r' + % (self.nqubits, qubits.nqubits) + ) + + # See 'Quantum Computer Science' by David Mermin p.92 -> W|a> result + # Return (2/(sqrt(2^n)))|phi> - |a> where |a> is the current basis + # state and phi is the superposition of basis states (see function + # create_computational_basis above) + basis_states = superposition_basis(self.nqubits) + change_to_basis = (2/sqrt(2**self.nqubits))*basis_states + return change_to_basis - qubits + + +def grover_iteration(qstate, oracle): + """Applies one application of the Oracle and W Gate, WV. + + Parameters + ========== + + qstate : Qubit + A superposition of qubits. + oracle : OracleGate + The black box operator that flips the sign of the desired basis qubits. + + Returns + ======= + + Qubit : The qubits after applying the Oracle and W gate. + + Examples + ======== + + Perform one iteration of grover's algorithm to see a phase change:: + + >>> from sympy.physics.quantum.qapply import qapply + >>> from sympy.physics.quantum.qubit import IntQubit + >>> from sympy.physics.quantum.grover import OracleGate + >>> from sympy.physics.quantum.grover import superposition_basis + >>> from sympy.physics.quantum.grover import grover_iteration + >>> numqubits = 2 + >>> basis_states = superposition_basis(numqubits) + >>> f = lambda qubits: qubits == IntQubit(2) + >>> v = OracleGate(numqubits, f) + >>> qapply(grover_iteration(basis_states, v)) + |2> + + """ + wgate = WGate(oracle.nqubits) + return wgate*oracle*qstate + + +def apply_grover(oracle, nqubits, iterations=None): + """Applies grover's algorithm. + + Parameters + ========== + + oracle : callable + The unknown callable function that returns true when applied to the + desired qubits and false otherwise. + + Returns + ======= + + state : Expr + The resulting state after Grover's algorithm has been iterated. + + Examples + ======== + + Apply grover's algorithm to an even superposition of 2 qubits:: + + >>> from sympy.physics.quantum.qapply import qapply + >>> from sympy.physics.quantum.qubit import IntQubit + >>> from sympy.physics.quantum.grover import apply_grover + >>> f = lambda qubits: qubits == IntQubit(2) + >>> qapply(apply_grover(f, 2)) + |2> + + """ + if nqubits <= 0: + raise QuantumError( + 'Grover\'s algorithm needs nqubits > 0, received %r qubits' + % nqubits + ) + if iterations is None: + iterations = floor(sqrt(2**nqubits)*(pi/4)) + + v = OracleGate(nqubits, oracle) + iterated = superposition_basis(nqubits) + for iter in range(iterations): + iterated = grover_iteration(iterated, v) + iterated = qapply(iterated) + + return iterated diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/hilbert.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/hilbert.py new file mode 100644 index 0000000000000000000000000000000000000000..f475a9e83a6ccc93e9e2dbb9873ad111c1d05f93 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/hilbert.py @@ -0,0 +1,653 @@ +"""Hilbert spaces for quantum mechanics. + +Authors: +* Brian Granger +* Matt Curry +""" + +from functools import reduce + +from sympy.core.basic import Basic +from sympy.core.singleton import S +from sympy.core.sympify import sympify +from sympy.sets.sets import Interval +from sympy.printing.pretty.stringpict import prettyForm +from sympy.physics.quantum.qexpr import QuantumError + + +__all__ = [ + 'HilbertSpaceError', + 'HilbertSpace', + 'TensorProductHilbertSpace', + 'TensorPowerHilbertSpace', + 'DirectSumHilbertSpace', + 'ComplexSpace', + 'L2', + 'FockSpace' +] + +#----------------------------------------------------------------------------- +# Main objects +#----------------------------------------------------------------------------- + + +class HilbertSpaceError(QuantumError): + pass + +#----------------------------------------------------------------------------- +# Main objects +#----------------------------------------------------------------------------- + + +class HilbertSpace(Basic): + """An abstract Hilbert space for quantum mechanics. + + In short, a Hilbert space is an abstract vector space that is complete + with inner products defined [1]_. + + Examples + ======== + + >>> from sympy.physics.quantum.hilbert import HilbertSpace + >>> hs = HilbertSpace() + >>> hs + H + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hilbert_space + """ + + def __new__(cls): + obj = Basic.__new__(cls) + return obj + + @property + def dimension(self): + """Return the Hilbert dimension of the space.""" + raise NotImplementedError('This Hilbert space has no dimension.') + + def __add__(self, other): + return DirectSumHilbertSpace(self, other) + + def __radd__(self, other): + return DirectSumHilbertSpace(other, self) + + def __mul__(self, other): + return TensorProductHilbertSpace(self, other) + + def __rmul__(self, other): + return TensorProductHilbertSpace(other, self) + + def __pow__(self, other, mod=None): + if mod is not None: + raise ValueError('The third argument to __pow__ is not supported \ + for Hilbert spaces.') + return TensorPowerHilbertSpace(self, other) + + def __contains__(self, other): + """Is the operator or state in this Hilbert space. + + This is checked by comparing the classes of the Hilbert spaces, not + the instances. This is to allow Hilbert Spaces with symbolic + dimensions. + """ + if other.hilbert_space.__class__ == self.__class__: + return True + else: + return False + + def _sympystr(self, printer, *args): + return 'H' + + def _pretty(self, printer, *args): + ustr = '\N{LATIN CAPITAL LETTER H}' + return prettyForm(ustr) + + def _latex(self, printer, *args): + return r'\mathcal{H}' + + +class ComplexSpace(HilbertSpace): + """Finite dimensional Hilbert space of complex vectors. + + The elements of this Hilbert space are n-dimensional complex valued + vectors with the usual inner product that takes the complex conjugate + of the vector on the right. + + A classic example of this type of Hilbert space is spin-1/2, which is + ``ComplexSpace(2)``. Generalizing to spin-s, the space is + ``ComplexSpace(2*s+1)``. Quantum computing with N qubits is done with the + direct product space ``ComplexSpace(2)**N``. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.quantum.hilbert import ComplexSpace + >>> c1 = ComplexSpace(2) + >>> c1 + C(2) + >>> c1.dimension + 2 + + >>> n = symbols('n') + >>> c2 = ComplexSpace(n) + >>> c2 + C(n) + >>> c2.dimension + n + + """ + + def __new__(cls, dimension): + dimension = sympify(dimension) + r = cls.eval(dimension) + if isinstance(r, Basic): + return r + obj = Basic.__new__(cls, dimension) + return obj + + @classmethod + def eval(cls, dimension): + if len(dimension.atoms()) == 1: + if not (dimension.is_Integer and dimension > 0 or dimension is S.Infinity + or dimension.is_Symbol): + raise TypeError('The dimension of a ComplexSpace can only' + 'be a positive integer, oo, or a Symbol: %r' + % dimension) + else: + for dim in dimension.atoms(): + if not (dim.is_Integer or dim is S.Infinity or dim.is_Symbol): + raise TypeError('The dimension of a ComplexSpace can only' + ' contain integers, oo, or a Symbol: %r' + % dim) + + @property + def dimension(self): + return self.args[0] + + def _sympyrepr(self, printer, *args): + return "%s(%s)" % (self.__class__.__name__, + printer._print(self.dimension, *args)) + + def _sympystr(self, printer, *args): + return "C(%s)" % printer._print(self.dimension, *args) + + def _pretty(self, printer, *args): + ustr = '\N{LATIN CAPITAL LETTER C}' + pform_exp = printer._print(self.dimension, *args) + pform_base = prettyForm(ustr) + return pform_base**pform_exp + + def _latex(self, printer, *args): + return r'\mathcal{C}^{%s}' % printer._print(self.dimension, *args) + + +class L2(HilbertSpace): + """The Hilbert space of square integrable functions on an interval. + + An L2 object takes in a single SymPy Interval argument which represents + the interval its functions (vectors) are defined on. + + Examples + ======== + + >>> from sympy import Interval, oo + >>> from sympy.physics.quantum.hilbert import L2 + >>> hs = L2(Interval(0,oo)) + >>> hs + L2(Interval(0, oo)) + >>> hs.dimension + oo + >>> hs.interval + Interval(0, oo) + + """ + + def __new__(cls, interval): + if not isinstance(interval, Interval): + raise TypeError('L2 interval must be an Interval instance: %r' + % interval) + obj = Basic.__new__(cls, interval) + return obj + + @property + def dimension(self): + return S.Infinity + + @property + def interval(self): + return self.args[0] + + def _sympyrepr(self, printer, *args): + return "L2(%s)" % printer._print(self.interval, *args) + + def _sympystr(self, printer, *args): + return "L2(%s)" % printer._print(self.interval, *args) + + def _pretty(self, printer, *args): + pform_exp = prettyForm('2') + pform_base = prettyForm('L') + return pform_base**pform_exp + + def _latex(self, printer, *args): + interval = printer._print(self.interval, *args) + return r'{\mathcal{L}^2}\left( %s \right)' % interval + + +class FockSpace(HilbertSpace): + """The Hilbert space for second quantization. + + Technically, this Hilbert space is a infinite direct sum of direct + products of single particle Hilbert spaces [1]_. This is a mess, so we have + a class to represent it directly. + + Examples + ======== + + >>> from sympy.physics.quantum.hilbert import FockSpace + >>> hs = FockSpace() + >>> hs + F + >>> hs.dimension + oo + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Fock_space + """ + + def __new__(cls): + obj = Basic.__new__(cls) + return obj + + @property + def dimension(self): + return S.Infinity + + def _sympyrepr(self, printer, *args): + return "FockSpace()" + + def _sympystr(self, printer, *args): + return "F" + + def _pretty(self, printer, *args): + ustr = '\N{LATIN CAPITAL LETTER F}' + return prettyForm(ustr) + + def _latex(self, printer, *args): + return r'\mathcal{F}' + + +class TensorProductHilbertSpace(HilbertSpace): + """A tensor product of Hilbert spaces [1]_. + + The tensor product between Hilbert spaces is represented by the + operator ``*`` Products of the same Hilbert space will be combined into + tensor powers. + + A ``TensorProductHilbertSpace`` object takes in an arbitrary number of + ``HilbertSpace`` objects as its arguments. In addition, multiplication of + ``HilbertSpace`` objects will automatically return this tensor product + object. + + Examples + ======== + + >>> from sympy.physics.quantum.hilbert import ComplexSpace, FockSpace + >>> from sympy import symbols + + >>> c = ComplexSpace(2) + >>> f = FockSpace() + >>> hs = c*f + >>> hs + C(2)*F + >>> hs.dimension + oo + >>> hs.spaces + (C(2), F) + + >>> c1 = ComplexSpace(2) + >>> n = symbols('n') + >>> c2 = ComplexSpace(n) + >>> hs = c1*c2 + >>> hs + C(2)*C(n) + >>> hs.dimension + 2*n + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hilbert_space#Tensor_products + """ + + def __new__(cls, *args): + r = cls.eval(args) + if isinstance(r, Basic): + return r + obj = Basic.__new__(cls, *args) + return obj + + @classmethod + def eval(cls, args): + """Evaluates the direct product.""" + new_args = [] + recall = False + #flatten arguments + for arg in args: + if isinstance(arg, TensorProductHilbertSpace): + new_args.extend(arg.args) + recall = True + elif isinstance(arg, (HilbertSpace, TensorPowerHilbertSpace)): + new_args.append(arg) + else: + raise TypeError('Hilbert spaces can only be multiplied by \ + other Hilbert spaces: %r' % arg) + #combine like arguments into direct powers + comb_args = [] + prev_arg = None + for new_arg in new_args: + if prev_arg is not None: + if isinstance(new_arg, TensorPowerHilbertSpace) and \ + isinstance(prev_arg, TensorPowerHilbertSpace) and \ + new_arg.base == prev_arg.base: + prev_arg = new_arg.base**(new_arg.exp + prev_arg.exp) + elif isinstance(new_arg, TensorPowerHilbertSpace) and \ + new_arg.base == prev_arg: + prev_arg = prev_arg**(new_arg.exp + 1) + elif isinstance(prev_arg, TensorPowerHilbertSpace) and \ + new_arg == prev_arg.base: + prev_arg = new_arg**(prev_arg.exp + 1) + elif new_arg == prev_arg: + prev_arg = new_arg**2 + else: + comb_args.append(prev_arg) + prev_arg = new_arg + elif prev_arg is None: + prev_arg = new_arg + comb_args.append(prev_arg) + if recall: + return TensorProductHilbertSpace(*comb_args) + elif len(comb_args) == 1: + return TensorPowerHilbertSpace(comb_args[0].base, comb_args[0].exp) + else: + return None + + @property + def dimension(self): + arg_list = [arg.dimension for arg in self.args] + if S.Infinity in arg_list: + return S.Infinity + else: + return reduce(lambda x, y: x*y, arg_list) + + @property + def spaces(self): + """A tuple of the Hilbert spaces in this tensor product.""" + return self.args + + def _spaces_printer(self, printer, *args): + spaces_strs = [] + for arg in self.args: + s = printer._print(arg, *args) + if isinstance(arg, DirectSumHilbertSpace): + s = '(%s)' % s + spaces_strs.append(s) + return spaces_strs + + def _sympyrepr(self, printer, *args): + spaces_reprs = self._spaces_printer(printer, *args) + return "TensorProductHilbertSpace(%s)" % ','.join(spaces_reprs) + + def _sympystr(self, printer, *args): + spaces_strs = self._spaces_printer(printer, *args) + return '*'.join(spaces_strs) + + def _pretty(self, printer, *args): + length = len(self.args) + pform = printer._print('', *args) + for i in range(length): + next_pform = printer._print(self.args[i], *args) + if isinstance(self.args[i], (DirectSumHilbertSpace, + TensorProductHilbertSpace)): + next_pform = prettyForm( + *next_pform.parens(left='(', right=')') + ) + pform = prettyForm(*pform.right(next_pform)) + if i != length - 1: + if printer._use_unicode: + pform = prettyForm(*pform.right(' ' + '\N{N-ARY CIRCLED TIMES OPERATOR}' + ' ')) + else: + pform = prettyForm(*pform.right(' x ')) + return pform + + def _latex(self, printer, *args): + length = len(self.args) + s = '' + for i in range(length): + arg_s = printer._print(self.args[i], *args) + if isinstance(self.args[i], (DirectSumHilbertSpace, + TensorProductHilbertSpace)): + arg_s = r'\left(%s\right)' % arg_s + s = s + arg_s + if i != length - 1: + s = s + r'\otimes ' + return s + + +class DirectSumHilbertSpace(HilbertSpace): + """A direct sum of Hilbert spaces [1]_. + + This class uses the ``+`` operator to represent direct sums between + different Hilbert spaces. + + A ``DirectSumHilbertSpace`` object takes in an arbitrary number of + ``HilbertSpace`` objects as its arguments. Also, addition of + ``HilbertSpace`` objects will automatically return a direct sum object. + + Examples + ======== + + >>> from sympy.physics.quantum.hilbert import ComplexSpace, FockSpace + + >>> c = ComplexSpace(2) + >>> f = FockSpace() + >>> hs = c+f + >>> hs + C(2)+F + >>> hs.dimension + oo + >>> list(hs.spaces) + [C(2), F] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hilbert_space#Direct_sums + """ + def __new__(cls, *args): + r = cls.eval(args) + if isinstance(r, Basic): + return r + obj = Basic.__new__(cls, *args) + return obj + + @classmethod + def eval(cls, args): + """Evaluates the direct product.""" + new_args = [] + recall = False + #flatten arguments + for arg in args: + if isinstance(arg, DirectSumHilbertSpace): + new_args.extend(arg.args) + recall = True + elif isinstance(arg, HilbertSpace): + new_args.append(arg) + else: + raise TypeError('Hilbert spaces can only be summed with other \ + Hilbert spaces: %r' % arg) + if recall: + return DirectSumHilbertSpace(*new_args) + else: + return None + + @property + def dimension(self): + arg_list = [arg.dimension for arg in self.args] + if S.Infinity in arg_list: + return S.Infinity + else: + return reduce(lambda x, y: x + y, arg_list) + + @property + def spaces(self): + """A tuple of the Hilbert spaces in this direct sum.""" + return self.args + + def _sympyrepr(self, printer, *args): + spaces_reprs = [printer._print(arg, *args) for arg in self.args] + return "DirectSumHilbertSpace(%s)" % ','.join(spaces_reprs) + + def _sympystr(self, printer, *args): + spaces_strs = [printer._print(arg, *args) for arg in self.args] + return '+'.join(spaces_strs) + + def _pretty(self, printer, *args): + length = len(self.args) + pform = printer._print('', *args) + for i in range(length): + next_pform = printer._print(self.args[i], *args) + if isinstance(self.args[i], (DirectSumHilbertSpace, + TensorProductHilbertSpace)): + next_pform = prettyForm( + *next_pform.parens(left='(', right=')') + ) + pform = prettyForm(*pform.right(next_pform)) + if i != length - 1: + if printer._use_unicode: + pform = prettyForm(*pform.right(' \N{CIRCLED PLUS} ')) + else: + pform = prettyForm(*pform.right(' + ')) + return pform + + def _latex(self, printer, *args): + length = len(self.args) + s = '' + for i in range(length): + arg_s = printer._print(self.args[i], *args) + if isinstance(self.args[i], (DirectSumHilbertSpace, + TensorProductHilbertSpace)): + arg_s = r'\left(%s\right)' % arg_s + s = s + arg_s + if i != length - 1: + s = s + r'\oplus ' + return s + + +class TensorPowerHilbertSpace(HilbertSpace): + """An exponentiated Hilbert space [1]_. + + Tensor powers (repeated tensor products) are represented by the + operator ``**`` Identical Hilbert spaces that are multiplied together + will be automatically combined into a single tensor power object. + + Any Hilbert space, product, or sum may be raised to a tensor power. The + ``TensorPowerHilbertSpace`` takes two arguments: the Hilbert space; and the + tensor power (number). + + Examples + ======== + + >>> from sympy.physics.quantum.hilbert import ComplexSpace, FockSpace + >>> from sympy import symbols + + >>> n = symbols('n') + >>> c = ComplexSpace(2) + >>> hs = c**n + >>> hs + C(2)**n + >>> hs.dimension + 2**n + + >>> c = ComplexSpace(2) + >>> c*c + C(2)**2 + >>> f = FockSpace() + >>> c*f*f + C(2)*F**2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hilbert_space#Tensor_products + """ + + def __new__(cls, *args): + r = cls.eval(args) + if isinstance(r, Basic): + return r + return Basic.__new__(cls, *r) + + @classmethod + def eval(cls, args): + new_args = args[0], sympify(args[1]) + exp = new_args[1] + #simplify hs**1 -> hs + if exp is S.One: + return args[0] + #simplify hs**0 -> 1 + if exp is S.Zero: + return S.One + #check (and allow) for hs**(x+42+y...) case + if len(exp.atoms()) == 1: + if not (exp.is_Integer and exp >= 0 or exp.is_Symbol): + raise ValueError('Hilbert spaces can only be raised to \ + positive integers or Symbols: %r' % exp) + else: + for power in exp.atoms(): + if not (power.is_Integer or power.is_Symbol): + raise ValueError('Tensor powers can only contain integers \ + or Symbols: %r' % power) + return new_args + + @property + def base(self): + return self.args[0] + + @property + def exp(self): + return self.args[1] + + @property + def dimension(self): + if self.base.dimension is S.Infinity: + return S.Infinity + else: + return self.base.dimension**self.exp + + def _sympyrepr(self, printer, *args): + return "TensorPowerHilbertSpace(%s,%s)" % (printer._print(self.base, + *args), printer._print(self.exp, *args)) + + def _sympystr(self, printer, *args): + return "%s**%s" % (printer._print(self.base, *args), + printer._print(self.exp, *args)) + + def _pretty(self, printer, *args): + pform_exp = printer._print(self.exp, *args) + if printer._use_unicode: + pform_exp = prettyForm(*pform_exp.left(prettyForm('\N{N-ARY CIRCLED TIMES OPERATOR}'))) + else: + pform_exp = prettyForm(*pform_exp.left(prettyForm('x'))) + pform_base = printer._print(self.base, *args) + return pform_base**pform_exp + + def _latex(self, printer, *args): + base = printer._print(self.base, *args) + exp = printer._print(self.exp, *args) + return r'{%s}^{\otimes %s}' % (base, exp) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/identitysearch.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/identitysearch.py new file mode 100644 index 0000000000000000000000000000000000000000..9a178e9b808450b7ce91175600d6b393fc9797d6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/identitysearch.py @@ -0,0 +1,853 @@ +from collections import deque +from sympy.core.random import randint + +from sympy.external import import_module +from sympy.core.basic import Basic +from sympy.core.mul import Mul +from sympy.core.numbers import Number, equal_valued +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.physics.quantum.represent import represent +from sympy.physics.quantum.dagger import Dagger + +__all__ = [ + # Public interfaces + 'generate_gate_rules', + 'generate_equivalent_ids', + 'GateIdentity', + 'bfs_identity_search', + 'random_identity_search', + + # "Private" functions + 'is_scalar_sparse_matrix', + 'is_scalar_nonsparse_matrix', + 'is_degenerate', + 'is_reducible', +] + +np = import_module('numpy') +scipy = import_module('scipy', import_kwargs={'fromlist': ['sparse']}) + + +def is_scalar_sparse_matrix(circuit, nqubits, identity_only, eps=1e-11): + """Checks if a given scipy.sparse matrix is a scalar matrix. + + A scalar matrix is such that B = bI, where B is the scalar + matrix, b is some scalar multiple, and I is the identity + matrix. A scalar matrix would have only the element b along + it's main diagonal and zeroes elsewhere. + + Parameters + ========== + + circuit : Gate tuple + Sequence of quantum gates representing a quantum circuit + nqubits : int + Number of qubits in the circuit + identity_only : bool + Check for only identity matrices + eps : number + The tolerance value for zeroing out elements in the matrix. + Values in the range [-eps, +eps] will be changed to a zero. + """ + + if not np or not scipy: + pass + + matrix = represent(Mul(*circuit), nqubits=nqubits, + format='scipy.sparse') + + # In some cases, represent returns a 1D scalar value in place + # of a multi-dimensional scalar matrix + if (isinstance(matrix, int)): + return matrix == 1 if identity_only else True + + # If represent returns a matrix, check if the matrix is diagonal + # and if every item along the diagonal is the same + else: + # Due to floating pointing operations, must zero out + # elements that are "very" small in the dense matrix + # See parameter for default value. + + # Get the ndarray version of the dense matrix + dense_matrix = matrix.todense().getA() + # Since complex values can't be compared, must split + # the matrix into real and imaginary components + # Find the real values in between -eps and eps + bool_real = np.logical_and(dense_matrix.real > -eps, + dense_matrix.real < eps) + # Find the imaginary values between -eps and eps + bool_imag = np.logical_and(dense_matrix.imag > -eps, + dense_matrix.imag < eps) + # Replaces values between -eps and eps with 0 + corrected_real = np.where(bool_real, 0.0, dense_matrix.real) + corrected_imag = np.where(bool_imag, 0.0, dense_matrix.imag) + # Convert the matrix with real values into imaginary values + corrected_imag = corrected_imag * complex(1j) + # Recombine the real and imaginary components + corrected_dense = corrected_real + corrected_imag + + # Check if it's diagonal + row_indices = corrected_dense.nonzero()[0] + col_indices = corrected_dense.nonzero()[1] + # Check if the rows indices and columns indices are the same + # If they match, then matrix only contains elements along diagonal + bool_indices = row_indices == col_indices + is_diagonal = bool_indices.all() + + first_element = corrected_dense[0][0] + # If the first element is a zero, then can't rescale matrix + # and definitely not diagonal + if (first_element == 0.0 + 0.0j): + return False + + # The dimensions of the dense matrix should still + # be 2^nqubits if there are elements all along the + # the main diagonal + trace_of_corrected = (corrected_dense/first_element).trace() + expected_trace = pow(2, nqubits) + has_correct_trace = trace_of_corrected == expected_trace + + # If only looking for identity matrices + # first element must be a 1 + real_is_one = abs(first_element.real - 1.0) < eps + imag_is_zero = abs(first_element.imag) < eps + is_one = real_is_one and imag_is_zero + is_identity = is_one if identity_only else True + return bool(is_diagonal and has_correct_trace and is_identity) + + +def is_scalar_nonsparse_matrix(circuit, nqubits, identity_only, eps=None): + """Checks if a given circuit, in matrix form, is equivalent to + a scalar value. + + Parameters + ========== + + circuit : Gate tuple + Sequence of quantum gates representing a quantum circuit + nqubits : int + Number of qubits in the circuit + identity_only : bool + Check for only identity matrices + eps : number + This argument is ignored. It is just for signature compatibility with + is_scalar_sparse_matrix. + + Note: Used in situations when is_scalar_sparse_matrix has bugs + """ + + matrix = represent(Mul(*circuit), nqubits=nqubits) + + # In some cases, represent returns a 1D scalar value in place + # of a multi-dimensional scalar matrix + if (isinstance(matrix, Number)): + return matrix == 1 if identity_only else True + + # If represent returns a matrix, check if the matrix is diagonal + # and if every item along the diagonal is the same + else: + # Added up the diagonal elements + matrix_trace = matrix.trace() + # Divide the trace by the first element in the matrix + # if matrix is not required to be the identity matrix + adjusted_matrix_trace = (matrix_trace/matrix[0] + if not identity_only + else matrix_trace) + + is_identity = equal_valued(matrix[0], 1) if identity_only else True + + has_correct_trace = adjusted_matrix_trace == pow(2, nqubits) + + # The matrix is scalar if it's diagonal and the adjusted trace + # value is equal to 2^nqubits + return bool( + matrix.is_diagonal() and has_correct_trace and is_identity) + +if np and scipy: + is_scalar_matrix = is_scalar_sparse_matrix +else: + is_scalar_matrix = is_scalar_nonsparse_matrix + + +def _get_min_qubits(a_gate): + if isinstance(a_gate, Pow): + return a_gate.base.min_qubits + else: + return a_gate.min_qubits + + +def ll_op(left, right): + """Perform a LL operation. + + A LL operation multiplies both left and right circuits + with the dagger of the left circuit's leftmost gate, and + the dagger is multiplied on the left side of both circuits. + + If a LL is possible, it returns the new gate rule as a + 2-tuple (LHS, RHS), where LHS is the left circuit and + and RHS is the right circuit of the new rule. + If a LL is not possible, None is returned. + + Parameters + ========== + + left : Gate tuple + The left circuit of a gate rule expression. + right : Gate tuple + The right circuit of a gate rule expression. + + Examples + ======== + + Generate a new gate rule using a LL operation: + + >>> from sympy.physics.quantum.identitysearch import ll_op + >>> from sympy.physics.quantum.gate import X, Y, Z + >>> x = X(0); y = Y(0); z = Z(0) + >>> ll_op((x, y, z), ()) + ((Y(0), Z(0)), (X(0),)) + + >>> ll_op((y, z), (x,)) + ((Z(0),), (Y(0), X(0))) + """ + + if (len(left) > 0): + ll_gate = left[0] + ll_gate_is_unitary = is_scalar_matrix( + (Dagger(ll_gate), ll_gate), _get_min_qubits(ll_gate), True) + + if (len(left) > 0 and ll_gate_is_unitary): + # Get the new left side w/o the leftmost gate + new_left = left[1:len(left)] + # Add the leftmost gate to the left position on the right side + new_right = (Dagger(ll_gate),) + right + # Return the new gate rule + return (new_left, new_right) + + return None + + +def lr_op(left, right): + """Perform a LR operation. + + A LR operation multiplies both left and right circuits + with the dagger of the left circuit's rightmost gate, and + the dagger is multiplied on the right side of both circuits. + + If a LR is possible, it returns the new gate rule as a + 2-tuple (LHS, RHS), where LHS is the left circuit and + and RHS is the right circuit of the new rule. + If a LR is not possible, None is returned. + + Parameters + ========== + + left : Gate tuple + The left circuit of a gate rule expression. + right : Gate tuple + The right circuit of a gate rule expression. + + Examples + ======== + + Generate a new gate rule using a LR operation: + + >>> from sympy.physics.quantum.identitysearch import lr_op + >>> from sympy.physics.quantum.gate import X, Y, Z + >>> x = X(0); y = Y(0); z = Z(0) + >>> lr_op((x, y, z), ()) + ((X(0), Y(0)), (Z(0),)) + + >>> lr_op((x, y), (z,)) + ((X(0),), (Z(0), Y(0))) + """ + + if (len(left) > 0): + lr_gate = left[len(left) - 1] + lr_gate_is_unitary = is_scalar_matrix( + (Dagger(lr_gate), lr_gate), _get_min_qubits(lr_gate), True) + + if (len(left) > 0 and lr_gate_is_unitary): + # Get the new left side w/o the rightmost gate + new_left = left[0:len(left) - 1] + # Add the rightmost gate to the right position on the right side + new_right = right + (Dagger(lr_gate),) + # Return the new gate rule + return (new_left, new_right) + + return None + + +def rl_op(left, right): + """Perform a RL operation. + + A RL operation multiplies both left and right circuits + with the dagger of the right circuit's leftmost gate, and + the dagger is multiplied on the left side of both circuits. + + If a RL is possible, it returns the new gate rule as a + 2-tuple (LHS, RHS), where LHS is the left circuit and + and RHS is the right circuit of the new rule. + If a RL is not possible, None is returned. + + Parameters + ========== + + left : Gate tuple + The left circuit of a gate rule expression. + right : Gate tuple + The right circuit of a gate rule expression. + + Examples + ======== + + Generate a new gate rule using a RL operation: + + >>> from sympy.physics.quantum.identitysearch import rl_op + >>> from sympy.physics.quantum.gate import X, Y, Z + >>> x = X(0); y = Y(0); z = Z(0) + >>> rl_op((x,), (y, z)) + ((Y(0), X(0)), (Z(0),)) + + >>> rl_op((x, y), (z,)) + ((Z(0), X(0), Y(0)), ()) + """ + + if (len(right) > 0): + rl_gate = right[0] + rl_gate_is_unitary = is_scalar_matrix( + (Dagger(rl_gate), rl_gate), _get_min_qubits(rl_gate), True) + + if (len(right) > 0 and rl_gate_is_unitary): + # Get the new right side w/o the leftmost gate + new_right = right[1:len(right)] + # Add the leftmost gate to the left position on the left side + new_left = (Dagger(rl_gate),) + left + # Return the new gate rule + return (new_left, new_right) + + return None + + +def rr_op(left, right): + """Perform a RR operation. + + A RR operation multiplies both left and right circuits + with the dagger of the right circuit's rightmost gate, and + the dagger is multiplied on the right side of both circuits. + + If a RR is possible, it returns the new gate rule as a + 2-tuple (LHS, RHS), where LHS is the left circuit and + and RHS is the right circuit of the new rule. + If a RR is not possible, None is returned. + + Parameters + ========== + + left : Gate tuple + The left circuit of a gate rule expression. + right : Gate tuple + The right circuit of a gate rule expression. + + Examples + ======== + + Generate a new gate rule using a RR operation: + + >>> from sympy.physics.quantum.identitysearch import rr_op + >>> from sympy.physics.quantum.gate import X, Y, Z + >>> x = X(0); y = Y(0); z = Z(0) + >>> rr_op((x, y), (z,)) + ((X(0), Y(0), Z(0)), ()) + + >>> rr_op((x,), (y, z)) + ((X(0), Z(0)), (Y(0),)) + """ + + if (len(right) > 0): + rr_gate = right[len(right) - 1] + rr_gate_is_unitary = is_scalar_matrix( + (Dagger(rr_gate), rr_gate), _get_min_qubits(rr_gate), True) + + if (len(right) > 0 and rr_gate_is_unitary): + # Get the new right side w/o the rightmost gate + new_right = right[0:len(right) - 1] + # Add the rightmost gate to the right position on the right side + new_left = left + (Dagger(rr_gate),) + # Return the new gate rule + return (new_left, new_right) + + return None + + +def generate_gate_rules(gate_seq, return_as_muls=False): + """Returns a set of gate rules. Each gate rules is represented + as a 2-tuple of tuples or Muls. An empty tuple represents an arbitrary + scalar value. + + This function uses the four operations (LL, LR, RL, RR) + to generate the gate rules. + + A gate rule is an expression such as ABC = D or AB = CD, where + A, B, C, and D are gates. Each value on either side of the + equal sign represents a circuit. The four operations allow + one to find a set of equivalent circuits from a gate identity. + The letters denoting the operation tell the user what + activities to perform on each expression. The first letter + indicates which side of the equal sign to focus on. The + second letter indicates which gate to focus on given the + side. Once this information is determined, the inverse + of the gate is multiplied on both circuits to create a new + gate rule. + + For example, given the identity, ABCD = 1, a LL operation + means look at the left value and multiply both left sides by the + inverse of the leftmost gate A. If A is Hermitian, the inverse + of A is still A. The resulting new rule is BCD = A. + + The following is a summary of the four operations. Assume + that in the examples, all gates are Hermitian. + + LL : left circuit, left multiply + ABCD = E -> AABCD = AE -> BCD = AE + LR : left circuit, right multiply + ABCD = E -> ABCDD = ED -> ABC = ED + RL : right circuit, left multiply + ABC = ED -> EABC = EED -> EABC = D + RR : right circuit, right multiply + AB = CD -> ABD = CDD -> ABD = C + + The number of gate rules generated is n*(n+1), where n + is the number of gates in the sequence (unproven). + + Parameters + ========== + + gate_seq : Gate tuple, Mul, or Number + A variable length tuple or Mul of Gates whose product is equal to + a scalar matrix + return_as_muls : bool + True to return a set of Muls; False to return a set of tuples + + Examples + ======== + + Find the gate rules of the current circuit using tuples: + + >>> from sympy.physics.quantum.identitysearch import generate_gate_rules + >>> from sympy.physics.quantum.gate import X, Y, Z + >>> x = X(0); y = Y(0); z = Z(0) + >>> generate_gate_rules((x, x)) + {((X(0),), (X(0),)), ((X(0), X(0)), ())} + + >>> generate_gate_rules((x, y, z)) + {((), (X(0), Z(0), Y(0))), ((), (Y(0), X(0), Z(0))), + ((), (Z(0), Y(0), X(0))), ((X(0),), (Z(0), Y(0))), + ((Y(0),), (X(0), Z(0))), ((Z(0),), (Y(0), X(0))), + ((X(0), Y(0)), (Z(0),)), ((Y(0), Z(0)), (X(0),)), + ((Z(0), X(0)), (Y(0),)), ((X(0), Y(0), Z(0)), ()), + ((Y(0), Z(0), X(0)), ()), ((Z(0), X(0), Y(0)), ())} + + Find the gate rules of the current circuit using Muls: + + >>> generate_gate_rules(x*x, return_as_muls=True) + {(1, 1)} + + >>> generate_gate_rules(x*y*z, return_as_muls=True) + {(1, X(0)*Z(0)*Y(0)), (1, Y(0)*X(0)*Z(0)), + (1, Z(0)*Y(0)*X(0)), (X(0)*Y(0), Z(0)), + (Y(0)*Z(0), X(0)), (Z(0)*X(0), Y(0)), + (X(0)*Y(0)*Z(0), 1), (Y(0)*Z(0)*X(0), 1), + (Z(0)*X(0)*Y(0), 1), (X(0), Z(0)*Y(0)), + (Y(0), X(0)*Z(0)), (Z(0), Y(0)*X(0))} + """ + + if isinstance(gate_seq, Number): + if return_as_muls: + return {(S.One, S.One)} + else: + return {((), ())} + + elif isinstance(gate_seq, Mul): + gate_seq = gate_seq.args + + # Each item in queue is a 3-tuple: + # i) first item is the left side of an equality + # ii) second item is the right side of an equality + # iii) third item is the number of operations performed + # The argument, gate_seq, will start on the left side, and + # the right side will be empty, implying the presence of an + # identity. + queue = deque() + # A set of gate rules + rules = set() + # Maximum number of operations to perform + max_ops = len(gate_seq) + + def process_new_rule(new_rule, ops): + if new_rule is not None: + new_left, new_right = new_rule + + if new_rule not in rules and (new_right, new_left) not in rules: + rules.add(new_rule) + # If haven't reached the max limit on operations + if ops + 1 < max_ops: + queue.append(new_rule + (ops + 1,)) + + queue.append((gate_seq, (), 0)) + rules.add((gate_seq, ())) + + while len(queue) > 0: + left, right, ops = queue.popleft() + + # Do a LL + new_rule = ll_op(left, right) + process_new_rule(new_rule, ops) + # Do a LR + new_rule = lr_op(left, right) + process_new_rule(new_rule, ops) + # Do a RL + new_rule = rl_op(left, right) + process_new_rule(new_rule, ops) + # Do a RR + new_rule = rr_op(left, right) + process_new_rule(new_rule, ops) + + if return_as_muls: + # Convert each rule as tuples into a rule as muls + mul_rules = set() + for rule in rules: + left, right = rule + mul_rules.add((Mul(*left), Mul(*right))) + + rules = mul_rules + + return rules + + +def generate_equivalent_ids(gate_seq, return_as_muls=False): + """Returns a set of equivalent gate identities. + + A gate identity is a quantum circuit such that the product + of the gates in the circuit is equal to a scalar value. + For example, XYZ = i, where X, Y, Z are the Pauli gates and + i is the imaginary value, is considered a gate identity. + + This function uses the four operations (LL, LR, RL, RR) + to generate the gate rules and, subsequently, to locate equivalent + gate identities. + + Note that all equivalent identities are reachable in n operations + from the starting gate identity, where n is the number of gates + in the sequence. + + The max number of gate identities is 2n, where n is the number + of gates in the sequence (unproven). + + Parameters + ========== + + gate_seq : Gate tuple, Mul, or Number + A variable length tuple or Mul of Gates whose product is equal to + a scalar matrix. + return_as_muls: bool + True to return as Muls; False to return as tuples + + Examples + ======== + + Find equivalent gate identities from the current circuit with tuples: + + >>> from sympy.physics.quantum.identitysearch import generate_equivalent_ids + >>> from sympy.physics.quantum.gate import X, Y, Z + >>> x = X(0); y = Y(0); z = Z(0) + >>> generate_equivalent_ids((x, x)) + {(X(0), X(0))} + + >>> generate_equivalent_ids((x, y, z)) + {(X(0), Y(0), Z(0)), (X(0), Z(0), Y(0)), (Y(0), X(0), Z(0)), + (Y(0), Z(0), X(0)), (Z(0), X(0), Y(0)), (Z(0), Y(0), X(0))} + + Find equivalent gate identities from the current circuit with Muls: + + >>> generate_equivalent_ids(x*x, return_as_muls=True) + {1} + + >>> generate_equivalent_ids(x*y*z, return_as_muls=True) + {X(0)*Y(0)*Z(0), X(0)*Z(0)*Y(0), Y(0)*X(0)*Z(0), + Y(0)*Z(0)*X(0), Z(0)*X(0)*Y(0), Z(0)*Y(0)*X(0)} + """ + + if isinstance(gate_seq, Number): + return {S.One} + elif isinstance(gate_seq, Mul): + gate_seq = gate_seq.args + + # Filter through the gate rules and keep the rules + # with an empty tuple either on the left or right side + + # A set of equivalent gate identities + eq_ids = set() + + gate_rules = generate_gate_rules(gate_seq) + for rule in gate_rules: + l, r = rule + if l == (): + eq_ids.add(r) + elif r == (): + eq_ids.add(l) + + if return_as_muls: + convert_to_mul = lambda id_seq: Mul(*id_seq) + eq_ids = set(map(convert_to_mul, eq_ids)) + + return eq_ids + + +class GateIdentity(Basic): + """Wrapper class for circuits that reduce to a scalar value. + + A gate identity is a quantum circuit such that the product + of the gates in the circuit is equal to a scalar value. + For example, XYZ = i, where X, Y, Z are the Pauli gates and + i is the imaginary value, is considered a gate identity. + + Parameters + ========== + + args : Gate tuple + A variable length tuple of Gates that form an identity. + + Examples + ======== + + Create a GateIdentity and look at its attributes: + + >>> from sympy.physics.quantum.identitysearch import GateIdentity + >>> from sympy.physics.quantum.gate import X, Y, Z + >>> x = X(0); y = Y(0); z = Z(0) + >>> an_identity = GateIdentity(x, y, z) + >>> an_identity.circuit + X(0)*Y(0)*Z(0) + + >>> an_identity.equivalent_ids + {(X(0), Y(0), Z(0)), (X(0), Z(0), Y(0)), (Y(0), X(0), Z(0)), + (Y(0), Z(0), X(0)), (Z(0), X(0), Y(0)), (Z(0), Y(0), X(0))} + """ + + def __new__(cls, *args): + # args should be a tuple - a variable length argument list + obj = Basic.__new__(cls, *args) + obj._circuit = Mul(*args) + obj._rules = generate_gate_rules(args) + obj._eq_ids = generate_equivalent_ids(args) + + return obj + + @property + def circuit(self): + return self._circuit + + @property + def gate_rules(self): + return self._rules + + @property + def equivalent_ids(self): + return self._eq_ids + + @property + def sequence(self): + return self.args + + def __str__(self): + """Returns the string of gates in a tuple.""" + return str(self.circuit) + + +def is_degenerate(identity_set, gate_identity): + """Checks if a gate identity is a permutation of another identity. + + Parameters + ========== + + identity_set : set + A Python set with GateIdentity objects. + gate_identity : GateIdentity + The GateIdentity to check for existence in the set. + + Examples + ======== + + Check if the identity is a permutation of another identity: + + >>> from sympy.physics.quantum.identitysearch import ( + ... GateIdentity, is_degenerate) + >>> from sympy.physics.quantum.gate import X, Y, Z + >>> x = X(0); y = Y(0); z = Z(0) + >>> an_identity = GateIdentity(x, y, z) + >>> id_set = {an_identity} + >>> another_id = (y, z, x) + >>> is_degenerate(id_set, another_id) + True + + >>> another_id = (x, x) + >>> is_degenerate(id_set, another_id) + False + """ + + # For now, just iteratively go through the set and check if the current + # gate_identity is a permutation of an identity in the set + for an_id in identity_set: + if (gate_identity in an_id.equivalent_ids): + return True + return False + + +def is_reducible(circuit, nqubits, begin, end): + """Determines if a circuit is reducible by checking + if its subcircuits are scalar values. + + Parameters + ========== + + circuit : Gate tuple + A tuple of Gates representing a circuit. The circuit to check + if a gate identity is contained in a subcircuit. + nqubits : int + The number of qubits the circuit operates on. + begin : int + The leftmost gate in the circuit to include in a subcircuit. + end : int + The rightmost gate in the circuit to include in a subcircuit. + + Examples + ======== + + Check if the circuit can be reduced: + + >>> from sympy.physics.quantum.identitysearch import is_reducible + >>> from sympy.physics.quantum.gate import X, Y, Z + >>> x = X(0); y = Y(0); z = Z(0) + >>> is_reducible((x, y, z), 1, 0, 3) + True + + Check if an interval in the circuit can be reduced: + + >>> is_reducible((x, y, z), 1, 1, 3) + False + + >>> is_reducible((x, y, y), 1, 1, 3) + True + """ + + current_circuit = () + # Start from the gate at "end" and go down to almost the gate at "begin" + for ndx in reversed(range(begin, end)): + next_gate = circuit[ndx] + current_circuit = (next_gate,) + current_circuit + + # If a circuit as a matrix is equivalent to a scalar value + if (is_scalar_matrix(current_circuit, nqubits, False)): + return True + + return False + + +def bfs_identity_search(gate_list, nqubits, max_depth=None, + identity_only=False): + """Constructs a set of gate identities from the list of possible gates. + + Performs a breadth first search over the space of gate identities. + This allows the finding of the shortest gate identities first. + + Parameters + ========== + + gate_list : list, Gate + A list of Gates from which to search for gate identities. + nqubits : int + The number of qubits the quantum circuit operates on. + max_depth : int + The longest quantum circuit to construct from gate_list. + identity_only : bool + True to search for gate identities that reduce to identity; + False to search for gate identities that reduce to a scalar. + + Examples + ======== + + Find a list of gate identities: + + >>> from sympy.physics.quantum.identitysearch import bfs_identity_search + >>> from sympy.physics.quantum.gate import X, Y, Z + >>> x = X(0); y = Y(0); z = Z(0) + >>> bfs_identity_search([x], 1, max_depth=2) + {GateIdentity(X(0), X(0))} + + >>> bfs_identity_search([x, y, z], 1) + {GateIdentity(X(0), X(0)), GateIdentity(Y(0), Y(0)), + GateIdentity(Z(0), Z(0)), GateIdentity(X(0), Y(0), Z(0))} + + Find a list of identities that only equal to 1: + + >>> bfs_identity_search([x, y, z], 1, identity_only=True) + {GateIdentity(X(0), X(0)), GateIdentity(Y(0), Y(0)), + GateIdentity(Z(0), Z(0))} + """ + + if max_depth is None or max_depth <= 0: + max_depth = len(gate_list) + + id_only = identity_only + + # Start with an empty sequence (implicitly contains an IdentityGate) + queue = deque([()]) + + # Create an empty set of gate identities + ids = set() + + # Begin searching for gate identities in given space. + while (len(queue) > 0): + current_circuit = queue.popleft() + + for next_gate in gate_list: + new_circuit = current_circuit + (next_gate,) + + # Determines if a (strict) subcircuit is a scalar matrix + circuit_reducible = is_reducible(new_circuit, nqubits, + 1, len(new_circuit)) + + # In many cases when the matrix is a scalar value, + # the evaluated matrix will actually be an integer + if (is_scalar_matrix(new_circuit, nqubits, id_only) and + not is_degenerate(ids, new_circuit) and + not circuit_reducible): + ids.add(GateIdentity(*new_circuit)) + + elif (len(new_circuit) < max_depth and + not circuit_reducible): + queue.append(new_circuit) + + return ids + + +def random_identity_search(gate_list, numgates, nqubits): + """Randomly selects numgates from gate_list and checks if it is + a gate identity. + + If the circuit is a gate identity, the circuit is returned; + Otherwise, None is returned. + """ + + gate_size = len(gate_list) + circuit = () + + for i in range(numgates): + next_gate = gate_list[randint(0, gate_size - 1)] + circuit = circuit + (next_gate,) + + is_scalar = is_scalar_matrix(circuit, nqubits, False) + + return circuit if is_scalar else None diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/innerproduct.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/innerproduct.py new file mode 100644 index 0000000000000000000000000000000000000000..11fed882b6068a4df5a787ff90eee5392f97447a --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/innerproduct.py @@ -0,0 +1,138 @@ +"""Symbolic inner product.""" + +from sympy.core.expr import Expr +from sympy.core.kind import NumberKind +from sympy.functions.elementary.complexes import conjugate +from sympy.printing.pretty.stringpict import prettyForm +from sympy.physics.quantum.dagger import Dagger + + +__all__ = [ + 'InnerProduct' +] + + +# InnerProduct is not an QExpr because it is really just a regular commutative +# number. We have gone back and forth about this, but we gain a lot by having +# it subclass Expr. The main challenges were getting Dagger to work +# (we use _eval_conjugate) and represent (we can use atoms and subs). Having +# it be an Expr, mean that there are no commutative QExpr subclasses, +# which simplifies the design of everything. + +class InnerProduct(Expr): + """An unevaluated inner product between a Bra and a Ket [1]. + + Parameters + ========== + + bra : BraBase or subclass + The bra on the left side of the inner product. + ket : KetBase or subclass + The ket on the right side of the inner product. + + Examples + ======== + + Create an InnerProduct and check its properties: + + >>> from sympy.physics.quantum import Bra, Ket + >>> b = Bra('b') + >>> k = Ket('k') + >>> ip = b*k + >>> ip + + >>> ip.bra + >> ip.ket + |k> + + In quantum expressions, inner products will be automatically + identified and created:: + + >>> b*k + + + In more complex expressions, where there is ambiguity in whether inner or + outer products should be created, inner products have high priority:: + + >>> k*b*k*b + *|k> moved to the left of the expression + because inner products are commutative complex numbers. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Inner_product + """ + + kind = NumberKind + + is_complex = True + + def __new__(cls, bra, ket): + # Keep the import of BraBase and KetBase here to avoid problems + # with circular imports. + from sympy.physics.quantum.state import KetBase, BraBase + if not isinstance(ket, KetBase): + raise TypeError('KetBase subclass expected, got: %r' % ket) + if not isinstance(bra, BraBase): + raise TypeError('BraBase subclass expected, got: %r' % ket) + obj = Expr.__new__(cls, bra, ket) + return obj + + @property + def bra(self): + return self.args[0] + + @property + def ket(self): + return self.args[1] + + def _eval_conjugate(self): + return InnerProduct(Dagger(self.ket), Dagger(self.bra)) + + def _sympyrepr(self, printer, *args): + return '%s(%s,%s)' % (self.__class__.__name__, + printer._print(self.bra, *args), printer._print(self.ket, *args)) + + def _sympystr(self, printer, *args): + sbra = printer._print(self.bra) + sket = printer._print(self.ket) + return '%s|%s' % (sbra[:-1], sket[1:]) + + def _pretty(self, printer, *args): + # Print state contents + bra = self.bra._print_contents_pretty(printer, *args) + ket = self.ket._print_contents_pretty(printer, *args) + # Print brackets + height = max(bra.height(), ket.height()) + use_unicode = printer._use_unicode + lbracket, _ = self.bra._pretty_brackets(height, use_unicode) + cbracket, rbracket = self.ket._pretty_brackets(height, use_unicode) + # Build innerproduct + pform = prettyForm(*bra.left(lbracket)) + pform = prettyForm(*pform.right(cbracket)) + pform = prettyForm(*pform.right(ket)) + pform = prettyForm(*pform.right(rbracket)) + return pform + + def _latex(self, printer, *args): + bra_label = self.bra._print_contents_latex(printer, *args) + ket = printer._print(self.ket, *args) + return r'\left\langle %s \right. %s' % (bra_label, ket) + + def doit(self, **hints): + try: + r = self.ket._eval_innerproduct(self.bra, **hints) + except NotImplementedError: + try: + r = conjugate( + self.bra.dual._eval_innerproduct(self.ket.dual, **hints) + ) + except NotImplementedError: + r = None + if r is not None: + return r + return self diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/kind.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/kind.py new file mode 100644 index 0000000000000000000000000000000000000000..14b5bd2c7b0c87f49dc7e6dc9c1b492fbfad6d56 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/kind.py @@ -0,0 +1,103 @@ +"""Kinds for Operators, Bras, and Kets. + +This module defines kinds for operators, bras, and kets. These are useful +in various places in ``sympy.physics.quantum`` as you often want to know +what the kind is of a compound expression. For example, if you multiply +an operator, bra, or ket by a number, you get back another operator, bra, +or ket - even though if you did an ``isinstance`` check you would find that +you have a ``Mul`` instead. The kind system is meant to give you a quick +way of determining how a compound expression behaves in terms of lower +level kinds. + +The resolution calculation of kinds for compound expressions can be found +either in container classes or in functions that are registered with +kind dispatchers. +""" + +from sympy.core.mul import Mul +from sympy.core.kind import Kind, _NumberKind + + +__all__ = [ + '_KetKind', + 'KetKind', + '_BraKind', + 'BraKind', + '_OperatorKind', + 'OperatorKind', +] + + +class _KetKind(Kind): + """A kind for quantum kets.""" + + def __new__(cls): + obj = super().__new__(cls) + return obj + + def __repr__(self): + return "KetKind" + +# Create an instance as many situations need this. +KetKind = _KetKind() + + +class _BraKind(Kind): + """A kind for quantum bras.""" + + def __new__(cls): + obj = super().__new__(cls) + return obj + + def __repr__(self): + return "BraKind" + +# Create an instance as many situations need this. +BraKind = _BraKind() + + +from sympy.core.kind import Kind + +class _OperatorKind(Kind): + """A kind for quantum operators.""" + + def __new__(cls): + obj = super().__new__(cls) + return obj + + def __repr__(self): + return "OperatorKind" + +# Create an instance as many situations need this. +OperatorKind = _OperatorKind() + + +#----------------------------------------------------------------------------- +# Kind resolution. +#----------------------------------------------------------------------------- + +# Note: We can't currently add kind dispatchers for the following combinations +# as the Mul._kind_dispatcher is set to commutative and will also +# register the opposite order, which isn't correct for these pairs: +# +# 1. (_OperatorKind, _KetKind) +# 2. (_BraKind, _OperatorKind) +# 3. (_BraKind, _KetKind) + + +@Mul._kind_dispatcher.register(_NumberKind, _KetKind) +def _mul_number_ket_kind(lhs, rhs): + """Perform the kind calculation of NumberKind*KetKind -> KetKind.""" + return KetKind + + +@Mul._kind_dispatcher.register(_NumberKind, _BraKind) +def _mul_number_bra_kind(lhs, rhs): + """Perform the kind calculation of NumberKind*BraKind -> BraKind.""" + return BraKind + + +@Mul._kind_dispatcher.register(_NumberKind, _OperatorKind) +def _mul_operator_kind(lhs, rhs): + """Perform the kind calculation of NumberKind*OperatorKind -> OperatorKind.""" + return OperatorKind diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/matrixcache.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/matrixcache.py new file mode 100644 index 0000000000000000000000000000000000000000..3cfab3c3490c909966d8a56af395ffa578724ea7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/matrixcache.py @@ -0,0 +1,103 @@ +"""A cache for storing small matrices in multiple formats.""" + +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.power import Pow +from sympy.functions.elementary.exponential import exp +from sympy.matrices.dense import Matrix + +from sympy.physics.quantum.matrixutils import ( + to_sympy, to_numpy, to_scipy_sparse +) + + +class MatrixCache: + """A cache for small matrices in different formats. + + This class takes small matrices in the standard ``sympy.Matrix`` format, + and then converts these to both ``numpy.matrix`` and + ``scipy.sparse.csr_matrix`` matrices. These matrices are then stored for + future recovery. + """ + + def __init__(self, dtype='complex'): + self._cache = {} + self.dtype = dtype + + def cache_matrix(self, name, m): + """Cache a matrix by its name. + + Parameters + ---------- + name : str + A descriptive name for the matrix, like "identity2". + m : list of lists + The raw matrix data as a SymPy Matrix. + """ + try: + self._sympy_matrix(name, m) + except ImportError: + pass + try: + self._numpy_matrix(name, m) + except ImportError: + pass + try: + self._scipy_sparse_matrix(name, m) + except ImportError: + pass + + def get_matrix(self, name, format): + """Get a cached matrix by name and format. + + Parameters + ---------- + name : str + A descriptive name for the matrix, like "identity2". + format : str + The format desired ('sympy', 'numpy', 'scipy.sparse') + """ + m = self._cache.get((name, format)) + if m is not None: + return m + raise NotImplementedError( + 'Matrix with name %s and format %s is not available.' % + (name, format) + ) + + def _store_matrix(self, name, format, m): + self._cache[(name, format)] = m + + def _sympy_matrix(self, name, m): + self._store_matrix(name, 'sympy', to_sympy(m)) + + def _numpy_matrix(self, name, m): + m = to_numpy(m, dtype=self.dtype) + self._store_matrix(name, 'numpy', m) + + def _scipy_sparse_matrix(self, name, m): + # TODO: explore different sparse formats. But sparse.kron will use + # coo in most cases, so we use that here. + m = to_scipy_sparse(m, dtype=self.dtype) + self._store_matrix(name, 'scipy.sparse', m) + + +sqrt2_inv = Pow(2, Rational(-1, 2), evaluate=False) + +# Save the common matrices that we will need +matrix_cache = MatrixCache() +matrix_cache.cache_matrix('eye2', Matrix([[1, 0], [0, 1]])) +matrix_cache.cache_matrix('op11', Matrix([[0, 0], [0, 1]])) # |1><1| +matrix_cache.cache_matrix('op00', Matrix([[1, 0], [0, 0]])) # |0><0| +matrix_cache.cache_matrix('op10', Matrix([[0, 0], [1, 0]])) # |1><0| +matrix_cache.cache_matrix('op01', Matrix([[0, 1], [0, 0]])) # |0><1| +matrix_cache.cache_matrix('X', Matrix([[0, 1], [1, 0]])) +matrix_cache.cache_matrix('Y', Matrix([[0, -I], [I, 0]])) +matrix_cache.cache_matrix('Z', Matrix([[1, 0], [0, -1]])) +matrix_cache.cache_matrix('S', Matrix([[1, 0], [0, I]])) +matrix_cache.cache_matrix('T', Matrix([[1, 0], [0, exp(I*pi/4)]])) +matrix_cache.cache_matrix('H', sqrt2_inv*Matrix([[1, 1], [1, -1]])) +matrix_cache.cache_matrix('Hsqrt2', Matrix([[1, 1], [1, -1]])) +matrix_cache.cache_matrix( + 'SWAP', Matrix([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])) +matrix_cache.cache_matrix('ZX', sqrt2_inv*Matrix([[1, 1], [1, -1]])) +matrix_cache.cache_matrix('ZY', Matrix([[I, 0], [0, -I]])) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/matrixutils.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/matrixutils.py new file mode 100644 index 0000000000000000000000000000000000000000..1082ea326b68256dac96030e36d72efa664495d2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/matrixutils.py @@ -0,0 +1,272 @@ +"""Utilities to deal with sympy.Matrix, numpy and scipy.sparse.""" + +from sympy.core.expr import Expr +from sympy.core.numbers import I +from sympy.core.singleton import S +from sympy.matrices.matrixbase import MatrixBase +from sympy.matrices import eye, zeros +from sympy.external import import_module + +__all__ = [ + 'numpy_ndarray', + 'scipy_sparse_matrix', + 'sympy_to_numpy', + 'sympy_to_scipy_sparse', + 'numpy_to_sympy', + 'scipy_sparse_to_sympy', + 'flatten_scalar', + 'matrix_dagger', + 'to_sympy', + 'to_numpy', + 'to_scipy_sparse', + 'matrix_tensor_product', + 'matrix_zeros' +] + +# Conditionally define the base classes for numpy and scipy.sparse arrays +# for use in isinstance tests. + +np = import_module('numpy') +if not np: + class numpy_ndarray: + pass +else: + numpy_ndarray = np.ndarray # type: ignore + +scipy = import_module('scipy', import_kwargs={'fromlist': ['sparse']}) +if not scipy: + class scipy_sparse_matrix: + pass + sparse = None +else: + sparse = scipy.sparse + scipy_sparse_matrix = sparse.spmatrix # type: ignore + + +def sympy_to_numpy(m, **options): + """Convert a SymPy Matrix/complex number to a numpy matrix or scalar.""" + if not np: + raise ImportError + dtype = options.get('dtype', 'complex') + if isinstance(m, MatrixBase): + return np.array(m.tolist(), dtype=dtype) + elif isinstance(m, Expr): + if m.is_Number or m.is_NumberSymbol or m == I: + return complex(m) + raise TypeError('Expected MatrixBase or complex scalar, got: %r' % m) + + +def sympy_to_scipy_sparse(m, **options): + """Convert a SymPy Matrix/complex number to a numpy matrix or scalar.""" + if not np or not sparse: + raise ImportError + dtype = options.get('dtype', 'complex') + if isinstance(m, MatrixBase): + return sparse.csr_matrix(np.array(m.tolist(), dtype=dtype)) + elif isinstance(m, Expr): + if m.is_Number or m.is_NumberSymbol or m == I: + return complex(m) + raise TypeError('Expected MatrixBase or complex scalar, got: %r' % m) + + +def scipy_sparse_to_sympy(m, **options): + """Convert a scipy.sparse matrix to a SymPy matrix.""" + return MatrixBase(m.todense()) + + +def numpy_to_sympy(m, **options): + """Convert a numpy matrix to a SymPy matrix.""" + return MatrixBase(m) + + +def to_sympy(m, **options): + """Convert a numpy/scipy.sparse matrix to a SymPy matrix.""" + if isinstance(m, MatrixBase): + return m + elif isinstance(m, numpy_ndarray): + return numpy_to_sympy(m) + elif isinstance(m, scipy_sparse_matrix): + return scipy_sparse_to_sympy(m) + elif isinstance(m, Expr): + return m + raise TypeError('Expected sympy/numpy/scipy.sparse matrix, got: %r' % m) + + +def to_numpy(m, **options): + """Convert a sympy/scipy.sparse matrix to a numpy matrix.""" + dtype = options.get('dtype', 'complex') + if isinstance(m, (MatrixBase, Expr)): + return sympy_to_numpy(m, dtype=dtype) + elif isinstance(m, numpy_ndarray): + return m + elif isinstance(m, scipy_sparse_matrix): + return m.todense() + raise TypeError('Expected sympy/numpy/scipy.sparse matrix, got: %r' % m) + + +def to_scipy_sparse(m, **options): + """Convert a sympy/numpy matrix to a scipy.sparse matrix.""" + dtype = options.get('dtype', 'complex') + if isinstance(m, (MatrixBase, Expr)): + return sympy_to_scipy_sparse(m, dtype=dtype) + elif isinstance(m, numpy_ndarray): + if not sparse: + raise ImportError + return sparse.csr_matrix(m) + elif isinstance(m, scipy_sparse_matrix): + return m + raise TypeError('Expected sympy/numpy/scipy.sparse matrix, got: %r' % m) + + +def flatten_scalar(e): + """Flatten a 1x1 matrix to a scalar, return larger matrices unchanged.""" + if isinstance(e, MatrixBase): + if e.shape == (1, 1): + e = e[0] + if isinstance(e, (numpy_ndarray, scipy_sparse_matrix)): + if e.shape == (1, 1): + e = complex(e[0, 0]) + return e + + +def matrix_dagger(e): + """Return the dagger of a sympy/numpy/scipy.sparse matrix.""" + if isinstance(e, MatrixBase): + return e.H + elif isinstance(e, (numpy_ndarray, scipy_sparse_matrix)): + return e.conjugate().transpose() + raise TypeError('Expected sympy/numpy/scipy.sparse matrix, got: %r' % e) + + +# TODO: Move this into sympy.matrices. +def _sympy_tensor_product(*matrices): + """Compute the kronecker product of a sequence of SymPy Matrices. + """ + from sympy.matrices.expressions.kronecker import matrix_kronecker_product + + return matrix_kronecker_product(*matrices) + + +def _numpy_tensor_product(*product): + """numpy version of tensor product of multiple arguments.""" + if not np: + raise ImportError + answer = product[0] + for item in product[1:]: + answer = np.kron(answer, item) + return answer + + +def _scipy_sparse_tensor_product(*product): + """scipy.sparse version of tensor product of multiple arguments.""" + if not sparse: + raise ImportError + answer = product[0] + for item in product[1:]: + answer = sparse.kron(answer, item) + # The final matrices will just be multiplied, so csr is a good final + # sparse format. + return sparse.csr_matrix(answer) + + +def matrix_tensor_product(*product): + """Compute the matrix tensor product of sympy/numpy/scipy.sparse matrices.""" + if isinstance(product[0], MatrixBase): + return _sympy_tensor_product(*product) + elif isinstance(product[0], numpy_ndarray): + return _numpy_tensor_product(*product) + elif isinstance(product[0], scipy_sparse_matrix): + return _scipy_sparse_tensor_product(*product) + + +def _numpy_eye(n): + """numpy version of complex eye.""" + if not np: + raise ImportError + return np.array(np.eye(n, dtype='complex')) + + +def _scipy_sparse_eye(n): + """scipy.sparse version of complex eye.""" + if not sparse: + raise ImportError + return sparse.eye(n, n, dtype='complex') + + +def matrix_eye(n, **options): + """Get the version of eye and tensor_product for a given format.""" + format = options.get('format', 'sympy') + if format == 'sympy': + return eye(n) + elif format == 'numpy': + return _numpy_eye(n) + elif format == 'scipy.sparse': + return _scipy_sparse_eye(n) + raise NotImplementedError('Invalid format: %r' % format) + + +def _numpy_zeros(m, n, **options): + """numpy version of zeros.""" + dtype = options.get('dtype', 'float64') + if not np: + raise ImportError + return np.zeros((m, n), dtype=dtype) + + +def _scipy_sparse_zeros(m, n, **options): + """scipy.sparse version of zeros.""" + spmatrix = options.get('spmatrix', 'csr') + dtype = options.get('dtype', 'float64') + if not sparse: + raise ImportError + if spmatrix == 'lil': + return sparse.lil_matrix((m, n), dtype=dtype) + elif spmatrix == 'csr': + return sparse.csr_matrix((m, n), dtype=dtype) + + +def matrix_zeros(m, n, **options): + """"Get a zeros matrix for a given format.""" + format = options.get('format', 'sympy') + if format == 'sympy': + return zeros(m, n) + elif format == 'numpy': + return _numpy_zeros(m, n, **options) + elif format == 'scipy.sparse': + return _scipy_sparse_zeros(m, n, **options) + raise NotImplementedError('Invaild format: %r' % format) + + +def _numpy_matrix_to_zero(e): + """Convert a numpy zero matrix to the zero scalar.""" + if not np: + raise ImportError + test = np.zeros_like(e) + if np.allclose(e, test): + return 0.0 + else: + return e + + +def _scipy_sparse_matrix_to_zero(e): + """Convert a scipy.sparse zero matrix to the zero scalar.""" + if not np: + raise ImportError + edense = e.todense() + test = np.zeros_like(edense) + if np.allclose(edense, test): + return 0.0 + else: + return e + + +def matrix_to_zero(e): + """Convert a zero matrix to the scalar zero.""" + if isinstance(e, MatrixBase): + if zeros(*e.shape) == e: + e = S.Zero + elif isinstance(e, numpy_ndarray): + e = _numpy_matrix_to_zero(e) + elif isinstance(e, scipy_sparse_matrix): + e = _scipy_sparse_matrix_to_zero(e) + return e diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/operator.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/operator.py new file mode 100644 index 0000000000000000000000000000000000000000..b44617e15c19e8b30b76f011630430787233e724 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/operator.py @@ -0,0 +1,653 @@ +"""Quantum mechanical operators. + +TODO: + +* Fix early 0 in apply_operators. +* Debug and test apply_operators. +* Get cse working with classes in this file. +* Doctests and documentation of special methods for InnerProduct, Commutator, + AntiCommutator, represent, apply_operators. +""" +from typing import Optional + +from sympy.core.add import Add +from sympy.core.expr import Expr +from sympy.core.function import (Derivative, expand) +from sympy.core.mul import Mul +from sympy.core.numbers import oo +from sympy.core.singleton import S +from sympy.printing.pretty.stringpict import prettyForm +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.kind import OperatorKind +from sympy.physics.quantum.qexpr import QExpr, dispatch_method +from sympy.matrices import eye +from sympy.utilities.exceptions import sympy_deprecation_warning + + + +__all__ = [ + 'Operator', + 'HermitianOperator', + 'UnitaryOperator', + 'IdentityOperator', + 'OuterProduct', + 'DifferentialOperator' +] + +#----------------------------------------------------------------------------- +# Operators and outer products +#----------------------------------------------------------------------------- + + +class Operator(QExpr): + """Base class for non-commuting quantum operators. + + An operator maps between quantum states [1]_. In quantum mechanics, + observables (including, but not limited to, measured physical values) are + represented as Hermitian operators [2]_. + + Parameters + ========== + + args : tuple + The list of numbers or parameters that uniquely specify the + operator. For time-dependent operators, this will include the time. + + Examples + ======== + + Create an operator and examine its attributes:: + + >>> from sympy.physics.quantum import Operator + >>> from sympy import I + >>> A = Operator('A') + >>> A + A + >>> A.hilbert_space + H + >>> A.label + (A,) + >>> A.is_commutative + False + + Create another operator and do some arithmetic operations:: + + >>> B = Operator('B') + >>> C = 2*A*A + I*B + >>> C + 2*A**2 + I*B + + Operators do not commute:: + + >>> A.is_commutative + False + >>> B.is_commutative + False + >>> A*B == B*A + False + + Polymonials of operators respect the commutation properties:: + + >>> e = (A+B)**3 + >>> e.expand() + A*B*A + A*B**2 + A**2*B + A**3 + B*A*B + B*A**2 + B**2*A + B**3 + + Operator inverses are handle symbolically:: + + >>> A.inv() + A**(-1) + >>> A*A.inv() + 1 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Operator_%28physics%29 + .. [2] https://en.wikipedia.org/wiki/Observable + """ + is_hermitian: Optional[bool] = None + is_unitary: Optional[bool] = None + @classmethod + def default_args(self): + return ("O",) + + kind = OperatorKind + + #------------------------------------------------------------------------- + # Printing + #------------------------------------------------------------------------- + + _label_separator = ',' + + def _print_operator_name(self, printer, *args): + return self.__class__.__name__ + + _print_operator_name_latex = _print_operator_name + + def _print_operator_name_pretty(self, printer, *args): + return prettyForm(self.__class__.__name__) + + def _print_contents(self, printer, *args): + if len(self.label) == 1: + return self._print_label(printer, *args) + else: + return '%s(%s)' % ( + self._print_operator_name(printer, *args), + self._print_label(printer, *args) + ) + + def _print_contents_pretty(self, printer, *args): + if len(self.label) == 1: + return self._print_label_pretty(printer, *args) + else: + pform = self._print_operator_name_pretty(printer, *args) + label_pform = self._print_label_pretty(printer, *args) + label_pform = prettyForm( + *label_pform.parens(left='(', right=')') + ) + pform = prettyForm(*pform.right(label_pform)) + return pform + + def _print_contents_latex(self, printer, *args): + if len(self.label) == 1: + return self._print_label_latex(printer, *args) + else: + return r'%s\left(%s\right)' % ( + self._print_operator_name_latex(printer, *args), + self._print_label_latex(printer, *args) + ) + + #------------------------------------------------------------------------- + # _eval_* methods + #------------------------------------------------------------------------- + + def _eval_commutator(self, other, **options): + """Evaluate [self, other] if known, return None if not known.""" + return dispatch_method(self, '_eval_commutator', other, **options) + + def _eval_anticommutator(self, other, **options): + """Evaluate [self, other] if known.""" + return dispatch_method(self, '_eval_anticommutator', other, **options) + + #------------------------------------------------------------------------- + # Operator application + #------------------------------------------------------------------------- + + def _apply_operator(self, ket, **options): + return dispatch_method(self, '_apply_operator', ket, **options) + + def _apply_from_right_to(self, bra, **options): + return None + + def matrix_element(self, *args): + raise NotImplementedError('matrix_elements is not defined') + + def inverse(self): + return self._eval_inverse() + + inv = inverse + + def _eval_inverse(self): + return self**(-1) + + +class HermitianOperator(Operator): + """A Hermitian operator that satisfies H == Dagger(H). + + Parameters + ========== + + args : tuple + The list of numbers or parameters that uniquely specify the + operator. For time-dependent operators, this will include the time. + + Examples + ======== + + >>> from sympy.physics.quantum import Dagger, HermitianOperator + >>> H = HermitianOperator('H') + >>> Dagger(H) + H + """ + + is_hermitian = True + + def _eval_inverse(self): + if isinstance(self, UnitaryOperator): + return self + else: + return Operator._eval_inverse(self) + + def _eval_power(self, exp): + if isinstance(self, UnitaryOperator): + # so all eigenvalues of self are 1 or -1 + if exp.is_even: + from sympy.core.singleton import S + return S.One # is identity, see Issue 24153. + elif exp.is_odd: + return self + # No simplification in all other cases + return Operator._eval_power(self, exp) + + +class UnitaryOperator(Operator): + """A unitary operator that satisfies U*Dagger(U) == 1. + + Parameters + ========== + + args : tuple + The list of numbers or parameters that uniquely specify the + operator. For time-dependent operators, this will include the time. + + Examples + ======== + + >>> from sympy.physics.quantum import Dagger, UnitaryOperator + >>> U = UnitaryOperator('U') + >>> U*Dagger(U) + 1 + """ + is_unitary = True + def _eval_adjoint(self): + return self._eval_inverse() + + +class IdentityOperator(Operator): + """An identity operator I that satisfies op * I == I * op == op for any + operator op. + + .. deprecated:: 1.14. + Use the scalar S.One instead as the multiplicative identity for + operators and states. + + Parameters + ========== + + N : Integer + Optional parameter that specifies the dimension of the Hilbert space + of operator. This is used when generating a matrix representation. + + Examples + ======== + + >>> from sympy.physics.quantum import IdentityOperator + >>> IdentityOperator() # doctest: +SKIP + I + """ + is_hermitian = True + is_unitary = True + @property + def dimension(self): + return self.N + + @classmethod + def default_args(self): + return (oo,) + + def __init__(self, *args, **hints): + sympy_deprecation_warning( + """ + IdentityOperator has been deprecated. In the future, please use + S.One as the identity for quantum operators and states. + """, + deprecated_since_version="1.14", + active_deprecations_target='deprecated-operator-identity', + ) + if not len(args) in (0, 1): + raise ValueError('0 or 1 parameters expected, got %s' % args) + + self.N = args[0] if (len(args) == 1 and args[0]) else oo + + def _eval_commutator(self, other, **hints): + return S.Zero + + def _eval_anticommutator(self, other, **hints): + return 2 * other + + def _eval_inverse(self): + return self + + def _eval_adjoint(self): + return self + + def _apply_operator(self, ket, **options): + return ket + + def _apply_from_right_to(self, bra, **options): + return bra + + def _eval_power(self, exp): + return self + + def _print_contents(self, printer, *args): + return 'I' + + def _print_contents_pretty(self, printer, *args): + return prettyForm('I') + + def _print_contents_latex(self, printer, *args): + return r'{\mathcal{I}}' + + def _represent_default_basis(self, **options): + if not self.N or self.N == oo: + raise NotImplementedError('Cannot represent infinite dimensional' + + ' identity operator as a matrix') + + format = options.get('format', 'sympy') + if format != 'sympy': + raise NotImplementedError('Representation in format ' + + '%s not implemented.' % format) + + return eye(self.N) + + +class OuterProduct(Operator): + """An unevaluated outer product between a ket and bra. + + This constructs an outer product between any subclass of ``KetBase`` and + ``BraBase`` as ``|a>>> from sympy.physics.quantum import Ket, Bra, OuterProduct, Dagger + + >>> k = Ket('k') + >>> b = Bra('b') + >>> op = OuterProduct(k, b) + >>> op + |k>>> op.hilbert_space + H + >>> op.ket + |k> + >>> op.bra + >> Dagger(op) + |b>>> k*b + |k>>> b*k*b + *>> from sympy import Derivative, Function, Symbol + >>> from sympy.physics.quantum.operator import DifferentialOperator + >>> from sympy.physics.quantum.state import Wavefunction + >>> from sympy.physics.quantum.qapply import qapply + >>> f = Function('f') + >>> x = Symbol('x') + >>> d = DifferentialOperator(1/x*Derivative(f(x), x), f(x)) + >>> w = Wavefunction(x**2, x) + >>> d.function + f(x) + >>> d.variables + (x,) + >>> qapply(d*w) + Wavefunction(2, x) + + """ + + @property + def variables(self): + """ + Returns the variables with which the function in the specified + arbitrary expression is evaluated + + Examples + ======== + + >>> from sympy.physics.quantum.operator import DifferentialOperator + >>> from sympy import Symbol, Function, Derivative + >>> x = Symbol('x') + >>> f = Function('f') + >>> d = DifferentialOperator(1/x*Derivative(f(x), x), f(x)) + >>> d.variables + (x,) + >>> y = Symbol('y') + >>> d = DifferentialOperator(Derivative(f(x, y), x) + + ... Derivative(f(x, y), y), f(x, y)) + >>> d.variables + (x, y) + """ + + return self.args[-1].args + + @property + def function(self): + """ + Returns the function which is to be replaced with the Wavefunction + + Examples + ======== + + >>> from sympy.physics.quantum.operator import DifferentialOperator + >>> from sympy import Function, Symbol, Derivative + >>> x = Symbol('x') + >>> f = Function('f') + >>> d = DifferentialOperator(Derivative(f(x), x), f(x)) + >>> d.function + f(x) + >>> y = Symbol('y') + >>> d = DifferentialOperator(Derivative(f(x, y), x) + + ... Derivative(f(x, y), y), f(x, y)) + >>> d.function + f(x, y) + """ + + return self.args[-1] + + @property + def expr(self): + """ + Returns the arbitrary expression which is to have the Wavefunction + substituted into it + + Examples + ======== + + >>> from sympy.physics.quantum.operator import DifferentialOperator + >>> from sympy import Function, Symbol, Derivative + >>> x = Symbol('x') + >>> f = Function('f') + >>> d = DifferentialOperator(Derivative(f(x), x), f(x)) + >>> d.expr + Derivative(f(x), x) + >>> y = Symbol('y') + >>> d = DifferentialOperator(Derivative(f(x, y), x) + + ... Derivative(f(x, y), y), f(x, y)) + >>> d.expr + Derivative(f(x, y), x) + Derivative(f(x, y), y) + """ + + return self.args[0] + + @property + def free_symbols(self): + """ + Return the free symbols of the expression. + """ + + return self.expr.free_symbols + + def _apply_operator_Wavefunction(self, func, **options): + from sympy.physics.quantum.state import Wavefunction + var = self.variables + wf_vars = func.args[1:] + + f = self.function + new_expr = self.expr.subs(f, func(*var)) + new_expr = new_expr.doit() + + return Wavefunction(new_expr, *wf_vars) + + def _eval_derivative(self, symbol): + new_expr = Derivative(self.expr, symbol) + return DifferentialOperator(new_expr, self.args[-1]) + + #------------------------------------------------------------------------- + # Printing + #------------------------------------------------------------------------- + + def _print(self, printer, *args): + return '%s(%s)' % ( + self._print_operator_name(printer, *args), + self._print_label(printer, *args) + ) + + def _print_pretty(self, printer, *args): + pform = self._print_operator_name_pretty(printer, *args) + label_pform = self._print_label_pretty(printer, *args) + label_pform = prettyForm( + *label_pform.parens(left='(', right=')') + ) + pform = prettyForm(*pform.right(label_pform)) + return pform diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/operatorordering.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/operatorordering.py new file mode 100644 index 0000000000000000000000000000000000000000..d6ba3dd83b4b79b773793b0094e636cc8a901f44 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/operatorordering.py @@ -0,0 +1,290 @@ +"""Functions for reordering operator expressions.""" + +import warnings + +from sympy.core.add import Add +from sympy.core.mul import Mul +from sympy.core.numbers import Integer +from sympy.core.power import Pow +from sympy.physics.quantum import Commutator, AntiCommutator +from sympy.physics.quantum.boson import BosonOp +from sympy.physics.quantum.fermion import FermionOp + +__all__ = [ + 'normal_order', + 'normal_ordered_form' +] + + +def _expand_powers(factors): + """ + Helper function for normal_ordered_form and normal_order: Expand a + power expression to a multiplication expression so that that the + expression can be handled by the normal ordering functions. + """ + + new_factors = [] + for factor in factors.args: + if (isinstance(factor, Pow) + and isinstance(factor.args[1], Integer) + and factor.args[1] > 0): + for n in range(factor.args[1]): + new_factors.append(factor.args[0]) + else: + new_factors.append(factor) + + return new_factors + +def _normal_ordered_form_factor(product, independent=False, recursive_limit=10, + _recursive_depth=0): + """ + Helper function for normal_ordered_form_factor: Write multiplication + expression with bosonic or fermionic operators on normally ordered form, + using the bosonic and fermionic commutation relations. The resulting + operator expression is equivalent to the argument, but will in general be + a sum of operator products instead of a simple product. + """ + + factors = _expand_powers(product) + + new_factors = [] + n = 0 + while n < len(factors) - 1: + current, next = factors[n], factors[n + 1] + if any(not isinstance(f, (FermionOp, BosonOp)) for f in (current, next)): + new_factors.append(current) + n += 1 + continue + + key_1 = (current.is_annihilation, str(current.name)) + key_2 = (next.is_annihilation, str(next.name)) + + if key_1 <= key_2: + new_factors.append(current) + n += 1 + continue + + n += 2 + if current.is_annihilation and not next.is_annihilation: + if isinstance(current, BosonOp) and isinstance(next, BosonOp): + if current.args[0] != next.args[0]: + if independent: + c = 0 + else: + c = Commutator(current, next) + new_factors.append(next * current + c) + else: + new_factors.append(next * current + 1) + elif isinstance(current, FermionOp) and isinstance(next, FermionOp): + if current.args[0] != next.args[0]: + if independent: + c = 0 + else: + c = AntiCommutator(current, next) + new_factors.append(-next * current + c) + else: + new_factors.append(-next * current + 1) + elif (current.is_annihilation == next.is_annihilation and + isinstance(current, FermionOp) and isinstance(next, FermionOp)): + new_factors.append(-next * current) + else: + new_factors.append(next * current) + + if n == len(factors) - 1: + new_factors.append(factors[-1]) + + if new_factors == factors: + return product + else: + expr = Mul(*new_factors).expand() + return normal_ordered_form(expr, + recursive_limit=recursive_limit, + _recursive_depth=_recursive_depth + 1, + independent=independent) + + +def _normal_ordered_form_terms(expr, independent=False, recursive_limit=10, + _recursive_depth=0): + """ + Helper function for normal_ordered_form: loop through each term in an + addition expression and call _normal_ordered_form_factor to perform the + factor to an normally ordered expression. + """ + + new_terms = [] + for term in expr.args: + if isinstance(term, Mul): + new_term = _normal_ordered_form_factor( + term, recursive_limit=recursive_limit, + _recursive_depth=_recursive_depth, independent=independent) + new_terms.append(new_term) + else: + new_terms.append(term) + + return Add(*new_terms) + + +def normal_ordered_form(expr, independent=False, recursive_limit=10, + _recursive_depth=0): + """Write an expression with bosonic or fermionic operators on normal + ordered form, where each term is normally ordered. Note that this + normal ordered form is equivalent to the original expression. + + Parameters + ========== + + expr : expression + The expression write on normal ordered form. + independent : bool (default False) + Whether to consider operator with different names as operating in + different Hilbert spaces. If False, the (anti-)commutation is left + explicit. + recursive_limit : int (default 10) + The number of allowed recursive applications of the function. + + Examples + ======== + + >>> from sympy.physics.quantum import Dagger + >>> from sympy.physics.quantum.boson import BosonOp + >>> from sympy.physics.quantum.operatorordering import normal_ordered_form + >>> a = BosonOp("a") + >>> normal_ordered_form(a * Dagger(a)) + 1 + Dagger(a)*a + """ + + if _recursive_depth > recursive_limit: + warnings.warn("Too many recursions, aborting") + return expr + + if isinstance(expr, Add): + return _normal_ordered_form_terms(expr, + recursive_limit=recursive_limit, + _recursive_depth=_recursive_depth, + independent=independent) + elif isinstance(expr, Mul): + return _normal_ordered_form_factor(expr, + recursive_limit=recursive_limit, + _recursive_depth=_recursive_depth, + independent=independent) + else: + return expr + + +def _normal_order_factor(product, recursive_limit=10, _recursive_depth=0): + """ + Helper function for normal_order: Normal order a multiplication expression + with bosonic or fermionic operators. In general the resulting operator + expression will not be equivalent to original product. + """ + + factors = _expand_powers(product) + + n = 0 + new_factors = [] + while n < len(factors) - 1: + + if (isinstance(factors[n], BosonOp) and + factors[n].is_annihilation): + # boson + if not isinstance(factors[n + 1], BosonOp): + new_factors.append(factors[n]) + else: + if factors[n + 1].is_annihilation: + new_factors.append(factors[n]) + else: + if factors[n].args[0] != factors[n + 1].args[0]: + new_factors.append(factors[n + 1] * factors[n]) + else: + new_factors.append(factors[n + 1] * factors[n]) + n += 1 + + elif (isinstance(factors[n], FermionOp) and + factors[n].is_annihilation): + # fermion + if not isinstance(factors[n + 1], FermionOp): + new_factors.append(factors[n]) + else: + if factors[n + 1].is_annihilation: + new_factors.append(factors[n]) + else: + if factors[n].args[0] != factors[n + 1].args[0]: + new_factors.append(-factors[n + 1] * factors[n]) + else: + new_factors.append(-factors[n + 1] * factors[n]) + n += 1 + + else: + new_factors.append(factors[n]) + + n += 1 + + if n == len(factors) - 1: + new_factors.append(factors[-1]) + + if new_factors == factors: + return product + else: + expr = Mul(*new_factors).expand() + return normal_order(expr, + recursive_limit=recursive_limit, + _recursive_depth=_recursive_depth + 1) + + +def _normal_order_terms(expr, recursive_limit=10, _recursive_depth=0): + """ + Helper function for normal_order: look through each term in an addition + expression and call _normal_order_factor to perform the normal ordering + on the factors. + """ + + new_terms = [] + for term in expr.args: + if isinstance(term, Mul): + new_term = _normal_order_factor(term, + recursive_limit=recursive_limit, + _recursive_depth=_recursive_depth) + new_terms.append(new_term) + else: + new_terms.append(term) + + return Add(*new_terms) + + +def normal_order(expr, recursive_limit=10, _recursive_depth=0): + """Normal order an expression with bosonic or fermionic operators. Note + that this normal order is not equivalent to the original expression, but + the creation and annihilation operators in each term in expr is reordered + so that the expression becomes normal ordered. + + Parameters + ========== + + expr : expression + The expression to normal order. + + recursive_limit : int (default 10) + The number of allowed recursive applications of the function. + + Examples + ======== + + >>> from sympy.physics.quantum import Dagger + >>> from sympy.physics.quantum.boson import BosonOp + >>> from sympy.physics.quantum.operatorordering import normal_order + >>> a = BosonOp("a") + >>> normal_order(a * Dagger(a)) + Dagger(a)*a + """ + if _recursive_depth > recursive_limit: + warnings.warn("Too many recursions, aborting") + return expr + + if isinstance(expr, Add): + return _normal_order_terms(expr, recursive_limit=recursive_limit, + _recursive_depth=_recursive_depth) + elif isinstance(expr, Mul): + return _normal_order_factor(expr, recursive_limit=recursive_limit, + _recursive_depth=_recursive_depth) + else: + return expr diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/operatorset.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/operatorset.py new file mode 100644 index 0000000000000000000000000000000000000000..bf32bcabbe5d33381dff0b94a9b130375032adef --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/operatorset.py @@ -0,0 +1,279 @@ +""" A module for mapping operators to their corresponding eigenstates +and vice versa + +It contains a global dictionary with eigenstate-operator pairings. +If a new state-operator pair is created, this dictionary should be +updated as well. + +It also contains functions operators_to_state and state_to_operators +for mapping between the two. These can handle both classes and +instances of operators and states. See the individual function +descriptions for details. + +TODO List: +- Update the dictionary with a complete list of state-operator pairs +""" + +from sympy.physics.quantum.cartesian import (XOp, YOp, ZOp, XKet, PxOp, PxKet, + PositionKet3D) +from sympy.physics.quantum.operator import Operator +from sympy.physics.quantum.state import StateBase, BraBase, Ket +from sympy.physics.quantum.spin import (JxOp, JyOp, JzOp, J2Op, JxKet, JyKet, + JzKet) + +__all__ = [ + 'operators_to_state', + 'state_to_operators' +] + +#state_mapping stores the mappings between states and their associated +#operators or tuples of operators. This should be updated when new +#classes are written! Entries are of the form PxKet : PxOp or +#something like 3DKet : (ROp, ThetaOp, PhiOp) + +#frozenset is used so that the reverse mapping can be made +#(regular sets are not hashable because they are mutable +state_mapping = { JxKet: frozenset((J2Op, JxOp)), + JyKet: frozenset((J2Op, JyOp)), + JzKet: frozenset((J2Op, JzOp)), + Ket: Operator, + PositionKet3D: frozenset((XOp, YOp, ZOp)), + PxKet: PxOp, + XKet: XOp } + +op_mapping = {v: k for k, v in state_mapping.items()} + + +def operators_to_state(operators, **options): + """ Returns the eigenstate of the given operator or set of operators + + A global function for mapping operator classes to their associated + states. It takes either an Operator or a set of operators and + returns the state associated with these. + + This function can handle both instances of a given operator or + just the class itself (i.e. both XOp() and XOp) + + There are multiple use cases to consider: + + 1) A class or set of classes is passed: First, we try to + instantiate default instances for these operators. If this fails, + then the class is simply returned. If we succeed in instantiating + default instances, then we try to call state._operators_to_state + on the operator instances. If this fails, the class is returned. + Otherwise, the instance returned by _operators_to_state is returned. + + 2) An instance or set of instances is passed: In this case, + state._operators_to_state is called on the instances passed. If + this fails, a state class is returned. If the method returns an + instance, that instance is returned. + + In both cases, if the operator class or set does not exist in the + state_mapping dictionary, None is returned. + + Parameters + ========== + + arg: Operator or set + The class or instance of the operator or set of operators + to be mapped to a state + + Examples + ======== + + >>> from sympy.physics.quantum.cartesian import XOp, PxOp + >>> from sympy.physics.quantum.operatorset import operators_to_state + >>> from sympy.physics.quantum.operator import Operator + >>> operators_to_state(XOp) + |x> + >>> operators_to_state(XOp()) + |x> + >>> operators_to_state(PxOp) + |px> + >>> operators_to_state(PxOp()) + |px> + >>> operators_to_state(Operator) + |psi> + >>> operators_to_state(Operator()) + |psi> + """ + + if not (isinstance(operators, (Operator, set)) or issubclass(operators, Operator)): + raise NotImplementedError("Argument is not an Operator or a set!") + + if isinstance(operators, set): + for s in operators: + if not (isinstance(s, Operator) + or issubclass(s, Operator)): + raise NotImplementedError("Set is not all Operators!") + + ops = frozenset(operators) + + if ops in op_mapping: # ops is a list of classes in this case + #Try to get an object from default instances of the + #operators...if this fails, return the class + try: + op_instances = [op() for op in ops] + ret = _get_state(op_mapping[ops], set(op_instances), **options) + except NotImplementedError: + ret = op_mapping[ops] + + return ret + else: + tmp = [type(o) for o in ops] + classes = frozenset(tmp) + + if classes in op_mapping: + ret = _get_state(op_mapping[classes], ops, **options) + else: + ret = None + + return ret + else: + if operators in op_mapping: + try: + op_instance = operators() + ret = _get_state(op_mapping[operators], op_instance, **options) + except NotImplementedError: + ret = op_mapping[operators] + + return ret + elif type(operators) in op_mapping: + return _get_state(op_mapping[type(operators)], operators, **options) + else: + return None + + +def state_to_operators(state, **options): + """ Returns the operator or set of operators corresponding to the + given eigenstate + + A global function for mapping state classes to their associated + operators or sets of operators. It takes either a state class + or instance. + + This function can handle both instances of a given state or just + the class itself (i.e. both XKet() and XKet) + + There are multiple use cases to consider: + + 1) A state class is passed: In this case, we first try + instantiating a default instance of the class. If this succeeds, + then we try to call state._state_to_operators on that instance. + If the creation of the default instance or if the calling of + _state_to_operators fails, then either an operator class or set of + operator classes is returned. Otherwise, the appropriate + operator instances are returned. + + 2) A state instance is returned: Here, state._state_to_operators + is called for the instance. If this fails, then a class or set of + operator classes is returned. Otherwise, the instances are returned. + + In either case, if the state's class does not exist in + state_mapping, None is returned. + + Parameters + ========== + + arg: StateBase class or instance (or subclasses) + The class or instance of the state to be mapped to an + operator or set of operators + + Examples + ======== + + >>> from sympy.physics.quantum.cartesian import XKet, PxKet, XBra, PxBra + >>> from sympy.physics.quantum.operatorset import state_to_operators + >>> from sympy.physics.quantum.state import Ket, Bra + >>> state_to_operators(XKet) + X + >>> state_to_operators(XKet()) + X + >>> state_to_operators(PxKet) + Px + >>> state_to_operators(PxKet()) + Px + >>> state_to_operators(PxBra) + Px + >>> state_to_operators(XBra) + X + >>> state_to_operators(Ket) + O + >>> state_to_operators(Bra) + O + """ + + if not (isinstance(state, StateBase) or issubclass(state, StateBase)): + raise NotImplementedError("Argument is not a state!") + + if state in state_mapping: # state is a class + state_inst = _make_default(state) + try: + ret = _get_ops(state_inst, + _make_set(state_mapping[state]), **options) + except (NotImplementedError, TypeError): + ret = state_mapping[state] + elif type(state) in state_mapping: + ret = _get_ops(state, + _make_set(state_mapping[type(state)]), **options) + elif isinstance(state, BraBase) and state.dual_class() in state_mapping: + ret = _get_ops(state, + _make_set(state_mapping[state.dual_class()])) + elif issubclass(state, BraBase) and state.dual_class() in state_mapping: + state_inst = _make_default(state) + try: + ret = _get_ops(state_inst, + _make_set(state_mapping[state.dual_class()])) + except (NotImplementedError, TypeError): + ret = state_mapping[state.dual_class()] + else: + ret = None + + return _make_set(ret) + + +def _make_default(expr): + # XXX: Catching TypeError like this is a bad way of distinguishing between + # classes and instances. The logic using this function should be rewritten + # somehow. + try: + ret = expr() + except TypeError: + ret = expr + + return ret + + +def _get_state(state_class, ops, **options): + # Try to get a state instance from the operator INSTANCES. + # If this fails, get the class + try: + ret = state_class._operators_to_state(ops, **options) + except NotImplementedError: + ret = _make_default(state_class) + + return ret + + +def _get_ops(state_inst, op_classes, **options): + # Try to get operator instances from the state INSTANCE. + # If this fails, just return the classes + try: + ret = state_inst._state_to_operators(op_classes, **options) + except NotImplementedError: + if isinstance(op_classes, (set, tuple, frozenset)): + ret = tuple(_make_default(x) for x in op_classes) + else: + ret = _make_default(op_classes) + + if isinstance(ret, set) and len(ret) == 1: + return ret[0] + + return ret + + +def _make_set(ops): + if isinstance(ops, (tuple, list, frozenset)): + return set(ops) + else: + return ops diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/pauli.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/pauli.py new file mode 100644 index 0000000000000000000000000000000000000000..89762ed2b38e1c5df3775714ee08d3700df0fa65 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/pauli.py @@ -0,0 +1,675 @@ +"""Pauli operators and states""" + +from sympy.core.add import Add +from sympy.core.mul import Mul +from sympy.core.numbers import I +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.functions.elementary.exponential import exp +from sympy.physics.quantum import Operator, Ket, Bra +from sympy.physics.quantum import ComplexSpace +from sympy.matrices import Matrix +from sympy.functions.special.tensor_functions import KroneckerDelta + +__all__ = [ + 'SigmaX', 'SigmaY', 'SigmaZ', 'SigmaMinus', 'SigmaPlus', 'SigmaZKet', + 'SigmaZBra', 'qsimplify_pauli' +] + + +class SigmaOpBase(Operator): + """Pauli sigma operator, base class""" + + @property + def name(self): + return self.args[0] + + @property + def use_name(self): + return bool(self.args[0]) is not False + + @classmethod + def default_args(self): + return (False,) + + def __new__(cls, *args, **hints): + return Operator.__new__(cls, *args, **hints) + + def _eval_commutator_BosonOp(self, other, **hints): + return S.Zero + + +class SigmaX(SigmaOpBase): + """Pauli sigma x operator + + Parameters + ========== + + name : str + An optional string that labels the operator. Pauli operators with + different names commute. + + Examples + ======== + + >>> from sympy.physics.quantum import represent + >>> from sympy.physics.quantum.pauli import SigmaX + >>> sx = SigmaX() + >>> sx + SigmaX() + >>> represent(sx) + Matrix([ + [0, 1], + [1, 0]]) + """ + + def __new__(cls, *args, **hints): + return SigmaOpBase.__new__(cls, *args, **hints) + + def _eval_commutator_SigmaY(self, other, **hints): + if self.name != other.name: + return S.Zero + else: + return 2 * I * SigmaZ(self.name) + + def _eval_commutator_SigmaZ(self, other, **hints): + if self.name != other.name: + return S.Zero + else: + return - 2 * I * SigmaY(self.name) + + def _eval_commutator_BosonOp(self, other, **hints): + return S.Zero + + def _eval_anticommutator_SigmaY(self, other, **hints): + return S.Zero + + def _eval_anticommutator_SigmaZ(self, other, **hints): + return S.Zero + + def _eval_adjoint(self): + return self + + def _print_contents_latex(self, printer, *args): + if self.use_name: + return r'{\sigma_x^{(%s)}}' % str(self.name) + else: + return r'{\sigma_x}' + + def _print_contents(self, printer, *args): + return 'SigmaX()' + + def _eval_power(self, e): + if e.is_Integer and e.is_positive: + return SigmaX(self.name).__pow__(int(e) % 2) + + def _represent_default_basis(self, **options): + format = options.get('format', 'sympy') + if format == 'sympy': + return Matrix([[0, 1], [1, 0]]) + else: + raise NotImplementedError('Representation in format ' + + format + ' not implemented.') + + +class SigmaY(SigmaOpBase): + """Pauli sigma y operator + + Parameters + ========== + + name : str + An optional string that labels the operator. Pauli operators with + different names commute. + + Examples + ======== + + >>> from sympy.physics.quantum import represent + >>> from sympy.physics.quantum.pauli import SigmaY + >>> sy = SigmaY() + >>> sy + SigmaY() + >>> represent(sy) + Matrix([ + [0, -I], + [I, 0]]) + """ + + def __new__(cls, *args, **hints): + return SigmaOpBase.__new__(cls, *args) + + def _eval_commutator_SigmaZ(self, other, **hints): + if self.name != other.name: + return S.Zero + else: + return 2 * I * SigmaX(self.name) + + def _eval_commutator_SigmaX(self, other, **hints): + if self.name != other.name: + return S.Zero + else: + return - 2 * I * SigmaZ(self.name) + + def _eval_anticommutator_SigmaX(self, other, **hints): + return S.Zero + + def _eval_anticommutator_SigmaZ(self, other, **hints): + return S.Zero + + def _eval_adjoint(self): + return self + + def _print_contents_latex(self, printer, *args): + if self.use_name: + return r'{\sigma_y^{(%s)}}' % str(self.name) + else: + return r'{\sigma_y}' + + def _print_contents(self, printer, *args): + return 'SigmaY()' + + def _eval_power(self, e): + if e.is_Integer and e.is_positive: + return SigmaY(self.name).__pow__(int(e) % 2) + + def _represent_default_basis(self, **options): + format = options.get('format', 'sympy') + if format == 'sympy': + return Matrix([[0, -I], [I, 0]]) + else: + raise NotImplementedError('Representation in format ' + + format + ' not implemented.') + + +class SigmaZ(SigmaOpBase): + """Pauli sigma z operator + + Parameters + ========== + + name : str + An optional string that labels the operator. Pauli operators with + different names commute. + + Examples + ======== + + >>> from sympy.physics.quantum import represent + >>> from sympy.physics.quantum.pauli import SigmaZ + >>> sz = SigmaZ() + >>> sz ** 3 + SigmaZ() + >>> represent(sz) + Matrix([ + [1, 0], + [0, -1]]) + """ + + def __new__(cls, *args, **hints): + return SigmaOpBase.__new__(cls, *args) + + def _eval_commutator_SigmaX(self, other, **hints): + if self.name != other.name: + return S.Zero + else: + return 2 * I * SigmaY(self.name) + + def _eval_commutator_SigmaY(self, other, **hints): + if self.name != other.name: + return S.Zero + else: + return - 2 * I * SigmaX(self.name) + + def _eval_anticommutator_SigmaX(self, other, **hints): + return S.Zero + + def _eval_anticommutator_SigmaY(self, other, **hints): + return S.Zero + + def _eval_adjoint(self): + return self + + def _print_contents_latex(self, printer, *args): + if self.use_name: + return r'{\sigma_z^{(%s)}}' % str(self.name) + else: + return r'{\sigma_z}' + + def _print_contents(self, printer, *args): + return 'SigmaZ()' + + def _eval_power(self, e): + if e.is_Integer and e.is_positive: + return SigmaZ(self.name).__pow__(int(e) % 2) + + def _represent_default_basis(self, **options): + format = options.get('format', 'sympy') + if format == 'sympy': + return Matrix([[1, 0], [0, -1]]) + else: + raise NotImplementedError('Representation in format ' + + format + ' not implemented.') + + +class SigmaMinus(SigmaOpBase): + """Pauli sigma minus operator + + Parameters + ========== + + name : str + An optional string that labels the operator. Pauli operators with + different names commute. + + Examples + ======== + + >>> from sympy.physics.quantum import represent, Dagger + >>> from sympy.physics.quantum.pauli import SigmaMinus + >>> sm = SigmaMinus() + >>> sm + SigmaMinus() + >>> Dagger(sm) + SigmaPlus() + >>> represent(sm) + Matrix([ + [0, 0], + [1, 0]]) + """ + + def __new__(cls, *args, **hints): + return SigmaOpBase.__new__(cls, *args) + + def _eval_commutator_SigmaX(self, other, **hints): + if self.name != other.name: + return S.Zero + else: + return -SigmaZ(self.name) + + def _eval_commutator_SigmaY(self, other, **hints): + if self.name != other.name: + return S.Zero + else: + return I * SigmaZ(self.name) + + def _eval_commutator_SigmaZ(self, other, **hints): + return 2 * self + + def _eval_commutator_SigmaMinus(self, other, **hints): + return SigmaZ(self.name) + + def _eval_anticommutator_SigmaZ(self, other, **hints): + return S.Zero + + def _eval_anticommutator_SigmaX(self, other, **hints): + return S.One + + def _eval_anticommutator_SigmaY(self, other, **hints): + return I * S.NegativeOne + + def _eval_anticommutator_SigmaPlus(self, other, **hints): + return S.One + + def _eval_adjoint(self): + return SigmaPlus(self.name) + + def _eval_power(self, e): + if e.is_Integer and e.is_positive: + return S.Zero + + def _print_contents_latex(self, printer, *args): + if self.use_name: + return r'{\sigma_-^{(%s)}}' % str(self.name) + else: + return r'{\sigma_-}' + + def _print_contents(self, printer, *args): + return 'SigmaMinus()' + + def _represent_default_basis(self, **options): + format = options.get('format', 'sympy') + if format == 'sympy': + return Matrix([[0, 0], [1, 0]]) + else: + raise NotImplementedError('Representation in format ' + + format + ' not implemented.') + + +class SigmaPlus(SigmaOpBase): + """Pauli sigma plus operator + + Parameters + ========== + + name : str + An optional string that labels the operator. Pauli operators with + different names commute. + + Examples + ======== + + >>> from sympy.physics.quantum import represent, Dagger + >>> from sympy.physics.quantum.pauli import SigmaPlus + >>> sp = SigmaPlus() + >>> sp + SigmaPlus() + >>> Dagger(sp) + SigmaMinus() + >>> represent(sp) + Matrix([ + [0, 1], + [0, 0]]) + """ + + def __new__(cls, *args, **hints): + return SigmaOpBase.__new__(cls, *args) + + def _eval_commutator_SigmaX(self, other, **hints): + if self.name != other.name: + return S.Zero + else: + return SigmaZ(self.name) + + def _eval_commutator_SigmaY(self, other, **hints): + if self.name != other.name: + return S.Zero + else: + return I * SigmaZ(self.name) + + def _eval_commutator_SigmaZ(self, other, **hints): + if self.name != other.name: + return S.Zero + else: + return -2 * self + + def _eval_commutator_SigmaMinus(self, other, **hints): + return SigmaZ(self.name) + + def _eval_anticommutator_SigmaZ(self, other, **hints): + return S.Zero + + def _eval_anticommutator_SigmaX(self, other, **hints): + return S.One + + def _eval_anticommutator_SigmaY(self, other, **hints): + return I + + def _eval_anticommutator_SigmaMinus(self, other, **hints): + return S.One + + def _eval_adjoint(self): + return SigmaMinus(self.name) + + def _eval_mul(self, other): + return self * other + + def _eval_power(self, e): + if e.is_Integer and e.is_positive: + return S.Zero + + def _print_contents_latex(self, printer, *args): + if self.use_name: + return r'{\sigma_+^{(%s)}}' % str(self.name) + else: + return r'{\sigma_+}' + + def _print_contents(self, printer, *args): + return 'SigmaPlus()' + + def _represent_default_basis(self, **options): + format = options.get('format', 'sympy') + if format == 'sympy': + return Matrix([[0, 1], [0, 0]]) + else: + raise NotImplementedError('Representation in format ' + + format + ' not implemented.') + + +class SigmaZKet(Ket): + """Ket for a two-level system quantum system. + + Parameters + ========== + + n : Number + The state number (0 or 1). + + """ + + def __new__(cls, n): + if n not in (0, 1): + raise ValueError("n must be 0 or 1") + return Ket.__new__(cls, n) + + @property + def n(self): + return self.label[0] + + @classmethod + def dual_class(self): + return SigmaZBra + + @classmethod + def _eval_hilbert_space(cls, label): + return ComplexSpace(2) + + def _eval_innerproduct_SigmaZBra(self, bra, **hints): + return KroneckerDelta(self.n, bra.n) + + def _apply_from_right_to_SigmaZ(self, op, **options): + if self.n == 0: + return self + else: + return S.NegativeOne * self + + def _apply_from_right_to_SigmaX(self, op, **options): + return SigmaZKet(1) if self.n == 0 else SigmaZKet(0) + + def _apply_from_right_to_SigmaY(self, op, **options): + return I * SigmaZKet(1) if self.n == 0 else (-I) * SigmaZKet(0) + + def _apply_from_right_to_SigmaMinus(self, op, **options): + if self.n == 0: + return SigmaZKet(1) + else: + return S.Zero + + def _apply_from_right_to_SigmaPlus(self, op, **options): + if self.n == 0: + return S.Zero + else: + return SigmaZKet(0) + + def _represent_default_basis(self, **options): + format = options.get('format', 'sympy') + if format == 'sympy': + return Matrix([[1], [0]]) if self.n == 0 else Matrix([[0], [1]]) + else: + raise NotImplementedError('Representation in format ' + + format + ' not implemented.') + + +class SigmaZBra(Bra): + """Bra for a two-level quantum system. + + Parameters + ========== + + n : Number + The state number (0 or 1). + + """ + + def __new__(cls, n): + if n not in (0, 1): + raise ValueError("n must be 0 or 1") + return Bra.__new__(cls, n) + + @property + def n(self): + return self.label[0] + + @classmethod + def dual_class(self): + return SigmaZKet + + +def _qsimplify_pauli_product(a, b): + """ + Internal helper function for simplifying products of Pauli operators. + """ + if not (isinstance(a, SigmaOpBase) and isinstance(b, SigmaOpBase)): + return Mul(a, b) + + if a.name != b.name: + # Pauli matrices with different labels commute; sort by name + if a.name < b.name: + return Mul(a, b) + else: + return Mul(b, a) + + elif isinstance(a, SigmaX): + + if isinstance(b, SigmaX): + return S.One + + if isinstance(b, SigmaY): + return I * SigmaZ(a.name) + + if isinstance(b, SigmaZ): + return - I * SigmaY(a.name) + + if isinstance(b, SigmaMinus): + return (S.Half + SigmaZ(a.name)/2) + + if isinstance(b, SigmaPlus): + return (S.Half - SigmaZ(a.name)/2) + + elif isinstance(a, SigmaY): + + if isinstance(b, SigmaX): + return - I * SigmaZ(a.name) + + if isinstance(b, SigmaY): + return S.One + + if isinstance(b, SigmaZ): + return I * SigmaX(a.name) + + if isinstance(b, SigmaMinus): + return -I * (S.One + SigmaZ(a.name))/2 + + if isinstance(b, SigmaPlus): + return I * (S.One - SigmaZ(a.name))/2 + + elif isinstance(a, SigmaZ): + + if isinstance(b, SigmaX): + return I * SigmaY(a.name) + + if isinstance(b, SigmaY): + return - I * SigmaX(a.name) + + if isinstance(b, SigmaZ): + return S.One + + if isinstance(b, SigmaMinus): + return - SigmaMinus(a.name) + + if isinstance(b, SigmaPlus): + return SigmaPlus(a.name) + + elif isinstance(a, SigmaMinus): + + if isinstance(b, SigmaX): + return (S.One - SigmaZ(a.name))/2 + + if isinstance(b, SigmaY): + return - I * (S.One - SigmaZ(a.name))/2 + + if isinstance(b, SigmaZ): + # (SigmaX(a.name) - I * SigmaY(a.name))/2 + return SigmaMinus(b.name) + + if isinstance(b, SigmaMinus): + return S.Zero + + if isinstance(b, SigmaPlus): + return S.Half - SigmaZ(a.name)/2 + + elif isinstance(a, SigmaPlus): + + if isinstance(b, SigmaX): + return (S.One + SigmaZ(a.name))/2 + + if isinstance(b, SigmaY): + return I * (S.One + SigmaZ(a.name))/2 + + if isinstance(b, SigmaZ): + #-(SigmaX(a.name) + I * SigmaY(a.name))/2 + return -SigmaPlus(a.name) + + if isinstance(b, SigmaMinus): + return (S.One + SigmaZ(a.name))/2 + + if isinstance(b, SigmaPlus): + return S.Zero + + else: + return a * b + + +def qsimplify_pauli(e): + """ + Simplify an expression that includes products of pauli operators. + + Parameters + ========== + + e : expression + An expression that contains products of Pauli operators that is + to be simplified. + + Examples + ======== + + >>> from sympy.physics.quantum.pauli import SigmaX, SigmaY + >>> from sympy.physics.quantum.pauli import qsimplify_pauli + >>> sx, sy = SigmaX(), SigmaY() + >>> sx * sy + SigmaX()*SigmaY() + >>> qsimplify_pauli(sx * sy) + I*SigmaZ() + """ + if isinstance(e, Operator): + return e + + if isinstance(e, (Add, Pow, exp)): + t = type(e) + return t(*(qsimplify_pauli(arg) for arg in e.args)) + + if isinstance(e, Mul): + + c, nc = e.args_cnc() + + nc_s = [] + while nc: + curr = nc.pop(0) + + while (len(nc) and + isinstance(curr, SigmaOpBase) and + isinstance(nc[0], SigmaOpBase) and + curr.name == nc[0].name): + + x = nc.pop(0) + y = _qsimplify_pauli_product(curr, x) + c1, nc1 = y.args_cnc() + curr = Mul(*nc1) + c = c + c1 + + nc_s.append(curr) + + return Mul(*c) * Mul(*nc_s) + + return e diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/piab.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/piab.py new file mode 100644 index 0000000000000000000000000000000000000000..f8ac8135ee03e640f745070602c7dd8ca20f2767 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/piab.py @@ -0,0 +1,72 @@ +"""1D quantum particle in a box.""" + +from sympy.core.numbers import pi +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin +from sympy.sets.sets import Interval + +from sympy.physics.quantum.operator import HermitianOperator +from sympy.physics.quantum.state import Ket, Bra +from sympy.physics.quantum.constants import hbar +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.physics.quantum.hilbert import L2 + +m = Symbol('m') +L = Symbol('L') + + +__all__ = [ + 'PIABHamiltonian', + 'PIABKet', + 'PIABBra' +] + + +class PIABHamiltonian(HermitianOperator): + """Particle in a box Hamiltonian operator.""" + + @classmethod + def _eval_hilbert_space(cls, label): + return L2(Interval(S.NegativeInfinity, S.Infinity)) + + def _apply_operator_PIABKet(self, ket, **options): + n = ket.label[0] + return (n**2*pi**2*hbar**2)/(2*m*L**2)*ket + + +class PIABKet(Ket): + """Particle in a box eigenket.""" + + @classmethod + def _eval_hilbert_space(cls, args): + return L2(Interval(S.NegativeInfinity, S.Infinity)) + + @classmethod + def dual_class(self): + return PIABBra + + def _represent_default_basis(self, **options): + return self._represent_XOp(None, **options) + + def _represent_XOp(self, basis, **options): + x = Symbol('x') + n = Symbol('n') + subs_info = options.get('subs', {}) + return sqrt(2/L)*sin(n*pi*x/L).subs(subs_info) + + def _eval_innerproduct_PIABBra(self, bra): + return KroneckerDelta(bra.label[0], self.label[0]) + + +class PIABBra(Bra): + """Particle in a box eigenbra.""" + + @classmethod + def _eval_hilbert_space(cls, label): + return L2(Interval(S.NegativeInfinity, S.Infinity)) + + @classmethod + def dual_class(self): + return PIABKet diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/qapply.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/qapply.py new file mode 100644 index 0000000000000000000000000000000000000000..a2d8c92e51552c8114d65a1304fcd1925ae752f4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/qapply.py @@ -0,0 +1,263 @@ +"""Logic for applying operators to states. + +Todo: +* Sometimes the final result needs to be expanded, we should do this by hand. +""" + +from sympy.concrete import Sum +from sympy.core.add import Add +from sympy.core.kind import NumberKind +from sympy.core.mul import Mul +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.sympify import sympify, _sympify + +from sympy.physics.quantum.anticommutator import AntiCommutator +from sympy.physics.quantum.commutator import Commutator +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.innerproduct import InnerProduct +from sympy.physics.quantum.operator import OuterProduct, Operator +from sympy.physics.quantum.state import State, KetBase, BraBase, Wavefunction +from sympy.physics.quantum.tensorproduct import TensorProduct + +__all__ = [ + 'qapply' +] + + +#----------------------------------------------------------------------------- +# Main code +#----------------------------------------------------------------------------- + + +def ip_doit_func(e): + """Transform the inner products in an expression by calling ``.doit()``.""" + return e.replace(InnerProduct, lambda *args: InnerProduct(*args).doit()) + + +def sum_doit_func(e): + """Transform the sums in an expression by calling ``.doit()``.""" + return e.replace(Sum, lambda *args: Sum(*args).doit()) + + +def qapply(e, **options): + """Apply operators to states in a quantum expression. + + Parameters + ========== + + e : Expr + The expression containing operators and states. This expression tree + will be walked to find operators acting on states symbolically. + options : dict + A dict of key/value pairs that determine how the operator actions + are carried out. + + The following options are valid: + + * ``dagger``: try to apply Dagger operators to the left + (default: False). + * ``ip_doit``: call ``.doit()`` in inner products when they are + encountered (default: True). + * ``sum_doit``: call ``.doit()`` on sums when they are encountered + (default: False). This is helpful for collapsing sums over Kronecker + delta's that are created when calling ``qapply``. + + Returns + ======= + + e : Expr + The original expression, but with the operators applied to states. + + Examples + ======== + + >>> from sympy.physics.quantum import qapply, Ket, Bra + >>> b = Bra('b') + >>> k = Ket('k') + >>> A = k * b + >>> A + |k>>> qapply(A * b.dual / (b * b.dual)) + |k> + >>> qapply(k.dual * A / (k.dual * k)) + and A*(|a>+|b>) and all Commutators and + # TensorProducts. The only problem with this is that if we can't apply + # all the Operators, we have just expanded everything. + # TODO: don't expand the scalars in front of each Mul. + e = e.expand(commutator=True, tensorproduct=True) + + # If we just have a raw ket, return it. + if isinstance(e, KetBase): + return e + + # We have an Add(a, b, c, ...) and compute + # Add(qapply(a), qapply(b), ...) + elif isinstance(e, Add): + result = 0 + for arg in e.args: + result += qapply(arg, **options) + return result.expand() + + # For a Density operator call qapply on its state + elif isinstance(e, Density): + new_args = [(qapply(state, **options), prob) for (state, + prob) in e.args] + return Density(*new_args) + + # For a raw TensorProduct, call qapply on its args. + elif isinstance(e, TensorProduct): + return TensorProduct(*[qapply(t, **options) for t in e.args]) + + # For a Sum, call qapply on its function. + elif isinstance(e, Sum): + result = Sum(qapply(e.function, **options), *e.limits) + result = sum_doit_func(result) if sum_doit else result + return result + + # For a Pow, call qapply on its base. + elif isinstance(e, Pow): + return qapply(e.base, **options)**e.exp + + # We have a Mul where there might be actual operators to apply to kets. + elif isinstance(e, Mul): + c_part, nc_part = e.args_cnc() + c_mul = Mul(*c_part) + nc_mul = Mul(*nc_part) + if not nc_part: # If we only have a commuting part, just return it. + result = c_mul + elif isinstance(nc_mul, Mul): + result = c_mul*qapply_Mul(nc_mul, **options) + else: + result = c_mul*qapply(nc_mul, **options) + if result == e and dagger: + result = Dagger(qapply_Mul(Dagger(e), **options)) + result = ip_doit_func(result) if ip_doit else result + result = sum_doit_func(result) if sum_doit else result + return result + + # In all other cases (State, Operator, Pow, Commutator, InnerProduct, + # OuterProduct) we won't ever have operators to apply to kets. + else: + return e + + +def qapply_Mul(e, **options): + + args = list(e.args) + extra = S.One + result = None + + # If we only have 0 or 1 args, we have nothing to do and return. + if len(args) <= 1 or not isinstance(e, Mul): + return e + rhs = args.pop() + lhs = args.pop() + + # Make sure we have two non-commutative objects before proceeding. + if (not isinstance(rhs, Wavefunction) and sympify(rhs).is_commutative) or \ + (not isinstance(lhs, Wavefunction) and sympify(lhs).is_commutative): + return e + + # For a Pow with an integer exponent, apply one of them and reduce the + # exponent by one. + if isinstance(lhs, Pow) and lhs.exp.is_Integer: + args.append(lhs.base**(lhs.exp - 1)) + lhs = lhs.base + + # Pull OuterProduct apart + if isinstance(lhs, OuterProduct): + args.append(lhs.ket) + lhs = lhs.bra + + if isinstance(rhs, OuterProduct): + extra = rhs.bra # Append to the right of the result + rhs = rhs.ket + + # Call .doit() on Commutator/AntiCommutator. + if isinstance(lhs, (Commutator, AntiCommutator)): + comm = lhs.doit() + if isinstance(comm, Add): + return qapply( + e.func(*(args + [comm.args[0], rhs])) + + e.func(*(args + [comm.args[1], rhs])), + **options + )*extra + else: + return qapply(e.func(*args)*comm*rhs, **options)*extra + + # Apply tensor products of operators to states + if isinstance(lhs, TensorProduct) and all(isinstance(arg, (Operator, State, Mul, Pow)) or arg == 1 for arg in lhs.args) and \ + isinstance(rhs, TensorProduct) and all(isinstance(arg, (Operator, State, Mul, Pow)) or arg == 1 for arg in rhs.args) and \ + len(lhs.args) == len(rhs.args): + result = TensorProduct(*[qapply(lhs.args[n]*rhs.args[n], **options) for n in range(len(lhs.args))]).expand(tensorproduct=True) + return qapply_Mul(e.func(*args), **options)*result*extra + + # For Sums, move the Sum to the right. + if isinstance(rhs, Sum): + if isinstance(lhs, Sum): + if set(lhs.variables).intersection(set(rhs.variables)): + raise ValueError('Duplicated dummy indices in separate sums in qapply.') + limits = lhs.limits + rhs.limits + result = Sum(qapply(lhs.function*rhs.function, **options), *limits) + return qapply_Mul(e.func(*args)*result, **options) + else: + result = Sum(qapply(lhs*rhs.function, **options), *rhs.limits) + return qapply_Mul(e.func(*args)*result, **options) + + if isinstance(lhs, Sum): + result = Sum(qapply(lhs.function*rhs, **options), *lhs.limits) + return qapply_Mul(e.func(*args)*result, **options) + + # Now try to actually apply the operator and build an inner product. + _apply = getattr(lhs, '_apply_operator', None) + if _apply is not None: + try: + result = _apply(rhs, **options) + except NotImplementedError: + result = None + else: + result = None + + if result is None: + _apply_right = getattr(rhs, '_apply_from_right_to', None) + if _apply_right is not None: + try: + result = _apply_right(lhs, **options) + except NotImplementedError: + result = None + + if result is None: + if isinstance(lhs, BraBase) and isinstance(rhs, KetBase): + result = InnerProduct(lhs, rhs) + + # TODO: I may need to expand before returning the final result. + if isinstance(result, (int, complex, float)): + return _sympify(result) + elif result is None: + if len(args) == 0: + # We had two args to begin with so args=[]. + return e + else: + return qapply_Mul(e.func(*(args + [lhs])), **options)*rhs*extra + elif isinstance(result, InnerProduct): + return result*qapply_Mul(e.func(*args), **options)*extra + else: # result is a scalar times a Mul, Add or TensorProduct + return qapply(e.func(*args)*result, **options)*extra diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/qasm.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/qasm.py new file mode 100644 index 0000000000000000000000000000000000000000..39b49d9a67399114e7d03f12148854b2e41b0b26 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/qasm.py @@ -0,0 +1,224 @@ +""" + +qasm.py - Functions to parse a set of qasm commands into a SymPy Circuit. + +Examples taken from Chuang's page: https://web.archive.org/web/20220120121541/https://www.media.mit.edu/quanta/qasm2circ/ + +The code returns a circuit and an associated list of labels. + +>>> from sympy.physics.quantum.qasm import Qasm +>>> q = Qasm('qubit q0', 'qubit q1', 'h q0', 'cnot q0,q1') +>>> q.get_circuit() +CNOT(1,0)*H(1) + +>>> q = Qasm('qubit q0', 'qubit q1', 'cnot q0,q1', 'cnot q1,q0', 'cnot q0,q1') +>>> q.get_circuit() +CNOT(1,0)*CNOT(0,1)*CNOT(1,0) +""" + +__all__ = [ + 'Qasm', + ] + +from math import prod + +from sympy.physics.quantum.gate import H, CNOT, X, Z, CGate, CGateS, SWAP, S, T,CPHASE +from sympy.physics.quantum.circuitplot import Mz + +def read_qasm(lines): + return Qasm(*lines.splitlines()) + +def read_qasm_file(filename): + return Qasm(*open(filename).readlines()) + +def flip_index(i, n): + """Reorder qubit indices from largest to smallest. + + >>> from sympy.physics.quantum.qasm import flip_index + >>> flip_index(0, 2) + 1 + >>> flip_index(1, 2) + 0 + """ + return n-i-1 + +def trim(line): + """Remove everything following comment # characters in line. + + >>> from sympy.physics.quantum.qasm import trim + >>> trim('nothing happens here') + 'nothing happens here' + >>> trim('something #happens here') + 'something ' + """ + if '#' not in line: + return line + return line.split('#')[0] + +def get_index(target, labels): + """Get qubit labels from the rest of the line,and return indices + + >>> from sympy.physics.quantum.qasm import get_index + >>> get_index('q0', ['q0', 'q1']) + 1 + >>> get_index('q1', ['q0', 'q1']) + 0 + """ + nq = len(labels) + return flip_index(labels.index(target), nq) + +def get_indices(targets, labels): + return [get_index(t, labels) for t in targets] + +def nonblank(args): + for line in args: + line = trim(line) + if line.isspace(): + continue + yield line + return + +def fullsplit(line): + words = line.split() + rest = ' '.join(words[1:]) + return fixcommand(words[0]), [s.strip() for s in rest.split(',')] + +def fixcommand(c): + """Fix Qasm command names. + + Remove all of forbidden characters from command c, and + replace 'def' with 'qdef'. + """ + forbidden_characters = ['-'] + c = c.lower() + for char in forbidden_characters: + c = c.replace(char, '') + if c == 'def': + return 'qdef' + return c + +def stripquotes(s): + """Replace explicit quotes in a string. + + >>> from sympy.physics.quantum.qasm import stripquotes + >>> stripquotes("'S'") == 'S' + True + >>> stripquotes('"S"') == 'S' + True + >>> stripquotes('S') == 'S' + True + """ + s = s.replace('"', '') # Remove second set of quotes? + s = s.replace("'", '') + return s + +class Qasm: + """Class to form objects from Qasm lines + + >>> from sympy.physics.quantum.qasm import Qasm + >>> q = Qasm('qubit q0', 'qubit q1', 'h q0', 'cnot q0,q1') + >>> q.get_circuit() + CNOT(1,0)*H(1) + >>> q = Qasm('qubit q0', 'qubit q1', 'cnot q0,q1', 'cnot q1,q0', 'cnot q0,q1') + >>> q.get_circuit() + CNOT(1,0)*CNOT(0,1)*CNOT(1,0) + """ + def __init__(self, *args, **kwargs): + self.defs = {} + self.circuit = [] + self.labels = [] + self.inits = {} + self.add(*args) + self.kwargs = kwargs + + def add(self, *lines): + for line in nonblank(lines): + command, rest = fullsplit(line) + if self.defs.get(command): #defs come first, since you can override built-in + function = self.defs.get(command) + indices = self.indices(rest) + if len(indices) == 1: + self.circuit.append(function(indices[0])) + else: + self.circuit.append(function(indices[:-1], indices[-1])) + elif hasattr(self, command): + function = getattr(self, command) + function(*rest) + else: + print("Function %s not defined. Skipping" % command) + + def get_circuit(self): + return prod(reversed(self.circuit)) + + def get_labels(self): + return list(reversed(self.labels)) + + def plot(self): + from sympy.physics.quantum.circuitplot import CircuitPlot + circuit, labels = self.get_circuit(), self.get_labels() + CircuitPlot(circuit, len(labels), labels=labels, inits=self.inits) + + def qubit(self, arg, init=None): + self.labels.append(arg) + if init: self.inits[arg] = init + + def indices(self, args): + return get_indices(args, self.labels) + + def index(self, arg): + return get_index(arg, self.labels) + + def nop(self, *args): + pass + + def x(self, arg): + self.circuit.append(X(self.index(arg))) + + def z(self, arg): + self.circuit.append(Z(self.index(arg))) + + def h(self, arg): + self.circuit.append(H(self.index(arg))) + + def s(self, arg): + self.circuit.append(S(self.index(arg))) + + def t(self, arg): + self.circuit.append(T(self.index(arg))) + + def measure(self, arg): + self.circuit.append(Mz(self.index(arg))) + + def cnot(self, a1, a2): + self.circuit.append(CNOT(*self.indices([a1, a2]))) + + def swap(self, a1, a2): + self.circuit.append(SWAP(*self.indices([a1, a2]))) + + def cphase(self, a1, a2): + self.circuit.append(CPHASE(*self.indices([a1, a2]))) + + def toffoli(self, a1, a2, a3): + i1, i2, i3 = self.indices([a1, a2, a3]) + self.circuit.append(CGateS((i1, i2), X(i3))) + + def cx(self, a1, a2): + fi, fj = self.indices([a1, a2]) + self.circuit.append(CGate(fi, X(fj))) + + def cz(self, a1, a2): + fi, fj = self.indices([a1, a2]) + self.circuit.append(CGate(fi, Z(fj))) + + def defbox(self, *args): + print("defbox not supported yet. Skipping: ", args) + + def qdef(self, name, ncontrols, symbol): + from sympy.physics.quantum.circuitplot import CreateOneQubitGate, CreateCGate + ncontrols = int(ncontrols) + command = fixcommand(name) + symbol = stripquotes(symbol) + if ncontrols > 0: + self.defs[command] = CreateCGate(symbol) + else: + self.defs[command] = CreateOneQubitGate(symbol) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/qexpr.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/qexpr.py new file mode 100644 index 0000000000000000000000000000000000000000..64f7e2a200fa7d89b35db1da551bcbd25492f2d9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/qexpr.py @@ -0,0 +1,409 @@ +from sympy.core.expr import Expr +from sympy.core.symbol import Symbol +from sympy.core.sympify import sympify +from sympy.matrices.dense import Matrix +from sympy.printing.pretty.stringpict import prettyForm +from sympy.core.containers import Tuple +from sympy.utilities.iterables import is_sequence + +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.matrixutils import ( + numpy_ndarray, scipy_sparse_matrix, + to_sympy, to_numpy, to_scipy_sparse +) + +__all__ = [ + 'QuantumError', + 'QExpr' +] + + +#----------------------------------------------------------------------------- +# Error handling +#----------------------------------------------------------------------------- + +class QuantumError(Exception): + pass + + +def _qsympify_sequence(seq): + """Convert elements of a sequence to standard form. + + This is like sympify, but it performs special logic for arguments passed + to QExpr. The following conversions are done: + + * (list, tuple, Tuple) => _qsympify_sequence each element and convert + sequence to a Tuple. + * basestring => Symbol + * Matrix => Matrix + * other => sympify + + Strings are passed to Symbol, not sympify to make sure that variables like + 'pi' are kept as Symbols, not the SymPy built-in number subclasses. + + Examples + ======== + + >>> from sympy.physics.quantum.qexpr import _qsympify_sequence + >>> _qsympify_sequence((1,2,[3,4,[1,]])) + (1, 2, (3, 4, (1,))) + + """ + + return tuple(__qsympify_sequence_helper(seq)) + + +def __qsympify_sequence_helper(seq): + """ + Helper function for _qsympify_sequence + This function does the actual work. + """ + #base case. If not a list, do Sympification + if not is_sequence(seq): + if isinstance(seq, Matrix): + return seq + elif isinstance(seq, str): + return Symbol(seq) + else: + return sympify(seq) + + # base condition, when seq is QExpr and also + # is iterable. + if isinstance(seq, QExpr): + return seq + + #if list, recurse on each item in the list + result = [__qsympify_sequence_helper(item) for item in seq] + + return Tuple(*result) + + +#----------------------------------------------------------------------------- +# Basic Quantum Expression from which all objects descend +#----------------------------------------------------------------------------- + +class QExpr(Expr): + """A base class for all quantum object like operators and states.""" + + # In sympy, slots are for instance attributes that are computed + # dynamically by the __new__ method. They are not part of args, but they + # derive from args. + + # The Hilbert space a quantum Object belongs to. + __slots__ = ('hilbert_space', ) + + is_commutative = False + + # The separator used in printing the label. + _label_separator = '' + + def __new__(cls, *args, **kwargs): + """Construct a new quantum object. + + Parameters + ========== + + args : tuple + The list of numbers or parameters that uniquely specify the + quantum object. For a state, this will be its symbol or its + set of quantum numbers. + + Examples + ======== + + >>> from sympy.physics.quantum.qexpr import QExpr + >>> q = QExpr(0) + >>> q + 0 + >>> q.label + (0,) + >>> q.hilbert_space + H + >>> q.args + (0,) + >>> q.is_commutative + False + """ + + # First compute args and call Expr.__new__ to create the instance + args = cls._eval_args(args, **kwargs) + if len(args) == 0: + args = cls._eval_args(tuple(cls.default_args()), **kwargs) + inst = Expr.__new__(cls, *args) + # Now set the slots on the instance + inst.hilbert_space = cls._eval_hilbert_space(args) + return inst + + @classmethod + def _new_rawargs(cls, hilbert_space, *args, **old_assumptions): + """Create new instance of this class with hilbert_space and args. + + This is used to bypass the more complex logic in the ``__new__`` + method in cases where you already have the exact ``hilbert_space`` + and ``args``. This should be used when you are positive these + arguments are valid, in their final, proper form and want to optimize + the creation of the object. + """ + + obj = Expr.__new__(cls, *args, **old_assumptions) + obj.hilbert_space = hilbert_space + return obj + + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + + @property + def label(self): + """The label is the unique set of identifiers for the object. + + Usually, this will include all of the information about the state + *except* the time (in the case of time-dependent objects). + + This must be a tuple, rather than a Tuple. + """ + if len(self.args) == 0: # If there is no label specified, return the default + return self._eval_args(list(self.default_args())) + else: + return self.args + + @property + def is_symbolic(self): + return True + + @classmethod + def default_args(self): + """If no arguments are specified, then this will return a default set + of arguments to be run through the constructor. + + NOTE: Any classes that override this MUST return a tuple of arguments. + Should be overridden by subclasses to specify the default arguments for kets and operators + """ + raise NotImplementedError("No default arguments for this class!") + + #------------------------------------------------------------------------- + # _eval_* methods + #------------------------------------------------------------------------- + + def _eval_adjoint(self): + obj = Expr._eval_adjoint(self) + if obj is None: + obj = Expr.__new__(Dagger, self) + if isinstance(obj, QExpr): + obj.hilbert_space = self.hilbert_space + return obj + + @classmethod + def _eval_args(cls, args): + """Process the args passed to the __new__ method. + + This simply runs args through _qsympify_sequence. + """ + return _qsympify_sequence(args) + + @classmethod + def _eval_hilbert_space(cls, args): + """Compute the Hilbert space instance from the args. + """ + from sympy.physics.quantum.hilbert import HilbertSpace + return HilbertSpace() + + #------------------------------------------------------------------------- + # Printing + #------------------------------------------------------------------------- + + # Utilities for printing: these operate on raw SymPy objects + + def _print_sequence(self, seq, sep, printer, *args): + result = [] + for item in seq: + result.append(printer._print(item, *args)) + return sep.join(result) + + def _print_sequence_pretty(self, seq, sep, printer, *args): + pform = printer._print(seq[0], *args) + for item in seq[1:]: + pform = prettyForm(*pform.right(sep)) + pform = prettyForm(*pform.right(printer._print(item, *args))) + return pform + + # Utilities for printing: these operate prettyForm objects + + def _print_subscript_pretty(self, a, b): + top = prettyForm(*b.left(' '*a.width())) + bot = prettyForm(*a.right(' '*b.width())) + return prettyForm(binding=prettyForm.POW, *bot.below(top)) + + def _print_superscript_pretty(self, a, b): + return a**b + + def _print_parens_pretty(self, pform, left='(', right=')'): + return prettyForm(*pform.parens(left=left, right=right)) + + # Printing of labels (i.e. args) + + def _print_label(self, printer, *args): + """Prints the label of the QExpr + + This method prints self.label, using self._label_separator to separate + the elements. This method should not be overridden, instead, override + _print_contents to change printing behavior. + """ + return self._print_sequence( + self.label, self._label_separator, printer, *args + ) + + def _print_label_repr(self, printer, *args): + return self._print_sequence( + self.label, ',', printer, *args + ) + + def _print_label_pretty(self, printer, *args): + return self._print_sequence_pretty( + self.label, self._label_separator, printer, *args + ) + + def _print_label_latex(self, printer, *args): + return self._print_sequence( + self.label, self._label_separator, printer, *args + ) + + # Printing of contents (default to label) + + def _print_contents(self, printer, *args): + """Printer for contents of QExpr + + Handles the printing of any unique identifying contents of a QExpr to + print as its contents, such as any variables or quantum numbers. The + default is to print the label, which is almost always the args. This + should not include printing of any brackets or parentheses. + """ + return self._print_label(printer, *args) + + def _print_contents_pretty(self, printer, *args): + return self._print_label_pretty(printer, *args) + + def _print_contents_latex(self, printer, *args): + return self._print_label_latex(printer, *args) + + # Main printing methods + + def _sympystr(self, printer, *args): + """Default printing behavior of QExpr objects + + Handles the default printing of a QExpr. To add other things to the + printing of the object, such as an operator name to operators or + brackets to states, the class should override the _print/_pretty/_latex + functions directly and make calls to _print_contents where appropriate. + This allows things like InnerProduct to easily control its printing the + printing of contents. + """ + return self._print_contents(printer, *args) + + def _sympyrepr(self, printer, *args): + classname = self.__class__.__name__ + label = self._print_label_repr(printer, *args) + return '%s(%s)' % (classname, label) + + def _pretty(self, printer, *args): + pform = self._print_contents_pretty(printer, *args) + return pform + + def _latex(self, printer, *args): + return self._print_contents_latex(printer, *args) + + #------------------------------------------------------------------------- + # Represent + #------------------------------------------------------------------------- + + def _represent_default_basis(self, **options): + raise NotImplementedError('This object does not have a default basis') + + def _represent(self, *, basis=None, **options): + """Represent this object in a given basis. + + This method dispatches to the actual methods that perform the + representation. Subclases of QExpr should define various methods to + determine how the object will be represented in various bases. The + format of these methods is:: + + def _represent_BasisName(self, basis, **options): + + Thus to define how a quantum object is represented in the basis of + the operator Position, you would define:: + + def _represent_Position(self, basis, **options): + + Usually, basis object will be instances of Operator subclasses, but + there is a chance we will relax this in the future to accommodate other + types of basis sets that are not associated with an operator. + + If the ``format`` option is given it can be ("sympy", "numpy", + "scipy.sparse"). This will ensure that any matrices that result from + representing the object are returned in the appropriate matrix format. + + Parameters + ========== + + basis : Operator + The Operator whose basis functions will be used as the basis for + representation. + options : dict + A dictionary of key/value pairs that give options and hints for + the representation, such as the number of basis functions to + be used. + """ + if basis is None: + result = self._represent_default_basis(**options) + else: + result = dispatch_method(self, '_represent', basis, **options) + + # If we get a matrix representation, convert it to the right format. + format = options.get('format', 'sympy') + result = self._format_represent(result, format) + return result + + def _format_represent(self, result, format): + if format == 'sympy' and not isinstance(result, Matrix): + return to_sympy(result) + elif format == 'numpy' and not isinstance(result, numpy_ndarray): + return to_numpy(result) + elif format == 'scipy.sparse' and \ + not isinstance(result, scipy_sparse_matrix): + return to_scipy_sparse(result) + + return result + + +def split_commutative_parts(e): + """Split into commutative and non-commutative parts.""" + c_part, nc_part = e.args_cnc() + c_part = list(c_part) + return c_part, nc_part + + +def split_qexpr_parts(e): + """Split an expression into Expr and noncommutative QExpr parts.""" + expr_part = [] + qexpr_part = [] + for arg in e.args: + if not isinstance(arg, QExpr): + expr_part.append(arg) + else: + qexpr_part.append(arg) + return expr_part, qexpr_part + + +def dispatch_method(self, basename, arg, **options): + """Dispatch a method to the proper handlers.""" + method_name = '%s_%s' % (basename, arg.__class__.__name__) + if hasattr(self, method_name): + f = getattr(self, method_name) + # This can raise and we will allow it to propagate. + result = f(arg, **options) + if result is not None: + return result + raise NotImplementedError( + "%s.%s cannot handle: %r" % + (self.__class__.__name__, basename, arg) + ) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/qft.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/qft.py new file mode 100644 index 0000000000000000000000000000000000000000..c6a3fa4539267f7bb6cf015521007e292b3d4cfd --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/qft.py @@ -0,0 +1,215 @@ +"""An implementation of qubits and gates acting on them. + +Todo: + +* Update docstrings. +* Update tests. +* Implement apply using decompose. +* Implement represent using decompose or something smarter. For this to + work we first have to implement represent for SWAP. +* Decide if we want upper index to be inclusive in the constructor. +* Fix the printing of Rk gates in plotting. +""" + +from sympy.core.expr import Expr +from sympy.core.numbers import (I, Integer, pi) +from sympy.core.symbol import Symbol +from sympy.functions.elementary.exponential import exp +from sympy.matrices.dense import Matrix +from sympy.functions import sqrt + +from sympy.physics.quantum.qapply import qapply +from sympy.physics.quantum.qexpr import QuantumError, QExpr +from sympy.matrices import eye +from sympy.physics.quantum.tensorproduct import matrix_tensor_product + +from sympy.physics.quantum.gate import ( + Gate, HadamardGate, SwapGate, OneQubitGate, CGate, PhaseGate, TGate, ZGate +) + +from sympy.functions.elementary.complexes import sign + +__all__ = [ + 'QFT', + 'IQFT', + 'RkGate', + 'Rk' +] + +#----------------------------------------------------------------------------- +# Fourier stuff +#----------------------------------------------------------------------------- + + +class RkGate(OneQubitGate): + """This is the R_k gate of the QTF.""" + gate_name = 'Rk' + gate_name_latex = 'R' + + def __new__(cls, *args): + if len(args) != 2: + raise QuantumError( + 'Rk gates only take two arguments, got: %r' % args + ) + # For small k, Rk gates simplify to other gates, using these + # substitutions give us familiar results for the QFT for small numbers + # of qubits. + target = args[0] + k = args[1] + if k == 1: + return ZGate(target) + elif k == 2: + return PhaseGate(target) + elif k == 3: + return TGate(target) + args = cls._eval_args(args) + inst = Expr.__new__(cls, *args) + inst.hilbert_space = cls._eval_hilbert_space(args) + return inst + + @classmethod + def _eval_args(cls, args): + # Fall back to this, because Gate._eval_args assumes that args is + # all targets and can't contain duplicates. + return QExpr._eval_args(args) + + @property + def k(self): + return self.label[1] + + @property + def targets(self): + return self.label[:1] + + @property + def gate_name_plot(self): + return r'$%s_%s$' % (self.gate_name_latex, str(self.k)) + + def get_target_matrix(self, format='sympy'): + if format == 'sympy': + return Matrix([[1, 0], [0, exp(sign(self.k)*Integer(2)*pi*I/(Integer(2)**abs(self.k)))]]) + raise NotImplementedError( + 'Invalid format for the R_k gate: %r' % format) + + +Rk = RkGate + + +class Fourier(Gate): + """Superclass of Quantum Fourier and Inverse Quantum Fourier Gates.""" + + @classmethod + def _eval_args(self, args): + if len(args) != 2: + raise QuantumError( + 'QFT/IQFT only takes two arguments, got: %r' % args + ) + if args[0] >= args[1]: + raise QuantumError("Start must be smaller than finish") + return Gate._eval_args(args) + + def _represent_default_basis(self, **options): + return self._represent_ZGate(None, **options) + + def _represent_ZGate(self, basis, **options): + """ + Represents the (I)QFT In the Z Basis + """ + nqubits = options.get('nqubits', 0) + if nqubits == 0: + raise QuantumError( + 'The number of qubits must be given as nqubits.') + if nqubits < self.min_qubits: + raise QuantumError( + 'The number of qubits %r is too small for the gate.' % nqubits + ) + size = self.size + omega = self.omega + + #Make a matrix that has the basic Fourier Transform Matrix + arrayFT = [[omega**( + i*j % size)/sqrt(size) for i in range(size)] for j in range(size)] + matrixFT = Matrix(arrayFT) + + #Embed the FT Matrix in a higher space, if necessary + if self.label[0] != 0: + matrixFT = matrix_tensor_product(eye(2**self.label[0]), matrixFT) + if self.min_qubits < nqubits: + matrixFT = matrix_tensor_product( + matrixFT, eye(2**(nqubits - self.min_qubits))) + + return matrixFT + + @property + def targets(self): + return range(self.label[0], self.label[1]) + + @property + def min_qubits(self): + return self.label[1] + + @property + def size(self): + """Size is the size of the QFT matrix""" + return 2**(self.label[1] - self.label[0]) + + @property + def omega(self): + return Symbol('omega') + + +class QFT(Fourier): + """The forward quantum Fourier transform.""" + + gate_name = 'QFT' + gate_name_latex = 'QFT' + + def decompose(self): + """Decomposes QFT into elementary gates.""" + start = self.label[0] + finish = self.label[1] + circuit = 1 + for level in reversed(range(start, finish)): + circuit = HadamardGate(level)*circuit + for i in range(level - start): + circuit = CGate(level - i - 1, RkGate(level, i + 2))*circuit + for i in range((finish - start)//2): + circuit = SwapGate(i + start, finish - i - 1)*circuit + return circuit + + def _apply_operator_Qubit(self, qubits, **options): + return qapply(self.decompose()*qubits) + + def _eval_inverse(self): + return IQFT(*self.args) + + @property + def omega(self): + return exp(2*pi*I/self.size) + + +class IQFT(Fourier): + """The inverse quantum Fourier transform.""" + + gate_name = 'IQFT' + gate_name_latex = '{QFT^{-1}}' + + def decompose(self): + """Decomposes IQFT into elementary gates.""" + start = self.args[0] + finish = self.args[1] + circuit = 1 + for i in range((finish - start)//2): + circuit = SwapGate(i + start, finish - i - 1)*circuit + for level in range(start, finish): + for i in reversed(range(level - start)): + circuit = CGate(level - i - 1, RkGate(level, -i - 2))*circuit + circuit = HadamardGate(level)*circuit + return circuit + + def _eval_inverse(self): + return QFT(*self.args) + + @property + def omega(self): + return exp(-2*pi*I/self.size) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/qubit.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/qubit.py new file mode 100644 index 0000000000000000000000000000000000000000..71d1dbc01e3a16e2a4b64eec3c3800b7218b2636 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/qubit.py @@ -0,0 +1,811 @@ +"""Qubits for quantum computing. + +Todo: +* Finish implementing measurement logic. This should include POVM. +* Update docstrings. +* Update tests. +""" + + +import math + +from sympy.core.add import Add +from sympy.core.mul import Mul +from sympy.core.numbers import Integer +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.functions.elementary.complexes import conjugate +from sympy.functions.elementary.exponential import log +from sympy.core.basic import _sympify +from sympy.external.gmpy import SYMPY_INTS +from sympy.matrices import Matrix, zeros +from sympy.printing.pretty.stringpict import prettyForm + +from sympy.physics.quantum.hilbert import ComplexSpace +from sympy.physics.quantum.state import Ket, Bra, State + +from sympy.physics.quantum.qexpr import QuantumError +from sympy.physics.quantum.represent import represent +from sympy.physics.quantum.matrixutils import ( + numpy_ndarray, scipy_sparse_matrix +) +from mpmath.libmp.libintmath import bitcount + +__all__ = [ + 'Qubit', + 'QubitBra', + 'IntQubit', + 'IntQubitBra', + 'qubit_to_matrix', + 'matrix_to_qubit', + 'matrix_to_density', + 'measure_all', + 'measure_partial', + 'measure_partial_oneshot', + 'measure_all_oneshot' +] + +#----------------------------------------------------------------------------- +# Qubit Classes +#----------------------------------------------------------------------------- + + +class QubitState(State): + """Base class for Qubit and QubitBra.""" + + #------------------------------------------------------------------------- + # Initialization/creation + #------------------------------------------------------------------------- + + @classmethod + def _eval_args(cls, args): + # If we are passed a QubitState or subclass, we just take its qubit + # values directly. + if len(args) == 1 and isinstance(args[0], QubitState): + return args[0].qubit_values + + # Turn strings into tuple of strings + if len(args) == 1 and isinstance(args[0], str): + args = tuple( S.Zero if qb == "0" else S.One for qb in args[0]) + else: + args = tuple( S.Zero if qb == "0" else S.One if qb == "1" else qb for qb in args) + args = tuple(_sympify(arg) for arg in args) + + # Validate input (must have 0 or 1 input) + for element in args: + if element not in (S.Zero, S.One): + raise ValueError( + "Qubit values must be 0 or 1, got: %r" % element) + return args + + @classmethod + def _eval_hilbert_space(cls, args): + return ComplexSpace(2)**len(args) + + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + + @property + def dimension(self): + """The number of Qubits in the state.""" + return len(self.qubit_values) + + @property + def nqubits(self): + return self.dimension + + @property + def qubit_values(self): + """Returns the values of the qubits as a tuple.""" + return self.label + + #------------------------------------------------------------------------- + # Special methods + #------------------------------------------------------------------------- + + def __len__(self): + return self.dimension + + def __getitem__(self, bit): + return self.qubit_values[int(self.dimension - bit - 1)] + + #------------------------------------------------------------------------- + # Utility methods + #------------------------------------------------------------------------- + + def flip(self, *bits): + """Flip the bit(s) given.""" + newargs = list(self.qubit_values) + for i in bits: + bit = int(self.dimension - i - 1) + if newargs[bit] == 1: + newargs[bit] = 0 + else: + newargs[bit] = 1 + return self.__class__(*tuple(newargs)) + + +class Qubit(QubitState, Ket): + """A multi-qubit ket in the computational (z) basis. + + We use the normal convention that the least significant qubit is on the + right, so ``|00001>`` has a 1 in the least significant qubit. + + Parameters + ========== + + values : list, str + The qubit values as a list of ints ([0,0,0,1,1,]) or a string ('011'). + + Examples + ======== + + Create a qubit in a couple of different ways and look at their attributes: + + >>> from sympy.physics.quantum.qubit import Qubit + >>> Qubit(0,0,0) + |000> + >>> q = Qubit('0101') + >>> q + |0101> + + >>> q.nqubits + 4 + >>> len(q) + 4 + >>> q.dimension + 4 + >>> q.qubit_values + (0, 1, 0, 1) + + We can flip the value of an individual qubit: + + >>> q.flip(1) + |0111> + + We can take the dagger of a Qubit to get a bra: + + >>> from sympy.physics.quantum.dagger import Dagger + >>> Dagger(q) + <0101| + >>> type(Dagger(q)) + + + Inner products work as expected: + + >>> ip = Dagger(q)*q + >>> ip + <0101|0101> + >>> ip.doit() + 1 + """ + + @classmethod + def dual_class(self): + return QubitBra + + def _eval_innerproduct_QubitBra(self, bra, **hints): + if self.label == bra.label: + return S.One + else: + return S.Zero + + def _represent_default_basis(self, **options): + return self._represent_ZGate(None, **options) + + def _represent_ZGate(self, basis, **options): + """Represent this qubits in the computational basis (ZGate). + """ + _format = options.get('format', 'sympy') + n = 1 + definite_state = 0 + for it in reversed(self.qubit_values): + definite_state += n*it + n = n*2 + result = [0]*(2**self.dimension) + result[int(definite_state)] = 1 + if _format == 'sympy': + return Matrix(result) + elif _format == 'numpy': + import numpy as np + return np.array(result, dtype='complex').transpose() + elif _format == 'scipy.sparse': + from scipy import sparse + return sparse.csr_matrix(result, dtype='complex').transpose() + + def _eval_trace(self, bra, **kwargs): + indices = kwargs.get('indices', []) + + #sort index list to begin trace from most-significant + #qubit + sorted_idx = list(indices) + if len(sorted_idx) == 0: + sorted_idx = list(range(0, self.nqubits)) + sorted_idx.sort() + + #trace out for each of index + new_mat = self*bra + for i in range(len(sorted_idx) - 1, -1, -1): + # start from tracing out from leftmost qubit + new_mat = self._reduced_density(new_mat, int(sorted_idx[i])) + + if (len(sorted_idx) == self.nqubits): + #in case full trace was requested + return new_mat[0] + else: + return matrix_to_density(new_mat) + + def _reduced_density(self, matrix, qubit, **options): + """Compute the reduced density matrix by tracing out one qubit. + The qubit argument should be of type Python int, since it is used + in bit operations + """ + def find_index_that_is_projected(j, k, qubit): + bit_mask = 2**qubit - 1 + return ((j >> qubit) << (1 + qubit)) + (j & bit_mask) + (k << qubit) + + old_matrix = represent(matrix, **options) + old_size = old_matrix.cols + #we expect the old_size to be even + new_size = old_size//2 + new_matrix = Matrix().zeros(new_size) + + for i in range(new_size): + for j in range(new_size): + for k in range(2): + col = find_index_that_is_projected(j, k, qubit) + row = find_index_that_is_projected(i, k, qubit) + new_matrix[i, j] += old_matrix[row, col] + + return new_matrix + + +class QubitBra(QubitState, Bra): + """A multi-qubit bra in the computational (z) basis. + + We use the normal convention that the least significant qubit is on the + right, so ``|00001>`` has a 1 in the least significant qubit. + + Parameters + ========== + + values : list, str + The qubit values as a list of ints ([0,0,0,1,1,]) or a string ('011'). + + See also + ======== + + Qubit: Examples using qubits + + """ + @classmethod + def dual_class(self): + return Qubit + + +class IntQubitState(QubitState): + """A base class for qubits that work with binary representations.""" + + @classmethod + def _eval_args(cls, args, nqubits=None): + # The case of a QubitState instance + if len(args) == 1 and isinstance(args[0], QubitState): + return QubitState._eval_args(args) + # otherwise, args should be integer + elif not all(isinstance(a, (int, Integer)) for a in args): + raise ValueError('values must be integers, got (%s)' % (tuple(type(a) for a in args),)) + # use nqubits if specified + if nqubits is not None: + if not isinstance(nqubits, (int, Integer)): + raise ValueError('nqubits must be an integer, got (%s)' % type(nqubits)) + if len(args) != 1: + raise ValueError( + 'too many positional arguments (%s). should be (number, nqubits=n)' % (args,)) + return cls._eval_args_with_nqubits(args[0], nqubits) + # For a single argument, we construct the binary representation of + # that integer with the minimal number of bits. + if len(args) == 1 and args[0] > 1: + #rvalues is the minimum number of bits needed to express the number + rvalues = reversed(range(bitcount(abs(args[0])))) + qubit_values = [(args[0] >> i) & 1 for i in rvalues] + return QubitState._eval_args(qubit_values) + # For two numbers, the second number is the number of bits + # on which it is expressed, so IntQubit(0,5) == |00000>. + elif len(args) == 2 and args[1] > 1: + return cls._eval_args_with_nqubits(args[0], args[1]) + else: + return QubitState._eval_args(args) + + @classmethod + def _eval_args_with_nqubits(cls, number, nqubits): + need = bitcount(abs(number)) + if nqubits < need: + raise ValueError( + 'cannot represent %s with %s bits' % (number, nqubits)) + qubit_values = [(number >> i) & 1 for i in reversed(range(nqubits))] + return QubitState._eval_args(qubit_values) + + def as_int(self): + """Return the numerical value of the qubit.""" + number = 0 + n = 1 + for i in reversed(self.qubit_values): + number += n*i + n = n << 1 + return number + + def _print_label(self, printer, *args): + return str(self.as_int()) + + def _print_label_pretty(self, printer, *args): + label = self._print_label(printer, *args) + return prettyForm(label) + + _print_label_repr = _print_label + _print_label_latex = _print_label + + +class IntQubit(IntQubitState, Qubit): + """A qubit ket that store integers as binary numbers in qubit values. + + The differences between this class and ``Qubit`` are: + + * The form of the constructor. + * The qubit values are printed as their corresponding integer, rather + than the raw qubit values. The internal storage format of the qubit + values in the same as ``Qubit``. + + Parameters + ========== + + values : int, tuple + If a single argument, the integer we want to represent in the qubit + values. This integer will be represented using the fewest possible + number of qubits. + If a pair of integers and the second value is more than one, the first + integer gives the integer to represent in binary form and the second + integer gives the number of qubits to use. + List of zeros and ones is also accepted to generate qubit by bit pattern. + + nqubits : int + The integer that represents the number of qubits. + This number should be passed with keyword ``nqubits=N``. + You can use this in order to avoid ambiguity of Qubit-style tuple of bits. + Please see the example below for more details. + + Examples + ======== + + Create a qubit for the integer 5: + + >>> from sympy.physics.quantum.qubit import IntQubit + >>> from sympy.physics.quantum.qubit import Qubit + >>> q = IntQubit(5) + >>> q + |5> + + We can also create an ``IntQubit`` by passing a ``Qubit`` instance. + + >>> q = IntQubit(Qubit('101')) + >>> q + |5> + >>> q.as_int() + 5 + >>> q.nqubits + 3 + >>> q.qubit_values + (1, 0, 1) + + We can go back to the regular qubit form. + + >>> Qubit(q) + |101> + + Please note that ``IntQubit`` also accepts a ``Qubit``-style list of bits. + So, the code below yields qubits 3, not a single bit ``1``. + + >>> IntQubit(1, 1) + |3> + + To avoid ambiguity, use ``nqubits`` parameter. + Use of this keyword is recommended especially when you provide the values by variables. + + >>> IntQubit(1, nqubits=1) + |1> + >>> a = 1 + >>> IntQubit(a, nqubits=1) + |1> + """ + @classmethod + def dual_class(self): + return IntQubitBra + + def _eval_innerproduct_IntQubitBra(self, bra, **hints): + return Qubit._eval_innerproduct_QubitBra(self, bra) + +class IntQubitBra(IntQubitState, QubitBra): + """A qubit bra that store integers as binary numbers in qubit values.""" + + @classmethod + def dual_class(self): + return IntQubit + + +#----------------------------------------------------------------------------- +# Qubit <---> Matrix conversion functions +#----------------------------------------------------------------------------- + + +def matrix_to_qubit(matrix): + """Convert from the matrix repr. to a sum of Qubit objects. + + Parameters + ---------- + matrix : Matrix, numpy.matrix, scipy.sparse + The matrix to build the Qubit representation of. This works with + SymPy matrices, numpy matrices and scipy.sparse sparse matrices. + + Examples + ======== + + Represent a state and then go back to its qubit form: + + >>> from sympy.physics.quantum.qubit import matrix_to_qubit, Qubit + >>> from sympy.physics.quantum.represent import represent + >>> q = Qubit('01') + >>> matrix_to_qubit(represent(q)) + |01> + """ + # Determine the format based on the type of the input matrix + format = 'sympy' + if isinstance(matrix, numpy_ndarray): + format = 'numpy' + if isinstance(matrix, scipy_sparse_matrix): + format = 'scipy.sparse' + + # Make sure it is of correct dimensions for a Qubit-matrix representation. + # This logic should work with sympy, numpy or scipy.sparse matrices. + if matrix.shape[0] == 1: + mlistlen = matrix.shape[1] + nqubits = log(mlistlen, 2) + ket = False + cls = QubitBra + elif matrix.shape[1] == 1: + mlistlen = matrix.shape[0] + nqubits = log(mlistlen, 2) + ket = True + cls = Qubit + else: + raise QuantumError( + 'Matrix must be a row/column vector, got %r' % matrix + ) + if not isinstance(nqubits, Integer): + raise QuantumError('Matrix must be a row/column vector of size ' + '2**nqubits, got: %r' % matrix) + # Go through each item in matrix, if element is non-zero, make it into a + # Qubit item times the element. + result = 0 + for i in range(mlistlen): + if ket: + element = matrix[i, 0] + else: + element = matrix[0, i] + if format in ('numpy', 'scipy.sparse'): + element = complex(element) + if element: + # Form Qubit array; 0 in bit-locations where i is 0, 1 in + # bit-locations where i is 1 + qubit_array = [int(i & (1 << x) != 0) for x in range(nqubits)] + qubit_array.reverse() + result = result + element*cls(*qubit_array) + + # If SymPy simplified by pulling out a constant coefficient, undo that. + if isinstance(result, (Mul, Add, Pow)): + result = result.expand() + + return result + + +def matrix_to_density(mat): + """ + Works by finding the eigenvectors and eigenvalues of the matrix. + We know we can decompose rho by doing: + sum(EigenVal*|Eigenvect>>> from sympy.physics.quantum.qubit import Qubit, measure_all + >>> from sympy.physics.quantum.gate import H + >>> from sympy.physics.quantum.qapply import qapply + + >>> c = H(0)*H(1)*Qubit('00') + >>> c + H(0)*H(1)*|00> + >>> q = qapply(c) + >>> measure_all(q) + [(|00>, 1/4), (|01>, 1/4), (|10>, 1/4), (|11>, 1/4)] + """ + m = qubit_to_matrix(qubit, format) + + if format == 'sympy': + results = [] + + if normalize: + m = m.normalized() + + size = max(m.shape) # Max of shape to account for bra or ket + nqubits = int(math.log(size)/math.log(2)) + for i in range(size): + if m[i]: + results.append( + (Qubit(IntQubit(i, nqubits=nqubits)), m[i]*conjugate(m[i])) + ) + return results + else: + raise NotImplementedError( + "This function cannot handle non-SymPy matrix formats yet" + ) + + +def measure_partial(qubit, bits, format='sympy', normalize=True): + """Perform a partial ensemble measure on the specified qubits. + + Parameters + ========== + + qubits : Qubit + The qubit to measure. This can be any Qubit or a linear combination + of them. + bits : tuple + The qubits to measure. + format : str + The format of the intermediate matrices to use. Possible values are + ('sympy','numpy','scipy.sparse'). Currently only 'sympy' is + implemented. + + Returns + ======= + + result : list + A list that consists of primitive states and their probabilities. + + Examples + ======== + + >>> from sympy.physics.quantum.qubit import Qubit, measure_partial + >>> from sympy.physics.quantum.gate import H + >>> from sympy.physics.quantum.qapply import qapply + + >>> c = H(0)*H(1)*Qubit('00') + >>> c + H(0)*H(1)*|00> + >>> q = qapply(c) + >>> measure_partial(q, (0,)) + [(sqrt(2)*|00>/2 + sqrt(2)*|10>/2, 1/2), (sqrt(2)*|01>/2 + sqrt(2)*|11>/2, 1/2)] + """ + m = qubit_to_matrix(qubit, format) + + if isinstance(bits, (SYMPY_INTS, Integer)): + bits = (int(bits),) + + if format == 'sympy': + if normalize: + m = m.normalized() + + possible_outcomes = _get_possible_outcomes(m, bits) + + # Form output from function. + output = [] + for outcome in possible_outcomes: + # Calculate probability of finding the specified bits with + # given values. + prob_of_outcome = 0 + prob_of_outcome += (outcome.H*outcome)[0] + + # If the output has a chance, append it to output with found + # probability. + if prob_of_outcome != 0: + if normalize: + next_matrix = matrix_to_qubit(outcome.normalized()) + else: + next_matrix = matrix_to_qubit(outcome) + + output.append(( + next_matrix, + prob_of_outcome + )) + + return output + else: + raise NotImplementedError( + "This function cannot handle non-SymPy matrix formats yet" + ) + + +def measure_partial_oneshot(qubit, bits, format='sympy'): + """Perform a partial oneshot measurement on the specified qubits. + + A oneshot measurement is equivalent to performing a measurement on a + quantum system. This type of measurement does not return the probabilities + like an ensemble measurement does, but rather returns *one* of the + possible resulting states. The exact state that is returned is determined + by picking a state randomly according to the ensemble probabilities. + + Parameters + ---------- + qubits : Qubit + The qubit to measure. This can be any Qubit or a linear combination + of them. + bits : tuple + The qubits to measure. + format : str + The format of the intermediate matrices to use. Possible values are + ('sympy','numpy','scipy.sparse'). Currently only 'sympy' is + implemented. + + Returns + ------- + result : Qubit + The qubit that the system collapsed to upon measurement. + """ + import random + m = qubit_to_matrix(qubit, format) + + if format == 'sympy': + m = m.normalized() + possible_outcomes = _get_possible_outcomes(m, bits) + + # Form output from function + random_number = random.random() + total_prob = 0 + for outcome in possible_outcomes: + # Calculate probability of finding the specified bits + # with given values + total_prob += (outcome.H*outcome)[0] + if total_prob >= random_number: + return matrix_to_qubit(outcome.normalized()) + else: + raise NotImplementedError( + "This function cannot handle non-SymPy matrix formats yet" + ) + + +def _get_possible_outcomes(m, bits): + """Get the possible states that can be produced in a measurement. + + Parameters + ---------- + m : Matrix + The matrix representing the state of the system. + bits : tuple, list + Which bits will be measured. + + Returns + ------- + result : list + The list of possible states which can occur given this measurement. + These are un-normalized so we can derive the probability of finding + this state by taking the inner product with itself + """ + + # This is filled with loads of dirty binary tricks...You have been warned + + size = max(m.shape) # Max of shape to account for bra or ket + nqubits = int(math.log2(size) + .1) # Number of qubits possible + + # Make the output states and put in output_matrices, nothing in them now. + # Each state will represent a possible outcome of the measurement + # Thus, output_matrices[0] is the matrix which we get when all measured + # bits return 0. and output_matrices[1] is the matrix for only the 0th + # bit being true + output_matrices = [] + for i in range(1 << len(bits)): + output_matrices.append(zeros(2**nqubits, 1)) + + # Bitmasks will help sort how to determine possible outcomes. + # When the bit mask is and-ed with a matrix-index, + # it will determine which state that index belongs to + bit_masks = [] + for bit in bits: + bit_masks.append(1 << bit) + + # Make possible outcome states + for i in range(2**nqubits): + trueness = 0 # This tells us to which output_matrix this value belongs + # Find trueness + for j in range(len(bit_masks)): + if i & bit_masks[j]: + trueness += j + 1 + # Put the value in the correct output matrix + output_matrices[trueness][i] = m[i] + return output_matrices + + +def measure_all_oneshot(qubit, format='sympy'): + """Perform a oneshot ensemble measurement on all qubits. + + A oneshot measurement is equivalent to performing a measurement on a + quantum system. This type of measurement does not return the probabilities + like an ensemble measurement does, but rather returns *one* of the + possible resulting states. The exact state that is returned is determined + by picking a state randomly according to the ensemble probabilities. + + Parameters + ---------- + qubits : Qubit + The qubit to measure. This can be any Qubit or a linear combination + of them. + format : str + The format of the intermediate matrices to use. Possible values are + ('sympy','numpy','scipy.sparse'). Currently only 'sympy' is + implemented. + + Returns + ------- + result : Qubit + The qubit that the system collapsed to upon measurement. + """ + import random + m = qubit_to_matrix(qubit) + + if format == 'sympy': + m = m.normalized() + random_number = random.random() + total = 0 + result = 0 + for i in m: + total += i*i.conjugate() + if total > random_number: + break + result += 1 + return Qubit(IntQubit(result, nqubits=int(math.log2(max(m.shape)) + .1))) + else: + raise NotImplementedError( + "This function cannot handle non-SymPy matrix formats yet" + ) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/represent.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/represent.py new file mode 100644 index 0000000000000000000000000000000000000000..3a1ada80aa6a3dd2caad43ec132fb9a148947106 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/represent.py @@ -0,0 +1,574 @@ +"""Logic for representing operators in state in various bases. + +TODO: + +* Get represent working with continuous hilbert spaces. +* Document default basis functionality. +""" + +from sympy.core.add import Add +from sympy.core.expr import Expr +from sympy.core.mul import Mul +from sympy.core.numbers import I +from sympy.core.power import Pow +from sympy.integrals.integrals import integrate +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.commutator import Commutator +from sympy.physics.quantum.anticommutator import AntiCommutator +from sympy.physics.quantum.innerproduct import InnerProduct +from sympy.physics.quantum.qexpr import QExpr +from sympy.physics.quantum.tensorproduct import TensorProduct +from sympy.physics.quantum.matrixutils import flatten_scalar +from sympy.physics.quantum.state import KetBase, BraBase, StateBase +from sympy.physics.quantum.operator import Operator, OuterProduct +from sympy.physics.quantum.qapply import qapply +from sympy.physics.quantum.operatorset import operators_to_state, state_to_operators + + +__all__ = [ + 'represent', + 'rep_innerproduct', + 'rep_expectation', + 'integrate_result', + 'get_basis', + 'enumerate_states' +] + +#----------------------------------------------------------------------------- +# Represent +#----------------------------------------------------------------------------- + + +def _sympy_to_scalar(e): + """Convert from a SymPy scalar to a Python scalar.""" + if isinstance(e, Expr): + if e.is_Integer: + return int(e) + elif e.is_Float: + return float(e) + elif e.is_Rational: + return float(e) + elif e.is_Number or e.is_NumberSymbol or e == I: + return complex(e) + raise TypeError('Expected number, got: %r' % e) + + +def represent(expr, **options): + """Represent the quantum expression in the given basis. + + In quantum mechanics abstract states and operators can be represented in + various basis sets. Under this operation the follow transforms happen: + + * Ket -> column vector or function + * Bra -> row vector of function + * Operator -> matrix or differential operator + + This function is the top-level interface for this action. + + This function walks the SymPy expression tree looking for ``QExpr`` + instances that have a ``_represent`` method. This method is then called + and the object is replaced by the representation returned by this method. + By default, the ``_represent`` method will dispatch to other methods + that handle the representation logic for a particular basis set. The + naming convention for these methods is the following:: + + def _represent_FooBasis(self, e, basis, **options) + + This function will have the logic for representing instances of its class + in the basis set having a class named ``FooBasis``. + + Parameters + ========== + + expr : Expr + The expression to represent. + basis : Operator, basis set + An object that contains the information about the basis set. If an + operator is used, the basis is assumed to be the orthonormal + eigenvectors of that operator. In general though, the basis argument + can be any object that contains the basis set information. + options : dict + Key/value pairs of options that are passed to the underlying method + that finds the representation. These options can be used to + control how the representation is done. For example, this is where + the size of the basis set would be set. + + Returns + ======= + + e : Expr + The SymPy expression of the represented quantum expression. + + Examples + ======== + + Here we subclass ``Operator`` and ``Ket`` to create the z-spin operator + and its spin 1/2 up eigenstate. By defining the ``_represent_SzOp`` + method, the ket can be represented in the z-spin basis. + + >>> from sympy.physics.quantum import Operator, represent, Ket + >>> from sympy import Matrix + + >>> class SzUpKet(Ket): + ... def _represent_SzOp(self, basis, **options): + ... return Matrix([1,0]) + ... + >>> class SzOp(Operator): + ... pass + ... + >>> sz = SzOp('Sz') + >>> up = SzUpKet('up') + >>> represent(up, basis=sz) + Matrix([ + [1], + [0]]) + + Here we see an example of representations in a continuous + basis. We see that the result of representing various combinations + of cartesian position operators and kets give us continuous + expressions involving DiracDelta functions. + + >>> from sympy.physics.quantum.cartesian import XOp, XKet, XBra + >>> X = XOp() + >>> x = XKet() + >>> y = XBra('y') + >>> represent(X*x) + x*DiracDelta(x - x_2) + """ + + format = options.get('format', 'sympy') + if format == 'numpy': + import numpy as np + if isinstance(expr, QExpr) and not isinstance(expr, OuterProduct): + options['replace_none'] = False + temp_basis = get_basis(expr, **options) + if temp_basis is not None: + options['basis'] = temp_basis + try: + return expr._represent(**options) + except NotImplementedError as strerr: + #If no _represent_FOO method exists, map to the + #appropriate basis state and try + #the other methods of representation + options['replace_none'] = True + + if isinstance(expr, (KetBase, BraBase)): + try: + return rep_innerproduct(expr, **options) + except NotImplementedError: + raise NotImplementedError(strerr) + elif isinstance(expr, Operator): + try: + return rep_expectation(expr, **options) + except NotImplementedError: + raise NotImplementedError(strerr) + else: + raise NotImplementedError(strerr) + elif isinstance(expr, Add): + result = represent(expr.args[0], **options) + for args in expr.args[1:]: + # scipy.sparse doesn't support += so we use plain = here. + result = result + represent(args, **options) + return result + elif isinstance(expr, Pow): + base, exp = expr.as_base_exp() + if format in ('numpy', 'scipy.sparse'): + exp = _sympy_to_scalar(exp) + base = represent(base, **options) + # scipy.sparse doesn't support negative exponents + # and warns when inverting a matrix in csr format. + if format == 'scipy.sparse' and exp < 0: + from scipy.sparse.linalg import inv + exp = - exp + base = inv(base.tocsc()).tocsr() + if format == 'numpy': + return np.linalg.matrix_power(base, exp) + return base ** exp + elif isinstance(expr, TensorProduct): + new_args = [represent(arg, **options) for arg in expr.args] + return TensorProduct(*new_args) + elif isinstance(expr, Dagger): + return Dagger(represent(expr.args[0], **options)) + elif isinstance(expr, Commutator): + A = expr.args[0] + B = expr.args[1] + return represent(Mul(A, B) - Mul(B, A), **options) + elif isinstance(expr, AntiCommutator): + A = expr.args[0] + B = expr.args[1] + return represent(Mul(A, B) + Mul(B, A), **options) + elif not isinstance(expr, (Mul, OuterProduct, InnerProduct)): + # We have removed special handling of inner products that used to be + # required (before automatic transforms). + # For numpy and scipy.sparse, we can only handle numerical prefactors. + if format in ('numpy', 'scipy.sparse'): + return _sympy_to_scalar(expr) + return expr + + if not isinstance(expr, (Mul, OuterProduct, InnerProduct)): + raise TypeError('Mul expected, got: %r' % expr) + + if "index" in options: + options["index"] += 1 + else: + options["index"] = 1 + + if "unities" not in options: + options["unities"] = [] + + result = represent(expr.args[-1], **options) + last_arg = expr.args[-1] + + for arg in reversed(expr.args[:-1]): + if isinstance(last_arg, Operator): + options["index"] += 1 + options["unities"].append(options["index"]) + elif isinstance(last_arg, BraBase) and isinstance(arg, KetBase): + options["index"] += 1 + elif isinstance(last_arg, KetBase) and isinstance(arg, Operator): + options["unities"].append(options["index"]) + elif isinstance(last_arg, KetBase) and isinstance(arg, BraBase): + options["unities"].append(options["index"]) + + next_arg = represent(arg, **options) + if format == 'numpy' and isinstance(next_arg, np.ndarray): + # Must use np.matmult to "matrix multiply" two np.ndarray + result = np.matmul(next_arg, result) + else: + result = next_arg*result + last_arg = arg + + # All three matrix formats create 1 by 1 matrices when inner products of + # vectors are taken. In these cases, we simply return a scalar. + result = flatten_scalar(result) + + result = integrate_result(expr, result, **options) + + return result + + +def rep_innerproduct(expr, **options): + """ + Returns an innerproduct like representation (e.g. ````) for the + given state. + + Attempts to calculate inner product with a bra from the specified + basis. Should only be passed an instance of KetBase or BraBase + + Parameters + ========== + + expr : KetBase or BraBase + The expression to be represented + + Examples + ======== + + >>> from sympy.physics.quantum.represent import rep_innerproduct + >>> from sympy.physics.quantum.cartesian import XOp, XKet, PxOp, PxKet + >>> rep_innerproduct(XKet()) + DiracDelta(x - x_1) + >>> rep_innerproduct(XKet(), basis=PxOp()) + sqrt(2)*exp(-I*px_1*x/hbar)/(2*sqrt(hbar)*sqrt(pi)) + >>> rep_innerproduct(PxKet(), basis=XOp()) + sqrt(2)*exp(I*px*x_1/hbar)/(2*sqrt(hbar)*sqrt(pi)) + + """ + + if not isinstance(expr, (KetBase, BraBase)): + raise TypeError("expr passed is not a Bra or Ket") + + basis = get_basis(expr, **options) + + if not isinstance(basis, StateBase): + raise NotImplementedError("Can't form this representation!") + + if "index" not in options: + options["index"] = 1 + + basis_kets = enumerate_states(basis, options["index"], 2) + + if isinstance(expr, BraBase): + bra = expr + ket = (basis_kets[1] if basis_kets[0].dual == expr else basis_kets[0]) + else: + bra = (basis_kets[1].dual if basis_kets[0] + == expr else basis_kets[0].dual) + ket = expr + + prod = InnerProduct(bra, ket) + result = prod.doit() + + format = options.get('format', 'sympy') + result = expr._format_represent(result, format) + return result + + +def rep_expectation(expr, **options): + """ + Returns an ```` type representation for the given operator. + + Parameters + ========== + + expr : Operator + Operator to be represented in the specified basis + + Examples + ======== + + >>> from sympy.physics.quantum.cartesian import XOp, PxOp, PxKet + >>> from sympy.physics.quantum.represent import rep_expectation + >>> rep_expectation(XOp()) + x_1*DiracDelta(x_1 - x_2) + >>> rep_expectation(XOp(), basis=PxOp()) + + >>> rep_expectation(XOp(), basis=PxKet()) + + + """ + + if "index" not in options: + options["index"] = 1 + + if not isinstance(expr, Operator): + raise TypeError("The passed expression is not an operator") + + basis_state = get_basis(expr, **options) + + if basis_state is None or not isinstance(basis_state, StateBase): + raise NotImplementedError("Could not get basis kets for this operator") + + basis_kets = enumerate_states(basis_state, options["index"], 2) + + bra = basis_kets[1].dual + ket = basis_kets[0] + + result = qapply(bra*expr*ket) + return result + + +def integrate_result(orig_expr, result, **options): + """ + Returns the result of integrating over any unities ``(|x>>> from sympy import symbols, DiracDelta + >>> from sympy.physics.quantum.represent import integrate_result + >>> from sympy.physics.quantum.cartesian import XOp, XKet + >>> x_ket = XKet() + >>> X_op = XOp() + >>> x, x_1, x_2 = symbols('x, x_1, x_2') + >>> integrate_result(X_op*x_ket, x*DiracDelta(x-x_1)*DiracDelta(x_1-x_2)) + x*DiracDelta(x - x_1)*DiracDelta(x_1 - x_2) + >>> integrate_result(X_op*x_ket, x*DiracDelta(x-x_1)*DiracDelta(x_1-x_2), + ... unities=[1]) + x*DiracDelta(x - x_2) + + """ + if not isinstance(result, Expr): + return result + + options['replace_none'] = True + if "basis" not in options: + arg = orig_expr.args[-1] + options["basis"] = get_basis(arg, **options) + elif not isinstance(options["basis"], StateBase): + options["basis"] = get_basis(orig_expr, **options) + + basis = options.pop("basis", None) + + if basis is None: + return result + + unities = options.pop("unities", []) + + if len(unities) == 0: + return result + + kets = enumerate_states(basis, unities) + coords = [k.label[0] for k in kets] + + for coord in coords: + if coord in result.free_symbols: + #TODO: Add support for sets of operators + basis_op = state_to_operators(basis) + start = basis_op.hilbert_space.interval.start + end = basis_op.hilbert_space.interval.end + result = integrate(result, (coord, start, end)) + + return result + + +def get_basis(expr, *, basis=None, replace_none=True, **options): + """ + Returns a basis state instance corresponding to the basis specified in + options=s. If no basis is specified, the function tries to form a default + basis state of the given expression. + + There are three behaviors: + + 1. The basis specified in options is already an instance of StateBase. If + this is the case, it is simply returned. If the class is specified but + not an instance, a default instance is returned. + + 2. The basis specified is an operator or set of operators. If this + is the case, the operator_to_state mapping method is used. + + 3. No basis is specified. If expr is a state, then a default instance of + its class is returned. If expr is an operator, then it is mapped to the + corresponding state. If it is neither, then we cannot obtain the basis + state. + + If the basis cannot be mapped, then it is not changed. + + This will be called from within represent, and represent will + only pass QExpr's. + + TODO (?): Support for Muls and other types of expressions? + + Parameters + ========== + + expr : Operator or StateBase + Expression whose basis is sought + + Examples + ======== + + >>> from sympy.physics.quantum.represent import get_basis + >>> from sympy.physics.quantum.cartesian import XOp, XKet, PxOp, PxKet + >>> x = XKet() + >>> X = XOp() + >>> get_basis(x) + |x> + >>> get_basis(X) + |x> + >>> get_basis(x, basis=PxOp()) + |px> + >>> get_basis(x, basis=PxKet) + |px> + + """ + + if basis is None and not replace_none: + return None + + if basis is None: + if isinstance(expr, KetBase): + return _make_default(expr.__class__) + elif isinstance(expr, BraBase): + return _make_default(expr.dual_class()) + elif isinstance(expr, Operator): + state_inst = operators_to_state(expr) + return (state_inst if state_inst is not None else None) + else: + return None + elif (isinstance(basis, Operator) or + (not isinstance(basis, StateBase) and issubclass(basis, Operator))): + state = operators_to_state(basis) + if state is None: + return None + elif isinstance(state, StateBase): + return state + else: + return _make_default(state) + elif isinstance(basis, StateBase): + return basis + elif issubclass(basis, StateBase): + return _make_default(basis) + else: + return None + + +def _make_default(expr): + # XXX: Catching TypeError like this is a bad way of distinguishing + # instances from classes. The logic using this function should be + # rewritten somehow. + try: + expr = expr() + except TypeError: + return expr + + return expr + + +def enumerate_states(*args, **options): + """ + Returns instances of the given state with dummy indices appended + + Operates in two different modes: + + 1. Two arguments are passed to it. The first is the base state which is to + be indexed, and the second argument is a list of indices to append. + + 2. Three arguments are passed. The first is again the base state to be + indexed. The second is the start index for counting. The final argument + is the number of kets you wish to receive. + + Tries to call state._enumerate_state. If this fails, returns an empty list + + Parameters + ========== + + args : list + See list of operation modes above for explanation + + Examples + ======== + + >>> from sympy.physics.quantum.cartesian import XBra, XKet + >>> from sympy.physics.quantum.represent import enumerate_states + >>> test = XKet('foo') + >>> enumerate_states(test, 1, 3) + [|foo_1>, |foo_2>, |foo_3>] + >>> test2 = XBra('bar') + >>> enumerate_states(test2, [4, 5, 10]) + [>> from sympy.physics.quantum.sho1d import RaisingOp + >>> from sympy.physics.quantum import Dagger + + >>> ad = RaisingOp('a') + >>> ad.rewrite('xp').doit() + sqrt(2)*(m*omega*X - I*Px)/(2*sqrt(hbar)*sqrt(m*omega)) + + >>> Dagger(ad) + a + + Taking the commutator of a^dagger with other Operators: + + >>> from sympy.physics.quantum import Commutator + >>> from sympy.physics.quantum.sho1d import RaisingOp, LoweringOp + >>> from sympy.physics.quantum.sho1d import NumberOp + + >>> ad = RaisingOp('a') + >>> a = LoweringOp('a') + >>> N = NumberOp('N') + >>> Commutator(ad, a).doit() + -1 + >>> Commutator(ad, N).doit() + -RaisingOp(a) + + Apply a^dagger to a state: + + >>> from sympy.physics.quantum import qapply + >>> from sympy.physics.quantum.sho1d import RaisingOp, SHOKet + + >>> ad = RaisingOp('a') + >>> k = SHOKet('k') + >>> qapply(ad*k) + sqrt(k + 1)*|k + 1> + + Matrix Representation + + >>> from sympy.physics.quantum.sho1d import RaisingOp + >>> from sympy.physics.quantum.represent import represent + >>> ad = RaisingOp('a') + >>> represent(ad, basis=N, ndim=4, format='sympy') + Matrix([ + [0, 0, 0, 0], + [1, 0, 0, 0], + [0, sqrt(2), 0, 0], + [0, 0, sqrt(3), 0]]) + + """ + + def _eval_rewrite_as_xp(self, *args, **kwargs): + return (S.One/sqrt(Integer(2)*hbar*m*omega))*( + S.NegativeOne*I*Px + m*omega*X) + + def _eval_adjoint(self): + return LoweringOp(*self.args) + + def _eval_commutator_LoweringOp(self, other): + return S.NegativeOne + + def _eval_commutator_NumberOp(self, other): + return S.NegativeOne*self + + def _apply_operator_SHOKet(self, ket, **options): + temp = ket.n + S.One + return sqrt(temp)*SHOKet(temp) + + def _represent_default_basis(self, **options): + return self._represent_NumberOp(None, **options) + + def _represent_XOp(self, basis, **options): + # This logic is good but the underlying position + # representation logic is broken. + # temp = self.rewrite('xp').doit() + # result = represent(temp, basis=X) + # return result + raise NotImplementedError('Position representation is not implemented') + + def _represent_NumberOp(self, basis, **options): + ndim_info = options.get('ndim', 4) + format = options.get('format','sympy') + matrix = matrix_zeros(ndim_info, ndim_info, **options) + for i in range(ndim_info - 1): + value = sqrt(i + 1) + if format == 'scipy.sparse': + value = float(value) + matrix[i + 1, i] = value + if format == 'scipy.sparse': + matrix = matrix.tocsr() + return matrix + + #-------------------------------------------------------------------------- + # Printing Methods + #-------------------------------------------------------------------------- + + def _print_contents(self, printer, *args): + arg0 = printer._print(self.args[0], *args) + return '%s(%s)' % (self.__class__.__name__, arg0) + + def _print_contents_pretty(self, printer, *args): + from sympy.printing.pretty.stringpict import prettyForm + pform = printer._print(self.args[0], *args) + pform = pform**prettyForm('\N{DAGGER}') + return pform + + def _print_contents_latex(self, printer, *args): + arg = printer._print(self.args[0]) + return '%s^{\\dagger}' % arg + +class LoweringOp(SHOOp): + """The Lowering Operator or 'a'. + + When 'a' acts on a state it lowers the state up by one. Taking + the adjoint of 'a' returns a^dagger, the Raising Operator. 'a' + can be rewritten in terms of position and momentum. We can + represent 'a' as a matrix, which will be its default basis. + + Parameters + ========== + + args : tuple + The list of numbers or parameters that uniquely specify the + operator. + + Examples + ======== + + Create a Lowering Operator and rewrite it in terms of position and + momentum, and show that taking its adjoint returns a^dagger: + + >>> from sympy.physics.quantum.sho1d import LoweringOp + >>> from sympy.physics.quantum import Dagger + + >>> a = LoweringOp('a') + >>> a.rewrite('xp').doit() + sqrt(2)*(m*omega*X + I*Px)/(2*sqrt(hbar)*sqrt(m*omega)) + + >>> Dagger(a) + RaisingOp(a) + + Taking the commutator of 'a' with other Operators: + + >>> from sympy.physics.quantum import Commutator + >>> from sympy.physics.quantum.sho1d import LoweringOp, RaisingOp + >>> from sympy.physics.quantum.sho1d import NumberOp + + >>> a = LoweringOp('a') + >>> ad = RaisingOp('a') + >>> N = NumberOp('N') + >>> Commutator(a, ad).doit() + 1 + >>> Commutator(a, N).doit() + a + + Apply 'a' to a state: + + >>> from sympy.physics.quantum import qapply + >>> from sympy.physics.quantum.sho1d import LoweringOp, SHOKet + + >>> a = LoweringOp('a') + >>> k = SHOKet('k') + >>> qapply(a*k) + sqrt(k)*|k - 1> + + Taking 'a' of the lowest state will return 0: + + >>> from sympy.physics.quantum import qapply + >>> from sympy.physics.quantum.sho1d import LoweringOp, SHOKet + + >>> a = LoweringOp('a') + >>> k = SHOKet(0) + >>> qapply(a*k) + 0 + + Matrix Representation + + >>> from sympy.physics.quantum.sho1d import LoweringOp + >>> from sympy.physics.quantum.represent import represent + >>> a = LoweringOp('a') + >>> represent(a, basis=N, ndim=4, format='sympy') + Matrix([ + [0, 1, 0, 0], + [0, 0, sqrt(2), 0], + [0, 0, 0, sqrt(3)], + [0, 0, 0, 0]]) + + """ + + def _eval_rewrite_as_xp(self, *args, **kwargs): + return (S.One/sqrt(Integer(2)*hbar*m*omega))*( + I*Px + m*omega*X) + + def _eval_adjoint(self): + return RaisingOp(*self.args) + + def _eval_commutator_RaisingOp(self, other): + return S.One + + def _eval_commutator_NumberOp(self, other): + return self + + def _apply_operator_SHOKet(self, ket, **options): + temp = ket.n - Integer(1) + if ket.n is S.Zero: + return S.Zero + else: + return sqrt(ket.n)*SHOKet(temp) + + def _represent_default_basis(self, **options): + return self._represent_NumberOp(None, **options) + + def _represent_XOp(self, basis, **options): + # This logic is good but the underlying position + # representation logic is broken. + # temp = self.rewrite('xp').doit() + # result = represent(temp, basis=X) + # return result + raise NotImplementedError('Position representation is not implemented') + + def _represent_NumberOp(self, basis, **options): + ndim_info = options.get('ndim', 4) + format = options.get('format', 'sympy') + matrix = matrix_zeros(ndim_info, ndim_info, **options) + for i in range(ndim_info - 1): + value = sqrt(i + 1) + if format == 'scipy.sparse': + value = float(value) + matrix[i,i + 1] = value + if format == 'scipy.sparse': + matrix = matrix.tocsr() + return matrix + + +class NumberOp(SHOOp): + """The Number Operator is simply a^dagger*a + + It is often useful to write a^dagger*a as simply the Number Operator + because the Number Operator commutes with the Hamiltonian. And can be + expressed using the Number Operator. Also the Number Operator can be + applied to states. We can represent the Number Operator as a matrix, + which will be its default basis. + + Parameters + ========== + + args : tuple + The list of numbers or parameters that uniquely specify the + operator. + + Examples + ======== + + Create a Number Operator and rewrite it in terms of the ladder + operators, position and momentum operators, and Hamiltonian: + + >>> from sympy.physics.quantum.sho1d import NumberOp + + >>> N = NumberOp('N') + >>> N.rewrite('a').doit() + RaisingOp(a)*a + >>> N.rewrite('xp').doit() + -1/2 + (m**2*omega**2*X**2 + Px**2)/(2*hbar*m*omega) + >>> N.rewrite('H').doit() + -1/2 + H/(hbar*omega) + + Take the Commutator of the Number Operator with other Operators: + + >>> from sympy.physics.quantum import Commutator + >>> from sympy.physics.quantum.sho1d import NumberOp, Hamiltonian + >>> from sympy.physics.quantum.sho1d import RaisingOp, LoweringOp + + >>> N = NumberOp('N') + >>> H = Hamiltonian('H') + >>> ad = RaisingOp('a') + >>> a = LoweringOp('a') + >>> Commutator(N,H).doit() + 0 + >>> Commutator(N,ad).doit() + RaisingOp(a) + >>> Commutator(N,a).doit() + -a + + Apply the Number Operator to a state: + + >>> from sympy.physics.quantum import qapply + >>> from sympy.physics.quantum.sho1d import NumberOp, SHOKet + + >>> N = NumberOp('N') + >>> k = SHOKet('k') + >>> qapply(N*k) + k*|k> + + Matrix Representation + + >>> from sympy.physics.quantum.sho1d import NumberOp + >>> from sympy.physics.quantum.represent import represent + >>> N = NumberOp('N') + >>> represent(N, basis=N, ndim=4, format='sympy') + Matrix([ + [0, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 2, 0], + [0, 0, 0, 3]]) + + """ + + def _eval_rewrite_as_a(self, *args, **kwargs): + return ad*a + + def _eval_rewrite_as_xp(self, *args, **kwargs): + return (S.One/(Integer(2)*m*hbar*omega))*(Px**2 + ( + m*omega*X)**2) - S.Half + + def _eval_rewrite_as_H(self, *args, **kwargs): + return H/(hbar*omega) - S.Half + + def _apply_operator_SHOKet(self, ket, **options): + return ket.n*ket + + def _eval_commutator_Hamiltonian(self, other): + return S.Zero + + def _eval_commutator_RaisingOp(self, other): + return other + + def _eval_commutator_LoweringOp(self, other): + return S.NegativeOne*other + + def _represent_default_basis(self, **options): + return self._represent_NumberOp(None, **options) + + def _represent_XOp(self, basis, **options): + # This logic is good but the underlying position + # representation logic is broken. + # temp = self.rewrite('xp').doit() + # result = represent(temp, basis=X) + # return result + raise NotImplementedError('Position representation is not implemented') + + def _represent_NumberOp(self, basis, **options): + ndim_info = options.get('ndim', 4) + format = options.get('format', 'sympy') + matrix = matrix_zeros(ndim_info, ndim_info, **options) + for i in range(ndim_info): + value = i + if format == 'scipy.sparse': + value = float(value) + matrix[i,i] = value + if format == 'scipy.sparse': + matrix = matrix.tocsr() + return matrix + + +class Hamiltonian(SHOOp): + """The Hamiltonian Operator. + + The Hamiltonian is used to solve the time-independent Schrodinger + equation. The Hamiltonian can be expressed using the ladder operators, + as well as by position and momentum. We can represent the Hamiltonian + Operator as a matrix, which will be its default basis. + + Parameters + ========== + + args : tuple + The list of numbers or parameters that uniquely specify the + operator. + + Examples + ======== + + Create a Hamiltonian Operator and rewrite it in terms of the ladder + operators, position and momentum, and the Number Operator: + + >>> from sympy.physics.quantum.sho1d import Hamiltonian + + >>> H = Hamiltonian('H') + >>> H.rewrite('a').doit() + hbar*omega*(1/2 + RaisingOp(a)*a) + >>> H.rewrite('xp').doit() + (m**2*omega**2*X**2 + Px**2)/(2*m) + >>> H.rewrite('N').doit() + hbar*omega*(1/2 + N) + + Take the Commutator of the Hamiltonian and the Number Operator: + + >>> from sympy.physics.quantum import Commutator + >>> from sympy.physics.quantum.sho1d import Hamiltonian, NumberOp + + >>> H = Hamiltonian('H') + >>> N = NumberOp('N') + >>> Commutator(H,N).doit() + 0 + + Apply the Hamiltonian Operator to a state: + + >>> from sympy.physics.quantum import qapply + >>> from sympy.physics.quantum.sho1d import Hamiltonian, SHOKet + + >>> H = Hamiltonian('H') + >>> k = SHOKet('k') + >>> qapply(H*k) + hbar*k*omega*|k> + hbar*omega*|k>/2 + + Matrix Representation + + >>> from sympy.physics.quantum.sho1d import Hamiltonian + >>> from sympy.physics.quantum.represent import represent + + >>> H = Hamiltonian('H') + >>> represent(H, basis=N, ndim=4, format='sympy') + Matrix([ + [hbar*omega/2, 0, 0, 0], + [ 0, 3*hbar*omega/2, 0, 0], + [ 0, 0, 5*hbar*omega/2, 0], + [ 0, 0, 0, 7*hbar*omega/2]]) + + """ + + def _eval_rewrite_as_a(self, *args, **kwargs): + return hbar*omega*(ad*a + S.Half) + + def _eval_rewrite_as_xp(self, *args, **kwargs): + return (S.One/(Integer(2)*m))*(Px**2 + (m*omega*X)**2) + + def _eval_rewrite_as_N(self, *args, **kwargs): + return hbar*omega*(N + S.Half) + + def _apply_operator_SHOKet(self, ket, **options): + return (hbar*omega*(ket.n + S.Half))*ket + + def _eval_commutator_NumberOp(self, other): + return S.Zero + + def _represent_default_basis(self, **options): + return self._represent_NumberOp(None, **options) + + def _represent_XOp(self, basis, **options): + # This logic is good but the underlying position + # representation logic is broken. + # temp = self.rewrite('xp').doit() + # result = represent(temp, basis=X) + # return result + raise NotImplementedError('Position representation is not implemented') + + def _represent_NumberOp(self, basis, **options): + ndim_info = options.get('ndim', 4) + format = options.get('format', 'sympy') + matrix = matrix_zeros(ndim_info, ndim_info, **options) + for i in range(ndim_info): + value = i + S.Half + if format == 'scipy.sparse': + value = float(value) + matrix[i,i] = value + if format == 'scipy.sparse': + matrix = matrix.tocsr() + return hbar*omega*matrix + +#------------------------------------------------------------------------------ + +class SHOState(State): + """State class for SHO states""" + + @classmethod + def _eval_hilbert_space(cls, label): + return ComplexSpace(S.Infinity) + + @property + def n(self): + return self.args[0] + + +class SHOKet(SHOState, Ket): + """1D eigenket. + + Inherits from SHOState and Ket. + + Parameters + ========== + + args : tuple + The list of numbers or parameters that uniquely specify the ket + This is usually its quantum numbers or its symbol. + + Examples + ======== + + Ket's know about their associated bra: + + >>> from sympy.physics.quantum.sho1d import SHOKet + + >>> k = SHOKet('k') + >>> k.dual + >> k.dual_class() + + + Take the Inner Product with a bra: + + >>> from sympy.physics.quantum import InnerProduct + >>> from sympy.physics.quantum.sho1d import SHOKet, SHOBra + + >>> k = SHOKet('k') + >>> b = SHOBra('b') + >>> InnerProduct(b,k).doit() + KroneckerDelta(b, k) + + Vector representation of a numerical state ket: + + >>> from sympy.physics.quantum.sho1d import SHOKet, NumberOp + >>> from sympy.physics.quantum.represent import represent + + >>> k = SHOKet(3) + >>> N = NumberOp('N') + >>> represent(k, basis=N, ndim=4) + Matrix([ + [0], + [0], + [0], + [1]]) + + """ + + @classmethod + def dual_class(self): + return SHOBra + + def _eval_innerproduct_SHOBra(self, bra, **hints): + result = KroneckerDelta(self.n, bra.n) + return result + + def _represent_default_basis(self, **options): + return self._represent_NumberOp(None, **options) + + def _represent_NumberOp(self, basis, **options): + ndim_info = options.get('ndim', 4) + format = options.get('format', 'sympy') + options['spmatrix'] = 'lil' + vector = matrix_zeros(ndim_info, 1, **options) + if isinstance(self.n, Integer): + if self.n >= ndim_info: + return ValueError("N-Dimension too small") + if format == 'scipy.sparse': + vector[int(self.n), 0] = 1.0 + vector = vector.tocsr() + elif format == 'numpy': + vector[int(self.n), 0] = 1.0 + else: + vector[self.n, 0] = S.One + return vector + else: + return ValueError("Not Numerical State") + + +class SHOBra(SHOState, Bra): + """A time-independent Bra in SHO. + + Inherits from SHOState and Bra. + + Parameters + ========== + + args : tuple + The list of numbers or parameters that uniquely specify the ket + This is usually its quantum numbers or its symbol. + + Examples + ======== + + Bra's know about their associated ket: + + >>> from sympy.physics.quantum.sho1d import SHOBra + + >>> b = SHOBra('b') + >>> b.dual + |b> + >>> b.dual_class() + + + Vector representation of a numerical state bra: + + >>> from sympy.physics.quantum.sho1d import SHOBra, NumberOp + >>> from sympy.physics.quantum.represent import represent + + >>> b = SHOBra(3) + >>> N = NumberOp('N') + >>> represent(b, basis=N, ndim=4) + Matrix([[0, 0, 0, 1]]) + + """ + + @classmethod + def dual_class(self): + return SHOKet + + def _represent_default_basis(self, **options): + return self._represent_NumberOp(None, **options) + + def _represent_NumberOp(self, basis, **options): + ndim_info = options.get('ndim', 4) + format = options.get('format', 'sympy') + options['spmatrix'] = 'lil' + vector = matrix_zeros(1, ndim_info, **options) + if isinstance(self.n, Integer): + if self.n >= ndim_info: + return ValueError("N-Dimension too small") + if format == 'scipy.sparse': + vector[0, int(self.n)] = 1.0 + vector = vector.tocsr() + elif format == 'numpy': + vector[0, int(self.n)] = 1.0 + else: + vector[0, self.n] = S.One + return vector + else: + return ValueError("Not Numerical State") + + +ad = RaisingOp('a') +a = LoweringOp('a') +H = Hamiltonian('H') +N = NumberOp('N') +omega = Symbol('omega') +m = Symbol('m') diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/shor.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/shor.py new file mode 100644 index 0000000000000000000000000000000000000000..fc9e55229d74634bdb82efc03c2d1649e088efb3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/shor.py @@ -0,0 +1,173 @@ +"""Shor's algorithm and helper functions. + +Todo: + +* Get the CMod gate working again using the new Gate API. +* Fix everything. +* Update docstrings and reformat. +""" + +import math +import random + +from sympy.core.mul import Mul +from sympy.core.singleton import S +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.core.intfunc import igcd +from sympy.ntheory import continued_fraction_periodic as continued_fraction +from sympy.utilities.iterables import variations + +from sympy.physics.quantum.gate import Gate +from sympy.physics.quantum.qubit import Qubit, measure_partial_oneshot +from sympy.physics.quantum.qapply import qapply +from sympy.physics.quantum.qft import QFT +from sympy.physics.quantum.qexpr import QuantumError + + +class OrderFindingException(QuantumError): + pass + + +class CMod(Gate): + """A controlled mod gate. + + This is black box controlled Mod function for use by shor's algorithm. + TODO: implement a decompose property that returns how to do this in terms + of elementary gates + """ + + @classmethod + def _eval_args(cls, args): + # t = args[0] + # a = args[1] + # N = args[2] + raise NotImplementedError('The CMod gate has not been completed.') + + @property + def t(self): + """Size of 1/2 input register. First 1/2 holds output.""" + return self.label[0] + + @property + def a(self): + """Base of the controlled mod function.""" + return self.label[1] + + @property + def N(self): + """N is the type of modular arithmetic we are doing.""" + return self.label[2] + + def _apply_operator_Qubit(self, qubits, **options): + """ + This directly calculates the controlled mod of the second half of + the register and puts it in the second + This will look pretty when we get Tensor Symbolically working + """ + n = 1 + k = 0 + # Determine the value stored in high memory. + for i in range(self.t): + k += n*qubits[self.t + i] + n *= 2 + + # The value to go in low memory will be out. + out = int(self.a**k % self.N) + + # Create array for new qbit-ket which will have high memory unaffected + outarray = list(qubits.args[0][:self.t]) + + # Place out in low memory + for i in reversed(range(self.t)): + outarray.append((out >> i) & 1) + + return Qubit(*outarray) + + +def shor(N): + """This function implements Shor's factoring algorithm on the Integer N + + The algorithm starts by picking a random number (a) and seeing if it is + coprime with N. If it is not, then the gcd of the two numbers is a factor + and we are done. Otherwise, it begins the period_finding subroutine which + finds the period of a in modulo N arithmetic. This period, if even, can + be used to calculate factors by taking a**(r/2)-1 and a**(r/2)+1. + These values are returned. + """ + a = random.randrange(N - 2) + 2 + if igcd(N, a) != 1: + return igcd(N, a) + r = period_find(a, N) + if r % 2 == 1: + shor(N) + answer = (igcd(a**(r/2) - 1, N), igcd(a**(r/2) + 1, N)) + return answer + + +def getr(x, y, N): + fraction = continued_fraction(x, y) + # Now convert into r + total = ratioize(fraction, N) + return total + + +def ratioize(list, N): + if list[0] > N: + return S.Zero + if len(list) == 1: + return list[0] + return list[0] + ratioize(list[1:], N) + + +def period_find(a, N): + """Finds the period of a in modulo N arithmetic + + This is quantum part of Shor's algorithm. It takes two registers, + puts first in superposition of states with Hadamards so: ``|k>|0>`` + with k being all possible choices. It then does a controlled mod and + a QFT to determine the order of a. + """ + epsilon = .5 + # picks out t's such that maintains accuracy within epsilon + t = int(2*math.ceil(log(N, 2))) + # make the first half of register be 0's |000...000> + start = [0 for x in range(t)] + # Put second half into superposition of states so we have |1>x|0> + |2>x|0> + ... |k>x>|0> + ... + |2**n-1>x|0> + factor = 1/sqrt(2**t) + qubits = 0 + for arr in variations(range(2), t, repetition=True): + qbitArray = list(arr) + start + qubits = qubits + Qubit(*qbitArray) + circuit = (factor*qubits).expand() + # Controlled second half of register so that we have: + # |1>x|a**1 %N> + |2>x|a**2 %N> + ... + |k>x|a**k %N >+ ... + |2**n-1=k>x|a**k % n> + circuit = CMod(t, a, N)*circuit + # will measure first half of register giving one of the a**k%N's + + circuit = qapply(circuit) + for i in range(t): + circuit = measure_partial_oneshot(circuit, i) + # Now apply Inverse Quantum Fourier Transform on the second half of the register + + circuit = qapply(QFT(t, t*2).decompose()*circuit, floatingPoint=True) + for i in range(t): + circuit = measure_partial_oneshot(circuit, i + t) + if isinstance(circuit, Qubit): + register = circuit + elif isinstance(circuit, Mul): + register = circuit.args[-1] + else: + register = circuit.args[-1].args[-1] + + n = 1 + answer = 0 + for i in range(len(register)/2): + answer += n*register[i + t] + n = n << 1 + if answer == 0: + raise OrderFindingException( + "Order finder returned 0. Happens with chance %f" % epsilon) + #turn answer into r using continued fractions + g = getr(answer, 2**t, N) + return g diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/spin.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/spin.py new file mode 100644 index 0000000000000000000000000000000000000000..6be53d01711adbed8c078fffca1d618c1aa3c6e6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/spin.py @@ -0,0 +1,2150 @@ +"""Quantum mechanical angular momentum.""" + +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.containers import Tuple +from sympy.core.expr import Expr +from sympy.core.numbers import int_valued +from sympy.core.mul import Mul +from sympy.core.numbers import (I, Integer, Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, symbols) +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import (binomial, factorial) +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.simplify.simplify import simplify +from sympy.matrices import zeros +from sympy.printing.pretty.stringpict import prettyForm, stringPict +from sympy.printing.pretty.pretty_symbology import pretty_symbol + +from sympy.physics.quantum.qexpr import QExpr +from sympy.physics.quantum.operator import (HermitianOperator, Operator, + UnitaryOperator) +from sympy.physics.quantum.state import Bra, Ket, State +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.physics.quantum.constants import hbar +from sympy.physics.quantum.hilbert import ComplexSpace, DirectSumHilbertSpace +from sympy.physics.quantum.tensorproduct import TensorProduct +from sympy.physics.quantum.cg import CG +from sympy.physics.quantum.qapply import qapply + + +__all__ = [ + 'm_values', + 'Jplus', + 'Jminus', + 'Jx', + 'Jy', + 'Jz', + 'J2', + 'Rotation', + 'WignerD', + 'JxKet', + 'JxBra', + 'JyKet', + 'JyBra', + 'JzKet', + 'JzBra', + 'JzOp', + 'J2Op', + 'JxKetCoupled', + 'JxBraCoupled', + 'JyKetCoupled', + 'JyBraCoupled', + 'JzKetCoupled', + 'JzBraCoupled', + 'couple', + 'uncouple' +] + + +def m_values(j): + j = sympify(j) + size = 2*j + 1 + if not size.is_Integer or not size > 0: + raise ValueError( + 'Only integer or half-integer values allowed for j, got: : %r' % j + ) + return size, [j - i for i in range(int(2*j + 1))] + + +#----------------------------------------------------------------------------- +# Spin Operators +#----------------------------------------------------------------------------- + + +class SpinOpBase: + """Base class for spin operators.""" + + @classmethod + def _eval_hilbert_space(cls, label): + # We consider all j values so our space is infinite. + return ComplexSpace(S.Infinity) + + @property + def name(self): + return self.args[0] + + def _print_contents(self, printer, *args): + return '%s%s' % (self.name, self._coord) + + def _print_contents_pretty(self, printer, *args): + a = stringPict(str(self.name)) + b = stringPict(self._coord) + return self._print_subscript_pretty(a, b) + + def _print_contents_latex(self, printer, *args): + return r'%s_%s' % ((self.name, self._coord)) + + def _represent_base(self, basis, **options): + j = options.get('j', S.Half) + size, mvals = m_values(j) + result = zeros(size, size) + for p in range(size): + for q in range(size): + me = self.matrix_element(j, mvals[p], j, mvals[q]) + result[p, q] = me + return result + + def _apply_op(self, ket, orig_basis, **options): + state = ket.rewrite(self.basis) + # If the state has only one term + if isinstance(state, State): + ret = (hbar*state.m)*state + # state is a linear combination of states + elif isinstance(state, Sum): + ret = self._apply_operator_Sum(state, **options) + else: + ret = qapply(self*state) + if ret == self*state: + raise NotImplementedError + return ret.rewrite(orig_basis) + + def _apply_operator_JxKet(self, ket, **options): + return self._apply_op(ket, 'Jx', **options) + + def _apply_operator_JxKetCoupled(self, ket, **options): + return self._apply_op(ket, 'Jx', **options) + + def _apply_operator_JyKet(self, ket, **options): + return self._apply_op(ket, 'Jy', **options) + + def _apply_operator_JyKetCoupled(self, ket, **options): + return self._apply_op(ket, 'Jy', **options) + + def _apply_operator_JzKet(self, ket, **options): + return self._apply_op(ket, 'Jz', **options) + + def _apply_operator_JzKetCoupled(self, ket, **options): + return self._apply_op(ket, 'Jz', **options) + + def _apply_operator_TensorProduct(self, tp, **options): + # Uncoupling operator is only easily found for coordinate basis spin operators + # TODO: add methods for uncoupling operators + if not isinstance(self, (JxOp, JyOp, JzOp)): + raise NotImplementedError + result = [] + for n in range(len(tp.args)): + arg = [] + arg.extend(tp.args[:n]) + arg.append(self._apply_operator(tp.args[n])) + arg.extend(tp.args[n + 1:]) + result.append(tp.__class__(*arg)) + return Add(*result).expand() + + # TODO: move this to qapply_Mul + def _apply_operator_Sum(self, s, **options): + new_func = qapply(self*s.function) + if new_func == self*s.function: + raise NotImplementedError + return Sum(new_func, *s.limits) + + def _eval_trace(self, **options): + #TODO: use options to use different j values + #For now eval at default basis + + # is it efficient to represent each time + # to do a trace? + return self._represent_default_basis().trace() + + +class JplusOp(SpinOpBase, Operator): + """The J+ operator.""" + + _coord = '+' + + basis = 'Jz' + + def _eval_commutator_JminusOp(self, other): + return 2*hbar*JzOp(self.name) + + def _apply_operator_JzKet(self, ket, **options): + j = ket.j + m = ket.m + if m.is_Number and j.is_Number: + if m >= j: + return S.Zero + return hbar*sqrt(j*(j + S.One) - m*(m + S.One))*JzKet(j, m + S.One) + + def _apply_operator_JzKetCoupled(self, ket, **options): + j = ket.j + m = ket.m + jn = ket.jn + coupling = ket.coupling + if m.is_Number and j.is_Number: + if m >= j: + return S.Zero + return hbar*sqrt(j*(j + S.One) - m*(m + S.One))*JzKetCoupled(j, m + S.One, jn, coupling) + + def matrix_element(self, j, m, jp, mp): + result = hbar*sqrt(j*(j + S.One) - mp*(mp + S.One)) + result *= KroneckerDelta(m, mp + 1) + result *= KroneckerDelta(j, jp) + return result + + def _represent_default_basis(self, **options): + return self._represent_JzOp(None, **options) + + def _represent_JzOp(self, basis, **options): + return self._represent_base(basis, **options) + + def _eval_rewrite_as_xyz(self, *args, **kwargs): + return JxOp(args[0]) + I*JyOp(args[0]) + + +class JminusOp(SpinOpBase, Operator): + """The J- operator.""" + + _coord = '-' + + basis = 'Jz' + + def _apply_operator_JzKet(self, ket, **options): + j = ket.j + m = ket.m + if m.is_Number and j.is_Number: + if m <= -j: + return S.Zero + return hbar*sqrt(j*(j + S.One) - m*(m - S.One))*JzKet(j, m - S.One) + + def _apply_operator_JzKetCoupled(self, ket, **options): + j = ket.j + m = ket.m + jn = ket.jn + coupling = ket.coupling + if m.is_Number and j.is_Number: + if m <= -j: + return S.Zero + return hbar*sqrt(j*(j + S.One) - m*(m - S.One))*JzKetCoupled(j, m - S.One, jn, coupling) + + def matrix_element(self, j, m, jp, mp): + result = hbar*sqrt(j*(j + S.One) - mp*(mp - S.One)) + result *= KroneckerDelta(m, mp - 1) + result *= KroneckerDelta(j, jp) + return result + + def _represent_default_basis(self, **options): + return self._represent_JzOp(None, **options) + + def _represent_JzOp(self, basis, **options): + return self._represent_base(basis, **options) + + def _eval_rewrite_as_xyz(self, *args, **kwargs): + return JxOp(args[0]) - I*JyOp(args[0]) + + +class JxOp(SpinOpBase, HermitianOperator): + """The Jx operator.""" + + _coord = 'x' + + basis = 'Jx' + + def _eval_commutator_JyOp(self, other): + return I*hbar*JzOp(self.name) + + def _eval_commutator_JzOp(self, other): + return -I*hbar*JyOp(self.name) + + def _apply_operator_JzKet(self, ket, **options): + jp = JplusOp(self.name)._apply_operator_JzKet(ket, **options) + jm = JminusOp(self.name)._apply_operator_JzKet(ket, **options) + return (jp + jm)/Integer(2) + + def _apply_operator_JzKetCoupled(self, ket, **options): + jp = JplusOp(self.name)._apply_operator_JzKetCoupled(ket, **options) + jm = JminusOp(self.name)._apply_operator_JzKetCoupled(ket, **options) + return (jp + jm)/Integer(2) + + def _represent_default_basis(self, **options): + return self._represent_JzOp(None, **options) + + def _represent_JzOp(self, basis, **options): + jp = JplusOp(self.name)._represent_JzOp(basis, **options) + jm = JminusOp(self.name)._represent_JzOp(basis, **options) + return (jp + jm)/Integer(2) + + def _eval_rewrite_as_plusminus(self, *args, **kwargs): + return (JplusOp(args[0]) + JminusOp(args[0]))/2 + + +class JyOp(SpinOpBase, HermitianOperator): + """The Jy operator.""" + + _coord = 'y' + + basis = 'Jy' + + def _eval_commutator_JzOp(self, other): + return I*hbar*JxOp(self.name) + + def _eval_commutator_JxOp(self, other): + return -I*hbar*J2Op(self.name) + + def _apply_operator_JzKet(self, ket, **options): + jp = JplusOp(self.name)._apply_operator_JzKet(ket, **options) + jm = JminusOp(self.name)._apply_operator_JzKet(ket, **options) + return (jp - jm)/(Integer(2)*I) + + def _apply_operator_JzKetCoupled(self, ket, **options): + jp = JplusOp(self.name)._apply_operator_JzKetCoupled(ket, **options) + jm = JminusOp(self.name)._apply_operator_JzKetCoupled(ket, **options) + return (jp - jm)/(Integer(2)*I) + + def _represent_default_basis(self, **options): + return self._represent_JzOp(None, **options) + + def _represent_JzOp(self, basis, **options): + jp = JplusOp(self.name)._represent_JzOp(basis, **options) + jm = JminusOp(self.name)._represent_JzOp(basis, **options) + return (jp - jm)/(Integer(2)*I) + + def _eval_rewrite_as_plusminus(self, *args, **kwargs): + return (JplusOp(args[0]) - JminusOp(args[0]))/(2*I) + + +class JzOp(SpinOpBase, HermitianOperator): + """The Jz operator.""" + + _coord = 'z' + + basis = 'Jz' + + def _eval_commutator_JxOp(self, other): + return I*hbar*JyOp(self.name) + + def _eval_commutator_JyOp(self, other): + return -I*hbar*JxOp(self.name) + + def _eval_commutator_JplusOp(self, other): + return hbar*JplusOp(self.name) + + def _eval_commutator_JminusOp(self, other): + return -hbar*JminusOp(self.name) + + def matrix_element(self, j, m, jp, mp): + result = hbar*mp + result *= KroneckerDelta(m, mp) + result *= KroneckerDelta(j, jp) + return result + + def _represent_default_basis(self, **options): + return self._represent_JzOp(None, **options) + + def _represent_JzOp(self, basis, **options): + return self._represent_base(basis, **options) + + +class J2Op(SpinOpBase, HermitianOperator): + """The J^2 operator.""" + + _coord = '2' + + def _eval_commutator_JxOp(self, other): + return S.Zero + + def _eval_commutator_JyOp(self, other): + return S.Zero + + def _eval_commutator_JzOp(self, other): + return S.Zero + + def _eval_commutator_JplusOp(self, other): + return S.Zero + + def _eval_commutator_JminusOp(self, other): + return S.Zero + + def _apply_operator_JxKet(self, ket, **options): + j = ket.j + return hbar**2*j*(j + 1)*ket + + def _apply_operator_JxKetCoupled(self, ket, **options): + j = ket.j + return hbar**2*j*(j + 1)*ket + + def _apply_operator_JyKet(self, ket, **options): + j = ket.j + return hbar**2*j*(j + 1)*ket + + def _apply_operator_JyKetCoupled(self, ket, **options): + j = ket.j + return hbar**2*j*(j + 1)*ket + + def _apply_operator_JzKet(self, ket, **options): + j = ket.j + return hbar**2*j*(j + 1)*ket + + def _apply_operator_JzKetCoupled(self, ket, **options): + j = ket.j + return hbar**2*j*(j + 1)*ket + + def matrix_element(self, j, m, jp, mp): + result = (hbar**2)*j*(j + 1) + result *= KroneckerDelta(m, mp) + result *= KroneckerDelta(j, jp) + return result + + def _represent_default_basis(self, **options): + return self._represent_JzOp(None, **options) + + def _represent_JzOp(self, basis, **options): + return self._represent_base(basis, **options) + + def _print_contents_pretty(self, printer, *args): + a = prettyForm(str(self.name)) + b = prettyForm('2') + return a**b + + def _print_contents_latex(self, printer, *args): + return r'%s^2' % str(self.name) + + def _eval_rewrite_as_xyz(self, *args, **kwargs): + return JxOp(args[0])**2 + JyOp(args[0])**2 + JzOp(args[0])**2 + + def _eval_rewrite_as_plusminus(self, *args, **kwargs): + a = args[0] + return JzOp(a)**2 + \ + S.Half*(JplusOp(a)*JminusOp(a) + JminusOp(a)*JplusOp(a)) + + +class Rotation(UnitaryOperator): + """Wigner D operator in terms of Euler angles. + + Defines the rotation operator in terms of the Euler angles defined by + the z-y-z convention for a passive transformation. That is the coordinate + axes are rotated first about the z-axis, giving the new x'-y'-z' axes. Then + this new coordinate system is rotated about the new y'-axis, giving new + x''-y''-z'' axes. Then this new coordinate system is rotated about the + z''-axis. Conventions follow those laid out in [1]_. + + Parameters + ========== + + alpha : Number, Symbol + First Euler Angle + beta : Number, Symbol + Second Euler angle + gamma : Number, Symbol + Third Euler angle + + Examples + ======== + + A simple example rotation operator: + + >>> from sympy import pi + >>> from sympy.physics.quantum.spin import Rotation + >>> Rotation(pi, 0, pi/2) + R(pi,0,pi/2) + + With symbolic Euler angles and calculating the inverse rotation operator: + + >>> from sympy import symbols + >>> a, b, c = symbols('a b c') + >>> Rotation(a, b, c) + R(a,b,c) + >>> Rotation(a, b, c).inverse() + R(-c,-b,-a) + + See Also + ======== + + WignerD: Symbolic Wigner-D function + D: Wigner-D function + d: Wigner small-d function + + References + ========== + + .. [1] Varshalovich, D A, Quantum Theory of Angular Momentum. 1988. + """ + + @classmethod + def _eval_args(cls, args): + args = QExpr._eval_args(args) + if len(args) != 3: + raise ValueError('3 Euler angles required, got: %r' % args) + return args + + @classmethod + def _eval_hilbert_space(cls, label): + # We consider all j values so our space is infinite. + return ComplexSpace(S.Infinity) + + @property + def alpha(self): + return self.label[0] + + @property + def beta(self): + return self.label[1] + + @property + def gamma(self): + return self.label[2] + + def _print_operator_name(self, printer, *args): + return 'R' + + def _print_operator_name_pretty(self, printer, *args): + if printer._use_unicode: + return prettyForm('\N{SCRIPT CAPITAL R}' + ' ') + else: + return prettyForm("R ") + + def _print_operator_name_latex(self, printer, *args): + return r'\mathcal{R}' + + def _eval_inverse(self): + return Rotation(-self.gamma, -self.beta, -self.alpha) + + @classmethod + def D(cls, j, m, mp, alpha, beta, gamma): + """Wigner D-function. + + Returns an instance of the WignerD class corresponding to the Wigner-D + function specified by the parameters. + + Parameters + =========== + + j : Number + Total angular momentum + m : Number + Eigenvalue of angular momentum along axis after rotation + mp : Number + Eigenvalue of angular momentum along rotated axis + alpha : Number, Symbol + First Euler angle of rotation + beta : Number, Symbol + Second Euler angle of rotation + gamma : Number, Symbol + Third Euler angle of rotation + + Examples + ======== + + Return the Wigner-D matrix element for a defined rotation, both + numerical and symbolic: + + >>> from sympy.physics.quantum.spin import Rotation + >>> from sympy import pi, symbols + >>> alpha, beta, gamma = symbols('alpha beta gamma') + >>> Rotation.D(1, 1, 0,pi, pi/2,-pi) + WignerD(1, 1, 0, pi, pi/2, -pi) + + See Also + ======== + + WignerD: Symbolic Wigner-D function + + """ + return WignerD(j, m, mp, alpha, beta, gamma) + + @classmethod + def d(cls, j, m, mp, beta): + """Wigner small-d function. + + Returns an instance of the WignerD class corresponding to the Wigner-D + function specified by the parameters with the alpha and gamma angles + given as 0. + + Parameters + =========== + + j : Number + Total angular momentum + m : Number + Eigenvalue of angular momentum along axis after rotation + mp : Number + Eigenvalue of angular momentum along rotated axis + beta : Number, Symbol + Second Euler angle of rotation + + Examples + ======== + + Return the Wigner-D matrix element for a defined rotation, both + numerical and symbolic: + + >>> from sympy.physics.quantum.spin import Rotation + >>> from sympy import pi, symbols + >>> beta = symbols('beta') + >>> Rotation.d(1, 1, 0, pi/2) + WignerD(1, 1, 0, 0, pi/2, 0) + + See Also + ======== + + WignerD: Symbolic Wigner-D function + + """ + return WignerD(j, m, mp, 0, beta, 0) + + def matrix_element(self, j, m, jp, mp): + result = self.__class__.D( + jp, m, mp, self.alpha, self.beta, self.gamma + ) + result *= KroneckerDelta(j, jp) + return result + + def _represent_base(self, basis, **options): + j = sympify(options.get('j', S.Half)) + # TODO: move evaluation up to represent function/implement elsewhere + evaluate = sympify(options.get('doit')) + size, mvals = m_values(j) + result = zeros(size, size) + for p in range(size): + for q in range(size): + me = self.matrix_element(j, mvals[p], j, mvals[q]) + if evaluate: + result[p, q] = me.doit() + else: + result[p, q] = me + return result + + def _represent_default_basis(self, **options): + return self._represent_JzOp(None, **options) + + def _represent_JzOp(self, basis, **options): + return self._represent_base(basis, **options) + + def _apply_operator_uncoupled(self, state, ket, *, dummy=True, **options): + a = self.alpha + b = self.beta + g = self.gamma + j = ket.j + m = ket.m + if j.is_number: + s = [] + size = m_values(j) + sz = size[1] + for mp in sz: + r = Rotation.D(j, m, mp, a, b, g) + z = r.doit() + s.append(z*state(j, mp)) + return Add(*s) + else: + if dummy: + mp = Dummy('mp') + else: + mp = symbols('mp') + return Sum(Rotation.D(j, m, mp, a, b, g)*state(j, mp), (mp, -j, j)) + + def _apply_operator_JxKet(self, ket, **options): + return self._apply_operator_uncoupled(JxKet, ket, **options) + + def _apply_operator_JyKet(self, ket, **options): + return self._apply_operator_uncoupled(JyKet, ket, **options) + + def _apply_operator_JzKet(self, ket, **options): + return self._apply_operator_uncoupled(JzKet, ket, **options) + + def _apply_operator_coupled(self, state, ket, *, dummy=True, **options): + a = self.alpha + b = self.beta + g = self.gamma + j = ket.j + m = ket.m + jn = ket.jn + coupling = ket.coupling + if j.is_number: + s = [] + size = m_values(j) + sz = size[1] + for mp in sz: + r = Rotation.D(j, m, mp, a, b, g) + z = r.doit() + s.append(z*state(j, mp, jn, coupling)) + return Add(*s) + else: + if dummy: + mp = Dummy('mp') + else: + mp = symbols('mp') + return Sum(Rotation.D(j, m, mp, a, b, g)*state( + j, mp, jn, coupling), (mp, -j, j)) + + def _apply_operator_JxKetCoupled(self, ket, **options): + return self._apply_operator_coupled(JxKetCoupled, ket, **options) + + def _apply_operator_JyKetCoupled(self, ket, **options): + return self._apply_operator_coupled(JyKetCoupled, ket, **options) + + def _apply_operator_JzKetCoupled(self, ket, **options): + return self._apply_operator_coupled(JzKetCoupled, ket, **options) + +class WignerD(Expr): + r"""Wigner-D function + + The Wigner D-function gives the matrix elements of the rotation + operator in the jm-representation. For the Euler angles `\alpha`, + `\beta`, `\gamma`, the D-function is defined such that: + + .. math :: + = \delta_{jj'} D(j, m, m', \alpha, \beta, \gamma) + + Where the rotation operator is as defined by the Rotation class [1]_. + + The Wigner D-function defined in this way gives: + + .. math :: + D(j, m, m', \alpha, \beta, \gamma) = e^{-i m \alpha} d(j, m, m', \beta) e^{-i m' \gamma} + + Where d is the Wigner small-d function, which is given by Rotation.d. + + The Wigner small-d function gives the component of the Wigner + D-function that is determined by the second Euler angle. That is the + Wigner D-function is: + + .. math :: + D(j, m, m', \alpha, \beta, \gamma) = e^{-i m \alpha} d(j, m, m', \beta) e^{-i m' \gamma} + + Where d is the small-d function. The Wigner D-function is given by + Rotation.D. + + Note that to evaluate the D-function, the j, m and mp parameters must + be integer or half integer numbers. + + Parameters + ========== + + j : Number + Total angular momentum + m : Number + Eigenvalue of angular momentum along axis after rotation + mp : Number + Eigenvalue of angular momentum along rotated axis + alpha : Number, Symbol + First Euler angle of rotation + beta : Number, Symbol + Second Euler angle of rotation + gamma : Number, Symbol + Third Euler angle of rotation + + Examples + ======== + + Evaluate the Wigner-D matrix elements of a simple rotation: + + >>> from sympy.physics.quantum.spin import Rotation + >>> from sympy import pi + >>> rot = Rotation.D(1, 1, 0, pi, pi/2, 0) + >>> rot + WignerD(1, 1, 0, pi, pi/2, 0) + >>> rot.doit() + sqrt(2)/2 + + Evaluate the Wigner-d matrix elements of a simple rotation + + >>> rot = Rotation.d(1, 1, 0, pi/2) + >>> rot + WignerD(1, 1, 0, 0, pi/2, 0) + >>> rot.doit() + -sqrt(2)/2 + + See Also + ======== + + Rotation: Rotation operator + + References + ========== + + .. [1] Varshalovich, D A, Quantum Theory of Angular Momentum. 1988. + """ + + is_commutative = True + + def __new__(cls, *args, **hints): + if not len(args) == 6: + raise ValueError('6 parameters expected, got %s' % args) + args = sympify(args) + evaluate = hints.get('evaluate', False) + if evaluate: + return Expr.__new__(cls, *args)._eval_wignerd() + return Expr.__new__(cls, *args) + + @property + def j(self): + return self.args[0] + + @property + def m(self): + return self.args[1] + + @property + def mp(self): + return self.args[2] + + @property + def alpha(self): + return self.args[3] + + @property + def beta(self): + return self.args[4] + + @property + def gamma(self): + return self.args[5] + + def _latex(self, printer, *args): + if self.alpha == 0 and self.gamma == 0: + return r'd^{%s}_{%s,%s}\left(%s\right)' % \ + ( + printer._print(self.j), printer._print( + self.m), printer._print(self.mp), + printer._print(self.beta) ) + return r'D^{%s}_{%s,%s}\left(%s,%s,%s\right)' % \ + ( + printer._print( + self.j), printer._print(self.m), printer._print(self.mp), + printer._print(self.alpha), printer._print(self.beta), printer._print(self.gamma) ) + + def _pretty(self, printer, *args): + top = printer._print(self.j) + + bot = printer._print(self.m) + bot = prettyForm(*bot.right(',')) + bot = prettyForm(*bot.right(printer._print(self.mp))) + + pad = max(top.width(), bot.width()) + top = prettyForm(*top.left(' ')) + bot = prettyForm(*bot.left(' ')) + if pad > top.width(): + top = prettyForm(*top.right(' '*(pad - top.width()))) + if pad > bot.width(): + bot = prettyForm(*bot.right(' '*(pad - bot.width()))) + if self.alpha == 0 and self.gamma == 0: + args = printer._print(self.beta) + s = stringPict('d' + ' '*pad) + else: + args = printer._print(self.alpha) + args = prettyForm(*args.right(',')) + args = prettyForm(*args.right(printer._print(self.beta))) + args = prettyForm(*args.right(',')) + args = prettyForm(*args.right(printer._print(self.gamma))) + + s = stringPict('D' + ' '*pad) + + args = prettyForm(*args.parens()) + s = prettyForm(*s.above(top)) + s = prettyForm(*s.below(bot)) + s = prettyForm(*s.right(args)) + return s + + def doit(self, **hints): + hints['evaluate'] = True + return WignerD(*self.args, **hints) + + def _eval_wignerd(self): + j = self.j + m = self.m + mp = self.mp + alpha = self.alpha + beta = self.beta + gamma = self.gamma + if alpha == 0 and beta == 0 and gamma == 0: + return KroneckerDelta(m, mp) + if not j.is_number: + raise ValueError( + 'j parameter must be numerical to evaluate, got %s' % j) + r = 0 + if beta == pi/2: + # Varshalovich Equation (5), Section 4.16, page 113, setting + # alpha=gamma=0. + for k in range(2*j + 1): + if k > j + mp or k > j - m or k < mp - m: + continue + r += (S.NegativeOne)**k*binomial(j + mp, k)*binomial(j - mp, k + m - mp) + r *= (S.NegativeOne)**(m - mp) / 2**j*sqrt(factorial(j + m) * + factorial(j - m) / (factorial(j + mp)*factorial(j - mp))) + else: + # Varshalovich Equation(5), Section 4.7.2, page 87, where we set + # beta1=beta2=pi/2, and we get alpha=gamma=pi/2 and beta=phi+pi, + # then we use the Eq. (1), Section 4.4. page 79, to simplify: + # d(j, m, mp, beta+pi) = (-1)**(j-mp)*d(j, m, -mp, beta) + # This happens to be almost the same as in Eq.(10), Section 4.16, + # except that we need to substitute -mp for mp. + size, mvals = m_values(j) + for mpp in mvals: + r += Rotation.d(j, m, mpp, pi/2).doit()*(cos(-mpp*beta) + I*sin(-mpp*beta))*\ + Rotation.d(j, mpp, -mp, pi/2).doit() + # Empirical normalization factor so results match Varshalovich + # Tables 4.3-4.12 + # Note that this exact normalization does not follow from the + # above equations + r = r*I**(2*j - m - mp)*(-1)**(2*m) + # Finally, simplify the whole expression + r = simplify(r) + r *= exp(-I*m*alpha)*exp(-I*mp*gamma) + return r + + +Jx = JxOp('J') +Jy = JyOp('J') +Jz = JzOp('J') +J2 = J2Op('J') +Jplus = JplusOp('J') +Jminus = JminusOp('J') + + +#----------------------------------------------------------------------------- +# Spin States +#----------------------------------------------------------------------------- + + +class SpinState(State): + """Base class for angular momentum states.""" + + _label_separator = ',' + + def __new__(cls, j, m): + j = sympify(j) + m = sympify(m) + if j.is_number: + if 2*j != int(2*j): + raise ValueError( + 'j must be integer or half-integer, got: %s' % j) + if j < 0: + raise ValueError('j must be >= 0, got: %s' % j) + if m.is_number: + if 2*m != int(2*m): + raise ValueError( + 'm must be integer or half-integer, got: %s' % m) + if j.is_number and m.is_number: + if abs(m) > j: + raise ValueError('Allowed values for m are -j <= m <= j, got j, m: %s, %s' % (j, m)) + if int(j - m) != j - m: + raise ValueError('Both j and m must be integer or half-integer, got j, m: %s, %s' % (j, m)) + return State.__new__(cls, j, m) + + @property + def j(self): + return self.label[0] + + @property + def m(self): + return self.label[1] + + @classmethod + def _eval_hilbert_space(cls, label): + return ComplexSpace(2*label[0] + 1) + + def _represent_base(self, **options): + j = self.j + m = self.m + alpha = sympify(options.get('alpha', 0)) + beta = sympify(options.get('beta', 0)) + gamma = sympify(options.get('gamma', 0)) + size, mvals = m_values(j) + result = zeros(size, 1) + # breaks finding angles on L930 + for p, mval in enumerate(mvals): + if m.is_number: + result[p, 0] = Rotation.D( + self.j, mval, self.m, alpha, beta, gamma).doit() + else: + result[p, 0] = Rotation.D(self.j, mval, + self.m, alpha, beta, gamma) + return result + + def _eval_rewrite_as_Jx(self, *args, **options): + if isinstance(self, Bra): + return self._rewrite_basis(Jx, JxBra, **options) + return self._rewrite_basis(Jx, JxKet, **options) + + def _eval_rewrite_as_Jy(self, *args, **options): + if isinstance(self, Bra): + return self._rewrite_basis(Jy, JyBra, **options) + return self._rewrite_basis(Jy, JyKet, **options) + + def _eval_rewrite_as_Jz(self, *args, **options): + if isinstance(self, Bra): + return self._rewrite_basis(Jz, JzBra, **options) + return self._rewrite_basis(Jz, JzKet, **options) + + def _rewrite_basis(self, basis, evect, **options): + from sympy.physics.quantum.represent import represent + j = self.j + args = self.args[2:] + if j.is_number: + if isinstance(self, CoupledSpinState): + if j == int(j): + start = j**2 + else: + start = (2*j - 1)*(2*j + 1)/4 + else: + start = 0 + vect = represent(self, basis=basis, **options) + result = Add( + *[vect[start + i]*evect(j, j - i, *args) for i in range(2*j + 1)]) + if isinstance(self, CoupledSpinState) and options.get('coupled') is False: + return uncouple(result) + return result + else: + i = 0 + mi = symbols('mi') + # make sure not to introduce a symbol already in the state + while self.subs(mi, 0) != self: + i += 1 + mi = symbols('mi%d' % i) + break + # TODO: better way to get angles of rotation + if isinstance(self, CoupledSpinState): + test_args = (0, mi, (0, 0)) + else: + test_args = (0, mi) + if isinstance(self, Ket): + angles = represent( + self.__class__(*test_args), basis=basis)[0].args[3:6] + else: + angles = represent(self.__class__( + *test_args), basis=basis)[0].args[0].args[3:6] + if angles == (0, 0, 0): + return self + else: + state = evect(j, mi, *args) + lt = Rotation.D(j, mi, self.m, *angles) + return Sum(lt*state, (mi, -j, j)) + + def _eval_innerproduct_JxBra(self, bra, **hints): + result = KroneckerDelta(self.j, bra.j) + if bra.dual_class() is not self.__class__: + result *= self._represent_JxOp(None)[bra.j - bra.m] + else: + result *= KroneckerDelta( + self.j, bra.j)*KroneckerDelta(self.m, bra.m) + return result + + def _eval_innerproduct_JyBra(self, bra, **hints): + result = KroneckerDelta(self.j, bra.j) + if bra.dual_class() is not self.__class__: + result *= self._represent_JyOp(None)[bra.j - bra.m] + else: + result *= KroneckerDelta( + self.j, bra.j)*KroneckerDelta(self.m, bra.m) + return result + + def _eval_innerproduct_JzBra(self, bra, **hints): + result = KroneckerDelta(self.j, bra.j) + if bra.dual_class() is not self.__class__: + result *= self._represent_JzOp(None)[bra.j - bra.m] + else: + result *= KroneckerDelta( + self.j, bra.j)*KroneckerDelta(self.m, bra.m) + return result + + def _eval_trace(self, bra, **hints): + + # One way to implement this method is to assume the basis set k is + # passed. + # Then we can apply the discrete form of Trace formula here + # Tr(|i> + #then we do qapply() on each each inner product and sum over them. + + # OR + + # Inner product of |i>>> from sympy.physics.quantum.spin import JzKet, JxKet + >>> from sympy import symbols + >>> JzKet(1, 0) + |1,0> + >>> j, m = symbols('j m') + >>> JzKet(j, m) + |j,m> + + Rewriting the JzKet in terms of eigenkets of the Jx operator: + Note: that the resulting eigenstates are JxKet's + + >>> JzKet(1,1).rewrite("Jx") + |1,-1>/2 - sqrt(2)*|1,0>/2 + |1,1>/2 + + Get the vector representation of a state in terms of the basis elements + of the Jx operator: + + >>> from sympy.physics.quantum.represent import represent + >>> from sympy.physics.quantum.spin import Jx, Jz + >>> represent(JzKet(1,-1), basis=Jx) + Matrix([ + [ 1/2], + [sqrt(2)/2], + [ 1/2]]) + + Apply innerproducts between states: + + >>> from sympy.physics.quantum.innerproduct import InnerProduct + >>> from sympy.physics.quantum.spin import JxBra + >>> i = InnerProduct(JxBra(1,1), JzKet(1,1)) + >>> i + <1,1|1,1> + >>> i.doit() + 1/2 + + *Uncoupled States:* + + Define an uncoupled state as a TensorProduct between two Jz eigenkets: + + >>> from sympy.physics.quantum.tensorproduct import TensorProduct + >>> j1,m1,j2,m2 = symbols('j1 m1 j2 m2') + >>> TensorProduct(JzKet(1,0), JzKet(1,1)) + |1,0>x|1,1> + >>> TensorProduct(JzKet(j1,m1), JzKet(j2,m2)) + |j1,m1>x|j2,m2> + + A TensorProduct can be rewritten, in which case the eigenstates that make + up the tensor product is rewritten to the new basis: + + >>> TensorProduct(JzKet(1,1),JxKet(1,1)).rewrite('Jz') + |1,1>x|1,-1>/2 + sqrt(2)*|1,1>x|1,0>/2 + |1,1>x|1,1>/2 + + The represent method for TensorProduct's gives the vector representation of + the state. Note that the state in the product basis is the equivalent of the + tensor product of the vector representation of the component eigenstates: + + >>> represent(TensorProduct(JzKet(1,0),JzKet(1,1))) + Matrix([ + [0], + [0], + [0], + [1], + [0], + [0], + [0], + [0], + [0]]) + >>> represent(TensorProduct(JzKet(1,1),JxKet(1,1)), basis=Jz) + Matrix([ + [ 1/2], + [sqrt(2)/2], + [ 1/2], + [ 0], + [ 0], + [ 0], + [ 0], + [ 0], + [ 0]]) + + See Also + ======== + + JzKetCoupled: Coupled eigenstates + sympy.physics.quantum.tensorproduct.TensorProduct: Used to specify uncoupled states + uncouple: Uncouples states given coupling parameters + couple: Couples uncoupled states + + """ + + @classmethod + def dual_class(self): + return JzBra + + @classmethod + def coupled_class(self): + return JzKetCoupled + + def _represent_default_basis(self, **options): + return self._represent_JzOp(None, **options) + + def _represent_JxOp(self, basis, **options): + return self._represent_base(beta=pi*Rational(3, 2), **options) + + def _represent_JyOp(self, basis, **options): + return self._represent_base(alpha=pi*Rational(3, 2), beta=pi/2, gamma=pi/2, **options) + + def _represent_JzOp(self, basis, **options): + return self._represent_base(**options) + + +class JzBra(SpinState, Bra): + """Eigenbra of Jz. + + See the JzKet for the usage of spin eigenstates. + + See Also + ======== + + JzKet: Usage of spin states + + """ + + @classmethod + def dual_class(self): + return JzKet + + @classmethod + def coupled_class(self): + return JzBraCoupled + + +# Method used primarily to create coupled_n and coupled_jn by __new__ in +# CoupledSpinState +# This same method is also used by the uncouple method, and is separated from +# the CoupledSpinState class to maintain consistency in defining coupling +def _build_coupled(jcoupling, length): + n_list = [ [n + 1] for n in range(length) ] + coupled_jn = [] + coupled_n = [] + for n1, n2, j_new in jcoupling: + coupled_jn.append(j_new) + coupled_n.append( (n_list[n1 - 1], n_list[n2 - 1]) ) + n_sort = sorted(n_list[n1 - 1] + n_list[n2 - 1]) + n_list[n_sort[0] - 1] = n_sort + return coupled_n, coupled_jn + + +class CoupledSpinState(SpinState): + """Base class for coupled angular momentum states.""" + + def __new__(cls, j, m, jn, *jcoupling): + # Check j and m values using SpinState + SpinState(j, m) + # Build and check coupling scheme from arguments + if len(jcoupling) == 0: + # Use default coupling scheme + jcoupling = [] + for n in range(2, len(jn)): + jcoupling.append( (1, n, Add(*[jn[i] for i in range(n)])) ) + jcoupling.append( (1, len(jn), j) ) + elif len(jcoupling) == 1: + # Use specified coupling scheme + jcoupling = jcoupling[0] + else: + raise TypeError("CoupledSpinState only takes 3 or 4 arguments, got: %s" % (len(jcoupling) + 3) ) + # Check arguments have correct form + if not isinstance(jn, (list, tuple, Tuple)): + raise TypeError('jn must be Tuple, list or tuple, got %s' % + jn.__class__.__name__) + if not isinstance(jcoupling, (list, tuple, Tuple)): + raise TypeError('jcoupling must be Tuple, list or tuple, got %s' % + jcoupling.__class__.__name__) + if not all(isinstance(term, (list, tuple, Tuple)) for term in jcoupling): + raise TypeError( + 'All elements of jcoupling must be list, tuple or Tuple') + if not len(jn) - 1 == len(jcoupling): + raise ValueError('jcoupling must have length of %d, got %d' % + (len(jn) - 1, len(jcoupling))) + if not all(len(x) == 3 for x in jcoupling): + raise ValueError('All elements of jcoupling must have length 3') + # Build sympified args + j = sympify(j) + m = sympify(m) + jn = Tuple( *[sympify(ji) for ji in jn] ) + jcoupling = Tuple( *[Tuple(sympify( + n1), sympify(n2), sympify(ji)) for (n1, n2, ji) in jcoupling] ) + # Check values in coupling scheme give physical state + if any(2*ji != int(2*ji) for ji in jn if ji.is_number): + raise ValueError('All elements of jn must be integer or half-integer, got: %s' % jn) + if any(n1 != int(n1) or n2 != int(n2) for (n1, n2, _) in jcoupling): + raise ValueError('Indices in jcoupling must be integers') + if any(n1 < 1 or n2 < 1 or n1 > len(jn) or n2 > len(jn) for (n1, n2, _) in jcoupling): + raise ValueError('Indices must be between 1 and the number of coupled spin spaces') + if any(2*ji != int(2*ji) for (_, _, ji) in jcoupling if ji.is_number): + raise ValueError('All coupled j values in coupling scheme must be integer or half-integer') + coupled_n, coupled_jn = _build_coupled(jcoupling, len(jn)) + jvals = list(jn) + for n, (n1, n2) in enumerate(coupled_n): + j1 = jvals[min(n1) - 1] + j2 = jvals[min(n2) - 1] + j3 = coupled_jn[n] + if sympify(j1).is_number and sympify(j2).is_number and sympify(j3).is_number: + if j1 + j2 < j3: + raise ValueError('All couplings must have j1+j2 >= j3, ' + 'in coupling number %d got j1,j2,j3: %d,%d,%d' % (n + 1, j1, j2, j3)) + if abs(j1 - j2) > j3: + raise ValueError("All couplings must have |j1+j2| <= j3, " + "in coupling number %d got j1,j2,j3: %d,%d,%d" % (n + 1, j1, j2, j3)) + if int_valued(j1 + j2): + pass + jvals[min(n1 + n2) - 1] = j3 + if len(jcoupling) > 0 and jcoupling[-1][2] != j: + raise ValueError('Last j value coupled together must be the final j of the state') + # Return state + return State.__new__(cls, j, m, jn, jcoupling) + + def _print_label(self, printer, *args): + label = [printer._print(self.j), printer._print(self.m)] + for i, ji in enumerate(self.jn, start=1): + label.append('j%d=%s' % ( + i, printer._print(ji) + )) + for jn, (n1, n2) in zip(self.coupled_jn[:-1], self.coupled_n[:-1]): + label.append('j(%s)=%s' % ( + ','.join(str(i) for i in sorted(n1 + n2)), printer._print(jn) + )) + return ','.join(label) + + def _print_label_pretty(self, printer, *args): + label = [self.j, self.m] + for i, ji in enumerate(self.jn, start=1): + symb = 'j%d' % i + symb = pretty_symbol(symb) + symb = prettyForm(symb + '=') + item = prettyForm(*symb.right(printer._print(ji))) + label.append(item) + for jn, (n1, n2) in zip(self.coupled_jn[:-1], self.coupled_n[:-1]): + n = ','.join(pretty_symbol("j%d" % i)[-1] for i in sorted(n1 + n2)) + symb = prettyForm('j' + n + '=') + item = prettyForm(*symb.right(printer._print(jn))) + label.append(item) + return self._print_sequence_pretty( + label, self._label_separator, printer, *args + ) + + def _print_label_latex(self, printer, *args): + label = [ + printer._print(self.j, *args), + printer._print(self.m, *args) + ] + for i, ji in enumerate(self.jn, start=1): + label.append('j_{%d}=%s' % (i, printer._print(ji, *args)) ) + for jn, (n1, n2) in zip(self.coupled_jn[:-1], self.coupled_n[:-1]): + n = ','.join(str(i) for i in sorted(n1 + n2)) + label.append('j_{%s}=%s' % (n, printer._print(jn, *args)) ) + return self._label_separator.join(label) + + @property + def jn(self): + return self.label[2] + + @property + def coupling(self): + return self.label[3] + + @property + def coupled_jn(self): + return _build_coupled(self.label[3], len(self.label[2]))[1] + + @property + def coupled_n(self): + return _build_coupled(self.label[3], len(self.label[2]))[0] + + @classmethod + def _eval_hilbert_space(cls, label): + j = Add(*label[2]) + if j.is_number: + return DirectSumHilbertSpace(*[ ComplexSpace(x) for x in range(int(2*j + 1), 0, -2) ]) + else: + # TODO: Need hilbert space fix, see issue 5732 + # Desired behavior: + #ji = symbols('ji') + #ret = Sum(ComplexSpace(2*ji + 1), (ji, 0, j)) + # Temporary fix: + return ComplexSpace(2*j + 1) + + def _represent_coupled_base(self, **options): + evect = self.uncoupled_class() + if not self.j.is_number: + raise ValueError( + 'State must not have symbolic j value to represent') + if not self.hilbert_space.dimension.is_number: + raise ValueError( + 'State must not have symbolic j values to represent') + result = zeros(self.hilbert_space.dimension, 1) + if self.j == int(self.j): + start = self.j**2 + else: + start = (2*self.j - 1)*(1 + 2*self.j)/4 + result[start:start + 2*self.j + 1, 0] = evect( + self.j, self.m)._represent_base(**options) + return result + + def _eval_rewrite_as_Jx(self, *args, **options): + if isinstance(self, Bra): + return self._rewrite_basis(Jx, JxBraCoupled, **options) + return self._rewrite_basis(Jx, JxKetCoupled, **options) + + def _eval_rewrite_as_Jy(self, *args, **options): + if isinstance(self, Bra): + return self._rewrite_basis(Jy, JyBraCoupled, **options) + return self._rewrite_basis(Jy, JyKetCoupled, **options) + + def _eval_rewrite_as_Jz(self, *args, **options): + if isinstance(self, Bra): + return self._rewrite_basis(Jz, JzBraCoupled, **options) + return self._rewrite_basis(Jz, JzKetCoupled, **options) + + +class JxKetCoupled(CoupledSpinState, Ket): + """Coupled eigenket of Jx. + + See JzKetCoupled for the usage of coupled spin eigenstates. + + See Also + ======== + + JzKetCoupled: Usage of coupled spin states + + """ + + @classmethod + def dual_class(self): + return JxBraCoupled + + @classmethod + def uncoupled_class(self): + return JxKet + + def _represent_default_basis(self, **options): + return self._represent_JzOp(None, **options) + + def _represent_JxOp(self, basis, **options): + return self._represent_coupled_base(**options) + + def _represent_JyOp(self, basis, **options): + return self._represent_coupled_base(alpha=pi*Rational(3, 2), **options) + + def _represent_JzOp(self, basis, **options): + return self._represent_coupled_base(beta=pi/2, **options) + + +class JxBraCoupled(CoupledSpinState, Bra): + """Coupled eigenbra of Jx. + + See JzKetCoupled for the usage of coupled spin eigenstates. + + See Also + ======== + + JzKetCoupled: Usage of coupled spin states + + """ + + @classmethod + def dual_class(self): + return JxKetCoupled + + @classmethod + def uncoupled_class(self): + return JxBra + + +class JyKetCoupled(CoupledSpinState, Ket): + """Coupled eigenket of Jy. + + See JzKetCoupled for the usage of coupled spin eigenstates. + + See Also + ======== + + JzKetCoupled: Usage of coupled spin states + + """ + + @classmethod + def dual_class(self): + return JyBraCoupled + + @classmethod + def uncoupled_class(self): + return JyKet + + def _represent_default_basis(self, **options): + return self._represent_JzOp(None, **options) + + def _represent_JxOp(self, basis, **options): + return self._represent_coupled_base(gamma=pi/2, **options) + + def _represent_JyOp(self, basis, **options): + return self._represent_coupled_base(**options) + + def _represent_JzOp(self, basis, **options): + return self._represent_coupled_base(alpha=pi*Rational(3, 2), beta=-pi/2, gamma=pi/2, **options) + + +class JyBraCoupled(CoupledSpinState, Bra): + """Coupled eigenbra of Jy. + + See JzKetCoupled for the usage of coupled spin eigenstates. + + See Also + ======== + + JzKetCoupled: Usage of coupled spin states + + """ + + @classmethod + def dual_class(self): + return JyKetCoupled + + @classmethod + def uncoupled_class(self): + return JyBra + + +class JzKetCoupled(CoupledSpinState, Ket): + r"""Coupled eigenket of Jz + + Spin state that is an eigenket of Jz which represents the coupling of + separate spin spaces. + + The arguments for creating instances of JzKetCoupled are ``j``, ``m``, + ``jn`` and an optional ``jcoupling`` argument. The ``j`` and ``m`` options + are the total angular momentum quantum numbers, as used for normal states + (e.g. JzKet). + + The other required parameter in ``jn``, which is a tuple defining the `j_n` + angular momentum quantum numbers of the product spaces. So for example, if + a state represented the coupling of the product basis state + `\left|j_1,m_1\right\rangle\times\left|j_2,m_2\right\rangle`, the ``jn`` + for this state would be ``(j1,j2)``. + + The final option is ``jcoupling``, which is used to define how the spaces + specified by ``jn`` are coupled, which includes both the order these spaces + are coupled together and the quantum numbers that arise from these + couplings. The ``jcoupling`` parameter itself is a list of lists, such that + each of the sublists defines a single coupling between the spin spaces. If + there are N coupled angular momentum spaces, that is ``jn`` has N elements, + then there must be N-1 sublists. Each of these sublists making up the + ``jcoupling`` parameter have length 3. The first two elements are the + indices of the product spaces that are considered to be coupled together. + For example, if we want to couple `j_1` and `j_4`, the indices would be 1 + and 4. If a state has already been coupled, it is referenced by the + smallest index that is coupled, so if `j_2` and `j_4` has already been + coupled to some `j_{24}`, then this value can be coupled by referencing it + with index 2. The final element of the sublist is the quantum number of the + coupled state. So putting everything together, into a valid sublist for + ``jcoupling``, if `j_1` and `j_2` are coupled to an angular momentum space + with quantum number `j_{12}` with the value ``j12``, the sublist would be + ``(1,2,j12)``, N-1 of these sublists are used in the list for + ``jcoupling``. + + Note the ``jcoupling`` parameter is optional, if it is not specified, the + default coupling is taken. This default value is to coupled the spaces in + order and take the quantum number of the coupling to be the maximum value. + For example, if the spin spaces are `j_1`, `j_2`, `j_3`, `j_4`, then the + default coupling couples `j_1` and `j_2` to `j_{12}=j_1+j_2`, then, + `j_{12}` and `j_3` are coupled to `j_{123}=j_{12}+j_3`, and finally + `j_{123}` and `j_4` to `j=j_{123}+j_4`. The jcoupling value that would + correspond to this is: + + ``((1,2,j1+j2),(1,3,j1+j2+j3))`` + + Parameters + ========== + + args : tuple + The arguments that must be passed are ``j``, ``m``, ``jn``, and + ``jcoupling``. The ``j`` value is the total angular momentum. The ``m`` + value is the eigenvalue of the Jz spin operator. The ``jn`` list are + the j values of argular momentum spaces coupled together. The + ``jcoupling`` parameter is an optional parameter defining how the spaces + are coupled together. See the above description for how these coupling + parameters are defined. + + Examples + ======== + + Defining simple spin states, both numerical and symbolic: + + >>> from sympy.physics.quantum.spin import JzKetCoupled + >>> from sympy import symbols + >>> JzKetCoupled(1, 0, (1, 1)) + |1,0,j1=1,j2=1> + >>> j, m, j1, j2 = symbols('j m j1 j2') + >>> JzKetCoupled(j, m, (j1, j2)) + |j,m,j1=j1,j2=j2> + + Defining coupled spin states for more than 2 coupled spaces with various + coupling parameters: + + >>> JzKetCoupled(2, 1, (1, 1, 1)) + |2,1,j1=1,j2=1,j3=1,j(1,2)=2> + >>> JzKetCoupled(2, 1, (1, 1, 1), ((1,2,2),(1,3,2)) ) + |2,1,j1=1,j2=1,j3=1,j(1,2)=2> + >>> JzKetCoupled(2, 1, (1, 1, 1), ((2,3,1),(1,2,2)) ) + |2,1,j1=1,j2=1,j3=1,j(2,3)=1> + + Rewriting the JzKetCoupled in terms of eigenkets of the Jx operator: + Note: that the resulting eigenstates are JxKetCoupled + + >>> JzKetCoupled(1,1,(1,1)).rewrite("Jx") + |1,-1,j1=1,j2=1>/2 - sqrt(2)*|1,0,j1=1,j2=1>/2 + |1,1,j1=1,j2=1>/2 + + The rewrite method can be used to convert a coupled state to an uncoupled + state. This is done by passing coupled=False to the rewrite function: + + >>> JzKetCoupled(1, 0, (1, 1)).rewrite('Jz', coupled=False) + -sqrt(2)*|1,-1>x|1,1>/2 + sqrt(2)*|1,1>x|1,-1>/2 + + Get the vector representation of a state in terms of the basis elements + of the Jx operator: + + >>> from sympy.physics.quantum.represent import represent + >>> from sympy.physics.quantum.spin import Jx + >>> from sympy import S + >>> represent(JzKetCoupled(1,-1,(S(1)/2,S(1)/2)), basis=Jx) + Matrix([ + [ 0], + [ 1/2], + [sqrt(2)/2], + [ 1/2]]) + + See Also + ======== + + JzKet: Normal spin eigenstates + uncouple: Uncoupling of coupling spin states + couple: Coupling of uncoupled spin states + + """ + + @classmethod + def dual_class(self): + return JzBraCoupled + + @classmethod + def uncoupled_class(self): + return JzKet + + def _represent_default_basis(self, **options): + return self._represent_JzOp(None, **options) + + def _represent_JxOp(self, basis, **options): + return self._represent_coupled_base(beta=pi*Rational(3, 2), **options) + + def _represent_JyOp(self, basis, **options): + return self._represent_coupled_base(alpha=pi*Rational(3, 2), beta=pi/2, gamma=pi/2, **options) + + def _represent_JzOp(self, basis, **options): + return self._represent_coupled_base(**options) + + +class JzBraCoupled(CoupledSpinState, Bra): + """Coupled eigenbra of Jz. + + See the JzKetCoupled for the usage of coupled spin eigenstates. + + See Also + ======== + + JzKetCoupled: Usage of coupled spin states + + """ + + @classmethod + def dual_class(self): + return JzKetCoupled + + @classmethod + def uncoupled_class(self): + return JzBra + +#----------------------------------------------------------------------------- +# Coupling/uncoupling +#----------------------------------------------------------------------------- + + +def couple(expr, jcoupling_list=None): + """ Couple a tensor product of spin states + + This function can be used to couple an uncoupled tensor product of spin + states. All of the eigenstates to be coupled must be of the same class. It + will return a linear combination of eigenstates that are subclasses of + CoupledSpinState determined by Clebsch-Gordan angular momentum coupling + coefficients. + + Parameters + ========== + + expr : Expr + An expression involving TensorProducts of spin states to be coupled. + Each state must be a subclass of SpinState and they all must be the + same class. + + jcoupling_list : list or tuple + Elements of this list are sub-lists of length 2 specifying the order of + the coupling of the spin spaces. The length of this must be N-1, where N + is the number of states in the tensor product to be coupled. The + elements of this sublist are the same as the first two elements of each + sublist in the ``jcoupling`` parameter defined for JzKetCoupled. If this + parameter is not specified, the default value is taken, which couples + the first and second product basis spaces, then couples this new coupled + space to the third product space, etc + + Examples + ======== + + Couple a tensor product of numerical states for two spaces: + + >>> from sympy.physics.quantum.spin import JzKet, couple + >>> from sympy.physics.quantum.tensorproduct import TensorProduct + >>> couple(TensorProduct(JzKet(1,0), JzKet(1,1))) + -sqrt(2)*|1,1,j1=1,j2=1>/2 + sqrt(2)*|2,1,j1=1,j2=1>/2 + + + Numerical coupling of three spaces using the default coupling method, i.e. + first and second spaces couple, then this couples to the third space: + + >>> couple(TensorProduct(JzKet(1,1), JzKet(1,1), JzKet(1,0))) + sqrt(6)*|2,2,j1=1,j2=1,j3=1,j(1,2)=2>/3 + sqrt(3)*|3,2,j1=1,j2=1,j3=1,j(1,2)=2>/3 + + Perform this same coupling, but we define the coupling to first couple + the first and third spaces: + + >>> couple(TensorProduct(JzKet(1,1), JzKet(1,1), JzKet(1,0)), ((1,3),(1,2)) ) + sqrt(2)*|2,2,j1=1,j2=1,j3=1,j(1,3)=1>/2 - sqrt(6)*|2,2,j1=1,j2=1,j3=1,j(1,3)=2>/6 + sqrt(3)*|3,2,j1=1,j2=1,j3=1,j(1,3)=2>/3 + + Couple a tensor product of symbolic states: + + >>> from sympy import symbols + >>> j1,m1,j2,m2 = symbols('j1 m1 j2 m2') + >>> couple(TensorProduct(JzKet(j1,m1), JzKet(j2,m2))) + Sum(CG(j1, m1, j2, m2, j, m1 + m2)*|j,m1 + m2,j1=j1,j2=j2>, (j, m1 + m2, j1 + j2)) + + """ + a = expr.atoms(TensorProduct) + for tp in a: + # Allow other tensor products to be in expression + if not all(isinstance(state, SpinState) for state in tp.args): + continue + # If tensor product has all spin states, raise error for invalid tensor product state + if not all(state.__class__ is tp.args[0].__class__ for state in tp.args): + raise TypeError('All states must be the same basis') + expr = expr.subs(tp, _couple(tp, jcoupling_list)) + return expr + + +def _couple(tp, jcoupling_list): + states = tp.args + coupled_evect = states[0].coupled_class() + + # Define default coupling if none is specified + if jcoupling_list is None: + jcoupling_list = [] + for n in range(1, len(states)): + jcoupling_list.append( (1, n + 1) ) + + # Check jcoupling_list valid + if not len(jcoupling_list) == len(states) - 1: + raise TypeError('jcoupling_list must be length %d, got %d' % + (len(states) - 1, len(jcoupling_list))) + if not all( len(coupling) == 2 for coupling in jcoupling_list): + raise ValueError('Each coupling must define 2 spaces') + if any(n1 == n2 for n1, n2 in jcoupling_list): + raise ValueError('Spin spaces cannot couple to themselves') + if all(sympify(n1).is_number and sympify(n2).is_number for n1, n2 in jcoupling_list): + j_test = [0]*len(states) + for n1, n2 in jcoupling_list: + if j_test[n1 - 1] == -1 or j_test[n2 - 1] == -1: + raise ValueError('Spaces coupling j_n\'s are referenced by smallest n value') + j_test[max(n1, n2) - 1] = -1 + + # j values of states to be coupled together + jn = [state.j for state in states] + mn = [state.m for state in states] + + # Create coupling_list, which defines all the couplings between all + # the spaces from jcoupling_list + coupling_list = [] + n_list = [ [i + 1] for i in range(len(states)) ] + for j_coupling in jcoupling_list: + # Least n for all j_n which is coupled as first and second spaces + n1, n2 = j_coupling + # List of all n's coupled in first and second spaces + j1_n = list(n_list[n1 - 1]) + j2_n = list(n_list[n2 - 1]) + coupling_list.append( (j1_n, j2_n) ) + # Set new j_n to be coupling of all j_n in both first and second spaces + n_list[ min(n1, n2) - 1 ] = sorted(j1_n + j2_n) + + if all(state.j.is_number and state.m.is_number for state in states): + # Numerical coupling + # Iterate over difference between maximum possible j value of each coupling and the actual value + diff_max = [ Add( *[ jn[n - 1] - mn[n - 1] for n in coupling[0] + + coupling[1] ] ) for coupling in coupling_list ] + result = [] + for diff in range(diff_max[-1] + 1): + # Determine available configurations + n = len(coupling_list) + tot = binomial(diff + n - 1, diff) + + for config_num in range(tot): + diff_list = _confignum_to_difflist(config_num, diff, n) + + # Skip the configuration if non-physical + # This is a lazy check for physical states given the loose restrictions of diff_max + if any(d > m for d, m in zip(diff_list, diff_max)): + continue + + # Determine term + cg_terms = [] + coupled_j = list(jn) + jcoupling = [] + for (j1_n, j2_n), coupling_diff in zip(coupling_list, diff_list): + j1 = coupled_j[ min(j1_n) - 1 ] + j2 = coupled_j[ min(j2_n) - 1 ] + j3 = j1 + j2 - coupling_diff + coupled_j[ min(j1_n + j2_n) - 1 ] = j3 + m1 = Add( *[ mn[x - 1] for x in j1_n] ) + m2 = Add( *[ mn[x - 1] for x in j2_n] ) + m3 = m1 + m2 + cg_terms.append( (j1, m1, j2, m2, j3, m3) ) + jcoupling.append( (min(j1_n), min(j2_n), j3) ) + # Better checks that state is physical + if any(abs(term[5]) > term[4] for term in cg_terms): + continue + if any(term[0] + term[2] < term[4] for term in cg_terms): + continue + if any(abs(term[0] - term[2]) > term[4] for term in cg_terms): + continue + coeff = Mul( *[ CG(*term).doit() for term in cg_terms] ) + state = coupled_evect(j3, m3, jn, jcoupling) + result.append(coeff*state) + return Add(*result) + else: + # Symbolic coupling + cg_terms = [] + jcoupling = [] + sum_terms = [] + coupled_j = list(jn) + for j1_n, j2_n in coupling_list: + j1 = coupled_j[ min(j1_n) - 1 ] + j2 = coupled_j[ min(j2_n) - 1 ] + if len(j1_n + j2_n) == len(states): + j3 = symbols('j') + else: + j3_name = 'j' + ''.join(["%s" % n for n in j1_n + j2_n]) + j3 = symbols(j3_name) + coupled_j[ min(j1_n + j2_n) - 1 ] = j3 + m1 = Add( *[ mn[x - 1] for x in j1_n] ) + m2 = Add( *[ mn[x - 1] for x in j2_n] ) + m3 = m1 + m2 + cg_terms.append( (j1, m1, j2, m2, j3, m3) ) + jcoupling.append( (min(j1_n), min(j2_n), j3) ) + sum_terms.append((j3, m3, j1 + j2)) + coeff = Mul( *[ CG(*term) for term in cg_terms] ) + state = coupled_evect(j3, m3, jn, jcoupling) + return Sum(coeff*state, *sum_terms) + + +def uncouple(expr, jn=None, jcoupling_list=None): + """ Uncouple a coupled spin state + + Gives the uncoupled representation of a coupled spin state. Arguments must + be either a spin state that is a subclass of CoupledSpinState or a spin + state that is a subclass of SpinState and an array giving the j values + of the spaces that are to be coupled + + Parameters + ========== + + expr : Expr + The expression containing states that are to be coupled. If the states + are a subclass of SpinState, the ``jn`` and ``jcoupling`` parameters + must be defined. If the states are a subclass of CoupledSpinState, + ``jn`` and ``jcoupling`` will be taken from the state. + + jn : list or tuple + The list of the j-values that are coupled. If state is a + CoupledSpinState, this parameter is ignored. This must be defined if + state is not a subclass of CoupledSpinState. The syntax of this + parameter is the same as the ``jn`` parameter of JzKetCoupled. + + jcoupling_list : list or tuple + The list defining how the j-values are coupled together. If state is a + CoupledSpinState, this parameter is ignored. This must be defined if + state is not a subclass of CoupledSpinState. The syntax of this + parameter is the same as the ``jcoupling`` parameter of JzKetCoupled. + + Examples + ======== + + Uncouple a numerical state using a CoupledSpinState state: + + >>> from sympy.physics.quantum.spin import JzKetCoupled, uncouple + >>> from sympy import S + >>> uncouple(JzKetCoupled(1, 0, (S(1)/2, S(1)/2))) + sqrt(2)*|1/2,-1/2>x|1/2,1/2>/2 + sqrt(2)*|1/2,1/2>x|1/2,-1/2>/2 + + Perform the same calculation using a SpinState state: + + >>> from sympy.physics.quantum.spin import JzKet + >>> uncouple(JzKet(1, 0), (S(1)/2, S(1)/2)) + sqrt(2)*|1/2,-1/2>x|1/2,1/2>/2 + sqrt(2)*|1/2,1/2>x|1/2,-1/2>/2 + + Uncouple a numerical state of three coupled spaces using a CoupledSpinState state: + + >>> uncouple(JzKetCoupled(1, 1, (1, 1, 1), ((1,3,1),(1,2,1)) )) + |1,-1>x|1,1>x|1,1>/2 - |1,0>x|1,0>x|1,1>/2 + |1,1>x|1,0>x|1,0>/2 - |1,1>x|1,1>x|1,-1>/2 + + Perform the same calculation using a SpinState state: + + >>> uncouple(JzKet(1, 1), (1, 1, 1), ((1,3,1),(1,2,1)) ) + |1,-1>x|1,1>x|1,1>/2 - |1,0>x|1,0>x|1,1>/2 + |1,1>x|1,0>x|1,0>/2 - |1,1>x|1,1>x|1,-1>/2 + + Uncouple a symbolic state using a CoupledSpinState state: + + >>> from sympy import symbols + >>> j,m,j1,j2 = symbols('j m j1 j2') + >>> uncouple(JzKetCoupled(j, m, (j1, j2))) + Sum(CG(j1, m1, j2, m2, j, m)*|j1,m1>x|j2,m2>, (m1, -j1, j1), (m2, -j2, j2)) + + Perform the same calculation using a SpinState state + + >>> uncouple(JzKet(j, m), (j1, j2)) + Sum(CG(j1, m1, j2, m2, j, m)*|j1,m1>x|j2,m2>, (m1, -j1, j1), (m2, -j2, j2)) + + """ + a = expr.atoms(SpinState) + for state in a: + expr = expr.subs(state, _uncouple(state, jn, jcoupling_list)) + return expr + + +def _uncouple(state, jn, jcoupling_list): + if isinstance(state, CoupledSpinState): + jn = state.jn + coupled_n = state.coupled_n + coupled_jn = state.coupled_jn + evect = state.uncoupled_class() + elif isinstance(state, SpinState): + if jn is None: + raise ValueError("Must specify j-values for coupled state") + if not isinstance(jn, (list, tuple)): + raise TypeError("jn must be list or tuple") + if jcoupling_list is None: + # Use default + jcoupling_list = [] + for i in range(1, len(jn)): + jcoupling_list.append( + (1, 1 + i, Add(*[jn[j] for j in range(i + 1)])) ) + if not isinstance(jcoupling_list, (list, tuple)): + raise TypeError("jcoupling must be a list or tuple") + if not len(jcoupling_list) == len(jn) - 1: + raise ValueError("Must specify 2 fewer coupling terms than the number of j values") + coupled_n, coupled_jn = _build_coupled(jcoupling_list, len(jn)) + evect = state.__class__ + else: + raise TypeError("state must be a spin state") + j = state.j + m = state.m + coupling_list = [] + j_list = list(jn) + + # Create coupling, which defines all the couplings between all the spaces + for j3, (n1, n2) in zip(coupled_jn, coupled_n): + # j's which are coupled as first and second spaces + j1 = j_list[n1[0] - 1] + j2 = j_list[n2[0] - 1] + # Build coupling list + coupling_list.append( (n1, n2, j1, j2, j3) ) + # Set new value in j_list + j_list[min(n1 + n2) - 1] = j3 + + if j.is_number and m.is_number: + diff_max = [ 2*x for x in jn ] + diff = Add(*jn) - m + + n = len(jn) + tot = binomial(diff + n - 1, diff) + + result = [] + for config_num in range(tot): + diff_list = _confignum_to_difflist(config_num, diff, n) + if any(d > p for d, p in zip(diff_list, diff_max)): + continue + + cg_terms = [] + for coupling in coupling_list: + j1_n, j2_n, j1, j2, j3 = coupling + m1 = Add( *[ jn[x - 1] - diff_list[x - 1] for x in j1_n ] ) + m2 = Add( *[ jn[x - 1] - diff_list[x - 1] for x in j2_n ] ) + m3 = m1 + m2 + cg_terms.append( (j1, m1, j2, m2, j3, m3) ) + coeff = Mul( *[ CG(*term).doit() for term in cg_terms ] ) + state = TensorProduct( + *[ evect(j, j - d) for j, d in zip(jn, diff_list) ] ) + result.append(coeff*state) + return Add(*result) + else: + # Symbolic coupling + m_str = "m1:%d" % (len(jn) + 1) + mvals = symbols(m_str) + cg_terms = [(j1, Add(*[mvals[n - 1] for n in j1_n]), + j2, Add(*[mvals[n - 1] for n in j2_n]), + j3, Add(*[mvals[n - 1] for n in j1_n + j2_n])) for j1_n, j2_n, j1, j2, j3 in coupling_list[:-1] ] + cg_terms.append(*[(j1, Add(*[mvals[n - 1] for n in j1_n]), + j2, Add(*[mvals[n - 1] for n in j2_n]), + j, m) for j1_n, j2_n, j1, j2, j3 in [coupling_list[-1]] ]) + cg_coeff = Mul(*[CG(*cg_term) for cg_term in cg_terms]) + sum_terms = [ (m, -j, j) for j, m in zip(jn, mvals) ] + state = TensorProduct( *[ evect(j, m) for j, m in zip(jn, mvals) ] ) + return Sum(cg_coeff*state, *sum_terms) + + +def _confignum_to_difflist(config_num, diff, list_len): + # Determines configuration of diffs into list_len number of slots + diff_list = [] + for n in range(list_len): + prev_diff = diff + # Number of spots after current one + rem_spots = list_len - n - 1 + # Number of configurations of distributing diff among the remaining spots + rem_configs = binomial(diff + rem_spots - 1, diff) + while config_num >= rem_configs: + config_num -= rem_configs + diff -= 1 + rem_configs = binomial(diff + rem_spots - 1, diff) + diff_list.append(prev_diff - diff) + return diff_list diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/state.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/state.py new file mode 100644 index 0000000000000000000000000000000000000000..4ccd1ce9b9875b59a5d1293ab3026808bdc85b27 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/state.py @@ -0,0 +1,987 @@ +"""Dirac notation for states.""" + +from sympy.core.cache import cacheit +from sympy.core.containers import Tuple +from sympy.core.expr import Expr +from sympy.core.function import Function +from sympy.core.numbers import oo, equal_valued +from sympy.core.singleton import S +from sympy.functions.elementary.complexes import conjugate +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.integrals.integrals import integrate +from sympy.printing.pretty.stringpict import stringPict +from sympy.physics.quantum.qexpr import QExpr, dispatch_method +from sympy.physics.quantum.kind import KetKind, BraKind + + +__all__ = [ + 'KetBase', + 'BraBase', + 'StateBase', + 'State', + 'Ket', + 'Bra', + 'TimeDepState', + 'TimeDepBra', + 'TimeDepKet', + 'OrthogonalKet', + 'OrthogonalBra', + 'OrthogonalState', + 'Wavefunction' +] + + +#----------------------------------------------------------------------------- +# States, bras and kets. +#----------------------------------------------------------------------------- + +# ASCII brackets +_lbracket = "<" +_rbracket = ">" +_straight_bracket = "|" + + +# Unicode brackets +# MATHEMATICAL ANGLE BRACKETS +_lbracket_ucode = "\N{MATHEMATICAL LEFT ANGLE BRACKET}" +_rbracket_ucode = "\N{MATHEMATICAL RIGHT ANGLE BRACKET}" +# LIGHT VERTICAL BAR +_straight_bracket_ucode = "\N{LIGHT VERTICAL BAR}" + +# Other options for unicode printing of <, > and | for Dirac notation. + +# LEFT-POINTING ANGLE BRACKET +# _lbracket = "\u2329" +# _rbracket = "\u232A" + +# LEFT ANGLE BRACKET +# _lbracket = "\u3008" +# _rbracket = "\u3009" + +# VERTICAL LINE +# _straight_bracket = "\u007C" + + +class StateBase(QExpr): + """Abstract base class for general abstract states in quantum mechanics. + + All other state classes defined will need to inherit from this class. It + carries the basic structure for all other states such as dual, _eval_adjoint + and label. + + This is an abstract base class and you should not instantiate it directly, + instead use State. + """ + + @classmethod + def _operators_to_state(self, ops, **options): + """ Returns the eigenstate instance for the passed operators. + + This method should be overridden in subclasses. It will handle being + passed either an Operator instance or set of Operator instances. It + should return the corresponding state INSTANCE or simply raise a + NotImplementedError. See cartesian.py for an example. + """ + + raise NotImplementedError("Cannot map operators to states in this class. Method not implemented!") + + def _state_to_operators(self, op_classes, **options): + """ Returns the operators which this state instance is an eigenstate + of. + + This method should be overridden in subclasses. It will be called on + state instances and be passed the operator classes that we wish to make + into instances. The state instance will then transform the classes + appropriately, or raise a NotImplementedError if it cannot return + operator instances. See cartesian.py for examples, + """ + + raise NotImplementedError( + "Cannot map this state to operators. Method not implemented!") + + @property + def operators(self): + """Return the operator(s) that this state is an eigenstate of""" + from .operatorset import state_to_operators # import internally to avoid circular import errors + return state_to_operators(self) + + def _enumerate_state(self, num_states, **options): + raise NotImplementedError("Cannot enumerate this state!") + + def _represent_default_basis(self, **options): + return self._represent(basis=self.operators) + + def _apply_operator(self, op, **options): + return None + + #------------------------------------------------------------------------- + # Dagger/dual + #------------------------------------------------------------------------- + + @property + def dual(self): + """Return the dual state of this one.""" + return self.dual_class()._new_rawargs(self.hilbert_space, *self.args) + + @classmethod + def dual_class(self): + """Return the class used to construct the dual.""" + raise NotImplementedError( + 'dual_class must be implemented in a subclass' + ) + + def _eval_adjoint(self): + """Compute the dagger of this state using the dual.""" + return self.dual + + #------------------------------------------------------------------------- + # Printing + #------------------------------------------------------------------------- + + def _pretty_brackets(self, height, use_unicode=True): + # Return pretty printed brackets for the state + # Ideally, this could be done by pform.parens but it does not support the angled < and > + + # Setup for unicode vs ascii + if use_unicode: + lbracket, rbracket = getattr(self, 'lbracket_ucode', ""), getattr(self, 'rbracket_ucode', "") + slash, bslash, vert = '\N{BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT}', \ + '\N{BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT}', \ + '\N{BOX DRAWINGS LIGHT VERTICAL}' + else: + lbracket, rbracket = getattr(self, 'lbracket', ""), getattr(self, 'rbracket', "") + slash, bslash, vert = '/', '\\', '|' + + # If height is 1, just return brackets + if height == 1: + return stringPict(lbracket), stringPict(rbracket) + # Make height even + height += (height % 2) + + brackets = [] + for bracket in lbracket, rbracket: + # Create left bracket + if bracket in {_lbracket, _lbracket_ucode}: + bracket_args = [ ' ' * (height//2 - i - 1) + + slash for i in range(height // 2)] + bracket_args.extend( + [' ' * i + bslash for i in range(height // 2)]) + # Create right bracket + elif bracket in {_rbracket, _rbracket_ucode}: + bracket_args = [ ' ' * i + bslash for i in range(height // 2)] + bracket_args.extend([ ' ' * ( + height//2 - i - 1) + slash for i in range(height // 2)]) + # Create straight bracket + elif bracket in {_straight_bracket, _straight_bracket_ucode}: + bracket_args = [vert] * height + else: + raise ValueError(bracket) + brackets.append( + stringPict('\n'.join(bracket_args), baseline=height//2)) + return brackets + + def _sympystr(self, printer, *args): + contents = self._print_contents(printer, *args) + return '%s%s%s' % (getattr(self, 'lbracket', ""), contents, getattr(self, 'rbracket', "")) + + def _pretty(self, printer, *args): + from sympy.printing.pretty.stringpict import prettyForm + # Get brackets + pform = self._print_contents_pretty(printer, *args) + lbracket, rbracket = self._pretty_brackets( + pform.height(), printer._use_unicode) + # Put together state + pform = prettyForm(*pform.left(lbracket)) + pform = prettyForm(*pform.right(rbracket)) + return pform + + def _latex(self, printer, *args): + contents = self._print_contents_latex(printer, *args) + # The extra {} brackets are needed to get matplotlib's latex + # rendered to render this properly. + return '{%s%s%s}' % (getattr(self, 'lbracket_latex', ""), contents, getattr(self, 'rbracket_latex', "")) + + +class KetBase(StateBase): + """Base class for Kets. + + This class defines the dual property and the brackets for printing. This is + an abstract base class and you should not instantiate it directly, instead + use Ket. + """ + + kind = KetKind + + lbracket = _straight_bracket + rbracket = _rbracket + lbracket_ucode = _straight_bracket_ucode + rbracket_ucode = _rbracket_ucode + lbracket_latex = r'\left|' + rbracket_latex = r'\right\rangle ' + + @classmethod + def default_args(self): + return ("psi",) + + @classmethod + def dual_class(self): + return BraBase + + #------------------------------------------------------------------------- + # _eval_* methods + #------------------------------------------------------------------------- + + def _eval_innerproduct(self, bra, **hints): + """Evaluate the inner product between this ket and a bra. + + This is called to compute , where the ket is ``self``. + + This method will dispatch to sub-methods having the format:: + + ``def _eval_innerproduct_BraClass(self, **hints):`` + + Subclasses should define these methods (one for each BraClass) to + teach the ket how to take inner products with bras. + """ + return dispatch_method(self, '_eval_innerproduct', bra, **hints) + + def _apply_from_right_to(self, op, **options): + """Apply an Operator to this Ket as Operator*Ket + + This method will dispatch to methods having the format:: + + ``def _apply_from_right_to_OperatorName(op, **options):`` + + Subclasses should define these methods (one for each OperatorName) to + teach the Ket how to implement OperatorName*Ket + + Parameters + ========== + + op : Operator + The Operator that is acting on the Ket as op*Ket + options : dict + A dict of key/value pairs that control how the operator is applied + to the Ket. + """ + return dispatch_method(self, '_apply_from_right_to', op, **options) + + +class BraBase(StateBase): + """Base class for Bras. + + This class defines the dual property and the brackets for printing. This + is an abstract base class and you should not instantiate it directly, + instead use Bra. + """ + + kind = BraKind + + lbracket = _lbracket + rbracket = _straight_bracket + lbracket_ucode = _lbracket_ucode + rbracket_ucode = _straight_bracket_ucode + lbracket_latex = r'\left\langle ' + rbracket_latex = r'\right|' + + @classmethod + def _operators_to_state(self, ops, **options): + state = self.dual_class()._operators_to_state(ops, **options) + return state.dual + + def _state_to_operators(self, op_classes, **options): + return self.dual._state_to_operators(op_classes, **options) + + def _enumerate_state(self, num_states, **options): + dual_states = self.dual._enumerate_state(num_states, **options) + return [x.dual for x in dual_states] + + @classmethod + def default_args(self): + return self.dual_class().default_args() + + @classmethod + def dual_class(self): + return KetBase + + def _represent(self, **options): + """A default represent that uses the Ket's version.""" + from sympy.physics.quantum.dagger import Dagger + return Dagger(self.dual._represent(**options)) + + +class State(StateBase): + """General abstract quantum state used as a base class for Ket and Bra.""" + pass + + +class Ket(State, KetBase): + """A general time-independent Ket in quantum mechanics. + + Inherits from State and KetBase. This class should be used as the base + class for all physical, time-independent Kets in a system. This class + and its subclasses will be the main classes that users will use for + expressing Kets in Dirac notation [1]_. + + Parameters + ========== + + args : tuple + The list of numbers or parameters that uniquely specify the + ket. This will usually be its symbol or its quantum numbers. For + time-dependent state, this will include the time. + + Examples + ======== + + Create a simple Ket and looking at its properties:: + + >>> from sympy.physics.quantum import Ket + >>> from sympy import symbols, I + >>> k = Ket('psi') + >>> k + |psi> + >>> k.hilbert_space + H + >>> k.is_commutative + False + >>> k.label + (psi,) + + Ket's know about their associated bra:: + + >>> k.dual + >> k.dual_class() + + + Take a linear combination of two kets:: + + >>> k0 = Ket(0) + >>> k1 = Ket(1) + >>> 2*I*k0 - 4*k1 + 2*I*|0> - 4*|1> + + Compound labels are passed as tuples:: + + >>> n, m = symbols('n,m') + >>> k = Ket(n,m) + >>> k + |nm> + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Bra-ket_notation + """ + + @classmethod + def dual_class(self): + return Bra + + +class Bra(State, BraBase): + """A general time-independent Bra in quantum mechanics. + + Inherits from State and BraBase. A Bra is the dual of a Ket [1]_. This + class and its subclasses will be the main classes that users will use for + expressing Bras in Dirac notation. + + Parameters + ========== + + args : tuple + The list of numbers or parameters that uniquely specify the + ket. This will usually be its symbol or its quantum numbers. For + time-dependent state, this will include the time. + + Examples + ======== + + Create a simple Bra and look at its properties:: + + >>> from sympy.physics.quantum import Bra + >>> from sympy import symbols, I + >>> b = Bra('psi') + >>> b + >> b.hilbert_space + H + >>> b.is_commutative + False + + Bra's know about their dual Ket's:: + + >>> b.dual + |psi> + >>> b.dual_class() + + + Like Kets, Bras can have compound labels and be manipulated in a similar + manner:: + + >>> n, m = symbols('n,m') + >>> b = Bra(n,m) - I*Bra(m,n) + >>> b + -I*>> b.subs(n,m) + >> from sympy.physics.quantum import TimeDepKet + >>> k = TimeDepKet('psi', 't') + >>> k + |psi;t> + >>> k.time + t + >>> k.label + (psi,) + >>> k.hilbert_space + H + + TimeDepKets know about their dual bra:: + + >>> k.dual + >> k.dual_class() + + """ + + @classmethod + def dual_class(self): + return TimeDepBra + + +class TimeDepBra(TimeDepState, BraBase): + """General time-dependent Bra in quantum mechanics. + + This inherits from TimeDepState and BraBase and is the main class that + should be used for Bras that vary with time. Its dual is a TimeDepBra. + + Parameters + ========== + + args : tuple + The list of numbers or parameters that uniquely specify the ket. This + will usually be its symbol or its quantum numbers. For time-dependent + state, this will include the time as the final argument. + + Examples + ======== + + >>> from sympy.physics.quantum import TimeDepBra + >>> b = TimeDepBra('psi', 't') + >>> b + >> b.time + t + >>> b.label + (psi,) + >>> b.hilbert_space + H + >>> b.dual + |psi;t> + """ + + @classmethod + def dual_class(self): + return TimeDepKet + + +class OrthogonalState(State): + """General abstract quantum state used as a base class for Ket and Bra.""" + pass + +class OrthogonalKet(OrthogonalState, KetBase): + """Orthogonal Ket in quantum mechanics. + + The inner product of two states with different labels will give zero, + states with the same label will give one. + + >>> from sympy.physics.quantum import OrthogonalBra, OrthogonalKet + >>> from sympy.abc import m, n + >>> (OrthogonalBra(n)*OrthogonalKet(n)).doit() + 1 + >>> (OrthogonalBra(n)*OrthogonalKet(n+1)).doit() + 0 + >>> (OrthogonalBra(n)*OrthogonalKet(m)).doit() + + """ + + @classmethod + def dual_class(self): + return OrthogonalBra + + def _eval_innerproduct(self, bra, **hints): + + if len(self.args) != len(bra.args): + raise ValueError('Cannot multiply a ket that has a different number of labels.') + + for arg, bra_arg in zip(self.args, bra.args): + diff = arg - bra_arg + diff = diff.expand() + + is_zero = diff.is_zero + + if is_zero is False: + return S.Zero # i.e. Integer(0) + + if is_zero is None: + return None + + return S.One # i.e. Integer(1) + + +class OrthogonalBra(OrthogonalState, BraBase): + """Orthogonal Bra in quantum mechanics. + """ + + @classmethod + def dual_class(self): + return OrthogonalKet + + +class Wavefunction(Function): + """Class for representations in continuous bases + + This class takes an expression and coordinates in its constructor. It can + be used to easily calculate normalizations and probabilities. + + Parameters + ========== + + expr : Expr + The expression representing the functional form of the w.f. + + coords : Symbol or tuple + The coordinates to be integrated over, and their bounds + + Examples + ======== + + Particle in a box, specifying bounds in the more primitive way of using + Piecewise: + + >>> from sympy import Symbol, Piecewise, pi, N + >>> from sympy.functions import sqrt, sin + >>> from sympy.physics.quantum.state import Wavefunction + >>> x = Symbol('x', real=True) + >>> n = 1 + >>> L = 1 + >>> g = Piecewise((0, x < 0), (0, x > L), (sqrt(2//L)*sin(n*pi*x/L), True)) + >>> f = Wavefunction(g, x) + >>> f.norm + 1 + >>> f.is_normalized + True + >>> p = f.prob() + >>> p(0) + 0 + >>> p(L) + 0 + >>> p(0.5) + 2 + >>> p(0.85*L) + 2*sin(0.85*pi)**2 + >>> N(p(0.85*L)) + 0.412214747707527 + + Additionally, you can specify the bounds of the function and the indices in + a more compact way: + + >>> from sympy import symbols, pi, diff + >>> from sympy.functions import sqrt, sin + >>> from sympy.physics.quantum.state import Wavefunction + >>> x, L = symbols('x,L', positive=True) + >>> n = symbols('n', integer=True, positive=True) + >>> g = sqrt(2/L)*sin(n*pi*x/L) + >>> f = Wavefunction(g, (x, 0, L)) + >>> f.norm + 1 + >>> f(L+1) + 0 + >>> f(L-1) + sqrt(2)*sin(pi*n*(L - 1)/L)/sqrt(L) + >>> f(-1) + 0 + >>> f(0.85) + sqrt(2)*sin(0.85*pi*n/L)/sqrt(L) + >>> f(0.85, n=1, L=1) + sqrt(2)*sin(0.85*pi) + >>> f.is_commutative + False + + All arguments are automatically sympified, so you can define the variables + as strings rather than symbols: + + >>> expr = x**2 + >>> f = Wavefunction(expr, 'x') + >>> type(f.variables[0]) + + + Derivatives of Wavefunctions will return Wavefunctions: + + >>> diff(f, x) + Wavefunction(2*x, x) + + """ + + #Any passed tuples for coordinates and their bounds need to be + #converted to Tuples before Function's constructor is called, to + #avoid errors from calling is_Float in the constructor + def __new__(cls, *args, **options): + new_args = [None for i in args] + ct = 0 + for arg in args: + if isinstance(arg, tuple): + new_args[ct] = Tuple(*arg) + else: + new_args[ct] = arg + ct += 1 + + return super().__new__(cls, *new_args, **options) + + def __call__(self, *args, **options): + var = self.variables + + if len(args) != len(var): + raise NotImplementedError( + "Incorrect number of arguments to function!") + + ct = 0 + #If the passed value is outside the specified bounds, return 0 + for v in var: + lower, upper = self.limits[v] + + #Do the comparison to limits only if the passed symbol is actually + #a symbol present in the limits; + #Had problems with a comparison of x > L + if isinstance(args[ct], Expr) and \ + not (lower in args[ct].free_symbols + or upper in args[ct].free_symbols): + continue + + if (args[ct] < lower) == True or (args[ct] > upper) == True: + return S.Zero + + ct += 1 + + expr = self.expr + + #Allows user to make a call like f(2, 4, m=1, n=1) + for symbol in list(expr.free_symbols): + if str(symbol) in options.keys(): + val = options[str(symbol)] + expr = expr.subs(symbol, val) + + return expr.subs(zip(var, args)) + + def _eval_derivative(self, symbol): + expr = self.expr + deriv = expr._eval_derivative(symbol) + + return Wavefunction(deriv, *self.args[1:]) + + def _eval_conjugate(self): + return Wavefunction(conjugate(self.expr), *self.args[1:]) + + def _eval_transpose(self): + return self + + @property + def is_commutative(self): + """ + Override Function's is_commutative so that order is preserved in + represented expressions + """ + return False + + @classmethod + def eval(self, *args): + return None + + @property + def variables(self): + """ + Return the coordinates which the wavefunction depends on + + Examples + ======== + + >>> from sympy.physics.quantum.state import Wavefunction + >>> from sympy import symbols + >>> x,y = symbols('x,y') + >>> f = Wavefunction(x*y, x, y) + >>> f.variables + (x, y) + >>> g = Wavefunction(x*y, x) + >>> g.variables + (x,) + + """ + var = [g[0] if isinstance(g, Tuple) else g for g in self._args[1:]] + return tuple(var) + + @property + def limits(self): + """ + Return the limits of the coordinates which the w.f. depends on If no + limits are specified, defaults to ``(-oo, oo)``. + + Examples + ======== + + >>> from sympy.physics.quantum.state import Wavefunction + >>> from sympy import symbols + >>> x, y = symbols('x, y') + >>> f = Wavefunction(x**2, (x, 0, 1)) + >>> f.limits + {x: (0, 1)} + >>> f = Wavefunction(x**2, x) + >>> f.limits + {x: (-oo, oo)} + >>> f = Wavefunction(x**2 + y**2, x, (y, -1, 2)) + >>> f.limits + {x: (-oo, oo), y: (-1, 2)} + + """ + limits = [(g[1], g[2]) if isinstance(g, Tuple) else (-oo, oo) + for g in self._args[1:]] + return dict(zip(self.variables, tuple(limits))) + + @property + def expr(self): + """ + Return the expression which is the functional form of the Wavefunction + + Examples + ======== + + >>> from sympy.physics.quantum.state import Wavefunction + >>> from sympy import symbols + >>> x, y = symbols('x, y') + >>> f = Wavefunction(x**2, x) + >>> f.expr + x**2 + + """ + return self._args[0] + + @property + def is_normalized(self): + """ + Returns true if the Wavefunction is properly normalized + + Examples + ======== + + >>> from sympy import symbols, pi + >>> from sympy.functions import sqrt, sin + >>> from sympy.physics.quantum.state import Wavefunction + >>> x, L = symbols('x,L', positive=True) + >>> n = symbols('n', integer=True, positive=True) + >>> g = sqrt(2/L)*sin(n*pi*x/L) + >>> f = Wavefunction(g, (x, 0, L)) + >>> f.is_normalized + True + + """ + + return equal_valued(self.norm, 1) + + @property # type: ignore + @cacheit + def norm(self): + """ + Return the normalization of the specified functional form. + + This function integrates over the coordinates of the Wavefunction, with + the bounds specified. + + Examples + ======== + + >>> from sympy import symbols, pi + >>> from sympy.functions import sqrt, sin + >>> from sympy.physics.quantum.state import Wavefunction + >>> x, L = symbols('x,L', positive=True) + >>> n = symbols('n', integer=True, positive=True) + >>> g = sqrt(2/L)*sin(n*pi*x/L) + >>> f = Wavefunction(g, (x, 0, L)) + >>> f.norm + 1 + >>> g = sin(n*pi*x/L) + >>> f = Wavefunction(g, (x, 0, L)) + >>> f.norm + sqrt(2)*sqrt(L)/2 + + """ + + exp = self.expr*conjugate(self.expr) + var = self.variables + limits = self.limits + + for v in var: + curr_limits = limits[v] + exp = integrate(exp, (v, curr_limits[0], curr_limits[1])) + + return sqrt(exp) + + def normalize(self): + """ + Return a normalized version of the Wavefunction + + Examples + ======== + + >>> from sympy import symbols, pi + >>> from sympy.functions import sin + >>> from sympy.physics.quantum.state import Wavefunction + >>> x = symbols('x', real=True) + >>> L = symbols('L', positive=True) + >>> n = symbols('n', integer=True, positive=True) + >>> g = sin(n*pi*x/L) + >>> f = Wavefunction(g, (x, 0, L)) + >>> f.normalize() + Wavefunction(sqrt(2)*sin(pi*n*x/L)/sqrt(L), (x, 0, L)) + + """ + const = self.norm + + if const is oo: + raise NotImplementedError("The function is not normalizable!") + else: + return Wavefunction((const)**(-1)*self.expr, *self.args[1:]) + + def prob(self): + r""" + Return the absolute magnitude of the w.f., `|\psi(x)|^2` + + Examples + ======== + + >>> from sympy import symbols, pi + >>> from sympy.functions import sin + >>> from sympy.physics.quantum.state import Wavefunction + >>> x, L = symbols('x,L', real=True) + >>> n = symbols('n', integer=True) + >>> g = sin(n*pi*x/L) + >>> f = Wavefunction(g, (x, 0, L)) + >>> f.prob() + Wavefunction(sin(pi*n*x/L)**2, x) + + """ + + return Wavefunction(self.expr*conjugate(self.expr), *self.variables) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tensorproduct.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tensorproduct.py new file mode 100644 index 0000000000000000000000000000000000000000..058b3459227e5a020e2d0397fc66f56a2f917293 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tensorproduct.py @@ -0,0 +1,363 @@ +"""Abstract tensor product.""" + +from sympy.core.add import Add +from sympy.core.expr import Expr +from sympy.core.kind import KindDispatcher +from sympy.core.mul import Mul +from sympy.core.power import Pow +from sympy.core.sympify import sympify +from sympy.matrices.dense import DenseMatrix as Matrix +from sympy.matrices.immutable import ImmutableDenseMatrix as ImmutableMatrix +from sympy.printing.pretty.stringpict import prettyForm +from sympy.utilities.exceptions import sympy_deprecation_warning + +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.kind import ( + KetKind, _KetKind, + BraKind, _BraKind, + OperatorKind, _OperatorKind +) +from sympy.physics.quantum.matrixutils import ( + numpy_ndarray, + scipy_sparse_matrix, + matrix_tensor_product +) +from sympy.physics.quantum.state import Ket, Bra +from sympy.physics.quantum.trace import Tr + + +__all__ = [ + 'TensorProduct', + 'tensor_product_simp' +] + +#----------------------------------------------------------------------------- +# Tensor product +#----------------------------------------------------------------------------- + +_combined_printing = False + + +def combined_tensor_printing(combined): + """Set flag controlling whether tensor products of states should be + printed as a combined bra/ket or as an explicit tensor product of different + bra/kets. This is a global setting for all TensorProduct class instances. + + Parameters + ---------- + combine : bool + When true, tensor product states are combined into one ket/bra, and + when false explicit tensor product notation is used between each + ket/bra. + """ + global _combined_printing + _combined_printing = combined + + +class TensorProduct(Expr): + """The tensor product of two or more arguments. + + For matrices, this uses ``matrix_tensor_product`` to compute the Kronecker + or tensor product matrix. For other objects a symbolic ``TensorProduct`` + instance is returned. The tensor product is a non-commutative + multiplication that is used primarily with operators and states in quantum + mechanics. + + Currently, the tensor product distinguishes between commutative and + non-commutative arguments. Commutative arguments are assumed to be scalars + and are pulled out in front of the ``TensorProduct``. Non-commutative + arguments remain in the resulting ``TensorProduct``. + + Parameters + ========== + + args : tuple + A sequence of the objects to take the tensor product of. + + Examples + ======== + + Start with a simple tensor product of SymPy matrices:: + + >>> from sympy import Matrix + >>> from sympy.physics.quantum import TensorProduct + + >>> m1 = Matrix([[1,2],[3,4]]) + >>> m2 = Matrix([[1,0],[0,1]]) + >>> TensorProduct(m1, m2) + Matrix([ + [1, 0, 2, 0], + [0, 1, 0, 2], + [3, 0, 4, 0], + [0, 3, 0, 4]]) + >>> TensorProduct(m2, m1) + Matrix([ + [1, 2, 0, 0], + [3, 4, 0, 0], + [0, 0, 1, 2], + [0, 0, 3, 4]]) + + We can also construct tensor products of non-commutative symbols: + + >>> from sympy import Symbol + >>> A = Symbol('A',commutative=False) + >>> B = Symbol('B',commutative=False) + >>> tp = TensorProduct(A, B) + >>> tp + AxB + + We can take the dagger of a tensor product (note the order does NOT reverse + like the dagger of a normal product): + + >>> from sympy.physics.quantum import Dagger + >>> Dagger(tp) + Dagger(A)xDagger(B) + + Expand can be used to distribute a tensor product across addition: + + >>> C = Symbol('C',commutative=False) + >>> tp = TensorProduct(A+B,C) + >>> tp + (A + B)xC + >>> tp.expand(tensorproduct=True) + AxC + BxC + """ + is_commutative = False + + _kind_dispatcher = KindDispatcher("TensorProduct_kind_dispatcher", commutative=True) + + @property + def kind(self): + """Calculate the kind of a tensor product by looking at its children.""" + arg_kinds = (a.kind for a in self.args) + return self._kind_dispatcher(*arg_kinds) + + def __new__(cls, *args): + if isinstance(args[0], (Matrix, ImmutableMatrix, numpy_ndarray, + scipy_sparse_matrix)): + return matrix_tensor_product(*args) + c_part, new_args = cls.flatten(sympify(args)) + c_part = Mul(*c_part) + if len(new_args) == 0: + return c_part + elif len(new_args) == 1: + return c_part * new_args[0] + else: + tp = Expr.__new__(cls, *new_args) + return c_part * tp + + @classmethod + def flatten(cls, args): + # TODO: disallow nested TensorProducts. + c_part = [] + nc_parts = [] + for arg in args: + cp, ncp = arg.args_cnc() + c_part.extend(list(cp)) + nc_parts.append(Mul._from_args(ncp)) + return c_part, nc_parts + + def _eval_adjoint(self): + return TensorProduct(*[Dagger(i) for i in self.args]) + + def _eval_rewrite(self, rule, args, **hints): + return TensorProduct(*args).expand(tensorproduct=True) + + def _sympystr(self, printer, *args): + length = len(self.args) + s = '' + for i in range(length): + if isinstance(self.args[i], (Add, Pow, Mul)): + s = s + '(' + s = s + printer._print(self.args[i]) + if isinstance(self.args[i], (Add, Pow, Mul)): + s = s + ')' + if i != length - 1: + s = s + 'x' + return s + + def _pretty(self, printer, *args): + + if (_combined_printing and + (all(isinstance(arg, Ket) for arg in self.args) or + all(isinstance(arg, Bra) for arg in self.args))): + + length = len(self.args) + pform = printer._print('', *args) + for i in range(length): + next_pform = printer._print('', *args) + length_i = len(self.args[i].args) + for j in range(length_i): + part_pform = printer._print(self.args[i].args[j], *args) + next_pform = prettyForm(*next_pform.right(part_pform)) + if j != length_i - 1: + next_pform = prettyForm(*next_pform.right(', ')) + + if len(self.args[i].args) > 1: + next_pform = prettyForm( + *next_pform.parens(left='{', right='}')) + pform = prettyForm(*pform.right(next_pform)) + if i != length - 1: + pform = prettyForm(*pform.right(',' + ' ')) + + pform = prettyForm(*pform.left(self.args[0].lbracket)) + pform = prettyForm(*pform.right(self.args[0].rbracket)) + return pform + + length = len(self.args) + pform = printer._print('', *args) + for i in range(length): + next_pform = printer._print(self.args[i], *args) + if isinstance(self.args[i], (Add, Mul)): + next_pform = prettyForm( + *next_pform.parens(left='(', right=')') + ) + pform = prettyForm(*pform.right(next_pform)) + if i != length - 1: + if printer._use_unicode: + pform = prettyForm(*pform.right('\N{N-ARY CIRCLED TIMES OPERATOR}' + ' ')) + else: + pform = prettyForm(*pform.right('x' + ' ')) + return pform + + def _latex(self, printer, *args): + + if (_combined_printing and + (all(isinstance(arg, Ket) for arg in self.args) or + all(isinstance(arg, Bra) for arg in self.args))): + + def _label_wrap(label, nlabels): + return label if nlabels == 1 else r"\left\{%s\right\}" % label + + s = r", ".join([_label_wrap(arg._print_label_latex(printer, *args), + len(arg.args)) for arg in self.args]) + + return r"{%s%s%s}" % (self.args[0].lbracket_latex, s, + self.args[0].rbracket_latex) + + length = len(self.args) + s = '' + for i in range(length): + if isinstance(self.args[i], (Add, Mul)): + s = s + '\\left(' + # The extra {} brackets are needed to get matplotlib's latex + # rendered to render this properly. + s = s + '{' + printer._print(self.args[i], *args) + '}' + if isinstance(self.args[i], (Add, Mul)): + s = s + '\\right)' + if i != length - 1: + s = s + '\\otimes ' + return s + + def doit(self, **hints): + return TensorProduct(*[item.doit(**hints) for item in self.args]) + + def _eval_expand_tensorproduct(self, **hints): + """Distribute TensorProducts across addition.""" + args = self.args + add_args = [] + for i in range(len(args)): + if isinstance(args[i], Add): + for aa in args[i].args: + tp = TensorProduct(*args[:i] + (aa,) + args[i + 1:]) + c_part, nc_part = tp.args_cnc() + # Check for TensorProduct object: is the one object in nc_part, if any: + # (Note: any other object type to be expanded must be added here) + if len(nc_part) == 1 and isinstance(nc_part[0], TensorProduct): + nc_part = (nc_part[0]._eval_expand_tensorproduct(), ) + add_args.append(Mul(*c_part)*Mul(*nc_part)) + break + + if add_args: + return Add(*add_args) + else: + return self + + def _eval_trace(self, **kwargs): + indices = kwargs.get('indices', None) + exp = self + + if indices is None or len(indices) == 0: + return Mul(*[Tr(arg).doit() for arg in exp.args]) + else: + return Mul(*[Tr(value).doit() if idx in indices else value + for idx, value in enumerate(exp.args)]) + + +def tensor_product_simp_Mul(e): + """Simplify a Mul with tensor products. + + .. deprecated:: 1.14. + The transformations applied by this function are not done automatically + when tensor products are combined. + + Originally, the main use of this function is to simplify a ``Mul`` of + ``TensorProduct``s to a ``TensorProduct`` of ``Muls``. + """ + sympy_deprecation_warning( + """ + tensor_product_simp_Mul has been deprecated. The transformations + performed by this function are now done automatically when + tensor products are multiplied. + """, + deprecated_since_version="1.14", + active_deprecations_target='deprecated-tensorproduct-simp' + ) + return e + +def tensor_product_simp_Pow(e): + """Evaluates ``Pow`` expressions whose base is ``TensorProduct`` + + .. deprecated:: 1.14. + The transformations applied by this function are not done automatically + when tensor products are combined. + """ + sympy_deprecation_warning( + """ + tensor_product_simp_Pow has been deprecated. The transformations + performed by this function are now done automatically when + tensor products are exponentiated. + """, + deprecated_since_version="1.14", + active_deprecations_target='deprecated-tensorproduct-simp' + ) + return e + + +def tensor_product_simp(e, **hints): + """Try to simplify and combine tensor products. + + .. deprecated:: 1.14. + The transformations applied by this function are not done automatically + when tensor products are combined. + + Originally, this function tried to pull expressions inside of ``TensorProducts``. + It only worked for relatively simple cases where the products have + only scalars, raw ``TensorProducts``, not ``Add``, ``Pow``, ``Commutators`` + of ``TensorProducts``. + """ + sympy_deprecation_warning( + """ + tensor_product_simp has been deprecated. The transformations + performed by this function are now done automatically when + tensor products are combined. + """, + deprecated_since_version="1.14", + active_deprecations_target='deprecated-tensorproduct-simp' + ) + return e + + +@TensorProduct._kind_dispatcher.register(_OperatorKind, _OperatorKind) +def find_op_kind(e1, e2): + return OperatorKind + + +@TensorProduct._kind_dispatcher.register(_KetKind, _KetKind) +def find_ket_kind(e1, e2): + return KetKind + + +@TensorProduct._kind_dispatcher.register(_BraKind, _BraKind) +def find_bra_kind(e1, e2): + return BraKind diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_anticommutator.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_anticommutator.py new file mode 100644 index 0000000000000000000000000000000000000000..0e6b6cbc50651742fcbbbe6adce3f20dfadc2ec5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_anticommutator.py @@ -0,0 +1,56 @@ +from sympy.core.numbers import Integer +from sympy.core.symbol import symbols + +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.anticommutator import AntiCommutator as AComm +from sympy.physics.quantum.operator import Operator + + +a, b, c = symbols('a,b,c') +A, B, C, D = symbols('A,B,C,D', commutative=False) + + +def test_anticommutator(): + ac = AComm(A, B) + assert isinstance(ac, AComm) + assert ac.is_commutative is False + assert ac.subs(A, C) == AComm(C, B) + + +def test_commutator_identities(): + assert AComm(a*A, b*B) == a*b*AComm(A, B) + assert AComm(A, A) == 2*A**2 + assert AComm(A, B) == AComm(B, A) + assert AComm(a, b) == 2*a*b + assert AComm(A, B).doit() == A*B + B*A + + +def test_anticommutator_dagger(): + assert Dagger(AComm(A, B)) == AComm(Dagger(A), Dagger(B)) + + +class Foo(Operator): + + def _eval_anticommutator_Bar(self, bar): + return Integer(0) + + +class Bar(Operator): + pass + + +class Tam(Operator): + + def _eval_anticommutator_Foo(self, foo): + return Integer(1) + + +def test_eval_commutator(): + F = Foo('F') + B = Bar('B') + T = Tam('T') + assert AComm(F, B).doit() == 0 + assert AComm(B, F).doit() == 0 + assert AComm(F, T).doit() == 1 + assert AComm(T, F).doit() == 1 + assert AComm(B, T).doit() == B*T + T*B diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_boson.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_boson.py new file mode 100644 index 0000000000000000000000000000000000000000..cd8dab745bede8b1c70303917dae81146fc03395 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_boson.py @@ -0,0 +1,50 @@ +from math import prod + +from sympy.core.numbers import Rational +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.physics.quantum import Dagger, Commutator, qapply +from sympy.physics.quantum.boson import BosonOp +from sympy.physics.quantum.boson import ( + BosonFockKet, BosonFockBra, BosonCoherentKet, BosonCoherentBra) + + +def test_bosonoperator(): + a = BosonOp('a') + b = BosonOp('b') + + assert isinstance(a, BosonOp) + assert isinstance(Dagger(a), BosonOp) + + assert a.is_annihilation + assert not Dagger(a).is_annihilation + + assert BosonOp("a") == BosonOp("a", True) + assert BosonOp("a") != BosonOp("c") + assert BosonOp("a", True) != BosonOp("a", False) + + assert Commutator(a, Dagger(a)).doit() == 1 + + assert Commutator(a, Dagger(b)).doit() == a * Dagger(b) - Dagger(b) * a + + assert Dagger(exp(a)) == exp(Dagger(a)) + + +def test_boson_states(): + a = BosonOp("a") + + # Fock states + n = 3 + assert (BosonFockBra(0) * BosonFockKet(1)).doit() == 0 + assert (BosonFockBra(1) * BosonFockKet(1)).doit() == 1 + assert qapply(BosonFockBra(n) * Dagger(a)**n * BosonFockKet(0)) \ + == sqrt(prod(range(1, n+1))) + + # Coherent states + alpha1, alpha2 = 1.2, 4.3 + assert (BosonCoherentBra(alpha1) * BosonCoherentKet(alpha1)).doit() == 1 + assert (BosonCoherentBra(alpha2) * BosonCoherentKet(alpha2)).doit() == 1 + assert abs((BosonCoherentBra(alpha1) * BosonCoherentKet(alpha2)).doit() - + exp((alpha1 - alpha2) ** 2 * Rational(-1, 2))) < 1e-12 + assert qapply(a * BosonCoherentKet(alpha1)) == \ + alpha1 * BosonCoherentKet(alpha1) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_cartesian.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_cartesian.py new file mode 100644 index 0000000000000000000000000000000000000000..f1dd435fab68c9c71ac3602bc4c53847cbe39d57 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_cartesian.py @@ -0,0 +1,113 @@ +"""Tests for cartesian.py""" + +from sympy.core.numbers import (I, pi) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.special.delta_functions import DiracDelta +from sympy.sets.sets import Interval +from sympy.testing.pytest import XFAIL + +from sympy.physics.quantum import qapply, represent, L2, Dagger +from sympy.physics.quantum import Commutator, hbar +from sympy.physics.quantum.cartesian import ( + XOp, YOp, ZOp, PxOp, X, Y, Z, Px, XKet, XBra, PxKet, PxBra, + PositionKet3D, PositionBra3D +) +from sympy.physics.quantum.operator import DifferentialOperator + +x, y, z, x_1, x_2, x_3, y_1, z_1 = symbols('x,y,z,x_1,x_2,x_3,y_1,z_1') +px, py, px_1, px_2 = symbols('px py px_1 px_2') + + +def test_x(): + assert X.hilbert_space == L2(Interval(S.NegativeInfinity, S.Infinity)) + assert Commutator(X, Px).doit() == I*hbar + assert qapply(X*XKet(x)) == x*XKet(x) + assert XKet(x).dual_class() == XBra + assert XBra(x).dual_class() == XKet + assert (Dagger(XKet(y))*XKet(x)).doit() == DiracDelta(x - y) + assert (PxBra(px)*XKet(x)).doit() == \ + exp(-I*x*px/hbar)/sqrt(2*pi*hbar) + assert represent(XKet(x)) == DiracDelta(x - x_1) + assert represent(XBra(x)) == DiracDelta(-x + x_1) + assert XBra(x).position == x + assert represent(XOp()*XKet()) == x*DiracDelta(x - x_2) + assert represent(XBra("y")*XKet()) == DiracDelta(x - y) + assert represent( + XKet()*XBra()) == DiracDelta(x - x_2) * DiracDelta(x_1 - x) + + rep_p = represent(XOp(), basis=PxOp) + assert rep_p == hbar*I*DiracDelta(px_1 - px_2)*DifferentialOperator(px_1) + assert rep_p == represent(XOp(), basis=PxOp()) + assert rep_p == represent(XOp(), basis=PxKet) + assert rep_p == represent(XOp(), basis=PxKet()) + + assert represent(XOp()*PxKet(), basis=PxKet) == \ + hbar*I*DiracDelta(px - px_2)*DifferentialOperator(px) + + +@XFAIL +def _text_x_broken(): + # represent has some broken logic that is relying in particular + # forms of input, rather than a full and proper handling of + # all valid quantum expressions. Marking this test as XFAIL until + # we can refactor represent. + assert represent(XOp()*XKet()*XBra('y')) == \ + x*DiracDelta(x - x_3)*DiracDelta(x_1 - y) + + +def test_p(): + assert Px.hilbert_space == L2(Interval(S.NegativeInfinity, S.Infinity)) + assert qapply(Px*PxKet(px)) == px*PxKet(px) + assert PxKet(px).dual_class() == PxBra + assert PxBra(x).dual_class() == PxKet + assert (Dagger(PxKet(py))*PxKet(px)).doit() == DiracDelta(px - py) + assert (XBra(x)*PxKet(px)).doit() == \ + exp(I*x*px/hbar)/sqrt(2*pi*hbar) + assert represent(PxKet(px)) == DiracDelta(px - px_1) + + rep_x = represent(PxOp(), basis=XOp) + assert rep_x == -hbar*I*DiracDelta(x_1 - x_2)*DifferentialOperator(x_1) + assert rep_x == represent(PxOp(), basis=XOp()) + assert rep_x == represent(PxOp(), basis=XKet) + assert rep_x == represent(PxOp(), basis=XKet()) + + assert represent(PxOp()*XKet(), basis=XKet) == \ + -hbar*I*DiracDelta(x - x_2)*DifferentialOperator(x) + assert represent(XBra("y")*PxOp()*XKet(), basis=XKet) == \ + -hbar*I*DiracDelta(x - y)*DifferentialOperator(x) + + +def test_3dpos(): + assert Y.hilbert_space == L2(Interval(S.NegativeInfinity, S.Infinity)) + assert Z.hilbert_space == L2(Interval(S.NegativeInfinity, S.Infinity)) + + test_ket = PositionKet3D(x, y, z) + assert qapply(X*test_ket) == x*test_ket + assert qapply(Y*test_ket) == y*test_ket + assert qapply(Z*test_ket) == z*test_ket + assert qapply(X*Y*test_ket) == x*y*test_ket + assert qapply(X*Y*Z*test_ket) == x*y*z*test_ket + assert qapply(Y*Z*test_ket) == y*z*test_ket + + assert PositionKet3D() == test_ket + assert YOp() == Y + assert ZOp() == Z + + assert PositionKet3D.dual_class() == PositionBra3D + assert PositionBra3D.dual_class() == PositionKet3D + + other_ket = PositionKet3D(x_1, y_1, z_1) + assert (Dagger(other_ket)*test_ket).doit() == \ + DiracDelta(x - x_1)*DiracDelta(y - y_1)*DiracDelta(z - z_1) + + assert test_ket.position_x == x + assert test_ket.position_y == y + assert test_ket.position_z == z + assert other_ket.position_x == x_1 + assert other_ket.position_y == y_1 + assert other_ket.position_z == z_1 + + # TODO: Add tests for representations diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_cg.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_cg.py new file mode 100644 index 0000000000000000000000000000000000000000..384512aaac7a8d984ff2a733e6349161dc9414a0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_cg.py @@ -0,0 +1,183 @@ +from sympy.concrete.summations import Sum +from sympy.core.numbers import Rational +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.physics.quantum.cg import Wigner3j, Wigner6j, Wigner9j, CG, cg_simp +from sympy.functions.special.tensor_functions import KroneckerDelta + + +def test_cg_simp_add(): + j, m1, m1p, m2, m2p = symbols('j m1 m1p m2 m2p') + # Test Varshalovich 8.7.1 Eq 1 + a = CG(S.Half, S.Half, 0, 0, S.Half, S.Half) + b = CG(S.Half, Rational(-1, 2), 0, 0, S.Half, Rational(-1, 2)) + c = CG(1, 1, 0, 0, 1, 1) + d = CG(1, 0, 0, 0, 1, 0) + e = CG(1, -1, 0, 0, 1, -1) + assert cg_simp(a + b) == 2 + assert cg_simp(c + d + e) == 3 + assert cg_simp(a + b + c + d + e) == 5 + assert cg_simp(a + b + c) == 2 + c + assert cg_simp(2*a + b) == 2 + a + assert cg_simp(2*c + d + e) == 3 + c + assert cg_simp(5*a + 5*b) == 10 + assert cg_simp(5*c + 5*d + 5*e) == 15 + assert cg_simp(-a - b) == -2 + assert cg_simp(-c - d - e) == -3 + assert cg_simp(-6*a - 6*b) == -12 + assert cg_simp(-4*c - 4*d - 4*e) == -12 + a = CG(S.Half, S.Half, j, 0, S.Half, S.Half) + b = CG(S.Half, Rational(-1, 2), j, 0, S.Half, Rational(-1, 2)) + c = CG(1, 1, j, 0, 1, 1) + d = CG(1, 0, j, 0, 1, 0) + e = CG(1, -1, j, 0, 1, -1) + assert cg_simp(a + b) == 2*KroneckerDelta(j, 0) + assert cg_simp(c + d + e) == 3*KroneckerDelta(j, 0) + assert cg_simp(a + b + c + d + e) == 5*KroneckerDelta(j, 0) + assert cg_simp(a + b + c) == 2*KroneckerDelta(j, 0) + c + assert cg_simp(2*a + b) == 2*KroneckerDelta(j, 0) + a + assert cg_simp(2*c + d + e) == 3*KroneckerDelta(j, 0) + c + assert cg_simp(5*a + 5*b) == 10*KroneckerDelta(j, 0) + assert cg_simp(5*c + 5*d + 5*e) == 15*KroneckerDelta(j, 0) + assert cg_simp(-a - b) == -2*KroneckerDelta(j, 0) + assert cg_simp(-c - d - e) == -3*KroneckerDelta(j, 0) + assert cg_simp(-6*a - 6*b) == -12*KroneckerDelta(j, 0) + assert cg_simp(-4*c - 4*d - 4*e) == -12*KroneckerDelta(j, 0) + # Test Varshalovich 8.7.1 Eq 2 + a = CG(S.Half, S.Half, S.Half, Rational(-1, 2), 0, 0) + b = CG(S.Half, Rational(-1, 2), S.Half, S.Half, 0, 0) + c = CG(1, 1, 1, -1, 0, 0) + d = CG(1, 0, 1, 0, 0, 0) + e = CG(1, -1, 1, 1, 0, 0) + assert cg_simp(a - b) == sqrt(2) + assert cg_simp(c - d + e) == sqrt(3) + assert cg_simp(a - b + c - d + e) == sqrt(2) + sqrt(3) + assert cg_simp(a - b + c) == sqrt(2) + c + assert cg_simp(2*a - b) == sqrt(2) + a + assert cg_simp(2*c - d + e) == sqrt(3) + c + assert cg_simp(5*a - 5*b) == 5*sqrt(2) + assert cg_simp(5*c - 5*d + 5*e) == 5*sqrt(3) + assert cg_simp(-a + b) == -sqrt(2) + assert cg_simp(-c + d - e) == -sqrt(3) + assert cg_simp(-6*a + 6*b) == -6*sqrt(2) + assert cg_simp(-4*c + 4*d - 4*e) == -4*sqrt(3) + a = CG(S.Half, S.Half, S.Half, Rational(-1, 2), j, 0) + b = CG(S.Half, Rational(-1, 2), S.Half, S.Half, j, 0) + c = CG(1, 1, 1, -1, j, 0) + d = CG(1, 0, 1, 0, j, 0) + e = CG(1, -1, 1, 1, j, 0) + assert cg_simp(a - b) == sqrt(2)*KroneckerDelta(j, 0) + assert cg_simp(c - d + e) == sqrt(3)*KroneckerDelta(j, 0) + assert cg_simp(a - b + c - d + e) == sqrt( + 2)*KroneckerDelta(j, 0) + sqrt(3)*KroneckerDelta(j, 0) + assert cg_simp(a - b + c) == sqrt(2)*KroneckerDelta(j, 0) + c + assert cg_simp(2*a - b) == sqrt(2)*KroneckerDelta(j, 0) + a + assert cg_simp(2*c - d + e) == sqrt(3)*KroneckerDelta(j, 0) + c + assert cg_simp(5*a - 5*b) == 5*sqrt(2)*KroneckerDelta(j, 0) + assert cg_simp(5*c - 5*d + 5*e) == 5*sqrt(3)*KroneckerDelta(j, 0) + assert cg_simp(-a + b) == -sqrt(2)*KroneckerDelta(j, 0) + assert cg_simp(-c + d - e) == -sqrt(3)*KroneckerDelta(j, 0) + assert cg_simp(-6*a + 6*b) == -6*sqrt(2)*KroneckerDelta(j, 0) + assert cg_simp(-4*c + 4*d - 4*e) == -4*sqrt(3)*KroneckerDelta(j, 0) + # Test Varshalovich 8.7.2 Eq 9 + # alpha=alphap,beta=betap case + # numerical + a = CG(S.Half, S.Half, S.Half, Rational(-1, 2), 1, 0)**2 + b = CG(S.Half, S.Half, S.Half, Rational(-1, 2), 0, 0)**2 + c = CG(1, 0, 1, 1, 1, 1)**2 + d = CG(1, 0, 1, 1, 2, 1)**2 + assert cg_simp(a + b) == 1 + assert cg_simp(c + d) == 1 + assert cg_simp(a + b + c + d) == 2 + assert cg_simp(4*a + 4*b) == 4 + assert cg_simp(4*c + 4*d) == 4 + assert cg_simp(5*a + 3*b) == 3 + 2*a + assert cg_simp(5*c + 3*d) == 3 + 2*c + assert cg_simp(-a - b) == -1 + assert cg_simp(-c - d) == -1 + # symbolic + a = CG(S.Half, m1, S.Half, m2, 1, 1)**2 + b = CG(S.Half, m1, S.Half, m2, 1, 0)**2 + c = CG(S.Half, m1, S.Half, m2, 1, -1)**2 + d = CG(S.Half, m1, S.Half, m2, 0, 0)**2 + assert cg_simp(a + b + c + d) == 1 + assert cg_simp(4*a + 4*b + 4*c + 4*d) == 4 + assert cg_simp(3*a + 5*b + 3*c + 4*d) == 3 + 2*b + d + assert cg_simp(-a - b - c - d) == -1 + a = CG(1, m1, 1, m2, 2, 2)**2 + b = CG(1, m1, 1, m2, 2, 1)**2 + c = CG(1, m1, 1, m2, 2, 0)**2 + d = CG(1, m1, 1, m2, 2, -1)**2 + e = CG(1, m1, 1, m2, 2, -2)**2 + f = CG(1, m1, 1, m2, 1, 1)**2 + g = CG(1, m1, 1, m2, 1, 0)**2 + h = CG(1, m1, 1, m2, 1, -1)**2 + i = CG(1, m1, 1, m2, 0, 0)**2 + assert cg_simp(a + b + c + d + e + f + g + h + i) == 1 + assert cg_simp(4*(a + b + c + d + e + f + g + h + i)) == 4 + assert cg_simp(a + b + 2*c + d + 4*e + f + g + h + i) == 1 + c + 3*e + assert cg_simp(-a - b - c - d - e - f - g - h - i) == -1 + # alpha!=alphap or beta!=betap case + # numerical + a = CG(S.Half, S( + 1)/2, S.Half, Rational(-1, 2), 1, 0)*CG(S.Half, Rational(-1, 2), S.Half, S.Half, 1, 0) + b = CG(S.Half, S( + 1)/2, S.Half, Rational(-1, 2), 0, 0)*CG(S.Half, Rational(-1, 2), S.Half, S.Half, 0, 0) + c = CG(1, 1, 1, 0, 2, 1)*CG(1, 0, 1, 1, 2, 1) + d = CG(1, 1, 1, 0, 1, 1)*CG(1, 0, 1, 1, 1, 1) + assert cg_simp(a + b) == 0 + assert cg_simp(c + d) == 0 + # symbolic + a = CG(S.Half, m1, S.Half, m2, 1, 1)*CG(S.Half, m1p, S.Half, m2p, 1, 1) + b = CG(S.Half, m1, S.Half, m2, 1, 0)*CG(S.Half, m1p, S.Half, m2p, 1, 0) + c = CG(S.Half, m1, S.Half, m2, 1, -1)*CG(S.Half, m1p, S.Half, m2p, 1, -1) + d = CG(S.Half, m1, S.Half, m2, 0, 0)*CG(S.Half, m1p, S.Half, m2p, 0, 0) + assert cg_simp(a + b + c + d) == KroneckerDelta(m1, m1p)*KroneckerDelta(m2, m2p) + a = CG(1, m1, 1, m2, 2, 2)*CG(1, m1p, 1, m2p, 2, 2) + b = CG(1, m1, 1, m2, 2, 1)*CG(1, m1p, 1, m2p, 2, 1) + c = CG(1, m1, 1, m2, 2, 0)*CG(1, m1p, 1, m2p, 2, 0) + d = CG(1, m1, 1, m2, 2, -1)*CG(1, m1p, 1, m2p, 2, -1) + e = CG(1, m1, 1, m2, 2, -2)*CG(1, m1p, 1, m2p, 2, -2) + f = CG(1, m1, 1, m2, 1, 1)*CG(1, m1p, 1, m2p, 1, 1) + g = CG(1, m1, 1, m2, 1, 0)*CG(1, m1p, 1, m2p, 1, 0) + h = CG(1, m1, 1, m2, 1, -1)*CG(1, m1p, 1, m2p, 1, -1) + i = CG(1, m1, 1, m2, 0, 0)*CG(1, m1p, 1, m2p, 0, 0) + assert cg_simp( + a + b + c + d + e + f + g + h + i) == KroneckerDelta(m1, m1p)*KroneckerDelta(m2, m2p) + + +def test_cg_simp_sum(): + x, a, b, c, cp, alpha, beta, gamma, gammap = symbols( + 'x a b c cp alpha beta gamma gammap') + # Varshalovich 8.7.1 Eq 1 + assert cg_simp(x * Sum(CG(a, alpha, b, 0, a, alpha), (alpha, -a, a) + )) == x*(2*a + 1)*KroneckerDelta(b, 0) + assert cg_simp(x * Sum(CG(a, alpha, b, 0, a, alpha), (alpha, -a, a)) + CG(1, 0, 1, 0, 1, 0)) == x*(2*a + 1)*KroneckerDelta(b, 0) + CG(1, 0, 1, 0, 1, 0) + assert cg_simp(2 * Sum(CG(1, alpha, 0, 0, 1, alpha), (alpha, -1, 1))) == 6 + # Varshalovich 8.7.1 Eq 2 + assert cg_simp(x*Sum((-1)**(a - alpha) * CG(a, alpha, a, -alpha, c, + 0), (alpha, -a, a))) == x*sqrt(2*a + 1)*KroneckerDelta(c, 0) + assert cg_simp(3*Sum((-1)**(2 - alpha) * CG( + 2, alpha, 2, -alpha, 0, 0), (alpha, -2, 2))) == 3*sqrt(5) + # Varshalovich 8.7.2 Eq 4 + assert cg_simp(Sum(CG(a, alpha, b, beta, c, gamma)*CG(a, alpha, b, beta, cp, gammap), (alpha, -a, a), (beta, -b, b))) == KroneckerDelta(c, cp)*KroneckerDelta(gamma, gammap) + assert cg_simp(Sum(CG(a, alpha, b, beta, c, gamma)*CG(a, alpha, b, beta, c, gammap), (alpha, -a, a), (beta, -b, b))) == KroneckerDelta(gamma, gammap) + assert cg_simp(Sum(CG(a, alpha, b, beta, c, gamma)*CG(a, alpha, b, beta, cp, gamma), (alpha, -a, a), (beta, -b, b))) == KroneckerDelta(c, cp) + assert cg_simp(Sum(CG( + a, alpha, b, beta, c, gamma)**2, (alpha, -a, a), (beta, -b, b))) == 1 + assert cg_simp(Sum(CG(2, alpha, 1, beta, 2, gamma)*CG(2, alpha, 1, beta, 2, gammap), (alpha, -2, 2), (beta, -1, 1))) == KroneckerDelta(gamma, gammap) + + +def test_doit(): + assert Wigner3j(S.Half, Rational(-1, 2), S.Half, S.Half, 0, 0).doit() == -sqrt(2)/2 + assert Wigner3j(1/2,1/2,1/2,1/2,1/2,1/2).doit() == 0 + assert Wigner3j(9/2,9/2,9/2,9/2,9/2,9/2).doit() == 0 + assert Wigner6j(1, 2, 3, 2, 1, 2).doit() == sqrt(21)/105 + assert Wigner6j(3, 1, 2, 2, 2, 1).doit() == sqrt(21) / 105 + assert Wigner9j( + 2, 1, 1, Rational(3, 2), S.Half, 1, S.Half, S.Half, 0).doit() == sqrt(2)/12 + assert CG(S.Half, S.Half, S.Half, Rational(-1, 2), 1, 0).doit() == sqrt(2)/2 + # J minus M is not integer + assert Wigner3j(1, -1, S.Half, S.Half, 1, S.Half).doit() == 0 + assert CG(4, -1, S.Half, S.Half, 4, Rational(-1, 2)).doit() == 0 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_circuitplot.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_circuitplot.py new file mode 100644 index 0000000000000000000000000000000000000000..fcc89f77047450ad3f8663f371f483654dc70ea9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_circuitplot.py @@ -0,0 +1,69 @@ +from sympy.physics.quantum.circuitplot import labeller, render_label, Mz, CreateOneQubitGate,\ + CreateCGate +from sympy.physics.quantum.gate import CNOT, H, SWAP, CGate, S, T +from sympy.external import import_module +from sympy.testing.pytest import skip + +mpl = import_module('matplotlib') + +def test_render_label(): + assert render_label('q0') == r'$\left|q0\right\rangle$' + assert render_label('q0', {'q0': '0'}) == r'$\left|q0\right\rangle=\left|0\right\rangle$' + +def test_Mz(): + assert str(Mz(0)) == 'Mz(0)' + +def test_create1(): + Qgate = CreateOneQubitGate('Q') + assert str(Qgate(0)) == 'Q(0)' + +def test_createc(): + Qgate = CreateCGate('Q') + assert str(Qgate([1],0)) == 'C((1),Q(0))' + +def test_labeller(): + """Test the labeller utility""" + assert labeller(2) == ['q_1', 'q_0'] + assert labeller(3,'j') == ['j_2', 'j_1', 'j_0'] + +def test_cnot(): + """Test a simple cnot circuit. Right now this only makes sure the code doesn't + raise an exception, and some simple properties + """ + if not mpl: + skip("matplotlib not installed") + else: + from sympy.physics.quantum.circuitplot import CircuitPlot + + c = CircuitPlot(CNOT(1,0),2,labels=labeller(2)) + assert c.ngates == 2 + assert c.nqubits == 2 + assert c.labels == ['q_1', 'q_0'] + + c = CircuitPlot(CNOT(1,0),2) + assert c.ngates == 2 + assert c.nqubits == 2 + assert c.labels == [] + +def test_ex1(): + if not mpl: + skip("matplotlib not installed") + else: + from sympy.physics.quantum.circuitplot import CircuitPlot + + c = CircuitPlot(CNOT(1,0)*H(1),2,labels=labeller(2)) + assert c.ngates == 2 + assert c.nqubits == 2 + assert c.labels == ['q_1', 'q_0'] + +def test_ex4(): + if not mpl: + skip("matplotlib not installed") + else: + from sympy.physics.quantum.circuitplot import CircuitPlot + + c = CircuitPlot(SWAP(0,2)*H(0)* CGate((0,),S(1)) *H(1)*CGate((0,),T(2))\ + *CGate((1,),S(2))*H(2),3,labels=labeller(3,'j')) + assert c.ngates == 7 + assert c.nqubits == 3 + assert c.labels == ['j_2', 'j_1', 'j_0'] diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_circuitutils.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_circuitutils.py new file mode 100644 index 0000000000000000000000000000000000000000..8ea7232320417db8bf745871cff0e77aaf1901e7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_circuitutils.py @@ -0,0 +1,402 @@ +from sympy.core.mul import Mul +from sympy.core.numbers import Integer +from sympy.core.symbol import Symbol +from sympy.utilities import numbered_symbols +from sympy.physics.quantum.gate import X, Y, Z, H, CNOT, CGate +from sympy.physics.quantum.identitysearch import bfs_identity_search +from sympy.physics.quantum.circuitutils import (kmp_table, find_subcircuit, + replace_subcircuit, convert_to_symbolic_indices, + convert_to_real_indices, random_reduce, random_insert, + flatten_ids) +from sympy.testing.pytest import slow + + +def create_gate_sequence(qubit=0): + gates = (X(qubit), Y(qubit), Z(qubit), H(qubit)) + return gates + + +def test_kmp_table(): + word = ('a', 'b', 'c', 'd', 'a', 'b', 'd') + expected_table = [-1, 0, 0, 0, 0, 1, 2] + assert expected_table == kmp_table(word) + + word = ('P', 'A', 'R', 'T', 'I', 'C', 'I', 'P', 'A', 'T', 'E', ' ', + 'I', 'N', ' ', 'P', 'A', 'R', 'A', 'C', 'H', 'U', 'T', 'E') + expected_table = [-1, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, + 0, 0, 0, 0, 1, 2, 3, 0, 0, 0, 0, 0] + assert expected_table == kmp_table(word) + + x = X(0) + y = Y(0) + z = Z(0) + h = H(0) + word = (x, y, y, x, z) + expected_table = [-1, 0, 0, 0, 1] + assert expected_table == kmp_table(word) + + word = (x, x, y, h, z) + expected_table = [-1, 0, 1, 0, 0] + assert expected_table == kmp_table(word) + + +def test_find_subcircuit(): + x = X(0) + y = Y(0) + z = Z(0) + h = H(0) + x1 = X(1) + y1 = Y(1) + + i0 = Symbol('i0') + x_i0 = X(i0) + y_i0 = Y(i0) + z_i0 = Z(i0) + h_i0 = H(i0) + + circuit = (x, y, z) + + assert find_subcircuit(circuit, (x,)) == 0 + assert find_subcircuit(circuit, (x1,)) == -1 + assert find_subcircuit(circuit, (y,)) == 1 + assert find_subcircuit(circuit, (h,)) == -1 + assert find_subcircuit(circuit, Mul(x, h)) == -1 + assert find_subcircuit(circuit, Mul(x, y, z)) == 0 + assert find_subcircuit(circuit, Mul(y, z)) == 1 + assert find_subcircuit(Mul(*circuit), (x, y, z, h)) == -1 + assert find_subcircuit(Mul(*circuit), (z, y, x)) == -1 + assert find_subcircuit(circuit, (x,), start=2, end=1) == -1 + + circuit = (x, y, x, y, z) + assert find_subcircuit(Mul(*circuit), Mul(x, y, z)) == 2 + assert find_subcircuit(circuit, (x,), start=1) == 2 + assert find_subcircuit(circuit, (x, y), start=1, end=2) == -1 + assert find_subcircuit(Mul(*circuit), (x, y), start=1, end=3) == -1 + assert find_subcircuit(circuit, (x, y), start=1, end=4) == 2 + assert find_subcircuit(circuit, (x, y), start=2, end=4) == 2 + + circuit = (x, y, z, x1, x, y, z, h, x, y, x1, + x, y, z, h, y1, h) + assert find_subcircuit(circuit, (x, y, z, h, y1)) == 11 + + circuit = (x, y, x_i0, y_i0, z_i0, z) + assert find_subcircuit(circuit, (x_i0, y_i0, z_i0)) == 2 + + circuit = (x_i0, y_i0, z_i0, x_i0, y_i0, h_i0) + subcircuit = (x_i0, y_i0, z_i0) + result = find_subcircuit(circuit, subcircuit) + assert result == 0 + + +def test_replace_subcircuit(): + x = X(0) + y = Y(0) + z = Z(0) + h = H(0) + cnot = CNOT(1, 0) + cgate_z = CGate((0,), Z(1)) + + # Standard cases + circuit = (z, y, x, x) + remove = (z, y, x) + assert replace_subcircuit(circuit, Mul(*remove)) == (x,) + assert replace_subcircuit(circuit, remove + (x,)) == () + assert replace_subcircuit(circuit, remove, pos=1) == circuit + assert replace_subcircuit(circuit, remove, pos=0) == (x,) + assert replace_subcircuit(circuit, (x, x), pos=2) == (z, y) + assert replace_subcircuit(circuit, (h,)) == circuit + + circuit = (x, y, x, y, z) + remove = (x, y, z) + assert replace_subcircuit(Mul(*circuit), Mul(*remove)) == (x, y) + remove = (x, y, x, y) + assert replace_subcircuit(circuit, remove) == (z,) + + circuit = (x, h, cgate_z, h, cnot) + remove = (x, h, cgate_z) + assert replace_subcircuit(circuit, Mul(*remove), pos=-1) == (h, cnot) + assert replace_subcircuit(circuit, remove, pos=1) == circuit + remove = (h, h) + assert replace_subcircuit(circuit, remove) == circuit + remove = (h, cgate_z, h, cnot) + assert replace_subcircuit(circuit, remove) == (x,) + + replace = (h, x) + actual = replace_subcircuit(circuit, remove, + replace=replace) + assert actual == (x, h, x) + + circuit = (x, y, h, x, y, z) + remove = (x, y) + replace = (cnot, cgate_z) + actual = replace_subcircuit(circuit, remove, + replace=Mul(*replace)) + assert actual == (cnot, cgate_z, h, x, y, z) + + actual = replace_subcircuit(circuit, remove, + replace=replace, pos=1) + assert actual == (x, y, h, cnot, cgate_z, z) + + +def test_convert_to_symbolic_indices(): + (x, y, z, h) = create_gate_sequence() + + i0 = Symbol('i0') + exp_map = {i0: Integer(0)} + actual, act_map, sndx, gen = convert_to_symbolic_indices((x,)) + assert actual == (X(i0),) + assert act_map == exp_map + + expected = (X(i0), Y(i0), Z(i0), H(i0)) + exp_map = {i0: Integer(0)} + actual, act_map, sndx, gen = convert_to_symbolic_indices((x, y, z, h)) + assert actual == expected + assert exp_map == act_map + + (x1, y1, z1, h1) = create_gate_sequence(1) + i1 = Symbol('i1') + + expected = (X(i0), Y(i0), Z(i0), H(i0)) + exp_map = {i0: Integer(1)} + actual, act_map, sndx, gen = convert_to_symbolic_indices((x1, y1, z1, h1)) + assert actual == expected + assert act_map == exp_map + + expected = (X(i0), Y(i0), Z(i0), H(i0), X(i1), Y(i1), Z(i1), H(i1)) + exp_map = {i0: Integer(0), i1: Integer(1)} + actual, act_map, sndx, gen = convert_to_symbolic_indices((x, y, z, h, + x1, y1, z1, h1)) + assert actual == expected + assert act_map == exp_map + + exp_map = {i0: Integer(1), i1: Integer(0)} + actual, act_map, sndx, gen = convert_to_symbolic_indices(Mul(x1, y1, + z1, h1, x, y, z, h)) + assert actual == expected + assert act_map == exp_map + + expected = (X(i0), X(i1), Y(i0), Y(i1), Z(i0), Z(i1), H(i0), H(i1)) + exp_map = {i0: Integer(0), i1: Integer(1)} + actual, act_map, sndx, gen = convert_to_symbolic_indices(Mul(x, x1, + y, y1, z, z1, h, h1)) + assert actual == expected + assert act_map == exp_map + + exp_map = {i0: Integer(1), i1: Integer(0)} + actual, act_map, sndx, gen = convert_to_symbolic_indices((x1, x, y1, y, + z1, z, h1, h)) + assert actual == expected + assert act_map == exp_map + + cnot_10 = CNOT(1, 0) + cnot_01 = CNOT(0, 1) + cgate_z_10 = CGate(1, Z(0)) + cgate_z_01 = CGate(0, Z(1)) + + expected = (X(i0), X(i1), Y(i0), Y(i1), Z(i0), Z(i1), + H(i0), H(i1), CNOT(i1, i0), CNOT(i0, i1), + CGate(i1, Z(i0)), CGate(i0, Z(i1))) + exp_map = {i0: Integer(0), i1: Integer(1)} + args = (x, x1, y, y1, z, z1, h, h1, cnot_10, cnot_01, + cgate_z_10, cgate_z_01) + actual, act_map, sndx, gen = convert_to_symbolic_indices(args) + assert actual == expected + assert act_map == exp_map + + args = (x1, x, y1, y, z1, z, h1, h, cnot_10, cnot_01, + cgate_z_10, cgate_z_01) + expected = (X(i0), X(i1), Y(i0), Y(i1), Z(i0), Z(i1), + H(i0), H(i1), CNOT(i0, i1), CNOT(i1, i0), + CGate(i0, Z(i1)), CGate(i1, Z(i0))) + exp_map = {i0: Integer(1), i1: Integer(0)} + actual, act_map, sndx, gen = convert_to_symbolic_indices(args) + assert actual == expected + assert act_map == exp_map + + args = (cnot_10, h, cgate_z_01, h) + expected = (CNOT(i0, i1), H(i1), CGate(i1, Z(i0)), H(i1)) + exp_map = {i0: Integer(1), i1: Integer(0)} + actual, act_map, sndx, gen = convert_to_symbolic_indices(args) + assert actual == expected + assert act_map == exp_map + + args = (cnot_01, h1, cgate_z_10, h1) + exp_map = {i0: Integer(0), i1: Integer(1)} + actual, act_map, sndx, gen = convert_to_symbolic_indices(args) + assert actual == expected + assert act_map == exp_map + + args = (cnot_10, h1, cgate_z_01, h1) + expected = (CNOT(i0, i1), H(i0), CGate(i1, Z(i0)), H(i0)) + exp_map = {i0: Integer(1), i1: Integer(0)} + actual, act_map, sndx, gen = convert_to_symbolic_indices(args) + assert actual == expected + assert act_map == exp_map + + i2 = Symbol('i2') + ccgate_z = CGate(0, CGate(1, Z(2))) + ccgate_x = CGate(1, CGate(2, X(0))) + args = (ccgate_z, ccgate_x) + + expected = (CGate(i0, CGate(i1, Z(i2))), CGate(i1, CGate(i2, X(i0)))) + exp_map = {i0: Integer(0), i1: Integer(1), i2: Integer(2)} + actual, act_map, sndx, gen = convert_to_symbolic_indices(args) + assert actual == expected + assert act_map == exp_map + + ndx_map = {i0: Integer(0)} + index_gen = numbered_symbols(prefix='i', start=1) + actual, act_map, sndx, gen = convert_to_symbolic_indices(args, + qubit_map=ndx_map, + start=i0, + gen=index_gen) + assert actual == expected + assert act_map == exp_map + + i3 = Symbol('i3') + cgate_x0_c321 = CGate((3, 2, 1), X(0)) + exp_map = {i0: Integer(3), i1: Integer(2), + i2: Integer(1), i3: Integer(0)} + expected = (CGate((i0, i1, i2), X(i3)),) + args = (cgate_x0_c321,) + actual, act_map, sndx, gen = convert_to_symbolic_indices(args) + assert actual == expected + assert act_map == exp_map + + +def test_convert_to_real_indices(): + i0 = Symbol('i0') + i1 = Symbol('i1') + + (x, y, z, h) = create_gate_sequence() + + x_i0 = X(i0) + y_i0 = Y(i0) + z_i0 = Z(i0) + + qubit_map = {i0: 0} + args = (z_i0, y_i0, x_i0) + expected = (z, y, x) + actual = convert_to_real_indices(args, qubit_map) + assert actual == expected + + cnot_10 = CNOT(1, 0) + cnot_01 = CNOT(0, 1) + cgate_z_10 = CGate(1, Z(0)) + cgate_z_01 = CGate(0, Z(1)) + + cnot_i1_i0 = CNOT(i1, i0) + cnot_i0_i1 = CNOT(i0, i1) + cgate_z_i1_i0 = CGate(i1, Z(i0)) + + qubit_map = {i0: 0, i1: 1} + args = (cnot_i1_i0,) + expected = (cnot_10,) + actual = convert_to_real_indices(args, qubit_map) + assert actual == expected + + args = (cgate_z_i1_i0,) + expected = (cgate_z_10,) + actual = convert_to_real_indices(args, qubit_map) + assert actual == expected + + args = (cnot_i0_i1,) + expected = (cnot_01,) + actual = convert_to_real_indices(args, qubit_map) + assert actual == expected + + qubit_map = {i0: 1, i1: 0} + args = (cgate_z_i1_i0,) + expected = (cgate_z_01,) + actual = convert_to_real_indices(args, qubit_map) + assert actual == expected + + i2 = Symbol('i2') + ccgate_z = CGate(i0, CGate(i1, Z(i2))) + ccgate_x = CGate(i1, CGate(i2, X(i0))) + + qubit_map = {i0: 0, i1: 1, i2: 2} + args = (ccgate_z, ccgate_x) + expected = (CGate(0, CGate(1, Z(2))), CGate(1, CGate(2, X(0)))) + actual = convert_to_real_indices(Mul(*args), qubit_map) + assert actual == expected + + qubit_map = {i0: 1, i2: 0, i1: 2} + args = (ccgate_x, ccgate_z) + expected = (CGate(2, CGate(0, X(1))), CGate(1, CGate(2, Z(0)))) + actual = convert_to_real_indices(args, qubit_map) + assert actual == expected + + +@slow +def test_random_reduce(): + x = X(0) + y = Y(0) + z = Z(0) + h = H(0) + cnot = CNOT(1, 0) + cgate_z = CGate((0,), Z(1)) + + gate_list = [x, y, z] + ids = list(bfs_identity_search(gate_list, 1, max_depth=4)) + + circuit = (x, y, h, z, cnot) + assert random_reduce(circuit, []) == circuit + assert random_reduce(circuit, ids) == circuit + + seq = [2, 11, 9, 3, 5] + circuit = (x, y, z, x, y, h) + assert random_reduce(circuit, ids, seed=seq) == (x, y, h) + + circuit = (x, x, y, y, z, z) + assert random_reduce(circuit, ids, seed=seq) == (x, x, y, y) + + seq = [14, 13, 0] + assert random_reduce(circuit, ids, seed=seq) == (y, y, z, z) + + gate_list = [x, y, z, h, cnot, cgate_z] + ids = list(bfs_identity_search(gate_list, 2, max_depth=4)) + + seq = [25] + circuit = (x, y, z, y, h, y, h, cgate_z, h, cnot) + expected = (x, y, z, cgate_z, h, cnot) + assert random_reduce(circuit, ids, seed=seq) == expected + circuit = Mul(*circuit) + assert random_reduce(circuit, ids, seed=seq) == expected + + +@slow +def test_random_insert(): + x = X(0) + y = Y(0) + z = Z(0) + h = H(0) + cnot = CNOT(1, 0) + cgate_z = CGate((0,), Z(1)) + + choices = [(x, x)] + circuit = (y, y) + loc, choice = 0, 0 + actual = random_insert(circuit, choices, seed=[loc, choice]) + assert actual == (x, x, y, y) + + circuit = (x, y, z, h) + choices = [(h, h), (x, y, z)] + expected = (x, x, y, z, y, z, h) + loc, choice = 1, 1 + actual = random_insert(circuit, choices, seed=[loc, choice]) + assert actual == expected + + gate_list = [x, y, z, h, cnot, cgate_z] + ids = list(bfs_identity_search(gate_list, 2, max_depth=4)) + + eq_ids = flatten_ids(ids) + + circuit = (x, y, h, cnot, cgate_z) + expected = (x, z, x, z, x, y, h, cnot, cgate_z) + loc, choice = 1, 30 + actual = random_insert(circuit, eq_ids, seed=[loc, choice]) + assert actual == expected + circuit = Mul(*circuit) + actual = random_insert(circuit, eq_ids, seed=[loc, choice]) + assert actual == expected diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_commutator.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_commutator.py new file mode 100644 index 0000000000000000000000000000000000000000..04f45feddaca63d7306363a9235c63f534d11430 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_commutator.py @@ -0,0 +1,81 @@ +from sympy.core.numbers import Integer +from sympy.core.symbol import symbols + +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.commutator import Commutator as Comm +from sympy.physics.quantum.operator import Operator + + +a, b, c = symbols('a,b,c') +n = symbols('n', integer=True) +A, B, C, D = symbols('A,B,C,D', commutative=False) + + +def test_commutator(): + c = Comm(A, B) + assert c.is_commutative is False + assert isinstance(c, Comm) + assert c.subs(A, C) == Comm(C, B) + + +def test_commutator_identities(): + assert Comm(a*A, b*B) == a*b*Comm(A, B) + assert Comm(A, A) == 0 + assert Comm(a, b) == 0 + assert Comm(A, B) == -Comm(B, A) + assert Comm(A, B).doit() == A*B - B*A + assert Comm(A, B*C).expand(commutator=True) == Comm(A, B)*C + B*Comm(A, C) + assert Comm(A*B, C*D).expand(commutator=True) == \ + A*C*Comm(B, D) + A*Comm(B, C)*D + C*Comm(A, D)*B + Comm(A, C)*D*B + assert Comm(A, B**2).expand(commutator=True) == Comm(A, B)*B + B*Comm(A, B) + assert Comm(A**2, C**2).expand(commutator=True) == \ + Comm(A*B, C*D).expand(commutator=True).replace(B, A).replace(D, C) == \ + A*C*Comm(A, C) + A*Comm(A, C)*C + C*Comm(A, C)*A + Comm(A, C)*C*A + assert Comm(A, C**-2).expand(commutator=True) == \ + Comm(A, (1/C)*(1/D)).expand(commutator=True).replace(D, C) + assert Comm(A + B, C + D).expand(commutator=True) == \ + Comm(A, C) + Comm(A, D) + Comm(B, C) + Comm(B, D) + assert Comm(A, B + C).expand(commutator=True) == Comm(A, B) + Comm(A, C) + assert Comm(A**n, B).expand(commutator=True) == Comm(A**n, B) + + e = Comm(A, Comm(B, C)) + Comm(B, Comm(C, A)) + Comm(C, Comm(A, B)) + assert e.doit().expand() == 0 + + +def test_commutator_dagger(): + comm = Comm(A*B, C) + assert Dagger(comm).expand(commutator=True) == \ + - Comm(Dagger(B), Dagger(C))*Dagger(A) - \ + Dagger(B)*Comm(Dagger(A), Dagger(C)) + + +class Foo(Operator): + + def _eval_commutator_Bar(self, bar): + return Integer(0) + + +class Bar(Operator): + pass + + +class Tam(Operator): + + def _eval_commutator_Foo(self, foo): + return Integer(1) + + +def test_eval_commutator(): + F = Foo('F') + B = Bar('B') + T = Tam('T') + assert Comm(F, B).doit() == 0 + assert Comm(B, F).doit() == 0 + assert Comm(F, T).doit() == -1 + assert Comm(T, F).doit() == 1 + assert Comm(B, T).doit() == B*T - T*B + assert Comm(F**2, B).expand(commutator=True).doit() == 0 + assert Comm(F**2, T).expand(commutator=True).doit() == -2*F + assert Comm(F, T**2).expand(commutator=True).doit() == -2*T + assert Comm(T**2, F).expand(commutator=True).doit() == 2*T + assert Comm(T**2, F**3).expand(commutator=True).doit() == 2*F*T*F + 2*F**2*T + 2*T*F**2 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_constants.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_constants.py new file mode 100644 index 0000000000000000000000000000000000000000..48a773ea6b5afbaf956143b50b16b3b18aaf5beb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_constants.py @@ -0,0 +1,13 @@ +from sympy.core.numbers import Float + +from sympy.physics.quantum.constants import hbar + + +def test_hbar(): + assert hbar.is_commutative is True + assert hbar.is_real is True + assert hbar.is_positive is True + assert hbar.is_negative is False + assert hbar.is_irrational is True + + assert hbar.evalf() == Float(1.05457162e-34) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_dagger.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_dagger.py new file mode 100644 index 0000000000000000000000000000000000000000..1357c9320a20afa2ba905a117d90ed1ac2e9642c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_dagger.py @@ -0,0 +1,103 @@ +from sympy.core.expr import Expr +from sympy.core.mul import Mul +from sympy.core.numbers import (I, Integer) +from sympy.core.symbol import symbols +from sympy.functions.elementary.complexes import conjugate +from sympy.matrices.dense import Matrix + +from sympy.physics.quantum.dagger import adjoint, Dagger +from sympy.external import import_module +from sympy.testing.pytest import skip, warns_deprecated_sympy +from sympy.physics.quantum.operator import Operator, IdentityOperator + + +def test_scalars(): + x = symbols('x', complex=True) + assert Dagger(x) == conjugate(x) + assert Dagger(I*x) == -I*conjugate(x) + + i = symbols('i', real=True) + assert Dagger(i) == i + + p = symbols('p') + assert isinstance(Dagger(p), conjugate) + + i = Integer(3) + assert Dagger(i) == i + + A = symbols('A', commutative=False) + assert Dagger(A).is_commutative is False + + +def test_matrix(): + x = symbols('x') + m = Matrix([[I, x*I], [2, 4]]) + assert Dagger(m) == m.H + + +def test_dagger_mul(): + O = Operator('O') + assert Dagger(O)*O == Dagger(O)*O + with warns_deprecated_sympy(): + I = IdentityOperator() + assert Dagger(O)*O*I == Mul(Dagger(O), O)*I + assert Dagger(O)*Dagger(O) == Dagger(O)**2 + assert Dagger(O)*Dagger(I) == Dagger(O) + + +class Foo(Expr): + + def _eval_adjoint(self): + return I + + +def test_eval_adjoint(): + f = Foo() + d = Dagger(f) + assert d == I + +np = import_module('numpy') + + +def test_numpy_dagger(): + if not np: + skip("numpy not installed.") + + a = np.array([[1.0, 2.0j], [-1.0j, 2.0]]) + adag = a.copy().transpose().conjugate() + assert (Dagger(a) == adag).all() + + +scipy = import_module('scipy', import_kwargs={'fromlist': ['sparse']}) + + +def test_scipy_sparse_dagger(): + if not np: + skip("numpy not installed.") + if not scipy: + skip("scipy not installed.") + else: + sparse = scipy.sparse + + a = sparse.csr_matrix([[1.0 + 0.0j, 2.0j], [-1.0j, 2.0 + 0.0j]]) + adag = a.copy().transpose().conjugate() + assert np.linalg.norm((Dagger(a) - adag).todense()) == 0.0 + + +def test_unknown(): + """Check treatment of unknown objects. + Objects without adjoint or conjugate/transpose methods + are sympified and wrapped in dagger. + """ + x = symbols("x", commutative=False) + result = Dagger(x) + assert result.args == (x,) and isinstance(result, adjoint) + + +def test_unevaluated(): + """Check that evaluate=False returns unevaluated Dagger. + """ + x = symbols("x", real=True) + assert Dagger(x) == x + result = Dagger(x, evaluate=False) + assert result.args == (x,) and isinstance(result, adjoint) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_density.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_density.py new file mode 100644 index 0000000000000000000000000000000000000000..399acce6e201b39f65ea674048198fd2f087b4d0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_density.py @@ -0,0 +1,289 @@ +from sympy.core.numbers import Rational +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import log +from sympy.external import import_module +from sympy.physics.quantum.density import Density, entropy, fidelity +from sympy.physics.quantum.state import Ket, TimeDepKet +from sympy.physics.quantum.qubit import Qubit +from sympy.physics.quantum.represent import represent +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.cartesian import XKet, PxKet, PxOp, XOp +from sympy.physics.quantum.spin import JzKet +from sympy.physics.quantum.operator import OuterProduct +from sympy.physics.quantum.trace import Tr +from sympy.functions import sqrt +from sympy.testing.pytest import raises +from sympy.physics.quantum.matrixutils import scipy_sparse_matrix +from sympy.physics.quantum.tensorproduct import TensorProduct + + +def test_eval_args(): + # check instance created + assert isinstance(Density([Ket(0), 0.5], [Ket(1), 0.5]), Density) + assert isinstance(Density([Qubit('00'), 1/sqrt(2)], + [Qubit('11'), 1/sqrt(2)]), Density) + + #test if Qubit object type preserved + d = Density([Qubit('00'), 1/sqrt(2)], [Qubit('11'), 1/sqrt(2)]) + for (state, prob) in d.args: + assert isinstance(state, Qubit) + + # check for value error, when prob is not provided + raises(ValueError, lambda: Density([Ket(0)], [Ket(1)])) + + +def test_doit(): + + x, y = symbols('x y') + A, B, C, D, E, F = symbols('A B C D E F', commutative=False) + d = Density([XKet(), 0.5], [PxKet(), 0.5]) + assert (0.5*(PxKet()*Dagger(PxKet())) + + 0.5*(XKet()*Dagger(XKet()))) == d.doit() + + # check for kets with expr in them + d_with_sym = Density([XKet(x*y), 0.5], [PxKet(x*y), 0.5]) + assert (0.5*(PxKet(x*y)*Dagger(PxKet(x*y))) + + 0.5*(XKet(x*y)*Dagger(XKet(x*y)))) == d_with_sym.doit() + + d = Density([(A + B)*C, 1.0]) + assert d.doit() == (1.0*A*C*Dagger(C)*Dagger(A) + + 1.0*A*C*Dagger(C)*Dagger(B) + + 1.0*B*C*Dagger(C)*Dagger(A) + + 1.0*B*C*Dagger(C)*Dagger(B)) + + # With TensorProducts as args + # Density with simple tensor products as args + t = TensorProduct(A, B, C) + d = Density([t, 1.0]) + assert d.doit() == \ + 1.0 * TensorProduct(A*Dagger(A), B*Dagger(B), C*Dagger(C)) + + # Density with multiple Tensorproducts as states + t2 = TensorProduct(A, B) + t3 = TensorProduct(C, D) + + d = Density([t2, 0.5], [t3, 0.5]) + assert d.doit() == (0.5 * TensorProduct(A*Dagger(A), B*Dagger(B)) + + 0.5 * TensorProduct(C*Dagger(C), D*Dagger(D))) + + #Density with mixed states + d = Density([t2 + t3, 1.0]) + assert d.doit() == (1.0 * TensorProduct(A*Dagger(A), B*Dagger(B)) + + 1.0 * TensorProduct(A*Dagger(C), B*Dagger(D)) + + 1.0 * TensorProduct(C*Dagger(A), D*Dagger(B)) + + 1.0 * TensorProduct(C*Dagger(C), D*Dagger(D))) + + #Density operators with spin states + tp1 = TensorProduct(JzKet(1, 1), JzKet(1, -1)) + d = Density([tp1, 1]) + + # full trace + t = Tr(d) + assert t.doit() == 1 + + #Partial trace on density operators with spin states + t = Tr(d, [0]) + assert t.doit() == JzKet(1, -1) * Dagger(JzKet(1, -1)) + t = Tr(d, [1]) + assert t.doit() == JzKet(1, 1) * Dagger(JzKet(1, 1)) + + # with another spin state + tp2 = TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) + d = Density([tp2, 1]) + + #full trace + t = Tr(d) + assert t.doit() == 1 + + #Partial trace on density operators with spin states + t = Tr(d, [0]) + assert t.doit() == JzKet(S.Half, Rational(-1, 2)) * Dagger(JzKet(S.Half, Rational(-1, 2))) + t = Tr(d, [1]) + assert t.doit() == JzKet(S.Half, S.Half) * Dagger(JzKet(S.Half, S.Half)) + + +def test_apply_op(): + d = Density([Ket(0), 0.5], [Ket(1), 0.5]) + assert d.apply_op(XOp()) == Density([XOp()*Ket(0), 0.5], + [XOp()*Ket(1), 0.5]) + + +def test_represent(): + x, y = symbols('x y') + d = Density([XKet(), 0.5], [PxKet(), 0.5]) + assert (represent(0.5*(PxKet()*Dagger(PxKet()))) + + represent(0.5*(XKet()*Dagger(XKet())))) == represent(d) + + # check for kets with expr in them + d_with_sym = Density([XKet(x*y), 0.5], [PxKet(x*y), 0.5]) + assert (represent(0.5*(PxKet(x*y)*Dagger(PxKet(x*y)))) + + represent(0.5*(XKet(x*y)*Dagger(XKet(x*y))))) == \ + represent(d_with_sym) + + # check when given explicit basis + assert (represent(0.5*(XKet()*Dagger(XKet())), basis=PxOp()) + + represent(0.5*(PxKet()*Dagger(PxKet())), basis=PxOp())) == \ + represent(d, basis=PxOp()) + + +def test_states(): + d = Density([Ket(0), 0.5], [Ket(1), 0.5]) + states = d.states() + assert states[0] == Ket(0) and states[1] == Ket(1) + + +def test_probs(): + d = Density([Ket(0), .75], [Ket(1), 0.25]) + probs = d.probs() + assert probs[0] == 0.75 and probs[1] == 0.25 + + #probs can be symbols + x, y = symbols('x y') + d = Density([Ket(0), x], [Ket(1), y]) + probs = d.probs() + assert probs[0] == x and probs[1] == y + + +def test_get_state(): + x, y = symbols('x y') + d = Density([Ket(0), x], [Ket(1), y]) + states = (d.get_state(0), d.get_state(1)) + assert states[0] == Ket(0) and states[1] == Ket(1) + + +def test_get_prob(): + x, y = symbols('x y') + d = Density([Ket(0), x], [Ket(1), y]) + probs = (d.get_prob(0), d.get_prob(1)) + assert probs[0] == x and probs[1] == y + + +def test_entropy(): + up = JzKet(S.Half, S.Half) + down = JzKet(S.Half, Rational(-1, 2)) + d = Density((up, S.Half), (down, S.Half)) + + # test for density object + ent = entropy(d) + assert entropy(d) == log(2)/2 + assert d.entropy() == log(2)/2 + + np = import_module('numpy', min_module_version='1.4.0') + if np: + #do this test only if 'numpy' is available on test machine + np_mat = represent(d, format='numpy') + ent = entropy(np_mat) + assert isinstance(np_mat, np.ndarray) + assert ent.real == 0.69314718055994529 + assert ent.imag == 0 + + scipy = import_module('scipy', import_kwargs={'fromlist': ['sparse']}) + if scipy and np: + #do this test only if numpy and scipy are available + mat = represent(d, format="scipy.sparse") + assert isinstance(mat, scipy_sparse_matrix) + assert ent.real == 0.69314718055994529 + assert ent.imag == 0 + + +def test_eval_trace(): + up = JzKet(S.Half, S.Half) + down = JzKet(S.Half, Rational(-1, 2)) + d = Density((up, 0.5), (down, 0.5)) + + t = Tr(d) + assert t.doit() == 1.0 + + #test dummy time dependent states + class TestTimeDepKet(TimeDepKet): + def _eval_trace(self, bra, **options): + return 1 + + x, t = symbols('x t') + k1 = TestTimeDepKet(0, 0.5) + k2 = TestTimeDepKet(0, 1) + d = Density([k1, 0.5], [k2, 0.5]) + assert d.doit() == (0.5 * OuterProduct(k1, k1.dual) + + 0.5 * OuterProduct(k2, k2.dual)) + + t = Tr(d) + assert t.doit() == 1.0 + + +def test_fidelity(): + #test with kets + up = JzKet(S.Half, S.Half) + down = JzKet(S.Half, Rational(-1, 2)) + updown = (S.One/sqrt(2))*up + (S.One/sqrt(2))*down + + #check with matrices + up_dm = represent(up * Dagger(up)) + down_dm = represent(down * Dagger(down)) + updown_dm = represent(updown * Dagger(updown)) + + assert abs(fidelity(up_dm, up_dm) - 1) < 1e-3 + assert fidelity(up_dm, down_dm) < 1e-3 + assert abs(fidelity(up_dm, updown_dm) - (S.One/sqrt(2))) < 1e-3 + assert abs(fidelity(updown_dm, down_dm) - (S.One/sqrt(2))) < 1e-3 + + #check with density + up_dm = Density([up, 1.0]) + down_dm = Density([down, 1.0]) + updown_dm = Density([updown, 1.0]) + + assert abs(fidelity(up_dm, up_dm) - 1) < 1e-3 + assert abs(fidelity(up_dm, down_dm)) < 1e-3 + assert abs(fidelity(up_dm, updown_dm) - (S.One/sqrt(2))) < 1e-3 + assert abs(fidelity(updown_dm, down_dm) - (S.One/sqrt(2))) < 1e-3 + + #check mixed states with density + updown2 = sqrt(3)/2*up + S.Half*down + d1 = Density([updown, 0.25], [updown2, 0.75]) + d2 = Density([updown, 0.75], [updown2, 0.25]) + assert abs(fidelity(d1, d2) - 0.991) < 1e-3 + assert abs(fidelity(d2, d1) - fidelity(d1, d2)) < 1e-3 + + #using qubits/density(pure states) + state1 = Qubit('0') + state2 = Qubit('1') + state3 = S.One/sqrt(2)*state1 + S.One/sqrt(2)*state2 + state4 = sqrt(Rational(2, 3))*state1 + S.One/sqrt(3)*state2 + + state1_dm = Density([state1, 1]) + state2_dm = Density([state2, 1]) + state3_dm = Density([state3, 1]) + + assert fidelity(state1_dm, state1_dm) == 1 + assert fidelity(state1_dm, state2_dm) == 0 + assert abs(fidelity(state1_dm, state3_dm) - 1/sqrt(2)) < 1e-3 + assert abs(fidelity(state3_dm, state2_dm) - 1/sqrt(2)) < 1e-3 + + #using qubits/density(mixed states) + d1 = Density([state3, 0.70], [state4, 0.30]) + d2 = Density([state3, 0.20], [state4, 0.80]) + assert abs(fidelity(d1, d1) - 1) < 1e-3 + assert abs(fidelity(d1, d2) - 0.996) < 1e-3 + assert abs(fidelity(d1, d2) - fidelity(d2, d1)) < 1e-3 + + #TODO: test for invalid arguments + # non-square matrix + mat1 = [[0, 0], + [0, 0], + [0, 0]] + + mat2 = [[0, 0], + [0, 0]] + raises(ValueError, lambda: fidelity(mat1, mat2)) + + # unequal dimensions + mat1 = [[0, 0], + [0, 0]] + mat2 = [[0, 0, 0], + [0, 0, 0], + [0, 0, 0]] + raises(ValueError, lambda: fidelity(mat1, mat2)) + + # unsupported data-type + x, y = 1, 2 # random values that is not a matrix + raises(ValueError, lambda: fidelity(x, y)) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_fermion.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_fermion.py new file mode 100644 index 0000000000000000000000000000000000000000..061648c2d5578481196949c38e90ff169fcea972 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_fermion.py @@ -0,0 +1,62 @@ +from pytest import raises + +import sympy +from sympy.physics.quantum import Dagger, AntiCommutator, qapply +from sympy.physics.quantum.fermion import FermionOp +from sympy.physics.quantum.fermion import FermionFockKet, FermionFockBra +from sympy import Symbol + + +def test_fermionoperator(): + c = FermionOp('c') + d = FermionOp('d') + + assert isinstance(c, FermionOp) + assert isinstance(Dagger(c), FermionOp) + + assert c.is_annihilation + assert not Dagger(c).is_annihilation + + assert FermionOp("c") == FermionOp("c", True) + assert FermionOp("c") != FermionOp("d") + assert FermionOp("c", True) != FermionOp("c", False) + + assert AntiCommutator(c, Dagger(c)).doit() == 1 + + assert AntiCommutator(c, Dagger(d)).doit() == c * Dagger(d) + Dagger(d) * c + + +def test_fermion_states(): + c = FermionOp("c") + + # Fock states + assert (FermionFockBra(0) * FermionFockKet(1)).doit() == 0 + assert (FermionFockBra(1) * FermionFockKet(1)).doit() == 1 + + assert qapply(c * FermionFockKet(1)) == FermionFockKet(0) + assert qapply(c * FermionFockKet(0)) == 0 + + assert qapply(Dagger(c) * FermionFockKet(0)) == FermionFockKet(1) + assert qapply(Dagger(c) * FermionFockKet(1)) == 0 + + +def test_power(): + c = FermionOp("c") + assert c**0 == 1 + assert c**1 == c + assert c**2 == 0 + assert c**3 == 0 + assert Dagger(c)**1 == Dagger(c) + assert Dagger(c)**2 == 0 + + assert (c**Symbol('a')).func == sympy.core.power.Pow + assert (c**Symbol('a')).args == (c, Symbol('a')) + + with raises(ValueError): + c**-1 + + with raises(ValueError): + c**3.2 + + with raises(TypeError): + c**1j diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_gate.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_gate.py new file mode 100644 index 0000000000000000000000000000000000000000..2d7bf1d624faca8afe4b10699d23acc161ca0cdd --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_gate.py @@ -0,0 +1,360 @@ +from sympy.core.mul import Mul +from sympy.core.numbers import (I, Integer, Rational, pi) +from sympy.core.symbol import (Wild, symbols) +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.matrices import Matrix, ImmutableMatrix + +from sympy.physics.quantum.gate import (XGate, YGate, ZGate, random_circuit, + CNOT, IdentityGate, H, X, Y, S, T, Z, SwapGate, gate_simp, gate_sort, + CNotGate, TGate, HadamardGate, PhaseGate, UGate, CGate) +from sympy.physics.quantum.commutator import Commutator +from sympy.physics.quantum.anticommutator import AntiCommutator +from sympy.physics.quantum.represent import represent +from sympy.physics.quantum.qapply import qapply +from sympy.physics.quantum.qubit import Qubit, IntQubit, qubit_to_matrix, \ + matrix_to_qubit +from sympy.physics.quantum.matrixutils import matrix_to_zero +from sympy.physics.quantum.matrixcache import sqrt2_inv +from sympy.physics.quantum import Dagger + + +def test_gate(): + """Test a basic gate.""" + h = HadamardGate(1) + assert h.min_qubits == 2 + assert h.nqubits == 1 + + i0 = Wild('i0') + i1 = Wild('i1') + h0_w1 = HadamardGate(i0) + h0_w2 = HadamardGate(i0) + h1_w1 = HadamardGate(i1) + + assert h0_w1 == h0_w2 + assert h0_w1 != h1_w1 + assert h1_w1 != h0_w2 + + cnot_10_w1 = CNOT(i1, i0) + cnot_10_w2 = CNOT(i1, i0) + cnot_01_w1 = CNOT(i0, i1) + + assert cnot_10_w1 == cnot_10_w2 + assert cnot_10_w1 != cnot_01_w1 + assert cnot_10_w2 != cnot_01_w1 + + +def test_UGate(): + a, b, c, d = symbols('a,b,c,d') + uMat = Matrix([[a, b], [c, d]]) + + # Test basic case where gate exists in 1-qubit space + u1 = UGate((0,), uMat) + assert represent(u1, nqubits=1) == uMat + assert qapply(u1*Qubit('0')) == a*Qubit('0') + c*Qubit('1') + assert qapply(u1*Qubit('1')) == b*Qubit('0') + d*Qubit('1') + + # Test case where gate exists in a larger space + u2 = UGate((1,), uMat) + u2Rep = represent(u2, nqubits=2) + for i in range(4): + assert u2Rep*qubit_to_matrix(IntQubit(i, 2)) == \ + qubit_to_matrix(qapply(u2*IntQubit(i, 2))) + + +def test_cgate(): + """Test the general CGate.""" + # Test single control functionality + CNOTMatrix = Matrix( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) + assert represent(CGate(1, XGate(0)), nqubits=2) == CNOTMatrix + + # Test multiple control bit functionality + ToffoliGate = CGate((1, 2), XGate(0)) + assert represent(ToffoliGate, nqubits=3) == \ + Matrix( + [[1, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, + 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 1, 0]]) + + ToffoliGate = CGate((3, 0), XGate(1)) + assert qapply(ToffoliGate*Qubit('1001')) == \ + matrix_to_qubit(represent(ToffoliGate*Qubit('1001'), nqubits=4)) + assert qapply(ToffoliGate*Qubit('0000')) == \ + matrix_to_qubit(represent(ToffoliGate*Qubit('0000'), nqubits=4)) + + CYGate = CGate(1, YGate(0)) + CYGate_matrix = Matrix( + ((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 0, -I), (0, 0, I, 0))) + # Test 2 qubit controlled-Y gate decompose method. + assert represent(CYGate.decompose(), nqubits=2) == CYGate_matrix + + CZGate = CGate(0, ZGate(1)) + CZGate_matrix = Matrix( + ((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, -1))) + assert qapply(CZGate*Qubit('11')) == -Qubit('11') + assert matrix_to_qubit(represent(CZGate*Qubit('11'), nqubits=2)) == \ + -Qubit('11') + # Test 2 qubit controlled-Z gate decompose method. + assert represent(CZGate.decompose(), nqubits=2) == CZGate_matrix + + CPhaseGate = CGate(0, PhaseGate(1)) + assert qapply(CPhaseGate*Qubit('11')) == \ + I*Qubit('11') + assert matrix_to_qubit(represent(CPhaseGate*Qubit('11'), nqubits=2)) == \ + I*Qubit('11') + + # Test that the dagger, inverse, and power of CGate is evaluated properly + assert Dagger(CZGate) == CZGate + assert pow(CZGate, 1) == Dagger(CZGate) + assert Dagger(CZGate) == CZGate.inverse() + assert Dagger(CPhaseGate) != CPhaseGate + assert Dagger(CPhaseGate) == CPhaseGate.inverse() + assert Dagger(CPhaseGate) == pow(CPhaseGate, -1) + assert pow(CPhaseGate, -1) == CPhaseGate.inverse() + + +def test_UGate_CGate_combo(): + a, b, c, d = symbols('a,b,c,d') + uMat = Matrix([[a, b], [c, d]]) + cMat = Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, a, b], [0, 0, c, d]]) + + # Test basic case where gate exists in 1-qubit space. + u1 = UGate((0,), uMat) + cu1 = CGate(1, u1) + assert represent(cu1, nqubits=2) == cMat + assert qapply(cu1*Qubit('10')) == a*Qubit('10') + c*Qubit('11') + assert qapply(cu1*Qubit('11')) == b*Qubit('10') + d*Qubit('11') + assert qapply(cu1*Qubit('01')) == Qubit('01') + assert qapply(cu1*Qubit('00')) == Qubit('00') + + # Test case where gate exists in a larger space. + u2 = UGate((1,), uMat) + u2Rep = represent(u2, nqubits=2) + for i in range(4): + assert u2Rep*qubit_to_matrix(IntQubit(i, 2)) == \ + qubit_to_matrix(qapply(u2*IntQubit(i, 2))) + +def test_UGate_OneQubitGate_combo(): + v, w, f, g = symbols('v w f g') + uMat1 = ImmutableMatrix([[v, w], [f, g]]) + cMat1 = Matrix([[v, w + 1, 0, 0], [f + 1, g, 0, 0], [0, 0, v, w + 1], [0, 0, f + 1, g]]) + u1 = X(0) + UGate(0, uMat1) + assert represent(u1, nqubits=2) == cMat1 + + uMat2 = ImmutableMatrix([[1/sqrt(2), 1/sqrt(2)], [I/sqrt(2), -I/sqrt(2)]]) + cMat2_1 = Matrix([[Rational(1, 2) + I/2, Rational(1, 2) - I/2], + [Rational(1, 2) - I/2, Rational(1, 2) + I/2]]) + cMat2_2 = Matrix([[1, 0], [0, I]]) + u2 = UGate(0, uMat2) + assert represent(H(0)*u2, nqubits=1) == cMat2_1 + assert represent(u2*H(0), nqubits=1) == cMat2_2 + +def test_represent_hadamard(): + """Test the representation of the hadamard gate.""" + circuit = HadamardGate(0)*Qubit('00') + answer = represent(circuit, nqubits=2) + # Check that the answers are same to within an epsilon. + assert answer == Matrix([sqrt2_inv, sqrt2_inv, 0, 0]) + + +def test_represent_xgate(): + """Test the representation of the X gate.""" + circuit = XGate(0)*Qubit('00') + answer = represent(circuit, nqubits=2) + assert Matrix([0, 1, 0, 0]) == answer + + +def test_represent_ygate(): + """Test the representation of the Y gate.""" + circuit = YGate(0)*Qubit('00') + answer = represent(circuit, nqubits=2) + assert answer[0] == 0 and answer[1] == I and \ + answer[2] == 0 and answer[3] == 0 + + +def test_represent_zgate(): + """Test the representation of the Z gate.""" + circuit = ZGate(0)*Qubit('00') + answer = represent(circuit, nqubits=2) + assert Matrix([1, 0, 0, 0]) == answer + + +def test_represent_phasegate(): + """Test the representation of the S gate.""" + circuit = PhaseGate(0)*Qubit('01') + answer = represent(circuit, nqubits=2) + assert Matrix([0, I, 0, 0]) == answer + + +def test_represent_tgate(): + """Test the representation of the T gate.""" + circuit = TGate(0)*Qubit('01') + assert Matrix([0, exp(I*pi/4), 0, 0]) == represent(circuit, nqubits=2) + + +def test_compound_gates(): + """Test a compound gate representation.""" + circuit = YGate(0)*ZGate(0)*XGate(0)*HadamardGate(0)*Qubit('00') + answer = represent(circuit, nqubits=2) + assert Matrix([I/sqrt(2), I/sqrt(2), 0, 0]) == answer + + +def test_cnot_gate(): + """Test the CNOT gate.""" + circuit = CNotGate(1, 0) + assert represent(circuit, nqubits=2) == \ + Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) + circuit = circuit*Qubit('111') + assert matrix_to_qubit(represent(circuit, nqubits=3)) == \ + qapply(circuit) + + circuit = CNotGate(1, 0) + assert Dagger(circuit) == circuit + assert Dagger(Dagger(circuit)) == circuit + assert circuit*circuit == 1 + + +def test_gate_sort(): + """Test gate_sort.""" + for g in (X, Y, Z, H, S, T): + assert gate_sort(g(2)*g(1)*g(0)) == g(0)*g(1)*g(2) + e = gate_sort(X(1)*H(0)**2*CNOT(0, 1)*X(1)*X(0)) + assert e == H(0)**2*CNOT(0, 1)*X(0)*X(1)**2 + assert gate_sort(Z(0)*X(0)) == -X(0)*Z(0) + assert gate_sort(Z(0)*X(0)**2) == X(0)**2*Z(0) + assert gate_sort(Y(0)*H(0)) == -H(0)*Y(0) + assert gate_sort(Y(0)*X(0)) == -X(0)*Y(0) + assert gate_sort(Z(0)*Y(0)) == -Y(0)*Z(0) + assert gate_sort(T(0)*S(0)) == S(0)*T(0) + assert gate_sort(Z(0)*S(0)) == S(0)*Z(0) + assert gate_sort(Z(0)*T(0)) == T(0)*Z(0) + assert gate_sort(Z(0)*CNOT(0, 1)) == CNOT(0, 1)*Z(0) + assert gate_sort(S(0)*CNOT(0, 1)) == CNOT(0, 1)*S(0) + assert gate_sort(T(0)*CNOT(0, 1)) == CNOT(0, 1)*T(0) + assert gate_sort(X(1)*CNOT(0, 1)) == CNOT(0, 1)*X(1) + # This takes a long time and should only be uncommented once in a while. + # nqubits = 5 + # ngates = 10 + # trials = 10 + # for i in range(trials): + # c = random_circuit(ngates, nqubits) + # assert represent(c, nqubits=nqubits) == \ + # represent(gate_sort(c), nqubits=nqubits) + + +def test_gate_simp(): + """Test gate_simp.""" + e = H(0)*X(1)*H(0)**2*CNOT(0, 1)*X(1)**3*X(0)*Z(3)**2*S(4)**3 + assert gate_simp(e) == H(0)*CNOT(0, 1)*S(4)*X(0)*Z(4) + assert gate_simp(X(0)*X(0)) == 1 + assert gate_simp(Y(0)*Y(0)) == 1 + assert gate_simp(Z(0)*Z(0)) == 1 + assert gate_simp(H(0)*H(0)) == 1 + assert gate_simp(T(0)*T(0)) == S(0) + assert gate_simp(S(0)*S(0)) == Z(0) + assert gate_simp(Integer(1)) == Integer(1) + assert gate_simp(X(0)**2 + Y(0)**2) == Integer(2) + + +def test_swap_gate(): + """Test the SWAP gate.""" + swap_gate_matrix = Matrix( + ((1, 0, 0, 0), (0, 0, 1, 0), (0, 1, 0, 0), (0, 0, 0, 1))) + assert represent(SwapGate(1, 0).decompose(), nqubits=2) == swap_gate_matrix + assert qapply(SwapGate(1, 3)*Qubit('0010')) == Qubit('1000') + nqubits = 4 + for i in range(nqubits): + for j in range(i): + assert represent(SwapGate(i, j), nqubits=nqubits) == \ + represent(SwapGate(i, j).decompose(), nqubits=nqubits) + + +def test_one_qubit_commutators(): + """Test single qubit gate commutation relations.""" + for g1 in (IdentityGate, X, Y, Z, H, T, S): + for g2 in (IdentityGate, X, Y, Z, H, T, S): + e = Commutator(g1(0), g2(0)) + a = matrix_to_zero(represent(e, nqubits=1, format='sympy')) + b = matrix_to_zero(represent(e.doit(), nqubits=1, format='sympy')) + assert a == b + + e = Commutator(g1(0), g2(1)) + assert e.doit() == 0 + + +def test_one_qubit_anticommutators(): + """Test single qubit gate anticommutation relations.""" + for g1 in (IdentityGate, X, Y, Z, H): + for g2 in (IdentityGate, X, Y, Z, H): + e = AntiCommutator(g1(0), g2(0)) + a = matrix_to_zero(represent(e, nqubits=1, format='sympy')) + b = matrix_to_zero(represent(e.doit(), nqubits=1, format='sympy')) + assert a == b + e = AntiCommutator(g1(0), g2(1)) + a = matrix_to_zero(represent(e, nqubits=2, format='sympy')) + b = matrix_to_zero(represent(e.doit(), nqubits=2, format='sympy')) + assert a == b + + +def test_cnot_commutators(): + """Test commutators of involving CNOT gates.""" + assert Commutator(CNOT(0, 1), Z(0)).doit() == 0 + assert Commutator(CNOT(0, 1), T(0)).doit() == 0 + assert Commutator(CNOT(0, 1), S(0)).doit() == 0 + assert Commutator(CNOT(0, 1), X(1)).doit() == 0 + assert Commutator(CNOT(0, 1), CNOT(0, 1)).doit() == 0 + assert Commutator(CNOT(0, 1), CNOT(0, 2)).doit() == 0 + assert Commutator(CNOT(0, 2), CNOT(0, 1)).doit() == 0 + assert Commutator(CNOT(1, 2), CNOT(1, 0)).doit() == 0 + + +def test_random_circuit(): + c = random_circuit(10, 3) + assert isinstance(c, Mul) + m = represent(c, nqubits=3) + assert m.shape == (8, 8) + assert isinstance(m, Matrix) + + +def test_hermitian_XGate(): + x = XGate(1, 2) + x_dagger = Dagger(x) + + assert (x == x_dagger) + + +def test_hermitian_YGate(): + y = YGate(1, 2) + y_dagger = Dagger(y) + + assert (y == y_dagger) + + +def test_hermitian_ZGate(): + z = ZGate(1, 2) + z_dagger = Dagger(z) + + assert (z == z_dagger) + + +def test_unitary_XGate(): + x = XGate(1, 2) + x_dagger = Dagger(x) + + assert (x*x_dagger == 1) + + +def test_unitary_YGate(): + y = YGate(1, 2) + y_dagger = Dagger(y) + + assert (y*y_dagger == 1) + + +def test_unitary_ZGate(): + z = ZGate(1, 2) + z_dagger = Dagger(z) + + assert (z*z_dagger == 1) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_grover.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_grover.py new file mode 100644 index 0000000000000000000000000000000000000000..b93a5bc5e59380a993dc34e4a160e75f799b3493 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_grover.py @@ -0,0 +1,92 @@ +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.matrices.dense import Matrix +from sympy.physics.quantum.represent import represent +from sympy.physics.quantum.qapply import qapply +from sympy.physics.quantum.qubit import IntQubit +from sympy.physics.quantum.grover import (apply_grover, superposition_basis, + OracleGate, grover_iteration, WGate) + + +def return_one_on_two(qubits): + return qubits == IntQubit(2, qubits.nqubits) + + +def return_one_on_one(qubits): + return qubits == IntQubit(1, nqubits=qubits.nqubits) + + +def test_superposition_basis(): + nbits = 2 + first_half_state = IntQubit(0, nqubits=nbits)/2 + IntQubit(1, nqubits=nbits)/2 + second_half_state = IntQubit(2, nbits)/2 + IntQubit(3, nbits)/2 + assert first_half_state + second_half_state == superposition_basis(nbits) + + nbits = 3 + firstq = (1/sqrt(8))*IntQubit(0, nqubits=nbits) + (1/sqrt(8))*IntQubit(1, nqubits=nbits) + secondq = (1/sqrt(8))*IntQubit(2, nbits) + (1/sqrt(8))*IntQubit(3, nbits) + thirdq = (1/sqrt(8))*IntQubit(4, nbits) + (1/sqrt(8))*IntQubit(5, nbits) + fourthq = (1/sqrt(8))*IntQubit(6, nbits) + (1/sqrt(8))*IntQubit(7, nbits) + assert firstq + secondq + thirdq + fourthq == superposition_basis(nbits) + + +def test_OracleGate(): + v = OracleGate(1, lambda qubits: qubits == IntQubit(0)) + assert qapply(v*IntQubit(0)) == -IntQubit(0) + assert qapply(v*IntQubit(1)) == IntQubit(1) + + nbits = 2 + v = OracleGate(2, return_one_on_two) + assert qapply(v*IntQubit(0, nbits)) == IntQubit(0, nqubits=nbits) + assert qapply(v*IntQubit(1, nbits)) == IntQubit(1, nqubits=nbits) + assert qapply(v*IntQubit(2, nbits)) == -IntQubit(2, nbits) + assert qapply(v*IntQubit(3, nbits)) == IntQubit(3, nbits) + + assert represent(OracleGate(1, lambda qubits: qubits == IntQubit(0)), nqubits=1) == \ + Matrix([[-1, 0], [0, 1]]) + assert represent(v, nqubits=2) == Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]) + + +def test_WGate(): + nqubits = 2 + basis_states = superposition_basis(nqubits) + assert qapply(WGate(nqubits)*basis_states) == basis_states + + expected = ((2/sqrt(pow(2, nqubits)))*basis_states) - IntQubit(1, nqubits=nqubits) + assert qapply(WGate(nqubits)*IntQubit(1, nqubits=nqubits)) == expected + + +def test_grover_iteration_1(): + numqubits = 2 + basis_states = superposition_basis(numqubits) + v = OracleGate(numqubits, return_one_on_one) + expected = IntQubit(1, nqubits=numqubits) + assert qapply(grover_iteration(basis_states, v)) == expected + + +def test_grover_iteration_2(): + numqubits = 4 + basis_states = superposition_basis(numqubits) + v = OracleGate(numqubits, return_one_on_two) + # After (pi/4)sqrt(pow(2, n)), IntQubit(2) should have highest prob + # In this case, after around pi times (3 or 4) + iterated = grover_iteration(basis_states, v) + iterated = qapply(iterated) + iterated = grover_iteration(iterated, v) + iterated = qapply(iterated) + iterated = grover_iteration(iterated, v) + iterated = qapply(iterated) + # In this case, probability was highest after 3 iterations + # Probability of Qubit('0010') was 251/256 (3) vs 781/1024 (4) + # Ask about measurement + expected = (-13*basis_states)/64 + 264*IntQubit(2, numqubits)/256 + assert qapply(expected) == iterated + + +def test_grover(): + nqubits = 2 + assert apply_grover(return_one_on_one, nqubits) == IntQubit(1, nqubits=nqubits) + + nqubits = 4 + basis_states = superposition_basis(nqubits) + expected = (-13*basis_states)/64 + 264*IntQubit(2, nqubits)/256 + assert apply_grover(return_one_on_two, 4) == qapply(expected) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_hilbert.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_hilbert.py new file mode 100644 index 0000000000000000000000000000000000000000..9a0e5c4187c6c62e14505efb1597a5cd63c23fea --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_hilbert.py @@ -0,0 +1,110 @@ +from sympy.physics.quantum.hilbert import ( + HilbertSpace, ComplexSpace, L2, FockSpace, TensorProductHilbertSpace, + DirectSumHilbertSpace, TensorPowerHilbertSpace +) + +from sympy.core.numbers import oo +from sympy.core.symbol import Symbol +from sympy.printing.repr import srepr +from sympy.printing.str import sstr +from sympy.sets.sets import Interval + + +def test_hilbert_space(): + hs = HilbertSpace() + assert isinstance(hs, HilbertSpace) + assert sstr(hs) == 'H' + assert srepr(hs) == 'HilbertSpace()' + + +def test_complex_space(): + c1 = ComplexSpace(2) + assert isinstance(c1, ComplexSpace) + assert c1.dimension == 2 + assert sstr(c1) == 'C(2)' + assert srepr(c1) == 'ComplexSpace(Integer(2))' + + n = Symbol('n') + c2 = ComplexSpace(n) + assert isinstance(c2, ComplexSpace) + assert c2.dimension == n + assert sstr(c2) == 'C(n)' + assert srepr(c2) == "ComplexSpace(Symbol('n'))" + assert c2.subs(n, 2) == ComplexSpace(2) + + +def test_L2(): + b1 = L2(Interval(-oo, 1)) + assert isinstance(b1, L2) + assert b1.dimension is oo + assert b1.interval == Interval(-oo, 1) + + x = Symbol('x', real=True) + y = Symbol('y', real=True) + b2 = L2(Interval(x, y)) + assert b2.dimension is oo + assert b2.interval == Interval(x, y) + assert b2.subs(x, -1) == L2(Interval(-1, y)) + + +def test_fock_space(): + f1 = FockSpace() + f2 = FockSpace() + assert isinstance(f1, FockSpace) + assert f1.dimension is oo + assert f1 == f2 + + +def test_tensor_product(): + n = Symbol('n') + hs1 = ComplexSpace(2) + hs2 = ComplexSpace(n) + + h = hs1*hs2 + assert isinstance(h, TensorProductHilbertSpace) + assert h.dimension == 2*n + assert h.spaces == (hs1, hs2) + + h = hs2*hs2 + assert isinstance(h, TensorPowerHilbertSpace) + assert h.base == hs2 + assert h.exp == 2 + assert h.dimension == n**2 + + f = FockSpace() + h = hs1*hs2*f + assert h.dimension is oo + + +def test_tensor_power(): + n = Symbol('n') + hs1 = ComplexSpace(2) + hs2 = ComplexSpace(n) + + h = hs1**2 + assert isinstance(h, TensorPowerHilbertSpace) + assert h.base == hs1 + assert h.exp == 2 + assert h.dimension == 4 + + h = hs2**3 + assert isinstance(h, TensorPowerHilbertSpace) + assert h.base == hs2 + assert h.exp == 3 + assert h.dimension == n**3 + + +def test_direct_sum(): + n = Symbol('n') + hs1 = ComplexSpace(2) + hs2 = ComplexSpace(n) + + h = hs1 + hs2 + assert isinstance(h, DirectSumHilbertSpace) + assert h.dimension == 2 + n + assert h.spaces == (hs1, hs2) + + f = FockSpace() + h = hs1 + f + hs2 + assert h.dimension is oo + assert h.spaces == (hs1, f, hs2) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_identitysearch.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_identitysearch.py new file mode 100644 index 0000000000000000000000000000000000000000..8747b1f9d9630e699695f67734333f9d61581fb8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_identitysearch.py @@ -0,0 +1,492 @@ +from sympy.external import import_module +from sympy.core.mul import Mul +from sympy.core.numbers import Integer +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.gate import (X, Y, Z, H, CNOT, + IdentityGate, CGate, PhaseGate, TGate) +from sympy.physics.quantum.identitysearch import (generate_gate_rules, + generate_equivalent_ids, GateIdentity, bfs_identity_search, + is_scalar_sparse_matrix, + is_scalar_nonsparse_matrix, is_degenerate, is_reducible) +from sympy.testing.pytest import skip + + +def create_gate_sequence(qubit=0): + gates = (X(qubit), Y(qubit), Z(qubit), H(qubit)) + return gates + + +def test_generate_gate_rules_1(): + # Test with tuples + (x, y, z, h) = create_gate_sequence() + ph = PhaseGate(0) + cgate_t = CGate(0, TGate(1)) + + assert generate_gate_rules((x,)) == {((x,), ())} + + gate_rules = {((x, x), ()), + ((x,), (x,))} + assert generate_gate_rules((x, x)) == gate_rules + + gate_rules = {((x, y, x), ()), + ((y, x, x), ()), + ((x, x, y), ()), + ((y, x), (x,)), + ((x, y), (x,)), + ((y,), (x, x))} + assert generate_gate_rules((x, y, x)) == gate_rules + + gate_rules = {((x, y, z), ()), ((y, z, x), ()), ((z, x, y), ()), + ((), (x, z, y)), ((), (y, x, z)), ((), (z, y, x)), + ((x,), (z, y)), ((y, z), (x,)), ((y,), (x, z)), + ((z, x), (y,)), ((z,), (y, x)), ((x, y), (z,))} + actual = generate_gate_rules((x, y, z)) + assert actual == gate_rules + + gate_rules = { + ((), (h, z, y, x)), ((), (x, h, z, y)), ((), (y, x, h, z)), + ((), (z, y, x, h)), ((h,), (z, y, x)), ((x,), (h, z, y)), + ((y,), (x, h, z)), ((z,), (y, x, h)), ((h, x), (z, y)), + ((x, y), (h, z)), ((y, z), (x, h)), ((z, h), (y, x)), + ((h, x, y), (z,)), ((x, y, z), (h,)), ((y, z, h), (x,)), + ((z, h, x), (y,)), ((h, x, y, z), ()), ((x, y, z, h), ()), + ((y, z, h, x), ()), ((z, h, x, y), ())} + actual = generate_gate_rules((x, y, z, h)) + assert actual == gate_rules + + gate_rules = {((), (cgate_t**(-1), ph**(-1), x)), + ((), (ph**(-1), x, cgate_t**(-1))), + ((), (x, cgate_t**(-1), ph**(-1))), + ((cgate_t,), (ph**(-1), x)), + ((ph,), (x, cgate_t**(-1))), + ((x,), (cgate_t**(-1), ph**(-1))), + ((cgate_t, x), (ph**(-1),)), + ((ph, cgate_t), (x,)), + ((x, ph), (cgate_t**(-1),)), + ((cgate_t, x, ph), ()), + ((ph, cgate_t, x), ()), + ((x, ph, cgate_t), ())} + actual = generate_gate_rules((x, ph, cgate_t)) + assert actual == gate_rules + + gate_rules = {(Integer(1), cgate_t**(-1)*ph**(-1)*x), + (Integer(1), ph**(-1)*x*cgate_t**(-1)), + (Integer(1), x*cgate_t**(-1)*ph**(-1)), + (cgate_t, ph**(-1)*x), + (ph, x*cgate_t**(-1)), + (x, cgate_t**(-1)*ph**(-1)), + (cgate_t*x, ph**(-1)), + (ph*cgate_t, x), + (x*ph, cgate_t**(-1)), + (cgate_t*x*ph, Integer(1)), + (ph*cgate_t*x, Integer(1)), + (x*ph*cgate_t, Integer(1))} + actual = generate_gate_rules((x, ph, cgate_t), return_as_muls=True) + assert actual == gate_rules + + +def test_generate_gate_rules_2(): + # Test with Muls + (x, y, z, h) = create_gate_sequence() + ph = PhaseGate(0) + cgate_t = CGate(0, TGate(1)) + + # Note: 1 (type int) is not the same as 1 (type One) + expected = {(x, Integer(1))} + assert generate_gate_rules((x,), return_as_muls=True) == expected + + expected = {(Integer(1), Integer(1))} + assert generate_gate_rules(x*x, return_as_muls=True) == expected + + expected = {((), ())} + assert generate_gate_rules(x*x, return_as_muls=False) == expected + + gate_rules = {(x*y*x, Integer(1)), + (y, Integer(1)), + (y*x, x), + (x*y, x)} + assert generate_gate_rules(x*y*x, return_as_muls=True) == gate_rules + + gate_rules = {(x*y*z, Integer(1)), + (y*z*x, Integer(1)), + (z*x*y, Integer(1)), + (Integer(1), x*z*y), + (Integer(1), y*x*z), + (Integer(1), z*y*x), + (x, z*y), + (y*z, x), + (y, x*z), + (z*x, y), + (z, y*x), + (x*y, z)} + actual = generate_gate_rules(x*y*z, return_as_muls=True) + assert actual == gate_rules + + gate_rules = {(Integer(1), h*z*y*x), + (Integer(1), x*h*z*y), + (Integer(1), y*x*h*z), + (Integer(1), z*y*x*h), + (h, z*y*x), (x, h*z*y), + (y, x*h*z), (z, y*x*h), + (h*x, z*y), (z*h, y*x), + (x*y, h*z), (y*z, x*h), + (h*x*y, z), (x*y*z, h), + (y*z*h, x), (z*h*x, y), + (h*x*y*z, Integer(1)), + (x*y*z*h, Integer(1)), + (y*z*h*x, Integer(1)), + (z*h*x*y, Integer(1))} + actual = generate_gate_rules(x*y*z*h, return_as_muls=True) + assert actual == gate_rules + + gate_rules = {(Integer(1), cgate_t**(-1)*ph**(-1)*x), + (Integer(1), ph**(-1)*x*cgate_t**(-1)), + (Integer(1), x*cgate_t**(-1)*ph**(-1)), + (cgate_t, ph**(-1)*x), + (ph, x*cgate_t**(-1)), + (x, cgate_t**(-1)*ph**(-1)), + (cgate_t*x, ph**(-1)), + (ph*cgate_t, x), + (x*ph, cgate_t**(-1)), + (cgate_t*x*ph, Integer(1)), + (ph*cgate_t*x, Integer(1)), + (x*ph*cgate_t, Integer(1))} + actual = generate_gate_rules(x*ph*cgate_t, return_as_muls=True) + assert actual == gate_rules + + gate_rules = {((), (cgate_t**(-1), ph**(-1), x)), + ((), (ph**(-1), x, cgate_t**(-1))), + ((), (x, cgate_t**(-1), ph**(-1))), + ((cgate_t,), (ph**(-1), x)), + ((ph,), (x, cgate_t**(-1))), + ((x,), (cgate_t**(-1), ph**(-1))), + ((cgate_t, x), (ph**(-1),)), + ((ph, cgate_t), (x,)), + ((x, ph), (cgate_t**(-1),)), + ((cgate_t, x, ph), ()), + ((ph, cgate_t, x), ()), + ((x, ph, cgate_t), ())} + actual = generate_gate_rules(x*ph*cgate_t) + assert actual == gate_rules + + +def test_generate_equivalent_ids_1(): + # Test with tuples + (x, y, z, h) = create_gate_sequence() + + assert generate_equivalent_ids((x,)) == {(x,)} + assert generate_equivalent_ids((x, x)) == {(x, x)} + assert generate_equivalent_ids((x, y)) == {(x, y), (y, x)} + + gate_seq = (x, y, z) + gate_ids = {(x, y, z), (y, z, x), (z, x, y), (z, y, x), + (y, x, z), (x, z, y)} + assert generate_equivalent_ids(gate_seq) == gate_ids + + gate_ids = {Mul(x, y, z), Mul(y, z, x), Mul(z, x, y), + Mul(z, y, x), Mul(y, x, z), Mul(x, z, y)} + assert generate_equivalent_ids(gate_seq, return_as_muls=True) == gate_ids + + gate_seq = (x, y, z, h) + gate_ids = {(x, y, z, h), (y, z, h, x), + (h, x, y, z), (h, z, y, x), + (z, y, x, h), (y, x, h, z), + (z, h, x, y), (x, h, z, y)} + assert generate_equivalent_ids(gate_seq) == gate_ids + + gate_seq = (x, y, x, y) + gate_ids = {(x, y, x, y), (y, x, y, x)} + assert generate_equivalent_ids(gate_seq) == gate_ids + + cgate_y = CGate((1,), y) + gate_seq = (y, cgate_y, y, cgate_y) + gate_ids = {(y, cgate_y, y, cgate_y), (cgate_y, y, cgate_y, y)} + assert generate_equivalent_ids(gate_seq) == gate_ids + + cnot = CNOT(1, 0) + cgate_z = CGate((0,), Z(1)) + gate_seq = (cnot, h, cgate_z, h) + gate_ids = {(cnot, h, cgate_z, h), (h, cgate_z, h, cnot), + (h, cnot, h, cgate_z), (cgate_z, h, cnot, h)} + assert generate_equivalent_ids(gate_seq) == gate_ids + + +def test_generate_equivalent_ids_2(): + # Test with Muls + (x, y, z, h) = create_gate_sequence() + + assert generate_equivalent_ids((x,), return_as_muls=True) == {x} + + gate_ids = {Integer(1)} + assert generate_equivalent_ids(x*x, return_as_muls=True) == gate_ids + + gate_ids = {x*y, y*x} + assert generate_equivalent_ids(x*y, return_as_muls=True) == gate_ids + + gate_ids = {(x, y), (y, x)} + assert generate_equivalent_ids(x*y) == gate_ids + + circuit = Mul(*(x, y, z)) + gate_ids = {x*y*z, y*z*x, z*x*y, z*y*x, + y*x*z, x*z*y} + assert generate_equivalent_ids(circuit, return_as_muls=True) == gate_ids + + circuit = Mul(*(x, y, z, h)) + gate_ids = {x*y*z*h, y*z*h*x, + h*x*y*z, h*z*y*x, + z*y*x*h, y*x*h*z, + z*h*x*y, x*h*z*y} + assert generate_equivalent_ids(circuit, return_as_muls=True) == gate_ids + + circuit = Mul(*(x, y, x, y)) + gate_ids = {x*y*x*y, y*x*y*x} + assert generate_equivalent_ids(circuit, return_as_muls=True) == gate_ids + + cgate_y = CGate((1,), y) + circuit = Mul(*(y, cgate_y, y, cgate_y)) + gate_ids = {y*cgate_y*y*cgate_y, cgate_y*y*cgate_y*y} + assert generate_equivalent_ids(circuit, return_as_muls=True) == gate_ids + + cnot = CNOT(1, 0) + cgate_z = CGate((0,), Z(1)) + circuit = Mul(*(cnot, h, cgate_z, h)) + gate_ids = {cnot*h*cgate_z*h, h*cgate_z*h*cnot, + h*cnot*h*cgate_z, cgate_z*h*cnot*h} + assert generate_equivalent_ids(circuit, return_as_muls=True) == gate_ids + + +def test_is_scalar_nonsparse_matrix(): + numqubits = 2 + id_only = False + + id_gate = (IdentityGate(1),) + actual = is_scalar_nonsparse_matrix(id_gate, numqubits, id_only) + assert actual is True + + x0 = X(0) + xx_circuit = (x0, x0) + actual = is_scalar_nonsparse_matrix(xx_circuit, numqubits, id_only) + assert actual is True + + x1 = X(1) + y1 = Y(1) + xy_circuit = (x1, y1) + actual = is_scalar_nonsparse_matrix(xy_circuit, numqubits, id_only) + assert actual is False + + z1 = Z(1) + xyz_circuit = (x1, y1, z1) + actual = is_scalar_nonsparse_matrix(xyz_circuit, numqubits, id_only) + assert actual is True + + cnot = CNOT(1, 0) + cnot_circuit = (cnot, cnot) + actual = is_scalar_nonsparse_matrix(cnot_circuit, numqubits, id_only) + assert actual is True + + h = H(0) + hh_circuit = (h, h) + actual = is_scalar_nonsparse_matrix(hh_circuit, numqubits, id_only) + assert actual is True + + h1 = H(1) + xhzh_circuit = (x1, h1, z1, h1) + actual = is_scalar_nonsparse_matrix(xhzh_circuit, numqubits, id_only) + assert actual is True + + id_only = True + actual = is_scalar_nonsparse_matrix(xhzh_circuit, numqubits, id_only) + assert actual is True + actual = is_scalar_nonsparse_matrix(xyz_circuit, numqubits, id_only) + assert actual is False + actual = is_scalar_nonsparse_matrix(cnot_circuit, numqubits, id_only) + assert actual is True + actual = is_scalar_nonsparse_matrix(hh_circuit, numqubits, id_only) + assert actual is True + + +def test_is_scalar_sparse_matrix(): + np = import_module('numpy') + if not np: + skip("numpy not installed.") + + scipy = import_module('scipy', import_kwargs={'fromlist': ['sparse']}) + if not scipy: + skip("scipy not installed.") + + numqubits = 2 + id_only = False + + id_gate = (IdentityGate(1),) + assert is_scalar_sparse_matrix(id_gate, numqubits, id_only) is True + + x0 = X(0) + xx_circuit = (x0, x0) + assert is_scalar_sparse_matrix(xx_circuit, numqubits, id_only) is True + + x1 = X(1) + y1 = Y(1) + xy_circuit = (x1, y1) + assert is_scalar_sparse_matrix(xy_circuit, numqubits, id_only) is False + + z1 = Z(1) + xyz_circuit = (x1, y1, z1) + assert is_scalar_sparse_matrix(xyz_circuit, numqubits, id_only) is True + + cnot = CNOT(1, 0) + cnot_circuit = (cnot, cnot) + assert is_scalar_sparse_matrix(cnot_circuit, numqubits, id_only) is True + + h = H(0) + hh_circuit = (h, h) + assert is_scalar_sparse_matrix(hh_circuit, numqubits, id_only) is True + + # NOTE: + # The elements of the sparse matrix for the following circuit + # is actually 1.0000000000000002+0.0j. + h1 = H(1) + xhzh_circuit = (x1, h1, z1, h1) + assert is_scalar_sparse_matrix(xhzh_circuit, numqubits, id_only) is True + + id_only = True + assert is_scalar_sparse_matrix(xhzh_circuit, numqubits, id_only) is True + assert is_scalar_sparse_matrix(xyz_circuit, numqubits, id_only) is False + assert is_scalar_sparse_matrix(cnot_circuit, numqubits, id_only) is True + assert is_scalar_sparse_matrix(hh_circuit, numqubits, id_only) is True + + +def test_is_degenerate(): + (x, y, z, h) = create_gate_sequence() + + gate_id = GateIdentity(x, y, z) + ids = {gate_id} + + another_id = (z, y, x) + assert is_degenerate(ids, another_id) is True + + +def test_is_reducible(): + nqubits = 2 + (x, y, z, h) = create_gate_sequence() + + circuit = (x, y, y) + assert is_reducible(circuit, nqubits, 1, 3) is True + + circuit = (x, y, x) + assert is_reducible(circuit, nqubits, 1, 3) is False + + circuit = (x, y, y, x) + assert is_reducible(circuit, nqubits, 0, 4) is True + + circuit = (x, y, y, x) + assert is_reducible(circuit, nqubits, 1, 3) is True + + circuit = (x, y, z, y, y) + assert is_reducible(circuit, nqubits, 1, 5) is True + + +def test_bfs_identity_search(): + assert bfs_identity_search([], 1) == set() + + (x, y, z, h) = create_gate_sequence() + + gate_list = [x] + id_set = {GateIdentity(x, x)} + assert bfs_identity_search(gate_list, 1, max_depth=2) == id_set + + # Set should not contain degenerate quantum circuits + gate_list = [x, y, z] + id_set = {GateIdentity(x, x), + GateIdentity(y, y), + GateIdentity(z, z), + GateIdentity(x, y, z)} + assert bfs_identity_search(gate_list, 1) == id_set + + id_set = {GateIdentity(x, x), + GateIdentity(y, y), + GateIdentity(z, z), + GateIdentity(x, y, z), + GateIdentity(x, y, x, y), + GateIdentity(x, z, x, z), + GateIdentity(y, z, y, z)} + assert bfs_identity_search(gate_list, 1, max_depth=4) == id_set + assert bfs_identity_search(gate_list, 1, max_depth=5) == id_set + + gate_list = [x, y, z, h] + id_set = {GateIdentity(x, x), + GateIdentity(y, y), + GateIdentity(z, z), + GateIdentity(h, h), + GateIdentity(x, y, z), + GateIdentity(x, y, x, y), + GateIdentity(x, z, x, z), + GateIdentity(x, h, z, h), + GateIdentity(y, z, y, z), + GateIdentity(y, h, y, h)} + assert bfs_identity_search(gate_list, 1) == id_set + + id_set = {GateIdentity(x, x), + GateIdentity(y, y), + GateIdentity(z, z), + GateIdentity(h, h)} + assert id_set == bfs_identity_search(gate_list, 1, max_depth=3, + identity_only=True) + + id_set = {GateIdentity(x, x), + GateIdentity(y, y), + GateIdentity(z, z), + GateIdentity(h, h), + GateIdentity(x, y, z), + GateIdentity(x, y, x, y), + GateIdentity(x, z, x, z), + GateIdentity(x, h, z, h), + GateIdentity(y, z, y, z), + GateIdentity(y, h, y, h), + GateIdentity(x, y, h, x, h), + GateIdentity(x, z, h, y, h), + GateIdentity(y, z, h, z, h)} + assert bfs_identity_search(gate_list, 1, max_depth=5) == id_set + + id_set = {GateIdentity(x, x), + GateIdentity(y, y), + GateIdentity(z, z), + GateIdentity(h, h), + GateIdentity(x, h, z, h)} + assert id_set == bfs_identity_search(gate_list, 1, max_depth=4, + identity_only=True) + + cnot = CNOT(1, 0) + gate_list = [x, cnot] + id_set = {GateIdentity(x, x), + GateIdentity(cnot, cnot), + GateIdentity(x, cnot, x, cnot)} + assert bfs_identity_search(gate_list, 2, max_depth=4) == id_set + + cgate_x = CGate((1,), x) + gate_list = [x, cgate_x] + id_set = {GateIdentity(x, x), + GateIdentity(cgate_x, cgate_x), + GateIdentity(x, cgate_x, x, cgate_x)} + assert bfs_identity_search(gate_list, 2, max_depth=4) == id_set + + cgate_z = CGate((0,), Z(1)) + gate_list = [cnot, cgate_z, h] + id_set = {GateIdentity(h, h), + GateIdentity(cgate_z, cgate_z), + GateIdentity(cnot, cnot), + GateIdentity(cnot, h, cgate_z, h)} + assert bfs_identity_search(gate_list, 2, max_depth=4) == id_set + + s = PhaseGate(0) + t = TGate(0) + gate_list = [s, t] + id_set = {GateIdentity(s, s, s, s)} + assert bfs_identity_search(gate_list, 1, max_depth=4) == id_set + + +def test_bfs_identity_search_xfail(): + s = PhaseGate(0) + t = TGate(0) + gate_list = [Dagger(s), t] + id_set = {GateIdentity(Dagger(s), t, t)} + assert bfs_identity_search(gate_list, 1, max_depth=3) == id_set diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_innerproduct.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_innerproduct.py new file mode 100644 index 0000000000000000000000000000000000000000..2632031f8a9a9ec65dfab6d834eb704a00b621d3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_innerproduct.py @@ -0,0 +1,71 @@ +from sympy.core.numbers import (I, Integer) + +from sympy.physics.quantum.innerproduct import InnerProduct +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.state import Bra, Ket, StateBase + + +def test_innerproduct(): + k = Ket('k') + b = Bra('b') + ip = InnerProduct(b, k) + assert isinstance(ip, InnerProduct) + assert ip.bra == b + assert ip.ket == k + assert b*k == InnerProduct(b, k) + assert k*(b*k)*b == k*InnerProduct(b, k)*b + assert InnerProduct(b, k).subs(b, Dagger(k)) == Dagger(k)*k + + +def test_innerproduct_dagger(): + k = Ket('k') + b = Bra('b') + ip = b*k + assert Dagger(ip) == Dagger(k)*Dagger(b) + + +class FooState(StateBase): + pass + + +class FooKet(Ket, FooState): + + @classmethod + def dual_class(self): + return FooBra + + def _eval_innerproduct_FooBra(self, bra): + return Integer(1) + + def _eval_innerproduct_BarBra(self, bra): + return I + + +class FooBra(Bra, FooState): + @classmethod + def dual_class(self): + return FooKet + + +class BarState(StateBase): + pass + + +class BarKet(Ket, BarState): + @classmethod + def dual_class(self): + return BarBra + + +class BarBra(Bra, BarState): + @classmethod + def dual_class(self): + return BarKet + + +def test_doit(): + f = FooKet('foo') + b = BarBra('bar') + assert InnerProduct(b, f).doit() == I + assert InnerProduct(Dagger(f), Dagger(b)).doit() == -I + assert InnerProduct(Dagger(f), f).doit() == Integer(1) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_kind.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_kind.py new file mode 100644 index 0000000000000000000000000000000000000000..e50467db4c2d9bd8e19f4ea883c26bd5ac5bc8d8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_kind.py @@ -0,0 +1,75 @@ +"""Tests for sympy.physics.quantum.kind.""" + +from sympy.core.kind import NumberKind, UndefinedKind +from sympy.core.symbol import symbols + +from sympy.physics.quantum.kind import ( + OperatorKind, KetKind, BraKind +) +from sympy.physics.quantum.anticommutator import AntiCommutator +from sympy.physics.quantum.commutator import Commutator +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.operator import Operator +from sympy.physics.quantum.state import Ket, Bra +from sympy.physics.quantum.tensorproduct import TensorProduct + +k = Ket('k') +b = Bra('k') +A = Operator('A') +B = Operator('B') +x, y, z = symbols('x y z', integer=True) + +def test_bra_ket(): + assert k.kind == KetKind + assert b.kind == BraKind + assert (b*k).kind == NumberKind # inner product + assert (x*k).kind == KetKind + assert (x*b).kind == BraKind + + +def test_operator_kind(): + assert A.kind == OperatorKind + assert (A*B).kind == OperatorKind + assert (x*A).kind == OperatorKind + assert (x*A*B).kind == OperatorKind + assert (x*k*b).kind == OperatorKind # outer product + + +def test_undefind_kind(): + # Because of limitations in the kind dispatcher API, we are currently + # unable to have OperatorKind*KetKind -> KetKind (and similar for bras). + assert (A*k).kind == UndefinedKind + assert (b*A).kind == UndefinedKind + assert (x*b*A*k).kind == UndefinedKind + + +def test_dagger_kind(): + assert Dagger(k).kind == BraKind + assert Dagger(b).kind == KetKind + assert Dagger(A).kind == OperatorKind + + +def test_commutator_kind(): + assert Commutator(A, B).kind == OperatorKind + assert Commutator(A, x*B).kind == OperatorKind + assert Commutator(x*A, B).kind == OperatorKind + assert Commutator(x*A, x*B).kind == OperatorKind + + +def test_anticommutator_kind(): + assert AntiCommutator(A, B).kind == OperatorKind + assert AntiCommutator(A, x*B).kind == OperatorKind + assert AntiCommutator(x*A, B).kind == OperatorKind + assert AntiCommutator(x*A, x*B).kind == OperatorKind + + +def test_tensorproduct_kind(): + assert TensorProduct(k,k).kind == KetKind + assert TensorProduct(b,b).kind == BraKind + assert TensorProduct(x*k,y*k).kind == KetKind + assert TensorProduct(x*b,y*b).kind == BraKind + assert TensorProduct(x*b*k, y*b*k).kind == NumberKind + assert TensorProduct(x*k*b, y*k*b).kind == OperatorKind + assert TensorProduct(A, B).kind == OperatorKind + assert TensorProduct(A, x*B).kind == OperatorKind + assert TensorProduct(x*A, B).kind == OperatorKind diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_matrixutils.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_matrixutils.py new file mode 100644 index 0000000000000000000000000000000000000000..4d4fa8a0a2a4374d200473fa03c68fc453262a4c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_matrixutils.py @@ -0,0 +1,136 @@ +from sympy.core.random import randint + +from sympy.core.numbers import Integer +from sympy.matrices.dense import (Matrix, ones, zeros) + +from sympy.physics.quantum.matrixutils import ( + to_sympy, to_numpy, to_scipy_sparse, matrix_tensor_product, + matrix_to_zero, matrix_zeros, numpy_ndarray, scipy_sparse_matrix +) + +from sympy.external import import_module +from sympy.testing.pytest import skip + +m = Matrix([[1, 2], [3, 4]]) + + +def test_sympy_to_sympy(): + assert to_sympy(m) == m + + +def test_matrix_to_zero(): + assert matrix_to_zero(m) == m + assert matrix_to_zero(Matrix([[0, 0], [0, 0]])) == Integer(0) + +np = import_module('numpy') + + +def test_to_numpy(): + if not np: + skip("numpy not installed.") + + result = np.array([[1, 2], [3, 4]], dtype='complex') + assert (to_numpy(m) == result).all() + + +def test_matrix_tensor_product(): + if not np: + skip("numpy not installed.") + + l1 = zeros(4) + for i in range(16): + l1[i] = 2**i + l2 = zeros(4) + for i in range(16): + l2[i] = i + l3 = zeros(2) + for i in range(4): + l3[i] = i + vec = Matrix([1, 2, 3]) + + #test for Matrix known 4x4 matrices + numpyl1 = np.array(l1.tolist()) + numpyl2 = np.array(l2.tolist()) + numpy_product = np.kron(numpyl1, numpyl2) + args = [l1, l2] + sympy_product = matrix_tensor_product(*args) + assert numpy_product.tolist() == sympy_product.tolist() + numpy_product = np.kron(numpyl2, numpyl1) + args = [l2, l1] + sympy_product = matrix_tensor_product(*args) + assert numpy_product.tolist() == sympy_product.tolist() + + #test for other known matrix of different dimensions + numpyl2 = np.array(l3.tolist()) + numpy_product = np.kron(numpyl1, numpyl2) + args = [l1, l3] + sympy_product = matrix_tensor_product(*args) + assert numpy_product.tolist() == sympy_product.tolist() + numpy_product = np.kron(numpyl2, numpyl1) + args = [l3, l1] + sympy_product = matrix_tensor_product(*args) + assert numpy_product.tolist() == sympy_product.tolist() + + #test for non square matrix + numpyl2 = np.array(vec.tolist()) + numpy_product = np.kron(numpyl1, numpyl2) + args = [l1, vec] + sympy_product = matrix_tensor_product(*args) + assert numpy_product.tolist() == sympy_product.tolist() + numpy_product = np.kron(numpyl2, numpyl1) + args = [vec, l1] + sympy_product = matrix_tensor_product(*args) + assert numpy_product.tolist() == sympy_product.tolist() + + #test for random matrix with random values that are floats + random_matrix1 = np.random.rand(randint(1, 5), randint(1, 5)) + random_matrix2 = np.random.rand(randint(1, 5), randint(1, 5)) + numpy_product = np.kron(random_matrix1, random_matrix2) + args = [Matrix(random_matrix1.tolist()), Matrix(random_matrix2.tolist())] + sympy_product = matrix_tensor_product(*args) + assert not (sympy_product - Matrix(numpy_product.tolist())).tolist() > \ + (ones(sympy_product.rows, sympy_product.cols)*epsilon).tolist() + + #test for three matrix kronecker + sympy_product = matrix_tensor_product(l1, vec, l2) + + numpy_product = np.kron(l1, np.kron(vec, l2)) + assert numpy_product.tolist() == sympy_product.tolist() + + +scipy = import_module('scipy', import_kwargs={'fromlist': ['sparse']}) + + +def test_to_scipy_sparse(): + if not np: + skip("numpy not installed.") + if not scipy: + skip("scipy not installed.") + else: + sparse = scipy.sparse + + result = sparse.csr_matrix([[1, 2], [3, 4]], dtype='complex') + assert np.linalg.norm((to_scipy_sparse(m) - result).todense()) == 0.0 + +epsilon = .000001 + + +def test_matrix_zeros_sympy(): + sym = matrix_zeros(4, 4, format='sympy') + assert isinstance(sym, Matrix) + +def test_matrix_zeros_numpy(): + if not np: + skip("numpy not installed.") + + num = matrix_zeros(4, 4, format='numpy') + assert isinstance(num, numpy_ndarray) + +def test_matrix_zeros_scipy(): + if not np: + skip("numpy not installed.") + if not scipy: + skip("scipy not installed.") + + sci = matrix_zeros(4, 4, format='scipy.sparse') + assert isinstance(sci, scipy_sparse_matrix) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_operator.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_operator.py new file mode 100644 index 0000000000000000000000000000000000000000..100cacd9a800f7c4435b93672ef77877a3a99e5e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_operator.py @@ -0,0 +1,269 @@ +from sympy.core.function import (Derivative, Function, diff) +from sympy.core.mul import Mul +from sympy.core.numbers import (Integer, pi) +from sympy.core.symbol import (Symbol, symbols) +from sympy.core.sympify import sympify +from sympy.functions.elementary.trigonometric import sin +from sympy.physics.quantum.qexpr import QExpr +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.hilbert import HilbertSpace +from sympy.physics.quantum.operator import (Operator, UnitaryOperator, + HermitianOperator, OuterProduct, + DifferentialOperator, + IdentityOperator) +from sympy.physics.quantum.state import Ket, Bra, Wavefunction +from sympy.physics.quantum.qapply import qapply +from sympy.physics.quantum.represent import represent +from sympy.physics.quantum.spin import JzKet, JzBra +from sympy.physics.quantum.trace import Tr +from sympy.matrices import eye + +from sympy.testing.pytest import warns_deprecated_sympy + + +class CustomKet(Ket): + @classmethod + def default_args(self): + return ("t",) + + +class CustomOp(HermitianOperator): + @classmethod + def default_args(self): + return ("T",) + +t_ket = CustomKet() +t_op = CustomOp() + + +def test_operator(): + A = Operator('A') + B = Operator('B') + C = Operator('C') + + assert isinstance(A, Operator) + assert isinstance(A, QExpr) + + assert A.label == (Symbol('A'),) + assert A.is_commutative is False + assert A.hilbert_space == HilbertSpace() + + assert A*B != B*A + + assert (A*(B + C)).expand() == A*B + A*C + assert ((A + B)**2).expand() == A**2 + A*B + B*A + B**2 + + assert t_op.label[0] == Symbol(t_op.default_args()[0]) + + assert Operator() == Operator("O") + with warns_deprecated_sympy(): + assert A*IdentityOperator() == A + + +def test_operator_inv(): + A = Operator('A') + assert A*A.inv() == 1 + assert A.inv()*A == 1 + + +def test_hermitian(): + H = HermitianOperator('H') + + assert isinstance(H, HermitianOperator) + assert isinstance(H, Operator) + + assert Dagger(H) == H + assert H.inv() != H + assert H.is_commutative is False + assert Dagger(H).is_commutative is False + + +def test_unitary(): + U = UnitaryOperator('U') + + assert isinstance(U, UnitaryOperator) + assert isinstance(U, Operator) + + assert U.inv() == Dagger(U) + assert U*Dagger(U) == 1 + assert Dagger(U)*U == 1 + assert U.is_commutative is False + assert Dagger(U).is_commutative is False + + +def test_identity(): + with warns_deprecated_sympy(): + I = IdentityOperator() + O = Operator('O') + x = Symbol("x") + three = sympify(3) + + assert isinstance(I, IdentityOperator) + assert isinstance(I, Operator) + + assert I * O == O + assert O * I == O + assert I * Dagger(O) == Dagger(O) + assert Dagger(O) * I == Dagger(O) + assert isinstance(I * I, IdentityOperator) + assert three * I == three + assert I * x == x + assert I.inv() == I + assert Dagger(I) == I + assert qapply(I * O) == O + assert qapply(O * I) == O + + for n in [2, 3, 5]: + assert represent(IdentityOperator(n)) == eye(n) + + +def test_outer_product(): + k = Ket('k') + b = Bra('b') + op = OuterProduct(k, b) + + assert isinstance(op, OuterProduct) + assert isinstance(op, Operator) + + assert op.ket == k + assert op.bra == b + assert op.label == (k, b) + assert op.is_commutative is False + + op = k*b + + assert isinstance(op, OuterProduct) + assert isinstance(op, Operator) + + assert op.ket == k + assert op.bra == b + assert op.label == (k, b) + assert op.is_commutative is False + + op = 2*k*b + + assert op == Mul(Integer(2), k, b) + + op = 2*(k*b) + + assert op == Mul(Integer(2), OuterProduct(k, b)) + + assert Dagger(k*b) == OuterProduct(Dagger(b), Dagger(k)) + assert Dagger(k*b).is_commutative is False + + #test the _eval_trace + assert Tr(OuterProduct(JzKet(1, 1), JzBra(1, 1))).doit() == 1 + + # test scaled kets and bras + assert OuterProduct(2 * k, b) == 2 * OuterProduct(k, b) + assert OuterProduct(k, 2 * b) == 2 * OuterProduct(k, b) + + # test sums of kets and bras + k1, k2 = Ket('k1'), Ket('k2') + b1, b2 = Bra('b1'), Bra('b2') + assert (OuterProduct(k1 + k2, b1) == + OuterProduct(k1, b1) + OuterProduct(k2, b1)) + assert (OuterProduct(k1, b1 + b2) == + OuterProduct(k1, b1) + OuterProduct(k1, b2)) + assert (OuterProduct(1 * k1 + 2 * k2, 3 * b1 + 4 * b2) == + 3 * OuterProduct(k1, b1) + + 4 * OuterProduct(k1, b2) + + 6 * OuterProduct(k2, b1) + + 8 * OuterProduct(k2, b2)) + + +def test_operator_dagger(): + A = Operator('A') + B = Operator('B') + assert Dagger(A*B) == Dagger(B)*Dagger(A) + assert Dagger(A + B) == Dagger(A) + Dagger(B) + assert Dagger(A**2) == Dagger(A)**2 + + +def test_differential_operator(): + x = Symbol('x') + f = Function('f') + d = DifferentialOperator(Derivative(f(x), x), f(x)) + g = Wavefunction(x**2, x) + assert qapply(d*g) == Wavefunction(2*x, x) + assert d.expr == Derivative(f(x), x) + assert d.function == f(x) + assert d.variables == (x,) + assert diff(d, x) == DifferentialOperator(Derivative(f(x), x, 2), f(x)) + + d = DifferentialOperator(Derivative(f(x), x, 2), f(x)) + g = Wavefunction(x**3, x) + assert qapply(d*g) == Wavefunction(6*x, x) + assert d.expr == Derivative(f(x), x, 2) + assert d.function == f(x) + assert d.variables == (x,) + assert diff(d, x) == DifferentialOperator(Derivative(f(x), x, 3), f(x)) + + d = DifferentialOperator(1/x*Derivative(f(x), x), f(x)) + assert d.expr == 1/x*Derivative(f(x), x) + assert d.function == f(x) + assert d.variables == (x,) + assert diff(d, x) == \ + DifferentialOperator(Derivative(1/x*Derivative(f(x), x), x), f(x)) + assert qapply(d*g) == Wavefunction(3*x, x) + + # 2D cartesian Laplacian + y = Symbol('y') + d = DifferentialOperator(Derivative(f(x, y), x, 2) + + Derivative(f(x, y), y, 2), f(x, y)) + w = Wavefunction(x**3*y**2 + y**3*x**2, x, y) + assert d.expr == Derivative(f(x, y), x, 2) + Derivative(f(x, y), y, 2) + assert d.function == f(x, y) + assert d.variables == (x, y) + assert diff(d, x) == \ + DifferentialOperator(Derivative(d.expr, x), f(x, y)) + assert diff(d, y) == \ + DifferentialOperator(Derivative(d.expr, y), f(x, y)) + assert qapply(d*w) == Wavefunction(2*x**3 + 6*x*y**2 + 6*x**2*y + 2*y**3, + x, y) + + # 2D polar Laplacian (th = theta) + r, th = symbols('r th') + d = DifferentialOperator(1/r*Derivative(r*Derivative(f(r, th), r), r) + + 1/(r**2)*Derivative(f(r, th), th, 2), f(r, th)) + w = Wavefunction(r**2*sin(th), r, (th, 0, pi)) + assert d.expr == \ + 1/r*Derivative(r*Derivative(f(r, th), r), r) + \ + 1/(r**2)*Derivative(f(r, th), th, 2) + assert d.function == f(r, th) + assert d.variables == (r, th) + assert diff(d, r) == \ + DifferentialOperator(Derivative(d.expr, r), f(r, th)) + assert diff(d, th) == \ + DifferentialOperator(Derivative(d.expr, th), f(r, th)) + assert qapply(d*w) == Wavefunction(3*sin(th), r, (th, 0, pi)) + + +def test_eval_power(): + from sympy.core import Pow + from sympy.core.expr import unchanged + O = Operator('O') + U = UnitaryOperator('U') + H = HermitianOperator('H') + assert O**-1 == O.inv() # same as doc test + assert U**-1 == U.inv() + assert H**-1 == H.inv() + x = symbols("x", commutative = True) + assert unchanged(Pow, H, x) # verify Pow(H,x)=="X^n" + assert H**x == Pow(H, x) + assert Pow(H,x) == Pow(H, x, evaluate=False) # Just check + from sympy.physics.quantum.gate import XGate + X = XGate(0) # is hermitian and unitary + assert unchanged(Pow, X, x) # verify Pow(X,x)=="X^x" + assert X**x == Pow(X, x) + assert Pow(X, x, evaluate=False) == Pow(X, x) # Just check + n = symbols("n", integer=True, even=True) + assert X**n == 1 + n = symbols("n", integer=True, odd=True) + assert X**n == X + n = symbols("n", integer=True) + assert unchanged(Pow, X, n) # verify Pow(X,n)=="X^n" + assert X**n == Pow(X, n) + assert Pow(X, n, evaluate=False)==Pow(X, n) # Just check + assert X**4 == 1 + assert X**7 == X diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_operatorordering.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_operatorordering.py new file mode 100644 index 0000000000000000000000000000000000000000..f5255d555d1582b694dfe4ed681d894136ea0b70 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_operatorordering.py @@ -0,0 +1,50 @@ +from sympy.physics.quantum import Dagger +from sympy.physics.quantum.boson import BosonOp +from sympy.physics.quantum.fermion import FermionOp +from sympy.physics.quantum.operatorordering import (normal_order, + normal_ordered_form) + + +def test_normal_order(): + a = BosonOp('a') + + c = FermionOp('c') + + assert normal_order(a * Dagger(a)) == Dagger(a) * a + assert normal_order(Dagger(a) * a) == Dagger(a) * a + assert normal_order(a * Dagger(a) ** 2) == Dagger(a) ** 2 * a + + assert normal_order(c * Dagger(c)) == - Dagger(c) * c + assert normal_order(Dagger(c) * c) == Dagger(c) * c + assert normal_order(c * Dagger(c) ** 2) == Dagger(c) ** 2 * c + + +def test_normal_ordered_form(): + a = BosonOp('a') + b = BosonOp('b') + + c = FermionOp('c') + d = FermionOp('d') + + assert normal_ordered_form(Dagger(a) * a) == Dagger(a) * a + assert normal_ordered_form(a * Dagger(a)) == 1 + Dagger(a) * a + assert normal_ordered_form(a ** 2 * Dagger(a)) == \ + 2 * a + Dagger(a) * a ** 2 + assert normal_ordered_form(a ** 3 * Dagger(a)) == \ + 3 * a ** 2 + Dagger(a) * a ** 3 + + assert normal_ordered_form(Dagger(c) * c) == Dagger(c) * c + assert normal_ordered_form(c * Dagger(c)) == 1 - Dagger(c) * c + assert normal_ordered_form(c ** 2 * Dagger(c)) == Dagger(c) * c ** 2 + assert normal_ordered_form(c ** 3 * Dagger(c)) == \ + c ** 2 - Dagger(c) * c ** 3 + + assert normal_ordered_form(a * Dagger(b), True) == Dagger(b) * a + assert normal_ordered_form(Dagger(a) * b, True) == Dagger(a) * b + assert normal_ordered_form(b * a, True) == a * b + assert normal_ordered_form(Dagger(b) * Dagger(a), True) == Dagger(a) * Dagger(b) + + assert normal_ordered_form(c * Dagger(d), True) == -Dagger(d) * c + assert normal_ordered_form(Dagger(c) * d, True) == Dagger(c) * d + assert normal_ordered_form(d * c, True) == -c * d + assert normal_ordered_form(Dagger(d) * Dagger(c), True) == -Dagger(c) * Dagger(d) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_operatorset.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_operatorset.py new file mode 100644 index 0000000000000000000000000000000000000000..fff038bb12a7e6aa100ac00b0e145dc323a77e4d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_operatorset.py @@ -0,0 +1,68 @@ +from sympy.core.singleton import S + +from sympy.physics.quantum.operatorset import ( + operators_to_state, state_to_operators +) + +from sympy.physics.quantum.cartesian import ( + XOp, XKet, PxOp, PxKet, XBra, PxBra +) + +from sympy.physics.quantum.state import Ket, Bra +from sympy.physics.quantum.operator import Operator +from sympy.physics.quantum.spin import ( + JxKet, JyKet, JzKet, JxBra, JyBra, JzBra, + JxOp, JyOp, JzOp, J2Op +) + +from sympy.testing.pytest import raises + + +def test_spin(): + assert operators_to_state({J2Op, JxOp}) == JxKet + assert operators_to_state({J2Op, JyOp}) == JyKet + assert operators_to_state({J2Op, JzOp}) == JzKet + assert operators_to_state({J2Op(), JxOp()}) == JxKet + assert operators_to_state({J2Op(), JyOp()}) == JyKet + assert operators_to_state({J2Op(), JzOp()}) == JzKet + + assert state_to_operators(JxKet) == {J2Op, JxOp} + assert state_to_operators(JyKet) == {J2Op, JyOp} + assert state_to_operators(JzKet) == {J2Op, JzOp} + assert state_to_operators(JxBra) == {J2Op, JxOp} + assert state_to_operators(JyBra) == {J2Op, JyOp} + assert state_to_operators(JzBra) == {J2Op, JzOp} + + assert state_to_operators(JxKet(S.Half, S.Half)) == {J2Op(), JxOp()} + assert state_to_operators(JyKet(S.Half, S.Half)) == {J2Op(), JyOp()} + assert state_to_operators(JzKet(S.Half, S.Half)) == {J2Op(), JzOp()} + assert state_to_operators(JxBra(S.Half, S.Half)) == {J2Op(), JxOp()} + assert state_to_operators(JyBra(S.Half, S.Half)) == {J2Op(), JyOp()} + assert state_to_operators(JzBra(S.Half, S.Half)) == {J2Op(), JzOp()} + + +def test_op_to_state(): + assert operators_to_state(XOp) == XKet() + assert operators_to_state(PxOp) == PxKet() + assert operators_to_state(Operator) == Ket() + + assert state_to_operators(operators_to_state(XOp("Q"))) == XOp("Q") + assert state_to_operators(operators_to_state(XOp())) == XOp() + + raises(NotImplementedError, lambda: operators_to_state(XKet)) + + +def test_state_to_op(): + assert state_to_operators(XKet) == XOp() + assert state_to_operators(PxKet) == PxOp() + assert state_to_operators(XBra) == XOp() + assert state_to_operators(PxBra) == PxOp() + assert state_to_operators(Ket) == Operator() + assert state_to_operators(Bra) == Operator() + + assert operators_to_state(state_to_operators(XKet("test"))) == XKet("test") + assert operators_to_state(state_to_operators(XBra("test"))) == XKet("test") + assert operators_to_state(state_to_operators(XKet())) == XKet() + assert operators_to_state(state_to_operators(XBra())) == XKet() + + raises(NotImplementedError, lambda: state_to_operators(XOp)) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_pauli.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_pauli.py new file mode 100644 index 0000000000000000000000000000000000000000..77bbed93ac5b4b49680be01aefa2f779b62fc7ee --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_pauli.py @@ -0,0 +1,159 @@ +from sympy.core.mul import Mul +from sympy.core.numbers import I +from sympy.matrices.dense import Matrix +from sympy.printing.latex import latex +from sympy.physics.quantum import (Dagger, Commutator, AntiCommutator, qapply, + Operator, represent) +from sympy.physics.quantum.pauli import (SigmaOpBase, SigmaX, SigmaY, SigmaZ, + SigmaMinus, SigmaPlus, + qsimplify_pauli) +from sympy.physics.quantum.pauli import SigmaZKet, SigmaZBra +from sympy.testing.pytest import raises + + +sx, sy, sz = SigmaX(), SigmaY(), SigmaZ() +sx1, sy1, sz1 = SigmaX(1), SigmaY(1), SigmaZ(1) +sx2, sy2, sz2 = SigmaX(2), SigmaY(2), SigmaZ(2) + +sm, sp = SigmaMinus(), SigmaPlus() +sm1, sp1 = SigmaMinus(1), SigmaPlus(1) +A, B = Operator("A"), Operator("B") + + +def test_pauli_operators_types(): + + assert isinstance(sx, SigmaOpBase) and isinstance(sx, SigmaX) + assert isinstance(sy, SigmaOpBase) and isinstance(sy, SigmaY) + assert isinstance(sz, SigmaOpBase) and isinstance(sz, SigmaZ) + assert isinstance(sm, SigmaOpBase) and isinstance(sm, SigmaMinus) + assert isinstance(sp, SigmaOpBase) and isinstance(sp, SigmaPlus) + + +def test_pauli_operators_commutator(): + + assert Commutator(sx, sy).doit() == 2 * I * sz + assert Commutator(sy, sz).doit() == 2 * I * sx + assert Commutator(sz, sx).doit() == 2 * I * sy + + +def test_pauli_operators_commutator_with_labels(): + + assert Commutator(sx1, sy1).doit() == 2 * I * sz1 + assert Commutator(sy1, sz1).doit() == 2 * I * sx1 + assert Commutator(sz1, sx1).doit() == 2 * I * sy1 + + assert Commutator(sx2, sy2).doit() == 2 * I * sz2 + assert Commutator(sy2, sz2).doit() == 2 * I * sx2 + assert Commutator(sz2, sx2).doit() == 2 * I * sy2 + + assert Commutator(sx1, sy2).doit() == 0 + assert Commutator(sy1, sz2).doit() == 0 + assert Commutator(sz1, sx2).doit() == 0 + + +def test_pauli_operators_anticommutator(): + + assert AntiCommutator(sy, sz).doit() == 0 + assert AntiCommutator(sz, sx).doit() == 0 + assert AntiCommutator(sx, sm).doit() == 1 + assert AntiCommutator(sx, sp).doit() == 1 + + +def test_pauli_operators_adjoint(): + + assert Dagger(sx) == sx + assert Dagger(sy) == sy + assert Dagger(sz) == sz + + +def test_pauli_operators_adjoint_with_labels(): + + assert Dagger(sx1) == sx1 + assert Dagger(sy1) == sy1 + assert Dagger(sz1) == sz1 + + assert Dagger(sx1) != sx2 + assert Dagger(sy1) != sy2 + assert Dagger(sz1) != sz2 + + +def test_pauli_operators_multiplication(): + + assert qsimplify_pauli(sx * sx) == 1 + assert qsimplify_pauli(sy * sy) == 1 + assert qsimplify_pauli(sz * sz) == 1 + + assert qsimplify_pauli(sx * sy) == I * sz + assert qsimplify_pauli(sy * sz) == I * sx + assert qsimplify_pauli(sz * sx) == I * sy + + assert qsimplify_pauli(sy * sx) == - I * sz + assert qsimplify_pauli(sz * sy) == - I * sx + assert qsimplify_pauli(sx * sz) == - I * sy + + +def test_pauli_operators_multiplication_with_labels(): + + assert qsimplify_pauli(sx1 * sx1) == 1 + assert qsimplify_pauli(sy1 * sy1) == 1 + assert qsimplify_pauli(sz1 * sz1) == 1 + + assert isinstance(sx1 * sx2, Mul) + assert isinstance(sy1 * sy2, Mul) + assert isinstance(sz1 * sz2, Mul) + + assert qsimplify_pauli(sx1 * sy1 * sx2 * sy2) == - sz1 * sz2 + assert qsimplify_pauli(sy1 * sz1 * sz2 * sx2) == - sx1 * sy2 + + +def test_pauli_states(): + sx, sz = SigmaX(), SigmaZ() + + up = SigmaZKet(0) + down = SigmaZKet(1) + + assert qapply(sx * up) == down + assert qapply(sx * down) == up + assert qapply(sz * up) == up + assert qapply(sz * down) == - down + + up = SigmaZBra(0) + down = SigmaZBra(1) + + assert qapply(up * sx, dagger=True) == down + assert qapply(down * sx, dagger=True) == up + assert qapply(up * sz, dagger=True) == up + assert qapply(down * sz, dagger=True) == - down + + assert Dagger(SigmaZKet(0)) == SigmaZBra(0) + assert Dagger(SigmaZBra(1)) == SigmaZKet(1) + raises(ValueError, lambda: SigmaZBra(2)) + raises(ValueError, lambda: SigmaZKet(2)) + + +def test_use_name(): + assert sm.use_name is False + assert sm1.use_name is True + assert sx.use_name is False + assert sx1.use_name is True + + +def test_printing(): + assert latex(sx) == r'{\sigma_x}' + assert latex(sx1) == r'{\sigma_x^{(1)}}' + assert latex(sy) == r'{\sigma_y}' + assert latex(sy1) == r'{\sigma_y^{(1)}}' + assert latex(sz) == r'{\sigma_z}' + assert latex(sz1) == r'{\sigma_z^{(1)}}' + assert latex(sm) == r'{\sigma_-}' + assert latex(sm1) == r'{\sigma_-^{(1)}}' + assert latex(sp) == r'{\sigma_+}' + assert latex(sp1) == r'{\sigma_+^{(1)}}' + + +def test_represent(): + assert represent(sx) == Matrix([[0, 1], [1, 0]]) + assert represent(sy) == Matrix([[0, -I], [I, 0]]) + assert represent(sz) == Matrix([[1, 0], [0, -1]]) + assert represent(sm) == Matrix([[0, 0], [1, 0]]) + assert represent(sp) == Matrix([[0, 1], [0, 0]]) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_piab.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_piab.py new file mode 100644 index 0000000000000000000000000000000000000000..3a4c2540b3269593c74bdbae93bf72d131a94ed9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_piab.py @@ -0,0 +1,29 @@ +"""Tests for piab.py""" + +from sympy.core.numbers import pi +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin +from sympy.sets.sets import Interval +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.physics.quantum import L2, qapply, hbar, represent +from sympy.physics.quantum.piab import PIABHamiltonian, PIABKet, PIABBra, m, L + +i, j, n, x = symbols('i j n x') + + +def test_H(): + assert PIABHamiltonian('H').hilbert_space == \ + L2(Interval(S.NegativeInfinity, S.Infinity)) + assert qapply(PIABHamiltonian('H')*PIABKet(n)) == \ + (n**2*pi**2*hbar**2)/(2*m*L**2)*PIABKet(n) + + +def test_states(): + assert PIABKet(n).dual_class() == PIABBra + assert PIABKet(n).hilbert_space == \ + L2(Interval(S.NegativeInfinity, S.Infinity)) + assert represent(PIABKet(n)) == sqrt(2/L)*sin(n*pi*x/L) + assert (PIABBra(i)*PIABKet(j)).doit() == KroneckerDelta(i, j) + assert PIABBra(n).dual_class() == PIABKet diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_printing.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_printing.py new file mode 100644 index 0000000000000000000000000000000000000000..ce4004cee2f9e57b1c9e435f13a6850b92d929b3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_printing.py @@ -0,0 +1,900 @@ +# -*- encoding: utf-8 -*- +""" +TODO: +* Address Issue 2251, printing of spin states +""" +from __future__ import annotations +from typing import Any + +from sympy.physics.quantum.anticommutator import AntiCommutator +from sympy.physics.quantum.cg import CG, Wigner3j, Wigner6j, Wigner9j +from sympy.physics.quantum.commutator import Commutator +from sympy.physics.quantum.constants import hbar +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.gate import CGate, CNotGate, IdentityGate, UGate, XGate +from sympy.physics.quantum.hilbert import ComplexSpace, FockSpace, HilbertSpace, L2 +from sympy.physics.quantum.innerproduct import InnerProduct +from sympy.physics.quantum.operator import Operator, OuterProduct, DifferentialOperator +from sympy.physics.quantum.qexpr import QExpr +from sympy.physics.quantum.qubit import Qubit, IntQubit +from sympy.physics.quantum.spin import Jz, J2, JzBra, JzBraCoupled, JzKet, JzKetCoupled, Rotation, WignerD +from sympy.physics.quantum.state import Bra, Ket, TimeDepBra, TimeDepKet +from sympy.physics.quantum.tensorproduct import TensorProduct +from sympy.physics.quantum.sho1d import RaisingOp + +from sympy.core.function import (Derivative, Function) +from sympy.core.numbers import oo +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.matrices.dense import Matrix +from sympy.sets.sets import Interval +from sympy.testing.pytest import XFAIL + +# Imports used in srepr strings +from sympy.physics.quantum.spin import JzOp + +from sympy.printing import srepr +from sympy.printing.pretty import pretty as xpretty +from sympy.printing.latex import latex + +MutableDenseMatrix = Matrix + + +ENV: dict[str, Any] = {} +exec('from sympy import *', ENV) +exec('from sympy.physics.quantum import *', ENV) +exec('from sympy.physics.quantum.cg import *', ENV) +exec('from sympy.physics.quantum.spin import *', ENV) +exec('from sympy.physics.quantum.hilbert import *', ENV) +exec('from sympy.physics.quantum.qubit import *', ENV) +exec('from sympy.physics.quantum.qexpr import *', ENV) +exec('from sympy.physics.quantum.gate import *', ENV) +exec('from sympy.physics.quantum.constants import *', ENV) + + +def sT(expr, string): + """ + sT := sreprTest + from sympy/printing/tests/test_repr.py + """ + assert srepr(expr) == string + assert eval(string, ENV) == expr + + +def pretty(expr): + """ASCII pretty-printing""" + return xpretty(expr, use_unicode=False, wrap_line=False) + + +def upretty(expr): + """Unicode pretty-printing""" + return xpretty(expr, use_unicode=True, wrap_line=False) + + +def test_anticommutator(): + A = Operator('A') + B = Operator('B') + ac = AntiCommutator(A, B) + ac_tall = AntiCommutator(A**2, B) + assert str(ac) == '{A,B}' + assert pretty(ac) == '{A,B}' + assert upretty(ac) == '{A,B}' + assert latex(ac) == r'\left\{A,B\right\}' + sT(ac, "AntiCommutator(Operator(Symbol('A')),Operator(Symbol('B')))") + assert str(ac_tall) == '{A**2,B}' + ascii_str = \ +"""\ +/ 2 \\\n\ +\n\ +\\ /\ +""" + ucode_str = \ +"""\ +⎧ 2 ⎫\n\ +⎨A ,B⎬\n\ +⎩ ⎭\ +""" + assert pretty(ac_tall) == ascii_str + assert upretty(ac_tall) == ucode_str + assert latex(ac_tall) == r'\left\{A^{2},B\right\}' + sT(ac_tall, "AntiCommutator(Pow(Operator(Symbol('A')), Integer(2)),Operator(Symbol('B')))") + + +def test_cg(): + cg = CG(1, 2, 3, 4, 5, 6) + wigner3j = Wigner3j(1, 2, 3, 4, 5, 6) + wigner6j = Wigner6j(1, 2, 3, 4, 5, 6) + wigner9j = Wigner9j(1, 2, 3, 4, 5, 6, 7, 8, 9) + assert str(cg) == 'CG(1, 2, 3, 4, 5, 6)' + ascii_str = \ +"""\ + 5,6 \n\ +C \n\ + 1,2,3,4\ +""" + ucode_str = \ +"""\ + 5,6 \n\ +C \n\ + 1,2,3,4\ +""" + assert pretty(cg) == ascii_str + assert upretty(cg) == ucode_str + assert latex(cg) == 'C^{5,6}_{1,2,3,4}' + assert latex(cg ** 2) == R'\left(C^{5,6}_{1,2,3,4}\right)^{2}' + sT(cg, "CG(Integer(1), Integer(2), Integer(3), Integer(4), Integer(5), Integer(6))") + assert str(wigner3j) == 'Wigner3j(1, 2, 3, 4, 5, 6)' + ascii_str = \ +"""\ +/1 3 5\\\n\ +| |\n\ +\\2 4 6/\ +""" + ucode_str = \ +"""\ +⎛1 3 5⎞\n\ +⎜ ⎟\n\ +⎝2 4 6⎠\ +""" + assert pretty(wigner3j) == ascii_str + assert upretty(wigner3j) == ucode_str + assert latex(wigner3j) == \ + r'\left(\begin{array}{ccc} 1 & 3 & 5 \\ 2 & 4 & 6 \end{array}\right)' + sT(wigner3j, "Wigner3j(Integer(1), Integer(2), Integer(3), Integer(4), Integer(5), Integer(6))") + assert str(wigner6j) == 'Wigner6j(1, 2, 3, 4, 5, 6)' + ascii_str = \ +"""\ +/1 2 3\\\n\ +< >\n\ +\\4 5 6/\ +""" + ucode_str = \ +"""\ +⎧1 2 3⎫\n\ +⎨ ⎬\n\ +⎩4 5 6⎭\ +""" + assert pretty(wigner6j) == ascii_str + assert upretty(wigner6j) == ucode_str + assert latex(wigner6j) == \ + r'\left\{\begin{array}{ccc} 1 & 2 & 3 \\ 4 & 5 & 6 \end{array}\right\}' + sT(wigner6j, "Wigner6j(Integer(1), Integer(2), Integer(3), Integer(4), Integer(5), Integer(6))") + assert str(wigner9j) == 'Wigner9j(1, 2, 3, 4, 5, 6, 7, 8, 9)' + ascii_str = \ +"""\ +/1 2 3\\\n\ +| |\n\ +<4 5 6>\n\ +| |\n\ +\\7 8 9/\ +""" + ucode_str = \ +"""\ +⎧1 2 3⎫\n\ +⎪ ⎪\n\ +⎨4 5 6⎬\n\ +⎪ ⎪\n\ +⎩7 8 9⎭\ +""" + assert pretty(wigner9j) == ascii_str + assert upretty(wigner9j) == ucode_str + assert latex(wigner9j) == \ + r'\left\{\begin{array}{ccc} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{array}\right\}' + sT(wigner9j, "Wigner9j(Integer(1), Integer(2), Integer(3), Integer(4), Integer(5), Integer(6), Integer(7), Integer(8), Integer(9))") + + +def test_commutator(): + A = Operator('A') + B = Operator('B') + c = Commutator(A, B) + c_tall = Commutator(A**2, B) + assert str(c) == '[A,B]' + assert pretty(c) == '[A,B]' + assert upretty(c) == '[A,B]' + assert latex(c) == r'\left[A,B\right]' + sT(c, "Commutator(Operator(Symbol('A')),Operator(Symbol('B')))") + assert str(c_tall) == '[A**2,B]' + ascii_str = \ +"""\ +[ 2 ]\n\ +[A ,B]\ +""" + ucode_str = \ +"""\ +⎡ 2 ⎤\n\ +⎣A ,B⎦\ +""" + assert pretty(c_tall) == ascii_str + assert upretty(c_tall) == ucode_str + assert latex(c_tall) == r'\left[A^{2},B\right]' + sT(c_tall, "Commutator(Pow(Operator(Symbol('A')), Integer(2)),Operator(Symbol('B')))") + + +def test_constants(): + assert str(hbar) == 'hbar' + assert pretty(hbar) == 'hbar' + assert upretty(hbar) == 'ℏ' + assert latex(hbar) == r'\hbar' + sT(hbar, "HBar()") + + +def test_dagger(): + x = symbols('x', commutative=False) + expr = Dagger(x) + assert str(expr) == 'Dagger(x)' + ascii_str = \ +"""\ + +\n\ +x \ +""" + ucode_str = \ +"""\ + †\n\ +x \ +""" + assert pretty(expr) == ascii_str + assert upretty(expr) == ucode_str + assert latex(expr) == r'x^{\dagger}' + sT(expr, "Dagger(Symbol('x', commutative=False))") + + +@XFAIL +def test_gate_failing(): + a, b, c, d = symbols('a,b,c,d') + uMat = Matrix([[a, b], [c, d]]) + g = UGate((0,), uMat) + assert str(g) == 'U(0)' + + +def test_gate(): + a, b, c, d = symbols('a,b,c,d') + uMat = Matrix([[a, b], [c, d]]) + q = Qubit(1, 0, 1, 0, 1) + g1 = IdentityGate(2) + g2 = CGate((3, 0), XGate(1)) + g3 = CNotGate(1, 0) + g4 = UGate((0,), uMat) + assert str(g1) == '1(2)' + assert pretty(g1) == '1 \n 2' + assert upretty(g1) == '1 \n 2' + assert latex(g1) == r'1_{2}' + sT(g1, "IdentityGate(Integer(2))") + assert str(g1*q) == '1(2)*|10101>' + ascii_str = \ +"""\ +1 *|10101>\n\ + 2 \ +""" + ucode_str = \ +"""\ +1 ⋅❘10101⟩\n\ + 2 \ +""" + assert pretty(g1*q) == ascii_str + assert upretty(g1*q) == ucode_str + assert latex(g1*q) == r'1_{2} {\left|10101\right\rangle }' + sT(g1*q, "Mul(IdentityGate(Integer(2)), Qubit(Integer(1),Integer(0),Integer(1),Integer(0),Integer(1)))") + assert str(g2) == 'C((3,0),X(1))' + ascii_str = \ +"""\ +C /X \\\n\ + 3,0\\ 1/\ +""" + ucode_str = \ +"""\ +C ⎛X ⎞\n\ + 3,0⎝ 1⎠\ +""" + assert pretty(g2) == ascii_str + assert upretty(g2) == ucode_str + assert latex(g2) == r'C_{3,0}{\left(X_{1}\right)}' + sT(g2, "CGate(Tuple(Integer(3), Integer(0)),XGate(Integer(1)))") + assert str(g3) == 'CNOT(1,0)' + ascii_str = \ +"""\ +CNOT \n\ + 1,0\ +""" + ucode_str = \ +"""\ +CNOT \n\ + 1,0\ +""" + assert pretty(g3) == ascii_str + assert upretty(g3) == ucode_str + assert latex(g3) == r'\text{CNOT}_{1,0}' + sT(g3, "CNotGate(Integer(1),Integer(0))") + ascii_str = \ +"""\ +U \n\ + 0\ +""" + ucode_str = \ +"""\ +U \n\ + 0\ +""" + assert str(g4) == \ +"""\ +U((0,),Matrix([\n\ +[a, b],\n\ +[c, d]]))\ +""" + assert pretty(g4) == ascii_str + assert upretty(g4) == ucode_str + assert latex(g4) == r'U_{0}' + sT(g4, "UGate(Tuple(Integer(0)),ImmutableDenseMatrix([[Symbol('a'), Symbol('b')], [Symbol('c'), Symbol('d')]]))") + + +def test_hilbert(): + h1 = HilbertSpace() + h2 = ComplexSpace(2) + h3 = FockSpace() + h4 = L2(Interval(0, oo)) + assert str(h1) == 'H' + assert pretty(h1) == 'H' + assert upretty(h1) == 'H' + assert latex(h1) == r'\mathcal{H}' + sT(h1, "HilbertSpace()") + assert str(h2) == 'C(2)' + ascii_str = \ +"""\ + 2\n\ +C \ +""" + ucode_str = \ +"""\ + 2\n\ +C \ +""" + assert pretty(h2) == ascii_str + assert upretty(h2) == ucode_str + assert latex(h2) == r'\mathcal{C}^{2}' + sT(h2, "ComplexSpace(Integer(2))") + assert str(h3) == 'F' + assert pretty(h3) == 'F' + assert upretty(h3) == 'F' + assert latex(h3) == r'\mathcal{F}' + sT(h3, "FockSpace()") + assert str(h4) == 'L2(Interval(0, oo))' + ascii_str = \ +"""\ + 2\n\ +L \ +""" + ucode_str = \ +"""\ + 2\n\ +L \ +""" + assert pretty(h4) == ascii_str + assert upretty(h4) == ucode_str + assert latex(h4) == r'{\mathcal{L}^2}\left( \left[0, \infty\right) \right)' + sT(h4, "L2(Interval(Integer(0), oo, false, true))") + assert str(h1 + h2) == 'H+C(2)' + ascii_str = \ +"""\ + 2\n\ +H + C \ +""" + ucode_str = \ +"""\ + 2\n\ +H ⊕ C \ +""" + assert pretty(h1 + h2) == ascii_str + assert upretty(h1 + h2) == ucode_str + assert latex(h1 + h2) + sT(h1 + h2, "DirectSumHilbertSpace(HilbertSpace(),ComplexSpace(Integer(2)))") + assert str(h1*h2) == "H*C(2)" + ascii_str = \ +"""\ + 2\n\ +H x C \ +""" + ucode_str = \ +"""\ + 2\n\ +H ⨂ C \ +""" + assert pretty(h1*h2) == ascii_str + assert upretty(h1*h2) == ucode_str + assert latex(h1*h2) + sT(h1*h2, + "TensorProductHilbertSpace(HilbertSpace(),ComplexSpace(Integer(2)))") + assert str(h1**2) == 'H**2' + ascii_str = \ +"""\ + x2\n\ +H \ +""" + ucode_str = \ +"""\ + ⨂2\n\ +H \ +""" + assert pretty(h1**2) == ascii_str + assert upretty(h1**2) == ucode_str + assert latex(h1**2) == r'{\mathcal{H}}^{\otimes 2}' + sT(h1**2, "TensorPowerHilbertSpace(HilbertSpace(),Integer(2))") + + +def test_innerproduct(): + x = symbols('x') + ip1 = InnerProduct(Bra(), Ket()) + ip2 = InnerProduct(TimeDepBra(), TimeDepKet()) + ip3 = InnerProduct(JzBra(1, 1), JzKet(1, 1)) + ip4 = InnerProduct(JzBraCoupled(1, 1, (1, 1)), JzKetCoupled(1, 1, (1, 1))) + ip_tall1 = InnerProduct(Bra(x/2), Ket(x/2)) + ip_tall2 = InnerProduct(Bra(x), Ket(x/2)) + ip_tall3 = InnerProduct(Bra(x/2), Ket(x)) + assert str(ip1) == '' + assert pretty(ip1) == '' + assert upretty(ip1) == '⟨ψ❘ψ⟩' + assert latex( + ip1) == r'\left\langle \psi \right. {\left|\psi\right\rangle }' + sT(ip1, "InnerProduct(Bra(Symbol('psi')),Ket(Symbol('psi')))") + assert str(ip2) == '' + assert pretty(ip2) == '' + assert upretty(ip2) == '⟨ψ;t❘ψ;t⟩' + assert latex(ip2) == \ + r'\left\langle \psi;t \right. {\left|\psi;t\right\rangle }' + sT(ip2, "InnerProduct(TimeDepBra(Symbol('psi'),Symbol('t')),TimeDepKet(Symbol('psi'),Symbol('t')))") + assert str(ip3) == "<1,1|1,1>" + assert pretty(ip3) == '<1,1|1,1>' + assert upretty(ip3) == '⟨1,1❘1,1⟩' + assert latex(ip3) == r'\left\langle 1,1 \right. {\left|1,1\right\rangle }' + sT(ip3, "InnerProduct(JzBra(Integer(1),Integer(1)),JzKet(Integer(1),Integer(1)))") + assert str(ip4) == "<1,1,j1=1,j2=1|1,1,j1=1,j2=1>" + assert pretty(ip4) == '<1,1,j1=1,j2=1|1,1,j1=1,j2=1>' + assert upretty(ip4) == '⟨1,1,j₁=1,j₂=1❘1,1,j₁=1,j₂=1⟩' + assert latex(ip4) == \ + r'\left\langle 1,1,j_{1}=1,j_{2}=1 \right. {\left|1,1,j_{1}=1,j_{2}=1\right\rangle }' + sT(ip4, "InnerProduct(JzBraCoupled(Integer(1),Integer(1),Tuple(Integer(1), Integer(1)),Tuple(Tuple(Integer(1), Integer(2), Integer(1)))),JzKetCoupled(Integer(1),Integer(1),Tuple(Integer(1), Integer(1)),Tuple(Tuple(Integer(1), Integer(2), Integer(1)))))") + assert str(ip_tall1) == '' + ascii_str = \ +"""\ + / | \\ \n\ +/ x|x \\\n\ +\\ -|- /\n\ + \\2|2/ \ +""" + ucode_str = \ +"""\ + ╱ │ ╲ \n\ +╱ x│x ╲\n\ +╲ ─│─ ╱\n\ + ╲2│2╱ \ +""" + assert pretty(ip_tall1) == ascii_str + assert upretty(ip_tall1) == ucode_str + assert latex(ip_tall1) == \ + r'\left\langle \frac{x}{2} \right. {\left|\frac{x}{2}\right\rangle }' + sT(ip_tall1, "InnerProduct(Bra(Mul(Rational(1, 2), Symbol('x'))),Ket(Mul(Rational(1, 2), Symbol('x'))))") + assert str(ip_tall2) == '' + ascii_str = \ +"""\ + / | \\ \n\ +/ |x \\\n\ +\\ x|- /\n\ + \\ |2/ \ +""" + ucode_str = \ +"""\ + ╱ │ ╲ \n\ +╱ │x ╲\n\ +╲ x│─ ╱\n\ + ╲ │2╱ \ +""" + assert pretty(ip_tall2) == ascii_str + assert upretty(ip_tall2) == ucode_str + assert latex(ip_tall2) == \ + r'\left\langle x \right. {\left|\frac{x}{2}\right\rangle }' + sT(ip_tall2, + "InnerProduct(Bra(Symbol('x')),Ket(Mul(Rational(1, 2), Symbol('x'))))") + assert str(ip_tall3) == '' + ascii_str = \ +"""\ + / | \\ \n\ +/ x| \\\n\ +\\ -|x /\n\ + \\2| / \ +""" + ucode_str = \ +"""\ + ╱ │ ╲ \n\ +╱ x│ ╲\n\ +╲ ─│x ╱\n\ + ╲2│ ╱ \ +""" + assert pretty(ip_tall3) == ascii_str + assert upretty(ip_tall3) == ucode_str + assert latex(ip_tall3) == \ + r'\left\langle \frac{x}{2} \right. {\left|x\right\rangle }' + sT(ip_tall3, + "InnerProduct(Bra(Mul(Rational(1, 2), Symbol('x'))),Ket(Symbol('x')))") + + +def test_operator(): + a = Operator('A') + b = Operator('B', Symbol('t'), S.Half) + inv = a.inv() + f = Function('f') + x = symbols('x') + d = DifferentialOperator(Derivative(f(x), x), f(x)) + op = OuterProduct(Ket(), Bra()) + assert str(a) == 'A' + assert pretty(a) == 'A' + assert upretty(a) == 'A' + assert latex(a) == 'A' + sT(a, "Operator(Symbol('A'))") + assert str(inv) == 'A**(-1)' + ascii_str = \ +"""\ + -1\n\ +A \ +""" + ucode_str = \ +"""\ + -1\n\ +A \ +""" + assert pretty(inv) == ascii_str + assert upretty(inv) == ucode_str + assert latex(inv) == r'A^{-1}' + sT(inv, "Pow(Operator(Symbol('A')), Integer(-1))") + assert str(d) == 'DifferentialOperator(Derivative(f(x), x),f(x))' + ascii_str = \ +"""\ + /d \\\n\ +DifferentialOperator|--(f(x)),f(x)|\n\ + \\dx /\ +""" + ucode_str = \ +"""\ + ⎛d ⎞\n\ +DifferentialOperator⎜──(f(x)),f(x)⎟\n\ + ⎝dx ⎠\ +""" + assert pretty(d) == ascii_str + assert upretty(d) == ucode_str + assert latex(d) == \ + r'DifferentialOperator\left(\frac{d}{d x} f{\left(x \right)},f{\left(x \right)}\right)' + sT(d, "DifferentialOperator(Derivative(Function('f')(Symbol('x')), Tuple(Symbol('x'), Integer(1))),Function('f')(Symbol('x')))") + assert str(b) == 'Operator(B,t,1/2)' + assert pretty(b) == 'Operator(B,t,1/2)' + assert upretty(b) == 'Operator(B,t,1/2)' + assert latex(b) == r'Operator\left(B,t,\frac{1}{2}\right)' + sT(b, "Operator(Symbol('B'),Symbol('t'),Rational(1, 2))") + assert str(op) == '|psi>' + assert pretty(q1) == '|0101>' + assert upretty(q1) == '❘0101⟩' + assert latex(q1) == r'{\left|0101\right\rangle }' + sT(q1, "Qubit(Integer(0),Integer(1),Integer(0),Integer(1))") + assert str(q2) == '|8>' + assert pretty(q2) == '|8>' + assert upretty(q2) == '❘8⟩' + assert latex(q2) == r'{\left|8\right\rangle }' + sT(q2, "IntQubit(8)") + + +def test_spin(): + lz = JzOp('L') + ket = JzKet(1, 0) + bra = JzBra(1, 0) + cket = JzKetCoupled(1, 0, (1, 2)) + cbra = JzBraCoupled(1, 0, (1, 2)) + cket_big = JzKetCoupled(1, 0, (1, 2, 3)) + cbra_big = JzBraCoupled(1, 0, (1, 2, 3)) + rot = Rotation(1, 2, 3) + bigd = WignerD(1, 2, 3, 4, 5, 6) + smalld = WignerD(1, 2, 3, 0, 4, 0) + assert str(lz) == 'Lz' + ascii_str = \ +"""\ +L \n\ + z\ +""" + ucode_str = \ +"""\ +L \n\ + z\ +""" + assert pretty(lz) == ascii_str + assert upretty(lz) == ucode_str + assert latex(lz) == 'L_z' + sT(lz, "JzOp(Symbol('L'))") + assert str(J2) == 'J2' + ascii_str = \ +"""\ + 2\n\ +J \ +""" + ucode_str = \ +"""\ + 2\n\ +J \ +""" + assert pretty(J2) == ascii_str + assert upretty(J2) == ucode_str + assert latex(J2) == r'J^2' + sT(J2, "J2Op(Symbol('J'))") + assert str(Jz) == 'Jz' + ascii_str = \ +"""\ +J \n\ + z\ +""" + ucode_str = \ +"""\ +J \n\ + z\ +""" + assert pretty(Jz) == ascii_str + assert upretty(Jz) == ucode_str + assert latex(Jz) == 'J_z' + sT(Jz, "JzOp(Symbol('J'))") + assert str(ket) == '|1,0>' + assert pretty(ket) == '|1,0>' + assert upretty(ket) == '❘1,0⟩' + assert latex(ket) == r'{\left|1,0\right\rangle }' + sT(ket, "JzKet(Integer(1),Integer(0))") + assert str(bra) == '<1,0|' + assert pretty(bra) == '<1,0|' + assert upretty(bra) == '⟨1,0❘' + assert latex(bra) == r'{\left\langle 1,0\right|}' + sT(bra, "JzBra(Integer(1),Integer(0))") + assert str(cket) == '|1,0,j1=1,j2=2>' + assert pretty(cket) == '|1,0,j1=1,j2=2>' + assert upretty(cket) == '❘1,0,j₁=1,j₂=2⟩' + assert latex(cket) == r'{\left|1,0,j_{1}=1,j_{2}=2\right\rangle }' + sT(cket, "JzKetCoupled(Integer(1),Integer(0),Tuple(Integer(1), Integer(2)),Tuple(Tuple(Integer(1), Integer(2), Integer(1))))") + assert str(cbra) == '<1,0,j1=1,j2=2|' + assert pretty(cbra) == '<1,0,j1=1,j2=2|' + assert upretty(cbra) == '⟨1,0,j₁=1,j₂=2❘' + assert latex(cbra) == r'{\left\langle 1,0,j_{1}=1,j_{2}=2\right|}' + sT(cbra, "JzBraCoupled(Integer(1),Integer(0),Tuple(Integer(1), Integer(2)),Tuple(Tuple(Integer(1), Integer(2), Integer(1))))") + assert str(cket_big) == '|1,0,j1=1,j2=2,j3=3,j(1,2)=3>' + # TODO: Fix non-unicode pretty printing + # i.e. j1,2 -> j(1,2) + assert pretty(cket_big) == '|1,0,j1=1,j2=2,j3=3,j1,2=3>' + assert upretty(cket_big) == '❘1,0,j₁=1,j₂=2,j₃=3,j₁,₂=3⟩' + assert latex(cket_big) == \ + r'{\left|1,0,j_{1}=1,j_{2}=2,j_{3}=3,j_{1,2}=3\right\rangle }' + sT(cket_big, "JzKetCoupled(Integer(1),Integer(0),Tuple(Integer(1), Integer(2), Integer(3)),Tuple(Tuple(Integer(1), Integer(2), Integer(3)), Tuple(Integer(1), Integer(3), Integer(1))))") + assert str(cbra_big) == '<1,0,j1=1,j2=2,j3=3,j(1,2)=3|' + assert pretty(cbra_big) == '<1,0,j1=1,j2=2,j3=3,j1,2=3|' + assert upretty(cbra_big) == '⟨1,0,j₁=1,j₂=2,j₃=3,j₁,₂=3❘' + assert latex(cbra_big) == \ + r'{\left\langle 1,0,j_{1}=1,j_{2}=2,j_{3}=3,j_{1,2}=3\right|}' + sT(cbra_big, "JzBraCoupled(Integer(1),Integer(0),Tuple(Integer(1), Integer(2), Integer(3)),Tuple(Tuple(Integer(1), Integer(2), Integer(3)), Tuple(Integer(1), Integer(3), Integer(1))))") + assert str(rot) == 'R(1,2,3)' + assert pretty(rot) == 'R (1,2,3)' + assert upretty(rot) == 'ℛ (1,2,3)' + assert latex(rot) == r'\mathcal{R}\left(1,2,3\right)' + sT(rot, "Rotation(Integer(1),Integer(2),Integer(3))") + assert str(bigd) == 'WignerD(1, 2, 3, 4, 5, 6)' + ascii_str = \ +"""\ + 1 \n\ +D (4,5,6)\n\ + 2,3 \ +""" + ucode_str = \ +"""\ + 1 \n\ +D (4,5,6)\n\ + 2,3 \ +""" + assert pretty(bigd) == ascii_str + assert upretty(bigd) == ucode_str + assert latex(bigd) == r'D^{1}_{2,3}\left(4,5,6\right)' + sT(bigd, "WignerD(Integer(1), Integer(2), Integer(3), Integer(4), Integer(5), Integer(6))") + assert str(smalld) == 'WignerD(1, 2, 3, 0, 4, 0)' + ascii_str = \ +"""\ + 1 \n\ +d (4)\n\ + 2,3 \ +""" + ucode_str = \ +"""\ + 1 \n\ +d (4)\n\ + 2,3 \ +""" + assert pretty(smalld) == ascii_str + assert upretty(smalld) == ucode_str + assert latex(smalld) == r'd^{1}_{2,3}\left(4\right)' + sT(smalld, "WignerD(Integer(1), Integer(2), Integer(3), Integer(0), Integer(4), Integer(0))") + + +def test_state(): + x = symbols('x') + bra = Bra() + ket = Ket() + bra_tall = Bra(x/2) + ket_tall = Ket(x/2) + tbra = TimeDepBra() + tket = TimeDepKet() + assert str(bra) == '' + assert pretty(ket) == '|psi>' + assert upretty(ket) == '❘ψ⟩' + assert latex(ket) == r'{\left|\psi\right\rangle }' + sT(ket, "Ket(Symbol('psi'))") + assert str(bra_tall) == '' + ascii_str = \ +"""\ +| \\ \n\ +|x \\\n\ +|- /\n\ +|2/ \ +""" + ucode_str = \ +"""\ +│ ╲ \n\ +│x ╲\n\ +│─ ╱\n\ +│2╱ \ +""" + assert pretty(ket_tall) == ascii_str + assert upretty(ket_tall) == ucode_str + assert latex(ket_tall) == r'{\left|\frac{x}{2}\right\rangle }' + sT(ket_tall, "Ket(Mul(Rational(1, 2), Symbol('x')))") + assert str(tbra) == '' + assert pretty(tket) == '|psi;t>' + assert upretty(tket) == '❘ψ;t⟩' + assert latex(tket) == r'{\left|\psi;t\right\rangle }' + sT(tket, "TimeDepKet(Symbol('psi'),Symbol('t'))") + + +def test_tensorproduct(): + tp = TensorProduct(JzKet(1, 1), JzKet(1, 0)) + assert str(tp) == '|1,1>x|1,0>' + assert pretty(tp) == '|1,1>x |1,0>' + assert upretty(tp) == '❘1,1⟩⨂ ❘1,0⟩' + assert latex(tp) == \ + r'{{\left|1,1\right\rangle }}\otimes {{\left|1,0\right\rangle }}' + sT(tp, "TensorProduct(JzKet(Integer(1),Integer(1)), JzKet(Integer(1),Integer(0)))") + + +def test_big_expr(): + f = Function('f') + x = symbols('x') + e1 = Dagger(AntiCommutator(Operator('A') + Operator('B'), Pow(DifferentialOperator(Derivative(f(x), x), f(x)), 3))*TensorProduct(Jz**2, Operator('A') + Operator('B')))*(JzBra(1, 0) + JzBra(1, 1))*(JzKet(0, 0) + JzKet(1, -1)) + e2 = Commutator(Jz**2, Operator('A') + Operator('B'))*AntiCommutator(Dagger(Operator('C')*Operator('D')), Operator('E').inv()**2)*Dagger(Commutator(Jz, J2)) + e3 = Wigner3j(1, 2, 3, 4, 5, 6)*TensorProduct(Commutator(Operator('A') + Dagger(Operator('B')), Operator('C') + Operator('D')), Jz - J2)*Dagger(OuterProduct(Dagger(JzBra(1, 1)), JzBra(1, 0)))*TensorProduct(JzKetCoupled(1, 1, (1, 1)) + JzKetCoupled(1, 0, (1, 1)), JzKetCoupled(1, -1, (1, 1))) + e4 = (ComplexSpace(1)*ComplexSpace(2) + FockSpace()**2)*(L2(Interval( + 0, oo)) + HilbertSpace()) + assert str(e1) == '(Jz**2)x(Dagger(A) + Dagger(B))*{Dagger(DifferentialOperator(Derivative(f(x), x),f(x)))**3,Dagger(A) + Dagger(B)}*(<1,0| + <1,1|)*(|0,0> + |1,-1>)' + ascii_str = \ +"""\ + / 3 \\ \n\ + |/ +\\ | \n\ + 2 / + +\\ <| /d \\ | + +> \n\ +/J \\ x \\A + B /*||DifferentialOperator|--(f(x)),f(x)| | ,A + B |*(<1,0| + <1,1|)*(|0,0> + |1,-1>)\n\ +\\ z/ \\\\ \\dx / / / \ +""" + ucode_str = \ +"""\ + ⎧ 3 ⎫ \n\ + ⎪⎛ †⎞ ⎪ \n\ + 2 ⎛ † †⎞ ⎨⎜ ⎛d ⎞ ⎟ † †⎬ \n\ +⎛J ⎞ ⨂ ⎝A + B ⎠⋅⎪⎜DifferentialOperator⎜──(f(x)),f(x)⎟ ⎟ ,A + B ⎪⋅(⟨1,0❘ + ⟨1,1❘)⋅(❘0,0⟩ + ❘1,-1⟩)\n\ +⎝ z⎠ ⎩⎝ ⎝dx ⎠ ⎠ ⎭ \ +""" + assert pretty(e1) == ascii_str + assert upretty(e1) == ucode_str + assert latex(e1) == \ + r'{J_z^{2}}\otimes \left({A^{\dagger} + B^{\dagger}}\right) \left\{\left(DifferentialOperator\left(\frac{d}{d x} f{\left(x \right)},f{\left(x \right)}\right)^{\dagger}\right)^{3},A^{\dagger} + B^{\dagger}\right\} \left({\left\langle 1,0\right|} + {\left\langle 1,1\right|}\right) \left({\left|0,0\right\rangle } + {\left|1,-1\right\rangle }\right)' + sT(e1, "Mul(TensorProduct(Pow(JzOp(Symbol('J')), Integer(2)), Add(Dagger(Operator(Symbol('A'))), Dagger(Operator(Symbol('B'))))), AntiCommutator(Pow(Dagger(DifferentialOperator(Derivative(Function('f')(Symbol('x')), Tuple(Symbol('x'), Integer(1))),Function('f')(Symbol('x')))), Integer(3)),Add(Dagger(Operator(Symbol('A'))), Dagger(Operator(Symbol('B'))))), Add(JzBra(Integer(1),Integer(0)), JzBra(Integer(1),Integer(1))), Add(JzKet(Integer(0),Integer(0)), JzKet(Integer(1),Integer(-1))))") + assert str(e2) == '[Jz**2,A + B]*{E**(-2),Dagger(D)*Dagger(C)}*[J2,Jz]' + ascii_str = \ +"""\ +[ 2 ] / -2 + +\\ [ 2 ]\n\ +[/J \\ ,A + B]**[J ,J ]\n\ +[\\ z/ ] \\ / [ z]\ +""" + ucode_str = \ +"""\ +⎡ 2 ⎤ ⎧ -2 † †⎫ ⎡ 2 ⎤\n\ +⎢⎛J ⎞ ,A + B⎥⋅⎨E ,D ⋅C ⎬⋅⎢J ,J ⎥\n\ +⎣⎝ z⎠ ⎦ ⎩ ⎭ ⎣ z⎦\ +""" + assert pretty(e2) == ascii_str + assert upretty(e2) == ucode_str + assert latex(e2) == \ + r'\left[J_z^{2},A + B\right] \left\{E^{-2},D^{\dagger} C^{\dagger}\right\} \left[J^2,J_z\right]' + sT(e2, "Mul(Commutator(Pow(JzOp(Symbol('J')), Integer(2)),Add(Operator(Symbol('A')), Operator(Symbol('B')))), AntiCommutator(Pow(Operator(Symbol('E')), Integer(-2)),Mul(Dagger(Operator(Symbol('D'))), Dagger(Operator(Symbol('C'))))), Commutator(J2Op(Symbol('J')),JzOp(Symbol('J'))))") + assert str(e3) == \ + "Wigner3j(1, 2, 3, 4, 5, 6)*[Dagger(B) + A,C + D]x(-J2 + Jz)*|1,0><1,1|*(|1,0,j1=1,j2=1> + |1,1,j1=1,j2=1>)x|1,-1,j1=1,j2=1>" + ascii_str = \ +"""\ + [ + ] / 2 \\ \n\ +/1 3 5\\*[B + A,C + D]x |- J + J |*|1,0><1,1|*(|1,0,j1=1,j2=1> + |1,1,j1=1,j2=1>)x |1,-1,j1=1,j2=1>\n\ +| | \\ z/ \n\ +\\2 4 6/ \ +""" + ucode_str = \ +"""\ + ⎡ † ⎤ ⎛ 2 ⎞ \n\ +⎛1 3 5⎞⋅⎣B + A,C + D⎦⨂ ⎜- J + J ⎟⋅❘1,0⟩⟨1,1❘⋅(❘1,0,j₁=1,j₂=1⟩ + ❘1,1,j₁=1,j₂=1⟩)⨂ ❘1,-1,j₁=1,j₂=1⟩\n\ +⎜ ⎟ ⎝ z⎠ \n\ +⎝2 4 6⎠ \ +""" + assert pretty(e3) == ascii_str + assert upretty(e3) == ucode_str + assert latex(e3) == \ + r'\left(\begin{array}{ccc} 1 & 3 & 5 \\ 2 & 4 & 6 \end{array}\right) {\left[B^{\dagger} + A,C + D\right]}\otimes \left({- J^2 + J_z}\right) {\left|1,0\right\rangle }{\left\langle 1,1\right|} \left({{\left|1,0,j_{1}=1,j_{2}=1\right\rangle } + {\left|1,1,j_{1}=1,j_{2}=1\right\rangle }}\right)\otimes {{\left|1,-1,j_{1}=1,j_{2}=1\right\rangle }}' + sT(e3, "Mul(Wigner3j(Integer(1), Integer(2), Integer(3), Integer(4), Integer(5), Integer(6)), TensorProduct(Commutator(Add(Dagger(Operator(Symbol('B'))), Operator(Symbol('A'))),Add(Operator(Symbol('C')), Operator(Symbol('D')))), Add(Mul(Integer(-1), J2Op(Symbol('J'))), JzOp(Symbol('J')))), OuterProduct(JzKet(Integer(1),Integer(0)),JzBra(Integer(1),Integer(1))), TensorProduct(Add(JzKetCoupled(Integer(1),Integer(0),Tuple(Integer(1), Integer(1)),Tuple(Tuple(Integer(1), Integer(2), Integer(1)))), JzKetCoupled(Integer(1),Integer(1),Tuple(Integer(1), Integer(1)),Tuple(Tuple(Integer(1), Integer(2), Integer(1))))), JzKetCoupled(Integer(1),Integer(-1),Tuple(Integer(1), Integer(1)),Tuple(Tuple(Integer(1), Integer(2), Integer(1))))))") + assert str(e4) == '(C(1)*C(2)+F**2)*(L2(Interval(0, oo))+H)' + ascii_str = \ +"""\ +// 1 2\\ x2\\ / 2 \\\n\ +\\\\C x C / + F / x \\L + H/\ +""" + ucode_str = \ +"""\ +⎛⎛ 1 2⎞ ⨂2⎞ ⎛ 2 ⎞\n\ +⎝⎝C ⨂ C ⎠ ⊕ F ⎠ ⨂ ⎝L ⊕ H⎠\ +""" + assert pretty(e4) == ascii_str + assert upretty(e4) == ucode_str + assert latex(e4) == \ + r'\left(\left(\mathcal{C}^{1}\otimes \mathcal{C}^{2}\right)\oplus {\mathcal{F}}^{\otimes 2}\right)\otimes \left({\mathcal{L}^2}\left( \left[0, \infty\right) \right)\oplus \mathcal{H}\right)' + sT(e4, "TensorProductHilbertSpace((DirectSumHilbertSpace(TensorProductHilbertSpace(ComplexSpace(Integer(1)),ComplexSpace(Integer(2))),TensorPowerHilbertSpace(FockSpace(),Integer(2)))),(DirectSumHilbertSpace(L2(Interval(Integer(0), oo, false, true)),HilbertSpace())))") + + +def _test_sho1d(): + ad = RaisingOp('a') + assert pretty(ad) == ' \N{DAGGER}\na ' + assert latex(ad) == 'a^{\\dagger}' diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qapply.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qapply.py new file mode 100644 index 0000000000000000000000000000000000000000..be6f68d9869df84bc25bd0ebdfcde9ff49adc508 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qapply.py @@ -0,0 +1,152 @@ +from sympy.core.mul import Mul +from sympy.core.numbers import (I, Integer, Rational) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.miscellaneous import sqrt + +from sympy.physics.quantum.anticommutator import AntiCommutator +from sympy.physics.quantum.commutator import Commutator +from sympy.physics.quantum.constants import hbar +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.gate import H, XGate, IdentityGate +from sympy.physics.quantum.operator import Operator, IdentityOperator +from sympy.physics.quantum.qapply import qapply +from sympy.physics.quantum.spin import Jx, Jy, Jz, Jplus, Jminus, J2, JzKet +from sympy.physics.quantum.tensorproduct import TensorProduct +from sympy.physics.quantum.state import Ket +from sympy.physics.quantum.density import Density +from sympy.physics.quantum.qubit import Qubit, QubitBra +from sympy.physics.quantum.boson import BosonOp, BosonFockKet, BosonFockBra +from sympy.testing.pytest import warns_deprecated_sympy + + +j, jp, m, mp = symbols("j j' m m'") + +z = JzKet(1, 0) +po = JzKet(1, 1) +mo = JzKet(1, -1) + +A = Operator('A') + + +class Foo(Operator): + def _apply_operator_JzKet(self, ket, **options): + return ket + + +def test_basic(): + assert qapply(Jz*po) == hbar*po + assert qapply(Jx*z) == hbar*po/sqrt(2) + hbar*mo/sqrt(2) + assert qapply((Jplus + Jminus)*z/sqrt(2)) == hbar*po + hbar*mo + assert qapply(Jz*(po + mo)) == hbar*po - hbar*mo + assert qapply(Jz*po + Jz*mo) == hbar*po - hbar*mo + assert qapply(Jminus*Jminus*po) == 2*hbar**2*mo + assert qapply(Jplus**2*mo) == 2*hbar**2*po + assert qapply(Jplus**2*Jminus**2*po) == 4*hbar**4*po + + +def test_extra(): + extra = z.dual*A*z + assert qapply(Jz*po*extra) == hbar*po*extra + assert qapply(Jx*z*extra) == (hbar*po/sqrt(2) + hbar*mo/sqrt(2))*extra + assert qapply( + (Jplus + Jminus)*z/sqrt(2)*extra) == hbar*po*extra + hbar*mo*extra + assert qapply(Jz*(po + mo)*extra) == hbar*po*extra - hbar*mo*extra + assert qapply(Jz*po*extra + Jz*mo*extra) == hbar*po*extra - hbar*mo*extra + assert qapply(Jminus*Jminus*po*extra) == 2*hbar**2*mo*extra + assert qapply(Jplus**2*mo*extra) == 2*hbar**2*po*extra + assert qapply(Jplus**2*Jminus**2*po*extra) == 4*hbar**4*po*extra + + +def test_innerproduct(): + assert qapply(po.dual*Jz*po, ip_doit=False) == hbar*(po.dual*po) + assert qapply(po.dual*Jz*po) == hbar + + +def test_zero(): + assert qapply(0) == 0 + assert qapply(Integer(0)) == 0 + + +def test_commutator(): + assert qapply(Commutator(Jx, Jy)*Jz*po) == I*hbar**3*po + assert qapply(Commutator(J2, Jz)*Jz*po) == 0 + assert qapply(Commutator(Jz, Foo('F'))*po) == 0 + assert qapply(Commutator(Foo('F'), Jz)*po) == 0 + + +def test_anticommutator(): + assert qapply(AntiCommutator(Jz, Foo('F'))*po) == 2*hbar*po + assert qapply(AntiCommutator(Foo('F'), Jz)*po) == 2*hbar*po + + +def test_outerproduct(): + e = Jz*(mo*po.dual)*Jz*po + assert qapply(e) == -hbar**2*mo + assert qapply(e, ip_doit=False) == -hbar**2*(po.dual*po)*mo + assert qapply(e).doit() == -hbar**2*mo + + +def test_tensorproduct(): + a = BosonOp("a") + b = BosonOp("b") + ket1 = TensorProduct(BosonFockKet(1), BosonFockKet(2)) + ket2 = TensorProduct(BosonFockKet(0), BosonFockKet(0)) + ket3 = TensorProduct(BosonFockKet(0), BosonFockKet(2)) + bra1 = TensorProduct(BosonFockBra(0), BosonFockBra(0)) + bra2 = TensorProduct(BosonFockBra(1), BosonFockBra(2)) + assert qapply(TensorProduct(a, b ** 2) * ket1) == sqrt(2) * ket2 + assert qapply(TensorProduct(a, Dagger(b) * b) * ket1) == 2 * ket3 + assert qapply(bra1 * TensorProduct(a, b * b), + dagger=True) == sqrt(2) * bra2 + assert qapply(bra2 * ket1).doit() == S.One + assert qapply(TensorProduct(a, b * b) * ket1) == sqrt(2) * ket2 + assert qapply(Dagger(TensorProduct(a, b * b) * ket1), + dagger=True) == sqrt(2) * Dagger(ket2) + + +def test_dagger(): + lhs = Dagger(Qubit(0))*Dagger(H(0)) + rhs = Dagger(Qubit(1))/sqrt(2) + Dagger(Qubit(0))/sqrt(2) + assert qapply(lhs, dagger=True) == rhs + + +def test_issue_6073(): + x, y = symbols('x y', commutative=False) + A = Ket(x, y) + B = Operator('B') + assert qapply(A) == A + assert qapply(A.dual*B) == A.dual*B + + +def test_density(): + d = Density([Jz*mo, 0.5], [Jz*po, 0.5]) + assert qapply(d) == Density([-hbar*mo, 0.5], [hbar*po, 0.5]) + + +def test_issue3044(): + expr1 = TensorProduct(Jz*JzKet(S(2),S.NegativeOne)/sqrt(2), Jz*JzKet(S.Half,S.Half)) + result = Mul(S.NegativeOne, Rational(1, 4), 2**S.Half, hbar**2) + result *= TensorProduct(JzKet(2,-1), JzKet(S.Half,S.Half)) + assert qapply(expr1) == result + + +# Issue 24158: Tests whether qapply incorrectly evaluates some ket*op as op*ket +def test_issue24158_ket_times_op(): + P = BosonFockKet(0) * BosonOp("a") # undefined term + # Does lhs._apply_operator_BosonOp(rhs) still evaluate ket*op as op*ket? + assert qapply(P) == P # qapply(P) -> BosonOp("a")*BosonFockKet(0) = 0 before fix + P = Qubit(1) * XGate(0) # undefined term + # Does rhs._apply_operator_Qubit(lhs) still evaluate ket*op as op*ket? + assert qapply(P) == P # qapply(P) -> Qubit(0) before fix + P1 = Mul(QubitBra(0), Mul(QubitBra(0), Qubit(0)), XGate(0)) # legal expr <0| * (<1|*|1>) * X + assert qapply(P1) == QubitBra(0) * XGate(0) # qapply(P1) -> 0 before fix + P1 = qapply(P1, dagger = True) # unsatisfactorily -> <0|*X(0), expect <1| since dagger=True + assert qapply(P1, dagger = True) == QubitBra(1) # qapply(P1, dagger=True) -> 0 before fix + P2 = QubitBra(0) * (QubitBra(0) * Qubit(0)) * XGate(0) # 'forgot' to set brackets + P2 = qapply(P2, dagger = True) # unsatisfactorily -> <0|*X(0), expect <1| since dagger=True + assert P2 == QubitBra(1) # qapply(P1) -> 0 before fix + # Pull Request 24237: IdentityOperator from the right without dagger=True option + with warns_deprecated_sympy(): + assert qapply(QubitBra(1)*IdentityOperator()) == QubitBra(1) + assert qapply(IdentityGate(0)*(Qubit(0) + Qubit(1))) == Qubit(0) + Qubit(1) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qasm.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qasm.py new file mode 100644 index 0000000000000000000000000000000000000000..81c7ee8523e732d336211f7739a6e8f7fbab5220 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qasm.py @@ -0,0 +1,89 @@ +from sympy.physics.quantum.qasm import Qasm, flip_index, trim,\ + get_index, nonblank, fullsplit, fixcommand, stripquotes, read_qasm +from sympy.physics.quantum.gate import X, Z, H, S, T +from sympy.physics.quantum.gate import CNOT, SWAP, CPHASE, CGate, CGateS +from sympy.physics.quantum.circuitplot import Mz + +def test_qasm_readqasm(): + qasm_lines = """\ + qubit q_0 + qubit q_1 + h q_0 + cnot q_0,q_1 + """ + q = read_qasm(qasm_lines) + assert q.get_circuit() == CNOT(1,0)*H(1) + +def test_qasm_ex1(): + q = Qasm('qubit q0', 'qubit q1', 'h q0', 'cnot q0,q1') + assert q.get_circuit() == CNOT(1,0)*H(1) + +def test_qasm_ex1_methodcalls(): + q = Qasm() + q.qubit('q_0') + q.qubit('q_1') + q.h('q_0') + q.cnot('q_0', 'q_1') + assert q.get_circuit() == CNOT(1,0)*H(1) + +def test_qasm_swap(): + q = Qasm('qubit q0', 'qubit q1', 'cnot q0,q1', 'cnot q1,q0', 'cnot q0,q1') + assert q.get_circuit() == CNOT(1,0)*CNOT(0,1)*CNOT(1,0) + + +def test_qasm_ex2(): + q = Qasm('qubit q_0', 'qubit q_1', 'qubit q_2', 'h q_1', + 'cnot q_1,q_2', 'cnot q_0,q_1', 'h q_0', + 'measure q_1', 'measure q_0', + 'c-x q_1,q_2', 'c-z q_0,q_2') + assert q.get_circuit() == CGate(2,Z(0))*CGate(1,X(0))*Mz(2)*Mz(1)*H(2)*CNOT(2,1)*CNOT(1,0)*H(1) + +def test_qasm_1q(): + for symbol, gate in [('x', X), ('z', Z), ('h', H), ('s', S), ('t', T), ('measure', Mz)]: + q = Qasm('qubit q_0', '%s q_0' % symbol) + assert q.get_circuit() == gate(0) + +def test_qasm_2q(): + for symbol, gate in [('cnot', CNOT), ('swap', SWAP), ('cphase', CPHASE)]: + q = Qasm('qubit q_0', 'qubit q_1', '%s q_0,q_1' % symbol) + assert q.get_circuit() == gate(1,0) + +def test_qasm_3q(): + q = Qasm('qubit q0', 'qubit q1', 'qubit q2', 'toffoli q2,q1,q0') + assert q.get_circuit() == CGateS((0,1),X(2)) + +def test_qasm_flip_index(): + assert flip_index(0, 2) == 1 + assert flip_index(1, 2) == 0 + +def test_qasm_trim(): + assert trim('nothing happens here') == 'nothing happens here' + assert trim("Something #happens here") == "Something " + +def test_qasm_get_index(): + assert get_index('q0', ['q0', 'q1']) == 1 + assert get_index('q1', ['q0', 'q1']) == 0 + +def test_qasm_nonblank(): + assert list(nonblank('abcd')) == list('abcd') + assert list(nonblank('abc ')) == list('abc') + +def test_qasm_fullsplit(): + assert fullsplit('g q0,q1,q2, q3') == ('g', ['q0', 'q1', 'q2', 'q3']) + +def test_qasm_fixcommand(): + assert fixcommand('foo') == 'foo' + assert fixcommand('def') == 'qdef' + +def test_qasm_stripquotes(): + assert stripquotes("'S'") == 'S' + assert stripquotes('"S"') == 'S' + assert stripquotes('S') == 'S' + +def test_qasm_qdef(): + # weaker test condition (str) since we don't have access to the actual class + q = Qasm("def Q,0,Q",'qubit q0','Q q0') + assert str(q.get_circuit()) == 'Q(0)' + + q = Qasm("def CQ,1,Q", 'qubit q0', 'qubit q1', 'CQ q0,q1') + assert str(q.get_circuit()) == 'C((1),Q(0))' diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qexpr.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qexpr.py new file mode 100644 index 0000000000000000000000000000000000000000..c01817935a0f977e44c8e0dc29746e070b2cb693 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qexpr.py @@ -0,0 +1,64 @@ +from sympy.core.numbers import Integer +from sympy.core.symbol import Symbol +from sympy.concrete import Sum +from sympy.physics.quantum.qexpr import QExpr, _qsympify_sequence +from sympy.physics.quantum.hilbert import HilbertSpace +from sympy.core.containers import Tuple + +x = Symbol('x') +y = Symbol('y') +n = Symbol('n', integer=True) +m = Symbol('m', integer=True) + + +def test_qexpr_new(): + q = QExpr(0) + assert q.label == (0,) + assert q.hilbert_space == HilbertSpace() + assert q.is_commutative is False + + q = QExpr(0, 1) + assert q.label == (Integer(0), Integer(1)) + + q = QExpr._new_rawargs(HilbertSpace(), Integer(0), Integer(1)) + assert q.label == (Integer(0), Integer(1)) + assert q.hilbert_space == HilbertSpace() + + +def test_qexpr_commutative(): + q1 = QExpr(x) + q2 = QExpr(y) + assert q1.is_commutative is False + assert q2.is_commutative is False + assert q1*q2 != q2*q1 + + q = QExpr._new_rawargs(Integer(0), Integer(1), HilbertSpace()) + assert q.is_commutative is False + + +def test_qexpr_free_symbols(): + q1 = QExpr(x, y) + assert q1.free_symbols == {x, y} + + +def test_qexpr_sum(): + q1 = Sum(QExpr(n), (n,0,2)) + assert q1.doit() == QExpr(0) + QExpr(1) + QExpr(2) + + q2 = Sum(QExpr(n, m), (n, 0, 2), (m, 0, 2)) + assert q2.doit() == QExpr(0, 0) + QExpr(0, 1) + QExpr(0, 2) + \ + QExpr(1, 0) + QExpr(1, 1) + QExpr(1, 2) + \ + QExpr(2, 0) + QExpr(2, 1) + QExpr(2, 2) + + +def test_qexpr_subs(): + q1 = QExpr(x, y) + assert q1.subs(x, y) == QExpr(y, y) + assert q1.subs({x: 1, y: 2}) == QExpr(1, 2) + + +def test_qsympify(): + assert _qsympify_sequence([[1, 2], [1, 3]]) == (Tuple(1, 2), Tuple(1, 3)) + assert _qsympify_sequence(([1, 2, [3, 4, [2, ]], 1], 3)) == \ + (Tuple(1, 2, Tuple(3, 4, Tuple(2,)), 1), 3) + assert _qsympify_sequence((1,)) == (1,) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qft.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qft.py new file mode 100644 index 0000000000000000000000000000000000000000..832f0194702b2031cfdff9d061a259e85476a88d --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qft.py @@ -0,0 +1,52 @@ +from sympy.core.numbers import (I, pi) +from sympy.core.symbol import Symbol +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.matrices.dense import Matrix + +from sympy.physics.quantum.qft import QFT, IQFT, RkGate +from sympy.physics.quantum.gate import (ZGate, SwapGate, HadamardGate, CGate, + PhaseGate, TGate) +from sympy.physics.quantum.qubit import Qubit +from sympy.physics.quantum.qapply import qapply +from sympy.physics.quantum.represent import represent + +from sympy.functions.elementary.complexes import sign + + +def test_RkGate(): + x = Symbol('x') + assert RkGate(1, x).k == x + assert RkGate(1, x).targets == (1,) + assert RkGate(1, 1) == ZGate(1) + assert RkGate(2, 2) == PhaseGate(2) + assert RkGate(3, 3) == TGate(3) + + assert represent( + RkGate(0, x), nqubits=1) == Matrix([[1, 0], [0, exp(sign(x)*2*pi*I/(2**abs(x)))]]) + + +def test_quantum_fourier(): + assert QFT(0, 3).decompose() == \ + SwapGate(0, 2)*HadamardGate(0)*CGate((0,), PhaseGate(1)) * \ + HadamardGate(1)*CGate((0,), TGate(2))*CGate((1,), PhaseGate(2)) * \ + HadamardGate(2) + + assert IQFT(0, 3).decompose() == \ + HadamardGate(2)*CGate((1,), RkGate(2, -2))*CGate((0,), RkGate(2, -3)) * \ + HadamardGate(1)*CGate((0,), RkGate(1, -2))*HadamardGate(0)*SwapGate(0, 2) + + assert represent(QFT(0, 3), nqubits=3) == \ + Matrix([[exp(2*pi*I/8)**(i*j % 8)/sqrt(8) for i in range(8)] for j in range(8)]) + + assert QFT(0, 4).decompose() # non-trivial decomposition + assert qapply(QFT(0, 3).decompose()*Qubit(0, 0, 0)).expand() == qapply( + HadamardGate(0)*HadamardGate(1)*HadamardGate(2)*Qubit(0, 0, 0) + ).expand() + + +def test_qft_represent(): + c = QFT(0, 3) + a = represent(c, nqubits=3) + b = represent(c.decompose(), nqubits=3) + assert a.evalf(n=10) == b.evalf(n=10) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qubit.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qubit.py new file mode 100644 index 0000000000000000000000000000000000000000..b4c236008a6b8cf85b5a45c5167b9dc36fb21019 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_qubit.py @@ -0,0 +1,264 @@ +import random + +from sympy.core.numbers import (Integer, Rational) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.matrices.dense import Matrix +from sympy.physics.quantum.qubit import (measure_all, measure_all_oneshot, measure_partial, + matrix_to_qubit, matrix_to_density, + qubit_to_matrix, IntQubit, + IntQubitBra, QubitBra) +from sympy.physics.quantum.gate import (HadamardGate, CNOT, XGate, YGate, + ZGate, PhaseGate) +from sympy.physics.quantum.qapply import qapply +from sympy.physics.quantum.represent import represent +from sympy.physics.quantum.shor import Qubit +from sympy.testing.pytest import raises +from sympy.physics.quantum.density import Density +from sympy.physics.quantum.trace import Tr + +x, y = symbols('x,y') + +epsilon = .000001 + + +def test_Qubit(): + array = [0, 0, 1, 1, 0] + qb = Qubit('00110') + assert qb.flip(0) == Qubit('00111') + assert qb.flip(1) == Qubit('00100') + assert qb.flip(4) == Qubit('10110') + assert qb.qubit_values == (0, 0, 1, 1, 0) + assert qb.dimension == 5 + for i in range(5): + assert qb[i] == array[4 - i] + assert len(qb) == 5 + qb = Qubit('110') + + +def test_QubitBra(): + qb = Qubit(0) + qb_bra = QubitBra(0) + assert qb.dual_class() == QubitBra + assert qb_bra.dual_class() == Qubit + + qb = Qubit(1, 1, 0) + qb_bra = QubitBra(1, 1, 0) + assert represent(qb, nqubits=3).H == represent(qb_bra, nqubits=3) + + qb = Qubit(0, 1) + qb_bra = QubitBra(1,0) + assert qb._eval_innerproduct_QubitBra(qb_bra) == Integer(0) + + qb_bra = QubitBra(0, 1) + assert qb._eval_innerproduct_QubitBra(qb_bra) == Integer(1) + + +def test_IntQubit(): + # issue 9136 + iqb = IntQubit(0, nqubits=1) + assert qubit_to_matrix(Qubit('0')) == qubit_to_matrix(iqb) + + qb = Qubit('1010') + assert qubit_to_matrix(IntQubit(qb)) == qubit_to_matrix(qb) + + iqb = IntQubit(1, nqubits=1) + assert qubit_to_matrix(Qubit('1')) == qubit_to_matrix(iqb) + assert qubit_to_matrix(IntQubit(1)) == qubit_to_matrix(iqb) + + iqb = IntQubit(7, nqubits=4) + assert qubit_to_matrix(Qubit('0111')) == qubit_to_matrix(iqb) + assert qubit_to_matrix(IntQubit(7, 4)) == qubit_to_matrix(iqb) + + iqb = IntQubit(8) + assert iqb.as_int() == 8 + assert iqb.qubit_values == (1, 0, 0, 0) + + iqb = IntQubit(7, 4) + assert iqb.qubit_values == (0, 1, 1, 1) + assert IntQubit(3) == IntQubit(3, 2) + + #test Dual Classes + iqb = IntQubit(3) + iqb_bra = IntQubitBra(3) + assert iqb.dual_class() == IntQubitBra + assert iqb_bra.dual_class() == IntQubit + + iqb = IntQubit(5) + iqb_bra = IntQubitBra(5) + assert iqb._eval_innerproduct_IntQubitBra(iqb_bra) == Integer(1) + + iqb = IntQubit(4) + iqb_bra = IntQubitBra(5) + assert iqb._eval_innerproduct_IntQubitBra(iqb_bra) == Integer(0) + raises(ValueError, lambda: IntQubit(4, 1)) + + raises(ValueError, lambda: IntQubit('5')) + raises(ValueError, lambda: IntQubit(5, '5')) + raises(ValueError, lambda: IntQubit(5, nqubits='5')) + raises(TypeError, lambda: IntQubit(5, bad_arg=True)) + +def test_superposition_of_states(): + state = 1/sqrt(2)*Qubit('01') + 1/sqrt(2)*Qubit('10') + state_gate = CNOT(0, 1)*HadamardGate(0)*state + state_expanded = Qubit('01')/2 + Qubit('00')/2 - Qubit('11')/2 + Qubit('10')/2 + assert qapply(state_gate).expand() == state_expanded + assert matrix_to_qubit(represent(state_gate, nqubits=2)) == state_expanded + + +#test apply methods +def test_apply_represent_equality(): + gates = [HadamardGate(int(3*random.random())), + XGate(int(3*random.random())), ZGate(int(3*random.random())), + YGate(int(3*random.random())), ZGate(int(3*random.random())), + PhaseGate(int(3*random.random()))] + + circuit = Qubit(int(random.random()*2), int(random.random()*2), + int(random.random()*2), int(random.random()*2), int(random.random()*2), + int(random.random()*2)) + for i in range(int(random.random()*6)): + circuit = gates[int(random.random()*6)]*circuit + + mat = represent(circuit, nqubits=6) + states = qapply(circuit) + state_rep = matrix_to_qubit(mat) + states = states.expand() + state_rep = state_rep.expand() + assert state_rep == states + + +def test_matrix_to_qubits(): + qb = Qubit(0, 0, 0, 0) + mat = Matrix([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + assert matrix_to_qubit(mat) == qb + assert qubit_to_matrix(qb) == mat + + state = 2*sqrt(2)*(Qubit(0, 0, 0) + Qubit(0, 0, 1) + Qubit(0, 1, 0) + + Qubit(0, 1, 1) + Qubit(1, 0, 0) + Qubit(1, 0, 1) + + Qubit(1, 1, 0) + Qubit(1, 1, 1)) + ones = sqrt(2)*2*Matrix([1, 1, 1, 1, 1, 1, 1, 1]) + assert matrix_to_qubit(ones) == state.expand() + assert qubit_to_matrix(state) == ones + + +def test_measure_normalize(): + a, b = symbols('a b') + state = a*Qubit('110') + b*Qubit('111') + assert measure_partial(state, (0,), normalize=False) == \ + [(a*Qubit('110'), a*a.conjugate()), (b*Qubit('111'), b*b.conjugate())] + assert measure_all(state, normalize=False) == \ + [(Qubit('110'), a*a.conjugate()), (Qubit('111'), b*b.conjugate())] + + +def test_measure_partial(): + #Basic test of collapse of entangled two qubits (Bell States) + state = Qubit('01') + Qubit('10') + assert measure_partial(state, (0,)) == \ + [(Qubit('10'), S.Half), (Qubit('01'), S.Half)] + assert measure_partial(state, int(0)) == \ + [(Qubit('10'), S.Half), (Qubit('01'), S.Half)] + assert measure_partial(state, (0,)) == \ + measure_partial(state, (1,))[::-1] + + #Test of more complex collapse and probability calculation + state1 = sqrt(2)/sqrt(3)*Qubit('00001') + 1/sqrt(3)*Qubit('11111') + assert measure_partial(state1, (0,)) == \ + [(sqrt(2)/sqrt(3)*Qubit('00001') + 1/sqrt(3)*Qubit('11111'), 1)] + assert measure_partial(state1, (1, 2)) == measure_partial(state1, (3, 4)) + assert measure_partial(state1, (1, 2, 3)) == \ + [(Qubit('00001'), Rational(2, 3)), (Qubit('11111'), Rational(1, 3))] + + #test of measuring multiple bits at once + state2 = Qubit('1111') + Qubit('1101') + Qubit('1011') + Qubit('1000') + assert measure_partial(state2, (0, 1, 3)) == \ + [(Qubit('1000'), Rational(1, 4)), (Qubit('1101'), Rational(1, 4)), + (Qubit('1011')/sqrt(2) + Qubit('1111')/sqrt(2), S.Half)] + assert measure_partial(state2, (0,)) == \ + [(Qubit('1000'), Rational(1, 4)), + (Qubit('1111')/sqrt(3) + Qubit('1101')/sqrt(3) + + Qubit('1011')/sqrt(3), Rational(3, 4))] + + +def test_measure_all(): + assert measure_all(Qubit('11')) == [(Qubit('11'), 1)] + state = Qubit('11') + Qubit('10') + assert measure_all(state) == [(Qubit('10'), S.Half), + (Qubit('11'), S.Half)] + state2 = Qubit('11')/sqrt(5) + 2*Qubit('00')/sqrt(5) + assert measure_all(state2) == \ + [(Qubit('00'), Rational(4, 5)), (Qubit('11'), Rational(1, 5))] + + # from issue #12585 + assert measure_all(qapply(Qubit('0'))) == [(Qubit('0'), 1)] + + +def test_measure_all_oneshot(): + random.seed(42) + # for issue #27092 + assert measure_all_oneshot(Qubit('11')) == Qubit('11') + assert measure_all_oneshot(Qubit('1')) == Qubit('1') + assert measure_all_oneshot(Qubit('0')/sqrt(2) + Qubit('1')/sqrt(2)) == \ + Qubit('0') + + +def test_eval_trace(): + q1 = Qubit('10110') + q2 = Qubit('01010') + d = Density([q1, 0.6], [q2, 0.4]) + + t = Tr(d) + assert t.doit() == 1.0 + + # extreme bits + t = Tr(d, 0) + assert t.doit() == (0.4*Density([Qubit('0101'), 1]) + + 0.6*Density([Qubit('1011'), 1])) + t = Tr(d, 4) + assert t.doit() == (0.4*Density([Qubit('1010'), 1]) + + 0.6*Density([Qubit('0110'), 1])) + # index somewhere in between + t = Tr(d, 2) + assert t.doit() == (0.4*Density([Qubit('0110'), 1]) + + 0.6*Density([Qubit('1010'), 1])) + #trace all indices + t = Tr(d, [0, 1, 2, 3, 4]) + assert t.doit() == 1.0 + + # trace some indices, initialized in + # non-canonical order + t = Tr(d, [2, 1, 3]) + assert t.doit() == (0.4*Density([Qubit('00'), 1]) + + 0.6*Density([Qubit('10'), 1])) + + # mixed states + q = (1/sqrt(2)) * (Qubit('00') + Qubit('11')) + d = Density( [q, 1.0] ) + t = Tr(d, 0) + assert t.doit() == (0.5*Density([Qubit('0'), 1]) + + 0.5*Density([Qubit('1'), 1])) + + +def test_matrix_to_density(): + mat = Matrix([[0, 0], [0, 1]]) + assert matrix_to_density(mat) == Density([Qubit('1'), 1]) + + mat = Matrix([[1, 0], [0, 0]]) + assert matrix_to_density(mat) == Density([Qubit('0'), 1]) + + mat = Matrix([[0, 0], [0, 0]]) + assert matrix_to_density(mat) == 0 + + mat = Matrix([[0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 0]]) + + assert matrix_to_density(mat) == Density([Qubit('10'), 1]) + + mat = Matrix([[1, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]]) + + assert matrix_to_density(mat) == Density([Qubit('00'), 1]) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_represent.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_represent.py new file mode 100644 index 0000000000000000000000000000000000000000..c49dcbd7e7876f30cbe8e5426c91419903add5ff --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_represent.py @@ -0,0 +1,186 @@ +from sympy.core.numbers import (Float, I, Integer) +from sympy.matrices.dense import Matrix +from sympy.external import import_module +from sympy.testing.pytest import skip + +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.represent import (represent, rep_innerproduct, + rep_expectation, enumerate_states) +from sympy.physics.quantum.state import Bra, Ket +from sympy.physics.quantum.operator import Operator, OuterProduct +from sympy.physics.quantum.tensorproduct import TensorProduct +from sympy.physics.quantum.tensorproduct import matrix_tensor_product +from sympy.physics.quantum.commutator import Commutator +from sympy.physics.quantum.anticommutator import AntiCommutator +from sympy.physics.quantum.innerproduct import InnerProduct +from sympy.physics.quantum.matrixutils import (numpy_ndarray, + scipy_sparse_matrix, to_numpy, + to_scipy_sparse, to_sympy) +from sympy.physics.quantum.cartesian import XKet, XOp, XBra +from sympy.physics.quantum.qapply import qapply +from sympy.physics.quantum.operatorset import operators_to_state +from sympy.testing.pytest import raises + +Amat = Matrix([[1, I], [-I, 1]]) +Bmat = Matrix([[1, 2], [3, 4]]) +Avec = Matrix([[1], [I]]) + + +class AKet(Ket): + + @classmethod + def dual_class(self): + return ABra + + def _represent_default_basis(self, **options): + return self._represent_AOp(None, **options) + + def _represent_AOp(self, basis, **options): + return Avec + + +class ABra(Bra): + + @classmethod + def dual_class(self): + return AKet + + +class AOp(Operator): + + def _represent_default_basis(self, **options): + return self._represent_AOp(None, **options) + + def _represent_AOp(self, basis, **options): + return Amat + + +class BOp(Operator): + + def _represent_default_basis(self, **options): + return self._represent_AOp(None, **options) + + def _represent_AOp(self, basis, **options): + return Bmat + + +k = AKet('a') +b = ABra('a') +A = AOp('A') +B = BOp('B') + +_tests = [ + # Bra + (b, Dagger(Avec)), + (Dagger(b), Avec), + # Ket + (k, Avec), + (Dagger(k), Dagger(Avec)), + # Operator + (A, Amat), + (Dagger(A), Dagger(Amat)), + # OuterProduct + (OuterProduct(k, b), Avec*Avec.H), + # TensorProduct + (TensorProduct(A, B), matrix_tensor_product(Amat, Bmat)), + # Pow + (A**2, Amat**2), + # Add/Mul + (A*B + 2*A, Amat*Bmat + 2*Amat), + # Commutator + (Commutator(A, B), Amat*Bmat - Bmat*Amat), + # AntiCommutator + (AntiCommutator(A, B), Amat*Bmat + Bmat*Amat), + # InnerProduct + (InnerProduct(b, k), (Avec.H*Avec)[0]) +] + + +def test_format_sympy(): + for test in _tests: + lhs = represent(test[0], basis=A, format='sympy') + rhs = to_sympy(test[1]) + assert lhs == rhs + + +def test_scalar_sympy(): + assert represent(Integer(1)) == Integer(1) + assert represent(Float(1.0)) == Float(1.0) + assert represent(1.0 + I) == 1.0 + I + + +np = import_module('numpy') + + +def test_format_numpy(): + if not np: + skip("numpy not installed.") + + for test in _tests: + lhs = represent(test[0], basis=A, format='numpy') + rhs = to_numpy(test[1]) + if isinstance(lhs, numpy_ndarray): + assert (lhs == rhs).all() + else: + assert lhs == rhs + + +def test_scalar_numpy(): + if not np: + skip("numpy not installed.") + + assert represent(Integer(1), format='numpy') == 1 + assert represent(Float(1.0), format='numpy') == 1.0 + assert represent(1.0 + I, format='numpy') == 1.0 + 1.0j + + +scipy = import_module('scipy', import_kwargs={'fromlist': ['sparse']}) + + +def test_format_scipy_sparse(): + if not np: + skip("numpy not installed.") + if not scipy: + skip("scipy not installed.") + + for test in _tests: + lhs = represent(test[0], basis=A, format='scipy.sparse') + rhs = to_scipy_sparse(test[1]) + if isinstance(lhs, scipy_sparse_matrix): + assert np.linalg.norm((lhs - rhs).todense()) == 0.0 + else: + assert lhs == rhs + + +def test_scalar_scipy_sparse(): + if not np: + skip("numpy not installed.") + if not scipy: + skip("scipy not installed.") + + assert represent(Integer(1), format='scipy.sparse') == 1 + assert represent(Float(1.0), format='scipy.sparse') == 1.0 + assert represent(1.0 + I, format='scipy.sparse') == 1.0 + 1.0j + +x_ket = XKet('x') +x_bra = XBra('x') +x_op = XOp('X') + + +def test_innerprod_represent(): + assert rep_innerproduct(x_ket) == InnerProduct(XBra("x_1"), x_ket).doit() + assert rep_innerproduct(x_bra) == InnerProduct(x_bra, XKet("x_1")).doit() + raises(TypeError, lambda: rep_innerproduct(x_op)) + + +def test_operator_represent(): + basis_kets = enumerate_states(operators_to_state(x_op), 1, 2) + assert rep_expectation( + x_op) == qapply(basis_kets[1].dual*x_op*basis_kets[0]) + + +def test_enumerate_states(): + test = XKet("foo") + assert enumerate_states(test, 1, 1) == [XKet("foo_1")] + assert enumerate_states( + test, [1, 2, 4]) == [XKet("foo_1"), XKet("foo_2"), XKet("foo_4")] diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_sho1d.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_sho1d.py new file mode 100644 index 0000000000000000000000000000000000000000..6acb1f1e7044ac278061cf3b4f04c3c8c09d1848 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_sho1d.py @@ -0,0 +1,176 @@ +"""Tests for sho1d.py""" + +from sympy.concrete import Sum +from sympy.core import oo +from sympy.core.numbers import (I, Integer) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol, symbols +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.complexes import Abs +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.physics.quantum import Dagger +from sympy.physics.quantum.constants import hbar +from sympy.physics.quantum import Commutator +from sympy.physics.quantum.qapply import qapply +from sympy.physics.quantum.innerproduct import InnerProduct +from sympy.physics.quantum.cartesian import X, Px +from sympy.physics.quantum.hilbert import ComplexSpace +from sympy.physics.quantum.represent import represent +from sympy.simplify import simplify +from sympy.external import import_module +from sympy.tensor import IndexedBase, Idx +from sympy.testing.pytest import skip, raises + +from sympy.physics.quantum.sho1d import (RaisingOp, LoweringOp, + SHOKet, SHOBra, + Hamiltonian, NumberOp) + +ad = RaisingOp('a') +a = LoweringOp('a') +k = SHOKet('k') +kz = SHOKet(0) +kf = SHOKet(1) +k3 = SHOKet(3) +b = SHOBra('b') +b3 = SHOBra(3) +H = Hamiltonian('H') +N = NumberOp('N') +omega = Symbol('omega') +m = Symbol('m') +ndim = Integer(4) +p = Symbol('p', integer=True) +q = Symbol('q', nonnegative=True, integer=True) + + +np = import_module('numpy') +scipy = import_module('scipy', import_kwargs={'fromlist': ['sparse']}) + +ad_rep_sympy = represent(ad, basis=N, ndim=4, format='sympy') +a_rep = represent(a, basis=N, ndim=4, format='sympy') +N_rep = represent(N, basis=N, ndim=4, format='sympy') +H_rep = represent(H, basis=N, ndim=4, format='sympy') +k3_rep = represent(k3, basis=N, ndim=4, format='sympy') +b3_rep = represent(b3, basis=N, ndim=4, format='sympy') + +def test_RaisingOp(): + assert Dagger(ad) == a + assert Commutator(ad, a).doit() == Integer(-1) + assert Commutator(ad, N).doit() == Integer(-1)*ad + assert qapply(ad*k) == (sqrt(k.n + 1)*SHOKet(k.n + 1)).expand() + assert qapply(ad*kz) == (sqrt(kz.n + 1)*SHOKet(kz.n + 1)).expand() + assert qapply(ad*kf) == (sqrt(kf.n + 1)*SHOKet(kf.n + 1)).expand() + assert ad.rewrite('xp').doit() == \ + (Integer(1)/sqrt(Integer(2)*hbar*m*omega))*(Integer(-1)*I*Px + m*omega*X) + assert ad.hilbert_space == ComplexSpace(S.Infinity) + for i in range(ndim - 1): + assert ad_rep_sympy[i + 1,i] == sqrt(i + 1) + + if not np: + skip("numpy not installed.") + + ad_rep_numpy = represent(ad, basis=N, ndim=4, format='numpy') + for i in range(ndim - 1): + assert ad_rep_numpy[i + 1,i] == float(sqrt(i + 1)) + + if not np: + skip("numpy not installed.") + if not scipy: + skip("scipy not installed.") + + ad_rep_scipy = represent(ad, basis=N, ndim=4, format='scipy.sparse', spmatrix='lil') + for i in range(ndim - 1): + assert ad_rep_scipy[i + 1,i] == float(sqrt(i + 1)) + + assert ad_rep_numpy.dtype == 'float64' + assert ad_rep_scipy.dtype == 'float64' + +def test_LoweringOp(): + assert Dagger(a) == ad + assert Commutator(a, ad).doit() == Integer(1) + assert Commutator(a, N).doit() == a + assert qapply(a*k) == (sqrt(k.n)*SHOKet(k.n-Integer(1))).expand() + assert qapply(a*kz) == Integer(0) + assert qapply(a*kf) == (sqrt(kf.n)*SHOKet(kf.n-Integer(1))).expand() + assert a.rewrite('xp').doit() == \ + (Integer(1)/sqrt(Integer(2)*hbar*m*omega))*(I*Px + m*omega*X) + for i in range(ndim - 1): + assert a_rep[i,i + 1] == sqrt(i + 1) + +def test_NumberOp(): + assert Commutator(N, ad).doit() == ad + assert Commutator(N, a).doit() == Integer(-1)*a + assert Commutator(N, H).doit() == Integer(0) + assert qapply(N*k) == (k.n*k).expand() + assert N.rewrite('a').doit() == ad*a + assert N.rewrite('xp').doit() == (Integer(1)/(Integer(2)*m*hbar*omega))*( + Px**2 + (m*omega*X)**2) - Integer(1)/Integer(2) + assert N.rewrite('H').doit() == H/(hbar*omega) - Integer(1)/Integer(2) + for i in range(ndim): + assert N_rep[i,i] == i + assert N_rep == ad_rep_sympy*a_rep + +def test_Hamiltonian(): + assert Commutator(H, N).doit() == Integer(0) + assert qapply(H*k) == ((hbar*omega*(k.n + Integer(1)/Integer(2)))*k).expand() + assert H.rewrite('a').doit() == hbar*omega*(ad*a + Integer(1)/Integer(2)) + assert H.rewrite('xp').doit() == \ + (Integer(1)/(Integer(2)*m))*(Px**2 + (m*omega*X)**2) + assert H.rewrite('N').doit() == hbar*omega*(N + Integer(1)/Integer(2)) + for i in range(ndim): + assert H_rep[i,i] == hbar*omega*(i + Integer(1)/Integer(2)) + +def test_SHOKet(): + assert SHOKet('k').dual_class() == SHOBra + assert SHOBra('b').dual_class() == SHOKet + assert InnerProduct(b,k).doit() == KroneckerDelta(k.n, b.n) + assert k.hilbert_space == ComplexSpace(S.Infinity) + assert k3_rep[k3.n, 0] == Integer(1) + assert b3_rep[0, b3.n] == Integer(1) + +def test_sho_sums(): + e1 = Sum(SHOKet(p)*SHOBra(p), (p, 0, 1)) + assert e1.doit() == SHOKet(0)*SHOBra(0) + SHOKet(1)*SHOBra(1) + + # Test qapply with Sum on the left + assert qapply( + Sum(SHOKet(p)*SHOBra(p), (p, 0, oo))*SHOKet(q), + sum_doit=True + ) == SHOKet(q) + + # Test qapply with Sum on the right + a = IndexedBase('a') + n = symbols('n', cls=Idx) + result = qapply(SHOBra(q)*Sum(a[n]*SHOKet(n), (n,0,oo)), sum_doit=True) + assert result == a[q] + + # Test qapply with a product of Sums + result = qapply( + SHOBra(q)*Sum(SHOKet(p)*SHOBra(p), (p, 0, oo))*Sum(a[n]*SHOKet(n), (n,0,oo)), + sum_doit=True + ) + assert result == a[q] + + with raises(ValueError): + result = qapply( + SHOBra(q)*Sum(SHOKet(p)*SHOBra(p), (p, 0, oo))*Sum(a[p]*SHOKet(p), (p,0,oo)), + sum_doit=True + ) + +def test_sho_coherant_state(): + alpha = Symbol('alpha', is_complex=True) + cstate = exp(-Abs(alpha)**2/S(2))*Sum(((alpha**p)/sqrt(factorial(p)))*SHOKet(p), (p,0,oo)) + # Projection onto the number eigenstate + assert qapply(SHOBra(q)*cstate, sum_doit=True) == exp(-Abs(alpha)**2/S(2))*alpha**q/sqrt(factorial(q)) + # Ensure that the coherent state is an eigenstate of annihilation operator + assert simplify(qapply(SHOBra(q)*a*cstate, sum_doit=True)) == simplify(qapply(SHOBra(q)*alpha*cstate, sum_doit=True)) + +def test_issue_26495(): + nbar = Symbol('nbar', real=True, nonnegative=True) + n = Symbol('n', integer=True) + i = Symbol('i', integer=True, nonnegative=True) + j = Symbol('j', integer=True, nonnegative=True) + rho = Sum((nbar/(1+nbar))**n*SHOKet(n)*SHOBra(n), (n,0,oo)) + result = qapply(SHOBra(i)*rho*SHOKet(j), sum_doit=True) + assert simplify(result) == (nbar/(nbar+1))**i*KroneckerDelta(i,j) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_shor.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_shor.py new file mode 100644 index 0000000000000000000000000000000000000000..0ebccbc199be8640f2021933abbe58716c68f788 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_shor.py @@ -0,0 +1,21 @@ +from sympy.testing.pytest import XFAIL + +from sympy.physics.quantum.qapply import qapply +from sympy.physics.quantum.qubit import Qubit +from sympy.physics.quantum.shor import CMod, getr + + +@XFAIL +def test_CMod(): + assert qapply(CMod(4, 2, 2)*Qubit(0, 0, 1, 0, 0, 0, 0, 0)) == \ + Qubit(0, 0, 1, 0, 0, 0, 0, 0) + assert qapply(CMod(5, 5, 7)*Qubit(0, 0, 1, 0, 0, 0, 0, 0, 0, 0)) == \ + Qubit(0, 0, 1, 0, 0, 0, 0, 0, 1, 0) + assert qapply(CMod(3, 2, 3)*Qubit(0, 1, 0, 0, 0, 0)) == \ + Qubit(0, 1, 0, 0, 0, 1) + + +def test_continued_frac(): + assert getr(513, 1024, 10) == 2 + assert getr(169, 1024, 11) == 6 + assert getr(314, 4096, 16) == 13 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_spin.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_spin.py new file mode 100644 index 0000000000000000000000000000000000000000..f905a7de5aed31e24a6d7c882b6a768a787c61cb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_spin.py @@ -0,0 +1,4333 @@ +from sympy.concrete.summations import Sum +from sympy.core.function import expand +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.matrices.dense import Matrix +from sympy.abc import alpha, beta, gamma, j, m +from sympy.simplify import simplify + +from sympy.physics.quantum import hbar, represent, Commutator, InnerProduct +from sympy.physics.quantum.qapply import qapply +from sympy.physics.quantum.tensorproduct import TensorProduct +from sympy.physics.quantum.cg import CG +from sympy.physics.quantum.spin import ( + Jx, Jy, Jz, Jplus, Jminus, J2, + JxBra, JyBra, JzBra, + JxKet, JyKet, JzKet, + JxKetCoupled, JyKetCoupled, JzKetCoupled, + couple, uncouple, + Rotation, WignerD +) + +from sympy.testing.pytest import raises, slow + +j1, j2, j3, j4, m1, m2, m3, m4 = symbols('j1:5 m1:5') +j12, j13, j24, j34, j123, j134, mi, mi1, mp = symbols( + 'j12 j13 j24 j34 j123 j134 mi mi1 mp') + + +def assert_simplify_expand(e1, e2): + """Helper for simplifying and expanding results. + + This is needed to help us test complex expressions whose form + might change in subtle ways as the rest of sympy evolves. + """ + assert simplify(e1.expand(tensorproduct=True)) == \ + simplify(e2.expand(tensorproduct=True)) + + +def test_represent_spin_operators(): + assert represent(Jx) == hbar*Matrix([[0, 1], [1, 0]])/2 + assert represent( + Jx, j=1) == hbar*sqrt(2)*Matrix([[0, 1, 0], [1, 0, 1], [0, 1, 0]])/2 + assert represent(Jy) == hbar*I*Matrix([[0, -1], [1, 0]])/2 + assert represent(Jy, j=1) == hbar*I*sqrt(2)*Matrix([[0, -1, 0], [1, + 0, -1], [0, 1, 0]])/2 + assert represent(Jz) == hbar*Matrix([[1, 0], [0, -1]])/2 + assert represent( + Jz, j=1) == hbar*Matrix([[1, 0, 0], [0, 0, 0], [0, 0, -1]]) + + +def test_represent_spin_states(): + # Jx basis + assert represent(JxKet(S.Half, S.Half), basis=Jx) == Matrix([1, 0]) + assert represent(JxKet(S.Half, Rational(-1, 2)), basis=Jx) == Matrix([0, 1]) + assert represent(JxKet(1, 1), basis=Jx) == Matrix([1, 0, 0]) + assert represent(JxKet(1, 0), basis=Jx) == Matrix([0, 1, 0]) + assert represent(JxKet(1, -1), basis=Jx) == Matrix([0, 0, 1]) + assert represent( + JyKet(S.Half, S.Half), basis=Jx) == Matrix([exp(-I*pi/4), 0]) + assert represent( + JyKet(S.Half, Rational(-1, 2)), basis=Jx) == Matrix([0, exp(I*pi/4)]) + assert represent(JyKet(1, 1), basis=Jx) == Matrix([-I, 0, 0]) + assert represent(JyKet(1, 0), basis=Jx) == Matrix([0, 1, 0]) + assert represent(JyKet(1, -1), basis=Jx) == Matrix([0, 0, I]) + assert represent( + JzKet(S.Half, S.Half), basis=Jx) == sqrt(2)*Matrix([-1, 1])/2 + assert represent( + JzKet(S.Half, Rational(-1, 2)), basis=Jx) == sqrt(2)*Matrix([-1, -1])/2 + assert represent(JzKet(1, 1), basis=Jx) == Matrix([1, -sqrt(2), 1])/2 + assert represent(JzKet(1, 0), basis=Jx) == sqrt(2)*Matrix([1, 0, -1])/2 + assert represent(JzKet(1, -1), basis=Jx) == Matrix([1, sqrt(2), 1])/2 + # Jy basis + assert represent( + JxKet(S.Half, S.Half), basis=Jy) == Matrix([exp(I*pi*Rational(-3, 4)), 0]) + assert represent( + JxKet(S.Half, Rational(-1, 2)), basis=Jy) == Matrix([0, exp(I*pi*Rational(3, 4))]) + assert represent(JxKet(1, 1), basis=Jy) == Matrix([I, 0, 0]) + assert represent(JxKet(1, 0), basis=Jy) == Matrix([0, 1, 0]) + assert represent(JxKet(1, -1), basis=Jy) == Matrix([0, 0, -I]) + assert represent(JyKet(S.Half, S.Half), basis=Jy) == Matrix([1, 0]) + assert represent(JyKet(S.Half, Rational(-1, 2)), basis=Jy) == Matrix([0, 1]) + assert represent(JyKet(1, 1), basis=Jy) == Matrix([1, 0, 0]) + assert represent(JyKet(1, 0), basis=Jy) == Matrix([0, 1, 0]) + assert represent(JyKet(1, -1), basis=Jy) == Matrix([0, 0, 1]) + assert represent( + JzKet(S.Half, S.Half), basis=Jy) == sqrt(2)*Matrix([-1, I])/2 + assert represent( + JzKet(S.Half, Rational(-1, 2)), basis=Jy) == sqrt(2)*Matrix([I, -1])/2 + assert represent(JzKet(1, 1), basis=Jy) == Matrix([1, -I*sqrt(2), -1])/2 + assert represent( + JzKet(1, 0), basis=Jy) == Matrix([-sqrt(2)*I, 0, -sqrt(2)*I])/2 + assert represent(JzKet(1, -1), basis=Jy) == Matrix([-1, -sqrt(2)*I, 1])/2 + # Jz basis + assert represent( + JxKet(S.Half, S.Half), basis=Jz) == sqrt(2)*Matrix([1, 1])/2 + assert represent( + JxKet(S.Half, Rational(-1, 2)), basis=Jz) == sqrt(2)*Matrix([-1, 1])/2 + assert represent(JxKet(1, 1), basis=Jz) == Matrix([1, sqrt(2), 1])/2 + assert represent(JxKet(1, 0), basis=Jz) == sqrt(2)*Matrix([-1, 0, 1])/2 + assert represent(JxKet(1, -1), basis=Jz) == Matrix([1, -sqrt(2), 1])/2 + assert represent( + JyKet(S.Half, S.Half), basis=Jz) == sqrt(2)*Matrix([-1, -I])/2 + assert represent( + JyKet(S.Half, Rational(-1, 2)), basis=Jz) == sqrt(2)*Matrix([-I, -1])/2 + assert represent(JyKet(1, 1), basis=Jz) == Matrix([1, sqrt(2)*I, -1])/2 + assert represent(JyKet(1, 0), basis=Jz) == sqrt(2)*Matrix([I, 0, I])/2 + assert represent(JyKet(1, -1), basis=Jz) == Matrix([-1, sqrt(2)*I, 1])/2 + assert represent(JzKet(S.Half, S.Half), basis=Jz) == Matrix([1, 0]) + assert represent(JzKet(S.Half, Rational(-1, 2)), basis=Jz) == Matrix([0, 1]) + assert represent(JzKet(1, 1), basis=Jz) == Matrix([1, 0, 0]) + assert represent(JzKet(1, 0), basis=Jz) == Matrix([0, 1, 0]) + assert represent(JzKet(1, -1), basis=Jz) == Matrix([0, 0, 1]) + + +def test_represent_uncoupled_states(): + # Jx basis + assert represent(TensorProduct(JxKet(S.Half, S.Half), JxKet(S.Half, S.Half)), basis=Jx) == \ + Matrix([1, 0, 0, 0]) + assert represent(TensorProduct(JxKet(S.Half, S.Half), JxKet(S.Half, Rational(-1, 2))), basis=Jx) == \ + Matrix([0, 1, 0, 0]) + assert represent(TensorProduct(JxKet(S.Half, Rational(-1, 2)), JxKet(S.Half, S.Half)), basis=Jx) == \ + Matrix([0, 0, 1, 0]) + assert represent(TensorProduct(JxKet(S.Half, Rational(-1, 2)), JxKet(S.Half, Rational(-1, 2))), basis=Jx) == \ + Matrix([0, 0, 0, 1]) + assert represent(TensorProduct(JyKet(S.Half, S.Half), JyKet(S.Half, S.Half)), basis=Jx) == \ + Matrix([-I, 0, 0, 0]) + assert represent(TensorProduct(JyKet(S.Half, S.Half), JyKet(S.Half, Rational(-1, 2))), basis=Jx) == \ + Matrix([0, 1, 0, 0]) + assert represent(TensorProduct(JyKet(S.Half, Rational(-1, 2)), JyKet(S.Half, S.Half)), basis=Jx) == \ + Matrix([0, 0, 1, 0]) + assert represent(TensorProduct(JyKet(S.Half, Rational(-1, 2)), JyKet(S.Half, Rational(-1, 2))), basis=Jx) == \ + Matrix([0, 0, 0, I]) + assert represent(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)), basis=Jx) == \ + Matrix([S.Half, Rational(-1, 2), Rational(-1, 2), S.Half]) + assert represent(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), basis=Jx) == \ + Matrix([S.Half, S.Half, Rational(-1, 2), Rational(-1, 2)]) + assert represent(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), basis=Jx) == \ + Matrix([S.Half, Rational(-1, 2), S.Half, Rational(-1, 2)]) + assert represent(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), basis=Jx) == \ + Matrix([S.Half, S.Half, S.Half, S.Half]) + # Jy basis + assert represent(TensorProduct(JxKet(S.Half, S.Half), JxKet(S.Half, S.Half)), basis=Jy) == \ + Matrix([I, 0, 0, 0]) + assert represent(TensorProduct(JxKet(S.Half, S.Half), JxKet(S.Half, Rational(-1, 2))), basis=Jy) == \ + Matrix([0, 1, 0, 0]) + assert represent(TensorProduct(JxKet(S.Half, Rational(-1, 2)), JxKet(S.Half, S.Half)), basis=Jy) == \ + Matrix([0, 0, 1, 0]) + assert represent(TensorProduct(JxKet(S.Half, Rational(-1, 2)), JxKet(S.Half, Rational(-1, 2))), basis=Jy) == \ + Matrix([0, 0, 0, -I]) + assert represent(TensorProduct(JyKet(S.Half, S.Half), JyKet(S.Half, S.Half)), basis=Jy) == \ + Matrix([1, 0, 0, 0]) + assert represent(TensorProduct(JyKet(S.Half, S.Half), JyKet(S.Half, Rational(-1, 2))), basis=Jy) == \ + Matrix([0, 1, 0, 0]) + assert represent(TensorProduct(JyKet(S.Half, Rational(-1, 2)), JyKet(S.Half, S.Half)), basis=Jy) == \ + Matrix([0, 0, 1, 0]) + assert represent(TensorProduct(JyKet(S.Half, Rational(-1, 2)), JyKet(S.Half, Rational(-1, 2))), basis=Jy) == \ + Matrix([0, 0, 0, 1]) + assert represent(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)), basis=Jy) == \ + Matrix([S.Half, -I/2, -I/2, Rational(-1, 2)]) + assert represent(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), basis=Jy) == \ + Matrix([-I/2, S.Half, Rational(-1, 2), -I/2]) + assert represent(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), basis=Jy) == \ + Matrix([-I/2, Rational(-1, 2), S.Half, -I/2]) + assert represent(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), basis=Jy) == \ + Matrix([Rational(-1, 2), -I/2, -I/2, S.Half]) + # Jz basis + assert represent(TensorProduct(JxKet(S.Half, S.Half), JxKet(S.Half, S.Half)), basis=Jz) == \ + Matrix([S.Half, S.Half, S.Half, S.Half]) + assert represent(TensorProduct(JxKet(S.Half, S.Half), JxKet(S.Half, Rational(-1, 2))), basis=Jz) == \ + Matrix([Rational(-1, 2), S.Half, Rational(-1, 2), S.Half]) + assert represent(TensorProduct(JxKet(S.Half, Rational(-1, 2)), JxKet(S.Half, S.Half)), basis=Jz) == \ + Matrix([Rational(-1, 2), Rational(-1, 2), S.Half, S.Half]) + assert represent(TensorProduct(JxKet(S.Half, Rational(-1, 2)), JxKet(S.Half, Rational(-1, 2))), basis=Jz) == \ + Matrix([S.Half, Rational(-1, 2), Rational(-1, 2), S.Half]) + assert represent(TensorProduct(JyKet(S.Half, S.Half), JyKet(S.Half, S.Half)), basis=Jz) == \ + Matrix([S.Half, I/2, I/2, Rational(-1, 2)]) + assert represent(TensorProduct(JyKet(S.Half, S.Half), JyKet(S.Half, Rational(-1, 2))), basis=Jz) == \ + Matrix([I/2, S.Half, Rational(-1, 2), I/2]) + assert represent(TensorProduct(JyKet(S.Half, Rational(-1, 2)), JyKet(S.Half, S.Half)), basis=Jz) == \ + Matrix([I/2, Rational(-1, 2), S.Half, I/2]) + assert represent(TensorProduct(JyKet(S.Half, Rational(-1, 2)), JyKet(S.Half, Rational(-1, 2))), basis=Jz) == \ + Matrix([Rational(-1, 2), I/2, I/2, S.Half]) + assert represent(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)), basis=Jz) == \ + Matrix([1, 0, 0, 0]) + assert represent(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), basis=Jz) == \ + Matrix([0, 1, 0, 0]) + assert represent(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), basis=Jz) == \ + Matrix([0, 0, 1, 0]) + assert represent(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), basis=Jz) == \ + Matrix([0, 0, 0, 1]) + + +def test_represent_coupled_states(): + # Jx basis + assert represent(JxKetCoupled(0, 0, (S.Half, S.Half)), basis=Jx) == \ + Matrix([1, 0, 0, 0]) + assert represent(JxKetCoupled(1, 1, (S.Half, S.Half)), basis=Jx) == \ + Matrix([0, 1, 0, 0]) + assert represent(JxKetCoupled(1, 0, (S.Half, S.Half)), basis=Jx) == \ + Matrix([0, 0, 1, 0]) + assert represent(JxKetCoupled(1, -1, (S.Half, S.Half)), basis=Jx) == \ + Matrix([0, 0, 0, 1]) + assert represent(JyKetCoupled(0, 0, (S.Half, S.Half)), basis=Jx) == \ + Matrix([1, 0, 0, 0]) + assert represent(JyKetCoupled(1, 1, (S.Half, S.Half)), basis=Jx) == \ + Matrix([0, -I, 0, 0]) + assert represent(JyKetCoupled(1, 0, (S.Half, S.Half)), basis=Jx) == \ + Matrix([0, 0, 1, 0]) + assert represent(JyKetCoupled(1, -1, (S.Half, S.Half)), basis=Jx) == \ + Matrix([0, 0, 0, I]) + assert represent(JzKetCoupled(0, 0, (S.Half, S.Half)), basis=Jx) == \ + Matrix([1, 0, 0, 0]) + assert represent(JzKetCoupled(1, 1, (S.Half, S.Half)), basis=Jx) == \ + Matrix([0, S.Half, -sqrt(2)/2, S.Half]) + assert represent(JzKetCoupled(1, 0, (S.Half, S.Half)), basis=Jx) == \ + Matrix([0, sqrt(2)/2, 0, -sqrt(2)/2]) + assert represent(JzKetCoupled(1, -1, (S.Half, S.Half)), basis=Jx) == \ + Matrix([0, S.Half, sqrt(2)/2, S.Half]) + # Jy basis + assert represent(JxKetCoupled(0, 0, (S.Half, S.Half)), basis=Jy) == \ + Matrix([1, 0, 0, 0]) + assert represent(JxKetCoupled(1, 1, (S.Half, S.Half)), basis=Jy) == \ + Matrix([0, I, 0, 0]) + assert represent(JxKetCoupled(1, 0, (S.Half, S.Half)), basis=Jy) == \ + Matrix([0, 0, 1, 0]) + assert represent(JxKetCoupled(1, -1, (S.Half, S.Half)), basis=Jy) == \ + Matrix([0, 0, 0, -I]) + assert represent(JyKetCoupled(0, 0, (S.Half, S.Half)), basis=Jy) == \ + Matrix([1, 0, 0, 0]) + assert represent(JyKetCoupled(1, 1, (S.Half, S.Half)), basis=Jy) == \ + Matrix([0, 1, 0, 0]) + assert represent(JyKetCoupled(1, 0, (S.Half, S.Half)), basis=Jy) == \ + Matrix([0, 0, 1, 0]) + assert represent(JyKetCoupled(1, -1, (S.Half, S.Half)), basis=Jy) == \ + Matrix([0, 0, 0, 1]) + assert represent(JzKetCoupled(0, 0, (S.Half, S.Half)), basis=Jy) == \ + Matrix([1, 0, 0, 0]) + assert represent(JzKetCoupled(1, 1, (S.Half, S.Half)), basis=Jy) == \ + Matrix([0, S.Half, -I*sqrt(2)/2, Rational(-1, 2)]) + assert represent(JzKetCoupled(1, 0, (S.Half, S.Half)), basis=Jy) == \ + Matrix([0, -I*sqrt(2)/2, 0, -I*sqrt(2)/2]) + assert represent(JzKetCoupled(1, -1, (S.Half, S.Half)), basis=Jy) == \ + Matrix([0, Rational(-1, 2), -I*sqrt(2)/2, S.Half]) + # Jz basis + assert represent(JxKetCoupled(0, 0, (S.Half, S.Half)), basis=Jz) == \ + Matrix([1, 0, 0, 0]) + assert represent(JxKetCoupled(1, 1, (S.Half, S.Half)), basis=Jz) == \ + Matrix([0, S.Half, sqrt(2)/2, S.Half]) + assert represent(JxKetCoupled(1, 0, (S.Half, S.Half)), basis=Jz) == \ + Matrix([0, -sqrt(2)/2, 0, sqrt(2)/2]) + assert represent(JxKetCoupled(1, -1, (S.Half, S.Half)), basis=Jz) == \ + Matrix([0, S.Half, -sqrt(2)/2, S.Half]) + assert represent(JyKetCoupled(0, 0, (S.Half, S.Half)), basis=Jz) == \ + Matrix([1, 0, 0, 0]) + assert represent(JyKetCoupled(1, 1, (S.Half, S.Half)), basis=Jz) == \ + Matrix([0, S.Half, I*sqrt(2)/2, Rational(-1, 2)]) + assert represent(JyKetCoupled(1, 0, (S.Half, S.Half)), basis=Jz) == \ + Matrix([0, I*sqrt(2)/2, 0, I*sqrt(2)/2]) + assert represent(JyKetCoupled(1, -1, (S.Half, S.Half)), basis=Jz) == \ + Matrix([0, Rational(-1, 2), I*sqrt(2)/2, S.Half]) + assert represent(JzKetCoupled(0, 0, (S.Half, S.Half)), basis=Jz) == \ + Matrix([1, 0, 0, 0]) + assert represent(JzKetCoupled(1, 1, (S.Half, S.Half)), basis=Jz) == \ + Matrix([0, 1, 0, 0]) + assert represent(JzKetCoupled(1, 0, (S.Half, S.Half)), basis=Jz) == \ + Matrix([0, 0, 1, 0]) + assert represent(JzKetCoupled(1, -1, (S.Half, S.Half)), basis=Jz) == \ + Matrix([0, 0, 0, 1]) + + +def test_represent_rotation(): + assert represent(Rotation(0, pi/2, 0)) == \ + Matrix( + [[WignerD( + S( + 1)/2, S( + 1)/2, S( + 1)/2, 0, pi/2, 0), WignerD( + S.Half, S.Half, Rational(-1, 2), 0, pi/2, 0)], + [WignerD(S.Half, Rational(-1, 2), S.Half, 0, pi/2, 0), WignerD(S.Half, Rational(-1, 2), Rational(-1, 2), 0, pi/2, 0)]]) + assert represent(Rotation(0, pi/2, 0), doit=True) == \ + Matrix([[sqrt(2)/2, -sqrt(2)/2], + [sqrt(2)/2, sqrt(2)/2]]) + + +def test_rewrite_same(): + # Rewrite to same basis + assert JxBra(1, 1).rewrite('Jx') == JxBra(1, 1) + assert JxBra(j, m).rewrite('Jx') == JxBra(j, m) + assert JxKet(1, 1).rewrite('Jx') == JxKet(1, 1) + assert JxKet(j, m).rewrite('Jx') == JxKet(j, m) + + +def test_rewrite_Bra(): + # Numerical + assert JxBra(1, 1).rewrite('Jy') == -I*JyBra(1, 1) + assert JxBra(1, 0).rewrite('Jy') == JyBra(1, 0) + assert JxBra(1, -1).rewrite('Jy') == I*JyBra(1, -1) + assert JxBra(1, 1).rewrite( + 'Jz') == JzBra(1, 1)/2 + JzBra(1, 0)/sqrt(2) + JzBra(1, -1)/2 + assert JxBra( + 1, 0).rewrite('Jz') == -sqrt(2)*JzBra(1, 1)/2 + sqrt(2)*JzBra(1, -1)/2 + assert JxBra(1, -1).rewrite( + 'Jz') == JzBra(1, 1)/2 - JzBra(1, 0)/sqrt(2) + JzBra(1, -1)/2 + assert JyBra(1, 1).rewrite('Jx') == I*JxBra(1, 1) + assert JyBra(1, 0).rewrite('Jx') == JxBra(1, 0) + assert JyBra(1, -1).rewrite('Jx') == -I*JxBra(1, -1) + assert JyBra(1, 1).rewrite( + 'Jz') == JzBra(1, 1)/2 - sqrt(2)*I*JzBra(1, 0)/2 - JzBra(1, -1)/2 + assert JyBra(1, 0).rewrite( + 'Jz') == -sqrt(2)*I*JzBra(1, 1)/2 - sqrt(2)*I*JzBra(1, -1)/2 + assert JyBra(1, -1).rewrite( + 'Jz') == -JzBra(1, 1)/2 - sqrt(2)*I*JzBra(1, 0)/2 + JzBra(1, -1)/2 + assert JzBra(1, 1).rewrite( + 'Jx') == JxBra(1, 1)/2 - sqrt(2)*JxBra(1, 0)/2 + JxBra(1, -1)/2 + assert JzBra( + 1, 0).rewrite('Jx') == sqrt(2)*JxBra(1, 1)/2 - sqrt(2)*JxBra(1, -1)/2 + assert JzBra(1, -1).rewrite( + 'Jx') == JxBra(1, 1)/2 + sqrt(2)*JxBra(1, 0)/2 + JxBra(1, -1)/2 + assert JzBra(1, 1).rewrite( + 'Jy') == JyBra(1, 1)/2 + sqrt(2)*I*JyBra(1, 0)/2 - JyBra(1, -1)/2 + assert JzBra(1, 0).rewrite( + 'Jy') == sqrt(2)*I*JyBra(1, 1)/2 + sqrt(2)*I*JyBra(1, -1)/2 + assert JzBra(1, -1).rewrite( + 'Jy') == -JyBra(1, 1)/2 + sqrt(2)*I*JyBra(1, 0)/2 + JyBra(1, -1)/2 + # Symbolic + assert JxBra(j, m).rewrite('Jy') == Sum( + WignerD(j, mi, m, pi*Rational(3, 2), 0, 0) * JyBra(j, mi), (mi, -j, j)) + assert JxBra(j, m).rewrite('Jz') == Sum( + WignerD(j, mi, m, 0, pi/2, 0) * JzBra(j, mi), (mi, -j, j)) + assert JyBra(j, m).rewrite('Jx') == Sum( + WignerD(j, mi, m, 0, 0, pi/2) * JxBra(j, mi), (mi, -j, j)) + assert JyBra(j, m).rewrite('Jz') == Sum( + WignerD(j, mi, m, pi*Rational(3, 2), -pi/2, pi/2) * JzBra(j, mi), (mi, -j, j)) + assert JzBra(j, m).rewrite('Jx') == Sum( + WignerD(j, mi, m, 0, pi*Rational(3, 2), 0) * JxBra(j, mi), (mi, -j, j)) + assert JzBra(j, m).rewrite('Jy') == Sum( + WignerD(j, mi, m, pi*Rational(3, 2), pi/2, pi/2) * JyBra(j, mi), (mi, -j, j)) + + +def test_rewrite_Ket(): + # Numerical + assert JxKet(1, 1).rewrite('Jy') == I*JyKet(1, 1) + assert JxKet(1, 0).rewrite('Jy') == JyKet(1, 0) + assert JxKet(1, -1).rewrite('Jy') == -I*JyKet(1, -1) + assert JxKet(1, 1).rewrite( + 'Jz') == JzKet(1, 1)/2 + JzKet(1, 0)/sqrt(2) + JzKet(1, -1)/2 + assert JxKet( + 1, 0).rewrite('Jz') == -sqrt(2)*JzKet(1, 1)/2 + sqrt(2)*JzKet(1, -1)/2 + assert JxKet(1, -1).rewrite( + 'Jz') == JzKet(1, 1)/2 - JzKet(1, 0)/sqrt(2) + JzKet(1, -1)/2 + assert JyKet(1, 1).rewrite('Jx') == -I*JxKet(1, 1) + assert JyKet(1, 0).rewrite('Jx') == JxKet(1, 0) + assert JyKet(1, -1).rewrite('Jx') == I*JxKet(1, -1) + assert JyKet(1, 1).rewrite( + 'Jz') == JzKet(1, 1)/2 + sqrt(2)*I*JzKet(1, 0)/2 - JzKet(1, -1)/2 + assert JyKet(1, 0).rewrite( + 'Jz') == sqrt(2)*I*JzKet(1, 1)/2 + sqrt(2)*I*JzKet(1, -1)/2 + assert JyKet(1, -1).rewrite( + 'Jz') == -JzKet(1, 1)/2 + sqrt(2)*I*JzKet(1, 0)/2 + JzKet(1, -1)/2 + assert JzKet(1, 1).rewrite( + 'Jx') == JxKet(1, 1)/2 - sqrt(2)*JxKet(1, 0)/2 + JxKet(1, -1)/2 + assert JzKet( + 1, 0).rewrite('Jx') == sqrt(2)*JxKet(1, 1)/2 - sqrt(2)*JxKet(1, -1)/2 + assert JzKet(1, -1).rewrite( + 'Jx') == JxKet(1, 1)/2 + sqrt(2)*JxKet(1, 0)/2 + JxKet(1, -1)/2 + assert JzKet(1, 1).rewrite( + 'Jy') == JyKet(1, 1)/2 - sqrt(2)*I*JyKet(1, 0)/2 - JyKet(1, -1)/2 + assert JzKet(1, 0).rewrite( + 'Jy') == -sqrt(2)*I*JyKet(1, 1)/2 - sqrt(2)*I*JyKet(1, -1)/2 + assert JzKet(1, -1).rewrite( + 'Jy') == -JyKet(1, 1)/2 - sqrt(2)*I*JyKet(1, 0)/2 + JyKet(1, -1)/2 + # Symbolic + assert JxKet(j, m).rewrite('Jy') == Sum( + WignerD(j, mi, m, pi*Rational(3, 2), 0, 0) * JyKet(j, mi), (mi, -j, j)) + assert JxKet(j, m).rewrite('Jz') == Sum( + WignerD(j, mi, m, 0, pi/2, 0) * JzKet(j, mi), (mi, -j, j)) + assert JyKet(j, m).rewrite('Jx') == Sum( + WignerD(j, mi, m, 0, 0, pi/2) * JxKet(j, mi), (mi, -j, j)) + assert JyKet(j, m).rewrite('Jz') == Sum( + WignerD(j, mi, m, pi*Rational(3, 2), -pi/2, pi/2) * JzKet(j, mi), (mi, -j, j)) + assert JzKet(j, m).rewrite('Jx') == Sum( + WignerD(j, mi, m, 0, pi*Rational(3, 2), 0) * JxKet(j, mi), (mi, -j, j)) + assert JzKet(j, m).rewrite('Jy') == Sum( + WignerD(j, mi, m, pi*Rational(3, 2), pi/2, pi/2) * JyKet(j, mi), (mi, -j, j)) + + +def test_rewrite_uncoupled_state(): + # Numerical + assert TensorProduct(JyKet(1, 1), JxKet( + 1, 1)).rewrite('Jx') == -I*TensorProduct(JxKet(1, 1), JxKet(1, 1)) + assert TensorProduct(JyKet(1, 0), JxKet( + 1, 1)).rewrite('Jx') == TensorProduct(JxKet(1, 0), JxKet(1, 1)) + assert TensorProduct(JyKet(1, -1), JxKet( + 1, 1)).rewrite('Jx') == I*TensorProduct(JxKet(1, -1), JxKet(1, 1)) + assert TensorProduct(JzKet(1, 1), JxKet(1, 1)).rewrite('Jx') == \ + TensorProduct(JxKet(1, -1), JxKet(1, 1))/2 - sqrt(2)*TensorProduct(JxKet( + 1, 0), JxKet(1, 1))/2 + TensorProduct(JxKet(1, 1), JxKet(1, 1))/2 + assert TensorProduct(JzKet(1, 0), JxKet(1, 1)).rewrite('Jx') == \ + -sqrt(2)*TensorProduct(JxKet(1, -1), JxKet(1, 1))/2 + sqrt( + 2)*TensorProduct(JxKet(1, 1), JxKet(1, 1))/2 + assert TensorProduct(JzKet(1, -1), JxKet(1, 1)).rewrite('Jx') == \ + TensorProduct(JxKet(1, -1), JxKet(1, 1))/2 + sqrt(2)*TensorProduct(JxKet(1, 0), JxKet(1, 1))/2 + TensorProduct(JxKet(1, 1), JxKet(1, 1))/2 + assert TensorProduct(JxKet(1, 1), JyKet( + 1, 1)).rewrite('Jy') == I*TensorProduct(JyKet(1, 1), JyKet(1, 1)) + assert TensorProduct(JxKet(1, 0), JyKet( + 1, 1)).rewrite('Jy') == TensorProduct(JyKet(1, 0), JyKet(1, 1)) + assert TensorProduct(JxKet(1, -1), JyKet( + 1, 1)).rewrite('Jy') == -I*TensorProduct(JyKet(1, -1), JyKet(1, 1)) + assert TensorProduct(JzKet(1, 1), JyKet(1, 1)).rewrite('Jy') == \ + -TensorProduct(JyKet(1, -1), JyKet(1, 1))/2 - sqrt(2)*I*TensorProduct(JyKet(1, 0), JyKet(1, 1))/2 + TensorProduct(JyKet(1, 1), JyKet(1, 1))/2 + assert TensorProduct(JzKet(1, 0), JyKet(1, 1)).rewrite('Jy') == \ + -sqrt(2)*I*TensorProduct(JyKet(1, -1), JyKet( + 1, 1))/2 - sqrt(2)*I*TensorProduct(JyKet(1, 1), JyKet(1, 1))/2 + assert TensorProduct(JzKet(1, -1), JyKet(1, 1)).rewrite('Jy') == \ + TensorProduct(JyKet(1, -1), JyKet(1, 1))/2 - sqrt(2)*I*TensorProduct(JyKet(1, 0), JyKet(1, 1))/2 - TensorProduct(JyKet(1, 1), JyKet(1, 1))/2 + assert TensorProduct(JxKet(1, 1), JzKet(1, 1)).rewrite('Jz') == \ + TensorProduct(JzKet(1, -1), JzKet(1, 1))/2 + sqrt(2)*TensorProduct(JzKet(1, 0), JzKet(1, 1))/2 + TensorProduct(JzKet(1, 1), JzKet(1, 1))/2 + assert TensorProduct(JxKet(1, 0), JzKet(1, 1)).rewrite('Jz') == \ + sqrt(2)*TensorProduct(JzKet(1, -1), JzKet( + 1, 1))/2 - sqrt(2)*TensorProduct(JzKet(1, 1), JzKet(1, 1))/2 + assert TensorProduct(JxKet(1, -1), JzKet(1, 1)).rewrite('Jz') == \ + TensorProduct(JzKet(1, -1), JzKet(1, 1))/2 - sqrt(2)*TensorProduct(JzKet(1, 0), JzKet(1, 1))/2 + TensorProduct(JzKet(1, 1), JzKet(1, 1))/2 + assert TensorProduct(JyKet(1, 1), JzKet(1, 1)).rewrite('Jz') == \ + -TensorProduct(JzKet(1, -1), JzKet(1, 1))/2 + sqrt(2)*I*TensorProduct(JzKet(1, 0), JzKet(1, 1))/2 + TensorProduct(JzKet(1, 1), JzKet(1, 1))/2 + assert TensorProduct(JyKet(1, 0), JzKet(1, 1)).rewrite('Jz') == \ + sqrt(2)*I*TensorProduct(JzKet(1, -1), JzKet( + 1, 1))/2 + sqrt(2)*I*TensorProduct(JzKet(1, 1), JzKet(1, 1))/2 + assert TensorProduct(JyKet(1, -1), JzKet(1, 1)).rewrite('Jz') == \ + TensorProduct(JzKet(1, -1), JzKet(1, 1))/2 + sqrt(2)*I*TensorProduct(JzKet(1, 0), JzKet(1, 1))/2 - TensorProduct(JzKet(1, 1), JzKet(1, 1))/2 + # Symbolic + assert TensorProduct(JyKet(j1, m1), JxKet(j2, m2)).rewrite('Jy') == \ + TensorProduct(JyKet(j1, m1), Sum( + WignerD(j2, mi, m2, pi*Rational(3, 2), 0, 0) * JyKet(j2, mi), (mi, -j2, j2))) + assert TensorProduct(JzKet(j1, m1), JxKet(j2, m2)).rewrite('Jz') == \ + TensorProduct(JzKet(j1, m1), Sum( + WignerD(j2, mi, m2, 0, pi/2, 0) * JzKet(j2, mi), (mi, -j2, j2))) + assert TensorProduct(JxKet(j1, m1), JyKet(j2, m2)).rewrite('Jx') == \ + TensorProduct(JxKet(j1, m1), Sum( + WignerD(j2, mi, m2, 0, 0, pi/2) * JxKet(j2, mi), (mi, -j2, j2))) + assert TensorProduct(JzKet(j1, m1), JyKet(j2, m2)).rewrite('Jz') == \ + TensorProduct(JzKet(j1, m1), Sum(WignerD( + j2, mi, m2, pi*Rational(3, 2), -pi/2, pi/2) * JzKet(j2, mi), (mi, -j2, j2))) + assert TensorProduct(JxKet(j1, m1), JzKet(j2, m2)).rewrite('Jx') == \ + TensorProduct(JxKet(j1, m1), Sum( + WignerD(j2, mi, m2, 0, pi*Rational(3, 2), 0) * JxKet(j2, mi), (mi, -j2, j2))) + assert TensorProduct(JyKet(j1, m1), JzKet(j2, m2)).rewrite('Jy') == \ + TensorProduct(JyKet(j1, m1), Sum(WignerD( + j2, mi, m2, pi*Rational(3, 2), pi/2, pi/2) * JyKet(j2, mi), (mi, -j2, j2))) + + +def test_rewrite_coupled_state(): + # Numerical + assert JyKetCoupled(0, 0, (S.Half, S.Half)).rewrite('Jx') == \ + JxKetCoupled(0, 0, (S.Half, S.Half)) + assert JyKetCoupled(1, 1, (S.Half, S.Half)).rewrite('Jx') == \ + -I*JxKetCoupled(1, 1, (S.Half, S.Half)) + assert JyKetCoupled(1, 0, (S.Half, S.Half)).rewrite('Jx') == \ + JxKetCoupled(1, 0, (S.Half, S.Half)) + assert JyKetCoupled(1, -1, (S.Half, S.Half)).rewrite('Jx') == \ + I*JxKetCoupled(1, -1, (S.Half, S.Half)) + assert JzKetCoupled(0, 0, (S.Half, S.Half)).rewrite('Jx') == \ + JxKetCoupled(0, 0, (S.Half, S.Half)) + assert JzKetCoupled(1, 1, (S.Half, S.Half)).rewrite('Jx') == \ + JxKetCoupled(1, 1, (S.Half, S.Half))/2 - sqrt(2)*JxKetCoupled(1, 0, ( + S.Half, S.Half))/2 + JxKetCoupled(1, -1, (S.Half, S.Half))/2 + assert JzKetCoupled(1, 0, (S.Half, S.Half)).rewrite('Jx') == \ + sqrt(2)*JxKetCoupled(1, 1, (S( + 1)/2, S.Half))/2 - sqrt(2)*JxKetCoupled(1, -1, (S.Half, S.Half))/2 + assert JzKetCoupled(1, -1, (S.Half, S.Half)).rewrite('Jx') == \ + JxKetCoupled(1, 1, (S.Half, S.Half))/2 + sqrt(2)*JxKetCoupled(1, 0, ( + S.Half, S.Half))/2 + JxKetCoupled(1, -1, (S.Half, S.Half))/2 + assert JxKetCoupled(0, 0, (S.Half, S.Half)).rewrite('Jy') == \ + JyKetCoupled(0, 0, (S.Half, S.Half)) + assert JxKetCoupled(1, 1, (S.Half, S.Half)).rewrite('Jy') == \ + I*JyKetCoupled(1, 1, (S.Half, S.Half)) + assert JxKetCoupled(1, 0, (S.Half, S.Half)).rewrite('Jy') == \ + JyKetCoupled(1, 0, (S.Half, S.Half)) + assert JxKetCoupled(1, -1, (S.Half, S.Half)).rewrite('Jy') == \ + -I*JyKetCoupled(1, -1, (S.Half, S.Half)) + assert JzKetCoupled(0, 0, (S.Half, S.Half)).rewrite('Jy') == \ + JyKetCoupled(0, 0, (S.Half, S.Half)) + assert JzKetCoupled(1, 1, (S.Half, S.Half)).rewrite('Jy') == \ + JyKetCoupled(1, 1, (S.Half, S.Half))/2 - I*sqrt(2)*JyKetCoupled(1, 0, ( + S.Half, S.Half))/2 - JyKetCoupled(1, -1, (S.Half, S.Half))/2 + assert JzKetCoupled(1, 0, (S.Half, S.Half)).rewrite('Jy') == \ + -I*sqrt(2)*JyKetCoupled(1, 1, (S.Half, S.Half))/2 - I*sqrt( + 2)*JyKetCoupled(1, -1, (S.Half, S.Half))/2 + assert JzKetCoupled(1, -1, (S.Half, S.Half)).rewrite('Jy') == \ + -JyKetCoupled(1, 1, (S.Half, S.Half))/2 - I*sqrt(2)*JyKetCoupled(1, 0, (S.Half, S.Half))/2 + JyKetCoupled(1, -1, (S.Half, S.Half))/2 + assert JxKetCoupled(0, 0, (S.Half, S.Half)).rewrite('Jz') == \ + JzKetCoupled(0, 0, (S.Half, S.Half)) + assert JxKetCoupled(1, 1, (S.Half, S.Half)).rewrite('Jz') == \ + JzKetCoupled(1, 1, (S.Half, S.Half))/2 + sqrt(2)*JzKetCoupled(1, 0, ( + S.Half, S.Half))/2 + JzKetCoupled(1, -1, (S.Half, S.Half))/2 + assert JxKetCoupled(1, 0, (S.Half, S.Half)).rewrite('Jz') == \ + -sqrt(2)*JzKetCoupled(1, 1, (S( + 1)/2, S.Half))/2 + sqrt(2)*JzKetCoupled(1, -1, (S.Half, S.Half))/2 + assert JxKetCoupled(1, -1, (S.Half, S.Half)).rewrite('Jz') == \ + JzKetCoupled(1, 1, (S.Half, S.Half))/2 - sqrt(2)*JzKetCoupled(1, 0, ( + S.Half, S.Half))/2 + JzKetCoupled(1, -1, (S.Half, S.Half))/2 + assert JyKetCoupled(0, 0, (S.Half, S.Half)).rewrite('Jz') == \ + JzKetCoupled(0, 0, (S.Half, S.Half)) + assert JyKetCoupled(1, 1, (S.Half, S.Half)).rewrite('Jz') == \ + JzKetCoupled(1, 1, (S.Half, S.Half))/2 + I*sqrt(2)*JzKetCoupled(1, 0, ( + S.Half, S.Half))/2 - JzKetCoupled(1, -1, (S.Half, S.Half))/2 + assert JyKetCoupled(1, 0, (S.Half, S.Half)).rewrite('Jz') == \ + I*sqrt(2)*JzKetCoupled(1, 1, (S.Half, S.Half))/2 + I*sqrt( + 2)*JzKetCoupled(1, -1, (S.Half, S.Half))/2 + assert JyKetCoupled(1, -1, (S.Half, S.Half)).rewrite('Jz') == \ + -JzKetCoupled(1, 1, (S.Half, S.Half))/2 + I*sqrt(2)*JzKetCoupled(1, 0, (S.Half, S.Half))/2 + JzKetCoupled(1, -1, (S.Half, S.Half))/2 + # Symbolic + assert JyKetCoupled(j, m, (j1, j2)).rewrite('Jx') == \ + Sum(WignerD(j, mi, m, 0, 0, pi/2) * JxKetCoupled(j, mi, ( + j1, j2)), (mi, -j, j)) + assert JzKetCoupled(j, m, (j1, j2)).rewrite('Jx') == \ + Sum(WignerD(j, mi, m, 0, pi*Rational(3, 2), 0) * JxKetCoupled(j, mi, ( + j1, j2)), (mi, -j, j)) + assert JxKetCoupled(j, m, (j1, j2)).rewrite('Jy') == \ + Sum(WignerD(j, mi, m, pi*Rational(3, 2), 0, 0) * JyKetCoupled(j, mi, ( + j1, j2)), (mi, -j, j)) + assert JzKetCoupled(j, m, (j1, j2)).rewrite('Jy') == \ + Sum(WignerD(j, mi, m, pi*Rational(3, 2), pi/2, pi/2) * JyKetCoupled(j, + mi, (j1, j2)), (mi, -j, j)) + assert JxKetCoupled(j, m, (j1, j2)).rewrite('Jz') == \ + Sum(WignerD(j, mi, m, 0, pi/2, 0) * JzKetCoupled(j, mi, ( + j1, j2)), (mi, -j, j)) + assert JyKetCoupled(j, m, (j1, j2)).rewrite('Jz') == \ + Sum(WignerD(j, mi, m, pi*Rational(3, 2), -pi/2, pi/2) * JzKetCoupled( + j, mi, (j1, j2)), (mi, -j, j)) + + +def test_innerproducts_of_rewritten_states(): + # Numerical + assert qapply(JxBra(1, 1)*JxKet(1, 1).rewrite('Jy')).doit() == 1 + assert qapply(JxBra(1, 0)*JxKet(1, 0).rewrite('Jy')).doit() == 1 + assert qapply(JxBra(1, -1)*JxKet(1, -1).rewrite('Jy')).doit() == 1 + assert qapply(JxBra(1, 1)*JxKet(1, 1).rewrite('Jz')).doit() == 1 + assert qapply(JxBra(1, 0)*JxKet(1, 0).rewrite('Jz')).doit() == 1 + assert qapply(JxBra(1, -1)*JxKet(1, -1).rewrite('Jz')).doit() == 1 + assert qapply(JyBra(1, 1)*JyKet(1, 1).rewrite('Jx')).doit() == 1 + assert qapply(JyBra(1, 0)*JyKet(1, 0).rewrite('Jx')).doit() == 1 + assert qapply(JyBra(1, -1)*JyKet(1, -1).rewrite('Jx')).doit() == 1 + assert qapply(JyBra(1, 1)*JyKet(1, 1).rewrite('Jz')).doit() == 1 + assert qapply(JyBra(1, 0)*JyKet(1, 0).rewrite('Jz')).doit() == 1 + assert qapply(JyBra(1, -1)*JyKet(1, -1).rewrite('Jz')).doit() == 1 + assert qapply(JyBra(1, 1)*JyKet(1, 1).rewrite('Jz')).doit() == 1 + assert qapply(JyBra(1, 0)*JyKet(1, 0).rewrite('Jz')).doit() == 1 + assert qapply(JyBra(1, -1)*JyKet(1, -1).rewrite('Jz')).doit() == 1 + assert qapply(JzBra(1, 1)*JzKet(1, 1).rewrite('Jy')).doit() == 1 + assert qapply(JzBra(1, 0)*JzKet(1, 0).rewrite('Jy')).doit() == 1 + assert qapply(JzBra(1, -1)*JzKet(1, -1).rewrite('Jy')).doit() == 1 + assert qapply(JxBra(1, 1)*JxKet(1, 0).rewrite('Jy')).doit() == 0 + assert qapply(JxBra(1, 1)*JxKet(1, -1).rewrite('Jy')) == 0 + assert qapply(JxBra(1, 1)*JxKet(1, 0).rewrite('Jz')).doit() == 0 + assert qapply(JxBra(1, 1)*JxKet(1, -1).rewrite('Jz')) == 0 + assert qapply(JyBra(1, 1)*JyKet(1, 0).rewrite('Jx')).doit() == 0 + assert qapply(JyBra(1, 1)*JyKet(1, -1).rewrite('Jx')) == 0 + assert qapply(JyBra(1, 1)*JyKet(1, 0).rewrite('Jz')).doit() == 0 + assert qapply(JyBra(1, 1)*JyKet(1, -1).rewrite('Jz')) == 0 + assert qapply(JzBra(1, 1)*JzKet(1, 0).rewrite('Jx')).doit() == 0 + assert qapply(JzBra(1, 1)*JzKet(1, -1).rewrite('Jx')) == 0 + assert qapply(JzBra(1, 1)*JzKet(1, 0).rewrite('Jy')).doit() == 0 + assert qapply(JzBra(1, 1)*JzKet(1, -1).rewrite('Jy')) == 0 + assert qapply(JxBra(1, 0)*JxKet(1, 1).rewrite('Jy')) == 0 + assert qapply(JxBra(1, 0)*JxKet(1, -1).rewrite('Jy')) == 0 + assert qapply(JxBra(1, 0)*JxKet(1, 1).rewrite('Jz')) == 0 + assert qapply(JxBra(1, 0)*JxKet(1, -1).rewrite('Jz')) == 0 + assert qapply(JyBra(1, 0)*JyKet(1, 1).rewrite('Jx')) == 0 + assert qapply(JyBra(1, 0)*JyKet(1, -1).rewrite('Jx')) == 0 + assert qapply(JyBra(1, 0)*JyKet(1, 1).rewrite('Jz')) == 0 + assert qapply(JyBra(1, 0)*JyKet(1, -1).rewrite('Jz')) == 0 + assert qapply(JzBra(1, 0)*JzKet(1, 1).rewrite('Jx')) == 0 + assert qapply(JzBra(1, 0)*JzKet(1, -1).rewrite('Jx')) == 0 + assert qapply(JzBra(1, 0)*JzKet(1, 1).rewrite('Jy')) == 0 + assert qapply(JzBra(1, 0)*JzKet(1, -1).rewrite('Jy')) == 0 + assert qapply(JxBra(1, -1)*JxKet(1, 1).rewrite('Jy')) == 0 + assert qapply(JxBra(1, -1)*JxKet(1, 0).rewrite('Jy')).doit() == 0 + assert qapply(JxBra(1, -1)*JxKet(1, 1).rewrite('Jz')) == 0 + assert qapply(JxBra(1, -1)*JxKet(1, 0).rewrite('Jz')).doit() == 0 + assert qapply(JyBra(1, -1)*JyKet(1, 1).rewrite('Jx')) == 0 + assert qapply(JyBra(1, -1)*JyKet(1, 0).rewrite('Jx')).doit() == 0 + assert qapply(JyBra(1, -1)*JyKet(1, 1).rewrite('Jz')) == 0 + assert qapply(JyBra(1, -1)*JyKet(1, 0).rewrite('Jz')).doit() == 0 + assert qapply(JzBra(1, -1)*JzKet(1, 1).rewrite('Jx')) == 0 + assert qapply(JzBra(1, -1)*JzKet(1, 0).rewrite('Jx')).doit() == 0 + assert qapply(JzBra(1, -1)*JzKet(1, 1).rewrite('Jy')) == 0 + assert qapply(JzBra(1, -1)*JzKet(1, 0).rewrite('Jy')).doit() == 0 + + +def test_uncouple_2_coupled_states(): + # j1=1/2, j2=1/2 + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( + TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( + TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) ))) + # j1=1/2, j2=1 + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1)) == \ + expand(uncouple( + couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0)) == \ + expand(uncouple( + couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1)) == \ + expand(uncouple( + couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1)) == \ + expand(uncouple( + couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0)) == \ + expand(uncouple( + couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1)) == \ + expand(uncouple( + couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1)) ))) + # j1=1, j2=1 + assert TensorProduct(JzKet(1, 1), JzKet(1, 1)) == \ + expand(uncouple(couple( TensorProduct(JzKet(1, 1), JzKet(1, 1)) ))) + assert TensorProduct(JzKet(1, 1), JzKet(1, 0)) == \ + expand(uncouple(couple( TensorProduct(JzKet(1, 1), JzKet(1, 0)) ))) + assert TensorProduct(JzKet(1, 1), JzKet(1, -1)) == \ + expand(uncouple(couple( TensorProduct(JzKet(1, 1), JzKet(1, -1)) ))) + assert TensorProduct(JzKet(1, 0), JzKet(1, 1)) == \ + expand(uncouple(couple( TensorProduct(JzKet(1, 0), JzKet(1, 1)) ))) + assert TensorProduct(JzKet(1, 0), JzKet(1, 0)) == \ + expand(uncouple(couple( TensorProduct(JzKet(1, 0), JzKet(1, 0)) ))) + assert TensorProduct(JzKet(1, 0), JzKet(1, -1)) == \ + expand(uncouple(couple( TensorProduct(JzKet(1, 0), JzKet(1, -1)) ))) + assert TensorProduct(JzKet(1, -1), JzKet(1, 1)) == \ + expand(uncouple(couple( TensorProduct(JzKet(1, -1), JzKet(1, 1)) ))) + assert TensorProduct(JzKet(1, -1), JzKet(1, 0)) == \ + expand(uncouple(couple( TensorProduct(JzKet(1, -1), JzKet(1, 0)) ))) + assert TensorProduct(JzKet(1, -1), JzKet(1, -1)) == \ + expand(uncouple(couple( TensorProduct(JzKet(1, -1), JzKet(1, -1)) ))) + + +def test_uncouple_3_coupled_states(): + # Default coupling + # j1=1/2, j2=1/2, j3=1/2 + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet( + S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S( + 1)/2, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S( + 1)/2, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S( + 1)/2, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.NegativeOne/ + 2), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) ))) + # j1=1/2, j2=1, j3=1/2 + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct( + JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct( + JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct( + JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct( + JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct( + JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct( + JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct( + JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct( + JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct( + JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct( + JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct( + JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct( + JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) ))) + # Coupling j1+j3=j13, j13+j2=j + # j1=1/2, j2=1/2, j3=1/2 + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet( + S.Half, S.Half), JzKet(S.Half, S.Half)), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet( + S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet( + S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet( + S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet( + S.Half, S.Half), JzKet(S.Half, S.Half)), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet( + S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet( + S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet( + S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (1, 2)) ))) + # j1=1/2, j2=1, j3=1/2 + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S( + 1)/2), JzKet(1, 1), JzKet(S.Half, S.Half)), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S( + 1)/2), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S( + 1)/2), JzKet(1, 0), JzKet(S.Half, S.Half)), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S( + 1)/2), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S( + 1)/2), JzKet(1, -1), JzKet(S.Half, S.Half)), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S( + 1)/2), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S( + -1)/2), JzKet(1, 1), JzKet(S.Half, S.Half)), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S( + -1)/2), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S( + -1)/2), JzKet(1, 0), JzKet(S.Half, S.Half)), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S( + -1)/2), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S( + -1)/2), JzKet(1, -1), JzKet(S.Half, S.Half)), ((1, 3), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.NegativeOne/ + 2), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (1, 2)) ))) + + +@slow +def test_uncouple_4_coupled_states(): + # j1=1/2, j2=1/2, j3=1/2, j4=1/2 + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet( + S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S( + 1)/2, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S( + 1)/2, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S( + 1)/2, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S( + 1)/2, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S( + 1)/2, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet( + S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S( + 1)/2, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S( + 1)/2, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S( + 1)/2, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S( + 1)/2, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S( + 1)/2, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) ))) + # j1=1/2, j2=1/2, j3=1, j4=1/2 + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), + JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), + JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), + JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), + JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), + JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet( + S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), + JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet( + S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), + JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet( + S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet( + S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet( + S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), + JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), + JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), + JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), + JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), + JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet( + S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), + JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet( + S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), + JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet( + S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet( + S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, S.Half)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet( + S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) ))) + # Couple j1+j3=j13, j2+j4=j24, j13+j24=j + # j1=1/2, j2=1/2, j3=1/2, j4=1/2 + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + # j1=1/2, j2=1/2, j3=1, j4=1/2 + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, S.Half)) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, S.Half)), ((1, 3), (2, 4), (1, 2)) ))) + assert TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) == \ + expand(uncouple(couple( TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (2, 4), (1, 2)) ))) + + +def test_uncouple_2_coupled_states_numerical(): + # j1=1/2, j2=1/2 + assert uncouple(JzKetCoupled(0, 0, (S.Half, S.Half))) == \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)))/2 - \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half))/2 + assert uncouple(JzKetCoupled(1, 1, (S.Half, S.Half))) == \ + TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) + assert uncouple(JzKetCoupled(1, 0, (S.Half, S.Half))) == \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)))/2 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half))/2 + assert uncouple(JzKetCoupled(1, -1, (S.Half, S.Half))) == \ + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) + # j1=1, j2=1/2 + assert uncouple(JzKetCoupled(S.Half, S.Half, (1, S.Half))) == \ + -sqrt(3)*TensorProduct(JzKet(1, 0), JzKet(S.Half, S.Half))/3 + \ + sqrt(6)*TensorProduct(JzKet(1, 1), JzKet(S.Half, Rational(-1, 2)))/3 + assert uncouple(JzKetCoupled(S.Half, Rational(-1, 2), (1, S.Half))) == \ + sqrt(3)*TensorProduct(JzKet(1, 0), JzKet(S.Half, Rational(-1, 2)))/3 - \ + sqrt(6)*TensorProduct(JzKet(1, -1), JzKet(S.Half, S.Half))/3 + assert uncouple(JzKetCoupled(Rational(3, 2), Rational(3, 2), (1, S.Half))) == \ + TensorProduct(JzKet(1, 1), JzKet(S.Half, S.Half)) + assert uncouple(JzKetCoupled(Rational(3, 2), S.Half, (1, S.Half))) == \ + sqrt(3)*TensorProduct(JzKet(1, 1), JzKet(S.Half, Rational(-1, 2)))/3 + \ + sqrt(6)*TensorProduct(JzKet(1, 0), JzKet(S.Half, S.Half))/3 + assert uncouple(JzKetCoupled(Rational(3, 2), Rational(-1, 2), (1, S.Half))) == \ + sqrt(6)*TensorProduct(JzKet(1, 0), JzKet(S.Half, Rational(-1, 2)))/3 + \ + sqrt(3)*TensorProduct(JzKet(1, -1), JzKet(S.Half, S.Half))/3 + assert uncouple(JzKetCoupled(Rational(3, 2), Rational(-3, 2), (1, S.Half))) == \ + TensorProduct(JzKet(1, -1), JzKet(S.Half, Rational(-1, 2))) + # j1=1, j2=1 + assert uncouple(JzKetCoupled(0, 0, (1, 1))) == \ + sqrt(3)*TensorProduct(JzKet(1, 1), JzKet(1, -1))/3 - \ + sqrt(3)*TensorProduct(JzKet(1, 0), JzKet(1, 0))/3 + \ + sqrt(3)*TensorProduct(JzKet(1, -1), JzKet(1, 1))/3 + assert uncouple(JzKetCoupled(1, 1, (1, 1))) == \ + sqrt(2)*TensorProduct(JzKet(1, 1), JzKet(1, 0))/2 - \ + sqrt(2)*TensorProduct(JzKet(1, 0), JzKet(1, 1))/2 + assert uncouple(JzKetCoupled(1, 0, (1, 1))) == \ + sqrt(2)*TensorProduct(JzKet(1, 1), JzKet(1, -1))/2 - \ + sqrt(2)*TensorProduct(JzKet(1, -1), JzKet(1, 1))/2 + assert uncouple(JzKetCoupled(1, -1, (1, 1))) == \ + sqrt(2)*TensorProduct(JzKet(1, 0), JzKet(1, -1))/2 - \ + sqrt(2)*TensorProduct(JzKet(1, -1), JzKet(1, 0))/2 + assert uncouple(JzKetCoupled(2, 2, (1, 1))) == \ + TensorProduct(JzKet(1, 1), JzKet(1, 1)) + assert uncouple(JzKetCoupled(2, 1, (1, 1))) == \ + sqrt(2)*TensorProduct(JzKet(1, 1), JzKet(1, 0))/2 + \ + sqrt(2)*TensorProduct(JzKet(1, 0), JzKet(1, 1))/2 + assert uncouple(JzKetCoupled(2, 0, (1, 1))) == \ + sqrt(6)*TensorProduct(JzKet(1, 1), JzKet(1, -1))/6 + \ + sqrt(6)*TensorProduct(JzKet(1, 0), JzKet(1, 0))/3 + \ + sqrt(6)*TensorProduct(JzKet(1, -1), JzKet(1, 1))/6 + assert uncouple(JzKetCoupled(2, -1, (1, 1))) == \ + sqrt(2)*TensorProduct(JzKet(1, 0), JzKet(1, -1))/2 + \ + sqrt(2)*TensorProduct(JzKet(1, -1), JzKet(1, 0))/2 + assert uncouple(JzKetCoupled(2, -2, (1, 1))) == \ + TensorProduct(JzKet(1, -1), JzKet(1, -1)) + + +def test_uncouple_3_coupled_states_numerical(): + # Default coupling + # j1=1/2, j2=1/2, j3=1/2 + assert uncouple(JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half))) == \ + TensorProduct(JzKet( + S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)) + assert uncouple(JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half))) == \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half))/3 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half))/3 + \ + sqrt(3)*TensorProduct(JzKet( + S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)))/3 + assert uncouple(JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half))) == \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half))/3 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)))/3 + \ + sqrt(3)*TensorProduct(JzKet( + S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)))/3 + assert uncouple(JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half))) == \ + TensorProduct(JzKet( + S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))) + # j1=1/2, j2=1/2, j3=1 + assert uncouple(JzKetCoupled(2, 2, (S.Half, S.Half, 1))) == \ + TensorProduct( + JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1)) + assert uncouple(JzKetCoupled(2, 1, (S.Half, S.Half, 1))) == \ + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1))/2 + \ + TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1))/2 + \ + sqrt(2)*TensorProduct( + JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0))/2 + assert uncouple(JzKetCoupled(2, 0, (S.Half, S.Half, 1))) == \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1))/6 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0))/3 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0))/3 + \ + sqrt(6)*TensorProduct( + JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1))/6 + assert uncouple(JzKetCoupled(2, -1, (S.Half, S.Half, 1))) == \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0))/2 + \ + TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1))/2 + \ + TensorProduct( + JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1))/2 + assert uncouple(JzKetCoupled(2, -2, (S.Half, S.Half, 1))) == \ + TensorProduct( + JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1)) + assert uncouple(JzKetCoupled(1, 1, (S.Half, S.Half, 1))) == \ + -TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1))/2 - \ + TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1))/2 + \ + sqrt(2)*TensorProduct( + JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0))/2 + assert uncouple(JzKetCoupled(1, 0, (S.Half, S.Half, 1))) == \ + -sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1))/2 + \ + sqrt(2)*TensorProduct( + JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1))/2 + assert uncouple(JzKetCoupled(1, -1, (S.Half, S.Half, 1))) == \ + -sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0))/2 + \ + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1))/2 + \ + TensorProduct( + JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1))/2 + # j1=1/2, j2=1, j3=1 + assert uncouple(JzKetCoupled(Rational(5, 2), Rational(5, 2), (S.Half, 1, 1))) == \ + TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 1)) + assert uncouple(JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, 1, 1))) == \ + sqrt(5)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1))/5 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/5 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), + JzKet(1, 0))/5 + assert uncouple(JzKetCoupled(Rational(5, 2), S.Half, (S.Half, 1, 1))) == \ + sqrt(5)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/5 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/5 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/10 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))/5 + \ + sqrt(10)*TensorProduct( + JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, -1))/10 + assert uncouple(JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, 1, 1))) == \ + sqrt(10)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/10 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))/5 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/10 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/5 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), + JzKet(1, -1))/5 + assert uncouple(JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, 1, 1))) == \ + sqrt(10)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/5 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/5 + \ + sqrt(5)*TensorProduct( + JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, -1))/5 + assert uncouple(JzKetCoupled(Rational(5, 2), Rational(-5, 2), (S.Half, 1, 1))) == \ + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, -1)) + assert uncouple(JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, 1, 1))) == \ + -sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1))/15 - \ + 2*sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/15 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), + JzKet(1, 0))/5 + assert uncouple(JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1))) == \ + -4*sqrt(5)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/15 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/15 - \ + 2*sqrt(10)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/15 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))/15 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), + JzKet(1, -1))/5 + assert uncouple(JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1))) == \ + -sqrt(10)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/5 - \ + sqrt(10)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))/15 + \ + 2*sqrt(10)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/15 - \ + sqrt(5)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/15 + \ + 4*sqrt(5)*TensorProduct( + JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/15 + assert uncouple(JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, 1, 1))) == \ + -sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/5 + \ + 2*sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/15 + \ + sqrt(30)*TensorProduct( + JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, -1))/15 + assert uncouple(JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1))) == \ + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/3 - \ + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/3 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/6 - \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))/3 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), + JzKet(1, -1))/2 + assert uncouple(JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1))) == \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/2 - \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))/3 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/6 - \ + TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/3 + \ + TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/3 + # j1=1, j2=1, j3=1 + assert uncouple(JzKetCoupled(3, 3, (1, 1, 1))) == \ + TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, 1)) + assert uncouple(JzKetCoupled(3, 2, (1, 1, 1))) == \ + sqrt(3)*TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, 1))/3 + \ + sqrt(3)*TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, 1))/3 + \ + sqrt(3)*TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, 0))/3 + assert uncouple(JzKetCoupled(3, 1, (1, 1, 1))) == \ + sqrt(15)*TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, 1))/15 + \ + 2*sqrt(15)*TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, 1))/15 + \ + 2*sqrt(15)*TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, 1))/15 + \ + 2*sqrt(15)*TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, -1))/15 + assert uncouple(JzKetCoupled(3, 0, (1, 1, 1))) == \ + sqrt(10)*TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, 1))/10 + \ + sqrt(10)*TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, 0))/10 + \ + sqrt(10)*TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, 1))/10 + \ + sqrt(10)*TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, 0))/5 + \ + sqrt(10)*TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, -1))/10 + \ + sqrt(10)*TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, 0))/10 + \ + sqrt(10)*TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, -1))/10 + assert uncouple(JzKetCoupled(3, -1, (1, 1, 1))) == \ + sqrt(15)*TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, 1))/15 + \ + 2*sqrt(15)*TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, -1))/15 + \ + 2*sqrt(15)*TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, 0))/15 + \ + 2*sqrt(15)*TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, -1))/15 + \ + sqrt(15)*TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, -1))/15 + assert uncouple(JzKetCoupled(3, -2, (1, 1, 1))) == \ + sqrt(3)*TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, 0))/3 + \ + sqrt(3)*TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, -1))/3 + \ + sqrt(3)*TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, -1))/3 + assert uncouple(JzKetCoupled(3, -3, (1, 1, 1))) == \ + TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, -1)) + assert uncouple(JzKetCoupled(2, 2, (1, 1, 1))) == \ + -sqrt(6)*TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, 1))/6 - \ + sqrt(6)*TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, 1))/6 + \ + sqrt(6)*TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, 0))/3 + assert uncouple(JzKetCoupled(2, 1, (1, 1, 1))) == \ + -sqrt(3)*TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, 1))/6 - \ + sqrt(3)*TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, 1))/3 + \ + sqrt(3)*TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, 0))/6 - \ + sqrt(3)*TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, 1))/6 + \ + sqrt(3)*TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, 0))/6 + \ + sqrt(3)*TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, -1))/3 + assert uncouple(JzKetCoupled(2, 0, (1, 1, 1))) == \ + -TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, 1))/2 - \ + TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, 1))/2 + \ + TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, -1))/2 + \ + TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, -1))/2 + assert uncouple(JzKetCoupled(2, -1, (1, 1, 1))) == \ + -sqrt(3)*TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, 1))/3 - \ + sqrt(3)*TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, 0))/6 + \ + sqrt(3)*TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, -1))/6 - \ + sqrt(3)*TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, 0))/6 + \ + sqrt(3)*TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, -1))/3 + \ + sqrt(3)*TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, -1))/6 + assert uncouple(JzKetCoupled(2, -2, (1, 1, 1))) == \ + -sqrt(6)*TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, 0))/3 + \ + sqrt(6)*TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, -1))/6 + \ + sqrt(6)*TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, -1))/6 + assert uncouple(JzKetCoupled(1, 1, (1, 1, 1))) == \ + sqrt(15)*TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, 1))/30 + \ + sqrt(15)*TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, 1))/15 - \ + sqrt(15)*TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, 0))/10 + \ + sqrt(15)*TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, 1))/30 - \ + sqrt(15)*TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, 0))/10 + \ + sqrt(15)*TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, -1))/5 + assert uncouple(JzKetCoupled(1, 0, (1, 1, 1))) == \ + sqrt(15)*TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, 1))/10 - \ + sqrt(15)*TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, 1))/10 - \ + 2*sqrt(15)*TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, -1))/10 - \ + sqrt(15)*TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, -1))/10 + assert uncouple(JzKetCoupled(1, -1, (1, 1, 1))) == \ + sqrt(15)*TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, 1))/5 - \ + sqrt(15)*TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, 0))/10 + \ + sqrt(15)*TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, -1))/30 - \ + sqrt(15)*TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, 0))/10 + \ + sqrt(15)*TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, -1))/15 + \ + sqrt(15)*TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, -1))/30 + # Defined j13 + # j1=1/2, j2=1/2, j3=1, j13=1/2 + assert uncouple(JzKetCoupled(1, 1, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 1)) )) == \ + -sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1))/3 + \ + sqrt(3)*TensorProduct( + JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0))/3 + assert uncouple(JzKetCoupled(1, 0, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 1)) )) == \ + -sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1))/3 - \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0))/6 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0))/6 + \ + sqrt(3)*TensorProduct( + JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1))/3 + assert uncouple(JzKetCoupled(1, -1, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 1)) )) == \ + -sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0))/3 + \ + sqrt(6)*TensorProduct( + JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1))/3 + # j1=1/2, j2=1, j3=1, j13=1/2 + assert uncouple(JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))))) == \ + -sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1))/3 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), + JzKet(1, 0))/3 + assert uncouple(JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))))) == \ + -2*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/3 - \ + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/3 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))/3 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), + JzKet(1, -1))/3 + assert uncouple(JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))))) == \ + -sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/3 - \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))/3 + \ + TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/3 + \ + 2*TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/3 + assert uncouple(JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))))) == \ + -sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/3 + \ + sqrt(6)*TensorProduct( + JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, -1))/3 + # j1=1, j2=1, j3=1, j13=1 + assert uncouple(JzKetCoupled(2, 2, (1, 1, 1), ((1, 3, 1), (1, 2, 2)))) == \ + -sqrt(2)*TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, 1))/2 + \ + sqrt(2)*TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, 0))/2 + assert uncouple(JzKetCoupled(2, 1, (1, 1, 1), ((1, 3, 1), (1, 2, 2)))) == \ + -TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, 1))/2 - \ + TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, 1))/2 + \ + TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, 0))/2 + \ + TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, -1))/2 + assert uncouple(JzKetCoupled(2, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 2)))) == \ + -sqrt(3)*TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, 1))/3 - \ + sqrt(3)*TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, 0))/6 - \ + sqrt(3)*TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, 1))/6 + \ + sqrt(3)*TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, -1))/6 + \ + sqrt(3)*TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, 0))/6 + \ + sqrt(3)*TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, -1))/3 + assert uncouple(JzKetCoupled(2, -1, (1, 1, 1), ((1, 3, 1), (1, 2, 2)))) == \ + -TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, 1))/2 - \ + TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, 0))/2 + \ + TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, -1))/2 + \ + TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, -1))/2 + assert uncouple(JzKetCoupled(2, -2, (1, 1, 1), ((1, 3, 1), (1, 2, 2)))) == \ + -sqrt(2)*TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, 0))/2 + \ + sqrt(2)*TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, -1))/2 + assert uncouple(JzKetCoupled(1, 1, (1, 1, 1), ((1, 3, 1), (1, 2, 1)))) == \ + TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, 1))/2 - \ + TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, 1))/2 + \ + TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, 0))/2 - \ + TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, -1))/2 + assert uncouple(JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 1)))) == \ + TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, 0))/2 - \ + TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, 1))/2 - \ + TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, -1))/2 + \ + TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, 0))/2 + assert uncouple(JzKetCoupled(1, -1, (1, 1, 1), ((1, 3, 1), (1, 2, 1)))) == \ + -TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, 1))/2 + \ + TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, 0))/2 - \ + TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, -1))/2 + \ + TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, -1))/2 + + +def test_uncouple_4_coupled_states_numerical(): + # j1=1/2, j2=1/2, j3=1, j4=1, default coupling + assert uncouple(JzKetCoupled(3, 3, (S.Half, S.Half, 1, 1))) == \ + TensorProduct(JzKet( + S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 1)) + assert uncouple(JzKetCoupled(3, 2, (S.Half, S.Half, 1, 1))) == \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 1))/6 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1))/6 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/3 + \ + sqrt(3)*TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0))/3 + assert uncouple(JzKetCoupled(3, 1, (S.Half, S.Half, 1, 1))) == \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/15 + \ + 2*sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + S.Half), JzKet(1, 1), JzKet(1, -1))/15 + assert uncouple(JzKetCoupled(3, 0, (S.Half, S.Half, 1, 1))) == \ + sqrt(10)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/10 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/10 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/10 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))/5 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, -1))/10 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/10 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))/5 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/10 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/10 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + S.Half), JzKet(1, 0), JzKet(1, -1))/10 + assert uncouple(JzKetCoupled(3, -1, (S.Half, S.Half, 1, 1))) == \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/15 + \ + 2*sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/15 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + S.Half), JzKet(1, -1), JzKet(1, -1))/15 + assert uncouple(JzKetCoupled(3, -2, (S.Half, S.Half, 1, 1))) == \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/3 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/3 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, -1))/6 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + Rational(-1, 2)), JzKet(1, -1), JzKet(1, -1))/6 + assert uncouple(JzKetCoupled(3, -3, (S.Half, S.Half, 1, 1))) == \ + TensorProduct(JzKet(S.Half, -S( + 1)/2), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, -1)) + assert uncouple(JzKetCoupled(2, 2, (S.Half, S.Half, 1, 1))) == \ + -sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 1))/6 - \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1))/6 - \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/6 + \ + sqrt(6)*TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0))/3 + assert uncouple(JzKetCoupled(2, 1, (S.Half, S.Half, 1, 1))) == \ + -sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1))/6 - \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/6 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0))/12 - \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/6 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/12 - \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/6 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))/6 + \ + sqrt(3)*TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, -1))/3 + assert uncouple(JzKetCoupled(2, 0, (S.Half, S.Half, 1, 1))) == \ + -TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/2 - \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/4 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, -1))/4 - \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/4 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/4 + \ + TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/2 + assert uncouple(JzKetCoupled(2, -1, (S.Half, S.Half, 1, 1))) == \ + -sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/3 - \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))/6 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/6 - \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/12 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/6 - \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/12 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/6 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + S.Half), JzKet(1, -1), JzKet(1, -1))/6 + assert uncouple(JzKetCoupled(2, -2, (S.Half, S.Half, 1, 1))) == \ + -sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/3 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/6 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, -1))/6 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + Rational(-1, 2)), JzKet(1, -1), JzKet(1, -1))/6 + assert uncouple(JzKetCoupled(1, 1, (S.Half, S.Half, 1, 1))) == \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1))/30 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/30 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0))/20 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/30 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/20 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/30 - \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))/10 + \ + sqrt(15)*TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, -1))/5 + assert uncouple(JzKetCoupled(1, 0, (S.Half, S.Half, 1, 1))) == \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/10 - \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/20 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, -1))/20 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/20 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/20 - \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + S.Half), JzKet(1, 0), JzKet(1, -1))/10 + assert uncouple(JzKetCoupled(1, -1, (S.Half, S.Half, 1, 1))) == \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/5 - \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))/10 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/30 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/20 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/30 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/20 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/30 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + S.Half), JzKet(1, -1), JzKet(1, -1))/30 + # j1=1/2, j2=1/2, j3=1, j4=1, j12=1, j34=1 + assert uncouple(JzKetCoupled(2, 2, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 1), (1, 3, 2)))) == \ + -sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/2 + \ + sqrt(2)*TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0))/2 + assert uncouple(JzKetCoupled(2, 1, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 1), (1, 3, 2)))) == \ + -sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/4 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0))/4 - \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/4 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/4 - \ + TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/2 + \ + TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, -1))/2 + assert uncouple(JzKetCoupled(2, 0, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 1), (1, 3, 2)))) == \ + -sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/6 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/6 - \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/6 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, -1))/6 - \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/6 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/6 - \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/6 + \ + sqrt(3)*TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/6 + assert uncouple(JzKetCoupled(2, -1, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 1), (1, 3, 2)))) == \ + -TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/2 + \ + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/2 - \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/4 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/4 - \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/4 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/4 + assert uncouple(JzKetCoupled(2, -2, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 1), (1, 3, 2)))) == \ + -sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/2 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, + Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/2 + assert uncouple(JzKetCoupled(1, 1, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 1), (1, 3, 1)))) == \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/4 - \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0))/4 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/4 - \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/4 - \ + TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/2 + \ + TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, -1))/2 + assert uncouple(JzKetCoupled(1, 0, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 1), (1, 3, 1)))) == \ + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/2 - \ + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/2 - \ + TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/2 + \ + TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/2 + assert uncouple(JzKetCoupled(1, -1, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 1), (1, 3, 1)))) == \ + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/2 - \ + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/2 - \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/4 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/4 - \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/4 + \ + sqrt(2)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/4 + # j1=1/2, j2=1/2, j3=1, j4=1, j12=1, j34=2 + assert uncouple(JzKetCoupled(3, 3, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 3)))) == \ + TensorProduct(JzKet( + S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 1)) + assert uncouple(JzKetCoupled(3, 2, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 3)))) == \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 1))/6 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1))/6 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/3 + \ + sqrt(3)*TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0))/3 + assert uncouple(JzKetCoupled(3, 1, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 3)))) == \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/15 + \ + 2*sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + S.Half), JzKet(1, 1), JzKet(1, -1))/15 + assert uncouple(JzKetCoupled(3, 0, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 3)))) == \ + sqrt(10)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/10 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/10 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/10 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))/5 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, -1))/10 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/10 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))/5 + \ + sqrt(5)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/10 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/10 + \ + sqrt(10)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + S.Half), JzKet(1, 0), JzKet(1, -1))/10 + assert uncouple(JzKetCoupled(3, -1, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 3)))) == \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/15 + \ + 2*sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/15 + \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/15 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + S.Half), JzKet(1, -1), JzKet(1, -1))/15 + assert uncouple(JzKetCoupled(3, -2, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 3)))) == \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/3 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/3 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, -1))/6 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + Rational(-1, 2)), JzKet(1, -1), JzKet(1, -1))/6 + assert uncouple(JzKetCoupled(3, -3, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 3)))) == \ + TensorProduct(JzKet(S.Half, -S( + 1)/2), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, -1)) + assert uncouple(JzKetCoupled(2, 2, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 2)))) == \ + -sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 1))/3 - \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1))/3 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/6 + \ + sqrt(6)*TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0))/6 + assert uncouple(JzKetCoupled(2, 1, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 2)))) == \ + -sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1))/3 - \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/12 - \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0))/12 - \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/12 - \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/12 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/6 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))/3 + \ + sqrt(3)*TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, -1))/6 + assert uncouple(JzKetCoupled(2, 0, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 2)))) == \ + -TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/2 - \ + TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/2 + \ + TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/2 + \ + TensorProduct(JzKet(S( + 1)/2, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/2 + assert uncouple(JzKetCoupled(2, -1, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 2)))) == \ + -sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/6 - \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))/3 - \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/6 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/12 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/12 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/12 + \ + sqrt(6)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/12 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + S.Half), JzKet(1, -1), JzKet(1, -1))/3 + assert uncouple(JzKetCoupled(2, -2, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 2)))) == \ + -sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/6 - \ + sqrt(6)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/6 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, -1))/3 + \ + sqrt(3)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + Rational(-1, 2)), JzKet(1, -1), JzKet(1, -1))/3 + assert uncouple(JzKetCoupled(1, 1, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 1)))) == \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1))/5 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))/20 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0))/20 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/20 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/20 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/30 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + S.Half), JzKet(1, 1), JzKet(1, -1))/30 + assert uncouple(JzKetCoupled(1, 0, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 1)))) == \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))/10 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))/10 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))/30 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))/15 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, -1))/30 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/30 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))/15 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/30 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/10 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + S.Half), JzKet(1, 0), JzKet(1, -1))/10 + assert uncouple(JzKetCoupled(1, -1, (S.Half, S.Half, 1, 1), ((1, 2, 1), (3, 4, 2), (1, 3, 1)))) == \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))/30 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))/15 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))/30 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))/20 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))/20 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))/20 - \ + sqrt(30)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))/20 + \ + sqrt(15)*TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, + S.Half), JzKet(1, -1), JzKet(1, -1))/5 + + +def test_uncouple_symbolic(): + assert uncouple(JzKetCoupled(j, m, (j1, j2) )) == \ + Sum(CG(j1, m1, j2, m2, j, m) * + TensorProduct(JzKet(j1, m1), JzKet(j2, m2)), + (m1, -j1, j1), (m2, -j2, j2)) + assert uncouple(JzKetCoupled(j, m, (j1, j2, j3) )) == \ + Sum(CG(j1, m1, j2, m2, j1 + j2, m1 + m2) * CG(j1 + j2, m1 + m2, j3, m3, j, m) * + TensorProduct(JzKet(j1, m1), JzKet(j2, m2), JzKet(j3, m3)), + (m1, -j1, j1), (m2, -j2, j2), (m3, -j3, j3)) + assert uncouple(JzKetCoupled(j, m, (j1, j2, j3), ((1, 3, j13), (1, 2, j)) )) == \ + Sum(CG(j1, m1, j3, m3, j13, m1 + m3) * CG(j13, m1 + m3, j2, m2, j, m) * + TensorProduct(JzKet(j1, m1), JzKet(j2, m2), JzKet(j3, m3)), + (m1, -j1, j1), (m2, -j2, j2), (m3, -j3, j3)) + assert uncouple(JzKetCoupled(j, m, (j1, j2, j3, j4) )) == \ + Sum(CG(j1, m1, j2, m2, j1 + j2, m1 + m2) * CG(j1 + j2, m1 + m2, j3, m3, j1 + j2 + j3, m1 + m2 + m3) * CG(j1 + j2 + j3, m1 + m2 + m3, j4, m4, j, m) * + TensorProduct( + JzKet(j1, m1), JzKet(j2, m2), JzKet(j3, m3), JzKet(j4, m4)), + (m1, -j1, j1), (m2, -j2, j2), (m3, -j3, j3), (m4, -j4, j4)) + assert uncouple(JzKetCoupled(j, m, (j1, j2, j3, j4), ((1, 3, j13), (2, 4, j24), (1, 2, j)) )) == \ + Sum(CG(j1, m1, j3, m3, j13, m1 + m3) * CG(j2, m2, j4, m4, j24, m2 + m4) * CG(j13, m1 + m3, j24, m2 + m4, j, m) * + TensorProduct( + JzKet(j1, m1), JzKet(j2, m2), JzKet(j3, m3), JzKet(j4, m4)), + (m1, -j1, j1), (m2, -j2, j2), (m3, -j3, j3), (m4, -j4, j4)) + + +def test_couple_2_states(): + # j1=1/2, j2=1/2 + assert JzKetCoupled(0, 0, (S.Half, S.Half)) == \ + expand(couple(uncouple( JzKetCoupled(0, 0, (S.Half, S.Half)) ))) + assert JzKetCoupled(1, 1, (S.Half, S.Half)) == \ + expand(couple(uncouple( JzKetCoupled(1, 1, (S.Half, S.Half)) ))) + assert JzKetCoupled(1, 0, (S.Half, S.Half)) == \ + expand(couple(uncouple( JzKetCoupled(1, 0, (S.Half, S.Half)) ))) + assert JzKetCoupled(1, -1, (S.Half, S.Half)) == \ + expand(couple(uncouple( JzKetCoupled(1, -1, (S.Half, S.Half)) ))) + # j1=1, j2=1/2 + assert JzKetCoupled(S.Half, S.Half, (1, S.Half)) == \ + expand(couple(uncouple( JzKetCoupled(S.Half, S.Half, (1, S.Half)) ))) + assert JzKetCoupled(S.Half, Rational(-1, 2), (1, S.Half)) == \ + expand(couple(uncouple( JzKetCoupled(S.Half, Rational(-1, 2), (1, S.Half)) ))) + assert JzKetCoupled(Rational(3, 2), Rational(3, 2), (1, S.Half)) == \ + expand(couple(uncouple( JzKetCoupled(Rational(3, 2), Rational(3, 2), (1, S.Half)) ))) + assert JzKetCoupled(Rational(3, 2), S.Half, (1, S.Half)) == \ + expand(couple(uncouple( JzKetCoupled(Rational(3, 2), S.Half, (1, S.Half)) ))) + assert JzKetCoupled(Rational(3, 2), Rational(-1, 2), (1, S.Half)) == \ + expand(couple(uncouple( JzKetCoupled(Rational(3, 2), Rational(-1, 2), (1, S.Half)) ))) + assert JzKetCoupled(Rational(3, 2), Rational(-3, 2), (1, S.Half)) == \ + expand(couple(uncouple( JzKetCoupled(Rational(3, 2), Rational(-3, 2), (1, S.Half)) ))) + # j1=1, j2=1 + assert JzKetCoupled(0, 0, (1, 1)) == \ + expand(couple(uncouple( JzKetCoupled(0, 0, (1, 1)) ))) + assert JzKetCoupled(1, 1, (1, 1)) == \ + expand(couple(uncouple( JzKetCoupled(1, 1, (1, 1)) ))) + assert JzKetCoupled(1, 0, (1, 1)) == \ + expand(couple(uncouple( JzKetCoupled(1, 0, (1, 1)) ))) + assert JzKetCoupled(1, -1, (1, 1)) == \ + expand(couple(uncouple( JzKetCoupled(1, -1, (1, 1)) ))) + assert JzKetCoupled(2, 2, (1, 1)) == \ + expand(couple(uncouple( JzKetCoupled(2, 2, (1, 1)) ))) + assert JzKetCoupled(2, 1, (1, 1)) == \ + expand(couple(uncouple( JzKetCoupled(2, 1, (1, 1)) ))) + assert JzKetCoupled(2, 0, (1, 1)) == \ + expand(couple(uncouple( JzKetCoupled(2, 0, (1, 1)) ))) + assert JzKetCoupled(2, -1, (1, 1)) == \ + expand(couple(uncouple( JzKetCoupled(2, -1, (1, 1)) ))) + assert JzKetCoupled(2, -2, (1, 1)) == \ + expand(couple(uncouple( JzKetCoupled(2, -2, (1, 1)) ))) + # j1=1/2, j2=3/2 + assert JzKetCoupled(1, 1, (S.Half, Rational(3, 2))) == \ + expand(couple(uncouple( JzKetCoupled(1, 1, (S.Half, Rational(3, 2))) ))) + assert JzKetCoupled(1, 0, (S.Half, Rational(3, 2))) == \ + expand(couple(uncouple( JzKetCoupled(1, 0, (S.Half, Rational(3, 2))) ))) + assert JzKetCoupled(1, -1, (S.Half, Rational(3, 2))) == \ + expand(couple(uncouple( JzKetCoupled(1, -1, (S.Half, Rational(3, 2))) ))) + assert JzKetCoupled(2, 2, (S.Half, Rational(3, 2))) == \ + expand(couple(uncouple( JzKetCoupled(2, 2, (S.Half, Rational(3, 2))) ))) + assert JzKetCoupled(2, 1, (S.Half, Rational(3, 2))) == \ + expand(couple(uncouple( JzKetCoupled(2, 1, (S.Half, Rational(3, 2))) ))) + assert JzKetCoupled(2, 0, (S.Half, Rational(3, 2))) == \ + expand(couple(uncouple( JzKetCoupled(2, 0, (S.Half, Rational(3, 2))) ))) + assert JzKetCoupled(2, -1, (S.Half, Rational(3, 2))) == \ + expand(couple(uncouple( JzKetCoupled(2, -1, (S.Half, Rational(3, 2))) ))) + assert JzKetCoupled(2, -2, (S.Half, Rational(3, 2))) == \ + expand(couple(uncouple( JzKetCoupled(2, -2, (S.Half, Rational(3, 2))) ))) + + +def test_couple_3_states(): + # Default coupling + # j1=1/2, j2=1/2, j3=1/2 + assert JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half)) == \ + expand(couple(uncouple( + JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half)) ))) + assert JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half)) == \ + expand(couple(uncouple( + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half)) ))) + assert JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half)) == \ + expand(couple(uncouple( + JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half)) ))) + assert JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half)) == \ + expand(couple(uncouple( + JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half)) ))) + assert JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half)) == \ + expand(couple(uncouple( + JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half)) ))) + assert JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half)) == \ + expand(couple(uncouple( + JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half)) ))) + # j1=1/2, j2=1/2, j3=1 + assert JzKetCoupled(0, 0, (S.Half, S.Half, 1)) == \ + expand(couple(uncouple( JzKetCoupled(0, 0, (S.Half, S.Half, 1)) ))) + assert JzKetCoupled(1, 1, (S.Half, S.Half, 1)) == \ + expand(couple(uncouple( JzKetCoupled(1, 1, (S.Half, S.Half, 1)) ))) + assert JzKetCoupled(1, 0, (S.Half, S.Half, 1)) == \ + expand(couple(uncouple( JzKetCoupled(1, 0, (S.Half, S.Half, 1)) ))) + assert JzKetCoupled(1, -1, (S.Half, S.Half, 1)) == \ + expand(couple(uncouple( JzKetCoupled(1, -1, (S.Half, S.Half, 1)) ))) + assert JzKetCoupled(2, 2, (S.Half, S.Half, 1)) == \ + expand(couple(uncouple( JzKetCoupled(2, 2, (S.Half, S.Half, 1)) ))) + assert JzKetCoupled(2, 1, (S.Half, S.Half, 1)) == \ + expand(couple(uncouple( JzKetCoupled(2, 1, (S.Half, S.Half, 1)) ))) + assert JzKetCoupled(2, 0, (S.Half, S.Half, 1)) == \ + expand(couple(uncouple( JzKetCoupled(2, 0, (S.Half, S.Half, 1)) ))) + assert JzKetCoupled(2, -1, (S.Half, S.Half, 1)) == \ + expand(couple(uncouple( JzKetCoupled(2, -1, (S.Half, S.Half, 1)) ))) + assert JzKetCoupled(2, -2, (S.Half, S.Half, 1)) == \ + expand(couple(uncouple( JzKetCoupled(2, -2, (S.Half, S.Half, 1)) ))) + # Couple j1+j3=j13, j13+j2=j + # j1=1/2, j2=1/2, j3=1/2, j13=0 + assert JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half), ((1, 3, 0), (1, 2, S.Half))) == \ + expand(couple(uncouple( JzKetCoupled(S.Half, S.Half, (S.Half, S( + 1)/2, S.Half), ((1, 3, 0), (1, 2, S.Half))) ), ((1, 3), (1, 2)) )) + assert JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half), ((1, 3, 0), (1, 2, S.Half))) == \ + expand(couple(uncouple( JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S( + 1)/2, S.Half), ((1, 3, 0), (1, 2, S.Half))) ), ((1, 3), (1, 2)) )) + # j1=1, j2=1/2, j3=1, j13=1 + assert JzKetCoupled(S.Half, S.Half, (1, S.Half, 1), ((1, 3, 1), (1, 2, S.Half))) == \ + expand(couple(uncouple( JzKetCoupled(S.Half, S.Half, ( + 1, S.Half, 1), ((1, 3, 1), (1, 2, S.Half))) ), ((1, 3), (1, 2)) )) + assert JzKetCoupled(S.Half, Rational(-1, 2), (1, S.Half, 1), ((1, 3, 1), (1, 2, S.Half))) == \ + expand(couple(uncouple( JzKetCoupled(S.Half, Rational(-1, 2), ( + 1, S.Half, 1), ((1, 3, 1), (1, 2, S.Half))) ), ((1, 3), (1, 2)) )) + assert JzKetCoupled(Rational(3, 2), Rational(3, 2), (1, S.Half, 1), ((1, 3, 1), (1, 2, Rational(3, 2)))) == \ + expand(couple(uncouple( JzKetCoupled(Rational(3, 2), Rational(3, 2), ( + 1, S.Half, 1), ((1, 3, 1), (1, 2, Rational(3, 2)))) ), ((1, 3), (1, 2)) )) + assert JzKetCoupled(Rational(3, 2), S.Half, (1, S.Half, 1), ((1, 3, 1), (1, 2, Rational(3, 2)))) == \ + expand(couple(uncouple( JzKetCoupled(Rational(3, 2), S.Half, ( + 1, S.Half, 1), ((1, 3, 1), (1, 2, Rational(3, 2)))) ), ((1, 3), (1, 2)) )) + assert JzKetCoupled(Rational(3, 2), Rational(-1, 2), (1, S.Half, 1), ((1, 3, 1), (1, 2, Rational(3, 2)))) == \ + expand(couple(uncouple( JzKetCoupled(Rational(3, 2), Rational(-1, 2), ( + 1, S.Half, 1), ((1, 3, 1), (1, 2, Rational(3, 2)))) ), ((1, 3), (1, 2)) )) + assert JzKetCoupled(Rational(3, 2), Rational(-3, 2), (1, S.Half, 1), ((1, 3, 1), (1, 2, Rational(3, 2)))) == \ + expand(couple(uncouple( JzKetCoupled(Rational(3, 2), Rational(-3, 2), ( + 1, S.Half, 1), ((1, 3, 1), (1, 2, Rational(3, 2)))) ), ((1, 3), (1, 2)) )) + + +def test_couple_4_states(): + # Default coupling + # j1=1/2, j2=1/2, j3=1/2, j4=1/2 + assert JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half)) == \ + expand(couple( + uncouple( JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half)) ))) + assert JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half)) == \ + expand(couple( + uncouple( JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half)) ))) + assert JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half)) == \ + expand(couple(uncouple( + JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half)) ))) + assert JzKetCoupled(2, 2, (S.Half, S.Half, S.Half, S.Half)) == \ + expand(couple( + uncouple( JzKetCoupled(2, 2, (S.Half, S.Half, S.Half, S.Half)) ))) + assert JzKetCoupled(2, 1, (S.Half, S.Half, S.Half, S.Half)) == \ + expand(couple( + uncouple( JzKetCoupled(2, 1, (S.Half, S.Half, S.Half, S.Half)) ))) + assert JzKetCoupled(2, 0, (S.Half, S.Half, S.Half, S.Half)) == \ + expand(couple( + uncouple( JzKetCoupled(2, 0, (S.Half, S.Half, S.Half, S.Half)) ))) + assert JzKetCoupled(2, -1, (S.Half, S.Half, S.Half, S.Half)) == \ + expand(couple(uncouple( + JzKetCoupled(2, -1, (S.Half, S.Half, S.Half, S.Half)) ))) + assert JzKetCoupled(2, -2, (S.Half, S.Half, S.Half, S.Half)) == \ + expand(couple(uncouple( + JzKetCoupled(2, -2, (S.Half, S.Half, S.Half, S.Half)) ))) + # j1=1/2, j2=1/2, j3=1/2, j4=1 + assert JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1)) == \ + expand(couple(uncouple( + JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1)) ))) + assert JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1)) == \ + expand(couple(uncouple( + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1)) ))) + assert JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1)) == \ + expand(couple(uncouple( + JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1)) ))) + assert JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1)) == \ + expand(couple(uncouple( + JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1)) ))) + assert JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1)) == \ + expand(couple(uncouple( + JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1)) ))) + assert JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1)) == \ + expand(couple(uncouple( + JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1)) ))) + assert JzKetCoupled(Rational(5, 2), Rational(5, 2), (S.Half, S.Half, S.Half, 1)) == \ + expand(couple(uncouple( + JzKetCoupled(Rational(5, 2), Rational(5, 2), (S.Half, S.Half, S.Half, 1)) ))) + assert JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1)) == \ + expand(couple(uncouple( + JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1)) ))) + assert JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S.Half, S.Half, 1)) == \ + expand(couple(uncouple( + JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S.Half, S.Half, 1)) ))) + assert JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1)) == \ + expand(couple(uncouple( + JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1)) ))) + assert JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1)) == \ + expand(couple(uncouple( + JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1)) ))) + assert JzKetCoupled(Rational(5, 2), Rational(-5, 2), (S.Half, S.Half, S.Half, 1)) == \ + expand(couple(uncouple( + JzKetCoupled(Rational(5, 2), Rational(-5, 2), (S.Half, S.Half, S.Half, 1)) ))) + # Coupling j1+j3=j13, j2+j4=j24, j13+j24=j + # j1=1/2, j2=1/2, j3=1/2, j4=1/2, j13=1, j24=0 + assert JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 3, 1), (2, 4, 0), (1, 2, 1)) ) == \ + expand(couple(uncouple( JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 3, 1), (2, 4, 0), (1, 2, 1)) ) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 3, 1), (2, 4, 0), (1, 2, 1)) ) == \ + expand(couple(uncouple( JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 3, 1), (2, 4, 0), (1, 2, 1)) ) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 3, 1), (2, 4, 0), (1, 2, 1)) ) == \ + expand(couple(uncouple( JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 3, 1), (2, 4, 0), (1, 2, 1)) ) ), ((1, 3), (2, 4), (1, 2)) )) + # j1=1/2, j2=1/2, j3=1/2, j4=1, j13=1, j24=1/2 + assert JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 3, 1), (2, 4, S.Half), (1, 2, S.Half)) ) == \ + expand(couple(uncouple( JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 3, 1), (2, 4, S.Half), (1, 2, S.Half)) )), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 3, 1), (2, 4, S.Half), (1, 2, S.Half)) ) == \ + expand(couple(uncouple( JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 3, 1), (2, 4, S.Half), (1, 2, S.Half)) ) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 3, 1), (2, 4, S.Half), (1, 2, Rational(3, 2))) ) == \ + expand(couple(uncouple( JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 3, 1), (2, 4, S.Half), (1, 2, Rational(3, 2))) ) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 3, 1), (2, 4, S.Half), (1, 2, Rational(3, 2))) ) == \ + expand(couple(uncouple( JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 3, 1), (2, 4, S.Half), (1, 2, Rational(3, 2))) ) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 3, 1), (2, 4, S.Half), (1, 2, Rational(3, 2))) ) == \ + expand(couple(uncouple( JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 3, 1), (2, 4, S.Half), (1, 2, Rational(3, 2))) ) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 3, 1), (2, 4, S.Half), (1, 2, Rational(3, 2))) ) == \ + expand(couple(uncouple( JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 3, 1), (2, 4, S.Half), (1, 2, Rational(3, 2))) ) ), ((1, 3), (2, 4), (1, 2)) )) + # j1=1/2, j2=1, j3=1/2, j4=1, j13=0, j24=1 + assert JzKetCoupled(1, 1, (S.Half, 1, S.Half, 1), ((1, 3, 0), (2, 4, 1), (1, 2, 1)) ) == \ + expand(couple(uncouple( JzKetCoupled(1, 1, (S.Half, 1, S.Half, 1), ( + (1, 3, 0), (2, 4, 1), (1, 2, 1))) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(1, 0, (S.Half, 1, S.Half, 1), ((1, 3, 0), (2, 4, 1), (1, 2, 1)) ) == \ + expand(couple(uncouple( JzKetCoupled(1, 0, (S.Half, 1, S.Half, 1), ( + (1, 3, 0), (2, 4, 1), (1, 2, 1))) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(1, -1, (S.Half, 1, S.Half, 1), ((1, 3, 0), (2, 4, 1), (1, 2, 1)) ) == \ + expand(couple(uncouple( JzKetCoupled(1, -1, (S.Half, 1, S.Half, 1), ( + (1, 3, 0), (2, 4, 1), (1, 2, 1))) ), ((1, 3), (2, 4), (1, 2)) )) + # j1=1/2, j2=1, j3=1/2, j4=1, j13=1, j24=1 + assert JzKetCoupled(0, 0, (S.Half, 1, S.Half, 1), ((1, 3, 1), (2, 4, 1), (1, 2, 0)) ) == \ + expand(couple(uncouple( JzKetCoupled(0, 0, (S.Half, 1, S.Half, 1), ( + (1, 3, 1), (2, 4, 1), (1, 2, 0))) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(1, 1, (S.Half, 1, S.Half, 1), ((1, 3, 1), (2, 4, 1), (1, 2, 1)) ) == \ + expand(couple(uncouple( JzKetCoupled(1, 1, (S.Half, 1, S.Half, 1), ( + (1, 3, 1), (2, 4, 1), (1, 2, 1))) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(1, 0, (S.Half, 1, S.Half, 1), ((1, 3, 1), (2, 4, 1), (1, 2, 1)) ) == \ + expand(couple(uncouple( JzKetCoupled(1, 0, (S.Half, 1, S.Half, 1), ( + (1, 3, 1), (2, 4, 1), (1, 2, 1))) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(1, -1, (S.Half, 1, S.Half, 1), ((1, 3, 1), (2, 4, 1), (1, 2, 1)) ) == \ + expand(couple(uncouple( JzKetCoupled(1, -1, (S.Half, 1, S.Half, 1), ( + (1, 3, 1), (2, 4, 1), (1, 2, 1))) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(2, 2, (S.Half, 1, S.Half, 1), ((1, 3, 1), (2, 4, 1), (1, 2, 2)) ) == \ + expand(couple(uncouple( JzKetCoupled(2, 2, (S.Half, 1, S.Half, 1), ( + (1, 3, 1), (2, 4, 1), (1, 2, 2))) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(2, 1, (S.Half, 1, S.Half, 1), ((1, 3, 1), (2, 4, 1), (1, 2, 2)) ) == \ + expand(couple(uncouple( JzKetCoupled(2, 1, (S.Half, 1, S.Half, 1), ( + (1, 3, 1), (2, 4, 1), (1, 2, 2))) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(2, 0, (S.Half, 1, S.Half, 1), ((1, 3, 1), (2, 4, 1), (1, 2, 2)) ) == \ + expand(couple(uncouple( JzKetCoupled(2, 0, (S.Half, 1, S.Half, 1), ( + (1, 3, 1), (2, 4, 1), (1, 2, 2))) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(2, -1, (S.Half, 1, S.Half, 1), ((1, 3, 1), (2, 4, 1), (1, 2, 2)) ) == \ + expand(couple(uncouple( JzKetCoupled(2, -1, (S.Half, 1, S.Half, 1), ( + (1, 3, 1), (2, 4, 1), (1, 2, 2))) ), ((1, 3), (2, 4), (1, 2)) )) + assert JzKetCoupled(2, -2, (S.Half, 1, S.Half, 1), ((1, 3, 1), (2, 4, 1), (1, 2, 2)) ) == \ + expand(couple(uncouple( JzKetCoupled(2, -2, (S.Half, 1, S.Half, 1), ( + (1, 3, 1), (2, 4, 1), (1, 2, 2))) ), ((1, 3), (2, 4), (1, 2)) )) + + +def test_couple_2_states_numerical(): + # j1=1/2, j2=1/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half))) == \ + JzKetCoupled(1, 1, (S.Half, S.Half)) + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)))) == \ + sqrt(2)*JzKetCoupled(0, 0, (S( + 1)/2, S.Half))/2 + sqrt(2)*JzKetCoupled(1, 0, (S.Half, S.Half))/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half))) == \ + -sqrt(2)*JzKetCoupled(0, 0, (S( + 1)/2, S.Half))/2 + sqrt(2)*JzKetCoupled(1, 0, (S.Half, S.Half))/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)))) == \ + JzKetCoupled(1, -1, (S.Half, S.Half)) + # j1=1, j2=1/2 + assert couple(TensorProduct(JzKet(1, 1), JzKet(S.Half, S.Half))) == \ + JzKetCoupled(Rational(3, 2), Rational(3, 2), (1, S.Half)) + assert couple(TensorProduct(JzKet(1, 1), JzKet(S.Half, Rational(-1, 2)))) == \ + sqrt(6)*JzKetCoupled(S.Half, S.Half, (1, S.Half))/3 + sqrt( + 3)*JzKetCoupled(Rational(3, 2), S.Half, (1, S.Half))/3 + assert couple(TensorProduct(JzKet(1, 0), JzKet(S.Half, S.Half))) == \ + -sqrt(3)*JzKetCoupled(S.Half, S.Half, (1, S.Half))/3 + \ + sqrt(6)*JzKetCoupled(Rational(3, 2), S.Half, (1, S.Half))/3 + assert couple(TensorProduct(JzKet(1, 0), JzKet(S.Half, Rational(-1, 2)))) == \ + sqrt(3)*JzKetCoupled(S.Half, Rational(-1, 2), (1, S.Half))/3 + \ + sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (1, S.Half))/3 + assert couple(TensorProduct(JzKet(1, -1), JzKet(S.Half, S.Half))) == \ + -sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (1, S( + 1)/2))/3 + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (1, S.Half))/3 + assert couple(TensorProduct(JzKet(1, -1), JzKet(S.Half, Rational(-1, 2)))) == \ + JzKetCoupled(Rational(3, 2), Rational(-3, 2), (1, S.Half)) + # j1=1, j2=1 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, 1))) == \ + JzKetCoupled(2, 2, (1, 1)) + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, 0))) == \ + sqrt(2)*JzKetCoupled( + 1, 1, (1, 1))/2 + sqrt(2)*JzKetCoupled(2, 1, (1, 1))/2 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, -1))) == \ + sqrt(3)*JzKetCoupled(0, 0, (1, 1))/3 + sqrt(2)*JzKetCoupled( + 1, 0, (1, 1))/2 + sqrt(6)*JzKetCoupled(2, 0, (1, 1))/6 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, 1))) == \ + -sqrt(2)*JzKetCoupled( + 1, 1, (1, 1))/2 + sqrt(2)*JzKetCoupled(2, 1, (1, 1))/2 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, 0))) == \ + -sqrt(3)*JzKetCoupled( + 0, 0, (1, 1))/3 + sqrt(6)*JzKetCoupled(2, 0, (1, 1))/3 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, -1))) == \ + sqrt(2)*JzKetCoupled( + 1, -1, (1, 1))/2 + sqrt(2)*JzKetCoupled(2, -1, (1, 1))/2 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, 1))) == \ + sqrt(3)*JzKetCoupled(0, 0, (1, 1))/3 - sqrt(2)*JzKetCoupled( + 1, 0, (1, 1))/2 + sqrt(6)*JzKetCoupled(2, 0, (1, 1))/6 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, 0))) == \ + -sqrt(2)*JzKetCoupled( + 1, -1, (1, 1))/2 + sqrt(2)*JzKetCoupled(2, -1, (1, 1))/2 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, -1))) == \ + JzKetCoupled(2, -2, (1, 1)) + # j1=3/2, j2=1/2 + assert couple(TensorProduct(JzKet(Rational(3, 2), Rational(3, 2)), JzKet(S.Half, S.Half))) == \ + JzKetCoupled(2, 2, (Rational(3, 2), S.Half)) + assert couple(TensorProduct(JzKet(Rational(3, 2), Rational(3, 2)), JzKet(S.Half, Rational(-1, 2)))) == \ + sqrt(3)*JzKetCoupled( + 1, 1, (Rational(3, 2), S.Half))/2 + JzKetCoupled(2, 1, (Rational(3, 2), S.Half))/2 + assert couple(TensorProduct(JzKet(Rational(3, 2), S.Half), JzKet(S.Half, S.Half))) == \ + -JzKetCoupled(1, 1, (S( + 3)/2, S.Half))/2 + sqrt(3)*JzKetCoupled(2, 1, (Rational(3, 2), S.Half))/2 + assert couple(TensorProduct(JzKet(Rational(3, 2), S.Half), JzKet(S.Half, Rational(-1, 2)))) == \ + sqrt(2)*JzKetCoupled(1, 0, (S( + 3)/2, S.Half))/2 + sqrt(2)*JzKetCoupled(2, 0, (Rational(3, 2), S.Half))/2 + assert couple(TensorProduct(JzKet(Rational(3, 2), Rational(-1, 2)), JzKet(S.Half, S.Half))) == \ + -sqrt(2)*JzKetCoupled(1, 0, (S( + 3)/2, S.Half))/2 + sqrt(2)*JzKetCoupled(2, 0, (Rational(3, 2), S.Half))/2 + assert couple(TensorProduct(JzKet(Rational(3, 2), Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)))) == \ + JzKetCoupled(1, -1, (S( + 3)/2, S.Half))/2 + sqrt(3)*JzKetCoupled(2, -1, (Rational(3, 2), S.Half))/2 + assert couple(TensorProduct(JzKet(Rational(3, 2), Rational(-3, 2)), JzKet(S.Half, S.Half))) == \ + -sqrt(3)*JzKetCoupled(1, -1, (Rational(3, 2), S.Half))/2 + \ + JzKetCoupled(2, -1, (Rational(3, 2), S.Half))/2 + assert couple(TensorProduct(JzKet(Rational(3, 2), Rational(-3, 2)), JzKet(S.Half, Rational(-1, 2)))) == \ + JzKetCoupled(2, -2, (Rational(3, 2), S.Half)) + + +def test_couple_3_states_numerical(): + # Default coupling + # j1=1/2,j2=1/2,j3=1/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half))) == \ + JzKetCoupled(Rational(3, 2), S( + 3)/2, (S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2))) ) + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)))) == \ + sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half)) )/3 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.One/ + 2), ((1, 2, 1), (1, 3, Rational(3, 2))) )/3 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half))) == \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half), ((1, 2, 0), (1, 3, S.Half)) )/2 - \ + sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half)) )/6 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.One/ + 2), ((1, 2, 1), (1, 3, Rational(3, 2))) )/3 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)))) == \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half), ((1, 2, 0), (1, 3, S.Half)) )/2 + \ + sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half)) )/6 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.One + /2), ((1, 2, 1), (1, 3, Rational(3, 2))) )/3 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half))) == \ + -sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half), ((1, 2, 0), (1, 3, S.Half)) )/2 - \ + sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half)) )/6 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.One/ + 2), ((1, 2, 1), (1, 3, Rational(3, 2))) )/3 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)))) == \ + -sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half), ((1, 2, 0), (1, 3, S.Half)) )/2 + \ + sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half)) )/6 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.One + /2), ((1, 2, 1), (1, 3, Rational(3, 2))) )/3 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half))) == \ + -sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half)) )/3 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.One + /2), ((1, 2, 1), (1, 3, Rational(3, 2))) )/3 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)))) == \ + JzKetCoupled(Rational(3, 2), -S( + 3)/2, (S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2))) ) + # j1=S.Half, j2=S.Half, j3=1 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1))) == \ + JzKetCoupled(2, 2, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 2)) ) + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0))) == \ + sqrt(2)*JzKetCoupled(1, 1, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + sqrt(2)*JzKetCoupled( + 2, 1, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1))) == \ + sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 0)) )/3 + \ + sqrt(2)*JzKetCoupled(1, 0, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + sqrt(6)*JzKetCoupled( + 2, 0, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1))) == \ + sqrt(2)*JzKetCoupled(1, 1, (S.Half, S.Half, 1), ((1, 2, 0), (1, 3, 1)) )/2 - \ + JzKetCoupled(1, 1, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(2, 1, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0))) == \ + -sqrt(6)*JzKetCoupled(0, 0, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 0)) )/6 + \ + sqrt(2)*JzKetCoupled(1, 0, (S.Half, S.Half, 1), ((1, 2, 0), (1, 3, 1)) )/2 + \ + sqrt(3)*JzKetCoupled( + 2, 0, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 2)) )/3 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1))) == \ + sqrt(2)*JzKetCoupled(1, -1, (S.Half, S.Half, 1), ((1, 2, 0), (1, 3, 1)) )/2 + \ + JzKetCoupled(1, -1, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(2, -1, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1))) == \ + -sqrt(2)*JzKetCoupled(1, 1, (S.Half, S.Half, 1), ((1, 2, 0), (1, 3, 1)) )/2 - \ + JzKetCoupled(1, 1, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(2, 1, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0))) == \ + -sqrt(6)*JzKetCoupled(0, 0, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 0)) )/6 - \ + sqrt(2)*JzKetCoupled(1, 0, (S.Half, S.Half, 1), ((1, 2, 0), (1, 3, 1)) )/2 + \ + sqrt(3)*JzKetCoupled( + 2, 0, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 2)) )/3 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1))) == \ + -sqrt(2)*JzKetCoupled(1, -1, (S.Half, S.Half, 1), ((1, 2, 0), (1, 3, 1)) )/2 + \ + JzKetCoupled(1, -1, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(2, -1, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1))) == \ + sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 0)) )/3 - \ + sqrt(2)*JzKetCoupled(1, 0, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + sqrt(6)*JzKetCoupled( + 2, 0, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0))) == \ + -sqrt(2)*JzKetCoupled(1, -1, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + sqrt(2)*JzKetCoupled( + 2, -1, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1))) == \ + JzKetCoupled(2, -2, (S.Half, S.Half, 1), ((1, 2, 1), (1, 3, 2)) ) + # j1=S.Half, j2=1, j3=1 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 1))) == \ + JzKetCoupled( + Rational(5, 2), Rational(5, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(5, 2))) ) + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0))) == \ + sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(S( + 5)/2, Rational(3, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, -1))) == \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, S.Half)) )/2 + \ + sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, 1, 1), ((1, + 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1))) == \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, Rational(3, 2))) )/3 - \ + 2*sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(S( + 5)/2, Rational(3, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0))) == \ + JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, S.Half)) )/3 - \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, S.Half)) )/3 + \ + sqrt(2)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, Rational(3, 2))) )/3 + \ + sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(S( + 5)/2, S.Half, (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1))) == \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, S.Half)) )/3 + \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, S.Half)) )/3 + \ + JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, Rational(3, 2))) )/3 + \ + 4*sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, + 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1))) == \ + -2*JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, S.Half)) )/3 + \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, S.Half)) )/6 + \ + sqrt(2)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, Rational(3, 2))) )/3 - \ + 2*sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, 1, 1), ((1, + 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0))) == \ + -sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, S.Half)) )/3 - \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, S.Half)) )/3 + \ + 2*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, Rational(3, 2))) )/3 - \ + sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, + 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, -1))) == \ + sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, Rational(3, 2))) )/3 + \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, + 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1))) == \ + -sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, Rational(3, 2))) )/3 - \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(S( + 5)/2, Rational(3, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0))) == \ + -sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, S.Half)) )/3 - \ + JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, S.Half)) )/3 - \ + 2*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, Rational(3, 2))) )/3 + \ + sqrt(5)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(S( + 5)/2, S.Half, (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1))) == \ + -2*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, S.Half)) )/3 + \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, S.Half)) )/6 - \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, Rational(3, 2))) )/3 + \ + 2*sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, + 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1))) == \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, S.Half)) )/3 + \ + JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, S.Half)) )/3 - \ + JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, Rational(3, 2))) )/3 - \ + 4*sqrt(5)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(S( + 5)/2, S.Half, (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0))) == \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, S.Half)) )/3 - \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, S.Half)) )/3 - \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, Rational(3, 2))) )/3 - \ + sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, + 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1))) == \ + -sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, 2, S.Half), (1, 3, Rational(3, 2))) )/3 + \ + 2*sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, + 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1))) == \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, S.Half)) )/2 - \ + sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, + 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0))) == \ + -sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, + 2, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, -1))) == \ + JzKetCoupled(S( + 5)/2, Rational(-5, 2), (S.Half, 1, 1), ((1, 2, Rational(3, 2)), (1, 3, Rational(5, 2))) ) + # j1=1, j2=1, j3=1 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, 1))) == \ + JzKetCoupled(3, 3, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) ) + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, 0))) == \ + sqrt(6)*JzKetCoupled(2, 2, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/3 + \ + sqrt(3)*JzKetCoupled(3, 2, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/3 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, -1))) == \ + sqrt(15)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/5 + \ + sqrt(3)*JzKetCoupled(2, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/3 + \ + sqrt(15)*JzKetCoupled(3, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, 1))) == \ + sqrt(2)*JzKetCoupled(2, 2, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/2 - \ + sqrt(6)*JzKetCoupled(2, 2, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/6 + \ + sqrt(3)*JzKetCoupled(3, 2, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/3 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, 0))) == \ + JzKetCoupled(1, 1, (1, 1, 1), ((1, 2, 1), (1, 3, 1)) )/2 - \ + sqrt(15)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/10 + \ + JzKetCoupled(2, 1, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/2 + \ + sqrt(3)*JzKetCoupled(2, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/6 + \ + 2*sqrt(15)*JzKetCoupled(3, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, -1))) == \ + sqrt(6)*JzKetCoupled(0, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 0)) )/6 + \ + JzKetCoupled(1, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/10 + \ + sqrt(3)*JzKetCoupled(2, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/6 + \ + JzKetCoupled(2, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/2 + \ + sqrt(10)*JzKetCoupled(3, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/10 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, 1))) == \ + sqrt(3)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 2, 0), (1, 3, 1)) )/3 - \ + JzKetCoupled(1, 1, (1, 1, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/30 + \ + JzKetCoupled(2, 1, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/2 - \ + sqrt(3)*JzKetCoupled(2, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/6 + \ + sqrt(15)*JzKetCoupled(3, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, 0))) == \ + -sqrt(6)*JzKetCoupled(0, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 0)) )/6 + \ + sqrt(3)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 2, 0), (1, 3, 1)) )/3 - \ + sqrt(15)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/15 + \ + sqrt(3)*JzKetCoupled(2, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/3 + \ + sqrt(10)*JzKetCoupled(3, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/10 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, -1))) == \ + sqrt(3)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 2, 0), (1, 3, 1)) )/3 + \ + JzKetCoupled(1, -1, (1, 1, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/30 + \ + JzKetCoupled(2, -1, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/2 + \ + sqrt(3)*JzKetCoupled(2, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/6 + \ + sqrt(15)*JzKetCoupled(3, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, 1))) == \ + -sqrt(2)*JzKetCoupled(2, 2, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/2 - \ + sqrt(6)*JzKetCoupled(2, 2, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/6 + \ + sqrt(3)*JzKetCoupled(3, 2, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/3 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, 0))) == \ + -JzKetCoupled(1, 1, (1, 1, 1), ((1, 2, 1), (1, 3, 1)) )/2 - \ + sqrt(15)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/10 - \ + JzKetCoupled(2, 1, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/2 + \ + sqrt(3)*JzKetCoupled(2, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/6 + \ + 2*sqrt(15)*JzKetCoupled(3, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, -1))) == \ + -sqrt(6)*JzKetCoupled(0, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 0)) )/6 - \ + JzKetCoupled(1, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/10 - \ + sqrt(3)*JzKetCoupled(2, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/6 + \ + JzKetCoupled(2, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/2 + \ + sqrt(10)*JzKetCoupled(3, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/10 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, 1))) == \ + -sqrt(3)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 2, 0), (1, 3, 1)) )/3 + \ + sqrt(15)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/15 - \ + sqrt(3)*JzKetCoupled(2, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/3 + \ + 2*sqrt(15)*JzKetCoupled(3, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, 0))) == \ + -sqrt(3)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 2, 0), (1, 3, 1)) )/3 - \ + 2*sqrt(15)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/15 + \ + sqrt(10)*JzKetCoupled(3, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/5 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, -1))) == \ + -sqrt(3)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 2, 0), (1, 3, 1)) )/3 + \ + sqrt(15)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/15 + \ + sqrt(3)*JzKetCoupled(2, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/3 + \ + 2*sqrt(15)*JzKetCoupled(3, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, 1))) == \ + sqrt(6)*JzKetCoupled(0, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 0)) )/6 - \ + JzKetCoupled(1, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/10 + \ + sqrt(3)*JzKetCoupled(2, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/6 - \ + JzKetCoupled(2, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/2 + \ + sqrt(10)*JzKetCoupled(3, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/10 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, 0))) == \ + -JzKetCoupled(1, -1, (1, 1, 1), ((1, 2, 1), (1, 3, 1)) )/2 - \ + sqrt(15)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/10 + \ + JzKetCoupled(2, -1, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/2 - \ + sqrt(3)*JzKetCoupled(2, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/6 + \ + 2*sqrt(15)*JzKetCoupled(3, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, -1))) == \ + sqrt(2)*JzKetCoupled(2, -2, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/2 + \ + sqrt(6)*JzKetCoupled(2, -2, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/6 + \ + sqrt(3)*JzKetCoupled(3, -2, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/3 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, 1))) == \ + sqrt(3)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 2, 0), (1, 3, 1)) )/3 + \ + JzKetCoupled(1, 1, (1, 1, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/30 - \ + JzKetCoupled(2, 1, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/2 - \ + sqrt(3)*JzKetCoupled(2, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/6 + \ + sqrt(15)*JzKetCoupled(3, 1, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/15 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, 0))) == \ + sqrt(6)*JzKetCoupled(0, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 0)) )/6 + \ + sqrt(3)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 2, 0), (1, 3, 1)) )/3 - \ + sqrt(15)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/15 - \ + sqrt(3)*JzKetCoupled(2, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/3 + \ + sqrt(10)*JzKetCoupled(3, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/10 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, -1))) == \ + sqrt(3)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 2, 0), (1, 3, 1)) )/3 - \ + JzKetCoupled(1, -1, (1, 1, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/30 - \ + JzKetCoupled(2, -1, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/2 + \ + sqrt(3)*JzKetCoupled(2, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/6 + \ + sqrt(15)*JzKetCoupled(3, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/15 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, 1))) == \ + -sqrt(6)*JzKetCoupled(0, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 0)) )/6 + \ + JzKetCoupled(1, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/10 - \ + sqrt(3)*JzKetCoupled(2, 0, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/6 - \ + JzKetCoupled(2, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/2 + \ + sqrt(10)*JzKetCoupled(3, 0, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/10 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, 0))) == \ + JzKetCoupled(1, -1, (1, 1, 1), ((1, 2, 1), (1, 3, 1)) )/2 - \ + sqrt(15)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/10 - \ + JzKetCoupled(2, -1, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/2 - \ + sqrt(3)*JzKetCoupled(2, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/6 + \ + 2*sqrt(15)*JzKetCoupled(3, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/15 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, -1))) == \ + -sqrt(2)*JzKetCoupled(2, -2, (1, 1, 1), ((1, 2, 1), (1, 3, 2)) )/2 + \ + sqrt(6)*JzKetCoupled(2, -2, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/6 + \ + sqrt(3)*JzKetCoupled(3, -2, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/3 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, 1))) == \ + sqrt(15)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 1)) )/5 - \ + sqrt(3)*JzKetCoupled(2, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/3 + \ + sqrt(15)*JzKetCoupled(3, -1, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/15 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, 0))) == \ + -sqrt(6)*JzKetCoupled(2, -2, (1, 1, 1), ((1, 2, 2), (1, 3, 2)) )/3 + \ + sqrt(3)*JzKetCoupled(3, -2, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) )/3 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, -1))) == \ + JzKetCoupled(3, -3, (1, 1, 1), ((1, 2, 2), (1, 3, 3)) ) + # j1=S.Half, j2=S.Half, j3=Rational(3, 2) + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), Rational(3, 2)))) == \ + JzKetCoupled(Rational(5, 2), S( + 5)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(5, 2))) ) + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), S.Half))) == \ + sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(3, 2))) )/5 + \ + sqrt(15)*JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S.Half, S(3) + /2), ((1, 2, 1), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), Rational(-1, 2)))) == \ + sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, S.Half)) )/6 + \ + 2*sqrt(30)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(30)*JzKetCoupled(Rational(5, 2), S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), Rational(-3, 2)))) == \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, S.Half)) )/2 + \ + sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), -S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), Rational(3, 2)))) == \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 0), (1, 3, Rational(3, 2))) )/2 - \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(3, 2))) )/10 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S.Half, S(3)/ + 2), ((1, 2, 1), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), S.Half))) == \ + -sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, S.Half)) )/6 + \ + sqrt(2)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 0), (1, 3, Rational(3, 2))) )/2 - \ + sqrt(30)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(3, 2))) )/30 + \ + sqrt(30)*JzKetCoupled(Rational(5, 2), S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), Rational(-1, 2)))) == \ + -sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, S.Half)) )/6 + \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 0), (1, 3, Rational(3, 2))) )/2 + \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(3, 2))) )/30 + \ + sqrt(30)*JzKetCoupled(Rational(5, 2), -S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), Rational(-3, 2)))) == \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 0), (1, 3, Rational(3, 2))) )/2 + \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(3, 2))) )/10 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S.Half, S(3) + /2), ((1, 2, 1), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), Rational(3, 2)))) == \ + -sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 0), (1, 3, Rational(3, 2))) )/2 - \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(3, 2))) )/10 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S.Half, S(3)/ + 2), ((1, 2, 1), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), S.Half))) == \ + -sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, S.Half)) )/6 - \ + sqrt(2)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 0), (1, 3, Rational(3, 2))) )/2 - \ + sqrt(30)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(3, 2))) )/30 + \ + sqrt(30)*JzKetCoupled(Rational(5, 2), S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), Rational(-1, 2)))) == \ + -sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, S.Half)) )/6 - \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 0), (1, 3, Rational(3, 2))) )/2 + \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(3, 2))) )/30 + \ + sqrt(30)*JzKetCoupled(Rational(5, 2), -S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), Rational(-3, 2)))) == \ + -sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 0), (1, 3, Rational(3, 2))) )/2 + \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(3, 2))) )/10 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S.Half, S(3) + /2), ((1, 2, 1), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), Rational(3, 2)))) == \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, S.Half)) )/2 - \ + sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), S.Half))) == \ + sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, S.Half)) )/6 - \ + 2*sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(30)*JzKetCoupled(Rational(5, 2), -S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), Rational(-1, 2)))) == \ + -sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(3, 2))) )/5 + \ + sqrt(15)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S.Half, S( + 3)/2), ((1, 2, 1), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), Rational(-3, 2)))) == \ + JzKetCoupled(Rational(5, 2), -S( + 5)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 2, 1), (1, 3, Rational(5, 2))) ) + # Couple j1 to j3 + # j1=1/2, j2=1/2, j3=1/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)), ((1, 3), (1, 2)) ) == \ + JzKetCoupled(Rational(3, 2), S( + 3)/2, (S.Half, S.Half, S.Half), ((1, 3, 1), (1, 2, Rational(3, 2))) ) + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (1, 2)) ) == \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half), ((1, 3, 0), (1, 2, S.Half)) )/2 - \ + sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half), ((1, 3, 1), (1, 2, S.Half)) )/6 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.One/ + 2), ((1, 3, 1), (1, 2, Rational(3, 2))) )/3 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), ((1, 3), (1, 2)) ) == \ + sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half), ((1, 3, 1), (1, 2, S.Half)) )/3 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.One/ + 2), ((1, 3, 1), (1, 2, Rational(3, 2))) )/3 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (1, 2)) ) == \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half), ((1, 3, 0), (1, 2, S.Half)) )/2 + \ + sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half), ((1, 3, 1), (1, 2, S.Half)) )/6 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.One + /2), ((1, 3, 1), (1, 2, Rational(3, 2))) )/3 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)), ((1, 3), (1, 2)) ) == \ + -sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half), ((1, 3, 0), (1, 2, S.Half)) )/2 - \ + sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half), ((1, 3, 1), (1, 2, S.Half)) )/6 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.One/ + 2), ((1, 3, 1), (1, 2, Rational(3, 2))) )/3 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (1, 2)) ) == \ + -sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half), ((1, 3, 1), (1, 2, S.Half)) )/3 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.One + /2), ((1, 3, 1), (1, 2, Rational(3, 2))) )/3 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), ((1, 3), (1, 2)) ) == \ + -sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half), ((1, 3, 0), (1, 2, S.Half)) )/2 + \ + sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half), ((1, 3, 1), (1, 2, S.Half)) )/6 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.One + /2), ((1, 3, 1), (1, 2, Rational(3, 2))) )/3 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), ((1, 3), (1, 2)) ) == \ + JzKetCoupled(Rational(3, 2), -S( + 3)/2, (S.Half, S.Half, S.Half), ((1, 3, 1), (1, 2, Rational(3, 2))) ) + # j1=1/2, j2=1/2, j3=1 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + JzKetCoupled(2, 2, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 2)) ) + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + sqrt(3)*JzKetCoupled(1, 1, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 1)) )/3 - \ + sqrt(6)*JzKetCoupled(1, 1, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 1)) )/6 + \ + sqrt(2)*JzKetCoupled( + 2, 1, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + -sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 0)) )/3 + \ + sqrt(3)*JzKetCoupled(1, 0, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 1)) )/3 - \ + sqrt(6)*JzKetCoupled(1, 0, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 1)) )/6 + \ + sqrt(6)*JzKetCoupled( + 2, 0, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + sqrt(3)*JzKetCoupled(1, 1, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 1)) )/2 + \ + JzKetCoupled(2, 1, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + sqrt(6)*JzKetCoupled(0, 0, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 0)) )/6 + \ + sqrt(6)*JzKetCoupled(1, 0, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 1)) )/6 + \ + sqrt(3)*JzKetCoupled(1, 0, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 1)) )/3 + \ + sqrt(3)*JzKetCoupled( + 2, 0, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 2)) )/3 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + sqrt(6)*JzKetCoupled(1, -1, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 1)) )/3 + \ + sqrt(3)*JzKetCoupled(1, -1, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 1)) )/6 + \ + JzKetCoupled( + 2, -1, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + -sqrt(6)*JzKetCoupled(1, 1, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 1)) )/3 - \ + sqrt(3)*JzKetCoupled(1, 1, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 1)) )/6 + \ + JzKetCoupled(2, 1, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + sqrt(6)*JzKetCoupled(0, 0, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 0)) )/6 - \ + sqrt(6)*JzKetCoupled(1, 0, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 1)) )/6 - \ + sqrt(3)*JzKetCoupled(1, 0, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 1)) )/3 + \ + sqrt(3)*JzKetCoupled( + 2, 0, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 2)) )/3 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + -sqrt(3)*JzKetCoupled(1, -1, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 1)) )/2 + \ + JzKetCoupled( + 2, -1, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + -sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 0)) )/3 - \ + sqrt(3)*JzKetCoupled(1, 0, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 1)) )/3 + \ + sqrt(6)*JzKetCoupled(1, 0, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 1)) )/6 + \ + sqrt(6)*JzKetCoupled( + 2, 0, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + -sqrt(3)*JzKetCoupled(1, -1, (S.Half, S.Half, 1), ((1, 3, S.Half), (1, 2, 1)) )/3 + \ + sqrt(6)*JzKetCoupled(1, -1, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 1)) )/6 + \ + sqrt(2)*JzKetCoupled( + 2, -1, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + JzKetCoupled(2, -2, (S.Half, S.Half, 1), ((1, 3, Rational(3, 2)), (1, 2, 2)) ) + # j 1=1/2, j 2=1, j 3=1 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + JzKetCoupled( + Rational(5, 2), Rational(5, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(5, 2))) ) + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))) )/3 - \ + 2*sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(S( + 5)/2, Rational(3, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 1), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + -2*JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, S.Half)) )/3 + \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, S.Half)) )/6 + \ + sqrt(2)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))) )/3 - \ + 2*sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, 1, 1), ((1, + 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(S( + 5)/2, Rational(3, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, S.Half)) )/3 - \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, S.Half)) )/3 + \ + sqrt(2)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))) )/3 + \ + sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(S( + 5)/2, S.Half, (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, 0), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + -sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, S.Half)) )/3 - \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, S.Half)) )/3 + \ + 2*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))) )/3 - \ + sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, + 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, S.Half)) )/2 + \ + sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, 1, 1), ((1, + 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, S.Half)) )/3 + \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, S.Half)) )/3 + \ + JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))) )/3 + \ + 4*sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, + 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(1, -1), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))) )/3 + \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, + 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + -sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))) )/3 - \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(S( + 5)/2, Rational(3, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, S.Half)) )/3 + \ + JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, S.Half)) )/3 - \ + JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))) )/3 - \ + 4*sqrt(5)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(S( + 5)/2, S.Half, (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, S.Half)) )/2 - \ + sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, + 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + -sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, S.Half)) )/3 - \ + JzKetCoupled(S.Half, S.Half, (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, S.Half)) )/3 - \ + 2*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))) )/3 + \ + sqrt(5)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(S( + 5)/2, S.Half, (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, S.Half)) )/3 - \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, S.Half)) )/3 - \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))) )/3 - \ + sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, + 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + -sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, + 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + -2*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, S.Half)) )/3 + \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, S.Half)) )/6 - \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))) )/3 + \ + 2*sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, 1, 1), ((1, + 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + -sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, 3, S.Half), (1, 2, Rational(3, 2))) )/3 + \ + 2*sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, 1, 1), ((1, + 3, Rational(3, 2)), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + JzKetCoupled(S( + 5)/2, Rational(-5, 2), (S.Half, 1, 1), ((1, 3, Rational(3, 2)), (1, 2, Rational(5, 2))) ) + # j1=1, 1, 1 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + JzKetCoupled(3, 3, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) ) + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + sqrt(2)*JzKetCoupled(2, 2, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/2 - \ + sqrt(6)*JzKetCoupled(2, 2, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/6 + \ + sqrt(3)*JzKetCoupled(3, 2, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/3 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, 1), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + sqrt(3)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 3, 0), (1, 2, 1)) )/3 - \ + JzKetCoupled(1, 1, (1, 1, 1), ((1, 3, 1), (1, 2, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/30 + \ + JzKetCoupled(2, 1, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/2 - \ + sqrt(3)*JzKetCoupled(2, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/6 + \ + sqrt(15)*JzKetCoupled(3, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + sqrt(6)*JzKetCoupled(2, 2, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/3 + \ + sqrt(3)*JzKetCoupled(3, 2, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/3 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + JzKetCoupled(1, 1, (1, 1, 1), ((1, 3, 1), (1, 2, 1)) )/2 - \ + sqrt(15)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/10 + \ + JzKetCoupled(2, 1, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/2 + \ + sqrt(3)*JzKetCoupled(2, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/6 + \ + 2*sqrt(15)*JzKetCoupled(3, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, 0), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + -sqrt(6)*JzKetCoupled(0, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 0)) )/6 + \ + sqrt(3)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 0), (1, 2, 1)) )/3 - \ + sqrt(15)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/15 + \ + sqrt(3)*JzKetCoupled(2, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/3 + \ + sqrt(10)*JzKetCoupled(3, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/10 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + sqrt(15)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/5 + \ + sqrt(3)*JzKetCoupled(2, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/3 + \ + sqrt(15)*JzKetCoupled(3, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + sqrt(6)*JzKetCoupled(0, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 0)) )/6 + \ + JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/10 + \ + sqrt(3)*JzKetCoupled(2, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/6 + \ + JzKetCoupled(2, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/2 + \ + sqrt(10)*JzKetCoupled(3, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/10 + assert couple(TensorProduct(JzKet(1, 1), JzKet(1, -1), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + sqrt(3)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 3, 0), (1, 2, 1)) )/3 + \ + JzKetCoupled(1, -1, (1, 1, 1), ((1, 3, 1), (1, 2, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/30 + \ + JzKetCoupled(2, -1, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/2 + \ + sqrt(3)*JzKetCoupled(2, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/6 + \ + sqrt(15)*JzKetCoupled(3, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + -sqrt(2)*JzKetCoupled(2, 2, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/2 - \ + sqrt(6)*JzKetCoupled(2, 2, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/6 + \ + sqrt(3)*JzKetCoupled(3, 2, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/3 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + -sqrt(3)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 3, 0), (1, 2, 1)) )/3 + \ + sqrt(15)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/15 - \ + sqrt(3)*JzKetCoupled(2, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/3 + \ + 2*sqrt(15)*JzKetCoupled(3, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, 1), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + sqrt(6)*JzKetCoupled(0, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 0)) )/6 - \ + JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/10 + \ + sqrt(3)*JzKetCoupled(2, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/6 - \ + JzKetCoupled(2, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/2 + \ + sqrt(10)*JzKetCoupled(3, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/10 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + -JzKetCoupled(1, 1, (1, 1, 1), ((1, 3, 1), (1, 2, 1)) )/2 - \ + sqrt(15)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/10 - \ + JzKetCoupled(2, 1, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/2 + \ + sqrt(3)*JzKetCoupled(2, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/6 + \ + 2*sqrt(15)*JzKetCoupled(3, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + -sqrt(3)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 0), (1, 2, 1)) )/3 - \ + 2*sqrt(15)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/15 + \ + sqrt(10)*JzKetCoupled(3, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/5 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, 0), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + -JzKetCoupled(1, -1, (1, 1, 1), ((1, 3, 1), (1, 2, 1)) )/2 - \ + sqrt(15)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/10 + \ + JzKetCoupled(2, -1, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/2 - \ + sqrt(3)*JzKetCoupled(2, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/6 + \ + 2*sqrt(15)*JzKetCoupled(3, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + -sqrt(6)*JzKetCoupled(0, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 0)) )/6 - \ + JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/10 - \ + sqrt(3)*JzKetCoupled(2, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/6 + \ + JzKetCoupled(2, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/2 + \ + sqrt(10)*JzKetCoupled(3, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/10 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + -sqrt(3)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 3, 0), (1, 2, 1)) )/3 + \ + sqrt(15)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/15 + \ + sqrt(3)*JzKetCoupled(2, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/3 + \ + 2*sqrt(15)*JzKetCoupled(3, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/15 + assert couple(TensorProduct(JzKet(1, 0), JzKet(1, -1), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + sqrt(2)*JzKetCoupled(2, -2, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/2 + \ + sqrt(6)*JzKetCoupled(2, -2, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/6 + \ + sqrt(3)*JzKetCoupled(3, -2, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/3 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + sqrt(3)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 3, 0), (1, 2, 1)) )/3 + \ + JzKetCoupled(1, 1, (1, 1, 1), ((1, 3, 1), (1, 2, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/30 - \ + JzKetCoupled(2, 1, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/2 - \ + sqrt(3)*JzKetCoupled(2, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/6 + \ + sqrt(15)*JzKetCoupled(3, 1, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/15 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + -sqrt(6)*JzKetCoupled(0, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 0)) )/6 + \ + JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/10 - \ + sqrt(3)*JzKetCoupled(2, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/6 - \ + JzKetCoupled(2, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/2 + \ + sqrt(10)*JzKetCoupled(3, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/10 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, 1), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + sqrt(15)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/5 - \ + sqrt(3)*JzKetCoupled(2, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/3 + \ + sqrt(15)*JzKetCoupled(3, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/15 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + sqrt(6)*JzKetCoupled(0, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 0)) )/6 + \ + sqrt(3)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 0), (1, 2, 1)) )/3 - \ + sqrt(15)*JzKetCoupled(1, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/15 - \ + sqrt(3)*JzKetCoupled(2, 0, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/3 + \ + sqrt(10)*JzKetCoupled(3, 0, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/10 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + JzKetCoupled(1, -1, (1, 1, 1), ((1, 3, 1), (1, 2, 1)) )/2 - \ + sqrt(15)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/10 - \ + JzKetCoupled(2, -1, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/2 - \ + sqrt(3)*JzKetCoupled(2, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/6 + \ + 2*sqrt(15)*JzKetCoupled(3, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/15 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, 0), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + -sqrt(6)*JzKetCoupled(2, -2, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/3 + \ + sqrt(3)*JzKetCoupled(3, -2, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/3 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, 1)), ((1, 3), (1, 2)) ) == \ + sqrt(3)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 3, 0), (1, 2, 1)) )/3 - \ + JzKetCoupled(1, -1, (1, 1, 1), ((1, 3, 1), (1, 2, 1)) )/2 + \ + sqrt(15)*JzKetCoupled(1, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 1)) )/30 - \ + JzKetCoupled(2, -1, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/2 + \ + sqrt(3)*JzKetCoupled(2, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/6 + \ + sqrt(15)*JzKetCoupled(3, -1, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/15 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, 0)), ((1, 3), (1, 2)) ) == \ + -sqrt(2)*JzKetCoupled(2, -2, (1, 1, 1), ((1, 3, 1), (1, 2, 2)) )/2 + \ + sqrt(6)*JzKetCoupled(2, -2, (1, 1, 1), ((1, 3, 2), (1, 2, 2)) )/6 + \ + sqrt(3)*JzKetCoupled(3, -2, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) )/3 + assert couple(TensorProduct(JzKet(1, -1), JzKet(1, -1), JzKet(1, -1)), ((1, 3), (1, 2)) ) == \ + JzKetCoupled(3, -3, (1, 1, 1), ((1, 3, 2), (1, 2, 3)) ) + # j1=1/2, j2=1/2, j3=3/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), Rational(3, 2))), ((1, 3), (1, 2)) ) == \ + JzKetCoupled(Rational(5, 2), S( + 5)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(5, 2))) ) + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), S.Half)), ((1, 3), (1, 2)) ) == \ + JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, Rational(3, 2))) )/2 - \ + sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(3, 2))) )/10 + \ + sqrt(15)*JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S.Half, S(3) + /2), ((1, 3, 2), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), Rational(-1, 2))), ((1, 3), (1, 2)) ) == \ + -sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, S.Half)) )/6 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, Rational(3, 2))) )/3 - \ + sqrt(5)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(3, 2))) )/5 + \ + sqrt(30)*JzKetCoupled(Rational(5, 2), S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), Rational(-3, 2))), ((1, 3), (1, 2)) ) == \ + -sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, S.Half)) )/2 + \ + JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, Rational(3, 2))) )/2 - \ + sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(3, 2))) )/10 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), -S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), Rational(3, 2))), ((1, 3), (1, 2)) ) == \ + 2*sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(3, 2))) )/5 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S.Half, S(3)/ + 2), ((1, 3, 2), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), S.Half)), ((1, 3), (1, 2)) ) == \ + sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, S.Half)) )/6 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, Rational(3, 2))) )/6 + \ + 3*sqrt(5)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(3, 2))) )/10 + \ + sqrt(30)*JzKetCoupled(Rational(5, 2), S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), Rational(-1, 2))), ((1, 3), (1, 2)) ) == \ + sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, S.Half)) )/6 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, Rational(3, 2))) )/3 + \ + sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(3, 2))) )/5 + \ + sqrt(30)*JzKetCoupled(Rational(5, 2), -S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), Rational(-3, 2))), ((1, 3), (1, 2)) ) == \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, Rational(3, 2))) )/2 + \ + sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(3, 2))) )/10 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S.Half, S(3) + /2), ((1, 3, 2), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), Rational(3, 2))), ((1, 3), (1, 2)) ) == \ + -sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, Rational(3, 2))) )/2 - \ + sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(3, 2))) )/10 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S.Half, S(3)/ + 2), ((1, 3, 2), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), S.Half)), ((1, 3), (1, 2)) ) == \ + sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, S.Half)) )/6 - \ + sqrt(3)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, Rational(3, 2))) )/3 - \ + sqrt(5)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(3, 2))) )/5 + \ + sqrt(30)*JzKetCoupled(Rational(5, 2), S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), Rational(-1, 2))), ((1, 3), (1, 2)) ) == \ + sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, S.Half)) )/6 - \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, Rational(3, 2))) )/6 - \ + 3*sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(3, 2))) )/10 + \ + sqrt(30)*JzKetCoupled(Rational(5, 2), -S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(Rational(3, 2), Rational(-3, 2))), ((1, 3), (1, 2)) ) == \ + -2*sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(3, 2))) )/5 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S.Half, S(3) + /2), ((1, 3, 2), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), Rational(3, 2))), ((1, 3), (1, 2)) ) == \ + -sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, S.Half)) )/2 - \ + JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, Rational(3, 2))) )/2 + \ + sqrt(15)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(3, 2))) )/10 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), S.Half)), ((1, 3), (1, 2)) ) == \ + -sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, S.Half)) )/6 - \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, Rational(3, 2))) )/3 + \ + sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(3, 2))) )/5 + \ + sqrt(30)*JzKetCoupled(Rational(5, 2), -S( + 1)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), Rational(-1, 2))), ((1, 3), (1, 2)) ) == \ + -JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 1), (1, 2, Rational(3, 2))) )/2 + \ + sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(3, 2))) )/10 + \ + sqrt(15)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S.Half, S( + 3)/2), ((1, 3, 2), (1, 2, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(Rational(3, 2), Rational(-3, 2))), ((1, 3), (1, 2)) ) == \ + JzKetCoupled(Rational(5, 2), -S( + 5)/2, (S.Half, S.Half, Rational(3, 2)), ((1, 3, 2), (1, 2, Rational(5, 2))) ) + + +def test_couple_4_states_numerical(): + # Default coupling + # j1=1/2, j2=1/2, j3=1/2, j4=1/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half))) == \ + JzKetCoupled(2, 2, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) ) + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)))) == \ + sqrt(3)*JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 1)) )/2 + \ + JzKetCoupled(2, 1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half))) == \ + sqrt(6)*JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 1)) )/3 - \ + sqrt(3)*JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 1)) )/6 + \ + JzKetCoupled(2, 1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)))) == \ + sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 0)) )/3 + \ + sqrt(3)*JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 1)) )/3 + \ + sqrt(6)*JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 1)) )/6 + \ + sqrt(6)*JzKetCoupled(2, 0, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half))) == \ + sqrt(2)*JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (1, 3, S.Half), (1, 4, 1)) )/2 - \ + sqrt(6)*JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 1)) )/6 - \ + sqrt(3)*JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 1)) )/6 + \ + JzKetCoupled(2, 1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), + JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)))) == \ + JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), + ((1, 2, 0), (1, 3, S.Half), (1, 4, 0)))/2 - \ + sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), + ((1, 2, 1), (1, 3, S.Half), (1, 4, 0)))/6 + \ + JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), + ((1, 2, 0), (1, 3, S.Half), (1, 4, 1)))/2 - \ + sqrt(3)*JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), + ((1, 2, 1), (1, 3, S.Half), (1, 4, 1)))/6 + \ + sqrt(6)*JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), + ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 1)))/6 + \ + sqrt(6)*JzKetCoupled(2, 0, (S.Half, S.Half, S.Half, S.Half), + ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)))/6 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half))) == \ + -JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (1, 3, S.Half), (1, 4, 0)) )/2 - \ + sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 0)) )/6 + \ + JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (1, 3, S.Half), (1, 4, 1)) )/2 + \ + sqrt(3)*JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 1)) )/6 - \ + sqrt(6)*JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 1)) )/6 + \ + sqrt(6)*JzKetCoupled(2, 0, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)))) == \ + sqrt(2)*JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (1, 3, S.Half), (1, 4, 1)) )/2 + \ + sqrt(6)*JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 1)) )/6 + \ + sqrt(3)*JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 1)) )/6 + \ + JzKetCoupled(2, -1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half))) == \ + -sqrt(2)*JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (1, 3, S.Half), (1, 4, 1)) )/2 - \ + sqrt(6)*JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 1)) )/6 - \ + sqrt(3)*JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 1)) )/6 + \ + JzKetCoupled(2, 1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)))) == \ + -JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (1, 3, S.Half), (1, 4, 0)) )/2 - \ + sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 0)) )/6 - \ + JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (1, 3, S.Half), (1, 4, 1)) )/2 - \ + sqrt(3)*JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 1)) )/6 + \ + sqrt(6)*JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 1)) )/6 + \ + sqrt(6)*JzKetCoupled(2, 0, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half))) == \ + JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (1, 3, S.Half), (1, 4, 0)) )/2 - \ + sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 0)) )/6 - \ + JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (1, 3, S.Half), (1, 4, 1)) )/2 + \ + sqrt(3)*JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 1)) )/6 - \ + sqrt(6)*JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 1)) )/6 + \ + sqrt(6)*JzKetCoupled(2, 0, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)))) == \ + -sqrt(2)*JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (1, 3, S.Half), (1, 4, 1)) )/2 + \ + sqrt(6)*JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 1)) )/6 + \ + sqrt(3)*JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 1)) )/6 + \ + JzKetCoupled(2, -1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half))) == \ + sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 0)) )/3 - \ + sqrt(3)*JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 1)) )/3 - \ + sqrt(6)*JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 1)) )/6 + \ + sqrt(6)*JzKetCoupled(2, 0, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)))) == \ + -sqrt(6)*JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, S.Half), (1, 4, 1)) )/3 + \ + sqrt(3)*JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 1)) )/6 + \ + JzKetCoupled(2, -1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half))) == \ + -sqrt(3)*JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 1)) )/2 + \ + JzKetCoupled(2, -1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)))) == \ + JzKetCoupled(2, -2, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, 2)) ) + # j1=S.Half, S.Half, S.Half, 1 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1))) == \ + JzKetCoupled(Rational(5, 2), Rational(5, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) ) + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0))) == \ + sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1))) == \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, S.Half)) )/2 + \ + sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1))) == \ + sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/3 - \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0))) == \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, S.Half)) )/3 - \ + JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, S.Half)) )/3 + \ + 2*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/3 + \ + sqrt(5)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1))) == \ + 2*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, S.Half)) )/3 + \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, S.Half)) )/6 + \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/3 + \ + 2*sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1))) == \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/2 - \ + sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/6 - \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0))) == \ + sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, S.Half)) )/6 - \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, S.Half)) )/6 - \ + JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, S.Half)) )/3 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/3 - \ + JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/3 + \ + sqrt(5)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1))) == \ + sqrt(3)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, S.Half)) )/3 - \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, S.Half)) )/3 + \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, S.Half)) )/6 + \ + sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/6 - \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/6 + \ + 2*sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1))) == \ + -sqrt(3)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, S.Half)) )/3 - \ + JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, S.Half)) )/3 + \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, S.Half)) )/6 + \ + sqrt(6)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/6 + \ + sqrt(2)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/6 - \ + 2*sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0))) == \ + -sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, S.Half)) )/6 - \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, S.Half)) )/6 - \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, S.Half)) )/3 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/3 + \ + JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/3 - \ + sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1))) == \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/2 + \ + sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/6 + \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1))) == \ + -sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/2 - \ + sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/6 - \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0))) == \ + -sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, S.Half)) )/6 - \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, S.Half)) )/6 - \ + JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, S.Half)) )/3 - \ + sqrt(3)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/3 - \ + JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/3 + \ + sqrt(5)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1))) == \ + -sqrt(3)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, S.Half)) )/3 - \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, S.Half)) )/3 + \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, S.Half)) )/6 - \ + sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/6 - \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/6 + \ + 2*sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1))) == \ + sqrt(3)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, S.Half)) )/3 - \ + JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, S.Half)) )/3 + \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, S.Half)) )/6 - \ + sqrt(6)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/6 + \ + sqrt(2)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/6 - \ + 2*sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0))) == \ + sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, S.Half)) )/6 - \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, S.Half)) )/6 - \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, S.Half)) )/3 - \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/3 + \ + JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/3 - \ + sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1))) == \ + -sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/2 + \ + sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/6 + \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1))) == \ + 2*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, S.Half)) )/3 + \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, S.Half)) )/6 - \ + sqrt(2)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/3 - \ + 2*sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0))) == \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, S.Half)) )/3 - \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, S.Half)) )/3 - \ + 2*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/3 - \ + sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1))) == \ + -sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, S.Half), (1, 4, Rational(3, 2))) )/3 + \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1))) == \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, S.Half)) )/2 - \ + sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0))) == \ + -sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1))) == \ + JzKetCoupled(Rational(5, 2), Rational(-5, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (1, 3, Rational(3, 2)), (1, 4, Rational(5, 2))) ) + # Couple j1 to j2, j3 to j4 + # j1=1/2, j2=1/2, j3=1/2, j4=1/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)), ((1, 2), (3, 4), (1, 3)) ) == \ + JzKetCoupled(2, 2, (S( + 1)/2, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) ) + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(2)*JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 0), (1, 3, 1)) )/2 + \ + JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(2, 1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), ((1, 2), (3, 4), (1, 3)) ) == \ + -sqrt(2)*JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 0), (1, 3, 1)) )/2 + \ + JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(2, 1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 0)) )/3 + \ + sqrt(2)*JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 1)) )/2 + \ + sqrt(6)*JzKetCoupled(2, 0, (S.Half, S.Half, S.Half, S.One/ + 2), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(2)*JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (3, 4, 1), (1, 3, 1)) )/2 - \ + JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(2, 1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), ((1, 2), (3, 4), (1, 3)) ) == \ + JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (3, 4, 0), (1, 3, 0)) )/2 - \ + sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 0)) )/6 + \ + JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (3, 4, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 0), (1, 3, 1)) )/2 + \ + sqrt(6)*JzKetCoupled(2, 0, (S.Half, S.Half, S.Half, S.One/ + 2), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), ((1, 2), (3, 4), (1, 3)) ) == \ + -JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (3, 4, 0), (1, 3, 0)) )/2 - \ + sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 0)) )/6 + \ + JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (3, 4, 1), (1, 3, 1)) )/2 - \ + JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 0), (1, 3, 1)) )/2 + \ + sqrt(6)*JzKetCoupled(2, 0, (S.Half, S.Half, S.Half, S.One/ + 2), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(2)*JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (3, 4, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(2, -1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)), ((1, 2), (3, 4), (1, 3)) ) == \ + -sqrt(2)*JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (3, 4, 1), (1, 3, 1)) )/2 - \ + JzKetCoupled(1, 1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(2, 1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), ((1, 2), (3, 4), (1, 3)) ) == \ + -JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (3, 4, 0), (1, 3, 0)) )/2 - \ + sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 0)) )/6 - \ + JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (3, 4, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 0), (1, 3, 1)) )/2 + \ + sqrt(6)*JzKetCoupled(2, 0, (S.Half, S.Half, S.Half, S.One/ + 2), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), ((1, 2), (3, 4), (1, 3)) ) == \ + JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (3, 4, 0), (1, 3, 0)) )/2 - \ + sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 0)) )/6 - \ + JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (3, 4, 1), (1, 3, 1)) )/2 - \ + JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 0), (1, 3, 1)) )/2 + \ + sqrt(6)*JzKetCoupled(2, 0, (S.Half, S.Half, S.Half, S.One/ + 2), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), ((1, 2), (3, 4), (1, 3)) ) == \ + -sqrt(2)*JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 0), (3, 4, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(2, -1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half)), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(3)*JzKetCoupled(0, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 0)) )/3 - \ + sqrt(2)*JzKetCoupled(1, 0, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 1)) )/2 + \ + sqrt(6)*JzKetCoupled(2, 0, (S.Half, S.Half, S.Half, S.One/ + 2), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) )/6 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(2)*JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 0), (1, 3, 1)) )/2 - \ + JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(2, -1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half)), ((1, 2), (3, 4), (1, 3)) ) == \ + -sqrt(2)*JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 0), (1, 3, 1)) )/2 - \ + JzKetCoupled(1, -1, (S.Half, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 1)) )/2 + \ + JzKetCoupled(2, -1, (S.Half, S( + 1)/2, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) )/2 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2))), ((1, 2), (3, 4), (1, 3)) ) == \ + JzKetCoupled(2, -2, (S( + 1)/2, S.Half, S.Half, S.Half), ((1, 2, 1), (3, 4, 1), (1, 3, 2)) ) + # j1=S.Half, S.Half, S.Half, 1 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1)), ((1, 2), (3, 4), (1, 3)) ) == \ + JzKetCoupled(Rational(5, 2), Rational(5, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) ) + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0)), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 + \ + 2*sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1)), ((1, 2), (3, 4), (1, 3)) ) == \ + 2*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, S.Half)) )/3 + \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, S.Half)) )/6 + \ + sqrt(2)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 + \ + 2*sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1)), ((1, 2), (3, 4), (1, 3)) ) == \ + -sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 + \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0)), ((1, 2), (3, 4), (1, 3)) ) == \ + -sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, S.Half)) )/3 + \ + JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, S.Half)) )/3 - \ + JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 + \ + 4*sqrt(5)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1)), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, S.Half)) )/2 + \ + sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1)), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/2 - \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/10 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0)), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, S.Half), (1, 3, S.Half)) )/6 - \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, S.Half)) )/6 - \ + JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, S.Half)) )/3 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/3 + \ + JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 - \ + sqrt(5)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1)), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(3)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, S.Half), (1, 3, S.Half)) )/3 + \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, S.Half)) )/3 - \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, S.Half)) )/6 + \ + sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/6 + \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 + \ + sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/30 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1)), ((1, 2), (3, 4), (1, 3)) ) == \ + -sqrt(3)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, S.Half), (1, 3, S.Half)) )/3 + \ + JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, S.Half)) )/3 - \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, S.Half)) )/6 + \ + sqrt(6)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/6 - \ + sqrt(2)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 - \ + sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/30 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0)), ((1, 2), (3, 4), (1, 3)) ) == \ + -sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, S.Half), (1, 3, S.Half)) )/6 - \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, S.Half)) )/6 - \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, S.Half)) )/3 + \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/3 - \ + JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 + \ + sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1)), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/2 + \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/10 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 1)), ((1, 2), (3, 4), (1, 3)) ) == \ + -sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/2 - \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/10 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, 0)), ((1, 2), (3, 4), (1, 3)) ) == \ + -sqrt(6)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, S.Half), (1, 3, S.Half)) )/6 - \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, S.Half)) )/6 - \ + JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, S.Half)) )/3 - \ + sqrt(3)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/3 + \ + JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 - \ + sqrt(5)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, S.Half), JzKet(1, -1)), ((1, 2), (3, 4), (1, 3)) ) == \ + -sqrt(3)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, S.Half), (1, 3, S.Half)) )/3 + \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, S.Half)) )/3 - \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, S.Half)) )/6 - \ + sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/6 + \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 + \ + sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/30 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1)), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(3)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, S.Half), (1, 3, S.Half)) )/3 + \ + JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, S.Half)) )/3 - \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, S.Half)) )/6 - \ + sqrt(6)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/6 - \ + sqrt(2)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 - \ + sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/30 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0)), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(6)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, S.Half), (1, 3, S.Half)) )/6 - \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, S.Half)) )/6 - \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, S.Half)) )/3 - \ + sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/3 - \ + JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 + \ + sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1)), ((1, 2), (3, 4), (1, 3)) ) == \ + -sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 0), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/2 + \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/10 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 1)), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(2)*JzKetCoupled(S.Half, S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, S.Half)) )/2 - \ + sqrt(10)*JzKetCoupled(Rational(3, 2), S.Half, (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/5 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), S.Half, (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, 0)), ((1, 2), (3, 4), (1, 3)) ) == \ + -sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, S.Half)) )/3 + \ + JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, S.Half)) )/3 + \ + JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 - \ + 4*sqrt(5)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, S.Half), JzKet(1, -1)), ((1, 2), (3, 4), (1, 3)) ) == \ + sqrt(6)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 - \ + sqrt(30)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(5)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 1)), ((1, 2), (3, 4), (1, 3)) ) == \ + 2*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, S.Half)) )/3 + \ + sqrt(2)*JzKetCoupled(S.Half, Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, S.Half)) )/6 - \ + sqrt(2)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 - \ + 2*sqrt(10)*JzKetCoupled(Rational(3, 2), Rational(-1, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-1, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/10 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, 0)), ((1, 2), (3, 4), (1, 3)) ) == \ + -sqrt(3)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, S.Half), (1, 3, Rational(3, 2))) )/3 - \ + 2*sqrt(15)*JzKetCoupled(Rational(3, 2), Rational(-3, 2), (S.Half, S.Half, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(3, 2))) )/15 + \ + sqrt(10)*JzKetCoupled(Rational(5, 2), Rational(-3, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) )/5 + assert couple(TensorProduct(JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(S.Half, Rational(-1, 2)), JzKet(1, -1)), ((1, 2), (3, 4), (1, 3)) ) == \ + JzKetCoupled(Rational(5, 2), Rational(-5, 2), (S.Half, S( + 1)/2, S.Half, 1), ((1, 2, 1), (3, 4, Rational(3, 2)), (1, 3, Rational(5, 2))) ) + + +def test_couple_symbolic(): + assert couple(TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) == \ + Sum(CG(j1, m1, j2, m2, j, m1 + m2) * JzKetCoupled(j, m1 + m2, ( + j1, j2)), (j, m1 + m2, j1 + j2)) + assert couple(TensorProduct(JzKet(j1, m1), JzKet(j2, m2), JzKet(j3, m3))) == \ + Sum(CG(j1, m1, j2, m2, j12, m1 + m2) * CG(j12, m1 + m2, j3, m3, j, m1 + m2 + m3) * + JzKetCoupled(j, m1 + m2 + m3, (j1, j2, j3), ((1, 2, j12), (1, 3, j)) ), + (j12, m1 + m2, j1 + j2), (j, m1 + m2 + m3, j12 + j3)) + assert couple(TensorProduct(JzKet(j1, m1), JzKet(j2, m2), JzKet(j3, m3)), ((1, 3), (1, 2)) ) == \ + Sum(CG(j1, m1, j3, m3, j13, m1 + m3) * CG(j13, m1 + m3, j2, m2, j, m1 + m2 + m3) * + JzKetCoupled(j, m1 + m2 + m3, (j1, j2, j3), ((1, 3, j13), (1, 2, j)) ), + (j13, m1 + m3, j1 + j3), (j, m1 + m2 + m3, j13 + j2)) + assert couple(TensorProduct(JzKet(j1, m1), JzKet(j2, m2), JzKet(j3, m3), JzKet(j4, m4))) == \ + Sum(CG(j1, m1, j2, m2, j12, m1 + m2) * CG(j12, m1 + m2, j3, m3, j123, m1 + m2 + m3) * CG(j123, m1 + m2 + m3, j4, m4, j, m1 + m2 + m3 + m4) * + JzKetCoupled(j, m1 + m2 + m3 + m4, ( + j1, j2, j3, j4), ((1, 2, j12), (1, 3, j123), (1, 4, j)) ), + (j12, m1 + m2, j1 + j2), (j123, m1 + m2 + m3, j12 + j3), (j, m1 + m2 + m3 + m4, j123 + j4)) + assert couple(TensorProduct(JzKet(j1, m1), JzKet(j2, m2), JzKet(j3, m3), JzKet(j4, m4)), ((1, 2), (3, 4), (1, 3)) ) == \ + Sum(CG(j1, m1, j2, m2, j12, m1 + m2) * CG(j3, m3, j4, m4, j34, m3 + m4) * CG(j12, m1 + m2, j34, m3 + m4, j, m1 + m2 + m3 + m4) * + JzKetCoupled(j, m1 + m2 + m3 + m4, ( + j1, j2, j3, j4), ((1, 2, j12), (3, 4, j34), (1, 3, j)) ), + (j12, m1 + m2, j1 + j2), (j34, m3 + m4, j3 + j4), (j, m1 + m2 + m3 + m4, j12 + j34)) + assert couple(TensorProduct(JzKet(j1, m1), JzKet(j2, m2), JzKet(j3, m3), JzKet(j4, m4)), ((1, 3), (1, 4), (1, 2)) ) == \ + Sum(CG(j1, m1, j3, m3, j13, m1 + m3) * CG(j13, m1 + m3, j4, m4, j134, m1 + m3 + m4) * CG(j134, m1 + m3 + m4, j2, m2, j, m1 + m2 + m3 + m4) * + JzKetCoupled(j, m1 + m2 + m3 + m4, ( + j1, j2, j3, j4), ((1, 3, j13), (1, 4, j134), (1, 2, j)) ), + (j13, m1 + m3, j1 + j3), (j134, m1 + m3 + m4, j13 + j4), (j, m1 + m2 + m3 + m4, j134 + j2)) + + +def test_innerproduct(): + assert InnerProduct(JzBra(1, 1), JzKet(1, 1)).doit() == 1 + assert InnerProduct( + JzBra(S.Half, S.Half), JzKet(S.Half, Rational(-1, 2))).doit() == 0 + assert InnerProduct(JzBra(j, m), JzKet(j, m)).doit() == 1 + assert InnerProduct(JzBra(1, 0), JyKet(1, 1)).doit() == I/sqrt(2) + assert InnerProduct( + JxBra(S.Half, S.Half), JzKet(S.Half, S.Half)).doit() == -sqrt(2)/2 + assert InnerProduct(JyBra(1, 1), JzKet(1, 1)).doit() == S.Half + assert InnerProduct(JxBra(1, -1), JyKet(1, 1)).doit() == 0 + + +def test_rotation_small_d(): + # Symbolic tests + # j = 1/2 + assert Rotation.d(S.Half, S.Half, S.Half, beta).doit() == cos(beta/2) + assert Rotation.d(S.Half, S.Half, Rational(-1, 2), beta).doit() == -sin(beta/2) + assert Rotation.d(S.Half, Rational(-1, 2), S.Half, beta).doit() == sin(beta/2) + assert Rotation.d(S.Half, Rational(-1, 2), Rational(-1, 2), beta).doit() == cos(beta/2) + # j = 1 + assert Rotation.d(1, 1, 1, beta).doit() == (1 + cos(beta))/2 + assert Rotation.d(1, 1, 0, beta).doit() == -sin(beta)/sqrt(2) + assert Rotation.d(1, 1, -1, beta).doit() == (1 - cos(beta))/2 + assert Rotation.d(1, 0, 1, beta).doit() == sin(beta)/sqrt(2) + assert Rotation.d(1, 0, 0, beta).doit() == cos(beta) + assert Rotation.d(1, 0, -1, beta).doit() == -sin(beta)/sqrt(2) + assert Rotation.d(1, -1, 1, beta).doit() == (1 - cos(beta))/2 + assert Rotation.d(1, -1, 0, beta).doit() == sin(beta)/sqrt(2) + assert Rotation.d(1, -1, -1, beta).doit() == (1 + cos(beta))/2 + # j = 3/2 + assert Rotation.d(S( + 3)/2, Rational(3, 2), Rational(3, 2), beta).doit() == (3*cos(beta/2) + cos(beta*Rational(3, 2)))/4 + assert Rotation.d(Rational(3, 2), S( + 3)/2, S.Half, beta).doit() == -sqrt(3)*(sin(beta/2) + sin(beta*Rational(3, 2)))/4 + assert Rotation.d(Rational(3, 2), S( + 3)/2, Rational(-1, 2), beta).doit() == sqrt(3)*(cos(beta/2) - cos(beta*Rational(3, 2)))/4 + assert Rotation.d(Rational(3, 2), S( + 3)/2, Rational(-3, 2), beta).doit() == (-3*sin(beta/2) + sin(beta*Rational(3, 2)))/4 + assert Rotation.d(Rational(3, 2), S( + 1)/2, Rational(3, 2), beta).doit() == sqrt(3)*(sin(beta/2) + sin(beta*Rational(3, 2)))/4 + assert Rotation.d(S( + 3)/2, S.Half, S.Half, beta).doit() == (cos(beta/2) + 3*cos(beta*Rational(3, 2)))/4 + assert Rotation.d(S( + 3)/2, S.Half, Rational(-1, 2), beta).doit() == (sin(beta/2) - 3*sin(beta*Rational(3, 2)))/4 + assert Rotation.d(Rational(3, 2), S( + 1)/2, Rational(-3, 2), beta).doit() == sqrt(3)*(cos(beta/2) - cos(beta*Rational(3, 2)))/4 + assert Rotation.d(Rational(3, 2), -S( + 1)/2, Rational(3, 2), beta).doit() == sqrt(3)*(cos(beta/2) - cos(beta*Rational(3, 2)))/4 + assert Rotation.d(Rational(3, 2), -S( + 1)/2, S.Half, beta).doit() == (-sin(beta/2) + 3*sin(beta*Rational(3, 2)))/4 + assert Rotation.d(Rational(3, 2), -S( + 1)/2, Rational(-1, 2), beta).doit() == (cos(beta/2) + 3*cos(beta*Rational(3, 2)))/4 + assert Rotation.d(Rational(3, 2), -S( + 1)/2, Rational(-3, 2), beta).doit() == -sqrt(3)*(sin(beta/2) + sin(beta*Rational(3, 2)))/4 + assert Rotation.d(S( + 3)/2, Rational(-3, 2), Rational(3, 2), beta).doit() == (3*sin(beta/2) - sin(beta*Rational(3, 2)))/4 + assert Rotation.d(Rational(3, 2), -S( + 3)/2, S.Half, beta).doit() == sqrt(3)*(cos(beta/2) - cos(beta*Rational(3, 2)))/4 + assert Rotation.d(Rational(3, 2), -S( + 3)/2, Rational(-1, 2), beta).doit() == sqrt(3)*(sin(beta/2) + sin(beta*Rational(3, 2)))/4 + assert Rotation.d(Rational(3, 2), -S( + 3)/2, Rational(-3, 2), beta).doit() == (3*cos(beta/2) + cos(beta*Rational(3, 2)))/4 + # j = 2 + assert Rotation.d(2, 2, 2, beta).doit() == (3 + 4*cos(beta) + cos(2*beta))/8 + assert Rotation.d(2, 2, 1, beta).doit() == -((cos(beta) + 1)*sin(beta))/2 + assert Rotation.d(2, 2, 0, beta).doit() == sqrt(6)*sin(beta)**2/4 + assert Rotation.d(2, 2, -1, beta).doit() == (cos(beta) - 1)*sin(beta)/2 + assert Rotation.d(2, 2, -2, beta).doit() == (3 - 4*cos(beta) + cos(2*beta))/8 + assert Rotation.d(2, 1, 2, beta).doit() == (cos(beta) + 1)*sin(beta)/2 + assert Rotation.d(2, 1, 1, beta).doit() == (cos(beta) + cos(2*beta))/2 + assert Rotation.d(2, 1, 0, beta).doit() == -sqrt(6)*sin(2*beta)/4 + assert Rotation.d(2, 1, -1, beta).doit() == (cos(beta) - cos(2*beta))/2 + assert Rotation.d(2, 1, -2, beta).doit() == (cos(beta) - 1)*sin(beta)/2 + assert Rotation.d(2, 0, 2, beta).doit() == sqrt(6)*sin(beta)**2/4 + assert Rotation.d(2, 0, 1, beta).doit() == sqrt(6)*sin(2*beta)/4 + assert Rotation.d(2, 0, 0, beta).doit() == (1 + 3*cos(2*beta))/4 + assert Rotation.d(2, 0, -1, beta).doit() == -sqrt(6)*sin(2*beta)/4 + assert Rotation.d(2, 0, -2, beta).doit() == sqrt(6)*sin(beta)**2/4 + assert Rotation.d(2, -1, 2, beta).doit() == (2*sin(beta) - sin(2*beta))/4 + assert Rotation.d(2, -1, 1, beta).doit() == (cos(beta) - cos(2*beta))/2 + assert Rotation.d(2, -1, 0, beta).doit() == sqrt(6)*sin(2*beta)/4 + assert Rotation.d(2, -1, -1, beta).doit() == (cos(beta) + cos(2*beta))/2 + assert Rotation.d(2, -1, -2, beta).doit() == -((cos(beta) + 1)*sin(beta))/2 + assert Rotation.d(2, -2, 2, beta).doit() == (3 - 4*cos(beta) + cos(2*beta))/8 + assert Rotation.d(2, -2, 1, beta).doit() == (2*sin(beta) - sin(2*beta))/4 + assert Rotation.d(2, -2, 0, beta).doit() == sqrt(6)*sin(beta)**2/4 + assert Rotation.d(2, -2, -1, beta).doit() == (cos(beta) + 1)*sin(beta)/2 + assert Rotation.d(2, -2, -2, beta).doit() == (3 + 4*cos(beta) + cos(2*beta))/8 + # Numerical tests + # j = 1/2 + assert Rotation.d(S.Half, S.Half, S.Half, pi/2).doit() == sqrt(2)/2 + assert Rotation.d(S.Half, S.Half, Rational(-1, 2), pi/2).doit() == -sqrt(2)/2 + assert Rotation.d(S.Half, Rational(-1, 2), S.Half, pi/2).doit() == sqrt(2)/2 + assert Rotation.d(S.Half, Rational(-1, 2), Rational(-1, 2), pi/2).doit() == sqrt(2)/2 + # j = 1 + assert Rotation.d(1, 1, 1, pi/2).doit() == S.Half + assert Rotation.d(1, 1, 0, pi/2).doit() == -sqrt(2)/2 + assert Rotation.d(1, 1, -1, pi/2).doit() == S.Half + assert Rotation.d(1, 0, 1, pi/2).doit() == sqrt(2)/2 + assert Rotation.d(1, 0, 0, pi/2).doit() == 0 + assert Rotation.d(1, 0, -1, pi/2).doit() == -sqrt(2)/2 + assert Rotation.d(1, -1, 1, pi/2).doit() == S.Half + assert Rotation.d(1, -1, 0, pi/2).doit() == sqrt(2)/2 + assert Rotation.d(1, -1, -1, pi/2).doit() == S.Half + # j = 3/2 + assert Rotation.d(Rational(3, 2), Rational(3, 2), Rational(3, 2), pi/2).doit() == sqrt(2)/4 + assert Rotation.d(Rational(3, 2), Rational(3, 2), S.Half, pi/2).doit() == -sqrt(6)/4 + assert Rotation.d(Rational(3, 2), Rational(3, 2), Rational(-1, 2), pi/2).doit() == sqrt(6)/4 + assert Rotation.d(Rational(3, 2), Rational(3, 2), Rational(-3, 2), pi/2).doit() == -sqrt(2)/4 + assert Rotation.d(Rational(3, 2), S.Half, Rational(3, 2), pi/2).doit() == sqrt(6)/4 + assert Rotation.d(Rational(3, 2), S.Half, S.Half, pi/2).doit() == -sqrt(2)/4 + assert Rotation.d(Rational(3, 2), S.Half, Rational(-1, 2), pi/2).doit() == -sqrt(2)/4 + assert Rotation.d(Rational(3, 2), S.Half, Rational(-3, 2), pi/2).doit() == sqrt(6)/4 + assert Rotation.d(Rational(3, 2), Rational(-1, 2), Rational(3, 2), pi/2).doit() == sqrt(6)/4 + assert Rotation.d(Rational(3, 2), Rational(-1, 2), S.Half, pi/2).doit() == sqrt(2)/4 + assert Rotation.d(Rational(3, 2), Rational(-1, 2), Rational(-1, 2), pi/2).doit() == -sqrt(2)/4 + assert Rotation.d(Rational(3, 2), Rational(-1, 2), Rational(-3, 2), pi/2).doit() == -sqrt(6)/4 + assert Rotation.d(Rational(3, 2), Rational(-3, 2), Rational(3, 2), pi/2).doit() == sqrt(2)/4 + assert Rotation.d(Rational(3, 2), Rational(-3, 2), S.Half, pi/2).doit() == sqrt(6)/4 + assert Rotation.d(Rational(3, 2), Rational(-3, 2), Rational(-1, 2), pi/2).doit() == sqrt(6)/4 + assert Rotation.d(Rational(3, 2), Rational(-3, 2), Rational(-3, 2), pi/2).doit() == sqrt(2)/4 + # j = 2 + assert Rotation.d(2, 2, 2, pi/2).doit() == Rational(1, 4) + assert Rotation.d(2, 2, 1, pi/2).doit() == Rational(-1, 2) + assert Rotation.d(2, 2, 0, pi/2).doit() == sqrt(6)/4 + assert Rotation.d(2, 2, -1, pi/2).doit() == Rational(-1, 2) + assert Rotation.d(2, 2, -2, pi/2).doit() == Rational(1, 4) + assert Rotation.d(2, 1, 2, pi/2).doit() == S.Half + assert Rotation.d(2, 1, 1, pi/2).doit() == Rational(-1, 2) + assert Rotation.d(2, 1, 0, pi/2).doit() == 0 + assert Rotation.d(2, 1, -1, pi/2).doit() == S.Half + assert Rotation.d(2, 1, -2, pi/2).doit() == Rational(-1, 2) + assert Rotation.d(2, 0, 2, pi/2).doit() == sqrt(6)/4 + assert Rotation.d(2, 0, 1, pi/2).doit() == 0 + assert Rotation.d(2, 0, 0, pi/2).doit() == Rational(-1, 2) + assert Rotation.d(2, 0, -1, pi/2).doit() == 0 + assert Rotation.d(2, 0, -2, pi/2).doit() == sqrt(6)/4 + assert Rotation.d(2, -1, 2, pi/2).doit() == S.Half + assert Rotation.d(2, -1, 1, pi/2).doit() == S.Half + assert Rotation.d(2, -1, 0, pi/2).doit() == 0 + assert Rotation.d(2, -1, -1, pi/2).doit() == Rational(-1, 2) + assert Rotation.d(2, -1, -2, pi/2).doit() == Rational(-1, 2) + assert Rotation.d(2, -2, 2, pi/2).doit() == Rational(1, 4) + assert Rotation.d(2, -2, 1, pi/2).doit() == S.Half + assert Rotation.d(2, -2, 0, pi/2).doit() == sqrt(6)/4 + assert Rotation.d(2, -2, -1, pi/2).doit() == S.Half + assert Rotation.d(2, -2, -2, pi/2).doit() == Rational(1, 4) + + +def test_rotation_d(): + # Symbolic tests + # j = 1/2 + assert Rotation.D(S.Half, S.Half, S.Half, alpha, beta, gamma).doit() == \ + cos(beta/2)*exp(-I*alpha/2)*exp(-I*gamma/2) + assert Rotation.D(S.Half, S.Half, Rational(-1, 2), alpha, beta, gamma).doit() == \ + -sin(beta/2)*exp(-I*alpha/2)*exp(I*gamma/2) + assert Rotation.D(S.Half, Rational(-1, 2), S.Half, alpha, beta, gamma).doit() == \ + sin(beta/2)*exp(I*alpha/2)*exp(-I*gamma/2) + assert Rotation.D(S.Half, Rational(-1, 2), Rational(-1, 2), alpha, beta, gamma).doit() == \ + cos(beta/2)*exp(I*alpha/2)*exp(I*gamma/2) + # j = 1 + assert Rotation.D(1, 1, 1, alpha, beta, gamma).doit() == \ + (1 + cos(beta))/2*exp(-I*alpha)*exp(-I*gamma) + assert Rotation.D(1, 1, 0, alpha, beta, gamma).doit() == -sin( + beta)/sqrt(2)*exp(-I*alpha) + assert Rotation.D(1, 1, -1, alpha, beta, gamma).doit() == \ + (1 - cos(beta))/2*exp(-I*alpha)*exp(I*gamma) + assert Rotation.D(1, 0, 1, alpha, beta, gamma).doit() == \ + sin(beta)/sqrt(2)*exp(-I*gamma) + assert Rotation.D(1, 0, 0, alpha, beta, gamma).doit() == cos(beta) + assert Rotation.D(1, 0, -1, alpha, beta, gamma).doit() == \ + -sin(beta)/sqrt(2)*exp(I*gamma) + assert Rotation.D(1, -1, 1, alpha, beta, gamma).doit() == \ + (1 - cos(beta))/2*exp(I*alpha)*exp(-I*gamma) + assert Rotation.D(1, -1, 0, alpha, beta, gamma).doit() == \ + sin(beta)/sqrt(2)*exp(I*alpha) + assert Rotation.D(1, -1, -1, alpha, beta, gamma).doit() == \ + (1 + cos(beta))/2*exp(I*alpha)*exp(I*gamma) + # j = 3/2 + assert Rotation.D(Rational(3, 2), Rational(3, 2), Rational(3, 2), alpha, beta, gamma).doit() == \ + (3*cos(beta/2) + cos(beta*Rational(3, 2)))/4*exp(I*alpha*Rational(-3, 2))*exp(I*gamma*Rational(-3, 2)) + assert Rotation.D(Rational(3, 2), Rational(3, 2), S.Half, alpha, beta, gamma).doit() == \ + -sqrt(3)*(sin(beta/2) + sin(beta*Rational(3, 2)))/4*exp(I*alpha*Rational(-3, 2))*exp(-I*gamma/2) + assert Rotation.D(Rational(3, 2), Rational(3, 2), Rational(-1, 2), alpha, beta, gamma).doit() == \ + sqrt(3)*(cos(beta/2) - cos(beta*Rational(3, 2)))/4*exp(I*alpha*Rational(-3, 2))*exp(I*gamma/2) + assert Rotation.D(Rational(3, 2), Rational(3, 2), Rational(-3, 2), alpha, beta, gamma).doit() == \ + (-3*sin(beta/2) + sin(beta*Rational(3, 2)))/4*exp(I*alpha*Rational(-3, 2))*exp(I*gamma*Rational(3, 2)) + assert Rotation.D(Rational(3, 2), S.Half, Rational(3, 2), alpha, beta, gamma).doit() == \ + sqrt(3)*(sin(beta/2) + sin(beta*Rational(3, 2)))/4*exp(-I*alpha/2)*exp(I*gamma*Rational(-3, 2)) + assert Rotation.D(Rational(3, 2), S.Half, S.Half, alpha, beta, gamma).doit() == \ + (cos(beta/2) + 3*cos(beta*Rational(3, 2)))/4*exp(-I*alpha/2)*exp(-I*gamma/2) + assert Rotation.D(Rational(3, 2), S.Half, Rational(-1, 2), alpha, beta, gamma).doit() == \ + (sin(beta/2) - 3*sin(beta*Rational(3, 2)))/4*exp(-I*alpha/2)*exp(I*gamma/2) + assert Rotation.D(Rational(3, 2), S.Half, Rational(-3, 2), alpha, beta, gamma).doit() == \ + sqrt(3)*(cos(beta/2) - cos(beta*Rational(3, 2)))/4*exp(-I*alpha/2)*exp(I*gamma*Rational(3, 2)) + assert Rotation.D(Rational(3, 2), Rational(-1, 2), Rational(3, 2), alpha, beta, gamma).doit() == \ + sqrt(3)*(cos(beta/2) - cos(beta*Rational(3, 2)))/4*exp(I*alpha/2)*exp(I*gamma*Rational(-3, 2)) + assert Rotation.D(Rational(3, 2), Rational(-1, 2), S.Half, alpha, beta, gamma).doit() == \ + (-sin(beta/2) + 3*sin(beta*Rational(3, 2)))/4*exp(I*alpha/2)*exp(-I*gamma/2) + assert Rotation.D(Rational(3, 2), Rational(-1, 2), Rational(-1, 2), alpha, beta, gamma).doit() == \ + (cos(beta/2) + 3*cos(beta*Rational(3, 2)))/4*exp(I*alpha/2)*exp(I*gamma/2) + assert Rotation.D(Rational(3, 2), Rational(-1, 2), Rational(-3, 2), alpha, beta, gamma).doit() == \ + -sqrt(3)*(sin(beta/2) + sin(beta*Rational(3, 2)))/4*exp(I*alpha/2)*exp(I*gamma*Rational(3, 2)) + assert Rotation.D(Rational(3, 2), Rational(-3, 2), Rational(3, 2), alpha, beta, gamma).doit() == \ + (3*sin(beta/2) - sin(beta*Rational(3, 2)))/4*exp(I*alpha*Rational(3, 2))*exp(I*gamma*Rational(-3, 2)) + assert Rotation.D(Rational(3, 2), Rational(-3, 2), S.Half, alpha, beta, gamma).doit() == \ + sqrt(3)*(cos(beta/2) - cos(beta*Rational(3, 2)))/4*exp(I*alpha*Rational(3, 2))*exp(-I*gamma/2) + assert Rotation.D(Rational(3, 2), Rational(-3, 2), Rational(-1, 2), alpha, beta, gamma).doit() == \ + sqrt(3)*(sin(beta/2) + sin(beta*Rational(3, 2)))/4*exp(I*alpha*Rational(3, 2))*exp(I*gamma/2) + assert Rotation.D(Rational(3, 2), Rational(-3, 2), Rational(-3, 2), alpha, beta, gamma).doit() == \ + (3*cos(beta/2) + cos(beta*Rational(3, 2)))/4*exp(I*alpha*Rational(3, 2))*exp(I*gamma*Rational(3, 2)) + # j = 2 + assert Rotation.D(2, 2, 2, alpha, beta, gamma).doit() == \ + (3 + 4*cos(beta) + cos(2*beta))/8*exp(-2*I*alpha)*exp(-2*I*gamma) + assert Rotation.D(2, 2, 1, alpha, beta, gamma).doit() == \ + -((cos(beta) + 1)*exp(-2*I*alpha)*exp(-I*gamma)*sin(beta))/2 + assert Rotation.D(2, 2, 0, alpha, beta, gamma).doit() == \ + sqrt(6)*sin(beta)**2/4*exp(-2*I*alpha) + assert Rotation.D(2, 2, -1, alpha, beta, gamma).doit() == \ + (cos(beta) - 1)*sin(beta)/2*exp(-2*I*alpha)*exp(I*gamma) + assert Rotation.D(2, 2, -2, alpha, beta, gamma).doit() == \ + (3 - 4*cos(beta) + cos(2*beta))/8*exp(-2*I*alpha)*exp(2*I*gamma) + assert Rotation.D(2, 1, 2, alpha, beta, gamma).doit() == \ + (cos(beta) + 1)*sin(beta)/2*exp(-I*alpha)*exp(-2*I*gamma) + assert Rotation.D(2, 1, 1, alpha, beta, gamma).doit() == \ + (cos(beta) + cos(2*beta))/2*exp(-I*alpha)*exp(-I*gamma) + assert Rotation.D(2, 1, 0, alpha, beta, gamma).doit() == -sqrt(6)* \ + sin(2*beta)/4*exp(-I*alpha) + assert Rotation.D(2, 1, -1, alpha, beta, gamma).doit() == \ + (cos(beta) - cos(2*beta))/2*exp(-I*alpha)*exp(I*gamma) + assert Rotation.D(2, 1, -2, alpha, beta, gamma).doit() == \ + (cos(beta) - 1)*sin(beta)/2*exp(-I*alpha)*exp(2*I*gamma) + assert Rotation.D(2, 0, 2, alpha, beta, gamma).doit() == \ + sqrt(6)*sin(beta)**2/4*exp(-2*I*gamma) + assert Rotation.D(2, 0, 1, alpha, beta, gamma).doit() == sqrt(6)* \ + sin(2*beta)/4*exp(-I*gamma) + assert Rotation.D( + 2, 0, 0, alpha, beta, gamma).doit() == (1 + 3*cos(2*beta))/4 + assert Rotation.D(2, 0, -1, alpha, beta, gamma).doit() == -sqrt(6)* \ + sin(2*beta)/4*exp(I*gamma) + assert Rotation.D(2, 0, -2, alpha, beta, gamma).doit() == \ + sqrt(6)*sin(beta)**2/4*exp(2*I*gamma) + assert Rotation.D(2, -1, 2, alpha, beta, gamma).doit() == \ + (2*sin(beta) - sin(2*beta))/4*exp(I*alpha)*exp(-2*I*gamma) + assert Rotation.D(2, -1, 1, alpha, beta, gamma).doit() == \ + (cos(beta) - cos(2*beta))/2*exp(I*alpha)*exp(-I*gamma) + assert Rotation.D(2, -1, 0, alpha, beta, gamma).doit() == sqrt(6)* \ + sin(2*beta)/4*exp(I*alpha) + assert Rotation.D(2, -1, -1, alpha, beta, gamma).doit() == \ + (cos(beta) + cos(2*beta))/2*exp(I*alpha)*exp(I*gamma) + assert Rotation.D(2, -1, -2, alpha, beta, gamma).doit() == \ + -((cos(beta) + 1)*sin(beta))/2*exp(I*alpha)*exp(2*I*gamma) + assert Rotation.D(2, -2, 2, alpha, beta, gamma).doit() == \ + (3 - 4*cos(beta) + cos(2*beta))/8*exp(2*I*alpha)*exp(-2*I*gamma) + assert Rotation.D(2, -2, 1, alpha, beta, gamma).doit() == \ + (2*sin(beta) - sin(2*beta))/4*exp(2*I*alpha)*exp(-I*gamma) + assert Rotation.D(2, -2, 0, alpha, beta, gamma).doit() == \ + sqrt(6)*sin(beta)**2/4*exp(2*I*alpha) + assert Rotation.D(2, -2, -1, alpha, beta, gamma).doit() == \ + (cos(beta) + 1)*sin(beta)/2*exp(2*I*alpha)*exp(I*gamma) + assert Rotation.D(2, -2, -2, alpha, beta, gamma).doit() == \ + (3 + 4*cos(beta) + cos(2*beta))/8*exp(2*I*alpha)*exp(2*I*gamma) + # Numerical tests + # j = 1/2 + assert Rotation.D( + S.Half, S.Half, S.Half, pi/2, pi/2, pi/2).doit() == -I*sqrt(2)/2 + assert Rotation.D( + S.Half, S.Half, Rational(-1, 2), pi/2, pi/2, pi/2).doit() == -sqrt(2)/2 + assert Rotation.D( + S.Half, Rational(-1, 2), S.Half, pi/2, pi/2, pi/2).doit() == sqrt(2)/2 + assert Rotation.D( + S.Half, Rational(-1, 2), Rational(-1, 2), pi/2, pi/2, pi/2).doit() == I*sqrt(2)/2 + # j = 1 + assert Rotation.D(1, 1, 1, pi/2, pi/2, pi/2).doit() == Rational(-1, 2) + assert Rotation.D(1, 1, 0, pi/2, pi/2, pi/2).doit() == I*sqrt(2)/2 + assert Rotation.D(1, 1, -1, pi/2, pi/2, pi/2).doit() == S.Half + assert Rotation.D(1, 0, 1, pi/2, pi/2, pi/2).doit() == -I*sqrt(2)/2 + assert Rotation.D(1, 0, 0, pi/2, pi/2, pi/2).doit() == 0 + assert Rotation.D(1, 0, -1, pi/2, pi/2, pi/2).doit() == -I*sqrt(2)/2 + assert Rotation.D(1, -1, 1, pi/2, pi/2, pi/2).doit() == S.Half + assert Rotation.D(1, -1, 0, pi/2, pi/2, pi/2).doit() == I*sqrt(2)/2 + assert Rotation.D(1, -1, -1, pi/2, pi/2, pi/2).doit() == Rational(-1, 2) + # j = 3/2 + assert Rotation.D( + Rational(3, 2), Rational(3, 2), Rational(3, 2), pi/2, pi/2, pi/2).doit() == I*sqrt(2)/4 + assert Rotation.D( + Rational(3, 2), Rational(3, 2), S.Half, pi/2, pi/2, pi/2).doit() == sqrt(6)/4 + assert Rotation.D( + Rational(3, 2), Rational(3, 2), Rational(-1, 2), pi/2, pi/2, pi/2).doit() == -I*sqrt(6)/4 + assert Rotation.D( + Rational(3, 2), Rational(3, 2), Rational(-3, 2), pi/2, pi/2, pi/2).doit() == -sqrt(2)/4 + assert Rotation.D( + Rational(3, 2), S.Half, Rational(3, 2), pi/2, pi/2, pi/2).doit() == -sqrt(6)/4 + assert Rotation.D( + Rational(3, 2), S.Half, S.Half, pi/2, pi/2, pi/2).doit() == I*sqrt(2)/4 + assert Rotation.D( + Rational(3, 2), S.Half, Rational(-1, 2), pi/2, pi/2, pi/2).doit() == -sqrt(2)/4 + assert Rotation.D( + Rational(3, 2), S.Half, Rational(-3, 2), pi/2, pi/2, pi/2).doit() == I*sqrt(6)/4 + assert Rotation.D( + Rational(3, 2), Rational(-1, 2), Rational(3, 2), pi/2, pi/2, pi/2).doit() == -I*sqrt(6)/4 + assert Rotation.D( + Rational(3, 2), Rational(-1, 2), S.Half, pi/2, pi/2, pi/2).doit() == sqrt(2)/4 + assert Rotation.D( + Rational(3, 2), Rational(-1, 2), Rational(-1, 2), pi/2, pi/2, pi/2).doit() == -I*sqrt(2)/4 + assert Rotation.D( + Rational(3, 2), Rational(-1, 2), Rational(-3, 2), pi/2, pi/2, pi/2).doit() == sqrt(6)/4 + assert Rotation.D( + Rational(3, 2), Rational(-3, 2), Rational(3, 2), pi/2, pi/2, pi/2).doit() == sqrt(2)/4 + assert Rotation.D( + Rational(3, 2), Rational(-3, 2), S.Half, pi/2, pi/2, pi/2).doit() == I*sqrt(6)/4 + assert Rotation.D( + Rational(3, 2), Rational(-3, 2), Rational(-1, 2), pi/2, pi/2, pi/2).doit() == -sqrt(6)/4 + assert Rotation.D( + Rational(3, 2), Rational(-3, 2), Rational(-3, 2), pi/2, pi/2, pi/2).doit() == -I*sqrt(2)/4 + # j = 2 + assert Rotation.D(2, 2, 2, pi/2, pi/2, pi/2).doit() == Rational(1, 4) + assert Rotation.D(2, 2, 1, pi/2, pi/2, pi/2).doit() == -I/2 + assert Rotation.D(2, 2, 0, pi/2, pi/2, pi/2).doit() == -sqrt(6)/4 + assert Rotation.D(2, 2, -1, pi/2, pi/2, pi/2).doit() == I/2 + assert Rotation.D(2, 2, -2, pi/2, pi/2, pi/2).doit() == Rational(1, 4) + assert Rotation.D(2, 1, 2, pi/2, pi/2, pi/2).doit() == I/2 + assert Rotation.D(2, 1, 1, pi/2, pi/2, pi/2).doit() == S.Half + assert Rotation.D(2, 1, 0, pi/2, pi/2, pi/2).doit() == 0 + assert Rotation.D(2, 1, -1, pi/2, pi/2, pi/2).doit() == S.Half + assert Rotation.D(2, 1, -2, pi/2, pi/2, pi/2).doit() == -I/2 + assert Rotation.D(2, 0, 2, pi/2, pi/2, pi/2).doit() == -sqrt(6)/4 + assert Rotation.D(2, 0, 1, pi/2, pi/2, pi/2).doit() == 0 + assert Rotation.D(2, 0, 0, pi/2, pi/2, pi/2).doit() == Rational(-1, 2) + assert Rotation.D(2, 0, -1, pi/2, pi/2, pi/2).doit() == 0 + assert Rotation.D(2, 0, -2, pi/2, pi/2, pi/2).doit() == -sqrt(6)/4 + assert Rotation.D(2, -1, 2, pi/2, pi/2, pi/2).doit() == -I/2 + assert Rotation.D(2, -1, 1, pi/2, pi/2, pi/2).doit() == S.Half + assert Rotation.D(2, -1, 0, pi/2, pi/2, pi/2).doit() == 0 + assert Rotation.D(2, -1, -1, pi/2, pi/2, pi/2).doit() == S.Half + assert Rotation.D(2, -1, -2, pi/2, pi/2, pi/2).doit() == I/2 + assert Rotation.D(2, -2, 2, pi/2, pi/2, pi/2).doit() == Rational(1, 4) + assert Rotation.D(2, -2, 1, pi/2, pi/2, pi/2).doit() == I/2 + assert Rotation.D(2, -2, 0, pi/2, pi/2, pi/2).doit() == -sqrt(6)/4 + assert Rotation.D(2, -2, -1, pi/2, pi/2, pi/2).doit() == -I/2 + assert Rotation.D(2, -2, -2, pi/2, pi/2, pi/2).doit() == Rational(1, 4) + + +def test_wignerd(): + assert Rotation.D( + j, m, mp, alpha, beta, gamma) == WignerD(j, m, mp, alpha, beta, gamma) + assert Rotation.d(j, m, mp, beta) == WignerD(j, m, mp, 0, beta, 0) + +def test_wignerD(): + i,j=symbols('i j') + assert Rotation.D(1, 1, 1, 0, 0, 0) == WignerD(1, 1, 1, 0, 0, 0) + assert Rotation.D(1, 1, 2, 0, 0, 0) == WignerD(1, 1, 2, 0, 0, 0) + assert Rotation.D(1, i**2 - j**2, i**2 - j**2, 0, 0, 0) == WignerD(1, i**2 - j**2, i**2 - j**2, 0, 0, 0) + assert Rotation.D(1, i, i, 0, 0, 0) == WignerD(1, i, i, 0, 0, 0) + assert Rotation.D(1, i, i+1, 0, 0, 0) == WignerD(1, i, i+1, 0, 0, 0) + assert Rotation.D(1, 0, 0, 0, 0, 0) == WignerD(1, 0, 0, 0, 0, 0) + +def test_jplus(): + assert Commutator(Jplus, Jminus).doit() == 2*hbar*Jz + assert Jplus.matrix_element(1, 1, 1, 1) == 0 + assert Jplus.rewrite('xyz') == Jx + I*Jy + # Normal operators, normal states + # Numerical + assert qapply(Jplus*JxKet(1, 1)) == \ + -hbar*sqrt(2)*JxKet(1, 0)/2 + hbar*JxKet(1, 1) + assert qapply(Jplus*JyKet(1, 1)) == \ + hbar*sqrt(2)*JyKet(1, 0)/2 + I*hbar*JyKet(1, 1) + assert qapply(Jplus*JzKet(1, 1)) == 0 + # Symbolic + assert qapply(Jplus*JxKet(j, m)) == \ + Sum(hbar * sqrt(-mi**2 - mi + j**2 + j) * WignerD(j, mi, m, 0, pi/2, 0) * + Sum(WignerD(j, mi1, mi + 1, 0, pi*Rational(3, 2), 0) * JxKet(j, mi1), + (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jplus*JyKet(j, m)) == \ + Sum(hbar * sqrt(j**2 + j - mi**2 - mi) * WignerD(j, mi, m, pi*Rational(3, 2), -pi/2, pi/2) * + Sum(WignerD(j, mi1, mi + 1, pi*Rational(3, 2), pi/2, pi/2) * JyKet(j, mi1), + (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jplus*JzKet(j, m)) == \ + hbar*sqrt(j**2 + j - m**2 - m)*JzKet(j, m + 1) + # Normal operators, coupled states + # Numerical + assert qapply(Jplus*JxKetCoupled(1, 1, (1, 1))) == -hbar*sqrt(2) * \ + JxKetCoupled(1, 0, (1, 1))/2 + hbar*JxKetCoupled(1, 1, (1, 1)) + assert qapply(Jplus*JyKetCoupled(1, 1, (1, 1))) == hbar*sqrt(2) * \ + JyKetCoupled(1, 0, (1, 1))/2 + I*hbar*JyKetCoupled(1, 1, (1, 1)) + assert qapply(Jplus*JzKet(1, 1)) == 0 + # Symbolic + assert qapply(Jplus*JxKetCoupled(j, m, (j1, j2))) == \ + Sum(hbar * sqrt(-mi**2 - mi + j**2 + j) * WignerD(j, mi, m, 0, pi/2, 0) * + Sum( + WignerD( + j, mi1, mi + 1, 0, pi*Rational(3, 2), 0) * JxKetCoupled(j, mi1, (j1, j2)), + (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jplus*JyKetCoupled(j, m, (j1, j2))) == \ + Sum(hbar * sqrt(j**2 + j - mi**2 - mi) * WignerD(j, mi, m, pi*Rational(3, 2), -pi/2, pi/2) * + Sum( + WignerD(j, mi1, mi + 1, pi*Rational(3, 2), pi/2, pi/2) * + JyKetCoupled(j, mi1, (j1, j2)), + (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jplus*JzKetCoupled(j, m, (j1, j2))) == \ + hbar*sqrt(j**2 + j - m**2 - m)*JzKetCoupled(j, m + 1, (j1, j2)) + # Uncoupled operators, uncoupled states + # Numerical + e1 = qapply(TensorProduct(Jplus, 1)*TensorProduct(JxKet(1, 1), JxKet(1, -1))) + e2 = -hbar*sqrt(2)*TensorProduct(JxKet(1, 0), JxKet(1, -1))/2 + \ + hbar*TensorProduct(JxKet(1, 1), JxKet(1, -1)) + assert_simplify_expand(e1, e2) + e1 = qapply(TensorProduct(1, Jplus)*TensorProduct(JxKet(1, 1), JxKet(1, -1))) + e2 = -hbar*TensorProduct(JxKet(1, 1), JxKet(1, -1)) + \ + hbar*sqrt(2)*TensorProduct(JxKet(1, 1), JxKet(1, 0))/2 + assert_simplify_expand(e1, e2) + e1 = qapply(TensorProduct(Jplus, 1)*TensorProduct(JyKet(1, 1), JyKet(1, -1))) + e2 = hbar*sqrt(2)*TensorProduct(JyKet(1, 0), JyKet(1, -1))/2 + \ + hbar*I*TensorProduct(JyKet(1, 1), JyKet(1, -1)) + assert_simplify_expand(e1, e2) + e1 = qapply(TensorProduct(1, Jplus)*TensorProduct(JyKet(1, 1), JyKet(1, -1))) + e2 = -hbar*I*TensorProduct(JyKet(1, 1), JyKet(1, -1)) + \ + hbar*sqrt(2)*TensorProduct(JyKet(1, 1), JyKet(1, 0))/2 + assert_simplify_expand(e1, e2) + assert qapply( + TensorProduct(Jplus, 1)*TensorProduct(JzKet(1, 1), JzKet(1, -1))) == 0 + assert qapply(TensorProduct(1, Jplus)*TensorProduct(JzKet(1, 1), JzKet(1, -1))) == \ + hbar*sqrt(2)*TensorProduct(JzKet(1, 1), JzKet(1, 0)) + # Symbolic + assert qapply(TensorProduct(Jplus, 1)*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) == \ + TensorProduct(Sum(hbar * sqrt(-mi**2 - mi + j1**2 + j1) * WignerD(j1, mi, m1, 0, pi/2, 0) * + Sum(WignerD(j1, mi1, mi + 1, 0, pi*Rational(3, 2), 0) * JxKet(j1, mi1), + (mi1, -j1, j1)), (mi, -j1, j1)), JxKet(j2, m2)) + assert qapply(TensorProduct(1, Jplus)*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) == \ + TensorProduct(JxKet(j1, m1), Sum(hbar * sqrt(-mi**2 - mi + j2**2 + j2) * WignerD(j2, mi, m2, 0, pi/2, 0) * + Sum(WignerD(j2, mi1, mi + 1, 0, pi*Rational(3, 2), 0) * JxKet(j2, mi1), + (mi1, -j2, j2)), (mi, -j2, j2))) + assert qapply(TensorProduct(Jplus, 1)*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) == \ + TensorProduct(Sum(hbar * sqrt(j1**2 + j1 - mi**2 - mi) * WignerD(j1, mi, m1, pi*Rational(3, 2), -pi/2, pi/2) * + Sum(WignerD(j1, mi1, mi + 1, pi*Rational(3, 2), pi/2, pi/2) * JyKet(j1, mi1), + (mi1, -j1, j1)), (mi, -j1, j1)), JyKet(j2, m2)) + assert qapply(TensorProduct(1, Jplus)*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) == \ + TensorProduct(JyKet(j1, m1), Sum(hbar * sqrt(j2**2 + j2 - mi**2 - mi) * WignerD(j2, mi, m2, pi*Rational(3, 2), -pi/2, pi/2) * + Sum(WignerD(j2, mi1, mi + 1, pi*Rational(3, 2), pi/2, pi/2) * JyKet(j2, mi1), + (mi1, -j2, j2)), (mi, -j2, j2))) + assert qapply(TensorProduct(Jplus, 1)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) == \ + hbar*sqrt( + j1**2 + j1 - m1**2 - m1)*TensorProduct(JzKet(j1, m1 + 1), JzKet(j2, m2)) + assert qapply(TensorProduct(1, Jplus)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) == \ + hbar*sqrt( + j2**2 + j2 - m2**2 - m2)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2 + 1)) + + +def test_jminus(): + assert qapply(Jminus*JzKet(1, -1)) == 0 + assert Jminus.matrix_element(1, 0, 1, 1) == sqrt(2)*hbar + assert Jminus.rewrite('xyz') == Jx - I*Jy + # Normal operators, normal states + # Numerical + assert qapply(Jminus*JxKet(1, 1)) == \ + hbar*sqrt(2)*JxKet(1, 0)/2 + hbar*JxKet(1, 1) + assert qapply(Jminus*JyKet(1, 1)) == \ + hbar*sqrt(2)*JyKet(1, 0)/2 - hbar*I*JyKet(1, 1) + assert qapply(Jminus*JzKet(1, 1)) == sqrt(2)*hbar*JzKet(1, 0) + # Symbolic + assert qapply(Jminus*JxKet(j, m)) == \ + Sum(hbar*sqrt(j**2 + j - mi**2 + mi)*WignerD(j, mi, m, 0, pi/2, 0) * + Sum(WignerD(j, mi1, mi - 1, 0, pi*Rational(3, 2), 0)*JxKet(j, mi1), + (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jminus*JyKet(j, m)) == \ + Sum(hbar*sqrt(j**2 + j - mi**2 + mi)*WignerD(j, mi, m, pi*Rational(3, 2), -pi/2, pi/2) * + Sum(WignerD(j, mi1, mi - 1, pi*Rational(3, 2), pi/2, pi/2)*JyKet(j, mi1), + (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jminus*JzKet(j, m)) == \ + hbar*sqrt(j**2 + j - m**2 + m)*JzKet(j, m - 1) + # Normal operators, coupled states + # Numerical + assert qapply(Jminus*JxKetCoupled(1, 1, (1, 1))) == \ + hbar*sqrt(2)*JxKetCoupled(1, 0, (1, 1))/2 + \ + hbar*JxKetCoupled(1, 1, (1, 1)) + assert qapply(Jminus*JyKetCoupled(1, 1, (1, 1))) == \ + hbar*sqrt(2)*JyKetCoupled(1, 0, (1, 1))/2 - \ + hbar*I*JyKetCoupled(1, 1, (1, 1)) + assert qapply(Jminus*JzKetCoupled(1, 1, (1, 1))) == \ + sqrt(2)*hbar*JzKetCoupled(1, 0, (1, 1)) + # Symbolic + assert qapply(Jminus*JxKetCoupled(j, m, (j1, j2))) == \ + Sum(hbar*sqrt(j**2 + j - mi**2 + mi)*WignerD(j, mi, m, 0, pi/2, 0) * + Sum(WignerD(j, mi1, mi - 1, 0, pi*Rational(3, 2), 0)*JxKetCoupled(j, mi1, (j1, j2)), + (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jminus*JyKetCoupled(j, m, (j1, j2))) == \ + Sum(hbar*sqrt(j**2 + j - mi**2 + mi)*WignerD(j, mi, m, pi*Rational(3, 2), -pi/2, pi/2) * + Sum( + WignerD(j, mi1, mi - 1, pi*Rational(3, 2), pi/2, pi/2)* + JyKetCoupled(j, mi1, (j1, j2)), + (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jminus*JzKetCoupled(j, m, (j1, j2))) == \ + hbar*sqrt(j**2 + j - m**2 + m)*JzKetCoupled(j, m - 1, (j1, j2)) + # Uncoupled operators, uncoupled states + # Numerical + e1 = qapply(TensorProduct(Jminus, 1)*TensorProduct(JxKet(1, 1), JxKet(1, -1))) + e2 = hbar*sqrt(2)*TensorProduct(JxKet(1, 0), JxKet(1, -1))/2 + \ + hbar*TensorProduct(JxKet(1, 1), JxKet(1, -1)) + assert_simplify_expand(e1, e2) + e1 = qapply(TensorProduct(1, Jminus)*TensorProduct(JxKet(1, 1), JxKet(1, -1))) + e2 = -hbar*TensorProduct(JxKet(1, 1), JxKet(1, -1)) - \ + hbar*sqrt(2)*TensorProduct(JxKet(1, 1), JxKet(1, 0))/2 + assert_simplify_expand(e1, e2) + e1 = qapply(TensorProduct(Jminus, 1)*TensorProduct(JyKet(1, 1), JyKet(1, -1))) + e2 = hbar*sqrt(2)*TensorProduct(JyKet(1, 0), JyKet(1, -1))/2 - \ + hbar*I*TensorProduct(JyKet(1, 1), JyKet(1, -1)) + assert_simplify_expand(e1, e2) + e1 = qapply(TensorProduct(1, Jminus)*TensorProduct(JyKet(1, 1), JyKet(1, -1))) + e2 = hbar*I*TensorProduct(JyKet(1, 1), JyKet(1, -1)) + \ + hbar*sqrt(2)*TensorProduct(JyKet(1, 1), JyKet(1, 0))/2 + assert_simplify_expand(e1, e2) + assert qapply(TensorProduct(Jminus, 1)*TensorProduct(JzKet(1, 1), JzKet(1, -1))) == \ + sqrt(2)*hbar*TensorProduct(JzKet(1, 0), JzKet(1, -1)) + assert qapply(TensorProduct( + 1, Jminus)*TensorProduct(JzKet(1, 1), JzKet(1, -1))) == 0 + # Symbolic + assert qapply(TensorProduct(Jminus, 1)*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) == \ + TensorProduct(Sum(hbar*sqrt(j1**2 + j1 - mi**2 + mi)*WignerD(j1, mi, m1, 0, pi/2, 0) * + Sum(WignerD(j1, mi1, mi - 1, 0, pi*Rational(3, 2), 0)*JxKet(j1, mi1), + (mi1, -j1, j1)), (mi, -j1, j1)), JxKet(j2, m2)) + assert qapply(TensorProduct(1, Jminus)*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) == \ + TensorProduct(JxKet(j1, m1), Sum(hbar*sqrt(j2**2 + j2 - mi**2 + mi)*WignerD(j2, mi, m2, 0, pi/2, 0) * + Sum(WignerD(j2, mi1, mi - 1, 0, pi*Rational(3, 2), 0)*JxKet(j2, mi1), + (mi1, -j2, j2)), (mi, -j2, j2))) + assert qapply(TensorProduct(Jminus, 1)*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) == \ + TensorProduct(Sum(hbar*sqrt(j1**2 + j1 - mi**2 + mi)*WignerD(j1, mi, m1, pi*Rational(3, 2), -pi/2, pi/2) * + Sum(WignerD(j1, mi1, mi - 1, pi*Rational(3, 2), pi/2, pi/2)*JyKet(j1, mi1), + (mi1, -j1, j1)), (mi, -j1, j1)), JyKet(j2, m2)) + assert qapply(TensorProduct(1, Jminus)*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) == \ + TensorProduct(JyKet(j1, m1), Sum(hbar*sqrt(j2**2 + j2 - mi**2 + mi)*WignerD(j2, mi, m2, pi*Rational(3, 2), -pi/2, pi/2) * + Sum(WignerD(j2, mi1, mi - 1, pi*Rational(3, 2), pi/2, pi/2)*JyKet(j2, mi1), + (mi1, -j2, j2)), (mi, -j2, j2))) + assert qapply(TensorProduct(Jminus, 1)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) == \ + hbar*sqrt( + j1**2 + j1 - m1**2 + m1)*TensorProduct(JzKet(j1, m1 - 1), JzKet(j2, m2)) + assert qapply(TensorProduct(1, Jminus)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) == \ + hbar*sqrt( + j2**2 + j2 - m2**2 + m2)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2 - 1)) + + +def test_j2(): + assert Commutator(J2, Jz).doit() == 0 + assert J2.matrix_element(1, 1, 1, 1) == 2*hbar**2 + # Normal operators, normal states + # Numerical + assert qapply(J2*JxKet(1, 1)) == 2*hbar**2*JxKet(1, 1) + assert qapply(J2*JyKet(1, 1)) == 2*hbar**2*JyKet(1, 1) + assert qapply(J2*JzKet(1, 1)) == 2*hbar**2*JzKet(1, 1) + # Symbolic + assert qapply(J2*JxKet(j, m)) == \ + hbar**2*j**2*JxKet(j, m) + hbar**2*j*JxKet(j, m) + assert qapply(J2*JyKet(j, m)) == \ + hbar**2*j**2*JyKet(j, m) + hbar**2*j*JyKet(j, m) + assert qapply(J2*JzKet(j, m)) == \ + hbar**2*j**2*JzKet(j, m) + hbar**2*j*JzKet(j, m) + # Normal operators, coupled states + # Numerical + assert qapply(J2*JxKetCoupled(1, 1, (1, 1))) == \ + 2*hbar**2*JxKetCoupled(1, 1, (1, 1)) + assert qapply(J2*JyKetCoupled(1, 1, (1, 1))) == \ + 2*hbar**2*JyKetCoupled(1, 1, (1, 1)) + assert qapply(J2*JzKetCoupled(1, 1, (1, 1))) == \ + 2*hbar**2*JzKetCoupled(1, 1, (1, 1)) + # Symbolic + assert qapply(J2*JxKetCoupled(j, m, (j1, j2))) == \ + hbar**2*j**2*JxKetCoupled(j, m, (j1, j2)) + \ + hbar**2*j*JxKetCoupled(j, m, (j1, j2)) + assert qapply(J2*JyKetCoupled(j, m, (j1, j2))) == \ + hbar**2*j**2*JyKetCoupled(j, m, (j1, j2)) + \ + hbar**2*j*JyKetCoupled(j, m, (j1, j2)) + assert qapply(J2*JzKetCoupled(j, m, (j1, j2))) == \ + hbar**2*j**2*JzKetCoupled(j, m, (j1, j2)) + \ + hbar**2*j*JzKetCoupled(j, m, (j1, j2)) + # Uncoupled operators, uncoupled states + # Numerical + assert qapply(TensorProduct(J2, 1)*TensorProduct(JxKet(1, 1), JxKet(1, -1))) == \ + 2*hbar**2*TensorProduct(JxKet(1, 1), JxKet(1, -1)) + assert qapply(TensorProduct(1, J2)*TensorProduct(JxKet(1, 1), JxKet(1, -1))) == \ + 2*hbar**2*TensorProduct(JxKet(1, 1), JxKet(1, -1)) + assert qapply(TensorProduct(J2, 1)*TensorProduct(JyKet(1, 1), JyKet(1, -1))) == \ + 2*hbar**2*TensorProduct(JyKet(1, 1), JyKet(1, -1)) + assert qapply(TensorProduct(1, J2)*TensorProduct(JyKet(1, 1), JyKet(1, -1))) == \ + 2*hbar**2*TensorProduct(JyKet(1, 1), JyKet(1, -1)) + assert qapply(TensorProduct(J2, 1)*TensorProduct(JzKet(1, 1), JzKet(1, -1))) == \ + 2*hbar**2*TensorProduct(JzKet(1, 1), JzKet(1, -1)) + assert qapply(TensorProduct(1, J2)*TensorProduct(JzKet(1, 1), JzKet(1, -1))) == \ + 2*hbar**2*TensorProduct(JzKet(1, 1), JzKet(1, -1)) + # Symbolic + e1 = qapply(TensorProduct(J2, 1)*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) + e2 = hbar**2*j1**2*TensorProduct(JxKet(j1, m1), JxKet(j2, m2)) + \ + hbar**2*j1*TensorProduct(JxKet(j1, m1), JxKet(j2, m2)) + assert_simplify_expand(e1, e2) + e1 = qapply(TensorProduct(1, J2)*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) + e2 = hbar**2*j2**2*TensorProduct(JxKet(j1, m1), JxKet(j2, m2)) + \ + hbar**2*j2*TensorProduct(JxKet(j1, m1), JxKet(j2, m2)) + assert_simplify_expand(e1, e2) + e1 = qapply(TensorProduct(J2, 1)*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) + e2 = hbar**2*j1**2*TensorProduct(JyKet(j1, m1), JyKet(j2, m2)) + \ + hbar**2*j1*TensorProduct(JyKet(j1, m1), JyKet(j2, m2)) + assert_simplify_expand(e1, e2) + e1 = qapply(TensorProduct(1, J2)*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) + e2 = hbar**2*j2**2*TensorProduct(JyKet(j1, m1), JyKet(j2, m2)) + \ + hbar**2*j2*TensorProduct(JyKet(j1, m1), JyKet(j2, m2)) + assert_simplify_expand(e1, e2) + e1 = qapply(TensorProduct(J2, 1)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) + e2 = hbar**2*j1**2*TensorProduct(JzKet(j1, m1), JzKet(j2, m2)) + \ + hbar**2*j1*TensorProduct(JzKet(j1, m1), JzKet(j2, m2)) + assert_simplify_expand(e1, e2) + e1 = qapply(TensorProduct(1, J2)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) + e2 = hbar**2*j2**2*TensorProduct(JzKet(j1, m1), JzKet(j2, m2)) + \ + hbar**2*j2*TensorProduct(JzKet(j1, m1), JzKet(j2, m2)) + assert_simplify_expand(e1, e2) + + +def test_jx(): + assert Commutator(Jx, Jz).doit() == -I*hbar*Jy + assert Jx.rewrite('plusminus') == (Jminus + Jplus)/2 + assert represent(Jx, basis=Jz, j=1) == ( + represent(Jplus, basis=Jz, j=1) + represent(Jminus, basis=Jz, j=1))/2 + # Normal operators, normal states + # Numerical + assert qapply(Jx*JxKet(1, 1)) == hbar*JxKet(1, 1) + assert qapply(Jx*JyKet(1, 1)) == hbar*JyKet(1, 1) + assert qapply(Jx*JzKet(1, 1)) == sqrt(2)*hbar*JzKet(1, 0)/2 + # Symbolic + assert qapply(Jx*JxKet(j, m)) == hbar*m*JxKet(j, m) + assert qapply(Jx*JyKet(j, m)) == \ + Sum(hbar*mi*WignerD(j, mi, m, 0, 0, pi/2)*Sum(WignerD(j, + mi1, mi, pi*Rational(3, 2), 0, 0)*JyKet(j, mi1), (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jx*JzKet(j, m)) == \ + hbar*sqrt(j**2 + j - m**2 - m)*JzKet(j, m + 1)/2 + hbar*sqrt(j**2 + + j - m**2 + m)*JzKet(j, m - 1)/2 + # Normal operators, coupled states + # Numerical + assert qapply(Jx*JxKetCoupled(1, 1, (1, 1))) == \ + hbar*JxKetCoupled(1, 1, (1, 1)) + assert qapply(Jx*JyKetCoupled(1, 1, (1, 1))) == \ + hbar*JyKetCoupled(1, 1, (1, 1)) + assert qapply(Jx*JzKetCoupled(1, 1, (1, 1))) == \ + sqrt(2)*hbar*JzKetCoupled(1, 0, (1, 1))/2 + # Symbolic + assert qapply(Jx*JxKetCoupled(j, m, (j1, j2))) == \ + hbar*m*JxKetCoupled(j, m, (j1, j2)) + assert qapply(Jx*JyKetCoupled(j, m, (j1, j2))) == \ + Sum(hbar*mi*WignerD(j, mi, m, 0, 0, pi/2)*Sum(WignerD(j, mi1, mi, pi*Rational(3, 2), 0, 0)*JyKetCoupled(j, mi1, (j1, j2)), (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jx*JzKetCoupled(j, m, (j1, j2))) == \ + hbar*sqrt(j**2 + j - m**2 - m)*JzKetCoupled(j, m + 1, (j1, j2))/2 + \ + hbar*sqrt(j**2 + j - m**2 + m)*JzKetCoupled(j, m - 1, (j1, j2))/2 + # Normal operators, uncoupled states + # Numerical + assert qapply(Jx*TensorProduct(JxKet(1, 1), JxKet(1, 1))) == \ + 2*hbar*TensorProduct(JxKet(1, 1), JxKet(1, 1)) + assert qapply(Jx*TensorProduct(JyKet(1, 1), JyKet(1, 1))) == \ + hbar*TensorProduct(JyKet(1, 1), JyKet(1, 1)) + \ + hbar*TensorProduct(JyKet(1, 1), JyKet(1, 1)) + assert qapply(Jx*TensorProduct(JzKet(1, 1), JzKet(1, 1))) == \ + sqrt(2)*hbar*TensorProduct(JzKet(1, 1), JzKet(1, 0))/2 + \ + sqrt(2)*hbar*TensorProduct(JzKet(1, 0), JzKet(1, 1))/2 + assert qapply(Jx*TensorProduct(JxKet(1, 1), JxKet(1, -1))) == 0 + # Symbolic + assert qapply(Jx*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) == \ + hbar*m1*TensorProduct(JxKet(j1, m1), JxKet(j2, m2)) + \ + hbar*m2*TensorProduct(JxKet(j1, m1), JxKet(j2, m2)) + assert qapply(Jx*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) == \ + TensorProduct(Sum(hbar*mi*WignerD(j1, mi, m1, 0, 0, pi/2)*Sum(WignerD(j1, mi1, mi, pi*Rational(3, 2), 0, 0)*JyKet(j1, mi1), (mi1, -j1, j1)), (mi, -j1, j1)), JyKet(j2, m2)) + \ + TensorProduct(JyKet(j1, m1), Sum(hbar*mi*WignerD(j2, mi, m2, 0, 0, pi/2)*Sum(WignerD(j2, mi1, mi, pi*Rational(3, 2), 0, 0)*JyKet(j2, mi1), (mi1, -j2, j2)), (mi, -j2, j2))) + assert qapply(Jx*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) == \ + hbar*sqrt(j1**2 + j1 - m1**2 - m1)*TensorProduct(JzKet(j1, m1 + 1), JzKet(j2, m2))/2 + \ + hbar*sqrt(j1**2 + j1 - m1**2 + m1)*TensorProduct(JzKet(j1, m1 - 1), JzKet(j2, m2))/2 + \ + hbar*sqrt(j2**2 + j2 - m2**2 - m2)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2 + 1))/2 + \ + hbar*sqrt( + j2**2 + j2 - m2**2 + m2)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2 - 1))/2 + # Uncoupled operators, uncoupled states + # Numerical + assert qapply(TensorProduct(Jx, 1)*TensorProduct(JxKet(1, 1), JxKet(1, -1))) == \ + hbar*TensorProduct(JxKet(1, 1), JxKet(1, -1)) + assert qapply(TensorProduct(1, Jx)*TensorProduct(JxKet(1, 1), JxKet(1, -1))) == \ + -hbar*TensorProduct(JxKet(1, 1), JxKet(1, -1)) + assert qapply(TensorProduct(Jx, 1)*TensorProduct(JyKet(1, 1), JyKet(1, -1))) == \ + hbar*TensorProduct(JyKet(1, 1), JyKet(1, -1)) + assert qapply(TensorProduct(1, Jx)*TensorProduct(JyKet(1, 1), JyKet(1, -1))) == \ + -hbar*TensorProduct(JyKet(1, 1), JyKet(1, -1)) + assert qapply(TensorProduct(Jx, 1)*TensorProduct(JzKet(1, 1), JzKet(1, -1))) == \ + hbar*sqrt(2)*TensorProduct(JzKet(1, 0), JzKet(1, -1))/2 + assert qapply(TensorProduct(1, Jx)*TensorProduct(JzKet(1, 1), JzKet(1, -1))) == \ + hbar*sqrt(2)*TensorProduct(JzKet(1, 1), JzKet(1, 0))/2 + # Symbolic + assert qapply(TensorProduct(Jx, 1)*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) == \ + hbar*m1*TensorProduct(JxKet(j1, m1), JxKet(j2, m2)) + assert qapply(TensorProduct(1, Jx)*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) == \ + hbar*m2*TensorProduct(JxKet(j1, m1), JxKet(j2, m2)) + assert qapply(TensorProduct(Jx, 1)*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) == \ + TensorProduct(Sum(hbar*mi*WignerD(j1, mi, m1, 0, 0, pi/2) * Sum(WignerD(j1, mi1, mi, pi*Rational(3, 2), 0, 0)*JyKet(j1, mi1), (mi1, -j1, j1)), (mi, -j1, j1)), JyKet(j2, m2)) + assert qapply(TensorProduct(1, Jx)*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) == \ + TensorProduct(JyKet(j1, m1), Sum(hbar*mi*WignerD(j2, mi, m2, 0, 0, pi/2) * Sum(WignerD(j2, mi1, mi, pi*Rational(3, 2), 0, 0)*JyKet(j2, mi1), (mi1, -j2, j2)), (mi, -j2, j2))) + e1 = qapply(TensorProduct(Jx, 1)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) + e2 = hbar*sqrt(j1**2 + j1 - m1**2 - m1)*TensorProduct(JzKet(j1, m1 + 1), JzKet(j2, m2))/2 + \ + hbar*sqrt( + j1**2 + j1 - m1**2 + m1)*TensorProduct(JzKet(j1, m1 - 1), JzKet(j2, m2))/2 + assert_simplify_expand(e1, e2) + e1 = qapply(TensorProduct(1, Jx)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) + e2 = hbar*sqrt(j2**2 + j2 - m2**2 - m2)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2 + 1))/2 + \ + hbar*sqrt( + j2**2 + j2 - m2**2 + m2)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2 - 1))/2 + assert_simplify_expand(e1, e2) + + +def test_jy(): + assert Commutator(Jy, Jz).doit() == I*hbar*Jx + assert Jy.rewrite('plusminus') == (Jplus - Jminus)/(2*I) + assert represent(Jy, basis=Jz) == ( + represent(Jplus, basis=Jz) - represent(Jminus, basis=Jz))/(2*I) + # Normal operators, normal states + # Numerical + assert qapply(Jy*JxKet(1, 1)) == hbar*JxKet(1, 1) + assert qapply(Jy*JyKet(1, 1)) == hbar*JyKet(1, 1) + assert qapply(Jy*JzKet(1, 1)) == sqrt(2)*hbar*I*JzKet(1, 0)/2 + # Symbolic + assert qapply(Jy*JxKet(j, m)) == \ + Sum(hbar*mi*WignerD(j, mi, m, pi*Rational(3, 2), 0, 0)*Sum(WignerD( + j, mi1, mi, 0, 0, pi/2)*JxKet(j, mi1), (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jy*JyKet(j, m)) == hbar*m*JyKet(j, m) + assert qapply(Jy*JzKet(j, m)) == \ + -hbar*I*sqrt(j**2 + j - m**2 - m)*JzKet( + j, m + 1)/2 + hbar*I*sqrt(j**2 + j - m**2 + m)*JzKet(j, m - 1)/2 + # Normal operators, coupled states + # Numerical + assert qapply(Jy*JxKetCoupled(1, 1, (1, 1))) == \ + hbar*JxKetCoupled(1, 1, (1, 1)) + assert qapply(Jy*JyKetCoupled(1, 1, (1, 1))) == \ + hbar*JyKetCoupled(1, 1, (1, 1)) + assert qapply(Jy*JzKetCoupled(1, 1, (1, 1))) == \ + sqrt(2)*hbar*I*JzKetCoupled(1, 0, (1, 1))/2 + # Symbolic + assert qapply(Jy*JxKetCoupled(j, m, (j1, j2))) == \ + Sum(hbar*mi*WignerD(j, mi, m, pi*Rational(3, 2), 0, 0)*Sum(WignerD(j, mi1, mi, 0, 0, pi/2)*JxKetCoupled(j, mi1, (j1, j2)), (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jy*JyKetCoupled(j, m, (j1, j2))) == \ + hbar*m*JyKetCoupled(j, m, (j1, j2)) + assert qapply(Jy*JzKetCoupled(j, m, (j1, j2))) == \ + -hbar*I*sqrt(j**2 + j - m**2 - m)*JzKetCoupled(j, m + 1, (j1, j2))/2 + \ + hbar*I*sqrt(j**2 + j - m**2 + m)*JzKetCoupled(j, m - 1, (j1, j2))/2 + # Normal operators, uncoupled states + # Numerical + assert qapply(Jy*TensorProduct(JxKet(1, 1), JxKet(1, 1))) == \ + hbar*TensorProduct(JxKet(1, 1), JxKet(1, 1)) + \ + hbar*TensorProduct(JxKet(1, 1), JxKet(1, 1)) + assert qapply(Jy*TensorProduct(JyKet(1, 1), JyKet(1, 1))) == \ + 2*hbar*TensorProduct(JyKet(1, 1), JyKet(1, 1)) + assert qapply(Jy*TensorProduct(JzKet(1, 1), JzKet(1, 1))) == \ + sqrt(2)*hbar*I*TensorProduct(JzKet(1, 1), JzKet(1, 0))/2 + \ + sqrt(2)*hbar*I*TensorProduct(JzKet(1, 0), JzKet(1, 1))/2 + assert qapply(Jy*TensorProduct(JyKet(1, 1), JyKet(1, -1))) == 0 + # Symbolic + assert qapply(Jy*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) == \ + TensorProduct(JxKet(j1, m1), Sum(hbar*mi*WignerD(j2, mi, m2, pi*Rational(3, 2), 0, 0)*Sum(WignerD(j2, mi1, mi, 0, 0, pi/2)*JxKet(j2, mi1), (mi1, -j2, j2)), (mi, -j2, j2))) + \ + TensorProduct(Sum(hbar*mi*WignerD(j1, mi, m1, pi*Rational(3, 2), 0, 0)*Sum(WignerD(j1, mi1, mi, 0, 0, pi/2)*JxKet(j1, mi1), (mi1, -j1, j1)), (mi, -j1, j1)), JxKet(j2, m2)) + assert qapply(Jy*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) == \ + hbar*m1*TensorProduct(JyKet(j1, m1), JyKet( + j2, m2)) + hbar*m2*TensorProduct(JyKet(j1, m1), JyKet(j2, m2)) + assert qapply(Jy*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) == \ + -hbar*I*sqrt(j1**2 + j1 - m1**2 - m1)*TensorProduct(JzKet(j1, m1 + 1), JzKet(j2, m2))/2 + \ + hbar*I*sqrt(j1**2 + j1 - m1**2 + m1)*TensorProduct(JzKet(j1, m1 - 1), JzKet(j2, m2))/2 + \ + -hbar*I*sqrt(j2**2 + j2 - m2**2 - m2)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2 + 1))/2 + \ + hbar*I*sqrt( + j2**2 + j2 - m2**2 + m2)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2 - 1))/2 + # Uncoupled operators, uncoupled states + # Numerical + assert qapply(TensorProduct(Jy, 1)*TensorProduct(JxKet(1, 1), JxKet(1, -1))) == \ + hbar*TensorProduct(JxKet(1, 1), JxKet(1, -1)) + assert qapply(TensorProduct(1, Jy)*TensorProduct(JxKet(1, 1), JxKet(1, -1))) == \ + -hbar*TensorProduct(JxKet(1, 1), JxKet(1, -1)) + assert qapply(TensorProduct(Jy, 1)*TensorProduct(JyKet(1, 1), JyKet(1, -1))) == \ + hbar*TensorProduct(JyKet(1, 1), JyKet(1, -1)) + assert qapply(TensorProduct(1, Jy)*TensorProduct(JyKet(1, 1), JyKet(1, -1))) == \ + -hbar*TensorProduct(JyKet(1, 1), JyKet(1, -1)) + assert qapply(TensorProduct(Jy, 1)*TensorProduct(JzKet(1, 1), JzKet(1, -1))) == \ + hbar*sqrt(2)*I*TensorProduct(JzKet(1, 0), JzKet(1, -1))/2 + assert qapply(TensorProduct(1, Jy)*TensorProduct(JzKet(1, 1), JzKet(1, -1))) == \ + -hbar*sqrt(2)*I*TensorProduct(JzKet(1, 1), JzKet(1, 0))/2 + # Symbolic + assert qapply(TensorProduct(Jy, 1)*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) == \ + TensorProduct(Sum(hbar*mi*WignerD(j1, mi, m1, pi*Rational(3, 2), 0, 0) * Sum(WignerD(j1, mi1, mi, 0, 0, pi/2)*JxKet(j1, mi1), (mi1, -j1, j1)), (mi, -j1, j1)), JxKet(j2, m2)) + assert qapply(TensorProduct(1, Jy)*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) == \ + TensorProduct(JxKet(j1, m1), Sum(hbar*mi*WignerD(j2, mi, m2, pi*Rational(3, 2), 0, 0) * Sum(WignerD(j2, mi1, mi, 0, 0, pi/2)*JxKet(j2, mi1), (mi1, -j2, j2)), (mi, -j2, j2))) + assert qapply(TensorProduct(Jy, 1)*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) == \ + hbar*m1*TensorProduct(JyKet(j1, m1), JyKet(j2, m2)) + assert qapply(TensorProduct(1, Jy)*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) == \ + hbar*m2*TensorProduct(JyKet(j1, m1), JyKet(j2, m2)) + e1 = qapply(TensorProduct(Jy, 1)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) + e2 = -hbar*I*sqrt(j1**2 + j1 - m1**2 - m1)*TensorProduct(JzKet(j1, m1 + 1), JzKet(j2, m2))/2 + \ + hbar*I*sqrt( + j1**2 + j1 - m1**2 + m1)*TensorProduct(JzKet(j1, m1 - 1), JzKet(j2, m2))/2 + assert_simplify_expand(e1, e2) + e1 = qapply(TensorProduct(1, Jy)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) + e2 = -hbar*I*sqrt(j2**2 + j2 - m2**2 - m2)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2 + 1))/2 + \ + hbar*I*sqrt( + j2**2 + j2 - m2**2 + m2)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2 - 1))/2 + assert_simplify_expand(e1, e2) + + +def test_jz(): + assert Commutator(Jz, Jminus).doit() == -hbar*Jminus + # Normal operators, normal states + # Numerical + assert qapply(Jz*JxKet(1, 1)) == -sqrt(2)*hbar*JxKet(1, 0)/2 + assert qapply(Jz*JyKet(1, 1)) == -sqrt(2)*hbar*I*JyKet(1, 0)/2 + assert qapply(Jz*JzKet(2, 1)) == hbar*JzKet(2, 1) + # Symbolic + assert qapply(Jz*JxKet(j, m)) == \ + Sum(hbar*mi*WignerD(j, mi, m, 0, pi/2, 0)*Sum(WignerD(j, + mi1, mi, 0, pi*Rational(3, 2), 0)*JxKet(j, mi1), (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jz*JyKet(j, m)) == \ + Sum(hbar*mi*WignerD(j, mi, m, pi*Rational(3, 2), -pi/2, pi/2)*Sum(WignerD(j, mi1, + mi, pi*Rational(3, 2), pi/2, pi/2)*JyKet(j, mi1), (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jz*JzKet(j, m)) == hbar*m*JzKet(j, m) + # Normal operators, coupled states + # Numerical + assert qapply(Jz*JxKetCoupled(1, 1, (1, 1))) == \ + -sqrt(2)*hbar*JxKetCoupled(1, 0, (1, 1))/2 + assert qapply(Jz*JyKetCoupled(1, 1, (1, 1))) == \ + -sqrt(2)*hbar*I*JyKetCoupled(1, 0, (1, 1))/2 + assert qapply(Jz*JzKetCoupled(1, 1, (1, 1))) == \ + hbar*JzKetCoupled(1, 1, (1, 1)) + # Symbolic + assert qapply(Jz*JxKetCoupled(j, m, (j1, j2))) == \ + Sum(hbar*mi*WignerD(j, mi, m, 0, pi/2, 0)*Sum(WignerD(j, mi1, mi, 0, pi*Rational(3, 2), 0)*JxKetCoupled(j, mi1, (j1, j2)), (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jz*JyKetCoupled(j, m, (j1, j2))) == \ + Sum(hbar*mi*WignerD(j, mi, m, pi*Rational(3, 2), -pi/2, pi/2)*Sum(WignerD(j, mi1, mi, pi*Rational(3, 2), pi/2, pi/2)*JyKetCoupled(j, mi1, (j1, j2)), (mi1, -j, j)), (mi, -j, j)) + assert qapply(Jz*JzKetCoupled(j, m, (j1, j2))) == \ + hbar*m*JzKetCoupled(j, m, (j1, j2)) + # Normal operators, uncoupled states + # Numerical + assert qapply(Jz*TensorProduct(JxKet(1, 1), JxKet(1, 1))) == \ + -sqrt(2)*hbar*TensorProduct(JxKet(1, 1), JxKet(1, 0))/2 - \ + sqrt(2)*hbar*TensorProduct(JxKet(1, 0), JxKet(1, 1))/2 + assert qapply(Jz*TensorProduct(JyKet(1, 1), JyKet(1, 1))) == \ + -sqrt(2)*hbar*I*TensorProduct(JyKet(1, 1), JyKet(1, 0))/2 - \ + sqrt(2)*hbar*I*TensorProduct(JyKet(1, 0), JyKet(1, 1))/2 + assert qapply(Jz*TensorProduct(JzKet(1, 1), JzKet(1, 1))) == \ + 2*hbar*TensorProduct(JzKet(1, 1), JzKet(1, 1)) + assert qapply(Jz*TensorProduct(JzKet(1, 1), JzKet(1, -1))) == 0 + # Symbolic + assert qapply(Jz*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) == \ + TensorProduct(JxKet(j1, m1), Sum(hbar*mi*WignerD(j2, mi, m2, 0, pi/2, 0)*Sum(WignerD(j2, mi1, mi, 0, pi*Rational(3, 2), 0)*JxKet(j2, mi1), (mi1, -j2, j2)), (mi, -j2, j2))) + \ + TensorProduct(Sum(hbar*mi*WignerD(j1, mi, m1, 0, pi/2, 0)*Sum(WignerD(j1, mi1, mi, 0, pi*Rational(3, 2), 0)*JxKet(j1, mi1), (mi1, -j1, j1)), (mi, -j1, j1)), JxKet(j2, m2)) + assert qapply(Jz*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) == \ + TensorProduct(JyKet(j1, m1), Sum(hbar*mi*WignerD(j2, mi, m2, pi*Rational(3, 2), -pi/2, pi/2)*Sum(WignerD(j2, mi1, mi, pi*Rational(3, 2), pi/2, pi/2)*JyKet(j2, mi1), (mi1, -j2, j2)), (mi, -j2, j2))) + \ + TensorProduct(Sum(hbar*mi*WignerD(j1, mi, m1, pi*Rational(3, 2), -pi/2, pi/2)*Sum(WignerD(j1, mi1, mi, pi*Rational(3, 2), pi/2, pi/2)*JyKet(j1, mi1), (mi1, -j1, j1)), (mi, -j1, j1)), JyKet(j2, m2)) + assert qapply(Jz*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) == \ + hbar*m1*TensorProduct(JzKet(j1, m1), JzKet( + j2, m2)) + hbar*m2*TensorProduct(JzKet(j1, m1), JzKet(j2, m2)) + # Uncoupled Operators + # Numerical + assert qapply(TensorProduct(Jz, 1)*TensorProduct(JxKet(1, 1), JxKet(1, -1))) == \ + -sqrt(2)*hbar*TensorProduct(JxKet(1, 0), JxKet(1, -1))/2 + assert qapply(TensorProduct(1, Jz)*TensorProduct(JxKet(1, 1), JxKet(1, -1))) == \ + -sqrt(2)*hbar*TensorProduct(JxKet(1, 1), JxKet(1, 0))/2 + assert qapply(TensorProduct(Jz, 1)*TensorProduct(JyKet(1, 1), JyKet(1, -1))) == \ + -sqrt(2)*I*hbar*TensorProduct(JyKet(1, 0), JyKet(1, -1))/2 + assert qapply(TensorProduct(1, Jz)*TensorProduct(JyKet(1, 1), JyKet(1, -1))) == \ + sqrt(2)*I*hbar*TensorProduct(JyKet(1, 1), JyKet(1, 0))/2 + assert qapply(TensorProduct(Jz, 1)*TensorProduct(JzKet(1, 1), JzKet(1, -1))) == \ + hbar*TensorProduct(JzKet(1, 1), JzKet(1, -1)) + assert qapply(TensorProduct(1, Jz)*TensorProduct(JzKet(1, 1), JzKet(1, -1))) == \ + -hbar*TensorProduct(JzKet(1, 1), JzKet(1, -1)) + # Symbolic + assert qapply(TensorProduct(Jz, 1)*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) == \ + TensorProduct(Sum(hbar*mi*WignerD(j1, mi, m1, 0, pi/2, 0)*Sum(WignerD(j1, mi1, mi, 0, pi*Rational(3, 2), 0)*JxKet(j1, mi1), (mi1, -j1, j1)), (mi, -j1, j1)), JxKet(j2, m2)) + assert qapply(TensorProduct(1, Jz)*TensorProduct(JxKet(j1, m1), JxKet(j2, m2))) == \ + TensorProduct(JxKet(j1, m1), Sum(hbar*mi*WignerD(j2, mi, m2, 0, pi/2, 0)*Sum(WignerD(j2, mi1, mi, 0, pi*Rational(3, 2), 0)*JxKet(j2, mi1), (mi1, -j2, j2)), (mi, -j2, j2))) + assert qapply(TensorProduct(Jz, 1)*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) == \ + TensorProduct(Sum(hbar*mi*WignerD(j1, mi, m1, pi*Rational(3, 2), -pi/2, pi/2)*Sum(WignerD(j1, mi1, mi, pi*Rational(3, 2), pi/2, pi/2)*JyKet(j1, mi1), (mi1, -j1, j1)), (mi, -j1, j1)), JyKet(j2, m2)) + assert qapply(TensorProduct(1, Jz)*TensorProduct(JyKet(j1, m1), JyKet(j2, m2))) == \ + TensorProduct(JyKet(j1, m1), Sum(hbar*mi*WignerD(j2, mi, m2, pi*Rational(3, 2), -pi/2, pi/2)*Sum(WignerD(j2, mi1, mi, pi*Rational(3, 2), pi/2, pi/2)*JyKet(j2, mi1), (mi1, -j2, j2)), (mi, -j2, j2))) + assert qapply(TensorProduct(Jz, 1)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) == \ + hbar*m1*TensorProduct(JzKet(j1, m1), JzKet(j2, m2)) + assert qapply(TensorProduct(1, Jz)*TensorProduct(JzKet(j1, m1), JzKet(j2, m2))) == \ + hbar*m2*TensorProduct(JzKet(j1, m1), JzKet(j2, m2)) + + +def test_rotation(): + a, b, g = symbols('a b g') + j, m = symbols('j m') + #Uncoupled + answ = [JxKet(1,-1)/2 - sqrt(2)*JxKet(1,0)/2 + JxKet(1,1)/2 , + JyKet(1,-1)/2 - sqrt(2)*JyKet(1,0)/2 + JyKet(1,1)/2 , + JzKet(1,-1)/2 - sqrt(2)*JzKet(1,0)/2 + JzKet(1,1)/2] + fun = [state(1, 1) for state in (JxKet, JyKet, JzKet)] + for state in fun: + got = qapply(Rotation(0, pi/2, 0)*state) + assert got in answ + answ.remove(got) + assert not answ + arg = Rotation(a, b, g)*fun[0] + assert qapply(arg) == (-exp(-I*a)*exp(I*g)*cos(b)*JxKet(1,-1)/2 + + exp(-I*a)*exp(I*g)*JxKet(1,-1)/2 - sqrt(2)*exp(-I*a)*sin(b)*JxKet(1,0)/2 + + exp(-I*a)*exp(-I*g)*cos(b)*JxKet(1,1)/2 + exp(-I*a)*exp(-I*g)*JxKet(1,1)/2) + #dummy effective + assert str(qapply(Rotation(a, b, g)*JzKet(j, m), dummy=False)) == str( + qapply(Rotation(a, b, g)*JzKet(j, m), dummy=True)).replace('_','') + #Coupled + ans = [JxKetCoupled(1,-1,(1,1))/2 - sqrt(2)*JxKetCoupled(1,0,(1,1))/2 + + JxKetCoupled(1,1,(1,1))/2 , + JyKetCoupled(1,-1,(1,1))/2 - sqrt(2)*JyKetCoupled(1,0,(1,1))/2 + + JyKetCoupled(1,1,(1,1))/2 , + JzKetCoupled(1,-1,(1,1))/2 - sqrt(2)*JzKetCoupled(1,0,(1,1))/2 + + JzKetCoupled(1,1,(1,1))/2] + fun = [state(1, 1, (1,1)) for state in (JxKetCoupled, JyKetCoupled, JzKetCoupled)] + for state in fun: + got = qapply(Rotation(0, pi/2, 0)*state) + assert got in ans + ans.remove(got) + assert not ans + arg = Rotation(a, b, g)*fun[0] + assert qapply(arg) == ( + -exp(-I*a)*exp(I*g)*cos(b)*JxKetCoupled(1,-1,(1,1))/2 + + exp(-I*a)*exp(I*g)*JxKetCoupled(1,-1,(1,1))/2 - + sqrt(2)*exp(-I*a)*sin(b)*JxKetCoupled(1,0,(1,1))/2 + + exp(-I*a)*exp(-I*g)*cos(b)*JxKetCoupled(1,1,(1,1))/2 + + exp(-I*a)*exp(-I*g)*JxKetCoupled(1,1,(1,1))/2) + #dummy effective + assert str(qapply(Rotation(a,b,g)*JzKetCoupled(j,m,(j1,j2)), dummy=False)) == str( + qapply(Rotation(a,b,g)*JzKetCoupled(j,m,(j1,j2)), dummy=True)).replace('_','') + + +def test_jzket(): + j, m = symbols('j m') + # j not integer or half integer + raises(ValueError, lambda: JzKet(Rational(2, 3), Rational(-1, 3))) + raises(ValueError, lambda: JzKet(Rational(2, 3), m)) + # j < 0 + raises(ValueError, lambda: JzKet(-1, 1)) + raises(ValueError, lambda: JzKet(-1, m)) + # m not integer or half integer + raises(ValueError, lambda: JzKet(j, Rational(-1, 3))) + # abs(m) > j + raises(ValueError, lambda: JzKet(1, 2)) + raises(ValueError, lambda: JzKet(1, -2)) + # j-m not integer + raises(ValueError, lambda: JzKet(1, S.Half)) + + +def test_jzketcoupled(): + j, m = symbols('j m') + # j not integer or half integer + raises(ValueError, lambda: JzKetCoupled(Rational(2, 3), Rational(-1, 3), (1,))) + raises(ValueError, lambda: JzKetCoupled(Rational(2, 3), m, (1,))) + # j < 0 + raises(ValueError, lambda: JzKetCoupled(-1, 1, (1,))) + raises(ValueError, lambda: JzKetCoupled(-1, m, (1,))) + # m not integer or half integer + raises(ValueError, lambda: JzKetCoupled(j, Rational(-1, 3), (1,))) + # abs(m) > j + raises(ValueError, lambda: JzKetCoupled(1, 2, (1,))) + raises(ValueError, lambda: JzKetCoupled(1, -2, (1,))) + # j-m not integer + raises(ValueError, lambda: JzKetCoupled(1, S.Half, (1,))) + # checks types on coupling scheme + raises(TypeError, lambda: JzKetCoupled(1, 1, 1)) + raises(TypeError, lambda: JzKetCoupled(1, 1, (1,), 1)) + raises(TypeError, lambda: JzKetCoupled(1, 1, (1, 1), (1,))) + raises(TypeError, lambda: JzKetCoupled(1, 1, (1, 1, 1), (1, 2, 1), + (1, 3, 1))) + # checks length of coupling terms + raises(ValueError, lambda: JzKetCoupled(1, 1, (1,), ((1, 2, 1),))) + raises(ValueError, lambda: JzKetCoupled(1, 1, (1, 1), ((1, 2),))) + # all jn are integer or half-integer + raises(ValueError, lambda: JzKetCoupled(1, 1, (Rational(1, 3), Rational(2, 3)))) + # indices in coupling scheme must be integers + raises(ValueError, lambda: JzKetCoupled(1, 1, (1, 1), ((S.Half, 1, 2),) )) + raises(ValueError, lambda: JzKetCoupled(1, 1, (1, 1), ((1, S.Half, 2),) )) + # indices out of range + raises(ValueError, lambda: JzKetCoupled(1, 1, (1, 1), ((0, 2, 1),) )) + raises(ValueError, lambda: JzKetCoupled(1, 1, (1, 1), ((3, 2, 1),) )) + raises(ValueError, lambda: JzKetCoupled(1, 1, (1, 1), ((1, 0, 1),) )) + raises(ValueError, lambda: JzKetCoupled(1, 1, (1, 1), ((1, 3, 1),) )) + # all j values in coupling scheme must by integer or half-integer + raises(ValueError, lambda: JzKetCoupled(1, 1, (1, 1, 1), ((1, 2, S( + 4)/3), (1, 3, 1)) )) + # each coupling must satisfy |j1-j2| <= j3 <= j1+j2 + raises(ValueError, lambda: JzKetCoupled(1, 1, (1, 5))) + raises(ValueError, lambda: JzKetCoupled(5, 1, (1, 1))) + # final j of coupling must be j of the state + raises(ValueError, lambda: JzKetCoupled(1, 1, (1, 1), ((1, 2, 2),) )) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_state.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_state.py new file mode 100644 index 0000000000000000000000000000000000000000..c9fd5029fa3d77c2ddfc6899187624da02796ffa --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_state.py @@ -0,0 +1,248 @@ +from sympy.core.add import Add +from sympy.core.function import diff +from sympy.core.mul import Mul +from sympy.core.numbers import (I, Integer, Rational, oo, pi) +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.core.sympify import sympify +from sympy.functions.elementary.complexes import conjugate +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin +from sympy.testing.pytest import raises + +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.qexpr import QExpr +from sympy.physics.quantum.state import ( + Ket, Bra, TimeDepKet, TimeDepBra, + KetBase, BraBase, StateBase, Wavefunction, + OrthogonalKet, OrthogonalBra +) +from sympy.physics.quantum.hilbert import HilbertSpace + +x, y, t = symbols('x,y,t') + + +class CustomKet(Ket): + @classmethod + def default_args(self): + return ("test",) + + +class CustomKetMultipleLabels(Ket): + @classmethod + def default_args(self): + return ("r", "theta", "phi") + + +class CustomTimeDepKet(TimeDepKet): + @classmethod + def default_args(self): + return ("test", "t") + + +class CustomTimeDepKetMultipleLabels(TimeDepKet): + @classmethod + def default_args(self): + return ("r", "theta", "phi", "t") + + +def test_ket(): + k = Ket('0') + + assert isinstance(k, Ket) + assert isinstance(k, KetBase) + assert isinstance(k, StateBase) + assert isinstance(k, QExpr) + + assert k.label == (Symbol('0'),) + assert k.hilbert_space == HilbertSpace() + assert k.is_commutative is False + + # Make sure this doesn't get converted to the number pi. + k = Ket('pi') + assert k.label == (Symbol('pi'),) + + k = Ket(x, y) + assert k.label == (x, y) + assert k.hilbert_space == HilbertSpace() + assert k.is_commutative is False + + assert k.dual_class() == Bra + assert k.dual == Bra(x, y) + assert k.subs(x, y) == Ket(y, y) + + k = CustomKet() + assert k == CustomKet("test") + + k = CustomKetMultipleLabels() + assert k == CustomKetMultipleLabels("r", "theta", "phi") + + assert Ket() == Ket('psi') + + +def test_bra(): + b = Bra('0') + + assert isinstance(b, Bra) + assert isinstance(b, BraBase) + assert isinstance(b, StateBase) + assert isinstance(b, QExpr) + + assert b.label == (Symbol('0'),) + assert b.hilbert_space == HilbertSpace() + assert b.is_commutative is False + + # Make sure this doesn't get converted to the number pi. + b = Bra('pi') + assert b.label == (Symbol('pi'),) + + b = Bra(x, y) + assert b.label == (x, y) + assert b.hilbert_space == HilbertSpace() + assert b.is_commutative is False + + assert b.dual_class() == Ket + assert b.dual == Ket(x, y) + assert b.subs(x, y) == Bra(y, y) + + assert Bra() == Bra('psi') + + +def test_ops(): + k0 = Ket(0) + k1 = Ket(1) + k = 2*I*k0 - (x/sqrt(2))*k1 + assert k == Add(Mul(2, I, k0), + Mul(Rational(-1, 2), x, Pow(2, S.Half), k1)) + + +def test_time_dep_ket(): + k = TimeDepKet(0, t) + + assert isinstance(k, TimeDepKet) + assert isinstance(k, KetBase) + assert isinstance(k, StateBase) + assert isinstance(k, QExpr) + + assert k.label == (Integer(0),) + assert k.args == (Integer(0), t) + assert k.time == t + + assert k.dual_class() == TimeDepBra + assert k.dual == TimeDepBra(0, t) + + assert k.subs(t, 2) == TimeDepKet(0, 2) + + k = TimeDepKet(x, 0.5) + assert k.label == (x,) + assert k.args == (x, sympify(0.5)) + + k = CustomTimeDepKet() + assert k.label == (Symbol("test"),) + assert k.time == Symbol("t") + assert k == CustomTimeDepKet("test", "t") + + k = CustomTimeDepKetMultipleLabels() + assert k.label == (Symbol("r"), Symbol("theta"), Symbol("phi")) + assert k.time == Symbol("t") + assert k == CustomTimeDepKetMultipleLabels("r", "theta", "phi", "t") + + assert TimeDepKet() == TimeDepKet("psi", "t") + + +def test_time_dep_bra(): + b = TimeDepBra(0, t) + + assert isinstance(b, TimeDepBra) + assert isinstance(b, BraBase) + assert isinstance(b, StateBase) + assert isinstance(b, QExpr) + + assert b.label == (Integer(0),) + assert b.args == (Integer(0), t) + assert b.time == t + + assert b.dual_class() == TimeDepKet + assert b.dual == TimeDepKet(0, t) + + k = TimeDepBra(x, 0.5) + assert k.label == (x,) + assert k.args == (x, sympify(0.5)) + + assert TimeDepBra() == TimeDepBra("psi", "t") + + +def test_bra_ket_dagger(): + x = symbols('x', complex=True) + k = Ket('k') + b = Bra('b') + assert Dagger(k) == Bra('k') + assert Dagger(b) == Ket('b') + assert Dagger(k).is_commutative is False + + k2 = Ket('k2') + e = 2*I*k + x*k2 + assert Dagger(e) == conjugate(x)*Dagger(k2) - 2*I*Dagger(k) + + +def test_wavefunction(): + x, y = symbols('x y', real=True) + L = symbols('L', positive=True) + n = symbols('n', integer=True, positive=True) + + f = Wavefunction(x**2, x) + p = f.prob() + lims = f.limits + + assert f.is_normalized is False + assert f.norm is oo + assert f(10) == 100 + assert p(10) == 10000 + assert lims[x] == (-oo, oo) + assert diff(f, x) == Wavefunction(2*x, x) + raises(NotImplementedError, lambda: f.normalize()) + assert conjugate(f) == Wavefunction(conjugate(f.expr), x) + assert conjugate(f) == Dagger(f) + + g = Wavefunction(x**2*y + y**2*x, (x, 0, 1), (y, 0, 2)) + lims_g = g.limits + + assert lims_g[x] == (0, 1) + assert lims_g[y] == (0, 2) + assert g.is_normalized is False + assert g.norm == sqrt(42)/3 + assert g(2, 4) == 0 + assert g(1, 1) == 2 + assert diff(diff(g, x), y) == Wavefunction(2*x + 2*y, (x, 0, 1), (y, 0, 2)) + assert conjugate(g) == Wavefunction(conjugate(g.expr), *g.args[1:]) + assert conjugate(g) == Dagger(g) + + h = Wavefunction(sqrt(5)*x**2, (x, 0, 1)) + assert h.is_normalized is True + assert h.normalize() == h + assert conjugate(h) == Wavefunction(conjugate(h.expr), (x, 0, 1)) + assert conjugate(h) == Dagger(h) + + piab = Wavefunction(sin(n*pi*x/L), (x, 0, L)) + assert piab.norm == sqrt(L/2) + assert piab(L + 1) == 0 + assert piab(0.5) == sin(0.5*n*pi/L) + assert piab(0.5, n=1, L=1) == sin(0.5*pi) + assert piab.normalize() == \ + Wavefunction(sqrt(2)/sqrt(L)*sin(n*pi*x/L), (x, 0, L)) + assert conjugate(piab) == Wavefunction(conjugate(piab.expr), (x, 0, L)) + assert conjugate(piab) == Dagger(piab) + + k = Wavefunction(x**2, 'x') + assert type(k.variables[0]) == Symbol + +def test_orthogonal_states(): + bracket = OrthogonalBra(x) * OrthogonalKet(x) + assert bracket.doit() == 1 + + bracket = OrthogonalBra(x) * OrthogonalKet(x+1) + assert bracket.doit() == 0 + + bracket = OrthogonalBra(x) * OrthogonalKet(y) + assert bracket.doit() == bracket diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_tensorproduct.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_tensorproduct.py new file mode 100644 index 0000000000000000000000000000000000000000..c17d533ae6d4ae97cb313eb345219fd82c6e483c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_tensorproduct.py @@ -0,0 +1,142 @@ +from sympy.core.numbers import I +from sympy.core.symbol import symbols +from sympy.core.expr import unchanged +from sympy.matrices import Matrix, SparseMatrix, ImmutableMatrix +from sympy.testing.pytest import warns_deprecated_sympy + +from sympy.physics.quantum.commutator import Commutator as Comm +from sympy.physics.quantum.tensorproduct import TensorProduct +from sympy.physics.quantum.tensorproduct import TensorProduct as TP +from sympy.physics.quantum.tensorproduct import tensor_product_simp +from sympy.physics.quantum.dagger import Dagger +from sympy.physics.quantum.qubit import Qubit, QubitBra +from sympy.physics.quantum.operator import OuterProduct, Operator +from sympy.physics.quantum.density import Density +from sympy.physics.quantum.trace import Tr + +A = Operator('A') +B = Operator('B') +C = Operator('C') +D = Operator('D') +x = symbols('x') +y = symbols('y', integer=True, positive=True) + +mat1 = Matrix([[1, 2*I], [1 + I, 3]]) +mat2 = Matrix([[2*I, 3], [4*I, 2]]) + + +def test_sparse_matrices(): + spm = SparseMatrix.diag(1, 0) + assert unchanged(TensorProduct, spm, spm) + + +def test_tensor_product_dagger(): + assert Dagger(TensorProduct(I*A, B)) == \ + -I*TensorProduct(Dagger(A), Dagger(B)) + assert Dagger(TensorProduct(mat1, mat2)) == \ + TensorProduct(Dagger(mat1), Dagger(mat2)) + + +def test_tensor_product_abstract(): + + assert TP(x*A, 2*B) == x*2*TP(A, B) + assert TP(A, B) != TP(B, A) + assert TP(A, B).is_commutative is False + assert isinstance(TP(A, B), TP) + assert TP(A, B).subs(A, C) == TP(C, B) + + +def test_tensor_product_expand(): + assert TP(A + B, B + C).expand(tensorproduct=True) == \ + TP(A, B) + TP(A, C) + TP(B, B) + TP(B, C) + #Tests for fix of issue #24142 + assert TP(A-B, B-A).expand(tensorproduct=True) == \ + TP(A, B) - TP(A, A) - TP(B, B) + TP(B, A) + assert TP(2*A + B, A + B).expand(tensorproduct=True) == \ + 2 * TP(A, A) + 2 * TP(A, B) + TP(B, A) + TP(B, B) + assert TP(2 * A * B + A, A + B).expand(tensorproduct=True) == \ + 2 * TP(A*B, A) + 2 * TP(A*B, B) + TP(A, A) + TP(A, B) + + +def test_tensor_product_commutator(): + assert TP(Comm(A, B), C).doit().expand(tensorproduct=True) == \ + TP(A*B, C) - TP(B*A, C) + assert Comm(TP(A, B), TP(B, C)).doit() == \ + TP(A, B)*TP(B, C) - TP(B, C)*TP(A, B) + + +def test_tensor_product_simp(): + with warns_deprecated_sympy(): + assert tensor_product_simp(TP(A, B)*TP(B, C)) == TP(A*B, B*C) + # tests for Pow-expressions + assert TP(A, B)**y == TP(A**y, B**y) + assert tensor_product_simp(TP(A, B)**y) == TP(A**y, B**y) + assert tensor_product_simp(x*TP(A, B)**2) == x*TP(A**2,B**2) + assert tensor_product_simp(x*(TP(A, B)**2)*TP(C,D)) == x*TP(A**2*C,B**2*D) + assert tensor_product_simp(TP(A,B)-TP(C,D)**y) == TP(A,B)-TP(C**y,D**y) + + +def test_issue_5923(): + # most of the issue regarding sympification of args has been handled + # and is tested internally by the use of args_cnc through the quantum + # module, but the following is a test from the issue that used to raise. + assert TensorProduct(1, Qubit('1')*Qubit('1').dual) == \ + TensorProduct(1, OuterProduct(Qubit(1), QubitBra(1))) + + +def test_eval_trace(): + # This test includes tests with dependencies between TensorProducts + #and density operators. Since, the test is more to test the behavior of + #TensorProducts it remains here + + # Density with simple tensor products as args + t = TensorProduct(A, B) + d = Density([t, 1.0]) + tr = Tr(d) + assert tr.doit() == 1.0*Tr(A*Dagger(A))*Tr(B*Dagger(B)) + + ## partial trace with simple tensor products as args + t = TensorProduct(A, B, C) + d = Density([t, 1.0]) + tr = Tr(d, [1]) + assert tr.doit() == 1.0*A*Dagger(A)*Tr(B*Dagger(B))*C*Dagger(C) + + tr = Tr(d, [0, 2]) + assert tr.doit() == 1.0*Tr(A*Dagger(A))*B*Dagger(B)*Tr(C*Dagger(C)) + + # Density with multiple Tensorproducts as states + t2 = TensorProduct(A, B) + t3 = TensorProduct(C, D) + + d = Density([t2, 0.5], [t3, 0.5]) + t = Tr(d) + assert t.doit() == (0.5*Tr(A*Dagger(A))*Tr(B*Dagger(B)) + + 0.5*Tr(C*Dagger(C))*Tr(D*Dagger(D))) + + t = Tr(d, [0]) + assert t.doit() == (0.5*Tr(A*Dagger(A))*B*Dagger(B) + + 0.5*Tr(C*Dagger(C))*D*Dagger(D)) + + #Density with mixed states + d = Density([t2 + t3, 1.0]) + t = Tr(d) + assert t.doit() == ( 1.0*Tr(A*Dagger(A))*Tr(B*Dagger(B)) + + 1.0*Tr(A*Dagger(C))*Tr(B*Dagger(D)) + + 1.0*Tr(C*Dagger(A))*Tr(D*Dagger(B)) + + 1.0*Tr(C*Dagger(C))*Tr(D*Dagger(D))) + + t = Tr(d, [1] ) + assert t.doit() == ( 1.0*A*Dagger(A)*Tr(B*Dagger(B)) + + 1.0*A*Dagger(C)*Tr(B*Dagger(D)) + + 1.0*C*Dagger(A)*Tr(D*Dagger(B)) + + 1.0*C*Dagger(C)*Tr(D*Dagger(D))) + + +def test_pr24993(): + from sympy.matrices.expressions.kronecker import matrix_kronecker_product + from sympy.physics.quantum.matrixutils import matrix_tensor_product + X = Matrix([[0, 1], [1, 0]]) + Xi = ImmutableMatrix(X) + assert TensorProduct(Xi, Xi) == TensorProduct(X, X) + assert TensorProduct(Xi, Xi) == matrix_tensor_product(X, X) + assert TensorProduct(Xi, Xi) == matrix_kronecker_product(X, X) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_trace.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_trace.py new file mode 100644 index 0000000000000000000000000000000000000000..85db6c60ad9d2bd1fbfafcf5d84b97d2fe304250 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_trace.py @@ -0,0 +1,109 @@ +from sympy.core.containers import Tuple +from sympy.core.symbol import symbols +from sympy.matrices.dense import Matrix +from sympy.physics.quantum.trace import Tr +from sympy.testing.pytest import raises, warns_deprecated_sympy + + +def test_trace_new(): + a, b, c, d, Y = symbols('a b c d Y') + A, B, C, D = symbols('A B C D', commutative=False) + + assert Tr(a + b) == a + b + assert Tr(A + B) == Tr(A) + Tr(B) + + #check trace args not implicitly permuted + assert Tr(C*D*A*B).args[0].args == (C, D, A, B) + + # check for mul and adds + assert Tr((a*b) + ( c*d)) == (a*b) + (c*d) + # Tr(scalar*A) = scalar*Tr(A) + assert Tr(a*A) == a*Tr(A) + assert Tr(a*A*B*b) == a*b*Tr(A*B) + + # since A is symbol and not commutative + assert isinstance(Tr(A), Tr) + + #POW + assert Tr(pow(a, b)) == a**b + assert isinstance(Tr(pow(A, a)), Tr) + + #Matrix + M = Matrix([[1, 1], [2, 2]]) + assert Tr(M) == 3 + + ##test indices in different forms + #no index + t = Tr(A) + assert t.args[1] == Tuple() + + #single index + t = Tr(A, 0) + assert t.args[1] == Tuple(0) + + #index in a list + t = Tr(A, [0]) + assert t.args[1] == Tuple(0) + + t = Tr(A, [0, 1, 2]) + assert t.args[1] == Tuple(0, 1, 2) + + #index is tuple + t = Tr(A, (0)) + assert t.args[1] == Tuple(0) + + t = Tr(A, (1, 2)) + assert t.args[1] == Tuple(1, 2) + + #trace indices test + t = Tr((A + B), [2]) + assert t.args[0].args[1] == Tuple(2) and t.args[1].args[1] == Tuple(2) + + t = Tr(a*A, [2, 3]) + assert t.args[1].args[1] == Tuple(2, 3) + + #class with trace method defined + #to simulate numpy objects + class Foo: + def trace(self): + return 1 + assert Tr(Foo()) == 1 + + #argument test + # check for value error, when either/both arguments are not provided + raises(ValueError, lambda: Tr()) + raises(ValueError, lambda: Tr(A, 1, 2)) + + +def test_trace_doit(): + a, b, c, d = symbols('a b c d') + A, B, C, D = symbols('A B C D', commutative=False) + + #TODO: needed while testing reduced density operations, etc. + + +def test_permute(): + A, B, C, D, E, F, G = symbols('A B C D E F G', commutative=False) + t = Tr(A*B*C*D*E*F*G) + + assert t.permute(0).args[0].args == (A, B, C, D, E, F, G) + assert t.permute(2).args[0].args == (F, G, A, B, C, D, E) + assert t.permute(4).args[0].args == (D, E, F, G, A, B, C) + assert t.permute(6).args[0].args == (B, C, D, E, F, G, A) + assert t.permute(8).args[0].args == t.permute(1).args[0].args + + assert t.permute(-1).args[0].args == (B, C, D, E, F, G, A) + assert t.permute(-3).args[0].args == (D, E, F, G, A, B, C) + assert t.permute(-5).args[0].args == (F, G, A, B, C, D, E) + assert t.permute(-8).args[0].args == t.permute(-1).args[0].args + + t = Tr((A + B)*(B*B)*C*D) + assert t.permute(2).args[0].args == (C, D, (A + B), (B**2)) + + t1 = Tr(A*B) + t2 = t1.permute(1) + assert id(t1) != id(t2) and t1 == t2 + +def test_deprecated_core_trace(): + with warns_deprecated_sympy(): + from sympy.core.trace import Tr # noqa:F401 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_transforms.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_transforms.py new file mode 100644 index 0000000000000000000000000000000000000000..55349ebe3b8003b5a107648516706034beaf22af --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/tests/test_transforms.py @@ -0,0 +1,75 @@ +"""Tests of transforms of quantum expressions for Mul and Pow.""" + +from sympy.core.symbol import symbols +from sympy.testing.pytest import raises + +from sympy.physics.quantum.operator import ( + Operator, OuterProduct +) +from sympy.physics.quantum.state import Ket, Bra +from sympy.physics.quantum.innerproduct import InnerProduct +from sympy.physics.quantum.tensorproduct import TensorProduct + + +k1 = Ket('k1') +k2 = Ket('k2') +k3 = Ket('k3') +b1 = Bra('b1') +b2 = Bra('b2') +b3 = Bra('b3') +A = Operator('A') +B = Operator('B') +C = Operator('C') +x, y, z = symbols('x y z') + + +def test_bra_ket(): + assert b1*k1 == InnerProduct(b1, k1) + assert k1*b1 == OuterProduct(k1, b1) + # Test priority of inner product + assert OuterProduct(k1, b1)*k2 == InnerProduct(b1, k2)*k1 + assert b1*OuterProduct(k1, b2) == InnerProduct(b1, k1)*b2 + + +def test_tensor_product(): + # We are attempting to be rigourous and raise TypeError when a user tries + # to combine bras, kets, and operators in a manner that doesn't make sense. + # In particular, we are not trying to interpret regular ``*`` multiplication + # as a tensor product. + with raises(TypeError): + k1*k1 + with raises(TypeError): + b1*b1 + with raises(TypeError): + k1*TensorProduct(k2, k3) + with raises(TypeError): + b1*TensorProduct(b2, b3) + with raises(TypeError): + TensorProduct(k2, k3)*k1 + with raises(TypeError): + TensorProduct(b2, b3)*b1 + + assert TensorProduct(A, B, C)*TensorProduct(k1, k2, k3) == \ + TensorProduct(A*k1, B*k2, C*k3) + assert TensorProduct(b1, b2, b3)*TensorProduct(A, B, C) == \ + TensorProduct(b1*A, b2*B, b3*C) + assert TensorProduct(b1, b2, b3)*TensorProduct(k1, k2, k3) == \ + InnerProduct(b1, k1)*InnerProduct(b2, k2)*InnerProduct(b3, k3) + assert TensorProduct(b1, b2, b3)*TensorProduct(A, B, C)*TensorProduct(k1, k2, k3) == \ + TensorProduct(b1*A*k1, b2*B*k2, b3*C*k3) + + +def test_outer_product(): + assert OuterProduct(k1, b1)*OuterProduct(k2, b2) == \ + InnerProduct(b1, k2)*OuterProduct(k1, b2) + + +def test_compound(): + e1 = b1*A*B*k1*b2*k2*b3 + assert e1 == InnerProduct(b2, k2)*b1*A*B*OuterProduct(k1, b3) + + e2 = TensorProduct(k1, k2)*TensorProduct(b1, b2) + assert e2 == TensorProduct( + OuterProduct(k1, b1), + OuterProduct(k2, b2) + ) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/trace.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/trace.py new file mode 100644 index 0000000000000000000000000000000000000000..03ab18f78a1bfcf5bfcd679f00eac8685144fd8c --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/trace.py @@ -0,0 +1,230 @@ +from sympy.core.add import Add +from sympy.core.containers import Tuple +from sympy.core.expr import Expr +from sympy.core.mul import Mul +from sympy.core.power import Pow +from sympy.core.sorting import default_sort_key +from sympy.core.sympify import sympify +from sympy.matrices import Matrix + + +def _is_scalar(e): + """ Helper method used in Tr""" + + # sympify to set proper attributes + e = sympify(e) + if isinstance(e, Expr): + if (e.is_Integer or e.is_Float or + e.is_Rational or e.is_Number or + (e.is_Symbol and e.is_commutative) + ): + return True + + return False + + +def _cycle_permute(l): + """ Cyclic permutations based on canonical ordering + + Explanation + =========== + + This method does the sort based ascii values while + a better approach would be to used lexicographic sort. + + TODO: Handle condition such as symbols have subscripts/superscripts + in case of lexicographic sort + + """ + + if len(l) == 1: + return l + + min_item = min(l, key=default_sort_key) + indices = [i for i, x in enumerate(l) if x == min_item] + + le = list(l) + le.extend(l) # duplicate and extend string for easy processing + + # adding the first min_item index back for easier looping + indices.append(len(l) + indices[0]) + + # create sublist of items with first item as min_item and last_item + # in each of the sublist is item just before the next occurrence of + # minitem in the cycle formed. + sublist = [[le[indices[i]:indices[i + 1]]] for i in + range(len(indices) - 1)] + + # we do comparison of strings by comparing elements + # in each sublist + idx = sublist.index(min(sublist)) + ordered_l = le[indices[idx]:indices[idx] + len(l)] + + return ordered_l + + +def _rearrange_args(l): + """ this just moves the last arg to first position + to enable expansion of args + A,B,A ==> A**2,B + """ + if len(l) == 1: + return l + + x = list(l[-1:]) + x.extend(l[0:-1]) + return Mul(*x).args + + +class Tr(Expr): + """ Generic Trace operation than can trace over: + + a) SymPy matrix + b) operators + c) outer products + + Parameters + ========== + o : operator, matrix, expr + i : tuple/list indices (optional) + + Examples + ======== + + # TODO: Need to handle printing + + a) Trace(A+B) = Tr(A) + Tr(B) + b) Trace(scalar*Operator) = scalar*Trace(Operator) + + >>> from sympy.physics.quantum.trace import Tr + >>> from sympy import symbols, Matrix + >>> a, b = symbols('a b', commutative=True) + >>> A, B = symbols('A B', commutative=False) + >>> Tr(a*A,[2]) + a*Tr(A) + >>> m = Matrix([[1,2],[1,1]]) + >>> Tr(m) + 2 + + """ + def __new__(cls, *args): + """ Construct a Trace object. + + Parameters + ========== + args = SymPy expression + indices = tuple/list if indices, optional + + """ + + # expect no indices,int or a tuple/list/Tuple + if (len(args) == 2): + if not isinstance(args[1], (list, Tuple, tuple)): + indices = Tuple(args[1]) + else: + indices = Tuple(*args[1]) + + expr = args[0] + elif (len(args) == 1): + indices = Tuple() + expr = args[0] + else: + raise ValueError("Arguments to Tr should be of form " + "(expr[, [indices]])") + + if isinstance(expr, Matrix): + return expr.trace() + elif hasattr(expr, 'trace') and callable(expr.trace): + #for any objects that have trace() defined e.g numpy + return expr.trace() + elif isinstance(expr, Add): + return Add(*[Tr(arg, indices) for arg in expr.args]) + elif isinstance(expr, Mul): + c_part, nc_part = expr.args_cnc() + if len(nc_part) == 0: + return Mul(*c_part) + else: + obj = Expr.__new__(cls, Mul(*nc_part), indices ) + #this check is needed to prevent cached instances + #being returned even if len(c_part)==0 + return Mul(*c_part)*obj if len(c_part) > 0 else obj + elif isinstance(expr, Pow): + if (_is_scalar(expr.args[0]) and + _is_scalar(expr.args[1])): + return expr + else: + return Expr.__new__(cls, expr, indices) + else: + if (_is_scalar(expr)): + return expr + + return Expr.__new__(cls, expr, indices) + + @property + def kind(self): + expr = self.args[0] + expr_kind = expr.kind + return expr_kind.element_kind + + def doit(self, **hints): + """ Perform the trace operation. + + #TODO: Current version ignores the indices set for partial trace. + + >>> from sympy.physics.quantum.trace import Tr + >>> from sympy.physics.quantum.operator import OuterProduct + >>> from sympy.physics.quantum.spin import JzKet, JzBra + >>> t = Tr(OuterProduct(JzKet(1,1), JzBra(1,1))) + >>> t.doit() + 1 + + """ + if hasattr(self.args[0], '_eval_trace'): + return self.args[0]._eval_trace(indices=self.args[1]) + + return self + + @property + def is_number(self): + # TODO : improve this implementation + return True + + #TODO: Review if the permute method is needed + # and if it needs to return a new instance + def permute(self, pos): + """ Permute the arguments cyclically. + + Parameters + ========== + + pos : integer, if positive, shift-right, else shift-left + + Examples + ======== + + >>> from sympy.physics.quantum.trace import Tr + >>> from sympy import symbols + >>> A, B, C, D = symbols('A B C D', commutative=False) + >>> t = Tr(A*B*C*D) + >>> t.permute(2) + Tr(C*D*A*B) + >>> t.permute(-2) + Tr(C*D*A*B) + + """ + if pos > 0: + pos = pos % len(self.args[0].args) + else: + pos = -(abs(pos) % len(self.args[0].args)) + + args = list(self.args[0].args[-pos:] + self.args[0].args[0:-pos]) + + return Tr(Mul(*(args))) + + def _hashable_content(self): + if isinstance(self.args[0], Mul): + args = _cycle_permute(_rearrange_args(self.args[0].args)) + else: + args = [self.args[0]] + + return tuple(args) + (self.args[1], ) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/quantum/transforms.py b/venv/lib/python3.11/site-packages/sympy/physics/quantum/transforms.py new file mode 100644 index 0000000000000000000000000000000000000000..dcbbcd9040b4f8f987375c2f903031610d6f9061 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/quantum/transforms.py @@ -0,0 +1,291 @@ +"""Transforms that are always applied to quantum expressions. + +This module uses the kind and _constructor_postprocessor_mapping APIs +to transform different combinations of Operators, Bras, and Kets into +Inner/Outer/TensorProducts. These transformations are registered +with the postprocessing API of core classes like `Mul` and `Pow` and +are always applied to any expression involving Bras, Kets, and +Operators. This API replaces the custom `__mul__` and `__pow__` +methods of the quantum classes, which were found to be inconsistent. + +THIS IS EXPERIMENTAL. +""" +from sympy.core.basic import Basic +from sympy.core.expr import Expr +from sympy.core.mul import Mul +from sympy.core.singleton import S +from sympy.multipledispatch.dispatcher import ( + Dispatcher, ambiguity_register_error_ignore_dup +) +from sympy.utilities.misc import debug + +from sympy.physics.quantum.innerproduct import InnerProduct +from sympy.physics.quantum.kind import KetKind, BraKind, OperatorKind +from sympy.physics.quantum.operator import ( + OuterProduct, IdentityOperator, Operator +) +from sympy.physics.quantum.state import BraBase, KetBase, StateBase +from sympy.physics.quantum.tensorproduct import TensorProduct + + +#----------------------------------------------------------------------------- +# Multipledispatch based transformed for Mul and Pow +#----------------------------------------------------------------------------- + +_transform_state_pair = Dispatcher('_transform_state_pair') +"""Transform a pair of expression in a Mul to their canonical form. + +All functions that are registered with this dispatcher need to take +two inputs and return either tuple of transformed outputs, or None if no +transform is applied. The output tuple is inserted into the right place +of the ``Mul`` that is being put into canonical form. It works something like +the following: + +``Mul(a, b, c, d, e, f) -> Mul(*(_transform_state_pair(a, b) + (c, d, e, f))))`` + +The transforms here are always applied when quantum objects are multiplied. + +THIS IS EXPERIMENTAL. + +However, users of ``sympy.physics.quantum`` can import this dispatcher and +register their own transforms to control the canonical form of products +of quantum expressions. +""" + +@_transform_state_pair.register(Expr, Expr) +def _transform_expr(a, b): + """Default transformer that does nothing for base types.""" + return None + + +# The identity times anything is the anything. +_transform_state_pair.add( + (IdentityOperator, Expr), + lambda x, y: (y,), + on_ambiguity=ambiguity_register_error_ignore_dup +) +_transform_state_pair.add( + (Expr, IdentityOperator), + lambda x, y: (x,), + on_ambiguity=ambiguity_register_error_ignore_dup +) +_transform_state_pair.add( + (IdentityOperator, IdentityOperator), + lambda x, y: S.One, + on_ambiguity=ambiguity_register_error_ignore_dup +) + +@_transform_state_pair.register(BraBase, KetBase) +def _transform_bra_ket(a, b): + """Transform a bra*ket -> InnerProduct(bra, ket).""" + return (InnerProduct(a, b),) + +@_transform_state_pair.register(KetBase, BraBase) +def _transform_ket_bra(a, b): + """Transform a keT*bra -> OuterProduct(ket, bra).""" + return (OuterProduct(a, b),) + +@_transform_state_pair.register(KetBase, KetBase) +def _transform_ket_ket(a, b): + """Raise a TypeError if a user tries to multiply two kets. + + Multiplication based on `*` is not a shorthand for tensor products. + """ + raise TypeError( + 'Multiplication of two kets is not allowed. Use TensorProduct instead.' + ) + +@_transform_state_pair.register(BraBase, BraBase) +def _transform_bra_bra(a, b): + """Raise a TypeError if a user tries to multiply two bras. + + Multiplication based on `*` is not a shorthand for tensor products. + """ + raise TypeError( + 'Multiplication of two bras is not allowed. Use TensorProduct instead.' + ) + +@_transform_state_pair.register(OuterProduct, KetBase) +def _transform_op_ket(a, b): + return (InnerProduct(a.bra, b), a.ket) + +@_transform_state_pair.register(BraBase, OuterProduct) +def _transform_bra_op(a, b): + return (InnerProduct(a, b.ket), b.bra) + +@_transform_state_pair.register(TensorProduct, KetBase) +def _transform_tp_ket(a, b): + """Raise a TypeError if a user tries to multiply TensorProduct(*kets)*ket. + + Multiplication based on `*` is not a shorthand for tensor products. + """ + if a.kind == KetKind: + raise TypeError( + 'Multiplication of TensorProduct(*kets)*ket is invalid.' + ) + +@_transform_state_pair.register(KetBase, TensorProduct) +def _transform_ket_tp(a, b): + """Raise a TypeError if a user tries to multiply ket*TensorProduct(*kets). + + Multiplication based on `*` is not a shorthand for tensor products. + """ + if b.kind == KetKind: + raise TypeError( + 'Multiplication of ket*TensorProduct(*kets) is invalid.' + ) + +@_transform_state_pair.register(TensorProduct, BraBase) +def _transform_tp_bra(a, b): + """Raise a TypeError if a user tries to multiply TensorProduct(*bras)*bra. + + Multiplication based on `*` is not a shorthand for tensor products. + """ + if a.kind == BraKind: + raise TypeError( + 'Multiplication of TensorProduct(*bras)*bra is invalid.' + ) + +@_transform_state_pair.register(BraBase, TensorProduct) +def _transform_bra_tp(a, b): + """Raise a TypeError if a user tries to multiply bra*TensorProduct(*bras). + + Multiplication based on `*` is not a shorthand for tensor products. + """ + if b.kind == BraKind: + raise TypeError( + 'Multiplication of bra*TensorProduct(*bras) is invalid.' + ) + +@_transform_state_pair.register(TensorProduct, TensorProduct) +def _transform_tp_tp(a, b): + """Combine a product of tensor products if their number of args matches.""" + debug('_transform_tp_tp', a, b) + if len(a.args) == len(b.args): + if a.kind == BraKind and b.kind == KetKind: + return tuple([InnerProduct(i, j) for (i, j) in zip(a.args, b.args)]) + else: + return (TensorProduct(*(i*j for (i, j) in zip(a.args, b.args))), ) + +@_transform_state_pair.register(OuterProduct, OuterProduct) +def _transform_op_op(a, b): + """Extract an inner produt from a product of outer products.""" + return (InnerProduct(a.bra, b.ket), OuterProduct(a.ket, b.bra)) + + +#----------------------------------------------------------------------------- +# Postprocessing transforms for Mul and Pow +#----------------------------------------------------------------------------- + + +def _postprocess_state_mul(expr): + """Transform a ``Mul`` of quantum expressions into canonical form. + + This function is registered ``_constructor_postprocessor_mapping`` as a + transformer for ``Mul``. This means that every time a quantum expression + is multiplied, this function will be called to transform it into canonical + form as defined by the binary functions registered with + ``_transform_state_pair``. + + The algorithm of this function is as follows. It walks the args + of the input ``Mul`` from left to right and calls ``_transform_state_pair`` + on every overlapping pair of args. Each time ``_transform_state_pair`` + is called it can return a tuple of items or None. If None, the pair isn't + transformed. If a tuple, then the last element of the tuple goes back into + the args to be transformed again and the others are extended onto the result + args list. + + The algorithm can be visualized in the following table: + + step result args + ============================================================================ + #0 [] [a, b, c, d, e, f] + #1 [] [T(a,b), c, d, e, f] + #2 [T(a,b)[:-1]] [T(a,b)[-1], c, d, e, f] + #3 [T(a,b)[:-1]] [T(T(a,b)[-1], c), d, e, f] + #4 [T(a,b)[:-1], T(T(a,b)[-1], c)[:-1]] [T(T(T(a,b)[-1], c)[-1], d), e, f] + #5 ... + + One limitation of the current implementation is that we assume that only the + last item of the transformed tuple goes back into the args to be transformed + again. These seems to handle the cases needed for Mul. However, we may need + to extend the algorithm to have the entire tuple go back into the args for + further transformation. + """ + args = list(expr.args) + result = [] + + # Continue as long as we have at least 2 elements + while len(args) > 1: + # Get first two elements + first = args.pop(0) + second = args[0] # Look at second element without popping yet + + transformed = _transform_state_pair(first, second) + + if transformed is None: + # If transform returns None, append first element + result.append(first) + else: + # This item was transformed, pop and discard + args.pop(0) + # The last item goes back to be transformed again + args.insert(0, transformed[-1]) + # All other items go directly into the result + result.extend(transformed[:-1]) + + # Append any remaining element + if args: + result.append(args[0]) + + return Mul._from_args(result, is_commutative=False) + + +def _postprocess_state_pow(expr): + """Handle bras and kets raised to powers. + + Under ``*`` multiplication this is invalid. Users should use a + TensorProduct instead. + """ + base, exp = expr.as_base_exp() + if base.kind == KetKind or base.kind == BraKind: + raise TypeError( + 'A bra or ket to a power is invalid, use TensorProduct instead.' + ) + + +def _postprocess_tp_pow(expr): + """Handle TensorProduct(*operators)**(positive integer). + + This handles a tensor product of operators, to an integer power. + The power here is interpreted as regular multiplication, not + tensor product exponentiation. The form of exponentiation performed + here leaves the space and dimension of the object the same. + + This operation does not make sense for tensor product's of states. + """ + base, exp = expr.as_base_exp() + debug('_postprocess_tp_pow: ', base, exp, expr.args) + if isinstance(base, TensorProduct) and exp.is_integer and exp.is_positive and base.kind == OperatorKind: + new_args = [a**exp for a in base.args] + return TensorProduct(*new_args) + + +#----------------------------------------------------------------------------- +# Register the transformers with Basic._constructor_postprocessor_mapping +#----------------------------------------------------------------------------- + + +Basic._constructor_postprocessor_mapping[StateBase] = { + "Mul": [_postprocess_state_mul], + "Pow": [_postprocess_state_pow] +} + +Basic._constructor_postprocessor_mapping[TensorProduct] = { + "Mul": [_postprocess_state_mul], + "Pow": [_postprocess_tp_pow] +} + +Basic._constructor_postprocessor_mapping[Operator] = { + "Mul": [_postprocess_state_mul] +} diff --git a/venv/lib/python3.11/site-packages/sympy/physics/secondquant.py b/venv/lib/python3.11/site-packages/sympy/physics/secondquant.py new file mode 100644 index 0000000000000000000000000000000000000000..189e8e8b50c785759b03f19f28285f7988cfca75 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/secondquant.py @@ -0,0 +1,3125 @@ +""" +Second quantization operators and states for bosons. + +This follow the formulation of Fetter and Welecka, "Quantum Theory +of Many-Particle Systems." +""" +from collections import defaultdict + +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.cache import cacheit +from sympy.core.containers import Tuple +from sympy.core.expr import Expr +from sympy.core.function import Function +from sympy.core.mul import Mul +from sympy.core.numbers import I +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.sorting import default_sort_key +from sympy.core.symbol import Dummy, Symbol +from sympy.core.sympify import sympify +from sympy.functions.elementary.complexes import conjugate +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.matrices.dense import zeros +from sympy.printing.str import StrPrinter +from sympy.utilities.iterables import has_dups + +__all__ = [ + 'Dagger', + 'KroneckerDelta', + 'BosonicOperator', + 'AnnihilateBoson', + 'CreateBoson', + 'AnnihilateFermion', + 'CreateFermion', + 'FockState', + 'FockStateBra', + 'FockStateKet', + 'FockStateBosonKet', + 'FockStateBosonBra', + 'FockStateFermionKet', + 'FockStateFermionBra', + 'BBra', + 'BKet', + 'FBra', + 'FKet', + 'F', + 'Fd', + 'B', + 'Bd', + 'apply_operators', + 'InnerProduct', + 'BosonicBasis', + 'VarBosonicBasis', + 'FixedBosonicBasis', + 'Commutator', + 'matrix_rep', + 'contraction', + 'wicks', + 'NO', + 'evaluate_deltas', + 'AntiSymmetricTensor', + 'substitute_dummies', + 'PermutationOperator', + 'simplify_index_permutations', +] + + +class SecondQuantizationError(Exception): + pass + + +class AppliesOnlyToSymbolicIndex(SecondQuantizationError): + pass + + +class ContractionAppliesOnlyToFermions(SecondQuantizationError): + pass + + +class ViolationOfPauliPrinciple(SecondQuantizationError): + pass + + +class SubstitutionOfAmbigousOperatorFailed(SecondQuantizationError): + pass + + +class WicksTheoremDoesNotApply(SecondQuantizationError): + pass + + +class Dagger(Expr): + """ + Hermitian conjugate of creation/annihilation operators. + + Examples + ======== + + >>> from sympy import I + >>> from sympy.physics.secondquant import Dagger, B, Bd + >>> Dagger(2*I) + -2*I + >>> Dagger(B(0)) + CreateBoson(0) + >>> Dagger(Bd(0)) + AnnihilateBoson(0) + + """ + + def __new__(cls, arg): + arg = sympify(arg) + r = cls.eval(arg) + if isinstance(r, Basic): + return r + obj = Basic.__new__(cls, arg) + return obj + + @classmethod + def eval(cls, arg): + """ + Evaluates the Dagger instance. + + Examples + ======== + + >>> from sympy import I + >>> from sympy.physics.secondquant import Dagger, B, Bd + >>> Dagger(2*I) + -2*I + >>> Dagger(B(0)) + CreateBoson(0) + >>> Dagger(Bd(0)) + AnnihilateBoson(0) + + The eval() method is called automatically. + + """ + dagger = getattr(arg, '_dagger_', None) + if dagger is not None: + return dagger() + if isinstance(arg, Symbol) and arg.is_commutative: + return conjugate(arg) + if isinstance(arg, Basic): + if arg.is_Add: + return Add(*tuple(map(Dagger, arg.args))) + if arg.is_Mul: + return Mul(*tuple(map(Dagger, reversed(arg.args)))) + if arg.is_Number: + return arg + if arg.is_Pow: + return Pow(Dagger(arg.args[0]), arg.args[1]) + if arg == I: + return -arg + if isinstance(arg, Function): + if all(a.is_commutative for a in arg.args): + return arg.func(*[Dagger(a) for a in arg.args]) + else: + return None + + def _dagger_(self): + return self.args[0] + + +class TensorSymbol(Expr): + + is_commutative = True + + +class AntiSymmetricTensor(TensorSymbol): + """Stores upper and lower indices in separate Tuple's. + + Each group of indices is assumed to be antisymmetric. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.secondquant import AntiSymmetricTensor + >>> i, j = symbols('i j', below_fermi=True) + >>> a, b = symbols('a b', above_fermi=True) + >>> AntiSymmetricTensor('v', (a, i), (b, j)) + AntiSymmetricTensor(v, (a, i), (b, j)) + >>> AntiSymmetricTensor('v', (i, a), (b, j)) + -AntiSymmetricTensor(v, (a, i), (b, j)) + + As you can see, the indices are automatically sorted to a canonical form. + + """ + + def __new__(cls, symbol, upper, lower): + + try: + upper, signu = _sort_anticommuting_fermions( + upper, key=_sqkey_index) + lower, signl = _sort_anticommuting_fermions( + lower, key=_sqkey_index) + + except ViolationOfPauliPrinciple: + return S.Zero + + symbol = sympify(symbol) + upper = Tuple(*upper) + lower = Tuple(*lower) + + if (signu + signl) % 2: + return -TensorSymbol.__new__(cls, symbol, upper, lower) + else: + + return TensorSymbol.__new__(cls, symbol, upper, lower) + + def _latex(self, printer): + return "{%s^{%s}_{%s}}" % ( + self.symbol, + "".join([ printer._print(i) for i in self.args[1]]), + "".join([ printer._print(i) for i in self.args[2]]) + ) + + @property + def symbol(self): + """ + Returns the symbol of the tensor. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.secondquant import AntiSymmetricTensor + >>> i, j = symbols('i,j', below_fermi=True) + >>> a, b = symbols('a,b', above_fermi=True) + >>> AntiSymmetricTensor('v', (a, i), (b, j)) + AntiSymmetricTensor(v, (a, i), (b, j)) + >>> AntiSymmetricTensor('v', (a, i), (b, j)).symbol + v + + """ + return self.args[0] + + @property + def upper(self): + """ + Returns the upper indices. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.secondquant import AntiSymmetricTensor + >>> i, j = symbols('i,j', below_fermi=True) + >>> a, b = symbols('a,b', above_fermi=True) + >>> AntiSymmetricTensor('v', (a, i), (b, j)) + AntiSymmetricTensor(v, (a, i), (b, j)) + >>> AntiSymmetricTensor('v', (a, i), (b, j)).upper + (a, i) + + + """ + return self.args[1] + + @property + def lower(self): + """ + Returns the lower indices. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.secondquant import AntiSymmetricTensor + >>> i, j = symbols('i,j', below_fermi=True) + >>> a, b = symbols('a,b', above_fermi=True) + >>> AntiSymmetricTensor('v', (a, i), (b, j)) + AntiSymmetricTensor(v, (a, i), (b, j)) + >>> AntiSymmetricTensor('v', (a, i), (b, j)).lower + (b, j) + + """ + return self.args[2] + + def __str__(self): + return "%s(%s,%s)" % self.args + + +class SqOperator(Expr): + """ + Base class for Second Quantization operators. + """ + + op_symbol = 'sq' + + is_commutative = False + + def __new__(cls, k): + obj = Basic.__new__(cls, sympify(k)) + return obj + + @property + def state(self): + """ + Returns the state index related to this operator. + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import F, Fd, B, Bd + >>> p = Symbol('p') + >>> F(p).state + p + >>> Fd(p).state + p + >>> B(p).state + p + >>> Bd(p).state + p + + """ + return self.args[0] + + @property + def is_symbolic(self): + """ + Returns True if the state is a symbol (as opposed to a number). + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import F + >>> p = Symbol('p') + >>> F(p).is_symbolic + True + >>> F(1).is_symbolic + False + + """ + if self.state.is_Integer: + return False + else: + return True + + def __repr__(self): + return NotImplemented + + def __str__(self): + return "%s(%r)" % (self.op_symbol, self.state) + + def apply_operator(self, state): + """ + Applies an operator to itself. + """ + raise NotImplementedError('implement apply_operator in a subclass') + + +class BosonicOperator(SqOperator): + pass + + +class Annihilator(SqOperator): + pass + + +class Creator(SqOperator): + pass + + +class AnnihilateBoson(BosonicOperator, Annihilator): + """ + Bosonic annihilation operator. + + Examples + ======== + + >>> from sympy.physics.secondquant import B + >>> from sympy.abc import x + >>> B(x) + AnnihilateBoson(x) + """ + + op_symbol = 'b' + + def _dagger_(self): + return CreateBoson(self.state) + + def apply_operator(self, state): + """ + Apply state to self if self is not symbolic and state is a FockStateKet, else + multiply self by state. + + Examples + ======== + + >>> from sympy.physics.secondquant import B, BKet + >>> from sympy.abc import x, y, n + >>> B(x).apply_operator(y) + y*AnnihilateBoson(x) + >>> B(0).apply_operator(BKet((n,))) + sqrt(n)*FockStateBosonKet((n - 1,)) + + """ + if not self.is_symbolic and isinstance(state, FockStateKet): + element = self.state + amp = sqrt(state[element]) + return amp*state.down(element) + else: + return Mul(self, state) + + def __repr__(self): + return "AnnihilateBoson(%s)" % self.state + + def _latex(self, printer): + if self.state is S.Zero: + return "b_{0}" + else: + return "b_{%s}" % printer._print(self.state) + +class CreateBoson(BosonicOperator, Creator): + """ + Bosonic creation operator. + """ + + op_symbol = 'b+' + + def _dagger_(self): + return AnnihilateBoson(self.state) + + def apply_operator(self, state): + """ + Apply state to self if self is not symbolic and state is a FockStateKet, else + multiply self by state. + + Examples + ======== + + >>> from sympy.physics.secondquant import B, Dagger, BKet + >>> from sympy.abc import x, y, n + >>> Dagger(B(x)).apply_operator(y) + y*CreateBoson(x) + >>> B(0).apply_operator(BKet((n,))) + sqrt(n)*FockStateBosonKet((n - 1,)) + """ + if not self.is_symbolic and isinstance(state, FockStateKet): + element = self.state + amp = sqrt(state[element] + 1) + return amp*state.up(element) + else: + return Mul(self, state) + + def __repr__(self): + return "CreateBoson(%s)" % self.state + + def _latex(self, printer): + if self.state is S.Zero: + return "{b^\\dagger_{0}}" + else: + return "{b^\\dagger_{%s}}" % printer._print(self.state) + +B = AnnihilateBoson +Bd = CreateBoson + + +class FermionicOperator(SqOperator): + + @property + def is_restricted(self): + """ + Is this FermionicOperator restricted with respect to fermi level? + + Returns + ======= + + 1 : restricted to orbits above fermi + 0 : no restriction + -1 : restricted to orbits below fermi + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import F, Fd + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + + >>> F(a).is_restricted + 1 + >>> Fd(a).is_restricted + 1 + >>> F(i).is_restricted + -1 + >>> Fd(i).is_restricted + -1 + >>> F(p).is_restricted + 0 + >>> Fd(p).is_restricted + 0 + + """ + ass = self.args[0].assumptions0 + if ass.get("below_fermi"): + return -1 + if ass.get("above_fermi"): + return 1 + return 0 + + @property + def is_above_fermi(self): + """ + Does the index of this FermionicOperator allow values above fermi? + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import F + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + + >>> F(a).is_above_fermi + True + >>> F(i).is_above_fermi + False + >>> F(p).is_above_fermi + True + + Note + ==== + + The same applies to creation operators Fd + + """ + return not self.args[0].assumptions0.get("below_fermi") + + @property + def is_below_fermi(self): + """ + Does the index of this FermionicOperator allow values below fermi? + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import F + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + + >>> F(a).is_below_fermi + False + >>> F(i).is_below_fermi + True + >>> F(p).is_below_fermi + True + + The same applies to creation operators Fd + + """ + return not self.args[0].assumptions0.get("above_fermi") + + @property + def is_only_below_fermi(self): + """ + Is the index of this FermionicOperator restricted to values below fermi? + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import F + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + + >>> F(a).is_only_below_fermi + False + >>> F(i).is_only_below_fermi + True + >>> F(p).is_only_below_fermi + False + + The same applies to creation operators Fd + """ + return self.is_below_fermi and not self.is_above_fermi + + @property + def is_only_above_fermi(self): + """ + Is the index of this FermionicOperator restricted to values above fermi? + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import F + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + + >>> F(a).is_only_above_fermi + True + >>> F(i).is_only_above_fermi + False + >>> F(p).is_only_above_fermi + False + + The same applies to creation operators Fd + """ + return self.is_above_fermi and not self.is_below_fermi + + def _sortkey(self): + h = hash(self) + label = str(self.args[0]) + + if self.is_only_q_creator: + return 1, label, h + if self.is_only_q_annihilator: + return 4, label, h + if isinstance(self, Annihilator): + return 3, label, h + if isinstance(self, Creator): + return 2, label, h + + +class AnnihilateFermion(FermionicOperator, Annihilator): + """ + Fermionic annihilation operator. + """ + + op_symbol = 'f' + + def _dagger_(self): + return CreateFermion(self.state) + + def apply_operator(self, state): + """ + Apply state to self if self is not symbolic and state is a FockStateKet, else + multiply self by state. + + Examples + ======== + + >>> from sympy.physics.secondquant import B, Dagger, BKet + >>> from sympy.abc import x, y, n + >>> Dagger(B(x)).apply_operator(y) + y*CreateBoson(x) + >>> B(0).apply_operator(BKet((n,))) + sqrt(n)*FockStateBosonKet((n - 1,)) + """ + if isinstance(state, FockStateFermionKet): + element = self.state + return state.down(element) + + elif isinstance(state, Mul): + c_part, nc_part = state.args_cnc() + if isinstance(nc_part[0], FockStateFermionKet): + element = self.state + return Mul(*(c_part + [nc_part[0].down(element)] + nc_part[1:])) + else: + return Mul(self, state) + + else: + return Mul(self, state) + + @property + def is_q_creator(self): + """ + Can we create a quasi-particle? (create hole or create particle) + If so, would that be above or below the fermi surface? + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import F + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + + >>> F(a).is_q_creator + 0 + >>> F(i).is_q_creator + -1 + >>> F(p).is_q_creator + -1 + + """ + if self.is_below_fermi: + return -1 + return 0 + + @property + def is_q_annihilator(self): + """ + Can we destroy a quasi-particle? (annihilate hole or annihilate particle) + If so, would that be above or below the fermi surface? + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import F + >>> a = Symbol('a', above_fermi=1) + >>> i = Symbol('i', below_fermi=1) + >>> p = Symbol('p') + + >>> F(a).is_q_annihilator + 1 + >>> F(i).is_q_annihilator + 0 + >>> F(p).is_q_annihilator + 1 + + """ + if self.is_above_fermi: + return 1 + return 0 + + @property + def is_only_q_creator(self): + """ + Always create a quasi-particle? (create hole or create particle) + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import F + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + + >>> F(a).is_only_q_creator + False + >>> F(i).is_only_q_creator + True + >>> F(p).is_only_q_creator + False + + """ + return self.is_only_below_fermi + + @property + def is_only_q_annihilator(self): + """ + Always destroy a quasi-particle? (annihilate hole or annihilate particle) + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import F + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + + >>> F(a).is_only_q_annihilator + True + >>> F(i).is_only_q_annihilator + False + >>> F(p).is_only_q_annihilator + False + + """ + return self.is_only_above_fermi + + def __repr__(self): + return "AnnihilateFermion(%s)" % self.state + + def _latex(self, printer): + if self.state is S.Zero: + return "a_{0}" + else: + return "a_{%s}" % printer._print(self.state) + + +class CreateFermion(FermionicOperator, Creator): + """ + Fermionic creation operator. + """ + + op_symbol = 'f+' + + def _dagger_(self): + return AnnihilateFermion(self.state) + + def apply_operator(self, state): + """ + Apply state to self if self is not symbolic and state is a FockStateKet, else + multiply self by state. + + Examples + ======== + + >>> from sympy.physics.secondquant import B, Dagger, BKet + >>> from sympy.abc import x, y, n + >>> Dagger(B(x)).apply_operator(y) + y*CreateBoson(x) + >>> B(0).apply_operator(BKet((n,))) + sqrt(n)*FockStateBosonKet((n - 1,)) + """ + if isinstance(state, FockStateFermionKet): + element = self.state + return state.up(element) + + elif isinstance(state, Mul): + c_part, nc_part = state.args_cnc() + if isinstance(nc_part[0], FockStateFermionKet): + element = self.state + return Mul(*(c_part + [nc_part[0].up(element)] + nc_part[1:])) + + return Mul(self, state) + + @property + def is_q_creator(self): + """ + Can we create a quasi-particle? (create hole or create particle) + If so, would that be above or below the fermi surface? + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import Fd + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + + >>> Fd(a).is_q_creator + 1 + >>> Fd(i).is_q_creator + 0 + >>> Fd(p).is_q_creator + 1 + + """ + if self.is_above_fermi: + return 1 + return 0 + + @property + def is_q_annihilator(self): + """ + Can we destroy a quasi-particle? (annihilate hole or annihilate particle) + If so, would that be above or below the fermi surface? + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import Fd + >>> a = Symbol('a', above_fermi=1) + >>> i = Symbol('i', below_fermi=1) + >>> p = Symbol('p') + + >>> Fd(a).is_q_annihilator + 0 + >>> Fd(i).is_q_annihilator + -1 + >>> Fd(p).is_q_annihilator + -1 + + """ + if self.is_below_fermi: + return -1 + return 0 + + @property + def is_only_q_creator(self): + """ + Always create a quasi-particle? (create hole or create particle) + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import Fd + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + + >>> Fd(a).is_only_q_creator + True + >>> Fd(i).is_only_q_creator + False + >>> Fd(p).is_only_q_creator + False + + """ + return self.is_only_above_fermi + + @property + def is_only_q_annihilator(self): + """ + Always destroy a quasi-particle? (annihilate hole or annihilate particle) + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import Fd + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + + >>> Fd(a).is_only_q_annihilator + False + >>> Fd(i).is_only_q_annihilator + True + >>> Fd(p).is_only_q_annihilator + False + + """ + return self.is_only_below_fermi + + def __repr__(self): + return "CreateFermion(%s)" % self.state + + def _latex(self, printer): + if self.state is S.Zero: + return "{a^\\dagger_{0}}" + else: + return "{a^\\dagger_{%s}}" % printer._print(self.state) + +Fd = CreateFermion +F = AnnihilateFermion + + +class FockState(Expr): + """ + Many particle Fock state with a sequence of occupation numbers. + + Anywhere you can have a FockState, you can also have S.Zero. + All code must check for this! + + Base class to represent FockStates. + """ + is_commutative = False + + def __new__(cls, occupations): + """ + occupations is a list with two possible meanings: + + - For bosons it is a list of occupation numbers. + Element i is the number of particles in state i. + + - For fermions it is a list of occupied orbits. + Element 0 is the state that was occupied first, element i + is the i'th occupied state. + """ + occupations = list(map(sympify, occupations)) + obj = Basic.__new__(cls, Tuple(*occupations)) + return obj + + def __getitem__(self, i): + i = int(i) + return self.args[0][i] + + def __repr__(self): + return ("FockState(%r)") % (self.args) + + def __str__(self): + return "%s%r%s" % (getattr(self, 'lbracket', ""), self._labels(), getattr(self, 'rbracket', "")) + + def _labels(self): + return self.args[0] + + def __len__(self): + return len(self.args[0]) + + def _latex(self, printer): + return "%s%s%s" % (getattr(self, 'lbracket_latex', ""), printer._print(self._labels()), getattr(self, 'rbracket_latex', "")) + + +class BosonState(FockState): + """ + Base class for FockStateBoson(Ket/Bra). + """ + + def up(self, i): + """ + Performs the action of a creation operator. + + Examples + ======== + + >>> from sympy.physics.secondquant import BBra + >>> b = BBra([1, 2]) + >>> b + FockStateBosonBra((1, 2)) + >>> b.up(1) + FockStateBosonBra((1, 3)) + """ + i = int(i) + new_occs = list(self.args[0]) + new_occs[i] = new_occs[i] + S.One + return self.__class__(new_occs) + + def down(self, i): + """ + Performs the action of an annihilation operator. + + Examples + ======== + + >>> from sympy.physics.secondquant import BBra + >>> b = BBra([1, 2]) + >>> b + FockStateBosonBra((1, 2)) + >>> b.down(1) + FockStateBosonBra((1, 1)) + """ + i = int(i) + new_occs = list(self.args[0]) + if new_occs[i] == S.Zero: + return S.Zero + else: + new_occs[i] = new_occs[i] - S.One + return self.__class__(new_occs) + + +class FermionState(FockState): + """ + Base class for FockStateFermion(Ket/Bra). + """ + + fermi_level = 0 + + def __new__(cls, occupations, fermi_level=0): + occupations = list(map(sympify, occupations)) + if len(occupations) > 1: + try: + (occupations, sign) = _sort_anticommuting_fermions( + occupations, key=_sqkey_index) + except ViolationOfPauliPrinciple: + return S.Zero + else: + sign = 0 + + cls.fermi_level = fermi_level + + if cls._count_holes(occupations) > fermi_level: + return S.Zero + + if sign % 2: + return S.NegativeOne*FockState.__new__(cls, occupations) + else: + return FockState.__new__(cls, occupations) + + def up(self, i): + """ + Performs the action of a creation operator. + + Explanation + =========== + + If below fermi we try to remove a hole, + if above fermi we try to create a particle. + + If general index p we return ``Kronecker(p,i)*self`` + where ``i`` is a new symbol with restriction above or below. + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import FKet + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + + >>> FKet([]).up(a) + FockStateFermionKet((a,)) + + A creator acting on vacuum below fermi vanishes + + >>> FKet([]).up(i) + 0 + + + """ + present = i in self.args[0] + + if self._only_above_fermi(i): + if present: + return S.Zero + else: + return self._add_orbit(i) + elif self._only_below_fermi(i): + if present: + return self._remove_orbit(i) + else: + return S.Zero + else: + if present: + hole = Dummy("i", below_fermi=True) + return KroneckerDelta(i, hole)*self._remove_orbit(i) + else: + particle = Dummy("a", above_fermi=True) + return KroneckerDelta(i, particle)*self._add_orbit(i) + + def down(self, i): + """ + Performs the action of an annihilation operator. + + Explanation + =========== + + If below fermi we try to create a hole, + If above fermi we try to remove a particle. + + If general index p we return ``Kronecker(p,i)*self`` + where ``i`` is a new symbol with restriction above or below. + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.physics.secondquant import FKet + >>> a = Symbol('a', above_fermi=True) + >>> i = Symbol('i', below_fermi=True) + >>> p = Symbol('p') + + An annihilator acting on vacuum above fermi vanishes + + >>> FKet([]).down(a) + 0 + + Also below fermi, it vanishes, unless we specify a fermi level > 0 + + >>> FKet([]).down(i) + 0 + >>> FKet([],4).down(i) + FockStateFermionKet((i,)) + + """ + present = i in self.args[0] + + if self._only_above_fermi(i): + if present: + return self._remove_orbit(i) + else: + return S.Zero + + elif self._only_below_fermi(i): + if present: + return S.Zero + else: + return self._add_orbit(i) + else: + if present: + hole = Dummy("i", below_fermi=True) + return KroneckerDelta(i, hole)*self._add_orbit(i) + else: + particle = Dummy("a", above_fermi=True) + return KroneckerDelta(i, particle)*self._remove_orbit(i) + + @classmethod + def _only_below_fermi(cls, i): + """ + Tests if given orbit is only below fermi surface. + + If nothing can be concluded we return a conservative False. + """ + if i.is_number: + return i <= cls.fermi_level + if i.assumptions0.get('below_fermi'): + return True + return False + + @classmethod + def _only_above_fermi(cls, i): + """ + Tests if given orbit is only above fermi surface. + + If fermi level has not been set we return True. + If nothing can be concluded we return a conservative False. + """ + if i.is_number: + return i > cls.fermi_level + if i.assumptions0.get('above_fermi'): + return True + return not cls.fermi_level + + def _remove_orbit(self, i): + """ + Removes particle/fills hole in orbit i. No input tests performed here. + """ + new_occs = list(self.args[0]) + pos = new_occs.index(i) + del new_occs[pos] + if (pos) % 2: + return S.NegativeOne*self.__class__(new_occs, self.fermi_level) + else: + return self.__class__(new_occs, self.fermi_level) + + def _add_orbit(self, i): + """ + Adds particle/creates hole in orbit i. No input tests performed here. + """ + return self.__class__((i,) + self.args[0], self.fermi_level) + + @classmethod + def _count_holes(cls, occupations): + """ + Returns the number of identified hole states in occupations list. + """ + return len([i for i in occupations if cls._only_below_fermi(i)]) + + def _negate_holes(self, occupations): + """ + Returns the occupations list where states below the fermi level have negative labels. + + For symbolic state labels, no sign is included. + """ + return tuple([-i if self._only_below_fermi(i) and i.is_number else i for i in occupations]) + + def __repr__(self): + if self.fermi_level: + return "FockStateKet(%r, fermi_level=%s)" % (self.args[0], self.fermi_level) + else: + return "FockStateKet(%r)" % (self.args[0],) + + def _labels(self): + return self._negate_holes(self.args[0]) + + +class FockStateKet(FockState): + """ + Representation of a ket. + """ + lbracket = '|' + rbracket = '>' + lbracket_latex = r'\left|' + rbracket_latex = r'\right\rangle' + + +class FockStateBra(FockState): + """ + Representation of a bra. + """ + lbracket = '<' + rbracket = '|' + lbracket_latex = r'\left\langle' + rbracket_latex = r'\right|' + + def __mul__(self, other): + if isinstance(other, FockStateKet): + return InnerProduct(self, other) + else: + return Expr.__mul__(self, other) + + +class FockStateBosonKet(BosonState, FockStateKet): + """ + Many particle Fock state with a sequence of occupation numbers. + + Occupation numbers can be any integer >= 0. + + Examples + ======== + + >>> from sympy.physics.secondquant import BKet + >>> BKet([1, 2]) + FockStateBosonKet((1, 2)) + """ + def _dagger_(self): + return FockStateBosonBra(*self.args) + + +class FockStateBosonBra(BosonState, FockStateBra): + """ + Describes a collection of BosonBra particles. + + Examples + ======== + + >>> from sympy.physics.secondquant import BBra + >>> BBra([1, 2]) + FockStateBosonBra((1, 2)) + """ + def _dagger_(self): + return FockStateBosonKet(*self.args) + + +class FockStateFermionKet(FermionState, FockStateKet): + """ + Many-particle Fock state with a sequence of occupied orbits. + + Explanation + =========== + + Each state can only have one particle, so we choose to store a list of + occupied orbits rather than a tuple with occupation numbers (zeros and ones). + + states below fermi level are holes, and are represented by negative labels + in the occupation list. + + For symbolic state labels, the fermi_level caps the number of allowed hole- + states. + + Examples + ======== + + >>> from sympy.physics.secondquant import FKet + >>> FKet([1, 2]) + FockStateFermionKet((1, 2)) + """ + def _dagger_(self): + return FockStateFermionBra(*self.args) + + +class FockStateFermionBra(FermionState, FockStateBra): + """ + See Also + ======== + + FockStateFermionKet + + Examples + ======== + + >>> from sympy.physics.secondquant import FBra + >>> FBra([1, 2]) + FockStateFermionBra((1, 2)) + """ + def _dagger_(self): + return FockStateFermionKet(*self.args) + +BBra = FockStateBosonBra +BKet = FockStateBosonKet +FBra = FockStateFermionBra +FKet = FockStateFermionKet + + +def _apply_Mul(m): + """ + Take a Mul instance with operators and apply them to states. + + Explanation + =========== + + This method applies all operators with integer state labels + to the actual states. For symbolic state labels, nothing is done. + When inner products of FockStates are encountered (like ), + they are converted to instances of InnerProduct. + + This does not currently work on double inner products like, + . + + If the argument is not a Mul, it is simply returned as is. + """ + if not isinstance(m, Mul): + return m + c_part, nc_part = m.args_cnc() + n_nc = len(nc_part) + if n_nc in (0, 1): + return m + else: + last = nc_part[-1] + next_to_last = nc_part[-2] + if isinstance(last, FockStateKet): + if isinstance(next_to_last, SqOperator): + if next_to_last.is_symbolic: + return m + else: + result = next_to_last.apply_operator(last) + if result == 0: + return S.Zero + else: + return _apply_Mul(Mul(*(c_part + nc_part[:-2] + [result]))) + elif isinstance(next_to_last, Pow): + if isinstance(next_to_last.base, SqOperator) and \ + next_to_last.exp.is_Integer: + if next_to_last.base.is_symbolic: + return m + else: + result = last + for i in range(next_to_last.exp): + result = next_to_last.base.apply_operator(result) + if result == 0: + break + if result == 0: + return S.Zero + else: + return _apply_Mul(Mul(*(c_part + nc_part[:-2] + [result]))) + else: + return m + elif isinstance(next_to_last, FockStateBra): + result = InnerProduct(next_to_last, last) + if result == 0: + return S.Zero + else: + return _apply_Mul(Mul(*(c_part + nc_part[:-2] + [result]))) + else: + return m + else: + return m + + +def apply_operators(e): + """ + Take a SymPy expression with operators and states and apply the operators. + + Examples + ======== + + >>> from sympy.physics.secondquant import apply_operators + >>> from sympy import sympify + >>> apply_operators(sympify(3)+4) + 7 + """ + e = e.expand() + muls = e.atoms(Mul) + subs_list = [(m, _apply_Mul(m)) for m in iter(muls)] + return e.subs(subs_list) + + +class InnerProduct(Basic): + """ + An unevaluated inner product between a bra and ket. + + Explanation + =========== + + Currently this class just reduces things to a product of + Kronecker Deltas. In the future, we could introduce abstract + states like ``|a>`` and ``|b>``, and leave the inner product unevaluated as + ````. + + """ + is_commutative = True + + def __new__(cls, bra, ket): + if not isinstance(bra, FockStateBra): + raise TypeError("must be a bra") + if not isinstance(ket, FockStateKet): + raise TypeError("must be a ket") + return cls.eval(bra, ket) + + @classmethod + def eval(cls, bra, ket): + result = S.One + for i, j in zip(bra.args[0], ket.args[0]): + result *= KroneckerDelta(i, j) + if result == 0: + break + return result + + @property + def bra(self): + """Returns the bra part of the state""" + return self.args[0] + + @property + def ket(self): + """Returns the ket part of the state""" + return self.args[1] + + def __repr__(self): + sbra = repr(self.bra) + sket = repr(self.ket) + return "%s|%s" % (sbra[:-1], sket[1:]) + + def __str__(self): + return self.__repr__() + + +def matrix_rep(op, basis): + """ + Find the representation of an operator in a basis. + + Examples + ======== + + >>> from sympy.physics.secondquant import VarBosonicBasis, B, matrix_rep + >>> b = VarBosonicBasis(5) + >>> o = B(0) + >>> matrix_rep(o, b) + Matrix([ + [0, 1, 0, 0, 0], + [0, 0, sqrt(2), 0, 0], + [0, 0, 0, sqrt(3), 0], + [0, 0, 0, 0, 2], + [0, 0, 0, 0, 0]]) + """ + a = zeros(len(basis)) + for i in range(len(basis)): + for j in range(len(basis)): + a[i, j] = apply_operators(Dagger(basis[i])*op*basis[j]) + return a + + +class BosonicBasis: + """ + Base class for a basis set of bosonic Fock states. + """ + pass + + +class VarBosonicBasis: + """ + A single state, variable particle number basis set. + + Examples + ======== + + >>> from sympy.physics.secondquant import VarBosonicBasis + >>> b = VarBosonicBasis(5) + >>> b + [FockState((0,)), FockState((1,)), FockState((2,)), + FockState((3,)), FockState((4,))] + """ + + def __init__(self, n_max): + self.n_max = n_max + self._build_states() + + def _build_states(self): + self.basis = [] + for i in range(self.n_max): + self.basis.append(FockStateBosonKet([i])) + self.n_basis = len(self.basis) + + def index(self, state): + """ + Returns the index of state in basis. + + Examples + ======== + + >>> from sympy.physics.secondquant import VarBosonicBasis + >>> b = VarBosonicBasis(3) + >>> state = b.state(1) + >>> b + [FockState((0,)), FockState((1,)), FockState((2,))] + >>> state + FockStateBosonKet((1,)) + >>> b.index(state) + 1 + """ + return self.basis.index(state) + + def state(self, i): + """ + The state of a single basis. + + Examples + ======== + + >>> from sympy.physics.secondquant import VarBosonicBasis + >>> b = VarBosonicBasis(5) + >>> b.state(3) + FockStateBosonKet((3,)) + """ + return self.basis[i] + + def __getitem__(self, i): + return self.state(i) + + def __len__(self): + return len(self.basis) + + def __repr__(self): + return repr(self.basis) + + +class FixedBosonicBasis(BosonicBasis): + """ + Fixed particle number basis set. + + Examples + ======== + + >>> from sympy.physics.secondquant import FixedBosonicBasis + >>> b = FixedBosonicBasis(2, 2) + >>> state = b.state(1) + >>> b + [FockState((2, 0)), FockState((1, 1)), FockState((0, 2))] + >>> state + FockStateBosonKet((1, 1)) + >>> b.index(state) + 1 + """ + def __init__(self, n_particles, n_levels): + self.n_particles = n_particles + self.n_levels = n_levels + self._build_particle_locations() + self._build_states() + + def _build_particle_locations(self): + tup = ["i%i" % i for i in range(self.n_particles)] + first_loop = "for i0 in range(%i)" % self.n_levels + other_loops = '' + for cur, prev in zip(tup[1:], tup): + temp = "for %s in range(%s + 1) " % (cur, prev) + other_loops = other_loops + temp + tup_string = "(%s)" % ", ".join(tup) + list_comp = "[%s %s %s]" % (tup_string, first_loop, other_loops) + result = eval(list_comp) + if self.n_particles == 1: + result = [(item,) for item in result] + self.particle_locations = result + + def _build_states(self): + self.basis = [] + for tuple_of_indices in self.particle_locations: + occ_numbers = self.n_levels*[0] + for level in tuple_of_indices: + occ_numbers[level] += 1 + self.basis.append(FockStateBosonKet(occ_numbers)) + self.n_basis = len(self.basis) + + def index(self, state): + """Returns the index of state in basis. + + Examples + ======== + + >>> from sympy.physics.secondquant import FixedBosonicBasis + >>> b = FixedBosonicBasis(2, 3) + >>> b.index(b.state(3)) + 3 + """ + return self.basis.index(state) + + def state(self, i): + """Returns the state that lies at index i of the basis + + Examples + ======== + + >>> from sympy.physics.secondquant import FixedBosonicBasis + >>> b = FixedBosonicBasis(2, 3) + >>> b.state(3) + FockStateBosonKet((1, 0, 1)) + """ + return self.basis[i] + + def __getitem__(self, i): + return self.state(i) + + def __len__(self): + return len(self.basis) + + def __repr__(self): + return repr(self.basis) + + +class Commutator(Function): + """ + The Commutator: [A, B] = A*B - B*A + + The arguments are ordered according to .__cmp__() + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.secondquant import Commutator + >>> A, B = symbols('A,B', commutative=False) + >>> Commutator(B, A) + -Commutator(A, B) + + Evaluate the commutator with .doit() + + >>> comm = Commutator(A,B); comm + Commutator(A, B) + >>> comm.doit() + A*B - B*A + + + For two second quantization operators the commutator is evaluated + immediately: + + >>> from sympy.physics.secondquant import Fd, F + >>> a = symbols('a', above_fermi=True) + >>> i = symbols('i', below_fermi=True) + >>> p,q = symbols('p,q') + + >>> Commutator(Fd(a),Fd(i)) + 2*NO(CreateFermion(a)*CreateFermion(i)) + + But for more complicated expressions, the evaluation is triggered by + a call to .doit() + + >>> comm = Commutator(Fd(p)*Fd(q),F(i)); comm + Commutator(CreateFermion(p)*CreateFermion(q), AnnihilateFermion(i)) + >>> comm.doit(wicks=True) + -KroneckerDelta(i, p)*CreateFermion(q) + + KroneckerDelta(i, q)*CreateFermion(p) + + """ + + is_commutative = False + + @classmethod + def eval(cls, a, b): + """ + The Commutator [A,B] is on canonical form if A < B. + + Examples + ======== + + >>> from sympy.physics.secondquant import Commutator, F, Fd + >>> from sympy.abc import x + >>> c1 = Commutator(F(x), Fd(x)) + >>> c2 = Commutator(Fd(x), F(x)) + >>> Commutator.eval(c1, c2) + 0 + """ + if not (a and b): + return S.Zero + if a == b: + return S.Zero + if a.is_commutative or b.is_commutative: + return S.Zero + + # + # [A+B,C] -> [A,C] + [B,C] + # + a = a.expand() + if isinstance(a, Add): + return Add(*[cls(term, b) for term in a.args]) + b = b.expand() + if isinstance(b, Add): + return Add(*[cls(a, term) for term in b.args]) + + # + # [xA,yB] -> xy*[A,B] + # + ca, nca = a.args_cnc() + cb, ncb = b.args_cnc() + c_part = list(ca) + list(cb) + if c_part: + return Mul(Mul(*c_part), cls(Mul._from_args(nca), Mul._from_args(ncb))) + + # + # single second quantization operators + # + if isinstance(a, BosonicOperator) and isinstance(b, BosonicOperator): + if isinstance(b, CreateBoson) and isinstance(a, AnnihilateBoson): + return KroneckerDelta(a.state, b.state) + if isinstance(a, CreateBoson) and isinstance(b, AnnihilateBoson): + return S.NegativeOne*KroneckerDelta(a.state, b.state) + else: + return S.Zero + if isinstance(a, FermionicOperator) and isinstance(b, FermionicOperator): + return wicks(a*b) - wicks(b*a) + + # + # Canonical ordering of arguments + # + if a.sort_key() > b.sort_key(): + return S.NegativeOne*cls(b, a) + + def doit(self, **hints): + """ + Enables the computation of complex expressions. + + Examples + ======== + + >>> from sympy.physics.secondquant import Commutator, F, Fd + >>> from sympy import symbols + >>> i, j = symbols('i,j', below_fermi=True) + >>> a, b = symbols('a,b', above_fermi=True) + >>> c = Commutator(Fd(a)*F(i),Fd(b)*F(j)) + >>> c.doit(wicks=True) + 0 + """ + a = self.args[0] + b = self.args[1] + + if hints.get("wicks"): + a = a.doit(**hints) + b = b.doit(**hints) + try: + return wicks(a*b) - wicks(b*a) + except ContractionAppliesOnlyToFermions: + pass + except WicksTheoremDoesNotApply: + pass + + return (a*b - b*a).doit(**hints) + + def __repr__(self): + return "Commutator(%s,%s)" % (self.args[0], self.args[1]) + + def __str__(self): + return "[%s,%s]" % (self.args[0], self.args[1]) + + def _latex(self, printer): + return "\\left[%s,%s\\right]" % tuple([ + printer._print(arg) for arg in self.args]) + + +class NO(Expr): + """ + This Object is used to represent normal ordering brackets. + + i.e. {abcd} sometimes written :abcd: + + Explanation + =========== + + Applying the function NO(arg) to an argument means that all operators in + the argument will be assumed to anticommute, and have vanishing + contractions. This allows an immediate reordering to canonical form + upon object creation. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.secondquant import NO, F, Fd + >>> p,q = symbols('p,q') + >>> NO(Fd(p)*F(q)) + NO(CreateFermion(p)*AnnihilateFermion(q)) + >>> NO(F(q)*Fd(p)) + -NO(CreateFermion(p)*AnnihilateFermion(q)) + + + Note + ==== + + If you want to generate a normal ordered equivalent of an expression, you + should use the function wicks(). This class only indicates that all + operators inside the brackets anticommute, and have vanishing contractions. + Nothing more, nothing less. + + """ + is_commutative = False + + def __new__(cls, arg): + """ + Use anticommutation to get canonical form of operators. + + Explanation + =========== + + Employ associativity of normal ordered product: {ab{cd}} = {abcd} + but note that {ab}{cd} /= {abcd}. + + We also employ distributivity: {ab + cd} = {ab} + {cd}. + + Canonical form also implies expand() {ab(c+d)} = {abc} + {abd}. + + """ + + # {ab + cd} = {ab} + {cd} + arg = sympify(arg) + arg = arg.expand() + if arg.is_Add: + return Add(*[ cls(term) for term in arg.args]) + + if arg.is_Mul: + + # take coefficient outside of normal ordering brackets + c_part, seq = arg.args_cnc() + if c_part: + coeff = Mul(*c_part) + if not seq: + return coeff + else: + coeff = S.One + + # {ab{cd}} = {abcd} + newseq = [] + foundit = False + for fac in seq: + if isinstance(fac, NO): + newseq.extend(fac.args) + foundit = True + else: + newseq.append(fac) + if foundit: + return coeff*cls(Mul(*newseq)) + + # We assume that the user don't mix B and F operators + if isinstance(seq[0], BosonicOperator): + raise NotImplementedError + + try: + newseq, sign = _sort_anticommuting_fermions(seq) + except ViolationOfPauliPrinciple: + return S.Zero + + if sign % 2: + return (S.NegativeOne*coeff)*cls(Mul(*newseq)) + elif sign: + return coeff*cls(Mul(*newseq)) + else: + pass # since sign==0, no permutations was necessary + + # if we couldn't do anything with Mul object, we just + # mark it as normal ordered + if coeff != S.One: + return coeff*cls(Mul(*newseq)) + return Expr.__new__(cls, Mul(*newseq)) + + if isinstance(arg, NO): + return arg + + # if object was not Mul or Add, normal ordering does not apply + return arg + + @property + def has_q_creators(self): + """ + Return 0 if the leftmost argument of the first argument is a not a + q_creator, else 1 if it is above fermi or -1 if it is below fermi. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.secondquant import NO, F, Fd + + >>> a = symbols('a', above_fermi=True) + >>> i = symbols('i', below_fermi=True) + >>> NO(Fd(a)*Fd(i)).has_q_creators + 1 + >>> NO(F(i)*F(a)).has_q_creators + -1 + >>> NO(Fd(i)*F(a)).has_q_creators #doctest: +SKIP + 0 + + """ + return self.args[0].args[0].is_q_creator + + @property + def has_q_annihilators(self): + """ + Return 0 if the rightmost argument of the first argument is a not a + q_annihilator, else 1 if it is above fermi or -1 if it is below fermi. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.secondquant import NO, F, Fd + + >>> a = symbols('a', above_fermi=True) + >>> i = symbols('i', below_fermi=True) + >>> NO(Fd(a)*Fd(i)).has_q_annihilators + -1 + >>> NO(F(i)*F(a)).has_q_annihilators + 1 + >>> NO(Fd(a)*F(i)).has_q_annihilators + 0 + + """ + return self.args[0].args[-1].is_q_annihilator + + def doit(self, **hints): + """ + Either removes the brackets or enables complex computations + in its arguments. + + Examples + ======== + + >>> from sympy.physics.secondquant import NO, Fd, F + >>> from textwrap import fill + >>> from sympy import symbols, Dummy + >>> p,q = symbols('p,q', cls=Dummy) + >>> print(fill(str(NO(Fd(p)*F(q)).doit()))) + KroneckerDelta(_a, _p)*KroneckerDelta(_a, + _q)*CreateFermion(_a)*AnnihilateFermion(_a) + KroneckerDelta(_a, + _p)*KroneckerDelta(_i, _q)*CreateFermion(_a)*AnnihilateFermion(_i) - + KroneckerDelta(_a, _q)*KroneckerDelta(_i, + _p)*AnnihilateFermion(_a)*CreateFermion(_i) - KroneckerDelta(_i, + _p)*KroneckerDelta(_i, _q)*AnnihilateFermion(_i)*CreateFermion(_i) + """ + if hints.get("remove_brackets", True): + return self._remove_brackets() + else: + return self.__new__(type(self), self.args[0].doit(**hints)) + + def _remove_brackets(self): + """ + Returns the sorted string without normal order brackets. + + The returned string have the property that no nonzero + contractions exist. + """ + + # check if any creator is also an annihilator + subslist = [] + for i in self.iter_q_creators(): + if self[i].is_q_annihilator: + assume = self[i].state.assumptions0 + + # only operators with a dummy index can be split in two terms + if isinstance(self[i].state, Dummy): + + # create indices with fermi restriction + assume.pop("above_fermi", None) + assume["below_fermi"] = True + below = Dummy('i', **assume) + assume.pop("below_fermi", None) + assume["above_fermi"] = True + above = Dummy('a', **assume) + + cls = type(self[i]) + split = ( + self[i].__new__(cls, below) + * KroneckerDelta(below, self[i].state) + + self[i].__new__(cls, above) + * KroneckerDelta(above, self[i].state) + ) + subslist.append((self[i], split)) + else: + raise SubstitutionOfAmbigousOperatorFailed(self[i]) + if subslist: + result = NO(self.subs(subslist)) + if isinstance(result, Add): + return Add(*[term.doit() for term in result.args]) + else: + return self.args[0] + + def _expand_operators(self): + """ + Returns a sum of NO objects that contain no ambiguous q-operators. + + Explanation + =========== + + If an index q has range both above and below fermi, the operator F(q) + is ambiguous in the sense that it can be both a q-creator and a q-annihilator. + If q is dummy, it is assumed to be a summation variable and this method + rewrites it into a sum of NO terms with unambiguous operators: + + {Fd(p)*F(q)} = {Fd(a)*F(b)} + {Fd(a)*F(i)} + {Fd(j)*F(b)} -{F(i)*Fd(j)} + + where a,b are above and i,j are below fermi level. + """ + return NO(self._remove_brackets) + + def __getitem__(self, i): + if isinstance(i, slice): + indices = i.indices(len(self)) + return [self.args[0].args[i] for i in range(*indices)] + else: + return self.args[0].args[i] + + def __len__(self): + return len(self.args[0].args) + + def iter_q_annihilators(self): + """ + Iterates over the annihilation operators. + + Examples + ======== + + >>> from sympy import symbols + >>> i, j = symbols('i j', below_fermi=True) + >>> a, b = symbols('a b', above_fermi=True) + >>> from sympy.physics.secondquant import NO, F, Fd + >>> no = NO(Fd(a)*F(i)*F(b)*Fd(j)) + + >>> no.iter_q_creators() + + >>> list(no.iter_q_creators()) + [0, 1] + >>> list(no.iter_q_annihilators()) + [3, 2] + + """ + ops = self.args[0].args + iter = range(len(ops) - 1, -1, -1) + for i in iter: + if ops[i].is_q_annihilator: + yield i + else: + break + + def iter_q_creators(self): + """ + Iterates over the creation operators. + + Examples + ======== + + >>> from sympy import symbols + >>> i, j = symbols('i j', below_fermi=True) + >>> a, b = symbols('a b', above_fermi=True) + >>> from sympy.physics.secondquant import NO, F, Fd + >>> no = NO(Fd(a)*F(i)*F(b)*Fd(j)) + + >>> no.iter_q_creators() + + >>> list(no.iter_q_creators()) + [0, 1] + >>> list(no.iter_q_annihilators()) + [3, 2] + + """ + + ops = self.args[0].args + iter = range(0, len(ops)) + for i in iter: + if ops[i].is_q_creator: + yield i + else: + break + + def get_subNO(self, i): + """ + Returns a NO() without FermionicOperator at index i. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.secondquant import F, NO + >>> p, q, r = symbols('p,q,r') + + >>> NO(F(p)*F(q)*F(r)).get_subNO(1) + NO(AnnihilateFermion(p)*AnnihilateFermion(r)) + + """ + arg0 = self.args[0] # it's a Mul by definition of how it's created + mul = arg0._new_rawargs(*(arg0.args[:i] + arg0.args[i + 1:])) + return NO(mul) + + def _latex(self, printer): + return "\\left\\{%s\\right\\}" % printer._print(self.args[0]) + + def __repr__(self): + return "NO(%s)" % self.args[0] + + def __str__(self): + return ":%s:" % self.args[0] + + +def contraction(a, b): + """ + Calculates contraction of Fermionic operators a and b. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.physics.secondquant import F, Fd, contraction + >>> p, q = symbols('p,q') + >>> a, b = symbols('a,b', above_fermi=True) + >>> i, j = symbols('i,j', below_fermi=True) + + A contraction is non-zero only if a quasi-creator is to the right of a + quasi-annihilator: + + >>> contraction(F(a),Fd(b)) + KroneckerDelta(a, b) + >>> contraction(Fd(i),F(j)) + KroneckerDelta(i, j) + + For general indices a non-zero result restricts the indices to below/above + the fermi surface: + + >>> contraction(Fd(p),F(q)) + KroneckerDelta(_i, q)*KroneckerDelta(p, q) + >>> contraction(F(p),Fd(q)) + KroneckerDelta(_a, q)*KroneckerDelta(p, q) + + Two creators or two annihilators always vanishes: + + >>> contraction(F(p),F(q)) + 0 + >>> contraction(Fd(p),Fd(q)) + 0 + + """ + if isinstance(b, FermionicOperator) and isinstance(a, FermionicOperator): + if isinstance(a, AnnihilateFermion) and isinstance(b, CreateFermion): + if b.state.assumptions0.get("below_fermi"): + return S.Zero + if a.state.assumptions0.get("below_fermi"): + return S.Zero + if b.state.assumptions0.get("above_fermi"): + return KroneckerDelta(a.state, b.state) + if a.state.assumptions0.get("above_fermi"): + return KroneckerDelta(a.state, b.state) + + return (KroneckerDelta(a.state, b.state)* + KroneckerDelta(b.state, Dummy('a', above_fermi=True))) + if isinstance(b, AnnihilateFermion) and isinstance(a, CreateFermion): + if b.state.assumptions0.get("above_fermi"): + return S.Zero + if a.state.assumptions0.get("above_fermi"): + return S.Zero + if b.state.assumptions0.get("below_fermi"): + return KroneckerDelta(a.state, b.state) + if a.state.assumptions0.get("below_fermi"): + return KroneckerDelta(a.state, b.state) + + return (KroneckerDelta(a.state, b.state)* + KroneckerDelta(b.state, Dummy('i', below_fermi=True))) + + # vanish if 2xAnnihilator or 2xCreator + return S.Zero + + else: + #not fermion operators + t = ( isinstance(i, FermionicOperator) for i in (a, b) ) + raise ContractionAppliesOnlyToFermions(*t) + + +def _sqkey_operator(sq_operator): + """Generates key for canonical sorting of SQ operators.""" + return sq_operator._sortkey() + +def _sqkey_index(index): + """Key for sorting of indices. + + particle < hole < general + + FIXME: This is a bottle-neck, can we do it faster? + """ + h = hash(index) + label = str(index) + if isinstance(index, Dummy): + if index.assumptions0.get('above_fermi'): + return (20, label, h) + elif index.assumptions0.get('below_fermi'): + return (21, label, h) + else: + return (22, label, h) + + if index.assumptions0.get('above_fermi'): + return (10, label, h) + elif index.assumptions0.get('below_fermi'): + return (11, label, h) + else: + return (12, label, h) + + + +def _sort_anticommuting_fermions(string1, key=_sqkey_operator): + """Sort fermionic operators to canonical order, assuming all pairs anticommute. + + Explanation + =========== + + Uses a bidirectional bubble sort. Items in string1 are not referenced + so in principle they may be any comparable objects. The sorting depends on the + operators '>' and '=='. + + If the Pauli principle is violated, an exception is raised. + + Returns + ======= + + tuple (sorted_str, sign) + + sorted_str: list containing the sorted operators + sign: int telling how many times the sign should be changed + (if sign==0 the string was already sorted) + """ + + verified = False + sign = 0 + rng = list(range(len(string1) - 1)) + rev = list(range(len(string1) - 3, -1, -1)) + + keys = list(map(key, string1)) + key_val = dict(list(zip(keys, string1))) + + while not verified: + verified = True + for i in rng: + left = keys[i] + right = keys[i + 1] + if left == right: + raise ViolationOfPauliPrinciple([left, right]) + if left > right: + verified = False + keys[i:i + 2] = [right, left] + sign = sign + 1 + if verified: + break + for i in rev: + left = keys[i] + right = keys[i + 1] + if left == right: + raise ViolationOfPauliPrinciple([left, right]) + if left > right: + verified = False + keys[i:i + 2] = [right, left] + sign = sign + 1 + string1 = [ key_val[k] for k in keys ] + return (string1, sign) + + +def evaluate_deltas(e): + """ + We evaluate KroneckerDelta symbols in the expression assuming Einstein summation. + + Explanation + =========== + + If one index is repeated it is summed over and in effect substituted with + the other one. If both indices are repeated we substitute according to what + is the preferred index. this is determined by + KroneckerDelta.preferred_index and KroneckerDelta.killable_index. + + In case there are no possible substitutions or if a substitution would + imply a loss of information, nothing is done. + + In case an index appears in more than one KroneckerDelta, the resulting + substitution depends on the order of the factors. Since the ordering is platform + dependent, the literal expression resulting from this function may be hard to + predict. + + Examples + ======== + + We assume the following: + + >>> from sympy import symbols, Function, Dummy, KroneckerDelta + >>> from sympy.physics.secondquant import evaluate_deltas + >>> i,j = symbols('i j', below_fermi=True, cls=Dummy) + >>> a,b = symbols('a b', above_fermi=True, cls=Dummy) + >>> p,q = symbols('p q', cls=Dummy) + >>> f = Function('f') + >>> t = Function('t') + + The order of preference for these indices according to KroneckerDelta is + (a, b, i, j, p, q). + + Trivial cases: + + >>> evaluate_deltas(KroneckerDelta(i,j)*f(i)) # d_ij f(i) -> f(j) + f(_j) + >>> evaluate_deltas(KroneckerDelta(i,j)*f(j)) # d_ij f(j) -> f(i) + f(_i) + >>> evaluate_deltas(KroneckerDelta(i,p)*f(p)) # d_ip f(p) -> f(i) + f(_i) + >>> evaluate_deltas(KroneckerDelta(q,p)*f(p)) # d_qp f(p) -> f(q) + f(_q) + >>> evaluate_deltas(KroneckerDelta(q,p)*f(q)) # d_qp f(q) -> f(p) + f(_p) + + More interesting cases: + + >>> evaluate_deltas(KroneckerDelta(i,p)*t(a,i)*f(p,q)) + f(_i, _q)*t(_a, _i) + >>> evaluate_deltas(KroneckerDelta(a,p)*t(a,i)*f(p,q)) + f(_a, _q)*t(_a, _i) + >>> evaluate_deltas(KroneckerDelta(p,q)*f(p,q)) + f(_p, _p) + + Finally, here are some cases where nothing is done, because that would + imply a loss of information: + + >>> evaluate_deltas(KroneckerDelta(i,p)*f(q)) + f(_q)*KroneckerDelta(_i, _p) + >>> evaluate_deltas(KroneckerDelta(i,p)*f(i)) + f(_i)*KroneckerDelta(_i, _p) + """ + + # We treat Deltas only in mul objects + # for general function objects we don't evaluate KroneckerDeltas in arguments, + # but here we hard code exceptions to this rule + accepted_functions = ( + Add, + ) + if isinstance(e, accepted_functions): + return e.func(*[evaluate_deltas(arg) for arg in e.args]) + + elif isinstance(e, Mul): + # find all occurrences of delta function and count each index present in + # expression. + deltas = [] + indices = {} + for i in e.args: + for s in i.free_symbols: + if s in indices: + indices[s] += 1 + else: + indices[s] = 0 # geek counting simplifies logic below + if isinstance(i, KroneckerDelta): + deltas.append(i) + + for d in deltas: + # If we do something, and there are more deltas, we should recurse + # to treat the resulting expression properly + if d.killable_index.is_Symbol and indices[d.killable_index]: + e = e.subs(d.killable_index, d.preferred_index) + if len(deltas) > 1: + return evaluate_deltas(e) + elif (d.preferred_index.is_Symbol and indices[d.preferred_index] + and d.indices_contain_equal_information): + e = e.subs(d.preferred_index, d.killable_index) + if len(deltas) > 1: + return evaluate_deltas(e) + else: + pass + + return e + # nothing to do, maybe we hit a Symbol or a number + else: + return e + + +def substitute_dummies(expr, new_indices=False, pretty_indices={}): + """ + Collect terms by substitution of dummy variables. + + Explanation + =========== + + This routine allows simplification of Add expressions containing terms + which differ only due to dummy variables. + + The idea is to substitute all dummy variables consistently depending on + the structure of the term. For each term, we obtain a sequence of all + dummy variables, where the order is determined by the index range, what + factors the index belongs to and its position in each factor. See + _get_ordered_dummies() for more information about the sorting of dummies. + The index sequence is then substituted consistently in each term. + + Examples + ======== + + >>> from sympy import symbols, Function, Dummy + >>> from sympy.physics.secondquant import substitute_dummies + >>> a,b,c,d = symbols('a b c d', above_fermi=True, cls=Dummy) + >>> i,j = symbols('i j', below_fermi=True, cls=Dummy) + >>> f = Function('f') + + >>> expr = f(a,b) + f(c,d); expr + f(_a, _b) + f(_c, _d) + + Since a, b, c and d are equivalent summation indices, the expression can be + simplified to a single term (for which the dummy indices are still summed over) + + >>> substitute_dummies(expr) + 2*f(_a, _b) + + + Controlling output: + + By default the dummy symbols that are already present in the expression + will be reused in a different permutation. However, if new_indices=True, + new dummies will be generated and inserted. The keyword 'pretty_indices' + can be used to control this generation of new symbols. + + By default the new dummies will be generated on the form i_1, i_2, a_1, + etc. If you supply a dictionary with key:value pairs in the form: + + { index_group: string_of_letters } + + The letters will be used as labels for the new dummy symbols. The + index_groups must be one of 'above', 'below' or 'general'. + + >>> expr = f(a,b,i,j) + >>> my_dummies = { 'above':'st', 'below':'uv' } + >>> substitute_dummies(expr, new_indices=True, pretty_indices=my_dummies) + f(_s, _t, _u, _v) + + If we run out of letters, or if there is no keyword for some index_group + the default dummy generator will be used as a fallback: + + >>> p,q = symbols('p q', cls=Dummy) # general indices + >>> expr = f(p,q) + >>> substitute_dummies(expr, new_indices=True, pretty_indices=my_dummies) + f(_p_0, _p_1) + + """ + + # setup the replacing dummies + if new_indices: + letters_above = pretty_indices.get('above', "") + letters_below = pretty_indices.get('below', "") + letters_general = pretty_indices.get('general', "") + len_above = len(letters_above) + len_below = len(letters_below) + len_general = len(letters_general) + + def _i(number): + try: + return letters_below[number] + except IndexError: + return 'i_' + str(number - len_below) + + def _a(number): + try: + return letters_above[number] + except IndexError: + return 'a_' + str(number - len_above) + + def _p(number): + try: + return letters_general[number] + except IndexError: + return 'p_' + str(number - len_general) + + aboves = [] + belows = [] + generals = [] + + dummies = expr.atoms(Dummy) + if not new_indices: + dummies = sorted(dummies, key=default_sort_key) + + # generate lists with the dummies we will insert + a = i = p = 0 + for d in dummies: + assum = d.assumptions0 + + if assum.get("above_fermi"): + if new_indices: + sym = _a(a) + a += 1 + l1 = aboves + elif assum.get("below_fermi"): + if new_indices: + sym = _i(i) + i += 1 + l1 = belows + else: + if new_indices: + sym = _p(p) + p += 1 + l1 = generals + + if new_indices: + l1.append(Dummy(sym, **assum)) + else: + l1.append(d) + + expr = expr.expand() + terms = Add.make_args(expr) + new_terms = [] + for term in terms: + i = iter(belows) + a = iter(aboves) + p = iter(generals) + ordered = _get_ordered_dummies(term) + subsdict = {} + for d in ordered: + if d.assumptions0.get('below_fermi'): + subsdict[d] = next(i) + elif d.assumptions0.get('above_fermi'): + subsdict[d] = next(a) + else: + subsdict[d] = next(p) + subslist = [] + final_subs = [] + for k, v in subsdict.items(): + if k == v: + continue + if v in subsdict: + # We check if the sequence of substitutions end quickly. In + # that case, we can avoid temporary symbols if we ensure the + # correct substitution order. + if subsdict[v] in subsdict: + # (x, y) -> (y, x), we need a temporary variable + x = Dummy('x') + subslist.append((k, x)) + final_subs.append((x, v)) + else: + # (x, y) -> (y, a), x->y must be done last + # but before temporary variables are resolved + final_subs.insert(0, (k, v)) + else: + subslist.append((k, v)) + subslist.extend(final_subs) + new_terms.append(term.subs(subslist)) + return Add(*new_terms) + + +class KeyPrinter(StrPrinter): + """Printer for which only equal objects are equal in print""" + def _print_Dummy(self, expr): + return "(%s_%i)" % (expr.name, expr.dummy_index) + + +def __kprint(expr): + p = KeyPrinter() + return p.doprint(expr) + + +def _get_ordered_dummies(mul, verbose=False): + """Returns all dummies in the mul sorted in canonical order. + + Explanation + =========== + + The purpose of the canonical ordering is that dummies can be substituted + consistently across terms with the result that equivalent terms can be + simplified. + + It is not possible to determine if two terms are equivalent based solely on + the dummy order. However, a consistent substitution guided by the ordered + dummies should lead to trivially (non-)equivalent terms, thereby revealing + the equivalence. This also means that if two terms have identical sequences of + dummies, the (non-)equivalence should already be apparent. + + Strategy + -------- + + The canonical order is given by an arbitrary sorting rule. A sort key + is determined for each dummy as a tuple that depends on all factors where + the index is present. The dummies are thereby sorted according to the + contraction structure of the term, instead of sorting based solely on the + dummy symbol itself. + + After all dummies in the term has been assigned a key, we check for identical + keys, i.e. unorderable dummies. If any are found, we call a specialized + method, _determine_ambiguous(), that will determine a unique order based + on recursive calls to _get_ordered_dummies(). + + Key description + --------------- + + A high level description of the sort key: + + 1. Range of the dummy index + 2. Relation to external (non-dummy) indices + 3. Position of the index in the first factor + 4. Position of the index in the second factor + + The sort key is a tuple with the following components: + + 1. A single character indicating the range of the dummy (above, below + or general.) + 2. A list of strings with fully masked string representations of all + factors where the dummy is present. By masked, we mean that dummies + are represented by a symbol to indicate either below fermi, above or + general. No other information is displayed about the dummies at + this point. The list is sorted stringwise. + 3. An integer number indicating the position of the index, in the first + factor as sorted in 2. + 4. An integer number indicating the position of the index, in the second + factor as sorted in 2. + + If a factor is either of type AntiSymmetricTensor or SqOperator, the index + position in items 3 and 4 is indicated as 'upper' or 'lower' only. + (Creation operators are considered upper and annihilation operators lower.) + + If the masked factors are identical, the two factors cannot be ordered + unambiguously in item 2. In this case, items 3, 4 are left out. If several + indices are contracted between the unorderable factors, it will be handled by + _determine_ambiguous() + + + """ + # setup dicts to avoid repeated calculations in key() + args = Mul.make_args(mul) + fac_dum = { fac: fac.atoms(Dummy) for fac in args } + fac_repr = { fac: __kprint(fac) for fac in args } + all_dums = set().union(*fac_dum.values()) + mask = {} + for d in all_dums: + if d.assumptions0.get('below_fermi'): + mask[d] = '0' + elif d.assumptions0.get('above_fermi'): + mask[d] = '1' + else: + mask[d] = '2' + dum_repr = {d: __kprint(d) for d in all_dums} + + def _key(d): + dumstruct = [ fac for fac in fac_dum if d in fac_dum[fac] ] + other_dums = set().union(*[fac_dum[fac] for fac in dumstruct]) + fac = dumstruct[-1] + if other_dums is fac_dum[fac]: + other_dums = fac_dum[fac].copy() + other_dums.remove(d) + masked_facs = [ fac_repr[fac] for fac in dumstruct ] + for d2 in other_dums: + masked_facs = [ fac.replace(dum_repr[d2], mask[d2]) + for fac in masked_facs ] + all_masked = [ fac.replace(dum_repr[d], mask[d]) + for fac in masked_facs ] + masked_facs = dict(list(zip(dumstruct, masked_facs))) + + # dummies for which the ordering cannot be determined + if has_dups(all_masked): + all_masked.sort() + return mask[d], tuple(all_masked) # positions are ambiguous + + # sort factors according to fully masked strings + keydict = dict(list(zip(dumstruct, all_masked))) + dumstruct.sort(key=lambda x: keydict[x]) + all_masked.sort() + + pos_val = [] + for fac in dumstruct: + if isinstance(fac, AntiSymmetricTensor): + if d in fac.upper: + pos_val.append('u') + if d in fac.lower: + pos_val.append('l') + elif isinstance(fac, Creator): + pos_val.append('u') + elif isinstance(fac, Annihilator): + pos_val.append('l') + elif isinstance(fac, NO): + ops = [ op for op in fac if op.has(d) ] + for op in ops: + if isinstance(op, Creator): + pos_val.append('u') + else: + pos_val.append('l') + else: + # fallback to position in string representation + facpos = -1 + while 1: + facpos = masked_facs[fac].find(dum_repr[d], facpos + 1) + if facpos == -1: + break + pos_val.append(facpos) + return (mask[d], tuple(all_masked), pos_val[0], pos_val[-1]) + dumkey = dict(list(zip(all_dums, list(map(_key, all_dums))))) + result = sorted(all_dums, key=lambda x: dumkey[x]) + if has_dups(iter(dumkey.values())): + # We have ambiguities + unordered = defaultdict(set) + for d, k in dumkey.items(): + unordered[k].add(d) + for k in [ k for k in unordered if len(unordered[k]) < 2 ]: + del unordered[k] + + unordered = [ unordered[k] for k in sorted(unordered) ] + result = _determine_ambiguous(mul, result, unordered) + return result + + +def _determine_ambiguous(term, ordered, ambiguous_groups): + # We encountered a term for which the dummy substitution is ambiguous. + # This happens for terms with 2 or more contractions between factors that + # cannot be uniquely ordered independent of summation indices. For + # example: + # + # Sum(p, q) v^{p, .}_{q, .}v^{q, .}_{p, .} + # + # Assuming that the indices represented by . are dummies with the + # same range, the factors cannot be ordered, and there is no + # way to determine a consistent ordering of p and q. + # + # The strategy employed here, is to relabel all unambiguous dummies with + # non-dummy symbols and call _get_ordered_dummies again. This procedure is + # applied to the entire term so there is a possibility that + # _determine_ambiguous() is called again from a deeper recursion level. + + # break recursion if there are no ordered dummies + all_ambiguous = set() + for dummies in ambiguous_groups: + all_ambiguous |= dummies + all_ordered = set(ordered) - all_ambiguous + if not all_ordered: + # FIXME: If we arrive here, there are no ordered dummies. A method to + # handle this needs to be implemented. In order to return something + # useful nevertheless, we choose arbitrarily the first dummy and + # determine the rest from this one. This method is dependent on the + # actual dummy labels which violates an assumption for the + # canonicalization procedure. A better implementation is needed. + group = [ d for d in ordered if d in ambiguous_groups[0] ] + d = group[0] + all_ordered.add(d) + ambiguous_groups[0].remove(d) + + stored_counter = _symbol_factory._counter + subslist = [] + for d in [ d for d in ordered if d in all_ordered ]: + nondum = _symbol_factory._next() + subslist.append((d, nondum)) + newterm = term.subs(subslist) + neworder = _get_ordered_dummies(newterm) + _symbol_factory._set_counter(stored_counter) + + # update ordered list with new information + for group in ambiguous_groups: + ordered_group = [ d for d in neworder if d in group ] + ordered_group.reverse() + result = [] + for d in ordered: + if d in group: + result.append(ordered_group.pop()) + else: + result.append(d) + ordered = result + return ordered + + +class _SymbolFactory: + def __init__(self, label): + self._counterVar = 0 + self._label = label + + def _set_counter(self, value): + """ + Sets counter to value. + """ + self._counterVar = value + + @property + def _counter(self): + """ + What counter is currently at. + """ + return self._counterVar + + def _next(self): + """ + Generates the next symbols and increments counter by 1. + """ + s = Symbol("%s%i" % (self._label, self._counterVar)) + self._counterVar += 1 + return s +_symbol_factory = _SymbolFactory('_]"]_') # most certainly a unique label + + +@cacheit +def _get_contractions(string1, keep_only_fully_contracted=False): + """ + Returns Add-object with contracted terms. + + Uses recursion to find all contractions. -- Internal helper function -- + + Will find nonzero contractions in string1 between indices given in + leftrange and rightrange. + + """ + + # Should we store current level of contraction? + if keep_only_fully_contracted and string1: + result = [] + else: + result = [NO(Mul(*string1))] + + for i in range(len(string1) - 1): + for j in range(i + 1, len(string1)): + + c = contraction(string1[i], string1[j]) + + if c: + sign = (j - i + 1) % 2 + if sign: + coeff = S.NegativeOne*c + else: + coeff = c + + # + # Call next level of recursion + # ============================ + # + # We now need to find more contractions among operators + # + # oplist = string1[:i]+ string1[i+1:j] + string1[j+1:] + # + # To prevent overcounting, we don't allow contractions + # we have already encountered. i.e. contractions between + # string1[:i] <---> string1[i+1:j] + # and string1[:i] <---> string1[j+1:]. + # + # This leaves the case: + oplist = string1[i + 1:j] + string1[j + 1:] + + if oplist: + + result.append(coeff*NO( + Mul(*string1[:i])*_get_contractions( oplist, + keep_only_fully_contracted=keep_only_fully_contracted))) + + else: + result.append(coeff*NO( Mul(*string1[:i]))) + + if keep_only_fully_contracted: + break # next iteration over i leaves leftmost operator string1[0] uncontracted + + return Add(*result) + + +def wicks(e, **kw_args): + """ + Returns the normal ordered equivalent of an expression using Wicks Theorem. + + Examples + ======== + + >>> from sympy import symbols, Dummy + >>> from sympy.physics.secondquant import wicks, F, Fd + >>> p, q, r = symbols('p,q,r') + >>> wicks(Fd(p)*F(q)) + KroneckerDelta(_i, q)*KroneckerDelta(p, q) + NO(CreateFermion(p)*AnnihilateFermion(q)) + + By default, the expression is expanded: + + >>> wicks(F(p)*(F(q)+F(r))) + NO(AnnihilateFermion(p)*AnnihilateFermion(q)) + NO(AnnihilateFermion(p)*AnnihilateFermion(r)) + + With the keyword 'keep_only_fully_contracted=True', only fully contracted + terms are returned. + + By request, the result can be simplified in the following order: + -- KroneckerDelta functions are evaluated + -- Dummy variables are substituted consistently across terms + + >>> p, q, r = symbols('p q r', cls=Dummy) + >>> wicks(Fd(p)*(F(q)+F(r)), keep_only_fully_contracted=True) + KroneckerDelta(_i, _q)*KroneckerDelta(_p, _q) + KroneckerDelta(_i, _r)*KroneckerDelta(_p, _r) + + """ + + if not e: + return S.Zero + + opts = { + 'simplify_kronecker_deltas': False, + 'expand': True, + 'simplify_dummies': False, + 'keep_only_fully_contracted': False + } + opts.update(kw_args) + + # check if we are already normally ordered + if isinstance(e, NO): + if opts['keep_only_fully_contracted']: + return S.Zero + else: + return e + elif isinstance(e, FermionicOperator): + if opts['keep_only_fully_contracted']: + return S.Zero + else: + return e + + # break up any NO-objects, and evaluate commutators + e = e.doit(wicks=True) + + # make sure we have only one term to consider + e = e.expand() + if isinstance(e, Add): + if opts['simplify_dummies']: + return substitute_dummies(Add(*[ wicks(term, **kw_args) for term in e.args])) + else: + return Add(*[ wicks(term, **kw_args) for term in e.args]) + + # For Mul-objects we can actually do something + if isinstance(e, Mul): + + # we don't want to mess around with commuting part of Mul + # so we factorize it out before starting recursion + c_part = [] + string1 = [] + for factor in e.args: + if factor.is_commutative: + c_part.append(factor) + else: + string1.append(factor) + n = len(string1) + + # catch trivial cases + if n == 0: + result = e + elif n == 1: + if opts['keep_only_fully_contracted']: + return S.Zero + else: + result = e + + else: # non-trivial + + if isinstance(string1[0], BosonicOperator): + raise NotImplementedError + + string1 = tuple(string1) + + # recursion over higher order contractions + result = _get_contractions(string1, + keep_only_fully_contracted=opts['keep_only_fully_contracted'] ) + result = Mul(*c_part)*result + + if opts['expand']: + result = result.expand() + if opts['simplify_kronecker_deltas']: + result = evaluate_deltas(result) + + return result + + # there was nothing to do + return e + + +class PermutationOperator(Expr): + """ + Represents the index permutation operator P(ij). + + P(ij)*f(i)*g(j) = f(i)*g(j) - f(j)*g(i) + """ + is_commutative = True + + def __new__(cls, i, j): + i, j = sorted(map(sympify, (i, j)), key=default_sort_key) + obj = Basic.__new__(cls, i, j) + return obj + + def get_permuted(self, expr): + """ + Returns -expr with permuted indices. + + Explanation + =========== + + >>> from sympy import symbols, Function + >>> from sympy.physics.secondquant import PermutationOperator + >>> p,q = symbols('p,q') + >>> f = Function('f') + >>> PermutationOperator(p,q).get_permuted(f(p,q)) + -f(q, p) + + """ + i = self.args[0] + j = self.args[1] + if expr.has(i) and expr.has(j): + tmp = Dummy() + expr = expr.subs(i, tmp) + expr = expr.subs(j, i) + expr = expr.subs(tmp, j) + return S.NegativeOne*expr + else: + return expr + + def _latex(self, printer): + return "P(%s%s)" % tuple(printer._print(i) for i in self.args) + + +def simplify_index_permutations(expr, permutation_operators): + """ + Performs simplification by introducing PermutationOperators where appropriate. + + Explanation + =========== + + Schematically: + [abij] - [abji] - [baij] + [baji] -> P(ab)*P(ij)*[abij] + + permutation_operators is a list of PermutationOperators to consider. + + If permutation_operators=[P(ab),P(ij)] we will try to introduce the + permutation operators P(ij) and P(ab) in the expression. If there are other + possible simplifications, we ignore them. + + >>> from sympy import symbols, Function + >>> from sympy.physics.secondquant import simplify_index_permutations + >>> from sympy.physics.secondquant import PermutationOperator + >>> p,q,r,s = symbols('p,q,r,s') + >>> f = Function('f') + >>> g = Function('g') + + >>> expr = f(p)*g(q) - f(q)*g(p); expr + f(p)*g(q) - f(q)*g(p) + >>> simplify_index_permutations(expr,[PermutationOperator(p,q)]) + f(p)*g(q)*PermutationOperator(p, q) + + >>> PermutList = [PermutationOperator(p,q),PermutationOperator(r,s)] + >>> expr = f(p,r)*g(q,s) - f(q,r)*g(p,s) + f(q,s)*g(p,r) - f(p,s)*g(q,r) + >>> simplify_index_permutations(expr,PermutList) + f(p, r)*g(q, s)*PermutationOperator(p, q)*PermutationOperator(r, s) + + """ + + def _get_indices(expr, ind): + """ + Collects indices recursively in predictable order. + """ + result = [] + for arg in expr.args: + if arg in ind: + result.append(arg) + else: + if arg.args: + result.extend(_get_indices(arg, ind)) + return result + + def _choose_one_to_keep(a, b, ind): + # we keep the one where indices in ind are in order ind[0] < ind[1] + return min(a, b, key=lambda x: default_sort_key(_get_indices(x, ind))) + + expr = expr.expand() + if isinstance(expr, Add): + terms = set(expr.args) + + for P in permutation_operators: + new_terms = set() + on_hold = set() + while terms: + term = terms.pop() + permuted = P.get_permuted(term) + if permuted in terms | on_hold: + try: + terms.remove(permuted) + except KeyError: + on_hold.remove(permuted) + keep = _choose_one_to_keep(term, permuted, P.args) + new_terms.add(P*keep) + else: + + # Some terms must get a second chance because the permuted + # term may already have canonical dummy ordering. Then + # substitute_dummies() does nothing. However, the other + # term, if it exists, will be able to match with us. + permuted1 = permuted + permuted = substitute_dummies(permuted) + if permuted1 == permuted: + on_hold.add(term) + elif permuted in terms | on_hold: + try: + terms.remove(permuted) + except KeyError: + on_hold.remove(permuted) + keep = _choose_one_to_keep(term, permuted, P.args) + new_terms.add(P*keep) + else: + new_terms.add(term) + terms = new_terms | on_hold + return Add(*terms) + return expr diff --git a/venv/lib/python3.11/site-packages/sympy/physics/sho.py b/venv/lib/python3.11/site-packages/sympy/physics/sho.py new file mode 100644 index 0000000000000000000000000000000000000000..c55b31b3fa9fca4fa33a9f8e91c90c2174fe81a5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/sho.py @@ -0,0 +1,95 @@ +from sympy.core import S, pi, Rational +from sympy.functions import assoc_laguerre, sqrt, exp, factorial, factorial2 + + +def R_nl(n, l, nu, r): + """ + Returns the radial wavefunction R_{nl} for a 3d isotropic harmonic + oscillator. + + Parameters + ========== + + n : + The "nodal" quantum number. Corresponds to the number of nodes in + the wavefunction. ``n >= 0`` + l : + The quantum number for orbital angular momentum. + nu : + mass-scaled frequency: nu = m*omega/(2*hbar) where `m` is the mass + and `omega` the frequency of the oscillator. + (in atomic units ``nu == omega/2``) + r : + Radial coordinate. + + Examples + ======== + + >>> from sympy.physics.sho import R_nl + >>> from sympy.abc import r, nu, l + >>> R_nl(0, 0, 1, r) + 2*2**(3/4)*exp(-r**2)/pi**(1/4) + >>> R_nl(1, 0, 1, r) + 4*2**(1/4)*sqrt(3)*(3/2 - 2*r**2)*exp(-r**2)/(3*pi**(1/4)) + + l, nu and r may be symbolic: + + >>> R_nl(0, 0, nu, r) + 2*2**(3/4)*sqrt(nu**(3/2))*exp(-nu*r**2)/pi**(1/4) + >>> R_nl(0, l, 1, r) + r**l*sqrt(2**(l + 3/2)*2**(l + 2)/factorial2(2*l + 1))*exp(-r**2)/pi**(1/4) + + The normalization of the radial wavefunction is: + + >>> from sympy import Integral, oo + >>> Integral(R_nl(0, 0, 1, r)**2*r**2, (r, 0, oo)).n() + 1.00000000000000 + >>> Integral(R_nl(1, 0, 1, r)**2*r**2, (r, 0, oo)).n() + 1.00000000000000 + >>> Integral(R_nl(1, 1, 1, r)**2*r**2, (r, 0, oo)).n() + 1.00000000000000 + + """ + n, l, nu, r = map(S, [n, l, nu, r]) + + # formula uses n >= 1 (instead of nodal n >= 0) + n = n + 1 + C = sqrt( + ((2*nu)**(l + Rational(3, 2))*2**(n + l + 1)*factorial(n - 1))/ + (sqrt(pi)*(factorial2(2*n + 2*l - 1))) + ) + return C*r**(l)*exp(-nu*r**2)*assoc_laguerre(n - 1, l + S.Half, 2*nu*r**2) + + +def E_nl(n, l, hw): + """ + Returns the Energy of an isotropic harmonic oscillator. + + Parameters + ========== + + n : + The "nodal" quantum number. + l : + The orbital angular momentum. + hw : + The harmonic oscillator parameter. + + Notes + ===== + + The unit of the returned value matches the unit of hw, since the energy is + calculated as: + + E_nl = (2*n + l + 3/2)*hw + + Examples + ======== + + >>> from sympy.physics.sho import E_nl + >>> from sympy import symbols + >>> x, y, z = symbols('x, y, z') + >>> E_nl(x, y, z) + z*(2*x + y + 3/2) + """ + return (2*n + l + Rational(3, 2))*hw diff --git a/venv/lib/python3.11/site-packages/sympy/physics/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/tests/test_clebsch_gordan.py b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_clebsch_gordan.py new file mode 100644 index 0000000000000000000000000000000000000000..e4313e3e412d6d1883efaf693c13e0f967daf9da --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_clebsch_gordan.py @@ -0,0 +1,223 @@ +from sympy.core.numbers import (I, pi, Rational) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.functions.special.spherical_harmonics import Ynm +from sympy.matrices.dense import Matrix +from sympy.physics.wigner import (clebsch_gordan, wigner_9j, wigner_6j, gaunt, + real_gaunt, racah, dot_rot_grad_Ynm, wigner_3j, wigner_d_small, wigner_d) +from sympy.testing.pytest import raises, skip + +# for test cases, refer : https://en.wikipedia.org/wiki/Table_of_Clebsch%E2%80%93Gordan_coefficients + +def test_clebsch_gordan_docs(): + assert clebsch_gordan(Rational(3, 2), S.Half, 2, Rational(3, 2), S.Half, 2) == 1 + assert clebsch_gordan(Rational(3, 2), S.Half, 1, Rational(3, 2), Rational(-1, 2), 1) == sqrt(3)/2 + assert clebsch_gordan(Rational(3, 2), S.Half, 1, Rational(-1, 2), S.Half, 0) == -sqrt(2)/2 + + +def test_clebsch_gordan(): + # Argument order: (j_1, j_2, j, m_1, m_2, m) + + h = S.One + k = S.Half + l = Rational(3, 2) + i = Rational(-1, 2) + n = Rational(7, 2) + p = Rational(5, 2) + assert clebsch_gordan(k, k, 1, k, k, 1) == 1 + assert clebsch_gordan(k, k, 1, k, k, 0) == 0 + assert clebsch_gordan(k, k, 1, i, i, -1) == 1 + assert clebsch_gordan(k, k, 1, k, i, 0) == sqrt(2)/2 + assert clebsch_gordan(k, k, 0, k, i, 0) == sqrt(2)/2 + assert clebsch_gordan(k, k, 1, i, k, 0) == sqrt(2)/2 + assert clebsch_gordan(k, k, 0, i, k, 0) == -sqrt(2)/2 + assert clebsch_gordan(h, k, l, 1, k, l) == 1 + assert clebsch_gordan(h, k, l, 1, i, k) == 1/sqrt(3) + assert clebsch_gordan(h, k, k, 1, i, k) == sqrt(2)/sqrt(3) + assert clebsch_gordan(h, k, k, 0, k, k) == -1/sqrt(3) + assert clebsch_gordan(h, k, l, 0, k, k) == sqrt(2)/sqrt(3) + assert clebsch_gordan(h, h, S(2), 1, 1, S(2)) == 1 + assert clebsch_gordan(h, h, S(2), 1, 0, 1) == 1/sqrt(2) + assert clebsch_gordan(h, h, S(2), 0, 1, 1) == 1/sqrt(2) + assert clebsch_gordan(h, h, 1, 1, 0, 1) == 1/sqrt(2) + assert clebsch_gordan(h, h, 1, 0, 1, 1) == -1/sqrt(2) + assert clebsch_gordan(l, l, S(3), l, l, S(3)) == 1 + assert clebsch_gordan(l, l, S(2), l, k, S(2)) == 1/sqrt(2) + assert clebsch_gordan(l, l, S(3), l, k, S(2)) == 1/sqrt(2) + assert clebsch_gordan(S(2), S(2), S(4), S(2), S(2), S(4)) == 1 + assert clebsch_gordan(S(2), S(2), S(3), S(2), 1, S(3)) == 1/sqrt(2) + assert clebsch_gordan(S(2), S(2), S(3), 1, 1, S(2)) == 0 + assert clebsch_gordan(p, h, n, p, 1, n) == 1 + assert clebsch_gordan(p, h, p, p, 0, p) == sqrt(5)/sqrt(7) + assert clebsch_gordan(p, h, l, k, 1, l) == 1/sqrt(15) + + +def test_clebsch_gordan_numpy(): + try: + import numpy as np + except ImportError: + skip("numpy not installed") + assert clebsch_gordan(*np.zeros(6).astype(np.int64)) == 1 + assert wigner_3j(2, np.float64(6.0), 4.0, 0, 0, 0) == sqrt(715)/143 + assert wigner_3j(0, 0.5, 0.5, 0, 0.5, -0.5) == sqrt(2)/2 + raises(ValueError, lambda: wigner_3j(2.1, 6, 4, 0, 0, 0)) + + +def test_wigner(): + try: + import numpy as np + except ImportError: + skip("numpy not installed") + def tn(a, b): + return (a - b).n(64) < S('1e-64') + assert tn(wigner_9j(1, 1, 1, 1, 1, 1, 1, 1, 0, prec=64), Rational(1, 18)) + assert wigner_9j(3, 3, 2, 3, 3, 2, 3, 3, 2) == 3221*sqrt( + 70)/(246960*sqrt(105)) - 365/(3528*sqrt(70)*sqrt(105)) + assert wigner_6j(5, 5, 5, 5, 5, 5) == Rational(1, 52) + assert tn(wigner_6j(8, 8, 8, 8, 8, 8, prec=64), Rational(-12219, 965770)) + assert wigner_6j(1, 1, 1, 1.0, np.float64(1.0), 1) == Rational(1, 6) + assert wigner_6j(3.0, np.float32(3), 3.0, 3, 3, 3) == Rational(-1, 14) + # regression test for #8747 + half = S.Half + assert wigner_9j(0, 0, 0, 0, half, half, 0, half, half) == half + assert (wigner_9j(3, 5, 4, + 7 * half, 5 * half, 4, + 9 * half, 9 * half, 0) + == -sqrt(Rational(361, 205821000))) + assert (wigner_9j(1, 4, 3, + 5 * half, 4, 5 * half, + 5 * half, 2, 7 * half) + == -sqrt(Rational(3971, 373403520))) + assert (wigner_9j(4, 9 * half, 5 * half, + 2, 4, 4, + 5, 7 * half, 7 * half) + == -sqrt(Rational(3481, 5042614500))) + assert (wigner_9j(5, 5, 5.0, + np.float64(5.0), 5, 5, + 5, 5, 5) + == 0) + assert (wigner_9j(1.0, 2.0, 3.0, + 3, 2, 1, + 2, 1, 3) + == -4*sqrt(70)/11025) + + +def test_gaunt(): + def tn(a, b): + return (a - b).n(64) < S('1e-64') + assert gaunt(1, 0, 1, 1, 0, -1) == -1/(2*sqrt(pi)) + assert isinstance(gaunt(1, 1, 0, -1, 1, 0).args[0], Rational) + assert isinstance(gaunt(0, 1, 1, 0, -1, 1).args[0], Rational) + + assert tn(gaunt( + 10, 10, 12, 9, 3, -12, prec=64), (Rational(-98, 62031)) * sqrt(6279)/sqrt(pi)) + def gaunt_ref(l1, l2, l3, m1, m2, m3): + return ( + sqrt((2 * l1 + 1) * (2 * l2 + 1) * (2 * l3 + 1) / (4 * pi)) * + wigner_3j(l1, l2, l3, 0, 0, 0) * + wigner_3j(l1, l2, l3, m1, m2, m3) + ) + threshold = 1e-10 + l_max = 3 + l3_max = 24 + for l1 in range(l_max + 1): + for l2 in range(l_max + 1): + for l3 in range(l3_max + 1): + for m1 in range(-l1, l1 + 1): + for m2 in range(-l2, l2 + 1): + for m3 in range(-l3, l3 + 1): + args = l1, l2, l3, m1, m2, m3 + g = gaunt(*args) + g0 = gaunt_ref(*args) + assert abs(g - g0) < threshold + if m1 + m2 + m3 != 0: + assert abs(g) < threshold + if (l1 + l2 + l3) % 2: + assert abs(g) < threshold + assert gaunt(1, 1, 0, 0, 2, -2) is S.Zero + + +def test_realgaunt(): + # All non-zero values corresponding to l values from 0 to 2 + for l in range(3): + for m in range(-l, l+1): + assert real_gaunt(0, l, l, 0, m, m) == 1/(2*sqrt(pi)) + assert real_gaunt(1, 1, 2, 0, 0, 0) == sqrt(5)/(5*sqrt(pi)) + assert real_gaunt(1, 1, 2, 1, 1, 0) == -sqrt(5)/(10*sqrt(pi)) + assert real_gaunt(2, 2, 2, 0, 0, 0) == sqrt(5)/(7*sqrt(pi)) + assert real_gaunt(2, 2, 2, 0, 2, 2) == -sqrt(5)/(7*sqrt(pi)) + assert real_gaunt(2, 2, 2, -2, -2, 0) == -sqrt(5)/(7*sqrt(pi)) + assert real_gaunt(1, 1, 2, -1, 0, -1) == sqrt(15)/(10*sqrt(pi)) + assert real_gaunt(1, 1, 2, 0, 1, 1) == sqrt(15)/(10*sqrt(pi)) + assert real_gaunt(1, 1, 2, 1, 1, 2) == sqrt(15)/(10*sqrt(pi)) + assert real_gaunt(1, 1, 2, -1, 1, -2) == sqrt(15)/(10*sqrt(pi)) + assert real_gaunt(1, 1, 2, -1, -1, 2) == -sqrt(15)/(10*sqrt(pi)) + assert real_gaunt(2, 2, 2, 0, 1, 1) == sqrt(5)/(14*sqrt(pi)) + assert real_gaunt(2, 2, 2, 1, 1, 2) == sqrt(15)/(14*sqrt(pi)) + assert real_gaunt(2, 2, 2, -1, -1, 2) == -sqrt(15)/(14*sqrt(pi)) + + assert real_gaunt(-2, -2, -2, -2, -2, 0) is S.Zero # m test + assert real_gaunt(-2, 1, 0, 1, 1, 1) is S.Zero # l test + assert real_gaunt(-2, -1, -2, -1, -1, 0) is S.Zero # m and l test + assert real_gaunt(-2, -2, -2, -2, -2, -2) is S.Zero # m and k test + assert real_gaunt(-2, -1, -2, -1, -1, -1) is S.Zero # m, l and k test + + x = symbols('x', integer=True) + v = [0]*6 + for i in range(len(v)): + v[i] = x # non literal ints fail + raises(ValueError, lambda: real_gaunt(*v)) + v[i] = 0 + + +def test_racah(): + assert racah(3,3,3,3,3,3) == Rational(-1,14) + assert racah(2,2,2,2,2,2) == Rational(-3,70) + assert racah(7,8,7,1,7,7, prec=4).is_Float + assert racah(5.5,7.5,9.5,6.5,8,9) == -719*sqrt(598)/1158924 + assert abs(racah(5.5,7.5,9.5,6.5,8,9, prec=4) - (-0.01517)) < S('1e-4') + + +def test_dot_rota_grad_SH(): + theta, phi = symbols("theta phi") + assert dot_rot_grad_Ynm(1, 1, 1, 1, 1, 0) != \ + sqrt(30)*Ynm(2, 2, 1, 0)/(10*sqrt(pi)) + assert dot_rot_grad_Ynm(1, 1, 1, 1, 1, 0).doit() == \ + sqrt(30)*Ynm(2, 2, 1, 0)/(10*sqrt(pi)) + assert dot_rot_grad_Ynm(1, 5, 1, 1, 1, 2) != \ + 0 + assert dot_rot_grad_Ynm(1, 5, 1, 1, 1, 2).doit() == \ + 0 + assert dot_rot_grad_Ynm(3, 3, 3, 3, theta, phi).doit() == \ + 15*sqrt(3003)*Ynm(6, 6, theta, phi)/(143*sqrt(pi)) + assert dot_rot_grad_Ynm(3, 3, 1, 1, theta, phi).doit() == \ + sqrt(3)*Ynm(4, 4, theta, phi)/sqrt(pi) + assert dot_rot_grad_Ynm(3, 2, 2, 0, theta, phi).doit() == \ + 3*sqrt(55)*Ynm(5, 2, theta, phi)/(11*sqrt(pi)) + assert dot_rot_grad_Ynm(3, 2, 3, 2, theta, phi).doit().expand() == \ + -sqrt(70)*Ynm(4, 4, theta, phi)/(11*sqrt(pi)) + \ + 45*sqrt(182)*Ynm(6, 4, theta, phi)/(143*sqrt(pi)) + + +def test_wigner_d(): + half = S(1)/2 + assert wigner_d_small(half, 0) == Matrix([[1, 0], [0, 1]]) + assert wigner_d_small(half, pi/2) == Matrix([[1, 1], [-1, 1]])/sqrt(2) + assert wigner_d_small(half, pi) == Matrix([[0, 1], [-1, 0]]) + + alpha, beta, gamma = symbols("alpha, beta, gamma", real=True) + D = wigner_d(half, alpha, beta, gamma) + assert D[0, 0] == exp(I*alpha/2)*exp(I*gamma/2)*cos(beta/2) + assert D[0, 1] == exp(I*alpha/2)*exp(-I*gamma/2)*sin(beta/2) + assert D[1, 0] == -exp(-I*alpha/2)*exp(I*gamma/2)*sin(beta/2) + assert D[1, 1] == exp(-I*alpha/2)*exp(-I*gamma/2)*cos(beta/2) + + # Test Y_{n mi}(g*x)=\sum_{mj}D^n_{mi mj}*Y_{n mj}(x) + theta, phi = symbols("theta phi", real=True) + v = Matrix([Ynm(1, mj, theta, phi) for mj in range(1, -2, -1)]) + w = wigner_d(1, -pi/2, pi/2, -pi/2)@v.subs({theta: pi/4, phi: pi}) + w_ = v.subs({theta: pi/2, phi: pi/4}) + assert w.expand(func=True).as_real_imag() == w_.expand(func=True).as_real_imag() diff --git a/venv/lib/python3.11/site-packages/sympy/physics/tests/test_hydrogen.py b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_hydrogen.py new file mode 100644 index 0000000000000000000000000000000000000000..eb11744dd8e731f24fcd6f6be2a92ada4fffc554 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_hydrogen.py @@ -0,0 +1,126 @@ +from sympy.core.numbers import (I, Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.integrals.integrals import integrate +from sympy.simplify.simplify import simplify +from sympy.physics.hydrogen import R_nl, E_nl, E_nl_dirac, Psi_nlm +from sympy.testing.pytest import raises + +n, r, Z = symbols('n r Z') + + +def feq(a, b, max_relative_error=1e-12, max_absolute_error=1e-12): + a = float(a) + b = float(b) + # if the numbers are close enough (absolutely), then they are equal + if abs(a - b) < max_absolute_error: + return True + # if not, they can still be equal if their relative error is small + if abs(b) > abs(a): + relative_error = abs((a - b)/b) + else: + relative_error = abs((a - b)/a) + return relative_error <= max_relative_error + + +def test_wavefunction(): + a = 1/Z + R = { + (1, 0): 2*sqrt(1/a**3) * exp(-r/a), + (2, 0): sqrt(1/(2*a**3)) * exp(-r/(2*a)) * (1 - r/(2*a)), + (2, 1): S.Half * sqrt(1/(6*a**3)) * exp(-r/(2*a)) * r/a, + (3, 0): Rational(2, 3) * sqrt(1/(3*a**3)) * exp(-r/(3*a)) * + (1 - 2*r/(3*a) + Rational(2, 27) * (r/a)**2), + (3, 1): Rational(4, 27) * sqrt(2/(3*a**3)) * exp(-r/(3*a)) * + (1 - r/(6*a)) * r/a, + (3, 2): Rational(2, 81) * sqrt(2/(15*a**3)) * exp(-r/(3*a)) * (r/a)**2, + (4, 0): Rational(1, 4) * sqrt(1/a**3) * exp(-r/(4*a)) * + (1 - 3*r/(4*a) + Rational(1, 8) * (r/a)**2 - Rational(1, 192) * (r/a)**3), + (4, 1): Rational(1, 16) * sqrt(5/(3*a**3)) * exp(-r/(4*a)) * + (1 - r/(4*a) + Rational(1, 80) * (r/a)**2) * (r/a), + (4, 2): Rational(1, 64) * sqrt(1/(5*a**3)) * exp(-r/(4*a)) * + (1 - r/(12*a)) * (r/a)**2, + (4, 3): Rational(1, 768) * sqrt(1/(35*a**3)) * exp(-r/(4*a)) * (r/a)**3, + } + for n, l in R: + assert simplify(R_nl(n, l, r, Z) - R[(n, l)]) == 0 + + +def test_norm(): + # Maximum "n" which is tested: + n_max = 2 # it works, but is slow, for n_max > 2 + for n in range(n_max + 1): + for l in range(n): + assert integrate(R_nl(n, l, r)**2 * r**2, (r, 0, oo)) == 1 + +def test_psi_nlm(): + r=S('r') + phi=S('phi') + theta=S('theta') + assert (Psi_nlm(1, 0, 0, r, phi, theta) == exp(-r) / sqrt(pi)) + assert (Psi_nlm(2, 1, -1, r, phi, theta)) == S.Half * exp(-r / (2)) * r \ + * (sin(theta) * exp(-I * phi) / (4 * sqrt(pi))) + assert (Psi_nlm(3, 2, 1, r, phi, theta, 2) == -sqrt(2) * sin(theta) \ + * exp(I * phi) * cos(theta) / (4 * sqrt(pi)) * S(2) / 81 \ + * sqrt(2 * 2 ** 3) * exp(-2 * r / (3)) * (r * 2) ** 2) + +def test_hydrogen_energies(): + assert E_nl(n, Z) == -Z**2/(2*n**2) + assert E_nl(n) == -1/(2*n**2) + + assert E_nl(1, 47) == -S(47)**2/(2*1**2) + assert E_nl(2, 47) == -S(47)**2/(2*2**2) + + assert E_nl(1) == -S.One/(2*1**2) + assert E_nl(2) == -S.One/(2*2**2) + assert E_nl(3) == -S.One/(2*3**2) + assert E_nl(4) == -S.One/(2*4**2) + assert E_nl(100) == -S.One/(2*100**2) + + raises(ValueError, lambda: E_nl(0)) + + +def test_hydrogen_energies_relat(): + # First test exact formulas for small "c" so that we get nice expressions: + assert E_nl_dirac(2, 0, Z=1, c=1) == 1/sqrt(2) - 1 + assert simplify(E_nl_dirac(2, 0, Z=1, c=2) - ( (8*sqrt(3) + 16) + / sqrt(16*sqrt(3) + 32) - 4)) == 0 + assert simplify(E_nl_dirac(2, 0, Z=1, c=3) - ( (54*sqrt(2) + 81) + / sqrt(108*sqrt(2) + 162) - 9)) == 0 + + # Now test for almost the correct speed of light, without floating point + # numbers: + assert simplify(E_nl_dirac(2, 0, Z=1, c=137) - ( (352275361 + 10285412 * + sqrt(1173)) / sqrt(704550722 + 20570824 * sqrt(1173)) - 18769)) == 0 + assert simplify(E_nl_dirac(2, 0, Z=82, c=137) - ( (352275361 + 2571353 * + sqrt(12045)) / sqrt(704550722 + 5142706*sqrt(12045)) - 18769)) == 0 + + # Test using exact speed of light, and compare against the nonrelativistic + # energies: + for n in range(1, 5): + for l in range(n): + assert feq(E_nl_dirac(n, l), E_nl(n), 1e-5, 1e-5) + if l > 0: + assert feq(E_nl_dirac(n, l, False), E_nl(n), 1e-5, 1e-5) + + Z = 2 + for n in range(1, 5): + for l in range(n): + assert feq(E_nl_dirac(n, l, Z=Z), E_nl(n, Z), 1e-4, 1e-4) + if l > 0: + assert feq(E_nl_dirac(n, l, False, Z), E_nl(n, Z), 1e-4, 1e-4) + + Z = 3 + for n in range(1, 5): + for l in range(n): + assert feq(E_nl_dirac(n, l, Z=Z), E_nl(n, Z), 1e-3, 1e-3) + if l > 0: + assert feq(E_nl_dirac(n, l, False, Z), E_nl(n, Z), 1e-3, 1e-3) + + # Test the exceptions: + raises(ValueError, lambda: E_nl_dirac(0, 0)) + raises(ValueError, lambda: E_nl_dirac(1, -1)) + raises(ValueError, lambda: E_nl_dirac(1, 0, False)) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/tests/test_paulialgebra.py b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_paulialgebra.py new file mode 100644 index 0000000000000000000000000000000000000000..f773470a1802f2864b79f56d38be1de030ff86dc --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_paulialgebra.py @@ -0,0 +1,57 @@ +from sympy.core.numbers import I +from sympy.core.symbol import symbols +from sympy.physics.paulialgebra import Pauli +from sympy.testing.pytest import XFAIL +from sympy.physics.quantum import TensorProduct + +sigma1 = Pauli(1) +sigma2 = Pauli(2) +sigma3 = Pauli(3) + +tau1 = symbols("tau1", commutative = False) + + +def test_Pauli(): + + assert sigma1 == sigma1 + assert sigma1 != sigma2 + + assert sigma1*sigma2 == I*sigma3 + assert sigma3*sigma1 == I*sigma2 + assert sigma2*sigma3 == I*sigma1 + + assert sigma1*sigma1 == 1 + assert sigma2*sigma2 == 1 + assert sigma3*sigma3 == 1 + + assert sigma1**0 == 1 + assert sigma1**1 == sigma1 + assert sigma1**2 == 1 + assert sigma1**3 == sigma1 + assert sigma1**4 == 1 + + assert sigma3**2 == 1 + + assert sigma1*2*sigma1 == 2 + + +def test_evaluate_pauli_product(): + from sympy.physics.paulialgebra import evaluate_pauli_product + + assert evaluate_pauli_product(I*sigma2*sigma3) == -sigma1 + + # Check issue 6471 + assert evaluate_pauli_product(-I*4*sigma1*sigma2) == 4*sigma3 + + assert evaluate_pauli_product( + 1 + I*sigma1*sigma2*sigma1*sigma2 + \ + I*sigma1*sigma2*tau1*sigma1*sigma3 + \ + ((tau1**2).subs(tau1, I*sigma1)) + \ + sigma3*((tau1**2).subs(tau1, I*sigma1)) + \ + TensorProduct(I*sigma1*sigma2*sigma1*sigma2, 1) + ) == 1 -I + I*sigma3*tau1*sigma2 - 1 - sigma3 - I*TensorProduct(1,1) + + +@XFAIL +def test_Pauli_should_work(): + assert sigma1*sigma3*sigma1 == -sigma3 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/tests/test_physics_matrices.py b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_physics_matrices.py new file mode 100644 index 0000000000000000000000000000000000000000..14fa47668d0760826e0354c8cafae787a24256eb --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_physics_matrices.py @@ -0,0 +1,84 @@ +from sympy.physics.matrices import msigma, mgamma, minkowski_tensor, pat_matrix, mdft +from sympy.core.numbers import (I, Rational) +from sympy.core.singleton import S +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.matrices.dense import (Matrix, eye, zeros) +from sympy.testing.pytest import warns_deprecated_sympy + + +def test_parallel_axis_theorem(): + # This tests the parallel axis theorem matrix by comparing to test + # matrices. + + # First case, 1 in all directions. + mat1 = Matrix(((2, -1, -1), (-1, 2, -1), (-1, -1, 2))) + assert pat_matrix(1, 1, 1, 1) == mat1 + assert pat_matrix(2, 1, 1, 1) == 2*mat1 + + # Second case, 1 in x, 0 in all others + mat2 = Matrix(((0, 0, 0), (0, 1, 0), (0, 0, 1))) + assert pat_matrix(1, 1, 0, 0) == mat2 + assert pat_matrix(2, 1, 0, 0) == 2*mat2 + + # Third case, 1 in y, 0 in all others + mat3 = Matrix(((1, 0, 0), (0, 0, 0), (0, 0, 1))) + assert pat_matrix(1, 0, 1, 0) == mat3 + assert pat_matrix(2, 0, 1, 0) == 2*mat3 + + # Fourth case, 1 in z, 0 in all others + mat4 = Matrix(((1, 0, 0), (0, 1, 0), (0, 0, 0))) + assert pat_matrix(1, 0, 0, 1) == mat4 + assert pat_matrix(2, 0, 0, 1) == 2*mat4 + + +def test_Pauli(): + #this and the following test are testing both Pauli and Dirac matrices + #and also that the general Matrix class works correctly in a real world + #situation + sigma1 = msigma(1) + sigma2 = msigma(2) + sigma3 = msigma(3) + + assert sigma1 == sigma1 + assert sigma1 != sigma2 + + # sigma*I -> I*sigma (see #354) + assert sigma1*sigma2 == sigma3*I + assert sigma3*sigma1 == sigma2*I + assert sigma2*sigma3 == sigma1*I + + assert sigma1*sigma1 == eye(2) + assert sigma2*sigma2 == eye(2) + assert sigma3*sigma3 == eye(2) + + assert sigma1*2*sigma1 == 2*eye(2) + assert sigma1*sigma3*sigma1 == -sigma3 + + +def test_Dirac(): + gamma0 = mgamma(0) + gamma1 = mgamma(1) + gamma2 = mgamma(2) + gamma3 = mgamma(3) + gamma5 = mgamma(5) + + # gamma*I -> I*gamma (see #354) + assert gamma5 == gamma0 * gamma1 * gamma2 * gamma3 * I + assert gamma1 * gamma2 + gamma2 * gamma1 == zeros(4) + assert gamma0 * gamma0 == eye(4) * minkowski_tensor[0, 0] + assert gamma2 * gamma2 != eye(4) * minkowski_tensor[0, 0] + assert gamma2 * gamma2 == eye(4) * minkowski_tensor[2, 2] + + assert mgamma(5, True) == \ + mgamma(0, True)*mgamma(1, True)*mgamma(2, True)*mgamma(3, True)*I + +def test_mdft(): + with warns_deprecated_sympy(): + assert mdft(1) == Matrix([[1]]) + with warns_deprecated_sympy(): + assert mdft(2) == 1/sqrt(2)*Matrix([[1,1],[1,-1]]) + with warns_deprecated_sympy(): + assert mdft(4) == Matrix([[S.Half, S.Half, S.Half, S.Half], + [S.Half, -I/2, Rational(-1,2), I/2], + [S.Half, Rational(-1,2), S.Half, Rational(-1,2)], + [S.Half, I/2, Rational(-1,2), -I/2]]) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/tests/test_pring.py b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_pring.py new file mode 100644 index 0000000000000000000000000000000000000000..ed7398eac4a8bb1cd4af810825caf3fcefb5f18f --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_pring.py @@ -0,0 +1,41 @@ +from sympy.physics.pring import wavefunction, energy +from sympy.core.numbers import (I, pi) +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.integrals.integrals import integrate +from sympy.simplify.simplify import simplify +from sympy.abc import m, x, r +from sympy.physics.quantum.constants import hbar + + +def test_wavefunction(): + Psi = { + 0: (1/sqrt(2 * pi)), + 1: (1/sqrt(2 * pi)) * exp(I * x), + 2: (1/sqrt(2 * pi)) * exp(2 * I * x), + 3: (1/sqrt(2 * pi)) * exp(3 * I * x) + } + for n in Psi: + assert simplify(wavefunction(n, x) - Psi[n]) == 0 + + +def test_norm(n=1): + # Maximum "n" which is tested: + for i in range(n + 1): + assert integrate( + wavefunction(i, x) * wavefunction(-i, x), (x, 0, 2 * pi)) == 1 + + +def test_orthogonality(n=1): + # Maximum "n" which is tested: + for i in range(n + 1): + for j in range(i+1, n+1): + assert integrate( + wavefunction(i, x) * wavefunction(j, x), (x, 0, 2 * pi)) == 0 + + +def test_energy(n=1): + # Maximum "n" which is tested: + for i in range(n+1): + assert simplify( + energy(i, m, r) - ((i**2 * hbar**2) / (2 * m * r**2))) == 0 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/tests/test_qho_1d.py b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_qho_1d.py new file mode 100644 index 0000000000000000000000000000000000000000..34e52c9e3a721496fc61f7d2b31414db15caa7a8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_qho_1d.py @@ -0,0 +1,50 @@ +from sympy.core.numbers import (Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.integrals.integrals import integrate +from sympy.simplify.simplify import simplify +from sympy.abc import omega, m, x +from sympy.physics.qho_1d import psi_n, E_n, coherent_state +from sympy.physics.quantum.constants import hbar + +nu = m * omega / hbar + + +def test_wavefunction(): + Psi = { + 0: (nu/pi)**Rational(1, 4) * exp(-nu * x**2 /2), + 1: (nu/pi)**Rational(1, 4) * sqrt(2*nu) * x * exp(-nu * x**2 /2), + 2: (nu/pi)**Rational(1, 4) * (2 * nu * x**2 - 1)/sqrt(2) * exp(-nu * x**2 /2), + 3: (nu/pi)**Rational(1, 4) * sqrt(nu/3) * (2 * nu * x**3 - 3 * x) * exp(-nu * x**2 /2) + } + for n in Psi: + assert simplify(psi_n(n, x, m, omega) - Psi[n]) == 0 + + +def test_norm(n=1): + # Maximum "n" which is tested: + for i in range(n + 1): + assert integrate(psi_n(i, x, 1, 1)**2, (x, -oo, oo)) == 1 + + +def test_orthogonality(n=1): + # Maximum "n" which is tested: + for i in range(n + 1): + for j in range(i + 1, n + 1): + assert integrate( + psi_n(i, x, 1, 1)*psi_n(j, x, 1, 1), (x, -oo, oo)) == 0 + + +def test_energies(n=1): + # Maximum "n" which is tested: + for i in range(n + 1): + assert E_n(i, omega) == hbar * omega * (i + S.Half) + +def test_coherent_state(n=10): + # Maximum "n" which is tested: + # test whether coherent state is the eigenstate of annihilation operator + alpha = Symbol("alpha") + for i in range(n + 1): + assert simplify(sqrt(n + 1) * coherent_state(n + 1, alpha)) == simplify(alpha * coherent_state(n, alpha)) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/tests/test_secondquant.py b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_secondquant.py new file mode 100644 index 0000000000000000000000000000000000000000..e7f60fab05497aead65ad748460802c9c29740ce --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_secondquant.py @@ -0,0 +1,1301 @@ +from sympy.functions.elementary.complexes import conjugate +from sympy.functions.elementary.exponential import exp +from sympy.physics.secondquant import ( + Dagger, Bd, VarBosonicBasis, BBra, B, BKet, FixedBosonicBasis, + matrix_rep, apply_operators, InnerProduct, Commutator, KroneckerDelta, + AnnihilateBoson, CreateBoson, BosonicOperator, + F, Fd, FKet, BosonState, CreateFermion, AnnihilateFermion, + evaluate_deltas, AntiSymmetricTensor, contraction, NO, wicks, + PermutationOperator, simplify_index_permutations, + _sort_anticommuting_fermions, _get_ordered_dummies, + substitute_dummies, FockStateBosonKet, + ContractionAppliesOnlyToFermions +) + +from sympy.concrete.summations import Sum +from sympy.core.function import (Function, expand) +from sympy.core.numbers import (I, Rational) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.printing.repr import srepr +from sympy.simplify.simplify import simplify + +from sympy.testing.pytest import slow, raises +from sympy.printing.latex import latex + + +def test_PermutationOperator(): + p, q, r, s = symbols('p,q,r,s') + f, g, h, i = map(Function, 'fghi') + P = PermutationOperator + assert P(p, q).get_permuted(f(p)*g(q)) == -f(q)*g(p) + assert P(p, q).get_permuted(f(p, q)) == -f(q, p) + assert P(p, q).get_permuted(f(p)) == f(p) + expr = (f(p)*g(q)*h(r)*i(s) + - f(q)*g(p)*h(r)*i(s) + - f(p)*g(q)*h(s)*i(r) + + f(q)*g(p)*h(s)*i(r)) + perms = [P(p, q), P(r, s)] + assert (simplify_index_permutations(expr, perms) == + P(p, q)*P(r, s)*f(p)*g(q)*h(r)*i(s)) + assert latex(P(p, q)) == 'P(pq)' + + p1, p2 = symbols('p1,p2') + assert latex(P(p1,p2) == 'P(p_{1}p_{2})') + +def test_index_permutations_with_dummies(): + a, b, c, d = symbols('a b c d') + p, q, r, s = symbols('p q r s', cls=Dummy) + f, g = map(Function, 'fg') + P = PermutationOperator + + # No dummy substitution necessary + expr = f(a, b, p, q) - f(b, a, p, q) + assert simplify_index_permutations( + expr, [P(a, b)]) == P(a, b)*f(a, b, p, q) + + # Cases where dummy substitution is needed + expected = P(a, b)*substitute_dummies(f(a, b, p, q)) + + expr = f(a, b, p, q) - f(b, a, q, p) + result = simplify_index_permutations(expr, [P(a, b)]) + assert expected == substitute_dummies(result) + + expr = f(a, b, q, p) - f(b, a, p, q) + result = simplify_index_permutations(expr, [P(a, b)]) + assert expected == substitute_dummies(result) + + # A case where nothing can be done + expr = f(a, b, q, p) - g(b, a, p, q) + result = simplify_index_permutations(expr, [P(a, b)]) + assert expr == result + + +def test_dagger(): + i, j, n, m = symbols('i,j,n,m') + assert Dagger(1) == 1 + assert Dagger(1.0) == 1.0 + assert Dagger(2*I) == -2*I + assert Dagger(S.Half*I/3.0) == I*Rational(-1, 2)/3.0 + assert Dagger(BKet([n])) == BBra([n]) + assert Dagger(B(0)) == Bd(0) + assert Dagger(Bd(0)) == B(0) + assert Dagger(B(n)) == Bd(n) + assert Dagger(Bd(n)) == B(n) + assert Dagger(B(0) + B(1)) == Bd(0) + Bd(1) + assert Dagger(n*m) == Dagger(n)*Dagger(m) # n, m commute + assert Dagger(B(n)*B(m)) == Bd(m)*Bd(n) + assert Dagger(B(n)**10) == Dagger(B(n))**10 + assert Dagger('a') == Dagger(Symbol('a')) + assert Dagger(Dagger('a')) == Symbol('a') + assert Dagger(exp(2 * I)) == exp(-2 * I) + assert Dagger(i) == conjugate(i) + + +def test_operator(): + i, j = symbols('i,j') + o = BosonicOperator(i) + assert o.state == i + assert o.is_symbolic + o = BosonicOperator(1) + assert o.state == 1 + assert not o.is_symbolic + + +def test_create(): + i, j, n, m, p1 = symbols('i,j,n,m,p1') + o = Bd(i) + assert latex(o) == "{b^\\dagger_{i}}" + assert latex(Bd(p1)) == "{b^\\dagger_{p_{1}}}" + assert isinstance(o, CreateBoson) + o = o.subs(i, j) + assert o.atoms(Symbol) == {j} + o = Bd(0) + assert o.apply_operator(BKet([n])) == sqrt(n + 1)*BKet([n + 1]) + o = Bd(n) + assert o.apply_operator(BKet([n])) == o*BKet([n]) + + +def test_annihilate(): + i, j, n, m, p1 = symbols('i,j,n,m,p1') + o = B(i) + assert latex(o) == "b_{i}" + assert latex(B(p1)) == "b_{p_{1}}" + assert isinstance(o, AnnihilateBoson) + o = o.subs(i, j) + assert o.atoms(Symbol) == {j} + o = B(0) + assert o.apply_operator(BKet([n])) == sqrt(n)*BKet([n - 1]) + o = B(n) + assert o.apply_operator(BKet([n])) == o*BKet([n]) + + +def test_basic_state(): + i, j, n, m = symbols('i,j,n,m') + s = BosonState([0, 1, 2, 3, 4]) + assert len(s) == 5 + assert s.args[0] == tuple(range(5)) + assert s.up(0) == BosonState([1, 1, 2, 3, 4]) + assert s.down(4) == BosonState([0, 1, 2, 3, 3]) + for i in range(5): + assert s.up(i).down(i) == s + assert s.down(0) == 0 + for i in range(5): + assert s[i] == i + s = BosonState([n, m]) + assert s.down(0) == BosonState([n - 1, m]) + assert s.up(0) == BosonState([n + 1, m]) + + +def test_basic_apply(): + n = symbols("n") + e = B(0)*BKet([n]) + assert apply_operators(e) == sqrt(n)*BKet([n - 1]) + e = Bd(0)*BKet([n]) + assert apply_operators(e) == sqrt(n + 1)*BKet([n + 1]) + + +def test_complex_apply(): + n, m = symbols("n,m") + o = Bd(0)*B(0)*Bd(1)*B(0) + e = apply_operators(o*BKet([n, m])) + answer = sqrt(n)*sqrt(m + 1)*(-1 + n)*BKet([-1 + n, 1 + m]) + assert expand(e) == expand(answer) + + +def test_number_operator(): + n = symbols("n") + o = Bd(0)*B(0) + e = apply_operators(o*BKet([n])) + assert e == n*BKet([n]) + + +def test_inner_product(): + i, j, k, l = symbols('i,j,k,l') + s1 = BBra([0]) + s2 = BKet([1]) + assert InnerProduct(s1, Dagger(s1)) == 1 + assert InnerProduct(s1, s2) == 0 + s1 = BBra([i, j]) + s2 = BKet([k, l]) + r = InnerProduct(s1, s2) + assert r == KroneckerDelta(i, k)*KroneckerDelta(j, l) + + +def test_symbolic_matrix_elements(): + n, m = symbols('n,m') + s1 = BBra([n]) + s2 = BKet([m]) + o = B(0) + e = apply_operators(s1*o*s2) + assert e == sqrt(m)*KroneckerDelta(n, m - 1) + + +def test_matrix_elements(): + b = VarBosonicBasis(5) + o = B(0) + m = matrix_rep(o, b) + for i in range(4): + assert m[i, i + 1] == sqrt(i + 1) + o = Bd(0) + m = matrix_rep(o, b) + for i in range(4): + assert m[i + 1, i] == sqrt(i + 1) + + +def test_fixed_bosonic_basis(): + b = FixedBosonicBasis(2, 2) + # assert b == [FockState((2, 0)), FockState((1, 1)), FockState((0, 2))] + state = b.state(1) + assert state == FockStateBosonKet((1, 1)) + assert b.index(state) == 1 + assert b.state(1) == b[1] + assert len(b) == 3 + assert str(b) == '[FockState((2, 0)), FockState((1, 1)), FockState((0, 2))]' + assert repr(b) == '[FockState((2, 0)), FockState((1, 1)), FockState((0, 2))]' + assert srepr(b) == '[FockState((2, 0)), FockState((1, 1)), FockState((0, 2))]' + + +@slow +def test_sho(): + n, m = symbols('n,m') + h_n = Bd(n)*B(n)*(n + S.Half) + H = Sum(h_n, (n, 0, 5)) + o = H.doit(deep=False) + b = FixedBosonicBasis(2, 6) + m = matrix_rep(o, b) + # We need to double check these energy values to make sure that they + # are correct and have the proper degeneracies! + diag = [1, 2, 3, 3, 4, 5, 4, 5, 6, 7, 5, 6, 7, 8, 9, 6, 7, 8, 9, 10, 11] + for i in range(len(diag)): + assert diag[i] == m[i, i] + + +def test_commutation(): + n, m = symbols("n,m", above_fermi=True) + c = Commutator(B(0), Bd(0)) + assert c == 1 + c = Commutator(Bd(0), B(0)) + assert c == -1 + c = Commutator(B(n), Bd(0)) + assert c == KroneckerDelta(n, 0) + c = Commutator(B(0), B(0)) + assert c == 0 + c = Commutator(B(0), Bd(0)) + e = simplify(apply_operators(c*BKet([n]))) + assert e == BKet([n]) + c = Commutator(B(0), B(1)) + e = simplify(apply_operators(c*BKet([n, m]))) + assert e == 0 + + c = Commutator(F(m), Fd(m)) + assert c == +1 - 2*NO(Fd(m)*F(m)) + c = Commutator(Fd(m), F(m)) + assert c.expand() == -1 + 2*NO(Fd(m)*F(m)) + + C = Commutator + X, Y, Z = symbols('X,Y,Z', commutative=False) + assert C(C(X, Y), Z) != 0 + assert C(C(X, Z), Y) != 0 + assert C(Y, C(X, Z)) != 0 + + i, j, k, l = symbols('i,j,k,l', below_fermi=True) + a, b, c, d = symbols('a,b,c,d', above_fermi=True) + p, q, r, s = symbols('p,q,r,s') + D = KroneckerDelta + + assert C(Fd(a), F(i)) == -2*NO(F(i)*Fd(a)) + assert C(Fd(j), NO(Fd(a)*F(i))).doit(wicks=True) == -D(j, i)*Fd(a) + assert C(Fd(a)*F(i), Fd(b)*F(j)).doit(wicks=True) == 0 + + c1 = Commutator(F(a), Fd(a)) + assert Commutator.eval(c1, c1) == 0 + c = Commutator(Fd(a)*F(i),Fd(b)*F(j)) + assert latex(c) == r'\left[{a^\dagger_{a}} a_{i},{a^\dagger_{b}} a_{j}\right]' + assert repr(c) == 'Commutator(CreateFermion(a)*AnnihilateFermion(i),CreateFermion(b)*AnnihilateFermion(j))' + assert str(c) == '[CreateFermion(a)*AnnihilateFermion(i),CreateFermion(b)*AnnihilateFermion(j)]' + + +def test_create_f(): + i, j, n, m = symbols('i,j,n,m') + o = Fd(i) + assert isinstance(o, CreateFermion) + o = o.subs(i, j) + assert o.atoms(Symbol) == {j} + o = Fd(1) + assert o.apply_operator(FKet([n])) == FKet([1, n]) + assert o.apply_operator(FKet([n])) == -FKet([n, 1]) + o = Fd(n) + assert o.apply_operator(FKet([])) == FKet([n]) + + vacuum = FKet([], fermi_level=4) + assert vacuum == FKet([], fermi_level=4) + + i, j, k, l = symbols('i,j,k,l', below_fermi=True) + a, b, c, d = symbols('a,b,c,d', above_fermi=True) + p, q, r, s = symbols('p,q,r,s') + p1 = symbols("p1") + + assert Fd(i).apply_operator(FKet([i, j, k], 4)) == FKet([j, k], 4) + assert Fd(a).apply_operator(FKet([i, b, k], 4)) == FKet([a, i, b, k], 4) + + assert Dagger(B(p)).apply_operator(q) == q*CreateBoson(p) + assert repr(Fd(p)) == 'CreateFermion(p)' + assert srepr(Fd(p)) == "CreateFermion(Symbol('p'))" + assert latex(Fd(p)) == r'{a^\dagger_{p}}' + assert latex(Fd(p1)) == r'{a^\dagger_{p_{1}}}' + assert latex(FKet([a,i], 1)) == r"\left|\left( a, \ i\right)\right\rangle" + assert latex(FKet([j,i,b,a], 2)) == r"\left|\left( a, \ b, \ i, \ j\right)\right\rangle" + + +def test_annihilate_f(): + i, j, n, m = symbols('i,j,n,m') + o = F(i) + assert isinstance(o, AnnihilateFermion) + o = o.subs(i, j) + assert o.atoms(Symbol) == {j} + o = F(1) + assert o.apply_operator(FKet([1, n])) == FKet([n]) + assert o.apply_operator(FKet([n, 1])) == -FKet([n]) + o = F(n) + assert o.apply_operator(FKet([n])) == FKet([]) + + i, j, k, l = symbols('i,j,k,l', below_fermi=True) + a, b, c, d = symbols('a,b,c,d', above_fermi=True) + p, q, r, s = symbols('p,q,r,s') + p1 = symbols('p1') + + assert F(i).apply_operator(FKet([i, j, k], 4)) == 0 + assert F(a).apply_operator(FKet([i, b, k], 4)) == 0 + assert F(l).apply_operator(FKet([i, j, k], 3)) == 0 + assert F(l).apply_operator(FKet([i, j, k], 4)) == FKet([l, i, j, k], 4) + assert str(F(p)) == 'f(p)' + assert repr(F(p)) == 'AnnihilateFermion(p)' + assert srepr(F(p)) == "AnnihilateFermion(Symbol('p'))" + assert latex(F(p)) == 'a_{p}' + assert latex(F(p1)) == 'a_{p_{1}}' + + +def test_create_b(): + i, j, n, m = symbols('i,j,n,m') + o = Bd(i) + assert isinstance(o, CreateBoson) + o = o.subs(i, j) + assert o.atoms(Symbol) == {j} + o = Bd(0) + assert o.apply_operator(BKet([n])) == sqrt(n + 1)*BKet([n + 1]) + o = Bd(n) + assert o.apply_operator(BKet([n])) == o*BKet([n]) + + +def test_annihilate_b(): + i, j, n, m = symbols('i,j,n,m') + o = B(i) + assert isinstance(o, AnnihilateBoson) + o = o.subs(i, j) + assert o.atoms(Symbol) == {j} + o = B(0) + + +def test_wicks(): + p, q, r, s = symbols('p,q,r,s', above_fermi=True) + + # Testing for particles only + + str = F(p)*Fd(q) + assert wicks(str) == NO(F(p)*Fd(q)) + KroneckerDelta(p, q) + str = Fd(p)*F(q) + assert wicks(str) == NO(Fd(p)*F(q)) + + str = F(p)*Fd(q)*F(r)*Fd(s) + nstr = wicks(str) + fasit = NO( + KroneckerDelta(p, q)*KroneckerDelta(r, s) + + KroneckerDelta(p, q)*AnnihilateFermion(r)*CreateFermion(s) + + KroneckerDelta(r, s)*AnnihilateFermion(p)*CreateFermion(q) + - KroneckerDelta(p, s)*AnnihilateFermion(r)*CreateFermion(q) + - AnnihilateFermion(p)*AnnihilateFermion(r)*CreateFermion(q)*CreateFermion(s)) + assert nstr == fasit + + assert (p*q*nstr).expand() == wicks(p*q*str) + assert (nstr*p*q*2).expand() == wicks(str*p*q*2) + + # Testing CC equations particles and holes + i, j, k, l = symbols('i j k l', below_fermi=True, cls=Dummy) + a, b, c, d = symbols('a b c d', above_fermi=True, cls=Dummy) + p, q, r, s = symbols('p q r s', cls=Dummy) + + assert (wicks(F(a)*NO(F(i)*F(j))*Fd(b)) == + NO(F(a)*F(i)*F(j)*Fd(b)) + + KroneckerDelta(a, b)*NO(F(i)*F(j))) + assert (wicks(F(a)*NO(F(i)*F(j)*F(k))*Fd(b)) == + NO(F(a)*F(i)*F(j)*F(k)*Fd(b)) - + KroneckerDelta(a, b)*NO(F(i)*F(j)*F(k))) + + expr = wicks(Fd(i)*NO(Fd(j)*F(k))*F(l)) + assert (expr == + -KroneckerDelta(i, k)*NO(Fd(j)*F(l)) - + KroneckerDelta(j, l)*NO(Fd(i)*F(k)) - + KroneckerDelta(i, k)*KroneckerDelta(j, l) + + KroneckerDelta(i, l)*NO(Fd(j)*F(k)) + + NO(Fd(i)*Fd(j)*F(k)*F(l))) + expr = wicks(F(a)*NO(F(b)*Fd(c))*Fd(d)) + assert (expr == + -KroneckerDelta(a, c)*NO(F(b)*Fd(d)) - + KroneckerDelta(b, d)*NO(F(a)*Fd(c)) - + KroneckerDelta(a, c)*KroneckerDelta(b, d) + + KroneckerDelta(a, d)*NO(F(b)*Fd(c)) + + NO(F(a)*F(b)*Fd(c)*Fd(d))) + + +def test_NO(): + i, j, k, l = symbols('i j k l', below_fermi=True) + a, b, c, d = symbols('a b c d', above_fermi=True) + p, q, r, s = symbols('p q r s', cls=Dummy) + + assert (NO(Fd(p)*F(q) + Fd(a)*F(b)) == + NO(Fd(p)*F(q)) + NO(Fd(a)*F(b))) + assert (NO(Fd(i)*NO(F(j)*Fd(a))) == + NO(Fd(i)*F(j)*Fd(a))) + assert NO(1) == 1 + assert NO(i) == i + assert (NO(Fd(a)*Fd(b)*(F(c) + F(d))) == + NO(Fd(a)*Fd(b)*F(c)) + + NO(Fd(a)*Fd(b)*F(d))) + + assert NO(Fd(a)*F(b))._remove_brackets() == Fd(a)*F(b) + assert NO(F(j)*Fd(i))._remove_brackets() == F(j)*Fd(i) + + assert (NO(Fd(p)*F(q)).subs(Fd(p), Fd(a) + Fd(i)) == + NO(Fd(a)*F(q)) + NO(Fd(i)*F(q))) + assert (NO(Fd(p)*F(q)).subs(F(q), F(a) + F(i)) == + NO(Fd(p)*F(a)) + NO(Fd(p)*F(i))) + + expr = NO(Fd(p)*F(q))._remove_brackets() + assert wicks(expr) == NO(expr) + + assert NO(Fd(a)*F(b)) == - NO(F(b)*Fd(a)) + + no = NO(Fd(a)*F(i)*F(b)*Fd(j)) + l1 = list(no.iter_q_creators()) + assert l1 == [0, 1] + l2 = list(no.iter_q_annihilators()) + assert l2 == [3, 2] + no = NO(Fd(a)*Fd(i)) + assert no.has_q_creators == 1 + assert no.has_q_annihilators == -1 + assert str(no) == ':CreateFermion(a)*CreateFermion(i):' + assert repr(no) == 'NO(CreateFermion(a)*CreateFermion(i))' + assert latex(no) == r'\left\{{a^\dagger_{a}} {a^\dagger_{i}}\right\}' + raises(NotImplementedError, lambda: NO(Bd(p)*F(q))) + + +def test_sorting(): + i, j = symbols('i,j', below_fermi=True) + a, b = symbols('a,b', above_fermi=True) + p, q = symbols('p,q') + + # p, q + assert _sort_anticommuting_fermions([Fd(p), F(q)]) == ([Fd(p), F(q)], 0) + assert _sort_anticommuting_fermions([F(p), Fd(q)]) == ([Fd(q), F(p)], 1) + + # i, p + assert _sort_anticommuting_fermions([F(p), Fd(i)]) == ([F(p), Fd(i)], 0) + assert _sort_anticommuting_fermions([Fd(i), F(p)]) == ([F(p), Fd(i)], 1) + assert _sort_anticommuting_fermions([Fd(p), Fd(i)]) == ([Fd(p), Fd(i)], 0) + assert _sort_anticommuting_fermions([Fd(i), Fd(p)]) == ([Fd(p), Fd(i)], 1) + assert _sort_anticommuting_fermions([F(p), F(i)]) == ([F(i), F(p)], 1) + assert _sort_anticommuting_fermions([F(i), F(p)]) == ([F(i), F(p)], 0) + assert _sort_anticommuting_fermions([Fd(p), F(i)]) == ([F(i), Fd(p)], 1) + assert _sort_anticommuting_fermions([F(i), Fd(p)]) == ([F(i), Fd(p)], 0) + + # a, p + assert _sort_anticommuting_fermions([F(p), Fd(a)]) == ([Fd(a), F(p)], 1) + assert _sort_anticommuting_fermions([Fd(a), F(p)]) == ([Fd(a), F(p)], 0) + assert _sort_anticommuting_fermions([Fd(p), Fd(a)]) == ([Fd(a), Fd(p)], 1) + assert _sort_anticommuting_fermions([Fd(a), Fd(p)]) == ([Fd(a), Fd(p)], 0) + assert _sort_anticommuting_fermions([F(p), F(a)]) == ([F(p), F(a)], 0) + assert _sort_anticommuting_fermions([F(a), F(p)]) == ([F(p), F(a)], 1) + assert _sort_anticommuting_fermions([Fd(p), F(a)]) == ([Fd(p), F(a)], 0) + assert _sort_anticommuting_fermions([F(a), Fd(p)]) == ([Fd(p), F(a)], 1) + + # i, a + assert _sort_anticommuting_fermions([F(i), Fd(j)]) == ([F(i), Fd(j)], 0) + assert _sort_anticommuting_fermions([Fd(j), F(i)]) == ([F(i), Fd(j)], 1) + assert _sort_anticommuting_fermions([Fd(a), Fd(i)]) == ([Fd(a), Fd(i)], 0) + assert _sort_anticommuting_fermions([Fd(i), Fd(a)]) == ([Fd(a), Fd(i)], 1) + assert _sort_anticommuting_fermions([F(a), F(i)]) == ([F(i), F(a)], 1) + assert _sort_anticommuting_fermions([F(i), F(a)]) == ([F(i), F(a)], 0) + + +def test_contraction(): + i, j, k, l = symbols('i,j,k,l', below_fermi=True) + a, b, c, d = symbols('a,b,c,d', above_fermi=True) + p, q, r, s = symbols('p,q,r,s') + assert contraction(Fd(i), F(j)) == KroneckerDelta(i, j) + assert contraction(F(a), Fd(b)) == KroneckerDelta(a, b) + assert contraction(F(a), Fd(i)) == 0 + assert contraction(Fd(a), F(i)) == 0 + assert contraction(F(i), Fd(a)) == 0 + assert contraction(Fd(i), F(a)) == 0 + assert contraction(Fd(i), F(p)) == KroneckerDelta(i, p) + restr = evaluate_deltas(contraction(Fd(p), F(q))) + assert restr.is_only_below_fermi + restr = evaluate_deltas(contraction(F(p), Fd(q))) + assert restr.is_only_above_fermi + raises(ContractionAppliesOnlyToFermions, lambda: contraction(B(a), Fd(b))) + + +def test_evaluate_deltas(): + i, j, k = symbols('i,j,k') + + r = KroneckerDelta(i, j) * KroneckerDelta(j, k) + assert evaluate_deltas(r) == KroneckerDelta(i, k) + + r = KroneckerDelta(i, 0) * KroneckerDelta(j, k) + assert evaluate_deltas(r) == KroneckerDelta(i, 0) * KroneckerDelta(j, k) + + r = KroneckerDelta(1, j) * KroneckerDelta(j, k) + assert evaluate_deltas(r) == KroneckerDelta(1, k) + + r = KroneckerDelta(j, 2) * KroneckerDelta(k, j) + assert evaluate_deltas(r) == KroneckerDelta(2, k) + + r = KroneckerDelta(i, 0) * KroneckerDelta(i, j) * KroneckerDelta(j, 1) + assert evaluate_deltas(r) == 0 + + r = (KroneckerDelta(0, i) * KroneckerDelta(0, j) + * KroneckerDelta(1, j) * KroneckerDelta(1, j)) + assert evaluate_deltas(r) == 0 + + +def test_Tensors(): + i, j, k, l = symbols('i j k l', below_fermi=True, cls=Dummy) + a, b, c, d = symbols('a b c d', above_fermi=True, cls=Dummy) + p, q, r, s = symbols('p q r s') + + AT = AntiSymmetricTensor + assert AT('t', (a, b), (i, j)) == -AT('t', (b, a), (i, j)) + assert AT('t', (a, b), (i, j)) == AT('t', (b, a), (j, i)) + assert AT('t', (a, b), (i, j)) == -AT('t', (a, b), (j, i)) + assert AT('t', (a, a), (i, j)) == 0 + assert AT('t', (a, b), (i, i)) == 0 + assert AT('t', (a, b, c), (i, j)) == -AT('t', (b, a, c), (i, j)) + assert AT('t', (a, b, c), (i, j, k)) == AT('t', (b, a, c), (i, k, j)) + + tabij = AT('t', (a, b), (i, j)) + assert tabij.has(a) + assert tabij.has(b) + assert tabij.has(i) + assert tabij.has(j) + assert tabij.subs(b, c) == AT('t', (a, c), (i, j)) + assert (2*tabij).subs(i, c) == 2*AT('t', (a, b), (c, j)) + assert tabij.symbol == Symbol('t') + assert latex(tabij) == '{t^{ab}_{ij}}' + assert str(tabij) == 't((_a, _b),(_i, _j))' + + assert AT('t', (a, a), (i, j)).subs(a, b) == AT('t', (b, b), (i, j)) + assert AT('t', (a, i), (a, j)).subs(a, b) == AT('t', (b, i), (b, j)) + + a1, a2, a3, a4 = symbols('alpha1:5') + u_alpha1234 = AntiSymmetricTensor("u", (a1, a2), (a3, a4)) + + assert latex(u_alpha1234) == r'{u^{\alpha_{1}\alpha_{2}}_{\alpha_{3}\alpha_{4}}}' + assert str(u_alpha1234) == 'u((alpha1, alpha2),(alpha3, alpha4))' + + +def test_fully_contracted(): + i, j, k, l = symbols('i j k l', below_fermi=True) + a, b, c, d = symbols('a b c d', above_fermi=True) + p, q, r, s = symbols('p q r s', cls=Dummy) + + Fock = (AntiSymmetricTensor('f', (p,), (q,))* + NO(Fd(p)*F(q))) + V = (AntiSymmetricTensor('v', (p, q), (r, s))* + NO(Fd(p)*Fd(q)*F(s)*F(r)))/4 + + Fai = wicks(NO(Fd(i)*F(a))*Fock, + keep_only_fully_contracted=True, + simplify_kronecker_deltas=True) + assert Fai == AntiSymmetricTensor('f', (a,), (i,)) + Vabij = wicks(NO(Fd(i)*Fd(j)*F(b)*F(a))*V, + keep_only_fully_contracted=True, + simplify_kronecker_deltas=True) + assert Vabij == AntiSymmetricTensor('v', (a, b), (i, j)) + + +def test_substitute_dummies_without_dummies(): + i, j = symbols('i,j') + assert substitute_dummies(att(i, j) + 2) == att(i, j) + 2 + assert substitute_dummies(att(i, j) + 1) == att(i, j) + 1 + + +def test_substitute_dummies_NO_operator(): + i, j = symbols('i j', cls=Dummy) + assert substitute_dummies(att(i, j)*NO(Fd(i)*F(j)) + - att(j, i)*NO(Fd(j)*F(i))) == 0 + + +def test_substitute_dummies_SQ_operator(): + i, j = symbols('i j', cls=Dummy) + assert substitute_dummies(att(i, j)*Fd(i)*F(j) + - att(j, i)*Fd(j)*F(i)) == 0 + + +def test_substitute_dummies_new_indices(): + i, j = symbols('i j', below_fermi=True, cls=Dummy) + a, b = symbols('a b', above_fermi=True, cls=Dummy) + p, q = symbols('p q', cls=Dummy) + f = Function('f') + assert substitute_dummies(f(i, a, p) - f(j, b, q), new_indices=True) == 0 + + +def test_substitute_dummies_substitution_order(): + i, j, k, l = symbols('i j k l', below_fermi=True, cls=Dummy) + f = Function('f') + from sympy.utilities.iterables import variations + for permut in variations([i, j, k, l], 4): + assert substitute_dummies(f(*permut) - f(i, j, k, l)) == 0 + + +def test_dummy_order_inner_outer_lines_VT1T1T1(): + ii = symbols('i', below_fermi=True) + aa = symbols('a', above_fermi=True) + k, l = symbols('k l', below_fermi=True, cls=Dummy) + c, d = symbols('c d', above_fermi=True, cls=Dummy) + + v = Function('v') + t = Function('t') + dums = _get_ordered_dummies + + # Coupled-Cluster T1 terms with V*T1*T1*T1 + # t^{a}_{k} t^{c}_{i} t^{d}_{l} v^{lk}_{dc} + exprs = [ + # permut v and t <=> swapping internal lines, equivalent + # irrespective of symmetries in v + v(k, l, c, d)*t(c, ii)*t(d, l)*t(aa, k), + v(l, k, c, d)*t(c, ii)*t(d, k)*t(aa, l), + v(k, l, d, c)*t(d, ii)*t(c, l)*t(aa, k), + v(l, k, d, c)*t(d, ii)*t(c, k)*t(aa, l), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) != dums(permut) + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + + +def test_dummy_order_inner_outer_lines_VT1T1T1T1(): + ii, jj = symbols('i j', below_fermi=True) + aa, bb = symbols('a b', above_fermi=True) + k, l = symbols('k l', below_fermi=True, cls=Dummy) + c, d = symbols('c d', above_fermi=True, cls=Dummy) + + v = Function('v') + t = Function('t') + dums = _get_ordered_dummies + + # Coupled-Cluster T2 terms with V*T1*T1*T1*T1 + exprs = [ + # permut t <=> swapping external lines, not equivalent + # except if v has certain symmetries. + v(k, l, c, d)*t(c, ii)*t(d, jj)*t(aa, k)*t(bb, l), + v(k, l, c, d)*t(c, jj)*t(d, ii)*t(aa, k)*t(bb, l), + v(k, l, c, d)*t(c, ii)*t(d, jj)*t(bb, k)*t(aa, l), + v(k, l, c, d)*t(c, jj)*t(d, ii)*t(bb, k)*t(aa, l), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) != dums(permut) + assert substitute_dummies(exprs[0]) != substitute_dummies(permut) + exprs = [ + # permut v <=> swapping external lines, not equivalent + # except if v has certain symmetries. + # + # Note that in contrast to above, these permutations have identical + # dummy order. That is because the proximity to external indices + # has higher influence on the canonical dummy ordering than the + # position of a dummy on the factors. In fact, the terms here are + # similar in structure as the result of the dummy substitutions above. + v(k, l, c, d)*t(c, ii)*t(d, jj)*t(aa, k)*t(bb, l), + v(l, k, c, d)*t(c, ii)*t(d, jj)*t(aa, k)*t(bb, l), + v(k, l, d, c)*t(c, ii)*t(d, jj)*t(aa, k)*t(bb, l), + v(l, k, d, c)*t(c, ii)*t(d, jj)*t(aa, k)*t(bb, l), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) == dums(permut) + assert substitute_dummies(exprs[0]) != substitute_dummies(permut) + exprs = [ + # permut t and v <=> swapping internal lines, equivalent. + # Canonical dummy order is different, and a consistent + # substitution reveals the equivalence. + v(k, l, c, d)*t(c, ii)*t(d, jj)*t(aa, k)*t(bb, l), + v(k, l, d, c)*t(c, jj)*t(d, ii)*t(aa, k)*t(bb, l), + v(l, k, c, d)*t(c, ii)*t(d, jj)*t(bb, k)*t(aa, l), + v(l, k, d, c)*t(c, jj)*t(d, ii)*t(bb, k)*t(aa, l), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) != dums(permut) + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + + +def test_get_subNO(): + p, q, r = symbols('p,q,r') + assert NO(F(p)*F(q)*F(r)).get_subNO(1) == NO(F(p)*F(r)) + assert NO(F(p)*F(q)*F(r)).get_subNO(0) == NO(F(q)*F(r)) + assert NO(F(p)*F(q)*F(r)).get_subNO(2) == NO(F(p)*F(q)) + + +def test_equivalent_internal_lines_VT1T1(): + i, j, k, l = symbols('i j k l', below_fermi=True, cls=Dummy) + a, b, c, d = symbols('a b c d', above_fermi=True, cls=Dummy) + + v = Function('v') + t = Function('t') + dums = _get_ordered_dummies + + exprs = [ # permute v. Different dummy order. Not equivalent. + v(i, j, a, b)*t(a, i)*t(b, j), + v(j, i, a, b)*t(a, i)*t(b, j), + v(i, j, b, a)*t(a, i)*t(b, j), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) != dums(permut) + assert substitute_dummies(exprs[0]) != substitute_dummies(permut) + + exprs = [ # permute v. Different dummy order. Equivalent + v(i, j, a, b)*t(a, i)*t(b, j), + v(j, i, b, a)*t(a, i)*t(b, j), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) != dums(permut) + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + + exprs = [ # permute t. Same dummy order, not equivalent. + v(i, j, a, b)*t(a, i)*t(b, j), + v(i, j, a, b)*t(b, i)*t(a, j), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) == dums(permut) + assert substitute_dummies(exprs[0]) != substitute_dummies(permut) + + exprs = [ # permute v and t. Different dummy order, equivalent + v(i, j, a, b)*t(a, i)*t(b, j), + v(j, i, a, b)*t(a, j)*t(b, i), + v(i, j, b, a)*t(b, i)*t(a, j), + v(j, i, b, a)*t(b, j)*t(a, i), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) != dums(permut) + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + + +def test_equivalent_internal_lines_VT2conjT2(): + # this diagram requires special handling in TCE + i, j, k, l, m, n = symbols('i j k l m n', below_fermi=True, cls=Dummy) + a, b, c, d, e, f = symbols('a b c d e f', above_fermi=True, cls=Dummy) + p1, p2, p3, p4 = symbols('p1 p2 p3 p4', above_fermi=True, cls=Dummy) + h1, h2, h3, h4 = symbols('h1 h2 h3 h4', below_fermi=True, cls=Dummy) + + from sympy.utilities.iterables import variations + + v = Function('v') + t = Function('t') + dums = _get_ordered_dummies + + # v(abcd)t(abij)t(ijcd) + template = v(p1, p2, p3, p4)*t(p1, p2, i, j)*t(i, j, p3, p4) + permutator = variations([a, b, c, d], 4) + base = template.subs(zip([p1, p2, p3, p4], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4], permut) + expr = template.subs(subslist) + assert dums(base) != dums(expr) + assert substitute_dummies(expr) == substitute_dummies(base) + template = v(p1, p2, p3, p4)*t(p1, p2, j, i)*t(j, i, p3, p4) + permutator = variations([a, b, c, d], 4) + base = template.subs(zip([p1, p2, p3, p4], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4], permut) + expr = template.subs(subslist) + assert dums(base) != dums(expr) + assert substitute_dummies(expr) == substitute_dummies(base) + + # v(abcd)t(abij)t(jicd) + template = v(p1, p2, p3, p4)*t(p1, p2, i, j)*t(j, i, p3, p4) + permutator = variations([a, b, c, d], 4) + base = template.subs(zip([p1, p2, p3, p4], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4], permut) + expr = template.subs(subslist) + assert dums(base) != dums(expr) + assert substitute_dummies(expr) == substitute_dummies(base) + template = v(p1, p2, p3, p4)*t(p1, p2, j, i)*t(i, j, p3, p4) + permutator = variations([a, b, c, d], 4) + base = template.subs(zip([p1, p2, p3, p4], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4], permut) + expr = template.subs(subslist) + assert dums(base) != dums(expr) + assert substitute_dummies(expr) == substitute_dummies(base) + + +def test_equivalent_internal_lines_VT2conjT2_ambiguous_order(): + # These diagrams invokes _determine_ambiguous() because the + # dummies can not be ordered unambiguously by the key alone + i, j, k, l, m, n = symbols('i j k l m n', below_fermi=True, cls=Dummy) + a, b, c, d, e, f = symbols('a b c d e f', above_fermi=True, cls=Dummy) + p1, p2, p3, p4 = symbols('p1 p2 p3 p4', above_fermi=True, cls=Dummy) + h1, h2, h3, h4 = symbols('h1 h2 h3 h4', below_fermi=True, cls=Dummy) + + from sympy.utilities.iterables import variations + + v = Function('v') + t = Function('t') + dums = _get_ordered_dummies + + # v(abcd)t(abij)t(cdij) + template = v(p1, p2, p3, p4)*t(p1, p2, i, j)*t(p3, p4, i, j) + permutator = variations([a, b, c, d], 4) + base = template.subs(zip([p1, p2, p3, p4], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4], permut) + expr = template.subs(subslist) + assert dums(base) != dums(expr) + assert substitute_dummies(expr) == substitute_dummies(base) + template = v(p1, p2, p3, p4)*t(p1, p2, j, i)*t(p3, p4, i, j) + permutator = variations([a, b, c, d], 4) + base = template.subs(zip([p1, p2, p3, p4], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4], permut) + expr = template.subs(subslist) + assert dums(base) != dums(expr) + assert substitute_dummies(expr) == substitute_dummies(base) + + +def test_equivalent_internal_lines_VT2(): + i, j, k, l = symbols('i j k l', below_fermi=True, cls=Dummy) + a, b, c, d = symbols('a b c d', above_fermi=True, cls=Dummy) + + v = Function('v') + t = Function('t') + dums = _get_ordered_dummies + exprs = [ + # permute v. Same dummy order, not equivalent. + # + # This test show that the dummy order may not be sensitive to all + # index permutations. The following expressions have identical + # structure as the resulting terms from of the dummy substitutions + # in the test above. Here, all expressions have the same dummy + # order, so they cannot be simplified by means of dummy + # substitution. In order to simplify further, it is necessary to + # exploit symmetries in the objects, for instance if t or v is + # antisymmetric. + v(i, j, a, b)*t(a, b, i, j), + v(j, i, a, b)*t(a, b, i, j), + v(i, j, b, a)*t(a, b, i, j), + v(j, i, b, a)*t(a, b, i, j), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) == dums(permut) + assert substitute_dummies(exprs[0]) != substitute_dummies(permut) + + exprs = [ + # permute t. + v(i, j, a, b)*t(a, b, i, j), + v(i, j, a, b)*t(b, a, i, j), + v(i, j, a, b)*t(a, b, j, i), + v(i, j, a, b)*t(b, a, j, i), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) != dums(permut) + assert substitute_dummies(exprs[0]) != substitute_dummies(permut) + + exprs = [ # permute v and t. Relabelling of dummies should be equivalent. + v(i, j, a, b)*t(a, b, i, j), + v(j, i, a, b)*t(a, b, j, i), + v(i, j, b, a)*t(b, a, i, j), + v(j, i, b, a)*t(b, a, j, i), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) != dums(permut) + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + + +def test_internal_external_VT2T2(): + ii, jj = symbols('i j', below_fermi=True) + aa, bb = symbols('a b', above_fermi=True) + k, l = symbols('k l', below_fermi=True, cls=Dummy) + c, d = symbols('c d', above_fermi=True, cls=Dummy) + + v = Function('v') + t = Function('t') + dums = _get_ordered_dummies + + exprs = [ + v(k, l, c, d)*t(aa, c, ii, k)*t(bb, d, jj, l), + v(l, k, c, d)*t(aa, c, ii, l)*t(bb, d, jj, k), + v(k, l, d, c)*t(aa, d, ii, k)*t(bb, c, jj, l), + v(l, k, d, c)*t(aa, d, ii, l)*t(bb, c, jj, k), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) != dums(permut) + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + exprs = [ + v(k, l, c, d)*t(aa, c, ii, k)*t(d, bb, jj, l), + v(l, k, c, d)*t(aa, c, ii, l)*t(d, bb, jj, k), + v(k, l, d, c)*t(aa, d, ii, k)*t(c, bb, jj, l), + v(l, k, d, c)*t(aa, d, ii, l)*t(c, bb, jj, k), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) != dums(permut) + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + exprs = [ + v(k, l, c, d)*t(c, aa, ii, k)*t(bb, d, jj, l), + v(l, k, c, d)*t(c, aa, ii, l)*t(bb, d, jj, k), + v(k, l, d, c)*t(d, aa, ii, k)*t(bb, c, jj, l), + v(l, k, d, c)*t(d, aa, ii, l)*t(bb, c, jj, k), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) != dums(permut) + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + + +def test_internal_external_pqrs(): + ii, jj = symbols('i j') + aa, bb = symbols('a b') + k, l = symbols('k l', cls=Dummy) + c, d = symbols('c d', cls=Dummy) + + v = Function('v') + t = Function('t') + dums = _get_ordered_dummies + + exprs = [ + v(k, l, c, d)*t(aa, c, ii, k)*t(bb, d, jj, l), + v(l, k, c, d)*t(aa, c, ii, l)*t(bb, d, jj, k), + v(k, l, d, c)*t(aa, d, ii, k)*t(bb, c, jj, l), + v(l, k, d, c)*t(aa, d, ii, l)*t(bb, c, jj, k), + ] + for permut in exprs[1:]: + assert dums(exprs[0]) != dums(permut) + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + + +def test_dummy_order_well_defined(): + aa, bb = symbols('a b', above_fermi=True) + k, l, m = symbols('k l m', below_fermi=True, cls=Dummy) + c, d = symbols('c d', above_fermi=True, cls=Dummy) + p, q = symbols('p q', cls=Dummy) + + A = Function('A') + B = Function('B') + C = Function('C') + dums = _get_ordered_dummies + + # We go through all key components in the order of increasing priority, + # and consider only fully orderable expressions. Non-orderable expressions + # are tested elsewhere. + + # pos in first factor determines sort order + assert dums(A(k, l)*B(l, k)) == [k, l] + assert dums(A(l, k)*B(l, k)) == [l, k] + assert dums(A(k, l)*B(k, l)) == [k, l] + assert dums(A(l, k)*B(k, l)) == [l, k] + + # factors involving the index + assert dums(A(k, l)*B(l, m)*C(k, m)) == [l, k, m] + assert dums(A(k, l)*B(l, m)*C(m, k)) == [l, k, m] + assert dums(A(l, k)*B(l, m)*C(k, m)) == [l, k, m] + assert dums(A(l, k)*B(l, m)*C(m, k)) == [l, k, m] + assert dums(A(k, l)*B(m, l)*C(k, m)) == [l, k, m] + assert dums(A(k, l)*B(m, l)*C(m, k)) == [l, k, m] + assert dums(A(l, k)*B(m, l)*C(k, m)) == [l, k, m] + assert dums(A(l, k)*B(m, l)*C(m, k)) == [l, k, m] + + # same, but with factor order determined by non-dummies + assert dums(A(k, aa, l)*A(l, bb, m)*A(bb, k, m)) == [l, k, m] + assert dums(A(k, aa, l)*A(l, bb, m)*A(bb, m, k)) == [l, k, m] + assert dums(A(k, aa, l)*A(m, bb, l)*A(bb, k, m)) == [l, k, m] + assert dums(A(k, aa, l)*A(m, bb, l)*A(bb, m, k)) == [l, k, m] + assert dums(A(l, aa, k)*A(l, bb, m)*A(bb, k, m)) == [l, k, m] + assert dums(A(l, aa, k)*A(l, bb, m)*A(bb, m, k)) == [l, k, m] + assert dums(A(l, aa, k)*A(m, bb, l)*A(bb, k, m)) == [l, k, m] + assert dums(A(l, aa, k)*A(m, bb, l)*A(bb, m, k)) == [l, k, m] + + # index range + assert dums(A(p, c, k)*B(p, c, k)) == [k, c, p] + assert dums(A(p, k, c)*B(p, c, k)) == [k, c, p] + assert dums(A(c, k, p)*B(p, c, k)) == [k, c, p] + assert dums(A(c, p, k)*B(p, c, k)) == [k, c, p] + assert dums(A(k, c, p)*B(p, c, k)) == [k, c, p] + assert dums(A(k, p, c)*B(p, c, k)) == [k, c, p] + assert dums(B(p, c, k)*A(p, c, k)) == [k, c, p] + assert dums(B(p, k, c)*A(p, c, k)) == [k, c, p] + assert dums(B(c, k, p)*A(p, c, k)) == [k, c, p] + assert dums(B(c, p, k)*A(p, c, k)) == [k, c, p] + assert dums(B(k, c, p)*A(p, c, k)) == [k, c, p] + assert dums(B(k, p, c)*A(p, c, k)) == [k, c, p] + + +def test_dummy_order_ambiguous(): + aa, bb = symbols('a b', above_fermi=True) + i, j, k, l, m = symbols('i j k l m', below_fermi=True, cls=Dummy) + a, b, c, d, e = symbols('a b c d e', above_fermi=True, cls=Dummy) + p, q = symbols('p q', cls=Dummy) + p1, p2, p3, p4 = symbols('p1 p2 p3 p4', above_fermi=True, cls=Dummy) + p5, p6, p7, p8 = symbols('p5 p6 p7 p8', above_fermi=True, cls=Dummy) + h1, h2, h3, h4 = symbols('h1 h2 h3 h4', below_fermi=True, cls=Dummy) + h5, h6, h7, h8 = symbols('h5 h6 h7 h8', below_fermi=True, cls=Dummy) + + A = Function('A') + B = Function('B') + + from sympy.utilities.iterables import variations + + # A*A*A*A*B -- ordering of p5 and p4 is used to figure out the rest + template = A(p1, p2)*A(p4, p1)*A(p2, p3)*A(p3, p5)*B(p5, p4) + permutator = variations([a, b, c, d, e], 5) + base = template.subs(zip([p1, p2, p3, p4, p5], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4, p5], permut) + expr = template.subs(subslist) + assert substitute_dummies(expr) == substitute_dummies(base) + + # A*A*A*A*A -- an arbitrary index is assigned and the rest are figured out + template = A(p1, p2)*A(p4, p1)*A(p2, p3)*A(p3, p5)*A(p5, p4) + permutator = variations([a, b, c, d, e], 5) + base = template.subs(zip([p1, p2, p3, p4, p5], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4, p5], permut) + expr = template.subs(subslist) + assert substitute_dummies(expr) == substitute_dummies(base) + + # A*A*A -- ordering of p5 and p4 is used to figure out the rest + template = A(p1, p2, p4, p1)*A(p2, p3, p3, p5)*A(p5, p4) + permutator = variations([a, b, c, d, e], 5) + base = template.subs(zip([p1, p2, p3, p4, p5], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4, p5], permut) + expr = template.subs(subslist) + assert substitute_dummies(expr) == substitute_dummies(base) + + +def atv(*args): + return AntiSymmetricTensor('v', args[:2], args[2:] ) + + +def att(*args): + if len(args) == 4: + return AntiSymmetricTensor('t', args[:2], args[2:] ) + elif len(args) == 2: + return AntiSymmetricTensor('t', (args[0],), (args[1],)) + + +def test_dummy_order_inner_outer_lines_VT1T1T1_AT(): + ii = symbols('i', below_fermi=True) + aa = symbols('a', above_fermi=True) + k, l = symbols('k l', below_fermi=True, cls=Dummy) + c, d = symbols('c d', above_fermi=True, cls=Dummy) + + # Coupled-Cluster T1 terms with V*T1*T1*T1 + # t^{a}_{k} t^{c}_{i} t^{d}_{l} v^{lk}_{dc} + exprs = [ + # permut v and t <=> swapping internal lines, equivalent + # irrespective of symmetries in v + atv(k, l, c, d)*att(c, ii)*att(d, l)*att(aa, k), + atv(l, k, c, d)*att(c, ii)*att(d, k)*att(aa, l), + atv(k, l, d, c)*att(d, ii)*att(c, l)*att(aa, k), + atv(l, k, d, c)*att(d, ii)*att(c, k)*att(aa, l), + ] + for permut in exprs[1:]: + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + + +def test_dummy_order_inner_outer_lines_VT1T1T1T1_AT(): + ii, jj = symbols('i j', below_fermi=True) + aa, bb = symbols('a b', above_fermi=True) + k, l = symbols('k l', below_fermi=True, cls=Dummy) + c, d = symbols('c d', above_fermi=True, cls=Dummy) + + # Coupled-Cluster T2 terms with V*T1*T1*T1*T1 + # non-equivalent substitutions (change of sign) + exprs = [ + # permut t <=> swapping external lines + atv(k, l, c, d)*att(c, ii)*att(d, jj)*att(aa, k)*att(bb, l), + atv(k, l, c, d)*att(c, jj)*att(d, ii)*att(aa, k)*att(bb, l), + atv(k, l, c, d)*att(c, ii)*att(d, jj)*att(bb, k)*att(aa, l), + ] + for permut in exprs[1:]: + assert substitute_dummies(exprs[0]) == -substitute_dummies(permut) + + # equivalent substitutions + exprs = [ + atv(k, l, c, d)*att(c, ii)*att(d, jj)*att(aa, k)*att(bb, l), + # permut t <=> swapping external lines + atv(k, l, c, d)*att(c, jj)*att(d, ii)*att(bb, k)*att(aa, l), + ] + for permut in exprs[1:]: + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + + +def test_equivalent_internal_lines_VT1T1_AT(): + i, j, k, l = symbols('i j k l', below_fermi=True, cls=Dummy) + a, b, c, d = symbols('a b c d', above_fermi=True, cls=Dummy) + + exprs = [ # permute v. Different dummy order. Not equivalent. + atv(i, j, a, b)*att(a, i)*att(b, j), + atv(j, i, a, b)*att(a, i)*att(b, j), + atv(i, j, b, a)*att(a, i)*att(b, j), + ] + for permut in exprs[1:]: + assert substitute_dummies(exprs[0]) != substitute_dummies(permut) + + exprs = [ # permute v. Different dummy order. Equivalent + atv(i, j, a, b)*att(a, i)*att(b, j), + atv(j, i, b, a)*att(a, i)*att(b, j), + ] + for permut in exprs[1:]: + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + + exprs = [ # permute t. Same dummy order, not equivalent. + atv(i, j, a, b)*att(a, i)*att(b, j), + atv(i, j, a, b)*att(b, i)*att(a, j), + ] + for permut in exprs[1:]: + assert substitute_dummies(exprs[0]) != substitute_dummies(permut) + + exprs = [ # permute v and t. Different dummy order, equivalent + atv(i, j, a, b)*att(a, i)*att(b, j), + atv(j, i, a, b)*att(a, j)*att(b, i), + atv(i, j, b, a)*att(b, i)*att(a, j), + atv(j, i, b, a)*att(b, j)*att(a, i), + ] + for permut in exprs[1:]: + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + + +def test_equivalent_internal_lines_VT2conjT2_AT(): + # this diagram requires special handling in TCE + i, j, k, l, m, n = symbols('i j k l m n', below_fermi=True, cls=Dummy) + a, b, c, d, e, f = symbols('a b c d e f', above_fermi=True, cls=Dummy) + p1, p2, p3, p4 = symbols('p1 p2 p3 p4', above_fermi=True, cls=Dummy) + h1, h2, h3, h4 = symbols('h1 h2 h3 h4', below_fermi=True, cls=Dummy) + + from sympy.utilities.iterables import variations + + # atv(abcd)att(abij)att(ijcd) + template = atv(p1, p2, p3, p4)*att(p1, p2, i, j)*att(i, j, p3, p4) + permutator = variations([a, b, c, d], 4) + base = template.subs(zip([p1, p2, p3, p4], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4], permut) + expr = template.subs(subslist) + assert substitute_dummies(expr) == substitute_dummies(base) + template = atv(p1, p2, p3, p4)*att(p1, p2, j, i)*att(j, i, p3, p4) + permutator = variations([a, b, c, d], 4) + base = template.subs(zip([p1, p2, p3, p4], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4], permut) + expr = template.subs(subslist) + assert substitute_dummies(expr) == substitute_dummies(base) + + # atv(abcd)att(abij)att(jicd) + template = atv(p1, p2, p3, p4)*att(p1, p2, i, j)*att(j, i, p3, p4) + permutator = variations([a, b, c, d], 4) + base = template.subs(zip([p1, p2, p3, p4], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4], permut) + expr = template.subs(subslist) + assert substitute_dummies(expr) == substitute_dummies(base) + template = atv(p1, p2, p3, p4)*att(p1, p2, j, i)*att(i, j, p3, p4) + permutator = variations([a, b, c, d], 4) + base = template.subs(zip([p1, p2, p3, p4], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4], permut) + expr = template.subs(subslist) + assert substitute_dummies(expr) == substitute_dummies(base) + + +def test_equivalent_internal_lines_VT2conjT2_ambiguous_order_AT(): + # These diagrams invokes _determine_ambiguous() because the + # dummies can not be ordered unambiguously by the key alone + i, j, k, l, m, n = symbols('i j k l m n', below_fermi=True, cls=Dummy) + a, b, c, d, e, f = symbols('a b c d e f', above_fermi=True, cls=Dummy) + p1, p2, p3, p4 = symbols('p1 p2 p3 p4', above_fermi=True, cls=Dummy) + h1, h2, h3, h4 = symbols('h1 h2 h3 h4', below_fermi=True, cls=Dummy) + + from sympy.utilities.iterables import variations + + # atv(abcd)att(abij)att(cdij) + template = atv(p1, p2, p3, p4)*att(p1, p2, i, j)*att(p3, p4, i, j) + permutator = variations([a, b, c, d], 4) + base = template.subs(zip([p1, p2, p3, p4], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4], permut) + expr = template.subs(subslist) + assert substitute_dummies(expr) == substitute_dummies(base) + template = atv(p1, p2, p3, p4)*att(p1, p2, j, i)*att(p3, p4, i, j) + permutator = variations([a, b, c, d], 4) + base = template.subs(zip([p1, p2, p3, p4], next(permutator))) + for permut in permutator: + subslist = zip([p1, p2, p3, p4], permut) + expr = template.subs(subslist) + assert substitute_dummies(expr) == substitute_dummies(base) + + +def test_equivalent_internal_lines_VT2_AT(): + i, j, k, l = symbols('i j k l', below_fermi=True, cls=Dummy) + a, b, c, d = symbols('a b c d', above_fermi=True, cls=Dummy) + + exprs = [ + # permute v. Same dummy order, not equivalent. + atv(i, j, a, b)*att(a, b, i, j), + atv(j, i, a, b)*att(a, b, i, j), + atv(i, j, b, a)*att(a, b, i, j), + ] + for permut in exprs[1:]: + assert substitute_dummies(exprs[0]) != substitute_dummies(permut) + + exprs = [ + # permute t. + atv(i, j, a, b)*att(a, b, i, j), + atv(i, j, a, b)*att(b, a, i, j), + atv(i, j, a, b)*att(a, b, j, i), + ] + for permut in exprs[1:]: + assert substitute_dummies(exprs[0]) != substitute_dummies(permut) + + exprs = [ # permute v and t. Relabelling of dummies should be equivalent. + atv(i, j, a, b)*att(a, b, i, j), + atv(j, i, a, b)*att(a, b, j, i), + atv(i, j, b, a)*att(b, a, i, j), + atv(j, i, b, a)*att(b, a, j, i), + ] + for permut in exprs[1:]: + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + + +def test_internal_external_VT2T2_AT(): + ii, jj = symbols('i j', below_fermi=True) + aa, bb = symbols('a b', above_fermi=True) + k, l = symbols('k l', below_fermi=True, cls=Dummy) + c, d = symbols('c d', above_fermi=True, cls=Dummy) + + exprs = [ + atv(k, l, c, d)*att(aa, c, ii, k)*att(bb, d, jj, l), + atv(l, k, c, d)*att(aa, c, ii, l)*att(bb, d, jj, k), + atv(k, l, d, c)*att(aa, d, ii, k)*att(bb, c, jj, l), + atv(l, k, d, c)*att(aa, d, ii, l)*att(bb, c, jj, k), + ] + for permut in exprs[1:]: + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + exprs = [ + atv(k, l, c, d)*att(aa, c, ii, k)*att(d, bb, jj, l), + atv(l, k, c, d)*att(aa, c, ii, l)*att(d, bb, jj, k), + atv(k, l, d, c)*att(aa, d, ii, k)*att(c, bb, jj, l), + atv(l, k, d, c)*att(aa, d, ii, l)*att(c, bb, jj, k), + ] + for permut in exprs[1:]: + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + exprs = [ + atv(k, l, c, d)*att(c, aa, ii, k)*att(bb, d, jj, l), + atv(l, k, c, d)*att(c, aa, ii, l)*att(bb, d, jj, k), + atv(k, l, d, c)*att(d, aa, ii, k)*att(bb, c, jj, l), + atv(l, k, d, c)*att(d, aa, ii, l)*att(bb, c, jj, k), + ] + for permut in exprs[1:]: + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + + +def test_internal_external_pqrs_AT(): + ii, jj = symbols('i j') + aa, bb = symbols('a b') + k, l = symbols('k l', cls=Dummy) + c, d = symbols('c d', cls=Dummy) + + exprs = [ + atv(k, l, c, d)*att(aa, c, ii, k)*att(bb, d, jj, l), + atv(l, k, c, d)*att(aa, c, ii, l)*att(bb, d, jj, k), + atv(k, l, d, c)*att(aa, d, ii, k)*att(bb, c, jj, l), + atv(l, k, d, c)*att(aa, d, ii, l)*att(bb, c, jj, k), + ] + for permut in exprs[1:]: + assert substitute_dummies(exprs[0]) == substitute_dummies(permut) + + +def test_issue_19661(): + a = Symbol('0') + assert latex(Commutator(Bd(a)**2, B(a)) + ) == '- \\left[b_{0},{b^\\dagger_{0}}^{2}\\right]' + + +def test_canonical_ordering_AntiSymmetricTensor(): + v = symbols("v") + + c, d = symbols(('c','d'), above_fermi=True, + cls=Dummy) + k, l = symbols(('k','l'), below_fermi=True, + cls=Dummy) + + # formerly, the left gave either the left or the right + assert AntiSymmetricTensor(v, (k, l), (d, c) + ) == -AntiSymmetricTensor(v, (l, k), (d, c)) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/tests/test_sho.py b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_sho.py new file mode 100644 index 0000000000000000000000000000000000000000..7248838b4bb9ad280fd4211bbe208063b65adcf5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/tests/test_sho.py @@ -0,0 +1,21 @@ +from sympy.core import symbols, Rational, Function, diff +from sympy.physics.sho import R_nl, E_nl +from sympy.simplify.simplify import simplify + + +def test_sho_R_nl(): + omega, r = symbols('omega r') + l = symbols('l', integer=True) + u = Function('u') + + # check that it obeys the Schrodinger equation + for n in range(5): + schreq = ( -diff(u(r), r, 2)/2 + ((l*(l + 1))/(2*r**2) + + omega**2*r**2/2 - E_nl(n, l, omega))*u(r) ) + result = schreq.subs(u(r), r*R_nl(n, l, omega/2, r)) + assert simplify(result.doit()) == 0 + + +def test_energy(): + n, l, hw = symbols('n l hw') + assert simplify(E_nl(n, l, hw) - (2*n + l + Rational(3, 2))*hw) == 0 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/units/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bf17c7f3051b03d9c0fc794d9d79885c94cc878e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/__init__.py @@ -0,0 +1,453 @@ +# isort:skip_file +""" +Dimensional analysis and unit systems. + +This module defines dimension/unit systems and physical quantities. It is +based on a group-theoretical construction where dimensions are represented as +vectors (coefficients being the exponents), and units are defined as a dimension +to which we added a scale. + +Quantities are built from a factor and a unit, and are the basic objects that +one will use when doing computations. + +All objects except systems and prefixes can be used in SymPy expressions. +Note that as part of a CAS, various objects do not combine automatically +under operations. + +Details about the implementation can be found in the documentation, and we +will not repeat all the explanations we gave there concerning our approach. +Ideas about future developments can be found on the `Github wiki +`_, and you should consult +this page if you are willing to help. + +Useful functions: + +- ``find_unit``: easily lookup pre-defined units. +- ``convert_to(expr, newunit)``: converts an expression into the same + expression expressed in another unit. + +""" + +from .dimensions import Dimension, DimensionSystem +from .unitsystem import UnitSystem +from .util import convert_to +from .quantities import Quantity + +from .definitions.dimension_definitions import ( + amount_of_substance, acceleration, action, area, + capacitance, charge, conductance, current, energy, + force, frequency, impedance, inductance, length, + luminous_intensity, magnetic_density, + magnetic_flux, mass, momentum, power, pressure, temperature, time, + velocity, voltage, volume +) + +Unit = Quantity + +speed = velocity +luminosity = luminous_intensity +magnetic_flux_density = magnetic_density +amount = amount_of_substance + +from .prefixes import ( + # 10-power based: + yotta, + zetta, + exa, + peta, + tera, + giga, + mega, + kilo, + hecto, + deca, + deci, + centi, + milli, + micro, + nano, + pico, + femto, + atto, + zepto, + yocto, + # 2-power based: + kibi, + mebi, + gibi, + tebi, + pebi, + exbi, +) + +from .definitions import ( + percent, percents, + permille, + rad, radian, radians, + deg, degree, degrees, + sr, steradian, steradians, + mil, angular_mil, angular_mils, + m, meter, meters, + kg, kilogram, kilograms, + s, second, seconds, + A, ampere, amperes, + K, kelvin, kelvins, + mol, mole, moles, + cd, candela, candelas, + g, gram, grams, + mg, milligram, milligrams, + ug, microgram, micrograms, + t, tonne, metric_ton, + newton, newtons, N, + joule, joules, J, + watt, watts, W, + pascal, pascals, Pa, pa, + hertz, hz, Hz, + coulomb, coulombs, C, + volt, volts, v, V, + ohm, ohms, + siemens, S, mho, mhos, + farad, farads, F, + henry, henrys, H, + tesla, teslas, T, + weber, webers, Wb, wb, + optical_power, dioptre, D, + lux, lx, + katal, kat, + gray, Gy, + becquerel, Bq, + km, kilometer, kilometers, + dm, decimeter, decimeters, + cm, centimeter, centimeters, + mm, millimeter, millimeters, + um, micrometer, micrometers, micron, microns, + nm, nanometer, nanometers, + pm, picometer, picometers, + ft, foot, feet, + inch, inches, + yd, yard, yards, + mi, mile, miles, + nmi, nautical_mile, nautical_miles, + angstrom, angstroms, + ha, hectare, + l, L, liter, liters, + dl, dL, deciliter, deciliters, + cl, cL, centiliter, centiliters, + ml, mL, milliliter, milliliters, + ms, millisecond, milliseconds, + us, microsecond, microseconds, + ns, nanosecond, nanoseconds, + ps, picosecond, picoseconds, + minute, minutes, + h, hour, hours, + day, days, + anomalistic_year, anomalistic_years, + sidereal_year, sidereal_years, + tropical_year, tropical_years, + common_year, common_years, + julian_year, julian_years, + draconic_year, draconic_years, + gaussian_year, gaussian_years, + full_moon_cycle, full_moon_cycles, + year, years, + G, gravitational_constant, + c, speed_of_light, + elementary_charge, + hbar, + planck, + eV, electronvolt, electronvolts, + avogadro_number, + avogadro, avogadro_constant, + boltzmann, boltzmann_constant, + stefan, stefan_boltzmann_constant, + R, molar_gas_constant, + faraday_constant, + josephson_constant, + von_klitzing_constant, + Da, dalton, amu, amus, atomic_mass_unit, atomic_mass_constant, + me, electron_rest_mass, + gee, gees, acceleration_due_to_gravity, + u0, magnetic_constant, vacuum_permeability, + e0, electric_constant, vacuum_permittivity, + Z0, vacuum_impedance, + coulomb_constant, electric_force_constant, + atmosphere, atmospheres, atm, + kPa, + bar, bars, + pound, pounds, + psi, + dHg0, + mmHg, torr, + mmu, mmus, milli_mass_unit, + quart, quarts, + ly, lightyear, lightyears, + au, astronomical_unit, astronomical_units, + planck_mass, + planck_time, + planck_temperature, + planck_length, + planck_charge, + planck_area, + planck_volume, + planck_momentum, + planck_energy, + planck_force, + planck_power, + planck_density, + planck_energy_density, + planck_intensity, + planck_angular_frequency, + planck_pressure, + planck_current, + planck_voltage, + planck_impedance, + planck_acceleration, + bit, bits, + byte, + kibibyte, kibibytes, + mebibyte, mebibytes, + gibibyte, gibibytes, + tebibyte, tebibytes, + pebibyte, pebibytes, + exbibyte, exbibytes, +) + +from .systems import ( + mks, mksa, si +) + + +def find_unit(quantity, unit_system="SI"): + """ + Return a list of matching units or dimension names. + + - If ``quantity`` is a string -- units/dimensions containing the string + `quantity`. + - If ``quantity`` is a unit or dimension -- units having matching base + units or dimensions. + + Examples + ======== + + >>> from sympy.physics import units as u + >>> u.find_unit('charge') + ['C', 'coulomb', 'coulombs', 'planck_charge', 'elementary_charge'] + >>> u.find_unit(u.charge) + ['C', 'coulomb', 'coulombs', 'planck_charge', 'elementary_charge'] + >>> u.find_unit("ampere") + ['ampere', 'amperes'] + >>> u.find_unit('angstrom') + ['angstrom', 'angstroms'] + >>> u.find_unit('volt') + ['volt', 'volts', 'electronvolt', 'electronvolts', 'planck_voltage'] + >>> u.find_unit(u.inch**3)[:9] + ['L', 'l', 'cL', 'cl', 'dL', 'dl', 'mL', 'ml', 'liter'] + """ + unit_system = UnitSystem.get_unit_system(unit_system) + + import sympy.physics.units as u + rv = [] + if isinstance(quantity, str): + rv = [i for i in dir(u) if quantity in i and isinstance(getattr(u, i), Quantity)] + dim = getattr(u, quantity) + if isinstance(dim, Dimension): + rv.extend(find_unit(dim)) + else: + for i in sorted(dir(u)): + other = getattr(u, i) + if not isinstance(other, Quantity): + continue + if isinstance(quantity, Quantity): + if quantity.dimension == other.dimension: + rv.append(str(i)) + elif isinstance(quantity, Dimension): + if other.dimension == quantity: + rv.append(str(i)) + elif other.dimension == Dimension(unit_system.get_dimensional_expr(quantity)): + rv.append(str(i)) + return sorted(set(rv), key=lambda x: (len(x), x)) + +# NOTE: the old units module had additional variables: +# 'density', 'illuminance', 'resistance'. +# They were not dimensions, but units (old Unit class). + +__all__ = [ + 'Dimension', 'DimensionSystem', + 'UnitSystem', + 'convert_to', + 'Quantity', + + 'amount_of_substance', 'acceleration', 'action', 'area', + 'capacitance', 'charge', 'conductance', 'current', 'energy', + 'force', 'frequency', 'impedance', 'inductance', 'length', + 'luminous_intensity', 'magnetic_density', + 'magnetic_flux', 'mass', 'momentum', 'power', 'pressure', 'temperature', 'time', + 'velocity', 'voltage', 'volume', + + 'Unit', + + 'speed', + 'luminosity', + 'magnetic_flux_density', + 'amount', + + 'yotta', + 'zetta', + 'exa', + 'peta', + 'tera', + 'giga', + 'mega', + 'kilo', + 'hecto', + 'deca', + 'deci', + 'centi', + 'milli', + 'micro', + 'nano', + 'pico', + 'femto', + 'atto', + 'zepto', + 'yocto', + + 'kibi', + 'mebi', + 'gibi', + 'tebi', + 'pebi', + 'exbi', + + 'percent', 'percents', + 'permille', + 'rad', 'radian', 'radians', + 'deg', 'degree', 'degrees', + 'sr', 'steradian', 'steradians', + 'mil', 'angular_mil', 'angular_mils', + 'm', 'meter', 'meters', + 'kg', 'kilogram', 'kilograms', + 's', 'second', 'seconds', + 'A', 'ampere', 'amperes', + 'K', 'kelvin', 'kelvins', + 'mol', 'mole', 'moles', + 'cd', 'candela', 'candelas', + 'g', 'gram', 'grams', + 'mg', 'milligram', 'milligrams', + 'ug', 'microgram', 'micrograms', + 't', 'tonne', 'metric_ton', + 'newton', 'newtons', 'N', + 'joule', 'joules', 'J', + 'watt', 'watts', 'W', + 'pascal', 'pascals', 'Pa', 'pa', + 'hertz', 'hz', 'Hz', + 'coulomb', 'coulombs', 'C', + 'volt', 'volts', 'v', 'V', + 'ohm', 'ohms', + 'siemens', 'S', 'mho', 'mhos', + 'farad', 'farads', 'F', + 'henry', 'henrys', 'H', + 'tesla', 'teslas', 'T', + 'weber', 'webers', 'Wb', 'wb', + 'optical_power', 'dioptre', 'D', + 'lux', 'lx', + 'katal', 'kat', + 'gray', 'Gy', + 'becquerel', 'Bq', + 'km', 'kilometer', 'kilometers', + 'dm', 'decimeter', 'decimeters', + 'cm', 'centimeter', 'centimeters', + 'mm', 'millimeter', 'millimeters', + 'um', 'micrometer', 'micrometers', 'micron', 'microns', + 'nm', 'nanometer', 'nanometers', + 'pm', 'picometer', 'picometers', + 'ft', 'foot', 'feet', + 'inch', 'inches', + 'yd', 'yard', 'yards', + 'mi', 'mile', 'miles', + 'nmi', 'nautical_mile', 'nautical_miles', + 'angstrom', 'angstroms', + 'ha', 'hectare', + 'l', 'L', 'liter', 'liters', + 'dl', 'dL', 'deciliter', 'deciliters', + 'cl', 'cL', 'centiliter', 'centiliters', + 'ml', 'mL', 'milliliter', 'milliliters', + 'ms', 'millisecond', 'milliseconds', + 'us', 'microsecond', 'microseconds', + 'ns', 'nanosecond', 'nanoseconds', + 'ps', 'picosecond', 'picoseconds', + 'minute', 'minutes', + 'h', 'hour', 'hours', + 'day', 'days', + 'anomalistic_year', 'anomalistic_years', + 'sidereal_year', 'sidereal_years', + 'tropical_year', 'tropical_years', + 'common_year', 'common_years', + 'julian_year', 'julian_years', + 'draconic_year', 'draconic_years', + 'gaussian_year', 'gaussian_years', + 'full_moon_cycle', 'full_moon_cycles', + 'year', 'years', + 'G', 'gravitational_constant', + 'c', 'speed_of_light', + 'elementary_charge', + 'hbar', + 'planck', + 'eV', 'electronvolt', 'electronvolts', + 'avogadro_number', + 'avogadro', 'avogadro_constant', + 'boltzmann', 'boltzmann_constant', + 'stefan', 'stefan_boltzmann_constant', + 'R', 'molar_gas_constant', + 'faraday_constant', + 'josephson_constant', + 'von_klitzing_constant', + 'Da', 'dalton', 'amu', 'amus', 'atomic_mass_unit', 'atomic_mass_constant', + 'me', 'electron_rest_mass', + 'gee', 'gees', 'acceleration_due_to_gravity', + 'u0', 'magnetic_constant', 'vacuum_permeability', + 'e0', 'electric_constant', 'vacuum_permittivity', + 'Z0', 'vacuum_impedance', + 'coulomb_constant', 'electric_force_constant', + 'atmosphere', 'atmospheres', 'atm', + 'kPa', + 'bar', 'bars', + 'pound', 'pounds', + 'psi', + 'dHg0', + 'mmHg', 'torr', + 'mmu', 'mmus', 'milli_mass_unit', + 'quart', 'quarts', + 'ly', 'lightyear', 'lightyears', + 'au', 'astronomical_unit', 'astronomical_units', + 'planck_mass', + 'planck_time', + 'planck_temperature', + 'planck_length', + 'planck_charge', + 'planck_area', + 'planck_volume', + 'planck_momentum', + 'planck_energy', + 'planck_force', + 'planck_power', + 'planck_density', + 'planck_energy_density', + 'planck_intensity', + 'planck_angular_frequency', + 'planck_pressure', + 'planck_current', + 'planck_voltage', + 'planck_impedance', + 'planck_acceleration', + 'bit', 'bits', + 'byte', + 'kibibyte', 'kibibytes', + 'mebibyte', 'mebibytes', + 'gibibyte', 'gibibytes', + 'tebibyte', 'tebibytes', + 'pebibyte', 'pebibytes', + 'exbibyte', 'exbibytes', + + 'mks', 'mksa', 'si', +] diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/definitions/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/units/definitions/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..759889695d38c6e78237cc64974da3ecca6425cd --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/definitions/__init__.py @@ -0,0 +1,265 @@ +from .unit_definitions import ( + percent, percents, + permille, + rad, radian, radians, + deg, degree, degrees, + sr, steradian, steradians, + mil, angular_mil, angular_mils, + m, meter, meters, + kg, kilogram, kilograms, + s, second, seconds, + A, ampere, amperes, + K, kelvin, kelvins, + mol, mole, moles, + cd, candela, candelas, + g, gram, grams, + mg, milligram, milligrams, + ug, microgram, micrograms, + t, tonne, metric_ton, + newton, newtons, N, + joule, joules, J, + watt, watts, W, + pascal, pascals, Pa, pa, + hertz, hz, Hz, + coulomb, coulombs, C, + volt, volts, v, V, + ohm, ohms, + siemens, S, mho, mhos, + farad, farads, F, + henry, henrys, H, + tesla, teslas, T, + weber, webers, Wb, wb, + optical_power, dioptre, D, + lux, lx, + katal, kat, + gray, Gy, + becquerel, Bq, + km, kilometer, kilometers, + dm, decimeter, decimeters, + cm, centimeter, centimeters, + mm, millimeter, millimeters, + um, micrometer, micrometers, micron, microns, + nm, nanometer, nanometers, + pm, picometer, picometers, + ft, foot, feet, + inch, inches, + yd, yard, yards, + mi, mile, miles, + nmi, nautical_mile, nautical_miles, + ha, hectare, + l, L, liter, liters, + dl, dL, deciliter, deciliters, + cl, cL, centiliter, centiliters, + ml, mL, milliliter, milliliters, + ms, millisecond, milliseconds, + us, microsecond, microseconds, + ns, nanosecond, nanoseconds, + ps, picosecond, picoseconds, + minute, minutes, + h, hour, hours, + day, days, + anomalistic_year, anomalistic_years, + sidereal_year, sidereal_years, + tropical_year, tropical_years, + common_year, common_years, + julian_year, julian_years, + draconic_year, draconic_years, + gaussian_year, gaussian_years, + full_moon_cycle, full_moon_cycles, + year, years, + G, gravitational_constant, + c, speed_of_light, + elementary_charge, + hbar, + planck, + eV, electronvolt, electronvolts, + avogadro_number, + avogadro, avogadro_constant, + boltzmann, boltzmann_constant, + stefan, stefan_boltzmann_constant, + R, molar_gas_constant, + faraday_constant, + josephson_constant, + von_klitzing_constant, + Da, dalton, amu, amus, atomic_mass_unit, atomic_mass_constant, + me, electron_rest_mass, + gee, gees, acceleration_due_to_gravity, + u0, magnetic_constant, vacuum_permeability, + e0, electric_constant, vacuum_permittivity, + Z0, vacuum_impedance, + coulomb_constant, coulombs_constant, electric_force_constant, + atmosphere, atmospheres, atm, + kPa, kilopascal, + bar, bars, + pound, pounds, + psi, + dHg0, + mmHg, torr, + mmu, mmus, milli_mass_unit, + quart, quarts, + angstrom, angstroms, + ly, lightyear, lightyears, + au, astronomical_unit, astronomical_units, + planck_mass, + planck_time, + planck_temperature, + planck_length, + planck_charge, + planck_area, + planck_volume, + planck_momentum, + planck_energy, + planck_force, + planck_power, + planck_density, + planck_energy_density, + planck_intensity, + planck_angular_frequency, + planck_pressure, + planck_current, + planck_voltage, + planck_impedance, + planck_acceleration, + bit, bits, + byte, + kibibyte, kibibytes, + mebibyte, mebibytes, + gibibyte, gibibytes, + tebibyte, tebibytes, + pebibyte, pebibytes, + exbibyte, exbibytes, + curie, rutherford +) + +__all__ = [ + 'percent', 'percents', + 'permille', + 'rad', 'radian', 'radians', + 'deg', 'degree', 'degrees', + 'sr', 'steradian', 'steradians', + 'mil', 'angular_mil', 'angular_mils', + 'm', 'meter', 'meters', + 'kg', 'kilogram', 'kilograms', + 's', 'second', 'seconds', + 'A', 'ampere', 'amperes', + 'K', 'kelvin', 'kelvins', + 'mol', 'mole', 'moles', + 'cd', 'candela', 'candelas', + 'g', 'gram', 'grams', + 'mg', 'milligram', 'milligrams', + 'ug', 'microgram', 'micrograms', + 't', 'tonne', 'metric_ton', + 'newton', 'newtons', 'N', + 'joule', 'joules', 'J', + 'watt', 'watts', 'W', + 'pascal', 'pascals', 'Pa', 'pa', + 'hertz', 'hz', 'Hz', + 'coulomb', 'coulombs', 'C', + 'volt', 'volts', 'v', 'V', + 'ohm', 'ohms', + 'siemens', 'S', 'mho', 'mhos', + 'farad', 'farads', 'F', + 'henry', 'henrys', 'H', + 'tesla', 'teslas', 'T', + 'weber', 'webers', 'Wb', 'wb', + 'optical_power', 'dioptre', 'D', + 'lux', 'lx', + 'katal', 'kat', + 'gray', 'Gy', + 'becquerel', 'Bq', + 'km', 'kilometer', 'kilometers', + 'dm', 'decimeter', 'decimeters', + 'cm', 'centimeter', 'centimeters', + 'mm', 'millimeter', 'millimeters', + 'um', 'micrometer', 'micrometers', 'micron', 'microns', + 'nm', 'nanometer', 'nanometers', + 'pm', 'picometer', 'picometers', + 'ft', 'foot', 'feet', + 'inch', 'inches', + 'yd', 'yard', 'yards', + 'mi', 'mile', 'miles', + 'nmi', 'nautical_mile', 'nautical_miles', + 'ha', 'hectare', + 'l', 'L', 'liter', 'liters', + 'dl', 'dL', 'deciliter', 'deciliters', + 'cl', 'cL', 'centiliter', 'centiliters', + 'ml', 'mL', 'milliliter', 'milliliters', + 'ms', 'millisecond', 'milliseconds', + 'us', 'microsecond', 'microseconds', + 'ns', 'nanosecond', 'nanoseconds', + 'ps', 'picosecond', 'picoseconds', + 'minute', 'minutes', + 'h', 'hour', 'hours', + 'day', 'days', + 'anomalistic_year', 'anomalistic_years', + 'sidereal_year', 'sidereal_years', + 'tropical_year', 'tropical_years', + 'common_year', 'common_years', + 'julian_year', 'julian_years', + 'draconic_year', 'draconic_years', + 'gaussian_year', 'gaussian_years', + 'full_moon_cycle', 'full_moon_cycles', + 'year', 'years', + 'G', 'gravitational_constant', + 'c', 'speed_of_light', + 'elementary_charge', + 'hbar', + 'planck', + 'eV', 'electronvolt', 'electronvolts', + 'avogadro_number', + 'avogadro', 'avogadro_constant', + 'boltzmann', 'boltzmann_constant', + 'stefan', 'stefan_boltzmann_constant', + 'R', 'molar_gas_constant', + 'faraday_constant', + 'josephson_constant', + 'von_klitzing_constant', + 'Da', 'dalton', 'amu', 'amus', 'atomic_mass_unit', 'atomic_mass_constant', + 'me', 'electron_rest_mass', + 'gee', 'gees', 'acceleration_due_to_gravity', + 'u0', 'magnetic_constant', 'vacuum_permeability', + 'e0', 'electric_constant', 'vacuum_permittivity', + 'Z0', 'vacuum_impedance', + 'coulomb_constant', 'coulombs_constant', 'electric_force_constant', + 'atmosphere', 'atmospheres', 'atm', + 'kPa', 'kilopascal', + 'bar', 'bars', + 'pound', 'pounds', + 'psi', + 'dHg0', + 'mmHg', 'torr', + 'mmu', 'mmus', 'milli_mass_unit', + 'quart', 'quarts', + 'angstrom', 'angstroms', + 'ly', 'lightyear', 'lightyears', + 'au', 'astronomical_unit', 'astronomical_units', + 'planck_mass', + 'planck_time', + 'planck_temperature', + 'planck_length', + 'planck_charge', + 'planck_area', + 'planck_volume', + 'planck_momentum', + 'planck_energy', + 'planck_force', + 'planck_power', + 'planck_density', + 'planck_energy_density', + 'planck_intensity', + 'planck_angular_frequency', + 'planck_pressure', + 'planck_current', + 'planck_voltage', + 'planck_impedance', + 'planck_acceleration', + 'bit', 'bits', + 'byte', + 'kibibyte', 'kibibytes', + 'mebibyte', 'mebibytes', + 'gibibyte', 'gibibytes', + 'tebibyte', 'tebibytes', + 'pebibyte', 'pebibytes', + 'exbibyte', 'exbibytes', + 'curie', 'rutherford', +] diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/definitions/dimension_definitions.py b/venv/lib/python3.11/site-packages/sympy/physics/units/definitions/dimension_definitions.py new file mode 100644 index 0000000000000000000000000000000000000000..b2b5f1dee01f9107d966a99d1e616c79a070a5b8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/definitions/dimension_definitions.py @@ -0,0 +1,43 @@ +from sympy.physics.units import Dimension + + +angle: Dimension = Dimension(name="angle") + +# base dimensions (MKS) +length = Dimension(name="length", symbol="L") +mass = Dimension(name="mass", symbol="M") +time = Dimension(name="time", symbol="T") + +# base dimensions (MKSA not in MKS) +current: Dimension = Dimension(name='current', symbol='I') + +# other base dimensions: +temperature: Dimension = Dimension("temperature", "T") +amount_of_substance: Dimension = Dimension("amount_of_substance") +luminous_intensity: Dimension = Dimension("luminous_intensity") + +# derived dimensions (MKS) +velocity = Dimension(name="velocity") +acceleration = Dimension(name="acceleration") +momentum = Dimension(name="momentum") +force = Dimension(name="force", symbol="F") +energy = Dimension(name="energy", symbol="E") +power = Dimension(name="power") +pressure = Dimension(name="pressure") +frequency = Dimension(name="frequency", symbol="f") +action = Dimension(name="action", symbol="A") +area = Dimension("area") +volume = Dimension("volume") + +# derived dimensions (MKSA not in MKS) +voltage: Dimension = Dimension(name='voltage', symbol='U') +impedance: Dimension = Dimension(name='impedance', symbol='Z') +conductance: Dimension = Dimension(name='conductance', symbol='G') +capacitance: Dimension = Dimension(name='capacitance') +inductance: Dimension = Dimension(name='inductance') +charge: Dimension = Dimension(name='charge', symbol='Q') +magnetic_density: Dimension = Dimension(name='magnetic_density', symbol='B') +magnetic_flux: Dimension = Dimension(name='magnetic_flux') + +# Dimensions in information theory: +information: Dimension = Dimension(name='information') diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/definitions/unit_definitions.py b/venv/lib/python3.11/site-packages/sympy/physics/units/definitions/unit_definitions.py new file mode 100644 index 0000000000000000000000000000000000000000..c0a89802a444a40172a0dc70094321f07a7e396b --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/definitions/unit_definitions.py @@ -0,0 +1,407 @@ +from sympy.physics.units.definitions.dimension_definitions import current, temperature, amount_of_substance, \ + luminous_intensity, angle, charge, voltage, impedance, conductance, capacitance, inductance, magnetic_density, \ + magnetic_flux, information + +from sympy.core.numbers import (Rational, pi) +from sympy.core.singleton import S as S_singleton +from sympy.physics.units.prefixes import kilo, mega, milli, micro, deci, centi, nano, pico, kibi, mebi, gibi, tebi, pebi, exbi +from sympy.physics.units.quantities import PhysicalConstant, Quantity + +One = S_singleton.One + +#### UNITS #### + +# Dimensionless: +percent = percents = Quantity("percent", latex_repr=r"\%") +percent.set_global_relative_scale_factor(Rational(1, 100), One) + +permille = Quantity("permille") +permille.set_global_relative_scale_factor(Rational(1, 1000), One) + + +# Angular units (dimensionless) +rad = radian = radians = Quantity("radian", abbrev="rad") +radian.set_global_dimension(angle) +deg = degree = degrees = Quantity("degree", abbrev="deg", latex_repr=r"^\circ") +degree.set_global_relative_scale_factor(pi/180, radian) +sr = steradian = steradians = Quantity("steradian", abbrev="sr") +mil = angular_mil = angular_mils = Quantity("angular_mil", abbrev="mil") + +# Base units: +m = meter = meters = Quantity("meter", abbrev="m") + +# gram; used to define its prefixed units +g = gram = grams = Quantity("gram", abbrev="g") + +# NOTE: the `kilogram` has scale factor 1000. In SI, kg is a base unit, but +# nonetheless we are trying to be compatible with the `kilo` prefix. In a +# similar manner, people using CGS or gaussian units could argue that the +# `centimeter` rather than `meter` is the fundamental unit for length, but the +# scale factor of `centimeter` will be kept as 1/100 to be compatible with the +# `centi` prefix. The current state of the code assumes SI unit dimensions, in +# the future this module will be modified in order to be unit system-neutral +# (that is, support all kinds of unit systems). +kg = kilogram = kilograms = Quantity("kilogram", abbrev="kg") +kg.set_global_relative_scale_factor(kilo, gram) + +s = second = seconds = Quantity("second", abbrev="s") +A = ampere = amperes = Quantity("ampere", abbrev='A') +ampere.set_global_dimension(current) +K = kelvin = kelvins = Quantity("kelvin", abbrev='K') +kelvin.set_global_dimension(temperature) +mol = mole = moles = Quantity("mole", abbrev="mol") +mole.set_global_dimension(amount_of_substance) +cd = candela = candelas = Quantity("candela", abbrev="cd") +candela.set_global_dimension(luminous_intensity) + +# derived units +newton = newtons = N = Quantity("newton", abbrev="N") + +kilonewton = kilonewtons = kN = Quantity("kilonewton", abbrev="kN") +kilonewton.set_global_relative_scale_factor(kilo, newton) + +meganewton = meganewtons = MN = Quantity("meganewton", abbrev="MN") +meganewton.set_global_relative_scale_factor(mega, newton) + +joule = joules = J = Quantity("joule", abbrev="J") +watt = watts = W = Quantity("watt", abbrev="W") +pascal = pascals = Pa = pa = Quantity("pascal", abbrev="Pa") +hertz = hz = Hz = Quantity("hertz", abbrev="Hz") + +# CGS derived units: +dyne = Quantity("dyne") +dyne.set_global_relative_scale_factor(One/10**5, newton) +erg = Quantity("erg") +erg.set_global_relative_scale_factor(One/10**7, joule) + +# MKSA extension to MKS: derived units +coulomb = coulombs = C = Quantity("coulomb", abbrev='C') +coulomb.set_global_dimension(charge) +volt = volts = v = V = Quantity("volt", abbrev='V') +volt.set_global_dimension(voltage) +ohm = ohms = Quantity("ohm", abbrev='ohm', latex_repr=r"\Omega") +ohm.set_global_dimension(impedance) +siemens = S = mho = mhos = Quantity("siemens", abbrev='S') +siemens.set_global_dimension(conductance) +farad = farads = F = Quantity("farad", abbrev='F') +farad.set_global_dimension(capacitance) +henry = henrys = H = Quantity("henry", abbrev='H') +henry.set_global_dimension(inductance) +tesla = teslas = T = Quantity("tesla", abbrev='T') +tesla.set_global_dimension(magnetic_density) +weber = webers = Wb = wb = Quantity("weber", abbrev='Wb') +weber.set_global_dimension(magnetic_flux) + +# CGS units for electromagnetic quantities: +statampere = Quantity("statampere") +statcoulomb = statC = franklin = Quantity("statcoulomb", abbrev="statC") +statvolt = Quantity("statvolt") +gauss = Quantity("gauss") +maxwell = Quantity("maxwell") +debye = Quantity("debye") +oersted = Quantity("oersted") + +# Other derived units: +optical_power = dioptre = diopter = D = Quantity("dioptre") +lux = lx = Quantity("lux", abbrev="lx") + +# katal is the SI unit of catalytic activity +katal = kat = Quantity("katal", abbrev="kat") + +# gray is the SI unit of absorbed dose +gray = Gy = Quantity("gray") + +# becquerel is the SI unit of radioactivity +becquerel = Bq = Quantity("becquerel", abbrev="Bq") + + +# Common mass units + +mg = milligram = milligrams = Quantity("milligram", abbrev="mg") +mg.set_global_relative_scale_factor(milli, gram) + +ug = microgram = micrograms = Quantity("microgram", abbrev="ug", latex_repr=r"\mu\text{g}") +ug.set_global_relative_scale_factor(micro, gram) + +# Atomic mass constant +Da = dalton = amu = amus = atomic_mass_unit = atomic_mass_constant = PhysicalConstant("atomic_mass_constant") + +t = metric_ton = tonne = Quantity("tonne", abbrev="t") +tonne.set_global_relative_scale_factor(mega, gram) + +# Electron rest mass +me = electron_rest_mass = Quantity("electron_rest_mass", abbrev="me") + + +# Common length units + +km = kilometer = kilometers = Quantity("kilometer", abbrev="km") +km.set_global_relative_scale_factor(kilo, meter) + +dm = decimeter = decimeters = Quantity("decimeter", abbrev="dm") +dm.set_global_relative_scale_factor(deci, meter) + +cm = centimeter = centimeters = Quantity("centimeter", abbrev="cm") +cm.set_global_relative_scale_factor(centi, meter) + +mm = millimeter = millimeters = Quantity("millimeter", abbrev="mm") +mm.set_global_relative_scale_factor(milli, meter) + +um = micrometer = micrometers = micron = microns = \ + Quantity("micrometer", abbrev="um", latex_repr=r'\mu\text{m}') +um.set_global_relative_scale_factor(micro, meter) + +nm = nanometer = nanometers = Quantity("nanometer", abbrev="nm") +nm.set_global_relative_scale_factor(nano, meter) + +pm = picometer = picometers = Quantity("picometer", abbrev="pm") +pm.set_global_relative_scale_factor(pico, meter) + +ft = foot = feet = Quantity("foot", abbrev="ft") +ft.set_global_relative_scale_factor(Rational(3048, 10000), meter) + +inch = inches = Quantity("inch") +inch.set_global_relative_scale_factor(Rational(1, 12), foot) + +yd = yard = yards = Quantity("yard", abbrev="yd") +yd.set_global_relative_scale_factor(3, feet) + +mi = mile = miles = Quantity("mile") +mi.set_global_relative_scale_factor(5280, feet) + +nmi = nautical_mile = nautical_miles = Quantity("nautical_mile") +nmi.set_global_relative_scale_factor(6076, feet) + +angstrom = angstroms = Quantity("angstrom", latex_repr=r'\r{A}') +angstrom.set_global_relative_scale_factor(Rational(1, 10**10), meter) + + +# Common volume and area units + +ha = hectare = Quantity("hectare", abbrev="ha") + +l = L = liter = liters = Quantity("liter", abbrev="l") + +dl = dL = deciliter = deciliters = Quantity("deciliter", abbrev="dl") +dl.set_global_relative_scale_factor(Rational(1, 10), liter) + +cl = cL = centiliter = centiliters = Quantity("centiliter", abbrev="cl") +cl.set_global_relative_scale_factor(Rational(1, 100), liter) + +ml = mL = milliliter = milliliters = Quantity("milliliter", abbrev="ml") +ml.set_global_relative_scale_factor(Rational(1, 1000), liter) + + +# Common time units + +ms = millisecond = milliseconds = Quantity("millisecond", abbrev="ms") +millisecond.set_global_relative_scale_factor(milli, second) + +us = microsecond = microseconds = Quantity("microsecond", abbrev="us", latex_repr=r'\mu\text{s}') +microsecond.set_global_relative_scale_factor(micro, second) + +ns = nanosecond = nanoseconds = Quantity("nanosecond", abbrev="ns") +nanosecond.set_global_relative_scale_factor(nano, second) + +ps = picosecond = picoseconds = Quantity("picosecond", abbrev="ps") +picosecond.set_global_relative_scale_factor(pico, second) + +minute = minutes = Quantity("minute") +minute.set_global_relative_scale_factor(60, second) + +h = hour = hours = Quantity("hour") +hour.set_global_relative_scale_factor(60, minute) + +day = days = Quantity("day") +day.set_global_relative_scale_factor(24, hour) + +anomalistic_year = anomalistic_years = Quantity("anomalistic_year") +anomalistic_year.set_global_relative_scale_factor(365.259636, day) + +sidereal_year = sidereal_years = Quantity("sidereal_year") +sidereal_year.set_global_relative_scale_factor(31558149.540, seconds) + +tropical_year = tropical_years = Quantity("tropical_year") +tropical_year.set_global_relative_scale_factor(365.24219, day) + +common_year = common_years = Quantity("common_year") +common_year.set_global_relative_scale_factor(365, day) + +julian_year = julian_years = Quantity("julian_year") +julian_year.set_global_relative_scale_factor((365 + One/4), day) + +draconic_year = draconic_years = Quantity("draconic_year") +draconic_year.set_global_relative_scale_factor(346.62, day) + +gaussian_year = gaussian_years = Quantity("gaussian_year") +gaussian_year.set_global_relative_scale_factor(365.2568983, day) + +full_moon_cycle = full_moon_cycles = Quantity("full_moon_cycle") +full_moon_cycle.set_global_relative_scale_factor(411.78443029, day) + +year = years = tropical_year + + +#### CONSTANTS #### + +# Newton constant +G = gravitational_constant = PhysicalConstant("gravitational_constant", abbrev="G") + +# speed of light +c = speed_of_light = PhysicalConstant("speed_of_light", abbrev="c") + +# elementary charge +elementary_charge = PhysicalConstant("elementary_charge", abbrev="e") + +# Planck constant +planck = PhysicalConstant("planck", abbrev="h") + +# Reduced Planck constant +hbar = PhysicalConstant("hbar", abbrev="hbar") + +# Electronvolt +eV = electronvolt = electronvolts = PhysicalConstant("electronvolt", abbrev="eV") + +# Avogadro number +avogadro_number = PhysicalConstant("avogadro_number") + +# Avogadro constant +avogadro = avogadro_constant = PhysicalConstant("avogadro_constant") + +# Boltzmann constant +boltzmann = boltzmann_constant = PhysicalConstant("boltzmann_constant") + +# Stefan-Boltzmann constant +stefan = stefan_boltzmann_constant = PhysicalConstant("stefan_boltzmann_constant") + +# Molar gas constant +R = molar_gas_constant = PhysicalConstant("molar_gas_constant", abbrev="R") + +# Faraday constant +faraday_constant = PhysicalConstant("faraday_constant") + +# Josephson constant +josephson_constant = PhysicalConstant("josephson_constant", abbrev="K_j") + +# Von Klitzing constant +von_klitzing_constant = PhysicalConstant("von_klitzing_constant", abbrev="R_k") + +# Acceleration due to gravity (on the Earth surface) +gee = gees = acceleration_due_to_gravity = PhysicalConstant("acceleration_due_to_gravity", abbrev="g") + +# magnetic constant: +u0 = magnetic_constant = vacuum_permeability = PhysicalConstant("magnetic_constant") + +# electric constat: +e0 = electric_constant = vacuum_permittivity = PhysicalConstant("vacuum_permittivity") + +# vacuum impedance: +Z0 = vacuum_impedance = PhysicalConstant("vacuum_impedance", abbrev='Z_0', latex_repr=r'Z_{0}') + +# Coulomb's constant: +coulomb_constant = coulombs_constant = electric_force_constant = \ + PhysicalConstant("coulomb_constant", abbrev="k_e") + + +atmosphere = atmospheres = atm = Quantity("atmosphere", abbrev="atm") + +kPa = kilopascal = Quantity("kilopascal", abbrev="kPa") +kilopascal.set_global_relative_scale_factor(kilo, Pa) + +bar = bars = Quantity("bar", abbrev="bar") + +pound = pounds = Quantity("pound") # exact + +psi = Quantity("psi") + +dHg0 = 13.5951 # approx value at 0 C +mmHg = torr = Quantity("mmHg") + +atmosphere.set_global_relative_scale_factor(101325, pascal) +bar.set_global_relative_scale_factor(100, kPa) +pound.set_global_relative_scale_factor(Rational(45359237, 100000000), kg) + +mmu = mmus = milli_mass_unit = Quantity("milli_mass_unit") + +quart = quarts = Quantity("quart") + + +# Other convenient units and magnitudes + +ly = lightyear = lightyears = Quantity("lightyear", abbrev="ly") + +au = astronomical_unit = astronomical_units = Quantity("astronomical_unit", abbrev="AU") + + +# Fundamental Planck units: +planck_mass = Quantity("planck_mass", abbrev="m_P", latex_repr=r'm_\text{P}') + +planck_time = Quantity("planck_time", abbrev="t_P", latex_repr=r't_\text{P}') + +planck_temperature = Quantity("planck_temperature", abbrev="T_P", + latex_repr=r'T_\text{P}') + +planck_length = Quantity("planck_length", abbrev="l_P", latex_repr=r'l_\text{P}') + +planck_charge = Quantity("planck_charge", abbrev="q_P", latex_repr=r'q_\text{P}') + + +# Derived Planck units: +planck_area = Quantity("planck_area") + +planck_volume = Quantity("planck_volume") + +planck_momentum = Quantity("planck_momentum") + +planck_energy = Quantity("planck_energy", abbrev="E_P", latex_repr=r'E_\text{P}') + +planck_force = Quantity("planck_force", abbrev="F_P", latex_repr=r'F_\text{P}') + +planck_power = Quantity("planck_power", abbrev="P_P", latex_repr=r'P_\text{P}') + +planck_density = Quantity("planck_density", abbrev="rho_P", latex_repr=r'\rho_\text{P}') + +planck_energy_density = Quantity("planck_energy_density", abbrev="rho^E_P") + +planck_intensity = Quantity("planck_intensity", abbrev="I_P", latex_repr=r'I_\text{P}') + +planck_angular_frequency = Quantity("planck_angular_frequency", abbrev="omega_P", + latex_repr=r'\omega_\text{P}') + +planck_pressure = Quantity("planck_pressure", abbrev="p_P", latex_repr=r'p_\text{P}') + +planck_current = Quantity("planck_current", abbrev="I_P", latex_repr=r'I_\text{P}') + +planck_voltage = Quantity("planck_voltage", abbrev="V_P", latex_repr=r'V_\text{P}') + +planck_impedance = Quantity("planck_impedance", abbrev="Z_P", latex_repr=r'Z_\text{P}') + +planck_acceleration = Quantity("planck_acceleration", abbrev="a_P", + latex_repr=r'a_\text{P}') + + +# Information theory units: +bit = bits = Quantity("bit") +bit.set_global_dimension(information) + +byte = bytes = Quantity("byte") + +kibibyte = kibibytes = Quantity("kibibyte") +mebibyte = mebibytes = Quantity("mebibyte") +gibibyte = gibibytes = Quantity("gibibyte") +tebibyte = tebibytes = Quantity("tebibyte") +pebibyte = pebibytes = Quantity("pebibyte") +exbibyte = exbibytes = Quantity("exbibyte") + +byte.set_global_relative_scale_factor(8, bit) +kibibyte.set_global_relative_scale_factor(kibi, byte) +mebibyte.set_global_relative_scale_factor(mebi, byte) +gibibyte.set_global_relative_scale_factor(gibi, byte) +tebibyte.set_global_relative_scale_factor(tebi, byte) +pebibyte.set_global_relative_scale_factor(pebi, byte) +exbibyte.set_global_relative_scale_factor(exbi, byte) + +# Older units for radioactivity +curie = Ci = Quantity("curie", abbrev="Ci") + +rutherford = Rd = Quantity("rutherford", abbrev="Rd") diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/dimensions.py b/venv/lib/python3.11/site-packages/sympy/physics/units/dimensions.py new file mode 100644 index 0000000000000000000000000000000000000000..de42912edca025a6cb53d457fd3e03d8fa30931e --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/dimensions.py @@ -0,0 +1,590 @@ +""" +Definition of physical dimensions. + +Unit systems will be constructed on top of these dimensions. + +Most of the examples in the doc use MKS system and are presented from the +computer point of view: from a human point, adding length to time is not legal +in MKS but it is in natural system; for a computer in natural system there is +no time dimension (but a velocity dimension instead) - in the basis - so the +question of adding time to length has no meaning. +""" + +from __future__ import annotations + +import collections +from functools import reduce + +from sympy.core.basic import Basic +from sympy.core.containers import (Dict, Tuple) +from sympy.core.singleton import S +from sympy.core.sorting import default_sort_key +from sympy.core.symbol import Symbol +from sympy.core.sympify import sympify +from sympy.matrices.dense import Matrix +from sympy.functions.elementary.trigonometric import TrigonometricFunction +from sympy.core.expr import Expr +from sympy.core.power import Pow + + +class _QuantityMapper: + + _quantity_scale_factors_global: dict[Expr, Expr] = {} + _quantity_dimensional_equivalence_map_global: dict[Expr, Expr] = {} + _quantity_dimension_global: dict[Expr, Expr] = {} + + def __init__(self, *args, **kwargs): + self._quantity_dimension_map = {} + self._quantity_scale_factors = {} + + def set_quantity_dimension(self, quantity, dimension): + """ + Set the dimension for the quantity in a unit system. + + If this relation is valid in every unit system, use + ``quantity.set_global_dimension(dimension)`` instead. + """ + from sympy.physics.units import Quantity + dimension = sympify(dimension) + if not isinstance(dimension, Dimension): + if dimension == 1: + dimension = Dimension(1) + else: + raise ValueError("expected dimension or 1") + elif isinstance(dimension, Quantity): + dimension = self.get_quantity_dimension(dimension) + self._quantity_dimension_map[quantity] = dimension + + def set_quantity_scale_factor(self, quantity, scale_factor): + """ + Set the scale factor of a quantity relative to another quantity. + + It should be used only once per quantity to just one other quantity, + the algorithm will then be able to compute the scale factors to all + other quantities. + + In case the scale factor is valid in every unit system, please use + ``quantity.set_global_relative_scale_factor(scale_factor)`` instead. + """ + from sympy.physics.units import Quantity + from sympy.physics.units.prefixes import Prefix + scale_factor = sympify(scale_factor) + # replace all prefixes by their ratio to canonical units: + scale_factor = scale_factor.replace( + lambda x: isinstance(x, Prefix), + lambda x: x.scale_factor + ) + # replace all quantities by their ratio to canonical units: + scale_factor = scale_factor.replace( + lambda x: isinstance(x, Quantity), + lambda x: self.get_quantity_scale_factor(x) + ) + self._quantity_scale_factors[quantity] = scale_factor + + def get_quantity_dimension(self, unit): + from sympy.physics.units import Quantity + # First look-up the local dimension map, then the global one: + if unit in self._quantity_dimension_map: + return self._quantity_dimension_map[unit] + if unit in self._quantity_dimension_global: + return self._quantity_dimension_global[unit] + if unit in self._quantity_dimensional_equivalence_map_global: + dep_unit = self._quantity_dimensional_equivalence_map_global[unit] + if isinstance(dep_unit, Quantity): + return self.get_quantity_dimension(dep_unit) + else: + return Dimension(self.get_dimensional_expr(dep_unit)) + if isinstance(unit, Quantity): + return Dimension(unit.name) + else: + return Dimension(1) + + def get_quantity_scale_factor(self, unit): + if unit in self._quantity_scale_factors: + return self._quantity_scale_factors[unit] + if unit in self._quantity_scale_factors_global: + mul_factor, other_unit = self._quantity_scale_factors_global[unit] + return mul_factor*self.get_quantity_scale_factor(other_unit) + return S.One + + +class Dimension(Expr): + """ + This class represent the dimension of a physical quantities. + + The ``Dimension`` constructor takes as parameters a name and an optional + symbol. + + For example, in classical mechanics we know that time is different from + temperature and dimensions make this difference (but they do not provide + any measure of these quantities. + + >>> from sympy.physics.units import Dimension + >>> length = Dimension('length') + >>> length + Dimension(length) + >>> time = Dimension('time') + >>> time + Dimension(time) + + Dimensions can be composed using multiplication, division and + exponentiation (by a number) to give new dimensions. Addition and + subtraction is defined only when the two objects are the same dimension. + + >>> velocity = length / time + >>> velocity + Dimension(length/time) + + It is possible to use a dimension system object to get the dimensionsal + dependencies of a dimension, for example the dimension system used by the + SI units convention can be used: + + >>> from sympy.physics.units.systems.si import dimsys_SI + >>> dimsys_SI.get_dimensional_dependencies(velocity) + {Dimension(length, L): 1, Dimension(time, T): -1} + >>> length + length + Dimension(length) + >>> l2 = length**2 + >>> l2 + Dimension(length**2) + >>> dimsys_SI.get_dimensional_dependencies(l2) + {Dimension(length, L): 2} + + """ + + _op_priority = 13.0 + + # XXX: This doesn't seem to be used anywhere... + _dimensional_dependencies = {} # type: ignore + + is_commutative = True + is_number = False + # make sqrt(M**2) --> M + is_positive = True + is_real = True + + def __new__(cls, name, symbol=None): + + if isinstance(name, str): + name = Symbol(name) + else: + name = sympify(name) + + if not isinstance(name, Expr): + raise TypeError("Dimension name needs to be a valid math expression") + + if isinstance(symbol, str): + symbol = Symbol(symbol) + elif symbol is not None: + assert isinstance(symbol, Symbol) + + obj = Expr.__new__(cls, name) + + obj._name = name + obj._symbol = symbol + return obj + + @property + def name(self): + return self._name + + @property + def symbol(self): + return self._symbol + + def __str__(self): + """ + Display the string representation of the dimension. + """ + if self.symbol is None: + return "Dimension(%s)" % (self.name) + else: + return "Dimension(%s, %s)" % (self.name, self.symbol) + + def __repr__(self): + return self.__str__() + + def __neg__(self): + return self + + def __add__(self, other): + from sympy.physics.units.quantities import Quantity + other = sympify(other) + if isinstance(other, Basic): + if other.has(Quantity): + raise TypeError("cannot sum dimension and quantity") + if isinstance(other, Dimension) and self == other: + return self + return super().__add__(other) + return self + + def __radd__(self, other): + return self.__add__(other) + + def __sub__(self, other): + # there is no notion of ordering (or magnitude) among dimension, + # subtraction is equivalent to addition when the operation is legal + return self + other + + def __rsub__(self, other): + # there is no notion of ordering (or magnitude) among dimension, + # subtraction is equivalent to addition when the operation is legal + return self + other + + def __pow__(self, other): + return self._eval_power(other) + + def _eval_power(self, other): + other = sympify(other) + return Dimension(self.name**other) + + def __mul__(self, other): + from sympy.physics.units.quantities import Quantity + if isinstance(other, Basic): + if other.has(Quantity): + raise TypeError("cannot sum dimension and quantity") + if isinstance(other, Dimension): + return Dimension(self.name*other.name) + if not other.free_symbols: # other.is_number cannot be used + return self + return super().__mul__(other) + return self + + def __rmul__(self, other): + return self.__mul__(other) + + def __truediv__(self, other): + return self*Pow(other, -1) + + def __rtruediv__(self, other): + return other * pow(self, -1) + + @classmethod + def _from_dimensional_dependencies(cls, dependencies): + return reduce(lambda x, y: x * y, ( + d**e for d, e in dependencies.items() + ), 1) + + def has_integer_powers(self, dim_sys): + """ + Check if the dimension object has only integer powers. + + All the dimension powers should be integers, but rational powers may + appear in intermediate steps. This method may be used to check that the + final result is well-defined. + """ + + return all(dpow.is_Integer for dpow in dim_sys.get_dimensional_dependencies(self).values()) + + +# Create dimensions according to the base units in MKSA. +# For other unit systems, they can be derived by transforming the base +# dimensional dependency dictionary. + + +class DimensionSystem(Basic, _QuantityMapper): + r""" + DimensionSystem represents a coherent set of dimensions. + + The constructor takes three parameters: + + - base dimensions; + - derived dimensions: these are defined in terms of the base dimensions + (for example velocity is defined from the division of length by time); + - dependency of dimensions: how the derived dimensions depend + on the base dimensions. + + Optionally either the ``derived_dims`` or the ``dimensional_dependencies`` + may be omitted. + """ + + def __new__(cls, base_dims, derived_dims=(), dimensional_dependencies={}): + dimensional_dependencies = dict(dimensional_dependencies) + + def parse_dim(dim): + if isinstance(dim, str): + dim = Dimension(Symbol(dim)) + elif isinstance(dim, Dimension): + pass + elif isinstance(dim, Symbol): + dim = Dimension(dim) + else: + raise TypeError("%s wrong type" % dim) + return dim + + base_dims = [parse_dim(i) for i in base_dims] + derived_dims = [parse_dim(i) for i in derived_dims] + + for dim in base_dims: + if (dim in dimensional_dependencies + and (len(dimensional_dependencies[dim]) != 1 or + dimensional_dependencies[dim].get(dim, None) != 1)): + raise IndexError("Repeated value in base dimensions") + dimensional_dependencies[dim] = Dict({dim: 1}) + + def parse_dim_name(dim): + if isinstance(dim, Dimension): + return dim + elif isinstance(dim, str): + return Dimension(Symbol(dim)) + elif isinstance(dim, Symbol): + return Dimension(dim) + else: + raise TypeError("unrecognized type %s for %s" % (type(dim), dim)) + + for dim in dimensional_dependencies.keys(): + dim = parse_dim(dim) + if (dim not in derived_dims) and (dim not in base_dims): + derived_dims.append(dim) + + def parse_dict(d): + return Dict({parse_dim_name(i): j for i, j in d.items()}) + + # Make sure everything is a SymPy type: + dimensional_dependencies = {parse_dim_name(i): parse_dict(j) for i, j in + dimensional_dependencies.items()} + + for dim in derived_dims: + if dim in base_dims: + raise ValueError("Dimension %s both in base and derived" % dim) + if dim not in dimensional_dependencies: + # TODO: should this raise a warning? + dimensional_dependencies[dim] = Dict({dim: 1}) + + base_dims.sort(key=default_sort_key) + derived_dims.sort(key=default_sort_key) + + base_dims = Tuple(*base_dims) + derived_dims = Tuple(*derived_dims) + dimensional_dependencies = Dict({i: Dict(j) for i, j in dimensional_dependencies.items()}) + obj = Basic.__new__(cls, base_dims, derived_dims, dimensional_dependencies) + return obj + + @property + def base_dims(self): + return self.args[0] + + @property + def derived_dims(self): + return self.args[1] + + @property + def dimensional_dependencies(self): + return self.args[2] + + def _get_dimensional_dependencies_for_name(self, dimension): + if isinstance(dimension, str): + dimension = Dimension(Symbol(dimension)) + elif not isinstance(dimension, Dimension): + dimension = Dimension(dimension) + + if dimension.name.is_Symbol: + # Dimensions not included in the dependencies are considered + # as base dimensions: + return dict(self.dimensional_dependencies.get(dimension, {dimension: 1})) + + if dimension.name.is_number or dimension.name.is_NumberSymbol: + return {} + + get_for_name = self._get_dimensional_dependencies_for_name + + if dimension.name.is_Mul: + ret = collections.defaultdict(int) + dicts = [get_for_name(i) for i in dimension.name.args] + for d in dicts: + for k, v in d.items(): + ret[k] += v + return {k: v for (k, v) in ret.items() if v != 0} + + if dimension.name.is_Add: + dicts = [get_for_name(i) for i in dimension.name.args] + if all(d == dicts[0] for d in dicts[1:]): + return dicts[0] + raise TypeError("Only equivalent dimensions can be added or subtracted.") + + if dimension.name.is_Pow: + dim_base = get_for_name(dimension.name.base) + dim_exp = get_for_name(dimension.name.exp) + if dim_exp == {} or dimension.name.exp.is_Symbol: + return {k: v * dimension.name.exp for (k, v) in dim_base.items()} + else: + raise TypeError("The exponent for the power operator must be a Symbol or dimensionless.") + + if dimension.name.is_Function: + args = (Dimension._from_dimensional_dependencies( + get_for_name(arg)) for arg in dimension.name.args) + result = dimension.name.func(*args) + + dicts = [get_for_name(i) for i in dimension.name.args] + + if isinstance(result, Dimension): + return self.get_dimensional_dependencies(result) + elif result.func == dimension.name.func: + if isinstance(dimension.name, TrigonometricFunction): + if dicts[0] in ({}, {Dimension('angle'): 1}): + return {} + else: + raise TypeError("The input argument for the function {} must be dimensionless or have dimensions of angle.".format(dimension.func)) + else: + if all(item == {} for item in dicts): + return {} + else: + raise TypeError("The input arguments for the function {} must be dimensionless.".format(dimension.func)) + else: + return get_for_name(result) + + raise TypeError("Type {} not implemented for get_dimensional_dependencies".format(type(dimension.name))) + + def get_dimensional_dependencies(self, name, mark_dimensionless=False): + dimdep = self._get_dimensional_dependencies_for_name(name) + if mark_dimensionless and dimdep == {}: + return {Dimension(1): 1} + return dict(dimdep.items()) + + def equivalent_dims(self, dim1, dim2): + deps1 = self.get_dimensional_dependencies(dim1) + deps2 = self.get_dimensional_dependencies(dim2) + return deps1 == deps2 + + def extend(self, new_base_dims, new_derived_dims=(), new_dim_deps=None): + deps = dict(self.dimensional_dependencies) + if new_dim_deps: + deps.update(new_dim_deps) + + new_dim_sys = DimensionSystem( + tuple(self.base_dims) + tuple(new_base_dims), + tuple(self.derived_dims) + tuple(new_derived_dims), + deps + ) + new_dim_sys._quantity_dimension_map.update(self._quantity_dimension_map) + new_dim_sys._quantity_scale_factors.update(self._quantity_scale_factors) + return new_dim_sys + + def is_dimensionless(self, dimension): + """ + Check if the dimension object really has a dimension. + + A dimension should have at least one component with non-zero power. + """ + if dimension.name == 1: + return True + return self.get_dimensional_dependencies(dimension) == {} + + @property + def list_can_dims(self): + """ + Useless method, kept for compatibility with previous versions. + + DO NOT USE. + + List all canonical dimension names. + """ + dimset = set() + for i in self.base_dims: + dimset.update(set(self.get_dimensional_dependencies(i).keys())) + return tuple(sorted(dimset, key=str)) + + @property + def inv_can_transf_matrix(self): + """ + Useless method, kept for compatibility with previous versions. + + DO NOT USE. + + Compute the inverse transformation matrix from the base to the + canonical dimension basis. + + It corresponds to the matrix where columns are the vector of base + dimensions in canonical basis. + + This matrix will almost never be used because dimensions are always + defined with respect to the canonical basis, so no work has to be done + to get them in this basis. Nonetheless if this matrix is not square + (or not invertible) it means that we have chosen a bad basis. + """ + matrix = reduce(lambda x, y: x.row_join(y), + [self.dim_can_vector(d) for d in self.base_dims]) + return matrix + + @property + def can_transf_matrix(self): + """ + Useless method, kept for compatibility with previous versions. + + DO NOT USE. + + Return the canonical transformation matrix from the canonical to the + base dimension basis. + + It is the inverse of the matrix computed with inv_can_transf_matrix(). + """ + + #TODO: the inversion will fail if the system is inconsistent, for + # example if the matrix is not a square + return reduce(lambda x, y: x.row_join(y), + [self.dim_can_vector(d) for d in sorted(self.base_dims, key=str)] + ).inv() + + def dim_can_vector(self, dim): + """ + Useless method, kept for compatibility with previous versions. + + DO NOT USE. + + Dimensional representation in terms of the canonical base dimensions. + """ + + vec = [] + for d in self.list_can_dims: + vec.append(self.get_dimensional_dependencies(dim).get(d, 0)) + return Matrix(vec) + + def dim_vector(self, dim): + """ + Useless method, kept for compatibility with previous versions. + + DO NOT USE. + + + Vector representation in terms of the base dimensions. + """ + return self.can_transf_matrix * Matrix(self.dim_can_vector(dim)) + + def print_dim_base(self, dim): + """ + Give the string expression of a dimension in term of the basis symbols. + """ + dims = self.dim_vector(dim) + symbols = [i.symbol if i.symbol is not None else i.name for i in self.base_dims] + res = S.One + for (s, p) in zip(symbols, dims): + res *= s**p + return res + + @property + def dim(self): + """ + Useless method, kept for compatibility with previous versions. + + DO NOT USE. + + Give the dimension of the system. + + That is return the number of dimensions forming the basis. + """ + return len(self.base_dims) + + @property + def is_consistent(self): + """ + Useless method, kept for compatibility with previous versions. + + DO NOT USE. + + Check if the system is well defined. + """ + + # not enough or too many base dimensions compared to independent + # dimensions + # in vector language: the set of vectors do not form a basis + return self.inv_can_transf_matrix.is_square diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/prefixes.py b/venv/lib/python3.11/site-packages/sympy/physics/units/prefixes.py new file mode 100644 index 0000000000000000000000000000000000000000..44fd7cb9efe4b1d6307810af6b9cd140817126f9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/prefixes.py @@ -0,0 +1,219 @@ +""" +Module defining unit prefixe class and some constants. + +Constant dict for SI and binary prefixes are defined as PREFIXES and +BIN_PREFIXES. +""" +from sympy.core.expr import Expr +from sympy.core.sympify import sympify +from sympy.core.singleton import S + +class Prefix(Expr): + """ + This class represent prefixes, with their name, symbol and factor. + + Prefixes are used to create derived units from a given unit. They should + always be encapsulated into units. + + The factor is constructed from a base (default is 10) to some power, and + it gives the total multiple or fraction. For example the kilometer km + is constructed from the meter (factor 1) and the kilo (10 to the power 3, + i.e. 1000). The base can be changed to allow e.g. binary prefixes. + + A prefix multiplied by something will always return the product of this + other object times the factor, except if the other object: + + - is a prefix and they can be combined into a new prefix; + - defines multiplication with prefixes (which is the case for the Unit + class). + """ + _op_priority = 13.0 + is_commutative = True + + def __new__(cls, name, abbrev, exponent, base=sympify(10), latex_repr=None): + + name = sympify(name) + abbrev = sympify(abbrev) + exponent = sympify(exponent) + base = sympify(base) + + obj = Expr.__new__(cls, name, abbrev, exponent, base) + obj._name = name + obj._abbrev = abbrev + obj._scale_factor = base**exponent + obj._exponent = exponent + obj._base = base + obj._latex_repr = latex_repr + return obj + + @property + def name(self): + return self._name + + @property + def abbrev(self): + return self._abbrev + + @property + def scale_factor(self): + return self._scale_factor + + def _latex(self, printer): + if self._latex_repr is None: + return r'\text{%s}' % self._abbrev + return self._latex_repr + + @property + def base(self): + return self._base + + def __str__(self): + return str(self._abbrev) + + def __repr__(self): + if self.base == 10: + return "Prefix(%r, %r, %r)" % ( + str(self.name), str(self.abbrev), self._exponent) + else: + return "Prefix(%r, %r, %r, %r)" % ( + str(self.name), str(self.abbrev), self._exponent, self.base) + + def __mul__(self, other): + from sympy.physics.units import Quantity + if not isinstance(other, (Quantity, Prefix)): + return super().__mul__(other) + + fact = self.scale_factor * other.scale_factor + + if isinstance(other, Prefix): + if fact == 1: + return S.One + # simplify prefix + for p in PREFIXES: + if PREFIXES[p].scale_factor == fact: + return PREFIXES[p] + return fact + + return self.scale_factor * other + + def __truediv__(self, other): + if not hasattr(other, "scale_factor"): + return super().__truediv__(other) + + fact = self.scale_factor / other.scale_factor + + if fact == 1: + return S.One + elif isinstance(other, Prefix): + for p in PREFIXES: + if PREFIXES[p].scale_factor == fact: + return PREFIXES[p] + return fact + + return self.scale_factor / other + + def __rtruediv__(self, other): + if other == 1: + for p in PREFIXES: + if PREFIXES[p].scale_factor == 1 / self.scale_factor: + return PREFIXES[p] + return other / self.scale_factor + + +def prefix_unit(unit, prefixes): + """ + Return a list of all units formed by unit and the given prefixes. + + You can use the predefined PREFIXES or BIN_PREFIXES, but you can also + pass as argument a subdict of them if you do not want all prefixed units. + + >>> from sympy.physics.units.prefixes import (PREFIXES, + ... prefix_unit) + >>> from sympy.physics.units import m + >>> pref = {"m": PREFIXES["m"], "c": PREFIXES["c"], "d": PREFIXES["d"]} + >>> prefix_unit(m, pref) # doctest: +SKIP + [millimeter, centimeter, decimeter] + """ + + from sympy.physics.units.quantities import Quantity + from sympy.physics.units import UnitSystem + + prefixed_units = [] + + for prefix in prefixes.values(): + quantity = Quantity( + "%s%s" % (prefix.name, unit.name), + abbrev=("%s%s" % (prefix.abbrev, unit.abbrev)), + is_prefixed=True, + ) + UnitSystem._quantity_dimensional_equivalence_map_global[quantity] = unit + UnitSystem._quantity_scale_factors_global[quantity] = (prefix.scale_factor, unit) + prefixed_units.append(quantity) + + return prefixed_units + + +yotta = Prefix('yotta', 'Y', 24) +zetta = Prefix('zetta', 'Z', 21) +exa = Prefix('exa', 'E', 18) +peta = Prefix('peta', 'P', 15) +tera = Prefix('tera', 'T', 12) +giga = Prefix('giga', 'G', 9) +mega = Prefix('mega', 'M', 6) +kilo = Prefix('kilo', 'k', 3) +hecto = Prefix('hecto', 'h', 2) +deca = Prefix('deca', 'da', 1) +deci = Prefix('deci', 'd', -1) +centi = Prefix('centi', 'c', -2) +milli = Prefix('milli', 'm', -3) +micro = Prefix('micro', 'mu', -6, latex_repr=r"\mu") +nano = Prefix('nano', 'n', -9) +pico = Prefix('pico', 'p', -12) +femto = Prefix('femto', 'f', -15) +atto = Prefix('atto', 'a', -18) +zepto = Prefix('zepto', 'z', -21) +yocto = Prefix('yocto', 'y', -24) + + +# https://physics.nist.gov/cuu/Units/prefixes.html +PREFIXES = { + 'Y': yotta, + 'Z': zetta, + 'E': exa, + 'P': peta, + 'T': tera, + 'G': giga, + 'M': mega, + 'k': kilo, + 'h': hecto, + 'da': deca, + 'd': deci, + 'c': centi, + 'm': milli, + 'mu': micro, + 'n': nano, + 'p': pico, + 'f': femto, + 'a': atto, + 'z': zepto, + 'y': yocto, +} + + +kibi = Prefix('kibi', 'Y', 10, 2) +mebi = Prefix('mebi', 'Y', 20, 2) +gibi = Prefix('gibi', 'Y', 30, 2) +tebi = Prefix('tebi', 'Y', 40, 2) +pebi = Prefix('pebi', 'Y', 50, 2) +exbi = Prefix('exbi', 'Y', 60, 2) + + +# https://physics.nist.gov/cuu/Units/binary.html +BIN_PREFIXES = { + 'Ki': kibi, + 'Mi': mebi, + 'Gi': gibi, + 'Ti': tebi, + 'Pi': pebi, + 'Ei': exbi, +} diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/quantities.py b/venv/lib/python3.11/site-packages/sympy/physics/units/quantities.py new file mode 100644 index 0000000000000000000000000000000000000000..cc19e72aea83b5bd8ae7cf2f63dd49388a3815ee --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/quantities.py @@ -0,0 +1,152 @@ +""" +Physical quantities. +""" + +from sympy.core.expr import AtomicExpr +from sympy.core.symbol import Symbol +from sympy.core.sympify import sympify +from sympy.physics.units.dimensions import _QuantityMapper +from sympy.physics.units.prefixes import Prefix + + +class Quantity(AtomicExpr): + """ + Physical quantity: can be a unit of measure, a constant or a generic quantity. + """ + + is_commutative = True + is_real = True + is_number = False + is_nonzero = True + is_physical_constant = False + _diff_wrt = True + + def __new__(cls, name, abbrev=None, + latex_repr=None, pretty_unicode_repr=None, + pretty_ascii_repr=None, mathml_presentation_repr=None, + is_prefixed=False, + **assumptions): + + if not isinstance(name, Symbol): + name = Symbol(name) + + if abbrev is None: + abbrev = name + elif isinstance(abbrev, str): + abbrev = Symbol(abbrev) + + # HACK: These are here purely for type checking. They actually get assigned below. + cls._is_prefixed = is_prefixed + + obj = AtomicExpr.__new__(cls, name, abbrev) + obj._name = name + obj._abbrev = abbrev + obj._latex_repr = latex_repr + obj._unicode_repr = pretty_unicode_repr + obj._ascii_repr = pretty_ascii_repr + obj._mathml_repr = mathml_presentation_repr + obj._is_prefixed = is_prefixed + return obj + + def set_global_dimension(self, dimension): + _QuantityMapper._quantity_dimension_global[self] = dimension + + def set_global_relative_scale_factor(self, scale_factor, reference_quantity): + """ + Setting a scale factor that is valid across all unit system. + """ + from sympy.physics.units import UnitSystem + scale_factor = sympify(scale_factor) + if isinstance(scale_factor, Prefix): + self._is_prefixed = True + # replace all prefixes by their ratio to canonical units: + scale_factor = scale_factor.replace( + lambda x: isinstance(x, Prefix), + lambda x: x.scale_factor + ) + scale_factor = sympify(scale_factor) + UnitSystem._quantity_scale_factors_global[self] = (scale_factor, reference_quantity) + UnitSystem._quantity_dimensional_equivalence_map_global[self] = reference_quantity + + @property + def name(self): + return self._name + + @property + def dimension(self): + from sympy.physics.units import UnitSystem + unit_system = UnitSystem.get_default_unit_system() + return unit_system.get_quantity_dimension(self) + + @property + def abbrev(self): + """ + Symbol representing the unit name. + + Prepend the abbreviation with the prefix symbol if it is defines. + """ + return self._abbrev + + @property + def scale_factor(self): + """ + Overall magnitude of the quantity as compared to the canonical units. + """ + from sympy.physics.units import UnitSystem + unit_system = UnitSystem.get_default_unit_system() + return unit_system.get_quantity_scale_factor(self) + + def _eval_is_positive(self): + return True + + def _eval_is_constant(self): + return True + + def _eval_Abs(self): + return self + + def _eval_subs(self, old, new): + if isinstance(new, Quantity) and self != old: + return self + + def _latex(self, printer): + if self._latex_repr: + return self._latex_repr + else: + return r'\text{{{}}}'.format(self.args[1] \ + if len(self.args) >= 2 else self.args[0]) + + def convert_to(self, other, unit_system="SI"): + """ + Convert the quantity to another quantity of same dimensions. + + Examples + ======== + + >>> from sympy.physics.units import speed_of_light, meter, second + >>> speed_of_light + speed_of_light + >>> speed_of_light.convert_to(meter/second) + 299792458*meter/second + + >>> from sympy.physics.units import liter + >>> liter.convert_to(meter**3) + meter**3/1000 + """ + from .util import convert_to + return convert_to(self, other, unit_system) + + @property + def free_symbols(self): + """Return free symbols from quantity.""" + return set() + + @property + def is_prefixed(self): + """Whether or not the quantity is prefixed. Eg. `kilogram` is prefixed, but `gram` is not.""" + return self._is_prefixed + +class PhysicalConstant(Quantity): + """Represents a physical constant, eg. `speed_of_light` or `avogadro_constant`.""" + + is_physical_constant = True diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/systems/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/units/systems/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7c4f28d42eec86be8d679227f7b11ed7d48e61f1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/systems/__init__.py @@ -0,0 +1,6 @@ +from sympy.physics.units.systems.mks import MKS +from sympy.physics.units.systems.mksa import MKSA +from sympy.physics.units.systems.natural import natural +from sympy.physics.units.systems.si import SI + +__all__ = ['MKS', 'MKSA', 'natural', 'SI'] diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/systems/cgs.py b/venv/lib/python3.11/site-packages/sympy/physics/units/systems/cgs.py new file mode 100644 index 0000000000000000000000000000000000000000..1f5ee0b5454f1998672e1979ae4eaabe57a8edb4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/systems/cgs.py @@ -0,0 +1,82 @@ +from sympy.core.singleton import S +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.physics.units import UnitSystem, centimeter, gram, second, coulomb, charge, speed_of_light, current, mass, \ + length, voltage, magnetic_density, magnetic_flux +from sympy.physics.units.definitions import coulombs_constant +from sympy.physics.units.definitions.unit_definitions import statcoulomb, statampere, statvolt, volt, tesla, gauss, \ + weber, maxwell, debye, oersted, ohm, farad, henry, erg, ampere, coulomb_constant +from sympy.physics.units.systems.mks import dimsys_length_weight_time + +One = S.One + +dimsys_cgs = dimsys_length_weight_time.extend( + [], + new_dim_deps={ + # Dimensional dependencies for derived dimensions + "impedance": {"time": 1, "length": -1}, + "conductance": {"time": -1, "length": 1}, + "capacitance": {"length": 1}, + "inductance": {"time": 2, "length": -1}, + "charge": {"mass": S.Half, "length": S(3)/2, "time": -1}, + "current": {"mass": One/2, "length": 3*One/2, "time": -2}, + "voltage": {"length": -One/2, "mass": One/2, "time": -1}, + "magnetic_density": {"length": -One/2, "mass": One/2, "time": -1}, + "magnetic_flux": {"length": 3*One/2, "mass": One/2, "time": -1}, + } +) + +cgs_gauss = UnitSystem( + base_units=[centimeter, gram, second], + units=[], + name="cgs_gauss", + dimension_system=dimsys_cgs) + + +cgs_gauss.set_quantity_scale_factor(coulombs_constant, 1) + +cgs_gauss.set_quantity_dimension(statcoulomb, charge) +cgs_gauss.set_quantity_scale_factor(statcoulomb, centimeter**(S(3)/2)*gram**(S.Half)/second) + +cgs_gauss.set_quantity_dimension(coulomb, charge) + +cgs_gauss.set_quantity_dimension(statampere, current) +cgs_gauss.set_quantity_scale_factor(statampere, statcoulomb/second) + +cgs_gauss.set_quantity_dimension(statvolt, voltage) +cgs_gauss.set_quantity_scale_factor(statvolt, erg/statcoulomb) + +cgs_gauss.set_quantity_dimension(volt, voltage) + +cgs_gauss.set_quantity_dimension(gauss, magnetic_density) +cgs_gauss.set_quantity_scale_factor(gauss, sqrt(gram/centimeter)/second) + +cgs_gauss.set_quantity_dimension(tesla, magnetic_density) + +cgs_gauss.set_quantity_dimension(maxwell, magnetic_flux) +cgs_gauss.set_quantity_scale_factor(maxwell, sqrt(centimeter**3*gram)/second) + +# SI units expressed in CGS-gaussian units: +cgs_gauss.set_quantity_scale_factor(coulomb, 10*speed_of_light*statcoulomb) +cgs_gauss.set_quantity_scale_factor(ampere, 10*speed_of_light*statcoulomb/second) +cgs_gauss.set_quantity_scale_factor(volt, 10**6/speed_of_light*statvolt) +cgs_gauss.set_quantity_scale_factor(weber, 10**8*maxwell) +cgs_gauss.set_quantity_scale_factor(tesla, 10**4*gauss) +cgs_gauss.set_quantity_scale_factor(debye, One/10**18*statcoulomb*centimeter) +cgs_gauss.set_quantity_scale_factor(oersted, sqrt(gram/centimeter)/second) +cgs_gauss.set_quantity_scale_factor(ohm, 10**5/speed_of_light**2*second/centimeter) +cgs_gauss.set_quantity_scale_factor(farad, One/10**5*speed_of_light**2*centimeter) +cgs_gauss.set_quantity_scale_factor(henry, 10**5/speed_of_light**2/centimeter*second**2) + +# Coulomb's constant: +cgs_gauss.set_quantity_dimension(coulomb_constant, 1) +cgs_gauss.set_quantity_scale_factor(coulomb_constant, 1) + +__all__ = [ + 'ohm', 'tesla', 'maxwell', 'speed_of_light', 'volt', 'second', 'voltage', + 'debye', 'dimsys_length_weight_time', 'centimeter', 'coulomb_constant', + 'farad', 'sqrt', 'UnitSystem', 'current', 'charge', 'weber', 'gram', + 'statcoulomb', 'gauss', 'S', 'statvolt', 'oersted', 'statampere', + 'dimsys_cgs', 'coulomb', 'magnetic_density', 'magnetic_flux', 'One', + 'length', 'erg', 'mass', 'coulombs_constant', 'henry', 'ampere', + 'cgs_gauss', +] diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/systems/length_weight_time.py b/venv/lib/python3.11/site-packages/sympy/physics/units/systems/length_weight_time.py new file mode 100644 index 0000000000000000000000000000000000000000..dca4ded82afb8ff0e45f197e51c23850ca824737 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/systems/length_weight_time.py @@ -0,0 +1,156 @@ +from sympy.core.singleton import S + +from sympy.core.numbers import pi + +from sympy.physics.units import DimensionSystem, hertz, kilogram +from sympy.physics.units.definitions import ( + G, Hz, J, N, Pa, W, c, g, kg, m, s, meter, gram, second, newton, + joule, watt, pascal) +from sympy.physics.units.definitions.dimension_definitions import ( + acceleration, action, energy, force, frequency, momentum, + power, pressure, velocity, length, mass, time) +from sympy.physics.units.prefixes import PREFIXES, prefix_unit +from sympy.physics.units.prefixes import ( + kibi, mebi, gibi, tebi, pebi, exbi +) +from sympy.physics.units.definitions import ( + cd, K, coulomb, volt, ohm, siemens, farad, henry, tesla, weber, dioptre, + lux, katal, gray, becquerel, inch, hectare, liter, julian_year, + gravitational_constant, speed_of_light, elementary_charge, planck, hbar, + electronvolt, avogadro_number, avogadro_constant, boltzmann_constant, + stefan_boltzmann_constant, atomic_mass_constant, molar_gas_constant, + faraday_constant, josephson_constant, von_klitzing_constant, + acceleration_due_to_gravity, magnetic_constant, vacuum_permittivity, + vacuum_impedance, coulomb_constant, atmosphere, bar, pound, psi, mmHg, + milli_mass_unit, quart, lightyear, astronomical_unit, planck_mass, + planck_time, planck_temperature, planck_length, planck_charge, + planck_area, planck_volume, planck_momentum, planck_energy, planck_force, + planck_power, planck_density, planck_energy_density, planck_intensity, + planck_angular_frequency, planck_pressure, planck_current, planck_voltage, + planck_impedance, planck_acceleration, bit, byte, kibibyte, mebibyte, + gibibyte, tebibyte, pebibyte, exbibyte, curie, rutherford, radian, degree, + steradian, angular_mil, atomic_mass_unit, gee, kPa, ampere, u0, kelvin, + mol, mole, candela, electric_constant, boltzmann, angstrom +) + + +dimsys_length_weight_time = DimensionSystem([ + # Dimensional dependencies for MKS base dimensions + length, + mass, + time, +], dimensional_dependencies={ + # Dimensional dependencies for derived dimensions + "velocity": {"length": 1, "time": -1}, + "acceleration": {"length": 1, "time": -2}, + "momentum": {"mass": 1, "length": 1, "time": -1}, + "force": {"mass": 1, "length": 1, "time": -2}, + "energy": {"mass": 1, "length": 2, "time": -2}, + "power": {"length": 2, "mass": 1, "time": -3}, + "pressure": {"mass": 1, "length": -1, "time": -2}, + "frequency": {"time": -1}, + "action": {"length": 2, "mass": 1, "time": -1}, + "area": {"length": 2}, + "volume": {"length": 3}, +}) + + +One = S.One + + +# Base units: +dimsys_length_weight_time.set_quantity_dimension(meter, length) +dimsys_length_weight_time.set_quantity_scale_factor(meter, One) + +# gram; used to define its prefixed units +dimsys_length_weight_time.set_quantity_dimension(gram, mass) +dimsys_length_weight_time.set_quantity_scale_factor(gram, One) + +dimsys_length_weight_time.set_quantity_dimension(second, time) +dimsys_length_weight_time.set_quantity_scale_factor(second, One) + +# derived units + +dimsys_length_weight_time.set_quantity_dimension(newton, force) +dimsys_length_weight_time.set_quantity_scale_factor(newton, kilogram*meter/second**2) + +dimsys_length_weight_time.set_quantity_dimension(joule, energy) +dimsys_length_weight_time.set_quantity_scale_factor(joule, newton*meter) + +dimsys_length_weight_time.set_quantity_dimension(watt, power) +dimsys_length_weight_time.set_quantity_scale_factor(watt, joule/second) + +dimsys_length_weight_time.set_quantity_dimension(pascal, pressure) +dimsys_length_weight_time.set_quantity_scale_factor(pascal, newton/meter**2) + +dimsys_length_weight_time.set_quantity_dimension(hertz, frequency) +dimsys_length_weight_time.set_quantity_scale_factor(hertz, One) + +# Other derived units: + +dimsys_length_weight_time.set_quantity_dimension(dioptre, 1 / length) +dimsys_length_weight_time.set_quantity_scale_factor(dioptre, 1/meter) + +# Common volume and area units + +dimsys_length_weight_time.set_quantity_dimension(hectare, length**2) +dimsys_length_weight_time.set_quantity_scale_factor(hectare, (meter**2)*(10000)) + +dimsys_length_weight_time.set_quantity_dimension(liter, length**3) +dimsys_length_weight_time.set_quantity_scale_factor(liter, meter**3/1000) + + +# Newton constant +# REF: NIST SP 959 (June 2019) + +dimsys_length_weight_time.set_quantity_dimension(gravitational_constant, length ** 3 * mass ** -1 * time ** -2) +dimsys_length_weight_time.set_quantity_scale_factor(gravitational_constant, 6.67430e-11*m**3/(kg*s**2)) + +# speed of light + +dimsys_length_weight_time.set_quantity_dimension(speed_of_light, velocity) +dimsys_length_weight_time.set_quantity_scale_factor(speed_of_light, 299792458*meter/second) + + +# Planck constant +# REF: NIST SP 959 (June 2019) + +dimsys_length_weight_time.set_quantity_dimension(planck, action) +dimsys_length_weight_time.set_quantity_scale_factor(planck, 6.62607015e-34*joule*second) + +# Reduced Planck constant +# REF: NIST SP 959 (June 2019) + +dimsys_length_weight_time.set_quantity_dimension(hbar, action) +dimsys_length_weight_time.set_quantity_scale_factor(hbar, planck / (2 * pi)) + + +__all__ = [ + 'mmHg', 'atmosphere', 'newton', 'meter', 'vacuum_permittivity', 'pascal', + 'magnetic_constant', 'angular_mil', 'julian_year', 'weber', 'exbibyte', + 'liter', 'molar_gas_constant', 'faraday_constant', 'avogadro_constant', + 'planck_momentum', 'planck_density', 'gee', 'mol', 'bit', 'gray', 'kibi', + 'bar', 'curie', 'prefix_unit', 'PREFIXES', 'planck_time', 'gram', + 'candela', 'force', 'planck_intensity', 'energy', 'becquerel', + 'planck_acceleration', 'speed_of_light', 'dioptre', 'second', 'frequency', + 'Hz', 'power', 'lux', 'planck_current', 'momentum', 'tebibyte', + 'planck_power', 'degree', 'mebi', 'K', 'planck_volume', + 'quart', 'pressure', 'W', 'joule', 'boltzmann_constant', 'c', 'g', + 'planck_force', 'exbi', 's', 'watt', 'action', 'hbar', 'gibibyte', + 'DimensionSystem', 'cd', 'volt', 'planck_charge', 'angstrom', + 'dimsys_length_weight_time', 'pebi', 'vacuum_impedance', 'planck', + 'farad', 'gravitational_constant', 'u0', 'hertz', 'tesla', 'steradian', + 'josephson_constant', 'planck_area', 'stefan_boltzmann_constant', + 'astronomical_unit', 'J', 'N', 'planck_voltage', 'planck_energy', + 'atomic_mass_constant', 'rutherford', 'elementary_charge', 'Pa', + 'planck_mass', 'henry', 'planck_angular_frequency', 'ohm', 'pound', + 'planck_pressure', 'G', 'avogadro_number', 'psi', 'von_klitzing_constant', + 'planck_length', 'radian', 'mole', 'acceleration', + 'planck_energy_density', 'mebibyte', 'length', + 'acceleration_due_to_gravity', 'planck_temperature', 'tebi', 'inch', + 'electronvolt', 'coulomb_constant', 'kelvin', 'kPa', 'boltzmann', + 'milli_mass_unit', 'gibi', 'planck_impedance', 'electric_constant', 'kg', + 'coulomb', 'siemens', 'byte', 'atomic_mass_unit', 'm', 'kibibyte', + 'kilogram', 'lightyear', 'mass', 'time', 'pebibyte', 'velocity', + 'ampere', 'katal', +] diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/systems/mks.py b/venv/lib/python3.11/site-packages/sympy/physics/units/systems/mks.py new file mode 100644 index 0000000000000000000000000000000000000000..18cc4b1be5e2cbf5773845e48a0cb552fb750fae --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/systems/mks.py @@ -0,0 +1,46 @@ +""" +MKS unit system. + +MKS stands for "meter, kilogram, second". +""" + +from sympy.physics.units import UnitSystem +from sympy.physics.units.definitions import gravitational_constant, hertz, joule, newton, pascal, watt, speed_of_light, gram, kilogram, meter, second +from sympy.physics.units.definitions.dimension_definitions import ( + acceleration, action, energy, force, frequency, momentum, + power, pressure, velocity, length, mass, time) +from sympy.physics.units.prefixes import PREFIXES, prefix_unit +from sympy.physics.units.systems.length_weight_time import dimsys_length_weight_time + +dims = (velocity, acceleration, momentum, force, energy, power, pressure, + frequency, action) + +units = [meter, gram, second, joule, newton, watt, pascal, hertz] +all_units = [] + +# Prefixes of units like gram, joule, newton etc get added using `prefix_unit` +# in the for loop, but the actual units have to be added manually. +all_units.extend([gram, joule, newton, watt, pascal, hertz]) + +for u in units: + all_units.extend(prefix_unit(u, PREFIXES)) +all_units.extend([gravitational_constant, speed_of_light]) + +# unit system +MKS = UnitSystem(base_units=(meter, kilogram, second), units=all_units, name="MKS", dimension_system=dimsys_length_weight_time, derived_units={ + power: watt, + time: second, + pressure: pascal, + length: meter, + frequency: hertz, + mass: kilogram, + force: newton, + energy: joule, + velocity: meter/second, + acceleration: meter/(second**2), +}) + + +__all__ = [ + 'MKS', 'units', 'all_units', 'dims', +] diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/systems/mksa.py b/venv/lib/python3.11/site-packages/sympy/physics/units/systems/mksa.py new file mode 100644 index 0000000000000000000000000000000000000000..c18c0d6ae3801358d8828e2309d091cb9cb987d8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/systems/mksa.py @@ -0,0 +1,54 @@ +""" +MKS unit system. + +MKS stands for "meter, kilogram, second, ampere". +""" + +from __future__ import annotations + +from sympy.physics.units.definitions import Z0, ampere, coulomb, farad, henry, siemens, tesla, volt, weber, ohm +from sympy.physics.units.definitions.dimension_definitions import ( + capacitance, charge, conductance, current, impedance, inductance, + magnetic_density, magnetic_flux, voltage) +from sympy.physics.units.prefixes import PREFIXES, prefix_unit +from sympy.physics.units.systems.mks import MKS, dimsys_length_weight_time +from sympy.physics.units.quantities import Quantity + +dims = (voltage, impedance, conductance, current, capacitance, inductance, charge, + magnetic_density, magnetic_flux) + +units = [ampere, volt, ohm, siemens, farad, henry, coulomb, tesla, weber] + +all_units: list[Quantity] = [] +for u in units: + all_units.extend(prefix_unit(u, PREFIXES)) +all_units.extend(units) + +all_units.append(Z0) + +dimsys_MKSA = dimsys_length_weight_time.extend([ + # Dimensional dependencies for base dimensions (MKSA not in MKS) + current, +], new_dim_deps={ + # Dimensional dependencies for derived dimensions + "voltage": {"mass": 1, "length": 2, "current": -1, "time": -3}, + "impedance": {"mass": 1, "length": 2, "current": -2, "time": -3}, + "conductance": {"mass": -1, "length": -2, "current": 2, "time": 3}, + "capacitance": {"mass": -1, "length": -2, "current": 2, "time": 4}, + "inductance": {"mass": 1, "length": 2, "current": -2, "time": -2}, + "charge": {"current": 1, "time": 1}, + "magnetic_density": {"mass": 1, "current": -1, "time": -2}, + "magnetic_flux": {"length": 2, "mass": 1, "current": -1, "time": -2}, +}) + +MKSA = MKS.extend(base=(ampere,), units=all_units, name='MKSA', dimension_system=dimsys_MKSA, derived_units={ + magnetic_flux: weber, + impedance: ohm, + current: ampere, + voltage: volt, + inductance: henry, + conductance: siemens, + magnetic_density: tesla, + charge: coulomb, + capacitance: farad, +}) diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/systems/natural.py b/venv/lib/python3.11/site-packages/sympy/physics/units/systems/natural.py new file mode 100644 index 0000000000000000000000000000000000000000..13eb2c19e982438fab4b1422ddc5a25b16204be8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/systems/natural.py @@ -0,0 +1,27 @@ +""" +Naturalunit system. + +The natural system comes from "setting c = 1, hbar = 1". From the computer +point of view it means that we use velocity and action instead of length and +time. Moreover instead of mass we use energy. +""" + +from sympy.physics.units import DimensionSystem +from sympy.physics.units.definitions import c, eV, hbar +from sympy.physics.units.definitions.dimension_definitions import ( + action, energy, force, frequency, length, mass, momentum, + power, time, velocity) +from sympy.physics.units.prefixes import PREFIXES, prefix_unit +from sympy.physics.units.unitsystem import UnitSystem + + +# dimension system +_natural_dim = DimensionSystem( + base_dims=(action, energy, velocity), + derived_dims=(length, mass, time, momentum, force, power, frequency) +) + +units = prefix_unit(eV, PREFIXES) + +# unit system +natural = UnitSystem(base_units=(hbar, eV, c), units=units, name="Natural system") diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/systems/si.py b/venv/lib/python3.11/site-packages/sympy/physics/units/systems/si.py new file mode 100644 index 0000000000000000000000000000000000000000..2bfa7805871b8663c70b8af7da9ca1dc9b4afab3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/systems/si.py @@ -0,0 +1,377 @@ +""" +SI unit system. +Based on MKSA, which stands for "meter, kilogram, second, ampere". +Added kelvin, candela and mole. + +""" + +from __future__ import annotations + +from sympy.physics.units import DimensionSystem, Dimension, dHg0 + +from sympy.physics.units.quantities import Quantity + +from sympy.core.numbers import (Rational, pi) +from sympy.core.singleton import S +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.physics.units.definitions.dimension_definitions import ( + acceleration, action, current, impedance, length, mass, time, velocity, + amount_of_substance, temperature, information, frequency, force, pressure, + energy, power, charge, voltage, capacitance, conductance, magnetic_flux, + magnetic_density, inductance, luminous_intensity +) +from sympy.physics.units.definitions import ( + kilogram, newton, second, meter, gram, cd, K, joule, watt, pascal, hertz, + coulomb, volt, ohm, siemens, farad, henry, tesla, weber, dioptre, lux, + katal, gray, becquerel, inch, liter, julian_year, gravitational_constant, + speed_of_light, elementary_charge, planck, hbar, electronvolt, + avogadro_number, avogadro_constant, boltzmann_constant, electron_rest_mass, + stefan_boltzmann_constant, Da, atomic_mass_constant, molar_gas_constant, + faraday_constant, josephson_constant, von_klitzing_constant, + acceleration_due_to_gravity, magnetic_constant, vacuum_permittivity, + vacuum_impedance, coulomb_constant, atmosphere, bar, pound, psi, mmHg, + milli_mass_unit, quart, lightyear, astronomical_unit, planck_mass, + planck_time, planck_temperature, planck_length, planck_charge, planck_area, + planck_volume, planck_momentum, planck_energy, planck_force, planck_power, + planck_density, planck_energy_density, planck_intensity, + planck_angular_frequency, planck_pressure, planck_current, planck_voltage, + planck_impedance, planck_acceleration, bit, byte, kibibyte, mebibyte, + gibibyte, tebibyte, pebibyte, exbibyte, curie, rutherford, radian, degree, + steradian, angular_mil, atomic_mass_unit, gee, kPa, ampere, u0, c, kelvin, + mol, mole, candela, m, kg, s, electric_constant, G, boltzmann +) +from sympy.physics.units.prefixes import PREFIXES, prefix_unit +from sympy.physics.units.systems.mksa import MKSA, dimsys_MKSA + +derived_dims = (frequency, force, pressure, energy, power, charge, voltage, + capacitance, conductance, magnetic_flux, + magnetic_density, inductance, luminous_intensity) +base_dims = (amount_of_substance, luminous_intensity, temperature) + +units = [mol, cd, K, lux, hertz, newton, pascal, joule, watt, coulomb, volt, + farad, ohm, siemens, weber, tesla, henry, candela, lux, becquerel, + gray, katal] + +all_units: list[Quantity] = [] +for u in units: + all_units.extend(prefix_unit(u, PREFIXES)) + +all_units.extend(units) +all_units.extend([mol, cd, K, lux]) + + +dimsys_SI = dimsys_MKSA.extend( + [ + # Dimensional dependencies for other base dimensions: + temperature, + amount_of_substance, + luminous_intensity, + ]) + +dimsys_default = dimsys_SI.extend( + [information], +) + +SI = MKSA.extend(base=(mol, cd, K), units=all_units, name='SI', dimension_system=dimsys_SI, derived_units={ + power: watt, + magnetic_flux: weber, + time: second, + impedance: ohm, + pressure: pascal, + current: ampere, + voltage: volt, + length: meter, + frequency: hertz, + inductance: henry, + temperature: kelvin, + amount_of_substance: mole, + luminous_intensity: candela, + conductance: siemens, + mass: kilogram, + magnetic_density: tesla, + charge: coulomb, + force: newton, + capacitance: farad, + energy: joule, + velocity: meter/second, +}) + +One = S.One + +SI.set_quantity_dimension(radian, One) + +SI.set_quantity_scale_factor(ampere, One) + +SI.set_quantity_scale_factor(kelvin, One) + +SI.set_quantity_scale_factor(mole, One) + +SI.set_quantity_scale_factor(candela, One) + +# MKSA extension to MKS: derived units + +SI.set_quantity_scale_factor(coulomb, One) + +SI.set_quantity_scale_factor(volt, joule/coulomb) + +SI.set_quantity_scale_factor(ohm, volt/ampere) + +SI.set_quantity_scale_factor(siemens, ampere/volt) + +SI.set_quantity_scale_factor(farad, coulomb/volt) + +SI.set_quantity_scale_factor(henry, volt*second/ampere) + +SI.set_quantity_scale_factor(tesla, volt*second/meter**2) + +SI.set_quantity_scale_factor(weber, joule/ampere) + + +SI.set_quantity_dimension(lux, luminous_intensity / length ** 2) +SI.set_quantity_scale_factor(lux, steradian*candela/meter**2) + +# katal is the SI unit of catalytic activity + +SI.set_quantity_dimension(katal, amount_of_substance / time) +SI.set_quantity_scale_factor(katal, mol/second) + +# gray is the SI unit of absorbed dose + +SI.set_quantity_dimension(gray, energy / mass) +SI.set_quantity_scale_factor(gray, meter**2/second**2) + +# becquerel is the SI unit of radioactivity + +SI.set_quantity_dimension(becquerel, 1 / time) +SI.set_quantity_scale_factor(becquerel, 1/second) + +#### CONSTANTS #### + +# elementary charge +# REF: NIST SP 959 (June 2019) + +SI.set_quantity_dimension(elementary_charge, charge) +SI.set_quantity_scale_factor(elementary_charge, 1.602176634e-19*coulomb) + +# Electronvolt +# REF: NIST SP 959 (June 2019) + +SI.set_quantity_dimension(electronvolt, energy) +SI.set_quantity_scale_factor(electronvolt, 1.602176634e-19*joule) + +# Avogadro number +# REF: NIST SP 959 (June 2019) + +SI.set_quantity_dimension(avogadro_number, One) +SI.set_quantity_scale_factor(avogadro_number, 6.02214076e23) + +# Avogadro constant + +SI.set_quantity_dimension(avogadro_constant, amount_of_substance ** -1) +SI.set_quantity_scale_factor(avogadro_constant, avogadro_number / mol) + +# Boltzmann constant +# REF: NIST SP 959 (June 2019) + +SI.set_quantity_dimension(boltzmann_constant, energy / temperature) +SI.set_quantity_scale_factor(boltzmann_constant, 1.380649e-23*joule/kelvin) + +# Stefan-Boltzmann constant +# REF: NIST SP 959 (June 2019) + +SI.set_quantity_dimension(stefan_boltzmann_constant, energy * time ** -1 * length ** -2 * temperature ** -4) +SI.set_quantity_scale_factor(stefan_boltzmann_constant, pi**2 * boltzmann_constant**4 / (60 * hbar**3 * speed_of_light ** 2)) + +# Atomic mass +# REF: NIST SP 959 (June 2019) + +SI.set_quantity_dimension(atomic_mass_constant, mass) +SI.set_quantity_scale_factor(atomic_mass_constant, 1.66053906660e-24*gram) + +# Molar gas constant +# REF: NIST SP 959 (June 2019) + +SI.set_quantity_dimension(molar_gas_constant, energy / (temperature * amount_of_substance)) +SI.set_quantity_scale_factor(molar_gas_constant, boltzmann_constant * avogadro_constant) + +# Faraday constant + +SI.set_quantity_dimension(faraday_constant, charge / amount_of_substance) +SI.set_quantity_scale_factor(faraday_constant, elementary_charge * avogadro_constant) + +# Josephson constant + +SI.set_quantity_dimension(josephson_constant, frequency / voltage) +SI.set_quantity_scale_factor(josephson_constant, 0.5 * planck / elementary_charge) + +# Von Klitzing constant + +SI.set_quantity_dimension(von_klitzing_constant, voltage / current) +SI.set_quantity_scale_factor(von_klitzing_constant, hbar / elementary_charge ** 2) + +# Acceleration due to gravity (on the Earth surface) + +SI.set_quantity_dimension(acceleration_due_to_gravity, acceleration) +SI.set_quantity_scale_factor(acceleration_due_to_gravity, 9.80665*meter/second**2) + +# magnetic constant: + +SI.set_quantity_dimension(magnetic_constant, force / current ** 2) +SI.set_quantity_scale_factor(magnetic_constant, 4*pi/10**7 * newton/ampere**2) + +# electric constant: + +SI.set_quantity_dimension(vacuum_permittivity, capacitance / length) +SI.set_quantity_scale_factor(vacuum_permittivity, 1/(u0 * c**2)) + +# vacuum impedance: + +SI.set_quantity_dimension(vacuum_impedance, impedance) +SI.set_quantity_scale_factor(vacuum_impedance, u0 * c) + +# Electron rest mass +SI.set_quantity_dimension(electron_rest_mass, mass) +SI.set_quantity_scale_factor(electron_rest_mass, 9.1093837015e-31*kilogram) + +# Coulomb's constant: +SI.set_quantity_dimension(coulomb_constant, force * length ** 2 / charge ** 2) +SI.set_quantity_scale_factor(coulomb_constant, 1/(4*pi*vacuum_permittivity)) + +SI.set_quantity_dimension(psi, pressure) +SI.set_quantity_scale_factor(psi, pound * gee / inch ** 2) + +SI.set_quantity_dimension(mmHg, pressure) +SI.set_quantity_scale_factor(mmHg, dHg0 * acceleration_due_to_gravity * kilogram / meter**2) + +SI.set_quantity_dimension(milli_mass_unit, mass) +SI.set_quantity_scale_factor(milli_mass_unit, atomic_mass_unit/1000) + +SI.set_quantity_dimension(quart, length ** 3) +SI.set_quantity_scale_factor(quart, Rational(231, 4) * inch**3) + +# Other convenient units and magnitudes + +SI.set_quantity_dimension(lightyear, length) +SI.set_quantity_scale_factor(lightyear, speed_of_light*julian_year) + +SI.set_quantity_dimension(astronomical_unit, length) +SI.set_quantity_scale_factor(astronomical_unit, 149597870691*meter) + +# Fundamental Planck units: + +SI.set_quantity_dimension(planck_mass, mass) +SI.set_quantity_scale_factor(planck_mass, sqrt(hbar*speed_of_light/G)) + +SI.set_quantity_dimension(planck_time, time) +SI.set_quantity_scale_factor(planck_time, sqrt(hbar*G/speed_of_light**5)) + +SI.set_quantity_dimension(planck_temperature, temperature) +SI.set_quantity_scale_factor(planck_temperature, sqrt(hbar*speed_of_light**5/G/boltzmann**2)) + +SI.set_quantity_dimension(planck_length, length) +SI.set_quantity_scale_factor(planck_length, sqrt(hbar*G/speed_of_light**3)) + +SI.set_quantity_dimension(planck_charge, charge) +SI.set_quantity_scale_factor(planck_charge, sqrt(4*pi*electric_constant*hbar*speed_of_light)) + +# Derived Planck units: + +SI.set_quantity_dimension(planck_area, length ** 2) +SI.set_quantity_scale_factor(planck_area, planck_length**2) + +SI.set_quantity_dimension(planck_volume, length ** 3) +SI.set_quantity_scale_factor(planck_volume, planck_length**3) + +SI.set_quantity_dimension(planck_momentum, mass * velocity) +SI.set_quantity_scale_factor(planck_momentum, planck_mass * speed_of_light) + +SI.set_quantity_dimension(planck_energy, energy) +SI.set_quantity_scale_factor(planck_energy, planck_mass * speed_of_light**2) + +SI.set_quantity_dimension(planck_force, force) +SI.set_quantity_scale_factor(planck_force, planck_energy / planck_length) + +SI.set_quantity_dimension(planck_power, power) +SI.set_quantity_scale_factor(planck_power, planck_energy / planck_time) + +SI.set_quantity_dimension(planck_density, mass / length ** 3) +SI.set_quantity_scale_factor(planck_density, planck_mass / planck_length**3) + +SI.set_quantity_dimension(planck_energy_density, energy / length ** 3) +SI.set_quantity_scale_factor(planck_energy_density, planck_energy / planck_length**3) + +SI.set_quantity_dimension(planck_intensity, mass * time ** (-3)) +SI.set_quantity_scale_factor(planck_intensity, planck_energy_density * speed_of_light) + +SI.set_quantity_dimension(planck_angular_frequency, 1 / time) +SI.set_quantity_scale_factor(planck_angular_frequency, 1 / planck_time) + +SI.set_quantity_dimension(planck_pressure, pressure) +SI.set_quantity_scale_factor(planck_pressure, planck_force / planck_length**2) + +SI.set_quantity_dimension(planck_current, current) +SI.set_quantity_scale_factor(planck_current, planck_charge / planck_time) + +SI.set_quantity_dimension(planck_voltage, voltage) +SI.set_quantity_scale_factor(planck_voltage, planck_energy / planck_charge) + +SI.set_quantity_dimension(planck_impedance, impedance) +SI.set_quantity_scale_factor(planck_impedance, planck_voltage / planck_current) + +SI.set_quantity_dimension(planck_acceleration, acceleration) +SI.set_quantity_scale_factor(planck_acceleration, speed_of_light / planck_time) + +# Older units for radioactivity + +SI.set_quantity_dimension(curie, 1 / time) +SI.set_quantity_scale_factor(curie, 37000000000*becquerel) + +SI.set_quantity_dimension(rutherford, 1 / time) +SI.set_quantity_scale_factor(rutherford, 1000000*becquerel) + + +# check that scale factors are the right SI dimensions: +for _scale_factor, _dimension in zip( + SI._quantity_scale_factors.values(), + SI._quantity_dimension_map.values() +): + dimex = SI.get_dimensional_expr(_scale_factor) + if dimex != 1: + # XXX: equivalent_dims is an instance method taking two arguments in + # addition to self so this can not work: + if not DimensionSystem.equivalent_dims(_dimension, Dimension(dimex)): # type: ignore + raise ValueError("quantity value and dimension mismatch") +del _scale_factor, _dimension + +__all__ = [ + 'mmHg', 'atmosphere', 'inductance', 'newton', 'meter', + 'vacuum_permittivity', 'pascal', 'magnetic_constant', 'voltage', + 'angular_mil', 'luminous_intensity', 'all_units', + 'julian_year', 'weber', 'exbibyte', 'liter', + 'molar_gas_constant', 'faraday_constant', 'avogadro_constant', + 'lightyear', 'planck_density', 'gee', 'mol', 'bit', 'gray', + 'planck_momentum', 'bar', 'magnetic_density', 'prefix_unit', 'PREFIXES', + 'planck_time', 'dimex', 'gram', 'candela', 'force', 'planck_intensity', + 'energy', 'becquerel', 'planck_acceleration', 'speed_of_light', + 'conductance', 'frequency', 'coulomb_constant', 'degree', 'lux', 'planck', + 'current', 'planck_current', 'tebibyte', 'planck_power', 'MKSA', 'power', + 'K', 'planck_volume', 'quart', 'pressure', 'amount_of_substance', + 'joule', 'boltzmann_constant', 'Dimension', 'c', 'planck_force', 'length', + 'watt', 'action', 'hbar', 'gibibyte', 'DimensionSystem', 'cd', 'volt', + 'planck_charge', 'dioptre', 'vacuum_impedance', 'dimsys_default', 'farad', + 'charge', 'gravitational_constant', 'temperature', 'u0', 'hertz', + 'capacitance', 'tesla', 'steradian', 'planck_mass', 'josephson_constant', + 'planck_area', 'stefan_boltzmann_constant', 'base_dims', + 'astronomical_unit', 'radian', 'planck_voltage', 'impedance', + 'planck_energy', 'Da', 'atomic_mass_constant', 'rutherford', 'second', 'inch', + 'elementary_charge', 'SI', 'electronvolt', 'dimsys_SI', 'henry', + 'planck_angular_frequency', 'ohm', 'pound', 'planck_pressure', 'G', 'psi', + 'dHg0', 'von_klitzing_constant', 'planck_length', 'avogadro_number', + 'mole', 'acceleration', 'information', 'planck_energy_density', + 'mebibyte', 's', 'acceleration_due_to_gravity', 'electron_rest_mass', + 'planck_temperature', 'units', 'mass', 'dimsys_MKSA', 'kelvin', 'kPa', + 'boltzmann', 'milli_mass_unit', 'planck_impedance', 'electric_constant', + 'derived_dims', 'kg', 'coulomb', 'siemens', 'byte', 'magnetic_flux', + 'atomic_mass_unit', 'm', 'kibibyte', 'kilogram', 'One', 'curie', 'u', + 'time', 'pebibyte', 'velocity', 'ampere', 'katal', +] diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/tests/__init__.py b/venv/lib/python3.11/site-packages/sympy/physics/units/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/lib/python3.11/site-packages/sympy/physics/units/tests/test_dimensions.py b/venv/lib/python3.11/site-packages/sympy/physics/units/tests/test_dimensions.py new file mode 100644 index 0000000000000000000000000000000000000000..6455df41068a07c966c5f3e782e561fec4d16a97 --- /dev/null +++ b/venv/lib/python3.11/site-packages/sympy/physics/units/tests/test_dimensions.py @@ -0,0 +1,150 @@ +from sympy.physics.units.systems.si import dimsys_SI + +from sympy.core.numbers import pi +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (acos, atan2, cos) +from sympy.physics.units.dimensions import Dimension +from sympy.physics.units.definitions.dimension_definitions import ( + length, time, mass, force, pressure, angle +) +from sympy.physics.units import foot +from sympy.testing.pytest import raises + + +def test_Dimension_definition(): + assert dimsys_SI.get_dimensional_dependencies(length) == {length: 1} + assert length.name == Symbol("length") + assert length.symbol == Symbol("L") + + halflength = sqrt(length) + assert dimsys_SI.get_dimensional_dependencies(halflength) == {length: S.Half} + + +def test_Dimension_error_definition(): + # tuple with more or less than two entries + raises(TypeError, lambda: Dimension(("length", 1, 2))) + raises(TypeError, lambda: Dimension(["length"])) + + # non-number power + raises(TypeError, lambda: Dimension({"length": "a"})) + + # non-number with named argument + raises(TypeError, lambda: Dimension({"length": (1, 2)})) + + # symbol should by Symbol or str + raises(AssertionError, lambda: Dimension("length", symbol=1)) + + +def test_str(): + assert str(Dimension("length")) == "Dimension(length)" + assert str(Dimension("length", "L")) == "Dimension(length, L)" + + +def test_Dimension_properties(): + assert dimsys_SI.is_dimensionless(length) is False + assert dimsys_SI.is_dimensionless(length/length) is True + assert dimsys_SI.is_dimensionless(Dimension("undefined")) is False + + assert length.has_integer_powers(dimsys_SI) is True + assert (length**(-1)).has_integer_powers(dimsys_SI) is True + assert (length**1.5).has_integer_powers(dimsys_SI) is False + + +def test_Dimension_add_sub(): + assert length + length == length + assert length - length == length + assert -length == length + + raises(TypeError, lambda: length + foot) + raises(TypeError, lambda: foot + length) + raises(TypeError, lambda: length - foot) + raises(TypeError, lambda: foot - length) + + # issue 14547 - only raise error for dimensional args; allow + # others to pass + x = Symbol('x') + e = length + x + assert e == x + length and e.is_Add and set(e.args) == {length, x} + e = length + 1 + assert e == 1 + length == 1 - length and e.is_Add and set(e.args) == {length, 1} + + assert dimsys_SI.get_dimensional_dependencies(mass * length / time**2 + force) == \ + {length: 1, mass: 1, time: -2} + assert dimsys_SI.get_dimensional_dependencies(mass * length / time**2 + force - + pressure * length**2) == \ + {length: 1, mass: 1, time: -2} + + raises(TypeError, lambda: dimsys_SI.get_dimensional_dependencies(mass * length / time**2 + pressure)) + +def test_Dimension_mul_div_exp(): + assert 2*length == length*2 == length/2 == length + assert 2/length == 1/length + x = Symbol('x') + m = x*length + assert m == length*x and m.is_Mul and set(m.args) == {x, length} + d = x/length + assert d == x*length**-1 and d.is_Mul and set(d.args) == {x, 1/length} + d = length/x + assert d == length*x**-1 and d.is_Mul and set(d.args) == {1/x, length} + + velo = length / time + + assert (length * length) == length ** 2 + + assert dimsys_SI.get_dimensional_dependencies(length * length) == {length: 2} + assert dimsys_SI.get_dimensional_dependencies(length ** 2) == {length: 2} + assert dimsys_SI.get_dimensional_dependencies(length * time) == {length: 1, time: 1} + assert dimsys_SI.get_dimensional_dependencies(velo) == {length: 1, time: -1} + assert dimsys_SI.get_dimensional_dependencies(velo ** 2) == {length: 2, time: -2} + + assert dimsys_SI.get_dimensional_dependencies(length / length) == {} + assert dimsys_SI.get_dimensional_dependencies(velo / length * time) == {} + assert dimsys_SI.get_dimensional_dependencies(length ** -1) == {length: -1} + assert dimsys_SI.get_dimensional_dependencies(velo ** -1.5) == {length: -1.5, time: 1.5} + + length_a = length**"a" + assert dimsys_SI.get_dimensional_dependencies(length_a) == {length: Symbol("a")} + + assert dimsys_SI.get_dimensional_dependencies(length**pi) == {length: pi} + assert dimsys_SI.get_dimensional_dependencies(length**(length/length)) == {length: Dimension(1)} + + raises(TypeError, lambda: dimsys_SI.get_dimensional_dependencies(length**length)) + + assert length != 1 + assert length / length != 1 + + length_0 = length ** 0 + assert dimsys_SI.get_dimensional_dependencies(length_0) == {} + + # issue 18738 + a = Symbol('a') + b = Symbol('b') + c = sqrt(a**2 + b**2) + c_dim = c.subs({a: length, b: length}) + assert dimsys_SI.equivalent_dims(c_dim, length) + +def test_Dimension_functions(): + raises(TypeError, lambda: dimsys_SI.get_dimensional_dependencies(cos(length))) + raises(TypeError, lambda: dimsys_SI.get_dimensional_dependencies(acos(angle))) + raises(TypeError, lambda: dimsys_SI.get_dimensional_dependencies(atan2(length, time))) + raises(TypeError, lambda: dimsys_SI.get_dimensional_dependencies(log(length))) + raises(TypeError, lambda: dimsys_SI.get_dimensional_dependencies(log(100, length))) + raises(TypeError, lambda: dimsys_SI.get_dimensional_dependencies(log(length, 10))) + + assert dimsys_SI.get_dimensional_dependencies(pi) == {} + + assert dimsys_SI.get_dimensional_dependencies(cos(1)) == {} + assert dimsys_SI.get_dimensional_dependencies(cos(angle)) == {} + + assert dimsys_SI.get_dimensional_dependencies(atan2(length, length)) == {} + + assert dimsys_SI.get_dimensional_dependencies(log(length / length, length / length)) == {} + + assert dimsys_SI.get_dimensional_dependencies(Abs(length)) == {length: 1} + assert dimsys_SI.get_dimensional_dependencies(Abs(length / length)) == {} + + assert dimsys_SI.get_dimensional_dependencies(sqrt(-1)) == {}